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