@phyzess/pappra 0.1.0-beta.13 → 0.1.0-beta.18

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 (357) hide show
  1. package/README.md +134 -15
  2. package/assets/integrations/skills/pappra/SKILL.md +28 -7
  3. package/assets/integrations/skills/pappra/references/collaboration.md +633 -0
  4. package/assets/integrations/skills/pappra/references/environment.md +360 -0
  5. package/assets/integrations/skills/pappra/references/hosts.md +20 -0
  6. package/assets/integrations/skills/pappra/references/operations.md +36 -5
  7. package/dist/adapters/pi/recovery-extension.d.ts +65 -0
  8. package/dist/adapters/pi/recovery-extension.js +238 -0
  9. package/dist/adapters/pi/session-transition-extension.d.ts +30 -0
  10. package/dist/adapters/pi/session-transition-extension.js +265 -0
  11. package/dist/app/automatic-handoff-events.d.ts +26 -0
  12. package/dist/app/automatic-handoff-events.js +134 -0
  13. package/dist/app/bounded-file.d.ts +2 -0
  14. package/dist/app/bounded-file.js +2 -0
  15. package/dist/app/capability-evidence/claude.d.ts +37 -0
  16. package/dist/app/capability-evidence/claude.js +206 -0
  17. package/dist/app/capability-evidence/codex.d.ts +35 -0
  18. package/dist/app/capability-evidence/codex.js +193 -0
  19. package/dist/app/capability-evidence/index.d.ts +33 -0
  20. package/dist/app/capability-evidence/index.js +117 -0
  21. package/dist/app/capability-evidence/pi.d.ts +73 -0
  22. package/dist/app/capability-evidence/pi.js +669 -0
  23. package/dist/app/capability-evidence/shared.d.ts +69 -0
  24. package/dist/app/capability-evidence/shared.js +195 -0
  25. package/dist/app/collab-auto-handoff.d.ts +55 -0
  26. package/dist/app/collab-auto-handoff.js +384 -0
  27. package/dist/app/collab-bindings.d.ts +57 -0
  28. package/dist/app/collab-bindings.js +1006 -0
  29. package/dist/app/collab-bootstrap-transport.d.ts +163 -0
  30. package/dist/app/collab-bootstrap-transport.js +769 -0
  31. package/dist/app/collab-bootstrap.d.ts +137 -0
  32. package/dist/app/collab-bootstrap.js +3455 -0
  33. package/dist/app/collab-handoff.d.ts +12 -0
  34. package/dist/app/collab-handoff.js +511 -0
  35. package/dist/app/collab-messages.d.ts +2 -0
  36. package/dist/app/collab-messages.js +2 -0
  37. package/dist/app/collab-notify.d.ts +156 -0
  38. package/dist/app/collab-notify.js +1208 -0
  39. package/dist/app/collab-preflight.d.ts +34 -0
  40. package/dist/app/collab-preflight.js +96 -0
  41. package/dist/app/collab-recovery-executor.d.ts +161 -0
  42. package/dist/app/collab-recovery-executor.js +1153 -0
  43. package/dist/app/collab-recovery-projection.d.ts +103 -0
  44. package/dist/app/collab-recovery-projection.js +659 -0
  45. package/dist/app/collab-recovery.d.ts +25 -0
  46. package/dist/app/collab-recovery.js +429 -0
  47. package/dist/app/collab-resources.d.ts +16 -0
  48. package/dist/app/collab-resources.js +278 -0
  49. package/dist/app/collab-role-events.d.ts +22 -0
  50. package/dist/app/collab-role-events.js +329 -0
  51. package/dist/app/collab-role-orchestration.d.ts +53 -0
  52. package/dist/app/collab-role-orchestration.js +601 -0
  53. package/dist/app/collab-role-plan.d.ts +81 -0
  54. package/dist/app/collab-role-plan.js +504 -0
  55. package/dist/app/collab-role-repair.d.ts +109 -0
  56. package/dist/app/collab-role-repair.js +821 -0
  57. package/dist/app/collab-rollover.d.ts +263 -0
  58. package/dist/app/collab-rollover.js +1112 -0
  59. package/dist/app/collab-route.d.ts +64 -0
  60. package/dist/app/collab-route.js +240 -0
  61. package/dist/app/collab-session-transition.d.ts +221 -0
  62. package/dist/app/collab-session-transition.js +3334 -0
  63. package/dist/app/collab-state.d.ts +7 -0
  64. package/dist/app/collab-state.js +162 -0
  65. package/dist/app/collab-task-store.d.ts +22 -0
  66. package/dist/app/collab-task-store.js +204 -0
  67. package/dist/app/collab-work-execution.d.ts +127 -0
  68. package/dist/app/collab-work-execution.js +1027 -0
  69. package/dist/app/collab-work-plan.d.ts +87 -0
  70. package/dist/app/collab-work-plan.js +437 -0
  71. package/dist/app/collaboration.d.ts +8 -2
  72. package/dist/app/collaboration.js +101 -12
  73. package/dist/app/doctor.js +108 -5
  74. package/dist/app/host-mcp.js +34 -8
  75. package/dist/app/host-probe.js +4 -4
  76. package/dist/app/init.d.ts +58 -4
  77. package/dist/app/init.js +768 -155
  78. package/dist/app/managed-pi-bootstrap-admission.d.ts +14 -0
  79. package/dist/app/managed-pi-bootstrap-admission.js +104 -0
  80. package/dist/app/managed-pi-bootstrap-refresh.d.ts +10 -0
  81. package/dist/app/managed-pi-bootstrap-refresh.js +78 -0
  82. package/dist/app/managed-pi-delivery.d.ts +8 -0
  83. package/dist/app/managed-pi-delivery.js +110 -0
  84. package/dist/app/managed-pi-handoff-receiver.d.ts +15 -0
  85. package/dist/app/managed-pi-handoff-receiver.js +263 -0
  86. package/dist/app/managed-pi-handoff-reference.d.ts +3 -0
  87. package/dist/app/managed-pi-handoff-reference.js +60 -0
  88. package/dist/app/managed-pi-herdr.d.ts +21 -0
  89. package/dist/app/managed-pi-herdr.js +185 -0
  90. package/dist/app/managed-pi-initial-admission.d.ts +28 -0
  91. package/dist/app/managed-pi-initial-admission.js +416 -0
  92. package/dist/app/managed-pi-initial-launch.d.ts +5 -0
  93. package/dist/app/managed-pi-initial-launch.js +61 -0
  94. package/dist/app/managed-pi-native-observation.d.ts +36 -0
  95. package/dist/app/managed-pi-native-observation.js +191 -0
  96. package/dist/app/managed-pi-readiness.d.ts +29 -0
  97. package/dist/app/managed-pi-readiness.js +288 -0
  98. package/dist/app/managed-pi-repair-stop.d.ts +21 -0
  99. package/dist/app/managed-pi-repair-stop.js +371 -0
  100. package/dist/app/managed-pi-resume-admission.d.ts +32 -0
  101. package/dist/app/managed-pi-resume-admission.js +618 -0
  102. package/dist/app/managed-pi-retirement.d.ts +28 -0
  103. package/dist/app/managed-pi-retirement.js +112 -0
  104. package/dist/app/managed-pi-sdk-control.d.ts +7 -0
  105. package/dist/app/managed-pi-sdk-control.js +169 -0
  106. package/dist/app/managed-pi-sdk-runtime.d.ts +3 -0
  107. package/dist/app/managed-pi-sdk-runtime.js +483 -0
  108. package/dist/app/managed-pi-source-admission.d.ts +14 -0
  109. package/dist/app/managed-pi-source-admission.js +323 -0
  110. package/dist/app/managed-pi-transition-host.d.ts +36 -0
  111. package/dist/app/managed-pi-transition-host.js +292 -0
  112. package/dist/app/managed-pi.d.ts +54 -0
  113. package/dist/app/managed-pi.js +248 -0
  114. package/dist/app/shell-readiness.d.ts +66 -0
  115. package/dist/app/shell-readiness.js +368 -0
  116. package/dist/app/task-pin.d.ts +2 -23
  117. package/dist/app/task-pin.js +2 -113
  118. package/dist/cli/commands/collaboration-binding.d.ts +13 -0
  119. package/dist/cli/commands/collaboration-binding.js +118 -0
  120. package/dist/cli/commands/collaboration-bootstrap.d.ts +10 -0
  121. package/dist/cli/commands/collaboration-bootstrap.js +84 -0
  122. package/dist/cli/commands/collaboration-capabilities.d.ts +13 -0
  123. package/dist/cli/commands/collaboration-capabilities.js +127 -0
  124. package/dist/cli/commands/collaboration-handoff.d.ts +13 -0
  125. package/dist/cli/commands/collaboration-handoff.js +102 -0
  126. package/dist/cli/commands/collaboration-message.d.ts +17 -0
  127. package/dist/cli/commands/collaboration-message.js +177 -0
  128. package/dist/cli/commands/collaboration-plan.d.ts +8 -0
  129. package/dist/cli/commands/collaboration-plan.js +108 -0
  130. package/dist/cli/commands/collaboration-preflight.d.ts +10 -0
  131. package/dist/cli/commands/collaboration-preflight.js +167 -0
  132. package/dist/cli/commands/collaboration-recovery-run.d.ts +46 -0
  133. package/dist/cli/commands/collaboration-recovery-run.js +224 -0
  134. package/dist/cli/commands/collaboration-recovery.d.ts +11 -0
  135. package/dist/cli/commands/collaboration-recovery.js +104 -0
  136. package/dist/cli/commands/collaboration-resources.d.ts +11 -0
  137. package/dist/cli/commands/collaboration-resources.js +96 -0
  138. package/dist/cli/commands/collaboration-role-plan.d.ts +9 -0
  139. package/dist/cli/commands/collaboration-role-plan.js +69 -0
  140. package/dist/cli/commands/collaboration-role-run.d.ts +11 -0
  141. package/dist/cli/commands/collaboration-role-run.js +128 -0
  142. package/dist/cli/commands/collaboration-rollover.d.ts +10 -0
  143. package/dist/cli/commands/collaboration-rollover.js +103 -0
  144. package/dist/cli/commands/collaboration-state.d.ts +10 -0
  145. package/dist/cli/commands/collaboration-state.js +96 -0
  146. package/dist/cli/commands/collaboration-transition.d.ts +10 -0
  147. package/dist/cli/commands/collaboration-transition.js +75 -0
  148. package/dist/cli/commands/collaboration-work-run.d.ts +34 -0
  149. package/dist/cli/commands/collaboration-work-run.js +178 -0
  150. package/dist/cli/commands/collaboration.d.ts +4 -0
  151. package/dist/cli/commands/collaboration.js +39 -4
  152. package/dist/cli/commands/continuity.js +2 -6
  153. package/dist/cli/commands/docs.d.ts +2 -0
  154. package/dist/cli/commands/docs.js +89 -15
  155. package/dist/cli/commands/host.d.ts +7 -1
  156. package/dist/cli/commands/host.js +122 -7
  157. package/dist/cli/commands/init.d.ts +20 -3
  158. package/dist/cli/commands/init.js +897 -232
  159. package/dist/cli/commands/mcp.d.ts +1 -0
  160. package/dist/cli/commands/mcp.js +11 -5
  161. package/dist/cli/commands/repair.d.ts +12 -0
  162. package/dist/cli/commands/repair.js +41 -0
  163. package/dist/cli/commands/runtime.d.ts +1 -1
  164. package/dist/cli/commands/runtime.js +139 -38
  165. package/dist/cli/commands/session.d.ts +7 -0
  166. package/dist/cli/commands/session.js +82 -0
  167. package/dist/cli/commands/task.js +19 -5
  168. package/dist/cli/run.js +866 -24
  169. package/dist/code/identity.d.ts +7 -0
  170. package/dist/code/identity.js +7 -3
  171. package/dist/code/registration.js +7 -0
  172. package/dist/code/service.d.ts +9 -0
  173. package/dist/code/service.js +16 -3
  174. package/dist/code/text.js +19 -7
  175. package/dist/collab/admission.d.ts +91 -66
  176. package/dist/collab/admission.js +21 -6
  177. package/dist/collab/canonical.d.ts +1 -1
  178. package/dist/collab/canonical.js +44 -3
  179. package/dist/collab/envelope.d.ts +2 -2
  180. package/dist/collab/envelope.js +12 -12
  181. package/dist/collab/first-work.d.ts +3 -0
  182. package/dist/collab/first-work.js +15 -0
  183. package/dist/collab/handoff.d.ts +16 -0
  184. package/dist/collab/handoff.js +39 -5
  185. package/dist/collab/index.d.ts +97 -1
  186. package/dist/collab/index.js +9 -1
  187. package/dist/collab/messages.js +126 -3
  188. package/dist/collab/persisted-messages.d.ts +123 -0
  189. package/dist/collab/persisted-messages.js +496 -0
  190. package/dist/collab/preflight.d.ts +1 -1
  191. package/dist/collab/resources.d.ts +13 -3
  192. package/dist/collab/resources.js +37 -6
  193. package/dist/collab/rollover.d.ts +311 -0
  194. package/dist/collab/rollover.js +1024 -0
  195. package/dist/collab/store.d.ts +1 -0
  196. package/dist/collab/store.js +1 -4
  197. package/dist/collab/types.d.ts +2 -354
  198. package/dist/collab/validation.d.ts +2 -10
  199. package/dist/collab/validation.js +2 -303
  200. package/dist/collab/work-plan.d.ts +105 -0
  201. package/dist/collab/work-plan.js +843 -0
  202. package/dist/continuity/context.d.ts +11 -2
  203. package/dist/continuity/context.js +100 -13
  204. package/dist/continuity/handoff-bundle.d.ts +296 -0
  205. package/dist/continuity/handoff-bundle.js +477 -0
  206. package/dist/continuity/index.d.ts +2 -0
  207. package/dist/continuity/index.js +1 -0
  208. package/dist/continuity/paths.js +10 -2
  209. package/dist/continuity/service.js +91 -15
  210. package/dist/continuity/task-pin.d.ts +24 -0
  211. package/dist/continuity/task-pin.js +590 -0
  212. package/dist/doctor.d.ts +1 -0
  213. package/dist/doctor.js +6 -0
  214. package/dist/experimental/index.d.ts +13 -0
  215. package/dist/experimental/index.js +25 -0
  216. package/dist/experimental/pi.d.ts +4 -0
  217. package/dist/experimental/pi.js +12 -0
  218. package/dist/hosts/claude/native.js +1 -1
  219. package/dist/hosts/codex/native.js +1 -1
  220. package/dist/hosts/codex/protocol.js +1 -1
  221. package/dist/hosts/integration/bootstrap-config.d.ts +58 -0
  222. package/dist/hosts/integration/bootstrap-config.js +254 -0
  223. package/dist/hosts/integration/bundle-export.d.ts +44 -0
  224. package/dist/hosts/integration/bundle-export.js +366 -3
  225. package/dist/hosts/integration/bundle.js +3 -0
  226. package/dist/hosts/integration/hook.js +36 -9
  227. package/dist/hosts/integration/pappra-bootstrap-extension.mjs +80 -0
  228. package/dist/hosts/integration/pappra-extension-runtime.mjs +59 -24631
  229. package/dist/hosts/integration/pappra-hook.mjs +61 -24749
  230. package/dist/hosts/integration/pappra-recovery-extension.mjs +259 -0
  231. package/dist/hosts/integration/pappra-session-transition-extension.mjs +276 -0
  232. package/dist/hosts/integration/pi-bootstrap-extension.d.ts +36 -0
  233. package/dist/hosts/integration/pi-bootstrap-extension.js +101 -0
  234. package/dist/hosts/integration/pi-bootstrap-guard.d.ts +125 -0
  235. package/dist/hosts/integration/pi-bootstrap-guard.js +438 -0
  236. package/dist/hosts/integration/pi-bootstrap-runtime.d.ts +63 -0
  237. package/dist/hosts/integration/pi-bootstrap-runtime.js +223 -0
  238. package/dist/hosts/integration/pi-native-events.d.ts +112 -0
  239. package/dist/hosts/integration/pi-native-events.js +348 -0
  240. package/dist/hosts/integration/pi-rollover-extension.d.ts +166 -0
  241. package/dist/hosts/integration/pi-rollover-extension.js +431 -0
  242. package/dist/hosts/integration/recovery-config.d.ts +48 -0
  243. package/dist/hosts/integration/recovery-config.js +117 -0
  244. package/dist/hosts/integration/service.d.ts +6 -1
  245. package/dist/hosts/integration/service.js +485 -35
  246. package/dist/hosts/integration/support.d.ts +7 -0
  247. package/dist/hosts/integration/support.js +9 -1
  248. package/dist/hosts/integration/templates.d.ts +3 -0
  249. package/dist/hosts/integration/templates.js +26 -3
  250. package/dist/hosts/pi/managed-host-identity.d.ts +2 -0
  251. package/dist/hosts/pi/managed-host-identity.js +40 -0
  252. package/dist/hosts/pi/managed-provider-diagnostics.d.ts +6 -0
  253. package/dist/hosts/pi/managed-provider-diagnostics.js +184 -0
  254. package/dist/hosts/pi/managed-sdk-session.d.ts +335 -0
  255. package/dist/hosts/pi/managed-sdk-session.js +1639 -0
  256. package/dist/hosts/pi/managed-session-extension.d.ts +20 -0
  257. package/dist/hosts/pi/managed-session-extension.js +84 -0
  258. package/dist/hosts/pi/managed-session.d.ts +177 -0
  259. package/dist/hosts/pi/managed-session.js +1557 -0
  260. package/dist/hosts/pi/protocol.js +1 -1
  261. package/dist/hosts/pi/query-result.js +1 -1
  262. package/dist/hosts/pi/resume-capability.d.ts +41 -0
  263. package/dist/hosts/pi/resume-capability.js +33 -0
  264. package/dist/hosts/pi/rpc.d.ts +182 -134
  265. package/dist/hosts/pi/rpc.js +230 -37
  266. package/dist/hosts/pilot.d.ts +97 -74
  267. package/dist/hosts/pilot.js +5 -1
  268. package/dist/hosts/version.d.ts +3 -0
  269. package/dist/hosts/version.js +18 -0
  270. package/dist/index.d.ts +0 -1
  271. package/dist/index.js +0 -1
  272. package/dist/infra/bounded-file.d.ts +19 -0
  273. package/dist/infra/bounded-file.js +66 -0
  274. package/dist/infra/fs.js +10 -2
  275. package/dist/infra/git.d.ts +29 -0
  276. package/dist/infra/git.js +45 -9
  277. package/dist/infra/json-property.d.ts +2 -0
  278. package/dist/infra/json-property.js +69 -0
  279. package/dist/infra/json.d.ts +4 -0
  280. package/dist/infra/json.js +24 -0
  281. package/dist/infra/owned-process-group.js +1 -1
  282. package/dist/infra/private-lock.d.ts +16 -0
  283. package/dist/infra/private-lock.js +148 -0
  284. package/dist/infra/private-state.d.ts +7 -0
  285. package/dist/infra/private-state.js +146 -0
  286. package/dist/infra/process-identity.d.ts +12 -0
  287. package/dist/infra/process-identity.js +142 -0
  288. package/dist/infra/validation.d.ts +5 -0
  289. package/dist/infra/validation.js +15 -0
  290. package/dist/init.d.ts +1 -0
  291. package/dist/init.js +6 -0
  292. package/dist/install/apply.d.ts +9 -1
  293. package/dist/install/apply.js +176 -12
  294. package/dist/install/dependencies.d.ts +38 -0
  295. package/dist/install/dependencies.js +282 -0
  296. package/dist/install/ownership.js +30 -1
  297. package/dist/install/references.d.ts +31 -0
  298. package/dist/install/references.js +208 -0
  299. package/dist/install/repair.d.ts +37 -0
  300. package/dist/install/repair.js +798 -0
  301. package/dist/install/reset-local.d.ts +16 -0
  302. package/dist/install/reset-local.js +304 -0
  303. package/dist/install/rollback.d.ts +3 -0
  304. package/dist/install/rollback.js +9 -3
  305. package/dist/install/runtime.d.ts +10 -6
  306. package/dist/install/runtime.js +299 -59
  307. package/dist/install/source-fingerprint.d.ts +2 -0
  308. package/dist/install/source-fingerprint.js +103 -0
  309. package/dist/knowledge/catalog.d.ts +1 -0
  310. package/dist/knowledge/catalog.js +143 -10
  311. package/dist/knowledge/journal.js +17 -7
  312. package/dist/knowledge/paths.js +2 -1
  313. package/dist/knowledge/refs.js +4 -0
  314. package/dist/knowledge/register-batch.d.ts +4 -0
  315. package/dist/knowledge/register-batch.js +225 -0
  316. package/dist/knowledge/relocate.d.ts +5 -0
  317. package/dist/knowledge/relocate.js +92 -0
  318. package/dist/knowledge/scan.js +103 -47
  319. package/dist/knowledge/service.d.ts +3 -0
  320. package/dist/knowledge/service.js +33 -1
  321. package/dist/knowledge/write.d.ts +4 -1
  322. package/dist/knowledge/write.js +1 -1
  323. package/dist/mcp/server.d.ts +4 -2
  324. package/dist/mcp/server.js +87 -136
  325. package/dist/schemas/collaboration-state-validation.d.ts +6 -0
  326. package/dist/schemas/collaboration-state-validation.js +342 -0
  327. package/dist/schemas/collaboration-state.d.ts +436 -0
  328. package/dist/schemas/collaboration-state.js +1 -0
  329. package/dist/schemas/collaboration.d.ts +335 -0
  330. package/dist/schemas/collaboration.js +272 -0
  331. package/dist/schemas/continuity.d.ts +8 -0
  332. package/dist/schemas/continuity.js +13 -3
  333. package/dist/schemas/doctor.d.ts +1 -0
  334. package/dist/schemas/doctor.js +1 -0
  335. package/dist/schemas/document.d.ts +61 -0
  336. package/dist/schemas/document.js +9 -0
  337. package/dist/schemas/errors.d.ts +1 -0
  338. package/dist/schemas/errors.js +1 -0
  339. package/dist/schemas/host.d.ts +24 -0
  340. package/dist/schemas/host.js +19 -0
  341. package/dist/schemas/install.d.ts +8 -2
  342. package/dist/schemas/install.js +11 -1
  343. package/dist/schemas/managed-pi-execution.d.ts +16 -0
  344. package/dist/schemas/managed-pi-execution.js +2 -0
  345. package/dist/schemas/outcome.d.ts +9 -5
  346. package/dist/workspace/config-file.js +1 -3
  347. package/dist/workspace/documents.js +12 -2
  348. package/dist/workspace/repositories.d.ts +7 -0
  349. package/dist/workspace/repositories.js +14 -5
  350. package/package.json +35 -35
  351. package/skills/pappra/SKILL.md +28 -7
  352. package/skills/pappra/references/collaboration.md +633 -0
  353. package/skills/pappra/references/environment.md +360 -0
  354. package/skills/pappra/references/hosts.md +20 -0
  355. package/skills/pappra/references/operations.md +36 -5
  356. package/dist/app/collab-from.d.ts +0 -35
  357. package/dist/app/collab-from.js +0 -84
