@arnilo/prism-coding-agent 0.2.4 → 0.2.6

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +10 -0
  3. package/dist/coding-checkpoint.js +4 -0
  4. package/dist/diagnostics.d.ts +83 -0
  5. package/dist/diagnostics.js +179 -0
  6. package/dist/git.d.ts +27 -6
  7. package/dist/git.js +58 -1
  8. package/dist/index.d.ts +13 -4
  9. package/dist/index.js +13 -2
  10. package/dist/language/client.d.ts +25 -0
  11. package/dist/language/client.js +54 -0
  12. package/dist/language/framing.d.ts +9 -1
  13. package/dist/language/framing.js +89 -17
  14. package/dist/language/index.d.ts +1 -1
  15. package/dist/language/intelligence.js +58 -0
  16. package/dist/language/types.d.ts +32 -0
  17. package/dist/limits.d.ts +59 -0
  18. package/dist/limits.js +59 -0
  19. package/dist/process/index.d.ts +4 -1
  20. package/dist/process/index.js +1 -0
  21. package/dist/process/recovery.d.ts +174 -0
  22. package/dist/process/recovery.js +320 -0
  23. package/dist/process/sessions.js +714 -25
  24. package/dist/process/types.d.ts +128 -4
  25. package/dist/process/types.js +7 -1
  26. package/dist/repository/glob.d.ts +4 -0
  27. package/dist/repository/glob.js +143 -0
  28. package/dist/repository/indexed-search.d.ts +121 -0
  29. package/dist/repository/indexed-search.js +313 -0
  30. package/dist/repository/list.d.ts +3 -0
  31. package/dist/repository/list.js +119 -0
  32. package/dist/repository/operations.d.ts +5 -0
  33. package/dist/repository/operations.js +14 -0
  34. package/dist/repository/path.d.ts +18 -0
  35. package/dist/repository/path.js +91 -0
  36. package/dist/repository/search.d.ts +9 -0
  37. package/dist/repository/search.js +284 -0
  38. package/dist/repository/types.d.ts +138 -0
  39. package/dist/repository/types.js +31 -0
  40. package/dist/repository/walk.d.ts +22 -0
  41. package/dist/repository/walk.js +99 -0
  42. package/dist/repository.d.ts +11 -172
  43. package/dist/repository.js +11 -748
  44. package/dist/review.d.ts +150 -0
  45. package/dist/review.js +222 -0
  46. package/dist/search.d.ts +3 -1
  47. package/dist/search.js +42 -7
  48. package/dist/workspace-lifecycle.d.ts +153 -0
  49. package/dist/workspace-lifecycle.js +629 -0
  50. package/package.json +3 -3
