@scotthuang/agent-knock-knock 0.13.16 → 0.13.18

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 (184) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +4 -4
  3. package/dist/src/claude-terminal-agent-adapter.js +1 -1
  4. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  5. package/dist/src/codex-app-server-interaction-client.js +68 -26
  6. package/dist/src/codex-app-server-interaction-client.js.map +1 -1
  7. package/dist/src/codex-app-server-read-client.js +52 -20
  8. package/dist/src/codex-app-server-read-client.js.map +1 -1
  9. package/dist/src/codex-astra-composer-proof.js +1 -1
  10. package/dist/src/codex-astra-composer-proof.js.map +1 -1
  11. package/dist/src/codex-async-question-adapter.js +14 -4
  12. package/dist/src/codex-async-question-adapter.js.map +1 -1
  13. package/dist/src/codex-lifecycle-compatibility.d.ts +1 -1
  14. package/dist/src/codex-lifecycle-compatibility.js +9 -3
  15. package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
  16. package/dist/src/codex-open-rollout-inventory.d.ts +51 -0
  17. package/dist/src/codex-open-rollout-inventory.js +365 -0
  18. package/dist/src/codex-open-rollout-inventory.js.map +1 -0
  19. package/dist/src/codex-paginated-thread-binding.js +1 -1
  20. package/dist/src/codex-paginated-thread-binding.js.map +1 -1
  21. package/dist/src/codex-permission-surface.d.ts +1 -1
  22. package/dist/src/codex-permission-surface.js +1 -1
  23. package/dist/src/codex-permission-surface.js.map +1 -1
  24. package/dist/src/codex-rollout-bound-completion.d.ts +44 -0
  25. package/dist/src/codex-rollout-bound-completion.js +357 -0
  26. package/dist/src/codex-rollout-bound-completion.js.map +1 -0
  27. package/dist/src/codex-rollout-file.d.ts +15 -0
  28. package/dist/src/codex-rollout-file.js +98 -0
  29. package/dist/src/codex-rollout-file.js.map +1 -0
  30. package/dist/src/codex-rollout-human-task.d.ts +61 -0
  31. package/dist/src/codex-rollout-human-task.js +555 -0
  32. package/dist/src/codex-rollout-human-task.js.map +1 -0
  33. package/dist/src/codex-rollout-interaction-evidence.d.ts +48 -0
  34. package/dist/src/codex-rollout-interaction-evidence.js +521 -0
  35. package/dist/src/codex-rollout-interaction-evidence.js.map +1 -0
  36. package/dist/src/codex-rollout-inventory.d.ts +5 -0
  37. package/dist/src/codex-rollout-inventory.js +81 -0
  38. package/dist/src/codex-rollout-inventory.js.map +1 -0
  39. package/dist/src/codex-rollout-records.d.ts +49 -0
  40. package/dist/src/codex-rollout-records.js +298 -0
  41. package/dist/src/codex-rollout-records.js.map +1 -0
  42. package/dist/src/codex-sqlite-thread-query.d.ts +85 -0
  43. package/dist/src/codex-sqlite-thread-query.js +440 -0
  44. package/dist/src/codex-sqlite-thread-query.js.map +1 -0
  45. package/dist/src/codex-status-history-navigation.d.ts +1 -1
  46. package/dist/src/codex-status-history-navigation.js +1 -1
  47. package/dist/src/codex-status-history-navigation.js.map +1 -1
  48. package/dist/src/codex-store-adapter.d.ts +3 -50
  49. package/dist/src/codex-store-adapter.js +5 -796
  50. package/dist/src/codex-store-adapter.js.map +1 -1
  51. package/dist/src/codex-terminal-agent-adapter.js +4 -2
  52. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  53. package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +4 -134
  54. package/dist/src/native-thread-lifecycle-cli-adapter.js +19 -633
  55. package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
  56. package/dist/src/native-thread-lifecycle-cli-contract.d.ts +138 -0
  57. package/dist/src/native-thread-lifecycle-cli-contract.js +2 -0
  58. package/dist/src/native-thread-lifecycle-cli-contract.js.map +1 -0
  59. package/dist/src/semantic-permission-control.js +33 -6
  60. package/dist/src/semantic-permission-control.js.map +1 -1
  61. package/dist/src/semantic-tool-descriptions.d.ts +26 -0
  62. package/dist/src/semantic-tool-descriptions.js +259 -0
  63. package/dist/src/semantic-tool-descriptions.js.map +1 -0
  64. package/dist/src/semantic-tool-runtime.js +45 -37
  65. package/dist/src/semantic-tool-runtime.js.map +1 -1
  66. package/dist/src/semantic-tool-schemas.js +134 -33
  67. package/dist/src/semantic-tool-schemas.js.map +1 -1
  68. package/dist/src/terminal-action-contracts.d.ts +4 -0
  69. package/dist/src/terminal-action-contracts.js +695 -0
  70. package/dist/src/terminal-action-contracts.js.map +1 -0
  71. package/dist/src/terminal-composer-classifier.d.ts +1 -1
  72. package/dist/src/terminal-composer-classifier.js +2 -2
  73. package/dist/src/terminal-composer-classifier.js.map +1 -1
  74. package/dist/src/terminal-interaction-response-bridge.js +1 -1
  75. package/dist/src/terminal-interaction-response-bridge.js.map +1 -1
  76. package/dist/src/terminal-list-renderer.d.ts +1 -2
  77. package/dist/src/terminal-list-renderer.js +8 -424
  78. package/dist/src/terminal-list-renderer.js.map +1 -1
  79. package/dist/src/terminal-model-control-action-policy.d.ts +1 -1
  80. package/dist/src/terminal-model-control-availability.d.ts +1 -1
  81. package/dist/src/terminal-model-control-availability.js +1 -1
  82. package/dist/src/terminal-model-control-availability.js.map +1 -1
  83. package/dist/src/terminal-model-control-cli-adapter.d.ts +34 -0
  84. package/dist/src/terminal-model-control-cli-adapter.js +669 -0
  85. package/dist/src/terminal-model-control-cli-adapter.js.map +1 -0
  86. package/dist/src/terminal-model-control-contract.d.ts +198 -0
  87. package/dist/src/terminal-model-control-contract.js +15 -0
  88. package/dist/src/terminal-model-control-contract.js.map +1 -0
  89. package/dist/src/terminal-model-control-input.d.ts +29 -0
  90. package/dist/src/terminal-model-control-input.js +86 -0
  91. package/dist/src/terminal-model-control-input.js.map +1 -0
  92. package/dist/src/terminal-model-control-profile.d.ts +5 -1
  93. package/dist/src/terminal-model-control-profile.js +21 -7
  94. package/dist/src/terminal-model-control-profile.js.map +1 -1
  95. package/dist/src/terminal-model-control-subject.d.ts +52 -0
  96. package/dist/src/terminal-model-control-subject.js +73 -0
  97. package/dist/src/terminal-model-control-subject.js.map +1 -1
  98. package/dist/src/terminal-model-control-surface.d.ts +82 -0
  99. package/dist/src/terminal-model-control-surface.js +688 -0
  100. package/dist/src/terminal-model-control-surface.js.map +1 -0
  101. package/dist/src/terminal-model-control.d.ts +6 -270
  102. package/dist/src/terminal-model-control.js +11 -988
  103. package/dist/src/terminal-model-control.js.map +1 -1
  104. package/dist/src/terminal-model-selection.d.ts +33 -0
  105. package/dist/src/terminal-model-selection.js +143 -0
  106. package/dist/src/terminal-model-selection.js.map +1 -0
  107. package/dist/src/terminal-monitor-application-service.js +38 -59
  108. package/dist/src/terminal-monitor-application-service.js.map +1 -1
  109. package/dist/src/terminal-monitor-attention-store.d.ts +65 -0
  110. package/dist/src/terminal-monitor-attention-store.js +518 -0
  111. package/dist/src/terminal-monitor-attention-store.js.map +1 -0
  112. package/dist/src/terminal-monitor-collateral-recovery.d.ts +12 -0
  113. package/dist/src/terminal-monitor-collateral-recovery.js +415 -0
  114. package/dist/src/terminal-monitor-collateral-recovery.js.map +1 -0
  115. package/dist/src/terminal-monitor-interaction-projection.d.ts +19 -0
  116. package/dist/src/terminal-monitor-interaction-projection.js +48 -0
  117. package/dist/src/terminal-monitor-interaction-projection.js.map +1 -0
  118. package/dist/src/terminal-monitor-startup-recovery.d.ts +15 -0
  119. package/dist/src/terminal-monitor-startup-recovery.js +767 -0
  120. package/dist/src/terminal-monitor-startup-recovery.js.map +1 -0
  121. package/dist/src/terminal-monitor-state-cli-adapter.d.ts +2 -104
  122. package/dist/src/terminal-monitor-state-cli-adapter.js +28 -1640
  123. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  124. package/dist/src/terminal-monitor-state-contract.d.ts +108 -0
  125. package/dist/src/terminal-monitor-state-contract.js +8 -0
  126. package/dist/src/terminal-monitor-state-contract.js.map +1 -0
  127. package/dist/src/terminal-native-inspection-bridge.d.ts +4 -153
  128. package/dist/src/terminal-native-inspection-bridge.js +7 -646
  129. package/dist/src/terminal-native-inspection-bridge.js.map +1 -1
  130. package/dist/src/terminal-native-inspection-contract.d.ts +128 -0
  131. package/dist/src/terminal-native-inspection-contract.js +38 -0
  132. package/dist/src/terminal-native-inspection-contract.js.map +1 -0
  133. package/dist/src/terminal-native-inspection-profile.d.ts +15 -0
  134. package/dist/src/terminal-native-inspection-profile.js +189 -0
  135. package/dist/src/terminal-native-inspection-profile.js.map +1 -0
  136. package/dist/src/terminal-native-inspection-proof.d.ts +62 -0
  137. package/dist/src/terminal-native-inspection-proof.js +362 -0
  138. package/dist/src/terminal-native-inspection-proof.js.map +1 -0
  139. package/dist/src/terminal-permission-control-bridge.js +1 -1
  140. package/dist/src/terminal-permission-control-bridge.js.map +1 -1
  141. package/dist/src/terminal-permission-control.js +5 -1
  142. package/dist/src/terminal-permission-control.js.map +1 -1
  143. package/dist/src/terminal-questionnaire-adapter.js +5 -2
  144. package/dist/src/terminal-questionnaire-adapter.js.map +1 -1
  145. package/dist/src/terminal-runtime-cli-adapter.d.ts +2 -1
  146. package/dist/src/terminal-runtime-cli-adapter.js +1 -1
  147. package/dist/src/terminal-runtime-cli-adapter.js.map +1 -1
  148. package/dist/src/terminal-submission-acceptance.d.ts +5 -148
  149. package/dist/src/terminal-submission-acceptance.js +21 -1845
  150. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  151. package/dist/src/terminal-watch-anchor-capture.d.ts +9 -0
  152. package/dist/src/terminal-watch-anchor-capture.js +139 -0
  153. package/dist/src/terminal-watch-anchor-capture.js.map +1 -0
  154. package/dist/src/terminal-watch-cli-adapter.d.ts +3 -117
  155. package/dist/src/terminal-watch-cli-adapter.js +10 -1992
  156. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  157. package/dist/src/terminal-watch-cli-contract.d.ts +117 -0
  158. package/dist/src/terminal-watch-cli-contract.js +2 -0
  159. package/dist/src/terminal-watch-cli-contract.js.map +1 -0
  160. package/dist/src/terminal-watch-fallback-observation.d.ts +15 -0
  161. package/dist/src/terminal-watch-fallback-observation.js +249 -0
  162. package/dist/src/terminal-watch-fallback-observation.js.map +1 -0
  163. package/dist/src/terminal-watch-interaction-observation.d.ts +26 -0
  164. package/dist/src/terminal-watch-interaction-observation.js +262 -0
  165. package/dist/src/terminal-watch-interaction-observation.js.map +1 -0
  166. package/dist/src/terminal-watch-observation-adapter.d.ts +4 -0
  167. package/dist/src/terminal-watch-observation-adapter.js +307 -0
  168. package/dist/src/terminal-watch-observation-adapter.js.map +1 -0
  169. package/dist/src/terminal-watch-observation-evidence.d.ts +25 -0
  170. package/dist/src/terminal-watch-observation-evidence.js +184 -0
  171. package/dist/src/terminal-watch-observation-evidence.js.map +1 -0
  172. package/dist/src/terminal-watch-response-authority.d.ts +6 -0
  173. package/dist/src/terminal-watch-response-authority.js +333 -0
  174. package/dist/src/terminal-watch-response-authority.js.map +1 -0
  175. package/dist/src/terminal-watch-response-cli-adapter.d.ts +3 -0
  176. package/dist/src/terminal-watch-response-cli-adapter.js +230 -0
  177. package/dist/src/terminal-watch-response-cli-adapter.js.map +1 -0
  178. package/dist/src/terminal-watch-terminal-identity.d.ts +39 -0
  179. package/dist/src/terminal-watch-terminal-identity.js +380 -0
  180. package/dist/src/terminal-watch-terminal-identity.js.map +1 -0
  181. package/docs/codex-0.160.0-compatibility.md +126 -0
  182. package/docs/codex-permissions.md +12 -1
  183. package/package.json +3 -2
  184. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +3 -3
