@arnilo/prism-coding-agent 0.2.5 → 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.
- package/CHANGELOG.md +16 -0
- package/README.md +10 -0
- package/dist/coding-checkpoint.js +4 -0
- package/dist/diagnostics.d.ts +83 -0
- package/dist/diagnostics.js +179 -0
- package/dist/git.d.ts +27 -6
- package/dist/git.js +58 -1
- package/dist/index.d.ts +13 -4
- package/dist/index.js +13 -2
- package/dist/language/client.d.ts +25 -0
- package/dist/language/client.js +54 -0
- package/dist/language/index.d.ts +1 -1
- package/dist/language/intelligence.js +58 -0
- package/dist/language/types.d.ts +32 -0
- package/dist/limits.d.ts +59 -0
- package/dist/limits.js +59 -0
- package/dist/process/index.d.ts +4 -1
- package/dist/process/index.js +1 -0
- package/dist/process/recovery.d.ts +174 -0
- package/dist/process/recovery.js +320 -0
- package/dist/process/sessions.js +714 -25
- package/dist/process/types.d.ts +128 -4
- package/dist/process/types.js +7 -1
- package/dist/repository/indexed-search.d.ts +121 -0
- package/dist/repository/indexed-search.js +313 -0
- package/dist/repository/types.d.ts +14 -2
- package/dist/repository.d.ts +1 -0
- package/dist/repository.js +1 -0
- package/dist/review.d.ts +150 -0
- package/dist/review.js +222 -0
- package/dist/search.d.ts +3 -1
- package/dist/search.js +42 -7
- package/dist/workspace-lifecycle.d.ts +153 -0
- package/dist/workspace-lifecycle.js +629 -0
- package/package.json +3 -3
|
@@ -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
|