feat(edge-worker): resume prior runner session on issue re-delegation (opt-in)#1286
Open
AGC-prod wants to merge 3 commits into
Open
feat(edge-worker): resume prior runner session on issue re-delegation (opt-in)#1286AGC-prod wants to merge 3 commits into
AGC-prod wants to merge 3 commits into
Conversation
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>
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.
Problem
When the agent is re-delegated / re-mentioned on a Linear issue it has already worked on, Linear fires a fresh
agentSessionCreatedwebhook with a brand-newagentSession.id.EdgeWorker.initializeAgentRunnerpassesresumeSessionId = undefinedunconditionally, 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.
issueLastSession) incyrus-corestate +AgentSessionManager. Captured wherever a runner session id is captured; not cleared byremoveSession(), 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.initializeAgentRunnerresolves aresumeSessionId: (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 soRunnerConfigBuilder.buildIssueConfigkeeps 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.ClaudeRunnerfalls 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.CYRUS_RESUME_ACROSS_SESSIONS=1or per-repositoryresumeAcrossSessions: 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, survivesremoveSession(), 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-fallback—isResumeFailureheuristic; 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 logsResuming prior claude session <id> … (cross-session resume)and[event:session_resumed], and the new session's messages carry the priorclaudeSessionId— confirming it continued the previous conversation rather than cold-starting.