@@ -1,33 +1,27 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
1
  import path from "node:path";
3
- import { validateCodexRolloutAcceptanceAnchor } from "./terminal-submission-acceptance.js";
4
- import { deferredForegroundActiveEnterDispatchedAt, deferredForegroundActiveMessageId, deferredForegroundActivePreparedAt, deferredForegroundActiveTextInjectedAt, isDeferredForegroundSubmissionRetryPending, listDeferredForegroundTransfers, loadDeferredForegroundTransfer, saveDeferredForegroundTransfer } from "./deferred-foreground-transfer.js";
2
+ import { loadDeferredForegroundTransfer } from "./deferred-foreground-transfer.js";
5
3
  import { isFinalDeferredForegroundTransferStatus } from "./deferred-foreground-transfer-policy.js";
6
- import { executorForConversation, createMessage, effectiveTurnStatus, isExplicitUserAbandonedManagementTurn, isSessionSendBlockingStatus, isTerminalDispatchOwnerReleasedStatus, isWaitingForAgentStatus, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
7
- import { executorDefinitionForKind } from "./executors.js";
4
+ import { executorForConversation, createMessage, isWaitingForAgentStatus, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
8
5
  import { retireAbsentAsyncQuestionNotification } from "./terminal-monitor-interaction-store.js";
9
6
  import { redactString } from "./runtime-log.js";
10
- import { appendEvent, listConversations, loadState, logPathForStatePath, pathsForConversationDir, saveState, statePathForConversationId, withStoreWriterLease, withStoreWriterLeaseAsync } from "./store.js";
11
- import { readNdjsonLog } from "./transcript.js";
7
+ import { appendEvent, loadState, logPathForStatePath, pathsForConversationDir, saveState, statePathForConversationId, withStoreWriterLease, withStoreWriterLeaseAsync } from "./store.js";
12
8
  import { createFileLockCliAdapter } from "./file-lock-cli-adapter.js";
13
9
  import { cliEnv, cliExit, cliNow, cliNowMs, cliPid, cliRuntimeLog, cliSleepSync } from "./cli-runtime-context.js";
14
10
  import { expandHome } from "./cli-command-runtime.js";
15
11
  import { terminalControlFromTakeover } from "./terminal-runtime-cli-adapter.js";
16
12
  import { terminalControlsShareIncarnation } from "./terminal-authority-policy.js";
17
- import { terminalControlEvidenceMatches } from "./terminal-control-ref.js";
18
- import { applyTerminalBridgeSubmission, terminalAcceptanceEvidenceForConversation, terminalBridgeEnabled, terminalBridgeRequestFingerprint, terminalBridgeSubmission } from "./terminal-dispatch-receipt.js";
19
- import { terminalMonitorActivityPersistIntervalMs, terminalMonitorApprovalCandidate, terminalMonitorDeadlineAt, validTerminalMonitorTimestampMs } from "./terminal-monitor-decision-policy.js";
13
+ import { applyTerminalBridgeSubmission, terminalBridgeEnabled, terminalBridgeRequestFingerprint, terminalBridgeSubmission } from "./terminal-dispatch-receipt.js";
14
+ import { terminalMonitorActivityPersistIntervalMs, terminalMonitorApprovalCandidate, validTerminalMonitorTimestampMs } from "./terminal-monitor-decision-policy.js";
20
15
  import { runTerminalMonitor as runTerminalMonitorService } from "./terminal-monitor-application-service.js";
21
- import { pollTerminalMonitor, presentTerminalMonitor, reconcileMonitorAcceptance, recordMonitorApprovalNotification, recordMonitorInteractionNotification, validMonitorInteractionProjection as validInteractionProjection, repairLaggingAcceptedMonitorAuthority, recoverPreparedMonitorSubmission, terminalMonitorStoreLeaseTimeout, terminalMonitorStoreOperationTimeout } from "./terminal-monitor-cli-adapter.js";
16
+ import { pollTerminalMonitor, presentTerminalMonitor, reconcileMonitorAcceptance, recordMonitorApprovalNotification, recordMonitorInteractionNotification, repairLaggingAcceptedMonitorAuthority, recoverPreparedMonitorSubmission, terminalMonitorStoreLeaseTimeout, terminalMonitorStoreOperationTimeout } from "./terminal-monitor-cli-adapter.js";
22
17
  import { terminalMonitorReconciliationEligibility } from "./terminal-monitor-reconciliation-eligibility.js";
23
18
  import { reconcileTerminalMonitorStateCandidate } from "./terminal-monitor-state-reconciliation-service.js";
24
- import { sameCanonicalStatePath } from "./terminal-dispatch-ledger-codec.js";
25
- import { terminalSubmissionPayload } from "./terminal-dispatch-execution.js";
26
- import { loadTerminalSubmissionRetry, decideTerminalSubmissionRetryStartup, projectTerminalSubmissionRetryPending, saveTerminalSubmissionRetry, terminalSubmissionRetryLedgerFields } from "./terminal-submission-retry-service.js";
27
- import { callbackExpectedForConversation, callbackRouteFingerprintForConversation } from "./callback-route-authority.js";
28
- import { canonicalJson } from "./canonical-json.js";
19
+ import { callbackExpectedForConversation } from "./callback-route-authority.js";
29
20
  import { isRecord, nonBlankString } from "./value-guards.js";
30
- const COLLATERAL_STALL_REASON = "a newer terminal submission has an uncertain outcome; inspect the shared terminal pane before continuing";
21
+ import { takeoverFor } from "./terminal-monitor-state-contract.js";
22
+ import { TerminalMonitorStartupRecovery } from "./terminal-monitor-startup-recovery.js";
23
+ import { TerminalMonitorCollateralRecovery } from "./terminal-monitor-collateral-recovery.js";
24
+ import { TerminalMonitorAttentionStore, terminalBridgeApprovalInstructions } from "./terminal-monitor-attention-store.js";
31
25
  /** Invocation-local monitor state and reconciliation CLI boundary. */
32
26
  export function createTerminalMonitorStateCliAdapter(dependencies) {
33
27
  const application = new TerminalMonitorStateCliApplication(dependencies);
@@ -43,6 +37,9 @@ export function createTerminalMonitorStateCliAdapter(dependencies) {
43
37
  });
44
38
  }
45
39
  class TerminalMonitorStateCliApplication {
40
+ #startup;
41
+ #collateral;
42
+ #attention;
46
43
  #dependencies;
47
44
  #stateFileLock = createFileLockCliAdapter({
48
45
  now: cliNow,
@@ -52,6 +49,9 @@ class TerminalMonitorStateCliApplication {
52
49
  });
53
50
  constructor(dependencies) {
54
51
  this.#dependencies = dependencies;
52
+ this.#startup = new TerminalMonitorStartupRecovery(dependencies, this.#stateFileLock);
53
+ this.#collateral = new TerminalMonitorCollateralRecovery(dependencies, this.#stateFileLock);
54
+ this.#attention = new TerminalMonitorAttentionStore(dependencies, this.#stateFileLock);
55
55
  }
56
56
  async runService(input) {
57
57
  await runTerminalMonitorService({
@@ -181,8 +181,8 @@ class TerminalMonitorStateCliApplication {
181
181
  logPath: paths.logPath,
182
182
  options
183
183
  }),
184
- recoverSubmissionRetry: (storeDir, conversation, paths) => this.#recoverSubmissionRetry(storeDir, conversation, paths),
185
- recoverDeferred: (storeDir, conversation, paths) => this.#recoverDeferred(options, storeDir, conversation, paths),
184
+ recoverSubmissionRetry: (storeDir, conversation, paths) => this.#startup.recoverSubmissionRetry(storeDir, conversation, paths),
185
+ recoverDeferred: (storeDir, conversation, paths) => this.#startup.recoverDeferred(options, storeDir, conversation, paths),
186
186
  recoverVirgin: async (conversation, paths) => (await this.#dependencies.acceptance.recoverVirgin({
187
187
  options,
188
188
  conversation,
@@ -202,311 +202,6 @@ class TerminalMonitorStateCliApplication {
202
202
  }
203
203
  };
204
204
  }
205
- async #recoverSubmissionRetry(storeDir, initialConversation, paths) {
206
- const initialAttempt = loadTerminalSubmissionRetry(paths.statePath);
207
- if (!initialAttempt) {
208
- return initialConversation;
209
- }
210
- const initialTakeover = takeoverFor(initialConversation);
211
- const initialControl = terminalControlFromTakeover(initialTakeover);
212
- if (executorForConversation(initialConversation).kind !== "codex" ||
213
- !initialControl) {
214
- throw new Error("submission retry startup recovery lost its exact Codex terminal authority");
215
- }
216
- const canonical = pathsForConversationDir(path.dirname(paths.statePath));
217
- if (path.resolve(canonical.storeDir) !== path.resolve(storeDir) ||
218
- path.resolve(canonical.statePath) !== path.resolve(paths.statePath) ||
219
- path.resolve(canonical.logPath) !== path.resolve(paths.logPath)) {
220
- throw new Error("submission retry startup recovery is outside its canonical Turn Store");
221
- }
222
- const releaseTerminal = this.#dependencies.dispatch.repository.acquire(storeDir, initialControl, { timeoutMs: 30000 });
223
- try {
224
- return await withStoreWriterLeaseAsync(storeDir, async () => {
225
- const releaseState = this.#stateFileLock.acquire(`${paths.statePath}.lock`);
226
- try {
227
- let conversation = loadState(paths.statePath);
228
- const takeover = takeoverFor(conversation);
229
- const control = terminalControlFromTakeover(takeover);
230
- const attempt = loadTerminalSubmissionRetry(paths.statePath);
231
- if (!attempt) {
232
- return conversation;
233
- }
234
- if (executorForConversation(conversation).kind !== "codex" ||
235
- !control ||
236
- !terminalControlsShareIncarnation(control, initialControl)) {
237
- throw new Error("submission retry startup recovery terminal authority changed");
238
- }
239
- const callbackDelivery = isRecord(conversation.callback_delivery)
240
- ? conversation.callback_delivery
241
- : undefined;
242
- if (callbackDelivery?.status === "delivered" ||
243
- isTerminalDispatchOwnerReleasedStatus(effectiveTurnStatus(conversation))) {
244
- return conversation;
245
- }
246
- const submission = terminalBridgeSubmission(conversation);
247
- const ledger = this.#dependencies.dispatch.repository.load(control);
248
- if (!submission || !ledger) {
249
- throw new Error("submission retry startup recovery lacks its Turn or ledger receipt");
250
- }
251
- if (!["agent_accepted"].includes(String(submission.status)) &&
252
- !["agent_accepted"].includes(String(ledger.status)) &&
253
- !["enter_dispatched", "agent_accepted"].includes(attempt.state)) {
254
- return conversation;
255
- }
256
- const authority = assertTerminalSubmissionRetryStartupAuthority({
257
- storeDir,
258
- paths,
259
- conversation,
260
- takeover,
261
- submission,
262
- ledger,
263
- attempt,
264
- control,
265
- ledgerMatchesControl: this.#dependencies.dispatch.repository
266
- .matchesControl(ledger, control)
267
- });
268
- const startup = decideTerminalSubmissionRetryStartup({
269
- attempt,
270
- submission,
271
- ledger
272
- });
273
- if (startup.action === "refuse") {
274
- throw new Error(startup.reason);
275
- }
276
- if (startup.action === "finalize_accepted") {
277
- return finalizeTerminalSubmissionRetryStartupAccepted({
278
- storeDir,
279
- paths,
280
- conversation,
281
- submission,
282
- ledger,
283
- attempt,
284
- control,
285
- authority,
286
- saveLedger: (next) => this.#dependencies.dispatch.repository
287
- .save(control, next)
288
- });
289
- }
290
- if (startup.action === "repair_terminal_ledger") {
291
- reconcileTerminalSubmissionRetryStartupTerminalLedger({
292
- paths,
293
- conversation,
294
- ledger,
295
- attempt,
296
- startup,
297
- saveLedger: (next) => this.#dependencies.dispatch.repository
298
- .save(control, next)
299
- });
300
- return conversation;
301
- }
302
- if (startup.action === "repair_terminal_state") {
303
- return reconcileTerminalSubmissionRetryStartupTerminalState({
304
- storeDir,
305
- paths,
306
- conversation,
307
- submission,
308
- attempt,
309
- control,
310
- authority,
311
- startup
312
- });
313
- }
314
- if (startup.action === "no_change")
315
- return conversation;
316
- const projection = authority.projection;
317
- if (!projection) {
318
- return conversation;
319
- }
320
- const receiptAlreadyPending = submission.status === "enter_dispatched";
321
- if (receiptAlreadyPending
322
- ? conversation.status !== "waiting_for_agent"
323
- : !["stalled", "waiting_for_agent"].includes(String(conversation.status))) {
324
- return conversation;
325
- }
326
- let repairedTransfer = false;
327
- if (authority.deferredTransferId) {
328
- repairedTransfer = mirrorDeferredSubmissionRetryEnter({
329
- storeDir,
330
- transferId: authority.deferredTransferId,
331
- attempt,
332
- messageId: projection.messageId,
333
- preparedAt: projection.preparedAt,
334
- textInjectedAt: projection.textInjectedAt,
335
- requestHash: authority.requestHash,
336
- sessionId: sessionIdForConversation(conversation),
337
- turnId: turnIdForConversation(conversation),
338
- statePath: paths.statePath
339
- });
340
- }
341
- const stateNeedsRepair = !receiptAlreadyPending;
342
- if (receiptAlreadyPending && !terminalSubmissionRetryStateIsPending({
343
- conversation,
344
- submission,
345
- projection
346
- })) {
347
- return conversation;
348
- }
349
- if (stateNeedsRepair) {
350
- const epochConversation = terminalSubmissionRetryMonitorEpoch(conversation, projection.enterDispatchedAt);
351
- conversation = applyTerminalBridgeSubmission({
352
- conversation: epochConversation,
353
- messageId: projection.messageId,
354
- messageType: authority.messageType,
355
- requestText: authority.requestText,
356
- status: "enter_dispatched",
357
- preparedAt: projection.preparedAt,
358
- textInjectedAt: projection.textInjectedAt,
359
- enterDispatchedAt: projection.enterDispatchedAt,
360
- lastProvenStage: "enter_dispatched"
361
- }, {
362
- dispatcherPid: cliPid(),
363
- storeDir,
364
- terminalControl: control
365
- });
366
- saveState(paths.statePath, conversation);
367
- }
368
- const ledgerNeedsRepair = ledger.status !== "enter_dispatched" ||
369
- nonBlankString(ledger.text_injected_at) !==
370
- projection.textInjectedAt ||
371
- nonBlankString(ledger.enter_dispatched_at) !==
372
- projection.enterDispatchedAt ||
373
- ledger.submission_retry_state !== "enter_dispatched" ||
374
- ledger.submission_retry_revision !== attempt.revision;
375
- if (ledgerNeedsRepair) {
376
- this.#dependencies.dispatch.repository.save(control, {
377
- ...ledger,
378
- ...terminalSubmissionRetryLedgerFields(attempt),
379
- status: "enter_dispatched",
380
- text_injected_at: projection.textInjectedAt,
381
- enter_dispatched_at: projection.enterDispatchedAt,
382
- enter_not_attempted_at: undefined,
383
- enter_not_attempted_reason: undefined,
384
- uncertain_at: undefined,
385
- safe_to_retry: undefined,
386
- acceptance_evidence: undefined,
387
- agent_accepted_at: undefined,
388
- not_accepted_at: undefined,
389
- dispatcher_pid: null
390
- });
391
- }
392
- if (repairedTransfer || stateNeedsRepair || ledgerNeedsRepair) {
393
- appendEvent(paths.logPath, {
394
- ts: projection.enterDispatchedAt,
395
- conversation_id: conversation.conversation_id,
396
- event: "terminal_submission_retry_startup_reconciled",
397
- message_id: projection.messageId,
398
- terminal_input_sent: false
399
- });
400
- }
401
- return loadState(paths.statePath);
402
- }
403
- finally {
404
- releaseState();
405
- }
406
- });
407
- }
408
- finally {
409
- releaseTerminal();
410
- }
411
- }
412
- async #recoverDeferred(options, storeDir, initialConversation, paths) {
413
- if (isExplicitUserAbandonedManagementTurn(initialConversation)) {
414
- return initialConversation;
415
- }
416
- const takeover = takeoverFor(initialConversation);
417
- const transferId = nonBlankString(takeover?.deferred_foreground_transfer_id);
418
- if (!transferId) {
419
- return initialConversation;
420
- }
421
- const initialControl = terminalControlFromTakeover(takeover);
422
- const initialPid = Number(takeover?.terminal_agent_pid);
423
- if (!initialControl ||
424
- !Number.isSafeInteger(initialPid) ||
425
- initialPid <= 1) {
426
- throw new Error(`deferred foreground Turn ${initialConversation.conversation_id} ` +
427
- "lost its exact terminal process authority");
428
- }
429
- const releaseTerminal = this.#dependencies.dispatch.repository.acquire(storeDir, initialControl, { timeoutMs: 30000 });
430
- try {
431
- return await withStoreWriterLeaseAsync(storeDir, async () => {
432
- const releaseState = this.#stateFileLock.acquire(`${paths.statePath}.lock`);
433
- let conversation;
434
- try {
435
- conversation = loadState(paths.statePath);
436
- if (isExplicitUserAbandonedManagementTurn(conversation)) {
437
- return conversation;
438
- }
439
- }
440
- finally {
441
- releaseState();
442
- }
443
- const currentTakeover = takeoverFor(conversation);
444
- const currentTransferId = nonBlankString(currentTakeover.deferred_foreground_transfer_id);
445
- const currentControl = terminalControlFromTakeover(currentTakeover);
446
- const currentPid = Number(currentTakeover.terminal_agent_pid);
447
- if (currentTransferId !== transferId ||
448
- !currentControl ||
449
- !terminalControlsShareIncarnation(currentControl, initialControl) ||
450
- !Number.isSafeInteger(currentPid) ||
451
- currentPid !== initialPid) {
452
- throw new Error(`deferred foreground Turn ${conversation.conversation_id} ` +
453
- "changed its exact terminal process authority during recovery");
454
- }
455
- const transfer = loadDeferredForegroundTransfer(storeDir, transferId);
456
- if (isFinalDeferredForegroundTransferStatus(transfer.status)) {
457
- return conversation;
458
- }
459
- const terminal = await this.#dependencies.authority
460
- .createBridge(options)
461
- .resolveStoredTerminal("codex", currentPid, currentControl, { pid: currentPid });
462
- await this.#dependencies.authority.handoff
463
- .recoverDeferredCodexForegroundTransferBeforeMutation({
464
- options,
465
- terminal: terminal
466
- });
467
- const releaseFinalState = this.#stateFileLock.acquire(`${paths.statePath}.lock`);
468
- try {
469
- conversation = loadState(paths.statePath);
470
- this.#assertExactPendingDeferred(conversation, storeDir, transferId);
471
- return conversation;
472
- }
473
- finally {
474
- releaseFinalState();
475
- }
476
- });
477
- }
478
- finally {
479
- releaseTerminal();
480
- }
481
- }
482
- #assertExactPendingDeferred(conversation, storeDir, transferId) {
483
- const transfer = loadDeferredForegroundTransfer(storeDir, transferId);
484
- if (isFinalDeferredForegroundTransferStatus(transfer.status)) {
485
- return;
486
- }
487
- const takeover = takeoverFor(conversation);
488
- const submission = terminalBridgeSubmission(conversation);
489
- const anchor = validateCodexRolloutAcceptanceAnchor(takeover?.codex_rollout_acceptance_anchor);
490
- const activeMessageId = deferredForegroundActiveMessageId(transfer);
491
- const exactRetryPending = isDeferredForegroundSubmissionRetryPending(transfer) &&
492
- activeMessageId === transfer.submission_retry_message_id &&
493
- nonBlankString(submission?.prepared_at) ===
494
- deferredForegroundActivePreparedAt(transfer) &&
495
- nonBlankString(submission?.text_injected_at) ===
496
- deferredForegroundActiveTextInjectedAt(transfer) &&
497
- nonBlankString(submission?.enter_dispatched_at) ===
498
- deferredForegroundActiveEnterDispatchedAt(transfer);
499
- if (anchor?.version !== 3 ||
500
- conversation.status !== "waiting_for_agent" ||
501
- submission?.status !== "enter_dispatched" ||
502
- nonBlankString(submission.message_id) !==
503
- nonBlankString(takeover?.terminal_bridge_message_id) ||
504
- nonBlankString(submission.message_id) !== activeMessageId ||
505
- !(transfer.status === "dispatch_started" || exactRetryPending)) {
506
- throw new Error(`deferred foreground Turn ${conversation.conversation_id} ` +
507
- "is not an exact pending candidate-set acceptance");
508
- }
509
- }
510
205
  prepareLaunch(input) {
511
206
  const storeDir = pathsForConversationDir(path.dirname(input.statePath))
512
207
  .storeDir;
@@ -608,7 +303,7 @@ class TerminalMonitorStateCliApplication {
608
303
  recordApprovalNotification: (request) => recordMonitorApprovalNotification({
609
304
  ...request,
610
305
  ports: {
611
- record: (recordRequest) => this.#recordApprovalNotification({
306
+ record: (recordRequest) => this.#attention.recordApprovalNotification({
612
307
  ...recordRequest,
613
308
  statePath: input.statePath,
614
309
  logPath: input.logPath
@@ -626,7 +321,7 @@ class TerminalMonitorStateCliApplication {
626
321
  recordInteractionNotification: (request) => recordMonitorInteractionNotification({
627
322
  ...request,
628
323
  ports: {
629
- record: (recordRequest) => this.#recordInteractionNotification({
324
+ record: (recordRequest) => this.#attention.recordInteractionNotification({
630
325
  ...recordRequest,
631
326
  statePath: input.statePath,
632
327
  logPath: input.logPath
@@ -687,7 +382,7 @@ class TerminalMonitorStateCliApplication {
687
382
  now: cliNow,
688
383
  appendEvent: (event) => appendEvent(input.logPath, event),
689
384
  stallCollateral: (stallRequest) => {
690
- this.#stallOtherConversations({
385
+ this.#collateral.stallOther({
691
386
  storeDir: stateStoreDir(),
692
387
  ...stallRequest
693
388
  });
@@ -758,787 +453,6 @@ class TerminalMonitorStateCliApplication {
758
453
  }
759
454
  };
760
455
  }
761
- #stallOtherConversations(input) {
762
- const stalledConversationIds = [];
763
- for (const listed of listConversations(input.storeDir)) {
764
- if (listed.conversation_id === input.currentConversationId ||
765
- !isSessionSendBlockingStatus(listed.status)) {
766
- continue;
767
- }
768
- const listedTakeover = takeoverFor(listed);
769
- if (listedTakeover?.terminal_bridge !== true ||
770
- !terminalControlsShareIncarnation(terminalControlFromTakeover(listedTakeover), input.terminalControl)) {
771
- continue;
772
- }
773
- const statePath = nonBlankString(listed.state_path);
774
- if (!statePath) {
775
- continue;
776
- }
777
- const release = this.#stateFileLock.acquire(`${statePath}.lock`);
778
- try {
779
- const current = loadState(statePath);
780
- const currentTakeover = takeoverFor(current);
781
- if (!isSessionSendBlockingStatus(current.status) ||
782
- currentTakeover?.terminal_bridge !== true ||
783
- !terminalControlsShareIncarnation(terminalControlFromTakeover(currentTakeover), input.terminalControl)) {
784
- continue;
785
- }
786
- const stalledAt = cliNow().toISOString();
787
- const stalledConversation = {
788
- ...current,
789
- status: "stalled",
790
- stalled_at: stalledAt,
791
- stalled_reason: COLLATERAL_STALL_REASON,
792
- native_session_takeover: {
793
- ...currentTakeover,
794
- terminal_bridge_uncertain_dispatch_fence: {
795
- message_id: input.uncertainMessageId,
796
- observed_at: stalledAt,
797
- previous_status: current.status
798
- }
799
- },
800
- updated_at: stalledAt
801
- };
802
- saveState(statePath, stalledConversation);
803
- try {
804
- appendEvent(logPathForStatePath(statePath), {
805
- ts: stalledAt,
806
- conversation_id: current.conversation_id,
807
- event: "terminal_bridge_stalled_by_uncertain_dispatch",
808
- terminal_control: input.terminalControl,
809
- uncertain_message_id: input.uncertainMessageId
810
- });
811
- }
812
- catch {
813
- // State plus the terminal ledger are the authoritative fence.
814
- }
815
- stalledConversationIds.push(current.conversation_id);
816
- }
817
- finally {
818
- release();
819
- }
820
- }
821
- return stalledConversationIds;
822
- }
823
- stallOther(input) {
824
- return this.#stallOtherConversations(input);
825
- }
826
- #exactCollateralRepairEvidence(conversation, storeDir) {
827
- if (conversation.status !== "stalled" ||
828
- conversation.stalled_reason !== COLLATERAL_STALL_REASON) {
829
- return undefined;
830
- }
831
- const takeover = takeoverFor(conversation);
832
- const fence = isRecord(takeover?.terminal_bridge_uncertain_dispatch_fence)
833
- ? takeover.terminal_bridge_uncertain_dispatch_fence
834
- : undefined;
835
- const uncertainMessageId = nonBlankString(fence?.message_id);
836
- const fenceObservedAt = nonBlankString(fence?.observed_at);
837
- const previousStatus = nonBlankString(fence?.previous_status);
838
- const ownMessageId = nonBlankString(takeover?.terminal_bridge_message_id);
839
- const ownSubmission = terminalBridgeSubmission(conversation);
840
- const completionClaim = isRecord(takeover?.terminal_bridge_completion_claim)
841
- ? takeover.terminal_bridge_completion_claim
842
- : undefined;
843
- const callbackDelivery = isRecord(conversation.callback_delivery)
844
- ? conversation.callback_delivery
845
- : undefined;
846
- const callbackMessage = isRecord(callbackDelivery?.message)
847
- ? callbackDelivery.message
848
- : undefined;
849
- const idleSince = nonBlankString(conversation.idle_since);
850
- const deliveredAt = nonBlankString(callbackDelivery?.delivered_at);
851
- const claimedAt = nonBlankString(completionClaim?.claimed_at);
852
- const fenceAtMs = validTerminalMonitorTimestampMs(fenceObservedAt);
853
- const idleAtMs = validTerminalMonitorTimestampMs(idleSince);
854
- const deliveredAtMs = validTerminalMonitorTimestampMs(deliveredAt);
855
- const claimedAtMs = validTerminalMonitorTimestampMs(claimedAt);
856
- if (takeover?.terminal_bridge !== true ||
857
- !uncertainMessageId ||
858
- !fenceObservedAt ||
859
- fenceAtMs === undefined ||
860
- uncertainMessageId === ownMessageId ||
861
- (previousStatus !== undefined && previousStatus !== "idle") ||
862
- conversation.stalled_at !== fenceObservedAt ||
863
- conversation.updated_at !== fenceObservedAt ||
864
- !idleSince ||
865
- idleAtMs === undefined ||
866
- idleAtMs > fenceAtMs ||
867
- !ownMessageId ||
868
- ownSubmission?.status !== "agent_accepted" ||
869
- nonBlankString(ownSubmission.message_id) !== ownMessageId ||
870
- nonBlankString(ownSubmission.session_id) !==
871
- sessionIdForConversation(conversation) ||
872
- nonBlankString(ownSubmission.turn_id) !== turnIdForConversation(conversation) ||
873
- !completionClaim ||
874
- nonBlankString(completionClaim.terminal_bridge_message_id) !== ownMessageId ||
875
- completionClaim.outcome !== "success" ||
876
- !claimedAt ||
877
- claimedAtMs === undefined ||
878
- claimedAtMs > fenceAtMs ||
879
- callbackDelivery?.status !== "delivered" ||
880
- callbackDelivery.final_status !== "idle" ||
881
- callbackDelivery.preserve_conversation_status !== true ||
882
- !callbackMessage ||
883
- callbackMessage.type !== "done" ||
884
- callbackMessage.requires_response !== false ||
885
- nonBlankString(callbackMessage.id) !==
886
- nonBlankString(completionClaim.callback_message_id) ||
887
- nonBlankString(callbackMessage.conversation_id) !==
888
- conversation.conversation_id ||
889
- nonBlankString(callbackMessage.session_id) !==
890
- sessionIdForConversation(conversation) ||
891
- nonBlankString(callbackMessage.turn_id) !== turnIdForConversation(conversation) ||
892
- nonBlankString(isRecord(callbackMessage.metadata)
893
- ? callbackMessage.metadata.terminal_bridge_message_id
894
- : undefined) !== ownMessageId ||
895
- !deliveredAt ||
896
- deliveredAtMs === undefined ||
897
- deliveredAtMs > fenceAtMs) {
898
- return undefined;
899
- }
900
- const control = this.#dependencies.authority
901
- .terminalControlForConversation(conversation);
902
- if (!control) {
903
- return undefined;
904
- }
905
- const ownerCandidates = listConversations(storeDir).filter((candidate) => {
906
- if (candidate.conversation_id === conversation.conversation_id) {
907
- return false;
908
- }
909
- const candidateTakeover = takeoverFor(candidate);
910
- const candidateSubmission = terminalBridgeSubmission(candidate);
911
- return candidateTakeover?.terminal_bridge === true &&
912
- nonBlankString(candidateTakeover.terminal_bridge_message_id) ===
913
- uncertainMessageId &&
914
- nonBlankString(candidateSubmission?.message_id) === uncertainMessageId &&
915
- terminalControlsShareIncarnation(this.#dependencies.authority.terminalControlForConversation(candidate), control);
916
- });
917
- if (ownerCandidates.length !== 1) {
918
- return undefined;
919
- }
920
- return this.#verifyCollateralOwner({
921
- conversation,
922
- takeover,
923
- ownMessageId,
924
- completionClaim,
925
- callbackMessage,
926
- callbackMessageId: nonBlankString(callbackMessage.id),
927
- deliveredAt,
928
- claimedAt,
929
- fenceObservedAt,
930
- fenceAtMs,
931
- uncertainMessageId,
932
- control,
933
- ownerListed: ownerCandidates[0]
934
- });
935
- }
936
- #verifyCollateralOwner(input) {
937
- const ownerStatePath = nonBlankString(input.ownerListed.state_path);
938
- if (!ownerStatePath) {
939
- return undefined;
940
- }
941
- let owner;
942
- let ownerEvents;
943
- let events;
944
- try {
945
- owner = loadState(ownerStatePath);
946
- ownerEvents = readNdjsonLog(nonBlankString(owner.event_log_path) ??
947
- logPathForStatePath(ownerStatePath));
948
- const statePath = nonBlankString(input.conversation.state_path);
949
- if (!statePath) {
950
- return undefined;
951
- }
952
- events = readNdjsonLog(nonBlankString(input.conversation.event_log_path) ??
953
- logPathForStatePath(statePath));
954
- }
955
- catch {
956
- return undefined;
957
- }
958
- const ownerTakeover = takeoverFor(owner);
959
- const ownerSubmission = terminalBridgeSubmission(owner);
960
- const ownerClosedAt = nonBlankString(owner.closed_at);
961
- const ownerClosedAtMs = validTerminalMonitorTimestampMs(ownerClosedAt);
962
- if (owner.conversation_id !== input.ownerListed.conversation_id ||
963
- owner.status !== "closed" ||
964
- !ownerClosedAt ||
965
- ownerClosedAtMs === undefined ||
966
- ownerClosedAtMs < input.fenceAtMs ||
967
- !nonBlankString(owner.close_reason) ||
968
- owner.updated_at !== ownerClosedAt ||
969
- ownerTakeover?.terminal_bridge !== true ||
970
- nonBlankString(ownerTakeover.terminal_bridge_message_id) !==
971
- input.uncertainMessageId ||
972
- ownerSubmission?.status !== "uncertain" ||
973
- nonBlankString(ownerSubmission.message_id) !== input.uncertainMessageId ||
974
- nonBlankString(ownerSubmission.session_id) !== sessionIdForConversation(owner) ||
975
- nonBlankString(ownerSubmission.turn_id) !== turnIdForConversation(owner) ||
976
- isRecord(ownerTakeover.terminal_bridge_uncertain_dispatch_fence) ||
977
- !terminalControlsShareIncarnation(this.#dependencies.authority.terminalControlForConversation(owner), input.control)) {
978
- return undefined;
979
- }
980
- if (!this.#hasExactCollateralEvents(input, owner, ownerClosedAt, events, ownerEvents)) {
981
- return undefined;
982
- }
983
- return {
984
- uncertainMessageId: input.uncertainMessageId,
985
- ownerConversationId: owner.conversation_id,
986
- restoredStatus: "idle"
987
- };
988
- }
989
- #hasExactCollateralEvents(input, owner, ownerClosedAt, events, ownerEvents) {
990
- const hasCompletionClaim = events.some((event) => event.event === "terminal_bridge_completion_claimed" &&
991
- event.conversation_id === input.conversation.conversation_id &&
992
- event.terminal_bridge_message_id === input.ownMessageId &&
993
- event.callback_message_id === input.callbackMessageId &&
994
- event.completion_fingerprint === input.completionClaim.completion_fingerprint &&
995
- event.completion_id === input.completionClaim.completion_id &&
996
- event.outcome === "success" &&
997
- event.ts === input.claimedAt);
998
- const hasCompletionDetected = events.some((event) => event.event === "terminal_bridge_completion_detected" &&
999
- event.conversation_id === input.conversation.conversation_id &&
1000
- event.terminal_bridge_message_id === input.ownMessageId &&
1001
- event.callback_message_id === input.callbackMessageId &&
1002
- event.completion_id === input.completionClaim.completion_id &&
1003
- event.completion_outcome === "success" &&
1004
- validTerminalMonitorTimestampMs(event.ts) !== undefined &&
1005
- validTerminalMonitorTimestampMs(event.ts) <= input.fenceAtMs);
1006
- const hasDeliveredCallback = events.some((event) => event.event === "callback_delivery_succeeded" &&
1007
- event.conversation_id === input.conversation.conversation_id &&
1008
- event.message_id === input.callbackMessageId &&
1009
- event.status === "idle" &&
1010
- event.ts === input.deliveredAt);
1011
- const hasExactFence = events.some((event) => event.event === "terminal_bridge_stalled_by_uncertain_dispatch" &&
1012
- event.conversation_id === input.conversation.conversation_id &&
1013
- event.uncertain_message_id === input.uncertainMessageId &&
1014
- event.ts === input.fenceObservedAt);
1015
- const hasExactOwnerClose = ownerEvents.some((event) => event.event === "conversation_closed" &&
1016
- event.conversation_id === owner.conversation_id &&
1017
- event.status === "closed" &&
1018
- event.ts === ownerClosedAt &&
1019
- event.reason === owner.close_reason);
1020
- return hasCompletionClaim && hasCompletionDetected &&
1021
- hasDeliveredCallback && hasExactFence && hasExactOwnerClose;
1022
- }
1023
- async reconcileCollateral(storeDir, conversationId) {
1024
- return withStoreWriterLeaseAsync(storeDir, async () => this.#reconcileCollateralLocked(storeDir, conversationId));
1025
- }
1026
- #reconcileCollateralLocked(storeDir, conversationId) {
1027
- const reservedSourceTurnIds = new Set(listDeferredForegroundTransfers(storeDir)
1028
- .filter((transfer) => transfer.version === 2 &&
1029
- transfer.source_kind === "candidate_rollout_quiescent" &&
1030
- !isFinalDeferredForegroundTransferStatus(transfer.status))
1031
- .flatMap((transfer) => (transfer.source_turn_history ?? []).map((turn) => turn.turn_id)));
1032
- const candidates = listConversations(storeDir).filter((conversation) => {
1033
- const takeover = takeoverFor(conversation);
1034
- return (conversationId === undefined ||
1035
- conversation.conversation_id === conversationId) &&
1036
- !reservedSourceTurnIds.has(turnIdForConversation(conversation)) &&
1037
- conversation.status === "stalled" &&
1038
- isRecord(takeover?.terminal_bridge_uncertain_dispatch_fence);
1039
- });
1040
- const result = {
1041
- checked: candidates.length,
1042
- repaired: 0,
1043
- skipped: 0,
1044
- errors: [],
1045
- items: []
1046
- };
1047
- for (const listed of candidates) {
1048
- this.#reconcileCollateralCandidate(storeDir, listed, result);
1049
- }
1050
- return result;
1051
- }
1052
- #reconcileCollateralCandidate(storeDir, listed, result) {
1053
- const statePath = nonBlankString(listed.state_path);
1054
- if (!statePath) {
1055
- result.skipped += 1;
1056
- return;
1057
- }
1058
- let release;
1059
- try {
1060
- release = this.#stateFileLock.acquire(`${statePath}.lock`);
1061
- const current = loadState(statePath);
1062
- const evidence = this.#exactCollateralRepairEvidence(current, storeDir);
1063
- if (!evidence) {
1064
- result.skipped += 1;
1065
- return;
1066
- }
1067
- const repaired = this.#persistCollateralRepair(statePath, current, evidence);
1068
- result.repaired += 1;
1069
- result.items.push(repaired);
1070
- }
1071
- catch (error) {
1072
- result.skipped += 1;
1073
- const reason = error instanceof Error ? error.message : String(error);
1074
- result.errors.push(`${listed.conversation_id}: ${reason}`);
1075
- result.items.push({
1076
- conversation_id: listed.conversation_id,
1077
- status: "error",
1078
- reason
1079
- });
1080
- }
1081
- finally {
1082
- release?.();
1083
- }
1084
- }
1085
- #persistCollateralRepair(statePath, current, evidence) {
1086
- const takeover = { ...takeoverFor(current) };
1087
- delete takeover.terminal_bridge_uncertain_dispatch_fence;
1088
- const repairedAt = cliNow().toISOString();
1089
- takeover.terminal_bridge_collateral_stall_repair = {
1090
- repaired_at: repairedAt,
1091
- uncertain_message_id: evidence.uncertainMessageId,
1092
- uncertain_owner_conversation_id: evidence.ownerConversationId,
1093
- restored_status: evidence.restoredStatus,
1094
- evidence: "foreign_uncertain_fence+completion_claim+delivered_callback+closed_owner"
1095
- };
1096
- const conversation = {
1097
- ...current,
1098
- status: evidence.restoredStatus,
1099
- native_session_takeover: takeover,
1100
- updated_at: repairedAt
1101
- };
1102
- delete conversation.stalled_at;
1103
- delete conversation.stalled_reason;
1104
- saveState(statePath, conversation);
1105
- const eventWarning = this.#appendCollateralRepairEvent(statePath, current, evidence, repairedAt);
1106
- return {
1107
- conversation_id: current.conversation_id,
1108
- status: "repaired",
1109
- reason: "legacy_terminal_bridge_collateral_stall",
1110
- uncertain_message_id: evidence.uncertainMessageId,
1111
- uncertain_owner_conversation_id: evidence.ownerConversationId,
1112
- restored_status: evidence.restoredStatus,
1113
- ...(eventWarning ? { event_warning: eventWarning } : {})
1114
- };
1115
- }
1116
- #appendCollateralRepairEvent(statePath, current, evidence, repairedAt) {
1117
- try {
1118
- appendEvent(nonBlankString(current.event_log_path) ??
1119
- logPathForStatePath(statePath), {
1120
- ts: repairedAt,
1121
- conversation_id: current.conversation_id,
1122
- event: "terminal_bridge_collateral_stall_repaired",
1123
- uncertain_message_id: evidence.uncertainMessageId,
1124
- uncertain_owner_conversation_id: evidence.ownerConversationId,
1125
- previous_status: "stalled",
1126
- restored_status: evidence.restoredStatus,
1127
- evidence: "foreign_uncertain_fence+completion_claim+delivered_callback+closed_owner"
1128
- });
1129
- return undefined;
1130
- }
1131
- catch (error) {
1132
- const warning = error instanceof Error ? error.message : String(error);
1133
- cliRuntimeLog("warn", "terminal_bridge_collateral_stall_repair_event_failed", {
1134
- conversation_id: current.conversation_id,
1135
- uncertain_message_id: evidence.uncertainMessageId,
1136
- error: warning
1137
- });
1138
- return warning;
1139
- }
1140
- }
1141
- #recordApprovalNotification(input) {
1142
- const storeDir = pathsForConversationDir(path.dirname(input.statePath))
1143
- .storeDir;
1144
- return withStoreWriterLease(storeDir, () => {
1145
- const release = this.#stateFileLock.acquire(`${input.statePath}.lock`);
1146
- try {
1147
- const conversation = loadState(input.statePath);
1148
- if (!this.#approvalSnapshotMatches(conversation, input)) {
1149
- return {
1150
- conversation,
1151
- duplicate: false,
1152
- stale: true,
1153
- recorded: undefined
1154
- };
1155
- }
1156
- return this.#recordMatchingApproval(this.#approvalPersistenceContext(conversation, input));
1157
- }
1158
- finally {
1159
- release();
1160
- }
1161
- });
1162
- }
1163
- #approvalSnapshotMatches(conversation, input) {
1164
- const takeover = takeoverFor(conversation);
1165
- const currentControl = terminalControlFromTakeover(takeover);
1166
- return isWaitingForAgentStatus(conversation.status) &&
1167
- conversation.conversation_id === input.expectedConversation.conversationId &&
1168
- conversation.status === input.expectedConversation.status &&
1169
- conversation.updated_at === input.expectedConversation.updatedAt &&
1170
- takeover?.terminal_bridge === true &&
1171
- nonBlankString(takeover.terminal_bridge_message_id) ===
1172
- input.expectedConversation.messageId &&
1173
- currentControl !== undefined &&
1174
- terminalControlsShareIncarnation(currentControl, input.terminalControl);
1175
- }
1176
- #approvalPersistenceContext(conversation, input) {
1177
- const nativeTakeover = { ...takeoverFor(conversation) };
1178
- const screen = isRecord(input.terminalStatus.screen)
1179
- ? input.terminalStatus.screen
1180
- : undefined;
1181
- const approvalScreenDigest = nonBlankString(screen?.digest);
1182
- const previousApproval = isRecord(nativeTakeover.terminal_bridge_approval)
1183
- ? nativeTakeover.terminal_bridge_approval
1184
- : undefined;
1185
- const previousNotifiedAt = validTerminalMonitorTimestampMs(previousApproval?.notified_at);
1186
- const callbackDelivery = isRecord(conversation.callback_delivery)
1187
- ? conversation.callback_delivery
1188
- : undefined;
1189
- const callbackMessage = isRecord(callbackDelivery?.message)
1190
- ? callbackDelivery.message
1191
- : undefined;
1192
- const previousCallbackMessageId = nonBlankString(previousApproval?.callback_message_id);
1193
- const matchingApprovalOutbox = callbackDelivery?.kind === "approval_notification" &&
1194
- previousCallbackMessageId !== undefined &&
1195
- callbackMessage?.id === previousCallbackMessageId;
1196
- const deliveryStatus = nonBlankString(callbackDelivery?.status);
1197
- const deliveryAttempts = Number(callbackDelivery?.attempts ?? 0);
1198
- const conflictingActiveOutbox = !matchingApprovalOutbox && (deliveryStatus === "pending" ||
1199
- (deliveryStatus === "failed" &&
1200
- Number.isFinite(deliveryAttempts) &&
1201
- deliveryAttempts <= this.#dependencies.runtime.callbackRetryLimit));
1202
- return {
1203
- input,
1204
- conversation,
1205
- nativeTakeover,
1206
- approvalScreenDigest,
1207
- previousApproval,
1208
- previousNotifiedAt,
1209
- previousCallbackMessageId,
1210
- matchingApprovalOutbox,
1211
- conflictingActiveOutbox
1212
- };
1213
- }
1214
- #recordMatchingApproval(context) {
1215
- const duplicate = context.previousApproval?.fingerprint === context.input.fingerprint &&
1216
- context.previousNotifiedAt !== undefined &&
1217
- cliNowMs() - context.previousNotifiedAt <=
1218
- this.#dependencies.runtime.approvalTtlMs;
1219
- if (!duplicate) {
1220
- return this.#recordNewApproval(context);
1221
- }
1222
- if (context.conflictingActiveOutbox) {
1223
- return {
1224
- conversation: context.conversation,
1225
- duplicate: false,
1226
- stale: true,
1227
- deferred: true,
1228
- previousApproval: context.previousApproval,
1229
- recorded: undefined
1230
- };
1231
- }
1232
- if (!context.matchingApprovalOutbox) {
1233
- return this.#recoverApprovalOutbox(context);
1234
- }
1235
- return {
1236
- conversation: context.conversation,
1237
- duplicate: true,
1238
- stale: false,
1239
- previousApproval: context.previousApproval,
1240
- recorded: undefined
1241
- };
1242
- }
1243
- #recoverApprovalOutbox(context) {
1244
- const messageId = context.previousCallbackMessageId ?? `msg-${randomUUID()}`;
1245
- const messageTs = nonBlankString(context.previousApproval?.callback_message_ts) ?? nonBlankString(context.previousApproval?.notified_at) ??
1246
- cliNow().toISOString();
1247
- const conversation = context.previousCallbackMessageId
1248
- ? context.conversation
1249
- : {
1250
- ...context.conversation,
1251
- native_session_takeover: {
1252
- ...context.nativeTakeover,
1253
- terminal_bridge_approval: {
1254
- ...context.previousApproval,
1255
- callback_message_id: messageId,
1256
- callback_message_ts: messageTs
1257
- }
1258
- }
1259
- };
1260
- if (!context.previousCallbackMessageId) {
1261
- saveState(context.input.statePath, conversation);
1262
- }
1263
- const recorded = context.input.onRecorded(conversation, {
1264
- recoverMissingOutbox: true
1265
- });
1266
- appendEvent(context.input.logPath, {
1267
- ts: cliNow().toISOString(),
1268
- conversation_id: conversation.conversation_id,
1269
- event: "terminal_bridge_approval_notification_outbox_recovered",
1270
- terminal_control: context.input.terminalControl,
1271
- fingerprint: context.input.fingerprint,
1272
- callback_message_id: messageId
1273
- });
1274
- return {
1275
- conversation: recorded.prepared?.conversation ?? conversation,
1276
- duplicate: false,
1277
- recovered: true,
1278
- stale: false,
1279
- previousApproval: context.previousApproval,
1280
- recorded
1281
- };
1282
- }
1283
- #recordNewApproval(context) {
1284
- const now = cliNow().toISOString();
1285
- const callbackMessageId = `msg-${randomUUID()}`;
1286
- const conversation = {
1287
- ...context.conversation,
1288
- native_session_takeover: {
1289
- ...context.nativeTakeover,
1290
- terminal_bridge_approval: {
1291
- fingerprint: context.input.fingerprint,
1292
- screen_digest: context.approvalScreenDigest,
1293
- notified_at: now,
1294
- terminal_control: context.input.terminalControl,
1295
- approval_state: context.input.terminalStatus.approval_state,
1296
- callback_message_id: callbackMessageId,
1297
- callback_message_ts: now
1298
- }
1299
- },
1300
- updated_at: now
1301
- };
1302
- saveState(context.input.statePath, conversation);
1303
- appendEvent(context.input.logPath, {
1304
- ts: now,
1305
- conversation_id: context.conversation.conversation_id,
1306
- event: "terminal_bridge_approval_notification_recorded",
1307
- terminal_control: context.input.terminalControl,
1308
- fingerprint: context.input.fingerprint,
1309
- screen_digest: context.approvalScreenDigest
1310
- });
1311
- const recorded = context.input.onRecorded(conversation);
1312
- return {
1313
- conversation: recorded.prepared?.conversation ?? conversation,
1314
- duplicate: false,
1315
- stale: false,
1316
- recorded
1317
- };
1318
- }
1319
- #recordInteractionNotification(input) {
1320
- const storeDir = pathsForConversationDir(path.dirname(input.statePath))
1321
- .storeDir;
1322
- return withStoreWriterLease(storeDir, () => {
1323
- const release = this.#stateFileLock.acquire(`${input.statePath}.lock`);
1324
- try {
1325
- const conversation = loadState(input.statePath);
1326
- if (!this.#interactionSnapshotMatches(conversation, input)) {
1327
- return {
1328
- conversation,
1329
- duplicate: false,
1330
- stale: true,
1331
- recorded: undefined
1332
- };
1333
- }
1334
- return this.#recordMatchingInteraction(this.#interactionPersistenceContext(conversation, input));
1335
- }
1336
- finally {
1337
- release();
1338
- }
1339
- });
1340
- }
1341
- #interactionSnapshotMatches(conversation, input) {
1342
- const takeover = takeoverFor(conversation);
1343
- const currentControl = terminalControlFromTakeover(takeover);
1344
- const projection = input.terminalStatus.interaction_state;
1345
- return conversation.status === "waiting_for_agent" &&
1346
- conversation.conversation_id === input.expectedConversation.conversationId &&
1347
- conversation.status === input.expectedConversation.status &&
1348
- conversation.updated_at === input.expectedConversation.updatedAt &&
1349
- takeover?.terminal_bridge === true &&
1350
- nonBlankString(takeover.terminal_bridge_message_id) ===
1351
- input.expectedConversation.messageId &&
1352
- currentControl !== undefined &&
1353
- terminalControlsShareIncarnation(currentControl, input.terminalControl) &&
1354
- projection?.interaction_id === input.interactionId &&
1355
- projection.questions.length === 1 &&
1356
- projection.questions[0]?.question_id === input.questionId &&
1357
- input.terminalStatus.interaction_prompt_fingerprint === input.fingerprint &&
1358
- input.terminalStatus.interaction_surface_id === input.surfaceId &&
1359
- validTerminalInteractionSurfaceId(input.surfaceId);
1360
- }
1361
- #interactionPersistenceContext(conversation, input) {
1362
- const nativeTakeover = { ...takeoverFor(conversation) };
1363
- const screen = isRecord(input.terminalStatus.screen)
1364
- ? input.terminalStatus.screen
1365
- : undefined;
1366
- const interactionScreenDigest = nonBlankString(screen?.digest);
1367
- const previousNotification = isRecord(nativeTakeover.terminal_bridge_interaction_notification)
1368
- ? nativeTakeover.terminal_bridge_interaction_notification
1369
- : undefined;
1370
- const previousInteractionState = validInteractionProjection(previousNotification?.interaction_state);
1371
- const callbackField = input.terminalStatus.interaction_state?.kind ===
1372
- "async_question"
1373
- ? "callback_notification_delivery"
1374
- : "callback_delivery";
1375
- const callbackDelivery = isRecord(conversation[callbackField])
1376
- ? conversation[callbackField]
1377
- : undefined;
1378
- const callbackMessage = isRecord(callbackDelivery?.message)
1379
- ? callbackDelivery.message
1380
- : undefined;
1381
- const callbackMetadata = isRecord(callbackMessage?.metadata)
1382
- ? callbackMessage.metadata
1383
- : undefined;
1384
- const callbackInteractionState = validInteractionProjection(callbackMetadata?.interaction_state);
1385
- const callbackInteractionReason = callbackInteractionState?.state === "pending" &&
1386
- callbackInteractionState.capabilities.respond === true &&
1387
- callbackInteractionState.questions[0]?.response_kind !== "multi_select"
1388
- ? "interaction_required"
1389
- : "interaction_manual_required";
1390
- const previousCallbackMessageId = nonBlankString(previousNotification?.callback_message_id);
1391
- const matchingInteractionOutbox = callbackDelivery?.kind === "interaction_notification" &&
1392
- previousCallbackMessageId !== undefined &&
1393
- callbackMessage?.id === previousCallbackMessageId &&
1394
- callbackMetadata?.source === "terminal_bridge" &&
1395
- callbackMetadata?.reason === callbackInteractionReason &&
1396
- callbackInteractionState !== undefined &&
1397
- callbackInteractionState.interaction_id ===
1398
- previousNotification?.interaction_id &&
1399
- callbackInteractionState.questions.length === 1 &&
1400
- callbackInteractionState.questions[0]?.question_id ===
1401
- previousNotification?.question_id &&
1402
- previousNotification?.surface_id === input.surfaceId &&
1403
- callbackInteractionState.turn_id === turnIdForConversation(conversation);
1404
- const deliveryStatus = nonBlankString(callbackDelivery?.status);
1405
- const deliveryAttempts = Number(callbackDelivery?.attempts ?? 0);
1406
- const activeOutbox = deliveryStatus === "pending" ||
1407
- (deliveryStatus === "failed" &&
1408
- Number.isFinite(deliveryAttempts) &&
1409
- deliveryAttempts <= this.#dependencies.runtime.callbackRetryLimit);
1410
- const conflictingActiveOutbox = activeOutbox && !matchingInteractionOutbox;
1411
- return {
1412
- input,
1413
- conversation,
1414
- nativeTakeover,
1415
- interactionScreenDigest,
1416
- previousNotification,
1417
- previousInteractionState,
1418
- previousCallbackMessageId,
1419
- matchingInteractionOutbox,
1420
- activeOutbox,
1421
- conflictingActiveOutbox
1422
- };
1423
- }
1424
- #recordMatchingInteraction(context) {
1425
- const duplicate = context.previousNotification?.terminal_bridge_message_id ===
1426
- context.input.expectedConversation.messageId &&
1427
- context.previousNotification?.interaction_id ===
1428
- context.input.interactionId &&
1429
- context.previousNotification?.question_id === context.input.questionId &&
1430
- context.previousNotification?.prompt_fingerprint ===
1431
- context.input.fingerprint &&
1432
- context.previousNotification?.surface_id === context.input.surfaceId &&
1433
- context.previousInteractionState?.interaction_id ===
1434
- context.input.interactionId &&
1435
- context.previousInteractionState.questions.length === 1 &&
1436
- context.previousInteractionState.questions[0]?.question_id ===
1437
- context.input.questionId;
1438
- if (context.conflictingActiveOutbox ||
1439
- (context.activeOutbox && !duplicate)) {
1440
- return {
1441
- conversation: context.conversation,
1442
- duplicate: false,
1443
- stale: true,
1444
- deferred: true,
1445
- recorded: undefined
1446
- };
1447
- }
1448
- if (!duplicate) {
1449
- return this.#recordNewInteraction(context);
1450
- }
1451
- if (!context.matchingInteractionOutbox) {
1452
- return this.#recoverInteractionOutbox(context);
1453
- }
1454
- return {
1455
- conversation: context.conversation,
1456
- duplicate: true,
1457
- stale: false,
1458
- recorded: undefined
1459
- };
1460
- }
1461
- #recoverInteractionOutbox(context) {
1462
- const messageId = context.previousCallbackMessageId ?? `msg-${randomUUID()}`;
1463
- const messageTs = nonBlankString(context.previousNotification?.callback_message_ts) ?? nonBlankString(context.previousNotification?.notified_at) ??
1464
- cliNow().toISOString();
1465
- const conversation = context.previousCallbackMessageId
1466
- ? context.conversation
1467
- : {
1468
- ...context.conversation,
1469
- native_session_takeover: {
1470
- ...context.nativeTakeover,
1471
- terminal_bridge_interaction_notification: {
1472
- ...context.previousNotification,
1473
- callback_message_id: messageId,
1474
- callback_message_ts: messageTs
1475
- }
1476
- }
1477
- };
1478
- if (!context.previousCallbackMessageId) {
1479
- saveState(context.input.statePath, conversation);
1480
- }
1481
- const recorded = context.input.onRecorded(conversation, {
1482
- recoverMissingOutbox: true
1483
- });
1484
- appendEvent(context.input.logPath, {
1485
- ts: cliNow().toISOString(),
1486
- conversation_id: conversation.conversation_id,
1487
- event: "terminal_bridge_interaction_notification_outbox_recovered",
1488
- terminal_control: context.input.terminalControl,
1489
- interaction_id: context.input.interactionId,
1490
- question_id: context.input.questionId,
1491
- callback_message_id: messageId
1492
- });
1493
- return {
1494
- conversation: recorded.prepared?.conversation ?? conversation,
1495
- duplicate: false,
1496
- recovered: true,
1497
- stale: false,
1498
- recorded
1499
- };
1500
- }
1501
- #recordNewInteraction(context) {
1502
- const now = cliNow().toISOString();
1503
- const callbackMessageId = `msg-${randomUUID()}`;
1504
- const conversation = {
1505
- ...context.conversation,
1506
- native_session_takeover: {
1507
- ...context.nativeTakeover,
1508
- terminal_bridge_interaction_notification: {
1509
- terminal_bridge_message_id: context.input.expectedConversation.messageId,
1510
- interaction_id: context.input.interactionId,
1511
- question_id: context.input.questionId,
1512
- prompt_fingerprint: context.input.fingerprint,
1513
- surface_id: context.input.surfaceId,
1514
- screen_digest: context.interactionScreenDigest,
1515
- notified_at: now,
1516
- terminal_control: context.input.terminalControl,
1517
- interaction_state: context.input.terminalStatus.interaction_state,
1518
- callback_message_id: callbackMessageId,
1519
- callback_message_ts: now
1520
- }
1521
- },
1522
- updated_at: now
1523
- };
1524
- saveState(context.input.statePath, conversation);
1525
- appendEvent(context.input.logPath, {
1526
- ts: now,
1527
- conversation_id: context.conversation.conversation_id,
1528
- event: "terminal_bridge_interaction_notification_recorded",
1529
- terminal_control: context.input.terminalControl,
1530
- interaction_id: context.input.interactionId,
1531
- question_id: context.input.questionId,
1532
- screen_digest: context.interactionScreenDigest
1533
- });
1534
- const recorded = context.input.onRecorded(conversation);
1535
- return {
1536
- conversation: recorded.prepared?.conversation ?? conversation,
1537
- duplicate: false,
1538
- stale: false,
1539
- recorded
1540
- };
1541
- }
1542
456
  #markApprovalPromptCleared(input) {
