@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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The evidence `host gc` needs before it may remove an endpoint: whether anything still runs behind
|
|
3
|
+
* it, and whether anything still answers on it. Each answer errs toward "alive" - a record that
|
|
4
|
+
* cannot be proven dead, a probe that neither connects nor is refused, a claim whose owner cannot be
|
|
5
|
+
* read - because the cost of keeping a dead directory is disk, and the cost of removing a live one
|
|
6
|
+
* is every session its host holds.
|
|
7
|
+
*
|
|
8
|
+
* Reading only: nothing here writes, unlinks or signals.
|
|
9
|
+
*/
|
|
10
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
11
|
+
import { createConnection } from "node:net";
|
|
12
|
+
import { basename, dirname, join } from "node:path";
|
|
13
|
+
import { ProcessIdentityUnreadableError, parseDaemonPidFile, processMatchesPidFile, } from "../app-server/daemon/process.js";
|
|
14
|
+
import { generationPaths } from "./host-daemon-paths.js";
|
|
15
|
+
import { parseJson, readFileOrUndefined } from "./host-daemon-state.js";
|
|
16
|
+
import { claimOwnerIsLive, readSessionPathClaims } from "./host-reservations.js";
|
|
17
|
+
import { readSocketSecret, resolveSocketTransportAddress, socketSecretPath } from "./socket-transport.js";
|
|
18
|
+
/** A connect that neither succeeds nor fails within this budget counts as an answer. */
|
|
19
|
+
const SOCKET_PROBE_TIMEOUT_MS = 2_000;
|
|
20
|
+
/**
|
|
21
|
+
* The three-part test, in the order its answers are reported: (a) no generation pidfile names a live
|
|
22
|
+
* process - the pointer's generation included, since its record lives under `generations/` too - (b) no
|
|
23
|
+
* session-path claim has a live owner, (c) the public socket - and every `.next-*` successor bind beside
|
|
24
|
+
* it - refuses the connection or does not exist.
|
|
25
|
+
*/
|
|
26
|
+
export async function endpointInUse(paths, socket) {
|
|
27
|
+
if (await anyGenerationLive(paths))
|
|
28
|
+
return { inUse: "live_generation" };
|
|
29
|
+
for (const claim of await readSessionPathClaims(paths.reservationsDir)) {
|
|
30
|
+
if (await claimOwnerIsLive(claim.owner))
|
|
31
|
+
return { inUse: "live_claim" };
|
|
32
|
+
}
|
|
33
|
+
const silence = await socketSilence(socket);
|
|
34
|
+
if (silence === undefined)
|
|
35
|
+
return { inUse: "reachable" };
|
|
36
|
+
// A successor mid-handoff listens on `<socket>.next-<gen>` before it registers anywhere.
|
|
37
|
+
const successors = (await socketSiblings(socket)).filter((name) => name.startsWith(`${basename(socket)}.next-`));
|
|
38
|
+
for (const successor of successors) {
|
|
39
|
+
if ((await socketSilence(join(dirname(socket), successor))) === undefined)
|
|
40
|
+
return { inUse: "reachable" };
|
|
41
|
+
}
|
|
42
|
+
return { inUse: undefined, silence };
|
|
43
|
+
}
|
|
44
|
+
/** Every entry beside `socket` that belongs to it: `<name>.next-*` successor binds and `<name>.shield-*`. */
|
|
45
|
+
export async function socketSiblings(socket) {
|
|
46
|
+
const name = basename(socket);
|
|
47
|
+
const entries = await readdir(dirname(socket)).catch(() => []);
|
|
48
|
+
return entries.filter((entry) => entry.startsWith(`${name}.next-`) || entry.startsWith(`${name}.shield-`));
|
|
49
|
+
}
|
|
50
|
+
/** EVERY generation, not only the pointer's: a predecessor draining after a handoff is still serving. */
|
|
51
|
+
async function anyGenerationLive(paths) {
|
|
52
|
+
for (const instanceId of await readdir(paths.generationsDir).catch(() => [])) {
|
|
53
|
+
const record = generationRecord(await readFileOrUndefined(generationPaths(paths, instanceId).pidFile));
|
|
54
|
+
if (record !== undefined && (await recordIsLive(record)))
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A pidfile's process, or nothing when it names none. A record whose identity guard does not parse
|
|
61
|
+
* still names a pid, and is read as unguarded rather than as absent: its process may be running.
|
|
62
|
+
*/
|
|
63
|
+
function generationRecord(text) {
|
|
64
|
+
if (text === undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
const record = parseDaemonPidFile(text);
|
|
67
|
+
if (record !== undefined)
|
|
68
|
+
return record;
|
|
69
|
+
const pid = parseJson(text)?.pid;
|
|
70
|
+
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? { pid, processStartTime: null } : undefined;
|
|
71
|
+
}
|
|
72
|
+
/** Live and still the recorded process; a live pid whose identity cannot be read counts as live. */
|
|
73
|
+
async function recordIsLive(record) {
|
|
74
|
+
try {
|
|
75
|
+
return await processMatchesPidFile(record);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof ProcessIdentityUnreadableError)
|
|
79
|
+
return true;
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* How the socket proved silent, or `undefined` when it did not: only ECONNREFUSED and ENOENT prove
|
|
85
|
+
* nobody listens. A connect that succeeds, times out or fails any other way is treated as an answer.
|
|
86
|
+
* ECONNREFUSED counts only when the entry IS a socket: Linux refuses a connect to a regular file the
|
|
87
|
+
* same way it refuses a dead socket, and a file named where the socket should be is not ours to unlink.
|
|
88
|
+
*/
|
|
89
|
+
async function socketSilence(socket) {
|
|
90
|
+
let secret;
|
|
91
|
+
if (process.platform === "win32") {
|
|
92
|
+
// The pipe name includes the secret; without one no client can reach the pipe either.
|
|
93
|
+
secret = await readSocketSecret(socketSecretPath(socket)).catch(() => undefined);
|
|
94
|
+
if (secret === undefined)
|
|
95
|
+
return "socket_absent";
|
|
96
|
+
}
|
|
97
|
+
return new Promise((resolveProbe) => {
|
|
98
|
+
const connection = createConnection(resolveSocketTransportAddress(socket, process.platform, secret));
|
|
99
|
+
const finish = (value) => {
|
|
100
|
+
clearTimeout(timeout);
|
|
101
|
+
connection.destroy();
|
|
102
|
+
resolveProbe(value);
|
|
103
|
+
};
|
|
104
|
+
const timeout = setTimeout(() => finish(undefined), SOCKET_PROBE_TIMEOUT_MS);
|
|
105
|
+
connection.once("connect", () => finish(undefined));
|
|
106
|
+
connection.once("error", (error) => {
|
|
107
|
+
if (error.code === "ECONNREFUSED")
|
|
108
|
+
void refusedBySocket(socket).then(finish);
|
|
109
|
+
else if (error.code === "ENOENT")
|
|
110
|
+
finish("socket_absent");
|
|
111
|
+
else
|
|
112
|
+
finish(undefined);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/** A refusal proves silence only from a socket entry; an entry of any other type is an answer. */
|
|
117
|
+
async function refusedBySocket(socket) {
|
|
118
|
+
// Named pipes and abstract sockets have no filesystem entry to be of the wrong type.
|
|
119
|
+
if (process.platform === "win32" || socket.startsWith("\0"))
|
|
120
|
+
return "socket_refused";
|
|
121
|
+
try {
|
|
122
|
+
return (await lstat(socket)).isSocket() ? "socket_refused" : undefined;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT" ? "socket_absent" : undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=host-gc-evidence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-gc-evidence.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-gc-evidence.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAEN,8BAA8B,EAC9B,kBAAkB,EAClB,qBAAqB,GACrB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,eAAe,EAA4B,MAAM,wBAAwB,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AACjF,OAAO,EAAE,gBAAgB,EAAE,6BAA6B,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAM1G,wFAAwF;AACxF,MAAM,uBAAuB,GAAG,KAAK,CAAC;AAEtC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,KAA0B,EAC1B,MAAc;IAEd,IAAI,MAAM,iBAAiB,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC;IACxE,KAAK,MAAM,KAAK,IAAI,MAAM,qBAAqB,CAAC,KAAK,CAAC,eAAe,CAAC,EAAE,CAAC;QACxE,IAAI,MAAM,gBAAgB,CAAC,KAAK,CAAC,KAAK,CAAC;YAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC;IACzE,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IACzD,yFAAyF;IACzF,MAAM,UAAU,GAAG,CAAC,MAAM,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IACjH,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACpC,IAAI,CAAC,MAAM,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,KAAK,SAAS;YAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAC1G,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;AACtC,CAAC;AAED,6GAA6G;AAC7G,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,MAAc;IAClD,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC9B,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAc,CAAC,CAAC;IAC3E,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC;AAC5G,CAAC;AAED,yGAAyG;AACzG,KAAK,UAAU,iBAAiB,CAAC,KAA0B;IAC1D,KAAK,MAAM,UAAU,IAAI,MAAM,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAc,CAAC,EAAE,CAAC;QAC1F,MAAM,MAAM,GAAG,gBAAgB,CAAC,MAAM,mBAAmB,CAAC,eAAe,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QACvG,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;IACvE,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,SAAS,gBAAgB,CAAC,IAAwB;IACjD,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACzC,MAAM,MAAM,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACxC,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC;IACjC,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAClH,CAAC;AAED,oGAAoG;AACpG,KAAK,UAAU,YAAY,CAAC,MAAqB;IAChD,IAAI,CAAC;QACJ,OAAO,MAAM,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC5C,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACzB,IAAI,KAAK,YAAY,8BAA8B;YAAE,OAAO,IAAI,CAAC;QACjE,MAAM,KAAK,CAAC;IACb,CAAC;AACF,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,aAAa,CAAC,MAAc;IAC1C,IAAI,MAA0B,CAAC;IAC/B,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAClC,sFAAsF;QACtF,MAAM,GAAG,MAAM,gBAAgB,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACjF,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,eAAe,CAAC;IAClD,CAAC;IACD,OAAO,IAAI,OAAO,CAAC,CAAC,YAAY,EAAE,EAAE;QACnC,MAAM,UAAU,GAAG,gBAAgB,CAAC,6BAA6B,CAAC,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;QACrG,MAAM,MAAM,GAAG,CAAC,KAAgC,EAAQ,EAAE;YACzD,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,UAAU,CAAC,OAAO,EAAE,CAAC;YACrB,YAAY,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC,CAAC;QACF,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,uBAAuB,CAAC,CAAC;QAC7E,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;QACpD,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAA4B,EAAE,EAAE;YACzD,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc;gBAAE,KAAK,eAAe,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;iBACxE,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,CAAC,eAAe,CAAC,CAAC;;gBACrD,MAAM,CAAC,SAAS,CAAC,CAAC;QACxB,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACJ,CAAC;AAED,kGAAkG;AAClG,KAAK,UAAU,eAAe,CAAC,MAAc;IAC5C,qFAAqF;IACrF,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,gBAAgB,CAAC;IACrF,IAAI,CAAC;QACJ,OAAO,CAAC,MAAM,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,SAAS,CAAC;IACxE,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACzB,OAAO,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3G,CAAC;AACF,CAAC","sourcesContent":["/**\n * The evidence `host gc` needs before it may remove an endpoint: whether anything still runs behind\n * it, and whether anything still answers on it. Each answer errs toward \"alive\" - a record that\n * cannot be proven dead, a probe that neither connects nor is refused, a claim whose owner cannot be\n * read - because the cost of keeping a dead directory is disk, and the cost of removing a live one\n * is every session its host holds.\n *\n * Reading only: nothing here writes, unlinks or signals.\n */\nimport { lstat, readdir } from \"node:fs/promises\";\nimport { createConnection } from \"node:net\";\nimport { basename, dirname, join } from \"node:path\";\nimport {\n\ttype DaemonPidFile,\n\tProcessIdentityUnreadableError,\n\tparseDaemonPidFile,\n\tprocessMatchesPidFile,\n} from \"../app-server/daemon/process.ts\";\nimport { generationPaths, type HostDaemonDirectory } from \"./host-daemon-paths.ts\";\nimport { parseJson, readFileOrUndefined } from \"./host-daemon-state.ts\";\nimport { claimOwnerIsLive, readSessionPathClaims } from \"./host-reservations.ts\";\nimport { readSocketSecret, resolveSocketTransportAddress, socketSecretPath } from \"./socket-transport.ts\";\n\nexport type EndpointInUse = \"live_generation\" | \"live_claim\" | \"reachable\";\n\nexport type SocketSilence = \"socket_refused\" | \"socket_absent\";\n\n/** A connect that neither succeeds nor fails within this budget counts as an answer. */\nconst SOCKET_PROBE_TIMEOUT_MS = 2_000;\n\n/**\n * The three-part test, in the order its answers are reported: (a) no generation pidfile names a live\n * process - the pointer's generation included, since its record lives under `generations/` too - (b) no\n * session-path claim has a live owner, (c) the public socket - and every `.next-*` successor bind beside\n * it - refuses the connection or does not exist.\n */\nexport async function endpointInUse(\n\tpaths: HostDaemonDirectory,\n\tsocket: string,\n): Promise<{ readonly inUse: EndpointInUse } | { readonly inUse: undefined; readonly silence: SocketSilence }> {\n\tif (await anyGenerationLive(paths)) return { inUse: \"live_generation\" };\n\tfor (const claim of await readSessionPathClaims(paths.reservationsDir)) {\n\t\tif (await claimOwnerIsLive(claim.owner)) return { inUse: \"live_claim\" };\n\t}\n\tconst silence = await socketSilence(socket);\n\tif (silence === undefined) return { inUse: \"reachable\" };\n\t// A successor mid-handoff listens on `<socket>.next-<gen>` before it registers anywhere.\n\tconst successors = (await socketSiblings(socket)).filter((name) => name.startsWith(`${basename(socket)}.next-`));\n\tfor (const successor of successors) {\n\t\tif ((await socketSilence(join(dirname(socket), successor))) === undefined) return { inUse: \"reachable\" };\n\t}\n\treturn { inUse: undefined, silence };\n}\n\n/** Every entry beside `socket` that belongs to it: `<name>.next-*` successor binds and `<name>.shield-*`. */\nexport async function socketSiblings(socket: string): Promise<readonly string[]> {\n\tconst name = basename(socket);\n\tconst entries = await readdir(dirname(socket)).catch(() => [] as string[]);\n\treturn entries.filter((entry) => entry.startsWith(`${name}.next-`) || entry.startsWith(`${name}.shield-`));\n}\n\n/** EVERY generation, not only the pointer's: a predecessor draining after a handoff is still serving. */\nasync function anyGenerationLive(paths: HostDaemonDirectory): Promise<boolean> {\n\tfor (const instanceId of await readdir(paths.generationsDir).catch(() => [] as string[])) {\n\t\tconst record = generationRecord(await readFileOrUndefined(generationPaths(paths, instanceId).pidFile));\n\t\tif (record !== undefined && (await recordIsLive(record))) return true;\n\t}\n\treturn false;\n}\n\n/**\n * A pidfile's process, or nothing when it names none. A record whose identity guard does not parse\n * still names a pid, and is read as unguarded rather than as absent: its process may be running.\n */\nfunction generationRecord(text: string | undefined): DaemonPidFile | undefined {\n\tif (text === undefined) return undefined;\n\tconst record = parseDaemonPidFile(text);\n\tif (record !== undefined) return record;\n\tconst pid = parseJson(text)?.pid;\n\treturn typeof pid === \"number\" && Number.isInteger(pid) && pid > 0 ? { pid, processStartTime: null } : undefined;\n}\n\n/** Live and still the recorded process; a live pid whose identity cannot be read counts as live. */\nasync function recordIsLive(record: DaemonPidFile): Promise<boolean> {\n\ttry {\n\t\treturn await processMatchesPidFile(record);\n\t} catch (error: unknown) {\n\t\tif (error instanceof ProcessIdentityUnreadableError) return true;\n\t\tthrow error;\n\t}\n}\n\n/**\n * How the socket proved silent, or `undefined` when it did not: only ECONNREFUSED and ENOENT prove\n * nobody listens. A connect that succeeds, times out or fails any other way is treated as an answer.\n * ECONNREFUSED counts only when the entry IS a socket: Linux refuses a connect to a regular file the\n * same way it refuses a dead socket, and a file named where the socket should be is not ours to unlink.\n */\nasync function socketSilence(socket: string): Promise<SocketSilence | undefined> {\n\tlet secret: Buffer | undefined;\n\tif (process.platform === \"win32\") {\n\t\t// The pipe name includes the secret; without one no client can reach the pipe either.\n\t\tsecret = await readSocketSecret(socketSecretPath(socket)).catch(() => undefined);\n\t\tif (secret === undefined) return \"socket_absent\";\n\t}\n\treturn new Promise((resolveProbe) => {\n\t\tconst connection = createConnection(resolveSocketTransportAddress(socket, process.platform, secret));\n\t\tconst finish = (value: SocketSilence | undefined): void => {\n\t\t\tclearTimeout(timeout);\n\t\t\tconnection.destroy();\n\t\t\tresolveProbe(value);\n\t\t};\n\t\tconst timeout = setTimeout(() => finish(undefined), SOCKET_PROBE_TIMEOUT_MS);\n\t\tconnection.once(\"connect\", () => finish(undefined));\n\t\tconnection.once(\"error\", (error: NodeJS.ErrnoException) => {\n\t\t\tif (error.code === \"ECONNREFUSED\") void refusedBySocket(socket).then(finish);\n\t\t\telse if (error.code === \"ENOENT\") finish(\"socket_absent\");\n\t\t\telse finish(undefined);\n\t\t});\n\t});\n}\n\n/** A refusal proves silence only from a socket entry; an entry of any other type is an answer. */\nasync function refusedBySocket(socket: string): Promise<SocketSilence | undefined> {\n\t// Named pipes and abstract sockets have no filesystem entry to be of the wrong type.\n\tif (process.platform === \"win32\" || socket.startsWith(\"\\0\")) return \"socket_refused\";\n\ttry {\n\t\treturn (await lstat(socket)).isSocket() ? \"socket_refused\" : undefined;\n\t} catch (error: unknown) {\n\t\treturn error instanceof Error && \"code\" in error && error.code === \"ENOENT\" ? \"socket_absent\" : undefined;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type EndpointInUse, type SocketSilence } from "./host-gc-evidence.ts";
|
|
2
|
+
export type HostGcKeptReason = EndpointInUse | "locked" | "legacy_layout" | "unknown_identity" | "failed";
|
|
3
|
+
/** A sibling a removal left in place because of what it is. */
|
|
4
|
+
export interface HostGcSkippedEntry {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly type: "directory";
|
|
7
|
+
}
|
|
8
|
+
export interface HostGcEntry<Reason extends string> {
|
|
9
|
+
readonly socket: string | null;
|
|
10
|
+
readonly dir: string;
|
|
11
|
+
readonly reason: Reason;
|
|
12
|
+
/** Removed entries only, and only when a sibling was left in place. */
|
|
13
|
+
readonly skipped?: readonly HostGcSkippedEntry[];
|
|
14
|
+
/** `failed` entries only: the message of what threw. */
|
|
15
|
+
readonly error?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface HostGcResult {
|
|
18
|
+
readonly removed: readonly HostGcEntry<SocketSilence>[];
|
|
19
|
+
readonly kept: readonly HostGcEntry<HostGcKeptReason>[];
|
|
20
|
+
}
|
|
21
|
+
export interface HostGcOptions {
|
|
22
|
+
readonly _test?: {
|
|
23
|
+
/** Runs inside an endpoint's ensure lock, before any evidence is read. */
|
|
24
|
+
readonly afterLockAcquired?: (socket: string) => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export declare function gcHostEndpoints(agentDir: string, options?: HostGcOptions): Promise<HostGcResult>;
|
|
28
|
+
//# sourceMappingURL=host-gc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-gc.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/host-gc.ts"],"names":[],"mappings":"AA+BA,OAAO,EAAE,KAAK,aAAa,EAAiB,KAAK,aAAa,EAAkB,MAAM,uBAAuB,CAAC;AAE9G,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,QAAQ,GAAG,eAAe,GAAG,kBAAkB,GAAG,QAAQ,CAAC;AAE1G,+DAA+D;AAC/D,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW,CAAC,MAAM,SAAS,MAAM;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACjD,wDAAwD;IACxD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,CAAC,aAAa,CAAC,EAAE,CAAC;IACxD,QAAQ,CAAC,IAAI,EAAE,SAAS,WAAW,CAAC,gBAAgB,CAAC,EAAE,CAAC;CACxD;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE;QAChB,0EAA0E;QAC1E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KAC/D,CAAC;CACF;AAKD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAqB1G"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `senpi host gc`: reclaims the endpoint directories of hosts that are PROVABLY gone, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* Endpoint state accumulates by design - `endpoint.json` outlives every generation so `status --all`
|
|
5
|
+
* can still name an endpoint whose host exited - and this is the only path that ever removes it. It
|
|
6
|
+
* never runs implicitly (not inside `ensure`, not inside `status`), it never signals a process, and it
|
|
7
|
+
* removes an endpoint only on three-part evidence evaluated INSIDE that endpoint's ensure lock, the one
|
|
8
|
+
* `ensureHost` serializes on (`hostEnsureLockTarget`), so an ensure can neither start a host into a
|
|
9
|
+
* directory being removed nor have its fresh registration removed under it. Every endpoint that fails
|
|
10
|
+
* any part of the evidence is kept, with the reason:
|
|
11
|
+
*
|
|
12
|
+
* live_generation a generation pidfile (any, the pointer's included) names a live process
|
|
13
|
+
* live_claim a session-path claim in `reservations/` has a live owner
|
|
14
|
+
* reachable the socket (or a `.next-*` successor bind) did not refuse the connection
|
|
15
|
+
* locked the ensure lock was not free within 2 s
|
|
16
|
+
* legacy_layout the agent dir predates layout 2; its flat files belong to a legacy host
|
|
17
|
+
* unknown_identity nothing in the directory names its socket, so its lock cannot be taken
|
|
18
|
+
* failed reading the evidence or removing the endpoint threw; `error` says what
|
|
19
|
+
*
|
|
20
|
+
* A removal goes siblings first, the socket next and the endpoint directory LAST: until the directory
|
|
21
|
+
* is gone the endpoint is still listed, so a removal that fails part-way is finished by a later gc
|
|
22
|
+
* instead of leaving a socket nothing names. A sibling that is a directory is not a socket's leftover
|
|
23
|
+
* and not gc's to delete; it stays, and is reported under `skipped`. One endpoint's failure is recorded
|
|
24
|
+
* and the run goes on to the next.
|
|
25
|
+
*/
|
|
26
|
+
import { lstat, rm } from "node:fs/promises";
|
|
27
|
+
import { dirname, join } from "node:path";
|
|
28
|
+
import { HOST_DAEMON_LAYOUT, hostDaemonDirectoryPaths } from "./host-daemon-paths.js";
|
|
29
|
+
import { parseJson, readFileOrUndefined } from "./host-daemon-state.js";
|
|
30
|
+
import { listHostEndpoints } from "./host-endpoints.js";
|
|
31
|
+
import { acquireHostEnsureLock } from "./host-ensure-lock.js";
|
|
32
|
+
import { endpointInUse, socketSiblings } from "./host-gc-evidence.js";
|
|
33
|
+
/** 2 s, the budget a concurrent ensure's critical section gets before gc reports `locked`. */
|
|
34
|
+
const GC_LOCK_WAIT_MS = 2_000;
|
|
35
|
+
export async function gcHostEndpoints(agentDir, options = {}) {
|
|
36
|
+
const removed = [];
|
|
37
|
+
const kept = [];
|
|
38
|
+
const legacy = await legacyFlatDirectory(agentDir);
|
|
39
|
+
if (legacy !== undefined)
|
|
40
|
+
kept.push({ socket: null, dir: legacy, reason: "legacy_layout" });
|
|
41
|
+
for (const endpoint of await listHostEndpoints(agentDir)) {
|
|
42
|
+
if (endpoint.socket === null) {
|
|
43
|
+
kept.push({ socket: null, dir: endpoint.dir, reason: "unknown_identity" });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const entry = { socket: endpoint.socket, dir: endpoint.dir };
|
|
47
|
+
try {
|
|
48
|
+
const outcome = await gcEndpoint(endpoint.socket, endpoint.dir, options);
|
|
49
|
+
if (!outcome.removed)
|
|
50
|
+
kept.push({ ...entry, reason: outcome.reason });
|
|
51
|
+
else if (outcome.skipped.length === 0)
|
|
52
|
+
removed.push({ ...entry, reason: outcome.reason });
|
|
53
|
+
else
|
|
54
|
+
removed.push({ ...entry, reason: outcome.reason, skipped: outcome.skipped });
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
kept.push({ ...entry, reason: "failed", error: error instanceof Error ? error.message : String(error) });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { removed, kept };
|
|
61
|
+
}
|
|
62
|
+
async function gcEndpoint(socket, dir, options) {
|
|
63
|
+
const release = await acquireEnsureLock(socket);
|
|
64
|
+
if (release === undefined)
|
|
65
|
+
return { removed: false, reason: "locked" };
|
|
66
|
+
try {
|
|
67
|
+
await options._test?.afterLockAcquired?.(socket);
|
|
68
|
+
const evidence = await endpointInUse(hostDaemonDirectoryPaths(dir), socket);
|
|
69
|
+
if (evidence.inUse !== undefined)
|
|
70
|
+
return { removed: false, reason: evidence.inUse };
|
|
71
|
+
const skipped = await removeEndpoint(socket, dir);
|
|
72
|
+
return { removed: true, reason: evidence.silence, skipped };
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await release();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Siblings by their actual type, then the socket, then the directory that lists the endpoint. */
|
|
79
|
+
async function removeEndpoint(socket, dir) {
|
|
80
|
+
const skipped = [];
|
|
81
|
+
for (const sibling of await socketSiblings(socket)) {
|
|
82
|
+
const path = join(dirname(socket), sibling);
|
|
83
|
+
const entry = await lstat(path).catch((error) => {
|
|
84
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
85
|
+
return undefined;
|
|
86
|
+
throw error;
|
|
87
|
+
});
|
|
88
|
+
if (entry?.isDirectory())
|
|
89
|
+
skipped.push({ path, type: "directory" });
|
|
90
|
+
else if (entry !== undefined)
|
|
91
|
+
await rm(path, { force: true });
|
|
92
|
+
}
|
|
93
|
+
await rm(socket, { force: true });
|
|
94
|
+
await rm(dir, { recursive: true, force: true });
|
|
95
|
+
return skipped;
|
|
96
|
+
}
|
|
97
|
+
/** The ensure lock of `socket`, taken exactly as `ensureHost` takes it; `undefined` when not free in time. */
|
|
98
|
+
async function acquireEnsureLock(socket) {
|
|
99
|
+
return acquireHostEnsureLock(socket, GC_LOCK_WAIT_MS).catch(() => undefined);
|
|
100
|
+
}
|
|
101
|
+
/** The flat daemon directory when it holds state but no layout-2 marker: a legacy host's, never touched. */
|
|
102
|
+
async function legacyFlatDirectory(agentDir) {
|
|
103
|
+
const flatDir = join(agentDir, "rpc-host-daemon");
|
|
104
|
+
const marker = parseJson(await readFileOrUndefined(join(flatDir, "layout.json")).catch(() => undefined));
|
|
105
|
+
if (marker?.layout === HOST_DAEMON_LAYOUT)
|
|
106
|
+
return undefined;
|
|
107
|
+
const hasState = (await readFileOrUndefined(join(flatDir, "host.pid")).catch(() => undefined)) !== undefined;
|
|
108
|
+
return hasState ? flatDir : undefined;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=host-gc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-gc.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-gc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `senpi host gc`: reclaims the endpoint directories of hosts that are PROVABLY gone, and nothing else.\n *\n * Endpoint state accumulates by design - `endpoint.json` outlives every generation so `status --all`\n * can still name an endpoint whose host exited - and this is the only path that ever removes it. It\n * never runs implicitly (not inside `ensure`, not inside `status`), it never signals a process, and it\n * removes an endpoint only on three-part evidence evaluated INSIDE that endpoint's ensure lock, the one\n * `ensureHost` serializes on (`hostEnsureLockTarget`), so an ensure can neither start a host into a\n * directory being removed nor have its fresh registration removed under it. Every endpoint that fails\n * any part of the evidence is kept, with the reason:\n *\n * live_generation a generation pidfile (any, the pointer's included) names a live process\n * live_claim a session-path claim in `reservations/` has a live owner\n * reachable the socket (or a `.next-*` successor bind) did not refuse the connection\n * locked the ensure lock was not free within 2 s\n * legacy_layout the agent dir predates layout 2; its flat files belong to a legacy host\n * unknown_identity nothing in the directory names its socket, so its lock cannot be taken\n * failed reading the evidence or removing the endpoint threw; `error` says what\n *\n * A removal goes siblings first, the socket next and the endpoint directory LAST: until the directory\n * is gone the endpoint is still listed, so a removal that fails part-way is finished by a later gc\n * instead of leaving a socket nothing names. A sibling that is a directory is not a socket's leftover\n * and not gc's to delete; it stays, and is reported under `skipped`. One endpoint's failure is recorded\n * and the run goes on to the next.\n */\nimport { lstat, rm } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { HOST_DAEMON_LAYOUT, hostDaemonDirectoryPaths } from \"./host-daemon-paths.ts\";\nimport { parseJson, readFileOrUndefined } from \"./host-daemon-state.ts\";\nimport { listHostEndpoints } from \"./host-endpoints.ts\";\nimport { acquireHostEnsureLock } from \"./host-ensure-lock.ts\";\nimport { type EndpointInUse, endpointInUse, type SocketSilence, socketSiblings } from \"./host-gc-evidence.ts\";\n\nexport type HostGcKeptReason = EndpointInUse | \"locked\" | \"legacy_layout\" | \"unknown_identity\" | \"failed\";\n\n/** A sibling a removal left in place because of what it is. */\nexport interface HostGcSkippedEntry {\n\treadonly path: string;\n\treadonly type: \"directory\";\n}\n\nexport interface HostGcEntry<Reason extends string> {\n\treadonly socket: string | null;\n\treadonly dir: string;\n\treadonly reason: Reason;\n\t/** Removed entries only, and only when a sibling was left in place. */\n\treadonly skipped?: readonly HostGcSkippedEntry[];\n\t/** `failed` entries only: the message of what threw. */\n\treadonly error?: string;\n}\n\nexport interface HostGcResult {\n\treadonly removed: readonly HostGcEntry<SocketSilence>[];\n\treadonly kept: readonly HostGcEntry<HostGcKeptReason>[];\n}\n\nexport interface HostGcOptions {\n\treadonly _test?: {\n\t\t/** Runs inside an endpoint's ensure lock, before any evidence is read. */\n\t\treadonly afterLockAcquired?: (socket: string) => Promise<void>;\n\t};\n}\n\n/** 2 s, the budget a concurrent ensure's critical section gets before gc reports `locked`. */\nconst GC_LOCK_WAIT_MS = 2_000;\n\nexport async function gcHostEndpoints(agentDir: string, options: HostGcOptions = {}): Promise<HostGcResult> {\n\tconst removed: HostGcEntry<SocketSilence>[] = [];\n\tconst kept: HostGcEntry<HostGcKeptReason>[] = [];\n\tconst legacy = await legacyFlatDirectory(agentDir);\n\tif (legacy !== undefined) kept.push({ socket: null, dir: legacy, reason: \"legacy_layout\" });\n\tfor (const endpoint of await listHostEndpoints(agentDir)) {\n\t\tif (endpoint.socket === null) {\n\t\t\tkept.push({ socket: null, dir: endpoint.dir, reason: \"unknown_identity\" });\n\t\t\tcontinue;\n\t\t}\n\t\tconst entry = { socket: endpoint.socket, dir: endpoint.dir };\n\t\ttry {\n\t\t\tconst outcome = await gcEndpoint(endpoint.socket, endpoint.dir, options);\n\t\t\tif (!outcome.removed) kept.push({ ...entry, reason: outcome.reason });\n\t\t\telse if (outcome.skipped.length === 0) removed.push({ ...entry, reason: outcome.reason });\n\t\t\telse removed.push({ ...entry, reason: outcome.reason, skipped: outcome.skipped });\n\t\t} catch (error: unknown) {\n\t\t\tkept.push({ ...entry, reason: \"failed\", error: error instanceof Error ? error.message : String(error) });\n\t\t}\n\t}\n\treturn { removed, kept };\n}\n\nasync function gcEndpoint(\n\tsocket: string,\n\tdir: string,\n\toptions: HostGcOptions,\n): Promise<\n\t| { readonly removed: true; readonly reason: SocketSilence; readonly skipped: readonly HostGcSkippedEntry[] }\n\t| { readonly removed: false; readonly reason: HostGcKeptReason }\n> {\n\tconst release = await acquireEnsureLock(socket);\n\tif (release === undefined) return { removed: false, reason: \"locked\" };\n\ttry {\n\t\tawait options._test?.afterLockAcquired?.(socket);\n\t\tconst evidence = await endpointInUse(hostDaemonDirectoryPaths(dir), socket);\n\t\tif (evidence.inUse !== undefined) return { removed: false, reason: evidence.inUse };\n\t\tconst skipped = await removeEndpoint(socket, dir);\n\t\treturn { removed: true, reason: evidence.silence, skipped };\n\t} finally {\n\t\tawait release();\n\t}\n}\n\n/** Siblings by their actual type, then the socket, then the directory that lists the endpoint. */\nasync function removeEndpoint(socket: string, dir: string): Promise<readonly HostGcSkippedEntry[]> {\n\tconst skipped: HostGcSkippedEntry[] = [];\n\tfor (const sibling of await socketSiblings(socket)) {\n\t\tconst path = join(dirname(socket), sibling);\n\t\tconst entry = await lstat(path).catch((error: unknown) => {\n\t\t\tif (error instanceof Error && \"code\" in error && error.code === \"ENOENT\") return undefined;\n\t\t\tthrow error;\n\t\t});\n\t\tif (entry?.isDirectory()) skipped.push({ path, type: \"directory\" });\n\t\telse if (entry !== undefined) await rm(path, { force: true });\n\t}\n\tawait rm(socket, { force: true });\n\tawait rm(dir, { recursive: true, force: true });\n\treturn skipped;\n}\n\n/** The ensure lock of `socket`, taken exactly as `ensureHost` takes it; `undefined` when not free in time. */\nasync function acquireEnsureLock(socket: string): Promise<(() => Promise<void>) | undefined> {\n\treturn acquireHostEnsureLock(socket, GC_LOCK_WAIT_MS).catch(() => undefined);\n}\n\n/** The flat daemon directory when it holds state but no layout-2 marker: a legacy host's, never touched. */\nasync function legacyFlatDirectory(agentDir: string): Promise<string | undefined> {\n\tconst flatDir = join(agentDir, \"rpc-host-daemon\");\n\tconst marker = parseJson(await readFileOrUndefined(join(flatDir, \"layout.json\")).catch(() => undefined));\n\tif (marker?.layout === HOST_DAEMON_LAYOUT) return undefined;\n\tconst hasState = (await readFileOrUndefined(join(flatDir, \"host.pid\")).catch(() => undefined)) !== undefined;\n\treturn hasState ? flatDir : undefined;\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type HostDaemonPaths } from "./host-daemon-paths.ts";
|
|
1
|
+
import { type HostDaemonDirectory, type HostDaemonPaths } from "./host-daemon-paths.ts";
|
|
2
2
|
/** One generation that is still running, as the daemon directory and the OS describe it. */
|
|
3
3
|
export interface HostGenerationRow {
|
|
4
4
|
readonly instanceId: string;
|
|
@@ -30,5 +30,13 @@ export interface PrunedDaemonState {
|
|
|
30
30
|
* LEFT - an ensure writing one right now must not be mistaken for a generation that ended.
|
|
31
31
|
*/
|
|
32
32
|
export declare function pruneDeadGenerations(paths: HostDaemonPaths): Promise<PrunedDaemonState>;
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Every generation of this daemon that is still running, newest ordinal last. A record naming a
|
|
35
|
+
* dead pid is omitted rather than reported as history: `host status` answers who is alive now.
|
|
36
|
+
* `includeDead` is for the reader that must not prune (`host status --all`): a dead record is then
|
|
37
|
+
* reported as a row with `alive: false` instead of being dropped, so the stale state stays visible.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readGenerationRows(paths: HostDaemonDirectory, options?: {
|
|
40
|
+
readonly includeDead?: boolean;
|
|
41
|
+
}): Promise<readonly HostGenerationRow[]>;
|
|
34
42
|
//# sourceMappingURL=host-generations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-generations.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/host-generations.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"host-generations.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/host-generations.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAmB,KAAK,mBAAmB,EAAE,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAKzG,4FAA4F;AAC5F,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,6FAA6F;IAC7F,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,mGAAmG;IACnG,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACxB;AAED,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IACjC,gEAAgE;IAChE,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,oDAAoD;IACpD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,sEAAsE;IACtE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAoB7F;AAMD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACvC,KAAK,EAAE,mBAAmB,EAC1B,OAAO,GAAE;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9C,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAsBvC"}
|
|
@@ -47,31 +47,36 @@ export async function pruneDeadGenerations(paths) {
|
|
|
47
47
|
}
|
|
48
48
|
return { generations, claims, pointer: pointerRemoved };
|
|
49
49
|
}
|
|
50
|
+
function memoryOf(metrics) {
|
|
51
|
+
return { rss_mb: metrics.rss_mb, host_rss_mb: metrics.host_rss_mb };
|
|
52
|
+
}
|
|
50
53
|
/**
|
|
51
54
|
* Every generation of this daemon that is still running, newest ordinal last. A record naming a
|
|
52
55
|
* dead pid is omitted rather than reported as history: `host status` answers who is alive now.
|
|
56
|
+
* `includeDead` is for the reader that must not prune (`host status --all`): a dead record is then
|
|
57
|
+
* reported as a row with `alive: false` instead of being dropped, so the stale state stays visible.
|
|
53
58
|
*/
|
|
54
|
-
function
|
|
55
|
-
return { rss_mb: metrics.rss_mb, host_rss_mb: metrics.host_rss_mb };
|
|
56
|
-
}
|
|
57
|
-
export async function readGenerationRows(paths) {
|
|
59
|
+
export async function readGenerationRows(paths, options = {}) {
|
|
58
60
|
const pointer = parseJson(await readFileOrUndefined(paths.pointerFile).catch(() => undefined));
|
|
59
61
|
const currentId = typeof pointer?.instance_id === "string" ? pointer.instance_id : undefined;
|
|
60
62
|
const claims = await readSessionPathClaims(paths.reservationsDir);
|
|
61
63
|
const rows = [];
|
|
62
64
|
for (const instanceId of await readdir(paths.generationsDir).catch(() => [])) {
|
|
63
65
|
const record = parseJson(await readFileOrUndefined(generationPaths(paths, instanceId).pidFile));
|
|
64
|
-
if (typeof record?.pid !== "number"
|
|
66
|
+
if (typeof record?.pid !== "number")
|
|
67
|
+
continue;
|
|
68
|
+
const alive = processIsLive(record.pid);
|
|
69
|
+
if (!alive && options.includeDead !== true)
|
|
65
70
|
continue;
|
|
66
71
|
rows.push({
|
|
67
72
|
instanceId,
|
|
68
73
|
generation: typeof record.generation === "number" ? record.generation : 0,
|
|
69
74
|
pid: record.pid,
|
|
70
75
|
engineVersion: typeof record.engineVersion === "string" ? record.engineVersion : null,
|
|
71
|
-
...memoryOf(await readHostProcessMetrics(record.pid)),
|
|
76
|
+
...(alive ? memoryOf(await readHostProcessMetrics(record.pid)) : { rss_mb: null, host_rss_mb: null }),
|
|
72
77
|
sessions: claims.filter((claim) => claim.owner.instanceId === instanceId).length,
|
|
73
78
|
current: instanceId === currentId,
|
|
74
|
-
alive
|
|
79
|
+
alive,
|
|
75
80
|
});
|
|
76
81
|
}
|
|
77
82
|
return rows.sort((left, right) => left.generation - right.generation);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-generations.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-generations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"host-generations.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-generations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Which generations of one daemon are still RUNNING, and what a dead one leaves behind.\n *\n * The daemon directory accumulates a record per generation, and nothing used to remove one: after a\n * handoff, an idle exit or a kill, `generations/<instanceId>/host.pid` stayed, the pointer could go\n * on naming a process nobody runs, and `reservations/` kept claims of hosts that had been gone for\n * days (#1893: the directory described only dead pids while three supervisors were alive). Reading\n * it therefore has to mean pruning it - a record that names a pid nobody is running is not history,\n * it is a lie about what serves this endpoint.\n *\n * Both answers come from files rather than from the socket on purpose: a generation that no longer\n * answers is exactly what an operator is looking for, and a superseded host holding gigabytes\n * cannot be asked how many sessions it has.\n */\nimport { readdir, rm } from \"node:fs/promises\";\nimport { processIsLive } from \"../app-server/daemon/process.ts\";\nimport { generationPaths, type HostDaemonDirectory, type HostDaemonPaths } from \"./host-daemon-paths.ts\";\nimport { parseJson, readFileOrUndefined } from \"./host-daemon-state.ts\";\nimport { type HostProcessMetrics, readHostProcessMetrics } from \"./host-process-metrics.ts\";\nimport { claimOwnerIsLive, readSessionPathClaims } from \"./host-reservations.ts\";\n\n/** One generation that is still running, as the daemon directory and the OS describe it. */\nexport interface HostGenerationRow {\n\treadonly instanceId: string;\n\treadonly generation: number;\n\treadonly pid: number;\n\treadonly engineVersion: string | null;\n\t/** Resident memory of that generation's process tree, `null` where the platform hides it. */\n\treadonly rss_mb: number | null;\n\t/** Resident memory of that generation's own supervisor and host processes, without their tools. */\n\treadonly host_rss_mb: number | null;\n\t/** Session files this generation still claims in `reservations/`. */\n\treadonly sessions: number;\n\t/** True for the generation the pointer names: the one serving the socket. */\n\treadonly current: boolean;\n\treadonly alive: boolean;\n}\n\n/** What one prune removed, so a caller can report it instead of guessing. */\nexport interface PrunedDaemonState {\n\t/** Instance ids whose generation directory named a dead pid. */\n\treadonly generations: readonly string[];\n\t/** Session paths whose claim named a dead owner. */\n\treadonly claims: readonly string[];\n\t/** Whether the pointer itself named one of those dead generations. */\n\treadonly pointer: boolean;\n}\n\n/**\n * Drops every record of a generation that is no longer running: its directory, the pointer while it\n * still names it, and the session-path claims its pid published. A record that cannot be parsed is\n * LEFT - an ensure writing one right now must not be mistaken for a generation that ended.\n */\nexport async function pruneDeadGenerations(paths: HostDaemonPaths): Promise<PrunedDaemonState> {\n\tconst pointer = parseJson(await readFileOrUndefined(paths.pointerFile).catch(() => undefined));\n\tconst pointedAt = typeof pointer?.instance_id === \"string\" ? pointer.instance_id : undefined;\n\tconst generations: string[] = [];\n\tfor (const instanceId of await readdir(paths.generationsDir).catch(() => [] as string[])) {\n\t\tconst record = parseJson(await readFileOrUndefined(generationPaths(paths, instanceId).pidFile));\n\t\tif (typeof record?.pid !== \"number\" || processIsLive(record.pid)) continue;\n\t\tawait rm(generationPaths(paths, instanceId).dir, { recursive: true, force: true }).catch(() => undefined);\n\t\tgenerations.push(instanceId);\n\t}\n\tconst pointerRemoved = pointedAt !== undefined && generations.includes(pointedAt);\n\t// The pointer outliving its generation is what makes a client probe a socket nobody serves.\n\tif (pointerRemoved) await rm(paths.pointerFile, { force: true }).catch(() => undefined);\n\tconst claims: string[] = [];\n\tfor (const claim of await readSessionPathClaims(paths.reservationsDir)) {\n\t\tif (await claimOwnerIsLive(claim.owner)) continue;\n\t\tawait rm(claim.file, { force: true }).catch(() => undefined);\n\t\tclaims.push(claim.owner.sessionPath);\n\t}\n\treturn { generations, claims, pointer: pointerRemoved };\n}\n\nfunction memoryOf(metrics: HostProcessMetrics): Pick<HostGenerationRow, \"rss_mb\" | \"host_rss_mb\"> {\n\treturn { rss_mb: metrics.rss_mb, host_rss_mb: metrics.host_rss_mb };\n}\n\n/**\n * Every generation of this daemon that is still running, newest ordinal last. A record naming a\n * dead pid is omitted rather than reported as history: `host status` answers who is alive now.\n * `includeDead` is for the reader that must not prune (`host status --all`): a dead record is then\n * reported as a row with `alive: false` instead of being dropped, so the stale state stays visible.\n */\nexport async function readGenerationRows(\n\tpaths: HostDaemonDirectory,\n\toptions: { readonly includeDead?: boolean } = {},\n): Promise<readonly HostGenerationRow[]> {\n\tconst pointer = parseJson(await readFileOrUndefined(paths.pointerFile).catch(() => undefined));\n\tconst currentId = typeof pointer?.instance_id === \"string\" ? pointer.instance_id : undefined;\n\tconst claims = await readSessionPathClaims(paths.reservationsDir);\n\tconst rows: HostGenerationRow[] = [];\n\tfor (const instanceId of await readdir(paths.generationsDir).catch(() => [] as string[])) {\n\t\tconst record = parseJson(await readFileOrUndefined(generationPaths(paths, instanceId).pidFile));\n\t\tif (typeof record?.pid !== \"number\") continue;\n\t\tconst alive = processIsLive(record.pid);\n\t\tif (!alive && options.includeDead !== true) continue;\n\t\trows.push({\n\t\t\tinstanceId,\n\t\t\tgeneration: typeof record.generation === \"number\" ? record.generation : 0,\n\t\t\tpid: record.pid,\n\t\t\tengineVersion: typeof record.engineVersion === \"string\" ? record.engineVersion : null,\n\t\t\t...(alive ? memoryOf(await readHostProcessMetrics(record.pid)) : { rss_mb: null, host_rss_mb: null }),\n\t\t\tsessions: claims.filter((claim) => claim.owner.instanceId === instanceId).length,\n\t\t\tcurrent: instanceId === currentId,\n\t\t\talive,\n\t\t});\n\t}\n\treturn rows.sort((left, right) => left.generation - right.generation);\n}\n"]}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { HostLifecyclePolicyInput } from "./host-lifecycle.ts";
|
|
2
|
+
/** The longest a handoff holds the ensure lock: its probe of the running host, then the successor's start. */
|
|
3
|
+
export declare const HANDOFF_LOCK_HOLD_MS: number;
|
|
2
4
|
export interface HandoffHostOptions {
|
|
3
5
|
readonly socket: string;
|
|
4
6
|
readonly agentDir?: string;
|
|
@@ -16,6 +18,8 @@ export interface HandoffHostOptions {
|
|
|
16
18
|
};
|
|
17
19
|
/** Runs after the public socket identity is captured and before the successor is spawned. */
|
|
18
20
|
readonly beforeSpawn?: () => Promise<void>;
|
|
21
|
+
/** Runs once the successor is spawned and its generation recorded, before its answer is awaited. */
|
|
22
|
+
readonly afterSpawn?: (pid: number) => Promise<void>;
|
|
19
23
|
readonly platform?: NodeJS.Platform;
|
|
20
24
|
};
|
|
21
25
|
}
|
|
@@ -52,4 +56,6 @@ export type HandoffResult = {
|
|
|
52
56
|
* whether an upgrade is warranted (`decideHostAction`); this performs the one it asked for.
|
|
53
57
|
*/
|
|
54
58
|
export declare function handoffHost(options: HandoffHostOptions): Promise<HandoffResult>;
|
|
59
|
+
/** The handoff itself, for a caller that already holds this socket's ensure lock: an ensure's upgrade. */
|
|
60
|
+
export declare function handoffHostLocked(options: HandoffHostOptions): Promise<HandoffResult>;
|
|
55
61
|
//# sourceMappingURL=host-handoff.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-handoff.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/host-handoff.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"host-handoff.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/host-handoff.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAMpE,8GAA8G;AAC9G,eAAO,MAAM,oBAAoB,QAAuD,CAAC;AAQzF,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,mFAAmF;IACnF,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IACvD,QAAQ,CAAC,MAAM,CAAC,EAAE,wBAAwB,CAAC;IAC3C,QAAQ,CAAC,KAAK,CAAC,EAAE;QAChB,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;QACrC,6FAA6F;QAC7F,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;SAAE,CAAC;QAC5F,6FAA6F;QAC7F,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3C,oGAAoG;QACpG,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QACrD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;KACpC,CAAC;CACF;AAED,yFAAyF;AACzF,MAAM,MAAM,cAAc;AACzB,8EAA8E;AAC5E,SAAS;AACX,gFAAgF;GAC9E,qBAAqB;AACvB,8DAA8D;GAC5D,qBAAqB;AACvB,6FAA6F;GAC3F,eAAe;AACjB,gFAAgF;GAC9E,iBAAiB;AACnB,2EAA2E;GACzE,sBAAsB;AACxB,kGAAkG;GAChG,uBAAuB,CAAC;AAE3B,MAAM,MAAM,aAAa,GACtB;IACA,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC3B,GACD;IACA,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEL;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAQrF;AAED,0GAA0G;AAC1G,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAgB3F"}
|
|
@@ -22,26 +22,50 @@
|
|
|
22
22
|
*
|
|
23
23
|
* win32 has neither a renameable named pipe nor SIGUSR1, so every entry point here refuses
|
|
24
24
|
* with `upgrade_unsupported`; upgrades there apply after a drain-stop or an idle exit.
|
|
25
|
+
*
|
|
26
|
+
* A handoff changes who serves the socket, so it runs inside the endpoint's ENSURE lock, like an
|
|
27
|
+
* ensure and `host gc`: an ensure arriving meanwhile attaches to whichever generation the handoff
|
|
28
|
+
* left registered, and gc never judges the endpoint halfway through.
|
|
25
29
|
*/
|
|
26
30
|
import { createDaemonDirectories, createHostDaemonPaths } from "./host-daemon-paths.js";
|
|
27
31
|
import { provenOwner, readHostRegistration } from "./host-daemon-registration.js";
|
|
28
32
|
import { GENERATION_HANDOFF_CAPABILITY } from "./host-decision.js";
|
|
33
|
+
import { acquireHostEnsureLock } from "./host-ensure-lock.js";
|
|
29
34
|
import { probeProtocolInfo } from "./host-probe.js";
|
|
30
|
-
import { startSuccessor } from "./host-successor.js";
|
|
35
|
+
import { SUCCESSOR_START_BUDGET_MS, startSuccessor } from "./host-successor.js";
|
|
36
|
+
const HANDOFF_PROBE_TIMEOUT_MS = 10_000;
|
|
37
|
+
/** The longest a handoff holds the ensure lock: its probe of the running host, then the successor's start. */
|
|
38
|
+
export const HANDOFF_LOCK_HOLD_MS = HANDOFF_PROBE_TIMEOUT_MS + SUCCESSOR_START_BUDGET_MS;
|
|
39
|
+
/**
|
|
40
|
+
* How long a handoff waits for the lock. The longest holder is an ensure that probes the running host
|
|
41
|
+
* and then hands it off itself (its upgrade path); 10 s of headroom for a slow runner.
|
|
42
|
+
*/
|
|
43
|
+
const HANDOFF_LOCK_WAIT_MS = HANDOFF_PROBE_TIMEOUT_MS + HANDOFF_LOCK_HOLD_MS + 10_000;
|
|
31
44
|
/**
|
|
32
45
|
* Hands the socket to a new generation of this build. Forced by design: the caller decides
|
|
33
46
|
* whether an upgrade is warranted (`decideHostAction`); this performs the one it asked for.
|
|
34
47
|
*/
|
|
35
48
|
export async function handoffHost(options) {
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
if (handoffUnsupported(options))
|
|
50
|
+
return { action: "refuse", reason: "upgrade_unsupported", upgradeable: false };
|
|
51
|
+
const release = await acquireHostEnsureLock(options.socket, HANDOFF_LOCK_WAIT_MS);
|
|
52
|
+
try {
|
|
53
|
+
return await handoffHostLocked(options);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
await release();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** The handoff itself, for a caller that already holds this socket's ensure lock: an ensure's upgrade. */
|
|
60
|
+
export async function handoffHostLocked(options) {
|
|
61
|
+
if (handoffUnsupported(options))
|
|
38
62
|
return { action: "refuse", reason: "upgrade_unsupported", upgradeable: false };
|
|
39
63
|
const paths = createHostDaemonPaths({
|
|
40
64
|
socket: options.socket,
|
|
41
65
|
...(options.agentDir ? { agentDir: options.agentDir } : {}),
|
|
42
66
|
});
|
|
43
67
|
await createDaemonDirectories(paths);
|
|
44
|
-
const host = await probeProtocolInfo(options.socket,
|
|
68
|
+
const host = await probeProtocolInfo(options.socket, HANDOFF_PROBE_TIMEOUT_MS);
|
|
45
69
|
if (!host)
|
|
46
70
|
return { action: "refuse", reason: "no_host", upgradeable: false };
|
|
47
71
|
if (!host.capabilities.includes(GENERATION_HANDOFF_CAPABILITY)) {
|
|
@@ -53,4 +77,7 @@ export async function handoffHost(options) {
|
|
|
53
77
|
return { action: "refuse", reason: "unknown_owner", upgradeable: true };
|
|
54
78
|
return startSuccessor({ options, paths, host, owner });
|
|
55
79
|
}
|
|
80
|
+
function handoffUnsupported(options) {
|
|
81
|
+
return (options._test?.platform ?? process.platform) === "win32";
|
|
82
|
+
}
|
|
56
83
|
//# sourceMappingURL=host-handoff.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-handoff.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-handoff.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"host-handoff.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-handoff.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Replacing a running daemon without ending the work it is doing.\n *\n * An upgrade cannot mean \"kill the host and start a newer one\": one machine-wide daemon\n * holds every client's sessions, so that is a data-loss operation dressed as a version\n * bump. A GENERATION HANDOFF replaces the process while the work continues:\n *\n * 1. the successor binds `<public>.next-<gen>` - never the live public path, which it\n * has no right to unlink - and only renames its own entry over the public path once\n * its host answers, and only while that path still holds the exact socket this\n * handoff was decided against (`--replace <dev>:<ino>`);\n * 2. the predecessor is then asked to DRAIN with SIGUSR1: it stops accepting, keeps\n * every connection it is already proxying, parks each retained session as its turn\n * settles, and exits through its ordinary idle path;\n * 3. clients that arrive in between reach whichever generation owns the path at that\n * instant - both are alive and serving.\n *\n * Two guards make this safe rather than merely clever. SIGUSR1 has a DEFAULT DISPOSITION OF\n * TERMINATE, so a host that does not advertise `generation_handoff` is never signalled - it\n * would die, taking its sessions with it. And a host whose pidfile identity cannot be proven\n * is never signalled either (I1), because \"the process at this pid\" is not evidence.\n *\n * win32 has neither a renameable named pipe nor SIGUSR1, so every entry point here refuses\n * with `upgrade_unsupported`; upgrades there apply after a drain-stop or an idle exit.\n *\n * A handoff changes who serves the socket, so it runs inside the endpoint's ENSURE lock, like an\n * ensure and `host gc`: an ensure arriving meanwhile attaches to whichever generation the handoff\n * left registered, and gc never judges the endpoint halfway through.\n */\nimport { createDaemonDirectories, createHostDaemonPaths } from \"./host-daemon-paths.ts\";\nimport { provenOwner, readHostRegistration } from \"./host-daemon-registration.ts\";\nimport { GENERATION_HANDOFF_CAPABILITY } from \"./host-decision.ts\";\nimport { acquireHostEnsureLock } from \"./host-ensure-lock.ts\";\nimport type { HostLifecyclePolicyInput } from \"./host-lifecycle.ts\";\nimport { probeProtocolInfo } from \"./host-probe.ts\";\nimport { SUCCESSOR_START_BUDGET_MS, startSuccessor } from \"./host-successor.ts\";\n\nconst HANDOFF_PROBE_TIMEOUT_MS = 10_000;\n\n/** The longest a handoff holds the ensure lock: its probe of the running host, then the successor's start. */\nexport const HANDOFF_LOCK_HOLD_MS = HANDOFF_PROBE_TIMEOUT_MS + SUCCESSOR_START_BUDGET_MS;\n\n/**\n * How long a handoff waits for the lock. The longest holder is an ensure that probes the running host\n * and then hands it off itself (its upgrade path); 10 s of headroom for a slow runner.\n */\nconst HANDOFF_LOCK_WAIT_MS = HANDOFF_PROBE_TIMEOUT_MS + HANDOFF_LOCK_HOLD_MS + 10_000;\n\nexport interface HandoffHostOptions {\n\treadonly socket: string;\n\treadonly agentDir?: string;\n\t/** Extra CLI arguments the successor's host child is launched with (provider pinning, extensions). */\n\treadonly hostArgs?: readonly string[];\n\t/** Environment for the successor; a `null` value removes an inherited variable. */\n\treadonly env?: Readonly<Record<string, string | null>>;\n\treadonly policy?: HostLifecyclePolicyInput;\n\treadonly _test?: {\n\t\treadonly readinessTimeoutMs?: number;\n\t\t/** Builds the spawnable command from supervisor argv; tests point it at the source entry. */\n\t\treadonly launch?: (args: readonly string[]) => { command: string; args: readonly string[] };\n\t\t/** Runs after the public socket identity is captured and before the successor is spawned. */\n\t\treadonly beforeSpawn?: () => Promise<void>;\n\t\t/** Runs once the successor is spawned and its generation recorded, before its answer is awaited. */\n\t\treadonly afterSpawn?: (pid: number) => Promise<void>;\n\t\treadonly platform?: NodeJS.Platform;\n\t};\n}\n\n/** Why a handoff did not happen. Every one of them leaves the running host untouched. */\nexport type HandoffRefusal =\n\t/** Nothing is serving the socket: there is no generation to hand off from. */\n\t| \"no_host\"\n\t/** The running host predates the drain handler; signalling it would kill it. */\n\t| \"handoff_unsupported\"\n\t/** win32: a named pipe can be neither renamed nor drained. */\n\t| \"upgrade_unsupported\"\n\t/** The pidfile cannot prove which process serves this socket, so it may not be signalled. */\n\t| \"unknown_owner\"\n\t/** The public socket stopped being the one this handoff was decided against. */\n\t| \"socket_replaced\"\n\t/** `<public>.next-<gen>` would exceed the platform's socket path limit. */\n\t| \"socket_path_too_long\"\n\t/** The successor never answered on the public socket; it was stopped and nothing was replaced. */\n\t| \"successor_unavailable\";\n\nexport type HandoffResult =\n\t| {\n\t\t\treadonly action: \"handoff\";\n\t\t\treadonly pid: number;\n\t\t\treadonly socket: string;\n\t\t\treadonly generation: number;\n\t\t\treadonly instanceId: string;\n\t }\n\t| {\n\t\t\treadonly action: \"refuse\";\n\t\t\treadonly reason: HandoffRefusal;\n\t\t\treadonly upgradeable: boolean;\n\t\t\treadonly detail?: string;\n\t };\n\n/**\n * Hands the socket to a new generation of this build. Forced by design: the caller decides\n * whether an upgrade is warranted (`decideHostAction`); this performs the one it asked for.\n */\nexport async function handoffHost(options: HandoffHostOptions): Promise<HandoffResult> {\n\tif (handoffUnsupported(options)) return { action: \"refuse\", reason: \"upgrade_unsupported\", upgradeable: false };\n\tconst release = await acquireHostEnsureLock(options.socket, HANDOFF_LOCK_WAIT_MS);\n\ttry {\n\t\treturn await handoffHostLocked(options);\n\t} finally {\n\t\tawait release();\n\t}\n}\n\n/** The handoff itself, for a caller that already holds this socket's ensure lock: an ensure's upgrade. */\nexport async function handoffHostLocked(options: HandoffHostOptions): Promise<HandoffResult> {\n\tif (handoffUnsupported(options)) return { action: \"refuse\", reason: \"upgrade_unsupported\", upgradeable: false };\n\tconst paths = createHostDaemonPaths({\n\t\tsocket: options.socket,\n\t\t...(options.agentDir ? { agentDir: options.agentDir } : {}),\n\t});\n\tawait createDaemonDirectories(paths);\n\tconst host = await probeProtocolInfo(options.socket, HANDOFF_PROBE_TIMEOUT_MS);\n\tif (!host) return { action: \"refuse\", reason: \"no_host\", upgradeable: false };\n\tif (!host.capabilities.includes(GENERATION_HANDOFF_CAPABILITY)) {\n\t\treturn { action: \"refuse\", reason: \"handoff_unsupported\", upgradeable: false };\n\t}\n\tconst registered = await readHostRegistration(paths);\n\tconst owner = await provenOwner(registered, options.socket);\n\tif (!owner) return { action: \"refuse\", reason: \"unknown_owner\", upgradeable: true };\n\treturn startSuccessor({ options, paths, host, owner });\n}\n\nfunction handoffUnsupported(options: HandoffHostOptions): boolean {\n\treturn (options._test?.platform ?? process.platform) === \"win32\";\n}\n"]}
|