@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,1389 @@
|
|
|
1
|
+
// CLI infrastructure for verified-empty, deferred foreground, and observed handoff authority.
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { canonicalJson } from "./canonical-json.js";
|
|
5
|
+
import { cliCwd, cliEnv, cliExit, cliNow, cliPid, cliRuntimeLog as runtimeLog } from "./cli-runtime-context.js";
|
|
6
|
+
import { DeferredForegroundApplicationService } from "./deferred-foreground-application-service.js";
|
|
7
|
+
import { TerminalHandoffApplicationService } from "./terminal-handoff-application-service.js";
|
|
8
|
+
import { bindDeferredForegroundApplicationScope, bindDeferredForegroundWriterScope } from "./deferred-foreground-capability.js";
|
|
9
|
+
import { createDeferredForegroundTransferId, listDeferredForegroundTransfers, loadDeferredForegroundTransfer } from "./deferred-foreground-transfer.js";
|
|
10
|
+
import { isFinalDeferredForegroundTransferStatus } from "./deferred-foreground-transfer-policy.js";
|
|
11
|
+
import { DeferredForegroundRecoveryService } from "./deferred-foreground-recovery-service.js";
|
|
12
|
+
import * as deferredRecoveryAdapter from "./deferred-foreground-recovery-cli-adapter.js";
|
|
13
|
+
import { prepareDeferredForegroundBinding } from "./deferred-foreground-preparation-service.js";
|
|
14
|
+
import { assertDeferredForegroundBoundary, deferredForegroundBoundaryProjection, deferredForegroundConcreteBoundary, deferredForegroundTransferMatchesTerminal, projectDeferredForegroundTerminalFacts } from "./deferred-foreground-preparation-cli-adapter.js";
|
|
15
|
+
import { executorDefinitionForKind } from "./executors.js";
|
|
16
|
+
import { createManagedSessionId, createNativeThreadTransitionId, isExactNativeThreadId, managedSessionBindingToken, managedSessionRevision, nativeThreadCommandFingerprint, terminalBindingFrom } from "./managed-session.js";
|
|
17
|
+
import { withCanonicalMutationLocks } from "./mutation-transaction.js";
|
|
18
|
+
import { claudeComposerEmpty, codexComposerEmpty } from "./native-thread-lifecycle-recovery-adapter.js";
|
|
19
|
+
import { sessionIdForConversation, turnIdForConversation } from "./protocol.js";
|
|
20
|
+
import { commitVerifiedLifecycleTransition, listManagedSessions, loadManagedSession, loadNativeThreadTransition, saveManagedSession, saveNativeThreadTransition, tryLoadManagedSession } from "./session-store.js";
|
|
21
|
+
import { pathsForConversation, pathsForConversationDir } from "./store.js";
|
|
22
|
+
import { activeTurnHandoffDecisionToken as projectActiveTurnHandoffDecisionToken, codexIdentityFence, deferredCodexForegroundBindingToken, exactBoundCodexSendSource, observedHandoffAuthorityToken as projectObservedHandoffAuthorityToken, terminalControlsShareIncarnation, verifiedEmptyCodexHandoffToken } from "./terminal-authority-policy.js";
|
|
23
|
+
import { terminalControlEvidenceMatches } from "./terminal-control-ref.js";
|
|
24
|
+
import { nativeThreadTransitionRevision, terminalSubmissionPayload } from "./terminal-dispatch-execution.js";
|
|
25
|
+
import { nativeThreadLifecycleLedger as lifecycleLedger } from "./terminal-dispatch-ledger-codec.js";
|
|
26
|
+
import { terminalBridgeRequestFingerprint, terminalBridgeSubmission, terminalBridgeSubmissionReceipts, terminalDispatchTextSummary } from "./terminal-dispatch-receipt.js";
|
|
27
|
+
import { captureCodexCandidateSetRolloutAcceptanceAnchor } from "./terminal-submission-acceptance.js";
|
|
28
|
+
import { validTerminalMonitorTimestampMs as validTimestampMs } from "./terminal-monitor-decision-policy.js";
|
|
29
|
+
import { terminalControlFromTakeover } from "./terminal-runtime-cli-adapter.js";
|
|
30
|
+
import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
|
|
31
|
+
const TERMINAL_INPUT_EVIDENCE_FIELDS = ["text_injected_at",
|
|
32
|
+
"enter_dispatched_at", "submitted_at", "agent_accepted_at",
|
|
33
|
+
"not_accepted_at", "uncertain_at", "acceptance_evidence"];
|
|
34
|
+
const HUMAN_OBSERVED_HANDOFF_FINGERPRINT = nativeThreadCommandFingerprint("adopt_external_thread:human_observed:no_terminal_input:v1");
|
|
35
|
+
const handoffApplication = new TerminalHandoffApplicationService();
|
|
36
|
+
const terminalHandoffContext = new AsyncLocalStorage();
|
|
37
|
+
function terminalHandoffRuntime() {
|
|
38
|
+
const runtime = terminalHandoffContext.getStore();
|
|
39
|
+
if (!runtime) {
|
|
40
|
+
throw new Error("Terminal handoff facade runtime is unavailable");
|
|
41
|
+
}
|
|
42
|
+
return runtime;
|
|
43
|
+
}
|
|
44
|
+
function contextualPort(group, name) {
|
|
45
|
+
return ((...arguments_) => {
|
|
46
|
+
const operation = terminalHandoffRuntime().dependencies[group][name];
|
|
47
|
+
return operation(...arguments_);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const storeDirFromOptions = contextualPort("runtime", "storeDir");
|
|
51
|
+
const createTerminalAgentBridge = contextualPort("runtime", "createBridge");
|
|
52
|
+
const terminalRuntimeForLiveIdentity = contextualPort("identity", "terminalRuntimeForLiveIdentity");
|
|
53
|
+
const agentVersionForRunningProcess = contextualPort("runtime", "agentVersion");
|
|
54
|
+
const required = contextualPort("runtime", "required");
|
|
55
|
+
const textSummary = terminalDispatchTextSummary;
|
|
56
|
+
const isStoreMutationLockTimeout = contextualPort("runtime", "isStoreMutationLockTimeout");
|
|
57
|
+
const codexProcessIncarnationForPid = contextualPort("identity", "codexProcessIncarnationForPid");
|
|
58
|
+
const inspectCodexOpenRootRolloutInventory = contextualPort("acceptance", "inspectCodexOpenRoots");
|
|
59
|
+
const observeCurrentNativeAgentSessionIdentity = contextualPort("acceptance", "observeNativeIdentity");
|
|
60
|
+
const resolveCurrentNativeAgentSessionIdentity = contextualPort("acceptance", "resolveNativeIdentity");
|
|
61
|
+
const exactLifecycleProcessIdentity = contextualPort("identity", "exactLifecycleProcessIdentity");
|
|
62
|
+
const managedBindingConflictKindForResolvedTerminal = contextualPort("identity", "managedBindingConflictKindForResolvedTerminal");
|
|
63
|
+
const assertNativeThreadHasExclusiveOwnership = contextualPort("authority", "assertExclusive");
|
|
64
|
+
const assertCodexComposerReadyForAutomatedInput = contextualPort("identity", "assertCodexComposerReadyForAutomatedInput");
|
|
65
|
+
const assertNativeAgentIdentityForTurn = contextualPort("acceptance", "assertTurnIdentity");
|
|
66
|
+
const assertVerifiedEmptyCodexHandoffBoundary = contextualPort("identity", "assertVerifiedEmptyCodexHandoffBoundary");
|
|
67
|
+
const observeDeferredCodexAuthority = contextualPort("identity", "observeDeferredCodexAuthority");
|
|
68
|
+
const deferredCodexForegroundDispatchSnapshot = contextualPort("identity", "deferredCodexForegroundDispatchSnapshot");
|
|
69
|
+
const codexCandidateInventoryHasNoOtherManagedClaim = contextualPort("identity", "codexCandidateInventoryHasNoOtherManagedClaim");
|
|
70
|
+
const deferredCandidateSourceTurnHistory = contextualPort("identity", "deferredCandidateSourceTurnHistory");
|
|
71
|
+
const explicitlyAbandonedCandidateSourceFingerprint = contextualPort("identity", "explicitlyAbandonedCandidateSourceFingerprint");
|
|
72
|
+
const assertFrozenExplicitlyAbandonedPredecessorAuthority = contextualPort("identity", "assertFrozenExplicitlyAbandonedPredecessorAuthority");
|
|
73
|
+
const deferredCodexPreviousDispatchSnapshotMatches = contextualPort("identity", "deferredCodexPreviousDispatchSnapshotMatches");
|
|
74
|
+
const terminalListCliFacade = {
|
|
75
|
+
assertTerminalIncarnationCanStartTurn: contextualPort("authority", "assertTerminalCanStartTurn"),
|
|
76
|
+
managedSessionHasUnresolvedNativeTransition: contextualPort("authority", "hasUnresolvedTransition")
|
|
77
|
+
};
|
|
78
|
+
const assertManagedSessionCanStartTurn = contextualPort("authority", "assertSessionCanStartTurn");
|
|
79
|
+
const managedTurnsForSession = contextualPort("authority", "turnsForSession");
|
|
80
|
+
const assertTerminalLifecycleReady = contextualPort("authority", "assertTerminalReady");
|
|
81
|
+
const assertSafeTerminalSend = contextualPort("authority", "assertSafeSend");
|
|
82
|
+
const managedSessionStoreDirForConversation = contextualPort("repository", "storeDirForConversation");
|
|
83
|
+
const loadTerminalBridgeDispatchLedger = contextualPort("repository", "loadLedger");
|
|
84
|
+
const saveTerminalBridgeDispatchLedger = contextualPort("repository", "saveLedger");
|
|
85
|
+
const terminalDispatchRecordMatchesControl = contextualPort("repository", "ledgerMatchesControl");
|
|
86
|
+
const terminalBindingLedgerFields = contextualPort("repository", "bindingFields");
|
|
87
|
+
const withNativeAgentSessionIdentity = contextualPort("repository", "withNativeIdentity");
|
|
88
|
+
const withTerminalBridgeSubmission = contextualPort("repository", "withSubmission");
|
|
89
|
+
const saveLifecycleTerminalDispatchLedger = contextualPort("repository", "saveLifecycleLedger");
|
|
90
|
+
const terminalWriterMutationLocks = contextualPort("repository", "mutationLocks");
|
|
91
|
+
const withTerminalDispatchStateScope = contextualPort("repository", "withStateScope");
|
|
92
|
+
const originalExpectedTerminalSelector = {
|
|
93
|
+
has: (options) => terminalHandoffRuntime()
|
|
94
|
+
.originalExpectedTerminalSelector.has(options),
|
|
95
|
+
get: (options) => terminalHandoffRuntime()
|
|
96
|
+
.originalExpectedTerminalSelector.get(options)
|
|
97
|
+
};
|
|
98
|
+
function assertSafeAbortedTerminalRetryBinding({ owner, receipt, storeDir, terminalControl, messageId }) {
|
|
99
|
+
if (!(receipt.status === "aborted" && receipt.safe_to_retry === true)) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
const sessionId = sessionIdForConversation(owner);
|
|
103
|
+
const managedSession = tryLoadManagedSession(storeDir, sessionId);
|
|
104
|
+
const binding = managedSession?.binding;
|
|
105
|
+
const receiptBindingId = stringValue(receipt.binding_id);
|
|
106
|
+
const receiptBindingGeneration = Number(receipt.binding_generation);
|
|
107
|
+
const receiptNativeThreadId = stringValue(receipt.native_thread_id);
|
|
108
|
+
const ownerTakeover = isRecord(owner.native_session_takeover)
|
|
109
|
+
? owner.native_session_takeover
|
|
110
|
+
: undefined;
|
|
111
|
+
const ownerControl = terminalControlFromTakeover(ownerTakeover);
|
|
112
|
+
const ownerNativeThreadId = stringValue(owner.native_thread_id) ??
|
|
113
|
+
stringValue(ownerTakeover?.terminal_agent_session_id) ??
|
|
114
|
+
stringValue(ownerTakeover?.terminal_agent_expected_session_id);
|
|
115
|
+
const ownerAgentPid = Number(ownerTakeover?.terminal_agent_pid);
|
|
116
|
+
if (handoffApplication.authorityChecksPass([
|
|
117
|
+
() => managedSession !== undefined,
|
|
118
|
+
() => managedSession?.status === "bound", () => binding !== undefined,
|
|
119
|
+
() => receiptBindingId !== undefined,
|
|
120
|
+
() => Number.isSafeInteger(receiptBindingGeneration),
|
|
121
|
+
() => receiptNativeThreadId !== undefined,
|
|
122
|
+
() => receiptBindingId === stringValue(owner.terminal_binding_id),
|
|
123
|
+
() => receiptBindingGeneration === Number(owner.terminal_binding_generation),
|
|
124
|
+
() => receiptNativeThreadId === ownerNativeThreadId,
|
|
125
|
+
() => binding.binding_id === receiptBindingId,
|
|
126
|
+
() => binding.generation === receiptBindingGeneration,
|
|
127
|
+
() => binding.native_thread_id === receiptNativeThreadId,
|
|
128
|
+
() => Number.isSafeInteger(ownerAgentPid),
|
|
129
|
+
() => binding.native_process.pid === ownerAgentPid,
|
|
130
|
+
() => ownerControl !== undefined,
|
|
131
|
+
() => terminalControlsShareIncarnation(ownerControl, terminalControl),
|
|
132
|
+
() => terminalControlsShareIncarnation(binding.terminal_control, terminalControl)
|
|
133
|
+
])) {
|
|
134
|
+
return managedSession;
|
|
135
|
+
}
|
|
136
|
+
const recoveredSource = safeAbortedDeferredRetrySourceSession({
|
|
137
|
+
owner,
|
|
138
|
+
receipt,
|
|
139
|
+
storeDir,
|
|
140
|
+
terminalControl,
|
|
141
|
+
messageId
|
|
142
|
+
});
|
|
143
|
+
if (!recoveredSource) {
|
|
144
|
+
throw new Error(`terminal idempotency key ${messageId} belongs to a safe-aborted Turn ` +
|
|
145
|
+
"whose Session binding is no longer current; no terminal input was sent");
|
|
146
|
+
}
|
|
147
|
+
return recoveredSource;
|
|
148
|
+
}
|
|
149
|
+
function safeAbortedDeferredRetrySourceSession({ owner, receipt, storeDir, terminalControl, messageId }) {
|
|
150
|
+
const takeover = isRecord(owner.native_session_takeover)
|
|
151
|
+
? owner.native_session_takeover
|
|
152
|
+
: undefined;
|
|
153
|
+
const transferId = stringValue(takeover?.deferred_foreground_transfer_id);
|
|
154
|
+
const ownerControl = terminalControlFromTakeover(takeover);
|
|
155
|
+
if (!transferId || !takeover || !ownerControl) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
const transfer = loadDeferredForegroundTransfer(storeDir, transferId);
|
|
159
|
+
const target = tryLoadManagedSession(storeDir, transfer.target_session_id);
|
|
160
|
+
const source = tryLoadManagedSession(storeDir, transfer.source_session_id);
|
|
161
|
+
const submission = terminalBridgeSubmission(owner);
|
|
162
|
+
const matchingReceipts = terminalBridgeSubmissionReceipts(owner).filter((candidate) => stringValue(candidate.message_id) === messageId);
|
|
163
|
+
const canonical = pathsForConversation(owner.conversation_id, storeDir);
|
|
164
|
+
const targetBinding = transfer.abort_target_after_binding;
|
|
165
|
+
const sourceBinding = transfer.abort_source_after_binding;
|
|
166
|
+
const transferDispatchStartedAt = stringValue(transfer.dispatch_started_at);
|
|
167
|
+
const terminalInputNotStartedAt = stringValue(transfer.terminal_input_not_started_at);
|
|
168
|
+
const chronology = handoffApplication.zeroInputChronology({
|
|
169
|
+
inputStage: transfer.input_stage,
|
|
170
|
+
dispatchStartedAt: transferDispatchStartedAt,
|
|
171
|
+
inputNotStartedAt: terminalInputNotStartedAt,
|
|
172
|
+
dispatchStartedAtMs: validTimestampMs(transferDispatchStartedAt),
|
|
173
|
+
inputNotStartedAtMs: validTimestampMs(terminalInputNotStartedAt)
|
|
174
|
+
});
|
|
175
|
+
if (!handoffApplication.authorityChecksPass([
|
|
176
|
+
() => transfer.version === 2, () => transfer.status === "abort_resolved",
|
|
177
|
+
() => chronology.safe, () => transfer.text_injected_at === undefined,
|
|
178
|
+
() => transfer.enter_dispatched_at === undefined,
|
|
179
|
+
() => transfer.agent_accepted_at === undefined,
|
|
180
|
+
() => transfer.target_session_id === sessionIdForConversation(owner),
|
|
181
|
+
() => transfer.turn_id === turnIdForConversation(owner),
|
|
182
|
+
() => transfer.turn_id === owner.conversation_id,
|
|
183
|
+
() => transfer.message_id === messageId,
|
|
184
|
+
() => transfer.terminal_id === stringValue(takeover.native_session_id),
|
|
185
|
+
() => transfer.process_pid === Number(takeover.terminal_agent_pid),
|
|
186
|
+
() => transfer.process_uuid ===
|
|
187
|
+
stringValue(takeover.terminal_agent_process_uuid),
|
|
188
|
+
() => transfer.process_birth ===
|
|
189
|
+
stringValue(takeover.terminal_agent_process_birth),
|
|
190
|
+
() => path.resolve(transfer.workspace) === path.resolve(owner.workspace),
|
|
191
|
+
() => terminalControlsShareIncarnation(ownerControl, terminalControl),
|
|
192
|
+
() => terminalControlEvidenceMatches(transfer.terminal_endpoint, terminalControl),
|
|
193
|
+
() => owner.status === "failed", () => !isRecord(owner.callback_delivery),
|
|
194
|
+
() => !isRecord(owner.terminal_bridge_completion_claim),
|
|
195
|
+
() => !isRecord(takeover.terminal_bridge_completion_claim),
|
|
196
|
+
() => stringValue(takeover.terminal_bridge_message_id) === messageId,
|
|
197
|
+
() => stringValue(takeover.terminal_bridge_request_hash) ===
|
|
198
|
+
transfer.request_hash,
|
|
199
|
+
() => stringValue(takeover.terminal_bridge_request_hash) ===
|
|
200
|
+
stringValue(receipt.request_hash),
|
|
201
|
+
() => stringValue(takeover.deferred_foreground_transfer_id) ===
|
|
202
|
+
transfer.transfer_id,
|
|
203
|
+
() => path.resolve(stringValue(owner.state_path) ?? "") ===
|
|
204
|
+
path.resolve(canonical.statePath),
|
|
205
|
+
() => path.resolve(stringValue(owner.event_log_path) ?? "") ===
|
|
206
|
+
path.resolve(canonical.logPath),
|
|
207
|
+
() => path.resolve(managedSessionStoreDirForConversation(owner) ?? "") ===
|
|
208
|
+
path.resolve(storeDir),
|
|
209
|
+
() => submission !== undefined, () => matchingReceipts.length === 1,
|
|
210
|
+
() => canonicalJson(matchingReceipts[0]) === canonicalJson(submission),
|
|
211
|
+
() => canonicalJson(submission) === canonicalJson(receipt),
|
|
212
|
+
() => submission?.status === "aborted",
|
|
213
|
+
() => submission?.safe_to_retry === true,
|
|
214
|
+
() => stringValue(submission?.last_proven_stage) === "prepared",
|
|
215
|
+
() => Boolean(validTimestampMs(submission?.prepared_at)),
|
|
216
|
+
() => Boolean(validTimestampMs(submission?.aborted_at)),
|
|
217
|
+
() => Date.parse(String(submission?.aborted_at)) >=
|
|
218
|
+
Date.parse(String(submission?.prepared_at)),
|
|
219
|
+
() => !chronology.dispatchIntentProvedNotStarted ||
|
|
220
|
+
stringValue(submission?.aborted_at) === terminalInputNotStartedAt,
|
|
221
|
+
() => !TERMINAL_INPUT_EVIDENCE_FIELDS.some((field) => submission?.[field] !== undefined),
|
|
222
|
+
() => stringValue(submission?.session_id) === transfer.target_session_id,
|
|
223
|
+
() => stringValue(submission?.turn_id) === transfer.turn_id,
|
|
224
|
+
() => stringValue(submission?.message_id) === transfer.message_id,
|
|
225
|
+
() => stringValue(submission?.request_hash) === transfer.request_hash,
|
|
226
|
+
() => stringValue(submission?.binding_id) ===
|
|
227
|
+
transfer.target_before_binding?.binding_id,
|
|
228
|
+
() => Number(submission?.binding_generation) ===
|
|
229
|
+
transfer.target_before_binding?.generation,
|
|
230
|
+
() => stringValue(submission?.native_thread_id) === undefined,
|
|
231
|
+
() => target !== undefined, () => target?.status === "detached",
|
|
232
|
+
() => target?.last_transition_id === transfer.transfer_id,
|
|
233
|
+
() => target?.lineage.transition_id === transfer.transfer_id,
|
|
234
|
+
() => target?.lineage.previous_session_id === transfer.source_session_id,
|
|
235
|
+
() => transfer.abort_target_after_status === "detached",
|
|
236
|
+
() => targetBinding !== undefined,
|
|
237
|
+
() => managedSessionRevision(target) === transfer.abort_target_after_revision,
|
|
238
|
+
() => managedSessionBindingToken(target) ===
|
|
239
|
+
transfer.abort_target_after_binding_token,
|
|
240
|
+
() => JSON.stringify(target?.binding) === JSON.stringify(targetBinding),
|
|
241
|
+
() => source !== undefined, () => source?.status === "bound",
|
|
242
|
+
() => source?.last_transition_id ===
|
|
243
|
+
transfer.source_previous_last_transition_id,
|
|
244
|
+
() => transfer.abort_source_after_status === "bound",
|
|
245
|
+
() => sourceBinding !== undefined,
|
|
246
|
+
() => managedSessionRevision(source) === transfer.abort_source_after_revision,
|
|
247
|
+
() => managedSessionBindingToken(source) ===
|
|
248
|
+
transfer.abort_source_after_binding_token,
|
|
249
|
+
() => JSON.stringify(source?.binding) === JSON.stringify(sourceBinding),
|
|
250
|
+
() => JSON.stringify(sourceBinding) ===
|
|
251
|
+
JSON.stringify(transfer.source_before_binding)
|
|
252
|
+
])) {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
const ledger = loadTerminalBridgeDispatchLedger(terminalControl);
|
|
256
|
+
if (!ledger) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
deferredRecoveryAdapter.assertDeferredForegroundResolvedZeroInputLedger(deferredForegroundRecoveryAdapterPorts(), {
|
|
260
|
+
storeDir,
|
|
261
|
+
terminal: { terminalControl },
|
|
262
|
+
transfer,
|
|
263
|
+
ledger,
|
|
264
|
+
statePath: canonical.statePath
|
|
265
|
+
});
|
|
266
|
+
if (chronology.dispatchIntentProvedNotStarted &&
|
|
267
|
+
stringValue(ledger.aborted_at) !== terminalInputNotStartedAt) {
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
return source;
|
|
271
|
+
}
|
|
272
|
+
function exactSafeAbortedRecoveredSessionMatches({ owner, receipt, storeDir, terminalControl, messageId, expectedSessionId }) {
|
|
273
|
+
const exactReceipt = receipt ?? (() => {
|
|
274
|
+
const matches = terminalBridgeSubmissionReceipts(owner).filter((candidate) => stringValue(candidate.message_id) === messageId);
|
|
275
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
276
|
+
})();
|
|
277
|
+
if (!exactReceipt ||
|
|
278
|
+
exactReceipt.status !== "aborted" ||
|
|
279
|
+
exactReceipt.safe_to_retry !== true) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
const recoveredSession = assertSafeAbortedTerminalRetryBinding({
|
|
283
|
+
owner,
|
|
284
|
+
receipt: exactReceipt,
|
|
285
|
+
storeDir,
|
|
286
|
+
terminalControl,
|
|
287
|
+
messageId
|
|
288
|
+
});
|
|
289
|
+
return recoveredSession?.session_id === expectedSessionId;
|
|
290
|
+
}
|
|
291
|
+
async function maybeDetachVerifiedEmptyCodexSource({ options, terminal, sourceSession, observation }) {
|
|
292
|
+
if (terminal.agent !== "codex" ||
|
|
293
|
+
!sourceSession?.binding ||
|
|
294
|
+
observation.status !== "verified_absent") {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
const processUuid = sourceSession.binding.native_process.process_uuid;
|
|
298
|
+
const processBirth = sourceSession.binding.native_process.process_birth;
|
|
299
|
+
const workspace = terminal.terminalControl.currentPath;
|
|
300
|
+
const liveIncarnation = codexProcessIncarnationForPid(terminal.pid);
|
|
301
|
+
if (!processUuid ||
|
|
302
|
+
!processBirth ||
|
|
303
|
+
!workspace ||
|
|
304
|
+
!exactBoundCodexSendSource({
|
|
305
|
+
kind: "verified_empty",
|
|
306
|
+
sourceSession,
|
|
307
|
+
context: {
|
|
308
|
+
terminalId: terminal.conversationId,
|
|
309
|
+
terminalControl: terminal.terminalControl,
|
|
310
|
+
pid: terminal.pid,
|
|
311
|
+
workspace,
|
|
312
|
+
liveProcessUuid: liveIncarnation.processUuid,
|
|
313
|
+
liveProcessBirth: liveIncarnation.processBirth
|
|
314
|
+
}
|
|
315
|
+
})) {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
const expectedToken = verifiedEmptyCodexHandoffToken({
|
|
319
|
+
terminalId: terminal.conversationId,
|
|
320
|
+
terminalControl: terminal.terminalControl,
|
|
321
|
+
pid: terminal.pid,
|
|
322
|
+
workspace,
|
|
323
|
+
processUuid,
|
|
324
|
+
processBirth,
|
|
325
|
+
sourceSession
|
|
326
|
+
});
|
|
327
|
+
if (stringValue(options.expectedTerminalToken) !== expectedToken) {
|
|
328
|
+
throw new Error("verified-empty Codex handoff requires the fresh exact terminal token " +
|
|
329
|
+
"advertised by AKK list");
|
|
330
|
+
}
|
|
331
|
+
await assertVerifiedEmptyCodexHandoffBoundary({
|
|
332
|
+
options,
|
|
333
|
+
terminal,
|
|
334
|
+
sourceSession,
|
|
335
|
+
expectedSourceStatus: "bound",
|
|
336
|
+
requireNoDispatch: true
|
|
337
|
+
});
|
|
338
|
+
await assertNativeThreadHasExclusiveOwnership({
|
|
339
|
+
options,
|
|
340
|
+
agent: "codex",
|
|
341
|
+
currentPid: terminal.pid,
|
|
342
|
+
nativeThreadId: sourceSession.binding.native_thread_id,
|
|
343
|
+
storeDir: storeDirFromOptions(options),
|
|
344
|
+
terminalControl: terminal.terminalControl,
|
|
345
|
+
excludedManagedSessionId: sourceSession.session_id
|
|
346
|
+
});
|
|
347
|
+
const detachedAt = cliNow().toISOString();
|
|
348
|
+
const detached = saveManagedSession(storeDirFromOptions(options), {
|
|
349
|
+
...sourceSession,
|
|
350
|
+
status: "detached",
|
|
351
|
+
detached_at: detachedAt,
|
|
352
|
+
updated_at: detachedAt
|
|
353
|
+
}, {
|
|
354
|
+
expectedRevision: managedSessionRevision(sourceSession)
|
|
355
|
+
});
|
|
356
|
+
runtimeLog("info", "verified_empty_codex_source_detached", {
|
|
357
|
+
terminal_id: terminal.conversationId,
|
|
358
|
+
source_session_id: sourceSession.session_id,
|
|
359
|
+
native_thread_id: sourceSession.binding.native_thread_id,
|
|
360
|
+
process_uuid: processUuid,
|
|
361
|
+
process_birth: processBirth,
|
|
362
|
+
terminal_input_sent: false
|
|
363
|
+
});
|
|
364
|
+
return {
|
|
365
|
+
detached,
|
|
366
|
+
boundary: {
|
|
367
|
+
terminal,
|
|
368
|
+
detachedSourceSessionId: detached.session_id,
|
|
369
|
+
detachedSourceRevision: managedSessionRevision(detached),
|
|
370
|
+
detachedSourceBindingToken: managedSessionBindingToken(detached),
|
|
371
|
+
processUuid,
|
|
372
|
+
processBirth
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
async function assertVerifiedEmptyCodexTransportBoundary({ options, boundary, requireEmptyComposer }) {
|
|
377
|
+
const source = loadManagedSession(storeDirFromOptions(options), boundary.detachedSourceSessionId);
|
|
378
|
+
if (source.status !== "detached" ||
|
|
379
|
+
managedSessionRevision(source) !== boundary.detachedSourceRevision ||
|
|
380
|
+
managedSessionBindingToken(source) !==
|
|
381
|
+
boundary.detachedSourceBindingToken ||
|
|
382
|
+
source.binding?.native_process.process_uuid !== boundary.processUuid ||
|
|
383
|
+
source.binding.native_process.process_birth !== boundary.processBirth) {
|
|
384
|
+
throw new Error("verified-empty Codex source authority changed before text injection");
|
|
385
|
+
}
|
|
386
|
+
await assertVerifiedEmptyCodexHandoffBoundary({
|
|
387
|
+
options,
|
|
388
|
+
terminal: boundary.terminal,
|
|
389
|
+
sourceSession: source,
|
|
390
|
+
expectedSourceStatus: "detached",
|
|
391
|
+
requireNoDispatch: false,
|
|
392
|
+
requireEmptyComposer
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function deferredForegroundBoundaryAdapterPorts(options, storeDir) {
|
|
396
|
+
return {
|
|
397
|
+
processIncarnation: codexProcessIncarnationForPid,
|
|
398
|
+
inventory: (boundary) => inspectCodexOpenRootRolloutInventory({
|
|
399
|
+
options,
|
|
400
|
+
pid: boundary.terminal.pid,
|
|
401
|
+
cwd: boundary.terminal.terminalControl.currentPath
|
|
402
|
+
}),
|
|
403
|
+
nativeIdentity: (boundary) => observeCurrentNativeAgentSessionIdentity({
|
|
404
|
+
options,
|
|
405
|
+
agent: "codex",
|
|
406
|
+
pid: boundary.terminal.pid,
|
|
407
|
+
cwd: boundary.terminal.terminalControl.currentPath
|
|
408
|
+
}),
|
|
409
|
+
authority: ({ boundary, sourceAsBound, candidateInventory, expectedSourceStatus }) => observeDeferredCodexAuthority({
|
|
410
|
+
mode: expectedSourceStatus === "bound"
|
|
411
|
+
? "boundary_bound"
|
|
412
|
+
: "boundary_transitioning",
|
|
413
|
+
storeDir,
|
|
414
|
+
context: {
|
|
415
|
+
terminalId: boundary.terminal.conversationId,
|
|
416
|
+
terminalControl: boundary.terminal.terminalControl,
|
|
417
|
+
pid: boundary.terminal.pid,
|
|
418
|
+
workspace: boundary.terminal.terminalControl.currentPath,
|
|
419
|
+
liveProcessUuid: boundary.processUuid,
|
|
420
|
+
liveProcessBirth: boundary.processBirth
|
|
421
|
+
},
|
|
422
|
+
sourceSession: sourceAsBound,
|
|
423
|
+
candidateInventory,
|
|
424
|
+
abandonment: "never",
|
|
425
|
+
fixedSourceRolloutAuthority: boundary.sourceRolloutAuthority,
|
|
426
|
+
fixedDispatchSnapshot: boundary.previousDispatchSnapshot,
|
|
427
|
+
sourceRevision: boundary.sourceBoundRevision,
|
|
428
|
+
sourceBindingToken: boundary.sourceBoundBindingToken
|
|
429
|
+
}),
|
|
430
|
+
assertNoDispatch: (_scope, boundary) => terminalListCliFacade.assertTerminalIncarnationCanStartTurn(storeDir, boundary.terminal.terminalControl),
|
|
431
|
+
dispatchSnapshot: (boundary) => deferredCodexForegroundDispatchSnapshot(boundary.terminal.terminalControl),
|
|
432
|
+
status: async (boundary) => {
|
|
433
|
+
const status = await createTerminalAgentBridge(options).status("codex", boundary.terminal.terminalControl, {
|
|
434
|
+
runtime: terminalRuntimeForLiveIdentity({
|
|
435
|
+
terminal: boundary.terminal,
|
|
436
|
+
expectedEmptyNativeSession: boundary.candidateAcceptanceAnchor === undefined,
|
|
437
|
+
physicalOnly: boundary.candidateAcceptanceAnchor !== undefined
|
|
438
|
+
})
|
|
439
|
+
});
|
|
440
|
+
return {
|
|
441
|
+
reachable: status.reachable === true,
|
|
442
|
+
approvalBlocked: status.approval_state.blocked === true,
|
|
443
|
+
activityState: status.activity_state,
|
|
444
|
+
activityReason: status.activity_reason
|
|
445
|
+
};
|
|
446
|
+
},
|
|
447
|
+
assertComposerReady: (boundary) => assertCodexComposerReadyForAutomatedInput({
|
|
448
|
+
options,
|
|
449
|
+
terminalControl: boundary.terminal.terminalControl
|
|
450
|
+
}),
|
|
451
|
+
valuesMatch: (left, right) => JSON.stringify(left) === JSON.stringify(right)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
async function assertDeferredCodexForegroundBindingBoundary({ options, scope, boundary, expectedSourceStatus, requireNoDispatch, requireEmptyComposer }) {
|
|
455
|
+
const storeDir = storeDirFromOptions(options);
|
|
456
|
+
return assertDeferredForegroundBoundary({
|
|
457
|
+
scope,
|
|
458
|
+
storeDir,
|
|
459
|
+
boundary,
|
|
460
|
+
applicationBoundary: deferredForegroundBoundaryProjection(boundary),
|
|
461
|
+
expectedSourceStatus,
|
|
462
|
+
requireNoDispatch,
|
|
463
|
+
requireEmptyComposer,
|
|
464
|
+
ports: deferredForegroundBoundaryAdapterPorts(options, storeDir)
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
async function prepareDeferredCodexForegroundBinding({ options, scope, terminal, sourceSession, observation, candidateInventory, requestText, allowImplicitFreshAuthority = false }) {
|
|
468
|
+
const storeDir = storeDirFromOptions(options);
|
|
469
|
+
const prepared = await prepareDeferredForegroundBinding({
|
|
470
|
+
scope,
|
|
471
|
+
terminal: projectDeferredForegroundTerminalFacts(terminal),
|
|
472
|
+
sourceSession,
|
|
473
|
+
nativeIdentityVerifiedAbsent: observation.status === "verified_absent",
|
|
474
|
+
candidateInventory,
|
|
475
|
+
requestText,
|
|
476
|
+
expectedTerminalToken: stringValue(options.expectedTerminalToken),
|
|
477
|
+
allowImplicitFreshAuthority
|
|
478
|
+
}, {
|
|
479
|
+
authority: {
|
|
480
|
+
processIncarnation: codexProcessIncarnationForPid,
|
|
481
|
+
observeFresh: ({ sourceSession, candidateInventory, liveIncarnation }) => observeDeferredCodexAuthority({
|
|
482
|
+
mode: "prepare",
|
|
483
|
+
storeDir,
|
|
484
|
+
context: {
|
|
485
|
+
terminalId: terminal.conversationId,
|
|
486
|
+
terminalControl: terminal.terminalControl,
|
|
487
|
+
pid: terminal.pid,
|
|
488
|
+
workspace: terminal.terminalControl.currentPath,
|
|
489
|
+
liveProcessUuid: liveIncarnation.processUuid,
|
|
490
|
+
liveProcessBirth: liveIncarnation.processBirth
|
|
491
|
+
},
|
|
492
|
+
sourceSession,
|
|
493
|
+
candidateInventory,
|
|
494
|
+
abandonment: "missing_inventory_rollout"
|
|
495
|
+
}),
|
|
496
|
+
revalidate: async (activeScope, boundary) => {
|
|
497
|
+
await assertDeferredCodexForegroundBindingBoundary({
|
|
498
|
+
options,
|
|
499
|
+
scope: activeScope,
|
|
500
|
+
boundary: deferredForegroundConcreteBoundary(boundary, terminal),
|
|
501
|
+
expectedSourceStatus: "bound",
|
|
502
|
+
requireNoDispatch: true,
|
|
503
|
+
requireEmptyComposer: true
|
|
504
|
+
});
|
|
505
|
+
},
|
|
506
|
+
assertExclusive: (_activeScope, boundary, nativeThreadId, excludedManagedSessionId) => assertNativeThreadHasExclusiveOwnership({
|
|
507
|
+
options,
|
|
508
|
+
agent: "codex",
|
|
509
|
+
currentPid: boundary.terminal.pid,
|
|
510
|
+
nativeThreadId,
|
|
511
|
+
storeDir,
|
|
512
|
+
terminalControl: terminal.terminalControl,
|
|
513
|
+
excludedManagedSessionId
|
|
514
|
+
}),
|
|
515
|
+
candidateInventoryUnclaimed: ({ sourceSession, inventory, includeDetached }) => codexCandidateInventoryHasNoOtherManagedClaim({
|
|
516
|
+
storeDir,
|
|
517
|
+
inventory,
|
|
518
|
+
sourceSessionId: sourceSession.session_id,
|
|
519
|
+
includeDetached
|
|
520
|
+
}),
|
|
521
|
+
abandonmentStillFresh: ({ sourceSession, sourceTurnHistory, sourceAbandonmentFingerprint, dispatchSnapshot }) => {
|
|
522
|
+
const freshHistory = deferredCandidateSourceTurnHistory(storeDir, sourceSession);
|
|
523
|
+
const freshFingerprint = freshHistory
|
|
524
|
+
? explicitlyAbandonedCandidateSourceFingerprint({
|
|
525
|
+
storeDir,
|
|
526
|
+
session: sourceSession,
|
|
527
|
+
sourceTurnHistory: freshHistory,
|
|
528
|
+
dispatchSnapshot
|
|
529
|
+
})
|
|
530
|
+
: undefined;
|
|
531
|
+
return JSON.stringify(freshHistory) ===
|
|
532
|
+
JSON.stringify(sourceTurnHistory) &&
|
|
533
|
+
freshFingerprint === sourceAbandonmentFingerprint;
|
|
534
|
+
},
|
|
535
|
+
transferMatchesTerminal: (transfer) => deferredForegroundTransferMatchesTerminal(transfer, terminal)
|
|
536
|
+
},
|
|
537
|
+
identity: {
|
|
538
|
+
targetSessionId: createManagedSessionId,
|
|
539
|
+
transferId: createDeferredForegroundTransferId,
|
|
540
|
+
captureCandidateAnchor: (inventory, now) => captureCodexCandidateSetRolloutAcceptanceAnchor({ inventory, now }),
|
|
541
|
+
bindingToken: ({ sourceSession, authority, candidateInventory }) => deferredCodexForegroundBindingToken({
|
|
542
|
+
terminalId: terminal.conversationId,
|
|
543
|
+
terminalControl: terminal.terminalControl,
|
|
544
|
+
pid: terminal.pid,
|
|
545
|
+
workspace: required(terminal.terminalControl.currentPath, "deferred Codex terminal workspace is unavailable"),
|
|
546
|
+
processUuid: sourceSession.binding.native_process.process_uuid,
|
|
547
|
+
processBirth: sourceSession.binding.native_process.process_birth,
|
|
548
|
+
sourceSession,
|
|
549
|
+
dispatchSnapshot: authority.dispatchSnapshot,
|
|
550
|
+
candidateInventory,
|
|
551
|
+
sourceTurnHistory: authority.sourceTurnHistory,
|
|
552
|
+
sourceRolloutAuthority: authority.sourceRolloutAuthority,
|
|
553
|
+
sourceAbandonmentFingerprint: authority.sourceAbandonmentFingerprint
|
|
554
|
+
}),
|
|
555
|
+
requestHash: (text) => required(terminalBridgeRequestFingerprint(terminalSubmissionPayload(text)), "deferred foreground request hash is unavailable")
|
|
556
|
+
},
|
|
557
|
+
runtime: {
|
|
558
|
+
now: cliNow,
|
|
559
|
+
pid: cliPid,
|
|
560
|
+
log: runtimeLog
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
return prepared
|
|
564
|
+
? deferredForegroundConcreteBoundary(prepared, terminal)
|
|
565
|
+
: undefined;
|
|
566
|
+
}
|
|
567
|
+
function deferredForegroundApplication(options, terminal) {
|
|
568
|
+
const concreteBoundary = (boundary) => deferredForegroundConcreteBoundary(boundary, required(terminal, "deferred foreground terminal authority is unavailable"));
|
|
569
|
+
return new DeferredForegroundApplicationService({
|
|
570
|
+
authority: {
|
|
571
|
+
verifyReservedSource: (scope, boundary) => assertDeferredCodexForegroundBindingBoundary({
|
|
572
|
+
options,
|
|
573
|
+
scope,
|
|
574
|
+
boundary: concreteBoundary(boundary),
|
|
575
|
+
expectedSourceStatus: "bound",
|
|
576
|
+
requireNoDispatch: true,
|
|
577
|
+
requireEmptyComposer: true
|
|
578
|
+
}),
|
|
579
|
+
assertExclusive: (_scope, boundary, request) => assertNativeThreadHasExclusiveOwnership({
|
|
580
|
+
options,
|
|
581
|
+
agent: "codex",
|
|
582
|
+
currentPid: request.processPid,
|
|
583
|
+
nativeThreadId: request.nativeThreadId,
|
|
584
|
+
storeDir: storeDirFromOptions(options),
|
|
585
|
+
terminalControl: concreteBoundary(boundary).terminal.terminalControl,
|
|
586
|
+
excludedManagedSessionId: request.excludedManagedSessionId,
|
|
587
|
+
allowedManagedSessionIds: request.allowedManagedSessionIds
|
|
588
|
+
}),
|
|
589
|
+
assertFrozenPredecessor: (_scope, boundary, transfer) => assertFrozenExplicitlyAbandonedPredecessorAuthority({
|
|
590
|
+
storeDir: storeDirFromOptions(options),
|
|
591
|
+
transfer,
|
|
592
|
+
terminalControl: concreteBoundary(boundary).terminal.terminalControl
|
|
593
|
+
}),
|
|
594
|
+
valuesMatch: (left, right) => JSON.stringify(left) === JSON.stringify(right)
|
|
595
|
+
},
|
|
596
|
+
clock: { now: cliNow },
|
|
597
|
+
runtime: {
|
|
598
|
+
crashAt: (point) => {
|
|
599
|
+
const key = {
|
|
600
|
+
source_session_reserved: "AKK_TEST_EXIT_AFTER_DEFERRED_SOURCE_SESSION_RESERVED",
|
|
601
|
+
source_reserved: "AKK_TEST_EXIT_AFTER_DEFERRED_SOURCE_RESERVED",
|
|
602
|
+
target_prepared: "AKK_TEST_EXIT_AFTER_DEFERRED_TARGET_PREPARED",
|
|
603
|
+
source_scrubbed: "AKK_TEST_EXIT_AFTER_DEFERRED_SOURCE_SCRUBBED",
|
|
604
|
+
target_accepted: "AKK_TEST_EXIT_AFTER_DEFERRED_TARGET_ACCEPTED",
|
|
605
|
+
committed: "AKK_TEST_EXIT_AFTER_DEFERRED_COMMITTED",
|
|
606
|
+
source_detached: "AKK_TEST_EXIT_AFTER_DEFERRED_SOURCE_DETACHED",
|
|
607
|
+
target_bound: "AKK_TEST_EXIT_AFTER_DEFERRED_TARGET_BOUND"
|
|
608
|
+
}[point];
|
|
609
|
+
if (cliEnv()[key] === "1")
|
|
610
|
+
cliExit(86);
|
|
611
|
+
},
|
|
612
|
+
errorReceipt: (reason) => JSON.stringify(textSummary(reason)),
|
|
613
|
+
summary: textSummary,
|
|
614
|
+
log: runtimeLog
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function deferredForegroundRecoveryAdapterPorts() {
|
|
619
|
+
return {
|
|
620
|
+
native: {
|
|
621
|
+
processIncarnation: codexProcessIncarnationForPid,
|
|
622
|
+
inventory: inspectCodexOpenRootRolloutInventory,
|
|
623
|
+
identity: resolveCurrentNativeAgentSessionIdentity
|
|
624
|
+
},
|
|
625
|
+
turn: {
|
|
626
|
+
terminalControl: terminalControlFromTakeover,
|
|
627
|
+
storeDir: managedSessionStoreDirForConversation,
|
|
628
|
+
withIdentity: withNativeAgentSessionIdentity,
|
|
629
|
+
withSubmission: withTerminalBridgeSubmission
|
|
630
|
+
},
|
|
631
|
+
ledger: {
|
|
632
|
+
load: loadTerminalBridgeDispatchLedger,
|
|
633
|
+
save: saveTerminalBridgeDispatchLedger,
|
|
634
|
+
matchesControl: terminalDispatchRecordMatchesControl,
|
|
635
|
+
bindingFields: terminalBindingLedgerFields,
|
|
636
|
+
previousSnapshotMatches: deferredCodexPreviousDispatchSnapshotMatches
|
|
637
|
+
},
|
|
638
|
+
authority: {
|
|
639
|
+
assertFrozen: assertFrozenExplicitlyAbandonedPredecessorAuthority,
|
|
640
|
+
assertTurnIdentity: assertNativeAgentIdentityForTurn
|
|
641
|
+
},
|
|
642
|
+
application: {
|
|
643
|
+
abortBeforeInput: ({ options, scope, boundary, reason, terminalInputNotStartedAt }) => deferredForegroundApplication(options, boundary.terminal).abortBeforeInput({
|
|
644
|
+
scope,
|
|
645
|
+
boundary: deferredForegroundBoundaryProjection(boundary),
|
|
646
|
+
reason,
|
|
647
|
+
terminalInputNotStartedAt
|
|
648
|
+
}),
|
|
649
|
+
commit: ({ options, scope, boundary, identity, acceptedAt }) => deferredForegroundApplication(options, boundary.terminal).commit({
|
|
650
|
+
scope,
|
|
651
|
+
boundary: deferredForegroundBoundaryProjection(boundary),
|
|
652
|
+
identity,
|
|
653
|
+
acceptedAt
|
|
654
|
+
})
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function observedHandoffAuthorityToken({ terminal, identity, sourceSession, target }) {
|
|
659
|
+
const exact = exactLifecycleProcessIdentity(terminal, identity);
|
|
660
|
+
return projectObservedHandoffAuthorityToken({
|
|
661
|
+
terminalId: terminal.conversationId,
|
|
662
|
+
terminalControl: terminal.terminalControl,
|
|
663
|
+
agent: terminal.agent,
|
|
664
|
+
pid: terminal.pid,
|
|
665
|
+
workspace: terminal.terminalControl.currentPath ?? cliCwd(),
|
|
666
|
+
identity: exact,
|
|
667
|
+
sourceSession,
|
|
668
|
+
target
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
function activeTurnHandoffDecisionToken({ handoffToken, turn, ledger }) {
|
|
672
|
+
const takeover = isRecord(turn.native_session_takeover)
|
|
673
|
+
? turn.native_session_takeover
|
|
674
|
+
: undefined;
|
|
675
|
+
const submission = terminalBridgeSubmission(turn);
|
|
676
|
+
return projectActiveTurnHandoffDecisionToken({
|
|
677
|
+
handoffToken,
|
|
678
|
+
sessionId: sessionIdForConversation(turn),
|
|
679
|
+
turnId: turnIdForConversation(turn),
|
|
680
|
+
turnStatus: turn.status,
|
|
681
|
+
turnUpdatedAt: turn.updated_at ?? null,
|
|
682
|
+
currentMessageId: stringValue(takeover?.terminal_bridge_message_id) ??
|
|
683
|
+
stringValue(submission?.message_id) ??
|
|
684
|
+
null,
|
|
685
|
+
ledgerGenerationId: stringValue(ledger?.generation_id) ?? null,
|
|
686
|
+
ledgerMessageId: stringValue(ledger?.message_id) ?? null,
|
|
687
|
+
ledgerStatus: stringValue(ledger?.status) ?? null
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
function assertExpectedHandoffTokenUsesExactTerminalSelector({ options, terminal }) {
|
|
691
|
+
if (!stringValue(options.expectedTerminalToken)) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const supplied = originalExpectedTerminalSelector.has(options)
|
|
695
|
+
? originalExpectedTerminalSelector.get(options)
|
|
696
|
+
: stringValue(options.session ?? options.conversation ?? options.conversationId)?.trim();
|
|
697
|
+
if (supplied !== terminal.conversationId) {
|
|
698
|
+
throw new Error("--expected-terminal-token is valid only with the exact full terminal " +
|
|
699
|
+
"conversation selector advertised by AKK list");
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async function observedExternalHandoffIdentity({ options, terminal, sourceSession, resolvedIdentity, requireSafeTerminal = true }) {
|
|
703
|
+
const bridge = createTerminalAgentBridge(options);
|
|
704
|
+
const status = await bridge.status(terminal.agent, terminal.terminalControl, { runtime: terminalRuntimeForLiveIdentity({ terminal, physicalOnly: true }) });
|
|
705
|
+
if (requireSafeTerminal) {
|
|
706
|
+
assertSafeTerminalSend(terminal.agent, status);
|
|
707
|
+
}
|
|
708
|
+
if (terminal.agent !== "codex") {
|
|
709
|
+
return { identity: resolvedIdentity, status };
|
|
710
|
+
}
|
|
711
|
+
const sourceBinding = sourceSession.binding;
|
|
712
|
+
const statusCard = terminal.adapter.observeThreadLifecycle?.({
|
|
713
|
+
operation: { kind: "new_thread" },
|
|
714
|
+
phase: "before",
|
|
715
|
+
screen: status.screen.excerpt ?? ""
|
|
716
|
+
});
|
|
717
|
+
const statusCardId = statusCard?.status === "observed" &&
|
|
718
|
+
isExactNativeThreadId(statusCard.nativeThreadId)
|
|
719
|
+
? statusCard.nativeThreadId.toLowerCase()
|
|
720
|
+
: undefined;
|
|
721
|
+
const sourceId = sourceBinding?.native_thread_id?.toLowerCase();
|
|
722
|
+
if (statusCardId && sourceId && statusCardId !== sourceId) {
|
|
723
|
+
const processUuid = sourceBinding?.native_process.process_uuid;
|
|
724
|
+
const processBirth = sourceBinding?.native_process.process_birth;
|
|
725
|
+
if (!processUuid || !processBirth) {
|
|
726
|
+
return { identity: undefined, status };
|
|
727
|
+
}
|
|
728
|
+
const resolvedMatchesStatus = resolvedIdentity?.sessionId.toLowerCase() === statusCardId;
|
|
729
|
+
return {
|
|
730
|
+
identity: {
|
|
731
|
+
sessionId: statusCardId,
|
|
732
|
+
processUuid,
|
|
733
|
+
processBirth,
|
|
734
|
+
rollout: resolvedMatchesStatus ? resolvedIdentity?.rollout : undefined,
|
|
735
|
+
evidence: resolvedMatchesStatus
|
|
736
|
+
? `${resolvedIdentity?.evidence ?? "native_thread_boundary"}+codex_status_card`
|
|
737
|
+
: statusCard?.evidence ?? "codex_status_card"
|
|
738
|
+
},
|
|
739
|
+
status
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
return { identity: resolvedIdentity, status };
|
|
743
|
+
}
|
|
744
|
+
function observedHandoffTargetResolution({ storeDir, agent, workspace, nativeThreadId, sourceSessionId }) {
|
|
745
|
+
const matches = listManagedSessions(storeDir).filter((session) => session.session_id !== sourceSessionId &&
|
|
746
|
+
session.agent === agent &&
|
|
747
|
+
session.binding?.native_thread_id?.toLowerCase() === nativeThreadId &&
|
|
748
|
+
path.resolve(session.workspace) === path.resolve(workspace));
|
|
749
|
+
if (matches.length > 1) {
|
|
750
|
+
return {
|
|
751
|
+
status: "blocked",
|
|
752
|
+
reason: `native thread ${nativeThreadId} is claimed by multiple managed Sessions`
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
const target = matches[0];
|
|
756
|
+
if (!target) {
|
|
757
|
+
return { status: "eligible", snapshot: { state: "absent" } };
|
|
758
|
+
}
|
|
759
|
+
if (!target.binding ||
|
|
760
|
+
target.status !== "detached" ||
|
|
761
|
+
terminalListCliFacade.managedSessionHasUnresolvedNativeTransition(storeDir, target)) {
|
|
762
|
+
return {
|
|
763
|
+
status: "blocked",
|
|
764
|
+
reason: `managed Session ${target.session_id} cannot be adopted from ` +
|
|
765
|
+
`${target.status} state or while its lifecycle is unresolved`
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
assertManagedSessionCanStartTurn(managedTurnsForSession(storeDir, target.session_id));
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
return {
|
|
773
|
+
status: "blocked",
|
|
774
|
+
reason: `managed Session ${target.session_id} has unresolved work: ` +
|
|
775
|
+
`${error instanceof Error ? error.message : String(error)}`
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
status: "eligible",
|
|
780
|
+
session: target,
|
|
781
|
+
snapshot: {
|
|
782
|
+
state: "detached",
|
|
783
|
+
session_id: target.session_id,
|
|
784
|
+
revision: managedSessionRevision(target),
|
|
785
|
+
status: "detached",
|
|
786
|
+
binding_token: managedSessionBindingToken(target)
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
async function maybeAdoptObservedExternalThread({ options, terminal, sourceSession, resolvedIdentity, storeDir }) {
|
|
791
|
+
if (!sourceSession?.binding) {
|
|
792
|
+
return { identity: resolvedIdentity, adopted: false };
|
|
793
|
+
}
|
|
794
|
+
const observed = await observedExternalHandoffIdentity({
|
|
795
|
+
options,
|
|
796
|
+
terminal,
|
|
797
|
+
sourceSession,
|
|
798
|
+
resolvedIdentity,
|
|
799
|
+
// Snapshot-bound terminal actions apply their own stricter boundary
|
|
800
|
+
// before any input. In particular, a post-/clear Codex composer can be
|
|
801
|
+
// safely empty while the passive activity classifier is still unknown;
|
|
802
|
+
// do not force the unrelated external-handoff idle check before the
|
|
803
|
+
// deferred candidate boundary gets a chance to revalidate it.
|
|
804
|
+
requireSafeTerminal: !stringValue(options.expectedTerminalToken)
|
|
805
|
+
});
|
|
806
|
+
const identity = observed.identity;
|
|
807
|
+
const conflictKind = managedBindingConflictKindForResolvedTerminal({
|
|
808
|
+
storeDir,
|
|
809
|
+
session: sourceSession,
|
|
810
|
+
terminal,
|
|
811
|
+
identity
|
|
812
|
+
});
|
|
813
|
+
if (conflictKind !== "live_external_thread_change") {
|
|
814
|
+
return { identity, adopted: false };
|
|
815
|
+
}
|
|
816
|
+
assertTerminalLifecycleReady({
|
|
817
|
+
options,
|
|
818
|
+
terminal,
|
|
819
|
+
terminalStatus: observed.status
|
|
820
|
+
});
|
|
821
|
+
if (!identity || !isExactNativeThreadId(identity.sessionId)) {
|
|
822
|
+
throw new Error("the externally selected native thread has no exact supported identity");
|
|
823
|
+
}
|
|
824
|
+
if (terminalListCliFacade.managedSessionHasUnresolvedNativeTransition(storeDir, sourceSession)) {
|
|
825
|
+
throw new Error(`managed Session ${sourceSession.session_id} has an unresolved native-thread transition`);
|
|
826
|
+
}
|
|
827
|
+
assertManagedSessionCanStartTurn(managedTurnsForSession(storeDir, sourceSession.session_id));
|
|
828
|
+
if (terminal.agent === "codex"
|
|
829
|
+
? !codexComposerEmpty(observed.status.screen.excerpt)
|
|
830
|
+
: !claudeComposerEmpty(observed.status.screen.excerpt)) {
|
|
831
|
+
throw new Error("external handoff adoption requires an exact empty idle composer");
|
|
832
|
+
}
|
|
833
|
+
if (terminal.agent === "codex") {
|
|
834
|
+
await assertCodexComposerReadyForAutomatedInput({
|
|
835
|
+
options,
|
|
836
|
+
terminalControl: terminal.terminalControl
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
const targetNativeThreadId = identity.sessionId.toLowerCase();
|
|
840
|
+
const targetResolution = observedHandoffTargetResolution({
|
|
841
|
+
storeDir,
|
|
842
|
+
agent: terminal.agent,
|
|
843
|
+
workspace: terminal.terminalControl.currentPath ?? cliCwd(),
|
|
844
|
+
nativeThreadId: targetNativeThreadId,
|
|
845
|
+
sourceSessionId: sourceSession.session_id
|
|
846
|
+
});
|
|
847
|
+
const expectedTerminalToken = stringValue(options.expectedTerminalToken);
|
|
848
|
+
if (targetResolution.status === "blocked") {
|
|
849
|
+
if (expectedTerminalToken) {
|
|
850
|
+
throw new Error("live source or target Session snapshot changed after the handoff was " +
|
|
851
|
+
"listed; refresh AKK list");
|
|
852
|
+
}
|
|
853
|
+
throw new Error(targetResolution.reason);
|
|
854
|
+
}
|
|
855
|
+
const freshHandoffToken = observedHandoffAuthorityToken({
|
|
856
|
+
terminal,
|
|
857
|
+
identity,
|
|
858
|
+
sourceSession,
|
|
859
|
+
target: targetResolution.snapshot
|
|
860
|
+
});
|
|
861
|
+
if (expectedTerminalToken &&
|
|
862
|
+
expectedTerminalToken !== freshHandoffToken) {
|
|
863
|
+
throw new Error("live source, target, or terminal identity changed after the handoff " +
|
|
864
|
+
"was listed; refresh AKK list");
|
|
865
|
+
}
|
|
866
|
+
const targetSession = targetResolution.session;
|
|
867
|
+
await assertNativeThreadHasExclusiveOwnership({
|
|
868
|
+
options,
|
|
869
|
+
agent: terminal.agent,
|
|
870
|
+
currentPid: terminal.pid,
|
|
871
|
+
nativeThreadId: targetNativeThreadId,
|
|
872
|
+
storeDir,
|
|
873
|
+
terminalControl: terminal.terminalControl,
|
|
874
|
+
excludedManagedSessionId: targetSession?.session_id
|
|
875
|
+
});
|
|
876
|
+
const adapterVersion = required(stringValue(agentVersionForRunningProcess(terminal.agent, terminal.pid, options)), "external handoff adoption requires the exact running agent version");
|
|
877
|
+
const capability = terminal.adapter.probeThreadLifecycle?.(adapterVersion);
|
|
878
|
+
if (capability?.status !== "supported") {
|
|
879
|
+
throw new Error(capability?.reason ?? "external handoff adoption is unsupported for this agent version");
|
|
880
|
+
}
|
|
881
|
+
const now = cliNow();
|
|
882
|
+
const sourceBinding = sourceSession.binding;
|
|
883
|
+
const targetSessionId = targetSession?.session_id ?? createManagedSessionId(now);
|
|
884
|
+
const transitionId = createNativeThreadTransitionId();
|
|
885
|
+
const exactIdentity = exactLifecycleProcessIdentity(terminal, identity);
|
|
886
|
+
const nextBinding = terminalBindingFrom({
|
|
887
|
+
terminalId: terminal.conversationId,
|
|
888
|
+
terminalControl: terminal.terminalControl,
|
|
889
|
+
pid: terminal.pid,
|
|
890
|
+
nativeThreadId: targetNativeThreadId,
|
|
891
|
+
processUuid: exactIdentity.processUuid,
|
|
892
|
+
processBirth: exactIdentity.processBirth,
|
|
893
|
+
rollout: exactIdentity.rollout,
|
|
894
|
+
evidence: `${exactIdentity.evidence}+human_observed`,
|
|
895
|
+
generation: (targetSession?.binding?.generation ?? 0) + 1,
|
|
896
|
+
now
|
|
897
|
+
});
|
|
898
|
+
const previousLedger = loadTerminalBridgeDispatchLedger(terminal.terminalControl);
|
|
899
|
+
let transition = {
|
|
900
|
+
schema: "agent-knock-knock/native-thread-transition",
|
|
901
|
+
version: 1,
|
|
902
|
+
transition_id: transitionId,
|
|
903
|
+
operation: "adopt_external_thread",
|
|
904
|
+
origin: "human_observed",
|
|
905
|
+
terminal_input_sent: false,
|
|
906
|
+
status: "prepared",
|
|
907
|
+
terminal_id: terminal.conversationId,
|
|
908
|
+
agent: terminal.agent,
|
|
909
|
+
workspace: terminal.terminalControl.currentPath ?? cliCwd(),
|
|
910
|
+
source_session_id: sourceSession.session_id,
|
|
911
|
+
source_expected_revision: managedSessionRevision(sourceSession),
|
|
912
|
+
source_previous_last_transition_id: sourceSession.last_transition_id,
|
|
913
|
+
target_session_id: targetSessionId,
|
|
914
|
+
target_expected_revision: targetSession
|
|
915
|
+
? managedSessionRevision(targetSession)
|
|
916
|
+
: null,
|
|
917
|
+
target_native_thread_id: targetNativeThreadId,
|
|
918
|
+
before_native_thread_id: sourceBinding.native_thread_id,
|
|
919
|
+
before_process_uuid: sourceBinding.native_process.process_uuid,
|
|
920
|
+
before_process_started_at: exactIdentity.processStartedAt,
|
|
921
|
+
before_process_birth: sourceBinding.native_process.process_birth,
|
|
922
|
+
before_process_rollout: sourceBinding.native_process.rollout,
|
|
923
|
+
before_binding: sourceBinding,
|
|
924
|
+
adapter_version: adapterVersion,
|
|
925
|
+
command_fingerprint: HUMAN_OBSERVED_HANDOFF_FINGERPRINT,
|
|
926
|
+
dispatcher_pid: cliPid(),
|
|
927
|
+
prepared_at: now.toISOString()
|
|
928
|
+
};
|
|
929
|
+
transition = saveNativeThreadTransition(storeDir, transition, {
|
|
930
|
+
expectedRevision: null
|
|
931
|
+
});
|
|
932
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_HANDOFF_TRANSITION_BEFORE_LEDGER === "1") {
|
|
933
|
+
cliExit(88);
|
|
934
|
+
}
|
|
935
|
+
saveLifecycleTerminalDispatchLedger(terminal.terminalControl, lifecycleLedger(transition, storeDir, { phase: "prepared", previous: previousLedger, targetNativeThreadId }), { expectedTransitionId: null });
|
|
936
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_LIFECYCLE_PREPARED === "1") {
|
|
937
|
+
cliExit(86);
|
|
938
|
+
}
|
|
939
|
+
const sourceTransitioning = saveManagedSession(storeDir, {
|
|
940
|
+
...sourceSession,
|
|
941
|
+
status: "transitioning",
|
|
942
|
+
last_transition_id: transitionId,
|
|
943
|
+
updated_at: now.toISOString()
|
|
944
|
+
}, { expectedRevision: managedSessionRevision(sourceSession) });
|
|
945
|
+
try {
|
|
946
|
+
const reObserved = await observedExternalHandoffIdentity({
|
|
947
|
+
options,
|
|
948
|
+
terminal,
|
|
949
|
+
sourceSession: sourceTransitioning,
|
|
950
|
+
resolvedIdentity: await resolveCurrentNativeAgentSessionIdentity({
|
|
951
|
+
options,
|
|
952
|
+
agent: terminal.agent,
|
|
953
|
+
pid: terminal.pid,
|
|
954
|
+
cwd: terminal.terminalControl.currentPath,
|
|
955
|
+
preferredSessionId: targetNativeThreadId,
|
|
956
|
+
allowedCompanionIdentity: codexIdentityFence({
|
|
957
|
+
sessionId: sourceBinding.native_thread_id,
|
|
958
|
+
processUuid: sourceBinding.native_process.process_uuid,
|
|
959
|
+
processBirth: sourceBinding.native_process.process_birth,
|
|
960
|
+
rollout: sourceBinding.native_process.rollout,
|
|
961
|
+
evidence: sourceBinding.native_process.evidence
|
|
962
|
+
}),
|
|
963
|
+
allowedAdditionalIdentities: []
|
|
964
|
+
})
|
|
965
|
+
});
|
|
966
|
+
const reObservedExact = reObserved.identity
|
|
967
|
+
? exactLifecycleProcessIdentity(terminal, reObserved.identity)
|
|
968
|
+
: undefined;
|
|
969
|
+
if (reObservedExact?.sessionId.toLowerCase() !== targetNativeThreadId ||
|
|
970
|
+
reObservedExact.processUuid !== nextBinding.native_process.process_uuid ||
|
|
971
|
+
reObservedExact.processBirth !== nextBinding.native_process.process_birth ||
|
|
972
|
+
JSON.stringify(reObservedExact.rollout ?? null) !==
|
|
973
|
+
JSON.stringify(nextBinding.native_process.rollout ?? null)) {
|
|
974
|
+
throw new Error("live native thread changed during external handoff adoption");
|
|
975
|
+
}
|
|
976
|
+
await assertNativeThreadHasExclusiveOwnership({
|
|
977
|
+
options,
|
|
978
|
+
agent: terminal.agent,
|
|
979
|
+
currentPid: terminal.pid,
|
|
980
|
+
nativeThreadId: targetNativeThreadId,
|
|
981
|
+
storeDir,
|
|
982
|
+
terminalControl: terminal.terminalControl,
|
|
983
|
+
excludedManagedSessionId: targetSession?.session_id
|
|
984
|
+
});
|
|
985
|
+
transition = saveNativeThreadTransition(storeDir, {
|
|
986
|
+
...transition,
|
|
987
|
+
status: "verified",
|
|
988
|
+
after_binding: nextBinding,
|
|
989
|
+
verified_at: cliNow().toISOString()
|
|
990
|
+
}, { expectedRevision: nativeThreadTransitionRevision(transition) });
|
|
991
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_HANDOFF_VERIFIED_TRANSITION_BEFORE_LEDGER ===
|
|
992
|
+
"1") {
|
|
993
|
+
cliExit(89);
|
|
994
|
+
}
|
|
995
|
+
saveLifecycleTerminalDispatchLedger(terminal.terminalControl, lifecycleLedger(transition, storeDir, { phase: "verified", binding: sourceBinding }), {
|
|
996
|
+
expectedTransitionId: transitionId,
|
|
997
|
+
expectedStatus: "prepared"
|
|
998
|
+
});
|
|
999
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_LIFECYCLE_VERIFIED === "1") {
|
|
1000
|
+
cliExit(87);
|
|
1001
|
+
}
|
|
1002
|
+
const committedTarget = commitVerifiedLifecycleTransition(storeDir, transition, cliNow().toISOString());
|
|
1003
|
+
transition = saveNativeThreadTransition(storeDir, {
|
|
1004
|
+
...transition,
|
|
1005
|
+
status: "committed",
|
|
1006
|
+
committed_at: cliNow().toISOString()
|
|
1007
|
+
}, { expectedRevision: nativeThreadTransitionRevision(transition) });
|
|
1008
|
+
saveLifecycleTerminalDispatchLedger(terminal.terminalControl, lifecycleLedger(transition, storeDir, {
|
|
1009
|
+
phase: "resolved_with_binding", at: cliNow().toISOString(),
|
|
1010
|
+
binding: committedTarget.binding,
|
|
1011
|
+
reason: "verified human-observed native thread handoff committed"
|
|
1012
|
+
}), {
|
|
1013
|
+
expectedTransitionId: transitionId,
|
|
1014
|
+
expectedStatus: "verified"
|
|
1015
|
+
});
|
|
1016
|
+
runtimeLog("info", "human_observed_handoff_adopted", {
|
|
1017
|
+
transition_id: transitionId,
|
|
1018
|
+
terminal_id: terminal.conversationId,
|
|
1019
|
+
source_session_id: sourceSession.session_id,
|
|
1020
|
+
target_session_id: committedTarget.session_id,
|
|
1021
|
+
native_thread_id: targetNativeThreadId,
|
|
1022
|
+
terminal_input_sent: false
|
|
1023
|
+
});
|
|
1024
|
+
return {
|
|
1025
|
+
session: committedTarget,
|
|
1026
|
+
identity: exactIdentity,
|
|
1027
|
+
transition,
|
|
1028
|
+
adopted: true
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
catch (error) {
|
|
1032
|
+
const failedAt = cliNow().toISOString();
|
|
1033
|
+
const durable = loadNativeThreadTransition(storeDir, transitionId);
|
|
1034
|
+
if (durable.status === "verified" || durable.status === "committed") {
|
|
1035
|
+
throw error;
|
|
1036
|
+
}
|
|
1037
|
+
const uncertain = saveNativeThreadTransition(storeDir, {
|
|
1038
|
+
...durable,
|
|
1039
|
+
status: "uncertain",
|
|
1040
|
+
uncertain_at: failedAt,
|
|
1041
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1042
|
+
do_not_retry: true
|
|
1043
|
+
}, { expectedRevision: nativeThreadTransitionRevision(durable) });
|
|
1044
|
+
saveManagedSession(storeDir, {
|
|
1045
|
+
...sourceTransitioning,
|
|
1046
|
+
status: "quarantined",
|
|
1047
|
+
quarantine_reason: "human-observed handoff could not be revalidated",
|
|
1048
|
+
updated_at: failedAt
|
|
1049
|
+
}, { expectedRevision: managedSessionRevision(sourceTransitioning) });
|
|
1050
|
+
saveLifecycleTerminalDispatchLedger(terminal.terminalControl, lifecycleLedger(uncertain, storeDir, {
|
|
1051
|
+
phase: "uncertain", at: failedAt,
|
|
1052
|
+
reason: "human-observed handoff could not be revalidated"
|
|
1053
|
+
}), { expectedTransitionId: transitionId });
|
|
1054
|
+
throw error;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
async function assertObservedHandoffTransportBoundary({ options, terminal, transition, requireEmptyComposer }) {
|
|
1058
|
+
const storeDir = storeDirFromOptions(options);
|
|
1059
|
+
const durable = loadNativeThreadTransition(storeDir, transition.transition_id);
|
|
1060
|
+
handoffApplication.assertAuthority([
|
|
1061
|
+
() => durable.operation === "adopt_external_thread",
|
|
1062
|
+
() => durable.origin === "human_observed",
|
|
1063
|
+
() => durable.terminal_input_sent === false,
|
|
1064
|
+
() => durable.status === "committed",
|
|
1065
|
+
() => Boolean(durable.source_session_id),
|
|
1066
|
+
() => durable.before_binding !== undefined,
|
|
1067
|
+
() => durable.after_binding !== undefined,
|
|
1068
|
+
() => JSON.stringify(durable.after_binding) ===
|
|
1069
|
+
JSON.stringify(transition.after_binding)
|
|
1070
|
+
], "human-observed handoff changed before terminal transport");
|
|
1071
|
+
const sourceSessionId = durable.source_session_id;
|
|
1072
|
+
const beforeBinding = durable.before_binding;
|
|
1073
|
+
const afterBinding = durable.after_binding;
|
|
1074
|
+
const source = loadManagedSession(storeDir, sourceSessionId);
|
|
1075
|
+
const target = loadManagedSession(storeDir, durable.target_session_id);
|
|
1076
|
+
handoffApplication.assertAuthority([
|
|
1077
|
+
() => source.status === "detached",
|
|
1078
|
+
() => source.last_transition_id === durable.transition_id,
|
|
1079
|
+
() => JSON.stringify(source.binding) === JSON.stringify(beforeBinding),
|
|
1080
|
+
() => target.status === "bound",
|
|
1081
|
+
() => target.last_transition_id === durable.transition_id,
|
|
1082
|
+
() => JSON.stringify(target.binding) === JSON.stringify(afterBinding)
|
|
1083
|
+
], "human-observed handoff Session authority changed before send");
|
|
1084
|
+
const targetId = afterBinding.native_thread_id;
|
|
1085
|
+
if (!targetId) {
|
|
1086
|
+
throw new Error("human-observed handoff target identity is incomplete");
|
|
1087
|
+
}
|
|
1088
|
+
const resolved = await resolveCurrentNativeAgentSessionIdentity({
|
|
1089
|
+
options,
|
|
1090
|
+
agent: terminal.agent,
|
|
1091
|
+
pid: terminal.pid,
|
|
1092
|
+
cwd: terminal.terminalControl.currentPath,
|
|
1093
|
+
preferredSessionId: targetId,
|
|
1094
|
+
allowedCompanionIdentity: codexIdentityFence({
|
|
1095
|
+
sessionId: durable.before_native_thread_id,
|
|
1096
|
+
processUuid: durable.before_process_uuid,
|
|
1097
|
+
processBirth: durable.before_process_birth,
|
|
1098
|
+
rollout: durable.before_process_rollout,
|
|
1099
|
+
evidence: beforeBinding.native_process.evidence
|
|
1100
|
+
}),
|
|
1101
|
+
allowedAdditionalIdentities: []
|
|
1102
|
+
});
|
|
1103
|
+
const bridge = createTerminalAgentBridge(options);
|
|
1104
|
+
const status = await bridge.status(terminal.agent, terminal.terminalControl, { runtime: terminalRuntimeForLiveIdentity({ terminal, physicalOnly: true }) });
|
|
1105
|
+
if (requireEmptyComposer) {
|
|
1106
|
+
assertSafeTerminalSend(terminal.agent, status);
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
const displayName = executorDefinitionForKind(terminal.agent).displayName;
|
|
1110
|
+
const approval = isRecord(status?.approval_state)
|
|
1111
|
+
? status.approval_state
|
|
1112
|
+
: undefined;
|
|
1113
|
+
if (status?.reachable !== true) {
|
|
1114
|
+
throw new Error(`${displayName} terminal status is unavailable`);
|
|
1115
|
+
}
|
|
1116
|
+
if (approval?.blocked === true) {
|
|
1117
|
+
throw new Error(stringValue(approval.reason) ??
|
|
1118
|
+
`${displayName} is waiting at a permission dialog`);
|
|
1119
|
+
}
|
|
1120
|
+
// With an exact draft in the composer, native TUIs can classify the screen
|
|
1121
|
+
// as `unknown` instead of `idle`. Exact draft materialization is proven by
|
|
1122
|
+
// the bridge before Enter, so only a positively busy state is unsafe here.
|
|
1123
|
+
if (status.activity_state !== "idle" &&
|
|
1124
|
+
status.activity_state !== "unknown") {
|
|
1125
|
+
throw new Error(`${displayName} terminal became ${stringValue(status.activity_state) ?? "unknown"} before handoff submission`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
let liveIdentity = resolved;
|
|
1129
|
+
if (terminal.agent === "codex") {
|
|
1130
|
+
const foreground = terminal.adapter.observeThreadLifecycle?.({
|
|
1131
|
+
operation: { kind: "new_thread" },
|
|
1132
|
+
phase: "before",
|
|
1133
|
+
screen: status.screen.excerpt ?? ""
|
|
1134
|
+
});
|
|
1135
|
+
const foregroundId = foreground?.status === "observed" &&
|
|
1136
|
+
isExactNativeThreadId(foreground.nativeThreadId)
|
|
1137
|
+
? foreground.nativeThreadId.toLowerCase()
|
|
1138
|
+
: undefined;
|
|
1139
|
+
if (foregroundId && foregroundId !== targetId.toLowerCase()) {
|
|
1140
|
+
throw new Error("Codex foreground native thread changed after handoff adoption");
|
|
1141
|
+
}
|
|
1142
|
+
if (!afterBinding.native_process.rollout) {
|
|
1143
|
+
if (requireEmptyComposer &&
|
|
1144
|
+
foregroundId !== targetId.toLowerCase()) {
|
|
1145
|
+
throw new Error("status-card-only Codex handoff lost its exact foreground identity");
|
|
1146
|
+
}
|
|
1147
|
+
liveIdentity = resolved?.sessionId.toLowerCase() === targetId.toLowerCase()
|
|
1148
|
+
? resolved
|
|
1149
|
+
: {
|
|
1150
|
+
sessionId: targetId,
|
|
1151
|
+
processUuid: afterBinding.native_process.process_uuid,
|
|
1152
|
+
processBirth: afterBinding.native_process.process_birth,
|
|
1153
|
+
evidence: foreground?.evidence ?? "codex_status_card"
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
else if (!liveIdentity ||
|
|
1157
|
+
liveIdentity.sessionId.toLowerCase() !== targetId.toLowerCase()) {
|
|
1158
|
+
throw new Error("Codex handoff rollout identity changed before terminal transport");
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (!liveIdentity) {
|
|
1162
|
+
throw new Error("human-observed handoff identity is unavailable before send");
|
|
1163
|
+
}
|
|
1164
|
+
const exact = exactLifecycleProcessIdentity(terminal, liveIdentity);
|
|
1165
|
+
handoffApplication.assertAuthority([
|
|
1166
|
+
() => exact.sessionId.toLowerCase() === targetId.toLowerCase(),
|
|
1167
|
+
() => exact.processUuid === afterBinding.native_process.process_uuid,
|
|
1168
|
+
() => exact.processBirth === afterBinding.native_process.process_birth,
|
|
1169
|
+
() => JSON.stringify(exact.rollout ?? null) ===
|
|
1170
|
+
JSON.stringify(afterBinding.native_process.rollout ?? null)
|
|
1171
|
+
], "human-observed handoff identity changed before send");
|
|
1172
|
+
if (requireEmptyComposer) {
|
|
1173
|
+
const empty = terminal.agent === "codex"
|
|
1174
|
+
? codexComposerEmpty(status.screen.excerpt)
|
|
1175
|
+
: claudeComposerEmpty(status.screen.excerpt);
|
|
1176
|
+
if (!empty) {
|
|
1177
|
+
throw new Error("human-observed handoff composer changed before text injection");
|
|
1178
|
+
}
|
|
1179
|
+
if (terminal.agent === "codex") {
|
|
1180
|
+
await assertCodexComposerReadyForAutomatedInput({
|
|
1181
|
+
options,
|
|
1182
|
+
terminalControl: terminal.terminalControl
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
function assertConversationHasNoNonterminalDeferredForegroundTransfer({ storeDir, conversation, action }) {
|
|
1188
|
+
const turnId = turnIdForConversation(conversation);
|
|
1189
|
+
const sourceTransfer = listDeferredForegroundTransfers(storeDir).find((candidate) => candidate.version === 2 &&
|
|
1190
|
+
candidate.source_kind === "candidate_rollout_quiescent" &&
|
|
1191
|
+
!isFinalDeferredForegroundTransferStatus(candidate.status) &&
|
|
1192
|
+
(candidate.source_turn_history ?? []).some((sourceTurn) => sourceTurn.turn_id === turnId));
|
|
1193
|
+
if (sourceTransfer) {
|
|
1194
|
+
throw new Error(`cannot ${action} Turn ${turnId} while deferred foreground transfer ` +
|
|
1195
|
+
`${sourceTransfer.transfer_id} reserves it as immutable source ` +
|
|
1196
|
+
`history in ${sourceTransfer.status}; dedicated transfer recovery ` +
|
|
1197
|
+
"must finish first");
|
|
1198
|
+
}
|
|
1199
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
1200
|
+
? conversation.native_session_takeover
|
|
1201
|
+
: undefined;
|
|
1202
|
+
const transferId = stringValue(takeover?.deferred_foreground_transfer_id);
|
|
1203
|
+
if (!transferId) {
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const transfer = loadDeferredForegroundTransfer(storeDir, transferId);
|
|
1207
|
+
if (!isFinalDeferredForegroundTransferStatus(transfer.status)) {
|
|
1208
|
+
throw new Error(`cannot ${action} Turn ${turnIdForConversation(conversation)} while ` +
|
|
1209
|
+
`deferred foreground transfer ${transfer.transfer_id} is ` +
|
|
1210
|
+
`${transfer.status}; dedicated transfer recovery must finish first`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
function assertTerminalHasNoNonterminalDeferredForegroundTransfer({ storeDir, pid, terminalControl, action }) {
|
|
1214
|
+
const transfer = listDeferredForegroundTransfers(storeDir).find((candidate) => !isFinalDeferredForegroundTransferStatus(candidate.status) &&
|
|
1215
|
+
candidate.process_pid === pid &&
|
|
1216
|
+
terminalControlEvidenceMatches(candidate.terminal_endpoint, terminalControl));
|
|
1217
|
+
if (!transfer) {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
throw new Error(`cannot ${action} terminal ${terminalControl.target} while deferred ` +
|
|
1221
|
+
`foreground transfer ${transfer.transfer_id} is ${transfer.status}; ` +
|
|
1222
|
+
"dedicated transfer recovery must finish first");
|
|
1223
|
+
}
|
|
1224
|
+
async function recoverDeferredCodexForegroundTransferBeforeMutation({ options, terminal }) {
|
|
1225
|
+
const storeDir = storeDirFromOptions(options);
|
|
1226
|
+
const locks = terminalWriterMutationLocks(storeDir, terminal.terminalControl);
|
|
1227
|
+
await withCanonicalMutationLocks({
|
|
1228
|
+
...locks,
|
|
1229
|
+
// Every caller already owns this exact terminal lock. The no-op adapter
|
|
1230
|
+
// creates the canonical scope without recursively acquiring the raw lock.
|
|
1231
|
+
acquireTerminal: () => () => { }
|
|
1232
|
+
}, (scopes, resources) => recoverDeferredCodexForegroundTransferWhileWriterLease({
|
|
1233
|
+
options,
|
|
1234
|
+
terminal,
|
|
1235
|
+
storeDir,
|
|
1236
|
+
scopes,
|
|
1237
|
+
resources
|
|
1238
|
+
}));
|
|
1239
|
+
}
|
|
1240
|
+
async function withDeferredForegroundRecoveryScope({ scopes, resources, transfer, operation }) {
|
|
1241
|
+
if (!transfer.state_path) {
|
|
1242
|
+
return operation(bindDeferredForegroundWriterScope(scopes, resources));
|
|
1243
|
+
}
|
|
1244
|
+
const statePath = path.resolve(transfer.state_path);
|
|
1245
|
+
const paths = pathsForConversationDir(path.dirname(statePath));
|
|
1246
|
+
return withTerminalDispatchStateScope(scopes, resources, statePath, paths.logPath, (stateScopes, stateResources) => operation(bindDeferredForegroundApplicationScope(stateScopes, stateResources)));
|
|
1247
|
+
}
|
|
1248
|
+
function matchingDeferredForegroundTransfers(scope, terminal) {
|
|
1249
|
+
return scope.listTransfers().filter((transfer) => transfer.terminal_id === terminal.conversationId &&
|
|
1250
|
+
transfer.process_pid === terminal.pid &&
|
|
1251
|
+
terminalControlEvidenceMatches(transfer.terminal_endpoint, terminal.terminalControl));
|
|
1252
|
+
}
|
|
1253
|
+
async function recoverDeferredCodexForegroundTransferWhileWriterLease({ options, terminal, storeDir, scopes, resources }) {
|
|
1254
|
+
options = Object.freeze({ ...options, storeDir });
|
|
1255
|
+
const service = new DeferredForegroundRecoveryService({
|
|
1256
|
+
transaction: {
|
|
1257
|
+
writerScope: () => bindDeferredForegroundWriterScope(scopes, resources),
|
|
1258
|
+
withTransferScope: (transfer, operation) => withDeferredForegroundRecoveryScope({
|
|
1259
|
+
scopes,
|
|
1260
|
+
resources,
|
|
1261
|
+
transfer,
|
|
1262
|
+
operation
|
|
1263
|
+
})
|
|
1264
|
+
},
|
|
1265
|
+
repository: {
|
|
1266
|
+
all: (scope) => scope.listTransfers(),
|
|
1267
|
+
matching: (scope) => matchingDeferredForegroundTransfers(scope, terminal),
|
|
1268
|
+
load: (scope, transferId) => scope.loadTransfer(transferId),
|
|
1269
|
+
markUncertain: (scope, boundary, reason) => deferredForegroundApplication(options, terminal).markUncertain({
|
|
1270
|
+
scope,
|
|
1271
|
+
boundary,
|
|
1272
|
+
reason
|
|
1273
|
+
})
|
|
1274
|
+
},
|
|
1275
|
+
recovery: {
|
|
1276
|
+
boundary: (transfer) => deferredForegroundBoundaryProjection(deferredRecoveryAdapter.deferredCodexBoundaryFromTransfer(deferredForegroundRecoveryAdapterPorts(), { terminal, transfer })),
|
|
1277
|
+
assertRoute: (scope, transfer, boundary) => deferredForegroundApplication(options, terminal)
|
|
1278
|
+
.assertTransferAuthority(scope, transfer, boundary),
|
|
1279
|
+
finalizeAbort: (scope, transfer) => {
|
|
1280
|
+
deferredForegroundApplication(options).finalizeAbort(scope, transfer);
|
|
1281
|
+
},
|
|
1282
|
+
persistCommitted: (scope, transfer) => deferredRecoveryAdapter.persistCommittedDeferredForegroundTurnAcceptance(deferredForegroundRecoveryAdapterPorts(), {
|
|
1283
|
+
options,
|
|
1284
|
+
scope,
|
|
1285
|
+
storeDir,
|
|
1286
|
+
terminal,
|
|
1287
|
+
transfer
|
|
1288
|
+
}),
|
|
1289
|
+
crashAfterCommittedBackfill: () => {
|
|
1290
|
+
if (cliEnv().AKK_TEST_EXIT_AFTER_DEFERRED_COMMITTED_ACCEPTANCE_BACKFILL ===
|
|
1291
|
+
"1") {
|
|
1292
|
+
cliExit(86);
|
|
1293
|
+
}
|
|
1294
|
+
},
|
|
1295
|
+
resolveCommitted: async (scope, boundary) => {
|
|
1296
|
+
await deferredForegroundApplication(options, terminal).resolve({
|
|
1297
|
+
scope,
|
|
1298
|
+
boundary
|
|
1299
|
+
});
|
|
1300
|
+
},
|
|
1301
|
+
assertAcceptedTurn: (accepted) => assertNativeAgentIdentityForTurn({
|
|
1302
|
+
conversation: accepted.conversation,
|
|
1303
|
+
currentIdentity: accepted.identity,
|
|
1304
|
+
operation: "recover committed deferred foreground binding for"
|
|
1305
|
+
}),
|
|
1306
|
+
abortPrepared: (scope, transfer, boundary, at) => deferredRecoveryAdapter.abortPreparedDeferredForegroundTurn(deferredForegroundRecoveryAdapterPorts(), {
|
|
1307
|
+
options,
|
|
1308
|
+
scope,
|
|
1309
|
+
storeDir,
|
|
1310
|
+
terminal,
|
|
1311
|
+
transfer,
|
|
1312
|
+
boundary: deferredForegroundConcreteBoundary(boundary, terminal),
|
|
1313
|
+
terminalInputNotStartedAt: at
|
|
1314
|
+
}),
|
|
1315
|
+
durableInputNotStartedAt: (scope, transfer) => deferredRecoveryAdapter.deferredCodexDurableInputNotStartedAt(deferredForegroundRecoveryAdapterPorts(), scope, transfer),
|
|
1316
|
+
recoverAccepted: (scope, transfer, boundary) => deferredRecoveryAdapter.recoverAcceptedDeferredForegroundDispatch(deferredForegroundRecoveryAdapterPorts(), {
|
|
1317
|
+
options,
|
|
1318
|
+
scope,
|
|
1319
|
+
storeDir,
|
|
1320
|
+
terminal,
|
|
1321
|
+
transfer,
|
|
1322
|
+
boundary: deferredForegroundConcreteBoundary(boundary, terminal)
|
|
1323
|
+
}),
|
|
1324
|
+
pendingAnchorVersion: (scope, transfer) => deferredRecoveryAdapter.loadDeferredForegroundTurnAuthority(deferredForegroundRecoveryAdapterPorts(), {
|
|
1325
|
+
storeDir,
|
|
1326
|
+
terminal,
|
|
1327
|
+
transfer,
|
|
1328
|
+
scope
|
|
1329
|
+
}).anchor.version
|
|
1330
|
+
},
|
|
1331
|
+
runtime: {
|
|
1332
|
+
terminalTarget: terminal.terminalControl.target,
|
|
1333
|
+
isStoreMutationLockTimeout
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
await service.recover();
|
|
1337
|
+
}
|
|
1338
|
+
function bindContextObject(target, call) {
|
|
1339
|
+
return new Proxy(target, {
|
|
1340
|
+
get(object, property) {
|
|
1341
|
+
const value = Reflect.get(object, property, object);
|
|
1342
|
+
if (typeof value === "function") {
|
|
1343
|
+
return (...arguments_) => call(() => Reflect.apply(value, object, arguments_));
|
|
1344
|
+
}
|
|
1345
|
+
return value && typeof value === "object"
|
|
1346
|
+
? bindContextObject(value, call)
|
|
1347
|
+
: value;
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
const terminalHandoffOperations = {
|
|
1352
|
+
assertSafeAbortedTerminalRetryBinding,
|
|
1353
|
+
exactSafeAbortedRecoveredSessionMatches,
|
|
1354
|
+
maybeDetachVerifiedEmptyCodexSource,
|
|
1355
|
+
assertVerifiedEmptyCodexTransportBoundary,
|
|
1356
|
+
assertDeferredCodexForegroundBindingBoundary,
|
|
1357
|
+
prepareDeferredCodexForegroundBinding,
|
|
1358
|
+
observedHandoffAuthorityToken,
|
|
1359
|
+
activeTurnHandoffDecisionToken,
|
|
1360
|
+
assertExpectedHandoffTokenUsesExactTerminalSelector,
|
|
1361
|
+
observedExternalHandoffIdentity,
|
|
1362
|
+
observedHandoffTargetResolution,
|
|
1363
|
+
maybeAdoptObservedExternalThread,
|
|
1364
|
+
assertObservedHandoffTransportBoundary,
|
|
1365
|
+
assertConversationHasNoNonterminalDeferredForegroundTransfer,
|
|
1366
|
+
assertTerminalHasNoNonterminalDeferredForegroundTransfer,
|
|
1367
|
+
recoverDeferredCodexForegroundTransferBeforeMutation,
|
|
1368
|
+
recoverDeferredCodexForegroundTransferWhileWriterLease
|
|
1369
|
+
};
|
|
1370
|
+
export function createTerminalHandoffCliFacade(dependencies) {
|
|
1371
|
+
const runtime = {
|
|
1372
|
+
dependencies,
|
|
1373
|
+
originalExpectedTerminalSelector: new WeakMap()
|
|
1374
|
+
};
|
|
1375
|
+
const call = (operation) => terminalHandoffContext.run(runtime, operation);
|
|
1376
|
+
const contextualOperations = Object.fromEntries(Object.entries(terminalHandoffOperations).map(([name, operation]) => [
|
|
1377
|
+
name,
|
|
1378
|
+
(...arguments_) => call(() => operation(...arguments_))
|
|
1379
|
+
]));
|
|
1380
|
+
return Object.freeze({
|
|
1381
|
+
...contextualOperations,
|
|
1382
|
+
rememberOriginalExpectedTerminalSelector: (options, selector) => {
|
|
1383
|
+
runtime.originalExpectedTerminalSelector.set(options, selector);
|
|
1384
|
+
},
|
|
1385
|
+
deferredForegroundApplication: (...arguments_) => bindContextObject(call(() => deferredForegroundApplication(...arguments_)), call),
|
|
1386
|
+
deferredForegroundRecoveryAdapterPorts: () => bindContextObject(call(() => deferredForegroundRecoveryAdapterPorts()), call)
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
//# sourceMappingURL=terminal-handoff-cli-adapter.js.map
|