@the-open-engine/zeroshot 6.17.0 → 6.18.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 (76) hide show
  1. package/cli/index.js +9 -13
  2. package/lib/agent-cli-provider/adapters/claude.d.ts +2 -2
  3. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  4. package/lib/agent-cli-provider/adapters/claude.js +30 -0
  5. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  6. package/lib/agent-cli-provider/adapters/codex.d.ts +2 -2
  7. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  8. package/lib/agent-cli-provider/adapters/codex.js +37 -0
  9. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  10. package/lib/agent-cli-provider/adapters/gemini.d.ts +2 -2
  11. package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -1
  12. package/lib/agent-cli-provider/adapters/gemini.js +115 -4
  13. package/lib/agent-cli-provider/adapters/gemini.js.map +1 -1
  14. package/lib/agent-cli-provider/adapters/opencode.d.ts +2 -2
  15. package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
  16. package/lib/agent-cli-provider/adapters/opencode.js +108 -0
  17. package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
  18. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  19. package/lib/agent-cli-provider/contract-options.js +12 -0
  20. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  21. package/lib/agent-cli-provider/index.d.ts +2 -2
  22. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  23. package/lib/agent-cli-provider/index.js +2 -1
  24. package/lib/agent-cli-provider/index.js.map +1 -1
  25. package/lib/agent-cli-provider/provider-registry.d.ts +19 -7
  26. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  27. package/lib/agent-cli-provider/provider-registry.js +4 -0
  28. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  29. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  30. package/lib/agent-cli-provider/single-agent-runtime.js +35 -4
  31. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  32. package/lib/agent-cli-provider/types.d.ts +30 -3
  33. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  34. package/lib/agent-cli-provider/types.js.map +1 -1
  35. package/lib/cluster/errors.cjs +4 -1
  36. package/lib/cluster/errors.d.ts +2 -0
  37. package/lib/cluster/errors.mjs +2 -0
  38. package/lib/cluster/index.cjs +8 -1
  39. package/lib/cluster/index.d.ts +4 -1
  40. package/lib/cluster/index.mjs +4 -1
  41. package/lib/cluster/json-source.cjs +61 -0
  42. package/lib/cluster/json-source.d.ts +4 -0
  43. package/lib/cluster/json-source.mjs +55 -0
  44. package/lib/cluster/payload-value.cjs +76 -0
  45. package/lib/cluster/payload-value.d.ts +7 -0
  46. package/lib/cluster/payload-value.mjs +72 -0
  47. package/lib/cluster/validators.cjs +21 -0
  48. package/lib/cluster/validators.d.ts +4 -1
  49. package/lib/cluster/validators.mjs +19 -1
  50. package/lib/provider-names.js +5 -0
  51. package/package.json +2 -2
  52. package/src/agent/agent-task-executor.js +194 -410
  53. package/src/agent/output-extraction.js +39 -14
  54. package/src/agent/output-reformatter.js +154 -111
  55. package/src/agent-cli-provider/adapters/claude.ts +39 -2
  56. package/src/agent-cli-provider/adapters/codex.ts +56 -2
  57. package/src/agent-cli-provider/adapters/gemini.ts +102 -6
  58. package/src/agent-cli-provider/adapters/opencode.ts +85 -2
  59. package/src/agent-cli-provider/contract-options.ts +16 -0
  60. package/src/agent-cli-provider/index.ts +4 -0
  61. package/src/agent-cli-provider/provider-registry.ts +23 -3
  62. package/src/agent-cli-provider/single-agent-runtime.ts +50 -4
  63. package/src/agent-cli-provider/types.ts +36 -2
  64. package/src/agent-wrapper.js +2 -2
  65. package/src/cluster/errors.ts +1 -0
  66. package/src/cluster/index.ts +4 -0
  67. package/src/cluster/json-source.ts +71 -0
  68. package/src/cluster/payload-value.ts +84 -0
  69. package/src/cluster/validators.ts +31 -2
  70. package/src/orchestrator.js +33 -13
  71. package/task-lib/attachable-watcher.js +1 -0
  72. package/task-lib/command-spec-cleanup.js +57 -11
  73. package/task-lib/commands/run.js +1 -0
  74. package/task-lib/provider-session-capture.js +8 -0
  75. package/task-lib/runner.js +9 -6
  76. package/task-lib/watcher.js +1 -0
