@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,2444 @@
|
|
|
1
|
+
// Infrastructure composition for terminal list discovery and selector projection.
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { listDeferredForegroundTransfers } from "./deferred-foreground-transfer.js";
|
|
6
|
+
import { isFinalDeferredForegroundTransferStatus } from "./deferred-foreground-transfer-policy.js";
|
|
7
|
+
import { humanObservedHandoffBindingToken, isExactNativeThreadId, managedSessionBindingToken, managedSessionRevision, unmanagedTerminalBindingToken } from "./managed-session.js";
|
|
8
|
+
import { executorForConversation, isSessionSendBlockingStatus, isTerminalDispatchOwnerReleasedStatus, resolveExecutor, sessionIdForConversation, turnIdForConversation } from "./protocol.js";
|
|
9
|
+
import { resolveSessionSelector, sessionShortRef } from "./session-selector.js";
|
|
10
|
+
import { projectSessionSelectorCandidate } from "./terminal-selector-projection-service.js";
|
|
11
|
+
import { listManagedSessions, loadNativeThreadTransition, nativeThreadTransitionsDir, tryLoadManagedSession } from "./session-store.js";
|
|
12
|
+
import { defaultStoreDir, ensureStoreWritable, inspectStoreCompatibility, listConversations, STORE_SESSION_AUTHORITY_PROTOCOL } from "./store.js";
|
|
13
|
+
import { exactCodexReadyStyledComposerCapture } from "./terminal-agent-bridge.js";
|
|
14
|
+
import { captureCodexHumanStartedActiveTaskAnchor } from "./terminal-submission-acceptance.js";
|
|
15
|
+
import { applySessionAuthorityToDispatch, authoritativeTerminalIdentity, compareManagedConversationRecency, decideManagedTerminalAssociation, decideLocalTerminalDispatchOwnership, decideTerminalSendAuthority, decideTerminalSessionAuthorityConflict, managedTurnNeedsAttention as terminalManagedTurnNeedsAttention, nonOwnerTerminalActions, projectBlockingTurn, projectHandoffDecision, projectHandoffPresentation, projectPublicManagementConflict, projectReconcileBindingAction, projectTerminalDispatchConflict, projectTerminalManagement, selectManagedTerminalHistory, selectTerminalAvailableActions } from "./terminal-action-projection.js";
|
|
16
|
+
import { childProcessIdsForRoot, decideManagedBindingConflict, deferredCodexForegroundBindingToken, exactBoundCodexSendSource, isCompleteNativeRollout, nativeAgentIdentityMatchesTurn, processIncarnationRelationship, selectRootTerminalProcesses, terminalControlAliasMatches, terminalControlsShareIncarnation, verifiedEmptyCodexHandoffToken } from "./terminal-authority-policy.js";
|
|
17
|
+
import { decideTerminalBindingMatch, terminalObservationFromListEntry } from "./terminal-binding-authority.js";
|
|
18
|
+
import { decideTerminalScopedCodexApprovalAuthority, terminalScopedCodexApprovalPromptSnapshot } from "./terminal-scoped-approval-authority.js";
|
|
19
|
+
import { sameCanonicalStatePath } from "./terminal-dispatch-ledger-codec.js";
|
|
20
|
+
import * as dispatch from "./terminal-dispatch-policy.js";
|
|
21
|
+
import { actionsForManagedSessionBinding, currentTerminalActions, listActionContracts, readOnlyListActions, readOnlyManagedTurn, renderAvailableListActions, renderCurrentManagedTurn, renderHistoricalManagedTurn, renderManagedTurnListEntry, safeUnavailableManagedTurnActions, sendActionForManagedSession, withoutGenericHandoffSourceClose } from "./terminal-list-renderer.js";
|
|
22
|
+
import { hasCanonicalTerminalEndpoint, terminalControlEvidenceMatches } from "./terminal-control-ref.js";
|
|
23
|
+
import { validTerminalMonitorTimestampMs as validTimestampMs } from "./terminal-monitor-decision-policy.js";
|
|
24
|
+
import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
|
|
25
|
+
import { expandHome, writeCliJson as printJson } from "./cli-command-runtime.js";
|
|
26
|
+
import { cliCwd, cliNowMs, cliRuntimeLog as runtimeLog } from "./cli-runtime-context.js";
|
|
27
|
+
const terminalListRuntimeContext = new AsyncLocalStorage();
|
|
28
|
+
function terminalListRuntime() {
|
|
29
|
+
const runtime = terminalListRuntimeContext.getStore();
|
|
30
|
+
if (!runtime) {
|
|
31
|
+
throw new Error("Terminal list facade runtime is unavailable");
|
|
32
|
+
}
|
|
33
|
+
return runtime;
|
|
34
|
+
}
|
|
35
|
+
function withTerminalListRuntime(runtime, operation) {
|
|
36
|
+
return terminalListRuntimeContext.run(runtime, operation);
|
|
37
|
+
}
|
|
38
|
+
export function createTerminalListCliFacade(dependencies) {
|
|
39
|
+
const runtime = {
|
|
40
|
+
...dependencies.reconciliation,
|
|
41
|
+
...dependencies.discovery,
|
|
42
|
+
...dependencies.store,
|
|
43
|
+
...dependencies.authority,
|
|
44
|
+
...dependencies.policy
|
|
45
|
+
};
|
|
46
|
+
const call = (operation) => withTerminalListRuntime(runtime, operation);
|
|
47
|
+
return {
|
|
48
|
+
runList: (options) => call(() => runList(options)),
|
|
49
|
+
buildTerminalListGroup: (request) => call(() => buildTerminalListGroup(request)),
|
|
50
|
+
observeExactTerminal: (request) => call(() => observeExactTerminal(request)),
|
|
51
|
+
provisionalManagedBindingTurnCount: (storeDir, session) => call(() => provisionalManagedBindingTurnCount(storeDir, session)),
|
|
52
|
+
managedSessionHasUnresolvedNativeTransition: (storeDir, session) => call(() => managedSessionHasUnresolvedNativeTransition(storeDir, session)),
|
|
53
|
+
managedSessionHasAnyNativeTransition: (storeDir, session) => call(() => managedSessionHasAnyNativeTransition(storeDir, session)),
|
|
54
|
+
terminalControlForManagedConversation: (conversation) => call(() => terminalControlForManagedConversation(conversation)),
|
|
55
|
+
terminalIncarnationBlockingTurns: (storeDir, terminalControl) => call(() => terminalIncarnationBlockingTurns(storeDir, terminalControl)),
|
|
56
|
+
managedTurnNeedsAttention: (conversation) => call(() => managedTurnNeedsAttention(conversation)),
|
|
57
|
+
assertTerminalIncarnationCanStartTurn: (storeDir, terminalControl) => call(() => assertTerminalIncarnationCanStartTurn(storeDir, terminalControl)),
|
|
58
|
+
terminalDispatchOwnership: (terminalControl) => call(() => terminalDispatchOwnership(terminalControl)),
|
|
59
|
+
resolveTerminalScopedCodexApproval: (request) => call(() => resolveTerminalScopedCodexApproval(request)),
|
|
60
|
+
resolveConversationSelectorOption: (commandName, options) => call(() => resolveConversationSelectorOption(commandName, options))
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function projectSelectorCandidate(entry, commandName, observedAtMs, options) {
|
|
64
|
+
return projectSessionSelectorCandidate(entry, commandName, observedAtMs, options, { isActiveStatus: terminalListRuntime().isActiveStatus });
|
|
65
|
+
}
|
|
66
|
+
async function runList(options) {
|
|
67
|
+
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(cliCwd()));
|
|
68
|
+
const store = inspectStoreCompatibility(storeDir);
|
|
69
|
+
const reconciliation = options.reconcile === true
|
|
70
|
+
? await reconcileStoreForList(storeDir, options)
|
|
71
|
+
: {
|
|
72
|
+
status: "disabled",
|
|
73
|
+
reason: "standalone list is read-only unless --reconcile is supplied"
|
|
74
|
+
};
|
|
75
|
+
const agentFilter = options.agent ? resolveExecutor({ kind: options.agent }).kind : undefined;
|
|
76
|
+
const statusFilter = options.status;
|
|
77
|
+
const terminalScan = await buildTerminalListGroup({ options, agentFilter, statusFilter });
|
|
78
|
+
const projected = projectTerminalListScan({
|
|
79
|
+
options,
|
|
80
|
+
storeDir,
|
|
81
|
+
store,
|
|
82
|
+
terminalScan,
|
|
83
|
+
agentFilter,
|
|
84
|
+
statusFilter
|
|
85
|
+
});
|
|
86
|
+
printJson({
|
|
87
|
+
store_dir: storeDir,
|
|
88
|
+
store,
|
|
89
|
+
reconciliation,
|
|
90
|
+
action_contracts: listActionContracts(),
|
|
91
|
+
terminals: projected.terminals,
|
|
92
|
+
terminal_watches: projected.terminalWatches,
|
|
93
|
+
unavailable_managed_turns: projected.unavailableManagedTurns,
|
|
94
|
+
terminal_scan: {
|
|
95
|
+
...terminalScan.summary,
|
|
96
|
+
terminal_count: projected.terminals.length
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
runtimeLog("info", "terminals_listed", {
|
|
100
|
+
store_dir: storeDir,
|
|
101
|
+
terminal_count: projected.terminals.length,
|
|
102
|
+
unavailable_managed_turn_count: projected.unavailableManagedTurns.length,
|
|
103
|
+
terminal_scan_error: terminalScan.summary.error,
|
|
104
|
+
include_all: projected.includeAll,
|
|
105
|
+
agent_filter: agentFilter,
|
|
106
|
+
status_filter: statusFilter,
|
|
107
|
+
reconciliation
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function projectTerminalListScan(input) {
|
|
111
|
+
const { options, storeDir, store, terminalScan, agentFilter, statusFilter, tolerateInvalidWatchRecords } = input;
|
|
112
|
+
const includeAll = Boolean(options.all);
|
|
113
|
+
const allManagedConversations = listConversations(storeDir)
|
|
114
|
+
.filter(terminalListRuntime().isDiscoverableTmuxConversation);
|
|
115
|
+
const managedSessions = store.readable
|
|
116
|
+
? listManagedSessions(storeDir)
|
|
117
|
+
: [];
|
|
118
|
+
const displayedConversations = allManagedConversations
|
|
119
|
+
.filter((conversation) => includeAll || terminalListRuntime().isActiveStatus(conversation.status))
|
|
120
|
+
.filter((conversation) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, conversation.workspace))
|
|
121
|
+
.filter((conversation) => !agentFilter || executorForConversation(conversation).kind === agentFilter)
|
|
122
|
+
.filter((conversation) => !statusFilter || conversation.status === statusFilter);
|
|
123
|
+
const workspaceConversations = allManagedConversations.filter((conversation) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, conversation.workspace));
|
|
124
|
+
const physicalTerminals = terminalScan.terminalControlled.filter((entry) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, entry.workspace ?? entry.cwd));
|
|
125
|
+
const projection = terminalFirstListProjection({
|
|
126
|
+
storeDir,
|
|
127
|
+
terminals: physicalTerminals,
|
|
128
|
+
managedSessions,
|
|
129
|
+
sessionAuthorityRequired: Number(store.writer_protocol) >= STORE_SESSION_AUTHORITY_PROTOCOL,
|
|
130
|
+
allConversations: workspaceConversations,
|
|
131
|
+
displayedConversations,
|
|
132
|
+
includeAll,
|
|
133
|
+
managedOnly: options.managedOnly === true,
|
|
134
|
+
statusFilter,
|
|
135
|
+
mutationsAllowed: store.writable === true
|
|
136
|
+
});
|
|
137
|
+
const exactWatchScan = terminalListRuntime()
|
|
138
|
+
.scanTerminalWatchesForExactObservation;
|
|
139
|
+
const watchObservation = tolerateInvalidWatchRecords && exactWatchScan
|
|
140
|
+
? exactWatchScan(storeDir, { includeAll })
|
|
141
|
+
: {
|
|
142
|
+
watches: terminalListRuntime().listTerminalWatches?.(storeDir, { includeAll }) ?? [],
|
|
143
|
+
activeOverlayTrusted: true
|
|
144
|
+
};
|
|
145
|
+
const observedTerminalWatches = watchObservation.watches;
|
|
146
|
+
const terminalWatches = options.managedOnly
|
|
147
|
+
? []
|
|
148
|
+
: observedTerminalWatches
|
|
149
|
+
.filter((watch) => !agentFilter || watch.agent === agentFilter)
|
|
150
|
+
.filter((watch) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, watch.workspace))
|
|
151
|
+
.filter((watch) => !statusFilter || watch.status === statusFilter);
|
|
152
|
+
const activeWatchedTerminals = new Set(observedTerminalWatches
|
|
153
|
+
.filter((watch) => watch.status === "active")
|
|
154
|
+
.map((watch) => stringValue(watch.terminal_id))
|
|
155
|
+
.filter((terminalId) => terminalId !== undefined));
|
|
156
|
+
const terminals = projection.terminals.map((terminal) => !watchObservation.activeOverlayTrusted ||
|
|
157
|
+
activeWatchedTerminals.has(stringValue(terminal.id) ?? "")
|
|
158
|
+
? withoutTerminalWatchAuthority(terminal)
|
|
159
|
+
: terminal);
|
|
160
|
+
return {
|
|
161
|
+
includeAll,
|
|
162
|
+
terminals,
|
|
163
|
+
terminalWatches,
|
|
164
|
+
unavailableManagedTurns: projection.unavailableManagedTurns
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
async function observeExactTerminal(request) {
|
|
168
|
+
const { options, terminalId } = request;
|
|
169
|
+
const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(cliCwd()));
|
|
170
|
+
const store = inspectStoreCompatibility(storeDir);
|
|
171
|
+
const scan = await buildTerminalListGroup({ options, terminalId });
|
|
172
|
+
const matches = scan.terminalControlled.filter((terminal) => stringValue(terminal.id) === terminalId);
|
|
173
|
+
if (matches.length !== 1) {
|
|
174
|
+
return scan.summary.error
|
|
175
|
+
? {
|
|
176
|
+
state: "unavailable",
|
|
177
|
+
reason: scan.summary.error,
|
|
178
|
+
summary: scan.summary
|
|
179
|
+
}
|
|
180
|
+
: { state: "absent", summary: scan.summary };
|
|
181
|
+
}
|
|
182
|
+
const projected = projectTerminalListScan({
|
|
183
|
+
options,
|
|
184
|
+
storeDir,
|
|
185
|
+
store,
|
|
186
|
+
terminalScan: scan,
|
|
187
|
+
tolerateInvalidWatchRecords: true
|
|
188
|
+
});
|
|
189
|
+
const projectedMatches = projected.terminals.filter((terminal) => stringValue(terminal.id) === terminalId);
|
|
190
|
+
if (projectedMatches.length !== 1) {
|
|
191
|
+
return {
|
|
192
|
+
state: "unavailable",
|
|
193
|
+
reason: "the exact terminal could not be projected authoritatively",
|
|
194
|
+
summary: scan.summary
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
state: "available",
|
|
199
|
+
rawTerminal: matches[0],
|
|
200
|
+
terminal: projectedMatches[0],
|
|
201
|
+
summary: scan.summary
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function withoutAvailableAction(terminal, action) {
|
|
205
|
+
if (!isRecord(terminal.available_actions))
|
|
206
|
+
return terminal;
|
|
207
|
+
const actions = { ...terminal.available_actions };
|
|
208
|
+
delete actions[action];
|
|
209
|
+
return { ...terminal, available_actions: actions };
|
|
210
|
+
}
|
|
211
|
+
function withoutTerminalWatchAuthority(terminal) {
|
|
212
|
+
const projected = withoutAvailableAction(terminal, "watch");
|
|
213
|
+
if (!Object.hasOwn(projected, "terminal_watch_hint"))
|
|
214
|
+
return projected;
|
|
215
|
+
const safe = { ...projected };
|
|
216
|
+
delete safe.terminal_watch_hint;
|
|
217
|
+
return safe;
|
|
218
|
+
}
|
|
219
|
+
async function reconcileStoreForList(storeDir, options) {
|
|
220
|
+
try {
|
|
221
|
+
ensureStoreWritable(storeDir);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (isRecord(error) && error.code === "AKK_STORE_INCOMPATIBLE") {
|
|
225
|
+
return {
|
|
226
|
+
status: "skipped",
|
|
227
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
228
|
+
store: inspectStoreCompatibility(storeDir)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
const monitors = await terminalListRuntime().reconcileMonitors(options, {
|
|
234
|
+
includeCallbackRecovery: false,
|
|
235
|
+
reason: "list_reconciliation",
|
|
236
|
+
conversationId: undefined
|
|
237
|
+
});
|
|
238
|
+
const idle = terminalListRuntime().reconcileIdleConversations(storeDir, options);
|
|
239
|
+
return {
|
|
240
|
+
status: "completed",
|
|
241
|
+
checked: Math.max(idle.checked, monitors.checked),
|
|
242
|
+
changed: idle.closed + monitors.launched + monitors.repaired,
|
|
243
|
+
closed: idle.closed,
|
|
244
|
+
repaired: monitors.repaired,
|
|
245
|
+
collateral_stalls_checked: monitors.collateral_stalls_checked,
|
|
246
|
+
collateral_stalls_skipped: monitors.collateral_stalls_skipped,
|
|
247
|
+
collateral_stall_repairs: monitors.items.filter((item) => item.status === "repaired"),
|
|
248
|
+
monitors_launched: monitors.launched,
|
|
249
|
+
monitors_already_running: monitors.already_running,
|
|
250
|
+
skipped: idle.skipped + monitors.skipped,
|
|
251
|
+
errors: monitors.errors,
|
|
252
|
+
idle_timeout_minutes: idle.idle_timeout_minutes
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async function buildTerminalListGroup({ options, agentFilter, statusFilter, terminalId }) {
|
|
256
|
+
const empty = {
|
|
257
|
+
terminalControlled: [],
|
|
258
|
+
summary: {
|
|
259
|
+
enabled: false,
|
|
260
|
+
agents: [],
|
|
261
|
+
error: undefined
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
if (options.managedOnly) {
|
|
265
|
+
return empty;
|
|
266
|
+
}
|
|
267
|
+
const registry = terminalListRuntime().createRuntimeTerminalAgentRegistry(options);
|
|
268
|
+
const adapters = agentFilter
|
|
269
|
+
? [registry.get(agentFilter)].filter((adapter) => adapter !== undefined)
|
|
270
|
+
: registry.list();
|
|
271
|
+
if (agentFilter && adapters.length === 0) {
|
|
272
|
+
return {
|
|
273
|
+
...empty,
|
|
274
|
+
summary: {
|
|
275
|
+
enabled: true,
|
|
276
|
+
agents: [],
|
|
277
|
+
skipped: `terminal agent adapter is not registered for ${agentFilter}`
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const terminalProvider = terminalListRuntime().createTerminalControlProvider(options);
|
|
282
|
+
const bridge = terminalListRuntime().createTerminalAgentBridge(options, terminalProvider, registry);
|
|
283
|
+
const terminalDiagnostics = options.terminalDebug
|
|
284
|
+
? await terminalControlDiagnostics(terminalProvider)
|
|
285
|
+
: undefined;
|
|
286
|
+
const terminalControlled = [];
|
|
287
|
+
let activeCount = 0;
|
|
288
|
+
const errors = [];
|
|
289
|
+
try {
|
|
290
|
+
const processSource = terminalListRuntime().createTerminalProcessSource(options);
|
|
291
|
+
const snapshots = await processSource.listProcessSnapshots((snapshot) => adapters.some((adapter) => adapter.capabilities.processDiscovery && adapter.classifyProcess(snapshot) !== undefined), { includeAncestors: true });
|
|
292
|
+
const activeSessions = await bridge.listProcesses(snapshots, adapters.map((adapter) => adapter.agent));
|
|
293
|
+
const rootSessions = selectRootTerminalProcesses(activeSessions);
|
|
294
|
+
const controlledSessions = rootSessions.filter((session) => session.terminalControl !== undefined);
|
|
295
|
+
activeCount = controlledSessions.length;
|
|
296
|
+
const selectedSessions = terminalId
|
|
297
|
+
? controlledSessions.filter((session) => bridge.terminalConversationId(session) === terminalId)
|
|
298
|
+
: controlledSessions;
|
|
299
|
+
for (const session of selectedSessions) {
|
|
300
|
+
try {
|
|
301
|
+
terminalControlled.push(await terminalControlledListEntry(session, activeSessions, options, bridge));
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
errors.push(`terminal process ${session.pid}: ` +
|
|
305
|
+
(error instanceof Error ? error.message : String(error)));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
terminalControlled,
|
|
314
|
+
summary: {
|
|
315
|
+
enabled: true,
|
|
316
|
+
agents: adapters.map((adapter) => adapter.agent),
|
|
317
|
+
active_count: activeCount,
|
|
318
|
+
terminal_count: terminalControlled.length,
|
|
319
|
+
approval_scan: options.noApprovalScan ? "disabled" : "enabled",
|
|
320
|
+
diagnostics: terminalDiagnostics,
|
|
321
|
+
error: errors.length > 0 ? errors.join("; ") : undefined
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
async function terminalControlDiagnostics(provider) {
|
|
326
|
+
return provider.diagnostics();
|
|
327
|
+
}
|
|
328
|
+
function managedTurnListEntry(task, { terminalBridge = false, approvalState, conversation } = {}) {
|
|
329
|
+
return renderManagedTurnListEntry(task, {
|
|
330
|
+
terminalBridge,
|
|
331
|
+
approvalState,
|
|
332
|
+
actionFacts: managedTurnListActionFacts(task, conversation)
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function managedTurnListActionFacts(task, conversation) {
|
|
336
|
+
const nativeTakeover = isRecord(conversation?.native_session_takeover)
|
|
337
|
+
? conversation.native_session_takeover
|
|
338
|
+
: undefined;
|
|
339
|
+
const managedApprovalPending = isRecord(nativeTakeover?.terminal_bridge_approval);
|
|
340
|
+
const terminalBridgeReady = Boolean(conversation &&
|
|
341
|
+
terminalListRuntime().terminalBridgeEnabled(conversation) &&
|
|
342
|
+
terminalListRuntime().terminalControlFromTakeover(nativeTakeover) !== undefined);
|
|
343
|
+
const renewEligible = Boolean(terminalBridgeReady &&
|
|
344
|
+
task.status === "stalled" &&
|
|
345
|
+
terminalListRuntime().terminalBridgeSubmission(conversation)?.status !== "uncertain" &&
|
|
346
|
+
!terminalListRuntime().isVerifiedDeadTerminalAgentProcess(conversation ?? {}));
|
|
347
|
+
const callbackDelivery = isRecord(conversation?.callback_delivery)
|
|
348
|
+
? conversation.callback_delivery
|
|
349
|
+
: undefined;
|
|
350
|
+
const retryCallbackEligible = Boolean(conversation &&
|
|
351
|
+
conversation.legacy_callback_status_error === undefined &&
|
|
352
|
+
terminalListRuntime().callbackRetryDisposition(callbackDelivery).state ===
|
|
353
|
+
"retryable");
|
|
354
|
+
return {
|
|
355
|
+
terminalBridgeReady,
|
|
356
|
+
managedApprovalPending,
|
|
357
|
+
renewEligible,
|
|
358
|
+
retryCallbackEligible
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
async function terminalControlledListEntry(session, activeSessions, options, bridge = terminalListRuntime().createTerminalAgentBridge(options)) {
|
|
362
|
+
const terminalControl = session.terminalControl;
|
|
363
|
+
if (!terminalControl) {
|
|
364
|
+
throw new Error(`process ${session.pid} is not terminal-controlled`);
|
|
365
|
+
}
|
|
366
|
+
const terminalState = await listStateForTerminal(session.agent, terminalControl, options, bridge, {
|
|
367
|
+
pid: session.pid,
|
|
368
|
+
cwd: session.cwd,
|
|
369
|
+
// Raw Codex status and approval use the exact full terminal selector as
|
|
370
|
+
// their runtime conversation identity. Use the same identity while
|
|
371
|
+
// projecting list actions so a prompt-bound terminal token can be
|
|
372
|
+
// revalidated by the later status/approve calls.
|
|
373
|
+
...(session.agent === "codex"
|
|
374
|
+
? { conversationId: bridge.terminalConversationId(session) }
|
|
375
|
+
: { sessionId: session.sessionId }),
|
|
376
|
+
terminalTarget: terminalControl.target
|
|
377
|
+
});
|
|
378
|
+
const { orphanedDispatch, nativeIdentityObservation, nativeAgentIdentity, codexOpenRootRolloutInventory, nativeProcessUuid, nativeProcessBirth, nativeProcessEvidence } = await observeTerminalNativeListIdentity(session, terminalControl, options, bridge);
|
|
379
|
+
const effectiveTerminalState = effectiveTerminalListState({
|
|
380
|
+
session,
|
|
381
|
+
terminalState,
|
|
382
|
+
nativeIdentityObservation,
|
|
383
|
+
nativeAgentIdentity,
|
|
384
|
+
nativeProcessUuid,
|
|
385
|
+
nativeProcessBirth
|
|
386
|
+
});
|
|
387
|
+
const terminalStatusSnapshot = effectiveTerminalState._terminal_status_snapshot
|
|
388
|
+
? terminalBridgeStatusWithActivity(effectiveTerminalState._terminal_status_snapshot, effectiveTerminalState.activity_state, effectiveTerminalState.activity_reason)
|
|
389
|
+
: undefined;
|
|
390
|
+
const statusCardObservation = session.agent === "codex" &&
|
|
391
|
+
typeof effectiveTerminalState.screen_excerpt === "string"
|
|
392
|
+
? bridge.registry.require("codex").observeThreadLifecycle?.({
|
|
393
|
+
operation: { kind: "new_thread" },
|
|
394
|
+
phase: "before",
|
|
395
|
+
screen: effectiveTerminalState.screen_excerpt
|
|
396
|
+
})
|
|
397
|
+
: undefined;
|
|
398
|
+
const statusCardNativeThreadId = statusCardObservation?.status === "observed" &&
|
|
399
|
+
effectiveTerminalState.activity_state === "idle" &&
|
|
400
|
+
effectiveTerminalState.approval_state.blocked !== true &&
|
|
401
|
+
isExactNativeThreadId(statusCardObservation.nativeThreadId)
|
|
402
|
+
? statusCardObservation.nativeThreadId
|
|
403
|
+
: undefined;
|
|
404
|
+
const agentVersion = terminalListRuntime().agentVersionForRunningProcess(session.agent, session.pid, options);
|
|
405
|
+
const lifecycleCapability = bridge.registry.require(session.agent)
|
|
406
|
+
.probeThreadLifecycle?.(agentVersion) ?? {
|
|
407
|
+
status: "unsupported",
|
|
408
|
+
agentVersion,
|
|
409
|
+
newThread: false,
|
|
410
|
+
resumeExact: false,
|
|
411
|
+
reason: "native thread lifecycle is unavailable"
|
|
412
|
+
};
|
|
413
|
+
const nativeInspectionCapability = bridge.registry.require(session.agent)
|
|
414
|
+
.probeNativeInspection?.(agentVersion) ?? {
|
|
415
|
+
status: "unsupported",
|
|
416
|
+
agentVersion,
|
|
417
|
+
statusInspection: false,
|
|
418
|
+
reason: "native inspection is unavailable"
|
|
419
|
+
};
|
|
420
|
+
const codexLatentClearResumeObservationValue = session.agent === "codex"
|
|
421
|
+
? terminalListRuntime().codexLatentClearResumeObservation({
|
|
422
|
+
screen: effectiveTerminalState.screen_excerpt,
|
|
423
|
+
agentVersion
|
|
424
|
+
})
|
|
425
|
+
: undefined;
|
|
426
|
+
const lifecycleBindingToken = unmanagedTerminalBindingToken({
|
|
427
|
+
terminalId: bridge.terminalConversationId(session),
|
|
428
|
+
terminalControl,
|
|
429
|
+
agent: session.agent,
|
|
430
|
+
pid: session.pid,
|
|
431
|
+
workspace: session.cwd ?? terminalControl.currentPath ?? cliCwd(),
|
|
432
|
+
nativeThreadId: nativeAgentIdentity?.sessionId,
|
|
433
|
+
processUuid: nativeProcessUuid,
|
|
434
|
+
processBirth: nativeProcessBirth,
|
|
435
|
+
rollout: nativeAgentIdentity?.rollout
|
|
436
|
+
});
|
|
437
|
+
const codexLifecycleIncarnationAvailable = session.agent !== "codex" ||
|
|
438
|
+
Boolean(nativeProcessUuid && nativeProcessBirth);
|
|
439
|
+
const terminalHasBlockingTurn = terminalIncarnationBlockingTurns(terminalListRuntime().storeDirFromOptions(options), terminalControl).length > 0;
|
|
440
|
+
const automatedInputComposerReady = await observeAutomatedInputComposerReady({
|
|
441
|
+
session,
|
|
442
|
+
terminalControl,
|
|
443
|
+
terminalState: effectiveTerminalState,
|
|
444
|
+
nativeIdentityObservation,
|
|
445
|
+
codexOpenRootRolloutInventory,
|
|
446
|
+
options
|
|
447
|
+
});
|
|
448
|
+
const entry = {
|
|
449
|
+
id: bridge.terminalConversationId(session),
|
|
450
|
+
short_ref: sessionShortRef(bridge.terminalConversationId(session)),
|
|
451
|
+
source: "terminal",
|
|
452
|
+
agent: session.agent,
|
|
453
|
+
process_state: "active",
|
|
454
|
+
pid: session.pid,
|
|
455
|
+
child_pids: childProcessIdsForRoot(session, activeSessions),
|
|
456
|
+
command: session.command,
|
|
457
|
+
cwd: session.cwd,
|
|
458
|
+
workspace: session.cwd,
|
|
459
|
+
elapsed: session.elapsed,
|
|
460
|
+
native_agent_session_id: nativeAgentIdentity?.sessionId,
|
|
461
|
+
native_agent_status_card_session_id: statusCardNativeThreadId,
|
|
462
|
+
native_agent_process_uuid: nativeProcessUuid,
|
|
463
|
+
native_agent_process_birth: nativeProcessBirth,
|
|
464
|
+
native_agent_rollout: nativeAgentIdentity?.rollout,
|
|
465
|
+
native_agent_identity_evidence: nativeProcessEvidence,
|
|
466
|
+
native_agent_identity_observation: {
|
|
467
|
+
status: nativeIdentityObservation.status,
|
|
468
|
+
...(nativeIdentityObservation.status === "unavailable"
|
|
469
|
+
? { reason: nativeIdentityObservation.reason }
|
|
470
|
+
: nativeIdentityObservation.status === "verified_absent"
|
|
471
|
+
? { evidence: nativeIdentityObservation.evidence }
|
|
472
|
+
: {})
|
|
473
|
+
},
|
|
474
|
+
agent_version: agentVersion,
|
|
475
|
+
native_thread_lifecycle: lifecycleCapability,
|
|
476
|
+
native_inspection: nativeInspectionCapability,
|
|
477
|
+
lifecycle_binding_token: lifecycleBindingToken,
|
|
478
|
+
confidence: session.confidence,
|
|
479
|
+
reason: session.reason,
|
|
480
|
+
terminal_control: terminalControl,
|
|
481
|
+
approval_state: effectiveTerminalState.approval_state,
|
|
482
|
+
activity_state: effectiveTerminalState.activity_state,
|
|
483
|
+
activity_reason: effectiveTerminalState.activity_reason,
|
|
484
|
+
// Internal exact-observation evidence. The public projection strips this
|
|
485
|
+
// object; raw terminal status reuses it so screen, approval, and activity
|
|
486
|
+
// all describe the same capture.
|
|
487
|
+
...(terminalStatusSnapshot
|
|
488
|
+
? { _terminal_status_snapshot: terminalStatusSnapshot }
|
|
489
|
+
: {}),
|
|
490
|
+
// Internal action-projection evidence. terminalFirstListProjection strips
|
|
491
|
+
// this field after gating every automated-input action that can follow a
|
|
492
|
+
// human native-thread switch.
|
|
493
|
+
_automated_input_composer_ready: automatedInputComposerReady,
|
|
494
|
+
...(codexLatentClearResumeObservationValue
|
|
495
|
+
? {
|
|
496
|
+
_codex_latent_clear_resume: {
|
|
497
|
+
source_native_thread_id: codexLatentClearResumeObservationValue.sourceNativeThreadId,
|
|
498
|
+
fingerprint: codexLatentClearResumeObservationValue.fingerprint
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
: {}),
|
|
502
|
+
...(codexOpenRootRolloutInventory
|
|
503
|
+
? {
|
|
504
|
+
_codex_open_root_rollout_inventory: codexOpenRootRolloutInventory
|
|
505
|
+
}
|
|
506
|
+
: {}),
|
|
507
|
+
...(orphanedDispatch
|
|
508
|
+
? {
|
|
509
|
+
orphaned_terminal_dispatch: {
|
|
510
|
+
kind: stringValue(orphanedDispatch.kind) ?? "turn",
|
|
511
|
+
status: orphanedDispatch.status,
|
|
512
|
+
owner_conversation_id: stringValue(orphanedDispatch.conversation_id),
|
|
513
|
+
message_id: stringValue(orphanedDispatch.message_id),
|
|
514
|
+
transition_id: stringValue(orphanedDispatch.transition_id),
|
|
515
|
+
recovery: `/akk close ${bridge.terminalConversationId(session)} ` +
|
|
516
|
+
(orphanedDispatch.kind === "lifecycle"
|
|
517
|
+
? `--expected-transition-id ${String(orphanedDispatch.transition_id)}`
|
|
518
|
+
: `--expected-message-id ${String(orphanedDispatch.message_id)}`)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
: {}),
|
|
522
|
+
commands: terminalListCommands({
|
|
523
|
+
agent: session.agent,
|
|
524
|
+
terminalControl,
|
|
525
|
+
terminalState: effectiveTerminalState,
|
|
526
|
+
lifecycleCapability,
|
|
527
|
+
nativeInspectionCapability,
|
|
528
|
+
nativeAgentIdentity,
|
|
529
|
+
nativeProcessUuid,
|
|
530
|
+
nativeProcessBirth,
|
|
531
|
+
codexLifecycleIncarnationAvailable,
|
|
532
|
+
automatedInputComposerReady,
|
|
533
|
+
hasOrphanedDispatch: orphanedDispatch !== undefined,
|
|
534
|
+
terminalHasBlockingTurn
|
|
535
|
+
})
|
|
536
|
+
};
|
|
537
|
+
const availableActions = renderAvailableListActions(entry);
|
|
538
|
+
const { commands: _commands, ...publicEntry } = entry;
|
|
539
|
+
return {
|
|
540
|
+
...publicEntry,
|
|
541
|
+
available_actions: availableActions
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function terminalListCommands(input) {
|
|
545
|
+
const { agent, terminalControl, terminalState, lifecycleCapability, nativeInspectionCapability, nativeAgentIdentity, nativeProcessUuid, nativeProcessBirth, codexLifecycleIncarnationAvailable, automatedInputComposerReady, hasOrphanedDispatch, terminalHasBlockingTurn } = input;
|
|
546
|
+
return {
|
|
547
|
+
send: !terminalHasBlockingTurn,
|
|
548
|
+
approve: terminalControl.capabilities.includes("terminal_approval") &&
|
|
549
|
+
terminalState.approval_state.approvable === true,
|
|
550
|
+
status: true,
|
|
551
|
+
cancel: terminalControl.capabilities.includes("terminal_cancel"),
|
|
552
|
+
close: hasOrphanedDispatch,
|
|
553
|
+
new_thread: lifecycleCapability.status === "supported" &&
|
|
554
|
+
lifecycleCapability.newThread === true &&
|
|
555
|
+
codexLifecycleIncarnationAvailable &&
|
|
556
|
+
!terminalHasBlockingTurn,
|
|
557
|
+
list_resumable_threads: lifecycleCapability.status === "supported" &&
|
|
558
|
+
lifecycleCapability.resumeExact === true &&
|
|
559
|
+
codexLifecycleIncarnationAvailable,
|
|
560
|
+
native_inspect: nativeInspectionCapability.status === "supported" &&
|
|
561
|
+
nativeInspectionCapability.statusInspection === true &&
|
|
562
|
+
terminalState.activity_state === "idle" &&
|
|
563
|
+
automatedInputComposerReady &&
|
|
564
|
+
terminalControl.capabilities.includes("send_keys") &&
|
|
565
|
+
terminalControl.capabilities.includes("screen_status") &&
|
|
566
|
+
(agent === "codex"
|
|
567
|
+
? codexLifecycleIncarnationAvailable
|
|
568
|
+
: Boolean(nativeAgentIdentity?.sessionId &&
|
|
569
|
+
nativeAgentIdentity.processUuid)) &&
|
|
570
|
+
!hasOrphanedDispatch &&
|
|
571
|
+
!terminalHasBlockingTurn,
|
|
572
|
+
watch: lifecycleCapability.status === "supported" &&
|
|
573
|
+
(terminalState.activity_state === "working" ||
|
|
574
|
+
terminalState.activity_state === "awaiting_approval") &&
|
|
575
|
+
Boolean(nativeAgentIdentity?.sessionId) &&
|
|
576
|
+
(agent !== "codex" || Boolean(nativeAgentIdentity?.rollout)) &&
|
|
577
|
+
Boolean(nativeProcessUuid) &&
|
|
578
|
+
Boolean(nativeProcessBirth) &&
|
|
579
|
+
!hasOrphanedDispatch &&
|
|
580
|
+
!terminalHasBlockingTurn
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function effectiveTerminalListState(input) {
|
|
584
|
+
const { session, terminalState, nativeIdentityObservation, nativeAgentIdentity, nativeProcessUuid, nativeProcessBirth } = input;
|
|
585
|
+
if (session.agent !== "codex" ||
|
|
586
|
+
terminalState.activity_state === "working" ||
|
|
587
|
+
terminalState.activity_state === "awaiting_approval" ||
|
|
588
|
+
terminalState.approval_state.blocked === true) {
|
|
589
|
+
return terminalState;
|
|
590
|
+
}
|
|
591
|
+
if (nativeIdentityObservation.status === "verified_absent") {
|
|
592
|
+
return terminalState;
|
|
593
|
+
}
|
|
594
|
+
if (nativeIdentityObservation.status !== "resolved" ||
|
|
595
|
+
!nativeAgentIdentity?.rollout ||
|
|
596
|
+
!nativeAgentIdentity.sessionId ||
|
|
597
|
+
!nativeProcessUuid ||
|
|
598
|
+
!nativeProcessBirth) {
|
|
599
|
+
return terminalListStateWithUnavailableDurableActivity(terminalState, nativeIdentityObservation.status === "unavailable"
|
|
600
|
+
? nativeIdentityObservation.reason ??
|
|
601
|
+
"exact Codex identity observation failed"
|
|
602
|
+
: "exact Codex rollout/process identity is incomplete");
|
|
603
|
+
}
|
|
604
|
+
const currentIdentity = {
|
|
605
|
+
sessionId: nativeAgentIdentity.sessionId,
|
|
606
|
+
processUuid: nativeProcessUuid,
|
|
607
|
+
processBirth: nativeProcessBirth,
|
|
608
|
+
rollout: nativeAgentIdentity.rollout
|
|
609
|
+
};
|
|
610
|
+
try {
|
|
611
|
+
if (!captureCodexHumanStartedActiveTaskAnchor({ currentIdentity })) {
|
|
612
|
+
return terminalState;
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
...terminalState,
|
|
616
|
+
activity_state: "working",
|
|
617
|
+
activity_reason: "Codex rollout contains an exact unfinished human-started task"
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
622
|
+
runtimeLog("warn", "terminal_durable_activity_unavailable", {
|
|
623
|
+
agent: session.agent,
|
|
624
|
+
pid: session.pid,
|
|
625
|
+
reason
|
|
626
|
+
});
|
|
627
|
+
return terminalListStateWithUnavailableDurableActivity(terminalState, reason);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function terminalListStateWithUnavailableDurableActivity(terminalState, reason) {
|
|
631
|
+
return {
|
|
632
|
+
...terminalState,
|
|
633
|
+
activity_state: "unknown",
|
|
634
|
+
activity_reason: `durable Codex activity evidence is unavailable: ${reason}`
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function terminalBridgeStatusWithActivity(status, activityState, activityReason) {
|
|
638
|
+
const descriptors = Object.getOwnPropertyDescriptors(status);
|
|
639
|
+
descriptors.activity_state = {
|
|
640
|
+
configurable: true,
|
|
641
|
+
enumerable: true,
|
|
642
|
+
value: activityState,
|
|
643
|
+
writable: true
|
|
644
|
+
};
|
|
645
|
+
descriptors.activity_reason = {
|
|
646
|
+
configurable: true,
|
|
647
|
+
enumerable: true,
|
|
648
|
+
value: activityReason,
|
|
649
|
+
writable: true
|
|
650
|
+
};
|
|
651
|
+
return Object.create(Object.getPrototypeOf(status), descriptors);
|
|
652
|
+
}
|
|
653
|
+
async function observeTerminalNativeListIdentity(session, terminalControl, options, bridge) {
|
|
654
|
+
const orphanedDispatch = terminalListRuntime().orphanedTerminalDispatchForRecovery(terminalControl);
|
|
655
|
+
let codexIdentityContext;
|
|
656
|
+
if (session.agent === "codex") {
|
|
657
|
+
try {
|
|
658
|
+
codexIdentityContext = terminalListRuntime().codexManagedIdentityResolutionContext({
|
|
659
|
+
storeDir: terminalListRuntime().storeDirFromOptions(options),
|
|
660
|
+
terminal: {
|
|
661
|
+
conversationId: bridge.terminalConversationId(session),
|
|
662
|
+
agent: session.agent,
|
|
663
|
+
pid: session.pid,
|
|
664
|
+
terminalControl
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
// Preserve generic discovery when Store hints are unavailable. Once an
|
|
670
|
+
// exact hint exists, however, the constrained resolver below must fail
|
|
671
|
+
// closed rather than retrying without its managed lineage fences.
|
|
672
|
+
runtimeLog("warn", "terminal_managed_identity_context_unavailable", {
|
|
673
|
+
agent: session.agent,
|
|
674
|
+
terminal_target: terminalControl.target,
|
|
675
|
+
pid: session.pid,
|
|
676
|
+
error: error instanceof Error ? error.message : String(error)
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const nativeIdentityObservation = await terminalListRuntime().observeCurrentNativeAgentSessionIdentity({
|
|
681
|
+
options,
|
|
682
|
+
agent: session.agent,
|
|
683
|
+
pid: session.pid,
|
|
684
|
+
cwd: session.cwd ?? terminalControl.currentPath,
|
|
685
|
+
preferredSessionId: codexIdentityContext?.preferredSessionId,
|
|
686
|
+
allowedCompanionIdentity: codexIdentityContext?.companions.primary,
|
|
687
|
+
allowedAdditionalIdentities: codexIdentityContext?.companions.additional
|
|
688
|
+
});
|
|
689
|
+
const nativeAgentIdentity = nativeIdentityObservation.status === "resolved"
|
|
690
|
+
? nativeIdentityObservation.identity
|
|
691
|
+
: undefined;
|
|
692
|
+
let codexOpenRootRolloutInventory;
|
|
693
|
+
if (session.agent === "codex") {
|
|
694
|
+
try {
|
|
695
|
+
const inventory = await terminalListRuntime().inspectCodexOpenRootRolloutInventory({
|
|
696
|
+
options,
|
|
697
|
+
pid: session.pid,
|
|
698
|
+
cwd: session.cwd ?? terminalControl.currentPath
|
|
699
|
+
});
|
|
700
|
+
if (inventory.roots.length > 0) {
|
|
701
|
+
codexOpenRootRolloutInventory = inventory;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
catch (error) {
|
|
705
|
+
runtimeLog("warn", "terminal_codex_open_root_inventory_unavailable", {
|
|
706
|
+
terminal_target: terminalControl.target,
|
|
707
|
+
pid: session.pid,
|
|
708
|
+
error: error instanceof Error ? error.message : String(error)
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (nativeIdentityObservation.status === "unavailable") {
|
|
713
|
+
runtimeLog("warn", "terminal_native_session_identity_unavailable", {
|
|
714
|
+
agent: session.agent,
|
|
715
|
+
terminal_target: terminalControl.target,
|
|
716
|
+
pid: session.pid,
|
|
717
|
+
error: nativeIdentityObservation.reason
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
let nativeProcessUuid = nativeAgentIdentity?.processUuid;
|
|
721
|
+
let nativeProcessBirth = nativeAgentIdentity?.processBirth;
|
|
722
|
+
let nativeProcessEvidence = nativeAgentIdentity?.evidence;
|
|
723
|
+
if (codexOpenRootRolloutInventory) {
|
|
724
|
+
nativeProcessUuid = codexOpenRootRolloutInventory.processUuid;
|
|
725
|
+
nativeProcessBirth = codexOpenRootRolloutInventory.processBirth;
|
|
726
|
+
nativeProcessEvidence = "codex_open_root_rollout_inventory";
|
|
727
|
+
}
|
|
728
|
+
if (session.agent === "codex" &&
|
|
729
|
+
(!nativeProcessUuid || !nativeProcessBirth)) {
|
|
730
|
+
try {
|
|
731
|
+
const incarnation = terminalListRuntime().codexProcessIncarnationForPid(session.pid);
|
|
732
|
+
nativeProcessUuid = incarnation.processUuid;
|
|
733
|
+
nativeProcessBirth = incarnation.processBirth;
|
|
734
|
+
nativeProcessEvidence = nativeProcessEvidence ?? incarnation.evidence;
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
runtimeLog("warn", "terminal_process_incarnation_unavailable", {
|
|
738
|
+
agent: session.agent,
|
|
739
|
+
terminal_target: terminalControl.target,
|
|
740
|
+
pid: session.pid,
|
|
741
|
+
error: error instanceof Error ? error.message : String(error)
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
orphanedDispatch,
|
|
747
|
+
nativeIdentityObservation,
|
|
748
|
+
nativeAgentIdentity,
|
|
749
|
+
codexOpenRootRolloutInventory,
|
|
750
|
+
nativeProcessUuid,
|
|
751
|
+
nativeProcessBirth,
|
|
752
|
+
nativeProcessEvidence
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
async function observeAutomatedInputComposerReady({ session, terminalControl, terminalState, nativeIdentityObservation, codexOpenRootRolloutInventory, options }) {
|
|
756
|
+
let ready = terminalListRuntime().nativeInspectionComposerEmpty(session.agent, terminalState.screen_excerpt);
|
|
757
|
+
if (session.agent !== "codex" ||
|
|
758
|
+
(terminalState.activity_state !== "idle" &&
|
|
759
|
+
(terminalState.activity_state !== "unknown" ||
|
|
760
|
+
(nativeIdentityObservation.status !== "verified_absent" &&
|
|
761
|
+
codexOpenRootRolloutInventory === undefined))) ||
|
|
762
|
+
terminalState.approval_state.blocked === true ||
|
|
763
|
+
!terminalControl.capabilities.includes("send_keys") ||
|
|
764
|
+
!terminalControl.capabilities.includes("screen_status")) {
|
|
765
|
+
return ready;
|
|
766
|
+
}
|
|
767
|
+
try {
|
|
768
|
+
const provider = terminalListRuntime().createTerminalControlProvider(options);
|
|
769
|
+
const resolvedTerminal = await provider.resolve(provider.endpoint(terminalControl));
|
|
770
|
+
const styledScreen = await provider.capture(resolvedTerminal, { scrollbackLines: 40, preserveEscapes: true });
|
|
771
|
+
ready = exactCodexReadyStyledComposerCapture(styledScreen) !== undefined;
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
// Advertising an input action is optional. The action itself repeats the
|
|
775
|
+
// same styled composer proof under the terminal lock before any input.
|
|
776
|
+
ready = false;
|
|
777
|
+
}
|
|
778
|
+
return ready;
|
|
779
|
+
}
|
|
780
|
+
function observeTerminalListBindingAuthority(terminal, context) {
|
|
781
|
+
const { storeDir, managedSessions, allConversations, displayedConversations, mutationsAllowed, nonterminalDeferredTransfers } = context;
|
|
782
|
+
const { _automated_input_composer_ready: automatedInputComposerReady, _codex_open_root_rollout_inventory: codexOpenRootRolloutInventoryValue, _codex_latent_clear_resume: codexLatentClearResumeValue, _terminal_status_snapshot: _terminalStatusSnapshot, ...publicTerminal } = terminal;
|
|
783
|
+
const codexOpenRootRolloutInventory = isRecord(codexOpenRootRolloutInventoryValue)
|
|
784
|
+
? codexOpenRootRolloutInventoryValue
|
|
785
|
+
: undefined;
|
|
786
|
+
const terminalControl = isRecord(terminal.terminal_control)
|
|
787
|
+
? terminal.terminal_control
|
|
788
|
+
: undefined;
|
|
789
|
+
const terminalHasNonterminalDeferredTransfer = Boolean(terminalControl && nonterminalDeferredTransfers.some((transfer) => transfer.terminal_id === String(terminal.id) &&
|
|
790
|
+
transfer.process_pid === Number(terminal.pid) &&
|
|
791
|
+
terminalControlEvidenceMatches(transfer.terminal_endpoint, terminalControl)));
|
|
792
|
+
const allRelated = terminalControl
|
|
793
|
+
? allConversations.filter((conversation) => terminalControlsShareIncarnation(terminalControlForManagedConversation(conversation), terminalControl))
|
|
794
|
+
: [];
|
|
795
|
+
const displayedRelated = terminalControl
|
|
796
|
+
? displayedConversations.filter((conversation) => terminalControlsShareIncarnation(terminalControlForManagedConversation(conversation), terminalControl))
|
|
797
|
+
: [];
|
|
798
|
+
const relatedSessions = terminalControl
|
|
799
|
+
? managedSessions.filter((session) => terminalControlsShareIncarnation(session.binding?.terminal_control, terminalControl))
|
|
800
|
+
: [];
|
|
801
|
+
const matchingSessions = relatedSessions.filter((session) => managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir));
|
|
802
|
+
const conflictingBoundSessionClaims = relatedSessions.flatMap((session) => {
|
|
803
|
+
const kind = managedBindingConflictKindForLiveTerminalEntry({
|
|
804
|
+
storeDir,
|
|
805
|
+
session,
|
|
806
|
+
terminal
|
|
807
|
+
});
|
|
808
|
+
return kind && kind !== "stale_process_incarnation"
|
|
809
|
+
? [{ session, kind }]
|
|
810
|
+
: [];
|
|
811
|
+
});
|
|
812
|
+
const unresolvedSessionClaims = relatedSessions.filter((session) => ["transitioning", "quarantined"].includes(session.status) &&
|
|
813
|
+
managedSessionClaimsLiveTerminalEntry(session, terminal));
|
|
814
|
+
const sessionAuthorityConflict = decideTerminalSessionAuthorityConflict({
|
|
815
|
+
unresolvedSessionClaims,
|
|
816
|
+
conflictingBoundSessionClaims,
|
|
817
|
+
matchingSessions
|
|
818
|
+
});
|
|
819
|
+
const authoritativeSession = matchingSessions[0];
|
|
820
|
+
const discoveredOwnership = terminalControl
|
|
821
|
+
? terminalDispatchOwnership(terminalControl)
|
|
822
|
+
: { state: "none" };
|
|
823
|
+
const localOwnership = discoveredOwnership.state === "current"
|
|
824
|
+
? localTerminalDispatchOwnership(discoveredOwnership.conversation, allRelated, terminal)
|
|
825
|
+
: discoveredOwnership;
|
|
826
|
+
const dispatchOwnerMismatch = !sessionAuthorityConflict &&
|
|
827
|
+
localOwnership.state === "current" &&
|
|
828
|
+
authoritativeSession &&
|
|
829
|
+
sessionIdForConversation(localOwnership.conversation) !==
|
|
830
|
+
authoritativeSession.session_id
|
|
831
|
+
? {
|
|
832
|
+
ownerSessionId: sessionIdForConversation(discoveredOwnership.state === "current"
|
|
833
|
+
? discoveredOwnership.conversation
|
|
834
|
+
: localOwnership.conversation)
|
|
835
|
+
}
|
|
836
|
+
: undefined;
|
|
837
|
+
const ownership = applySessionAuthorityToDispatch({
|
|
838
|
+
localOwnership,
|
|
839
|
+
sessionAuthorityConflict,
|
|
840
|
+
authoritativeSession,
|
|
841
|
+
dispatchOwnerMismatch
|
|
842
|
+
});
|
|
843
|
+
const discoveredRawActions = isRecord(terminal.available_actions)
|
|
844
|
+
? terminal.available_actions
|
|
845
|
+
: {};
|
|
846
|
+
const rawActions = mutationsAllowed
|
|
847
|
+
? discoveredRawActions
|
|
848
|
+
: readOnlyListActions(discoveredRawActions);
|
|
849
|
+
const rawSendAction = isRecord(rawActions.send)
|
|
850
|
+
? rawActions.send
|
|
851
|
+
: {};
|
|
852
|
+
const bindingAwareRawActions = authoritativeSession
|
|
853
|
+
? actionsForManagedSessionBinding(rawActions, authoritativeSession)
|
|
854
|
+
: rawActions;
|
|
855
|
+
const sessionAwareRawActionsBase = authoritativeSession &&
|
|
856
|
+
managedSessionHasUnresolvedNativeTransition(storeDir, authoritativeSession)
|
|
857
|
+
? Object.fromEntries(Object.entries(bindingAwareRawActions).filter(([actionName]) => actionName !== "native_inspect"))
|
|
858
|
+
: bindingAwareRawActions;
|
|
859
|
+
const sessionAwareRawActions = terminalHasNonterminalDeferredTransfer
|
|
860
|
+
? readOnlyListActions(sessionAwareRawActionsBase)
|
|
861
|
+
: sessionAwareRawActionsBase;
|
|
862
|
+
const soleBindingConflict = conflictingBoundSessionClaims.length === 1
|
|
863
|
+
? conflictingBoundSessionClaims[0]
|
|
864
|
+
: undefined;
|
|
865
|
+
const externalHandoffDetected = conflictingBoundSessionClaims.some(({ kind }) => kind === "live_external_thread_change");
|
|
866
|
+
const conflictingSessionRevision = Number(soleBindingConflict?.session.revision);
|
|
867
|
+
const conflictingSessionTurns = soleBindingConflict
|
|
868
|
+
? terminalListRuntime().managedTurnsForSession(storeDir, soleBindingConflict.session.session_id)
|
|
869
|
+
: [];
|
|
870
|
+
const expectedTerminalToken = stringValue(terminal.lifecycle_binding_token);
|
|
871
|
+
const externalHandoffNativeThreadId = stringValue(terminal.native_agent_status_card_session_id) ?? stringValue(terminal.native_agent_session_id);
|
|
872
|
+
const resolvedNativeThreadId = stringValue(terminal.native_agent_session_id);
|
|
873
|
+
const externalHandoffTerminalToken = terminalControl &&
|
|
874
|
+
externalHandoffNativeThreadId &&
|
|
875
|
+
isExactNativeThreadId(externalHandoffNativeThreadId)
|
|
876
|
+
? unmanagedTerminalBindingToken({
|
|
877
|
+
terminalId: stringValue(terminal.id),
|
|
878
|
+
terminalControl,
|
|
879
|
+
agent: terminal.agent,
|
|
880
|
+
pid: Number(terminal.pid),
|
|
881
|
+
workspace: terminal.workspace ?? terminal.cwd ?? cliCwd(),
|
|
882
|
+
nativeThreadId: externalHandoffNativeThreadId,
|
|
883
|
+
processUuid: stringValue(terminal.native_agent_process_uuid),
|
|
884
|
+
processBirth: stringValue(terminal.native_agent_process_birth),
|
|
885
|
+
rollout: resolvedNativeThreadId === externalHandoffNativeThreadId &&
|
|
886
|
+
isRecord(terminal.native_agent_rollout)
|
|
887
|
+
? terminal.native_agent_rollout
|
|
888
|
+
: undefined
|
|
889
|
+
})
|
|
890
|
+
: undefined;
|
|
891
|
+
const externalHandoffTarget = soleBindingConflict?.kind === "live_external_thread_change" &&
|
|
892
|
+
externalHandoffNativeThreadId &&
|
|
893
|
+
isExactNativeThreadId(externalHandoffNativeThreadId)
|
|
894
|
+
? terminalListRuntime().observedHandoffTargetResolution({
|
|
895
|
+
storeDir,
|
|
896
|
+
agent: terminal.agent,
|
|
897
|
+
workspace: terminal.workspace ?? terminal.cwd ?? cliCwd(),
|
|
898
|
+
nativeThreadId: externalHandoffNativeThreadId.toLowerCase(),
|
|
899
|
+
sourceSessionId: soleBindingConflict.session.session_id
|
|
900
|
+
})
|
|
901
|
+
: undefined;
|
|
902
|
+
const externalHandoffSnapshotToken = externalHandoffTerminalToken &&
|
|
903
|
+
soleBindingConflict?.kind === "live_external_thread_change" &&
|
|
904
|
+
externalHandoffTarget?.status === "eligible"
|
|
905
|
+
? humanObservedHandoffBindingToken({
|
|
906
|
+
terminal_token: externalHandoffTerminalToken,
|
|
907
|
+
source_session_id: soleBindingConflict.session.session_id,
|
|
908
|
+
source_revision: managedSessionRevision(soleBindingConflict.session),
|
|
909
|
+
source_binding_token: managedSessionBindingToken(soleBindingConflict.session),
|
|
910
|
+
target: externalHandoffTarget.snapshot
|
|
911
|
+
})
|
|
912
|
+
: undefined;
|
|
913
|
+
const blockingHandoffTurns = conflictingSessionTurns.filter((turn) => isSessionSendBlockingStatus(turn.status));
|
|
914
|
+
const terminalBlockingTurns = terminalControl
|
|
915
|
+
? terminalIncarnationBlockingTurns(storeDir, terminalControl, allRelated)
|
|
916
|
+
: [];
|
|
917
|
+
const externalHandoffSourceSessionIds = new Set(conflictingBoundSessionClaims
|
|
918
|
+
.filter(({ kind }) => kind === "live_external_thread_change")
|
|
919
|
+
.map(({ session }) => session.session_id));
|
|
920
|
+
const handoffSourceBlockingTurns = terminalBlockingTurns.filter((turn) => externalHandoffSourceSessionIds.has(sessionIdForConversation(turn)));
|
|
921
|
+
const nativeIdentityObservation = isRecord(terminal.native_agent_identity_observation)
|
|
922
|
+
? terminal.native_agent_identity_observation
|
|
923
|
+
: undefined;
|
|
924
|
+
const codexProcessUuid = stringValue(terminal.native_agent_process_uuid);
|
|
925
|
+
const codexProcessBirth = stringValue(terminal.native_agent_process_birth);
|
|
926
|
+
const codexWorkspace = stringValue(terminal.workspace ?? terminal.cwd);
|
|
927
|
+
const codexSendAuthorityContext = terminalControl
|
|
928
|
+
? {
|
|
929
|
+
terminalId: String(terminal.id),
|
|
930
|
+
terminalControl,
|
|
931
|
+
pid: Number(terminal.pid),
|
|
932
|
+
workspace: codexWorkspace,
|
|
933
|
+
liveProcessUuid: codexProcessUuid,
|
|
934
|
+
liveProcessBirth: codexProcessBirth
|
|
935
|
+
}
|
|
936
|
+
: undefined;
|
|
937
|
+
return {
|
|
938
|
+
automatedInputComposerReady,
|
|
939
|
+
codexOpenRootRolloutInventory,
|
|
940
|
+
codexLatentClearResumeValue,
|
|
941
|
+
publicTerminal,
|
|
942
|
+
terminalControl,
|
|
943
|
+
terminalHasNonterminalDeferredTransfer,
|
|
944
|
+
allRelated,
|
|
945
|
+
displayedRelated,
|
|
946
|
+
relatedSessions,
|
|
947
|
+
matchingSessions,
|
|
948
|
+
conflictingBoundSessionClaims,
|
|
949
|
+
unresolvedSessionClaims,
|
|
950
|
+
sessionAuthorityConflict,
|
|
951
|
+
authoritativeSession,
|
|
952
|
+
discoveredOwnership,
|
|
953
|
+
ownership,
|
|
954
|
+
rawActions,
|
|
955
|
+
rawSendAction,
|
|
956
|
+
sessionAwareRawActions,
|
|
957
|
+
soleBindingConflict,
|
|
958
|
+
externalHandoffDetected,
|
|
959
|
+
conflictingSessionRevision,
|
|
960
|
+
conflictingSessionTurns,
|
|
961
|
+
expectedTerminalToken,
|
|
962
|
+
externalHandoffNativeThreadId,
|
|
963
|
+
externalHandoffTarget,
|
|
964
|
+
externalHandoffSnapshotToken,
|
|
965
|
+
blockingHandoffTurns,
|
|
966
|
+
terminalBlockingTurns,
|
|
967
|
+
handoffSourceBlockingTurns,
|
|
968
|
+
nativeIdentityObservation,
|
|
969
|
+
codexProcessUuid,
|
|
970
|
+
codexProcessBirth,
|
|
971
|
+
codexWorkspace,
|
|
972
|
+
codexSendAuthorityContext
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
function observeVerifiedEmptyTerminalAuthority(terminal, context, binding) {
|
|
976
|
+
const { storeDir, terminals, mutationsAllowed } = context;
|
|
977
|
+
const { automatedInputComposerReady, terminalControl, matchingSessions, unresolvedSessionClaims, discoveredOwnership, rawActions, soleBindingConflict, blockingHandoffTurns, terminalBlockingTurns, nativeIdentityObservation, codexProcessUuid, codexProcessBirth, codexWorkspace, codexSendAuthorityContext } = binding;
|
|
978
|
+
const verifiedEmptySourceNativeThreadId = stringValue(soleBindingConflict?.session.binding?.native_thread_id)?.toLowerCase();
|
|
979
|
+
const verifiedEmptySourceActiveElsewhere = Boolean(verifiedEmptySourceNativeThreadId &&
|
|
980
|
+
terminals.some((candidate) => candidate !== terminal &&
|
|
981
|
+
candidate.agent === "codex" &&
|
|
982
|
+
stringValue(candidate.native_agent_session_id)?.toLowerCase() ===
|
|
983
|
+
verifiedEmptySourceNativeThreadId));
|
|
984
|
+
const verifiedEmptyRawSendAction = isRecord(rawActions.send)
|
|
985
|
+
? rawActions.send
|
|
986
|
+
: {
|
|
987
|
+
tool: "agent_knock_knock_send",
|
|
988
|
+
arguments: { selector: stringValue(terminal.id) },
|
|
989
|
+
missing_required: ["request"]
|
|
990
|
+
};
|
|
991
|
+
const verifiedEmptyCodexHandoffEligible = Boolean(mutationsAllowed &&
|
|
992
|
+
terminal.agent === "codex" &&
|
|
993
|
+
terminalControl &&
|
|
994
|
+
discoveredOwnership.state === "none" &&
|
|
995
|
+
unresolvedSessionClaims.length === 0 &&
|
|
996
|
+
matchingSessions.length === 0 &&
|
|
997
|
+
soleBindingConflict?.kind === "unverifiable" &&
|
|
998
|
+
nativeIdentityObservation?.status === "verified_absent" &&
|
|
999
|
+
codexSendAuthorityContext &&
|
|
1000
|
+
codexProcessUuid &&
|
|
1001
|
+
codexProcessBirth &&
|
|
1002
|
+
codexWorkspace &&
|
|
1003
|
+
exactBoundCodexSendSource({
|
|
1004
|
+
kind: "verified_empty",
|
|
1005
|
+
sourceSession: soleBindingConflict.session,
|
|
1006
|
+
context: codexSendAuthorityContext
|
|
1007
|
+
}) &&
|
|
1008
|
+
["idle", "unknown"].includes(String(terminal.activity_state)) &&
|
|
1009
|
+
automatedInputComposerReady === true &&
|
|
1010
|
+
!(isRecord(terminal.approval_state) &&
|
|
1011
|
+
terminal.approval_state.blocked === true) &&
|
|
1012
|
+
blockingHandoffTurns.length === 0 &&
|
|
1013
|
+
terminalBlockingTurns.length === 0 &&
|
|
1014
|
+
!managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
|
|
1015
|
+
!verifiedEmptySourceActiveElsewhere &&
|
|
1016
|
+
terminal.orphaned_terminal_dispatch === undefined &&
|
|
1017
|
+
terminalControl?.capabilities.includes("send_keys") &&
|
|
1018
|
+
terminalControl.capabilities.includes("screen_status"));
|
|
1019
|
+
const verifiedEmptyCodexSnapshotToken = verifiedEmptyCodexHandoffEligible &&
|
|
1020
|
+
codexSendAuthorityContext &&
|
|
1021
|
+
soleBindingConflict &&
|
|
1022
|
+
codexProcessUuid &&
|
|
1023
|
+
codexProcessBirth &&
|
|
1024
|
+
codexWorkspace
|
|
1025
|
+
? verifiedEmptyCodexHandoffToken({
|
|
1026
|
+
terminalId: String(terminal.id),
|
|
1027
|
+
terminalControl: codexSendAuthorityContext.terminalControl,
|
|
1028
|
+
pid: Number(terminal.pid),
|
|
1029
|
+
workspace: codexWorkspace,
|
|
1030
|
+
processUuid: codexProcessUuid,
|
|
1031
|
+
processBirth: codexProcessBirth,
|
|
1032
|
+
sourceSession: soleBindingConflict.session
|
|
1033
|
+
})
|
|
1034
|
+
: undefined;
|
|
1035
|
+
return {
|
|
1036
|
+
...binding,
|
|
1037
|
+
verifiedEmptyRawSendAction,
|
|
1038
|
+
verifiedEmptyCodexSnapshotToken
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
function observeDeferredSourceAuthority(terminal, context, observation) {
|
|
1042
|
+
const { terminals } = context;
|
|
1043
|
+
const { codexLatentClearResumeValue, codexOpenRootRolloutInventory, authoritativeSession, matchingSessions, conflictingBoundSessionClaims, unresolvedSessionClaims, soleBindingConflict, nativeIdentityObservation } = observation;
|
|
1044
|
+
const deferredCodexCandidateInventory = codexOpenRootRolloutInventory &&
|
|
1045
|
+
codexOpenRootRolloutInventory.roots.length > 0
|
|
1046
|
+
? codexOpenRootRolloutInventory
|
|
1047
|
+
: undefined;
|
|
1048
|
+
const abandonedConflictSource = !authoritativeSession &&
|
|
1049
|
+
soleBindingConflict?.kind === "unverifiable" &&
|
|
1050
|
+
matchingSessions.length === 0 &&
|
|
1051
|
+
conflictingBoundSessionClaims.length === 1 &&
|
|
1052
|
+
unresolvedSessionClaims.length === 0 &&
|
|
1053
|
+
terminal.agent === "codex"
|
|
1054
|
+
? soleBindingConflict.session
|
|
1055
|
+
: undefined;
|
|
1056
|
+
const deferredCodexSource = (authoritativeSession ??
|
|
1057
|
+
abandonedConflictSource) &&
|
|
1058
|
+
terminal.agent === "codex" &&
|
|
1059
|
+
(nativeIdentityObservation?.status === "verified_absent" ||
|
|
1060
|
+
deferredCodexCandidateInventory !== undefined)
|
|
1061
|
+
? authoritativeSession ?? abandonedConflictSource
|
|
1062
|
+
: undefined;
|
|
1063
|
+
const deferredCodexSourceNativeThreadId = stringValue(deferredCodexSource?.binding?.native_thread_id)?.toLowerCase();
|
|
1064
|
+
const deferredCodexLatentClearResumeFingerprint = isRecord(codexLatentClearResumeValue) &&
|
|
1065
|
+
stringValue(codexLatentClearResumeValue.source_native_thread_id)?.toLowerCase() === deferredCodexSourceNativeThreadId
|
|
1066
|
+
? stringValue(codexLatentClearResumeValue.fingerprint)
|
|
1067
|
+
: undefined;
|
|
1068
|
+
if (deferredCodexLatentClearResumeFingerprint) {
|
|
1069
|
+
// The resume hint is useful operational context, but it is not durable
|
|
1070
|
+
// foreground authority: it can scroll away while the latent thread is
|
|
1071
|
+
// still current. Candidate routing and its token rely on the complete
|
|
1072
|
+
// rollout inventory and Store authority below instead.
|
|
1073
|
+
runtimeLog("info", "terminal_codex_latent_clear_hint_observed", {
|
|
1074
|
+
terminal_id: String(terminal.id),
|
|
1075
|
+
source_session_id: deferredCodexSource?.session_id,
|
|
1076
|
+
source_native_thread_id: deferredCodexSourceNativeThreadId
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
const deferredCodexSourceActiveElsewhere = Boolean(deferredCodexSourceNativeThreadId &&
|
|
1080
|
+
terminals.some((candidate) => candidate !== terminal &&
|
|
1081
|
+
candidate.agent === "codex" &&
|
|
1082
|
+
stringValue(candidate.native_agent_session_id)?.toLowerCase() ===
|
|
1083
|
+
deferredCodexSourceNativeThreadId));
|
|
1084
|
+
return {
|
|
1085
|
+
...observation,
|
|
1086
|
+
deferredCodexCandidateInventory,
|
|
1087
|
+
abandonedConflictSource,
|
|
1088
|
+
deferredCodexSource,
|
|
1089
|
+
deferredCodexSourceActiveElsewhere
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
function observeDeferredTerminalAuthority(terminal, context, observation) {
|
|
1093
|
+
const { storeDir, mutationsAllowed } = context;
|
|
1094
|
+
const { automatedInputComposerReady, terminalControl, terminalHasNonterminalDeferredTransfer, matchingSessions, conflictingBoundSessionClaims, unresolvedSessionClaims, discoveredOwnership, terminalBlockingTurns, codexProcessUuid, codexProcessBirth, codexWorkspace, codexSendAuthorityContext, deferredCodexCandidateInventory, abandonedConflictSource, deferredCodexSource, deferredCodexSourceActiveElsewhere } = observation;
|
|
1095
|
+
const deferredCodexAuthority = codexSendAuthorityContext
|
|
1096
|
+
? terminalListRuntime().observeDeferredCodexAuthority({
|
|
1097
|
+
mode: "list",
|
|
1098
|
+
storeDir,
|
|
1099
|
+
context: codexSendAuthorityContext,
|
|
1100
|
+
sourceSession: deferredCodexSource,
|
|
1101
|
+
candidateInventory: deferredCodexCandidateInventory,
|
|
1102
|
+
abandonment: abandonedConflictSource
|
|
1103
|
+
? "missing_rollout"
|
|
1104
|
+
: "never",
|
|
1105
|
+
requireUnclaimedCandidate: true
|
|
1106
|
+
})
|
|
1107
|
+
: undefined;
|
|
1108
|
+
const deferredCodexSourceRolloutAuthority = deferredCodexAuthority?.sourceRolloutAuthority ?? "present";
|
|
1109
|
+
const deferredCodexDispatchSnapshot = deferredCodexAuthority?.dispatchSnapshot;
|
|
1110
|
+
const deferredCodexForegroundEligible = Boolean(mutationsAllowed &&
|
|
1111
|
+
!terminalHasNonterminalDeferredTransfer &&
|
|
1112
|
+
deferredCodexSource &&
|
|
1113
|
+
terminalControl &&
|
|
1114
|
+
hasCanonicalTerminalEndpoint(terminalControl) &&
|
|
1115
|
+
discoveredOwnership.state === "none" &&
|
|
1116
|
+
unresolvedSessionClaims.length === 0 &&
|
|
1117
|
+
((matchingSessions.length === 1 &&
|
|
1118
|
+
conflictingBoundSessionClaims.length === 0) ||
|
|
1119
|
+
(deferredCodexSourceRolloutAuthority ===
|
|
1120
|
+
"explicitly_abandoned_predecessor" &&
|
|
1121
|
+
matchingSessions.length === 0 &&
|
|
1122
|
+
conflictingBoundSessionClaims.length === 1)) &&
|
|
1123
|
+
codexProcessUuid &&
|
|
1124
|
+
codexProcessBirth &&
|
|
1125
|
+
codexWorkspace &&
|
|
1126
|
+
deferredCodexAuthority?.exactSource &&
|
|
1127
|
+
terminalBlockingTurns.length === 0 &&
|
|
1128
|
+
terminal.orphaned_terminal_dispatch === undefined &&
|
|
1129
|
+
deferredCodexDispatchSnapshot &&
|
|
1130
|
+
["idle", "unknown"].includes(String(terminal.activity_state)) &&
|
|
1131
|
+
automatedInputComposerReady === true &&
|
|
1132
|
+
!(isRecord(terminal.approval_state) &&
|
|
1133
|
+
terminal.approval_state.blocked === true) &&
|
|
1134
|
+
!deferredCodexSourceActiveElsewhere &&
|
|
1135
|
+
terminalControl.capabilities.includes("send_keys") &&
|
|
1136
|
+
terminalControl.capabilities.includes("screen_status"));
|
|
1137
|
+
const deferredCodexForegroundToken = deferredCodexForegroundEligible &&
|
|
1138
|
+
deferredCodexSource &&
|
|
1139
|
+
terminalControl &&
|
|
1140
|
+
codexProcessUuid &&
|
|
1141
|
+
codexProcessBirth &&
|
|
1142
|
+
codexWorkspace &&
|
|
1143
|
+
deferredCodexDispatchSnapshot
|
|
1144
|
+
? deferredCodexForegroundBindingToken({
|
|
1145
|
+
terminalId: String(terminal.id),
|
|
1146
|
+
terminalControl,
|
|
1147
|
+
pid: Number(terminal.pid),
|
|
1148
|
+
workspace: codexWorkspace,
|
|
1149
|
+
processUuid: codexProcessUuid,
|
|
1150
|
+
processBirth: codexProcessBirth,
|
|
1151
|
+
sourceSession: deferredCodexSource,
|
|
1152
|
+
dispatchSnapshot: deferredCodexDispatchSnapshot,
|
|
1153
|
+
sourceTurnHistory: deferredCodexAuthority?.sourceTurnHistory,
|
|
1154
|
+
sourceRolloutAuthority: deferredCodexSourceRolloutAuthority,
|
|
1155
|
+
sourceAbandonmentFingerprint: deferredCodexAuthority?.sourceAbandonmentFingerprint,
|
|
1156
|
+
...(deferredCodexCandidateInventory
|
|
1157
|
+
? { candidateInventory: deferredCodexCandidateInventory }
|
|
1158
|
+
: {})
|
|
1159
|
+
})
|
|
1160
|
+
: undefined;
|
|
1161
|
+
return {
|
|
1162
|
+
...observation,
|
|
1163
|
+
deferredCodexSourceRolloutAuthority,
|
|
1164
|
+
deferredCodexForegroundToken
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
function observeTerminalHandoffAuthority(terminal, context, observation) {
|
|
1168
|
+
const { storeDir, mutationsAllowed, conversationHasNonterminalDeferredTransfer } = context;
|
|
1169
|
+
const { automatedInputComposerReady, terminalControl, matchingSessions, unresolvedSessionClaims, discoveredOwnership, rawActions, soleBindingConflict, conflictingSessionRevision, conflictingSessionTurns, expectedTerminalToken, externalHandoffNativeThreadId, externalHandoffTarget, externalHandoffSnapshotToken, blockingHandoffTurns, terminalBlockingTurns, handoffSourceBlockingTurns } = observation;
|
|
1170
|
+
const reconcileBindingAction = mutationsAllowed &&
|
|
1171
|
+
discoveredOwnership.state === "none" &&
|
|
1172
|
+
unresolvedSessionClaims.length === 0 &&
|
|
1173
|
+
matchingSessions.length === 0 &&
|
|
1174
|
+
soleBindingConflict &&
|
|
1175
|
+
soleBindingConflict.kind !== "unverifiable" &&
|
|
1176
|
+
Number.isSafeInteger(conflictingSessionRevision) &&
|
|
1177
|
+
conflictingSessionRevision > 0 &&
|
|
1178
|
+
expectedTerminalToken &&
|
|
1179
|
+
terminal.activity_state === "idle" &&
|
|
1180
|
+
!(isRecord(terminal.approval_state) &&
|
|
1181
|
+
terminal.approval_state.blocked === true) &&
|
|
1182
|
+
!conflictingSessionTurns.some((turn) => isSessionSendBlockingStatus(turn.status)) &&
|
|
1183
|
+
terminalBlockingTurns.length === 0 &&
|
|
1184
|
+
!managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session)
|
|
1185
|
+
? projectReconcileBindingAction({
|
|
1186
|
+
terminalId: stringValue(terminal.id),
|
|
1187
|
+
conflictingSession: soleBindingConflict.session,
|
|
1188
|
+
conflictingSessionRevision,
|
|
1189
|
+
expectedTerminalToken
|
|
1190
|
+
})
|
|
1191
|
+
: undefined;
|
|
1192
|
+
const externalHandoffAdoptable = Boolean(mutationsAllowed &&
|
|
1193
|
+
discoveredOwnership.state === "none" &&
|
|
1194
|
+
unresolvedSessionClaims.length === 0 &&
|
|
1195
|
+
soleBindingConflict?.kind === "live_external_thread_change" &&
|
|
1196
|
+
terminal.activity_state === "idle" &&
|
|
1197
|
+
automatedInputComposerReady === true &&
|
|
1198
|
+
!(isRecord(terminal.approval_state) &&
|
|
1199
|
+
terminal.approval_state.blocked === true) &&
|
|
1200
|
+
!conflictingSessionTurns.some((turn) => isSessionSendBlockingStatus(turn.status)) &&
|
|
1201
|
+
terminalBlockingTurns.length === 0 &&
|
|
1202
|
+
!managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
|
|
1203
|
+
externalHandoffTarget?.status === "eligible" &&
|
|
1204
|
+
isRecord(rawActions.send) &&
|
|
1205
|
+
Boolean(externalHandoffSnapshotToken));
|
|
1206
|
+
const handoffDecisionTurn = mutationsAllowed &&
|
|
1207
|
+
soleBindingConflict?.kind === "live_external_thread_change" &&
|
|
1208
|
+
externalHandoffTarget?.status === "eligible" &&
|
|
1209
|
+
terminal.activity_state === "idle" &&
|
|
1210
|
+
!(isRecord(terminal.approval_state) &&
|
|
1211
|
+
terminal.approval_state.blocked === true) &&
|
|
1212
|
+
!managedSessionHasUnresolvedNativeTransition(storeDir, soleBindingConflict.session) &&
|
|
1213
|
+
blockingHandoffTurns.length === 1 &&
|
|
1214
|
+
terminalBlockingTurns.every((turn) => turn.conversation_id === blockingHandoffTurns[0].conversation_id)
|
|
1215
|
+
? blockingHandoffTurns[0]
|
|
1216
|
+
: undefined;
|
|
1217
|
+
const handoffDecisionToken = handoffDecisionTurn &&
|
|
1218
|
+
externalHandoffSnapshotToken &&
|
|
1219
|
+
terminalControl
|
|
1220
|
+
? terminalListRuntime().activeTurnHandoffDecisionToken({
|
|
1221
|
+
handoffToken: externalHandoffSnapshotToken,
|
|
1222
|
+
turn: handoffDecisionTurn,
|
|
1223
|
+
ledger: terminalListRuntime().loadTerminalBridgeDispatchLedger(terminalControl)
|
|
1224
|
+
})
|
|
1225
|
+
: undefined;
|
|
1226
|
+
const handoffDecision = handoffDecisionTurn &&
|
|
1227
|
+
handoffDecisionToken &&
|
|
1228
|
+
externalHandoffNativeThreadId
|
|
1229
|
+
? projectHandoffDecision({
|
|
1230
|
+
sourceSessionId: soleBindingConflict?.session.session_id,
|
|
1231
|
+
sourceTurnId: turnIdForConversation(handoffDecisionTurn),
|
|
1232
|
+
liveNativeThreadId: externalHandoffNativeThreadId,
|
|
1233
|
+
handoffDecisionToken,
|
|
1234
|
+
actionTurnId: turnIdForConversation(handoffDecisionTurn)
|
|
1235
|
+
})
|
|
1236
|
+
: undefined;
|
|
1237
|
+
const blockingHandoffTurnIds = new Set(handoffSourceBlockingTurns.map((turn) => turn.conversation_id));
|
|
1238
|
+
// An active source Turn may be superseded only through the snapshot-bound
|
|
1239
|
+
// handoff decision above. Never expose the generic Store-only close action
|
|
1240
|
+
// for that Turn, because doing so would bypass expected_handoff_token after
|
|
1241
|
+
// the live native thread or Turn generation changed. `blocking_turns` is
|
|
1242
|
+
// reserved for other same-incarnation blockers such as legacy collateral
|
|
1243
|
+
// stalls; clear those first, refresh, and only then decide the handoff.
|
|
1244
|
+
const terminalRecoveryBlockingTurns = terminalBlockingTurns.filter((turn) => !blockingHandoffTurnIds.has(turn.conversation_id) &&
|
|
1245
|
+
!conversationHasNonterminalDeferredTransfer(turn));
|
|
1246
|
+
return {
|
|
1247
|
+
...observation,
|
|
1248
|
+
reconcileBindingAction,
|
|
1249
|
+
externalHandoffAdoptable,
|
|
1250
|
+
handoffDecision,
|
|
1251
|
+
blockingHandoffTurnIds,
|
|
1252
|
+
terminalRecoveryBlockingTurns
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
function observeTerminalScopedApprovalAuthority(terminal, context, observation) {
|
|
1256
|
+
const { storeDir, mutationsAllowed } = context;
|
|
1257
|
+
const { terminalControl, terminalHasNonterminalDeferredTransfer, conflictingBoundSessionClaims, unresolvedSessionClaims, sessionAuthorityConflict, authoritativeSession, discoveredOwnership, ownership, rawActions } = observation;
|
|
1258
|
+
let terminalScopedCodexApprovalAction;
|
|
1259
|
+
const terminalScopedCodexApprovalPrompt = terminalScopedCodexApprovalPromptSnapshot(terminal.approval_state);
|
|
1260
|
+
if (mutationsAllowed &&
|
|
1261
|
+
terminal.agent === "codex" &&
|
|
1262
|
+
terminalControl &&
|
|
1263
|
+
hasCanonicalTerminalEndpoint(terminalControl) &&
|
|
1264
|
+
authoritativeSession &&
|
|
1265
|
+
!sessionAuthorityConflict &&
|
|
1266
|
+
!terminalHasNonterminalDeferredTransfer &&
|
|
1267
|
+
unresolvedSessionClaims.length === 0 &&
|
|
1268
|
+
conflictingBoundSessionClaims.length === 0 &&
|
|
1269
|
+
terminalScopedCodexApprovalPrompt &&
|
|
1270
|
+
isRecord(rawActions.approve)) {
|
|
1271
|
+
try {
|
|
1272
|
+
const ledger = terminalListRuntime().loadTerminalBridgeDispatchLedger(terminalControl);
|
|
1273
|
+
const boundary = ownership.state === "conflict" &&
|
|
1274
|
+
discoveredOwnership.state === "current" &&
|
|
1275
|
+
ledger
|
|
1276
|
+
? terminalScopedCodexApprovalBoundary({
|
|
1277
|
+
storeDir,
|
|
1278
|
+
terminal,
|
|
1279
|
+
owner: discoveredOwnership.conversation,
|
|
1280
|
+
session: authoritativeSession,
|
|
1281
|
+
ledger,
|
|
1282
|
+
approval: terminalScopedCodexApprovalPrompt
|
|
1283
|
+
})
|
|
1284
|
+
: ownership.state === "none" &&
|
|
1285
|
+
discoveredOwnership.state === "none"
|
|
1286
|
+
? terminalScopedCodexApprovalBoundary({
|
|
1287
|
+
storeDir,
|
|
1288
|
+
terminal,
|
|
1289
|
+
session: authoritativeSession,
|
|
1290
|
+
ledger,
|
|
1291
|
+
approval: terminalScopedCodexApprovalPrompt
|
|
1292
|
+
})
|
|
1293
|
+
: undefined;
|
|
1294
|
+
if (!boundary) {
|
|
1295
|
+
throw new Error("terminal-scoped Codex approval has no eligible managed authority");
|
|
1296
|
+
}
|
|
1297
|
+
terminalScopedCodexApprovalAction = {
|
|
1298
|
+
...rawActions.approve,
|
|
1299
|
+
arguments: {
|
|
1300
|
+
conversation_id: String(terminal.id),
|
|
1301
|
+
expected_terminal_token: boundary.token
|
|
1302
|
+
},
|
|
1303
|
+
scope: "terminal_current_prompt",
|
|
1304
|
+
authority: boundary.authority.kind,
|
|
1305
|
+
managed_state_unchanged: true,
|
|
1306
|
+
automatic_approval_eligible: false,
|
|
1307
|
+
durable_dispatch_receipt: false,
|
|
1308
|
+
uncertain_outcome_recovery: "refresh status and inspect the live prompt; do not retry blindly"
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
catch (error) {
|
|
1312
|
+
runtimeLog("info", "terminal_scoped_codex_approval_not_advertised", {
|
|
1313
|
+
terminal_id: String(terminal.id),
|
|
1314
|
+
terminal_target: terminalControl.target,
|
|
1315
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
const rolloutBackedCodexSession = Boolean(authoritativeSession?.agent === "codex" &&
|
|
1320
|
+
isCompleteNativeRollout(authoritativeSession.binding?.native_process.rollout));
|
|
1321
|
+
return {
|
|
1322
|
+
...observation,
|
|
1323
|
+
terminalScopedCodexApprovalAction,
|
|
1324
|
+
rolloutBackedCodexSession
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
function observeTerminalListActionAuthority(terminal, context, binding) {
|
|
1328
|
+
return observeTerminalScopedApprovalAuthority(terminal, context, observeTerminalHandoffAuthority(terminal, context, observeDeferredTerminalAuthority(terminal, context, observeDeferredSourceAuthority(terminal, context, observeVerifiedEmptyTerminalAuthority(terminal, context, binding)))));
|
|
1329
|
+
}
|
|
1330
|
+
function renderTerminalFirstListEntry(terminal, context, observation) {
|
|
1331
|
+
const { sessionAuthorityRequired, includeAll, mutationsAllowed, conversationHasNonterminalDeferredTransfer } = context;
|
|
1332
|
+
const { automatedInputComposerReady, publicTerminal, allRelated, displayedRelated, relatedSessions, authoritativeSession, ownership, rawSendAction, sessionAwareRawActions, externalHandoffDetected, externalHandoffSnapshotToken, handoffSourceBlockingTurns, verifiedEmptyRawSendAction, verifiedEmptyCodexSnapshotToken, deferredCodexSourceRolloutAuthority, deferredCodexForegroundToken, reconcileBindingAction, externalHandoffAdoptable, handoffDecision, blockingHandoffTurnIds, terminalRecoveryBlockingTurns, terminalScopedCodexApprovalAction, rolloutBackedCodexSession } = observation;
|
|
1333
|
+
const association = decideManagedTerminalAssociation({
|
|
1334
|
+
allRelated,
|
|
1335
|
+
displayedRelated,
|
|
1336
|
+
authoritativeSession,
|
|
1337
|
+
sessionAuthorityRequired,
|
|
1338
|
+
currentOwner: ownership.state === "current"
|
|
1339
|
+
? ownership.conversation
|
|
1340
|
+
: undefined
|
|
1341
|
+
});
|
|
1342
|
+
const { managedSessionId, sessionIds, sessionAllRelated, sessionDisplayedRelated } = association;
|
|
1343
|
+
// History remains useful when a pane has restarted and therefore no
|
|
1344
|
+
// first-class Session is authoritative for the new process incarnation.
|
|
1345
|
+
// Keep that display-only association separate from the send target: under
|
|
1346
|
+
// protocol 3, only authoritativeSession may populate managedSessionId.
|
|
1347
|
+
const sessionBindingMatchesLiveTerminal = authoritativeSession
|
|
1348
|
+
? true
|
|
1349
|
+
: Boolean(!sessionAuthorityRequired &&
|
|
1350
|
+
sessionAllRelated.some((turn) => managedTurnMatchesLiveTerminal(turn, terminal)));
|
|
1351
|
+
const currentTurnValue = ownership.state === "current"
|
|
1352
|
+
? currentManagedTurnForTerminal(ownership.conversation, terminal, sessionAwareRawActions)
|
|
1353
|
+
: undefined;
|
|
1354
|
+
const currentTurnProjection = currentTurnValue && (!mutationsAllowed ||
|
|
1355
|
+
(ownership.state === "current" &&
|
|
1356
|
+
conversationHasNonterminalDeferredTransfer(ownership.conversation)))
|
|
1357
|
+
? readOnlyManagedTurn(currentTurnValue)
|
|
1358
|
+
: currentTurnValue;
|
|
1359
|
+
const currentTurn = currentTurnProjection
|
|
1360
|
+
? withoutGenericHandoffSourceClose(currentTurnProjection, blockingHandoffTurnIds)
|
|
1361
|
+
: undefined;
|
|
1362
|
+
const nonOwnerRawActions = nonOwnerTerminalActions(sessionAwareRawActions, {
|
|
1363
|
+
hasAuthoritativeSession: Boolean(authoritativeSession),
|
|
1364
|
+
rolloutBackedCodexSession
|
|
1365
|
+
});
|
|
1366
|
+
const { recentConversation, historyConversations } = selectManagedTerminalHistory({
|
|
1367
|
+
displayedRelated: sessionDisplayedRelated,
|
|
1368
|
+
currentConversationId: stringValue(currentTurn?.conversation_id),
|
|
1369
|
+
hasCurrentTurn: Boolean(currentTurn),
|
|
1370
|
+
includeAll
|
|
1371
|
+
});
|
|
1372
|
+
const recentTurnValue = recentConversation
|
|
1373
|
+
? historicalManagedTurnForTerminal(recentConversation)
|
|
1374
|
+
: undefined;
|
|
1375
|
+
const recentTurnProjection = recentTurnValue && (!mutationsAllowed ||
|
|
1376
|
+
Boolean(recentConversation &&
|
|
1377
|
+
conversationHasNonterminalDeferredTransfer(recentConversation)))
|
|
1378
|
+
? readOnlyManagedTurn(recentTurnValue)
|
|
1379
|
+
: recentTurnValue;
|
|
1380
|
+
const recentTurn = recentTurnProjection
|
|
1381
|
+
? withoutGenericHandoffSourceClose(recentTurnProjection, blockingHandoffTurnIds)
|
|
1382
|
+
: undefined;
|
|
1383
|
+
const history = historyConversations.map((conversation) => {
|
|
1384
|
+
const turn = historicalManagedTurnForTerminal(conversation);
|
|
1385
|
+
return withoutGenericHandoffSourceClose(mutationsAllowed &&
|
|
1386
|
+
!conversationHasNonterminalDeferredTransfer(conversation)
|
|
1387
|
+
? turn
|
|
1388
|
+
: readOnlyManagedTurn(turn), blockingHandoffTurnIds);
|
|
1389
|
+
});
|
|
1390
|
+
const visibleTurnIds = new Set([currentTurn, recentTurn, ...history]
|
|
1391
|
+
.map((turn) => stringValue(turn?.conversation_id))
|
|
1392
|
+
.filter((id) => id !== undefined));
|
|
1393
|
+
const managedSessionShortReference = managedSessionId
|
|
1394
|
+
? sessionShortRef(managedSessionId)
|
|
1395
|
+
: null;
|
|
1396
|
+
const management = projectTerminalManagement({
|
|
1397
|
+
managedSessionId,
|
|
1398
|
+
managedSessionShortRef: managedSessionShortReference,
|
|
1399
|
+
currentTurn,
|
|
1400
|
+
recentTurn,
|
|
1401
|
+
sessionAllRelatedCount: sessionAllRelated.length,
|
|
1402
|
+
hiddenTurnCount: sessionAllRelated.filter((conversation) => !visibleTurnIds.has(conversation.conversation_id)).length,
|
|
1403
|
+
sessionCount: new Set([
|
|
1404
|
+
...sessionIds,
|
|
1405
|
+
...relatedSessions.map((session) => session.session_id)
|
|
1406
|
+
]).size,
|
|
1407
|
+
authoritativeSession,
|
|
1408
|
+
history: includeAll ? history : undefined
|
|
1409
|
+
});
|
|
1410
|
+
const sendAuthority = decideTerminalSendAuthority({
|
|
1411
|
+
ownership: ownership.state,
|
|
1412
|
+
verifiedEmptyToken: verifiedEmptyCodexSnapshotToken,
|
|
1413
|
+
externalToken: externalHandoffAdoptable
|
|
1414
|
+
? externalHandoffSnapshotToken
|
|
1415
|
+
: undefined,
|
|
1416
|
+
deferredToken: ownership.state !== "conflict" ||
|
|
1417
|
+
deferredCodexSourceRolloutAuthority ===
|
|
1418
|
+
"explicitly_abandoned_predecessor"
|
|
1419
|
+
? deferredCodexForegroundToken
|
|
1420
|
+
: undefined,
|
|
1421
|
+
managedSendSessionId: managedSessionId &&
|
|
1422
|
+
!rolloutBackedCodexSession &&
|
|
1423
|
+
sessionBindingMatchesLiveTerminal &&
|
|
1424
|
+
isRecord(sessionAwareRawActions.send)
|
|
1425
|
+
? managedSessionId
|
|
1426
|
+
: undefined
|
|
1427
|
+
});
|
|
1428
|
+
const tokenSendAction = sendAuthority.mode === "external_handoff"
|
|
1429
|
+
? rawSendAction
|
|
1430
|
+
: sendAuthority.mode === "verified_empty" ||
|
|
1431
|
+
sendAuthority.mode === "deferred"
|
|
1432
|
+
? verifiedEmptyRawSendAction
|
|
1433
|
+
: undefined;
|
|
1434
|
+
const authoritativeSendAction = sendAuthority.mode === "managed" && isRecord(sessionAwareRawActions.send)
|
|
1435
|
+
? sendActionForManagedSession(sessionAwareRawActions.send, sendAuthority.sessionId)
|
|
1436
|
+
: tokenSendAction &&
|
|
1437
|
+
"token" in sendAuthority &&
|
|
1438
|
+
sendAuthority.token
|
|
1439
|
+
? {
|
|
1440
|
+
...tokenSendAction,
|
|
1441
|
+
arguments: {
|
|
1442
|
+
...(isRecord(tokenSendAction.arguments)
|
|
1443
|
+
? tokenSendAction.arguments
|
|
1444
|
+
: {}),
|
|
1445
|
+
expected_terminal_token: sendAuthority.token
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
: undefined;
|
|
1449
|
+
const availableActions = selectTerminalAvailableActions({
|
|
1450
|
+
ownership: ownership.state,
|
|
1451
|
+
currentActions: ownership.state === "current"
|
|
1452
|
+
? currentTerminalActions(currentTurn)
|
|
1453
|
+
: {},
|
|
1454
|
+
sessionAwareRawActions: sessionAwareRawActions,
|
|
1455
|
+
nonOwnerRawActions,
|
|
1456
|
+
authoritativeSendAction,
|
|
1457
|
+
reconcileBindingAction,
|
|
1458
|
+
terminalScopedApprovalAction: terminalScopedCodexApprovalAction,
|
|
1459
|
+
isAction: isRecord
|
|
1460
|
+
});
|
|
1461
|
+
// An explicit undefined rollout prevents a lingering resolver rollout from
|
|
1462
|
+
// being presented as the authoritative status-card thread.
|
|
1463
|
+
const authoritativeIdentity = authoritativeTerminalIdentity(authoritativeSession);
|
|
1464
|
+
const publicManagementConflict = ownership.state === "conflict"
|
|
1465
|
+
? projectPublicManagementConflict({
|
|
1466
|
+
conflict: ownership.conflict,
|
|
1467
|
+
verifiedEmptyToken: verifiedEmptyCodexSnapshotToken,
|
|
1468
|
+
deferredToken: deferredCodexForegroundToken,
|
|
1469
|
+
explicitlyAbandonedPredecessor: deferredCodexSourceRolloutAuthority ===
|
|
1470
|
+
"explicitly_abandoned_predecessor"
|
|
1471
|
+
})
|
|
1472
|
+
: undefined;
|
|
1473
|
+
const handoffPresentation = projectHandoffPresentation({
|
|
1474
|
+
externalHandoffDetected,
|
|
1475
|
+
externalHandoffAdoptable,
|
|
1476
|
+
recoveryBlockingTurnCount: terminalRecoveryBlockingTurns.length,
|
|
1477
|
+
hasHandoffDecision: Boolean(handoffDecision),
|
|
1478
|
+
sourceBlockingTurnCount: handoffSourceBlockingTurns.length,
|
|
1479
|
+
automatedInputComposerReady: automatedInputComposerReady === true,
|
|
1480
|
+
verifiedEmptyToken: verifiedEmptyCodexSnapshotToken
|
|
1481
|
+
});
|
|
1482
|
+
return {
|
|
1483
|
+
...publicTerminal,
|
|
1484
|
+
...authoritativeIdentity,
|
|
1485
|
+
management_state: ownership.state === "conflict"
|
|
1486
|
+
? "conflict"
|
|
1487
|
+
: ownership.state === "current" || Boolean(authoritativeSession)
|
|
1488
|
+
? "managed"
|
|
1489
|
+
: "unmanaged",
|
|
1490
|
+
...(ownership.state === "conflict"
|
|
1491
|
+
? { management_conflict: publicManagementConflict }
|
|
1492
|
+
: {}),
|
|
1493
|
+
...handoffPresentation,
|
|
1494
|
+
...(handoffDecision ? { handoff_decision: handoffDecision } : {}),
|
|
1495
|
+
...(terminalRecoveryBlockingTurns.length > 0
|
|
1496
|
+
? {
|
|
1497
|
+
blocking_turns: terminalRecoveryBlockingTurns.map((turn) => projectBlockingTurn({
|
|
1498
|
+
sessionId: sessionIdForConversation(turn),
|
|
1499
|
+
turnId: turnIdForConversation(turn),
|
|
1500
|
+
status: turn.status,
|
|
1501
|
+
recoveryTurnId: turnIdForConversation(turn)
|
|
1502
|
+
}))
|
|
1503
|
+
}
|
|
1504
|
+
: {}),
|
|
1505
|
+
managed: management,
|
|
1506
|
+
available_actions: availableActions
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
function terminalFirstListProjection({ storeDir, terminals, managedSessions, sessionAuthorityRequired, allConversations, displayedConversations, includeAll, managedOnly, statusFilter, mutationsAllowed }) {
|
|
1510
|
+
const nonterminalDeferredTransfers = listDeferredForegroundTransfers(storeDir).filter((transfer) => !isFinalDeferredForegroundTransferStatus(transfer.status));
|
|
1511
|
+
const nonterminalDeferredTransferIds = new Set(nonterminalDeferredTransfers.map((transfer) => transfer.transfer_id));
|
|
1512
|
+
const nonterminalDeferredSourceTurnIds = new Set(nonterminalDeferredTransfers.flatMap((transfer) => transfer.version === 2 &&
|
|
1513
|
+
transfer.source_kind === "candidate_rollout_quiescent"
|
|
1514
|
+
? (transfer.source_turn_history ?? []).map((turn) => turn.turn_id)
|
|
1515
|
+
: []));
|
|
1516
|
+
const conversationHasNonterminalDeferredTransfer = (conversation) => {
|
|
1517
|
+
if (nonterminalDeferredSourceTurnIds.has(turnIdForConversation(conversation))) {
|
|
1518
|
+
return true;
|
|
1519
|
+
}
|
|
1520
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
1521
|
+
? conversation.native_session_takeover
|
|
1522
|
+
: undefined;
|
|
1523
|
+
const transferId = stringValue(takeover?.deferred_foreground_transfer_id);
|
|
1524
|
+
return Boolean(transferId && nonterminalDeferredTransferIds.has(transferId));
|
|
1525
|
+
};
|
|
1526
|
+
const discoveredTerminalControls = terminals.flatMap((terminal) => {
|
|
1527
|
+
const control = isRecord(terminal.terminal_control)
|
|
1528
|
+
? terminal.terminal_control
|
|
1529
|
+
: undefined;
|
|
1530
|
+
return control ? [control] : [];
|
|
1531
|
+
});
|
|
1532
|
+
const projectionContext = {
|
|
1533
|
+
storeDir,
|
|
1534
|
+
terminals,
|
|
1535
|
+
managedSessions,
|
|
1536
|
+
sessionAuthorityRequired,
|
|
1537
|
+
allConversations,
|
|
1538
|
+
displayedConversations,
|
|
1539
|
+
includeAll,
|
|
1540
|
+
mutationsAllowed,
|
|
1541
|
+
nonterminalDeferredTransfers,
|
|
1542
|
+
conversationHasNonterminalDeferredTransfer
|
|
1543
|
+
};
|
|
1544
|
+
const projectedTerminals = terminals.map((terminal) => renderTerminalFirstListEntry(terminal, projectionContext, observeTerminalListActionAuthority(terminal, projectionContext, observeTerminalListBindingAuthority(terminal, projectionContext))));
|
|
1545
|
+
const unavailableManagedTurns = displayedConversations
|
|
1546
|
+
.filter((conversation) => {
|
|
1547
|
+
const managedControl = terminalControlForManagedConversation(conversation);
|
|
1548
|
+
if (discoveredTerminalControls.some((control) => terminalControlsShareIncarnation(managedControl, control))) {
|
|
1549
|
+
return false;
|
|
1550
|
+
}
|
|
1551
|
+
return (includeAll ||
|
|
1552
|
+
managedOnly ||
|
|
1553
|
+
statusFilter !== undefined ||
|
|
1554
|
+
managedTurnNeedsAttention(conversation));
|
|
1555
|
+
})
|
|
1556
|
+
.sort(compareManagedConversationRecency)
|
|
1557
|
+
.map((conversation) => {
|
|
1558
|
+
const managedTurn = managedTurnListEntry(terminalListRuntime().summarizeConversation(conversation), {
|
|
1559
|
+
terminalBridge: terminalListRuntime().terminalBridgeEnabled(conversation),
|
|
1560
|
+
approvalState: managedListApprovalState(conversation),
|
|
1561
|
+
conversation
|
|
1562
|
+
});
|
|
1563
|
+
return {
|
|
1564
|
+
...managedTurn,
|
|
1565
|
+
available_actions: mutationsAllowed &&
|
|
1566
|
+
!conversationHasNonterminalDeferredTransfer(conversation)
|
|
1567
|
+
? safeUnavailableManagedTurnActions(isRecord(managedTurn.available_actions)
|
|
1568
|
+
? managedTurn.available_actions
|
|
1569
|
+
: {})
|
|
1570
|
+
: readOnlyListActions(isRecord(managedTurn.available_actions)
|
|
1571
|
+
? managedTurn.available_actions
|
|
1572
|
+
: {}),
|
|
1573
|
+
terminal_availability: {
|
|
1574
|
+
available: false,
|
|
1575
|
+
reason: managedOnly
|
|
1576
|
+
? "terminal discovery was disabled by --managed-only"
|
|
1577
|
+
: "the referenced terminal pane is not currently available"
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
});
|
|
1581
|
+
return {
|
|
1582
|
+
terminals: projectedTerminals,
|
|
1583
|
+
unavailableManagedTurns
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
function managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir) {
|
|
1587
|
+
const binding = session.binding;
|
|
1588
|
+
const liveControl = isRecord(terminal.terminal_control)
|
|
1589
|
+
? terminal.terminal_control
|
|
1590
|
+
: undefined;
|
|
1591
|
+
if (session.status !== "bound" ||
|
|
1592
|
+
!binding ||
|
|
1593
|
+
session.agent !== terminal.agent ||
|
|
1594
|
+
binding.native_process.pid !== Number(terminal.pid)) {
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1597
|
+
const terminalAliasMatches = terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.id, liveControl);
|
|
1598
|
+
if (!terminalAliasMatches) {
|
|
1599
|
+
return false;
|
|
1600
|
+
}
|
|
1601
|
+
const workspaceMatches = terminalListRuntime().matchesConfiguredWorkspace(session.workspace, terminal.workspace ?? terminal.cwd);
|
|
1602
|
+
if (!workspaceMatches) {
|
|
1603
|
+
return false;
|
|
1604
|
+
}
|
|
1605
|
+
const observation = terminalObservationFromListEntry(terminal, session.agent);
|
|
1606
|
+
const evidence = {
|
|
1607
|
+
terminalAliasMatches,
|
|
1608
|
+
workspaceMatches
|
|
1609
|
+
};
|
|
1610
|
+
let decision = decideTerminalBindingMatch(session, observation, evidence);
|
|
1611
|
+
if (decision.state === "not_exact" &&
|
|
1612
|
+
decision.reason === "native_identity_mismatch" &&
|
|
1613
|
+
session.agent === "codex" &&
|
|
1614
|
+
observation.nativeIdentity.status === "resolved") {
|
|
1615
|
+
decision = decideTerminalBindingMatch(session, observation, {
|
|
1616
|
+
...evidence,
|
|
1617
|
+
codexLingeringBeforeMatches: terminalListRuntime().codexLingeringBeforeIdentityMatchesSession({
|
|
1618
|
+
storeDir,
|
|
1619
|
+
session,
|
|
1620
|
+
identity: observation.nativeIdentity.identity
|
|
1621
|
+
})
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
return decision.state === "exact";
|
|
1625
|
+
}
|
|
1626
|
+
function managedSessionClaimsLiveTerminalEntry(session, terminal) {
|
|
1627
|
+
const binding = session.binding;
|
|
1628
|
+
const liveControl = isRecord(terminal.terminal_control)
|
|
1629
|
+
? terminal.terminal_control
|
|
1630
|
+
: undefined;
|
|
1631
|
+
return Boolean(binding &&
|
|
1632
|
+
session.agent === terminal.agent &&
|
|
1633
|
+
binding.native_process.pid === Number(terminal.pid) &&
|
|
1634
|
+
terminalControlsShareIncarnation(binding.terminal_control, liveControl));
|
|
1635
|
+
}
|
|
1636
|
+
function listedTerminalProcessIncarnation(terminal) {
|
|
1637
|
+
const processUuid = stringValue(terminal.native_agent_process_uuid);
|
|
1638
|
+
const processBirth = stringValue(terminal.native_agent_process_birth);
|
|
1639
|
+
if (terminal.agent !== "codex" ||
|
|
1640
|
+
(processUuid && processBirth)) {
|
|
1641
|
+
return { processUuid, processBirth };
|
|
1642
|
+
}
|
|
1643
|
+
const pid = Number(terminal.pid);
|
|
1644
|
+
if (!Number.isSafeInteger(pid) || pid <= 1) {
|
|
1645
|
+
return { processUuid, processBirth };
|
|
1646
|
+
}
|
|
1647
|
+
try {
|
|
1648
|
+
const incarnation = terminalListRuntime().codexProcessIncarnationForPid(pid);
|
|
1649
|
+
return {
|
|
1650
|
+
processUuid: processUuid ?? incarnation.processUuid,
|
|
1651
|
+
processBirth: processBirth ?? incarnation.processBirth
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
catch {
|
|
1655
|
+
return { processUuid, processBirth };
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function managedBindingConflictKindForLiveTerminalEntry({ storeDir, session, terminal }) {
|
|
1659
|
+
const binding = session.binding;
|
|
1660
|
+
if (session.status !== "bound" ||
|
|
1661
|
+
!binding ||
|
|
1662
|
+
!managedSessionClaimsLiveTerminalEntry(session, terminal) ||
|
|
1663
|
+
managedSessionMatchesLiveTerminalEntry(session, terminal, storeDir)) {
|
|
1664
|
+
return undefined;
|
|
1665
|
+
}
|
|
1666
|
+
const livePid = Number(terminal.pid);
|
|
1667
|
+
const incarnation = listedTerminalProcessIncarnation(terminal);
|
|
1668
|
+
const relationship = processIncarnationRelationship({
|
|
1669
|
+
binding,
|
|
1670
|
+
livePid,
|
|
1671
|
+
liveProcessUuid: incarnation.processUuid,
|
|
1672
|
+
liveProcessBirth: incarnation.processBirth
|
|
1673
|
+
});
|
|
1674
|
+
if (relationship === "different") {
|
|
1675
|
+
return "stale_process_incarnation";
|
|
1676
|
+
}
|
|
1677
|
+
if (!terminalControlAliasMatches(binding.terminal_id, binding.terminal_control, terminal.id, isRecord(terminal.terminal_control)
|
|
1678
|
+
? terminal.terminal_control
|
|
1679
|
+
: undefined) ||
|
|
1680
|
+
!terminalListRuntime().matchesConfiguredWorkspace(session.workspace, terminal.workspace ?? terminal.cwd)) {
|
|
1681
|
+
return "unverifiable";
|
|
1682
|
+
}
|
|
1683
|
+
return decideManagedBindingConflict({
|
|
1684
|
+
session,
|
|
1685
|
+
claimsTerminal: true,
|
|
1686
|
+
exactBinding: false,
|
|
1687
|
+
ownerConclusivelyInactive: false,
|
|
1688
|
+
processRelationship: relationship,
|
|
1689
|
+
liveNativeThreadId: stringValue(terminal.native_agent_session_id),
|
|
1690
|
+
statusCardNativeThreadId: stringValue(terminal.native_agent_status_card_session_id),
|
|
1691
|
+
managedTurnCount: provisionalManagedBindingTurnCount(storeDir, session)
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
function provisionalManagedBindingTurnCount(storeDir, session) {
|
|
1695
|
+
const binding = session.binding;
|
|
1696
|
+
return binding && session.lineage.created_by === "attach" &&
|
|
1697
|
+
!session.last_transition_id &&
|
|
1698
|
+
!binding.native_thread_id &&
|
|
1699
|
+
!binding.native_process.rollout
|
|
1700
|
+
? terminalListRuntime().managedTurnsForSession(storeDir, session.session_id).length
|
|
1701
|
+
: undefined;
|
|
1702
|
+
}
|
|
1703
|
+
function managedSessionHasUnresolvedNativeTransition(storeDir, session) {
|
|
1704
|
+
const root = nativeThreadTransitionsDir(storeDir);
|
|
1705
|
+
if (!fs.existsSync(root)) {
|
|
1706
|
+
return false;
|
|
1707
|
+
}
|
|
1708
|
+
let entries;
|
|
1709
|
+
try {
|
|
1710
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
1711
|
+
}
|
|
1712
|
+
catch {
|
|
1713
|
+
return true;
|
|
1714
|
+
}
|
|
1715
|
+
for (const entry of entries) {
|
|
1716
|
+
if (!entry.isDirectory()) {
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
let transition;
|
|
1720
|
+
try {
|
|
1721
|
+
transition = loadNativeThreadTransition(storeDir, entry.name);
|
|
1722
|
+
}
|
|
1723
|
+
catch {
|
|
1724
|
+
return true;
|
|
1725
|
+
}
|
|
1726
|
+
if (transition.source_session_id !== session.session_id &&
|
|
1727
|
+
transition.target_session_id !== session.session_id) {
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
if (!["committed", "aborted"].includes(transition.status)) {
|
|
1731
|
+
return true;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return false;
|
|
1735
|
+
}
|
|
1736
|
+
function managedSessionHasAnyNativeTransition(storeDir, session) {
|
|
1737
|
+
const root = nativeThreadTransitionsDir(storeDir);
|
|
1738
|
+
if (!fs.existsSync(root)) {
|
|
1739
|
+
return false;
|
|
1740
|
+
}
|
|
1741
|
+
let entries;
|
|
1742
|
+
try {
|
|
1743
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
1744
|
+
}
|
|
1745
|
+
catch {
|
|
1746
|
+
return true;
|
|
1747
|
+
}
|
|
1748
|
+
for (const entry of entries) {
|
|
1749
|
+
if (!entry.isDirectory()) {
|
|
1750
|
+
continue;
|
|
1751
|
+
}
|
|
1752
|
+
let transition;
|
|
1753
|
+
try {
|
|
1754
|
+
transition = loadNativeThreadTransition(storeDir, entry.name);
|
|
1755
|
+
}
|
|
1756
|
+
catch {
|
|
1757
|
+
return true;
|
|
1758
|
+
}
|
|
1759
|
+
if (transition.source_session_id === session.session_id ||
|
|
1760
|
+
transition.target_session_id === session.session_id) {
|
|
1761
|
+
return true;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
return false;
|
|
1765
|
+
}
|
|
1766
|
+
function terminalControlForManagedConversation(conversation) {
|
|
1767
|
+
return terminalListRuntime().terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
|
|
1768
|
+
? conversation.native_session_takeover
|
|
1769
|
+
: undefined);
|
|
1770
|
+
}
|
|
1771
|
+
function terminalIncarnationBlockingTurns(storeDir, terminalControl, conversations = listConversations(storeDir)) {
|
|
1772
|
+
return conversations
|
|
1773
|
+
.filter(terminalListRuntime().isDiscoverableTmuxConversation)
|
|
1774
|
+
.filter((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminalControl) &&
|
|
1775
|
+
isSessionSendBlockingStatus(turn.status))
|
|
1776
|
+
.sort(compareManagedConversationRecency);
|
|
1777
|
+
}
|
|
1778
|
+
function managedTurnNeedsAttention(conversation) {
|
|
1779
|
+
return terminalManagedTurnNeedsAttention({
|
|
1780
|
+
status: conversation.status,
|
|
1781
|
+
get callbackDeliveryStatus() {
|
|
1782
|
+
const delivery = isRecord(conversation.callback_delivery)
|
|
1783
|
+
? conversation.callback_delivery
|
|
1784
|
+
: undefined;
|
|
1785
|
+
return String(delivery?.status ?? "");
|
|
1786
|
+
}
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
function assertTerminalIncarnationCanStartTurn(storeDir, terminalControl) {
|
|
1790
|
+
const blocker = terminalIncarnationBlockingTurns(storeDir, terminalControl)[0];
|
|
1791
|
+
if (!blocker) {
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
throw new Error(`terminal ${terminalControl.target} still has unresolved Turn ` +
|
|
1795
|
+
`${turnIdForConversation(blocker)} (${blocker.status})`);
|
|
1796
|
+
}
|
|
1797
|
+
function terminalDispatchOwnership(terminalControl) {
|
|
1798
|
+
let ledger;
|
|
1799
|
+
try {
|
|
1800
|
+
ledger = terminalListRuntime().loadTerminalBridgeDispatchLedger(terminalControl);
|
|
1801
|
+
}
|
|
1802
|
+
catch (error) {
|
|
1803
|
+
const decision = dispatch.decideTerminalDispatchOwnership("unreadable");
|
|
1804
|
+
return {
|
|
1805
|
+
state: "conflict",
|
|
1806
|
+
conflict: {
|
|
1807
|
+
reason: decision.code === "ledger_unreadable"
|
|
1808
|
+
? error instanceof Error ? error.message : String(error)
|
|
1809
|
+
: "terminal dispatch ledger is unreadable",
|
|
1810
|
+
recovery: "inspect the shared terminal pane before performing a side effect"
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
const ledgerAuthority = !ledger
|
|
1815
|
+
? "absent"
|
|
1816
|
+
: ledger.status === "resolved"
|
|
1817
|
+
? "resolved"
|
|
1818
|
+
: terminalListRuntime().terminalDispatchRecordMatchesControl(ledger, terminalControl, {
|
|
1819
|
+
requireProcessAnchor: false
|
|
1820
|
+
}) && !terminalListRuntime().terminalDispatchRecordMatchesControl(ledger, terminalControl)
|
|
1821
|
+
? "stale_process_incarnation"
|
|
1822
|
+
: dispatch.isActiveTerminalDispatchStatus(String(ledger.status))
|
|
1823
|
+
? "active"
|
|
1824
|
+
: "inactive_status";
|
|
1825
|
+
let decision = dispatch.decideTerminalDispatchOwnership(ledgerAuthority);
|
|
1826
|
+
if (decision.state === "none" || !ledger)
|
|
1827
|
+
return { state: "none" };
|
|
1828
|
+
const owner = terminalListRuntime().loadTerminalDispatchLedgerOwner(ledger);
|
|
1829
|
+
const ledgerMessageId = stringValue(ledger.message_id);
|
|
1830
|
+
const ownerAuthority = !owner
|
|
1831
|
+
? "unavailable"
|
|
1832
|
+
: isTerminalDispatchOwnerReleasedStatus(owner.status)
|
|
1833
|
+
? "released"
|
|
1834
|
+
: !terminalControlsShareIncarnation(terminalControlForManagedConversation(owner), terminalControl)
|
|
1835
|
+
? "terminal_mismatch"
|
|
1836
|
+
: ledgerMessageId && stringValue(isRecord(owner.native_session_takeover)
|
|
1837
|
+
? owner.native_session_takeover.terminal_bridge_message_id
|
|
1838
|
+
: undefined) !== ledgerMessageId
|
|
1839
|
+
? "generation_mismatch"
|
|
1840
|
+
: "current";
|
|
1841
|
+
decision = dispatch.decideTerminalDispatchOwnership(ledgerAuthority, ownerAuthority);
|
|
1842
|
+
if (decision.state === "none")
|
|
1843
|
+
return { state: "none" };
|
|
1844
|
+
if (decision.state === "current" && owner) {
|
|
1845
|
+
return { state: "current", conversation: owner };
|
|
1846
|
+
}
|
|
1847
|
+
const reason = decision.state === "conflict" &&
|
|
1848
|
+
decision.code === "owner_terminal_mismatch"
|
|
1849
|
+
? "dispatch owner does not reference this terminal pane incarnation"
|
|
1850
|
+
: decision.state === "conflict" &&
|
|
1851
|
+
decision.code === "owner_generation_mismatch"
|
|
1852
|
+
? "dispatch generation does not match the owner state"
|
|
1853
|
+
: "dispatch owner state is unavailable";
|
|
1854
|
+
return {
|
|
1855
|
+
state: "conflict",
|
|
1856
|
+
conflict: projectTerminalDispatchConflict({
|
|
1857
|
+
reason,
|
|
1858
|
+
dispatchStatus: stringValue(ledger.status),
|
|
1859
|
+
ownerConversationId: stringValue(ledger.conversation_id),
|
|
1860
|
+
messageId: stringValue(ledger.message_id)
|
|
1861
|
+
})
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
function localTerminalDispatchOwnership(ledgerOwner, localConversations, terminal) {
|
|
1865
|
+
const localOwner = localConversations.find((conversation) => conversation.conversation_id === ledgerOwner.conversation_id &&
|
|
1866
|
+
sameCanonicalStatePath(conversation.state_path, ledgerOwner.state_path));
|
|
1867
|
+
return decideLocalTerminalDispatchOwnership({
|
|
1868
|
+
ledgerOwnerId: ledgerOwner.conversation_id,
|
|
1869
|
+
localOwner,
|
|
1870
|
+
localOwnerMatchesLiveTerminal: localOwner
|
|
1871
|
+
? managedTurnMatchesLiveTerminal(localOwner, terminal)
|
|
1872
|
+
: false
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
function terminalScopedCodexApprovalBoundary({ storeDir, terminal, session, ledger, approval, owner }) {
|
|
1876
|
+
const terminalRecord = isRecord(terminal) ? terminal : {};
|
|
1877
|
+
const terminalControl = isRecord(terminalRecord.terminal_control)
|
|
1878
|
+
? terminalRecord.terminal_control
|
|
1879
|
+
: undefined;
|
|
1880
|
+
const control = terminalControl;
|
|
1881
|
+
const terminalId = String(terminalRecord.id);
|
|
1882
|
+
const relatedBoundSessionIds = () => listManagedSessions(storeDir)
|
|
1883
|
+
.filter((candidate) => candidate.status === "bound" &&
|
|
1884
|
+
candidate.agent === "codex" &&
|
|
1885
|
+
candidate.binding?.native_process.pid === Number(terminalRecord.pid) &&
|
|
1886
|
+
terminalControlsShareIncarnation(candidate.binding?.terminal_control, terminalControl))
|
|
1887
|
+
.map((candidate) => candidate.session_id);
|
|
1888
|
+
const blockingTurnIds = () => terminalIncarnationBlockingTurns(storeDir, control)
|
|
1889
|
+
.map((turn) => turnIdForConversation(turn));
|
|
1890
|
+
const hasDeferredRecovery = () => listDeferredForegroundTransfers(storeDir).some((transfer) => !isFinalDeferredForegroundTransferStatus(transfer.status) &&
|
|
1891
|
+
(transfer.source_session_id === session.session_id ||
|
|
1892
|
+
transfer.target_session_id === session.session_id ||
|
|
1893
|
+
(transfer.terminal_id === terminalId &&
|
|
1894
|
+
terminalControlEvidenceMatches(transfer.terminal_endpoint, control))));
|
|
1895
|
+
const commonChecks = {
|
|
1896
|
+
relatedBoundSessionIds, blockingTurnIds,
|
|
1897
|
+
hasNativeTransition: () => managedSessionHasAnyNativeTransition(storeDir, session),
|
|
1898
|
+
hasDeferredRecovery,
|
|
1899
|
+
ledgerMatchesTerminal: () => terminalListRuntime().terminalDispatchRecordMatchesControl(ledger, control)
|
|
1900
|
+
};
|
|
1901
|
+
if (owner && ledger) {
|
|
1902
|
+
return decideTerminalScopedCodexApprovalAuthority({
|
|
1903
|
+
kind: "current_dispatch_owner", storeDir, terminal, owner, session,
|
|
1904
|
+
ledger, approval,
|
|
1905
|
+
checks: {
|
|
1906
|
+
...commonChecks,
|
|
1907
|
+
assertDispatchOwner: () => terminalListRuntime().assertManagedTerminalDispatchOwner({
|
|
1908
|
+
storeDir, conversation: owner, terminalControl: control, action: "approve"
|
|
1909
|
+
}),
|
|
1910
|
+
ownerMatchesNativeIdentity: (identity) => nativeAgentIdentityMatchesTurn(owner, identity
|
|
1911
|
+
? { ...identity, evidence: "terminal_scoped_approval" }
|
|
1912
|
+
: undefined)
|
|
1913
|
+
}
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
return decideTerminalScopedCodexApprovalAuthority({
|
|
1917
|
+
kind: "managed_session_no_dispatch_owner", storeDir, terminal, session,
|
|
1918
|
+
ledger, approval,
|
|
1919
|
+
checks: {
|
|
1920
|
+
...commonChecks,
|
|
1921
|
+
dispatchOwnershipIsNone: () => terminalDispatchOwnership(control).state === "none",
|
|
1922
|
+
hasOrphanedDispatch: () => Boolean(terminalListRuntime().orphanedTerminalDispatchForRecovery(control))
|
|
1923
|
+
}
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
async function resolveTerminalScopedCodexApproval({ options, terminal, approvalSnapshot }) {
|
|
1927
|
+
if (terminal.agent !== "codex") {
|
|
1928
|
+
return { state: "blocked", reason: "raw Claude approval remains unsupported" };
|
|
1929
|
+
}
|
|
1930
|
+
const storeDir = path.resolve(terminalListRuntime().storeDirFromOptions(options));
|
|
1931
|
+
let ledger;
|
|
1932
|
+
try {
|
|
1933
|
+
ledger = terminalListRuntime().loadTerminalBridgeDispatchLedger(terminal.terminalControl);
|
|
1934
|
+
}
|
|
1935
|
+
catch (error) {
|
|
1936
|
+
return {
|
|
1937
|
+
state: "blocked",
|
|
1938
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
const relatedSessions = listManagedSessions(storeDir).filter((session) => session.binding?.native_process.pid === terminal.pid &&
|
|
1942
|
+
terminalControlsShareIncarnation(session.binding?.terminal_control, terminal.terminalControl));
|
|
1943
|
+
const relatedTurns = listConversations(storeDir).filter((turn) => terminalControlsShareIncarnation(terminalControlForManagedConversation(turn), terminal.terminalControl));
|
|
1944
|
+
if (ledger?.status === "uncertain") {
|
|
1945
|
+
return {
|
|
1946
|
+
state: "blocked",
|
|
1947
|
+
reason: "terminal dispatch is uncertain; inspect the pane and do not retry approval"
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
const ownership = terminalDispatchOwnership(terminal.terminalControl);
|
|
1951
|
+
if (ownership.state === "conflict") {
|
|
1952
|
+
return {
|
|
1953
|
+
state: "blocked",
|
|
1954
|
+
reason: stringValue(ownership.conflict.reason) ??
|
|
1955
|
+
"terminal dispatch ownership is conflicted"
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
if (ownership.state === "none" &&
|
|
1959
|
+
relatedSessions.length === 0 &&
|
|
1960
|
+
relatedTurns.length === 0) {
|
|
1961
|
+
return { state: "unmanaged" };
|
|
1962
|
+
}
|
|
1963
|
+
const owner = ownership.state === "current"
|
|
1964
|
+
? ownership.conversation
|
|
1965
|
+
: undefined;
|
|
1966
|
+
if (owner) {
|
|
1967
|
+
const ownerStoreDir = terminalListRuntime().managedSessionStoreDirForConversation(owner);
|
|
1968
|
+
if (!ownerStoreDir || path.resolve(ownerStoreDir) !== storeDir) {
|
|
1969
|
+
return {
|
|
1970
|
+
state: "blocked",
|
|
1971
|
+
reason: "terminal dispatch owner belongs to another AKK Store"
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
const ownerSession = owner
|
|
1976
|
+
? tryLoadManagedSession(storeDir, sessionIdForConversation(owner))
|
|
1977
|
+
: undefined;
|
|
1978
|
+
if (owner && !ownerSession) {
|
|
1979
|
+
return {
|
|
1980
|
+
state: "blocked",
|
|
1981
|
+
reason: "terminal dispatch owner has no current managed Session"
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
let identityContext;
|
|
1985
|
+
try {
|
|
1986
|
+
identityContext = terminalListRuntime().codexManagedIdentityResolutionContext({
|
|
1987
|
+
storeDir,
|
|
1988
|
+
terminal
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
catch {
|
|
1992
|
+
// The terminal-scoped path deliberately tolerates an unavailable Codex
|
|
1993
|
+
// rollout resolver. Store, pane, process, owner, and prompt fences below
|
|
1994
|
+
// remain mandatory.
|
|
1995
|
+
}
|
|
1996
|
+
const observation = await terminalListRuntime().observeCurrentNativeAgentSessionIdentity({
|
|
1997
|
+
options,
|
|
1998
|
+
agent: "codex",
|
|
1999
|
+
pid: terminal.pid,
|
|
2000
|
+
cwd: terminal.terminalControl.currentPath,
|
|
2001
|
+
preferredSessionId: identityContext?.preferredSessionId,
|
|
2002
|
+
allowedCompanionIdentity: identityContext?.companions.primary,
|
|
2003
|
+
allowedAdditionalIdentities: identityContext?.companions.additional
|
|
2004
|
+
});
|
|
2005
|
+
let processIncarnation;
|
|
2006
|
+
try {
|
|
2007
|
+
processIncarnation = terminalListRuntime().codexProcessIncarnationForPid(terminal.pid);
|
|
2008
|
+
}
|
|
2009
|
+
catch (error) {
|
|
2010
|
+
return {
|
|
2011
|
+
state: "blocked",
|
|
2012
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
const identity = observation.status === "resolved"
|
|
2016
|
+
? observation.identity
|
|
2017
|
+
: undefined;
|
|
2018
|
+
let approval = approvalSnapshot;
|
|
2019
|
+
if (!approval) {
|
|
2020
|
+
try {
|
|
2021
|
+
const terminalStatus = await terminalListRuntime().terminalStatusForControl("codex", terminal.terminalControl, options, {
|
|
2022
|
+
pid: terminal.pid,
|
|
2023
|
+
cwd: terminal.terminalControl.currentPath,
|
|
2024
|
+
conversationId: terminal.conversationId,
|
|
2025
|
+
terminalTarget: terminal.terminalControl.target
|
|
2026
|
+
});
|
|
2027
|
+
approval = terminalScopedCodexApprovalPromptSnapshot(terminalStatus.approval_state);
|
|
2028
|
+
}
|
|
2029
|
+
catch (error) {
|
|
2030
|
+
return {
|
|
2031
|
+
state: "blocked",
|
|
2032
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
if (!approval) {
|
|
2037
|
+
return {
|
|
2038
|
+
state: "blocked",
|
|
2039
|
+
reason: "terminal-scoped Codex approval requires one fresh exact approval prompt"
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
const terminalSnapshot = {
|
|
2043
|
+
id: terminal.conversationId,
|
|
2044
|
+
source: "terminal",
|
|
2045
|
+
agent: "codex",
|
|
2046
|
+
pid: terminal.pid,
|
|
2047
|
+
cwd: terminal.terminalControl.currentPath,
|
|
2048
|
+
workspace: terminal.terminalControl.currentPath,
|
|
2049
|
+
terminal_control: terminal.terminalControl,
|
|
2050
|
+
native_agent_session_id: identity?.sessionId,
|
|
2051
|
+
native_agent_process_uuid: identity?.processUuid ?? processIncarnation.processUuid,
|
|
2052
|
+
native_agent_process_birth: identity?.processBirth ?? processIncarnation.processBirth,
|
|
2053
|
+
native_agent_rollout: identity?.rollout,
|
|
2054
|
+
native_agent_identity_observation: { status: observation.status },
|
|
2055
|
+
approval_state: {
|
|
2056
|
+
approvable: true,
|
|
2057
|
+
fingerprint: approval.fingerprint,
|
|
2058
|
+
keys: approval.keys,
|
|
2059
|
+
decision_mode: "keys",
|
|
2060
|
+
request_id: approval.requestId
|
|
2061
|
+
}
|
|
2062
|
+
};
|
|
2063
|
+
try {
|
|
2064
|
+
if (owner && ownerSession && ledger) {
|
|
2065
|
+
return {
|
|
2066
|
+
state: "eligible",
|
|
2067
|
+
boundary: terminalScopedCodexApprovalBoundary({
|
|
2068
|
+
storeDir,
|
|
2069
|
+
terminal: terminalSnapshot,
|
|
2070
|
+
owner,
|
|
2071
|
+
session: ownerSession,
|
|
2072
|
+
ledger,
|
|
2073
|
+
approval
|
|
2074
|
+
})
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
if (ownership.state !== "none") {
|
|
2078
|
+
throw new Error("managed terminal approval has no current exact dispatch owner");
|
|
2079
|
+
}
|
|
2080
|
+
if (ledger && ledger.status !== "resolved") {
|
|
2081
|
+
throw new Error(`terminal-scoped Codex approval cannot use ${String(ledger.status)} dispatch ownership`);
|
|
2082
|
+
}
|
|
2083
|
+
const matchingSessions = relatedSessions.filter((session) => managedSessionMatchesLiveTerminalEntry(session, terminalSnapshot, storeDir));
|
|
2084
|
+
if (matchingSessions.length !== 1) {
|
|
2085
|
+
throw new Error("terminal-scoped Codex approval has no single exact managed Session");
|
|
2086
|
+
}
|
|
2087
|
+
return {
|
|
2088
|
+
state: "eligible",
|
|
2089
|
+
boundary: terminalScopedCodexApprovalBoundary({
|
|
2090
|
+
storeDir,
|
|
2091
|
+
terminal: terminalSnapshot,
|
|
2092
|
+
session: matchingSessions[0],
|
|
2093
|
+
ledger,
|
|
2094
|
+
approval
|
|
2095
|
+
})
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
catch (error) {
|
|
2099
|
+
return {
|
|
2100
|
+
state: "blocked",
|
|
2101
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
function historicalManagedTurnForTerminal(conversation) {
|
|
2106
|
+
return renderHistoricalManagedTurn(managedTurnListEntry(terminalListRuntime().summarizeConversation(conversation), {
|
|
2107
|
+
terminalBridge: terminalListRuntime().terminalBridgeEnabled(conversation),
|
|
2108
|
+
approvalState: managedListApprovalState(conversation),
|
|
2109
|
+
conversation
|
|
2110
|
+
}));
|
|
2111
|
+
}
|
|
2112
|
+
function managedTurnMatchesLiveTerminal(conversation, terminal) {
|
|
2113
|
+
const takeover = isRecord(conversation.native_session_takeover)
|
|
2114
|
+
? conversation.native_session_takeover
|
|
2115
|
+
: undefined;
|
|
2116
|
+
const liveControl = isRecord(terminal.terminal_control)
|
|
2117
|
+
? terminal.terminal_control
|
|
2118
|
+
: undefined;
|
|
2119
|
+
const storedControl = terminalListRuntime().terminalControlFromTakeover(takeover);
|
|
2120
|
+
const livePid = Number(terminal.pid);
|
|
2121
|
+
const storedPid = Number(takeover?.terminal_agent_pid);
|
|
2122
|
+
if (executorForConversation(conversation).kind !== terminal.agent ||
|
|
2123
|
+
!Number.isSafeInteger(livePid) ||
|
|
2124
|
+
livePid <= 1 ||
|
|
2125
|
+
storedPid !== livePid ||
|
|
2126
|
+
!terminalControlAliasMatches(stringValue(takeover?.native_session_id), storedControl, stringValue(terminal.id), liveControl) ||
|
|
2127
|
+
!terminalControlsShareIncarnation(storedControl, liveControl)) {
|
|
2128
|
+
return false;
|
|
2129
|
+
}
|
|
2130
|
+
const liveSessionId = stringValue(terminal.native_agent_session_id);
|
|
2131
|
+
const liveProcessUuid = stringValue(terminal.native_agent_process_uuid);
|
|
2132
|
+
const liveProcessBirth = stringValue(terminal.native_agent_process_birth);
|
|
2133
|
+
const liveRollout = isRecord(terminal.native_agent_rollout)
|
|
2134
|
+
? terminal.native_agent_rollout
|
|
2135
|
+
: undefined;
|
|
2136
|
+
const liveNativeIdentity = liveSessionId
|
|
2137
|
+
? {
|
|
2138
|
+
sessionId: liveSessionId,
|
|
2139
|
+
...(liveProcessUuid ? { processUuid: liveProcessUuid } : {}),
|
|
2140
|
+
...(liveProcessBirth ? { processBirth: liveProcessBirth } : {}),
|
|
2141
|
+
...(liveRollout
|
|
2142
|
+
? {
|
|
2143
|
+
rollout: {
|
|
2144
|
+
fd: String(liveRollout.fd ?? ""),
|
|
2145
|
+
device: String(liveRollout.device ?? ""),
|
|
2146
|
+
inode: String(liveRollout.inode ?? ""),
|
|
2147
|
+
path: String(liveRollout.path ?? "")
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
: {}),
|
|
2151
|
+
evidence: "live_terminal"
|
|
2152
|
+
}
|
|
2153
|
+
: undefined;
|
|
2154
|
+
if (!nativeAgentIdentityMatchesTurn(conversation, liveNativeIdentity)) {
|
|
2155
|
+
return false;
|
|
2156
|
+
}
|
|
2157
|
+
const liveWorkspace = terminal.workspace ?? terminal.cwd;
|
|
2158
|
+
if (!terminalListRuntime().matchesConfiguredWorkspace(conversation.workspace, liveWorkspace)) {
|
|
2159
|
+
return false;
|
|
2160
|
+
}
|
|
2161
|
+
const livePanePath = liveControl?.currentPath;
|
|
2162
|
+
if (livePanePath !== undefined &&
|
|
2163
|
+
!terminalListRuntime().matchesConfiguredWorkspace(conversation.workspace, livePanePath)) {
|
|
2164
|
+
return false;
|
|
2165
|
+
}
|
|
2166
|
+
return true;
|
|
2167
|
+
}
|
|
2168
|
+
function currentManagedTurnForTerminal(conversation, terminal, rawTerminalActions) {
|
|
2169
|
+
const managedTurn = managedTurnListEntry(terminalListRuntime().summarizeConversation(conversation), {
|
|
2170
|
+
terminalBridge: terminalListRuntime().terminalBridgeEnabled(conversation),
|
|
2171
|
+
approvalState: managedListApprovalState(conversation),
|
|
2172
|
+
conversation
|
|
2173
|
+
});
|
|
2174
|
+
const rawApproval = isRecord(rawTerminalActions.approve)
|
|
2175
|
+
? rawTerminalActions.approve
|
|
2176
|
+
: undefined;
|
|
2177
|
+
if (!rawApproval || executorForConversation(conversation).kind !== "codex") {
|
|
2178
|
+
return managedTurn;
|
|
2179
|
+
}
|
|
2180
|
+
return renderCurrentManagedTurn(managedTurn, {
|
|
2181
|
+
isCodex: true,
|
|
2182
|
+
ownerId: conversation.conversation_id,
|
|
2183
|
+
rawApproval,
|
|
2184
|
+
terminalApprovalState: () => isRecord(terminal.approval_state)
|
|
2185
|
+
? terminal.approval_state
|
|
2186
|
+
: undefined
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
async function listStateForTerminal(agent, terminalControl, options, bridge = terminalListRuntime().createTerminalAgentBridge(options), runtime) {
|
|
2190
|
+
if (options.noApprovalScan) {
|
|
2191
|
+
return {
|
|
2192
|
+
approval_state: {
|
|
2193
|
+
scanned: false,
|
|
2194
|
+
blocked: false,
|
|
2195
|
+
approvable: false,
|
|
2196
|
+
reason: "approval scan disabled"
|
|
2197
|
+
},
|
|
2198
|
+
activity_state: "unknown",
|
|
2199
|
+
activity_reason: "terminal screen scan disabled"
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
try {
|
|
2203
|
+
const status = await bridge.status(agent, terminalControl, {
|
|
2204
|
+
scrollbackLines: Number(options.scrollbackLines ?? 120),
|
|
2205
|
+
runtime
|
|
2206
|
+
});
|
|
2207
|
+
return {
|
|
2208
|
+
approval_state: {
|
|
2209
|
+
...status.approval_state,
|
|
2210
|
+
screen_excerpt: status.approval_state.blocked ? status.screen.excerpt?.slice(-1000) : undefined
|
|
2211
|
+
},
|
|
2212
|
+
activity_state: status.activity_state,
|
|
2213
|
+
activity_reason: status.activity_reason,
|
|
2214
|
+
capability_limitation: status.capability_limitation,
|
|
2215
|
+
_terminal_status_snapshot: status,
|
|
2216
|
+
// Internal projection evidence; terminalControlledListEntry selects all
|
|
2217
|
+
// public fields explicitly and never exposes the pane excerpt itself.
|
|
2218
|
+
screen_excerpt: status.screen.excerpt
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
catch (error) {
|
|
2222
|
+
return {
|
|
2223
|
+
approval_state: {
|
|
2224
|
+
scanned: false,
|
|
2225
|
+
blocked: false,
|
|
2226
|
+
approvable: false,
|
|
2227
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2228
|
+
},
|
|
2229
|
+
activity_state: "unknown",
|
|
2230
|
+
activity_reason: error instanceof Error ? error.message : String(error)
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
function managedListApprovalState(conversation) {
|
|
2235
|
+
if (!terminalListRuntime().terminalBridgeEnabled(conversation) ||
|
|
2236
|
+
!["waiting_for_agent", "waiting_for_openclaw"].includes(String(conversation.status))) {
|
|
2237
|
+
return undefined;
|
|
2238
|
+
}
|
|
2239
|
+
const nativeTakeover = isRecord(conversation.native_session_takeover)
|
|
2240
|
+
? conversation.native_session_takeover
|
|
2241
|
+
: undefined;
|
|
2242
|
+
if (!terminalListRuntime().terminalControlFromTakeover(nativeTakeover) ||
|
|
2243
|
+
!stringValue(nativeTakeover?.terminal_bridge_message_id)) {
|
|
2244
|
+
return undefined;
|
|
2245
|
+
}
|
|
2246
|
+
const approval = isRecord(nativeTakeover?.terminal_bridge_approval)
|
|
2247
|
+
? nativeTakeover.terminal_bridge_approval
|
|
2248
|
+
: undefined;
|
|
2249
|
+
const approvalState = isRecord(approval?.approval_state)
|
|
2250
|
+
? approval.approval_state
|
|
2251
|
+
: undefined;
|
|
2252
|
+
const fingerprint = stringValue(approval?.fingerprint);
|
|
2253
|
+
const notifiedAt = stringValue(approval?.notified_at);
|
|
2254
|
+
const notifiedAtMs = validTimestampMs(notifiedAt);
|
|
2255
|
+
if (!approvalState ||
|
|
2256
|
+
!fingerprint ||
|
|
2257
|
+
notifiedAtMs === undefined ||
|
|
2258
|
+
cliNowMs() - notifiedAtMs > terminalListRuntime().approvalTtlMs) {
|
|
2259
|
+
return undefined;
|
|
2260
|
+
}
|
|
2261
|
+
return {
|
|
2262
|
+
...approvalState,
|
|
2263
|
+
fingerprint,
|
|
2264
|
+
notified_at: notifiedAt
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
async function resolveConversationSelectorOption(commandName, options) {
|
|
2268
|
+
const sendOperation = commandName === "send";
|
|
2269
|
+
if (stringValue(options.expectedTerminalToken)) {
|
|
2270
|
+
// Selector resolution may replace an alias (or an omitted selector) with a
|
|
2271
|
+
// discovered full terminal id. Preserve the caller's actual authority so
|
|
2272
|
+
// the handoff token fence cannot mistake that convenience resolution for
|
|
2273
|
+
// an exact selector supplied by the caller.
|
|
2274
|
+
terminalListRuntime().rememberOriginalExpectedTerminalSelector(options, stringValue(sendOperation
|
|
2275
|
+
? options.session ?? options.conversation ?? options.conversationId
|
|
2276
|
+
: options.turn ?? options.conversation ?? options.conversationId)?.trim());
|
|
2277
|
+
}
|
|
2278
|
+
if (!terminalListRuntime().selectorCommands.has(String(commandName ?? "")) ||
|
|
2279
|
+
options.state) {
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
const supplied = stringValue(sendOperation
|
|
2283
|
+
? options.session ?? options.conversation ?? options.conversationId
|
|
2284
|
+
: options.turn ?? options.conversation ?? options.conversationId)?.trim();
|
|
2285
|
+
if (supplied && !isSessionSelectorSyntax(supplied)) {
|
|
2286
|
+
// Full authoritative IDs keep their existing command-specific validation
|
|
2287
|
+
// path. This avoids a discovery scan before option validation and preserves
|
|
2288
|
+
// precise downstream errors for closed or currently non-actionable state.
|
|
2289
|
+
if (sendOperation) {
|
|
2290
|
+
options.session = supplied;
|
|
2291
|
+
}
|
|
2292
|
+
else {
|
|
2293
|
+
options.turn = supplied;
|
|
2294
|
+
}
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
const candidates = await sessionSelectorCandidates(commandName, options);
|
|
2298
|
+
const resolution = resolveSessionSelector(supplied, candidates, {
|
|
2299
|
+
operation: commandName
|
|
2300
|
+
});
|
|
2301
|
+
if (sendOperation) {
|
|
2302
|
+
options.session = resolution.id;
|
|
2303
|
+
}
|
|
2304
|
+
else {
|
|
2305
|
+
options.turn = resolution.id;
|
|
2306
|
+
}
|
|
2307
|
+
options.conversation = resolution.id;
|
|
2308
|
+
delete options.conversationId;
|
|
2309
|
+
}
|
|
2310
|
+
function isSessionSelectorSyntax(value) {
|
|
2311
|
+
return (/^(?:only|latest|codex|claude|(?:codex|claude):latest)$/iu.test(value) ||
|
|
2312
|
+
/^@[0-9a-f]+$/iu.test(value));
|
|
2313
|
+
}
|
|
2314
|
+
async function sessionSelectorCandidates(commandName, options) {
|
|
2315
|
+
const storeDir = terminalListRuntime().storeDirFromOptions(options);
|
|
2316
|
+
const mutationsAllowed = inspectStoreCompatibility(storeDir).writable === true;
|
|
2317
|
+
const selectorStore = inspectStoreCompatibility(storeDir);
|
|
2318
|
+
const storedConversations = listConversations(storeDir);
|
|
2319
|
+
const workspaceConversations = storedConversations
|
|
2320
|
+
.filter((conversation) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, conversation.workspace));
|
|
2321
|
+
const discoverableWorkspaceConversations = workspaceConversations
|
|
2322
|
+
.filter(terminalListRuntime().isDiscoverableTmuxConversation);
|
|
2323
|
+
const managed = discoverableWorkspaceConversations.map((conversation) => managedTurnListEntry(terminalListRuntime().summarizeConversation(conversation), {
|
|
2324
|
+
terminalBridge: terminalListRuntime().terminalBridgeEnabled(conversation),
|
|
2325
|
+
approvalState: managedListApprovalState(conversation),
|
|
2326
|
+
conversation
|
|
2327
|
+
}));
|
|
2328
|
+
const terminalScan = await buildTerminalListGroup({
|
|
2329
|
+
options: {
|
|
2330
|
+
...options,
|
|
2331
|
+
noApprovalScan: ["send", "approve", "cancel"].includes(commandName)
|
|
2332
|
+
? options.noApprovalScan
|
|
2333
|
+
: true
|
|
2334
|
+
},
|
|
2335
|
+
agentFilter: undefined,
|
|
2336
|
+
statusFilter: undefined
|
|
2337
|
+
});
|
|
2338
|
+
const terminalProjection = terminalFirstListProjection({
|
|
2339
|
+
storeDir,
|
|
2340
|
+
terminals: terminalScan.terminalControlled.filter((entry) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, entry.workspace ?? entry.cwd)),
|
|
2341
|
+
managedSessions: selectorStore.readable
|
|
2342
|
+
? listManagedSessions(storeDir).filter((session) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, session.workspace))
|
|
2343
|
+
: [],
|
|
2344
|
+
sessionAuthorityRequired: Number(selectorStore.writer_protocol) >= STORE_SESSION_AUTHORITY_PROTOCOL,
|
|
2345
|
+
allConversations: discoverableWorkspaceConversations,
|
|
2346
|
+
displayedConversations: discoverableWorkspaceConversations,
|
|
2347
|
+
includeAll: false,
|
|
2348
|
+
managedOnly: options.managedOnly === true,
|
|
2349
|
+
statusFilter: undefined,
|
|
2350
|
+
mutationsAllowed
|
|
2351
|
+
});
|
|
2352
|
+
const observedAtMs = cliNowMs();
|
|
2353
|
+
if (commandName === "send") {
|
|
2354
|
+
const sessionEntries = terminalProjection.terminals.flatMap((entry) => {
|
|
2355
|
+
const managedState = isRecord(entry.managed) ? entry.managed : undefined;
|
|
2356
|
+
const recentTurn = isRecord(managedState?.recent_turn)
|
|
2357
|
+
? managedState.recent_turn
|
|
2358
|
+
: undefined;
|
|
2359
|
+
const currentTurn = isRecord(managedState?.current_turn)
|
|
2360
|
+
? managedState.current_turn
|
|
2361
|
+
: undefined;
|
|
2362
|
+
const sessionId = stringValue(managedState?.session_id);
|
|
2363
|
+
const actions = isRecord(entry.available_actions)
|
|
2364
|
+
? entry.available_actions
|
|
2365
|
+
: undefined;
|
|
2366
|
+
const sendAction = isRecord(actions?.send)
|
|
2367
|
+
? actions.send
|
|
2368
|
+
: undefined;
|
|
2369
|
+
const sendArguments = isRecord(sendAction?.arguments)
|
|
2370
|
+
? sendAction.arguments
|
|
2371
|
+
: undefined;
|
|
2372
|
+
if (!sessionId ||
|
|
2373
|
+
!sendAction ||
|
|
2374
|
+
stringValue(sendArguments?.session_id) !== sessionId) {
|
|
2375
|
+
return [];
|
|
2376
|
+
}
|
|
2377
|
+
const commonEntry = {
|
|
2378
|
+
agent: entry.agent,
|
|
2379
|
+
status: "idle",
|
|
2380
|
+
workspace: entry.workspace ?? entry.cwd,
|
|
2381
|
+
updated_at: recentTurn?.updated_at ??
|
|
2382
|
+
currentTurn?.updated_at,
|
|
2383
|
+
available_actions: {
|
|
2384
|
+
send: sendAction
|
|
2385
|
+
}
|
|
2386
|
+
};
|
|
2387
|
+
return [
|
|
2388
|
+
{
|
|
2389
|
+
...commonEntry,
|
|
2390
|
+
id: sessionId,
|
|
2391
|
+
short_ref: sessionShortRef(sessionId),
|
|
2392
|
+
source: "managed_session"
|
|
2393
|
+
},
|
|
2394
|
+
{
|
|
2395
|
+
...commonEntry,
|
|
2396
|
+
id: String(entry.id),
|
|
2397
|
+
short_ref: stringValue(entry.short_ref) ??
|
|
2398
|
+
sessionShortRef(String(entry.id)),
|
|
2399
|
+
source: "managed_session_terminal_alias"
|
|
2400
|
+
}
|
|
2401
|
+
];
|
|
2402
|
+
});
|
|
2403
|
+
const rawTerminalEntries = terminalProjection.terminals.filter((entry) => {
|
|
2404
|
+
const managedState = isRecord(entry.managed) ? entry.managed : undefined;
|
|
2405
|
+
const actions = isRecord(entry.available_actions)
|
|
2406
|
+
? entry.available_actions
|
|
2407
|
+
: undefined;
|
|
2408
|
+
const sendAction = isRecord(actions?.send) ? actions.send : undefined;
|
|
2409
|
+
const sendArguments = isRecord(sendAction?.arguments)
|
|
2410
|
+
? sendAction.arguments
|
|
2411
|
+
: undefined;
|
|
2412
|
+
return (!stringValue(managedState?.session_id) ||
|
|
2413
|
+
!stringValue(sendArguments?.session_id));
|
|
2414
|
+
});
|
|
2415
|
+
return [
|
|
2416
|
+
...managed.map((entry) => projectSelectorCandidate(entry, commandName, observedAtMs, {
|
|
2417
|
+
defaultActionable: false,
|
|
2418
|
+
mutationsAllowed
|
|
2419
|
+
})),
|
|
2420
|
+
...sessionEntries.map((entry) => projectSelectorCandidate(entry, commandName, observedAtMs, {
|
|
2421
|
+
// A managed terminal's full id/@short-ref is an explicit alias for
|
|
2422
|
+
// its Session send target. It must not duplicate the Session in
|
|
2423
|
+
// omitted, only, latest, or agent-name selection.
|
|
2424
|
+
defaultActionable: entry.source !== "managed_session_terminal_alias",
|
|
2425
|
+
mutationsAllowed
|
|
2426
|
+
})),
|
|
2427
|
+
...rawTerminalEntries.map((entry) => projectSelectorCandidate(entry, commandName, observedAtMs, {
|
|
2428
|
+
defaultActionable: true,
|
|
2429
|
+
mutationsAllowed
|
|
2430
|
+
}))
|
|
2431
|
+
];
|
|
2432
|
+
}
|
|
2433
|
+
return [
|
|
2434
|
+
...managed.map((entry) => projectSelectorCandidate(entry, commandName, observedAtMs, {
|
|
2435
|
+
defaultActionable: options.managedOnly === true,
|
|
2436
|
+
mutationsAllowed
|
|
2437
|
+
})),
|
|
2438
|
+
...terminalProjection.terminals.map((entry) => projectSelectorCandidate(entry, commandName, observedAtMs, {
|
|
2439
|
+
defaultActionable: true,
|
|
2440
|
+
mutationsAllowed
|
|
2441
|
+
}))
|
|
2442
|
+
];
|
|
2443
|
+
}
|
|
2444
|
+
//# sourceMappingURL=terminal-list-cli-adapter.js.map
|