@scotthuang/agent-knock-knock 0.12.11 → 0.12.13
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 +40 -0
- package/README.md +43 -30
- package/dist/src/approval-policy.d.ts +11 -2
- package/dist/src/approval-policy.js +33 -7
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/callback-cli-adapter.d.ts +47 -0
- package/dist/src/callback-cli-adapter.js +156 -0
- package/dist/src/callback-cli-adapter.js.map +1 -0
- package/dist/src/callback-outbox-policy.d.ts +95 -0
- package/dist/src/callback-outbox-policy.js +149 -0
- package/dist/src/callback-outbox-policy.js.map +1 -0
- package/dist/src/callback-outbox-service.d.ts +247 -0
- package/dist/src/callback-outbox-service.js +1053 -0
- package/dist/src/callback-outbox-service.js.map +1 -0
- package/dist/src/callback-outbox-settlement.d.ts +54 -0
- package/dist/src/callback-outbox-settlement.js +241 -0
- package/dist/src/callback-outbox-settlement.js.map +1 -0
- package/dist/src/canonical-json.d.ts +1 -0
- package/dist/src/canonical-json.js +14 -0
- package/dist/src/canonical-json.js.map +1 -0
- package/dist/src/claude-lifecycle-compatibility.js +15 -0
- package/dist/src/claude-lifecycle-compatibility.js.map +1 -1
- package/dist/src/claude-local-transcript-provider.d.ts +82 -1
- package/dist/src/claude-local-transcript-provider.js +873 -23
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/cli-command-runtime.d.ts +14 -0
- package/dist/src/cli-command-runtime.js +164 -0
- package/dist/src/cli-command-runtime.js.map +1 -0
- package/dist/src/cli-core.d.ts +4 -42
- package/dist/src/cli-core.js +958 -28684
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/cli-runtime-context.d.ts +57 -0
- package/dist/src/cli-runtime-context.js +118 -0
- package/dist/src/cli-runtime-context.js.map +1 -0
- package/dist/src/codex-lifecycle-compatibility.js +2 -1
- package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
- package/dist/src/codex-local-session-provider.d.ts +18 -0
- package/dist/src/codex-local-session-provider.js +67 -0
- package/dist/src/codex-local-session-provider.js.map +1 -1
- package/dist/src/codex-session-provider.js +1 -3
- package/dist/src/codex-session-provider.js.map +1 -1
- package/dist/src/conversation-trace.d.ts +30 -0
- package/dist/src/conversation-trace.js +185 -0
- package/dist/src/conversation-trace.js.map +1 -0
- package/dist/src/deferred-foreground-application-service.d.ts +84 -0
- package/dist/src/deferred-foreground-application-service.js +742 -0
- package/dist/src/deferred-foreground-application-service.js.map +1 -0
- package/dist/src/deferred-foreground-authority-cli-adapter.d.ts +78 -0
- package/dist/src/deferred-foreground-authority-cli-adapter.js +795 -0
- package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -0
- package/dist/src/deferred-foreground-boundary.d.ts +61 -0
- package/dist/src/deferred-foreground-boundary.js +2 -0
- package/dist/src/deferred-foreground-boundary.js.map +1 -0
- package/dist/src/deferred-foreground-capability.d.ts +4 -0
- package/dist/src/deferred-foreground-capability.js +184 -0
- package/dist/src/deferred-foreground-capability.js.map +1 -0
- package/dist/src/deferred-foreground-preparation-cli-adapter.d.ts +53 -0
- package/dist/src/deferred-foreground-preparation-cli-adapter.js +210 -0
- package/dist/src/deferred-foreground-preparation-cli-adapter.js.map +1 -0
- package/dist/src/deferred-foreground-preparation-service.d.ts +72 -0
- package/dist/src/deferred-foreground-preparation-service.js +179 -0
- package/dist/src/deferred-foreground-preparation-service.js.map +1 -0
- package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +139 -0
- package/dist/src/deferred-foreground-recovery-cli-adapter.js +1125 -0
- package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -0
- package/dist/src/deferred-foreground-recovery-service.d.ts +48 -0
- package/dist/src/deferred-foreground-recovery-service.js +91 -0
- package/dist/src/deferred-foreground-recovery-service.js.map +1 -0
- package/dist/src/deferred-foreground-transfer-policy.d.ts +2 -0
- package/dist/src/deferred-foreground-transfer-policy.js +10 -0
- package/dist/src/deferred-foreground-transfer-policy.js.map +1 -0
- package/dist/src/deferred-foreground-transfer.js +46 -105
- package/dist/src/deferred-foreground-transfer.js.map +1 -1
- package/dist/src/doctor-capabilities.d.ts +9 -0
- package/dist/src/doctor-capabilities.js +22 -0
- package/dist/src/doctor-capabilities.js.map +1 -1
- package/dist/src/durable-json-file.d.ts +22 -0
- package/dist/src/durable-json-file.js +127 -0
- package/dist/src/durable-json-file.js.map +1 -0
- package/dist/src/file-lock-cli-adapter.d.ts +43 -0
- package/dist/src/file-lock-cli-adapter.js +150 -0
- package/dist/src/file-lock-cli-adapter.js.map +1 -0
- package/dist/src/herdr-terminal-control-provider.js +1 -3
- package/dist/src/herdr-terminal-control-provider.js.map +1 -1
- package/dist/src/install-doctor-command-adapter.d.ts +17 -0
- package/dist/src/install-doctor-command-adapter.js +225 -0
- package/dist/src/install-doctor-command-adapter.js.map +1 -0
- package/dist/src/live-lifecycle-evidence.js +76 -46
- package/dist/src/live-lifecycle-evidence.js.map +1 -1
- package/dist/src/live-lifecycle-smoke.js +74 -50
- package/dist/src/live-lifecycle-smoke.js.map +1 -1
- package/dist/src/managed-session.d.ts +2 -0
- package/dist/src/managed-session.js +24 -0
- package/dist/src/managed-session.js.map +1 -1
- package/dist/src/managed-turn-recovery-service.d.ts +86 -0
- package/dist/src/managed-turn-recovery-service.js +145 -0
- package/dist/src/managed-turn-recovery-service.js.map +1 -0
- package/dist/src/mutation-transaction.d.ts +48 -0
- package/dist/src/mutation-transaction.js +73 -0
- package/dist/src/mutation-transaction.js.map +1 -0
- package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +154 -0
- package/dist/src/native-thread-lifecycle-cli-adapter.js +786 -0
- package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -0
- package/dist/src/native-thread-lifecycle-ledger-cli-adapter.d.ts +25 -0
- package/dist/src/native-thread-lifecycle-ledger-cli-adapter.js +50 -0
- package/dist/src/native-thread-lifecycle-ledger-cli-adapter.js.map +1 -0
- package/dist/src/native-thread-lifecycle-query-service.d.ts +75 -0
- package/dist/src/native-thread-lifecycle-query-service.js +297 -0
- package/dist/src/native-thread-lifecycle-query-service.js.map +1 -0
- package/dist/src/native-thread-lifecycle-recovery-adapter.d.ts +45 -0
- package/dist/src/native-thread-lifecycle-recovery-adapter.js +154 -0
- package/dist/src/native-thread-lifecycle-recovery-adapter.js.map +1 -0
- package/dist/src/native-thread-lifecycle-recovery-service.d.ts +211 -0
- package/dist/src/native-thread-lifecycle-recovery-service.js +1093 -0
- package/dist/src/native-thread-lifecycle-recovery-service.js.map +1 -0
- package/dist/src/native-thread-resume-snapshot-policy.d.ts +73 -0
- package/dist/src/native-thread-resume-snapshot-policy.js +218 -0
- package/dist/src/native-thread-resume-snapshot-policy.js.map +1 -0
- package/dist/src/native-thread-resume-snapshot.d.ts +3 -61
- package/dist/src/native-thread-resume-snapshot.js +2 -143
- package/dist/src/native-thread-resume-snapshot.js.map +1 -1
- package/dist/src/native-thread-transition-application.d.ts +147 -0
- package/dist/src/native-thread-transition-application.js +974 -0
- package/dist/src/native-thread-transition-application.js.map +1 -0
- package/dist/src/native-thread-transition-policy.d.ts +127 -0
- package/dist/src/native-thread-transition-policy.js +154 -0
- package/dist/src/native-thread-transition-policy.js.map +1 -0
- package/dist/src/native-thread-transition-resource-adapter.d.ts +12 -0
- package/dist/src/native-thread-transition-resource-adapter.js +44 -0
- package/dist/src/native-thread-transition-resource-adapter.js.map +1 -0
- package/dist/src/native-thread-transition-settlement-service.d.ts +94 -0
- package/dist/src/native-thread-transition-settlement-service.js +134 -0
- package/dist/src/native-thread-transition-settlement-service.js.map +1 -0
- package/dist/src/native-thread-transition-verification-adapter.d.ts +62 -0
- package/dist/src/native-thread-transition-verification-adapter.js +451 -0
- package/dist/src/native-thread-transition-verification-adapter.js.map +1 -0
- package/dist/src/openclaw-callback-transport.d.ts +69 -0
- package/dist/src/openclaw-callback-transport.js +583 -0
- package/dist/src/openclaw-callback-transport.js.map +1 -0
- package/dist/src/openclaw-plugin-callback-adapter.d.ts +1 -0
- package/dist/src/openclaw-plugin-callback-adapter.js +423 -0
- package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -0
- package/dist/src/openclaw-plugin-command-adapter.d.ts +16 -0
- package/dist/src/openclaw-plugin-command-adapter.js +1868 -0
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -0
- package/dist/src/openclaw-plugin-helpers.d.ts +27 -4
- package/dist/src/openclaw-plugin-helpers.js +356 -81
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.d.ts +358 -0
- package/dist/src/openclaw-plugin-schemas.js +366 -0
- package/dist/src/openclaw-plugin-schemas.js.map +1 -0
- package/dist/src/openclaw-plugin-supervisor.d.ts +6 -0
- package/dist/src/openclaw-plugin-supervisor.js +123 -0
- package/dist/src/openclaw-plugin-supervisor.js.map +1 -0
- package/dist/src/openclaw-plugin.js +7 -2212
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/openclaw-private-authority-offers.d.ts +22 -0
- package/dist/src/openclaw-private-authority-offers.js +177 -0
- package/dist/src/openclaw-private-authority-offers.js.map +1 -0
- package/dist/src/protocol.d.ts +5 -0
- package/dist/src/protocol.js +59 -3
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/session-store.d.ts +1 -0
- package/dist/src/session-store.js +84 -106
- package/dist/src/session-store.js.map +1 -1
- package/dist/src/store.d.ts +1 -0
- package/dist/src/store.js +5 -2
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-acceptance-application-service.d.ts +65 -0
- package/dist/src/terminal-acceptance-application-service.js +63 -0
- package/dist/src/terminal-acceptance-application-service.js.map +1 -0
- package/dist/src/terminal-acceptance-cli-adapter.d.ts +210 -0
- package/dist/src/terminal-acceptance-cli-adapter.js +957 -0
- package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -0
- package/dist/src/terminal-action-projection.d.ts +309 -0
- package/dist/src/terminal-action-projection.js +376 -0
- package/dist/src/terminal-action-projection.js.map +1 -0
- package/dist/src/terminal-agent-bridge.d.ts +3 -0
- package/dist/src/terminal-agent-bridge.js +29 -8
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-authority-policy.d.ts +141 -0
- package/dist/src/terminal-authority-policy.js +367 -0
- package/dist/src/terminal-authority-policy.js.map +1 -0
- package/dist/src/terminal-binding-authority.d.ts +118 -0
- package/dist/src/terminal-binding-authority.js +245 -0
- package/dist/src/terminal-binding-authority.js.map +1 -0
- package/dist/src/terminal-binding-reconciliation-service.d.ts +75 -0
- package/dist/src/terminal-binding-reconciliation-service.js +73 -0
- package/dist/src/terminal-binding-reconciliation-service.js.map +1 -0
- package/dist/src/terminal-command-cli-adapter.d.ts +380 -0
- package/dist/src/terminal-command-cli-adapter.js +3288 -0
- package/dist/src/terminal-command-cli-adapter.js.map +1 -0
- package/dist/src/terminal-control-ref.d.ts +4 -0
- package/dist/src/terminal-control-ref.js +11 -8
- package/dist/src/terminal-control-ref.js.map +1 -1
- package/dist/src/terminal-delegate-cli-adapter.d.ts +41 -0
- package/dist/src/terminal-delegate-cli-adapter.js +242 -0
- package/dist/src/terminal-delegate-cli-adapter.js.map +1 -0
- package/dist/src/terminal-dispatch-abort.d.ts +22 -0
- package/dist/src/terminal-dispatch-abort.js +30 -0
- package/dist/src/terminal-dispatch-abort.js.map +1 -0
- package/dist/src/terminal-dispatch-application.d.ts +115 -0
- package/dist/src/terminal-dispatch-application.js +478 -0
- package/dist/src/terminal-dispatch-application.js.map +1 -0
- package/dist/src/terminal-dispatch-capability.d.ts +63 -0
- package/dist/src/terminal-dispatch-capability.js +133 -0
- package/dist/src/terminal-dispatch-capability.js.map +1 -0
- package/dist/src/terminal-dispatch-completion-cli-adapter.d.ts +22 -0
- package/dist/src/terminal-dispatch-completion-cli-adapter.js +96 -0
- package/dist/src/terminal-dispatch-completion-cli-adapter.js.map +1 -0
- package/dist/src/terminal-dispatch-composition.d.ts +79 -0
- package/dist/src/terminal-dispatch-composition.js +2 -0
- package/dist/src/terminal-dispatch-composition.js.map +1 -0
- package/dist/src/terminal-dispatch-execution.d.ts +290 -0
- package/dist/src/terminal-dispatch-execution.js +916 -0
- package/dist/src/terminal-dispatch-execution.js.map +1 -0
- package/dist/src/terminal-dispatch-ledger-codec.d.ts +110 -0
- package/dist/src/terminal-dispatch-ledger-codec.js +399 -0
- package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -0
- package/dist/src/terminal-dispatch-policy.d.ts +93 -0
- package/dist/src/terminal-dispatch-policy.js +122 -0
- package/dist/src/terminal-dispatch-policy.js.map +1 -0
- package/dist/src/terminal-dispatch-presenter.d.ts +65 -0
- package/dist/src/terminal-dispatch-presenter.js +258 -0
- package/dist/src/terminal-dispatch-presenter.js.map +1 -0
- package/dist/src/terminal-dispatch-receipt.d.ts +79 -0
- package/dist/src/terminal-dispatch-receipt.js +314 -0
- package/dist/src/terminal-dispatch-receipt.js.map +1 -0
- package/dist/src/terminal-dispatch-recovery-cli-adapter.d.ts +132 -0
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js +1330 -0
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -0
- package/dist/src/terminal-dispatch-recovery-service.d.ts +271 -0
- package/dist/src/terminal-dispatch-recovery-service.js +374 -0
- package/dist/src/terminal-dispatch-recovery-service.js.map +1 -0
- package/dist/src/terminal-dispatch-repository-cli-adapter.d.ts +35 -0
- package/dist/src/terminal-dispatch-repository-cli-adapter.js +483 -0
- package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -0
- package/dist/src/terminal-handoff-application-service.d.ts +7 -0
- package/dist/src/terminal-handoff-application-service.js +18 -0
- package/dist/src/terminal-handoff-application-service.js.map +1 -0
- package/dist/src/terminal-handoff-cli-adapter.d.ts +229 -0
- package/dist/src/terminal-handoff-cli-adapter.js +1389 -0
- package/dist/src/terminal-handoff-cli-adapter.js.map +1 -0
- package/dist/src/terminal-handoff-facts.d.ts +12 -0
- package/dist/src/terminal-handoff-facts.js +16 -0
- package/dist/src/terminal-handoff-facts.js.map +1 -0
- package/dist/src/terminal-identity-authority-cli-adapter.d.ts +318 -0
- package/dist/src/terminal-identity-authority-cli-adapter.js +1072 -0
- package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -0
- package/dist/src/terminal-identity-authority-service.d.ts +48 -0
- package/dist/src/terminal-identity-authority-service.js +45 -0
- package/dist/src/terminal-identity-authority-service.js.map +1 -0
- package/dist/src/terminal-list-cli-adapter.d.ts +274 -0
- package/dist/src/terminal-list-cli-adapter.js +2444 -0
- package/dist/src/terminal-list-cli-adapter.js.map +1 -0
- package/dist/src/terminal-list-renderer.d.ts +34 -0
- package/dist/src/terminal-list-renderer.js +643 -0
- package/dist/src/terminal-list-renderer.js.map +1 -0
- package/dist/src/terminal-maintenance-cli-adapter.d.ts +102 -0
- package/dist/src/terminal-maintenance-cli-adapter.js +1059 -0
- package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -0
- package/dist/src/terminal-monitor-application-service.d.ts +275 -0
- package/dist/src/terminal-monitor-application-service.js +982 -0
- package/dist/src/terminal-monitor-application-service.js.map +1 -0
- package/dist/src/terminal-monitor-cli-adapter.d.ts +130 -0
- package/dist/src/terminal-monitor-cli-adapter.js +442 -0
- package/dist/src/terminal-monitor-cli-adapter.js.map +1 -0
- package/dist/src/terminal-monitor-decision-policy.d.ts +98 -0
- package/dist/src/terminal-monitor-decision-policy.js +246 -0
- package/dist/src/terminal-monitor-decision-policy.js.map +1 -0
- package/dist/src/terminal-monitor-launch-plan.d.ts +45 -0
- package/dist/src/terminal-monitor-launch-plan.js +94 -0
- package/dist/src/terminal-monitor-launch-plan.js.map +1 -0
- package/dist/src/terminal-monitor-ownership-policy.d.ts +31 -0
- package/dist/src/terminal-monitor-ownership-policy.js +64 -0
- package/dist/src/terminal-monitor-ownership-policy.js.map +1 -0
- package/dist/src/terminal-monitor-poll-policy.d.ts +48 -0
- package/dist/src/terminal-monitor-poll-policy.js +67 -0
- package/dist/src/terminal-monitor-poll-policy.js.map +1 -0
- package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +53 -0
- package/dist/src/terminal-monitor-reconciliation-eligibility.js +114 -0
- package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -0
- package/dist/src/terminal-monitor-state-cli-adapter.d.ts +106 -0
- package/dist/src/terminal-monitor-state-cli-adapter.js +1492 -0
- package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +87 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.js +106 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +122 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.js +685 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -0
- package/dist/src/terminal-process-facts.d.ts +2 -0
- package/dist/src/terminal-process-facts.js +19 -0
- package/dist/src/terminal-process-facts.js.map +1 -0
- package/dist/src/terminal-process-source.d.ts +6 -4
- package/dist/src/terminal-process-source.js +25 -20
- package/dist/src/terminal-process-source.js.map +1 -1
- package/dist/src/terminal-runtime-cli-adapter.d.ts +80 -0
- package/dist/src/terminal-runtime-cli-adapter.js +488 -0
- package/dist/src/terminal-runtime-cli-adapter.js.map +1 -0
- package/dist/src/terminal-scoped-approval-authority.d.ts +62 -0
- package/dist/src/terminal-scoped-approval-authority.js +281 -0
- package/dist/src/terminal-scoped-approval-authority.js.map +1 -0
- package/dist/src/terminal-selector-projection-service.d.ts +26 -0
- package/dist/src/terminal-selector-projection-service.js +81 -0
- package/dist/src/terminal-selector-projection-service.js.map +1 -0
- package/dist/src/terminal-status-cli-adapter.d.ts +126 -0
- package/dist/src/terminal-status-cli-adapter.js +577 -0
- package/dist/src/terminal-status-cli-adapter.js.map +1 -0
- package/dist/src/terminal-status-facts.d.ts +56 -0
- package/dist/src/terminal-status-facts.js +199 -0
- package/dist/src/terminal-status-facts.js.map +1 -0
- package/dist/src/terminal-submission-acceptance.d.ts +62 -145
- package/dist/src/terminal-submission-acceptance.js +555 -222
- package/dist/src/terminal-submission-acceptance.js.map +1 -1
- package/dist/src/terminal-submission-facts.d.ts +123 -0
- package/dist/src/terminal-submission-facts.js +205 -0
- package/dist/src/terminal-submission-facts.js.map +1 -0
- package/dist/src/terminal-turn-binding-authority-cli-adapter.d.ts +14 -0
- package/dist/src/terminal-turn-binding-authority-cli-adapter.js +90 -0
- package/dist/src/terminal-turn-binding-authority-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-callback-cli-adapter.d.ts +20 -0
- package/dist/src/terminal-watch-callback-cli-adapter.js +55 -0
- package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-cli-adapter.d.ts +64 -0
- package/dist/src/terminal-watch-cli-adapter.js +508 -0
- package/dist/src/terminal-watch-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-service.d.ts +97 -0
- package/dist/src/terminal-watch-service.js +714 -0
- package/dist/src/terminal-watch-service.js.map +1 -0
- package/dist/src/terminal-watch-store.d.ts +149 -0
- package/dist/src/terminal-watch-store.js +861 -0
- package/dist/src/terminal-watch-store.js.map +1 -0
- package/dist/src/value-guards.d.ts +4 -0
- package/dist/src/value-guards.js +12 -0
- package/dist/src/value-guards.js.map +1 -0
- package/dist/src/verified-dead-agent-policy.d.ts +192 -0
- package/dist/src/verified-dead-agent-policy.js +262 -0
- package/dist/src/verified-dead-agent-policy.js.map +1 -0
- package/docs/quickstart-herdr.md +8 -0
- package/docs/quickstart-tmux.md +33 -6
- package/openclaw.plugin.json +11 -3
- package/package.json +3 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +58 -40
|
@@ -0,0 +1,3288 @@
|
|
|
1
|
+
// Raw CLI infrastructure for ordinary terminal send/respond/approval commands.
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { captureClaudeTranscriptAnchor, defaultClaudeHome } from "./claude-local-transcript-provider.js";
|
|
6
|
+
import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
|
|
7
|
+
import { applyMessageToConversation, budgetAction, createMessage, effectiveTurnStatus, executorForConversation, isTerminalDispatchOwnerReleasedStatus, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
|
|
8
|
+
import { readNdjsonLog } from "./transcript.js";
|
|
9
|
+
import { appendEvent, ensureDir, ensureStoreWritable, listConversations, loadState, messageEvent, pathsForConversationDir, saveState, withStoreWriterLeaseAsync } from "./store.js";
|
|
10
|
+
import { createManagedSessionId, managedSessionBindingToken } from "./managed-session.js";
|
|
11
|
+
import { loadManagedSession, saveManagedSession, tryLoadManagedSession } from "./session-store.js";
|
|
12
|
+
import { terminalControlEvidenceMatches } from "./terminal-control-ref.js";
|
|
13
|
+
import { TerminalInputNotStartedError } from "./terminal-agent-bridge.js";
|
|
14
|
+
import { evaluateApprovalPolicy } from "./approval-policy.js";
|
|
15
|
+
import { codexCompanionsPresentInOpenRootInventory, isCompleteNativeRollout, nativeIdentityMatchesCodexPreMaterialization, terminalControlsShareIncarnation } from "./terminal-authority-policy.js";
|
|
16
|
+
import { decideTerminalSendAuthority } from "./terminal-action-projection.js";
|
|
17
|
+
import { terminalScopedCodexApprovalPromptSnapshot } from "./terminal-scoped-approval-authority.js";
|
|
18
|
+
import { withCanonicalMutationLocks } from "./mutation-transaction.js";
|
|
19
|
+
import { assertTerminalDispatchRouteMatches, bindTerminalDispatchCapabilities, bindTerminalDispatchRoute, withExactTerminalDispatchRoute } from "./terminal-dispatch-capability.js";
|
|
20
|
+
import { claudeTranscriptApprovalIdentity, terminalMonitorDeadlineAt as deadlineAt, terminalMonitorScreenFingerprint as terminalBridgeScreenFingerprint, validTerminalMonitorTimestampMs as validTimestampMs } from "./terminal-monitor-decision-policy.js";
|
|
21
|
+
import * as monitorOwner from "./terminal-monitor-ownership-policy.js";
|
|
22
|
+
import { sameCanonicalStatePath, terminalDispatchLedgerLooksLifecycle, terminalDispatchReceiptHistory as terminalLedgerReceiptHistory } from "./terminal-dispatch-ledger-codec.js";
|
|
23
|
+
import * as dispatchApplication from "./terminal-dispatch-application.js";
|
|
24
|
+
import { bindDeferredForegroundApplicationScope, bindDeferredForegroundWriterScope } from "./deferred-foreground-capability.js";
|
|
25
|
+
import * as deferredRecoveryAdapter from "./deferred-foreground-recovery-cli-adapter.js";
|
|
26
|
+
import { deferredForegroundBoundaryProjection } from "./deferred-foreground-preparation-cli-adapter.js";
|
|
27
|
+
import { terminalSubmissionPayload } from "./terminal-dispatch-execution.js";
|
|
28
|
+
import { presentTerminalCompleted, presentTerminalDispatchReplay, presentTerminalIdentityFailure, presentTerminalUncertain, presentTerminalZeroInputAbort as renderTerminalZeroInputAbort } from "./terminal-dispatch-presenter.js";
|
|
29
|
+
import * as dispatchReceipt from "./terminal-dispatch-receipt.js";
|
|
30
|
+
import { expandHome, positiveMilliseconds, writeCliJson as printJson } from "./cli-command-runtime.js";
|
|
31
|
+
import { cliEnv, cliExit, cliNow, cliNowMs, cliPid, cliRuntimeLog as runtimeLog } from "./cli-runtime-context.js";
|
|
32
|
+
const terminalCommandContext = new AsyncLocalStorage();
|
|
33
|
+
function terminalCommandRuntime() {
|
|
34
|
+
const runtime = terminalCommandContext.getStore();
|
|
35
|
+
if (!runtime) {
|
|
36
|
+
throw new Error("Terminal command facade runtime is unavailable");
|
|
37
|
+
}
|
|
38
|
+
return runtime;
|
|
39
|
+
}
|
|
40
|
+
function rawPort(name) {
|
|
41
|
+
return ((...arguments_) => {
|
|
42
|
+
const operation = terminalCommandRuntime().ports[name];
|
|
43
|
+
if (typeof operation !== "function") {
|
|
44
|
+
throw new Error(`Terminal command port ${String(name)} is unavailable`);
|
|
45
|
+
}
|
|
46
|
+
return operation(...arguments_);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const acquireFileLock = rawPort("acquireFileLock");
|
|
50
|
+
const acquireTerminalBridgeSendLock = rawPort("acquireTerminalBridgeSendLock");
|
|
51
|
+
const assertCodexComposerReadyForAutomatedInput = rawPort("assertCodexComposerReadyForAutomatedInput");
|
|
52
|
+
const assertDeferredCodexForegroundBindingBoundary = rawPort("assertDeferredCodexForegroundBindingBoundary");
|
|
53
|
+
const assertExpectedHandoffTokenUsesExactTerminalSelector = rawPort("assertExpectedHandoffTokenUsesExactTerminalSelector");
|
|
54
|
+
const assertManagedSessionCanStartTurn = rawPort("assertManagedSessionCanStartTurn");
|
|
55
|
+
const assertManagedTerminalDispatchOwner = rawPort("assertManagedTerminalDispatchOwner");
|
|
56
|
+
const assertNativeAgentIdentityForTurn = rawPort("assertNativeAgentIdentityForTurn");
|
|
57
|
+
const assertNativeThreadHasExclusiveOwnership = rawPort("assertNativeThreadHasExclusiveOwnership");
|
|
58
|
+
const assertObservedHandoffTransportBoundary = rawPort("assertObservedHandoffTransportBoundary");
|
|
59
|
+
const assertSafeAbortedTerminalRetryBinding = rawPort("assertSafeAbortedTerminalRetryBinding");
|
|
60
|
+
const assertSafeTerminalSend = rawPort("assertSafeTerminalSend");
|
|
61
|
+
const assertVerifiedEmptyCodexTransportBoundary = rawPort("assertVerifiedEmptyCodexTransportBoundary");
|
|
62
|
+
const bindingMatchesLiveTerminal = rawPort("bindingMatchesLiveTerminal");
|
|
63
|
+
const codexAllowedCompanionSetForManagedSession = rawPort("codexAllowedCompanionSetForManagedSession");
|
|
64
|
+
const codexPreMaterializationIdentityForManagedSession = rawPort("codexPreMaterializationIdentityForManagedSession");
|
|
65
|
+
const createBoundManagedSession = rawPort("createBoundManagedSession");
|
|
66
|
+
const createManagedTerminalTurn = rawPort("createManagedTerminalTurn");
|
|
67
|
+
const createRuntimeTerminalAgentRegistry = rawPort("createRuntimeTerminalAgentRegistry");
|
|
68
|
+
const createTerminalAgentBridge = rawPort("createTerminalAgentBridge");
|
|
69
|
+
const deferredForegroundApplication = rawPort("deferredForegroundApplication");
|
|
70
|
+
const deferredForegroundRecoveryAdapterPorts = rawPort("deferredForegroundRecoveryAdapterPorts");
|
|
71
|
+
const ensureTerminalBridgeMonitorAfterApproval = rawPort("ensureTerminalBridgeMonitorAfterApproval");
|
|
72
|
+
const exactSafeAbortedRecoveredSessionMatches = rawPort("exactSafeAbortedRecoveredSessionMatches");
|
|
73
|
+
const inspectCodexOpenRootRolloutInventory = rawPort("inspectCodexOpenRootRolloutInventory");
|
|
74
|
+
const isDiscoverableTmuxConversation = rawPort("isDiscoverableTmuxConversation");
|
|
75
|
+
const loadClaudeAgentRows = rawPort("loadClaudeAgentRows");
|
|
76
|
+
const loadConversationFromOptions = rawPort("loadConversationFromOptions");
|
|
77
|
+
const loadTerminalBridgeDispatchLedger = rawPort("loadTerminalBridgeDispatchLedger");
|
|
78
|
+
const loadTerminalDispatchLedgerOwner = rawPort("loadTerminalDispatchLedgerOwner");
|
|
79
|
+
const logicalIdentityForManagedSession = rawPort("logicalIdentityForManagedSession");
|
|
80
|
+
const managedSessionStoreDirForConversation = rawPort("managedSessionStoreDirForConversation");
|
|
81
|
+
const managedTurnsForSession = rawPort("managedTurnsForSession");
|
|
82
|
+
const materializeCurrentManagedSession = rawPort("materializeCurrentManagedSession");
|
|
83
|
+
const maybeAdoptObservedExternalThread = rawPort("maybeAdoptObservedExternalThread");
|
|
84
|
+
const maybeDetachVerifiedEmptyCodexSource = rawPort("maybeDetachVerifiedEmptyCodexSource");
|
|
85
|
+
const migrateLegacyTerminalAgentIdentity = rawPort("migrateLegacyTerminalAgentIdentity");
|
|
86
|
+
const openClawYieldNextAction = rawPort("openClawYieldNextAction");
|
|
87
|
+
const observeCurrentNativeAgentSessionIdentity = rawPort("observeCurrentNativeAgentSessionIdentity");
|
|
88
|
+
const parseJsonOption = rawPort("parseJsonOption");
|
|
89
|
+
const persistManagedSessionNativeIdentity = rawPort("persistManagedSessionNativeIdentity");
|
|
90
|
+
const positiveMinutes = rawPort("positiveMinutes");
|
|
91
|
+
const prepareDeferredCodexForegroundBinding = rawPort("prepareDeferredCodexForegroundBinding");
|
|
92
|
+
const quarantineManagedSessionBinding = rawPort("quarantineManagedSessionBinding");
|
|
93
|
+
const reattachManagedSessionForNativeIdentity = rawPort("reattachManagedSessionForNativeIdentity");
|
|
94
|
+
const reconcilePreparedTerminalDispatchLedger = rawPort("reconcilePreparedTerminalDispatchLedger");
|
|
95
|
+
const refineManagedSessionNativeIdentity = rawPort("refineManagedSessionNativeIdentity");
|
|
96
|
+
const refineTerminalTurnEndpoint = rawPort("refineTerminalTurnEndpoint");
|
|
97
|
+
const required = rawPort("required");
|
|
98
|
+
const resolveCurrentNativeAgentSessionIdentity = rawPort("resolveCurrentNativeAgentSessionIdentity");
|
|
99
|
+
const resolveTerminalDispatchLedgerPaneIncarnation = rawPort("resolveTerminalDispatchLedgerPaneIncarnation");
|
|
100
|
+
const resolveTerminalConversationFromOptions = rawPort("resolveTerminalConversationFromOptions");
|
|
101
|
+
const stallOtherTerminalBridgeConversationsForUncertainDispatch = rawPort("stallOtherTerminalBridgeConversationsForUncertainDispatch");
|
|
102
|
+
const startTerminalBridgeMonitorForConversation = rawPort("startTerminalBridgeMonitorForConversation");
|
|
103
|
+
const storeDirFromOptions = rawPort("storeDirFromOptions");
|
|
104
|
+
const soleBoundManagedSessionClaimForTerminal = rawPort("soleBoundManagedSessionClaimForTerminal");
|
|
105
|
+
const terminalBindingLedgerFields = rawPort("terminalBindingLedgerFields");
|
|
106
|
+
const terminalBridgeEnabled = rawPort("terminalBridgeEnabled");
|
|
107
|
+
const terminalBridgeRequestFingerprint = rawPort("terminalBridgeRequestFingerprint");
|
|
108
|
+
const terminalBridgeRuntimeKey = rawPort("terminalBridgeRuntimeKey");
|
|
109
|
+
const terminalControlFromTakeover = rawPort("terminalControlFromTakeover");
|
|
110
|
+
const terminalDispatchCapabilityRepositories = rawPort("terminalDispatchCapabilityRepositories");
|
|
111
|
+
const terminalDispatchExecution = rawPort("terminalDispatchExecution");
|
|
112
|
+
const terminalDispatchRecordMatchesControl = rawPort("terminalDispatchRecordMatchesControl");
|
|
113
|
+
const terminalDurableRequestForConversation = rawPort("terminalDurableRequestForConversation");
|
|
114
|
+
const terminalRuntimeForLiveIdentity = rawPort("terminalRuntimeForLiveIdentity");
|
|
115
|
+
const terminalRuntimeIdentityForConversation = rawPort("terminalRuntimeIdentityForConversation");
|
|
116
|
+
const terminalWriterMutationLocks = rawPort("terminalWriterMutationLocks");
|
|
117
|
+
const textSummary = rawPort("textSummary");
|
|
118
|
+
const verifyCodexPendingManagedSendStatus = rawPort("verifyCodexPendingManagedSendStatus");
|
|
119
|
+
const withTerminalBridgeSubmission = rawPort("withTerminalBridgeSubmission");
|
|
120
|
+
const withTerminalDispatchStateScope = rawPort("withTerminalDispatchStateScope");
|
|
121
|
+
const mutationDispatchLedger = new Proxy({}, {
|
|
122
|
+
get: (_target, property) => terminalCommandRuntime().ports.mutationDispatchLedger[property]
|
|
123
|
+
});
|
|
124
|
+
const terminalListCliFacade = new Proxy({}, {
|
|
125
|
+
get: (_target, property) => terminalCommandRuntime().ports.terminalList[property]
|
|
126
|
+
});
|
|
127
|
+
const DEFAULT_AGENT_TIMEOUT_MINUTES = 60;
|
|
128
|
+
const DEFAULT_AGENT_HARD_TIMEOUT_MINUTES = 720;
|
|
129
|
+
const DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS = 5000;
|
|
130
|
+
const DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS = 50;
|
|
131
|
+
const CLAUDE_SCREEN_APPROVAL_TTL_MS = 10 * 60 * 1000;
|
|
132
|
+
function replayReceiptBindingMismatch(receipt, expectation) {
|
|
133
|
+
return Boolean(!terminalDispatchRecordMatchesControl(receipt, expectation.terminalControl) ||
|
|
134
|
+
(stringValue(receipt.store_dir) !== undefined &&
|
|
135
|
+
path.resolve(String(receipt.store_dir)) !==
|
|
136
|
+
path.resolve(expectation.expectedStoreDir)) ||
|
|
137
|
+
(expectation.expectedSessionId &&
|
|
138
|
+
stringValue(receipt.session_id) !== undefined &&
|
|
139
|
+
stringValue(receipt.session_id) !== expectation.expectedSessionId) ||
|
|
140
|
+
(expectation.expectedTurnId &&
|
|
141
|
+
stringValue(receipt.turn_id) !== undefined &&
|
|
142
|
+
stringValue(receipt.turn_id) !== expectation.expectedTurnId) ||
|
|
143
|
+
(expectation.expectedStatePath &&
|
|
144
|
+
stringValue(receipt.state_path) !== undefined &&
|
|
145
|
+
!sameCanonicalStatePath(receipt.state_path, expectation.expectedStatePath)));
|
|
146
|
+
}
|
|
147
|
+
function replayReceiptPayloadMismatch(receipt, expectation, expectedMessageBodyHash) {
|
|
148
|
+
return ((stringValue(receipt.message_type) !== undefined &&
|
|
149
|
+
stringValue(receipt.message_type) !== expectation.expectedMessageType) ||
|
|
150
|
+
(stringValue(receipt.message_body_hash) !== undefined &&
|
|
151
|
+
stringValue(receipt.message_body_hash) !== expectedMessageBodyHash) ||
|
|
152
|
+
(stringValue(receipt.request_hash) !== undefined &&
|
|
153
|
+
stringValue(receipt.request_hash) !== expectation.requestHash) ||
|
|
154
|
+
(stringValue(expectation.options.openclawSession) !== undefined &&
|
|
155
|
+
stringValue(receipt.openclaw_session) !== undefined &&
|
|
156
|
+
stringValue(receipt.openclaw_session) !==
|
|
157
|
+
stringValue(expectation.options.openclawSession)));
|
|
158
|
+
}
|
|
159
|
+
function replayReceiptConflicts(receipt, expectation, expectedMessageBodyHash) {
|
|
160
|
+
return Boolean(receipt &&
|
|
161
|
+
!(receipt.status === "aborted" && receipt.safe_to_retry === true) &&
|
|
162
|
+
(replayReceiptBindingMismatch(receipt, expectation) ||
|
|
163
|
+
replayReceiptPayloadMismatch(receipt, expectation, expectedMessageBodyHash)));
|
|
164
|
+
}
|
|
165
|
+
function activeReplayLedgerBindingMismatch(ledger, expectation, storeDir, sessionId, statePath, matchesRecoveredSession) {
|
|
166
|
+
return Boolean(!terminalDispatchRecordMatchesControl(ledger, expectation.terminalControl) ||
|
|
167
|
+
(storeDir !== undefined &&
|
|
168
|
+
path.resolve(storeDir) !== path.resolve(expectation.expectedStoreDir)) ||
|
|
169
|
+
(expectation.expectedSessionId &&
|
|
170
|
+
sessionId !== undefined &&
|
|
171
|
+
sessionId !== expectation.expectedSessionId &&
|
|
172
|
+
!matchesRecoveredSession) ||
|
|
173
|
+
(expectation.expectedTurnId &&
|
|
174
|
+
stringValue(ledger.turn_id) !== undefined &&
|
|
175
|
+
stringValue(ledger.turn_id) !== expectation.expectedTurnId) ||
|
|
176
|
+
(expectation.expectedStatePath &&
|
|
177
|
+
statePath !== undefined &&
|
|
178
|
+
!sameCanonicalStatePath(statePath, expectation.expectedStatePath)));
|
|
179
|
+
}
|
|
180
|
+
function activeReplayLedgerPayloadMismatch(ledger, owner, expectation) {
|
|
181
|
+
return Boolean((stringValue(ledger.message_type) !== undefined &&
|
|
182
|
+
stringValue(ledger.message_type) !== expectation.expectedMessageType) ||
|
|
183
|
+
(stringValue(ledger.request_hash) !== undefined &&
|
|
184
|
+
stringValue(ledger.request_hash) !== expectation.requestHash) ||
|
|
185
|
+
(stringValue(expectation.options.openclawSession) !== undefined &&
|
|
186
|
+
owner !== undefined &&
|
|
187
|
+
owner.openclaw_session !==
|
|
188
|
+
stringValue(expectation.options.openclawSession)));
|
|
189
|
+
}
|
|
190
|
+
function activeReplayLedgerConflicts(input) {
|
|
191
|
+
return Boolean(input.ledger &&
|
|
192
|
+
input.ledgerMessageId === input.messageId &&
|
|
193
|
+
(activeReplayLedgerBindingMismatch(input.ledger, input.expectation, input.storeDir, input.sessionId, input.statePath, input.matchesRecoveredSession) ||
|
|
194
|
+
activeReplayLedgerPayloadMismatch(input.ledger, input.owner, input.expectation)));
|
|
195
|
+
}
|
|
196
|
+
function durableReceiptCannotDispatchAgain(receipt, ledger, ledgerMessageId, messageId) {
|
|
197
|
+
return Boolean(receipt &&
|
|
198
|
+
!(receipt.status === "aborted" && receipt.safe_to_retry === true) &&
|
|
199
|
+
(ledgerMessageId !== messageId ||
|
|
200
|
+
!["submitted", "enter_dispatched", "agent_accepted"].includes(String(receipt.status)) ||
|
|
201
|
+
!["submitted", "enter_dispatched", "agent_accepted"].includes(String(ledger?.status))));
|
|
202
|
+
}
|
|
203
|
+
function replayableReconciledLedger(ledger, expectation, messageId) {
|
|
204
|
+
if (!ledger ||
|
|
205
|
+
!["submitted", "enter_dispatched", "agent_accepted"].includes(String(ledger.status)) ||
|
|
206
|
+
!terminalDispatchRecordMatchesControl(ledger, expectation.terminalControl) ||
|
|
207
|
+
stringValue(ledger.message_id) !== messageId ||
|
|
208
|
+
(stringValue(ledger.message_type) !== undefined &&
|
|
209
|
+
stringValue(ledger.message_type) !== expectation.expectedMessageType) ||
|
|
210
|
+
stringValue(ledger.request_hash) !== expectation.requestHash) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
return ledger;
|
|
214
|
+
}
|
|
215
|
+
function activeReplayOwnerMismatch(input) {
|
|
216
|
+
const { owner, ledger, ledgerStoreDir, ownerStoreDir, expectation } = input;
|
|
217
|
+
return Boolean(!owner ||
|
|
218
|
+
isTerminalDispatchOwnerReleasedStatus(owner.status) ||
|
|
219
|
+
!ledgerStoreDir ||
|
|
220
|
+
!ownerStoreDir ||
|
|
221
|
+
path.resolve(ledgerStoreDir) !== path.resolve(expectation.expectedStoreDir) ||
|
|
222
|
+
path.resolve(ownerStoreDir) !== path.resolve(expectation.expectedStoreDir) ||
|
|
223
|
+
(expectation.expectedSessionId &&
|
|
224
|
+
sessionIdForConversation(owner) !== expectation.expectedSessionId) ||
|
|
225
|
+
(expectation.expectedTurnId &&
|
|
226
|
+
turnIdForConversation(owner) !== expectation.expectedTurnId) ||
|
|
227
|
+
(expectation.expectedStatePath &&
|
|
228
|
+
!sameCanonicalStatePath(ledger.state_path, expectation.expectedStatePath)) ||
|
|
229
|
+
(stringValue(expectation.options.openclawSession) &&
|
|
230
|
+
owner.openclaw_session !==
|
|
231
|
+
stringValue(expectation.options.openclawSession)));
|
|
232
|
+
}
|
|
233
|
+
function activeReplaySubmissionMismatch(input) {
|
|
234
|
+
return (stringValue(input.nativeTakeover?.terminal_bridge_message_id) !==
|
|
235
|
+
input.messageId ||
|
|
236
|
+
stringValue(input.submission?.message_id) !== input.messageId ||
|
|
237
|
+
(stringValue(input.submission?.message_type) !== undefined &&
|
|
238
|
+
stringValue(input.submission?.message_type) !==
|
|
239
|
+
input.expectation.expectedMessageType) ||
|
|
240
|
+
stringValue(input.submission?.request_hash) !== input.expectation.requestHash);
|
|
241
|
+
}
|
|
242
|
+
function replayLoggedMessageMismatch(loggedMessage, owner, expectedMessageType, requestText) {
|
|
243
|
+
return (loggedMessage.type !== expectedMessageType ||
|
|
244
|
+
loggedMessage.body !== requestText ||
|
|
245
|
+
loggedMessage.conversation_id !== owner.conversation_id ||
|
|
246
|
+
loggedMessage.session_id !== sessionIdForConversation(owner) ||
|
|
247
|
+
loggedMessage.turn_id !== turnIdForConversation(owner));
|
|
248
|
+
}
|
|
249
|
+
function replayExactActiveTerminalSubmission({ options, terminalControl, requestText, expectedStoreDir, expectedSessionId, expectedTurnId, expectedMessageType = "task", expectedStatePath }) {
|
|
250
|
+
const messageId = stringValue(options.messageId);
|
|
251
|
+
if (!messageId) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
const terminalPayload = terminalSubmissionPayload(requestText);
|
|
255
|
+
const requestHash = terminalBridgeRequestFingerprint(terminalPayload);
|
|
256
|
+
if (!requestHash) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
const loadedLedger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
260
|
+
const ledgerReceiptMatches = terminalLedgerReceiptHistory(loadedLedger)
|
|
261
|
+
.filter((receipt) => stringValue(receipt.message_id) === messageId);
|
|
262
|
+
if (ledgerReceiptMatches.length > 1) {
|
|
263
|
+
throw new Error(`terminal idempotency key ${messageId} has multiple terminal-ledger receipts`);
|
|
264
|
+
}
|
|
265
|
+
const ledgerReceipt = ledgerReceiptMatches[0];
|
|
266
|
+
const loadedLedgerMessageId = stringValue(loadedLedger?.message_id);
|
|
267
|
+
const loadedLedgerStoreDir = stringValue(loadedLedger?.store_dir);
|
|
268
|
+
const loadedLedgerStatePath = stringValue(loadedLedger?.state_path);
|
|
269
|
+
const loadedLedgerOwner = loadedLedger && loadedLedgerMessageId === messageId
|
|
270
|
+
? loadTerminalDispatchLedgerOwner(loadedLedger)
|
|
271
|
+
: undefined;
|
|
272
|
+
const loadedLedgerSessionId = stringValue(loadedLedger?.session_id);
|
|
273
|
+
const loadedLedgerMatchesRecoveredSession = Boolean(expectedSessionId &&
|
|
274
|
+
loadedLedgerSessionId !== undefined &&
|
|
275
|
+
loadedLedgerSessionId !== expectedSessionId &&
|
|
276
|
+
loadedLedgerOwner &&
|
|
277
|
+
exactSafeAbortedRecoveredSessionMatches({
|
|
278
|
+
owner: loadedLedgerOwner,
|
|
279
|
+
storeDir: expectedStoreDir,
|
|
280
|
+
terminalControl,
|
|
281
|
+
messageId,
|
|
282
|
+
expectedSessionId
|
|
283
|
+
}));
|
|
284
|
+
const expectedMessageBodyHash = createHash("sha256")
|
|
285
|
+
.update(requestText)
|
|
286
|
+
.digest("hex");
|
|
287
|
+
const expectation = {
|
|
288
|
+
terminalControl,
|
|
289
|
+
expectedStoreDir,
|
|
290
|
+
expectedSessionId,
|
|
291
|
+
expectedTurnId,
|
|
292
|
+
expectedMessageType,
|
|
293
|
+
expectedStatePath,
|
|
294
|
+
requestHash,
|
|
295
|
+
options
|
|
296
|
+
};
|
|
297
|
+
if (replayReceiptConflicts(ledgerReceipt, expectation, expectedMessageBodyHash)) {
|
|
298
|
+
throw new Error(`terminal idempotency key ${messageId} does not match its durable ` +
|
|
299
|
+
"terminal receipt; no terminal input was sent");
|
|
300
|
+
}
|
|
301
|
+
if (activeReplayLedgerConflicts({
|
|
302
|
+
ledger: loadedLedger,
|
|
303
|
+
ledgerMessageId: loadedLedgerMessageId,
|
|
304
|
+
messageId,
|
|
305
|
+
owner: loadedLedgerOwner,
|
|
306
|
+
expectation,
|
|
307
|
+
storeDir: loadedLedgerStoreDir,
|
|
308
|
+
sessionId: loadedLedgerSessionId,
|
|
309
|
+
statePath: loadedLedgerStatePath,
|
|
310
|
+
matchesRecoveredSession: loadedLedgerMatchesRecoveredSession
|
|
311
|
+
})) {
|
|
312
|
+
throw new Error(`terminal idempotency key ${messageId} does not match its original ` +
|
|
313
|
+
"Store, Session, Turn, OpenClaw session, message, or terminal binding; " +
|
|
314
|
+
"no terminal input was sent");
|
|
315
|
+
}
|
|
316
|
+
if (replayExactStoredTerminalSubmission({
|
|
317
|
+
options,
|
|
318
|
+
terminalControl,
|
|
319
|
+
requestText,
|
|
320
|
+
requestHash,
|
|
321
|
+
messageId,
|
|
322
|
+
expectedStoreDir,
|
|
323
|
+
expectedSessionId,
|
|
324
|
+
expectedTurnId,
|
|
325
|
+
expectedMessageType,
|
|
326
|
+
expectedStatePath
|
|
327
|
+
})) {
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
if (durableReceiptCannotDispatchAgain(ledgerReceipt, loadedLedger, loadedLedgerMessageId, messageId)) {
|
|
331
|
+
throw new Error(`terminal idempotency key ${messageId} already has durable ` +
|
|
332
|
+
`${String(ledgerReceipt.status)} proof and cannot be dispatched again; ` +
|
|
333
|
+
"no terminal input was sent");
|
|
334
|
+
}
|
|
335
|
+
const incarnationLedger = resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, loadedLedger);
|
|
336
|
+
const ledger = replayableReconciledLedger(reconcilePreparedTerminalDispatchLedger(terminalControl, incarnationLedger), expectation, messageId);
|
|
337
|
+
if (!ledger) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
const ownerCandidate = loadTerminalDispatchLedgerOwner(ledger);
|
|
341
|
+
const ledgerStoreDir = stringValue(ledger.store_dir);
|
|
342
|
+
const ownerStoreDir = ownerCandidate
|
|
343
|
+
? managedSessionStoreDirForConversation(ownerCandidate)
|
|
344
|
+
: undefined;
|
|
345
|
+
if (activeReplayOwnerMismatch({
|
|
346
|
+
owner: ownerCandidate,
|
|
347
|
+
ledger,
|
|
348
|
+
ledgerStoreDir,
|
|
349
|
+
ownerStoreDir,
|
|
350
|
+
expectation
|
|
351
|
+
})) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
const owner = ownerCandidate;
|
|
355
|
+
const submission = terminalBridgeSubmission(owner);
|
|
356
|
+
const nativeTakeover = isRecord(owner.native_session_takeover)
|
|
357
|
+
? owner.native_session_takeover
|
|
358
|
+
: undefined;
|
|
359
|
+
if (activeReplaySubmissionMismatch({
|
|
360
|
+
submission,
|
|
361
|
+
nativeTakeover,
|
|
362
|
+
messageId,
|
|
363
|
+
expectation
|
|
364
|
+
})) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
const executor = executorForConversation(owner);
|
|
368
|
+
const nativeThreadId = stringValue(owner.native_thread_id) ??
|
|
369
|
+
stringValue(nativeTakeover?.terminal_agent_session_id) ??
|
|
370
|
+
stringValue(nativeTakeover?.terminal_agent_expected_session_id) ??
|
|
371
|
+
sessionIdForConversation(owner);
|
|
372
|
+
const proofLevel = String(ledger.status);
|
|
373
|
+
const replayReceipt = dispatchReceipt.terminalSubmissionReplayReceipt({
|
|
374
|
+
proofLevel,
|
|
375
|
+
evidence: ledger.acceptance_evidence,
|
|
376
|
+
expected: {
|
|
377
|
+
source: executor.kind === "codex"
|
|
378
|
+
? "codex_rollout"
|
|
379
|
+
: "claude_transcript",
|
|
380
|
+
nativeThreadId,
|
|
381
|
+
requestHash
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
const logPath = stringValue(ledger.event_log_path) ??
|
|
385
|
+
stringValue(owner.event_log_path);
|
|
386
|
+
let loggedMessage;
|
|
387
|
+
if (logPath) {
|
|
388
|
+
try {
|
|
389
|
+
loggedMessage = readNdjsonLog(logPath).find((event) => isRecord(event.message) && event.message.id === messageId)?.message;
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
loggedMessage = undefined;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (isRecord(loggedMessage) &&
|
|
396
|
+
replayLoggedMessageMismatch(loggedMessage, owner, expectedMessageType, requestText)) {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
const durableMessageType = stringValue(ledger.message_type) ??
|
|
400
|
+
stringValue(submission?.message_type) ??
|
|
401
|
+
(isRecord(loggedMessage) ? stringValue(loggedMessage.type) : undefined);
|
|
402
|
+
if (durableMessageType !== expectedMessageType) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
const replayedMessage = isRecord(loggedMessage)
|
|
406
|
+
? loggedMessage
|
|
407
|
+
: createMessage({
|
|
408
|
+
conversation: owner,
|
|
409
|
+
id: messageId,
|
|
410
|
+
from: "openclaw",
|
|
411
|
+
to: executor.actor,
|
|
412
|
+
type: expectedMessageType,
|
|
413
|
+
body: requestText,
|
|
414
|
+
metadata: {
|
|
415
|
+
executor_kind: executor.kind,
|
|
416
|
+
executor_session: executor.session
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
const acceptanceInvalid = replayReceipt.submission_outcome === "uncertain";
|
|
420
|
+
printJson({
|
|
421
|
+
session_id: sessionIdForConversation(owner),
|
|
422
|
+
turn_id: turnIdForConversation(owner),
|
|
423
|
+
conversation: owner,
|
|
424
|
+
message: replayedMessage,
|
|
425
|
+
delivered: replayReceipt.delivered,
|
|
426
|
+
status: replayReceipt.status,
|
|
427
|
+
submission_outcome: replayReceipt.submission_outcome,
|
|
428
|
+
background: true,
|
|
429
|
+
callback_expected: !acceptanceInvalid && Boolean(owner.gateway_method ?? ledger.callback_expected),
|
|
430
|
+
terminal_control: terminalControl,
|
|
431
|
+
executor,
|
|
432
|
+
replayed: replayReceipt.replayed,
|
|
433
|
+
delivery_receipt: replayReceipt.delivery_receipt,
|
|
434
|
+
...(replayReceipt.do_not_retry
|
|
435
|
+
? { do_not_retry: replayReceipt.do_not_retry }
|
|
436
|
+
: {}),
|
|
437
|
+
reason: replayReceipt.delivered
|
|
438
|
+
? "AKK replayed the durable native acceptance receipt and sent no additional terminal input."
|
|
439
|
+
: acceptanceInvalid
|
|
440
|
+
? "AKK replayed an invalid native acceptance receipt as uncertain and sent no additional terminal input."
|
|
441
|
+
: "AKK replayed the original transport proof without upgrading it and sent no additional terminal input.",
|
|
442
|
+
openclaw_next_action: replayReceipt.delivered
|
|
443
|
+
? openClawYieldNextAction({
|
|
444
|
+
conversationId: owner.conversation_id,
|
|
445
|
+
sessionId: sessionIdForConversation(owner),
|
|
446
|
+
turnId: turnIdForConversation(owner),
|
|
447
|
+
source: "terminal_control",
|
|
448
|
+
callbackExpected: Boolean(owner.gateway_method ?? ledger.callback_expected)
|
|
449
|
+
})
|
|
450
|
+
: {
|
|
451
|
+
action: "inspect",
|
|
452
|
+
conversation_id: owner.conversation_id,
|
|
453
|
+
session_id: sessionIdForConversation(owner),
|
|
454
|
+
turn_id: turnIdForConversation(owner),
|
|
455
|
+
do_not_retry: true,
|
|
456
|
+
reason: acceptanceInvalid
|
|
457
|
+
? "Stored native acceptance evidence is invalid."
|
|
458
|
+
: "Only terminal transport is proven."
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
function storedReceiptTerminalBoundaryMismatch(input) {
|
|
464
|
+
return (!input.ownerStoreDir ||
|
|
465
|
+
!input.receiptStoreDir ||
|
|
466
|
+
path.resolve(input.ownerStoreDir) !== path.resolve(input.expectedStoreDir) ||
|
|
467
|
+
path.resolve(input.receiptStoreDir) !== path.resolve(input.expectedStoreDir) ||
|
|
468
|
+
!input.storedControl ||
|
|
469
|
+
!terminalControlsShareIncarnation(input.storedControl, input.terminalControl) ||
|
|
470
|
+
!terminalControlEvidenceMatches(input.receiptTerminalEvidence, input.terminalControl));
|
|
471
|
+
}
|
|
472
|
+
function storedReceiptAuthorityMismatch(input) {
|
|
473
|
+
return (input.receiptSessionId !== sessionIdForConversation(input.owner) ||
|
|
474
|
+
input.receiptTurnId !== turnIdForConversation(input.owner) ||
|
|
475
|
+
(input.expectedSessionId &&
|
|
476
|
+
input.receiptSessionId !== input.expectedSessionId &&
|
|
477
|
+
!input.receiptMatchesRecoveredSession) ||
|
|
478
|
+
(input.expectedTurnId && input.receiptTurnId !== input.expectedTurnId) ||
|
|
479
|
+
(input.expectedStatePath &&
|
|
480
|
+
!sameCanonicalStatePath(input.owner.state_path, input.expectedStatePath)) ||
|
|
481
|
+
(input.requestedOpenClawSession &&
|
|
482
|
+
input.receiptOpenClawSession !== input.requestedOpenClawSession) ||
|
|
483
|
+
stringValue(input.receipt.request_hash) !== input.requestHash ||
|
|
484
|
+
(stringValue(input.receipt.executor_kind) !== undefined &&
|
|
485
|
+
stringValue(input.receipt.executor_kind) !==
|
|
486
|
+
executorForConversation(input.owner).kind));
|
|
487
|
+
}
|
|
488
|
+
function validateStoredTerminalSubmissionMatch({ owner, receipt, options, terminalControl, requestText, requestHash, expectedStoreDir, expectedSessionId, expectedTurnId, expectedMessageType, expectedStatePath }) {
|
|
489
|
+
const messageId = required(stringValue(receipt.message_id), "stored terminal receipt message id is required");
|
|
490
|
+
const ownerStoreDir = managedSessionStoreDirForConversation(owner);
|
|
491
|
+
const nativeTakeover = isRecord(owner.native_session_takeover)
|
|
492
|
+
? owner.native_session_takeover
|
|
493
|
+
: undefined;
|
|
494
|
+
const storedControl = terminalControlFromTakeover(nativeTakeover);
|
|
495
|
+
const receiptStoreDir = stringValue(receipt.store_dir) ?? ownerStoreDir;
|
|
496
|
+
const receiptSessionId = stringValue(receipt.session_id) ??
|
|
497
|
+
sessionIdForConversation(owner);
|
|
498
|
+
const receiptMatchesRecoveredSession = Boolean(expectedSessionId &&
|
|
499
|
+
receiptSessionId !== expectedSessionId &&
|
|
500
|
+
exactSafeAbortedRecoveredSessionMatches({
|
|
501
|
+
owner,
|
|
502
|
+
receipt,
|
|
503
|
+
storeDir: expectedStoreDir,
|
|
504
|
+
terminalControl,
|
|
505
|
+
messageId,
|
|
506
|
+
expectedSessionId
|
|
507
|
+
}));
|
|
508
|
+
const receiptTurnId = stringValue(receipt.turn_id) ??
|
|
509
|
+
turnIdForConversation(owner);
|
|
510
|
+
const receiptOpenClawSession = stringValue(receipt.openclaw_session) ??
|
|
511
|
+
owner.openclaw_session;
|
|
512
|
+
const receiptTerminalEvidence = receipt.terminal_endpoint !== undefined
|
|
513
|
+
? receipt.terminal_endpoint
|
|
514
|
+
: {
|
|
515
|
+
kind: "tmux",
|
|
516
|
+
target: stringValue(receipt.terminal_target) ?? storedControl?.target,
|
|
517
|
+
socket_path: receipt.terminal_socket_path === null
|
|
518
|
+
? null
|
|
519
|
+
: stringValue(receipt.terminal_socket_path) ??
|
|
520
|
+
storedControl?.socketPath ??
|
|
521
|
+
null,
|
|
522
|
+
pane_pid: Number(receipt.terminal_pane_pid ?? storedControl?.panePid)
|
|
523
|
+
};
|
|
524
|
+
const requestedOpenClawSession = stringValue(options.openclawSession);
|
|
525
|
+
if (storedReceiptTerminalBoundaryMismatch({
|
|
526
|
+
ownerStoreDir,
|
|
527
|
+
receiptStoreDir,
|
|
528
|
+
expectedStoreDir,
|
|
529
|
+
storedControl,
|
|
530
|
+
terminalControl,
|
|
531
|
+
receiptTerminalEvidence
|
|
532
|
+
}) ||
|
|
533
|
+
storedReceiptAuthorityMismatch({
|
|
534
|
+
owner,
|
|
535
|
+
receipt,
|
|
536
|
+
receiptSessionId,
|
|
537
|
+
receiptTurnId,
|
|
538
|
+
receiptOpenClawSession,
|
|
539
|
+
receiptMatchesRecoveredSession,
|
|
540
|
+
expectedSessionId,
|
|
541
|
+
expectedTurnId,
|
|
542
|
+
expectedStatePath,
|
|
543
|
+
requestedOpenClawSession,
|
|
544
|
+
requestHash
|
|
545
|
+
})) {
|
|
546
|
+
throw new Error(`terminal idempotency key ${messageId} does not match its original ` +
|
|
547
|
+
"Store, Session, Turn, OpenClaw session, or terminal binding; no terminal input was sent");
|
|
548
|
+
}
|
|
549
|
+
const logPath = stringValue(owner.event_log_path);
|
|
550
|
+
let loggedMessages = [];
|
|
551
|
+
if (logPath) {
|
|
552
|
+
try {
|
|
553
|
+
loggedMessages = readNdjsonLog(logPath)
|
|
554
|
+
.filter((event) => isRecord(event.message) && event.message.id === messageId)
|
|
555
|
+
.map((event) => event.message);
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
loggedMessages = [];
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (loggedMessages.length > 1) {
|
|
562
|
+
throw new Error(`terminal idempotency key ${messageId} has duplicate durable messages`);
|
|
563
|
+
}
|
|
564
|
+
const loggedMessage = loggedMessages[0];
|
|
565
|
+
if (isRecord(loggedMessage) &&
|
|
566
|
+
replayLoggedMessageMismatch(loggedMessage, owner, expectedMessageType, requestText)) {
|
|
567
|
+
throw new Error(`terminal idempotency key ${messageId} does not match its durable message; no terminal input was sent`);
|
|
568
|
+
}
|
|
569
|
+
const durableMessageType = stringValue(receipt.message_type) ??
|
|
570
|
+
(isRecord(loggedMessage) ? stringValue(loggedMessage.type) : undefined);
|
|
571
|
+
const durableMessageBodyHash = stringValue(receipt.message_body_hash) ??
|
|
572
|
+
(isRecord(loggedMessage) && typeof loggedMessage.body === "string"
|
|
573
|
+
? createHash("sha256").update(loggedMessage.body).digest("hex")
|
|
574
|
+
: undefined);
|
|
575
|
+
const expectedMessageBodyHash = createHash("sha256")
|
|
576
|
+
.update(requestText)
|
|
577
|
+
.digest("hex");
|
|
578
|
+
if (durableMessageType !== expectedMessageType ||
|
|
579
|
+
durableMessageBodyHash !== expectedMessageBodyHash) {
|
|
580
|
+
throw new Error(`terminal idempotency key ${messageId} was already used for a different message; no terminal input was sent`);
|
|
581
|
+
}
|
|
582
|
+
return loggedMessage;
|
|
583
|
+
}
|
|
584
|
+
function replayExactStoredTerminalSubmission({ options, terminalControl, requestText, requestHash, messageId, expectedStoreDir, expectedSessionId, expectedTurnId, expectedMessageType, expectedStatePath }) {
|
|
585
|
+
const allMatches = listConversations(expectedStoreDir).flatMap((owner) => terminalBridgeSubmissionReceipts(owner)
|
|
586
|
+
.filter((receipt) => stringValue(receipt.message_id) === messageId)
|
|
587
|
+
.map((receipt) => ({ owner, receipt })));
|
|
588
|
+
const validatedMatches = allMatches.map((match) => ({
|
|
589
|
+
...match,
|
|
590
|
+
loggedMessage: validateStoredTerminalSubmissionMatch({
|
|
591
|
+
...match,
|
|
592
|
+
options,
|
|
593
|
+
terminalControl,
|
|
594
|
+
requestText,
|
|
595
|
+
requestHash,
|
|
596
|
+
expectedStoreDir,
|
|
597
|
+
expectedSessionId,
|
|
598
|
+
expectedTurnId,
|
|
599
|
+
expectedMessageType,
|
|
600
|
+
expectedStatePath
|
|
601
|
+
})
|
|
602
|
+
}));
|
|
603
|
+
const matches = validatedMatches.filter(({ receipt }) => !(receipt.status === "aborted" && receipt.safe_to_retry === true));
|
|
604
|
+
if (matches.length > 1) {
|
|
605
|
+
throw new Error(`terminal idempotency key ${messageId} has multiple durable receipts in one Store`);
|
|
606
|
+
}
|
|
607
|
+
const match = matches[0];
|
|
608
|
+
if (!match) {
|
|
609
|
+
for (const { owner, receipt } of validatedMatches) {
|
|
610
|
+
if (expectedSessionId) {
|
|
611
|
+
if (!exactSafeAbortedRecoveredSessionMatches({
|
|
612
|
+
owner,
|
|
613
|
+
receipt,
|
|
614
|
+
storeDir: expectedStoreDir,
|
|
615
|
+
terminalControl,
|
|
616
|
+
messageId,
|
|
617
|
+
expectedSessionId
|
|
618
|
+
})) {
|
|
619
|
+
throw new Error(`terminal idempotency key ${messageId} does not match its ` +
|
|
620
|
+
"restored retry Session; no terminal input was sent");
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
assertSafeAbortedTerminalRetryBinding({
|
|
625
|
+
owner,
|
|
626
|
+
receipt,
|
|
627
|
+
storeDir: expectedStoreDir,
|
|
628
|
+
terminalControl,
|
|
629
|
+
messageId
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// A prepared-stage failure with a restored ledger proves that tmux was
|
|
634
|
+
// untouched. The same exact id may therefore start a fresh attempt; all
|
|
635
|
+
// immutable boundaries above were still validated before allowing it.
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
const { owner, receipt, loggedMessage } = match;
|
|
639
|
+
const nativeTakeover = isRecord(owner.native_session_takeover)
|
|
640
|
+
? owner.native_session_takeover
|
|
641
|
+
: undefined;
|
|
642
|
+
const currentSubmission = terminalBridgeSubmission(owner);
|
|
643
|
+
const isCurrentSubmission = stringValue(currentSubmission?.message_id) === messageId;
|
|
644
|
+
if (isCurrentSubmission &&
|
|
645
|
+
!isTerminalDispatchOwnerReleasedStatus(owner.status)) {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
if (!["submitted", "enter_dispatched", "agent_accepted"].includes(String(receipt.status))) {
|
|
649
|
+
throw new Error(`terminal idempotency key ${messageId} has durable ${String(receipt.status)} ` +
|
|
650
|
+
"proof and must not be retried; no terminal input was sent");
|
|
651
|
+
}
|
|
652
|
+
const executor = executorForConversation(owner);
|
|
653
|
+
const nativeThreadId = stringValue(receipt.native_thread_id) ??
|
|
654
|
+
stringValue(owner.native_thread_id) ??
|
|
655
|
+
stringValue(nativeTakeover?.terminal_agent_session_id) ??
|
|
656
|
+
stringValue(nativeTakeover?.terminal_agent_expected_session_id) ??
|
|
657
|
+
sessionIdForConversation(owner);
|
|
658
|
+
const proofLevel = String(receipt.status);
|
|
659
|
+
const replayReceipt = dispatchReceipt.terminalSubmissionReplayReceipt({
|
|
660
|
+
proofLevel,
|
|
661
|
+
evidence: receipt.acceptance_evidence,
|
|
662
|
+
expected: {
|
|
663
|
+
source: executor.kind === "codex"
|
|
664
|
+
? "codex_rollout"
|
|
665
|
+
: "claude_transcript",
|
|
666
|
+
nativeThreadId,
|
|
667
|
+
requestHash
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
const replayedMessage = isRecord(loggedMessage)
|
|
671
|
+
? loggedMessage
|
|
672
|
+
: createMessage({
|
|
673
|
+
conversation: owner,
|
|
674
|
+
id: messageId,
|
|
675
|
+
from: "openclaw",
|
|
676
|
+
to: executor.actor,
|
|
677
|
+
type: expectedMessageType,
|
|
678
|
+
body: requestText,
|
|
679
|
+
metadata: {
|
|
680
|
+
executor_kind: executor.kind,
|
|
681
|
+
executor_session: executor.session
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
const acceptanceInvalid = replayReceipt.submission_outcome === "uncertain";
|
|
685
|
+
const callbackExpected = Boolean(owner.gateway_method);
|
|
686
|
+
printJson({
|
|
687
|
+
session_id: sessionIdForConversation(owner),
|
|
688
|
+
turn_id: turnIdForConversation(owner),
|
|
689
|
+
conversation: owner,
|
|
690
|
+
message: replayedMessage,
|
|
691
|
+
delivered: replayReceipt.delivered,
|
|
692
|
+
status: replayReceipt.status,
|
|
693
|
+
submission_outcome: replayReceipt.submission_outcome,
|
|
694
|
+
background: true,
|
|
695
|
+
callback_expected: !acceptanceInvalid && callbackExpected,
|
|
696
|
+
terminal_control: terminalControl,
|
|
697
|
+
executor,
|
|
698
|
+
replayed: true,
|
|
699
|
+
delivery_receipt: replayReceipt.delivery_receipt,
|
|
700
|
+
...(replayReceipt.do_not_retry
|
|
701
|
+
? { do_not_retry: replayReceipt.do_not_retry }
|
|
702
|
+
: {}),
|
|
703
|
+
reason: replayReceipt.delivered
|
|
704
|
+
? "AKK replayed the stored durable native acceptance receipt and sent no additional terminal input."
|
|
705
|
+
: acceptanceInvalid
|
|
706
|
+
? "AKK replayed an invalid stored native acceptance receipt as uncertain and sent no additional terminal input."
|
|
707
|
+
: "AKK replayed the stored transport proof without upgrading it and sent no additional terminal input.",
|
|
708
|
+
openclaw_next_action: replayReceipt.delivered
|
|
709
|
+
? openClawYieldNextAction({
|
|
710
|
+
conversationId: owner.conversation_id,
|
|
711
|
+
sessionId: sessionIdForConversation(owner),
|
|
712
|
+
turnId: turnIdForConversation(owner),
|
|
713
|
+
source: "terminal_control",
|
|
714
|
+
callbackExpected
|
|
715
|
+
})
|
|
716
|
+
: {
|
|
717
|
+
action: "inspect",
|
|
718
|
+
conversation_id: owner.conversation_id,
|
|
719
|
+
session_id: sessionIdForConversation(owner),
|
|
720
|
+
turn_id: turnIdForConversation(owner),
|
|
721
|
+
do_not_retry: true,
|
|
722
|
+
reason: acceptanceInvalid
|
|
723
|
+
? "Stored native acceptance evidence is invalid."
|
|
724
|
+
: "Only terminal transport is proven."
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
const terminalBridgeSubmission = dispatchReceipt.terminalBridgeSubmission;
|
|
730
|
+
const terminalBridgeSubmissionReceipts = dispatchReceipt.terminalBridgeSubmissionReceipts;
|
|
731
|
+
const unresolvedTerminalBridgeSubmission = dispatchReceipt.unresolvedTerminalBridgeSubmission;
|
|
732
|
+
function assertNoUnresolvedTerminalBridgeSubmission(storeDir, terminalControl, currentConversationId, requestText) {
|
|
733
|
+
const requestHash = terminalBridgeRequestFingerprint(requestText);
|
|
734
|
+
for (const candidate of listConversations(storeDir)) {
|
|
735
|
+
const submission = terminalBridgeSubmission(candidate);
|
|
736
|
+
if (candidate.conversation_id === currentConversationId ||
|
|
737
|
+
!submission ||
|
|
738
|
+
isTerminalDispatchOwnerReleasedStatus(effectiveTurnStatus(candidate)) ||
|
|
739
|
+
![
|
|
740
|
+
"prepared",
|
|
741
|
+
"text_injected",
|
|
742
|
+
"enter_dispatched",
|
|
743
|
+
"agent_accepted",
|
|
744
|
+
"submitted",
|
|
745
|
+
"not_accepted",
|
|
746
|
+
"uncertain"
|
|
747
|
+
].includes(String(submission.status))) {
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
if (["submitted", "agent_accepted"].includes(String(submission.status)) &&
|
|
751
|
+
stringValue(submission.request_hash) !== requestHash) {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const nativeTakeover = isRecord(candidate.native_session_takeover)
|
|
755
|
+
? candidate.native_session_takeover
|
|
756
|
+
: undefined;
|
|
757
|
+
if (terminalControlsShareIncarnation(terminalControlFromTakeover(nativeTakeover), terminalControl)) {
|
|
758
|
+
throw new Error(`terminal ${terminalControl.target} has a conflicting ${String(submission.status)} ` +
|
|
759
|
+
`AKK submission in ${candidate.conversation_id}; inspect that conversation and pane, ` +
|
|
760
|
+
"then close it before retrying");
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function prepareManagedSend({ options, statePath, logPath, messageBody, stateLockHeld = false, persist = true, rejectTerminalControl = false }) {
|
|
765
|
+
if (!stateLockHeld) {
|
|
766
|
+
const releaseLock = acquireFileLock(`${statePath}.lock`);
|
|
767
|
+
try {
|
|
768
|
+
return prepareManagedSend({
|
|
769
|
+
options,
|
|
770
|
+
statePath,
|
|
771
|
+
logPath,
|
|
772
|
+
messageBody,
|
|
773
|
+
stateLockHeld: true,
|
|
774
|
+
persist,
|
|
775
|
+
rejectTerminalControl
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
finally {
|
|
779
|
+
releaseLock();
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
const conversation = loadState(statePath);
|
|
783
|
+
if (conversation.status !== "waiting_for_openclaw" ||
|
|
784
|
+
options.type !== "answer") {
|
|
785
|
+
throw new Error(`cannot respond to turn ${turnIdForConversation(conversation)}; ` +
|
|
786
|
+
`turn is ${conversation.status}`);
|
|
787
|
+
}
|
|
788
|
+
const executor = executorForConversation(conversation);
|
|
789
|
+
const type = "answer";
|
|
790
|
+
const nativeTakeoverForSend = isRecord(conversation.native_session_takeover)
|
|
791
|
+
? conversation.native_session_takeover
|
|
792
|
+
: undefined;
|
|
793
|
+
const unresolvedSubmission = unresolvedTerminalBridgeSubmission(conversation);
|
|
794
|
+
if (unresolvedSubmission) {
|
|
795
|
+
throw new Error(`cannot send to ${conversation.conversation_id}; its previous terminal submission is ` +
|
|
796
|
+
`${unresolvedSubmission.status}. Inspect the conversation and terminal pane, then close ` +
|
|
797
|
+
"the AKK conversation before creating a replacement task.");
|
|
798
|
+
}
|
|
799
|
+
if (rejectTerminalControl &&
|
|
800
|
+
terminalControlFromTakeover(nativeTakeoverForSend)) {
|
|
801
|
+
throw new Error("terminal control changed while waiting to send; refresh status and retry");
|
|
802
|
+
}
|
|
803
|
+
const message = createMessage({
|
|
804
|
+
conversation,
|
|
805
|
+
id: stringValue(options.messageId),
|
|
806
|
+
from: "openclaw",
|
|
807
|
+
to: executor.actor,
|
|
808
|
+
type,
|
|
809
|
+
body: messageBody,
|
|
810
|
+
metadata: {
|
|
811
|
+
executor_kind: executor.kind,
|
|
812
|
+
executor_session: executor.session
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
const nextConversation = {
|
|
816
|
+
...applyMessageToConversation(conversation, message),
|
|
817
|
+
executor,
|
|
818
|
+
claude_session: executor.kind === "claude"
|
|
819
|
+
? executor.session
|
|
820
|
+
: conversation.claude_session
|
|
821
|
+
};
|
|
822
|
+
if (persist) {
|
|
823
|
+
saveState(statePath, nextConversation);
|
|
824
|
+
appendEvent(logPath, messageEvent(message));
|
|
825
|
+
runtimeLog("info", "message_created", {
|
|
826
|
+
conversation_id: conversation.conversation_id,
|
|
827
|
+
agent: executor.kind,
|
|
828
|
+
executor_session: executor.session,
|
|
829
|
+
message_type: type,
|
|
830
|
+
state_path: statePath,
|
|
831
|
+
event_log_path: logPath,
|
|
832
|
+
message: textSummary(messageBody)
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
return {
|
|
836
|
+
conversation,
|
|
837
|
+
executor,
|
|
838
|
+
nativeTakeoverForSend,
|
|
839
|
+
message,
|
|
840
|
+
nextConversation
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
async function runSend(options) {
|
|
844
|
+
const messageBody = required(options.message ?? options.request, "--message is required");
|
|
845
|
+
// Ordinary send/respond owns Turn creation only. Reject native lifecycle
|
|
846
|
+
// slash commands before resolving or mutating any Session/Turn state so a
|
|
847
|
+
// caller cannot bypass lifecycle capabilities, CAS tokens, transition
|
|
848
|
+
// recovery, or binding-generation updates by writing directly to tmux.
|
|
849
|
+
terminalSubmissionPayload(messageBody);
|
|
850
|
+
if (options.agentHardTimeoutMinutes !== undefined) {
|
|
851
|
+
positiveMinutes(options.agentHardTimeoutMinutes, "--agent-hard-timeout-minutes");
|
|
852
|
+
}
|
|
853
|
+
if (options.respond === true) {
|
|
854
|
+
return runTurnResponse({ options, messageBody });
|
|
855
|
+
}
|
|
856
|
+
if ((options.type ?? "task") !== "task") {
|
|
857
|
+
throw new Error("ordinary send only accepts message type task; use respond --turn to answer an in-flight Turn");
|
|
858
|
+
}
|
|
859
|
+
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
860
|
+
if (terminalConversation) {
|
|
861
|
+
return runRawTerminalSend(options, messageBody, terminalConversation);
|
|
862
|
+
}
|
|
863
|
+
return runManagedSessionSend(options, messageBody);
|
|
864
|
+
}
|
|
865
|
+
function rawTerminalInitialAuthority({ options, terminal, storeDir }) {
|
|
866
|
+
const claimedSession = soleBoundManagedSessionClaimForTerminal(storeDir, terminal);
|
|
867
|
+
const suppliedExpectedTerminalToken = stringValue(options.expectedTerminalToken);
|
|
868
|
+
const implicitCodexCandidateAuthority = Boolean(terminal.agent === "codex" &&
|
|
869
|
+
suppliedExpectedTerminalToken === undefined &&
|
|
870
|
+
claimedSession?.status === "bound" &&
|
|
871
|
+
isCompleteNativeRollout(claimedSession.binding?.native_process.rollout));
|
|
872
|
+
const knownCodexCompanions = claimedSession
|
|
873
|
+
? codexAllowedCompanionSetForManagedSession({
|
|
874
|
+
storeDir,
|
|
875
|
+
session: claimedSession
|
|
876
|
+
})
|
|
877
|
+
: { additional: [] };
|
|
878
|
+
return {
|
|
879
|
+
claimedSession,
|
|
880
|
+
suppliedExpectedTerminalToken,
|
|
881
|
+
implicitCodexCandidateAuthority,
|
|
882
|
+
knownCodexCompanions
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function assertRawTerminalCandidateAuthority({ terminal, nativeIdentityObservation, deferredCodexCandidateInventory, implicitCodexCandidateAuthority }) {
|
|
886
|
+
if (implicitCodexCandidateAuthority &&
|
|
887
|
+
!deferredCodexCandidateInventory) {
|
|
888
|
+
throw new Error("rollout-backed Codex terminal send requires a fresh complete " +
|
|
889
|
+
"nonempty open-root inventory; refresh AKK list before sending");
|
|
890
|
+
}
|
|
891
|
+
if (nativeIdentityObservation.status === "unavailable" &&
|
|
892
|
+
!deferredCodexCandidateInventory) {
|
|
893
|
+
throw new Error(`native ${terminal.agent} identity observation is ` +
|
|
894
|
+
`unavailable: ${nativeIdentityObservation.reason}`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
async function runRawTerminalSend(options, messageBody, terminalConversation) {
|
|
898
|
+
// A token copied from list is authority for exactly the advertised full
|
|
899
|
+
// terminal selector. Reject aliases and implicit/no-selector resolution
|
|
900
|
+
// before taking locks or touching Store state.
|
|
901
|
+
assertExpectedHandoffTokenUsesExactTerminalSelector({
|
|
902
|
+
options,
|
|
903
|
+
terminal: terminalConversation
|
|
904
|
+
});
|
|
905
|
+
if (!options.background) {
|
|
906
|
+
throw new Error("raw terminal sends require --background so AKK can persist and monitor the submission safely");
|
|
907
|
+
}
|
|
908
|
+
const rawStoreDir = storeDirFromOptions(options);
|
|
909
|
+
await withCanonicalMutationLocks(terminalWriterMutationLocks(rawStoreDir, terminalConversation.terminalControl), async (scopes, resources) => {
|
|
910
|
+
await mutationDispatchLedger.beforeMutation(scopes, resources, options, terminalConversation);
|
|
911
|
+
// Upgrade legacy Stores before resolving their Session authority. The
|
|
912
|
+
// protocol migration, rather than mutable Turn recency, is the only code
|
|
913
|
+
// allowed to materialize Session records from existing Turns.
|
|
914
|
+
ensureStoreWritable(rawStoreDir);
|
|
915
|
+
if (replayExactActiveTerminalSubmission({
|
|
916
|
+
options,
|
|
917
|
+
terminalControl: terminalConversation.terminalControl,
|
|
918
|
+
requestText: String(messageBody),
|
|
919
|
+
expectedStoreDir: rawStoreDir,
|
|
920
|
+
expectedMessageType: "task"
|
|
921
|
+
})) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
terminalListCliFacade.assertTerminalIncarnationCanStartTurn(rawStoreDir, terminalConversation.terminalControl);
|
|
925
|
+
const initialAuthority = rawTerminalInitialAuthority({
|
|
926
|
+
options,
|
|
927
|
+
terminal: terminalConversation,
|
|
928
|
+
storeDir: rawStoreDir
|
|
929
|
+
});
|
|
930
|
+
let { claimedSession, knownCodexCompanions } = initialAuthority;
|
|
931
|
+
const { suppliedExpectedTerminalToken, implicitCodexCandidateAuthority } = initialAuthority;
|
|
932
|
+
const nativeIdentityObservation = await observeCurrentNativeAgentSessionIdentity({
|
|
933
|
+
options,
|
|
934
|
+
agent: terminalConversation.agent,
|
|
935
|
+
pid: terminalConversation.pid,
|
|
936
|
+
cwd: terminalConversation.terminalControl.currentPath,
|
|
937
|
+
preferredSessionId: knownCodexCompanions.primary
|
|
938
|
+
? claimedSession?.binding?.native_thread_id
|
|
939
|
+
: undefined,
|
|
940
|
+
allowedCompanionIdentity: knownCodexCompanions.primary,
|
|
941
|
+
allowedAdditionalIdentities: knownCodexCompanions.additional
|
|
942
|
+
});
|
|
943
|
+
let deferredCodexCandidateInventory;
|
|
944
|
+
if (terminalConversation.agent === "codex" &&
|
|
945
|
+
(suppliedExpectedTerminalToken || implicitCodexCandidateAuthority)) {
|
|
946
|
+
try {
|
|
947
|
+
const inventory = await inspectCodexOpenRootRolloutInventory({
|
|
948
|
+
options,
|
|
949
|
+
pid: terminalConversation.pid,
|
|
950
|
+
cwd: terminalConversation.terminalControl.currentPath
|
|
951
|
+
});
|
|
952
|
+
if (inventory.roots.length > 0) {
|
|
953
|
+
deferredCodexCandidateInventory = inventory;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
if (implicitCodexCandidateAuthority ||
|
|
958
|
+
nativeIdentityObservation.status === "unavailable") {
|
|
959
|
+
throw new Error(`native Codex foreground attribution requires a fresh complete ` +
|
|
960
|
+
`open-root inventory: ${error instanceof Error ? error.message : String(error)}`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
assertRawTerminalCandidateAuthority({
|
|
965
|
+
terminal: terminalConversation,
|
|
966
|
+
nativeIdentityObservation,
|
|
967
|
+
deferredCodexCandidateInventory,
|
|
968
|
+
implicitCodexCandidateAuthority
|
|
969
|
+
});
|
|
970
|
+
let currentNativeIdentity = nativeIdentityObservation.status === "resolved"
|
|
971
|
+
? nativeIdentityObservation.identity
|
|
972
|
+
: undefined;
|
|
973
|
+
// A fresh nonempty inventory is the stronger physical authority for an
|
|
974
|
+
// implicit candidate send. Do not let an earlier verified-absent
|
|
975
|
+
// observation divert this path into the token-only empty handoff.
|
|
976
|
+
const verifiedEmptyHandoff = implicitCodexCandidateAuthority
|
|
977
|
+
? undefined
|
|
978
|
+
: await maybeDetachVerifiedEmptyCodexSource({
|
|
979
|
+
options,
|
|
980
|
+
terminal: terminalConversation,
|
|
981
|
+
sourceSession: claimedSession,
|
|
982
|
+
observation: nativeIdentityObservation
|
|
983
|
+
});
|
|
984
|
+
if (verifiedEmptyHandoff) {
|
|
985
|
+
// The old rollout is conclusively closed. Never carry it forward as
|
|
986
|
+
// a pre-materialization companion for the new virgin Session.
|
|
987
|
+
claimedSession = undefined;
|
|
988
|
+
knownCodexCompanions = { additional: [] };
|
|
989
|
+
currentNativeIdentity = undefined;
|
|
990
|
+
}
|
|
991
|
+
const physicalNativeIdentityBeforeHandoff = currentNativeIdentity;
|
|
992
|
+
let handoff = await maybeAdoptObservedExternalThread({
|
|
993
|
+
options,
|
|
994
|
+
terminal: terminalConversation,
|
|
995
|
+
// A no-token raw send to an already managed rollout-backed Codex pane
|
|
996
|
+
// is an internal follow-current delegation, never a sole-root strict
|
|
997
|
+
// continuation or an external-handoff adoption. The dedicated v3
|
|
998
|
+
// transfer below owns attribution and lineage.
|
|
999
|
+
sourceSession: implicitCodexCandidateAuthority
|
|
1000
|
+
? undefined
|
|
1001
|
+
: claimedSession,
|
|
1002
|
+
resolvedIdentity: currentNativeIdentity,
|
|
1003
|
+
storeDir: rawStoreDir
|
|
1004
|
+
});
|
|
1005
|
+
currentNativeIdentity =
|
|
1006
|
+
handoff.adopted &&
|
|
1007
|
+
terminalConversation.agent === "codex" &&
|
|
1008
|
+
!handoff.session?.binding?.native_process.rollout
|
|
1009
|
+
? physicalNativeIdentityBeforeHandoff
|
|
1010
|
+
: handoff.identity;
|
|
1011
|
+
const deferredCodexForegroundBinding = !handoff.adopted
|
|
1012
|
+
? await prepareDeferredCodexForegroundBinding({
|
|
1013
|
+
options,
|
|
1014
|
+
scope: bindDeferredForegroundWriterScope(scopes, resources),
|
|
1015
|
+
terminal: terminalConversation,
|
|
1016
|
+
sourceSession: claimedSession,
|
|
1017
|
+
observation: nativeIdentityObservation,
|
|
1018
|
+
candidateInventory: deferredCodexCandidateInventory,
|
|
1019
|
+
requestText: String(messageBody),
|
|
1020
|
+
allowImplicitFreshAuthority: implicitCodexCandidateAuthority
|
|
1021
|
+
})
|
|
1022
|
+
: undefined;
|
|
1023
|
+
const freshSendAuthority = decideTerminalSendAuthority({
|
|
1024
|
+
ownership: "conflict",
|
|
1025
|
+
verifiedEmpty: Boolean(verifiedEmptyHandoff),
|
|
1026
|
+
externalHandoff: handoff.adopted,
|
|
1027
|
+
deferred: Boolean(deferredCodexForegroundBinding)
|
|
1028
|
+
});
|
|
1029
|
+
if (freshSendAuthority.mode === "deferred" &&
|
|
1030
|
+
deferredCodexForegroundBinding) {
|
|
1031
|
+
claimedSession = undefined;
|
|
1032
|
+
knownCodexCompanions = { additional: [] };
|
|
1033
|
+
currentNativeIdentity = undefined;
|
|
1034
|
+
handoff = { identity: undefined, adopted: false };
|
|
1035
|
+
}
|
|
1036
|
+
else if ((suppliedExpectedTerminalToken || implicitCodexCandidateAuthority) &&
|
|
1037
|
+
freshSendAuthority.mode === "conflict") {
|
|
1038
|
+
throw new Error("the expected terminal token no longer authorizes the current " +
|
|
1039
|
+
"terminal context; refresh AKK list");
|
|
1040
|
+
}
|
|
1041
|
+
let managedSession = deferredCodexForegroundBinding
|
|
1042
|
+
? undefined
|
|
1043
|
+
: handoff.session ??
|
|
1044
|
+
materializeCurrentManagedSession({
|
|
1045
|
+
options,
|
|
1046
|
+
terminal: terminalConversation,
|
|
1047
|
+
identity: currentNativeIdentity
|
|
1048
|
+
});
|
|
1049
|
+
if (!managedSession && currentNativeIdentity) {
|
|
1050
|
+
managedSession = await reattachManagedSessionForNativeIdentity({
|
|
1051
|
+
options,
|
|
1052
|
+
terminal: terminalConversation,
|
|
1053
|
+
identity: currentNativeIdentity,
|
|
1054
|
+
storeDir: rawStoreDir
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
let pendingRawAttachSessionCreate;
|
|
1058
|
+
if (!managedSession) {
|
|
1059
|
+
if (currentNativeIdentity) {
|
|
1060
|
+
await assertNativeThreadHasExclusiveOwnership({
|
|
1061
|
+
options,
|
|
1062
|
+
agent: terminalConversation.agent,
|
|
1063
|
+
currentPid: terminalConversation.pid,
|
|
1064
|
+
nativeThreadId: currentNativeIdentity.sessionId,
|
|
1065
|
+
storeDir: rawStoreDir,
|
|
1066
|
+
terminalControl: terminalConversation.terminalControl
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
managedSession = createBoundManagedSession({
|
|
1070
|
+
sessionId: deferredCodexForegroundBinding?.targetSessionId ??
|
|
1071
|
+
createManagedSessionId(),
|
|
1072
|
+
terminal: terminalConversation,
|
|
1073
|
+
identity: currentNativeIdentity,
|
|
1074
|
+
lineage: deferredCodexForegroundBinding
|
|
1075
|
+
? {
|
|
1076
|
+
created_by: "attach",
|
|
1077
|
+
previous_session_id: deferredCodexForegroundBinding.sourceSessionId,
|
|
1078
|
+
transition_id: deferredCodexForegroundBinding.transferId
|
|
1079
|
+
}
|
|
1080
|
+
: { created_by: "attach" }
|
|
1081
|
+
});
|
|
1082
|
+
if (terminalConversation.agent === "codex" &&
|
|
1083
|
+
!currentNativeIdentity) {
|
|
1084
|
+
if (deferredCodexForegroundBinding) {
|
|
1085
|
+
managedSession = {
|
|
1086
|
+
...managedSession,
|
|
1087
|
+
// Both endpoints stay fenced from older writers until the
|
|
1088
|
+
// dedicated transfer reaches its resolved receipt.
|
|
1089
|
+
status: "transitioning",
|
|
1090
|
+
last_transition_id: deferredCodexForegroundBinding.transferId
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
pendingRawAttachSessionCreate = managedSession;
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
managedSession = saveManagedSession(rawStoreDir, managedSession, { expectedRevision: null });
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const logicalNativeIdentity = logicalIdentityForManagedSession({
|
|
1100
|
+
storeDir: rawStoreDir,
|
|
1101
|
+
session: managedSession,
|
|
1102
|
+
observedIdentity: currentNativeIdentity
|
|
1103
|
+
});
|
|
1104
|
+
const allowedPreMaterializationIdentity = codexPreMaterializationIdentityForManagedSession({
|
|
1105
|
+
storeDir: rawStoreDir,
|
|
1106
|
+
session: managedSession,
|
|
1107
|
+
observedIdentity: currentNativeIdentity
|
|
1108
|
+
}) ?? (currentNativeIdentity === undefined ||
|
|
1109
|
+
nativeIdentityMatchesCodexPreMaterialization(currentNativeIdentity, knownCodexCompanions.primary)
|
|
1110
|
+
? knownCodexCompanions.primary
|
|
1111
|
+
: undefined);
|
|
1112
|
+
const allowedAdditionalIdentities = allowedPreMaterializationIdentity
|
|
1113
|
+
? knownCodexCompanions.additional
|
|
1114
|
+
: [];
|
|
1115
|
+
const materializedNativeIdentity = currentNativeIdentity?.sessionId === logicalNativeIdentity?.sessionId
|
|
1116
|
+
? currentNativeIdentity
|
|
1117
|
+
: undefined;
|
|
1118
|
+
if (!(handoff.adopted &&
|
|
1119
|
+
terminalConversation.agent === "codex" &&
|
|
1120
|
+
!managedSession.binding?.native_process.rollout)) {
|
|
1121
|
+
await verifyCodexPendingManagedSendStatus({
|
|
1122
|
+
options,
|
|
1123
|
+
terminal: terminalConversation,
|
|
1124
|
+
session: managedSession,
|
|
1125
|
+
logicalIdentity: logicalNativeIdentity,
|
|
1126
|
+
allowedPreMaterializationIdentity,
|
|
1127
|
+
allowedAdditionalIdentities
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
const managedNativeThreadId = logicalNativeIdentity?.sessionId ??
|
|
1131
|
+
managedSession.binding?.native_thread_id;
|
|
1132
|
+
if (managedNativeThreadId) {
|
|
1133
|
+
await assertNativeThreadHasExclusiveOwnership({
|
|
1134
|
+
options,
|
|
1135
|
+
agent: terminalConversation.agent,
|
|
1136
|
+
currentPid: terminalConversation.pid,
|
|
1137
|
+
nativeThreadId: managedNativeThreadId,
|
|
1138
|
+
storeDir: rawStoreDir,
|
|
1139
|
+
terminalControl: terminalConversation.terminalControl,
|
|
1140
|
+
excludedManagedSessionId: managedSession.session_id
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
managedSession = refineManagedSessionNativeIdentity({
|
|
1144
|
+
storeDir: rawStoreDir,
|
|
1145
|
+
session: managedSession,
|
|
1146
|
+
terminalControl: terminalConversation.terminalControl,
|
|
1147
|
+
identity: logicalNativeIdentity
|
|
1148
|
+
});
|
|
1149
|
+
const sessionTurns = managedTurnsForSession(rawStoreDir, managedSession.session_id);
|
|
1150
|
+
const reusableTurn = sessionTurns[0];
|
|
1151
|
+
assertManagedSessionCanStartTurn(sessionTurns);
|
|
1152
|
+
const managed = createManagedTerminalTurn({
|
|
1153
|
+
options,
|
|
1154
|
+
conversationId: terminalConversation.conversationId,
|
|
1155
|
+
agent: terminalConversation.agent,
|
|
1156
|
+
pid: terminalConversation.pid,
|
|
1157
|
+
messageBody,
|
|
1158
|
+
terminalControl: terminalConversation.terminalControl,
|
|
1159
|
+
previousTurn: reusableTurn,
|
|
1160
|
+
managedSession,
|
|
1161
|
+
nativeAgentIdentity: materializedNativeIdentity,
|
|
1162
|
+
deferredForegroundTransferId: deferredCodexForegroundBinding?.transferId
|
|
1163
|
+
});
|
|
1164
|
+
ensureStoreWritable(managed.conversation.store_dir);
|
|
1165
|
+
ensureDir(path.dirname(managed.statePath));
|
|
1166
|
+
await withTerminalDispatchStateScope(scopes, resources, managed.statePath, managed.logPath, async (dispatchScopes, dispatchResources) => {
|
|
1167
|
+
await runTerminalControlSend({
|
|
1168
|
+
transaction: {
|
|
1169
|
+
scopes: dispatchScopes,
|
|
1170
|
+
resources: dispatchResources
|
|
1171
|
+
},
|
|
1172
|
+
options,
|
|
1173
|
+
conversation: managed.conversation,
|
|
1174
|
+
nextConversation: managed.nextConversation,
|
|
1175
|
+
executor: managed.executor,
|
|
1176
|
+
message: managed.message,
|
|
1177
|
+
recordMessageAfterSend: true,
|
|
1178
|
+
recordRawAttachmentAfterSend: reusableTurn === undefined,
|
|
1179
|
+
onTerminalPreflightVerified: pendingRawAttachSessionCreate
|
|
1180
|
+
? async (route) => {
|
|
1181
|
+
const exactRoute = withExactTerminalDispatchRoute(route, {
|
|
1182
|
+
terminalControl: terminalConversation.terminalControl,
|
|
1183
|
+
terminalKey: terminalBridgeRuntimeKey(terminalConversation.terminalControl),
|
|
1184
|
+
storeDir: rawStoreDir,
|
|
1185
|
+
statePath: managed.statePath,
|
|
1186
|
+
logPath: managed.logPath
|
|
1187
|
+
}, (boundRoute) => boundRoute);
|
|
1188
|
+
let createdSession;
|
|
1189
|
+
if (deferredCodexForegroundBinding) {
|
|
1190
|
+
const deferredScope = bindDeferredForegroundApplicationScope(dispatchScopes, dispatchResources);
|
|
1191
|
+
const reserved = await deferredForegroundApplication(options, deferredCodexForegroundBinding.terminal).reserve({
|
|
1192
|
+
scope: deferredScope,
|
|
1193
|
+
boundary: deferredForegroundBoundaryProjection(deferredCodexForegroundBinding),
|
|
1194
|
+
targetSession: pendingRawAttachSessionCreate,
|
|
1195
|
+
messageId: managed.message.id,
|
|
1196
|
+
turnId: turnIdForConversation(managed.conversation)
|
|
1197
|
+
});
|
|
1198
|
+
createdSession = reserved.createdSession;
|
|
1199
|
+
managedSession = createdSession;
|
|
1200
|
+
pendingRawAttachSessionCreate = undefined;
|
|
1201
|
+
return (rollbackRoute) => {
|
|
1202
|
+
assertTerminalDispatchRouteMatches(rollbackRoute, exactRoute);
|
|
1203
|
+
reserved.rollback(deferredScope);
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
createdSession = saveManagedSession(exactRoute.storeDir, pendingRawAttachSessionCreate, {
|
|
1207
|
+
expectedRevision: null
|
|
1208
|
+
});
|
|
1209
|
+
managedSession = createdSession;
|
|
1210
|
+
pendingRawAttachSessionCreate = undefined;
|
|
1211
|
+
return (rollbackRoute) => {
|
|
1212
|
+
assertTerminalDispatchRouteMatches(rollbackRoute, route);
|
|
1213
|
+
const current = loadManagedSession(rollbackRoute.storeDir, createdSession.session_id);
|
|
1214
|
+
if (current.status !== "bound" ||
|
|
1215
|
+
current.revision !== createdSession.revision ||
|
|
1216
|
+
managedSessionBindingToken(current) !==
|
|
1217
|
+
managedSessionBindingToken(createdSession)) {
|
|
1218
|
+
throw new Error(`new raw-attach Session ${createdSession.session_id} changed before pre-transport rollback`);
|
|
1219
|
+
}
|
|
1220
|
+
const detachedAt = cliNow().toISOString();
|
|
1221
|
+
managedSession = saveManagedSession(rollbackRoute.storeDir, {
|
|
1222
|
+
...current,
|
|
1223
|
+
status: "detached",
|
|
1224
|
+
detached_at: detachedAt,
|
|
1225
|
+
updated_at: detachedAt
|
|
1226
|
+
}, {
|
|
1227
|
+
expectedRevision: current.revision
|
|
1228
|
+
});
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
: undefined,
|
|
1232
|
+
allowedPreMaterializationIdentity,
|
|
1233
|
+
allowedAdditionalIdentities,
|
|
1234
|
+
observedHandoff: handoff.adopted && handoff.transition
|
|
1235
|
+
? {
|
|
1236
|
+
terminal: terminalConversation,
|
|
1237
|
+
transition: handoff.transition
|
|
1238
|
+
}
|
|
1239
|
+
: undefined,
|
|
1240
|
+
verifiedEmptyCodexHandoff: verifiedEmptyHandoff?.boundary,
|
|
1241
|
+
deferredCodexForegroundBinding
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
});
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
async function runManagedSessionSend(options, messageBody) {
|
|
1248
|
+
if (stringValue(options.expectedTerminalToken)) {
|
|
1249
|
+
throw new Error("--expected-terminal-token cannot be used with a managed Session; " +
|
|
1250
|
+
"use the exact full terminal selector advertised by AKK list");
|
|
1251
|
+
}
|
|
1252
|
+
const sessionId = required(stringValue(options.session ?? options.conversation ?? options.conversationId), "--session is required for an ordinary managed send");
|
|
1253
|
+
const storeDir = storeDirFromOptions(options);
|
|
1254
|
+
// Read only enough legacy/Session authority to identify the physical
|
|
1255
|
+
// terminal. Store migration is deliberately deferred until the terminal
|
|
1256
|
+
// lock has reconciled any lifecycle fence.
|
|
1257
|
+
let initialSession = tryLoadManagedSession(storeDir, sessionId);
|
|
1258
|
+
let initialTurns = managedTurnsForSession(storeDir, sessionId);
|
|
1259
|
+
if (!initialSession) {
|
|
1260
|
+
const exactTurn = listConversations(storeDir)
|
|
1261
|
+
.filter(isDiscoverableTmuxConversation)
|
|
1262
|
+
.find((turn) => turnIdForConversation(turn) === sessionId ||
|
|
1263
|
+
turn.conversation_id === sessionId);
|
|
1264
|
+
if (exactTurn && sessionIdForConversation(exactTurn) !== sessionId) {
|
|
1265
|
+
throw new Error(`turn ${sessionId} is an execution identity, not an ordinary send target; ` +
|
|
1266
|
+
`send to session ${sessionIdForConversation(exactTurn)} instead`);
|
|
1267
|
+
}
|
|
1268
|
+
if (initialTurns.length === 0) {
|
|
1269
|
+
throw new Error(`managed session ${sessionId} was not found`);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
const legacyBindingTurn = initialTurns[0];
|
|
1273
|
+
const legacyTakeover = legacyBindingTurn && isRecord(legacyBindingTurn.native_session_takeover)
|
|
1274
|
+
? legacyBindingTurn.native_session_takeover
|
|
1275
|
+
: undefined;
|
|
1276
|
+
const rawTerminalId = initialSession?.binding?.terminal_id ??
|
|
1277
|
+
stringValue(legacyTakeover?.native_session_id);
|
|
1278
|
+
if (!rawTerminalId) {
|
|
1279
|
+
throw new Error(`managed Session ${sessionId} has no authoritative terminal binding`);
|
|
1280
|
+
}
|
|
1281
|
+
const storedTerminalControl = initialSession?.binding?.terminal_control ??
|
|
1282
|
+
terminalControlFromTakeover(legacyTakeover);
|
|
1283
|
+
const storedAgent = initialSession?.agent ??
|
|
1284
|
+
(legacyBindingTurn
|
|
1285
|
+
? executorForConversation(legacyBindingTurn).kind
|
|
1286
|
+
: undefined);
|
|
1287
|
+
const storedPid = initialSession?.binding?.native_process.pid ??
|
|
1288
|
+
Number(legacyTakeover?.terminal_agent_pid);
|
|
1289
|
+
const terminalBridge = createTerminalAgentBridge(options);
|
|
1290
|
+
const resolvedTerminal = storedTerminalControl && storedAgent &&
|
|
1291
|
+
Number.isSafeInteger(storedPid) && storedPid > 1
|
|
1292
|
+
? await terminalBridge.resolveStoredTerminal(storedAgent, storedPid, storedTerminalControl, { pid: storedPid })
|
|
1293
|
+
: await terminalBridge.resolveConversationId(rawTerminalId);
|
|
1294
|
+
if (!resolvedTerminal) {
|
|
1295
|
+
throw new Error(`session ${sessionId} is not attached to a live terminal`);
|
|
1296
|
+
}
|
|
1297
|
+
await withCanonicalMutationLocks(terminalWriterMutationLocks(storeDir, resolvedTerminal.terminalControl), async (scopes, resources) => {
|
|
1298
|
+
const lockedStrictSession = tryLoadManagedSession(storeDir, sessionId);
|
|
1299
|
+
if (lockedStrictSession?.agent === "codex" &&
|
|
1300
|
+
lockedStrictSession.session_id === sessionId &&
|
|
1301
|
+
lockedStrictSession.status === "bound" &&
|
|
1302
|
+
isCompleteNativeRollout(lockedStrictSession.binding?.native_process.rollout)) {
|
|
1303
|
+
throw new Error(`Codex rollout-backed managed Session ${sessionId} cannot use a ` +
|
|
1304
|
+
"strict session_id send because an open rollout does not prove the " +
|
|
1305
|
+
"current TUI foreground thread. Refresh AKK list and use its exact " +
|
|
1306
|
+
"selector plus expected_terminal_token. No Turn was created and no " +
|
|
1307
|
+
"terminal input was sent.");
|
|
1308
|
+
}
|
|
1309
|
+
await mutationDispatchLedger.beforeMutation(scopes, resources, options, resolvedTerminal);
|
|
1310
|
+
// A protocol-1/2 Store materializes authoritative Session records as one
|
|
1311
|
+
// atomic migration. Once protocol 3 is active, a missing Session is corrupt
|
|
1312
|
+
// state and must not be reconstructed from whichever Turn looks newest.
|
|
1313
|
+
ensureStoreWritable(storeDir);
|
|
1314
|
+
if (replayExactActiveTerminalSubmission({
|
|
1315
|
+
options,
|
|
1316
|
+
terminalControl: resolvedTerminal.terminalControl,
|
|
1317
|
+
requestText: String(messageBody),
|
|
1318
|
+
expectedStoreDir: storeDir,
|
|
1319
|
+
expectedSessionId: sessionId,
|
|
1320
|
+
expectedMessageType: "task"
|
|
1321
|
+
})) {
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
terminalListCliFacade.assertTerminalIncarnationCanStartTurn(storeDir, resolvedTerminal.terminalControl);
|
|
1325
|
+
let currentSession = tryLoadManagedSession(storeDir, sessionId);
|
|
1326
|
+
let knownCodexCompanions = currentSession
|
|
1327
|
+
? codexAllowedCompanionSetForManagedSession({
|
|
1328
|
+
storeDir,
|
|
1329
|
+
session: currentSession
|
|
1330
|
+
})
|
|
1331
|
+
: { additional: [] };
|
|
1332
|
+
if (currentSession?.agent === "codex" &&
|
|
1333
|
+
knownCodexCompanions.primary) {
|
|
1334
|
+
try {
|
|
1335
|
+
const inventory = await inspectCodexOpenRootRolloutInventory({
|
|
1336
|
+
options,
|
|
1337
|
+
pid: resolvedTerminal.pid,
|
|
1338
|
+
cwd: resolvedTerminal.terminalControl.currentPath
|
|
1339
|
+
});
|
|
1340
|
+
knownCodexCompanions = codexCompanionsPresentInOpenRootInventory(knownCodexCompanions, inventory);
|
|
1341
|
+
}
|
|
1342
|
+
catch {
|
|
1343
|
+
// Inventory proof is an optimization only. Preserve the existing
|
|
1344
|
+
// closed /status fence when exact open-root membership is unavailable.
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
const lockedNativeIdentity = await resolveCurrentNativeAgentSessionIdentity({
|
|
1348
|
+
options,
|
|
1349
|
+
agent: resolvedTerminal.agent,
|
|
1350
|
+
pid: resolvedTerminal.pid,
|
|
1351
|
+
cwd: resolvedTerminal.terminalControl.currentPath,
|
|
1352
|
+
preferredSessionId: knownCodexCompanions.primary
|
|
1353
|
+
? currentSession?.binding?.native_thread_id
|
|
1354
|
+
: undefined,
|
|
1355
|
+
allowedCompanionIdentity: knownCodexCompanions.primary,
|
|
1356
|
+
allowedAdditionalIdentities: knownCodexCompanions.additional
|
|
1357
|
+
});
|
|
1358
|
+
if (!currentSession) {
|
|
1359
|
+
currentSession = materializeCurrentManagedSession({
|
|
1360
|
+
options,
|
|
1361
|
+
terminal: resolvedTerminal,
|
|
1362
|
+
identity: lockedNativeIdentity
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
if (!currentSession ||
|
|
1366
|
+
currentSession.session_id !== sessionId ||
|
|
1367
|
+
currentSession.status !== "bound" ||
|
|
1368
|
+
!currentSession.binding) {
|
|
1369
|
+
throw new Error(`managed Session ${sessionId} is no longer bound; refresh list and retry`);
|
|
1370
|
+
}
|
|
1371
|
+
const currentTurns = managedTurnsForSession(storeDir, sessionId);
|
|
1372
|
+
assertManagedSessionCanStartTurn(currentTurns);
|
|
1373
|
+
if (!bindingMatchesLiveTerminal(currentSession, resolvedTerminal, lockedNativeIdentity, storeDir)) {
|
|
1374
|
+
throw new Error("managed session identity changed while waiting to send; refresh list and retry");
|
|
1375
|
+
}
|
|
1376
|
+
const logicalLockedNativeIdentity = logicalIdentityForManagedSession({
|
|
1377
|
+
storeDir,
|
|
1378
|
+
session: currentSession,
|
|
1379
|
+
observedIdentity: lockedNativeIdentity
|
|
1380
|
+
});
|
|
1381
|
+
const allowedPreMaterializationIdentity = codexPreMaterializationIdentityForManagedSession({
|
|
1382
|
+
storeDir,
|
|
1383
|
+
session: currentSession,
|
|
1384
|
+
observedIdentity: lockedNativeIdentity
|
|
1385
|
+
}) ?? knownCodexCompanions.primary;
|
|
1386
|
+
const allowedAdditionalIdentities = knownCodexCompanions.additional;
|
|
1387
|
+
const materializedLockedNativeIdentity = lockedNativeIdentity?.sessionId === logicalLockedNativeIdentity?.sessionId
|
|
1388
|
+
? lockedNativeIdentity
|
|
1389
|
+
: undefined;
|
|
1390
|
+
await verifyCodexPendingManagedSendStatus({
|
|
1391
|
+
options,
|
|
1392
|
+
terminal: resolvedTerminal,
|
|
1393
|
+
session: currentSession,
|
|
1394
|
+
logicalIdentity: logicalLockedNativeIdentity,
|
|
1395
|
+
allowedPreMaterializationIdentity,
|
|
1396
|
+
allowedAdditionalIdentities
|
|
1397
|
+
});
|
|
1398
|
+
const managedNativeThreadId = logicalLockedNativeIdentity?.sessionId ??
|
|
1399
|
+
currentSession.binding.native_thread_id;
|
|
1400
|
+
if (managedNativeThreadId) {
|
|
1401
|
+
await assertNativeThreadHasExclusiveOwnership({
|
|
1402
|
+
options,
|
|
1403
|
+
agent: resolvedTerminal.agent,
|
|
1404
|
+
currentPid: resolvedTerminal.pid,
|
|
1405
|
+
nativeThreadId: managedNativeThreadId,
|
|
1406
|
+
storeDir,
|
|
1407
|
+
terminalControl: resolvedTerminal.terminalControl,
|
|
1408
|
+
excludedManagedSessionId: currentSession.session_id
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
currentSession = refineManagedSessionNativeIdentity({
|
|
1412
|
+
storeDir,
|
|
1413
|
+
session: currentSession,
|
|
1414
|
+
terminalControl: resolvedTerminal.terminalControl,
|
|
1415
|
+
identity: logicalLockedNativeIdentity
|
|
1416
|
+
});
|
|
1417
|
+
const managed = createManagedTerminalTurn({
|
|
1418
|
+
options,
|
|
1419
|
+
conversationId: resolvedTerminal.conversationId,
|
|
1420
|
+
agent: resolvedTerminal.agent,
|
|
1421
|
+
pid: resolvedTerminal.pid,
|
|
1422
|
+
messageBody,
|
|
1423
|
+
terminalControl: resolvedTerminal.terminalControl,
|
|
1424
|
+
previousTurn: currentTurns[0],
|
|
1425
|
+
managedSession: currentSession,
|
|
1426
|
+
nativeAgentIdentity: materializedLockedNativeIdentity
|
|
1427
|
+
});
|
|
1428
|
+
ensureStoreWritable(managed.conversation.store_dir);
|
|
1429
|
+
ensureDir(path.dirname(managed.statePath));
|
|
1430
|
+
await withTerminalDispatchStateScope(scopes, resources, managed.statePath, managed.logPath, async (dispatchScopes, dispatchResources) => {
|
|
1431
|
+
await runTerminalControlSend({
|
|
1432
|
+
transaction: {
|
|
1433
|
+
scopes: dispatchScopes,
|
|
1434
|
+
resources: dispatchResources
|
|
1435
|
+
},
|
|
1436
|
+
options,
|
|
1437
|
+
conversation: managed.conversation,
|
|
1438
|
+
nextConversation: managed.nextConversation,
|
|
1439
|
+
executor: managed.executor,
|
|
1440
|
+
message: managed.message,
|
|
1441
|
+
recordMessageAfterSend: true,
|
|
1442
|
+
allowedPreMaterializationIdentity,
|
|
1443
|
+
allowedAdditionalIdentities
|
|
1444
|
+
});
|
|
1445
|
+
});
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
async function runRespond(options) {
|
|
1449
|
+
const turnId = required(stringValue(options.turn ?? options.conversation ?? options.conversationId), "--turn is required");
|
|
1450
|
+
return runSend({
|
|
1451
|
+
...options,
|
|
1452
|
+
turn: turnId,
|
|
1453
|
+
conversation: turnId,
|
|
1454
|
+
session: undefined,
|
|
1455
|
+
type: "answer",
|
|
1456
|
+
respond: true
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
async function runTurnResponse({ options, messageBody }) {
|
|
1460
|
+
const loaded = loadConversationFromOptions(options);
|
|
1461
|
+
const { statePath, logPath } = loaded;
|
|
1462
|
+
const initialConversation = loaded.conversation;
|
|
1463
|
+
const requestedOpenClawSession = stringValue(options.openclawSession);
|
|
1464
|
+
if (requestedOpenClawSession &&
|
|
1465
|
+
initialConversation.openclaw_session !== requestedOpenClawSession) {
|
|
1466
|
+
throw new Error(`turn ${turnIdForConversation(initialConversation)} belongs to a ` +
|
|
1467
|
+
"different OpenClaw session; no terminal input was sent");
|
|
1468
|
+
}
|
|
1469
|
+
const nativeTakeover = isRecord(initialConversation.native_session_takeover)
|
|
1470
|
+
? initialConversation.native_session_takeover
|
|
1471
|
+
: undefined;
|
|
1472
|
+
const storedTerminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
1473
|
+
const nativeTerminalId = stringValue(nativeTakeover?.native_session_id);
|
|
1474
|
+
if (!storedTerminalControl || !nativeTerminalId) {
|
|
1475
|
+
throw new Error(`turn ${turnIdForConversation(initialConversation)} is not attached to a live terminal`);
|
|
1476
|
+
}
|
|
1477
|
+
const storedPid = Number(nativeTakeover?.terminal_agent_pid);
|
|
1478
|
+
const storedAgent = executorForConversation(initialConversation).kind;
|
|
1479
|
+
const liveTerminal = Number.isSafeInteger(storedPid) && storedPid > 1
|
|
1480
|
+
? await createTerminalAgentBridge(options).resolveStoredTerminal(storedAgent, storedPid, storedTerminalControl, terminalRuntimeIdentityForConversation(initialConversation, storedTerminalControl))
|
|
1481
|
+
: undefined;
|
|
1482
|
+
if (!liveTerminal ||
|
|
1483
|
+
liveTerminal.agent !== executorForConversation(initialConversation).kind ||
|
|
1484
|
+
!terminalControlsShareIncarnation(liveTerminal.terminalControl, storedTerminalControl)) {
|
|
1485
|
+
throw new Error(`turn ${turnIdForConversation(initialConversation)} is not attached to its expected live terminal`);
|
|
1486
|
+
}
|
|
1487
|
+
const terminalControl = liveTerminal.terminalControl;
|
|
1488
|
+
const responseStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
1489
|
+
await withCanonicalMutationLocks(terminalWriterMutationLocks(responseStoreDir, terminalControl), async (scopes, resources) => {
|
|
1490
|
+
// Legacy identity migration may write the Turn, so keep it inside the
|
|
1491
|
+
// canonical terminal -> Store writer -> state ordering as well.
|
|
1492
|
+
await migrateLegacyTerminalAgentIdentity({ ...loaded, options });
|
|
1493
|
+
await withTerminalDispatchStateScope(scopes, resources, statePath, logPath, async (dispatchScopes, dispatchResources) => {
|
|
1494
|
+
let lockedConversation = loadState(statePath);
|
|
1495
|
+
let lockedTakeover = isRecord(lockedConversation.native_session_takeover)
|
|
1496
|
+
? lockedConversation.native_session_takeover
|
|
1497
|
+
: undefined;
|
|
1498
|
+
const lockedControl = terminalControlFromTakeover(lockedTakeover);
|
|
1499
|
+
if (stringValue(lockedTakeover?.native_session_id) !== nativeTerminalId ||
|
|
1500
|
+
executorForConversation(lockedConversation).kind !== liveTerminal.agent ||
|
|
1501
|
+
!lockedControl ||
|
|
1502
|
+
!terminalControlsShareIncarnation(lockedControl, terminalControl)) {
|
|
1503
|
+
throw new Error("terminal control changed while waiting to respond; refresh status and retry");
|
|
1504
|
+
}
|
|
1505
|
+
if (requestedOpenClawSession &&
|
|
1506
|
+
lockedConversation.openclaw_session !== requestedOpenClawSession) {
|
|
1507
|
+
throw new Error(`turn ${turnIdForConversation(lockedConversation)} belongs to a ` +
|
|
1508
|
+
"different OpenClaw session; no terminal input was sent");
|
|
1509
|
+
}
|
|
1510
|
+
lockedConversation = refineTerminalTurnEndpoint({
|
|
1511
|
+
conversation: lockedConversation,
|
|
1512
|
+
statePath,
|
|
1513
|
+
terminalControl
|
|
1514
|
+
});
|
|
1515
|
+
lockedTakeover = isRecord(lockedConversation.native_session_takeover)
|
|
1516
|
+
? lockedConversation.native_session_takeover
|
|
1517
|
+
: undefined;
|
|
1518
|
+
if (replayExactActiveTerminalSubmission({
|
|
1519
|
+
options,
|
|
1520
|
+
terminalControl,
|
|
1521
|
+
requestText: String(messageBody),
|
|
1522
|
+
expectedStoreDir: responseStoreDir,
|
|
1523
|
+
expectedSessionId: sessionIdForConversation(lockedConversation),
|
|
1524
|
+
expectedTurnId: turnIdForConversation(lockedConversation),
|
|
1525
|
+
expectedMessageType: "answer",
|
|
1526
|
+
expectedStatePath: statePath
|
|
1527
|
+
})) {
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
if (lockedConversation.status !== "waiting_for_openclaw") {
|
|
1531
|
+
throw new Error(`cannot respond to turn ${turnIdForConversation(lockedConversation)}; ` +
|
|
1532
|
+
`turn is ${lockedConversation.status}, not waiting_for_openclaw`);
|
|
1533
|
+
}
|
|
1534
|
+
const prepared = prepareManagedSend({
|
|
1535
|
+
options: { ...options, type: "answer" },
|
|
1536
|
+
statePath,
|
|
1537
|
+
logPath,
|
|
1538
|
+
messageBody,
|
|
1539
|
+
stateLockHeld: true,
|
|
1540
|
+
persist: false
|
|
1541
|
+
});
|
|
1542
|
+
const preparedTakeover = isRecord(prepared.conversation.native_session_takeover)
|
|
1543
|
+
? prepared.conversation.native_session_takeover
|
|
1544
|
+
: undefined;
|
|
1545
|
+
const terminalAgentPid = Number(preparedTakeover?.terminal_agent_pid);
|
|
1546
|
+
let responseManagedSession = tryLoadManagedSession(responseStoreDir, sessionIdForConversation(prepared.conversation));
|
|
1547
|
+
const responseCodexCompanions = prepared.executor.kind === "codex" && responseManagedSession
|
|
1548
|
+
? codexAllowedCompanionSetForManagedSession({
|
|
1549
|
+
storeDir: responseStoreDir,
|
|
1550
|
+
session: responseManagedSession
|
|
1551
|
+
})
|
|
1552
|
+
: { additional: [] };
|
|
1553
|
+
const currentNativeIdentity = await resolveCurrentNativeAgentSessionIdentity({
|
|
1554
|
+
options,
|
|
1555
|
+
agent: prepared.executor.kind,
|
|
1556
|
+
pid: terminalAgentPid,
|
|
1557
|
+
cwd: terminalControl.currentPath,
|
|
1558
|
+
preferredSessionId: responseCodexCompanions.primary
|
|
1559
|
+
? stringValue(preparedTakeover?.terminal_agent_session_id)
|
|
1560
|
+
: undefined,
|
|
1561
|
+
allowedCompanionIdentity: responseCodexCompanions.primary,
|
|
1562
|
+
allowedAdditionalIdentities: responseCodexCompanions.additional
|
|
1563
|
+
});
|
|
1564
|
+
assertNativeAgentIdentityForTurn({
|
|
1565
|
+
conversation: prepared.conversation,
|
|
1566
|
+
currentIdentity: currentNativeIdentity,
|
|
1567
|
+
operation: "respond to"
|
|
1568
|
+
});
|
|
1569
|
+
if (responseManagedSession) {
|
|
1570
|
+
const logicalResponseIdentity = logicalIdentityForManagedSession({
|
|
1571
|
+
storeDir: responseStoreDir,
|
|
1572
|
+
session: responseManagedSession,
|
|
1573
|
+
observedIdentity: currentNativeIdentity
|
|
1574
|
+
});
|
|
1575
|
+
responseManagedSession = refineManagedSessionNativeIdentity({
|
|
1576
|
+
storeDir: responseStoreDir,
|
|
1577
|
+
session: responseManagedSession,
|
|
1578
|
+
terminalControl,
|
|
1579
|
+
identity: logicalResponseIdentity
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
const currentTerminalControl = terminalControlFromTakeover(prepared.nativeTakeoverForSend);
|
|
1583
|
+
if (!currentTerminalControl ||
|
|
1584
|
+
!terminalControlsShareIncarnation(currentTerminalControl, terminalControl)) {
|
|
1585
|
+
throw new Error("terminal control changed while waiting to respond; refresh status and retry");
|
|
1586
|
+
}
|
|
1587
|
+
const responseOptions = {
|
|
1588
|
+
...options,
|
|
1589
|
+
type: "answer",
|
|
1590
|
+
agentTimeoutMinutes: options.agentTimeoutMinutes ??
|
|
1591
|
+
preparedTakeover?.terminal_bridge_inactivity_timeout_minutes ??
|
|
1592
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES,
|
|
1593
|
+
agentHardTimeoutMinutes: options.agentHardTimeoutMinutes ??
|
|
1594
|
+
preparedTakeover?.terminal_bridge_hard_timeout_minutes ??
|
|
1595
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES
|
|
1596
|
+
};
|
|
1597
|
+
await runTerminalControlSend({
|
|
1598
|
+
transaction: {
|
|
1599
|
+
scopes: dispatchScopes,
|
|
1600
|
+
resources: dispatchResources
|
|
1601
|
+
},
|
|
1602
|
+
options: responseOptions,
|
|
1603
|
+
conversation: prepared.conversation,
|
|
1604
|
+
nextConversation: prepared.nextConversation,
|
|
1605
|
+
executor: prepared.executor,
|
|
1606
|
+
message: prepared.message,
|
|
1607
|
+
recordMessageAfterSend: true,
|
|
1608
|
+
allowedPreMaterializationIdentity: responseCodexCompanions.primary,
|
|
1609
|
+
allowedAdditionalIdentities: responseCodexCompanions.additional,
|
|
1610
|
+
continuingTurnResponse: true
|
|
1611
|
+
});
|
|
1612
|
+
});
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
async function runApprove(options) {
|
|
1616
|
+
const terminalConversation = await resolveTerminalConversationFromOptions(options);
|
|
1617
|
+
if (terminalConversation) {
|
|
1618
|
+
assertExpectedHandoffTokenUsesExactTerminalSelector({
|
|
1619
|
+
options,
|
|
1620
|
+
terminal: terminalConversation
|
|
1621
|
+
});
|
|
1622
|
+
if (options.autoApproved === true) {
|
|
1623
|
+
throw new Error("automatic approval requires an exact managed Turn and cannot use a raw terminal selector");
|
|
1624
|
+
}
|
|
1625
|
+
await runTerminalConversationApprove({
|
|
1626
|
+
options,
|
|
1627
|
+
terminal: terminalConversation
|
|
1628
|
+
});
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
const loaded = loadConversationFromOptions(options);
|
|
1632
|
+
const { statePath, logPath } = loaded;
|
|
1633
|
+
assertAutoApprovalCallbackRoute({
|
|
1634
|
+
options,
|
|
1635
|
+
conversation: loaded.conversation,
|
|
1636
|
+
statePath
|
|
1637
|
+
});
|
|
1638
|
+
const conversation = await migrateLegacyTerminalAgentIdentity({
|
|
1639
|
+
...loaded,
|
|
1640
|
+
options
|
|
1641
|
+
});
|
|
1642
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
1643
|
+
? conversation.native_session_takeover
|
|
1644
|
+
: undefined;
|
|
1645
|
+
const terminalControl = terminalControlFromTakeover(nativeTakeover);
|
|
1646
|
+
if (!terminalControl) {
|
|
1647
|
+
throw new Error(`conversation ${conversation.conversation_id} is not controlled through a terminal`);
|
|
1648
|
+
}
|
|
1649
|
+
const autoApproved = options.autoApproved === true;
|
|
1650
|
+
const callbackAuthority = autoApprovalCallbackAuthorityFromOptions(options);
|
|
1651
|
+
if (!["waiting_for_agent", "waiting_for_openclaw"].includes(conversation.status) &&
|
|
1652
|
+
!(autoApproved && callbackAuthority)) {
|
|
1653
|
+
throw new Error(`cannot approve ${conversation.conversation_id}; conversation is ${conversation.status}`);
|
|
1654
|
+
}
|
|
1655
|
+
const executor = executorForConversation(conversation);
|
|
1656
|
+
const monitoredApproval = isRecord(nativeTakeover?.["terminal_bridge_approval"])
|
|
1657
|
+
? nativeTakeover.terminal_bridge_approval
|
|
1658
|
+
: undefined;
|
|
1659
|
+
const suppliedExpectedFingerprint = stringValue(options.expectedApprovalFingerprint);
|
|
1660
|
+
const expectedFingerprint = suppliedExpectedFingerprint ??
|
|
1661
|
+
stringValue(monitoredApproval?.fingerprint);
|
|
1662
|
+
const claudeScreenApproval = executor.kind === "claude";
|
|
1663
|
+
if (claudeScreenApproval) {
|
|
1664
|
+
const monitoredState = isRecord(monitoredApproval?.approval_state)
|
|
1665
|
+
? monitoredApproval.approval_state
|
|
1666
|
+
: undefined;
|
|
1667
|
+
const pendingDispatch = isRecord(nativeTakeover?.terminal_bridge_approval_dispatch)
|
|
1668
|
+
? nativeTakeover.terminal_bridge_approval_dispatch
|
|
1669
|
+
: undefined;
|
|
1670
|
+
const lastApprovalFingerprint = stringValue(nativeTakeover?.terminal_bridge_last_approval_fingerprint);
|
|
1671
|
+
const lastApprovalMessageId = stringValue(nativeTakeover?.terminal_bridge_last_approval_message_id);
|
|
1672
|
+
const currentMessageId = stringValue(nativeTakeover?.terminal_bridge_message_id);
|
|
1673
|
+
const approvalResolvedAt = validTimestampMs(nativeTakeover?.terminal_bridge_approval_resolved_at);
|
|
1674
|
+
if (autoApproved &&
|
|
1675
|
+
callbackAuthority === undefined &&
|
|
1676
|
+
monitoredApproval === undefined &&
|
|
1677
|
+
pendingDispatch === undefined &&
|
|
1678
|
+
conversation.status === "waiting_for_agent" &&
|
|
1679
|
+
suppliedExpectedFingerprint !== undefined &&
|
|
1680
|
+
suppliedExpectedFingerprint === lastApprovalFingerprint &&
|
|
1681
|
+
lastApprovalMessageId !== undefined &&
|
|
1682
|
+
lastApprovalMessageId === currentMessageId &&
|
|
1683
|
+
approvalResolvedAt !== undefined) {
|
|
1684
|
+
const monitor = ensureTerminalBridgeMonitorAfterApproval({
|
|
1685
|
+
conversation,
|
|
1686
|
+
statePath,
|
|
1687
|
+
logPath,
|
|
1688
|
+
terminalControl,
|
|
1689
|
+
options,
|
|
1690
|
+
reason: "approval_already_resolved"
|
|
1691
|
+
});
|
|
1692
|
+
printJson({
|
|
1693
|
+
conversation,
|
|
1694
|
+
approved: false,
|
|
1695
|
+
already_approved: true,
|
|
1696
|
+
blocked: false,
|
|
1697
|
+
reason: "Claude screen approval fingerprint was already consumed",
|
|
1698
|
+
terminal_control: terminalControl,
|
|
1699
|
+
monitor_pid: monitor.monitorPid ?? null,
|
|
1700
|
+
monitor_handoff_pid: monitor.handoffWatchdog?.pid ?? null
|
|
1701
|
+
});
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
// A provenance-bound callback whose approval is already absent must reach
|
|
1705
|
+
// the terminal+state lock. Only the persisted callback message and the
|
|
1706
|
+
// locked consumed-approval receipt may classify it as an idempotent replay.
|
|
1707
|
+
if (callbackAuthority === undefined || monitoredApproval !== undefined) {
|
|
1708
|
+
const notifiedAt = validTimestampMs(monitoredApproval?.notified_at);
|
|
1709
|
+
if (conversation.status !== "waiting_for_openclaw" ||
|
|
1710
|
+
monitoredState?.decision_mode !== "keys" ||
|
|
1711
|
+
!stringValue(monitoredApproval?.fingerprint)) {
|
|
1712
|
+
printJson({
|
|
1713
|
+
conversation,
|
|
1714
|
+
approved: false,
|
|
1715
|
+
blocked: true,
|
|
1716
|
+
reason: "Claude screen approval requires a current managed-turn approval notification",
|
|
1717
|
+
terminal_control: terminalControl
|
|
1718
|
+
});
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
if (notifiedAt === undefined ||
|
|
1722
|
+
cliNowMs() - notifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS) {
|
|
1723
|
+
printJson({
|
|
1724
|
+
conversation,
|
|
1725
|
+
approved: false,
|
|
1726
|
+
blocked: true,
|
|
1727
|
+
reason: "Claude screen approval expired; inspect and resolve the terminal manually",
|
|
1728
|
+
terminal_control: terminalControl
|
|
1729
|
+
});
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
if (!suppliedExpectedFingerprint ||
|
|
1733
|
+
expectedFingerprint !== monitoredApproval?.fingerprint) {
|
|
1734
|
+
printJson({
|
|
1735
|
+
conversation,
|
|
1736
|
+
approved: false,
|
|
1737
|
+
blocked: true,
|
|
1738
|
+
reason: "Claude screen approval requires the latest notified fingerprint",
|
|
1739
|
+
terminal_control: terminalControl
|
|
1740
|
+
});
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
if (pendingDispatch?.state === "reserved" &&
|
|
1744
|
+
pendingDispatch.terminal_bridge_message_id ===
|
|
1745
|
+
nativeTakeover?.terminal_bridge_message_id) {
|
|
1746
|
+
printJson({
|
|
1747
|
+
conversation,
|
|
1748
|
+
approved: false,
|
|
1749
|
+
blocked: true,
|
|
1750
|
+
reason: "a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually",
|
|
1751
|
+
terminal_control: terminalControl
|
|
1752
|
+
});
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (expectedFingerprint ===
|
|
1756
|
+
stringValue(nativeTakeover?.terminal_bridge_last_approval_fingerprint)) {
|
|
1757
|
+
printJson({
|
|
1758
|
+
conversation,
|
|
1759
|
+
approved: false,
|
|
1760
|
+
blocked: true,
|
|
1761
|
+
reason: "Claude screen approval fingerprint was already consumed",
|
|
1762
|
+
terminal_control: terminalControl
|
|
1763
|
+
});
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
return runManagedApprovalDispatch({
|
|
1769
|
+
options, conversation, statePath, logPath,
|
|
1770
|
+
nativeTakeover: nativeTakeover,
|
|
1771
|
+
terminalControl, executor, monitoredApproval, expectedFingerprint,
|
|
1772
|
+
autoApproved, claudeScreenApproval
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
function approvalPolicyCandidateForInspection({ agent, currentTerminalControl, inspection, fingerprint }) {
|
|
1776
|
+
const evidence = inspection.approval.approvable
|
|
1777
|
+
? inspection.approval.policyEvidence
|
|
1778
|
+
: undefined;
|
|
1779
|
+
return {
|
|
1780
|
+
agent,
|
|
1781
|
+
kind: evidence?.kind ?? inspection.approval.promptKind ?? "unknown",
|
|
1782
|
+
decisionMode: inspection.approval.approvable
|
|
1783
|
+
? inspection.approval.action.mode ?? "keys"
|
|
1784
|
+
: undefined,
|
|
1785
|
+
command: evidence?.command ?? inspection.approval.command,
|
|
1786
|
+
cwd: evidence?.cwd ?? inspection.approval.cwd ?? currentTerminalControl.currentPath,
|
|
1787
|
+
fingerprint: fingerprint ?? "",
|
|
1788
|
+
terminalTarget: currentTerminalControl.target,
|
|
1789
|
+
...(evidence?.source === "claude_transcript"
|
|
1790
|
+
? {
|
|
1791
|
+
evidenceSource: "claude_transcript",
|
|
1792
|
+
evidenceFingerprint: evidence.evidenceFingerprint
|
|
1793
|
+
}
|
|
1794
|
+
: {})
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
async function runManagedApprovalDispatch({ options, conversation, statePath, logPath, nativeTakeover, terminalControl, executor, monitoredApproval, expectedFingerprint, autoApproved, claudeScreenApproval }) {
|
|
1798
|
+
const policyRuleId = stringValue(options.policyRuleId);
|
|
1799
|
+
const policyFingerprint = stringValue(options.policyFingerprint);
|
|
1800
|
+
const autoApprovalPolicy = autoApproved
|
|
1801
|
+
? parseJsonOption(options.autoApprovalPolicyJson, "--auto-approval-policy-json")
|
|
1802
|
+
: undefined;
|
|
1803
|
+
let executorPolicyDecision;
|
|
1804
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDirFromOptions(options), terminalControl, { timeoutMs: 30000 });
|
|
1805
|
+
let terminalLockReleased = false;
|
|
1806
|
+
const releaseApprovalTerminalLock = () => {
|
|
1807
|
+
if (!terminalLockReleased) {
|
|
1808
|
+
terminalLockReleased = true;
|
|
1809
|
+
releaseTerminalLock();
|
|
1810
|
+
}
|
|
1811
|
+
};
|
|
1812
|
+
let releaseStateLock;
|
|
1813
|
+
let approvalDispatchReserved = false;
|
|
1814
|
+
const releaseApprovalStateLock = () => {
|
|
1815
|
+
if (releaseStateLock) {
|
|
1816
|
+
const release = releaseStateLock;
|
|
1817
|
+
releaseStateLock = undefined;
|
|
1818
|
+
release();
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
|
|
1822
|
+
const runApprovalWithStateLock = async () => {
|
|
1823
|
+
let approval;
|
|
1824
|
+
let lockedConversation = conversation;
|
|
1825
|
+
const currentConversation = loadState(statePath);
|
|
1826
|
+
const currentTakeover = isRecord(currentConversation.native_session_takeover)
|
|
1827
|
+
? currentConversation.native_session_takeover
|
|
1828
|
+
: undefined;
|
|
1829
|
+
const currentControl = terminalControlFromTakeover(currentTakeover);
|
|
1830
|
+
const currentApproval = isRecord(currentTakeover?.terminal_bridge_approval)
|
|
1831
|
+
? currentTakeover.terminal_bridge_approval
|
|
1832
|
+
: undefined;
|
|
1833
|
+
const callbackAuthorityState = assertAutoApprovalCallbackAuthority({
|
|
1834
|
+
options,
|
|
1835
|
+
conversation: currentConversation,
|
|
1836
|
+
statePath,
|
|
1837
|
+
takeover: currentTakeover,
|
|
1838
|
+
approval: currentApproval,
|
|
1839
|
+
expectedFingerprint
|
|
1840
|
+
});
|
|
1841
|
+
if (!currentControl ||
|
|
1842
|
+
!terminalControlsShareIncarnation(currentControl, terminalControl) ||
|
|
1843
|
+
(callbackAuthorityState !== "already_approved" &&
|
|
1844
|
+
(currentConversation.status !== conversation.status ||
|
|
1845
|
+
currentTakeover?.terminal_bridge_message_id !==
|
|
1846
|
+
nativeTakeover?.terminal_bridge_message_id ||
|
|
1847
|
+
(claudeScreenApproval &&
|
|
1848
|
+
currentApproval?.fingerprint !== monitoredApproval?.fingerprint)))) {
|
|
1849
|
+
throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
|
|
1850
|
+
}
|
|
1851
|
+
lockedConversation = currentConversation;
|
|
1852
|
+
if (callbackAuthorityState === "already_approved") {
|
|
1853
|
+
releaseApprovalStateLock();
|
|
1854
|
+
printJson({
|
|
1855
|
+
conversation: lockedConversation,
|
|
1856
|
+
approved: false,
|
|
1857
|
+
already_approved: true,
|
|
1858
|
+
blocked: false,
|
|
1859
|
+
reason: "automatic approval callback was already handled",
|
|
1860
|
+
terminal_control: currentControl
|
|
1861
|
+
});
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
assertManagedTerminalDispatchOwner({
|
|
1865
|
+
storeDir: writerStoreDir,
|
|
1866
|
+
conversation: currentConversation,
|
|
1867
|
+
terminalControl: currentControl,
|
|
1868
|
+
action: "approve"
|
|
1869
|
+
});
|
|
1870
|
+
const currentRuntimeIdentity = terminalRuntimeIdentityForConversation(currentConversation, currentControl);
|
|
1871
|
+
approval = await createTerminalAgentBridge(options).approve(executor.kind, currentControl, {
|
|
1872
|
+
expectedFingerprint,
|
|
1873
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
1874
|
+
runtime: currentRuntimeIdentity,
|
|
1875
|
+
managedRequest: terminalDurableRequestForConversation(currentConversation, currentControl),
|
|
1876
|
+
requiredDecisionMode: autoApproved && executor.kind === "claude" ? "keys" : undefined,
|
|
1877
|
+
authorize: autoApproved
|
|
1878
|
+
? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
|
|
1879
|
+
if (!autoApprovalPolicy) {
|
|
1880
|
+
return {
|
|
1881
|
+
approved: false,
|
|
1882
|
+
reason: "automatic approval requires an executor-side policy"
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
const candidate = approvalPolicyCandidateForInspection({
|
|
1886
|
+
agent,
|
|
1887
|
+
currentTerminalControl,
|
|
1888
|
+
inspection,
|
|
1889
|
+
fingerprint
|
|
1890
|
+
});
|
|
1891
|
+
executorPolicyDecision = evaluateApprovalPolicy({
|
|
1892
|
+
policy: autoApprovalPolicy,
|
|
1893
|
+
candidate
|
|
1894
|
+
});
|
|
1895
|
+
if (executorPolicyDecision.action !== "approve") {
|
|
1896
|
+
return {
|
|
1897
|
+
approved: false,
|
|
1898
|
+
reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
|
|
1902
|
+
return {
|
|
1903
|
+
approved: false,
|
|
1904
|
+
reason: "executor-side auto-approval rule changed before execution"
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
if (policyFingerprint &&
|
|
1908
|
+
executorPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
1909
|
+
return {
|
|
1910
|
+
approved: false,
|
|
1911
|
+
reason: "executor-side auto-approval policy changed before execution"
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
return { approved: true };
|
|
1915
|
+
}
|
|
1916
|
+
: undefined,
|
|
1917
|
+
beforeKeyDispatch: claudeScreenApproval
|
|
1918
|
+
? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
|
|
1919
|
+
if (autoApproved) {
|
|
1920
|
+
if (!autoApprovalPolicy) {
|
|
1921
|
+
throw new Error("automatic approval requires an executor-side policy before dispatch");
|
|
1922
|
+
}
|
|
1923
|
+
const freshPolicyDecision = evaluateApprovalPolicy({
|
|
1924
|
+
policy: autoApprovalPolicy,
|
|
1925
|
+
candidate: approvalPolicyCandidateForInspection({
|
|
1926
|
+
agent: executor.kind,
|
|
1927
|
+
currentTerminalControl: dispatchControl,
|
|
1928
|
+
inspection,
|
|
1929
|
+
fingerprint
|
|
1930
|
+
})
|
|
1931
|
+
});
|
|
1932
|
+
if (freshPolicyDecision.action !== "approve") {
|
|
1933
|
+
throw new Error(`executor-side auto-approval policy rejected the recaptured request: ${freshPolicyDecision.reason}`);
|
|
1934
|
+
}
|
|
1935
|
+
if (executorPolicyDecision?.ruleId &&
|
|
1936
|
+
freshPolicyDecision.ruleId !== executorPolicyDecision.ruleId) {
|
|
1937
|
+
throw new Error("executor-side auto-approval rule changed after recapture");
|
|
1938
|
+
}
|
|
1939
|
+
if (policyRuleId && freshPolicyDecision.ruleId !== policyRuleId) {
|
|
1940
|
+
throw new Error("executor-side auto-approval rule changed before dispatch");
|
|
1941
|
+
}
|
|
1942
|
+
if (policyFingerprint &&
|
|
1943
|
+
freshPolicyDecision.policyFingerprint !== policyFingerprint) {
|
|
1944
|
+
throw new Error("executor-side auto-approval policy changed before dispatch");
|
|
1945
|
+
}
|
|
1946
|
+
executorPolicyDecision = freshPolicyDecision;
|
|
1947
|
+
}
|
|
1948
|
+
if (approvalDispatchReserved) {
|
|
1949
|
+
throw new Error("Claude approval dispatch was already reserved");
|
|
1950
|
+
}
|
|
1951
|
+
approvalDispatchReserved = true;
|
|
1952
|
+
if (!releaseStateLock) {
|
|
1953
|
+
throw new Error("approval state lock was released before terminal dispatch");
|
|
1954
|
+
}
|
|
1955
|
+
const latestConversation = loadState(statePath);
|
|
1956
|
+
const latestTakeover = isRecord(latestConversation.native_session_takeover)
|
|
1957
|
+
? latestConversation.native_session_takeover
|
|
1958
|
+
: undefined;
|
|
1959
|
+
const latestControl = terminalControlFromTakeover(latestTakeover);
|
|
1960
|
+
const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
|
|
1961
|
+
? latestTakeover.terminal_bridge_approval
|
|
1962
|
+
: undefined;
|
|
1963
|
+
const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
|
|
1964
|
+
const latestApprovalState = isRecord(latestApproval?.approval_state)
|
|
1965
|
+
? latestApproval.approval_state
|
|
1966
|
+
: undefined;
|
|
1967
|
+
const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
|
|
1968
|
+
? latestApprovalState.policy_evidence
|
|
1969
|
+
: undefined;
|
|
1970
|
+
const recapturedPolicyEvidence = inspection.approval.approvable
|
|
1971
|
+
? inspection.approval.policyEvidence
|
|
1972
|
+
: undefined;
|
|
1973
|
+
const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
|
|
1974
|
+
? latestTakeover.terminal_bridge_approval_dispatch
|
|
1975
|
+
: undefined;
|
|
1976
|
+
if (!latestTakeover ||
|
|
1977
|
+
latestConversation.status !== "waiting_for_openclaw" ||
|
|
1978
|
+
latestTakeover.terminal_bridge_message_id !==
|
|
1979
|
+
nativeTakeover?.terminal_bridge_message_id ||
|
|
1980
|
+
latestApproval?.fingerprint !== fingerprint ||
|
|
1981
|
+
latestNotifiedAt === undefined ||
|
|
1982
|
+
cliNowMs() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
|
|
1983
|
+
expectedFingerprint !== fingerprint ||
|
|
1984
|
+
!terminalControlsShareIncarnation(latestControl, dispatchControl) ||
|
|
1985
|
+
(autoApproved &&
|
|
1986
|
+
(latestPolicyEvidence?.source !== "claude_transcript" ||
|
|
1987
|
+
latestPolicyEvidence.evidence_fingerprint !==
|
|
1988
|
+
recapturedPolicyEvidence?.evidenceFingerprint))) {
|
|
1989
|
+
throw new Error("approval state changed before terminal dispatch; refresh status and retry");
|
|
1990
|
+
}
|
|
1991
|
+
if (latestDispatch?.state === "reserved" &&
|
|
1992
|
+
latestDispatch.terminal_bridge_message_id ===
|
|
1993
|
+
latestTakeover.terminal_bridge_message_id) {
|
|
1994
|
+
throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
|
|
1995
|
+
}
|
|
1996
|
+
const reservedAt = cliNow().toISOString();
|
|
1997
|
+
const reservedConversation = {
|
|
1998
|
+
...latestConversation,
|
|
1999
|
+
native_session_takeover: {
|
|
2000
|
+
...latestTakeover,
|
|
2001
|
+
terminal_bridge_approval_dispatch: {
|
|
2002
|
+
state: "reserved",
|
|
2003
|
+
attempt_id: randomUUID(),
|
|
2004
|
+
fingerprint,
|
|
2005
|
+
keys,
|
|
2006
|
+
terminal_target: dispatchControl.target,
|
|
2007
|
+
terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
|
|
2008
|
+
reserved_at: reservedAt
|
|
2009
|
+
}
|
|
2010
|
+
},
|
|
2011
|
+
updated_at: reservedAt
|
|
2012
|
+
};
|
|
2013
|
+
saveState(statePath, reservedConversation);
|
|
2014
|
+
lockedConversation = reservedConversation;
|
|
2015
|
+
}
|
|
2016
|
+
: undefined
|
|
2017
|
+
});
|
|
2018
|
+
const actualFingerprint = approval.fingerprint;
|
|
2019
|
+
const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
|
|
2020
|
+
const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
|
|
2021
|
+
if (!approval.approved) {
|
|
2022
|
+
releaseApprovalStateLock();
|
|
2023
|
+
if (autoApproved) {
|
|
2024
|
+
appendEvent(logPath, {
|
|
2025
|
+
ts: cliNow().toISOString(),
|
|
2026
|
+
conversation_id: conversation.conversation_id,
|
|
2027
|
+
event: "terminal_auto_approval_decision",
|
|
2028
|
+
action: "rejected",
|
|
2029
|
+
reason: approval.reason,
|
|
2030
|
+
terminal_control: terminalControl,
|
|
2031
|
+
expected_fingerprint: expectedFingerprint,
|
|
2032
|
+
actual_fingerprint: actualFingerprint,
|
|
2033
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
2034
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
printJson({
|
|
2038
|
+
conversation,
|
|
2039
|
+
approved: false,
|
|
2040
|
+
blocked: approval.blocked,
|
|
2041
|
+
reason: approval.reason,
|
|
2042
|
+
terminal_control: terminalControl,
|
|
2043
|
+
expected_approval_fingerprint: expectedFingerprint,
|
|
2044
|
+
actual_approval_fingerprint: actualFingerprint,
|
|
2045
|
+
screen_excerpt: approval.screenExcerpt
|
|
2046
|
+
});
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
appendEvent(logPath, {
|
|
2050
|
+
ts: cliNow().toISOString(),
|
|
2051
|
+
conversation_id: conversation.conversation_id,
|
|
2052
|
+
event: "terminal_approval_send",
|
|
2053
|
+
terminal_control: terminalControl,
|
|
2054
|
+
key: approval.key,
|
|
2055
|
+
keys: approval.keys,
|
|
2056
|
+
label: approval.label,
|
|
2057
|
+
decision_mode: approval.decisionMode,
|
|
2058
|
+
request_id: approval.requestId,
|
|
2059
|
+
approval_fingerprint: actualFingerprint,
|
|
2060
|
+
auto_approved: autoApproved,
|
|
2061
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
2062
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
2063
|
+
});
|
|
2064
|
+
if (autoApproved) {
|
|
2065
|
+
appendEvent(logPath, {
|
|
2066
|
+
ts: cliNow().toISOString(),
|
|
2067
|
+
conversation_id: conversation.conversation_id,
|
|
2068
|
+
event: "terminal_auto_approval_decision",
|
|
2069
|
+
action: "approved",
|
|
2070
|
+
terminal_control: terminalControl,
|
|
2071
|
+
approval_fingerprint: actualFingerprint,
|
|
2072
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
2073
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
runtimeLog("info", "terminal_approval_send", {
|
|
2077
|
+
conversation_id: conversation.conversation_id,
|
|
2078
|
+
terminal_target: terminalControl.target,
|
|
2079
|
+
key: approval.key,
|
|
2080
|
+
keys: approval.keys,
|
|
2081
|
+
label: approval.label,
|
|
2082
|
+
decision_mode: approval.decisionMode,
|
|
2083
|
+
request_id: approval.requestId,
|
|
2084
|
+
approval_fingerprint: actualFingerprint,
|
|
2085
|
+
auto_approved: autoApproved,
|
|
2086
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
2087
|
+
policy_fingerprint: effectivePolicyFingerprint
|
|
2088
|
+
});
|
|
2089
|
+
const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
|
|
2090
|
+
? { ...lockedConversation.native_session_takeover }
|
|
2091
|
+
: {};
|
|
2092
|
+
const resolvedApproval = isRecord(nativeTakeoverForUpdate.terminal_bridge_approval)
|
|
2093
|
+
? nativeTakeoverForUpdate.terminal_bridge_approval
|
|
2094
|
+
: undefined;
|
|
2095
|
+
const resolvedApprovalScreenDigest = stringValue(resolvedApproval?.screen_digest);
|
|
2096
|
+
const resolvedApprovalState = isRecord(resolvedApproval?.approval_state)
|
|
2097
|
+
? resolvedApproval.approval_state
|
|
2098
|
+
: undefined;
|
|
2099
|
+
const resolvedTranscriptIdentity = claudeTranscriptApprovalIdentity(resolvedApprovalState);
|
|
2100
|
+
const approvalResolvedAt = cliNow().toISOString();
|
|
2101
|
+
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
|
|
2102
|
+
nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
|
|
2103
|
+
DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
2104
|
+
const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
|
|
2105
|
+
nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
|
|
2106
|
+
DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
2107
|
+
const nextNativeTakeover = {
|
|
2108
|
+
...nativeTakeoverForUpdate,
|
|
2109
|
+
terminal_bridge_approval: undefined,
|
|
2110
|
+
terminal_bridge_approval_dispatch: undefined,
|
|
2111
|
+
terminal_bridge_approval_resolved_at: approvalResolvedAt,
|
|
2112
|
+
terminal_bridge_last_approval_fingerprint: actualFingerprint,
|
|
2113
|
+
terminal_bridge_last_approval_screen_digest: resolvedApprovalScreenDigest,
|
|
2114
|
+
terminal_bridge_last_approval_request_id: resolvedTranscriptIdentity?.requestId,
|
|
2115
|
+
terminal_bridge_last_approval_evidence_fingerprint: resolvedTranscriptIdentity?.evidenceFingerprint,
|
|
2116
|
+
terminal_bridge_last_approval_prompt_cleared_at: undefined,
|
|
2117
|
+
terminal_bridge_last_approval_at: approvalResolvedAt,
|
|
2118
|
+
terminal_bridge_last_approval_message_id: nativeTakeoverForUpdate.terminal_bridge_message_id,
|
|
2119
|
+
terminal_bridge_monitor_lock_version: monitorOwner.LOCK_VERSION,
|
|
2120
|
+
terminal_bridge_monitor_started_at: approvalResolvedAt,
|
|
2121
|
+
terminal_bridge_last_activity_at: approvalResolvedAt,
|
|
2122
|
+
terminal_bridge_last_activity_reason: "approval resolved",
|
|
2123
|
+
terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
|
|
2124
|
+
terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
|
|
2125
|
+
terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
|
|
2126
|
+
terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
|
|
2127
|
+
};
|
|
2128
|
+
delete nextNativeTakeover.terminal_bridge_approval;
|
|
2129
|
+
delete nextNativeTakeover.terminal_bridge_approval_dispatch;
|
|
2130
|
+
delete nextNativeTakeover.terminal_bridge_last_approval_prompt_cleared_at;
|
|
2131
|
+
const nextConversation = {
|
|
2132
|
+
...lockedConversation,
|
|
2133
|
+
status: terminalBridgeEnabled(lockedConversation)
|
|
2134
|
+
? "waiting_for_agent"
|
|
2135
|
+
: lockedConversation.status,
|
|
2136
|
+
native_session_takeover: nextNativeTakeover,
|
|
2137
|
+
updated_at: approvalResolvedAt
|
|
2138
|
+
};
|
|
2139
|
+
saveState(statePath, nextConversation);
|
|
2140
|
+
releaseApprovalStateLock();
|
|
2141
|
+
const bridgeMonitor = ensureTerminalBridgeMonitorAfterApproval({
|
|
2142
|
+
conversation: nextConversation,
|
|
2143
|
+
statePath,
|
|
2144
|
+
logPath,
|
|
2145
|
+
terminalControl,
|
|
2146
|
+
options
|
|
2147
|
+
});
|
|
2148
|
+
printJson({
|
|
2149
|
+
conversation: nextConversation,
|
|
2150
|
+
approved: true,
|
|
2151
|
+
terminal_control: terminalControl,
|
|
2152
|
+
key: approval.key,
|
|
2153
|
+
keys: approval.keys,
|
|
2154
|
+
label: approval.label,
|
|
2155
|
+
decision_mode: approval.decisionMode,
|
|
2156
|
+
request_id: approval.requestId,
|
|
2157
|
+
approval_fingerprint: actualFingerprint,
|
|
2158
|
+
auto_approved: autoApproved,
|
|
2159
|
+
policy_rule_id: effectivePolicyRuleId,
|
|
2160
|
+
policy_fingerprint: effectivePolicyFingerprint,
|
|
2161
|
+
monitor_pid: bridgeMonitor.monitorPid ?? null,
|
|
2162
|
+
monitor_handoff_pid: bridgeMonitor.handoffWatchdog?.pid ?? null
|
|
2163
|
+
});
|
|
2164
|
+
};
|
|
2165
|
+
try {
|
|
2166
|
+
return await withStoreWriterLeaseAsync(writerStoreDir, async () => {
|
|
2167
|
+
releaseStateLock = acquireFileLock(`${statePath}.lock`);
|
|
2168
|
+
try {
|
|
2169
|
+
return await runApprovalWithStateLock();
|
|
2170
|
+
}
|
|
2171
|
+
finally {
|
|
2172
|
+
releaseApprovalStateLock();
|
|
2173
|
+
}
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
finally {
|
|
2177
|
+
try {
|
|
2178
|
+
releaseApprovalStateLock();
|
|
2179
|
+
}
|
|
2180
|
+
finally {
|
|
2181
|
+
releaseApprovalTerminalLock();
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
function assertAutoApprovalCallbackAuthority(input) {
|
|
2186
|
+
if (input.options.autoApproved !== true) {
|
|
2187
|
+
return "not_callback";
|
|
2188
|
+
}
|
|
2189
|
+
const expected = autoApprovalCallbackAuthorityFromOptions(input.options);
|
|
2190
|
+
if (!expected) {
|
|
2191
|
+
return "not_callback";
|
|
2192
|
+
}
|
|
2193
|
+
assertAutoApprovalCallbackRoute({
|
|
2194
|
+
options: input.options,
|
|
2195
|
+
conversation: input.conversation,
|
|
2196
|
+
statePath: input.statePath
|
|
2197
|
+
});
|
|
2198
|
+
const delivery = isRecord(input.conversation.callback_delivery)
|
|
2199
|
+
? input.conversation.callback_delivery
|
|
2200
|
+
: undefined;
|
|
2201
|
+
const callbackMessage = isRecord(delivery?.message)
|
|
2202
|
+
? delivery.message
|
|
2203
|
+
: undefined;
|
|
2204
|
+
const callbackMetadata = isRecord(callbackMessage?.metadata)
|
|
2205
|
+
? callbackMessage.metadata
|
|
2206
|
+
: undefined;
|
|
2207
|
+
const callbackCandidate = isRecord(callbackMetadata?.approval_candidate)
|
|
2208
|
+
? callbackMetadata.approval_candidate
|
|
2209
|
+
: undefined;
|
|
2210
|
+
const callbackTerminalStatus = isRecord(callbackMetadata?.terminal_status)
|
|
2211
|
+
? callbackMetadata.terminal_status
|
|
2212
|
+
: undefined;
|
|
2213
|
+
const callbackApprovalState = isRecord(callbackTerminalStatus?.approval_state)
|
|
2214
|
+
? callbackTerminalStatus.approval_state
|
|
2215
|
+
: undefined;
|
|
2216
|
+
const persistedApprovalState = isRecord(input.approval?.approval_state)
|
|
2217
|
+
? input.approval.approval_state
|
|
2218
|
+
: undefined;
|
|
2219
|
+
const callbackFingerprints = [
|
|
2220
|
+
stringValue(callbackMetadata?.approval_fingerprint),
|
|
2221
|
+
stringValue(callbackCandidate?.fingerprint),
|
|
2222
|
+
stringValue(callbackApprovalState?.fingerprint)
|
|
2223
|
+
];
|
|
2224
|
+
const commonAuthorityMismatch = delivery?.kind !== "approval_notification" ||
|
|
2225
|
+
stringValue(callbackMessage?.id) !== expected.messageId ||
|
|
2226
|
+
stringValue(callbackMessage?.conversation_id) !== expected.conversationId ||
|
|
2227
|
+
stringValue(callbackMessage?.session_id) !== expected.sessionId ||
|
|
2228
|
+
stringValue(callbackMessage?.turn_id) !== expected.turnId ||
|
|
2229
|
+
!input.expectedFingerprint ||
|
|
2230
|
+
!/^[a-f0-9]{64}$/u.test(input.expectedFingerprint) ||
|
|
2231
|
+
callbackFingerprints.some((value) => value !== input.expectedFingerprint) ||
|
|
2232
|
+
input.takeover?.terminal_bridge !== true;
|
|
2233
|
+
if (commonAuthorityMismatch) {
|
|
2234
|
+
throw new Error("automatic approval callback no longer matches the locked Turn state; refresh status and retry");
|
|
2235
|
+
}
|
|
2236
|
+
if (input.approval) {
|
|
2237
|
+
const currentFingerprints = [
|
|
2238
|
+
stringValue(input.approval.fingerprint),
|
|
2239
|
+
stringValue(persistedApprovalState?.fingerprint)
|
|
2240
|
+
];
|
|
2241
|
+
if (input.conversation.status !== "waiting_for_openclaw" ||
|
|
2242
|
+
currentFingerprints.some((value) => value !== input.expectedFingerprint) ||
|
|
2243
|
+
stringValue(input.approval.callback_message_id) !== expected.messageId) {
|
|
2244
|
+
throw new Error("automatic approval callback no longer matches the locked Turn state; refresh status and retry");
|
|
2245
|
+
}
|
|
2246
|
+
return "current";
|
|
2247
|
+
}
|
|
2248
|
+
if (input.takeover?.terminal_bridge_approval !== undefined ||
|
|
2249
|
+
input.takeover?.terminal_bridge_approval_dispatch !== undefined ||
|
|
2250
|
+
!isPostApprovalCallbackReplayStatus(input.conversation.status) ||
|
|
2251
|
+
stringValue(input.takeover?.terminal_bridge_message_id) !==
|
|
2252
|
+
expected.messageId ||
|
|
2253
|
+
stringValue(input.takeover?.terminal_bridge_last_approval_message_id) !==
|
|
2254
|
+
expected.messageId ||
|
|
2255
|
+
stringValue(input.takeover?.terminal_bridge_last_approval_fingerprint) !==
|
|
2256
|
+
input.expectedFingerprint ||
|
|
2257
|
+
validTimestampMs(input.takeover?.terminal_bridge_approval_resolved_at) === undefined) {
|
|
2258
|
+
throw new Error("automatic approval callback no longer matches the locked Turn receipt; refresh status and retry");
|
|
2259
|
+
}
|
|
2260
|
+
return "already_approved";
|
|
2261
|
+
}
|
|
2262
|
+
function isPostApprovalCallbackReplayStatus(status) {
|
|
2263
|
+
return [
|
|
2264
|
+
"waiting_for_agent",
|
|
2265
|
+
"running",
|
|
2266
|
+
"idle",
|
|
2267
|
+
"stalled",
|
|
2268
|
+
"callback_pending",
|
|
2269
|
+
"callback_failed",
|
|
2270
|
+
"failed",
|
|
2271
|
+
"closed",
|
|
2272
|
+
"cancelled",
|
|
2273
|
+
"cancelling"
|
|
2274
|
+
].includes(status);
|
|
2275
|
+
}
|
|
2276
|
+
function autoApprovalCallbackAuthorityFromOptions(options) {
|
|
2277
|
+
const candidate = {
|
|
2278
|
+
conversationId: stringValue(options.expectedCallbackConversationId),
|
|
2279
|
+
sessionId: stringValue(options.expectedCallbackSessionId),
|
|
2280
|
+
turnId: stringValue(options.expectedCallbackTurnId),
|
|
2281
|
+
messageId: stringValue(options.expectedCallbackMessageId),
|
|
2282
|
+
openclawSession: stringValue(options.expectedCallbackOpenclawSession)
|
|
2283
|
+
};
|
|
2284
|
+
const values = Object.values(candidate);
|
|
2285
|
+
if (values.every((value) => value === undefined)) {
|
|
2286
|
+
return undefined;
|
|
2287
|
+
}
|
|
2288
|
+
if (values.some((value) => value === undefined)) {
|
|
2289
|
+
throw new Error("automatic approval callback identity is incomplete; no approval key was sent");
|
|
2290
|
+
}
|
|
2291
|
+
return candidate;
|
|
2292
|
+
}
|
|
2293
|
+
function assertAutoApprovalCallbackRoute(input) {
|
|
2294
|
+
if (input.options.autoApproved !== true) {
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
const expected = autoApprovalCallbackAuthorityFromOptions(input.options);
|
|
2298
|
+
if (!expected) {
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
const storedOpenClawSession = stringValue(input.conversation.gateway_session ?? input.conversation.openclaw_session);
|
|
2302
|
+
if (input.conversation.conversation_id !== expected.conversationId ||
|
|
2303
|
+
sessionIdForConversation(input.conversation) !== expected.sessionId ||
|
|
2304
|
+
turnIdForConversation(input.conversation) !== expected.turnId ||
|
|
2305
|
+
!sameCanonicalStatePath(input.conversation.state_path, input.statePath) ||
|
|
2306
|
+
storedOpenClawSession !== expected.openclawSession) {
|
|
2307
|
+
throw new Error("automatic approval callback does not match the selected Turn state; no state was changed");
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
async function runTerminalConversationApprove({ options, terminal }) {
|
|
2311
|
+
const { conversationId, agent, terminalControl, pid } = terminal;
|
|
2312
|
+
const storeDir = storeDirFromOptions(options);
|
|
2313
|
+
const releaseTerminalLock = acquireTerminalBridgeSendLock(storeDir, terminalControl, { timeoutMs: 30000 });
|
|
2314
|
+
try {
|
|
2315
|
+
if (agent === "claude") {
|
|
2316
|
+
printJson({
|
|
2317
|
+
conversation_id: conversationId,
|
|
2318
|
+
source: "terminal_control",
|
|
2319
|
+
approved: false,
|
|
2320
|
+
blocked: true,
|
|
2321
|
+
reason: "Claude screen approval requires `send --background` so AKK can bind it to an active managed turn",
|
|
2322
|
+
terminal_control: terminalControl
|
|
2323
|
+
});
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
const suppliedTerminalToken = stringValue(options.expectedTerminalToken);
|
|
2327
|
+
const initialResolution = await terminalListCliFacade.resolveTerminalScopedCodexApproval({
|
|
2328
|
+
options,
|
|
2329
|
+
terminal
|
|
2330
|
+
});
|
|
2331
|
+
if (initialResolution.state === "blocked") {
|
|
2332
|
+
throw new Error(initialResolution.reason);
|
|
2333
|
+
}
|
|
2334
|
+
if (initialResolution.state === "unmanaged" && suppliedTerminalToken) {
|
|
2335
|
+
throw new Error("--expected-terminal-token does not match an advertised terminal-scoped Codex approval");
|
|
2336
|
+
}
|
|
2337
|
+
if (initialResolution.state === "eligible" &&
|
|
2338
|
+
suppliedTerminalToken !== initialResolution.boundary.token) {
|
|
2339
|
+
throw new Error("terminal-scoped Codex approval token is missing or stale; refresh AKK list");
|
|
2340
|
+
}
|
|
2341
|
+
const runtime = {
|
|
2342
|
+
pid,
|
|
2343
|
+
cwd: terminalControl.currentPath,
|
|
2344
|
+
conversationId,
|
|
2345
|
+
terminalTarget: terminalControl.target
|
|
2346
|
+
};
|
|
2347
|
+
const approveCurrentPrompt = async (terminalScoped) => createTerminalAgentBridge(options).approve(agent, terminalControl, {
|
|
2348
|
+
expectedFingerprint: stringValue(options.expectedApprovalFingerprint),
|
|
2349
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2350
|
+
runtime,
|
|
2351
|
+
beforeKeyDispatch: terminalScoped
|
|
2352
|
+
? async (context) => {
|
|
2353
|
+
const approvalSnapshot = terminalScopedCodexApprovalPromptSnapshot({
|
|
2354
|
+
approvable: true,
|
|
2355
|
+
fingerprint: context.fingerprint,
|
|
2356
|
+
keys: context.keys,
|
|
2357
|
+
decision_mode: context.inspection.approval.approvable
|
|
2358
|
+
? context.inspection.approval.action.mode ?? "keys"
|
|
2359
|
+
: undefined,
|
|
2360
|
+
request_id: context.inspection.approval.approvable
|
|
2361
|
+
? context.inspection.approval.action.requestId
|
|
2362
|
+
: undefined
|
|
2363
|
+
});
|
|
2364
|
+
const current = await terminalListCliFacade.resolveTerminalScopedCodexApproval({
|
|
2365
|
+
options,
|
|
2366
|
+
terminal,
|
|
2367
|
+
approvalSnapshot
|
|
2368
|
+
});
|
|
2369
|
+
if (current.state !== "eligible" ||
|
|
2370
|
+
current.boundary.token !== suppliedTerminalToken) {
|
|
2371
|
+
throw new Error(current.state === "blocked"
|
|
2372
|
+
? current.reason
|
|
2373
|
+
: "terminal-scoped Codex approval authority changed before key dispatch");
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
: undefined
|
|
2377
|
+
});
|
|
2378
|
+
const terminalScoped = initialResolution.state === "eligible";
|
|
2379
|
+
const approval = terminalScoped
|
|
2380
|
+
? await withStoreWriterLeaseAsync(storeDir, async () => {
|
|
2381
|
+
const current = await terminalListCliFacade.resolveTerminalScopedCodexApproval({
|
|
2382
|
+
options,
|
|
2383
|
+
terminal
|
|
2384
|
+
});
|
|
2385
|
+
if (current.state !== "eligible" ||
|
|
2386
|
+
current.boundary.token !== suppliedTerminalToken) {
|
|
2387
|
+
throw new Error(current.state === "blocked"
|
|
2388
|
+
? current.reason
|
|
2389
|
+
: "terminal-scoped Codex approval authority changed while waiting for Store control");
|
|
2390
|
+
}
|
|
2391
|
+
return approveCurrentPrompt(true);
|
|
2392
|
+
})
|
|
2393
|
+
: await approveCurrentPrompt(false);
|
|
2394
|
+
if (!approval.approved) {
|
|
2395
|
+
printJson({
|
|
2396
|
+
conversation_id: conversationId,
|
|
2397
|
+
source: "terminal_control",
|
|
2398
|
+
approved: false,
|
|
2399
|
+
blocked: approval.blocked,
|
|
2400
|
+
reason: approval.reason,
|
|
2401
|
+
terminal_control: terminalControl,
|
|
2402
|
+
screen_excerpt: approval.screenExcerpt
|
|
2403
|
+
});
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
runtimeLog("info", "terminal_approval_send", {
|
|
2407
|
+
conversation_id: conversationId,
|
|
2408
|
+
agent,
|
|
2409
|
+
terminal_target: terminalControl.target,
|
|
2410
|
+
key: approval.key,
|
|
2411
|
+
keys: approval.keys,
|
|
2412
|
+
label: approval.label,
|
|
2413
|
+
decision_mode: approval.decisionMode,
|
|
2414
|
+
request_id: approval.requestId
|
|
2415
|
+
});
|
|
2416
|
+
printJson({
|
|
2417
|
+
conversation_id: conversationId,
|
|
2418
|
+
source: "terminal_control",
|
|
2419
|
+
approved: true,
|
|
2420
|
+
terminal_control: terminalControl,
|
|
2421
|
+
key: approval.key,
|
|
2422
|
+
keys: approval.keys,
|
|
2423
|
+
label: approval.label,
|
|
2424
|
+
approval_fingerprint: approval.fingerprint,
|
|
2425
|
+
decision_mode: approval.decisionMode,
|
|
2426
|
+
request_id: approval.requestId,
|
|
2427
|
+
terminal_scoped: terminalScoped,
|
|
2428
|
+
...(terminalScoped
|
|
2429
|
+
? {
|
|
2430
|
+
durable_dispatch_receipt: false,
|
|
2431
|
+
uncertain_outcome_recovery: "refresh status and inspect the live prompt; do not retry blindly"
|
|
2432
|
+
}
|
|
2433
|
+
: {})
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
finally {
|
|
2437
|
+
releaseTerminalLock();
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
async function prepareTerminalControlSend(request) {
|
|
2441
|
+
const { transaction, options, conversation, nextConversation, executor, message, recordRawAttachmentAfterSend = false, allowedPreMaterializationIdentity, allowedAdditionalIdentities = [], verifiedEmptyCodexHandoff, deferredCodexForegroundBinding, continuingTurnResponse = false } = request;
|
|
2442
|
+
const bridge = terminalBridgeEnabled(conversation);
|
|
2443
|
+
const route = bindTerminalDispatchRoute(transaction.scopes, transaction.resources);
|
|
2444
|
+
const { terminalControl, storeDir: lockedStoreDir, statePath, logPath } = route;
|
|
2445
|
+
const terminalBridge = createTerminalAgentBridge(options);
|
|
2446
|
+
const execution = terminalDispatchExecution(options, terminalBridge);
|
|
2447
|
+
const bridgeStartedAt = cliNow().toISOString();
|
|
2448
|
+
const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ?? DEFAULT_AGENT_TIMEOUT_MINUTES);
|
|
2449
|
+
const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ?? DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
|
|
2450
|
+
const terminalPayload = terminalSubmissionPayload(String(message.body ?? ""));
|
|
2451
|
+
const terminalRequestHash = required(terminalBridgeRequestFingerprint(terminalPayload), "terminal request hash is unavailable");
|
|
2452
|
+
const presentationContext = { message, executor, terminalControl };
|
|
2453
|
+
const presentationPorts = {
|
|
2454
|
+
write: printJson,
|
|
2455
|
+
budget: budgetAction,
|
|
2456
|
+
nextAction: openClawYieldNextAction,
|
|
2457
|
+
summarize: textSummary
|
|
2458
|
+
};
|
|
2459
|
+
let previousDispatchLedger = resolveTerminalDispatchLedgerPaneIncarnation(terminalControl, loadTerminalBridgeDispatchLedger(terminalControl));
|
|
2460
|
+
previousDispatchLedger = reconcilePreparedTerminalDispatchLedger(terminalControl, previousDispatchLedger);
|
|
2461
|
+
const previousDispatchLifecycle = terminalDispatchLedgerLooksLifecycle(previousDispatchLedger);
|
|
2462
|
+
const previousDispatchOwner = execution.preflightRequiresOwner(previousDispatchLedger, previousDispatchLifecycle)
|
|
2463
|
+
? loadTerminalDispatchLedgerOwner(previousDispatchLedger)
|
|
2464
|
+
: undefined;
|
|
2465
|
+
const dispatchPreflight = execution.evaluatePreflight({
|
|
2466
|
+
ledger: previousDispatchLedger,
|
|
2467
|
+
owner: previousDispatchOwner,
|
|
2468
|
+
conversation,
|
|
2469
|
+
requestHash: terminalRequestHash,
|
|
2470
|
+
requestText: terminalPayload,
|
|
2471
|
+
messageId: message.id,
|
|
2472
|
+
terminalTarget: terminalControl.target,
|
|
2473
|
+
ledgerLifecycle: previousDispatchLifecycle,
|
|
2474
|
+
statePathMatches: Boolean(previousDispatchOwner &&
|
|
2475
|
+
sameCanonicalStatePath(previousDispatchLedger.state_path, statePath)),
|
|
2476
|
+
continuingTurnResponse
|
|
2477
|
+
});
|
|
2478
|
+
if (dispatchPreflight.action === "replay") {
|
|
2479
|
+
presentTerminalDispatchReplay(dispatchPreflight, presentationContext, presentationPorts);
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
if (bridge) {
|
|
2483
|
+
assertNoUnresolvedTerminalBridgeSubmission(lockedStoreDir, terminalControl, conversation.conversation_id, terminalPayload);
|
|
2484
|
+
}
|
|
2485
|
+
const sendTakeover = isRecord(conversation.native_session_takeover)
|
|
2486
|
+
? conversation.native_session_takeover
|
|
2487
|
+
: undefined;
|
|
2488
|
+
const terminalAgentPid = Number(sendTakeover?.terminal_agent_pid);
|
|
2489
|
+
const expectedManagedNativeThreadId = stringValue(sendTakeover?.terminal_agent_expected_session_id) ?? stringValue(sendTakeover?.terminal_agent_session_id);
|
|
2490
|
+
const currentNativeIdentity = deferredCodexForegroundBinding
|
|
2491
|
+
?.candidateAcceptanceAnchor
|
|
2492
|
+
? undefined
|
|
2493
|
+
: await execution.resolveCurrentNativeIdentity({
|
|
2494
|
+
agent: executor.kind,
|
|
2495
|
+
pid: terminalAgentPid,
|
|
2496
|
+
cwd: terminalControl.currentPath,
|
|
2497
|
+
preferredSessionId: allowedPreMaterializationIdentity
|
|
2498
|
+
? expectedManagedNativeThreadId
|
|
2499
|
+
: undefined,
|
|
2500
|
+
allowedCompanionIdentity: allowedPreMaterializationIdentity,
|
|
2501
|
+
allowedAdditionalIdentities
|
|
2502
|
+
});
|
|
2503
|
+
const virginRawAttach = Boolean(recordRawAttachmentAfterSend &&
|
|
2504
|
+
!stringValue(sendTakeover?.terminal_agent_session_id));
|
|
2505
|
+
const pendingManagedNativeBinding = Boolean(!stringValue(sendTakeover?.terminal_agent_session_id) &&
|
|
2506
|
+
stringValue(sendTakeover?.terminal_agent_expected_session_id));
|
|
2507
|
+
const needsPostSendNativeBinding = virginRawAttach || pendingManagedNativeBinding;
|
|
2508
|
+
if (needsPostSendNativeBinding) {
|
|
2509
|
+
if (currentNativeIdentity &&
|
|
2510
|
+
!nativeIdentityMatchesCodexPreMaterialization(currentNativeIdentity, allowedPreMaterializationIdentity)) {
|
|
2511
|
+
throw new Error("native agent session appeared while preparing an unmaterialized terminal binding; refresh list and retry");
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
else {
|
|
2515
|
+
execution.assertTurnIdentity({
|
|
2516
|
+
conversation,
|
|
2517
|
+
currentIdentity: currentNativeIdentity,
|
|
2518
|
+
operation: "send to"
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
const preSendRuntime = {
|
|
2522
|
+
...(deferredCodexForegroundBinding?.candidateAcceptanceAnchor
|
|
2523
|
+
? terminalRuntimeForLiveIdentity({
|
|
2524
|
+
terminal: deferredCodexForegroundBinding.terminal,
|
|
2525
|
+
physicalOnly: true
|
|
2526
|
+
})
|
|
2527
|
+
: terminalRuntimeIdentityForConversation(nextConversation, terminalControl)),
|
|
2528
|
+
allowedPreMaterializationNativeIdentity: allowedPreMaterializationIdentity,
|
|
2529
|
+
allowedAdditionalNativeIdentities: allowedAdditionalIdentities,
|
|
2530
|
+
messageId: message.id
|
|
2531
|
+
};
|
|
2532
|
+
let preSendScreenFingerprint;
|
|
2533
|
+
let codexRolloutAcceptanceAnchor;
|
|
2534
|
+
let claudeTranscriptAnchor;
|
|
2535
|
+
const claudeHome = executor.kind === "claude"
|
|
2536
|
+
? path.resolve(expandHome(options.claudeHome) ?? defaultClaudeHome())
|
|
2537
|
+
: undefined;
|
|
2538
|
+
try {
|
|
2539
|
+
const status = await terminalBridge.status(executor.kind, terminalControl, {
|
|
2540
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2541
|
+
runtime: preSendRuntime
|
|
2542
|
+
});
|
|
2543
|
+
if (verifiedEmptyCodexHandoff || deferredCodexForegroundBinding) {
|
|
2544
|
+
if (executor.kind !== "codex" ||
|
|
2545
|
+
status.reachable !== true ||
|
|
2546
|
+
status.approval_state.blocked === true ||
|
|
2547
|
+
!["idle", "unknown"].includes(status.activity_state)) {
|
|
2548
|
+
throw new Error(`Codex deferred foreground send is not at a safe prompt ` +
|
|
2549
|
+
`(${status.activity_state}: ${status.activity_reason})`);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
else {
|
|
2553
|
+
assertSafeTerminalSend(executor.kind, status);
|
|
2554
|
+
}
|
|
2555
|
+
if (executor.kind === "codex" && needsPostSendNativeBinding) {
|
|
2556
|
+
if (pendingManagedNativeBinding || allowedPreMaterializationIdentity) {
|
|
2557
|
+
const expectedForegroundId = stringValue(sendTakeover?.terminal_agent_expected_session_id);
|
|
2558
|
+
const foreground = createRuntimeTerminalAgentRegistry(options)
|
|
2559
|
+
.require("codex")
|
|
2560
|
+
.observeThreadLifecycle?.({
|
|
2561
|
+
operation: { kind: "new_thread" },
|
|
2562
|
+
phase: "before",
|
|
2563
|
+
screen: status.screen.excerpt ?? ""
|
|
2564
|
+
});
|
|
2565
|
+
if (!expectedForegroundId ||
|
|
2566
|
+
foreground?.status !== "observed" ||
|
|
2567
|
+
foreground.nativeThreadId !== expectedForegroundId) {
|
|
2568
|
+
throw new Error("Codex foreground thread changed after the managed /status proof; " +
|
|
2569
|
+
"refresh list before sending");
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
await assertCodexComposerReadyForAutomatedInput({
|
|
2573
|
+
options,
|
|
2574
|
+
terminalControl
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
if (bridge) {
|
|
2578
|
+
preSendScreenFingerprint = stringValue(status.screen.digest) ??
|
|
2579
|
+
terminalBridgeScreenFingerprint(status.screen.excerpt);
|
|
2580
|
+
if (executor.kind === "codex") {
|
|
2581
|
+
codexRolloutAcceptanceAnchor = execution.captureCodexAcceptanceAnchor({
|
|
2582
|
+
currentIdentity: currentNativeIdentity,
|
|
2583
|
+
expectedNativeThreadId: expectedManagedNativeThreadId,
|
|
2584
|
+
boundProcessUuid: stringValue(sendTakeover?.terminal_agent_process_uuid),
|
|
2585
|
+
boundProcessBirth: stringValue(sendTakeover?.terminal_agent_process_birth),
|
|
2586
|
+
allowedPreMaterializationIdentity,
|
|
2587
|
+
needsPostSendNativeBinding,
|
|
2588
|
+
candidateSetAnchor: deferredCodexForegroundBinding?.candidateAcceptanceAnchor
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
else {
|
|
2592
|
+
claudeTranscriptAnchor = captureClaudeTranscriptAnchor({
|
|
2593
|
+
sessionId: preSendRuntime.sessionId,
|
|
2594
|
+
cwd: preSendRuntime.cwd,
|
|
2595
|
+
pid: preSendRuntime.pid,
|
|
2596
|
+
claudeHome,
|
|
2597
|
+
agentRows: loadClaudeAgentRows(options)
|
|
2598
|
+
});
|
|
2599
|
+
if (!claudeTranscriptAnchor) {
|
|
2600
|
+
throw new Error("the completion monitor could not bind an owner-private Claude transcript boundary");
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
catch (error) {
|
|
2606
|
+
throw new Error(`refusing to send to ${executor.display_name} without a verified idle terminal: ${error instanceof Error ? error.message : String(error)}`);
|
|
2607
|
+
}
|
|
2608
|
+
return {
|
|
2609
|
+
route, bridge, terminalControl, lockedStoreDir, statePath, logPath,
|
|
2610
|
+
terminalBridge, execution, bridgeStartedAt,
|
|
2611
|
+
agentTimeoutMinutes, agentHardTimeoutMinutes,
|
|
2612
|
+
terminalPayload, terminalRequestHash,
|
|
2613
|
+
presentationContext, presentationPorts, previousDispatchLedger,
|
|
2614
|
+
sendTakeover, terminalAgentPid, needsPostSendNativeBinding, preSendRuntime,
|
|
2615
|
+
preSendScreenFingerprint, codexRolloutAcceptanceAnchor,
|
|
2616
|
+
claudeTranscriptAnchor, claudeHome
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
async function resolveTerminalDispatchSubmissionOwner(prepared, request, application, stagedConversation) {
|
|
2620
|
+
const { transaction, options, executor, allowedPreMaterializationIdentity, allowedAdditionalIdentities = [], deferredCodexForegroundBinding: deferredBinding } = request;
|
|
2621
|
+
const { execution, needsPostSendNativeBinding, codexRolloutAcceptanceAnchor: acceptanceAnchor, terminalControl, terminalAgentPid, sendTakeover, terminalRequestHash, route } = prepared;
|
|
2622
|
+
const deferredForegroundScope = deferredBinding
|
|
2623
|
+
? bindDeferredForegroundApplicationScope(transaction.scopes, transaction.resources)
|
|
2624
|
+
: undefined;
|
|
2625
|
+
const ready = (conversation, pending = false) => ({
|
|
2626
|
+
conversation, deferredCandidateBindingPending: pending
|
|
2627
|
+
});
|
|
2628
|
+
if (!needsPostSendNativeBinding) {
|
|
2629
|
+
return ready(stagedConversation);
|
|
2630
|
+
}
|
|
2631
|
+
let boundIdentity;
|
|
2632
|
+
let boundConversation;
|
|
2633
|
+
let bindingError;
|
|
2634
|
+
try {
|
|
2635
|
+
if (deferredBinding &&
|
|
2636
|
+
cliEnv().AKK_TEST_ALLOW_SYNTHETIC_TERMINAL_ACCEPTANCE !== "1" &&
|
|
2637
|
+
![2, 3].includes(Number(acceptanceAnchor?.version))) {
|
|
2638
|
+
throw new Error("deferred Codex foreground binding requires a virgin rollout acceptance anchor");
|
|
2639
|
+
}
|
|
2640
|
+
boundIdentity = await execution.pollNativeIdentity({
|
|
2641
|
+
executor: executor.kind,
|
|
2642
|
+
terminalControl,
|
|
2643
|
+
pid: terminalAgentPid,
|
|
2644
|
+
expectedSessionId: stringValue(sendTakeover?.terminal_agent_expected_session_id),
|
|
2645
|
+
allowedPreMaterializationIdentity,
|
|
2646
|
+
allowedAdditionalIdentities,
|
|
2647
|
+
...(deferredBinding &&
|
|
2648
|
+
acceptanceAnchor &&
|
|
2649
|
+
[2, 3].includes(acceptanceAnchor.version)
|
|
2650
|
+
? {
|
|
2651
|
+
requiredCodexAcceptance: {
|
|
2652
|
+
anchor: acceptanceAnchor,
|
|
2653
|
+
requestHash: terminalRequestHash
|
|
2654
|
+
},
|
|
2655
|
+
attempts: Math.max(1, Math.ceil(DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS /
|
|
2656
|
+
DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS)),
|
|
2657
|
+
delayMs: DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS
|
|
2658
|
+
}
|
|
2659
|
+
: {})
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
catch (error) {
|
|
2663
|
+
bindingError = error instanceof Error ? error.message : String(error);
|
|
2664
|
+
}
|
|
2665
|
+
if (boundIdentity) {
|
|
2666
|
+
try {
|
|
2667
|
+
if (acceptanceAnchor &&
|
|
2668
|
+
(boundIdentity.processUuid !==
|
|
2669
|
+
acceptanceAnchor.process_uuid ||
|
|
2670
|
+
boundIdentity.processBirth !==
|
|
2671
|
+
acceptanceAnchor.process_birth)) {
|
|
2672
|
+
throw new Error("Codex process incarnation changed while its native thread materialized");
|
|
2673
|
+
}
|
|
2674
|
+
const expectedNativeThreadId = stringValue(sendTakeover?.terminal_agent_expected_session_id);
|
|
2675
|
+
if (expectedNativeThreadId &&
|
|
2676
|
+
boundIdentity.sessionId !== expectedNativeThreadId) {
|
|
2677
|
+
throw new Error(`native agent created thread ${boundIdentity.sessionId}, expected ` +
|
|
2678
|
+
`${expectedNativeThreadId}`);
|
|
2679
|
+
}
|
|
2680
|
+
const resolvedBoundConversation = execution.withNativeIdentity(stagedConversation, boundIdentity);
|
|
2681
|
+
boundConversation = resolvedBoundConversation;
|
|
2682
|
+
const bindingStoreDir = managedSessionStoreDirForConversation(resolvedBoundConversation);
|
|
2683
|
+
const bindingStatePath = stringValue(resolvedBoundConversation.state_path);
|
|
2684
|
+
if (!bindingStoreDir || !bindingStatePath) {
|
|
2685
|
+
throw new Error("managed Session Store is unavailable before native identity commit");
|
|
2686
|
+
}
|
|
2687
|
+
const identityRoute = withExactTerminalDispatchRoute(route, {
|
|
2688
|
+
terminalControl,
|
|
2689
|
+
terminalKey: terminalBridgeRuntimeKey(terminalControl),
|
|
2690
|
+
storeDir: bindingStoreDir,
|
|
2691
|
+
statePath: bindingStatePath,
|
|
2692
|
+
logPath: path.join(path.dirname(bindingStatePath), "events.ndjson")
|
|
2693
|
+
}, (exactRoute) => exactRoute);
|
|
2694
|
+
if (deferredBinding) {
|
|
2695
|
+
await deferredForegroundApplication(options, deferredBinding.terminal).commit({
|
|
2696
|
+
scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
|
|
2697
|
+
boundary: deferredForegroundBoundaryProjection(deferredBinding),
|
|
2698
|
+
identity: boundIdentity,
|
|
2699
|
+
acceptedAt: cliNow().toISOString()
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
else {
|
|
2703
|
+
await assertNativeThreadHasExclusiveOwnership({
|
|
2704
|
+
options,
|
|
2705
|
+
agent: executor.kind,
|
|
2706
|
+
currentPid: terminalAgentPid,
|
|
2707
|
+
nativeThreadId: boundIdentity.sessionId,
|
|
2708
|
+
storeDir: identityRoute.storeDir,
|
|
2709
|
+
terminalControl,
|
|
2710
|
+
excludedManagedSessionId: sessionIdForConversation(boundConversation)
|
|
2711
|
+
});
|
|
2712
|
+
persistManagedSessionNativeIdentity({
|
|
2713
|
+
conversation: boundConversation,
|
|
2714
|
+
terminalControl,
|
|
2715
|
+
identity: boundIdentity,
|
|
2716
|
+
storeDir: identityRoute.storeDir
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
if (acceptanceAnchor?.version === 2 &&
|
|
2720
|
+
cliEnv().AKK_TEST_EXIT_AFTER_VIRGIN_SESSION_BINDING === "1") {
|
|
2721
|
+
cliExit(86);
|
|
2722
|
+
}
|
|
2723
|
+
// Persist the observed identity before the full Turn-vs-Session check:
|
|
2724
|
+
// the provisional Turn and Session initially share only a binding id.
|
|
2725
|
+
execution.assertTurnIdentity({
|
|
2726
|
+
conversation: boundConversation,
|
|
2727
|
+
currentIdentity: boundIdentity,
|
|
2728
|
+
operation: "bind"
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
catch (error) {
|
|
2732
|
+
bindingError = error instanceof Error ? error.message : String(error);
|
|
2733
|
+
boundIdentity = undefined;
|
|
2734
|
+
boundConversation = undefined;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
if (!boundIdentity && deferredBinding) {
|
|
2738
|
+
try {
|
|
2739
|
+
const deferredScope = required(deferredForegroundScope, "deferred foreground mutation scope is unavailable");
|
|
2740
|
+
const transfer = deferredScope.loadTransfer(deferredBinding.transferId);
|
|
2741
|
+
if (["committed", "resolved"].includes(transfer.status)) {
|
|
2742
|
+
const recoveredTarget = await deferredForegroundApplication(options, deferredBinding.terminal).resolve({
|
|
2743
|
+
scope: deferredScope,
|
|
2744
|
+
boundary: deferredForegroundBoundaryProjection(deferredBinding)
|
|
2745
|
+
});
|
|
2746
|
+
const binding = recoveredTarget.binding;
|
|
2747
|
+
if (!binding?.native_thread_id ||
|
|
2748
|
+
!binding.native_process.process_uuid ||
|
|
2749
|
+
!binding.native_process.process_birth ||
|
|
2750
|
+
!isCompleteNativeRollout(binding.native_process.rollout)) {
|
|
2751
|
+
throw new Error("resolved deferred target lacks exact native identity");
|
|
2752
|
+
}
|
|
2753
|
+
const recoveredIdentity = {
|
|
2754
|
+
sessionId: binding.native_thread_id,
|
|
2755
|
+
processUuid: binding.native_process.process_uuid,
|
|
2756
|
+
processBirth: binding.native_process.process_birth,
|
|
2757
|
+
rollout: binding.native_process.rollout,
|
|
2758
|
+
evidence: binding.native_process.evidence
|
|
2759
|
+
};
|
|
2760
|
+
const recoveredConversation = execution.withNativeIdentity(stagedConversation, recoveredIdentity);
|
|
2761
|
+
execution.assertTurnIdentity({
|
|
2762
|
+
conversation: recoveredConversation,
|
|
2763
|
+
currentIdentity: recoveredIdentity,
|
|
2764
|
+
operation: "recover deferred foreground binding for"
|
|
2765
|
+
});
|
|
2766
|
+
boundIdentity = recoveredIdentity;
|
|
2767
|
+
boundConversation = recoveredConversation;
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
catch (error) {
|
|
2771
|
+
bindingError = error instanceof Error ? error.message : String(error);
|
|
2772
|
+
boundIdentity = undefined;
|
|
2773
|
+
boundConversation = undefined;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
if (boundIdentity && boundConversation) {
|
|
2777
|
+
return ready(boundConversation);
|
|
2778
|
+
}
|
|
2779
|
+
if (deferredBinding &&
|
|
2780
|
+
acceptanceAnchor?.version === 3 &&
|
|
2781
|
+
bindingError === undefined) {
|
|
2782
|
+
return ready(stagedConversation, true);
|
|
2783
|
+
}
|
|
2784
|
+
const bindingReason = bindingError
|
|
2785
|
+
? `AKK dispatched terminal input but could not verify the new native agent session: ${bindingError}`
|
|
2786
|
+
: "AKK dispatched terminal input but no exact native agent session appeared within the binding window";
|
|
2787
|
+
if (deferredBinding) {
|
|
2788
|
+
try {
|
|
2789
|
+
deferredForegroundApplication(options, deferredBinding.terminal).markUncertain({
|
|
2790
|
+
scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
|
|
2791
|
+
boundary: deferredForegroundBoundaryProjection(deferredBinding),
|
|
2792
|
+
reason: bindingReason
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
catch (error) {
|
|
2796
|
+
runtimeLog("error", "deferred_codex_foreground_uncertain_persist_failed", {
|
|
2797
|
+
transfer_id: deferredBinding.transferId,
|
|
2798
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
return {
|
|
2803
|
+
conversation: application.applyIdentityFailure(cliNow().toISOString(), bindingReason, deferredBinding
|
|
2804
|
+
? undefined
|
|
2805
|
+
: (current) => quarantineManagedSessionBinding({
|
|
2806
|
+
conversation: current,
|
|
2807
|
+
reason: bindingReason,
|
|
2808
|
+
storeDir: route.storeDir
|
|
2809
|
+
})),
|
|
2810
|
+
failureReason: bindingReason
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
function deferredTerminalInputNotStartedAt(transfer, terminalInputNotStartedAt) {
|
|
2814
|
+
if (terminalInputNotStartedAt === undefined ||
|
|
2815
|
+
transfer.input_stage === "none") {
|
|
2816
|
+
return undefined;
|
|
2817
|
+
}
|
|
2818
|
+
if (transfer.status === "dispatch_started" &&
|
|
2819
|
+
transfer.input_stage === "dispatch_started") {
|
|
2820
|
+
return terminalInputNotStartedAt;
|
|
2821
|
+
}
|
|
2822
|
+
throw new Error(`deferred foreground transfer ${transfer.transfer_id} has ` +
|
|
2823
|
+
`${transfer.input_stage} input evidence and cannot accept a ` +
|
|
2824
|
+
"terminal-input-not-started result");
|
|
2825
|
+
}
|
|
2826
|
+
function terminalDispatchRollbackRepositories({ request, prepared, deferredForegroundScope, preTransportRollback }) {
|
|
2827
|
+
const { options, conversation, deferredCodexForegroundBinding } = request;
|
|
2828
|
+
let rollbackPreTransportAttach = preTransportRollback;
|
|
2829
|
+
const rollbackBeforeInput = (route) => {
|
|
2830
|
+
if (!rollbackPreTransportAttach) {
|
|
2831
|
+
return true;
|
|
2832
|
+
}
|
|
2833
|
+
const rollback = rollbackPreTransportAttach;
|
|
2834
|
+
rollbackPreTransportAttach = undefined;
|
|
2835
|
+
try {
|
|
2836
|
+
rollback(route);
|
|
2837
|
+
return true;
|
|
2838
|
+
}
|
|
2839
|
+
catch (error) {
|
|
2840
|
+
runtimeLog("error", "raw_attach_pre_transport_rollback_failed", {
|
|
2841
|
+
conversation_id: conversation.conversation_id,
|
|
2842
|
+
terminal_target: route.terminalControl.target,
|
|
2843
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2844
|
+
});
|
|
2845
|
+
return false;
|
|
2846
|
+
}
|
|
2847
|
+
};
|
|
2848
|
+
const restoreDeferred = (route, terminalInputNotStartedAt) => {
|
|
2849
|
+
if (!deferredCodexForegroundBinding) {
|
|
2850
|
+
return false;
|
|
2851
|
+
}
|
|
2852
|
+
const deferredScope = required(deferredForegroundScope, "deferred foreground mutation scope is unavailable");
|
|
2853
|
+
const transfer = deferredScope.loadTransfer(deferredCodexForegroundBinding.transferId);
|
|
2854
|
+
if (terminalBridgeRuntimeKey(route.terminalControl) !==
|
|
2855
|
+
terminalBridgeRuntimeKey(deferredCodexForegroundBinding.terminal.terminalControl) ||
|
|
2856
|
+
(transfer.state_path !== undefined &&
|
|
2857
|
+
!sameCanonicalStatePath(transfer.state_path, route.statePath))) {
|
|
2858
|
+
throw new Error("deferred Codex pre-input abort escaped its exact mutation resources");
|
|
2859
|
+
}
|
|
2860
|
+
const durableTerminalInputNotStartedAt = deferredTerminalInputNotStartedAt(transfer, terminalInputNotStartedAt);
|
|
2861
|
+
deferredRecoveryAdapter.abortPreparedDeferredForegroundTurn(deferredForegroundRecoveryAdapterPorts(), {
|
|
2862
|
+
options,
|
|
2863
|
+
scope: deferredScope,
|
|
2864
|
+
storeDir: route.storeDir,
|
|
2865
|
+
terminal: {
|
|
2866
|
+
...deferredCodexForegroundBinding.terminal,
|
|
2867
|
+
terminalControl: route.terminalControl
|
|
2868
|
+
},
|
|
2869
|
+
transfer,
|
|
2870
|
+
boundary: deferredCodexForegroundBinding,
|
|
2871
|
+
terminalInputNotStartedAt: durableTerminalInputNotStartedAt
|
|
2872
|
+
});
|
|
2873
|
+
// The dedicated abort already durably restores the Turn, ledger,
|
|
2874
|
+
// transfer, and Sessions. Never invoke the attach rollback twice.
|
|
2875
|
+
rollbackPreTransportAttach = undefined;
|
|
2876
|
+
return true;
|
|
2877
|
+
};
|
|
2878
|
+
return { rollbackBeforeInput, restoreDeferred };
|
|
2879
|
+
}
|
|
2880
|
+
function createTerminalDispatchRuntime(request, prepared, rollback) {
|
|
2881
|
+
const { transaction, conversation, nextConversation, executor, message, recordRawAttachmentAfterSend = false } = request;
|
|
2882
|
+
const { bridge, terminalControl, lockedStoreDir, statePath, logPath, bridgeStartedAt, agentTimeoutMinutes, agentHardTimeoutMinutes, terminalPayload, terminalRequestHash, previousDispatchLedger, preSendScreenFingerprint, codexRolloutAcceptanceAnchor, claudeTranscriptAnchor, claudeHome } = prepared;
|
|
2883
|
+
const terminalMessage = message;
|
|
2884
|
+
const bridgeConversation = bridge
|
|
2885
|
+
? dispatchReceipt.withTerminalBridgeState({
|
|
2886
|
+
conversation: nextConversation,
|
|
2887
|
+
message: terminalMessage,
|
|
2888
|
+
requestText: terminalPayload,
|
|
2889
|
+
startedAt: bridgeStartedAt,
|
|
2890
|
+
agentTimeoutMinutes,
|
|
2891
|
+
agentHardTimeoutMinutes,
|
|
2892
|
+
monitorLockVersion: monitorOwner.LOCK_VERSION,
|
|
2893
|
+
preSendScreenFingerprint,
|
|
2894
|
+
codexRolloutAcceptanceAnchor,
|
|
2895
|
+
claudeTranscriptAnchor,
|
|
2896
|
+
claudeHome
|
|
2897
|
+
})
|
|
2898
|
+
: nextConversation;
|
|
2899
|
+
const preparedConversation = withTerminalBridgeSubmission({
|
|
2900
|
+
conversation: bridgeConversation,
|
|
2901
|
+
messageId: message.id,
|
|
2902
|
+
messageType: terminalMessage.type,
|
|
2903
|
+
messageBody: String(message.body),
|
|
2904
|
+
requestText: terminalPayload,
|
|
2905
|
+
status: "prepared",
|
|
2906
|
+
preparedAt: bridgeStartedAt
|
|
2907
|
+
});
|
|
2908
|
+
const previousGenerationId = stringValue(previousDispatchLedger?.generation_id) ??
|
|
2909
|
+
stringValue(previousDispatchLedger?.message_id);
|
|
2910
|
+
const progress = {
|
|
2911
|
+
stagedConversation: preparedConversation
|
|
2912
|
+
};
|
|
2913
|
+
let preparedMessageEvent;
|
|
2914
|
+
const recordPostTransportBookkeepingFailure = (phase, error) => {
|
|
2915
|
+
const warning = error instanceof Error ? error.message : String(error);
|
|
2916
|
+
progress.bookkeepingWarning ??= warning;
|
|
2917
|
+
runtimeLog("warn", "terminal_message_post_transport_bookkeeping_failed", {
|
|
2918
|
+
conversation_id: conversation.conversation_id,
|
|
2919
|
+
terminal_target: terminalControl.target,
|
|
2920
|
+
phase,
|
|
2921
|
+
error: warning
|
|
2922
|
+
});
|
|
2923
|
+
};
|
|
2924
|
+
const { applicationPorts } = bindTerminalDispatchCapabilities({
|
|
2925
|
+
scopes: transaction.scopes,
|
|
2926
|
+
resources: transaction.resources,
|
|
2927
|
+
repositories: terminalDispatchCapabilityRepositories({
|
|
2928
|
+
previousLedger: previousDispatchLedger,
|
|
2929
|
+
preparedMessageEvent: () => {
|
|
2930
|
+
if (!preparedMessageEvent) {
|
|
2931
|
+
throw new Error("prepared message event was not validated");
|
|
2932
|
+
}
|
|
2933
|
+
return preparedMessageEvent;
|
|
2934
|
+
},
|
|
2935
|
+
restoreDeferred: rollback.restoreDeferred,
|
|
2936
|
+
rollbackBeforeInput: rollback.rollbackBeforeInput
|
|
2937
|
+
}),
|
|
2938
|
+
local: {
|
|
2939
|
+
synchronizeStageProgress: (current, stage, at) => {
|
|
2940
|
+
progress.stagedConversation = current;
|
|
2941
|
+
progress.textInjectedAt = stage === "text_injected"
|
|
2942
|
+
? at
|
|
2943
|
+
: progress.textInjectedAt;
|
|
2944
|
+
progress.enterDispatchedAt = stage === "enter_dispatched"
|
|
2945
|
+
? at
|
|
2946
|
+
: progress.enterDispatchedAt;
|
|
2947
|
+
},
|
|
2948
|
+
audit: {
|
|
2949
|
+
log: runtimeLog,
|
|
2950
|
+
recordBookkeepingFailure: recordPostTransportBookkeepingFailure,
|
|
2951
|
+
recordPersistenceFailure: (phase, error, current) => {
|
|
2952
|
+
runtimeLog("error", phase, {
|
|
2953
|
+
conversation_id: current.conversation_id,
|
|
2954
|
+
terminal_target: terminalControl.target,
|
|
2955
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
});
|
|
2961
|
+
const application = new dispatchApplication.TerminalDispatchApplication({
|
|
2962
|
+
originalConversation: conversation,
|
|
2963
|
+
preparedConversation,
|
|
2964
|
+
message: terminalMessage,
|
|
2965
|
+
executor,
|
|
2966
|
+
terminalControl,
|
|
2967
|
+
receiptTerminalControl: terminalControlFromTakeover(isRecord(preparedConversation.native_session_takeover)
|
|
2968
|
+
? preparedConversation.native_session_takeover
|
|
2969
|
+
: undefined),
|
|
2970
|
+
requestText: terminalPayload,
|
|
2971
|
+
requestHash: terminalRequestHash,
|
|
2972
|
+
preparedAt: bridgeStartedAt,
|
|
2973
|
+
statePath,
|
|
2974
|
+
eventLogPath: logPath,
|
|
2975
|
+
previousGenerationId,
|
|
2976
|
+
dispatcherPid: cliPid(),
|
|
2977
|
+
storeDir: lockedStoreDir,
|
|
2978
|
+
recordRawAttachmentAfterSend,
|
|
2979
|
+
ledgerBindingFields: terminalBindingLedgerFields
|
|
2980
|
+
}, applicationPorts);
|
|
2981
|
+
application.persistPrepared();
|
|
2982
|
+
return {
|
|
2983
|
+
application,
|
|
2984
|
+
terminalMessage,
|
|
2985
|
+
progress,
|
|
2986
|
+
recordPostTransportBookkeepingFailure,
|
|
2987
|
+
validatePreparedMessageEvent: () => {
|
|
2988
|
+
preparedMessageEvent = messageEvent(terminalMessage);
|
|
2989
|
+
}
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
function terminalDispatchTransportLifecycle({ request, prepared, application, deferredForegroundScope }) {
|
|
2993
|
+
const { options, observedHandoff, verifiedEmptyCodexHandoff, deferredCodexForegroundBinding } = request;
|
|
2994
|
+
return prepared.execution.transportLifecycle({
|
|
2995
|
+
...(observedHandoff
|
|
2996
|
+
? {
|
|
2997
|
+
observedHandoff: {
|
|
2998
|
+
verify: (requireEmptyComposer) => assertObservedHandoffTransportBoundary({
|
|
2999
|
+
options,
|
|
3000
|
+
terminal: observedHandoff.terminal,
|
|
3001
|
+
transition: observedHandoff.transition,
|
|
3002
|
+
requireEmptyComposer
|
|
3003
|
+
})
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
: {}),
|
|
3007
|
+
...(verifiedEmptyCodexHandoff
|
|
3008
|
+
? {
|
|
3009
|
+
verifiedEmptyHandoff: {
|
|
3010
|
+
verify: (requireEmptyComposer) => assertVerifiedEmptyCodexTransportBoundary({
|
|
3011
|
+
options,
|
|
3012
|
+
boundary: verifiedEmptyCodexHandoff,
|
|
3013
|
+
requireEmptyComposer
|
|
3014
|
+
})
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
: {}),
|
|
3018
|
+
...(deferredCodexForegroundBinding
|
|
3019
|
+
? {
|
|
3020
|
+
deferredBinding: {
|
|
3021
|
+
verify: (requireEmptyComposer) => assertDeferredCodexForegroundBindingBoundary({
|
|
3022
|
+
options,
|
|
3023
|
+
scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
|
|
3024
|
+
boundary: deferredCodexForegroundBinding,
|
|
3025
|
+
expectedSourceStatus: "transitioning",
|
|
3026
|
+
requireNoDispatch: false,
|
|
3027
|
+
requireEmptyComposer
|
|
3028
|
+
}),
|
|
3029
|
+
begin: (at) => deferredForegroundApplication(options, deferredCodexForegroundBinding.terminal).begin({
|
|
3030
|
+
scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
|
|
3031
|
+
boundary: deferredForegroundBoundaryProjection(deferredCodexForegroundBinding),
|
|
3032
|
+
at
|
|
3033
|
+
}),
|
|
3034
|
+
advance: (stage, at) => deferredForegroundApplication(options, deferredCodexForegroundBinding.terminal).advance({
|
|
3035
|
+
scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
|
|
3036
|
+
boundary: deferredForegroundBoundaryProjection(deferredCodexForegroundBinding),
|
|
3037
|
+
stage,
|
|
3038
|
+
at
|
|
3039
|
+
})
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
: {}),
|
|
3043
|
+
recordStage: (stage, at, afterDurable) => application.recordTransportStage(stage, at, afterDurable)
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
async function terminalDispatchAcceptance({ request, prepared, conversation }) {
|
|
3047
|
+
const { options, executor } = request;
|
|
3048
|
+
const { execution, terminalControl } = prepared;
|
|
3049
|
+
const timeoutMs = positiveMilliseconds(options.terminalAcceptanceTimeoutMs ??
|
|
3050
|
+
DEFAULT_TERMINAL_ACCEPTANCE_TIMEOUT_MS, "--terminal-acceptance-timeout-ms");
|
|
3051
|
+
return execution.pollAcceptance({
|
|
3052
|
+
executor: executor.kind,
|
|
3053
|
+
conversation,
|
|
3054
|
+
terminalControl,
|
|
3055
|
+
timeoutMs,
|
|
3056
|
+
pollIntervalMs: Math.max(10, Math.min(timeoutMs, Number(options.terminalAcceptancePollIntervalMs ??
|
|
3057
|
+
DEFAULT_TERMINAL_ACCEPTANCE_POLL_INTERVAL_MS))),
|
|
3058
|
+
scrollbackLines: Number(options.scrollbackLines ?? 240)
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3061
|
+
function launchAcceptedTerminalMonitor({ request, prepared, conversation, acceptance, recordFailure }) {
|
|
3062
|
+
const { options } = request;
|
|
3063
|
+
const { bridge, statePath, logPath, terminalControl, agentTimeoutMinutes, agentHardTimeoutMinutes } = prepared;
|
|
3064
|
+
if (!bridge ||
|
|
3065
|
+
!(acceptance.outcome === "agent_accepted" ||
|
|
3066
|
+
acceptance.outcome === "pending_acceptance")) {
|
|
3067
|
+
return undefined;
|
|
3068
|
+
}
|
|
3069
|
+
try {
|
|
3070
|
+
const monitor = startTerminalBridgeMonitorForConversation({
|
|
3071
|
+
conversation,
|
|
3072
|
+
statePath,
|
|
3073
|
+
logPath,
|
|
3074
|
+
options
|
|
3075
|
+
});
|
|
3076
|
+
if (monitor) {
|
|
3077
|
+
appendEvent(logPath, {
|
|
3078
|
+
ts: cliNow().toISOString(),
|
|
3079
|
+
conversation_id: conversation.conversation_id,
|
|
3080
|
+
event: "terminal_bridge_monitor_launch",
|
|
3081
|
+
pid: monitor.pid ?? null,
|
|
3082
|
+
terminal_control: terminalControl,
|
|
3083
|
+
phase: acceptance.outcome,
|
|
3084
|
+
agent_timeout_minutes: agentTimeoutMinutes,
|
|
3085
|
+
agent_hard_timeout_minutes: agentHardTimeoutMinutes
|
|
3086
|
+
});
|
|
3087
|
+
}
|
|
3088
|
+
return monitor;
|
|
3089
|
+
}
|
|
3090
|
+
catch (error) {
|
|
3091
|
+
recordFailure("monitor_launch", error);
|
|
3092
|
+
return undefined;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
function presentTerminalDispatchTransportFailure({ error, request, prepared, application, progress, deferredForegroundScope, bridgeMonitor }) {
|
|
3096
|
+
const { options, executor, message, deferredCodexForegroundBinding } = request;
|
|
3097
|
+
const { lockedStoreDir, terminalControl, presentationContext, presentationPorts } = prepared;
|
|
3098
|
+
if (!progress.textInjectedAt &&
|
|
3099
|
+
error instanceof TerminalInputNotStartedError) {
|
|
3100
|
+
renderTerminalZeroInputAbort(application.recordZeroInputAbort({
|
|
3101
|
+
failureKind: "transport",
|
|
3102
|
+
error,
|
|
3103
|
+
abortedAt: cliNow().toISOString()
|
|
3104
|
+
}), presentationContext, presentationPorts, bridgeMonitor?.pid);
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
const uncertainAt = cliNow().toISOString();
|
|
3108
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3109
|
+
if (deferredCodexForegroundBinding) {
|
|
3110
|
+
try {
|
|
3111
|
+
deferredForegroundApplication(options, deferredCodexForegroundBinding.terminal).markUncertain({
|
|
3112
|
+
scope: required(deferredForegroundScope, "deferred foreground mutation scope is unavailable"),
|
|
3113
|
+
boundary: deferredForegroundBoundaryProjection(deferredCodexForegroundBinding),
|
|
3114
|
+
reason: errorMessage
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3117
|
+
catch (transferError) {
|
|
3118
|
+
runtimeLog("error", "deferred_codex_foreground_uncertain_persist_failed", {
|
|
3119
|
+
transfer_id: deferredCodexForegroundBinding.transferId,
|
|
3120
|
+
error: transferError instanceof Error
|
|
3121
|
+
? transferError.message
|
|
3122
|
+
: String(transferError)
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
const uncertainConversation = application.applyUncertain(uncertainAt, error);
|
|
3127
|
+
const stalledConversationIds = stallOtherTerminalBridgeConversationsForUncertainDispatch({
|
|
3128
|
+
storeDir: lockedStoreDir,
|
|
3129
|
+
terminalControl,
|
|
3130
|
+
currentConversationId: uncertainConversation.conversation_id,
|
|
3131
|
+
uncertainMessageId: message.id
|
|
3132
|
+
});
|
|
3133
|
+
runtimeLog("error", "terminal_message_submit_uncertain", {
|
|
3134
|
+
conversation_id: uncertainConversation.conversation_id,
|
|
3135
|
+
agent: executor.kind,
|
|
3136
|
+
terminal_target: terminalControl.target,
|
|
3137
|
+
error: errorMessage,
|
|
3138
|
+
do_not_retry: true,
|
|
3139
|
+
stalled_conversation_ids: stalledConversationIds
|
|
3140
|
+
});
|
|
3141
|
+
presentTerminalUncertain({
|
|
3142
|
+
conversation: uncertainConversation,
|
|
3143
|
+
stalledConversationIds,
|
|
3144
|
+
textInjected: progress.textInjectedAt !== undefined,
|
|
3145
|
+
enterDispatched: progress.enterDispatchedAt !== undefined,
|
|
3146
|
+
monitorPid: bridgeMonitor?.pid
|
|
3147
|
+
}, presentationContext, presentationPorts);
|
|
3148
|
+
}
|
|
3149
|
+
async function runTerminalDispatchTransport({ request, prepared, application, progress, recordPostTransportBookkeepingFailure, deferredForegroundScope }) {
|
|
3150
|
+
const { executor } = request;
|
|
3151
|
+
const { terminalBridge, terminalControl, terminalPayload, preSendRuntime, codexRolloutAcceptanceAnchor, presentationContext, presentationPorts } = prepared;
|
|
3152
|
+
let bridgeMonitor;
|
|
3153
|
+
try {
|
|
3154
|
+
const transportLifecycle = terminalDispatchTransportLifecycle({
|
|
3155
|
+
request,
|
|
3156
|
+
prepared,
|
|
3157
|
+
application,
|
|
3158
|
+
deferredForegroundScope
|
|
3159
|
+
});
|
|
3160
|
+
await terminalBridge.send(executor.kind, terminalControl, terminalPayload, { runtime: preSendRuntime, ...transportLifecycle });
|
|
3161
|
+
if (!progress.enterDispatchedAt) {
|
|
3162
|
+
throw new Error("terminal bridge returned without an enter_dispatched receipt");
|
|
3163
|
+
}
|
|
3164
|
+
if (codexRolloutAcceptanceAnchor?.version === 2 &&
|
|
3165
|
+
cliEnv().AKK_TEST_EXIT_AFTER_VIRGIN_ENTER_DISPATCHED === "1") {
|
|
3166
|
+
cliExit(86);
|
|
3167
|
+
}
|
|
3168
|
+
const submissionOwner = await resolveTerminalDispatchSubmissionOwner(prepared, request, application, progress.stagedConversation);
|
|
3169
|
+
if ("failureReason" in submissionOwner) {
|
|
3170
|
+
presentTerminalIdentityFailure(submissionOwner.conversation, submissionOwner.failureReason, presentationContext, presentationPorts, bridgeMonitor?.pid);
|
|
3171
|
+
return { outcome: "handled" };
|
|
3172
|
+
}
|
|
3173
|
+
const acceptance = submissionOwner.deferredCandidateBindingPending
|
|
3174
|
+
? { outcome: "pending_acceptance" }
|
|
3175
|
+
: await terminalDispatchAcceptance({
|
|
3176
|
+
request,
|
|
3177
|
+
prepared,
|
|
3178
|
+
conversation: submissionOwner.conversation
|
|
3179
|
+
});
|
|
3180
|
+
const deliveredConversation = application.applyAcceptance(submissionOwner.conversation, acceptance, cliNow().toISOString()).conversation;
|
|
3181
|
+
bridgeMonitor = launchAcceptedTerminalMonitor({
|
|
3182
|
+
request,
|
|
3183
|
+
prepared,
|
|
3184
|
+
conversation: deliveredConversation,
|
|
3185
|
+
acceptance,
|
|
3186
|
+
recordFailure: recordPostTransportBookkeepingFailure
|
|
3187
|
+
});
|
|
3188
|
+
return {
|
|
3189
|
+
outcome: "completed",
|
|
3190
|
+
conversation: deliveredConversation,
|
|
3191
|
+
acceptance,
|
|
3192
|
+
bridgeMonitor
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
catch (error) {
|
|
3196
|
+
presentTerminalDispatchTransportFailure({
|
|
3197
|
+
error,
|
|
3198
|
+
request,
|
|
3199
|
+
prepared,
|
|
3200
|
+
application,
|
|
3201
|
+
progress,
|
|
3202
|
+
deferredForegroundScope,
|
|
3203
|
+
bridgeMonitor
|
|
3204
|
+
});
|
|
3205
|
+
return { outcome: "handled" };
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
async function runTerminalControlSend(request) {
|
|
3209
|
+
const { transaction, recordMessageAfterSend = false, onTerminalPreflightVerified, deferredCodexForegroundBinding } = request;
|
|
3210
|
+
const prepared = await prepareTerminalControlSend(request);
|
|
3211
|
+
if (!prepared) {
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
const { route, presentationContext, presentationPorts } = prepared;
|
|
3215
|
+
const deferredForegroundScope = deferredCodexForegroundBinding
|
|
3216
|
+
? bindDeferredForegroundApplicationScope(transaction.scopes, transaction.resources)
|
|
3217
|
+
: undefined;
|
|
3218
|
+
// A newly discovered raw terminal may not have an authoritative Session
|
|
3219
|
+
// yet. Commit that Session only after every pre-input terminal and native
|
|
3220
|
+
// acceptance check has passed, but before the Turn or dispatch ledger can
|
|
3221
|
+
// become durable. This prevents a failed virgin attach from leaving a
|
|
3222
|
+
// zero-identity `bound` Session that fences every later control action.
|
|
3223
|
+
if (deferredCodexForegroundBinding) {
|
|
3224
|
+
deferredForegroundScope?.assertBoundary(deferredForegroundBoundaryProjection(deferredCodexForegroundBinding));
|
|
3225
|
+
}
|
|
3226
|
+
const preTransportRollback = await onTerminalPreflightVerified?.(route);
|
|
3227
|
+
const rollback = terminalDispatchRollbackRepositories({
|
|
3228
|
+
request,
|
|
3229
|
+
prepared,
|
|
3230
|
+
deferredForegroundScope,
|
|
3231
|
+
preTransportRollback: typeof preTransportRollback === "function"
|
|
3232
|
+
? preTransportRollback
|
|
3233
|
+
: undefined
|
|
3234
|
+
});
|
|
3235
|
+
const dispatch = createTerminalDispatchRuntime(request, prepared, rollback);
|
|
3236
|
+
const { application, progress, recordPostTransportBookkeepingFailure, validatePreparedMessageEvent } = dispatch;
|
|
3237
|
+
try {
|
|
3238
|
+
// Preserve legacy argument-evaluation priority inside the setup catch:
|
|
3239
|
+
// validate once before raw-attach bookkeeping, then let the gated
|
|
3240
|
+
// repository consume only this cached immutable event.
|
|
3241
|
+
application.recordPreparedBookkeeping(recordMessageAfterSend, cliEnv().AKK_TEST_TERMINAL_SETUP_FAILURE === "1", recordMessageAfterSend
|
|
3242
|
+
? validatePreparedMessageEvent
|
|
3243
|
+
: undefined);
|
|
3244
|
+
}
|
|
3245
|
+
catch (error) {
|
|
3246
|
+
renderTerminalZeroInputAbort(application.recordZeroInputAbort({
|
|
3247
|
+
failureKind: "setup",
|
|
3248
|
+
error,
|
|
3249
|
+
abortedAt: cliNow().toISOString(),
|
|
3250
|
+
injectStatePersistenceFailure: cliEnv().AKK_TEST_ABORTED_STATE_PERSISTENCE_FAILURE === "1"
|
|
3251
|
+
}), presentationContext, presentationPorts, undefined);
|
|
3252
|
+
return;
|
|
3253
|
+
}
|
|
3254
|
+
const transport = await runTerminalDispatchTransport({
|
|
3255
|
+
request,
|
|
3256
|
+
prepared,
|
|
3257
|
+
application,
|
|
3258
|
+
progress,
|
|
3259
|
+
recordPostTransportBookkeepingFailure,
|
|
3260
|
+
deferredForegroundScope
|
|
3261
|
+
});
|
|
3262
|
+
if (transport.outcome === "handled") {
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
const deliveredConversation = transport.conversation;
|
|
3266
|
+
const acceptanceResult = transport.acceptance;
|
|
3267
|
+
const bridgeMonitor = transport.bridgeMonitor;
|
|
3268
|
+
const nativeAccepted = acceptanceResult?.outcome === "agent_accepted";
|
|
3269
|
+
const postSubmissionWarning = application.recordPostSubmissionBookkeeping(deliveredConversation, nativeAccepted, () => cliNow().toISOString());
|
|
3270
|
+
if (postSubmissionWarning !== undefined) {
|
|
3271
|
+
progress.bookkeepingWarning = postSubmissionWarning;
|
|
3272
|
+
}
|
|
3273
|
+
presentTerminalCompleted({
|
|
3274
|
+
conversation: deliveredConversation,
|
|
3275
|
+
acceptance: acceptanceResult,
|
|
3276
|
+
monitorPid: bridgeMonitor?.pid,
|
|
3277
|
+
bookkeepingWarning: progress.bookkeepingWarning
|
|
3278
|
+
}, presentationContext, presentationPorts);
|
|
3279
|
+
}
|
|
3280
|
+
export function createTerminalCommandCliFacade(dependencies) {
|
|
3281
|
+
const call = (operation) => terminalCommandContext.run(dependencies, operation);
|
|
3282
|
+
return Object.freeze({
|
|
3283
|
+
runSend: (options) => call(() => runSend(options)),
|
|
3284
|
+
runRespond: (options) => call(() => runRespond(options)),
|
|
3285
|
+
runApprove: (options) => call(() => runApprove(options))
|
|
3286
|
+
});
|
|
3287
|
+
}
|
|
3288
|
+
//# sourceMappingURL=terminal-command-cli-adapter.js.map
|