@@ -0,0 +1,71 @@
1
+ import { ClusterRequestError } from './errors.js';
2
+ import { MAX_FRAME_BYTES } from './generated/protocol.js';
3
+
4
+ export const MAX_REQUEST_BYTES = MAX_FRAME_BYTES;
5
+
6
+ export function decodeBoundedJson(bytes: Uint8Array): unknown {
7
+ if (bytes.length > MAX_REQUEST_BYTES) {
8
+ throw new ClusterRequestError(
9
+ `request payload of ${bytes.length} bytes exceeds the ${MAX_REQUEST_BYTES} byte limit`,
10
+ 'OVERSIZED_JSON',
11
+ );
12
+ }
13
+ let text: string;
14
+ try {
15
+ text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
16
+ } catch {
17
+ throw new ClusterRequestError('request payload is not valid UTF-8', 'INVALID_UTF8');
18
+ }
19
+ try {
20
+ return JSON.parse(text) as unknown;
21
+ } catch {
22
+ throw new ClusterRequestError('request payload is not valid JSON', 'MALFORMED_JSON');
23
+ }
24
+ }
25
+
26
+ // Callers resolve `--graph`/`--input` specifiers ('-' vs. a file path) to a byte
27
+ // source themselves; opening the filesystem stream stays outside src/cluster so this
28
+ // module keeps no host-specific dependency (not even the `NodeJS` ambient namespace,
29
+ // which isn't resolvable from a consumer's declaration-only install) beyond the
30
+ // standard async-iterable protocol every Node Readable already implements — mirrors
31
+ // how WebSocketLike is injected instead of importing 'ws' directly, see socket.ts.
32
+ export async function readBoundedSource(
33
+ source: AsyncIterable<Uint8Array | string>,
34
+ ): Promise<Uint8Array> {
35
+ const encoder = new TextEncoder();
36
+ const chunks: Uint8Array[] = [];
37
+ let total = 0;
38
+ for await (const chunk of source) {
39
+ const bytes = typeof chunk === 'string' ? encoder.encode(chunk) : new Uint8Array(chunk);
40
+ total += bytes.length;
41
+ if (total > MAX_REQUEST_BYTES) {
42
+ throw new ClusterRequestError(
43
+ `request payload exceeds the ${MAX_REQUEST_BYTES} byte limit`,
44
+ 'OVERSIZED_JSON',
45
+ );
46
+ }
47
+ chunks.push(bytes);
48
+ }
49
+ const assembled = new Uint8Array(total);
50
+ let offset = 0;
51
+ for (const bytes of chunks) {
52
+ assembled.set(bytes, offset);
53
+ offset += bytes.length;
54
+ }
55
+ return assembled;
56
+ }
57
+
58
+ export function assertDistinctRequestSources(
59
+ graph: string,
60
+ input: string | undefined,
61
+ ): asserts input is string {
62
+ if (input === undefined) {
63
+ throw new ClusterRequestError('--input is required for every hosted run', 'MISSING_INPUT');
64
+ }
65
+ if (graph === '-' && input === '-') {
66
+ throw new ClusterRequestError(
67
+ 'graph and input cannot both read from stdin',
68
+ 'AMBIGUOUS_STDIN_SOURCE',
69
+ );
70
+ }
71
+ }
@@ -0,0 +1,84 @@
1
+ import type { PayloadType } from './generated/protocol.js';
2
+ import { ClusterRequestError } from './errors.js';
3
+
4
+ export type InputValidationIssue = {
5
+ readonly path: string;
6
+ readonly code: 'TYPE_MISMATCH' | 'MISSING_REQUIRED_FIELD' | 'UNKNOWN_FIELD' | 'UNKNOWN_ENUM_LABEL';
7
+ };
8
+
9
+ type InternalIssue = InputValidationIssue & { readonly expectedKind: string };
10
+
11
+ function isJsonInteger(value: unknown): boolean {
12
+ return typeof value === 'number' && Number.isFinite(value) && Number.isInteger(value);
13
+ }
14
+
15
+ function evaluate(type: PayloadType, value: unknown, path: string): InternalIssue | null {
16
+ switch (type.kind) {
17
+ case 'null':
18
+ return value === null ? null : { path, code: 'TYPE_MISMATCH', expectedKind: 'null' };
19
+ case 'boolean':
20
+ return typeof value === 'boolean' ? null : { path, code: 'TYPE_MISMATCH', expectedKind: 'boolean' };
21
+ case 'integer':
22
+ return isJsonInteger(value) ? null : { path, code: 'TYPE_MISMATCH', expectedKind: 'integer' };
23
+ case 'number':
24
+ return typeof value === 'number' && Number.isFinite(value)
25
+ ? null
26
+ : { path, code: 'TYPE_MISMATCH', expectedKind: 'number' };
27
+ case 'string':
28
+ return typeof value === 'string' ? null : { path, code: 'TYPE_MISMATCH', expectedKind: 'string' };
29
+ case 'enum':
30
+ if (typeof value !== 'string') return { path, code: 'TYPE_MISMATCH', expectedKind: 'enum' };
31
+ return type.values.includes(value)
32
+ ? null
33
+ : { path, code: 'UNKNOWN_ENUM_LABEL', expectedKind: 'enum' };
34
+ case 'array': {
35
+ if (!Array.isArray(value)) return { path, code: 'TYPE_MISMATCH', expectedKind: 'array' };
36
+ for (let index = 0; index < value.length; index += 1) {
37
+ const issue = evaluate(type.items, value[index], `${path}/${index}`);
38
+ if (issue) return issue;
39
+ }
40
+ return null;
41
+ }
42
+ case 'record': {
43
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
44
+ return { path, code: 'TYPE_MISMATCH', expectedKind: 'record' };
45
+ }
46
+ const record = value as Readonly<Record<string, unknown>>;
47
+ const fieldNames = Object.keys(type.fields).sort();
48
+ for (const name of fieldNames) {
49
+ const field = type.fields[name];
50
+ if (!field) continue;
51
+ const fieldPath = `${path}/${name}`;
52
+ if (Object.prototype.hasOwnProperty.call(record, name)) {
53
+ const issue = evaluate(field.type, record[name], fieldPath);
54
+ if (issue) return issue;
55
+ } else if (field.required) {
56
+ return { path: fieldPath, code: 'MISSING_REQUIRED_FIELD', expectedKind: field.type.kind };
57
+ }
58
+ }
59
+ const declared = new Set(fieldNames);
60
+ const unknownField = Object.keys(record).sort().find((name) => !declared.has(name));
61
+ return unknownField
62
+ ? { path: `${path}/${unknownField}`, code: 'UNKNOWN_FIELD', expectedKind: 'record' }
63
+ : null;
64
+ }
65
+ }
66
+ }
67
+
68
+ export function firstInputValidationIssue(
69
+ type: PayloadType,
70
+ value: unknown,
71
+ path = '',
72
+ ): InputValidationIssue | null {
73
+ const issue = evaluate(type, value, path);
74
+ return issue ? { path: issue.path, code: issue.code } : null;
75
+ }
76
+
77
+ export function assertInputValue(type: PayloadType, value: unknown): void {
78
+ const issue = evaluate(type, value, '');
79
+ if (!issue) return;
80
+ throw new ClusterRequestError(
81
+ `input value at '${issue.path || '/'}' failed validation: ${issue.code}, expected ${issue.expectedKind}`,
82
+ 'INVALID_INPUT',
83
+ );
84
+ }
@@ -4,9 +4,9 @@ import {
4
4
  METHOD_RESULT_DEFINITIONS,
5
5
  PROTOCOL_VERSION,
6
6
  } from './generated/protocol.js';
