@scotthuang/agent-knock-knock 0.12.13 → 0.12.15

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 (170) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +13 -13
  3. package/dist/src/callback-cli-adapter.d.ts +9 -6
  4. package/dist/src/callback-cli-adapter.js +119 -30
  5. package/dist/src/callback-cli-adapter.js.map +1 -1
  6. package/dist/src/callback-outbox-policy.d.ts +39 -0
  7. package/dist/src/callback-outbox-policy.js +163 -6
  8. package/dist/src/callback-outbox-policy.js.map +1 -1
  9. package/dist/src/callback-outbox-service.d.ts +28 -1
  10. package/dist/src/callback-outbox-service.js +413 -82
  11. package/dist/src/callback-outbox-service.js.map +1 -1
  12. package/dist/src/callback-outbox-settlement.d.ts +14 -1
  13. package/dist/src/callback-outbox-settlement.js +151 -30
  14. package/dist/src/callback-outbox-settlement.js.map +1 -1
  15. package/dist/src/callback-route-authority.d.ts +59 -0
  16. package/dist/src/callback-route-authority.js +123 -0
  17. package/dist/src/callback-route-authority.js.map +1 -0
  18. package/dist/src/callback-transport.d.ts +134 -0
  19. package/dist/src/callback-transport.js +256 -0
  20. package/dist/src/callback-transport.js.map +1 -0
  21. package/dist/src/cli-core.js +56 -18
  22. package/dist/src/cli-core.js.map +1 -1
  23. package/dist/src/cli-runtime-context.d.ts +2 -0
  24. package/dist/src/cli-runtime-context.js.map +1 -1
  25. package/dist/src/codex-lifecycle-compatibility.js +2 -1
  26. package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
  27. package/dist/src/deferred-foreground-application-service.d.ts +18 -0
  28. package/dist/src/deferred-foreground-application-service.js +72 -0
  29. package/dist/src/deferred-foreground-application-service.js.map +1 -1
  30. package/dist/src/deferred-foreground-authority-cli-adapter.js +10 -1
  31. package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
  32. package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +1 -1
  33. package/dist/src/deferred-foreground-recovery-cli-adapter.js +108 -37
  34. package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -1
  35. package/dist/src/deferred-foreground-recovery-service.d.ts +1 -1
  36. package/dist/src/deferred-foreground-recovery-service.js +10 -3
  37. package/dist/src/deferred-foreground-recovery-service.js.map +1 -1
  38. package/dist/src/deferred-foreground-transfer-policy.d.ts +1 -1
  39. package/dist/src/deferred-foreground-transfer-policy.js +3 -2
  40. package/dist/src/deferred-foreground-transfer-policy.js.map +1 -1
  41. package/dist/src/deferred-foreground-transfer.d.ts +17 -1
  42. package/dist/src/deferred-foreground-transfer.js +244 -35
  43. package/dist/src/deferred-foreground-transfer.js.map +1 -1
  44. package/dist/src/deferred-foreground-user-close.d.ts +18 -0
  45. package/dist/src/deferred-foreground-user-close.js +180 -0
  46. package/dist/src/deferred-foreground-user-close.js.map +1 -0
  47. package/dist/src/install-doctor-command-adapter.js +1 -1
  48. package/dist/src/install-doctor-command-adapter.js.map +1 -1
  49. package/dist/src/openclaw-callback-transport.d.ts +4 -1
  50. package/dist/src/openclaw-callback-transport.js +403 -10
  51. package/dist/src/openclaw-callback-transport.js.map +1 -1
  52. package/dist/src/openclaw-doctor.js +16 -2
  53. package/dist/src/openclaw-doctor.js.map +1 -1
  54. package/dist/src/openclaw-managed-callback-cli-adapter.d.ts +23 -0
  55. package/dist/src/openclaw-managed-callback-cli-adapter.js +77 -0
  56. package/dist/src/openclaw-managed-callback-cli-adapter.js.map +1 -0
  57. package/dist/src/openclaw-plugin-command-adapter.js +93 -39
  58. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  59. package/dist/src/openclaw-plugin-helpers.d.ts +1 -0
  60. package/dist/src/openclaw-plugin-helpers.js +9 -1
  61. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  62. package/dist/src/openclaw-plugin-schemas.d.ts +20 -1
  63. package/dist/src/openclaw-plugin-schemas.js +27 -3
  64. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  65. package/dist/src/protocol.d.ts +3 -0
  66. package/dist/src/protocol.js +4 -0
  67. package/dist/src/protocol.js.map +1 -1
  68. package/dist/src/store.d.ts +12 -1
  69. package/dist/src/store.js +81 -3
  70. package/dist/src/store.js.map +1 -1
  71. package/dist/src/terminal-acceptance-cli-adapter.d.ts +2 -0
  72. package/dist/src/terminal-acceptance-cli-adapter.js +62 -12
  73. package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -1
  74. package/dist/src/terminal-action-projection.d.ts +31 -3
  75. package/dist/src/terminal-action-projection.js +64 -9
  76. package/dist/src/terminal-action-projection.js.map +1 -1
  77. package/dist/src/terminal-agent-bridge.d.ts +82 -0
  78. package/dist/src/terminal-agent-bridge.js +553 -109
  79. package/dist/src/terminal-agent-bridge.js.map +1 -1
  80. package/dist/src/terminal-command-cli-adapter.d.ts +26 -2
  81. package/dist/src/terminal-command-cli-adapter.js +3162 -214
  82. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  83. package/dist/src/terminal-delegate-cli-adapter.d.ts +4 -22
  84. package/dist/src/terminal-delegate-cli-adapter.js +239 -192
  85. package/dist/src/terminal-delegate-cli-adapter.js.map +1 -1
  86. package/dist/src/terminal-delegate-send-binding.d.ts +63 -0
  87. package/dist/src/terminal-delegate-send-binding.js +360 -0
  88. package/dist/src/terminal-delegate-send-binding.js.map +1 -0
  89. package/dist/src/terminal-dispatch-application.d.ts +4 -1
  90. package/dist/src/terminal-dispatch-application.js +11 -2
  91. package/dist/src/terminal-dispatch-application.js.map +1 -1
  92. package/dist/src/terminal-dispatch-composition.d.ts +5 -0
  93. package/dist/src/terminal-dispatch-execution.js +3 -1
  94. package/dist/src/terminal-dispatch-execution.js.map +1 -1
  95. package/dist/src/terminal-dispatch-ledger-codec.d.ts +3 -0
  96. package/dist/src/terminal-dispatch-ledger-codec.js +46 -19
  97. package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -1
  98. package/dist/src/terminal-dispatch-presenter.js +5 -3
  99. package/dist/src/terminal-dispatch-presenter.js.map +1 -1
  100. package/dist/src/terminal-dispatch-receipt.d.ts +3 -0
  101. package/dist/src/terminal-dispatch-receipt.js +15 -1
  102. package/dist/src/terminal-dispatch-receipt.js.map +1 -1
  103. package/dist/src/terminal-dispatch-recovery-cli-adapter.js +68 -7
  104. package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -1
  105. package/dist/src/terminal-dispatch-recovery-service.d.ts +2 -0
  106. package/dist/src/terminal-dispatch-recovery-service.js +9 -0
  107. package/dist/src/terminal-dispatch-recovery-service.js.map +1 -1
  108. package/dist/src/terminal-dispatch-repository-cli-adapter.d.ts +4 -2
  109. package/dist/src/terminal-dispatch-repository-cli-adapter.js +10 -3
  110. package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -1
  111. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +2 -0
  112. package/dist/src/terminal-identity-authority-cli-adapter.js +34 -27
  113. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  114. package/dist/src/terminal-list-cli-adapter.d.ts +6 -0
  115. package/dist/src/terminal-list-cli-adapter.js +391 -177
  116. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  117. package/dist/src/terminal-list-renderer.d.ts +9 -1
  118. package/dist/src/terminal-list-renderer.js +226 -104
  119. package/dist/src/terminal-list-renderer.js.map +1 -1
  120. package/dist/src/terminal-maintenance-cli-adapter.js +253 -326
  121. package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -1
  122. package/dist/src/terminal-monitor-application-service.d.ts +5 -0
  123. package/dist/src/terminal-monitor-application-service.js +5 -0
  124. package/dist/src/terminal-monitor-application-service.js.map +1 -1
  125. package/dist/src/terminal-monitor-cli-adapter.d.ts +25 -1
  126. package/dist/src/terminal-monitor-cli-adapter.js +170 -27
  127. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  128. package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +1 -1
  129. package/dist/src/terminal-monitor-reconciliation-eligibility.js +220 -32
  130. package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -1
  131. package/dist/src/terminal-monitor-state-cli-adapter.d.ts +1 -1
  132. package/dist/src/terminal-monitor-state-cli-adapter.js +808 -147
  133. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  134. package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +2 -1
  135. package/dist/src/terminal-monitor-state-reconciliation-service.js +12 -0
  136. package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -1
  137. package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +2 -0
  138. package/dist/src/terminal-monitor-supervision-cli-adapter.js +31 -11
  139. package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -1
  140. package/dist/src/terminal-process-source.d.ts +13 -0
  141. package/dist/src/terminal-process-source.js +37 -0
  142. package/dist/src/terminal-process-source.js.map +1 -1
  143. package/dist/src/terminal-runtime-cli-adapter.js +14 -2
  144. package/dist/src/terminal-runtime-cli-adapter.js.map +1 -1
  145. package/dist/src/terminal-status-cli-adapter.d.ts +1 -0
  146. package/dist/src/terminal-status-cli-adapter.js +30 -28
  147. package/dist/src/terminal-status-cli-adapter.js.map +1 -1
  148. package/dist/src/terminal-submission-acceptance.js +78 -25
  149. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  150. package/dist/src/terminal-submission-retry-service.d.ts +114 -0
  151. package/dist/src/terminal-submission-retry-service.js +589 -0
  152. package/dist/src/terminal-submission-retry-service.js.map +1 -0
  153. package/dist/src/terminal-user-send-intent.d.ts +65 -0
  154. package/dist/src/terminal-user-send-intent.js +317 -0
  155. package/dist/src/terminal-user-send-intent.js.map +1 -0
  156. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +10 -2
  157. package/dist/src/terminal-watch-callback-cli-adapter.js +76 -37
  158. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -1
  159. package/dist/src/terminal-watch-cli-adapter.js +41 -17
  160. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  161. package/dist/src/terminal-watch-service.d.ts +11 -6
  162. package/dist/src/terminal-watch-service.js +210 -41
  163. package/dist/src/terminal-watch-service.js.map +1 -1
  164. package/dist/src/terminal-watch-store.d.ts +23 -0
  165. package/dist/src/terminal-watch-store.js +162 -4
  166. package/dist/src/terminal-watch-store.js.map +1 -1
  167. package/docs/quickstart-herdr.md +1 -1
  168. package/docs/quickstart-tmux.md +3 -3
  169. package/package.json +1 -1
  170. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +14 -11
