@code-yeongyu/senpi 2026.9.24-3 → 2026.9.25
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 +26 -0
- package/dist/bundle/chunks/{app-server-command-OTRHYRIB.js → app-server-command-RB6QUPRI.js} +1 -1
- package/dist/bundle/chunks/{chunk-UM43CBL4.js → chunk-JJ3NJZBD.js} +118 -111
- package/dist/bundle/chunks/chunk-SGYEOIOJ.js +8 -0
- package/dist/bundle/chunks/chunk-YS5YEX4S.js +2 -0
- package/dist/bundle/chunks/{cli-main-TX6I4X4F.js → cli-main-KO53KYDN.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-UI5Q6FGY.js → interactive-mode-ABDNLJSI.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-D6HMI3RQ.js → multi-session-host-JV3MNWXZ.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-3UWH3ZZS.js → package-manager-cli-ZYYAVCXU.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-LXIZHJSJ.js → rpc-mode-Q2OUJURB.js} +1 -1
- package/dist/bundle/chunks/runtime-session-XBT646O5.js +2 -0
- package/dist/bundle/chunks/session-worker.js +184 -177
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/builtin/cursor-cli-oauth/stream.d.ts +7 -0
- package/dist/core/extensions/builtin/cursor-cli-oauth/stream.d.ts.map +1 -1
- package/dist/core/extensions/builtin/cursor-cli-oauth/stream.js +23 -4
- package/dist/core/extensions/builtin/cursor-cli-oauth/stream.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/durable-command.d.ts +11 -16
- package/dist/core/extensions/builtin/terminal/durable-command.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/durable-command.js +97 -43
- package/dist/core/extensions/builtin/terminal/durable-command.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/durable-file.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/durable-file.js +11 -4
- package/dist/core/extensions/builtin/terminal/durable-file.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/extension-state.d.ts +26 -0
- package/dist/core/extensions/builtin/terminal/extension-state.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/extension-state.js +151 -0
- package/dist/core/extensions/builtin/terminal/extension-state.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/extension.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/extension.js +32 -308
- package/dist/core/extensions/builtin/terminal/extension.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/lease-file.d.ts +52 -0
- package/dist/core/extensions/builtin/terminal/lease-file.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/lease-file.js +205 -0
- package/dist/core/extensions/builtin/terminal/lease-file.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/lease-keeper.d.ts +33 -0
- package/dist/core/extensions/builtin/terminal/lease-keeper.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/lease-keeper.js +110 -0
- package/dist/core/extensions/builtin/terminal/lease-keeper.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/manager.d.ts +1 -0
- package/dist/core/extensions/builtin/terminal/manager.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/manager.js +6 -0
- package/dist/core/extensions/builtin/terminal/manager.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/manifest-lease.d.ts +48 -4
- package/dist/core/extensions/builtin/terminal/manifest-lease.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/manifest-lease.js +141 -59
- package/dist/core/extensions/builtin/terminal/manifest-lease.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts +2 -0
- package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/monitor-registry.js +15 -10
- package/dist/core/extensions/builtin/terminal/monitor-registry.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/monitor-state-dir.d.ts +11 -0
- package/dist/core/extensions/builtin/terminal/monitor-state-dir.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/monitor-state-dir.js +28 -0
- package/dist/core/extensions/builtin/terminal/monitor-state-dir.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/orphan-reaper.d.ts +41 -0
- package/dist/core/extensions/builtin/terminal/orphan-reaper.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/orphan-reaper.js +131 -0
- package/dist/core/extensions/builtin/terminal/orphan-reaper.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/process-identity.d.ts +21 -0
- package/dist/core/extensions/builtin/terminal/process-identity.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/process-identity.js +34 -0
- package/dist/core/extensions/builtin/terminal/process-identity.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/process-start-probe.d.ts +6 -0
- package/dist/core/extensions/builtin/terminal/process-start-probe.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/process-start-probe.js +60 -0
- package/dist/core/extensions/builtin/terminal/process-start-probe.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/prompt.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/prompt.js +8 -4
- package/dist/core/extensions/builtin/terminal/prompt.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/restore-digest.d.ts +54 -0
- package/dist/core/extensions/builtin/terminal/restore-digest.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/restore-digest.js +133 -0
- package/dist/core/extensions/builtin/terminal/restore-digest.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/restore-session.d.ts +56 -0
- package/dist/core/extensions/builtin/terminal/restore-session.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/restore-session.js +238 -0
- package/dist/core/extensions/builtin/terminal/restore-session.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/restore.d.ts +52 -19
- package/dist/core/extensions/builtin/terminal/restore.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/restore.js +63 -138
- package/dist/core/extensions/builtin/terminal/restore.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/runtime-session.d.ts +5 -0
- package/dist/core/extensions/builtin/terminal/runtime-session.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/runtime-session.js +17 -0
- package/dist/core/extensions/builtin/terminal/runtime-session.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/session-activity.d.ts +8 -0
- package/dist/core/extensions/builtin/terminal/session-activity.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/session-activity.js +41 -0
- package/dist/core/extensions/builtin/terminal/session-activity.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/shared.d.ts +14 -0
- package/dist/core/extensions/builtin/terminal/shared.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/shared.js +14 -0
- package/dist/core/extensions/builtin/terminal/shared.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/terminal-manifest-model.d.ts +8 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest-model.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/terminal-manifest-model.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.d.ts +15 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.js +148 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest.d.ts +15 -2
- package/dist/core/extensions/builtin/terminal/terminal-manifest.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/terminal-manifest.js +62 -11
- package/dist/core/extensions/builtin/terminal/terminal-manifest.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/terminal-state-gc.d.ts +17 -0
- package/dist/core/extensions/builtin/terminal/terminal-state-gc.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/terminal-state-gc.js +81 -0
- package/dist/core/extensions/builtin/terminal/terminal-state-gc.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/tools/context.d.ts +5 -0
- package/dist/core/extensions/builtin/terminal/tools/context.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/tools/context.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/tools/kill-bash.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/tools/kill-bash.js +16 -1
- package/dist/core/extensions/builtin/terminal/tools/kill-bash.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/tools/monitor-manifest-binding.d.ts +27 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-manifest-binding.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-manifest-binding.js +75 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-manifest-binding.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-schema.d.ts +35 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-schema.d.ts.map +1 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-schema.js +67 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-schema.js.map +1 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts +14 -35
- package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/tools/monitor.js +30 -110
- package/dist/core/extensions/builtin/terminal/tools/monitor.js.map +1 -1
- package/dist/core/extensions/builtin/todotools/first-turn.d.ts +7 -6
- package/dist/core/extensions/builtin/todotools/first-turn.d.ts.map +1 -1
- package/dist/core/extensions/builtin/todotools/first-turn.js +8 -8
- package/dist/core/extensions/builtin/todotools/first-turn.js.map +1 -1
- package/dist/core/extensions/builtin/todotools/index.js +1 -1
- package/dist/core/extensions/builtin/todotools/index.js.map +1 -1
- package/dist/core/extensions/builtin/todotools/todo-format.js +1 -1
- package/dist/core/extensions/builtin/todotools/todo-format.js.map +1 -1
- package/dist/core/extensions/builtin/ttsr/detectors/repetitive-turns.d.ts.map +1 -1
- package/dist/core/extensions/builtin/ttsr/detectors/repetitive-turns.js +4 -0
- package/dist/core/extensions/builtin/ttsr/detectors/repetitive-turns.js.map +1 -1
- package/dist/core/extensions/builtin/ttsr/prompts.js +1 -1
- package/dist/core/extensions/builtin/ttsr/prompts.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +1 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +6 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/docs/extensions.md +2 -0
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +12 -0
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@code-yeongyu/senpi-desktop-engine/README.md +31 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.d.ts +38 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.js +117 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.d.ts +3 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.js +3 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.d.ts +42 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.js +79 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/native/check-prebuild-fresh.mjs +160 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/native/index.d.ts +20 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/native/index.js +23 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/native/prebuilds/darwin-arm64/senpi-desktop-engine +0 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/package.json +53 -0
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/README.md +20 -0
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.d.ts +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.js +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/package.json +44 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/README.md +20 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.d.ts +38 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.js +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.d.ts +28 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.js +102 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.d.ts +14 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.js +71 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.d.ts +7 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.js +4 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.d.ts +66 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.js +8 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.d.ts +44 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.js +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.d.ts +59 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.js +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/package.json +45 -0
- package/node_modules/@code-yeongyu/senpi-desktop-service/README.md +27 -0
- package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.d.ts +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.js +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-service/package.json +44 -0
- package/node_modules/@code-yeongyu/senpi-desktop-tool/README.md +20 -0
- package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.d.ts +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.d.ts.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.js +2 -0
- package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.js.map +1 -0
- package/node_modules/@code-yeongyu/senpi-desktop-tool/package.json +44 -0
- 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/cerebras.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/providers/data/vercel-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/dist/pipe-fallback.d.ts +2 -0
- package/node_modules/@earendil-works/pi-pty/dist/pipe-fallback.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/pipe-fallback.js +6 -0
- package/node_modules/@earendil-works/pi-pty/dist/pipe-fallback.js.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/session-bun.d.ts +1 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-bun.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/session-bun.js +1 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-bun.js.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/session-native.js +5 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-native.js.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/session-raw-tail.d.ts +11 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-raw-tail.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-raw-tail.js +24 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-raw-tail.js.map +1 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-types.d.ts +2 -0
- package/node_modules/@earendil-works/pi-pty/dist/session-types.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/session-types.js.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/session.d.ts +4 -5
- package/node_modules/@earendil-works/pi-pty/dist/session.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-pty/dist/session.js +11 -16
- package/node_modules/@earendil-works/pi-pty/dist/session.js.map +1 -1
- package/node_modules/@earendil-works/pi-pty/native/prebuilds/darwin-arm64/senpi_pty.darwin-arm64.node +0 -0
- 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/package.json +1 -1
- package/package.json +22 -7
- package/dist/bundle/chunks/chunk-GFUZPFHN.js +0 -2
- package/dist/bundle/chunks/chunk-JBOV6VNK.js +0 -8
- package/dist/bundle/chunks/runtime-session-F3EXC44D.js +0 -2
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A session's persistence lifecycle across process restarts: take the per-session lease, restore
|
|
3
|
+
* the persisted monitors on a detached promise (session_start never waits on a grace window), and
|
|
4
|
+
* deliver ONE decided digest through a slot that holds it until a model is bound. A live foreign
|
|
5
|
+
* holder defers everything: the footer says who holds the monitors, and a lease keeper takes the
|
|
6
|
+
* session over the moment that process exits, running the same restore exactly once. Monitors
|
|
7
|
+
* created while waiting are queued and reach the manifest after the takeover.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { encodedSessionId } from "../../../session-sidecar-store.js";
|
|
11
|
+
import { createRestartableCommandHandler } from "./durable-command.js";
|
|
12
|
+
import { createCheckpointedFileRestoreHandler } from "./durable-file.js";
|
|
13
|
+
import { createLeaseKeeper } from "./lease-keeper.js";
|
|
14
|
+
import { acquireTerminalLease, releaseTerminalLease, retireLeaseToken, } from "./manifest-lease.js";
|
|
15
|
+
import { removeMonitorStateDir, terminalStateDir } from "./monitor-state-dir.js";
|
|
16
|
+
import { ownProcessStartedAtMs, processBootAtMs } from "./process-identity.js";
|
|
17
|
+
import { reapplyPersistedMute, restoreTerminalState } from "./restore.js";
|
|
18
|
+
import { buildRestoreDigest, createDigestSlot, deliverRestoreDigest } from "./restore-digest.js";
|
|
19
|
+
import { createTerminalManifestStore, TerminalManifestWriter } from "./terminal-manifest.js";
|
|
20
|
+
import { sweepTerminalStateDir } from "./terminal-state-gc.js";
|
|
21
|
+
import { bindTerminalManifestWriter, unbindTerminalManifestWriter } from "./tools/monitor.js";
|
|
22
|
+
export const RESTORE_STATUS_KEY = "terminal-restore";
|
|
23
|
+
const errorText = (error) => (error instanceof Error ? error.message : String(error));
|
|
24
|
+
/** Test-only timing overrides; production uses the modules' own defaults. */
|
|
25
|
+
export const restoreSessionTestHooks = {};
|
|
26
|
+
const decisions = new Map();
|
|
27
|
+
function armDecision(sessionKey) {
|
|
28
|
+
let resolve = () => { };
|
|
29
|
+
const promise = new Promise((settle) => {
|
|
30
|
+
resolve = settle;
|
|
31
|
+
});
|
|
32
|
+
decisions.set(sessionKey, { promise, resolve });
|
|
33
|
+
return resolve;
|
|
34
|
+
}
|
|
35
|
+
/** Settles once this session's current generation has decided its restore (or had nothing to decide). */
|
|
36
|
+
export function whenRestoreDecided(sessionKey) {
|
|
37
|
+
return decisions.get(sessionKey)?.promise ?? Promise.resolve();
|
|
38
|
+
}
|
|
39
|
+
export function flushRestoreDigest(pi, state) {
|
|
40
|
+
state.digestSlot.flush((message) => {
|
|
41
|
+
// terminal.notify "off" keeps the model channel silent; the user still sees the digest.
|
|
42
|
+
if (state.settings.notify !== "off")
|
|
43
|
+
return deliverRestoreDigest(pi, state.ctx, message);
|
|
44
|
+
state.ctx?.ui?.notify?.(message.content, "info");
|
|
45
|
+
return true;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function emptyDigest() {
|
|
49
|
+
return {
|
|
50
|
+
restored: 0,
|
|
51
|
+
lost: 0,
|
|
52
|
+
expired: 0,
|
|
53
|
+
muted: 0,
|
|
54
|
+
completed: 0,
|
|
55
|
+
attachedElsewhere: 0,
|
|
56
|
+
storeError: false,
|
|
57
|
+
results: [],
|
|
58
|
+
backgroundSessions: [],
|
|
59
|
+
downtimeMs: 0,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function restoreHandlers(state, toolCtx, bundle) {
|
|
63
|
+
const writer = state.manifestWriter;
|
|
64
|
+
if (writer === null)
|
|
65
|
+
throw new Error("restore handlers need the bound manifest writer");
|
|
66
|
+
const registry = bundle.monitors;
|
|
67
|
+
const adopting = (handler) => async (monitor, context) => {
|
|
68
|
+
const result = await handler(monitor, context);
|
|
69
|
+
// Only a LIVE entry is re-adopted, so the next persist keeps it and drops the rest.
|
|
70
|
+
if (result.outcome === "restored" || result.outcome === "muted") {
|
|
71
|
+
writer.adoptRestored(result.runtime === undefined ? monitor : { ...monitor, runtime: result.runtime });
|
|
72
|
+
registry.adoptFireWindow(monitor.monitorId, monitor.fireWindow);
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
};
|
|
76
|
+
let freshFileRuntimeId;
|
|
77
|
+
const checkpointedFile = createCheckpointedFileRestoreHandler({
|
|
78
|
+
registry,
|
|
79
|
+
writer,
|
|
80
|
+
bindMonitorId: (monitorId, runtimeId) => {
|
|
81
|
+
freshFileRuntimeId = runtimeId;
|
|
82
|
+
bundle.manager.bindMonitorId(monitorId, runtimeId);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
"restartable-command": adopting(createRestartableCommandHandler({
|
|
87
|
+
ctx: toolCtx,
|
|
88
|
+
registry,
|
|
89
|
+
...(restoreSessionTestHooks.graceMs !== undefined ? { graceMs: restoreSessionTestHooks.graceMs } : {}),
|
|
90
|
+
})),
|
|
91
|
+
"checkpointed-file": adopting(async (monitor, context) => {
|
|
92
|
+
freshFileRuntimeId = undefined;
|
|
93
|
+
const result = await checkpointedFile(monitor, context);
|
|
94
|
+
if (result.outcome !== "restored" || freshFileRuntimeId === undefined)
|
|
95
|
+
return result;
|
|
96
|
+
return { ...result, outcome: reapplyPersistedMute(registry, monitor, freshFileRuntimeId) };
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Non-reload session_start (and a reload that does not hold the lease): take ownership or wait for it. */
|
|
101
|
+
export async function startPersistence({ pi, state, toolCtx, sessionKey }) {
|
|
102
|
+
state.generation += 1;
|
|
103
|
+
state.digestSlot = createDigestSlot();
|
|
104
|
+
const decided = armDecision(sessionKey);
|
|
105
|
+
const ctx = state.ctx;
|
|
106
|
+
const dir = terminalStateDir(ctx);
|
|
107
|
+
const sessionManager = ctx?.sessionManager;
|
|
108
|
+
if (dir === undefined || sessionManager === undefined)
|
|
109
|
+
return decided();
|
|
110
|
+
const encoded = encodedSessionId(sessionKey);
|
|
111
|
+
const generation = state.generation;
|
|
112
|
+
const own = async (lease) => {
|
|
113
|
+
ctx?.ui?.setStatus?.(RESTORE_STATUS_KEY, undefined);
|
|
114
|
+
state.lease = { path: lease.path, pid: lease.pid, token: lease.token };
|
|
115
|
+
const writer = new TerminalManifestWriter({ session: sessionManager });
|
|
116
|
+
state.manifestWriter = writer;
|
|
117
|
+
state.recordedBackgroundIds.clear();
|
|
118
|
+
const bundle = state.bundle;
|
|
119
|
+
let restoredFromReadableManifest = false;
|
|
120
|
+
if (bundle !== null && existsSync(writer.store.filePath)) {
|
|
121
|
+
let digest;
|
|
122
|
+
try {
|
|
123
|
+
digest = await restoreTerminalState({
|
|
124
|
+
manifest: writer.store,
|
|
125
|
+
handlers: restoreHandlers(state, toolCtx, bundle),
|
|
126
|
+
sessionFile: sessionManager.getSessionFile?.(),
|
|
127
|
+
processStartedAtMs: ownProcessStartedAtMs(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
// A restore that throws (an unreadable transcript, a kill error on a confirmed orphan)
|
|
132
|
+
// still decides: the session hears it failed, and the lease stays bound for shutdown.
|
|
133
|
+
ctx?.ui?.notify?.(`Terminal restore failed: ${errorText(error)}`, "warning");
|
|
134
|
+
digest = { ...emptyDigest(), storeError: true };
|
|
135
|
+
}
|
|
136
|
+
// A watch that did not come back never runs again: its baseline dir goes with it.
|
|
137
|
+
await Promise.all(digest.results
|
|
138
|
+
.filter((result) => result.outcome === "lost" || result.outcome === "expired" || result.outcome === "completed")
|
|
139
|
+
.map((result) => removeMonitorStateDir(dir, result.monitorId)));
|
|
140
|
+
if (generation !== state.generation)
|
|
141
|
+
return;
|
|
142
|
+
restoredFromReadableManifest = !digest.storeError;
|
|
143
|
+
if (digest.storeError || digest.results.length > 0 || digest.backgroundSessions.length > 0) {
|
|
144
|
+
state.digestSlot.set(buildRestoreDigest(digest, { generation, outcome: "decided" }));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Bound AFTER the restore read the file: draining queued specs writes the manifest.
|
|
148
|
+
bindTerminalManifestWriter(sessionKey, writer);
|
|
149
|
+
// A corrupt manifest is left as it was (fail closed); a readable one is rewritten to the truth.
|
|
150
|
+
if (restoredFromReadableManifest)
|
|
151
|
+
await writer.persistRestored();
|
|
152
|
+
state.ensurePersistence = null;
|
|
153
|
+
const self = { pid: process.pid, bootAtMs: processBootAtMs(), processStartedAtMs: ownProcessStartedAtMs() };
|
|
154
|
+
// Housekeeping only: a failed sweep leaves the files for the next start to reclaim.
|
|
155
|
+
void sweepTerminalStateDir(dir, { self, keep: new Set([`${encoded}.lease`, `${encoded}.json`]) }).catch(() => undefined);
|
|
156
|
+
flushRestoreDigest(pi, state);
|
|
157
|
+
decided();
|
|
158
|
+
};
|
|
159
|
+
const wait = (holder) => {
|
|
160
|
+
ctx?.ui?.setStatus?.(RESTORE_STATUS_KEY, holder === null ? "monitors waiting for the session lease" : `monitors held by pid ${holder.pid}`);
|
|
161
|
+
state.digestSlot.set(buildRestoreDigest(emptyDigest(), {
|
|
162
|
+
generation,
|
|
163
|
+
outcome: "deferred",
|
|
164
|
+
...(holder === null ? {} : { holderPid: holder.pid }),
|
|
165
|
+
}));
|
|
166
|
+
state.keeper?.stop();
|
|
167
|
+
state.keeper = createLeaseKeeper({
|
|
168
|
+
dir,
|
|
169
|
+
encodedSessionId: encoded,
|
|
170
|
+
...(restoreSessionTestHooks.keeperIntervalMs !== undefined
|
|
171
|
+
? { intervalMs: restoreSessionTestHooks.keeperIntervalMs }
|
|
172
|
+
: {}),
|
|
173
|
+
onTakeover: (lease) => {
|
|
174
|
+
state.restoreInFlight = own(lease);
|
|
175
|
+
return state.restoreInFlight;
|
|
176
|
+
},
|
|
177
|
+
onError: (error) => {
|
|
178
|
+
const waiting = state.keeper?.state === "waiting";
|
|
179
|
+
ctx?.ui?.notify?.(`Terminal lease ${waiting ? "check failed, still waiting" : "takeover failed"}: ${errorText(error)}`, "warning");
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
state.keeper.start(holder);
|
|
183
|
+
};
|
|
184
|
+
const acquire = async () => {
|
|
185
|
+
// The lease kept changing (or the dir failed): never leave the session undecided; the keeper retries.
|
|
186
|
+
const lease = await acquireTerminalLease({ dir, encodedSessionId: encoded }).catch((error) => {
|
|
187
|
+
ctx?.ui?.notify?.(`Terminal lease not acquired yet, retrying: ${errorText(error)}`, "warning");
|
|
188
|
+
return null;
|
|
189
|
+
});
|
|
190
|
+
if (lease === null)
|
|
191
|
+
return wait(null);
|
|
192
|
+
if (lease.acquired) {
|
|
193
|
+
state.restoreInFlight = own(lease);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
wait(lease.holder);
|
|
197
|
+
};
|
|
198
|
+
if (existsSync(createTerminalManifestStore(sessionManager).filePath))
|
|
199
|
+
return acquire();
|
|
200
|
+
// Nothing to restore: the lease and the recorder wait for the first real registration.
|
|
201
|
+
decided();
|
|
202
|
+
let binding = null;
|
|
203
|
+
state.ensurePersistence = () => {
|
|
204
|
+
binding ??= acquire().then(() => state.restoreInFlight);
|
|
205
|
+
return binding;
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** session_shutdown (not reload): stop waiting, finish any restore, suspend and flush, release. */
|
|
209
|
+
export async function stopPersistence(state, sessionKey) {
|
|
210
|
+
const keeper = state.keeper;
|
|
211
|
+
keeper?.stop();
|
|
212
|
+
state.keeper = null;
|
|
213
|
+
// A tick in flight may still be acquiring (then releases) or taking over (then restores).
|
|
214
|
+
await keeper?.settled();
|
|
215
|
+
const lease = state.lease;
|
|
216
|
+
state.lease = null;
|
|
217
|
+
if (lease !== null)
|
|
218
|
+
retireLeaseToken(lease.token);
|
|
219
|
+
await state.restoreInFlight;
|
|
220
|
+
const writer = state.manifestWriter;
|
|
221
|
+
if (writer && state.bundle)
|
|
222
|
+
await writer.observeMonitorState(state.bundle.monitors.snapshot());
|
|
223
|
+
detachPersistence(state, sessionKey);
|
|
224
|
+
if (writer)
|
|
225
|
+
await writer.recordShutdown();
|
|
226
|
+
if (lease !== null)
|
|
227
|
+
await releaseTerminalLease(lease);
|
|
228
|
+
decisions.get(sessionKey)?.resolve();
|
|
229
|
+
}
|
|
230
|
+
/** Detach the manifest recorder without writing (a reload keeps live state instead). */
|
|
231
|
+
export function detachPersistence(state, sessionKey) {
|
|
232
|
+
state.manifestWriter = null;
|
|
233
|
+
state.ensurePersistence = null;
|
|
234
|
+
state.recordedBackgroundIds.clear();
|
|
235
|
+
if (sessionKey !== undefined)
|
|
236
|
+
unbindTerminalManifestWriter(sessionKey);
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=restore-session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"restore-session.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/restore-session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A session's persistence lifecycle across process restarts: take the per-session lease, restore\n * the persisted monitors on a detached promise (session_start never waits on a grace window), and\n * deliver ONE decided digest through a slot that holds it until a model is bound. A live foreign\n * holder defers everything: the footer says who holds the monitors, and a lease keeper takes the\n * session over the moment that process exits, running the same restore exactly once. Monitors\n * created while waiting are queued and reach the manifest after the takeover.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { encodedSessionId } from \"../../../session-sidecar-store.ts\";\nimport type { ExtensionAPI, ExtensionContext } from \"../../types.ts\";\nimport { createRestartableCommandHandler } from \"./durable-command.ts\";\nimport { createCheckpointedFileRestoreHandler } from \"./durable-file.ts\";\nimport { createLeaseKeeper, type LeaseKeeper } from \"./lease-keeper.ts\";\nimport {\n\ttype AcquireTerminalLeaseResult,\n\tacquireTerminalLease,\n\ttype LeaseHolder,\n\treleaseTerminalLease,\n\tretireLeaseToken,\n} from \"./manifest-lease.ts\";\nimport { removeMonitorStateDir, terminalStateDir } from \"./monitor-state-dir.ts\";\nimport { ownProcessStartedAtMs, processBootAtMs } from \"./process-identity.ts\";\nimport { type RestoreDigest, type RestoreHandler, reapplyPersistedMute, restoreTerminalState } from \"./restore.ts\";\nimport { buildRestoreDigest, createDigestSlot, type DigestSlot, deliverRestoreDigest } from \"./restore-digest.ts\";\nimport type { TerminalSessionBundle } from \"./session-bundle.ts\";\nimport { createTerminalManifestStore, TerminalManifestWriter } from \"./terminal-manifest.ts\";\nimport { sweepTerminalStateDir } from \"./terminal-state-gc.ts\";\nimport type { TerminalToolContext } from \"./tools/context.ts\";\nimport { bindTerminalManifestWriter, unbindTerminalManifestWriter } from \"./tools/monitor.ts\";\n\nexport const RESTORE_STATUS_KEY = \"terminal-restore\";\n\nconst errorText = (error: unknown): string => (error instanceof Error ? error.message : String(error));\n\n/** Test-only timing overrides; production uses the modules' own defaults. */\nexport const restoreSessionTestHooks: { keeperIntervalMs?: number; graceMs?: number } = {};\n\nexport interface PersistenceState {\n\tctx: ExtensionContext | undefined;\n\treadonly settings: { readonly notify: string };\n\tbundle: TerminalSessionBundle | null;\n\tlease: { path: string; pid: number; token: string } | null;\n\tmanifestWriter: TerminalManifestWriter | null;\n\tensurePersistence: (() => Promise<void>) | null;\n\trecordedBackgroundIds: Set<string>;\n\tkeeper: LeaseKeeper | null;\n\tdigestSlot: DigestSlot;\n\tgeneration: number;\n\trestoreInFlight: Promise<void>;\n}\n\nconst decisions = new Map<string, { promise: Promise<void>; resolve: () => void }>();\n\nfunction armDecision(sessionKey: string): () => void {\n\tlet resolve: () => void = () => {};\n\tconst promise = new Promise<void>((settle) => {\n\t\tresolve = settle;\n\t});\n\tdecisions.set(sessionKey, { promise, resolve });\n\treturn resolve;\n}\n\n/** Settles once this session's current generation has decided its restore (or had nothing to decide). */\nexport function whenRestoreDecided(sessionKey: string): Promise<void> {\n\treturn decisions.get(sessionKey)?.promise ?? Promise.resolve();\n}\n\nexport function flushRestoreDigest(pi: ExtensionAPI, state: PersistenceState): void {\n\tstate.digestSlot.flush((message) => {\n\t\t// terminal.notify \"off\" keeps the model channel silent; the user still sees the digest.\n\t\tif (state.settings.notify !== \"off\") return deliverRestoreDigest(pi, state.ctx, message);\n\t\tstate.ctx?.ui?.notify?.(message.content, \"info\");\n\t\treturn true;\n\t});\n}\n\nfunction emptyDigest(): RestoreDigest {\n\treturn {\n\t\trestored: 0,\n\t\tlost: 0,\n\t\texpired: 0,\n\t\tmuted: 0,\n\t\tcompleted: 0,\n\t\tattachedElsewhere: 0,\n\t\tstoreError: false,\n\t\tresults: [],\n\t\tbackgroundSessions: [],\n\t\tdowntimeMs: 0,\n\t};\n}\n\nfunction restoreHandlers(state: PersistenceState, toolCtx: TerminalToolContext, bundle: TerminalSessionBundle) {\n\tconst writer = state.manifestWriter;\n\tif (writer === null) throw new Error(\"restore handlers need the bound manifest writer\");\n\tconst registry = bundle.monitors;\n\tconst adopting =\n\t\t(handler: RestoreHandler): RestoreHandler =>\n\t\tasync (monitor, context) => {\n\t\t\tconst result = await handler(monitor, context);\n\t\t\t// Only a LIVE entry is re-adopted, so the next persist keeps it and drops the rest.\n\t\t\tif (result.outcome === \"restored\" || result.outcome === \"muted\") {\n\t\t\t\twriter.adoptRestored(result.runtime === undefined ? monitor : { ...monitor, runtime: result.runtime });\n\t\t\t\tregistry.adoptFireWindow(monitor.monitorId, monitor.fireWindow);\n\t\t\t}\n\t\t\treturn result;\n\t\t};\n\tlet freshFileRuntimeId: string | undefined;\n\tconst checkpointedFile = createCheckpointedFileRestoreHandler({\n\t\tregistry,\n\t\twriter,\n\t\tbindMonitorId: (monitorId, runtimeId) => {\n\t\t\tfreshFileRuntimeId = runtimeId;\n\t\t\tbundle.manager.bindMonitorId(monitorId, runtimeId);\n\t\t},\n\t});\n\treturn {\n\t\t\"restartable-command\": adopting(\n\t\t\tcreateRestartableCommandHandler({\n\t\t\t\tctx: toolCtx,\n\t\t\t\tregistry,\n\t\t\t\t...(restoreSessionTestHooks.graceMs !== undefined ? { graceMs: restoreSessionTestHooks.graceMs } : {}),\n\t\t\t}),\n\t\t),\n\t\t\"checkpointed-file\": adopting(async (monitor, context) => {\n\t\t\tfreshFileRuntimeId = undefined;\n\t\t\tconst result = await checkpointedFile(monitor, context);\n\t\t\tif (result.outcome !== \"restored\" || freshFileRuntimeId === undefined) return result;\n\t\t\treturn { ...result, outcome: reapplyPersistedMute(registry, monitor, freshFileRuntimeId) };\n\t\t}),\n\t};\n}\n\ninterface StartOptions {\n\treadonly pi: ExtensionAPI;\n\treadonly state: PersistenceState;\n\treadonly toolCtx: TerminalToolContext;\n\treadonly sessionKey: string;\n}\n\n/** Non-reload session_start (and a reload that does not hold the lease): take ownership or wait for it. */\nexport async function startPersistence({ pi, state, toolCtx, sessionKey }: StartOptions): Promise<void> {\n\tstate.generation += 1;\n\tstate.digestSlot = createDigestSlot();\n\tconst decided = armDecision(sessionKey);\n\tconst ctx = state.ctx;\n\tconst dir = terminalStateDir(ctx);\n\tconst sessionManager = ctx?.sessionManager;\n\tif (dir === undefined || sessionManager === undefined) return decided();\n\tconst encoded = encodedSessionId(sessionKey);\n\tconst generation = state.generation;\n\n\tconst own = async (lease: AcquireTerminalLeaseResult & { acquired: true }): Promise<void> => {\n\t\tctx?.ui?.setStatus?.(RESTORE_STATUS_KEY, undefined);\n\t\tstate.lease = { path: lease.path, pid: lease.pid, token: lease.token };\n\t\tconst writer = new TerminalManifestWriter({ session: sessionManager });\n\t\tstate.manifestWriter = writer;\n\t\tstate.recordedBackgroundIds.clear();\n\t\tconst bundle = state.bundle;\n\t\tlet restoredFromReadableManifest = false;\n\t\tif (bundle !== null && existsSync(writer.store.filePath)) {\n\t\t\tlet digest: RestoreDigest;\n\t\t\ttry {\n\t\t\t\tdigest = await restoreTerminalState({\n\t\t\t\t\tmanifest: writer.store,\n\t\t\t\t\thandlers: restoreHandlers(state, toolCtx, bundle),\n\t\t\t\t\tsessionFile: sessionManager.getSessionFile?.(),\n\t\t\t\t\tprocessStartedAtMs: ownProcessStartedAtMs(),\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\t// A restore that throws (an unreadable transcript, a kill error on a confirmed orphan)\n\t\t\t\t// still decides: the session hears it failed, and the lease stays bound for shutdown.\n\t\t\t\tctx?.ui?.notify?.(`Terminal restore failed: ${errorText(error)}`, \"warning\");\n\t\t\t\tdigest = { ...emptyDigest(), storeError: true };\n\t\t\t}\n\t\t\t// A watch that did not come back never runs again: its baseline dir goes with it.\n\t\t\tawait Promise.all(\n\t\t\t\tdigest.results\n\t\t\t\t\t.filter(\n\t\t\t\t\t\t(result) =>\n\t\t\t\t\t\t\tresult.outcome === \"lost\" || result.outcome === \"expired\" || result.outcome === \"completed\",\n\t\t\t\t\t)\n\t\t\t\t\t.map((result) => removeMonitorStateDir(dir, result.monitorId)),\n\t\t\t);\n\t\t\tif (generation !== state.generation) return;\n\t\t\trestoredFromReadableManifest = !digest.storeError;\n\t\t\tif (digest.storeError || digest.results.length > 0 || digest.backgroundSessions.length > 0) {\n\t\t\t\tstate.digestSlot.set(buildRestoreDigest(digest, { generation, outcome: \"decided\" }));\n\t\t\t}\n\t\t}\n\n\t\t// Bound AFTER the restore read the file: draining queued specs writes the manifest.\n\t\tbindTerminalManifestWriter(sessionKey, writer);\n\t\t// A corrupt manifest is left as it was (fail closed); a readable one is rewritten to the truth.\n\t\tif (restoredFromReadableManifest) await writer.persistRestored();\n\t\tstate.ensurePersistence = null;\n\t\tconst self = { pid: process.pid, bootAtMs: processBootAtMs(), processStartedAtMs: ownProcessStartedAtMs() };\n\t\t// Housekeeping only: a failed sweep leaves the files for the next start to reclaim.\n\t\tvoid sweepTerminalStateDir(dir, { self, keep: new Set([`${encoded}.lease`, `${encoded}.json`]) }).catch(\n\t\t\t() => undefined,\n\t\t);\n\t\tflushRestoreDigest(pi, state);\n\t\tdecided();\n\t};\n\n\tconst wait = (holder: LeaseHolder | null): void => {\n\t\tctx?.ui?.setStatus?.(\n\t\t\tRESTORE_STATUS_KEY,\n\t\t\tholder === null ? \"monitors waiting for the session lease\" : `monitors held by pid ${holder.pid}`,\n\t\t);\n\t\tstate.digestSlot.set(\n\t\t\tbuildRestoreDigest(emptyDigest(), {\n\t\t\t\tgeneration,\n\t\t\t\toutcome: \"deferred\",\n\t\t\t\t...(holder === null ? {} : { holderPid: holder.pid }),\n\t\t\t}),\n\t\t);\n\t\tstate.keeper?.stop();\n\t\tstate.keeper = createLeaseKeeper({\n\t\t\tdir,\n\t\t\tencodedSessionId: encoded,\n\t\t\t...(restoreSessionTestHooks.keeperIntervalMs !== undefined\n\t\t\t\t? { intervalMs: restoreSessionTestHooks.keeperIntervalMs }\n\t\t\t\t: {}),\n\t\t\tonTakeover: (lease) => {\n\t\t\t\tstate.restoreInFlight = own(lease);\n\t\t\t\treturn state.restoreInFlight;\n\t\t\t},\n\t\t\tonError: (error) => {\n\t\t\t\tconst waiting = state.keeper?.state === \"waiting\";\n\t\t\t\tctx?.ui?.notify?.(\n\t\t\t\t\t`Terminal lease ${waiting ? \"check failed, still waiting\" : \"takeover failed\"}: ${errorText(error)}`,\n\t\t\t\t\t\"warning\",\n\t\t\t\t);\n\t\t\t},\n\t\t});\n\t\tstate.keeper.start(holder);\n\t};\n\n\tconst acquire = async (): Promise<void> => {\n\t\t// The lease kept changing (or the dir failed): never leave the session undecided; the keeper retries.\n\t\tconst lease = await acquireTerminalLease({ dir, encodedSessionId: encoded }).catch((error: unknown) => {\n\t\t\tctx?.ui?.notify?.(`Terminal lease not acquired yet, retrying: ${errorText(error)}`, \"warning\");\n\t\t\treturn null;\n\t\t});\n\t\tif (lease === null) return wait(null);\n\t\tif (lease.acquired) {\n\t\t\tstate.restoreInFlight = own(lease);\n\t\t\treturn;\n\t\t}\n\t\twait(lease.holder);\n\t};\n\n\tif (existsSync(createTerminalManifestStore(sessionManager).filePath)) return acquire();\n\t// Nothing to restore: the lease and the recorder wait for the first real registration.\n\tdecided();\n\tlet binding: Promise<void> | null = null;\n\tstate.ensurePersistence = () => {\n\t\tbinding ??= acquire().then(() => state.restoreInFlight);\n\t\treturn binding;\n\t};\n}\n\n/** session_shutdown (not reload): stop waiting, finish any restore, suspend and flush, release. */\nexport async function stopPersistence(state: PersistenceState, sessionKey: string): Promise<void> {\n\tconst keeper = state.keeper;\n\tkeeper?.stop();\n\tstate.keeper = null;\n\t// A tick in flight may still be acquiring (then releases) or taking over (then restores).\n\tawait keeper?.settled();\n\tconst lease = state.lease;\n\tstate.lease = null;\n\tif (lease !== null) retireLeaseToken(lease.token);\n\tawait state.restoreInFlight;\n\tconst writer = state.manifestWriter;\n\tif (writer && state.bundle) await writer.observeMonitorState(state.bundle.monitors.snapshot());\n\tdetachPersistence(state, sessionKey);\n\tif (writer) await writer.recordShutdown();\n\tif (lease !== null) await releaseTerminalLease(lease);\n\tdecisions.get(sessionKey)?.resolve();\n}\n\n/** Detach the manifest recorder without writing (a reload keeps live state instead). */\nexport function detachPersistence(state: PersistenceState, sessionKey: string | undefined): void {\n\tstate.manifestWriter = null;\n\tstate.ensurePersistence = null;\n\tstate.recordedBackgroundIds.clear();\n\tif (sessionKey !== undefined) unbindTerminalManifestWriter(sessionKey);\n}\n"]}
|
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* handler slots ship as stubs reporting `lost` — later phases plug the real ones in.
|
|
2
|
+
* The restore orchestrator: `restoreTerminalState` replays the manifest after a restart. Every
|
|
3
|
+
* monitor is classified up front (expired, ephemeral with or without time left, durable) and the
|
|
4
|
+
* durable and still-live ephemeral ones are handed to their kind's handler, all concurrently.
|
|
5
|
+
* The result carries per-monitor outcomes and reasons, background sessions told apart as still
|
|
6
|
+
* running or exited, and an upper bound on how long the session was detached.
|
|
8
7
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
/** Strict fail-closed domain parse; the sidecar store has already checked version and session. */
|
|
17
|
-
export declare function parseTerminalManifest(raw: unknown, ref: SidecarStoreRef): TerminalManifest;
|
|
18
|
-
export type RestoreOutcome = "restored" | "lost" | "muted" | "attachedElsewhere";
|
|
8
|
+
import type { SidecarStore } from "../../../session-sidecar-store.ts";
|
|
9
|
+
import type { ChildProcessIdentity } from "./process-identity.ts";
|
|
10
|
+
import type { ManifestBackgroundSession, ManifestMonitor, TerminalManifest } from "./terminal-manifest.ts";
|
|
11
|
+
export { InvalidTerminalManifestError, parseTerminalManifest } from "./terminal-manifest-parse.ts";
|
|
12
|
+
export type RestoreOutcome = "restored" | "lost" | "muted" | "completed" | "attachedElsewhere";
|
|
19
13
|
export interface RestoreHandlerResult {
|
|
20
14
|
readonly outcome: RestoreOutcome;
|
|
21
|
-
/** Why a
|
|
15
|
+
/** Why a monitor could not come back (or what its restore did), shown per monitor in the digest. */
|
|
22
16
|
readonly reason?: string;
|
|
17
|
+
/** The watcher a crash left running: killed before the respawn, or left alone when unverifiable. */
|
|
18
|
+
readonly orphan?: {
|
|
19
|
+
readonly pid: number;
|
|
20
|
+
readonly action: "killed" | "unverified";
|
|
21
|
+
};
|
|
22
|
+
/** The re-spawned process, so the manifest can find it after the next crash. */
|
|
23
|
+
readonly runtime?: ChildProcessIdentity;
|
|
23
24
|
}
|
|
24
|
-
export
|
|
25
|
+
export interface RestoreContext {
|
|
26
|
+
/** Upper bound on how long the session was detached (never an exact gap). */
|
|
27
|
+
readonly downtimeMs: number;
|
|
28
|
+
/** Time left for an ephemeral watch whose deadline has not passed; undefined for durable ones. */
|
|
29
|
+
readonly remainingMs?: number;
|
|
30
|
+
}
|
|
31
|
+
export type RestoreHandler = (monitor: ManifestMonitor, context: RestoreContext) => RestoreHandlerResult | Promise<RestoreHandlerResult>;
|
|
25
32
|
export interface RestoreHandlers {
|
|
26
33
|
readonly "restartable-command": RestoreHandler;
|
|
27
34
|
readonly "checkpointed-file": RestoreHandler;
|
|
@@ -37,20 +44,46 @@ export interface PersistedMuteRegistry {
|
|
|
37
44
|
* runtime id only, so passing the persisted `mon_` id silently no-ops and the mute is lost.
|
|
38
45
|
*/
|
|
39
46
|
export declare function reapplyPersistedMute(registry: PersistedMuteRegistry, monitor: Pick<ManifestMonitor, "deliveryPaused">, runtimeId: string): RestoreOutcome;
|
|
40
|
-
/** Stub
|
|
47
|
+
/** Stub handlers: every monitor is reported lost until a caller plugs the real ones in. */
|
|
41
48
|
export declare const stubRestoreHandlers: RestoreHandlers;
|
|
49
|
+
export type MonitorRestoreOutcome = RestoreOutcome | "expired";
|
|
50
|
+
export interface MonitorRestoreResult {
|
|
51
|
+
readonly monitorId: string;
|
|
52
|
+
readonly description: string;
|
|
53
|
+
readonly kind: ManifestMonitor["runtimeKind"];
|
|
54
|
+
readonly outcome: MonitorRestoreOutcome;
|
|
55
|
+
readonly command?: string;
|
|
56
|
+
readonly path?: string;
|
|
57
|
+
readonly reason?: string;
|
|
58
|
+
readonly orphan?: RestoreHandlerResult["orphan"];
|
|
59
|
+
}
|
|
60
|
+
export interface BackgroundRestoreResult {
|
|
61
|
+
readonly id: string;
|
|
62
|
+
readonly command: string;
|
|
63
|
+
readonly outcome: "running" | "exited";
|
|
64
|
+
readonly pid?: number;
|
|
65
|
+
}
|
|
42
66
|
export interface RestoreDigest {
|
|
43
67
|
restored: number;
|
|
44
68
|
lost: number;
|
|
45
69
|
expired: number;
|
|
46
70
|
muted: number;
|
|
71
|
+
completed: number;
|
|
47
72
|
attachedElsewhere: number;
|
|
48
73
|
storeError: boolean;
|
|
74
|
+
readonly results: readonly MonitorRestoreResult[];
|
|
75
|
+
readonly backgroundSessions: readonly BackgroundRestoreResult[];
|
|
76
|
+
/** Upper bound on how long the session was detached; 0 when nothing was persisted. */
|
|
77
|
+
readonly downtimeMs: number;
|
|
49
78
|
}
|
|
50
79
|
export interface RestoreTerminalStateOptions {
|
|
51
80
|
readonly manifest: SidecarStore<TerminalManifest>;
|
|
52
81
|
readonly handlers?: Partial<RestoreHandlers>;
|
|
53
82
|
readonly now?: () => number;
|
|
83
|
+
/** The session transcript; its last entry written before this process started bounds the downtime. */
|
|
84
|
+
readonly sessionFile?: string;
|
|
85
|
+
readonly processStartedAtMs?: number;
|
|
86
|
+
readonly isBackgroundAlive?: (session: ManifestBackgroundSession) => Promise<boolean>;
|
|
54
87
|
}
|
|
55
88
|
export declare function restoreTerminalState(options: RestoreTerminalStateOptions): Promise<RestoreDigest>;
|
|
56
89
|
//# sourceMappingURL=restore.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"restore.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/restore.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"restore.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/terminal/restore.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,124 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* handler slots ship as stubs reporting `lost` — later phases plug the real ones in.
|
|
2
|
+
* The restore orchestrator: `restoreTerminalState` replays the manifest after a restart. Every
|
|
3
|
+
* monitor is classified up front (expired, ephemeral with or without time left, durable) and the
|
|
4
|
+
* durable and still-live ephemeral ones are handed to their kind's handler, all concurrently.
|
|
5
|
+
* The result carries per-monitor outcomes and reasons, background sessions told apart as still
|
|
6
|
+
* running or exited, and an upper bound on how long the session was detached.
|
|
8
7
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
export
|
|
12
|
-
constructor(message, options) {
|
|
13
|
-
super(message, options);
|
|
14
|
-
this.name = "InvalidTerminalManifestError";
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
const RUNTIME_KINDS = ["command", "file"];
|
|
18
|
-
const DURABILITY_CLASSES = ["ephemeral", "restartable-command", "checkpointed-file"];
|
|
19
|
-
const FILE_EVENTS = ["create", "modify"];
|
|
20
|
-
const isStr = (value) => typeof value === "string" && value.length > 0;
|
|
21
|
-
const isNum = (value) => typeof value === "number" && Number.isFinite(value);
|
|
22
|
-
const isBool = (value) => typeof value === "boolean";
|
|
23
|
-
const isObj = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
-
function invalid(message) {
|
|
25
|
-
throw new InvalidTerminalManifestError(`terminal manifest is invalid: ${message}`);
|
|
26
|
-
}
|
|
27
|
-
function str(raw, field) {
|
|
28
|
-
if (!isStr(raw[field]))
|
|
29
|
-
invalid(`field ${field} must be a non-empty string`);
|
|
30
|
-
return raw[field];
|
|
31
|
-
}
|
|
32
|
-
function num(raw, field) {
|
|
33
|
-
if (!isNum(raw[field]))
|
|
34
|
-
invalid(`field ${field} must be a finite number`);
|
|
35
|
-
return raw[field];
|
|
36
|
-
}
|
|
37
|
-
function digest(raw, field) {
|
|
38
|
-
if (typeof raw[field] !== "string")
|
|
39
|
-
invalid(`field ${field} must be a string`);
|
|
40
|
-
return raw[field];
|
|
41
|
-
}
|
|
42
|
-
function bool(raw, field) {
|
|
43
|
-
if (!isBool(raw[field]))
|
|
44
|
-
invalid(`field ${field} must be a boolean`);
|
|
45
|
-
return raw[field];
|
|
46
|
-
}
|
|
47
|
-
function opt(raw, field, required) {
|
|
48
|
-
return raw[field] === undefined ? undefined : required(raw, field);
|
|
49
|
-
}
|
|
50
|
-
function oneOf(values, raw, field) {
|
|
51
|
-
const value = raw[field];
|
|
52
|
-
if (typeof value !== "string" || !values.includes(value))
|
|
53
|
-
invalid(`field ${field} must be one of: ${values.join(", ")}`);
|
|
54
|
-
return value;
|
|
55
|
-
}
|
|
56
|
-
function checkpoint(raw) {
|
|
57
|
-
if (!isObj(raw))
|
|
58
|
-
invalid("field lastCheckpoint must be an object");
|
|
59
|
-
return {
|
|
60
|
-
dev: num(raw, "dev"),
|
|
61
|
-
ino: num(raw, "ino"),
|
|
62
|
-
size: num(raw, "size"),
|
|
63
|
-
mtimeMs: num(raw, "mtimeMs"),
|
|
64
|
-
// A checkpoint without a digest cannot detect a same-size, same-mtime rewrite, so the field is
|
|
65
|
-
// required — but an absent file legitimately checkpoints an empty digest, so "" is valid.
|
|
66
|
-
digest: digest(raw, "digest"),
|
|
67
|
-
present: bool(raw, "present"),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
function fireWindow(raw) {
|
|
71
|
-
if (!isObj(raw))
|
|
72
|
-
invalid("field fireWindow must be an object");
|
|
73
|
-
return { startMs: num(raw, "startMs"), count: num(raw, "count") };
|
|
74
|
-
}
|
|
75
|
-
function parseMonitor(entry) {
|
|
76
|
-
if (!isObj(entry))
|
|
77
|
-
invalid("a monitor entry must be an object");
|
|
78
|
-
return {
|
|
79
|
-
monitorId: str(entry, "monitorId"),
|
|
80
|
-
sessionId: str(entry, "sessionId"),
|
|
81
|
-
description: str(entry, "description"),
|
|
82
|
-
runtimeKind: oneOf(RUNTIME_KINDS, entry, "runtimeKind"),
|
|
83
|
-
durabilityClass: oneOf(DURABILITY_CLASSES, entry, "durabilityClass"),
|
|
84
|
-
command: opt(entry, "command", str),
|
|
85
|
-
path: opt(entry, "path", str),
|
|
86
|
-
event: opt(entry, "event", (raw, field) => oneOf(FILE_EVENTS, raw, field)),
|
|
87
|
-
filter: opt(entry, "filter", str),
|
|
88
|
-
cwd: opt(entry, "cwd", str),
|
|
89
|
-
approvedParent: opt(entry, "approvedParent", str),
|
|
90
|
-
createdAt: num(entry, "createdAt"),
|
|
91
|
-
expiresAt: entry.expiresAt === undefined || entry.expiresAt === null ? null : num(entry, "expiresAt"),
|
|
92
|
-
persistent: bool(entry, "persistent"),
|
|
93
|
-
suspended: bool(entry, "suspended"),
|
|
94
|
-
lastCheckpoint: entry.lastCheckpoint === null ? null : checkpoint(entry.lastCheckpoint),
|
|
95
|
-
deliveryPaused: bool(entry, "deliveryPaused"),
|
|
96
|
-
// Unknown keys are ignored by this field-by-field parse, so a manifest written before
|
|
97
|
-
// `wakeCount` was dropped still reads back cleanly; no version bump is needed.
|
|
98
|
-
fireWindow: fireWindow(entry.fireWindow),
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
function parseBackgroundSession(entry) {
|
|
102
|
-
if (!isObj(entry))
|
|
103
|
-
invalid("a background session entry must be an object");
|
|
104
|
-
return { id: str(entry, "id"), command: str(entry, "command"), startedAtMs: num(entry, "startedAtMs") };
|
|
105
|
-
}
|
|
106
|
-
/** Strict fail-closed domain parse; the sidecar store has already checked version and session. */
|
|
107
|
-
export function parseTerminalManifest(raw, ref) {
|
|
108
|
-
if (!isObj(raw))
|
|
109
|
-
invalid("the payload must be an object");
|
|
110
|
-
if (!Array.isArray(raw.monitors))
|
|
111
|
-
invalid("field monitors must be an array");
|
|
112
|
-
if (!Array.isArray(raw.backgroundSessions))
|
|
113
|
-
invalid("field backgroundSessions must be an array");
|
|
114
|
-
return {
|
|
115
|
-
version: TERMINAL_MANIFEST_VERSION,
|
|
116
|
-
sessionId: ref.sessionId,
|
|
117
|
-
monitors: raw.monitors.map(parseMonitor),
|
|
118
|
-
backgroundSessions: raw.backgroundSessions.map(parseBackgroundSession),
|
|
119
|
-
updatedAt: num(raw, "updatedAt"),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
8
|
+
import { confirmOwner } from "./orphan-reaper.js";
|
|
9
|
+
import { sessionActivityBeforeMs } from "./session-activity.js";
|
|
10
|
+
export { InvalidTerminalManifestError, parseTerminalManifest } from "./terminal-manifest-parse.js";
|
|
122
11
|
/**
|
|
123
12
|
* Re-apply a persisted `deliveryPaused` mute to a freshly restored monitor and report the
|
|
124
13
|
* outcome it contributes to the digest. The mute MUST be applied by the FRESH runtime id
|
|
@@ -131,47 +20,83 @@ export function reapplyPersistedMute(registry, monitor, runtimeId) {
|
|
|
131
20
|
registry.pause([runtimeId]);
|
|
132
21
|
return "muted";
|
|
133
22
|
}
|
|
134
|
-
/** Stub
|
|
23
|
+
/** Stub handlers: every monitor is reported lost until a caller plugs the real ones in. */
|
|
135
24
|
export const stubRestoreHandlers = {
|
|
136
|
-
"restartable-command": () => ({ outcome: "lost" }),
|
|
137
|
-
"checkpointed-file": () => ({ outcome: "lost" }),
|
|
25
|
+
"restartable-command": () => ({ outcome: "lost", reason: "no restore handler" }),
|
|
26
|
+
"checkpointed-file": () => ({ outcome: "lost", reason: "no restore handler" }),
|
|
138
27
|
};
|
|
139
|
-
|
|
140
|
-
|
|
28
|
+
function emptyDigest() {
|
|
29
|
+
return {
|
|
141
30
|
restored: 0,
|
|
142
31
|
lost: 0,
|
|
143
32
|
expired: 0,
|
|
144
33
|
muted: 0,
|
|
34
|
+
completed: 0,
|
|
145
35
|
attachedElsewhere: 0,
|
|
146
36
|
storeError: false,
|
|
37
|
+
results: [],
|
|
38
|
+
backgroundSessions: [],
|
|
39
|
+
downtimeMs: 0,
|
|
147
40
|
};
|
|
41
|
+
}
|
|
42
|
+
async function defaultBackgroundAlive(session) {
|
|
43
|
+
if (session.runtime === undefined)
|
|
44
|
+
return false;
|
|
45
|
+
return (await confirmOwner(session.runtime, undefined)) === "confirmed";
|
|
46
|
+
}
|
|
47
|
+
function resultFor(monitor, outcome, handled) {
|
|
48
|
+
return {
|
|
49
|
+
monitorId: monitor.monitorId,
|
|
50
|
+
description: monitor.description,
|
|
51
|
+
kind: monitor.runtimeKind,
|
|
52
|
+
outcome,
|
|
53
|
+
...(monitor.command !== undefined ? { command: monitor.command } : {}),
|
|
54
|
+
...(monitor.path !== undefined ? { path: monitor.path } : {}),
|
|
55
|
+
...(handled?.reason !== undefined ? { reason: handled.reason } : {}),
|
|
56
|
+
...(handled?.orphan !== undefined ? { orphan: handled.orphan } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export async function restoreTerminalState(options) {
|
|
148
60
|
let state;
|
|
149
61
|
try {
|
|
150
62
|
state = await options.manifest.read();
|
|
151
63
|
}
|
|
152
64
|
catch {
|
|
153
65
|
// Fail closed: a corrupt or foreign manifest restores nothing and reports the store error.
|
|
154
|
-
return { ...
|
|
66
|
+
return { ...emptyDigest(), storeError: true };
|
|
155
67
|
}
|
|
156
68
|
if (state === null)
|
|
157
|
-
return
|
|
69
|
+
return emptyDigest();
|
|
158
70
|
const now = (options.now ?? Date.now)();
|
|
71
|
+
const lastSeen = Math.max(state.updatedAt, (await sessionActivityBeforeMs(options.sessionFile, options.processStartedAtMs ?? now)) ?? 0);
|
|
72
|
+
const downtimeMs = Math.max(0, now - lastSeen);
|
|
159
73
|
const handlers = { ...stubRestoreHandlers, ...options.handlers };
|
|
160
|
-
|
|
74
|
+
const results = await Promise.all(state.monitors.map(async (monitor) => {
|
|
161
75
|
// At the deadline the entry is already expired: the handler must not see it at all.
|
|
162
|
-
if (monitor.expiresAt !== null && monitor.expiresAt <= now)
|
|
163
|
-
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
76
|
+
if (monitor.expiresAt !== null && monitor.expiresAt <= now)
|
|
77
|
+
return resultFor(monitor, "expired");
|
|
166
78
|
if (monitor.durabilityClass === "ephemeral") {
|
|
167
|
-
|
|
168
|
-
|
|
79
|
+
if (monitor.deadlineMs === undefined || monitor.deadlineMs <= now) {
|
|
80
|
+
return resultFor(monitor, "lost", { outcome: "lost", reason: "deadline passed while detached" });
|
|
81
|
+
}
|
|
82
|
+
const handler = monitor.runtimeKind === "file" ? handlers["checkpointed-file"] : handlers["restartable-command"];
|
|
83
|
+
const handled = await handler(monitor, { downtimeMs, remainingMs: monitor.deadlineMs - now });
|
|
84
|
+
return resultFor(monitor, handled.outcome, handled);
|
|
169
85
|
}
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
86
|
+
const handled = await handlers[monitor.durabilityClass](monitor, { downtimeMs });
|
|
87
|
+
return resultFor(monitor, handled.outcome, handled);
|
|
88
|
+
}));
|
|
89
|
+
const isAlive = options.isBackgroundAlive ?? defaultBackgroundAlive;
|
|
90
|
+
const backgroundSessions = await Promise.all(state.backgroundSessions.map(async (session) => {
|
|
91
|
+
if (session.runtime !== undefined && (await isAlive(session))) {
|
|
92
|
+
return { id: session.id, command: session.command, outcome: "running", pid: session.runtime.pid };
|
|
93
|
+
}
|
|
94
|
+
return { id: session.id, command: session.command, outcome: "exited" };
|
|
95
|
+
}));
|
|
96
|
+
const digest = { ...emptyDigest(), results, backgroundSessions, downtimeMs };
|
|
97
|
+
for (const result of results)
|
|
98
|
+
digest[result.outcome] += 1;
|
|
99
|
+
digest.lost += backgroundSessions.length;
|
|
175
100
|
return digest;
|
|
176
101
|
}
|
|
177
102
|
//# sourceMappingURL=restore.js.map
|