7
- import type { ClusterMethod } from './generated/protocol.js';
7
+ import type { ClusterMethod, GraphProfile, GraphSpec, ServerCapabilities } from './generated/protocol.js';
8
8
  import { CLUSTER_PROTOCOL_SCHEMA } from './generated/protocol-schema.js';
9
- import { ClusterProtocolError } from './errors.js';
9
+ import { ClusterProtocolError, ClusterRequestError } from './errors.js';
10
10
 
11
11
  const ajv = new Ajv2020({
12
12
  allErrors: true,
@@ -54,3 +54,32 @@ export function assertMethodResult(method: ClusterMethod, value: unknown): void
54
54
  throw error;
55
55
  }
56
56
  }
57
+
58
+ function assertRequestDefinition(definition: string, value: unknown, code: string): void {
59
+ const validate = validatorFor(definition);
60
+ if (validate(value)) return;
61
+ const details = (validate.errors ?? []).map((error) =>
62
+ `${error.instancePath || '/'} ${error.message ?? 'is invalid'}`
63
+ ).join('; ');
64
+ throw new ClusterRequestError(`${definition} validation failed: ${details}`, code);
65
+ }
66
+
67
+ export function assertGraphSpec(value: unknown): asserts value is GraphSpec {
68
+ assertRequestDefinition('GraphSpec', value, 'INVALID_GRAPH');
69
+ }
70
+
71
+ export function assertGraphProfile(value: unknown): asserts value is GraphProfile {
72
+ assertRequestDefinition('GraphProfile', value, 'INVALID_GRAPH_PROFILE');
73
+ }
74
+
75
+ export function assertGraphProfileSupported(
76
+ profile: GraphProfile,
77
+ capabilities: ServerCapabilities,
78
+ ): void {
79
+ if (!capabilities.graphProfiles?.includes(profile)) {
80
+ throw new ClusterRequestError(
81
+ `graph profile ${profile} is not among the server's advertised graphProfiles`,
82
+ 'UNSUPPORTED_GRAPH_PROFILE',
83
+ );
84
+ }
85
+ }
@@ -270,6 +270,7 @@ class Orchestrator {
270
270
 
271
271
  // Track if orchestrator is closed (prevents _saveClusters race conditions during cleanup)
272
272
  this.closed = false;
273
+ this._conductorWatchdogs = new Set();
273
274
 
274
275
  // Track if clusters are loaded (for lazy loading pattern)
275
276
  this._clustersLoaded = options.skipLoad === true;
@@ -1654,6 +1655,28 @@ class Orchestrator {
1654
1655
  const timeoutMs = 30000;
1655
1656
  let watchdogTimer = null;
1656
1657
  let completedAt = null;
1658
+ const watchdog = {
1659
+ clear: () => {
1660
+ if (!watchdogTimer) {
1661
+ return;
1662
+ }
1663
+ clearTimeout(watchdogTimer);
1664
+ watchdogTimer = null;
1665
+ this._conductorWatchdogs.delete(watchdog);
1666
+ const elapsed = completedAt ? Date.now() - completedAt : 0;
1667
+ this._log(
1668
+ `✅ CLUSTER_OPERATIONS received (${elapsed}ms after conductor completed) - watchdog cleared`
1669
+ );
1670
+ },
1671
+ dispose: () => {
1672
+ if (watchdogTimer) {
1673
+ clearTimeout(watchdogTimer);
1674
+ watchdogTimer = null;
1675
+ }
1676
+ this._conductorWatchdogs.delete(watchdog);
1677
+ },
1678
+ };
1679
+ this._conductorWatchdogs.add(watchdog);
1657
1680
 
1658
1681
  this._subscribeToClusterTopic(messageBus, clusterId, 'AGENT_LIFECYCLE', (message) => {
1659
1682
  const event = message.content?.data?.event;
@@ -1666,6 +1689,11 @@ class Orchestrator {
1666
1689
  );
1667
1690
 
1668
1691
  watchdogTimer = setTimeout(() => {
1692
+ watchdogTimer = null;
1693
+ this._conductorWatchdogs.delete(watchdog);
1694
+ if (this.closed || messageBus._closed) {
1695
+ return;
1696
+ }
1669
1697
  const clusterOps = messageBus.query({
1670
1698
  cluster_id: clusterId,
1671
1699
  topic: 'CLUSTER_OPERATIONS',
@@ -1700,19 +1728,7 @@ class Orchestrator {
1700
1728
  }
1701
1729
  });
1702
1730
 
1703
- return {
1704
- clear: () => {
1705
- if (!watchdogTimer) {
1706
- return;
1707
- }
1708
- clearTimeout(watchdogTimer);
1709
- watchdogTimer = null;
1710
- const elapsed = completedAt ? Date.now() - completedAt : 0;
1711
- this._log(
1712
- `✅ CLUSTER_OPERATIONS received (${elapsed}ms after conductor completed) - watchdog cleared`
1713
- );
1714
- },
1715
- };
1731
+ return watchdog;
1716
1732
  }
1717
1733
 
1718
1734
  _registerClusterOperationsHandler(
@@ -2414,6 +2430,10 @@ class Orchestrator {
2414
2430
  return;
2415
2431
  }
2416
2432
  this.closed = true;
2433
+ for (const watchdog of this._conductorWatchdogs) {
2434
+ watchdog.dispose();
2435
+ }
2436
+ this._conductorWatchdogs.clear();
2417
2437
 
2418
2438
  for (const cluster of this.clusters.values()) {
2419
2439
  if (typeof cluster.snapshotter?.stop === 'function') {
@@ -111,6 +111,7 @@ const providerSessionCapture = createProviderSessionCapture({
111
111
  requestedSessionId: persistedTask?.requestedResumeSessionId || null,
112
112
  initialSessionId: persistedTask?.sessionId || null,
113
113
  initialSessionIdConflict: persistedTask?.sessionIdConflict === true,
114
+ disabled: config.structuredOutputRecovery === true,
114
115
  });
115
116
  let outputBuffer = '';
116
117
 
@@ -14,6 +14,10 @@ const CLEANUP_METADATA_KEYS = ['kind', 'path', 'provider', 'reason'];
14
14
  const SCHEMA_DIRECTORY_PATTERN = /^zeroshot-schema-[A-Za-z0-9_-]+$/u;
15
15
  const SCHEMA_FILE_PATTERN =
16
16
  /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.json$/u;
17
+ const POLICY_DIRECTORY_PATTERN = /^zeroshot-gemini-policy-[A-Za-z0-9_-]+$/u;
18
+ const POLICY_FILE_PATTERN =
19
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.toml$/u;
20
+ const OPENCODE_CONFIG_DIRECTORY_PATTERN = /^zeroshot-opencode-config-[A-Za-z0-9_-]+$/u;
17
21
 
18
22
  function assertClosedCleanupMetadata(cleanupPath, metadata) {
19
23
  if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
@@ -59,6 +63,37 @@ function createCleanupPlan(commandSpec) {
59
63
  }
60
64
 
61
65
  function assertOwnedTempDirectory(cleanupPath, metadata) {
66
+ const isOpenCodeConfig =
67
+ metadata.provider === 'opencode' && metadata.reason === 'isolated-config';
68
+ if (isOpenCodeConfig) {
69
+ const canonical =
70
+ isAbsolute(cleanupPath) &&
71
+ resolve(cleanupPath) === cleanupPath &&
72
+ dirname(cleanupPath) === resolve(tmpdir()) &&
73
+ OPENCODE_CONFIG_DIRECTORY_PATTERN.test(basename(cleanupPath));
74
+ if (!canonical) {
75
+ throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
76
+ }
77
+ let stat;
78
+ try {
79
+ stat = lstatSync(cleanupPath);
80
+ } catch (error) {
81
+ if (error?.code === 'ENOENT') return true;
82
+ throw error;
83
+ }
84
+ const tempRoot = realpathSync(tmpdir());
85
+ const realDirectory = realpathSync(cleanupPath);
86
+ if (
87
+ stat.isSymbolicLink() ||
88
+ !stat.isDirectory() ||
89
+ dirname(realDirectory) !== tempRoot ||
90
+ basename(realDirectory) !== basename(cleanupPath)
91
+ ) {
92
+ throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
93
+ }
94
+ return false;
95
+ }
96
+
62
97
  if (
63
98
  metadata.provider !== 'claude' ||
64
99
  metadata.reason !== 'settings-overlay' ||
@@ -79,22 +114,33 @@ function assertOwnedTempDirectory(cleanupPath, metadata) {
79
114
  }
80
115
 
81
116
  function assertCanonicalSchemaPath(cleanupPath, metadata) {
117
+ const isCodexSchema =
118
+ metadata.kind === 'temp-file' &&
119
+ metadata.provider === 'codex' &&
120
+ metadata.reason === 'output-schema';
121
+ const isGeminiPolicy =
122
+ metadata.kind === 'temp-file' &&
123
+ metadata.provider === 'gemini' &&
124
+ metadata.reason === 'admin-policy';
82
125
  if (
83
- metadata.kind !== 'temp-file' ||
84
- metadata.provider !== 'codex' ||
85
- metadata.reason !== 'output-schema' ||
126
+ (!isCodexSchema && !isGeminiPolicy) ||
86
127
  !isAbsolute(cleanupPath) ||
87
128
  resolve(cleanupPath) !== cleanupPath
88
129
  ) {
89
- throw new Error(`Refusing unowned output-schema cleanup: ${cleanupPath}`);
130
+ throw new Error(
131
+ `Refusing unowned ${isGeminiPolicy ? 'admin-policy' : 'output-schema'} cleanup: ${cleanupPath}`
132
+ );
90
133
  }
91
- const schemaDirectory = dirname(cleanupPath);
92
- if (
93
- dirname(schemaDirectory) !== resolve(tmpdir()) ||
94
- !SCHEMA_DIRECTORY_PATTERN.test(basename(schemaDirectory)) ||
95
- !SCHEMA_FILE_PATTERN.test(basename(cleanupPath))
96
- ) {
97
- throw new Error(`Refusing non-canonical output-schema cleanup: ${cleanupPath}`);
134
+ const ownedDirectory = dirname(cleanupPath);
135
+ const matchesOwnedName = isCodexSchema
136
+ ? SCHEMA_DIRECTORY_PATTERN.test(basename(ownedDirectory)) &&
137
+ SCHEMA_FILE_PATTERN.test(basename(cleanupPath))
138
+ : POLICY_DIRECTORY_PATTERN.test(basename(ownedDirectory)) &&
139
+ POLICY_FILE_PATTERN.test(basename(cleanupPath));
140
+ if (dirname(ownedDirectory) !== resolve(tmpdir()) || !matchesOwnedName) {
141
+ throw new Error(
142
+ `Refusing non-canonical ${isGeminiPolicy ? 'admin-policy' : 'output-schema'} cleanup: ${cleanupPath}`
143
+ );
98
144
  }
99
145
  }
100
146
 
@@ -40,6 +40,7 @@ export async function runTask(prompt, options = {}) {
40
40
  jsonSchema,
41
41
  mcpConfig: options.mcpConfig,
42
42
  silentJsonOutput,
43
+ structuredOutputRecovery: options.structuredOutputRecovery === true,
43
44
  });
44
45
 
45
46
  console.log(chalk.green(`\n✓ Task spawned: ${chalk.cyan(task.id)}`));
@@ -71,7 +71,15 @@ export function createProviderSessionCapture({
71
71
  requestedSessionId = null,
72
72
  initialSessionId = null,
73
73
  initialSessionIdConflict = false,
74
+ disabled = false,
74
75
  }) {
76
+ if (disabled) {
77
+ return {
78
+ captureLine() {},
79
+ getCompletionError: () => null,
80
+ getCompletionUpdate: () => ({ resumeIdentityVerified: false }),
81
+ };
82
+ }
75
83
  let currentSessionId = initialSessionId;
76
84
  let sessionIdConflict = initialSessionIdConflict;
77
85
  let persistenceError = null;
@@ -116,19 +116,21 @@ function resolveJsonSchema(options, outputFormat) {
116
116
  }
117
117
 
118
118
  function buildProviderOptions(options, runtime, modelSelection) {
119
+ const structuredOutputRecovery = options.structuredOutputRecovery === true;
119
120
  return {
120
121
  outputFormat: runtime.outputFormat,
121
122
  jsonSchema: runtime.jsonSchema,
122
123
  cwd: runtime.cwd,
123
- autoApprove: true,
124
+ autoApprove: !structuredOutputRecovery,
124
125
  ...(modelSelection === undefined ? {} : { modelSpec: modelSelection.modelSpec }),
125
- ...mcpConfigOption(options),
126
+ ...(structuredOutputRecovery ? {} : mcpConfigOption(options)),
126
127
  ...claudeSettingsFileOption(),
127
- ...(options.resume ? { resumeSessionId: options.resume } : {}),
128
+ ...(!structuredOutputRecovery && options.resume ? { resumeSessionId: options.resume } : {}),
128
129
  ...(process.env.ZEROSHOT_OPENCODE_AGENT?.trim()
129
130
  ? { agentName: process.env.ZEROSHOT_OPENCODE_AGENT.trim() }
130
131
  : {}),
131
- ...(options.continue ? { continueSession: true } : {}),
132
+ ...(!structuredOutputRecovery && options.continue ? { continueSession: true } : {}),
133
+ ...(structuredOutputRecovery ? { structuredOutputRecovery: true } : {}),
132
134
  };
133
135
  }
134
136
 
@@ -218,10 +220,10 @@ export function buildTaskRecord({
218
220
  // accepted that session identity.
219
221
  sessionId: null,
220
222
  sessionIdConflict: false,
221
- requestedResumeSessionId: options.resume || null,
223
+ requestedResumeSessionId: options.structuredOutputRecovery ? null : options.resume || null,
222
224
  // Resumed tasks start fail-closed. Only the watcher terminal transaction
223
225
  // may prove that the requested identity completed without conflict.
224
- resumeIdentityVerified: !options.resume,
226
+ resumeIdentityVerified: options.structuredOutputRecovery || !options.resume,
225
227
  logFile,
226
228
  createdAt: new Date().toISOString(),
227
229
  updatedAt: new Date().toISOString(),
@@ -253,6 +255,7 @@ function buildWatcherConfig(outputFormat, jsonSchema, options, providerName, com
253
255
  outputFormat,
254
256
  jsonSchema,
255
257
  silentJsonOutput: options.silentJsonOutput || false,
258
+ structuredOutputRecovery: options.structuredOutputRecovery === true,
256
259
  provider: providerName,
257
260
  command: commandSpec.binary,
258
261
  env: commandSpec.env || {},
@@ -64,6 +64,7 @@ const providerSessionCapture = createProviderSessionCapture({
64
64
  requestedSessionId: storedTask?.requestedResumeSessionId || null,
65
65
  initialSessionId: storedTask?.sessionId || null,
66
66
  initialSessionIdConflict: storedTask?.sessionIdConflict === true,
67
+ disabled: config.structuredOutputRecovery === true,
67
68
  });
68
69
 
69
70
  let crashStarted = false;