@arnilo/prism-tools 0.0.0-stage → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +418 -0
- package/LICENSE +21 -0
- package/README.md +44 -2
- package/browser/README.md +98 -0
- package/browser/skills/browser-automation/SKILL.md +14 -0
- package/browser/src/actions/cdp.ts +131 -0
- package/browser/src/actions/dialog.ts +25 -0
- package/browser/src/actions/download_release.ts +22 -0
- package/browser/src/actions/index.ts +37 -0
- package/browser/src/actions/locator.ts +70 -0
- package/browser/src/actions/navigate.ts +23 -0
- package/browser/src/actions/screenshot.ts +27 -0
- package/browser/src/actions/scroll.ts +24 -0
- package/browser/src/actions/select_page.ts +21 -0
- package/browser/src/actions/types.ts +114 -0
- package/browser/src/actions/upload.ts +44 -0
- package/browser/src/actions/wait.ts +45 -0
- package/browser/src/browser.ts +197 -0
- package/browser/src/cdp.ts +178 -0
- package/browser/src/checkpoint.ts +189 -0
- package/browser/src/config.ts +72 -0
- package/browser/src/downloads.ts +253 -0
- package/browser/src/errors.ts +31 -0
- package/browser/src/evaluate.ts +101 -0
- package/browser/src/index.ts +129 -0
- package/browser/src/limits.ts +271 -0
- package/browser/src/manager.ts +730 -0
- package/browser/src/network.ts +263 -0
- package/browser/src/observe.ts +178 -0
- package/browser/src/policy.ts +133 -0
- package/browser/src/screenshot.ts +107 -0
- package/browser/src/shared-sandbox.ts +53 -0
- package/browser/src/snapshot.ts +99 -0
- package/browser/src/targets.ts +165 -0
- package/browser/src/tools.ts +480 -0
- package/browser/src/types.ts +316 -0
- package/browser/src/uploads.ts +174 -0
- package/coding/README.md +337 -0
- package/coding/src/agent/acp-operations.ts +146 -0
- package/coding/src/agent/artifacts.ts +38 -0
- package/coding/src/agent/ask-user-decision.ts +623 -0
- package/coding/src/agent/atomic-write.ts +23 -0
- package/coding/src/agent/bounded-file.ts +21 -0
- package/coding/src/agent/checks.ts +301 -0
- package/coding/src/agent/coding-checkpoint.ts +828 -0
- package/coding/src/agent/delete.ts +237 -0
- package/coding/src/agent/diagnostics-tool.ts +163 -0
- package/coding/src/agent/diagnostics.ts +262 -0
- package/coding/src/agent/edit-diff.ts +382 -0
- package/coding/src/agent/edit.ts +322 -0
- package/coding/src/agent/effects.ts +102 -0
- package/coding/src/agent/env.ts +28 -0
- package/coding/src/agent/execution-policy.ts +89 -0
- package/coding/src/agent/file-mutation-queue.ts +72 -0
- package/coding/src/agent/forge/github.ts +603 -0
- package/coding/src/agent/forge/index.ts +16 -0
- package/coding/src/agent/forge/types.ts +185 -0
- package/coding/src/agent/git-aware-repository.ts +305 -0
- package/coding/src/agent/git-exec.ts +326 -0
- package/coding/src/agent/git-status.ts +177 -0
- package/coding/src/agent/git-tools.ts +360 -0
- package/coding/src/agent/git.ts +1074 -0
- package/coding/src/agent/glob-match.ts +146 -0
- package/coding/src/agent/glob.ts +182 -0
- package/coding/src/agent/goal-verify.ts +385 -0
- package/coding/src/agent/grep-worker.ts +74 -0
- package/coding/src/agent/grep.ts +459 -0
- package/coding/src/agent/identity.ts +48 -0
- package/coding/src/agent/index.ts +676 -0
- package/coding/src/agent/language/client.ts +429 -0
- package/coding/src/agent/language/framing.ts +190 -0
- package/coding/src/agent/language/index.ts +23 -0
- package/coding/src/agent/language/intelligence.ts +682 -0
- package/coding/src/agent/language/types.ts +187 -0
- package/coding/src/agent/lifecycle-events.ts +121 -0
- package/coding/src/agent/lifecycle.ts +166 -0
- package/coding/src/agent/limits.ts +214 -0
- package/coding/src/agent/move.ts +178 -0
- package/coding/src/agent/mutation-path.ts +52 -0
- package/coding/src/agent/output-accumulator.ts +304 -0
- package/coding/src/agent/path-utils.ts +120 -0
- package/coding/src/agent/process/index.ts +58 -0
- package/coding/src/agent/process/recovery.ts +508 -0
- package/coding/src/agent/process/sessions-host.ts +222 -0
- package/coding/src/agent/process/sessions-monitor.ts +141 -0
- package/coding/src/agent/process/sessions-recovery.ts +241 -0
- package/coding/src/agent/process/sessions-spawn.ts +613 -0
- package/coding/src/agent/process/sessions-teardown.ts +281 -0
- package/coding/src/agent/process/sessions.ts +30 -0
- package/coding/src/agent/process/types.ts +358 -0
- package/coding/src/agent/read-path-set.ts +126 -0
- package/coding/src/agent/read.ts +713 -0
- package/coding/src/agent/repository/glob.ts +147 -0
- package/coding/src/agent/repository/indexed-search.ts +480 -0
- package/coding/src/agent/repository/list.ts +131 -0
- package/coding/src/agent/repository/operations.ts +22 -0
- package/coding/src/agent/repository/path.ts +98 -0
- package/coding/src/agent/repository/search.ts +277 -0
- package/coding/src/agent/repository/types.ts +243 -0
- package/coding/src/agent/repository/walk.ts +126 -0
- package/coding/src/agent/repository.ts +12 -0
- package/coding/src/agent/shell-background.ts +264 -0
- package/coding/src/agent/shell-output.ts +75 -0
- package/coding/src/agent/shell.ts +526 -0
- package/coding/src/agent/todo.ts +399 -0
- package/coding/src/agent/truncate.ts +251 -0
- package/coding/src/agent/write.ts +170 -0
- package/coding/src/ask-user-host.ts +48 -0
- package/coding/src/config.ts +278 -0
- package/coding/src/hard-deny.ts +97 -0
- package/coding/src/index.ts +318 -0
- package/coding/src/principal-guard.ts +84 -0
- package/coding/src/sandbox.ts +92 -0
- package/coding/src/security/approval.ts +144 -0
- package/coding/src/security/command-rules.ts +57 -0
- package/coding/src/security/docker-cli.ts +222 -0
- package/coding/src/security/docker-sandbox.ts +1205 -0
- package/coding/src/security/e2b-sandbox.ts +853 -0
- package/coding/src/security/egress/dns-pin.ts +88 -0
- package/coding/src/security/egress/index.ts +32 -0
- package/coding/src/security/egress/limits.ts +57 -0
- package/coding/src/security/egress/policy.ts +137 -0
- package/coding/src/security/egress/proxy.ts +599 -0
- package/coding/src/security/egress/types.ts +30 -0
- package/coding/src/security/index.ts +199 -0
- package/coding/src/security/native-sandbox.ts +639 -0
- package/coding/src/security/path-containment.ts +55 -0
- package/coding/src/security/sandbox-coding-operations.ts +392 -0
- package/coding/src/security/sandbox-fs-operations.ts +644 -0
- package/coding/src/security/sandbox-limits.ts +147 -0
- package/coding/src/security/sandbox-tar.ts +308 -0
- package/coding/src/security/sandbox.ts +179 -0
- package/coding/src/security/semaphore.ts +55 -0
- package/coding/src/targets.ts +110 -0
- package/coding/src/workspace/diff.ts +78 -0
- package/coding/src/workspace/errors.ts +29 -0
- package/coding/src/workspace/index.ts +5 -0
- package/coding/src/workspace/merge.ts +151 -0
- package/coding/src/workspace/routing.ts +76 -0
- package/coding/src/workspace/service.ts +584 -0
- package/computer-use/README.md +105 -0
- package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
- package/computer-use/src/classify.ts +53 -0
- package/computer-use/src/command.ts +29 -0
- package/computer-use/src/config.ts +69 -0
- package/computer-use/src/create.ts +170 -0
- package/computer-use/src/devices.ts +247 -0
- package/computer-use/src/env.ts +28 -0
- package/computer-use/src/index.ts +157 -0
- package/computer-use/src/skill.ts +21 -0
- package/openapi/README.md +97 -0
- package/openapi/src/compile.ts +386 -0
- package/openapi/src/config.ts +73 -0
- package/openapi/src/errors.ts +16 -0
- package/openapi/src/execute.ts +241 -0
- package/openapi/src/index.ts +124 -0
- package/openapi/src/limits.ts +63 -0
- package/openapi/src/specs.ts +86 -0
- package/openapi/src/tools.ts +60 -0
- package/openapi/src/types.ts +61 -0
- package/package.json +171 -4
- package/research/README.md +72 -0
- package/research/skills/research-report/SKILL.md +12 -0
- package/research/skills/research-scoping/SKILL.md +10 -0
- package/research/skills/research-synthesis/SKILL.md +10 -0
- package/research/skills/source-triage/SKILL.md +10 -0
- package/research/src/check.ts +71 -0
- package/research/src/config.ts +41 -0
- package/research/src/index.ts +147 -0
- package/research/src/ledger.ts +157 -0
- package/research/src/urls.ts +21 -0
- package/web/README.md +287 -0
- package/web/skills/web-research/SKILL.md +17 -0
- package/web/src/brave.ts +66 -0
- package/web/src/cache.ts +151 -0
- package/web/src/config.ts +210 -0
- package/web/src/controls.ts +128 -0
- package/web/src/domains.ts +114 -0
- package/web/src/exa.ts +60 -0
- package/web/src/firecrawl.ts +160 -0
- package/web/src/index.ts +387 -0
- package/web/src/kagi.ts +73 -0
- package/web/src/limits.ts +55 -0
- package/web/src/native/convert.ts +84 -0
- package/web/src/native/fetch.ts +226 -0
- package/web/src/normalize.ts +131 -0
- package/web/src/obscura/cdp.ts +201 -0
- package/web/src/obscura/classify.ts +35 -0
- package/web/src/obscura/cli.ts +254 -0
- package/web/src/obscura/errors.ts +8 -0
- package/web/src/obscura/index.ts +59 -0
- package/web/src/obscura/limits.ts +92 -0
- package/web/src/obscura/mcp.ts +104 -0
- package/web/src/obscura/playwright.ts +133 -0
- package/web/src/obscura/process.ts +235 -0
- package/web/src/obscura/search-profile.ts +73 -0
- package/web/src/obscura/types.ts +48 -0
- package/web/src/obscura/web.ts +419 -0
- package/web/src/prompt-fetch.ts +88 -0
- package/web/src/render.ts +75 -0
- package/web/src/searxng.ts +78 -0
- package/web/src/slice.ts +139 -0
- package/web/src/tavily.ts +77 -0
- package/web/src/tools.ts +182 -0
- package/web/src/transport.ts +310 -0
- package/web/src/types.ts +199 -0
- package/work/README.md +77 -0
- package/work/THIRD_PARTY_NOTICES +24 -0
- package/work/docling/ocr.py +150 -0
- package/work/sandbox/Dockerfile +36 -0
- package/work/sandbox/soffice.sh +12 -0
- package/work/src/command.ts +29 -0
- package/work/src/config.ts +138 -0
- package/work/src/connectors/cli.ts +343 -0
- package/work/src/connectors/drafts.ts +647 -0
- package/work/src/connectors/errors.ts +8 -0
- package/work/src/connectors/file-bytes.ts +149 -0
- package/work/src/connectors/google-workspace-http.ts +399 -0
- package/work/src/connectors/google-workspace.ts +465 -0
- package/work/src/connectors/gws-tools.ts +378 -0
- package/work/src/connectors/http.ts +123 -0
- package/work/src/connectors/idempotency.ts +235 -0
- package/work/src/connectors/identity.ts +34 -0
- package/work/src/connectors/index.ts +93 -0
- package/work/src/connectors/limits.ts +50 -0
- package/work/src/connectors/m365-tools.ts +308 -0
- package/work/src/connectors/microsoft365-http.ts +428 -0
- package/work/src/connectors/microsoft365.ts +402 -0
- package/work/src/connectors/normalize.ts +120 -0
- package/work/src/connectors/tool-helpers.ts +398 -0
- package/work/src/connectors/tools.ts +42 -0
- package/work/src/connectors/types.ts +399 -0
- package/work/src/diagrams/canonicalize.ts +166 -0
- package/work/src/diagrams/caps.ts +52 -0
- package/work/src/diagrams/embed.ts +340 -0
- package/work/src/diagrams/errors.ts +76 -0
- package/work/src/diagrams/index.ts +81 -0
- package/work/src/diagrams/messages.ts +150 -0
- package/work/src/diagrams/origin.ts +52 -0
- package/work/src/diagrams/telemetry.ts +34 -0
- package/work/src/diagrams/xml.ts +267 -0
- package/work/src/document-extraction/index.ts +563 -0
- package/work/src/document-reader/ambient.d.ts +2 -0
- package/work/src/document-reader/errors.ts +7 -0
- package/work/src/document-reader/index.ts +181 -0
- package/work/src/document-reader/mistral-ocr.ts +318 -0
- package/work/src/document-reader/parsers.ts +171 -0
- package/work/src/documents/caps.ts +135 -0
- package/work/src/documents/diff.ts +269 -0
- package/work/src/documents/errors.ts +44 -0
- package/work/src/documents/fidelity.ts +98 -0
- package/work/src/documents/generate.ts +96 -0
- package/work/src/documents/hash.ts +8 -0
- package/work/src/documents/index.ts +14 -0
- package/work/src/documents/model-schema.ts +639 -0
- package/work/src/documents/parse.ts +252 -0
- package/work/src/documents/patch-history.ts +81 -0
- package/work/src/documents/patch.ts +476 -0
- package/work/src/documents/preview-html.ts +245 -0
- package/work/src/documents/preview.ts +227 -0
- package/work/src/documents/telemetry.ts +36 -0
- package/work/src/documents/translate/docx.ts +292 -0
- package/work/src/documents/translate/pptx.ts +304 -0
- package/work/src/documents/translate/xlsx.ts +137 -0
- package/work/src/documents/types.ts +188 -0
- package/work/src/index.ts +311 -0
- package/work/src/runtime/worker-pool-entry.ts +102 -0
- package/work/src/runtime/worker-pool.ts +267 -0
- package/work/src/sandbox/composition.ts +212 -0
- package/work/src/sandbox/image.ts +6 -0
- package/work/src/sandbox/index.ts +14 -0
- package/work/src/sheets/caps.ts +68 -0
- package/work/src/sheets/csv.ts +351 -0
- package/work/src/sheets/decimal.ts +388 -0
- package/work/src/sheets/errors.ts +52 -0
- package/work/src/sheets/index.ts +62 -0
- package/work/src/sheets/inference.ts +378 -0
- package/work/src/sheets/telemetry.ts +34 -0
- package/work/src/sheets/types.ts +162 -0
- package/work/src/sheets/xlsx.ts +388 -0
- package/work/src/skills/index.ts +8 -0
- package/work/src/skills/load.ts +43 -0
- package/work/src/tools/filesystem.ts +21 -0
- package/work/src/tools/index.ts +1 -0
- package/work/src/tools/local-filesystem.ts +33 -0
- package/work/src/tools/office.ts +311 -0
- package/work/vendor/hermes-agent/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
- package/work/vendor-lock.json +11 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared mutable registry for managed process sessions.
|
|
3
|
+
* Lifted out of the createProcessSessions closure so phase modules take it explicitly.
|
|
4
|
+
*/
|
|
5
|
+
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
8
|
+
import { DEFAULT_MAX_TERMINAL_COLUMNS, DEFAULT_MAX_TERMINAL_ROWS } from "../limits.js";
|
|
9
|
+
import type { OutputAccumulator } from "../output-accumulator.js";
|
|
10
|
+
import { type ProcessRecoveryBackend, ProcessRecoveryError, resolveProcessRecoveryLimits } from "./recovery.js";
|
|
11
|
+
import {
|
|
12
|
+
type CodingProcessEvent,
|
|
13
|
+
type CreateProcessSessionsOptions,
|
|
14
|
+
type ProcessExitResult,
|
|
15
|
+
type ProcessPtyHandle,
|
|
16
|
+
type ProcessSandboxHandle,
|
|
17
|
+
type ProcessSession,
|
|
18
|
+
ProcessSessionError,
|
|
19
|
+
type ProcessSessionState,
|
|
20
|
+
type ProcessTerminalRequest,
|
|
21
|
+
type ResolvedProcessSessionLimits,
|
|
22
|
+
resolveProcessSessionLimits,
|
|
23
|
+
} from "./types.js";
|
|
24
|
+
|
|
25
|
+
/** Default TERM for PTY sessions (validated <= maxTerminalTermBytes). */
|
|
26
|
+
export const DEFAULT_TERM = "xterm-256color";
|
|
27
|
+
|
|
28
|
+
export function ownershipKey(
|
|
29
|
+
ownership: CreateProcessSessionsOptions["ownership"],
|
|
30
|
+
identity: CreateProcessSessionsOptions["identity"],
|
|
31
|
+
): string {
|
|
32
|
+
if (ownership) {
|
|
33
|
+
return `${ownership.tenantId ?? ""}:${ownership.accountId ?? ""}:${ownership.userId ?? ""}`;
|
|
34
|
+
}
|
|
35
|
+
if (identity) {
|
|
36
|
+
return `${identity.tenantId}:${identity.accountId ?? ""}:${identity.userId ?? ""}`;
|
|
37
|
+
}
|
|
38
|
+
return "default";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function commandFingerprint(command: string, args: readonly string[]): string {
|
|
42
|
+
return createHash("sha256")
|
|
43
|
+
.update(JSON.stringify([command, ...args]))
|
|
44
|
+
.digest("hex");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isInsideRoot(root: string, target: string): boolean {
|
|
48
|
+
const from = resolve(root);
|
|
49
|
+
const to = resolve(target);
|
|
50
|
+
if (to === from) return true;
|
|
51
|
+
const rel = relative(from, to);
|
|
52
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function nowIso(): string {
|
|
56
|
+
return new Date().toISOString();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SessionRecord {
|
|
60
|
+
id: string;
|
|
61
|
+
owner: string;
|
|
62
|
+
workspace: string;
|
|
63
|
+
command: string;
|
|
64
|
+
args: readonly string[];
|
|
65
|
+
commandFingerprint: string;
|
|
66
|
+
policyDecision: string;
|
|
67
|
+
startedAt: string;
|
|
68
|
+
exitedAt?: string;
|
|
69
|
+
releaseOnCancel: boolean;
|
|
70
|
+
expiresAt: number;
|
|
71
|
+
state: ProcessSessionState;
|
|
72
|
+
exitCode: number | null;
|
|
73
|
+
child?: ChildProcessWithoutNullStreams;
|
|
74
|
+
backend?: ProcessSandboxHandle;
|
|
75
|
+
pty?: ProcessPtyHandle;
|
|
76
|
+
ptyTerminal?: { columns: number; rows: number; term: string };
|
|
77
|
+
ptyResizeAt: number[];
|
|
78
|
+
pid?: number;
|
|
79
|
+
accumulator: OutputAccumulator;
|
|
80
|
+
waiters: Array<(result: ProcessExitResult) => void>;
|
|
81
|
+
/** Readers waiting for the next publication (`ProcessSession.onOutput`). */
|
|
82
|
+
outputListeners: Set<() => void>;
|
|
83
|
+
stdinClosed: boolean;
|
|
84
|
+
handle: ProcessSession;
|
|
85
|
+
/** Durable recovery bookkeeping (plan 026 Task 5); absent when durability is not configured. */
|
|
86
|
+
backendRef?: string;
|
|
87
|
+
recoveryFencingToken: number;
|
|
88
|
+
recoveryVersion: number;
|
|
89
|
+
/** Opaque lease claim token for renewal/release; never serialized, memory only. */
|
|
90
|
+
recoveryLeaseToken?: string;
|
|
91
|
+
/** Serializes durable transition writes per record so CAS order never inverts on slow stores. */
|
|
92
|
+
recoveryWriteChain?: Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface SessionsHost {
|
|
96
|
+
readonly workspace: string;
|
|
97
|
+
readonly limits: ResolvedProcessSessionLimits;
|
|
98
|
+
readonly defaultOwner: string;
|
|
99
|
+
readonly policy: CreateProcessSessionsOptions["policy"];
|
|
100
|
+
readonly onEvent: CreateProcessSessionsOptions["onEvent"];
|
|
101
|
+
readonly sandbox: CreateProcessSessionsOptions["sandbox"];
|
|
102
|
+
readonly ptyBackend: CreateProcessSessionsOptions["ptyBackend"];
|
|
103
|
+
readonly ptyResizeCapable: boolean;
|
|
104
|
+
readonly sessions: Map<string, SessionRecord>;
|
|
105
|
+
disposed: boolean;
|
|
106
|
+
sandboxLost: boolean;
|
|
107
|
+
readonly checkpoints: CreateProcessSessionsOptions["checkpoints"];
|
|
108
|
+
readonly leases: CreateProcessSessionsOptions["leases"];
|
|
109
|
+
readonly ownerId: CreateProcessSessionsOptions["ownerId"];
|
|
110
|
+
readonly recoveryBackend: ProcessRecoveryBackend | undefined;
|
|
111
|
+
readonly recoveryLimits: ReturnType<typeof resolveProcessRecoveryLimits>;
|
|
112
|
+
readonly durable: boolean;
|
|
113
|
+
readonly ownership: CreateProcessSessionsOptions["ownership"];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function createSessionsHost(options: CreateProcessSessionsOptions): SessionsHost {
|
|
117
|
+
const checkpoints = options.checkpoints;
|
|
118
|
+
const leases = options.leases;
|
|
119
|
+
const ownerId = options.ownerId;
|
|
120
|
+
const recoveryBackend =
|
|
121
|
+
options.recoveryBackend ??
|
|
122
|
+
(options.sandbox && typeof options.sandbox.attachProcess === "function"
|
|
123
|
+
? { attach: (ref: string) => options.sandbox!.attachProcess!(ref) }
|
|
124
|
+
: undefined);
|
|
125
|
+
const durable = checkpoints !== undefined || leases !== undefined || ownerId !== undefined || recoveryBackend !== undefined;
|
|
126
|
+
if (durable && !(checkpoints && leases && ownerId)) {
|
|
127
|
+
throw new ProcessRecoveryError(
|
|
128
|
+
"ERR_PRISM_RECOVERY_UNSUPPORTED",
|
|
129
|
+
"durable process recovery requires checkpoints, leases, and ownerId together",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
workspace: resolve(options.cwd),
|
|
134
|
+
limits: resolveProcessSessionLimits(options.limits),
|
|
135
|
+
defaultOwner: ownershipKey(options.ownership, options.identity),
|
|
136
|
+
policy: options.policy,
|
|
137
|
+
onEvent: options.onEvent,
|
|
138
|
+
sandbox: options.sandbox,
|
|
139
|
+
ptyBackend: options.ptyBackend,
|
|
140
|
+
ptyResizeCapable: options.ptyBackend?.capabilities?.resize === true,
|
|
141
|
+
sessions: new Map(),
|
|
142
|
+
disposed: false,
|
|
143
|
+
sandboxLost: false,
|
|
144
|
+
checkpoints,
|
|
145
|
+
leases,
|
|
146
|
+
ownerId,
|
|
147
|
+
recoveryBackend,
|
|
148
|
+
recoveryLimits: resolveProcessRecoveryLimits(options.recoveryLimits),
|
|
149
|
+
durable,
|
|
150
|
+
ownership: options.ownership,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Durable recovery seams. `createSessionsHost` already fails closed when a durable host
|
|
156
|
+
* is missing any of the three, so callers under a `host.durable` guard can take them here
|
|
157
|
+
* without non-null assertions — and a host that somehow lost one fails closed again.
|
|
158
|
+
*/
|
|
159
|
+
export function durableSeams(host: SessionsHost): {
|
|
160
|
+
readonly checkpoints: NonNullable<SessionsHost["checkpoints"]>;
|
|
161
|
+
readonly leases: NonNullable<SessionsHost["leases"]>;
|
|
162
|
+
readonly ownerId: NonNullable<SessionsHost["ownerId"]>;
|
|
163
|
+
} {
|
|
164
|
+
const { checkpoints, leases, ownerId } = host;
|
|
165
|
+
if (!checkpoints || !leases || !ownerId) {
|
|
166
|
+
throw new ProcessRecoveryError(
|
|
167
|
+
"ERR_PRISM_RECOVERY_UNSUPPORTED",
|
|
168
|
+
"durable process recovery requires checkpoints, leases, and ownerId together",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return { checkpoints, leases, ownerId };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function emit(host: SessionsHost, event: CodingProcessEvent): void {
|
|
175
|
+
host.onEvent?.(event);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function assertNotDisposed(host: SessionsHost): void {
|
|
179
|
+
if (host.disposed) throw new ProcessSessionError("ERR_PRISM_PROCESS_STATE", "process sessions disposed");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function isTerminalState(state: ProcessSessionState): boolean {
|
|
183
|
+
return state === "exited" || state === "killed" || state === "released" || state === "expired" || state === "unknown";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Wakes every reader waiting on this record: a chunk landed, or the session went terminal. */
|
|
187
|
+
export function notifyOutput(record: SessionRecord): void {
|
|
188
|
+
for (const listener of [...record.outputListeners]) {
|
|
189
|
+
try {
|
|
190
|
+
listener();
|
|
191
|
+
} catch {
|
|
192
|
+
// swallowed on purpose: a reader must never fail the process that produced the output
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function settleWaiters(record: SessionRecord): void {
|
|
198
|
+
const result: ProcessExitResult = { exitCode: record.exitCode, state: record.state };
|
|
199
|
+
const waiters = record.waiters;
|
|
200
|
+
record.waiters = [];
|
|
201
|
+
for (const resolveWait of waiters) resolveWait(result);
|
|
202
|
+
notifyOutput(record);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function resolveTerminal(
|
|
206
|
+
host: SessionsHost,
|
|
207
|
+
terminal: ProcessTerminalRequest | undefined,
|
|
208
|
+
): { columns: number; rows: number; term: string } {
|
|
209
|
+
const columns = terminal?.columns ?? Math.min(DEFAULT_MAX_TERMINAL_COLUMNS, host.limits.maxTerminalColumns);
|
|
210
|
+
const rows = terminal?.rows ?? Math.min(DEFAULT_MAX_TERMINAL_ROWS, host.limits.maxTerminalRows);
|
|
211
|
+
const term = terminal?.term ?? DEFAULT_TERM;
|
|
212
|
+
if (!Number.isSafeInteger(columns) || columns < 1 || columns > host.limits.maxTerminalColumns) {
|
|
213
|
+
throw new ProcessSessionError("ERR_PRISM_PROCESS_PTY_LIMIT", `columns must be 1..${host.limits.maxTerminalColumns}`);
|
|
214
|
+
}
|
|
215
|
+
if (!Number.isSafeInteger(rows) || rows < 1 || rows > host.limits.maxTerminalRows) {
|
|
216
|
+
throw new ProcessSessionError("ERR_PRISM_PROCESS_PTY_LIMIT", `rows must be 1..${host.limits.maxTerminalRows}`);
|
|
217
|
+
}
|
|
218
|
+
if (Buffer.byteLength(term, "utf8") > host.limits.maxTerminalTermBytes) {
|
|
219
|
+
throw new ProcessSessionError("ERR_PRISM_PROCESS_PTY_LIMIT", `term exceeds maxTerminalTermBytes (${host.limits.maxTerminalTermBytes})`);
|
|
220
|
+
}
|
|
221
|
+
return { columns, rows, term };
|
|
222
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable persist / CAS transitions for managed process sessions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ProcessRecoveryRecord } from "./recovery.js";
|
|
6
|
+
import {
|
|
7
|
+
buildProcessRecoveryRecord,
|
|
8
|
+
deleteProcessRecoveryRecord,
|
|
9
|
+
loadProcessRecoveryRecords,
|
|
10
|
+
PROCESS_RECOVERY_LEASE_NAMESPACE,
|
|
11
|
+
releaseRecordLease,
|
|
12
|
+
saveProcessRecoveryRecord,
|
|
13
|
+
} from "./recovery.js";
|
|
14
|
+
import { durableSeams, isTerminalState, nowIso, type SessionRecord, type SessionsHost } from "./sessions-host.js";
|
|
15
|
+
|
|
16
|
+
// Durable transition write: fire-and-forget CAS (fence/version conflicts mean
|
|
17
|
+
// another replica moved the record first — the newer state wins). The crash
|
|
18
|
+
// window between a terminal transition and its durable write converges on
|
|
19
|
+
// recovery to attach/terminal/unknown, never a duplicate spawn.
|
|
20
|
+
export function persistTransition(host: SessionsHost, record: SessionRecord): void {
|
|
21
|
+
if (!host.durable || record.recoveryVersion === 0) return;
|
|
22
|
+
const seams = durableSeams(host);
|
|
23
|
+
const next = buildProcessRecoveryRecord({
|
|
24
|
+
id: record.id,
|
|
25
|
+
owner: record.owner,
|
|
26
|
+
workspace: record.workspace,
|
|
27
|
+
command: record.command,
|
|
28
|
+
args: record.args,
|
|
29
|
+
commandFingerprint: record.commandFingerprint,
|
|
30
|
+
policyDecision: record.policyDecision,
|
|
31
|
+
startedAt: record.startedAt,
|
|
32
|
+
state: record.state,
|
|
33
|
+
exitCode: record.exitCode,
|
|
34
|
+
releaseOnCancel: record.releaseOnCancel,
|
|
35
|
+
expiresAt: record.expiresAt,
|
|
36
|
+
...(record.backendRef !== undefined ? { backendRef: record.backendRef } : {}),
|
|
37
|
+
...(record.ptyTerminal !== undefined ? { pty: record.ptyTerminal } : {}),
|
|
38
|
+
fencingToken: record.recoveryFencingToken,
|
|
39
|
+
});
|
|
40
|
+
const expectedVersion = record.recoveryVersion;
|
|
41
|
+
record.recoveryVersion += 1;
|
|
42
|
+
const write = async (): Promise<void> => {
|
|
43
|
+
try {
|
|
44
|
+
await saveProcessRecoveryRecord({
|
|
45
|
+
checkpoints: seams.checkpoints,
|
|
46
|
+
record: next,
|
|
47
|
+
expectedVersion,
|
|
48
|
+
version: record.recoveryVersion,
|
|
49
|
+
ownership: host.ownership,
|
|
50
|
+
});
|
|
51
|
+
} catch {
|
|
52
|
+
// stale fence or store failure: the durable record keeps its last state
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
void evictRecoveryOverflow(host);
|
|
56
|
+
// Terminal transition: release the record lease (clean shutdown makes
|
|
57
|
+
// recovery immediate). Live running/starting transitions renew it so a
|
|
58
|
+
// crashed replica's lease lapses within TTL while a live one stays
|
|
59
|
+
// fenced. Best effort on both.
|
|
60
|
+
if (isTerminalState(record.state)) {
|
|
61
|
+
if (record.recoveryLeaseToken) {
|
|
62
|
+
void releaseRecordLease({
|
|
63
|
+
leases: seams.leases,
|
|
64
|
+
id: record.id,
|
|
65
|
+
ownerId: seams.ownerId,
|
|
66
|
+
token: record.recoveryLeaseToken,
|
|
67
|
+
ownership: host.ownership,
|
|
68
|
+
});
|
|
69
|
+
record.recoveryLeaseToken = undefined;
|
|
70
|
+
}
|
|
71
|
+
} else if (record.recoveryLeaseToken) {
|
|
72
|
+
void seams.leases
|
|
73
|
+
.renewLease({
|
|
74
|
+
namespace: PROCESS_RECOVERY_LEASE_NAMESPACE,
|
|
75
|
+
key: `recover:${record.id}`,
|
|
76
|
+
ownerId: seams.ownerId,
|
|
77
|
+
token: record.recoveryLeaseToken,
|
|
78
|
+
ttlMs: host.recoveryLimits.leaseTtlMs,
|
|
79
|
+
...host.ownership,
|
|
80
|
+
})
|
|
81
|
+
.catch(() => {
|
|
82
|
+
// lease lapsed or fenced elsewhere; recovery is attestation-gated
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
// Per-record chain: transition CAS writes must never invert order on slow
|
|
87
|
+
// stores (a later terminal write landing before the running write would
|
|
88
|
+
// fail its CAS and leave the record running forever).
|
|
89
|
+
record.recoveryWriteChain = (record.recoveryWriteChain ?? Promise.resolve()).then(write, write);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Bound durable record growth: after a terminal transition, drop the oldest
|
|
93
|
+
// terminal records until at most maxRecords remain (running/starting records
|
|
94
|
+
// are never evicted). Best effort, bounded work.
|
|
95
|
+
export async function evictRecoveryOverflow(host: SessionsHost): Promise<void> {
|
|
96
|
+
if (!host.durable) return;
|
|
97
|
+
const seams = durableSeams(host);
|
|
98
|
+
try {
|
|
99
|
+
const page = await loadProcessRecoveryRecords({
|
|
100
|
+
checkpoints: seams.checkpoints,
|
|
101
|
+
limits: { ...host.recoveryLimits, maxRecords: host.recoveryLimits.maxRecords + 1 },
|
|
102
|
+
ownership: host.ownership,
|
|
103
|
+
});
|
|
104
|
+
if (page.records.length <= host.recoveryLimits.maxRecords) return;
|
|
105
|
+
const overflow = page.records.length - host.recoveryLimits.maxRecords;
|
|
106
|
+
let evicted = 0;
|
|
107
|
+
for (const { record } of [...page.records].reverse()) {
|
|
108
|
+
if (evicted >= overflow) break;
|
|
109
|
+
if (record.state === "running" || record.state === "starting") continue;
|
|
110
|
+
await deleteProcessRecoveryRecord({ checkpoints: seams.checkpoints, id: record.id, ownership: host.ownership });
|
|
111
|
+
evicted += 1;
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// best effort: caps are enforced again on the next transition
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Atomic starting|running -> unknown (never an exit code). Returns false when
|
|
119
|
+
// a fence/version conflict means another replica moved the record first.
|
|
120
|
+
export async function persistRecoveryUnknown(
|
|
121
|
+
host: SessionsHost,
|
|
122
|
+
current: ProcessRecoveryRecord,
|
|
123
|
+
version: number,
|
|
124
|
+
signal?: AbortSignal,
|
|
125
|
+
): Promise<boolean> {
|
|
126
|
+
const next = buildProcessRecoveryRecord({ ...current, state: "unknown", exitCode: null, updatedAt: nowIso() });
|
|
127
|
+
const seams = durableSeams(host);
|
|
128
|
+
try {
|
|
129
|
+
await saveProcessRecoveryRecord({
|
|
130
|
+
checkpoints: seams.checkpoints,
|
|
131
|
+
record: next,
|
|
132
|
+
expectedVersion: version,
|
|
133
|
+
version: version + 1,
|
|
134
|
+
ownership: host.ownership,
|
|
135
|
+
signal,
|
|
136
|
+
});
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable recover / reattach phase for managed process sessions.
|
|
3
|
+
*/
|
|
4
|
+
import { OutputAccumulator } from "../output-accumulator.js";
|
|
5
|
+
import {
|
|
6
|
+
acquireRecordLease,
|
|
7
|
+
attachWithTimeout,
|
|
8
|
+
buildProcessRecoveryRecord,
|
|
9
|
+
loadProcessRecoveryRecord,
|
|
10
|
+
loadProcessRecoveryRecords,
|
|
11
|
+
ProcessRecoveryError,
|
|
12
|
+
type ProcessRecoveryRecord,
|
|
13
|
+
type ProcessRecoveryRecordReport,
|
|
14
|
+
type ProcessRecoveryReport,
|
|
15
|
+
releaseRecordLease,
|
|
16
|
+
saveProcessRecoveryRecord,
|
|
17
|
+
} from "./recovery.js";
|
|
18
|
+
import { assertNotDisposed, durableSeams, isTerminalState, nowIso, type SessionRecord, type SessionsHost } from "./sessions-host.js";
|
|
19
|
+
import { persistRecoveryUnknown } from "./sessions-monitor.js";
|
|
20
|
+
import { makeHandle } from "./sessions-spawn.js";
|
|
21
|
+
import { sweepExpired, terminateRecord } from "./sessions-teardown.js";
|
|
22
|
+
import type { ProcessPtyHandle, ProcessSandboxHandle, ProcessSession } from "./types.js";
|
|
23
|
+
|
|
24
|
+
// Reattach an attested handle into the live registry. Recovered sessions
|
|
25
|
+
// expose control (input/signal/kill/release/resize/wait) through the handle;
|
|
26
|
+
// output streaming is not re-established after a restart — the host backend
|
|
27
|
+
// owns any buffered output behind its opaque ref.
|
|
28
|
+
export function attachRecoveredHandle(
|
|
29
|
+
host: SessionsHost,
|
|
30
|
+
current: ProcessRecoveryRecord,
|
|
31
|
+
handle: ProcessPtyHandle | ProcessSandboxHandle,
|
|
32
|
+
version: number,
|
|
33
|
+
): void {
|
|
34
|
+
const accumulator = new OutputAccumulator({
|
|
35
|
+
maxBytes: host.limits.maxOutputChunkBytes,
|
|
36
|
+
maxLines: 100_000,
|
|
37
|
+
maxTotalOutputBytes: host.limits.maxTotalOutputBytes,
|
|
38
|
+
tempFilePrefix: "prism-proc",
|
|
39
|
+
});
|
|
40
|
+
const record: SessionRecord = {
|
|
41
|
+
id: current.id,
|
|
42
|
+
owner: current.owner,
|
|
43
|
+
workspace: current.workspace,
|
|
44
|
+
command: current.command,
|
|
45
|
+
args: current.args,
|
|
46
|
+
commandFingerprint: current.commandFingerprint,
|
|
47
|
+
policyDecision: current.policyDecision,
|
|
48
|
+
startedAt: current.startedAt,
|
|
49
|
+
releaseOnCancel: current.releaseOnCancel,
|
|
50
|
+
expiresAt: current.expiresAt,
|
|
51
|
+
state: "running",
|
|
52
|
+
exitCode: null,
|
|
53
|
+
ptyTerminal: current.pty,
|
|
54
|
+
ptyResizeAt: [],
|
|
55
|
+
accumulator,
|
|
56
|
+
waiters: [],
|
|
57
|
+
outputListeners: new Set(),
|
|
58
|
+
stdinClosed: false,
|
|
59
|
+
handle: null as unknown as ProcessSession,
|
|
60
|
+
backendRef: current.backendRef,
|
|
61
|
+
recoveryFencingToken: current.fencingToken,
|
|
62
|
+
recoveryVersion: version,
|
|
63
|
+
};
|
|
64
|
+
if (current.pty !== undefined) {
|
|
65
|
+
record.pty = handle as ProcessPtyHandle;
|
|
66
|
+
} else {
|
|
67
|
+
record.backend = handle as ProcessSandboxHandle;
|
|
68
|
+
}
|
|
69
|
+
record.handle = makeHandle(host, record);
|
|
70
|
+
host.sessions.set(record.id, record);
|
|
71
|
+
void handle
|
|
72
|
+
.wait()
|
|
73
|
+
.then((result) => {
|
|
74
|
+
if (record.state !== "running" && record.state !== "starting") return;
|
|
75
|
+
terminateRecord(host, record, "exited", result.exitCode);
|
|
76
|
+
})
|
|
77
|
+
.catch(() => {
|
|
78
|
+
if (record.state !== "running" && record.state !== "starting") return;
|
|
79
|
+
terminateRecord(host, record, "unknown", null);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function recoverSessions(host: SessionsHost, recoverOptions?: { signal?: AbortSignal }): Promise<ProcessRecoveryReport> {
|
|
84
|
+
assertNotDisposed(host);
|
|
85
|
+
if (!host.durable) {
|
|
86
|
+
throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNSUPPORTED", "durable process recovery is not configured on this host");
|
|
87
|
+
}
|
|
88
|
+
const seams = durableSeams(host);
|
|
89
|
+
sweepExpired(host);
|
|
90
|
+
const { records } = await loadProcessRecoveryRecords({
|
|
91
|
+
checkpoints: seams.checkpoints,
|
|
92
|
+
limits: host.recoveryLimits,
|
|
93
|
+
ownership: host.ownership,
|
|
94
|
+
signal: recoverOptions?.signal,
|
|
95
|
+
});
|
|
96
|
+
const report: ProcessRecoveryRecordReport[] = [];
|
|
97
|
+
let attached = 0;
|
|
98
|
+
let terminal = 0;
|
|
99
|
+
let unknown = 0;
|
|
100
|
+
for (const { record } of records) {
|
|
101
|
+
const live = host.sessions.get(record.id);
|
|
102
|
+
if (live) {
|
|
103
|
+
// Already live in this registry: nothing to do, report the truth.
|
|
104
|
+
report.push({ id: record.id, outcome: "attached", state: live.state, exitCode: live.exitCode });
|
|
105
|
+
if (live.state === "running" || live.state === "starting") attached += 1;
|
|
106
|
+
else terminal += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (
|
|
110
|
+
record.state === "exited" ||
|
|
111
|
+
record.state === "killed" ||
|
|
112
|
+
record.state === "released" ||
|
|
113
|
+
record.state === "expired" ||
|
|
114
|
+
record.state === "unknown"
|
|
115
|
+
) {
|
|
116
|
+
report.push({ id: record.id, outcome: "terminal", state: record.state, exitCode: record.exitCode });
|
|
117
|
+
terminal += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
// starting | running durable record without a live handle: attach-if-
|
|
121
|
+
// attested, else atomic unknown. Never fabricate an exit code.
|
|
122
|
+
const lease = await acquireRecordLease({
|
|
123
|
+
leases: seams.leases,
|
|
124
|
+
id: record.id,
|
|
125
|
+
ownerId: seams.ownerId,
|
|
126
|
+
ttlMs: host.recoveryLimits.leaseTtlMs,
|
|
127
|
+
ownership: host.ownership,
|
|
128
|
+
signal: recoverOptions?.signal,
|
|
129
|
+
});
|
|
130
|
+
if (!lease) {
|
|
131
|
+
// Another replica owns or is recovering this record.
|
|
132
|
+
report.push({ id: record.id, outcome: "unknown", state: record.state, exitCode: null });
|
|
133
|
+
unknown += 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
// Fresh CAS read under the lease: another replica may have moved the record.
|
|
138
|
+
const fresh = await loadProcessRecoveryRecord({
|
|
139
|
+
checkpoints: seams.checkpoints,
|
|
140
|
+
id: record.id,
|
|
141
|
+
limits: host.recoveryLimits,
|
|
142
|
+
ownership: host.ownership,
|
|
143
|
+
signal: recoverOptions?.signal,
|
|
144
|
+
});
|
|
145
|
+
if (!fresh) {
|
|
146
|
+
report.push({ id: record.id, outcome: "terminal", state: "unknown", exitCode: null, error: "ERR_PRISM_RECOVERY_UNKNOWN" });
|
|
147
|
+
terminal += 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const current = fresh.record;
|
|
151
|
+
if (
|
|
152
|
+
current.state === "exited" ||
|
|
153
|
+
current.state === "killed" ||
|
|
154
|
+
current.state === "released" ||
|
|
155
|
+
current.state === "expired" ||
|
|
156
|
+
current.state === "unknown"
|
|
157
|
+
) {
|
|
158
|
+
report.push({ id: current.id, outcome: "terminal", state: current.state, exitCode: current.exitCode });
|
|
159
|
+
terminal += 1;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (Date.now() >= current.expiresAt) {
|
|
163
|
+
// Lifetime lapsed while unrecovered: persist the expiry.
|
|
164
|
+
const expired = buildProcessRecoveryRecord({
|
|
165
|
+
...current,
|
|
166
|
+
state: "expired",
|
|
167
|
+
exitCode: null,
|
|
168
|
+
updatedAt: nowIso(),
|
|
169
|
+
});
|
|
170
|
+
await saveProcessRecoveryRecord({
|
|
171
|
+
checkpoints: seams.checkpoints,
|
|
172
|
+
record: expired,
|
|
173
|
+
expectedVersion: fresh.version,
|
|
174
|
+
version: fresh.version + 1,
|
|
175
|
+
ownership: host.ownership,
|
|
176
|
+
signal: recoverOptions?.signal,
|
|
177
|
+
});
|
|
178
|
+
report.push({ id: current.id, outcome: "terminal", state: "expired", exitCode: null });
|
|
179
|
+
terminal += 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const active = [...host.sessions.values()].filter((s) => s.state === "running" || s.state === "starting").length;
|
|
183
|
+
if (active >= host.limits.maxSessions) {
|
|
184
|
+
// Cannot admit another live session: record unknown (fails closed).
|
|
185
|
+
await persistRecoveryUnknown(host, current, fresh.version, recoverOptions?.signal);
|
|
186
|
+
report.push({ id: current.id, outcome: "unknown", state: "unknown", exitCode: null });
|
|
187
|
+
unknown += 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
let attachErrorCode: ProcessRecoveryRecordReport["error"];
|
|
191
|
+
if (current.backendRef !== undefined && host.recoveryBackend) {
|
|
192
|
+
try {
|
|
193
|
+
const handle = await attachWithTimeout(host.recoveryBackend, current.backendRef, host.recoveryLimits.attachTimeoutMs);
|
|
194
|
+
if (handle) {
|
|
195
|
+
attachRecoveredHandle(host, current, handle, fresh.version);
|
|
196
|
+
attached += 1;
|
|
197
|
+
report.push({ id: current.id, outcome: "attached", state: "running", exitCode: null });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
} catch (attachError) {
|
|
201
|
+
attachErrorCode = attachError instanceof ProcessRecoveryError ? attachError.code : ("ERR_PRISM_RECOVERY_UNKNOWN" as const);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// No ref, no backend, unattested attach, or attach failure: atomic unknown.
|
|
205
|
+
const saved = await persistRecoveryUnknown(host, current, fresh.version, recoverOptions?.signal);
|
|
206
|
+
if (!saved) {
|
|
207
|
+
// CAS/fence conflict: another replica moved the record; re-report its state.
|
|
208
|
+
const again = await loadProcessRecoveryRecord({
|
|
209
|
+
checkpoints: seams.checkpoints,
|
|
210
|
+
id: current.id,
|
|
211
|
+
limits: host.recoveryLimits,
|
|
212
|
+
ownership: host.ownership,
|
|
213
|
+
signal: recoverOptions?.signal,
|
|
214
|
+
});
|
|
215
|
+
const reported = again?.record;
|
|
216
|
+
report.push({
|
|
217
|
+
id: current.id,
|
|
218
|
+
outcome: reported && isTerminalState(reported.state) ? "terminal" : "unknown",
|
|
219
|
+
state: reported?.state ?? "unknown",
|
|
220
|
+
exitCode: reported?.exitCode ?? null,
|
|
221
|
+
error: attachErrorCode,
|
|
222
|
+
});
|
|
223
|
+
if (reported && isTerminalState(reported.state)) terminal += 1;
|
|
224
|
+
else unknown += 1;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
report.push({ id: current.id, outcome: "unknown", state: "unknown", exitCode: null, error: attachErrorCode });
|
|
228
|
+
unknown += 1;
|
|
229
|
+
} finally {
|
|
230
|
+
await releaseRecordLease({
|
|
231
|
+
leases: seams.leases,
|
|
232
|
+
id: record.id,
|
|
233
|
+
ownerId: seams.ownerId,
|
|
234
|
+
token: lease.token,
|
|
235
|
+
ownership: host.ownership,
|
|
236
|
+
signal: recoverOptions?.signal,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return { records: report, attached, terminal, unknown };
|
|
241
|
+
}
|