@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,3334 @@
1
+ import { object as isRecord } from '../infra/validation.js';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { readManagedSourceAssignment } from './managed-pi-source-admission.js';
6
+ import { hasPendingCancellation } from '../collab/admission.js';
7
+ import { canonicalJson } from '../collab/envelope.js';
8
+ import { findAttempt } from '../collab/lifecycle.js';
9
+ import * as rollover from '../collab/rollover.js';
10
+ import { transact } from '../collab/store.js';
11
+ import { handoffBundleRelativeRef } from '../continuity/index.js';
12
+ import { collabBindingSchema, } from '../schemas/collaboration.js';
13
+ import { isPathInside } from '../schemas/paths.js';
14
+ import { applyVisibleBootstrap, loadVisibleBootstrapRequest, planVisibleBootstrap, } from './collab-bootstrap.js';
15
+ import { reportedSessionIdentity, createHerdrTransport, } from './collab-notify.js';
16
+ import { assertTaskStoreCurrent, openTaskStore, } from './collab-task-store.js';
17
+ import { openMessageTask, readRegisteredMessageSafe, } from './collab-messages.js';
18
+ import { readBoundedFile } from './bounded-file.js';
19
+ import { runCollaborationPreflight } from './collab-preflight.js';
20
+ import { COLLAB_ROLLOVER_REQUEST_SCHEMA, inspectOrPlanRollover, } from './collab-rollover.js';
21
+ import { createHerdrBootstrapOps, } from './collab-bootstrap-transport.js';
22
+ import { routeExclusionFor, validateSuccession } from './collab-route.js';
23
+ import { isTaskRecord } from '../collab/validation.js';
24
+ import { isTrustedManagedPiScopedProof } from './managed-pi-transition-host.js';
25
+ import { verifyManagedPiSdkInstallation } from '../hosts/pi/managed-sdk-session.js';
26
+ import { MANAGED_PI_EXECUTION_SCOPE_SCHEMA, } from '../schemas/managed-pi-execution.js';
27
+ export { MANAGED_PI_EXECUTION_SCOPE_SCHEMA };
28
+ export const COLLAB_SESSION_TRANSITION_REQUEST_SCHEMA = 'pappra/collaboration-session-transition-request/1';
29
+ export const COLLAB_SESSION_TRANSITION_REPORT_SCHEMA = 'pappra/collaboration-session-transition/1';
30
+ export const SESSION_TRANSITION_ARCHIVE_SCHEMA = 'pappra/session-transition-archive/1';
31
+ const MAX_REQUEST_BYTES = 64 * 1024;
32
+ const MAX_JOURNAL_ENTRIES = 32;
33
+ const MAX_JOURNAL_BYTES = 64 * 1024;
34
+ const MAX_ARCHIVE_BYTES = 16 * 1024;
35
+ const MAX_QUIESCENCE_AGE_MS = 30_000;
36
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
37
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
38
+ export const SESSION_TRANSITION_JOURNAL_KEY = 'session_transition_journal';
39
+ const JOURNAL_KEY = SESSION_TRANSITION_JOURNAL_KEY;
40
+ export const MANAGED_PI_SCOPED_PROOF_ADAPTER_ID = 'pappra/managed-pi-session-transition-host/2';
41
+ export const MANAGED_PI_SCOPED_RECEIPT_SCHEMA = 'pappra/session-transition-quiescence-receipt/2';
42
+ function digestText(bytes) {
43
+ return `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`;
44
+ }
45
+ function failure(kind, code, message, nextAction) {
46
+ return { ok: false, kind, code, message, nextAction };
47
+ }
48
+ function hasOnlyKeys(value, keys) {
49
+ const allowed = new Set(keys);
50
+ return Object.keys(value).every((key) => allowed.has(key));
51
+ }
52
+ function isText(value, max = 2048) {
53
+ return (typeof value === 'string' &&
54
+ value.length > 0 &&
55
+ value.length <= max &&
56
+ !value.includes('\u0000'));
57
+ }
58
+ function isPositiveInt(value) {
59
+ return Number.isSafeInteger(value) && value > 0;
60
+ }
61
+ function isNonNegativeInt(value) {
62
+ return Number.isSafeInteger(value) && value >= 0;
63
+ }
64
+ function isDigest(value) {
65
+ return typeof value === 'string' && DIGEST.test(value);
66
+ }
67
+ function isSafeId(value) {
68
+ return typeof value === 'string' && SAFE_ID.test(value);
69
+ }
70
+ function sameIdentity(left, right) {
71
+ return (left !== null &&
72
+ left !== undefined &&
73
+ left.agent_id === right.agent_id &&
74
+ left.session_id === right.session_id);
75
+ }
76
+ function sameHarnessIdentity(left, right) {
77
+ if (!sameIdentity(left, right))
78
+ return false;
79
+ // Task claim/assignee records may omit optional harness/cwd fields; compare them exactly when present.
80
+ if (left?.harness !== undefined &&
81
+ left.harness !== null &&
82
+ left.harness !== right.harness)
83
+ return false;
84
+ if (left?.cwd !== undefined &&
85
+ right.cwd !== undefined &&
86
+ left.cwd !== right.cwd)
87
+ return false;
88
+ return true;
89
+ }
90
+ function safePathEqual(left, right) {
91
+ try {
92
+ return fs.realpathSync(left) === fs.realpathSync(right);
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ function nowIso(clock) {
99
+ return new Date(clock()).toISOString();
100
+ }
101
+ function immutableDigest(request) {
102
+ return digestText(canonicalJson({
103
+ schema: request.schema,
104
+ task_root: request.task_root,
105
+ transition_id: request.transition_id,
106
+ intent: request.intent,
107
+ bundle: request.bundle,
108
+ bootstrap_request: request.bootstrap_request,
109
+ archive_mode: request.archive_mode,
110
+ ...(request.managed_execution_scope === undefined
111
+ ? {}
112
+ : { managed_execution_scope: request.managed_execution_scope }),
113
+ }));
114
+ }
115
+ function parseIdentity(value) {
116
+ if (!isRecord(value) ||
117
+ !hasOnlyKeys(value, ['agent_id', 'session_id', 'harness', 'cwd']))
118
+ return null;
119
+ if (!isText(value['agent_id'], 128) || !isText(value['session_id'], 256))
120
+ return null;
121
+ if (value['harness'] !== 'pi')
122
+ return null;
123
+ if (!isText(value['cwd'], 4096) || !path.isAbsolute(value['cwd']))
124
+ return null;
125
+ return {
126
+ agent_id: value['agent_id'],
127
+ session_id: value['session_id'],
128
+ harness: 'pi',
129
+ cwd: value['cwd'],
130
+ };
131
+ }
132
+ function parseIntent(value) {
133
+ if (!isRecord(value) ||
134
+ !hasOnlyKeys(value, [
135
+ 'intent_id',
136
+ 'generation',
137
+ 'handoff_id',
138
+ 'subtask_id',
139
+ 'attempt',
140
+ 'input_revision',
141
+ 'from',
142
+ 'to',
143
+ 'claims',
144
+ 'coordinator_epoch',
145
+ ]))
146
+ return null;
147
+ const from = parseIdentity(value['from']);
148
+ const to = parseIdentity(value['to']);
149
+ const claims = value['claims'];
150
+ if (!isSafeId(value['intent_id']) ||
151
+ !isPositiveInt(value['generation']) ||
152
+ !isSafeId(value['handoff_id']) ||
153
+ !isSafeId(value['subtask_id']) ||
154
+ !isPositiveInt(value['attempt']) ||
155
+ !isDigest(value['input_revision']) ||
156
+ from === null ||
157
+ to === null ||
158
+ !isPositiveInt(value['coordinator_epoch']) ||
159
+ !Array.isArray(claims) ||
160
+ claims.length === 0 ||
161
+ claims.length > 64 ||
162
+ sameIdentity(from, to))
163
+ return null;
164
+ const parsedClaims = [];
165
+ const seen = new Set();
166
+ for (const claim of claims) {
167
+ if (!isRecord(claim) ||
168
+ !hasOnlyKeys(claim, ['claim_id', 'resource_epoch']) ||
169
+ !isSafeId(claim['claim_id']) ||
170
+ !isNonNegativeInt(claim['resource_epoch']) ||
171
+ seen.has(claim['claim_id']))
172
+ return null;
173
+ seen.add(claim['claim_id']);
174
+ parsedClaims.push({
175
+ claim_id: claim['claim_id'],
176
+ resource_epoch: claim['resource_epoch'],
177
+ });
178
+ }
179
+ return {
180
+ intent_id: value['intent_id'],
181
+ generation: value['generation'],
182
+ handoff_id: value['handoff_id'],
183
+ subtask_id: value['subtask_id'],
184
+ attempt: value['attempt'],
185
+ input_revision: value['input_revision'],
186
+ from,
187
+ to,
188
+ claims: parsedClaims,
189
+ coordinator_epoch: value['coordinator_epoch'],
190
+ };
191
+ }
192
+ function parseManagedPiExecutionScope(value) {
193
+ if (!isRecord(value) ||
194
+ !hasOnlyKeys(value, [
195
+ 'schema',
196
+ 'task_id',
197
+ 'task_root',
198
+ 'session_id',
199
+ 'cwd',
200
+ 'subtask_id',
201
+ 'attempt',
202
+ 'owner_epoch',
203
+ 'claim_epochs',
204
+ ]) ||
205
+ value['schema'] !== MANAGED_PI_EXECUTION_SCOPE_SCHEMA ||
206
+ !isSafeId(value['task_id']) ||
207
+ !isText(value['task_root'], 4096) ||
208
+ !path.isAbsolute(value['task_root']) ||
209
+ !isText(value['session_id'], 256) ||
210
+ !isText(value['cwd'], 4096) ||
211
+ !path.isAbsolute(value['cwd']) ||
212
+ !isSafeId(value['subtask_id']) ||
213
+ !isPositiveInt(value['attempt']) ||
214
+ !isPositiveInt(value['owner_epoch']) ||
215
+ !Array.isArray(value['claim_epochs']) ||
216
+ value['claim_epochs'].length === 0 ||
217
+ value['claim_epochs'].length > 64)
218
+ return null;
219
+ const claim_epochs = [];
220
+ const seen = new Set();
221
+ for (const raw of value['claim_epochs']) {
222
+ if (!isRecord(raw) ||
223
+ !hasOnlyKeys(raw, ['claim_id', 'resource_epoch']) ||
224
+ !isSafeId(raw['claim_id']) ||
225
+ !isPositiveInt(raw['resource_epoch']) ||
226
+ seen.has(raw['claim_id']))
227
+ return null;
228
+ seen.add(raw['claim_id']);
229
+ claim_epochs.push({
230
+ claim_id: raw['claim_id'],
231
+ resource_epoch: raw['resource_epoch'],
232
+ });
233
+ }
234
+ return {
235
+ schema: MANAGED_PI_EXECUTION_SCOPE_SCHEMA,
236
+ task_id: value['task_id'],
237
+ task_root: value['task_root'],
238
+ session_id: value['session_id'],
239
+ cwd: value['cwd'],
240
+ subtask_id: value['subtask_id'],
241
+ attempt: value['attempt'],
242
+ owner_epoch: value['owner_epoch'],
243
+ claim_epochs,
244
+ };
245
+ }
246
+ export function validateSessionTransitionRequest(value) {
247
+ if (!isRecord(value) ||
248
+ !hasOnlyKeys(value, [
249
+ 'schema',
250
+ 'task_root',
251
+ 'transition_id',
252
+ 'intent',
253
+ 'bundle',
254
+ 'bootstrap_request',
255
+ 'evidence',
256
+ 'archive_mode',
257
+ 'managed_execution_scope',
258
+ ]))
259
+ return {
260
+ ok: false,
261
+ code: 'request_shape_invalid',
262
+ message: 'Session-transition request must use only documented fields.',
263
+ };
264
+ if (value['schema'] !== COLLAB_SESSION_TRANSITION_REQUEST_SCHEMA)
265
+ return {
266
+ ok: false,
267
+ code: 'request_schema_invalid',
268
+ message: `request.schema must be ${COLLAB_SESSION_TRANSITION_REQUEST_SCHEMA}.`,
269
+ };
270
+ if (!isText(value['task_root'], 4096) || !path.isAbsolute(value['task_root']))
271
+ return {
272
+ ok: false,
273
+ code: 'task_root_invalid',
274
+ message: 'task_root must be an absolute task-root path.',
275
+ };
276
+ const intent = parseIntent(value['intent']);
277
+ if (intent === null)
278
+ return {
279
+ ok: false,
280
+ code: 'intent_invalid',
281
+ message: 'intent must pin exact source/successor identities, attempt, input, epoch and claims.',
282
+ };
283
+ if (value['transition_id'] !== intent.intent_id)
284
+ return {
285
+ ok: false,
286
+ code: 'transition_id_mismatch',
287
+ message: 'transition_id must equal intent.intent_id.',
288
+ };
289
+ const bundleValue = value['bundle'];
290
+ if (!isRecord(bundleValue) ||
291
+ !hasOnlyKeys(bundleValue, [
292
+ 'root',
293
+ 'continuity_task_id',
294
+ 'bundle_id',
295
+ 'digest',
296
+ ]) ||
297
+ !isText(bundleValue['root'], 4096) ||
298
+ !path.isAbsolute(bundleValue['root']) ||
299
+ !isSafeId(bundleValue['continuity_task_id']) ||
300
+ !isSafeId(bundleValue['bundle_id']) ||
301
+ !isDigest(bundleValue['digest']))
302
+ return {
303
+ ok: false,
304
+ code: 'bundle_invalid',
305
+ message: 'bundle must name one absolute workspace root and exact linked bundle digest.',
306
+ };
307
+ const bootstrapValue = value['bootstrap_request'];
308
+ if (!isRecord(bootstrapValue) ||
309
+ !hasOnlyKeys(bootstrapValue, ['ref', 'digest']) ||
310
+ !isText(bootstrapValue['ref'], 4096) ||
311
+ !path.isAbsolute(bootstrapValue['ref']) ||
312
+ !isDigest(bootstrapValue['digest']))
313
+ return {
314
+ ok: false,
315
+ code: 'bootstrap_request_invalid',
316
+ message: 'bootstrap_request must pin one absolute bounded request file and raw digest.',
317
+ };
318
+ const evidenceValue = value['evidence'];
319
+ if (!isRecord(evidenceValue) ||
320
+ !hasOnlyKeys(evidenceValue, ['ack_message_id', 'first_work_report_id']))
321
+ return {
322
+ ok: false,
323
+ code: 'evidence_invalid',
324
+ message: 'evidence must contain only the optional registered ACK and REPORT ids.',
325
+ };
326
+ const ack = evidenceValue['ack_message_id'];
327
+ const report = evidenceValue['first_work_report_id'];
328
+ if ((ack !== null && !isSafeId(ack)) ||
329
+ (report !== null && !isSafeId(report)))
330
+ return {
331
+ ok: false,
332
+ code: 'evidence_id_invalid',
333
+ message: 'Evidence ids must be null or safe registered message ids.',
334
+ };
335
+ if (value['archive_mode'] !== 'task-retirement')
336
+ return {
337
+ ok: false,
338
+ code: 'archive_mode_invalid',
339
+ message: 'Only the reversible task-retirement marker is supported; native archive is unavailable.',
340
+ };
341
+ const scopeValue = value['managed_execution_scope'];
342
+ const managedExecutionScope = scopeValue === undefined ? null : parseManagedPiExecutionScope(scopeValue);
343
+ if (scopeValue !== undefined &&
344
+ (managedExecutionScope === null ||
345
+ managedExecutionScope.task_id !== path.basename(value['task_root']) ||
346
+ !safePathEqual(managedExecutionScope.task_root, value['task_root']) ||
347
+ managedExecutionScope.session_id !== intent.from.session_id ||
348
+ !safePathEqual(managedExecutionScope.cwd, intent.from.cwd ?? '') ||
349
+ managedExecutionScope.subtask_id !== intent.subtask_id ||
350
+ managedExecutionScope.attempt !== intent.attempt ||
351
+ managedExecutionScope.owner_epoch !== intent.coordinator_epoch ||
352
+ managedExecutionScope.claim_epochs.length !== intent.claims.length ||
353
+ managedExecutionScope.claim_epochs.some((pin, index) => pin.claim_id !== intent.claims[index]?.claim_id ||
354
+ pin.resource_epoch !== intent.claims[index]?.resource_epoch)))
355
+ return {
356
+ ok: false,
357
+ code: 'managed_execution_scope_mismatch',
358
+ message: 'Managed execution scope must exactly match task, source session, attempt, owner epoch and claim pins.',
359
+ };
360
+ return {
361
+ ok: true,
362
+ request: {
363
+ schema: COLLAB_SESSION_TRANSITION_REQUEST_SCHEMA,
364
+ task_root: value['task_root'],
365
+ transition_id: value['transition_id'],
366
+ intent,
367
+ bundle: {
368
+ root: bundleValue['root'],
369
+ continuity_task_id: bundleValue['continuity_task_id'],
370
+ bundle_id: bundleValue['bundle_id'],
371
+ digest: bundleValue['digest'],
372
+ },
373
+ bootstrap_request: {
374
+ ref: bootstrapValue['ref'],
375
+ digest: bootstrapValue['digest'],
376
+ },
377
+ evidence: {
378
+ ack_message_id: typeof ack === 'string' ? ack : null,
379
+ first_work_report_id: typeof report === 'string' ? report : null,
380
+ },
381
+ archive_mode: 'task-retirement',
382
+ ...(managedExecutionScope === null
383
+ ? {}
384
+ : { managed_execution_scope: managedExecutionScope }),
385
+ },
386
+ };
387
+ }
388
+ export function loadSessionTransitionRequest(file) {
389
+ const read = readBoundedFile(file, MAX_REQUEST_BYTES);
390
+ if (!read.ok)
391
+ return {
392
+ ok: false,
393
+ kind: 'invalid',
394
+ code: 'request_unreadable',
395
+ message: `Request must be an absolute bounded regular file (${read.code}).`,
396
+ next_action: 'Pass a regular JSON request file; no task mutation was attempted.',
397
+ };
398
+ let parsed;
399
+ try {
400
+ parsed = JSON.parse(read.text);
401
+ }
402
+ catch {
403
+ return {
404
+ ok: false,
405
+ kind: 'invalid',
406
+ code: 'request_invalid_json',
407
+ message: 'Session-transition request is not valid JSON.',
408
+ next_action: 'Fix the request document and retry explicitly.',
409
+ };
410
+ }
411
+ const validated = validateSessionTransitionRequest(parsed);
412
+ if (!validated.ok)
413
+ return { ...validated, kind: 'invalid', next_action: null };
414
+ return {
415
+ ok: true,
416
+ value: {
417
+ request: validated.request,
418
+ file,
419
+ file_digest: read.digest,
420
+ immutable_digest: immutableDigest(validated.request),
421
+ },
422
+ };
423
+ }
424
+ function requestOf(value) {
425
+ return 'request' in value ? value.request : value;
426
+ }
427
+ function immutableDigestOf(value) {
428
+ return 'immutable_digest' in value
429
+ ? value.immutable_digest
430
+ : immutableDigest(value);
431
+ }
432
+ const LIMITATIONS = Object.freeze([
433
+ 'managed-tool freeze is not a native or OS-level stop proof',
434
+ 'native-event queue, approval and in-flight completeness are unknown on the installed Pi API',
435
+ 'Herdr identity metadata is not process affiliation or ownership proof',
436
+ 'a submitted ACK/report pointer is not consumption or acceptance',
437
+ 'native Pi session archive is unavailable; task retirement preserves original session data in place',
438
+ ]);
439
+ function capabilities(host) {
440
+ return {
441
+ record_reducer: 'available',
442
+ bootstrap: 'available',
443
+ pi_host_extension: host === null ? 'not_loaded' : 'available',
444
+ supported_quiescence: host?.proof_support === 'available'
445
+ ? 'available'
446
+ : host === null
447
+ ? 'unknown'
448
+ : 'unavailable',
449
+ native_session_archive: 'unavailable',
450
+ task_retirement_archive: 'available',
451
+ task_route_transfer: 'available',
452
+ notification_route_exclusion: 'pending',
453
+ };
454
+ }
455
+ function causeRef(request) {
456
+ return `rollover:${path.basename(request.task_root)}:${request.transition_id}`;
457
+ }
458
+ function emptyReport(action, request, status, stateRevision, taskId, fields = {}) {
459
+ return {
460
+ schema: COLLAB_SESSION_TRANSITION_REPORT_SCHEMA,
461
+ action,
462
+ ok: status === 'planned' || status === 'progressed' || status === 'waiting',
463
+ status,
464
+ code: null,
465
+ message: null,
466
+ next_action: null,
467
+ task_id: taskId,
468
+ task_root: request.task_root,
469
+ state_revision: stateRevision,
470
+ transition_id: request.transition_id,
471
+ cause_ref: causeRef(request),
472
+ stage: 'not_started',
473
+ intent: null,
474
+ handoff: null,
475
+ rollover: null,
476
+ bootstrap: null,
477
+ receiver_ack: null,
478
+ transfer: null,
479
+ first_work: null,
480
+ route_transfer: null,
481
+ archive: null,
482
+ host: null,
483
+ steps: [],
484
+ capabilities: capabilities(null),
485
+ limits: LIMITATIONS,
486
+ ...fields,
487
+ };
488
+ }
489
+ function step(name, status, code = null, detail = null) {
490
+ return { step: name, status, code, detail };
491
+ }
492
+ function sameIntentRequest(existing, requested, request) {
493
+ const expectedBundleRef = handoffBundleRelativeRef(request.bundle.bundle_id);
494
+ const existingClaims = [...existing.claims]
495
+ .map((claim) => ({
496
+ claim_id: claim.claim_id,
497
+ resource_epoch: claim.resource_epoch,
498
+ }))
499
+ .sort((a, b) => a.claim_id.localeCompare(b.claim_id));
500
+ const requestedClaims = [...requested.claims]
501
+ .map((claim) => ({
502
+ claim_id: claim.claim_id,
503
+ resource_epoch: claim.resource_epoch,
504
+ }))
505
+ .sort((a, b) => a.claim_id.localeCompare(b.claim_id));
506
+ return (existing.intent_id === requested.intent_id &&
507
+ existing.generation === requested.generation &&
508
+ existing.handoff_id === requested.handoff_id &&
509
+ existing.task_id === path.basename(request.task_root) &&
510
+ existing.subtask_id === requested.subtask_id &&
511
+ existing.attempt === requested.attempt &&
512
+ existing.input_revision === requested.input_revision &&
513
+ existing.coordinator_epoch === requested.coordinator_epoch &&
514
+ sameHarnessIdentity(existing.from, requested.from) &&
515
+ sameHarnessIdentity(existing.to, requested.to) &&
516
+ existing.bundle.ref === expectedBundleRef &&
517
+ existing.bundle.digest === request.bundle.digest &&
518
+ JSON.stringify(existingClaims) === JSON.stringify(requestedClaims));
519
+ }
520
+ function getIntent(record, intentId) {
521
+ return ((record.rollover ?? []).find((entry) => entry.intent_id === intentId) ??
522
+ null);
523
+ }
524
+ function getHandoff(record, handoffId) {
525
+ return ((record.handoffs ?? []).find((entry) => entry.handoff_id === handoffId) ??
526
+ null);
527
+ }
528
+ function buildRolloverRequest(request, record) {
529
+ const existing = getIntent(record, request.intent.intent_id);
530
+ return {
531
+ schema: COLLAB_ROLLOVER_REQUEST_SCHEMA,
532
+ action: existing === null ? 'plan' : 'inspect',
533
+ task_root: request.task_root,
534
+ intent_id: existing === null ? null : existing.intent_id,
535
+ proposed_intent: existing === null ? request.intent : null,
536
+ bundle: request.bundle,
537
+ evidence: {
538
+ ack_message_id: request.evidence.ack_message_id,
539
+ report_message_id: request.evidence.first_work_report_id,
540
+ },
541
+ };
542
+ }
543
+ function parseRegisteredMessageEntry(record, registered) {
544
+ return (record.messages ?? []).some((entry) => entry.message_id === registered.message.message_id &&
545
+ entry.content_digest === registered.message.content_digest &&
546
+ ['applied', 'recorded'].includes(entry.state));
547
+ }
548
+ function parseQuiescenceReceipt(value) {
549
+ if (!isRecord(value) || !isRecord(value['freeze_token']))
550
+ return null;
551
+ const token = value['freeze_token'];
552
+ const legacy = value['schema'] === 'pappra/session-transition-quiescence-receipt/1' &&
553
+ value['adapter_id'] === 'pappra/pi-session-transition-host/1';
554
+ const scoped = value['schema'] === MANAGED_PI_SCOPED_RECEIPT_SCHEMA &&
555
+ value['adapter_id'] === MANAGED_PI_SCOPED_PROOF_ADAPTER_ID &&
556
+ isDigest(value['execution_scope_digest']);
557
+ if ((!legacy && !scoped) ||
558
+ !isText(value['session_id'], 256) ||
559
+ !isText(value['cwd'], 4096) ||
560
+ !path.isAbsolute(value['cwd']) ||
561
+ !isText(value['observed_at'], 64) ||
562
+ !Number.isFinite(Date.parse(value['observed_at'])) ||
563
+ !Number.isSafeInteger(value['sequence']) ||
564
+ value['sequence'] < 0 ||
565
+ !isText(value['evidence_ref'], 4096) ||
566
+ !isDigest(value['inventory_digest']) ||
567
+ !isText(token['sessionId'], 256) ||
568
+ !isSafeId(token['taskId']) ||
569
+ !isSafeId(token['handoffId']) ||
570
+ !isPositiveInt(token['generation']) ||
571
+ token['sessionId'] !== value['session_id'])
572
+ return null;
573
+ return {
574
+ schema: value['schema'],
575
+ adapter_id: value['adapter_id'],
576
+ session_id: value['session_id'],
577
+ cwd: value['cwd'],
578
+ observed_at: value['observed_at'],
579
+ sequence: value['sequence'],
580
+ evidence_ref: value['evidence_ref'],
581
+ freeze_token: {
582
+ sessionId: token['sessionId'],
583
+ taskId: token['taskId'],
584
+ handoffId: token['handoffId'],
585
+ generation: token['generation'],
586
+ },
587
+ inventory_digest: value['inventory_digest'],
588
+ ...(scoped
589
+ ? { execution_scope_digest: value['execution_scope_digest'] }
590
+ : {}),
591
+ };
592
+ }
593
+ function readJournal(record) {
594
+ const raw = record[JOURNAL_KEY];
595
+ if (raw === undefined)
596
+ return {
597
+ ok: true,
598
+ value: { schema: 'pappra/session-transition-journal/1', entries: [] },
599
+ };
600
+ if (!isRecord(raw) ||
601
+ raw['schema'] !== 'pappra/session-transition-journal/1' ||
602
+ !Array.isArray(raw['entries']) ||
603
+ raw['entries'].length > MAX_JOURNAL_ENTRIES)
604
+ return { ok: false, code: 'transition_journal_invalid' };
605
+ const entries = [];
606
+ for (const item of raw['entries']) {
607
+ if (!isRecord(item) ||
608
+ !isSafeId(item['transition_id']) ||
609
+ !isDigest(item['request_digest']) ||
610
+ !isDigest(item['bootstrap_request_digest']) ||
611
+ !isText(item['created_at'], 64) ||
612
+ !isText(item['updated_at'], 64) ||
613
+ !isRecord(item['effects']))
614
+ return { ok: false, code: 'transition_journal_invalid' };
615
+ const effects = {};
616
+ for (const [key, value] of Object.entries(item['effects'])) {
617
+ if (!isSafeId(key) ||
618
+ !isRecord(value) ||
619
+ !['pending', 'completed', 'unknown', 'failed'].includes(String(value['state'])) ||
620
+ !isText(value['intent_at'], 64))
621
+ return { ok: false, code: 'transition_journal_invalid' };
622
+ effects[key] = {
623
+ state: value['state'],
624
+ intent_at: value['intent_at'],
625
+ ...(isText(value['outcome_at'], 64)
626
+ ? { outcome_at: value['outcome_at'] }
627
+ : {}),
628
+ ...(isText(value['code'], 128) ? { code: value['code'] } : {}),
629
+ ...(isDigest(value['digest']) ? { digest: value['digest'] } : {}),
630
+ ...(isText(value['ref'], 4096) ? { ref: value['ref'] } : {}),
631
+ };
632
+ }
633
+ const sourceReceiptValue = item['source_quiescence_receipt'];
634
+ const transferReceiptValue = item['transfer_quiescence_receipt'];
635
+ const sourceReceipt = sourceReceiptValue === undefined
636
+ ? null
637
+ : parseQuiescenceReceipt(sourceReceiptValue);
638
+ const transferReceipt = transferReceiptValue === undefined
639
+ ? null
640
+ : parseQuiescenceReceipt(transferReceiptValue);
641
+ if ((sourceReceiptValue !== undefined && sourceReceipt === null) ||
642
+ (transferReceiptValue !== undefined && transferReceipt === null))
643
+ return { ok: false, code: 'transition_journal_invalid' };
644
+ entries.push({
645
+ transition_id: item['transition_id'],
646
+ request_digest: item['request_digest'],
647
+ bootstrap_request_digest: item['bootstrap_request_digest'],
648
+ created_at: item['created_at'],
649
+ updated_at: item['updated_at'],
650
+ effects,
651
+ ...(sourceReceipt === null
652
+ ? {}
653
+ : { source_quiescence_receipt: sourceReceipt }),
654
+ ...(transferReceipt === null
655
+ ? {}
656
+ : { transfer_quiescence_receipt: transferReceipt }),
657
+ ...(isText(item['route_binding_ref'], 4096)
658
+ ? { route_binding_ref: item['route_binding_ref'] }
659
+ : {}),
660
+ ...(isDigest(item['route_binding_digest'])
661
+ ? { route_binding_digest: item['route_binding_digest'] }
662
+ : {}),
663
+ ...(isText(item['archive_ref'], 4096)
664
+ ? { archive_ref: item['archive_ref'] }
665
+ : {}),
666
+ ...(isDigest(item['archive_digest'])
667
+ ? { archive_digest: item['archive_digest'] }
668
+ : {}),
669
+ });
670
+ }
671
+ return {
672
+ ok: true,
673
+ value: { schema: 'pappra/session-transition-journal/1', entries },
674
+ };
675
+ }
676
+ function journalEntry(record, request, requestDigest) {
677
+ const journal = readJournal(record);
678
+ if (!journal.ok)
679
+ return null;
680
+ const entry = journal.value.entries.find((candidate) => candidate.transition_id === request.transition_id);
681
+ if (entry === undefined)
682
+ return null;
683
+ return entry.request_digest === requestDigest &&
684
+ entry.bootstrap_request_digest === request.bootstrap_request.digest
685
+ ? entry
686
+ : null;
687
+ }
688
+ function currentStateDrift(record, request, phase) {
689
+ if (!isTaskRecord(record))
690
+ return {
691
+ code: 'record_invalid',
692
+ message: 'Authoritative task record failed validation.',
693
+ };
694
+ if (record.task_id !== path.basename(request.task_root) ||
695
+ !safePathEqual(record.task_root, request.task_root))
696
+ return {
697
+ code: 'task_namespace_changed',
698
+ message: 'Task identity/root changed during transition.',
699
+ };
700
+ const coordinator = record.coordinator;
701
+ if (coordinator === null ||
702
+ coordinator.owner_epoch !== request.intent.coordinator_epoch)
703
+ return {
704
+ code: 'stale_epoch',
705
+ message: 'Coordinator epoch changed during transition.',
706
+ };
707
+ const attempt = findAttempt(record, request.intent.subtask_id, request.intent.attempt);
708
+ if (attempt === null)
709
+ return {
710
+ code: 'unknown_attempt',
711
+ message: 'The pinned attempt is absent.',
712
+ };
713
+ const latest = (record.attempts ?? [])
714
+ .filter((entry) => entry.subtask_id === request.intent.subtask_id)
715
+ .reduce((max, entry) => Math.max(max, entry.attempt), 0);
716
+ if (latest !== request.intent.attempt)
717
+ return {
718
+ code: 'attempt_superseded',
719
+ message: 'A newer attempt superseded the transition.',
720
+ };
721
+ if (attempt.input_revision !== request.intent.input_revision)
722
+ return {
723
+ code: 'input_changed',
724
+ message: 'The pinned input revision changed.',
725
+ };
726
+ if (record.dispatch_frozen === true)
727
+ return {
728
+ code: 'dispatch_frozen',
729
+ message: 'Dispatch is frozen; no transition effect is authorized.',
730
+ };
731
+ if (attempt.awaiting !== undefined)
732
+ return {
733
+ code: attempt.awaiting === 'approval'
734
+ ? 'awaiting_approval'
735
+ : 'attempt_blocked',
736
+ message: `The attempt is waiting for ${attempt.awaiting}.`,
737
+ };
738
+ if (attempt.cancel_requested_at != null ||
739
+ hasPendingCancellation(record, request.intent.subtask_id, request.intent.attempt))
740
+ return {
741
+ code: 'cancel_unconfirmed',
742
+ message: 'Cancellation is pending or unconfirmed.',
743
+ };
744
+ const transferred = phase !== 'source';
745
+ const expectedIdentity = transferred
746
+ ? request.intent.to
747
+ : request.intent.from;
748
+ if (!sameHarnessIdentity(attempt.assignee, expectedIdentity))
749
+ return {
750
+ code: 'assignee_mismatch',
751
+ message: 'Current assignee differs from the pinned transition stage.',
752
+ };
753
+ if (!transferred &&
754
+ !['assigned', 'accepted', 'running'].includes(attempt.state))
755
+ return {
756
+ code: 'attempt_not_active',
757
+ message: `Source attempt state ${attempt.state} is not active.`,
758
+ };
759
+ const intent = getIntent(record, request.intent.intent_id);
760
+ const expectedNative = transferred
761
+ ? (intent?.successor_native ?? null)
762
+ : (intent?.source_native ?? null);
763
+ if (expectedNative !== null) {
764
+ const native = attempt.native ?? null;
765
+ if (native === null ||
766
+ native.observation !== 'confirmed' ||
767
+ native.session_id !== expectedNative.session_id ||
768
+ native.runner !== expectedNative.runner ||
769
+ native.run_id !== expectedNative.run_id)
770
+ return {
771
+ code: 'native_identity_drift',
772
+ message: 'Current attempt native identity differs from the pinned identity.',
773
+ };
774
+ }
775
+ for (const reference of request.intent.claims) {
776
+ const claim = (record.claims ?? []).find((entry) => entry.claim_id === reference.claim_id);
777
+ if (claim === undefined || claim.released_at !== null)
778
+ return {
779
+ code: 'claim_unavailable',
780
+ message: `Claim ${reference.claim_id} is missing or released.`,
781
+ };
782
+ const expectedEpoch = transferred
783
+ ? reference.resource_epoch + 1
784
+ : reference.resource_epoch;
785
+ if (!sameIdentity(claim.holder, expectedIdentity) ||
786
+ (claim.holder.harness !== null &&
787
+ claim.holder.harness !== expectedIdentity.harness) ||
788
+ (claim.resource_epoch ?? 0) !== expectedEpoch)
789
+ return {
790
+ code: 'claim_drift',
791
+ message: `Claim ${reference.claim_id} holder/epoch changed.`,
792
+ };
793
+ }
794
+ return null;
795
+ }
796
+ function managedExecutionScopeDrift(request, record) {
797
+ const scope = request.managed_execution_scope;
798
+ if (scope === undefined)
799
+ return null;
800
+ if (scope.schema !== MANAGED_PI_EXECUTION_SCOPE_SCHEMA ||
801
+ scope.task_id !== record.task_id ||
802
+ !safePathEqual(scope.task_root, record.task_root) ||
803
+ scope.session_id !== request.intent.from.session_id ||
804
+ !safePathEqual(scope.cwd, request.intent.from.cwd ?? '') ||
805
+ scope.subtask_id !== request.intent.subtask_id ||
806
+ scope.attempt !== request.intent.attempt ||
807
+ scope.owner_epoch !== request.intent.coordinator_epoch ||
808
+ record.coordinator?.owner_epoch !== scope.owner_epoch)
809
+ return {
810
+ code: 'managed_execution_scope_mismatch',
811
+ message: 'The scoped SDK binding no longer matches the task/source pins.',
812
+ };
813
+ const attempt = findAttempt(record, scope.subtask_id, scope.attempt);
814
+ const latestAttempt = (record.attempts ?? [])
815
+ .filter((candidate) => candidate.subtask_id === scope.subtask_id)
816
+ .reduce((latest, candidate) => Math.max(latest, candidate.attempt), 0);
817
+ if (attempt === null ||
818
+ latestAttempt !== scope.attempt ||
819
+ !['assigned', 'accepted', 'running'].includes(attempt.state) ||
820
+ attempt.owner_epoch !== scope.owner_epoch ||
821
+ attempt.assignee.harness !== 'pi' ||
822
+ attempt.assignee.agent_id !== request.intent.from.agent_id ||
823
+ attempt.assignee.session_id !== scope.session_id ||
824
+ (attempt.assignee.cwd !== undefined &&
825
+ !safePathEqual(attempt.assignee.cwd, scope.cwd)))
826
+ return {
827
+ code: 'managed_execution_scope_attempt_stale',
828
+ message: 'The scoped source attempt or owner epoch changed.',
829
+ };
830
+ if (scope.claim_epochs.length === 0 ||
831
+ scope.claim_epochs.length !== request.intent.claims.length)
832
+ return {
833
+ code: 'managed_execution_scope_claims_invalid',
834
+ message: 'The scope must pin every transition claim.',
835
+ };
836
+ for (let index = 0; index < scope.claim_epochs.length; index++) {
837
+ const pin = scope.claim_epochs[index];
838
+ const intentPin = request.intent.claims[index];
839
+ const claim = (record.claims ?? []).find((entry) => entry.claim_id === pin.claim_id);
840
+ if (pin.resource_epoch <= 0 ||
841
+ pin.claim_id !== intentPin?.claim_id ||
842
+ pin.resource_epoch !== intentPin?.resource_epoch ||
843
+ claim === undefined ||
844
+ claim.released_at !== null ||
845
+ (claim.resource_epoch ?? 0) !== pin.resource_epoch ||
846
+ claim.holder.agent_id !== request.intent.from.agent_id ||
847
+ claim.holder.session_id !== scope.session_id ||
848
+ claim.holder.harness !== 'pi')
849
+ return {
850
+ code: 'managed_execution_scope_claim_stale',
851
+ message: `Scoped claim ${pin.claim_id} is missing, released, reassigned, or at a different epoch.`,
852
+ };
853
+ }
854
+ return null;
855
+ }
856
+ function exactQuiescenceReceipt(request, receipt) {
857
+ const scoped = request.managed_execution_scope;
858
+ const expectedScopeDigest = scoped === undefined ? null : digestText(canonicalJson(scoped));
859
+ return (receipt !== undefined &&
860
+ (scoped === undefined
861
+ ? receipt.schema === 'pappra/session-transition-quiescence-receipt/1' &&
862
+ receipt.adapter_id === 'pappra/pi-session-transition-host/1'
863
+ : receipt.schema === MANAGED_PI_SCOPED_RECEIPT_SCHEMA &&
864
+ receipt.adapter_id === MANAGED_PI_SCOPED_PROOF_ADAPTER_ID &&
865
+ receipt.execution_scope_digest === expectedScopeDigest) &&
866
+ receipt.session_id === request.intent.from.session_id &&
867
+ safePathEqual(receipt.cwd, request.intent.from.cwd ?? '') &&
868
+ isText(receipt.evidence_ref, 4096) &&
869
+ isDigest(receipt.inventory_digest) &&
870
+ Number.isSafeInteger(receipt.sequence) &&
871
+ receipt.freeze_token.sessionId === request.intent.from.session_id &&
872
+ receipt.freeze_token.taskId === path.basename(request.task_root) &&
873
+ receipt.freeze_token.handoffId === request.intent.handoff_id &&
874
+ receipt.freeze_token.generation === request.intent.generation);
875
+ }
876
+ function exactRouteBindingReceipt(request, entry) {
877
+ if (entry === undefined ||
878
+ !isText(entry.route_binding_ref, 4096) ||
879
+ !isDigest(entry.route_binding_digest) ||
880
+ entry.effects['successor-route-binding']?.state !== 'completed' ||
881
+ entry.effects['successor-route-binding'].ref !== entry.route_binding_ref ||
882
+ entry.effects['successor-route-binding'].digest !==
883
+ entry.route_binding_digest)
884
+ return false;
885
+ const read = readTaskLocalFile(entry.route_binding_ref, request.task_root, 64 * 1024);
886
+ if (!read.ok || read.digest !== entry.route_binding_digest)
887
+ return false;
888
+ let parsed;
889
+ try {
890
+ parsed = JSON.parse(read.bytes.toString('utf8'));
891
+ }
892
+ catch {
893
+ return false;
894
+ }
895
+ const binding = collabBindingSchema.safeParse(parsed);
896
+ if (!binding.success ||
897
+ binding.data.task_id !== path.basename(request.task_root) ||
898
+ !safePathEqual(binding.data.task_root, request.task_root) ||
899
+ binding.data.participants.length !== 1)
900
+ return false;
901
+ const participant = binding.data.participants[0];
902
+ return (participant.agent_id === request.intent.to.agent_id &&
903
+ participant.harness === request.intent.to.harness &&
904
+ participant.native_session_id === request.intent.to.session_id &&
905
+ safePathEqual(participant.cwd, request.intent.to.cwd ?? ''));
906
+ }
907
+ function exactRetirementReceipt(request, record, entry, succession) {
908
+ if (entry === undefined ||
909
+ !isText(entry.archive_ref, 4096) ||
910
+ !isDigest(entry.archive_digest) ||
911
+ entry.effects['task-retirement-marker']?.state !== 'completed' ||
912
+ entry.effects['task-retirement-marker'].ref !== entry.archive_ref ||
913
+ entry.effects['task-retirement-marker'].digest !== entry.archive_digest ||
914
+ !exactRouteBindingReceipt(request, entry))
915
+ return false;
916
+ const read = readTaskLocalFile(entry.archive_ref, request.task_root, MAX_ARCHIVE_BYTES);
917
+ if (!read.ok || read.digest !== entry.archive_digest)
918
+ return false;
919
+ let parsed;
920
+ try {
921
+ parsed = JSON.parse(read.bytes.toString('utf8'));
922
+ }
923
+ catch {
924
+ return false;
925
+ }
926
+ if (!isRecord(parsed) ||
927
+ !isRecord(parsed['first_work']) ||
928
+ !isRecord(parsed['successor_route_binding']))
929
+ return false;
930
+ return (parsed['schema'] === SESSION_TRANSITION_ARCHIVE_SCHEMA &&
931
+ parsed['mode'] === 'task-retirement' &&
932
+ parsed['task_id'] === record.task_id &&
933
+ safePathEqual(String(parsed['task_root'] ?? ''), request.task_root) &&
934
+ parsed['transition_id'] === request.transition_id &&
935
+ parsed['intent_id'] === request.intent.intent_id &&
936
+ parsed['handoff_id'] === request.intent.handoff_id &&
937
+ parsed['generation'] === request.intent.generation &&
938
+ isText(parsed['recorded_at'], 64) &&
939
+ parsed['first_work']['message_id'] === succession.proof.message_id &&
940
+ parsed['first_work']['digest'] === succession.proof.digest &&
941
+ parsed['successor_route_binding']['ref'] === entry.route_binding_ref &&
942
+ parsed['successor_route_binding']['digest'] ===
943
+ entry.route_binding_digest &&
944
+ parsed['native_archive'] === 'unavailable' &&
945
+ parsed['original_transcript'] === 'retained-in-place' &&
946
+ parsed['reversible'] === true);
947
+ }
948
+ function reportForContext(action, request, record, rolloverReport, bootstrapReport, hostObservation, steps, status, extra = {}) {
949
+ const intent = getIntent(record, request.intent.intent_id);
950
+ const handoff = getHandoff(record, request.intent.handoff_id);
951
+ const succession = (record.successions ?? []).find((entry) => entry.intent_id === request.intent.intent_id &&
952
+ entry.handoff_id === request.intent.handoff_id &&
953
+ entry.generation === request.intent.generation) ?? null;
954
+ const successionValidation = succession === null ? null : validateSuccession(record, succession);
955
+ const successionValid = successionValidation?.ok === true;
956
+ const routeExclusion = routeExclusionFor(record, request.intent.from);
957
+ const journal = readJournal(record);
958
+ const journalCandidate = journal.ok
959
+ ? journal.value.entries.find((entry) => entry.transition_id === request.transition_id)
960
+ : undefined;
961
+ const journalItem = journalCandidate?.request_digest === immutableDigest(request) &&
962
+ journalCandidate.bootstrap_request_digest ===
963
+ request.bootstrap_request.digest
964
+ ? journalCandidate
965
+ : undefined;
966
+ const routeBindingValid = exactRouteBindingReceipt(request, journalItem);
967
+ const archiveValid = successionValid &&
968
+ succession !== null &&
969
+ exactRetirementReceipt(request, record, journalItem, succession);
970
+ const archiveRef = archiveValid ? (journalItem?.archive_ref ?? null) : null;
971
+ const sourceQuiescenceReceipt = journalItem?.source_quiescence_receipt;
972
+ const transferQuiescenceReceipt = journalItem?.transfer_quiescence_receipt;
973
+ const loadedBootstrap = loadVisibleBootstrapRequest(request.bootstrap_request.ref);
974
+ const bootstrapReservation = loadedBootstrap.ok
975
+ ? (record.launch_reservations ?? []).find((entry) => bootstrapData(entry)['request_id'] ===
976
+ loadedBootstrap.value.request.request_id)
977
+ : undefined;
978
+ const bootstrapReservationData = bootstrapReservation === undefined
979
+ ? {}
980
+ : bootstrapData(bootstrapReservation);
981
+ const bootstrapCompleted = bootstrapReservation?.phase === 'completed' &&
982
+ ['submitted', 'already_recorded'].includes(String(bootstrapReservation.outcome)) &&
983
+ bootstrapReservationData['phase'] === 'completed';
984
+ const stage = archiveValid
985
+ ? 'task_retired'
986
+ : successionValid
987
+ ? 'first_work_recorded'
988
+ : succession !== null
989
+ ? 'unknown'
990
+ : handoff?.state === 'completed'
991
+ ? 'awaiting_first_work'
992
+ : handoff?.state === 'receiver_confirmed'
993
+ ? 'receiver_confirmed'
994
+ : handoff?.state === 'transferring'
995
+ ? bootstrapReservation === undefined
996
+ ? 'prepared'
997
+ : bootstrapCompleted
998
+ ? 'awaiting_receiver_ack'
999
+ : 'successor_bootstrap'
1000
+ : handoff?.state === 'old_writer_stopped'
1001
+ ? 'prepared'
1002
+ : handoff?.state === 'requested'
1003
+ ? 'awaiting_quiescence'
1004
+ : intent !== null
1005
+ ? 'intent_created'
1006
+ : 'not_started';
1007
+ return {
1008
+ schema: COLLAB_SESSION_TRANSITION_REPORT_SCHEMA,
1009
+ action,
1010
+ ok: status === 'planned' || status === 'progressed' || status === 'waiting',
1011
+ status,
1012
+ code: null,
1013
+ message: null,
1014
+ next_action: null,
1015
+ task_id: record.task_id,
1016
+ task_root: request.task_root,
1017
+ state_revision: record.state_revision,
1018
+ transition_id: request.transition_id,
1019
+ cause_ref: `rollover:${record.task_id}:${request.transition_id}`,
1020
+ stage,
1021
+ intent: intent === null
1022
+ ? null
1023
+ : {
1024
+ intent_id: intent.intent_id,
1025
+ generation: intent.generation,
1026
+ handoff_id: intent.handoff_id,
1027
+ subtask_id: intent.subtask_id,
1028
+ attempt: intent.attempt,
1029
+ input_revision: intent.input_revision,
1030
+ from: intent.from,
1031
+ to: intent.to,
1032
+ source_native: intent.source_native,
1033
+ successor_native: intent.successor_native ?? null,
1034
+ transfer_boundary: intent.transfer_boundary ?? null,
1035
+ },
1036
+ handoff: handoff === null
1037
+ ? null
1038
+ : {
1039
+ handoff_id: handoff.handoff_id,
1040
+ state: handoff.state,
1041
+ from: handoff.from,
1042
+ to: handoff.to,
1043
+ owner_epoch: handoff.owner_epoch,
1044
+ },
1045
+ rollover: rolloverReport,
1046
+ bootstrap: bootstrapReport,
1047
+ receiver_ack: handoff?.confirmation_message_id
1048
+ ? { message_id: handoff.confirmation_message_id, status: 'confirmed' }
1049
+ : null,
1050
+ transfer: intent?.transfer_boundary
1051
+ ? {
1052
+ status: 'core-transfer-recorded',
1053
+ boundary: intent.transfer_boundary,
1054
+ claim_count: request.intent.claims.length,
1055
+ source_quiescence_receipt: exactQuiescenceReceipt(request, sourceQuiescenceReceipt)
1056
+ ? sourceQuiescenceReceipt
1057
+ : null,
1058
+ transfer_quiescence_receipt: exactQuiescenceReceipt(request, transferQuiescenceReceipt)
1059
+ ? transferQuiescenceReceipt
1060
+ : null,
1061
+ }
1062
+ : null,
1063
+ first_work: succession === null
1064
+ ? null
1065
+ : successionValid
1066
+ ? {
1067
+ status: 'core-first-work-recorded',
1068
+ message_id: succession.proof.message_id,
1069
+ digest: succession.proof.digest,
1070
+ }
1071
+ : {
1072
+ status: 'succession-reconcile-required',
1073
+ message_id: succession.proof.message_id,
1074
+ digest: succession.proof.digest,
1075
+ },
1076
+ route_transfer: intent?.transfer_boundary
1077
+ ? {
1078
+ status: routeBindingValid
1079
+ ? 'successor-binding-written'
1080
+ : journalItem?.route_binding_ref
1081
+ ? 'successor-binding-reconcile-required'
1082
+ : 'task-assignee-and-claims-transferred',
1083
+ binding_ref: journalItem?.route_binding_ref ?? null,
1084
+ binding_digest: journalItem?.route_binding_digest ?? null,
1085
+ notification_route_exclusion: routeExclusion.state,
1086
+ }
1087
+ : null,
1088
+ archive: succession === null
1089
+ ? null
1090
+ : {
1091
+ status: !successionValid
1092
+ ? 'succession-reconcile-required'
1093
+ : archiveValid
1094
+ ? 'task-retirement-marker-recorded'
1095
+ : journalItem?.archive_ref
1096
+ ? 'task-retirement-reconcile-required'
1097
+ : 'task-history-retained',
1098
+ ref: archiveValid ? archiveRef : (journalItem?.archive_ref ?? null),
1099
+ digest: archiveValid ? (journalItem?.archive_digest ?? null) : null,
1100
+ native_archive: 'unavailable',
1101
+ original_transcript: 'retained-in-place',
1102
+ reversible: true,
1103
+ },
1104
+ host: hostObservation === null
1105
+ ? null
1106
+ : {
1107
+ session_id: hostObservation.session_id,
1108
+ cwd: hostObservation.cwd,
1109
+ pane_id: hostObservation.pane_id,
1110
+ proof_support: hostObservation.proof_support,
1111
+ proof_code: hostObservation.unavailable_code,
1112
+ freeze_active: hostObservation.freeze_status?.frozen ?? false,
1113
+ native_facts: hostObservation.native_facts,
1114
+ process_affiliation: 'not-established',
1115
+ },
1116
+ steps,
1117
+ capabilities: {
1118
+ record_reducer: 'available',
1119
+ bootstrap: 'available',
1120
+ pi_host_extension: hostObservation === null ? 'not_loaded' : 'available',
1121
+ supported_quiescence: hostObservation?.proof_support === 'available'
1122
+ ? 'available'
1123
+ : hostObservation === null
1124
+ ? 'unknown'
1125
+ : 'unavailable',
1126
+ native_session_archive: 'unavailable',
1127
+ task_retirement_archive: 'available',
1128
+ task_route_transfer: 'available',
1129
+ notification_route_exclusion: routeExclusion.state,
1130
+ },
1131
+ limits: LIMITATIONS,
1132
+ ...extra,
1133
+ };
1134
+ }
1135
+ function failureReport(action, request, status, code, message, nextAction, task = null, details = {}) {
1136
+ const base = task === null
1137
+ ? emptyReport(action, request, status, null, null)
1138
+ : reportForContext(action, request, task.record, null, null, null, [], status);
1139
+ return {
1140
+ ...base,
1141
+ ok: false,
1142
+ status,
1143
+ code,
1144
+ message,
1145
+ next_action: nextAction,
1146
+ ...details,
1147
+ };
1148
+ }
1149
+ function bootstrapExecutor(dependencies) {
1150
+ if (dependencies.bootstrap !== undefined)
1151
+ return dependencies.bootstrap;
1152
+ return {
1153
+ plan: (request) => planVisibleBootstrap(request, dependencies.bootstrapDependencies),
1154
+ apply: (request) => applyVisibleBootstrap(request, dependencies.bootstrapDependencies),
1155
+ };
1156
+ }
1157
+ function validateBootstrapTransition(request, bootstrap, contact, record) {
1158
+ const next = bootstrap.request;
1159
+ if (!safePathEqual(next.task_root, request.task_root) ||
1160
+ next.target.choice !== 'sibling' ||
1161
+ next.participant.agent_id !== request.intent.to.agent_id ||
1162
+ next.participant.native_session_selector !== request.intent.to.session_id ||
1163
+ next.participant.harness !== request.intent.to.harness ||
1164
+ next.target.cwd !== request.intent.to.cwd ||
1165
+ next.admission.subtask_id !== request.intent.subtask_id ||
1166
+ next.admission.attempt !== request.intent.attempt ||
1167
+ next.admission.input_revision !== request.intent.input_revision ||
1168
+ next.binding_ref === undefined)
1169
+ return failure('invalid', 'bootstrap_transition_mismatch', 'Successor bootstrap identity, attempt, cwd or binding does not match the transition.', null);
1170
+ const bindingFile = readTaskLocalFile(next.binding_ref, record.task_root, 64 * 1024);
1171
+ if (!bindingFile.ok)
1172
+ return failure('needs_input', 'bootstrap_binding_unavailable', 'The exact source routing binding is missing, unreadable or outside the task root.', 'Provide one bounded task-local pappra/collab-binding/1 file; no focus-based fallback is allowed.');
1173
+ let bindingValue;
1174
+ try {
1175
+ bindingValue = JSON.parse(bindingFile.bytes.toString('utf8'));
1176
+ }
1177
+ catch {
1178
+ return failure('invalid', 'bootstrap_binding_invalid', 'The source routing binding is not valid JSON.', null);
1179
+ }
1180
+ const binding = collabBindingSchema.safeParse(bindingValue);
1181
+ if (!binding.success ||
1182
+ binding.data.task_id !== record.task_id ||
1183
+ !safePathEqual(binding.data.task_root, record.task_root))
1184
+ return failure('needs_input', 'bootstrap_binding_invalid', 'The source routing binding schema or task identity does not match.', 'Reconcile the task-local binding before transition.');
1185
+ const sourceParticipants = binding.data.participants.filter((participant) => participant.agent_id === request.intent.from.agent_id);
1186
+ if (sourceParticipants.length !== 1 ||
1187
+ sourceParticipants[0].harness !== request.intent.from.harness ||
1188
+ sourceParticipants[0].native_session_id !==
1189
+ request.intent.from.session_id ||
1190
+ !safePathEqual(sourceParticipants[0].cwd, request.intent.from.cwd ?? '') ||
1191
+ (next.caller.pane_id !== null &&
1192
+ sourceParticipants[0].pane_id !== next.caller.pane_id) ||
1193
+ binding.data.participants.some((participant) => participant.agent_id === request.intent.to.agent_id))
1194
+ return failure('needs_input', 'bootstrap_source_binding_mismatch', 'The task-local binding does not uniquely identify the pinned caller or already contains the sibling successor.', 'Reconcile the exact source/successor binding; no focus or silent reuse fallback is allowed.');
1195
+ const message = contact.message;
1196
+ if (contact.digest !== next.message_pointer.content_digest ||
1197
+ contact.file !== next.message_pointer.content_ref ||
1198
+ !parseRegisteredMessageEntry(record, contact))
1199
+ return failure('needs_input', 'bootstrap_pointer_not_registered', 'Bootstrap pointer is not the exact registered message file/content.', 'Register and apply a task-local transition status pointer before launching the successor.');
1200
+ if (message.type !== 'STATUS' ||
1201
+ message.task_id !== record.task_id ||
1202
+ message.subtask_id !== request.intent.subtask_id ||
1203
+ message.attempt !== request.intent.attempt ||
1204
+ message.owner_epoch !== request.intent.coordinator_epoch ||
1205
+ !sameHarnessIdentity(message.from, request.intent.from) ||
1206
+ !sameHarnessIdentity(message.to, request.intent.to) ||
1207
+ message.body?.['handoff_id'] !== request.intent.handoff_id ||
1208
+ message.body?.['rollover_intent_id'] !== request.intent.intent_id ||
1209
+ message.body?.['rollover_generation'] !== request.intent.generation ||
1210
+ message.body?.['input_revision'] !== request.intent.input_revision)
1211
+ return failure('needs_input', 'bootstrap_pointer_correlation_mismatch', 'Registered STATUS pointer does not correlate to the exact transition.', 'Register a STATUS pointer with matching identities, handoff, generation and input.');
1212
+ return null;
1213
+ }
1214
+ async function loadContext(input) {
1215
+ const request = requestOf(input);
1216
+ const requestDigest = immutableDigestOf(input);
1217
+ const task = await openTaskStore(request.task_root);
1218
+ if (!task.ok)
1219
+ return failure('needs_input', task.code, task.message, task.nextAction);
1220
+ if (!safePathEqual(task.value.taskRoot, request.task_root))
1221
+ return failure('invalid', 'task_root_mismatch', 'Request task_root is not the canonical task root.', 'Use the canonical task root.');
1222
+ const loadedBootstrap = loadVisibleBootstrapRequest(request.bootstrap_request.ref);
1223
+ if (!loadedBootstrap.ok)
1224
+ return failure('invalid', loadedBootstrap.code, loadedBootstrap.message, null);
1225
+ if (loadedBootstrap.value.digest !== request.bootstrap_request.digest)
1226
+ return failure('invalid', 'bootstrap_request_digest_mismatch', 'The pinned bootstrap request digest changed.', null);
1227
+ if (loadedBootstrap.value.request.managed_runtime) {
1228
+ try {
1229
+ verifyManagedPiSdkInstallation(loadedBootstrap.value.request.managed_runtime.sdk_module_path);
1230
+ }
1231
+ catch {
1232
+ return failure('needs_input', 'successor_sdk_incompatible', 'The successor SDK is unavailable or incompatible; no source freeze was attempted.', 'Select a supported SDK installation and re-plan before freezing the source.');
1233
+ }
1234
+ }
1235
+ const messageTask = await openMessageTask(task.value.taskRoot);
1236
+ if (!messageTask.ok)
1237
+ return failure('needs_input', messageTask.code, messageTask.message, messageTask.nextAction);
1238
+ const contact = await readRegisteredMessageSafe(messageTask.value, loadedBootstrap.value.request.message_pointer.message_id);
1239
+ if (!contact.ok)
1240
+ return failure('needs_input', contact.code, contact.message, contact.nextAction);
1241
+ const transitionCheck = validateBootstrapTransition(request, loadedBootstrap.value, contact.value, task.value.record);
1242
+ if (transitionCheck !== null)
1243
+ return transitionCheck;
1244
+ const changed = await assertTaskStoreCurrent(task.value);
1245
+ if (changed !== null)
1246
+ return failure('needs_input', changed.code, changed.message, changed.nextAction);
1247
+ return {
1248
+ ok: true,
1249
+ value: {
1250
+ request,
1251
+ requestDigest,
1252
+ task: task.value,
1253
+ bootstrap: loadedBootstrap.value,
1254
+ contactMessage: contact.value,
1255
+ },
1256
+ };
1257
+ }
1258
+ function hasJournalConflict(record, request, requestDigest) {
1259
+ const journal = readJournal(record);
1260
+ if (!journal.ok)
1261
+ return true;
1262
+ const entry = journal.value.entries.find((candidate) => candidate.transition_id === request.transition_id);
1263
+ return (entry !== undefined &&
1264
+ (entry.request_digest !== requestDigest ||
1265
+ entry.bootstrap_request_digest !== request.bootstrap_request.digest));
1266
+ }
1267
+ function validatePreflight(context, now) {
1268
+ const attempt = findAttempt(context.task.record, context.request.intent.subtask_id, context.request.intent.attempt);
1269
+ if (attempt === null ||
1270
+ attempt.assignment_ref === null ||
1271
+ attempt.assignment_ref === undefined)
1272
+ return {
1273
+ ok: false,
1274
+ code: 'assignment_missing',
1275
+ message: 'Current attempt has no assignment reference.',
1276
+ };
1277
+ let assignmentFile;
1278
+ try {
1279
+ assignmentFile = readManagedSourceAssignment(context.task.record, attempt, context.request.intent.from).file;
1280
+ }
1281
+ catch {
1282
+ return {
1283
+ ok: false,
1284
+ code: 'source_admission_unverified',
1285
+ message: 'The source assignment does not match its immutable admission lineage.',
1286
+ };
1287
+ }
1288
+ const result = runCollaborationPreflight({
1289
+ assignmentFile,
1290
+ capabilitiesFile: context.bootstrap.request.admission.capability_card_ref,
1291
+ maxAgeMinutes: 30,
1292
+ now,
1293
+ });
1294
+ return result.status === 'ready'
1295
+ ? { ok: true, report: result }
1296
+ : {
1297
+ ok: false,
1298
+ code: result.blockers[0]?.code ?? result.errors[0] ?? 'admission_not_ready',
1299
+ message: `Current collaboration preflight is ${result.status}.`,
1300
+ };
1301
+ }
1302
+ function sourceStateFailure(record, request) {
1303
+ return currentStateDrift(record, request, 'source');
1304
+ }
1305
+ function addJournalEntry(record, request, requestDigest, at) {
1306
+ const loaded = readJournal(record);
1307
+ if (!loaded.ok)
1308
+ return loaded;
1309
+ const existing = loaded.value.entries.find((entry) => entry.transition_id === request.transition_id);
1310
+ if (existing !== undefined) {
1311
+ if (existing.request_digest !== requestDigest ||
1312
+ existing.bootstrap_request_digest !== request.bootstrap_request.digest)
1313
+ return { ok: false, code: 'transition_request_conflict' };
1314
+ return { ok: true };
1315
+ }
1316
+ if (loaded.value.entries.length >= MAX_JOURNAL_ENTRIES)
1317
+ return { ok: false, code: 'transition_journal_limit' };
1318
+ const entry = {
1319
+ transition_id: request.transition_id,
1320
+ request_digest: requestDigest,
1321
+ bootstrap_request_digest: request.bootstrap_request.digest,
1322
+ created_at: at,
1323
+ updated_at: at,
1324
+ effects: {},
1325
+ };
1326
+ const journal = {
1327
+ schema: 'pappra/session-transition-journal/1',
1328
+ entries: [...loaded.value.entries, entry],
1329
+ };
1330
+ const encoded = JSON.stringify(journal);
1331
+ if (Buffer.byteLength(encoded) > MAX_JOURNAL_BYTES)
1332
+ return { ok: false, code: 'transition_journal_limit' };
1333
+ record[JOURNAL_KEY] = journal;
1334
+ return { ok: true };
1335
+ }
1336
+ function setJournalEffect(record, transitionId, input) {
1337
+ const loaded = readJournal(record);
1338
+ if (!loaded.ok)
1339
+ return loaded;
1340
+ const entries = [...loaded.value.entries];
1341
+ const index = entries.findIndex((entry) => entry.transition_id === transitionId);
1342
+ if (index < 0)
1343
+ return { ok: false, code: 'transition_journal_missing' };
1344
+ const current = entries[index];
1345
+ const effects = {
1346
+ ...current.effects,
1347
+ [input.name]: {
1348
+ state: input.state,
1349
+ intent_at: current.effects[input.name]?.intent_at ?? input.at,
1350
+ ...(input.state === 'pending' ? {} : { outcome_at: input.at }),
1351
+ ...(input.code === undefined ? {} : { code: input.code }),
1352
+ ...(input.digest === undefined ? {} : { digest: input.digest }),
1353
+ ...(input.ref === undefined ? {} : { ref: input.ref }),
1354
+ },
1355
+ };
1356
+ let updated = {
1357
+ ...current,
1358
+ updated_at: input.at,
1359
+ effects,
1360
+ };
1361
+ if (input.state === 'completed' && input.artifact === 'route-binding')
1362
+ updated = {
1363
+ ...updated,
1364
+ route_binding_ref: input.ref ?? null,
1365
+ route_binding_digest: input.digest ?? null,
1366
+ };
1367
+ if (input.state === 'completed' && input.artifact === 'task-retirement')
1368
+ updated = {
1369
+ ...updated,
1370
+ archive_ref: input.ref ?? null,
1371
+ archive_digest: input.digest ?? null,
1372
+ };
1373
+ entries[index] = updated;
1374
+ const journal = { ...loaded.value, entries };
1375
+ if (Buffer.byteLength(JSON.stringify(journal)) > MAX_JOURNAL_BYTES)
1376
+ return { ok: false, code: 'transition_journal_limit' };
1377
+ record[JOURNAL_KEY] = journal;
1378
+ return { ok: true };
1379
+ }
1380
+ function setJournalQuiescenceReceipt(record, transitionId, stage, receipt) {
1381
+ const loaded = readJournal(record);
1382
+ if (!loaded.ok)
1383
+ return loaded;
1384
+ const entries = [...loaded.value.entries];
1385
+ const index = entries.findIndex((entry) => entry.transition_id === transitionId);
1386
+ if (index < 0)
1387
+ return { ok: false, code: 'transition_journal_missing' };
1388
+ const current = entries[index];
1389
+ const key = stage === 'source'
1390
+ ? 'source_quiescence_receipt'
1391
+ : 'transfer_quiescence_receipt';
1392
+ const prior = current[key];
1393
+ if (prior !== undefined && canonicalJson(prior) !== canonicalJson(receipt))
1394
+ return { ok: false, code: `${stage}_quiescence_receipt_conflict` };
1395
+ const updated = {
1396
+ ...current,
1397
+ updated_at: receipt.observed_at,
1398
+ [key]: receipt,
1399
+ };
1400
+ entries[index] = updated;
1401
+ const journal = { ...loaded.value, entries };
1402
+ if (Buffer.byteLength(JSON.stringify(journal)) > MAX_JOURNAL_BYTES)
1403
+ return { ok: false, code: 'transition_journal_limit' };
1404
+ record[JOURNAL_KEY] = journal;
1405
+ return { ok: true };
1406
+ }
1407
+ function intentTransaction(context, dependencies, now) {
1408
+ return Promise.resolve(transact(context.task.taskRoot, {
1409
+ actor: context.request.intent.from.agent_id,
1410
+ purpose: 'session-transition:create-intent',
1411
+ expectRevision: context.task.record.state_revision,
1412
+ clock: now(),
1413
+ apply: (record) => {
1414
+ if (dependencies.signal?.aborted)
1415
+ return { ok: false, reason: 'abort_requested' };
1416
+ const drift = sourceStateFailure(record, context.request);
1417
+ if (drift !== null)
1418
+ return { ok: false, reason: drift.code };
1419
+ const existing = getIntent(record, context.request.intent.intent_id);
1420
+ if (existing !== null) {
1421
+ if (!sameIntentRequest(existing, context.request.intent, context.request))
1422
+ return { ok: false, reason: 'intent_input_conflict' };
1423
+ const journal = addJournalEntry(record, context.request, context.requestDigest, nowIso(now));
1424
+ if (!journal.ok)
1425
+ return { ok: false, reason: journal.code };
1426
+ return {
1427
+ record,
1428
+ op: 'session-transition-intent-replay',
1429
+ summary: { transition_id: context.request.transition_id },
1430
+ result: { replayed: true },
1431
+ };
1432
+ }
1433
+ const created = rollover.createRolloverIntent(record, {
1434
+ ...context.request.intent,
1435
+ bundle: {
1436
+ ref: handoffBundleRelativeRef(context.request.bundle.bundle_id),
1437
+ digest: context.request.bundle.digest,
1438
+ },
1439
+ clock: now(),
1440
+ });
1441
+ if (!created.ok)
1442
+ return created;
1443
+ const journal = addJournalEntry(record, context.request, context.requestDigest, nowIso(now));
1444
+ if (!journal.ok)
1445
+ return { ok: false, reason: journal.code };
1446
+ return {
1447
+ record,
1448
+ op: 'session-transition-intent',
1449
+ summary: { transition_id: context.request.transition_id },
1450
+ result: { replayed: false },
1451
+ };
1452
+ },
1453
+ }));
1454
+ }
1455
+ function effectPhase(name) {
1456
+ if (name === 'source-freeze')
1457
+ return 'source';
1458
+ if (name === 'successor-route-binding')
1459
+ return 'successor';
1460
+ return 'retired';
1461
+ }
1462
+ async function recordEffectIntent(context, currentTask, name, now, signal) {
1463
+ if (signal?.aborted)
1464
+ return {
1465
+ ok: false,
1466
+ kind: 'needs_input',
1467
+ code: 'abort_requested',
1468
+ message: 'The transition was aborted before the effect intent was committed.',
1469
+ nextAction: 'Reconcile the current task before a new explicit apply.',
1470
+ };
1471
+ const current = await assertTaskStoreCurrent(currentTask);
1472
+ if (current !== null)
1473
+ return {
1474
+ ok: false,
1475
+ kind: 'needs_input',
1476
+ code: current.code,
1477
+ message: current.message,
1478
+ nextAction: current.nextAction,
1479
+ };
1480
+ if (hasJournalConflict(currentTask.record, context.request, context.requestDigest))
1481
+ return {
1482
+ ok: false,
1483
+ kind: 'invalid',
1484
+ code: 'transition_journal_conflict',
1485
+ message: 'The durable journal is invalid or pins different immutable request inputs.',
1486
+ nextAction: 'Inspect the task record; do not perform the effect.',
1487
+ };
1488
+ const journal = journalEntry(currentTask.record, context.request, context.requestDigest);
1489
+ if (journal === null)
1490
+ return {
1491
+ ok: false,
1492
+ kind: 'needs_input',
1493
+ code: 'transition_journal_missing',
1494
+ message: 'The transition effect has no matching durable intent journal.',
1495
+ nextAction: 'Inspect the task record before any effect.',
1496
+ };
1497
+ const prior = journal.effects[name];
1498
+ if (prior !== undefined) {
1499
+ if (prior.state === 'completed')
1500
+ return { ok: true, task: currentTask, replayed: true };
1501
+ return {
1502
+ ok: false,
1503
+ kind: 'needs_input',
1504
+ code: prior.state === 'failed'
1505
+ ? `${name.replaceAll('-', '_')}_failed`
1506
+ : `${name.replaceAll('-', '_')}_reconcile_required`,
1507
+ message: `The ${name} effect already has a ${prior.state} journal receipt; it will not be replayed.`,
1508
+ nextAction: 'Reconcile the exact host/file effect and journal before continuing.',
1509
+ };
1510
+ }
1511
+ const outcome = transact(currentTask.taskRoot, {
1512
+ actor: context.request.intent.from.agent_id,
1513
+ purpose: `session-transition:${name}:intent`,
1514
+ expectRevision: currentTask.record.state_revision,
1515
+ clock: now(),
1516
+ apply: (record) => {
1517
+ if (signal?.aborted)
1518
+ return { ok: false, reason: 'abort_requested' };
1519
+ const drift = currentStateDrift(record, context.request, effectPhase(name));
1520
+ if (drift !== null)
1521
+ return { ok: false, reason: drift.code };
1522
+ if (hasJournalConflict(record, context.request, context.requestDigest))
1523
+ return { ok: false, reason: 'transition_journal_conflict' };
1524
+ const entry = journalEntry(record, context.request, context.requestDigest);
1525
+ if (entry === null)
1526
+ return { ok: false, reason: 'transition_journal_missing' };
1527
+ if (entry.effects[name] !== undefined)
1528
+ return { ok: false, reason: 'effect_intent_raced' };
1529
+ const saved = setJournalEffect(record, context.request.transition_id, {
1530
+ name,
1531
+ state: 'pending',
1532
+ at: nowIso(now),
1533
+ });
1534
+ if (!saved.ok)
1535
+ return { ok: false, reason: saved.code };
1536
+ return {
1537
+ record,
1538
+ op: 'session-transition-effect-intent',
1539
+ summary: { transition_id: context.request.transition_id, effect: name },
1540
+ result: { effect: name, state: 'pending' },
1541
+ };
1542
+ },
1543
+ });
1544
+ if (!outcome.ok)
1545
+ return {
1546
+ ok: false,
1547
+ kind: 'needs_input',
1548
+ code: outcome.reason,
1549
+ message: 'The durable effect intent was not committed.',
1550
+ nextAction: 'Re-read current task authority and journal before any retry.',
1551
+ };
1552
+ const savedTask = await openTaskStore(currentTask.taskRoot);
1553
+ if (!savedTask.ok)
1554
+ return {
1555
+ ok: false,
1556
+ kind: 'needs_input',
1557
+ code: savedTask.code,
1558
+ message: savedTask.message,
1559
+ nextAction: savedTask.nextAction,
1560
+ };
1561
+ const savedJournal = journalEntry(savedTask.value.record, context.request, context.requestDigest);
1562
+ if (savedJournal?.effects[name]?.state !== 'pending')
1563
+ return {
1564
+ ok: false,
1565
+ kind: 'needs_input',
1566
+ code: 'effect_intent_unconfirmed',
1567
+ message: 'The effect-intent transaction returned without a matching durable pending receipt.',
1568
+ nextAction: 'Inspect the task journal; do not invoke the effect.',
1569
+ };
1570
+ return { ok: true, task: savedTask.value, replayed: false };
1571
+ }
1572
+ async function recordEffectOutcome(context, currentTask, name, state, clock, code, options = {}) {
1573
+ const current = await assertTaskStoreCurrent(currentTask);
1574
+ if (current !== null)
1575
+ return { ok: false, code: current.code };
1576
+ const outcome = transact(currentTask.taskRoot, {
1577
+ actor: context.request.intent.from.agent_id,
1578
+ purpose: `session-transition:${name}:outcome`,
1579
+ expectRevision: currentTask.record.state_revision,
1580
+ clock,
1581
+ apply: (record) => {
1582
+ if (!isTaskRecord(record) ||
1583
+ record.task_id !== path.basename(context.request.task_root) ||
1584
+ !safePathEqual(record.task_root, context.request.task_root))
1585
+ return { ok: false, reason: 'task_namespace_changed' };
1586
+ if (hasJournalConflict(record, context.request, context.requestDigest))
1587
+ return { ok: false, reason: 'transition_journal_conflict' };
1588
+ const journal = journalEntry(record, context.request, context.requestDigest);
1589
+ if (journal === null)
1590
+ return { ok: false, reason: 'transition_journal_missing' };
1591
+ const prior = journal.effects[name];
1592
+ if (prior === undefined)
1593
+ return { ok: false, reason: 'effect_intent_missing' };
1594
+ if (prior.state === state) {
1595
+ const same = (options.digest === undefined || prior.digest === options.digest) &&
1596
+ (options.ref === undefined || prior.ref === options.ref) &&
1597
+ (code === null || prior.code === code);
1598
+ if (!same)
1599
+ return { ok: false, reason: 'effect_outcome_conflict' };
1600
+ return {
1601
+ record,
1602
+ op: 'session-transition-effect-outcome-replay',
1603
+ summary: {
1604
+ transition_id: context.request.transition_id,
1605
+ effect: name,
1606
+ state,
1607
+ },
1608
+ result: { effect: name, state, replayed: true },
1609
+ };
1610
+ }
1611
+ if (prior.state !== 'pending' && prior.state !== 'unknown')
1612
+ return { ok: false, reason: 'effect_outcome_conflict' };
1613
+ const saved = setJournalEffect(record, context.request.transition_id, {
1614
+ name,
1615
+ state,
1616
+ at: new Date(clock).toISOString(),
1617
+ ...(code === null ? {} : { code }),
1618
+ ...(options.digest === undefined ? {} : { digest: options.digest }),
1619
+ ...(options.ref === undefined ? {} : { ref: options.ref }),
1620
+ ...(options.artifact === undefined
1621
+ ? {}
1622
+ : { artifact: options.artifact }),
1623
+ });
1624
+ if (!saved.ok)
1625
+ return { ok: false, reason: saved.code };
1626
+ return {
1627
+ record,
1628
+ op: 'session-transition-effect-outcome',
1629
+ summary: {
1630
+ transition_id: context.request.transition_id,
1631
+ effect: name,
1632
+ state,
1633
+ },
1634
+ result: { effect: name, state, replayed: false },
1635
+ };
1636
+ },
1637
+ });
1638
+ if (!outcome.ok)
1639
+ return { ok: false, code: outcome.reason };
1640
+ const savedTask = await openTaskStore(currentTask.taskRoot);
1641
+ if (!savedTask.ok)
1642
+ return { ok: false, code: savedTask.code };
1643
+ const savedJournal = journalEntry(savedTask.value.record, context.request, context.requestDigest);
1644
+ const savedEffect = savedJournal?.effects[name];
1645
+ if (savedEffect?.state !== state ||
1646
+ (options.digest !== undefined && savedEffect.digest !== options.digest) ||
1647
+ (options.ref !== undefined && savedEffect.ref !== options.ref))
1648
+ return { ok: false, code: 'effect_outcome_unconfirmed' };
1649
+ return { ok: true, task: savedTask.value };
1650
+ }
1651
+ export async function planSessionTransition(input, dependencies = {}) {
1652
+ const request = requestOf(input);
1653
+ const context = await loadContext(input);
1654
+ if (!context.ok)
1655
+ return failureReport('plan', request, context.kind === 'invalid' ? 'invalid' : 'needs_input', context.code, context.message, context.nextAction);
1656
+ const now = dependencies.clock ?? Date.now;
1657
+ const handoffAtRead = getHandoff(context.value.task.record, request.intent.handoff_id);
1658
+ if (handoffAtRead?.state !== 'receiver_confirmed' &&
1659
+ handoffAtRead?.state !== 'completed') {
1660
+ const preflight = validatePreflight(context.value, now());
1661
+ if (!preflight.ok)
1662
+ return failureReport('plan', request, 'needs_input', preflight.code, preflight.message, 'Refresh the exact current assignment/card evidence; no transition effect was attempted.', context.value.task);
1663
+ }
1664
+ const rolloverResult = await inspectOrPlanRollover(buildRolloverRequest(request, context.value.task.record));
1665
+ if (!rolloverResult.ok)
1666
+ return failureReport('plan', request, 'needs_input', rolloverResult.code, rolloverResult.message, rolloverResult.nextAction, context.value.task);
1667
+ let bootstrap;
1668
+ try {
1669
+ bootstrap = await bootstrapExecutor(dependencies).plan(context.value.bootstrap);
1670
+ }
1671
+ catch {
1672
+ return failureReport('plan', request, 'unknown', 'bootstrap_plan_failed', 'The read-only bootstrap planner did not return a report.', 'Reconcile the exact bootstrap request and current task; no transition effect was attempted.', context.value.task);
1673
+ }
1674
+ const changed = await assertTaskStoreCurrent(context.value.task);
1675
+ if (changed !== null)
1676
+ return failureReport('plan', request, 'needs_input', changed.code, changed.message, changed.nextAction, context.value.task);
1677
+ let hostObservation = null;
1678
+ if (dependencies.host !== undefined) {
1679
+ try {
1680
+ hostObservation = await dependencies.host.observe();
1681
+ }
1682
+ catch {
1683
+ return failureReport('plan', request, 'needs_input', 'host_observation_unavailable', 'The opt-in Pi host did not return an observation.', 'Re-read the exact session/cwd and host status; planning did not attempt a transition effect.', context.value.task);
1684
+ }
1685
+ const identityIssue = hostIdentityFailure(hostObservation, request);
1686
+ if (identityIssue !== null)
1687
+ return failureReport('plan', request, 'needs_input', identityIssue.code, identityIssue.message, identityIssue.nextAction, context.value.task, { host: { id: dependencies.host.id, ...hostObservation } });
1688
+ const changedAfterHost = await assertTaskStoreCurrent(context.value.task);
1689
+ if (changedAfterHost !== null)
1690
+ return failureReport('plan', request, 'needs_input', changedAfterHost.code, changedAfterHost.message, changedAfterHost.nextAction, context.value.task, { host: { id: dependencies.host.id, ...hostObservation } });
1691
+ }
1692
+ const refreshed = await openTaskStore(request.task_root);
1693
+ if (!refreshed.ok)
1694
+ return failureReport('plan', request, 'needs_input', refreshed.code, refreshed.message, refreshed.nextAction, context.value.task);
1695
+ const existingIntent = getIntent(refreshed.value.record, request.intent.intent_id);
1696
+ const handoff = getHandoff(refreshed.value.record, request.intent.handoff_id);
1697
+ const drift = currentStateDrift(refreshed.value.record, request, handoff?.state === 'completed' ? 'successor' : 'source');
1698
+ const steps = plannedSteps(rolloverResult.value, bootstrap, hostObservation, existingIntent !== null, handoff?.state ?? null);
1699
+ const report = reportForContext('plan', request, refreshed.value.record, rolloverResult.value, bootstrap, hostObservation, steps, 'planned');
1700
+ return drift === null
1701
+ ? report
1702
+ : {
1703
+ ...report,
1704
+ ok: false,
1705
+ status: 'needs_input',
1706
+ code: drift.code,
1707
+ message: drift.message,
1708
+ next_action: 'Reconcile current attempt/claims/epoch before applying.',
1709
+ };
1710
+ }
1711
+ export async function applySessionTransition(input, dependencies = {}) {
1712
+ const request = requestOf(input);
1713
+ const now = dependencies.clock ?? Date.now;
1714
+ const context = await loadContext(input);
1715
+ if (!context.ok)
1716
+ return failureReport('apply', request, context.kind === 'invalid' ? 'invalid' : 'needs_input', context.code, context.message, context.nextAction);
1717
+ const existingHandoff = getHandoff(context.value.task.record, request.intent.handoff_id);
1718
+ if (existingHandoff?.state !== 'completed') {
1719
+ const scopeDrift = managedExecutionScopeDrift(request, context.value.task.record);
1720
+ if (scopeDrift !== null)
1721
+ return failureReport('apply', request, 'needs_input', scopeDrift.code, scopeDrift.message, 'Rebind only after the authoritative task, attempt, owner epoch and claim pins are reconciled; no transition effect was attempted.', context.value.task);
1722
+ }
1723
+ if (existingHandoff?.state === 'completed') {
1724
+ const drift = currentStateDrift(context.value.task.record, request, 'successor');
1725
+ if (drift !== null)
1726
+ return failureReport('apply', request, 'needs_input', drift.code, drift.message, 'Reconcile successor authority, attempt and claim epochs before first-work or retirement.', context.value.task);
1727
+ return applyFirstWorkAndRetirement(context.value, context.value.task, null, dependencies, now);
1728
+ }
1729
+ if (existingHandoff?.state !== 'receiver_confirmed') {
1730
+ const preflight = validatePreflight(context.value, now());
1731
+ if (!preflight.ok)
1732
+ return failureReport('apply', request, 'needs_input', preflight.code, preflight.message, 'Refresh the matching current assignment/card and re-plan.', context.value.task);
1733
+ }
1734
+ if (dependencies.host === undefined) {
1735
+ const currentIntent = getIntent(context.value.task.record, request.intent.intent_id);
1736
+ return failureReport('apply', request, 'needs_input', 'pi_transition_host_required', 'Effectful transition requires the explicitly loaded Pi session-transition host adapter; CLI-only metadata is not quiescence proof.', 'Load the opt-in Pi extension and invoke its transition command; no freeze, bootstrap, transfer or archive was performed.', context.value.task, {
1737
+ stage: currentIntent === null ? 'not_started' : 'intent_created',
1738
+ intent: currentIntent === null
1739
+ ? null
1740
+ : { intent_id: currentIntent.intent_id },
1741
+ capabilities: {
1742
+ ...capabilities(null),
1743
+ pi_host_extension: 'not_loaded',
1744
+ },
1745
+ });
1746
+ }
1747
+ let hostBefore;
1748
+ try {
1749
+ hostBefore = await dependencies.host.observe();
1750
+ }
1751
+ catch {
1752
+ return failureReport('apply', request, 'needs_input', 'host_observation_unavailable', 'The opt-in Pi host did not return a current observation.', 'Reconcile exact source session/cwd and controller status; no transition effect was attempted.', context.value.task);
1753
+ }
1754
+ const changedAfterHostRead = await assertTaskStoreCurrent(context.value.task);
1755
+ if (changedAfterHostRead !== null)
1756
+ return failureReport('apply', request, 'needs_input', changedAfterHostRead.code, changedAfterHostRead.message, changedAfterHostRead.nextAction, context.value.task, { host: { id: dependencies.host.id, ...hostBefore } });
1757
+ const identityIssue = hostIdentityFailure(hostBefore, request);
1758
+ if (identityIssue !== null)
1759
+ return failureReport('apply', request, 'needs_input', identityIssue.code, identityIssue.message, identityIssue.nextAction, context.value.task, { host: { id: dependencies.host.id, ...hostBefore } });
1760
+ const rolloverResult = await inspectOrPlanRollover(buildRolloverRequest(request, context.value.task.record));
1761
+ if (!rolloverResult.ok)
1762
+ return failureReport('apply', request, 'needs_input', rolloverResult.code, rolloverResult.message, rolloverResult.nextAction, context.value.task, { host: { id: dependencies.host.id, ...hostBefore } });
1763
+ const existing = getIntent(context.value.task.record, request.intent.intent_id);
1764
+ const blockingDrift = rolloverResult.value.drift.find((item) => !(existing === null && item.code === 'unknown_handoff'));
1765
+ if (blockingDrift !== undefined)
1766
+ return failureReport('apply', request, 'needs_input', blockingDrift.code, blockingDrift.detail, 'Reconcile task drift before transition effects.', context.value.task, {
1767
+ rollover: rolloverResult.value,
1768
+ host: { id: dependencies.host.id, ...hostBefore },
1769
+ });
1770
+ if (existing !== null &&
1771
+ !sameIntentRequest(existing, request.intent, request))
1772
+ return failureReport('apply', request, 'invalid', 'intent_input_conflict', 'The transition id is already pinned to different immutable inputs.', 'Choose a new transition id only after reconciling the existing handoff.', context.value.task);
1773
+ if (existing === null) {
1774
+ const created = await intentTransaction(context.value, dependencies, now);
1775
+ if (!created.ok)
1776
+ return failureReport('apply', request, 'needs_input', created.reason, 'The durable rollover intent could not be committed.', 'Re-read current task revision/claims and reconcile before retrying.', context.value.task, { host: { id: dependencies.host.id, ...hostBefore } });
1777
+ }
1778
+ const currentTask = await openTaskStore(request.task_root);
1779
+ if (!currentTask.ok)
1780
+ return failureReport('apply', request, 'needs_input', currentTask.code, currentTask.message, currentTask.nextAction, context.value.task);
1781
+ const currentIntent = getIntent(currentTask.value.record, request.intent.intent_id);
1782
+ if (currentIntent === null ||
1783
+ !sameIntentRequest(currentIntent, request.intent, request))
1784
+ return failureReport('apply', request, 'unknown', 'intent_reconcile_required', 'The intent commit could not be confirmed from the current record.', 'Inspect the authoritative task record; no native effect was attempted.', currentTask.value, { host: { id: dependencies.host.id, ...hostBefore } });
1785
+ const handoff = getHandoff(currentTask.value.record, request.intent.handoff_id);
1786
+ if (handoff === null)
1787
+ return failureReport('apply', request, 'unknown', 'handoff_missing', 'The rollover intent is recorded but its handoff is missing.', 'Inspect the task record; do not create a second handoff.', currentTask.value);
1788
+ if (handoff.state === 'requested') {
1789
+ if (hostBefore.proof_support !== 'available') {
1790
+ const currentRollover = await inspectOrPlanRollover(buildRolloverRequest(request, currentTask.value.record));
1791
+ const blocker = hostBefore.unavailable_code ?? 'host_stop_proof_unavailable';
1792
+ const detail = hostBefore.detail ??
1793
+ 'No accepted host can produce complete native quiescence proof.';
1794
+ return failureReport('apply', request, 'waiting', blocker, detail, 'Do not infer stop from idle, settled events, a freeze latch, or transcript data. Wait for the accepted session-bound native proof API; no source freeze, successor bootstrap, or transfer was attempted.', currentTask.value, {
1795
+ stage: 'awaiting_quiescence',
1796
+ intent: {
1797
+ intent_id: currentIntent.intent_id,
1798
+ generation: currentIntent.generation,
1799
+ handoff_id: currentIntent.handoff_id,
1800
+ },
1801
+ handoff: { handoff_id: handoff.handoff_id, state: handoff.state },
1802
+ rollover: currentRollover.ok ? currentRollover.value : null,
1803
+ host: { id: dependencies.host.id, ...hostBefore },
1804
+ steps: [
1805
+ step('create-intent', 'satisfied'),
1806
+ step('source-quiescence', 'unavailable', blocker, detail),
1807
+ step('successor-bootstrap', 'blocked', 'predecessor_step_unavailable', 'Bootstrap cannot start until source quiescence is supported.'),
1808
+ step('receiver-ack', 'blocked', 'predecessor_step_unavailable', 'No successor was started.'),
1809
+ step('claim-route-transfer', 'blocked', 'predecessor_step_unavailable', 'Claims and assignment remain with the source.'),
1810
+ step('first-work', 'blocked', 'predecessor_step_unavailable', 'No successor work is authorized.'),
1811
+ step('task-retirement', 'blocked', 'predecessor_step_unavailable', 'Retirement is forbidden before validated first work.'),
1812
+ ],
1813
+ capabilities: capabilities(hostBefore),
1814
+ });
1815
+ }
1816
+ const prepared = await freezeAndPrepare(context.value, currentTask.value, hostBefore, dependencies, now);
1817
+ return prepared.report;
1818
+ }
1819
+ if (handoff.state === 'transferring') {
1820
+ let bootstrapReport;
1821
+ try {
1822
+ bootstrapReport = await bootstrapExecutor(dependencies).apply(context.value.bootstrap);
1823
+ }
1824
+ catch {
1825
+ return failureReport('apply', request, 'unknown', 'bootstrap_outcome_unknown', 'The successor bootstrap executor did not return a durable result.', 'Inspect the exact bootstrap reservation, pane, binding and notification receipt; never repeat an ambiguous launch or prompt.', currentTask.value);
1826
+ }
1827
+ const afterBootstrap = await openTaskStore(request.task_root);
1828
+ if (!afterBootstrap.ok)
1829
+ return failureReport('apply', request, 'unknown', afterBootstrap.code, afterBootstrap.message, afterBootstrap.nextAction, currentTask.value, { bootstrap: bootstrapReport });
1830
+ const current = getHandoff(afterBootstrap.value.record, request.intent.handoff_id);
1831
+ if (current?.state === 'receiver_confirmed')
1832
+ return applyConfirmedTransfer(context.value, afterBootstrap.value, hostBefore, dependencies, now, bootstrapReport);
1833
+ if (current?.state !== 'transferring')
1834
+ return failureReport('apply', request, 'unknown', 'handoff_state_changed', 'Handoff changed during successor bootstrap; inspect before continuing.', 'Re-read the task record and bootstrap reservation; no further effect was attempted.', afterBootstrap.value, { bootstrap: bootstrapReport });
1835
+ if (request.evidence.ack_message_id !== null &&
1836
+ (bootstrapReport.status === 'applied' ||
1837
+ bootstrapReport.status === 'already_recorded'))
1838
+ return applyConfirmedTransfer(context.value, afterBootstrap.value, hostBefore, dependencies, now, bootstrapReport);
1839
+ const bootstrapComplete = bootstrapReport.status === 'applied' ||
1840
+ bootstrapReport.status === 'already_recorded';
1841
+ return reportForContext('apply', request, afterBootstrap.value.record, null, bootstrapReport, hostBefore, [
1842
+ step('create-intent', 'satisfied'),
1843
+ step('source-quiescence', 'satisfied'),
1844
+ step('successor-bootstrap', bootstrapComplete ? 'satisfied' : 'unknown', bootstrapReport.code, bootstrapReport.message),
1845
+ step('receiver-ack', bootstrapComplete ? 'waiting' : 'blocked', bootstrapComplete ? null : 'predecessor_step_unavailable', 'Wait for and register the exact successor ACK; no second prompt or successor start is automatic.'),
1846
+ step('claim-route-transfer', 'blocked', 'predecessor_step_unavailable', 'Claims and assignment remain with the source until the correlated ACK.'),
1847
+ step('first-work', 'blocked', 'predecessor_step_unavailable', 'Successor first work is not yet authorized.'),
1848
+ step('task-retirement', 'blocked', 'predecessor_step_unavailable', 'Retirement follows validated first work.'),
1849
+ ], bootstrapComplete ? 'waiting' : 'unknown', {
1850
+ next_action: bootstrapComplete
1851
+ ? 'Wait for the exact registered successor ACK and apply once with its message id.'
1852
+ : 'Reconcile the durable bootstrap reservation before continuing.',
1853
+ });
1854
+ }
1855
+ if (handoff.state === 'receiver_confirmed') {
1856
+ return applyConfirmedTransfer(context.value, currentTask.value, hostBefore, dependencies, now);
1857
+ }
1858
+ if (handoff.state === 'completed') {
1859
+ return applyFirstWorkAndRetirement(context.value, currentTask.value, hostBefore, dependencies, now);
1860
+ }
1861
+ if (handoff.state === 'aborted')
1862
+ return failureReport('apply', request, 'needs_input', 'handoff_aborted', 'The recorded handoff is aborted and cannot transition.', 'Create a new explicitly authorized transition only after reconciliation.', currentTask.value);
1863
+ return failureReport('apply', request, 'unknown', 'handoff_state_unknown', `Handoff is in unsupported state ${handoff.state}.`, 'Inspect the authoritative record; no automatic recovery is attempted.', currentTask.value);
1864
+ }
1865
+ function plannedSteps(rolloverReport, bootstrapReport, host, intentFound, handoffState) {
1866
+ const rollStep = (name) => rolloverReport.steps.find((item) => item.step === name);
1867
+ const prepared = rolloverReport.prepared.recorded ||
1868
+ [
1869
+ 'old-writer-stopped',
1870
+ 'transferring',
1871
+ 'receiver-confirmed',
1872
+ 'completed',
1873
+ ].includes(rolloverReport.stage);
1874
+ const confirmed = handoffState === 'receiver_confirmed' || handoffState === 'completed';
1875
+ const transferred = handoffState === 'completed';
1876
+ const successionValid = rolloverReport.succession.valid;
1877
+ const proofSupported = host?.proof_support === 'available';
1878
+ return [
1879
+ step('create-intent', intentFound
1880
+ ? 'satisfied'
1881
+ : (rollStep('create-intent')?.status ?? 'blocked'), rollStep('create-intent')?.blockers[0]?.code ?? null, rollStep('create-intent')?.blockers[0]?.detail ?? null),
1882
+ step('source-quiescence', prepared ? 'satisfied' : proofSupported ? 'ready' : 'unavailable', prepared
1883
+ ? null
1884
+ : (host?.unavailable_code ?? 'host_stop_proof_unavailable'), prepared
1885
+ ? null
1886
+ : (host?.detail ??
1887
+ 'The installed Pi producer does not provide complete, trusted quiescence proof.')),
1888
+ step('successor-bootstrap', prepared
1889
+ ? bootstrapReport.status === 'planned' ||
1890
+ bootstrapReport.status === 'applied' ||
1891
+ bootstrapReport.status === 'already_recorded'
1892
+ ? bootstrapReport.status === 'planned'
1893
+ ? 'ready'
1894
+ : 'satisfied'
1895
+ : 'blocked'
1896
+ : 'blocked', prepared ? bootstrapReport.code : 'predecessor_step_unavailable', prepared
1897
+ ? bootstrapReport.message
1898
+ : 'Successor launch is not allowed before source quiescence.'),
1899
+ step('receiver-ack', confirmed ? 'satisfied' : prepared ? 'waiting' : 'blocked', confirmed ? null : prepared ? null : 'predecessor_step_unavailable', confirmed
1900
+ ? null
1901
+ : 'Requires a registered ACK correlated to this exact intent and the original assignment.'),
1902
+ step('claim-route-transfer', transferred ? 'ready' : confirmed ? 'ready' : 'blocked', transferred || confirmed ? null : 'predecessor_step_unavailable', transferred
1903
+ ? 'Core assignee/claim transfer is recorded; the explicit successor-only route binding still needs reconciliation.'
1904
+ : 'Claims and the current task assignee transfer only after the correlated receiver ACK.'),
1905
+ step('first-work', successionValid ? 'satisfied' : transferred ? 'waiting' : 'blocked', successionValid
1906
+ ? null
1907
+ : transferred
1908
+ ? null
1909
+ : 'predecessor_step_unavailable', successionValid
1910
+ ? null
1911
+ : 'Requires an admitted completed successor REPORT after the recorded transfer boundary.'),
1912
+ step('task-retirement', successionValid ? 'ready' : 'blocked', successionValid ? null : 'predecessor_step_unavailable', successionValid
1913
+ ? 'Writes a reversible task-local retirement marker only; native Pi archive is unavailable.'
1914
+ : 'Retirement is forbidden before validated first work.'),
1915
+ ];
1916
+ }
1917
+ function hostIdentityFailure(observation, request) {
1918
+ if (observation.session_id !== request.intent.from.session_id)
1919
+ return failure('needs_input', 'source_session_mismatch', 'The opt-in Pi host is not running in the exact source session.', 'Load the extension in the task-bound source session; no transition effect was attempted.');
1920
+ if (observation.cwd !== request.intent.from.cwd)
1921
+ return failure('needs_input', 'source_cwd_mismatch', 'The opt-in Pi host cwd does not match the pinned source identity.', 'Run from the exact task-bound cwd; no transition effect was attempted.');
1922
+ if (observation.pane_id !== null && !isText(observation.pane_id, 64))
1923
+ return failure('needs_input', 'source_pane_invalid', 'The source pane observation is malformed.', 'Re-read the exact Herdr pane identity; no transition effect was attempted.');
1924
+ return null;
1925
+ }
1926
+ function proofSupportFailure(observation, request, now) {
1927
+ if (observation.proof_support !== 'available')
1928
+ return failure('needs_input', observation.unavailable_code ?? 'host_stop_proof_unavailable', observation.detail ??
1929
+ 'The installed Pi host exposes only incomplete/unauthenticated quiescence observations.', 'Do not infer stop from idle, settled, a freeze latch, or a transcript. Resume only when an accepted session-bound proof adapter is available; no successor was started.');
1930
+ const rawProof = observation.proof;
1931
+ const rawFreeze = observation.freeze_status;
1932
+ if (!isRecord(rawProof) || !isRecord(rawFreeze))
1933
+ return failure('needs_input', 'host_quiescence_proof_invalid', 'The host reported proof support without a complete structured proof and active freeze receipt.', 'Reconcile the current source session and inventory; no ownership transfer was attempted.');
1934
+ const proof = rawProof;
1935
+ const tokenValue = proof['freeze_token'];
1936
+ const inventoryValue = proof['inventory'];
1937
+ const freeze = rawFreeze;
1938
+ const tokenSnapshotValue = freeze['token'];
1939
+ if (!isRecord(tokenValue) ||
1940
+ !isRecord(inventoryValue) ||
1941
+ !isRecord(tokenSnapshotValue))
1942
+ return failure('needs_input', 'host_quiescence_proof_invalid', 'The host quiescence receipt is missing its freeze token, inventory or freeze status.', 'Reconcile the current source session and inventory; no ownership transfer was attempted.');
1943
+ const token = tokenValue;
1944
+ const inventory = inventoryValue;
1945
+ const age = now - Date.parse(String(proof['observed_at'] ?? ''));
1946
+ const tokenSnapshot = tokenSnapshotValue;
1947
+ const managedScope = request.managed_execution_scope;
1948
+ const scopedProofMatches = managedScope === undefined
1949
+ ? proof['adapter_id'] === 'pappra/pi-session-transition-host/1'
1950
+ : isTrustedManagedPiScopedProof(rawProof) &&
1951
+ proof['adapter_id'] === MANAGED_PI_SCOPED_PROOF_ADAPTER_ID &&
1952
+ proof['scope'] === 'pinned_pi_1_4_owned_runtime' &&
1953
+ isRecord(proof['execution_scope']) &&
1954
+ canonicalJson(proof['execution_scope']) ===
1955
+ canonicalJson(managedScope) &&
1956
+ proof['execution_scope_digest'] ===
1957
+ digestText(canonicalJson(managedScope)) &&
1958
+ Array.isArray(proof['excluded_scope']) &&
1959
+ proof['excluded_scope'].length === 2 &&
1960
+ proof['excluded_scope'][0] === 'arbitrary_external_writers' &&
1961
+ proof['excluded_scope'][1] === 'unowned_processes_and_descendants' &&
1962
+ proof['runtime_quiescent'] === true &&
1963
+ Array.isArray(proof['runtime_blockers']) &&
1964
+ proof['runtime_blockers'].length === 0 &&
1965
+ proof['controller_journal_healthy'] === true &&
1966
+ isNonNegativeInt(proof['controller_sequence']) &&
1967
+ isDigest(proof['controller_head_digest']) &&
1968
+ isNonNegativeInt(proof['sequence']) &&
1969
+ proof['sequence'] >=
1970
+ proof['controller_sequence'];
1971
+ if (!isRecord(tokenSnapshot) ||
1972
+ !scopedProofMatches ||
1973
+ proof['session_id'] !== request.intent.from.session_id ||
1974
+ proof['cwd'] !== request.intent.from.cwd ||
1975
+ !isText(proof['observed_at'], 64) ||
1976
+ !Number.isFinite(Date.parse(proof['observed_at'])) ||
1977
+ age < 0 ||
1978
+ age > MAX_QUIESCENCE_AGE_MS ||
1979
+ !isText(proof['evidence_ref'], 4096) ||
1980
+ !Number.isSafeInteger(proof['sequence']) ||
1981
+ proof['sequence'] < 0 ||
1982
+ token['sessionId'] !== request.intent.from.session_id ||
1983
+ token['taskId'] !== path.basename(request.task_root) ||
1984
+ token['handoffId'] !== request.intent.handoff_id ||
1985
+ token['generation'] !== request.intent.generation ||
1986
+ freeze['frozen'] !== true ||
1987
+ tokenSnapshot['sessionId'] !== token['sessionId'] ||
1988
+ tokenSnapshot['taskId'] !== token['taskId'] ||
1989
+ tokenSnapshot['handoffId'] !== token['handoffId'] ||
1990
+ tokenSnapshot['generation'] !== token['generation'] ||
1991
+ inventory['completeness'] !== 'complete' ||
1992
+ !Array.isArray(inventory['inflight']) ||
1993
+ inventory['inflight'].length !== 0 ||
1994
+ !Array.isArray(inventory['pending_approvals']) ||
1995
+ inventory['pending_approvals'].length !== 0 ||
1996
+ !Array.isArray(inventory['pending_messages']) ||
1997
+ inventory['pending_messages'].length !== 0 ||
1998
+ (inventory['partial_outputs'] !== undefined &&
1999
+ !Array.isArray(inventory['partial_outputs'])))
2000
+ return failure('needs_input', 'host_quiescence_proof_invalid', 'Host quiescence evidence is stale, incomplete, mismatched, or not bound to the active freeze token.', 'Reconcile the current source session and inventory; no ownership transfer was attempted.');
2001
+ return null;
2002
+ }
2003
+ function freezeTokenMatches(status, expected) {
2004
+ if (!isRecord(status) ||
2005
+ status['frozen'] !== true ||
2006
+ !isRecord(status['token']))
2007
+ return false;
2008
+ const token = status['token'];
2009
+ return (token['sessionId'] === expected.sessionId &&
2010
+ token['taskId'] === expected.taskId &&
2011
+ token['handoffId'] === expected.handoffId &&
2012
+ token['generation'] === expected.generation);
2013
+ }
2014
+ function sameFreezeToken(left, right) {
2015
+ return (left.sessionId === right.sessionId &&
2016
+ left.taskId === right.taskId &&
2017
+ left.handoffId === right.handoffId &&
2018
+ left.generation === right.generation);
2019
+ }
2020
+ function quiescenceInventoryDigest(inventory) {
2021
+ if (inventory.completeness !== 'complete' ||
2022
+ !Array.isArray(inventory.inflight) ||
2023
+ !Array.isArray(inventory.pending_approvals) ||
2024
+ !Array.isArray(inventory.pending_messages) ||
2025
+ (inventory.partial_outputs !== undefined &&
2026
+ !Array.isArray(inventory.partial_outputs)))
2027
+ return null;
2028
+ try {
2029
+ return digestText(canonicalJson({
2030
+ completeness: 'complete',
2031
+ inflight: inventory.inflight,
2032
+ pending_approvals: inventory.pending_approvals,
2033
+ pending_messages: inventory.pending_messages,
2034
+ partial_outputs: inventory.partial_outputs ?? [],
2035
+ }));
2036
+ }
2037
+ catch {
2038
+ return null;
2039
+ }
2040
+ }
2041
+ function quiescenceReceipt(proof, inventoryDigest) {
2042
+ const scoped = proof.adapter_id === MANAGED_PI_SCOPED_PROOF_ADAPTER_ID;
2043
+ return {
2044
+ schema: scoped
2045
+ ? MANAGED_PI_SCOPED_RECEIPT_SCHEMA
2046
+ : 'pappra/session-transition-quiescence-receipt/1',
2047
+ adapter_id: proof.adapter_id,
2048
+ session_id: proof.session_id,
2049
+ cwd: proof.cwd,
2050
+ observed_at: proof.observed_at,
2051
+ sequence: proof.sequence,
2052
+ evidence_ref: proof.evidence_ref,
2053
+ freeze_token: { ...proof.freeze_token },
2054
+ inventory_digest: inventoryDigest,
2055
+ ...(scoped && proof.execution_scope_digest !== undefined
2056
+ ? { execution_scope_digest: proof.execution_scope_digest }
2057
+ : {}),
2058
+ };
2059
+ }
2060
+ async function freezeAndPrepare(context, currentTask, initialObservation, dependencies, now) {
2061
+ const request = context.request;
2062
+ const host = dependencies.host;
2063
+ if (host === undefined)
2064
+ return {
2065
+ ok: false,
2066
+ report: failureReport('apply', request, 'needs_input', 'pi_transition_host_required', 'No opt-in Pi host is connected.', 'Load the transition extension explicitly; no native effect was attempted.', currentTask),
2067
+ };
2068
+ const token = {
2069
+ sessionId: request.intent.from.session_id,
2070
+ taskId: currentTask.record.task_id,
2071
+ handoffId: request.intent.handoff_id,
2072
+ generation: request.intent.generation,
2073
+ };
2074
+ let observation;
2075
+ try {
2076
+ observation = await host.observe();
2077
+ }
2078
+ catch {
2079
+ return {
2080
+ ok: false,
2081
+ report: failureReport('apply', request, 'unknown', 'host_observation_unknown', 'The host observation failed before the source freeze could be reconciled.', 'Re-read the exact task-bound host and durable journal; no freeze or transfer was attempted.', currentTask),
2082
+ };
2083
+ }
2084
+ const namespace = await assertTaskStoreCurrent(currentTask);
2085
+ if (namespace !== null)
2086
+ return {
2087
+ ok: false,
2088
+ report: failureReport('apply', request, 'needs_input', namespace.code, namespace.message, namespace.nextAction, currentTask),
2089
+ };
2090
+ const identityIssue = hostIdentityFailure(observation, request);
2091
+ if (identityIssue !== null)
2092
+ return {
2093
+ ok: false,
2094
+ report: failureReport('apply', request, identityIssue.kind, identityIssue.code, identityIssue.message, identityIssue.nextAction, currentTask, { host: { id: host.id, ...observation } }),
2095
+ };
2096
+ const scopeDrift = managedExecutionScopeDrift(request, currentTask.record);
2097
+ if (scopeDrift !== null)
2098
+ return {
2099
+ ok: false,
2100
+ report: failureReport('apply', request, 'needs_input', scopeDrift.code, scopeDrift.message, 'Reconcile the current task/attempt/claim pins before source freeze; no new effect was attempted.', currentTask),
2101
+ };
2102
+ if (observation.proof_support !== 'available')
2103
+ return {
2104
+ ok: false,
2105
+ report: failureReport('apply', request, 'waiting', observation.unavailable_code ?? 'host_stop_proof_unavailable', observation.detail ??
2106
+ 'No accepted host can produce complete quiescence proof.', 'Do not freeze or transfer; wait for the supported native proof API.', currentTask, { host: { id: host.id, ...observation } }),
2107
+ };
2108
+ if (initialObservation.session_id !== observation.session_id ||
2109
+ initialObservation.cwd !== observation.cwd)
2110
+ return {
2111
+ ok: false,
2112
+ report: failureReport('apply', request, 'needs_input', 'host_identity_changed', 'The source host identity changed between the initial and effect-boundary observations.', 'Reconcile the current session/cwd and re-plan; no freeze or transfer was attempted.', currentTask, { host: { id: host.id, ...observation } }),
2113
+ };
2114
+ let journal = journalEntry(currentTask.record, request, context.requestDigest);
2115
+ if (journal === null)
2116
+ return {
2117
+ ok: false,
2118
+ report: failureReport('apply', request, 'unknown', 'transition_journal_missing', 'The transition intent lacks a matching durable effect journal.', 'Inspect the task record before any native operation.', currentTask),
2119
+ };
2120
+ let freezeReceipt = journal.effects['source-freeze'];
2121
+ if (freezeReceipt === undefined &&
2122
+ context.bootstrap.request.managed_runtime) {
2123
+ try {
2124
+ verifyManagedPiSdkInstallation(context.bootstrap.request.managed_runtime.sdk_module_path);
2125
+ }
2126
+ catch {
2127
+ return {
2128
+ ok: false,
2129
+ report: failureReport('apply', request, 'needs_input', 'successor_sdk_incompatible', 'The successor SDK changed or is incompatible; no source freeze was attempted.', 'Select a supported SDK installation and re-plan.', currentTask),
2130
+ };
2131
+ }
2132
+ }
2133
+ if (freezeReceipt?.state === 'failed')
2134
+ return {
2135
+ ok: false,
2136
+ report: failureReport('apply', request, 'needs_input', 'source_freeze_failed', 'A prior source-freeze operation returned a known failure and will not be replayed automatically.', 'Reconcile the source-controller state; start a new transition only with explicit authorization.', currentTask, { host: { id: host.id, ...observation } }),
2137
+ };
2138
+ if (freezeReceipt?.state === 'pending' ||
2139
+ freezeReceipt?.state === 'unknown') {
2140
+ if (!freezeTokenMatches(observation.freeze_status, token))
2141
+ return {
2142
+ ok: false,
2143
+ report: failureReport('apply', request, 'reconcile_required', 'source_freeze_outcome_unknown', 'A previous freeze may have taken effect, but the exact token is not confirmed by the current host observation.', 'Reconcile the in-process controller and journal; do not repeat freeze or continue transfer.', currentTask, { host: { id: host.id, ...observation } }),
2144
+ };
2145
+ const reconciled = await recordEffectOutcome(context, currentTask, 'source-freeze', 'completed', now(), null);
2146
+ if (!reconciled.ok)
2147
+ return {
2148
+ ok: false,
2149
+ report: failureReport('apply', request, 'unknown', reconciled.code, 'The exact source freeze is visible, but its durable journal outcome could not be recorded.', 'Inspect host status and task journal; do not repeat the freeze.', currentTask, { host: { id: host.id, ...observation } }),
2150
+ };
2151
+ currentTask = reconciled.task;
2152
+ journal = journalEntry(currentTask.record, request, context.requestDigest);
2153
+ freezeReceipt = journal?.effects['source-freeze'];
2154
+ }
2155
+ let currentObservation = observation;
2156
+ if (freezeReceipt?.state !== 'completed') {
2157
+ const intent = await recordEffectIntent(context, currentTask, 'source-freeze', now, dependencies.signal);
2158
+ if (!intent.ok)
2159
+ return {
2160
+ ok: false,
2161
+ report: failureReport('apply', request, intent.kind === 'invalid' ? 'invalid' : 'needs_input', intent.code, intent.message, intent.nextAction, currentTask, { host: { id: host.id, ...observation } }),
2162
+ };
2163
+ currentTask = intent.task;
2164
+ let freezeOutcome = null;
2165
+ try {
2166
+ freezeOutcome = await host.freeze(token);
2167
+ }
2168
+ catch {
2169
+ let afterFailure = null;
2170
+ try {
2171
+ afterFailure = await host.observe();
2172
+ }
2173
+ catch {
2174
+ // The pending journal receipt remains the reconciliation fence.
2175
+ }
2176
+ if (afterFailure !== null &&
2177
+ freezeTokenMatches(afterFailure.freeze_status, token)) {
2178
+ const reconciled = await recordEffectOutcome(context, currentTask, 'source-freeze', 'completed', now(), null);
2179
+ if (!reconciled.ok)
2180
+ return {
2181
+ ok: false,
2182
+ report: failureReport('apply', request, 'unknown', reconciled.code, 'The host may have frozen the source, but the outcome receipt could not be persisted.', 'Reconcile the in-process controller and journal; do not retry freeze.', currentTask, { host: { id: host.id, ...afterFailure } }),
2183
+ };
2184
+ currentTask = reconciled.task;
2185
+ currentObservation = afterFailure;
2186
+ }
2187
+ else {
2188
+ const unknown = await recordEffectOutcome(context, currentTask, 'source-freeze', 'unknown', now(), 'freeze_result_unknown');
2189
+ const outcomeTask = unknown.ok ? unknown.task : currentTask;
2190
+ return {
2191
+ ok: false,
2192
+ report: failureReport('apply', request, 'reconcile_required', 'source_freeze_outcome_unknown', 'The host freeze call threw and its effect cannot be determined.', 'Inspect the source controller and task journal; never replay the freeze blindly.', outcomeTask, {
2193
+ host: afterFailure === null
2194
+ ? { id: host.id }
2195
+ : { id: host.id, ...afterFailure },
2196
+ }),
2197
+ };
2198
+ }
2199
+ }
2200
+ if (freezeOutcome !== null) {
2201
+ const frozen = freezeOutcome;
2202
+ if (!frozen.ok) {
2203
+ const recorded = await recordEffectOutcome(context, currentTask, 'source-freeze', 'failed', now(), frozen.reason);
2204
+ const outcomeTask = recorded.ok ? recorded.task : currentTask;
2205
+ return {
2206
+ ok: false,
2207
+ report: failureReport('apply', request, 'needs_input', frozen.reason, frozen.detail, 'Reconcile the source freeze status; no transfer or successor start occurred.', outcomeTask, { host: { id: host.id, ...observation } }),
2208
+ };
2209
+ }
2210
+ if (!sameFreezeToken(frozen.token, token)) {
2211
+ const unknown = await recordEffectOutcome(context, currentTask, 'source-freeze', 'unknown', now(), 'freeze_token_mismatch');
2212
+ const outcomeTask = unknown.ok ? unknown.task : currentTask;
2213
+ return {
2214
+ ok: false,
2215
+ report: failureReport('apply', request, 'reconcile_required', 'source_freeze_token_mismatch', 'The host freeze returned a different token.', 'Inspect the in-process controller and journal; do not continue or replay.', outcomeTask, { host: { id: host.id, ...observation } }),
2216
+ };
2217
+ }
2218
+ try {
2219
+ currentObservation = await host.observe();
2220
+ }
2221
+ catch {
2222
+ const unknown = await recordEffectOutcome(context, currentTask, 'source-freeze', 'unknown', now(), 'post_freeze_observation_unknown');
2223
+ const outcomeTask = unknown.ok ? unknown.task : currentTask;
2224
+ return {
2225
+ ok: false,
2226
+ report: failureReport('apply', request, 'reconcile_required', 'post_freeze_observation_unknown', 'Freeze returned success, but its current host status could not be observed.', 'Reconcile the freeze and complete quiescence proof before transfer.', outcomeTask),
2227
+ };
2228
+ }
2229
+ if (!freezeTokenMatches(currentObservation.freeze_status, token)) {
2230
+ const unknown = await recordEffectOutcome(context, currentTask, 'source-freeze', 'unknown', now(), 'freeze_receipt_not_observed');
2231
+ const outcomeTask = unknown.ok ? unknown.task : currentTask;
2232
+ return {
2233
+ ok: false,
2234
+ report: failureReport('apply', request, 'reconcile_required', 'freeze_receipt_not_observed', 'Freeze returned success but the exact token is absent from the current host status.', 'Reconcile the source controller; no ownership transfer was attempted.', outcomeTask, { host: { id: host.id, ...currentObservation } }),
2235
+ };
2236
+ }
2237
+ const recorded = await recordEffectOutcome(context, currentTask, 'source-freeze', 'completed', now(), null);
2238
+ if (!recorded.ok)
2239
+ return {
2240
+ ok: false,
2241
+ report: failureReport('apply', request, 'unknown', recorded.code, 'Freeze was applied but its outcome could not be durably recorded.', 'Inspect the source controller and task journal; never retry freeze blindly.', currentTask, { host: { id: host.id, ...currentObservation } }),
2242
+ };
2243
+ currentTask = recorded.task;
2244
+ }
2245
+ }
2246
+ else if (!freezeTokenMatches(observation.freeze_status, token)) {
2247
+ return {
2248
+ ok: false,
2249
+ report: failureReport('apply', request, 'reconcile_required', 'source_freeze_receipt_mismatch', 'The journal says the source is frozen, but the current controller does not expose the exact freeze token.', 'Reconcile the in-process controller; no transfer is permitted.', currentTask, { host: { id: host.id, ...observation } }),
2250
+ };
2251
+ }
2252
+ const afterIdentityIssue = hostIdentityFailure(currentObservation, request);
2253
+ if (afterIdentityIssue !== null)
2254
+ return {
2255
+ ok: false,
2256
+ report: failureReport('apply', request, 'needs_input', afterIdentityIssue.code, afterIdentityIssue.message, afterIdentityIssue.nextAction, currentTask, { host: { id: host.id, ...currentObservation } }),
2257
+ };
2258
+ const changedAfterHost = await assertTaskStoreCurrent(currentTask);
2259
+ if (changedAfterHost !== null)
2260
+ return {
2261
+ ok: false,
2262
+ report: failureReport('apply', request, 'needs_input', changedAfterHost.code, changedAfterHost.message, changedAfterHost.nextAction, currentTask, { host: { id: host.id, ...currentObservation } }),
2263
+ };
2264
+ const proofIssue = proofSupportFailure(currentObservation, request, now());
2265
+ if (proofIssue !== null)
2266
+ return {
2267
+ ok: false,
2268
+ report: failureReport('apply', request, 'waiting', proofIssue.code, 'Source tools are frozen but the post-freeze proof is not sufficient to transfer ownership.', 'Keep the source frozen and reconcile with an accepted complete host proof; no successor was launched.', currentTask, { host: { id: host.id, ...currentObservation } }),
2269
+ };
2270
+ const proof = currentObservation.proof;
2271
+ const outcome = transact(currentTask.taskRoot, {
2272
+ actor: request.intent.from.agent_id,
2273
+ purpose: 'session-transition:prepare-transfer',
2274
+ expectRevision: currentTask.record.state_revision,
2275
+ clock: now(),
2276
+ apply: (record) => {
2277
+ if (dependencies.signal?.aborted)
2278
+ return { ok: false, reason: 'abort_requested' };
2279
+ const drift = sourceStateFailure(record, request);
2280
+ if (drift !== null)
2281
+ return { ok: false, reason: drift.code };
2282
+ const effect = journalEntry(record, request, context.requestDigest)
2283
+ ?.effects['source-freeze'];
2284
+ if (effect?.state !== 'completed')
2285
+ return { ok: false, reason: 'source_freeze_receipt_missing' };
2286
+ const prepared = rollover.prepareRolloverTransfer(record, {
2287
+ intent_id: request.intent.intent_id,
2288
+ observation: 'confirmed',
2289
+ evidence_ref: proof.evidence_ref,
2290
+ inventory: proof.inventory,
2291
+ actor: request.intent.from.agent_id,
2292
+ clock: now(),
2293
+ });
2294
+ if (!prepared.ok)
2295
+ return prepared;
2296
+ const preparedIntent = getIntent(record, request.intent.intent_id);
2297
+ const inventoryDigest = preparedIntent?.prepared?.inventory;
2298
+ if (!isDigest(inventoryDigest))
2299
+ return {
2300
+ ok: false,
2301
+ reason: 'prepared_inventory_digest_missing',
2302
+ };
2303
+ const receipt = quiescenceReceipt(proof, inventoryDigest);
2304
+ const savedReceipt = setJournalQuiescenceReceipt(record, request.transition_id, 'source', receipt);
2305
+ if (!savedReceipt.ok)
2306
+ return { ok: false, reason: savedReceipt.code };
2307
+ return {
2308
+ record,
2309
+ op: 'session-transition-prepare-transfer',
2310
+ summary: {
2311
+ transition_id: request.transition_id,
2312
+ handoff_id: request.intent.handoff_id,
2313
+ },
2314
+ result: prepared,
2315
+ };
2316
+ },
2317
+ });
2318
+ if (!outcome.ok)
2319
+ return {
2320
+ ok: false,
2321
+ report: failureReport('apply', request, 'needs_input', outcome.reason, 'Core rollover refused the prepared transfer.', 'Re-read current task/claims and host evidence before any retry.', currentTask, { host: { id: host.id, ...currentObservation } }),
2322
+ };
2323
+ const refreshed = await openTaskStore(request.task_root);
2324
+ if (!refreshed.ok)
2325
+ return {
2326
+ ok: false,
2327
+ report: failureReport('apply', request, 'unknown', refreshed.code, refreshed.message, refreshed.nextAction, currentTask),
2328
+ };
2329
+ const roll = await inspectOrPlanRollover(buildRolloverRequest(request, refreshed.value.record));
2330
+ const report = reportForContext('apply', request, refreshed.value.record, roll.ok ? roll.value : null, null, currentObservation, [
2331
+ step('create-intent', 'satisfied'),
2332
+ step('source-quiescence', 'satisfied'),
2333
+ step('prepare-transfer', 'satisfied'),
2334
+ step('successor-bootstrap', 'ready'),
2335
+ step('receiver-ack', 'blocked', 'predecessor_step_unavailable'),
2336
+ step('claim-route-transfer', 'blocked', 'predecessor_step_unavailable'),
2337
+ step('first-work', 'blocked', 'predecessor_step_unavailable'),
2338
+ step('task-retirement', 'blocked', 'predecessor_step_unavailable'),
2339
+ ], 'progressed', {
2340
+ next_action: 'Apply the validated successor bootstrap once; then wait for its registered correlated ACK.',
2341
+ });
2342
+ return { ok: true, report };
2343
+ }
2344
+ function ensureTaskLocalDirectory(taskRoot, directory, create) {
2345
+ try {
2346
+ const root = fs.realpathSync(taskRoot);
2347
+ if (root !== path.resolve(taskRoot) || !isPathInside(root, directory))
2348
+ return { ok: false, code: 'artifact_path_escape' };
2349
+ let current = root;
2350
+ const relative = path.relative(root, directory);
2351
+ for (const part of relative.split(path.sep).filter(Boolean)) {
2352
+ current = path.join(current, part);
2353
+ try {
2354
+ const stat = fs.lstatSync(current);
2355
+ if (!stat.isDirectory() ||
2356
+ stat.isSymbolicLink() ||
2357
+ fs.realpathSync(current) !== current)
2358
+ return { ok: false, code: 'artifact_directory_unsafe' };
2359
+ }
2360
+ catch (error) {
2361
+ const code = isRecord(error) ? error['code'] : null;
2362
+ if (code !== 'ENOENT' || !create)
2363
+ return {
2364
+ ok: false,
2365
+ code: code === 'ENOENT'
2366
+ ? 'artifact_missing'
2367
+ : 'artifact_directory_unavailable',
2368
+ };
2369
+ fs.mkdirSync(current, { mode: 0o700 });
2370
+ const stat = fs.lstatSync(current);
2371
+ if (!stat.isDirectory() ||
2372
+ stat.isSymbolicLink() ||
2373
+ fs.realpathSync(current) !== current)
2374
+ return { ok: false, code: 'artifact_directory_unsafe' };
2375
+ }
2376
+ }
2377
+ return { ok: true, root };
2378
+ }
2379
+ catch {
2380
+ return { ok: false, code: 'artifact_directory_unavailable' };
2381
+ }
2382
+ }
2383
+ function readTaskLocalFile(file, taskRoot, maxBytes) {
2384
+ if (!path.isAbsolute(file) || path.resolve(file) !== file)
2385
+ return { ok: false, code: 'artifact_path_invalid' };
2386
+ const directory = path.dirname(file);
2387
+ const safeDirectory = ensureTaskLocalDirectory(taskRoot, directory, false);
2388
+ if (!safeDirectory.ok)
2389
+ return safeDirectory.code === 'artifact_missing'
2390
+ ? { ok: false, code: 'missing' }
2391
+ : { ok: false, code: safeDirectory.code };
2392
+ try {
2393
+ const stat = fs.lstatSync(file);
2394
+ if (stat.isSymbolicLink() ||
2395
+ !stat.isFile() ||
2396
+ fs.realpathSync(file) !== file)
2397
+ return { ok: false, code: 'artifact_file_unsafe' };
2398
+ }
2399
+ catch (error) {
2400
+ const code = isRecord(error) ? error['code'] : null;
2401
+ return {
2402
+ ok: false,
2403
+ code: code === 'ENOENT' ? 'missing' : 'artifact_unreadable',
2404
+ };
2405
+ }
2406
+ const read = readBoundedFile(file, maxBytes);
2407
+ if (!read.ok)
2408
+ return { ok: false, code: `artifact_${read.code}` };
2409
+ try {
2410
+ if (!isPathInside(safeDirectory.root, fs.realpathSync(file)))
2411
+ return { ok: false, code: 'artifact_path_escape' };
2412
+ }
2413
+ catch {
2414
+ return { ok: false, code: 'artifact_unreadable' };
2415
+ }
2416
+ return { ok: true, bytes: read.bytes, digest: read.digest, file };
2417
+ }
2418
+ function writeTaskLocalExclusive(file, taskRoot, bytes, maxBytes) {
2419
+ if (bytes.length > maxBytes)
2420
+ return { ok: false, code: 'artifact_too_large' };
2421
+ const expectedDigest = digestText(bytes);
2422
+ const prior = readTaskLocalFile(file, taskRoot, maxBytes);
2423
+ if (prior.ok)
2424
+ return prior.bytes.equals(bytes)
2425
+ ? { ok: true, digest: prior.digest }
2426
+ : { ok: false, code: 'artifact_conflict' };
2427
+ if (prior.code !== 'missing')
2428
+ return { ok: false, code: prior.code };
2429
+ const directory = ensureTaskLocalDirectory(taskRoot, path.dirname(file), true);
2430
+ if (!directory.ok)
2431
+ return { ok: false, code: directory.code };
2432
+ let fd = null;
2433
+ try {
2434
+ const noFollow = fs.constants.O_NOFOLLOW ?? 0;
2435
+ fd = fs.openSync(file, fs.constants.O_WRONLY |
2436
+ fs.constants.O_CREAT |
2437
+ fs.constants.O_EXCL |
2438
+ noFollow, 0o600);
2439
+ let offset = 0;
2440
+ while (offset < bytes.length) {
2441
+ const written = fs.writeSync(fd, bytes, offset, bytes.length - offset, offset);
2442
+ if (written <= 0)
2443
+ throw new Error('short_write');
2444
+ offset += written;
2445
+ }
2446
+ fs.fsyncSync(fd);
2447
+ fs.closeSync(fd);
2448
+ fd = null;
2449
+ const directoryFd = fs.openSync(path.dirname(file), fs.constants.O_RDONLY);
2450
+ try {
2451
+ fs.fsyncSync(directoryFd);
2452
+ }
2453
+ finally {
2454
+ fs.closeSync(directoryFd);
2455
+ }
2456
+ }
2457
+ catch (error) {
2458
+ if (fd !== null) {
2459
+ try {
2460
+ fs.closeSync(fd);
2461
+ }
2462
+ catch {
2463
+ /* Preserve the original write outcome. */
2464
+ }
2465
+ }
2466
+ const code = isRecord(error) ? error['code'] : null;
2467
+ if (code === 'EEXIST') {
2468
+ const raced = readTaskLocalFile(file, taskRoot, maxBytes);
2469
+ return raced.ok && raced.bytes.equals(bytes)
2470
+ ? { ok: true, digest: raced.digest }
2471
+ : { ok: false, code: raced.ok ? 'artifact_conflict' : raced.code };
2472
+ }
2473
+ return { ok: false, code: 'artifact_write_unknown' };
2474
+ }
2475
+ const written = readTaskLocalFile(file, taskRoot, maxBytes);
2476
+ if (!written.ok)
2477
+ return { ok: false, code: 'artifact_write_unconfirmed' };
2478
+ return written.digest === expectedDigest && written.bytes.equals(bytes)
2479
+ ? { ok: true, digest: written.digest }
2480
+ : { ok: false, code: 'artifact_write_conflict' };
2481
+ }
2482
+ function bootstrapData(entry) {
2483
+ const value = entry
2484
+ .bootstrap;
2485
+ return isRecord(value) ? value : {};
2486
+ }
2487
+ function findBootstrapReservation(record, context) {
2488
+ const requestId = context.bootstrap.request.request_id;
2489
+ const matches = (record.launch_reservations ?? []).filter((entry) => {
2490
+ const data = bootstrapData(entry);
2491
+ return data['request_id'] === requestId;
2492
+ });
2493
+ return matches.length > 1 ? 'ambiguous' : (matches[0] ?? null);
2494
+ }
2495
+ function validateSuccessorBinding(context, record) {
2496
+ const request = context.request;
2497
+ const reservation = findBootstrapReservation(record, context);
2498
+ if (reservation === 'ambiguous')
2499
+ return {
2500
+ ok: false,
2501
+ code: 'bootstrap_reservation_ambiguous',
2502
+ message: 'More than one bootstrap reservation matches this transition.',
2503
+ };
2504
+ if (reservation === null)
2505
+ return {
2506
+ ok: false,
2507
+ code: 'bootstrap_reservation_missing',
2508
+ message: 'No task-bound successor bootstrap reservation is recorded.',
2509
+ };
2510
+ const data = bootstrapData(reservation);
2511
+ if (reservation.subtask_id !== request.intent.subtask_id ||
2512
+ reservation.attempt !== request.intent.attempt ||
2513
+ reservation.requested_session_id !== request.intent.to.session_id ||
2514
+ !sameHarnessIdentity(reservation.holder, request.intent.from) ||
2515
+ typeof reservation.released_at !== 'string' ||
2516
+ !Number.isFinite(Date.parse(reservation.released_at)) ||
2517
+ reservation.phase !== 'completed' ||
2518
+ !['submitted', 'already_recorded'].includes(String(reservation.outcome)) ||
2519
+ data['request_id'] !== context.bootstrap.request.request_id ||
2520
+ data['request_digest'] !== context.bootstrap.digest ||
2521
+ data['request_file'] !== context.bootstrap.file ||
2522
+ data['config_digest'] !==
2523
+ context.bootstrap.request.bootstrap.config_digest ||
2524
+ data['bundle_manifest_digest'] !==
2525
+ context.bootstrap.request.bootstrap.bundle_digest ||
2526
+ data['target_choice'] !== 'sibling' ||
2527
+ data['participant_agent_id'] !== request.intent.to.agent_id ||
2528
+ data['participant_session_id'] !== request.intent.to.session_id ||
2529
+ data['message_id'] !==
2530
+ context.bootstrap.request.message_pointer.message_id ||
2531
+ data['message_digest'] !==
2532
+ context.bootstrap.request.message_pointer.content_digest ||
2533
+ data['phase'] !== 'completed' ||
2534
+ !isText(data['binding_file'], 4096) ||
2535
+ !isText(data['herdr_agent'], 32))
2536
+ return {
2537
+ ok: false,
2538
+ code: 'bootstrap_reservation_mismatch',
2539
+ message: 'The durable bootstrap reservation does not prove this exact completed successor request.',
2540
+ };
2541
+ const file = data['binding_file'];
2542
+ const read = readTaskLocalFile(file, context.task.taskRoot, 64 * 1024);
2543
+ if (!read.ok)
2544
+ return {
2545
+ ok: false,
2546
+ code: read.code === 'missing'
2547
+ ? 'successor_binding_missing'
2548
+ : 'successor_binding_unreadable',
2549
+ message: 'The exact task-local bootstrap binding is unavailable or unsafe.',
2550
+ };
2551
+ let parsed;
2552
+ try {
2553
+ parsed = JSON.parse(read.bytes.toString('utf8'));
2554
+ }
2555
+ catch {
2556
+ return {
2557
+ ok: false,
2558
+ code: 'successor_binding_invalid',
2559
+ message: 'The task-local bootstrap binding is not valid JSON.',
2560
+ };
2561
+ }
2562
+ const result = collabBindingSchema.safeParse(parsed);
2563
+ if (!result.success ||
2564
+ result.data.task_id !== record.task_id ||
2565
+ !safePathEqual(result.data.task_root, context.task.taskRoot))
2566
+ return {
2567
+ ok: false,
2568
+ code: 'successor_binding_invalid',
2569
+ message: 'The task-local binding schema or task identity is invalid.',
2570
+ };
2571
+ const participants = result.data.participants.filter((participant) => participant.agent_id === request.intent.to.agent_id);
2572
+ if (participants.length !== 1)
2573
+ return {
2574
+ ok: false,
2575
+ code: 'successor_binding_ambiguous',
2576
+ message: 'The binding must contain exactly one successor participant.',
2577
+ };
2578
+ const participant = participants[0];
2579
+ if (participant.harness !== request.intent.to.harness ||
2580
+ participant.native_session_id !== request.intent.to.session_id ||
2581
+ !safePathEqual(participant.cwd, request.intent.to.cwd ?? '') ||
2582
+ participant.herdr_agent !== data['herdr_agent'])
2583
+ return {
2584
+ ok: false,
2585
+ code: 'successor_binding_identity_mismatch',
2586
+ message: 'The successor binding differs from the pinned agent, harness, cwd or Herdr name.',
2587
+ };
2588
+ const successorBinding = collabBindingSchema.safeParse({
2589
+ ...result.data,
2590
+ participants: [participant],
2591
+ });
2592
+ if (!successorBinding.success)
2593
+ return {
2594
+ ok: false,
2595
+ code: 'successor_route_binding_invalid',
2596
+ message: 'A successor-only task-local route binding could not be validated.',
2597
+ };
2598
+ return {
2599
+ ok: true,
2600
+ value: {
2601
+ source_file: read.file,
2602
+ source_digest: read.digest,
2603
+ participant,
2604
+ successor_binding: successorBinding.data,
2605
+ herdr_agent: participant.herdr_agent,
2606
+ },
2607
+ };
2608
+ }
2609
+ function routeBindingFailure(context, task, status, code, message, nextAction) {
2610
+ return {
2611
+ ok: false,
2612
+ report: failureReport('apply', context.request, status, code, message, nextAction, task),
2613
+ };
2614
+ }
2615
+ async function ensureSuccessorRouteBinding(context, initialTask, dependencies, now) {
2616
+ const request = context.request;
2617
+ const drift = currentStateDrift(initialTask.record, request, 'successor');
2618
+ if (drift !== null)
2619
+ return routeBindingFailure(context, initialTask, 'needs_input', drift.code, drift.message, 'Reconcile successor assignment, claim epochs and coordinator authority before route changes.');
2620
+ const validated = validateSuccessorBinding(context, initialTask.record);
2621
+ if (!validated.ok)
2622
+ return routeBindingFailure(context, initialTask, 'needs_input', validated.code, validated.message, 'Reconcile the exact completed bootstrap reservation and task-local binding; do not infer a route.');
2623
+ const file = path.join(initialTask.taskRoot, 'notifications', 'session-transition-bindings', `${request.transition_id}.json`);
2624
+ const bytes = Buffer.from(`${JSON.stringify(validated.value.successor_binding, null, 2)}\n`, 'utf8');
2625
+ const expectedDigest = digestText(bytes);
2626
+ let task = initialTask;
2627
+ let journal = journalEntry(task.record, request, context.requestDigest);
2628
+ if (journal === null)
2629
+ return routeBindingFailure(context, task, 'unknown', 'transition_journal_missing', 'The transition journal is missing or conflicts with its immutable request.', 'Inspect the authoritative task record before any route-binding effect.');
2630
+ const receipt = journal.effects['successor-route-binding'];
2631
+ if (receipt?.state === 'failed')
2632
+ return routeBindingFailure(context, task, 'needs_input', 'successor_route_binding_failed', 'A prior route-binding write has a known failed receipt and will not be replayed.', 'Inspect the exact task-local file and journal before continuing.');
2633
+ if (receipt?.state === 'completed') {
2634
+ const read = readTaskLocalFile(file, task.taskRoot, 64 * 1024);
2635
+ if (receipt.ref !== file ||
2636
+ receipt.digest !== expectedDigest ||
2637
+ journal.route_binding_ref !== file ||
2638
+ journal.route_binding_digest !== expectedDigest ||
2639
+ !read.ok ||
2640
+ read.digest !== expectedDigest ||
2641
+ !read.bytes.equals(bytes))
2642
+ return routeBindingFailure(context, task, 'reconcile_required', 'successor_route_binding_receipt_mismatch', 'The completed route-binding receipt no longer matches its exact task-local artifact.', 'Reconcile the binding file and journal; do not overwrite or re-create it.');
2643
+ return {
2644
+ ok: true,
2645
+ task,
2646
+ ref: file,
2647
+ digest: expectedDigest,
2648
+ binding: validated.value.successor_binding,
2649
+ };
2650
+ }
2651
+ if (receipt?.state === 'pending' || receipt?.state === 'unknown') {
2652
+ const read = readTaskLocalFile(file, task.taskRoot, 64 * 1024);
2653
+ if (!read.ok || read.digest !== expectedDigest || !read.bytes.equals(bytes))
2654
+ return routeBindingFailure(context, task, 'reconcile_required', 'successor_route_binding_outcome_unknown', 'A prior route-binding effect is pending/unknown and its exact artifact is not present for reconciliation.', 'Inspect the task-local path and journal; never replay an ambiguous write.');
2655
+ const reconciled = await recordEffectOutcome(context, task, 'successor-route-binding', 'completed', now(), null, {
2656
+ digest: expectedDigest,
2657
+ ref: file,
2658
+ artifact: 'route-binding',
2659
+ });
2660
+ if (!reconciled.ok)
2661
+ return routeBindingFailure(context, task, 'unknown', reconciled.code, 'The exact route binding exists but its durable receipt could not be reconciled.', 'Inspect the file and task journal before continuing.');
2662
+ task = reconciled.task;
2663
+ return {
2664
+ ok: true,
2665
+ task,
2666
+ ref: file,
2667
+ digest: expectedDigest,
2668
+ binding: validated.value.successor_binding,
2669
+ };
2670
+ }
2671
+ const intent = await recordEffectIntent(context, task, 'successor-route-binding', now, dependencies.signal);
2672
+ if (!intent.ok)
2673
+ return routeBindingFailure(context, task, intent.kind === 'invalid' ? 'needs_input' : 'reconcile_required', intent.code, intent.message, intent.nextAction);
2674
+ task = intent.task;
2675
+ const currentBinding = validateSuccessorBinding(context, task.record);
2676
+ if (!currentBinding.ok ||
2677
+ currentBinding.value.source_file !== validated.value.source_file ||
2678
+ currentBinding.value.source_digest !== validated.value.source_digest ||
2679
+ canonicalJson(currentBinding.value.successor_binding) !==
2680
+ canonicalJson(validated.value.successor_binding)) {
2681
+ const failed = await recordEffectOutcome(context, task, 'successor-route-binding', 'failed', now(), 'successor_binding_changed_before_write');
2682
+ const resultTask = failed.ok ? failed.task : task;
2683
+ return routeBindingFailure(context, resultTask, 'needs_input', 'successor_binding_changed_before_write', 'The source binding or bootstrap reservation changed after the durable effect intent.', 'Reconcile the exact binding and re-plan before any route file is written.');
2684
+ }
2685
+ const written = writeTaskLocalExclusive(file, task.taskRoot, bytes, 64 * 1024);
2686
+ if (!written.ok) {
2687
+ const unknown = await recordEffectOutcome(context, task, 'successor-route-binding', 'unknown', now(), written.code);
2688
+ const resultTask = unknown.ok ? unknown.task : task;
2689
+ return routeBindingFailure(context, resultTask, 'reconcile_required', 'successor_route_binding_outcome_unknown', `The successor route binding write did not return a confirmed result (${written.code}).`, 'Inspect the exact task-local artifact and journal; do not replay the write blindly.');
2690
+ }
2691
+ const recorded = await recordEffectOutcome(context, task, 'successor-route-binding', 'completed', now(), null, {
2692
+ digest: written.digest,
2693
+ ref: file,
2694
+ artifact: 'route-binding',
2695
+ });
2696
+ if (!recorded.ok)
2697
+ return routeBindingFailure(context, task, 'unknown', recorded.code, 'The route-binding file is written but its outcome receipt could not be persisted.', 'Inspect the binding and journal; do not overwrite or repeat the transfer.');
2698
+ return {
2699
+ ok: true,
2700
+ task: recorded.task,
2701
+ ref: file,
2702
+ digest: written.digest,
2703
+ binding: currentBinding.value.successor_binding,
2704
+ };
2705
+ }
2706
+ async function ensureTaskRetirementMarker(context, initialTask, routeBinding, dependencies, now) {
2707
+ const request = context.request;
2708
+ const drift = currentStateDrift(initialTask.record, request, 'retired');
2709
+ if (drift !== null)
2710
+ return {
2711
+ ok: false,
2712
+ report: failureReport('apply', request, 'needs_input', drift.code, drift.message, 'Reconcile successor authority and claim epochs before task retirement.', initialTask),
2713
+ };
2714
+ const succession = (initialTask.record.successions ?? []).find((item) => item.intent_id === request.intent.intent_id &&
2715
+ item.handoff_id === request.intent.handoff_id &&
2716
+ item.generation === request.intent.generation);
2717
+ if (succession === undefined)
2718
+ return {
2719
+ ok: false,
2720
+ report: failureReport('apply', request, 'needs_input', 'first_work_not_recorded', 'No exact core succession proves successor first work.', 'Register and apply the admitted successor REPORT before task retirement.', initialTask),
2721
+ };
2722
+ const validation = validateSuccession(initialTask.record, succession);
2723
+ if (!validation.ok)
2724
+ return {
2725
+ ok: false,
2726
+ report: failureReport('apply', request, 'needs_input', validation.code, validation.detail, 'Reconcile the first-work REPORT and recorded transfer boundary before task retirement.', initialTask),
2727
+ };
2728
+ const routeReceipt = journalEntry(initialTask.record, request, context.requestDigest);
2729
+ if (routeReceipt?.effects['successor-route-binding']?.state !== 'completed' ||
2730
+ routeReceipt?.route_binding_ref !== routeBinding.ref ||
2731
+ routeReceipt?.route_binding_digest !== routeBinding.digest)
2732
+ return {
2733
+ ok: false,
2734
+ report: failureReport('apply', request, 'reconcile_required', 'successor_route_binding_receipt_missing', 'Task retirement requires the exact durable successor route-binding receipt.', 'Reconcile the route binding before creating a retirement marker.', initialTask),
2735
+ };
2736
+ const routeFile = readTaskLocalFile(routeBinding.ref, initialTask.taskRoot, 64 * 1024);
2737
+ if (!routeFile.ok || routeFile.digest !== routeBinding.digest)
2738
+ return {
2739
+ ok: false,
2740
+ report: failureReport('apply', request, 'reconcile_required', 'successor_route_binding_missing', 'The recorded successor route binding is missing or has changed.', 'Reconcile the task-local binding before retirement; no archive marker was written.', initialTask),
2741
+ };
2742
+ const file = path.join(initialTask.taskRoot, 'archive', 'session-transitions', `${request.transition_id}.json`);
2743
+ let task = initialTask;
2744
+ let journal = journalEntry(task.record, request, context.requestDigest);
2745
+ if (journal === null)
2746
+ return {
2747
+ ok: false,
2748
+ report: failureReport('apply', request, 'unknown', 'transition_journal_missing', 'The retirement marker has no matching durable transition journal.', 'Inspect the task record before writing an archive marker.', task),
2749
+ };
2750
+ let receipt = journal.effects['task-retirement-marker'];
2751
+ let effectJustCreated = false;
2752
+ if (receipt?.state === 'failed')
2753
+ return {
2754
+ ok: false,
2755
+ report: failureReport('apply', request, 'needs_input', 'task_retirement_marker_failed', 'A prior retirement marker write has a known failed receipt and will not be replayed.', 'Inspect the exact task-local marker and journal before continuing.', task),
2756
+ };
2757
+ if (receipt === undefined) {
2758
+ const intent = await recordEffectIntent(context, task, 'task-retirement-marker', now, dependencies.signal);
2759
+ if (!intent.ok)
2760
+ return {
2761
+ ok: false,
2762
+ report: failureReport('apply', request, intent.kind === 'invalid' ? 'needs_input' : 'reconcile_required', intent.code, intent.message, intent.nextAction, task),
2763
+ };
2764
+ task = intent.task;
2765
+ effectJustCreated = true;
2766
+ journal = journalEntry(task.record, request, context.requestDigest);
2767
+ receipt = journal?.effects['task-retirement-marker'];
2768
+ }
2769
+ if (receipt === undefined)
2770
+ return {
2771
+ ok: false,
2772
+ report: failureReport('apply', request, 'unknown', 'task_retirement_intent_unconfirmed', 'The retirement intent was not durably recorded.', 'Inspect the transition journal; do not write an unjournaled marker.', task),
2773
+ };
2774
+ const marker = {
2775
+ schema: SESSION_TRANSITION_ARCHIVE_SCHEMA,
2776
+ mode: 'task-retirement',
2777
+ task_id: task.record.task_id,
2778
+ task_root: task.taskRoot,
2779
+ transition_id: request.transition_id,
2780
+ intent_id: request.intent.intent_id,
2781
+ handoff_id: request.intent.handoff_id,
2782
+ generation: request.intent.generation,
2783
+ recorded_at: receipt.intent_at,
2784
+ first_work: {
2785
+ message_id: validation.proof.message_id,
2786
+ digest: validation.proof.content_digest,
2787
+ },
2788
+ successor_route_binding: {
2789
+ ref: routeBinding.ref,
2790
+ digest: routeBinding.digest,
2791
+ },
2792
+ native_archive: 'unavailable',
2793
+ original_transcript: 'retained-in-place',
2794
+ reversible: true,
2795
+ };
2796
+ const bytes = Buffer.from(`${JSON.stringify(marker, null, 2)}\n`, 'utf8');
2797
+ const expectedDigest = digestText(bytes);
2798
+ if (receipt.state === 'completed') {
2799
+ const read = readTaskLocalFile(file, task.taskRoot, MAX_ARCHIVE_BYTES);
2800
+ if (receipt.ref !== file ||
2801
+ receipt.digest !== expectedDigest ||
2802
+ journal?.archive_ref !== file ||
2803
+ journal.archive_digest !== expectedDigest ||
2804
+ !read.ok ||
2805
+ read.digest !== expectedDigest ||
2806
+ !read.bytes.equals(bytes))
2807
+ return {
2808
+ ok: false,
2809
+ report: failureReport('apply', request, 'reconcile_required', 'task_retirement_receipt_mismatch', 'The completed reversible retirement marker no longer matches its durable receipt.', 'Reconcile the marker and task journal; do not overwrite or re-create it.', task),
2810
+ };
2811
+ return { ok: true, task, ref: file, digest: expectedDigest };
2812
+ }
2813
+ if ((receipt.state === 'pending' || receipt.state === 'unknown') &&
2814
+ !effectJustCreated) {
2815
+ const read = readTaskLocalFile(file, task.taskRoot, MAX_ARCHIVE_BYTES);
2816
+ if (!read.ok || read.digest !== expectedDigest || !read.bytes.equals(bytes))
2817
+ return {
2818
+ ok: false,
2819
+ report: failureReport('apply', request, 'reconcile_required', 'task_retirement_outcome_unknown', 'A prior retirement marker effect is pending/unknown and its exact artifact is not available for reconciliation.', 'Inspect the task-local path and journal; never replay an ambiguous archive write.', task),
2820
+ };
2821
+ const reconciled = await recordEffectOutcome(context, task, 'task-retirement-marker', 'completed', now(), null, {
2822
+ digest: expectedDigest,
2823
+ ref: file,
2824
+ artifact: 'task-retirement',
2825
+ });
2826
+ if (!reconciled.ok)
2827
+ return {
2828
+ ok: false,
2829
+ report: failureReport('apply', request, 'unknown', reconciled.code, 'The exact retirement marker exists but its durable receipt could not be reconciled.', 'Inspect the file and task journal before continuing.', task),
2830
+ };
2831
+ return {
2832
+ ok: true,
2833
+ task: reconciled.task,
2834
+ ref: file,
2835
+ digest: expectedDigest,
2836
+ };
2837
+ }
2838
+ const written = writeTaskLocalExclusive(file, task.taskRoot, bytes, MAX_ARCHIVE_BYTES);
2839
+ if (!written.ok) {
2840
+ const unknown = await recordEffectOutcome(context, task, 'task-retirement-marker', 'unknown', now(), written.code);
2841
+ const resultTask = unknown.ok ? unknown.task : task;
2842
+ return {
2843
+ ok: false,
2844
+ report: failureReport('apply', request, 'reconcile_required', 'task_retirement_outcome_unknown', `The retirement marker write did not return a confirmed result (${written.code}).`, 'Inspect the exact task-local artifact and journal; do not replay the write blindly.', resultTask),
2845
+ };
2846
+ }
2847
+ const recorded = await recordEffectOutcome(context, task, 'task-retirement-marker', 'completed', now(), null, {
2848
+ digest: written.digest,
2849
+ ref: file,
2850
+ artifact: 'task-retirement',
2851
+ });
2852
+ if (!recorded.ok)
2853
+ return {
2854
+ ok: false,
2855
+ report: failureReport('apply', request, 'unknown', recorded.code, 'The reversible retirement marker is written but its outcome receipt could not be persisted.', 'Inspect the marker and task journal; do not overwrite or repeat the transfer.', task),
2856
+ };
2857
+ return { ok: true, task: recorded.task, ref: file, digest: written.digest };
2858
+ }
2859
+ function herdrOpsFor(dependencies, taskRoot) {
2860
+ if (dependencies.herdr !== undefined)
2861
+ return dependencies.herdr;
2862
+ const bootstrap = dependencies.bootstrapDependencies;
2863
+ if (bootstrap?.ops !== undefined)
2864
+ return bootstrap.ops;
2865
+ return bootstrap?.transport === undefined
2866
+ ? createHerdrBootstrapOps({ transport: createHerdrTransport({ taskRoot }) })
2867
+ : createHerdrBootstrapOps({ transport: bootstrap.transport });
2868
+ }
2869
+ async function readRegisteredTaskMessage(context, messageId) {
2870
+ const opened = await openMessageTask(context.task.taskRoot);
2871
+ if (!opened.ok)
2872
+ return { ok: false, code: opened.code, message: opened.message };
2873
+ const registered = await readRegisteredMessageSafe(opened.value, messageId);
2874
+ if (!registered.ok)
2875
+ return { ok: false, code: registered.code, message: registered.message };
2876
+ return { ok: true, value: registered.value };
2877
+ }
2878
+ function registeredApplied(record, registered, type) {
2879
+ return (registered.message.type === type &&
2880
+ (record.messages ?? []).some((entry) => entry.message_id === registered.message.message_id &&
2881
+ entry.content_digest === registered.message.content_digest &&
2882
+ entry.state === 'applied' &&
2883
+ entry.type === type));
2884
+ }
2885
+ function transitionProgressReport(context, task, host, bootstrapReport, steps, nextAction) {
2886
+ return reportForContext('apply', context.request, task.record, null, bootstrapReport, host, steps, 'progressed', { next_action: nextAction });
2887
+ }
2888
+ async function applyConfirmedTransfer(context, initialTask, initialHost, dependencies, now, bootstrapReport = null) {
2889
+ const request = context.request;
2890
+ let task = initialTask;
2891
+ let handoff = getHandoff(task.record, request.intent.handoff_id);
2892
+ if (handoff === null)
2893
+ return failureReport('apply', request, 'unknown', 'handoff_missing', 'The exact receiver handoff is missing.', 'Inspect the authoritative task record before continuing.', task, { bootstrap: bootstrapReport });
2894
+ const ackId = request.evidence.ack_message_id ?? handoff.confirmation_message_id ?? null;
2895
+ if (handoff.state === 'transferring' && ackId === null)
2896
+ return failureReport('apply', request, 'waiting', 'receiver_ack_required', 'The successor has not supplied a correlated registered ACK.', 'Wait for the successor ACK, register/apply it, then reapply once with its message id.', task, {
2897
+ bootstrap: bootstrapReport,
2898
+ host: { id: dependencies.host?.id ?? 'not-loaded', ...initialHost },
2899
+ steps: [
2900
+ step('create-intent', 'satisfied'),
2901
+ step('source-quiescence', 'satisfied'),
2902
+ step('successor-bootstrap', bootstrapReport?.status === 'applied' ||
2903
+ bootstrapReport?.status === 'already_recorded'
2904
+ ? 'satisfied'
2905
+ : 'unknown'),
2906
+ step('receiver-ack', 'waiting', 'receiver_ack_required'),
2907
+ step('claim-route-transfer', 'blocked', 'predecessor_step_unavailable'),
2908
+ step('first-work', 'blocked', 'predecessor_step_unavailable'),
2909
+ step('task-retirement', 'blocked', 'predecessor_step_unavailable'),
2910
+ ],
2911
+ });
2912
+ if (ackId === null)
2913
+ return failureReport('apply', request, 'needs_input', 'receiver_ack_missing', 'No correlated receiver ACK id is recorded for this handoff.', 'Inspect the handoff state and register the exact ACK; do not infer recipient acceptance.', task, { bootstrap: bootstrapReport });
2914
+ const loadedAck = await readRegisteredTaskMessage(context, ackId);
2915
+ if (!loadedAck.ok)
2916
+ return failureReport('apply', request, 'waiting', 'receiver_ack_not_registered', `ACK ${ackId} is not available as a safe registered message (${loadedAck.code}).`, 'Register and apply the exact successor ACK; do not treat notification submission as acceptance.', task, { bootstrap: bootstrapReport });
2917
+ let ack = loadedAck.value;
2918
+ const currentAfterAckRead = await openTaskStore(request.task_root);
2919
+ if (!currentAfterAckRead.ok)
2920
+ return failureReport('apply', request, 'needs_input', currentAfterAckRead.code, currentAfterAckRead.message, currentAfterAckRead.nextAction, task, { bootstrap: bootstrapReport });
2921
+ task = currentAfterAckRead.value;
2922
+ if (!registeredApplied(task.record, ack, 'ACK'))
2923
+ return failureReport('apply', request, 'waiting', 'receiver_ack_not_applied', 'The exact ACK file exists but the current task ledger does not show it as applied.', 'Apply the registered ACK under the current task revision before transfer.', task, { bootstrap: bootstrapReport });
2924
+ handoff = getHandoff(task.record, request.intent.handoff_id);
2925
+ if (handoff === null)
2926
+ return failureReport('apply', request, 'unknown', 'handoff_missing', 'The exact handoff disappeared while reading the ACK.', 'Inspect the authoritative task record.', task, { bootstrap: bootstrapReport });
2927
+ if (handoff.state === 'transferring') {
2928
+ if (dependencies.signal?.aborted)
2929
+ return failureReport('apply', request, 'needs_input', 'abort_requested', 'The transition was aborted before receiver confirmation.', 'Reconcile the current ACK and task state before continuing.', task, { bootstrap: bootstrapReport });
2930
+ const outcome = transact(task.taskRoot, {
2931
+ actor: request.intent.from.agent_id,
2932
+ purpose: 'session-transition:confirm-receiver',
2933
+ expectRevision: task.record.state_revision,
2934
+ clock: now(),
2935
+ apply: (record) => {
2936
+ const drift = currentStateDrift(record, request, 'source');
2937
+ if (drift !== null)
2938
+ return { ok: false, reason: drift.code };
2939
+ const entry = (record.messages ?? []).find((item) => item.message_id === ackId);
2940
+ if (entry?.state !== 'applied' ||
2941
+ entry.content_digest !== ack.message.content_digest ||
2942
+ entry.type !== 'ACK')
2943
+ return { ok: false, reason: 'receiver_ack_not_applied' };
2944
+ const confirmed = rollover.confirmRolloverReceiver(record, {
2945
+ intent_id: request.intent.intent_id,
2946
+ message: ack.message,
2947
+ clock: now(),
2948
+ });
2949
+ if (!confirmed.ok)
2950
+ return confirmed;
2951
+ return {
2952
+ record,
2953
+ op: 'session-transition-confirm-receiver',
2954
+ summary: {
2955
+ transition_id: request.transition_id,
2956
+ ack_message_id: ackId,
2957
+ },
2958
+ result: confirmed,
2959
+ };
2960
+ },
2961
+ });
2962
+ if (!outcome.ok) {
2963
+ const waiting = outcome.reason === 'receiver_ack_not_applied' ||
2964
+ outcome.reason === 'ack_not_registered' ||
2965
+ outcome.reason === 'ack_not_applied';
2966
+ return failureReport('apply', request, waiting ? 'waiting' : 'needs_input', outcome.reason, 'Core rollover refused receiver confirmation.', 'Reconcile the exact ACK, current epoch, assignment and claims before any transfer.', task, { bootstrap: bootstrapReport });
2967
+ }
2968
+ const confirmedTask = await openTaskStore(request.task_root);
2969
+ if (!confirmedTask.ok)
2970
+ return failureReport('apply', request, 'unknown', confirmedTask.code, confirmedTask.message, confirmedTask.nextAction, task, { bootstrap: bootstrapReport });
2971
+ task = confirmedTask.value;
2972
+ handoff = getHandoff(task.record, request.intent.handoff_id);
2973
+ }
2974
+ if (handoff?.state !== 'receiver_confirmed')
2975
+ return failureReport('apply', request, 'unknown', 'receiver_confirmation_unconfirmed', 'The durable task record does not confirm the receiver ACK.', 'Inspect the exact ACK, handoff and task revision before any transfer.', task, { bootstrap: bootstrapReport });
2976
+ if (handoff.confirmation_message_id !== ackId)
2977
+ return failureReport('apply', request, 'invalid', 'receiver_ack_conflict', 'The ACK id differs from the confirmation already recorded for this handoff.', 'Use the exact recorded confirmation id; do not replace receiver evidence.', task, { bootstrap: bootstrapReport });
2978
+ if (!registeredApplied(task.record, ack, 'ACK')) {
2979
+ const refreshedAck = await readRegisteredTaskMessage(context, ackId);
2980
+ if (!refreshedAck.ok ||
2981
+ !registeredApplied(task.record, refreshedAck.value, 'ACK'))
2982
+ return failureReport('apply', request, 'needs_input', 'receiver_ack_receipt_changed', 'The recorded ACK is no longer present as the exact applied task message.', 'Reconcile the registered message digest and handoff confirmation before transfer.', task, { bootstrap: bootstrapReport });
2983
+ ack = refreshedAck.value;
2984
+ }
2985
+ const bindingBefore = validateSuccessorBinding(context, task.record);
2986
+ if (!bindingBefore.ok)
2987
+ return failureReport('apply', request, 'needs_input', bindingBefore.code, bindingBefore.message, 'Reconcile the exact bootstrap reservation and successor binding before claim transfer.', task, { bootstrap: bootstrapReport });
2988
+ const herdr = herdrOpsFor(dependencies, task.taskRoot);
2989
+ let listed;
2990
+ try {
2991
+ listed = await herdr.listAgents();
2992
+ }
2993
+ catch {
2994
+ return failureReport('apply', request, 'needs_input', 'herdr_agent_list_unavailable', 'The live successor identity could not be re-observed.', 'Refresh Herdr metadata for the exact successor binding; no claim transfer was attempted.', task, { bootstrap: bootstrapReport });
2995
+ }
2996
+ if (!listed.ok)
2997
+ return failureReport('apply', request, 'needs_input', 'herdr_agent_list_unavailable', listed.detail, 'Refresh Herdr metadata for the exact successor binding; no claim transfer was attempted.', task, { bootstrap: bootstrapReport });
2998
+ const liveMatches = listed.agents.filter((agent) => agent.name === bindingBefore.value.herdr_agent);
2999
+ if (liveMatches.length !== 1)
3000
+ return failureReport('apply', request, 'needs_input', 'successor_live_identity_not_unique', 'Herdr does not report exactly one live participant for the reserved successor name.', 'Reconcile the exact task-local binding and live Herdr roster; no claims were transferred.', task, { bootstrap: bootstrapReport });
3001
+ const live = liveMatches[0];
3002
+ const liveSession = reportedSessionIdentity(live.sessionKind, live.sessionValue);
3003
+ if (live.paneId !== bindingBefore.value.participant.pane_id ||
3004
+ live.harness !== request.intent.to.harness ||
3005
+ live.cwd === null ||
3006
+ !safePathEqual(live.cwd, request.intent.to.cwd ?? '') ||
3007
+ liveSession.status !== 'match' ||
3008
+ liveSession.identity !== request.intent.to.session_id)
3009
+ return failureReport('apply', request, 'needs_input', 'successor_live_identity_mismatch', 'Live Herdr pane, harness, cwd or reported native session differs from the reserved successor binding.', 'Reconcile the exact successor identity; Herdr metadata does not prove process affiliation.', task, { bootstrap: bootstrapReport });
3010
+ const host = dependencies.host;
3011
+ if (host === undefined)
3012
+ return failureReport('apply', request, 'needs_input', 'pi_transition_host_required', 'The source host disappeared before ownership transfer.', 'Reopen the opt-in source-session adapter and re-plan; no claims were transferred.', task, { bootstrap: bootstrapReport });
3013
+ let currentHost;
3014
+ try {
3015
+ currentHost = await host.observe();
3016
+ }
3017
+ catch {
3018
+ return failureReport('apply', request, 'waiting', 'host_observation_unknown', 'The source session could not be re-observed immediately before claim transfer.', 'Reconcile the exact source freeze and complete quiescence proof; no transfer was attempted.', task, { bootstrap: bootstrapReport });
3019
+ }
3020
+ const sourceIdentityIssue = hostIdentityFailure(currentHost, request);
3021
+ if (sourceIdentityIssue !== null)
3022
+ return failureReport('apply', request, 'needs_input', sourceIdentityIssue.code, sourceIdentityIssue.message, sourceIdentityIssue.nextAction, task, { host: { id: host.id, ...currentHost }, bootstrap: bootstrapReport });
3023
+ const proofIssue = proofSupportFailure(currentHost, request, now());
3024
+ if (proofIssue !== null)
3025
+ return failureReport('apply', request, 'waiting', proofIssue.code, proofIssue.message, 'Keep the source frozen and reconcile a fresh complete native proof before moving claims.', task, { host: { id: host.id, ...currentHost }, bootstrap: bootstrapReport });
3026
+ const transferProof = currentHost.proof;
3027
+ const transferInventoryDigest = transferProof === null
3028
+ ? null
3029
+ : quiescenceInventoryDigest(transferProof.inventory);
3030
+ if (transferProof === null || transferInventoryDigest === null)
3031
+ return failureReport('apply', request, 'waiting', 'host_quiescence_proof_invalid', 'The immediate pre-transfer observation did not include a complete inventory digest.', 'Reconcile a fresh complete native proof; no claims were transferred.', task, { host: { id: host.id, ...currentHost }, bootstrap: bootstrapReport });
3032
+ const transferQuiescenceReceipt = quiescenceReceipt(transferProof, transferInventoryDigest);
3033
+ const unchanged = await assertTaskStoreCurrent(task);
3034
+ if (unchanged !== null)
3035
+ return failureReport('apply', request, 'needs_input', unchanged.code, unchanged.message, unchanged.nextAction, task, { host: { id: host.id, ...currentHost }, bootstrap: bootstrapReport });
3036
+ const latest = await openTaskStore(request.task_root);
3037
+ if (!latest.ok)
3038
+ return failureReport('apply', request, 'needs_input', latest.code, latest.message, latest.nextAction, task, { host: { id: host.id, ...currentHost }, bootstrap: bootstrapReport });
3039
+ task = latest.value;
3040
+ const drift = currentStateDrift(task.record, request, 'source');
3041
+ if (drift !== null)
3042
+ return failureReport('apply', request, 'needs_input', drift.code, drift.message, 'Reconcile current attempt, approval/cancellation, coordinator epoch, source identity and claims before transfer.', task, { host: { id: host.id, ...currentHost }, bootstrap: bootstrapReport });
3043
+ handoff = getHandoff(task.record, request.intent.handoff_id);
3044
+ if (handoff?.state !== 'receiver_confirmed' ||
3045
+ handoff.confirmation_message_id !== ackId)
3046
+ return failureReport('apply', request, 'needs_input', 'handoff_confirmation_changed', 'Receiver confirmation changed before claim transfer.', 'Reconcile the authoritative handoff and exact ACK before continuing.', task, { host: { id: host.id, ...currentHost }, bootstrap: bootstrapReport });
3047
+ const bindingNow = validateSuccessorBinding(context, task.record);
3048
+ if (!bindingNow.ok ||
3049
+ bindingNow.value.source_file !== bindingBefore.value.source_file ||
3050
+ bindingNow.value.source_digest !== bindingBefore.value.source_digest ||
3051
+ bindingNow.value.participant.pane_id !== live.paneId)
3052
+ return failureReport('apply', request, 'needs_input', 'successor_binding_changed', bindingNow.ok
3053
+ ? 'The exact successor binding changed after live identity observation.'
3054
+ : bindingNow.message, 'Reconcile task-local binding and live Herdr evidence before transfer.', task, { host: { id: host.id, ...currentHost }, bootstrap: bootstrapReport });
3055
+ const nativeEvidenceRef = `${herdr.id}:${live.name}:session:${liveSession.identity}`;
3056
+ const transfer = transact(task.taskRoot, {
3057
+ actor: request.intent.from.agent_id,
3058
+ purpose: 'session-transition:complete-transfer',
3059
+ expectRevision: task.record.state_revision,
3060
+ clock: now(),
3061
+ apply: (record) => {
3062
+ if (dependencies.signal?.aborted)
3063
+ return { ok: false, reason: 'abort_requested' };
3064
+ const sourceDrift = currentStateDrift(record, request, 'source');
3065
+ if (sourceDrift !== null)
3066
+ return { ok: false, reason: sourceDrift.code };
3067
+ const currentHandoff = getHandoff(record, request.intent.handoff_id);
3068
+ if (currentHandoff?.state !== 'receiver_confirmed' ||
3069
+ currentHandoff.confirmation_message_id !== ackId)
3070
+ return { ok: false, reason: 'handoff_confirmation_changed' };
3071
+ const ackEntry = (record.messages ?? []).find((entry) => entry.message_id === ackId);
3072
+ if (ackEntry?.state !== 'applied' ||
3073
+ ackEntry.content_digest !== ack.message.content_digest ||
3074
+ ackEntry.type !== 'ACK')
3075
+ return { ok: false, reason: 'receiver_ack_receipt_changed' };
3076
+ const successorBinding = validateSuccessorBinding(context, record);
3077
+ if (!successorBinding.ok ||
3078
+ successorBinding.value.source_digest !==
3079
+ bindingBefore.value.source_digest)
3080
+ return { ok: false, reason: 'successor_binding_changed' };
3081
+ const completed = rollover.completeRolloverTransfer(record, {
3082
+ intent_id: request.intent.intent_id,
3083
+ successor_native: {
3084
+ agent_id: request.intent.to.agent_id,
3085
+ runner: 'pi',
3086
+ session_id: request.intent.to.session_id,
3087
+ run_id: null,
3088
+ mission_id: null,
3089
+ evidence_ref: nativeEvidenceRef,
3090
+ observed_at: new Date(now()).toISOString(),
3091
+ },
3092
+ actor: request.intent.from.agent_id,
3093
+ clock: now(),
3094
+ });
3095
+ if (!completed.ok)
3096
+ return completed;
3097
+ const savedQuiescence = setJournalQuiescenceReceipt(record, request.transition_id, 'transfer', transferQuiescenceReceipt);
3098
+ if (!savedQuiescence.ok)
3099
+ return { ok: false, reason: savedQuiescence.code };
3100
+ return {
3101
+ record,
3102
+ op: 'session-transition-complete-transfer',
3103
+ summary: {
3104
+ transition_id: request.transition_id,
3105
+ ack_message_id: ackId,
3106
+ },
3107
+ result: completed,
3108
+ };
3109
+ },
3110
+ });
3111
+ if (!transfer.ok)
3112
+ return failureReport('apply', request, 'needs_input', transfer.reason, 'Core rollover refused the ownership/claim transfer.', 'Reconcile the exact ACK, successor binding, live Herdr session, source proof and current claims before retrying.', task, { host: { id: host.id, ...currentHost }, bootstrap: bootstrapReport });
3113
+ const transferredTask = await openTaskStore(request.task_root);
3114
+ if (!transferredTask.ok)
3115
+ return failureReport('apply', request, 'unknown', transferredTask.code, transferredTask.message, transferredTask.nextAction, task, { host: { id: host.id, ...currentHost }, bootstrap: bootstrapReport });
3116
+ const routeBinding = await ensureSuccessorRouteBinding(context, transferredTask.value, dependencies, now);
3117
+ if (!routeBinding.ok)
3118
+ return {
3119
+ ...routeBinding.report,
3120
+ host: { id: host.id, ...currentHost },
3121
+ bootstrap: bootstrapReport,
3122
+ };
3123
+ return transitionProgressReport(context, routeBinding.task, currentHost, bootstrapReport, [
3124
+ step('create-intent', 'satisfied'),
3125
+ step('source-quiescence', 'satisfied'),
3126
+ step('successor-bootstrap', 'satisfied'),
3127
+ step('receiver-ack', 'satisfied'),
3128
+ step('claim-route-transfer', 'satisfied'),
3129
+ step('first-work', 'waiting'),
3130
+ step('task-retirement', 'blocked', 'predecessor_step_unavailable'),
3131
+ ], 'The task assignee and claims moved after correlated ACK, and a successor-only task-local binding was recorded. Wait for the admitted successor REPORT; native session archival remains unavailable.');
3132
+ }
3133
+ async function applyFirstWorkAndRetirement(context, initialTask, hostObservation, dependencies, now) {
3134
+ const request = context.request;
3135
+ const observedHost = hostObservation ?? {};
3136
+ let task = initialTask;
3137
+ const handoff = getHandoff(task.record, request.intent.handoff_id);
3138
+ if (handoff?.state !== 'completed')
3139
+ return failureReport('apply', request, 'needs_input', 'transfer_not_completed', 'Successor first work cannot be recorded before the core claim/assignee transfer.', 'Reconcile the receiver ACK and transfer boundary first.', task, {
3140
+ host: { id: dependencies.host?.id ?? 'not-loaded', ...observedHost },
3141
+ });
3142
+ const routeBinding = await ensureSuccessorRouteBinding(context, task, dependencies, now);
3143
+ if (!routeBinding.ok)
3144
+ return {
3145
+ ...routeBinding.report,
3146
+ host: { id: dependencies.host?.id ?? 'not-loaded', ...observedHost },
3147
+ };
3148
+ task = routeBinding.task;
3149
+ let succession = (task.record.successions ?? []).find((item) => item.intent_id === request.intent.intent_id &&
3150
+ item.handoff_id === request.intent.handoff_id &&
3151
+ item.generation === request.intent.generation);
3152
+ if (succession !== undefined) {
3153
+ const validation = validateSuccession(task.record, succession);
3154
+ if (!validation.ok)
3155
+ return failureReport('apply', request, 'needs_input', validation.code, validation.detail, 'Reconcile the exact first-work REPORT and transfer boundary; retirement remains blocked.', task, {
3156
+ host: {
3157
+ id: dependencies.host?.id ?? 'not-loaded',
3158
+ ...observedHost,
3159
+ },
3160
+ });
3161
+ if (request.evidence.first_work_report_id !== null &&
3162
+ request.evidence.first_work_report_id !== validation.proof.message_id)
3163
+ return failureReport('apply', request, 'invalid', 'first_work_report_conflict', 'The request REPORT id differs from the first-work proof already recorded for this transition.', 'Use the exact recorded first-work report; do not replace accepted evidence.', task, {
3164
+ host: {
3165
+ id: dependencies.host?.id ?? 'not-loaded',
3166
+ ...observedHost,
3167
+ },
3168
+ });
3169
+ }
3170
+ else {
3171
+ const reportId = request.evidence.first_work_report_id;
3172
+ if (reportId === null)
3173
+ return failureReport('apply', request, 'waiting', 'first_work_report_required', 'Task retirement is waiting for an admitted successor first-work REPORT.', 'Register/apply the exact completed successor REPORT, then reapply with its message id; no archive marker is written before it.', task, {
3174
+ host: {
3175
+ id: dependencies.host?.id ?? 'not-loaded',
3176
+ ...observedHost,
3177
+ },
3178
+ steps: [
3179
+ step('create-intent', 'satisfied'),
3180
+ step('source-quiescence', 'satisfied'),
3181
+ step('successor-bootstrap', 'satisfied'),
3182
+ step('receiver-ack', 'satisfied'),
3183
+ step('claim-route-transfer', 'satisfied'),
3184
+ step('first-work', 'waiting', 'first_work_report_required'),
3185
+ step('task-retirement', 'blocked', 'predecessor_step_unavailable'),
3186
+ ],
3187
+ });
3188
+ const loadedReport = await readRegisteredTaskMessage(context, reportId);
3189
+ if (!loadedReport.ok)
3190
+ return failureReport('apply', request, 'waiting', 'first_work_report_not_registered', `REPORT ${reportId} is not available as a safe registered message (${loadedReport.code}).`, 'Register/apply the exact successor REPORT; submission alone is not first-work evidence.', task, {
3191
+ host: {
3192
+ id: dependencies.host?.id ?? 'not-loaded',
3193
+ ...observedHost,
3194
+ },
3195
+ });
3196
+ const latest = await openTaskStore(request.task_root);
3197
+ if (!latest.ok)
3198
+ return failureReport('apply', request, 'needs_input', latest.code, latest.message, latest.nextAction, task, {
3199
+ host: {
3200
+ id: dependencies.host?.id ?? 'not-loaded',
3201
+ ...observedHost,
3202
+ },
3203
+ });
3204
+ task = latest.value;
3205
+ const drift = currentStateDrift(task.record, request, 'successor');
3206
+ if (drift !== null)
3207
+ return failureReport('apply', request, 'needs_input', drift.code, drift.message, 'Reconcile successor attempt, approval/cancellation, input, epoch, native identity and claim epochs before first-work recording.', task, {
3208
+ host: {
3209
+ id: dependencies.host?.id ?? 'not-loaded',
3210
+ ...observedHost,
3211
+ },
3212
+ });
3213
+ if (!registeredApplied(task.record, loadedReport.value, 'REPORT'))
3214
+ return failureReport('apply', request, 'waiting', 'first_work_report_not_applied', 'The REPORT file exists but the task ledger does not show it as applied.', 'Apply the exact registered successor REPORT before first-work recording.', task, {
3215
+ host: {
3216
+ id: dependencies.host?.id ?? 'not-loaded',
3217
+ ...observedHost,
3218
+ },
3219
+ });
3220
+ const routeReceipt = journalEntry(task.record, request, context.requestDigest);
3221
+ const routeFile = readTaskLocalFile(routeBinding.ref, task.taskRoot, 64 * 1024);
3222
+ if (routeReceipt?.effects['successor-route-binding']?.state !== 'completed' ||
3223
+ routeReceipt?.route_binding_ref !== routeBinding.ref ||
3224
+ routeReceipt?.route_binding_digest !== routeBinding.digest ||
3225
+ !routeFile.ok ||
3226
+ routeFile.digest !== routeBinding.digest)
3227
+ return failureReport('apply', request, 'reconcile_required', 'successor_route_binding_receipt_missing', 'The route binding changed before first-work recording.', 'Reconcile the successor-only binding and journal before proceeding.', task, {
3228
+ host: {
3229
+ id: dependencies.host?.id ?? 'not-loaded',
3230
+ ...observedHost,
3231
+ },
3232
+ });
3233
+ const outcome = transact(task.taskRoot, {
3234
+ actor: request.intent.to.agent_id,
3235
+ purpose: 'session-transition:record-first-work',
3236
+ expectRevision: task.record.state_revision,
3237
+ clock: now(),
3238
+ apply: (record) => {
3239
+ if (dependencies.signal?.aborted)
3240
+ return { ok: false, reason: 'abort_requested' };
3241
+ const currentDrift = currentStateDrift(record, request, 'successor');
3242
+ if (currentDrift !== null)
3243
+ return { ok: false, reason: currentDrift.code };
3244
+ const savedRoute = journalEntry(record, request, context.requestDigest);
3245
+ if (savedRoute?.effects['successor-route-binding']?.state !==
3246
+ 'completed' ||
3247
+ savedRoute?.route_binding_ref !== routeBinding.ref ||
3248
+ savedRoute?.route_binding_digest !== routeBinding.digest)
3249
+ return {
3250
+ ok: false,
3251
+ reason: 'successor_route_binding_receipt_missing',
3252
+ };
3253
+ const entry = (record.messages ?? []).find((item) => item.message_id === reportId);
3254
+ if (entry?.state !== 'applied' ||
3255
+ entry.content_digest !== loadedReport.value.message.content_digest ||
3256
+ entry.type !== 'REPORT')
3257
+ return {
3258
+ ok: false,
3259
+ reason: 'first_work_report_not_applied',
3260
+ };
3261
+ const recorded = rollover.recordRolloverFirstWork(record, {
3262
+ intent_id: request.intent.intent_id,
3263
+ message: loadedReport.value.message,
3264
+ clock: now(),
3265
+ });
3266
+ if (!recorded.ok)
3267
+ return recorded;
3268
+ return {
3269
+ record,
3270
+ op: 'session-transition-record-first-work',
3271
+ summary: {
3272
+ transition_id: request.transition_id,
3273
+ report_message_id: reportId,
3274
+ },
3275
+ result: recorded,
3276
+ };
3277
+ },
3278
+ });
3279
+ if (!outcome.ok) {
3280
+ const waiting = outcome.reason === 'first_work_report_not_registered' ||
3281
+ outcome.reason === 'first_work_report_not_applied';
3282
+ return failureReport('apply', request, waiting ? 'waiting' : 'needs_input', outcome.reason, 'Core rollover refused the first-work proof.', 'Reconcile the registered REPORT, post-transfer boundary and current successor claims before retirement.', task, {
3283
+ host: {
3284
+ id: dependencies.host?.id ?? 'not-loaded',
3285
+ ...observedHost,
3286
+ },
3287
+ });
3288
+ }
3289
+ const recordedTask = await openTaskStore(request.task_root);
3290
+ if (!recordedTask.ok)
3291
+ return failureReport('apply', request, 'unknown', recordedTask.code, recordedTask.message, recordedTask.nextAction, task, {
3292
+ host: {
3293
+ id: dependencies.host?.id ?? 'not-loaded',
3294
+ ...observedHost,
3295
+ },
3296
+ });
3297
+ task = recordedTask.value;
3298
+ succession = (task.record.successions ?? []).find((item) => item.intent_id === request.intent.intent_id &&
3299
+ item.handoff_id === request.intent.handoff_id &&
3300
+ item.generation === request.intent.generation);
3301
+ if (succession === undefined)
3302
+ return failureReport('apply', request, 'unknown', 'first_work_outcome_unconfirmed', 'The core first-work reducer returned without a matching succession record.', 'Inspect the authoritative task record; do not submit a second report.', task, {
3303
+ host: {
3304
+ id: dependencies.host?.id ?? 'not-loaded',
3305
+ ...observedHost,
3306
+ },
3307
+ });
3308
+ }
3309
+ const validation = validateSuccession(task.record, succession);
3310
+ if (!validation.ok)
3311
+ return failureReport('apply', request, 'needs_input', validation.code, validation.detail, 'Reconcile the exact first-work REPORT and transfer boundary before retirement.', task, {
3312
+ host: { id: dependencies.host?.id ?? 'not-loaded', ...observedHost },
3313
+ });
3314
+ const exclusion = routeExclusionFor(task.record, request.intent.from);
3315
+ if (exclusion.state !== 'excluded')
3316
+ return failureReport('apply', request, 'needs_input', exclusion.conflict ?? 'route_exclusion_unconfirmed', 'The validated succession does not yet exclude the old identity from notify routing.', 'Reconcile the succession record before retirement; native routing removal is still not claimed.', task, {
3317
+ host: { id: dependencies.host?.id ?? 'not-loaded', ...observedHost },
3318
+ });
3319
+ const retirement = await ensureTaskRetirementMarker(context, task, routeBinding, dependencies, now);
3320
+ if (!retirement.ok)
3321
+ return {
3322
+ ...retirement.report,
3323
+ host: { id: dependencies.host?.id ?? 'not-loaded', ...observedHost },
3324
+ };
3325
+ return transitionProgressReport(context, retirement.task, hostObservation, null, [
3326
+ step('create-intent', 'satisfied'),
3327
+ step('source-quiescence', 'satisfied'),
3328
+ step('successor-bootstrap', 'satisfied'),
3329
+ step('receiver-ack', 'satisfied'),
3330
+ step('claim-route-transfer', 'satisfied'),
3331
+ step('first-work', 'satisfied'),
3332
+ step('task-retirement', 'satisfied'),
3333
+ ], 'A reversible task-retirement marker is recorded; the original transcript remains in place and native Pi session archive is unavailable.');
3334
+ }