oh-my-codex 0.20.0 → 0.20.2
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 +6 -6
- package/Cargo.toml +1 -1
- package/README.md +10 -1
- package/dist/adapt/__tests__/hermes.test.js.map +1 -1
- package/dist/adapt/contracts.d.ts.map +1 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/auth/__tests__/hotswap.test.d.ts +2 -0
- package/dist/auth/__tests__/hotswap.test.d.ts.map +1 -0
- package/dist/auth/__tests__/hotswap.test.js +183 -0
- package/dist/auth/__tests__/hotswap.test.js.map +1 -0
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +17 -4
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/auth/paths.js.map +1 -1
- package/dist/auth/storage.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +4 -4
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/completion-gate.js.map +1 -1
- package/dist/autopilot/fsm.d.ts +1 -1
- package/dist/autopilot/fsm.d.ts.map +1 -1
- package/dist/autoresearch/goal.js.map +1 -1
- package/dist/autoresearch/runtime.js.map +1 -1
- package/dist/capabilities/lockfile.js.map +1 -1
- package/dist/catalog/reader.js.map +1 -1
- package/dist/catalog/skill-mirror.js.map +1 -1
- package/dist/cli/__tests__/adapt-help.test.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/agents.test.js.map +1 -1
- package/dist/cli/__tests__/api.test.js.map +1 -1
- package/dist/cli/__tests__/ask.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
- package/dist/cli/__tests__/capabilities.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +27 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-context-window-warning.test.js +83 -78
- package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-invalid-config.test.js +56 -4
- package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +735 -14
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +216 -11
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +49 -3
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +84 -12
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts +2 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts.map +1 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +128 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -0
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +6 -0
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +8 -3
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +187 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +129 -0
- package/dist/cli/__tests__/ralplan.test.js.map +1 -0
- package/dist/cli/__tests__/resume.test.js +6 -1
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +25 -0
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/session-search.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +79 -0
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-gh-star.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +131 -5
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +285 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -0
- package/dist/cli/__tests__/setup-install-mode.test.js +2264 -17
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-preferences.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-preferences.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-preferences.test.js +79 -0
- package/dist/cli/__tests__/setup-preferences.test.js.map +1 -0
- package/dist/cli/__tests__/setup-refresh.test.js +168 -5
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +73 -6
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/state.test.js +37 -0
- package/dist/cli/__tests__/state.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +61 -1
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +2067 -26
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +58 -0
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/url.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/adapt.js.map +1 -1
- package/dist/cli/agents-init.js.map +1 -1
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/ask.d.ts +1 -1
- package/dist/cli/ask.d.ts.map +1 -1
- package/dist/cli/ask.js.map +1 -1
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/autoresearch-guided.js.map +1 -1
- package/dist/cli/autoresearch-intake.js.map +1 -1
- package/dist/cli/capabilities.js.map +1 -1
- package/dist/cli/cleanup.js.map +1 -1
- package/dist/cli/codex-feature-probe.js.map +1 -1
- 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 +636 -161
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +11 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +108 -20
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-serve.js.map +1 -1
- package/dist/cli/mission.js.map +1 -1
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +18 -0
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -0
- package/dist/cli/native-hook-claim-journal.js +239 -0
- package/dist/cli/native-hook-claim-journal.js.map +1 -0
- package/dist/cli/omx.js.map +1 -1
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/project-runtime-codex-homes.js.map +1 -1
- package/dist/cli/question.js.map +1 -1
- package/dist/cli/ralph.d.ts.map +1 -1
- package/dist/cli/ralph.js +3 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +14 -0
- package/dist/cli/ralplan.d.ts.map +1 -0
- package/dist/cli/ralplan.js +203 -0
- package/dist/cli/ralplan.js.map +1 -0
- package/dist/cli/setup-preferences.d.ts +11 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +32 -0
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +53 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +1403 -400
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/star-prompt.js.map +1 -1
- package/dist/cli/state.d.ts.map +1 -1
- package/dist/cli/state.js +2 -1
- package/dist/cli/state.js.map +1 -1
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/ultragoal.d.ts +5 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +34 -2
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts +8 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +947 -99
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +8 -1
- package/dist/cli/update.js.map +1 -1
- package/dist/cli/url.js.map +1 -1
- package/dist/compat/__tests__/doctor-contract.test.js +5 -1
- package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.js +1287 -11
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +1049 -135
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +28 -49
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/codex-feature-flags.d.ts.map +1 -1
- package/dist/config/codex-feature-flags.js.map +1 -1
- package/dist/config/codex-hooks.d.ts +124 -1
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +2142 -265
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/commit-lore-guard.js.map +1 -1
- package/dist/config/generator.d.ts +40 -26
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +1159 -288
- package/dist/config/generator.js.map +1 -1
- package/dist/config/mcp-registry.js.map +1 -1
- package/dist/config/models.d.ts +2 -2
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js.map +1 -1
- package/dist/config/omx-first-party-mcp.js.map +1 -1
- package/dist/config/team-mode.d.ts.map +1 -1
- package/dist/document-refresh/__tests__/enforcer.test.js.map +1 -1
- package/dist/document-refresh/enforcer.js.map +1 -1
- package/dist/exec/followup.js.map +1 -1
- package/dist/goal-workflows/artifacts.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +7 -4
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +9 -3
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +1372 -2
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +14 -0
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +669 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +2 -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 +142 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +48 -2
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +61 -44
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +3 -2
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js +243 -4
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +8 -1
- 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.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
- package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts +2 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.js +82 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.js.map +1 -0
- package/dist/hooks/__tests__/session.test.js +532 -11
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +5 -5
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/code-simplifier/index.js.map +1 -1
- package/dist/hooks/explore-routing.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/extensibility/events.js.map +1 -1
- package/dist/hooks/extensibility/loader.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -1
- package/dist/hooks/extensibility/sdk/logging.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +33 -7
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +2228 -119
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts +12 -0
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +41 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/prompt-session-provenance.d.ts +87 -0
- package/dist/hooks/prompt-session-provenance.d.ts.map +1 -0
- package/dist/hooks/prompt-session-provenance.js +180 -0
- package/dist/hooks/prompt-session-provenance.js.map +1 -0
- package/dist/hooks/session.d.ts +139 -34
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1239 -317
- package/dist/hooks/session.js.map +1 -1
- package/dist/hooks/task-size-detector.js.map +1 -1
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/authority.js.map +1 -1
- package/dist/hud/constants.js.map +1 -1
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +146 -2
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +26 -5
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +131 -7
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +15 -2
- package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
- package/dist/mcp/__tests__/state-paths.test.js +98 -1
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts +58 -58
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/mcp/hermes-server.d.ts +70 -70
- package/dist/mcp/hermes-server.d.ts.map +1 -1
- package/dist/mcp/lifecycle-telemetry.js.map +1 -1
- package/dist/mcp/memory-server.d.ts +34 -34
- package/dist/mcp/memory-server.d.ts.map +1 -1
- package/dist/mcp/memory-server.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +14 -0
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +96 -20
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +47 -47
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/trace-server.d.ts +4 -4
- package/dist/mcp/trace-server.d.ts.map +1 -1
- package/dist/mcp/trace-server.js.map +1 -1
- package/dist/mcp/wiki-server.d.ts +23 -23
- package/dist/mcp/wiki-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +53 -1
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +5 -5
- package/dist/modes/base.js.map +1 -1
- package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +1 -1
- package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +177 -85
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +1 -1
- package/dist/notifications/__tests__/session-registry.test.js.map +1 -1
- package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
- package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
- package/dist/notifications/http-client.js.map +1 -1
- package/dist/notifications/index.d.ts +4 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +3 -9
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/lifecycle-dedupe.js.map +1 -1
- package/dist/notifications/reply-listener.d.ts.map +1 -1
- package/dist/notifications/reply-listener.js.map +1 -1
- package/dist/notifications/session-registry.js.map +1 -1
- package/dist/notifications/session-status.js.map +1 -1
- package/dist/notifications/temp-contract.js.map +1 -1
- package/dist/notifications/tmux-detector.js.map +1 -1
- package/dist/notifications/tmux.js.map +1 -1
- package/dist/openclaw/config.js.map +1 -1
- package/dist/openclaw/dispatcher.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +8 -5
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/stages/code-review.js.map +1 -1
- package/dist/pipeline/stages/ralph-verify.js.map +1 -1
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/pipeline/stages/team-exec.js.map +1 -1
- package/dist/pipeline/stages/ultragoal.js.map +1 -1
- package/dist/pipeline/stages/ultraqa.js.map +1 -1
- package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
- package/dist/planning/artifact-names.js.map +1 -1
- package/dist/planning/artifacts.js.map +1 -1
- package/dist/question/__tests__/state.test.js.map +1 -1
- package/dist/question/__tests__/ui.test.js.map +1 -1
- package/dist/question/autopilot-wait.js.map +1 -1
- package/dist/question/client.d.ts.map +1 -1
- package/dist/question/client.js.map +1 -1
- package/dist/question/deep-interview.js.map +1 -1
- package/dist/question/events.js.map +1 -1
- package/dist/question/renderer.js.map +1 -1
- package/dist/question/state.d.ts.map +1 -1
- package/dist/question/state.js.map +1 -1
- package/dist/question/ui.js.map +1 -1
- package/dist/ralph/contract.d.ts +1 -1
- package/dist/ralph/contract.d.ts.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +403 -8
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +112 -14
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +3 -3
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +155 -60
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/runtime.d.ts +2 -2
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +1 -2
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/runtime/process-tree.js.map +1 -1
- package/dist/runtime/run-loop.js.map +1 -1
- package/dist/runtime/run-outcome.d.ts +3 -3
- package/dist/runtime/run-outcome.d.ts.map +1 -1
- package/dist/runtime/run-outcome.js.map +1 -1
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +5414 -801
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts +2 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts.map +1 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js +634 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js.map +1 -0
- package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -1
- package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts +2 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.js +174 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.js.map +1 -0
- package/dist/scripts/__tests__/notify-state-io.test.js +57 -1
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts +2 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +205 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts +2 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +175 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -0
- package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +848 -4
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +1 -1
- package/dist/scripts/build-api.js.map +1 -1
- package/dist/scripts/build-explore-harness.js.map +1 -1
- package/dist/scripts/build-sparkshell.js.map +1 -1
- package/dist/scripts/cleanup-explore-harness.js.map +1 -1
- package/dist/scripts/codex-execution-surface.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +825 -172
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/generate-native-release-manifest.js.map +1 -1
- package/dist/scripts/generate-release-body.js.map +1 -1
- package/dist/scripts/hook-payload-guard.d.ts.map +1 -1
- package/dist/scripts/hook-payload-guard.js.map +1 -1
- package/dist/scripts/notify-fallback-delivery.d.ts +48 -0
- package/dist/scripts/notify-fallback-delivery.d.ts.map +1 -0
- package/dist/scripts/notify-fallback-delivery.js +419 -0
- package/dist/scripts/notify-fallback-delivery.js.map +1 -0
- package/dist/scripts/notify-fallback-watcher.js +136 -30
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts +18 -9
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +39 -13
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.d.ts +23 -0
- package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.js +122 -12
- package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
- package/dist/scripts/notify-hook/orchestration-intent.d.ts +7 -7
- package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts +5 -2
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +33 -37
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts +17 -0
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +73 -11
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -2
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +4 -4
- package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts +2 -2
- package/dist/scripts/notify-hook/tmux-injection.d.ts +8 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +25 -16
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook/visual-verdict.d.ts +1 -1
- package/dist/scripts/notify-hook/visual-verdict.d.ts.map +1 -1
- package/dist/scripts/notify-hook/visual-verdict.js +15 -13
- package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
- package/dist/scripts/notify-hook.d.ts +24 -1
- package/dist/scripts/notify-hook.d.ts.map +1 -1
- package/dist/scripts/notify-hook.js +266 -90
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/prompt-inventory.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +879 -2
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +1528 -7
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/test-reply-listener-live.js.map +1 -1
- package/dist/scripts/test-sparkshell.js.map +1 -1
- package/dist/scripts/verify-native-agents.js.map +1 -1
- package/dist/session-history/friction.js.map +1 -1
- package/dist/session-history/search.js.map +1 -1
- package/dist/sidecar/__tests__/boundary.test.js.map +1 -1
- package/dist/sidecar/collector.js.map +1 -1
- package/dist/sidecar/index.js.map +1 -1
- package/dist/sidecar/render.js.map +1 -1
- package/dist/sidecar/tmux.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +64 -15
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +19 -1
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/__tests__/workflow-transition.test.js +61 -0
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/operations.d.ts +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +49 -55
- package/dist/state/operations.js.map +1 -1
- package/dist/state/paths.d.ts +1 -1
- package/dist/state/paths.d.ts.map +1 -1
- package/dist/state/paths.js +1 -1
- package/dist/state/paths.js.map +1 -1
- package/dist/state/skill-active.d.ts +13 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +43 -4
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/terminal-normalization.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +20 -4
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +2 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +1 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.js +988 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.js.map +1 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts +2 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts.map +1 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +273 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -0
- package/dist/subagents/__tests__/tracker.test.js +1550 -10
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/adapted-role-binding.d.ts +17 -0
- package/dist/subagents/adapted-role-binding.d.ts.map +1 -0
- package/dist/subagents/adapted-role-binding.js +75 -0
- package/dist/subagents/adapted-role-binding.js.map +1 -0
- package/dist/subagents/role-routing-marker.d.ts +10 -0
- package/dist/subagents/role-routing-marker.d.ts.map +1 -0
- package/dist/subagents/role-routing-marker.js +136 -0
- package/dist/subagents/role-routing-marker.js.map +1 -0
- package/dist/subagents/tracker.d.ts +172 -0
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +1077 -61
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/approved-execution.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +231 -1
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/role-router.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +401 -1
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +181 -0
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +434 -4
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
- package/dist/team/api-interop.d.ts +2 -2
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/commit-hygiene.js.map +1 -1
- package/dist/team/contracts.d.ts +5 -5
- package/dist/team/contracts.d.ts.map +1 -1
- package/dist/team/delivery-log.js.map +1 -1
- package/dist/team/followup-planner.js.map +1 -1
- package/dist/team/goal-workflow.js.map +1 -1
- package/dist/team/idle-nudge.d.ts.map +1 -1
- package/dist/team/model-contract.d.ts +20 -1
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +267 -30
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/orchestrator.js.map +1 -1
- package/dist/team/pane-status.js.map +1 -1
- package/dist/team/phase-controller.js.map +1 -1
- package/dist/team/reminder-intents.d.ts +1 -1
- package/dist/team/reminder-intents.d.ts.map +1 -1
- package/dist/team/reminder-intents.js.map +1 -1
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +83 -52
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts +9 -7
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +69 -28
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/dispatch-lock.js.map +1 -1
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/state/events.js.map +1 -1
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-identity.d.ts.map +1 -1
- package/dist/team/team-identity.js.map +1 -1
- package/dist/team/tmux-session.d.ts +2 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +90 -42
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/team/worktree.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +4 -1
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/url-reader/index.js.map +1 -1
- package/dist/utils/__tests__/agents-md.test.js +16 -0
- package/dist/utils/__tests__/agents-md.test.js.map +1 -1
- package/dist/utils/agents-md.d.ts.map +1 -1
- package/dist/utils/agents-md.js +4 -3
- package/dist/utils/agents-md.js.map +1 -1
- package/dist/utils/agents-model-table.js.map +1 -1
- package/dist/utils/package.js.map +1 -1
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/platform-command.js.map +1 -1
- package/dist/utils/repo-deps.js.map +1 -1
- package/dist/utils/version.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +1 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
- package/dist/verification/verifier.js.map +1 -1
- package/dist/visual/constants.d.ts +1 -1
- package/dist/visual/constants.d.ts.map +1 -1
- package/dist/vscode/__tests__/index.test.js.map +1 -1
- package/dist/vscode/index.js.map +1 -1
- package/dist/wiki/__tests__/cjk-tokenize.test.js.map +1 -1
- package/dist/wiki/__tests__/lint.test.js.map +1 -1
- package/dist/wiki/__tests__/query.test.js.map +1 -1
- package/dist/wiki/__tests__/storage.test.js.map +1 -1
- package/dist/wiki/__tests__/test-helpers.d.ts.map +1 -1
- package/dist/wiki/lint.js.map +1 -1
- package/dist/wiki/query.js.map +1 -1
- package/dist/wiki/storage.js.map +1 -1
- package/package.json +2 -2
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +10 -5
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +4 -2
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +5 -2
- package/skills/omx-setup/SKILL.md +10 -5
- package/skills/ralph/SKILL.md +4 -2
- package/skills/ralplan/SKILL.md +5 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +20497 -12377
- package/src/scripts/__tests__/fixtures/issue-3138-installed-smoke.ts +734 -0
- package/src/scripts/__tests__/notify-fallback-delivery.test.ts +177 -0
- package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +215 -0
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +193 -0
- package/src/scripts/__tests__/smoke-packed-install.test.ts +991 -4
- package/src/scripts/codex-native-hook.ts +918 -172
- package/src/scripts/notify-fallback-delivery.ts +416 -0
- package/src/scripts/notify-fallback-watcher.ts +132 -40
- package/src/scripts/notify-hook/auto-nudge.ts +43 -12
- package/src/scripts/notify-hook/managed-tmux.ts +149 -12
- package/src/scripts/notify-hook/ralph-session-resume.ts +37 -36
- package/src/scripts/notify-hook/state-io.ts +100 -13
- package/src/scripts/notify-hook/tmux-injection.ts +38 -21
- package/src/scripts/notify-hook/visual-verdict.ts +22 -12
- package/src/scripts/notify-hook.ts +335 -90
- package/src/scripts/smoke-packed-install.ts +1829 -6
|
@@ -1,16 +1,47 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { closeSync, existsSync, ftruncateSync, linkSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, writeSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { hostname } from 'node:os';
|
|
5
|
+
import { basename, dirname, join } from 'node:path';
|
|
6
|
+
import { AGENT_DEFINITIONS } from '../agents/definitions.js';
|
|
7
|
+
import { getBaseStateDir, getBaseStateDirWithSource } from '../state/paths.js';
|
|
8
|
+
import { canonicalizeOriginCwd } from '../leader/contract.js';
|
|
9
|
+
import { codexAgentsDir, projectCodexAgentsDir } from '../utils/paths.js';
|
|
5
10
|
export const SUBAGENT_TRACKING_SCHEMA_VERSION = 1;
|
|
6
11
|
export const DEFAULT_SUBAGENT_ACTIVE_WINDOW_MS = 120_000;
|
|
12
|
+
export const OMX_ADAPTED_PROVENANCE = 'omx_adapted';
|
|
13
|
+
export const NATIVE_SUBAGENT_PROVENANCE = 'native_subagent';
|
|
14
|
+
const KNOWN_TYPED_AGENT_ROLES = new Set(Object.keys(AGENT_DEFINITIONS).map((role) => role.toLowerCase()));
|
|
7
15
|
export function subagentTrackingPath(cwd) {
|
|
8
16
|
return join(getBaseStateDir(cwd), 'subagent-tracking.json');
|
|
9
17
|
}
|
|
18
|
+
export function resolveInstalledRoleName(role, codexHomeOverride) {
|
|
19
|
+
const normalizedRole = role.trim().toLowerCase();
|
|
20
|
+
if (!normalizedRole)
|
|
21
|
+
return null;
|
|
22
|
+
if (KNOWN_TYPED_AGENT_ROLES.has(normalizedRole))
|
|
23
|
+
return normalizedRole;
|
|
24
|
+
for (const agentsDir of [codexAgentsDir(codexHomeOverride), projectCodexAgentsDir()]) {
|
|
25
|
+
try {
|
|
26
|
+
for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
|
|
27
|
+
if (!entry.isFile() || !entry.name.endsWith('.toml'))
|
|
28
|
+
continue;
|
|
29
|
+
const installedRole = entry.name.slice(0, -'.toml'.length).trim().toLowerCase();
|
|
30
|
+
if (installedRole === normalizedRole)
|
|
31
|
+
return installedRole;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Missing or unreadable agent directories do not invalidate built-in roles.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
10
40
|
export function createSubagentTrackingState() {
|
|
11
41
|
return {
|
|
12
42
|
schemaVersion: SUBAGENT_TRACKING_SCHEMA_VERSION,
|
|
13
43
|
sessions: {},
|
|
44
|
+
pending_role_intents: [],
|
|
14
45
|
};
|
|
15
46
|
}
|
|
16
47
|
function normalizeSubagentStatus(value) {
|
|
@@ -88,6 +119,40 @@ export function isTrustedSubagentThread(session, threadId) {
|
|
|
88
119
|
return false;
|
|
89
120
|
return session.threads[normalizedThreadId]?.kind === 'subagent';
|
|
90
121
|
}
|
|
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
|
+
}
|
|
91
156
|
export function normalizeSubagentTrackingState(input) {
|
|
92
157
|
const base = createSubagentTrackingState();
|
|
93
158
|
if (!input || typeof input !== 'object')
|
|
@@ -102,9 +167,7 @@ export function normalizeSubagentTrackingState(input) {
|
|
|
102
167
|
if (!rawThread || typeof rawThread !== 'object')
|
|
103
168
|
continue;
|
|
104
169
|
const candidate = rawThread;
|
|
105
|
-
const normalizedThreadId = typeof candidate.thread_id === 'string' && candidate.thread_id.trim().length > 0
|
|
106
|
-
? candidate.thread_id.trim()
|
|
107
|
-
: threadId.trim();
|
|
170
|
+
const normalizedThreadId = typeof candidate.thread_id === 'string' && candidate.thread_id.trim().length > 0 ? candidate.thread_id.trim() : threadId.trim();
|
|
108
171
|
if (!normalizedThreadId)
|
|
109
172
|
continue;
|
|
110
173
|
const kind = candidate.kind === 'leader' ? 'leader' : 'subagent';
|
|
@@ -113,32 +176,31 @@ export function normalizeSubagentTrackingState(input) {
|
|
|
113
176
|
: typeof candidate.last_seen_at === 'string' && candidate.last_seen_at.trim().length > 0
|
|
114
177
|
? candidate.last_seen_at
|
|
115
178
|
: new Date(0).toISOString();
|
|
116
|
-
const lastSeenAt = typeof candidate.last_seen_at === 'string' && candidate.last_seen_at.trim().length > 0
|
|
117
|
-
? candidate.last_seen_at
|
|
118
|
-
: firstSeenAt;
|
|
179
|
+
const lastSeenAt = typeof candidate.last_seen_at === 'string' && candidate.last_seen_at.trim().length > 0 ? candidate.last_seen_at : firstSeenAt;
|
|
119
180
|
threads[normalizedThreadId] = {
|
|
120
181
|
thread_id: normalizedThreadId,
|
|
121
182
|
kind,
|
|
122
183
|
first_seen_at: firstSeenAt,
|
|
123
184
|
last_seen_at: lastSeenAt,
|
|
124
|
-
...(typeof candidate.last_turn_id === 'string' && candidate.last_turn_id.trim().length > 0
|
|
125
|
-
|
|
126
|
-
: {}),
|
|
127
|
-
...(typeof candidate.completed_at === 'string' && candidate.completed_at.trim().length > 0
|
|
128
|
-
? { completed_at: candidate.completed_at }
|
|
129
|
-
: {}),
|
|
185
|
+
...(typeof candidate.last_turn_id === 'string' && candidate.last_turn_id.trim().length > 0 ? { last_turn_id: candidate.last_turn_id } : {}),
|
|
186
|
+
...(typeof candidate.completed_at === 'string' && candidate.completed_at.trim().length > 0 ? { completed_at: candidate.completed_at } : {}),
|
|
130
187
|
...(typeof candidate.last_completed_turn_id === 'string' && candidate.last_completed_turn_id.trim().length > 0
|
|
131
188
|
? { last_completed_turn_id: candidate.last_completed_turn_id }
|
|
132
189
|
: {}),
|
|
133
|
-
turn_count: typeof candidate.turn_count === 'number' && Number.isFinite(candidate.turn_count) && candidate.turn_count > 0
|
|
134
|
-
? candidate.turn_count
|
|
135
|
-
: 1,
|
|
190
|
+
turn_count: typeof candidate.turn_count === 'number' && Number.isFinite(candidate.turn_count) && candidate.turn_count > 0 ? candidate.turn_count : 1,
|
|
136
191
|
...(typeof candidate.mode === 'string' && candidate.mode.trim().length > 0 ? { mode: candidate.mode } : {}),
|
|
137
192
|
...(typeof candidate.role === 'string' && candidate.role.trim().length > 0 ? { role: candidate.role.trim() } : {}),
|
|
193
|
+
...(typeof candidate.provenance_kind === 'string' && candidate.provenance_kind.trim().length > 0
|
|
194
|
+
? { provenance_kind: candidate.provenance_kind.trim() }
|
|
195
|
+
: {}),
|
|
138
196
|
...(typeof candidate.lane_id === 'string' && candidate.lane_id.trim().length > 0 ? { lane_id: candidate.lane_id.trim() } : {}),
|
|
139
197
|
...(typeof candidate.scope === 'string' && candidate.scope.trim().length > 0 ? { scope: candidate.scope.trim() } : {}),
|
|
140
|
-
...(typeof candidate.agent_nickname === 'string' && candidate.agent_nickname.trim().length > 0
|
|
141
|
-
|
|
198
|
+
...(typeof candidate.agent_nickname === 'string' && candidate.agent_nickname.trim().length > 0
|
|
199
|
+
? { agent_nickname: candidate.agent_nickname.trim() }
|
|
200
|
+
: {}),
|
|
201
|
+
...(typeof candidate.completion_source === 'string' && candidate.completion_source.trim().length > 0
|
|
202
|
+
? { completion_source: candidate.completion_source }
|
|
203
|
+
: {}),
|
|
142
204
|
...(normalizeSubagentStatus(candidate.status) ? { status: normalizeSubagentStatus(candidate.status) } : {}),
|
|
143
205
|
...(typeof candidate.last_handoff_summary === 'string' && candidate.last_handoff_summary.trim().length > 0
|
|
144
206
|
? { last_handoff_summary: candidate.last_handoff_summary.trim() }
|
|
@@ -158,8 +220,12 @@ export function normalizeSubagentTrackingState(input) {
|
|
|
158
220
|
};
|
|
159
221
|
}
|
|
160
222
|
const sessionCandidate = rawSession;
|
|
161
|
-
const leaderThreadId = typeof sessionCandidate.leader_thread_id === 'string'
|
|
162
|
-
|
|
223
|
+
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()
|
|
163
229
|
: undefined;
|
|
164
230
|
const updatedAt = typeof sessionCandidate.updated_at === 'string' && sessionCandidate.updated_at.trim().length > 0
|
|
165
231
|
? sessionCandidate.updated_at
|
|
@@ -167,15 +233,437 @@ export function normalizeSubagentTrackingState(input) {
|
|
|
167
233
|
sessions[sessionId] = {
|
|
168
234
|
session_id: sessionId,
|
|
169
235
|
leader_thread_id: leaderThreadId,
|
|
236
|
+
...(leaderAttestedAt ? { leader_attested_at: leaderAttestedAt } : {}),
|
|
237
|
+
...(leaderAttestSource ? { leader_attest_source: leaderAttestSource } : {}),
|
|
170
238
|
updated_at: updatedAt,
|
|
171
239
|
threads,
|
|
172
240
|
};
|
|
173
241
|
}
|
|
242
|
+
const pendingRoleIntents = Array.isArray(parsed.pending_role_intents)
|
|
243
|
+
? parsed.pending_role_intents.map((intent) => normalizePendingRoleIntent(intent)).filter((intent) => intent !== null)
|
|
244
|
+
: [];
|
|
174
245
|
return {
|
|
175
246
|
schemaVersion: SUBAGENT_TRACKING_SCHEMA_VERSION,
|
|
176
247
|
sessions,
|
|
248
|
+
pending_role_intents: pendingRoleIntents,
|
|
177
249
|
};
|
|
178
250
|
}
|
|
251
|
+
function atomicTrackingTempPath(path) {
|
|
252
|
+
return `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
253
|
+
}
|
|
254
|
+
export const DEFAULT_CROSS_PROCESS_LOCK_MAX_ATTEMPTS = 80;
|
|
255
|
+
export const DEFAULT_CROSS_PROCESS_LOCK_RETRY_MS = 2;
|
|
256
|
+
export const CROSS_PROCESS_LOCK_LEASE_MS = 60_000;
|
|
257
|
+
const crossProcessLockWaitArray = new Int32Array(new SharedArrayBuffer(4));
|
|
258
|
+
export class CrossProcessLockLostError extends Error {
|
|
259
|
+
constructor(lockPath) {
|
|
260
|
+
super(`Lost cross-process lock ownership at ${lockPath}`);
|
|
261
|
+
this.name = 'CrossProcessLockLostError';
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
export function crossProcessLockPath(resourcePath) {
|
|
265
|
+
return `${resourcePath}.lock`;
|
|
266
|
+
}
|
|
267
|
+
function sleepForCrossProcessLockSync(durationMs) {
|
|
268
|
+
Atomics.wait(crossProcessLockWaitArray, 0, 0, durationMs);
|
|
269
|
+
}
|
|
270
|
+
export function readProcessStartIdentity(pid) {
|
|
271
|
+
if (process.platform !== 'linux')
|
|
272
|
+
return undefined;
|
|
273
|
+
try {
|
|
274
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf-8');
|
|
275
|
+
const closingParenthesis = stat.lastIndexOf(')');
|
|
276
|
+
const fields = stat.slice(closingParenthesis + 1).trim().split(/\s+/);
|
|
277
|
+
const starttime = fields[19];
|
|
278
|
+
const bootId = readFileSync('/proc/sys/kernel/random/boot_id', 'utf-8').trim();
|
|
279
|
+
if (closingParenthesis < 0 || !starttime || !/^\d+$/.test(starttime) || !bootId)
|
|
280
|
+
return undefined;
|
|
281
|
+
return `${bootId}:${starttime}`;
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function createCrossProcessLockClaim(token) {
|
|
288
|
+
const pidStartId = readProcessStartIdentity(process.pid);
|
|
289
|
+
return {
|
|
290
|
+
token,
|
|
291
|
+
pid: process.pid,
|
|
292
|
+
host: hostname(),
|
|
293
|
+
acquiredAtMs: Date.now(),
|
|
294
|
+
...(pidStartId ? { pidStartId } : {}),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function serializeCrossProcessLockClaim(claim) {
|
|
298
|
+
return `${JSON.stringify({
|
|
299
|
+
token: claim.token,
|
|
300
|
+
pid: claim.pid,
|
|
301
|
+
host: claim.host,
|
|
302
|
+
acquired_at: new Date(claim.acquiredAtMs).toISOString(),
|
|
303
|
+
...(claim.pidStartId ? { pid_start_id: claim.pidStartId } : {}),
|
|
304
|
+
})}\n`;
|
|
305
|
+
}
|
|
306
|
+
function readCrossProcessLockState(lockPath) {
|
|
307
|
+
try {
|
|
308
|
+
const parsed = JSON.parse(readFileSync(lockPath, 'utf-8'));
|
|
309
|
+
const token = typeof parsed.token === 'string' && parsed.token === parsed.token.trim() && parsed.token ? parsed.token : undefined;
|
|
310
|
+
const pid = typeof parsed.pid === 'number' && Number.isSafeInteger(parsed.pid) && parsed.pid > 0 ? parsed.pid : undefined;
|
|
311
|
+
const host = typeof parsed.host === 'string' && parsed.host.trim() ? parsed.host : undefined;
|
|
312
|
+
const acquiredAtMs = typeof parsed.acquired_at === 'string' ? Date.parse(parsed.acquired_at) : Number.NaN;
|
|
313
|
+
const pidStartId = parsed.pid_start_id === undefined
|
|
314
|
+
? undefined
|
|
315
|
+
: typeof parsed.pid_start_id === 'string' && parsed.pid_start_id.trim()
|
|
316
|
+
? parsed.pid_start_id
|
|
317
|
+
: null;
|
|
318
|
+
if (!token || !pid || !host || !Number.isFinite(acquiredAtMs) || pidStartId === null)
|
|
319
|
+
return { kind: 'malformed' };
|
|
320
|
+
return {
|
|
321
|
+
kind: 'claim',
|
|
322
|
+
claim: { token, pid, host, acquiredAtMs, ...(pidStartId ? { pidStartId } : {}) },
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
if (error.code === 'ENOENT')
|
|
327
|
+
return { kind: 'missing' };
|
|
328
|
+
return { kind: 'malformed' };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function isCrossProcessLockOwnerDead(pid) {
|
|
332
|
+
try {
|
|
333
|
+
process.kill(pid, 0);
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
return error.code === 'ESRCH';
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function isCrossProcessLockOlderThanLease(acquiredAtMs, nowMs) {
|
|
341
|
+
return acquiredAtMs < nowMs - CROSS_PROCESS_LOCK_LEASE_MS;
|
|
342
|
+
}
|
|
343
|
+
function isCrossProcessLockReclaimable(claim) {
|
|
344
|
+
if (claim.host !== hostname())
|
|
345
|
+
return isCrossProcessLockOlderThanLease(claim.acquiredAtMs, Date.now());
|
|
346
|
+
if (isCrossProcessLockOwnerDead(claim.pid))
|
|
347
|
+
return true;
|
|
348
|
+
const currentPidStartId = readProcessStartIdentity(claim.pid);
|
|
349
|
+
if (claim.pidStartId && currentPidStartId)
|
|
350
|
+
return currentPidStartId !== claim.pidStartId;
|
|
351
|
+
return isCrossProcessLockOlderThanLease(claim.acquiredAtMs, Date.now());
|
|
352
|
+
}
|
|
353
|
+
function removeCrossProcessLockFile(path) {
|
|
354
|
+
try {
|
|
355
|
+
unlinkSync(path);
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
if (error.code !== 'ENOENT')
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function tryAcquireCrossProcessFileLock(lockPath, token) {
|
|
363
|
+
const temporaryPath = `${lockPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
364
|
+
let acquired = false;
|
|
365
|
+
try {
|
|
366
|
+
writeFileSync(temporaryPath, serializeCrossProcessLockClaim(createCrossProcessLockClaim(token)));
|
|
367
|
+
try {
|
|
368
|
+
linkSync(temporaryPath, lockPath);
|
|
369
|
+
acquired = true;
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
if (error.code === 'EEXIST')
|
|
374
|
+
return false;
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
try {
|
|
380
|
+
removeCrossProcessLockFile(temporaryPath);
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
if (!acquired)
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function restoreQuarantinedCrossProcessLock(lockPath, quarantinedPath) {
|
|
389
|
+
try {
|
|
390
|
+
linkSync(quarantinedPath, lockPath);
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
const code = error.code;
|
|
394
|
+
// EEXIST: a replacement claim already holds the lock; the displaced copy is redundant.
|
|
395
|
+
// ENOENT: the displaced artifact was already removed (e.g. by a concurrent bounded
|
|
396
|
+
// sweep) — a fenced clean terminal outcome, never a cleanup failure to throw on.
|
|
397
|
+
if (code === 'EEXIST') {
|
|
398
|
+
removeCrossProcessLockFile(quarantinedPath);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (code === 'ENOENT')
|
|
402
|
+
return;
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
removeCrossProcessLockFile(quarantinedPath);
|
|
406
|
+
}
|
|
407
|
+
function recoverCrossProcessFileLock(lockPath, observed) {
|
|
408
|
+
if (observed.kind === 'missing')
|
|
409
|
+
return true;
|
|
410
|
+
if (observed.kind === 'claim' && !isCrossProcessLockReclaimable(observed.claim))
|
|
411
|
+
return false;
|
|
412
|
+
const quarantinedPath = `${lockPath}.${Date.now()}.${randomUUID()}.quarantine`;
|
|
413
|
+
try {
|
|
414
|
+
renameSync(lockPath, quarantinedPath);
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
if (error.code === 'ENOENT')
|
|
418
|
+
return true;
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
const barrier = crossProcessQuarantineBarrier;
|
|
422
|
+
crossProcessQuarantineBarrier = null;
|
|
423
|
+
barrier?.(lockPath, quarantinedPath);
|
|
424
|
+
const captured = readCrossProcessLockState(quarantinedPath);
|
|
425
|
+
const capturedExpectedClaim = observed.kind === 'claim'
|
|
426
|
+
&& captured.kind === 'claim'
|
|
427
|
+
&& captured.claim.token === observed.claim.token;
|
|
428
|
+
const capturedRecoverable = captured.kind === 'malformed'
|
|
429
|
+
|| (capturedExpectedClaim && captured.kind === 'claim' && isCrossProcessLockReclaimable(captured.claim));
|
|
430
|
+
if (!capturedRecoverable) {
|
|
431
|
+
restoreQuarantinedCrossProcessLock(lockPath, quarantinedPath);
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
removeCrossProcessLockFile(quarantinedPath);
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
function assertCrossProcessFileLockOwnership(lockPath, token) {
|
|
438
|
+
const state = readCrossProcessLockState(lockPath);
|
|
439
|
+
if (state.kind === 'claim' && state.claim.token === token)
|
|
440
|
+
return;
|
|
441
|
+
throw new CrossProcessLockLostError(lockPath);
|
|
442
|
+
}
|
|
443
|
+
function releaseCrossProcessFileLock(lockPath, token) {
|
|
444
|
+
const observed = readCrossProcessLockState(lockPath);
|
|
445
|
+
if (observed.kind !== 'claim' || observed.claim.token !== token)
|
|
446
|
+
return;
|
|
447
|
+
const quarantinedPath = `${lockPath}.${Date.now()}.${randomUUID()}.release`;
|
|
448
|
+
try {
|
|
449
|
+
renameSync(lockPath, quarantinedPath);
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
if (error.code === 'ENOENT')
|
|
453
|
+
return;
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
const barrier = crossProcessQuarantineBarrier;
|
|
457
|
+
crossProcessQuarantineBarrier = null;
|
|
458
|
+
barrier?.(lockPath, quarantinedPath);
|
|
459
|
+
const captured = readCrossProcessLockState(quarantinedPath);
|
|
460
|
+
if (captured.kind === 'claim' && captured.claim.token === token) {
|
|
461
|
+
removeCrossProcessLockFile(quarantinedPath);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
restoreQuarantinedCrossProcessLock(lockPath, quarantinedPath);
|
|
465
|
+
}
|
|
466
|
+
function crossProcessLockStagePath(resourcePath, token) {
|
|
467
|
+
return `${resourcePath}.stage.${token}`;
|
|
468
|
+
}
|
|
469
|
+
function createCrossProcessLockStage(stagePath) {
|
|
470
|
+
const descriptor = openSync(stagePath, 'wx');
|
|
471
|
+
closeSync(descriptor);
|
|
472
|
+
}
|
|
473
|
+
function sweepForeignCrossProcessLockStages(resourcePath, token) {
|
|
474
|
+
const directory = dirname(resourcePath);
|
|
475
|
+
const stagePrefix = `${basename(resourcePath)}.stage.`;
|
|
476
|
+
for (const entry of readdirSync(directory)) {
|
|
477
|
+
if (!entry.startsWith(stagePrefix) || entry.slice(stagePrefix.length) === token)
|
|
478
|
+
continue;
|
|
479
|
+
removeCrossProcessLockFile(join(directory, entry));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
export const CROSS_PROCESS_LOCK_ARTIFACT_SWEEP_CAP = 64;
|
|
483
|
+
function sweepAbandonedCrossProcessLockArtifacts(resourcePath) {
|
|
484
|
+
const directory = dirname(resourcePath);
|
|
485
|
+
const lockName = basename(crossProcessLockPath(resourcePath));
|
|
486
|
+
const escapedLockName = lockName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
487
|
+
const artifactPattern = new RegExp(`^${escapedLockName}\\.([^.]+)\\.[^.]+\\.(?:quarantine|release)$`);
|
|
488
|
+
const nowMs = Date.now();
|
|
489
|
+
const aged = [];
|
|
490
|
+
for (const entry of readdirSync(directory)) {
|
|
491
|
+
const match = artifactPattern.exec(entry);
|
|
492
|
+
if (!match)
|
|
493
|
+
continue;
|
|
494
|
+
const displacedAtMs = Number(match[1]);
|
|
495
|
+
// Only lease-aged, parseable-timestamp artifacts are eligible; fresh/live/malformed-ts
|
|
496
|
+
// artifacts are always preserved (never delete a live successor's in-flight evidence).
|
|
497
|
+
if (!Number.isFinite(displacedAtMs) || nowMs - displacedAtMs <= CROSS_PROCESS_LOCK_LEASE_MS)
|
|
498
|
+
continue;
|
|
499
|
+
aged.push({ displacedAtMs, entry });
|
|
500
|
+
}
|
|
501
|
+
// Bounded, deterministic cleanup: process oldest-first (tie-break by name) and remove at
|
|
502
|
+
// most CROSS_PROCESS_LOCK_ARTIFACT_SWEEP_CAP per acquisition, so a large backlog drains
|
|
503
|
+
// predictably across acquisitions instead of doing unbounded work in a single lock take.
|
|
504
|
+
aged.sort((a, b) => a.displacedAtMs - b.displacedAtMs || (a.entry < b.entry ? -1 : a.entry > b.entry ? 1 : 0));
|
|
505
|
+
for (const { entry } of aged.slice(0, CROSS_PROCESS_LOCK_ARTIFACT_SWEEP_CAP)) {
|
|
506
|
+
removeCrossProcessLockFile(join(directory, entry));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
let crossProcessPublishBarrier = null;
|
|
510
|
+
let crossProcessQuarantineBarrier = null;
|
|
511
|
+
export function __setCrossProcessPublishBarrierForTest(barrier) {
|
|
512
|
+
crossProcessPublishBarrier = barrier;
|
|
513
|
+
}
|
|
514
|
+
export function __setCrossProcessQuarantineBarrierForTest(barrier) {
|
|
515
|
+
crossProcessQuarantineBarrier = barrier;
|
|
516
|
+
}
|
|
517
|
+
export function withCrossProcessFileLockSync(resourcePath, operation, options = {}) {
|
|
518
|
+
const lockPath = crossProcessLockPath(resourcePath);
|
|
519
|
+
const maxAttempts = typeof options.maxAttempts === 'number' && Number.isFinite(options.maxAttempts)
|
|
520
|
+
? Math.max(1, Math.floor(options.maxAttempts))
|
|
521
|
+
: DEFAULT_CROSS_PROCESS_LOCK_MAX_ATTEMPTS;
|
|
522
|
+
const retryMs = typeof options.retryMs === 'number' && Number.isFinite(options.retryMs)
|
|
523
|
+
? Math.max(1, Math.floor(options.retryMs))
|
|
524
|
+
: DEFAULT_CROSS_PROCESS_LOCK_RETRY_MS;
|
|
525
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
526
|
+
const token = randomUUID();
|
|
527
|
+
const stagePath = crossProcessLockStagePath(resourcePath, token);
|
|
528
|
+
let acquired = false;
|
|
529
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
530
|
+
if (tryAcquireCrossProcessFileLock(lockPath, token)) {
|
|
531
|
+
acquired = true;
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
const recovered = recoverCrossProcessFileLock(lockPath, readCrossProcessLockState(lockPath));
|
|
535
|
+
if (recovered && tryAcquireCrossProcessFileLock(lockPath, token)) {
|
|
536
|
+
acquired = true;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
if (attempt === maxAttempts - 1) {
|
|
540
|
+
throw new Error(`Timed out waiting for cross-process lock at ${lockPath}`);
|
|
541
|
+
}
|
|
542
|
+
sleepForCrossProcessLockSync(Math.min(25, retryMs * 2 ** Math.min(attempt, 4)));
|
|
543
|
+
}
|
|
544
|
+
if (!acquired) {
|
|
545
|
+
throw new Error(`Timed out waiting for cross-process lock at ${lockPath}`);
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
sweepForeignCrossProcessLockStages(resourcePath, token);
|
|
549
|
+
sweepAbandonedCrossProcessLockArtifacts(resourcePath);
|
|
550
|
+
createCrossProcessLockStage(stagePath);
|
|
551
|
+
return operation({
|
|
552
|
+
assertOwnership: () => assertCrossProcessFileLockOwnership(lockPath, token),
|
|
553
|
+
publish: (contents) => {
|
|
554
|
+
let descriptor;
|
|
555
|
+
try {
|
|
556
|
+
descriptor = openSync(stagePath, 'r+');
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
if (error.code === 'ENOENT')
|
|
560
|
+
throw new CrossProcessLockLostError(lockPath);
|
|
561
|
+
throw error;
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
ftruncateSync(descriptor, 0);
|
|
565
|
+
writeSync(descriptor, contents);
|
|
566
|
+
}
|
|
567
|
+
finally {
|
|
568
|
+
closeSync(descriptor);
|
|
569
|
+
}
|
|
570
|
+
const barrier = crossProcessPublishBarrier;
|
|
571
|
+
crossProcessPublishBarrier = null;
|
|
572
|
+
barrier?.();
|
|
573
|
+
try {
|
|
574
|
+
renameSync(stagePath, resourcePath);
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
if (error.code === 'ENOENT')
|
|
578
|
+
throw new CrossProcessLockLostError(lockPath);
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
createCrossProcessLockStage(stagePath);
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
removeCrossProcessLockFile(stagePath);
|
|
587
|
+
releaseCrossProcessFileLock(lockPath, token);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function readSubagentTrackingStateSync(cwd) {
|
|
591
|
+
const path = subagentTrackingPath(cwd);
|
|
592
|
+
if (!existsSync(path))
|
|
593
|
+
return createSubagentTrackingState();
|
|
594
|
+
try {
|
|
595
|
+
return normalizeSubagentTrackingState(JSON.parse(readFileSync(path, 'utf-8')));
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
return createSubagentTrackingState();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// #3181: strict readers for the leader-attestation security decision. A missing file is a
|
|
602
|
+
// legitimate empty state (fresh turn), but an existing file that fails to read/parse must
|
|
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
|
+
}
|
|
624
|
+
}
|
|
625
|
+
export async function readSubagentTrackingStateStrict(cwd) {
|
|
626
|
+
const path = subagentTrackingPath(cwd);
|
|
627
|
+
let raw;
|
|
628
|
+
try {
|
|
629
|
+
raw = await readFile(path, 'utf-8');
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
if (error?.code === 'ENOENT')
|
|
633
|
+
return { ok: true, state: createSubagentTrackingState() };
|
|
634
|
+
return { ok: false };
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
return { ok: true, state: normalizeSubagentTrackingState(JSON.parse(raw)) };
|
|
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;
|
|
652
|
+
}
|
|
653
|
+
function writeSubagentTrackingStateSync(cwd, state, publish) {
|
|
654
|
+
const normalized = normalizeSubagentTrackingState(state);
|
|
655
|
+
const path = subagentTrackingPath(cwd);
|
|
656
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
657
|
+
const contents = `${JSON.stringify(normalized, null, 2)}\n`;
|
|
658
|
+
if (publish) {
|
|
659
|
+
publish(contents);
|
|
660
|
+
return path;
|
|
661
|
+
}
|
|
662
|
+
const temporaryPath = atomicTrackingTempPath(path);
|
|
663
|
+
writeFileSync(temporaryPath, contents);
|
|
664
|
+
renameSync(temporaryPath, path);
|
|
665
|
+
return path;
|
|
666
|
+
}
|
|
179
667
|
export async function readSubagentTrackingState(cwd) {
|
|
180
668
|
const path = subagentTrackingPath(cwd);
|
|
181
669
|
if (!existsSync(path))
|
|
@@ -191,9 +679,525 @@ export async function writeSubagentTrackingState(cwd, state) {
|
|
|
191
679
|
const normalized = normalizeSubagentTrackingState(state);
|
|
192
680
|
const path = subagentTrackingPath(cwd);
|
|
193
681
|
await mkdir(dirname(path), { recursive: true });
|
|
194
|
-
|
|
682
|
+
const temporaryPath = atomicTrackingTempPath(path);
|
|
683
|
+
await writeFile(temporaryPath, `${JSON.stringify(normalized, null, 2)}\n`);
|
|
684
|
+
await rename(temporaryPath, path);
|
|
195
685
|
return path;
|
|
196
686
|
}
|
|
687
|
+
function normalizeNowMs(nowMs) {
|
|
688
|
+
return typeof nowMs === 'number' && Number.isFinite(nowMs) ? nowMs : Date.now();
|
|
689
|
+
}
|
|
690
|
+
function isExpiredPendingRoleIntent(intent, nowMs) {
|
|
691
|
+
if (intent.binding_state === 'bound')
|
|
692
|
+
return false;
|
|
693
|
+
const expiresAtMs = Date.parse(intent.expires_at);
|
|
694
|
+
return !Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs;
|
|
695
|
+
}
|
|
696
|
+
function pendingRoleIntentPredicates(cwd, canonicalOrigin, nowMs) {
|
|
697
|
+
const isCwdPartitionedStateRoot = getBaseStateDirWithSource(cwd).rootSource === 'cwd-default';
|
|
698
|
+
const isOwn = (intent) => (intent.origin_cwd
|
|
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);
|
|
1054
|
+
}
|
|
1055
|
+
return null;
|
|
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);
|
|
1072
|
+
}
|
|
1073
|
+
return {
|
|
1074
|
+
...adaptedIntent,
|
|
1075
|
+
claimantToken: undefined,
|
|
1076
|
+
alreadyBound: true,
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
const claimantToken = randomUUID();
|
|
1080
|
+
const boundState = bind(state, adaptedIntent);
|
|
1081
|
+
boundState.pending_role_intents = all
|
|
1082
|
+
.filter((intent) => !shouldPruneExpired(intent))
|
|
1083
|
+
.filter((intent) => (intent === matchedIntent
|
|
1084
|
+
|| !(isOwn(intent) && sameLogicalRoleIntent(intent, sessionId, parentThreadId, correlationToken))))
|
|
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,
|
|
1092
|
+
}
|
|
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
|
+
}
|
|
1129
|
+
state.pending_role_intents = all
|
|
1130
|
+
.filter((intent) => !shouldPruneExpired(intent))
|
|
1131
|
+
.filter((intent) => (intent.binding_state === 'bound'
|
|
1132
|
+
|| !(isOwn(intent) && sameLogicalRoleIntent(intent, sessionId, parentThreadId, correlationToken))));
|
|
1133
|
+
context.assertOwnership();
|
|
1134
|
+
writeSubagentTrackingStateSync(cwd, state, context.publish);
|
|
1135
|
+
return { role: consumed.role, provenanceKind: OMX_ADAPTED_PROVENANCE };
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
export function completeAdaptedRoleBinding(cwd, input) {
|
|
1139
|
+
const nowMs = normalizeNowMs(input.nowMs);
|
|
1140
|
+
const sessionId = input.sessionId.trim();
|
|
1141
|
+
const parentThreadId = input.parentThreadId.trim();
|
|
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';
|
|
1164
|
+
}
|
|
1165
|
+
const hasClaimant = Object.hasOwn(boundIntent, 'binding_claimant_token');
|
|
1166
|
+
if (!isCanonicalCorrelationToken(boundIntent.correlation_token)
|
|
1167
|
+
|| !isCanonicalCorrelationToken(correlationToken)
|
|
1168
|
+
|| boundIntent.correlation_token !== correlationToken
|
|
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] : [];
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
197
1201
|
export function recordSubagentTurn(state, input) {
|
|
198
1202
|
const sessionId = input.sessionId.trim();
|
|
199
1203
|
const threadId = input.threadId.trim();
|
|
@@ -209,42 +1213,29 @@ export function recordSubagentTurn(state, input) {
|
|
|
209
1213
|
const requestedKind = input.kind === 'leader' || input.kind === 'subagent' ? input.kind : undefined;
|
|
210
1214
|
const requestedLeaderThreadId = input.leaderThreadId?.trim();
|
|
211
1215
|
const existingThread = existingSession.threads[threadId];
|
|
212
|
-
const existingKind = existingThread?.kind === 'leader' || existingThread?.kind === 'subagent'
|
|
213
|
-
? existingThread.kind
|
|
214
|
-
: undefined;
|
|
1216
|
+
const existingKind = existingThread?.kind === 'leader' || existingThread?.kind === 'subagent' ? existingThread.kind : undefined;
|
|
215
1217
|
const existingLeaderThreadId = existingSession.leader_thread_id?.trim();
|
|
216
1218
|
// `leader_thread_id` is the session's top-level leader boundary. A native
|
|
217
1219
|
// subagent can itself be the immediate parent of a nested native role, but
|
|
218
1220
|
// that must not reclassify known subagent evidence as the session leader.
|
|
219
|
-
const requestedLeaderThread = requestedLeaderThreadId
|
|
220
|
-
? existingSession.threads[requestedLeaderThreadId]
|
|
221
|
-
: undefined;
|
|
1221
|
+
const requestedLeaderThread = requestedLeaderThreadId ? existingSession.threads[requestedLeaderThreadId] : undefined;
|
|
222
1222
|
const requestedLeaderWouldReclassifySubagent = requestedLeaderThread?.kind === 'subagent';
|
|
223
|
-
const requestedSessionLeaderThreadId = requestedLeaderWouldReclassifySubagent
|
|
224
|
-
? undefined
|
|
225
|
-
: requestedLeaderThreadId;
|
|
1223
|
+
const requestedSessionLeaderThreadId = requestedLeaderWouldReclassifySubagent ? undefined : requestedLeaderThreadId;
|
|
226
1224
|
const preserveExistingSubagent = existingKind === 'subagent' && requestedKind !== 'subagent';
|
|
227
|
-
const preserveKnownLeader = requestedKind === 'subagent'
|
|
228
|
-
&& (existingKind === 'leader' || existingLeaderThreadId === threadId);
|
|
1225
|
+
const preserveKnownLeader = requestedKind === 'subagent' && (existingKind === 'leader' || existingLeaderThreadId === threadId);
|
|
229
1226
|
const leaderThreadId = preserveKnownLeader
|
|
230
1227
|
? existingLeaderThreadId || threadId
|
|
231
|
-
: existingLeaderThreadId
|
|
232
|
-
|| requestedSessionLeaderThreadId
|
|
233
|
-
|| (requestedKind === 'subagent' || preserveExistingSubagent ? undefined : threadId);
|
|
1228
|
+
: existingLeaderThreadId || requestedSessionLeaderThreadId || (requestedKind === 'subagent' || preserveExistingSubagent ? undefined : threadId);
|
|
234
1229
|
const kind = preserveKnownLeader
|
|
235
1230
|
? 'leader'
|
|
236
1231
|
: requestedKind === 'leader' && existingKind === 'subagent'
|
|
237
1232
|
? 'subagent'
|
|
238
|
-
: requestedKind ?? (threadId === leaderThreadId ? 'leader' : existingKind ?? 'subagent');
|
|
1233
|
+
: (requestedKind ?? (threadId === leaderThreadId ? 'leader' : (existingKind ?? 'subagent')));
|
|
239
1234
|
const requestedStatus = normalizeSubagentStatus(input.status);
|
|
240
1235
|
const preservedStatus = normalizeSubagentStatus(existingThread?.status);
|
|
241
1236
|
const preserveCompletionEvidence = input.preserveCompletionEvidence === true;
|
|
242
|
-
const clearsPriorCompletion = input.completed !== true
|
|
243
|
-
|
|
244
|
-
&& Boolean(existingThread?.completed_at);
|
|
245
|
-
const status = requestedStatus
|
|
246
|
-
?? (input.completed ? 'closed' : undefined)
|
|
247
|
-
?? (clearsPriorCompletion ? undefined : preservedStatus);
|
|
1237
|
+
const clearsPriorCompletion = input.completed !== true && preserveCompletionEvidence !== true && Boolean(existingThread?.completed_at);
|
|
1238
|
+
const status = requestedStatus ?? (input.completed ? 'closed' : undefined) ?? (clearsPriorCompletion ? undefined : preservedStatus);
|
|
248
1239
|
const preservedCompletion = preserveCompletionEvidence && existingThread?.completed_at
|
|
249
1240
|
? {
|
|
250
1241
|
completed_at: existingThread.completed_at,
|
|
@@ -258,7 +1249,11 @@ export function recordSubagentTurn(state, input) {
|
|
|
258
1249
|
first_seen_at: existingThread?.first_seen_at ?? timestamp,
|
|
259
1250
|
last_seen_at: timestamp,
|
|
260
1251
|
turn_count: (existingThread?.turn_count ?? 0) + 1,
|
|
261
|
-
...(input.turnId?.trim()
|
|
1252
|
+
...(input.turnId?.trim()
|
|
1253
|
+
? { last_turn_id: input.turnId.trim() }
|
|
1254
|
+
: existingThread?.last_turn_id
|
|
1255
|
+
? { last_turn_id: existingThread.last_turn_id }
|
|
1256
|
+
: {}),
|
|
262
1257
|
...(input.completed
|
|
263
1258
|
? {
|
|
264
1259
|
completed_at: timestamp,
|
|
@@ -268,27 +1263,44 @@ export function recordSubagentTurn(state, input) {
|
|
|
268
1263
|
: preservedCompletion),
|
|
269
1264
|
...(input.mode?.trim() ? { mode: input.mode.trim() } : existingThread?.mode ? { mode: existingThread.mode } : {}),
|
|
270
1265
|
...(input.role?.trim() ? { role: input.role.trim() } : existingThread?.role ? { role: existingThread.role } : {}),
|
|
1266
|
+
...(input.provenanceKind?.trim()
|
|
1267
|
+
? { provenance_kind: input.provenanceKind.trim() }
|
|
1268
|
+
: existingThread?.provenance_kind
|
|
1269
|
+
? { provenance_kind: existingThread.provenance_kind }
|
|
1270
|
+
: {}),
|
|
271
1271
|
...(input.laneId?.trim() ? { lane_id: input.laneId.trim() } : existingThread?.lane_id ? { lane_id: existingThread.lane_id } : {}),
|
|
272
1272
|
...(input.scope?.trim() ? { scope: input.scope.trim() } : existingThread?.scope ? { scope: existingThread.scope } : {}),
|
|
273
1273
|
...(input.agentNickname?.trim()
|
|
274
1274
|
? { agent_nickname: input.agentNickname.trim() }
|
|
275
|
-
: existingThread?.agent_nickname
|
|
1275
|
+
: existingThread?.agent_nickname
|
|
1276
|
+
? { agent_nickname: existingThread.agent_nickname }
|
|
1277
|
+
: {}),
|
|
276
1278
|
...(status ? { status } : {}),
|
|
277
1279
|
...(input.lastHandoffSummary?.trim()
|
|
278
1280
|
? { last_handoff_summary: input.lastHandoffSummary.trim() }
|
|
279
|
-
: existingThread?.last_handoff_summary
|
|
1281
|
+
: existingThread?.last_handoff_summary
|
|
1282
|
+
? { last_handoff_summary: existingThread.last_handoff_summary }
|
|
1283
|
+
: {}),
|
|
280
1284
|
...(input.resumeRequestedAt?.trim()
|
|
281
1285
|
? { resume_requested_at: input.resumeRequestedAt.trim() }
|
|
282
|
-
: existingThread?.resume_requested_at
|
|
1286
|
+
: existingThread?.resume_requested_at
|
|
1287
|
+
? { resume_requested_at: existingThread.resume_requested_at }
|
|
1288
|
+
: {}),
|
|
283
1289
|
...(input.resumeCompletedAt?.trim()
|
|
284
1290
|
? { resume_completed_at: input.resumeCompletedAt.trim() }
|
|
285
|
-
: existingThread?.resume_completed_at
|
|
1291
|
+
: existingThread?.resume_completed_at
|
|
1292
|
+
? { resume_completed_at: existingThread.resume_completed_at }
|
|
1293
|
+
: {}),
|
|
286
1294
|
...(input.resumeFailedAt?.trim()
|
|
287
1295
|
? { resume_failed_at: input.resumeFailedAt.trim() }
|
|
288
|
-
: existingThread?.resume_failed_at
|
|
1296
|
+
: existingThread?.resume_failed_at
|
|
1297
|
+
? { resume_failed_at: existingThread.resume_failed_at }
|
|
1298
|
+
: {}),
|
|
289
1299
|
...(input.resumeFailureReason?.trim()
|
|
290
1300
|
? { resume_failure_reason: input.resumeFailureReason.trim() }
|
|
291
|
-
: existingThread?.resume_failure_reason
|
|
1301
|
+
: existingThread?.resume_failure_reason
|
|
1302
|
+
? { resume_failure_reason: existingThread.resume_failure_reason }
|
|
1303
|
+
: {}),
|
|
292
1304
|
};
|
|
293
1305
|
const threads = {
|
|
294
1306
|
...existingSession.threads,
|
|
@@ -303,16 +1315,24 @@ export function recordSubagentTurn(state, input) {
|
|
|
303
1315
|
normalized.sessions[sessionId] = {
|
|
304
1316
|
session_id: sessionId,
|
|
305
1317
|
...(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 } : {}),
|
|
306
1323
|
updated_at: timestamp,
|
|
307
1324
|
threads,
|
|
308
1325
|
};
|
|
309
1326
|
return normalized;
|
|
310
1327
|
}
|
|
311
1328
|
export async function recordSubagentTurnForSession(cwd, input) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
1329
|
+
return withCrossProcessFileLockSync(subagentTrackingPath(cwd), (context) => {
|
|
1330
|
+
const current = readSubagentTrackingStateSync(cwd);
|
|
1331
|
+
const next = recordSubagentTurn(current, input);
|
|
1332
|
+
context.assertOwnership();
|
|
1333
|
+
writeSubagentTrackingStateSync(cwd, next, context.publish);
|
|
1334
|
+
return next;
|
|
1335
|
+
});
|
|
316
1336
|
}
|
|
317
1337
|
export function summarizeSubagentSession(state, sessionId, options = {}) {
|
|
318
1338
|
const normalized = normalizeSubagentTrackingState(state);
|
|
@@ -320,11 +1340,7 @@ export function summarizeSubagentSession(state, sessionId, options = {}) {
|
|
|
320
1340
|
if (!session)
|
|
321
1341
|
return null;
|
|
322
1342
|
const activeWindowMs = options.activeWindowMs ?? DEFAULT_SUBAGENT_ACTIVE_WINDOW_MS;
|
|
323
|
-
const nowMs = typeof options.now === 'string'
|
|
324
|
-
? Date.parse(options.now)
|
|
325
|
-
: options.now instanceof Date
|
|
326
|
-
? options.now.getTime()
|
|
327
|
-
: Date.now();
|
|
1343
|
+
const nowMs = typeof options.now === 'string' ? Date.parse(options.now) : options.now instanceof Date ? options.now.getTime() : Date.now();
|
|
328
1344
|
const allThreadIds = Object.keys(session.threads).sort();
|
|
329
1345
|
const allSubagentThreadIds = allThreadIds.filter((threadId) => isTrustedSubagentThread(session, threadId));
|
|
330
1346
|
const activeSubagentThreadIds = allSubagentThreadIds.filter((threadId) => {
|