@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.
@@ -18,6 +18,10 @@ export class LspClient {
18
18
  capabilities = {};
19
19
  /** file URI → latest diagnostics payload from publishDiagnostics */
20
20
  diagnosticsByUri = new Map();
21
+ /** file URI → monotonic document version (didOpen=1, each didChange += 1) */
22
+ documentVersions = new Map();
23
+ /** file URI → last pull-diagnostic resultId (textDocument/diagnostic) */
24
+ pullResultIds = new Map();
21
25
  onUnexpectedExit;
22
26
  constructor(spec, limits, hooks) {
23
27
  this.spec = spec;
@@ -88,6 +92,55 @@ export class LspClient {
88
92
  return;
89
93
  this.write({ jsonrpc: "2.0", method, params });
90
94
  }
95
+ /**
96
+ * Full-content textDocument/didChange (LSP 3.17; protocol-valid, no diff
97
+ * engine needed). Monotonic version: didOpen stamps 1, each didChange
98
+ * increments. No-op when the document was never opened.
99
+ */
100
+ didChange(uri, text) {
101
+ const version = (this.documentVersions.get(uri) ?? 0) + 1;
102
+ this.documentVersions.set(uri, version);
103
+ this.notify("textDocument/didChange", {
104
+ textDocument: { uri, version },
105
+ contentChanges: [{ text }],
106
+ });
107
+ }
108
+ /** true when the server advertises textDocument/diagnostic (pull diagnostics). */
109
+ hasPullDiagnostics() {
110
+ const provider = this.capabilities.diagnosticProvider;
111
+ if (!provider || provider === false)
112
+ return false;
113
+ return true;
114
+ }
115
+ /**
116
+ * Pull diagnostics for one opened document (textDocument/diagnostic) with
117
+ * resultId reuse: `kind: "full"` replaces the set, `kind: "unchanged"`
118
+ * reuses the previous payload. Returns the version at request time so the
119
+ * caller can drop stale responses. Falls back to the push cache when the
120
+ * server has no pull support.
121
+ */
122
+ async pullDiagnostics(uri, signal) {
123
+ const version = this.documentVersions.get(uri) ?? 1;
124
+ if (!this.hasPullDiagnostics()) {
125
+ return { kind: "full", diagnostics: this.diagnosticsByUri.get(uri) ?? [], version };
126
+ }
127
+ const previousResultId = this.pullResultIds.get(uri);
128
+ const result = (await this.request("textDocument/diagnostic", { textDocument: { uri }, ...(previousResultId === undefined ? {} : { previousResultId }) }, signal));
129
+ if (!result || typeof result !== "object") {
130
+ return { kind: "full", diagnostics: this.diagnosticsByUri.get(uri) ?? [], version };
131
+ }
132
+ if (result.kind === "unchanged") {
133
+ const cached = this.diagnosticsByUri.get(uri) ?? [];
134
+ if (typeof result.resultId === "string")
135
+ this.pullResultIds.set(uri, result.resultId);
136
+ return { kind: "unchanged", diagnostics: cached, resultId: result.resultId, version };
137
+ }
138
+ const items = Array.isArray(result.items) ? result.items : [];
139
+ this.diagnosticsByUri.set(uri, items);
140
+ if (typeof result.resultId === "string")
141
+ this.pullResultIds.set(uri, result.resultId);
142
+ return { kind: "full", diagnostics: items, resultId: result.resultId, version };
143
+ }
91
144
  hasCapability(key) {
92
145
  return this.capabilities[key] !== undefined && this.capabilities[key] !== false;
93
146
  }
@@ -177,6 +230,7 @@ export class LspClient {
177
230
  textDocument: {
178
231
  hover: { contentFormat: ["plaintext", "markdown"] },
179
232
  publishDiagnostics: {},
233
+ diagnostic: { dynamicRegistration: false },
180
234
  },
181
235
  },
182
236
  workspaceFolders: [{ uri: this.spec.rootUri, name: "workspace" }],
@@ -1,4 +1,4 @@
1
1
  export { encodeLspFrame, LspFrameError, LspFrameReader } from "./framing.js";
2
2
  export { LspClient } from "./client.js";
3
3
  export { applyTextEdits, createLanguageIntelligence, LanguageIntelligenceError, resolveLanguageIntelligenceLimits, } from "./intelligence.js";
4
- export type { CreateLanguageIntelligenceOptions, LanguageDiagnostic, LanguageIntelligence, LanguageIntelligenceLimits, LanguageLocation, LanguageServerSpec, LanguageSymbol, LanguageTextEdit, LanguageWorkspaceEdit, } from "./types.js";
4
+ export type { CreateLanguageIntelligenceOptions, LanguageDiagnostic, LanguageDiagnosticDeltaRequest, LanguageDiagnosticDeltaResult, LanguageFileDiagnostics, LanguageIntelligence, LanguageIntelligenceLimits, LanguageLocation, LanguageServerSpec, LanguageSymbol, LanguageTextEdit, LanguageWorkspaceEdit, } from "./types.js";
@@ -7,6 +7,7 @@ import { extname, isAbsolute, relative, resolve } from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
  import { assertExecutionAllowed, ExecutionDeniedError } from "@arnilo/prism";
9
9
  import { atomicWriteUtf8File } from "../atomic-write.js";
10
+ import { diagnosticDelta } from "../diagnostics.js";
10
11
  import { withFileMutationQueue } from "../file-mutation-queue.js";
11
12
  import { resolveContainedMutationPath } from "../mutation-path.js";
12
13
  import { LspClient } from "./client.js";
@@ -126,6 +127,7 @@ export function createLanguageIntelligence(options) {
126
127
  client.notify("textDocument/didOpen", {
127
128
  textDocument: { uri, languageId, version: 1, text },
128
129
  });
130
+ client.documentVersions.set(uri, 1);
129
131
  set.add(uri);
130
132
  }
131
133
  async function ensureAllStarted(signal) {
@@ -212,6 +214,62 @@ export function createLanguageIntelligence(options) {
212
214
  const text = hoverToText(contents);
213
215
  return text === undefined ? undefined : { text };
214
216
  },
217
+ async syncDocument(file, opts) {
218
+ assertNotDisposed();
219
+ const { client, uri, abs } = await clientForFile(file, opts?.signal);
220
+ const text = await readFile(abs, "utf8");
221
+ if (opts?.signal?.aborted) {
222
+ throw new LanguageIntelligenceError("ERR_PRISM_LSP_TIMEOUT", "syncDocument aborted");
223
+ }
224
+ client.didChange(uri, text);
225
+ const version = client.documentVersions.get(uri) ?? 1;
226
+ return { version };
227
+ },
228
+ async diagnosticDelta(request, opts) {
229
+ assertNotDisposed();
230
+ if (!Array.isArray(request.files) || request.files.length === 0) {
231
+ throw new LanguageIntelligenceError("ERR_PRISM_LSP_UNSUPPORTED", "diagnosticDelta requires a non-empty files list");
232
+ }
233
+ if (request.files.length > limits.maxResultsPerQuery) {
234
+ throw new LanguageIntelligenceError("ERR_PRISM_LSP_LIMIT", `diagnosticDelta files exceed ${limits.maxResultsPerQuery}`);
235
+ }
236
+ const previous = request.previous ?? {};
237
+ const files = {};
238
+ let latestGeneration = 0;
239
+ for (const file of request.files) {
240
+ const { client, uri } = await clientForFile(file, opts?.signal);
241
+ const pulled = await client.pullDiagnostics(uri, opts?.signal);
242
+ // Stale-version guard: the pull result carries the version at request
243
+ // time; if the document advanced meanwhile, drop the response.
244
+ const currentVersion = client.documentVersions.get(uri) ?? 1;
245
+ if (pulled.version < currentVersion)
246
+ continue;
247
+ const generation = currentVersion;
248
+ latestGeneration = Math.max(latestGeneration, generation);
249
+ const prior = previous[file];
250
+ if (prior && prior.generation > generation)
251
+ continue; // stale previous view
252
+ const raw = normalizeDiagnostics(workspaceRoot, uri, pulled.diagnostics, limits.maxDiagnosticsPerFile);
253
+ const stamped = raw.map((diagnostic) => ({
254
+ file: diagnostic.file,
255
+ line: diagnostic.line,
256
+ character: diagnostic.character,
257
+ endLine: diagnostic.endLine,
258
+ endCharacter: diagnostic.endCharacter,
259
+ severity: diagnostic.severity,
260
+ message: diagnostic.message,
261
+ source: diagnostic.source ?? "lsp",
262
+ code: diagnostic.code,
263
+ generation,
264
+ }));
265
+ const delta = diagnosticDelta({
266
+ next: stamped,
267
+ previous: prior?.diagnostics,
268
+ });
269
+ files[file] = delta;
270
+ }
271
+ return { files, generation: latestGeneration };
272
+ },
215
273
  async rename(loc, opts) {
216
274
  if (!loc.newName || typeof loc.newName !== "string") {
217
275
  throw new LanguageIntelligenceError("ERR_PRISM_LSP_UNSUPPORTED", "newName is required");
@@ -96,8 +96,40 @@ export interface LanguageIntelligence {
96
96
  }, opts?: {
97
97
  signal?: AbortSignal;
98
98
  }): Promise<LanguageWorkspaceEdit>;
99
+ /**
100
+ * Re-sync one file after an external edit: full-content didChange with a
101
+ * monotonic version. No-op when no host server handles the file's language
102
+ * (throws ERR_PRISM_LSP_UNSUPPORTED only when servers exist but none match).
103
+ */
104
+ syncDocument(file: string, opts?: {
105
+ signal?: AbortSignal;
106
+ }): Promise<{
107
+ version: number;
108
+ }>;
109
+ /**
110
+ * Bounded push/pull diagnostic refresh for changed files. Generations are
111
+ * document versions; stale-version results never overwrite newer views.
112
+ */
113
+ diagnosticDelta(request: LanguageDiagnosticDeltaRequest, opts?: {
114
+ signal?: AbortSignal;
115
+ }): Promise<LanguageDiagnosticDeltaResult>;
99
116
  dispose(): Promise<void>;
100
117
  }
118
+ export interface LanguageDiagnosticDeltaRequest {
119
+ /** Workspace-relative files to refresh; bounded per request. */
120
+ readonly files: readonly string[];
121
+ /** Prior normalized view per file (generation + diagnostics); optional. */
122
+ readonly previous?: Readonly<Record<string, LanguageFileDiagnostics>>;
123
+ }
124
+ export interface LanguageFileDiagnostics {
125
+ readonly generation: number;
126
+ readonly diagnostics: readonly import("../diagnostics.js").NormalizedDiagnostic[];
127
+ }
128
+ export interface LanguageDiagnosticDeltaResult {
129
+ /** Per-file delta; stale files (previous generation >= new generation) are omitted. */
130
+ readonly files: Record<string, import("../diagnostics.js").DiagnosticDelta>;
131
+ readonly generation: number;
132
+ }
101
133
  export interface CreateLanguageIntelligenceOptions {
102
134
  readonly workspaceRoot: string;
103
135
  readonly servers: Readonly<Record<string, LanguageServerSpec>>;
package/dist/limits.d.ts CHANGED
@@ -122,6 +122,42 @@ export declare const HARD_MAX_PROCESS_OUTPUT_CHUNK_BYTES: number;
122
122
  /** Total output reuses existing accumulator ceilings (64 MiB / 1 GiB). */
123
123
  export declare const DEFAULT_MAX_PROCESS_TOTAL_OUTPUT_BYTES: number;
124
124
  export declare const HARD_MAX_PROCESS_TOTAL_OUTPUT_BYTES: number;
125
+ /** Phase 26 Task 0 freeze: host-selected PTY terminal caps (interactive sessions). */
126
+ export declare const DEFAULT_MAX_TERMINAL_COLUMNS = 120;
127
+ export declare const HARD_MAX_TERMINAL_COLUMNS = 500;
128
+ export declare const DEFAULT_MAX_TERMINAL_ROWS = 40;
129
+ export declare const HARD_MAX_TERMINAL_ROWS = 200;
130
+ export declare const DEFAULT_MAX_TERMINAL_TERM_BYTES = 64;
131
+ export declare const HARD_MAX_TERMINAL_TERM_BYTES = 256;
132
+ export declare const DEFAULT_MAX_TERMINAL_RESIZES_PER_MINUTE = 60;
133
+ export declare const HARD_MAX_TERMINAL_RESIZES_PER_MINUTE = 600;
134
+ export declare const DEFAULT_MAX_PTY_ATTACH_TIMEOUT_MS = 30000;
135
+ export declare const HARD_MAX_PTY_ATTACH_TIMEOUT_MS = 120000;
136
+ export declare const DEFAULT_MAX_PTY_BACKEND_METADATA_BYTES: number;
137
+ export declare const HARD_MAX_PTY_BACKEND_METADATA_BYTES: number;
138
+ /** Phase 26 Task 0 freeze: host index caps (indexed_literal/semantic repo_search). */
139
+ export declare const DEFAULT_MAX_INDEX_UPDATE_FILES = 1000;
140
+ export declare const HARD_MAX_INDEX_UPDATE_FILES = 10000;
141
+ export declare const DEFAULT_MAX_INDEX_UPDATE_BYTES: number;
142
+ export declare const HARD_MAX_INDEX_UPDATE_BYTES: number;
143
+ /** Index result cap reuses the repository results caps (1000 / 10000). */
144
+ export declare const DEFAULT_MAX_INDEX_SNIPPET_BYTES: number;
145
+ export declare const HARD_MAX_INDEX_SNIPPET_BYTES: number;
146
+ export declare const DEFAULT_MAX_INDEX_STALE_MAX_AGE_MS = 60000;
147
+ export declare const HARD_MAX_INDEX_STALE_MAX_AGE_MS = 300000;
148
+ export declare const DEFAULT_MAX_INDEX_QUERY_TIMEOUT_MS = 30000;
149
+ export declare const HARD_MAX_INDEX_QUERY_TIMEOUT_MS = 120000;
150
+ /** Phase 26 Task 0 freeze: patch-review manifest and diagnostic delta caps. */
151
+ export declare const DEFAULT_MAX_REVIEW_REVISIONS = 8;
152
+ export declare const HARD_MAX_REVIEW_REVISIONS = 32;
153
+ export declare const DEFAULT_MAX_REVIEW_DIAGNOSTICS = 500;
154
+ export declare const HARD_MAX_REVIEW_DIAGNOSTICS = 5000;
155
+ export declare const DEFAULT_MAX_REVIEW_MANIFEST_BYTES: number;
156
+ export declare const HARD_MAX_REVIEW_MANIFEST_BYTES: number;
157
+ export declare const DEFAULT_MAX_REVIEW_DELTA_ENTRIES = 2000;
158
+ export declare const HARD_MAX_REVIEW_DELTA_ENTRIES = 10000;
159
+ export declare const DEFAULT_MAX_DIAGNOSTIC_MESSAGE_BYTES: number;
160
+ export declare const HARD_MAX_DIAGNOSTIC_MESSAGE_BYTES: number;
125
161
  /** Forge (GitHub adapter) defaults and hard caps (Phase 9 Task 5). */
126
162
  export declare const DEFAULT_MAX_FORGE_PAGES_PER_OPERATION = 10;
127
163
  export declare const HARD_MAX_FORGE_PAGES_PER_OPERATION = 100;
@@ -133,6 +169,29 @@ export declare const DEFAULT_MAX_FORGE_REQUEST_CONCURRENCY = 4;
133
169
  export declare const HARD_MAX_FORGE_REQUEST_CONCURRENCY = 8;
134
170
  export declare const DEFAULT_MAX_FORGE_REQUEST_TIMEOUT_MS = 30000;
135
171
  export declare const HARD_MAX_FORGE_REQUEST_TIMEOUT_MS = 120000;
172
+ /** Phase 26 Task 0 freeze: coding workspace lifecycle caps (multi-repo/worktree, plan 026 Task 3). */
173
+ export declare const DEFAULT_MAX_WORKSPACE_REPOSITORIES = 4;
174
+ export declare const HARD_MAX_WORKSPACE_REPOSITORIES = 16;
175
+ /** Worktree cap reuses the git worktree caps (4 / 16). */
176
+ export declare const DEFAULT_MAX_WORKSPACE_WORKTREES = 4;
177
+ export declare const HARD_MAX_WORKSPACE_WORKTREES = 16;
178
+ export declare const DEFAULT_MAX_WORKSPACE_RECORD_BYTES: number;
179
+ export declare const HARD_MAX_WORKSPACE_RECORD_BYTES: number;
180
+ export declare const DEFAULT_MAX_WORKSPACE_LEASE_TTL_MS = 30000;
181
+ export declare const HARD_MAX_WORKSPACE_LEASE_TTL_MS = 300000;
182
+ export declare const DEFAULT_MAX_WORKSPACE_CLEANUP_OPERATIONS = 100;
183
+ export declare const HARD_MAX_WORKSPACE_CLEANUP_OPERATIONS = 1000;
184
+ /** Plan 026 Task 5: durable process/ACP recovery caps (frozen in the phase26 manifest). */
185
+ export declare const DEFAULT_MAX_RECOVERY_RECORDS = 32;
186
+ export declare const HARD_MAX_RECOVERY_RECORDS = 128;
187
+ export declare const DEFAULT_MAX_RECOVERY_LEASE_TTL_MS = 30000;
188
+ export declare const HARD_MAX_RECOVERY_LEASE_TTL_MS = 300000;
189
+ export declare const DEFAULT_MAX_RECOVERY_ATTACH_TIMEOUT_MS = 30000;
190
+ export declare const HARD_MAX_RECOVERY_ATTACH_TIMEOUT_MS = 120000;
191
+ export declare const DEFAULT_MAX_RECOVERY_BACKEND_REF_BYTES = 1024;
192
+ export declare const HARD_MAX_RECOVERY_BACKEND_REF_BYTES: number;
193
+ export declare const DEFAULT_MAX_RECOVERY_RECORD_BYTES: number;
194
+ export declare const HARD_MAX_RECOVERY_RECORD_BYTES: number;
136
195
  /** Validate one configurable coding resource limit. Invalid values fail instead of clamping. */
137
196
  export declare function validateCodingLimit(name: string, value: number, hardCap: number): number;
138
197
  /** Validate a non-negative integer limit (0 allowed), still capped. */
package/dist/limits.js CHANGED
@@ -122,6 +122,42 @@ export const HARD_MAX_PROCESS_OUTPUT_CHUNK_BYTES = 1024 * 1024;
122
122
  /** Total output reuses existing accumulator ceilings (64 MiB / 1 GiB). */
123
123
  export const DEFAULT_MAX_PROCESS_TOTAL_OUTPUT_BYTES = DEFAULT_MAX_TOTAL_OUTPUT_BYTES;
124
124
  export const HARD_MAX_PROCESS_TOTAL_OUTPUT_BYTES = HARD_MAX_TOTAL_OUTPUT_BYTES;
125
+ /** Phase 26 Task 0 freeze: host-selected PTY terminal caps (interactive sessions). */
126
+ export const DEFAULT_MAX_TERMINAL_COLUMNS = 120;
127
+ export const HARD_MAX_TERMINAL_COLUMNS = 500;
128
+ export const DEFAULT_MAX_TERMINAL_ROWS = 40;
129
+ export const HARD_MAX_TERMINAL_ROWS = 200;
130
+ export const DEFAULT_MAX_TERMINAL_TERM_BYTES = 64;
131
+ export const HARD_MAX_TERMINAL_TERM_BYTES = 256;
132
+ export const DEFAULT_MAX_TERMINAL_RESIZES_PER_MINUTE = 60;
133
+ export const HARD_MAX_TERMINAL_RESIZES_PER_MINUTE = 600;
134
+ export const DEFAULT_MAX_PTY_ATTACH_TIMEOUT_MS = 30_000;
135
+ export const HARD_MAX_PTY_ATTACH_TIMEOUT_MS = 120_000;
136
+ export const DEFAULT_MAX_PTY_BACKEND_METADATA_BYTES = 4 * 1024;
137
+ export const HARD_MAX_PTY_BACKEND_METADATA_BYTES = 16 * 1024;
138
+ /** Phase 26 Task 0 freeze: host index caps (indexed_literal/semantic repo_search). */
139
+ export const DEFAULT_MAX_INDEX_UPDATE_FILES = 1_000;
140
+ export const HARD_MAX_INDEX_UPDATE_FILES = 10_000;
141
+ export const DEFAULT_MAX_INDEX_UPDATE_BYTES = 16 * 1024 * 1024;
142
+ export const HARD_MAX_INDEX_UPDATE_BYTES = 64 * 1024 * 1024;
143
+ /** Index result cap reuses the repository results caps (1000 / 10000). */
144
+ export const DEFAULT_MAX_INDEX_SNIPPET_BYTES = 4 * 1024;
145
+ export const HARD_MAX_INDEX_SNIPPET_BYTES = 16 * 1024;
146
+ export const DEFAULT_MAX_INDEX_STALE_MAX_AGE_MS = 60_000;
147
+ export const HARD_MAX_INDEX_STALE_MAX_AGE_MS = 300_000;
148
+ export const DEFAULT_MAX_INDEX_QUERY_TIMEOUT_MS = 30_000;
149
+ export const HARD_MAX_INDEX_QUERY_TIMEOUT_MS = 120_000;
150
+ /** Phase 26 Task 0 freeze: patch-review manifest and diagnostic delta caps. */
151
+ export const DEFAULT_MAX_REVIEW_REVISIONS = 8;
152
+ export const HARD_MAX_REVIEW_REVISIONS = 32;
153
+ export const DEFAULT_MAX_REVIEW_DIAGNOSTICS = 500;
154
+ export const HARD_MAX_REVIEW_DIAGNOSTICS = 5_000;
155
+ export const DEFAULT_MAX_REVIEW_MANIFEST_BYTES = 64 * 1024;
156
+ export const HARD_MAX_REVIEW_MANIFEST_BYTES = 256 * 1024;
157
+ export const DEFAULT_MAX_REVIEW_DELTA_ENTRIES = 2_000;
158
+ export const HARD_MAX_REVIEW_DELTA_ENTRIES = 10_000;
159
+ export const DEFAULT_MAX_DIAGNOSTIC_MESSAGE_BYTES = 4 * 1024;
160
+ export const HARD_MAX_DIAGNOSTIC_MESSAGE_BYTES = 16 * 1024;
125
161
  /** Forge (GitHub adapter) defaults and hard caps (Phase 9 Task 5). */
126
162
  export const DEFAULT_MAX_FORGE_PAGES_PER_OPERATION = 10;
127
163
  export const HARD_MAX_FORGE_PAGES_PER_OPERATION = 100;
@@ -133,6 +169,29 @@ export const DEFAULT_MAX_FORGE_REQUEST_CONCURRENCY = 4;
133
169
  export const HARD_MAX_FORGE_REQUEST_CONCURRENCY = 8;
134
170
  export const DEFAULT_MAX_FORGE_REQUEST_TIMEOUT_MS = 30_000;
135
171
  export const HARD_MAX_FORGE_REQUEST_TIMEOUT_MS = 120_000;
172
+ /** Phase 26 Task 0 freeze: coding workspace lifecycle caps (multi-repo/worktree, plan 026 Task 3). */
173
+ export const DEFAULT_MAX_WORKSPACE_REPOSITORIES = 4;
174
+ export const HARD_MAX_WORKSPACE_REPOSITORIES = 16;
175
+ /** Worktree cap reuses the git worktree caps (4 / 16). */
176
+ export const DEFAULT_MAX_WORKSPACE_WORKTREES = DEFAULT_MAX_GIT_WORKTREES;
177
+ export const HARD_MAX_WORKSPACE_WORKTREES = HARD_MAX_GIT_WORKTREES;
178
+ export const DEFAULT_MAX_WORKSPACE_RECORD_BYTES = 64 * 1024;
179
+ export const HARD_MAX_WORKSPACE_RECORD_BYTES = 256 * 1024;
180
+ export const DEFAULT_MAX_WORKSPACE_LEASE_TTL_MS = 30_000;
181
+ export const HARD_MAX_WORKSPACE_LEASE_TTL_MS = 300_000;
182
+ export const DEFAULT_MAX_WORKSPACE_CLEANUP_OPERATIONS = 100;
183
+ export const HARD_MAX_WORKSPACE_CLEANUP_OPERATIONS = 1_000;
184
+ /** Plan 026 Task 5: durable process/ACP recovery caps (frozen in the phase26 manifest). */
185
+ export const DEFAULT_MAX_RECOVERY_RECORDS = 32;
186
+ export const HARD_MAX_RECOVERY_RECORDS = 128;
187
+ export const DEFAULT_MAX_RECOVERY_LEASE_TTL_MS = 30_000;
188
+ export const HARD_MAX_RECOVERY_LEASE_TTL_MS = 300_000;
189
+ export const DEFAULT_MAX_RECOVERY_ATTACH_TIMEOUT_MS = 30_000;
190
+ export const HARD_MAX_RECOVERY_ATTACH_TIMEOUT_MS = 120_000;
191
+ export const DEFAULT_MAX_RECOVERY_BACKEND_REF_BYTES = 1024;
192
+ export const HARD_MAX_RECOVERY_BACKEND_REF_BYTES = 4 * 1024;
193
+ export const DEFAULT_MAX_RECOVERY_RECORD_BYTES = 64 * 1024;
194
+ export const HARD_MAX_RECOVERY_RECORD_BYTES = 256 * 1024;
136
195
  /** Validate one configurable coding resource limit. Invalid values fail instead of clamping. */
137
196
  export function validateCodingLimit(name, value, hardCap) {
138
197
  if (!Number.isSafeInteger(value) || value < 1 || value > hardCap) {
@@ -1,3 +1,6 @@
1
- export type { CodingProcessEvent, CreateProcessSessionsOptions, ProcessExitResult, ProcessOutputChunk, ProcessSandboxBackend, ProcessSandboxHandle, ProcessSandboxStartRequest, ProcessSession, ProcessSessionLimits, ProcessSessionMetadata, ProcessSessions, ProcessSessionState, ProcessStartRequest, ResolvedProcessSessionLimits, } from "./types.js";
1
+ export type { CodingProcessEvent, CreateProcessSessionsOptions, ProcessExitResult, ProcessOutputChunk, ProcessPtyBackend, ProcessPtyHandle, ProcessPtyStartRequest, ProcessSandboxBackend, ProcessSandboxHandle, ProcessSandboxStartRequest, ProcessSession, ProcessSessionLimits, ProcessSessionMetadata, ProcessSessions, ProcessSessionState, ProcessStartRequest, ProcessTerminalRequest, ProcessTerminalResize, ResolvedProcessSessionLimits, } from "./types.js";
2
2
  export { ProcessSessionError, resolveProcessSessionLimits } from "./types.js";
3
3
  export { createProcessSessions } from "./sessions.js";
4
+ export { PROCESS_RECOVERY_CATEGORY, PROCESS_RECOVERY_LEASE_NAMESPACE, PROCESS_RECOVERY_NAMESPACE, PROCESS_RECOVERY_SCHEMA_VERSION, ProcessRecoveryError, acquireRecordLease, attachWithTimeout, buildProcessRecoveryRecord, deleteProcessRecoveryRecord, isOwnershipConflict, loadProcessRecoveryRecord, loadProcessRecoveryRecords, releaseRecordLease, resolveProcessRecoveryLimits, saveProcessRecoveryRecord, validateBackendRef, validateProcessRecoveryRecord, } from "./recovery.js";
5
+ export type { ProcessRecoveryErrorCode, ProcessRecoveryLimits, ProcessRecoveryRecord, ProcessRecoveryRecordReport, ProcessRecoveryReport, ResolvedProcessRecoveryLimits, RecoveryRecordPage, } from "./recovery.js";
6
+ export type { ProcessRecoveryBackend, ProcessRecoveryOutcome } from "./recovery.js";
@@ -1,3 +1,4 @@
1
1
  export { ProcessSessionError, resolveProcessSessionLimits } from "./types.js";
2
2
  export { createProcessSessions } from "./sessions.js";
3
+ export { PROCESS_RECOVERY_CATEGORY, PROCESS_RECOVERY_LEASE_NAMESPACE, PROCESS_RECOVERY_NAMESPACE, PROCESS_RECOVERY_SCHEMA_VERSION, ProcessRecoveryError, acquireRecordLease, attachWithTimeout, buildProcessRecoveryRecord, deleteProcessRecoveryRecord, isOwnershipConflict, loadProcessRecoveryRecord, loadProcessRecoveryRecords, releaseRecordLease, resolveProcessRecoveryLimits, saveProcessRecoveryRecord, validateBackendRef, validateProcessRecoveryRecord, } from "./recovery.js";
3
4
  //# sourceMappingURL=index.js.map
@@ -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;