@the-open-engine/zeroshot 6.9.1 → 6.10.0

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 (55) hide show
  1. package/cli/commands/inspect.js +1 -0
  2. package/cli/index.js +1 -1
  3. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  4. package/lib/agent-cli-provider/adapters/claude.js +15 -2
  5. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  6. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  7. package/lib/agent-cli-provider/adapters/codex.js +32 -2
  8. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  9. package/lib/agent-cli-provider/adapters/common.js +1 -1
  10. package/lib/agent-cli-provider/adapters/common.js.map +1 -1
  11. package/lib/agent-cli-provider/adapters/index.d.ts +1 -0
  12. package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
  13. package/lib/agent-cli-provider/adapters/index.js +8 -0
  14. package/lib/agent-cli-provider/adapters/index.js.map +1 -1
  15. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  16. package/lib/agent-cli-provider/contract-options.js +1 -0
  17. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  18. package/lib/agent-cli-provider/index.d.ts +1 -1
  19. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  20. package/lib/agent-cli-provider/index.js +2 -1
  21. package/lib/agent-cli-provider/index.js.map +1 -1
  22. package/lib/agent-cli-provider/provider-registry.d.ts +9 -0
  23. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  24. package/lib/agent-cli-provider/provider-registry.js +3 -0
  25. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  26. package/lib/agent-cli-provider/types.d.ts +4 -0
  27. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  28. package/lib/agent-cli-provider/types.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/agent/agent-context-builder.js +90 -4
  31. package/src/agent/agent-context-sources.js +20 -2
  32. package/src/agent/agent-lifecycle.js +30 -2
  33. package/src/agent/agent-task-executor.js +31 -1
  34. package/src/agent/guidance-queue.js +29 -7
  35. package/src/agent/provider-session.js +277 -0
  36. package/src/agent-cli-provider/adapters/claude.ts +18 -3
  37. package/src/agent-cli-provider/adapters/codex.ts +47 -3
  38. package/src/agent-cli-provider/adapters/common.ts +1 -1
  39. package/src/agent-cli-provider/adapters/index.ts +10 -0
  40. package/src/agent-cli-provider/contract-options.ts +1 -0
  41. package/src/agent-cli-provider/index.ts +1 -0
  42. package/src/agent-cli-provider/provider-registry.ts +13 -1
  43. package/src/agent-cli-provider/types.ts +4 -0
  44. package/src/agent-wrapper.js +44 -8
  45. package/src/ledger-sequence.js +63 -0
  46. package/src/ledger.js +114 -29
  47. package/src/orchestrator.js +40 -2
  48. package/task-lib/attachable-watcher.js +27 -5
  49. package/task-lib/commands/resume.js +29 -6
  50. package/task-lib/commands/status.js +3 -0
  51. package/task-lib/provider-helper-runtime.js +1 -0
  52. package/task-lib/provider-session-capture.js +138 -0
  53. package/task-lib/runner.js +10 -2
  54. package/task-lib/store.js +58 -6
  55. package/task-lib/watcher.js +27 -5
@@ -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,4 @@
1
- import { stringifyJson } from '../json';
1
+ import { getString, isRecord, stringifyJson, tryParseJson } from '../json';
2
2
  import {
3
3
  type BuildProviderCommandOptions,
4
4
  type ClaudeCliFeatures,
@@ -66,6 +66,7 @@ function detectCliFeatures(helpText?: string | null): ClaudeCliFeatures {
66
66
  supportsVerbose: unknown ? true : /--verbose/.test(help),
67
67
  supportsModel: unknown ? true : /--model/.test(help),
68
68
  supportsEffort: unknown ? true : /--effort/.test(help),
69
+ supportsResume: unknown ? true : /--resume/.test(help),
69
70
  unknown,
70
71
  };
71
72
  }
@@ -115,15 +116,28 @@ function addAutoApproveArgs(args: string[], options: BuildProviderCommandOptions
115
116
  }
116
117
 
