Skip to content

Claude JSONL transcript can disconnect while proxy/semantic stream stays live, breaking resume identity #290

Open

Description

@Juliusolsson05

Summary

Claude panes can keep working through the proxy/semantic/screen channels while Agent Code receives zero committed JSONL transcript entries. In that state the UI can still show live progress, ghosts, semantic events, and screen-rendered prompts, but the durable transcript path is absent or disconnected. When the pane is later reloaded/resumed/reopened, Agent Code has no reliable provider transcript identity and can fall back to old history, blank history, or a wrong/stale providerSessionId.

This is the long-running class of bug that originally forced the ghost-record fallback work: the live channel and committed transcript channel can diverge badly enough that the app appears alive while durable resume state is already broken.

Severity

High. This can lose apparent progress after resume/reload, corrupt the user's mental model of what is saved, and amplify memory/rendering failures because the renderer leans on semantic/ghost state for live display while JSONL never catches up.

Concrete evidence from 2026-06-17 debug artifacts

Debug bundle:

/Users/juliusolsson/.config/agent-code/debug-bundles/manual/2026-06-17T11-50-56-053-a6c70f19

Bundle facts:

  • Agent Code session id: a6c70f19-e093-4d91-8339-3a7c79abb79e
  • Provider kind: claude
  • Proxy run dir:
    /Users/juliusolsson/.config/agent-code/proxy/Users-juliusolsson-Desktop-general-hackathon-bot/shell-a6c70f19-e093-4d91-8339-3a7c79abb79e/2026-06-17T11-43-53-031Z
  • state-snapshot.json reports:
    • totalEntries: 0
    • lastJsonlEntryAt: null
    • transcriptStatus: "ready"
    • transcriptError: null
    • _counts.entries: 0
    • _counts.ghosts: 11
    • _counts.semanticLog: 200
    • streamPhase: "awaiting-tool"
    • pending tool: AskUserQuestion
  • feed-debug.jsonl contains active live/rendering events despite zero JSONL entries:
    • screen_update: 63
    • visible_rows: 13
    • submit: 3
    • flow_selected: 10
    • stream_phase: 36
    • turn_started: 8
    • text_delta: 49
    • tool_input_delta: 48
    • turn_completed: 8
    • ghost_orphan_sweep: 11

The proxy events for that run carry Claude's upstream session header:

x-claude-code-session-id: 184505cc-570f-4d45-8015-2145e839217c

But no matching root transcript file was found under ~/.claude/projects:

184505cc-570f-4d45-8015-2145e839217c.jsonl

That makes this more severe than a renderer-only stale state bug: Claude/proxy had a provider session id, Agent Code had live semantic traffic, but the committed transcript path Agent Code depends on was missing or not discoverable.

Additional observed failed runs

Later restarted run:

/Users/juliusolsson/.config/agent-code/proxy/Users-juliusolsson-Desktop-general-hackathon-bot/shell-6c04bc5d-8a8b-4ca4-9623-aad1c1ac8072/2026-06-17T11-52-57-121Z
  • Proxy events file was around 66MB.
  • Proxy events contained provider id:
    7f09da4d-8651-4e75-bf69-233df36d6d38
  • ~/.claude/projects/-Users-juliusolsson-Desktop-general-hackathon-bot/7f09da4d-8651-4e75-bf69-233df36d6d38/subagents existed.
  • That directory contained agent-*.meta.json files, but no root 7f09da4d-8651-4e75-bf69-233df36d6d38.jsonl was found during inspection.

Another later run had provider id:

7dcaab9f-0b5d-45df-bcfc-2db1b7cd5138

No matching root JSONL was found under ~/.claude/projects during the same inspection.

Important nuance: ~/.claude/projects does contain many normal .jsonl transcripts overall, so this is not simply "Claude no longer writes flat JSONL anywhere". It appears workspace/session/path/version specific.

Current workspace identity corruption is also present

A read-only scan of the current persisted workspace found:

/Users/juliusolsson/.config/agent-code/workspace.json
sessions: 75
provider ids: 62
duplicate providerSessionId groups: 8
agent records missing providerSessionId: 5

Examples of duplicate durable provider ids:

c0d44dcb-3972-4c55-9a31-a242bb482e2a -> 681d3f85-8183-4f06-8997-44aec6de27e6:claude | aa4dd3dd-982a-4c17-b4be-a7ca872728ae:codex
1a373e7d-7a66-4714-ac1e-497c849538b7 -> d4d53e1e-0354-49eb-b030-935fd9616a19:claude | 5c087ae1-1e13-4031-873a-b3f65a65f184:claude
cef609a0-8d65-4aee-8e7c-5c8c70e70745 -> 8e826a8c-135f-4655-8c10-d12d7463e16e:claude | 70417358-183a-4adb-b533-199699263806:claude
c6e490f9-09de-499e-85a5-1fe8f88fb299 -> e1815951-d8a1-4c19-ab06-7a32e0c9a7df:claude | 06f7e0d0-fc99-4f2d-8344-4149a1803af1:claude
331a16cb-3b74-41ad-85af-174433fcafd8 -> 92276c52-abe6-462f-991c-3847ca3f2c2d:claude | 473c7bcb-5eaa-45f1-8e10-7c9eaf50b955:claude
871a17a0-fa6f-4335-8206-62175b99e251 -> 5337ca6a-a110-4369-8d82-4521514f8fb0:claude | 448a18da-94a3-4dd8-b5c2-219087d350e9:claude

Examples of agent records with no durable provider id:

fcb3bedf-58ac-4721-8b2f-7ce36eff6ed2 | claude | /Users/juliusolsson/Desktop/general/hackathon-bot
847f4909-32f0-4cd7-a6ea-18a908ae2594 | claude | /Users/juliusolsson/Desktop/general

This does not prove the JSONL disappearance cause, but it proves the resume identity layer is already in a state where wrong-history or old-history behavior is plausible.

Relevant implementation facts

  • Fresh Claude panes do not persist providerSessionId at spawn time.
  • For fresh Claude, the renderer currently captures providerSessionId only from committed JSONL entries in session:jsonl-entries.
  • If committed JSONL never arrives, the pane can stay live through proxy/semantic/screen channels but never gets a durable provider id in workspace.json.
  • historyLoader.ts resolves Claude history as:
