@the-open-engine/zeroshot 6.9.1 → 6.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/cli/commands/inspect.js +1 -0
  2. package/cli/index.js +17 -1
  3. package/cluster-hooks/block-ask-user-question.py +2 -3
  4. package/cluster-hooks/block-dangerous-git.py +2 -3
  5. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  6. package/lib/agent-cli-provider/adapters/claude.js +57 -3
  7. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  8. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  9. package/lib/agent-cli-provider/adapters/codex.js +32 -2
  10. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  11. package/lib/agent-cli-provider/adapters/common.js +1 -1
  12. package/lib/agent-cli-provider/adapters/common.js.map +1 -1
  13. package/lib/agent-cli-provider/adapters/index.d.ts +1 -0
  14. package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
  15. package/lib/agent-cli-provider/adapters/index.js +8 -0
  16. package/lib/agent-cli-provider/adapters/index.js.map +1 -1
  17. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  18. package/lib/agent-cli-provider/contract-options.js +3 -0
  19. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  20. package/lib/agent-cli-provider/index.d.ts +1 -1
  21. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  22. package/lib/agent-cli-provider/index.js +2 -1
  23. package/lib/agent-cli-provider/index.js.map +1 -1
  24. package/lib/agent-cli-provider/provider-registry.d.ts +9 -0
  25. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  26. package/lib/agent-cli-provider/provider-registry.js +3 -0
  27. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  28. package/lib/agent-cli-provider/types.d.ts +10 -2
  29. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  30. package/lib/agent-cli-provider/types.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/agent/agent-context-builder.js +90 -4
  33. package/src/agent/agent-context-sources.js +20 -2
  34. package/src/agent/agent-lifecycle.js +43 -8
  35. package/src/agent/agent-task-executor.js +557 -314
  36. package/src/agent/guidance-queue.js +29 -7
  37. package/src/agent/provider-session.js +277 -0
  38. package/src/agent-cli-provider/adapters/claude.ts +64 -4
  39. package/src/agent-cli-provider/adapters/codex.ts +47 -3
  40. package/src/agent-cli-provider/adapters/common.ts +1 -1
  41. package/src/agent-cli-provider/adapters/index.ts +10 -0
  42. package/src/agent-cli-provider/contract-options.ts +7 -0
  43. package/src/agent-cli-provider/index.ts +1 -0
  44. package/src/agent-cli-provider/provider-registry.ts +13 -1
  45. package/src/agent-cli-provider/types.ts +13 -6
  46. package/src/agent-wrapper.js +44 -8
  47. package/src/claude-task-runner.js +153 -44
  48. package/src/ledger-sequence.js +63 -0
  49. package/src/ledger.js +114 -29
  50. package/src/orchestrator.js +40 -2
  51. package/src/task-spawn-cleanup-ownership.js +82 -0
  52. package/src/worktree-claude-config.js +193 -85
  53. package/task-lib/attachable-watcher.js +101 -253
  54. package/task-lib/command-spec-cleanup.js +219 -0
  55. package/task-lib/commands/clean.js +35 -5
  56. package/task-lib/commands/get-task-id-by-spawn-token.js +10 -0
  57. package/task-lib/commands/kill.js +117 -4
  58. package/task-lib/commands/resume.js +29 -6
  59. package/task-lib/commands/status.js +4 -0
  60. package/task-lib/provider-helper-runtime.js +1 -0
  61. package/task-lib/provider-session-capture.js +138 -0
  62. package/task-lib/runner.js +70 -5
  63. package/task-lib/store.js +126 -12
  64. package/task-lib/watcher-output-runtime.js +284 -0
  65. package/task-lib/watcher.js +131 -242
@@ -1,5 +1,6 @@
1
1
  const GUIDANCE_BLOCK_START = '<<GUIDANCE_QUEUE_START>>';
2
2
  const GUIDANCE_BLOCK_END = '<<GUIDANCE_QUEUE_END>>';
3
+ const { compareMessageSequences } = require('../ledger-sequence');
3
4
 
