@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +19 -2
  3. package/dist/src/claude-local-transcript-provider.d.ts +16 -0
  4. package/dist/src/claude-local-transcript-provider.js +111 -47
  5. package/dist/src/claude-local-transcript-provider.js.map +1 -1
  6. package/dist/src/cli.js +3738 -812
  7. package/dist/src/cli.js.map +1 -1
  8. package/dist/src/codex-lifecycle-compatibility.d.ts +2 -0
  9. package/dist/src/codex-lifecycle-compatibility.js +13 -0
  10. package/dist/src/codex-lifecycle-compatibility.js.map +1 -0
  11. package/dist/src/codex-store-adapter.js +8 -7
  12. package/dist/src/codex-store-adapter.js.map +1 -1
  13. package/dist/src/codex-terminal-agent-adapter.js +9 -8
  14. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  15. package/dist/src/live-lifecycle-evidence.d.ts +232 -0
  16. package/dist/src/live-lifecycle-evidence.js +1157 -0
  17. package/dist/src/live-lifecycle-evidence.js.map +1 -0
  18. package/dist/src/live-lifecycle-smoke-evidence.d.ts +13 -0
  19. package/dist/src/live-lifecycle-smoke-evidence.js +167 -0
  20. package/dist/src/live-lifecycle-smoke-evidence.js.map +1 -0
  21. package/dist/src/live-lifecycle-smoke.d.ts +123 -0
  22. package/dist/src/live-lifecycle-smoke.js +937 -0
  23. package/dist/src/live-lifecycle-smoke.js.map +1 -0
  24. package/dist/src/openclaw-plugin-helpers.d.ts +1 -0
  25. package/dist/src/openclaw-plugin-helpers.js +30 -2
  26. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  27. package/dist/src/openclaw-plugin.js +193 -31
  28. package/dist/src/openclaw-plugin.js.map +1 -1
  29. package/dist/src/protocol.d.ts +14 -0
  30. package/dist/src/protocol.js +87 -0
  31. package/dist/src/protocol.js.map +1 -1
  32. package/dist/src/store.js +17 -2
  33. package/dist/src/store.js.map +1 -1
  34. package/dist/src/terminal-agent-bridge.d.ts +31 -2
  35. package/dist/src/terminal-agent-bridge.js +231 -11
  36. package/dist/src/terminal-agent-bridge.js.map +1 -1
  37. package/dist/src/terminal-submission-acceptance.d.ts +78 -0
  38. package/dist/src/terminal-submission-acceptance.js +454 -0
  39. package/dist/src/terminal-submission-acceptance.js.map +1 -0
  40. 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