@@ -0,0 +1,174 @@
1
+ import type { CheckpointStore, LeaseStore, LeaseRecord, OwnershipScope } from "@arnilo/prism";
2
+ import type { ProcessPtyHandle, ProcessSandboxHandle, ProcessSessionState } from "./types.js";
3
+ /** Versioned durable namespace for managed-process recovery records (separate from CodingCheckpointMetadata v1). */
4
+ export declare const PROCESS_RECOVERY_NAMESPACE = "prism.coding-agent.process.v1";
5
+ /** Namespace for per-record recovery leases. */
6
+ export declare const PROCESS_RECOVERY_LEASE_NAMESPACE = "prism.coding-agent.process.lease.v1";
7
+ export declare const PROCESS_RECOVERY_SCHEMA_VERSION = 1;
8
+ export declare const PROCESS_RECOVERY_CATEGORY = "coding-process";
9
+ export type ProcessRecoveryOutcome = "attached" | "terminal" | "unknown";
10
+ /** One durable process recovery record. Metadata only — no handles, no output, no secrets. */
11
+ export interface ProcessRecoveryRecord {
12
+ readonly schemaVersion: typeof PROCESS_RECOVERY_SCHEMA_VERSION;
13
+ /** ProcessSessions session id (`proc_<hex>`); also the checkpoint key. */
14
+ readonly id: string;
15
+ readonly owner: string;
16
+ readonly workspace: string;
17
+ readonly command: string;
18
+ readonly args: readonly string[];
19
+ readonly commandFingerprint: string;
20
+ readonly policyDecision: string;
21
+ readonly startedAt: string;
22
+ readonly state: ProcessSessionState;
23
+ readonly exitCode: number | null;
24
+ readonly releaseOnCancel: boolean;
25
+ readonly expiresAt: number;
26
+ /** Opaque non-secret host reattachment ref; absent => no attach possible. */
27
+ readonly backendRef?: string;
28
+ /** Bounded PTY geometry metadata only (never terminal output); resolved columns/rows/term. */
29
+ readonly pty?: {
30
+ readonly columns: number;
31
+ readonly rows: number;
32
+ readonly term: string;
33
+ };
34
+ /** Monotonic lease fencing token from the record's recovery lease. */
35
+ readonly fencingToken: number;
36
+ readonly updatedAt: string;
37
+ }
38
+ /** Host-attested reattachment capability. `attach` resolves an opaque ref to a live handle or returns null. */
39
+ export interface ProcessRecoveryBackend {
40
+ /**
41
+ * Resolve an opaque non-secret ref to a live process handle. Return null when
42
+ * the ref cannot be attested. Throwing is treated as attach failure and the
43
+ * record becomes unknown; backend error text is never surfaced.
44
+ */
45
+ attach(ref: string): Promise<ProcessPtyHandle | ProcessSandboxHandle | null> | ProcessPtyHandle | ProcessSandboxHandle | null;
46
+ }
47
+ /** Per-record recovery report entry. */
48
+ export interface ProcessRecoveryRecordReport {
49
+ readonly id: string;
50
+ readonly outcome: ProcessRecoveryOutcome;
51
+ readonly state: ProcessSessionState;
52
+ readonly exitCode: number | null;
53
+ /** Generic failure code when the record could not be attached or transitioned (never backend error text). */
54
+ readonly error?: ProcessRecoveryErrorCode;
55
+ }
56
+ /** Bounded recover() report. */
57
+ export interface ProcessRecoveryReport {
58
+ readonly records: readonly ProcessRecoveryRecordReport[];
59
+ readonly attached: number;
60
+ readonly terminal: number;
61
+ readonly unknown: number;
62
+ }
63
+ export type ProcessRecoveryErrorCode = "ERR_PRISM_RECOVERY_UNSUPPORTED" | "ERR_PRISM_RECOVERY_LIMIT" | "ERR_PRISM_RECOVERY_OWNERSHIP" | "ERR_PRISM_RECOVERY_FENCE" | "ERR_PRISM_RECOVERY_UNKNOWN" | "ERR_PRISM_RECOVERY_UNTRUSTED" | "ERR_PRISM_RECOVERY_TIMEOUT";
64
+ /** Stable typed failures for the recovery seam. */
65
+ export declare class ProcessRecoveryError extends Error {
66
+ readonly code: ProcessRecoveryErrorCode;
67
+ constructor(code: ProcessRecoveryErrorCode, message: string);
68
+ }
69
+ export interface ProcessRecoveryLimits {
70
+ readonly maxRecords?: number;
71
+ readonly leaseTtlMs?: number;
72
+ readonly attachTimeoutMs?: number;
73
+ readonly backendRefBytes?: number;
74
+ readonly recordBytes?: number;
75
+ }
76
+ export interface ResolvedProcessRecoveryLimits {
77
+ readonly maxRecords: number;
78
+ readonly leaseTtlMs: number;
79
+ readonly attachTimeoutMs: number;
80
+ readonly backendRefBytes: number;
81
+ readonly recordBytes: number;
82
+ }
83
+ export declare function resolveProcessRecoveryLimits(limits?: ProcessRecoveryLimits): ResolvedProcessRecoveryLimits;
84
+ /** Bounded validation of one recovery record. Corrupt/oversized/foreign records fail closed. */
85
+ export declare function validateProcessRecoveryRecord(record: unknown, limits: ResolvedProcessRecoveryLimits): ProcessRecoveryRecord;
86
+ /** Validate one opaque backend ref (non-secret, bounded, control-free). */
87
+ export declare function validateBackendRef(ref: unknown, limits: ResolvedProcessRecoveryLimits): string;
88
+ /** Build a fresh record for an in-memory session (intent or transition). */
89
+ export declare function buildProcessRecoveryRecord(input: {
90
+ readonly id: string;
91
+ readonly owner: string;
92
+ readonly workspace: string;
93
+ readonly command: string;
94
+ readonly args: readonly string[];
95
+ readonly commandFingerprint: string;
96
+ readonly policyDecision: string;
97
+ readonly startedAt: string;
98
+ readonly state: ProcessSessionState;
99
+ readonly exitCode: number | null;
100
+ readonly releaseOnCancel: boolean;
101
+ readonly expiresAt: number;
102
+ readonly backendRef?: string;
103
+ readonly pty?: {
104
+ readonly columns: number;
105
+ readonly rows: number;
106
+ readonly term: string;
107
+ };
108
+ readonly fencingToken: number;
109
+ readonly updatedAt?: string;
110
+ }): ProcessRecoveryRecord;
111
+ export interface RecoveryRecordPage {
112
+ readonly records: ReadonlyArray<{
113
+ readonly record: ProcessRecoveryRecord;
114
+ readonly version: number;
115
+ }>;
116
+ }
117
+ /** Bounded load of recovery records under one ownership scope (O(maxRecords)). */
118
+ export declare function loadProcessRecoveryRecords(input: {
119
+ readonly checkpoints: CheckpointStore;
120
+ readonly limits: ResolvedProcessRecoveryLimits;
121
+ readonly ownership?: OwnershipScope;
122
+ readonly signal?: AbortSignal;
123
+ }): Promise<RecoveryRecordPage>;
124
+ /** Load one recovery record by session id (null when absent or corrupt). */
125
+ export declare function loadProcessRecoveryRecord(input: {
126
+ readonly checkpoints: CheckpointStore;
127
+ readonly id: string;
128
+ readonly limits: ResolvedProcessRecoveryLimits;
129
+ readonly ownership?: OwnershipScope;
130
+ readonly signal?: AbortSignal;
131
+ }): Promise<{
132
+ readonly record: ProcessRecoveryRecord;
133
+ readonly version: number;
134
+ } | null>;
135
+ /** CAS save of one recovery record. Fence/version conflicts throw ERR_PRISM_RECOVERY_FENCE. */
136
+ export declare function saveProcessRecoveryRecord(input: {
137
+ readonly checkpoints: CheckpointStore;
138
+ readonly record: ProcessRecoveryRecord;
139
+ readonly expectedVersion: number;
140
+ readonly version: number;
141
+ readonly ownership?: OwnershipScope;
142
+ readonly signal?: AbortSignal;
143
+ }): Promise<{
144
+ readonly version: number;
145
+ }>;
146
+ /** Delete one recovery record (false when absent). */
147
+ export declare function deleteProcessRecoveryRecord(input: {
148
+ readonly checkpoints: CheckpointStore;
149
+ readonly id: string;
150
+ readonly ownership?: OwnershipScope;
151
+ readonly signal?: AbortSignal;
152
+ }): Promise<boolean>;
153
+ /** Acquire the per-record recovery lease; null => another replica owns or is recovering the record. */
154
+ export declare function acquireRecordLease(input: {
155
+ readonly leases: LeaseStore;
156
+ readonly id: string;
157
+ readonly ownerId: string;
158
+ readonly ttlMs: number;
159
+ readonly ownership?: OwnershipScope;
160
+ readonly signal?: AbortSignal;
161
+ }): Promise<LeaseRecord | null>;
162
+ /** Release a recovery lease (best effort; ignores conflicts). */
163
+ export declare function releaseRecordLease(input: {
164
+ readonly leases: LeaseStore;
165
+ readonly id: string;
166
+ readonly ownerId: string;
167
+ readonly token: string;
168
+ readonly ownership?: OwnershipScope;
169
+ readonly signal?: AbortSignal;
170
+ }): Promise<void>;
171
+ /** Bounded attach deadline: a backend that does not answer within attachTimeoutMs fails closed. */
172
+ export declare function attachWithTimeout(backend: ProcessRecoveryBackend, ref: string, timeoutMs: number): Promise<ProcessPtyHandle | ProcessSandboxHandle | null>;
173
+ /** True when a checkpoint load/save failure is an ownership conflict (fail closed as OWNERSHIP). */
174
+ export declare function isOwnershipConflict(error: unknown): boolean;
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Durable managed-process recovery (plan 026 Task 5).
3
+ *
4
+ * Metadata-only recovery records over CheckpointStore CAS + LeaseStore fencing.
5
+ * A durable record captures bounded process intent and lifecycle metadata —
6
+ * never a child/PTY handle, controller, promise, raw output, env, token, or
7
+ * credential. Recovery is attach-if-attested: a host `ProcessRecoveryBackend`
8
+ * may reattach an opaque non-secret `backendRef`; otherwise `starting|running`
9
+ * records atomically become `unknown` with no fabricated exit code, and no
10
+ * PID probing or process survival claim is ever made.
11
+ *
12
+ * This module is the pure codec/storage seam: validation, record build, bounded
13
+ * checkpoint/lease access, and the bounded attach deadline. The ProcessSessions
14
+ * state machine (sessions.ts) owns when records are written and how a recovered
15
+ * handle is wired back into the live registry.
16
+ */
17
+ import { isAbsolute } from "node:path";
18
+ import { DEFAULT_MAX_RECOVERY_ATTACH_TIMEOUT_MS, DEFAULT_MAX_RECOVERY_BACKEND_REF_BYTES, DEFAULT_MAX_RECOVERY_LEASE_TTL_MS, DEFAULT_MAX_RECOVERY_RECORD_BYTES, DEFAULT_MAX_RECOVERY_RECORDS, HARD_MAX_RECOVERY_ATTACH_TIMEOUT_MS, HARD_MAX_RECOVERY_BACKEND_REF_BYTES, HARD_MAX_RECOVERY_LEASE_TTL_MS, HARD_MAX_RECOVERY_RECORD_BYTES, HARD_MAX_RECOVERY_RECORDS, validateCodingLimit, } from "../limits.js";
19
+ /** Versioned durable namespace for managed-process recovery records (separate from CodingCheckpointMetadata v1). */
20
+ export const PROCESS_RECOVERY_NAMESPACE = "prism.coding-agent.process.v1";
21
+ /** Namespace for per-record recovery leases. */
22
+ export const PROCESS_RECOVERY_LEASE_NAMESPACE = "prism.coding-agent.process.lease.v1";
23
+ export const PROCESS_RECOVERY_SCHEMA_VERSION = 1;
24
+ export const PROCESS_RECOVERY_CATEGORY = "coding-process";
25
+ /** Stable typed failures for the recovery seam. */
26
+ export class ProcessRecoveryError extends Error {
27
+ code;
28
+ constructor(code, message) {
29
+ super(message);
30
+ this.name = "ProcessRecoveryError";
31
+ this.code = code;
32
+ }
33
+ }
34
+ export function resolveProcessRecoveryLimits(limits) {
35
+ return {
36
+ maxRecords: validateCodingLimit("maxRecords", limits?.maxRecords ?? DEFAULT_MAX_RECOVERY_RECORDS, HARD_MAX_RECOVERY_RECORDS),
37
+ leaseTtlMs: validateCodingLimit("leaseTtlMs", limits?.leaseTtlMs ?? DEFAULT_MAX_RECOVERY_LEASE_TTL_MS, HARD_MAX_RECOVERY_LEASE_TTL_MS),
38
+ attachTimeoutMs: validateCodingLimit("attachTimeoutMs", limits?.attachTimeoutMs ?? DEFAULT_MAX_RECOVERY_ATTACH_TIMEOUT_MS, HARD_MAX_RECOVERY_ATTACH_TIMEOUT_MS),
39
+ backendRefBytes: validateCodingLimit("backendRefBytes", limits?.backendRefBytes ?? DEFAULT_MAX_RECOVERY_BACKEND_REF_BYTES, HARD_MAX_RECOVERY_BACKEND_REF_BYTES),
40
+ recordBytes: validateCodingLimit("recordBytes", limits?.recordBytes ?? DEFAULT_MAX_RECOVERY_RECORD_BYTES, HARD_MAX_RECOVERY_RECORD_BYTES),
41
+ };
42
+ }
43
+ const STATE_SET = new Set(["starting", "running", "exited", "killed", "released", "expired", "unknown"]);
44
+ /** Bounded validation of one recovery record. Corrupt/oversized/foreign records fail closed. */
45
+ export function validateProcessRecoveryRecord(record, limits) {
46
+ if (typeof record !== "object" || record === null) {
47
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "recovery record is not an object");
48
+ }
49
+ const value = record;
50
+ if (value.schemaVersion !== PROCESS_RECOVERY_SCHEMA_VERSION) {
51
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", `unsupported recovery record schema version`);
52
+ }
53
+ const id = value.id;
54
+ if (typeof id !== "string" || !/^proc_[0-9a-f]{16}$/.test(id)) {
55
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery record id");
56
+ }
57
+ for (const forbidden of ["env", "token", "credential", "secret", "output", "stdout", "stderr", "commandOutput", "rawOutput"]) {
58
+ if (forbidden in value) {
59
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", `forbidden field ${forbidden} in recovery record`);
60
+ }
61
+ }
62
+ const { owner, workspace, command, args, commandFingerprint, policyDecision, startedAt, state, releaseOnCancel, updatedAt } = value;
63
+ const exitCode = value.exitCode;
64
+ const expiresAt = value.expiresAt;
65
+ const fencingToken = value.fencingToken;
66
+ if (typeof owner !== "string" || owner.length === 0 || Buffer.byteLength(owner, "utf8") > 512) {
67
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery owner");
68
+ }
69
+ if (typeof workspace !== "string" || !isAbsolute(workspace) || Buffer.byteLength(workspace, "utf8") > 4096) {
70
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery workspace");
71
+ }
72
+ if (typeof command !== "string" || command.length === 0 || Buffer.byteLength(command, "utf8") > 4096) {
73
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery command");
74
+ }
75
+ if (!Array.isArray(args) || args.length > 64) {
76
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery args");
77
+ }
78
+ for (const arg of args) {
79
+ if (typeof arg !== "string" || Buffer.byteLength(arg, "utf8") > 4096) {
80
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery arg");
81
+ }
82
+ }
83
+ if (typeof commandFingerprint !== "string" || !/^[0-9a-f]{64}$/.test(commandFingerprint)) {
84
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery command fingerprint");
85
+ }
86
+ if (typeof policyDecision !== "string" || Buffer.byteLength(policyDecision, "utf8") > 512) {
87
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery policy decision");
88
+ }
89
+ if (typeof startedAt !== "string" || Number.isNaN(Date.parse(startedAt))) {
90
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery startedAt");
91
+ }
92
+ if (typeof state !== "string" || !STATE_SET.has(state)) {
93
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery state");
94
+ }
95
+ if (exitCode !== null && exitCode !== undefined && (!Number.isSafeInteger(exitCode) || exitCode < 0)) {
96
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery exitCode");
97
+ }
98
+ if (exitCode === undefined) {
99
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery exitCode");
100
+ }
101
+ if (typeof releaseOnCancel !== "boolean") {
102
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery releaseOnCancel");
103
+ }
104
+ if (typeof expiresAt !== "number" || !Number.isSafeInteger(expiresAt) || expiresAt < 0) {
105
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery expiresAt");
106
+ }
107
+ if (typeof fencingToken !== "number" || !Number.isSafeInteger(fencingToken) || fencingToken < 0) {
108
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery fencingToken");
109
+ }
110
+ if (typeof updatedAt !== "string" || Number.isNaN(Date.parse(updatedAt))) {
111
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery updatedAt");
112
+ }
113
+ let backendRef;
114
+ if (value.backendRef !== undefined) {
115
+ backendRef = validateBackendRef(value.backendRef, limits);
116
+ }
117
+ let pty;
118
+ if (value.pty !== undefined) {
119
+ const raw = value.pty;
120
+ const columns = raw.columns;
121
+ const rows = raw.rows;
122
+ const term = raw.term;
123
+ if (typeof columns !== "number" ||
124
+ !Number.isSafeInteger(columns) ||
125
+ columns < 1 ||
126
+ columns > 500 ||
127
+ typeof rows !== "number" ||
128
+ !Number.isSafeInteger(rows) ||
129
+ rows < 1 ||
130
+ rows > 200) {
131
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery pty geometry");
132
+ }
133
+ if (typeof term !== "string" || Buffer.byteLength(term, "utf8") > 256) {
134
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery pty term");
135
+ }
136
+ pty = { columns, rows, term };
137
+ }
138
+ const recordValue = {
139
+ schemaVersion: PROCESS_RECOVERY_SCHEMA_VERSION,
140
+ id,
141
+ owner,
142
+ workspace,
143
+ command,
144
+ args: [...args],
145
+ commandFingerprint,
146
+ policyDecision,
147
+ startedAt,
148
+ state: state,
149
+ exitCode,
150
+ releaseOnCancel,
151
+ expiresAt,
152
+ ...(backendRef !== undefined ? { backendRef } : {}),
153
+ ...(pty !== undefined ? { pty } : {}),
154
+ fencingToken,
155
+ updatedAt,
156
+ };
157
+ if (Buffer.byteLength(JSON.stringify(recordValue), "utf8") > limits.recordBytes) {
158
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_LIMIT", `recovery record exceeds ${limits.recordBytes} bytes`);
159
+ }
160
+ return recordValue;
161
+ }
162
+ /** Validate one opaque backend ref (non-secret, bounded, control-free). */
163
+ export function validateBackendRef(ref, limits) {
164
+ if (typeof ref !== "string" || ref.length === 0 || Buffer.byteLength(ref, "utf8") > limits.backendRefBytes) {
165
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", `invalid backend ref (max ${limits.backendRefBytes} bytes)`);
166
+ }
167
+ if (/[\u0000-\u001f\u007f]/.test(ref)) {
168
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "backend ref contains control characters");
169
+ }
170
+ return ref;
171
+ }
172
+ /** Build a fresh record for an in-memory session (intent or transition). */
173
+ export function buildProcessRecoveryRecord(input) {
174
+ const record = {
175
+ schemaVersion: PROCESS_RECOVERY_SCHEMA_VERSION,
176
+ id: input.id,
177
+ owner: input.owner,
178
+ workspace: input.workspace,
179
+ command: input.command,
180
+ args: [...input.args],
181
+ commandFingerprint: input.commandFingerprint,
182
+ policyDecision: input.policyDecision,
183
+ startedAt: input.startedAt,
184
+ state: input.state,
185
+ exitCode: input.exitCode,
186
+ releaseOnCancel: input.releaseOnCancel,
187
+ expiresAt: input.expiresAt,
188
+ ...(input.backendRef !== undefined ? { backendRef: input.backendRef } : {}),
189
+ ...(input.pty !== undefined ? { pty: input.pty } : {}),
190
+ fencingToken: input.fencingToken,
191
+ updatedAt: input.updatedAt ?? new Date().toISOString(),
192
+ };
193
+ return record;
194
+ }
195
+ /** Bounded load of recovery records under one ownership scope (O(maxRecords)). */
196
+ export async function loadProcessRecoveryRecords(input) {
197
+ const page = await input.checkpoints.listCheckpoints({
198
+ namespace: PROCESS_RECOVERY_NAMESPACE,
199
+ keyPrefix: "proc_",
200
+ category: PROCESS_RECOVERY_CATEGORY,
201
+ limit: input.limits.maxRecords,
202
+ ...input.ownership,
203
+ signal: input.signal,
204
+ });
205
+ const records = [];
206
+ for (const item of page.items) {
207
+ try {
208
+ records.push({ record: validateProcessRecoveryRecord(item.value, input.limits), version: item.version });
209
+ }
210
+ catch (error) {
211
+ if (error instanceof ProcessRecoveryError && error.code === "ERR_PRISM_RECOVERY_LIMIT")
212
+ throw error;
213
+ void error; // Corrupt/foreign records fail closed: dropped, never recovered, never fabricated.
214
+ }
215
+ }
216
+ return { records };
217
+ }
218
+ /** Load one recovery record by session id (null when absent or corrupt). */
219
+ export async function loadProcessRecoveryRecord(input) {
220
+ const item = await input.checkpoints.loadCheckpoint({
221
+ namespace: PROCESS_RECOVERY_NAMESPACE,
222
+ key: input.id,
223
+ ...input.ownership,
224
+ signal: input.signal,
225
+ });
226
+ if (!item)
227
+ return null;
228
+ try {
229
+ return { record: validateProcessRecoveryRecord(item.value, input.limits), version: item.version };
230
+ }
231
+ catch {
232
+ return null; // corrupt record fails closed
233
+ }
234
+ }
235
+ /** CAS save of one recovery record. Fence/version conflicts throw ERR_PRISM_RECOVERY_FENCE. */
236
+ export async function saveProcessRecoveryRecord(input) {
237
+ try {
238
+ const saved = await input.checkpoints.saveCheckpoint({
239
+ namespace: PROCESS_RECOVERY_NAMESPACE,
240
+ key: input.record.id,
241
+ category: PROCESS_RECOVERY_CATEGORY,
242
+ value: input.record,
243
+ version: input.version,
244
+ expectedVersion: input.expectedVersion,
245
+ fencingToken: input.record.fencingToken,
246
+ ...input.ownership,
247
+ signal: input.signal,
248
+ });
249
+ return { version: saved.version };
250
+ }
251
+ catch {
252
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_FENCE", "recovery record CAS or fencing conflict");
253
+ }
254
+ }
255
+ /** Delete one recovery record (false when absent). */
256
+ export async function deleteProcessRecoveryRecord(input) {
257
+ return input.checkpoints.deleteCheckpoint({
258
+ namespace: PROCESS_RECOVERY_NAMESPACE,
259
+ key: input.id,
260
+ ...input.ownership,
261
+ signal: input.signal,
262
+ });
263
+ }
264
+ /** Acquire the per-record recovery lease; null => another replica owns or is recovering the record. */
265
+ export async function acquireRecordLease(input) {
266
+ try {
267
+ return await input.leases.tryAcquireLease({
268
+ namespace: PROCESS_RECOVERY_LEASE_NAMESPACE,
269
+ key: `recover:${input.id}`,
270
+ ownerId: input.ownerId,
271
+ ttlMs: input.ttlMs,
272
+ ...input.ownership,
273
+ signal: input.signal,
274
+ });
275
+ }
276
+ catch (error) {
277
+ if (isOwnershipConflict(error)) {
278
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_OWNERSHIP", "recovery lease ownership mismatch");
279
+ }
280
+ throw error;
281
+ }
282
+ }
283
+ /** Release a recovery lease (best effort; ignores conflicts). */
284
+ export async function releaseRecordLease(input) {
285
+ try {
286
+ await input.leases.releaseLease({
287
+ namespace: PROCESS_RECOVERY_LEASE_NAMESPACE,
288
+ key: `recover:${input.id}`,
289
+ ownerId: input.ownerId,
290
+ token: input.token,
291
+ ...input.ownership,
292
+ signal: input.signal,
293
+ });
294
+ }
295
+ catch {
296
+ // best effort: lease expiry is the backstop
297
+ }
298
+ }
299
+ /** Bounded attach deadline: a backend that does not answer within attachTimeoutMs fails closed. */
300
+ export async function attachWithTimeout(backend, ref, timeoutMs) {
301
+ return await new Promise((resolve, reject) => {
302
+ const timer = setTimeout(() => reject(new ProcessRecoveryError("ERR_PRISM_RECOVERY_TIMEOUT", `recovery attach timed out (${timeoutMs}ms)`)), timeoutMs);
303
+ Promise.resolve()
304
+ .then(() => backend.attach(ref))
305
+ .then((handle) => {
306
+ clearTimeout(timer);
307
+ resolve(handle);
308
+ }, (error) => {
309
+ clearTimeout(timer);
310
+ reject(error instanceof ProcessRecoveryError
311
+ ? error
312
+ : new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNKNOWN", "recovery attach failed"));
313
+ });
314
+ });
315
+ }
316
+ /** True when a checkpoint load/save failure is an ownership conflict (fail closed as OWNERSHIP). */
317
+ export function isOwnershipConflict(error) {
318
+ return (typeof error === "object" && error !== null && "code" in error && error.code === "ERR_PRISM_LEASE_CONFLICT");
319
+ }
320
+ //# sourceMappingURL=recovery.js.map