Skip to content

Commit c5d0e74

Browse files
committed
Merge origin/main into codex/migrate-pr-806-to-main
# Conflicts: # AGENTS.md # src/agent-cli-provider/adapters/claude.ts # src/agent-cli-provider/types.ts # src/agent/agent-task-executor.js # task-lib/attachable-watcher.js # task-lib/runner.js # task-lib/store.js # task-lib/watcher.js # tests/agent-cli-provider/parity.test.js
2 parents be54862 + 0dab9f0 commit c5d0e74

43 files changed

Lines changed: 3165 additions & 103 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Destructive commands (need permission): `zeroshot kill`, `zeroshot clear`, `zero
5252
| Claude settings overlay | `src/worktree-claude-config.js` |
5353
| Detached task cleanup owner | `task-lib/command-spec-cleanup.js` |
5454
| Shared watcher output path | `task-lib/watcher-output-runtime.js` |
55+
| Provider session reuse | `src/agent/provider-session.js` |
5556
| Start-cluster helper | `lib/start-cluster.js` |
5657
| Legacy worker facade | `lib/cluster-worker/` |
5758
| Legacy worker executable | `bin/zeroshot-cluster-worker.js` |
@@ -351,13 +352,38 @@ and cleanup ownership intact; retry and cleanup stay blocked until a later kill
351352
Cancellation before PID publication is a durable task intent. Both watcher paths check it before
352353
provider spawn and immediately after publishing the owned PID boundary; callers retain their task
353354
handle until terminal state and command cleanup are both confirmed.
355+
Provider continuation is agent- and generation-owned and becomes durable only after logical output
356+
validation and the `onComplete` hook succeed. A requested resume is successful only when the
357+
watcher captures that exact same nonempty provider session ID; absent or forked identity fails the
358+
attempt before hooks and forces the retry to rebuild full context. Watchers track every unique
359+
session ID observed in a task; once two IDs differ, the persisted capture is permanently ambiguous
360+
even if a later event repeats the requested ID. Persist SQLite rowid high-water and applied-guidance
361+
cursors as canonical decimal strings, bind them to SQLite as `BigInt`, and never coerce them through
362+
JavaScript `Number`. Persist those cursors and a bounded SHA-256 selected-prompt identity with the
363+
observed provider session; never persist the selected prompt text. Restored
364+
continuations fail closed unless the final durable `TASK_COMPLETED` boundary and all provenance
365+
match. Full and continuation source/guidance reads are bounded through the captured high-water;
366+
continuations query strictly after their prior sequence and de-duplicate the exact triggering
367+
message by ledger ID. Timestamps are display/filter metadata, not continuation cursors: concurrent
368+
writers can share one millisecond. If the installed CLI cannot resume, rebuild full context or fail
369+
before launch—never send a continuation delta to a fresh provider session.
370+
371+
Provider session reuse is explicit-ID and agent-owned. Watcher-observed IDs are distinct from
372+
requested resume IDs. Commit continuation only after logical/structured success and bind it to the
373+
completed task, agent, generation, provider, cwd, and worktree. A resumed turn sends only new
374+
trigger/guidance context; it never replays static prompts or ISSUE_OPENED/PLAN_READY packs already in
375+
the provider session. Persist continuation in that agent's `agentStates` entry, never in native
376+
`ClusterLedger`, never select a cwd-wide "latest" session, and never share across agents. Durable
377+
restore fails closed unless the last lifecycle boundary is the exact matching `TASK_COMPLETED`;
378+
live, failed, retry/backoff, provider-switch, unsupported, Docker, and workspace-drift states start
379+
fresh.
354380

355381
### Guidance Messaging
356382

357383
- Topics: `USER_GUIDANCE_CLUSTER`, `USER_GUIDANCE_AGENT` (see `src/guidance-topics.js`).
358384
- Mailbox helper: `ledger.queryGuidanceMailbox()` with `messageBus.queryGuidanceMailbox()` passthrough.
359385
- Live injection: `Orchestrator.sendGuidanceToAgent()` uses `agent.injectInput()` to attempt PTY stdin; always persists `USER_GUIDANCE_AGENT` with `metadata.delivery` (`status: injected|unsupported`, `method: pty`, `taskId`, `reason`).
360-
- Safe-point queue fallback: `AgentWrapper._buildContext()` pulls queued guidance via `collectQueuedGuidance()` and injects a delimited block in `agent-context-builder` between Instructions and Output Schema. Cursor: `agent.lastGuidanceAppliedAt`.
386+
- Safe-point queue fallback: `AgentWrapper._buildContext()` pulls queued guidance via `collectQueuedGuidance()` and injects a delimited block in `agent-context-builder` between Instructions and Output Schema. Durable sequence: `agent.lastGuidanceAppliedId`.
361387

362388
### Agent Configuration (Minimal)
363389

cli/commands/inspect.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ function buildTaskSummary(task, details) {
140140
error: task.error || null,
141141
cwd: task.cwd || null,
142142
sessionId: task.sessionId || null,
143+
requestedResumeSessionId: task.requestedResumeSessionId || null,
143144
attachable: Boolean(task.attachable),
144145
socketPath: details.socketPath.path,
145146
socketPathExists: details.socketPath.exists,

cli/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2837,7 +2837,7 @@ taskCmd
28372837
.option('--model <model>', 'Model id override for the provider')
28382838
.option('--model-level <level>', 'Model level override (level1, level2, level3)')
28392839
.option('--reasoning-effort <effort>', 'Reasoning effort (low, medium, high, xhigh, max)')
2840-
.option('-r, --resume <sessionId>', 'Resume a specific Claude session (claude only)')
2840+
.option('-r, --resume <sessionId>', 'Resume a specific provider session (Claude or Codex)')
28412841
.option('-c, --continue', 'Continue the most recent Claude session (claude only)')
28422842
.option(
28432843
'-o, --output-format <format>',

src/agent-cli-provider/adapters/claude.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { stringifyJson } from '../json';
1+
import { getString, isRecord, stringifyJson, tryParseJson } from '../json';
22
import { contractError } from '../contract-errors';
33
import {
44
type BuildProviderCommandOptions,
@@ -71,6 +71,7 @@ function detectCliFeatures(helpText?: string | null): ClaudeCliFeatures {
7171
// fail closed before a provider process is spawned.
7272
supportsSettings: !unknown && /--settings/.test(help),
7373
supportsMcpConfig: !unknown && /--mcp-config/.test(help),
74+
supportsResume: unknown ? true : /--resume/.test(help),
7475
unknown,
7576
};
7677
}
@@ -120,11 +121,17 @@ function addAutoApproveArgs(args: string[], options: BuildProviderCommandOptions
120121
}
121122

122123
function addSessionArgs(args: string[], options: BuildProviderCommandOptions): void {
123-
if (options.resumeSessionId) {
124+
const features = optionFeatures(options);
125+
if ((options.resumeSessionId || options.continueSession) && features.supportsResume === false) {
126+
throw new Error(
127+
'Claude CLI cannot safely run continuation context because this installation lacks --resume.'
128+
);
129+
}
130+
if (options.resumeSessionId && features.supportsResume !== false) {
124131
args.push('--resume', options.resumeSessionId);
125132
return;
126133
}
127-
if (options.continueSession) {
134+
if (options.continueSession && features.supportsResume !== false) {
128135
args.push('--continue');
129136
}
130137
}
@@ -163,6 +170,13 @@ function failClosedUnsupportedRunConfig(options: BuildProviderCommandOptions): v
163170
}
164171
}
165172

173+
function extractSessionId(line: string): string | null {
174+
const event = tryParseJson(line.trim());
175+
if (!isRecord(event)) return null;
176+
const sessionId = getString(event, 'session_id');
177+
return sessionId?.trim() || null;
178+
}
179+
166180
function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[] {
167181
const features = optionFeatures(options);
168182
const warnings: WarningMetadata[] = [];
@@ -273,6 +287,7 @@ export const claudeAdapter: ProviderAdapter = {
273287
defaultMinLevel: 'level1',
274288
detectCliFeatures,
275289
buildCommand,
290+
extractSessionId,
276291
parseEvent: parseClaudeEvent,
277292
createParserState: () => createParserState('claude'),
278293
resolveModelSpec,

src/agent-cli-provider/adapters/codex.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getString, isRecord, tryParseJson } from '../json';
12
import { appendJsonSchemaPrompt, writeStrictOutputSchemaFile } from '../schema';
23
import {
34
type BuildProviderCommandOptions,
@@ -18,7 +19,6 @@ import {
1819
createParserState,
1920
optionFeatures,
2021
resolveModelSpecWithConfig,
21-
unsupportedSessionControlWarnings,
2222
validateModelIdFromCatalog,
2323
warning,
2424
} from './common';
@@ -58,10 +58,18 @@ function detectCliFeatures(helpText?: string | null): CodexCliFeatures {
5858
supportsConfigOverride: supports(help, /--config\b/),
5959
supportsModel: supports(help, /\s-m\b/) || supports(help, /--model\b/),
6060
supportsSkipGitRepoCheck: supports(help, /--skip-git-repo-check\b/),
61+
supportsResume: supports(help, /\bresume\b/),
6162
unknown,
6263
};
6364
}
6465

66+
function extractSessionId(line: string): string | null {
67+
const event = tryParseJson(line.trim());
68+
if (!isRecord(event) || getString(event, 'type') !== 'thread.started') return null;
69+
const sessionId = getString(event, 'thread_id');
70+
return sessionId?.trim() || null;
71+
}
72+
6573
function addOutputArgs(args: string[], options: BuildProviderCommandOptions): void {
6674
const features = optionFeatures(options);
6775
if (
@@ -121,7 +129,25 @@ function applySchemaArgs(
121129

122130
function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[] {
123131
const features = optionFeatures(options);
124-
const warnings: WarningMetadata[] = unsupportedSessionControlWarnings('codex', options);
132+
const warnings: WarningMetadata[] = [];
133+
if (options.continueSession) {
134+
warnings.push(
135+
warning(
136+
'codex',
137+
'unsupported-session-control',
138+
'Codex requires an explicit session ID; ignoring continueSession.'
139+
)
140+
);
141+
}
142+
if (options.resumeSessionId && features.supportsResume === false) {
143+
warnings.push(
144+
warning(
145+
'codex',
146+
'codex-session-resume-unsupported',
147+
'Codex CLI does not support exec resume; starting a fresh session.'
148+
)
149+
);
150+
}
125151
if (options.autoApprove && features.supportsAutoApprove === false) {
126152
warnings.push(
127153
warning(
@@ -153,16 +179,33 @@ function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[
153179
}
154180

155181
function buildCommand(context: string, options: BuildProviderCommandOptions = {}): CommandSpec {
182+
if (options.resumeSessionId && optionFeatures(options).supportsResume === false) {
183+
throw new Error(
184+
'Codex CLI cannot safely run continuation context because this installation lacks exec resume.'
185+
);
186+
}
156187
const args: string[] = ['exec'];
157188
const cleanup: string[] = [];
189+
const resumeSessionId =
190+
options.resumeSessionId && optionFeatures(options).supportsResume !== false
191+
? options.resumeSessionId
192+
: null;
193+
if (resumeSessionId) {
194+
args.push('resume');
195+
}
158196

159197
addOutputArgs(args, options);
160198
addModelArgs(args, options);
161-
addCwdArgs(args, options);
199+
if (!resumeSessionId) {
200+
addCwdArgs(args, options);
201+
}
162202
addAutoApproveArgs(args, options);
163203
addSkipGitArgs(args, options);
164204
const finalContext = applySchemaArgs(args, cleanup, context, options);
165205

206+
if (resumeSessionId) {
207+
args.push(resumeSessionId);
208+
}
166209
args.push(finalContext);
167210

168211
return commandSpec({
@@ -216,6 +259,7 @@ export const codexAdapter: ProviderAdapter = {
216259
defaultMinLevel: 'level1',
217260
detectCliFeatures,
218261
buildCommand,
262+
extractSessionId,
219263
parseEvent: parseCodexEvent,
220264
createParserState: () => createParserState('codex'),
221265
resolveModelSpec,

src/agent-cli-provider/adapters/common.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export function unsupportedSessionControlWarnings(
6464
warning(
6565
provider,
6666
'unsupported-session-control',
67-
'resume/continue is only supported for Claude CLI; ignoring.'
67+
`Provider ${provider} does not support resume/continue session control; ignoring.`
6868
),
6969
];
7070
}

src/agent-cli-provider/adapters/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ export function parseProviderChunk(
7979
return events;
8080
}
8181

82+
export function extractProviderSessionId(
83+
providerName: KnownProviderName | string,
84+
line: string
85+
): string | null {
86+
const adapter = getProviderAdapter(providerName || 'claude');
87+
const content = stripTimestampPrefix(line);
88+
if (!content || !adapter.extractSessionId) return null;
89+
return adapter.extractSessionId(content);
90+
}
91+
8292
export function resolveModelSpec(
8393
providerName: KnownProviderName | string,
8494
level: ModelLevel,

src/agent-cli-provider/contract-options.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const CLI_FEATURE_FIELDS = [
4343
'supportsNoAskUser',
4444
'supportsAddDir',
4545
'supportsMcpConfig',
46+
'supportsResume',
4647
'supportsBundledRunner',
4748
'supportsAcpStdio',
4849
'supportsPromptImages',

src/agent-cli-provider/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export {
33
classifyProviderError,
44
detectProviderFatalError,
55
detectProviderStreamingModeError,
6+
extractProviderSessionId,
67
getProviderAdapter,
78
listProviderAdapters,
89
parseProviderChunk,

src/agent-cli-provider/provider-registry.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface ProviderCapabilities {
1919
readonly streamJson: ProviderCapabilityState;
2020
readonly thinkingMode: ProviderCapabilityState;
2121
readonly reasoningEffort: ProviderCapabilityState;
22+
readonly sessionResume: ProviderCapabilityState;
2223
}
2324

2425
interface FixedProviderCommandSpec {
@@ -93,13 +94,22 @@ export interface ProviderRegistryEntry {
9394
}
9495

9596
const STANDARD_CAPABILITIES: Readonly<
96-
Pick<ProviderCapabilities, 'dockerIsolation' | 'worktreeIsolation' | 'mcpServers' | 'streamJson' | 'thinkingMode'>
97+
Pick<
98+
ProviderCapabilities,
99+
| 'dockerIsolation'
100+
| 'worktreeIsolation'
101+
| 'mcpServers'
102+
| 'streamJson'
103+
| 'thinkingMode'
104+
| 'sessionResume'
105+
>
97106
> = {
98107
dockerIsolation: true,
99108
worktreeIsolation: true,
100109
mcpServers: true,
101110
streamJson: true,
102111
thinkingMode: true,
112+
sessionResume: false,
103113
};
104114

105115
const CLAUDE_DOCKER_ENV_PASSTHROUGH = [
@@ -161,6 +171,7 @@ export const providerRegistry = [
161171
...STANDARD_CAPABILITIES,
162172
jsonSchema: true,
163173
reasoningEffort: true,
174+
sessionResume: true,
164175
},
165176
docs: {
166177
label: 'Claude',
@@ -197,6 +208,7 @@ export const providerRegistry = [
197208
...STANDARD_CAPABILITIES,
198209
jsonSchema: true,
199210
reasoningEffort: true,
211+
sessionResume: true,
200212
},
201213
docs: {
202214
label: 'Codex',

0 commit comments

Comments
 (0)