@@ -1,10 +1,9 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
- import { classifyCallbackProcessFailure } from "./callback-outbox-policy.js";
4
3
  import { validateCodexRolloutAcceptanceAnchor } from "./terminal-submission-acceptance.js";
5
- import { listDeferredForegroundTransfers, loadDeferredForegroundTransfer } from "./deferred-foreground-transfer.js";
4
+ import { deferredForegroundActiveEnterDispatchedAt, deferredForegroundActiveMessageId, deferredForegroundActivePreparedAt, deferredForegroundActiveTextInjectedAt, isDeferredForegroundSubmissionRetryPending, listDeferredForegroundTransfers, loadDeferredForegroundTransfer, saveDeferredForegroundTransfer } from "./deferred-foreground-transfer.js";
6
5
  import { isFinalDeferredForegroundTransferStatus } from "./deferred-foreground-transfer-policy.js";
7
- import { executorForConversation, createMessage, isSessionSendBlockingStatus, isWaitingForAgentStatus, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
6
+ import { executorForConversation, createMessage, effectiveTurnStatus, isExplicitUserAbandonedManagementTurn, isSessionSendBlockingStatus, isTerminalDispatchOwnerReleasedStatus, isWaitingForAgentStatus, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
8
7
  import { executorDefinitionForKind } from "./executors.js";
9
8
  import { redactString } from "./runtime-log.js";
10
9
  import { appendEvent, listConversations, loadState, logPathForStatePath, pathsForConversationDir, saveState, statePathForConversationId, withStoreWriterLease, withStoreWriterLeaseAsync } from "./store.js";
@@ -14,12 +13,18 @@ import { cliEnv, cliExit, cliNow, cliNowMs, cliPid, cliRuntimeLog, cliSleepSync
14
13
  import { expandHome } from "./cli-command-runtime.js";
15
14
  import { terminalControlFromTakeover } from "./terminal-runtime-cli-adapter.js";
16
15
  import { terminalControlsShareIncarnation } from "./terminal-authority-policy.js";
17
- import { applyTerminalBridgeSubmission, terminalBridgeEnabled, terminalBridgeRequestFingerprint, terminalBridgeSubmission } from "./terminal-dispatch-receipt.js";
18
- import { terminalMonitorActivityPersistIntervalMs, terminalMonitorApprovalCandidate, validTerminalMonitorTimestampMs } from "./terminal-monitor-decision-policy.js";
16
+ import { terminalControlEvidenceMatches } from "./terminal-control-ref.js";
17
+ import { applyTerminalBridgeSubmission, terminalAcceptanceEvidenceForConversation, terminalBridgeEnabled, terminalBridgeRequestFingerprint, terminalBridgeSubmission } from "./terminal-dispatch-receipt.js";
18
+ import { terminalMonitorActivityPersistIntervalMs, terminalMonitorApprovalCandidate, terminalMonitorDeadlineAt, validTerminalMonitorTimestampMs } from "./terminal-monitor-decision-policy.js";
19
19
  import { runTerminalMonitor as runTerminalMonitorService } from "./terminal-monitor-application-service.js";
20
- import { pollTerminalMonitor, presentTerminalMonitor, reconcileMonitorAcceptance, recordMonitorApprovalNotification, recoverPreparedMonitorSubmission, terminalMonitorStoreLeaseTimeout, terminalMonitorStoreOperationTimeout } from "./terminal-monitor-cli-adapter.js";
20
+ import { pollTerminalMonitor, presentTerminalMonitor, reconcileMonitorAcceptance, recordMonitorApprovalNotification, repairLaggingAcceptedMonitorAuthority, recoverPreparedMonitorSubmission, terminalMonitorStoreLeaseTimeout, terminalMonitorStoreOperationTimeout } from "./terminal-monitor-cli-adapter.js";
21
21
  import { terminalMonitorReconciliationEligibility } from "./terminal-monitor-reconciliation-eligibility.js";
22
22
  import { reconcileTerminalMonitorStateCandidate } from "./terminal-monitor-state-reconciliation-service.js";
23
+ import { sameCanonicalStatePath } from "./terminal-dispatch-ledger-codec.js";
24
+ import { terminalSubmissionPayload } from "./terminal-dispatch-execution.js";
25
+ import { loadTerminalSubmissionRetry, decideTerminalSubmissionRetryStartup, projectTerminalSubmissionRetryPending, saveTerminalSubmissionRetry, terminalSubmissionRetryLedgerFields } from "./terminal-submission-retry-service.js";
26
+ import { callbackExpectedForConversation, callbackRouteFingerprintForConversation } from "./callback-route-authority.js";
27
+ import { canonicalJson } from "./canonical-json.js";
23
28
  import { isRecord, nonBlankString } from "./value-guards.js";
24
29
  const COLLATERAL_STALL_REASON = "a newer terminal submission has an uncertain outcome; inspect the shared terminal pane before continuing";
25
30
  /** Invocation-local monitor state and reconciliation CLI boundary. */
@@ -151,11 +156,20 @@ class TerminalMonitorStateCliApplication {
151
156
  })
152
157
  },
153
158
  callbacks: {
154
- reconcile: (storeDir, paths, delayMs) => withStoreWriterLease(storeDir, () => this.#dependencies.callbacks.reconcileDelivery({
155
- statePath: paths.statePath,
156
- logPath: paths.logPath,
157
- delayMs
158
- })),
159
+ reconcile: (storeDir, paths, delayMs) => withStoreWriterLease(storeDir, () => {
160
+ const lifecycle = this.#dependencies.callbacks.reconcileDelivery({
161
+ statePath: paths.statePath,
162
+ logPath: paths.logPath,
163
+ delayMs
164
+ });
165
+ const notification = this.#dependencies.callbacks.reconcileDelivery({
166
+ statePath: paths.statePath,
167
+ logPath: paths.logPath,
168
+ delayMs,
169
+ callbackOutboxLane: "notification"
170
+ });
171
+ return lifecycle.handled ? lifecycle : notification;
172
+ }),
159
173
  run: (prepared, callbackOptions) => this.#dependencies.callbacks.runPrepared(prepared, callbackOptions)
160
174
  },
161
175
  authority: {
@@ -166,6 +180,7 @@ class TerminalMonitorStateCliApplication {
166
180
  logPath: paths.logPath,
167
181
  options
168
182
  }),
183
+ recoverSubmissionRetry: (storeDir, conversation, paths) => this.#recoverSubmissionRetry(storeDir, conversation, paths),
169
184
  recoverDeferred: (storeDir, conversation, paths) => this.#recoverDeferred(options, storeDir, conversation, paths),
170
185
  recoverVirgin: async (conversation, paths) => (await this.#dependencies.acceptance.recoverVirgin({
171
186
  options,
@@ -186,38 +201,282 @@ class TerminalMonitorStateCliApplication {
186
201
  }
187
202
  };
188
203
  }
204
+ async #recoverSubmissionRetry(storeDir, initialConversation, paths) {
205
+ const initialAttempt = loadTerminalSubmissionRetry(paths.statePath);
206
+ if (!initialAttempt) {
207
+ return initialConversation;
208
+ }
209
+ const initialTakeover = takeoverFor(initialConversation);
210
+ const initialControl = terminalControlFromTakeover(initialTakeover);
211
+ if (executorForConversation(initialConversation).kind !== "codex" ||
212
+ !initialControl) {
213
+ throw new Error("submission retry startup recovery lost its exact Codex terminal authority");
214
+ }
215
+ const canonical = pathsForConversationDir(path.dirname(paths.statePath));
216
+ if (path.resolve(canonical.storeDir) !== path.resolve(storeDir) ||
217
+ path.resolve(canonical.statePath) !== path.resolve(paths.statePath) ||
218
+ path.resolve(canonical.logPath) !== path.resolve(paths.logPath)) {
219
+ throw new Error("submission retry startup recovery is outside its canonical Turn Store");
220
+ }
221
+ const releaseTerminal = this.#dependencies.dispatch.repository.acquire(storeDir, initialControl, { timeoutMs: 30000 });
222
+ try {
223
+ return await withStoreWriterLeaseAsync(storeDir, async () => {
224
+ const releaseState = this.#stateFileLock.acquire(`${paths.statePath}.lock`);
225
+ try {
226
+ let conversation = loadState(paths.statePath);
227
+ const takeover = takeoverFor(conversation);
228
+ const control = terminalControlFromTakeover(takeover);
229
+ const attempt = loadTerminalSubmissionRetry(paths.statePath);
230
+ if (!attempt) {
231
+ return conversation;
232
+ }
233
+ if (executorForConversation(conversation).kind !== "codex" ||
234
+ !control ||
235
+ !terminalControlsShareIncarnation(control, initialControl)) {
236
+ throw new Error("submission retry startup recovery terminal authority changed");
237
+ }
238
+ const callbackDelivery = isRecord(conversation.callback_delivery)
239
+ ? conversation.callback_delivery
240
+ : undefined;
241
+ if (callbackDelivery?.status === "delivered" ||
242
+ isTerminalDispatchOwnerReleasedStatus(effectiveTurnStatus(conversation))) {
243
+ return conversation;
244
+ }
245
+ const submission = terminalBridgeSubmission(conversation);
246
+ const ledger = this.#dependencies.dispatch.repository.load(control);
247
+ if (!submission || !ledger) {
248
+ throw new Error("submission retry startup recovery lacks its Turn or ledger receipt");
249
+ }
250
+ if (!["agent_accepted"].includes(String(submission.status)) &&
251
+ !["agent_accepted"].includes(String(ledger.status)) &&
252
+ !["enter_dispatched", "agent_accepted"].includes(attempt.state)) {
253
+ return conversation;
254
+ }
255
+ const authority = assertTerminalSubmissionRetryStartupAuthority({
256
+ storeDir,
257
+ paths,
258
+ conversation,
259
+ takeover,
260
+ submission,
261
+ ledger,
262
+ attempt,
263
+ control,
264
+ ledgerMatchesControl: this.#dependencies.dispatch.repository
265
+ .matchesControl(ledger, control)
266
+ });
267
+ const startup = decideTerminalSubmissionRetryStartup({
268
+ attempt,
269
+ submission,
270
+ ledger
271
+ });
272
+ if (startup.action === "refuse") {
273
+ throw new Error(startup.reason);
274
+ }
275
+ if (startup.action === "finalize_accepted") {
276
+ return finalizeTerminalSubmissionRetryStartupAccepted({
277
+ storeDir,
278
+ paths,
279
+ conversation,
280
+ submission,
281
+ ledger,
282
+ attempt,
283
+ control,
284
+ authority,
285
+ saveLedger: (next) => this.#dependencies.dispatch.repository
286
+ .save(control, next)
287
+ });
288
+ }
289
+ if (startup.action === "repair_terminal_ledger") {
290
+ reconcileTerminalSubmissionRetryStartupTerminalLedger({
291
+ paths,
292
+ conversation,
293
+ ledger,
294
+ attempt,
295
+ startup,
296
+ saveLedger: (next) => this.#dependencies.dispatch.repository
297
+ .save(control, next)
298
+ });
299
+ return conversation;
300
+ }
301
+ if (startup.action === "repair_terminal_state") {
302
+ return reconcileTerminalSubmissionRetryStartupTerminalState({
303
+ storeDir,
304
+ paths,
305
+ conversation,
306
+ submission,
307
+ attempt,
308
+ control,
309
+ authority,
310
+ startup
311
+ });
312
+ }
313
+ if (startup.action === "no_change")
314
+ return conversation;
315
+ const projection = authority.projection;
316
+ if (!projection) {
317
+ return conversation;
318
+ }
319
+ const receiptAlreadyPending = submission.status === "enter_dispatched";
320
+ if (receiptAlreadyPending
321
+ ? conversation.status !== "waiting_for_agent"
322
+ : !["stalled", "waiting_for_agent"].includes(String(conversation.status))) {
323
+ return conversation;
324
+ }
325
+ let repairedTransfer = false;
326
+ if (authority.deferredTransferId) {
327
+ repairedTransfer = mirrorDeferredSubmissionRetryEnter({
328
+ storeDir,
329
+ transferId: authority.deferredTransferId,
330
+ attempt,
331
+ messageId: projection.messageId,
332
+ preparedAt: projection.preparedAt,
333
+ textInjectedAt: projection.textInjectedAt,
334
+ requestHash: authority.requestHash,
335
+ sessionId: sessionIdForConversation(conversation),
336
+ turnId: turnIdForConversation(conversation),
337
+ statePath: paths.statePath
338
+ });
339
+ }
340
+ const stateNeedsRepair = !receiptAlreadyPending;
341
+ if (receiptAlreadyPending && !terminalSubmissionRetryStateIsPending({
342
+ conversation,
343
+ submission,
344
+ projection
345
+ })) {
346
+ return conversation;
347
+ }
348
+ if (stateNeedsRepair) {
349
+ const epochConversation = terminalSubmissionRetryMonitorEpoch(conversation, projection.enterDispatchedAt);
350
+ conversation = applyTerminalBridgeSubmission({
351
+ conversation: epochConversation,
352
+ messageId: projection.messageId,
353
+ messageType: authority.messageType,
354
+ requestText: authority.requestText,
355
+ status: "enter_dispatched",
356
+ preparedAt: projection.preparedAt,
357
+ textInjectedAt: projection.textInjectedAt,
358
+ enterDispatchedAt: projection.enterDispatchedAt,
359
+ lastProvenStage: "enter_dispatched"
360
+ }, {
361
+ dispatcherPid: cliPid(),
362
+ storeDir,
363
+ terminalControl: control
364
+ });
365
+ saveState(paths.statePath, conversation);
366
+ }
367
+ const ledgerNeedsRepair = ledger.status !== "enter_dispatched" ||
368
+ nonBlankString(ledger.text_injected_at) !==
369
+ projection.textInjectedAt ||
370
+ nonBlankString(ledger.enter_dispatched_at) !==
371
+ projection.enterDispatchedAt ||
372
+ ledger.submission_retry_state !== "enter_dispatched" ||
373
+ ledger.submission_retry_revision !== attempt.revision;
374
+ if (ledgerNeedsRepair) {
375
+ this.#dependencies.dispatch.repository.save(control, {
376
+ ...ledger,
377
+ ...terminalSubmissionRetryLedgerFields(attempt),
378
+ status: "enter_dispatched",
379
+ text_injected_at: projection.textInjectedAt,
380
+ enter_dispatched_at: projection.enterDispatchedAt,
381
+ enter_not_attempted_at: undefined,
382
+ enter_not_attempted_reason: undefined,
383
+ uncertain_at: undefined,
384
+ safe_to_retry: undefined,
385
+ acceptance_evidence: undefined,
386
+ agent_accepted_at: undefined,
387
+ not_accepted_at: undefined,
388
+ dispatcher_pid: null
389
+ });
390
+ }
391
+ if (repairedTransfer || stateNeedsRepair || ledgerNeedsRepair) {
392
+ appendEvent(paths.logPath, {
393
+ ts: projection.enterDispatchedAt,
394
+ conversation_id: conversation.conversation_id,
395
+ event: "terminal_submission_retry_startup_reconciled",
396
+ message_id: projection.messageId,
397
+ terminal_input_sent: false
398
+ });
399
+ }
400
+ return loadState(paths.statePath);
401
+ }
402
+ finally {
403
+ releaseState();
404
+ }
405
+ });
406
+ }
407
+ finally {
408
+ releaseTerminal();
409
+ }
410
+ }
189
411
  async #recoverDeferred(options, storeDir, initialConversation, paths) {
412
+ if (isExplicitUserAbandonedManagementTurn(initialConversation)) {
413
+ return initialConversation;
414
+ }
190
415
  const takeover = takeoverFor(initialConversation);
191
416
  const transferId = nonBlankString(takeover?.deferred_foreground_transfer_id);
192
417
  if (!transferId) {
193
418
  return initialConversation;
194
419
  }
195
- const transfer = loadDeferredForegroundTransfer(storeDir, transferId);
196
- if (isFinalDeferredForegroundTransferStatus(transfer.status)) {
197
- return initialConversation;
198
- }
199
- const control = terminalControlFromTakeover(takeover);
200
- const pid = Number(takeover?.terminal_agent_pid);
201
- if (!control || !Number.isSafeInteger(pid) || pid <= 1) {
420
+ const initialControl = terminalControlFromTakeover(takeover);
421
+ const initialPid = Number(takeover?.terminal_agent_pid);
422
+ if (!initialControl ||
423
+ !Number.isSafeInteger(initialPid) ||
424
+ initialPid <= 1) {
202
425
  throw new Error(`deferred foreground Turn ${initialConversation.conversation_id} ` +
203
426
  "lost its exact terminal process authority");
204
427
  }
205
- const terminal = await this.#dependencies.authority.createBridge(options)
206
- .resolveStoredTerminal("codex", pid, control, { pid });
207
- const release = this.#dependencies.dispatch.repository.acquire(storeDir, control, { timeoutMs: 30000 });
428
+ const releaseTerminal = this.#dependencies.dispatch.repository.acquire(storeDir, initialControl, { timeoutMs: 30000 });
208
429
  try {
209
- await this.#dependencies.authority.handoff
210
- .recoverDeferredCodexForegroundTransferBeforeMutation({
211
- options,
212
- terminal: terminal
430
+ return await withStoreWriterLeaseAsync(storeDir, async () => {
431
+ const releaseState = this.#stateFileLock.acquire(`${paths.statePath}.lock`);
432
+ let conversation;
433
+ try {
434
+ conversation = loadState(paths.statePath);
435
+ if (isExplicitUserAbandonedManagementTurn(conversation)) {
436
+ return conversation;
437
+ }
438
+ }
439
+ finally {
440
+ releaseState();
441
+ }
442
+ const currentTakeover = takeoverFor(conversation);
443
+ const currentTransferId = nonBlankString(currentTakeover.deferred_foreground_transfer_id);
444
+ const currentControl = terminalControlFromTakeover(currentTakeover);
445
+ const currentPid = Number(currentTakeover.terminal_agent_pid);
446
+ if (currentTransferId !== transferId ||
447
+ !currentControl ||
448
+ !terminalControlsShareIncarnation(currentControl, initialControl) ||
449
+ !Number.isSafeInteger(currentPid) ||
450
+ currentPid !== initialPid) {
451
+ throw new Error(`deferred foreground Turn ${conversation.conversation_id} ` +
452
+ "changed its exact terminal process authority during recovery");
453
+ }
454
+ const transfer = loadDeferredForegroundTransfer(storeDir, transferId);
455
+ if (isFinalDeferredForegroundTransferStatus(transfer.status)) {
456
+ return conversation;
457
+ }
458
+ const terminal = await this.#dependencies.authority
459
+ .createBridge(options)
460
+ .resolveStoredTerminal("codex", currentPid, currentControl, { pid: currentPid });
461
+ await this.#dependencies.authority.handoff
462
+ .recoverDeferredCodexForegroundTransferBeforeMutation({
463
+ options,
464
+ terminal: terminal
465
+ });
466
+ const releaseFinalState = this.#stateFileLock.acquire(`${paths.statePath}.lock`);
467
+ try {
468
+ conversation = loadState(paths.statePath);
469
+ this.#assertExactPendingDeferred(conversation, storeDir, transferId);
470
+ return conversation;
471
+ }
472
+ finally {
473
+ releaseFinalState();
474
+ }
213
475
  });
214
476
  }
215
477
  finally {
216
- release();
478
+ releaseTerminal();
217
479
  }
218
- const conversation = loadState(paths.statePath);
219
- this.#assertExactPendingDeferred(conversation, storeDir, transferId);
220
- return conversation;
221
480
  }
222
481
  #assertExactPendingDeferred(conversation, storeDir, transferId) {
223
482
  const transfer = loadDeferredForegroundTransfer(storeDir, transferId);
@@ -227,45 +486,59 @@ class TerminalMonitorStateCliApplication {
227
486
  const takeover = takeoverFor(conversation);
228
487
  const submission = terminalBridgeSubmission(conversation);
229
488
  const anchor = validateCodexRolloutAcceptanceAnchor(takeover?.codex_rollout_acceptance_anchor);
489
+ const activeMessageId = deferredForegroundActiveMessageId(transfer);
490
+ const exactRetryPending = isDeferredForegroundSubmissionRetryPending(transfer) &&
491
+ activeMessageId === transfer.submission_retry_message_id &&
492
+ nonBlankString(submission?.prepared_at) ===
493
+ deferredForegroundActivePreparedAt(transfer) &&
494
+ nonBlankString(submission?.text_injected_at) ===
495
+ deferredForegroundActiveTextInjectedAt(transfer) &&
496
+ nonBlankString(submission?.enter_dispatched_at) ===
497
+ deferredForegroundActiveEnterDispatchedAt(transfer);
230
498
  if (anchor?.version !== 3 ||
231
499
  conversation.status !== "waiting_for_agent" ||
232
500
  submission?.status !== "enter_dispatched" ||
233
501
  nonBlankString(submission.message_id) !==
234
502
  nonBlankString(takeover?.terminal_bridge_message_id) ||
235
- transfer.status !== "dispatch_started") {
503
+ nonBlankString(submission.message_id) !== activeMessageId ||
504
+ !(transfer.status === "dispatch_started" || exactRetryPending)) {
236
505
  throw new Error(`deferred foreground Turn ${conversation.conversation_id} ` +
237
506
  "is not an exact pending candidate-set acceptance");
238
507
  }
239
508
  }
240
509
  prepareLaunch(input) {
241
- const release = this.#stateFileLock.acquire(`${input.statePath}.lock`);
242
- try {
243
- const conversation = loadState(input.statePath);
244
- if (input.requireWaitingForAgentStatus &&
245
- conversation.status !== "waiting_for_agent") {
246
- return unprepared(`conversation_status_${String(conversation.status ?? "missing")}`);
247
- }
248
- const eligibility = this.eligibility(conversation);
249
- if (!eligibility.eligible) {
250
- return unprepared(eligibility.reason);
251
- }
252
- if (eligibility.terminalMessageId !== input.expectedMessageId) {
253
- return unprepared("terminal_bridge_task_replaced");
510
+ const storeDir = pathsForConversationDir(path.dirname(input.statePath))
511
+ .storeDir;
512
+ return withStoreWriterLease(storeDir, () => {
513
+ const release = this.#stateFileLock.acquire(`${input.statePath}.lock`);
514
+ try {
515
+ const conversation = loadState(input.statePath);
516
+ if (input.requireWaitingForAgentStatus &&
517
+ conversation.status !== "waiting_for_agent") {
518
+ return unprepared(`conversation_status_${String(conversation.status ?? "missing")}`);
519
+ }
520
+ const eligibility = this.eligibility(conversation);
521
+ if (!eligibility.eligible) {
522
+ return unprepared(eligibility.reason);
523
+ }
524
+ if (eligibility.terminalMessageId !== input.expectedMessageId) {
525
+ return unprepared("terminal_bridge_task_replaced");
526
+ }
527
+ const owner = input.activeOwner(input.statePath, eligibility.terminalMessageId);
528
+ if (owner) {
529
+ return {
530
+ prepared: false,
531
+ alreadyRunning: true,
532
+ reason: "monitor_lock_owner_alive",
533
+ ownerPid: owner.ownerPid
534
+ };
535
+ }
536
+ return this.#persistMonitorLockVersion(conversation, eligibility, input.monitorLockVersion, input.statePath);
254
537
  }
255
- const owner = input.activeOwner(input.statePath, eligibility.terminalMessageId);
256
- if (owner) {
257
- return {
258
- prepared: false,
259
- alreadyRunning: true,
260
- reason: "monitor_lock_owner_alive",
261
- ownerPid: owner.ownerPid
262
- };
538
+ finally {
539
+ release();
263
540
  }
264
- return this.#persistMonitorLockVersion(conversation, eligibility, input.monitorLockVersion, input.statePath);
265
- }
266
- finally {
267
- release();
268
- }
541
+ });
269
542
  }
270
543
  #persistMonitorLockVersion(conversation, eligibility, lockVersion, statePath) {
271
544
  const needsSave = eligibility.nativeTakeover.terminal_bridge_monitor_lock_version !==
@@ -401,6 +674,18 @@ class TerminalMonitorStateCliApplication {
401
674
  }
402
675
  }
403
676
  }),
677
+ repairLaggingAcceptedAuthority: (request) => repairLaggingAcceptedMonitorAuthority({
678
+ ...request,
679
+ ports: {
680
+ acquireTerminal: (control) => this.#dependencies.dispatch.repository.acquire(commandStoreDir(), control, { timeoutMs: 30000 }),
681
+ withWriter: (use) => withStoreWriterLeaseAsync(stateStoreDir(), use),
682
+ acquireState: () => this.#stateFileLock.acquire(`${input.statePath}.lock`),
683
+ loadConversation: () => loadState(input.statePath),
684
+ loadLedger: this.#dependencies.dispatch.repository.load,
685
+ reconcileLedger: this.#dependencies.dispatch.recovery.reconcilePrepared,
686
+ submission: terminalBridgeSubmission
687
+ }
688
+ }),
404
689
  assertBindingCurrent: (conversation) => this.#dependencies.authority.assertBindingCurrent(conversation, "monitor"),
405
690
  bindingSuperseded: this.#dependencies.runtime.bindingSuperseded,
406
691
  storeOperationTimeout: terminalMonitorStoreOperationTimeout,
@@ -411,6 +696,8 @@ class TerminalMonitorStateCliApplication {
411
696
  scrollbackLines: Number(input.options.scrollbackLines ?? 120),
412
697
  ports: {
413
698
  acquireTerminal: (control) => this.#dependencies.dispatch.repository.acquire(commandStoreDir(), control, { timeoutMs: 30000 }),
699
+ withWriter: (use) => withStoreWriterLeaseAsync(stateStoreDir(), use),
700
+ acquireState: () => this.#stateFileLock.acquire(`${input.statePath}.lock`),
414
701
  reconcileLedger: this.#dependencies.dispatch.recovery.reconcilePrepared,
415
702
  loadLedger: this.#dependencies.dispatch.repository.load,
416
703
  saveLedger: this.#dependencies.dispatch.repository.save,
@@ -1187,7 +1474,7 @@ class TerminalMonitorStateCliApplication {
1187
1474
  const storeDir = pathsForConversationDir(path.dirname(input.statePath))
1188
1475
  .storeDir;
1189
1476
  let stalledConversation;
1190
- let stalledMessage;
1477
+ let stalledNotification;
1191
1478
  let unchangedConversation;
1192
1479
  withStoreWriterLease(storeDir, () => {
1193
1480
  const release = this.#stateFileLock.acquire(`${input.statePath}.lock`);
@@ -1204,7 +1491,7 @@ class TerminalMonitorStateCliApplication {
1204
1491
  }
1205
1492
  const stalled = this.#stalledState(conversation, input);
1206
1493
  stalledConversation = stalled.conversation;
1207
- stalledMessage = stalled.message;
1494
+ stalledNotification = stalled.prepared;
1208
1495
  }
1209
1496
  finally {
1210
1497
  release();
@@ -1213,13 +1500,18 @@ class TerminalMonitorStateCliApplication {
1213
1500
  if (unchangedConversation) {
1214
1501
  return unchangedConversation;
1215
1502
  }
1216
- if (stalledConversation && stalledMessage) {
1217
- this.#deliverStalledNotification({
1218
- statePath: input.statePath,
1219
- logPath: input.logPath,
1220
- conversation: stalledConversation,
1221
- message: stalledMessage
1222
- });
1503
+ if (stalledNotification) {
1504
+ try {
1505
+ stalledConversation = this.#dependencies.callbacks.runPrepared(stalledNotification, { emit: false }).conversation;
1506
+ }
1507
+ catch (error) {
1508
+ stalledConversation = loadState(input.statePath);
1509
+ cliRuntimeLog("warn", "stalled_notification_delivery_failed", {
1510
+ conversation_id: stalledConversation.conversation_id,
1511
+ message_id: stalledNotification.message.id,
1512
+ error: error instanceof Error ? error.message : String(error)
1513
+ });
1514
+ }
1223
1515
  }
1224
1516
  return stalledConversation;
1225
1517
  }
@@ -1227,7 +1519,10 @@ class TerminalMonitorStateCliApplication {
1227
1519
  const now = cliNow().toISOString();
1228
1520
  const executor = executorForConversation(conversation);
1229
1521
  const terminalBridge = terminalBridgeEnabled(conversation);
1230
- const shouldNotify = Boolean(conversation.gateway_method && !conversation.stalled_notification_sent_at);
1522
+ const notificationDelivery = isRecord(conversation.callback_notification_delivery) ? conversation.callback_notification_delivery : undefined;
1523
+ const shouldNotify = callbackExpectedForConversation(conversation) &&
1524
+ !conversation.stalled_notification_sent_at &&
1525
+ !["pending", "failed"].includes(String(notificationDelivery?.status ?? ""));
1231
1526
  const message = shouldNotify
1232
1527
  ? stalledMessage(conversation, input.reason, executor, terminalBridge)
1233
1528
  : undefined;
@@ -1236,13 +1531,22 @@ class TerminalMonitorStateCliApplication {
1236
1531
  status: "stalled",
1237
1532
  stalled_at: now,
1238
1533
  stalled_reason: input.reason,
1239
- stalled_notification_sent_at: shouldNotify
1240
- ? now
1241
- : conversation.stalled_notification_sent_at,
1242
1534
  stalled_notification_message_id: message?.id ?? conversation.stalled_notification_message_id,
1243
1535
  updated_at: now
1244
1536
  };
1245
- saveState(input.statePath, nextConversation);
1537
+ const prepared = message
1538
+ ? this.#dependencies.callbacks.prepareStallNotification({
1539
+ options: { statePath: input.statePath },
1540
+ statePath: input.statePath,
1541
+ logPath: input.logPath,
1542
+ conversation: nextConversation,
1543
+ message
1544
+ }).prepared
1545
+ : undefined;
1546
+ const persistedConversation = prepared?.conversation ?? nextConversation;
1547
+ if (!prepared) {
1548
+ saveState(input.statePath, persistedConversation);
1549
+ }
1246
1550
  appendEvent(input.logPath, {
1247
1551
  ts: now,
1248
1552
  conversation_id: conversation.conversation_id,
@@ -1260,45 +1564,7 @@ class TerminalMonitorStateCliApplication {
1260
1564
  reason: input.reason,
1261
1565
  ...input.detail
1262
1566
  });
1263
- return { conversation: nextConversation, message };
1264
- }
1265
- #deliverStalledNotification(input) {
1266
- if (!input.conversation.gateway_method) {
1267
- return;
1268
- }
1269
- const eventPrefix = input.eventPrefix ?? "stalled";
1270
- const token = input.conversation.gateway_token;
1271
- const gatewayUrl = token ? input.conversation.gateway_url : undefined;
1272
- const delivery = this.#dependencies.callbacks.deliverGatewayMethod({
1273
- method: input.conversation.gateway_method,
1274
- openclawBin: input.conversation.openclaw_bin,
1275
- gatewayUrl,
1276
- token,
1277
- sessionKey: input.conversation.gateway_session ??
1278
- input.conversation.openclaw_session,
1279
- statePath: input.statePath,
1280
- logPath: input.logPath,
1281
- conversation: input.conversation,
1282
- message: input.message
1283
- });
1284
- recordStalledDelivery(input, eventPrefix, "gateway_method_delivery", delivery, true);
1285
- if (delivery.status !== 0) {
1286
- return;
1287
- }
1288
- const payload = parseOptionalJson(delivery.stdout);
1289
- const chatSend = isRecord(payload?.chat_send)
1290
- ? payload.chat_send
1291
- : undefined;
1292
- if (!chatSend) {
1293
- return;
1294
- }
1295
- const chatDelivery = this.#dependencies.callbacks.deliverChatSend({
1296
- openclawBin: input.conversation.openclaw_bin,
1297
- gatewayUrl,
1298
- token,
1299
- params: chatSend
1300
- });
1301
- recordStalledDelivery(input, eventPrefix, "chat_send_delivery", chatDelivery, false);
1567
+ return { conversation: persistedConversation, prepared };
1302
1568
  }
1303
1569
  }
1304
1570
  function monitorRuntimePort() {
@@ -1312,6 +1578,438 @@ function monitorRuntimePort() {
1312
1578
  exit: cliExit
1313
1579
  };
1314
1580
  }
1581
+ function terminalSubmissionRetryStartupAuthorityContext(input) {
1582
+ const sessionId = sessionIdForConversation(input.conversation);
1583
+ const turnId = turnIdForConversation(input.conversation);
1584
+ const messageId = nonBlankString(input.submission.message_id);
1585
+ const requestText = terminalSubmissionPayload(nonBlankString(input.takeover.terminal_bridge_request_text) ?? "");
1586
+ const requestHash = terminalBridgeRequestFingerprint(requestText);
1587
+ const messageType = nonBlankString(input.submission.message_type);
1588
+ const messageBodyHash = createHash("sha256")
1589
+ .update(requestText)
1590
+ .digest("hex");
1591
+ const callbackFingerprint = callbackRouteFingerprintForConversation(input.conversation) ?? null;
1592
+ const deferredReferences = [
1593
+ nonBlankString(input.takeover.deferred_foreground_transfer_id),
1594
+ nonBlankString(input.submission.deferred_foreground_transfer_id),
1595
+ nonBlankString(input.ledger.deferred_foreground_transfer_id),
1596
+ input.attempt.deferred_foreground_transfer_id ?? undefined
1597
+ ];
1598
+ const presentDeferredReferences = deferredReferences.filter((value) => value !== undefined);
1599
+ const deferredTransferId = presentDeferredReferences[0];
1600
+ const submissionTerminalEvidence = input.submission.terminal_endpoint !== undefined
1601
+ ? input.submission.terminal_endpoint
1602
+ : input.submission.terminal_control;
1603
+ const statePath = nonBlankString(input.conversation.state_path);
1604
+ const logPath = nonBlankString(input.conversation.event_log_path);
1605
+ const bindingId = nonBlankString(input.conversation.terminal_binding_id);
1606
+ const bindingGeneration = Number(input.conversation.terminal_binding_generation);
1607
+ return {
1608
+ sessionId,
1609
+ turnId,
1610
+ messageId,
1611
+ requestText,
1612
+ requestHash,
1613
+ messageType,
1614
+ messageBodyHash,
1615
+ callbackFingerprint,
1616
+ deferredReferences,
1617
+ presentDeferredReferences,
1618
+ deferredTransferId,
1619
+ submissionTerminalEvidence,
1620
+ statePath,
1621
+ logPath,
1622
+ bindingId,
1623
+ bindingGeneration
1624
+ };
1625
+ }
1626
+ function terminalSubmissionRetryStartupIdentityMatches(input, context) {
1627
+ return !(!context.requestText || !context.requestHash || !context.messageId ||
1628
+ (context.messageType !== "task" && context.messageType !== "answer") ||
1629
+ nonBlankString(input.takeover.terminal_bridge_message_id) !==
1630
+ context.messageId ||
1631
+ input.attempt.session_id !== context.sessionId ||
1632
+ input.attempt.turn_id !== context.turnId ||
1633
+ input.attempt.original_message_id !== context.messageId ||
1634
+ input.attempt.active_message_id !== context.messageId ||
1635
+ input.attempt.request_hash !== context.requestHash ||
1636
+ input.attempt.terminal_target !== input.control.target ||
1637
+ input.attempt.callback_route_fingerprint !== context.callbackFingerprint);
1638
+ }
1639
+ function terminalSubmissionRetryStartupSubmissionPathMatches(input) {
1640
+ return path.resolve(nonBlankString(input.submission.store_dir) ?? "") ===
1641
+ path.resolve(input.storeDir);
1642
+ }
1643
+ function terminalSubmissionRetryStartupSubmissionMatches(input, context) {
1644
+ return !(nonBlankString(input.submission.session_id) !== context.sessionId ||
1645
+ nonBlankString(input.submission.turn_id) !== context.turnId ||
1646
+ nonBlankString(input.submission.request_hash) !== context.requestHash ||
1647
+ nonBlankString(input.submission.message_body_hash) !==
1648
+ context.messageBodyHash ||
1649
+ nonBlankString(input.submission.executor_kind) !== "codex" ||
1650
+ !terminalSubmissionRetryStartupSubmissionPathMatches(input) ||
1651
+ (input.submission.callback_route_fingerprint ?? null) !==
1652
+ context.callbackFingerprint ||
1653
+ !terminalControlEvidenceMatches(context.submissionTerminalEvidence, input.control));
1654
+ }
1655
+ function terminalSubmissionRetryStartupLedgerPathMatches(input, context) {
1656
+ return !(path.resolve(nonBlankString(input.ledger.store_dir) ?? "") !==
1657
+ path.resolve(input.storeDir) ||
1658
+ !sameCanonicalStatePath(input.ledger.state_path, input.paths.statePath) ||
1659
+ !sameCanonicalStatePath(context.statePath, input.paths.statePath) ||
1660
+ !sameCanonicalStatePath(input.ledger.event_log_path, input.paths.logPath) ||
1661
+ !sameCanonicalStatePath(context.logPath, input.paths.logPath));
1662
+ }
1663
+ function terminalSubmissionRetryStartupLedgerMatches(input, context) {
1664
+ return !(nonBlankString(input.ledger.generation_id) !== context.messageId ||
1665
+ nonBlankString(input.ledger.message_id) !== context.messageId ||
1666
+ nonBlankString(input.ledger.conversation_id) !==
1667
+ input.conversation.conversation_id ||
1668
+ nonBlankString(input.ledger.session_id) !== context.sessionId ||
1669
+ nonBlankString(input.ledger.turn_id) !== context.turnId ||
1670
+ nonBlankString(input.ledger.message_type) !== context.messageType ||
1671
+ nonBlankString(input.ledger.message_body_hash) !==
1672
+ context.messageBodyHash ||
1673
+ nonBlankString(input.ledger.executor_kind) !== "codex" ||
1674
+ nonBlankString(input.ledger.request_hash) !== context.requestHash ||
1675
+ !terminalSubmissionRetryStartupLedgerPathMatches(input, context) ||
1676
+ (input.ledger.callback_route_fingerprint ?? null) !==
1677
+ context.callbackFingerprint ||
1678
+ input.ledger.callback_expected !==
1679
+ callbackExpectedForConversation(input.conversation) ||
1680
+ !input.ledgerMatchesControl ||
1681
+ (context.bindingId !== undefined &&
1682
+ nonBlankString(input.ledger.binding_id) !== context.bindingId) ||
1683
+ (Number.isSafeInteger(context.bindingGeneration) &&
1684
+ Number(input.ledger.binding_generation) !== context.bindingGeneration));
1685
+ }
1686
+ function terminalSubmissionRetryStartupDeferredMatches(input, context) {
1687
+ return !((context.deferredTransferId !== undefined &&
1688
+ (context.presentDeferredReferences.length !==
1689
+ context.deferredReferences.length ||
1690
+ new Set(context.presentDeferredReferences).size !== 1)) ||
1691
+ (context.deferredTransferId === undefined &&
1692
+ input.attempt.deferred_foreground_transfer_id !== null));
1693
+ }
1694
+ function terminalSubmissionRetryStartupProjectionFields(input) {
1695
+ if (input.attempt.state === "agent_accepted" ||
1696
+ ["agent_accepted", "not_accepted"].includes(String(input.submission.status)) ||
1697
+ ["agent_accepted", "not_accepted"].includes(String(input.ledger.status))) {
1698
+ return {};
1699
+ }
1700
+ return {
1701
+ projection: projectTerminalSubmissionRetryPending({
1702
+ attempt: input.attempt,
1703
+ submission: input.submission,
1704
+ ledger: input.ledger
1705
+ })
1706
+ };
1707
+ }
1708
+ function assertTerminalSubmissionRetryStartupAuthority(input) {
1709
+ const context = terminalSubmissionRetryStartupAuthorityContext(input);
1710
+ if (!terminalSubmissionRetryStartupIdentityMatches(input, context) ||
1711
+ !terminalSubmissionRetryStartupSubmissionMatches(input, context) ||
1712
+ !terminalSubmissionRetryStartupLedgerMatches(input, context) ||
1713
+ !terminalSubmissionRetryStartupDeferredMatches(input, context)) {
1714
+ throw new Error("submission retry startup recovery authority disagrees across Turn and ledger");
1715
+ }
1716
+ return {
1717
+ requestText: context.requestText,
1718
+ requestHash: context.requestHash,
1719
+ messageType: context.messageType,
1720
+ deferredTransferId: context.deferredTransferId,
1721
+ ...terminalSubmissionRetryStartupProjectionFields(input)
1722
+ };
1723
+ }
1724
+ function finalizeTerminalSubmissionRetryStartupAccepted(input) {
1725
+ const stateAccepted = input.submission.status === "agent_accepted";
1726
+ const ledgerAccepted = input.ledger.status === "agent_accepted";
1727
+ if (!stateAccepted && !ledgerAccepted) {
1728
+ throw new Error("accepted submission retry sidecar lacks native acceptance evidence");
1729
+ }
1730
+ const stateEvidence = stateAccepted
1731
+ ? terminalAcceptanceEvidenceForConversation(input.conversation, input.authority.requestText, input.submission.acceptance_evidence)
1732
+ : undefined;
1733
+ const ledgerEvidence = ledgerAccepted
1734
+ ? terminalAcceptanceEvidenceForConversation(input.conversation, input.authority.requestText, input.ledger.acceptance_evidence)
1735
+ : undefined;
1736
+ if (stateEvidence && ledgerEvidence &&
1737
+ canonicalJson(stateEvidence) !== canonicalJson(ledgerEvidence)) {
1738
+ throw new Error("accepted submission retry evidence conflicts across Turn and ledger");
1739
+ }
1740
+ const evidence = stateEvidence ?? ledgerEvidence;
1741
+ const stateAcceptedAt = stateAccepted
1742
+ ? nonBlankString(input.submission.agent_accepted_at)
1743
+ : undefined;
1744
+ const ledgerAcceptedAt = ledgerAccepted
1745
+ ? nonBlankString(input.ledger.agent_accepted_at)
1746
+ : undefined;
1747
+ if (!evidence || !stateAcceptedAt && !ledgerAcceptedAt ||
1748
+ stateAcceptedAt && ledgerAcceptedAt && stateAcceptedAt !== ledgerAcceptedAt) {
1749
+ throw new Error("accepted submission retry timestamp conflicts across Turn and ledger");
1750
+ }
1751
+ const acceptedAt = (stateAcceptedAt ?? ledgerAcceptedAt);
1752
+ if (input.authority.deferredTransferId) {
1753
+ const transfer = loadDeferredForegroundTransfer(input.storeDir, input.authority.deferredTransferId);
1754
+ if (transfer.status !== "resolved") {
1755
+ if (isFinalDeferredForegroundTransferStatus(transfer.status)) {
1756
+ throw new Error("accepted submission retry conflicts with an aborted deferred transfer");
1757
+ }
1758
+ return input.conversation;
1759
+ }
1760
+ if (transfer.agent_accepted_at !== acceptedAt ||
1761
+ transfer.message_id !== input.attempt.active_message_id) {
1762
+ throw new Error("accepted submission retry conflicts with its resolved deferred transfer");
1763
+ }
1764
+ }
1765
+ if (input.attempt.state === "agent_accepted" &&
1766
+ input.attempt.agent_accepted_at !== acceptedAt) {
1767
+ throw new Error("accepted submission retry sidecar timestamp conflicts with native evidence");
1768
+ }
1769
+ let conversation = input.conversation;
1770
+ const stateNeedsRepair = !stateAccepted;
1771
+ if (stateNeedsRepair) {
1772
+ const acceptedOwner = {
1773
+ ...conversation,
1774
+ status: "waiting_for_agent",
1775
+ updated_at: acceptedAt
1776
+ };
1777
+ delete acceptedOwner.stalled_at;
1778
+ delete acceptedOwner.stalled_reason;
1779
+ delete acceptedOwner.failed_at;
1780
+ delete acceptedOwner.failure_reason;
1781
+ delete acceptedOwner.idle_since;
1782
+ conversation = applyTerminalBridgeSubmission({
1783
+ conversation: acceptedOwner,
1784
+ messageId: input.attempt.active_message_id,
1785
+ messageType: input.authority.messageType,
1786
+ requestText: input.authority.requestText,
1787
+ status: "agent_accepted",
1788
+ preparedAt: nonBlankString(input.submission.prepared_at),
1789
+ textInjectedAt: nonBlankString(input.submission.text_injected_at) ??
1790
+ nonBlankString(input.ledger.text_injected_at),
1791
+ enterDispatchedAt: nonBlankString(input.submission.enter_dispatched_at) ??
1792
+ nonBlankString(input.ledger.enter_dispatched_at),
1793
+ agentAcceptedAt: acceptedAt,
1794
+ acceptanceEvidence: evidence,
1795
+ lastProvenStage: "agent_accepted"
1796
+ }, {
1797
+ dispatcherPid: cliPid(),
1798
+ storeDir: input.storeDir,
1799
+ terminalControl: input.control
1800
+ });
1801
+ saveState(input.paths.statePath, conversation);
1802
+ }
1803
+ let attempt = input.attempt;
1804
+ const attemptNeedsRepair = attempt.state !== "agent_accepted";
1805
+ if (attemptNeedsRepair) {
1806
+ attempt = saveTerminalSubmissionRetry(input.paths.statePath, {
1807
+ ...attempt,
1808
+ state: "agent_accepted",
1809
+ agent_accepted_at: acceptedAt,
1810
+ updated_at: acceptedAt
1811
+ }, attempt.revision);
1812
+ }
1813
+ const ledgerNeedsRepair = !ledgerAccepted ||
1814
+ input.ledger.submission_retry_state !== "agent_accepted" ||
1815
+ input.ledger.submission_retry_revision !== attempt.revision;
1816
+ if (ledgerNeedsRepair) {
1817
+ input.saveLedger({
1818
+ ...input.ledger,
1819
+ ...terminalSubmissionRetryLedgerFields(attempt),
1820
+ status: "agent_accepted",
1821
+ agent_accepted_at: acceptedAt,
1822
+ acceptance_evidence: evidence,
1823
+ not_accepted_at: undefined,
1824
+ uncertain_at: undefined,
1825
+ safe_to_retry: undefined,
1826
+ dispatcher_pid: null
1827
+ });
1828
+ }
1829
+ if (stateNeedsRepair || attemptNeedsRepair || ledgerNeedsRepair) {
1830
+ appendEvent(input.paths.logPath, {
1831
+ ts: acceptedAt,
1832
+ conversation_id: conversation.conversation_id,
1833
+ event: "terminal_submission_retry_startup_accepted_reconciled",
1834
+ message_id: input.attempt.active_message_id,
1835
+ terminal_input_sent: false
1836
+ });
1837
+ }
1838
+ return loadState(input.paths.statePath);
1839
+ }
1840
+ function reconcileTerminalSubmissionRetryStartupTerminalLedger(input) {
1841
+ const notAccepted = input.startup.outcome === "not_accepted";
1842
+ input.saveLedger({
1843
+ ...input.ledger,
1844
+ ...terminalSubmissionRetryLedgerFields(input.attempt),
1845
+ status: input.startup.outcome,
1846
+ ...(notAccepted
1847
+ ? {
1848
+ not_accepted_at: input.startup.at,
1849
+ uncertain_at: undefined,
1850
+ error: undefined,
1851
+ safe_to_retry: undefined
1852
+ }
1853
+ : {
1854
+ not_accepted_at: undefined,
1855
+ uncertain_at: input.startup.at,
1856
+ error: input.startup.reason,
1857
+ safe_to_retry: false
1858
+ }),
1859
+ acceptance_evidence: undefined,
1860
+ agent_accepted_at: undefined,
1861
+ dispatcher_pid: null
1862
+ });
1863
+ appendEvent(input.paths.logPath, {
1864
+ ts: input.startup.at,
1865
+ conversation_id: input.conversation.conversation_id,
1866
+ event: `terminal_submission_retry_startup_${input.startup.outcome}_reconciled`,
1867
+ message_id: input.attempt.active_message_id,
1868
+ terminal_input_sent: false,
1869
+ do_not_retry: true
1870
+ });
1871
+ }
1872
+ function reconcileTerminalSubmissionRetryStartupTerminalState(input) {
1873
+ const stalled = {
1874
+ ...input.conversation,
1875
+ status: "stalled",
1876
+ stalled_at: input.startup.at,
1877
+ stalled_reason: input.startup.reason ??
1878
+ "submission retry native acceptance became uncertain",
1879
+ updated_at: input.startup.at
1880
+ };
1881
+ const conversation = applyTerminalBridgeSubmission({
1882
+ conversation: stalled,
1883
+ messageId: input.attempt.active_message_id,
1884
+ messageType: input.authority.messageType,
1885
+ requestText: input.authority.requestText,
1886
+ status: "uncertain",
1887
+ preparedAt: nonBlankString(input.submission.prepared_at),
1888
+ textInjectedAt: nonBlankString(input.submission.text_injected_at),
1889
+ enterDispatchedAt: nonBlankString(input.submission.enter_dispatched_at),
1890
+ uncertainAt: input.startup.at,
1891
+ error: input.startup.reason,
1892
+ safeToRetry: false,
1893
+ lastProvenStage: "enter_dispatched"
1894
+ }, {
1895
+ dispatcherPid: cliPid(),
1896
+ storeDir: input.storeDir,
1897
+ terminalControl: input.control
1898
+ });
1899
+ saveState(input.paths.statePath, conversation);
1900
+ appendEvent(input.paths.logPath, {
1901
+ ts: input.startup.at,
1902
+ conversation_id: conversation.conversation_id,
1903
+ event: "terminal_submission_retry_startup_uncertain_reconciled",
1904
+ message_id: input.attempt.active_message_id,
1905
+ terminal_input_sent: false,
1906
+ do_not_retry: true
1907
+ });
1908
+ return conversation;
1909
+ }
1910
+ function mirrorDeferredSubmissionRetryEnter(input) {
1911
+ const transfer = loadDeferredForegroundTransfer(input.storeDir, input.transferId);
1912
+ assertDeferredSubmissionRetryMirrorAuthority(transfer, input);
1913
+ if (isFinalDeferredForegroundTransferStatus(transfer.status)) {
1914
+ return false;
1915
+ }
1916
+ if (transfer.submission_retry_enter_dispatched_at) {
1917
+ if (!isDeferredForegroundSubmissionRetryPending(transfer)) {
1918
+ throw new Error("deferred submission retry startup authority is not exactly pending");
1919
+ }
1920
+ return false;
1921
+ }
1922
+ if (transfer.status !== "uncertain" ||
1923
+ transfer.input_stage !== "text_injected" ||
1924
+ transfer.enter_dispatched_at !== undefined) {
1925
+ throw new Error("deferred submission retry startup recovery found an unsafe stage lag");
1926
+ }
1927
+ const repaired = saveDeferredForegroundTransfer(input.storeDir, {
1928
+ ...transfer,
1929
+ input_stage: "enter_dispatched",
1930
+ enter_dispatched_at: input.attempt.enter_dispatched_at,
1931
+ submission_retry_enter_dispatched_at: input.attempt.enter_dispatched_at
1932
+ }, {
1933
+ expectedRevision: Number(transfer.revision)
1934
+ });
1935
+ if (!isDeferredForegroundSubmissionRetryPending(repaired)) {
1936
+ throw new Error("deferred submission retry startup recovery did not produce exact pending authority");
1937
+ }
1938
+ return true;
1939
+ }
1940
+ function assertDeferredSubmissionRetryMirrorAuthority(transfer, input) {
1941
+ const exactMode = input.attempt.mode === "exact_draft_enter";
1942
+ if (transfer.transfer_id !== input.transferId ||
1943
+ transfer.turn_id !== input.turnId ||
1944
+ transfer.target_session_id !== input.sessionId ||
1945
+ !sameCanonicalStatePath(transfer.state_path, input.statePath) ||
1946
+ transfer.request_hash !== input.requestHash ||
1947
+ transfer.message_id !== input.messageId ||
1948
+ transfer.prepared_at !== input.preparedAt ||
1949
+ transfer.submission_retry_attempt_id !== input.attempt.attempt_id ||
1950
+ transfer.submission_retry_mode !== input.attempt.mode ||
1951
+ transfer.submission_retry_message_id !== input.messageId ||
1952
+ transfer.submission_retry_prepared_at !== input.preparedAt ||
1953
+ transfer.submission_retry_enter_reserved_at !==
1954
+ input.attempt.enter_reserved_at ||
1955
+ transfer.submission_retry_enter_dispatched_at !== undefined &&
1956
+ transfer.submission_retry_enter_dispatched_at !==
1957
+ input.attempt.enter_dispatched_at ||
1958
+ transfer.submission_retry_enter_dispatched_at !== undefined &&
1959
+ (transfer.input_stage !== "enter_dispatched" ||
1960
+ transfer.enter_dispatched_at !== input.attempt.enter_dispatched_at) ||
1961
+ (exactMode
1962
+ ? transfer.submission_retry_text_reserved_at !== undefined ||
1963
+ transfer.submission_retry_text_injected_at !== undefined ||
1964
+ transfer.text_injected_at !== input.textInjectedAt
1965
+ : transfer.submission_retry_text_reserved_at !==
1966
+ input.attempt.replacement_text_reserved_at ||
1967
+ transfer.submission_retry_text_injected_at !==
1968
+ input.attempt.replacement_text_injected_at)) {
1969
+ throw new Error("deferred submission retry startup authority disagrees with its attempt");
1970
+ }
1971
+ }
1972
+ function terminalSubmissionRetryMonitorEpoch(conversation, at) {
1973
+ const takeover = takeoverFor(conversation);
1974
+ const inactivityMinutes = Number(takeover.terminal_bridge_inactivity_timeout_minutes);
1975
+ const hardMinutes = Number(takeover.terminal_bridge_hard_timeout_minutes);
1976
+ const inactivityDeadlineAt = terminalMonitorDeadlineAt(at, inactivityMinutes);
1977
+ const hardDeadlineAt = terminalMonitorDeadlineAt(at, hardMinutes);
1978
+ if (!inactivityDeadlineAt || !hardDeadlineAt) {
1979
+ throw new Error("submission retry startup recovery lacks its configured monitor timeouts");
1980
+ }
1981
+ const next = {
1982
+ ...conversation,
1983
+ status: "waiting_for_agent",
1984
+ native_session_takeover: {
1985
+ ...takeover,
1986
+ terminal_bridge_started_at: at,
1987
+ terminal_bridge_monitor_started_at: at,
1988
+ terminal_bridge_last_activity_at: at,
1989
+ terminal_bridge_last_activity_reason: "submission retry Enter dispatched",
1990
+ terminal_bridge_inactivity_deadline_at: inactivityDeadlineAt,
1991
+ terminal_bridge_hard_deadline_at: hardDeadlineAt
1992
+ },
1993
+ updated_at: at
1994
+ };
1995
+ delete next.stalled_at;
1996
+ delete next.stalled_reason;
1997
+ delete next.failed_at;
1998
+ delete next.failure_reason;
1999
+ delete next.idle_since;
2000
+ return next;
2001
+ }
2002
+ function terminalSubmissionRetryStateIsPending(input) {
2003
+ return input.conversation.status === "waiting_for_agent" &&
2004
+ input.submission.status === "enter_dispatched" &&
2005
+ nonBlankString(input.submission.message_id) === input.projection.messageId &&
2006
+ nonBlankString(input.submission.prepared_at) ===
2007
+ input.projection.preparedAt &&
2008
+ nonBlankString(input.submission.text_injected_at) ===
2009
+ input.projection.textInjectedAt &&
2010
+ nonBlankString(input.submission.enter_dispatched_at) ===
2011
+ input.projection.enterDispatchedAt;
2012
+ }
1315
2013
  function takeoverFor(conversation) {
1316
2014
  return isRecord(conversation.native_session_takeover)
1317
2015
  ? conversation.native_session_takeover
@@ -1446,43 +2144,6 @@ function stalledMessage(conversation, reason, executor, terminalBridge) {
1446
2144
  ].join("\n")
1447
2145
  });
1448
2146
  }
1449
- function recordStalledDelivery(input, eventPrefix, suffix, delivery, includeMethod) {
1450
- appendEvent(input.logPath, {
1451
- ts: cliNow().toISOString(),
1452
- conversation_id: input.conversation.conversation_id,
1453
- event: `${eventPrefix}_${suffix}`,
1454
- ...(includeMethod ? { method: input.conversation.gateway_method } : {}),
1455
- message_id: input.message.id,
1456
- status: delivery.status,
1457
- stdout: redactString(delivery.stdout),
1458
- stderr: redactString(delivery.stderr)
1459
- });
1460
- cliRuntimeLog("info", `${eventPrefix}_${suffix}`, {
1461
- conversation_id: input.conversation.conversation_id,
1462
- ...(includeMethod ? { method: input.conversation.gateway_method } : {}),
1463
- message_id: input.message.id,
1464
- status: delivery.status,
1465
- failure_kind: classifyCallbackProcessFailure(delivery),
1466
- stdout: textSummary(delivery.stdout),
1467
- stderr: textSummary(delivery.stderr)
1468
- });
1469
- }
1470
- function parseOptionalJson(text) {
1471
- try {
1472
- const parsed = JSON.parse(String(text));
1473
- return isRecord(parsed) ? parsed : undefined;
1474
- }
1475
- catch {
1476
- return undefined;
1477
- }
1478
- }
1479
- function textSummary(text, maxLength = 240) {
1480
- const value = String(text ?? "");
1481
- return {
1482
- length: value.length,
1483
- preview: value ? value.slice(0, maxLength) : undefined
1484
- };
1485
- }
1486
2147
  function truncateText(value, maxLength) {
1487
2148
  const text = String(value ?? "").replace(/\s+/g, " ").trim();
1488
2149
  return text.length <= maxLength