@ponythewhite/base-context 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/NOTICE +0 -5
- package/README.md +44 -27
- package/dist/NOTICE +0 -5
- package/dist/base-context-runtime/pyproject.toml +1 -1
- package/dist/base-context-runtime/src/rlm/__init__.py +7 -5
- package/dist/base-context-runtime/test/test_subagent_registry.py +28 -1
- package/dist/base-context-runtime/uv.lock +1 -1
- package/dist/build-info.json +1 -1
- package/dist/bundle/amazon-bedrock.js +21 -0
- package/dist/bundle/{anthropic-YY4S534V.js → anthropic-UV5GZDGU.js} +5 -5
- package/dist/bundle/{azure-openai-responses-JRP6MIM3.js → azure-openai-responses-WSNPXLYC.js} +6 -6
- package/dist/bundle/{bundled-modules-P5HARVZA.js → bundled-modules-TXUMJ2XQ.js} +71 -16
- package/dist/bundle/chunk-27QCJ2AZ.js +2578 -0
- package/dist/bundle/{chunk-MZHTNNVA.js → chunk-4GXPNXOJ.js} +8 -0
- package/dist/bundle/{chunk-SC5KG5R4.js → chunk-DGNX7HQ3.js} +1 -7
- package/dist/bundle/{chunk-A53O363M.js → chunk-GRJFXLBM.js} +0 -1
- package/dist/bundle/chunk-HKINIBP4.js +29 -0
- package/dist/bundle/{chunk-UDSW6FNG.js → chunk-KTNETTHI.js} +1 -1
- package/dist/bundle/{chunk-BYVP7LWV.js → chunk-S3INFPZZ.js} +1861 -4662
- package/dist/bundle/{chunk-73DCPU3D.js → chunk-V3L54UD6.js} +1019 -3427
- package/dist/bundle/{chunk-U7AUVG3B.js → chunk-VSIYFWYI.js} +24 -6
- package/dist/bundle/{chunk-IW77OXFS.js → chunk-W46VZFI6.js} +0 -2099
- package/dist/bundle/{cli-main-O4X7GFGY.js → cli-main-BYMSRPH6.js} +8 -6
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/{google-AUTVGDGZ.js → google-T32FMD4G.js} +6 -6
- package/dist/bundle/{google-vertex-Q75D7RKS.js → google-vertex-56KP6I3B.js} +3 -3
- package/dist/bundle/{main-XKOW2BV6.js → main-ODOYCEQC.js} +7 -5
- package/dist/bundle/{mistral-7ESB7QUD.js → mistral-WGBMYIVU.js} +5 -5
- package/dist/bundle/{openai-codex-responses-LV3QRPN4.js → openai-codex-responses-PIV2RQ3G.js} +6 -6
- package/dist/bundle/{openai-completions-OJECLPXJ.js → openai-completions-64C3JPR4.js} +11 -22
- package/dist/bundle/{openai-responses-FN6HZMBC.js → openai-responses-UI7RSTDV.js} +6 -6
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +108 -59
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/daemon-ps.d.ts +4 -3
- package/dist/cli/daemon-ps.d.ts.map +1 -1
- package/dist/cli/daemon-ps.js +6 -1
- package/dist/cli/daemon-ps.js.map +1 -1
- package/dist/cli/product-doctor.d.ts +0 -1
- package/dist/cli/product-doctor.d.ts.map +1 -1
- package/dist/cli/product-doctor.js +6 -3
- package/dist/cli/product-doctor.js.map +1 -1
- package/dist/config.d.ts +0 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -8
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-config.d.ts +1 -2
- package/dist/core/agent-session-config.d.ts.map +1 -1
- package/dist/core/agent-session-config.js +0 -3
- package/dist/core/agent-session-config.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +5 -4
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +5 -3
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +2 -27
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +10 -6
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +107 -107
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +8 -35
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +33 -243
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/cron-jobs.d.ts.map +1 -1
- package/dist/core/cron-jobs.js +7 -1
- package/dist/core/cron-jobs.js.map +1 -1
- package/dist/core/inference-coordinator.d.ts +6 -1
- package/dist/core/inference-coordinator.d.ts.map +1 -1
- package/dist/core/inference-coordinator.js +18 -2
- package/dist/core/inference-coordinator.js.map +1 -1
- package/dist/core/kernel/bootstrap.d.ts.map +1 -1
- package/dist/core/kernel/bootstrap.js +2 -2
- package/dist/core/kernel/bootstrap.js.map +1 -1
- package/dist/core/kernel/repl-manager.d.ts +2 -0
- package/dist/core/kernel/repl-manager.d.ts.map +1 -1
- package/dist/core/kernel/repl-manager.js +44 -1
- package/dist/core/kernel/repl-manager.js.map +1 -1
- package/dist/core/logging.d.ts +4 -3
- package/dist/core/logging.d.ts.map +1 -1
- package/dist/core/logging.js +6 -6
- package/dist/core/logging.js.map +1 -1
- package/dist/core/mcp/mcp-manager.d.ts.map +1 -1
- package/dist/core/mcp/mcp-manager.js +3 -3
- package/dist/core/mcp/mcp-manager.js.map +1 -1
- package/dist/core/model-registry.d.ts +1 -16
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +88 -196
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +2 -19
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +36 -266
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/provider-contracts.d.ts +5 -6
- package/dist/core/provider-contracts.d.ts.map +1 -1
- package/dist/core/provider-contracts.js +25 -36
- package/dist/core/provider-contracts.js.map +1 -1
- package/dist/core/provider-display-names.d.ts.map +1 -1
- package/dist/core/provider-display-names.js +0 -2
- package/dist/core/provider-display-names.js.map +1 -1
- package/dist/core/rlm-max-subagents.d.ts +32 -0
- package/dist/core/rlm-max-subagents.d.ts.map +1 -0
- package/dist/core/rlm-max-subagents.js +50 -0
- package/dist/core/rlm-max-subagents.js.map +1 -0
- package/dist/core/rlm-runtime.d.ts +3 -3
- package/dist/core/rlm-runtime.d.ts.map +1 -1
- package/dist/core/rlm-runtime.js.map +1 -1
- package/dist/core/sdk.d.ts +4 -3
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +27 -14
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -16
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +12 -44
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +6 -6
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/installer.mjs +2 -3
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +39 -34
- package/dist/main.js.map +1 -1
- package/dist/modes/acp/acp-mode.d.ts.map +1 -1
- package/dist/modes/acp/acp-mode.js +1 -1
- package/dist/modes/acp/acp-mode.js.map +1 -1
- package/dist/modes/agent-connection/daemon-agent-connection.d.ts +6 -2
- package/dist/modes/agent-connection/daemon-agent-connection.d.ts.map +1 -1
- package/dist/modes/agent-connection/daemon-agent-connection.js +13 -2
- package/dist/modes/agent-connection/daemon-agent-connection.js.map +1 -1
- package/dist/modes/agent-connection/in-process-agent-connection.d.ts +2 -0
- package/dist/modes/agent-connection/in-process-agent-connection.d.ts.map +1 -1
- package/dist/modes/agent-connection/in-process-agent-connection.js +6 -0
- package/dist/modes/agent-connection/in-process-agent-connection.js.map +1 -1
- package/dist/modes/agent-connection/types.d.ts +3 -0
- package/dist/modes/agent-connection/types.d.ts.map +1 -1
- package/dist/modes/agent-connection/types.js.map +1 -1
- package/dist/modes/agents-view/agents-view-mode.d.ts.map +1 -1
- package/dist/modes/agents-view/agents-view-mode.js +2 -19
- package/dist/modes/agents-view/agents-view-mode.js.map +1 -1
- package/dist/modes/daemon/daemon-mode.d.ts +5 -0
- package/dist/modes/daemon/daemon-mode.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-mode.js +107 -12
- package/dist/modes/daemon/daemon-mode.js.map +1 -1
- package/dist/modes/daemon/daemon-protocol.d.ts +194 -165
- package/dist/modes/daemon/daemon-protocol.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-protocol.js +16 -11
- package/dist/modes/daemon/daemon-protocol.js.map +1 -1
- package/dist/modes/daemon/daemon-rlm-capacity-coordinator.d.ts +19 -0
- package/dist/modes/daemon/daemon-rlm-capacity-coordinator.d.ts.map +1 -0
- package/dist/modes/daemon/daemon-rlm-capacity-coordinator.js +74 -0
- package/dist/modes/daemon/daemon-rlm-capacity-coordinator.js.map +1 -0
- package/dist/modes/daemon/daemon-rlm-capacity.d.ts +19 -0
- package/dist/modes/daemon/daemon-rlm-capacity.d.ts.map +1 -0
- package/dist/modes/daemon/daemon-rlm-capacity.js +85 -0
- package/dist/modes/daemon/daemon-rlm-capacity.js.map +1 -0
- package/dist/modes/daemon/daemon-session-summarizer.d.ts +2 -3
- package/dist/modes/daemon/daemon-session-summarizer.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-session-summarizer.js +32 -23
- package/dist/modes/daemon/daemon-session-summarizer.js.map +1 -1
- package/dist/modes/daemon/daemon-state-root.d.ts +15 -0
- package/dist/modes/daemon/daemon-state-root.d.ts.map +1 -0
- package/dist/modes/daemon/daemon-state-root.js +40 -0
- package/dist/modes/daemon/daemon-state-root.js.map +1 -0
- package/dist/modes/daemon/daemon-supervisor-ownership.d.ts +2 -0
- package/dist/modes/daemon/daemon-supervisor-ownership.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-supervisor-ownership.js +27 -0
- package/dist/modes/daemon/daemon-supervisor-ownership.js.map +1 -1
- package/dist/modes/daemon/daemon-supervisor.d.ts +10 -1
- package/dist/modes/daemon/daemon-supervisor.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-supervisor.js +213 -20
- package/dist/modes/daemon/daemon-supervisor.js.map +1 -1
- package/dist/modes/daemon/daemon-worker-protocol.d.ts +28 -1
- package/dist/modes/daemon/daemon-worker-protocol.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-worker-protocol.js +16 -2
- package/dist/modes/daemon/daemon-worker-protocol.js.map +1 -1
- package/dist/modes/daemon/saved-session-catalog.d.ts.map +1 -1
- package/dist/modes/daemon/saved-session-catalog.js +2 -0
- package/dist/modes/daemon/saved-session-catalog.js.map +1 -1
- package/dist/modes/interactive/auth-flows.d.ts +0 -6
- package/dist/modes/interactive/auth-flows.d.ts.map +1 -1
- package/dist/modes/interactive/auth-flows.js +6 -126
- package/dist/modes/interactive/auth-flows.js.map +1 -1
- package/dist/modes/interactive/components/configuration-menu.d.ts +3 -0
- package/dist/modes/interactive/components/configuration-menu.d.ts.map +1 -1
- package/dist/modes/interactive/components/configuration-menu.js +13 -2
- package/dist/modes/interactive/components/configuration-menu.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +2 -2
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +0 -1
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +0 -1
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +0 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +3 -40
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +2 -0
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +13 -2
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +2 -11
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/feature-hints.d.ts.map +1 -1
- package/dist/modes/interactive/feature-hints.js +0 -4
- package/dist/modes/interactive/feature-hints.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -12
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +81 -507
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/onboarding.d.ts +0 -3
- package/dist/modes/interactive/onboarding.d.ts.map +1 -1
- package/dist/modes/interactive/onboarding.js +0 -17
- package/dist/modes/interactive/onboarding.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +2 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +24 -11
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/node/amazon-bedrock.d.ts +2 -0
- package/dist/node/amazon-bedrock.d.ts.map +1 -0
- package/dist/node/amazon-bedrock.js +6 -0
- package/dist/node/amazon-bedrock.js.map +1 -0
- package/dist/product-identity.d.ts +0 -1
- package/dist/product-identity.d.ts.map +1 -1
- package/dist/product-identity.js +0 -1
- package/dist/product-identity.js.map +1 -1
- package/docs/development.md +1 -1
- package/docs/fork-philosophy.md +4 -8
- package/docs/installation.md +83 -62
- package/docs/providers.md +23 -25
- package/docs/quickstart.md +19 -10
- package/docs/rlm-runtime.md +13 -5
- package/docs/rpc.md +4 -4
- package/docs/sdk.md +20 -11
- package/docs/sessions.md +0 -1
- package/docs/settings.md +9 -42
- package/docs/skills.md +0 -1
- package/docs/upstream-0.9.5.md +171 -0
- package/docs/usage.md +14 -10
- package/package.json +4 -4
- package/dist/core/agent-traces.d.ts +0 -149
- package/dist/core/agent-traces.d.ts.map +0 -1
- package/dist/core/agent-traces.js +0 -904
- package/dist/core/agent-traces.js.map +0 -1
- package/dist/core/prime-inference-auth.d.ts +0 -70
- package/dist/core/prime-inference-auth.d.ts.map +0 -1
- package/dist/core/prime-inference-auth.js +0 -352
- package/dist/core/prime-inference-auth.js.map +0 -1
- package/dist/core/prime-inference-model-selection.d.ts +0 -16
- package/dist/core/prime-inference-model-selection.d.ts.map +0 -1
- package/dist/core/prime-inference-model-selection.js +0 -16
- package/dist/core/prime-inference-model-selection.js.map +0 -1
- package/dist/core/prime-inference-models.d.ts +0 -6
- package/dist/core/prime-inference-models.d.ts.map +0 -1
- package/dist/core/prime-inference-models.js +0 -62
- package/dist/core/prime-inference-models.js.map +0 -1
- package/dist/core/telemetry.d.ts +0 -95
- package/dist/core/telemetry.d.ts.map +0 -1
- package/dist/core/telemetry.js +0 -637
- package/dist/core/telemetry.js.map +0 -1
- package/dist/modes/interactive/components/prime-onboarding-splash.d.ts +0 -39
- package/dist/modes/interactive/components/prime-onboarding-splash.d.ts.map +0 -1
- package/dist/modes/interactive/components/prime-onboarding-splash.js +0 -286
- package/dist/modes/interactive/components/prime-onboarding-splash.js.map +0 -1
- package/dist/modes/interactive/components/prime-team-selector.d.ts +0 -30
- package/dist/modes/interactive/components/prime-team-selector.d.ts.map +0 -1
- package/dist/modes/interactive/components/prime-team-selector.js +0 -166
- package/dist/modes/interactive/components/prime-team-selector.js.map +0 -1
- package/dist/skills/prime-intellect/SKILL.md +0 -87
- package/dist/skills/prime-intellect/references/compute.md +0 -45
- package/dist/skills/prime-intellect/references/environments.md +0 -73
- package/dist/skills/prime-intellect/references/inference.md +0 -47
- package/dist/skills/prime-intellect/references/sandboxes.md +0 -75
- package/skills/prime-intellect/SKILL.md +0 -87
- package/skills/prime-intellect/references/compute.md +0 -45
- package/skills/prime-intellect/references/environments.md +0 -73
- package/skills/prime-intellect/references/inference.md +0 -47
- package/skills/prime-intellect/references/sandboxes.md +0 -75
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon-ps.d.ts","sourceRoot":"","sources":["../../src/cli/daemon-ps.ts"],"names":[],"mappings":"AAiBA,OAAO,EAGN,KAAK,qBAAqB,EAG1B,MAAM,oCAAoC,CAAC;AAQ5C,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAExF;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,OAAO,GAAG,aAAa,GAAG,aAAa,CAAC;AAE/E,MAAM,WAAW,uBAAuB;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAWD,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAK/G;AASD,sFAAsF;AACtF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,uBAAuB,EAAE,CAkB3F;AAED,uGAAuG;AACvG,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,uBAAuB,EAAE,CAmB5E;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAcnF;AAED,wBAAgB,8BAA8B,CAC7C,GAAG,MAAM,EAAE,SAAS,uBAAuB,EAAE,EAAE,GAC7C,uBAAuB,EAAE,CAQ3B;AAED,2EAAyE;AACzE,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CASjE;AAqED,UAAU,WAAW;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,SAAS,EAAE,OAAO,CAAC;CACnB;AAED,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAuD1E;AAaD,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,sBAAsB,EAAE,MAAM,GAAG,SAAS,GACxC,MAAM,GAAG,SAAS,CAkBpB;AAED,uFAAuF;AACvF,wBAAsB,eAAe,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAkD7D;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAQ7D;AAED,wBAAsB,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYrF;AAED,MAAM,MAAM,UAAU,GACnB;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,EAAE,KAAK,EAAE,OAAO,GAAG,UAAU,EAAE,CA2CrF;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,EAAE,KAAK,EAAE,OAAO,GAAG,UAAU,EAAE,CAsB5F;AASD,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,QAAQ,GAAG,YAAY,GAAG,WAAW,CAAC;AAEtF,wBAAgB,wBAAwB,CACvC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,OAAO,EACd,UAAU,EAAE,OAAO,GAAG,SAAS,GAC7B,wBAAwB,CAI1B;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CjF;AAiYD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAO9D;AAoOD,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoEvG","sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { existsSync, lstatSync, readdirSync, readFileSync, rmSync, unlinkSync } from \"node:fs\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport chalk from \"chalk\";\nimport { APP_NAME, getAgentDir, VERSION } from \"../config.js\";\nimport {\n\tisOrphanProcessIdentityCurrent,\n\tkillOrphanProcess,\n\treadActiveOrphanProcesses,\n\tshouldReapOrphanProcess,\n} from \"../core/orphan-process-journal.js\";\nimport { getProcessStartId } from \"../core/session-lease.js\";\nimport {\n\tDaemonCapabilityUnavailableError,\n\tDaemonClient,\n\tDaemonProtocolMismatchError,\n} from \"../modes/daemon/daemon-client.js\";\nimport {\n\tDAEMON_PROTOCOL_VERSION,\n\tDAEMON_SCHEMA_ID,\n\ttype DaemonRuntimeIdentity,\n\tmeetsDaemonCommandCompatibility,\n\tNATIVE_WORK_COMPATIBILITIES,\n} from \"../modes/daemon/daemon-protocol.js\";\nimport { defaultDaemonSocketDir, defaultDaemonSocketPath, normalizeSocketPath } from \"../modes/daemon/daemon-socket.js\";\nimport { acquireDaemonShutdownAdmission } from \"../modes/daemon/daemon-supervisor-ownership.js\";\nimport { DaemonWorkerClient, DaemonWorkerCompatibilityError } from \"../modes/daemon/daemon-worker-client.js\";\nimport type { DaemonWorkerDescriptor } from \"../modes/daemon/daemon-worker-protocol.js\";\nimport { signalProcessGroupOrProcess } from \"../utils/child-process.js\";\nimport { formatDaemonListTable } from \"./daemon-ps-format.js\";\nimport { promptYesNo } from \"./daemon-stop-confirm.js\";\nimport { formatProductDiagnostics, type ProductDiagnostics } from \"./product-doctor.js\";\n\n/**\n * `daemon ps` discovers every base-context daemon on the machine, not just the\n * one on a single socket. Discovery has two sources merged by socket path:\n *\n * 1. The OS list of listening unix sockets owned by a base-context process\n * (`ss -lxp` on Linux, `lsof` on macOS). Daemons set process.title to\n * APP_NAME and carry nothing useful in argv, so the socket→pid mapping the\n * kernel keeps is the only reliable way to find daemons on arbitrary\n * `--daemon-socket` paths. This is the same data as `ss -lxp | grep\n * base-context`, just parsed.\n * 2. A sweep of the default socket dir, which catches orphaned socket *files*\n * left behind by daemons that are no longer running.\n *\n * Each discovered socket is then probed with the existing daemon_hello + list\n * primitives, so introspection works even against stale daemons running an\n * older build (a new protocol command would not).\n */\n\nexport type DaemonStatus = \"current\" | \"stale\" | \"unreachable\" | \"orphan-file\";\n\nexport interface DiscoveredDaemonProcess {\n\tpid: number;\n\tsocketPath: string;\n\tuptimeSeconds?: number;\n}\n\nexport interface DaemonInfo {\n\tsocketPath: string;\n\tpid?: number;\n\tuptimeSeconds?: number;\n\tversion?: string;\n\tprotocolVersion?: number;\n\tschemaId?: string;\n\tbuildId?: string;\n\texecutablePath?: string;\n\tpidSource?: \"listener\" | \"hello\";\n\tsessionCount?: number;\n\tstatus: DaemonStatus;\n\tisDefault: boolean;\n\thasTrackedWorkers?: boolean;\n}\n\nconst STATUS_ORDER: Record<DaemonStatus, number> = {\n\tcurrent: 0,\n\tstale: 1,\n\tunreachable: 2,\n\t\"orphan-file\": 3,\n};\nconst SHUTDOWN_QUIET_PERIOD_MS = 1000;\nconst SHUTDOWN_CONVERGENCE_TIMEOUT_MS = 10_000;\n\nexport function evaluateShutdownQuietPeriod(now: number, quietSince: number | undefined): \"complete\" | \"waiting\" {\n\tif (quietSince !== undefined && now - quietSince >= SHUTDOWN_QUIET_PERIOD_MS) {\n\t\treturn \"complete\";\n\t}\n\treturn \"waiting\";\n}\n\n// Linux comm names (and thus the process name ss reports) are capped at 15 chars.\nconst MAX_COMM_LENGTH = 15;\n\nfunction processNameMatches(name: string, appName: string): boolean {\n\treturn name === appName || appName.slice(0, MAX_COMM_LENGTH) === name;\n}\n\n/** Parse `ss -lxp` output into the base-context daemons listening on unix sockets. */\nexport function parseSsListeners(stdout: string, appName: string): DiscoveredDaemonProcess[] {\n\tconst daemons: DiscoveredDaemonProcess[] = [];\n\tfor (const line of stdout.split(\"\\n\")) {\n\t\tconst fields = line.trim().split(/\\s+/);\n\t\tif (fields[1] !== \"LISTEN\") {\n\t\t\tcontinue;\n\t\t}\n\t\tconst socketPath = fields[4];\n\t\tif (!socketPath?.startsWith(\"/\")) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst owner = line.match(/users:\\(\\(\"([^\"]+)\",pid=(\\d+)/);\n\t\tif (!owner || !processNameMatches(owner[1]!, appName)) {\n\t\t\tcontinue;\n\t\t}\n\t\tdaemons.push({ pid: Number.parseInt(owner[2]!, 10), socketPath: normalizeSocketPath(socketPath) });\n\t}\n\treturn daemons;\n}\n\n/** Parse `lsof -nP -F pn -U -a -c <app>` output into listening unix socket owners (macOS fallback). */\nexport function parseLsofListeners(stdout: string): DiscoveredDaemonProcess[] {\n\tconst daemons: DiscoveredDaemonProcess[] = [];\n\tconst seen = new Set<string>();\n\tlet pid: number | undefined;\n\tfor (const line of stdout.split(\"\\n\")) {\n\t\tconst field = line[0];\n\t\tconst value = line.slice(1);\n\t\tif (field === \"p\") {\n\t\t\tpid = Number.parseInt(value, 10);\n\t\t} else if (field === \"n\" && pid !== undefined && value.startsWith(\"/\")) {\n\t\t\tconst socketPath = normalizeSocketPath(value);\n\t\t\tconst key = `${pid}:${socketPath}`;\n\t\t\tif (!seen.has(key)) {\n\t\t\t\tseen.add(key);\n\t\t\t\tdaemons.push({ pid, socketPath });\n\t\t\t}\n\t\t}\n\t}\n\treturn daemons;\n}\n\nexport function parsePrimeAgentProcessIds(stdout: string, appName: string): number[] {\n\tconst pids: number[] = [];\n\tfor (const line of stdout.split(\"\\n\")) {\n\t\tconst match = line.trim().match(/^(\\d+)\\s+(\\S+)(?:\\s+(.*))?$/);\n\t\tif (!match) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst command = basename(match[2]!);\n\t\tconst argv0 = basename(match[3]?.trim().split(/\\s+/, 1)[0] ?? \"\");\n\t\tif (processNameMatches(command, appName) || processNameMatches(argv0, appName)) {\n\t\t\tpids.push(Number.parseInt(match[1]!, 10));\n\t\t}\n\t}\n\treturn pids;\n}\n\nexport function mergeDiscoveredDaemonProcesses(\n\t...groups: readonly DiscoveredDaemonProcess[][]\n): DiscoveredDaemonProcess[] {\n\tconst byIdentity = new Map<string, DiscoveredDaemonProcess>();\n\tfor (const group of groups) {\n\t\tfor (const daemon of group) {\n\t\t\tbyIdentity.set(`${daemon.pid}:${daemon.socketPath}`, daemon);\n\t\t}\n\t}\n\treturn [...byIdentity.values()];\n}\n\n/** Parse `ps -o pid=,etimes=` output into a pid → uptime-seconds map. */\nexport function parsePsEtimes(stdout: string): Map<number, number> {\n\tconst uptimes = new Map<number, number>();\n\tfor (const line of stdout.split(\"\\n\")) {\n\t\tconst match = line.trim().match(/^(\\d+)\\s+(\\d+)$/);\n\t\tif (match) {\n\t\t\tuptimes.set(Number.parseInt(match[1]!, 10), Number.parseInt(match[2]!, 10));\n\t\t}\n\t}\n\treturn uptimes;\n}\n\nfunction scanListeningDaemons(): DiscoveredDaemonProcess[] {\n\tif (process.platform === \"win32\") {\n\t\treturn [];\n\t}\n\tconst ss = spawnSync(\"ss\", [\"-lxp\"], { encoding: \"utf8\" });\n\tif (!ss.error && ss.status === 0 && typeof ss.stdout === \"string\") {\n\t\treturn enrichUptimes(parseSsListeners(ss.stdout, APP_NAME));\n\t}\n\tconst lsof = spawnSync(\"lsof\", [\"-nP\", \"-F\", \"pn\", \"-U\", \"-a\", \"-c\", APP_NAME], { encoding: \"utf8\" });\n\tconst byName = !lsof.error && typeof lsof.stdout === \"string\" ? parseLsofListeners(lsof.stdout) : [];\n\tlet byPid: DiscoveredDaemonProcess[] = [];\n\tconst ps = spawnSync(\"ps\", [\"-axo\", \"pid=,comm=,args=\"], { encoding: \"utf8\" });\n\tif (!ps.error && ps.status === 0 && typeof ps.stdout === \"string\") {\n\t\tconst pids = parsePrimeAgentProcessIds(ps.stdout, APP_NAME);\n\t\tif (pids.length > 0) {\n\t\t\tconst lsofByPid = spawnSync(\"lsof\", [\"-nP\", \"-F\", \"pn\", \"-U\", \"-a\", \"-p\", pids.join(\",\")], {\n\t\t\t\tencoding: \"utf8\",\n\t\t\t});\n\t\t\tif (!lsofByPid.error && typeof lsofByPid.stdout === \"string\") {\n\t\t\t\tbyPid = parseLsofListeners(lsofByPid.stdout);\n\t\t\t}\n\t\t}\n\t}\n\treturn enrichUptimes(mergeDiscoveredDaemonProcesses(byName, byPid));\n}\n\nfunction isDaemonProcessListening(pid: number, socketPath: string): boolean {\n\tconst target = normalizeSocketPath(socketPath);\n\treturn scanListeningDaemons().some((daemon) => daemon.pid === pid && daemon.socketPath === target);\n}\n\nfunction enrichUptimes(daemons: DiscoveredDaemonProcess[]): DiscoveredDaemonProcess[] {\n\tconst pids = daemons.map((daemon) => daemon.pid);\n\tif (pids.length === 0) {\n\t\treturn daemons;\n\t}\n\tconst ps = spawnSync(\"ps\", [\"-o\", \"pid=,etimes=\", \"-p\", pids.join(\",\")], { encoding: \"utf8\" });\n\tif (ps.error || typeof ps.stdout !== \"string\") {\n\t\treturn daemons;\n\t}\n\tconst uptimes = parsePsEtimes(ps.stdout);\n\treturn daemons.map((daemon) => ({ ...daemon, uptimeSeconds: uptimes.get(daemon.pid) }));\n}\n\n/** Socket files in the default socket dir (may be live daemons or orphaned files). */\nfunction scanSocketDir(): string[] {\n\tif (process.platform === \"win32\") {\n\t\treturn [];\n\t}\n\tconst dir = defaultDaemonSocketDir();\n\tif (!existsSync(dir)) {\n\t\treturn [];\n\t}\n\tconst sockets: string[] = [];\n\tfor (const entry of readdirSync(dir)) {\n\t\tconst socketPath = join(dir, entry);\n\t\ttry {\n\t\t\tif (lstatSync(socketPath).isSocket()) {\n\t\t\t\tsockets.push(normalizeSocketPath(socketPath));\n\t\t\t}\n\t\t} catch {\n\t\t\t// Entry vanished between readdir and lstat; ignore.\n\t\t}\n\t}\n\treturn sockets;\n}\n\ninterface ProbeResult {\n\tversion?: string;\n\tprotocolVersion?: number;\n\tschemaId?: string;\n\truntime?: DaemonRuntimeIdentity;\n\tsessionCount?: number;\n\tsupervisorPid?: number;\n\tsupervisorProcessStartId?: string;\n\treachable: boolean;\n}\n\nexport async function probeDaemon(socketPath: string): Promise<ProbeResult> {\n\tconst client = new DaemonClient(socketPath);\n\ttry {\n\t\tawait client.connect(300);\n\t} catch {\n\t\tclient.close();\n\t\treturn { reachable: false };\n\t}\n\ttry {\n\t\tlet version: string | undefined;\n\t\tlet protocolVersion: number | undefined;\n\t\tlet schemaId: string | undefined;\n\t\tlet runtime: DaemonRuntimeIdentity | undefined;\n\t\tlet supervisorPid: number | undefined;\n\t\tlet supervisorProcessStartId: string | undefined;\n\t\tlet greeted = false;\n\t\ttry {\n\t\t\tconst hello = await client.waitForHello(1500);\n\t\t\tversion = hello.appVersion;\n\t\t\tprotocolVersion = hello.protocol.version;\n\t\t\tschemaId = hello.schemaId;\n\t\t\truntime = hello.runtime;\n\t\t\tsupervisorPid = hello.supervisorPid;\n\t\t\tsupervisorProcessStartId = hello.supervisorProcessStartId;\n\t\t\tgreeted = true;\n\t\t} catch (error) {\n\t\t\tif (error instanceof DaemonProtocolMismatchError) throw error;\n\t\t\t// A silent peer is not evidence of a compatible command plane.\n\t\t}\n\t\tlet sessionCount: number | undefined;\n\t\ttry {\n\t\t\tconst response = await client.request({ type: \"list\" }, greeted ? 30000 : 1500);\n\t\t\tif (response.success) {\n\t\t\t\tconst sessions = (response.data as { sessions?: unknown })?.sessions;\n\t\t\t\tif (Array.isArray(sessions)) {\n\t\t\t\t\tsessionCount = sessions.length;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (error instanceof DaemonProtocolMismatchError) throw error;\n\t\t\t// Leave sessionCount undefined when the daemon will not answer list.\n\t\t}\n\t\treturn {\n\t\t\tversion,\n\t\t\tprotocolVersion,\n\t\t\tschemaId,\n\t\t\truntime,\n\t\t\tsessionCount,\n\t\t\tsupervisorPid,\n\t\t\tsupervisorProcessStartId,\n\t\t\treachable: true,\n\t\t};\n\t} finally {\n\t\tclient.close();\n\t}\n}\n\nfunction classifyReachable(probe: ProbeResult): DaemonStatus {\n\tif (\n\t\tprobe.protocolVersion === DAEMON_PROTOCOL_VERSION &&\n\t\tprobe.schemaId === DAEMON_SCHEMA_ID &&\n\t\tprobe.version === VERSION\n\t) {\n\t\treturn \"current\";\n\t}\n\treturn \"stale\";\n}\n\nexport function verifyHelloSupervisorPid(\n\tpid: number | undefined,\n\texpectedProcessStartId: string | undefined,\n): number | undefined {\n\tif (!Number.isInteger(pid) || pid === undefined || pid <= 0) {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code !== \"EPERM\") {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\tif (expectedProcessStartId) {\n\t\tconst observedStartId = getProcessStartId(pid);\n\t\tif (observedStartId !== expectedProcessStartId) {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\treturn pid;\n}\n\n/** Discover every daemon on the machine and probe each for version + session count. */\nexport async function discoverDaemons(): Promise<DaemonInfo[]> {\n\tconst processBySocket = new Map<string, DiscoveredDaemonProcess>();\n\tfor (const daemon of scanListeningDaemons()) {\n\t\tif (isWorkerSocketPath(daemon.socketPath)) {\n\t\t\tcontinue;\n\t\t}\n\t\tprocessBySocket.set(daemon.socketPath, daemon);\n\t}\n\n\tconst workerSockets = new Set(\n\t\tfindAllTrackedWorkers().map((worker) => normalizeSocketPath(worker.descriptor.supervisorSocketPath)),\n\t);\n\tconst sockets = new Set<string>([\n\t\t...processBySocket.keys(),\n\t\t...scanSocketDir().filter((socketPath) => !isWorkerSocketPath(socketPath)),\n\t\t...workerSockets,\n\t]);\n\tconst defaultSocket = normalizeSocketPath(defaultDaemonSocketPath());\n\n\tconst infos = await Promise.all(\n\t\t[...sockets].map(async (socketPath): Promise<DaemonInfo> => {\n\t\t\tconst proc = processBySocket.get(socketPath);\n\t\t\tconst probe = await probeDaemon(socketPath);\n\t\t\tconst pid = proc?.pid ?? verifyHelloSupervisorPid(probe.supervisorPid, probe.supervisorProcessStartId);\n\t\t\tconst hasTrackedWorkers = workerSockets.has(socketPath);\n\t\t\tconst status: DaemonStatus = probe.reachable\n\t\t\t\t? classifyReachable(probe)\n\t\t\t\t: proc || hasTrackedWorkers\n\t\t\t\t\t? \"unreachable\"\n\t\t\t\t\t: \"orphan-file\";\n\t\t\treturn {\n\t\t\t\tsocketPath,\n\t\t\t\tpid,\n\t\t\t\tuptimeSeconds: proc?.uptimeSeconds,\n\t\t\t\tversion: probe.version,\n\t\t\t\tprotocolVersion: probe.protocolVersion,\n\t\t\t\tschemaId: probe.schemaId,\n\t\t\t\tbuildId: probe.runtime?.buildId,\n\t\t\t\texecutablePath:\n\t\t\t\t\tprobe.runtime?.launcherPath ?? probe.runtime?.entrypointPath ?? probe.runtime?.executablePath,\n\t\t\t\t...(pid !== undefined ? { pidSource: proc ? (\"listener\" as const) : (\"hello\" as const) } : {}),\n\t\t\t\tsessionCount: probe.sessionCount,\n\t\t\t\tstatus,\n\t\t\t\tisDefault: socketPath === defaultSocket,\n\t\t\t\t...(hasTrackedWorkers ? { hasTrackedWorkers: true } : {}),\n\t\t\t};\n\t\t}),\n\t);\n\n\treturn sortDaemons(infos);\n}\n\nexport function sortDaemons(infos: DaemonInfo[]): DaemonInfo[] {\n\treturn [...infos].sort((left, right) => {\n\t\tif (left.isDefault !== right.isDefault) {\n\t\t\treturn left.isDefault ? -1 : 1;\n\t\t}\n\t\tconst statusDelta = STATUS_ORDER[left.status] - STATUS_ORDER[right.status];\n\t\treturn statusDelta || left.socketPath.localeCompare(right.socketPath);\n\t});\n}\n\nexport async function runPs(json: boolean, report?: ProductDiagnostics): Promise<void> {\n\tconst daemons = await discoverDaemons();\n\tif (json) {\n\t\tconsole.log(JSON.stringify(report ? { ...report, daemons } : daemons, null, 2));\n\t\treturn;\n\t}\n\tif (report) console.log(formatProductDiagnostics(report));\n\tif (daemons.length === 0) {\n\t\tconsole.log(\"No background services found.\");\n\t\treturn;\n\t}\n\tconsole.log(formatDaemonListTable(daemons));\n}\n\nexport type ReapAction =\n\t| { kind: \"remove-file\"; daemon: DaemonInfo }\n\t| { kind: \"kill\"; daemon: DaemonInfo }\n\t| { kind: \"shutdown\"; daemon: DaemonInfo }\n\t| { kind: \"skip\"; daemon: DaemonInfo; reason: string };\n\n/**\n * Decide what to do with each discovered daemon (pure, no side effects). Reap\n * targets only clearly-safe daemons: orphaned socket files (including a stale\n * default daemon.sock with no live process), and reachable idle daemons on\n * non-default sockets. A reachable default daemon, and any reachable daemon with\n * live sessions, are never touched.\n *\n * Unreachable (hung) daemons can't report a session count, so they are only\n * ever killed with `force`, and even then never via a pid that backs more than\n * one discovered daemon (e.g. macOS lsof reports every unix socket a process\n * holds, so killing a shared pid could take down a reachable daemon with live\n * sessions). runReap additionally re-probes a kill candidate immediately before\n * the SIGTERM and backs off to the session-aware paths if it has since become\n * reachable, so a daemon that recovered with live sessions is never killed.\n */\nexport function planReap(daemons: readonly DaemonInfo[], force: boolean): ReapAction[] {\n\tconst pidCounts = new Map<number, number>();\n\tfor (const daemon of daemons) {\n\t\tif (daemon.pid !== undefined) {\n\t\t\tpidCounts.set(daemon.pid, (pidCounts.get(daemon.pid) ?? 0) + 1);\n\t\t}\n\t}\n\n\treturn daemons.map((daemon): ReapAction => {\n\t\t// An orphan socket file has no owning process, so removing it is safe even\n\t\t// on the default path (a stale daemon.sock left by a crash). Decide this\n\t\t// before the default guard so a dead default socket still gets cleaned up.\n\t\tif (daemon.status === \"orphan-file\") {\n\t\t\treturn { kind: \"remove-file\", daemon };\n\t\t}\n\t\tif (daemon.protocolVersion !== undefined && daemon.protocolVersion !== DAEMON_PROTOCOL_VERSION) {\n\t\t\treturn {\n\t\t\t\tkind: \"skip\",\n\t\t\t\tdaemon,\n\t\t\t\treason: `Keeping incompatible daemon protocol ${daemon.protocolVersion}; canonical session ownership requires protocol ${DAEMON_PROTOCOL_VERSION}. Use its own runtime for cleanup.`,\n\t\t\t};\n\t\t}\n\t\tif (daemon.isDefault) {\n\t\t\treturn { kind: \"skip\", daemon, reason: \"default background service\" };\n\t\t}\n\t\tif (daemon.status === \"unreachable\") {\n\t\t\tif (!force || daemon.pid === undefined) {\n\t\t\t\treturn { kind: \"skip\", daemon, reason: 'unreachable; use \"base-context shutdown --force\" to stop it' };\n\t\t\t}\n\t\t\tif ((pidCounts.get(daemon.pid) ?? 0) > 1) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"skip\",\n\t\t\t\t\tdaemon,\n\t\t\t\t\treason: `unreachable; pid ${daemon.pid} also backs another daemon, not killing`,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn { kind: \"kill\", daemon };\n\t\t}\n\t\tif (daemon.sessionCount !== 0) {\n\t\t\treturn { kind: \"skip\", daemon, reason: `has ${daemon.sessionCount ?? \"unknown\"} session(s)` };\n\t\t}\n\t\treturn { kind: \"shutdown\", daemon };\n\t});\n}\n\nexport function planShutdownAll(daemons: readonly DaemonInfo[], force: boolean): ReapAction[] {\n\treturn daemons.map((daemon): ReapAction => {\n\t\tif (daemon.status === \"orphan-file\") {\n\t\t\treturn { kind: \"remove-file\", daemon };\n\t\t}\n\t\tif (daemon.protocolVersion !== undefined && daemon.protocolVersion !== DAEMON_PROTOCOL_VERSION) {\n\t\t\treturn {\n\t\t\t\tkind: \"skip\",\n\t\t\t\tdaemon,\n\t\t\t\treason: `Keeping incompatible daemon protocol ${daemon.protocolVersion}; canonical session ownership requires protocol ${DAEMON_PROTOCOL_VERSION}. Use its own runtime for cleanup.`,\n\t\t\t};\n\t\t}\n\t\tif (daemon.status === \"unreachable\") {\n\t\t\tif (daemon.pid === undefined) {\n\t\t\t\treturn force || !daemon.hasTrackedWorkers\n\t\t\t\t\t? { kind: \"remove-file\", daemon }\n\t\t\t\t\t: { kind: \"skip\", daemon, reason: \"has unreachable workers; use --force to kill\" };\n\t\t\t}\n\t\t\treturn force ? { kind: \"kill\", daemon } : { kind: \"skip\", daemon, reason: \"unreachable; use --force to kill\" };\n\t\t}\n\t\treturn { kind: \"shutdown\", daemon };\n\t});\n}\n\nconst SHUTDOWN_ALL_ACTION_ORDER: Record<ReapAction[\"kind\"], number> = {\n\tshutdown: 0,\n\t\"remove-file\": 1,\n\tkill: 2,\n\tskip: 3,\n};\n\nexport type ShutdownConfirmationPlan = \"none\" | \"prompt\" | \"json-error\" | \"tty-error\";\n\nexport function planShutdownConfirmation(\n\tdaemonCount: number,\n\tjson: boolean,\n\tforce: boolean,\n\tstdinIsTTY: boolean | undefined,\n): ShutdownConfirmationPlan {\n\tif (daemonCount === 0 || force) return \"none\";\n\tif (json) return \"json-error\";\n\treturn stdinIsTTY ? \"prompt\" : \"tty-error\";\n}\n\nexport async function runShutdownAll(json: boolean, force: boolean): Promise<void> {\n\tconst daemons = await discoverDaemons();\n\tswitch (planShutdownConfirmation(daemons.length, json, force, process.stdin.isTTY)) {\n\t\tcase \"json-error\":\n\t\t\tprocess.exitCode = 1;\n\t\t\tconsole.log(\n\t\t\t\tJSON.stringify(\n\t\t\t\t\t{\n\t\t\t\t\t\tstopped: [],\n\t\t\t\t\t\tfailed: daemons.map(({ socketPath }) => ({\n\t\t\t\t\t\t\tsocketPath,\n\t\t\t\t\t\t\treason: 'confirmation required; use \"base-context shutdown --force --json\"',\n\t\t\t\t\t\t})),\n\t\t\t\t\t},\n\t\t\t\t\tnull,\n\t\t\t\t\t2,\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn;\n\t\tcase \"tty-error\":\n\t\t\tthrow new Error(\n\t\t\t\t'Shutdown requires confirmation in an interactive terminal. Use \"base-context shutdown --force\".',\n\t\t\t);\n\t\tcase \"prompt\": {\n\t\t\tconst confirmed = await promptYesNo(\n\t\t\t\t\"Stop every agent and background service? Active work will be interrupted.\",\n\t\t\t);\n\t\t\tif (!confirmed) {\n\t\t\t\tconsole.log(chalk.dim(\"Shutdown cancelled.\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase \"none\":\n\t\t\tbreak;\n\t}\n\tconst admission = await acquireDaemonShutdownAdmission();\n\ttry {\n\t\tawait runShutdownAllConverging(json, force, () => admission.assertOrRenew());\n\t} finally {\n\t\tawait admission.release();\n\t}\n}\n\nasync function runShutdownAllConverging(\n\tjson: boolean,\n\tforce: boolean,\n\tassertAdmission: () => Promise<void>,\n): Promise<void> {\n\tconst stopped: Array<{ socketPath: string; action: string }> = [];\n\tconst failed: Array<{ socketPath: string; reason: string }> = [];\n\tconst handledPids = new Set<number>();\n\tconst reportedFailures = new Set<string>();\n\n\tif (force) {\n\t\tawait stopHiddenSupervisors(stopped, failed, handledPids, reportedFailures, assertAdmission);\n\t}\n\tconst daemons = (await discoverDaemons()).filter((daemon) => !isWorkerSocketPath(daemon.socketPath));\n\n\tconst actions = [...planShutdownAll(daemons, force)].sort(\n\t\t(left, right) => SHUTDOWN_ALL_ACTION_ORDER[left.kind] - SHUTDOWN_ALL_ACTION_ORDER[right.kind],\n\t);\n\n\tfor (const action of actions) {\n\t\tconst { socketPath, pid } = action.daemon;\n\t\tif (pid !== undefined && handledPids.has(pid)) {\n\t\t\tawait assertAdmission();\n\t\t\tremoveSocketFile(socketPath);\n\t\t\tstopped.push({ socketPath, action: `background service already stopped (pid ${pid})` });\n\t\t\tif (force) {\n\t\t\t\tfailed.push(\n\t\t\t\t\t...(await forceStopTrackedWorkers(socketPath, assertAdmission)).map((reason) => ({\n\t\t\t\t\t\tsocketPath,\n\t\t\t\t\t\treason,\n\t\t\t\t\t})),\n\t\t\t\t);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tswitch (action.kind) {\n\t\t\tcase \"remove-file\": {\n\t\t\t\tif ((await probeDaemon(socketPath)).reachable) {\n\t\t\t\t\tapply(\n\t\t\t\t\t\tawait stopBackgroundService(socketPath, pid, handledPids, force, assertAdmission),\n\t\t\t\t\t\tsocketPath,\n\t\t\t\t\t\tstopped,\n\t\t\t\t\t\tfailed,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tawait assertAdmission();\n\t\t\t\t\tif (removeSocketFile(socketPath)) {\n\t\t\t\t\t\tstopped.push({ socketPath, action: \"removed stale socket file\" });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfailed.push({ socketPath, reason: \"could not remove socket file\" });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"kill\": {\n\t\t\t\tif ((await probeDaemon(socketPath)).reachable) {\n\t\t\t\t\tapply(\n\t\t\t\t\t\tawait stopBackgroundService(socketPath, pid, handledPids, force, assertAdmission),\n\t\t\t\t\t\tsocketPath,\n\t\t\t\t\t\tstopped,\n\t\t\t\t\t\tfailed,\n\t\t\t\t\t);\n\t\t\t\t} else if (isDaemonProcessListening(pid!, socketPath)) {\n\t\t\t\t\tawait assertAdmission();\n\t\t\t\t\tconst refusal = await forceKillDaemon(pid!, socketPath);\n\t\t\t\t\tif (refusal) {\n\t\t\t\t\t\tfailed.push({ socketPath, reason: refusal });\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\thandledPids.add(pid!);\n\t\t\t\t\tawait assertAdmission();\n\t\t\t\t\tremoveSocketFile(socketPath);\n\t\t\t\t\tstopped.push({ socketPath, action: `killed unreachable background service (pid ${pid})` });\n\t\t\t\t} else if (isProcessAlive(pid!)) {\n\t\t\t\t\tfailed.push({\n\t\t\t\t\t\tsocketPath,\n\t\t\t\t\t\treason: `Keeping live daemon pid ${pid}: compatibility is unknown/unverified`,\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tawait assertAdmission();\n\t\t\t\t\tremoveSocketFile(socketPath);\n\t\t\t\t\tstopped.push({ socketPath, action: \"background service already stopped\" });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"shutdown\":\n\t\t\t\tapply(\n\t\t\t\t\tawait stopBackgroundService(socketPath, pid, handledPids, force, assertAdmission),\n\t\t\t\t\tsocketPath,\n\t\t\t\t\tstopped,\n\t\t\t\t\tfailed,\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"skip\":\n\t\t\t\tfailed.push({ socketPath, reason: action.reason });\n\t\t\t\tbreak;\n\t\t}\n\t\tif (force && action.kind !== \"skip\") {\n\t\t\tfailed.push(\n\t\t\t\t...(await forceStopTrackedWorkers(socketPath, assertAdmission)).map((reason) => ({ socketPath, reason })),\n\t\t\t);\n\t\t}\n\t}\n\n\tif (force) {\n\t\tawait terminateVerifiedResiduals(stopped, failed, handledPids, reportedFailures, assertAdmission);\n\t}\n\n\tif (json) {\n\t\tif (failed.length > 0) {\n\t\t\tprocess.exitCode = 1;\n\t\t}\n\t\tconsole.log(JSON.stringify({ stopped, failed }, null, 2));\n\t\treturn;\n\t}\n\tif (stopped.length === 0 && failed.length === 0) {\n\t\tconsole.log(\"No background services found.\");\n\t\treturn;\n\t}\n\tfor (const entry of stopped) {\n\t\tconsole.log(chalk.green(`stopped ${entry.socketPath}: ${entry.action}`));\n\t}\n\tfor (const entry of failed) {\n\t\tconsole.log(chalk.red(`failed ${entry.socketPath}: ${entry.reason}`));\n\t}\n\tif (failed.length > 0) {\n\t\tprocess.exitCode = 1;\n\t}\n}\n\nasync function stopHiddenSupervisors(\n\tstopped: Array<{ socketPath: string; action: string }>,\n\tfailed: Array<{ socketPath: string; reason: string }>,\n\thandledPids: Set<number>,\n\treportedFailures: Set<string>,\n\tassertAdmission: () => Promise<void>,\n): Promise<void> {\n\twhile (true) {\n\t\tconst listeners = scanListeningDaemons().filter((listener) => !isWorkerSocketPath(listener.socketPath));\n\t\tconst bySocket = new Map<string, DiscoveredDaemonProcess[]>();\n\t\tfor (const listener of listeners) {\n\t\t\tconst group = bySocket.get(listener.socketPath) ?? [];\n\t\t\tgroup.push(listener);\n\t\t\tbySocket.set(listener.socketPath, group);\n\t\t}\n\t\tconst hidden: DiscoveredDaemonProcess[] = [];\n\t\tfor (const [socketPath, group] of bySocket) {\n\t\t\tif (new Set(group.map((listener) => listener.pid)).size < 2) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst currentPid = (await probeDaemon(socketPath)).supervisorPid;\n\t\t\tif (currentPid === undefined || !group.some((listener) => listener.pid === currentPid)) {\n\t\t\t\trecordShutdownFailure(\n\t\t\t\t\tfailed,\n\t\t\t\t\treportedFailures,\n\t\t\t\t\tsocketPath,\n\t\t\t\t\t\"could not identify the current same-path daemon\",\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\thidden.push(...group.filter((listener) => listener.pid !== currentPid));\n\t\t}\n\t\tif (hidden.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst before = daemonListenerSignature(hidden);\n\t\tfor (const listener of hidden) {\n\t\t\tif (await terminateVerifiedListener(listener, failed, reportedFailures, assertAdmission)) {\n\t\t\t\thandledPids.add(listener.pid);\n\t\t\t\tstopped.push({ socketPath: listener.socketPath, action: `stopped hidden daemon (pid ${listener.pid})` });\n\t\t\t}\n\t\t}\n\t\tconst afterHidden = scanListeningDaemons().filter(\n\t\t\t(listener) =>\n\t\t\t\t!isWorkerSocketPath(listener.socketPath) &&\n\t\t\t\thidden.some((candidate) => candidate.pid === listener.pid && candidate.socketPath === listener.socketPath),\n\t\t);\n\t\tif (afterHidden.length === 0 || daemonListenerSignature(afterHidden) === before) {\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nasync function terminateVerifiedResiduals(\n\tstopped: Array<{ socketPath: string; action: string }>,\n\tfailed: Array<{ socketPath: string; reason: string }>,\n\thandledPids: Set<number>,\n\treportedFailures: Set<string>,\n\tassertAdmission: () => Promise<void>,\n): Promise<void> {\n\tlet previousSignature: string | undefined;\n\tlet quietSince: number | undefined;\n\tconst deadline = Date.now() + SHUTDOWN_CONVERGENCE_TIMEOUT_MS;\n\twhile (true) {\n\t\tawait assertAdmission();\n\t\tconst listeners = scanListeningDaemons();\n\t\tconst now = Date.now();\n\t\tif (listeners.length === 0) {\n\t\t\tpreviousSignature = undefined;\n\t\t\tquietSince ??= now;\n\t\t\tconst quietPeriod = evaluateShutdownQuietPeriod(now, quietSince);\n\t\t\tif (quietPeriod === \"complete\") {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait delay(100);\n\t\t\tcontinue;\n\t\t}\n\t\tquietSince = undefined;\n\t\tconst signature = daemonListenerSignature(listeners);\n\t\tif (now >= deadline) {\n\t\t\trecordResidualListenerFailures(listeners, failed, reportedFailures, \"kept respawning during shutdown\");\n\t\t\treturn;\n\t\t}\n\t\tif (signature === previousSignature) {\n\t\t\trecordResidualListenerFailures(listeners, failed, reportedFailures, \"remained after shutdown\");\n\t\t\treturn;\n\t\t}\n\t\tpreviousSignature = signature;\n\t\tconst seenPids = new Set<number>();\n\t\tfor (const listener of listeners) {\n\t\t\tif (seenPids.has(listener.pid)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tseenPids.add(listener.pid);\n\t\t\tconst alreadyReported = handledPids.has(listener.pid);\n\t\t\tif (await terminateVerifiedListener(listener, failed, reportedFailures, assertAdmission)) {\n\t\t\t\thandledPids.add(listener.pid);\n\t\t\t\tif (!alreadyReported) {\n\t\t\t\t\tstopped.push({\n\t\t\t\t\t\tsocketPath: listener.socketPath,\n\t\t\t\t\t\taction: `stopped residual daemon process (pid ${listener.pid})`,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction recordResidualListenerFailures(\n\tlisteners: readonly DiscoveredDaemonProcess[],\n\tfailed: Array<{ socketPath: string; reason: string }>,\n\treportedFailures: Set<string>,\n\treason: string,\n): void {\n\tfor (const listener of listeners) {\n\t\tconst processStartId = getProcessStartId(listener.pid);\n\t\tconst identity = processStartId\n\t\t\t? `pid ${listener.pid}, start ${processStartId}`\n\t\t\t: `pid ${listener.pid}, process identity unavailable`;\n\t\trecordShutdownFailure(\n\t\t\tfailed,\n\t\t\treportedFailures,\n\t\t\tlistener.socketPath,\n\t\t\t`daemon ${reason} (${identity})${describeDaemonParent(listener.pid)}`,\n\t\t);\n\t}\n}\nfunction describeDaemonParent(pid: number): string {\n\tconst result = spawnSync(\"ps\", [\"-o\", \"ppid=,tty=,command=\", \"-p\", String(pid)], { encoding: \"utf8\" });\n\tif (result.error || result.status !== 0 || typeof result.stdout !== \"string\") {\n\t\treturn \"\";\n\t}\n\tconst match = result.stdout.trim().match(/^(\\d+)\\s+(\\S+)\\s+(.+)$/);\n\tif (!match) {\n\t\treturn \"\";\n\t}\n\treturn `; close parent PID ${match[1]} on ${match[2]} (${match[3]}) and retry shutdown`;\n}\n\nasync function daemonCleanupRefusal(\n\tsocketPath: string,\n\tpid: number,\n\tprocessStartId: string | undefined,\n\tworker = isWorkerSocketPath(socketPath),\n): Promise<string | undefined> {\n\tconst unknown = `Keeping live daemon pid ${pid}: compatibility is unknown/unverified`;\n\tif (!processStartId || getProcessStartId(pid) !== processStartId) return unknown;\n\tconst ownsSocket = () => {\n\t\tconst owners = scanListeningDaemons().filter(\n\t\t\t(listener) => listener.socketPath === normalizeSocketPath(socketPath),\n\t\t);\n\t\treturn owners.length > 0 && owners.every((listener) => listener.pid === pid);\n\t};\n\tconst socketOwnedAtEntry = ownsSocket();\n\tconst client = worker ? new DaemonWorkerClient(socketPath) : new DaemonClient(socketPath);\n\ttry {\n\t\tawait client.connect(300);\n\t\tconst hello = await client.waitForHello(1500);\n\t\tconst helloIdentifiesOwner =\n\t\t\t!worker && hello.supervisorPid === pid && hello.supervisorProcessStartId === processStartId;\n\t\tif (\n\t\t\tgetProcessStartId(pid) !== processStartId ||\n\t\t\t(!helloIdentifiesOwner && !(socketOwnedAtEntry && ownsSocket()))\n\t\t) {\n\t\t\treturn unknown;\n\t\t}\n\t\tconst missing = NATIVE_WORK_COMPATIBILITIES.find(\n\t\t\t(requirement) => !meetsDaemonCommandCompatibility(hello, requirement),\n\t\t);\n\t\treturn missing ? new DaemonCapabilityUnavailableError(\"shutdown\", missing.capability).message : undefined;\n\t} catch (error) {\n\t\tif (\n\t\t\tsocketOwnedAtEntry &&\n\t\t\townsSocket() &&\n\t\t\tgetProcessStartId(pid) === processStartId &&\n\t\t\t(error instanceof DaemonProtocolMismatchError || error instanceof DaemonWorkerCompatibilityError)\n\t\t) {\n\t\t\treturn error.message;\n\t\t}\n\t\treturn `${unknown}: ${error instanceof Error ? error.message : String(error)}`;\n\t} finally {\n\t\tclient.close();\n\t}\n}\n\nasync function terminateVerifiedListener(\n\tlistener: DiscoveredDaemonProcess,\n\tfailed: Array<{ socketPath: string; reason: string }>,\n\treportedFailures: Set<string>,\n\tassertAdmission: () => Promise<void>,\n): Promise<boolean> {\n\tconst processStartId = getProcessStartId(listener.pid);\n\tif (!processStartId) {\n\t\trecordShutdownFailure(\n\t\t\tfailed,\n\t\t\treportedFailures,\n\t\t\tlistener.socketPath,\n\t\t\t`could not verify daemon process identity (pid ${listener.pid})`,\n\t\t);\n\t\treturn false;\n\t}\n\tconst refusal = await daemonCleanupRefusal(listener.socketPath, listener.pid, processStartId);\n\tif (refusal) {\n\t\trecordShutdownFailure(failed, reportedFailures, listener.socketPath, refusal);\n\t\treturn false;\n\t}\n\tif (getProcessStartId(listener.pid) !== processStartId) {\n\t\treturn false;\n\t}\n\tawait assertAdmission();\n\tif (getProcessStartId(listener.pid) !== processStartId) {\n\t\treturn false;\n\t}\n\tkillDaemon(listener.pid);\n\tconst deadline = Date.now() + 1000;\n\twhile (getProcessStartId(listener.pid) === processStartId && Date.now() < deadline) {\n\t\tawait delay(50);\n\t}\n\tif (getProcessStartId(listener.pid) === processStartId) {\n\t\tawait assertAdmission();\n\t\tif (getProcessStartId(listener.pid) !== processStartId) {\n\t\t\treturn false;\n\t\t}\n\t\ttry {\n\t\t\tprocess.kill(listener.pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\t// The verified process exited between the identity check and signal.\n\t\t}\n\t}\n\treturn getProcessStartId(listener.pid) !== processStartId;\n}\n\nfunction daemonListenerSignature(listeners: readonly DiscoveredDaemonProcess[]): string {\n\treturn listeners\n\t\t.map((listener) => `${listener.pid}:${getProcessStartId(listener.pid) ?? \"unknown\"}:${listener.socketPath}`)\n\t\t.sort()\n\t\t.join(\"\\n\");\n}\n\nfunction recordShutdownFailure(\n\tfailed: Array<{ socketPath: string; reason: string }>,\n\treportedFailures: Set<string>,\n\tsocketPath: string,\n\treason: string,\n): void {\n\tconst key = `${socketPath}\\0${reason}`;\n\tif (reportedFailures.has(key)) {\n\t\treturn;\n\t}\n\treportedFailures.add(key);\n\tfailed.push({ socketPath, reason });\n}\n\nexport function isWorkerSocketPath(socketPath: string): boolean {\n\treturn (\n\t\tprocess.platform !== \"win32\" &&\n\t\tresolve(dirname(socketPath)) === resolve(defaultDaemonSocketDir()) &&\n\t\tbasename(socketPath).startsWith(\"worker-\") &&\n\t\tbasename(socketPath).endsWith(\".sock\")\n\t);\n}\n\nasync function stopBackgroundService(\n\tsocketPath: string,\n\tpid: number | undefined,\n\thandledPids: Set<number>,\n\tforce: boolean,\n\tassertAdmission: () => Promise<void>,\n): Promise<ReapOutcome> {\n\tawait assertAdmission();\n\tif (pid !== undefined && isProcessAlive(pid)) {\n\t\tconst refusal = await daemonCleanupRefusal(socketPath, pid, getProcessStartId(pid));\n\t\tif (refusal) return { skipped: refusal };\n\t}\n\tif (await shutdownDaemon(socketPath, force)) {\n\t\tif (pid !== undefined) {\n\t\t\thandledPids.add(pid);\n\t\t}\n\t\treturn { reaped: `stopped background service${pid ? ` (pid ${pid})` : \"\"}` };\n\t}\n\tif (!(await canConnectToSocket(socketPath, 250))) {\n\t\tif (pid !== undefined && isProcessAlive(pid)) {\n\t\t\treturn { skipped: `Keeping live daemon pid ${pid}: compatibility is unknown/unverified` };\n\t\t}\n\t\tawait assertAdmission();\n\t\tremoveSocketFile(socketPath);\n\t\treturn { reaped: \"background service already stopped\" };\n\t}\n\tif (pid === undefined) {\n\t\treturn { skipped: \"still listening but no pid to kill\" };\n\t}\n\tif (!force) {\n\t\treturn { skipped: \"did not stop gracefully; retry with --force\" };\n\t}\n\tawait assertAdmission();\n\tconst refusal = await forceKillDaemon(pid, socketPath);\n\tif (refusal) return { skipped: refusal };\n\thandledPids.add(pid);\n\tawait assertAdmission();\n\tremoveSocketFile(socketPath);\n\treturn { reaped: `force-killed unresponsive background service (pid ${pid})` };\n}\n\ninterface TrackedWorker {\n\tdescriptor: DaemonWorkerDescriptor;\n\tdescriptorPath: string;\n}\n\nasync function forceStopTrackedWorkers(\n\tsupervisorSocketPath: string,\n\tassertAdmission: () => Promise<void>,\n): Promise<string[]> {\n\tconst failures: string[] = [];\n\tfor (const worker of findTrackedWorkers(supervisorSocketPath)) {\n\t\tconst { descriptor } = worker;\n\t\tif (isProcessAlive(descriptor.pid)) {\n\t\t\tconst refusal = await daemonCleanupRefusal(\n\t\t\t\tdescriptor.socketPath,\n\t\t\t\tdescriptor.pid,\n\t\t\t\tdescriptor.processStartId,\n\t\t\t\ttrue,\n\t\t\t);\n\t\t\tif (refusal) {\n\t\t\t\tfailures.push(`Worker ${descriptor.workerId}: ${refusal}`);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tlet cleanupWorkerRecords = await stopTrackedProcess(descriptor.pid, descriptor.processStartId, assertAdmission);\n\t\tif (!cleanupWorkerRecords) {\n\t\t\tfailures.push(`could not safely stop worker ${descriptor.workerId} (pid ${descriptor.pid})`);\n\t\t}\n\t\tif (descriptor.orphanProcessJournalPath) {\n\t\t\tlet orphans: ReturnType<typeof readActiveOrphanProcesses> = [];\n\t\t\ttry {\n\t\t\t\torphans = readActiveOrphanProcesses(descriptor.orphanProcessJournalPath, descriptor.pid);\n\t\t\t} catch (error) {\n\t\t\t\tcleanupWorkerRecords = false;\n\t\t\t\tfailures.push(`could not read child process records for worker ${descriptor.workerId}: ${String(error)}`);\n\t\t\t}\n\t\t\tfor (const orphan of orphans) {\n\t\t\t\t// Pid-only records go through the platform predicate (stopTrackedProcess needs a startId).\n\t\t\t\tif (orphan.processStartId === undefined) {\n\t\t\t\t\tif (shouldReapOrphanProcess(orphan)) {\n\t\t\t\t\t\tkillOrphanProcess(orphan.pid);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (!isOrphanProcessIdentityCurrent(orphan)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (process.platform === \"win32\") {\n\t\t\t\t\t// taskkill /T, like the sibling reapers: signalling only the shell pid leaves its descendants alive.\n\t\t\t\t\tawait assertAdmission();\n\t\t\t\t\tif (isOrphanProcessIdentityCurrent(orphan)) {\n\t\t\t\t\t\tkillOrphanProcess(orphan.pid);\n\t\t\t\t\t\tif (isProcessAlive(orphan.pid)) {\n\t\t\t\t\t\t\tcleanupWorkerRecords = false;\n\t\t\t\t\t\t\tfailures.push(`could not stop child process ${orphan.pid} for worker ${descriptor.workerId}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (!(await stopTrackedProcess(orphan.pid, orphan.processStartId, assertAdmission))) {\n\t\t\t\t\tcleanupWorkerRecords = false;\n\t\t\t\t\tfailures.push(`could not stop child process ${orphan.pid} for worker ${descriptor.workerId}`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (cleanupWorkerRecords) {\n\t\t\ttry {\n\t\t\t\tremoveSocketFile(descriptor.socketPath);\n\t\t\t\trmSync(worker.descriptorPath, { force: true });\n\t\t\t\trmSync(descriptor.recoveryJournalPath, { force: true });\n\t\t\t\tif (descriptor.orphanProcessJournalPath) {\n\t\t\t\t\trmSync(descriptor.orphanProcessJournalPath, { force: true });\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tfailures.push(`could not clean up worker ${descriptor.workerId}: ${String(error)}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn failures;\n}\n\nfunction findTrackedWorkers(supervisorSocketPath: string): TrackedWorker[] {\n\treturn findAllTrackedWorkers().filter(\n\t\t(worker) =>\n\t\t\tnormalizeSocketPath(worker.descriptor.supervisorSocketPath) === normalizeSocketPath(supervisorSocketPath),\n\t);\n}\n\nfunction findAllTrackedWorkers(): TrackedWorker[] {\n\tconst root = join(getAgentDir(), \"daemon-workers\");\n\tif (!existsSync(root)) {\n\t\treturn [];\n\t}\n\tconst workers: TrackedWorker[] = [];\n\tlet directoryNames: string[];\n\ttry {\n\t\tdirectoryNames = readdirSync(root);\n\t} catch {\n\t\treturn [];\n\t}\n\tfor (const directoryName of directoryNames) {\n\t\tconst directory = join(root, directoryName);\n\t\ttry {\n\t\t\tif (!lstatSync(directory).isDirectory()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tlet fileNames: string[];\n\t\ttry {\n\t\t\tfileNames = readdirSync(directory);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const fileName of fileNames) {\n\t\t\tif (!fileName.endsWith(\".json\")) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst descriptorPath = join(directory, fileName);\n\t\t\ttry {\n\t\t\t\tconst value: unknown = JSON.parse(readFileSync(descriptorPath, \"utf8\"));\n\t\t\t\tif (isTrackedWorkerDescriptor(value)) {\n\t\t\t\t\tworkers.push({ descriptor: value, descriptorPath });\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Invalid or concurrently removed descriptors are not safe shutdown targets.\n\t\t\t}\n\t\t}\n\t}\n\treturn workers;\n}\n\nfunction isTrackedWorkerDescriptor(value: unknown): value is DaemonWorkerDescriptor {\n\tif (!value || typeof value !== \"object\") {\n\t\treturn false;\n\t}\n\tconst descriptor = value as Partial<DaemonWorkerDescriptor>;\n\treturn (\n\t\t(descriptor.version === 1 || descriptor.version === 2) &&\n\t\ttypeof descriptor.supervisorSocketPath === \"string\" &&\n\t\ttypeof descriptor.workerId === \"string\" &&\n\t\tNumber.isInteger(descriptor.pid) &&\n\t\t(descriptor.pid ?? 0) > 0 &&\n\t\t(descriptor.processStartId === undefined || typeof descriptor.processStartId === \"string\") &&\n\t\ttypeof descriptor.socketPath === \"string\" &&\n\t\ttypeof descriptor.recoveryJournalPath === \"string\"\n\t);\n}\n\nasync function stopTrackedProcess(\n\tpid: number,\n\texpectedStartId: string | undefined,\n\tassertAdmission: () => Promise<void>,\n): Promise<boolean> {\n\tif (!isProcessAlive(pid)) {\n\t\treturn true;\n\t}\n\tif (!expectedStartId || getProcessStartId(pid) !== expectedStartId) {\n\t\treturn false;\n\t}\n\tawait assertAdmission();\n\tif (getProcessStartId(pid) !== expectedStartId) {\n\t\treturn false;\n\t}\n\tsignalProcessGroupOrProcess(pid, \"SIGTERM\");\n\tlet deadline = Date.now() + 500;\n\twhile (isProcessAlive(pid) && Date.now() < deadline) {\n\t\tawait delay(25);\n\t}\n\tif (!isProcessAlive(pid)) {\n\t\treturn true;\n\t}\n\tawait assertAdmission();\n\tif (getProcessStartId(pid) !== expectedStartId) {\n\t\treturn false;\n\t}\n\tsignalProcessGroupOrProcess(pid, \"SIGKILL\");\n\tdeadline = Date.now() + 1000;\n\twhile (isProcessAlive(pid) && Date.now() < deadline) {\n\t\tawait delay(25);\n\t}\n\treturn !isProcessAlive(pid);\n}\n\nexport async function runReap(json: boolean, force: boolean, report?: ProductDiagnostics): Promise<void> {\n\tconst daemons = await discoverDaemons();\n\tconst reaped: Array<{ socketPath: string; action: string }> = [];\n\tconst skipped: Array<{ socketPath: string; reason: string }> = [];\n\n\tfor (const action of planReap(daemons, force)) {\n\t\tconst { socketPath, pid } = action.daemon;\n\t\tswitch (action.kind) {\n\t\t\tcase \"skip\":\n\t\t\t\tskipped.push({ socketPath, reason: action.reason });\n\t\t\t\tbreak;\n\t\t\tcase \"remove-file\": {\n\t\t\t\t// Re-probe before unlinking: a path marked orphan-file at discovery\n\t\t\t\t// may have since become a live listener. Only remove it if it is\n\t\t\t\t// still unreachable, so we never delete a socket a daemon is using.\n\t\t\t\tif ((await probeDaemon(socketPath)).reachable) {\n\t\t\t\t\tskipped.push({ socketPath, reason: \"now reachable; not removing socket file\" });\n\t\t\t\t} else if (removeSocketFile(socketPath)) {\n\t\t\t\t\treaped.push({ socketPath, action: \"removed stale socket file\" });\n\t\t\t\t} else {\n\t\t\t\t\tskipped.push({ socketPath, reason: \"could not remove socket file\" });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"kill\": {\n\t\t\t\t// Re-probe right before killing: discovery and this kill happen at\n\t\t\t\t// different moments, so a daemon classified \"unreachable\" may have\n\t\t\t\t// since started answering. Never SIGTERM one that now responds;\n\t\t\t\t// defer to the session-aware shutdown path instead.\n\t\t\t\tconst recheck = await probeDaemon(socketPath);\n\t\t\t\tif (!recheck.reachable) {\n\t\t\t\t\tif (isProcessAlive(pid!)) {\n\t\t\t\t\t\tconst processStartId = getProcessStartId(pid!);\n\t\t\t\t\t\tconst refusal = await daemonCleanupRefusal(socketPath, pid!, processStartId);\n\t\t\t\t\t\tif (refusal || getProcessStartId(pid!) !== processStartId) {\n\t\t\t\t\t\t\tskipped.push({ socketPath, reason: refusal ?? \"Daemon process identity changed before cleanup\" });\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tkillDaemon(pid!);\n\t\t\t\t\tremoveSocketFile(socketPath);\n\t\t\t\t\treaped.push({ socketPath, action: `killed unreachable daemon (pid ${pid})` });\n\t\t\t\t} else {\n\t\t\t\t\tapply(await reapReachableDaemon(socketPath, pid), socketPath, reaped, skipped);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"shutdown\":\n\t\t\t\tapply(await reapReachableDaemon(socketPath, pid), socketPath, reaped, skipped);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tif (json) {\n\t\tconsole.log(JSON.stringify({ ...report, reaped, skipped }, null, 2));\n\t\treturn;\n\t}\n\tif (report) console.log(formatProductDiagnostics(report));\n\tif (reaped.length === 0 && skipped.length === 0) {\n\t\tconsole.log(\"No background services found.\");\n\t\treturn;\n\t}\n\tfor (const entry of reaped) {\n\t\tconsole.log(chalk.green(`reaped ${entry.socketPath}: ${entry.action}`));\n\t}\n\tfor (const entry of skipped) {\n\t\tconsole.log(chalk.dim(`kept ${entry.socketPath}: ${entry.reason}`));\n\t}\n}\n\ntype ReapOutcome = { reaped: string } | { skipped: string };\n\nfunction apply(\n\toutcome: ReapOutcome,\n\tsocketPath: string,\n\treaped: Array<{ socketPath: string; action: string }>,\n\tskipped: Array<{ socketPath: string; reason: string }>,\n): void {\n\tif (\"reaped\" in outcome) {\n\t\treaped.push({ socketPath, action: outcome.reaped });\n\t} else {\n\t\tskipped.push({ socketPath, reason: outcome.skipped });\n\t}\n}\n\n/**\n * Gracefully stop a daemon, but only after a fresh probe confirms it is idle.\n * Discovery and reap happen at different moments, so the session count is\n * re-checked here to avoid stopping a daemon that gained a session in between.\n */\nasync function reapReachableDaemon(socketPath: string, pid: number | undefined): Promise<ReapOutcome> {\n\tconst probe = await probeDaemon(socketPath);\n\tif (!probe.reachable) {\n\t\treturn { skipped: \"no longer reachable\" };\n\t}\n\tif (probe.sessionCount !== 0) {\n\t\treturn { skipped: `now has ${probe.sessionCount ?? \"unknown\"} session(s)` };\n\t}\n\tif (pid !== undefined && isProcessAlive(pid)) {\n\t\tconst refusal = await daemonCleanupRefusal(socketPath, pid, getProcessStartId(pid));\n\t\tif (refusal) return { skipped: refusal };\n\t}\n\treturn (await shutdownDaemon(socketPath, false))\n\t\t? { reaped: `stopped idle background service${pid ? ` (pid ${pid})` : \"\"}` }\n\t\t: { skipped: \"shutdown request failed\" };\n}\n\nfunction removeSocketFile(socketPath: string): boolean {\n\ttry {\n\t\tif (existsSync(socketPath)) {\n\t\t\tunlinkSync(socketPath);\n\t\t}\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction killDaemon(pid: number): void {\n\ttry {\n\t\tprocess.kill(pid, \"SIGTERM\");\n\t} catch {\n\t\t// Process already gone or not permitted; the socket file cleanup still runs.\n\t}\n}\n\nasync function forceKillDaemon(pid: number, socketPath: string): Promise<string | undefined> {\n\tif (!isProcessAlive(pid)) return undefined;\n\tconst processStartId = getProcessStartId(pid);\n\tconst refusal = await daemonCleanupRefusal(socketPath, pid, processStartId);\n\tif (refusal) return refusal;\n\tif (getProcessStartId(pid) !== processStartId) return \"Daemon process identity changed before cleanup\";\n\tkillDaemon(pid);\n\tconst deadline = Date.now() + 1000;\n\twhile (Date.now() < deadline) {\n\t\tif (!isProcessAlive(pid)) return undefined;\n\t\tif (getProcessStartId(pid) !== processStartId) return \"Daemon process identity changed during cleanup\";\n\t\tawait delay(50);\n\t}\n\tif (getProcessStartId(pid) !== processStartId) return \"Daemon process identity changed during cleanup\";\n\ttry {\n\t\tprocess.kill(pid, \"SIGKILL\");\n\t} catch {\n\t\t// Process already exited between the liveness check and the kill.\n\t}\n\treturn undefined;\n}\n\nfunction isProcessAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\nfunction delay(ms: number): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function canConnectToSocket(socketPath: string, timeoutMs: number): Promise<boolean> {\n\tconst client = new DaemonClient(socketPath);\n\ttry {\n\t\tawait client.connect(timeoutMs);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t} finally {\n\t\tclient.close();\n\t}\n}\n\n/**\n * Ask a daemon to shut down and confirm it actually stopped listening. The\n * shutdown ack alone is not proof, so success is reported only once the socket\n * stops accepting connections.\n */\nasync function shutdownDaemon(socketPath: string, force: boolean): Promise<boolean> {\n\tconst client = new DaemonClient(socketPath);\n\ttry {\n\t\tawait client.connect(1000);\n\t} catch {\n\t\tclient.close();\n\t\treturn false;\n\t}\n\ttry {\n\t\tawait client.request({ type: \"shutdown\", force }, 1500);\n\t} catch (error) {\n\t\tif (error instanceof DaemonProtocolMismatchError || error instanceof DaemonCapabilityUnavailableError)\n\t\t\tthrow error;\n\t\tif (!client.hello) {\n\t\t\tthrow new Error(\"Daemon compatibility is unknown/unverified; shutdown was not admitted\", { cause: error });\n\t\t}\n\t\t// The daemon may still stop; the connectivity check below is the source of truth.\n\t} finally {\n\t\tclient.close();\n\t}\n\n\tconst deadline = Date.now() + 5000;\n\twhile (Date.now() < deadline) {\n\t\tif (!(await canConnectToSocket(socketPath, 250))) {\n\t\t\treturn true;\n\t\t}\n\t\tawait delay(50);\n\t}\n\treturn false;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"daemon-ps.d.ts","sourceRoot":"","sources":["../../src/cli/daemon-ps.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { existsSync, lstatSync, readdirSync, readFileSync, rmSync, unlinkSync } from \"node:fs\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport chalk from \"chalk\";\nimport { APP_NAME, getAgentDir, VERSION } from \"../config.js\";\nimport {\n\tisOrphanProcessIdentityCurrent,\n\tkillOrphanProcess,\n\treadActiveOrphanProcesses,\n\tshouldReapOrphanProcess,\n} from \"../core/orphan-process-journal.js\";\nimport { getProcessStartId } from \"../core/session-lease.js\";\nimport {\n\tDaemonCapabilityUnavailableError,\n\tDaemonClient,\n\tDaemonProtocolMismatchError,\n} from \"../modes/daemon/daemon-client.js\";\nimport {\n\tDAEMON_PROTOCOL_VERSION,\n\tDAEMON_SCHEMA_ID,\n\ttype DaemonRuntimeIdentity,\n\tmeetsDaemonCommandCompatibility,\n\tNATIVE_WORK_COMPATIBILITIES,\n} from \"../modes/daemon/daemon-protocol.js\";\nimport { defaultDaemonSocketDir, defaultDaemonSocketPath, normalizeSocketPath } from \"../modes/daemon/daemon-socket.js\";\nimport { createDaemonStateRootMatcher } from \"../modes/daemon/daemon-state-root.js\";\nimport { acquireDaemonShutdownAdmission } from \"../modes/daemon/daemon-supervisor-ownership.js\";\nimport { DaemonWorkerClient, DaemonWorkerCompatibilityError } from \"../modes/daemon/daemon-worker-client.js\";\nimport type { DaemonWorkerDescriptor } from \"../modes/daemon/daemon-worker-protocol.js\";\nimport { signalProcessGroupOrProcess } from \"../utils/child-process.js\";\nimport { formatDaemonListTable } from \"./daemon-ps-format.js\";\nimport { promptYesNo } from \"./daemon-stop-confirm.js\";\nimport { formatProductDiagnostics, type ProductDiagnostics } from \"./product-doctor.js\";\n\n/**\n * `daemon ps` discovers base-context daemons in this state root, not just the\n * one on a single socket. Discovery has two sources merged by socket path:\n *\n * 1. The OS list of listening unix sockets owned by a base-context process\n * (`ss -lxp` on Linux, `lsof` on macOS). Daemons set process.title to\n * APP_NAME and carry nothing useful in argv, so the socket→pid mapping the\n * kernel keeps is the only reliable way to find daemons on arbitrary\n * `--daemon-socket` paths. This is the same data as `ss -lxp | grep\n * base-context`, parsed and filtered to this root before any listing or\n * cleanup. Other agent directories must not become shutdown targets.\n * 2. A sweep of the default socket dir, which catches orphaned socket *files*\n * left behind by daemons that are no longer running.\n *\n * Each discovered socket is then probed with the existing daemon_hello + list\n * primitives, so introspection works even against stale daemons running an\n * older build (a new protocol command would not).\n */\n\nexport type DaemonStatus = \"current\" | \"stale\" | \"unreachable\" | \"orphan-file\";\n\nexport interface DiscoveredDaemonProcess {\n\tpid: number;\n\tsocketPath: string;\n\tuptimeSeconds?: number;\n}\n\nexport interface DaemonInfo {\n\tsocketPath: string;\n\tpid?: number;\n\tuptimeSeconds?: number;\n\tversion?: string;\n\tprotocolVersion?: number;\n\tschemaId?: string;\n\tbuildId?: string;\n\texecutablePath?: string;\n\tpidSource?: \"listener\" | \"hello\";\n\tsessionCount?: number;\n\tstatus: DaemonStatus;\n\tisDefault: boolean;\n\thasTrackedWorkers?: boolean;\n}\n\nconst STATUS_ORDER: Record<DaemonStatus, number> = {\n\tcurrent: 0,\n\tstale: 1,\n\tunreachable: 2,\n\t\"orphan-file\": 3,\n};\nconst SHUTDOWN_QUIET_PERIOD_MS = 1000;\nconst SHUTDOWN_CONVERGENCE_TIMEOUT_MS = 10_000;\n\nexport function evaluateShutdownQuietPeriod(now: number, quietSince: number | undefined): \"complete\" | \"waiting\" {\n\tif (quietSince !== undefined && now - quietSince >= SHUTDOWN_QUIET_PERIOD_MS) {\n\t\treturn \"complete\";\n\t}\n\treturn \"waiting\";\n}\n\n// Linux comm names (and thus the process name ss reports) are capped at 15 chars.\nconst MAX_COMM_LENGTH = 15;\n\nfunction processNameMatches(name: string, appName: string): boolean {\n\treturn name === appName || appName.slice(0, MAX_COMM_LENGTH) === name;\n}\n\n/** Parse `ss -lxp` output into the base-context daemons listening on unix sockets. */\nexport function parseSsListeners(stdout: string, appName: string): DiscoveredDaemonProcess[] {\n\tconst daemons: DiscoveredDaemonProcess[] = [];\n\tfor (const line of stdout.split(\"\\n\")) {\n\t\tconst fields = line.trim().split(/\\s+/);\n\t\tif (fields[1] !== \"LISTEN\") {\n\t\t\tcontinue;\n\t\t}\n\t\tconst socketPath = fields[4];\n\t\tif (!socketPath?.startsWith(\"/\")) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst owner = line.match(/users:\\(\\(\"([^\"]+)\",pid=(\\d+)/);\n\t\tif (!owner || !processNameMatches(owner[1]!, appName)) {\n\t\t\tcontinue;\n\t\t}\n\t\tdaemons.push({ pid: Number.parseInt(owner[2]!, 10), socketPath: normalizeSocketPath(socketPath) });\n\t}\n\treturn daemons;\n}\n\n/** Parse `lsof -nP -F pn -U -a -c <app>` output into listening unix socket owners (macOS fallback). */\nexport function parseLsofListeners(stdout: string): DiscoveredDaemonProcess[] {\n\tconst daemons: DiscoveredDaemonProcess[] = [];\n\tconst seen = new Set<string>();\n\tlet pid: number | undefined;\n\tfor (const line of stdout.split(\"\\n\")) {\n\t\tconst field = line[0];\n\t\tconst value = line.slice(1);\n\t\tif (field === \"p\") {\n\t\t\tpid = Number.parseInt(value, 10);\n\t\t} else if (field === \"n\" && pid !== undefined && value.startsWith(\"/\")) {\n\t\t\tconst socketPath = normalizeSocketPath(value);\n\t\t\tconst key = `${pid}:${socketPath}`;\n\t\t\tif (!seen.has(key)) {\n\t\t\t\tseen.add(key);\n\t\t\t\tdaemons.push({ pid, socketPath });\n\t\t\t}\n\t\t}\n\t}\n\treturn daemons;\n}\n\nexport function parsePrimeAgentProcessIds(stdout: string, appName: string): number[] {\n\tconst pids: number[] = [];\n\tfor (const line of stdout.split(\"\\n\")) {\n\t\tconst match = line.trim().match(/^(\\d+)\\s+(\\S+)(?:\\s+(.*))?$/);\n\t\tif (!match) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst command = basename(match[2]!);\n\t\tconst argv0 = basename(match[3]?.trim().split(/\\s+/, 1)[0] ?? \"\");\n\t\tif (processNameMatches(command, appName) || processNameMatches(argv0, appName)) {\n\t\t\tpids.push(Number.parseInt(match[1]!, 10));\n\t\t}\n\t}\n\treturn pids;\n}\n\nexport function mergeDiscoveredDaemonProcesses(\n\t...groups: readonly DiscoveredDaemonProcess[][]\n): DiscoveredDaemonProcess[] {\n\tconst byIdentity = new Map<string, DiscoveredDaemonProcess>();\n\tfor (const group of groups) {\n\t\tfor (const daemon of group) {\n\t\t\tbyIdentity.set(`${daemon.pid}:${daemon.socketPath}`, daemon);\n\t\t}\n\t}\n\treturn [...byIdentity.values()];\n}\n\n/** Parse `ps -o pid=,etimes=` output into a pid → uptime-seconds map. */\nexport function parsePsEtimes(stdout: string): Map<number, number> {\n\tconst uptimes = new Map<number, number>();\n\tfor (const line of stdout.split(\"\\n\")) {\n\t\tconst match = line.trim().match(/^(\\d+)\\s+(\\d+)$/);\n\t\tif (match) {\n\t\t\tuptimes.set(Number.parseInt(match[1]!, 10), Number.parseInt(match[2]!, 10));\n\t\t}\n\t}\n\treturn uptimes;\n}\n\nfunction scanListeningDaemons(): DiscoveredDaemonProcess[] {\n\tconst belongsToStateRoot = createDaemonStateRootMatcher();\n\treturn scanAllListeningDaemons().filter((daemon) => belongsToStateRoot(daemon.socketPath));\n}\n\nfunction scanAllListeningDaemons(): DiscoveredDaemonProcess[] {\n\tif (process.platform === \"win32\") {\n\t\treturn [];\n\t}\n\tconst ss = spawnSync(\"ss\", [\"-lxp\"], { encoding: \"utf8\" });\n\tif (!ss.error && ss.status === 0 && typeof ss.stdout === \"string\") {\n\t\treturn enrichUptimes(parseSsListeners(ss.stdout, APP_NAME));\n\t}\n\tconst lsof = spawnSync(\"lsof\", [\"-nP\", \"-F\", \"pn\", \"-U\", \"-a\", \"-c\", APP_NAME], { encoding: \"utf8\" });\n\tconst byName = !lsof.error && typeof lsof.stdout === \"string\" ? parseLsofListeners(lsof.stdout) : [];\n\tlet byPid: DiscoveredDaemonProcess[] = [];\n\tconst ps = spawnSync(\"ps\", [\"-axo\", \"pid=,comm=,args=\"], { encoding: \"utf8\" });\n\tif (!ps.error && ps.status === 0 && typeof ps.stdout === \"string\") {\n\t\tconst pids = parsePrimeAgentProcessIds(ps.stdout, APP_NAME);\n\t\tif (pids.length > 0) {\n\t\t\tconst lsofByPid = spawnSync(\"lsof\", [\"-nP\", \"-F\", \"pn\", \"-U\", \"-a\", \"-p\", pids.join(\",\")], {\n\t\t\t\tencoding: \"utf8\",\n\t\t\t});\n\t\t\tif (!lsofByPid.error && typeof lsofByPid.stdout === \"string\") {\n\t\t\t\tbyPid = parseLsofListeners(lsofByPid.stdout);\n\t\t\t}\n\t\t}\n\t}\n\treturn enrichUptimes(mergeDiscoveredDaemonProcesses(byName, byPid));\n}\n\nfunction isDaemonProcessListening(pid: number, socketPath: string): boolean {\n\tconst target = normalizeSocketPath(socketPath);\n\treturn scanListeningDaemons().some((daemon) => daemon.pid === pid && daemon.socketPath === target);\n}\n\nfunction enrichUptimes(daemons: DiscoveredDaemonProcess[]): DiscoveredDaemonProcess[] {\n\tconst pids = daemons.map((daemon) => daemon.pid);\n\tif (pids.length === 0) {\n\t\treturn daemons;\n\t}\n\tconst ps = spawnSync(\"ps\", [\"-o\", \"pid=,etimes=\", \"-p\", pids.join(\",\")], { encoding: \"utf8\" });\n\tif (ps.error || typeof ps.stdout !== \"string\") {\n\t\treturn daemons;\n\t}\n\tconst uptimes = parsePsEtimes(ps.stdout);\n\treturn daemons.map((daemon) => ({ ...daemon, uptimeSeconds: uptimes.get(daemon.pid) }));\n}\n\n/** Socket files in the default socket dir (may be live daemons or orphaned files). */\nfunction scanSocketDir(): string[] {\n\tif (process.platform === \"win32\") {\n\t\treturn [];\n\t}\n\tconst dir = defaultDaemonSocketDir();\n\tif (!existsSync(dir)) {\n\t\treturn [];\n\t}\n\tconst sockets: string[] = [];\n\tfor (const entry of readdirSync(dir)) {\n\t\tconst socketPath = join(dir, entry);\n\t\ttry {\n\t\t\tif (lstatSync(socketPath).isSocket()) {\n\t\t\t\tsockets.push(normalizeSocketPath(socketPath));\n\t\t\t}\n\t\t} catch {\n\t\t\t// Entry vanished between readdir and lstat; ignore.\n\t\t}\n\t}\n\treturn sockets;\n}\n\ninterface ProbeResult {\n\tversion?: string;\n\tprotocolVersion?: number;\n\tschemaId?: string;\n\truntime?: DaemonRuntimeIdentity;\n\tsessionCount?: number;\n\tsupervisorPid?: number;\n\tsupervisorProcessStartId?: string;\n\treachable: boolean;\n}\n\nexport async function probeDaemon(socketPath: string): Promise<ProbeResult> {\n\tconst client = new DaemonClient(socketPath);\n\ttry {\n\t\tawait client.connect(300);\n\t} catch {\n\t\tclient.close();\n\t\treturn { reachable: false };\n\t}\n\ttry {\n\t\tlet version: string | undefined;\n\t\tlet protocolVersion: number | undefined;\n\t\tlet schemaId: string | undefined;\n\t\tlet runtime: DaemonRuntimeIdentity | undefined;\n\t\tlet supervisorPid: number | undefined;\n\t\tlet supervisorProcessStartId: string | undefined;\n\t\tlet greeted = false;\n\t\ttry {\n\t\t\tconst hello = await client.waitForHello(1500);\n\t\t\tversion = hello.appVersion;\n\t\t\tprotocolVersion = hello.protocol.version;\n\t\t\tschemaId = hello.schemaId;\n\t\t\truntime = hello.runtime;\n\t\t\tsupervisorPid = hello.supervisorPid;\n\t\t\tsupervisorProcessStartId = hello.supervisorProcessStartId;\n\t\t\tgreeted = true;\n\t\t} catch (error) {\n\t\t\tif (error instanceof DaemonProtocolMismatchError) throw error;\n\t\t\t// A silent peer is not evidence of a compatible command plane.\n\t\t}\n\t\tlet sessionCount: number | undefined;\n\t\ttry {\n\t\t\tconst response = await client.request({ type: \"list\" }, greeted ? 30000 : 1500);\n\t\t\tif (response.success) {\n\t\t\t\tconst sessions = (response.data as { sessions?: unknown })?.sessions;\n\t\t\t\tif (Array.isArray(sessions)) {\n\t\t\t\t\tsessionCount = sessions.length;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (error instanceof DaemonProtocolMismatchError) throw error;\n\t\t\t// Leave sessionCount undefined when the daemon will not answer list.\n\t\t}\n\t\treturn {\n\t\t\tversion,\n\t\t\tprotocolVersion,\n\t\t\tschemaId,\n\t\t\truntime,\n\t\t\tsessionCount,\n\t\t\tsupervisorPid,\n\t\t\tsupervisorProcessStartId,\n\t\t\treachable: true,\n\t\t};\n\t} finally {\n\t\tclient.close();\n\t}\n}\n\nfunction classifyReachable(probe: ProbeResult): DaemonStatus {\n\tif (\n\t\tprobe.protocolVersion === DAEMON_PROTOCOL_VERSION &&\n\t\tprobe.schemaId === DAEMON_SCHEMA_ID &&\n\t\tprobe.version === VERSION\n\t) {\n\t\treturn \"current\";\n\t}\n\treturn \"stale\";\n}\n\nexport function verifyHelloSupervisorPid(\n\tpid: number | undefined,\n\texpectedProcessStartId: string | undefined,\n): number | undefined {\n\tif (!Number.isInteger(pid) || pid === undefined || pid <= 0) {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code !== \"EPERM\") {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\tif (expectedProcessStartId) {\n\t\tconst observedStartId = getProcessStartId(pid);\n\t\tif (observedStartId !== expectedProcessStartId) {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\treturn pid;\n}\n\n/** Discover daemons in this state root and probe each for version + session count. */\nexport async function discoverDaemons(): Promise<DaemonInfo[]> {\n\tconst processBySocket = new Map<string, DiscoveredDaemonProcess>();\n\tfor (const daemon of scanListeningDaemons()) {\n\t\tif (isWorkerSocketPath(daemon.socketPath)) {\n\t\t\tcontinue;\n\t\t}\n\t\tprocessBySocket.set(daemon.socketPath, daemon);\n\t}\n\n\tconst workerSockets = new Set(\n\t\tfindAllTrackedWorkers().map((worker) => normalizeSocketPath(worker.descriptor.supervisorSocketPath)),\n\t);\n\tconst sockets = new Set<string>([\n\t\t...processBySocket.keys(),\n\t\t...scanSocketDir().filter((socketPath) => !isWorkerSocketPath(socketPath)),\n\t\t...workerSockets,\n\t]);\n\tconst defaultSocket = normalizeSocketPath(defaultDaemonSocketPath());\n\n\tconst infos = await Promise.all(\n\t\t[...sockets].map(async (socketPath): Promise<DaemonInfo> => {\n\t\t\tconst proc = processBySocket.get(socketPath);\n\t\t\tconst probe = await probeDaemon(socketPath);\n\t\t\tconst pid = proc?.pid ?? verifyHelloSupervisorPid(probe.supervisorPid, probe.supervisorProcessStartId);\n\t\t\tconst hasTrackedWorkers = workerSockets.has(socketPath);\n\t\t\tconst status: DaemonStatus = probe.reachable\n\t\t\t\t? classifyReachable(probe)\n\t\t\t\t: proc || hasTrackedWorkers\n\t\t\t\t\t? \"unreachable\"\n\t\t\t\t\t: \"orphan-file\";\n\t\t\treturn {\n\t\t\t\tsocketPath,\n\t\t\t\tpid,\n\t\t\t\tuptimeSeconds: proc?.uptimeSeconds,\n\t\t\t\tversion: probe.version,\n\t\t\t\tprotocolVersion: probe.protocolVersion,\n\t\t\t\tschemaId: probe.schemaId,\n\t\t\t\tbuildId: probe.runtime?.buildId,\n\t\t\t\texecutablePath:\n\t\t\t\t\tprobe.runtime?.launcherPath ?? probe.runtime?.entrypointPath ?? probe.runtime?.executablePath,\n\t\t\t\t...(pid !== undefined ? { pidSource: proc ? (\"listener\" as const) : (\"hello\" as const) } : {}),\n\t\t\t\tsessionCount: probe.sessionCount,\n\t\t\t\tstatus,\n\t\t\t\tisDefault: socketPath === defaultSocket,\n\t\t\t\t...(hasTrackedWorkers ? { hasTrackedWorkers: true } : {}),\n\t\t\t};\n\t\t}),\n\t);\n\n\treturn sortDaemons(infos);\n}\n\nexport function sortDaemons(infos: DaemonInfo[]): DaemonInfo[] {\n\treturn [...infos].sort((left, right) => {\n\t\tif (left.isDefault !== right.isDefault) {\n\t\t\treturn left.isDefault ? -1 : 1;\n\t\t}\n\t\tconst statusDelta = STATUS_ORDER[left.status] - STATUS_ORDER[right.status];\n\t\treturn statusDelta || left.socketPath.localeCompare(right.socketPath);\n\t});\n}\n\nexport async function runPs(json: boolean, report?: ProductDiagnostics): Promise<void> {\n\tconst daemons = await discoverDaemons();\n\tif (json) {\n\t\tconsole.log(JSON.stringify(report ? { ...report, daemons } : daemons, null, 2));\n\t\treturn;\n\t}\n\tif (report) console.log(formatProductDiagnostics(report));\n\tif (daemons.length === 0) {\n\t\tconsole.log(\"No background services found.\");\n\t\treturn;\n\t}\n\tconsole.log(formatDaemonListTable(daemons));\n}\n\nexport type ReapAction =\n\t| { kind: \"remove-file\"; daemon: DaemonInfo }\n\t| { kind: \"kill\"; daemon: DaemonInfo }\n\t| { kind: \"shutdown\"; daemon: DaemonInfo }\n\t| { kind: \"skip\"; daemon: DaemonInfo; reason: string };\n\n/**\n * Decide what to do with each discovered daemon (pure, no side effects). Reap\n * targets only clearly-safe daemons: orphaned socket files (including a stale\n * default daemon.sock with no live process), and reachable idle daemons on\n * non-default sockets. A reachable default daemon, and any reachable daemon with\n * live sessions, are never touched.\n *\n * Unreachable (hung) daemons can't report a session count, so they are only\n * ever killed with `force`, and even then never via a pid that backs more than\n * one discovered daemon (e.g. macOS lsof reports every unix socket a process\n * holds, so killing a shared pid could take down a reachable daemon with live\n * sessions). runReap additionally re-probes a kill candidate immediately before\n * the SIGTERM and backs off to the session-aware paths if it has since become\n * reachable, so a daemon that recovered with live sessions is never killed.\n */\nexport function planReap(daemons: readonly DaemonInfo[], force: boolean): ReapAction[] {\n\tconst pidCounts = new Map<number, number>();\n\tfor (const daemon of daemons) {\n\t\tif (daemon.pid !== undefined) {\n\t\t\tpidCounts.set(daemon.pid, (pidCounts.get(daemon.pid) ?? 0) + 1);\n\t\t}\n\t}\n\n\treturn daemons.map((daemon): ReapAction => {\n\t\t// An orphan socket file has no owning process, so removing it is safe even\n\t\t// on the default path (a stale daemon.sock left by a crash). Decide this\n\t\t// before the default guard so a dead default socket still gets cleaned up.\n\t\tif (daemon.status === \"orphan-file\") {\n\t\t\treturn { kind: \"remove-file\", daemon };\n\t\t}\n\t\tif (daemon.protocolVersion !== undefined && daemon.protocolVersion !== DAEMON_PROTOCOL_VERSION) {\n\t\t\treturn {\n\t\t\t\tkind: \"skip\",\n\t\t\t\tdaemon,\n\t\t\t\treason: `Keeping incompatible daemon protocol ${daemon.protocolVersion}; canonical session ownership requires protocol ${DAEMON_PROTOCOL_VERSION}. Use its own runtime for cleanup.`,\n\t\t\t};\n\t\t}\n\t\tif (daemon.isDefault) {\n\t\t\treturn { kind: \"skip\", daemon, reason: \"default background service\" };\n\t\t}\n\t\tif (daemon.status === \"unreachable\") {\n\t\t\tif (!force || daemon.pid === undefined) {\n\t\t\t\treturn { kind: \"skip\", daemon, reason: 'unreachable; use \"base-context shutdown --force\" to stop it' };\n\t\t\t}\n\t\t\tif ((pidCounts.get(daemon.pid) ?? 0) > 1) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"skip\",\n\t\t\t\t\tdaemon,\n\t\t\t\t\treason: `unreachable; pid ${daemon.pid} also backs another daemon, not killing`,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn { kind: \"kill\", daemon };\n\t\t}\n\t\tif (daemon.sessionCount !== 0) {\n\t\t\treturn { kind: \"skip\", daemon, reason: `has ${daemon.sessionCount ?? \"unknown\"} session(s)` };\n\t\t}\n\t\treturn { kind: \"shutdown\", daemon };\n\t});\n}\n\nexport function planShutdownAll(daemons: readonly DaemonInfo[], force: boolean): ReapAction[] {\n\treturn daemons.map((daemon): ReapAction => {\n\t\tif (daemon.status === \"orphan-file\") {\n\t\t\treturn { kind: \"remove-file\", daemon };\n\t\t}\n\t\tif (daemon.protocolVersion !== undefined && daemon.protocolVersion !== DAEMON_PROTOCOL_VERSION) {\n\t\t\treturn {\n\t\t\t\tkind: \"skip\",\n\t\t\t\tdaemon,\n\t\t\t\treason: `Keeping incompatible daemon protocol ${daemon.protocolVersion}; canonical session ownership requires protocol ${DAEMON_PROTOCOL_VERSION}. Use its own runtime for cleanup.`,\n\t\t\t};\n\t\t}\n\t\tif (daemon.status === \"unreachable\") {\n\t\t\tif (daemon.pid === undefined) {\n\t\t\t\treturn force || !daemon.hasTrackedWorkers\n\t\t\t\t\t? { kind: \"remove-file\", daemon }\n\t\t\t\t\t: { kind: \"skip\", daemon, reason: \"has unreachable workers; use --force to kill\" };\n\t\t\t}\n\t\t\treturn force ? { kind: \"kill\", daemon } : { kind: \"skip\", daemon, reason: \"unreachable; use --force to kill\" };\n\t\t}\n\t\treturn { kind: \"shutdown\", daemon };\n\t});\n}\n\nconst SHUTDOWN_ALL_ACTION_ORDER: Record<ReapAction[\"kind\"], number> = {\n\tshutdown: 0,\n\t\"remove-file\": 1,\n\tkill: 2,\n\tskip: 3,\n};\n\nexport type ShutdownConfirmationPlan = \"none\" | \"prompt\" | \"json-error\" | \"tty-error\";\n\nexport function planShutdownConfirmation(\n\tdaemonCount: number,\n\tjson: boolean,\n\tforce: boolean,\n\tstdinIsTTY: boolean | undefined,\n): ShutdownConfirmationPlan {\n\tif (daemonCount === 0 || force) return \"none\";\n\tif (json) return \"json-error\";\n\treturn stdinIsTTY ? \"prompt\" : \"tty-error\";\n}\n\nexport async function runShutdownAll(json: boolean, force: boolean): Promise<void> {\n\tconst daemons = await discoverDaemons();\n\tswitch (planShutdownConfirmation(daemons.length, json, force, process.stdin.isTTY)) {\n\t\tcase \"json-error\":\n\t\t\tprocess.exitCode = 1;\n\t\t\tconsole.log(\n\t\t\t\tJSON.stringify(\n\t\t\t\t\t{\n\t\t\t\t\t\tstopped: [],\n\t\t\t\t\t\tfailed: daemons.map(({ socketPath }) => ({\n\t\t\t\t\t\t\tsocketPath,\n\t\t\t\t\t\t\treason: 'confirmation required; use \"base-context shutdown --force --json\"',\n\t\t\t\t\t\t})),\n\t\t\t\t\t},\n\t\t\t\t\tnull,\n\t\t\t\t\t2,\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn;\n\t\tcase \"tty-error\":\n\t\t\tthrow new Error(\n\t\t\t\t'Shutdown requires confirmation in an interactive terminal. Use \"base-context shutdown --force\".',\n\t\t\t);\n\t\tcase \"prompt\": {\n\t\t\tconst confirmed = await promptYesNo(\n\t\t\t\t\"Stop every agent and background service? Active work will be interrupted.\",\n\t\t\t);\n\t\t\tif (!confirmed) {\n\t\t\t\tconsole.log(chalk.dim(\"Shutdown cancelled.\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase \"none\":\n\t\t\tbreak;\n\t}\n\tconst admission = await acquireDaemonShutdownAdmission();\n\ttry {\n\t\tawait runShutdownAllConverging(json, force, () => admission.assertOrRenew());\n\t} finally {\n\t\tawait admission.release();\n\t}\n}\n\nasync function runShutdownAllConverging(\n\tjson: boolean,\n\tforce: boolean,\n\tassertAdmission: () => Promise<void>,\n): Promise<void> {\n\tconst stopped: Array<{ socketPath: string; action: string }> = [];\n\tconst failed: Array<{ socketPath: string; reason: string }> = [];\n\tconst handledPids = new Set<number>();\n\tconst reportedFailures = new Set<string>();\n\n\tif (force) {\n\t\tawait stopHiddenSupervisors(stopped, failed, handledPids, reportedFailures, assertAdmission);\n\t}\n\tconst daemons = (await discoverDaemons()).filter((daemon) => !isWorkerSocketPath(daemon.socketPath));\n\n\tconst actions = [...planShutdownAll(daemons, force)].sort(\n\t\t(left, right) => SHUTDOWN_ALL_ACTION_ORDER[left.kind] - SHUTDOWN_ALL_ACTION_ORDER[right.kind],\n\t);\n\n\tfor (const action of actions) {\n\t\tconst { socketPath, pid } = action.daemon;\n\t\tif (pid !== undefined && handledPids.has(pid)) {\n\t\t\tawait assertAdmission();\n\t\t\tremoveSocketFile(socketPath);\n\t\t\tstopped.push({ socketPath, action: `background service already stopped (pid ${pid})` });\n\t\t\tif (force) {\n\t\t\t\tfailed.push(\n\t\t\t\t\t...(await forceStopTrackedWorkers(socketPath, assertAdmission)).map((reason) => ({\n\t\t\t\t\t\tsocketPath,\n\t\t\t\t\t\treason,\n\t\t\t\t\t})),\n\t\t\t\t);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tswitch (action.kind) {\n\t\t\tcase \"remove-file\": {\n\t\t\t\tif ((await probeDaemon(socketPath)).reachable) {\n\t\t\t\t\tapply(\n\t\t\t\t\t\tawait stopBackgroundService(socketPath, pid, handledPids, force, assertAdmission),\n\t\t\t\t\t\tsocketPath,\n\t\t\t\t\t\tstopped,\n\t\t\t\t\t\tfailed,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tawait assertAdmission();\n\t\t\t\t\tif (removeSocketFile(socketPath)) {\n\t\t\t\t\t\tstopped.push({ socketPath, action: \"removed stale socket file\" });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfailed.push({ socketPath, reason: \"could not remove socket file\" });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"kill\": {\n\t\t\t\tif ((await probeDaemon(socketPath)).reachable) {\n\t\t\t\t\tapply(\n\t\t\t\t\t\tawait stopBackgroundService(socketPath, pid, handledPids, force, assertAdmission),\n\t\t\t\t\t\tsocketPath,\n\t\t\t\t\t\tstopped,\n\t\t\t\t\t\tfailed,\n\t\t\t\t\t);\n\t\t\t\t} else if (isDaemonProcessListening(pid!, socketPath)) {\n\t\t\t\t\tawait assertAdmission();\n\t\t\t\t\tconst refusal = await forceKillDaemon(pid!, socketPath);\n\t\t\t\t\tif (refusal) {\n\t\t\t\t\t\tfailed.push({ socketPath, reason: refusal });\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\thandledPids.add(pid!);\n\t\t\t\t\tawait assertAdmission();\n\t\t\t\t\tremoveSocketFile(socketPath);\n\t\t\t\t\tstopped.push({ socketPath, action: `killed unreachable background service (pid ${pid})` });\n\t\t\t\t} else if (isProcessAlive(pid!)) {\n\t\t\t\t\tfailed.push({\n\t\t\t\t\t\tsocketPath,\n\t\t\t\t\t\treason: `Keeping live daemon pid ${pid}: compatibility is unknown/unverified`,\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tawait assertAdmission();\n\t\t\t\t\tremoveSocketFile(socketPath);\n\t\t\t\t\tstopped.push({ socketPath, action: \"background service already stopped\" });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"shutdown\":\n\t\t\t\tapply(\n\t\t\t\t\tawait stopBackgroundService(socketPath, pid, handledPids, force, assertAdmission),\n\t\t\t\t\tsocketPath,\n\t\t\t\t\tstopped,\n\t\t\t\t\tfailed,\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"skip\":\n\t\t\t\tfailed.push({ socketPath, reason: action.reason });\n\t\t\t\tbreak;\n\t\t}\n\t\tif (force && action.kind !== \"skip\") {\n\t\t\tfailed.push(\n\t\t\t\t...(await forceStopTrackedWorkers(socketPath, assertAdmission)).map((reason) => ({ socketPath, reason })),\n\t\t\t);\n\t\t}\n\t}\n\n\tif (force) {\n\t\tawait terminateVerifiedResiduals(stopped, failed, handledPids, reportedFailures, assertAdmission);\n\t}\n\n\tif (json) {\n\t\tif (failed.length > 0) {\n\t\t\tprocess.exitCode = 1;\n\t\t}\n\t\tconsole.log(JSON.stringify({ stopped, failed }, null, 2));\n\t\treturn;\n\t}\n\tif (stopped.length === 0 && failed.length === 0) {\n\t\tconsole.log(\"No background services found.\");\n\t\treturn;\n\t}\n\tfor (const entry of stopped) {\n\t\tconsole.log(chalk.green(`stopped ${entry.socketPath}: ${entry.action}`));\n\t}\n\tfor (const entry of failed) {\n\t\tconsole.log(chalk.red(`failed ${entry.socketPath}: ${entry.reason}`));\n\t}\n\tif (failed.length > 0) {\n\t\tprocess.exitCode = 1;\n\t}\n}\n\nasync function stopHiddenSupervisors(\n\tstopped: Array<{ socketPath: string; action: string }>,\n\tfailed: Array<{ socketPath: string; reason: string }>,\n\thandledPids: Set<number>,\n\treportedFailures: Set<string>,\n\tassertAdmission: () => Promise<void>,\n): Promise<void> {\n\twhile (true) {\n\t\tconst listeners = scanListeningDaemons().filter((listener) => !isWorkerSocketPath(listener.socketPath));\n\t\tconst bySocket = new Map<string, DiscoveredDaemonProcess[]>();\n\t\tfor (const listener of listeners) {\n\t\t\tconst group = bySocket.get(listener.socketPath) ?? [];\n\t\t\tgroup.push(listener);\n\t\t\tbySocket.set(listener.socketPath, group);\n\t\t}\n\t\tconst hidden: DiscoveredDaemonProcess[] = [];\n\t\tfor (const [socketPath, group] of bySocket) {\n\t\t\tif (new Set(group.map((listener) => listener.pid)).size < 2) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst currentPid = (await probeDaemon(socketPath)).supervisorPid;\n\t\t\tif (currentPid === undefined || !group.some((listener) => listener.pid === currentPid)) {\n\t\t\t\trecordShutdownFailure(\n\t\t\t\t\tfailed,\n\t\t\t\t\treportedFailures,\n\t\t\t\t\tsocketPath,\n\t\t\t\t\t\"could not identify the current same-path daemon\",\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\thidden.push(...group.filter((listener) => listener.pid !== currentPid));\n\t\t}\n\t\tif (hidden.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst before = daemonListenerSignature(hidden);\n\t\tfor (const listener of hidden) {\n\t\t\tif (await terminateVerifiedListener(listener, failed, reportedFailures, assertAdmission)) {\n\t\t\t\thandledPids.add(listener.pid);\n\t\t\t\tstopped.push({ socketPath: listener.socketPath, action: `stopped hidden daemon (pid ${listener.pid})` });\n\t\t\t}\n\t\t}\n\t\tconst afterHidden = scanListeningDaemons().filter(\n\t\t\t(listener) =>\n\t\t\t\t!isWorkerSocketPath(listener.socketPath) &&\n\t\t\t\thidden.some((candidate) => candidate.pid === listener.pid && candidate.socketPath === listener.socketPath),\n\t\t);\n\t\tif (afterHidden.length === 0 || daemonListenerSignature(afterHidden) === before) {\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nasync function terminateVerifiedResiduals(\n\tstopped: Array<{ socketPath: string; action: string }>,\n\tfailed: Array<{ socketPath: string; reason: string }>,\n\thandledPids: Set<number>,\n\treportedFailures: Set<string>,\n\tassertAdmission: () => Promise<void>,\n): Promise<void> {\n\tlet previousSignature: string | undefined;\n\tlet quietSince: number | undefined;\n\tconst deadline = Date.now() + SHUTDOWN_CONVERGENCE_TIMEOUT_MS;\n\twhile (true) {\n\t\tawait assertAdmission();\n\t\tconst listeners = scanListeningDaemons();\n\t\tconst now = Date.now();\n\t\tif (listeners.length === 0) {\n\t\t\tpreviousSignature = undefined;\n\t\t\tquietSince ??= now;\n\t\t\tconst quietPeriod = evaluateShutdownQuietPeriod(now, quietSince);\n\t\t\tif (quietPeriod === \"complete\") {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait delay(100);\n\t\t\tcontinue;\n\t\t}\n\t\tquietSince = undefined;\n\t\tconst signature = daemonListenerSignature(listeners);\n\t\tif (now >= deadline) {\n\t\t\trecordResidualListenerFailures(listeners, failed, reportedFailures, \"kept respawning during shutdown\");\n\t\t\treturn;\n\t\t}\n\t\tif (signature === previousSignature) {\n\t\t\trecordResidualListenerFailures(listeners, failed, reportedFailures, \"remained after shutdown\");\n\t\t\treturn;\n\t\t}\n\t\tpreviousSignature = signature;\n\t\tconst seenPids = new Set<number>();\n\t\tfor (const listener of listeners) {\n\t\t\tif (seenPids.has(listener.pid)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tseenPids.add(listener.pid);\n\t\t\tconst alreadyReported = handledPids.has(listener.pid);\n\t\t\tif (await terminateVerifiedListener(listener, failed, reportedFailures, assertAdmission)) {\n\t\t\t\thandledPids.add(listener.pid);\n\t\t\t\tif (!alreadyReported) {\n\t\t\t\t\tstopped.push({\n\t\t\t\t\t\tsocketPath: listener.socketPath,\n\t\t\t\t\t\taction: `stopped residual daemon process (pid ${listener.pid})`,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction recordResidualListenerFailures(\n\tlisteners: readonly DiscoveredDaemonProcess[],\n\tfailed: Array<{ socketPath: string; reason: string }>,\n\treportedFailures: Set<string>,\n\treason: string,\n): void {\n\tfor (const listener of listeners) {\n\t\tconst processStartId = getProcessStartId(listener.pid);\n\t\tconst identity = processStartId\n\t\t\t? `pid ${listener.pid}, start ${processStartId}`\n\t\t\t: `pid ${listener.pid}, process identity unavailable`;\n\t\trecordShutdownFailure(\n\t\t\tfailed,\n\t\t\treportedFailures,\n\t\t\tlistener.socketPath,\n\t\t\t`daemon ${reason} (${identity})${describeDaemonParent(listener.pid)}`,\n\t\t);\n\t}\n}\nfunction describeDaemonParent(pid: number): string {\n\tconst result = spawnSync(\"ps\", [\"-o\", \"ppid=,tty=,command=\", \"-p\", String(pid)], { encoding: \"utf8\" });\n\tif (result.error || result.status !== 0 || typeof result.stdout !== \"string\") {\n\t\treturn \"\";\n\t}\n\tconst match = result.stdout.trim().match(/^(\\d+)\\s+(\\S+)\\s+(.+)$/);\n\tif (!match) {\n\t\treturn \"\";\n\t}\n\treturn `; close parent PID ${match[1]} on ${match[2]} (${match[3]}) and retry shutdown`;\n}\n\nasync function daemonCleanupRefusal(\n\tsocketPath: string,\n\tpid: number,\n\tprocessStartId: string | undefined,\n\tworker = isWorkerSocketPath(socketPath),\n): Promise<string | undefined> {\n\tconst unknown = `Keeping live daemon pid ${pid}: compatibility is unknown/unverified`;\n\tif (!processStartId || getProcessStartId(pid) !== processStartId) return unknown;\n\tconst ownsSocket = () => {\n\t\tconst owners = scanListeningDaemons().filter(\n\t\t\t(listener) => listener.socketPath === normalizeSocketPath(socketPath),\n\t\t);\n\t\treturn owners.length > 0 && owners.every((listener) => listener.pid === pid);\n\t};\n\tconst socketOwnedAtEntry = ownsSocket();\n\tconst client = worker ? new DaemonWorkerClient(socketPath) : new DaemonClient(socketPath);\n\ttry {\n\t\tawait client.connect(300);\n\t\tconst hello = await client.waitForHello(1500);\n\t\tconst helloIdentifiesOwner =\n\t\t\t!worker && hello.supervisorPid === pid && hello.supervisorProcessStartId === processStartId;\n\t\tif (\n\t\t\tgetProcessStartId(pid) !== processStartId ||\n\t\t\t(!helloIdentifiesOwner && !(socketOwnedAtEntry && ownsSocket()))\n\t\t) {\n\t\t\treturn unknown;\n\t\t}\n\t\tconst missing = NATIVE_WORK_COMPATIBILITIES.find(\n\t\t\t(requirement) => !meetsDaemonCommandCompatibility(hello, requirement),\n\t\t);\n\t\treturn missing ? new DaemonCapabilityUnavailableError(\"shutdown\", missing.capability).message : undefined;\n\t} catch (error) {\n\t\tif (\n\t\t\tsocketOwnedAtEntry &&\n\t\t\townsSocket() &&\n\t\t\tgetProcessStartId(pid) === processStartId &&\n\t\t\t(error instanceof DaemonProtocolMismatchError || error instanceof DaemonWorkerCompatibilityError)\n\t\t) {\n\t\t\treturn error.message;\n\t\t}\n\t\treturn `${unknown}: ${error instanceof Error ? error.message : String(error)}`;\n\t} finally {\n\t\tclient.close();\n\t}\n}\n\nasync function terminateVerifiedListener(\n\tlistener: DiscoveredDaemonProcess,\n\tfailed: Array<{ socketPath: string; reason: string }>,\n\treportedFailures: Set<string>,\n\tassertAdmission: () => Promise<void>,\n): Promise<boolean> {\n\tconst processStartId = getProcessStartId(listener.pid);\n\tif (!processStartId) {\n\t\trecordShutdownFailure(\n\t\t\tfailed,\n\t\t\treportedFailures,\n\t\t\tlistener.socketPath,\n\t\t\t`could not verify daemon process identity (pid ${listener.pid})`,\n\t\t);\n\t\treturn false;\n\t}\n\tconst refusal = await daemonCleanupRefusal(listener.socketPath, listener.pid, processStartId);\n\tif (refusal) {\n\t\trecordShutdownFailure(failed, reportedFailures, listener.socketPath, refusal);\n\t\treturn false;\n\t}\n\tif (getProcessStartId(listener.pid) !== processStartId) {\n\t\treturn false;\n\t}\n\tawait assertAdmission();\n\tif (getProcessStartId(listener.pid) !== processStartId) {\n\t\treturn false;\n\t}\n\tkillDaemon(listener.pid);\n\tconst deadline = Date.now() + 1000;\n\twhile (getProcessStartId(listener.pid) === processStartId && Date.now() < deadline) {\n\t\tawait delay(50);\n\t}\n\tif (getProcessStartId(listener.pid) === processStartId) {\n\t\tawait assertAdmission();\n\t\tif (getProcessStartId(listener.pid) !== processStartId) {\n\t\t\treturn false;\n\t\t}\n\t\ttry {\n\t\t\tprocess.kill(listener.pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\t// The verified process exited between the identity check and signal.\n\t\t}\n\t}\n\treturn getProcessStartId(listener.pid) !== processStartId;\n}\n\nfunction daemonListenerSignature(listeners: readonly DiscoveredDaemonProcess[]): string {\n\treturn listeners\n\t\t.map((listener) => `${listener.pid}:${getProcessStartId(listener.pid) ?? \"unknown\"}:${listener.socketPath}`)\n\t\t.sort()\n\t\t.join(\"\\n\");\n}\n\nfunction recordShutdownFailure(\n\tfailed: Array<{ socketPath: string; reason: string }>,\n\treportedFailures: Set<string>,\n\tsocketPath: string,\n\treason: string,\n): void {\n\tconst key = `${socketPath}\\0${reason}`;\n\tif (reportedFailures.has(key)) {\n\t\treturn;\n\t}\n\treportedFailures.add(key);\n\tfailed.push({ socketPath, reason });\n}\n\nexport function isWorkerSocketPath(socketPath: string): boolean {\n\treturn (\n\t\tprocess.platform !== \"win32\" &&\n\t\tresolve(dirname(socketPath)) === resolve(defaultDaemonSocketDir()) &&\n\t\tbasename(socketPath).startsWith(\"worker-\") &&\n\t\tbasename(socketPath).endsWith(\".sock\")\n\t);\n}\n\nasync function stopBackgroundService(\n\tsocketPath: string,\n\tpid: number | undefined,\n\thandledPids: Set<number>,\n\tforce: boolean,\n\tassertAdmission: () => Promise<void>,\n): Promise<ReapOutcome> {\n\tawait assertAdmission();\n\tif (pid !== undefined && isProcessAlive(pid)) {\n\t\tconst refusal = await daemonCleanupRefusal(socketPath, pid, getProcessStartId(pid));\n\t\tif (refusal) return { skipped: refusal };\n\t}\n\tif (await shutdownDaemon(socketPath, force)) {\n\t\tif (pid !== undefined) {\n\t\t\thandledPids.add(pid);\n\t\t}\n\t\treturn { reaped: `stopped background service${pid ? ` (pid ${pid})` : \"\"}` };\n\t}\n\tif (!(await canConnectToSocket(socketPath, 250))) {\n\t\tif (pid !== undefined && isProcessAlive(pid)) {\n\t\t\treturn { skipped: `Keeping live daemon pid ${pid}: compatibility is unknown/unverified` };\n\t\t}\n\t\tawait assertAdmission();\n\t\tremoveSocketFile(socketPath);\n\t\treturn { reaped: \"background service already stopped\" };\n\t}\n\tif (pid === undefined) {\n\t\treturn { skipped: \"still listening but no pid to kill\" };\n\t}\n\tif (!force) {\n\t\treturn { skipped: \"did not stop gracefully; retry with --force\" };\n\t}\n\tawait assertAdmission();\n\tconst refusal = await forceKillDaemon(pid, socketPath);\n\tif (refusal) return { skipped: refusal };\n\thandledPids.add(pid);\n\tawait assertAdmission();\n\tremoveSocketFile(socketPath);\n\treturn { reaped: `force-killed unresponsive background service (pid ${pid})` };\n}\n\ninterface TrackedWorker {\n\tdescriptor: DaemonWorkerDescriptor;\n\tdescriptorPath: string;\n}\n\nasync function forceStopTrackedWorkers(\n\tsupervisorSocketPath: string,\n\tassertAdmission: () => Promise<void>,\n): Promise<string[]> {\n\tconst failures: string[] = [];\n\tfor (const worker of findTrackedWorkers(supervisorSocketPath)) {\n\t\tconst { descriptor } = worker;\n\t\tif (isProcessAlive(descriptor.pid)) {\n\t\t\tconst refusal = await daemonCleanupRefusal(\n\t\t\t\tdescriptor.socketPath,\n\t\t\t\tdescriptor.pid,\n\t\t\t\tdescriptor.processStartId,\n\t\t\t\ttrue,\n\t\t\t);\n\t\t\tif (refusal) {\n\t\t\t\tfailures.push(`Worker ${descriptor.workerId}: ${refusal}`);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tlet cleanupWorkerRecords = await stopTrackedProcess(descriptor.pid, descriptor.processStartId, assertAdmission);\n\t\tif (!cleanupWorkerRecords) {\n\t\t\tfailures.push(`could not safely stop worker ${descriptor.workerId} (pid ${descriptor.pid})`);\n\t\t}\n\t\tif (descriptor.orphanProcessJournalPath) {\n\t\t\tlet orphans: ReturnType<typeof readActiveOrphanProcesses> = [];\n\t\t\ttry {\n\t\t\t\torphans = readActiveOrphanProcesses(descriptor.orphanProcessJournalPath, descriptor.pid);\n\t\t\t} catch (error) {\n\t\t\t\tcleanupWorkerRecords = false;\n\t\t\t\tfailures.push(`could not read child process records for worker ${descriptor.workerId}: ${String(error)}`);\n\t\t\t}\n\t\t\tfor (const orphan of orphans) {\n\t\t\t\t// Pid-only records go through the platform predicate (stopTrackedProcess needs a startId).\n\t\t\t\tif (orphan.processStartId === undefined) {\n\t\t\t\t\tif (shouldReapOrphanProcess(orphan)) {\n\t\t\t\t\t\tkillOrphanProcess(orphan.pid);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (!isOrphanProcessIdentityCurrent(orphan)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (process.platform === \"win32\") {\n\t\t\t\t\t// taskkill /T, like the sibling reapers: signalling only the shell pid leaves its descendants alive.\n\t\t\t\t\tawait assertAdmission();\n\t\t\t\t\tif (isOrphanProcessIdentityCurrent(orphan)) {\n\t\t\t\t\t\tkillOrphanProcess(orphan.pid);\n\t\t\t\t\t\tif (isProcessAlive(orphan.pid)) {\n\t\t\t\t\t\t\tcleanupWorkerRecords = false;\n\t\t\t\t\t\t\tfailures.push(`could not stop child process ${orphan.pid} for worker ${descriptor.workerId}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (!(await stopTrackedProcess(orphan.pid, orphan.processStartId, assertAdmission))) {\n\t\t\t\t\tcleanupWorkerRecords = false;\n\t\t\t\t\tfailures.push(`could not stop child process ${orphan.pid} for worker ${descriptor.workerId}`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (cleanupWorkerRecords) {\n\t\t\ttry {\n\t\t\t\tremoveSocketFile(descriptor.socketPath);\n\t\t\t\trmSync(worker.descriptorPath, { force: true });\n\t\t\t\trmSync(descriptor.recoveryJournalPath, { force: true });\n\t\t\t\tif (descriptor.orphanProcessJournalPath) {\n\t\t\t\t\trmSync(descriptor.orphanProcessJournalPath, { force: true });\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tfailures.push(`could not clean up worker ${descriptor.workerId}: ${String(error)}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn failures;\n}\n\nfunction findTrackedWorkers(supervisorSocketPath: string): TrackedWorker[] {\n\treturn findAllTrackedWorkers().filter(\n\t\t(worker) =>\n\t\t\tnormalizeSocketPath(worker.descriptor.supervisorSocketPath) === normalizeSocketPath(supervisorSocketPath),\n\t);\n}\n\nfunction findAllTrackedWorkers(): TrackedWorker[] {\n\tconst root = join(getAgentDir(), \"daemon-workers\");\n\tif (!existsSync(root)) {\n\t\treturn [];\n\t}\n\tconst workers: TrackedWorker[] = [];\n\tlet directoryNames: string[];\n\ttry {\n\t\tdirectoryNames = readdirSync(root);\n\t} catch {\n\t\treturn [];\n\t}\n\tfor (const directoryName of directoryNames) {\n\t\tconst directory = join(root, directoryName);\n\t\ttry {\n\t\t\tif (!lstatSync(directory).isDirectory()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tlet fileNames: string[];\n\t\ttry {\n\t\t\tfileNames = readdirSync(directory);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const fileName of fileNames) {\n\t\t\tif (!fileName.endsWith(\".json\")) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst descriptorPath = join(directory, fileName);\n\t\t\ttry {\n\t\t\t\tconst value: unknown = JSON.parse(readFileSync(descriptorPath, \"utf8\"));\n\t\t\t\tif (isTrackedWorkerDescriptor(value)) {\n\t\t\t\t\tworkers.push({ descriptor: value, descriptorPath });\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Invalid or concurrently removed descriptors are not safe shutdown targets.\n\t\t\t}\n\t\t}\n\t}\n\treturn workers;\n}\n\nfunction isTrackedWorkerDescriptor(value: unknown): value is DaemonWorkerDescriptor {\n\tif (!value || typeof value !== \"object\") {\n\t\treturn false;\n\t}\n\tconst descriptor = value as Partial<DaemonWorkerDescriptor>;\n\treturn (\n\t\t(descriptor.version === 1 || descriptor.version === 2) &&\n\t\ttypeof descriptor.supervisorSocketPath === \"string\" &&\n\t\ttypeof descriptor.workerId === \"string\" &&\n\t\tNumber.isInteger(descriptor.pid) &&\n\t\t(descriptor.pid ?? 0) > 0 &&\n\t\t(descriptor.processStartId === undefined || typeof descriptor.processStartId === \"string\") &&\n\t\ttypeof descriptor.socketPath === \"string\" &&\n\t\ttypeof descriptor.recoveryJournalPath === \"string\"\n\t);\n}\n\nasync function stopTrackedProcess(\n\tpid: number,\n\texpectedStartId: string | undefined,\n\tassertAdmission: () => Promise<void>,\n): Promise<boolean> {\n\tif (!isProcessAlive(pid)) {\n\t\treturn true;\n\t}\n\tif (!expectedStartId || getProcessStartId(pid) !== expectedStartId) {\n\t\treturn false;\n\t}\n\tawait assertAdmission();\n\tif (getProcessStartId(pid) !== expectedStartId) {\n\t\treturn false;\n\t}\n\tsignalProcessGroupOrProcess(pid, \"SIGTERM\");\n\tlet deadline = Date.now() + 500;\n\twhile (isProcessAlive(pid) && Date.now() < deadline) {\n\t\tawait delay(25);\n\t}\n\tif (!isProcessAlive(pid)) {\n\t\treturn true;\n\t}\n\tawait assertAdmission();\n\tif (getProcessStartId(pid) !== expectedStartId) {\n\t\treturn false;\n\t}\n\tsignalProcessGroupOrProcess(pid, \"SIGKILL\");\n\tdeadline = Date.now() + 1000;\n\twhile (isProcessAlive(pid) && Date.now() < deadline) {\n\t\tawait delay(25);\n\t}\n\treturn !isProcessAlive(pid);\n}\n\nexport async function runReap(json: boolean, force: boolean, report?: ProductDiagnostics): Promise<void> {\n\tconst daemons = await discoverDaemons();\n\tconst reaped: Array<{ socketPath: string; action: string }> = [];\n\tconst skipped: Array<{ socketPath: string; reason: string }> = [];\n\n\tfor (const action of planReap(daemons, force)) {\n\t\tconst { socketPath, pid } = action.daemon;\n\t\tswitch (action.kind) {\n\t\t\tcase \"skip\":\n\t\t\t\tskipped.push({ socketPath, reason: action.reason });\n\t\t\t\tbreak;\n\t\t\tcase \"remove-file\": {\n\t\t\t\t// Re-probe before unlinking: a path marked orphan-file at discovery\n\t\t\t\t// may have since become a live listener. Only remove it if it is\n\t\t\t\t// still unreachable, so we never delete a socket a daemon is using.\n\t\t\t\tif ((await probeDaemon(socketPath)).reachable) {\n\t\t\t\t\tskipped.push({ socketPath, reason: \"now reachable; not removing socket file\" });\n\t\t\t\t} else if (removeSocketFile(socketPath)) {\n\t\t\t\t\treaped.push({ socketPath, action: \"removed stale socket file\" });\n\t\t\t\t} else {\n\t\t\t\t\tskipped.push({ socketPath, reason: \"could not remove socket file\" });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"kill\": {\n\t\t\t\t// Re-probe right before killing: discovery and this kill happen at\n\t\t\t\t// different moments, so a daemon classified \"unreachable\" may have\n\t\t\t\t// since started answering. Never SIGTERM one that now responds;\n\t\t\t\t// defer to the session-aware shutdown path instead.\n\t\t\t\tconst recheck = await probeDaemon(socketPath);\n\t\t\t\tif (!recheck.reachable) {\n\t\t\t\t\tif (isProcessAlive(pid!)) {\n\t\t\t\t\t\tconst processStartId = getProcessStartId(pid!);\n\t\t\t\t\t\tconst refusal = await daemonCleanupRefusal(socketPath, pid!, processStartId);\n\t\t\t\t\t\tif (refusal || getProcessStartId(pid!) !== processStartId) {\n\t\t\t\t\t\t\tskipped.push({ socketPath, reason: refusal ?? \"Daemon process identity changed before cleanup\" });\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tkillDaemon(pid!);\n\t\t\t\t\tremoveSocketFile(socketPath);\n\t\t\t\t\treaped.push({ socketPath, action: `killed unreachable daemon (pid ${pid})` });\n\t\t\t\t} else {\n\t\t\t\t\tapply(await reapReachableDaemon(socketPath, pid), socketPath, reaped, skipped);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"shutdown\":\n\t\t\t\tapply(await reapReachableDaemon(socketPath, pid), socketPath, reaped, skipped);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tif (json) {\n\t\tconsole.log(JSON.stringify({ ...report, reaped, skipped }, null, 2));\n\t\treturn;\n\t}\n\tif (report) console.log(formatProductDiagnostics(report));\n\tif (reaped.length === 0 && skipped.length === 0) {\n\t\tconsole.log(\"No background services found.\");\n\t\treturn;\n\t}\n\tfor (const entry of reaped) {\n\t\tconsole.log(chalk.green(`reaped ${entry.socketPath}: ${entry.action}`));\n\t}\n\tfor (const entry of skipped) {\n\t\tconsole.log(chalk.dim(`kept ${entry.socketPath}: ${entry.reason}`));\n\t}\n}\n\ntype ReapOutcome = { reaped: string } | { skipped: string };\n\nfunction apply(\n\toutcome: ReapOutcome,\n\tsocketPath: string,\n\treaped: Array<{ socketPath: string; action: string }>,\n\tskipped: Array<{ socketPath: string; reason: string }>,\n): void {\n\tif (\"reaped\" in outcome) {\n\t\treaped.push({ socketPath, action: outcome.reaped });\n\t} else {\n\t\tskipped.push({ socketPath, reason: outcome.skipped });\n\t}\n}\n\n/**\n * Gracefully stop a daemon, but only after a fresh probe confirms it is idle.\n * Discovery and reap happen at different moments, so the session count is\n * re-checked here to avoid stopping a daemon that gained a session in between.\n */\nasync function reapReachableDaemon(socketPath: string, pid: number | undefined): Promise<ReapOutcome> {\n\tconst probe = await probeDaemon(socketPath);\n\tif (!probe.reachable) {\n\t\treturn { skipped: \"no longer reachable\" };\n\t}\n\tif (probe.sessionCount !== 0) {\n\t\treturn { skipped: `now has ${probe.sessionCount ?? \"unknown\"} session(s)` };\n\t}\n\tif (pid !== undefined && isProcessAlive(pid)) {\n\t\tconst refusal = await daemonCleanupRefusal(socketPath, pid, getProcessStartId(pid));\n\t\tif (refusal) return { skipped: refusal };\n\t}\n\treturn (await shutdownDaemon(socketPath, false))\n\t\t? { reaped: `stopped idle background service${pid ? ` (pid ${pid})` : \"\"}` }\n\t\t: { skipped: \"shutdown request failed\" };\n}\n\nfunction removeSocketFile(socketPath: string): boolean {\n\ttry {\n\t\tif (existsSync(socketPath)) {\n\t\t\tunlinkSync(socketPath);\n\t\t}\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction killDaemon(pid: number): void {\n\ttry {\n\t\tprocess.kill(pid, \"SIGTERM\");\n\t} catch {\n\t\t// Process already gone or not permitted; the socket file cleanup still runs.\n\t}\n}\n\nasync function forceKillDaemon(pid: number, socketPath: string): Promise<string | undefined> {\n\tif (!isProcessAlive(pid)) return undefined;\n\tconst processStartId = getProcessStartId(pid);\n\tconst refusal = await daemonCleanupRefusal(socketPath, pid, processStartId);\n\tif (refusal) return refusal;\n\tif (getProcessStartId(pid) !== processStartId) return \"Daemon process identity changed before cleanup\";\n\tkillDaemon(pid);\n\tconst deadline = Date.now() + 1000;\n\twhile (Date.now() < deadline) {\n\t\tif (!isProcessAlive(pid)) return undefined;\n\t\tif (getProcessStartId(pid) !== processStartId) return \"Daemon process identity changed during cleanup\";\n\t\tawait delay(50);\n\t}\n\tif (getProcessStartId(pid) !== processStartId) return \"Daemon process identity changed during cleanup\";\n\ttry {\n\t\tprocess.kill(pid, \"SIGKILL\");\n\t} catch {\n\t\t// Process already exited between the liveness check and the kill.\n\t}\n\treturn undefined;\n}\n\nfunction isProcessAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\nfunction delay(ms: number): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function canConnectToSocket(socketPath: string, timeoutMs: number): Promise<boolean> {\n\tconst client = new DaemonClient(socketPath);\n\ttry {\n\t\tawait client.connect(timeoutMs);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t} finally {\n\t\tclient.close();\n\t}\n}\n\n/**\n * Ask a daemon to shut down and confirm it actually stopped listening. The\n * shutdown ack alone is not proof, so success is reported only once the socket\n * stops accepting connections.\n */\nasync function shutdownDaemon(socketPath: string, force: boolean): Promise<boolean> {\n\tconst client = new DaemonClient(socketPath);\n\ttry {\n\t\tawait client.connect(1000);\n\t} catch {\n\t\tclient.close();\n\t\treturn false;\n\t}\n\ttry {\n\t\tawait client.request({ type: \"shutdown\", force }, 1500);\n\t} catch (error) {\n\t\tif (error instanceof DaemonProtocolMismatchError || error instanceof DaemonCapabilityUnavailableError)\n\t\t\tthrow error;\n\t\tif (!client.hello) {\n\t\t\tthrow new Error(\"Daemon compatibility is unknown/unverified; shutdown was not admitted\", { cause: error });\n\t\t}\n\t\t// The daemon may still stop; the connectivity check below is the source of truth.\n\t} finally {\n\t\tclient.close();\n\t}\n\n\tconst deadline = Date.now() + 5000;\n\twhile (Date.now() < deadline) {\n\t\tif (!(await canConnectToSocket(socketPath, 250))) {\n\t\t\treturn true;\n\t\t}\n\t\tawait delay(50);\n\t}\n\treturn false;\n}\n"]}
|
package/dist/cli/daemon-ps.js
CHANGED
|
@@ -8,6 +8,7 @@ import { getProcessStartId } from "../core/session-lease.js";
|
|
|
8
8
|
import { DaemonCapabilityUnavailableError, DaemonClient, DaemonProtocolMismatchError, } from "../modes/daemon/daemon-client.js";
|
|
9
9
|
import { DAEMON_PROTOCOL_VERSION, DAEMON_SCHEMA_ID, meetsDaemonCommandCompatibility, NATIVE_WORK_COMPATIBILITIES, } from "../modes/daemon/daemon-protocol.js";
|
|
10
10
|
import { defaultDaemonSocketDir, defaultDaemonSocketPath, normalizeSocketPath } from "../modes/daemon/daemon-socket.js";
|
|
11
|
+
import { createDaemonStateRootMatcher } from "../modes/daemon/daemon-state-root.js";
|
|
11
12
|
import { acquireDaemonShutdownAdmission } from "../modes/daemon/daemon-supervisor-ownership.js";
|
|
12
13
|
import { DaemonWorkerClient, DaemonWorkerCompatibilityError } from "../modes/daemon/daemon-worker-client.js";
|
|
13
14
|
import { signalProcessGroupOrProcess } from "../utils/child-process.js";
|
|
@@ -111,6 +112,10 @@ export function parsePsEtimes(stdout) {
|
|
|
111
112
|
return uptimes;
|
|
112
113
|
}
|
|
113
114
|
function scanListeningDaemons() {
|
|
115
|
+
const belongsToStateRoot = createDaemonStateRootMatcher();
|
|
116
|
+
return scanAllListeningDaemons().filter((daemon) => belongsToStateRoot(daemon.socketPath));
|
|
117
|
+
}
|
|
118
|
+
function scanAllListeningDaemons() {
|
|
114
119
|
if (process.platform === "win32") {
|
|
115
120
|
return [];
|
|
116
121
|
}
|
|
@@ -264,7 +269,7 @@ export function verifyHelloSupervisorPid(pid, expectedProcessStartId) {
|
|
|
264
269
|
}
|
|
265
270
|
return pid;
|
|
266
271
|
}
|
|
267
|
-
/** Discover
|
|
272
|
+
/** Discover daemons in this state root and probe each for version + session count. */
|
|
268
273
|
export async function discoverDaemons() {
|
|
269
274
|
const processBySocket = new Map();
|
|
270
275
|
for (const daemon of scanListeningDaemons()) {
|