1543
457
  const storeDir = pathsForConversationDir(path.dirname(input.statePath))
1544
458
  .storeDir;
@@ -1809,6 +723,12 @@ class TerminalMonitorStateCliApplication {
1809
723
  });
1810
724
  return { conversation: persistedConversation, prepared };
1811
725
  }
726
+ reconcileCollateral(storeDir, conversationId) {
727
+ return this.#collateral.reconcileCollateral(storeDir, conversationId);
728
+ }
729
+ stallOther(input) {
730
+ return this.#collateral.stallOther(input);
731
+ }
1812
732
  }
1813
733
  function monitorRuntimePort() {
1814
734
  return {
@@ -1821,443 +741,6 @@ function monitorRuntimePort() {
1821
741
  exit: cliExit
1822
742
  };
1823
743
  }
1824
- function terminalSubmissionRetryStartupAuthorityContext(input) {
1825
- const sessionId = sessionIdForConversation(input.conversation);
1826
- const turnId = turnIdForConversation(input.conversation);
1827
- const messageId = nonBlankString(input.submission.message_id);
1828
- const requestText = terminalSubmissionPayload(nonBlankString(input.takeover.terminal_bridge_request_text) ?? "");
1829
- const requestHash = terminalBridgeRequestFingerprint(requestText);
1830
- const messageType = nonBlankString(input.submission.message_type);
1831
- const messageBodyHash = createHash("sha256")
1832
- .update(requestText)
1833
- .digest("hex");
1834
- const callbackFingerprint = callbackRouteFingerprintForConversation(input.conversation) ?? null;
1835
- const deferredReferences = [
1836
- nonBlankString(input.takeover.deferred_foreground_transfer_id),
1837
- nonBlankString(input.submission.deferred_foreground_transfer_id),
1838
- nonBlankString(input.ledger.deferred_foreground_transfer_id),
1839
- input.attempt.deferred_foreground_transfer_id ?? undefined
1840
- ];
1841
- const presentDeferredReferences = deferredReferences.filter((value) => value !== undefined);
1842
- const deferredTransferId = presentDeferredReferences[0];
1843
- const submissionTerminalEvidence = input.submission.terminal_endpoint !== undefined
1844
- ? input.submission.terminal_endpoint
1845
- : input.submission.terminal_control;
1846
- const statePath = nonBlankString(input.conversation.state_path);
1847
- const logPath = nonBlankString(input.conversation.event_log_path);
1848
- const bindingId = nonBlankString(input.conversation.terminal_binding_id);
1849
- const bindingGeneration = Number(input.conversation.terminal_binding_generation);
1850
- return {
1851
- sessionId,
1852
- turnId,
1853
- messageId,
1854
- requestText,
1855
- requestHash,
1856
- messageType,
1857
- messageBodyHash,
1858
- callbackFingerprint,
1859
- deferredReferences,
1860
- presentDeferredReferences,
1861
- deferredTransferId,
1862
- submissionTerminalEvidence,
1863
- statePath,
1864
- logPath,
1865
- bindingId,
1866
- bindingGeneration
1867
- };
1868
- }
1869
- function terminalSubmissionRetryStartupIdentityMatches(input, context) {
1870
- return !(!context.requestText || !context.requestHash || !context.messageId ||
1871
- (context.messageType !== "task" && context.messageType !== "answer") ||
1872
- nonBlankString(input.takeover.terminal_bridge_message_id) !==
1873
- context.messageId ||
1874
- input.attempt.session_id !== context.sessionId ||
1875
- input.attempt.turn_id !== context.turnId ||
1876
- input.attempt.original_message_id !== context.messageId ||
1877
- input.attempt.active_message_id !== context.messageId ||
1878
- input.attempt.request_hash !== context.requestHash ||
1879
- input.attempt.terminal_target !== input.control.target ||
1880
- input.attempt.callback_route_fingerprint !== context.callbackFingerprint);
1881
- }
1882
- function terminalSubmissionRetryStartupSubmissionPathMatches(input) {
1883
- return path.resolve(nonBlankString(input.submission.store_dir) ?? "") ===
1884
- path.resolve(input.storeDir);
1885
- }
1886
- function terminalSubmissionRetryStartupSubmissionMatches(input, context) {
1887
- return !(nonBlankString(input.submission.session_id) !== context.sessionId ||
1888
- nonBlankString(input.submission.turn_id) !== context.turnId ||
1889
- nonBlankString(input.submission.request_hash) !== context.requestHash ||
1890
- nonBlankString(input.submission.message_body_hash) !==
1891
- context.messageBodyHash ||
1892
- nonBlankString(input.submission.executor_kind) !== "codex" ||
1893
- !terminalSubmissionRetryStartupSubmissionPathMatches(input) ||
1894
- (input.submission.callback_route_fingerprint ?? null) !==
1895
- context.callbackFingerprint ||
1896
- !terminalControlEvidenceMatches(context.submissionTerminalEvidence, input.control));
1897
- }
1898
- function terminalSubmissionRetryStartupLedgerPathMatches(input, context) {
1899
- return !(path.resolve(nonBlankString(input.ledger.store_dir) ?? "") !==
1900
- path.resolve(input.storeDir) ||
1901
- !sameCanonicalStatePath(input.ledger.state_path, input.paths.statePath) ||
1902
- !sameCanonicalStatePath(context.statePath, input.paths.statePath) ||
1903
- !sameCanonicalStatePath(input.ledger.event_log_path, input.paths.logPath) ||
1904
- !sameCanonicalStatePath(context.logPath, input.paths.logPath));
1905
- }
1906
- function terminalSubmissionRetryStartupLedgerMatches(input, context) {
1907
- return !(nonBlankString(input.ledger.generation_id) !== context.messageId ||
1908
- nonBlankString(input.ledger.message_id) !== context.messageId ||
1909
- nonBlankString(input.ledger.conversation_id) !==
1910
- input.conversation.conversation_id ||
1911
- nonBlankString(input.ledger.session_id) !== context.sessionId ||
1912
- nonBlankString(input.ledger.turn_id) !== context.turnId ||
1913
- nonBlankString(input.ledger.message_type) !== context.messageType ||
1914
- nonBlankString(input.ledger.message_body_hash) !==
1915
- context.messageBodyHash ||
1916
- nonBlankString(input.ledger.executor_kind) !== "codex" ||
1917
- nonBlankString(input.ledger.request_hash) !== context.requestHash ||
1918
- !terminalSubmissionRetryStartupLedgerPathMatches(input, context) ||
1919
- (input.ledger.callback_route_fingerprint ?? null) !==
1920
- context.callbackFingerprint ||
1921
- input.ledger.callback_expected !==
1922
- callbackExpectedForConversation(input.conversation) ||
1923
- !input.ledgerMatchesControl ||
1924
- (context.bindingId !== undefined &&
1925
- nonBlankString(input.ledger.binding_id) !== context.bindingId) ||
1926
- (Number.isSafeInteger(context.bindingGeneration) &&
1927
- Number(input.ledger.binding_generation) !== context.bindingGeneration));
1928
- }
1929
- function terminalSubmissionRetryStartupDeferredMatches(input, context) {
1930
- return !((context.deferredTransferId !== undefined &&
1931
- (context.presentDeferredReferences.length !==
1932
- context.deferredReferences.length ||
1933
- new Set(context.presentDeferredReferences).size !== 1)) ||
1934
- (context.deferredTransferId === undefined &&
1935
- input.attempt.deferred_foreground_transfer_id !== null));
1936
- }
1937
- function terminalSubmissionRetryStartupProjectionFields(input) {
1938
- if (input.attempt.state === "agent_accepted" ||
1939
- ["agent_accepted", "not_accepted"].includes(String(input.submission.status)) ||
1940
- ["agent_accepted", "not_accepted"].includes(String(input.ledger.status))) {
1941
- return {};
1942
- }
1943
- return {
1944
- projection: projectTerminalSubmissionRetryPending({
1945
- attempt: input.attempt,
1946
- submission: input.submission,
1947
- ledger: input.ledger
1948
- })
1949
- };
1950
- }
1951
- function assertTerminalSubmissionRetryStartupAuthority(input) {
1952
- const context = terminalSubmissionRetryStartupAuthorityContext(input);
1953
- if (!terminalSubmissionRetryStartupIdentityMatches(input, context) ||
1954
- !terminalSubmissionRetryStartupSubmissionMatches(input, context) ||
1955
- !terminalSubmissionRetryStartupLedgerMatches(input, context) ||
1956
- !terminalSubmissionRetryStartupDeferredMatches(input, context)) {
1957
- throw new Error("submission retry startup recovery authority disagrees across Turn and ledger");
1958
- }
1959
- return {
1960
- requestText: context.requestText,
1961
- requestHash: context.requestHash,
1962
- messageType: context.messageType,
1963
- deferredTransferId: context.deferredTransferId,
1964
- ...terminalSubmissionRetryStartupProjectionFields(input)
1965
- };
1966
- }
1967
- function finalizeTerminalSubmissionRetryStartupAccepted(input) {
1968
- const stateAccepted = input.submission.status === "agent_accepted";
1969
- const ledgerAccepted = input.ledger.status === "agent_accepted";
1970
- if (!stateAccepted && !ledgerAccepted) {
1971
- throw new Error("accepted submission retry sidecar lacks native acceptance evidence");
1972
- }
1973
- const stateEvidence = stateAccepted
1974
- ? terminalAcceptanceEvidenceForConversation(input.conversation, input.authority.requestText, input.submission.acceptance_evidence)
1975
- : undefined;
1976
- const ledgerEvidence = ledgerAccepted
1977
- ? terminalAcceptanceEvidenceForConversation(input.conversation, input.authority.requestText, input.ledger.acceptance_evidence)
1978
- : undefined;
1979
- if (stateEvidence && ledgerEvidence &&
1980
- canonicalJson(stateEvidence) !== canonicalJson(ledgerEvidence)) {
1981
- throw new Error("accepted submission retry evidence conflicts across Turn and ledger");
1982
- }
1983
- const evidence = stateEvidence ?? ledgerEvidence;
1984
- const stateAcceptedAt = stateAccepted
1985
- ? nonBlankString(input.submission.agent_accepted_at)
1986
- : undefined;
1987
- const ledgerAcceptedAt = ledgerAccepted
1988
- ? nonBlankString(input.ledger.agent_accepted_at)
1989
- : undefined;
1990
- if (!evidence || !stateAcceptedAt && !ledgerAcceptedAt ||
1991
- stateAcceptedAt && ledgerAcceptedAt && stateAcceptedAt !== ledgerAcceptedAt) {
1992
- throw new Error("accepted submission retry timestamp conflicts across Turn and ledger");
1993
- }
1994
- const acceptedAt = (stateAcceptedAt ?? ledgerAcceptedAt);
1995
- if (input.authority.deferredTransferId) {
1996
- const transfer = loadDeferredForegroundTransfer(input.storeDir, input.authority.deferredTransferId);
1997
- if (transfer.status !== "resolved") {
1998
- if (isFinalDeferredForegroundTransferStatus(transfer.status)) {
1999
- throw new Error("accepted submission retry conflicts with an aborted deferred transfer");
2000
- }
2001
- return input.conversation;
2002
- }
2003
- if (transfer.agent_accepted_at !== acceptedAt ||
2004
- transfer.message_id !== input.attempt.active_message_id) {
2005
- throw new Error("accepted submission retry conflicts with its resolved deferred transfer");
2006
- }
2007
- }
2008
- if (input.attempt.state === "agent_accepted" &&
2009
- input.attempt.agent_accepted_at !== acceptedAt) {
2010
- throw new Error("accepted submission retry sidecar timestamp conflicts with native evidence");
2011
- }
2012
- let conversation = input.conversation;
2013
- const stateNeedsRepair = !stateAccepted;
2014
- if (stateNeedsRepair) {
2015
- const acceptedOwner = {
2016
- ...conversation,
2017
- status: "waiting_for_agent",
2018
- updated_at: acceptedAt
2019
- };
2020
- delete acceptedOwner.stalled_at;
2021
- delete acceptedOwner.stalled_reason;
2022
- delete acceptedOwner.failed_at;
2023
- delete acceptedOwner.failure_reason;
2024
- delete acceptedOwner.idle_since;
2025
- conversation = applyTerminalBridgeSubmission({
2026
- conversation: acceptedOwner,
2027
- messageId: input.attempt.active_message_id,
2028
- messageType: input.authority.messageType,
2029
- requestText: input.authority.requestText,
2030
- status: "agent_accepted",
2031
- preparedAt: nonBlankString(input.submission.prepared_at),
2032
- textInjectedAt: nonBlankString(input.submission.text_injected_at) ??
2033
- nonBlankString(input.ledger.text_injected_at),
2034
- enterDispatchedAt: nonBlankString(input.submission.enter_dispatched_at) ??
2035
- nonBlankString(input.ledger.enter_dispatched_at),
2036
- agentAcceptedAt: acceptedAt,
2037
- acceptanceEvidence: evidence,
2038
- lastProvenStage: "agent_accepted"
2039
- }, {
2040
- dispatcherPid: cliPid(),
2041
- storeDir: input.storeDir,
2042
- terminalControl: input.control
2043
- });
2044
- saveState(input.paths.statePath, conversation);
2045
- }
2046
- let attempt = input.attempt;
2047
- const attemptNeedsRepair = attempt.state !== "agent_accepted";
2048
- if (attemptNeedsRepair) {
2049
- attempt = saveTerminalSubmissionRetry(input.paths.statePath, {
2050
- ...attempt,
2051
- state: "agent_accepted",
2052
- agent_accepted_at: acceptedAt,
2053
- updated_at: acceptedAt
2054
- }, attempt.revision);
2055
- }
2056
- const ledgerNeedsRepair = !ledgerAccepted ||
2057
- input.ledger.submission_retry_state !== "agent_accepted" ||
2058
- input.ledger.submission_retry_revision !== attempt.revision;
2059
- if (ledgerNeedsRepair) {
2060
- input.saveLedger({
2061
- ...input.ledger,
2062
- ...terminalSubmissionRetryLedgerFields(attempt),
2063
- status: "agent_accepted",
2064
- agent_accepted_at: acceptedAt,
2065
- acceptance_evidence: evidence,
2066
- not_accepted_at: undefined,
2067
- uncertain_at: undefined,
2068
- safe_to_retry: undefined,
2069
- dispatcher_pid: null
2070
- });
2071
- }
2072
- if (stateNeedsRepair || attemptNeedsRepair || ledgerNeedsRepair) {
2073
- appendEvent(input.paths.logPath, {
2074
- ts: acceptedAt,
2075
- conversation_id: conversation.conversation_id,
2076
- event: "terminal_submission_retry_startup_accepted_reconciled",
2077
- message_id: input.attempt.active_message_id,
2078
- terminal_input_sent: false
2079
- });
2080
- }
2081
- return loadState(input.paths.statePath);
2082
- }
2083
- function reconcileTerminalSubmissionRetryStartupTerminalLedger(input) {
2084
- const notAccepted = input.startup.outcome === "not_accepted";
2085
- input.saveLedger({
2086
- ...input.ledger,
2087
- ...terminalSubmissionRetryLedgerFields(input.attempt),
2088
- status: input.startup.outcome,
2089
- ...(notAccepted
2090
- ? {
2091
- not_accepted_at: input.startup.at,
2092
- uncertain_at: undefined,
2093
- error: undefined,
2094
- safe_to_retry: undefined
2095
- }
2096
- : {
2097
- not_accepted_at: undefined,
2098
- uncertain_at: input.startup.at,
2099
- error: input.startup.reason,
2100
- safe_to_retry: false
2101
- }),
2102
- acceptance_evidence: undefined,
2103
- agent_accepted_at: undefined,
2104
- dispatcher_pid: null
2105
- });
2106
- appendEvent(input.paths.logPath, {
2107
- ts: input.startup.at,
2108
- conversation_id: input.conversation.conversation_id,
2109
- event: `terminal_submission_retry_startup_${input.startup.outcome}_reconciled`,
2110
- message_id: input.attempt.active_message_id,
2111
- terminal_input_sent: false,
2112
- do_not_retry: true
2113
- });
2114
- }
2115
- function reconcileTerminalSubmissionRetryStartupTerminalState(input) {
2116
- const stalled = {
2117
- ...input.conversation,
2118
- status: "stalled",
2119
- stalled_at: input.startup.at,
2120
- stalled_reason: input.startup.reason ??
2121
- "submission retry native acceptance became uncertain",
2122
- updated_at: input.startup.at
2123
- };
2124
- const conversation = applyTerminalBridgeSubmission({
2125
- conversation: stalled,
2126
- messageId: input.attempt.active_message_id,
2127
- messageType: input.authority.messageType,
2128
- requestText: input.authority.requestText,
2129
- status: "uncertain",
2130
- preparedAt: nonBlankString(input.submission.prepared_at),
2131
- textInjectedAt: nonBlankString(input.submission.text_injected_at),
2132
- enterDispatchedAt: nonBlankString(input.submission.enter_dispatched_at),
2133
- uncertainAt: input.startup.at,
2134
- error: input.startup.reason,
2135
- safeToRetry: false,
2136
- lastProvenStage: "enter_dispatched"
2137
- }, {
2138
- dispatcherPid: cliPid(),
2139
- storeDir: input.storeDir,
2140
- terminalControl: input.control
2141
- });
2142
- saveState(input.paths.statePath, conversation);
2143
- appendEvent(input.paths.logPath, {
2144
- ts: input.startup.at,
2145
- conversation_id: conversation.conversation_id,
2146
- event: "terminal_submission_retry_startup_uncertain_reconciled",
2147
- message_id: input.attempt.active_message_id,
2148
- terminal_input_sent: false,
2149
- do_not_retry: true
2150
- });
2151
- return conversation;
2152
- }
2153
- function mirrorDeferredSubmissionRetryEnter(input) {
2154
- const transfer = loadDeferredForegroundTransfer(input.storeDir, input.transferId);
2155
- assertDeferredSubmissionRetryMirrorAuthority(transfer, input);
2156
- if (isFinalDeferredForegroundTransferStatus(transfer.status)) {
2157
- return false;
2158
- }
2159
- if (transfer.submission_retry_enter_dispatched_at) {
2160
- if (!isDeferredForegroundSubmissionRetryPending(transfer)) {
2161
- throw new Error("deferred submission retry startup authority is not exactly pending");
2162
- }
2163
- return false;
2164
- }
2165
- if (transfer.status !== "uncertain" ||
2166
- transfer.input_stage !== "text_injected" ||
2167
- transfer.enter_dispatched_at !== undefined) {
2168
- throw new Error("deferred submission retry startup recovery found an unsafe stage lag");
2169
- }
2170
- const repaired = saveDeferredForegroundTransfer(input.storeDir, {
2171
- ...transfer,
2172
- input_stage: "enter_dispatched",
2173
- enter_dispatched_at: input.attempt.enter_dispatched_at,
2174
- submission_retry_enter_dispatched_at: input.attempt.enter_dispatched_at
2175
- }, {
2176
- expectedRevision: Number(transfer.revision)
2177
- });
2178
- if (!isDeferredForegroundSubmissionRetryPending(repaired)) {
2179
- throw new Error("deferred submission retry startup recovery did not produce exact pending authority");
2180
- }
2181
- return true;
2182
- }
2183
- function assertDeferredSubmissionRetryMirrorAuthority(transfer, input) {
2184
- const exactMode = input.attempt.mode === "exact_draft_enter";
2185
- if (transfer.transfer_id !== input.transferId ||
2186
- transfer.turn_id !== input.turnId ||
2187
- transfer.target_session_id !== input.sessionId ||
2188
- !sameCanonicalStatePath(transfer.state_path, input.statePath) ||
2189
- transfer.request_hash !== input.requestHash ||
2190
- transfer.message_id !== input.messageId ||
2191
- transfer.prepared_at !== input.preparedAt ||
2192
- transfer.submission_retry_attempt_id !== input.attempt.attempt_id ||
2193
- transfer.submission_retry_mode !== input.attempt.mode ||
2194
- transfer.submission_retry_message_id !== input.messageId ||
2195
- transfer.submission_retry_prepared_at !== input.preparedAt ||
2196
- transfer.submission_retry_enter_reserved_at !==
2197
- input.attempt.enter_reserved_at ||
2198
- transfer.submission_retry_enter_dispatched_at !== undefined &&
2199
- transfer.submission_retry_enter_dispatched_at !==
2200
- input.attempt.enter_dispatched_at ||
2201
- transfer.submission_retry_enter_dispatched_at !== undefined &&
2202
- (transfer.input_stage !== "enter_dispatched" ||
2203
- transfer.enter_dispatched_at !== input.attempt.enter_dispatched_at) ||
2204
- (exactMode
2205
- ? transfer.submission_retry_text_reserved_at !== undefined ||
2206
- transfer.submission_retry_text_injected_at !== undefined ||
2207
- transfer.text_injected_at !== input.textInjectedAt
2208
- : transfer.submission_retry_text_reserved_at !==
2209
- input.attempt.replacement_text_reserved_at ||
2210
- transfer.submission_retry_text_injected_at !==
2211
- input.attempt.replacement_text_injected_at)) {
2212
- throw new Error("deferred submission retry startup authority disagrees with its attempt");
2213
- }
2214
- }
2215
- function terminalSubmissionRetryMonitorEpoch(conversation, at) {
2216
- const takeover = takeoverFor(conversation);
2217
- const inactivityMinutes = Number(takeover.terminal_bridge_inactivity_timeout_minutes);
2218
- const hardMinutes = Number(takeover.terminal_bridge_hard_timeout_minutes);
2219
- const inactivityDeadlineAt = terminalMonitorDeadlineAt(at, inactivityMinutes);
2220
- const hardDeadlineAt = terminalMonitorDeadlineAt(at, hardMinutes);
2221
- if (!inactivityDeadlineAt || !hardDeadlineAt) {
2222
- throw new Error("submission retry startup recovery lacks its configured monitor timeouts");
2223
- }
2224
- const next = {
2225
- ...conversation,
2226
- status: "waiting_for_agent",
2227
- native_session_takeover: {
2228
- ...takeover,
2229
- terminal_bridge_started_at: at,
2230
- terminal_bridge_monitor_started_at: at,
2231
- terminal_bridge_last_activity_at: at,
2232
- terminal_bridge_last_activity_reason: "submission retry Enter dispatched",
2233
- terminal_bridge_inactivity_deadline_at: inactivityDeadlineAt,
2234
- terminal_bridge_hard_deadline_at: hardDeadlineAt
2235
- },
2236
- updated_at: at
2237
- };
2238
- delete next.stalled_at;
2239
- delete next.stalled_reason;
2240
- delete next.failed_at;
2241
- delete next.failure_reason;
2242
- delete next.idle_since;
2243
- return next;
2244
- }
2245
- function terminalSubmissionRetryStateIsPending(input) {
2246
- return input.conversation.status === "waiting_for_agent" &&
2247
- input.submission.status === "enter_dispatched" &&
2248
- nonBlankString(input.submission.message_id) === input.projection.messageId &&
2249
- nonBlankString(input.submission.prepared_at) ===
2250
- input.projection.preparedAt &&
2251
- nonBlankString(input.submission.text_injected_at) ===
2252
- input.projection.textInjectedAt &&
2253
- nonBlankString(input.submission.enter_dispatched_at) ===
2254
- input.projection.enterDispatchedAt;
2255
- }
2256
- function takeoverFor(conversation) {
2257
- return isRecord(conversation.native_session_takeover)
2258
- ? conversation.native_session_takeover
2259
- : {};
2260
- }
2261
744
  function unprepared(reason) {
2262
745
  return { prepared: false, alreadyRunning: false, reason };
2263
746
  }
@@ -2272,9 +755,6 @@ function approvalCanBeCleared(conversation, takeover, input) {
2272
755
  validTerminalMonitorTimestampMs(takeover.terminal_bridge_approval_resolved_at) !== undefined &&
2273
756
  validTerminalMonitorTimestampMs(takeover.terminal_bridge_last_approval_prompt_cleared_at) === undefined;
2274
757
  }
2275
- function validTerminalInteractionSurfaceId(value) {
2276
- return typeof value === "string" && /^tis_[0-9a-f]{40}$/u.test(value);
2277
- }
2278
758
  function detectorDiagnostic(input, existing, now) {
2279
759
  if (input.limitation && input.fingerprint) {
2280
760
  return {
@@ -2293,98 +773,6 @@ function sameDetectorDiagnostic(existing, next) {
2293
773
  return nonBlankString(existing?.status) === nonBlankString(next.status) &&
2294
774
  nonBlankString(existing?.fingerprint) === nonBlankString(next.fingerprint);
2295
775
  }
2296
- function terminalBridgeApprovalInstructions(input) {
2297
- const approval = isRecord(input.terminalStatus.approval_state)
2298
- ? input.terminalStatus.approval_state
2299
- : {};
2300
- const screen = isRecord(input.terminalStatus.screen)
2301
- ? input.terminalStatus.screen
2302
- : {};
2303
- const executor = executorForConversation(input.conversation);
2304
- const agentName = executorDefinitionForKind(executor.kind).displayName;
2305
- const label = nonBlankString(approval.label) ??
2306
- `the current ${agentName} approval prompt`;
2307
- const keys = Array.isArray(approval.keys)
2308
- ? approval.keys.filter((value) => typeof value === "string")
2309
- : [];
2310
- const decisionMode = nonBlankString(approval.decision_mode);
2311
- const keyDescription = keys.length > 0
2312
- ? keys.join(" then ")
2313
- : nonBlankString(approval.key) ?? "the detected approve key sequence";
2314
- const promptKind = nonBlankString(approval.prompt_kind);
2315
- const command = nonBlankString(approval.command);
2316
- const toolName = nonBlankString(approval.tool_name);
2317
- const requestDetail = nonBlankString(approval.request_detail);
2318
- const requestId = nonBlankString(approval.request_id);
2319
- const decisions = Array.isArray(approval.choices)
2320
- ? approval.choices.flatMap((choice) => isRecord(choice) &&
2321
- (choice.decision === "approve_once" || choice.decision === "reject")
2322
- ? [choice.decision]
2323
- : [])
2324
- : ["approve_once"];
2325
- const excerpt = nonBlankString(screen.excerpt) ??
2326
- "(No terminal excerpt was available.)";
2327
- const directReview = executor.kind === "claude" && decisionMode === "keys";
2328
- return approvalInstructionLines({
2329
- ...input,
2330
- approval,
2331
- agentName,
2332
- label,
2333
- keyDescription,
2334
- promptKind,
2335
- command,
2336
- toolName,
2337
- requestDetail,
2338
- requestId,
2339
- decisions,
2340
- excerpt,
2341
- directReview
2342
- }).filter((line) => line !== undefined).join("\n");
2343
- }
2344
- function approvalInstructionLines(input) {
2345
- const turnId = turnIdForConversation(input.conversation);
2346
- return [
2347
- `${input.agentName} is waiting for approval in a terminal-controlled AKK session.`,
2348
- "",
2349
- `Turn: ${turnId}`,
2350
- `Terminal: ${input.terminalControl.kind}:${input.terminalControl.target}`,
2351
- `Approval option: ${input.label} (${input.keyDescription})`,
2352
- input.promptKind ? `Request kind: ${input.promptKind}` : undefined,
2353
- input.toolName ? `Tool: ${input.toolName}` : undefined,
2354
- input.requestDetail ? `Request: ${input.requestDetail}` : undefined,
2355
- input.command ? `Command: ${input.command}` : undefined,
2356
- input.requestId ? `Request id: ${input.requestId}` : undefined,
2357
- "",
2358
- "Safe terminal excerpt:",
2359
- "```text",
2360
- input.excerpt,
2361
- "```",
2362
- "",
2363
- input.directReview
2364
- ? `Before asking for approval, have the user personally inspect the live ${input.terminalControl.kind} pane ${input.terminalControl.target}.`
2365
- : undefined,
2366
- input.directReview
2367
- ? "This hookless callback intentionally omits raw command details; do not approve from the summary alone."
2368
- : undefined,
2369
- input.directReview ? "" : undefined,
2370
- `Ask the user whether to approve or deny this ${input.agentName} request.`,
2371
- "",
2372
- "If the user approves, call `agent_knock_knock_approve` with only:",
2373
- `- turn_id: ${turnId}`,
2374
- "- decision: approve_once",
2375
- "",
2376
- input.decisions.includes("reject")
2377
- ? "If the user rejects this permission but wants the Turn to continue, call `agent_knock_knock_approve` with only:"
2378
- : "This exact prompt has no adapter-proven reject transport; denial requires manual terminal resolution.",
2379
- input.decisions.includes("reject") ? `- turn_id: ${turnId}` : undefined,
2380
- input.decisions.includes("reject") ? "- decision: reject" : undefined,
2381
- input.decisions.includes("reject") ? "" : undefined,
2382
- "If the user wants to stop the managed Turn, call `agent_knock_knock_cancel` with:",
2383
- `- turn_id: ${turnId}`,
2384
- "",
2385
- "Do not use raw tmux, shell, or manual key presses for this approval. Do not approve without explicit user confirmation."
2386
- ];
2387
- }
2388
776
  function stalledMessage(conversation, reason, executor, terminalBridge) {
2389
777
  return createMessage({
2390
778
  conversation,