@scotthuang/agent-knock-knock 0.10.0 → 0.10.2
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/CHANGELOG.md +27 -0
- package/README.md +19 -2
- package/dist/src/claude-local-transcript-provider.d.ts +16 -0
- package/dist/src/claude-local-transcript-provider.js +111 -47
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/cli.js +3738 -812
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-lifecycle-compatibility.d.ts +2 -0
- package/dist/src/codex-lifecycle-compatibility.js +13 -0
- package/dist/src/codex-lifecycle-compatibility.js.map +1 -0
- package/dist/src/codex-store-adapter.js +8 -7
- package/dist/src/codex-store-adapter.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.js +9 -8
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/live-lifecycle-evidence.d.ts +232 -0
- package/dist/src/live-lifecycle-evidence.js +1157 -0
- package/dist/src/live-lifecycle-evidence.js.map +1 -0
- package/dist/src/live-lifecycle-smoke-evidence.d.ts +13 -0
- package/dist/src/live-lifecycle-smoke-evidence.js +167 -0
- package/dist/src/live-lifecycle-smoke-evidence.js.map +1 -0
- package/dist/src/live-lifecycle-smoke.d.ts +123 -0
- package/dist/src/live-lifecycle-smoke.js +937 -0
- package/dist/src/live-lifecycle-smoke.js.map +1 -0
- package/dist/src/openclaw-plugin-helpers.d.ts +1 -0
- package/dist/src/openclaw-plugin-helpers.js +30 -2
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin.js +193 -31
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/protocol.d.ts +14 -0
- package/dist/src/protocol.js +87 -0
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/store.js +17 -2
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +31 -2
- package/dist/src/terminal-agent-bridge.js +231 -11
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-submission-acceptance.d.ts +78 -0
- package/dist/src/terminal-submission-acceptance.js +454 -0
- package/dist/src/terminal-submission-acceptance.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export class AkkClientInvocationError extends Error {
|
|
3
|
+
failureKind;
|
|
4
|
+
constructor(failureKind) {
|
|
5
|
+
super(`AKK invocation ${failureKind}`);
|
|
6
|
+
this.name = "AkkClientInvocationError";
|
|
7
|
+
this.failureKind = failureKind;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const DEFAULT_TIMEOUTS = {
|
|
11
|
+
readMs: 60_000,
|
|
12
|
+
mutationMs: 120_000,
|
|
13
|
+
completionMs: 10 * 60_000,
|
|
14
|
+
monitorPollIntervalMs: 500,
|
|
15
|
+
agentInactivityMinutes: 5,
|
|
16
|
+
agentHardTimeoutMinutes: 10
|
|
17
|
+
};
|
|
18
|
+
const NATIVE_THREAD_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
19
|
+
const UNRESOLVED_TURN_STATUSES = new Set([
|
|
20
|
+
"created",
|
|
21
|
+
"running",
|
|
22
|
+
"waiting_for_agent",
|
|
23
|
+
"waiting_for_openclaw",
|
|
24
|
+
"stalled",
|
|
25
|
+
"callback_pending",
|
|
26
|
+
"callback_failed",
|
|
27
|
+
"cancelling"
|
|
28
|
+
]);
|
|
29
|
+
class SmokeAbort extends Error {
|
|
30
|
+
errorCode;
|
|
31
|
+
constructor(errorCode) {
|
|
32
|
+
super(errorCode);
|
|
33
|
+
this.name = "SmokeAbort";
|
|
34
|
+
this.errorCode = errorCode;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function runLifecycleScenario(config, dependencies) {
|
|
38
|
+
const now = dependencies.now ?? Date.now;
|
|
39
|
+
// Kept injectable for callers that coordinate multiple bounded scenarios.
|
|
40
|
+
// The current golden path uses one foreground monitor and therefore sleeps
|
|
41
|
+
// inside AKK rather than in this orchestration layer.
|
|
42
|
+
const _sleep = dependencies.sleep ?? defaultSleep;
|
|
43
|
+
void _sleep;
|
|
44
|
+
const nonceFactory = dependencies.nonce ?? randomUUID;
|
|
45
|
+
const startedAt = safeNow(now);
|
|
46
|
+
const steps = [];
|
|
47
|
+
const partial = {};
|
|
48
|
+
let mutationAttempted = false;
|
|
49
|
+
let currentStep = "preflight";
|
|
50
|
+
let currentStepStartedAt = startedAt;
|
|
51
|
+
const result = (status, errorCode) => ({
|
|
52
|
+
schema: "agent-knock-knock/live-lifecycle-smoke-core-result",
|
|
53
|
+
version: 1,
|
|
54
|
+
agent: config.agent,
|
|
55
|
+
target: config.target,
|
|
56
|
+
pane_pid: config.expectedPanePid,
|
|
57
|
+
expected_agent_version: config.expectedAgentVersion,
|
|
58
|
+
status,
|
|
59
|
+
started_at_ms: startedAt,
|
|
60
|
+
finished_at_ms: safeNow(now),
|
|
61
|
+
steps: [...steps],
|
|
62
|
+
...(errorCode ? { error_code: errorCode } : {}),
|
|
63
|
+
...(status === "uncertain"
|
|
64
|
+
? { recovery: "inspect_selected_pane_do_not_retry" }
|
|
65
|
+
: {}),
|
|
66
|
+
...(partial.start ? { start: partial.start } : {}),
|
|
67
|
+
...(partial.newThread ? { new_thread: partial.newThread } : {}),
|
|
68
|
+
...(partial.activeAfterNew
|
|
69
|
+
? { active_after_new: partial.activeAfterNew }
|
|
70
|
+
: {}),
|
|
71
|
+
...(partial.turn ? { turn: partial.turn } : {}),
|
|
72
|
+
...(partial.resumeCandidate
|
|
73
|
+
? { resume_candidate: partial.resumeCandidate }
|
|
74
|
+
: {}),
|
|
75
|
+
...(partial.resumeThread
|
|
76
|
+
? { resume_thread: partial.resumeThread }
|
|
77
|
+
: {}),
|
|
78
|
+
...(partial.final ? { final: partial.final } : {})
|
|
79
|
+
});
|
|
80
|
+
const runStep = async (name, kind, operation) => {
|
|
81
|
+
currentStep = name;
|
|
82
|
+
currentStepStartedAt = safeNow(now);
|
|
83
|
+
try {
|
|
84
|
+
const value = await operation();
|
|
85
|
+
steps.push({
|
|
86
|
+
name,
|
|
87
|
+
status: "passed",
|
|
88
|
+
duration_ms: elapsed(currentStepStartedAt, safeNow(now))
|
|
89
|
+
});
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const status = mutationAttempted ? "uncertain" : "failed";
|
|
94
|
+
steps.push({
|
|
95
|
+
name,
|
|
96
|
+
status,
|
|
97
|
+
duration_ms: elapsed(currentStepStartedAt, safeNow(now))
|
|
98
|
+
});
|
|
99
|
+
throw new ScenarioStopped(status, errorCodeFor(error));
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
const prepared = await runStep("preflight", "read", async () => {
|
|
104
|
+
validateConfig(config);
|
|
105
|
+
const timeouts = normalizedTimeouts(config.timeouts);
|
|
106
|
+
const initialList = await invoke(dependencies.client, "list", ["--all", "--terminal-debug"], { kind: "read", timeoutMs: timeouts.readMs });
|
|
107
|
+
return {
|
|
108
|
+
timeouts,
|
|
109
|
+
start: selectTerminalSnapshot(initialList, config, {
|
|
110
|
+
requireNewThread: true,
|
|
111
|
+
requireListResumable: true,
|
|
112
|
+
requireSend: false,
|
|
113
|
+
allowUnmanagedCodexNativeProbe: true
|
|
114
|
+
})
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
const { timeouts, start } = prepared;
|
|
118
|
+
if (start.nativeThreadId) {
|
|
119
|
+
partial.start = terminalEvidence(start, "preflight_native_identity");
|
|
120
|
+
}
|
|
121
|
+
const newPhase = await runStep("new_thread", "mutation", async () => {
|
|
122
|
+
const action = start.newThreadAction;
|
|
123
|
+
if (!action?.expectedBindingToken) {
|
|
124
|
+
abort("preflight_action");
|
|
125
|
+
}
|
|
126
|
+
mutationAttempted = true;
|
|
127
|
+
const output = await invoke(dependencies.client, "new-thread", [
|
|
128
|
+
"--terminal",
|
|
129
|
+
action.terminalId,
|
|
130
|
+
"--expected-binding-token",
|
|
131
|
+
action.expectedBindingToken,
|
|
132
|
+
"--require-restorable-origin"
|
|
133
|
+
], { kind: "mutation", timeoutMs: timeouts.mutationMs });
|
|
134
|
+
const transition = parseTransition(output, "new_thread");
|
|
135
|
+
if (transition.terminal_id !== start.evidence.terminal_id) {
|
|
136
|
+
abort("new_thread_invalid");
|
|
137
|
+
}
|
|
138
|
+
const startEvidence = terminalEvidence(start, "new_thread_invalid", transition.previous_native_thread_id);
|
|
139
|
+
if (start.nativeThreadId !== null &&
|
|
140
|
+
transition.previous_native_thread_id !== start.nativeThreadId) {
|
|
141
|
+
abort("new_thread_invalid");
|
|
142
|
+
}
|
|
143
|
+
const listed = await invoke(dependencies.client, "list", ["--all", "--terminal-debug"], { kind: "read", timeoutMs: timeouts.readMs });
|
|
144
|
+
const snapshot = selectTerminalSnapshot(listed, config, {
|
|
145
|
+
requireNewThread: false,
|
|
146
|
+
requireListResumable: true,
|
|
147
|
+
requireSend: true,
|
|
148
|
+
allowUnmanagedCodexNativeProbe: false
|
|
149
|
+
});
|
|
150
|
+
assertSameTerminalIncarnation(start, snapshot);
|
|
151
|
+
const snapshotEvidence = terminalEvidence(snapshot, "new_thread_invalid");
|
|
152
|
+
if (snapshotEvidence.native_thread_id === startEvidence.native_thread_id ||
|
|
153
|
+
transition.previous_native_thread_id !==
|
|
154
|
+
startEvidence.native_thread_id ||
|
|
155
|
+
transition.native_thread_id !== snapshotEvidence.native_thread_id ||
|
|
156
|
+
transition.session_id !== snapshotEvidence.session_id ||
|
|
157
|
+
transition.previous_session_id !== startEvidence.session_id ||
|
|
158
|
+
transition.session_id === transition.previous_session_id ||
|
|
159
|
+
transition.binding_id !== snapshotEvidence.binding_id ||
|
|
160
|
+
transition.binding_generation !==
|
|
161
|
+
snapshotEvidence.binding_generation ||
|
|
162
|
+
snapshotEvidence.binding_generation !== 1 ||
|
|
163
|
+
transition.turn_created !== false ||
|
|
164
|
+
snapshotEvidence.turn_count !== 0 ||
|
|
165
|
+
snapshot.sendAction?.sessionId !== snapshotEvidence.session_id ||
|
|
166
|
+
snapshotEvidence.binding_fence === startEvidence.binding_fence ||
|
|
167
|
+
snapshotEvidence.binding_id ===
|
|
168
|
+
(startEvidence.binding_id ?? startEvidence.binding_fence)) {
|
|
169
|
+
abort("new_thread_invalid");
|
|
170
|
+
}
|
|
171
|
+
if (startEvidence.binding_id &&
|
|
172
|
+
snapshotEvidence.binding_id === startEvidence.binding_id) {
|
|
173
|
+
abort("new_thread_invalid");
|
|
174
|
+
}
|
|
175
|
+
return { transition, snapshot, startEvidence, snapshotEvidence };
|
|
176
|
+
});
|
|
177
|
+
const newThread = newPhase.transition;
|
|
178
|
+
const afterNew = newPhase.snapshot;
|
|
179
|
+
const startEvidence = newPhase.startEvidence;
|
|
180
|
+
const afterNewEvidence = newPhase.snapshotEvidence;
|
|
181
|
+
// For a Codex unmanaged origin without an open rollout descriptor, the
|
|
182
|
+
// fully verified New step proves A under the lifecycle locks with a fresh
|
|
183
|
+
// /status card. Only now expose that exact identity as public evidence.
|
|
184
|
+
partial.start = startEvidence;
|
|
185
|
+
partial.newThread = newThread;
|
|
186
|
+
partial.activeAfterNew = afterNewEvidence;
|
|
187
|
+
const sent = await runStep("send", "mutation", async () => {
|
|
188
|
+
const sessionId = afterNew.sendAction?.sessionId;
|
|
189
|
+
if (!sessionId || sessionId !== afterNew.evidence.session_id) {
|
|
190
|
+
abort("send_invalid");
|
|
191
|
+
}
|
|
192
|
+
const nonce = nonceFactory();
|
|
193
|
+
if (typeof nonce !== "string" || nonce.trim() === "") {
|
|
194
|
+
abort("configuration_invalid");
|
|
195
|
+
}
|
|
196
|
+
mutationAttempted = true;
|
|
197
|
+
const smokeRequest = [
|
|
198
|
+
`AKK lifecycle smoke sentinel ${nonce}.`,
|
|
199
|
+
"请确认这条多语言、多行请求已经由原生 Agent 接收;不要修改任何文件。"
|
|
200
|
+
].join("\n");
|
|
201
|
+
const output = await invoke(dependencies.client, "send", [
|
|
202
|
+
"--session",
|
|
203
|
+
sessionId,
|
|
204
|
+
"--message",
|
|
205
|
+
smokeRequest,
|
|
206
|
+
"--background",
|
|
207
|
+
"--disable-terminal-bridge-monitor"
|
|
208
|
+
], { kind: "mutation", timeoutMs: timeouts.mutationMs });
|
|
209
|
+
return parseSend(output, sessionId);
|
|
210
|
+
});
|
|
211
|
+
const monitored = await runStep("wait_completion", "mutation", async () => {
|
|
212
|
+
mutationAttempted = true;
|
|
213
|
+
const output = await invoke(dependencies.client, "monitor", [
|
|
214
|
+
"--terminal-bridge",
|
|
215
|
+
"--record-only",
|
|
216
|
+
"--state",
|
|
217
|
+
sent.statePath,
|
|
218
|
+
"--log",
|
|
219
|
+
sent.eventLogPath,
|
|
220
|
+
"--poll-interval-ms",
|
|
221
|
+
String(timeouts.monitorPollIntervalMs),
|
|
222
|
+
"--agent-timeout-minutes",
|
|
223
|
+
String(timeouts.agentInactivityMinutes),
|
|
224
|
+
"--agent-hard-timeout-minutes",
|
|
225
|
+
String(timeouts.agentHardTimeoutMinutes)
|
|
226
|
+
], { kind: "mutation", timeoutMs: timeouts.completionMs });
|
|
227
|
+
return parseMonitor(output, sent);
|
|
228
|
+
});
|
|
229
|
+
const resumable = await runStep("list_resumable_threads", "read", async () => {
|
|
230
|
+
const listed = await invoke(dependencies.client, "list", ["--all", "--terminal-debug"], { kind: "read", timeoutMs: timeouts.readMs });
|
|
231
|
+
const snapshot = selectTerminalSnapshot(listed, config, {
|
|
232
|
+
requireNewThread: false,
|
|
233
|
+
requireListResumable: true,
|
|
234
|
+
requireSend: true,
|
|
235
|
+
allowUnmanagedCodexNativeProbe: false
|
|
236
|
+
});
|
|
237
|
+
assertSameTerminalIncarnation(afterNew, snapshot);
|
|
238
|
+
const snapshotEvidence = terminalEvidence(snapshot, "turn_verification_failed");
|
|
239
|
+
if (snapshotEvidence.session_id !== afterNewEvidence.session_id ||
|
|
240
|
+
snapshotEvidence.native_thread_id !==
|
|
241
|
+
afterNewEvidence.native_thread_id ||
|
|
242
|
+
snapshotEvidence.binding_id !== afterNewEvidence.binding_id ||
|
|
243
|
+
snapshotEvidence.binding_generation !==
|
|
244
|
+
afterNewEvidence.binding_generation ||
|
|
245
|
+
snapshotEvidence.turn_count !== afterNewEvidence.turn_count + 1 ||
|
|
246
|
+
!snapshot.recentTurn ||
|
|
247
|
+
stringValue(snapshot.recentTurn.conversation_id) !== sent.turnId ||
|
|
248
|
+
stringValue(snapshot.recentTurn.status) !== "idle") {
|
|
249
|
+
abort("turn_verification_failed");
|
|
250
|
+
}
|
|
251
|
+
partial.turn = {
|
|
252
|
+
session_id: sent.sessionId,
|
|
253
|
+
turn_id: sent.turnId,
|
|
254
|
+
status: monitored.status,
|
|
255
|
+
turn_count_before: afterNewEvidence.turn_count,
|
|
256
|
+
turn_count_after: snapshotEvidence.turn_count
|
|
257
|
+
};
|
|
258
|
+
const action = snapshot.listResumableAction;
|
|
259
|
+
if (!action) {
|
|
260
|
+
abort("candidate_invalid");
|
|
261
|
+
}
|
|
262
|
+
const output = await invoke(dependencies.client, "list-resumable-threads", ["--terminal", action.terminalId], { kind: "read", timeoutMs: timeouts.readMs });
|
|
263
|
+
const candidate = parseResumeCandidate(output, snapshot, startEvidence.native_thread_id, startEvidence.session_id);
|
|
264
|
+
return { snapshot, snapshotEvidence, candidate };
|
|
265
|
+
});
|
|
266
|
+
const afterTurn = resumable.snapshot;
|
|
267
|
+
const afterTurnEvidence = resumable.snapshotEvidence;
|
|
268
|
+
const candidate = resumable.candidate;
|
|
269
|
+
partial.resumeCandidate = candidate.evidence;
|
|
270
|
+
const resumed = await runStep("resume_thread", "mutation", async () => {
|
|
271
|
+
mutationAttempted = true;
|
|
272
|
+
const output = await invoke(dependencies.client, "resume-thread", [
|
|
273
|
+
"--terminal",
|
|
274
|
+
candidate.terminalId,
|
|
275
|
+
"--native-thread",
|
|
276
|
+
candidate.nativeThreadId,
|
|
277
|
+
"--expected-binding-token",
|
|
278
|
+
candidate.expectedBindingToken,
|
|
279
|
+
"--candidate-token",
|
|
280
|
+
candidate.candidateToken
|
|
281
|
+
], { kind: "mutation", timeoutMs: timeouts.mutationMs });
|
|
282
|
+
const transition = parseTransition(output, "resume_thread");
|
|
283
|
+
if (transition.terminal_id !== afterTurn.evidence.terminal_id) {
|
|
284
|
+
abort("resume_thread_invalid");
|
|
285
|
+
}
|
|
286
|
+
return transition;
|
|
287
|
+
});
|
|
288
|
+
partial.resumeThread = resumed;
|
|
289
|
+
const final = await runStep("final_verify", "read", async () => {
|
|
290
|
+
const listed = await invoke(dependencies.client, "list", ["--all", "--terminal-debug"], { kind: "read", timeoutMs: timeouts.readMs });
|
|
291
|
+
const snapshot = selectTerminalSnapshot(listed, config, {
|
|
292
|
+
requireNewThread: true,
|
|
293
|
+
requireListResumable: true,
|
|
294
|
+
requireSend: true,
|
|
295
|
+
allowUnmanagedCodexNativeProbe: false
|
|
296
|
+
});
|
|
297
|
+
assertSameTerminalIncarnation(start, snapshot);
|
|
298
|
+
const snapshotEvidence = terminalEvidence(snapshot, "restore_verification_failed");
|
|
299
|
+
if (snapshotEvidence.native_thread_id !== startEvidence.native_thread_id ||
|
|
300
|
+
resumed.previous_session_id !== afterTurnEvidence.session_id ||
|
|
301
|
+
resumed.previous_native_thread_id !==
|
|
302
|
+
afterTurnEvidence.native_thread_id ||
|
|
303
|
+
resumed.native_thread_id !== startEvidence.native_thread_id ||
|
|
304
|
+
resumed.session_id !== snapshotEvidence.session_id ||
|
|
305
|
+
resumed.session_id === afterTurnEvidence.session_id ||
|
|
306
|
+
resumed.binding_id !== snapshotEvidence.binding_id ||
|
|
307
|
+
resumed.binding_generation !== snapshotEvidence.binding_generation ||
|
|
308
|
+
resumed.transition_id === newThread.transition_id ||
|
|
309
|
+
resumed.turn_created !== false ||
|
|
310
|
+
snapshotEvidence.binding_id === afterTurnEvidence.binding_id ||
|
|
311
|
+
snapshotEvidence.binding_id ===
|
|
312
|
+
(startEvidence.binding_id ?? startEvidence.binding_fence) ||
|
|
313
|
+
snapshotEvidence.binding_fence === afterTurnEvidence.binding_fence ||
|
|
314
|
+
snapshotEvidence.turn_count !== startEvidence.turn_count) {
|
|
315
|
+
abort("restore_verification_failed");
|
|
316
|
+
}
|
|
317
|
+
if (startEvidence.session_id) {
|
|
318
|
+
if (startEvidence.binding_generation === null ||
|
|
319
|
+
newThread.previous_session_id !== startEvidence.session_id ||
|
|
320
|
+
candidate.managedSessionId !== startEvidence.session_id ||
|
|
321
|
+
resumed.session_id !== startEvidence.session_id ||
|
|
322
|
+
snapshotEvidence.session_id !== startEvidence.session_id ||
|
|
323
|
+
snapshotEvidence.binding_generation !==
|
|
324
|
+
startEvidence.binding_generation + 1) {
|
|
325
|
+
abort("restore_verification_failed");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else if (startEvidence.binding_generation !== null ||
|
|
329
|
+
newThread.previous_session_id !== null ||
|
|
330
|
+
candidate.managedSessionId !== undefined ||
|
|
331
|
+
snapshotEvidence.binding_generation !== 1) {
|
|
332
|
+
abort("restore_verification_failed");
|
|
333
|
+
}
|
|
334
|
+
return snapshotEvidence;
|
|
335
|
+
});
|
|
336
|
+
partial.final = final;
|
|
337
|
+
return result("passed");
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
if (error instanceof ScenarioStopped) {
|
|
341
|
+
return result(error.status, error.errorCode);
|
|
342
|
+
}
|
|
343
|
+
const status = mutationAttempted
|
|
344
|
+
? "uncertain"
|
|
345
|
+
: "failed";
|
|
346
|
+
if (!steps.some((step) => step.name === currentStep)) {
|
|
347
|
+
steps.push({
|
|
348
|
+
name: currentStep,
|
|
349
|
+
status,
|
|
350
|
+
duration_ms: elapsed(currentStepStartedAt, safeNow(now))
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return result(status, errorCodeFor(error));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
export async function runLifecycleMatrix(configs, dependencies) {
|
|
357
|
+
const now = dependencies.now ?? Date.now;
|
|
358
|
+
const startedAt = safeNow(now);
|
|
359
|
+
if (!matrixPanesAreExplicitlyDistinct(configs)) {
|
|
360
|
+
const scenarios = configs.map((config) => {
|
|
361
|
+
const scenarioStartedAt = safeNow(now);
|
|
362
|
+
const scenarioFinishedAt = safeNow(now);
|
|
363
|
+
return {
|
|
364
|
+
schema: "agent-knock-knock/live-lifecycle-smoke-core-result",
|
|
365
|
+
version: 1,
|
|
366
|
+
agent: config.agent,
|
|
367
|
+
target: config.target,
|
|
368
|
+
pane_pid: config.expectedPanePid,
|
|
369
|
+
expected_agent_version: config.expectedAgentVersion,
|
|
370
|
+
status: "failed",
|
|
371
|
+
started_at_ms: scenarioStartedAt,
|
|
372
|
+
finished_at_ms: scenarioFinishedAt,
|
|
373
|
+
steps: [{
|
|
374
|
+
name: "preflight",
|
|
375
|
+
status: "failed",
|
|
376
|
+
duration_ms: elapsed(scenarioStartedAt, scenarioFinishedAt)
|
|
377
|
+
}],
|
|
378
|
+
error_code: "configuration_invalid"
|
|
379
|
+
};
|
|
380
|
+
});
|
|
381
|
+
return {
|
|
382
|
+
schema: "agent-knock-knock/live-lifecycle-smoke-core-matrix",
|
|
383
|
+
version: 1,
|
|
384
|
+
status: "failed",
|
|
385
|
+
started_at_ms: startedAt,
|
|
386
|
+
finished_at_ms: safeNow(now),
|
|
387
|
+
scenarios
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const scenarios = [];
|
|
391
|
+
for (const config of configs) {
|
|
392
|
+
scenarios.push(await runLifecycleScenario(config, dependencies));
|
|
393
|
+
}
|
|
394
|
+
const status = scenarios.some((entry) => entry.status === "uncertain")
|
|
395
|
+
? "uncertain"
|
|
396
|
+
: scenarios.length > 0 && scenarios.every((entry) => entry.status === "passed")
|
|
397
|
+
? "passed"
|
|
398
|
+
: "failed";
|
|
399
|
+
return {
|
|
400
|
+
schema: "agent-knock-knock/live-lifecycle-smoke-core-matrix",
|
|
401
|
+
version: 1,
|
|
402
|
+
status,
|
|
403
|
+
started_at_ms: startedAt,
|
|
404
|
+
finished_at_ms: safeNow(now),
|
|
405
|
+
scenarios
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function matrixPanesAreExplicitlyDistinct(configs) {
|
|
409
|
+
const targets = new Set();
|
|
410
|
+
const panePids = new Set();
|
|
411
|
+
for (const config of configs) {
|
|
412
|
+
if (targets.has(config.target) ||
|
|
413
|
+
panePids.has(config.expectedPanePid)) {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
targets.add(config.target);
|
|
417
|
+
panePids.add(config.expectedPanePid);
|
|
418
|
+
}
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
class ScenarioStopped extends Error {
|
|
422
|
+
status;
|
|
423
|
+
errorCode;
|
|
424
|
+
constructor(status, errorCode) {
|
|
425
|
+
super(errorCode);
|
|
426
|
+
this.name = "ScenarioStopped";
|
|
427
|
+
this.status = status;
|
|
428
|
+
this.errorCode = errorCode;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function invoke(client, command, args, options) {
|
|
432
|
+
return await client.invoke(command, args, options);
|
|
433
|
+
}
|
|
434
|
+
function selectTerminalSnapshot(value, config, requirements) {
|
|
435
|
+
const root = recordValue(value, "preflight_terminal_match");
|
|
436
|
+
const terminals = Array.isArray(root.terminals) ? root.terminals : [];
|
|
437
|
+
const matches = terminals.filter((candidate) => {
|
|
438
|
+
if (!isRecord(candidate) || !isRecord(candidate.terminal_control)) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
return candidate.agent === config.agent &&
|
|
442
|
+
candidate.terminal_control.target === config.target &&
|
|
443
|
+
Number(candidate.terminal_control.panePid) === config.expectedPanePid;
|
|
444
|
+
});
|
|
445
|
+
if (matches.length !== 1) {
|
|
446
|
+
abort("preflight_terminal_match");
|
|
447
|
+
}
|
|
448
|
+
const row = matches[0];
|
|
449
|
+
const terminalControl = recordValue(row.terminal_control, "preflight_terminal_identity");
|
|
450
|
+
const terminalId = requiredString(row.id, "preflight_terminal_identity");
|
|
451
|
+
const target = requiredString(terminalControl.target, "preflight_terminal_identity");
|
|
452
|
+
const panePid = positiveInteger(terminalControl.panePid, "preflight_terminal_identity");
|
|
453
|
+
if (row.source !== "terminal" ||
|
|
454
|
+
row.agent !== config.agent ||
|
|
455
|
+
row.process_state !== "active" ||
|
|
456
|
+
target !== config.target ||
|
|
457
|
+
panePid !== config.expectedPanePid) {
|
|
458
|
+
abort("preflight_terminal_identity");
|
|
459
|
+
}
|
|
460
|
+
const agentPid = positiveInteger(row.pid, "preflight_process_identity");
|
|
461
|
+
const agentVersion = requiredString(row.agent_version, "preflight_agent_version");
|
|
462
|
+
if (agentVersion !== config.expectedAgentVersion) {
|
|
463
|
+
abort("preflight_agent_version");
|
|
464
|
+
}
|
|
465
|
+
const lifecycle = recordValue(row.native_thread_lifecycle, "preflight_capability");
|
|
466
|
+
if (lifecycle.status !== "supported" ||
|
|
467
|
+
lifecycle.agentVersion !== agentVersion ||
|
|
468
|
+
lifecycle.newThread !== true ||
|
|
469
|
+
lifecycle.resumeExact !== true ||
|
|
470
|
+
lifecycle.candidateDiscovery !== true) {
|
|
471
|
+
abort("preflight_capability");
|
|
472
|
+
}
|
|
473
|
+
const behaviorProfile = requiredString(lifecycle.behaviorProfile, "preflight_capability");
|
|
474
|
+
const processUuid = requiredString(row.native_agent_process_uuid, "preflight_process_identity");
|
|
475
|
+
const processBirth = nullableString(row.native_agent_process_birth, "preflight_process_identity");
|
|
476
|
+
if (config.agent === "codex" && processBirth === null) {
|
|
477
|
+
abort("preflight_process_identity");
|
|
478
|
+
}
|
|
479
|
+
const workspace = requiredString(row.workspace ?? row.cwd, "preflight_workspace");
|
|
480
|
+
const nativeIdentity = row.native_agent_session_id;
|
|
481
|
+
const nativeThreadId = nativeIdentity === null || nativeIdentity === undefined
|
|
482
|
+
? null
|
|
483
|
+
: exactNativeThreadId(nativeIdentity, "preflight_native_identity");
|
|
484
|
+
if (row.activity_state !== "idle") {
|
|
485
|
+
abort("preflight_not_idle");
|
|
486
|
+
}
|
|
487
|
+
const approval = recordValue(row.approval_state, "preflight_approval");
|
|
488
|
+
if (approval.scanned !== true ||
|
|
489
|
+
approval.blocked !== false ||
|
|
490
|
+
approval.approvable !== false) {
|
|
491
|
+
abort("preflight_approval");
|
|
492
|
+
}
|
|
493
|
+
if (row.unresolved_lifecycle_transition !== undefined &&
|
|
494
|
+
row.unresolved_lifecycle_transition !== null) {
|
|
495
|
+
abort("preflight_management");
|
|
496
|
+
}
|
|
497
|
+
if (row.orphaned_terminal_dispatch !== undefined &&
|
|
498
|
+
row.orphaned_terminal_dispatch !== null) {
|
|
499
|
+
abort("preflight_management");
|
|
500
|
+
}
|
|
501
|
+
const managementState = requiredString(row.management_state, "preflight_management");
|
|
502
|
+
if (managementState === "conflict" ||
|
|
503
|
+
row.management_conflict !== undefined &&
|
|
504
|
+
row.management_conflict !== null) {
|
|
505
|
+
abort("preflight_management");
|
|
506
|
+
}
|
|
507
|
+
const managed = recordValue(row.managed, "preflight_management");
|
|
508
|
+
if (!("current_turn" in managed) || managed.current_turn !== null) {
|
|
509
|
+
abort("preflight_unresolved_turn");
|
|
510
|
+
}
|
|
511
|
+
assertNoUnresolvedManagedTurns(managed);
|
|
512
|
+
// The public list contract always names the Session slot explicitly. Treat
|
|
513
|
+
// an absent value as schema drift instead of silently normalizing it to the
|
|
514
|
+
// unmanaged state.
|
|
515
|
+
if (!("session_id" in managed)) {
|
|
516
|
+
abort("preflight_management");
|
|
517
|
+
}
|
|
518
|
+
const sessionId = nullableString(managed.session_id, "preflight_management");
|
|
519
|
+
const turnCount = nonNegativeInteger(managed.turn_count, "preflight_management");
|
|
520
|
+
let bindingId = null;
|
|
521
|
+
let bindingGeneration = null;
|
|
522
|
+
const bindingFence = requiredString(row.lifecycle_binding_token, "preflight_management");
|
|
523
|
+
if (sessionId) {
|
|
524
|
+
if (nativeThreadId === null) {
|
|
525
|
+
abort("preflight_native_identity");
|
|
526
|
+
}
|
|
527
|
+
if (managementState !== "managed" ||
|
|
528
|
+
managed.binding_status !== "bound" ||
|
|
529
|
+
managed.native_thread_id !== nativeThreadId) {
|
|
530
|
+
abort("preflight_management");
|
|
531
|
+
}
|
|
532
|
+
bindingId = requiredString(managed.binding_id, "preflight_management");
|
|
533
|
+
bindingGeneration = positiveInteger(managed.binding_generation, "preflight_management");
|
|
534
|
+
if (managed.binding_token !== undefined &&
|
|
535
|
+
managed.binding_token !== bindingFence) {
|
|
536
|
+
abort("preflight_management");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
if (managementState !== "unmanaged") {
|
|
541
|
+
abort("preflight_management");
|
|
542
|
+
}
|
|
543
|
+
if (nativeThreadId === null &&
|
|
544
|
+
!(requirements.allowUnmanagedCodexNativeProbe &&
|
|
545
|
+
config.agent === "codex")) {
|
|
546
|
+
abort("preflight_native_identity");
|
|
547
|
+
}
|
|
548
|
+
// An unmanaged terminal has a lifecycle fence, but no persisted Session
|
|
549
|
+
// or binding. Reject stale binding material rather than laundering a
|
|
550
|
+
// contradictory list response into a clean unmanaged origin.
|
|
551
|
+
for (const key of [
|
|
552
|
+
"session_short_ref",
|
|
553
|
+
"binding_status",
|
|
554
|
+
"binding_id",
|
|
555
|
+
"binding_generation",
|
|
556
|
+
"native_thread_id",
|
|
557
|
+
"binding_token"
|
|
558
|
+
]) {
|
|
559
|
+
if (managed[key] !== undefined && managed[key] !== null) {
|
|
560
|
+
abort("preflight_management");
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const actions = recordValue(row.available_actions, "preflight_action");
|
|
565
|
+
const newThreadAction = actionFor(actions, "new_thread", terminalId, {
|
|
566
|
+
bindingToken: true,
|
|
567
|
+
sessionId: false
|
|
568
|
+
});
|
|
569
|
+
const listResumableAction = actionFor(actions, "list_resumable_threads", terminalId, { bindingToken: false, sessionId: false });
|
|
570
|
+
// An unmanaged pane advertises Send with missing_required metadata, but it
|
|
571
|
+
// cannot include a Session until New materializes the first binding. Do not
|
|
572
|
+
// parse that intentionally incomplete action during the initial preflight.
|
|
573
|
+
const sendAction = requirements.requireSend
|
|
574
|
+
? actionFor(actions, "send", terminalId, {
|
|
575
|
+
bindingToken: false,
|
|
576
|
+
sessionId: true
|
|
577
|
+
})
|
|
578
|
+
: undefined;
|
|
579
|
+
if ((requirements.requireNewThread && !newThreadAction) ||
|
|
580
|
+
(requirements.requireListResumable && !listResumableAction) ||
|
|
581
|
+
(requirements.requireSend && !sendAction)) {
|
|
582
|
+
abort("preflight_action");
|
|
583
|
+
}
|
|
584
|
+
if (sendAction?.sessionId &&
|
|
585
|
+
sessionId &&
|
|
586
|
+
sendAction.sessionId !== sessionId) {
|
|
587
|
+
abort("preflight_action");
|
|
588
|
+
}
|
|
589
|
+
if (newThreadAction?.expectedBindingToken &&
|
|
590
|
+
newThreadAction.expectedBindingToken !== bindingFence) {
|
|
591
|
+
abort("preflight_action");
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
agent: config.agent,
|
|
595
|
+
evidence: {
|
|
596
|
+
terminal_id: terminalId,
|
|
597
|
+
agent_pid: agentPid,
|
|
598
|
+
process_uuid: processUuid,
|
|
599
|
+
process_birth: processBirth,
|
|
600
|
+
workspace,
|
|
601
|
+
session_id: sessionId,
|
|
602
|
+
binding_id: bindingId,
|
|
603
|
+
// An unmanaged pane has a lifecycle fence but no persisted binding yet.
|
|
604
|
+
// Generation one only exists after a lifecycle operation materializes
|
|
605
|
+
// the first Session/binding pair.
|
|
606
|
+
binding_generation: bindingGeneration,
|
|
607
|
+
binding_fence: bindingFence,
|
|
608
|
+
turn_count: turnCount,
|
|
609
|
+
agent_version: agentVersion,
|
|
610
|
+
behavior_profile: behaviorProfile
|
|
611
|
+
},
|
|
612
|
+
nativeThreadId,
|
|
613
|
+
target,
|
|
614
|
+
panePid,
|
|
615
|
+
managementState,
|
|
616
|
+
currentTurn: null,
|
|
617
|
+
recentTurn: isRecord(managed.recent_turn)
|
|
618
|
+
? managed.recent_turn
|
|
619
|
+
: managed.recent_turn === null
|
|
620
|
+
? null
|
|
621
|
+
: undefined,
|
|
622
|
+
...(newThreadAction ? { newThreadAction } : {}),
|
|
623
|
+
...(listResumableAction ? { listResumableAction } : {}),
|
|
624
|
+
...(sendAction ? { sendAction } : {})
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function actionFor(actions, name, terminalId, requirements) {
|
|
628
|
+
const value = actions[name];
|
|
629
|
+
if (value === undefined) {
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|
|
632
|
+
if (!isRecord(value) || !isRecord(value.arguments)) {
|
|
633
|
+
abort("preflight_action");
|
|
634
|
+
}
|
|
635
|
+
const actionTerminalId = requirements.sessionId
|
|
636
|
+
? terminalId
|
|
637
|
+
: requiredString(value.arguments.terminal_id, "preflight_action");
|
|
638
|
+
if (!requirements.sessionId && actionTerminalId !== terminalId) {
|
|
639
|
+
abort("preflight_action");
|
|
640
|
+
}
|
|
641
|
+
const expectedBindingToken = requirements.bindingToken
|
|
642
|
+
? requiredString(value.arguments.expected_binding_token, "preflight_action")
|
|
643
|
+
: undefined;
|
|
644
|
+
const sessionId = requirements.sessionId
|
|
645
|
+
? requiredString(value.arguments.session_id, "preflight_action")
|
|
646
|
+
: undefined;
|
|
647
|
+
return {
|
|
648
|
+
terminalId: actionTerminalId,
|
|
649
|
+
...(expectedBindingToken ? { expectedBindingToken } : {}),
|
|
650
|
+
...(sessionId ? { sessionId } : {})
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function assertNoUnresolvedManagedTurns(managed) {
|
|
654
|
+
const recent = managed.recent_turn;
|
|
655
|
+
if (recent !== null && recent !== undefined && !isRecord(recent)) {
|
|
656
|
+
abort("preflight_unresolved_turn");
|
|
657
|
+
}
|
|
658
|
+
const history = managed.history;
|
|
659
|
+
if (!Array.isArray(history)) {
|
|
660
|
+
// Every smoke list uses --all, whose public contract includes history.
|
|
661
|
+
abort("preflight_unresolved_turn");
|
|
662
|
+
}
|
|
663
|
+
const visible = [
|
|
664
|
+
...(isRecord(recent) ? [recent] : []),
|
|
665
|
+
...history
|
|
666
|
+
];
|
|
667
|
+
for (const candidate of visible) {
|
|
668
|
+
if (!isRecord(candidate)) {
|
|
669
|
+
abort("preflight_unresolved_turn");
|
|
670
|
+
}
|
|
671
|
+
const status = requiredString(candidate.status, "preflight_unresolved_turn");
|
|
672
|
+
if (UNRESOLVED_TURN_STATUSES.has(status)) {
|
|
673
|
+
abort("preflight_unresolved_turn");
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function parseTransition(value, operation) {
|
|
678
|
+
const record = recordValue(value, operation === "new_thread" ? "new_thread_invalid" : "resume_thread_invalid");
|
|
679
|
+
const status = stringValue(record.status);
|
|
680
|
+
if (status === "uncertain" ||
|
|
681
|
+
status === "verified_recovery_required" ||
|
|
682
|
+
record.do_not_retry === true) {
|
|
683
|
+
abort(operation === "new_thread"
|
|
684
|
+
? "new_thread_uncertain"
|
|
685
|
+
: "resume_thread_uncertain");
|
|
686
|
+
}
|
|
687
|
+
const invalidCode = operation === "new_thread"
|
|
688
|
+
? "new_thread_invalid"
|
|
689
|
+
: "resume_thread_invalid";
|
|
690
|
+
if (status !== "committed" ||
|
|
691
|
+
record.operation !== operation ||
|
|
692
|
+
record.turn_created !== false ||
|
|
693
|
+
!("previous_session_id" in record)) {
|
|
694
|
+
abort(invalidCode);
|
|
695
|
+
}
|
|
696
|
+
return {
|
|
697
|
+
terminal_id: requiredString(record.terminal_id, invalidCode),
|
|
698
|
+
transition_id: requiredString(record.transition_id, invalidCode),
|
|
699
|
+
operation,
|
|
700
|
+
previous_session_id: operation === "new_thread"
|
|
701
|
+
? nullableString(record.previous_session_id, invalidCode)
|
|
702
|
+
: requiredString(record.previous_session_id, invalidCode),
|
|
703
|
+
session_id: requiredString(record.session_id, invalidCode),
|
|
704
|
+
previous_native_thread_id: exactNativeThreadId(record.previous_native_thread_id, invalidCode),
|
|
705
|
+
native_thread_id: exactNativeThreadId(record.native_thread_id, invalidCode),
|
|
706
|
+
binding_id: requiredString(record.binding_id, invalidCode),
|
|
707
|
+
binding_generation: positiveInteger(record.binding_generation, invalidCode),
|
|
708
|
+
turn_created: false
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
function parseSend(value, expectedSessionId) {
|
|
712
|
+
const record = recordValue(value, "send_invalid");
|
|
713
|
+
if (record.submission_outcome === "uncertain" ||
|
|
714
|
+
record.do_not_retry === true ||
|
|
715
|
+
record.delivered !== true ||
|
|
716
|
+
record.bookkeeping_warning !== undefined &&
|
|
717
|
+
record.bookkeeping_warning !== null) {
|
|
718
|
+
abort("send_uncertain");
|
|
719
|
+
}
|
|
720
|
+
const sessionId = requiredString(record.session_id, "send_invalid");
|
|
721
|
+
const turnId = requiredString(record.turn_id, "send_invalid");
|
|
722
|
+
const conversation = recordValue(record.conversation, "send_invalid");
|
|
723
|
+
const statePath = requiredString(conversation.state_path, "send_invalid");
|
|
724
|
+
const eventLogPath = requiredString(conversation.event_log_path, "send_invalid");
|
|
725
|
+
if (record.status !== "async_pending" ||
|
|
726
|
+
record.submission_outcome !== "agent_accepted" ||
|
|
727
|
+
record.delivery_receipt !== "agent_accepted" ||
|
|
728
|
+
record.replayed === true ||
|
|
729
|
+
record.background !== true ||
|
|
730
|
+
sessionId !== expectedSessionId ||
|
|
731
|
+
conversation.session_id !== sessionId ||
|
|
732
|
+
conversation.turn_id !== turnId) {
|
|
733
|
+
abort("send_invalid");
|
|
734
|
+
}
|
|
735
|
+
return { sessionId, turnId, statePath, eventLogPath };
|
|
736
|
+
}
|
|
737
|
+
function parseMonitor(value, sent) {
|
|
738
|
+
const record = recordValue(value, "monitor_invalid");
|
|
739
|
+
if (record.submission_outcome === "uncertain" ||
|
|
740
|
+
record.do_not_retry === true ||
|
|
741
|
+
record.completed === false ||
|
|
742
|
+
record.stalled === true ||
|
|
743
|
+
record.awaiting_approval === true) {
|
|
744
|
+
abort("monitor_uncertain");
|
|
745
|
+
}
|
|
746
|
+
const conversation = recordValue(record.conversation, "monitor_invalid");
|
|
747
|
+
const message = recordValue(record.message, "monitor_invalid");
|
|
748
|
+
if (record.delivered !== false ||
|
|
749
|
+
record.duplicate !== false ||
|
|
750
|
+
message.type !== "done" ||
|
|
751
|
+
typeof message.body !== "string" ||
|
|
752
|
+
message.body.trim() === "" ||
|
|
753
|
+
message.session_id !== sent.sessionId ||
|
|
754
|
+
message.turn_id !== sent.turnId ||
|
|
755
|
+
conversation.session_id !== sent.sessionId ||
|
|
756
|
+
conversation.turn_id !== sent.turnId ||
|
|
757
|
+
conversation.status !== "idle" ||
|
|
758
|
+
conversation.state_path !== sent.statePath ||
|
|
759
|
+
conversation.event_log_path !== sent.eventLogPath) {
|
|
760
|
+
abort("monitor_invalid");
|
|
761
|
+
}
|
|
762
|
+
return { status: "idle" };
|
|
763
|
+
}
|
|
764
|
+
function parseResumeCandidate(value, current, nativeThreadId, expectedManagedSessionId) {
|
|
765
|
+
const currentNativeThreadId = exactNativeThreadId(current.nativeThreadId, "candidate_invalid");
|
|
766
|
+
const record = recordValue(value, "candidate_invalid");
|
|
767
|
+
if (record.terminal_id !== current.evidence.terminal_id ||
|
|
768
|
+
record.agent !== current.agent) {
|
|
769
|
+
abort("candidate_invalid");
|
|
770
|
+
}
|
|
771
|
+
if (record.workspace !== current.evidence.workspace ||
|
|
772
|
+
record.current_session_id !== current.evidence.session_id ||
|
|
773
|
+
record.current_native_thread_id !== currentNativeThreadId) {
|
|
774
|
+
abort("candidate_invalid");
|
|
775
|
+
}
|
|
776
|
+
const expectedBindingToken = requiredString(record.expected_binding_token, "candidate_invalid");
|
|
777
|
+
if (expectedBindingToken !== current.evidence.binding_fence) {
|
|
778
|
+
abort("candidate_invalid");
|
|
779
|
+
}
|
|
780
|
+
const threads = Array.isArray(record.threads) ? record.threads : [];
|
|
781
|
+
const matches = threads.filter((candidate) => isRecord(candidate) && candidate.native_thread_id === nativeThreadId);
|
|
782
|
+
if (matches.length !== 1) {
|
|
783
|
+
abort("candidate_invalid");
|
|
784
|
+
}
|
|
785
|
+
const candidate = matches[0];
|
|
786
|
+
const managedSessionId = nullableString(candidate.managed_session_id, "candidate_invalid");
|
|
787
|
+
if (candidate.resumable !== true ||
|
|
788
|
+
candidate.active_elsewhere !== false ||
|
|
789
|
+
candidate.unavailable_reason !== undefined ||
|
|
790
|
+
managedSessionId !== expectedManagedSessionId) {
|
|
791
|
+
abort("candidate_invalid");
|
|
792
|
+
}
|
|
793
|
+
const candidateToken = requiredString(candidate.candidate_token, "candidate_invalid");
|
|
794
|
+
const availableActions = recordValue(candidate.available_actions, "candidate_invalid");
|
|
795
|
+
const resume = recordValue(availableActions.resume_thread, "candidate_invalid");
|
|
796
|
+
const args = recordValue(resume.arguments, "candidate_invalid");
|
|
797
|
+
if (args.terminal_id !== current.evidence.terminal_id ||
|
|
798
|
+
args.native_thread_id !== nativeThreadId ||
|
|
799
|
+
args.expected_binding_token !== expectedBindingToken ||
|
|
800
|
+
args.candidate_token !== candidateToken) {
|
|
801
|
+
abort("candidate_invalid");
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
terminalId: current.evidence.terminal_id,
|
|
805
|
+
nativeThreadId,
|
|
806
|
+
expectedBindingToken,
|
|
807
|
+
candidateToken,
|
|
808
|
+
...(managedSessionId ? { managedSessionId } : {}),
|
|
809
|
+
evidence: {
|
|
810
|
+
native_thread_id: nativeThreadId,
|
|
811
|
+
managed_session_id: managedSessionId,
|
|
812
|
+
exact_candidate_count: 1,
|
|
813
|
+
resumable: true,
|
|
814
|
+
active_elsewhere: false,
|
|
815
|
+
fresh_candidate_token_present: true
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function terminalEvidence(snapshot, errorCode, probedNativeThreadId) {
|
|
820
|
+
const nativeThreadId = exactNativeThreadId(snapshot.nativeThreadId ?? probedNativeThreadId, errorCode);
|
|
821
|
+
if (snapshot.nativeThreadId !== null &&
|
|
822
|
+
probedNativeThreadId !== undefined &&
|
|
823
|
+
snapshot.nativeThreadId !== probedNativeThreadId) {
|
|
824
|
+
abort(errorCode);
|
|
825
|
+
}
|
|
826
|
+
return {
|
|
827
|
+
...snapshot.evidence,
|
|
828
|
+
native_thread_id: nativeThreadId
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
function assertSameTerminalIncarnation(expected, actual) {
|
|
832
|
+
if (actual.agent !== expected.agent ||
|
|
833
|
+
actual.evidence.terminal_id !== expected.evidence.terminal_id ||
|
|
834
|
+
actual.target !== expected.target ||
|
|
835
|
+
actual.panePid !== expected.panePid ||
|
|
836
|
+
actual.evidence.agent_pid !== expected.evidence.agent_pid ||
|
|
837
|
+
actual.evidence.process_uuid !== expected.evidence.process_uuid ||
|
|
838
|
+
actual.evidence.process_birth !== expected.evidence.process_birth ||
|
|
839
|
+
actual.evidence.workspace !== expected.evidence.workspace ||
|
|
840
|
+
actual.evidence.agent_version !== expected.evidence.agent_version ||
|
|
841
|
+
actual.evidence.behavior_profile !== expected.evidence.behavior_profile) {
|
|
842
|
+
abort("identity_drift");
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function normalizedTimeouts(configured) {
|
|
846
|
+
const timeouts = { ...DEFAULT_TIMEOUTS, ...(configured ?? {}) };
|
|
847
|
+
for (const value of Object.values(timeouts)) {
|
|
848
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
849
|
+
abort("configuration_invalid");
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (timeouts.agentHardTimeoutMinutes < timeouts.agentInactivityMinutes) {
|
|
853
|
+
abort("configuration_invalid");
|
|
854
|
+
}
|
|
855
|
+
return timeouts;
|
|
856
|
+
}
|
|
857
|
+
function validateConfig(config) {
|
|
858
|
+
if (!["codex", "claude"].includes(config.agent) ||
|
|
859
|
+
typeof config.target !== "string" ||
|
|
860
|
+
config.target.trim() === "" ||
|
|
861
|
+
!Number.isSafeInteger(config.expectedPanePid) ||
|
|
862
|
+
config.expectedPanePid <= 1 ||
|
|
863
|
+
typeof config.expectedAgentVersion !== "string" ||
|
|
864
|
+
config.expectedAgentVersion.trim() === "") {
|
|
865
|
+
abort("configuration_invalid");
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function errorCodeFor(error) {
|
|
869
|
+
if (error instanceof SmokeAbort) {
|
|
870
|
+
return error.errorCode;
|
|
871
|
+
}
|
|
872
|
+
if (error instanceof AkkClientInvocationError) {
|
|
873
|
+
return `client_${error.failureKind}`;
|
|
874
|
+
}
|
|
875
|
+
return "client_error";
|
|
876
|
+
}
|
|
877
|
+
function recordValue(value, errorCode) {
|
|
878
|
+
if (!isRecord(value)) {
|
|
879
|
+
abort(errorCode);
|
|
880
|
+
}
|
|
881
|
+
return value;
|
|
882
|
+
}
|
|
883
|
+
function requiredString(value, errorCode) {
|
|
884
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
885
|
+
abort(errorCode);
|
|
886
|
+
}
|
|
887
|
+
return value;
|
|
888
|
+
}
|
|
889
|
+
function nullableString(value, errorCode) {
|
|
890
|
+
if (value === null || value === undefined) {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
return requiredString(value, errorCode);
|
|
894
|
+
}
|
|
895
|
+
function exactNativeThreadId(value, errorCode) {
|
|
896
|
+
const result = requiredString(value, errorCode);
|
|
897
|
+
if (!NATIVE_THREAD_ID_PATTERN.test(result)) {
|
|
898
|
+
abort(errorCode);
|
|
899
|
+
}
|
|
900
|
+
return result.toLowerCase();
|
|
901
|
+
}
|
|
902
|
+
function positiveInteger(value, errorCode) {
|
|
903
|
+
const result = Number(value);
|
|
904
|
+
if (!Number.isSafeInteger(result) || result <= 0) {
|
|
905
|
+
abort(errorCode);
|
|
906
|
+
}
|
|
907
|
+
return result;
|
|
908
|
+
}
|
|
909
|
+
function nonNegativeInteger(value, errorCode) {
|
|
910
|
+
const result = Number(value);
|
|
911
|
+
if (!Number.isSafeInteger(result) || result < 0) {
|
|
912
|
+
abort(errorCode);
|
|
913
|
+
}
|
|
914
|
+
return result;
|
|
915
|
+
}
|
|
916
|
+
function stringValue(value) {
|
|
917
|
+
return typeof value === "string" && value.trim() !== ""
|
|
918
|
+
? value
|
|
919
|
+
: undefined;
|
|
920
|
+
}
|
|
921
|
+
function isRecord(value) {
|
|
922
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
923
|
+
}
|
|
924
|
+
function abort(errorCode) {
|
|
925
|
+
throw new SmokeAbort(errorCode);
|
|
926
|
+
}
|
|
927
|
+
function safeNow(now) {
|
|
928
|
+
const value = now();
|
|
929
|
+
return Number.isFinite(value) ? value : 0;
|
|
930
|
+
}
|
|
931
|
+
function elapsed(startedAt, finishedAt) {
|
|
932
|
+
return Math.max(0, Math.round(finishedAt - startedAt));
|
|
933
|
+
}
|
|
934
|
+
async function defaultSleep(milliseconds) {
|
|
935
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
936
|
+
}
|
|
937
|
+
//# sourceMappingURL=live-lifecycle-smoke.js.map
|