117
118
  function addSessionArgs(args: string[], options: BuildProviderCommandOptions): void {
118
- if (options.resumeSessionId) {
119
+ const features = optionFeatures(options);
120
+ if ((options.resumeSessionId || options.continueSession) && features.supportsResume === false) {
121
+ throw new Error(
122
+ 'Claude CLI cannot safely run continuation context because this installation lacks --resume.'
123
+ );
124
+ }
125
+ if (options.resumeSessionId && features.supportsResume !== false) {
119
126
  args.push('--resume', options.resumeSessionId);
120
127
  return;
121
128
  }
122
- if (options.continueSession) {
129
+ if (options.continueSession && features.supportsResume !== false) {
123
130
  args.push('--continue');
124
131
  }
125
132
  }
126
133
 
134
+ function extractSessionId(line: string): string | null {
135
+ const event = tryParseJson(line.trim());
136
+ if (!isRecord(event)) return null;
137
+ const sessionId = getString(event, 'session_id');
138
+ return sessionId?.trim() || null;
139
+ }
140
+
127
141
  function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[] {
128
142
  const features = optionFeatures(options);
129
143
  const warnings: WarningMetadata[] = [];
@@ -228,6 +242,7 @@ export const claudeAdapter: ProviderAdapter = {
228
242
  defaultMinLevel: 'level1',
229
243
  detectCliFeatures,
230
244
  buildCommand,
245
+ extractSessionId,
231
246
  parseEvent: parseClaudeEvent,
232
247
  createParserState: () => createParserState('claude'),
233
248
  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,
@@ -42,6 +42,7 @@ const CLI_FEATURE_FIELDS = [
42
42
  'supportsNoAskUser',
43
43
  'supportsAddDir',
44
44
  'supportsMcpConfig',
45
+ 'supportsResume',
45
46
  'supportsBundledRunner',
46
47
  'supportsAcpStdio',
47
48
  'supportsPromptImages',
@@ -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',
@@ -101,6 +101,7 @@ export interface ClaudeCliFeatures extends BaseCliFeatures {
101
101
  readonly supportsVerbose: boolean;
102
102
  readonly supportsModel: boolean;
103
103
  readonly supportsEffort: boolean;
104
+ readonly supportsResume: boolean;
104
105
  }
105
106
 
106
107
  export interface CodexCliFeatures extends BaseCliFeatures {
@@ -112,6 +113,7 @@ export interface CodexCliFeatures extends BaseCliFeatures {
112
113
  readonly supportsConfigOverride: boolean;
113
114
  readonly supportsModel: boolean;
114
115
  readonly supportsSkipGitRepoCheck: boolean;
116
+ readonly supportsResume: boolean;
115
117
  }
116
118
 
117
119
  export interface GeminiCliFeatures extends BaseCliFeatures {
@@ -212,6 +214,7 @@ export interface CliFeatureOverrides {
212
214
  readonly supportsNoAskUser?: boolean;
213
215
  readonly supportsAddDir?: boolean;
214
216
  readonly supportsMcpConfig?: boolean;
217
+ readonly supportsResume?: boolean;
215
218
  readonly supportsBundledRunner?: boolean;
216
219
  readonly supportsAcpStdio?: boolean;
217
220
  readonly supportsPromptImages?: boolean;
@@ -368,6 +371,7 @@ export interface ProviderAdapter {
368
371
  readonly defaultMinLevel: ModelLevel;
369
372
  detectCliFeatures(helpText?: string | null): ProviderCliFeatures;
370
373
  buildCommand(context: string, options?: BuildProviderCommandOptions): CommandSpec;
374
+ extractSessionId?(line: string): string | null;
371
375
  parseEvent(line: string, state: ProviderParserState): ProviderParseResult;
372
376
  createParserState(): ProviderParserState;
373
377
  resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec;
@@ -17,6 +17,11 @@ const { normalizeProviderName } = require('../lib/provider-names');
17
17
  const { getProvider } = require('./providers');
18
18
  const { buildContext } = require('./agent/agent-context-builder');
19
19
  const { collectQueuedGuidance } = require('./agent/guidance-queue');
20
+ const {
21
+ promptIdentity,
22
+ resolveAgentProviderSession,
23
+ updateAgentProviderSession,
24
+ } = require('./agent/provider-session');
20
25
  const { findMatchingTrigger, evaluateTrigger } = require('./agent/agent-trigger-evaluator');
21
26
  const { executeHook } = require('./agent/agent-hook-executor');
22
27
  const { injectInput: injectAgentInput } = require('./agent/agent-input-injector');
@@ -66,6 +71,11 @@ class AgentWrapper {
66
71
  this.currentTask = null;
67
72
  /** @type {string | null} */
68
73
  this.currentTaskId = null; // Track spawned task ID for resume capability
74
+ /** @type {{provider: string, sessionId: string, agentId: string, taskId: string, generation: number, cwd: string, worktreePath: string|null, contextSequence: string, guidanceSequence: string|null, promptIdentity: string|null} | null} */
75
+ this.providerSession = null; // Provider continuation state, owned by this logical agent only
76
+ this.currentContextSequence = '0';
77
+ this.currentGuidanceSequence = null;
78
+ this.currentPromptIdentity = null;
69
79
  /** @type {number | null} */
70
80
  this.processPid = null; // Track process PID for resource monitoring
71
81
  this.running = false;
@@ -76,7 +86,7 @@ class AgentWrapper {
76
86
  /** @type {number | null} */
77
87
  this.lastAgentStartTime = null; // Track when agent last began executing (for context filtering)
78
88
  /** @type {number | null} */
79
- this.lastGuidanceAppliedAt = null; // Track last queued guidance applied to prompt
89
+ this.lastGuidanceAppliedId = null; // Track last queued guidance sequence applied to prompt
80
90
 
81
91
  // LIVENESS DETECTION - Track output freshness to detect stuck agents
82
92
  /** @type {number | null} */
@@ -119,6 +129,7 @@ class AgentWrapper {
119
129
 
120
130
  this.testMode = options.testMode || false;
121
131
  this.quiet = options.quiet || false;
132
+ this.providerCliFeatures = options.providerCliFeatures || null;
122
133
 
123
134
  // ISOLATION SUPPORT - Run tasks inside Docker container
124
135
  this.isolation = options.isolation || null;
@@ -435,12 +446,29 @@ class AgentWrapper {
435
446
  */
436
447
  _buildContext(triggeringMessage) {
437
448
  const previousAgentStart = this.lastAgentStartTime;
449
+ const providerName = this._resolveProvider();
450
+ let providerSession = resolveAgentProviderSession(this, providerName);
451
+ if (providerSession && !this._providerSupportsSessionResume(providerName)) {
452
+ updateAgentProviderSession(this, null);
453
+ providerSession = null;
454
+ }
455
+ const contextMode = providerSession ? 'continuation' : 'full';
456
+ const selectedPrompt = this._selectPrompt();
457
+ const latestMessage = this.messageBus.findLast({
458
+ cluster_id: this.cluster.id,
459
+ orderBySequence: true,
460
+ });
461
+ this.currentContextSequence = latestMessage?.sequence || '0';
438
462
  const queuedGuidance = collectQueuedGuidance({
439
463
  messageBus: this.messageBus,
440
464
  clusterId: this.cluster.id,
441
465
  agentId: this.id,
442
- lastDeliveredAt: this.lastGuidanceAppliedAt,
466
+ afterId: this.lastGuidanceAppliedId,
467
+ throughId: this.currentContextSequence,
443
468
  });
469
+ this.currentGuidanceSequence =
470
+ queuedGuidance.latestSequence ?? this.lastGuidanceAppliedId ?? null;
471
+ this.currentPromptIdentity = promptIdentity(selectedPrompt);
444
472
  const context = buildContext({
445
473
  id: this.id,
446
474
  role: this.role,
@@ -451,27 +479,35 @@ class AgentWrapper {
451
479
  lastTaskEndTime: this.lastTaskEndTime,
452
480
  lastAgentStartTime: previousAgentStart,
453
481
  triggeringMessage,
454
- selectedPrompt: this._selectPrompt(),
482
+ selectedPrompt,
455
483
  queuedGuidance: queuedGuidance.guidanceBlock,
484
+ mode: contextMode,
485
+ continuationSequence: providerSession?.contextSequence,
486
+ contextThroughId: this.currentContextSequence,
487
+ previousPromptIdentity: providerSession?.promptIdentity,
488
+ currentPromptIdentity: this.currentPromptIdentity,
456
489
  // Pass isolation state for conditional git restriction
457
490
  worktree: this.worktree,
458
491
  isolation: this.isolation,
459
492
  });
460
493
 
461
494
  // Record when this iteration started so future "since: last_agent_start" filters work.
462
- const latestMessage = this.messageBus.findLast({ cluster_id: this.cluster.id });
463
495
  const latestTimestamp = latestMessage?.timestamp;
464
496
  const now = Date.now();
465
497
  this.lastAgentStartTime =
466
498
  typeof latestTimestamp === 'number' ? Math.max(now, latestTimestamp + 1) : now;
467
499
 
468
- if (queuedGuidance.latestTimestamp !== null) {
469
- this.lastGuidanceAppliedAt = queuedGuidance.latestTimestamp;
470
- }
471
-
472
500
  return context;
473
501
  }
474
502
 
503
+ _providerSupportsSessionResume(providerName) {
504
+ const override = this.providerCliFeatures?.[providerName];
505
+ if (override && Object.hasOwn(override, 'supportsResume')) {
506
+ return override.supportsResume === true;
507
+ }
508
+ return getProvider(providerName).getCliFeatures().supportsResume === true;
509
+ }
510
+
475
511
  /**
476
512
  * Spawn claude-zeroshots process and stream output via message bus
477
513
  * @private