@scotthuang/agent-knock-knock 0.12.37 → 0.13.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 (143) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +3 -0
  3. package/dist/src/agent-session-provider.d.ts +5 -0
  4. package/dist/src/agent-session-provider.js +10 -1
  5. package/dist/src/agent-session-provider.js.map +1 -1
  6. package/dist/src/callback-outbox-service.d.ts +2 -0
  7. package/dist/src/callback-outbox-service.js +1 -1
  8. package/dist/src/callback-outbox-service.js.map +1 -1
  9. package/dist/src/callback-transport.d.ts +7 -0
  10. package/dist/src/callback-transport.js +1 -1
  11. package/dist/src/callback-transport.js.map +1 -1
  12. package/dist/src/cli-core.js +35 -32
  13. package/dist/src/cli-core.js.map +1 -1
  14. package/dist/src/codex-store-adapter.d.ts +1 -1
  15. package/dist/src/codex-store-adapter.js +35 -8
  16. package/dist/src/codex-store-adapter.js.map +1 -1
  17. package/dist/src/deferred-foreground-authority-cli-adapter.d.ts +20 -0
  18. package/dist/src/deferred-foreground-authority-cli-adapter.js +216 -8
  19. package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
  20. package/dist/src/host-adapter.d.ts +1 -1
  21. package/dist/src/host-adapter.js +1 -1
  22. package/dist/src/host-bridge-tools.js +2 -2
  23. package/dist/src/managed-turn-recovery-service.d.ts +8 -1
  24. package/dist/src/managed-turn-recovery-service.js +55 -13
  25. package/dist/src/managed-turn-recovery-service.js.map +1 -1
  26. package/dist/src/mutation-transaction.d.ts +1 -0
  27. package/dist/src/mutation-transaction.js +19 -15
  28. package/dist/src/mutation-transaction.js.map +1 -1
  29. package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +39 -3
  30. package/dist/src/native-thread-lifecycle-cli-adapter.js +307 -7
  31. package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
  32. package/dist/src/openclaw-plugin-callback-adapter.js +17 -7
  33. package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -1
  34. package/dist/src/openclaw-plugin-command-adapter.js +297 -83
  35. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  36. package/dist/src/openclaw-plugin-schemas.d.ts +57 -0
  37. package/dist/src/openclaw-plugin-schemas.js +59 -1
  38. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  39. package/dist/src/openclaw-private-authority-offers.d.ts +17 -0
  40. package/dist/src/openclaw-private-authority-offers.js +75 -3
  41. package/dist/src/openclaw-private-authority-offers.js.map +1 -1
  42. package/dist/src/store.d.ts +1 -1
  43. package/dist/src/store.js +6 -4
  44. package/dist/src/store.js.map +1 -1
  45. package/dist/src/terminal-acceptance-application-service.js +2 -2
  46. package/dist/src/terminal-acceptance-application-service.js.map +1 -1
  47. package/dist/src/terminal-acceptance-cli-adapter.d.ts +11 -1
  48. package/dist/src/terminal-acceptance-cli-adapter.js +290 -16
  49. package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -1
  50. package/dist/src/terminal-action-projection.d.ts +1 -1
  51. package/dist/src/terminal-action-projection.js +6 -0
  52. package/dist/src/terminal-action-projection.js.map +1 -1
  53. package/dist/src/terminal-agent-adapter.d.ts +9 -0
  54. package/dist/src/terminal-agent-adapter.js.map +1 -1
  55. package/dist/src/terminal-agent-bridge.d.ts +52 -8
  56. package/dist/src/terminal-agent-bridge.js +111 -94
  57. package/dist/src/terminal-agent-bridge.js.map +1 -1
  58. package/dist/src/terminal-authority-policy.js +3 -0
  59. package/dist/src/terminal-authority-policy.js.map +1 -1
  60. package/dist/src/terminal-binding-authority.d.ts +2 -0
  61. package/dist/src/terminal-binding-authority.js +8 -0
  62. package/dist/src/terminal-binding-authority.js.map +1 -1
  63. package/dist/src/terminal-command-cli-adapter.d.ts +17 -1
  64. package/dist/src/terminal-command-cli-adapter.js +784 -286
  65. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  66. package/dist/src/terminal-delegate-cli-adapter.js +21 -7
  67. package/dist/src/terminal-delegate-cli-adapter.js.map +1 -1
  68. package/dist/src/terminal-dispatch-application.d.ts +3 -1
  69. package/dist/src/terminal-dispatch-application.js +13 -5
  70. package/dist/src/terminal-dispatch-application.js.map +1 -1
  71. package/dist/src/terminal-dispatch-composition.d.ts +43 -0
  72. package/dist/src/terminal-dispatch-execution.d.ts +2 -2
  73. package/dist/src/terminal-dispatch-execution.js +53 -3
  74. package/dist/src/terminal-dispatch-execution.js.map +1 -1
  75. package/dist/src/terminal-dispatch-policy.d.ts +20 -0
  76. package/dist/src/terminal-dispatch-policy.js +33 -1
  77. package/dist/src/terminal-dispatch-policy.js.map +1 -1
  78. package/dist/src/terminal-dispatch-presenter.d.ts +45 -0
  79. package/dist/src/terminal-dispatch-presenter.js +220 -18
  80. package/dist/src/terminal-dispatch-presenter.js.map +1 -1
  81. package/dist/src/terminal-dispatch-receipt.d.ts +5 -1
  82. package/dist/src/terminal-dispatch-receipt.js +26 -3
  83. package/dist/src/terminal-dispatch-receipt.js.map +1 -1
  84. package/dist/src/terminal-identity-authority-cli-adapter.js +103 -4
  85. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  86. package/dist/src/terminal-interaction-authority.d.ts +48 -1
  87. package/dist/src/terminal-interaction-authority.js +30 -0
  88. package/dist/src/terminal-interaction-authority.js.map +1 -1
  89. package/dist/src/terminal-interaction-cli-adapter.js +9 -0
  90. package/dist/src/terminal-interaction-cli-adapter.js.map +1 -1
  91. package/dist/src/terminal-interaction-core.d.ts +104 -0
  92. package/dist/src/terminal-interaction-core.js +407 -0
  93. package/dist/src/terminal-interaction-core.js.map +1 -0
  94. package/dist/src/terminal-interaction-protocol.d.ts +89 -0
  95. package/dist/src/terminal-interaction-protocol.js +199 -0
  96. package/dist/src/terminal-interaction-protocol.js.map +1 -1
  97. package/dist/src/terminal-list-cli-adapter.d.ts +5 -0
  98. package/dist/src/terminal-list-cli-adapter.js +190 -40
  99. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  100. package/dist/src/terminal-list-renderer.js +60 -7
  101. package/dist/src/terminal-list-renderer.js.map +1 -1
  102. package/dist/src/terminal-maintenance-cli-adapter.js +14 -3
  103. package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -1
  104. package/dist/src/terminal-monitor-application-service.d.ts +1 -0
  105. package/dist/src/terminal-monitor-application-service.js +72 -10
  106. package/dist/src/terminal-monitor-application-service.js.map +1 -1
  107. package/dist/src/terminal-monitor-cli-adapter.d.ts +3 -0
  108. package/dist/src/terminal-monitor-cli-adapter.js +24 -8
  109. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  110. package/dist/src/terminal-monitor-state-cli-adapter.js +15 -2
  111. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  112. package/dist/src/terminal-mutation-cli-runtime.d.ts +11 -1
  113. package/dist/src/terminal-mutation-cli-runtime.js +9 -4
  114. package/dist/src/terminal-mutation-cli-runtime.js.map +1 -1
  115. package/dist/src/terminal-status-cli-adapter.d.ts +1 -7
  116. package/dist/src/terminal-status-cli-adapter.js +29 -3
  117. package/dist/src/terminal-status-cli-adapter.js.map +1 -1
  118. package/dist/src/terminal-status-facts.d.ts +17 -1
  119. package/dist/src/terminal-status-facts.js +59 -0
  120. package/dist/src/terminal-status-facts.js.map +1 -1
  121. package/dist/src/terminal-submission-acceptance.d.ts +37 -2
  122. package/dist/src/terminal-submission-acceptance.js +633 -30
  123. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  124. package/dist/src/terminal-submission-facts.d.ts +5 -0
  125. package/dist/src/terminal-submission-facts.js.map +1 -1
  126. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +2 -1
  127. package/dist/src/terminal-watch-callback-cli-adapter.js +2 -1
  128. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -1
  129. package/dist/src/terminal-watch-cli-adapter.d.ts +6 -2
  130. package/dist/src/terminal-watch-cli-adapter.js +1051 -15
  131. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  132. package/dist/src/terminal-watch-service.d.ts +15 -1
  133. package/dist/src/terminal-watch-service.js +169 -13
  134. package/dist/src/terminal-watch-service.js.map +1 -1
  135. package/dist/src/terminal-watch-store.d.ts +72 -3
  136. package/dist/src/terminal-watch-store.js +407 -27
  137. package/dist/src/terminal-watch-store.js.map +1 -1
  138. package/docs/host-bridge-profiles.md +2 -2
  139. package/docs/quickstart-herdr.md +5 -3
  140. package/docs/quickstart-tmux.md +7 -5
  141. package/openclaw.plugin.json +8 -0
  142. package/package.json +1 -1
  143. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +15 -11
@@ -1,25 +1,30 @@
1
1
  // Raw CLI infrastructure for ordinary terminal send/respond/approval commands.
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
+ import fs from "node:fs";
4
5
  import os from "node:os";
5
6
  import path from "node:path";
6
7
  import { callbackExpectedForConversation, callbackExpectedForConversationWithLegacyFallback, callbackRouteFingerprintForConversation } from "./callback-route-authority.js";
7
- import { supersedeUnacceptedCallbackDeliveries } from "./callback-outbox-policy.js";
8
+ import { callbackDeliveryAttemptOutcome, supersedeUnacceptedCallbackDeliveries } from "./callback-outbox-policy.js";
8
9
  import { captureClaudeTranscriptAnchor, defaultClaudeHome } from "./claude-local-transcript-provider.js";
9
- import { validateCodexRolloutAcceptanceAnchor } from "./terminal-submission-acceptance.js";
10
+ import { captureCodexCandidateSetRolloutAcceptanceAnchor, validateCodexRolloutAcceptanceAnchor } from "./terminal-submission-acceptance.js";
11
+ import { fingerprint } from "./terminal-submission-facts.js";
10
12
  import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
