@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,138 @@
1
+ import { extractProviderSessionId } from './provider-helper-runtime.js';
2
+
3
+ /**
4
+ * Capture a provider-owned session ID from one complete output line.
5
+ *
6
+ * The caller owns persistence so watcher variants can share parsing without
7
+ * importing each other's process lifecycle. Repeated IDs are idempotent; any
8
+ * differing ID makes the capture permanently ambiguous.
9
+ */
10
+ export function captureProviderSessionLine({
11
+ providerName,
12
+ line,
13
+ currentSessionId = null,
14
+ observedSessionIds = new Set(currentSessionId ? [currentSessionId] : []),
15
+ sessionIdConflict = false,
16
+ onCapture = () => {},
17
+ onConflict = () => {},
18
+ }) {
19
+ const sessionId = extractProviderSessionId(providerName, line);
20
+ if (!sessionId) {
21
+ return { currentSessionId, sessionIdConflict };
22
+ }
23
+
24
+ observedSessionIds.add(sessionId);
25
+ if (sessionIdConflict || observedSessionIds.size > 1) {
26
+ if (!sessionIdConflict) {
27
+ onConflict([...observedSessionIds]);
28
+ }
29
+ return { currentSessionId: null, sessionIdConflict: true };
30
+ }
31
+
32
+ if (sessionId === currentSessionId) {
33
+ return { currentSessionId, sessionIdConflict: false };
34
+ }
35
+ onCapture(sessionId);
36
+ return { currentSessionId: sessionId, sessionIdConflict: false };
37
+ }
38
+
39
+ function providerSessionCompletionError({
40
+ requestedSessionId,
41
+ currentSessionId,
42
+ sessionIdConflict,
43
+ persistenceError,
44
+ }) {
45
+ if (persistenceError) {
46
+ return `Provider session identity could not be persisted: ${persistenceError.message}`;
47
+ }
48
+
49
+ const requested = requestedSessionId?.trim() || null;
50
+ if (!requested) {
51
+ return null;
52
+ }
53
+ if (sessionIdConflict) {
54
+ return 'Provider continuation emitted conflicting session identities';
55
+ }
56
+
57
+ const captured = currentSessionId?.trim() || null;
58
+ if (captured === requested) {
59
+ return null;
60
+ }
61
+ return captured
62
+ ? 'Provider continuation returned a different session identity'
63
+ : 'Provider continuation did not confirm the requested session identity';
64
+ }
65
+
66
+ export function createProviderSessionCapture({
67
+ providerName,
68
+ taskId,
69
+ updateTask,
70
+ log,
71
+ requestedSessionId = null,
72
+ initialSessionId = null,
73
+ initialSessionIdConflict = false,
74
+ }) {
75
+ let currentSessionId = initialSessionId;
76
+ let sessionIdConflict = initialSessionIdConflict;
77
+ let persistenceError = null;
78
+ const observedSessionIds = new Set(initialSessionId ? [initialSessionId] : []);
79
+
80
+ function persist(update) {
81
+ try {
82
+ updateTask(taskId, update);
83
+ } catch (error) {
84
+ persistenceError ||= error;
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ function captureLine(line) {
90
+ try {
91
+ ({ currentSessionId, sessionIdConflict } = captureProviderSessionLine({
92
+ providerName,
93
+ line,
94
+ currentSessionId,
95
+ observedSessionIds,
96
+ sessionIdConflict,
97
+ onCapture: (sessionId) => {
98
+ // Advance memory before persistence so a failed write can never leave
99
+ // this watcher believing the earlier identity is still trustworthy.
100
+ currentSessionId = sessionId;
101
+ persist({ sessionId });
102
+ },
103
+ onConflict: () => {
104
+ currentSessionId = null;
105
+ sessionIdConflict = true;
106
+ persist({
107
+ sessionId: null,
108
+ sessionIdConflict: true,
109
+ resumeIdentityVerified: false,
110
+ });
111
+ },
112
+ }));
113
+ } catch (error) {
114
+ log(`[${Date.now()}][SESSION] Failed to persist provider session: ${error.message}\n`);
115
+ }
116
+ }
117
+
118
+ function getCompletionError() {
119
+ return providerSessionCompletionError({
120
+ requestedSessionId,
121
+ currentSessionId,
122
+ sessionIdConflict,
123
+ persistenceError,
124
+ });
125
+ }
126
+
127
+ return {
128
+ captureLine,
129
+ getCompletionError,
130
+ getCompletionUpdate: (resolvedCode) => {
131
+ const error = getCompletionError();
132
+ return {
133
+ resumeIdentityVerified: resolvedCode === 0 && !error,
134
+ ...(error ? { sessionId: null, sessionIdConflict: true } : {}),
135
+ };
136
+ },
137
+ };
138
+ }
@@ -155,7 +155,7 @@ function providerLevelSelection(options) {
155
155
  return { modelSpec };
156
156
  }
157
157
 
158
- function buildTaskRecord({ id, prompt, cwd, options, logFile, providerName, modelSpec }) {
158
+ export function buildTaskRecord({ id, prompt, cwd, options, logFile, providerName, modelSpec }) {
159
159
  return {
160
160
  id,
161
161
  prompt: prompt.slice(0, 200) + (prompt.length > 200 ? '...' : ''),
@@ -163,7 +163,15 @@ function buildTaskRecord({ id, prompt, cwd, options, logFile, providerName, mode
163
163
  cwd,
164
164
  status: 'running',
165
165
  pid: null,
166
- sessionId: options.resume || options.sessionId || null,
166
+ // Only watcher-observed provider output may populate sessionId. A requested
167
+ // resume ID is diagnostic input, not proof the resumed provider emitted or
168
+ // accepted that session identity.
169
+ sessionId: null,
170
+ sessionIdConflict: false,
171
+ requestedResumeSessionId: options.resume || null,
172
+ // Resumed tasks start fail-closed. Only the watcher terminal transaction
173
+ // may prove that the requested identity completed without conflict.
174
+ resumeIdentityVerified: !options.resume,
167
175
  logFile,
168
176
  createdAt: new Date().toISOString(),
169
177
  updatedAt: new Date().toISOString(),
package/task-lib/store.js CHANGED
@@ -12,6 +12,7 @@ import { TASKS_DIR, LOGS_DIR } from './config.js';
12
12
  import { generateName } from './name-generator.js';
13
13
 
14
14
  const DB_FILE = join(TASKS_DIR, 'store.db');
15
+ export const TASK_STORE_SCHEMA_VERSION = 4;
15
16
 
16
17
  /** @type {Database.Database | null} */
17
18
  let db = null;
@@ -41,6 +42,9 @@ function getDb() {
41
42
  status TEXT NOT NULL DEFAULT 'pending',
42
43
  pid INTEGER,
43
44
  session_id TEXT,
45
+ session_id_conflict INTEGER NOT NULL DEFAULT 0,
46
+ requested_resume_session_id TEXT,
47
+ resume_identity_verified INTEGER NOT NULL DEFAULT 0,
44
48
  log_file TEXT,
45
49
  created_at TEXT NOT NULL,
46
50
  updated_at TEXT NOT NULL,
@@ -77,8 +81,7 @@ function getDb() {
77
81
  CREATE INDEX IF NOT EXISTS idx_schedules_enabled ON schedules(enabled);
78
82
  `);
79
83
 
80
- ensureTaskColumn(db, 'process_group_id', 'INTEGER');
81
- ensureTaskColumn(db, 'termination_strategy', 'TEXT');
84
+ migrateTaskStore(db);
82
85
 
83
86
  return db;
84
87
  }
@@ -90,6 +93,40 @@ function ensureTaskColumn(database, name, definition) {
90
93
  }
91
94
  }
92
95
 
96
+ export function migrateTaskStore(database) {
97
+ ensureTaskColumn(database, 'process_group_id', 'INTEGER');
98
+ ensureTaskColumn(database, 'termination_strategy', 'TEXT');
99
+ ensureTaskColumn(database, 'requested_resume_session_id', 'TEXT');
100
+ ensureTaskColumn(database, 'session_id_conflict', 'INTEGER NOT NULL DEFAULT 0');
101
+ ensureTaskColumn(database, 'resume_identity_verified', 'INTEGER NOT NULL DEFAULT 0');
102
+
103
+ const version = database.pragma('user_version', { simple: true });
104
+ if (version >= TASK_STORE_SCHEMA_VERSION) return;
105
+
106
+ database.transaction(() => {
107
+ if (version < 2) {
108
+ database
109
+ .prepare(
110
+ `UPDATE tasks
111
+ SET requested_resume_session_id = COALESCE(requested_resume_session_id, session_id),
112
+ session_id = NULL`
113
+ )
114
+ .run();
115
+ }
116
+ if (version < 3) {
117
+ database.prepare('UPDATE tasks SET session_id_conflict = 0').run();
118
+ }
119
+ if (version < 4) {
120
+ database.prepare('UPDATE tasks SET resume_identity_verified = 0').run();
121
+ }
122
+ database.pragma(`user_version = ${TASK_STORE_SCHEMA_VERSION}`);
123
+ })();
124
+ }
125
+
126
+ function nullable(value) {
127
+ return value || null;
128
+ }
129
+
93
130
  export function ensureDirs() {
94
131
  if (!existsSync(TASKS_DIR)) mkdirSync(TASKS_DIR, { recursive: true });
95
132
  if (!existsSync(LOGS_DIR)) mkdirSync(LOGS_DIR, { recursive: true });
@@ -112,6 +149,9 @@ function rowToTask(row) {
112
149
  status: row.status,
113
150
  pid: row.pid,
114
151
  sessionId: row.session_id,
152
+ sessionIdConflict: Boolean(row.session_id_conflict),
153
+ requestedResumeSessionId: row.requested_resume_session_id,
154
+ resumeIdentityVerified: Boolean(row.resume_identity_verified),
115
155
  logFile: row.log_file,
116
156
  createdAt: row.created_at,
117
157
  updatedAt: row.updated_at,
@@ -149,11 +189,11 @@ export function saveTasks(tasks) {
149
189
  const database = getDb();
150
190
  const insert = database.prepare(`
151
191
  INSERT OR REPLACE INTO tasks (
152
- id, prompt, full_prompt, cwd, status, pid, session_id, log_file,
192
+ id, prompt, full_prompt, cwd, status, pid, session_id, session_id_conflict, requested_resume_session_id, resume_identity_verified, log_file,
153
193
  created_at, updated_at, exit_code, error, provider, model,
154
194
  schedule_id, socket_path, attachable, process_group_id, termination_strategy
155
195
  ) VALUES (
156
- @id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @logFile,
196
+ @id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @sessionIdConflict, @requestedResumeSessionId, @resumeIdentityVerified, @logFile,
157
197
  @createdAt, @updatedAt, @exitCode, @error, @provider, @model,
158
198
  @scheduleId, @socketPath, @attachable, @processGroupId, @terminationStrategy
159
199
  )
@@ -172,6 +212,9 @@ export function saveTasks(tasks) {
172
212
  status: task.status || 'pending',
173
213
  pid: task.pid || null,
174
214
  sessionId: task.sessionId || null,
215
+ sessionIdConflict: task.sessionIdConflict ? 1 : 0,
216
+ requestedResumeSessionId: nullable(task.requestedResumeSessionId),
217
+ resumeIdentityVerified: task.resumeIdentityVerified ? 1 : 0,
175
218
  logFile: task.logFile || null,
176
219
  createdAt: task.createdAt || new Date().toISOString(),
177
220
  updatedAt: task.updatedAt || new Date().toISOString(),
@@ -240,6 +283,9 @@ export function updateTask(id, updates) {
240
283
  status = @status,
241
284
  pid = @pid,
242
285
  session_id = @sessionId,
286
+ session_id_conflict = @sessionIdConflict,
287
+ requested_resume_session_id = @requestedResumeSessionId,
288
+ resume_identity_verified = @resumeIdentityVerified,
243
289
  log_file = @logFile,
244
290
  updated_at = @updatedAt,
245
291
  exit_code = @exitCode,
@@ -262,6 +308,9 @@ export function updateTask(id, updates) {
262
308
  status: updated.status || 'pending',
263
309
  pid: updated.pid || null,
264
310
  sessionId: updated.sessionId || null,
311
+ sessionIdConflict: updated.sessionIdConflict ? 1 : 0,
312
+ requestedResumeSessionId: nullable(updated.requestedResumeSessionId),
313
+ resumeIdentityVerified: updated.resumeIdentityVerified ? 1 : 0,
265
314
  logFile: updated.logFile || null,
266
315
  updatedAt: updated.updatedAt,
267
316
  exitCode: updated.exitCode ?? null,
@@ -295,11 +344,11 @@ export function addTask(task) {
295
344
  .prepare(
296
345
  `
297
346
  INSERT INTO tasks (
298
- id, prompt, full_prompt, cwd, status, pid, session_id, log_file,
347
+ id, prompt, full_prompt, cwd, status, pid, session_id, session_id_conflict, requested_resume_session_id, resume_identity_verified, log_file,
299
348
  created_at, updated_at, exit_code, error, provider, model,
300
349
  schedule_id, socket_path, attachable, process_group_id, termination_strategy
301
350
  ) VALUES (
302
- @id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @logFile,
351
+ @id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @sessionIdConflict, @requestedResumeSessionId, @resumeIdentityVerified, @logFile,
303
352
  @createdAt, @updatedAt, @exitCode, @error, @provider, @model,
304
353
  @scheduleId, @socketPath, @attachable, @processGroupId, @terminationStrategy
305
354
  )
@@ -313,6 +362,9 @@ export function addTask(task) {
313
362
  status: fullTask.status || 'pending',
314
363
  pid: fullTask.pid || null,
315
364
  sessionId: fullTask.sessionId || null,
365
+ sessionIdConflict: fullTask.sessionIdConflict ? 1 : 0,
366
+ requestedResumeSessionId: nullable(fullTask.requestedResumeSessionId),
367
+ resumeIdentityVerified: fullTask.resumeIdentityVerified ? 1 : 0,
316
368
  logFile: fullTask.logFile || null,
317
369
  createdAt: fullTask.createdAt,
318
370
  updatedAt: fullTask.updatedAt,
@@ -8,13 +8,14 @@
8
8
  import { spawn } from 'child_process';
9
9
  import { appendFileSync, unlinkSync } from 'fs';
10
10
  import { unlink } from 'fs/promises';
11
- import { updateTask } from './store.js';
11
+ import { getTask, updateTask } from './store.js';
12
12
  import {
13
13
  detectProviderFatalError,
14
14
  detectProviderStreamingModeError,
15
15
  recoverProviderStructuredOutput,
16
16
  supportsProviderStructuredOutputRecovery,
17
17
  } from './provider-helper-runtime.js';
18
+ import { createProviderSessionCapture } from './provider-session-capture.js';
18
19
  import { createRequire } from 'module';
19
20
 
20
21
  const require = createRequire(import.meta.url);
@@ -36,6 +37,17 @@ function log(msg) {
36
37
 
37
38
  const providerName = normalizeProviderName(config.provider || 'claude');
38
39
  const enableRecovery = supportsProviderStructuredOutputRecovery(providerName);
40
+ const storedTask = getTask(taskId);
41
+ const providerSessionCapture = createProviderSessionCapture({
42
+ providerName,
43
+ taskId,
44
+ updateTask,
45
+ log,
46
+ requestedSessionId: storedTask?.requestedResumeSessionId || null,
47
+ initialSessionId: storedTask?.sessionId || null,
48
+ initialSessionIdConflict: storedTask?.sessionIdConflict === true,
49
+ });
50
+ const maybeCaptureProviderSession = providerSessionCapture.captureLine;
39
51
 
40
52
  const env = { ...process.env, ...(commandSpec.env || {}) };
41
53
  const command = commandSpec.binary;
@@ -136,6 +148,7 @@ function maybeCaptureStructuredOutput(line) {
136
148
  function handleSilentJsonLines(lines, timestamp) {
137
149
  for (const line of lines) {
138
150
  if (!line.trim()) continue;
151
+ maybeCaptureProviderSession(line);
139
152
  maybeHandleFatalError(line, timestamp);
140
153
  if (captureStreamingError(line, timestamp)) {
141
154
  continue;
@@ -146,6 +159,7 @@ function handleSilentJsonLines(lines, timestamp) {
146
159
 
147
160
  function handleStreamingLines(lines, timestamp) {
148
161
  for (const line of lines) {
162
+ maybeCaptureProviderSession(line);
149
163
  maybeHandleFatalError(line, timestamp);
150
164
  if (captureStreamingError(line, timestamp)) {
151
165
  continue;
@@ -159,6 +173,7 @@ function flushStdoutBuffer(timestamp) {
159
173
  return;
160
174
  }
161
175
 
176
+ maybeCaptureProviderSession(stdoutBuffer);
162
177
  if (!enableRecovery) {
163
178
  if (!silentJsonMode) {
164
179
  log(`[${timestamp}]${stdoutBuffer}\n`);
@@ -282,18 +297,25 @@ child.on('close', async (code, signal) => {
282
297
  log(finalResultJson + '\n');
283
298
  }
284
299
 
285
- writeCompletionFooter(code, signal);
300
+ const sessionIdentityError = providerSessionCapture.getCompletionError();
301
+ const resolvedCode =
302
+ fatalError || sessionIdentityError ? 1 : recovered?.payload ? 0 : code;
303
+ const status = resolvedCode === 0 ? 'completed' : 'failed';
304
+
305
+ writeCompletionFooter(resolvedCode, signal);
286
306
  await cleanupCommandSpec();
287
307
 
288
- const resolvedCode = fatalError ? 1 : recovered?.payload ? 0 : code;
289
- const status = resolvedCode === 0 ? 'completed' : 'failed';
290
308
  try {
291
309
  await updateTask(taskId, {
292
310
  status,
293
311
  pid: null,
294
312
  processGroupId: null,
295
313
  exitCode: resolvedCode,
296
- error: fatalError || (resolvedCode !== 0 && signal ? `Killed by ${signal}` : null),
314
+ ...providerSessionCapture.getCompletionUpdate(resolvedCode),
315
+ error:
316
+ fatalError ||
317
+ sessionIdentityError ||
318
+ (resolvedCode !== 0 && signal ? `Killed by ${signal}` : null),
297
319
  });
298
320
  } catch (updateError) {
299
321
  log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);