4
5
  function formatGuidanceMessage(message) {
5
6
  const timestamp = Number.isFinite(message.timestamp)
@@ -21,10 +22,19 @@ function formatGuidanceMessage(message) {
21
22
  return formatted.trimEnd();
22
23
  }
23
24
 
24
- function formatGuidanceBlock(messages) {
25
+ function orderGuidanceMessages(messages, orderBySequence) {
26
+ return messages.slice().sort((a, b) => {
27
+ if (orderBySequence) {
28
+ return compareMessageSequences(a.sequence || '0', b.sequence || '0');
29
+ }
30
+ return (a.timestamp || 0) - (b.timestamp || 0);
31
+ });
32
+ }
33
+
34
+ function formatGuidanceBlock(messages, { orderBySequence = false } = {}) {
25
35
  if (!Array.isArray(messages) || messages.length === 0) return '';
26
36
 
27
- const ordered = messages.slice().sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
37
+ const ordered = orderGuidanceMessages(messages, orderBySequence);
28
38
 
29
39
  let block = '## Guidance (Queued)\n\n';
30
40
  block += `${GUIDANCE_BLOCK_START}\n`;
@@ -40,7 +50,15 @@ function formatGuidanceBlock(messages) {
40
50
  return block;
41
51
  }
42
52
 
43
- function collectQueuedGuidance({ messageBus, clusterId, agentId, lastDeliveredAt, limit }) {
53
+ function collectQueuedGuidance({
54
+ messageBus,
55
+ clusterId,
56
+ agentId,
57
+ afterId,
58
+ throughId,
59
+ lastDeliveredAt,
60
+ limit,
61
+ }) {
44
62
  if (!messageBus) {
45
63
  throw new Error('collectQueuedGuidance: messageBus is required');
46
64
  }
@@ -54,19 +72,23 @@ function collectQueuedGuidance({ messageBus, clusterId, agentId, lastDeliveredAt
54
72
  const messages = messageBus.queryGuidanceMailbox({
55
73
  cluster_id: clusterId,
56
74
  target_agent_id: agentId,
75
+ afterId,
76
+ throughId,
57
77
  lastDeliveredAt,
58
78
  limit,
59
79
  });
60
80
 
61
81
  if (!messages.length) {
62
- return { messages: [], latestTimestamp: null, guidanceBlock: '' };
82
+ return { messages: [], latestSequence: null, latestTimestamp: null, guidanceBlock: '' };
63
83
  }
64
84
 
65
- const ordered = messages.slice().sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
85
+ const sequenceBounded = afterId !== undefined || throughId !== undefined;
86
+ const ordered = orderGuidanceMessages(messages, sequenceBounded);
87
+ const latestSequence = ordered[ordered.length - 1].sequence;
66
88
  const latestTimestamp = ordered[ordered.length - 1].timestamp;
67
- const guidanceBlock = formatGuidanceBlock(ordered);
89
+ const guidanceBlock = formatGuidanceBlock(ordered, { orderBySequence: sequenceBounded });
68
90
 
69
- return { messages: ordered, latestTimestamp, guidanceBlock };
91
+ return { messages: ordered, latestSequence, latestTimestamp, guidanceBlock };
70
92
  }
71
93
 
72
94
  module.exports = {
@@ -0,0 +1,277 @@
1
+ const crypto = require('crypto');
2
+ const path = require('path');
3
+
4
+ const { normalizeProviderName, providerSupportsCapability } = require('../../lib/provider-names');
5
+ const { tryCanonicalMessageSequence } = require('../ledger-sequence');
6
+
7
+ const DURABLE_SESSION_BOUNDARY_EVENTS = new Set([
8
+ 'TASK_STARTED',
9
+ 'TASK_COMPLETED',
10
+ 'TASK_FAILED',
11
+ 'RETRY_SCHEDULED',
12
+ 'AGENT_RESTART_ATTEMPT',
13
+ ]);
14
+ const RESTORABLE_AGENT_STATES = new Set(['idle', 'stopped', 'completed']);
15
+
16
+ function normalizeNonEmptyString(value) {
17
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
18
+ }
19
+
20
+ function normalizeAbsolutePath(value) {
21
+ const normalized = normalizeNonEmptyString(value);
22
+ return normalized ? path.resolve(normalized) : null;
23
+ }
24
+
25
+ function normalizeCursor(value) {
26
+ return tryCanonicalMessageSequence(value);
27
+ }
28
+
29
+ function normalizeNullableCursor(value) {
30
+ return value === null ? null : normalizeCursor(value);
31
+ }
32
+
33
+ function normalizePromptIdentity(value) {
34
+ if (value === null) {
35
+ return null;
36
+ }
37
+ return typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value) ? value : undefined;
38
+ }
39
+
40
+ function promptIdentity(value) {
41
+ if (value === null || value === undefined) {
42
+ return null;
43
+ }
44
+ return `sha256:${crypto.createHash('sha256').update(String(value)).digest('hex')}`;
45
+ }
46
+
47
+ function supportsSessionResume(providerName) {
48
+ try {
49
+ return providerSupportsCapability(providerName, 'sessionResume');
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ function normalizeProviderSession(value) {
56
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
57
+ return null;
58
+ }
59
+
60
+ const provider = normalizeProviderName(value.provider);
61
+ const sessionId = normalizeNonEmptyString(value.sessionId);
62
+ const agentId = normalizeNonEmptyString(value.agentId);
63
+ const taskId = normalizeNonEmptyString(value.taskId);
64
+ const generation = value.generation;
65
+ const cwd = normalizeAbsolutePath(value.cwd);
66
+ const worktreePath =
67
+ value.worktreePath === null ? null : normalizeAbsolutePath(value.worktreePath);
68
+ const contextSequence = normalizeCursor(value.contextSequence);
69
+ const guidanceSequence = normalizeNullableCursor(value.guidanceSequence);
70
+ const normalizedPromptIdentity = normalizePromptIdentity(value.promptIdentity);
71
+
72
+ if (
73
+ !provider ||
74
+ !sessionId ||
75
+ !agentId ||
76
+ !taskId ||
77
+ !Number.isInteger(generation) ||
78
+ generation < 1 ||
79
+ !cwd ||
80
+ !Object.hasOwn(value, 'worktreePath') ||
81
+ (value.worktreePath !== null && !worktreePath) ||
82
+ contextSequence === null ||
83
+ !Object.hasOwn(value, 'guidanceSequence') ||
84
+ (value.guidanceSequence !== null && guidanceSequence === null) ||
85
+ !Object.hasOwn(value, 'promptIdentity') ||
86
+ normalizedPromptIdentity === undefined ||
87
+ !supportsSessionResume(provider)
88
+ ) {
89
+ return null;
90
+ }
91
+
92
+ return {
93
+ provider,
94
+ sessionId,
95
+ agentId,
96
+ taskId,
97
+ generation,
98
+ cwd,
99
+ worktreePath,
100
+ contextSequence,
101
+ guidanceSequence,
102
+ promptIdentity: normalizedPromptIdentity,
103
+ };
104
+ }
105
+
106
+ function agentWorkspaceProvenance(agent) {
107
+ const worktreePath = agent?.worktree?.enabled ? normalizeAbsolutePath(agent.worktree.path) : null;
108
+ const cwd = normalizeAbsolutePath(agent?.config?.cwd || worktreePath || process.cwd());
109
+ return { cwd, worktreePath };
110
+ }
111
+
112
+ function agentCanReuseSession(agent, providerName) {
113
+ return !agent?.isolation?.enabled && supportsSessionResume(providerName);
114
+ }
115
+
116
+ function sessionMatchesAgent(session, agent, providerName, expectedGeneration) {
117
+ const provider = normalizeProviderName(providerName);
118
+ const workspace = agentWorkspaceProvenance(agent);
119
+ return (
120
+ agentCanReuseSession(agent, provider) &&
121
+ session.provider === provider &&
122
+ session.agentId === agent?.id &&
123
+ session.generation === expectedGeneration &&
124
+ session.cwd === workspace.cwd &&
125
+ session.worktreePath === workspace.worktreePath
126
+ );
127
+ }
128
+
129
+ function resolveAgentProviderSession(agent, providerName) {
130
+ const stored = normalizeProviderSession(agent?.providerSession);
131
+ const expectedGeneration = Number.isInteger(agent?.iteration) ? agent.iteration - 1 : -1;
132
+
133
+ if (!stored || !sessionMatchesAgent(stored, agent, providerName, expectedGeneration)) {
134
+ if (agent) {
135
+ if (agent.providerSession) {
136
+ agent.lastGuidanceAppliedId = null;
137
+ }
138
+ agent.providerSession = null;
139
+ }
140
+ return null;
141
+ }
142
+
143
+ return stored;
144
+ }
145
+
146
+ function resolveAgentResumeSessionId(agent, providerName) {
147
+ return resolveAgentProviderSession(agent, providerName)?.sessionId || null;
148
+ }
149
+
150
+ function validateCompletedResumeIdentity(taskInfo) {
151
+ const requestedSessionId = normalizeNonEmptyString(taskInfo?.requestedResumeSessionId);
152
+ if (!requestedSessionId) {
153
+ return null;
154
+ }
155
+ if (taskInfo?.resumeIdentityVerified !== true) {
156
+ return 'Provider continuation identity was not durably verified';
157
+ }
158
+ if (taskInfo?.sessionIdConflict === true) {
159
+ return 'Provider continuation emitted conflicting session identities';
160
+ }
161
+
162
+ const capturedSessionId = normalizeNonEmptyString(taskInfo?.sessionId);
163
+ if (capturedSessionId === requestedSessionId) {
164
+ return null;
165
+ }
166
+
167
+ return capturedSessionId
168
+ ? 'Provider continuation returned a different session identity'
169
+ : 'Provider continuation did not confirm the requested session identity';
170
+ }
171
+
172
+ function providerSessionFromCompletedTask({
173
+ agent,
174
+ providerName,
175
+ taskInfo,
176
+ logicalSuccess = true,
177
+ }) {
178
+ const provider = normalizeProviderName(providerName);
179
+ if (!logicalSuccess || !agentCanReuseSession(agent, provider)) {
180
+ return null;
181
+ }
182
+ if (!taskInfo || taskInfo.status !== 'completed') {
183
+ return null;
184
+ }
185
+ if (normalizeProviderName(taskInfo.provider) !== provider) {
186
+ return null;
187
+ }
188
+ if (taskInfo.sessionIdConflict === true) {
189
+ return null;
190
+ }
191
+ if (validateCompletedResumeIdentity(taskInfo)) {
192
+ return null;
193
+ }
194
+
195
+ const sessionId = normalizeNonEmptyString(taskInfo.sessionId);
196
+ const taskId = normalizeNonEmptyString(taskInfo.id);
197
+ const generation = agent?.iteration;
198
+ const agentId = normalizeNonEmptyString(agent?.id);
199
+ const workspace = agentWorkspaceProvenance(agent);
200
+ return normalizeProviderSession({
201
+ provider,
202
+ sessionId,
203
+ agentId,
204
+ taskId,
205
+ generation,
206
+ ...workspace,
207
+ contextSequence: agent?.currentContextSequence,
208
+ guidanceSequence: agent?.currentGuidanceSequence ?? null,
209
+ promptIdentity: agent?.currentPromptIdentity ?? null,
210
+ });
211
+ }
212
+
213
+ function updateAgentProviderSession(agent, value) {
214
+ if (agent.providerSession && value === null) {
215
+ agent.lastGuidanceAppliedId = null;
216
+ }
217
+ const session = normalizeProviderSession(value);
218
+ agent.providerSession = session;
219
+ return session;
220
+ }
221
+
222
+ function readLastDurableSessionBoundary(messageBus, clusterId, agentId) {
223
+ const lifecycle = messageBus.query({
224
+ cluster_id: clusterId,
225
+ topic: 'AGENT_LIFECYCLE',
226
+ sender: agentId,
227
+ afterId: '0',
228
+ });
229
+ return lifecycle
230
+ .filter((message) => DURABLE_SESSION_BOUNDARY_EVENTS.has(message.content?.data?.event))
231
+ .at(-1);
232
+ }
233
+
234
+ function restoreAgentProviderSession({ agent, savedState, messageBus, clusterId }) {
235
+ const session = normalizeProviderSession(savedState?.providerSession);
236
+ if (!session || !RESTORABLE_AGENT_STATES.has(savedState?.state)) {
237
+ return null;
238
+ }
239
+ if (
240
+ !Object.hasOwn(savedState, 'lastGuidanceAppliedId') ||
241
+ normalizeNullableCursor(savedState.lastGuidanceAppliedId) !== session.guidanceSequence
242
+ ) {
243
+ return null;
244
+ }
245
+ if (!sessionMatchesAgent(session, agent, session.provider, savedState.iteration)) {
246
+ return null;
247
+ }
248
+
249
+ const boundary = readLastDurableSessionBoundary(messageBus, clusterId, agent.id);
250
+ const data = boundary?.content?.data;
251
+ if (
252
+ data?.event !== 'TASK_COMPLETED' ||
253
+ data.taskId !== session.taskId ||
254
+ data.iteration !== session.generation ||
255
+ normalizeProviderName(data.provider) !== session.provider ||
256
+ normalizeCursor(data.contextSequence) !== session.contextSequence ||
257
+ normalizeNullableCursor(data.guidanceSequence) !== session.guidanceSequence ||
258
+ data.promptIdentity !== session.promptIdentity
259
+ ) {
260
+ return null;
261
+ }
262
+
263
+ return session;
264
+ }
265
+
266
+ module.exports = {
267
+ agentWorkspaceProvenance,
268
+ normalizeProviderSession,
269
+ promptIdentity,
270
+ providerSessionFromCompletedTask,
271
+ resolveAgentProviderSession,
272
+ resolveAgentResumeSessionId,
273
+ restoreAgentProviderSession,
274
+ supportsSessionResume,
275
+ updateAgentProviderSession,
276
+ validateCompletedResumeIdentity,
277
+ };
@@ -1,4 +1,5 @@
1
- import { stringifyJson } from '../json';
1
+ import { getString, isRecord, stringifyJson, tryParseJson } from '../json';
2
+ import { contractError } from '../contract-errors';
2
3
  import {
3
4
  type BuildProviderCommandOptions,
4
5
  type ClaudeCliFeatures,
@@ -66,6 +67,11 @@ function detectCliFeatures(helpText?: string | null): ClaudeCliFeatures {
66
67
  supportsVerbose: unknown ? true : /--verbose/.test(help),
67
68
  supportsModel: unknown ? true : /--model/.test(help),
68
69
  supportsEffort: unknown ? true : /--effort/.test(help),
70
+ // Settings carry mandatory safety hooks, so an unprobed or old CLI must
71
+ // fail closed before a provider process is spawned.
72
+ supportsSettings: !unknown && /--settings/.test(help),
73
+ supportsMcpConfig: !unknown && /--mcp-config/.test(help),
74
+ supportsResume: unknown ? true : /--resume/.test(help),
69
75
  unknown,
70
76
  };
71
77
  }
@@ -115,15 +121,62 @@ function addAutoApproveArgs(args: string[], options: BuildProviderCommandOptions
115
121
  }
116
122
 
117
123
  function addSessionArgs(args: string[], options: BuildProviderCommandOptions): void {
118
- 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) {
119
131
  args.push('--resume', options.resumeSessionId);
120
132
  return;
121
133
  }
122
- if (options.continueSession) {
134
+ if (options.continueSession && features.supportsResume !== false) {
123
135
  args.push('--continue');
124
136
  }
125
137
  }
126
138
 
139
+ function addSettingsArgs(args: string[], options: BuildProviderCommandOptions): void {
140
+ const settingsPath = options.claudeSettingsFile?.trim();
141
+ if (settingsPath) {
142
+ args.push('--settings', settingsPath);
143
+ }
144
+ }
145
+
146
+ function addMcpConfigArgs(args: string[], options: BuildProviderCommandOptions): void {
147
+ if (!options.mcpConfig?.length) return;
148
+ args.push('--mcp-config', ...options.mcpConfig);
149
+ }
150
+
151
+ function failClosedUnsupportedRunConfig(options: BuildProviderCommandOptions): void {
152
+ const features = optionFeatures(options);
153
+ if (options.claudeSettingsFile?.trim() && features.supportsSettings === false) {
154
+ throw contractError({
155
+ code: 'invalid-field',
156
+ field: 'options.cliFeatures.supportsSettings',
157
+ exitCode: 2,
158
+ message:
159
+ 'Claude CLI does not advertise --settings support required for Zeroshot safety hooks. Upgrade Claude Code before running this task.',
160
+ });
161
+ }
162
+ if (options.mcpConfig?.length && features.supportsMcpConfig === false) {
163
+ throw contractError({
164
+ code: 'invalid-field',
165
+ field: 'options.cliFeatures.supportsMcpConfig',
166
+ exitCode: 2,
167
+ message:
168
+ 'Claude CLI does not advertise --mcp-config support required to preserve repository MCP tools. Upgrade Claude Code before running this task.',
169
+ });
170
+ }
171
+ }
172
+
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
+
127
180
  function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[] {
128
181
  const features = optionFeatures(options);
129
182
  const warnings: WarningMetadata[] = [];
@@ -171,15 +224,21 @@ function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[
171
224
  }
172
225
 
173
226
  function buildCommand(context: string, options: BuildProviderCommandOptions = {}): CommandSpec {
227
+ failClosedUnsupportedRunConfig(options);
174
228
  const { command, args: commandPrefix } = resolveClaudeCommand();
175
- const args: string[] = [...commandPrefix, '--print', '--input-format', 'text'];
229
+ const args: string[] = [...commandPrefix, '--print'];
176
230
  const authEnv = options.authEnv ?? {};
177
231
 
232
+ // --mcp-config is variadic. A following option terminates its values before
233
+ // the positional prompt.
234
+ addMcpConfigArgs(args, options);
235
+ args.push('--input-format', 'text');
178
236
  addOutputArgs(args, options);
179
237
  addSchemaArgs(args, options);
180
238
  addModelArgs(args, options);
181
239
  addAutoApproveArgs(args, options);
182
240
  addSessionArgs(args, options);
241
+ addSettingsArgs(args, options);
183
242
 
184
243
  args.push(context);
185
244
 
@@ -228,6 +287,7 @@ export const claudeAdapter: ProviderAdapter = {
228
287
  defaultMinLevel: 'level1',
229
288
  detectCliFeatures,
230
289
  buildCommand,
290
+ extractSessionId,
231
291
  parseEvent: parseClaudeEvent,
232
292
  createParserState: () => createParserState('claude'),
233
293
  resolveModelSpec,
@@ -1,3 +1,4 @@
1
+ import { getString, isRecord, tryParseJson } from '../json';
1
2
  import { appendJsonSchemaPrompt, writeStrictOutputSchemaFile } from '../schema';
2
3
  import {
3
4
  type BuildProviderCommandOptions,
@@ -18,7 +19,6 @@ import {
18
19
  createParserState,
19
20
  optionFeatures,
20
21
  resolveModelSpecWithConfig,
21
- unsupportedSessionControlWarnings,
22
22
  validateModelIdFromCatalog,
23
23
  warning,
24
24
  } from './common';
@@ -58,10 +58,18 @@ function detectCliFeatures(helpText?: string | null): CodexCliFeatures {
58
58
  supportsConfigOverride: supports(help, /--config\b/),
59
59
  supportsModel: supports(help, /\s-m\b/) || supports(help, /--model\b/),
60
60
  supportsSkipGitRepoCheck: supports(help, /--skip-git-repo-check\b/),
61
+ supportsResume: supports(help, /\bresume\b/),
61
62
  unknown,
62
63
  };
63
64
  }
64
65
 
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
+
65
73
  function addOutputArgs(args: string[], options: BuildProviderCommandOptions): void {
66
74
  const features = optionFeatures(options);
67
75
  if (
@@ -121,7 +129,25 @@ function applySchemaArgs(
121
129
 
122
130
  function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[] {
123
131
  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
+ }
125
151
  if (options.autoApprove && features.supportsAutoApprove === false) {
126
152
  warnings.push(
127
153
  warning(
@@ -153,16 +179,33 @@ function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[
153
179
  }
154
180
 
155
181
  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
+ }
156
187
  const args: string[] = ['exec'];
157
188
  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
+ }
158
196
 
159
197
  addOutputArgs(args, options);
160
198
  addModelArgs(args, options);
161
- addCwdArgs(args, options);
199
+ if (!resumeSessionId) {
200
+ addCwdArgs(args, options);
201
+ }
162
202
  addAutoApproveArgs(args, options);
163
203
  addSkipGitArgs(args, options);
164
204
  const finalContext = applySchemaArgs(args, cleanup, context, options);
165
205
 
206
+ if (resumeSessionId) {
207
+ args.push(resumeSessionId);
208
+ }
166
209
  args.push(finalContext);
167
210
 
168
211
  return commandSpec({
@@ -216,6 +259,7 @@ export const codexAdapter: ProviderAdapter = {
216
259
  defaultMinLevel: 'level1',
217
260
  detectCliFeatures,
218
261
  buildCommand,
262
+ extractSessionId,
219
263
  parseEvent: parseCodexEvent,
220
264
  createParserState: () => createParserState('codex'),
221
265
  resolveModelSpec,
@@ -64,7 +64,7 @@ export function unsupportedSessionControlWarnings(
64
64
  warning(
65
65
  provider,
66
66
  'unsupported-session-control',
67
- 'resume/continue is only supported for Claude CLI; ignoring.'
67
+ `Provider ${provider} does not support resume/continue session control; ignoring.`
68
68
  ),
69
69
  ];
70
70
  }
@@ -79,6 +79,16 @@ export function parseProviderChunk(
79
79
  return events;
80
80
  }
81
81
 
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
+
82
92
  export function resolveModelSpec(
83
93
  providerName: KnownProviderName | string,
84
94
  level: ModelLevel,
@@ -23,6 +23,7 @@ const CLI_FEATURE_FIELDS = [
23
23
  'supportsVerbose',
24
24
  'supportsModel',
25
25
  'supportsEffort',
26
+ 'supportsSettings',
26
27
  'supportsJson',
27
28
  'supportsOutputSchema',
28
29
  'supportsDir',
@@ -42,6 +43,7 @@ const CLI_FEATURE_FIELDS = [
42
43
  'supportsNoAskUser',
43
44
  'supportsAddDir',
44
45
  'supportsMcpConfig',
46
+ 'supportsResume',
45
47
  'supportsBundledRunner',
46
48
  'supportsAcpStdio',
47
49
  'supportsPromptImages',
@@ -208,6 +210,11 @@ function normalizeBuildOptions(value: Record<string, unknown>): BuildProviderCom
208
210
  'continueSession',
209
211
  optionalBooleanValue(value.continueSession, 'options.continueSession')
210
212
  );
213
+ addDefined(
214
+ result,
215
+ 'claudeSettingsFile',
216
+ optionalStringValue(value.claudeSettingsFile, 'options.claudeSettingsFile')
217
+ );
211
218
  addDefined(result, 'cliFeatures', optionalCliFeatures(value.cliFeatures));
212
219
  addDefined(result, 'mcpConfig', optionalMcpConfig(value.mcpConfig));
213
220
  addDefined(
@@ -3,6 +3,7 @@ export {
3
3
  classifyProviderError,
4
4
  detectProviderFatalError,
5
5
  detectProviderStreamingModeError,
6
+ extractProviderSessionId,
6
7
  getProviderAdapter,
7
8
  listProviderAdapters,
8
9
  parseProviderChunk,
@@ -19,6 +19,7 @@ export interface ProviderCapabilities {
19
19
  readonly streamJson: ProviderCapabilityState;
20
20
  readonly thinkingMode: ProviderCapabilityState;
21
21
  readonly reasoningEffort: ProviderCapabilityState;
22
+ readonly sessionResume: ProviderCapabilityState;
22
23
  }
23
24
 
24
25
  interface FixedProviderCommandSpec {
@@ -93,13 +94,22 @@ export interface ProviderRegistryEntry {
93
94
  }
94
95
 
95
96
  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
+ >
97
106
  > = {
98
107
  dockerIsolation: true,
99
108
  worktreeIsolation: true,
100
109
  mcpServers: true,
101
110
  streamJson: true,
102
111
  thinkingMode: true,
112
+ sessionResume: false,
103
113
  };
104
114
 
105
115
  const CLAUDE_DOCKER_ENV_PASSTHROUGH = [
@@ -161,6 +171,7 @@ export const providerRegistry = [
161
171
  ...STANDARD_CAPABILITIES,
162
172
  jsonSchema: true,
163
173
  reasoningEffort: true,
174
+ sessionResume: true,
164
175
  },
165
176
  docs: {
166
177
  label: 'Claude',
@@ -197,6 +208,7 @@ export const providerRegistry = [
197
208
  ...STANDARD_CAPABILITIES,
198
209
  jsonSchema: true,
199
210
  reasoningEffort: true,
211
+ sessionResume: true,
200
212
  },
201
213
  docs: {
202
214
  label: 'Codex',