oh-my-codex 0.20.2 → 0.20.5
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/Cargo.lock +36 -18
- package/Cargo.toml +1 -1
- package/README.md +28 -2
- package/crates/omx-runtime/Cargo.toml +4 -0
- package/crates/omx-runtime/src/main.rs +415 -2
- package/crates/omx-runtime/tests/execution.rs +483 -0
- package/crates/omx-runtime-core/src/dispatch.rs +96 -10
- package/crates/omx-runtime-core/src/engine.rs +839 -29
- package/crates/omx-runtime-core/src/lib.rs +8 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
- package/dist/adapt/__tests__/herdr-target.test.js +72 -0
- package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
- package/dist/adapt/contracts.d.ts +1 -1
- package/dist/adapt/contracts.d.ts.map +1 -1
- package/dist/adapt/contracts.js +1 -1
- package/dist/adapt/contracts.js.map +1 -1
- package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
- package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
- package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
- package/dist/adapt/herdr/authority.d.ts +34 -0
- package/dist/adapt/herdr/authority.d.ts.map +1 -0
- package/dist/adapt/herdr/authority.js +71 -0
- package/dist/adapt/herdr/authority.js.map +1 -0
- package/dist/adapt/herdr/bridge.d.ts +84 -0
- package/dist/adapt/herdr/bridge.d.ts.map +1 -0
- package/dist/adapt/herdr/bridge.js +222 -0
- package/dist/adapt/herdr/bridge.js.map +1 -0
- package/dist/adapt/herdr/sanitize.d.ts +20 -0
- package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
- package/dist/adapt/herdr/sanitize.js +70 -0
- package/dist/adapt/herdr/sanitize.js.map +1 -0
- package/dist/adapt/herdr/semantic.d.ts +67 -0
- package/dist/adapt/herdr/semantic.d.ts.map +1 -0
- package/dist/adapt/herdr/semantic.js +170 -0
- package/dist/adapt/herdr/semantic.js.map +1 -0
- package/dist/adapt/herdr/seq-store.d.ts +32 -0
- package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
- package/dist/adapt/herdr/seq-store.js +126 -0
- package/dist/adapt/herdr/seq-store.js.map +1 -0
- package/dist/adapt/herdr/team-map.d.ts +30 -0
- package/dist/adapt/herdr/team-map.d.ts.map +1 -0
- package/dist/adapt/herdr/team-map.js +37 -0
- package/dist/adapt/herdr/team-map.js.map +1 -0
- package/dist/adapt/herdr/transport.d.ts +115 -0
- package/dist/adapt/herdr/transport.d.ts.map +1 -0
- package/dist/adapt/herdr/transport.js +260 -0
- package/dist/adapt/herdr/transport.js.map +1 -0
- package/dist/adapt/herdr/wiring.d.ts +24 -0
- package/dist/adapt/herdr/wiring.d.ts.map +1 -0
- package/dist/adapt/herdr/wiring.js +62 -0
- package/dist/adapt/herdr/wiring.js.map +1 -0
- package/dist/adapt/herdr.d.ts +19 -0
- package/dist/adapt/herdr.d.ts.map +1 -0
- package/dist/adapt/herdr.js +115 -0
- package/dist/adapt/herdr.js.map +1 -0
- package/dist/adapt/index.d.ts +4 -0
- package/dist/adapt/index.d.ts.map +1 -1
- package/dist/adapt/index.js +48 -0
- package/dist/adapt/index.js.map +1 -1
- package/dist/adapt/registry.d.ts.map +1 -1
- package/dist/adapt/registry.js +11 -0
- package/dist/adapt/registry.js.map +1 -1
- package/dist/agents/__tests__/definitions.test.js +81 -0
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +25 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/native-config.d.ts +2 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/auth/__tests__/hotswap.test.js +60 -1
- package/dist/auth/__tests__/hotswap.test.js.map +1 -1
- package/dist/auth/__tests__/storage.test.js +33 -1
- package/dist/auth/__tests__/storage.test.js.map +1 -1
- package/dist/auth/hotswap.d.ts +22 -2
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +107 -22
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/auth/storage.js +1 -1
- package/dist/auth/storage.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
- package/dist/autopilot/ralplan-gate.js +7 -6
- package/dist/autopilot/ralplan-gate.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js +4 -23
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/api.test.js +61 -24
- package/dist/cli/__tests__/api.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +63 -7
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +150 -2
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js +135 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +211 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +878 -463
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +1290 -127
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +8 -4
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-assets.test.js +398 -16
- package/dist/cli/__tests__/native-assets.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +132 -6
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +3 -3
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +49 -164
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +107 -109
- package/dist/cli/__tests__/ralplan.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +11 -10
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js +25 -1
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +224 -7
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +150 -8
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +117 -2
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +95 -4
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +669 -177
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +33 -2
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/api.d.ts.map +1 -1
- package/dist/cli/api.js +9 -10
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/codex-feature-probe.d.ts +16 -0
- package/dist/cli/codex-feature-probe.d.ts.map +1 -1
- package/dist/cli/codex-feature-probe.js +44 -0
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/codex-home.d.ts.map +1 -1
- package/dist/cli/codex-home.js +7 -4
- package/dist/cli/codex-home.js.map +1 -1
- package/dist/cli/doctor.d.ts +20 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +290 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +234 -12
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2409 -746
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-parity.js +2 -2
- package/dist/cli/mcp-parity.js.map +1 -1
- package/dist/cli/native-assets.d.ts +25 -0
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +448 -51
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +20 -5
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
- package/dist/cli/native-hook-claim-journal.js +67 -32
- package/dist/cli/native-hook-claim-journal.js.map +1 -1
- package/dist/cli/package-manager-ownership.d.ts +63 -0
- package/dist/cli/package-manager-ownership.d.ts.map +1 -0
- package/dist/cli/package-manager-ownership.js +323 -0
- package/dist/cli/package-manager-ownership.js.map +1 -0
- package/dist/cli/ralph.js +2 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +5 -7
- package/dist/cli/ralplan.d.ts.map +1 -1
- package/dist/cli/ralplan.js +83 -134
- package/dist/cli/ralplan.js.map +1 -1
- package/dist/cli/session-search.d.ts +5 -0
- package/dist/cli/session-search.d.ts.map +1 -1
- package/dist/cli/session-search.js +87 -0
- package/dist/cli/session-search.js.map +1 -1
- package/dist/cli/setup-preferences.d.ts +15 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +27 -1
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +119 -47
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +52 -9
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +2 -2
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts +9 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +71 -30
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update-worker.d.ts +2 -0
- package/dist/cli/update-worker.d.ts.map +1 -0
- package/dist/cli/update-worker.js +142 -0
- package/dist/cli/update-worker.js.map +1 -0
- package/dist/cli/update.d.ts +14 -22
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +192 -241
- package/dist/cli/update.js.map +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +503 -30
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +5 -3
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/__tests__/models.test.js +67 -3
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/generator.d.ts +27 -21
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +610 -152
- package/dist/config/generator.js.map +1 -1
- package/dist/config/models.d.ts +12 -2
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +15 -1
- package/dist/config/models.js.map +1 -1
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +5 -3
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +176 -37
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +402 -545
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +39 -4
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +161 -26
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +533 -14
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +159 -24
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +44 -4
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +3130 -365
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +1 -1
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js +20 -3
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner.js +1 -0
- package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
- package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
- package/dist/hooks/extensibility/runtime.js.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.js +2 -2
- package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
- package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
- package/dist/hooks/extensibility/sdk.d.ts +1 -0
- package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk.js +1 -1
- package/dist/hooks/extensibility/sdk.js.map +1 -1
- package/dist/hooks/extensibility/types.d.ts +7 -0
- package/dist/hooks/extensibility/types.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +10 -3
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +116 -20
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/session.d.ts +204 -14
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1500 -223
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +26 -16
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +83 -78
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +36 -31
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +155 -198
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/tmux.d.ts +42 -4
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +1017 -139
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +124 -37
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +17 -6
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +102 -26
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
- package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
- package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
- package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
- package/dist/mcp/__tests__/state-paths.test.js +735 -4
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +1 -1
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/__tests__/trace-server.test.js +2 -2
- package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
- package/dist/mcp/__tests__/wiki-server.test.js +18 -0
- package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js +13 -3
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/hermes-bridge.d.ts.map +1 -1
- package/dist/mcp/hermes-bridge.js +2 -2
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +78 -4
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +543 -40
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +4 -4
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.d.ts +11 -0
- package/dist/mcp/wiki-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.js +9 -2
- package/dist/mcp/wiki-server.js.map +1 -1
- package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
- package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +17 -8
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
- package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
- package/dist/modes/base.d.ts +5 -5
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +76 -21
- package/dist/modes/base.js.map +1 -1
- package/dist/native-assets/archive.d.ts +17 -0
- package/dist/native-assets/archive.d.ts.map +1 -0
- package/dist/native-assets/archive.js +238 -0
- package/dist/native-assets/archive.js.map +1 -0
- package/dist/native-assets/policy.d.ts +42 -0
- package/dist/native-assets/policy.d.ts.map +1 -0
- package/dist/native-assets/policy.js +155 -0
- package/dist/native-assets/policy.js.map +1 -0
- package/dist/notifications/temp-contract.d.ts.map +1 -1
- package/dist/notifications/temp-contract.js +4 -0
- package/dist/notifications/temp-contract.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +164 -42
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +72 -155
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +124 -18
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
- package/dist/pipeline/stages/ralplan.js +36 -8
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +73 -1316
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js +335 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
- package/dist/ralplan/__tests__/runtime.test.js +85 -101
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +9 -1
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +82 -338
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/documented-leader-preflight.d.ts +41 -0
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
- package/dist/ralplan/documented-leader-preflight.js +360 -0
- package/dist/ralplan/documented-leader-preflight.js.map +1 -0
- package/dist/ralplan/runtime.d.ts +2 -1
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +67 -168
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/__tests__/bridge.test.js +42 -1
- package/dist/runtime/__tests__/bridge.test.js.map +1 -1
- package/dist/runtime/bridge.d.ts +12 -0
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js +52 -1
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/runtime/run-state.d.ts +5 -1
- package/dist/runtime/run-state.d.ts.map +1 -1
- package/dist/runtime/run-state.js +15 -8
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +16323 -8978
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
- package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
- package/dist/scripts/__tests__/postinstall.test.js +19 -1
- package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +63 -173
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +74 -159
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +404 -13
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts +53 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +14448 -1480
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts +9 -0
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +35 -12
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/generate-native-release-manifest.js +80 -51
- package/dist/scripts/generate-native-release-manifest.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +78 -115
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +74 -3
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +5 -0
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +234 -79
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +71 -16
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +42 -21
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +82 -33
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +32 -3
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.js +79 -82
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall-advisory.d.ts +14 -0
- package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
- package/dist/scripts/postinstall-advisory.js +49 -0
- package/dist/scripts/postinstall-advisory.js.map +1 -0
- package/dist/scripts/postinstall.d.ts +1 -1
- package/dist/scripts/postinstall.d.ts.map +1 -1
- package/dist/scripts/postinstall.js +1 -1
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +80 -4
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +3162 -26
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +16 -1
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/verify-native-release-assets.js +37 -23
- package/dist/scripts/verify-native-release-assets.js.map +1 -1
- package/dist/state/__tests__/mode-state-context.test.js +61 -0
- package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +462 -850
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +400 -2
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/mode-state-context.d.ts +8 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +73 -0
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/operations.d.ts +3 -0
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +144 -40
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts +18 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +420 -23
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts +3 -0
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +10 -4
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +3 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +53 -247
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
- package/dist/subagents/__tests__/tracker.test.js +837 -1048
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/tracker.d.ts +58 -135
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +811 -632
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/api-interop.test.js +91 -1
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +58 -17
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/idle-nudge.test.js +45 -15
- package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
- package/dist/team/__tests__/mcp-comm.test.js +197 -30
- package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +110 -3
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
- package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
- package/dist/team/__tests__/notice-ledger.test.js +131 -0
- package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
- package/dist/team/__tests__/phase-controller.test.js +20 -0
- package/dist/team/__tests__/phase-controller.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +9 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +3963 -428
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +2405 -203
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js +117 -2
- package/dist/team/__tests__/state-root.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +804 -36
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/team-ops-contract.test.js +6 -0
- package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +4099 -1039
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.js +242 -15
- package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +53 -44
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/__tests__/worktree.test.js +6 -0
- package/dist/team/__tests__/worktree.test.js.map +1 -1
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +15 -9
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/exact-pane.d.ts +23 -0
- package/dist/team/exact-pane.d.ts.map +1 -0
- package/dist/team/exact-pane.js +133 -0
- package/dist/team/exact-pane.js.map +1 -0
- package/dist/team/idle-nudge.d.ts +12 -1
- package/dist/team/idle-nudge.d.ts.map +1 -1
- package/dist/team/idle-nudge.js +41 -14
- package/dist/team/idle-nudge.js.map +1 -1
- package/dist/team/model-contract.d.ts +2 -5
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +59 -17
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/notice-ledger.d.ts +54 -0
- package/dist/team/notice-ledger.d.ts.map +1 -0
- package/dist/team/notice-ledger.js +259 -0
- package/dist/team/notice-ledger.js.map +1 -0
- package/dist/team/phase-controller.d.ts.map +1 -1
- package/dist/team/phase-controller.js +2 -0
- package/dist/team/phase-controller.js.map +1 -1
- package/dist/team/runtime.d.ts +4 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +1527 -291
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts +13 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +1503 -387
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/dispatch.d.ts.map +1 -1
- package/dist/team/state/dispatch.js +26 -8
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/state/locks.d.ts.map +1 -1
- package/dist/team/state/locks.js +61 -13
- package/dist/team/state/locks.js.map +1 -1
- package/dist/team/state/monitor.d.ts +10 -0
- package/dist/team/state/monitor.d.ts.map +1 -1
- package/dist/team/state/monitor.js +13 -0
- package/dist/team/state/monitor.js.map +1 -1
- package/dist/team/state/tasks.d.ts.map +1 -1
- package/dist/team/state/tasks.js +4 -3
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state/types.d.ts +15 -0
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state-root.d.ts +12 -0
- package/dist/team/state-root.d.ts.map +1 -1
- package/dist/team/state-root.js +177 -0
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.d.ts +85 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +672 -129
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-ops.d.ts +5 -0
- package/dist/team/team-ops.d.ts.map +1 -1
- package/dist/team/team-ops.js +4 -0
- package/dist/team/team-ops.js.map +1 -1
- package/dist/team/tmux-session.d.ts +163 -27
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +1682 -376
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worktree.d.ts.map +1 -1
- package/dist/team/worktree.js +4 -0
- package/dist/team/worktree.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +35 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +232 -46
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
- package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
- package/dist/utils/__tests__/file-durability.test.js +114 -0
- package/dist/utils/__tests__/file-durability.test.js.map +1 -0
- package/dist/utils/__tests__/paths.test.js +21 -1
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/file-durability.d.ts +25 -0
- package/dist/utils/file-durability.d.ts.map +1 -0
- package/dist/utils/file-durability.js +67 -0
- package/dist/utils/file-durability.js.map +1 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +14 -6
- package/dist/utils/paths.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
- package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
- package/dist/wiki/lint.d.ts +4 -1
- package/dist/wiki/lint.d.ts.map +1 -1
- package/dist/wiki/lint.js +3 -2
- package/dist/wiki/lint.js.map +1 -1
- package/package.json +13 -5
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +105 -17
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
- package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +16 -17
- package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
- package/skills/autopilot/SKILL.md +13 -5
- package/skills/cancel/SKILL.md +30 -85
- package/skills/ralph/SKILL.md +6 -4
- package/skills/ralplan/SKILL.md +16 -17
- package/skills/team/SKILL.md +13 -9
- package/skills/ultragoal/SKILL.md +3 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +8831 -951
- package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
- package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
- package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
- package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
- package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
- package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
- package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
- package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
- package/src/scripts/__tests__/postinstall.test.ts +23 -1
- package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +67 -185
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +79 -178
- package/src/scripts/__tests__/smoke-packed-install.test.ts +525 -16
- package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
- package/src/scripts/codex-native-hook.ts +14238 -1429
- package/src/scripts/codex-native-pre-post.ts +43 -12
- package/src/scripts/generate-native-release-manifest.ts +83 -79
- package/src/scripts/notify-fallback-watcher.ts +91 -122
- package/src/scripts/notify-hook/auto-nudge.ts +71 -3
- package/src/scripts/notify-hook/state-io.ts +11 -0
- package/src/scripts/notify-hook/team-dispatch.ts +242 -75
- package/src/scripts/notify-hook/team-leader-nudge.ts +72 -22
- package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
- package/src/scripts/notify-hook/team-worker-stop.ts +43 -20
- package/src/scripts/notify-hook/team-worker.ts +82 -31
- package/src/scripts/notify-hook/tmux-injection.ts +33 -3
- package/src/scripts/notify-hook.ts +84 -82
- package/src/scripts/postinstall-advisory.ts +67 -0
- package/src/scripts/postinstall.ts +1 -1
- package/src/scripts/smoke-packed-install.ts +3608 -39
- package/src/scripts/sync-plugin-mirror.ts +17 -1
- package/src/scripts/verify-native-release-assets.ts +39 -31
- package/templates/AGENTS.md +1 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
- package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
- package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
- package/dist/subagents/adapted-role-binding.d.ts +0 -17
- package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
- package/dist/subagents/adapted-role-binding.js +0 -75
- package/dist/subagents/adapted-role-binding.js.map +0 -1
|
@@ -1,27 +1,61 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { closeSync, existsSync, ftruncateSync, linkSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, writeSync } from 'node:fs';
|
|
3
|
-
import {
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { hostname } from 'node:os';
|
|
5
5
|
import { basename, dirname, join } from 'node:path';
|
|
6
6
|
import { AGENT_DEFINITIONS } from '../agents/definitions.js';
|
|
7
|
-
import { getBaseStateDir
|
|
8
|
-
import { canonicalizeOriginCwd } from '../leader/contract.js';
|
|
7
|
+
import { getBaseStateDir } from '../state/paths.js';
|
|
9
8
|
import { codexAgentsDir, projectCodexAgentsDir } from '../utils/paths.js';
|
|
10
9
|
export const SUBAGENT_TRACKING_SCHEMA_VERSION = 1;
|
|
11
10
|
export const DEFAULT_SUBAGENT_ACTIVE_WINDOW_MS = 120_000;
|
|
12
|
-
export const OMX_ADAPTED_PROVENANCE = 'omx_adapted';
|
|
13
11
|
export const NATIVE_SUBAGENT_PROVENANCE = 'native_subagent';
|
|
12
|
+
// Legacy descriptive provenance written by pre-authority releases. Tolerated as
|
|
13
|
+
// descriptive-only evidence: it never grants reopen authority and never blocks a
|
|
14
|
+
// newly attested valid direct child, but it is preserved so authority-consistency
|
|
15
|
+
// checks can distinguish recognized legacy data from unknown descriptive views.
|
|
16
|
+
export const DESCRIPTIVE_ADAPTED_PROVENANCE = 'adapted';
|
|
14
17
|
const KNOWN_TYPED_AGENT_ROLES = new Set(Object.keys(AGENT_DEFINITIONS).map((role) => role.toLowerCase()));
|
|
15
18
|
export function subagentTrackingPath(cwd) {
|
|
16
19
|
return join(getBaseStateDir(cwd), 'subagent-tracking.json');
|
|
17
20
|
}
|
|
18
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Authority-relevant projection of a tracker state. The generic descriptive
|
|
23
|
+
* writer is allowed to publish descriptive churn, but it must never roll back
|
|
24
|
+
* an authority decision that another process committed after the caller read
|
|
25
|
+
* its snapshot. Comparing this projection is the durable equivalent of a CAS
|
|
26
|
+
* on the authority surface without forcing every descriptive caller to hold
|
|
27
|
+
* the tracker lock across its own read/modify/write.
|
|
28
|
+
*/
|
|
29
|
+
function authorityRevisionOf(state) {
|
|
30
|
+
const sessions = Object.keys(state.sessions).sort().map((sessionId) => {
|
|
31
|
+
const session = state.sessions[sessionId];
|
|
32
|
+
const threads = Object.keys(session.threads).sort().map((threadId) => {
|
|
33
|
+
const thread = session.threads[threadId];
|
|
34
|
+
return [
|
|
35
|
+
threadId,
|
|
36
|
+
thread.kind,
|
|
37
|
+
thread.provenance_kind ?? '',
|
|
38
|
+
thread.direct_child_root_id ?? '',
|
|
39
|
+
thread.direct_child_parent_id ?? '',
|
|
40
|
+
thread.reopen_authority_revoked === true ? '1' : '',
|
|
41
|
+
thread.reopen_authority_conflict_reason ?? '',
|
|
42
|
+
thread.reopen_authority_conflict_at ?? '',
|
|
43
|
+
// Availability gates whether an eligible child is actually emitted as
|
|
44
|
+
// a reopen target, so it is part of the authority surface.
|
|
45
|
+
thread.status ?? '',
|
|
46
|
+
].join('\u0001');
|
|
47
|
+
});
|
|
48
|
+
return [sessionId, session.leader_thread_id ?? '', ...threads].join('\u0002');
|
|
49
|
+
});
|
|
50
|
+
return sessions.join('\u0003');
|
|
51
|
+
}
|
|
52
|
+
export function resolveInstalledRoleName(role, codexHomeOverride, projectRootOverride) {
|
|
19
53
|
const normalizedRole = role.trim().toLowerCase();
|
|
20
54
|
if (!normalizedRole)
|
|
21
55
|
return null;
|
|
22
56
|
if (KNOWN_TYPED_AGENT_ROLES.has(normalizedRole))
|
|
23
57
|
return normalizedRole;
|
|
24
|
-
for (const agentsDir of [codexAgentsDir(codexHomeOverride), projectCodexAgentsDir()]) {
|
|
58
|
+
for (const agentsDir of [codexAgentsDir(codexHomeOverride), projectCodexAgentsDir(projectRootOverride)]) {
|
|
25
59
|
try {
|
|
26
60
|
for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
|
|
27
61
|
if (!entry.isFile() || !entry.name.endsWith('.toml'))
|
|
@@ -41,7 +75,6 @@ export function createSubagentTrackingState() {
|
|
|
41
75
|
return {
|
|
42
76
|
schemaVersion: SUBAGENT_TRACKING_SCHEMA_VERSION,
|
|
43
77
|
sessions: {},
|
|
44
|
-
pending_role_intents: [],
|
|
45
78
|
};
|
|
46
79
|
}
|
|
47
80
|
function normalizeSubagentStatus(value) {
|
|
@@ -119,40 +152,6 @@ export function isTrustedSubagentThread(session, threadId) {
|
|
|
119
152
|
return false;
|
|
120
153
|
return session.threads[normalizedThreadId]?.kind === 'subagent';
|
|
121
154
|
}
|
|
122
|
-
function normalizePendingRoleIntent(value) {
|
|
123
|
-
if (!value || typeof value !== 'object')
|
|
124
|
-
return null;
|
|
125
|
-
const candidate = value;
|
|
126
|
-
const role = readOptionalTrimmedString(candidate.role);
|
|
127
|
-
const sessionId = readOptionalTrimmedString(candidate.session_id);
|
|
128
|
-
const parentThreadId = readOptionalTrimmedString(candidate.parent_thread_id);
|
|
129
|
-
const createdAt = readOptionalTrimmedString(candidate.created_at);
|
|
130
|
-
const expiresAt = readOptionalTrimmedString(candidate.expires_at);
|
|
131
|
-
if (!role || !sessionId || !parentThreadId || !Object.hasOwn(candidate, 'correlation_token') || !createdAt || !expiresAt)
|
|
132
|
-
return null;
|
|
133
|
-
if (!Number.isFinite(Date.parse(createdAt)) || !Number.isFinite(Date.parse(expiresAt)))
|
|
134
|
-
return null;
|
|
135
|
-
const bindingState = candidate.binding_state === 'bound' ? 'bound' : undefined;
|
|
136
|
-
const boundAt = readOptionalTrimmedString(candidate.bound_at);
|
|
137
|
-
const hasValidBoundAt = Boolean(boundAt && Number.isFinite(Date.parse(boundAt)));
|
|
138
|
-
const originCwd = readOptionalTrimmedString(candidate.origin_cwd);
|
|
139
|
-
return {
|
|
140
|
-
role,
|
|
141
|
-
session_id: sessionId,
|
|
142
|
-
parent_thread_id: parentThreadId,
|
|
143
|
-
// Bound journals are durable security records. Retain malformed credentials verbatim so
|
|
144
|
-
// completion can reject them rather than silently converting them into claimant-less data.
|
|
145
|
-
correlation_token: candidate.correlation_token,
|
|
146
|
-
created_at: createdAt,
|
|
147
|
-
expires_at: expiresAt,
|
|
148
|
-
...(bindingState ? { binding_state: bindingState } : {}),
|
|
149
|
-
...(bindingState && Object.hasOwn(candidate, 'binding_claimant_token')
|
|
150
|
-
? { binding_claimant_token: candidate.binding_claimant_token }
|
|
151
|
-
: {}),
|
|
152
|
-
...(bindingState && hasValidBoundAt ? { bound_at: boundAt } : {}),
|
|
153
|
-
...(originCwd ? { origin_cwd: originCwd } : {}),
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
155
|
export function normalizeSubagentTrackingState(input) {
|
|
157
156
|
const base = createSubagentTrackingState();
|
|
158
157
|
if (!input || typeof input !== 'object')
|
|
@@ -190,9 +189,11 @@ export function normalizeSubagentTrackingState(input) {
|
|
|
190
189
|
turn_count: typeof candidate.turn_count === 'number' && Number.isFinite(candidate.turn_count) && candidate.turn_count > 0 ? candidate.turn_count : 1,
|
|
191
190
|
...(typeof candidate.mode === 'string' && candidate.mode.trim().length > 0 ? { mode: candidate.mode } : {}),
|
|
192
191
|
...(typeof candidate.role === 'string' && candidate.role.trim().length > 0 ? { role: candidate.role.trim() } : {}),
|
|
193
|
-
...(
|
|
194
|
-
? { provenance_kind:
|
|
195
|
-
:
|
|
192
|
+
...(candidate.provenance_kind === NATIVE_SUBAGENT_PROVENANCE
|
|
193
|
+
? { provenance_kind: NATIVE_SUBAGENT_PROVENANCE }
|
|
194
|
+
: candidate.provenance_kind === DESCRIPTIVE_ADAPTED_PROVENANCE
|
|
195
|
+
? { provenance_kind: DESCRIPTIVE_ADAPTED_PROVENANCE }
|
|
196
|
+
: {}),
|
|
196
197
|
...(typeof candidate.lane_id === 'string' && candidate.lane_id.trim().length > 0 ? { lane_id: candidate.lane_id.trim() } : {}),
|
|
197
198
|
...(typeof candidate.scope === 'string' && candidate.scope.trim().length > 0 ? { scope: candidate.scope.trim() } : {}),
|
|
198
199
|
...(typeof candidate.agent_nickname === 'string' && candidate.agent_nickname.trim().length > 0
|
|
@@ -217,37 +218,117 @@ export function normalizeSubagentTrackingState(input) {
|
|
|
217
218
|
...(typeof candidate.resume_failure_reason === 'string' && candidate.resume_failure_reason.trim().length > 0
|
|
218
219
|
? { resume_failure_reason: candidate.resume_failure_reason.trim() }
|
|
219
220
|
: {}),
|
|
221
|
+
...(typeof candidate.direct_child_root_id === 'string' && candidate.direct_child_root_id.trim().length > 0
|
|
222
|
+
? { direct_child_root_id: candidate.direct_child_root_id.trim() }
|
|
223
|
+
: {}),
|
|
224
|
+
...(typeof candidate.direct_child_parent_id === 'string' && candidate.direct_child_parent_id.trim().length > 0
|
|
225
|
+
? { direct_child_parent_id: candidate.direct_child_parent_id.trim() }
|
|
226
|
+
: {}),
|
|
227
|
+
...(candidate.reopen_authority_revoked === true ? { reopen_authority_revoked: true } : {}),
|
|
228
|
+
...(typeof candidate.reopen_authority_conflict_reason === 'string' && candidate.reopen_authority_conflict_reason.trim()
|
|
229
|
+
? { reopen_authority_conflict_reason: candidate.reopen_authority_conflict_reason.trim() }
|
|
230
|
+
: {}),
|
|
231
|
+
...(typeof candidate.reopen_authority_conflict_at === 'string' && candidate.reopen_authority_conflict_at.trim()
|
|
232
|
+
? { reopen_authority_conflict_at: candidate.reopen_authority_conflict_at.trim() }
|
|
233
|
+
: {}),
|
|
220
234
|
};
|
|
221
235
|
}
|
|
222
236
|
const sessionCandidate = rawSession;
|
|
223
237
|
const leaderThreadId = typeof sessionCandidate.leader_thread_id === 'string' ? sessionCandidate.leader_thread_id.trim() || undefined : undefined;
|
|
224
|
-
const leaderAttestedAt = typeof sessionCandidate.leader_attested_at === 'string' && sessionCandidate.leader_attested_at.trim().length > 0
|
|
225
|
-
? sessionCandidate.leader_attested_at.trim()
|
|
226
|
-
: undefined;
|
|
227
|
-
const leaderAttestSource = typeof sessionCandidate.leader_attest_source === 'string' && sessionCandidate.leader_attest_source.trim().length > 0
|
|
228
|
-
? sessionCandidate.leader_attest_source.trim()
|
|
229
|
-
: undefined;
|
|
230
238
|
const updatedAt = typeof sessionCandidate.updated_at === 'string' && sessionCandidate.updated_at.trim().length > 0
|
|
231
239
|
? sessionCandidate.updated_at
|
|
232
240
|
: new Date(0).toISOString();
|
|
233
241
|
sessions[sessionId] = {
|
|
234
242
|
session_id: sessionId,
|
|
235
|
-
leader_thread_id: leaderThreadId,
|
|
236
|
-
...(leaderAttestedAt ? { leader_attested_at: leaderAttestedAt } : {}),
|
|
237
|
-
...(leaderAttestSource ? { leader_attest_source: leaderAttestSource } : {}),
|
|
243
|
+
...(leaderThreadId ? { leader_thread_id: leaderThreadId } : {}),
|
|
238
244
|
updated_at: updatedAt,
|
|
239
245
|
threads,
|
|
240
246
|
};
|
|
241
247
|
}
|
|
242
|
-
const pendingRoleIntents = Array.isArray(parsed.pending_role_intents)
|
|
243
|
-
? parsed.pending_role_intents.map((intent) => normalizePendingRoleIntent(intent)).filter((intent) => intent !== null)
|
|
244
|
-
: [];
|
|
245
248
|
return {
|
|
246
249
|
schemaVersion: SUBAGENT_TRACKING_SCHEMA_VERSION,
|
|
247
250
|
sessions,
|
|
248
|
-
pending_role_intents: pendingRoleIntents,
|
|
249
251
|
};
|
|
250
252
|
}
|
|
253
|
+
function parseStrictSubagentTrackingState(raw) {
|
|
254
|
+
let parsed;
|
|
255
|
+
try {
|
|
256
|
+
parsed = JSON.parse(raw);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (!parsed || typeof parsed !== 'object')
|
|
262
|
+
return null;
|
|
263
|
+
const candidate = parsed;
|
|
264
|
+
if (candidate.schemaVersion !== SUBAGENT_TRACKING_SCHEMA_VERSION || !candidate.sessions || typeof candidate.sessions !== 'object' || Array.isArray(candidate.sessions))
|
|
265
|
+
return null;
|
|
266
|
+
const normalizedSessionIds = new Set();
|
|
267
|
+
for (const [sessionId, sessionValue] of Object.entries(candidate.sessions)) {
|
|
268
|
+
if (!sessionId || sessionId.trim() !== sessionId || normalizedSessionIds.has(sessionId.trim()))
|
|
269
|
+
return null;
|
|
270
|
+
normalizedSessionIds.add(sessionId.trim());
|
|
271
|
+
if (!sessionValue || typeof sessionValue !== 'object' || Array.isArray(sessionValue))
|
|
272
|
+
return null;
|
|
273
|
+
const session = sessionValue;
|
|
274
|
+
if (session.session_id !== sessionId || typeof session.updated_at !== 'string' || !session.updated_at.trim())
|
|
275
|
+
return null;
|
|
276
|
+
if (!session.threads || typeof session.threads !== 'object' || Array.isArray(session.threads))
|
|
277
|
+
return null;
|
|
278
|
+
if (session.leader_thread_id !== undefined && (typeof session.leader_thread_id !== 'string' || !session.leader_thread_id.trim() || session.leader_thread_id.trim() !== session.leader_thread_id))
|
|
279
|
+
return null;
|
|
280
|
+
const normalizedThreadIds = new Set();
|
|
281
|
+
for (const [threadId, threadValue] of Object.entries(session.threads)) {
|
|
282
|
+
if (!threadId || threadId.trim() !== threadId || normalizedThreadIds.has(threadId.trim()))
|
|
283
|
+
return null;
|
|
284
|
+
normalizedThreadIds.add(threadId.trim());
|
|
285
|
+
if (!threadValue || typeof threadValue !== 'object' || Array.isArray(threadValue))
|
|
286
|
+
return null;
|
|
287
|
+
const thread = threadValue;
|
|
288
|
+
if (thread.thread_id !== threadId || (thread.kind !== 'leader' && thread.kind !== 'subagent'))
|
|
289
|
+
return null;
|
|
290
|
+
if (typeof thread.first_seen_at !== 'string' || !thread.first_seen_at.trim())
|
|
291
|
+
return null;
|
|
292
|
+
if (typeof thread.last_seen_at !== 'string' || !thread.last_seen_at.trim())
|
|
293
|
+
return null;
|
|
294
|
+
if (typeof thread.turn_count !== 'number' || !Number.isFinite(thread.turn_count) || thread.turn_count < 1)
|
|
295
|
+
return null;
|
|
296
|
+
const hasAuthorityEvidence = thread.provenance_kind === NATIVE_SUBAGENT_PROVENANCE
|
|
297
|
+
|| thread.direct_child_root_id !== undefined
|
|
298
|
+
|| thread.direct_child_parent_id !== undefined
|
|
299
|
+
|| thread.reopen_authority_revoked !== undefined
|
|
300
|
+
|| thread.reopen_authority_conflict_reason !== undefined
|
|
301
|
+
|| thread.reopen_authority_conflict_at !== undefined;
|
|
302
|
+
if (hasAuthorityEvidence && thread.status !== undefined && normalizeSubagentStatus(thread.status) === undefined)
|
|
303
|
+
return null;
|
|
304
|
+
if (hasAuthorityEvidence && thread.provenance_kind !== undefined && thread.provenance_kind !== NATIVE_SUBAGENT_PROVENANCE)
|
|
305
|
+
return null;
|
|
306
|
+
for (const field of ['direct_child_root_id', 'direct_child_parent_id', 'reopen_authority_conflict_reason', 'reopen_authority_conflict_at']) {
|
|
307
|
+
if (thread[field] !== undefined && (typeof thread[field] !== 'string' || !thread[field].trim() || thread[field].trim() !== thread[field]))
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
if (thread.reopen_authority_revoked !== undefined && typeof thread.reopen_authority_revoked !== 'boolean')
|
|
311
|
+
return null;
|
|
312
|
+
if ((thread.direct_child_root_id === undefined) !== (thread.direct_child_parent_id === undefined))
|
|
313
|
+
return null;
|
|
314
|
+
if ((thread.reopen_authority_conflict_reason !== undefined || thread.reopen_authority_conflict_at !== undefined) && thread.reopen_authority_revoked !== true)
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return normalizeSubagentTrackingState(parsed);
|
|
319
|
+
}
|
|
320
|
+
function readSubagentTrackingStateStrictSync(cwd) {
|
|
321
|
+
const path = subagentTrackingPath(cwd);
|
|
322
|
+
if (!existsSync(path))
|
|
323
|
+
return { ok: true, state: createSubagentTrackingState() };
|
|
324
|
+
try {
|
|
325
|
+
const state = parseStrictSubagentTrackingState(readFileSync(path, 'utf-8'));
|
|
326
|
+
return state ? { ok: true, state } : { ok: false };
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return { ok: false };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
251
332
|
function atomicTrackingTempPath(path) {
|
|
252
333
|
return `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
253
334
|
}
|
|
@@ -587,41 +668,14 @@ export function withCrossProcessFileLockSync(resourcePath, operation, options =
|
|
|
587
668
|
releaseCrossProcessFileLock(lockPath, token);
|
|
588
669
|
}
|
|
589
670
|
}
|
|
590
|
-
function
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
return createSubagentTrackingState();
|
|
594
|
-
try {
|
|
595
|
-
return normalizeSubagentTrackingState(JSON.parse(readFileSync(path, 'utf-8')));
|
|
596
|
-
}
|
|
597
|
-
catch {
|
|
598
|
-
return createSubagentTrackingState();
|
|
599
|
-
}
|
|
671
|
+
function threadIsTrackedAsSubagent(state, threadId) {
|
|
672
|
+
const id = threadId.trim();
|
|
673
|
+
return Boolean(id) && Object.values(state.sessions).some((session) => isTrustedSubagentThread(session, id));
|
|
600
674
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
// NOT be silently treated as empty — that would let corrupt/unreadable subagent evidence
|
|
604
|
-
// be bypassed. Callers making a security decision (attest vs deny) use these.
|
|
605
|
-
function readSubagentTrackingStateSyncStrict(cwd) {
|
|
606
|
-
const path = subagentTrackingPath(cwd);
|
|
607
|
-
let raw;
|
|
608
|
-
try {
|
|
609
|
-
raw = readFileSync(path, 'utf-8');
|
|
610
|
-
}
|
|
611
|
-
catch (error) {
|
|
612
|
-
// Only a genuine ENOENT (no file) is a clean empty state. Any other read/access error
|
|
613
|
-
// (permissions, I/O, ELOOP, …) must fail closed rather than be treated as empty.
|
|
614
|
-
if (error?.code === 'ENOENT')
|
|
615
|
-
return { ok: true, state: createSubagentTrackingState() };
|
|
616
|
-
return { ok: false };
|
|
617
|
-
}
|
|
618
|
-
try {
|
|
619
|
-
return { ok: true, state: normalizeSubagentTrackingState(JSON.parse(raw)) };
|
|
620
|
-
}
|
|
621
|
-
catch {
|
|
622
|
-
return { ok: false };
|
|
623
|
-
}
|
|
675
|
+
export function hasLeaderSubagentCollision(state, leaderThreadId) {
|
|
676
|
+
return threadIsTrackedAsSubagent(state, leaderThreadId);
|
|
624
677
|
}
|
|
678
|
+
// Strict reads preserve fail-closed behavior for security-sensitive tracker decisions.
|
|
625
679
|
export async function readSubagentTrackingStateStrict(cwd) {
|
|
626
680
|
const path = subagentTrackingPath(cwd);
|
|
627
681
|
let raw;
|
|
@@ -633,22 +687,8 @@ export async function readSubagentTrackingStateStrict(cwd) {
|
|
|
633
687
|
return { ok: true, state: createSubagentTrackingState() };
|
|
634
688
|
return { ok: false };
|
|
635
689
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
639
|
-
catch {
|
|
640
|
-
return { ok: false };
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
function threadIsTrackedAsSubagent(state, threadId) {
|
|
644
|
-
const id = threadId.trim();
|
|
645
|
-
if (!id)
|
|
646
|
-
return false;
|
|
647
|
-
for (const session of Object.values(state.sessions)) {
|
|
648
|
-
if (session.threads[id]?.kind === 'subagent')
|
|
649
|
-
return true;
|
|
650
|
-
}
|
|
651
|
-
return false;
|
|
690
|
+
const state = parseStrictSubagentTrackingState(raw);
|
|
691
|
+
return state ? { ok: true, state } : { ok: false };
|
|
652
692
|
}
|
|
653
693
|
function writeSubagentTrackingStateSync(cwd, state, publish) {
|
|
654
694
|
const normalized = normalizeSubagentTrackingState(state);
|
|
@@ -676,526 +716,61 @@ export async function readSubagentTrackingState(cwd) {
|
|
|
676
716
|
}
|
|
677
717
|
}
|
|
678
718
|
export async function writeSubagentTrackingState(cwd, state) {
|
|
679
|
-
const normalized = normalizeSubagentTrackingState(state);
|
|
680
719
|
const path = subagentTrackingPath(cwd);
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
return
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
? canonicalizeOriginCwd(intent.origin_cwd) === canonicalOrigin
|
|
700
|
-
: isCwdPartitionedStateRoot);
|
|
701
|
-
const shouldPruneExpired = (intent) => (isOwn(intent)
|
|
702
|
-
&& intent.binding_state !== 'bound'
|
|
703
|
-
&& isExpiredPendingRoleIntent(intent, nowMs));
|
|
704
|
-
return { isOwn, shouldPruneExpired };
|
|
705
|
-
}
|
|
706
|
-
export function isRoleIntentOwnedByCwd(cwd, intent) {
|
|
707
|
-
return pendingRoleIntentPredicates(cwd, canonicalizeOriginCwd(cwd), Date.now()).isOwn(intent);
|
|
708
|
-
}
|
|
709
|
-
function sameLogicalRoleIntent(intent, sessionId, parentThreadId, correlationToken) {
|
|
710
|
-
return (intent.session_id === sessionId
|
|
711
|
-
&& intent.parent_thread_id === parentThreadId
|
|
712
|
-
&& (correlationToken === undefined || intent.correlation_token === correlationToken));
|
|
713
|
-
}
|
|
714
|
-
export function isCanonicalCorrelationToken(value) {
|
|
715
|
-
return typeof value === 'string' && /^[0-9a-f]{32}$/.test(value);
|
|
716
|
-
}
|
|
717
|
-
export function isCanonicalClaimantToken(value) {
|
|
718
|
-
return typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
|
|
719
|
-
}
|
|
720
|
-
function hasOwnBoundLogicalIntent(all, isOwn, sessionId, parentThreadId, correlationToken) {
|
|
721
|
-
return all.some((intent) => (isOwn(intent)
|
|
722
|
-
&& intent.binding_state === 'bound'
|
|
723
|
-
&& sameLogicalRoleIntent(intent, sessionId, parentThreadId, correlationToken)));
|
|
724
|
-
}
|
|
725
|
-
function selectDominantRoleIntent(candidates, canonicalOrigin) {
|
|
726
|
-
return [...candidates].sort((left, right) => {
|
|
727
|
-
const leftIsBound = left.binding_state === 'bound';
|
|
728
|
-
const rightIsBound = right.binding_state === 'bound';
|
|
729
|
-
if (leftIsBound !== rightIsBound)
|
|
730
|
-
return leftIsBound ? -1 : 1;
|
|
731
|
-
const leftIsExactOrigin = left.origin_cwd !== undefined
|
|
732
|
-
&& canonicalizeOriginCwd(left.origin_cwd) === canonicalOrigin;
|
|
733
|
-
const rightIsExactOrigin = right.origin_cwd !== undefined
|
|
734
|
-
&& canonicalizeOriginCwd(right.origin_cwd) === canonicalOrigin;
|
|
735
|
-
if (leftIsExactOrigin !== rightIsExactOrigin)
|
|
736
|
-
return leftIsExactOrigin ? -1 : 1;
|
|
737
|
-
const leftStableKey = [
|
|
738
|
-
left.role,
|
|
739
|
-
left.correlation_token,
|
|
740
|
-
left.created_at,
|
|
741
|
-
left.expires_at,
|
|
742
|
-
left.binding_state ?? '',
|
|
743
|
-
left.binding_claimant_token ?? '',
|
|
744
|
-
left.bound_at ?? '',
|
|
745
|
-
left.origin_cwd ?? '',
|
|
746
|
-
].join('\u0000');
|
|
747
|
-
const rightStableKey = [
|
|
748
|
-
right.role,
|
|
749
|
-
right.correlation_token,
|
|
750
|
-
right.created_at,
|
|
751
|
-
right.expires_at,
|
|
752
|
-
right.binding_state ?? '',
|
|
753
|
-
right.binding_claimant_token ?? '',
|
|
754
|
-
right.bound_at ?? '',
|
|
755
|
-
right.origin_cwd ?? '',
|
|
756
|
-
].join('\u0000');
|
|
757
|
-
return leftStableKey.localeCompare(rightStableKey);
|
|
758
|
-
})[0] ?? null;
|
|
759
|
-
}
|
|
760
|
-
export function recordPendingRoleIntent(cwd, input) {
|
|
761
|
-
const role = resolveInstalledRoleName(input.role);
|
|
762
|
-
if (!role)
|
|
763
|
-
return { ok: false, reason: 'unknown_role' };
|
|
764
|
-
const correlationToken = input.correlationToken;
|
|
765
|
-
if (!isCanonicalCorrelationToken(correlationToken)) {
|
|
766
|
-
return { ok: false, reason: 'invalid_correlation_token' };
|
|
767
|
-
}
|
|
768
|
-
const nowMs = normalizeNowMs(input.nowMs);
|
|
769
|
-
const sessionId = input.sessionId.trim();
|
|
770
|
-
const parentThreadId = input.parentThreadId.trim();
|
|
771
|
-
const canonicalOrigin = canonicalizeOriginCwd(cwd);
|
|
772
|
-
if (canonicalOrigin === null)
|
|
773
|
-
return { ok: false, reason: 'invalid_origin' };
|
|
774
|
-
const { isOwn, shouldPruneExpired } = pendingRoleIntentPredicates(cwd, canonicalOrigin, nowMs);
|
|
775
|
-
return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
|
|
776
|
-
const state = readSubagentTrackingStateSync(cwd);
|
|
777
|
-
const all = state.pending_role_intents;
|
|
778
|
-
if (all.some((intent) => (isOwn(intent)
|
|
779
|
-
&& intent.session_id === sessionId
|
|
780
|
-
&& intent.parent_thread_id === parentThreadId
|
|
781
|
-
&& (intent.binding_state === 'bound' || !isExpiredPendingRoleIntent(intent, nowMs))))) {
|
|
782
|
-
return { ok: false, reason: 'single_flight_conflict' };
|
|
783
|
-
}
|
|
784
|
-
const ttlMs = typeof input.ttlMs === 'number' && Number.isFinite(input.ttlMs) ? input.ttlMs : 10 * 60_000;
|
|
785
|
-
const intent = {
|
|
786
|
-
role,
|
|
787
|
-
session_id: sessionId,
|
|
788
|
-
parent_thread_id: parentThreadId,
|
|
789
|
-
correlation_token: correlationToken,
|
|
790
|
-
created_at: new Date(nowMs).toISOString(),
|
|
791
|
-
expires_at: new Date(nowMs + ttlMs).toISOString(),
|
|
792
|
-
// Store the canonical origin workspace so it authenticates future bind/complete/recover.
|
|
793
|
-
...(canonicalOrigin ? { origin_cwd: canonicalOrigin } : {}),
|
|
794
|
-
};
|
|
795
|
-
state.pending_role_intents = [...all.filter((candidate) => !shouldPruneExpired(candidate)), intent];
|
|
796
|
-
context.assertOwnership();
|
|
797
|
-
writeSubagentTrackingStateSync(cwd, state, context.publish);
|
|
798
|
-
return { ok: true, intent };
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
/**
|
|
802
|
-
* #3181 Phase 1 carrier write. Durably attest the authenticated leader thread for a
|
|
803
|
-
* session from native-hook-reconciled metadata. Idempotent and fail-closed: it seeds
|
|
804
|
-
* `leader_thread_id` + `leader_attested_at` + `leader_attest_source` only when the
|
|
805
|
-
* session has no conflicting leader already. A different existing leader is NEVER
|
|
806
|
-
* overwritten (returns `native_anchor_mismatch`) so a stale/foreign leader cannot be
|
|
807
|
-
* replaced from a later turn. Authority for `leaderThreadId` is the caller's native
|
|
808
|
-
* payload thread; it must never be derived from a native session id.
|
|
809
|
-
*/
|
|
810
|
-
export function attestLeaderThread(cwd, input) {
|
|
811
|
-
const sessionId = input.sessionId.trim();
|
|
812
|
-
const leaderThreadId = input.leaderThreadId.trim();
|
|
813
|
-
const source = input.source.trim() || 'native';
|
|
814
|
-
if (!sessionId || !leaderThreadId)
|
|
815
|
-
return { ok: false, reason: 'native_anchor_unavailable' };
|
|
816
|
-
const nowMs = normalizeNowMs(input.nowMs);
|
|
817
|
-
const nowIso = new Date(nowMs).toISOString();
|
|
818
|
-
return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
|
|
819
|
-
// Strict read under the lock: an unreadable/corrupt tracker denies attestation
|
|
820
|
-
// (fail closed) rather than silently reading empty.
|
|
821
|
-
const read = readSubagentTrackingStateSyncStrict(cwd);
|
|
822
|
-
if (!read.ok)
|
|
823
|
-
return { ok: false, reason: 'native_anchor_unavailable' };
|
|
824
|
-
const state = read.state;
|
|
825
|
-
// Atomic positive counter-evidence: a thread tracked as a subagent in ANY session is
|
|
826
|
-
// never attested as a leader. Checked under the same lock as the write so a concurrent
|
|
827
|
-
// child record cannot race in between a caller's pre-check and this attestation.
|
|
828
|
-
if (threadIsTrackedAsSubagent(state, leaderThreadId)) {
|
|
829
|
-
return { ok: false, reason: 'native_anchor_mismatch' };
|
|
830
|
-
}
|
|
831
|
-
const existing = state.sessions[sessionId];
|
|
832
|
-
const existingLeader = existing?.leader_thread_id?.trim();
|
|
833
|
-
if (existingLeader && existingLeader !== leaderThreadId) {
|
|
834
|
-
return { ok: false, reason: 'native_anchor_mismatch' };
|
|
835
|
-
}
|
|
836
|
-
if (existing && existingLeader === leaderThreadId && existing.leader_attested_at) {
|
|
837
|
-
return { ok: true, alreadyAttested: true };
|
|
838
|
-
}
|
|
839
|
-
const session = existing
|
|
840
|
-
? { ...existing }
|
|
841
|
-
: { session_id: sessionId, updated_at: nowIso, threads: {} };
|
|
842
|
-
session.leader_thread_id = leaderThreadId;
|
|
843
|
-
session.leader_attested_at = nowIso;
|
|
844
|
-
session.leader_attest_source = source;
|
|
845
|
-
session.updated_at = nowIso;
|
|
846
|
-
state.sessions = { ...state.sessions, [sessionId]: session };
|
|
847
|
-
context.assertOwnership();
|
|
848
|
-
writeSubagentTrackingStateSync(cwd, state, context.publish);
|
|
849
|
-
return { ok: true, alreadyAttested: false };
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
/**
|
|
853
|
-
* #3181 Phase 2. Single tracker-locked publish that self-heals the leader thread record
|
|
854
|
-
* from a durable native attestation and records the pending role intent in the same
|
|
855
|
-
* operation, closing the previous verify-then-record TOCTOU window. Fail-closed:
|
|
856
|
-
* requires a durable attested leader for the session (`native_anchor_unavailable`
|
|
857
|
-
* otherwise) and requires `parentThreadId` to equal the attested `leader_thread_id`
|
|
858
|
-
* (`native_anchor_mismatch` otherwise). `leaderThreadId` is never inferred from a
|
|
859
|
-
* native session id. Duplicate same-identity intents reuse the original receipt
|
|
860
|
-
* (idempotent), never a second leader or a second intent.
|
|
861
|
-
*/
|
|
862
|
-
export function ensureLeaderAndRecordIntent(cwd, input) {
|
|
863
|
-
const role = resolveInstalledRoleName(input.role);
|
|
864
|
-
if (!role)
|
|
865
|
-
return { ok: false, reason: 'unknown_role' };
|
|
866
|
-
const correlationToken = input.correlationToken;
|
|
867
|
-
if (!isCanonicalCorrelationToken(correlationToken)) {
|
|
868
|
-
return { ok: false, reason: 'invalid_correlation_token' };
|
|
869
|
-
}
|
|
870
|
-
const nowMs = normalizeNowMs(input.nowMs);
|
|
871
|
-
const sessionId = input.sessionId.trim();
|
|
872
|
-
const parentThreadId = input.parentThreadId.trim();
|
|
873
|
-
const canonicalOrigin = canonicalizeOriginCwd(cwd);
|
|
874
|
-
if (canonicalOrigin === null)
|
|
875
|
-
return { ok: false, reason: 'invalid_origin' };
|
|
876
|
-
const { isOwn, shouldPruneExpired } = pendingRoleIntentPredicates(cwd, canonicalOrigin, nowMs);
|
|
877
|
-
return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
|
|
878
|
-
// Strict read under the lock: an unreadable/corrupt tracker fails closed rather than
|
|
879
|
-
// being treated as empty state.
|
|
880
|
-
const read = readSubagentTrackingStateSyncStrict(cwd);
|
|
881
|
-
if (!read.ok)
|
|
882
|
-
return { ok: false, reason: 'native_anchor_unavailable' };
|
|
883
|
-
const state = read.state;
|
|
884
|
-
const session = state.sessions[sessionId];
|
|
885
|
-
const attestedLeader = session?.leader_thread_id?.trim();
|
|
886
|
-
// Fail closed unless a durable native attestation established the leader anchor.
|
|
887
|
-
if (!session || !attestedLeader || !session.leader_attested_at) {
|
|
888
|
-
return { ok: false, reason: 'native_anchor_unavailable' };
|
|
889
|
-
}
|
|
890
|
-
if (parentThreadId !== attestedLeader) {
|
|
891
|
-
return { ok: false, reason: 'native_anchor_mismatch' };
|
|
892
|
-
}
|
|
893
|
-
// Symmetric subagent exclusion: independent of which writer won the tracker lock, a
|
|
894
|
-
// leader thread that is ALSO recorded as a subagent in ANY session (even a different
|
|
895
|
-
// one) must never receive an adapted role intent. Re-scanned here under the intent-write
|
|
896
|
-
// lock so a cross-session child record committed after attestation cannot slip through.
|
|
897
|
-
if (threadIsTrackedAsSubagent(state, attestedLeader)) {
|
|
898
|
-
return { ok: false, reason: 'native_anchor_mismatch' };
|
|
899
|
-
}
|
|
900
|
-
// Single-flight, role-agnostic (preserves recordPendingRoleIntent's invariant): a
|
|
901
|
-
// live/bound own intent for the same (session, parent) blocks a second live intent.
|
|
902
|
-
// Same-identity (same role) retries reuse the original receipt (idempotent); a
|
|
903
|
-
// different-role request for the same live parent returns single_flight_conflict
|
|
904
|
-
// rather than creating a second ambiguous intent.
|
|
905
|
-
const all = state.pending_role_intents;
|
|
906
|
-
const liveOwnIntent = all.find((intent) => (isOwn(intent)
|
|
907
|
-
&& intent.session_id === sessionId
|
|
908
|
-
&& intent.parent_thread_id === parentThreadId
|
|
909
|
-
&& (intent.binding_state === 'bound' || !isExpiredPendingRoleIntent(intent, nowMs))));
|
|
910
|
-
if (liveOwnIntent) {
|
|
911
|
-
if (liveOwnIntent.role === role) {
|
|
912
|
-
return { ok: true, intent: liveOwnIntent, reused: true };
|
|
913
|
-
}
|
|
914
|
-
return { ok: false, reason: 'single_flight_conflict' };
|
|
915
|
-
}
|
|
916
|
-
// Self-heal the leader thread record so leader-anchor consumers see kind:"leader".
|
|
917
|
-
const nowIso = new Date(nowMs).toISOString();
|
|
918
|
-
const nextSession = { ...session, threads: { ...session.threads } };
|
|
919
|
-
const existingLeaderThread = nextSession.threads[attestedLeader];
|
|
920
|
-
if (!existingLeaderThread || existingLeaderThread.kind !== 'leader') {
|
|
921
|
-
nextSession.threads[attestedLeader] = {
|
|
922
|
-
...(existingLeaderThread ?? {}),
|
|
923
|
-
thread_id: attestedLeader,
|
|
924
|
-
kind: 'leader',
|
|
925
|
-
first_seen_at: existingLeaderThread?.first_seen_at ?? nowIso,
|
|
926
|
-
last_seen_at: nowIso,
|
|
927
|
-
turn_count: existingLeaderThread?.turn_count ?? 0,
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
nextSession.updated_at = nowIso;
|
|
931
|
-
const ttlMs = typeof input.ttlMs === 'number' && Number.isFinite(input.ttlMs) ? input.ttlMs : 10 * 60_000;
|
|
932
|
-
const intent = {
|
|
933
|
-
role,
|
|
934
|
-
session_id: sessionId,
|
|
935
|
-
parent_thread_id: parentThreadId,
|
|
936
|
-
correlation_token: correlationToken,
|
|
937
|
-
created_at: nowIso,
|
|
938
|
-
expires_at: new Date(nowMs + ttlMs).toISOString(),
|
|
939
|
-
...(canonicalOrigin ? { origin_cwd: canonicalOrigin } : {}),
|
|
940
|
-
};
|
|
941
|
-
state.sessions = { ...state.sessions, [sessionId]: nextSession };
|
|
942
|
-
state.pending_role_intents = [...all.filter((candidate) => !shouldPruneExpired(candidate)), intent];
|
|
943
|
-
context.assertOwnership();
|
|
944
|
-
writeSubagentTrackingStateSync(cwd, state, context.publish);
|
|
945
|
-
return { ok: true, intent, reused: false };
|
|
946
|
-
});
|
|
947
|
-
}
|
|
948
|
-
/**
|
|
949
|
-
* #3181 legacy/native fallback, made atomic and positive-provenance-only. When no durable
|
|
950
|
-
* attestation exists, an in-turn role intent may still be authorized ONLY against the
|
|
951
|
-
* tracker leader_thread_id — the positively-provenanced leader thread set by a real
|
|
952
|
-
* recorded leader turn (notify) or by PreToolUse attestation (gated when the caller has a
|
|
953
|
-
* usable current pointer). It deliberately does NOT trust the reconciled session.json
|
|
954
|
-
* native_session_id, which an ambiguous/malformed-child SessionStart can set. This is the
|
|
955
|
-
* strict tracker-locked replacement for the former CLI "activeLeaderThreadIds +
|
|
956
|
-
* recordPendingRoleIntent" fallback: it validates the leader anchor AND rejects any
|
|
957
|
-
* all-session subagent counter-evidence under the same lock as the intent write, so a
|
|
958
|
-
* subagent record landing after a pre-check (or a PreToolUse attestation that lost its
|
|
959
|
-
* race) can never be downgraded into an unvalidated legacy authorization. Fail-closed:
|
|
960
|
-
* unreadable/corrupt tracker → native_anchor_unavailable; parent not the tracker leader →
|
|
961
|
-
* parent_not_active_leader; parent tracked as a subagent anywhere → native_anchor_mismatch.
|
|
962
|
-
*/
|
|
963
|
-
export function recordNativeLeaderIntent(cwd, input) {
|
|
964
|
-
const role = resolveInstalledRoleName(input.role);
|
|
965
|
-
if (!role)
|
|
966
|
-
return { ok: false, reason: 'unknown_role' };
|
|
967
|
-
const correlationToken = input.correlationToken;
|
|
968
|
-
if (!isCanonicalCorrelationToken(correlationToken)) {
|
|
969
|
-
return { ok: false, reason: 'invalid_correlation_token' };
|
|
970
|
-
}
|
|
971
|
-
const nowMs = normalizeNowMs(input.nowMs);
|
|
972
|
-
const sessionId = input.sessionId.trim();
|
|
973
|
-
const parentThreadId = input.parentThreadId.trim();
|
|
974
|
-
const canonicalOrigin = canonicalizeOriginCwd(cwd);
|
|
975
|
-
if (canonicalOrigin === null)
|
|
976
|
-
return { ok: false, reason: 'invalid_origin' };
|
|
977
|
-
const { isOwn, shouldPruneExpired } = pendingRoleIntentPredicates(cwd, canonicalOrigin, nowMs);
|
|
978
|
-
return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
|
|
979
|
-
const read = readSubagentTrackingStateSyncStrict(cwd);
|
|
980
|
-
if (!read.ok)
|
|
981
|
-
return { ok: false, reason: 'native_anchor_unavailable' };
|
|
982
|
-
const state = read.state;
|
|
983
|
-
// Positive-provenance leader anchor ONLY: the tracker leader_thread_id is set by a real
|
|
984
|
-
// recorded leader turn (notify) or by PreToolUse attestation. It is intentionally NOT
|
|
985
|
-
// the reconciled session.json native_session_id, which an ambiguous/malformed-child
|
|
986
|
-
// SessionStart can set without positively classifying a root — trusting that pointer
|
|
987
|
-
// would re-open false-leader adoption. A malformed child never obtains a tracker leader
|
|
988
|
-
// thread, so it fails closed here.
|
|
989
|
-
const trackerLeader = input.allowTrackerLeader ? state.sessions[sessionId]?.leader_thread_id?.trim() : undefined;
|
|
990
|
-
if (!parentThreadId || !trackerLeader || parentThreadId !== trackerLeader) {
|
|
991
|
-
return { ok: false, reason: 'parent_not_active_leader' };
|
|
992
|
-
}
|
|
993
|
-
// Atomic all-session subagent exclusion: a parent thread recorded as a subagent in ANY
|
|
994
|
-
// session is never a valid leader, even if it matches the tracker leader anchor.
|
|
995
|
-
if (threadIsTrackedAsSubagent(state, parentThreadId)) {
|
|
996
|
-
return { ok: false, reason: 'native_anchor_mismatch' };
|
|
997
|
-
}
|
|
998
|
-
// Role-agnostic single-flight (same contract as recordPendingRoleIntent).
|
|
999
|
-
const all = state.pending_role_intents;
|
|
1000
|
-
const liveOwnIntent = all.find((intent) => (isOwn(intent)
|
|
1001
|
-
&& intent.session_id === sessionId
|
|
1002
|
-
&& intent.parent_thread_id === parentThreadId
|
|
1003
|
-
&& (intent.binding_state === 'bound' || !isExpiredPendingRoleIntent(intent, nowMs))));
|
|
1004
|
-
if (liveOwnIntent) {
|
|
1005
|
-
if (liveOwnIntent.role === role) {
|
|
1006
|
-
return { ok: true, intent: liveOwnIntent, reused: true };
|
|
1007
|
-
}
|
|
1008
|
-
return { ok: false, reason: 'single_flight_conflict' };
|
|
1009
|
-
}
|
|
1010
|
-
const nowIso = new Date(nowMs).toISOString();
|
|
1011
|
-
const ttlMs = typeof input.ttlMs === 'number' && Number.isFinite(input.ttlMs) ? input.ttlMs : 10 * 60_000;
|
|
1012
|
-
const intent = {
|
|
1013
|
-
role,
|
|
1014
|
-
session_id: sessionId,
|
|
1015
|
-
parent_thread_id: parentThreadId,
|
|
1016
|
-
correlation_token: correlationToken,
|
|
1017
|
-
created_at: nowIso,
|
|
1018
|
-
expires_at: new Date(nowMs + ttlMs).toISOString(),
|
|
1019
|
-
...(canonicalOrigin ? { origin_cwd: canonicalOrigin } : {}),
|
|
1020
|
-
};
|
|
1021
|
-
state.pending_role_intents = [...all.filter((candidate) => !shouldPruneExpired(candidate)), intent];
|
|
1022
|
-
context.assertOwnership();
|
|
1023
|
-
writeSubagentTrackingStateSync(cwd, state, context.publish);
|
|
1024
|
-
return { ok: true, intent, reused: false };
|
|
1025
|
-
});
|
|
1026
|
-
}
|
|
1027
|
-
export function bindPendingRoleIntentUnderLock(cwd, input, bind) {
|
|
1028
|
-
const nowMs = normalizeNowMs(input.nowMs);
|
|
1029
|
-
const sessionId = input.sessionId.trim();
|
|
1030
|
-
const parentThreadId = input.parentThreadId.trim();
|
|
1031
|
-
const correlationToken = input.correlationToken;
|
|
1032
|
-
if (!isCanonicalCorrelationToken(correlationToken))
|
|
1033
|
-
return null;
|
|
1034
|
-
// Fail-closed origin authentication: establish the caller's canonical origin workspace up
|
|
1035
|
-
// front. A malformed/unavailable origin can never disclose role/claimant, run the bind
|
|
1036
|
-
// callback, mutate pending->bound, or acquire the lock.
|
|
1037
|
-
const canonicalOrigin = canonicalizeOriginCwd(cwd);
|
|
1038
|
-
if (canonicalOrigin === null)
|
|
1039
|
-
return null;
|
|
1040
|
-
return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
|
|
1041
|
-
const state = readSubagentTrackingStateSync(cwd);
|
|
1042
|
-
const all = state.pending_role_intents;
|
|
1043
|
-
const { isOwn, shouldPruneExpired } = pendingRoleIntentPredicates(cwd, canonicalOrigin, nowMs);
|
|
1044
|
-
const matchedIntent = selectDominantRoleIntent(all.filter((intent) => (isOwn(intent)
|
|
1045
|
-
&& correlationToken !== undefined
|
|
1046
|
-
&& sameLogicalRoleIntent(intent, sessionId, parentThreadId, correlationToken)
|
|
1047
|
-
&& (intent.binding_state === 'bound' || !isExpiredPendingRoleIntent(intent, nowMs)))), canonicalOrigin);
|
|
1048
|
-
if (!matchedIntent) {
|
|
1049
|
-
const retained = all.filter((intent) => !shouldPruneExpired(intent));
|
|
1050
|
-
if (retained.length !== all.length) {
|
|
1051
|
-
state.pending_role_intents = retained;
|
|
1052
|
-
context.assertOwnership();
|
|
1053
|
-
writeSubagentTrackingStateSync(cwd, state, context.publish);
|
|
720
|
+
// Fail closed on every side of the write. The proposed state must survive
|
|
721
|
+
// strict authority validation (tolerant inputs must not launder malformed
|
|
722
|
+
// authority into a valid-looking attestation); existing on-disk bytes must
|
|
723
|
+
// not be overwritten when they fail strict parsing; and the publication must
|
|
724
|
+
// not roll back an authority decision committed after the caller read its
|
|
725
|
+
// snapshot. The whole read/validate/compare/publish transaction is
|
|
726
|
+
// serialized under the tracker lock.
|
|
727
|
+
const proposedRaw = JSON.stringify(state);
|
|
728
|
+
if (!parseStrictSubagentTrackingState(proposedRaw))
|
|
729
|
+
throw new Error('Malformed subagent tracker authority state');
|
|
730
|
+
const normalized = normalizeSubagentTrackingState(state);
|
|
731
|
+
const contents = `${JSON.stringify(normalized, null, 2)}\n`;
|
|
732
|
+
const proposedAuthorityRevision = authorityRevisionOf(normalized);
|
|
733
|
+
return withCrossProcessFileLockSync(path, (lock) => {
|
|
734
|
+
if (existsSync(path)) {
|
|
735
|
+
let raw;
|
|
736
|
+
try {
|
|
737
|
+
raw = readFileSync(path, 'utf-8');
|
|
1054
738
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
const adaptedIntent = {
|
|
1058
|
-
role: matchedIntent.role,
|
|
1059
|
-
provenanceKind: OMX_ADAPTED_PROVENANCE,
|
|
1060
|
-
};
|
|
1061
|
-
if (matchedIntent.binding_state === 'bound') {
|
|
1062
|
-
const next = all
|
|
1063
|
-
.filter((intent) => (intent.binding_state === 'bound'
|
|
1064
|
-
|| !(isOwn(intent) && sameLogicalRoleIntent(intent, sessionId, parentThreadId, correlationToken))))
|
|
1065
|
-
.map((intent) => (intent === matchedIntent && !intent.origin_cwd
|
|
1066
|
-
? { ...intent, origin_cwd: canonicalOrigin }
|
|
1067
|
-
: intent));
|
|
1068
|
-
if (next.length !== all.length || !matchedIntent.origin_cwd) {
|
|
1069
|
-
state.pending_role_intents = next;
|
|
1070
|
-
context.assertOwnership();
|
|
1071
|
-
writeSubagentTrackingStateSync(cwd, state, context.publish);
|
|
739
|
+
catch {
|
|
740
|
+
throw new Error('Malformed subagent tracker authority state');
|
|
1072
741
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
.map((intent) => (intent === matchedIntent
|
|
1086
|
-
? {
|
|
1087
|
-
...matchedIntent,
|
|
1088
|
-
binding_state: 'bound',
|
|
1089
|
-
binding_claimant_token: claimantToken,
|
|
1090
|
-
bound_at: new Date(nowMs).toISOString(),
|
|
1091
|
-
origin_cwd: matchedIntent.origin_cwd ?? canonicalOrigin,
|
|
742
|
+
const current = parseStrictSubagentTrackingState(raw);
|
|
743
|
+
if (!current)
|
|
744
|
+
throw new Error('Malformed subagent tracker authority state');
|
|
745
|
+
// The generic writer is descriptive-only: it may publish descriptive
|
|
746
|
+
// churn, but it must never CHANGE the authority surface in any
|
|
747
|
+
// direction. Granting, revoking, or rolling back authority is reserved
|
|
748
|
+
// for the lock-owning authority transactions. Requiring the proposed
|
|
749
|
+
// authority projection to equal the current on-disk projection makes a
|
|
750
|
+
// stale snapshot, a fresh-read escalation, and a concurrent interleaving
|
|
751
|
+
// all fail closed with one rule.
|
|
752
|
+
if (authorityRevisionOf(current) !== proposedAuthorityRevision) {
|
|
753
|
+
throw new Error('Stale subagent tracker authority state');
|
|
1092
754
|
}
|
|
1093
|
-
: intent));
|
|
1094
|
-
context.assertOwnership();
|
|
1095
|
-
writeSubagentTrackingStateSync(cwd, boundState, context.publish);
|
|
1096
|
-
return { ...adaptedIntent, claimantToken, alreadyBound: false };
|
|
1097
|
-
});
|
|
1098
|
-
}
|
|
1099
|
-
export function consumePendingRoleIntent(cwd, input) {
|
|
1100
|
-
const nowMs = normalizeNowMs(input.nowMs);
|
|
1101
|
-
const sessionId = input.sessionId.trim();
|
|
1102
|
-
const parentThreadId = input.parentThreadId.trim();
|
|
1103
|
-
const correlationToken = input.correlationToken;
|
|
1104
|
-
if (!isCanonicalCorrelationToken(correlationToken))
|
|
1105
|
-
return null;
|
|
1106
|
-
const canonicalOrigin = canonicalizeOriginCwd(cwd);
|
|
1107
|
-
if (canonicalOrigin === null)
|
|
1108
|
-
return null;
|
|
1109
|
-
return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
|
|
1110
|
-
const state = readSubagentTrackingStateSync(cwd);
|
|
1111
|
-
const all = state.pending_role_intents;
|
|
1112
|
-
const { isOwn, shouldPruneExpired } = pendingRoleIntentPredicates(cwd, canonicalOrigin, nowMs);
|
|
1113
|
-
if (hasOwnBoundLogicalIntent(all, isOwn, sessionId, parentThreadId, correlationToken))
|
|
1114
|
-
return null;
|
|
1115
|
-
const consumed = selectDominantRoleIntent(all.filter((intent) => (isOwn(intent)
|
|
1116
|
-
&& intent.binding_state !== 'bound'
|
|
1117
|
-
&& !isExpiredPendingRoleIntent(intent, nowMs)
|
|
1118
|
-
&& correlationToken !== undefined
|
|
1119
|
-
&& sameLogicalRoleIntent(intent, sessionId, parentThreadId, correlationToken))), canonicalOrigin);
|
|
1120
|
-
if (!consumed) {
|
|
1121
|
-
const retained = all.filter((intent) => !shouldPruneExpired(intent));
|
|
1122
|
-
if (retained.length !== all.length) {
|
|
1123
|
-
state.pending_role_intents = retained;
|
|
1124
|
-
context.assertOwnership();
|
|
1125
|
-
writeSubagentTrackingStateSync(cwd, state, context.publish);
|
|
1126
|
-
}
|
|
1127
|
-
return null;
|
|
1128
755
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
const correlationToken = input.correlationToken;
|
|
1143
|
-
const claimantToken = input.claimantToken;
|
|
1144
|
-
const canonicalOrigin = canonicalizeOriginCwd(cwd);
|
|
1145
|
-
if (canonicalOrigin === null)
|
|
1146
|
-
return 'not_found';
|
|
1147
|
-
return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
|
|
1148
|
-
const state = readSubagentTrackingStateSync(cwd);
|
|
1149
|
-
const all = state.pending_role_intents;
|
|
1150
|
-
const { isOwn, shouldPruneExpired } = pendingRoleIntentPredicates(cwd, canonicalOrigin, nowMs);
|
|
1151
|
-
// Select the owned bound scope before authenticating it. Credentials are not a lookup key:
|
|
1152
|
-
// otherwise an invalid dominant duplicate could be bypassed by a lower valid duplicate.
|
|
1153
|
-
const boundIntent = selectDominantRoleIntent(all.filter((intent) => (isOwn(intent)
|
|
1154
|
-
&& intent.binding_state === 'bound'
|
|
1155
|
-
&& sameLogicalRoleIntent(intent, sessionId, parentThreadId))), canonicalOrigin);
|
|
1156
|
-
if (!boundIntent) {
|
|
1157
|
-
const retained = all.filter((intent) => !shouldPruneExpired(intent));
|
|
1158
|
-
if (retained.length !== all.length) {
|
|
1159
|
-
state.pending_role_intents = retained;
|
|
1160
|
-
context.assertOwnership();
|
|
1161
|
-
writeSubagentTrackingStateSync(cwd, state, context.publish);
|
|
1162
|
-
}
|
|
1163
|
-
return 'not_found';
|
|
756
|
+
else if (proposedAuthorityRevision !== authorityRevisionOf(createSubagentTrackingState())) {
|
|
757
|
+
// Creating the tracker from nothing must not mint reopen authority.
|
|
758
|
+
// `leader_thread_id` is deliberately NOT included: it is ordinary
|
|
759
|
+
// descriptive lifecycle data that legitimate first writers set, and it
|
|
760
|
+
// never by itself makes a thread an eligible reopen target.
|
|
761
|
+
const mintsAuthority = Object.values(normalized.sessions).some((session) => Object.values(session.threads).some((thread) => thread.provenance_kind === NATIVE_SUBAGENT_PROVENANCE
|
|
762
|
+
|| thread.direct_child_root_id !== undefined
|
|
763
|
+
|| thread.direct_child_parent_id !== undefined
|
|
764
|
+
|| thread.reopen_authority_revoked !== undefined
|
|
765
|
+
|| thread.reopen_authority_conflict_reason !== undefined
|
|
766
|
+
|| thread.reopen_authority_conflict_at !== undefined));
|
|
767
|
+
if (mintsAuthority)
|
|
768
|
+
throw new Error('Stale subagent tracker authority state');
|
|
1164
769
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|| (hasClaimant && (!isCanonicalClaimantToken(boundIntent.binding_claimant_token)
|
|
1170
|
-
|| !isCanonicalClaimantToken(claimantToken)
|
|
1171
|
-
|| boundIntent.binding_claimant_token !== claimantToken)))
|
|
1172
|
-
return 'claimant_mismatch';
|
|
1173
|
-
state.pending_role_intents = all
|
|
1174
|
-
.filter((intent) => !shouldPruneExpired(intent))
|
|
1175
|
-
.filter((intent) => !(isOwn(intent) && sameLogicalRoleIntent(intent, sessionId, parentThreadId, boundIntent.correlation_token)));
|
|
1176
|
-
context.assertOwnership();
|
|
1177
|
-
writeSubagentTrackingStateSync(cwd, state, context.publish);
|
|
1178
|
-
return 'completed';
|
|
1179
|
-
});
|
|
1180
|
-
}
|
|
1181
|
-
export function listBoundAdaptedRoleIntents(cwd, _nowMs, ownedDominant = false) {
|
|
1182
|
-
const allBound = readSubagentTrackingStateSync(cwd).pending_role_intents.filter((intent) => intent.binding_state === 'bound');
|
|
1183
|
-
if (!ownedDominant)
|
|
1184
|
-
return allBound;
|
|
1185
|
-
const canonicalOrigin = canonicalizeOriginCwd(cwd);
|
|
1186
|
-
if (canonicalOrigin === null)
|
|
1187
|
-
return [];
|
|
1188
|
-
const { isOwn } = pendingRoleIntentPredicates(cwd, canonicalOrigin, Date.now());
|
|
1189
|
-
const scopes = new Map();
|
|
1190
|
-
for (const intent of allBound) {
|
|
1191
|
-
if (!isOwn(intent))
|
|
1192
|
-
continue;
|
|
1193
|
-
const key = `${intent.session_id}\u0000${intent.parent_thread_id}`;
|
|
1194
|
-
scopes.set(key, [...(scopes.get(key) ?? []), intent]);
|
|
1195
|
-
}
|
|
1196
|
-
return [...scopes.values()].flatMap((candidates) => {
|
|
1197
|
-
const dominant = selectDominantRoleIntent(candidates, canonicalOrigin);
|
|
1198
|
-
return dominant ? [dominant] : [];
|
|
770
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
771
|
+
lock.assertOwnership();
|
|
772
|
+
lock.publish(contents);
|
|
773
|
+
return path;
|
|
1199
774
|
});
|
|
1200
775
|
}
|
|
1201
776
|
export function recordSubagentTurn(state, input) {
|
|
@@ -1263,11 +838,13 @@ export function recordSubagentTurn(state, input) {
|
|
|
1263
838
|
: preservedCompletion),
|
|
1264
839
|
...(input.mode?.trim() ? { mode: input.mode.trim() } : existingThread?.mode ? { mode: existingThread.mode } : {}),
|
|
1265
840
|
...(input.role?.trim() ? { role: input.role.trim() } : existingThread?.role ? { role: existingThread.role } : {}),
|
|
1266
|
-
...(input.provenanceKind
|
|
1267
|
-
? { provenance_kind:
|
|
1268
|
-
: existingThread?.provenance_kind
|
|
1269
|
-
? { provenance_kind:
|
|
1270
|
-
:
|
|
841
|
+
...(input.provenanceKind === NATIVE_SUBAGENT_PROVENANCE
|
|
842
|
+
? { provenance_kind: NATIVE_SUBAGENT_PROVENANCE }
|
|
843
|
+
: existingThread?.provenance_kind === NATIVE_SUBAGENT_PROVENANCE
|
|
844
|
+
? { provenance_kind: NATIVE_SUBAGENT_PROVENANCE }
|
|
845
|
+
: existingThread?.provenance_kind === DESCRIPTIVE_ADAPTED_PROVENANCE
|
|
846
|
+
? { provenance_kind: DESCRIPTIVE_ADAPTED_PROVENANCE }
|
|
847
|
+
: {}),
|
|
1271
848
|
...(input.laneId?.trim() ? { lane_id: input.laneId.trim() } : existingThread?.lane_id ? { lane_id: existingThread.lane_id } : {}),
|
|
1272
849
|
...(input.scope?.trim() ? { scope: input.scope.trim() } : existingThread?.scope ? { scope: existingThread.scope } : {}),
|
|
1273
850
|
...(input.agentNickname?.trim()
|
|
@@ -1301,6 +878,11 @@ export function recordSubagentTurn(state, input) {
|
|
|
1301
878
|
: existingThread?.resume_failure_reason
|
|
1302
879
|
? { resume_failure_reason: existingThread.resume_failure_reason }
|
|
1303
880
|
: {}),
|
|
881
|
+
...(existingThread?.direct_child_root_id ? { direct_child_root_id: existingThread.direct_child_root_id } : {}),
|
|
882
|
+
...(existingThread?.direct_child_parent_id ? { direct_child_parent_id: existingThread.direct_child_parent_id } : {}),
|
|
883
|
+
...(existingThread?.reopen_authority_revoked ? { reopen_authority_revoked: true } : {}),
|
|
884
|
+
...(existingThread?.reopen_authority_conflict_reason ? { reopen_authority_conflict_reason: existingThread.reopen_authority_conflict_reason } : {}),
|
|
885
|
+
...(existingThread?.reopen_authority_conflict_at ? { reopen_authority_conflict_at: existingThread.reopen_authority_conflict_at } : {}),
|
|
1304
886
|
};
|
|
1305
887
|
const threads = {
|
|
1306
888
|
...existingSession.threads,
|
|
@@ -1315,11 +897,6 @@ export function recordSubagentTurn(state, input) {
|
|
|
1315
897
|
normalized.sessions[sessionId] = {
|
|
1316
898
|
session_id: sessionId,
|
|
1317
899
|
...(leaderThreadId ? { leader_thread_id: leaderThreadId } : {}),
|
|
1318
|
-
// #3181: preserve the durable native leader attestation across ordinary turn writes.
|
|
1319
|
-
// Dropping these would let a normal child SessionStart/bind erase authentication and
|
|
1320
|
-
// silently downgrade later role-intent writes to the legacy (non-atomic) path.
|
|
1321
|
-
...(existingSession.leader_attested_at ? { leader_attested_at: existingSession.leader_attested_at } : {}),
|
|
1322
|
-
...(existingSession.leader_attest_source ? { leader_attest_source: existingSession.leader_attest_source } : {}),
|
|
1323
900
|
updated_at: timestamp,
|
|
1324
901
|
threads,
|
|
1325
902
|
};
|
|
@@ -1327,13 +904,615 @@ export function recordSubagentTurn(state, input) {
|
|
|
1327
904
|
}
|
|
1328
905
|
export async function recordSubagentTurnForSession(cwd, input) {
|
|
1329
906
|
return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
|
|
1330
|
-
const
|
|
1331
|
-
|
|
907
|
+
const strict = readSubagentTrackingStateStrictSync(cwd);
|
|
908
|
+
if (!strict.ok)
|
|
909
|
+
throw new Error('Malformed subagent tracker authority state');
|
|
910
|
+
const next = recordSubagentTurn(strict.state, input);
|
|
911
|
+
context.assertOwnership();
|
|
912
|
+
writeSubagentTrackingStateSync(cwd, next, context.publish);
|
|
913
|
+
return next;
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
export function recordNativeSubagentAuthorityObservation(cwd, observation) {
|
|
917
|
+
const requestedSessionIds = [...new Set(observation.sessionIds.map((value) => value.trim()).filter(Boolean))];
|
|
918
|
+
const childThreadId = observation.childThreadId.trim();
|
|
919
|
+
const parentThreadId = observation.parentThreadId.trim();
|
|
920
|
+
const rootNativeSessionId = observation.rootNativeSessionId?.trim() ?? '';
|
|
921
|
+
const implicatedChildThreadIds = [...new Set([
|
|
922
|
+
childThreadId,
|
|
923
|
+
...(observation.implicatedChildThreadIds ?? []).map((value) => value.trim()),
|
|
924
|
+
].filter(Boolean))];
|
|
925
|
+
return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
|
|
926
|
+
const strict = readSubagentTrackingStateStrictSync(cwd);
|
|
927
|
+
if (!strict.ok)
|
|
928
|
+
throw new Error('Malformed subagent tracker authority state');
|
|
929
|
+
let next = strict.state;
|
|
930
|
+
const existingSessionIds = Object.entries(next.sessions)
|
|
931
|
+
.filter(([, session]) => implicatedChildThreadIds.some((id) => Boolean(session.threads[id]?.direct_child_root_id || session.threads[id]?.reopen_authority_revoked)))
|
|
932
|
+
.map(([sessionId]) => sessionId);
|
|
933
|
+
const sessionIds = [...new Set([...requestedSessionIds, ...existingSessionIds])];
|
|
934
|
+
const existingBindings = Object.values(next.sessions)
|
|
935
|
+
.map((session) => session.threads[childThreadId])
|
|
936
|
+
.filter((thread) => Boolean(thread?.direct_child_root_id));
|
|
937
|
+
const authorityEvidence = observation.authorityEvidence ?? (rootNativeSessionId ? 'valid' : 'absent');
|
|
938
|
+
const globalConflict = authorityEvidence === 'untrusted' && existingBindings.length > 0
|
|
939
|
+
|| authorityEvidence === 'valid' && existingBindings.some((thread) => thread.direct_child_root_id !== rootNativeSessionId || thread.direct_child_parent_id !== parentThreadId);
|
|
940
|
+
const timestamp = observation.timestamp ?? new Date().toISOString();
|
|
941
|
+
if (authorityEvidence === 'untrusted') {
|
|
942
|
+
for (const session of Object.values(next.sessions)) {
|
|
943
|
+
for (const implicatedId of implicatedChildThreadIds) {
|
|
944
|
+
const implicatedThread = session.threads[implicatedId];
|
|
945
|
+
if (!implicatedThread?.direct_child_root_id && !implicatedThread?.reopen_authority_revoked)
|
|
946
|
+
continue;
|
|
947
|
+
implicatedThread.reopen_authority_revoked = true;
|
|
948
|
+
implicatedThread.reopen_authority_conflict_reason = 'identity_untrusted';
|
|
949
|
+
implicatedThread.reopen_authority_conflict_at = timestamp;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
for (const sessionId of sessionIds) {
|
|
954
|
+
if (parentThreadId && parentThreadId !== childThreadId) {
|
|
955
|
+
next = recordSubagentTurn(next, { sessionId, threadId: parentThreadId, kind: 'leader', timestamp });
|
|
956
|
+
}
|
|
957
|
+
next = recordSubagentTurn(next, {
|
|
958
|
+
sessionId,
|
|
959
|
+
threadId: childThreadId,
|
|
960
|
+
kind: 'subagent',
|
|
961
|
+
...(parentThreadId && parentThreadId !== childThreadId ? { leaderThreadId: parentThreadId } : {}),
|
|
962
|
+
mode: observation.mode,
|
|
963
|
+
timestamp,
|
|
964
|
+
});
|
|
965
|
+
const thread = next.sessions[sessionId]?.threads[childThreadId];
|
|
966
|
+
if (!thread)
|
|
967
|
+
continue;
|
|
968
|
+
const mayAttest = authorityEvidence === 'valid'
|
|
969
|
+
&& Boolean(rootNativeSessionId)
|
|
970
|
+
&& childThreadId !== rootNativeSessionId
|
|
971
|
+
&& parentThreadId === rootNativeSessionId;
|
|
972
|
+
if (globalConflict) {
|
|
973
|
+
thread.reopen_authority_revoked = true;
|
|
974
|
+
thread.reopen_authority_conflict_reason = 'root_or_parent_mismatch';
|
|
975
|
+
thread.reopen_authority_conflict_at = timestamp;
|
|
976
|
+
}
|
|
977
|
+
else if (mayAttest) {
|
|
978
|
+
thread.direct_child_root_id = rootNativeSessionId;
|
|
979
|
+
thread.direct_child_parent_id = parentThreadId;
|
|
980
|
+
thread.provenance_kind = NATIVE_SUBAGENT_PROVENANCE;
|
|
981
|
+
delete thread.reopen_authority_revoked;
|
|
982
|
+
delete thread.reopen_authority_conflict_reason;
|
|
983
|
+
delete thread.reopen_authority_conflict_at;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
1332
986
|
context.assertOwnership();
|
|
1333
987
|
writeSubagentTrackingStateSync(cwd, next, context.publish);
|
|
1334
988
|
return next;
|
|
1335
989
|
});
|
|
1336
990
|
}
|
|
991
|
+
/**
|
|
992
|
+
* Durable fence for authority revocations that could not be published.
|
|
993
|
+
*
|
|
994
|
+
* A negative identity observation is only meaningful if reopen consumption can
|
|
995
|
+
* see it. When the revocation transaction fails (lock exhaustion, malformed
|
|
996
|
+
* bytes, I/O error) the affected ids are recorded here instead of being
|
|
997
|
+
* silently dropped, and `consumeDirectChildReopenContext` treats any fenced id
|
|
998
|
+
* as denied until the revocation is durably applied.
|
|
999
|
+
*/
|
|
1000
|
+
function authorityFencePath(cwd) {
|
|
1001
|
+
return `${subagentTrackingPath(cwd)}.authority-fence.json`;
|
|
1002
|
+
}
|
|
1003
|
+
/** Read the fenced ids. An unreadable/malformed fence fails closed for all ids. */
|
|
1004
|
+
function readAuthorityFence(cwd) {
|
|
1005
|
+
const path = authorityFencePath(cwd);
|
|
1006
|
+
if (!existsSync(path))
|
|
1007
|
+
return { ok: true, ids: new Set() };
|
|
1008
|
+
try {
|
|
1009
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
1010
|
+
if (!Array.isArray(parsed.ids))
|
|
1011
|
+
return { ok: false, ids: new Set() };
|
|
1012
|
+
const ids = parsed.ids.filter((id) => typeof id === 'string' && Boolean(id.trim()));
|
|
1013
|
+
if (ids.length !== parsed.ids.length)
|
|
1014
|
+
return { ok: false, ids: new Set() };
|
|
1015
|
+
return { ok: true, ids: new Set(ids.map((id) => id.trim())) };
|
|
1016
|
+
}
|
|
1017
|
+
catch {
|
|
1018
|
+
return { ok: false, ids: new Set() };
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/** Upper bound on fenced ids, so repeated failures cannot grow the fence without limit. */
|
|
1022
|
+
export const AUTHORITY_FENCE_MAX_IDS = 512;
|
|
1023
|
+
/**
|
|
1024
|
+
* Record ids whose revocation could not be persisted, so reopen stays denied.
|
|
1025
|
+
*
|
|
1026
|
+
* Every fence mutation runs under a dedicated cross-process lock on the fence
|
|
1027
|
+
* path so read/merge/publish is atomic with respect to concurrent fencing and
|
|
1028
|
+
* clearing. Returns true when the denial is durable; the caller must treat
|
|
1029
|
+
* false as an unresolved fail-closed condition.
|
|
1030
|
+
*/
|
|
1031
|
+
export function fenceNativeSubagentAuthorities(cwd, childThreadIds, reason) {
|
|
1032
|
+
const ids = childThreadIds.map((value) => value.trim()).filter(Boolean);
|
|
1033
|
+
if (!ids.length)
|
|
1034
|
+
return true;
|
|
1035
|
+
const path = authorityFencePath(cwd);
|
|
1036
|
+
try {
|
|
1037
|
+
return withCrossProcessFileLockSync(path, (lock) => {
|
|
1038
|
+
const existing = readAuthorityFence(cwd);
|
|
1039
|
+
// An unreadable fence already denies every id; do not weaken it by
|
|
1040
|
+
// rewriting it into a narrower explicit id list.
|
|
1041
|
+
if (!existing.ok)
|
|
1042
|
+
return true;
|
|
1043
|
+
const merged = [...new Set([...existing.ids, ...ids])].sort();
|
|
1044
|
+
if (merged.length > AUTHORITY_FENCE_MAX_IDS) {
|
|
1045
|
+
// Refuse to grow past the cap by writing a deliberately unreadable
|
|
1046
|
+
// sentinel: a global denial is strictly stronger than a partial list.
|
|
1047
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1048
|
+
lock.assertOwnership();
|
|
1049
|
+
lock.publish(`${JSON.stringify({ ids: 'all', reason: 'fence_capacity_exceeded', fenced_at: new Date().toISOString() }, null, 2)}\n`);
|
|
1050
|
+
return true;
|
|
1051
|
+
}
|
|
1052
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1053
|
+
lock.assertOwnership();
|
|
1054
|
+
lock.publish(`${JSON.stringify({ ids: merged, reason, fenced_at: new Date().toISOString() }, null, 2)}\n`);
|
|
1055
|
+
return true;
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
catch {
|
|
1059
|
+
// The denial could not be made durable. The caller must surface this
|
|
1060
|
+
// rather than continue as if the revocation had been applied.
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Last-resort global denial used when BOTH the tracker revocation transaction
|
|
1066
|
+
* and the locked fence publication fail. It deliberately avoids the fence lock
|
|
1067
|
+
* (which may be exactly what is unavailable) and writes an intentionally
|
|
1068
|
+
* unreadable sentinel directly, because `readAuthorityFence` treats any
|
|
1069
|
+
* unreadable fence as a denial of every id. Returns true when the denial is
|
|
1070
|
+
* durable on disk.
|
|
1071
|
+
*/
|
|
1072
|
+
export function forceGlobalAuthorityDenial(cwd, reason) {
|
|
1073
|
+
const path = authorityFencePath(cwd);
|
|
1074
|
+
const contents = `${JSON.stringify({ ids: 'all', reason, fenced_at: new Date().toISOString() }, null, 2)}\n`;
|
|
1075
|
+
try {
|
|
1076
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1077
|
+
const temporaryPath = atomicTrackingTempPath(path);
|
|
1078
|
+
writeFileSync(temporaryPath, contents);
|
|
1079
|
+
renameSync(temporaryPath, path);
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
catch {
|
|
1083
|
+
try {
|
|
1084
|
+
// Even a partial/truncated write is safe here: unreadable means denied.
|
|
1085
|
+
writeFileSync(path, contents);
|
|
1086
|
+
return true;
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Drop ids from the fence once their revocation is durably published.
|
|
1095
|
+
*
|
|
1096
|
+
* Runs under the fence lock and re-reads inside it, so it can never clear
|
|
1097
|
+
* based on a stale snapshot. An unreadable fence is a global denial and is
|
|
1098
|
+
* NEVER destroyed here: only an explicit, fully reconciled id list is removed.
|
|
1099
|
+
*/
|
|
1100
|
+
function clearAuthorityFence(cwd, childThreadIds) {
|
|
1101
|
+
const path = authorityFencePath(cwd);
|
|
1102
|
+
if (!existsSync(path))
|
|
1103
|
+
return;
|
|
1104
|
+
const cleared = new Set(childThreadIds.map((value) => value.trim()).filter(Boolean));
|
|
1105
|
+
if (!cleared.size)
|
|
1106
|
+
return;
|
|
1107
|
+
try {
|
|
1108
|
+
withCrossProcessFileLockSync(path, (lock) => {
|
|
1109
|
+
const existing = readAuthorityFence(cwd);
|
|
1110
|
+
// Preserve an unreadable/global denial. Reconciling it requires explicit
|
|
1111
|
+
// operator action, not an incidental unrelated revocation.
|
|
1112
|
+
if (!existing.ok)
|
|
1113
|
+
return;
|
|
1114
|
+
const remaining = [...existing.ids].filter((id) => !cleared.has(id)).sort();
|
|
1115
|
+
if (remaining.length === existing.ids.size)
|
|
1116
|
+
return;
|
|
1117
|
+
lock.assertOwnership();
|
|
1118
|
+
if (!remaining.length) {
|
|
1119
|
+
unlinkSync(path);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
lock.publish(`${JSON.stringify({ ids: remaining, reason: 'partial_clear', fenced_at: new Date().toISOString() }, null, 2)}\n`);
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
catch {
|
|
1126
|
+
// Leaving a stale fence entry is fail-closed and therefore acceptable.
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
export function revokeNativeSubagentAuthorities(cwd, childThreadIds, reason = 'identity_untrusted') {
|
|
1130
|
+
const ids = [...new Set(childThreadIds.map((value) => value.trim()).filter(Boolean))];
|
|
1131
|
+
return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
|
|
1132
|
+
const strict = readSubagentTrackingStateStrictSync(cwd);
|
|
1133
|
+
if (!strict.ok)
|
|
1134
|
+
throw new Error('Malformed subagent tracker authority state');
|
|
1135
|
+
const next = strict.state;
|
|
1136
|
+
const timestamp = new Date().toISOString();
|
|
1137
|
+
let changed = false;
|
|
1138
|
+
for (const session of Object.values(next.sessions)) {
|
|
1139
|
+
for (const id of ids) {
|
|
1140
|
+
const thread = session.threads[id];
|
|
1141
|
+
if (!thread?.direct_child_root_id && !thread?.reopen_authority_revoked)
|
|
1142
|
+
continue;
|
|
1143
|
+
thread.reopen_authority_revoked = true;
|
|
1144
|
+
thread.reopen_authority_conflict_reason = reason;
|
|
1145
|
+
thread.reopen_authority_conflict_at = timestamp;
|
|
1146
|
+
changed = true;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (changed) {
|
|
1150
|
+
context.assertOwnership();
|
|
1151
|
+
writeSubagentTrackingStateSync(cwd, next, context.publish);
|
|
1152
|
+
}
|
|
1153
|
+
// The revocation is now durable for these ids (either just published, or
|
|
1154
|
+
// already present on disk), so any prior fence for them can be released.
|
|
1155
|
+
clearAuthorityFence(cwd, ids);
|
|
1156
|
+
return next;
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
function formatReopenMetadata(entry) {
|
|
1160
|
+
const values = [
|
|
1161
|
+
entry.role ? `role: ${entry.role}` : null,
|
|
1162
|
+
entry.laneId ? `lane: ${entry.laneId}` : null,
|
|
1163
|
+
entry.scope ? `scope: ${entry.scope}` : null,
|
|
1164
|
+
`status: ${entry.status}`,
|
|
1165
|
+
entry.lastHandoffSummary ? `handoff: ${entry.lastHandoffSummary.slice(0, 120)}` : null,
|
|
1166
|
+
entry.resumeFailureReason ? `last failure: ${entry.resumeFailureReason.slice(0, 120)}` : null,
|
|
1167
|
+
].filter((value) => Boolean(value));
|
|
1168
|
+
return values.length ? ` (${values.join('; ')})` : '';
|
|
1169
|
+
}
|
|
1170
|
+
function persistedReopenAuthorityWarning(reason) {
|
|
1171
|
+
return [
|
|
1172
|
+
'[Persisted subagent reopen]',
|
|
1173
|
+
`- Warning: persisted subagent authority was excluded (${reason}); no resume authority or bookkeeping was granted.`,
|
|
1174
|
+
'- Silver rule: do not spawn a same-role/same-lane replacement solely because persisted reopen authority was unavailable; continue in the root or another compatible existing lane.',
|
|
1175
|
+
].join('\n');
|
|
1176
|
+
}
|
|
1177
|
+
function persistedReopenLegacyNotice(excludedCount, source) {
|
|
1178
|
+
return [
|
|
1179
|
+
'[Persisted subagent reopen]',
|
|
1180
|
+
`- SessionStart source: ${source}; saved subagent ids found: 0.`,
|
|
1181
|
+
`- Notice: ${excludedCount} persisted subagent record(s) were excluded as non-authoritative or legacy; no resume authority or bookkeeping was granted.`,
|
|
1182
|
+
'- Silver rule: do not spawn a same-role/same-lane replacement solely because persisted reopen authority was unavailable; continue in the root or another compatible existing lane.',
|
|
1183
|
+
].join('\n');
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* True when a stored partition is provably about the current root, and not an
|
|
1187
|
+
* unrelated/historical workspace session that merely happens to contain a
|
|
1188
|
+
* thread id equal to the current canonical session key. Canonical-key collision
|
|
1189
|
+
* quarantine is scoped to correlated partitions so it cannot destroy authority
|
|
1190
|
+
* or resume bookkeeping belonging to a different root.
|
|
1191
|
+
*/
|
|
1192
|
+
function partitionCorrelatesToRoot(session, sessionId, rootNativeSessionId) {
|
|
1193
|
+
if (session.session_id === sessionId || session.session_id === rootNativeSessionId)
|
|
1194
|
+
return true;
|
|
1195
|
+
if (session.leader_thread_id === rootNativeSessionId)
|
|
1196
|
+
return true;
|
|
1197
|
+
if (session.threads[rootNativeSessionId] !== undefined)
|
|
1198
|
+
return true;
|
|
1199
|
+
return Object.values(session.threads).some((thread) => thread.direct_child_root_id === rootNativeSessionId
|
|
1200
|
+
|| thread.direct_child_parent_id === rootNativeSessionId);
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Repair persisted root-as-subagent identity inversion inside an already-parsed
|
|
1204
|
+
* state. Only a thread whose id equals the exact native root id is conclusively
|
|
1205
|
+
* the root and is reclassified as leader. A thread whose id equals only the
|
|
1206
|
+
* canonical session key is always ambiguous — the storage key is not identity
|
|
1207
|
+
* proof — so it is never destructively reclassified: authority-bearing records
|
|
1208
|
+
* are revoked (fail closed) and every ambiguous record loses resume bookkeeping.
|
|
1209
|
+
* That ambiguity quarantine only applies inside partitions that correlate to
|
|
1210
|
+
* this root; unrelated partitions are left untouched.
|
|
1211
|
+
* Returns true when any record changed.
|
|
1212
|
+
*/
|
|
1213
|
+
function repairPersistedRootIdentityInState(state, sessionId, rootNativeSessionId, repairTimestamp) {
|
|
1214
|
+
let changed = false;
|
|
1215
|
+
for (const correlatedSession of Object.values(state.sessions)) {
|
|
1216
|
+
let sessionChanged = false;
|
|
1217
|
+
const correlated = partitionCorrelatesToRoot(correlatedSession, sessionId, rootNativeSessionId);
|
|
1218
|
+
for (const rootId of new Set([sessionId, rootNativeSessionId])) {
|
|
1219
|
+
const rootThread = correlatedSession.threads[rootId];
|
|
1220
|
+
if (!rootThread || rootThread.kind !== 'subagent')
|
|
1221
|
+
continue;
|
|
1222
|
+
const ambiguousCanonicalCollision = rootId === sessionId && sessionId !== rootNativeSessionId;
|
|
1223
|
+
if (ambiguousCanonicalCollision) {
|
|
1224
|
+
// An uncorrelated partition's identically-named thread belongs to a
|
|
1225
|
+
// different root: leave its authority and bookkeeping alone.
|
|
1226
|
+
if (!correlated)
|
|
1227
|
+
continue;
|
|
1228
|
+
const carriesAuthority = rootThread.provenance_kind === NATIVE_SUBAGENT_PROVENANCE
|
|
1229
|
+
|| rootThread.direct_child_root_id !== undefined
|
|
1230
|
+
|| rootThread.direct_child_parent_id !== undefined;
|
|
1231
|
+
if (carriesAuthority && rootThread.reopen_authority_revoked !== true) {
|
|
1232
|
+
// Fail closed without rewriting identity: the id collides with the
|
|
1233
|
+
// canonical root key but carries child-shaped authority evidence, so
|
|
1234
|
+
// it stays a subagent whose reopen authority is revoked until root
|
|
1235
|
+
// identity is independently established.
|
|
1236
|
+
rootThread.reopen_authority_revoked = true;
|
|
1237
|
+
rootThread.reopen_authority_conflict_reason = 'canonical_root_collision';
|
|
1238
|
+
rootThread.reopen_authority_conflict_at = repairTimestamp;
|
|
1239
|
+
sessionChanged = true;
|
|
1240
|
+
}
|
|
1241
|
+
if (rootThread.resume_requested_at !== undefined
|
|
1242
|
+
|| rootThread.resume_completed_at !== undefined
|
|
1243
|
+
|| rootThread.resume_failed_at !== undefined
|
|
1244
|
+
|| rootThread.resume_failure_reason !== undefined) {
|
|
1245
|
+
delete rootThread.resume_requested_at;
|
|
1246
|
+
delete rootThread.resume_completed_at;
|
|
1247
|
+
delete rootThread.resume_failed_at;
|
|
1248
|
+
delete rootThread.resume_failure_reason;
|
|
1249
|
+
sessionChanged = true;
|
|
1250
|
+
}
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
if (rootId !== rootNativeSessionId)
|
|
1254
|
+
continue;
|
|
1255
|
+
rootThread.kind = 'leader';
|
|
1256
|
+
delete rootThread.direct_child_root_id;
|
|
1257
|
+
delete rootThread.direct_child_parent_id;
|
|
1258
|
+
delete rootThread.reopen_authority_revoked;
|
|
1259
|
+
delete rootThread.reopen_authority_conflict_reason;
|
|
1260
|
+
delete rootThread.reopen_authority_conflict_at;
|
|
1261
|
+
delete rootThread.resume_requested_at;
|
|
1262
|
+
delete rootThread.resume_completed_at;
|
|
1263
|
+
delete rootThread.resume_failed_at;
|
|
1264
|
+
delete rootThread.resume_failure_reason;
|
|
1265
|
+
sessionChanged = true;
|
|
1266
|
+
}
|
|
1267
|
+
if (correlatedSession.threads[rootNativeSessionId] && correlatedSession.leader_thread_id !== rootNativeSessionId) {
|
|
1268
|
+
correlatedSession.leader_thread_id = rootNativeSessionId;
|
|
1269
|
+
sessionChanged = true;
|
|
1270
|
+
}
|
|
1271
|
+
if (sessionChanged) {
|
|
1272
|
+
correlatedSession.updated_at = repairTimestamp;
|
|
1273
|
+
changed = true;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return changed;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Pointer-authoritative root identity repair, decoupled from reopen candidate
|
|
1280
|
+
* output. Runs under the tracker lock and fails closed on malformed bytes.
|
|
1281
|
+
*/
|
|
1282
|
+
export function repairPersistedRootIdentity(cwd, context) {
|
|
1283
|
+
const sessionId = context.sessionId.trim();
|
|
1284
|
+
const rootNativeSessionId = context.rootNativeSessionId.trim();
|
|
1285
|
+
if (!sessionId || !rootNativeSessionId)
|
|
1286
|
+
return false;
|
|
1287
|
+
const path = subagentTrackingPath(cwd);
|
|
1288
|
+
return withCrossProcessFileLockSync(path, (lock) => {
|
|
1289
|
+
let raw;
|
|
1290
|
+
try {
|
|
1291
|
+
raw = readFileSync(path, 'utf-8');
|
|
1292
|
+
}
|
|
1293
|
+
catch {
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1296
|
+
const state = parseStrictSubagentTrackingState(raw);
|
|
1297
|
+
if (!state)
|
|
1298
|
+
return false;
|
|
1299
|
+
const changed = repairPersistedRootIdentityInState(state, sessionId, rootNativeSessionId, new Date().toISOString());
|
|
1300
|
+
if (changed) {
|
|
1301
|
+
lock.assertOwnership();
|
|
1302
|
+
writeSubagentTrackingStateSync(cwd, state, lock.publish);
|
|
1303
|
+
}
|
|
1304
|
+
return changed;
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Quarantine reopen authority for a root that is persisted as a subagent when
|
|
1309
|
+
* the identity evidence is contradictory (for example a self-parented
|
|
1310
|
+
* transcript marker). Unlike full repair this never asserts leader identity, so
|
|
1311
|
+
* legitimate descriptive subagent evidence is preserved; it only strips the
|
|
1312
|
+
* authority and resume bookkeeping that a root must never carry. Returns true
|
|
1313
|
+
* when any record changed.
|
|
1314
|
+
*/
|
|
1315
|
+
export function quarantinePersistedRootAuthority(cwd, context) {
|
|
1316
|
+
const sessionId = context.sessionId.trim();
|
|
1317
|
+
const rootNativeSessionId = context.rootNativeSessionId.trim();
|
|
1318
|
+
if (!sessionId || !rootNativeSessionId)
|
|
1319
|
+
return false;
|
|
1320
|
+
const path = subagentTrackingPath(cwd);
|
|
1321
|
+
return withCrossProcessFileLockSync(path, (lock) => {
|
|
1322
|
+
let raw;
|
|
1323
|
+
try {
|
|
1324
|
+
raw = readFileSync(path, 'utf-8');
|
|
1325
|
+
}
|
|
1326
|
+
catch {
|
|
1327
|
+
return false;
|
|
1328
|
+
}
|
|
1329
|
+
const state = parseStrictSubagentTrackingState(raw);
|
|
1330
|
+
if (!state)
|
|
1331
|
+
return false;
|
|
1332
|
+
const timestamp = new Date().toISOString();
|
|
1333
|
+
let changed = false;
|
|
1334
|
+
for (const session of Object.values(state.sessions)) {
|
|
1335
|
+
if (!partitionCorrelatesToRoot(session, sessionId, rootNativeSessionId))
|
|
1336
|
+
continue;
|
|
1337
|
+
const rootThread = session.threads[rootNativeSessionId];
|
|
1338
|
+
if (!rootThread || rootThread.kind !== 'subagent')
|
|
1339
|
+
continue;
|
|
1340
|
+
const carriesAuthority = rootThread.provenance_kind === NATIVE_SUBAGENT_PROVENANCE
|
|
1341
|
+
|| rootThread.direct_child_root_id !== undefined
|
|
1342
|
+
|| rootThread.direct_child_parent_id !== undefined;
|
|
1343
|
+
let sessionChanged = false;
|
|
1344
|
+
if (carriesAuthority && rootThread.reopen_authority_revoked !== true) {
|
|
1345
|
+
rootThread.reopen_authority_revoked = true;
|
|
1346
|
+
rootThread.reopen_authority_conflict_reason = 'contradictory_root_child_evidence';
|
|
1347
|
+
rootThread.reopen_authority_conflict_at = timestamp;
|
|
1348
|
+
sessionChanged = true;
|
|
1349
|
+
}
|
|
1350
|
+
if (rootThread.resume_requested_at !== undefined
|
|
1351
|
+
|| rootThread.resume_completed_at !== undefined
|
|
1352
|
+
|| rootThread.resume_failed_at !== undefined
|
|
1353
|
+
|| rootThread.resume_failure_reason !== undefined) {
|
|
1354
|
+
delete rootThread.resume_requested_at;
|
|
1355
|
+
delete rootThread.resume_completed_at;
|
|
1356
|
+
delete rootThread.resume_failed_at;
|
|
1357
|
+
delete rootThread.resume_failure_reason;
|
|
1358
|
+
sessionChanged = true;
|
|
1359
|
+
}
|
|
1360
|
+
if (sessionChanged) {
|
|
1361
|
+
session.updated_at = timestamp;
|
|
1362
|
+
changed = true;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
if (changed) {
|
|
1366
|
+
lock.assertOwnership();
|
|
1367
|
+
writeSubagentTrackingStateSync(cwd, state, lock.publish);
|
|
1368
|
+
}
|
|
1369
|
+
return changed;
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
/** Strict, lock-serialized authority consumption for SessionStart persisted reopen output. */
|
|
1373
|
+
export function consumeDirectChildReopenContext(cwd, context) {
|
|
1374
|
+
const sessionId = context.sessionId.trim();
|
|
1375
|
+
const rootNativeSessionId = context.rootNativeSessionId.trim();
|
|
1376
|
+
if (!sessionId || !rootNativeSessionId)
|
|
1377
|
+
return null;
|
|
1378
|
+
const path = subagentTrackingPath(cwd);
|
|
1379
|
+
return withCrossProcessFileLockSync(path, (lock) => {
|
|
1380
|
+
let raw;
|
|
1381
|
+
try {
|
|
1382
|
+
raw = readFileSync(path, 'utf-8');
|
|
1383
|
+
}
|
|
1384
|
+
catch (error) {
|
|
1385
|
+
if (error.code === 'ENOENT')
|
|
1386
|
+
return null;
|
|
1387
|
+
return persistedReopenAuthorityWarning('tracker_read_failed');
|
|
1388
|
+
}
|
|
1389
|
+
const state = parseStrictSubagentTrackingState(raw);
|
|
1390
|
+
if (!state)
|
|
1391
|
+
return persistedReopenAuthorityWarning('malformed_tracker_state');
|
|
1392
|
+
// A revocation that could not be published leaves a durable fence. Fenced
|
|
1393
|
+
// ids stay denied here even though the on-disk record still looks valid,
|
|
1394
|
+
// so a failed negative observation can never leave old authority usable.
|
|
1395
|
+
const fence = readAuthorityFence(cwd);
|
|
1396
|
+
if (!fence.ok)
|
|
1397
|
+
return persistedReopenAuthorityWarning('unreadable_authority_fence');
|
|
1398
|
+
const repairTimestamp = new Date().toISOString();
|
|
1399
|
+
// Root identity repair runs before candidate eligibility so a missing
|
|
1400
|
+
// canonical partition cannot leave a known root inversion unrepaired.
|
|
1401
|
+
const changed = repairPersistedRootIdentityInState(state, sessionId, rootNativeSessionId, repairTimestamp);
|
|
1402
|
+
const session = state.sessions[sessionId];
|
|
1403
|
+
if (!session) {
|
|
1404
|
+
if (changed) {
|
|
1405
|
+
lock.assertOwnership();
|
|
1406
|
+
writeSubagentTrackingStateSync(cwd, state, lock.publish);
|
|
1407
|
+
}
|
|
1408
|
+
return null;
|
|
1409
|
+
}
|
|
1410
|
+
const savedSubagents = Object.values(session.threads)
|
|
1411
|
+
.filter((thread) => {
|
|
1412
|
+
if (thread.kind !== 'subagent'
|
|
1413
|
+
|| thread.thread_id === sessionId
|
|
1414
|
+
|| thread.thread_id === rootNativeSessionId
|
|
1415
|
+
|| session.leader_thread_id === thread.thread_id
|
|
1416
|
+
|| fence.ids.has(thread.thread_id)
|
|
1417
|
+
|| thread.provenance_kind !== NATIVE_SUBAGENT_PROVENANCE
|
|
1418
|
+
|| thread.direct_child_root_id !== rootNativeSessionId
|
|
1419
|
+
|| thread.direct_child_parent_id !== rootNativeSessionId
|
|
1420
|
+
|| thread.reopen_authority_revoked === true)
|
|
1421
|
+
return false;
|
|
1422
|
+
return Object.values(state.sessions).every((correlationSession) => {
|
|
1423
|
+
const view = correlationSession.threads[thread.thread_id];
|
|
1424
|
+
if (!view)
|
|
1425
|
+
return true;
|
|
1426
|
+
// Recognized legacy adapted records are descriptive-only: they never
|
|
1427
|
+
// grant authority and never block a newly attested valid child. The
|
|
1428
|
+
// tolerance is shape-constrained: an adapted view may only waive
|
|
1429
|
+
// missing attestation for a non-leader subagent view and never waives
|
|
1430
|
+
// leader-kind or leader_thread_id contradictions.
|
|
1431
|
+
if (view.provenance_kind === DESCRIPTIVE_ADAPTED_PROVENANCE
|
|
1432
|
+
&& view.kind === 'subagent'
|
|
1433
|
+
&& correlationSession.leader_thread_id !== thread.thread_id)
|
|
1434
|
+
return true;
|
|
1435
|
+
return correlationSession.leader_thread_id !== thread.thread_id
|
|
1436
|
+
&& view.kind === 'subagent'
|
|
1437
|
+
&& view.provenance_kind === NATIVE_SUBAGENT_PROVENANCE
|
|
1438
|
+
&& view.direct_child_root_id === rootNativeSessionId
|
|
1439
|
+
&& view.direct_child_parent_id === rootNativeSessionId
|
|
1440
|
+
&& view.reopen_authority_revoked !== true;
|
|
1441
|
+
});
|
|
1442
|
+
})
|
|
1443
|
+
.map((thread) => normalizeLedgerEntry(thread, thread.status ?? 'closed'))
|
|
1444
|
+
.sort(compareResumeEntries);
|
|
1445
|
+
if (!savedSubagents.length) {
|
|
1446
|
+
if (changed) {
|
|
1447
|
+
session.updated_at = new Date().toISOString();
|
|
1448
|
+
lock.assertOwnership();
|
|
1449
|
+
writeSubagentTrackingStateSync(cwd, state, lock.publish);
|
|
1450
|
+
}
|
|
1451
|
+
// Surface intentional exclusion of legacy/non-authoritative records instead
|
|
1452
|
+
// of silently returning no context. The notice fires only when every
|
|
1453
|
+
// relevant view across every correlation partition is genuinely
|
|
1454
|
+
// descriptive/legacy: any authority-bearing, leader-kind, or
|
|
1455
|
+
// leader_thread_id-colliding view keeps the denial fail-closed null.
|
|
1456
|
+
const excludedIds = new Set(Object.values(session.threads)
|
|
1457
|
+
.filter((thread) => thread.kind === 'subagent')
|
|
1458
|
+
.map((thread) => thread.thread_id));
|
|
1459
|
+
const everyRelevantViewIsLegacy = excludedIds.size > 0
|
|
1460
|
+
&& Object.values(state.sessions).every((correlationSession) => {
|
|
1461
|
+
if (correlationSession.leader_thread_id && excludedIds.has(correlationSession.leader_thread_id))
|
|
1462
|
+
return false;
|
|
1463
|
+
return [...excludedIds].every((threadId) => {
|
|
1464
|
+
const view = correlationSession.threads[threadId];
|
|
1465
|
+
if (!view)
|
|
1466
|
+
return true;
|
|
1467
|
+
return view.kind === 'subagent'
|
|
1468
|
+
&& view.provenance_kind !== NATIVE_SUBAGENT_PROVENANCE
|
|
1469
|
+
&& view.direct_child_root_id === undefined
|
|
1470
|
+
&& view.direct_child_parent_id === undefined
|
|
1471
|
+
&& view.reopen_authority_revoked !== true;
|
|
1472
|
+
});
|
|
1473
|
+
});
|
|
1474
|
+
if (everyRelevantViewIsLegacy) {
|
|
1475
|
+
return persistedReopenLegacyNotice(excludedIds.size, context.source);
|
|
1476
|
+
}
|
|
1477
|
+
return null;
|
|
1478
|
+
}
|
|
1479
|
+
const reopenTargets = savedSubagents.filter((entry) => entry.status !== 'unavailable');
|
|
1480
|
+
const unavailableTargets = savedSubagents.filter((entry) => entry.status === 'unavailable');
|
|
1481
|
+
const failedTargets = savedSubagents.filter((entry) => entry.resumeFailedAt || entry.resumeFailureReason);
|
|
1482
|
+
const now = new Date().toISOString();
|
|
1483
|
+
for (const target of reopenTargets) {
|
|
1484
|
+
session.threads[target.threadId] = { ...session.threads[target.threadId], resume_requested_at: now };
|
|
1485
|
+
}
|
|
1486
|
+
if (reopenTargets.length || changed) {
|
|
1487
|
+
session.updated_at = now;
|
|
1488
|
+
lock.assertOwnership();
|
|
1489
|
+
writeSubagentTrackingStateSync(cwd, state, lock.publish);
|
|
1490
|
+
}
|
|
1491
|
+
const lines = [
|
|
1492
|
+
'[Persisted subagent reopen]',
|
|
1493
|
+
`- SessionStart source: ${context.source}; saved subagent ids found: ${savedSubagents.length}.`,
|
|
1494
|
+
];
|
|
1495
|
+
if (reopenTargets.length) {
|
|
1496
|
+
lines.push('- Reopen these persisted subagents by id before continuing work or spawning any same-role/same-lane replacement:');
|
|
1497
|
+
lines.push(...reopenTargets.slice(0, 12).map((entry) => ` - resume_agent(${JSON.stringify(entry.agentId)})${formatReopenMetadata(entry)}`));
|
|
1498
|
+
if (reopenTargets.length > 12)
|
|
1499
|
+
lines.push(` - ... ${reopenTargets.length - 12} more saved subagent id(s) omitted from this compact SessionStart context; consult .omx/state/subagent-tracking.json before spawning replacements.`);
|
|
1500
|
+
}
|
|
1501
|
+
else {
|
|
1502
|
+
lines.push('- No compatible saved subagent id is currently marked reopenable; do not spawn a replacement merely because reopen was unavailable.');
|
|
1503
|
+
}
|
|
1504
|
+
lines.push('- Silver rule: when follow-up work targets an existing role/lane, reuse the matching reopened id; avoid duplicate same-type subagent spawns.');
|
|
1505
|
+
lines.push('- If resume_agent fails, surface a clear warning with the id and reason, then continue in the root or another compatible existing lane; do not spawn a new agent solely because reopen failed.');
|
|
1506
|
+
const warnings = [...new Map([...unavailableTargets, ...failedTargets].map((entry) => [entry.agentId, entry])).values()];
|
|
1507
|
+
if (warnings.length) {
|
|
1508
|
+
lines.push('- Reopen warnings:');
|
|
1509
|
+
lines.push(...warnings.slice(0, 8).map((entry) => ` - ${entry.agentId}${formatReopenMetadata(entry)}`));
|
|
1510
|
+
if (warnings.length > 8)
|
|
1511
|
+
lines.push(` - ... ${warnings.length - 8} more warning(s) omitted from this compact SessionStart context.`);
|
|
1512
|
+
}
|
|
1513
|
+
return lines.join('\n');
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1337
1516
|
export function summarizeSubagentSession(state, sessionId, options = {}) {
|
|
1338
1517
|
const normalized = normalizeSubagentTrackingState(state);
|
|
1339
1518
|
const session = normalized.sessions[sessionId];
|