~/.claude/projects/<sanitized-realpath-cwd>/<providerSessionId>.jsonl
  • If that root file is missing, history load returns empty, while ghost/semantic state may still make the live pane look productive.
  • Soft reload is available for Claude/Codex even when providerSessionId is missing. In that state it resets renderer-derived state but cannot reload committed history, which can turn an already-disconnected pane into an apparent progress loss.
  • src/main/sessions/forwarder.ts starts subagent observation from parent jsonl-entry events. If the root JSONL path never emits, subagent live data may exist on disk/proxy but not enter the normal committed/subagent pipeline.

Related issues and PRs

Related open issues:

Related merged PRs / prior mitigations:

Working hypotheses to investigate, not conclusions

  1. Claude Code may sometimes create/advertise a session id over the API/proxy before or without creating the expected root JSONL transcript file.
  2. Claude Code may now write some sessions under a different path/layout for certain workspaces, versions, or subagent-heavy runs. The presence of session-id/subagents/*.meta.json without the root session-id.jsonl is suspicious.
  3. Agent Code may miss a freshly-created root JSONL file if the file appears before the fresh-session tailer snapshots/watches the project directory. A local hardening patch now treats files with mtime/ctime >= spawnStart as candidates, but this only covers a narrow race and does not explain runs where the file is absent.
  4. Agent Code's durable identity model is too dependent on committed JSONL. The proxy header has the provider session id earlier than JSONL; that id should probably be captured as provisional durable identity, then reconciled with JSONL rather than ignored until JSONL arrives.
  5. Duplicate providerSessionId rows in workspace.json can cause wrong-history resume/preview behavior even after JSONL begins working again. Some duplicates are intentional clone/rewind/provider-switch products, but reload/rehydrate also treat the field as a unique resume identity, so this needs a stricter invariant or explicit lineage model.
  6. Soft reload should not destructively clear a pane with no provider id unless it can preserve/replay semantic/ghost state or tell the user that no committed transcript exists.
  7. Heap pressure is likely a multiplier, not necessarily the root cause: large proxy events, retained tool outputs, ghost/semantic fallbacks, and subagent watcher retention can make a disconnected transcript state much worse.

Suggested investigation plan

  • Build a read-only diagnostic that correlates, per live/debug session:
    • Agent Code sessionId
    • Claude proxy x-claude-code-session-id
    • expected project dir from realpath(cwd) + sanitizer
    • root JSONL existence/mtime/size
    • nested session dir existence
    • subagent meta/body existence
    • renderer providerSessionId
    • workspace metadata owners and duplicates
  • Add a debug bundle section that records the expected Claude transcript path and whether it existed at capture time.
  • Add main-process telemetry for fresh Claude sessions:
    • when proxy first observes x-claude-code-session-id
    • when tailer attaches to a file
    • when first JSONL entry arrives
    • when no JSONL appears after N seconds despite semantic/proxy activity
  • Review Claude Code 2.1.179/2.1.181 storage behavior against our accepted support matrix (chore(upstream): Claude Code 2.1.301 is newer than accepted 2.1.143 #233). Confirm whether transcript file layout or flush timing changed upstream.
  • Audit workspace persistence for duplicate providerSessionId semantics. Decide whether duplicates are illegal, legal only with explicit clone lineage, or legal but must never participate in resume lookup without disambiguation.
  • Guard or redesign soft reload when providerSessionId is absent.
  • Add a failing regression harness with a fake Claude/proxy session that emits semantic/proxy events but no root JSONL, then verify the UI surfaces "live but not durable" instead of silently claiming transcript is ready.

Acceptance criteria

  • A fresh Claude pane that has live proxy/semantic traffic but no JSONL transcript visibly reports that the committed transcript is disconnected/not durable.
  • Agent Code captures the provider session id from the earliest trustworthy source and persists it with provenance, without pretending history is reloadable until the transcript path is confirmed.
  • Rehydrate/reload cannot silently resume a different/old transcript because of duplicate or stale providerSessionId metadata.
  • Soft reload cannot erase the only visible progress for a pane that has no committed transcript path.
  • Debug bundles contain enough path/session correlation to determine whether Claude did not write JSONL, wrote it somewhere else, or Agent Code failed to attach.
  • Heap behavior remains bounded when JSONL is absent and semantic/ghost/proxy streams are large.

Activity

  1. Juliusolsson05 commented on Jun 18, 2026

    @Juliusolsson05
    OwnerAuthor

    Investigation update: local forensic evidence from 2026-06-17/18

    This comment consolidates read-only local artifact inspection. It intentionally avoids dumping request bodies or body_b64 payloads.

    Primary debug bundle

    Inspected bundle:

    /Users/juliusolsson/.config/agent-code/debug-bundles/manual/2026-06-17T11-50-56-053-a6c70f19
    

    Manifest and runtime state show a live Claude pane with no committed transcript:

    Agent Code session id: a6c70f19-e093-4d91-8339-3a7c79abb79e
    kind: claude
    proxyRunDir: /Users/juliusolsson/.config/agent-code/proxy/Users-juliusolsson-Desktop-general-hackathon-bot/shell-a6c70f19-e093-4d91-8339-3a7c79abb79e/2026-06-17T11-43-53-031Z
    proxy-session-meta cwd: /Users/juliusolsson/Desktop/general/hackathon-bot
    

    state-snapshot.json:

    totalEntries: 0
    lastJsonlEntryAt: null
    transcriptStatus: "ready"
    transcriptError: null
    _counts.entries: 0
    _counts.ghosts: 11
    _counts.semanticLog: 200
    streamPhase: "awaiting-tool"
    streamPhasePendingToolName: "AskUserQuestion"
    

    feed-debug.jsonl proves live activity despite zero committed transcript entries:

    381 rows from 2026-06-17T11:43:55.028Z to 2026-06-17T11:49:46.799Z
    screen_update: 63
    text_delta: 49
    tool_input_delta: 48
    stream_phase: 36
    turn_started: 8
    turn_completed: 8
    ghost_orphan_sweep: 11
    

    render-diagnostics.json also agrees with the disconnected state:

    entries: 0
    committedAssistantRows: 0
    semanticHistory: 7
    

    Proxy header evidence from proxy-events.jsonl:

    x-claude-code-session-id: 184505cc-570f-4d45-8015-2145e839217c
    

    The expected root transcript path was absent:

    /Users/juliusolsson/.claude/projects/-Users-juliusolsson-Desktop-general-hackathon-bot/184505cc-570f-4d45-8015-2145e839217c.jsonl
    

    find /Users/juliusolsson/.claude/projects -path '*184505cc-570f-4d45-8015-2145e839217c*' -print returned no matches.

    Neighboring runs show the same pattern

    ~/.config/agent-code/proxy/Users-juliusolsson-Desktop-general-hackathon-bot/shell-6c04bc5d-8a8b-4ca4-9623-aad1c1ac8072/2026-06-17T11-52-57-121Z/proxy-events.jsonl:

    size: 66,036,597 bytes
    lines: 12,353
    proxy-observed provider id: 7f09da4d-8651-4e75-bf69-233df36d6d38
    header appearances: 303
    expected root JSONL: missing
    nested dir exists: ~/.claude/projects/-Users-juliusolsson-Desktop-general-hackathon-bot/7f09da4d-8651-4e75-bf69-233df36d6d38/subagents
    subagents content: 9 agent-*.meta.json files, keys only agentType/description/toolUseId
    

    ~/.config/agent-code/proxy/Users-juliusolsson-Desktop-general-hackathon-bot/shell-97a5cb95.../2026-06-17T12-28-41-681Z/proxy-events.jsonl:

    size: 927,175 bytes
    lines: 329
    proxy-observed provider id: 7dcaab9f-0b5d-45df-bcfc-2db1b7cd5138
    expected root JSONL: missing
    

    Current/recent run:

    shell-fcb3bedf.../2026-06-18T10-54-13-276Z/proxy-events.jsonl
    size: 66,748 bytes
    lines: 54
    proxy-observed provider id: 43be9317-6ed9-43d3-a243-a8d788fe3380
    expected root JSONL: missing
    workspace session fcb3bedf-58ac-4721-8b2f-7ce36eff6ed2 has no providerSessionId
    

    Expected missing root paths checked:

    ~/.claude/projects/-Users-juliusolsson-Desktop-general-hackathon-bot/184505cc-570f-4d45-8015-2145e839217c.jsonl
    ~/.claude/projects/-Users-juliusolsson-Desktop-general-hackathon-bot/7f09da4d-8651-4e75-bf69-233df36d6d38.jsonl
    ~/.claude/projects/-Users-juliusolsson-Desktop-general-hackathon-bot/7dcaab9f-0b5d-45df-bcfc-2db1b7cd5138.jsonl
    ~/.claude/projects/-Users-juliusolsson-Desktop-general-hackathon-bot/43be9317-6ed9-43d3-a243-a8d788fe3380.jsonl
    

    All were missing during inspection.

    Workspace metadata state

    Read-only scan of current workspace:

    /Users/juliusolsson/.config/agent-code/workspace.json
    sessions: 75
    provider ids present: 70
    missing providerSessionId: 5
    duplicate providerSessionId groups: 8
    

    Missing provider ids include live/relevant Claude records:

    fcb3bedf-58ac-4721-8b2f-7ce36eff6ed2  claude  /Users/juliusolsson/Desktop/general/hackathon-bot
    847f4909-32f0-4cd7-a6ea-18a908ae2594  claude  /Users/juliusolsson/Desktop/general
    

    Memory correlation

    From the primary bundle performance/metrics-tail.jsonl:

    main memory samples: 295
    min RSS: 632,274,944; heapUsed: 817,839,180 at 2026-06-17T11:28:07.206Z
    max RSS: 2,824,880,128; heapUsed: 2,842,064,092 at 2026-06-17T11:45:27.921Z
    last sample: RSS 1,500,708,864; heapUsed 1,891,894,044 at 2026-06-17T11:50:53.216Z
    renderer heap max: ~97,400,000
    

    This supports the #288 relationship: heap pressure is likely an amplifier or parallel failure in main-process retained strings/proxy/transcript/subagent data, not a renderer-only problem.

    Forensic conclusion

    The local evidence now spans multiple runs. Agent Code receives proxy/semantic/screen activity and sees Claude provider session headers, but the expected root Claude JSONL is missing. At least one run also has a nested session directory with subagents/ but no root <providerSessionId>.jsonl, which matches upstream Claude data-loss reports closely.

  2. Juliusolsson05 commented on Jun 18, 2026

    @Juliusolsson05
    OwnerAuthor

    Investigation update: upstream Claude Code correlation

    Anthropic's public anthropics/claude-code issue tracker has several data-loss reports that are materially similar to our observed failure. This does not absolve Agent Code; we still need better detection, persistence, and UI safeguards. But it moves “Claude Code sometimes does not create or later deletes the root transcript JSONL” from speculative to strongly plausible.

    Closest upstream match

    anthropics/claude-code#51504

    Title: Session conversation .jsonl files not written when using parallel subagents — history permanently lost

    Key overlap with our local evidence:

    • Sessions ran and left populated subagents/ directories.
    • No sibling top-level <session-id>.jsonl existed.
    • Reporter specifically distinguishes this from index corruption: the JSONL was missing, not merely hidden from UI.
    • Repro involved large numbers of parallel subagents.
    • The reported on-disk shape matches our 7f09da4d-8651-4e75-bf69-233df36d6d38/ case:
    <project>/<session-id>/subagents/ exists
    <project>/<session-id>.jsonl missing
    

    Upstream deletion/lifecycle reports

    anthropics/claude-code#60368

    Title: [BUG] Background-fleet "deleteJob" silently unlinks main session JSONL despite cleanupPeriodDays: 36500

    Relevant overlap:

    • Reports orphaned session directories where the sibling root .jsonl is deleted but subagents/, tool-results/, etc. are preserved.
    • Claims deletion can happen through a background/fleet lifecycle path independent of retention cleanup.
    • This matters for Agent Code because a transcript may have existed when Claude started, then disappear before our later resume/history load.

    anthropics/claude-code#59248

    Title: Silent retention cleanup deletes session transcripts with no warning, opt-in, or recovery

    Relevant overlap:

    • Reports parent .jsonl deletion with surviving subagents/ directories.
    • Describes cleanup/audit opacity and mismatch between expected retention and observed transcript loss.
    • This is another reason our debug bundle must record expected transcript path existence/mtime at capture time, not just rely on renderer state.

    Upstream embedded-terminal/transcript-write regression

    anthropics/claude-code#67603

    Title: v2.1.173 regression: TUI writes no transcript records when CLAUDE_CODE_CHILD_SESSION is inherited from a parent claude session — silent data loss, --resume broken

    Relevant overlap:

    • Reports interactive TUI sessions under embedded terminal hosts completing normally while conversation records are not written to the transcript.
    • Side artifacts still update, but root conversation records are absent or stub-only.
    • Although the report is Windows/ConPTY-specific, the failure class is exactly what Agent Code must handle: provider appears alive, tools/hooks/side artifacts work, resume transcript is not durable.

    Related upstream reports worth tracking

    Searches also surfaced these relevant issues:

    What this changes for our investigation

    The leading model should now be split:

    1. Upstream persistence failure/deletion is plausible and may be the root cause for missing root JSONL. Agent Code cannot force Claude to write a file that Claude never creates or later deletes.
    2. Agent Code still has a design bug: live proxy/semantic channels can continue while committed transcript durability is absent, but we mark transcriptStatus: "ready" and fail to preserve the proxy-observed provider id.
    3. Agent Code still needs defensive UX: a pane should show “live but not durable / transcript disconnected” rather than silently relying on ghosts/semantic state.
    4. Agent Code still needs identity safeguards: duplicate or missing providerSessionId in workspace.json can turn upstream transcript loss into wrong-history resume behavior.

    Immediate implications

    • Treat the proxy header x-claude-code-session-id as an early provider identity signal, with provenance proxy-header.
    • Do not mark that identity as history-loadable until the expected root JSONL exists or an alternate Claude-supported layout is confirmed.
    • Add watchdog telemetry: semantic/proxy activity with lastJsonlEntryAt === null after N seconds should become an explicit disconnected transcript state.
    • Debug bundles must include expected Claude transcript path, existence, size, mtime, nested session dir existence, and subagent dir summary.
  3. Juliusolsson05 commented on Jun 18, 2026

    @Juliusolsson05
    OwnerAuthor

    Investigation update: code-path findings for transcript discovery, proxy identity, and missing-file observability

    This folds in the transcript/tailer and proxy-channel research tracks.

    Claude transcript path assumptions

    Agent Code assumes Claude transcripts live at:

    ($CLAUDE_CONFIG_DIR ?? ~/.claude)/projects/<sanitized-realpath-cwd>/<sessionId>.jsonl
    

    Important code locations:

    • packages/claude-code-headless/src/transcript/ProjectDir.ts
      • honors CLAUDE_CONFIG_DIR
      • realpath()s cwd
      • NFC-normalizes
      • replaces non-alphanumeric characters with -
    • src/shared/runtime/projectDir.ts
      • app-local duplicate of the same assumptions
    • src/main/sessions/historyLoader.ts
      • resolves Claude history as <projectDir>/<providerSessionId>.jsonl
    • src/main/providerSwitch/shared.ts
      • duplicate/provider-switch helpers also rely on the same path contract

    Risk: the long-path branch is knowingly incomplete. Both helpers truncate sanitized cwd at 200 chars instead of implementing Claude's hash suffix behavior. That is probably not this hackathon-bot case because the cwd is short and realpath equals the visible path, but it is another transcript-discovery hazard.

    Fresh tailing behavior

    Fresh sessions have an unavoidable spawn/attach ordering problem:

    • src/providers/claude/runtime/claudeSession.ts captures freshSessionStartedAtMs immediately before spawning Claude.
    • ClaudeCodeHeadless.start() attaches the JSONL tailer before terminal attach, but the PTY/process already exists by then.
    • JsonlTailer.tailNewSessionFile() watches one project dir, snapshots existing .jsonls, and attaches to a fresh/new candidate.

    Recent local hardening added freshSinceMs so files that already exist during the initial readdir can still be treated as fresh if their mtime/ctime >= spawnStart. That covers a narrow file-created-before-watcher race.

    But it does not explain the strongest evidence in this issue: several proxy-observed provider ids have no matching root JSONL anywhere in the expected project dir.

    Missing-file observability gap

    The biggest confirmed Agent Code gap is that “no transcript file appeared” is not a first-class state.

    Current behavior:

    • Fresh mode has no timeout or no file selected event.
    • Resume mode silently tolerates a missing file: bootstrapTail() / readNew() stat failures return without surfacing an error.
    • jsonl-error loses context when forwarded; it does not include file path, mode, cwd, resume id, offset, or tailing stage.
    • A fresh runtime can still show:
    transcriptStatus: "ready"
    totalEntries: 0
    lastJsonlEntryAt: null
    transcriptError: null
    

    while semantic/proxy/screen activity continues.

    Provider id capture is currently JSONL-dependent

    Fresh Claude panes do not persist providerSessionId at spawn time. The renderer captures Claude provider id only from committed JSONL entries, via the bulk session:jsonl-entries handler.

    That means if JSONL never appears:

    • the pane can keep working via proxy/semantic/screen
    • the app can know the provider id from proxy logs
    • but workspace.json never gets durable provider identity
    • later reload/resume/soft reload cannot recover committed history

    Proxy already observes the session id before JSONL

    The MITM proxy records the x-claude-code-session-id header:

    • packages/claude-code-headless/src/proxy/mitmAddon.py allowlists and writes the header into proxy events.
    • ProxyServer polls proxy-events.jsonl and emits parsed raw transport events.
    • ClaudeSession forwards those events only into headless.handleProxyTransportEvent().
    • ClaudeProxyAdapter uses request headers for attribution policy, then drops them from durable/session state.
    • No semantic/screen/committed event currently includes provider session identity.

    This exactly matches the local bundles: proxy-events.jsonl has the Claude provider id, but the renderer session metadata does not.

    Recommended instrumentation/fix seams

    1. Add a first-class provider_session_observed event at the proxy boundary when a request has headers['x-claude-code-session-id'].

    Suggested payload:

    {
      provider: 'claude',
      providerSessionId: string,
      source: 'proxy-header',
      flowId?: string,
      observedAt: number,
      cwd?: string,
      agentCodeSessionId: string,
    }
    1. Forward that through ClaudeCodeHeadless → SessionManager → IPC → renderer.

    2. Renderer should persist this as provisional provider identity only when metadata has no provider id yet. Existing JSONL-confirmed identity should not be overwritten silently.

    3. Keep a separate durability bit/state. Proxy-observed id means “we know Claude's session UUID”; it does not mean “history is reloadable.” History becomes reloadable only after the root JSONL exists or a supported alternate layout is confirmed.

    4. Add a transcript watchdog:

    if semantic/proxy/screen activity exists and lastJsonlEntryAt is null after N seconds:
      transcriptStatus = disconnected / unavailable
      transcriptError = expected path + provider id + observed sources
    
    1. Make missing resume file explicit. A missing <projectDir>/<resumeSessionId>.jsonl during resume should surface an error/diagnostic instead of silently polling nothing.

    2. Expand jsonl-error payloads with:

    mode: fresh | resume
    cwd
    projectDir
    resumeSessionId
    selectedFile
    stage: readdir | watch | bootstrap | live-read | parse
    byteOffset
    
    1. Add debug bundle transcript correlation:
    cwd
    realpath cwd
    expected project dir
    proxy-observed provider ids
    for each id: root JSONL exists/size/mtime, nested dir exists, subagent dir summary
    current renderer providerSessionId and provenance
    

    Working conclusion for this track

    Agent Code cannot assume JSONL is the first or only source of provider identity anymore. The proxy observes Claude's session id early and reliably enough to use as a provisional identity signal. But JSONL existence must remain the durability gate for resume/history, because upstream Claude can apparently advertise a session id while failing to create or retaining no root transcript file.

  4. Juliusolsson05 commented on Jun 18, 2026

    @Juliusolsson05
    OwnerAuthor

    Investigation update: workspace identity and duplicate providerSessionId findings

    This covers the workspace persistence / rehydrate / ownership track.

    Conclusion

    Duplicate providerSessionId values are not healthy workspace state unless we add an explicit alias/lineage model. The current code does not have such a model.

    Duplicate local sessionId references can be valid in selection-only surfaces like Tiled Dispatch lanes. That is different from two different workspace sessions sharing one durable provider transcript id.

    Current duplicate provider ids are mostly hibernated/detached, so they are not an immediate startup fork-bomb risk after #258. But they are unsafe dormant aliases: if revived/reloaded/attached, multiple local panes can resume or mutate the same provider transcript.

    Current workspace evidence

    Read-only scan of:

    /Users/juliusolsson/.config/agent-code/workspace.json
    

    Summary:

    tabs: 6
    sessions: 75
    grid/live tile leaves: 6
    detached: 69
    buried: 0
    unownedSessions: 0
    providerIds: 62 unique in one scan / 70 present in another full scan
    duplicateProviderGroups: 8
    missingProviderAgentRecords: 5
    

    Examples of duplicate durable provider ids:

    1a373e7d-7a66-4714-ac1e-497c849538b7 -> d4d53e1e-0354-49eb-b030-935fd9616a19:claude | 5c087ae1-1e13-4031-873a-b3f65a65f184:claude
    cef609a0-8d65-4aee-8e7c-5c8c70e70745 -> 8e826a8c-135f-4655-8c10-d12d7463e16e:claude | 70417358-183a-4adb-b533-199699263806:claude
    c6e490f9-09de-499e-85a5-1fe8f88fb299 -> e1815951-d8a1-4c19-ab06-7a32e0c9a7df:claude | 06f7e0d0-fc99-4f2d-8344-4149a1803af1:claude
    871a17a0-fa6f-4335-8206-62175b99e251 -> 5337ca6a-a110-4369-8d82-4521514f8fb0:claude | 448a18da-94a3-4dd8-b5c2-219087d350e9:claude
    

    Cross-provider duplicate example:

    c0d44dcb-3972-4c55-9a31-a242bb482e2a -> 681d3f85-8183-4f06-8997-44aec6de27e6:claude | aa4dd3dd-982a-4c17-b4be-a7ca872728ae:codex
    

    Missing provider ids include relevant Claude panes:

    fcb3bedf-58ac-4721-8b2f-7ce36eff6ed2 | claude | /Users/juliusolsson/Desktop/general/hackathon-bot
    847f4909-32f0-4cd7-a6ea-18a908ae2594 | claude | /Users/juliusolsson/Desktop/general
    

    What is expected vs dangerous

    Expected:

    • Same local sessionId selected by multiple Tiled Dispatch lanes.
    • providerSessionId absent on a truly fresh, unused provider pane before the first durable provider transcript event.
    • Detached/buried records preserved as metadata during rehydrate.

    Dangerous:

    • Two different local sessionIds with the same (kind, cwd, providerSessionId).
    • Cross-kind duplicate provider ids after provider switch, because provider switch code intends to create/retarget to a fresh provider transcript id.
    • Missing providerSessionId after live semantic/proxy activity, because reload/history has no durable transcript identity.

    Corrupted unless proven otherwise:

    Code-path facts

    • providerSessionId is the provider’s durable transcript identity, distinct from Agent Code’s launch-local SessionId.
    • Fresh spawn() only writes providerSessionId when resumeSessionId was requested. Fresh panes otherwise wait for JSONL ingest.
    • Rehydrate passes providerSessionId back as resumeSessionId for live tile leaves.
    • Rehydrate preserves hibernated detached/buried metadata without spawning it.
    • Rehydrate explicitly warns not to repair ownership by provider id because it is provider history identity, not workspace ownership identity.
    • Duplicate/rewind/switch paths are supposed to create new provider transcript identities. Normal clone/rewind/provider-switch should not leave two sessions sharing the same provider id.

    Invariants we should enforce

    1. sessions keys are workspace ownership ids. Every key must be owned by exactly one owning surface: tile leaf, detached record, or buried record. Autosave already prunes unowned metadata.

    2. dispatchMode.focusedSessionId, pins, and tiled lanes are selection pointers, not ownership. They must not preserve or resurrect sessions.

    3. For resumable agent sessions, (provider kind, transcript path) should be unique among distinct workspace sessions. For Claude, transcript path is:

    getProjectDir(cwd) / <providerSessionId>.jsonl
    

    For Codex, it is the exact rollout file for the thread id.

    1. If duplicate provider ids are intentionally allowed later, they need explicit lineage/alias metadata and a rule preventing simultaneous resume/write against the same provider transcript. That model does not exist today.

    Why this matters to #290

    The missing JSONL problem explains “live pane later has no committed history.” The duplicate/missing provider id problem explains how recovery/resume can then appear to jump back to an old message or wrong history: the workspace has no reliable one-to-one mapping between visible Agent Code pane and durable provider transcript.

  5. Juliusolsson05 commented on Jun 18, 2026

    @Juliusolsson05
    OwnerAuthor

    Investigation update: regression test plan

    This is the proposed test plan from the dedicated regression-harness track. No files have been edited yet.

    Existing test setup

    • Root scripts already run Vitest projects for unit, integration, and renderer.
    • vitest.config.ts aliases package source directly, so package-local tests under packages/**/*.test.ts can run in the unit project.
    • testing/README.md says new coverage should go into Vitest, not new ad-hoc scripts/test-*.ts probes.
    • There are currently no Claude JSONL tailer tests or package-local headless tests.

    Priority 1: JsonlTailer fresh-file race

    Add:

    packages/claude-code-headless/src/transcript/JsonlTailer.test.ts
    

    Target behavior:

    • tailNewSessionFile attaches to a preexisting JSONL newer than freshSinceMs.
    • tailNewSessionFile ignores preexisting old JSONL when freshSinceMs is set.
    • tailNewSessionFile without freshSinceMs does not replay preexisting files, documenting the original race.

    Assertions:

    • Create old JSONL.
    • Capture freshSinceMs.
    • Create fresh <uuid>.jsonl.
    • Call tailNewSessionFile.
    • It emits entries from the fresh file and reports that file path.
    • It does not emit old-file entries.
    • Append after attach is still emitted, proving the file tailer stayed live.

    This should be a pure unit test using mkdtemp, real files, and a small waitFor helper.

    Priority 2: proxy-observed provider identity without JSONL

    Add:

    testing/unit/proxy/claudeProviderSessionIdentity.test.ts
    

    Required product seam first: emit a small provider identity event from the proxy/header path, likely from ClaudeProxyAdapter.handleTransportEvent or one layer above it.

    Test cases:

    emits provider_session_observed from x-claude-code-session-id before any JSONL
    does not require message_start or text_delta to observe provider session identity
    dedupes repeated header observations for the same flow/session
    emits a conflict diagnostic rather than silently overwriting if a different id appears later
    

    Assertions:

    • A proxy request/response event with x-claude-code-session-id: <uuid> publishes exactly one identity event.
    • A semantic turn can start even if no JSONL follows.
    • No jsonl-entry is needed for provider id observation.

    Add renderer-side identity tests:

    testing/unit/workspace/providerSessionIdentity.test.ts
    

    Test cases:

    captures proxy-observed providerSessionId into empty Claude session metadata
    does not overwrite existing JSONL-captured providerSessionId
    marks proxy-only identity as not yet durable until transcript path is confirmed
    

    Priority 3: soft reload without providerSessionId

    Current risk: softReloadRuntime resets renderer-derived visible state and sets transcriptStatus to ready when no provider id exists. That can erase the only visible progress for a disconnected pane.

    Add:

    testing/unit/workspace/softReloadAgentView.test.ts
    

    First extract/export a pure helper, e.g. buildSoftReloadRuntime, so this does not need a full React hook render.

    Test cases:

    missing providerSessionId soft reload preserves visible live-only progress
    missing providerSessionId soft reload reports transcript unavailable instead of ready
    present providerSessionId soft reload clears caches and enters loading
    

    Assertions:

    • Given runtime with entries/ghosts/semantic current turn/streamPhase: awaiting-tool and no provider id, soft reload must not erase the only visible progress.
    • It must not set transcriptStatus: ready with transcriptError: null.
    • With provider id present, current destructive reset remains valid and should set transcriptStatus: loading.

    Priority 4: duplicate providerSessionId diagnostics

    Add a pure helper first, likely:

    src/renderer/src/workspace/providerIdentity.ts
    analyzeProviderSessionOwnership(stateOrPersistedWorkspace)
    

    Add tests:

    testing/unit/workspace/providerSessionOwnership.test.ts
    

    Test cases:

    flags same-kind duplicate providerSessionId among live tile sessions
    ignores cross-provider UUID equality or reports it separately as cross-kind anomaly
    reports hibernated duplicate separately from live duplicate
    pruned unowned duplicate does not participate in rehydrate spawn
    rehydrate spawn requests are keyed by local session id, not provider id lookup
    

    Assertions:

    • Two live Claude tile leaves with the same providerSessionId are reported as a blocking ambiguity.
    • A detached/buried duplicate is preserved as metadata but not spawned on rehydrate.
    • An unowned duplicate row is dropped by pruneSessionOwnership.
    • Rehydrate calls spawnSession once per live local session and never chooses a session by provider id.

    Test priority order

    1. Tailer unit test: seam already exists and should pass or expose the race immediately.
    2. Proxy identity event tests: this is the missing durable-id source for no-JSONL sessions.
    3. Soft reload behavior: prevents user-visible progress erasure while disconnected.
    4. Duplicate-provider ownership diagnostics: prevents future fixes from turning stale duplicate metadata into wrong-history resume.
  6. Juliusolsson05 commented on Jun 18, 2026

    @Juliusolsson05
    OwnerAuthor

    Review follow-up: linked agents can render the same provider conversation twice

    Fresh symptom to explicitly capture: linked agents / orchestration children can become confused so the UI shows what appears to be the same conversation twice under different panes. This is not merely a visual row-rendering bug. It is consistent with the workspace identity findings above:

    local Agent Code sessionId A -> providerSessionId X
    local Agent Code sessionId B -> providerSessionId X
    

    If two distinct local sessions point at the same durable provider transcript, the UI can render the same conversation twice and later reload/resume can appear to jump to an old/wrong point. That makes duplicate/mis-bound provider identity a first-order failure mode, not a cleanup-only concern.

    Claude reviewer deltas to fold into the plan

    1. Fix the false-ready state first. softReloadRuntime and spawn currently produce transcriptStatus: 'ready' when there is no providerSessionId; for disconnected Claude panes this is the bug, not a neutral default.
    2. Decide target contracts before tests. provider_session_observed, disconnected/unavailable, and duplicate-safe resume lookup are new contracts, not existing behavior. Tests must encode the desired behavior rather than current behavior.
    3. Pair every freshSinceMs test with an absent-file test. freshSinceMs is only a narrow race mitigation; green tailer tests must not imply the missing-root-JSONL bug is fixed.
    4. Add the headline negative test: proxy/semantic activity plus totalEntries: 0 and lastJsonlEntryAt: null must never produce transcript status ready.
    5. Test provisional vs durable identity explicitly: proxy-observed id sets identity only when empty, never overwrites JSONL-confirmed identity, and never marks history loadable by itself.
    6. Add a mis-attachment test: a stale/touched transcript after spawn start must not be selected as the fresh session transcript; cover boundary equality and first-attach behavior.
    7. Add resume-path coverage: missing <resumeId>.jsonl must surface a diagnostic instead of silently polling forever.
    8. Reframe duplicate-id tests around resume/attach lookup, not a blanket “no duplicates in workspace.json.” Legitimate clone/rewind flows can mint new provider ids; ambiguous attach/resume must fail or require disambiguation.
    9. Add an integration harness that replays proxy/semantic events with no root JSONL and asserts the session never reports durable/ready history.
    10. Extract a pure transcript/identity reducer, e.g. deriveTranscriptState(...), so the core invariant is testable without React/happy-dom.
    11. Inject a clock for freshSinceMs and watchdog timing; avoid wall-clock/flaky watcher tests.
    12. Make proxy identity a first-class adapter emit and extract transcript file-selection into a pure function so tests do not depend on real watchers/network.

    Documentation cleanup suggested by reviewers

    The issue is strong but now dossier-sized. To make it easier for implementation:

    • Add a canonical evidence table near the top: run, Agent Code session id, proxy provider id, proxy size, root JSONL exists, nested/subagents exists, workspace provider id present.
    • Add observed environment/trigger conditions: Claude Code 2.1.179/2.1.181-class, subagent-heavy/linked-agent/orchestration runs, embedded Agent Code PTY/proxy path.
    • Mark the implementation/fix-seams comment as authoritative over the older body plan.
    • Clarify title/wording toward “JSONL absent while proxy/semantic live” because evidence currently supports absent/missing more strongly than a proven live disconnect after creation.
    • Note privacy/sensitivity explicitly: local UUIDs/paths only, no message bodies or proxy request bodies included.
  7. Juliusolsson05 commented on Jun 18, 2026

    @Juliusolsson05
    OwnerAuthor

    Implementation slice opened:

    What this slice does:

    • Adds a typed proxy semantic event for Claude's x-claude-code-session-id so the renderer can record provisional provider identity before JSONL durability exists.
    • Adds providerSessionIdSource so proxy-header identity does not get confused with JSONL-confirmed identity.
    • Marks panes disconnected when proxy/semantic activity proves a Claude session exists but no committed JSONL transcript has arrived.
    • Makes no-provider soft reload non-destructive instead of wiping live-only renderer state and then showing ready.
    • Preserves and renders the disconnected state through reload/rehydrate/status UI.

    Verification on the worktree:

    • npm --prefix packages/claude-code-headless run build
    • npm run test:unit -- testing/unit/channels/semanticChannel.promptSuggestion.test.ts testing/unit/proxy/promptSuggestionDetection.test.ts
    • npm run build

    Known caveat:

    • Full npm run test:unit currently fails only after initializing packages/agent-voice-dictation because Vitest discovers packages/agent-voice-dictation/apps/flow-electron/src/shared/hotkeyBinding.test.ts as a file with no Vitest suite. I did not fold that unrelated test discovery problem into this PR.
  8. Juliusolsson05 commented on Jun 18, 2026

    @Juliusolsson05
    OwnerAuthor

    PR #293 has been updated after orchestration review.

    The blocker was real: ids were marked provisional but still consumed by resume/history-load paths. Commit a820f6a fixes that by making provisional ids non-resumable and adding focused unit tests for the identity transitions.

    Reverified:

    • targeted workspace/channel/proxy unit tests
    • app build
  9. Juliusolsson05 commented on Jun 18, 2026

    @Juliusolsson05
    OwnerAuthor

    Corrected implementation note after review:

    The orchestration agents found a real blocker: proxy-header ids were marked provisional but still consumed by resume/history-load paths. PR #293 now fixes that in commit a820f6a by making proxy-header ids non-resumable/non-history-loadable everywhere those ids are consumed.

    Submodule PR #21 has been merged, and PR #293 now points at claude-code-headless main commit 6178461.

    Final verification:

    • npm --prefix packages/claude-code-headless run build
    • targeted workspace/channel/proxy unit tests
    • npm run build
  10. Juliusolsson05 commented on Jun 18, 2026

    @Juliusolsson05
    OwnerAuthor

    Merged implementation slice:

    The final merged version includes the orchestration review fix: proxy-header provider ids are recorded as provisional but are not used for resume/history-load. JSONL or explicit resume remains the durable source for reloadable transcript identity.

    Verified before merge:

    • npm --prefix packages/claude-code-headless run build
    • npm run test:unit -- testing/unit/workspace/applyPromptSuggestionToRuntime.test.ts testing/unit/channels/semanticChannel.promptSuggestion.test.ts testing/unit/proxy/promptSuggestionDetection.test.ts
    • npm run build
  11. Juliusolsson05 commented on Sep 20, 2026

    @Juliusolsson05
    OwnerAuthor

    Audit note (2026-09-20): the identity half of this shipped; the defensive UX cannot fire in the state this issue recorded. Worth knowing before anyone assumes the marker protects them.

    Shipped (PR #293, merge 63e54562): providerSessionIdentity.ts records proxy-header ids with providerSessionIdSource: 'proxy-header'; hasDurableProviderSession() / resumableProviderSessionId() make those non-resumable and non-history-loadable; applyJsonlProviderSessionId() rejects a conflicting durable id; decideJsonlProviderBurst() blocks a burst proven to belong to another transcript. transcriptStatus: 'disconnected' exists end to end and renders as "transcript disconnected" (readiness.ts:75). Renderer honesty when committed truth never arrives is pinned by fixtures.deadCommittedChannel159.test.ts, which names this issue.

    The gap. shouldMarkProviderSessionDisconnected (providerSessionIdentity.ts:164-172) returns false when transcriptStatus is 'loading' or 'ready' — only 'idle' passes. But a fresh Claude pane is seeded 'ready' (seedResumedRuntimeFields: meta?.providerSessionId ? 'loading' : 'ready'), emptyRuntime() also defaults 'ready' (session-runtime/state.ts:909), and nothing on main assigns 'idle' on a live spawn.

    That is precisely the bundle in this issue: transcriptStatus: "ready", totalEntries: 0, lastJsonlEntryAt: null. So the guard is real, the id can no longer be confused with a durable one — and the banner that was supposed to tell the user their transcript is dead is unreachable on the exact shape that motivated it.

    Also still unaddressed: the workspace.json duplicate-providerSessionId half, and the time-based watchdog ("after N seconds with a live proxy stream and no JSONL growth").

    Remaining work is therefore: make 'idle' reachable (or widen the gate to 'ready' with an age condition), then the watchdog.

  12. 1 remaining item

  13. added
    type:bugSomething works wrong
    class:C2-identityIdentity/ownership across reload, replace, restore, wake
    sev:P1Core flow broken or badly degraded
    on Sep 25, 2026
  14. Juliusolsson05 commented on Sep 25, 2026

    @Juliusolsson05
    OwnerAuthor

    Progress note (quality loop): #1222 ships the marker slice. After a completed Claude turn plus 15 s with zero committed entries and no durable identity, the pane shows "transcript disconnected" in both rendered and terminal view, and it clears on the first committed record.

    Still open here:

    1. The workspace duplicate providerSessionId cleanup.
    2. Detection for a pane that is still mid-turn (for example awaiting a tool, as in the recorded bundle's final state), which never reaches turn_completed.
    3. Surviving a window transfer or full renderer reload. The marker lives in renderer runtime state. A pane with no provider id that is transferred or reloaded before its next completed turn comes back ready, and managedTranscriptUnavailableReason then reports its transcript as available. Found by the fix(claude): say so when a finished turn never commits a transcript #1222 lifecycle review; it needs the completed-turn/no-commit evidence carried across adoption, or derived on the receiving side.
  15. Juliusolsson05 commented on Sep 25, 2026

    @Juliusolsson05
    OwnerAuthor

    Reopened: #1222 closed this on merge even though its final commit and PR body say "Refs #290" (the PR was first opened with "Fixes #290"; see the next comment for the cause). #1222 shipped only the marker slice. Still open, per the earlier comment here: duplicate providerSessionId cleanup, detection for a pane still mid-turn, and keeping the marker across a window transfer or reload.

  16. Juliusolsson05 commented on Sep 25, 2026

    @Juliusolsson05
    OwnerAuthor

    Cause: GitHub kept the closing link it created when #1222 was opened with "Fixes #290". Rewriting the PR body and amending the commit to "Refs" did not remove it (closedByPullRequestsReferences: #1222). The quality loop now checks closingIssuesReferences before every merge.

  17. Juliusolsson05 commented on Sep 25, 2026

    @Juliusolsson05
    OwnerAuthor

    Quality-loop evidence pass on the three remaining items (2026-09-25):

    3. The marker across a window transfer or reload: mostly covered already. Evidence, not a guess:

    • The proxy header gives a Claude pane a provisional id on its first request (providerSessionIdSource: 'proxy-header', persisted in SessionMeta).
    • initialHistory.ts reloads such a pane as disconnected when there is no committed transcript: see the isProvisional branch, and hasDurableProviderSession in providerSessionIdentity.ts.
    • Live workspace today: 14 Claude panes. 13 have jsonl-entry ids. 1 has a proxy-header id whose transcript file does not exist, and that is exactly the pane this path shows as disconnected after a reload. No Claude pane uses runtime-start.
    • What is left is a pane with no id at all that still completed a turn. That needs semantic turns without a proxy header, and none is recorded. Marking needs-evidence for that edge.

    2. Detection for a pane still mid-turn (for example awaiting a tool forever): needs-evidence. Any timeout here competes with legitimately long tools (see the goal-loop hold design). It needs a recorded case where a mid-turn pane never commits.

    1. Duplicate providerSessionId semantics: needs-owner. The issue itself frames this as a product decision: are duplicates illegal, legal only with lineage, or legal but excluded from resume lookup? Some duplicates are intentional (clone, rewind, provider switch).

  18. added a commit that references this issue on Sep 25, 2026
  19. Juliusolsson05 commented on Sep 27, 2026

    @Juliusolsson05
    OwnerAuthor

    Kept open, but it's not certain this still applies (owner decision, 2026-09-27). Since it was filed: #293 stopped an id taken from the proxy header being used to resume, and #1222 added a 'transcript disconnected' marker (finished turn + 15 s with nothing committed). Still unverified: a policy for duplicate provider session ids, mid-turn detection, and panes that never get a session id. The next step is to capture a fresh case, or confirm on current builds that it no longer happens.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingclass:C2-identityIdentity/ownership across reload, replace, restore, wakeneeds-evidenceNeeds a recording/real use to settleneeds-ownerOwner decision neededprovider:claudeAffects Claude Code integrationsev:P1Core flow broken or badly degradedtype:bugSomething works wrong

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions