fix: session resume for custom AI providers - #100
Open
mthorme wants to merge 2 commits into
Open
Conversation
A user-defined provider could never resume: every restart re-minted a fresh session instead of reopening the previous one. `buildMcpEnv` gated hook capability on the MODE ID (`HOOK_SUPPORTED_AGENT_IDS.has(mode)`), but that set holds AGENT ids. A built-in mode IS its agent id, while a custom provider has its own slug (`ccremote-t3b61`) and declares the agent it wraps in `terminal_modes.type`. So custom providers were structurally hook-incapable: no hook env at all, notify.sh exited at its `[ -z "$SLAYZONE_AGENT_HOOK_URL" ]` guard, `confirmSessionConversation` never ran, the `task_conversations` row stayed `pending-spawn` — not an honored origin — so `resolveSpawnConversation` found no id and `initialCommand` won on every spawn. Resolve the agent from `terminal_modes.type` (falling back to the mode id) and gate on that. `SLAYZONE_AGENT_ID` must stay a real agent id because the hook endpoint rejects anything else (`isAgentId`), so the mode travels separately as `ctx.mode`; the ledger and PTY lookups key off that instead of the agent id. Without it a custom provider's conversation would be filed under the wrapped agent's built-in row and the two could resume into each other's sessions. `ctx.mode` is omitted when it equals the agent id, so built-in hook payloads stay byte-identical and legacy payloads resolve unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second half of the custom-provider resume fix. `confirmSessionConversation` asked `TURN_TRACKED_MODES.has(row.mode)`, but that set holds AGENT ids. A custom provider's mode is its own slug, so it was classified as "turns unobservable" and its sessions were marked resumability-proven at spawn — before the provider had written any transcript. The next restart then resumed an id `--resume` cannot find, which fails, trips the conversation healer, and RESETS the task: the user sees an empty chat and the conversation pointer is gone. Observed end-to-end on a ccremote-t3b61 provider immediately after it became hook-capable. Resolve the agent from `terminal_modes.type` (falling back to the mode id, so built-ins are unchanged) and test that instead. A custom provider wrapping claude-code is now correctly turn-tracked: it stays unproven until a real first turn, which is exactly when its transcript exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix: session resume for custom AI providers
A user-defined provider could never resume a session — every restart started an empty chat, while built-in providers resumed fine.
Root cause
Two places ask a question about the agent but test the mode id. A built-in mode is its agent id (
claude-code); a user-defined provider has its own slug and declares the agent it wraps interminal_modes.type.1.
hookCapable(mcp-env.ts) — gated on the mode id, so a custom provider was structurally hook-incapable. It received no hook env at all, sonotify.shexited at its[ -z "$SLAYZONE_AGENT_HOOK_URL" ]guard,confirmSessionConversationnever ran, the row stayedpending-spawn(not an honored origin), andresolveSpawnConversationtherefore found no id —initialCommandwon on every spawn.2.
TURN_TRACKED_MODES(agent-sessions.ts) — same confusion. A custom provider counted as "turns unobservable", so its sessions were marked resumability-proven at spawn, before any transcript existed. The next restart resumed an id--resumecannot find, which fails, trips the healer, and resets the task — losing the conversation pointer.The second matters as much as the first: fixing only #1 promotes the provider straight into #2, making resume worse rather than better.
Changes
terminal_modes.type(falling back to the mode id, so built-ins are untouched) and gate on that in both places.SLAYZONE_AGENT_IDmust stay a real agent id — the hook endpoint rejects anything else viaisAgentId— so the mode travels separately asctx.mode, and the ledger/PTY lookups key off it. Without this a custom provider's conversation is filed under the wrapped agent's built-in row, and the two can resume into each other's sessions.ctx.modeis omitted when it equals the agent id, so built-in hook payloads stay byte-identical and legacy flat payloads resolve unchanged.Evidence
Diagnosed against a real custom provider (mode
ccremote-t3b61,type = claude-code, wrapping Claude Code in tmux):ccremote-t3b61 | pending-spawn | 5and zero honored rows, againstclaude-code | slay-spawned-fresh | 16+slay-spawned-resume | 5--session-id; not one with--resumeclaude --session-id Xthenclaude --resume Xworks, so the CLI was not at faultAfter the fix, on a live build: spawns record
usedResume: true, conversations self-confirm, zero resets.Verification
mcp-env.test.ts— 60 → 66 checks. New cases: a custom provider wrapping a hook-capable agent gets the hook env and carriesctx.mode; a built-in's ctx omitsmode(payload unchanged); a custom provider wrapping a non-hook agent stays incapable.agent-hook.test.ts— 71 pass.spawn-conversation.test.ts— 14 pass.packages/domains/terminal,packages/domains/task,packages/shared/transport.Known gap
agent-sessions.test.tscould not be executed in my checkout — it fails to resolvebetter-sqlite3under the repo's test runner, and fails identically on an unmodified tree, so it is pre-existing rather than caused by this change. That means commit 2 has no executed regression test. Happy to add one if you can point me at how that suite is meant to run in your environment.Greptile Summary
The PR fixes session resumption for custom AI providers by resolving behavior from the wrapped agent type while retaining the custom mode as the ledger and PTY key.
terminal_modes.type.Confidence Score: 5/5
The PR appears safe to merge, with no concrete blocking or independently actionable non-blocking issue established.
The changed paths consistently distinguish wrapped agent identity from terminal mode identity, preserve built-in behavior, and route custom-provider session confirmation, persistence, and PTY updates through the intended mode.
Important Files Changed
Sequence Diagram
sequenceDiagram participant Spawn as Custom provider spawn participant DB as terminal_modes participant Env as buildMcpEnv participant Agent as Wrapped agent participant Hook as Agent hook endpoint participant Ledger as Conversation ledger participant PTY as PTY registry Spawn->>DB: Resolve mode.type DB-->>Env: Wrapped agent ID Env->>Agent: Hook URL + agentId + ctx.mode Agent->>Hook: Session/turn hook Hook->>Ledger: Persist using custom mode Hook->>PTY: Find/update using custom mode Ledger-->>Spawn: Conversation becomes resumableReviews (1): Last reviewed commit: "fix(task): key turn-tracking on the agen..." | Re-trigger Greptile