Skip to content

feat(edge-worker): resume prior runner session on issue re-delegation (opt-in)#1286

Open
AGC-prod wants to merge 3 commits into
cyrusagents:mainfrom
AGC-prod:feat/cross-session-resume
Open

feat(edge-worker): resume prior runner session on issue re-delegation (opt-in)#1286
AGC-prod wants to merge 3 commits into
cyrusagents:mainfrom
AGC-prod:feat/cross-session-resume

Conversation

@AGC-prod

@AGC-prod AGC-prod commented Jun 3, 2026

Copy link
Copy Markdown

Problem

When the agent is re-delegated / re-mentioned on a Linear issue it has already worked on, Linear fires a fresh agentSessionCreated webhook with a brand-new agentSession.id. EdgeWorker.initializeAgentRunner passes resumeSessionId = undefined unconditionally, so every re-delegation cold-starts a new runner session and re-reads the entire issue context — burning tokens and latency, and losing the prior conversation.

(Follow-up comments within the same agent session already resume via claudeSessionId; only the new-agent-session path cold-starts.)

Observed in practice: a single issue accumulating many independent runner transcripts in its worktree, one per delegation, none resuming the others.

Change

Opt-in cross-session resume: when a new agent session is created on an issue with prior work, resume the most recent prior runner conversation instead of cold-starting.

  • Durable per-issue record (issueLastSession) in cyrus-core state + AgentSessionManager. Captured wherever a runner session id is captured; not cleared by removeSession(), so it survives the session pruning that happens when an issue goes terminal/unassigned. Additive optional state field — no persistence version bump, old state loads unchanged.
  • initializeAgentRunner resolves a resumeSessionId: (a) the most-recent still-live session for the issue, else (b) the durable record. It seeds the matching runner-id field on the new session so RunnerConfigBuilder.buildIssueConfig keeps the runner type consistent with the resumed transcript, and posts a short "Resuming from a previous session" note to the thread. The issue's worktree path is deterministic per identifier, so the runner cwd already matches the transcript — no workspace juggling needed.
  • ClaudeRunner falls back to a cold start exactly once if the SDK can't locate the prior transcript (e.g. it was cleaned up), so re-delegation never hard-fails.
  • Opt-in, default off: env CYRUS_RESUME_ACROSS_SESSIONS=1 or per-repository resumeAcrossSessions: true.

Why it's safe

Off by default. When on, the only new behavior is passing a resume id on session creation; if resume isn't possible the runner degrades to today's cold-start path.

Tests

  • AgentSessionManager.issue-last-session — record captured on init, survives removeSession(), round-trips through serialize/restore.
  • EdgeWorker.cross-session-resume — resolution + opt-in gating (env and per-repo), most-recent-wins, current-session excluded, durable-record fallback, runner-type mapping, disabled-by-default.
  • ClaudeRunner.resume-fallbackisResumeFailure heuristic; retries once cold on resume failure; no retry when resume isn't set.

Full suites pass (core 137, claude-runner 117, edge-worker 692); build, typecheck, biome, and committed JSON schemas all green.

Manual verification

With CYRUS_RESUME_ACROSS_SESSIONS=1, re-delegating on an issue with a prior completed session logs Resuming prior claude session <id> … (cross-session resume) and [event:session_resumed], and the new session's messages carry the prior claudeSessionId — confirming it continued the previous conversation rather than cold-starting.

AGC-prod and others added 3 commits June 2, 2026 20:34
When a new Linear agent session is created on an issue Cyrus has already
worked on (re-delegation / re-mention fires a fresh agentSession.id), the
runner cold-started and re-read all issue context. This adds opt-in
cross-session resume so the new session continues the most recent prior
runner conversation instead.

- Durable per-issue last-session record (survives session pruning on
  terminal/unassign); additive optional state field, no version bump.
- initializeAgentRunner resolves a resumeSessionId from the latest live
  prior session, else the durable record; seeds the matching runner id so
  the runner type stays consistent. Posts a "resuming" note to Linear.
- Opt-in via CYRUS_RESUME_ACROSS_SESSIONS env or per-repo
  resumeAcrossSessions; default off.
- ClaudeRunner falls back to a cold start (once) if the SDK cannot locate
  the prior transcript, so re-delegation never hard-fails.
- Tests: durable map persist/survive/round-trip, EdgeWorker resolution +
  gating, runner resume-fallback retry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(CRATE-153)

Linear access tokens live ~24h and the OLD token is revoked the moment a
refresh succeeds (verified empirically). Refresh was previously reactive-only
(daemon-side 401), leaving multi-hour dead-token windows during which agent
sessions snapshotted an expired token into their static MCP Authorization
header — status writes then failed with 'requires re-authorization' while
daemon-mediated comments kept working (observed in CRATE-80).

- linear-event-transport: track token expiry (expires_in -> expiresAt),
  expose getTokenExpiresAt()/ensureFreshToken(minTtl) with coalescing;
  onTokenRefresh now reports expiresAt for persistence
- core: persist linearTokenExpiresAt per workspace in config.json
- edge-worker: proactive refresh scheduler (idle-aware — refreshing while
  turns run would revoke their snapshots), turn-start guard so runner
  configs never snapshot a near-dead token, warm-instance invalidation on
  rotation, and loud error + self-healing refresh when a session hits
  'requires re-authorization'
- mcp-tools: new linear_update_issue_status tool backed by the daemon's
  auto-refreshing LinearClient, so the critical 'move to In Review' write
  works even when the hosted Linear MCP snapshot is stale

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…esh (CRATE-153)

Co-Authored-By: Claude Opus 4.8 (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