Skip to content

fix: session resume for custom AI providers - #100

Open
mthorme wants to merge 2 commits into
debuglebowski:mainfrom
mthorme:mt/upstream/custom-provider-session-resume
Open

fix: session resume for custom AI providers#100
mthorme wants to merge 2 commits into
debuglebowski:mainfrom
mthorme:mt/upstream/custom-provider-session-resume

Conversation

@mthorme

@mthorme mthorme commented Aug 7, 2026

Copy link
Copy Markdown

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 in terminal_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, so notify.sh exited at its [ -z "$SLAYZONE_AGENT_HOOK_URL" ] guard, confirmSessionConversation never ran, the row stayed pending-spawn (not an honored origin), and resolveSpawnConversation therefore found no id — initialCommand won 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 --resume cannot 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

  • Resolve the agent from terminal_modes.type (falling back to the mode id, so built-ins are untouched) and gate on that in both places.
  • SLAYZONE_AGENT_ID must stay a real agent id — the hook endpoint rejects anything else via isAgentId — so the mode travels separately as ctx.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.mode is 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):

  • ledger showed ccremote-t3b61 | pending-spawn | 5 and zero honored rows, against claude-code | slay-spawned-fresh | 16 + slay-spawned-resume | 5
  • all 22 live tmux sessions had been started with --session-id; not one with --resume
  • verified separately that claude --session-id X then claude --resume X works, so the CLI was not at fault

After 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 carries ctx.mode; a built-in's ctx omits mode (payload unchanged); a custom provider wrapping a non-hook agent stays incapable.
  • agent-hook.test.ts — 71 pass. spawn-conversation.test.ts — 14 pass.
  • Typecheck clean on packages/domains/terminal, packages/domains/task, packages/shared/transport.

Known gap

agent-sessions.test.ts could not be executed in my checkout — it fails to resolve better-sqlite3 under 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.

  • Resolves hook capability and turn tracking through terminal_modes.type.
  • Carries custom mode identity separately in hook context while preserving built-in payloads.
  • Uses the resolved mode for conversation persistence and live PTY state updates.
  • Adds coverage for hook-capable and non-hook-capable custom providers.

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

Filename Overview
packages/domains/task/src/server/ops/agent-sessions.ts Resolves the wrapped agent before deciding whether confirmation itself proves session resumability.
packages/domains/terminal/src/server/mcp-env.ts Enables hooks for custom providers wrapping supported agents and sends agent and mode identities separately.
packages/shared/transport/src/server/http/rest-api/agent-hook.ts Resolves optional mode context and consistently uses it for conversation-ledger and PTY lookups.
packages/domains/terminal/src/server/mcp-env.test.ts Adds regression checks for custom-provider hook capability, identity propagation, and built-in compatibility.

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 resumable
Loading

Reviews (1): Last reviewed commit: "fix(task): key turn-tracking on the agen..." | Re-trigger Greptile

mthorme and others added 2 commits August 7, 2026 21:09
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant