@scotthuang/agent-knock-knock 0.12.13 → 0.12.14
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.
- package/CHANGELOG.md +18 -0
- package/README.md +5 -5
- package/dist/src/callback-cli-adapter.d.ts +9 -6
- package/dist/src/callback-cli-adapter.js +119 -30
- package/dist/src/callback-cli-adapter.js.map +1 -1
- package/dist/src/callback-outbox-policy.d.ts +39 -0
- package/dist/src/callback-outbox-policy.js +163 -6
- package/dist/src/callback-outbox-policy.js.map +1 -1
- package/dist/src/callback-outbox-service.d.ts +28 -1
- package/dist/src/callback-outbox-service.js +413 -82
- package/dist/src/callback-outbox-service.js.map +1 -1
- package/dist/src/callback-outbox-settlement.d.ts +14 -1
- package/dist/src/callback-outbox-settlement.js +151 -30
- package/dist/src/callback-outbox-settlement.js.map +1 -1
- package/dist/src/callback-route-authority.d.ts +59 -0
- package/dist/src/callback-route-authority.js +123 -0
- package/dist/src/callback-route-authority.js.map +1 -0
- package/dist/src/callback-transport.d.ts +134 -0
- package/dist/src/callback-transport.js +256 -0
- package/dist/src/callback-transport.js.map +1 -0
- package/dist/src/cli-core.js +17 -4
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/codex-lifecycle-compatibility.js +2 -1
- package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
- package/dist/src/deferred-foreground-application-service.d.ts +18 -0
- package/dist/src/deferred-foreground-application-service.js +72 -0
- package/dist/src/deferred-foreground-application-service.js.map +1 -1
- package/dist/src/deferred-foreground-authority-cli-adapter.js +2 -1
- package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
- package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +1 -1
- package/dist/src/deferred-foreground-recovery-cli-adapter.js +108 -37
- package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -1
- package/dist/src/deferred-foreground-recovery-service.d.ts +1 -1
- package/dist/src/deferred-foreground-recovery-service.js +10 -3
- package/dist/src/deferred-foreground-recovery-service.js.map +1 -1
- package/dist/src/deferred-foreground-transfer-policy.d.ts +1 -1
- package/dist/src/deferred-foreground-transfer-policy.js +3 -2
- package/dist/src/deferred-foreground-transfer-policy.js.map +1 -1
- package/dist/src/deferred-foreground-transfer.d.ts +17 -1
- package/dist/src/deferred-foreground-transfer.js +244 -35
- package/dist/src/deferred-foreground-transfer.js.map +1 -1
- package/dist/src/deferred-foreground-user-close.d.ts +18 -0
- package/dist/src/deferred-foreground-user-close.js +180 -0
- package/dist/src/deferred-foreground-user-close.js.map +1 -0
- package/dist/src/openclaw-callback-transport.d.ts +4 -1
- package/dist/src/openclaw-callback-transport.js +403 -10
- package/dist/src/openclaw-callback-transport.js.map +1 -1
- package/dist/src/openclaw-managed-callback-cli-adapter.d.ts +23 -0
- package/dist/src/openclaw-managed-callback-cli-adapter.js +77 -0
- package/dist/src/openclaw-managed-callback-cli-adapter.js.map +1 -0
- package/dist/src/openclaw-plugin-command-adapter.js +28 -31
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.js +8 -0
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.d.ts +20 -1
- package/dist/src/openclaw-plugin-schemas.js +26 -2
- package/dist/src/openclaw-plugin-schemas.js.map +1 -1
- package/dist/src/protocol.d.ts +3 -0
- package/dist/src/protocol.js +4 -0
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/store.d.ts +9 -0
- package/dist/src/store.js +79 -1
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-acceptance-cli-adapter.d.ts +2 -0
- package/dist/src/terminal-acceptance-cli-adapter.js +62 -12
- package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -1
- package/dist/src/terminal-action-projection.d.ts +1 -2
- package/dist/src/terminal-action-projection.js +4 -5
- package/dist/src/terminal-action-projection.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +82 -0
- package/dist/src/terminal-agent-bridge.js +539 -111
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-command-cli-adapter.d.ts +9 -0
- package/dist/src/terminal-command-cli-adapter.js +2130 -5
- package/dist/src/terminal-command-cli-adapter.js.map +1 -1
- package/dist/src/terminal-dispatch-application.d.ts +4 -1
- package/dist/src/terminal-dispatch-application.js +11 -2
- package/dist/src/terminal-dispatch-application.js.map +1 -1
- package/dist/src/terminal-dispatch-execution.js +3 -1
- package/dist/src/terminal-dispatch-execution.js.map +1 -1
- package/dist/src/terminal-dispatch-ledger-codec.d.ts +3 -0
- package/dist/src/terminal-dispatch-ledger-codec.js +46 -19
- package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -1
- package/dist/src/terminal-dispatch-presenter.js +5 -3
- package/dist/src/terminal-dispatch-presenter.js.map +1 -1
- package/dist/src/terminal-dispatch-receipt.d.ts +3 -0
- package/dist/src/terminal-dispatch-receipt.js +15 -1
- package/dist/src/terminal-dispatch-receipt.js.map +1 -1
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js +68 -7
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -1
- package/dist/src/terminal-dispatch-recovery-service.d.ts +2 -0
- package/dist/src/terminal-dispatch-recovery-service.js +9 -0
- package/dist/src/terminal-dispatch-recovery-service.js.map +1 -1
- package/dist/src/terminal-identity-authority-cli-adapter.d.ts +2 -0
- package/dist/src/terminal-identity-authority-cli-adapter.js +30 -27
- package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-cli-adapter.d.ts +1 -0
- package/dist/src/terminal-list-cli-adapter.js +50 -31
- package/dist/src/terminal-list-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-renderer.d.ts +9 -1
- package/dist/src/terminal-list-renderer.js +219 -98
- package/dist/src/terminal-list-renderer.js.map +1 -1
- package/dist/src/terminal-maintenance-cli-adapter.js +253 -326
- package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-application-service.d.ts +5 -0
- package/dist/src/terminal-monitor-application-service.js +5 -0
- package/dist/src/terminal-monitor-application-service.js.map +1 -1
- package/dist/src/terminal-monitor-cli-adapter.d.ts +25 -1
- package/dist/src/terminal-monitor-cli-adapter.js +170 -27
- package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +1 -1
- package/dist/src/terminal-monitor-reconciliation-eligibility.js +220 -32
- package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -1
- package/dist/src/terminal-monitor-state-cli-adapter.d.ts +1 -1
- package/dist/src/terminal-monitor-state-cli-adapter.js +808 -147
- package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +2 -1
- package/dist/src/terminal-monitor-state-reconciliation-service.js +12 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -1
- package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +2 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.js +31 -11
- package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -1
- package/dist/src/terminal-status-cli-adapter.d.ts +1 -0
- package/dist/src/terminal-status-cli-adapter.js +30 -28
- package/dist/src/terminal-status-cli-adapter.js.map +1 -1
- package/dist/src/terminal-submission-acceptance.js +78 -25
- package/dist/src/terminal-submission-acceptance.js.map +1 -1
- package/dist/src/terminal-submission-retry-service.d.ts +114 -0
- package/dist/src/terminal-submission-retry-service.js +589 -0
- package/dist/src/terminal-submission-retry-service.js.map +1 -0
- package/dist/src/terminal-watch-callback-cli-adapter.d.ts +10 -2
- package/dist/src/terminal-watch-callback-cli-adapter.js +76 -37
- package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -1
- package/dist/src/terminal-watch-cli-adapter.js +41 -17
- package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
- package/dist/src/terminal-watch-service.d.ts +11 -6
- package/dist/src/terminal-watch-service.js +210 -41
- package/dist/src/terminal-watch-service.js.map +1 -1
- package/dist/src/terminal-watch-store.d.ts +23 -0
- package/dist/src/terminal-watch-store.js +162 -4
- package/dist/src/terminal-watch-store.js.map +1 -1
- package/docs/quickstart-tmux.md +1 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +8 -5
|
@@ -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 {
|
|
18
|
-
import {
|
|
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, () =>
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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
|
|
210
|
-
.
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|