@scotthuang/agent-knock-knock 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +18 -8
  3. package/dist/src/agent-session-provider.d.ts +1 -1
  4. package/dist/src/claude-local-transcript-provider.d.ts +29 -1
  5. package/dist/src/claude-local-transcript-provider.js +313 -0
  6. package/dist/src/claude-local-transcript-provider.js.map +1 -1
  7. package/dist/src/claude-terminal-agent-adapter.d.ts +6 -1
  8. package/dist/src/claude-terminal-agent-adapter.js +178 -0
  9. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  10. package/dist/src/cli.js +4745 -355
  11. package/dist/src/cli.js.map +1 -1
  12. package/dist/src/codex-local-session-provider.d.ts +2 -2
  13. package/dist/src/codex-local-session-provider.js +2 -2
  14. package/dist/src/codex-local-session-provider.js.map +1 -1
  15. package/dist/src/codex-store-adapter.d.ts +11 -3
  16. package/dist/src/codex-store-adapter.js +371 -4
  17. package/dist/src/codex-store-adapter.js.map +1 -1
  18. package/dist/src/codex-terminal-agent-adapter.d.ts +6 -1
  19. package/dist/src/codex-terminal-agent-adapter.js +192 -0
  20. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  21. package/dist/src/managed-session.d.ts +163 -0
  22. package/dist/src/managed-session.js +873 -0
  23. package/dist/src/managed-session.js.map +1 -0
  24. package/dist/src/native-thread-lifecycle-policy.d.ts +57 -0
  25. package/dist/src/native-thread-lifecycle-policy.js +116 -0
  26. package/dist/src/native-thread-lifecycle-policy.js.map +1 -0
  27. package/dist/src/openclaw-plugin-helpers.d.ts +31 -6
  28. package/dist/src/openclaw-plugin-helpers.js +226 -9
  29. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  30. package/dist/src/openclaw-plugin.js +238 -23
  31. package/dist/src/openclaw-plugin.js.map +1 -1
  32. package/dist/src/protocol.d.ts +6 -0
  33. package/dist/src/protocol.js.map +1 -1
  34. package/dist/src/session-store.d.ts +57 -0
  35. package/dist/src/session-store.js +529 -0
  36. package/dist/src/session-store.js.map +1 -0
  37. package/dist/src/store.d.ts +2 -1
  38. package/dist/src/store.js +233 -21
  39. package/dist/src/store.js.map +1 -1
  40. package/dist/src/terminal-agent-adapter.d.ts +153 -0
  41. package/dist/src/terminal-agent-adapter.js +12 -0
  42. package/dist/src/terminal-agent-adapter.js.map +1 -1
  43. package/dist/src/terminal-agent-bridge.d.ts +8 -0
  44. package/dist/src/terminal-agent-bridge.js +15 -0
  45. package/dist/src/terminal-agent-bridge.js.map +1 -1
  46. package/dist/src/terminal-control-provider.d.ts +2 -0
  47. package/dist/src/terminal-control-provider.js +1 -0
  48. package/dist/src/terminal-control-provider.js.map +1 -1
  49. package/docs/quickstart-tmux.md +12 -1
  50. package/openclaw.plugin.json +14 -2
  51. package/package.json +1 -1
  52. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +23 -5
