Skip to content

maintainer-standup persist fails when synthesize emits duplicate ARCHON_STATE_JSON_BEGIN/END blocks #1674

@Wirasm

Description

@Wirasm

Summary

  • What broke: maintainer-standup-persist.ts fails to extract the state JSON when the synthesize node emits more than one ARCHON_STATE_JSON_BEGIN block (e.g. a truncated/aborted block followed by a complete one). The script uses indexOf(BEGIN) and indexOf(END), which pairs BEGIN[0] with END[0] and slices across the embedded second BEGIN marker — yielding invalid JSON. The bash node exits 1 and the workflow is reported as failed, even though a usable brief and state were produced.
  • When it started (if known): pre-existing — surfaces whenever the synthesize node (Claude Sonnet, for maintainer-standup) restarts its output mid-emission. Observed today (2026-05-14) during a normal standup run after the synth node ran ~20 minutes.
  • Severity: major

Steps to Reproduce

  1. Run bun run cli workflow run maintainer-standup. Wait for the synthesize node to finish.
  2. If Claude emits the state block twice (e.g. partial + restart), the persist node fails with Bash node 'persist' failed [exit 1].
  3. The brief and state files are NOT written. The workflow exits non-zero.

Today's reproducer: synth output had two ARCHON_STATE_JSON_BEGIN lines and one ARCHON_STATE_JSON_END. The slice between BEGIN[0] and END[0] contained [truncated state JSON]\n\nARCHON_STATE_JSON_BEGIN\n[complete state JSON], which JSON.parse can't accept. Tier-2 (JSON-wrapper fallback) also fails because it tries to parse starting at the first {, which is inside the truncated copy.

Expected vs Actual

  • Expected: When the synthesize output contains the markers at least once correctly, persist should extract the last complete BEGIN..END block (treat earlier emissions as preamble) and write the brief and state.
  • Actual: persist exits 1, no files are written, and the user must hand-extract the brief and state from the run log.

User Flow

User                       Archon synthesize (Claude)    persist (bash)
────                       ──────────────────────────    ───────────────
runs maintainer-standup ─▶ emits partial state ──┐
                            then restarts:       │  ──▶ reads stdin
                            emits complete state │       indexOf(BEGIN) → first (partial)
                                                 │       indexOf(END)   → only one
                                                 ▼       slice spans both blocks
                                                         JSON.parse fails
                                                         [X] tier-2 also fails on truncated leading {
                                                         exits 1
sees "Workflow failed"    ◀── persist failed; no brief, no state.json written

Environment

  • Platform: CLI (also reproducible from any caller — the bug is in the persist script)
  • Database: SQLite
  • Running in worktree? No (worktree.enabled: false)
  • OS: macOS / Linux

Logs

{"level":40,"module":"workflow.dag-executor","exitCode":1,"killed":false,
 "stderrTail":"...truncated state JSON...ARCHON_STATE_JSON_END\\n--- END raw output ---",
 "nodeId":"persist","nodeType":"bash","isTimeout":false,"msg":"dag_node_failed"}
[persist] Failed: Bash node 'persist' failed [exit 1]: ...

Raw synth output (markers only, line numbers from one observed run):

59:  # Maintainer Standup — 2026-05-14
241: ARCHON_STATE_JSON_BEGIN     ← partial / aborted state JSON below
244: ARCHON_STATE_JSON_BEGIN     ← restart; complete state JSON below
246: ARCHON_STATE_JSON_END

Impact

  • Affected workflows/commands: maintainer-standup, maintainer-standup-minimax (same persist script). Any future workflow that uses delimiter-based JSON extraction with indexOf is vulnerable to the same pattern.
  • Reproduction rate: Intermittent — depends on whether the synth model restarts mid-emission. Observed once today. Cost is high when it does happen (~20-minute synth call is wasted because persist throws away the output).
  • Workaround: Hand-extract brief markdown and state JSON from the run log and write them manually to .archon/maintainer-standup/. Tedious but possible because the data IS in the log.
  • Data loss risk: No (raw run output is preserved on disk).

Scope

  • Package(s) likely involved: workflow-script (project-level, .archon/scripts/maintainer-standup-persist.ts)
  • Not in @archon/workflows itself — fix is in the project script, not the engine.

Proposed fix

Switch the tier-1 extractor from indexOf to a paired-marker scan that returns the last valid BEGIN..END block. Pseudocode:

// Find the last END, then the last BEGIN before it.
const endIdx = raw.lastIndexOf(END);
if (endIdx !== -1) {
  const beginIdx = raw.lastIndexOf(BEGIN, endIdx);
  if (beginIdx !== -1 && beginIdx < endIdx) {
    const stateText = raw.slice(beginIdx + BEGIN.length, endIdx).trim();
    state = JSON.parse(stateText);
    brief = raw.slice(0, beginIdx).trim();
    source = 'delimiter';
  }
}

This makes the script robust to:

  • Multiple BEGIN emissions before the END (today's failure mode — model restarted).
  • A trailing BEGIN after END that the model forgot to close (the last-END-then-look-back rule still picks the correct pair).

The tier-2 JSON-wrapper fallback can stay as-is, but should probably scan for the last { followed by valid JSON, not the first, for the same reason.

Optional follow-up: surface a warning when more than one BEGIN was seen so we know when the model is misbehaving, even though we recovered.

Related

The synthesize-node behavior of duplicating output mid-emission is its own concern, but fixing the parser is the right first move — the persist script's contract is "tolerate model output that contains the markers somewhere," and indexOf violates that contract whenever there's more than one BEGIN.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions