@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.
Files changed (331) hide show
  1. package/CHANGELOG.md +418 -0
  2. package/LICENSE +21 -0
  3. package/README.md +44 -2
  4. package/browser/README.md +98 -0
  5. package/browser/skills/browser-automation/SKILL.md +14 -0
  6. package/browser/src/actions/cdp.ts +131 -0
  7. package/browser/src/actions/dialog.ts +25 -0
  8. package/browser/src/actions/download_release.ts +22 -0
  9. package/browser/src/actions/index.ts +37 -0
  10. package/browser/src/actions/locator.ts +70 -0
  11. package/browser/src/actions/navigate.ts +23 -0
  12. package/browser/src/actions/screenshot.ts +27 -0
  13. package/browser/src/actions/scroll.ts +24 -0
  14. package/browser/src/actions/select_page.ts +21 -0
  15. package/browser/src/actions/types.ts +114 -0
  16. package/browser/src/actions/upload.ts +44 -0
  17. package/browser/src/actions/wait.ts +45 -0
  18. package/browser/src/browser.ts +197 -0
  19. package/browser/src/cdp.ts +178 -0
  20. package/browser/src/checkpoint.ts +189 -0
  21. package/browser/src/config.ts +72 -0
  22. package/browser/src/downloads.ts +253 -0
  23. package/browser/src/errors.ts +31 -0
  24. package/browser/src/evaluate.ts +101 -0
  25. package/browser/src/index.ts +129 -0
  26. package/browser/src/limits.ts +271 -0
  27. package/browser/src/manager.ts +730 -0
  28. package/browser/src/network.ts +263 -0
  29. package/browser/src/observe.ts +178 -0
  30. package/browser/src/policy.ts +133 -0
  31. package/browser/src/screenshot.ts +107 -0
  32. package/browser/src/shared-sandbox.ts +53 -0
  33. package/browser/src/snapshot.ts +99 -0
  34. package/browser/src/targets.ts +165 -0
  35. package/browser/src/tools.ts +480 -0
  36. package/browser/src/types.ts +316 -0
  37. package/browser/src/uploads.ts +174 -0
  38. package/coding/README.md +337 -0
  39. package/coding/src/agent/acp-operations.ts +146 -0
  40. package/coding/src/agent/artifacts.ts +38 -0
  41. package/coding/src/agent/ask-user-decision.ts +623 -0
  42. package/coding/src/agent/atomic-write.ts +23 -0
  43. package/coding/src/agent/bounded-file.ts +21 -0
  44. package/coding/src/agent/checks.ts +301 -0
  45. package/coding/src/agent/coding-checkpoint.ts +828 -0
  46. package/coding/src/agent/delete.ts +237 -0
  47. package/coding/src/agent/diagnostics-tool.ts +163 -0
  48. package/coding/src/agent/diagnostics.ts +262 -0
  49. package/coding/src/agent/edit-diff.ts +382 -0
  50. package/coding/src/agent/edit.ts +322 -0
  51. package/coding/src/agent/effects.ts +102 -0
  52. package/coding/src/agent/env.ts +28 -0
  53. package/coding/src/agent/execution-policy.ts +89 -0
  54. package/coding/src/agent/file-mutation-queue.ts +72 -0
  55. package/coding/src/agent/forge/github.ts +603 -0
  56. package/coding/src/agent/forge/index.ts +16 -0
  57. package/coding/src/agent/forge/types.ts +185 -0
  58. package/coding/src/agent/git-aware-repository.ts +305 -0
  59. package/coding/src/agent/git-exec.ts +326 -0
  60. package/coding/src/agent/git-status.ts +177 -0
  61. package/coding/src/agent/git-tools.ts +360 -0
  62. package/coding/src/agent/git.ts +1074 -0
  63. package/coding/src/agent/glob-match.ts +146 -0
  64. package/coding/src/agent/glob.ts +182 -0
  65. package/coding/src/agent/goal-verify.ts +385 -0
  66. package/coding/src/agent/grep-worker.ts +74 -0
  67. package/coding/src/agent/grep.ts +459 -0
  68. package/coding/src/agent/identity.ts +48 -0
  69. package/coding/src/agent/index.ts +676 -0
  70. package/coding/src/agent/language/client.ts +429 -0
  71. package/coding/src/agent/language/framing.ts +190 -0
  72. package/coding/src/agent/language/index.ts +23 -0
  73. package/coding/src/agent/language/intelligence.ts +682 -0
  74. package/coding/src/agent/language/types.ts +187 -0
  75. package/coding/src/agent/lifecycle-events.ts +121 -0
  76. package/coding/src/agent/lifecycle.ts +166 -0
  77. package/coding/src/agent/limits.ts +214 -0
  78. package/coding/src/agent/move.ts +178 -0
  79. package/coding/src/agent/mutation-path.ts +52 -0
  80. package/coding/src/agent/output-accumulator.ts +304 -0
  81. package/coding/src/agent/path-utils.ts +120 -0
  82. package/coding/src/agent/process/index.ts +58 -0
  83. package/coding/src/agent/process/recovery.ts +508 -0
  84. package/coding/src/agent/process/sessions-host.ts +222 -0
  85. package/coding/src/agent/process/sessions-monitor.ts +141 -0
  86. package/coding/src/agent/process/sessions-recovery.ts +241 -0
  87. package/coding/src/agent/process/sessions-spawn.ts +613 -0
  88. package/coding/src/agent/process/sessions-teardown.ts +281 -0
  89. package/coding/src/agent/process/sessions.ts +30 -0
  90. package/coding/src/agent/process/types.ts +358 -0
  91. package/coding/src/agent/read-path-set.ts +126 -0
  92. package/coding/src/agent/read.ts +713 -0
  93. package/coding/src/agent/repository/glob.ts +147 -0
  94. package/coding/src/agent/repository/indexed-search.ts +480 -0
  95. package/coding/src/agent/repository/list.ts +131 -0
  96. package/coding/src/agent/repository/operations.ts +22 -0
  97. package/coding/src/agent/repository/path.ts +98 -0
  98. package/coding/src/agent/repository/search.ts +277 -0
  99. package/coding/src/agent/repository/types.ts +243 -0
  100. package/coding/src/agent/repository/walk.ts +126 -0
  101. package/coding/src/agent/repository.ts +12 -0
  102. package/coding/src/agent/shell-background.ts +264 -0
  103. package/coding/src/agent/shell-output.ts +75 -0
  104. package/coding/src/agent/shell.ts +526 -0
  105. package/coding/src/agent/todo.ts +399 -0
  106. package/coding/src/agent/truncate.ts +251 -0
  107. package/coding/src/agent/write.ts +170 -0
  108. package/coding/src/ask-user-host.ts +48 -0
  109. package/coding/src/config.ts +278 -0
  110. package/coding/src/hard-deny.ts +97 -0
  111. package/coding/src/index.ts +318 -0
  112. package/coding/src/principal-guard.ts +84 -0
  113. package/coding/src/sandbox.ts +92 -0
  114. package/coding/src/security/approval.ts +144 -0
  115. package/coding/src/security/command-rules.ts +57 -0
  116. package/coding/src/security/docker-cli.ts +222 -0
  117. package/coding/src/security/docker-sandbox.ts +1205 -0
  118. package/coding/src/security/e2b-sandbox.ts +853 -0
  119. package/coding/src/security/egress/dns-pin.ts +88 -0
  120. package/coding/src/security/egress/index.ts +32 -0
  121. package/coding/src/security/egress/limits.ts +57 -0
  122. package/coding/src/security/egress/policy.ts +137 -0
  123. package/coding/src/security/egress/proxy.ts +599 -0
  124. package/coding/src/security/egress/types.ts +30 -0
  125. package/coding/src/security/index.ts +199 -0
  126. package/coding/src/security/native-sandbox.ts +639 -0
  127. package/coding/src/security/path-containment.ts +55 -0
  128. package/coding/src/security/sandbox-coding-operations.ts +392 -0
  129. package/coding/src/security/sandbox-fs-operations.ts +644 -0
  130. package/coding/src/security/sandbox-limits.ts +147 -0
  131. package/coding/src/security/sandbox-tar.ts +308 -0
  132. package/coding/src/security/sandbox.ts +179 -0
  133. package/coding/src/security/semaphore.ts +55 -0
  134. package/coding/src/targets.ts +110 -0
  135. package/coding/src/workspace/diff.ts +78 -0
  136. package/coding/src/workspace/errors.ts +29 -0
  137. package/coding/src/workspace/index.ts +5 -0
  138. package/coding/src/workspace/merge.ts +151 -0
  139. package/coding/src/workspace/routing.ts +76 -0
  140. package/coding/src/workspace/service.ts +584 -0
  141. package/computer-use/README.md +105 -0
  142. package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
  143. package/computer-use/src/classify.ts +53 -0
  144. package/computer-use/src/command.ts +29 -0
  145. package/computer-use/src/config.ts +69 -0
  146. package/computer-use/src/create.ts +170 -0
  147. package/computer-use/src/devices.ts +247 -0
  148. package/computer-use/src/env.ts +28 -0
  149. package/computer-use/src/index.ts +157 -0
  150. package/computer-use/src/skill.ts +21 -0
  151. package/openapi/README.md +97 -0
  152. package/openapi/src/compile.ts +386 -0
  153. package/openapi/src/config.ts +73 -0
  154. package/openapi/src/errors.ts +16 -0
  155. package/openapi/src/execute.ts +241 -0
  156. package/openapi/src/index.ts +124 -0
  157. package/openapi/src/limits.ts +63 -0
  158. package/openapi/src/specs.ts +86 -0
  159. package/openapi/src/tools.ts +60 -0
  160. package/openapi/src/types.ts +61 -0
  161. package/package.json +171 -4
  162. package/research/README.md +72 -0
  163. package/research/skills/research-report/SKILL.md +12 -0
  164. package/research/skills/research-scoping/SKILL.md +10 -0
  165. package/research/skills/research-synthesis/SKILL.md +10 -0
  166. package/research/skills/source-triage/SKILL.md +10 -0
  167. package/research/src/check.ts +71 -0
  168. package/research/src/config.ts +41 -0
  169. package/research/src/index.ts +147 -0
  170. package/research/src/ledger.ts +157 -0
  171. package/research/src/urls.ts +21 -0
  172. package/web/README.md +287 -0
  173. package/web/skills/web-research/SKILL.md +17 -0
  174. package/web/src/brave.ts +66 -0
  175. package/web/src/cache.ts +151 -0
  176. package/web/src/config.ts +210 -0
  177. package/web/src/controls.ts +128 -0
  178. package/web/src/domains.ts +114 -0
  179. package/web/src/exa.ts +60 -0
  180. package/web/src/firecrawl.ts +160 -0
  181. package/web/src/index.ts +387 -0
  182. package/web/src/kagi.ts +73 -0
  183. package/web/src/limits.ts +55 -0
  184. package/web/src/native/convert.ts +84 -0
  185. package/web/src/native/fetch.ts +226 -0
  186. package/web/src/normalize.ts +131 -0
  187. package/web/src/obscura/cdp.ts +201 -0
  188. package/web/src/obscura/classify.ts +35 -0
  189. package/web/src/obscura/cli.ts +254 -0
  190. package/web/src/obscura/errors.ts +8 -0
  191. package/web/src/obscura/index.ts +59 -0
  192. package/web/src/obscura/limits.ts +92 -0
  193. package/web/src/obscura/mcp.ts +104 -0
  194. package/web/src/obscura/playwright.ts +133 -0
  195. package/web/src/obscura/process.ts +235 -0
  196. package/web/src/obscura/search-profile.ts +73 -0
  197. package/web/src/obscura/types.ts +48 -0
  198. package/web/src/obscura/web.ts +419 -0
  199. package/web/src/prompt-fetch.ts +88 -0
  200. package/web/src/render.ts +75 -0
  201. package/web/src/searxng.ts +78 -0
  202. package/web/src/slice.ts +139 -0
  203. package/web/src/tavily.ts +77 -0
  204. package/web/src/tools.ts +182 -0
  205. package/web/src/transport.ts +310 -0
  206. package/web/src/types.ts +199 -0
  207. package/work/README.md +77 -0
  208. package/work/THIRD_PARTY_NOTICES +24 -0
  209. package/work/docling/ocr.py +150 -0
  210. package/work/sandbox/Dockerfile +36 -0
  211. package/work/sandbox/soffice.sh +12 -0
  212. package/work/src/command.ts +29 -0
  213. package/work/src/config.ts +138 -0
  214. package/work/src/connectors/cli.ts +343 -0
  215. package/work/src/connectors/drafts.ts +647 -0
  216. package/work/src/connectors/errors.ts +8 -0
  217. package/work/src/connectors/file-bytes.ts +149 -0
  218. package/work/src/connectors/google-workspace-http.ts +399 -0
  219. package/work/src/connectors/google-workspace.ts +465 -0
  220. package/work/src/connectors/gws-tools.ts +378 -0
  221. package/work/src/connectors/http.ts +123 -0
  222. package/work/src/connectors/idempotency.ts +235 -0
  223. package/work/src/connectors/identity.ts +34 -0
  224. package/work/src/connectors/index.ts +93 -0
  225. package/work/src/connectors/limits.ts +50 -0
  226. package/work/src/connectors/m365-tools.ts +308 -0
  227. package/work/src/connectors/microsoft365-http.ts +428 -0
  228. package/work/src/connectors/microsoft365.ts +402 -0
  229. package/work/src/connectors/normalize.ts +120 -0
  230. package/work/src/connectors/tool-helpers.ts +398 -0
  231. package/work/src/connectors/tools.ts +42 -0
  232. package/work/src/connectors/types.ts +399 -0
  233. package/work/src/diagrams/canonicalize.ts +166 -0
  234. package/work/src/diagrams/caps.ts +52 -0
  235. package/work/src/diagrams/embed.ts +340 -0
  236. package/work/src/diagrams/errors.ts +76 -0
  237. package/work/src/diagrams/index.ts +81 -0
  238. package/work/src/diagrams/messages.ts +150 -0
  239. package/work/src/diagrams/origin.ts +52 -0
  240. package/work/src/diagrams/telemetry.ts +34 -0
  241. package/work/src/diagrams/xml.ts +267 -0
  242. package/work/src/document-extraction/index.ts +563 -0
  243. package/work/src/document-reader/ambient.d.ts +2 -0
  244. package/work/src/document-reader/errors.ts +7 -0
  245. package/work/src/document-reader/index.ts +181 -0
  246. package/work/src/document-reader/mistral-ocr.ts +318 -0
  247. package/work/src/document-reader/parsers.ts +171 -0
  248. package/work/src/documents/caps.ts +135 -0
  249. package/work/src/documents/diff.ts +269 -0
  250. package/work/src/documents/errors.ts +44 -0
  251. package/work/src/documents/fidelity.ts +98 -0
  252. package/work/src/documents/generate.ts +96 -0
  253. package/work/src/documents/hash.ts +8 -0
  254. package/work/src/documents/index.ts +14 -0
  255. package/work/src/documents/model-schema.ts +639 -0
  256. package/work/src/documents/parse.ts +252 -0
  257. package/work/src/documents/patch-history.ts +81 -0
  258. package/work/src/documents/patch.ts +476 -0
  259. package/work/src/documents/preview-html.ts +245 -0
  260. package/work/src/documents/preview.ts +227 -0
  261. package/work/src/documents/telemetry.ts +36 -0
  262. package/work/src/documents/translate/docx.ts +292 -0
  263. package/work/src/documents/translate/pptx.ts +304 -0
  264. package/work/src/documents/translate/xlsx.ts +137 -0
  265. package/work/src/documents/types.ts +188 -0
  266. package/work/src/index.ts +311 -0
  267. package/work/src/runtime/worker-pool-entry.ts +102 -0
  268. package/work/src/runtime/worker-pool.ts +267 -0
  269. package/work/src/sandbox/composition.ts +212 -0
  270. package/work/src/sandbox/image.ts +6 -0
  271. package/work/src/sandbox/index.ts +14 -0
  272. package/work/src/sheets/caps.ts +68 -0
  273. package/work/src/sheets/csv.ts +351 -0
  274. package/work/src/sheets/decimal.ts +388 -0
  275. package/work/src/sheets/errors.ts +52 -0
  276. package/work/src/sheets/index.ts +62 -0
  277. package/work/src/sheets/inference.ts +378 -0
  278. package/work/src/sheets/telemetry.ts +34 -0
  279. package/work/src/sheets/types.ts +162 -0
  280. package/work/src/sheets/xlsx.ts +388 -0
  281. package/work/src/skills/index.ts +8 -0
  282. package/work/src/skills/load.ts +43 -0
  283. package/work/src/tools/filesystem.ts +21 -0
  284. package/work/src/tools/index.ts +1 -0
  285. package/work/src/tools/local-filesystem.ts +33 -0
  286. package/work/src/tools/office.ts +311 -0
  287. package/work/vendor/hermes-agent/LICENSE +21 -0
  288. package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
  289. package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
  290. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
  291. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
  292. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
  293. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
  294. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
  295. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
  296. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
  297. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
  298. package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
  299. package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
  300. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
  301. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
  302. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
  303. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
  304. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
  305. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
  306. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
  307. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
  308. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
  309. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
  310. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
  311. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
  312. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
  313. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
  314. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
  315. package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
  316. package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
  317. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
  318. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
  319. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
  320. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
  321. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
  322. package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
  323. package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
  324. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
  325. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
  326. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
  327. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
  328. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
  329. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
  330. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
  331. package/work/vendor-lock.json +11 -0
@@ -0,0 +1,58 @@
1
+ // host-selected pty backend contract (plan 026 Task 1): explicit capabilities,
2
+ // bounded geometry/TERM, attach timeout, resize rate limit, backend metadata caps.
3
+ // durable process recovery seam (plan 026 Task 5): versioned recovery records,
4
+ // attach-if-attested recovery backend, ERR_PRISM_RECOVERY_* failures
5
+
6
+ export type {
7
+ ProcessRecoveryBackend,
8
+ ProcessRecoveryErrorCode,
9
+ ProcessRecoveryLimits,
10
+ ProcessRecoveryOutcome,
11
+ ProcessRecoveryRecord,
12
+ ProcessRecoveryRecordReport,
13
+ ProcessRecoveryReport,
14
+ RecoveryRecordPage,
15
+ ResolvedProcessRecoveryLimits,
16
+ } from "./recovery.js";
17
+ export {
18
+ acquireRecordLease,
19
+ attachWithTimeout,
20
+ buildProcessRecoveryRecord,
21
+ deleteProcessRecoveryRecord,
22
+ isOwnershipConflict,
23
+ loadProcessRecoveryRecord,
24
+ loadProcessRecoveryRecords,
25
+ PROCESS_RECOVERY_CATEGORY,
26
+ PROCESS_RECOVERY_LEASE_NAMESPACE,
27
+ PROCESS_RECOVERY_NAMESPACE,
28
+ PROCESS_RECOVERY_SCHEMA_VERSION,
29
+ ProcessRecoveryError,
30
+ releaseRecordLease,
31
+ resolveProcessRecoveryLimits,
32
+ saveProcessRecoveryRecord,
33
+ validateBackendRef,
34
+ validateProcessRecoveryRecord,
35
+ } from "./recovery.js";
36
+ export { createProcessSessions } from "./sessions.js";
37
+ export type {
38
+ CodingProcessEvent,
39
+ CreateProcessSessionsOptions,
40
+ ProcessExitResult,
41
+ ProcessOutputChunk,
42
+ ProcessPtyBackend,
43
+ ProcessPtyHandle,
44
+ ProcessPtyStartRequest,
45
+ ProcessSandboxBackend,
46
+ ProcessSandboxHandle,
47
+ ProcessSandboxStartRequest,
48
+ ProcessSession,
49
+ ProcessSessionLimits,
50
+ ProcessSessionMetadata,
51
+ ProcessSessionState,
52
+ ProcessSessions,
53
+ ProcessStartRequest,
54
+ ProcessTerminalRequest,
55
+ ProcessTerminalResize,
56
+ ResolvedProcessSessionLimits,
57
+ } from "./types.js";
58
+ export { ProcessSessionError, resolveProcessSessionLimits } from "./types.js";
@@ -0,0 +1,508 @@
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 { type OwnershipScope } from "@arnilo/prism";
19
+ import type { CheckpointStore, LeaseRecord, LeaseStore } from "@arnilo/prism-extensions/workflows";
20
+ import {
21
+ DEFAULT_MAX_RECOVERY_ATTACH_TIMEOUT_MS,
22
+ DEFAULT_MAX_RECOVERY_BACKEND_REF_BYTES,
23
+ DEFAULT_MAX_RECOVERY_LEASE_TTL_MS,
24
+ DEFAULT_MAX_RECOVERY_RECORD_BYTES,
25
+ DEFAULT_MAX_RECOVERY_RECORDS,
26
+ HARD_MAX_RECOVERY_ATTACH_TIMEOUT_MS,
27
+ HARD_MAX_RECOVERY_BACKEND_REF_BYTES,
28
+ HARD_MAX_RECOVERY_LEASE_TTL_MS,
29
+ HARD_MAX_RECOVERY_RECORD_BYTES,
30
+ HARD_MAX_RECOVERY_RECORDS,
31
+ validateCodingLimit,
32
+ } from "../limits.js";
33
+ import type { ProcessPtyHandle, ProcessSandboxHandle, ProcessSessionState } from "./types.js";
34
+
35
+ /** Versioned durable namespace for managed-process recovery records (separate from CodingCheckpointMetadata v1). */
36
+ export const PROCESS_RECOVERY_NAMESPACE = "prism.coding-agent.process.v1";
37
+ /** Namespace for per-record recovery leases. */
38
+ export const PROCESS_RECOVERY_LEASE_NAMESPACE = "prism.coding-agent.process.lease.v1";
39
+ export const PROCESS_RECOVERY_SCHEMA_VERSION = 1;
40
+ export const PROCESS_RECOVERY_CATEGORY = "coding-process";
41
+
42
+ export type ProcessRecoveryOutcome = "attached" | "terminal" | "unknown";
43
+
44
+ /** One durable process recovery record. Metadata only — no handles, no output, no secrets. */
45
+ export interface ProcessRecoveryRecord {
46
+ readonly schemaVersion: typeof PROCESS_RECOVERY_SCHEMA_VERSION;
47
+ /** ProcessSessions session id (`proc_<hex>`); also the checkpoint key. */
48
+ readonly id: string;
49
+ readonly owner: string;
50
+ readonly workspace: string;
51
+ readonly command: string;
52
+ readonly args: readonly string[];
53
+ readonly commandFingerprint: string;
54
+ readonly policyDecision: string;
55
+ readonly startedAt: string;
56
+ readonly state: ProcessSessionState;
57
+ readonly exitCode: number | null;
58
+ readonly releaseOnCancel: boolean;
59
+ readonly expiresAt: number;
60
+ /** Opaque non-secret host reattachment ref; absent => no attach possible. */
61
+ readonly backendRef?: string;
62
+ /** Bounded PTY geometry metadata only (never terminal output); resolved columns/rows/term. */
63
+ readonly pty?: { readonly columns: number; readonly rows: number; readonly term: string };
64
+ /** Monotonic lease fencing token from the record's recovery lease. */
65
+ readonly fencingToken: number;
66
+ readonly updatedAt: string;
67
+ }
68
+
69
+ /** Host-attested reattachment capability. `attach` resolves an opaque ref to a live handle or returns null. */
70
+ export interface ProcessRecoveryBackend {
71
+ /**
72
+ * Resolve an opaque non-secret ref to a live process handle. Return null when
73
+ * the ref cannot be attested. Throwing is treated as attach failure and the
74
+ * record becomes unknown; backend error text is never surfaced.
75
+ */
76
+ attach(ref: string): Promise<ProcessPtyHandle | ProcessSandboxHandle | null> | ProcessPtyHandle | ProcessSandboxHandle | null;
77
+ }
78
+
79
+ /** Per-record recovery report entry. */
80
+ export interface ProcessRecoveryRecordReport {
81
+ readonly id: string;
82
+ readonly outcome: ProcessRecoveryOutcome;
83
+ readonly state: ProcessSessionState;
84
+ readonly exitCode: number | null;
85
+ /** Generic failure code when the record could not be attached or transitioned (never backend error text). */
86
+ readonly error?: ProcessRecoveryErrorCode;
87
+ }
88
+
89
+ /** Bounded recover() report. */
90
+ export interface ProcessRecoveryReport {
91
+ readonly records: readonly ProcessRecoveryRecordReport[];
92
+ readonly attached: number;
93
+ readonly terminal: number;
94
+ readonly unknown: number;
95
+ }
96
+
97
+ export type ProcessRecoveryErrorCode =
98
+ | "ERR_PRISM_RECOVERY_UNSUPPORTED"
99
+ | "ERR_PRISM_RECOVERY_LIMIT"
100
+ | "ERR_PRISM_RECOVERY_OWNERSHIP"
101
+ | "ERR_PRISM_RECOVERY_FENCE"
102
+ | "ERR_PRISM_RECOVERY_UNKNOWN"
103
+ | "ERR_PRISM_RECOVERY_UNTRUSTED"
104
+ | "ERR_PRISM_RECOVERY_TIMEOUT";
105
+
106
+ /** Stable typed failures for the recovery seam. */
107
+ export class ProcessRecoveryError extends Error {
108
+ readonly code: ProcessRecoveryErrorCode;
109
+ constructor(code: ProcessRecoveryErrorCode, message: string) {
110
+ super(message);
111
+ this.name = "ProcessRecoveryError";
112
+ this.code = code;
113
+ }
114
+ }
115
+
116
+ export interface ProcessRecoveryLimits {
117
+ readonly maxRecords?: number;
118
+ readonly leaseTtlMs?: number;
119
+ readonly attachTimeoutMs?: number;
120
+ readonly backendRefBytes?: number;
121
+ readonly recordBytes?: number;
122
+ }
123
+
124
+ export interface ResolvedProcessRecoveryLimits {
125
+ readonly maxRecords: number;
126
+ readonly leaseTtlMs: number;
127
+ readonly attachTimeoutMs: number;
128
+ readonly backendRefBytes: number;
129
+ readonly recordBytes: number;
130
+ }
131
+
132
+ export function resolveProcessRecoveryLimits(limits?: ProcessRecoveryLimits): ResolvedProcessRecoveryLimits {
133
+ return {
134
+ maxRecords: validateCodingLimit("maxRecords", limits?.maxRecords ?? DEFAULT_MAX_RECOVERY_RECORDS, HARD_MAX_RECOVERY_RECORDS),
135
+ leaseTtlMs: validateCodingLimit("leaseTtlMs", limits?.leaseTtlMs ?? DEFAULT_MAX_RECOVERY_LEASE_TTL_MS, HARD_MAX_RECOVERY_LEASE_TTL_MS),
136
+ attachTimeoutMs: validateCodingLimit(
137
+ "attachTimeoutMs",
138
+ limits?.attachTimeoutMs ?? DEFAULT_MAX_RECOVERY_ATTACH_TIMEOUT_MS,
139
+ HARD_MAX_RECOVERY_ATTACH_TIMEOUT_MS,
140
+ ),
141
+ backendRefBytes: validateCodingLimit(
142
+ "backendRefBytes",
143
+ limits?.backendRefBytes ?? DEFAULT_MAX_RECOVERY_BACKEND_REF_BYTES,
144
+ HARD_MAX_RECOVERY_BACKEND_REF_BYTES,
145
+ ),
146
+ recordBytes: validateCodingLimit(
147
+ "recordBytes",
148
+ limits?.recordBytes ?? DEFAULT_MAX_RECOVERY_RECORD_BYTES,
149
+ HARD_MAX_RECOVERY_RECORD_BYTES,
150
+ ),
151
+ };
152
+ }
153
+
154
+ const STATE_SET: ReadonlySet<string> = new Set(["starting", "running", "exited", "killed", "released", "expired", "unknown"]);
155
+
156
+ /** Bounded validation of one recovery record. Corrupt/oversized/foreign records fail closed. */
157
+ export function validateProcessRecoveryRecord(record: unknown, limits: ResolvedProcessRecoveryLimits): ProcessRecoveryRecord {
158
+ if (typeof record !== "object" || record === null) {
159
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "recovery record is not an object");
160
+ }
161
+ const value = record as Record<string, unknown>;
162
+ if (value.schemaVersion !== PROCESS_RECOVERY_SCHEMA_VERSION) {
163
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", `unsupported recovery record schema version`);
164
+ }
165
+ const id = value.id;
166
+ if (typeof id !== "string" || !/^proc_[0-9a-f]{16}$/.test(id)) {
167
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery record id");
168
+ }
169
+ for (const forbidden of ["env", "token", "credential", "secret", "output", "stdout", "stderr", "commandOutput", "rawOutput"]) {
170
+ if (forbidden in value) {
171
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", `forbidden field ${forbidden} in recovery record`);
172
+ }
173
+ }
174
+ const { owner, workspace, command, args, commandFingerprint, policyDecision, startedAt, state, releaseOnCancel, updatedAt } = value;
175
+ const exitCode = value.exitCode as number | null | undefined;
176
+ const expiresAt = value.expiresAt as number | undefined;
177
+ const fencingToken = value.fencingToken as number | undefined;
178
+ if (typeof owner !== "string" || owner.length === 0 || Buffer.byteLength(owner, "utf8") > 512) {
179
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery owner");
180
+ }
181
+ if (typeof workspace !== "string" || !isAbsolute(workspace) || Buffer.byteLength(workspace, "utf8") > 4096) {
182
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery workspace");
183
+ }
184
+ if (typeof command !== "string" || command.length === 0 || Buffer.byteLength(command, "utf8") > 4096) {
185
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery command");
186
+ }
187
+ if (!Array.isArray(args) || args.length > 64) {
188
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery args");
189
+ }
190
+ for (const arg of args) {
191
+ if (typeof arg !== "string" || Buffer.byteLength(arg, "utf8") > 4096) {
192
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery arg");
193
+ }
194
+ }
195
+ if (typeof commandFingerprint !== "string" || !/^[0-9a-f]{64}$/.test(commandFingerprint)) {
196
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery command fingerprint");
197
+ }
198
+ if (typeof policyDecision !== "string" || Buffer.byteLength(policyDecision, "utf8") > 512) {
199
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery policy decision");
200
+ }
201
+ if (typeof startedAt !== "string" || Number.isNaN(Date.parse(startedAt))) {
202
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery startedAt");
203
+ }
204
+ if (typeof state !== "string" || !STATE_SET.has(state)) {
205
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery state");
206
+ }
207
+ if (exitCode !== null && exitCode !== undefined && (!Number.isSafeInteger(exitCode) || exitCode < 0)) {
208
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery exitCode");
209
+ }
210
+ if (exitCode === undefined) {
211
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery exitCode");
212
+ }
213
+ if (typeof releaseOnCancel !== "boolean") {
214
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery releaseOnCancel");
215
+ }
216
+ if (typeof expiresAt !== "number" || !Number.isSafeInteger(expiresAt) || expiresAt < 0) {
217
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery expiresAt");
218
+ }
219
+ if (typeof fencingToken !== "number" || !Number.isSafeInteger(fencingToken) || fencingToken < 0) {
220
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery fencingToken");
221
+ }
222
+ if (typeof updatedAt !== "string" || Number.isNaN(Date.parse(updatedAt))) {
223
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery updatedAt");
224
+ }
225
+ let backendRef: string | undefined;
226
+ if (value.backendRef !== undefined) {
227
+ backendRef = validateBackendRef(value.backendRef, limits);
228
+ }
229
+ let pty: { columns: number; rows: number; term: string } | undefined;
230
+ if (value.pty !== undefined) {
231
+ const raw = value.pty as Record<string, unknown>;
232
+ const columns = raw.columns as number | undefined;
233
+ const rows = raw.rows as number | undefined;
234
+ const term = raw.term as string | undefined;
235
+ if (
236
+ typeof columns !== "number" ||
237
+ !Number.isSafeInteger(columns) ||
238
+ columns < 1 ||
239
+ columns > 500 ||
240
+ typeof rows !== "number" ||
241
+ !Number.isSafeInteger(rows) ||
242
+ rows < 1 ||
243
+ rows > 200
244
+ ) {
245
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery pty geometry");
246
+ }
247
+ if (typeof term !== "string" || Buffer.byteLength(term, "utf8") > 256) {
248
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "malformed recovery pty term");
249
+ }
250
+ pty = { columns, rows, term };
251
+ }
252
+ const recordValue: ProcessRecoveryRecord = {
253
+ schemaVersion: PROCESS_RECOVERY_SCHEMA_VERSION,
254
+ id,
255
+ owner,
256
+ workspace,
257
+ command,
258
+ args: [...args],
259
+ commandFingerprint,
260
+ policyDecision,
261
+ startedAt,
262
+ state: state as ProcessSessionState,
263
+ exitCode,
264
+ releaseOnCancel,
265
+ expiresAt,
266
+ ...(backendRef !== undefined ? { backendRef } : {}),
267
+ ...(pty !== undefined ? { pty } : {}),
268
+ fencingToken,
269
+ updatedAt,
270
+ };
271
+ if (Buffer.byteLength(JSON.stringify(recordValue), "utf8") > limits.recordBytes) {
272
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_LIMIT", `recovery record exceeds ${limits.recordBytes} bytes`);
273
+ }
274
+ return recordValue;
275
+ }
276
+
277
+ /** Validate one opaque backend ref (non-secret, bounded, control-free). */
278
+ export function validateBackendRef(ref: unknown, limits: ResolvedProcessRecoveryLimits): string {
279
+ if (typeof ref !== "string" || ref.length === 0 || Buffer.byteLength(ref, "utf8") > limits.backendRefBytes) {
280
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", `invalid backend ref (max ${limits.backendRefBytes} bytes)`);
281
+ }
282
+ if (/[\u0000-\u001f\u007f]/.test(ref)) {
283
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNTRUSTED", "backend ref contains control characters");
284
+ }
285
+ return ref;
286
+ }
287
+
288
+ /** Build a fresh record for an in-memory session (intent or transition). */
289
+ export function buildProcessRecoveryRecord(input: {
290
+ readonly id: string;
291
+ readonly owner: string;
292
+ readonly workspace: string;
293
+ readonly command: string;
294
+ readonly args: readonly string[];
295
+ readonly commandFingerprint: string;
296
+ readonly policyDecision: string;
297
+ readonly startedAt: string;
298
+ readonly state: ProcessSessionState;
299
+ readonly exitCode: number | null;
300
+ readonly releaseOnCancel: boolean;
301
+ readonly expiresAt: number;
302
+ readonly backendRef?: string;
303
+ readonly pty?: { readonly columns: number; readonly rows: number; readonly term: string };
304
+ readonly fencingToken: number;
305
+ readonly updatedAt?: string;
306
+ }): ProcessRecoveryRecord {
307
+ const record: ProcessRecoveryRecord = {
308
+ schemaVersion: PROCESS_RECOVERY_SCHEMA_VERSION,
309
+ id: input.id,
310
+ owner: input.owner,
311
+ workspace: input.workspace,
312
+ command: input.command,
313
+ args: [...input.args],
314
+ commandFingerprint: input.commandFingerprint,
315
+ policyDecision: input.policyDecision,
316
+ startedAt: input.startedAt,
317
+ state: input.state,
318
+ exitCode: input.exitCode,
319
+ releaseOnCancel: input.releaseOnCancel,
320
+ expiresAt: input.expiresAt,
321
+ ...(input.backendRef !== undefined ? { backendRef: input.backendRef } : {}),
322
+ ...(input.pty !== undefined ? { pty: input.pty } : {}),
323
+ fencingToken: input.fencingToken,
324
+ updatedAt: input.updatedAt ?? new Date().toISOString(),
325
+ };
326
+ return record;
327
+ }
328
+
329
+ export interface RecoveryRecordPage {
330
+ readonly records: ReadonlyArray<{ readonly record: ProcessRecoveryRecord; readonly version: number }>;
331
+ }
332
+
333
+ /** Bounded load of recovery records under one ownership scope (O(maxRecords)). */
334
+ export async function loadProcessRecoveryRecords(input: {
335
+ readonly checkpoints: CheckpointStore;
336
+ readonly limits: ResolvedProcessRecoveryLimits;
337
+ readonly ownership?: OwnershipScope;
338
+ readonly signal?: AbortSignal;
339
+ }): Promise<RecoveryRecordPage> {
340
+ const page = await input.checkpoints.listCheckpoints({
341
+ namespace: PROCESS_RECOVERY_NAMESPACE,
342
+ keyPrefix: "proc_",
343
+ category: PROCESS_RECOVERY_CATEGORY,
344
+ limit: input.limits.maxRecords,
345
+ ...input.ownership,
346
+ signal: input.signal,
347
+ });
348
+ const records: Array<{ readonly record: ProcessRecoveryRecord; readonly version: number }> = [];
349
+ for (const item of page.items) {
350
+ try {
351
+ records.push({ record: validateProcessRecoveryRecord(item.value, input.limits), version: item.version });
352
+ } catch (error) {
353
+ if (error instanceof ProcessRecoveryError && error.code === "ERR_PRISM_RECOVERY_LIMIT") throw error;
354
+ void error; // Corrupt/foreign records fail closed: dropped, never recovered, never fabricated.
355
+ }
356
+ }
357
+ return { records };
358
+ }
359
+
360
+ /** Load one recovery record by session id (null when absent or corrupt). */
361
+ export async function loadProcessRecoveryRecord(input: {
362
+ readonly checkpoints: CheckpointStore;
363
+ readonly id: string;
364
+ readonly limits: ResolvedProcessRecoveryLimits;
365
+ readonly ownership?: OwnershipScope;
366
+ readonly signal?: AbortSignal;
367
+ }): Promise<{ readonly record: ProcessRecoveryRecord; readonly version: number } | null> {
368
+ const item = await input.checkpoints.loadCheckpoint({
369
+ namespace: PROCESS_RECOVERY_NAMESPACE,
370
+ key: input.id,
371
+ ...input.ownership,
372
+ signal: input.signal,
373
+ });
374
+ if (!item) return null;
375
+ try {
376
+ return { record: validateProcessRecoveryRecord(item.value, input.limits), version: item.version };
377
+ } catch {
378
+ return null; // corrupt record fails closed
379
+ }
380
+ }
381
+
382
+ /** CAS save of one recovery record. Fence/version conflicts throw ERR_PRISM_RECOVERY_FENCE. */
383
+ export async function saveProcessRecoveryRecord(input: {
384
+ readonly checkpoints: CheckpointStore;
385
+ readonly record: ProcessRecoveryRecord;
386
+ readonly expectedVersion: number;
387
+ readonly version: number;
388
+ readonly ownership?: OwnershipScope;
389
+ readonly signal?: AbortSignal;
390
+ }): Promise<{ readonly version: number }> {
391
+ try {
392
+ const saved = await input.checkpoints.saveCheckpoint({
393
+ namespace: PROCESS_RECOVERY_NAMESPACE,
394
+ key: input.record.id,
395
+ category: PROCESS_RECOVERY_CATEGORY,
396
+ value: input.record,
397
+ version: input.version,
398
+ expectedVersion: input.expectedVersion,
399
+ fencingToken: input.record.fencingToken,
400
+ ...input.ownership,
401
+ signal: input.signal,
402
+ });
403
+ return { version: saved.version };
404
+ } catch {
405
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_FENCE", "recovery record CAS or fencing conflict");
406
+ }
407
+ }
408
+
409
+ /** Delete one recovery record (false when absent). */
410
+ export async function deleteProcessRecoveryRecord(input: {
411
+ readonly checkpoints: CheckpointStore;
412
+ readonly id: string;
413
+ readonly ownership?: OwnershipScope;
414
+ readonly signal?: AbortSignal;
415
+ }): Promise<boolean> {
416
+ return input.checkpoints.deleteCheckpoint({
417
+ namespace: PROCESS_RECOVERY_NAMESPACE,
418
+ key: input.id,
419
+ ...input.ownership,
420
+ signal: input.signal,
421
+ });
422
+ }
423
+
424
+ /** Acquire the per-record recovery lease; null => another replica owns or is recovering the record. */
425
+ export async function acquireRecordLease(input: {
426
+ readonly leases: LeaseStore;
427
+ readonly id: string;
428
+ readonly ownerId: string;
429
+ readonly ttlMs: number;
430
+ readonly ownership?: OwnershipScope;
431
+ readonly signal?: AbortSignal;
432
+ }): Promise<LeaseRecord | null> {
433
+ try {
434
+ return await input.leases.tryAcquireLease({
435
+ namespace: PROCESS_RECOVERY_LEASE_NAMESPACE,
436
+ key: `recover:${input.id}`,
437
+ ownerId: input.ownerId,
438
+ ttlMs: input.ttlMs,
439
+ ...input.ownership,
440
+ signal: input.signal,
441
+ });
442
+ } catch (error) {
443
+ if (isOwnershipConflict(error)) {
444
+ throw new ProcessRecoveryError("ERR_PRISM_RECOVERY_OWNERSHIP", "recovery lease ownership mismatch");
445
+ }
446
+ throw error;
447
+ }
448
+ }
449
+
450
+ /** Release a recovery lease (best effort; ignores conflicts). */
451
+ export async function releaseRecordLease(input: {
452
+ readonly leases: LeaseStore;
453
+ readonly id: string;
454
+ readonly ownerId: string;
455
+ readonly token: string;
456
+ readonly ownership?: OwnershipScope;
457
+ readonly signal?: AbortSignal;
458
+ }): Promise<void> {
459
+ try {
460
+ await input.leases.releaseLease({
461
+ namespace: PROCESS_RECOVERY_LEASE_NAMESPACE,
462
+ key: `recover:${input.id}`,
463
+ ownerId: input.ownerId,
464
+ token: input.token,
465
+ ...input.ownership,
466
+ signal: input.signal,
467
+ });
468
+ } catch {
469
+ // best effort: lease expiry is the backstop
470
+ }
471
+ }
472
+
473
+ /** Bounded attach deadline: a backend that does not answer within attachTimeoutMs fails closed. */
474
+ export async function attachWithTimeout(
475
+ backend: ProcessRecoveryBackend,
476
+ ref: string,
477
+ timeoutMs: number,
478
+ ): Promise<ProcessPtyHandle | ProcessSandboxHandle | null> {
479
+ return await new Promise<ProcessPtyHandle | ProcessSandboxHandle | null>((resolve, reject) => {
480
+ const timer = setTimeout(
481
+ () => reject(new ProcessRecoveryError("ERR_PRISM_RECOVERY_TIMEOUT", `recovery attach timed out (${timeoutMs}ms)`)),
482
+ timeoutMs,
483
+ );
484
+ Promise.resolve()
485
+ .then(() => backend.attach(ref))
486
+ .then(
487
+ (handle) => {
488
+ clearTimeout(timer);
489
+ resolve(handle);
490
+ },
491
+ (error) => {
492
+ clearTimeout(timer);
493
+ reject(
494
+ error instanceof ProcessRecoveryError
495
+ ? error
496
+ : new ProcessRecoveryError("ERR_PRISM_RECOVERY_UNKNOWN", "recovery attach failed"),
497
+ );
498
+ },
499
+ );
500
+ });
501
+ }
502
+
503
+ /** True when a checkpoint load/save failure is an ownership conflict (fail closed as OWNERSHIP). */
504
+ export function isOwnershipConflict(error: unknown): boolean {
505
+ return (
506
+ typeof error === "object" && error !== null && "code" in error && (error as { code?: unknown }).code === "ERR_PRISM_LEASE_CONFLICT"
507
+ );
508
+ }