@@ -0,0 +1,873 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { executorForConversation } from "./protocol.js";
3
+ export const MANAGED_SESSION_SCHEMA = "agent-knock-knock/session";
4
+ export const MANAGED_SESSION_VERSION = 1;
5
+ export const NATIVE_THREAD_TRANSITION_SCHEMA = "agent-knock-knock/native-thread-transition";
6
+ export const NATIVE_THREAD_TRANSITION_VERSION = 1;
7
+ export function createManagedSessionId(now = new Date()) {
8
+ return `session-${formatTimestamp(now)}-${randomUUID().slice(0, 8)}`;
9
+ }
10
+ export function createTerminalBindingId() {
11
+ return `binding-${randomUUID()}`;
12
+ }
13
+ export function createNativeThreadTransitionId() {
14
+ return `transition-${randomUUID()}`;
15
+ }
16
+ export function managedSessionStorageKey(sessionId) {
17
+ assertManagedSessionId(sessionId);
18
+ return createHash("sha256").update(sessionId).digest("hex");
19
+ }
20
+ export function managedSessionBindingToken(state) {
21
+ return createHash("sha256")
22
+ .update(JSON.stringify({
23
+ session_id: state.session_id,
24
+ status: state.status,
25
+ binding_id: state.binding?.binding_id ?? null,
26
+ binding_generation: state.binding?.generation ?? null,
27
+ terminal_id: state.binding?.terminal_id ?? null,
28
+ terminal_target: state.binding?.terminal_control.target ?? null,
29
+ socket_path: state.binding?.terminal_control.socketPath ?? null,
30
+ pane_pid: state.binding?.terminal_control.panePid ?? null,
31
+ agent_pid: state.binding?.native_process.pid ?? null,
32
+ process_uuid: state.binding?.native_process.process_uuid ?? null,
33
+ process_birth: state.binding?.native_process.process_birth ?? null,
34
+ rollout: state.binding?.native_process.rollout ?? null,
35
+ native_thread_id: state.binding?.native_thread_id ?? null
36
+ }))
37
+ .digest("hex");
38
+ }
39
+ export function unmanagedTerminalBindingToken(value) {
40
+ return createHash("sha256")
41
+ .update(JSON.stringify({
42
+ state: "unmanaged",
43
+ terminal_id: value.terminalId,
44
+ terminal_target: value.terminalControl.target,
45
+ socket_path: value.terminalControl.socketPath ?? null,
46
+ pane_pid: value.terminalControl.panePid,
47
+ agent: value.agent,
48
+ agent_pid: value.pid,
49
+ workspace: value.workspace,
50
+ native_thread_id: value.nativeThreadId ?? null,
51
+ process_uuid: value.processUuid ?? null,
52
+ process_birth: value.processBirth ?? null,
53
+ rollout: value.rollout ?? null
54
+ }))
55
+ .digest("hex");
56
+ }
57
+ export function terminalBindingFrom(value) {
58
+ const now = (value.now ?? new Date()).toISOString();
59
+ const binding = {
60
+ binding_id: createTerminalBindingId(),
61
+ generation: value.generation,
62
+ terminal_id: value.terminalId,
63
+ terminal_control: value.terminalControl,
64
+ native_thread_id: value.nativeThreadId,
65
+ native_process: {
66
+ pid: value.pid,
67
+ process_uuid: value.processUuid,
68
+ process_birth: value.processBirth,
69
+ rollout: value.rollout,
70
+ evidence: value.evidence
71
+ },
72
+ bound_at: now,
73
+ last_verified_at: now
74
+ };
75
+ assertManagedTerminalBinding(binding, "terminal binding");
76
+ return binding;
77
+ }
78
+ export function nativeThreadCommandFingerprint(command) {
79
+ return createHash("sha256").update(command).digest("hex");
80
+ }
81
+ export function isExactNativeThreadId(value) {
82
+ return typeof value === "string" &&
83
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(value);
84
+ }
85
+ export function assertManagedSessionId(value) {
86
+ if (typeof value !== "string" ||
87
+ value.trim().length === 0 ||
88
+ value.includes("\0")) {
89
+ throw new Error("session id must be a non-empty string without NUL bytes");
90
+ }
91
+ }
92
+ export function assertManagedSessionState(value, expectedSessionId, options = {}) {
93
+ if (!isRecord(value)) {
94
+ throw new Error("managed session state must be an object");
95
+ }
96
+ assertOnlyKeys(value, [
97
+ "schema",
98
+ "version",
99
+ "session_id",
100
+ "revision",
101
+ "agent",
102
+ "workspace",
103
+ "status",
104
+ "binding",
105
+ "lineage",
106
+ "created_at",
107
+ "updated_at",
108
+ "detached_at",
109
+ "quarantine_reason",
110
+ "last_transition_id"
111
+ ], "managed session state");
112
+ if (value.schema !== MANAGED_SESSION_SCHEMA ||
113
+ value.version !== MANAGED_SESSION_VERSION) {
114
+ throw new Error("managed session state has an unsupported schema or version");
115
+ }
116
+ assertManagedSessionId(value.session_id);
117
+ if (expectedSessionId !== undefined && value.session_id !== expectedSessionId) {
118
+ throw new Error(`managed session id ${value.session_id} does not match ${expectedSessionId}`);
119
+ }
120
+ if (value.revision === undefined
121
+ ? !options.allowMissingRevision
122
+ : !isPositiveSafeInteger(value.revision)) {
123
+ throw new Error("managed session revision must be a positive safe integer");
124
+ }
125
+ assertExecutorKind(value.agent, "managed session agent");
126
+ assertNonEmptyString(value.workspace, "managed session workspace");
127
+ if (!MANAGED_SESSION_STATUSES.has(value.status)) {
128
+ throw new Error("managed session status is invalid");
129
+ }
130
+ assertManagedSessionLineage(value.lineage);
131
+ assertTimestamp(value.created_at, "managed session created_at");
132
+ assertTimestamp(value.updated_at, "managed session updated_at");
133
+ assertOptionalTimestamp(value.detached_at, "managed session detached_at");
134
+ assertOptionalNonEmptyString(value.quarantine_reason, "managed session quarantine_reason");
135
+ assertOptionalNonEmptyString(value.last_transition_id, "managed session last_transition_id");
136
+ if (value.binding !== undefined) {
137
+ assertManagedTerminalBinding(value.binding, "managed session binding");
138
+ }
139
+ if (value.status === "bound" && value.binding === undefined) {
140
+ throw new Error("a bound managed session requires a terminal binding");
141
+ }
142
+ if (value.status === "transitioning" && value.binding === undefined) {
143
+ throw new Error("a transitioning managed session requires a terminal binding");
144
+ }
145
+ if (value.status === "quarantined" && value.quarantine_reason === undefined) {
146
+ throw new Error("a quarantined managed session requires quarantine_reason");
147
+ }
148
+ }
149
+ export function assertNativeThreadTransition(value, expectedTransitionId, options = {}) {
150
+ if (!isRecord(value)) {
151
+ throw new Error("native thread transition state must be an object");
152
+ }
153
+ assertOnlyKeys(value, [
154
+ "schema",
155
+ "version",
156
+ "transition_id",
157
+ "revision",
158
+ "operation",
159
+ "status",
160
+ "terminal_id",
161
+ "agent",
162
+ "workspace",
163
+ "source_session_id",
164
+ "source_expected_revision",
165
+ "source_previous_last_transition_id",
166
+ "target_session_id",
167
+ "target_expected_revision",
168
+ "target_native_thread_id",
169
+ "target_candidate_file_identity",
170
+ "before_native_thread_id",
171
+ "before_process_uuid",
172
+ "before_process_started_at",
173
+ "before_process_birth",
174
+ "before_process_rollout",
175
+ "before_binding",
176
+ "after_binding",
177
+ "adapter_version",
178
+ "command_fingerprint",
179
+ "dispatcher_pid",
180
+ "prepared_at",
181
+ "dispatching_at",
182
+ "submitted_at",
183
+ "verified_at",
184
+ "committed_at",
185
+ "aborted_at",
186
+ "uncertain_at",
187
+ "reconciled_outcome",
188
+ "reconciled_at",
189
+ "error",
190
+ "do_not_retry"
191
+ ], "native thread transition state");
192
+ if (value.schema !== NATIVE_THREAD_TRANSITION_SCHEMA ||
193
+ value.version !== NATIVE_THREAD_TRANSITION_VERSION) {
194
+ throw new Error("native thread transition has an unsupported schema or version");
195
+ }
196
+ assertStorageRecordId(value.transition_id, "transition id");
197
+ if (expectedTransitionId !== undefined &&
198
+ value.transition_id !== expectedTransitionId) {
199
+ throw new Error(`transition id ${value.transition_id} does not match ${expectedTransitionId}`);
200
+ }
201
+ if (value.revision === undefined
202
+ ? !options.allowMissingRevision
203
+ : !isPositiveSafeInteger(value.revision)) {
204
+ throw new Error("native thread transition revision must be a positive safe integer");
205
+ }
206
+ if (!NATIVE_THREAD_OPERATIONS.has(value.operation)) {
207
+ throw new Error("native thread transition operation is invalid");
208
+ }
209
+ if (!NATIVE_THREAD_TRANSITION_STATUSES.has(value.status)) {
210
+ throw new Error("native thread transition status is invalid");
211
+ }
212
+ assertNonEmptyString(value.terminal_id, "native thread transition terminal_id");
213
+ assertExecutorKind(value.agent, "native thread transition agent");
214
+ assertNonEmptyString(value.workspace, "native thread transition workspace");
215
+ if (value.source_session_id !== undefined) {
216
+ assertManagedSessionId(value.source_session_id);
217
+ }
218
+ if (value.source_expected_revision !== undefined &&
219
+ !isPositiveSafeInteger(value.source_expected_revision)) {
220
+ throw new Error("native thread transition source_expected_revision must be a positive safe integer");
221
+ }
222
+ if ((value.source_session_id === undefined) !==
223
+ (value.source_expected_revision === undefined)) {
224
+ throw new Error("native thread transition source identity and expected revision must appear together");
225
+ }
226
+ assertOptionalNonEmptyString(value.source_previous_last_transition_id, "native thread transition source_previous_last_transition_id");
227
+ if (value.source_previous_last_transition_id !== undefined &&
228
+ value.source_session_id === undefined) {
229
+ throw new Error("native thread transition source_previous_last_transition_id requires a source Session");
230
+ }
231
+ assertManagedSessionId(value.target_session_id);
232
+ if (value.target_expected_revision !== null &&
233
+ !isPositiveSafeInteger(value.target_expected_revision)) {
234
+ throw new Error("native thread transition target_expected_revision must be null or a positive safe integer");
235
+ }
236
+ if (value.target_native_thread_id !== undefined) {
237
+ assertExactNativeThreadId(value.target_native_thread_id, "native thread transition target_native_thread_id");
238
+ }
239
+ if (value.target_candidate_file_identity !== undefined) {
240
+ if (!isRecord(value.target_candidate_file_identity)) {
241
+ throw new Error("native thread transition target_candidate_file_identity must be an object");
242
+ }
243
+ assertOnlyKeys(value.target_candidate_file_identity, [
244
+ "path",
245
+ "device",
246
+ "inode"
247
+ ], "native thread transition target_candidate_file_identity");
248
+ assertNonEmptyString(value.target_candidate_file_identity.path, "native thread transition target candidate path");
249
+ assertNonEmptyString(value.target_candidate_file_identity.device, "native thread transition target candidate device");
250
+ assertNonEmptyString(value.target_candidate_file_identity.inode, "native thread transition target candidate inode");
251
+ if (value.agent !== "codex" ||
252
+ value.operation !== "resume_thread") {
253
+ throw new Error("only a Codex resume transition can carry target candidate file identity");
254
+ }
255
+ }
256
+ assertExactNativeThreadId(value.before_native_thread_id, "native thread transition before_native_thread_id");
257
+ assertNonEmptyString(value.before_process_uuid, "native thread transition before_process_uuid");
258
+ if (value.before_process_started_at !== undefined &&
259
+ !isPositiveSafeInteger(value.before_process_started_at)) {
260
+ throw new Error("native thread transition before_process_started_at must be a positive safe integer");
261
+ }
262
+ assertOptionalNonEmptyString(value.before_process_birth, "native thread transition before_process_birth");
263
+ if (value.before_process_rollout !== undefined) {
264
+ assertRollout(value.before_process_rollout, "native thread transition before_process_rollout");
265
+ }
266
+ if (value.agent === "codex" && value.before_process_birth === undefined) {
267
+ throw new Error("Codex transition requires before_process_birth");
268
+ }
269
+ if (value.agent === "claude" &&
270
+ value.before_process_started_at === undefined) {
271
+ throw new Error("Claude transition requires before_process_started_at");
272
+ }
273
+ if (value.operation === "resume_thread" &&
274
+ value.target_native_thread_id === undefined) {
275
+ throw new Error("resume_thread requires target_native_thread_id");
276
+ }
277
+ if (value.operation === "new_thread" &&
278
+ value.target_native_thread_id !== undefined) {
279
+ throw new Error("new_thread cannot carry target_native_thread_id");
280
+ }
281
+ if (value.source_session_id !== undefined &&
282
+ value.source_session_id === value.target_session_id) {
283
+ throw new Error("native thread transition source and target Sessions must differ");
284
+ }
285
+ if (value.before_binding !== undefined) {
286
+ assertManagedTerminalBinding(value.before_binding, "before_binding");
287
+ }
288
+ if (value.after_binding !== undefined) {
289
+ assertManagedTerminalBinding(value.after_binding, "after_binding");
290
+ }
291
+ assertNativeThreadTransitionBindingConsistency(value);
292
+ assertNonEmptyString(value.adapter_version, "native thread transition adapter_version");
293
+ if (typeof value.command_fingerprint !== "string" ||
294
+ !/^[0-9a-f]{64}$/u.test(value.command_fingerprint)) {
295
+ throw new Error("native thread transition command_fingerprint must be SHA-256 hex");
296
+ }
297
+ if (!isPositiveSafeInteger(value.dispatcher_pid)) {
298
+ throw new Error("native thread transition dispatcher_pid must be positive");
299
+ }
300
+ assertTimestamp(value.prepared_at, "native thread transition prepared_at");
301
+ for (const field of [
302
+ "dispatching_at",
303
+ "submitted_at",
304
+ "verified_at",
305
+ "committed_at",
306
+ "aborted_at",
307
+ "uncertain_at"
308
+ ]) {
309
+ assertOptionalTimestamp(value[field], `native thread transition ${field}`);
310
+ }
311
+ assertOptionalNonEmptyString(value.error, "native thread transition error");
312
+ if (value.reconciled_outcome !== undefined &&
313
+ !["before", "after"].includes(String(value.reconciled_outcome))) {
314
+ throw new Error("native thread transition reconciled_outcome is invalid");
315
+ }
316
+ assertOptionalTimestamp(value.reconciled_at, "native thread transition reconciled_at");
317
+ if ((value.reconciled_outcome === undefined) !==
318
+ (value.reconciled_at === undefined)) {
319
+ throw new Error("native thread transition reconciliation outcome and timestamp must appear together");
320
+ }
321
+ if (value.do_not_retry !== undefined && typeof value.do_not_retry !== "boolean") {
322
+ throw new Error("native thread transition do_not_retry must be boolean");
323
+ }
324
+ if (["dispatching", "submitted", "uncertain", "verified", "committed"].includes(String(value.status)) && value.dispatching_at === undefined) {
325
+ throw new Error(`${value.status} transition requires dispatching_at`);
326
+ }
327
+ if (["submitted", "verified", "committed"].includes(String(value.status)) && value.submitted_at === undefined) {
328
+ throw new Error(`${value.status} transition requires submitted_at`);
329
+ }
330
+ if (["verified", "committed"].includes(String(value.status)) &&
331
+ (value.verified_at === undefined || value.after_binding === undefined)) {
332
+ throw new Error(`${value.status} transition requires verified_at and after_binding`);
333
+ }
334
+ if (value.status === "committed" && value.committed_at === undefined) {
335
+ throw new Error("committed transition requires committed_at");
336
+ }
337
+ if (value.status === "aborted" && value.aborted_at === undefined) {
338
+ throw new Error("aborted transition requires aborted_at");
339
+ }
340
+ if (value.status === "uncertain" && value.uncertain_at === undefined) {
341
+ throw new Error("uncertain transition requires uncertain_at");
342
+ }
343
+ }
344
+ function assertNativeThreadTransitionBindingConsistency(transition) {
345
+ if ((transition.reconciled_outcome === "before" &&
346
+ transition.status !== "aborted") ||
347
+ (transition.reconciled_outcome === "after" &&
348
+ !["verified", "committed"].includes(transition.status))) {
349
+ throw new Error("native thread transition reconciliation outcome disagrees with status");
350
+ }
351
+ const before = transition.before_binding;
352
+ if (before) {
353
+ if (before.terminal_id !== transition.terminal_id ||
354
+ before.native_thread_id?.toLowerCase() !==
355
+ transition.before_native_thread_id.toLowerCase() ||
356
+ before.native_process.process_uuid !== transition.before_process_uuid ||
357
+ before.native_process.process_birth !== transition.before_process_birth ||
358
+ JSON.stringify(before.native_process.rollout ?? null) !==
359
+ JSON.stringify(transition.before_process_rollout ?? null)) {
360
+ throw new Error("native thread transition before_binding disagrees with prepare identity");
361
+ }
362
+ }
363
+ const after = transition.after_binding;
364
+ if (!after) {
365
+ return;
366
+ }
367
+ if (!["verified", "committed"].includes(transition.status)) {
368
+ throw new Error("native thread transition cannot carry after_binding before verification");
369
+ }
370
+ if (after.terminal_id !== transition.terminal_id ||
371
+ !after.native_thread_id ||
372
+ after.native_process.process_uuid !== transition.before_process_uuid ||
373
+ after.native_process.process_birth !== transition.before_process_birth) {
374
+ throw new Error("native thread transition after_binding disagrees with terminal or process identity");
375
+ }
376
+ if (before &&
377
+ (JSON.stringify(after.terminal_control) !==
378
+ JSON.stringify(before.terminal_control) ||
379
+ after.native_process.pid !== before.native_process.pid)) {
380
+ throw new Error("native thread transition changed terminal control or process PID");
381
+ }
382
+ const afterNativeThreadId = after.native_thread_id.toLowerCase();
383
+ if (transition.operation === "resume_thread"
384
+ ? afterNativeThreadId !== transition.target_native_thread_id?.toLowerCase()
385
+ : afterNativeThreadId === transition.before_native_thread_id.toLowerCase()) {
386
+ throw new Error("native thread transition after_binding does not satisfy its operation");
387
+ }
388
+ }
389
+ /**
390
+ * Materialize protocol-3 Session records from predecessor Turn records. The
391
+ * derivation never picks a Turn by mutable recency. A binding is accepted only
392
+ * from one unambiguous binding generation; otherwise the Session is
393
+ * quarantined and cannot become a routing authority.
394
+ */
395
+ export function managedSessionStatesFromConversations(conversations) {
396
+ const groups = new Map();
397
+ for (const conversation of conversations) {
398
+ const sessionId = requiredConversationSessionId(conversation);
399
+ const group = groups.get(sessionId) ?? [];
400
+ group.push(conversation);
401
+ groups.set(sessionId, group);
402
+ }
403
+ const states = [...groups.entries()]
404
+ .sort(([left], [right]) => left.localeCompare(right))
405
+ .map(([sessionId, turns]) => migratedSessionState(sessionId, turns));
406
+ const owners = new Map();
407
+ for (const state of states) {
408
+ const nativeThreadId = state.binding?.native_thread_id?.toLowerCase();
409
+ if (!nativeThreadId) {
410
+ continue;
411
+ }
412
+ const key = `${state.agent}\0${nativeThreadId}`;
413
+ const sessionIds = owners.get(key) ?? [];
414
+ sessionIds.push(state.session_id);
415
+ owners.set(key, sessionIds);
416
+ }
417
+ const conflicts = new Map();
418
+ for (const sessionIds of owners.values()) {
419
+ if (sessionIds.length < 2) {
420
+ continue;
421
+ }
422
+ const sorted = [...sessionIds].sort();
423
+ for (const sessionId of sorted) {
424
+ conflicts.set(sessionId, sorted.filter((candidate) => candidate !== sessionId));
425
+ }
426
+ }
427
+ return states.map((state) => {
428
+ const duplicateOwners = conflicts.get(state.session_id);
429
+ if (!duplicateOwners) {
430
+ return state;
431
+ }
432
+ const duplicateReason = "migrated native thread is referenced by multiple managed Sessions: " +
433
+ duplicateOwners.join(", ");
434
+ return {
435
+ ...state,
436
+ status: "quarantined",
437
+ quarantine_reason: state.quarantine_reason
438
+ ? `${state.quarantine_reason}; ${duplicateReason}`
439
+ : duplicateReason
440
+ };
441
+ });
442
+ }
443
+ function migratedSessionState(sessionId, turns) {
444
+ const agents = new Set(turns.map((turn) => executorForConversation(turn).kind));
445
+ const workspaces = new Set(turns.map((turn) => turn.workspace));
446
+ if (agents.size !== 1 || workspaces.size !== 1) {
447
+ throw new Error(`cannot migrate Session ${sessionId}: its Turns disagree on agent or workspace`);
448
+ }
449
+ const agent = [...agents][0];
450
+ const workspace = [...workspaces][0];
451
+ assertExecutorKind(agent, "migrated Session agent");
452
+ assertNonEmptyString(workspace, "migrated Session workspace");
453
+ const createdAt = minimumTimestamp(turns.map((turn) => turn.created_at), `Session ${sessionId} created_at`);
454
+ const updatedAt = maximumTimestamp(turns.map((turn) => turn.updated_at), `Session ${sessionId} updated_at`);
455
+ const candidates = turns
456
+ .map((turn) => migratedBindingCandidate(turn, agent))
457
+ .filter((candidate) => Boolean(candidate));
458
+ const selection = selectMigratedBinding(sessionId, candidates);
459
+ const common = {
460
+ schema: MANAGED_SESSION_SCHEMA,
461
+ version: MANAGED_SESSION_VERSION,
462
+ session_id: sessionId,
463
+ revision: 1,
464
+ agent,
465
+ workspace,
466
+ lineage: { created_by: "migration" },
467
+ created_at: createdAt,
468
+ updated_at: updatedAt
469
+ };
470
+ if (selection.kind === "none") {
471
+ return { ...common, status: "detached" };
472
+ }
473
+ if (selection.kind === "ambiguous") {
474
+ return {
475
+ ...common,
476
+ status: "quarantined",
477
+ quarantine_reason: selection.reason
478
+ };
479
+ }
480
+ const strong = hasStrongIncarnationEvidence(agent, selection.binding);
481
+ if (!strong) {
482
+ return {
483
+ ...common,
484
+ status: "quarantined",
485
+ binding: selection.binding,
486
+ quarantine_reason: "migrated Turn binding lacks exact native process-incarnation evidence"
487
+ };
488
+ }
489
+ return { ...common, status: "bound", binding: selection.binding };
490
+ }
491
+ function migratedBindingCandidate(conversation, agent) {
492
+ const takeover = isRecord(conversation.native_session_takeover)
493
+ ? conversation.native_session_takeover
494
+ : undefined;
495
+ if (!takeover || !isRecord(takeover.terminal_control)) {
496
+ return undefined;
497
+ }
498
+ const terminalControl = takeover.terminal_control;
499
+ try {
500
+ assertTerminalControlRef(terminalControl, "migrated terminal_control");
501
+ }
502
+ catch {
503
+ return undefined;
504
+ }
505
+ const terminalId = optionalNonEmptyString(takeover.native_session_id);
506
+ const pid = positiveInteger(takeover.terminal_agent_pid);
507
+ if (!terminalId || pid === undefined) {
508
+ return undefined;
509
+ }
510
+ const topBindingId = optionalNonEmptyString(conversation.terminal_binding_id);
511
+ const nestedBindingId = optionalNonEmptyString(takeover.terminal_binding_id);
512
+ if (topBindingId && nestedBindingId && topBindingId !== nestedBindingId) {
513
+ return undefined;
514
+ }
515
+ const topGeneration = positiveInteger(conversation.terminal_binding_generation);
516
+ const nestedGeneration = positiveInteger(takeover.terminal_binding_generation);
517
+ if (topGeneration !== undefined &&
518
+ nestedGeneration !== undefined &&
519
+ topGeneration !== nestedGeneration) {
520
+ return undefined;
521
+ }
522
+ const nativeIds = [
523
+ optionalNonEmptyString(conversation.native_thread_id),
524
+ optionalNonEmptyString(takeover.terminal_agent_expected_session_id),
525
+ optionalNonEmptyString(takeover.terminal_agent_session_id)
526
+ ].filter((value) => Boolean(value));
527
+ const exactNativeIds = [...new Set(nativeIds.filter(isExactNativeThreadId))];
528
+ if (exactNativeIds.length > 1) {
529
+ return undefined;
530
+ }
531
+ const nativeThreadId = exactNativeIds[0];
532
+ const rollout = migratedRollout(takeover.terminal_agent_rollout);
533
+ const processUuid = optionalNonEmptyString(takeover.terminal_agent_process_uuid);
534
+ const processBirth = optionalNonEmptyString(takeover.terminal_agent_process_birth);
535
+ const evidence = optionalNonEmptyString(takeover.terminal_agent_identity_evidence) ?? "store_protocol_migration";
536
+ const generation = topGeneration ?? nestedGeneration ?? 1;
537
+ const identity = {
538
+ agent,
539
+ terminal_id: terminalId,
540
+ terminal_control: terminalControl,
541
+ native_thread_id: nativeThreadId ?? null,
542
+ pid,
543
+ process_uuid: processUuid ?? null,
544
+ process_birth: processBirth ?? null,
545
+ rollout: rollout ?? null
546
+ };
547
+ const identityFingerprint = stableHash(identity);
548
+ const bindingId = topBindingId ?? nestedBindingId ??
549
+ `binding-migration-${identityFingerprint.slice(0, 32)}`;
550
+ const observedAt = validTimestamp(conversation.created_at) ??
551
+ validTimestamp(conversation.updated_at);
552
+ if (!observedAt) {
553
+ return undefined;
554
+ }
555
+ const binding = {
556
+ binding_id: bindingId,
557
+ generation,
558
+ terminal_id: terminalId,
559
+ terminal_control: terminalControl,
560
+ native_thread_id: nativeThreadId,
561
+ native_process: {
562
+ pid,
563
+ process_uuid: processUuid,
564
+ process_birth: processBirth,
565
+ rollout,
566
+ evidence
567
+ },
568
+ bound_at: observedAt,
569
+ last_verified_at: validTimestamp(conversation.updated_at) ?? observedAt
570
+ };
571
+ assertManagedTerminalBinding(binding, "migrated terminal binding");
572
+ return {
573
+ binding,
574
+ identityFingerprint,
575
+ explicitBindingId: Boolean(topBindingId ?? nestedBindingId)
576
+ };
577
+ }
578
+ function selectMigratedBinding(sessionId, candidates) {
579
+ if (candidates.length === 0) {
580
+ return { kind: "none" };
581
+ }
582
+ const maxGeneration = Math.max(...candidates.map(({ binding }) => binding.generation));
583
+ const latestGeneration = candidates.filter(({ binding }) => binding.generation === maxGeneration);
584
+ const fingerprints = new Set(latestGeneration.map(({ identityFingerprint }) => identityFingerprint));
585
+ const explicitIds = new Set(latestGeneration
586
+ .filter(({ explicitBindingId }) => explicitBindingId)
587
+ .map(({ binding }) => binding.binding_id));
588
+ if (fingerprints.size !== 1 || explicitIds.size > 1) {
589
+ return {
590
+ kind: "ambiguous",
591
+ reason: `migrated Turns for Session ${sessionId} contain conflicting binding generation ${maxGeneration}`
592
+ };
593
+ }
594
+ const selected = latestGeneration[0].binding;
595
+ const explicitId = explicitIds.values().next().value;
596
+ const boundAt = minimumTimestamp(latestGeneration.map(({ binding }) => binding.bound_at), `Session ${sessionId} binding bound_at`);
597
+ const lastVerifiedAt = maximumTimestamp(latestGeneration.map(({ binding }) => binding.last_verified_at), `Session ${sessionId} binding last_verified_at`);
598
+ return {
599
+ kind: "selected",
600
+ binding: {
601
+ ...selected,
602
+ binding_id: explicitId ?? selected.binding_id,
603
+ bound_at: boundAt,
604
+ last_verified_at: lastVerifiedAt
605
+ }
606
+ };
607
+ }
608
+ function hasStrongIncarnationEvidence(agent, binding) {
609
+ if (!binding.native_thread_id) {
610
+ return false;
611
+ }
612
+ return agent === "claude"
613
+ ? Boolean(binding.native_process.process_uuid)
614
+ : Boolean(binding.native_process.process_birth && binding.native_process.rollout);
615
+ }
616
+ function requiredConversationSessionId(conversation) {
617
+ const value = conversation.session_id ?? conversation.conversation_id;
618
+ assertManagedSessionId(value);
619
+ return value;
620
+ }
621
+ function assertManagedSessionLineage(value) {
622
+ if (!isRecord(value)) {
623
+ throw new Error("managed session lineage must be an object");
624
+ }
625
+ assertOnlyKeys(value, [
626
+ "created_by",
627
+ "previous_session_id",
628
+ "resumed_from_native_thread_id",
629
+ "transition_id"
630
+ ], "managed session lineage");
631
+ if (!MANAGED_SESSION_LINEAGE_KINDS.has(value.created_by)) {
632
+ throw new Error("managed session lineage created_by is invalid");
633
+ }
634
+ if (value.previous_session_id !== undefined) {
635
+ assertManagedSessionId(value.previous_session_id);
636
+ }
637
+ if (value.resumed_from_native_thread_id !== undefined) {
638
+ assertExactNativeThreadId(value.resumed_from_native_thread_id, "managed session lineage resumed_from_native_thread_id");
639
+ }
640
+ assertOptionalNonEmptyString(value.transition_id, "managed session lineage transition_id");
641
+ }
642
+ function assertManagedTerminalBinding(value, label) {
643
+ if (!isRecord(value)) {
644
+ throw new Error(`${label} must be an object`);
645
+ }
646
+ assertOnlyKeys(value, [
647
+ "binding_id",
648
+ "generation",
649
+ "terminal_id",
650
+ "terminal_control",
651
+ "native_thread_id",
652
+ "native_process",
653
+ "bound_at",
654
+ "last_verified_at"
655
+ ], label);
656
+ assertNonEmptyString(value.binding_id, `${label} binding_id`);
657
+ if (!isPositiveSafeInteger(value.generation)) {
658
+ throw new Error(`${label} generation must be a positive safe integer`);
659
+ }
660
+ assertNonEmptyString(value.terminal_id, `${label} terminal_id`);
661
+ assertTerminalControlRef(value.terminal_control, `${label} terminal_control`);
662
+ if (value.native_thread_id !== undefined) {
663
+ // Persist the exact opaque id reported by the adapter. Current Codex and
664
+ // Claude lifecycle selectors are UUIDs, but a Session binding must not
665
+ // truncate or reinterpret a future/native version-specific identity.
666
+ assertNonEmptyString(value.native_thread_id, `${label} native_thread_id`);
667
+ }
668
+ assertNativeProcessIdentity(value.native_process, `${label} native_process`);
669
+ assertTimestamp(value.bound_at, `${label} bound_at`);
670
+ assertTimestamp(value.last_verified_at, `${label} last_verified_at`);
671
+ }
672
+ function assertNativeProcessIdentity(value, label) {
673
+ if (!isRecord(value)) {
674
+ throw new Error(`${label} must be an object`);
675
+ }
676
+ assertOnlyKeys(value, [
677
+ "pid",
678
+ "process_uuid",
679
+ "process_birth",
680
+ "rollout",
681
+ "evidence"
682
+ ], label);
683
+ if (!isPositiveSafeInteger(value.pid)) {
684
+ throw new Error(`${label} pid must be a positive safe integer`);
685
+ }
686
+ assertOptionalNonEmptyString(value.process_uuid, `${label} process_uuid`);
687
+ assertOptionalNonEmptyString(value.process_birth, `${label} process_birth`);
688
+ if (value.rollout !== undefined) {
689
+ assertRollout(value.rollout, `${label} rollout`);
690
+ }
691
+ assertNonEmptyString(value.evidence, `${label} evidence`);
692
+ }
693
+ function assertRollout(value, label) {
694
+ if (!isRecord(value)) {
695
+ throw new Error(`${label} must be an object`);
696
+ }
697
+ assertOnlyKeys(value, ["fd", "device", "inode", "path"], label);
698
+ for (const field of ["fd", "device", "inode", "path"]) {
699
+ assertNonEmptyString(value[field], `${label} ${field}`);
700
+ }
701
+ }
702
+ function assertTerminalControlRef(value, label) {
703
+ if (!isRecord(value)) {
704
+ throw new Error(`${label} must be an object`);
705
+ }
706
+ assertOnlyKeys(value, [
707
+ "kind",
708
+ "target",
709
+ "socketPath",
710
+ "session",
711
+ "window",
712
+ "pane",
713
+ "panePid",
714
+ "currentCommand",
715
+ "currentPath",
716
+ "capabilities"
717
+ ], label);
718
+ if (value.kind !== "tmux") {
719
+ throw new Error(`${label} kind must be tmux`);
720
+ }
721
+ assertNonEmptyString(value.target, `${label} target`);
722
+ assertOptionalNonEmptyString(value.socketPath, `${label} socketPath`);
723
+ assertNonEmptyString(value.session, `${label} session`);
724
+ if (!isNonNegativeSafeInteger(value.window)) {
725
+ throw new Error(`${label} window must be a non-negative safe integer`);
726
+ }
727
+ if (!isNonNegativeSafeInteger(value.pane)) {
728
+ throw new Error(`${label} pane must be a non-negative safe integer`);
729
+ }
730
+ if (!isPositiveSafeInteger(value.panePid)) {
731
+ throw new Error(`${label} panePid must be a positive safe integer`);
732
+ }
733
+ assertOptionalNonEmptyString(value.currentCommand, `${label} currentCommand`);
734
+ assertOptionalNonEmptyString(value.currentPath, `${label} currentPath`);
735
+ if (!Array.isArray(value.capabilities) ||
736
+ value.capabilities.some((capability) => typeof capability !== "string" ||
737
+ !TERMINAL_CONTROL_CAPABILITIES.has(capability)) ||
738
+ new Set(value.capabilities).size !== value.capabilities.length) {
739
+ throw new Error(`${label} capabilities are invalid`);
740
+ }
741
+ }
742
+ function migratedRollout(value) {
743
+ try {
744
+ assertRollout(value, "migrated rollout");
745
+ return value;
746
+ }
747
+ catch {
748
+ return undefined;
749
+ }
750
+ }
751
+ function assertStorageRecordId(value, label) {
752
+ if (typeof value !== "string" ||
753
+ !value ||
754
+ value === "." ||
755
+ value === ".." ||
756
+ value.includes("\0") ||
757
+ /[/\\]/u.test(value)) {
758
+ throw new Error(`${label} is not safe for storage: ${String(value)}`);
759
+ }
760
+ }
761
+ function assertOnlyKeys(value, allowed, label) {
762
+ const allowedSet = new Set(allowed);
763
+ const unknown = Object.keys(value).find((key) => !allowedSet.has(key));
764
+ if (unknown !== undefined) {
765
+ throw new Error(`${label} contains unsupported field ${unknown}`);
766
+ }
767
+ }
768
+ function assertExecutorKind(value, label) {
769
+ if (value !== "codex" && value !== "claude") {
770
+ throw new Error(`${label} is invalid`);
771
+ }
772
+ }
773
+ function assertExactNativeThreadId(value, label) {
774
+ if (!isExactNativeThreadId(value)) {
775
+ throw new Error(`${label} must be an exact native thread UUID`);
776
+ }
777
+ }
778
+ function assertNonEmptyString(value, label) {
779
+ if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
780
+ throw new Error(`${label} must be a non-empty string`);
781
+ }
782
+ }
783
+ function assertOptionalNonEmptyString(value, label) {
784
+ if (value !== undefined) {
785
+ assertNonEmptyString(value, label);
786
+ }
787
+ }
788
+ function optionalNonEmptyString(value) {
789
+ return typeof value === "string" && value.trim().length > 0 && !value.includes("\0")
790
+ ? value
791
+ : undefined;
792
+ }
793
+ function assertTimestamp(value, label) {
794
+ if (validTimestamp(value) === undefined) {
795
+ throw new Error(`${label} must be a valid timestamp`);
796
+ }
797
+ }
798
+ function assertOptionalTimestamp(value, label) {
799
+ if (value !== undefined) {
800
+ assertTimestamp(value, label);
801
+ }
802
+ }
803
+ function validTimestamp(value) {
804
+ return typeof value === "string" && value.length > 0 && Number.isFinite(Date.parse(value))
805
+ ? value
806
+ : undefined;
807
+ }
808
+ function minimumTimestamp(values, label) {
809
+ const valid = values.map(validTimestamp);
810
+ if (valid.some((value) => value === undefined)) {
811
+ throw new Error(`${label} must be valid for every Turn`);
812
+ }
813
+ return valid.sort((left, right) => Date.parse(left) - Date.parse(right) || left.localeCompare(right))[0];
814
+ }
815
+ function maximumTimestamp(values, label) {
816
+ const valid = values.map(validTimestamp);
817
+ if (valid.some((value) => value === undefined)) {
818
+ throw new Error(`${label} must be valid for every Turn`);
819
+ }
820
+ return valid.sort((left, right) => Date.parse(right) - Date.parse(left) || right.localeCompare(left))[0];
821
+ }
822
+ function positiveInteger(value) {
823
+ return isPositiveSafeInteger(value) ? value : undefined;
824
+ }
825
+ function isPositiveSafeInteger(value) {
826
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
827
+ }
828
+ function isNonNegativeSafeInteger(value) {
829
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
830
+ }
831
+ function stableHash(value) {
832
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
833
+ }
834
+ function isRecord(value) {
835
+ return value !== null && typeof value === "object" && !Array.isArray(value);
836
+ }
837
+ const MANAGED_SESSION_STATUSES = new Set([
838
+ "bound",
839
+ "detached",
840
+ "transitioning",
841
+ "quarantined"
842
+ ]);
843
+ const MANAGED_SESSION_LINEAGE_KINDS = new Set([
844
+ "attach",
845
+ "new_thread",
846
+ "resume_thread",
847
+ "migration"
848
+ ]);
849
+ const NATIVE_THREAD_OPERATIONS = new Set([
850
+ "new_thread",
851
+ "resume_thread"
852
+ ]);
853
+ const NATIVE_THREAD_TRANSITION_STATUSES = new Set([
854
+ "prepared",
855
+ "dispatching",
856
+ "submitted",
857
+ "uncertain",
858
+ "verified",
859
+ "committed",
860
+ "aborted"
861
+ ]);
862
+ const TERMINAL_CONTROL_CAPABILITIES = new Set([
863
+ "screen_status",
864
+ "send_keys",
865
+ "terminal_approval",
866
+ "screen_completion",
867
+ "durable_completion",
868
+ "terminal_cancel"
869
+ ]);
870
+ function formatTimestamp(date) {
871
+ return date.toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
872
+ }
873
+ //# sourceMappingURL=managed-session.js.map