@scotthuang/agent-knock-knock 0.12.4 → 0.12.6

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.
@@ -0,0 +1,1135 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { assertManagedSessionId, assertManagedSessionState, isExactNativeThreadId, managedSessionBindingToken } from "./managed-session.js";
5
+ import { terminalEndpointIdentityFromEvidence } from "./terminal-control-ref.js";
6
+ import { assertStoreReadable, ensureDir, STORE_DEFERRED_FOREGROUND_TRANSFERS_DIRECTORY, withStoreWriterLease } from "./store.js";
7
+ export const DEFERRED_FOREGROUND_TRANSFER_SCHEMA = "agent-knock-knock/deferred-foreground-transfer";
8
+ export const DEFERRED_FOREGROUND_TRANSFER_VERSION = 2;
9
+ export const DEFERRED_FOREGROUND_TRANSFER_LEGACY_VERSION = 1;
10
+ const TRANSFER_STATE_FILE = "state.json";
11
+ const PRIVATE_FILE_MODE = 0o600;
12
+ const NO_FOLLOW_FLAG = typeof fs.constants.O_NOFOLLOW === "number"
13
+ ? fs.constants.O_NOFOLLOW
14
+ : 0;
15
+ export class DeferredForegroundTransferConflictError extends Error {
16
+ code = "AKK_DEFERRED_FOREGROUND_TRANSFER_CONFLICT";
17
+ transferId;
18
+ expectedRevision;
19
+ actualRevision;
20
+ constructor(transferId, expectedRevision, actualRevision, detail) {
21
+ super(`deferred foreground transfer ${transferId} changed concurrently` +
22
+ ` (expected revision ${String(expectedRevision)}, actual ` +
23
+ `${String(actualRevision)})` +
24
+ (detail ? `: ${detail}` : ""));
25
+ this.name = "DeferredForegroundTransferConflictError";
26
+ this.transferId = transferId;
27
+ this.expectedRevision = expectedRevision;
28
+ this.actualRevision = actualRevision;
29
+ }
30
+ }
31
+ export function createDeferredForegroundTransferId() {
32
+ return `deferred-transfer-${randomUUID()}`;
33
+ }
34
+ export function deferredForegroundTransfersDir(storeDir) {
35
+ return path.join(storeDir, STORE_DEFERRED_FOREGROUND_TRANSFERS_DIRECTORY);
36
+ }
37
+ export function pathsForDeferredForegroundTransfer(transferId, storeDir) {
38
+ assertRecordId(transferId, "deferred foreground transfer id");
39
+ const root = deferredForegroundTransfersDir(storeDir);
40
+ const directory = path.join(root, transferId);
41
+ assertContained(directory, root, "deferred foreground transfer directory");
42
+ return { directory, statePath: path.join(directory, TRANSFER_STATE_FILE) };
43
+ }
44
+ export function assertDeferredForegroundTransfer(value, expectedTransferId, options = {}) {
45
+ if (!isRecord(value)) {
46
+ throw new Error("deferred foreground transfer must be an object");
47
+ }
48
+ assertOnlyKeys(value, [
49
+ "schema",
50
+ "version",
51
+ "transfer_id",
52
+ "revision",
53
+ "status",
54
+ "input_stage",
55
+ "terminal_id",
56
+ "terminal_endpoint",
57
+ "process_pid",
58
+ "process_uuid",
59
+ "process_birth",
60
+ "workspace",
61
+ "source_session_id",
62
+ "source_expected_revision",
63
+ "source_binding_token",
64
+ "source_previous_last_transition_id",
65
+ "source_before_binding",
66
+ "source_kind",
67
+ "source_turn_history",
68
+ "target_session_id",
69
+ "target_expected_revision",
70
+ "previous_dispatch_status",
71
+ "previous_dispatch_fingerprint",
72
+ "target_prepared_revision",
73
+ "target_prepared_status",
74
+ "target_prepared_last_transition_id",
75
+ "target_prepared_binding_token",
76
+ "target_before_binding",
77
+ "request_hash",
78
+ "dispatcher_pid",
79
+ "prepared_at",
80
+ "source_reserved_at",
81
+ "target_prepared_at",
82
+ "dispatch_started_at",
83
+ "text_injected_at",
84
+ "enter_dispatched_at",
85
+ "agent_accepted_at",
86
+ "message_id",
87
+ "turn_id",
88
+ "state_path",
89
+ "target_native_thread_id",
90
+ "target_accepted_revision",
91
+ "target_accepted_status",
92
+ "target_accepted_binding_token",
93
+ "target_accepted_binding",
94
+ "source_pre_retirement_revision",
95
+ "source_pre_retirement_status",
96
+ "source_pre_retirement_binding_token",
97
+ "source_pre_retirement_binding",
98
+ "source_retirement",
99
+ "committed_at",
100
+ "target_after_revision",
101
+ "target_after_status",
102
+ "target_after_binding_token",
103
+ "source_after_revision",
104
+ "source_after_binding",
105
+ "source_after_binding_token",
106
+ "source_after_status",
107
+ "resolved_at",
108
+ "aborted_at",
109
+ "terminal_input_not_started_at",
110
+ "abort_cleanup_completed_at",
111
+ "abort_source_after_revision",
112
+ "abort_source_after_status",
113
+ "abort_source_after_binding_token",
114
+ "abort_source_after_binding",
115
+ "abort_target_after_status",
116
+ "abort_target_after_revision",
117
+ "abort_target_after_binding_token",
118
+ "abort_target_after_binding",
119
+ "uncertain_at",
120
+ "recovered_at",
121
+ "error",
122
+ "do_not_retry"
123
+ ], "deferred foreground transfer");
124
+ if (value.schema !== DEFERRED_FOREGROUND_TRANSFER_SCHEMA ||
125
+ ![
126
+ DEFERRED_FOREGROUND_TRANSFER_LEGACY_VERSION,
127
+ DEFERRED_FOREGROUND_TRANSFER_VERSION
128
+ ].includes(value.version)) {
129
+ throw new Error("deferred foreground transfer has an unsupported schema or version");
130
+ }
131
+ assertRecordId(value.transfer_id, "deferred foreground transfer id");
132
+ if (expectedTransferId !== undefined &&
133
+ value.transfer_id !== expectedTransferId) {
134
+ throw new Error(`deferred foreground transfer id ${String(value.transfer_id)} does not ` +
135
+ `match ${expectedTransferId}`);
136
+ }
137
+ if (value.revision === undefined
138
+ ? !options.allowMissingRevision
139
+ : !isPositiveSafeInteger(value.revision)) {
140
+ throw new Error("deferred foreground transfer revision must be a positive safe integer");
141
+ }
142
+ if (!TRANSFER_STATUSES.has(value.status)) {
143
+ throw new Error("deferred foreground transfer status is invalid");
144
+ }
145
+ if (!INPUT_STAGES.has(value.input_stage)) {
146
+ throw new Error("deferred foreground transfer input_stage is invalid");
147
+ }
148
+ assertNonEmptyString(value.terminal_id, "deferred transfer terminal_id");
149
+ if (!terminalEndpointIdentityFromEvidence(value.terminal_endpoint)) {
150
+ throw new Error("deferred transfer terminal_endpoint is invalid");
151
+ }
152
+ if (!isPositiveSafeInteger(value.process_pid)) {
153
+ throw new Error("deferred transfer process_pid must be positive");
154
+ }
155
+ assertNonEmptyString(value.process_uuid, "deferred transfer process_uuid");
156
+ assertNonEmptyString(value.process_birth, "deferred transfer process_birth");
157
+ assertAbsolutePath(value.workspace, "deferred transfer workspace");
158
+ assertManagedSessionId(value.source_session_id);
159
+ if (!isPositiveSafeInteger(value.source_expected_revision)) {
160
+ throw new Error("deferred transfer source_expected_revision must be positive");
161
+ }
162
+ assertSha256(value.source_binding_token, "deferred transfer source binding token");
163
+ assertOptionalNonEmptyString(value.source_previous_last_transition_id, "deferred transfer source previous transition");
164
+ assertBinding(value.source_before_binding, "deferred transfer source binding");
165
+ const sourceKind = value.version === DEFERRED_FOREGROUND_TRANSFER_LEGACY_VERSION
166
+ ? "status_card_only"
167
+ : value.source_kind;
168
+ if (value.version === DEFERRED_FOREGROUND_TRANSFER_LEGACY_VERSION
169
+ ? value.source_kind !== undefined || value.source_turn_history !== undefined
170
+ : !["status_card_only", "candidate_rollout_quiescent"].includes(String(sourceKind))) {
171
+ throw new Error("deferred transfer source kind is invalid");
172
+ }
173
+ const sourceTurnHistory = value.source_turn_history;
174
+ if (sourceKind === "candidate_rollout_quiescent") {
175
+ if (!Array.isArray(sourceTurnHistory) ||
176
+ sourceTurnHistory.length > 128) {
177
+ throw new Error("candidate-rollout deferred source requires exact Turn history authority");
178
+ }
179
+ const seenTurnIds = new Set();
180
+ for (const turn of sourceTurnHistory) {
181
+ if (!isRecord(turn) ||
182
+ Object.keys(turn).sort().join(",") !== [
183
+ "binding_generation",
184
+ "binding_id",
185
+ "native_thread_id",
186
+ "status",
187
+ "turn_fingerprint",
188
+ "turn_id",
189
+ "updated_at"
190
+ ].join(",") ||
191
+ !isSafeRecordId(turn.turn_id) ||
192
+ !["idle", "failed", "closed", "cancelled"].includes(String(turn.status)) ||
193
+ !isValidTimestamp(turn.updated_at) ||
194
+ !isSafeRecordId(turn.binding_id) ||
195
+ !isPositiveSafeInteger(turn.binding_generation) ||
196
+ !isExactNativeThreadId(turn.native_thread_id) ||
197
+ turn.binding_id !== value.source_before_binding.binding_id ||
198
+ turn.binding_generation !== value.source_before_binding.generation ||
199
+ turn.native_thread_id !== value.source_before_binding.native_thread_id ||
200
+ typeof turn.turn_fingerprint !== "string" ||
201
+ !/^[0-9a-f]{64}$/u.test(turn.turn_fingerprint) ||
202
+ seenTurnIds.has(String(turn.turn_id))) {
203
+ throw new Error("candidate-rollout deferred source Turn history is invalid");
204
+ }
205
+ seenTurnIds.add(String(turn.turn_id));
206
+ }
207
+ }
208
+ else if (sourceTurnHistory !== undefined) {
209
+ throw new Error("status-card deferred source cannot carry Turn history authority");
210
+ }
211
+ const statusCardSource = sourceKind === "status_card_only";
212
+ const candidateRolloutSource = sourceKind === "candidate_rollout_quiescent";
213
+ if (value.source_before_binding.native_process.pid !== value.process_pid ||
214
+ value.source_before_binding.native_process.process_uuid !==
215
+ value.process_uuid ||
216
+ value.source_before_binding.native_process.process_birth !==
217
+ value.process_birth ||
218
+ value.source_before_binding.terminal_id !== value.terminal_id ||
219
+ JSON.stringify(value.source_before_binding.terminal_endpoint) !==
220
+ JSON.stringify(value.terminal_endpoint) ||
221
+ !isExactNativeThreadId(value.source_before_binding.native_thread_id) ||
222
+ (statusCardSource &&
223
+ (value.source_before_binding.native_process.rollout !== undefined ||
224
+ !value.source_before_binding.native_process.evidence.includes("codex_status_card"))) ||
225
+ (candidateRolloutSource &&
226
+ value.source_before_binding.native_process.rollout === undefined) ||
227
+ (statusCardSource &&
228
+ value.source_previous_last_transition_id !== undefined) ||
229
+ managedSessionBindingToken({
230
+ session_id: value.source_session_id,
231
+ status: "bound",
232
+ binding: value.source_before_binding
233
+ }) !== value.source_binding_token) {
234
+ throw new Error("deferred transfer source binding disagrees with terminal/process authority");
235
+ }
236
+ assertManagedSessionId(value.target_session_id);
237
+ if (value.target_session_id === value.source_session_id) {
238
+ throw new Error("deferred transfer source and target Sessions must differ");
239
+ }
240
+ if (value.target_expected_revision !== null) {
241
+ throw new Error("deferred transfer target_expected_revision must be null");
242
+ }
243
+ if (!["none", "resolved"].includes(value.previous_dispatch_status)) {
244
+ throw new Error("deferred transfer previous_dispatch_status must be none or resolved");
245
+ }
246
+ assertSha256(value.previous_dispatch_fingerprint, "deferred transfer previous dispatch fingerprint");
247
+ if (value.target_prepared_revision !== undefined &&
248
+ !isPositiveSafeInteger(value.target_prepared_revision)) {
249
+ throw new Error("deferred transfer target_prepared_revision must be positive");
250
+ }
251
+ if (value.target_prepared_status !== undefined &&
252
+ value.target_prepared_status !== "transitioning") {
253
+ throw new Error("deferred transfer target_prepared_status must be transitioning");
254
+ }
255
+ assertOptionalNonEmptyString(value.target_prepared_last_transition_id, "deferred transfer target prepared last transition");
256
+ if (value.target_prepared_binding_token !== undefined) {
257
+ assertSha256(value.target_prepared_binding_token, "deferred transfer target prepared binding token");
258
+ }
259
+ if (value.target_before_binding !== undefined) {
260
+ assertBinding(value.target_before_binding, "deferred transfer target before binding");
261
+ if (value.target_before_binding.native_thread_id !== undefined ||
262
+ value.target_before_binding.native_process.rollout !== undefined ||
263
+ value.target_before_binding.native_process.pid !== value.process_pid ||
264
+ value.target_before_binding.native_process.process_uuid !==
265
+ value.process_uuid ||
266
+ value.target_before_binding.native_process.process_birth !==
267
+ value.process_birth ||
268
+ value.target_before_binding.terminal_id !== value.terminal_id ||
269
+ JSON.stringify(value.target_before_binding.terminal_endpoint) !==
270
+ JSON.stringify(value.terminal_endpoint) ||
271
+ managedSessionBindingToken({
272
+ session_id: value.target_session_id,
273
+ status: "transitioning",
274
+ binding: value.target_before_binding
275
+ }) !== value.target_prepared_binding_token) {
276
+ throw new Error("deferred transfer provisional target binding is not exact zero-UUID authority");
277
+ }
278
+ }
279
+ if (value.target_prepared_last_transition_id !== undefined &&
280
+ value.target_prepared_last_transition_id !== value.transfer_id) {
281
+ throw new Error("deferred transfer provisional target must be fenced by its transfer id");
282
+ }
283
+ assertSha256(value.request_hash, "deferred transfer request_hash");
284
+ if (!isPositiveSafeInteger(value.dispatcher_pid)) {
285
+ throw new Error("deferred transfer dispatcher_pid must be positive");
286
+ }
287
+ assertTimestamp(value.prepared_at, "deferred transfer prepared_at");
288
+ for (const field of [
289
+ "source_reserved_at",
290
+ "target_prepared_at",
291
+ "dispatch_started_at",
292
+ "text_injected_at",
293
+ "enter_dispatched_at",
294
+ "agent_accepted_at",
295
+ "committed_at",
296
+ "resolved_at",
297
+ "aborted_at",
298
+ "terminal_input_not_started_at",
299
+ "abort_cleanup_completed_at",
300
+ "uncertain_at",
301
+ "recovered_at"
302
+ ]) {
303
+ assertOptionalTimestamp(value[field], `deferred transfer ${field}`);
304
+ }
305
+ assertOptionalNonEmptyString(value.message_id, "deferred transfer message_id");
306
+ assertOptionalNonEmptyString(value.turn_id, "deferred transfer turn_id");
307
+ if (value.state_path !== undefined) {
308
+ assertAbsolutePath(value.state_path, "deferred transfer state_path");
309
+ }
310
+ assertOptionalNonEmptyString(value.error, "deferred transfer error");
311
+ if (value.do_not_retry !== undefined && typeof value.do_not_retry !== "boolean") {
312
+ throw new Error("deferred transfer do_not_retry must be boolean");
313
+ }
314
+ const requiresTargetPrepared = [
315
+ "target_prepared",
316
+ "dispatch_started",
317
+ "committed",
318
+ "resolved",
319
+ "uncertain"
320
+ ].includes(String(value.status));
321
+ const targetPreparedFields = [
322
+ value.target_prepared_at,
323
+ value.target_prepared_revision,
324
+ value.target_prepared_status,
325
+ value.target_prepared_last_transition_id,
326
+ value.target_prepared_binding_token,
327
+ value.target_before_binding,
328
+ value.message_id,
329
+ value.turn_id,
330
+ value.state_path
331
+ ];
332
+ const hasAnyTargetPreparedField = targetPreparedFields.some((field) => field !== undefined);
333
+ const hasAllTargetPreparedFields = targetPreparedFields.every((field) => field !== undefined);
334
+ if (hasAnyTargetPreparedField !== hasAllTargetPreparedFields ||
335
+ (requiresTargetPrepared && !hasAllTargetPreparedFields)) {
336
+ throw new Error(`${String(value.status)} deferred transfer requires its target/Turn identity`);
337
+ }
338
+ if (hasAllTargetPreparedFields) {
339
+ if (!value.source_reserved_at ||
340
+ value.target_prepared_revision !== 1 ||
341
+ value.target_prepared_status !== "transitioning" ||
342
+ value.target_prepared_last_transition_id !== value.transfer_id) {
343
+ throw new Error("deferred transfer provisional target requires exact create-only fence evidence");
344
+ }
345
+ }
346
+ if ([
347
+ "source_reserved",
348
+ "target_prepared",
349
+ "dispatch_started",
350
+ "committed",
351
+ "resolved",
352
+ "uncertain"
353
+ ]
354
+ .includes(String(value.status)) &&
355
+ !value.source_reserved_at) {
356
+ throw new Error(`${String(value.status)} deferred transfer requires source_reserved_at`);
357
+ }
358
+ if (value.status === "prepared" &&
359
+ (value.source_reserved_at !== undefined || hasAnyTargetPreparedField)) {
360
+ throw new Error("prepared deferred transfer cannot carry reservation evidence");
361
+ }
362
+ if (value.status === "source_reserved" && hasAnyTargetPreparedField) {
363
+ throw new Error("source_reserved deferred transfer cannot carry target evidence");
364
+ }
365
+ if (["dispatch_started", "committed", "resolved", "uncertain"].includes(String(value.status)) &&
366
+ (!value.dispatch_started_at || value.input_stage === "none")) {
367
+ throw new Error(`${String(value.status)} deferred transfer requires dispatch-start evidence`);
368
+ }
369
+ assertInputStageEvidence(value);
370
+ if ([
371
+ "prepared",
372
+ "source_reserved",
373
+ "target_prepared"
374
+ ].includes(String(value.status)) &&
375
+ value.input_stage !== "none") {
376
+ throw new Error(`${String(value.status)} deferred transfer cannot carry input evidence`);
377
+ }
378
+ const provedTerminalInputNotStarted = ["aborted", "abort_resolved"].includes(String(value.status)) &&
379
+ value.input_stage === "dispatch_started" &&
380
+ value.terminal_input_not_started_at !== undefined;
381
+ if (["aborted", "abort_resolved"].includes(String(value.status)) &&
382
+ value.input_stage !== "none" &&
383
+ !provedTerminalInputNotStarted) {
384
+ throw new Error(`${String(value.status)} deferred transfer requires exact no-input proof`);
385
+ }
386
+ assertTimestampOrder(value);
387
+ const committed = ["committed", "resolved"].includes(String(value.status));
388
+ const committedFields = [
389
+ value.target_native_thread_id,
390
+ value.target_accepted_revision,
391
+ value.target_accepted_status,
392
+ value.target_accepted_binding_token,
393
+ value.target_accepted_binding,
394
+ value.source_pre_retirement_revision,
395
+ value.source_pre_retirement_status,
396
+ value.source_pre_retirement_binding_token,
397
+ value.source_pre_retirement_binding,
398
+ value.source_retirement,
399
+ value.committed_at
400
+ ];
401
+ const hasAnyCommittedField = committedFields.some((field) => field !== undefined);
402
+ const hasAllCommittedFields = committedFields.every((field) => field !== undefined);
403
+ if (committed
404
+ ? !hasAllCommittedFields ||
405
+ value.input_stage !== "agent_accepted" ||
406
+ !value.agent_accepted_at
407
+ : hasAnyCommittedField) {
408
+ throw new Error("deferred transfer target identity is allowed only on a fully accepted commit");
409
+ }
410
+ if (committed) {
411
+ if (!isPositiveSafeInteger(value.target_accepted_revision) ||
412
+ value.target_accepted_revision !==
413
+ Number(value.target_prepared_revision) + 1 ||
414
+ value.target_accepted_status !== "transitioning" ||
415
+ !isPositiveSafeInteger(value.source_pre_retirement_revision) ||
416
+ value.source_pre_retirement_revision !==
417
+ value.source_expected_revision + (value.source_retirement === "binding_scrubbed_same_native_thread"
418
+ ? 2
419
+ : 1) ||
420
+ value.source_pre_retirement_status !== "transitioning" ||
421
+ !isExactNativeThreadId(value.target_native_thread_id)) {
422
+ throw new Error("deferred transfer commit lacks exact transitioning Session evidence");
423
+ }
424
+ assertSha256(value.target_accepted_binding_token, "deferred transfer target accepted binding token");
425
+ assertSha256(value.source_pre_retirement_binding_token, "deferred transfer source pre-retirement binding token");
426
+ assertBinding(value.target_accepted_binding, "deferred transfer target accepted binding");
427
+ assertBinding(value.source_pre_retirement_binding, "deferred transfer source pre-retirement binding");
428
+ if (value.target_accepted_binding.native_thread_id !==
429
+ value.target_native_thread_id ||
430
+ value.target_accepted_binding.terminal_id !== value.terminal_id ||
431
+ value.target_accepted_binding.native_process.pid !== value.process_pid ||
432
+ value.target_accepted_binding.native_process.process_uuid !==
433
+ value.process_uuid ||
434
+ value.target_accepted_binding.native_process.process_birth !==
435
+ value.process_birth ||
436
+ JSON.stringify(value.target_accepted_binding.terminal_endpoint) !==
437
+ JSON.stringify(value.terminal_endpoint) ||
438
+ !bindingIsMonotonicTargetRefinement(value.target_before_binding, value.target_accepted_binding) ||
439
+ managedSessionBindingToken({
440
+ session_id: value.target_session_id,
441
+ status: "transitioning",
442
+ binding: value.target_accepted_binding
443
+ }) !== value.target_accepted_binding_token ||
444
+ (value.target_native_thread_id ===
445
+ value.source_before_binding.native_thread_id
446
+ ? value.source_retirement !==
447
+ "binding_scrubbed_same_native_thread" ||
448
+ !bindingIsScrubbedSourceReplacement(value.source_before_binding, value.source_pre_retirement_binding)
449
+ : value.source_retirement !== "binding_retained"
450
+ || JSON.stringify(value.source_pre_retirement_binding) !==
451
+ JSON.stringify(value.source_before_binding)) ||
452
+ managedSessionBindingToken({
453
+ session_id: value.source_session_id,
454
+ status: "transitioning",
455
+ binding: value.source_pre_retirement_binding
456
+ }) !== value.source_pre_retirement_binding_token) {
457
+ throw new Error("deferred transfer binding disagrees with committed authority");
458
+ }
459
+ }
460
+ const resolvedFields = [
461
+ value.target_after_revision,
462
+ value.target_after_status,
463
+ value.target_after_binding_token,
464
+ value.source_after_revision,
465
+ value.source_after_binding,
466
+ value.source_after_binding_token,
467
+ value.source_after_status,
468
+ value.resolved_at
469
+ ];
470
+ const hasAnyResolvedField = resolvedFields.some((field) => field !== undefined);
471
+ const hasAllResolvedFields = resolvedFields.every((field) => field !== undefined);
472
+ if (value.status === "resolved"
473
+ ? !hasAllResolvedFields
474
+ : hasAnyResolvedField) {
475
+ throw new Error("resolved deferred transfer requires exact final Session evidence");
476
+ }
477
+ if (value.status === "resolved") {
478
+ if (!isPositiveSafeInteger(value.target_after_revision) ||
479
+ value.target_after_revision !==
480
+ Number(value.target_accepted_revision) + 1 ||
481
+ value.target_after_status !== "bound" ||
482
+ !isPositiveSafeInteger(value.source_after_revision) ||
483
+ value.source_after_revision !==
484
+ Number(value.source_pre_retirement_revision) + 1 ||
485
+ value.source_after_status !== "detached" ||
486
+ JSON.stringify(value.source_after_binding) !==
487
+ JSON.stringify(value.source_pre_retirement_binding)) {
488
+ throw new Error("resolved deferred transfer revision/status evidence is invalid");
489
+ }
490
+ assertBinding(value.source_after_binding, "deferred transfer retired source binding");
491
+ assertSha256(value.target_after_binding_token, "deferred transfer target final binding token");
492
+ assertSha256(value.source_after_binding_token, "deferred transfer retired source binding token");
493
+ if (managedSessionBindingToken({
494
+ session_id: value.target_session_id,
495
+ status: "bound",
496
+ binding: value.target_accepted_binding
497
+ }) !== value.target_after_binding_token ||
498
+ managedSessionBindingToken({
499
+ session_id: value.source_session_id,
500
+ status: "detached",
501
+ binding: value.source_after_binding
502
+ }) !== value.source_after_binding_token) {
503
+ throw new Error("resolved deferred transfer final binding tokens are inconsistent");
504
+ }
505
+ }
506
+ if (["aborted", "abort_resolved"].includes(String(value.status))
507
+ ? !value.aborted_at || !(value.input_stage === "none" || provedTerminalInputNotStarted)
508
+ : value.aborted_at !== undefined) {
509
+ throw new Error("only a proven zero-input deferred transfer can carry an aborted receipt");
510
+ }
511
+ if (value.terminal_input_not_started_at !== undefined
512
+ ? !provedTerminalInputNotStarted ||
513
+ value.text_injected_at !== undefined ||
514
+ value.enter_dispatched_at !== undefined ||
515
+ value.agent_accepted_at !== undefined
516
+ : value.input_stage === "dispatch_started" &&
517
+ ["aborted", "abort_resolved"].includes(String(value.status))) {
518
+ throw new Error("terminal-input-not-started proof must fence only a dispatch intent");
519
+ }
520
+ const abortCleanupSourceFields = [
521
+ value.abort_cleanup_completed_at,
522
+ value.abort_source_after_revision,
523
+ value.abort_source_after_status,
524
+ value.abort_source_after_binding_token,
525
+ value.abort_source_after_binding,
526
+ value.abort_target_after_status
527
+ ];
528
+ const abortCleanupTargetFields = [
529
+ value.abort_target_after_revision,
530
+ value.abort_target_after_binding_token,
531
+ value.abort_target_after_binding
532
+ ];
533
+ const hasAnyAbortCleanupField = [
534
+ ...abortCleanupSourceFields,
535
+ ...abortCleanupTargetFields
536
+ ].some((field) => field !== undefined);
537
+ if (value.status === "abort_resolved") {
538
+ if (abortCleanupSourceFields.some((field) => field === undefined)) {
539
+ throw new Error("resolved deferred abort requires exact source cleanup evidence");
540
+ }
541
+ const allowedSourceRevisions = value.source_reserved_at
542
+ ? [value.source_expected_revision + 2]
543
+ : [
544
+ value.source_expected_revision,
545
+ // The source reservation is the first Store mutation. A crash after
546
+ // that Session CAS but before source_reserved_at is published leaves
547
+ // the prepared transfer able to prove the exact +2 restore receipt.
548
+ value.source_expected_revision + 2
549
+ ];
550
+ if (!isPositiveSafeInteger(value.abort_source_after_revision) ||
551
+ !allowedSourceRevisions.includes(value.abort_source_after_revision) ||
552
+ value.abort_source_after_status !== "bound" ||
553
+ JSON.stringify(value.abort_source_after_binding) !==
554
+ JSON.stringify(value.source_before_binding)) {
555
+ throw new Error("resolved deferred abort source cleanup evidence is invalid");
556
+ }
557
+ assertSha256(value.abort_source_after_binding_token, "resolved deferred abort source binding token");
558
+ assertBinding(value.abort_source_after_binding, "resolved deferred abort source binding");
559
+ if (managedSessionBindingToken({
560
+ session_id: value.source_session_id,
561
+ status: "bound",
562
+ binding: value.abort_source_after_binding
563
+ }) !== value.abort_source_after_binding_token) {
564
+ throw new Error("resolved deferred abort source binding token is inconsistent");
565
+ }
566
+ if (value.abort_target_after_status === "absent") {
567
+ if (abortCleanupTargetFields.some((field) => field !== undefined)) {
568
+ throw new Error("absent deferred abort target cannot carry Session cleanup evidence");
569
+ }
570
+ }
571
+ else if (value.abort_target_after_status === "detached") {
572
+ if (abortCleanupTargetFields.some((field) => field === undefined) ||
573
+ !value.target_before_binding ||
574
+ !isPositiveSafeInteger(value.abort_target_after_revision) ||
575
+ value.abort_target_after_revision !==
576
+ Number(value.target_prepared_revision) + 1 ||
577
+ JSON.stringify(value.abort_target_after_binding) !==
578
+ JSON.stringify(value.target_before_binding)) {
579
+ throw new Error("resolved deferred abort target cleanup evidence is invalid");
580
+ }
581
+ assertSha256(value.abort_target_after_binding_token, "resolved deferred abort target binding token");
582
+ assertBinding(value.abort_target_after_binding, "resolved deferred abort target binding");
583
+ if (managedSessionBindingToken({
584
+ session_id: value.target_session_id,
585
+ status: "detached",
586
+ binding: value.abort_target_after_binding
587
+ }) !== value.abort_target_after_binding_token) {
588
+ throw new Error("resolved deferred abort target binding token is inconsistent");
589
+ }
590
+ }
591
+ }
592
+ else if (hasAnyAbortCleanupField) {
593
+ throw new Error("abort cleanup evidence is allowed only on an abort_resolved receipt");
594
+ }
595
+ if (value.status === "uncertain"
596
+ ? !value.uncertain_at || value.do_not_retry !== true ||
597
+ value.input_stage === "none"
598
+ : ["committed", "resolved"].includes(String(value.status)) &&
599
+ value.uncertain_at !== undefined
600
+ ? !value.recovered_at || value.do_not_retry !== true
601
+ : value.uncertain_at !== undefined ||
602
+ value.recovered_at !== undefined ||
603
+ value.do_not_retry !== undefined) {
604
+ throw new Error("uncertain deferred transfer requires possible input and do_not_retry=true");
605
+ }
606
+ if (value.error !== undefined &&
607
+ ![
608
+ "aborted",
609
+ "abort_resolved",
610
+ "uncertain",
611
+ ...(value.recovered_at ? ["committed", "resolved"] : [])
612
+ ].includes(String(value.status))) {
613
+ throw new Error("deferred transfer error is allowed only on failure/recovery receipts");
614
+ }
615
+ }
616
+ export function saveDeferredForegroundTransfer(storeDir, value, options) {
617
+ assertDeferredForegroundTransfer(value, undefined, {
618
+ allowMissingRevision: true
619
+ });
620
+ assertExpectedRevision(options?.expectedRevision);
621
+ const paths = pathsForDeferredForegroundTransfer(value.transfer_id, storeDir);
622
+ return withStoreWriterLease(storeDir, () => {
623
+ const current = tryLoadDeferredForegroundTransfer(storeDir, value.transfer_id);
624
+ const actualRevision = current?.revision ?? null;
625
+ if (actualRevision !== options.expectedRevision) {
626
+ throw new DeferredForegroundTransferConflictError(value.transfer_id, options.expectedRevision, actualRevision);
627
+ }
628
+ if (value.revision !== undefined &&
629
+ value.revision !== (options.expectedRevision ?? 1)) {
630
+ throw new DeferredForegroundTransferConflictError(value.transfer_id, options.expectedRevision, actualRevision, `candidate carries revision ${value.revision}`);
631
+ }
632
+ if (current) {
633
+ assertTransferAdvance(current, value);
634
+ }
635
+ else if (value.status !== "prepared" || value.input_stage !== "none") {
636
+ throw new Error("a deferred foreground transfer must be created as zero-input prepared");
637
+ }
638
+ const next = {
639
+ ...value,
640
+ revision: (actualRevision ?? 0) + 1
641
+ };
642
+ assertDeferredForegroundTransfer(next);
643
+ atomicSaveJson(paths.statePath, next);
644
+ return next;
645
+ });
646
+ }
647
+ export function loadDeferredForegroundTransfer(storeDir, transferId) {
648
+ assertStoreReadable(storeDir);
649
+ const paths = pathsForDeferredForegroundTransfer(transferId, storeDir);
650
+ const parsed = readJsonFile(paths.statePath, "deferred foreground transfer");
651
+ assertDeferredForegroundTransfer(parsed, transferId);
652
+ return parsed;
653
+ }
654
+ export function tryLoadDeferredForegroundTransfer(storeDir, transferId) {
655
+ try {
656
+ return loadDeferredForegroundTransfer(storeDir, transferId);
657
+ }
658
+ catch (error) {
659
+ if (isNodeError(error, "ENOENT")) {
660
+ return undefined;
661
+ }
662
+ throw error;
663
+ }
664
+ }
665
+ export function listDeferredForegroundTransfers(storeDir) {
666
+ if (!fs.existsSync(storeDir)) {
667
+ return [];
668
+ }
669
+ assertStoreReadable(storeDir);
670
+ const root = deferredForegroundTransfersDir(storeDir);
671
+ if (!fs.existsSync(root)) {
672
+ return [];
673
+ }
674
+ assertRealDirectory(root, "deferred foreground transfer root");
675
+ return fs.readdirSync(root, { withFileTypes: true })
676
+ .map((entry) => {
677
+ if (entry.isSymbolicLink() || !entry.isDirectory()) {
678
+ throw new Error("deferred foreground transfer root may contain only real record " +
679
+ `directories: ${entry.name}`);
680
+ }
681
+ return loadDeferredForegroundTransfer(storeDir, entry.name);
682
+ })
683
+ .sort((left, right) => left.transfer_id.localeCompare(right.transfer_id));
684
+ }
685
+ function assertTransferAdvance(current, candidate) {
686
+ const immutable = [
687
+ "schema",
688
+ "version",
689
+ "transfer_id",
690
+ "terminal_id",
691
+ "terminal_endpoint",
692
+ "process_pid",
693
+ "process_uuid",
694
+ "process_birth",
695
+ "workspace",
696
+ "source_session_id",
697
+ "source_expected_revision",
698
+ "source_binding_token",
699
+ "source_previous_last_transition_id",
700
+ "source_before_binding",
701
+ "source_kind",
702
+ "source_turn_history",
703
+ "target_session_id",
704
+ "target_expected_revision",
705
+ "previous_dispatch_status",
706
+ "previous_dispatch_fingerprint",
707
+ "request_hash",
708
+ "dispatcher_pid",
709
+ "prepared_at"
710
+ ];
711
+ const changed = immutable.find((field) => JSON.stringify(current[field]) !== JSON.stringify(candidate[field]));
712
+ if (changed) {
713
+ throw new Error(`deferred foreground transfer ${current.transfer_id} cannot change ` +
714
+ `immutable ${changed}`);
715
+ }
716
+ const allowed = {
717
+ prepared: ["source_reserved", "aborted"],
718
+ source_reserved: ["target_prepared", "aborted"],
719
+ target_prepared: ["dispatch_started", "uncertain", "aborted"],
720
+ dispatch_started: [
721
+ "dispatch_started",
722
+ "committed",
723
+ "uncertain",
724
+ "aborted"
725
+ ],
726
+ committed: ["resolved"],
727
+ resolved: [],
728
+ aborted: ["abort_resolved"],
729
+ abort_resolved: [],
730
+ uncertain: ["committed"]
731
+ };
732
+ if (!allowed[current.status].includes(candidate.status)) {
733
+ throw new Error(`deferred foreground transfer cannot move from ${current.status} to ` +
734
+ candidate.status);
735
+ }
736
+ if (INPUT_STAGE_RANK[candidate.input_stage] <
737
+ INPUT_STAGE_RANK[current.input_stage]) {
738
+ throw new Error("deferred foreground transfer input proof cannot regress");
739
+ }
740
+ for (const field of [
741
+ "target_prepared_revision",
742
+ "target_prepared_status",
743
+ "target_prepared_last_transition_id",
744
+ "target_prepared_binding_token",
745
+ "target_before_binding",
746
+ "message_id",
747
+ "turn_id",
748
+ "state_path"
749
+ ]) {
750
+ if (current[field] !== undefined &&
751
+ JSON.stringify(current[field]) !== JSON.stringify(candidate[field])) {
752
+ throw new Error(`deferred foreground transfer ${current.transfer_id} cannot change ${field}`);
753
+ }
754
+ }
755
+ for (const field of [
756
+ "source_reserved_at",
757
+ "target_prepared_at",
758
+ "dispatch_started_at",
759
+ "text_injected_at",
760
+ "enter_dispatched_at",
761
+ "agent_accepted_at",
762
+ "target_native_thread_id",
763
+ "target_accepted_revision",
764
+ "target_accepted_status",
765
+ "target_accepted_binding_token",
766
+ "target_accepted_binding",
767
+ "source_pre_retirement_revision",
768
+ "source_pre_retirement_status",
769
+ "source_pre_retirement_binding_token",
770
+ "source_pre_retirement_binding",
771
+ "source_retirement",
772
+ "committed_at",
773
+ "target_after_revision",
774
+ "target_after_status",
775
+ "target_after_binding_token",
776
+ "source_after_revision",
777
+ "source_after_binding",
778
+ "source_after_binding_token",
779
+ "source_after_status",
780
+ "resolved_at",
781
+ "aborted_at",
782
+ "terminal_input_not_started_at",
783
+ "abort_cleanup_completed_at",
784
+ "abort_source_after_revision",
785
+ "abort_source_after_status",
786
+ "abort_source_after_binding_token",
787
+ "abort_source_after_binding",
788
+ "abort_target_after_status",
789
+ "abort_target_after_revision",
790
+ "abort_target_after_binding_token",
791
+ "abort_target_after_binding",
792
+ "uncertain_at",
793
+ "recovered_at",
794
+ "error",
795
+ "do_not_retry"
796
+ ]) {
797
+ if (current[field] !== undefined &&
798
+ JSON.stringify(current[field]) !== JSON.stringify(candidate[field])) {
799
+ throw new Error(`deferred foreground transfer ${current.transfer_id} cannot change ${field}`);
800
+ }
801
+ }
802
+ if (candidate.status === "aborted" &&
803
+ current.input_stage !== "none" &&
804
+ !(current.input_stage === "dispatch_started" &&
805
+ candidate.input_stage === "dispatch_started" &&
806
+ candidate.terminal_input_not_started_at !== undefined)) {
807
+ throw new Error("deferred foreground transfer cannot abort after input may start");
808
+ }
809
+ }
810
+ function bindingIsMonotonicTargetRefinement(before, after) {
811
+ return before.binding_id === after.binding_id &&
812
+ before.generation === after.generation &&
813
+ before.terminal_id === after.terminal_id &&
814
+ JSON.stringify(before.terminal_control) ===
815
+ JSON.stringify(after.terminal_control) &&
816
+ JSON.stringify(before.terminal_endpoint) ===
817
+ JSON.stringify(after.terminal_endpoint) &&
818
+ before.native_process.pid === after.native_process.pid &&
819
+ before.native_process.process_uuid === after.native_process.process_uuid &&
820
+ before.native_process.process_birth === after.native_process.process_birth &&
821
+ before.bound_at === after.bound_at &&
822
+ before.native_thread_id === undefined &&
823
+ before.native_process.rollout === undefined &&
824
+ isExactNativeThreadId(after.native_thread_id) &&
825
+ after.native_process.rollout !== undefined;
826
+ }
827
+ function bindingIsScrubbedSourceReplacement(before, after) {
828
+ return before.binding_id !== after.binding_id &&
829
+ after.generation === before.generation + 1 &&
830
+ before.terminal_id === after.terminal_id &&
831
+ JSON.stringify(before.terminal_control) ===
832
+ JSON.stringify(after.terminal_control) &&
833
+ JSON.stringify(before.terminal_endpoint) ===
834
+ JSON.stringify(after.terminal_endpoint) &&
835
+ before.native_process.pid === after.native_process.pid &&
836
+ before.native_process.process_uuid === after.native_process.process_uuid &&
837
+ before.native_process.process_birth === after.native_process.process_birth &&
838
+ after.native_thread_id === undefined &&
839
+ after.native_process.rollout === undefined;
840
+ }
841
+ function assertInputStageEvidence(value) {
842
+ const stage = INPUT_STAGE_RANK[value.input_stage];
843
+ for (const [field, minimum] of [
844
+ ["dispatch_started_at", 1],
845
+ ["text_injected_at", 2],
846
+ ["enter_dispatched_at", 3],
847
+ ["agent_accepted_at", 4]
848
+ ]) {
849
+ if ((value[field] !== undefined) !== (stage >= minimum)) {
850
+ throw new Error(`deferred transfer ${value.input_stage} has inconsistent ${field}`);
851
+ }
852
+ }
853
+ }
854
+ function assertTimestampOrder(value) {
855
+ const inputTimeline = [
856
+ value.prepared_at,
857
+ value.source_reserved_at,
858
+ value.target_prepared_at,
859
+ value.dispatch_started_at,
860
+ value.text_injected_at,
861
+ value.enter_dispatched_at,
862
+ value.agent_accepted_at
863
+ ].filter((candidate) => candidate !== undefined);
864
+ assertMonotonicTimestamps(inputTimeline);
865
+ const latestInputEvidence = inputTimeline[inputTimeline.length - 1];
866
+ if (value.aborted_at) {
867
+ assertTimestampNotBefore(value.aborted_at, latestInputEvidence);
868
+ }
869
+ if (value.terminal_input_not_started_at) {
870
+ assertTimestampNotBefore(value.terminal_input_not_started_at, value.dispatch_started_at);
871
+ assertTimestampNotBefore(value.aborted_at, value.terminal_input_not_started_at);
872
+ }
873
+ if (value.abort_cleanup_completed_at) {
874
+ assertTimestampNotBefore(value.abort_cleanup_completed_at, value.aborted_at);
875
+ }
876
+ if (value.uncertain_at) {
877
+ // Recovery may prove acceptance after uncertainty was recorded, so the
878
+ // uncertainty timestamp is ordered after the evidence known at its own
879
+ // stage, not unconditionally after a later agent_accepted_at.
880
+ const latestPossibleDispatch = value.enter_dispatched_at ??
881
+ value.text_injected_at ?? value.dispatch_started_at ??
882
+ value.target_prepared_at ?? value.source_reserved_at ?? value.prepared_at;
883
+ assertTimestampNotBefore(value.uncertain_at, latestPossibleDispatch);
884
+ if (value.status === "uncertain" && value.input_stage === "agent_accepted") {
885
+ assertTimestampNotBefore(value.uncertain_at, value.agent_accepted_at);
886
+ }
887
+ }
888
+ if (value.committed_at) {
889
+ assertTimestampNotBefore(value.committed_at, value.agent_accepted_at);
890
+ assertTimestampNotBefore(value.committed_at, value.uncertain_at);
891
+ }
892
+ if (value.recovered_at) {
893
+ assertTimestampNotBefore(value.recovered_at, value.uncertain_at);
894
+ assertTimestampNotBefore(value.recovered_at, value.committed_at);
895
+ }
896
+ if (value.resolved_at) {
897
+ assertTimestampNotBefore(value.resolved_at, value.committed_at);
898
+ assertTimestampNotBefore(value.resolved_at, value.recovered_at);
899
+ }
900
+ }
901
+ function assertMonotonicTimestamps(values) {
902
+ for (let index = 1; index < values.length; index += 1) {
903
+ assertTimestampNotBefore(values[index], values[index - 1]);
904
+ }
905
+ }
906
+ function assertTimestampNotBefore(candidate, boundary) {
907
+ if (candidate !== undefined &&
908
+ boundary !== undefined &&
909
+ Date.parse(candidate) < Date.parse(boundary)) {
910
+ throw new Error("deferred foreground transfer timestamps must be monotonic");
911
+ }
912
+ }
913
+ function assertBinding(value, label) {
914
+ const now = new Date().toISOString();
915
+ assertManagedSessionState({
916
+ schema: "agent-knock-knock/session",
917
+ version: 1,
918
+ session_id: "session-deferred-binding-validation",
919
+ revision: 1,
920
+ agent: "codex",
921
+ workspace: "/deferred-binding-validation",
922
+ status: "bound",
923
+ binding: value,
924
+ lineage: { created_by: "attach" },
925
+ created_at: now,
926
+ updated_at: now
927
+ });
928
+ }
929
+ function atomicSaveJson(filePath, value) {
930
+ const root = path.dirname(path.dirname(filePath));
931
+ const directory = path.dirname(filePath);
932
+ ensureDir(root);
933
+ assertRealDirectory(root, "deferred foreground transfer root");
934
+ ensureDir(directory);
935
+ assertRealDirectory(directory, "deferred foreground transfer directory");
936
+ assertRegularOrAbsent(filePath, "deferred foreground transfer state");
937
+ const tempPath = path.join(directory, `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
938
+ let fd;
939
+ try {
940
+ fd = fs.openSync(tempPath, fs.constants.O_CREAT |
941
+ fs.constants.O_EXCL |
942
+ fs.constants.O_WRONLY |
943
+ NO_FOLLOW_FLAG, PRIVATE_FILE_MODE);
944
+ fs.fchmodSync(fd, PRIVATE_FILE_MODE);
945
+ fs.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`, "utf8");
946
+ fs.fsyncSync(fd);
947
+ fs.closeSync(fd);
948
+ fd = undefined;
949
+ assertRegularOrAbsent(filePath, "deferred foreground transfer state");
950
+ fs.renameSync(tempPath, filePath);
951
+ fs.chmodSync(filePath, PRIVATE_FILE_MODE);
952
+ fsyncDirectory(directory);
953
+ }
954
+ finally {
955
+ if (fd !== undefined) {
956
+ fs.closeSync(fd);
957
+ }
958
+ try {
959
+ fs.unlinkSync(tempPath);
960
+ }
961
+ catch (error) {
962
+ if (!isNodeError(error, "ENOENT")) {
963
+ throw error;
964
+ }
965
+ }
966
+ }
967
+ }
968
+ function readJsonFile(filePath, label) {
969
+ assertRealDirectory(path.dirname(path.dirname(filePath)), `${label} root`);
970
+ assertRealDirectory(path.dirname(filePath), `${label} directory`);
971
+ const stat = fs.lstatSync(filePath);
972
+ if (stat.isSymbolicLink() || !stat.isFile()) {
973
+ throw new Error(`${label} must be a regular file: ${filePath}`);
974
+ }
975
+ const fd = fs.openSync(filePath, fs.constants.O_RDONLY | NO_FOLLOW_FLAG);
976
+ try {
977
+ if (!fs.fstatSync(fd).isFile()) {
978
+ throw new Error(`${label} must be a regular file: ${filePath}`);
979
+ }
980
+ return JSON.parse(fs.readFileSync(fd, "utf8"));
981
+ }
982
+ finally {
983
+ fs.closeSync(fd);
984
+ }
985
+ }
986
+ function assertExpectedRevision(value) {
987
+ if (value !== null && !isPositiveSafeInteger(value)) {
988
+ throw new Error("expectedRevision must be null or a positive safe integer");
989
+ }
990
+ }
991
+ function assertRecordId(value, label) {
992
+ if (typeof value !== "string" ||
993
+ !value ||
994
+ value === "." ||
995
+ value === ".." ||
996
+ value.includes("\0") ||
997
+ path.posix.basename(value) !== value ||
998
+ path.win32.basename(value) !== value ||
999
+ path.isAbsolute(value)) {
1000
+ throw new Error(`${label} is not safe for storage: ${String(value)}`);
1001
+ }
1002
+ }
1003
+ function isSafeRecordId(value) {
1004
+ try {
1005
+ assertRecordId(value, "record id");
1006
+ return true;
1007
+ }
1008
+ catch {
1009
+ return false;
1010
+ }
1011
+ }
1012
+ function isValidTimestamp(value) {
1013
+ return typeof value === "string" && Number.isFinite(Date.parse(value));
1014
+ }
1015
+ function assertContained(candidate, root, label) {
1016
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
1017
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
1018
+ throw new Error(`${label} escapes its Store root`);
1019
+ }
1020
+ }
1021
+ function assertRealDirectory(directory, label) {
1022
+ const stat = fs.lstatSync(directory);
1023
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
1024
+ throw new Error(`${label} must be a real directory: ${directory}`);
1025
+ }
1026
+ }
1027
+ function assertRegularOrAbsent(filePath, label) {
1028
+ try {
1029
+ const stat = fs.lstatSync(filePath);
1030
+ if (stat.isSymbolicLink() || !stat.isFile()) {
1031
+ throw new Error(`${label} must be a regular file: ${filePath}`);
1032
+ }
1033
+ }
1034
+ catch (error) {
1035
+ if (!isNodeError(error, "ENOENT")) {
1036
+ throw error;
1037
+ }
1038
+ }
1039
+ }
1040
+ function fsyncDirectory(directory) {
1041
+ let fd;
1042
+ try {
1043
+ fd = fs.openSync(directory, fs.constants.O_RDONLY | NO_FOLLOW_FLAG);
1044
+ fs.fsyncSync(fd);
1045
+ }
1046
+ catch (error) {
1047
+ const code = error instanceof Error
1048
+ ? error.code
1049
+ : undefined;
1050
+ if (!["EINVAL", "ENOTSUP", "EPERM", "EISDIR"].includes(String(code))) {
1051
+ throw error;
1052
+ }
1053
+ }
1054
+ finally {
1055
+ if (fd !== undefined) {
1056
+ fs.closeSync(fd);
1057
+ }
1058
+ }
1059
+ }
1060
+ function assertOnlyKeys(value, allowed, label) {
1061
+ const known = new Set(allowed);
1062
+ const unsupported = Object.keys(value).find((key) => !known.has(key));
1063
+ if (unsupported !== undefined) {
1064
+ throw new Error(`${label} contains unsupported field ${unsupported}`);
1065
+ }
1066
+ }
1067
+ function assertNonEmptyString(value, label) {
1068
+ if (typeof value !== "string" || value.trim() === "" || value.includes("\0")) {
1069
+ throw new Error(`${label} must be a non-empty string`);
1070
+ }
1071
+ }
1072
+ function assertOptionalNonEmptyString(value, label) {
1073
+ if (value !== undefined) {
1074
+ assertNonEmptyString(value, label);
1075
+ }
1076
+ }
1077
+ function assertAbsolutePath(value, label) {
1078
+ assertNonEmptyString(value, label);
1079
+ if (!path.isAbsolute(value)) {
1080
+ throw new Error(`${label} must be absolute`);
1081
+ }
1082
+ }
1083
+ function assertSha256(value, label) {
1084
+ if (typeof value !== "string" || !/^[0-9a-f]{64}$/u.test(value)) {
1085
+ throw new Error(`${label} must be SHA-256 hex`);
1086
+ }
1087
+ }
1088
+ function assertTimestamp(value, label) {
1089
+ if (typeof value !== "string" ||
1090
+ value.length === 0 ||
1091
+ !Number.isFinite(Date.parse(value))) {
1092
+ throw new Error(`${label} must be a valid timestamp`);
1093
+ }
1094
+ }
1095
+ function assertOptionalTimestamp(value, label) {
1096
+ if (value !== undefined) {
1097
+ assertTimestamp(value, label);
1098
+ }
1099
+ }
1100
+ function isPositiveSafeInteger(value) {
1101
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
1102
+ }
1103
+ function isRecord(value) {
1104
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1105
+ }
1106
+ function isNodeError(error, code) {
1107
+ return error instanceof Error &&
1108
+ error.code === code;
1109
+ }
1110
+ const TRANSFER_STATUSES = new Set([
1111
+ "prepared",
1112
+ "source_reserved",
1113
+ "target_prepared",
1114
+ "dispatch_started",
1115
+ "committed",
1116
+ "resolved",
1117
+ "aborted",
1118
+ "abort_resolved",
1119
+ "uncertain"
1120
+ ]);
1121
+ const INPUT_STAGES = new Set([
1122
+ "none",
1123
+ "dispatch_started",
1124
+ "text_injected",
1125
+ "enter_dispatched",
1126
+ "agent_accepted"
1127
+ ]);
1128
+ const INPUT_STAGE_RANK = {
1129
+ none: 0,
1130
+ dispatch_started: 1,
1131
+ text_injected: 2,
1132
+ enter_dispatched: 3,
1133
+ agent_accepted: 4
1134
+ };
1135
+ //# sourceMappingURL=deferred-foreground-transfer.js.map