@@ -0,0 +1,1024 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonicalJson, contentDigest, validateEnvelope } from './envelope.js';
3
+ import { beginHandoff, completeHandoff, confirmReceiver, findHandoff, markOldWriterStopped, markTransferring, } from './handoff.js';
4
+ import { bindNative, findAttempt } from './lifecycle.js';
5
+ import { nowIso } from './store.js';
6
+ import { isTaskRecord, text } from './validation.js';
7
+ import { isNativeFirstWorkResult } from './first-work.js';
8
+ /**
9
+ * Bounded, record-level writer rollover correlation.
10
+ *
11
+ * This module only reduces the existing task record: handoff state and claims stay
12
+ * authoritative, and no second authority store, filesystem access, native control or
13
+ * continuity import is introduced. An intent pins the immutable inputs of one
14
+ * writer-to-writer transfer, including the confirmed source native identity; confirmation
15
+ * and first-work evidence must be stored, digest-verified protocol messages correlated to
16
+ * that intent. A caller-supplied object is never treated as authenticated native proof.
17
+ */
18
+ export const ROLLOVER_INTENT_SCHEMA = 'pappra/collab-rollover-intent/1';
19
+ export const ROLLOVER_SUCCESSION_SCHEMA = 'pappra/collab-succession/1';
20
+ const MAX_INTENTS = 32;
21
+ const MAX_SUCCESSIONS = 32;
22
+ const MAX_CLAIMS = 64;
23
+ const MAX_INVENTORY_ITEMS = 64;
24
+ const MAX_INVENTORY_BYTES = 16 * 1024;
25
+ const MAX_STRING = 2048;
26
+ const MAX_ID = 256;
27
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
28
+ const ACTIVE_ATTEMPT_STATES = new Set([
29
+ 'assigned',
30
+ 'accepted',
31
+ 'running',
32
+ 'unknown',
33
+ ]);
34
+ const CLOSED_HANDOFF_STATES = new Set(['completed', 'aborted']);
35
+ const PAST_PREPARE_STATES = new Set([
36
+ 'transferring',
37
+ 'receiver_confirmed',
38
+ 'completed',
39
+ ]);
40
+ const failure = (reason, detail = {}) => ({ ok: false, reason, ...detail });
41
+ const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
42
+ const isSafeString = (value, max = MAX_STRING) => typeof value === 'string' &&
43
+ value.length > 0 &&
44
+ value.length <= max &&
45
+ !value.includes('\u0000');
46
+ const isOptionalString = (value, max) => value === undefined || value === null || isSafeString(value, max);
47
+ const isCount = (value, minimum = 0) => Number.isSafeInteger(value) && value >= minimum;
48
+ const isIdentity = (value) => isObject(value) &&
49
+ isSafeString(value['agent_id'], MAX_ID) &&
50
+ isSafeString(value['session_id'], MAX_ID);
51
+ const sameIdentity = (left, right) => left.agent_id === right.agent_id && left.session_id === right.session_id;
52
+ const copyIdentity = (value) => ({
53
+ agent_id: value.agent_id,
54
+ session_id: value.session_id,
55
+ harness: value.harness ?? null,
56
+ ...(typeof value.cwd === 'string' ? { cwd: value.cwd } : {}),
57
+ });
58
+ const copyNative = (value) => ({
59
+ agent_id: value.agent_id,
60
+ runner: value.runner,
61
+ session_id: value.session_id,
62
+ run_id: value.run_id,
63
+ mission_id: value.mission_id,
64
+ });
65
+ const sameNative = (left, right) => left.agent_id === right.agent_id &&
66
+ left.runner === right.runner &&
67
+ left.session_id === right.session_id &&
68
+ left.run_id === right.run_id &&
69
+ left.mission_id === right.mission_id;
70
+ const sameClaimSet = (left, right) => left.length === right.length &&
71
+ left.every((claim) => right.some((other) => other.claim_id === claim.claim_id &&
72
+ other.resource_epoch === claim.resource_epoch));
73
+ const sameBundle = (left, right) => left.ref === right.ref && left.digest === right.digest;
74
+ /** Exact bounded inventory fingerprint; counts alone are not an identity. */
75
+ function inventoryFingerprint(inventory) {
76
+ if (!isObject(inventory))
77
+ return failure('inventory_required');
78
+ const inflight = inventory['inflight'];
79
+ const approvals = inventory['pending_approvals'];
80
+ const messages = inventory['pending_messages'];
81
+ const partial = inventory['partial_outputs'] ?? [];
82
+ if (inventory['completeness'] !== 'complete' ||
83
+ !Array.isArray(inflight) ||
84
+ !Array.isArray(approvals) ||
85
+ !Array.isArray(messages) ||
86
+ !Array.isArray(partial))
87
+ return failure('inventory_incomplete', {
88
+ completeness: inventory['completeness'] ?? null,
89
+ });
90
+ for (const list of [inflight, approvals, messages, partial]) {
91
+ if (list.length > MAX_INVENTORY_ITEMS)
92
+ return failure('inventory_too_large', { limit: MAX_INVENTORY_ITEMS });
93
+ }
94
+ let serialized;
95
+ try {
96
+ serialized = canonicalJson({
97
+ completeness: 'complete',
98
+ inflight,
99
+ pending_approvals: approvals,
100
+ pending_messages: messages,
101
+ partial_outputs: partial,
102
+ });
103
+ }
104
+ catch {
105
+ return failure('inventory_invalid');
106
+ }
107
+ if (serialized.length > MAX_INVENTORY_BYTES)
108
+ return failure('inventory_too_large', { limitBytes: MAX_INVENTORY_BYTES });
109
+ return {
110
+ ok: true,
111
+ digest: `sha256:${createHash('sha256').update(serialized).digest('hex')}`,
112
+ };
113
+ }
114
+ /**
115
+ * Bounded record validation for every public entry point. The authoritative `isTaskRecord`
116
+ * predicate validates nested native/claim/handoff/message/result/rollover shapes; this adds
117
+ * only the bounded collection counts. Malformed or oversized data is refused, never thrown.
118
+ */
119
+ function recordFailure(record) {
120
+ if (!isTaskRecord(record))
121
+ return failure('record_invalid');
122
+ if ((record.rollover ?? []).length > MAX_INTENTS)
123
+ return failure('record_too_large', {
124
+ field: 'rollover',
125
+ limit: MAX_INTENTS,
126
+ });
127
+ if ((record.successions ?? []).length > MAX_SUCCESSIONS)
128
+ return failure('record_too_large', {
129
+ field: 'successions',
130
+ limit: MAX_SUCCESSIONS,
131
+ });
132
+ return null;
133
+ }
134
+ /** Deterministic immutable-input comparison; no serialization order is trusted. */
135
+ function sameIntentInput(intent, input, taskId) {
136
+ return (intent.task_id === taskId &&
137
+ intent.handoff_id === input.handoff_id &&
138
+ intent.subtask_id === input.subtask_id &&
139
+ intent.attempt === input.attempt &&
140
+ intent.input_revision === input.input_revision &&
141
+ intent.coordinator_epoch === input.coordinator_epoch &&
142
+ intent.generation === input.generation &&
143
+ sameIdentity(intent.from, input.from) &&
144
+ sameIdentity(intent.to, input.to) &&
145
+ sameClaimSet(intent.claims, input.claims) &&
146
+ sameBundle(intent.bundle, input.bundle));
147
+ }
148
+ function validateInput(input) {
149
+ if (!isObject(input))
150
+ return failure('intent_invalid');
151
+ if (!isSafeString(input.intent_id, MAX_ID))
152
+ return failure('intent_id_invalid');
153
+ if (!isSafeString(input.handoff_id, MAX_ID))
154
+ return failure('handoff_id_invalid');
155
+ if (!isSafeString(input.subtask_id, MAX_ID))
156
+ return failure('subtask_id_invalid');
157
+ if (!isSafeString(input.input_revision, MAX_STRING))
158
+ return failure('input_revision_invalid');
159
+ if (!isCount(input.generation, 1) || input.generation > 1000)
160
+ return failure('generation_invalid');
161
+ if (!isCount(input.attempt, 1) || input.attempt > 1000)
162
+ return failure('attempt_invalid');
163
+ if (!isCount(input.coordinator_epoch, 1))
164
+ return failure('coordinator_epoch_invalid');
165
+ if (!isIdentity(input.from) || !isIdentity(input.to))
166
+ return failure('identity_invalid');
167
+ if (sameIdentity(input.from, input.to))
168
+ return failure('successor_matches_source');
169
+ if (!Array.isArray(input.claims) || input.claims.length === 0)
170
+ return failure('claims_required');
171
+ if (input.claims.length > MAX_CLAIMS)
172
+ return failure('claims_too_large');
173
+ const ids = new Set();
174
+ for (const claim of input.claims) {
175
+ if (!isObject(claim) || !isSafeString(claim.claim_id, MAX_ID))
176
+ return failure('claim_reference_invalid');
177
+ if (!isCount(claim.resource_epoch))
178
+ return failure('claim_epoch_invalid');
179
+ if (ids.has(claim.claim_id))
180
+ return failure('claim_reference_duplicate', { claim_id: claim.claim_id });
181
+ ids.add(claim.claim_id);
182
+ }
183
+ if (!isObject(input.bundle) ||
184
+ !isSafeString(input.bundle.ref) ||
185
+ typeof input.bundle.digest !== 'string' ||
186
+ !DIGEST.test(input.bundle.digest))
187
+ return failure('bundle_reference_invalid');
188
+ return null;
189
+ }
190
+ function intentList(record) {
191
+ return record.rollover ?? [];
192
+ }
193
+ function findIntent(record, intentId) {
194
+ return intentList(record).find((item) => item.intent_id === intentId) ?? null;
195
+ }
196
+ function findSuccession(record, handoffId, generation) {
197
+ return ((record.successions ?? []).find((item) => item.handoff_id === handoffId && item.generation === generation) ?? null);
198
+ }
199
+ function latestAttemptOf(record, subtaskId) {
200
+ let latest = null;
201
+ for (const attempt of record.attempts ?? []) {
202
+ if (attempt.subtask_id !== subtaskId)
203
+ continue;
204
+ latest =
205
+ latest === null ? attempt.attempt : Math.max(latest, attempt.attempt);
206
+ }
207
+ return latest;
208
+ }
209
+ /**
210
+ * A stored ACK/REPORT envelope is evidence only when the record holds a matching applied
211
+ * entry with the same content digest. The caller's object is never trusted on its own.
212
+ */
213
+ function storedEnvelope(record, raw, expectedType) {
214
+ let message;
215
+ try {
216
+ message = validateEnvelope(raw);
217
+ }
218
+ catch {
219
+ return failure(expectedType === 'ACK' ? 'ack_invalid' : 'report_invalid');
220
+ }
221
+ if (message.type !== expectedType)
222
+ return failure(expectedType === 'ACK' ? 'ack_type_invalid' : 'report_type_invalid');
223
+ const entries = record.messages ?? [];
224
+ const index = entries.findIndex((item) => item.message_id === message.message_id);
225
+ if (index < 0)
226
+ return failure(expectedType === 'ACK' ? 'ack_not_registered' : 'report_not_registered');
227
+ const entry = entries[index];
228
+ if (entry.state !== 'applied')
229
+ return failure(expectedType === 'ACK' ? 'ack_not_applied' : 'report_not_applied');
230
+ if (entry.content_digest !== contentDigest(message))
231
+ return failure(expectedType === 'ACK'
232
+ ? 'ack_content_mismatch'
233
+ : 'report_content_mismatch');
234
+ if (message.task_id !== record.task_id)
235
+ return failure(expectedType === 'ACK' ? 'ack_task_mismatch' : 'report_task_mismatch');
236
+ return { ok: true, message, entry, index };
237
+ }
238
+ /**
239
+ * Commits a reducer result only when the resulting record is persistable under the real
240
+ * record validator; otherwise the original record is left byte-identical.
241
+ */
242
+ function commit(record, next) {
243
+ if (!isTaskRecord(next))
244
+ return failure('record_invalid', { detail: 'mutation would not persist' });
245
+ Object.assign(record, next);
246
+ return null;
247
+ }
248
+ /** Current source and ownership facts that every pre-transfer step re-validates. */
249
+ function currentStateDrift(record, intent, expectedHandoffState) {
250
+ const coordinator = record.coordinator ?? null;
251
+ if (coordinator === null)
252
+ return failure('coordinator_required');
253
+ if (coordinator.owner_epoch !== intent.coordinator_epoch)
254
+ return failure('stale_epoch', {
255
+ expected: intent.coordinator_epoch,
256
+ actual: coordinator.owner_epoch,
257
+ });
258
+ const attempt = findAttempt(record, intent.subtask_id, intent.attempt);
259
+ if (attempt === null)
260
+ return failure('unknown_attempt');
261
+ if (latestAttemptOf(record, intent.subtask_id) !== intent.attempt)
262
+ return failure('attempt_superseded', {
263
+ attempt: intent.attempt,
264
+ latest: latestAttemptOf(record, intent.subtask_id),
265
+ });
266
+ if (!ACTIVE_ATTEMPT_STATES.has(attempt.state))
267
+ return failure('attempt_not_active', { state: attempt.state });
268
+ if (attempt.awaiting !== undefined)
269
+ return failure('writer_blocked', { awaiting: attempt.awaiting });
270
+ if (!sameIdentity(attempt.assignee, intent.from))
271
+ return failure('assignee_mismatch', {
272
+ expected: intent.from,
273
+ actual: attempt.assignee ?? null,
274
+ });
275
+ if (attempt.input_revision !== intent.input_revision)
276
+ return failure('input_changed', {
277
+ expected: intent.input_revision,
278
+ actual: attempt.input_revision ?? null,
279
+ });
280
+ // A changed or unknown source native identity invalidates previously recorded stop
281
+ // evidence; resources are never moved on stale proof.
282
+ const native = attempt.native ?? null;
283
+ if (native === null || native.observation !== 'confirmed')
284
+ return failure('source_native_unconfirmed', {
285
+ observation: native?.observation ?? null,
286
+ });
287
+ const currentNative = {
288
+ agent_id: intent.from.agent_id,
289
+ runner: native.runner ?? null,
290
+ session_id: native.session_id ?? null,
291
+ run_id: native.run_id ?? null,
292
+ mission_id: native.mission_id ?? null,
293
+ };
294
+ if (!sameNative(currentNative, intent.source_native))
295
+ return failure('source_native_drift', {
296
+ expected: intent.source_native,
297
+ actual: currentNative,
298
+ });
299
+ for (const reference of intent.claims) {
300
+ const claim = (record.claims ?? []).find((item) => item.claim_id === reference.claim_id);
301
+ if (claim === undefined)
302
+ return failure('claim_unavailable', { claim_id: reference.claim_id });
303
+ if (claim.released_at)
304
+ return failure('claim_released', { claim_id: reference.claim_id });
305
+ if (claim.holder === null || claim.holder === undefined)
306
+ return failure('claim_holder_missing', { claim_id: reference.claim_id });
307
+ if (!sameIdentity(claim.holder, intent.from))
308
+ return failure('claim_holder_mismatch', {
309
+ claim_id: reference.claim_id,
310
+ holder: claim.holder,
311
+ });
312
+ if ((claim.resource_epoch ?? 0) !== reference.resource_epoch)
313
+ return failure('claim_epoch_mismatch', {
314
+ claim_id: reference.claim_id,
315
+ expected: reference.resource_epoch,
316
+ actual: claim.resource_epoch ?? null,
317
+ });
318
+ }
319
+ const handoff = findHandoff(record, intent.handoff_id);
320
+ if (handoff === null)
321
+ return failure('unknown_handoff');
322
+ if (handoff.state !== expectedHandoffState)
323
+ return failure('handoff_not_ready', { state: handoff.state });
324
+ if (!sameIdentity(handoff.from, intent.from) ||
325
+ !sameIdentity(handoff.to, intent.to) ||
326
+ handoff.owner_epoch !== intent.coordinator_epoch)
327
+ return failure('handoff_mismatch', {
328
+ from: handoff.from ?? null,
329
+ to: handoff.to ?? null,
330
+ owner_epoch: handoff.owner_epoch,
331
+ });
332
+ return null;
333
+ }
334
+ /**
335
+ * Records the immutable intent and opens the underlying writer handoff. An exact replay of
336
+ * the same intent inputs returns the existing intent without another mutation.
337
+ */
338
+ export function createRolloverIntent(record, input) {
339
+ const invalidRecord = recordFailure(record);
340
+ if (invalidRecord !== null)
341
+ return invalidRecord;
342
+ const invalid = validateInput(input);
343
+ if (invalid !== null)
344
+ return invalid;
345
+ const clock = input.clock ?? Date.now();
346
+ const from = copyIdentity(input.from);
347
+ const to = copyIdentity(input.to);
348
+ const claims = input.claims.map((claim) => ({
349
+ claim_id: claim.claim_id,
350
+ resource_epoch: claim.resource_epoch,
351
+ }));
352
+ const bundle = { ref: input.bundle.ref, digest: input.bundle.digest };
353
+ const existing = findIntent(record, input.intent_id);
354
+ if (existing !== null) {
355
+ if (!sameIntentInput(existing, { ...input, from, to, claims, bundle }, record.task_id))
356
+ return failure('intent_input_conflict', {
357
+ intent_id: existing.intent_id,
358
+ });
359
+ return {
360
+ ok: true,
361
+ intent: existing,
362
+ handoff_state: findHandoff(record, existing.handoff_id)?.state ?? null,
363
+ replayed: true,
364
+ };
365
+ }
366
+ if (intentList(record).length >= MAX_INTENTS)
367
+ return failure('rollover_limit', { limit: MAX_INTENTS });
368
+ const targetHandoff = findHandoff(record, input.handoff_id);
369
+ if (targetHandoff !== null && CLOSED_HANDOFF_STATES.has(targetHandoff.state))
370
+ return failure('handoff_closed', { state: targetHandoff.state });
371
+ for (const other of intentList(record)) {
372
+ const handoff = findHandoff(record, other.handoff_id);
373
+ const closed = handoff === null || CLOSED_HANDOFF_STATES.has(handoff.state);
374
+ if (closed)
375
+ continue;
376
+ const sameAttempt = other.subtask_id === input.subtask_id && other.attempt === input.attempt;
377
+ const sharedClaim = other.claims.some((claim) => claims.some((candidate) => candidate.claim_id === claim.claim_id));
378
+ if (sameAttempt || sharedClaim)
379
+ return failure('intent_conflict', {
380
+ other_intent: other.intent_id,
381
+ other_handoff: other.handoff_id,
382
+ });
383
+ }
384
+ const next = structuredClone(record);
385
+ const coordinator = next.coordinator ?? null;
386
+ if (coordinator === null)
387
+ return failure('coordinator_required');
388
+ if (coordinator.owner_epoch !== input.coordinator_epoch)
389
+ return failure('stale_epoch', {
390
+ expected: coordinator.owner_epoch,
391
+ actual: input.coordinator_epoch,
392
+ });
393
+ const attempt = findAttempt(next, input.subtask_id, input.attempt);
394
+ if (attempt === null)
395
+ return failure('unknown_attempt');
396
+ if (latestAttemptOf(next, input.subtask_id) !== input.attempt)
397
+ return failure('attempt_superseded', {
398
+ attempt: input.attempt,
399
+ latest: latestAttemptOf(next, input.subtask_id),
400
+ });
401
+ if (!ACTIVE_ATTEMPT_STATES.has(attempt.state))
402
+ return failure('attempt_not_active', { state: attempt.state });
403
+ if (attempt.awaiting !== undefined)
404
+ return failure('writer_blocked', { awaiting: attempt.awaiting });
405
+ if (!sameIdentity(attempt.assignee, from))
406
+ return failure('assignee_mismatch', {
407
+ expected: from,
408
+ actual: attempt.assignee ?? null,
409
+ });
410
+ if (attempt.input_revision !== input.input_revision)
411
+ return failure('input_changed', {
412
+ expected: attempt.input_revision ?? null,
413
+ actual: input.input_revision,
414
+ });
415
+ // The source native identity must be confirmed and must belong to the source session.
416
+ const sourceNative = attempt.native ?? null;
417
+ if (sourceNative === null || sourceNative.observation !== 'confirmed')
418
+ return failure('source_native_unconfirmed', {
419
+ observation: sourceNative?.observation ?? null,
420
+ });
421
+ if (!isSafeString(sourceNative.session_id, MAX_ID) ||
422
+ sourceNative.session_id !== from.session_id)
423
+ return failure('source_native_mismatch', {
424
+ expected: from.session_id,
425
+ actual: sourceNative.session_id ?? null,
426
+ });
427
+ if (!isSafeString(sourceNative.runner, MAX_ID))
428
+ return failure('source_native_unconfirmed', { detail: 'runner missing' });
429
+ for (const reference of claims) {
430
+ const claim = (next.claims ?? []).find((item) => item.claim_id === reference.claim_id);
431
+ if (claim === undefined || claim.released_at)
432
+ return failure('claim_unavailable', { claim_id: reference.claim_id });
433
+ if (claim.holder === null || claim.holder === undefined)
434
+ return failure('claim_holder_missing', { claim_id: reference.claim_id });
435
+ if (!sameIdentity(claim.holder, from))
436
+ return failure('claim_holder_mismatch', {
437
+ claim_id: reference.claim_id,
438
+ holder: claim.holder,
439
+ });
440
+ if ((claim.resource_epoch ?? 0) !== reference.resource_epoch)
441
+ return failure('claim_epoch_mismatch', {
442
+ claim_id: reference.claim_id,
443
+ expected: reference.resource_epoch,
444
+ actual: claim.resource_epoch ?? null,
445
+ });
446
+ }
447
+ const begun = beginHandoff(next, {
448
+ handoffId: input.handoff_id,
449
+ kind: 'writer',
450
+ subtaskId: input.subtask_id,
451
+ attempt: input.attempt,
452
+ from,
453
+ to,
454
+ resources: claims.map((claim) => claim.claim_id),
455
+ ownerEpoch: input.coordinator_epoch,
456
+ clock,
457
+ reason: `rollover intent ${input.intent_id} generation ${input.generation}`,
458
+ });
459
+ if (!begun.ok)
460
+ return begun;
461
+ const intent = {
462
+ schema: ROLLOVER_INTENT_SCHEMA,
463
+ intent_id: input.intent_id,
464
+ generation: input.generation,
465
+ task_id: next.task_id,
466
+ handoff_id: input.handoff_id,
467
+ subtask_id: input.subtask_id,
468
+ attempt: input.attempt,
469
+ input_revision: input.input_revision,
470
+ coordinator_epoch: input.coordinator_epoch,
471
+ from,
472
+ to,
473
+ claims,
474
+ bundle,
475
+ created_at: nowIso(clock),
476
+ source_native: {
477
+ agent_id: from.agent_id,
478
+ runner: sourceNative.runner,
479
+ session_id: sourceNative.session_id,
480
+ run_id: sourceNative.run_id ?? null,
481
+ mission_id: sourceNative.mission_id ?? null,
482
+ },
483
+ prepared: null,
484
+ transfer_boundary: null,
485
+ successor_native: null,
486
+ };
487
+ next.rollover = [...intentList(next), intent];
488
+ const committed = commit(record, next);
489
+ if (committed !== null)
490
+ return committed;
491
+ return {
492
+ ok: true,
493
+ intent,
494
+ handoff_state: 'requested',
495
+ replayed: false,
496
+ };
497
+ }
498
+ /** Read-only projection of one intent, its handoff progress and any recorded succession. */
499
+ export function inspectRolloverIntent(record, intentId) {
500
+ const invalidRecord = recordFailure(record);
501
+ if (invalidRecord !== null)
502
+ return invalidRecord;
503
+ const intent = findIntent(record, intentId);
504
+ if (intent === null)
505
+ return failure('unknown_intent', { intent_id: intentId });
506
+ const handoff = findHandoff(record, intent.handoff_id);
507
+ const attempt = findAttempt(record, intent.subtask_id, intent.attempt);
508
+ const succession = findSuccession(record, intent.handoff_id, intent.generation);
509
+ return {
510
+ ok: true,
511
+ intent,
512
+ handoff_state: handoff?.state ?? null,
513
+ ownership: attempt?.ownership ?? null,
514
+ assignee: attempt?.assignee ?? null,
515
+ claims: intent.claims.map((reference) => {
516
+ const claim = (record.claims ?? []).find((item) => item.claim_id === reference.claim_id);
517
+ return {
518
+ claim_id: reference.claim_id,
519
+ expected_resource_epoch: reference.resource_epoch,
520
+ holder: claim?.holder ?? null,
521
+ resource_epoch: claim?.resource_epoch ?? null,
522
+ released_at: claim?.released_at ?? null,
523
+ };
524
+ }),
525
+ transfer_boundary: intent.transfer_boundary ?? null,
526
+ succession,
527
+ // Routing integration lives outside this record slice and stays explicitly pending.
528
+ routing: succession === null ? 'not_started' : 'pending',
529
+ native_archive: 'unavailable',
530
+ };
531
+ }
532
+ /**
533
+ * Records confirmed quiescence and freezes the transfer inventory. An absent or non-complete
534
+ * inventory, unknown stop evidence, a blocked writer and any drift are refused without
535
+ * mutation; an exact replay of a completed step is a no-op.
536
+ */
537
+ export function prepareRolloverTransfer(record, { intent_id, observation, evidence_ref, inventory, actor, clock = Date.now(), }) {
538
+ const invalidRecord = recordFailure(record);
539
+ if (invalidRecord !== null)
540
+ return invalidRecord;
541
+ const intent = findIntent(record, intent_id);
542
+ if (intent === null)
543
+ return failure('unknown_intent', { intent_id });
544
+ const handoff = findHandoff(record, intent.handoff_id);
545
+ if (handoff === null)
546
+ return failure('unknown_handoff');
547
+ const fingerprint = inventoryFingerprint(inventory);
548
+ if (!('ok' in fingerprint) || fingerprint.ok !== true)
549
+ return fingerprint;
550
+ const inventoryValue = inventory;
551
+ const pendingMessages = (inventoryValue.pending_messages ??
552
+ []);
553
+ if (inventoryValue.inflight.length > 0 ||
554
+ inventoryValue.pending_approvals.length > 0 ||
555
+ pendingMessages.length > 0)
556
+ return failure('incomplete_inventory', {
557
+ inflight: inventoryValue.inflight.length,
558
+ pending_approvals: inventoryValue.pending_approvals.length,
559
+ pending_messages: pendingMessages.length,
560
+ });
561
+ const digest = fingerprint.digest;
562
+ if (PAST_PREPARE_STATES.has(handoff.state)) {
563
+ const prepared = intent.prepared ?? null;
564
+ if (prepared !== null &&
565
+ prepared.evidence_ref === evidence_ref &&
566
+ prepared.inventory === digest)
567
+ return {
568
+ ok: true,
569
+ handoff_state: handoff.state,
570
+ replayed: true,
571
+ };
572
+ return failure('prepare_conflict', { state: handoff.state });
573
+ }
574
+ if (handoff.state !== 'requested')
575
+ return failure('handoff_not_ready', { state: handoff.state });
576
+ if (observation !== 'confirmed')
577
+ return failure('old_writer_not_stopped', { observation });
578
+ if (!text(evidence_ref))
579
+ return failure('stop_evidence_required');
580
+ const drift = currentStateDrift(record, intent, 'requested');
581
+ if (drift !== null)
582
+ return drift;
583
+ const next = structuredClone(record);
584
+ const stopped = markOldWriterStopped(next, {
585
+ handoffId: intent.handoff_id,
586
+ observation,
587
+ evidenceRef: evidence_ref,
588
+ actor: actor ?? intent.from.agent_id,
589
+ clock,
590
+ });
591
+ if (!stopped.ok)
592
+ return stopped;
593
+ const transferring = markTransferring(next, {
594
+ handoffId: intent.handoff_id,
595
+ partialOutputs: [...(inventoryValue.partial_outputs ?? [])],
596
+ inflight: [],
597
+ pendingApprovals: [],
598
+ actor: actor ?? intent.from.agent_id,
599
+ clock,
600
+ });
601
+ if (!transferring.ok)
602
+ return transferring;
603
+ next.rollover = intentList(next).map((item) => item.intent_id === intent.intent_id
604
+ ? {
605
+ ...item,
606
+ prepared: { evidence_ref, inventory: digest, at: nowIso(clock) },
607
+ }
608
+ : item);
609
+ const committed = commit(record, next);
610
+ if (committed !== null)
611
+ return committed;
612
+ return { ok: true, handoff_state: 'transferring', replayed: false };
613
+ }
614
+ /**
615
+ * Confirms the successor only from a stored, digest-verified ACK envelope that carries the
616
+ * pinned input revision, the current coordinator epoch and recipient, the intent/handoff/
617
+ * generation correlation and this attempt's registered assignment as its bootstrap.
618
+ */
619
+ export function confirmRolloverReceiver(record, { intent_id, message, clock = Date.now(), }) {
620
+ const invalidRecord = recordFailure(record);
621
+ if (invalidRecord !== null)
622
+ return invalidRecord;
623
+ const intent = findIntent(record, intent_id);
624
+ if (intent === null)
625
+ return failure('unknown_intent', { intent_id });
626
+ const handoff = findHandoff(record, intent.handoff_id);
627
+ if (handoff === null)
628
+ return failure('unknown_handoff');
629
+ const stored = storedEnvelope(record, message, 'ACK');
630
+ if (!('ok' in stored) || stored.ok !== true)
631
+ return stored;
632
+ const { message: ack, entry } = stored;
633
+ if (handoff.state === 'receiver_confirmed' || handoff.state === 'completed') {
634
+ if (handoff.confirmation_message_id === ack.message_id)
635
+ return {
636
+ ok: true,
637
+ handoff_state: handoff.state,
638
+ replayed: true,
639
+ };
640
+ return failure('confirmation_conflict', {
641
+ state: handoff.state,
642
+ confirmation_message_id: handoff.confirmation_message_id ?? null,
643
+ });
644
+ }
645
+ if (handoff.state !== 'transferring')
646
+ return failure('handoff_not_ready', { state: handoff.state });
647
+ if (!sameIdentity(ack.from, intent.to) ||
648
+ !sameIdentity(entry.from, intent.to))
649
+ return failure('ack_identity_mismatch', {
650
+ expected: intent.to,
651
+ actual: ack.from,
652
+ });
653
+ const coordinator = record.coordinator ?? null;
654
+ if (coordinator === null)
655
+ return failure('coordinator_required');
656
+ if (!sameIdentity(ack.to, coordinator) ||
657
+ (entry.to !== null && !sameIdentity(entry.to, coordinator)))
658
+ return failure('ack_recipient_mismatch', {
659
+ expected: coordinator,
660
+ actual: ack.to ?? null,
661
+ });
662
+ if (ack.owner_epoch !== coordinator.owner_epoch)
663
+ return failure('stale_epoch', {
664
+ expected: coordinator.owner_epoch,
665
+ actual: ack.owner_epoch ?? null,
666
+ });
667
+ if (ack.subtask_id !== intent.subtask_id || ack.attempt !== intent.attempt)
668
+ return failure('ack_correlation_mismatch', {
669
+ subtask_id: ack.subtask_id,
670
+ attempt: ack.attempt,
671
+ });
672
+ if (ack.body?.handoff_id !== intent.handoff_id)
673
+ return failure('ack_correlation_mismatch', {
674
+ handoff_id: ack.body?.handoff_id ?? null,
675
+ });
676
+ if (ack.body?.rollover_intent_id !== intent.intent_id)
677
+ return failure('ack_correlation_mismatch', {
678
+ intent_id: ack.body?.rollover_intent_id ?? null,
679
+ });
680
+ if (ack.body?.rollover_generation !== intent.generation)
681
+ return failure('ack_correlation_mismatch', {
682
+ generation: ack.body?.rollover_generation ?? null,
683
+ });
684
+ if (ack.body?.input_revision !== intent.input_revision)
685
+ return failure('ack_input_mismatch', {
686
+ expected: intent.input_revision,
687
+ actual: ack.body?.input_revision ?? null,
688
+ });
689
+ const attempt = findAttempt(record, intent.subtask_id, intent.attempt);
690
+ if (attempt === null)
691
+ return failure('unknown_attempt');
692
+ if (ack.reply_to !== attempt.assignment_message_id)
693
+ return failure('ack_bootstrap_mismatch', {
694
+ expected: attempt.assignment_message_id,
695
+ actual: ack.reply_to ?? null,
696
+ });
697
+ // A prior ACK cannot survive any current source, ownership or claim drift.
698
+ const drift = currentStateDrift(record, intent, 'transferring');
699
+ if (drift !== null)
700
+ return drift;
701
+ const next = structuredClone(record);
702
+ const confirmed = confirmReceiver(next, {
703
+ handoffId: intent.handoff_id,
704
+ message: ack,
705
+ clock,
706
+ });
707
+ if (!confirmed.ok)
708
+ return confirmed;
709
+ const committed = commit(record, next);
710
+ if (committed !== null)
711
+ return committed;
712
+ return {
713
+ ok: true,
714
+ handoff_state: 'receiver_confirmed',
715
+ replayed: false,
716
+ };
717
+ }
718
+ /**
719
+ * Completes the writer transfer: claims move to the successor with a raised resource epoch,
720
+ * the attempt is reassigned and its native identity is rebound explicitly. The coordinator
721
+ * owner epoch is never raised here.
722
+ */
723
+ export function completeRolloverTransfer(record, { intent_id, successor_native, actor, clock = Date.now(), }) {
724
+ const invalidRecord = recordFailure(record);
725
+ if (invalidRecord !== null)
726
+ return invalidRecord;
727
+ const intent = findIntent(record, intent_id);
728
+ if (intent === null)
729
+ return failure('unknown_intent', { intent_id });
730
+ const handoff = findHandoff(record, intent.handoff_id);
731
+ if (handoff === null)
732
+ return failure('unknown_handoff');
733
+ if (!isObject(successor_native) ||
734
+ !isSafeString(successor_native.session_id, MAX_ID) ||
735
+ !isSafeString(successor_native.agent_id, MAX_ID) ||
736
+ !isSafeString(successor_native.runner, MAX_ID) ||
737
+ !text(successor_native.evidence_ref) ||
738
+ !isOptionalString(successor_native.run_id, MAX_ID) ||
739
+ !isOptionalString(successor_native.mission_id, MAX_ID) ||
740
+ !isOptionalString(successor_native.observed_at, 64))
741
+ return failure('successor_native_invalid');
742
+ if (successor_native.agent_id !== intent.to.agent_id)
743
+ return failure('successor_native_mismatch', {
744
+ field: 'agent_id',
745
+ expected: intent.to.agent_id,
746
+ actual: successor_native.agent_id,
747
+ });
748
+ if (successor_native.session_id !== intent.to.session_id)
749
+ return failure('successor_native_mismatch', {
750
+ field: 'session_id',
751
+ expected: intent.to.session_id,
752
+ actual: successor_native.session_id,
753
+ });
754
+ if (successor_native.session_id === intent.from.session_id)
755
+ return failure('successor_native_inherited', {
756
+ session_id: successor_native.session_id,
757
+ });
758
+ const pinnedNative = copyNative({
759
+ agent_id: successor_native.agent_id,
760
+ runner: successor_native.runner,
761
+ session_id: successor_native.session_id,
762
+ run_id: successor_native.run_id ?? null,
763
+ mission_id: successor_native.mission_id ?? null,
764
+ });
765
+ if (intent.transfer_boundary !== null &&
766
+ intent.transfer_boundary !== undefined) {
767
+ const stored = intent.successor_native ?? null;
768
+ if (stored !== null && sameNative(stored, pinnedNative))
769
+ return {
770
+ ok: true,
771
+ handoff_state: handoff.state,
772
+ transfer_boundary: intent.transfer_boundary,
773
+ replayed: true,
774
+ };
775
+ return failure('transfer_already_completed', {
776
+ at: intent.transfer_boundary.at,
777
+ });
778
+ }
779
+ if (handoff.state !== 'receiver_confirmed')
780
+ return failure('handoff_not_ready', { state: handoff.state });
781
+ const drift = currentStateDrift(record, intent, 'receiver_confirmed');
782
+ if (drift !== null)
783
+ return drift;
784
+ const next = structuredClone(record);
785
+ const completed = completeHandoff(next, {
786
+ handoffId: intent.handoff_id,
787
+ actor: actor ?? intent.from.agent_id,
788
+ clock,
789
+ });
790
+ if (!completed.ok)
791
+ return completed;
792
+ const bound = bindNative(next, {
793
+ subtaskId: intent.subtask_id,
794
+ attempt: intent.attempt,
795
+ runner: pinnedNative.runner,
796
+ sessionId: pinnedNative.session_id,
797
+ runId: pinnedNative.run_id,
798
+ missionId: pinnedNative.mission_id,
799
+ observation: 'confirmed',
800
+ evidenceRef: successor_native.evidence_ref,
801
+ observedAt: successor_native.observed_at ?? null,
802
+ clock,
803
+ });
804
+ if (!bound.ok)
805
+ return bound;
806
+ const boundary = {
807
+ message_count: (next.messages ?? []).length,
808
+ state_revision: next.state_revision,
809
+ at: nowIso(clock),
810
+ };
811
+ next.rollover = intentList(next).map((item) => item.intent_id === intent.intent_id
812
+ ? {
813
+ ...item,
814
+ transfer_boundary: boundary,
815
+ successor_native: pinnedNative,
816
+ }
817
+ : item);
818
+ const committed = commit(record, next);
819
+ if (committed !== null)
820
+ return committed;
821
+ return {
822
+ ok: true,
823
+ handoff_state: 'completed',
824
+ transfer_boundary: boundary,
825
+ owner_epoch: next.coordinator?.owner_epoch ?? null,
826
+ replayed: false,
827
+ };
828
+ }
829
+ /**
830
+ * Records the source-session history marker only when a stored, admitted successor REPORT
831
+ * proves first work after the recorded transfer boundary, under the pinned input, current
832
+ * attempt, exact post-transfer claim epochs and the pinned successor native identity.
833
+ */
834
+ export function recordRolloverFirstWork(record, { intent_id, message, clock = Date.now(), }) {
835
+ const invalidRecord = recordFailure(record);
836
+ if (invalidRecord !== null)
837
+ return invalidRecord;
838
+ const intent = findIntent(record, intent_id);
839
+ if (intent === null)
840
+ return failure('unknown_intent', { intent_id });
841
+ const handoff = findHandoff(record, intent.handoff_id);
842
+ if (handoff === null)
843
+ return failure('unknown_handoff');
844
+ if (handoff.state !== 'completed')
845
+ return failure('transfer_not_completed', { state: handoff.state });
846
+ const boundary = intent.transfer_boundary ?? null;
847
+ if (boundary === null || boundary === undefined)
848
+ return failure('transfer_boundary_missing');
849
+ const existing = findSuccession(record, intent.handoff_id, intent.generation);
850
+ const stored = storedEnvelope(record, message, 'REPORT');
851
+ if (!('ok' in stored) || stored.ok !== true)
852
+ return stored;
853
+ const { message: report, entry, index } = stored;
854
+ if (existing !== null) {
855
+ if (existing.proof.message_id === report.message_id &&
856
+ existing.proof.digest === entry.content_digest) {
857
+ return {
858
+ ok: true,
859
+ succession: existing,
860
+ replayed: true,
861
+ routing: 'pending',
862
+ native_archive: 'unavailable',
863
+ };
864
+ }
865
+ return failure('first_work_already_recorded', {
866
+ message_id: existing.proof.message_id,
867
+ });
868
+ }
869
+ if (index < boundary.message_count)
870
+ return failure('report_pre_transfer', {
871
+ index,
872
+ boundary: boundary.message_count,
873
+ });
874
+ const attempt = findAttempt(record, intent.subtask_id, intent.attempt);
875
+ if (attempt === null)
876
+ return failure('unknown_attempt');
877
+ if (latestAttemptOf(record, intent.subtask_id) !== intent.attempt)
878
+ return failure('attempt_superseded', {
879
+ attempt: intent.attempt,
880
+ latest: latestAttemptOf(record, intent.subtask_id),
881
+ });
882
+ if (!sameIdentity(attempt.assignee, intent.to))
883
+ return failure('assignee_mismatch', {
884
+ expected: intent.to,
885
+ actual: attempt.assignee ?? null,
886
+ });
887
+ if (attempt.input_revision !== intent.input_revision)
888
+ return failure('input_changed', {
889
+ expected: intent.input_revision,
890
+ actual: attempt.input_revision ?? null,
891
+ });
892
+ if (!sameIdentity(report.from, intent.to) ||
893
+ !sameIdentity(entry.from, intent.to))
894
+ return failure('report_sender_mismatch', {
895
+ expected: intent.to,
896
+ actual: report.from,
897
+ });
898
+ if (report.subtask_id !== intent.subtask_id ||
899
+ report.attempt !== intent.attempt)
900
+ return failure('report_correlation_mismatch', {
901
+ subtask_id: report.subtask_id,
902
+ attempt: report.attempt,
903
+ });
904
+ if (report.body?.handoff_id !== intent.handoff_id)
905
+ return failure('report_correlation_mismatch', {
906
+ handoff_id: report.body?.handoff_id ?? null,
907
+ });
908
+ if (report.body?.rollover_generation !== intent.generation)
909
+ return failure('report_correlation_mismatch', {
910
+ generation: report.body?.rollover_generation ?? null,
911
+ });
912
+ const result = (record.results ?? []).find((item) => item.message_id === report.message_id);
913
+ if (result === undefined)
914
+ return failure('report_not_admitted', { detail: 'no result entry' });
915
+ if (result.classification !== 'applied')
916
+ return failure('report_not_admitted', {
917
+ classification: result.classification,
918
+ });
919
+ if (result.status !== 'completed' &&
920
+ !(report.body?.phase === 'handoff-first-work' &&
921
+ isNativeFirstWorkResult(result, intent)))
922
+ return failure('report_not_admitted', { status: result.status });
923
+ if (report.body?.status !== result.status)
924
+ return failure('report_status_mismatch', {
925
+ report: report.body?.status ?? null,
926
+ result: result.status,
927
+ });
928
+ if (result.subtask_id !== intent.subtask_id ||
929
+ result.attempt !== intent.attempt)
930
+ return failure('report_not_admitted', {
931
+ detail: 'result attempt mismatch',
932
+ });
933
+ if (report.body?.input_revision !== intent.input_revision ||
934
+ result.input_revision !== intent.input_revision)
935
+ return failure('report_input_mismatch', {
936
+ expected: intent.input_revision,
937
+ report: report.body?.input_revision ?? null,
938
+ result: result.input_revision ?? null,
939
+ });
940
+ const coordinator = record.coordinator ?? null;
941
+ if (coordinator === null ||
942
+ coordinator.owner_epoch !== intent.coordinator_epoch)
943
+ return failure('stale_epoch', {
944
+ expected: intent.coordinator_epoch,
945
+ actual: coordinator?.owner_epoch ?? null,
946
+ });
947
+ for (const reference of intent.claims) {
948
+ const claim = (record.claims ?? []).find((item) => item.claim_id === reference.claim_id);
949
+ if (claim === undefined || claim.released_at)
950
+ return failure('claim_unavailable', { claim_id: reference.claim_id });
951
+ if (claim.holder === null || claim.holder === undefined)
952
+ return failure('claim_holder_missing', { claim_id: reference.claim_id });
953
+ if (!sameIdentity(claim.holder, intent.to))
954
+ return failure('claim_holder_mismatch', {
955
+ claim_id: reference.claim_id,
956
+ holder: claim.holder,
957
+ });
958
+ if (claim.resource_epoch !== reference.resource_epoch + 1)
959
+ return failure('claim_epoch_mismatch', {
960
+ claim_id: reference.claim_id,
961
+ expected: reference.resource_epoch + 1,
962
+ actual: claim.resource_epoch ?? null,
963
+ });
964
+ }
965
+ const pinnedSuccessor = intent.successor_native ?? null;
966
+ const native = attempt.native ?? null;
967
+ if (pinnedSuccessor === null)
968
+ return failure('successor_native_missing');
969
+ if (native === null || native.observation !== 'confirmed')
970
+ return failure('native_identity_mismatch', {
971
+ observation: native?.observation ?? null,
972
+ });
973
+ if (native.session_id !== pinnedSuccessor.session_id)
974
+ return failure('native_identity_mismatch', {
975
+ field: 'session_id',
976
+ expected: pinnedSuccessor.session_id,
977
+ actual: native.session_id ?? null,
978
+ });
979
+ if (native.run_id !== pinnedSuccessor.run_id)
980
+ return failure('native_identity_mismatch', {
981
+ field: 'run_id',
982
+ expected: pinnedSuccessor.run_id,
983
+ actual: native.run_id ?? null,
984
+ });
985
+ if (native.runner !== pinnedSuccessor.runner)
986
+ return failure('native_identity_mismatch', {
987
+ field: 'runner',
988
+ expected: pinnedSuccessor.runner,
989
+ actual: native.runner ?? null,
990
+ });
991
+ const succession = {
992
+ schema: ROLLOVER_SUCCESSION_SCHEMA,
993
+ intent_id: intent.intent_id,
994
+ handoff_id: intent.handoff_id,
995
+ generation: intent.generation,
996
+ old_native: copyNative(intent.source_native),
997
+ successor_native: copyNative(pinnedSuccessor),
998
+ archived_at: nowIso(clock),
999
+ history_retained: true,
1000
+ // Routing integration is a later adapter slice: never claim effective removal.
1001
+ routing: 'pending',
1002
+ native_archive: 'unavailable',
1003
+ proof: { message_id: report.message_id, digest: entry.content_digest },
1004
+ };
1005
+ if ((record.successions ?? []).length >= MAX_SUCCESSIONS)
1006
+ return failure('rollover_limit', {
1007
+ field: 'successions',
1008
+ limit: MAX_SUCCESSIONS,
1009
+ });
1010
+ const next = structuredClone(record);
1011
+ next.successions = [...(next.successions ?? []), succession];
1012
+ if (recordFailure(next) !== null)
1013
+ return failure('record_invalid', {
1014
+ detail: 'succession would not persist',
1015
+ });
1016
+ commit(record, next);
1017
+ return {
1018
+ ok: true,
1019
+ succession,
1020
+ replayed: false,
1021
+ routing: 'pending',
1022
+ native_archive: 'unavailable',
1023
+ };
1024
+ }