11
- import { deferredForegroundActiveMessageId, isDeferredForegroundSubmissionRetryPending } from "./deferred-foreground-transfer.js";
13
+ import { deferredForegroundActiveMessageId, isDeferredForegroundSubmissionRetryPending, listDeferredForegroundTransfers } from "./deferred-foreground-transfer.js";
14
+ import { isFinalDeferredForegroundTransferStatus } from "./deferred-foreground-transfer-policy.js";
15
+ import { humanExplicitCallbackDebtDisposition, humanExplicitCallbackDebtRetirementDisposition, humanExplicitCallbackDebtManagedTokenMatches } from "./deferred-foreground-authority-cli-adapter.js";
12
16
  import { applyMessageToConversation, budgetAction, createMessage, effectiveTurnStatus, executorForConversation, isSessionSendBlockingStatus, isTerminalDispatchOwnerReleasedStatus, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
13
17
  import { readNdjsonLog } from "./transcript.js";
14
18
  import { appendEvent, appendExplicitUserCloseEvent, defaultStoreDir, ensureDir, ensureStoreWritable, listConversations, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveExplicitUserCloseState, saveState, withStoreWriterLeaseAsync } from "./store.js";
15
19
  import { createManagedSessionId, managedSessionBindingToken, managedSessionRevision, unmanagedTerminalBindingToken } from "./managed-session.js";
16
- import { listManagedSessions, loadManagedSession, saveManagedSession, tryLoadManagedSession } from "./session-store.js";
20
+ import { listNativeThreadTransitions, listManagedSessions, loadManagedSession, saveManagedSession, tryLoadManagedSession } from "./session-store.js";
17
21
  import { isTerminalApprovalDecision } from "./terminal-agent-adapter.js";
18
22
  import { terminalControlEvidenceMatches } from "./terminal-control-ref.js";
19
23
  import { isExactClaudeNativeInspectionIdleComposer, TerminalEnterDispatchNotAttemptedError, TerminalInputNotStartedError } from "./terminal-agent-bridge.js";
20
24
  import { decideTerminalSubmissionRetry, loadTerminalSubmissionRetry, projectTerminalSubmissionRetryPending, saveTerminalSubmissionRetry, terminalSubmissionRetryLedgerFields, TERMINAL_SUBMISSION_RETRY_SCHEMA, TERMINAL_SUBMISSION_RETRY_VERSION } from "./terminal-submission-retry-service.js";
21
25
  import { evaluateApprovalPolicy } from "./approval-policy.js";
22
- import { codexCompanionsPresentInOpenRootInventory, isCompleteNativeRollout, nativeIdentityMatchesCodexPreMaterialization, terminalControlAliasMatches, terminalControlsShareIncarnation } from "./terminal-authority-policy.js";
26
+ import { rolloutFileIdentityMatches } from "./terminal-binding-authority.js";
27
+ import { codexCompanionsPresentInOpenRootInventory, exactBoundCodexSendSource, isCompleteNativeRollout, nativeIdentityMatchesCodexPreMaterialization, terminalControlAliasMatches, terminalControlsShareIncarnation } from "./terminal-authority-policy.js";
23
28
  import { decideTerminalSendAuthority } from "./terminal-action-projection.js";
24
29
  import { terminalScopedCodexApprovalPromptSnapshot } from "./terminal-scoped-approval-authority.js";
25
30
  import { withCanonicalMutationLocks } from "./mutation-transaction.js";
@@ -28,6 +33,7 @@ import { claudeTranscriptApprovalIdentity, terminalMonitorDeadlineAt as deadline
28
33
  import * as monitorOwner from "./terminal-monitor-ownership-policy.js";
29
34
  import { sameCanonicalStatePath, terminalDispatchLedgerLooksLifecycle, terminalDispatchReceiptHistory as terminalLedgerReceiptHistory } from "./terminal-dispatch-ledger-codec.js";
30
35
  import * as dispatchApplication from "./terminal-dispatch-application.js";
36
+ import { decideUserExplicitTerminalInputSafety } from "./terminal-dispatch-policy.js";
31
37
  import { bindDeferredForegroundApplicationScope, bindDeferredForegroundWriterScope } from "./deferred-foreground-capability.js";
32
38
  import * as deferredRecoveryAdapter from "./deferred-foreground-recovery-cli-adapter.js";
33
39
  import { cleanupDeferredForegroundUserClose } from "./deferred-foreground-user-close.js";
@@ -35,11 +41,16 @@ import { deferredForegroundBoundaryProjection } from "./deferred-foreground-prep
35
41
  import { terminalSubmissionPayload } from "./terminal-dispatch-execution.js";
36
42
  import { createTerminalUserSendIntentRepository, TerminalUserSendIntentBoundaryConflictError, TerminalUserSendIntentUncertainError } from "./terminal-user-send-intent.js";
37
43
  import { terminalUserExplicitFallbackWatchId } from "./terminal-watch-store.js";
38
- import { presentTerminalCompleted, presentTerminalDispatchReplay, presentTerminalIdentityFailure, presentTerminalUncertain, presentTerminalZeroInputAbort as renderTerminalZeroInputAbort } from "./terminal-dispatch-presenter.js";
44
+ import { CODEX_FOREGROUND_IDENTIFICATION_SCROLLBACK_LINES } from "./native-thread-lifecycle-cli-adapter.js";
45
+ import { presentTerminalCompleted, presentTerminalDispatchReplay, presentTerminalIdentityFailure, terminalSendEnterDispatched, terminalSendResultContract, presentTerminalUncertain, presentTerminalZeroInputAbort as renderTerminalZeroInputAbort } from "./terminal-dispatch-presenter.js";
39
46
  import * as dispatchReceipt from "./terminal-dispatch-receipt.js";
40
47
  import { expandHome, positiveMilliseconds, writeCliJson as printJson } from "./cli-command-runtime.js";
41
48
  import { cliEnv, cliExit, cliNow, cliNowMs, cliPid, cliRuntimeLog as runtimeLog } from "./cli-runtime-context.js";
42
49
  const terminalCommandContext = new AsyncLocalStorage();
50
+ // An atomic identify-and-send proof is deliberately an in-process,
51
+ // unforgeable side channel. It must never be accepted from CLI arguments or
52
+ // persisted as Session authority.
53
+ const foregroundIdentificationProofs = new WeakMap();
43
54
  function terminalCommandRuntime() {
44
55
  const runtime = terminalCommandContext.getStore();
45
56
  if (!runtime) {
@@ -81,6 +92,7 @@ const deferredForegroundRecoveryAdapterPorts = rawPort("deferredForegroundRecove
81
92
  const ensureTerminalBridgeMonitorAfterApproval = rawPort("ensureTerminalBridgeMonitorAfterApproval");
82
93
  const exactSafeAbortedRecoveredSessionMatches = rawPort("exactSafeAbortedRecoveredSessionMatches");
83
94
  const inspectCodexOpenRootRolloutInventory = rawPort("inspectCodexOpenRootRolloutInventory");
95
+ const identifyCodexForegroundWhileLocked = rawPort("identifyCodexForegroundWhileLocked");
84
96
  const isDiscoverableTmuxConversation = rawPort("isDiscoverableTmuxConversation");
85
97
  const loadClaudeAgentRows = rawPort("loadClaudeAgentRows");
86
98
  const loadConversationFromOptions = rawPort("loadConversationFromOptions");
@@ -143,6 +155,11 @@ const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
143
155
  const DEFAULT_AGENT_HARD_TIMEOUT_MINUTES = 720;
144
156
  const DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS = 5000;
145
157
  const DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS = 50;
158
+ // Human-explicit Send still falls back to one physical terminal dispatch when
159
+ // managed preparation is genuinely unavailable. Give ordinary Store/monitor
160
+ // contention a short chance to clear first so a transient writer lease does
161
+ // not unnecessarily discard managed questionnaire response authority.
162
+ const USER_EXPLICIT_MANAGED_LOCK_GRACE_MS = 1_000;
146
163
  const CLAUDE_SCREEN_APPROVAL_TTL_MS = 10 * 60 * 1000;
147
164
  function replayReceiptBindingMismatch(receipt, expectation) {
148
165
  return Boolean(!terminalDispatchRecordMatchesControl(receipt, expectation.terminalControl) ||
@@ -261,7 +278,7 @@ function replayLoggedMessageMismatch(loggedMessage, owner, expectedMessageType,
261
278
  loggedMessage.session_id !== sessionIdForConversation(owner) ||
262
279
  loggedMessage.turn_id !== turnIdForConversation(owner));
263
280
  }
264
- function replayExactActiveTerminalSubmission({ options, terminalControl, requestText, expectedStoreDir, expectedSessionId, expectedTurnId, expectedMessageType = "task", expectedStatePath }) {
281
+ function replayExactActiveTerminalSubmission({ options, terminalControl, requestText, expectedStoreDir, expectedSessionId, expectedTurnId, expectedMessageType = "task", expectedStatePath, userExplicitTerminalId }) {
265
282
  const messageId = stringValue(options.messageId);
266
283
  if (!messageId) {
267
284
  return false;
@@ -338,14 +355,15 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
338
355
  expectedSessionId,
339
356
  expectedTurnId,
340
357
  expectedMessageType,
341
- expectedStatePath
358
+ expectedStatePath,
359
+ userExplicitTerminalId
342
360
  })) {
343
361
  return true;
344
362
  }
345
363
  if (durableReceiptCannotDispatchAgain(ledgerReceipt, loadedLedger, loadedLedgerMessageId, messageId)) {
346
364
  throw new Error(`terminal idempotency key ${messageId} already has durable ` +
347
365
  `${String(ledgerReceipt.status)} proof and cannot be dispatched again; ` +
348
- "no terminal input was sent");
366
+ "no additional terminal input was sent");
349
367
  }
350
368
  const incarnationLedger = resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, loadedLedger);
351
369
  const ledger = replayableReconciledLedger(reconcilePreparedTerminalDispatchLedger(terminalControl, incarnationLedger), expectation, messageId);
@@ -433,46 +451,31 @@ function replayExactActiveTerminalSubmission({ options, terminalControl, request
433
451
  });
434
452
  const acceptanceInvalid = replayReceipt.submission_outcome === "uncertain";
435
453
  const callbackExpected = callbackExpectedForConversationWithLegacyFallback(owner, ledger.callback_expected);
436
- printJson({
437
- session_id: sessionIdForConversation(owner),
438
- turn_id: turnIdForConversation(owner),
439
- conversation: owner,
454
+ presentTerminalDispatchReplay({
455
+ owner,
456
+ receipt: replayReceipt,
457
+ accepted: replayReceipt.submission_outcome === "agent_accepted",
458
+ acceptanceInvalid,
459
+ receiptConversationId: owner.conversation_id,
460
+ receiptMessageId: messageId,
461
+ callbackExpected,
462
+ ...(userExplicitTerminalId
463
+ ? {
464
+ userExplicit: {
465
+ terminalId: userExplicitTerminalId,
466
+ messageId
467
+ }
468
+ }
469
+ : {})
470
+ }, {
440
471
  message: replayedMessage,
441
- delivered: replayReceipt.delivered,
442
- status: replayReceipt.status,
443
- submission_outcome: replayReceipt.submission_outcome,
444
- background: true,
445
- callback_expected: !acceptanceInvalid && callbackExpected,
446
- terminal_control: terminalControl,
447
472
  executor,
448
- replayed: replayReceipt.replayed,
449
- delivery_receipt: replayReceipt.delivery_receipt,
450
- ...(replayReceipt.do_not_retry
451
- ? { do_not_retry: replayReceipt.do_not_retry }
452
- : {}),
453
- reason: replayReceipt.delivered
454
- ? "AKK replayed the durable native acceptance receipt and sent no additional terminal input."
455
- : acceptanceInvalid
456
- ? "AKK replayed an invalid native acceptance receipt as uncertain and sent no additional terminal input."
457
- : "AKK replayed the original transport proof without upgrading it and sent no additional terminal input.",
458
- openclaw_next_action: replayReceipt.delivered
459
- ? openClawYieldNextAction({
460
- conversationId: owner.conversation_id,
461
- sessionId: sessionIdForConversation(owner),
462
- turnId: turnIdForConversation(owner),
463
- source: "terminal_control",
464
- callbackExpected
465
- })
466
- : {
467
- action: "inspect",
468
- conversation_id: owner.conversation_id,
469
- session_id: sessionIdForConversation(owner),
470
- turn_id: turnIdForConversation(owner),
471
- do_not_retry: true,
472
- reason: acceptanceInvalid
473
- ? "Stored native acceptance evidence is invalid."
474
- : "Only terminal transport is proven."
475
- }
473
+ terminalControl
474
+ }, {
475
+ write: printJson,
476
+ budget: budgetAction,
477
+ nextAction: openClawYieldNextAction,
478
+ summarize: textSummary
476
479
  });
477
480
  return true;
478
481
  }
@@ -597,7 +600,7 @@ function validateStoredTerminalSubmissionMatch({ owner, receipt, options, termin
597
600
  }
598
601
  return loggedMessage;
599
602
  }
600
- function replayExactStoredTerminalSubmission({ options, terminalControl, requestText, requestHash, messageId, expectedStoreDir, expectedSessionId, expectedTurnId, expectedMessageType, expectedStatePath }) {
603
+ function replayExactStoredTerminalSubmission({ options, terminalControl, requestText, requestHash, messageId, expectedStoreDir, expectedSessionId, expectedTurnId, expectedMessageType, expectedStatePath, userExplicitTerminalId }) {
601
604
  const allMatches = listConversations(expectedStoreDir).flatMap((owner) => terminalBridgeSubmissionReceipts(owner)
602
605
  .filter((receipt) => stringValue(receipt.message_id) === messageId)
603
606
  .map((receipt) => ({ owner, receipt })));
@@ -663,7 +666,7 @@ function replayExactStoredTerminalSubmission({ options, terminalControl, request
663
666
  }
664
667
  if (!["submitted", "enter_dispatched", "agent_accepted"].includes(String(receipt.status))) {
665
668
  throw new Error(`terminal idempotency key ${messageId} has durable ${String(receipt.status)} ` +
666
- "proof and must not be retried; no terminal input was sent");
669
+ "proof and must not be retried; no additional terminal input was sent");
667
670
  }
668
671
  const executor = executorForConversation(owner);
669
672
  const nativeThreadId = stringValue(receipt.native_thread_id) ??
@@ -699,52 +702,45 @@ function replayExactStoredTerminalSubmission({ options, terminalControl, request
699
702
  });
700
703
  const acceptanceInvalid = replayReceipt.submission_outcome === "uncertain";
701
704
  const callbackExpected = callbackExpectedForConversation(owner);
702
- printJson({
703
- session_id: sessionIdForConversation(owner),
704
- turn_id: turnIdForConversation(owner),
705
- conversation: owner,
705
+ presentTerminalDispatchReplay({
706
+ owner,
707
+ receipt: replayReceipt,
708
+ accepted: replayReceipt.submission_outcome === "agent_accepted",
709
+ acceptanceInvalid,
710
+ receiptConversationId: owner.conversation_id,
711
+ receiptMessageId: messageId,
712
+ callbackExpected,
713
+ ...(userExplicitTerminalId
714
+ ? {
715
+ userExplicit: {
716
+ terminalId: userExplicitTerminalId,
717
+ messageId
718
+ }
719
+ }
720
+ : {})
721
+ }, {
706
722
  message: replayedMessage,
707
- delivered: replayReceipt.delivered,
708
- status: replayReceipt.status,
709
- submission_outcome: replayReceipt.submission_outcome,
710
- background: true,
711
- callback_expected: !acceptanceInvalid && callbackExpected,
712
- terminal_control: terminalControl,
713
723
  executor,
714
- replayed: true,
715
- delivery_receipt: replayReceipt.delivery_receipt,
716
- ...(replayReceipt.do_not_retry
717
- ? { do_not_retry: replayReceipt.do_not_retry }
718
- : {}),
719
- reason: replayReceipt.delivered
720
- ? "AKK replayed the stored durable native acceptance receipt and sent no additional terminal input."
721
- : acceptanceInvalid
722
- ? "AKK replayed an invalid stored native acceptance receipt as uncertain and sent no additional terminal input."
723
- : "AKK replayed the stored transport proof without upgrading it and sent no additional terminal input.",
724
- openclaw_next_action: replayReceipt.delivered
725
- ? openClawYieldNextAction({
726
- conversationId: owner.conversation_id,
727
- sessionId: sessionIdForConversation(owner),
728
- turnId: turnIdForConversation(owner),
729
- source: "terminal_control",
730
- callbackExpected
731
- })
732
- : {
733
- action: "inspect",
734
- conversation_id: owner.conversation_id,
735
- session_id: sessionIdForConversation(owner),
736
- turn_id: turnIdForConversation(owner),
737
- do_not_retry: true,
738
- reason: acceptanceInvalid
739
- ? "Stored native acceptance evidence is invalid."
740
- : "Only terminal transport is proven."
741
- }
724
+ terminalControl
725
+ }, {
726
+ write: printJson,
727
+ budget: budgetAction,
728
+ nextAction: openClawYieldNextAction,
729
+ summarize: textSummary
742
730
  });
743
731
  return true;
744
732
  }
745
733
  const terminalBridgeSubmission = dispatchReceipt.terminalBridgeSubmission;
746
734
  const terminalBridgeSubmissionReceipts = dispatchReceipt.terminalBridgeSubmissionReceipts;
747
735
  const unresolvedTerminalBridgeSubmission = dispatchReceipt.unresolvedTerminalBridgeSubmission;
736
+ function durableTerminalInputDispatched(conversation) {
737
+ try {
738
+ return dispatchReceipt.terminalInputDispatchedForConversation(conversation);
739
+ }
740
+ catch {
741
+ return dispatchReceipt.terminalSubmissionIndicatesInputDispatched(terminalBridgeSubmission(conversation));
742
+ }
743
+ }
748
744
  function assertNoUnresolvedTerminalBridgeSubmission(storeDir, terminalControl, currentConversationId, requestText) {
749
745
  const requestHash = terminalBridgeRequestFingerprint(requestText);
750
746
  for (const candidate of listConversations(storeDir)) {
@@ -1406,12 +1402,24 @@ function terminalSubmissionRetryLedgerFieldsAtState(attempt, state, revision) {
1406
1402
  return fields;
1407
1403
  }
1408
1404
  function printTerminalSubmissionRetryOutcome(input) {
1405
+ const submission = terminalBridgeSubmission(input.conversation);
1406
+ const enterDispatched = input.terminalInputSent ||
1407
+ ["agent_accepted", "enter_dispatched", "not_accepted"].includes(input.outcome) ||
1408
+ terminalSendEnterDispatched(submission) ||
1409
+ ["enter_dispatched", "agent_accepted"].includes(String(input.attempt?.state ?? ""));
1410
+ const terminalInputDispatched = enterDispatched ||
1411
+ durableTerminalInputDispatched(input.conversation);
1412
+ const accepted = input.outcome === "agent_accepted";
1413
+ const pending = input.outcome === "enter_dispatched";
1414
+ const callbackAvailable = !isTerminalDispatchOwnerReleasedStatus(input.conversation.status) &&
1415
+ (accepted || pending) &&
1416
+ callbackExpectedForConversation(input.conversation);
1409
1417
  printJson({
1410
1418
  session_id: sessionIdForConversation(input.conversation),
1411
1419
  turn_id: turnIdForConversation(input.conversation),
1412
1420
  conversation: input.conversation,
1413
- delivered: input.outcome === "agent_accepted",
1414
- delivery_receipt: input.outcome === "agent_accepted"
1421
+ delivered: enterDispatched,
1422
+ delivery_receipt: accepted
1415
1423
  ? "agent_accepted"
1416
1424
  : input.outcome === "enter_dispatched" ||
1417
1425
  input.outcome === "not_accepted"
@@ -1434,9 +1442,16 @@ function printTerminalSubmissionRetryOutcome(input) {
1434
1442
  replayed: true,
1435
1443
  terminal_control: input.terminalControl,
1436
1444
  terminal_input_sent: input.terminalInputSent,
1437
- callback_expected: ["refused", "not_accepted"].includes(input.outcome)
1438
- ? false
1439
- : callbackExpectedForConversation(input.conversation),
1445
+ callback_expected: callbackAvailable,
1446
+ ...terminalSendResultContract({
1447
+ terminalInputDispatched,
1448
+ agentAcceptance: accepted ? "proven" : "unproven",
1449
+ managementMode: "managed",
1450
+ observationMode: callbackAvailable ? "managed_monitor" : "none",
1451
+ callbackAvailable,
1452
+ interactionNotificationAvailable: accepted,
1453
+ interactionResponseAvailable: accepted
1454
+ }),
1440
1455
  ...(input.attempt
1441
1456
  ? {
1442
1457
  submission_retry_attempt_id: input.attempt.attempt_id,
@@ -2996,6 +3011,54 @@ async function runSend(options) {
2996
3011
  }
2997
3012
  return runManagedSessionSend(options, messageBody);
2998
3013
  }
3014
+ function captureDetachedCodexCandidateSessionClaims(input) {
3015
+ const { inventory, terminal } = input;
3016
+ const candidates = new Map(inventory.roots.map((root) => [
3017
+ root.sessionId.toLowerCase(),
3018
+ root
3019
+ ]));
3020
+ const claims = listManagedSessions(input.storeDir).flatMap((session) => {
3021
+ const binding = session.binding;
3022
+ const nativeThreadId = binding?.native_thread_id?.toLowerCase();
3023
+ const candidate = nativeThreadId ? candidates.get(nativeThreadId) : undefined;
3024
+ if (session.status !== "detached" || session.agent !== "codex" ||
3025
+ !binding || !nativeThreadId || !candidate ||
3026
+ !isCompleteNativeRollout(binding.native_process.rollout) ||
3027
+ path.resolve(session.workspace) !==
3028
+ path.resolve(terminal.terminalControl.currentPath ?? "") ||
3029
+ binding.native_process.pid !== inventory.pid ||
3030
+ binding.native_process.process_uuid !== inventory.processUuid ||
3031
+ binding.native_process.process_birth !== inventory.processBirth ||
3032
+ candidate.processUuid !== inventory.processUuid ||
3033
+ candidate.processBirth !== inventory.processBirth ||
3034
+ !terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.conversationId, terminal.terminalControl) ||
3035
+ !rolloutFileIdentityMatches(binding.native_process.rollout, candidate.rollout)) {
3036
+ return [];
3037
+ }
3038
+ return [{
3039
+ session_id: session.session_id,
3040
+ session_revision: managedSessionRevision(session),
3041
+ session_binding_token: managedSessionBindingToken(session),
3042
+ binding_id: binding.binding_id,
3043
+ binding_generation: binding.generation,
3044
+ native_thread_id: nativeThreadId,
3045
+ process_uuid: inventory.processUuid,
3046
+ process_birth: inventory.processBirth,
3047
+ source_rollout: { ...binding.native_process.rollout },
3048
+ candidate_rollout: { ...candidate.rollout }
3049
+ }];
3050
+ }).sort((left, right) => left.native_thread_id.localeCompare(right.native_thread_id) ||
3051
+ left.session_id.localeCompare(right.session_id));
3052
+ if (claims.length === 0)
3053
+ return undefined;
3054
+ const base = {
3055
+ schema: "agent-knock-knock/codex-detached-candidate-session-claims",
3056
+ version: 1,
3057
+ anchor_fingerprint: input.anchor.anchor_fingerprint,
3058
+ claims
3059
+ };
3060
+ return { ...base, claims_fingerprint: fingerprint(base) };
3061
+ }
2999
3062
  function rawTerminalInitialAuthority({ options, terminal, storeDir }) {
3000
3063
  const claimedSession = soleBoundManagedSessionClaimForTerminal(storeDir, terminal);
3001
3064
  const suppliedExpectedTerminalToken = stringValue(options.expectedTerminalToken);
@@ -3235,6 +3298,7 @@ function releaseTerminalManagementForExplicitSendUnderWriter(input) {
3235
3298
  warnings.push(`Turn ${explicitSendTurnReference(current)} dispatch cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
3236
3299
  }
3237
3300
  try {
3301
+ const terminalInputSent = durableTerminalInputDispatched(current);
3238
3302
  appendExplicitUserCloseEvent(paths.logPath, {
3239
3303
  ts: now,
3240
3304
  conversation_id: current.conversation_id,
@@ -3242,7 +3306,8 @@ function releaseTerminalManagementForExplicitSendUnderWriter(input) {
3242
3306
  status: "closed",
3243
3307
  reason: closed.close_reason,
3244
3308
  disposition: "user_abandoned_management",
3245
- terminal_input_sent: false,
3309
+ terminal_input_sent: terminalInputSent,
3310
+ terminal_input_dispatched: terminalInputSent,
3246
3311
  coding_agent_stopped: false,
3247
3312
  tmux_pane_closed: false
3248
3313
  });
@@ -3339,21 +3404,19 @@ function explicitTerminalSendIntentRuntimeDir() {
3339
3404
  : path.join(path.dirname(defaultStoreDir()), "runtime-v2");
3340
3405
  }
3341
3406
  function assertSafeUserExplicitTerminalSend(status) {
3342
- if (status?.reachable !== true) {
3343
- throw new Error("the explicitly selected terminal is unreachable");
3344
- }
3345
- const approval = isRecord(status.approval_state)
3407
+ const approval = status && isRecord(status.approval_state)
3346
3408
  ? status.approval_state
3347
3409
  : undefined;
3348
- if (approval?.scanned !== true) {
3349
- throw new Error("the explicitly selected terminal approval state could not be verified");
3350
- }
3351
- if (approval?.blocked === true) {
3352
- throw new Error(stringValue(approval.reason) ??
3353
- "the explicitly selected terminal is waiting at an approval prompt");
3354
- }
3355
- if (status.activity_state === "awaiting_approval") {
3356
- throw new Error("the explicitly selected terminal is waiting at an approval prompt");
3410
+ const decision = decideUserExplicitTerminalInputSafety({
3411
+ reachable: status?.reachable === true,
3412
+ approvalScanned: approval?.scanned === true,
3413
+ approvalBlocked: approval?.blocked === true,
3414
+ approvalReason: stringValue(approval?.reason),
3415
+ awaitingApproval: status?.activity_state === "awaiting_approval",
3416
+ questionnaireActive: status?.interaction_state !== undefined
3417
+ });
3418
+ if (decision.action === "reject") {
3419
+ throw new Error(decision.reason);
3357
3420
  }
3358
3421
  }
3359
3422
  function terminalUserSendIntentContext(options, messageBody, terminal) {
@@ -3432,13 +3495,19 @@ function acquireUserExplicitTerminalSendLock(options, terminal, warnings) {
3432
3495
  function printReplayedUserExplicitSend(options, terminal, intent, deliveryMode) {
3433
3496
  if (deliveryMode === "managed") {
3434
3497
  printJson({
3435
- delivered: false,
3498
+ delivered: true,
3436
3499
  replayed: true,
3437
3500
  status: "submission_pending_acceptance",
3438
3501
  submission_outcome: "pending_acceptance",
3439
3502
  delivery_receipt: "enter_dispatched",
3440
3503
  do_not_retry: true,
3441
- management_mode: "managed",
3504
+ ...terminalSendResultContract({
3505
+ terminalInputDispatched: true,
3506
+ agentAcceptance: "unproven",
3507
+ managementMode: "managed",
3508
+ observationMode: "none",
3509
+ callbackAvailable: false
3510
+ }),
3442
3511
  terminal_id: terminal.conversationId,
3443
3512
  message_id: intent.messageId,
3444
3513
  scope: "terminal_user_explicit"
@@ -3454,11 +3523,20 @@ function printReplayedUserExplicitSend(options, terminal, intent, deliveryMode)
3454
3523
  options,
3455
3524
  watchId
3456
3525
  });
3526
+ const callbackAvailable = callbackReceipt?.callback_expected === true;
3457
3527
  printJson({
3458
3528
  delivered: true,
3459
3529
  delivered_unmanaged: true,
3530
+ delivery_receipt: "enter_dispatched",
3460
3531
  ...(callbackReceipt ?? { callback_expected: false }),
3461
- management_mode: "unmanaged_fallback",
3532
+ ...terminalSendResultContract({
3533
+ terminalInputDispatched: true,
3534
+ agentAcceptance: "unproven",
3535
+ managementMode: "unmanaged",
3536
+ observationMode: callbackAvailable ? "terminal_watch" : "none",
3537
+ callbackAvailable,
3538
+ interactionNotificationAvailable: callbackAvailable
3539
+ }),
3462
3540
  replayed: true,
3463
3541
  terminal_id: terminal.conversationId,
3464
3542
  message_id: intent.messageId,
@@ -3748,9 +3826,11 @@ async function runUserExplicitTerminalFallback(options, terminal, managedFailure
3748
3826
  composer_disposition: composerDisposition,
3749
3827
  delivered_unmanaged: true
3750
3828
  });
3829
+ const callbackAvailable = callbackReceipt?.callback_expected === true;
3751
3830
  printJson({
3752
3831
  delivered: true,
3753
3832
  delivered_unmanaged: true,
3833
+ delivery_receipt: "enter_dispatched",
3754
3834
  cleanup_warnings: cleanupWarnings,
3755
3835
  intent_warnings: intentWarnings,
3756
3836
  callback_warnings: callbackWarnings,
@@ -3758,7 +3838,14 @@ async function runUserExplicitTerminalFallback(options, terminal, managedFailure
3758
3838
  terminal_id: fresh.conversationId,
3759
3839
  message_id: messageId,
3760
3840
  scope: "terminal_user_explicit",
3761
- management_mode: "unmanaged_fallback",
3841
+ ...terminalSendResultContract({
3842
+ terminalInputDispatched: true,
3843
+ agentAcceptance: "unproven",
3844
+ managementMode: "unmanaged",
3845
+ observationMode: callbackAvailable ? "terminal_watch" : "none",
3846
+ callbackAvailable,
3847
+ interactionNotificationAvailable: callbackAvailable
3848
+ }),
3762
3849
  composer_disposition: composerDisposition,
3763
3850
  composer_cleared_before_send: true,
3764
3851
  // Deprecated compatibility alias. The v22 policy always dispatches
@@ -3783,7 +3870,14 @@ async function runUserExplicitTerminalFallback(options, terminal, managedFailure
3783
3870
  }
3784
3871
  }
3785
3872
  async function runRawTerminalSend(options, messageBody, terminalConversation) {
3873
+ const identifyForeground = options.identifyForeground === true;
3786
3874
  const suppliedExpectedTerminalToken = stringValue(options.expectedTerminalToken);
3875
+ if (identifyForeground && terminalConversation.agent !== "codex") {
3876
+ throw new Error("atomic foreground identification currently supports only Codex terminals");
3877
+ }
3878
+ if (identifyForeground && !suppliedExpectedTerminalToken) {
3879
+ throw new Error("atomic foreground identification requires --expected-terminal-token");
3880
+ }
3787
3881
  const userExplicitAuthorityRequested = Boolean(suppliedExpectedTerminalToken && options.managedOnly !== true);
3788
3882
  const freshExplicitAuthority = userExplicitAuthorityRequested
3789
3883
  ? hasFreshExplicitTerminalSendToken(options, terminalConversation)
@@ -3821,6 +3915,14 @@ async function runRawTerminalSend(options, messageBody, terminalConversation) {
3821
3915
  }
3822
3916
  return;
3823
3917
  }
3918
+ if (identifyForeground) {
3919
+ // The explicit atomic contract never falls back to unmanaged delivery:
3920
+ // doing so would discard the very identity/respond capability the caller
3921
+ // asked this operation to establish. The task payload is proven unsent,
3922
+ // so release its same-id reservation for an explicit later retry.
3923
+ cancelProvenZeroInputUserExplicitSendIntent(intentLease, terminalConversation, managedResult.failure);
3924
+ throw managedResult.failure;
3925
+ }
3824
3926
  return runUserExplicitTerminalFallback(explicitOptions, terminalConversation, managedResult.failure, intentLease);
3825
3927
  }
3826
3928
  async function runManagedRawTerminalSend(options, messageBody, terminalConversation, deferZeroInputFailurePresentation = false) {
@@ -3843,6 +3945,343 @@ async function runManagedRawTerminalSend(options, messageBody, terminalConversat
3843
3945
  `automatic unmanaged fallback: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
3844
3946
  }
3845
3947
  }
3948
+ const USER_EXPLICIT_CALLBACK_SUPERSEDE_REASON = "superseded_by_user_explicit_send";
3949
+ function sessionHasUnresolvedForegroundMutation(storeDir, sessionId) {
3950
+ try {
3951
+ if (listNativeThreadTransitions(storeDir).some((transition) => (transition.source_session_id === sessionId ||
3952
+ transition.target_session_id === sessionId) &&
3953
+ !["committed", "aborted"].includes(transition.status))) {
3954
+ return true;
3955
+ }
3956
+ return listDeferredForegroundTransfers(storeDir).some((transfer) => (transfer.source_session_id === sessionId ||
3957
+ transfer.target_session_id === sessionId) &&
3958
+ !isFinalDeferredForegroundTransferStatus(transfer.status));
3959
+ }
3960
+ catch {
3961
+ // Corrupt or unreadable lifecycle evidence must never be bypassed.
3962
+ return true;
3963
+ }
3964
+ }
3965
+ async function supersedeExactHumanExplicitCallbackDebt(input) {
3966
+ const { options, terminal, session, candidateInventory, storeDir, scopes, resources } = input;
3967
+ const expectedManagedToken = stringValue(options.expectedTerminalToken);
3968
+ const binding = session?.binding;
3969
+ if (terminal.agent !== "codex" ||
3970
+ stringValue(options.expectedUserExplicitTerminalToken) === undefined ||
3971
+ !session ||
3972
+ session.status !== "bound" ||
3973
+ !binding ||
3974
+ !humanExplicitCallbackDebtManagedTokenMatches(expectedManagedToken, session) ||
3975
+ !candidateInventory ||
3976
+ !exactBoundCodexSendSource({
3977
+ kind: "candidate",
3978
+ sourceSession: session,
3979
+ context: {
3980
+ terminalId: terminal.conversationId,
3981
+ terminalControl: terminal.terminalControl,
3982
+ pid: terminal.pid,
3983
+ workspace: terminal.terminalControl.currentPath,
3984
+ liveProcessUuid: candidateInventory.processUuid,
3985
+ liveProcessBirth: candidateInventory.processBirth
3986
+ },
3987
+ inventory: candidateInventory,
3988
+ sourceRolloutAuthority: "present"
3989
+ }) ||
3990
+ sessionHasUnresolvedForegroundMutation(storeDir, session.session_id)) {
3991
+ return false;
3992
+ }
3993
+ const bindingTurns = managedTurnsForSession(storeDir, session.session_id)
3994
+ .filter((turn) => turn.terminal_binding_id === binding.binding_id &&
3995
+ turn.terminal_binding_generation === binding.generation);
3996
+ const dispositions = bindingTurns.map((turn) => ({
3997
+ turn,
3998
+ disposition: humanExplicitCallbackDebtRetirementDisposition(turn, session)
3999
+ }));
4000
+ const candidates = dispositions
4001
+ .filter(({ disposition }) => disposition === "supersedable")
4002
+ .map(({ turn }) => turn);
4003
+ if (candidates.length === 0 ||
4004
+ dispositions.some(({ disposition }) => disposition === undefined)) {
4005
+ return false;
4006
+ }
4007
+ let superseded = 0;
4008
+ for (const candidate of candidates) {
4009
+ const paths = pathsForConversation(candidate.conversation_id, storeDir);
4010
+ const expectedDelivery = JSON.stringify(candidate.callback_delivery);
4011
+ await withTerminalDispatchStateScope(scopes, resources, paths.statePath, paths.logPath, async () => {
4012
+ const current = loadState(paths.statePath);
4013
+ if (JSON.stringify(current.callback_delivery) !== expectedDelivery ||
4014
+ humanExplicitCallbackDebtDisposition(current, session) !==
4015
+ "supersedable") {
4016
+ return;
4017
+ }
4018
+ const at = cliNow().toISOString();
4019
+ const next = supersedeUnacceptedCallbackDeliveries(current, {
4020
+ at,
4021
+ reason: USER_EXPLICIT_CALLBACK_SUPERSEDE_REASON
4022
+ });
4023
+ if (next.callback_delivery === current.callback_delivery)
4024
+ return;
4025
+ saveState(paths.statePath, next);
4026
+ appendEvent(paths.logPath, {
4027
+ ts: at,
4028
+ conversation_id: current.conversation_id,
4029
+ event: "callback_delivery_superseded_by_user_explicit_send",
4030
+ status: current.status,
4031
+ reason: USER_EXPLICIT_CALLBACK_SUPERSEDE_REASON,
4032
+ prior_callback_status: isRecord(current.callback_delivery)
4033
+ ? current.callback_delivery.status
4034
+ : undefined,
4035
+ prior_callback_attempt_disposition: callbackDeliveryAttemptOutcome(current.callback_delivery)
4036
+ ?.disposition,
4037
+ // An uncertain transport may already have reached the controller.
4038
+ // This records that the human-priority Send retired only future
4039
+ // callback retries; it never asserts that delivery did not occur.
4040
+ human_override_of_uncertain_callback: callbackDeliveryAttemptOutcome(current.callback_delivery)
4041
+ ?.disposition === "uncertain",
4042
+ terminal_input_sent: false,
4043
+ terminal_input_dispatched: false
4044
+ });
4045
+ superseded += 1;
4046
+ });
4047
+ }
4048
+ return superseded === candidates.length;
4049
+ }
4050
+ function rawTerminalObservedIdentityForPreparation(input) {
4051
+ if (input.atomicForegroundIdentification ||
4052
+ input.sourceLessCandidateInventory !== undefined ||
4053
+ input.observation.status !== "resolved") {
4054
+ return undefined;
4055
+ }
4056
+ return input.observation.identity;
4057
+ }
4058
+ async function maybePrepareVerifiedEmptyCodexHandoff(input) {
4059
+ if (input.implicitCandidateAuthority ||
4060
+ input.atomicForegroundIdentification) {
4061
+ return undefined;
4062
+ }
4063
+ return maybeDetachVerifiedEmptyCodexSource({
4064
+ options: input.options,
4065
+ terminal: input.terminal,
4066
+ sourceSession: input.sourceSession,
4067
+ observation: input.observation
4068
+ });
4069
+ }
4070
+ function assertAtomicForegroundCandidateInventory(input) {
4071
+ if (!input.enabled || input.claimedSession === undefined)
4072
+ return;
4073
+ if (input.inventory && input.inventory.roots.length > 0)
4074
+ return;
4075
+ throw new Error("atomic foreground identification requires a fresh nonempty " +
4076
+ "candidate-set acceptance boundary; no task input was sent");
4077
+ }
4078
+ function assertAtomicForegroundAcceptanceBoundary(input) {
4079
+ if (!input.enabled)
4080
+ return;
4081
+ if (input.deferred?.candidateAcceptanceAnchor || input.postSend)
4082
+ return;
4083
+ throw new Error("atomic foreground identification could not establish an exact " +
4084
+ "task-acceptance boundary; no task input was sent");
4085
+ }
4086
+ async function prepareRawTerminalDispatchAuthority(input) {
4087
+ const { options, messageBody, terminal, storeDir, scopes, resources } = input;
4088
+ const atomicForegroundIdentification = options.identifyForeground === true;
4089
+ const initialAuthority = rawTerminalInitialAuthority({
4090
+ options,
4091
+ terminal,
4092
+ storeDir
4093
+ });
4094
+ let { claimedSession, knownCodexCompanions } = initialAuthority;
4095
+ const { suppliedExpectedTerminalToken, implicitCodexCandidateAuthority } = initialAuthority;
4096
+ const userExplicitTerminalToken = stringValue(options.expectedUserExplicitTerminalToken);
4097
+ const nativeIdentityObservation = await observeCurrentNativeAgentSessionIdentity({
4098
+ options,
4099
+ agent: terminal.agent,
4100
+ pid: terminal.pid,
4101
+ cwd: terminal.terminalControl.currentPath,
4102
+ preferredSessionId: knownCodexCompanions.primary
4103
+ ? claimedSession?.binding?.native_thread_id
4104
+ : undefined,
4105
+ allowedCompanionIdentity: knownCodexCompanions.primary,
4106
+ allowedAdditionalIdentities: knownCodexCompanions.additional
4107
+ });
4108
+ let deferredCodexCandidateInventory;
4109
+ if (terminal.agent === "codex" &&
4110
+ (userExplicitTerminalToken ||
4111
+ suppliedExpectedTerminalToken ||
4112
+ implicitCodexCandidateAuthority)) {
4113
+ try {
4114
+ const inventory = await inspectCodexOpenRootRolloutInventory({
4115
+ options,
4116
+ pid: terminal.pid,
4117
+ cwd: terminal.terminalControl.currentPath
4118
+ });
4119
+ // An empty set is meaningful only for a source-less human Send: it is
4120
+ // the frozen proof that the first accepting rollout must appear after
4121
+ // Enter. Preserve the older bound/deferred behavior otherwise.
4122
+ if (inventory.roots.length > 0 ||
4123
+ (userExplicitTerminalToken !== undefined && claimedSession === undefined)) {
4124
+ deferredCodexCandidateInventory = inventory;
4125
+ }
4126
+ }
4127
+ catch (error) {
4128
+ if ((userExplicitTerminalToken !== undefined &&
4129
+ claimedSession === undefined) ||
4130
+ implicitCodexCandidateAuthority ||
4131
+ nativeIdentityObservation.status === "unavailable") {
4132
+ throw new Error(`native Codex foreground attribution requires a fresh complete ` +
4133
+ `open-root inventory: ${error instanceof Error ? error.message : String(error)}`);
4134
+ }
4135
+ }
4136
+ }
4137
+ assertRawTerminalCandidateAuthority({
4138
+ terminal,
4139
+ nativeIdentityObservation,
4140
+ deferredCodexCandidateInventory,
4141
+ implicitCodexCandidateAuthority
4142
+ });
4143
+ const sourceLessCodexCandidateInventory = terminal.agent === "codex" &&
4144
+ userExplicitTerminalToken !== undefined &&
4145
+ claimedSession === undefined
4146
+ ? deferredCodexCandidateInventory
4147
+ : undefined;
4148
+ let currentNativeIdentity = rawTerminalObservedIdentityForPreparation({
4149
+ atomicForegroundIdentification,
4150
+ sourceLessCandidateInventory: sourceLessCodexCandidateInventory,
4151
+ observation: nativeIdentityObservation
4152
+ });
4153
+ // A fresh nonempty inventory is the stronger physical authority for an
4154
+ // implicit candidate send. Do not let an earlier verified-absent
4155
+ // observation divert this path into the token-only empty handoff.
4156
+ const verifiedEmptyHandoff = await maybePrepareVerifiedEmptyCodexHandoff({
4157
+ options,
4158
+ terminal,
4159
+ sourceSession: claimedSession,
4160
+ observation: nativeIdentityObservation,
4161
+ implicitCandidateAuthority: implicitCodexCandidateAuthority,
4162
+ atomicForegroundIdentification
4163
+ });
4164
+ if (verifiedEmptyHandoff) {
4165
+ // The old rollout is conclusively closed. Never carry it forward as a
4166
+ // pre-materialization companion for the new virgin Session.
4167
+ claimedSession = undefined;
4168
+ knownCodexCompanions = { additional: [] };
4169
+ currentNativeIdentity = undefined;
4170
+ }
4171
+ const physicalNativeIdentityBeforeHandoff = currentNativeIdentity;
4172
+ // The status card emitted by identify_foreground is diagnostic evidence,
4173
+ // never handoff authority. Let the existing candidate-set/deferred path
4174
+ // bind only the rollout that later accepts the exact task request.
4175
+ let handoff = atomicForegroundIdentification
4176
+ ? { identity: undefined, adopted: false }
4177
+ : await maybeAdoptObservedExternalThread({
4178
+ options,
4179
+ terminal,
4180
+ // A no-token raw send to an already managed rollout-backed Codex pane
4181
+ // is an internal follow-current delegation, not a sole-root
4182
+ // continuation.
4183
+ sourceSession: implicitCodexCandidateAuthority
4184
+ ? undefined
4185
+ : claimedSession,
4186
+ resolvedIdentity: sourceLessCodexCandidateInventory
4187
+ ? undefined
4188
+ : currentNativeIdentity,
4189
+ storeDir
4190
+ });
4191
+ currentNativeIdentity =
4192
+ handoff.adopted && terminal.agent === "codex" &&
4193
+ !handoff.session?.binding?.native_process.rollout
4194
+ ? physicalNativeIdentityBeforeHandoff
4195
+ : handoff.identity;
4196
+ if (!handoff.adopted) {
4197
+ await supersedeExactHumanExplicitCallbackDebt({
4198
+ options,
4199
+ terminal,
4200
+ session: claimedSession,
4201
+ candidateInventory: deferredCodexCandidateInventory,
4202
+ storeDir,
4203
+ scopes,
4204
+ resources
4205
+ });
4206
+ }
4207
+ assertAtomicForegroundCandidateInventory({
4208
+ enabled: atomicForegroundIdentification,
4209
+ claimedSession,
4210
+ inventory: deferredCodexCandidateInventory
4211
+ });
4212
+ const deferredCodexForegroundBinding = !handoff.adopted
4213
+ ? await prepareDeferredCodexForegroundBinding({
4214
+ options,
4215
+ scope: bindDeferredForegroundWriterScope(scopes, resources),
4216
+ terminal,
4217
+ sourceSession: claimedSession,
4218
+ observation: nativeIdentityObservation,
4219
+ candidateInventory: deferredCodexCandidateInventory,
4220
+ requestText: String(messageBody),
4221
+ allowImplicitFreshAuthority: implicitCodexCandidateAuthority
4222
+ })
4223
+ : undefined;
4224
+ const postSendCodexCandidateAnchor = terminal.agent === "codex" &&
4225
+ userExplicitTerminalToken !== undefined &&
4226
+ sourceLessCodexCandidateInventory !== undefined &&
4227
+ verifiedEmptyHandoff === undefined &&
4228
+ !handoff.adopted &&
4229
+ deferredCodexForegroundBinding === undefined &&
4230
+ claimedSession === undefined
4231
+ ? captureCodexCandidateSetRolloutAcceptanceAnchor({
4232
+ inventory: sourceLessCodexCandidateInventory,
4233
+ now: cliNow()
4234
+ })
4235
+ : undefined;
4236
+ const postSendCodexDetachedSessionClaims = postSendCodexCandidateAnchor &&
4237
+ sourceLessCodexCandidateInventory
4238
+ ? captureDetachedCodexCandidateSessionClaims({
4239
+ storeDir,
4240
+ terminal,
4241
+ inventory: sourceLessCodexCandidateInventory,
4242
+ anchor: postSendCodexCandidateAnchor
4243
+ })
4244
+ : undefined;
4245
+ assertAtomicForegroundAcceptanceBoundary({
4246
+ enabled: atomicForegroundIdentification,
4247
+ deferred: deferredCodexForegroundBinding,
4248
+ postSend: postSendCodexCandidateAnchor
4249
+ });
4250
+ const freshSendAuthority = decideTerminalSendAuthority({
4251
+ ownership: "conflict",
4252
+ verifiedEmpty: Boolean(verifiedEmptyHandoff),
4253
+ externalHandoff: handoff.adopted,
4254
+ deferred: Boolean(deferredCodexForegroundBinding)
4255
+ });
4256
+ if (freshSendAuthority.mode === "deferred" &&
4257
+ deferredCodexForegroundBinding) {
4258
+ claimedSession = undefined;
4259
+ knownCodexCompanions = { additional: [] };
4260
+ currentNativeIdentity = undefined;
4261
+ handoff = { identity: undefined, adopted: false };
4262
+ }
4263
+ else if ((userExplicitTerminalToken ||
4264
+ suppliedExpectedTerminalToken ||
4265
+ implicitCodexCandidateAuthority) &&
4266
+ !postSendCodexCandidateAnchor &&
4267
+ freshSendAuthority.mode === "conflict") {
4268
+ throw new Error("managed continuation authority is unavailable for the current " +
4269
+ "terminal context" +
4270
+ (userExplicitTerminalToken
4271
+ ? "; the physical terminal authority remains valid"
4272
+ : "; refresh AKK list before retrying managed delivery"));
4273
+ }
4274
+ return {
4275
+ nativeIdentityObservation,
4276
+ knownCodexCompanions,
4277
+ currentNativeIdentity,
4278
+ verifiedEmptyHandoff,
4279
+ handoff,
4280
+ deferredCodexForegroundBinding,
4281
+ postSendCodexCandidateAnchor,
4282
+ postSendCodexDetachedSessionClaims
4283
+ };
4284
+ }
3846
4285
  async function runManagedRawTerminalSendAttempt(options, messageBody, terminalConversation, deferZeroInputFailurePresentation, attempt) {
3847
4286
  // A token copied from list is authority for exactly the advertised full
3848
4287
  // terminal selector. Reject aliases and implicit/no-selector resolution
@@ -3856,7 +4295,33 @@ async function runManagedRawTerminalSendAttempt(options, messageBody, terminalCo
3856
4295
  }
3857
4296
  const rawStoreDir = storeDirFromOptions(options);
3858
4297
  let controlSendResult;
3859
- await withCanonicalMutationLocks(terminalWriterMutationLocks(rawStoreDir, terminalConversation.terminalControl, deferZeroInputFailurePresentation ? { timeoutMs: 0 } : undefined), async (scopes, resources) => {
4298
+ const atomicForegroundIdentification = options.identifyForeground === true;
4299
+ foregroundIdentificationProofs.delete(options);
4300
+ const userExplicitLockOptions = deferZeroInputFailurePresentation
4301
+ ? {
4302
+ terminalTimeoutMs: 0,
4303
+ storeWriterTimeoutMs: USER_EXPLICIT_MANAGED_LOCK_GRACE_MS
4304
+ }
4305
+ : undefined;
4306
+ const lockOptions = atomicForegroundIdentification
4307
+ ? {
4308
+ ...userExplicitLockOptions,
4309
+ afterTerminalAcquired: async () => {
4310
+ const expectedTerminalToken = required(stringValue(options.expectedUserExplicitTerminalToken), "atomic foreground identification requires fresh physical terminal authority");
4311
+ const proof = await identifyCodexForegroundWhileLocked({
4312
+ options,
4313
+ terminal: terminalConversation,
4314
+ expectedTerminalToken
4315
+ });
4316
+ if (proof.terminalId !== terminalConversation.conversationId ||
4317
+ proof.pid !== terminalConversation.pid) {
4318
+ throw new Error("foreground identification returned evidence for a different terminal");
4319
+ }
4320
+ foregroundIdentificationProofs.set(options, proof);
4321
+ }
4322
+ }
4323
+ : userExplicitLockOptions;
4324
+ await withCanonicalMutationLocks(terminalWriterMutationLocks(rawStoreDir, terminalConversation.terminalControl, lockOptions), async (scopes, resources) => {
3860
4325
  await assertFreshUserExplicitTerminalSendTargetWhileLocked(options, terminalConversation);
3861
4326
  await mutationDispatchLedger.beforeMutation(scopes, resources, options, terminalConversation);
3862
4327
  // Upgrade legacy Stores before resolving their Session authority. The
@@ -3868,129 +4333,24 @@ async function runManagedRawTerminalSendAttempt(options, messageBody, terminalCo
3868
4333
  terminalControl: terminalConversation.terminalControl,
3869
4334
  requestText: String(messageBody),
3870
4335
  expectedStoreDir: rawStoreDir,
3871
- expectedMessageType: "task"
4336
+ expectedMessageType: "task",
4337
+ ...(stringValue(options.expectedUserExplicitTerminalToken)
4338
+ ? { userExplicitTerminalId: terminalConversation.conversationId }
4339
+ : {})
3872
4340
  })) {
3873
4341
  controlSendResult = { outcome: "replayed" };
3874
4342
  attempt.result = controlSendResult;
3875
4343
  return;
3876
4344
  }
3877
4345
  terminalListCliFacade.assertTerminalIncarnationCanStartTurn(rawStoreDir, terminalConversation.terminalControl);
3878
- const initialAuthority = rawTerminalInitialAuthority({
3879
- options,
3880
- terminal: terminalConversation,
3881
- storeDir: rawStoreDir
3882
- });
3883
- let { claimedSession, knownCodexCompanions } = initialAuthority;
3884
- const { suppliedExpectedTerminalToken, implicitCodexCandidateAuthority } = initialAuthority;
3885
- const nativeIdentityObservation = await observeCurrentNativeAgentSessionIdentity({
3886
- options,
3887
- agent: terminalConversation.agent,
3888
- pid: terminalConversation.pid,
3889
- cwd: terminalConversation.terminalControl.currentPath,
3890
- preferredSessionId: knownCodexCompanions.primary
3891
- ? claimedSession?.binding?.native_thread_id
3892
- : undefined,
3893
- allowedCompanionIdentity: knownCodexCompanions.primary,
3894
- allowedAdditionalIdentities: knownCodexCompanions.additional
3895
- });
3896
- let deferredCodexCandidateInventory;
3897
- if (terminalConversation.agent === "codex" &&
3898
- (suppliedExpectedTerminalToken || implicitCodexCandidateAuthority)) {
3899
- try {
3900
- const inventory = await inspectCodexOpenRootRolloutInventory({
3901
- options,
3902
- pid: terminalConversation.pid,
3903
- cwd: terminalConversation.terminalControl.currentPath
3904
- });
3905
- if (inventory.roots.length > 0) {
3906
- deferredCodexCandidateInventory = inventory;
3907
- }
3908
- }
3909
- catch (error) {
3910
- if (implicitCodexCandidateAuthority ||
3911
- nativeIdentityObservation.status === "unavailable") {
3912
- throw new Error(`native Codex foreground attribution requires a fresh complete ` +
3913
- `open-root inventory: ${error instanceof Error ? error.message : String(error)}`);
3914
- }
3915
- }
3916
- }
3917
- assertRawTerminalCandidateAuthority({
3918
- terminal: terminalConversation,
3919
- nativeIdentityObservation,
3920
- deferredCodexCandidateInventory,
3921
- implicitCodexCandidateAuthority
3922
- });
3923
- let currentNativeIdentity = nativeIdentityObservation.status === "resolved"
3924
- ? nativeIdentityObservation.identity
3925
- : undefined;
3926
- // A fresh nonempty inventory is the stronger physical authority for an
3927
- // implicit candidate send. Do not let an earlier verified-absent
3928
- // observation divert this path into the token-only empty handoff.
3929
- const verifiedEmptyHandoff = implicitCodexCandidateAuthority
3930
- ? undefined
3931
- : await maybeDetachVerifiedEmptyCodexSource({
3932
- options,
3933
- terminal: terminalConversation,
3934
- sourceSession: claimedSession,
3935
- observation: nativeIdentityObservation
3936
- });
3937
- if (verifiedEmptyHandoff) {
3938
- // The old rollout is conclusively closed. Never carry it forward as
3939
- // a pre-materialization companion for the new virgin Session.
3940
- claimedSession = undefined;
3941
- knownCodexCompanions = { additional: [] };
3942
- currentNativeIdentity = undefined;
3943
- }
3944
- const physicalNativeIdentityBeforeHandoff = currentNativeIdentity;
3945
- let handoff = await maybeAdoptObservedExternalThread({
4346
+ const { nativeIdentityObservation, knownCodexCompanions, currentNativeIdentity, verifiedEmptyHandoff, handoff, deferredCodexForegroundBinding, postSendCodexCandidateAnchor, postSendCodexDetachedSessionClaims } = await prepareRawTerminalDispatchAuthority({
3946
4347
  options,
4348
+ messageBody,
3947
4349
  terminal: terminalConversation,
3948
- // A no-token raw send to an already managed rollout-backed Codex pane
3949
- // is an internal follow-current delegation, never a sole-root strict
3950
- // continuation or an external-handoff adoption. The dedicated v3
3951
- // transfer below owns attribution and lineage.
3952
- sourceSession: implicitCodexCandidateAuthority
3953
- ? undefined
3954
- : claimedSession,
3955
- resolvedIdentity: currentNativeIdentity,
3956
- storeDir: rawStoreDir
3957
- });
3958
- currentNativeIdentity =
3959
- handoff.adopted &&
3960
- terminalConversation.agent === "codex" &&
3961
- !handoff.session?.binding?.native_process.rollout
3962
- ? physicalNativeIdentityBeforeHandoff
3963
- : handoff.identity;
3964
- const deferredCodexForegroundBinding = !handoff.adopted
3965
- ? await prepareDeferredCodexForegroundBinding({
3966
- options,
3967
- scope: bindDeferredForegroundWriterScope(scopes, resources),
3968
- terminal: terminalConversation,
3969
- sourceSession: claimedSession,
3970
- observation: nativeIdentityObservation,
3971
- candidateInventory: deferredCodexCandidateInventory,
3972
- requestText: String(messageBody),
3973
- allowImplicitFreshAuthority: implicitCodexCandidateAuthority
3974
- })
3975
- : undefined;
3976
- const freshSendAuthority = decideTerminalSendAuthority({
3977
- ownership: "conflict",
3978
- verifiedEmpty: Boolean(verifiedEmptyHandoff),
3979
- externalHandoff: handoff.adopted,
3980
- deferred: Boolean(deferredCodexForegroundBinding)
4350
+ storeDir: rawStoreDir,
4351
+ scopes,
4352
+ resources
3981
4353
  });
3982
- if (freshSendAuthority.mode === "deferred" &&
3983
- deferredCodexForegroundBinding) {
3984
- claimedSession = undefined;
3985
- knownCodexCompanions = { additional: [] };
3986
- currentNativeIdentity = undefined;
3987
- handoff = { identity: undefined, adopted: false };
3988
- }
3989
- else if ((suppliedExpectedTerminalToken || implicitCodexCandidateAuthority) &&
3990
- freshSendAuthority.mode === "conflict") {
3991
- throw new Error("the expected terminal token no longer authorizes the current " +
3992
- "terminal context; refresh AKK list");
3993
- }
3994
4354
  let managedSession = deferredCodexForegroundBinding
3995
4355
  ? undefined
3996
4356
  : handoff.session ??
@@ -4068,7 +4428,7 @@ async function runManagedRawTerminalSendAttempt(options, messageBody, terminalCo
4068
4428
  const materializedNativeIdentity = currentNativeIdentity?.sessionId === logicalNativeIdentity?.sessionId
4069
4429
  ? currentNativeIdentity
4070
4430
  : undefined;
4071
- if (!(handoff.adopted &&
4431
+ if (options.identifyForeground !== true && !(handoff.adopted &&
4072
4432
  terminalConversation.agent === "codex" &&
4073
4433
  !managedSession.binding?.native_process.rollout)) {
4074
4434
  await verifyCodexPendingManagedSendStatus({
@@ -4197,10 +4557,14 @@ async function runManagedRawTerminalSendAttempt(options, messageBody, terminalCo
4197
4557
  }
4198
4558
  : undefined,
4199
4559
  verifiedEmptyCodexHandoff: verifiedEmptyHandoff?.boundary,
4560
+ postSendCodexCandidateAnchor,
4561
+ postSendCodexDetachedSessionClaims,
4200
4562
  deferredCodexForegroundBinding
4201
4563
  });
4202
4564
  attempt.result = controlSendResult;
4203
- }, deferZeroInputFailurePresentation ? { timeoutMs: 0 } : undefined);
4565
+ }, deferZeroInputFailurePresentation
4566
+ ? { timeoutMs: USER_EXPLICIT_MANAGED_LOCK_GRACE_MS }
4567
+ : undefined);
4204
4568
  });
4205
4569
  if (!controlSendResult) {
4206
4570
  throw new Error("managed terminal Send completed without an outcome");
@@ -5438,8 +5802,121 @@ async function runTerminalConversationApprove({ options, terminal }) {
5438
5802
  releaseTerminalLock();
5439
5803
  }
5440
5804
  }
5805
+ function terminalSendCandidateAcceptanceAnchor(request) {
5806
+ return request.deferredCodexForegroundBinding?.candidateAcceptanceAnchor ??
5807
+ request.postSendCodexCandidateAnchor;
5808
+ }
5809
+ function assertTerminalNativeBindingBeforeSend(input) {
5810
+ if (!input.needsPostSendNativeBinding) {
5811
+ input.execution.assertTurnIdentity({
5812
+ conversation: input.conversation,
5813
+ currentIdentity: input.currentNativeIdentity,
5814
+ operation: "send to"
5815
+ });
5816
+ return;
5817
+ }
5818
+ if (input.currentNativeIdentity &&
5819
+ !nativeIdentityMatchesCodexPreMaterialization(input.currentNativeIdentity, input.allowedPreMaterializationIdentity)) {
5820
+ throw new Error("native agent session appeared while preparing an unmaterialized terminal binding; refresh list and retry");
5821
+ }
5822
+ }
5823
+ function terminalPreSendRuntime(input) {
5824
+ const { request, terminalControl, terminalAgentPid } = input;
5825
+ const { conversation, nextConversation, executor, message, allowedPreMaterializationIdentity, allowedAdditionalIdentities = [], deferredCodexForegroundBinding } = request;
5826
+ const candidateAnchor = terminalSendCandidateAcceptanceAnchor(request);
5827
+ return {
5828
+ ...(candidateAnchor
5829
+ ? terminalRuntimeForLiveIdentity({
5830
+ terminal: deferredCodexForegroundBinding?.terminal ?? {
5831
+ conversationId: conversation.conversation_id,
5832
+ agent: executor.kind,
5833
+ pid: terminalAgentPid,
5834
+ terminalControl
5835
+ },
5836
+ physicalOnly: true
5837
+ })
5838
+ : terminalRuntimeIdentityForConversation(nextConversation, terminalControl)),
5839
+ allowedPreMaterializationNativeIdentity: allowedPreMaterializationIdentity,
5840
+ allowedAdditionalNativeIdentities: allowedAdditionalIdentities,
5841
+ messageId: message.id
5842
+ };
5843
+ }
5844
+ function assertTerminalPreSendStatus(input) {
5845
+ const { request, status } = input;
5846
+ if (stringValue(request.options.expectedUserExplicitTerminalToken)) {
5847
+ // Managed promotion is only a sidecar for a human-priority Send. It may
5848
+ // relax lifecycle/activity attribution, never the shared physical gate
5849
+ // that proves this TUI is not an approval or questionnaire input mode.
5850
+ assertSafeUserExplicitTerminalSend(status);
5851
+ }
5852
+ const deferredCodexPrompt = Boolean(request.verifiedEmptyCodexHandoff ||
5853
+ request.deferredCodexForegroundBinding ||
5854
+ request.postSendCodexCandidateAnchor);
5855
+ if (!deferredCodexPrompt) {
5856
+ assertSafeTerminalSend(request.executor.kind, status);
5857
+ return;
5858
+ }
5859
+ if (request.executor.kind !== "codex" ||
5860
+ status.reachable !== true ||
5861
+ status.approval_state.blocked === true ||
5862
+ status.interaction_state !== undefined ||
5863
+ !["idle", "unknown"].includes(status.activity_state)) {
5864
+ throw new Error(`Codex deferred foreground send is not at a safe prompt ` +
5865
+ `(${status.activity_state}: ${status.activity_reason})`);
5866
+ }
5867
+ }
5868
+ function assertAtomicForegroundIdentificationProof(input) {
5869
+ if (input.options.identifyForeground !== true)
5870
+ return;
5871
+ if (input.executor.kind !== "codex") {
5872
+ throw new Error("atomic foreground identification currently supports only Codex terminals");
5873
+ }
5874
+ const proof = foregroundIdentificationProofs.get(input.options);
5875
+ if (!proof) {
5876
+ throw new Error("atomic foreground identification proof is unavailable; no task input was sent");
5877
+ }
5878
+ const expiresAtMs = Date.parse(proof.expiresAt);
5879
+ if (!Number.isFinite(expiresAtMs) || cliNowMs() >= expiresAtMs) {
5880
+ throw new Error("atomic foreground identification proof expired before task dispatch; no task input was sent");
5881
+ }
5882
+ const processIncarnation = processIncarnationForPid(input.terminalAgentPid);
5883
+ if (proof.pid !== input.terminalAgentPid ||
5884
+ proof.processUuid !== processIncarnation.processUuid ||
5885
+ proof.processBirth !== processIncarnation.processBirth) {
5886
+ throw new Error("Codex process incarnation changed after foreground identification; no task input was sent");
5887
+ }
5888
+ const cwd = stringValue(input.terminalControl.currentPath);
5889
+ let realCwd;
5890
+ try {
5891
+ realCwd = cwd ? fs.realpathSync(cwd) : undefined;
5892
+ }
5893
+ catch {
5894
+ realCwd = undefined;
5895
+ }
5896
+ if (!realCwd || realCwd !== proof.realCwd) {
5897
+ throw new Error("Codex terminal cwd changed after foreground identification; no task input was sent");
5898
+ }
5899
+ if (!stringValue(input.status.screen.digest) ||
5900
+ input.status.screen.digest !== proof.postProbeScreenDigest) {
5901
+ throw new Error("Codex screen generation changed after foreground identification; no task input was sent");
5902
+ }
5903
+ const observation = createRuntimeTerminalAgentRegistry(input.options)
5904
+ .require("codex")
5905
+ .observeNativeInspection?.({
5906
+ operation: { kind: "status" },
5907
+ screen: input.status.screen.excerpt ?? "",
5908
+ expectedNativeThreadId: proof.nativeThreadId,
5909
+ expectedAgentVersion: proof.agentVersion,
5910
+ expectedCwd: proof.realCwd
5911
+ });
5912
+ if (observation?.status !== "observed" ||
5913
+ observation.nativeThreadId !== proof.nativeThreadId ||
5914
+ observation.evidenceFingerprint !== proof.evidenceFingerprint) {
5915
+ throw new Error("Codex /status identity changed after foreground identification; no task input was sent");
5916
+ }
5917
+ }
5441
5918
  async function prepareTerminalControlSend(request) {
5442
- const { transaction, options, conversation, nextConversation, executor, message, recordRawAttachmentAfterSend = false, allowedPreMaterializationIdentity, allowedAdditionalIdentities = [], verifiedEmptyCodexHandoff, deferredCodexForegroundBinding, continuingTurnResponse = false } = request;
5919
+ const { transaction, options, conversation, executor, message, recordRawAttachmentAfterSend = false, allowedPreMaterializationIdentity, allowedAdditionalIdentities = [], deferredCodexForegroundBinding, continuingTurnResponse = false } = request;
5443
5920
  const bridge = terminalBridgeEnabled(conversation);
5444
5921
  const route = bindTerminalDispatchRoute(transaction.scopes, transaction.resources);
5445
5922
  const { terminalControl, storeDir: lockedStoreDir, statePath, logPath } = route;
@@ -5490,8 +5967,8 @@ async function prepareTerminalControlSend(request) {
5490
5967
  : undefined;
5491
5968
  const terminalAgentPid = Number(sendTakeover?.terminal_agent_pid);
5492
5969
  const expectedManagedNativeThreadId = stringValue(sendTakeover?.terminal_agent_expected_session_id) ?? stringValue(sendTakeover?.terminal_agent_session_id);
5493
- const currentNativeIdentity = deferredCodexForegroundBinding
5494
- ?.candidateAcceptanceAnchor
5970
+ const candidateSetAnchor = terminalSendCandidateAcceptanceAnchor(request);
5971
+ const currentNativeIdentity = candidateSetAnchor
5495
5972
  ? undefined
5496
5973
  : await execution.resolveCurrentNativeIdentity({
5497
5974
  agent: executor.kind,
@@ -5508,30 +5985,18 @@ async function prepareTerminalControlSend(request) {
5508
5985
  const pendingManagedNativeBinding = Boolean(!stringValue(sendTakeover?.terminal_agent_session_id) &&
5509
5986
  stringValue(sendTakeover?.terminal_agent_expected_session_id));
5510
5987
  const needsPostSendNativeBinding = virginRawAttach || pendingManagedNativeBinding;
5511
- if (needsPostSendNativeBinding) {
5512
- if (currentNativeIdentity &&
5513
- !nativeIdentityMatchesCodexPreMaterialization(currentNativeIdentity, allowedPreMaterializationIdentity)) {
5514
- throw new Error("native agent session appeared while preparing an unmaterialized terminal binding; refresh list and retry");
5515
- }
5516
- }
5517
- else {
5518
- execution.assertTurnIdentity({
5519
- conversation,
5520
- currentIdentity: currentNativeIdentity,
5521
- operation: "send to"
5522
- });
5523
- }
5524
- const preSendRuntime = {
5525
- ...(deferredCodexForegroundBinding?.candidateAcceptanceAnchor
5526
- ? terminalRuntimeForLiveIdentity({
5527
- terminal: deferredCodexForegroundBinding.terminal,
5528
- physicalOnly: true
5529
- })
5530
- : terminalRuntimeIdentityForConversation(nextConversation, terminalControl)),
5531
- allowedPreMaterializationNativeIdentity: allowedPreMaterializationIdentity,
5532
- allowedAdditionalNativeIdentities: allowedAdditionalIdentities,
5533
- messageId: message.id
5534
- };
5988
+ assertTerminalNativeBindingBeforeSend({
5989
+ execution,
5990
+ conversation,
5991
+ currentNativeIdentity,
5992
+ needsPostSendNativeBinding,
5993
+ allowedPreMaterializationIdentity
5994
+ });
5995
+ const preSendRuntime = terminalPreSendRuntime({
5996
+ request,
5997
+ terminalControl,
5998
+ terminalAgentPid
5999
+ });
5535
6000
  let preSendScreenFingerprint;
5536
6001
  let codexRolloutAcceptanceAnchor;
5537
6002
  let claudeTranscriptAnchor;
@@ -5540,21 +6005,20 @@ async function prepareTerminalControlSend(request) {
5540
6005
  : undefined;
5541
6006
  try {
5542
6007
  const status = await terminalBridge.status(executor.kind, terminalControl, {
5543
- scrollbackLines: Number(options.scrollbackLines ?? 120),
6008
+ scrollbackLines: options.identifyForeground === true
6009
+ ? foregroundIdentificationProofs.get(options)
6010
+ ?.observationScrollbackLines ?? 240
6011
+ : Number(options.scrollbackLines ?? 120),
5544
6012
  runtime: preSendRuntime
5545
6013
  });
5546
- if (verifiedEmptyCodexHandoff || deferredCodexForegroundBinding) {
5547
- if (executor.kind !== "codex" ||
5548
- status.reachable !== true ||
5549
- status.approval_state.blocked === true ||
5550
- !["idle", "unknown"].includes(status.activity_state)) {
5551
- throw new Error(`Codex deferred foreground send is not at a safe prompt ` +
5552
- `(${status.activity_state}: ${status.activity_reason})`);
5553
- }
5554
- }
5555
- else {
5556
- assertSafeTerminalSend(executor.kind, status);
5557
- }
6014
+ assertAtomicForegroundIdentificationProof({
6015
+ options,
6016
+ executor,
6017
+ terminalControl,
6018
+ terminalAgentPid,
6019
+ status
6020
+ });
6021
+ assertTerminalPreSendStatus({ request, status });
5558
6022
  const userExplicitManagedCodexAttempt = Boolean(stringValue(options.expectedUserExplicitTerminalToken));
5559
6023
  if (executor.kind === "claude" &&
5560
6024
  userExplicitManagedCodexAttempt &&
@@ -5596,7 +6060,7 @@ async function prepareTerminalControlSend(request) {
5596
6060
  boundProcessBirth: stringValue(sendTakeover?.terminal_agent_process_birth),
5597
6061
  allowedPreMaterializationIdentity,
5598
6062
  needsPostSendNativeBinding,
5599
- candidateSetAnchor: deferredCodexForegroundBinding?.candidateAcceptanceAnchor
6063
+ candidateSetAnchor
5600
6064
  });
5601
6065
  }
5602
6066
  else {
@@ -5655,8 +6119,7 @@ async function resolveTerminalDispatchSubmissionOwner(prepared, request, applica
5655
6119
  expectedSessionId: stringValue(sendTakeover?.terminal_agent_expected_session_id),
5656
6120
  allowedPreMaterializationIdentity,
5657
6121
  allowedAdditionalIdentities,
5658
- ...(deferredBinding &&
5659
- acceptanceAnchor &&
6122
+ ...(acceptanceAnchor &&
5660
6123
  [2, 3].includes(acceptanceAnchor.version)
5661
6124
  ? {
5662
6125
  requiredCodexAcceptance: {
@@ -5711,15 +6174,26 @@ async function resolveTerminalDispatchSubmissionOwner(prepared, request, applica
5711
6174
  });
5712
6175
  }
5713
6176
  else {
5714
- await assertNativeThreadHasExclusiveOwnership({
5715
- options,
5716
- agent: executor.kind,
5717
- currentPid: terminalAgentPid,
5718
- nativeThreadId: boundIdentity.sessionId,
5719
- storeDir: identityRoute.storeDir,
5720
- terminalControl,
5721
- excludedManagedSessionId: sessionIdForConversation(boundConversation)
5722
- });
6177
+ if (executor.kind === "codex") {
6178
+ await rawPort("prepareManagedSessionNativeIdentityClaim")({
6179
+ options,
6180
+ conversation: boundConversation,
6181
+ identity: boundIdentity,
6182
+ storeDir: identityRoute.storeDir,
6183
+ terminalControl
6184
+ });
6185
+ }
6186
+ else {
6187
+ await assertNativeThreadHasExclusiveOwnership({
6188
+ options,
6189
+ agent: executor.kind,
6190
+ currentPid: terminalAgentPid,
6191
+ nativeThreadId: boundIdentity.sessionId,
6192
+ storeDir: identityRoute.storeDir,
6193
+ terminalControl,
6194
+ excludedManagedSessionId: sessionIdForConversation(boundConversation)
6195
+ });
6196
+ }
5723
6197
  persistManagedSessionNativeIdentity({
5724
6198
  conversation: boundConversation,
5725
6199
  terminalControl,
@@ -5727,7 +6201,8 @@ async function resolveTerminalDispatchSubmissionOwner(prepared, request, applica
5727
6201
  storeDir: identityRoute.storeDir
5728
6202
  });
5729
6203
  }
5730
- if (acceptanceAnchor?.version === 2 &&
6204
+ if (acceptanceAnchor &&
6205
+ [2, 3].includes(acceptanceAnchor.version) &&
5731
6206
  cliEnv().AKK_TEST_EXIT_AFTER_VIRGIN_SESSION_BINDING === "1") {
5732
6207
  cliExit(86);
5733
6208
  }
@@ -5787,8 +6262,7 @@ async function resolveTerminalDispatchSubmissionOwner(prepared, request, applica
5787
6262
  if (boundIdentity && boundConversation) {
5788
6263
  return ready(boundConversation);
5789
6264
  }
5790
- if (deferredBinding &&
5791
- acceptanceAnchor?.version === 3 &&
6265
+ if (acceptanceAnchor?.version === 3 &&
5792
6266
  bindingError === undefined) {
5793
6267
  return ready(stagedConversation, true);
5794
6268
  }
@@ -5889,7 +6363,7 @@ function terminalDispatchRollbackRepositories({ request, prepared, deferredForeg
5889
6363
  return { rollbackBeforeInput, restoreDeferred };
5890
6364
  }
5891
6365
  function createTerminalDispatchRuntime(request, prepared, rollback) {
5892
- const { transaction, conversation, nextConversation, executor, message, recordRawAttachmentAfterSend = false } = request;
6366
+ const { transaction, conversation, nextConversation, executor, message, recordRawAttachmentAfterSend = false, postSendCodexDetachedSessionClaims } = request;
5893
6367
  const { bridge, terminalControl, lockedStoreDir, statePath, logPath, bridgeStartedAt, submissionPreparedAt, agentTimeoutMinutes, agentHardTimeoutMinutes, terminalPayload, terminalRequestHash, previousDispatchLedger, preSendScreenFingerprint, codexRolloutAcceptanceAnchor, claudeTranscriptAnchor, claudeHome } = prepared;
5894
6368
  const terminalMessage = message;
5895
6369
  const bridgeConversation = bridge
@@ -5903,6 +6377,7 @@ function createTerminalDispatchRuntime(request, prepared, rollback) {
5903
6377
  monitorLockVersion: monitorOwner.LOCK_VERSION,
5904
6378
  preSendScreenFingerprint,
5905
6379
  codexRolloutAcceptanceAnchor,
6380
+ codexDetachedCandidateSessionClaims: postSendCodexDetachedSessionClaims,
5906
6381
  claudeTranscriptAnchor,
5907
6382
  claudeHome
5908
6383
  })
@@ -6059,16 +6534,38 @@ function terminalDispatchTransportLifecycle({ request, prepared, application, de
6059
6534
  : {}),
6060
6535
  recordStage: (stage, at, afterDurable) => application.recordTransportStage(stage, at, afterDurable)
6061
6536
  });
6537
+ const lifecycleBeforeText = lifecycle.beforeText;
6538
+ const guardedLifecycle = options.identifyForeground === true
6539
+ ? {
6540
+ ...lifecycle,
6541
+ beforeText: async () => {
6542
+ await lifecycleBeforeText?.();
6543
+ const proof = foregroundIdentificationProofs.get(options);
6544
+ const status = await prepared.terminalBridge.status(request.executor.kind, prepared.terminalControl, {
6545
+ runtime: prepared.preSendRuntime,
6546
+ scrollbackLines: proof?.observationScrollbackLines ??
6547
+ CODEX_FOREGROUND_IDENTIFICATION_SCROLLBACK_LINES
6548
+ });
6549
+ assertAtomicForegroundIdentificationProof({
6550
+ options,
6551
+ executor: request.executor,
6552
+ terminalControl: prepared.terminalControl,
6553
+ terminalAgentPid: prepared.terminalAgentPid,
6554
+ status
6555
+ });
6556
+ }
6557
+ }
6558
+ : lifecycle;
6062
6559
  const userExplicitTerminalSend = Boolean(stringValue(options.expectedUserExplicitTerminalToken));
6063
6560
  return userExplicitTerminalSend
6064
6561
  ? {
6065
- ...lifecycle,
6562
+ ...guardedLifecycle,
6066
6563
  requireExactEmptyComposerBeforeText: true,
6067
6564
  ...(request.executor.kind === "codex"
6068
6565
  ? { userExplicitEnterAfterTextWithoutComposerVeto: true }
6069
6566
  : {})
6070
6567
  }
6071
- : lifecycle;
6568
+ : guardedLifecycle;
6072
6569
  }
6073
6570
  async function terminalDispatchAcceptance({ request, prepared, conversation }) {
6074
6571
  const { options, executor } = request;
@@ -6222,7 +6719,8 @@ async function runTerminalDispatchTransport({ request, prepared, application, pr
6222
6719
  if (!progress.enterDispatchedAt) {
6223
6720
  throw new Error("terminal bridge returned without an enter_dispatched receipt");
6224
6721
  }
6225
- if (codexRolloutAcceptanceAnchor?.version === 2 &&
6722
+ if (codexRolloutAcceptanceAnchor &&
6723
+ [2, 3].includes(codexRolloutAcceptanceAnchor.version) &&
6226
6724
  cliEnv().AKK_TEST_EXIT_AFTER_VIRGIN_ENTER_DISPATCHED === "1") {
6227
6725
  cliExit(86);
6228
6726
  }