@code-yeongyu/senpi 2026.9.27-4 → 2026.9.28-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/dist/bundle/chunks/{app-server-command-3GVJZFFH.js → app-server-command-H6LFICZ2.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-4SUOOZZ4.js → azure-openai-responses-FCZNBRWE.js} +1 -1
- package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
- package/dist/bundle/chunks/{chunk-PSCNHY7D.js → chunk-3GMFUEU7.js} +1 -1
- package/dist/bundle/chunks/{chunk-HBG6SQU5.js → chunk-62XHQXCZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-EJKCBRYK.js → chunk-6BWCPOCV.js} +4 -4
- package/dist/bundle/chunks/{chunk-SLL375O7.js → chunk-6VD3EBQE.js} +1 -1
- package/dist/bundle/chunks/chunk-7YUGCTEV.js +15 -0
- package/dist/bundle/chunks/{chunk-FWBSGMAE.js → chunk-A4UMPXQF.js} +1 -1
- package/dist/bundle/chunks/chunk-BBK6SSYT.js +2 -0
- package/dist/bundle/chunks/{chunk-C2425O2Q.js → chunk-CNQ4JB45.js} +1 -1
- package/dist/bundle/chunks/{chunk-27IFXX3B.js → chunk-DRN4FQCE.js} +1 -1
- package/dist/bundle/chunks/{chunk-22MGY5JQ.js → chunk-EHSZ7UCW.js} +1 -1
- package/dist/bundle/chunks/chunk-FHJYIU3U.js +5 -0
- package/dist/bundle/chunks/chunk-GJUCSTAG.js +19 -0
- package/dist/bundle/chunks/chunk-KOPTADA3.js +8 -0
- package/dist/bundle/chunks/{chunk-VMKAYXHL.js → chunk-LSW7QSLV.js} +31 -26
- package/dist/bundle/chunks/{chunk-2BMD37YV.js → chunk-LUPPSEQW.js} +1 -1
- package/dist/bundle/chunks/{chunk-NRGMT74E.js → chunk-QOVUQZO4.js} +1 -1
- package/dist/bundle/chunks/{chunk-IKXPBYSO.js → chunk-SICBVLRH.js} +1 -1
- package/dist/bundle/chunks/chunk-SN4ZHPMN.js +135 -0
- package/dist/bundle/chunks/{chunk-MUTD2NOH.js → chunk-TFA3YAJK.js} +1 -1
- package/dist/bundle/chunks/{chunk-4T4GFAGK.js → chunk-VFSXUT7K.js} +1 -1
- package/dist/bundle/chunks/{cli-main-66NFBKVH.js → cli-main-GYRTYPMG.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-ZTGKZTGU.js → google-generative-ai-LFCBHG3Y.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-CCZ376YK.js → google-vertex-C474FXU7.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-NZ3I2UEN.js → help-fast-path-3U3YDEZ6.js} +1 -1
- package/dist/bundle/chunks/{host-command-RORPAC5X.js → host-command-6CJ7YM6W.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-UDMJGLOR.js → host-lifecycle-RZIMOFDM.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +6 -6
- package/dist/bundle/chunks/interactive-host-runtime-A3GEIFTU.js +4 -0
- package/dist/bundle/chunks/{interactive-mode-LOAADKLH.js → interactive-mode-LX665HTP.js} +1 -1
- package/dist/bundle/chunks/{list-tips-S5CANO5W.js → list-tips-S4U7TKER.js} +1 -1
- package/dist/bundle/chunks/multi-session-host-MFTUM6XQ.js +21 -0
- package/dist/bundle/chunks/{openai-codex-responses-YSGZZDD5.js → openai-codex-responses-2LDIFL6Z.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-KWHRWCBN.js → openai-completions-7KZLPKYP.js} +1 -1
- package/dist/bundle/chunks/{openai-images-6U5FCN5S.js → openai-images-I7DR7ERP.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-7PWMBGYX.js → openai-responses-LCHS3AF3.js} +1 -1
- package/dist/bundle/chunks/{openrouter-images-QIKWQZUD.js → openrouter-images-IPNDRK6C.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-5EEDA35I.js → package-manager-cli-6IAE5WG3.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-KZG4OVR7.js → rotation-stream-ZVJEGA2G.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-HOEA5NGQ.js → rpc-mode-XN3HNZMH.js} +1 -1
- package/dist/bundle/chunks/session-picker-FPKB4DAC.js +2 -0
- package/dist/bundle/chunks/session-worker.js +318 -309
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/host-command.d.ts +8 -1
- package/dist/cli/host-command.d.ts.map +1 -1
- package/dist/cli/host-command.js +66 -14
- package/dist/cli/host-command.js.map +1 -1
- package/dist/core/agent-session.d.ts +18 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +119 -3
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.d.ts +2 -2
- package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +13 -5
- package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js.map +1 -1
- package/dist/core/extensions/builtin/tool-search/native-search.d.ts.map +1 -1
- package/dist/core/extensions/builtin/tool-search/native-search.js +13 -1
- package/dist/core/extensions/builtin/tool-search/native-search.js.map +1 -1
- package/dist/core/retry-fallback/candidates.d.ts +26 -0
- package/dist/core/retry-fallback/candidates.d.ts.map +1 -0
- package/dist/core/retry-fallback/candidates.js +41 -0
- package/dist/core/retry-fallback/candidates.js.map +1 -0
- package/dist/core/retry-fallback/circuit-probes.d.ts +30 -0
- package/dist/core/retry-fallback/circuit-probes.d.ts.map +1 -0
- package/dist/core/retry-fallback/circuit-probes.js +60 -0
- package/dist/core/retry-fallback/circuit-probes.js.map +1 -0
- package/dist/core/retry-fallback/circuit.d.ts +101 -0
- package/dist/core/retry-fallback/circuit.d.ts.map +1 -0
- package/dist/core/retry-fallback/circuit.js +175 -0
- package/dist/core/retry-fallback/circuit.js.map +1 -0
- package/dist/core/retry-fallback/controller-types.d.ts +58 -0
- package/dist/core/retry-fallback/controller-types.d.ts.map +1 -0
- package/dist/core/retry-fallback/controller-types.js +2 -0
- package/dist/core/retry-fallback/controller-types.js.map +1 -0
- package/dist/core/retry-fallback/controller.d.ts +21 -49
- package/dist/core/retry-fallback/controller.d.ts.map +1 -1
- package/dist/core/retry-fallback/controller.js +88 -42
- package/dist/core/retry-fallback/controller.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +25 -17
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-failure-report.d.ts +33 -0
- package/dist/core/session-failure-report.d.ts.map +1 -0
- package/dist/core/session-failure-report.js +62 -0
- package/dist/core/session-failure-report.js.map +1 -0
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +4 -0
- 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 +1 -0
- 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 +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +2 -0
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/help-content.d.ts.map +1 -1
- package/dist/modes/interactive/help-content.js +3 -0
- package/dist/modes/interactive/help-content.js.map +1 -1
- package/dist/modes/interactive/interactive-host-attach.d.ts +10 -0
- package/dist/modes/interactive/interactive-host-attach.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-host-attach.js +14 -0
- package/dist/modes/interactive/interactive-host-attach.js.map +1 -0
- package/dist/modes/interactive/interactive-host-runtime.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-host-runtime.js +3 -6
- package/dist/modes/interactive/interactive-host-runtime.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +12 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +95 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/session-failure-info.d.ts +3 -0
- package/dist/modes/interactive/session-failure-info.d.ts.map +1 -0
- package/dist/modes/interactive/session-failure-info.js +21 -0
- package/dist/modes/interactive/session-failure-info.js.map +1 -0
- package/dist/modes/interactive/tips/catalog/model-tips.d.ts +7 -7
- package/dist/modes/interactive/tips/catalog/model-tips.d.ts.map +1 -1
- package/dist/modes/interactive/tips/catalog/model-tips.js +7 -1
- package/dist/modes/interactive/tips/catalog/model-tips.js.map +1 -1
- package/dist/modes/interactive/tips/catalog/session-tips.d.ts.map +1 -1
- package/dist/modes/interactive/tips/catalog/session-tips.js +1 -1
- package/dist/modes/interactive/tips/catalog/session-tips.js.map +1 -1
- package/dist/modes/rpc/host-attach-hold.d.ts +16 -0
- package/dist/modes/rpc/host-attach-hold.d.ts.map +1 -0
- package/dist/modes/rpc/host-attach-hold.js +9 -0
- package/dist/modes/rpc/host-attach-hold.js.map +1 -0
- package/dist/modes/rpc/host-cleanup-paths.d.ts +20 -0
- package/dist/modes/rpc/host-cleanup-paths.d.ts.map +1 -0
- package/dist/modes/rpc/host-cleanup-paths.js +13 -0
- package/dist/modes/rpc/host-cleanup-paths.js.map +1 -0
- package/dist/modes/rpc/host-client-occupancy.d.ts +24 -0
- package/dist/modes/rpc/host-client-occupancy.d.ts.map +1 -0
- package/dist/modes/rpc/host-client-occupancy.js +53 -0
- package/dist/modes/rpc/host-client-occupancy.js.map +1 -0
- package/dist/modes/rpc/host-daemon-paths.d.ts +61 -8
- package/dist/modes/rpc/host-daemon-paths.d.ts.map +1 -1
- package/dist/modes/rpc/host-daemon-paths.js +128 -11
- package/dist/modes/rpc/host-daemon-paths.js.map +1 -1
- package/dist/modes/rpc/host-daemon-registration.d.ts +11 -1
- package/dist/modes/rpc/host-daemon-registration.d.ts.map +1 -1
- package/dist/modes/rpc/host-daemon-registration.js +28 -15
- package/dist/modes/rpc/host-daemon-registration.js.map +1 -1
- package/dist/modes/rpc/host-endpoints.d.ts +12 -0
- package/dist/modes/rpc/host-endpoints.d.ts.map +1 -0
- package/dist/modes/rpc/host-endpoints.js +59 -0
- package/dist/modes/rpc/host-endpoints.js.map +1 -0
- package/dist/modes/rpc/host-ensure-lock.d.ts +11 -0
- package/dist/modes/rpc/host-ensure-lock.d.ts.map +1 -0
- package/dist/modes/rpc/host-ensure-lock.js +49 -0
- package/dist/modes/rpc/host-ensure-lock.js.map +1 -0
- package/dist/modes/rpc/host-ensure.d.ts +7 -0
- package/dist/modes/rpc/host-ensure.d.ts.map +1 -1
- package/dist/modes/rpc/host-ensure.js +42 -65
- package/dist/modes/rpc/host-ensure.js.map +1 -1
- package/dist/modes/rpc/host-gc-evidence.d.ts +18 -0
- package/dist/modes/rpc/host-gc-evidence.d.ts.map +1 -0
- package/dist/modes/rpc/host-gc-evidence.js +128 -0
- package/dist/modes/rpc/host-gc-evidence.js.map +1 -0
- package/dist/modes/rpc/host-gc.d.ts +28 -0
- package/dist/modes/rpc/host-gc.d.ts.map +1 -0
- package/dist/modes/rpc/host-gc.js +110 -0
- package/dist/modes/rpc/host-gc.js.map +1 -0
- package/dist/modes/rpc/host-generations.d.ts +10 -2
- package/dist/modes/rpc/host-generations.d.ts.map +1 -1
- package/dist/modes/rpc/host-generations.js +12 -7
- package/dist/modes/rpc/host-generations.js.map +1 -1
- package/dist/modes/rpc/host-handoff.d.ts +6 -0
- package/dist/modes/rpc/host-handoff.d.ts.map +1 -1
- package/dist/modes/rpc/host-handoff.js +31 -4
- package/dist/modes/rpc/host-handoff.js.map +1 -1
- package/dist/modes/rpc/host-lifecycle-policy.d.ts +53 -0
- package/dist/modes/rpc/host-lifecycle-policy.d.ts.map +1 -0
- package/dist/modes/rpc/host-lifecycle-policy.js +64 -0
- package/dist/modes/rpc/host-lifecycle-policy.js.map +1 -0
- package/dist/modes/rpc/host-lifecycle.d.ts +1 -44
- package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -1
- package/dist/modes/rpc/host-lifecycle.js +23 -85
- package/dist/modes/rpc/host-lifecycle.js.map +1 -1
- package/dist/modes/rpc/host-observe-request.d.ts +17 -0
- package/dist/modes/rpc/host-observe-request.d.ts.map +1 -0
- package/dist/modes/rpc/host-observe-request.js +30 -0
- package/dist/modes/rpc/host-observe-request.js.map +1 -0
- package/dist/modes/rpc/host-probe.d.ts +12 -0
- package/dist/modes/rpc/host-probe.d.ts.map +1 -1
- package/dist/modes/rpc/host-probe.js +28 -1
- package/dist/modes/rpc/host-probe.js.map +1 -1
- package/dist/modes/rpc/host-protocol-info.d.ts +2 -0
- package/dist/modes/rpc/host-protocol-info.d.ts.map +1 -1
- package/dist/modes/rpc/host-protocol-info.js +1 -0
- package/dist/modes/rpc/host-protocol-info.js.map +1 -1
- package/dist/modes/rpc/host-readiness.d.ts +22 -0
- package/dist/modes/rpc/host-readiness.d.ts.map +1 -0
- package/dist/modes/rpc/host-readiness.js +44 -0
- package/dist/modes/rpc/host-readiness.js.map +1 -0
- package/dist/modes/rpc/host-runner.d.ts +13 -0
- package/dist/modes/rpc/host-runner.d.ts.map +1 -1
- package/dist/modes/rpc/host-runner.js +23 -4
- package/dist/modes/rpc/host-runner.js.map +1 -1
- package/dist/modes/rpc/host-status-all.d.ts +31 -0
- package/dist/modes/rpc/host-status-all.d.ts.map +1 -0
- package/dist/modes/rpc/host-status-all.js +78 -0
- package/dist/modes/rpc/host-status-all.js.map +1 -0
- package/dist/modes/rpc/host-status-rows.d.ts +35 -0
- package/dist/modes/rpc/host-status-rows.d.ts.map +1 -0
- package/dist/modes/rpc/host-status-rows.js +42 -0
- package/dist/modes/rpc/host-status-rows.js.map +1 -0
- package/dist/modes/rpc/host-status.d.ts +32 -1
- package/dist/modes/rpc/host-status.d.ts.map +1 -1
- package/dist/modes/rpc/host-status.js +33 -27
- package/dist/modes/rpc/host-status.js.map +1 -1
- package/dist/modes/rpc/host-successor.d.ts +3 -1
- package/dist/modes/rpc/host-successor.d.ts.map +1 -1
- package/dist/modes/rpc/host-successor.js +105 -42
- package/dist/modes/rpc/host-successor.js.map +1 -1
- package/dist/modes/rpc/host-watchdog.d.ts +6 -0
- package/dist/modes/rpc/host-watchdog.d.ts.map +1 -1
- package/dist/modes/rpc/host-watchdog.js +12 -13
- package/dist/modes/rpc/host-watchdog.js.map +1 -1
- package/dist/modes/rpc/loop-lag-watchdog.d.ts +16 -0
- package/dist/modes/rpc/loop-lag-watchdog.d.ts.map +1 -1
- package/dist/modes/rpc/loop-lag-watchdog.js +34 -2
- package/dist/modes/rpc/loop-lag-watchdog.js.map +1 -1
- package/dist/modes/rpc/multi-session-host.d.ts +2 -1
- package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
- package/dist/modes/rpc/multi-session-host.js +33 -6
- package/dist/modes/rpc/multi-session-host.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +2 -2
- package/dist/modes/rpc/rpc-mode.js +2 -2
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/modes/rpc/session-command-router.d.ts +9 -1
- package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
- package/dist/modes/rpc/session-command-router.js +4 -1
- package/dist/modes/rpc/session-command-router.js.map +1 -1
- package/dist/modes/rpc/session-path-key.d.ts +3 -1
- package/dist/modes/rpc/session-path-key.d.ts.map +1 -1
- package/dist/modes/rpc/session-path-key.js +3 -1
- package/dist/modes/rpc/session-path-key.js.map +1 -1
- package/docs/rpc.md +186 -19
- package/docs/settings.md +25 -1
- package/docs/usage.md +5 -2
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +24 -0
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.d.ts +6 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.js +36 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +14 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/autocomplete.js +6 -4
- package/node_modules/@earendil-works/pi-tui/dist/autocomplete.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts +3 -0
- package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +20 -0
- package/node_modules/@earendil-works/pi-tui/dist/components/editor.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/editor-component.d.ts +2 -0
- package/node_modules/@earendil-works/pi-tui/dist/editor-component.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/editor-component.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/slash-command-autocomplete.d.ts +5 -0
- package/node_modules/@earendil-works/pi-tui/dist/slash-command-autocomplete.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/slash-command-autocomplete.js +7 -0
- package/node_modules/@earendil-works/pi-tui/dist/slash-command-autocomplete.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +7 -7
- package/dist/bundle/chunks/chunk-3DEXCYNN.js +0 -135
- package/dist/bundle/chunks/chunk-5X4KSPK5.js +0 -12
- package/dist/bundle/chunks/chunk-KMD5ZONU.js +0 -5
- package/dist/bundle/chunks/chunk-PEHZYGYA.js +0 -8
- package/dist/bundle/chunks/chunk-PRC5XIIH.js +0 -2
- package/dist/bundle/chunks/chunk-QKY33652.js +0 -18
- package/dist/bundle/chunks/interactive-host-runtime-TIFT2SKR.js +0 -4
- package/dist/bundle/chunks/multi-session-host-GRHKB7Q2.js +0 -21
- package/dist/bundle/chunks/session-picker-FTFCQYHH.js +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-status.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/host-status.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,KAAK,iBAAiB,EAA4C,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"host-status.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/host-status.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,24 +10,36 @@
|
|
|
10
10
|
* A socket nobody serves is not an error here: `reachable: false` with the same field set is the
|
|
11
11
|
* answer, so a caller parses one shape either way and branches on one boolean.
|
|
12
12
|
*/
|
|
13
|
+
import { readHostCrashRecords } from "./host-crash-record.js";
|
|
13
14
|
import { readDaemonEnvKeys } from "./host-daemon-env.js";
|
|
14
|
-
import { createHostDaemonPaths } from "./host-daemon-paths.js";
|
|
15
|
+
import { createHostDaemonPaths, parseShardSocket } from "./host-daemon-paths.js";
|
|
15
16
|
import { pruneDeadGenerations, readGenerationRows } from "./host-generations.js";
|
|
16
|
-
import {
|
|
17
|
+
import { OBSERVE_REQUEST_FIELD } from "./host-observe-request.js";
|
|
18
|
+
import { observeProtocolInfo, requestOnSocket } from "./host-probe.js";
|
|
17
19
|
import { readHostProcessMetrics } from "./host-process-metrics.js";
|
|
20
|
+
import { parseSessionRows, readClaimRows } from "./host-status-rows.js";
|
|
18
21
|
const STATUS_PROBE_TIMEOUT_MS = 10_000;
|
|
19
|
-
export async function readHostStatus(options) {
|
|
22
|
+
export async function readHostStatus(options, read = {}) {
|
|
20
23
|
const paths = createHostDaemonPaths({
|
|
21
24
|
socket: options.socket,
|
|
22
25
|
...(options.agentDir ? { agentDir: options.agentDir } : {}),
|
|
23
26
|
});
|
|
24
|
-
const
|
|
27
|
+
const prune = read.prune !== false;
|
|
28
|
+
const includeWorkers = options.includeWorkers === true;
|
|
29
|
+
// Both reads observe: looking at a host must never be what keeps it from idling out.
|
|
30
|
+
const timeoutMs = read.timeoutMs ?? STATUS_PROBE_TIMEOUT_MS;
|
|
31
|
+
const host = await observeProtocolInfo(options.socket, timeoutMs);
|
|
25
32
|
// Reading the directory is also when it is cleaned: an operator asking what runs here must not
|
|
26
33
|
// be shown generations that ended, and the next reader must get the same answer.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
34
|
+
if (prune)
|
|
35
|
+
await pruneDeadGenerations(paths);
|
|
36
|
+
const generations = await readGenerationRows(paths, { includeDead: !prune });
|
|
37
|
+
const current = generations.find((row) => row.current && row.alive);
|
|
30
38
|
const metrics = current ? await readHostProcessMetrics(current.pid) : UNOBSERVED_METRICS;
|
|
39
|
+
// A socket that did not answer who it is will not answer what it holds: asking again would only
|
|
40
|
+
// spend a second budget on a hung endpoint.
|
|
41
|
+
const listing = host === undefined ? [] : await readSessionListing(options.socket, includeWorkers, timeoutMs);
|
|
42
|
+
const claims = await readClaimRows(paths, generations);
|
|
31
43
|
return {
|
|
32
44
|
reachable: host !== undefined,
|
|
33
45
|
socket: options.socket,
|
|
@@ -37,13 +49,19 @@ export async function readHostStatus(options) {
|
|
|
37
49
|
engineVersion: host?.engineVersion ?? current?.engineVersion ?? null,
|
|
38
50
|
capabilities: host?.capabilities ?? [],
|
|
39
51
|
launchProfile: host?.launch_profile ?? null,
|
|
40
|
-
sessions:
|
|
52
|
+
sessions: countSessions(listing),
|
|
41
53
|
zombies: metrics.zombies,
|
|
42
54
|
rss_mb: metrics.rss_mb,
|
|
43
55
|
host_rss_mb: metrics.host_rss_mb,
|
|
44
56
|
open_fds: metrics.open_fds,
|
|
57
|
+
memory_pressure: host?.memory_pressure ?? null,
|
|
45
58
|
env_keys: await readDaemonEnvKeys(paths),
|
|
46
59
|
generations,
|
|
60
|
+
crashes: readHostCrashRecords(paths.dir).length,
|
|
61
|
+
shard: parseShardSocket(options.socket),
|
|
62
|
+
session_rows: includeWorkers ? listing : [],
|
|
63
|
+
claims_live: claims.filter((claim) => claim.live).length,
|
|
64
|
+
claims: includeWorkers ? claims : [],
|
|
47
65
|
};
|
|
48
66
|
}
|
|
49
67
|
/**
|
|
@@ -51,8 +69,13 @@ export async function readHostStatus(options) {
|
|
|
51
69
|
* see, and an empty count is what lets a stop proceed against a socket nobody serves.
|
|
52
70
|
*/
|
|
53
71
|
export async function readSessionCounts(socket, includeWorkers) {
|
|
54
|
-
|
|
55
|
-
|
|
72
|
+
return countSessions(await readSessionListing(socket, includeWorkers));
|
|
73
|
+
}
|
|
74
|
+
async function readSessionListing(socket, includeWorkers, timeoutMs = STATUS_PROBE_TIMEOUT_MS) {
|
|
75
|
+
const reply = await requestOnSocket(socket, { type: "list_sessions", [OBSERVE_REQUEST_FIELD]: true, ...(includeWorkers ? { include_workers: true } : {}) }, timeoutMs);
|
|
76
|
+
return parseSessionRows(reply);
|
|
77
|
+
}
|
|
78
|
+
function countSessions(rows) {
|
|
56
79
|
const attached = rows.filter((row) => row.attachments > 0).length;
|
|
57
80
|
const retained = rows.length - attached;
|
|
58
81
|
return {
|
|
@@ -79,21 +102,4 @@ export function hostSummary(host) {
|
|
|
79
102
|
};
|
|
80
103
|
}
|
|
81
104
|
const UNOBSERVED_METRICS = { rss_mb: null, host_rss_mb: null, open_fds: null, zombies: null };
|
|
82
|
-
function sessionRows(reply) {
|
|
83
|
-
if (!isRecord(reply) || !Array.isArray(reply.sessions))
|
|
84
|
-
return [];
|
|
85
|
-
return reply.sessions.flatMap((entry) => {
|
|
86
|
-
if (!isRecord(entry))
|
|
87
|
-
return [];
|
|
88
|
-
return [
|
|
89
|
-
{
|
|
90
|
-
kind: typeof entry.kind === "string" ? entry.kind : "interactive",
|
|
91
|
-
attachments: typeof entry.attachments === "number" ? entry.attachments : 0,
|
|
92
|
-
},
|
|
93
|
-
];
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
function isRecord(value) {
|
|
97
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
98
|
-
}
|
|
99
105
|
//# sourceMappingURL=host-status.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-status.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-status.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a client can observe about the daemon on one socket, in one record.\n *\n * Everything that describes the RUNNING host comes from the host's own answers - `get_protocol_info`\n * for identity, `list_sessions` for occupancy - because files describe the past and the socket\n * describes the present. Everything that describes the DAEMON DIRECTORY (which generations exist,\n * which environment scope the last ensure granted) comes from disk, because a generation that no\n * longer answers is exactly what an operator is looking for when they ask.\n *\n * A socket nobody serves is not an error here: `reachable: false` with the same field set is the\n * answer, so a caller parses one shape either way and branches on one boolean.\n */\nimport { readDaemonEnvKeys } from \"./host-daemon-env.ts\";\nimport { createHostDaemonPaths } from \"./host-daemon-paths.ts\";\nimport type { HostProtocolInfo } from \"./host-decision.ts\";\nimport { type HostGenerationRow, pruneDeadGenerations, readGenerationRows } from \"./host-generations.ts\";\nimport { probeProtocolInfo, requestOnSocket } from \"./host-probe.ts\";\nimport { type HostProcessMetrics, readHostProcessMetrics } from \"./host-process-metrics.ts\";\nimport type { RpcLaunchProfile } from \"./rpc-types.ts\";\n\nconst STATUS_PROBE_TIMEOUT_MS = 10_000;\n\n/**\n * Sessions the host reports. `foreign_*` is the same count from the point of view of a client that\n * holds none of them itself: for the CLI every session belongs to somebody else, which is exactly\n * why a hard stop is gated on it.\n */\nexport interface HostSessionCounts {\n\treadonly total: number;\n\treadonly interactive: number;\n\treadonly worker: number;\n\t/** Open at zero attachments: retained across a disconnect, still holding its transcript. */\n\treadonly retained: number;\n\treadonly foreign_attached: number;\n\treadonly foreign_retained: number;\n}\n\nexport type { HostGenerationRow } from \"./host-generations.ts\";\n\nexport interface HostStatusReport {\n\treadonly reachable: boolean;\n\treadonly socket: string;\n\treadonly pid: number | null;\n\treadonly instanceId: string | null;\n\treadonly generation: number | null;\n\treadonly engineVersion: string | null;\n\treadonly capabilities: readonly string[];\n\treadonly launchProfile: RpcLaunchProfile | null;\n\treadonly sessions: HostSessionCounts;\n\treadonly zombies: number | null;\n\treadonly rss_mb: number | null;\n\treadonly host_rss_mb: number | null;\n\treadonly open_fds: number | null;\n\t/** Environment NAMES the daemon was granted, never values. */\n\treadonly env_keys: readonly string[];\n\treadonly generations: readonly HostGenerationRow[];\n}\n\nexport interface HostStatusOptions {\n\treadonly socket: string;\n\treadonly agentDir?: string;\n\t/** Ask the host to include `kind: \"worker\"` rows, exactly as `list_sessions` defines the flag. */\n\treadonly includeWorkers?: boolean;\n}\n\nexport async function readHostStatus(options: HostStatusOptions): Promise<HostStatusReport> {\n\tconst paths = createHostDaemonPaths({\n\t\tsocket: options.socket,\n\t\t...(options.agentDir ? { agentDir: options.agentDir } : {}),\n\t});\n\tconst host = await probeProtocolInfo(options.socket, STATUS_PROBE_TIMEOUT_MS);\n\t// Reading the directory is also when it is cleaned: an operator asking what runs here must not\n\t// be shown generations that ended, and the next reader must get the same answer.\n\tawait pruneDeadGenerations(paths);\n\tconst generations = await readGenerationRows(paths);\n\tconst current = generations.find((row) => row.current);\n\tconst metrics = current ? await readHostProcessMetrics(current.pid) : UNOBSERVED_METRICS;\n\treturn {\n\t\treachable: host !== undefined,\n\t\tsocket: options.socket,\n\t\tpid: current?.pid ?? null,\n\t\tinstanceId: host?.instanceId ?? current?.instanceId ?? null,\n\t\tgeneration: host?.generation ?? current?.generation ?? null,\n\t\tengineVersion: host?.engineVersion ?? current?.engineVersion ?? null,\n\t\tcapabilities: host?.capabilities ?? [],\n\t\tlaunchProfile: host?.launch_profile ?? null,\n\t\tsessions: await readSessionCounts(options.socket, options.includeWorkers === true),\n\t\tzombies: metrics.zombies,\n\t\trss_mb: metrics.rss_mb,\n\t\thost_rss_mb: metrics.host_rss_mb,\n\t\topen_fds: metrics.open_fds,\n\t\tenv_keys: await readDaemonEnvKeys(paths),\n\t\tgenerations,\n\t};\n}\n\n/**\n * The occupancy a stop decision is made on. A host that does not answer holds nothing a client can\n * see, and an empty count is what lets a stop proceed against a socket nobody serves.\n */\nexport async function readSessionCounts(socket: string, includeWorkers: boolean): Promise<HostSessionCounts> {\n\tconst reply = await requestOnSocket(\n\t\tsocket,\n\t\t{ type: \"list_sessions\", ...(includeWorkers ? { include_workers: true } : {}) },\n\t\tSTATUS_PROBE_TIMEOUT_MS,\n\t);\n\tconst rows = sessionRows(reply);\n\tconst attached = rows.filter((row) => row.attachments > 0).length;\n\tconst retained = rows.length - attached;\n\treturn {\n\t\ttotal: rows.length,\n\t\tinteractive: rows.filter((row) => row.kind !== \"worker\").length,\n\t\tworker: rows.filter((row) => row.kind === \"worker\").length,\n\t\tretained,\n\t\tforeign_attached: attached,\n\t\tforeign_retained: retained,\n\t};\n}\n\n/** Identity fields of a probed host, for the `host` field of a refusal. */\nexport function hostSummary(host: HostProtocolInfo | undefined): Record<string, unknown> | null {\n\tif (host === undefined) return null;\n\treturn {\n\t\tprotocolVersion: host.protocolVersion,\n\t\tserverVersion: host.serverVersion,\n\t\tcapabilities: host.capabilities,\n\t\tinstanceId: host.instanceId ?? null,\n\t\tgeneration: host.generation ?? null,\n\t\tengineVersion: host.engineVersion ?? null,\n\t\tlaunchProfileId: host.launch_profile?.profile_id ?? null,\n\t};\n}\n\nconst UNOBSERVED_METRICS: HostProcessMetrics = { rss_mb: null, host_rss_mb: null, open_fds: null, zombies: null };\n\ninterface SessionRow {\n\treadonly kind: string;\n\treadonly attachments: number;\n}\n\nfunction sessionRows(reply: unknown): readonly SessionRow[] {\n\tif (!isRecord(reply) || !Array.isArray(reply.sessions)) return [];\n\treturn reply.sessions.flatMap((entry: unknown) => {\n\t\tif (!isRecord(entry)) return [];\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: typeof entry.kind === \"string\" ? entry.kind : \"interactive\",\n\t\t\t\tattachments: typeof entry.attachments === \"number\" ? entry.attachments : 0,\n\t\t\t},\n\t\t];\n\t});\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"host-status.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-status.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a client can observe about the daemon on one socket, in one record.\n *\n * Everything that describes the RUNNING host comes from the host's own answers - `get_protocol_info`\n * for identity, `list_sessions` for occupancy - because files describe the past and the socket\n * describes the present. Everything that describes the DAEMON DIRECTORY (which generations exist,\n * which environment scope the last ensure granted) comes from disk, because a generation that no\n * longer answers is exactly what an operator is looking for when they ask.\n *\n * A socket nobody serves is not an error here: `reachable: false` with the same field set is the\n * answer, so a caller parses one shape either way and branches on one boolean.\n */\nimport { readHostCrashRecords } from \"./host-crash-record.ts\";\nimport { readDaemonEnvKeys } from \"./host-daemon-env.ts\";\nimport { createHostDaemonPaths, parseShardSocket, type ShardKind } from \"./host-daemon-paths.ts\";\nimport type { HostProtocolInfo } from \"./host-decision.ts\";\nimport { type HostGenerationRow, pruneDeadGenerations, readGenerationRows } from \"./host-generations.ts\";\nimport { OBSERVE_REQUEST_FIELD } from \"./host-observe-request.ts\";\nimport { observeProtocolInfo, requestOnSocket } from \"./host-probe.ts\";\nimport { type HostProcessMetrics, readHostProcessMetrics } from \"./host-process-metrics.ts\";\nimport { type HostPathClaimRow, type HostSessionRow, parseSessionRows, readClaimRows } from \"./host-status-rows.ts\";\nimport type { RpcLaunchProfile } from \"./rpc-types.ts\";\n\nconst STATUS_PROBE_TIMEOUT_MS = 10_000;\n\n/**\n * Sessions the host reports. `foreign_*` is the same count from the point of view of a client that\n * holds none of them itself: for the CLI every session belongs to somebody else, which is exactly\n * why a hard stop is gated on it.\n */\nexport interface HostSessionCounts {\n\treadonly total: number;\n\treadonly interactive: number;\n\treadonly worker: number;\n\t/** Open at zero attachments: retained across a disconnect, still holding its transcript. */\n\treadonly retained: number;\n\treadonly foreign_attached: number;\n\treadonly foreign_retained: number;\n}\n\nexport type { HostGenerationRow } from \"./host-generations.ts\";\nexport type { HostPathClaimRow, HostSessionRow } from \"./host-status-rows.ts\";\n\nexport interface HostStatusReport {\n\treadonly reachable: boolean;\n\treadonly socket: string;\n\treadonly pid: number | null;\n\treadonly instanceId: string | null;\n\treadonly generation: number | null;\n\treadonly engineVersion: string | null;\n\treadonly capabilities: readonly string[];\n\treadonly launchProfile: RpcLaunchProfile | null;\n\treadonly sessions: HostSessionCounts;\n\treadonly zombies: number | null;\n\treadonly rss_mb: number | null;\n\treadonly host_rss_mb: number | null;\n\treadonly open_fds: number | null;\n\t/**\n\t * Whether the ANSWERING generation's memory sampler reads pressure right now (RSS above\n\t * `SENPI_RPC_HOST_RSS_WARN_MB`); `null` when nothing answers or the host predates the field.\n\t */\n\treadonly memory_pressure: boolean | null;\n\t/** Environment NAMES the daemon was granted, never values. */\n\treadonly env_keys: readonly string[];\n\treadonly generations: readonly HostGenerationRow[];\n\t/** Records in this endpoint's `crashes.jsonl`: supervised host children that DIED, not stopped. */\n\treadonly crashes: number;\n\t/** The owner-keyed shard the socket names (`<kind>-<16hex>.sock`), `null` for any other endpoint. */\n\treadonly shard: { readonly kind: ShardKind; readonly key: string } | null;\n\t/** Every listed session, only when workers were asked for; `[]` otherwise. */\n\treadonly session_rows: readonly HostSessionRow[];\n\t/** Session-path claims in `reservations/` whose owner process is still running. */\n\treadonly claims_live: number;\n\t/** Every claim in `reservations/`, whichever generation wrote it, only when workers were asked for. */\n\treadonly claims: readonly HostPathClaimRow[];\n}\n\nexport interface HostStatusOptions {\n\treadonly socket: string;\n\treadonly agentDir?: string;\n\t/** Ask the host to include `kind: \"worker\"` rows, exactly as `list_sessions` defines the flag. */\n\treadonly includeWorkers?: boolean;\n}\n\n/**\n * How a status read treats the daemon directory. `prune` (the default, and the single-socket\n * `status`) removes what ended before reporting; `prune: false` (`status --all`) reports dead\n * generations as `alive: false` rows and removes nothing.\n */\nexport interface HostStatusReadOptions {\n\treadonly prune?: boolean;\n\t/** Budget for each read of the socket, default 10 s. */\n\treadonly timeoutMs?: number;\n}\n\nexport async function readHostStatus(\n\toptions: HostStatusOptions,\n\tread: HostStatusReadOptions = {},\n): Promise<HostStatusReport> {\n\tconst paths = createHostDaemonPaths({\n\t\tsocket: options.socket,\n\t\t...(options.agentDir ? { agentDir: options.agentDir } : {}),\n\t});\n\tconst prune = read.prune !== false;\n\tconst includeWorkers = options.includeWorkers === true;\n\t// Both reads observe: looking at a host must never be what keeps it from idling out.\n\tconst timeoutMs = read.timeoutMs ?? STATUS_PROBE_TIMEOUT_MS;\n\tconst host = await observeProtocolInfo(options.socket, timeoutMs);\n\t// Reading the directory is also when it is cleaned: an operator asking what runs here must not\n\t// be shown generations that ended, and the next reader must get the same answer.\n\tif (prune) await pruneDeadGenerations(paths);\n\tconst generations = await readGenerationRows(paths, { includeDead: !prune });\n\tconst current = generations.find((row) => row.current && row.alive);\n\tconst metrics = current ? await readHostProcessMetrics(current.pid) : UNOBSERVED_METRICS;\n\t// A socket that did not answer who it is will not answer what it holds: asking again would only\n\t// spend a second budget on a hung endpoint.\n\tconst listing = host === undefined ? [] : await readSessionListing(options.socket, includeWorkers, timeoutMs);\n\tconst claims = await readClaimRows(paths, generations);\n\treturn {\n\t\treachable: host !== undefined,\n\t\tsocket: options.socket,\n\t\tpid: current?.pid ?? null,\n\t\tinstanceId: host?.instanceId ?? current?.instanceId ?? null,\n\t\tgeneration: host?.generation ?? current?.generation ?? null,\n\t\tengineVersion: host?.engineVersion ?? current?.engineVersion ?? null,\n\t\tcapabilities: host?.capabilities ?? [],\n\t\tlaunchProfile: host?.launch_profile ?? null,\n\t\tsessions: countSessions(listing),\n\t\tzombies: metrics.zombies,\n\t\trss_mb: metrics.rss_mb,\n\t\thost_rss_mb: metrics.host_rss_mb,\n\t\topen_fds: metrics.open_fds,\n\t\tmemory_pressure: host?.memory_pressure ?? null,\n\t\tenv_keys: await readDaemonEnvKeys(paths),\n\t\tgenerations,\n\t\tcrashes: readHostCrashRecords(paths.dir).length,\n\t\tshard: parseShardSocket(options.socket),\n\t\tsession_rows: includeWorkers ? listing : [],\n\t\tclaims_live: claims.filter((claim) => claim.live).length,\n\t\tclaims: includeWorkers ? claims : [],\n\t};\n}\n\n/**\n * The occupancy a stop decision is made on. A host that does not answer holds nothing a client can\n * see, and an empty count is what lets a stop proceed against a socket nobody serves.\n */\nexport async function readSessionCounts(socket: string, includeWorkers: boolean): Promise<HostSessionCounts> {\n\treturn countSessions(await readSessionListing(socket, includeWorkers));\n}\n\nasync function readSessionListing(\n\tsocket: string,\n\tincludeWorkers: boolean,\n\ttimeoutMs = STATUS_PROBE_TIMEOUT_MS,\n): Promise<readonly HostSessionRow[]> {\n\tconst reply = await requestOnSocket(\n\t\tsocket,\n\t\t{ type: \"list_sessions\", [OBSERVE_REQUEST_FIELD]: true, ...(includeWorkers ? { include_workers: true } : {}) },\n\t\ttimeoutMs,\n\t);\n\treturn parseSessionRows(reply);\n}\n\nfunction countSessions(rows: readonly HostSessionRow[]): HostSessionCounts {\n\tconst attached = rows.filter((row) => row.attachments > 0).length;\n\tconst retained = rows.length - attached;\n\treturn {\n\t\ttotal: rows.length,\n\t\tinteractive: rows.filter((row) => row.kind !== \"worker\").length,\n\t\tworker: rows.filter((row) => row.kind === \"worker\").length,\n\t\tretained,\n\t\tforeign_attached: attached,\n\t\tforeign_retained: retained,\n\t};\n}\n\n/** Identity fields of a probed host, for the `host` field of a refusal. */\nexport function hostSummary(host: HostProtocolInfo | undefined): Record<string, unknown> | null {\n\tif (host === undefined) return null;\n\treturn {\n\t\tprotocolVersion: host.protocolVersion,\n\t\tserverVersion: host.serverVersion,\n\t\tcapabilities: host.capabilities,\n\t\tinstanceId: host.instanceId ?? null,\n\t\tgeneration: host.generation ?? null,\n\t\tengineVersion: host.engineVersion ?? null,\n\t\tlaunchProfileId: host.launch_profile?.profile_id ?? null,\n\t};\n}\n\nconst UNOBSERVED_METRICS: HostProcessMetrics = { rss_mb: null, host_rss_mb: null, open_fds: null, zombies: null };\n"]}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type HostDaemonPaths } from "./host-daemon-paths.ts";
|
|
2
2
|
import type { HandoffHostOptions, HandoffResult } from "./host-handoff.ts";
|
|
3
3
|
import type { HostProtocolInfo } from "./host-protocol-info.ts";
|
|
4
|
+
/** The longest `startSuccessor` runs: its start-time read, the readiness window, one last probe and a refusal's exit wait. */
|
|
5
|
+
export declare const SUCCESSOR_START_BUDGET_MS: number;
|
|
4
6
|
export declare function startSuccessor(context: {
|
|
5
7
|
options: HandoffHostOptions;
|
|
6
8
|
paths: HostDaemonPaths;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-successor.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/host-successor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"host-successor.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/host-successor.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAyC,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGrG,OAAO,KAAK,EAAE,kBAAkB,EAAkB,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAI3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAiBhE,8HAA8H;AAC9H,eAAO,MAAM,yBAAyB,QAAyE,CAAC;AAEhH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC7C,OAAO,EAAE,kBAAkB,CAAC;IAC5B,KAAK,EAAE,eAAe,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;IACvB,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CACrE,GAAG,OAAO,CAAC,aAAa,CAAC,CA0GzB"}
|
|
@@ -10,13 +10,19 @@
|
|
|
10
10
|
* left both sockets alone. The predecessor is asked to drain only AFTER that proof and after the
|
|
11
11
|
* successor is registered - whether it may be asked at all was decided in `host-handoff.ts`, which
|
|
12
12
|
* proved the owner and checked that the running host advertises it can survive the signal.
|
|
13
|
+
*
|
|
14
|
+
* A REFUSED handoff leaves the endpoint as it found it: the successor is killed and, once it has
|
|
15
|
+
* exited, its generation record and directory are released, and the boot `settings.json` it
|
|
16
|
+
* overwrote before spawning is put back byte for byte - so neither `status --all` nor the running
|
|
17
|
+
* generation's next restart ever sees the successor that did not happen.
|
|
13
18
|
*/
|
|
14
19
|
import { spawn } from "node:child_process";
|
|
15
20
|
import { randomUUID } from "node:crypto";
|
|
16
|
-
import { open } from "node:fs/promises";
|
|
21
|
+
import { open, rm, writeFile } from "node:fs/promises";
|
|
17
22
|
import { waitForStartTime } from "../app-server/daemon/process.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
23
|
+
import { generationPaths, HOST_STATE_FILE_MODE } from "./host-daemon-paths.js";
|
|
24
|
+
import { releaseGeneration, writeGenerationRecord, writeHostRegistration } from "./host-daemon-registration.js";
|
|
25
|
+
import { readFileOrUndefined, readHostSettings, writeHostSettings } from "./host-daemon-state.js";
|
|
20
26
|
import { defaultHostLaunch, PINNED_HOST_CLIENT_CAPABILITIES } from "./host-launch.js";
|
|
21
27
|
import { DEFAULT_HOST_IDLE_EXIT_MS } from "./host-lifecycle.js";
|
|
22
28
|
import { probeProtocolInfo } from "./host-probe.js";
|
|
@@ -26,6 +32,10 @@ import { hostLaunchProfile } from "./protocol-identity.js";
|
|
|
26
32
|
import { generationBindPath, MAX_SOCKET_PATH_BYTES, statSocketIdentity, } from "./socket-ownership.js";
|
|
27
33
|
/** How long the successor has to answer on the PUBLIC socket before the handoff is abandoned. */
|
|
28
34
|
const DEFAULT_HANDOFF_READINESS_MS = 30_000;
|
|
35
|
+
/** How long a refused handoff waits for the successor it killed to exit before releasing its record. */
|
|
36
|
+
const SUCCESSOR_EXIT_WAIT_MS = 5_000;
|
|
37
|
+
/** The longest `startSuccessor` runs: its start-time read, the readiness window, one last probe and a refusal's exit wait. */
|
|
38
|
+
export const SUCCESSOR_START_BUDGET_MS = 10_000 + DEFAULT_HANDOFF_READINESS_MS + 2_000 + SUCCESSOR_EXIT_WAIT_MS;
|
|
29
39
|
export async function startSuccessor(context) {
|
|
30
40
|
const { options, paths, host, owner } = context;
|
|
31
41
|
const generation = (host.generation ?? 0) + 1;
|
|
@@ -42,6 +52,7 @@ export async function startSuccessor(context) {
|
|
|
42
52
|
// A handoff replaces the ENGINE, not the operator's lifecycle policy: the successor inherits
|
|
43
53
|
// what the running generation was started with unless this caller states its own.
|
|
44
54
|
const running = await readHostSettings(paths);
|
|
55
|
+
const bootSettings = await readFileOrUndefined(paths.settingsFile);
|
|
45
56
|
await writeHostSettings(paths, {
|
|
46
57
|
socket: options.socket,
|
|
47
58
|
capabilities: PINNED_HOST_CLIENT_CAPABILITIES,
|
|
@@ -50,51 +61,63 @@ export async function startSuccessor(context) {
|
|
|
50
61
|
generation,
|
|
51
62
|
instanceId,
|
|
52
63
|
});
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
bindSocket,
|
|
59
|
-
"--replace",
|
|
60
|
-
`${replaced.dev}:${replaced.ino}`,
|
|
61
|
-
...(options.hostArgs ?? []),
|
|
62
|
-
];
|
|
63
|
-
const launch = options._test?.launch?.(argv) ?? defaultHostLaunch(argv);
|
|
64
|
-
const stderr = await open(paths.stderrLog, "a", 0o600);
|
|
65
|
-
const child = spawn(launch.command, [...launch.args], {
|
|
66
|
-
detached: true,
|
|
67
|
-
windowsHide: true,
|
|
68
|
-
env: successorHostEnvironment({
|
|
69
|
-
agentDir: options.agentDir,
|
|
70
|
-
env: options.env,
|
|
71
|
-
paths,
|
|
72
|
-
generation,
|
|
73
|
-
instanceId,
|
|
74
|
-
}),
|
|
75
|
-
stdio: ["ignore", "ignore", stderr.fd],
|
|
76
|
-
});
|
|
77
|
-
await stderr.close();
|
|
78
|
-
const exited = new Promise((resolve) => child.once("exit", () => resolve()));
|
|
64
|
+
// From the overwrite on, every exit is a refusal that puts the boot settings back: a failure BEFORE
|
|
65
|
+
// the successor existed (the hook, the launch build, the stderr open, the spawn itself) has nothing
|
|
66
|
+
// to kill or release, but the settings it already overwrote are not the successor's to keep.
|
|
67
|
+
let child;
|
|
68
|
+
let exited;
|
|
79
69
|
try {
|
|
70
|
+
await options._test?.beforeSpawn?.();
|
|
71
|
+
const argv = [
|
|
72
|
+
"--socket",
|
|
73
|
+
options.socket,
|
|
74
|
+
"--bind",
|
|
75
|
+
bindSocket,
|
|
76
|
+
"--replace",
|
|
77
|
+
`${replaced.dev}:${replaced.ino}`,
|
|
78
|
+
...(options.hostArgs ?? []),
|
|
79
|
+
];
|
|
80
|
+
const launch = options._test?.launch?.(argv) ?? defaultHostLaunch(argv);
|
|
81
|
+
const stderr = await open(paths.stderrLog, "a", 0o600);
|
|
82
|
+
const spawned = spawn(launch.command, [...launch.args], {
|
|
83
|
+
detached: true,
|
|
84
|
+
windowsHide: true,
|
|
85
|
+
env: successorHostEnvironment({
|
|
86
|
+
agentDir: options.agentDir,
|
|
87
|
+
env: options.env,
|
|
88
|
+
paths,
|
|
89
|
+
generation,
|
|
90
|
+
instanceId,
|
|
91
|
+
}),
|
|
92
|
+
stdio: ["ignore", "ignore", stderr.fd],
|
|
93
|
+
});
|
|
94
|
+
await stderr.close();
|
|
95
|
+
child = spawned;
|
|
96
|
+
exited = new Promise((resolve) => spawned.once("exit", () => resolve()));
|
|
80
97
|
if (child.pid === undefined)
|
|
81
98
|
throw new Error("failed to spawn the successor generation");
|
|
82
|
-
const answer = await awaitSuccessor(options, host, exited);
|
|
83
|
-
if (!answer) {
|
|
84
|
-
if (child.exitCode === null && child.signalCode === null)
|
|
85
|
-
child.kill("SIGKILL");
|
|
86
|
-
return { action: "refuse", reason: await abortReason(options.socket, replaced), upgradeable: true };
|
|
87
|
-
}
|
|
88
99
|
const processStartTime = (await waitForStartTime(child.pid, 10_000).catch(() => undefined)) ?? null;
|
|
89
|
-
|
|
90
|
-
// clients reach is still the predecessor, and the pointer has to name whoever owns the socket.
|
|
91
|
-
await writeHostRegistration(paths, {
|
|
100
|
+
const registration = {
|
|
92
101
|
record: { pid: child.pid, processStartTime },
|
|
93
102
|
socket: options.socket,
|
|
94
103
|
instanceId,
|
|
95
104
|
generation,
|
|
96
105
|
launchProfileId: hostLaunchProfile(["--mode", "rpc", "--multi-session", ...(options.hostArgs ?? [])], process.cwd()).profile_id,
|
|
97
|
-
}
|
|
106
|
+
};
|
|
107
|
+
// The successor is running from this instant, long before it binds anything: its own record says
|
|
108
|
+
// so, so `host gc` never judges the endpoint by a predecessor that died meanwhile. The pointer is
|
|
109
|
+
// NOT moved here - the predecessor still owns the socket until the rename lands.
|
|
110
|
+
await writeGenerationRecord(paths, registration);
|
|
111
|
+
await options._test?.afterSpawn?.(child.pid);
|
|
112
|
+
const answer = await awaitSuccessor(options, host, exited);
|
|
113
|
+
if (!answer) {
|
|
114
|
+
const reason = await abortReason(options.socket, replaced);
|
|
115
|
+
const cleanupFailure = await abandonSuccessor({ child, exited, paths, instanceId, bootSettings });
|
|
116
|
+
return { action: "refuse", reason, upgradeable: true, ...(cleanupFailure && { detail: cleanupFailure }) };
|
|
117
|
+
}
|
|
118
|
+
// The pointer moves to the successor only now: until the rename landed, the generation the
|
|
119
|
+
// clients reach is still the predecessor, and the pointer has to name whoever owns the socket.
|
|
120
|
+
await writeHostRegistration(paths, registration);
|
|
98
121
|
child.unref();
|
|
99
122
|
// The successor owns the socket now: the predecessor may drain. SIGUSR1 is sent only here,
|
|
100
123
|
// to a pid the record proved and a host that advertised it can survive the signal. A
|
|
@@ -110,16 +133,56 @@ export async function startSuccessor(context) {
|
|
|
110
133
|
};
|
|
111
134
|
}
|
|
112
135
|
catch (cause) {
|
|
113
|
-
|
|
114
|
-
|
|
136
|
+
const cleanupFailure = await abandonSuccessor({ child, exited, paths, instanceId, bootSettings });
|
|
137
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
115
138
|
return {
|
|
116
139
|
action: "refuse",
|
|
117
140
|
reason: "successor_unavailable",
|
|
118
141
|
upgradeable: true,
|
|
119
|
-
detail:
|
|
142
|
+
detail: cleanupFailure ? `${detail}; ${cleanupFailure}` : detail,
|
|
120
143
|
};
|
|
121
144
|
}
|
|
122
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Undoes what a refused handoff left: kills the successor, releases its generation once it has exited
|
|
148
|
+
* (a successor that never got a pid never ran, so its directory goes at once; one that outlives the wait
|
|
149
|
+
* keeps its record until pruning finds it dead), and puts the boot settings back as they were. A
|
|
150
|
+
* successor that was never spawned leaves only the boot settings to undo. A cleanup that fails is
|
|
151
|
+
* answered, not thrown: the handoff still refused, and the caller is told why
|
|
152
|
+
* the endpoint may not be as it was.
|
|
153
|
+
*/
|
|
154
|
+
async function abandonSuccessor(context) {
|
|
155
|
+
const { child, exited, paths, instanceId, bootSettings } = context;
|
|
156
|
+
if (child !== undefined && child.exitCode === null && child.signalCode === null)
|
|
157
|
+
child.kill("SIGKILL");
|
|
158
|
+
try {
|
|
159
|
+
if (bootSettings === undefined)
|
|
160
|
+
await rm(paths.settingsFile, { force: true });
|
|
161
|
+
else
|
|
162
|
+
await writeFile(paths.settingsFile, bootSettings, { mode: HOST_STATE_FILE_MODE });
|
|
163
|
+
if (child?.pid === undefined)
|
|
164
|
+
await rm(generationPaths(paths, instanceId).dir, { recursive: true, force: true });
|
|
165
|
+
else if (exited !== undefined && (await exitedWithin(exited, SUCCESSOR_EXIT_WAIT_MS))) {
|
|
166
|
+
await releaseGeneration(paths, { instanceId, pid: child.pid });
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
catch (cause) {
|
|
171
|
+
return `successor cleanup failed: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function exitedWithin(exited, ms) {
|
|
175
|
+
let timer;
|
|
176
|
+
const timedOut = new Promise((resolve) => {
|
|
177
|
+
timer = setTimeout(() => resolve(false), ms);
|
|
178
|
+
});
|
|
179
|
+
try {
|
|
180
|
+
return await Promise.race([exited.then(() => true), timedOut]);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
123
186
|
/**
|
|
124
187
|
* The successor's own answer on the PUBLIC socket is the only proof the rename landed: it
|
|
125
188
|
* reports a different `instanceId` than the generation being replaced. An exit instead means
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-successor.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-successor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bringing up the SUCCESSOR generation: where it binds, what it is launched with, and the single\n * observation that proves it took the socket over.\n *\n * The successor binds `<public>.next-<gen>` - never the live public path, which it has no right to\n * unlink - and renames its own entry over the public path only while that path still holds the exact\n * socket this handoff was decided against (`--replace <dev>:<ino>`). Its own answer ON THE PUBLIC\n * SOCKET, reporting an `instanceId` different from the generation being replaced, is the only proof\n * the rename landed; an exit instead means it refused to replace a path that changed underneath and\n * left both sockets alone. The predecessor is asked to drain only AFTER that proof and after the\n * successor is registered - whether it may be asked at all was decided in `host-handoff.ts`, which\n * proved the owner and checked that the running host advertises it can survive the signal.\n */\nimport { spawn } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { open } from \"node:fs/promises\";\nimport { waitForStartTime } from \"../app-server/daemon/process.ts\";\nimport type { HostDaemonPaths } from \"./host-daemon-paths.ts\";\nimport { writeHostRegistration } from \"./host-daemon-registration.ts\";\nimport { readHostSettings, writeHostSettings } from \"./host-daemon-state.ts\";\nimport type { HandoffHostOptions, HandoffRefusal, HandoffResult } from \"./host-handoff.ts\";\nimport { defaultHostLaunch, PINNED_HOST_CLIENT_CAPABILITIES } from \"./host-launch.ts\";\nimport { DEFAULT_HOST_IDLE_EXIT_MS } from \"./host-lifecycle.ts\";\nimport { probeProtocolInfo } from \"./host-probe.ts\";\nimport type { HostProtocolInfo } from \"./host-protocol-info.ts\";\nimport { successorHostEnvironment } from \"./host-spawn-environment.ts\";\nimport { signalGeneration } from \"./host-stop.ts\";\nimport { hostLaunchProfile } from \"./protocol-identity.ts\";\nimport {\n\tgenerationBindPath,\n\tMAX_SOCKET_PATH_BYTES,\n\ttype SocketFileIdentity,\n\tstatSocketIdentity,\n} from \"./socket-ownership.ts\";\n\n/** How long the successor has to answer on the PUBLIC socket before the handoff is abandoned. */\nconst DEFAULT_HANDOFF_READINESS_MS = 30_000;\n\nexport async function startSuccessor(context: {\n\toptions: HandoffHostOptions;\n\tpaths: HostDaemonPaths;\n\thost: HostProtocolInfo;\n\towner: { pid: number; processStartTime: string; instanceId: string };\n}): Promise<HandoffResult> {\n\tconst { options, paths, host, owner } = context;\n\tconst generation = (host.generation ?? 0) + 1;\n\t// The successor's identity, chosen here so its generation directory holds its settings before it\n\t// boots and the pointer can name it the instant it answers on the public socket.\n\tconst instanceId = randomUUID();\n\tconst bindSocket = generationBindPath(options.socket, generation);\n\tif (Buffer.byteLength(bindSocket) > MAX_SOCKET_PATH_BYTES) {\n\t\treturn { action: \"refuse\", reason: \"socket_path_too_long\", upgradeable: true, detail: bindSocket };\n\t}\n\tconst replaced = await statSocketIdentity(options.socket);\n\tif (!replaced) return { action: \"refuse\", reason: \"socket_replaced\", upgradeable: true };\n\t// A handoff replaces the ENGINE, not the operator's lifecycle policy: the successor inherits\n\t// what the running generation was started with unless this caller states its own.\n\tconst running = await readHostSettings(paths);\n\tawait writeHostSettings(paths, {\n\t\tsocket: options.socket,\n\t\tcapabilities: PINNED_HOST_CLIENT_CAPABILITIES,\n\t\tcoldStart: options.policy?.coldStart ?? running?.coldStart ?? \"transient\",\n\t\tidleExitMs: options.policy?.idleExitMs ?? running?.idleExitMs ?? DEFAULT_HOST_IDLE_EXIT_MS,\n\t\tgeneration,\n\t\tinstanceId,\n\t});\n\tawait options._test?.beforeSpawn?.();\n\tconst argv = [\n\t\t\"--socket\",\n\t\toptions.socket,\n\t\t\"--bind\",\n\t\tbindSocket,\n\t\t\"--replace\",\n\t\t`${replaced.dev}:${replaced.ino}`,\n\t\t...(options.hostArgs ?? []),\n\t];\n\tconst launch = options._test?.launch?.(argv) ?? defaultHostLaunch(argv);\n\tconst stderr = await open(paths.stderrLog, \"a\", 0o600);\n\tconst child = spawn(launch.command, [...launch.args], {\n\t\tdetached: true,\n\t\twindowsHide: true,\n\t\tenv: successorHostEnvironment({\n\t\t\tagentDir: options.agentDir,\n\t\t\tenv: options.env,\n\t\t\tpaths,\n\t\t\tgeneration,\n\t\t\tinstanceId,\n\t\t}),\n\t\tstdio: [\"ignore\", \"ignore\", stderr.fd],\n\t});\n\tawait stderr.close();\n\tconst exited = new Promise<void>((resolve) => child.once(\"exit\", () => resolve()));\n\ttry {\n\t\tif (child.pid === undefined) throw new Error(\"failed to spawn the successor generation\");\n\t\tconst answer = await awaitSuccessor(options, host, exited);\n\t\tif (!answer) {\n\t\t\tif (child.exitCode === null && child.signalCode === null) child.kill(\"SIGKILL\");\n\t\t\treturn { action: \"refuse\", reason: await abortReason(options.socket, replaced), upgradeable: true };\n\t\t}\n\t\tconst processStartTime = (await waitForStartTime(child.pid, 10_000).catch(() => undefined)) ?? null;\n\t\t// The pointer moves to the successor only now: until the rename landed, the generation the\n\t\t// clients reach is still the predecessor, and the pointer has to name whoever owns the socket.\n\t\tawait writeHostRegistration(paths, {\n\t\t\trecord: { pid: child.pid, processStartTime },\n\t\t\tsocket: options.socket,\n\t\t\tinstanceId,\n\t\t\tgeneration,\n\t\t\tlaunchProfileId: hostLaunchProfile(\n\t\t\t\t[\"--mode\", \"rpc\", \"--multi-session\", ...(options.hostArgs ?? [])],\n\t\t\t\tprocess.cwd(),\n\t\t\t).profile_id,\n\t\t});\n\t\tchild.unref();\n\t\t// The successor owns the socket now: the predecessor may drain. SIGUSR1 is sent only here,\n\t\t// to a pid the record proved and a host that advertised it can survive the signal. A\n\t\t// predecessor that exited on its own in the meantime is already drained, and the handoff it\n\t\t// was being asked to make room for has already happened.\n\t\tsignalGeneration(owner.pid, \"SIGUSR1\");\n\t\treturn {\n\t\t\taction: \"handoff\",\n\t\t\tpid: child.pid,\n\t\t\tsocket: options.socket,\n\t\t\tgeneration: answer.generation ?? generation,\n\t\t\tinstanceId: answer.instanceId ?? \"\",\n\t\t};\n\t} catch (cause) {\n\t\tif (child.exitCode === null && child.signalCode === null) child.kill(\"SIGKILL\");\n\t\treturn {\n\t\t\taction: \"refuse\",\n\t\t\treason: \"successor_unavailable\",\n\t\t\tupgradeable: true,\n\t\t\tdetail: cause instanceof Error ? cause.message : String(cause),\n\t\t};\n\t}\n}\n\n/**\n * The successor's own answer on the PUBLIC socket is the only proof the rename landed: it\n * reports a different `instanceId` than the generation being replaced. An exit instead means\n * the successor refused to replace the path (a foreign socket, a live bind path) and left it alone.\n */\nasync function awaitSuccessor(\n\toptions: HandoffHostOptions,\n\tprevious: HostProtocolInfo,\n\texited: Promise<void>,\n): Promise<HostProtocolInfo | undefined> {\n\tconst deadline = Date.now() + (options._test?.readinessTimeoutMs ?? DEFAULT_HANDOFF_READINESS_MS);\n\tlet childGone = false;\n\tvoid exited.then(() => {\n\t\tchildGone = true;\n\t});\n\twhile (Date.now() <= deadline) {\n\t\tconst answer = await probeProtocolInfo(options.socket, 2_000);\n\t\tif (answer && answer.instanceId !== undefined && answer.instanceId !== previous.instanceId) return answer;\n\t\tif (childGone) return undefined;\n\t\tawait delay(50);\n\t}\n\treturn undefined;\n}\n\n/**\n * What actually stopped the handoff, read from the endpoint rather than guessed: a public path\n * that no longer holds the socket this handoff was decided against was taken by somebody else,\n * and the successor correctly refused to rename over it.\n */\nasync function abortReason(socket: string, replaced: SocketFileIdentity): Promise<HandoffRefusal> {\n\tconst current = await statSocketIdentity(socket).catch(() => undefined);\n\treturn current === undefined || current.dev !== replaced.dev || current.ino !== replaced.ino\n\t\t? \"socket_replaced\"\n\t\t: \"successor_unavailable\";\n}\n\nfunction delay(ms: number): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"host-successor.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-successor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bringing up the SUCCESSOR generation: where it binds, what it is launched with, and the single\n * observation that proves it took the socket over.\n *\n * The successor binds `<public>.next-<gen>` - never the live public path, which it has no right to\n * unlink - and renames its own entry over the public path only while that path still holds the exact\n * socket this handoff was decided against (`--replace <dev>:<ino>`). Its own answer ON THE PUBLIC\n * SOCKET, reporting an `instanceId` different from the generation being replaced, is the only proof\n * the rename landed; an exit instead means it refused to replace a path that changed underneath and\n * left both sockets alone. The predecessor is asked to drain only AFTER that proof and after the\n * successor is registered - whether it may be asked at all was decided in `host-handoff.ts`, which\n * proved the owner and checked that the running host advertises it can survive the signal.\n *\n * A REFUSED handoff leaves the endpoint as it found it: the successor is killed and, once it has\n * exited, its generation record and directory are released, and the boot `settings.json` it\n * overwrote before spawning is put back byte for byte - so neither `status --all` nor the running\n * generation's next restart ever sees the successor that did not happen.\n */\nimport { type ChildProcess, spawn } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { open, rm, writeFile } from \"node:fs/promises\";\nimport { waitForStartTime } from \"../app-server/daemon/process.ts\";\nimport { generationPaths, HOST_STATE_FILE_MODE, type HostDaemonPaths } from \"./host-daemon-paths.ts\";\nimport { releaseGeneration, writeGenerationRecord, writeHostRegistration } from \"./host-daemon-registration.ts\";\nimport { readFileOrUndefined, readHostSettings, writeHostSettings } from \"./host-daemon-state.ts\";\nimport type { HandoffHostOptions, HandoffRefusal, HandoffResult } from \"./host-handoff.ts\";\nimport { defaultHostLaunch, PINNED_HOST_CLIENT_CAPABILITIES } from \"./host-launch.ts\";\nimport { DEFAULT_HOST_IDLE_EXIT_MS } from \"./host-lifecycle.ts\";\nimport { probeProtocolInfo } from \"./host-probe.ts\";\nimport type { HostProtocolInfo } from \"./host-protocol-info.ts\";\nimport { successorHostEnvironment } from \"./host-spawn-environment.ts\";\nimport { signalGeneration } from \"./host-stop.ts\";\nimport { hostLaunchProfile } from \"./protocol-identity.ts\";\nimport {\n\tgenerationBindPath,\n\tMAX_SOCKET_PATH_BYTES,\n\ttype SocketFileIdentity,\n\tstatSocketIdentity,\n} from \"./socket-ownership.ts\";\n\n/** How long the successor has to answer on the PUBLIC socket before the handoff is abandoned. */\nconst DEFAULT_HANDOFF_READINESS_MS = 30_000;\n\n/** How long a refused handoff waits for the successor it killed to exit before releasing its record. */\nconst SUCCESSOR_EXIT_WAIT_MS = 5_000;\n\n/** The longest `startSuccessor` runs: its start-time read, the readiness window, one last probe and a refusal's exit wait. */\nexport const SUCCESSOR_START_BUDGET_MS = 10_000 + DEFAULT_HANDOFF_READINESS_MS + 2_000 + SUCCESSOR_EXIT_WAIT_MS;\n\nexport async function startSuccessor(context: {\n\toptions: HandoffHostOptions;\n\tpaths: HostDaemonPaths;\n\thost: HostProtocolInfo;\n\towner: { pid: number; processStartTime: string; instanceId: string };\n}): Promise<HandoffResult> {\n\tconst { options, paths, host, owner } = context;\n\tconst generation = (host.generation ?? 0) + 1;\n\t// The successor's identity, chosen here so its generation directory holds its settings before it\n\t// boots and the pointer can name it the instant it answers on the public socket.\n\tconst instanceId = randomUUID();\n\tconst bindSocket = generationBindPath(options.socket, generation);\n\tif (Buffer.byteLength(bindSocket) > MAX_SOCKET_PATH_BYTES) {\n\t\treturn { action: \"refuse\", reason: \"socket_path_too_long\", upgradeable: true, detail: bindSocket };\n\t}\n\tconst replaced = await statSocketIdentity(options.socket);\n\tif (!replaced) return { action: \"refuse\", reason: \"socket_replaced\", upgradeable: true };\n\t// A handoff replaces the ENGINE, not the operator's lifecycle policy: the successor inherits\n\t// what the running generation was started with unless this caller states its own.\n\tconst running = await readHostSettings(paths);\n\tconst bootSettings = await readFileOrUndefined(paths.settingsFile);\n\tawait writeHostSettings(paths, {\n\t\tsocket: options.socket,\n\t\tcapabilities: PINNED_HOST_CLIENT_CAPABILITIES,\n\t\tcoldStart: options.policy?.coldStart ?? running?.coldStart ?? \"transient\",\n\t\tidleExitMs: options.policy?.idleExitMs ?? running?.idleExitMs ?? DEFAULT_HOST_IDLE_EXIT_MS,\n\t\tgeneration,\n\t\tinstanceId,\n\t});\n\t// From the overwrite on, every exit is a refusal that puts the boot settings back: a failure BEFORE\n\t// the successor existed (the hook, the launch build, the stderr open, the spawn itself) has nothing\n\t// to kill or release, but the settings it already overwrote are not the successor's to keep.\n\tlet child: ChildProcess | undefined;\n\tlet exited: Promise<void> | undefined;\n\ttry {\n\t\tawait options._test?.beforeSpawn?.();\n\t\tconst argv = [\n\t\t\t\"--socket\",\n\t\t\toptions.socket,\n\t\t\t\"--bind\",\n\t\t\tbindSocket,\n\t\t\t\"--replace\",\n\t\t\t`${replaced.dev}:${replaced.ino}`,\n\t\t\t...(options.hostArgs ?? []),\n\t\t];\n\t\tconst launch = options._test?.launch?.(argv) ?? defaultHostLaunch(argv);\n\t\tconst stderr = await open(paths.stderrLog, \"a\", 0o600);\n\t\tconst spawned = spawn(launch.command, [...launch.args], {\n\t\t\tdetached: true,\n\t\t\twindowsHide: true,\n\t\t\tenv: successorHostEnvironment({\n\t\t\t\tagentDir: options.agentDir,\n\t\t\t\tenv: options.env,\n\t\t\t\tpaths,\n\t\t\t\tgeneration,\n\t\t\t\tinstanceId,\n\t\t\t}),\n\t\t\tstdio: [\"ignore\", \"ignore\", stderr.fd],\n\t\t});\n\t\tawait stderr.close();\n\t\tchild = spawned;\n\t\texited = new Promise<void>((resolve) => spawned.once(\"exit\", () => resolve()));\n\t\tif (child.pid === undefined) throw new Error(\"failed to spawn the successor generation\");\n\t\tconst processStartTime = (await waitForStartTime(child.pid, 10_000).catch(() => undefined)) ?? null;\n\t\tconst registration = {\n\t\t\trecord: { pid: child.pid, processStartTime },\n\t\t\tsocket: options.socket,\n\t\t\tinstanceId,\n\t\t\tgeneration,\n\t\t\tlaunchProfileId: hostLaunchProfile(\n\t\t\t\t[\"--mode\", \"rpc\", \"--multi-session\", ...(options.hostArgs ?? [])],\n\t\t\t\tprocess.cwd(),\n\t\t\t).profile_id,\n\t\t};\n\t\t// The successor is running from this instant, long before it binds anything: its own record says\n\t\t// so, so `host gc` never judges the endpoint by a predecessor that died meanwhile. The pointer is\n\t\t// NOT moved here - the predecessor still owns the socket until the rename lands.\n\t\tawait writeGenerationRecord(paths, registration);\n\t\tawait options._test?.afterSpawn?.(child.pid);\n\t\tconst answer = await awaitSuccessor(options, host, exited);\n\t\tif (!answer) {\n\t\t\tconst reason = await abortReason(options.socket, replaced);\n\t\t\tconst cleanupFailure = await abandonSuccessor({ child, exited, paths, instanceId, bootSettings });\n\t\t\treturn { action: \"refuse\", reason, upgradeable: true, ...(cleanupFailure && { detail: cleanupFailure }) };\n\t\t}\n\t\t// The pointer moves to the successor only now: until the rename landed, the generation the\n\t\t// clients reach is still the predecessor, and the pointer has to name whoever owns the socket.\n\t\tawait writeHostRegistration(paths, registration);\n\t\tchild.unref();\n\t\t// The successor owns the socket now: the predecessor may drain. SIGUSR1 is sent only here,\n\t\t// to a pid the record proved and a host that advertised it can survive the signal. A\n\t\t// predecessor that exited on its own in the meantime is already drained, and the handoff it\n\t\t// was being asked to make room for has already happened.\n\t\tsignalGeneration(owner.pid, \"SIGUSR1\");\n\t\treturn {\n\t\t\taction: \"handoff\",\n\t\t\tpid: child.pid,\n\t\t\tsocket: options.socket,\n\t\t\tgeneration: answer.generation ?? generation,\n\t\t\tinstanceId: answer.instanceId ?? \"\",\n\t\t};\n\t} catch (cause) {\n\t\tconst cleanupFailure = await abandonSuccessor({ child, exited, paths, instanceId, bootSettings });\n\t\tconst detail = cause instanceof Error ? cause.message : String(cause);\n\t\treturn {\n\t\t\taction: \"refuse\",\n\t\t\treason: \"successor_unavailable\",\n\t\t\tupgradeable: true,\n\t\t\tdetail: cleanupFailure ? `${detail}; ${cleanupFailure}` : detail,\n\t\t};\n\t}\n}\n\n/**\n * Undoes what a refused handoff left: kills the successor, releases its generation once it has exited\n * (a successor that never got a pid never ran, so its directory goes at once; one that outlives the wait\n * keeps its record until pruning finds it dead), and puts the boot settings back as they were. A\n * successor that was never spawned leaves only the boot settings to undo. A cleanup that fails is\n * answered, not thrown: the handoff still refused, and the caller is told why\n * the endpoint may not be as it was.\n */\nasync function abandonSuccessor(context: {\n\t/** Undefined when the successor was never spawned: nothing to kill or release. */\n\tchild: ChildProcess | undefined;\n\texited: Promise<void> | undefined;\n\tpaths: HostDaemonPaths;\n\tinstanceId: string;\n\tbootSettings: string | undefined;\n}): Promise<string | undefined> {\n\tconst { child, exited, paths, instanceId, bootSettings } = context;\n\tif (child !== undefined && child.exitCode === null && child.signalCode === null) child.kill(\"SIGKILL\");\n\ttry {\n\t\tif (bootSettings === undefined) await rm(paths.settingsFile, { force: true });\n\t\telse await writeFile(paths.settingsFile, bootSettings, { mode: HOST_STATE_FILE_MODE });\n\t\tif (child?.pid === undefined) await rm(generationPaths(paths, instanceId).dir, { recursive: true, force: true });\n\t\telse if (exited !== undefined && (await exitedWithin(exited, SUCCESSOR_EXIT_WAIT_MS))) {\n\t\t\tawait releaseGeneration(paths, { instanceId, pid: child.pid });\n\t\t}\n\t\treturn undefined;\n\t} catch (cause) {\n\t\treturn `successor cleanup failed: ${cause instanceof Error ? cause.message : String(cause)}`;\n\t}\n}\n\nasync function exitedWithin(exited: Promise<void>, ms: number): Promise<boolean> {\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst timedOut = new Promise<false>((resolve) => {\n\t\ttimer = setTimeout(() => resolve(false), ms);\n\t});\n\ttry {\n\t\treturn await Promise.race([exited.then(() => true), timedOut]);\n\t} finally {\n\t\tclearTimeout(timer);\n\t}\n}\n\n/**\n * The successor's own answer on the PUBLIC socket is the only proof the rename landed: it\n * reports a different `instanceId` than the generation being replaced. An exit instead means\n * the successor refused to replace the path (a foreign socket, a live bind path) and left it alone.\n */\nasync function awaitSuccessor(\n\toptions: HandoffHostOptions,\n\tprevious: HostProtocolInfo,\n\texited: Promise<void>,\n): Promise<HostProtocolInfo | undefined> {\n\tconst deadline = Date.now() + (options._test?.readinessTimeoutMs ?? DEFAULT_HANDOFF_READINESS_MS);\n\tlet childGone = false;\n\tvoid exited.then(() => {\n\t\tchildGone = true;\n\t});\n\twhile (Date.now() <= deadline) {\n\t\tconst answer = await probeProtocolInfo(options.socket, 2_000);\n\t\tif (answer && answer.instanceId !== undefined && answer.instanceId !== previous.instanceId) return answer;\n\t\tif (childGone) return undefined;\n\t\tawait delay(50);\n\t}\n\treturn undefined;\n}\n\n/**\n * What actually stopped the handoff, read from the endpoint rather than guessed: a public path\n * that no longer holds the socket this handoff was decided against was taken by somebody else,\n * and the successor correctly refused to rename over it.\n */\nasync function abortReason(socket: string, replaced: SocketFileIdentity): Promise<HandoffRefusal> {\n\tconst current = await statSocketIdentity(socket).catch(() => undefined);\n\treturn current === undefined || current.dev !== replaced.dev || current.ino !== replaced.ino\n\t\t? \"socket_replaced\"\n\t\t: \"successor_unavailable\";\n}\n\nfunction delay(ms: number): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms));\n}\n"]}
|
|
@@ -43,4 +43,10 @@ export declare function readHostWatchdogConfigFromBrandEnv(): HostWatchdogConfig
|
|
|
43
43
|
* binding is configured.
|
|
44
44
|
*/
|
|
45
45
|
export declare function armHostWatchdog(config: HostWatchdogConfig | undefined, onSupervisorGone: (reason: string, cleanup?: Promise<void>) => void): () => void;
|
|
46
|
+
/**
|
|
47
|
+
* Removes the cleanup paths one at a time, in list order, on every platform: the supervisor puts
|
|
48
|
+
* the registration pointer last (host-cleanup-paths.ts), and it must not vanish before the state
|
|
49
|
+
* it describes (senpi#2241).
|
|
50
|
+
*/
|
|
51
|
+
export declare function cleanupWatchdogPaths(config: HostWatchdogConfig): Promise<void>;
|
|
46
52
|
//# sourceMappingURL=host-watchdog.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-watchdog.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/host-watchdog.ts"],"names":[],"mappings":"AAqBA,sFAAsF;AACtF,eAAO,MAAM,iBAAiB,4BAA4B,CAAC;AAC3D,gFAAgF;AAChF,eAAO,MAAM,oBAAoB,+BAA+B,CAAC;AACjE,sFAAsF;AACtF,eAAO,MAAM,sBAAsB,iCAAiC,CAAC;AACrE,yEAAyE;AACzE,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAC/D,0CAA0C;AAC1C,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAC/C,eAAO,MAAM,sBAAsB,iCAAiC,CAAC;AAErE,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAQD;;;;GAIG;AACH,wBAAgB,sBAAsB,CACrC,GAAG,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,GAC7D,kBAAkB,GAAG,SAAS,CAchC;AAED,yEAAyE;AACzE,wBAAgB,kCAAkC,IAAI,kBAAkB,GAAG,SAAS,CAYnF;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC9B,MAAM,EAAE,kBAAkB,GAAG,SAAS,EACtC,gBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,GACjE,MAAM,IAAI,CAsBZ"}
|
|
1
|
+
{"version":3,"file":"host-watchdog.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/host-watchdog.ts"],"names":[],"mappings":"AAqBA,sFAAsF;AACtF,eAAO,MAAM,iBAAiB,4BAA4B,CAAC;AAC3D,gFAAgF;AAChF,eAAO,MAAM,oBAAoB,+BAA+B,CAAC;AACjE,sFAAsF;AACtF,eAAO,MAAM,sBAAsB,iCAAiC,CAAC;AACrE,yEAAyE;AACzE,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAC/D,0CAA0C;AAC1C,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAC/C,eAAO,MAAM,sBAAsB,iCAAiC,CAAC;AAErE,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAQD;;;;GAIG;AACH,wBAAgB,sBAAsB,CACrC,GAAG,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,GAC7D,kBAAkB,GAAG,SAAS,CAchC;AAED,yEAAyE;AACzE,wBAAgB,kCAAkC,IAAI,kBAAkB,GAAG,SAAS,CAYnF;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC9B,MAAM,EAAE,kBAAkB,GAAG,SAAS,EACtC,gBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,GACjE,MAAM,IAAI,CAsBZ;AA6FD;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CASpF"}
|
|
@@ -202,22 +202,21 @@ function captureBeforeCleanup(config) {
|
|
|
202
202
|
.then(config.beforeCleanup)
|
|
203
203
|
.catch(() => undefined);
|
|
204
204
|
}
|
|
205
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Removes the cleanup paths one at a time, in list order, on every platform: the supervisor puts
|
|
207
|
+
* the registration pointer last (host-cleanup-paths.ts), and it must not vanish before the state
|
|
208
|
+
* it describes (senpi#2241).
|
|
209
|
+
*/
|
|
210
|
+
export async function cleanupWatchdogPaths(config) {
|
|
206
211
|
const paths = [...(config.cleanupPaths ?? []), ...(config.scratchDir ? [config.scratchDir] : [])];
|
|
207
|
-
|
|
208
|
-
for (const path of paths) {
|
|
209
|
-
try {
|
|
210
|
-
rmSync(path, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
211
|
-
}
|
|
212
|
-
catch { }
|
|
213
|
-
}
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
await Promise.all(paths.map(async (path) => {
|
|
212
|
+
for (const path of paths) {
|
|
217
213
|
try {
|
|
218
|
-
|
|
214
|
+
if (process.platform === "win32")
|
|
215
|
+
rmSync(path, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
216
|
+
else
|
|
217
|
+
await rm(path, { recursive: true, force: true });
|
|
219
218
|
}
|
|
220
219
|
catch { }
|
|
221
|
-
}
|
|
220
|
+
}
|
|
222
221
|
}
|
|
223
222
|
//# sourceMappingURL=host-watchdog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-watchdog.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-watchdog.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Opt-in supervisor-lifetime watchdog for the RPC socket host.\n *\n * A host started under the `host-lifecycle.ts` supervisor must not outlive it.\n * Catchable signals cannot carry that guarantee: `kill -9`, an OOM kill, or a\n * crashed supervisor run no JS handler, and the host is then reparented to init\n * as a permanent orphan holding its private socket forever.\n *\n * The binding used here is the OS itself. The supervisor spawns the host with an\n * extra inherited pipe and keeps the write end open without ever writing to it.\n * The kernel closes that end when the supervisor dies for ANY reason, so the\n * host's read end reaches EOF and the host shuts down cleanly.\n *\n * Both variables are unset for every other host launch, so nothing changes for\n * plain `senpi --mode rpc` runs, hosts started by hand, or embedders.\n */\nimport { rmSync } from \"node:fs\";\nimport { rm } from \"node:fs/promises\";\nimport { Socket } from \"node:net\";\nimport { envValue } from \"../../core/brand.ts\";\n\n/** Inherited fd whose EOF means \"the supervisor died\"; set by the supervisor only. */\nexport const HOST_WATCH_FD_ENV = \"SENPI_RPC_HOST_WATCH_FD\";\n/** Supervisor-owned private directory the host removes on watchdog shutdown. */\nexport const HOST_SCRATCH_DIR_ENV = \"SENPI_RPC_HOST_SCRATCH_DIR\";\n/** Public socket path the supervisor bound; the host removes it ownership-checked. */\nexport const HOST_PUBLIC_SOCKET_ENV = \"SENPI_RPC_HOST_PUBLIC_SOCKET\";\n/** Fallback binding when no inherited fd is available: poll this pid. */\nexport const HOST_WATCH_PPID_ENV = \"SENPI_RPC_HOST_WATCH_PPID\";\n/** Poll cadence for the ppid fallback. */\nexport const HOST_WATCH_PPID_INTERVAL_MS = 250;\nexport const HOST_CLEANUP_PATHS_ENV = \"SENPI_RPC_HOST_CLEANUP_PATHS\";\n\nexport interface HostWatchdogConfig {\n\treadonly fd?: number;\n\treadonly ppid?: number;\n\treadonly scratchDir?: string;\n\treadonly cleanupPaths?: readonly string[];\n\treadonly publicSocket?: string;\n\t/**\n\t * Runs when the watchdog fires, BEFORE `scratchDir` and `cleanupPaths` are removed. The\n\t * host uses it to read state that lives inside the supervisor's private directory and\n\t * that its shutdown still needs - the public-socket ownership token above all. A\n\t * supervisor killed while the host is still starting up leaves that token unread, and\n\t * removing the directory first would turn the public socket into an unprovable orphan.\n\t */\n\treadonly beforeCleanup?: () => Promise<void>;\n}\n\nfunction parsePositiveInteger(value: string | undefined): number | undefined {\n\tif (value === undefined || !/^\\d+$/.test(value.trim())) return undefined;\n\tconst parsed = Number(value.trim());\n\treturn Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;\n}\n\n/**\n * Reads the watchdog configuration from the environment. Returns `undefined`\n * when neither binding is requested, which is the case for every host launch\n * that does not come from the lifecycle supervisor.\n */\nexport function readHostWatchdogConfig(\n\tenv: Readonly<Record<string, string | undefined>> = process.env,\n): HostWatchdogConfig | undefined {\n\tconst fd = parsePositiveInteger(env[HOST_WATCH_FD_ENV]);\n\tconst ppid = parsePositiveInteger(env[HOST_WATCH_PPID_ENV]);\n\tif (fd === undefined && ppid === undefined) return undefined;\n\tconst scratchDir = env[HOST_SCRATCH_DIR_ENV];\n\tconst cleanupPaths = env[HOST_CLEANUP_PATHS_ENV]?.split(\"\\n\").filter(Boolean);\n\tconst publicSocket = env[HOST_PUBLIC_SOCKET_ENV];\n\treturn {\n\t\tfd,\n\t\tppid,\n\t\tscratchDir: scratchDir === undefined || scratchDir === \"\" ? undefined : scratchDir,\n\t\tcleanupPaths,\n\t\tpublicSocket: publicSocket === \"\" ? undefined : publicSocket,\n\t};\n}\n\n/** Same configuration, resolved through the brand-aware env prefixes. */\nexport function readHostWatchdogConfigFromBrandEnv(): HostWatchdogConfig | undefined {\n\t// The lifecycle supervisor intentionally uses the canonical SENPI_RPC_HOST_*\n\t// names when spawning a child. envValue() resolves branded aliases for normal\n\t// launches, but must not hide the canonical variables from the supervisor\n\t// handoff or the lifetime binding is silently disabled.\n\treturn readHostWatchdogConfig({\n\t\t[HOST_WATCH_FD_ENV]: process.env[HOST_WATCH_FD_ENV] ?? envValue(\"RPC_HOST_WATCH_FD\"),\n\t\t[HOST_WATCH_PPID_ENV]: process.env[HOST_WATCH_PPID_ENV] ?? envValue(\"RPC_HOST_WATCH_PPID\"),\n\t\t[HOST_SCRATCH_DIR_ENV]: process.env[HOST_SCRATCH_DIR_ENV] ?? envValue(\"RPC_HOST_SCRATCH_DIR\"),\n\t\t[HOST_PUBLIC_SOCKET_ENV]: process.env[HOST_PUBLIC_SOCKET_ENV] ?? envValue(\"RPC_HOST_PUBLIC_SOCKET\"),\n\t\t[HOST_CLEANUP_PATHS_ENV]: process.env[HOST_CLEANUP_PATHS_ENV] ?? envValue(\"RPC_HOST_CLEANUP_PATHS\"),\n\t});\n}\n\n/**\n * Arms the configured watchdog. `onSupervisorGone` receives the reason and is\n * expected to perform the host's normal clean shutdown; the scratch directory\n * (the supervisor's private socket directory, which no longer has an owner) is\n * removed first so nothing is left behind even if shutdown then hangs.\n *\n * Takes ownership of `config.fd`: disarming (or firing) closes it, so callers\n * must not close it themselves. Returns a disarm function; a no-op when no\n * binding is configured.\n */\nexport function armHostWatchdog(\n\tconfig: HostWatchdogConfig | undefined,\n\tonSupervisorGone: (reason: string, cleanup?: Promise<void>) => void,\n): () => void {\n\tif (!config) return () => {};\n\tconst fire = (reason: string): void => {\n\t\tdisarm();\n\t\tconst captured = captureBeforeCleanup(config);\n\t\tif (process.platform === \"win32\") {\n\t\t\t// Arm the host shutdown fallback before attempting metadata cleanup. The\n\t\t\t// synchronous Win32 removal below handles the state files deterministically,\n\t\t\t// while the fallback still covers a named-pipe close that never completes.\n\t\t\tconst cleanup = captured.then(() => cleanupWatchdogPaths(config));\n\t\t\tonSupervisorGone(reason, cleanup);\n\t\t} else {\n\t\t\tvoid captured.then(() => cleanupWatchdogPaths(config)).finally(() => onSupervisorGone(reason));\n\t\t}\n\t};\n\tconst disarmers: Array<() => void> = [];\n\tconst disarm = (): void => {\n\t\tfor (const stop of disarmers.splice(0)) stop();\n\t};\n\tif (config.fd !== undefined) disarmers.push(watchFdForEof(config.fd, fire));\n\tif (config.ppid !== undefined) disarmers.push(watchPpid(config.ppid, fire));\n\treturn disarm;\n}\n\n/**\n * EOF on the inherited pipe is the primary signal. The supervisor never writes\n * to it, so any readable data is ignored; only close matters. An fd that cannot\n * be opened (never inherited) leaves the binding inert rather than killing a\n * healthy host.\n *\n * The pipe is read through a net.Socket rather than a file stream because the read has to live\n * on the EVENT LOOP, not in libuv's thread pool. A thread-pool read blocks in `read(2)` until the\n * supervisor dies, and `process.exit()` joins the thread pool before it terminates - so a host\n * that decides to exit on its own (an idle exit, or the park-everything drain of a generation\n * handoff) would hang in exit until something killed it. With a socket the same EOF arrives from\n * kqueue/epoll and nothing holds the exit.\n */\nfunction watchFdForEof(fd: number, fire: (reason: string) => void): () => void {\n\tlet stream: Socket;\n\tlet streamFailed = false;\n\tlet fired = false;\n\tconst fireOnce = (reason: string): void => {\n\t\tif (fired) return;\n\t\tfired = true;\n\t\tfire(reason);\n\t};\n\ttry {\n\t\t// The watchdog owns this inherited read end; destroying the socket releases it.\n\t\tstream = new Socket({ fd, readable: true, writable: false });\n\t} catch {\n\t\treturn () => {};\n\t}\n\tstream.resume();\n\tconst onEnd = (): void => fireOnce(`supervisor pipe fd ${fd} closed`);\n\tconst onClose = (): void => {\n\t\tif (!streamFailed) onEnd();\n\t};\n\tconst onError = (): void => {\n\t\tstreamFailed = true;\n\t};\n\tstream.once(\"end\", onEnd);\n\t// Win32 pipe teardown may report close without end; POSIX invalid-fd close is\n\t// not supervisor death and remains inert.\n\tif (process.platform === \"win32\") stream.once(\"close\", onClose);\n\tstream.once(\"error\", onError);\n\treturn () => {\n\t\tstream.off(\"end\", onEnd);\n\t\tstream.off(\"close\", onClose);\n\t\tstream.off(\"error\", onError);\n\t\tstream.destroy();\n\t};\n}\n\n/**\n * Fallback binding: the supervisor pid is gone, or this process was reparented\n * to init because the supervisor died. Polling covers the case where no extra\n * fd could be inherited.\n */\nfunction watchPpid(supervisorPid: number, fire: (reason: string) => void): () => void {\n\t// Tests and embedders may bind the watchdog to this process itself; that\n\t// is a valid live binding rather than evidence of supervisor loss.\n\tif (supervisorPid === process.pid) return () => {};\n\tconst timer = setInterval(() => {\n\t\tif (!processAlive(supervisorPid)) {\n\t\t\tfire(`supervisor pid ${supervisorPid} is gone (ppid=${process.ppid})`);\n\t\t\treturn;\n\t\t}\n\t\t// A dead supervisor is reaped by its own parent and this process is then\n\t\t// reparented, so the ppid comparison observes the loss without spawning\n\t\t// `ps` every tick; both checks here are free syscalls.\n\t\tif (process.ppid !== supervisorPid) {\n\t\t\tfire(`supervisor pid ${supervisorPid} is gone (ppid=${process.ppid})`);\n\t\t}\n\t}, HOST_WATCH_PPID_INTERVAL_MS);\n\ttimer.unref?.();\n\treturn () => clearInterval(timer);\n}\n\nfunction processAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (cause) {\n\t\treturn cause instanceof Error && \"code\" in cause && cause.code !== \"ESRCH\";\n\t}\n}\n\n// A failing capture must never block the cleanup or the shutdown it precedes.\nfunction captureBeforeCleanup(config: HostWatchdogConfig): Promise<void> {\n\tif (config.beforeCleanup === undefined) return Promise.resolve();\n\treturn Promise.resolve()\n\t\t.then(config.beforeCleanup)\n\t\t.catch(() => undefined);\n}\n\nasync function cleanupWatchdogPaths(config: HostWatchdogConfig): Promise<void> {\n\tconst paths = [...(config.cleanupPaths ?? []), ...(config.scratchDir ? [config.scratchDir] : [])];\n\tif (process.platform === \"win32\") {\n\t\tfor (const path of paths) {\n\t\t\ttry {\n\t\t\t\trmSync(path, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });\n\t\t\t} catch {}\n\t\t}\n\t\treturn;\n\t}\n\tawait Promise.all(\n\t\tpaths.map(async (path) => {\n\t\t\ttry {\n\t\t\t\tawait rm(path, { recursive: true, force: true });\n\t\t\t} catch {}\n\t\t}),\n\t);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"host-watchdog.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-watchdog.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Opt-in supervisor-lifetime watchdog for the RPC socket host.\n *\n * A host started under the `host-lifecycle.ts` supervisor must not outlive it.\n * Catchable signals cannot carry that guarantee: `kill -9`, an OOM kill, or a\n * crashed supervisor run no JS handler, and the host is then reparented to init\n * as a permanent orphan holding its private socket forever.\n *\n * The binding used here is the OS itself. The supervisor spawns the host with an\n * extra inherited pipe and keeps the write end open without ever writing to it.\n * The kernel closes that end when the supervisor dies for ANY reason, so the\n * host's read end reaches EOF and the host shuts down cleanly.\n *\n * Both variables are unset for every other host launch, so nothing changes for\n * plain `senpi --mode rpc` runs, hosts started by hand, or embedders.\n */\nimport { rmSync } from \"node:fs\";\nimport { rm } from \"node:fs/promises\";\nimport { Socket } from \"node:net\";\nimport { envValue } from \"../../core/brand.ts\";\n\n/** Inherited fd whose EOF means \"the supervisor died\"; set by the supervisor only. */\nexport const HOST_WATCH_FD_ENV = \"SENPI_RPC_HOST_WATCH_FD\";\n/** Supervisor-owned private directory the host removes on watchdog shutdown. */\nexport const HOST_SCRATCH_DIR_ENV = \"SENPI_RPC_HOST_SCRATCH_DIR\";\n/** Public socket path the supervisor bound; the host removes it ownership-checked. */\nexport const HOST_PUBLIC_SOCKET_ENV = \"SENPI_RPC_HOST_PUBLIC_SOCKET\";\n/** Fallback binding when no inherited fd is available: poll this pid. */\nexport const HOST_WATCH_PPID_ENV = \"SENPI_RPC_HOST_WATCH_PPID\";\n/** Poll cadence for the ppid fallback. */\nexport const HOST_WATCH_PPID_INTERVAL_MS = 250;\nexport const HOST_CLEANUP_PATHS_ENV = \"SENPI_RPC_HOST_CLEANUP_PATHS\";\n\nexport interface HostWatchdogConfig {\n\treadonly fd?: number;\n\treadonly ppid?: number;\n\treadonly scratchDir?: string;\n\treadonly cleanupPaths?: readonly string[];\n\treadonly publicSocket?: string;\n\t/**\n\t * Runs when the watchdog fires, BEFORE `scratchDir` and `cleanupPaths` are removed. The\n\t * host uses it to read state that lives inside the supervisor's private directory and\n\t * that its shutdown still needs - the public-socket ownership token above all. A\n\t * supervisor killed while the host is still starting up leaves that token unread, and\n\t * removing the directory first would turn the public socket into an unprovable orphan.\n\t */\n\treadonly beforeCleanup?: () => Promise<void>;\n}\n\nfunction parsePositiveInteger(value: string | undefined): number | undefined {\n\tif (value === undefined || !/^\\d+$/.test(value.trim())) return undefined;\n\tconst parsed = Number(value.trim());\n\treturn Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;\n}\n\n/**\n * Reads the watchdog configuration from the environment. Returns `undefined`\n * when neither binding is requested, which is the case for every host launch\n * that does not come from the lifecycle supervisor.\n */\nexport function readHostWatchdogConfig(\n\tenv: Readonly<Record<string, string | undefined>> = process.env,\n): HostWatchdogConfig | undefined {\n\tconst fd = parsePositiveInteger(env[HOST_WATCH_FD_ENV]);\n\tconst ppid = parsePositiveInteger(env[HOST_WATCH_PPID_ENV]);\n\tif (fd === undefined && ppid === undefined) return undefined;\n\tconst scratchDir = env[HOST_SCRATCH_DIR_ENV];\n\tconst cleanupPaths = env[HOST_CLEANUP_PATHS_ENV]?.split(\"\\n\").filter(Boolean);\n\tconst publicSocket = env[HOST_PUBLIC_SOCKET_ENV];\n\treturn {\n\t\tfd,\n\t\tppid,\n\t\tscratchDir: scratchDir === undefined || scratchDir === \"\" ? undefined : scratchDir,\n\t\tcleanupPaths,\n\t\tpublicSocket: publicSocket === \"\" ? undefined : publicSocket,\n\t};\n}\n\n/** Same configuration, resolved through the brand-aware env prefixes. */\nexport function readHostWatchdogConfigFromBrandEnv(): HostWatchdogConfig | undefined {\n\t// The lifecycle supervisor intentionally uses the canonical SENPI_RPC_HOST_*\n\t// names when spawning a child. envValue() resolves branded aliases for normal\n\t// launches, but must not hide the canonical variables from the supervisor\n\t// handoff or the lifetime binding is silently disabled.\n\treturn readHostWatchdogConfig({\n\t\t[HOST_WATCH_FD_ENV]: process.env[HOST_WATCH_FD_ENV] ?? envValue(\"RPC_HOST_WATCH_FD\"),\n\t\t[HOST_WATCH_PPID_ENV]: process.env[HOST_WATCH_PPID_ENV] ?? envValue(\"RPC_HOST_WATCH_PPID\"),\n\t\t[HOST_SCRATCH_DIR_ENV]: process.env[HOST_SCRATCH_DIR_ENV] ?? envValue(\"RPC_HOST_SCRATCH_DIR\"),\n\t\t[HOST_PUBLIC_SOCKET_ENV]: process.env[HOST_PUBLIC_SOCKET_ENV] ?? envValue(\"RPC_HOST_PUBLIC_SOCKET\"),\n\t\t[HOST_CLEANUP_PATHS_ENV]: process.env[HOST_CLEANUP_PATHS_ENV] ?? envValue(\"RPC_HOST_CLEANUP_PATHS\"),\n\t});\n}\n\n/**\n * Arms the configured watchdog. `onSupervisorGone` receives the reason and is\n * expected to perform the host's normal clean shutdown; the scratch directory\n * (the supervisor's private socket directory, which no longer has an owner) is\n * removed first so nothing is left behind even if shutdown then hangs.\n *\n * Takes ownership of `config.fd`: disarming (or firing) closes it, so callers\n * must not close it themselves. Returns a disarm function; a no-op when no\n * binding is configured.\n */\nexport function armHostWatchdog(\n\tconfig: HostWatchdogConfig | undefined,\n\tonSupervisorGone: (reason: string, cleanup?: Promise<void>) => void,\n): () => void {\n\tif (!config) return () => {};\n\tconst fire = (reason: string): void => {\n\t\tdisarm();\n\t\tconst captured = captureBeforeCleanup(config);\n\t\tif (process.platform === \"win32\") {\n\t\t\t// Arm the host shutdown fallback before attempting metadata cleanup. The\n\t\t\t// synchronous Win32 removal below handles the state files deterministically,\n\t\t\t// while the fallback still covers a named-pipe close that never completes.\n\t\t\tconst cleanup = captured.then(() => cleanupWatchdogPaths(config));\n\t\t\tonSupervisorGone(reason, cleanup);\n\t\t} else {\n\t\t\tvoid captured.then(() => cleanupWatchdogPaths(config)).finally(() => onSupervisorGone(reason));\n\t\t}\n\t};\n\tconst disarmers: Array<() => void> = [];\n\tconst disarm = (): void => {\n\t\tfor (const stop of disarmers.splice(0)) stop();\n\t};\n\tif (config.fd !== undefined) disarmers.push(watchFdForEof(config.fd, fire));\n\tif (config.ppid !== undefined) disarmers.push(watchPpid(config.ppid, fire));\n\treturn disarm;\n}\n\n/**\n * EOF on the inherited pipe is the primary signal. The supervisor never writes\n * to it, so any readable data is ignored; only close matters. An fd that cannot\n * be opened (never inherited) leaves the binding inert rather than killing a\n * healthy host.\n *\n * The pipe is read through a net.Socket rather than a file stream because the read has to live\n * on the EVENT LOOP, not in libuv's thread pool. A thread-pool read blocks in `read(2)` until the\n * supervisor dies, and `process.exit()` joins the thread pool before it terminates - so a host\n * that decides to exit on its own (an idle exit, or the park-everything drain of a generation\n * handoff) would hang in exit until something killed it. With a socket the same EOF arrives from\n * kqueue/epoll and nothing holds the exit.\n */\nfunction watchFdForEof(fd: number, fire: (reason: string) => void): () => void {\n\tlet stream: Socket;\n\tlet streamFailed = false;\n\tlet fired = false;\n\tconst fireOnce = (reason: string): void => {\n\t\tif (fired) return;\n\t\tfired = true;\n\t\tfire(reason);\n\t};\n\ttry {\n\t\t// The watchdog owns this inherited read end; destroying the socket releases it.\n\t\tstream = new Socket({ fd, readable: true, writable: false });\n\t} catch {\n\t\treturn () => {};\n\t}\n\tstream.resume();\n\tconst onEnd = (): void => fireOnce(`supervisor pipe fd ${fd} closed`);\n\tconst onClose = (): void => {\n\t\tif (!streamFailed) onEnd();\n\t};\n\tconst onError = (): void => {\n\t\tstreamFailed = true;\n\t};\n\tstream.once(\"end\", onEnd);\n\t// Win32 pipe teardown may report close without end; POSIX invalid-fd close is\n\t// not supervisor death and remains inert.\n\tif (process.platform === \"win32\") stream.once(\"close\", onClose);\n\tstream.once(\"error\", onError);\n\treturn () => {\n\t\tstream.off(\"end\", onEnd);\n\t\tstream.off(\"close\", onClose);\n\t\tstream.off(\"error\", onError);\n\t\tstream.destroy();\n\t};\n}\n\n/**\n * Fallback binding: the supervisor pid is gone, or this process was reparented\n * to init because the supervisor died. Polling covers the case where no extra\n * fd could be inherited.\n */\nfunction watchPpid(supervisorPid: number, fire: (reason: string) => void): () => void {\n\t// Tests and embedders may bind the watchdog to this process itself; that\n\t// is a valid live binding rather than evidence of supervisor loss.\n\tif (supervisorPid === process.pid) return () => {};\n\tconst timer = setInterval(() => {\n\t\tif (!processAlive(supervisorPid)) {\n\t\t\tfire(`supervisor pid ${supervisorPid} is gone (ppid=${process.ppid})`);\n\t\t\treturn;\n\t\t}\n\t\t// A dead supervisor is reaped by its own parent and this process is then\n\t\t// reparented, so the ppid comparison observes the loss without spawning\n\t\t// `ps` every tick; both checks here are free syscalls.\n\t\tif (process.ppid !== supervisorPid) {\n\t\t\tfire(`supervisor pid ${supervisorPid} is gone (ppid=${process.ppid})`);\n\t\t}\n\t}, HOST_WATCH_PPID_INTERVAL_MS);\n\ttimer.unref?.();\n\treturn () => clearInterval(timer);\n}\n\nfunction processAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (cause) {\n\t\treturn cause instanceof Error && \"code\" in cause && cause.code !== \"ESRCH\";\n\t}\n}\n\n// A failing capture must never block the cleanup or the shutdown it precedes.\nfunction captureBeforeCleanup(config: HostWatchdogConfig): Promise<void> {\n\tif (config.beforeCleanup === undefined) return Promise.resolve();\n\treturn Promise.resolve()\n\t\t.then(config.beforeCleanup)\n\t\t.catch(() => undefined);\n}\n\n/**\n * Removes the cleanup paths one at a time, in list order, on every platform: the supervisor puts\n * the registration pointer last (host-cleanup-paths.ts), and it must not vanish before the state\n * it describes (senpi#2241).\n */\nexport async function cleanupWatchdogPaths(config: HostWatchdogConfig): Promise<void> {\n\tconst paths = [...(config.cleanupPaths ?? []), ...(config.scratchDir ? [config.scratchDir] : [])];\n\tfor (const path of paths) {\n\t\ttry {\n\t\t\tif (process.platform === \"win32\")\n\t\t\t\trmSync(path, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });\n\t\t\telse await rm(path, { recursive: true, force: true });\n\t\t} catch {}\n\t}\n}\n"]}
|