@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.
- package/cli/commands/inspect.js +1 -0
- package/cli/index.js +17 -1
- package/cluster-hooks/block-ask-user-question.py +2 -3
- package/cluster-hooks/block-dangerous-git.py +2 -3
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +57 -3
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +32 -2
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/agent-cli-provider/adapters/common.js +1 -1
- package/lib/agent-cli-provider/adapters/common.js.map +1 -1
- package/lib/agent-cli-provider/adapters/index.d.ts +1 -0
- package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/index.js +8 -0
- package/lib/agent-cli-provider/adapters/index.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +3 -0
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/index.d.ts +1 -1
- package/lib/agent-cli-provider/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/index.js +2 -1
- package/lib/agent-cli-provider/index.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +9 -0
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +3 -0
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +10 -2
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/package.json +1 -1
- package/src/agent/agent-context-builder.js +90 -4
- package/src/agent/agent-context-sources.js +20 -2
- package/src/agent/agent-lifecycle.js +43 -8
- package/src/agent/agent-task-executor.js +557 -314
- package/src/agent/guidance-queue.js +29 -7
- package/src/agent/provider-session.js +277 -0
- package/src/agent-cli-provider/adapters/claude.ts +64 -4
- package/src/agent-cli-provider/adapters/codex.ts +47 -3
- package/src/agent-cli-provider/adapters/common.ts +1 -1
- package/src/agent-cli-provider/adapters/index.ts +10 -0
- package/src/agent-cli-provider/contract-options.ts +7 -0
- package/src/agent-cli-provider/index.ts +1 -0
- package/src/agent-cli-provider/provider-registry.ts +13 -1
- package/src/agent-cli-provider/types.ts +13 -6
- package/src/agent-wrapper.js +44 -8
- package/src/claude-task-runner.js +153 -44
- package/src/ledger-sequence.js +63 -0
- package/src/ledger.js +114 -29
- package/src/orchestrator.js +40 -2
- package/src/task-spawn-cleanup-ownership.js +82 -0
- package/src/worktree-claude-config.js +193 -85
- package/task-lib/attachable-watcher.js +101 -253
- package/task-lib/command-spec-cleanup.js +219 -0
- package/task-lib/commands/clean.js +35 -5
- package/task-lib/commands/get-task-id-by-spawn-token.js +10 -0
- package/task-lib/commands/kill.js +117 -4
- package/task-lib/commands/resume.js +29 -6
- package/task-lib/commands/status.js +4 -0
- package/task-lib/provider-helper-runtime.js +1 -0
- package/task-lib/provider-session-capture.js +138 -0
- package/task-lib/runner.js +70 -5
- package/task-lib/store.js +126 -12
- package/task-lib/watcher-output-runtime.js +284 -0
- 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
|
|
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
|
|
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({
|
|
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
|
|
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
|
-
|
|
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'
|
|
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[] =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
@@ -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<
|
|
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',
|