@quolu/lattice 0.1.0 → 0.12.2

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 (370) hide show
  1. package/README.md +87 -4
  2. package/bin/lattice-bridge.mjs +87 -0
  3. package/bin/lattice-dashboard.mjs +70 -0
  4. package/bin/lattice-mcp.mjs +9 -9
  5. package/bin/lattice.mjs +61 -8
  6. package/docs/bridge-setup.md +87 -0
  7. package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -0
  8. package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -0
  9. package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -0
  10. package/package.json +19 -7
  11. package/sensor/dist/bin/command-supervision.d.ts +1 -1
  12. package/sensor/dist/bin/command-supervision.js +4 -4
  13. package/sensor/dist/bin/command-supervision.js.map +1 -1
  14. package/sensor/dist/bin/fatal-handler.js +2 -2
  15. package/sensor/dist/bin/fatal-handler.js.map +1 -1
  16. package/sensor/dist/bin/{codegraph.js → lattice-sensor.js} +207 -328
  17. package/sensor/dist/bin/node-version-check.d.ts +7 -5
  18. package/sensor/dist/bin/node-version-check.d.ts.map +1 -1
  19. package/sensor/dist/bin/node-version-check.js +19 -14
  20. package/sensor/dist/bin/node-version-check.js.map +1 -1
  21. package/sensor/dist/context/index.d.ts +1 -1
  22. package/sensor/dist/context/index.js +6 -6
  23. package/sensor/dist/context/index.js.map +1 -1
  24. package/sensor/dist/db/index.d.ts +5 -5
  25. package/sensor/dist/db/index.d.ts.map +1 -1
  26. package/sensor/dist/db/index.js +7 -7
  27. package/sensor/dist/db/index.js.map +1 -1
  28. package/sensor/dist/db/migrations.js +1 -1
  29. package/sensor/dist/db/migrations.js.map +1 -1
  30. package/sensor/dist/db/queries.d.ts +1 -1
  31. package/sensor/dist/db/queries.js +5 -5
  32. package/sensor/dist/db/queries.js.map +1 -1
  33. package/sensor/dist/db/schema.sql +2 -2
  34. package/sensor/dist/db/sqlite-adapter.d.ts +2 -2
  35. package/sensor/dist/db/sqlite-adapter.js +3 -3
  36. package/sensor/dist/db/sqlite-adapter.js.map +1 -1
  37. package/sensor/dist/db/wal-valve.d.ts +1 -1
  38. package/sensor/dist/db/wal-valve.d.ts.map +1 -1
  39. package/sensor/dist/db/wal-valve.js +2 -2
  40. package/sensor/dist/db/wal-valve.js.map +1 -1
  41. package/sensor/dist/directory.d.ts +27 -63
  42. package/sensor/dist/directory.d.ts.map +1 -1
  43. package/sensor/dist/directory.js +78 -134
  44. package/sensor/dist/directory.js.map +1 -1
  45. package/sensor/dist/errors.d.ts +15 -15
  46. package/sensor/dist/errors.d.ts.map +1 -1
  47. package/sensor/dist/errors.js +21 -21
  48. package/sensor/dist/errors.js.map +1 -1
  49. package/sensor/dist/extraction/dynamic-import.d.ts +1 -1
  50. package/sensor/dist/extraction/dynamic-import.js +1 -1
  51. package/sensor/dist/extraction/extraction-version.d.ts +1 -1
  52. package/sensor/dist/extraction/extraction-version.js +1 -1
  53. package/sensor/dist/extraction/generated-detection.d.ts +1 -1
  54. package/sensor/dist/extraction/generated-detection.js +1 -1
  55. package/sensor/dist/extraction/grammars.d.ts +3 -3
  56. package/sensor/dist/extraction/grammars.js +5 -5
  57. package/sensor/dist/extraction/grammars.js.map +1 -1
  58. package/sensor/dist/extraction/index.d.ts +6 -6
  59. package/sensor/dist/extraction/index.js +50 -50
  60. package/sensor/dist/extraction/index.js.map +1 -1
  61. package/sensor/dist/extraction/kernel/decode.d.ts +1 -1
  62. package/sensor/dist/extraction/kernel/decode.js +1 -1
  63. package/sensor/dist/extraction/kernel/index.d.ts +3 -3
  64. package/sensor/dist/extraction/kernel/index.js +6 -6
  65. package/sensor/dist/extraction/kernel/index.js.map +1 -1
  66. package/sensor/dist/extraction/kernel/layout.d.ts +1 -1
  67. package/sensor/dist/extraction/kernel/layout.js +1 -1
  68. package/sensor/dist/extraction/kernel/loader.d.ts +6 -6
  69. package/sensor/dist/extraction/kernel/loader.js +13 -13
  70. package/sensor/dist/extraction/kernel/loader.js.map +1 -1
  71. package/sensor/dist/extraction/languages/cfquery.d.ts +1 -1
  72. package/sensor/dist/extraction/languages/cfquery.js +1 -1
  73. package/sensor/dist/extraction/languages/lua.d.ts +11 -0
  74. package/sensor/dist/extraction/languages/lua.d.ts.map +1 -1
  75. package/sensor/dist/extraction/languages/lua.js +1 -0
  76. package/sensor/dist/extraction/languages/lua.js.map +1 -1
  77. package/sensor/dist/extraction/parse-pool.d.ts +5 -5
  78. package/sensor/dist/extraction/parse-pool.d.ts.map +1 -1
  79. package/sensor/dist/extraction/parse-pool.js +4 -4
  80. package/sensor/dist/extraction/parse-worker.js +1 -1
  81. package/sensor/dist/extraction/parse-worker.js.map +1 -1
  82. package/sensor/dist/extraction/store-writer.d.ts +1 -1
  83. package/sensor/dist/extraction/store-writer.js +1 -1
  84. package/sensor/dist/extraction/svelte-extractor.d.ts +1 -1
  85. package/sensor/dist/extraction/svelte-extractor.js +1 -1
  86. package/sensor/dist/extraction/tree-sitter-helpers.js +1 -1
  87. package/sensor/dist/extraction/tree-sitter.d.ts +2 -2
  88. package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
  89. package/sensor/dist/extraction/tree-sitter.js +71 -4
  90. package/sensor/dist/extraction/tree-sitter.js.map +1 -1
  91. package/sensor/dist/extraction/wasm-runtime-flags.d.ts +2 -2
  92. package/sensor/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
  93. package/sensor/dist/extraction/wasm-runtime-flags.js +5 -5
  94. package/sensor/dist/extraction/wasm-runtime-flags.js.map +1 -1
  95. package/sensor/dist/index.d.ts +36 -36
  96. package/sensor/dist/index.d.ts.map +1 -1
  97. package/sensor/dist/index.js +71 -71
  98. package/sensor/dist/index.js.map +1 -1
  99. package/sensor/dist/mcp/daemon-manager.js +1 -1
  100. package/sensor/dist/mcp/daemon-paths.d.ts +3 -3
  101. package/sensor/dist/mcp/daemon-paths.js +7 -7
  102. package/sensor/dist/mcp/daemon-paths.js.map +1 -1
  103. package/sensor/dist/mcp/daemon-registry.d.ts +2 -2
  104. package/sensor/dist/mcp/daemon-registry.js +5 -5
  105. package/sensor/dist/mcp/daemon.d.ts +7 -7
  106. package/sensor/dist/mcp/daemon.d.ts.map +1 -1
  107. package/sensor/dist/mcp/daemon.js +21 -21
  108. package/sensor/dist/mcp/daemon.js.map +1 -1
  109. package/sensor/dist/mcp/dynamic-boundaries.js +1 -1
  110. package/sensor/dist/mcp/engine.d.ts +9 -9
  111. package/sensor/dist/mcp/engine.d.ts.map +1 -1
  112. package/sensor/dist/mcp/engine.js +44 -44
  113. package/sensor/dist/mcp/engine.js.map +1 -1
  114. package/sensor/dist/mcp/index.d.ts +14 -14
  115. package/sensor/dist/mcp/index.d.ts.map +1 -1
  116. package/sensor/dist/mcp/index.js +41 -41
  117. package/sensor/dist/mcp/index.js.map +1 -1
  118. package/sensor/dist/mcp/liveness-watchdog.js +7 -7
  119. package/sensor/dist/mcp/liveness-watchdog.js.map +1 -1
  120. package/sensor/dist/mcp/ppid-watchdog.d.ts +3 -3
  121. package/sensor/dist/mcp/ppid-watchdog.js +2 -2
  122. package/sensor/dist/mcp/proxy.d.ts.map +1 -1
  123. package/sensor/dist/mcp/proxy.js +37 -37
  124. package/sensor/dist/mcp/proxy.js.map +1 -1
  125. package/sensor/dist/mcp/query-pool.d.ts +3 -3
  126. package/sensor/dist/mcp/query-pool.js +7 -7
  127. package/sensor/dist/mcp/query-pool.js.map +1 -1
  128. package/sensor/dist/mcp/query-worker.d.ts +2 -2
  129. package/sensor/dist/mcp/query-worker.js +7 -7
  130. package/sensor/dist/mcp/query-worker.js.map +1 -1
  131. package/sensor/dist/mcp/server-instructions.d.ts +10 -12
  132. package/sensor/dist/mcp/server-instructions.d.ts.map +1 -1
  133. package/sensor/dist/mcp/server-instructions.js +39 -37
  134. package/sensor/dist/mcp/server-instructions.js.map +1 -1
  135. package/sensor/dist/mcp/session.d.ts +2 -2
  136. package/sensor/dist/mcp/session.js +18 -18
  137. package/sensor/dist/mcp/session.js.map +1 -1
  138. package/sensor/dist/mcp/startup-handshake.d.ts +3 -3
  139. package/sensor/dist/mcp/startup-handshake.d.ts.map +1 -1
  140. package/sensor/dist/mcp/startup-handshake.js +3 -3
  141. package/sensor/dist/mcp/startup-handshake.js.map +1 -1
  142. package/sensor/dist/mcp/tools.d.ts +53 -53
  143. package/sensor/dist/mcp/tools.d.ts.map +1 -1
  144. package/sensor/dist/mcp/tools.js +180 -216
  145. package/sensor/dist/mcp/tools.js.map +1 -1
  146. package/sensor/dist/mcp/transport.d.ts +1 -1
  147. package/sensor/dist/mcp/transport.js +6 -6
  148. package/sensor/dist/mcp/transport.js.map +1 -1
  149. package/sensor/dist/mcp/version.d.ts +2 -2
  150. package/sensor/dist/mcp/version.d.ts.map +1 -1
  151. package/sensor/dist/mcp/version.js +3 -3
  152. package/sensor/dist/mcp/version.js.map +1 -1
  153. package/sensor/dist/project-config.d.ts +6 -6
  154. package/sensor/dist/project-config.d.ts.map +1 -1
  155. package/sensor/dist/project-config.js +10 -10
  156. package/sensor/dist/project-config.js.map +1 -1
  157. package/sensor/dist/resolution/callback-synthesizer.js +4 -4
  158. package/sensor/dist/resolution/callback-synthesizer.js.map +1 -1
  159. package/sensor/dist/resolution/frameworks/drupal.d.ts +3 -3
  160. package/sensor/dist/resolution/frameworks/drupal.js +3 -3
  161. package/sensor/dist/resolution/frameworks/fabric.js +1 -1
  162. package/sensor/dist/resolution/frameworks/fabric.js.map +1 -1
  163. package/sensor/dist/resolution/frameworks/java.js +1 -1
  164. package/sensor/dist/resolution/frameworks/java.js.map +1 -1
  165. package/sensor/dist/resolution/frameworks/svelte.js +1 -1
  166. package/sensor/dist/resolution/frameworks/svelte.js.map +1 -1
  167. package/sensor/dist/resolution/goframe-synthesizer.d.ts +1 -1
  168. package/sensor/dist/resolution/goframe-synthesizer.js +1 -1
  169. package/sensor/dist/resolution/index.d.ts +1 -1
  170. package/sensor/dist/resolution/index.js +9 -9
  171. package/sensor/dist/resolution/index.js.map +1 -1
  172. package/sensor/dist/resolution/name-matcher.js +2 -2
  173. package/sensor/dist/resolution/name-matcher.js.map +1 -1
  174. package/sensor/dist/resolution/resolver-pool.d.ts +2 -2
  175. package/sensor/dist/resolution/resolver-pool.js +4 -4
  176. package/sensor/dist/resolution/resolver-pool.js.map +1 -1
  177. package/sensor/dist/resolution/resolver-worker.js +2 -2
  178. package/sensor/dist/resolution/resolver-worker.js.map +1 -1
  179. package/sensor/dist/resolution/workspace-packages.js +1 -1
  180. package/sensor/dist/resolution/workspace-packages.js.map +1 -1
  181. package/sensor/dist/search/identifier-segments.d.ts +1 -1
  182. package/sensor/dist/search/identifier-segments.js +1 -1
  183. package/sensor/dist/search/query-utils.d.ts +1 -0
  184. package/sensor/dist/search/query-utils.d.ts.map +1 -1
  185. package/sensor/dist/search/query-utils.js +10 -3
  186. package/sensor/dist/search/query-utils.js.map +1 -1
  187. package/sensor/dist/sync/git-hooks.d.ts +9 -9
  188. package/sensor/dist/sync/git-hooks.d.ts.map +1 -1
  189. package/sensor/dist/sync/git-hooks.js +15 -15
  190. package/sensor/dist/sync/git-hooks.js.map +1 -1
  191. package/sensor/dist/sync/watch-policy.d.ts +2 -2
  192. package/sensor/dist/sync/watch-policy.js +5 -5
  193. package/sensor/dist/sync/watch-policy.js.map +1 -1
  194. package/sensor/dist/sync/watcher.d.ts +2 -2
  195. package/sensor/dist/sync/watcher.d.ts.map +1 -1
  196. package/sensor/dist/sync/watcher.js +16 -16
  197. package/sensor/dist/sync/watcher.js.map +1 -1
  198. package/sensor/dist/sync/worktree.d.ts +5 -5
  199. package/sensor/dist/sync/worktree.d.ts.map +1 -1
  200. package/sensor/dist/sync/worktree.js +9 -9
  201. package/sensor/dist/sync/worktree.js.map +1 -1
  202. package/sensor/dist/telemetry/index.d.ts +8 -8
  203. package/sensor/dist/telemetry/index.d.ts.map +1 -1
  204. package/sensor/dist/telemetry/index.js +16 -16
  205. package/sensor/dist/telemetry/index.js.map +1 -1
  206. package/sensor/dist/types.d.ts +2 -2
  207. package/sensor/dist/types.js +1 -1
  208. package/sensor/dist/ui/color.d.ts +2 -2
  209. package/sensor/dist/ui/color.js +2 -2
  210. package/sensor/dist/ui/glyphs.d.ts +3 -3
  211. package/sensor/dist/ui/glyphs.js +7 -7
  212. package/sensor/dist/ui/glyphs.js.map +1 -1
  213. package/sensor/dist/utils.d.ts +4 -4
  214. package/sensor/dist/utils.js +8 -8
  215. package/sensor/dist/utils.js.map +1 -1
  216. package/sensor/package.json +8 -13
  217. package/src/artifact-contracts.mjs +2 -2
  218. package/src/boundary-compiler.mjs +28 -28
  219. package/src/boundary-observation-compiler-v2.mjs +10 -10
  220. package/src/bounded-seam.mjs +144 -0
  221. package/src/bridge-cli.mjs +227 -0
  222. package/src/bridge-config.mjs +346 -0
  223. package/src/bridge-daemon.mjs +328 -0
  224. package/src/bridge-launch-agent.mjs +311 -0
  225. package/src/bridge-server.mjs +309 -0
  226. package/src/cli-help.mjs +163 -0
  227. package/src/control-compiler.mjs +30 -30
  228. package/src/dag-chain.mjs +261 -0
  229. package/src/factory-diagnostics.mjs +8 -7
  230. package/src/hash-chain.mjs +76 -0
  231. package/src/node-version-guard.mjs +5 -4
  232. package/src/project-cli.mjs +392 -0
  233. package/src/project-identity.mjs +71 -0
  234. package/src/rc1-evidence-bundle.mjs +16 -16
  235. package/src/rc1-v4-campaign.mjs +15 -14
  236. package/src/rc1-v5-artifact-set.mjs +2 -2
  237. package/src/rc1-v5-campaign.mjs +15 -14
  238. package/src/rc1-v6-artifact-set.mjs +18 -18
  239. package/src/rc1-v6-campaign.mjs +16 -16
  240. package/src/rc1-v6-causal-binding.mjs +16 -14
  241. package/src/rc1-v6-measurement.mjs +20 -38
  242. package/src/rc2-artifact-set.mjs +45 -40
  243. package/src/rc2-campaign.mjs +48 -66
  244. package/src/rc2-delivery-policy-front-end.mjs +30 -30
  245. package/src/rc2-rc1-transfer-front-end.mjs +5 -5
  246. package/src/rc3-actual-dogfood.mjs +6 -6
  247. package/src/rc3-dogfood-scaffold.mjs +12 -7
  248. package/src/rc3-scripted-campaign.mjs +75 -21
  249. package/src/rc4-stage1-dogfood.mjs +24 -7
  250. package/src/runtime-cli.mjs +2424 -45
  251. package/src/runtime-contracts.mjs +7 -7
  252. package/src/runtime-control-store.mjs +547 -0
  253. package/src/runtime-controller-protocol.mjs +517 -0
  254. package/src/runtime-direct-os-observer.mjs +284 -0
  255. package/src/runtime-engine.mjs +2 -2
  256. package/src/runtime-errors.mjs +1 -1
  257. package/src/runtime-event-store.mjs +15 -43
  258. package/src/runtime-front-end.mjs +26 -26
  259. package/src/runtime-gate-store.mjs +477 -0
  260. package/src/runtime-hold-recompile.mjs +214 -23
  261. package/src/runtime-lifecycle-lock.mjs +290 -0
  262. package/src/runtime-managed-supervisor.mjs +1276 -0
  263. package/src/runtime-multi-epoch-store.mjs +761 -0
  264. package/src/runtime-projection.mjs +62 -0
  265. package/src/seam-transform.mjs +38 -38
  266. package/src/{codegraph-adapter.mjs → sensor-adapter.mjs} +7 -7
  267. package/src/sensor-cli.mjs +107 -0
  268. package/src/sensor-runtime.mjs +48 -0
  269. package/src/timestamp-contract.mjs +8 -0
  270. package/src/todo-chain.mjs +178 -0
  271. package/src/todo-cli.mjs +1032 -0
  272. package/src/todo-contracts.mjs +501 -0
  273. package/src/todo-dashboard-registry.mjs +318 -0
  274. package/src/todo-gantt-html.mjs +416 -0
  275. package/src/todo-gantt-layout.mjs +603 -0
  276. package/src/todo-gantt-live.mjs +269 -0
  277. package/src/todo-gantt-presentation.mjs +217 -0
  278. package/src/todo-gantt-svg.mjs +249 -0
  279. package/src/todo-markdown-renderer.mjs +260 -0
  280. package/src/todo-migration.mjs +260 -0
  281. package/src/todo-narrative-anchor.mjs +130 -0
  282. package/src/todo-revision.mjs +425 -0
  283. package/src/todo-status.mjs +340 -0
  284. package/src/todo-store.mjs +3451 -0
  285. package/src/treatment-compiler.mjs +28 -28
  286. package/src/treatment-runner.mjs +42 -44
  287. package/sensor/dist/bin/codegraph.d.ts +0 -26
  288. package/sensor/dist/bin/codegraph.d.ts.map +0 -1
  289. package/sensor/dist/bin/codegraph.js.map +0 -1
  290. package/sensor/dist/bin/uninstall.d.ts +0 -14
  291. package/sensor/dist/bin/uninstall.d.ts.map +0 -1
  292. package/sensor/dist/bin/uninstall.js +0 -36
  293. package/sensor/dist/bin/uninstall.js.map +0 -1
  294. package/sensor/dist/installer/beta-signup.d.ts +0 -54
  295. package/sensor/dist/installer/beta-signup.d.ts.map +0 -1
  296. package/sensor/dist/installer/beta-signup.js +0 -178
  297. package/sensor/dist/installer/beta-signup.js.map +0 -1
  298. package/sensor/dist/installer/config-writer.d.ts +0 -28
  299. package/sensor/dist/installer/config-writer.d.ts.map +0 -1
  300. package/sensor/dist/installer/config-writer.js +0 -91
  301. package/sensor/dist/installer/config-writer.js.map +0 -1
  302. package/sensor/dist/installer/index.d.ts +0 -142
  303. package/sensor/dist/installer/index.d.ts.map +0 -1
  304. package/sensor/dist/installer/index.js +0 -622
  305. package/sensor/dist/installer/index.js.map +0 -1
  306. package/sensor/dist/installer/instructions-template.d.ts +0 -41
  307. package/sensor/dist/installer/instructions-template.d.ts.map +0 -1
  308. package/sensor/dist/installer/instructions-template.js +0 -53
  309. package/sensor/dist/installer/instructions-template.js.map +0 -1
  310. package/sensor/dist/installer/targets/antigravity.d.ts +0 -57
  311. package/sensor/dist/installer/targets/antigravity.d.ts.map +0 -1
  312. package/sensor/dist/installer/targets/antigravity.js +0 -308
  313. package/sensor/dist/installer/targets/antigravity.js.map +0 -1
  314. package/sensor/dist/installer/targets/claude.d.ts +0 -62
  315. package/sensor/dist/installer/targets/claude.d.ts.map +0 -1
  316. package/sensor/dist/installer/targets/claude.js +0 -454
  317. package/sensor/dist/installer/targets/claude.js.map +0 -1
  318. package/sensor/dist/installer/targets/codex.d.ts +0 -18
  319. package/sensor/dist/installer/targets/codex.d.ts.map +0 -1
  320. package/sensor/dist/installer/targets/codex.js +0 -185
  321. package/sensor/dist/installer/targets/codex.js.map +0 -1
  322. package/sensor/dist/installer/targets/cursor.d.ts +0 -35
  323. package/sensor/dist/installer/targets/cursor.d.ts.map +0 -1
  324. package/sensor/dist/installer/targets/cursor.js +0 -254
  325. package/sensor/dist/installer/targets/cursor.js.map +0 -1
  326. package/sensor/dist/installer/targets/gemini.d.ts +0 -26
  327. package/sensor/dist/installer/targets/gemini.d.ts.map +0 -1
  328. package/sensor/dist/installer/targets/gemini.js +0 -165
  329. package/sensor/dist/installer/targets/gemini.js.map +0 -1
  330. package/sensor/dist/installer/targets/hermes.d.ts +0 -18
  331. package/sensor/dist/installer/targets/hermes.d.ts.map +0 -1
  332. package/sensor/dist/installer/targets/hermes.js +0 -359
  333. package/sensor/dist/installer/targets/hermes.js.map +0 -1
  334. package/sensor/dist/installer/targets/kiro.d.ts +0 -27
  335. package/sensor/dist/installer/targets/kiro.d.ts.map +0 -1
  336. package/sensor/dist/installer/targets/kiro.js +0 -178
  337. package/sensor/dist/installer/targets/kiro.js.map +0 -1
  338. package/sensor/dist/installer/targets/opencode.d.ts +0 -38
  339. package/sensor/dist/installer/targets/opencode.d.ts.map +0 -1
  340. package/sensor/dist/installer/targets/opencode.js +0 -288
  341. package/sensor/dist/installer/targets/opencode.js.map +0 -1
  342. package/sensor/dist/installer/targets/registry.d.ts +0 -35
  343. package/sensor/dist/installer/targets/registry.d.ts.map +0 -1
  344. package/sensor/dist/installer/targets/registry.js +0 -91
  345. package/sensor/dist/installer/targets/registry.js.map +0 -1
  346. package/sensor/dist/installer/targets/shared.d.ts +0 -101
  347. package/sensor/dist/installer/targets/shared.d.ts.map +0 -1
  348. package/sensor/dist/installer/targets/shared.js +0 -264
  349. package/sensor/dist/installer/targets/shared.js.map +0 -1
  350. package/sensor/dist/installer/targets/toml.d.ts +0 -52
  351. package/sensor/dist/installer/targets/toml.d.ts.map +0 -1
  352. package/sensor/dist/installer/targets/toml.js +0 -147
  353. package/sensor/dist/installer/targets/toml.js.map +0 -1
  354. package/sensor/dist/installer/targets/types.d.ts +0 -108
  355. package/sensor/dist/installer/targets/types.d.ts.map +0 -1
  356. package/sensor/dist/installer/targets/types.js +0 -16
  357. package/sensor/dist/installer/targets/types.js.map +0 -1
  358. package/sensor/dist/upgrade/index.d.ts +0 -171
  359. package/sensor/dist/upgrade/index.d.ts.map +0 -1
  360. package/sensor/dist/upgrade/index.js +0 -659
  361. package/sensor/dist/upgrade/index.js.map +0 -1
  362. package/sensor/dist/upgrade/remove-binary.d.ts +0 -87
  363. package/sensor/dist/upgrade/remove-binary.d.ts.map +0 -1
  364. package/sensor/dist/upgrade/remove-binary.js +0 -289
  365. package/sensor/dist/upgrade/remove-binary.js.map +0 -1
  366. package/sensor/dist/upgrade/update-check.d.ts +0 -92
  367. package/sensor/dist/upgrade/update-check.d.ts.map +0 -1
  368. package/sensor/dist/upgrade/update-check.js +0 -258
  369. package/sensor/dist/upgrade/update-check.js.map +0 -1
  370. package/src/bootstrap.mjs +0 -88
@@ -1,9 +1,24 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { lstat, readFile } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { constants as fsConstants } from 'node:fs';
4
+ import {
5
+ lstat,
6
+ mkdir,
7
+ mkdtemp,
8
+ open,
9
+ readFile,
10
+ readdir,
11
+ realpath,
12
+ rename,
13
+ rm,
14
+ unlink,
15
+ writeFile,
16
+ } from 'node:fs/promises';
3
17
  import path from 'node:path';
4
18
 
5
- import { digestArtifact } from './artifact-contracts.mjs';
6
- import { collectCodegraphEvidence } from './codegraph-adapter.mjs';
19
+ import { canonicalizeArtifact, digestArtifact } from './artifact-contracts.mjs';
20
+ import { collectSensorEvidence } from './sensor-adapter.mjs';
21
+ import { detectCheckpointFindings } from './runtime-diff-observer.mjs';
7
22
  import {
8
23
  compileRuntimePlanV1,
9
24
  evidenceFromCollectedOutcomes,
@@ -12,15 +27,54 @@ import {
12
27
  validateRunRequest,
13
28
  validateRuntimeBoundaryManifest,
14
29
  validateRuntimePlan,
30
+ verifyRuntimePlanBinding,
31
+ selfDigest,
15
32
  } from './runtime-contracts.mjs';
16
- import { initializeRunEvents } from './runtime-engine.mjs';
33
+ import {
34
+ buildNextRunEvent,
35
+ buildExecutorPackets,
36
+ closeRunIfComplete,
37
+ initializeRunEvents,
38
+ } from './runtime-engine.mjs';
17
39
  import {
18
40
  computeReadyFrontier,
19
41
  recomputeReceiptDecisions,
42
+ classifyObservedDiff,
20
43
  } from './runtime-decision-verifier.mjs';
21
44
  import { verifyRunEventChain } from './runtime-event-store.mjs';
22
- import { projectRuntimeState } from './runtime-projection.mjs';
45
+ import { projectRuntimeState, projectRuntimeStatusOverlays } from './runtime-projection.mjs';
46
+ import {
47
+ decideHoldAndCarryOver,
48
+ recompileNextEpochPlan,
49
+ validateRuntimeRecompileRequest,
50
+ } from './runtime-hold-recompile.mjs';
23
51
  import { verifySchedulabilityPlanV2 } from './schedulability-verifier-v2.mjs';
52
+ import {
53
+ activateEpochOneStore,
54
+ commitStagedSuccessorEpoch,
55
+ commitReleaseEpochBarrier,
56
+ RuntimeEpochStoreError,
57
+ isManagedRunFrozen,
58
+ readCommittedEpochStore,
59
+ recordRuntimeFinding,
60
+ stageSuccessorEpoch,
61
+ validateRuntimeEpochBundle,
62
+ validateRuntimeFindingCandidate,
63
+ validateRuntimeFindingRecord,
64
+ } from './runtime-multi-epoch-store.mjs';
65
+ import { createRuntimeControlRequest, validateRuntimeControlResponse } from './runtime-controller-protocol.mjs';
66
+ import { createRuntimeControlStore } from './runtime-control-store.mjs';
67
+ import { createRuntimeGateStore } from './runtime-gate-store.mjs';
68
+ import { acquireRuntimeLifecycleLock } from './runtime-lifecycle-lock.mjs';
69
+ import {
70
+ ManagedRuntimeError,
71
+ launchDurableSupervisor,
72
+ observeManagedProcessStartIdentity,
73
+ prepareManagedSupervisorRestart,
74
+ resolveActiveRuntimePaths,
75
+ sendRuntimeActivationRequest,
76
+ sendRuntimeControlRequest,
77
+ } from './runtime-managed-supervisor.mjs';
24
78
 
25
79
  /**
26
80
  * RC3-D CLI surface(ADR 0044 Decision 8)。
@@ -28,17 +82,20 @@ import { verifySchedulabilityPlanV2 } from './schedulability-verifier-v2.mjs';
28
82
  * lattice plan compile --request <run-request.json>
29
83
  * lattice plan verify --request <run-request.json> --plan <plan.json>
30
84
  * lattice run start --request <run-request.json> --executor <adapter>
31
- * lattice run observe --run <run-directory>
32
- * lattice run status --run <run-directory>
33
- * lattice event verify --run <run-directory>
85
+ * lattice run observe --run .lattice/runs/<run-id>
86
+ * lattice run status --run .lattice/runs/<run-id>
87
+ * lattice run resume --run .lattice/runs/<run-id>
88
+ * lattice run close --run .lattice/runs/<run-id>
89
+ * lattice run abandon --run .lattice/runs/<run-id> --reason <reason>
90
+ * lattice event verify --run .lattice/runs/<run-id>
34
91
  *
35
92
  * - stdout: versioned JSON 1行のみ。診断はstderr。
36
93
  * - exit 0: 成功(artifact refとdigestを含むversioned JSON)。
37
- * - exit 1: typed契約失敗。stderrへ`lattice.cli_error.v1` JSON 1行。
94
+ * - exit 1: typed契約失敗。stderrへ`lattice.cli_error.v2` JSON 1行。
38
95
  * - exit 2: usage違反(未知command、引数の欠落・重複・余剰・順序不正)。
39
96
  * - `--executor`等の暗黙fallbackを持たず、未実装surfaceはusage違反として拒否する。
40
97
  *
41
- * plan verifyは保存planを信用せず、requestとfresh Codegraph観測からplanを
98
+ * plan verifyは保存planを信用せず、requestとfresh LatticeSensor観測からplanを
42
99
  * producerで再コンパイルしてdigest完全一致を要求し、さらにschedule minimumを
43
100
  * producer非依存の`verifySchedulabilityPlanV2`で再計算する(成功条件5)。
44
101
  */
@@ -46,16 +103,19 @@ import { verifySchedulabilityPlanV2 } from './schedulability-verifier-v2.mjs';
46
103
  const MAX_INPUT_BYTES = 8_388_608;
47
104
  const COMPILE_RESULT_SCHEMA = 'lattice.plan_compile_result.v1';
48
105
  const VERIFY_RESULT_SCHEMA = 'lattice.plan_verify_result.v1';
49
- const CLI_ERROR_SCHEMA = 'lattice.cli_error.v1';
50
- // run event storeはDecision 10.1のLattice-owned root配下に置く。
51
- const RUN_STORE_ROOT = ['research', 'runs', 'rc3'];
106
+ const CLI_ERROR_SCHEMA = 'lattice.cli_error.v2';
107
+ // 現役run storeは対象Git repo内のLattice-owned・ignored rootへ限定する。
108
+ const RUN_STORE_ROOT = ['.lattice', 'runs'];
109
+ const RUN_REF = /^\.lattice\/runs\/([0-9A-Za-z](?:[0-9A-Za-z._-]{0,127}))$/u;
110
+ const ABANDON_REASON = /^[0-9A-Za-z](?:[0-9A-Za-z._:-]{0,127})$/u;
52
111
  const KNOWN_ADAPTERS = Object.freeze(['scripted', 'isolated-worktree', 'actual-agent']);
53
112
 
54
113
  class CliContractError extends Error {
55
- constructor(code, message) {
114
+ constructor(code, message, detail) {
56
115
  super(message);
57
116
  this.name = 'CliContractError';
58
117
  this.code = code;
118
+ this.detail = detail;
59
119
  }
60
120
  }
61
121
 
@@ -67,7 +127,10 @@ function usageFailure(stderr, args) {
67
127
 
68
128
  function typedFailure(stderr, code, message, detail) {
69
129
  const payload = { schema: CLI_ERROR_SCHEMA, code, message };
70
- if (detail !== undefined) payload.detail = detail;
130
+ // ADR 0052 Decision 1: detailは非空plain objectの場合だけ出す(null・非object・空objectは省略)。
131
+ if (detail !== null && typeof detail === 'object' && !Array.isArray(detail) && Object.keys(detail).length > 0) {
132
+ payload.detail = detail;
133
+ }
71
134
  stderr.write(`${JSON.stringify(payload)}\n`);
72
135
  return 1;
73
136
  }
@@ -133,6 +196,57 @@ async function resolveRepoBinding(cwd, request) {
133
196
  }
134
197
  }
135
198
 
199
+ async function resolveRepoRoot(cwd) {
200
+ const root = await runGit(['rev-parse', '--show-toplevel'], cwd);
201
+ if (root.code !== 0 || root.stdout.trim().length === 0) {
202
+ throw new CliContractError('REPO_UNRESOLVED', `cwdのgit rootを解決できない: ${root.stderr.trim()}`);
203
+ }
204
+ return path.resolve(root.stdout.trim());
205
+ }
206
+
207
+ async function requireIgnoredRunStore(repoRoot) {
208
+ const probe = '.lattice/runs/.lattice-ignore-probe';
209
+ const ignored = await runGit(['check-ignore', '-q', '--', probe], repoRoot);
210
+ if (ignored.code !== 0) {
211
+ throw new CliContractError(
212
+ 'RUN_STORE_NOT_IGNORED',
213
+ '.lattice/runs/ がgit ignore対象ではない',
214
+ { guidance: '.gitignoreへ `.lattice/runs/` を追加してから再実行する' },
215
+ );
216
+ }
217
+ }
218
+
219
+ async function requireSafeRunAncestors(repoRoot) {
220
+ for (const relative of ['.lattice', '.lattice/runs']) {
221
+ try {
222
+ const state = await lstat(path.join(repoRoot, relative));
223
+ if (state.isSymbolicLink() || !state.isDirectory()) {
224
+ throw new CliContractError('INVALID_RUN_STORE', `${relative}が安全なdirectoryではない`);
225
+ }
226
+ } catch (error) {
227
+ if (error?.code !== 'ENOENT') throw error;
228
+ }
229
+ }
230
+ }
231
+
232
+ async function resolveRunStore(cwd, runRef) {
233
+ const normalized = runRef.replaceAll('\\', '/');
234
+ const match = RUN_REF.exec(normalized);
235
+ if (match === null || path.isAbsolute(runRef)) {
236
+ throw new CliContractError(
237
+ 'INVALID_RUN_REF',
238
+ 'run refは `.lattice/runs/<run-id>` のrepo相対形式でなければならない',
239
+ );
240
+ }
241
+ const repoRoot = await resolveRepoRoot(cwd);
242
+ await requireSafeRunAncestors(repoRoot);
243
+ return { repoRoot, runDir: path.join(repoRoot, ...RUN_STORE_ROOT, match[1]), runRef: normalized };
244
+ }
245
+
246
+ function canonicalNow() {
247
+ return new Date().toISOString();
248
+ }
249
+
136
250
  async function loadRequest(requestPath) {
137
251
  const request = await readBoundedJson(requestPath, 'run request');
138
252
  if (!validateRunRequest(request)) {
@@ -142,20 +256,20 @@ async function loadRequest(requestPath) {
142
256
  }
143
257
 
144
258
  async function compileFromRepo({ request, cwd, planRef, planEpoch, predecessorRefs }) {
145
- const collected = await collectCodegraphEvidence({
259
+ const collected = await collectSensorEvidence({
146
260
  cwd,
147
- querySet: request.codegraph_query_set,
261
+ querySet: request.sensor_query_set,
148
262
  });
149
263
  // 観測中にHEADが動いた場合、観測とbase束縛が別snapshotになるためreject
150
264
  // (TOCTOU窓の閉鎖。base再確認は観測の後に行う)。
151
265
  await resolveRepoBinding(cwd, request);
152
- const codegraphEvidence = evidenceFromCollectedOutcomes({
153
- querySet: request.codegraph_query_set,
266
+ const sensorEvidence = evidenceFromCollectedOutcomes({
267
+ querySet: request.sensor_query_set,
154
268
  collected,
155
269
  });
156
270
  return compileRuntimePlanV1({
157
271
  request,
158
- codegraphEvidence,
272
+ sensorEvidence,
159
273
  planRef,
160
274
  planEpoch,
161
275
  predecessorRefs,
@@ -213,7 +327,7 @@ async function planVerify({ requestPath, planPath, cwd, stdout }) {
213
327
  }
214
328
  const planNodeIds = plan.nodes.map((node) => node.todo_id);
215
329
  if (artifact.manifests === null || typeof artifact.manifests !== 'object' || Array.isArray(artifact.manifests)
216
- || Object.keys(artifact.manifests).sort().join('') !== [...planNodeIds].sort().join('')) {
330
+ || Object.keys(artifact.manifests).sort().join('\0') !== [...planNodeIds].sort().join('\0')) {
217
331
  throw new CliContractError('INVALID_PLAN_ARTIFACT', 'manifestsのkey集合がplan nodesと一致しない');
218
332
  }
219
333
  for (const todoId of planNodeIds) {
@@ -274,6 +388,15 @@ function runStorePath(cwd, runId) {
274
388
  }
275
389
 
276
390
  async function readRunStore(runDir) {
391
+ let runState;
392
+ try {
393
+ runState = await lstat(runDir);
394
+ } catch {
395
+ throw new CliContractError('INVALID_RUN_STORE', 'run store directoryを読めない');
396
+ }
397
+ if (runState.isSymbolicLink() || !runState.isDirectory()) {
398
+ throw new CliContractError('INVALID_RUN_STORE', 'run storeが安全なdirectoryではない');
399
+ }
277
400
  const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
278
401
  if (!Array.isArray(events)) {
279
402
  throw new CliContractError('INVALID_RUN_STORE', 'events.jsonがarrayではない');
@@ -282,7 +405,196 @@ async function readRunStore(runDir) {
282
405
  const compileArtifact = await readBoundedJson(
283
406
  path.join(runDir, 'plan-compile-result.json'), 'plan compile result',
284
407
  );
285
- return { events, meta, compileArtifact };
408
+ const request = await readBoundedJson(path.join(runDir, 'request.json'), 'run request');
409
+ const compileKeys = ['schema', 'request_digest', 'plan', 'manifests', 'schedule', 'graph_digest', 'result_digest'];
410
+ const metaKeys = ['schema', 'run_id', 'executor_adapter', 'plan_digest'];
411
+ const { result_digest: claimedCompileDigest, ...compileBody } = compileArtifact ?? {};
412
+ const legacyMetaValid = meta !== null && typeof meta === 'object' && !Array.isArray(meta)
413
+ && Object.keys(meta).sort().join('\0') === metaKeys.sort().join('\0')
414
+ && meta.schema === 'lattice.run_meta.v1'
415
+ && meta.run_id === request.request_id
416
+ && KNOWN_ADAPTERS.includes(meta.executor_adapter)
417
+ && compileArtifact?.plan?.plan_digest === meta.plan_digest;
418
+ const managed = meta?.schema === 'lattice.run_meta.v2'
419
+ ? await readCommittedEpochStore(runDir)
420
+ : null;
421
+ const managedMetaValid = managed !== null
422
+ && managed.meta.run_id === request.request_id
423
+ && KNOWN_ADAPTERS.includes(managed.meta.executor_adapter)
424
+ && managed.meta.created_plan_digest === compileArtifact.plan.plan_digest;
425
+ if (!validateRunRequest(request)
426
+ || (!legacyMetaValid && !managedMetaValid)
427
+ || compileArtifact === null || typeof compileArtifact !== 'object' || Array.isArray(compileArtifact)
428
+ || Object.keys(compileArtifact).sort().join('\0') !== compileKeys.sort().join('\0')
429
+ || compileArtifact.schema !== COMPILE_RESULT_SCHEMA
430
+ || claimedCompileDigest !== digestArtifact(compileBody)
431
+ || compileArtifact.request_digest !== request.request_digest
432
+ || !validateRuntimePlan(compileArtifact?.plan)
433
+ || !verifyRuntimePlanBinding({ plan: compileArtifact.plan, request })
434
+ ) {
435
+ throw new CliContractError('INVALID_RUN_STORE', 'run storeのartifact bindingが不正');
436
+ }
437
+ if (managed === null) return { events, meta, compileArtifact, request, managed: null };
438
+ return {
439
+ events,
440
+ meta: {
441
+ schema: 'lattice.run_meta.v1',
442
+ run_id: managed.meta.run_id,
443
+ executor_adapter: managed.meta.executor_adapter,
444
+ plan_digest: managed.bundle.plan.plan_digest,
445
+ },
446
+ compileArtifact: {
447
+ ...compileArtifact,
448
+ request_digest: managed.bundle.request.request_digest,
449
+ plan: managed.bundle.plan,
450
+ manifests: managed.bundle.manifests,
451
+ },
452
+ request: managed.bundle.request,
453
+ managed,
454
+ };
455
+ }
456
+
457
+ async function writeJsonFile(filePath, value) {
458
+ await writeFile(filePath, `${JSON.stringify(value, null, 1)}\n`, { mode: 0o600, flag: 'wx' });
459
+ }
460
+
461
+ async function writeCanonicalJsonFile(filePath, value) {
462
+ await writeFile(filePath, `${canonicalizeArtifact(value)}\n`, { mode: 0o600, flag: 'wx' });
463
+ }
464
+
465
+ async function replaceEventsAtomically(runDir, events) {
466
+ const temporaryPath = path.join(runDir, `.events-${process.pid}-${Date.now()}.tmp`);
467
+ await writeJsonFile(temporaryPath, events);
468
+ await rename(temporaryPath, path.join(runDir, 'events.json'));
469
+ }
470
+
471
+ async function replaceNamedJsonAtomically(runDir, name, value) {
472
+ await durableReplaceBytes(runDir, name, Buffer.from(`${JSON.stringify(value, null, 1)}\n`));
473
+ }
474
+
475
+ async function replaceCanonicalJsonAtomically(directory, name, value) {
476
+ await durableReplaceBytes(directory, name, Buffer.from(`${canonicalizeArtifact(value)}\n`));
477
+ }
478
+
479
+ function runtimeEventBatchReceiptName(transactionId, phase) {
480
+ return `run-event-batch-${digestArtifact({ transaction_id: transactionId, phase })}.json`;
481
+ }
482
+
483
+ function sameDigestPrefix(left, right) {
484
+ return left.length <= right.length && left.every((digest, index) => digest === right[index]);
485
+ }
486
+
487
+ /**
488
+ * Recompileのrun event副作用をtransaction単位でexact-once publishする。
489
+ * receiptをevents.jsonより先にdurable化するため、receipt直後のcrashは同じ
490
+ * event object(recorded_atを含む)をroll-forwardできる。events.jsonはatomic
491
+ * replaceだが、検証はpartial prefixも受け、別branchや別batchは拒否する。
492
+ */
493
+ async function publishRuntimeEventBatch({ runDir, transactionId, phase, planEpoch,
494
+ bindingDigest, currentEvents, proposedBatch, crashInjector }) {
495
+ if (!verifyRunEventChain({ events: currentEvents }).valid) {
496
+ throw new ManagedRuntimeError('EVENT_CHAIN_INVALID', 'event batch publish前のrun event chainが不正');
497
+ }
498
+ const receiptName = runtimeEventBatchReceiptName(transactionId, phase);
499
+ const receiptPath = path.join(runDir, receiptName);
500
+ let receipt = await readBoundedJson(receiptPath, 'runtime event batch receipt').catch((error) => {
501
+ if (error?.code === 'INPUT_UNREADABLE') return null;
502
+ throw error;
503
+ });
504
+ if (receipt === null) {
505
+ const combined = [...currentEvents, ...proposedBatch];
506
+ const chain = verifyRunEventChain({ events: combined });
507
+ if (!chain.valid) {
508
+ throw new ManagedRuntimeError('EVENT_CHAIN_INVALID', 'recompile event batchのchainが不正');
509
+ }
510
+ receipt = {
511
+ schema: 'lattice.runtime_run_event_batch_receipt.v1',
512
+ transaction_id: transactionId,
513
+ phase,
514
+ plan_epoch: planEpoch,
515
+ binding_digest: bindingDigest,
516
+ base_event_digests: currentEvents.map((event) => event.event_digest),
517
+ batch_events: structuredClone(proposedBatch),
518
+ ordered_event_digests: proposedBatch.map((event) => event.event_digest),
519
+ batch_digest: digestArtifact(proposedBatch.map((event) => event.event_digest)),
520
+ receipt_digest: '',
521
+ };
522
+ receipt.receipt_digest = selfDigest(receipt, 'receipt_digest');
523
+ await replaceCanonicalJsonAtomically(runDir, receiptName, receipt);
524
+ if (typeof crashInjector === 'function') await crashInjector('after_run_event_batch_receipt', {
525
+ transaction_id: transactionId, phase, receipt_digest: receipt.receipt_digest,
526
+ });
527
+ }
528
+ const receiptValid = receipt?.schema === 'lattice.runtime_run_event_batch_receipt.v1'
529
+ && receipt.transaction_id === transactionId
530
+ && receipt.phase === phase
531
+ && receipt.plan_epoch === planEpoch
532
+ && receipt.binding_digest === bindingDigest
533
+ && receipt.receipt_digest === selfDigest(receipt, 'receipt_digest')
534
+ && Array.isArray(receipt.base_event_digests)
535
+ && Array.isArray(receipt.batch_events)
536
+ && Array.isArray(receipt.ordered_event_digests)
537
+ && canonicalizeArtifact(receipt.batch_events.map((event) => event.event_digest))
538
+ === canonicalizeArtifact(receipt.ordered_event_digests)
539
+ && receipt.batch_digest === digestArtifact(receipt.ordered_event_digests);
540
+ if (!receiptValid) {
541
+ throw new ManagedRuntimeError('RECOMPILE_EVENT_BATCH_CONFLICT', 'event batch receiptのbindingが不正');
542
+ }
543
+ const currentDigests = currentEvents.map((event) => event.event_digest);
544
+ const fullDigests = [...receipt.base_event_digests, ...receipt.ordered_event_digests];
545
+ if (!sameDigestPrefix(currentDigests, fullDigests) && !sameDigestPrefix(fullDigests, currentDigests)) {
546
+ throw new ManagedRuntimeError('RECOMPILE_EVENT_BATCH_CONFLICT', 'events.jsonがreceiptと異なるbranchにある');
547
+ }
548
+ if (currentDigests.length >= fullDigests.length) return currentEvents;
549
+ if (currentDigests.length < receipt.base_event_digests.length) {
550
+ throw new ManagedRuntimeError('RECOMPILE_EVENT_BATCH_CONFLICT', 'receiptのbase event chainが欠落している');
551
+ }
552
+ const baseEvents = currentEvents.slice(0, receipt.base_event_digests.length);
553
+ const completed = [...baseEvents, ...receipt.batch_events];
554
+ const chain = verifyRunEventChain({ events: completed });
555
+ if (!chain.valid) {
556
+ throw new ManagedRuntimeError('EVENT_CHAIN_INVALID', 'receiptから復元したevent batchのchainが不正');
557
+ }
558
+ await replaceEventsAtomically(runDir, completed);
559
+ return completed;
560
+ }
561
+
562
+ async function durableReplaceBytes(directory, name, bytes) {
563
+ const temporaryPath = path.join(directory, `.${name}-${process.pid}-${randomUUID()}.tmp`);
564
+ let handle;
565
+ try {
566
+ handle = await open(temporaryPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);
567
+ await handle.writeFile(bytes);
568
+ await handle.sync();
569
+ await handle.close();
570
+ handle = null;
571
+ await rename(temporaryPath, path.join(directory, name));
572
+ const directoryHandle = await open(directory, fsConstants.O_RDONLY);
573
+ try { await directoryHandle.sync(); } finally { await directoryHandle.close(); }
574
+ } finally {
575
+ if (handle) await handle.close().catch(() => {});
576
+ await rm(temporaryPath, { force: true }).catch(() => {});
577
+ }
578
+ }
579
+
580
+ async function withLifecycleLock(runDir, action) {
581
+ // managed mutationの正本lockはdurable supervisorが所有する。これはunmanaged/launch CLI競合だけを塞ぐ。
582
+ const lockPath = path.join(runDir, '.cli-lifecycle.lock');
583
+ let handle;
584
+ try {
585
+ handle = await open(lockPath, 'wx', 0o600);
586
+ } catch (error) {
587
+ if (error?.code === 'EEXIST') {
588
+ throw new CliContractError('RUN_BUSY', 'run lifecycle操作が既に進行中');
589
+ }
590
+ throw error;
591
+ }
592
+ try {
593
+ return await action();
594
+ } finally {
595
+ await handle.close();
596
+ await unlink(lockPath).catch(() => {});
597
+ }
286
598
  }
287
599
 
288
600
  async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
@@ -295,10 +607,13 @@ async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
295
607
  || !request.executor_capability.adapters.includes(executorAdapter)) {
296
608
  throw new CliContractError('UNKNOWN_ADAPTER', 'requestのexecutor_capabilityに含まれないadapter');
297
609
  }
298
- await resolveRepoBinding(cwd, request);
610
+ const repoRoot = await resolveRepoRoot(cwd);
611
+ await requireSafeRunAncestors(repoRoot);
612
+ await requireIgnoredRunStore(repoRoot);
613
+ await resolveRepoBinding(repoRoot, request);
299
614
  const result = await compileFromRepo({
300
615
  request,
301
- cwd,
616
+ cwd: repoRoot,
302
617
  planRef: `plan-${request.request_id}-e1`,
303
618
  planEpoch: 1,
304
619
  predecessorRefs: [],
@@ -322,13 +637,16 @@ async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
322
637
  manifests: result.manifests,
323
638
  recordedAt: new Date().toISOString().replace(/\.\d+Z$/u, '.000Z'),
324
639
  });
325
- const runDir = runStorePath(cwd, request.request_id);
326
- const { mkdir, writeFile } = await import('node:fs/promises');
640
+ const runDir = runStorePath(repoRoot, request.request_id);
327
641
  await mkdir(path.dirname(runDir), { recursive: true });
642
+ const temporaryDir = await mkdtemp(path.join(path.dirname(runDir), `.${request.request_id}.tmp-`));
328
643
  try {
329
- await mkdir(runDir);
644
+ await lstat(runDir);
645
+ await rm(temporaryDir, { recursive: true, force: true });
646
+ throw new CliContractError('RUN_EXISTS', 'run storeが既に存在する');
330
647
  } catch (error) {
331
- throw new CliContractError('RUN_EXISTS', `run storeが既に存在する: ${String(error?.code ?? error)}`);
648
+ if (error instanceof CliContractError) throw error;
649
+ if (error?.code !== 'ENOENT') throw error;
332
650
  }
333
651
  const meta = {
334
652
  schema: 'lattice.run_meta.v1',
@@ -336,14 +654,23 @@ async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
336
654
  executor_adapter: executorAdapter,
337
655
  plan_digest: result.plan.plan_digest,
338
656
  };
339
- await writeFile(path.join(runDir, 'request.json'), `${JSON.stringify(request, null, 1)}\n`);
340
- await writeFile(path.join(runDir, 'plan-compile-result.json'), `${JSON.stringify(compileArtifact, null, 1)}\n`);
341
- await writeFile(path.join(runDir, 'events.json'), `${JSON.stringify(events, null, 1)}\n`);
342
- await writeFile(path.join(runDir, 'run-meta.json'), `${JSON.stringify(meta, null, 1)}\n`);
657
+ try {
658
+ await writeJsonFile(path.join(temporaryDir, 'request.json'), request);
659
+ await writeJsonFile(path.join(temporaryDir, 'plan-compile-result.json'), compileArtifact);
660
+ await writeJsonFile(path.join(temporaryDir, 'events.json'), events);
661
+ await writeJsonFile(path.join(temporaryDir, 'run-meta.json'), meta);
662
+ await rename(temporaryDir, runDir);
663
+ } catch (error) {
664
+ await rm(temporaryDir, { recursive: true, force: true });
665
+ if (error?.code === 'EEXIST' || error?.code === 'ENOTEMPTY') {
666
+ throw new CliContractError('RUN_EXISTS', 'run storeが既に存在する');
667
+ }
668
+ throw error;
669
+ }
343
670
  const output = {
344
671
  schema: 'lattice.run_start_result.v1',
345
672
  run_id: request.request_id,
346
- run_dir: path.relative(cwd, runDir),
673
+ run_dir: path.relative(repoRoot, runDir),
347
674
  executor_adapter: executorAdapter,
348
675
  plan_digest: result.plan.plan_digest,
349
676
  events_digest: digestArtifact(events.map(({ event_digest: digest }) => digest)),
@@ -357,7 +684,7 @@ async function runObserve({ runDir, stdout }) {
357
684
  const { events } = await readRunStore(runDir);
358
685
  const chain = verifyRunEventChain({ events });
359
686
  if (!chain.valid) {
360
- throw new CliContractError('EVENT_CHAIN_INVALID', 'event chainが不正', chain.failed_conditions);
687
+ throw new CliContractError('EVENT_CHAIN_INVALID', 'event chainが不正', { failed_conditions: chain.failed_conditions });
361
688
  }
362
689
  const state = projectRuntimeState({ events });
363
690
  const output = {
@@ -378,13 +705,15 @@ async function runObserve({ runDir, stdout }) {
378
705
  }
379
706
 
380
707
  async function runStatus({ runDir, stdout }) {
381
- const { events, meta, compileArtifact } = await readRunStore(runDir);
708
+ const { events, meta, compileArtifact, managed } = await readRunStore(runDir);
382
709
  const chain = verifyRunEventChain({ events });
383
710
  if (!chain.valid) {
384
- throw new CliContractError('EVENT_CHAIN_INVALID', 'event chainが不正', chain.failed_conditions);
711
+ throw new CliContractError('EVENT_CHAIN_INVALID', 'event chainが不正', { failed_conditions: chain.failed_conditions });
385
712
  }
386
713
  const state = projectRuntimeState({ events });
387
- const frontier = computeReadyFrontier({ plan: compileArtifact.plan, events });
714
+ const computedFrontier = computeReadyFrontier({ plan: compileArtifact.plan, events });
715
+ const managedFrozen = managed === null ? false : await isManagedRunFrozen(runDir, events);
716
+ const frontier = managedFrozen ? { dispatchable: [] } : computedFrontier;
388
717
  const output = {
389
718
  schema: 'lattice.run_status.v1',
390
719
  run_id: meta.run_id,
@@ -397,11 +726,1961 @@ async function runStatus({ runDir, stdout }) {
397
726
  closed: state.closed,
398
727
  event_count: events.length,
399
728
  };
729
+ if (managed !== null) {
730
+ output.schema = 'lattice.managed_run_status.v1';
731
+ const runtimeProjection = {
732
+ schema: 'lattice.runtime_status_projection.v1',
733
+ ...projectRuntimeStatusOverlays({ events }),
734
+ runtime_frozen: managedFrozen,
735
+ };
736
+ runtimeProjection.projection_digest = digestArtifact(runtimeProjection);
737
+ output.runtime_projection = runtimeProjection;
738
+ }
739
+ output.result_digest = digestArtifact(output);
740
+ stdout.write(`${JSON.stringify(output)}\n`);
741
+ return 0;
742
+ }
743
+
744
+ async function runList({ cwd, stdout }) {
745
+ const repoRoot = await resolveRepoRoot(cwd);
746
+ await requireSafeRunAncestors(repoRoot);
747
+ const root = path.join(repoRoot, ...RUN_STORE_ROOT);
748
+ let entries;
749
+ try {
750
+ const rootState = await lstat(root);
751
+ if (rootState.isSymbolicLink() || !rootState.isDirectory()) {
752
+ throw new CliContractError('INVALID_RUN_STORE', '.lattice/runsが安全なdirectoryではない');
753
+ }
754
+ entries = await readdir(root, { withFileTypes: true });
755
+ } catch (error) {
756
+ if (error?.code === 'ENOENT') entries = [];
757
+ else throw error;
758
+ }
759
+ const activeRuns = [];
760
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
761
+ if (!entry.isDirectory() || !RUN_REF.test(`.lattice/runs/${entry.name}`)) {
762
+ throw new CliContractError('INVALID_RUN_STORE', `不正なrun store entry: ${entry.name}`);
763
+ }
764
+ const runDir = path.join(root, entry.name);
765
+ const { events, meta, request } = await readRunStore(runDir);
766
+ requireValidEventChain(events);
767
+ if (!projectRuntimeState({ events }).closed) {
768
+ activeRuns.push({
769
+ run_id: meta.run_id,
770
+ run_ref: `.lattice/runs/${entry.name}`,
771
+ base_sha: request.repo.base_sha,
772
+ executor_adapter: meta.executor_adapter,
773
+ });
774
+ }
775
+ }
776
+ const output = { schema: 'lattice.run_list.v1', active_runs: activeRuns };
777
+ output.result_digest = digestArtifact(output);
778
+ stdout.write(`${JSON.stringify(output)}\n`);
779
+ return 0;
780
+ }
781
+
782
+ function requireValidEventChain(events) {
783
+ const chain = verifyRunEventChain({ events });
784
+ if (!chain.valid) {
785
+ throw new CliContractError('EVENT_CHAIN_INVALID', 'event chainが不正', { failed_conditions: chain.failed_conditions });
786
+ }
787
+ }
788
+
789
+ async function runResume({ runDir, repoRoot, stdout }) {
790
+ const { events, meta, compileArtifact, request, managed } = await readRunStore(runDir);
791
+ requireValidEventChain(events);
792
+ const state = projectRuntimeState({ events });
793
+ if (state.closed) {
794
+ throw new CliContractError('RUN_CLOSED', 'closed runはresumeできない');
795
+ }
796
+ await resolveRepoBinding(repoRoot, request);
797
+ // managed write gateの完全検証とdispatchはsupervisorだけが所有する。read-only
798
+ // resumeがCLIからleaseを再認可しないよう、managed runでは常に空frontierを返す。
799
+ const managedFrozen = managed === null ? false : await isManagedRunFrozen(runDir, events);
800
+ const frontier = managedFrozen
801
+ ? { dispatchable: [] }
802
+ : computeReadyFrontier({ plan: compileArtifact.plan, events });
803
+ const output = {
804
+ schema: 'lattice.run_resume_result.v1',
805
+ outcome: 'resumable',
806
+ run_id: meta.run_id,
807
+ executor_adapter: meta.executor_adapter,
808
+ dispatchable: frontier.dispatchable,
809
+ running: state.running,
810
+ accepted: state.accepted,
811
+ event_count: events.length,
812
+ };
400
813
  output.result_digest = digestArtifact(output);
401
814
  stdout.write(`${JSON.stringify(output)}\n`);
402
815
  return 0;
403
816
  }
404
817
 
818
+ async function runClose({ runDir, repoRoot, stdout, requestId = null }) {
819
+ return withLifecycleLock(runDir, async () => {
820
+ const { events, meta, compileArtifact, request, managed } = await readRunStore(runDir);
821
+ requireValidEventChain(events);
822
+ const initialState = projectRuntimeState({ events });
823
+ let next = events;
824
+ let alreadyClosed = initialState.closed;
825
+ const existingClose = events.findLast((event) => event.kind === 'run_closed');
826
+ if (alreadyClosed && existingClose?.payload?.outcome === 'abandoned') {
827
+ throw new CliContractError('RUN_ABANDONED', 'abandoned runは正常closeへ変更できない');
828
+ }
829
+ await resolveRepoBinding(repoRoot, request);
830
+ if (!alreadyClosed) {
831
+ const closed = closeRunIfComplete({
832
+ runId: meta.run_id,
833
+ plan: compileArtifact.plan,
834
+ events,
835
+ recordedAt: canonicalNow(),
836
+ });
837
+ if (!closed.closed) {
838
+ throw new CliContractError(
839
+ 'RUN_NOT_COMPLETE',
840
+ '全TODOのreceipt accepted前はrunを正常closeできない',
841
+ { guidance: 'run statusで未完了TODOを確認するか、run abandonを明示実行する' },
842
+ );
843
+ }
844
+ if (managed !== null) {
845
+ await runManagedControl({ runDir, runRef: path.relative(repoRoot, runDir),
846
+ operation: 'close', artifactDigest: null, stdout, shutdownReason: 'normal-close',
847
+ emit: false, requestId });
848
+ next = (await readRunStore(runDir)).events;
849
+ } else {
850
+ next = closed.events;
851
+ await replaceEventsAtomically(runDir, next);
852
+ }
853
+ }
854
+ const output = {
855
+ schema: 'lattice.run_close_result.v1',
856
+ outcome: 'closed',
857
+ run_id: meta.run_id,
858
+ already_closed: alreadyClosed,
859
+ event_count: next.length,
860
+ events_digest: digestArtifact(next.map(({ event_digest: digest }) => digest)),
861
+ };
862
+ output.result_digest = digestArtifact(output);
863
+ stdout.write(`${JSON.stringify(output)}\n`);
864
+ return 0;
865
+ });
866
+ }
867
+
868
+ async function runAbandon({ runDir, runRef, reason, stdout, requestId = null }) {
869
+ if (!ABANDON_REASON.test(reason)) {
870
+ throw new CliContractError('INVALID_ABANDON_REASON', 'reasonは128文字以下の識別子でなければならない');
871
+ }
872
+ return withLifecycleLock(runDir, async () => {
873
+ const current = await readRunStore(runDir);
874
+ const { events, meta, compileArtifact } = current;
875
+ requireValidEventChain(events);
876
+ const state = projectRuntimeState({ events });
877
+ if (state.closed) {
878
+ const existing = events.findLast((event) => event.kind === 'run_closed');
879
+ if (current.managed !== null && existing?.payload?.outcome === 'abandoned'
880
+ && existing.payload.reason === reason) {
881
+ const output = { schema: 'lattice.run_abandon_result.v1', outcome: 'abandoned', reason,
882
+ run_id: meta.run_id, event_count: events.length,
883
+ events_digest: digestArtifact(events.map(({ event_digest: value }) => value)) };
884
+ output.result_digest = digestArtifact(output); stdout.write(`${JSON.stringify(output)}\n`); return 0;
885
+ }
886
+ throw new CliContractError('RUN_CLOSED', 'closed runはabandonできない');
887
+ }
888
+ let next = [...events];
889
+ if (current.managed !== null) {
890
+ await runManagedControl({ runDir, runRef, operation: 'abandon', artifactDigest: null,
891
+ shutdownReason: reason, stdout, emit: false, requestId });
892
+ next = (await readRunStore(runDir)).events;
893
+ } else next.push(buildNextRunEvent({
894
+ events: next,
895
+ runId: meta.run_id,
896
+ kind: 'run_closed',
897
+ planEpoch: compileArtifact.plan.plan_epoch,
898
+ subject: { kind: 'runtime_plan', ref: compileArtifact.plan.plan_ref },
899
+ payload: { outcome: 'abandoned', reason, accepted: state.accepted },
900
+ recordedAt: canonicalNow(),
901
+ }));
902
+ if (current.managed === null) await replaceEventsAtomically(runDir, next);
903
+ const output = {
904
+ schema: 'lattice.run_abandon_result.v1',
905
+ outcome: 'abandoned',
906
+ reason,
907
+ run_id: meta.run_id,
908
+ event_count: next.length,
909
+ events_digest: digestArtifact(next.map(({ event_digest: digest }) => digest)),
910
+ };
911
+ output.result_digest = digestArtifact(output);
912
+ stdout.write(`${JSON.stringify(output)}\n`);
913
+ return 0;
914
+ });
915
+ }
916
+
917
+ async function readManagedSession(runDir) {
918
+ const supervisorDir = path.join(runDir, 'supervisor');
919
+ const activePaths = await resolveActiveRuntimePaths({ runDir });
920
+ const descriptorPath = activePaths.descriptorPath;
921
+ const sessionPath = activePaths.sessionPath;
922
+ const descriptor = await readBoundedJson(descriptorPath, 'runtime supervisor descriptor');
923
+ const sessionInfo = await lstat(sessionPath).catch(() => null);
924
+ const descriptorKeys = ['schema', 'run_id', 'pid', 'process_start_identity', 'socket_ref',
925
+ 'session_nonce_digest', 'protocol_version', 'activated_at', 'descriptor_digest'];
926
+ const identityKeys = ['schema', 'platform', 'pid', 'started_identity', 'identity_digest'];
927
+ const descriptorBody = { ...descriptor };
928
+ delete descriptorBody.descriptor_digest;
929
+ const identity = descriptor?.process_start_identity;
930
+ const identityBody = { ...identity };
931
+ delete identityBody.identity_digest;
932
+ if (descriptor?.schema !== 'lattice.runtime_supervisor_descriptor.v1'
933
+ || Object.keys(descriptor).sort().join('\0') !== descriptorKeys.sort().join('\0')
934
+ || typeof descriptor.run_id !== 'string'
935
+ || !Number.isSafeInteger(descriptor.pid) || descriptor.pid < 1
936
+ || identity?.schema !== 'lattice.process_start_identity.v1'
937
+ || Object.keys(identity).sort().join('\0') !== identityKeys.sort().join('\0')
938
+ || identity.pid !== descriptor.pid
939
+ || identity.identity_digest !== digestArtifact(identityBody)
940
+ || descriptor.descriptor_digest !== digestArtifact(descriptorBody)
941
+ || descriptor.socket_ref !== 'supervisor/control.sock'
942
+ || sessionInfo === null || !sessionInfo.isFile() || sessionInfo.isSymbolicLink()
943
+ || (sessionInfo.mode & 0o077) !== 0) {
944
+ throw new CliContractError('RUN_NOT_MANAGED', 'live supervisor bindingが不正');
945
+ }
946
+ const sessionNonce = (await readFile(sessionPath, 'utf8')).trim();
947
+ const nonceDigest = digestArtifact(sessionNonce);
948
+ if (sessionNonce.length < 32 || nonceDigest !== descriptor.session_nonce_digest) {
949
+ throw new CliContractError('RUN_NOT_MANAGED', 'supervisor sessionがdescriptorと一致しない');
950
+ }
951
+ if (activePaths.pointer !== null
952
+ && (activePaths.pointer.descriptor_digest !== descriptor.descriptor_digest
953
+ || activePaths.pointer.session_nonce_digest !== nonceDigest)) {
954
+ throw new CliContractError('RUN_NOT_MANAGED', 'active runtime pointer bindingが不正');
955
+ }
956
+ const socketPath = path.join(supervisorDir, 'control.sock');
957
+ const socketInfo = await lstat(socketPath).catch(() => null);
958
+ if (socketInfo === null || !socketInfo.isSocket() || socketInfo.isSymbolicLink()) {
959
+ throw new CliContractError('RUN_NOT_MANAGED', 'supervisor control socketがliveではない');
960
+ }
961
+ return { descriptor, sessionNonce, socketPath };
962
+ }
963
+
964
+ function controlOperationPayload({ operation, runRef, artifact = null, artifactDigest,
965
+ checkpointDigest = null, expectedEpoch, expectedQueueDigest, shutdownReason = null }) {
966
+ const value = {
967
+ schema: 'lattice.runtime_control_operation.v1',
968
+ operation,
969
+ run_ref: runRef,
970
+ artifact,
971
+ artifact_digest: artifactDigest,
972
+ checkpoint_digest: checkpointDigest,
973
+ expected_epoch: expectedEpoch,
974
+ expected_queue_digest: expectedQueueDigest,
975
+ shutdown_reason: shutdownReason,
976
+ };
977
+ value.operation_digest = digestArtifact(value);
978
+ return value;
979
+ }
980
+
981
+ async function isActiveSupervisorGateCommitted(runDir, events, expectedEpoch) {
982
+ if (await isManagedRunFrozen(runDir, events)) return false;
983
+ const active = await resolveActiveRuntimePaths({ runDir });
984
+ const sessionNonceDigest = active.pointer?.session_nonce_digest
985
+ ?? digestArtifact((await readFile(active.sessionPath, 'utf8')).trim());
986
+ const gateDocument = await readBoundedJson(path.join(runDir, 'supervisor', 'write-gate.json'),
987
+ 'supervisor write gate');
988
+ const committedGate = await createRuntimeGateStore({ runDir, runId: gateDocument.run_id,
989
+ sessionNonceDigest, controlEventsPath: active.controlEventsPath }).read();
990
+ if (committedGate === null || committedGate.gate.plan_epoch !== expectedEpoch) return false;
991
+ const gate = committedGate.gate;
992
+ const controlEvents = await readBoundedJson(active.controlEventsPath,
993
+ 'runtime control events');
994
+ const resumed = controlEvents.findLast((event) => event.kind === 'intake_resumed'
995
+ && event.payload?.plan_epoch === expectedEpoch
996
+ && event.payload?.gate_digest === gate.gate_digest);
997
+ return resumed !== undefined && resumed.session_nonce_digest === sessionNonceDigest;
998
+ }
999
+
1000
+ async function runManagedControl({ runDir, runRef, operation, artifact = null, artifactDigest, stdout,
1001
+ checkpointDigest = null, shutdownReason = null, emit = true, requestId = null }) {
1002
+ const committed = await readCommittedEpochStore(runDir);
1003
+ if (committed === null) throw new CliContractError('RUN_NOT_MANAGED', 'runがmanaged storeへactivateされていない');
1004
+ const { descriptor, sessionNonce, socketPath } = await readManagedSession(runDir);
1005
+ if (descriptor.run_id !== committed.meta.run_id) {
1006
+ throw new CliContractError('RUN_NOT_MANAGED', 'supervisor descriptorが別runに属する');
1007
+ }
1008
+ let expectedQueueDigest = null;
1009
+ try {
1010
+ const queue = await readBoundedJson(path.join(runDir, 'queued-events.json'), 'runtime queue');
1011
+ expectedQueueDigest = queue.queue_digest;
1012
+ } catch (error) {
1013
+ if (!(error instanceof CliContractError) || error.code !== 'INPUT_UNREADABLE') throw error;
1014
+ }
1015
+ let expectedEpoch = operation === 'activate' ? 1 : operation === 'recompile'
1016
+ && Number.isSafeInteger(artifact?.predecessor_epoch)
1017
+ ? artifact.predecessor_epoch : committed.pointer.plan_epoch;
1018
+ if (operation === 'recompile' && requestId !== null) {
1019
+ const transaction = await readBoundedJson(path.join(runDir,
1020
+ 'epoch-activation-transaction.json'), 'epoch activation transaction').catch(() => null);
1021
+ if (transaction?.request_id === requestId
1022
+ && transaction.recompile_request_digest === artifact?.request_digest) {
1023
+ expectedEpoch = transaction.successor_epoch - 1;
1024
+ expectedQueueDigest = transaction.queue_head_digest;
1025
+ }
1026
+ }
1027
+ if (operation === 'reprocess' && requestId !== null) {
1028
+ const pending = await readBoundedJson(path.join(runDir, 'pending-recompile.json'),
1029
+ 'pending recompile').catch(() => null);
1030
+ if (pending?.reprocess_request_id === requestId
1031
+ && pending.pending_digest === selfDigest(pending, 'pending_digest')) {
1032
+ expectedEpoch = pending.predecessor_epoch;
1033
+ expectedQueueDigest = pending.reprocess_queue_digest;
1034
+ } else {
1035
+ const transaction = await readBoundedJson(path.join(runDir,
1036
+ 'epoch-activation-transaction.json'), 'epoch activation transaction').catch(() => null);
1037
+ if (transaction?.schema === 'lattice.runtime_epoch_activation_transaction.v1'
1038
+ && transaction.transaction_digest === selfDigest(transaction, 'transaction_digest')) {
1039
+ expectedEpoch = transaction.successor_epoch - 1;
1040
+ expectedQueueDigest = transaction.queue_head_digest;
1041
+ }
1042
+ }
1043
+ }
1044
+ if (operation === 'activate') expectedQueueDigest = null;
1045
+ const payload = controlOperationPayload({
1046
+ operation, runRef, artifact, artifactDigest, checkpointDigest,
1047
+ expectedEpoch,
1048
+ expectedQueueDigest, shutdownReason,
1049
+ });
1050
+ const request = createRuntimeControlRequest({
1051
+ requestId: requestId ?? randomUUID(), runId: committed.meta.run_id, operation,
1052
+ payload, sessionNonce,
1053
+ });
1054
+ if (operation === 'reprocess' && requestId !== null
1055
+ && committed.pointer.plan_epoch === expectedEpoch + 1) {
1056
+ const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
1057
+ if (await isActiveSupervisorGateCommitted(runDir, events, committed.pointer.plan_epoch)) {
1058
+ const activeRuntime = await resolveActiveRuntimePaths({ runDir });
1059
+ const journal = await readBoundedJson(activeRuntime.controlEventsPath,
1060
+ 'runtime control events');
1061
+ const result = buildControlResult({ operation: 'reprocess', outcome: 'reprocessed',
1062
+ eventHeadDigest: events.at(-1)?.event_digest ?? null,
1063
+ controlHeadDigest: journal.at(-1)?.event_digest ?? null,
1064
+ activeEpoch: committed.pointer.plan_epoch, stagedEpoch: null });
1065
+ let response = buildControlResponse(request, 'completed', result,
1066
+ journal.at(-1)?.event_digest ?? null);
1067
+ const store = createRuntimeControlStore({ runDir, runId: committed.meta.run_id,
1068
+ clock: canonicalNow });
1069
+ const known = await store.readRequest(request);
1070
+ if (known?.state === 'completed' && known.response.outcome === 'completed') {
1071
+ response = known.response;
1072
+ } else if (known?.state === 'completed' && known.response.outcome === 'rejected') {
1073
+ response = await store.recoverCompletedRequest(request, response);
1074
+ } else {
1075
+ response = await store.completeRequest(request, response);
1076
+ }
1077
+ if (emit) stdout.write(`${JSON.stringify(response.result)}\n`);
1078
+ return emit ? 0 : response.result;
1079
+ }
1080
+ }
1081
+ let response;
1082
+ let ambiguous = false;
1083
+ for (let attempt = 0; attempt < 100; attempt += 1) {
1084
+ try {
1085
+ response = await sendRuntimeControlRequest({ socketPath, request,
1086
+ timeoutMs: ['recompile', 'reprocess'].includes(operation) ? 60_000 : 5_000 });
1087
+ } catch (error) {
1088
+ if (error?.code === 'RUN_OUTCOME_UNKNOWN') ambiguous = true;
1089
+ else if (!(ambiguous && error?.code === 'RUN_NOT_MANAGED')) throw error;
1090
+ if (operation === 'reprocess' && requestId !== null) {
1091
+ const latest = await readCommittedEpochStore(runDir);
1092
+ const latestEvents = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
1093
+ if (latest?.pointer.plan_epoch === expectedEpoch + 1
1094
+ && await isActiveSupervisorGateCommitted(runDir, latestEvents,
1095
+ latest.pointer.plan_epoch)) {
1096
+ return runManagedControl({ runDir, runRef, operation, artifact, artifactDigest,
1097
+ stdout, checkpointDigest, shutdownReason, emit, requestId });
1098
+ }
1099
+ }
1100
+ if (attempt === 99) throw new CliContractError('RUN_OUTCOME_UNKNOWN',
1101
+ `managed ${operation}の結果が確定しない。同一request_id=${request.request_id}`);
1102
+ await new Promise((resolve) => setTimeout(resolve, 50));
1103
+ continue;
1104
+ }
1105
+ if (response.outcome !== 'unknown') break;
1106
+ if (operation === 'reprocess' && requestId !== null) {
1107
+ const latest = await readCommittedEpochStore(runDir);
1108
+ const latestEvents = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
1109
+ if (latest?.pointer.plan_epoch === expectedEpoch + 1
1110
+ && await isActiveSupervisorGateCommitted(runDir, latestEvents,
1111
+ latest.pointer.plan_epoch)) {
1112
+ return runManagedControl({ runDir, runRef, operation, artifact, artifactDigest,
1113
+ stdout, checkpointDigest, shutdownReason, emit, requestId });
1114
+ }
1115
+ }
1116
+ if (attempt === 99) throw new CliContractError('RUN_OUTCOME_UNKNOWN',
1117
+ `managed ${operation}の結果が確定しない。同一request_id=${request.request_id}`);
1118
+ await new Promise((resolve) => setTimeout(resolve, 50));
1119
+ }
1120
+ if (response.outcome !== 'completed') {
1121
+ const code = response.result?.unmet?.[0] ?? 'RUN_FROZEN';
1122
+ throw new CliContractError(code, `managed ${operation}が${response.outcome}で終了した: ${response.result?.unmet?.[1] ?? code}`);
1123
+ }
1124
+ const resultKeys = ['schema', 'operation', 'outcome', 'event_head_digest', 'control_head_digest',
1125
+ 'active_epoch', 'staged_epoch', 'unmet', 'result_digest'];
1126
+ if (operation === 'finding_record') resultKeys.push('finding_digest');
1127
+ const resultBody = { ...response.result };
1128
+ delete resultBody.result_digest;
1129
+ if (response.result?.schema !== 'lattice.runtime_control_result.v1'
1130
+ || Object.keys(response.result).sort().join('\0') !== resultKeys.sort().join('\0')
1131
+ || response.result.operation !== operation
1132
+ || response.result.result_digest !== digestArtifact(resultBody)) {
1133
+ throw new CliContractError('RUN_NOT_MANAGED', 'managed control result bindingが不正');
1134
+ }
1135
+ if (emit) stdout.write(`${JSON.stringify(response.result)}\n`);
1136
+ return emit ? 0 : response.result;
1137
+ }
1138
+
1139
+ function buildControlResult({ operation, outcome, eventHeadDigest, controlHeadDigest, activeEpoch, stagedEpoch = null, unmet = [] }) {
1140
+ const result = {
1141
+ schema: 'lattice.runtime_control_result.v1', operation, outcome,
1142
+ event_head_digest: eventHeadDigest, control_head_digest: controlHeadDigest,
1143
+ active_epoch: activeEpoch, staged_epoch: stagedEpoch, unmet,
1144
+ };
1145
+ result.result_digest = digestArtifact(result);
1146
+ return result;
1147
+ }
1148
+
1149
+ function buildControlResponse(request, outcome, result, controlHeadDigest) {
1150
+ const response = {
1151
+ schema: 'lattice.runtime_control_response.v1', request_id: request.request_id,
1152
+ run_id: request.run_id, outcome, result, control_head_digest: controlHeadDigest,
1153
+ };
1154
+ response.response_digest = digestArtifact(response);
1155
+ return response;
1156
+ }
1157
+
1158
+ function validateRuntimeQueue(queue, runId, frozenEpoch) {
1159
+ return queue !== null && typeof queue === 'object' && !Array.isArray(queue)
1160
+ && Object.keys(queue).sort().join('\0') === ['schema', 'run_id', 'frozen_epoch', 'entries', 'queue_digest'].sort().join('\0')
1161
+ && queue.schema === 'lattice.runtime_queue.v1' && queue.run_id === runId
1162
+ && queue.frozen_epoch === frozenEpoch && Array.isArray(queue.entries)
1163
+ && queue.entries.every((entry, index) => entry !== null && typeof entry === 'object'
1164
+ && !Array.isArray(entry)
1165
+ && Object.keys(entry).sort().join('\0') === ['sequence', 'kind', 'subject_digest', 'artifact_digest'].sort().join('\0')
1166
+ && Number.isSafeInteger(entry.sequence) && entry.sequence === index + 1
1167
+ && typeof entry.kind === 'string' && entry.kind.length > 0
1168
+ && /^[0-9a-f]{64}$/u.test(entry.subject_digest)
1169
+ && /^[0-9a-f]{64}$/u.test(entry.artifact_digest))
1170
+ && queue.queue_digest === selfDigest(queue, 'queue_digest');
1171
+ }
1172
+
1173
+ function controlIntentDigest(request) {
1174
+ return selfDigest({ request_id: request.request_id, run_id: request.run_id,
1175
+ operation: request.operation, payload: request.payload, intent_digest: '' }, 'intent_digest');
1176
+ }
1177
+
1178
+ export function reconstructHoldResultFromJournal({ journal, runId, requestId, intentDigest }) {
1179
+ if (!Array.isArray(journal)) return null;
1180
+ const preparedIndex = journal.findIndex((event) => event.kind === 'hold_prepared'
1181
+ && event.payload?.request_id === requestId
1182
+ && event.payload?.logical_intent_digest === intentDigest);
1183
+ const prepared = journal[preparedIndex];
1184
+ const barrierIndex = prepared === undefined ? -1 : journal.findIndex((event, index) => (
1185
+ index > preparedIndex && event.kind === 'barrier_requested'
1186
+ && event.payload?.barrier_id === prepared.payload.barrier_id));
1187
+ const barrier = journal[barrierIndex];
1188
+ if (prepared === undefined || barrier === undefined) return null;
1189
+ const acknowledgements = journal.slice(barrierIndex + 1)
1190
+ .filter((event) => event.kind === 'executor_quiesced'
1191
+ && event.payload?.barrier_id === prepared.payload.barrier_id
1192
+ && event.payload?.barrier_control_digest === barrier.event_digest);
1193
+ const acknowledgedTodos = acknowledgements.map((event) => event.payload.todo_id).sort();
1194
+ if (acknowledgements.length !== barrier.payload.running_count
1195
+ || acknowledgements.some((event) => typeof event.payload?.ack?.ack_digest !== 'string')
1196
+ || new Set(acknowledgedTodos).size !== acknowledgedTodos.length
1197
+ || JSON.stringify(acknowledgedTodos) !== JSON.stringify(barrier.payload.running_todo_ids)) return null;
1198
+ const result = {
1199
+ schema: 'lattice.runtime_hold_result.v1', run_id: runId,
1200
+ finding_digest: prepared.payload.finding_digest, barrier_id: prepared.payload.barrier_id,
1201
+ quiescence_ack_digests: acknowledgements.map((event) => event.payload.ack.ack_digest).sort(),
1202
+ outcome: 'held', recorded_at: prepared.payload.recorded_at, result_digest: '',
1203
+ };
1204
+ result.result_digest = selfDigest(result, 'result_digest');
1205
+ return result;
1206
+ }
1207
+
1208
+ async function runActivate({ runDir, runRef, repoRoot, stdout, requestId = null }) {
1209
+ return withLifecycleLock(runDir, async () => {
1210
+ const { events, meta, managed } = await readRunStore(runDir);
1211
+ if (!['lattice.run_meta.v1', 'lattice.run_meta.v2'].includes(meta.schema)) throw new CliContractError('RUN_NOT_MANAGED', 'run metaがactivate対象でない');
1212
+ if (projectRuntimeState({ events }).closed) throw new CliContractError('RUN_CLOSED', 'closed runは再activateできない');
1213
+ if (managed !== null && requestId !== null) {
1214
+ try {
1215
+ return await runManagedControl({ runDir, runRef, operation: 'activate', artifactDigest: null,
1216
+ stdout, requestId });
1217
+ } catch (error) {
1218
+ if (error?.code !== 'RUN_NOT_MANAGED') throw error;
1219
+ }
1220
+ }
1221
+ if (managed !== null) await prepareManagedSupervisorRestart({ runDir });
1222
+ const launched = await launchDurableSupervisor({ runDir });
1223
+ const payload = controlOperationPayload({
1224
+ operation: 'activate', runRef, artifactDigest: null, expectedEpoch: 1, expectedQueueDigest: null,
1225
+ });
1226
+ const request = createRuntimeControlRequest({ requestId: requestId ?? randomUUID(), runId: meta.run_id,
1227
+ operation: 'activate', payload, sessionNonce: launched.sessionNonce });
1228
+ let response;
1229
+ let ambiguous = false;
1230
+ for (let attempt = 0; attempt < 100; attempt += 1) {
1231
+ try {
1232
+ response = await sendRuntimeActivationRequest({
1233
+ socketPath: launched.socketPath, request, expectedPid: launched.pid,
1234
+ expectedProcessStartIdentity: launched.processStartIdentity,
1235
+ });
1236
+ } catch (error) {
1237
+ if (error?.code === 'RUN_OUTCOME_UNKNOWN') ambiguous = true;
1238
+ else if (!(ambiguous && error?.code === 'RUN_NOT_MANAGED')) throw error;
1239
+ if (attempt === 99) throw new CliContractError('RUN_OUTCOME_UNKNOWN',
1240
+ `managed activateの結果が確定しない。同一request_id=${request.request_id}`);
1241
+ await new Promise((resolve) => setTimeout(resolve, 50));
1242
+ continue;
1243
+ }
1244
+ if (response.outcome !== 'unknown') break;
1245
+ if (attempt === 99) throw new CliContractError('RUN_OUTCOME_UNKNOWN',
1246
+ `managed activateの結果が確定しない。同一request_id=${request.request_id}`);
1247
+ await new Promise((resolve) => setTimeout(resolve, 50));
1248
+ }
1249
+ if (response.outcome !== 'completed') {
1250
+ const code = response.result?.unmet?.[0] ?? 'ADAPTER_CONTROLLER_UNAVAILABLE';
1251
+ const deadline = Date.now() + 2_000;
1252
+ while (Date.now() < deadline) {
1253
+ try { await lstat(path.join(runDir, 'supervisor')); } catch (error) {
1254
+ if (error?.code === 'ENOENT') break;
1255
+ }
1256
+ await new Promise((resolve) => setTimeout(resolve, 20));
1257
+ }
1258
+ throw new CliContractError(code, `managed activateが${response.outcome}で終了した: ${response.result?.unmet?.[1] ?? code}`);
1259
+ }
1260
+ // daemonがpointerまでcommitしたことを再読してから成功を返す。
1261
+ const committed = await readCommittedEpochStore(runDir);
1262
+ if (committed === null
1263
+ || !events.some((event) => event.event_digest === committed.pointer.activation_run_event_digest)) {
1264
+ throw new CliContractError('RUN_NOT_MANAGED', 'activate commitを再検証できない');
1265
+ }
1266
+ stdout.write(`${JSON.stringify(response.result)}\n`);
1267
+ return 0;
1268
+ });
1269
+ }
1270
+
1271
+ export async function runManagedSupervisorDaemon({
1272
+ runDir, sessionNonce, serveRuntimeControlSocket, activateController, onReady,
1273
+ registerDaemonCleanup = () => {}, crashInjector = null,
1274
+ validatePhaseRevision = null, commitPhaseRevision = null,
1275
+ }) {
1276
+ const repoRoot = await realpath(path.resolve(runDir, '..', '..', '..'));
1277
+ const request = await readBoundedJson(path.join(runDir, 'request.json'), 'run request');
1278
+ const compileArtifact = await readBoundedJson(path.join(runDir, 'plan-compile-result.json'), 'plan compile result');
1279
+ const legacyMeta = await readBoundedJson(path.join(runDir, 'run-meta.json'), 'run meta');
1280
+ const supervisorDir = path.join(runDir, 'supervisor');
1281
+ await mkdir(supervisorDir, { recursive: true, mode: 0o700 });
1282
+ const socketPath = path.join(supervisorDir, 'control.sock');
1283
+ let managedSupervisor = null;
1284
+ let activation = null;
1285
+ let additionalActivations = [];
1286
+ let activationCommitted = false;
1287
+ let controlEvents = [];
1288
+ const restarting = legacyMeta.schema === 'lattice.run_meta.v2';
1289
+ let candidateDir = null;
1290
+ let candidateControllerDir = null;
1291
+ let recoveryRegistrationDigest = null;
1292
+ const requestStore = createRuntimeControlStore({ runDir, runId: request.request_id,
1293
+ clock: canonicalNow });
1294
+ let eventStore = requestStore;
1295
+ let server;
1296
+
1297
+ const appendControl = async ({ run_id: runId, kind, session_nonce_digest: sessionDigest, payload }) => {
1298
+ const eventDigest = await eventStore.append({ run_id: runId, kind,
1299
+ session_nonce_digest: sessionDigest, payload });
1300
+ controlEvents = await eventStore.readEvents();
1301
+ return eventDigest;
1302
+ };
1303
+ let gateWriter = null;
1304
+ const resolveObservationBinding = async ({ binding }) => {
1305
+ const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
1306
+ const dispatch = events.findLast((event) => event.kind === 'executor_dispatched'
1307
+ && event.subject?.kind === 'todo' && event.subject.ref === binding?.todo_id
1308
+ && event.payload?.executor_handle === binding?.executor_handle);
1309
+ const observation = dispatch?.payload?.direct_os_observation_binding;
1310
+ if (observation === null || typeof observation !== 'object' || Array.isArray(observation)) {
1311
+ throw new ManagedRuntimeError('HOLD_ACKS_INCOMPLETE', `durable Direct OS binding不足: ${binding?.todo_id ?? 'unknown'}`);
1312
+ }
1313
+ return structuredClone(observation);
1314
+ };
1315
+ const resolveRunningBindings = async ({ runId }) => {
1316
+ const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
1317
+ const state = projectRuntimeState({ events });
1318
+ return state.running.map((todoId) => {
1319
+ const dispatch = events.findLast((event) => event.kind === 'executor_dispatched'
1320
+ && event.subject?.kind === 'todo' && event.subject.ref === todoId);
1321
+ const payload = dispatch?.payload ?? {};
1322
+ const binding = {
1323
+ todo_id: todoId, executor_handle: payload.executor_handle,
1324
+ worktree_id: payload.worktree_id, plan_epoch: dispatch?.plan_epoch,
1325
+ packet_digest: payload.packet_digest, write_lease_id: payload.write_lease_id,
1326
+ controller_registration_digest: recoveryRegistrationDigest
1327
+ ?? payload.controller_registration_digest,
1328
+ };
1329
+ if (Object.values(binding).some((value) => value === undefined)) {
1330
+ throw new ManagedRuntimeError('HOLD_ACKS_INCOMPLETE', `durable running binding不足: ${todoId}`);
1331
+ }
1332
+ return binding;
1333
+ });
1334
+ };
1335
+
1336
+ const executeControl = async (controlRequest) => {
1337
+ if (controlRequest.run_id !== request.request_id || controlRequest.session_nonce !== sessionNonce) {
1338
+ throw new ManagedRuntimeError('RUN_NOT_MANAGED', 'daemon session binding不一致');
1339
+ }
1340
+ try {
1341
+ if (controlRequest.operation === 'activate') {
1342
+ if (activation !== null) throw new ManagedRuntimeError('RUN_BUSY', '既にactivate済み');
1343
+ if (restarting) {
1344
+ candidateDir = path.join(supervisorDir, 'restart-candidates', digestArtifact(sessionNonce));
1345
+ await mkdir(candidateDir, { recursive: true, mode: 0o700 });
1346
+ const priorActive = await resolveActiveRuntimePaths({ runDir });
1347
+ const priorControl = await readFile(priorActive.controlEventsPath).catch((error) => {
1348
+ if (error?.code === 'ENOENT') return Buffer.from('[]\n');
1349
+ throw error;
1350
+ });
1351
+ await durableReplaceBytes(candidateDir, 'control-events.json', priorControl);
1352
+ eventStore = createRuntimeControlStore({ runDir: candidateDir,
1353
+ runId: request.request_id, clock: canonicalNow });
1354
+ }
1355
+ activation = await activateController({ repoRoot, runId: request.request_id,
1356
+ adapterKind: legacyMeta.executor_adapter });
1357
+ if (process.env.NODE_ENV === 'test'
1358
+ && process.env.LATTICE_INTERNAL_TEST_CONTROLLER_COUNT === '2') {
1359
+ additionalActivations = [await activateController({ repoRoot, runId: request.request_id,
1360
+ adapterKind: legacyMeta.executor_adapter })];
1361
+ }
1362
+ if (restarting) recoveryRegistrationDigest = activation.registration.registration_digest;
1363
+ const activationControlDigest = await appendControl({ run_id: request.request_id,
1364
+ kind: 'supervisor_activated', session_nonce_digest: digestArtifact(sessionNonce),
1365
+ payload: activation.activationControlEvent.payload });
1366
+ if (restarting) {
1367
+ const recoveryEvents = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
1368
+ const runningTodos = projectRuntimeState({ events: recoveryEvents }).running;
1369
+ const oldRegistrations = [...new Set(runningTodos.map((todoId) => recoveryEvents.findLast((event) => (
1370
+ event.kind === 'executor_dispatched' && event.subject?.ref === todoId
1371
+ ))?.payload?.controller_registration_digest).filter(Boolean))].sort();
1372
+ await appendControl({ run_id: request.request_id, kind: 'controller_recovery_rebound',
1373
+ session_nonce_digest: digestArtifact(sessionNonce), payload: {
1374
+ old_registration_digests: oldRegistrations,
1375
+ new_registration_digest: activation.registration.registration_digest,
1376
+ running_todo_ids: [...runningTodos].sort(),
1377
+ } });
1378
+ }
1379
+ const controllerDir = path.join(runDir, 'controllers', activation.controllerDescriptor.controller_id);
1380
+ candidateControllerDir = controllerDir;
1381
+ await mkdir(controllerDir, { recursive: true, mode: 0o700 });
1382
+ await writeCanonicalJsonFile(path.join(controllerDir, 'descriptor.json'), activation.controllerDescriptor);
1383
+ await writeCanonicalJsonFile(path.join(controllerDir, 'registration.json'), activation.registration);
1384
+ for (const extra of additionalActivations) {
1385
+ const extraDir = path.join(runDir, 'controllers', extra.controllerDescriptor.controller_id);
1386
+ await mkdir(extraDir, { recursive: true, mode: 0o700 });
1387
+ await writeCanonicalJsonFile(path.join(extraDir, 'descriptor.json'), extra.controllerDescriptor);
1388
+ await writeCanonicalJsonFile(path.join(extraDir, 'registration.json'), extra.registration);
1389
+ }
1390
+ if (restarting) {
1391
+ await replaceCanonicalJsonAtomically(candidateDir, 'descriptor.json', activation.supervisorDescriptor);
1392
+ await durableReplaceBytes(candidateDir, 'session', Buffer.from(`${sessionNonce}\n`));
1393
+ } else {
1394
+ await writeCanonicalJsonFile(path.join(supervisorDir, 'descriptor.json'), activation.supervisorDescriptor);
1395
+ await durableReplaceBytes(supervisorDir, 'session', Buffer.from(`${sessionNonce}\n`));
1396
+ }
1397
+ gateWriter = createRuntimeGateStore({ runDir, runId: request.request_id,
1398
+ sessionNonceDigest: digestArtifact(sessionNonce),
1399
+ controlEventsPath: restarting ? path.join(candidateDir, 'control-events.json')
1400
+ : path.join(runDir, 'control-events.json') });
1401
+ const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
1402
+ const committed = restarting
1403
+ ? await readCommittedEpochStore(runDir)
1404
+ : await activateEpochOneStore({ runDir, request, compileArtifact, legacyMeta,
1405
+ activationRunEventDigest: events.at(-1).event_digest,
1406
+ activationControlEventDigest: activationControlDigest });
1407
+ managedSupervisor = await activation.createManagedSupervisor({
1408
+ resolveObservationBinding, resolveRunningBindings,
1409
+ journal: { append: appendControl }, gateWriter,
1410
+ });
1411
+ for (const extra of additionalActivations) {
1412
+ await extra.registerWithManagedSupervisor(managedSupervisor);
1413
+ }
1414
+ if (restarting) {
1415
+ await managedSupervisor.recoveryBarrier({ barrierId: `recovery-${randomUUID()}`,
1416
+ frozenEventDigest: events.at(-1).event_digest });
1417
+ // recovery barrierのdurable receipt後、単一pointer renameをcommit pointにする。
1418
+ const candidateEvents = await eventStore.readEvents();
1419
+ const restartResult = buildControlResult({ operation: 'activate', outcome: 'activated',
1420
+ eventHeadDigest: events.at(-1).event_digest,
1421
+ controlHeadDigest: candidateEvents.at(-1).event_digest,
1422
+ activeEpoch: committed.pointer.plan_epoch });
1423
+ const restartResponse = buildControlResponse(controlRequest, 'completed', restartResult,
1424
+ candidateEvents.at(-1).event_digest);
1425
+ await replaceCanonicalJsonAtomically(candidateDir, 'activation-response.json', restartResponse);
1426
+ const activePointer = {
1427
+ schema: 'lattice.runtime_active_pointer.v1', run_id: request.request_id,
1428
+ candidate_ref: path.relative(runDir, candidateDir),
1429
+ activation_request_id: controlRequest.request_id,
1430
+ activation_request_digest: controlRequest.request_digest,
1431
+ activation_intent_digest: controlIntentDigest(controlRequest),
1432
+ activation_response_digest: restartResponse.response_digest,
1433
+ descriptor_digest: activation.supervisorDescriptor.descriptor_digest,
1434
+ session_nonce_digest: digestArtifact(sessionNonce),
1435
+ control_head_digest: candidateEvents.at(-1).event_digest,
1436
+ control_head_sequence: candidateEvents.at(-1).sequence,
1437
+ committed_at: canonicalNow(), pointer_digest: '',
1438
+ };
1439
+ activePointer.pointer_digest = selfDigest(activePointer, 'pointer_digest');
1440
+ await replaceCanonicalJsonAtomically(supervisorDir, 'active-runtime.json', activePointer);
1441
+ activationCommitted = true;
1442
+ if (typeof crashInjector === 'function') await crashInjector(
1443
+ 'after_active_runtime_pointer_commit', { request_id: controlRequest.request_id,
1444
+ pointer_digest: activePointer.pointer_digest });
1445
+ return restartResponse;
1446
+ }
1447
+ if (!restarting) activationCommitted = true;
1448
+ const result = buildControlResult({ operation: 'activate', outcome: 'activated',
1449
+ eventHeadDigest: events.at(-1).event_digest, controlHeadDigest: controlEvents.at(-1).event_digest,
1450
+ activeEpoch: committed.pointer.plan_epoch });
1451
+ return buildControlResponse(controlRequest, 'completed', result, controlEvents.at(-1).event_digest);
1452
+ }
1453
+ if (managedSupervisor === null) throw new ManagedRuntimeError('RUN_NOT_MANAGED', 'activate未完了');
1454
+ if (controlRequest.operation === 'finding_record') {
1455
+ const candidate = controlRequest.payload.artifact;
1456
+ if (!validateRuntimeFindingCandidate(candidate)
1457
+ || digestArtifact(candidate) !== controlRequest.payload.artifact_digest) {
1458
+ throw new ManagedRuntimeError('FINDING_UNRESOLVED', 'finding candidate/digest binding不正');
1459
+ }
1460
+ const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
1461
+ const active = await readCommittedEpochStore(runDir);
1462
+ const checkpointDigest = controlRequest.payload.checkpoint_digest;
1463
+ const observed = events.findLast((event) => event.plan_epoch === controlRequest.payload.expected_epoch
1464
+ && event.payload?.checkpoint_digest === checkpointDigest);
1465
+ if (observed === undefined) {
1466
+ if (events.some((event) => event.payload?.checkpoint_digest === checkpointDigest)) {
1467
+ throw new ManagedRuntimeError('STALE_FINDING', 'checkpointは旧epochに属する');
1468
+ }
1469
+ throw new ManagedRuntimeError('FINDING_UNRESOLVED', '保存checkpointをactive event prefixから解決できない');
1470
+ }
1471
+ const activeRequestV1 = active.bundle.request.schema === 'lattice.run_request.v1'
1472
+ ? structuredClone(active.bundle.request) : {
1473
+ schema: 'lattice.run_request.v1', request_id: active.bundle.request.request_id,
1474
+ repo: active.bundle.request.repo, capacity: active.bundle.request.capacity,
1475
+ todos: active.bundle.request.todos, manual_witness: active.bundle.request.manual_witness,
1476
+ sensor_query_set: active.bundle.request.sensor_query_set,
1477
+ executor_capability: active.bundle.request.executor_capability,
1478
+ claim_mode: active.bundle.request.claim_mode, request_digest: '',
1479
+ };
1480
+ activeRequestV1.request_digest = selfDigest(activeRequestV1, 'request_digest');
1481
+ const fresh = await compileFromRepo({ request: activeRequestV1, cwd: repoRoot,
1482
+ planRef: active.bundle.plan.plan_ref, planEpoch: active.pointer.plan_epoch,
1483
+ predecessorRefs: active.bundle.plan.predecessor_refs });
1484
+ if (fresh.outcome !== 'dispatchable'
1485
+ || canonicalizeArtifact(fresh.manifests) !== canonicalizeArtifact(active.bundle.manifests)
1486
+ || canonicalizeArtifact(fresh.plan.nodes.map(({ todo_id: id }) => id))
1487
+ !== canonicalizeArtifact(active.bundle.plan.nodes.map(({ todo_id: id }) => id))) {
1488
+ throw new ManagedRuntimeError('STALE_FINDING', 'current compile/sensorがactive epochから変化した');
1489
+ }
1490
+ const checkpointEntries = Array.isArray(observed.payload?.diff?.entries)
1491
+ ? observed.payload.diff.entries : [];
1492
+ const durableEvidence = new Set([checkpointDigest, observed.event_digest]);
1493
+ for (const entry of checkpointEntries) {
1494
+ if (/^[0-9a-f]{64}$/u.test(entry?.content_digest ?? '')) durableEvidence.add(entry.content_digest);
1495
+ }
1496
+ for (const manifest of Object.values(fresh.manifests)) {
1497
+ for (const graph of manifest.graph_evidence) {
1498
+ if (/^[0-9a-f]{64}$/u.test(graph?.result_digest ?? '')) durableEvidence.add(graph.result_digest);
1499
+ }
1500
+ }
1501
+ let derivedTodoIds;
1502
+ let derivedKind;
1503
+ const observationSet = events.filter((event) => event.plan_epoch === active.pointer.plan_epoch
1504
+ && event.kind === 'checkpoint_observed' && Array.isArray(event.payload?.diff?.entries)
1505
+ && event.subject?.kind === 'todo').map((event) => ({ todo_id: event.subject.ref,
1506
+ paths: event.payload.diff.entries.map((entry) => entry.path) }));
1507
+ const independentlyClassified = classifyObservedDiff({ plan: active.bundle.plan,
1508
+ manifests: fresh.manifests, observations: observationSet }).findings;
1509
+ if (candidate.path !== null) {
1510
+ const producer = detectCheckpointFindings({ todoId: observed.subject.ref,
1511
+ checkpoint: observed.payload, packets: active.bundle.executor_packets,
1512
+ runningTodoIds: projectRuntimeState({ events }).running }).findings;
1513
+ const match = producer.find((findingValue) => findingValue.kind === candidate.proposed_kind
1514
+ && findingValue.path === candidate.path
1515
+ && canonicalizeArtifact(findingValue.todo_ids) === canonicalizeArtifact(candidate.todo_ids));
1516
+ const verified = independentlyClassified.find((findingValue) => findingValue.kind === candidate.proposed_kind
1517
+ && findingValue.path === candidate.path
1518
+ && canonicalizeArtifact(findingValue.todo_ids) === canonicalizeArtifact(candidate.todo_ids));
1519
+ if (match === undefined || verified === undefined) throw new ManagedRuntimeError(
1520
+ 'FINDING_UNRESOLVED', 'path findingのproducer/verifier再導出が一致しない');
1521
+ derivedTodoIds = [...match.todo_ids];
1522
+ derivedKind = match.kind;
1523
+ } else {
1524
+ const match = independentlyClassified.find((findingValue) => findingValue.kind === candidate.proposed_kind
1525
+ && findingValue.resource_id === candidate.resource_id
1526
+ && canonicalizeArtifact(findingValue.todo_ids) === canonicalizeArtifact(candidate.todo_ids));
1527
+ if (match === undefined) throw new ManagedRuntimeError('FINDING_UNRESOLVED',
1528
+ 'resource findingをindependent checkpoint classifierから再導出できない');
1529
+ derivedTodoIds = [...match.todo_ids];
1530
+ derivedKind = match.kind;
1531
+ }
1532
+ if (derivedTodoIds.length === 0
1533
+ || derivedKind !== candidate.proposed_kind
1534
+ || canonicalizeArtifact(derivedTodoIds) !== canonicalizeArtifact(candidate.todo_ids)) {
1535
+ throw new ManagedRuntimeError('FINDING_UNRESOLVED', 'candidate todo集合をcheckpoint/current manifestsから再導出できない');
1536
+ }
1537
+ if (!candidate.evidence_digests.every((digest) => durableEvidence.has(digest))) {
1538
+ throw new ManagedRuntimeError('FINDING_UNRESOLVED', 'candidate evidenceをdurable checkpoint/sensor artifactへ解決できない');
1539
+ }
1540
+ const observer = { schema: 'lattice.runtime_observer_identity.v1', kind: 'supervisor',
1541
+ controller_registration_digest: activation.registration.registration_digest,
1542
+ executor_handle: null, identity_digest: '' };
1543
+ observer.identity_digest = selfDigest(observer, 'identity_digest');
1544
+ const finding = await recordRuntimeFinding({ runDir, candidate, checkpointDigest,
1545
+ observedEventDigest: observed.event_digest, recordedBy: observer,
1546
+ // producerとは別のcallbackでも同一active bundle/checkpoint bindingを再検査する。
1547
+ deriveFinding: (_untrusted, evidence) => {
1548
+ return { schema: 'lattice.runtime_conflict_finding.v1',
1549
+ kind: derivedKind, todo_ids: derivedTodoIds,
1550
+ path: candidate.path, resource_id: candidate.resource_id,
1551
+ evidence_digests: [...new Set([checkpointDigest,
1552
+ ...candidate.evidence_digests])].sort(), finding_digest: '' };
1553
+ },
1554
+ verifyFinding: (derived, evidence) => derived.kind === candidate.proposed_kind
1555
+ && derived.finding_digest === selfDigest(derived, 'finding_digest')
1556
+ && evidence.checkpoint.event_digest === observed.event_digest
1557
+ && canonicalizeArtifact(evidence.bundle.manifests)
1558
+ === canonicalizeArtifact(fresh.manifests)
1559
+ && derived.todo_ids.every((id) => Object.hasOwn(fresh.manifests, id)) });
1560
+ if (projectRuntimeState({ events }).freeze !== null) {
1561
+ let queue = await readBoundedJson(path.join(runDir, 'queued-events.json'),
1562
+ 'runtime queue').catch((error) => {
1563
+ if (error instanceof CliContractError && error.code === 'INPUT_UNREADABLE') return null;
1564
+ throw error;
1565
+ });
1566
+ if (queue === null) queue = { schema: 'lattice.runtime_queue.v1',
1567
+ run_id: request.request_id, frozen_epoch: controlRequest.payload.expected_epoch,
1568
+ entries: [], queue_digest: '' };
1569
+ else if (!validateRuntimeQueue(queue, request.request_id,
1570
+ controlRequest.payload.expected_epoch)) {
1571
+ throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED', 'finding queue binding不正');
1572
+ }
1573
+ queue.entries.push({ sequence: queue.entries.length + 1, kind: 'finding',
1574
+ subject_digest: candidate.candidate_digest,
1575
+ artifact_digest: finding.finding_digest });
1576
+ queue.queue_digest = selfDigest(queue, 'queue_digest');
1577
+ await replaceCanonicalJsonAtomically(runDir, 'queued-events.json', queue);
1578
+ }
1579
+ const result = buildControlResult({ operation: 'finding_record', outcome: 'recorded',
1580
+ eventHeadDigest: events.at(-1).event_digest,
1581
+ controlHeadDigest: controlEvents.at(-1).event_digest,
1582
+ activeEpoch: controlRequest.payload.expected_epoch,
1583
+ stagedEpoch: null });
1584
+ result.finding_digest = finding.finding_digest;
1585
+ const unsigned = { ...result }; delete unsigned.result_digest;
1586
+ result.result_digest = digestArtifact(unsigned);
1587
+ return buildControlResponse(controlRequest, 'completed', result,
1588
+ controlEvents.at(-1).event_digest);
1589
+ }
1590
+ if (controlRequest.operation === 'conflict') {
1591
+ const findingDigest = controlRequest.payload.artifact_digest;
1592
+ const finding = await readBoundedJson(path.join(runDir, 'findings', `${findingDigest}.json`), 'runtime finding');
1593
+ const active = await readCommittedEpochStore(runDir);
1594
+ if (!validateRuntimeFindingRecord(finding)
1595
+ || finding.finding_digest !== findingDigest
1596
+ || finding.run_id !== request.request_id) {
1597
+ throw new ManagedRuntimeError('FINDING_UNRESOLVED', 'finding binding不正');
1598
+ }
1599
+ if (finding.plan_epoch !== active.pointer.plan_epoch) {
1600
+ throw new ManagedRuntimeError('STALE_FINDING', 'findingはactive epochに属さない');
1601
+ }
1602
+ let events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
1603
+ const todoId = finding.finding?.todo_ids?.[0] ?? active.bundle.plan.nodes[0].todo_id;
1604
+ events.push(buildNextRunEvent({ events, runId: request.request_id, kind: 'conflict_found', planEpoch: active.pointer.plan_epoch,
1605
+ subject: { kind: 'todo', ref: todoId }, payload: {
1606
+ ...finding.finding, finding_digest: findingDigest, reported_by: 'lattice-supervisor',
1607
+ }, recordedAt: canonicalNow() }));
1608
+ events.push(buildNextRunEvent({ events, runId: request.request_id, kind: 'intake_frozen', planEpoch: active.pointer.plan_epoch,
1609
+ subject: { kind: 'runtime_plan', ref: active.bundle.plan.plan_ref },
1610
+ payload: { frozen_prefix_digest: digestArtifact(events.map(({ event_digest: value }) => value)), reason_kind: finding.finding.kind }, recordedAt: canonicalNow() }));
1611
+ await replaceEventsAtomically(runDir, events);
1612
+ const result = buildControlResult({ operation: 'conflict', outcome: 'frozen',
1613
+ eventHeadDigest: events.at(-1).event_digest, controlHeadDigest: controlEvents.at(-1).event_digest,
1614
+ activeEpoch: active.pointer.plan_epoch });
1615
+ return buildControlResponse(controlRequest, 'completed', result, controlEvents.at(-1).event_digest);
1616
+ }
1617
+ if (controlRequest.operation === 'hold') {
1618
+ let events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
1619
+ const conflict = events.findLast((event) => event.kind === 'conflict_found');
1620
+ if (!conflict) throw new ManagedRuntimeError('FINDING_UNRESOLVED', '保存済みconflictなし');
1621
+ const holdBarrierId = `barrier-${randomUUID()}`;
1622
+ const holdRecordedAt = canonicalNow();
1623
+ await appendControl({ run_id: request.request_id, kind: 'hold_prepared',
1624
+ session_nonce_digest: digestArtifact(sessionNonce), payload: {
1625
+ request_id: controlRequest.request_id,
1626
+ logical_intent_digest: controlIntentDigest(controlRequest),
1627
+ finding_digest: conflict.payload.finding_digest,
1628
+ barrier_id: holdBarrierId, recorded_at: holdRecordedAt,
1629
+ } });
1630
+ const held = await managedSupervisor.holdConflict({
1631
+ findingDigest: conflict.payload.finding_digest, frozenEventDigest: events.at(-1).event_digest,
1632
+ barrierId: holdBarrierId, reason: conflict.payload.kind,
1633
+ recordedAt: holdRecordedAt,
1634
+ });
1635
+ controlEvents = await eventStore.readEvents();
1636
+ const quiesced = controlEvents.filter((event) => event.kind === 'executor_quiesced'
1637
+ && held.quiescence_ack_digests.includes(event.payload?.ack?.ack_digest));
1638
+ for (const evidence of quiesced) {
1639
+ const checkpoint = evidence.payload.direct_observation.checkpoint;
1640
+ events.push(buildNextRunEvent({ events, runId: request.request_id,
1641
+ kind: 'checkpoint_observed', planEpoch: controlRequest.payload.expected_epoch,
1642
+ subject: { kind: 'todo', ref: evidence.payload.todo_id },
1643
+ payload: { ...structuredClone(checkpoint), barrier_final: true,
1644
+ barrier_evidence_digest: evidence.payload.evidence_digest },
1645
+ recordedAt: holdRecordedAt }));
1646
+ }
1647
+ const active = await readCommittedEpochStore(runDir);
1648
+ const decided = decideHoldAndCarryOver({ runId: request.request_id,
1649
+ request: active.bundle.request, plan: active.bundle.plan,
1650
+ manifests: active.bundle.manifests, packets: active.bundle.executor_packets,
1651
+ events, recordedAt: holdRecordedAt });
1652
+ events = decided.events;
1653
+ await replaceEventsAtomically(runDir, events);
1654
+ if (typeof crashInjector === 'function') await crashInjector('after_hold_effect', {
1655
+ request_id: controlRequest.request_id, hold_result_digest: held.result_digest,
1656
+ });
1657
+ await replaceNamedJsonAtomically(runDir, 'hold-result.json', held);
1658
+ const operationReceipt = {
1659
+ schema: 'lattice.runtime_control_operation_receipt.v1',
1660
+ request_id: controlRequest.request_id,
1661
+ logical_intent_digest: controlIntentDigest(controlRequest),
1662
+ operation: 'hold', outcome: 'held', effect_digest: held.result_digest,
1663
+ receipt_digest: '',
1664
+ };
1665
+ operationReceipt.receipt_digest = selfDigest(operationReceipt, 'receipt_digest');
1666
+ await replaceCanonicalJsonAtomically(runDir, 'hold-operation-receipt.json', operationReceipt);
1667
+ const result = buildControlResult({ operation: 'hold', outcome: 'held',
1668
+ eventHeadDigest: events.at(-1).event_digest, controlHeadDigest: controlEvents.at(-1).event_digest,
1669
+ activeEpoch: 1 });
1670
+ return buildControlResponse(controlRequest, 'completed', result, controlEvents.at(-1).event_digest);
1671
+ }
1672
+ if (controlRequest.operation === 'recompile') {
1673
+ const recompileRequest = controlRequest.payload.artifact;
1674
+ if (digestArtifact(recompileRequest) !== controlRequest.payload.artifact_digest) {
1675
+ throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'recompile artifact digest不一致');
1676
+ }
1677
+ const active = await readCommittedEpochStore(runDir);
1678
+ let events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
1679
+ const freeze = events.findLast((event) => event.kind === 'intake_frozen');
1680
+ const holdEvent = events.findLast((event) => event.kind === 'hold_decided'
1681
+ && event.plan_epoch === active.pointer.plan_epoch);
1682
+ if (freeze === undefined || holdEvent === undefined
1683
+ || !validateRuntimeRecompileRequest(recompileRequest, {
1684
+ predecessorBundle: active.bundle, frozenEventDigest: freeze.event_digest,
1685
+ holdDecisionDigest: holdEvent.payload?.decision_digest, validatePhaseRevision,
1686
+ })) throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'frozen/hold/successor binding不正');
1687
+
1688
+ // front-endはv1入力を再利用するが、successor identityはv2 digestへ再封印する。
1689
+ const successorV1 = { schema: 'lattice.run_request.v1',
1690
+ request_id: recompileRequest.successor_request.request_id,
1691
+ repo: recompileRequest.successor_request.repo,
1692
+ capacity: recompileRequest.successor_request.capacity,
1693
+ todos: recompileRequest.successor_request.todos,
1694
+ manual_witness: recompileRequest.successor_request.manual_witness,
1695
+ sensor_query_set: recompileRequest.successor_request.sensor_query_set,
1696
+ executor_capability: recompileRequest.successor_request.executor_capability,
1697
+ claim_mode: recompileRequest.successor_request.claim_mode, request_digest: '' };
1698
+ successorV1.request_digest = selfDigest(successorV1, 'request_digest');
1699
+ const nextEpoch = active.pointer.plan_epoch + 1;
1700
+ const compiled = await compileFromRepo({ request: successorV1, cwd: repoRoot,
1701
+ planRef: `plan-${recompileRequest.run_id}-e${nextEpoch}`, planEpoch: nextEpoch,
1702
+ predecessorRefs: [active.bundle.plan.plan_ref] });
1703
+ if (compiled.outcome !== 'dispatchable') {
1704
+ throw new ManagedRuntimeError(compiled.code ?? 'SEAM_SPLIT_UNPROVEN', 'successor compileがdispatchableでない');
1705
+ }
1706
+ const newPlan = structuredClone(compiled.plan);
1707
+ newPlan.request_digest = recompileRequest.successor_request.request_digest;
1708
+ if (recompileRequest.mode === 'intentional_serial') {
1709
+ const serial = recompileRequest.intentional_serial;
1710
+ const key = `${serial.todo_ids.join('\0')}\0${serial.resource_id}`;
1711
+ const exists = newPlan.conflicts.some((entry) => `${entry.todo_ids.join('\0')}\0${entry.resource_id}` === key);
1712
+ if (!exists) newPlan.conflicts.push({ todo_ids: [...serial.todo_ids], resource_id: serial.resource_id });
1713
+ newPlan.conflicts.sort((left, right) => `${left.todo_ids.join('\0')}\0${left.resource_id}`
1714
+ .localeCompare(`${right.todo_ids.join('\0')}\0${right.resource_id}`));
1715
+ }
1716
+ newPlan.plan_digest = selfDigest(newPlan, 'plan_digest');
1717
+ const executorPackets = buildExecutorPackets({ plan: newPlan, manifests: compiled.manifests });
1718
+
1719
+ // 既存pure coreからcontext invalidationとcarry rebind packetだけを再利用する。
1720
+ const predecessorRequest = active.bundle.request.schema === 'lattice.run_request.v1'
1721
+ ? active.bundle.request : { ...active.bundle.request, schema: 'lattice.run_request.v1' };
1722
+ delete predecessorRequest.predecessor_request_digest;
1723
+ delete predecessorRequest.task_migration_digest;
1724
+ predecessorRequest.request_digest = selfDigest(predecessorRequest, 'request_digest');
1725
+ const corePlan = active.bundle.request.schema === 'lattice.run_request.v1'
1726
+ ? active.bundle.plan : { ...active.bundle.plan, request_digest: predecessorRequest.request_digest };
1727
+ corePlan.plan_digest = selfDigest(corePlan, 'plan_digest');
1728
+ const predecessorEvents = events;
1729
+ // event batch receiptより先に、outer reprocessが同じrecompile transactionを
1730
+ // 再構成できるbindingをdurable化する。
1731
+ const priorPending = await readBoundedJson(path.join(runDir, 'pending-recompile.json'),
1732
+ 'pending recompile').catch(() => null);
1733
+ const pendingRecompile = {
1734
+ schema: 'lattice.runtime_pending_recompile.v1',
1735
+ control_request_id: controlRequest.request_id,
1736
+ reprocess_request_id: priorPending?.reprocess_request_id ?? null,
1737
+ reprocess_queue_digest: priorPending?.reprocess_queue_digest ?? null,
1738
+ recovery_response_outcome: priorPending?.recovery_response_outcome ?? null,
1739
+ recompile_request: structuredClone(recompileRequest),
1740
+ predecessor_epoch: active.pointer.plan_epoch,
1741
+ frozen_event_digest: freeze.event_digest,
1742
+ hold_decision_digest: holdEvent.payload.decision_digest,
1743
+ pending_digest: '',
1744
+ };
1745
+ pendingRecompile.pending_digest = selfDigest(pendingRecompile, 'pending_digest');
1746
+ await replaceCanonicalJsonAtomically(runDir, 'pending-recompile.json', pendingRecompile);
1747
+ const core = recompileNextEpochPlan({ runId: active.meta.run_id,
1748
+ request: predecessorRequest, plan: corePlan, manifests: active.bundle.manifests,
1749
+ packets: active.bundle.executor_packets, events, holdDecision: holdEvent.payload,
1750
+ additionalConflicts: recompileRequest.mode === 'intentional_serial'
1751
+ ? [{ todo_ids: recompileRequest.intentional_serial.todo_ids,
1752
+ resource_id: recompileRequest.intentional_serial.resource_id }] : [],
1753
+ recordedAt: canonicalNow() });
1754
+ const proposedCoreBatch = [];
1755
+ let proposedCoreEvents = [...predecessorEvents];
1756
+ for (const proposed of core.events.slice(predecessorEvents.length)) {
1757
+ const payload = proposed.kind === 'plan_recompiled'
1758
+ ? { ...proposed.payload, new_plan_digest: newPlan.plan_digest }
1759
+ : proposed.payload;
1760
+ const event = buildNextRunEvent({ events: proposedCoreEvents, runId: active.meta.run_id,
1761
+ kind: proposed.kind, planEpoch: proposed.plan_epoch,
1762
+ subject: proposed.kind === 'plan_recompiled'
1763
+ ? { kind: 'runtime_plan', ref: newPlan.plan_ref } : proposed.subject,
1764
+ payload, recordedAt: proposed.recorded_at });
1765
+ proposedCoreEvents.push(event);
1766
+ proposedCoreBatch.push(event);
1767
+ }
1768
+ events = [...predecessorEvents];
1769
+ const planDiff = { ...core.planDiff, new_plan_ref: newPlan.plan_ref };
1770
+ planDiff.diff_digest = selfDigest(planDiff, 'diff_digest');
1771
+ const rebindPackets = Object.fromEntries(Object.entries(core.rebindPackets)
1772
+ .filter(([todoId]) => Object.hasOwn(executorPackets, todoId)));
1773
+ const bundleBody = { schema: 'lattice.runtime_epoch_bundle.v1', run_id: active.meta.run_id,
1774
+ plan_epoch: nextEpoch, request: recompileRequest.successor_request, plan: newPlan,
1775
+ manifests: compiled.manifests, executor_packets: executorPackets,
1776
+ rebind_packets: rebindPackets, plan_diff: planDiff,
1777
+ task_migration: recompileRequest.task_migration,
1778
+ treatment: recompileRequest.mode === 'seam_split'
1779
+ ? recompileRequest.seam_split : recompileRequest.intentional_serial,
1780
+ phase_revision_digest: null, phase_revision_commit_receipt: null,
1781
+ predecessor_bundle_digest: active.bundle.bundle_digest };
1782
+ bundleBody.bundle_digest = digestArtifact(bundleBody);
1783
+ const treatment = recompileRequest.mode === 'seam_split'
1784
+ ? recompileRequest.seam_split : recompileRequest.intentional_serial;
1785
+ const treatmentFinding = await readBoundedJson(path.join(runDir, 'findings',
1786
+ `${treatment.finding_digest}.json`), 'runtime finding');
1787
+ if (!validateRuntimeFindingRecord(treatmentFinding)
1788
+ || treatmentFinding.run_id !== active.meta.run_id
1789
+ || treatmentFinding.plan_epoch !== active.pointer.plan_epoch
1790
+ || treatmentFinding.finding_digest !== treatment.finding_digest) {
1791
+ throw new ManagedRuntimeError(treatmentFinding?.plan_epoch !== active.pointer.plan_epoch
1792
+ ? 'STALE_FINDING' : 'FINDING_UNRESOLVED', 'treatment findingがactive epochへbindしない');
1793
+ }
1794
+ if (treatment.finding_digest !== holdEvent.payload?.finding?.finding_digest
1795
+ || canonicalizeArtifact(treatmentFinding.finding.todo_ids)
1796
+ !== canonicalizeArtifact(treatment.todo_ids ?? treatment.predecessor_task_ids)) {
1797
+ throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'treatmentがhold finding/todo集合と一致しない');
1798
+ }
1799
+ if (recompileRequest.mode === 'intentional_serial'
1800
+ && treatmentFinding.finding.resource_id !== treatment.resource_id) {
1801
+ throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'serial resourceがfinding resourceと一致しない');
1802
+ }
1803
+ if (recompileRequest.mode === 'intentional_serial'
1804
+ && !treatment.todo_ids.every((todoId) => {
1805
+ const manifest = active.bundle.manifests[todoId];
1806
+ return manifest?.resources?.includes(treatment.resource_id)
1807
+ || manifest?.state_effects?.some((effect) => effect.resource_id === treatment.resource_id);
1808
+ })) {
1809
+ throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'serial resourceをfresh ownershipから再導出できない');
1810
+ }
1811
+ if (recompileRequest.mode === 'seam_split') {
1812
+ const ownership = (manifests) => Object.entries(manifests).flatMap(([todoId, manifest]) => [
1813
+ ...manifest.resources.map((resourceId) => ({ resource_id: resourceId,
1814
+ owner_todo_id: todoId, access_kind: 'own' })),
1815
+ ...manifest.state_effects.map((effect) => ({ resource_id: effect.resource_id,
1816
+ owner_todo_id: todoId, access_kind: 'write' })),
1817
+ ]).sort((left, right) => canonicalizeArtifact(left).localeCompare(canonicalizeArtifact(right)));
1818
+ const edges = (planValue) => [
1819
+ ...planValue.precedence.map((edge) => ({ from_todo_id: edge.from_todo_id,
1820
+ to_todo_id: edge.to_todo_id, kind: 'hard_dependency' })),
1821
+ ...planValue.conflicts.map((edge) => ({ from_todo_id: edge.todo_ids[0],
1822
+ to_todo_id: edge.todo_ids[1], kind: 'conflict' })),
1823
+ ].sort((left, right) => canonicalizeArtifact(left).localeCompare(canonicalizeArtifact(right)));
1824
+ const difference = (left, right) => left.filter((entry) => !right.some((other) =>
1825
+ canonicalizeArtifact(entry) === canonicalizeArtifact(other)));
1826
+ const beforeOwnership = ownership(active.bundle.manifests);
1827
+ const afterOwnership = ownership(compiled.manifests);
1828
+ const beforeEdges = edges(active.bundle.plan);
1829
+ const afterEdges = edges(newPlan);
1830
+ const derivedOwnership = { added: difference(afterOwnership, beforeOwnership),
1831
+ removed: difference(beforeOwnership, afterOwnership) };
1832
+ const derivedEdges = { added: difference(afterEdges, beforeEdges),
1833
+ removed: difference(beforeEdges, afterEdges) };
1834
+ if (canonicalizeArtifact(derivedOwnership.added)
1835
+ !== canonicalizeArtifact(treatment.ownership_diff.added)
1836
+ || canonicalizeArtifact(derivedOwnership.removed)
1837
+ !== canonicalizeArtifact(treatment.ownership_diff.removed)
1838
+ || canonicalizeArtifact(derivedEdges.added)
1839
+ !== canonicalizeArtifact(treatment.edge_diff.added)
1840
+ || canonicalizeArtifact(derivedEdges.removed)
1841
+ !== canonicalizeArtifact(treatment.edge_diff.removed)) {
1842
+ throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST',
1843
+ 'seam ownership/edge diffをfresh predecessor/successorから再導出できない');
1844
+ }
1845
+ const findingPath = treatmentFinding.finding.path;
1846
+ if (findingPath !== null) {
1847
+ const coversPath = (manifest) => manifest.writes.some((declared) =>
1848
+ declared === findingPath || (declared.endsWith('/') && findingPath.startsWith(declared)));
1849
+ const overlappingBefore = Object.values(active.bundle.manifests)
1850
+ .filter(coversPath).length;
1851
+ const overlappingAfter = Object.values(compiled.manifests)
1852
+ .filter(coversPath).length;
1853
+ if (overlappingAfter > 1) {
1854
+ throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST',
1855
+ 'seam successorでfinding pathがsingle ownerへ収束していない');
1856
+ }
1857
+ } else {
1858
+ const findingResource = treatmentFinding.finding.resource_id;
1859
+ const resourceOwners = (manifests) => Object.values(manifests).filter((manifest) =>
1860
+ manifest.resources.includes(findingResource)
1861
+ || manifest.state_effects.some((effect) => effect.resource_id === findingResource)).length;
1862
+ if (resourceOwners(compiled.manifests) > 1) {
1863
+ throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST',
1864
+ 'seam successorでresourceがsingle ownerへ収束していない');
1865
+ }
1866
+ }
1867
+ const findingConflictResource = treatmentFinding.finding.resource_id ?? treatmentFinding.finding.path;
1868
+ if (newPlan.conflicts.some((conflict) => conflict.resource_id === findingConflictResource
1869
+ && treatmentFinding.finding.todo_ids.every((todoId) => conflict.todo_ids.includes(todoId)))) {
1870
+ throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST',
1871
+ 'seam successorにfinding conflict edgeが残存している');
1872
+ }
1873
+ }
1874
+ const validateCompiledSuccessor = (candidate, context) => validateRuntimeEpochBundle(candidate)
1875
+ && candidate.predecessor_bundle_digest === context.predecessor.bundle_digest
1876
+ && candidate.plan_epoch === context.pointer.plan_epoch + 1
1877
+ && candidate.request.request_digest === recompileRequest.successor_request.request_digest
1878
+ && candidate.task_migration.migration_digest === recompileRequest.task_migration.migration_digest
1879
+ && canonicalizeArtifact(candidate.treatment) === canonicalizeArtifact(treatment)
1880
+ && canonicalizeArtifact(candidate.plan) === canonicalizeArtifact(newPlan)
1881
+ && canonicalizeArtifact(candidate.manifests) === canonicalizeArtifact(compiled.manifests)
1882
+ && canonicalizeArtifact(candidate.executor_packets) === canonicalizeArtifact(executorPackets)
1883
+ && (recompileRequest.mode !== 'intentional_serial'
1884
+ || candidate.plan.conflicts.some((entry) => canonicalizeArtifact(entry.todo_ids)
1885
+ === canonicalizeArtifact(recompileRequest.intentional_serial.todo_ids)
1886
+ && entry.resource_id === recompileRequest.intentional_serial.resource_id));
1887
+ const staged = await stageSuccessorEpoch({ runDir,
1888
+ transactionId: recompileRequest.request_id, bundle: bundleBody, recompileRequest,
1889
+ validateSuccessor: validateCompiledSuccessor, validatePhaseRevision, commitPhaseRevision });
1890
+ if (typeof crashInjector === 'function') await crashInjector('after_successor_stage', {
1891
+ request_id: controlRequest.request_id, bundle_digest: staged.bundle_digest,
1892
+ });
1893
+ const queue = await readBoundedJson(path.join(runDir, 'queued-events.json'),
1894
+ 'runtime queue').catch((error) => {
1895
+ if (error instanceof CliContractError && error.code === 'INPUT_UNREADABLE') return null;
1896
+ throw error;
1897
+ });
1898
+ let queuedReplayCommit = null;
1899
+ if (queue !== null) {
1900
+ if (!validateRuntimeQueue(queue, active.meta.run_id, active.pointer.plan_epoch)
1901
+ || queue.queue_digest !== controlRequest.payload.expected_queue_digest) {
1902
+ throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED', 'queued event prefix binding不正');
1903
+ }
1904
+ const head = queue.entries[0];
1905
+ if (head !== undefined && (!['finding', 'conflict_found'].includes(head.kind)
1906
+ || head.artifact_digest !== treatment.finding_digest)) {
1907
+ throw new ManagedRuntimeError('QUEUED_REPLAY_REQUIRED', 'queue headを同一stagingへ順序通りreplayする必要がある');
1908
+ }
1909
+ const remaining = head === undefined ? [] : queue.entries.slice(1);
1910
+ if (remaining.length > 0) throw new ManagedRuntimeError('QUEUED_REPLAY_REQUIRED',
1911
+ 'successor activation前にqueue全entryの順序replayが必要');
1912
+ const cleared = { schema: 'lattice.runtime_queue.v1', run_id: active.meta.run_id,
1913
+ frozen_epoch: active.pointer.plan_epoch, entries: remaining, queue_digest: '' };
1914
+ cleared.queue_digest = selfDigest(cleared, 'queue_digest');
1915
+ queuedReplayCommit = cleared;
1916
+ }
1917
+
1918
+ // treatment/finding、fresh successor、stage、queue headを全検証した後にだけ
1919
+ // public run event batchをpublishする。
1920
+ events = await publishRuntimeEventBatch({ runDir,
1921
+ transactionId: recompileRequest.request_id, phase: 'recompile', planEpoch: nextEpoch,
1922
+ bindingDigest: digestArtifact({ successor_epoch: nextEpoch,
1923
+ successor_plan_digest: newPlan.plan_digest,
1924
+ recompile_request_digest: recompileRequest.request_digest }),
1925
+ currentEvents: predecessorEvents, proposedBatch: proposedCoreBatch, crashInjector });
1926
+
1927
+ const controllerActivations = [activation, ...additionalActivations];
1928
+ const sortedSuccessorIds = newPlan.nodes.map((node) => node.todo_id).sort();
1929
+ const controllerForTodo = (todoId) => controllerActivations[
1930
+ Math.max(0, sortedSuccessorIds.indexOf(todoId)) % controllerActivations.length];
1931
+ const nonceDigest = digestArtifact(sessionNonce);
1932
+ const issuedControlDigest = controlEvents.at(-1).event_digest;
1933
+ const activationTransaction = {
1934
+ schema: 'lattice.runtime_epoch_activation_transaction.v1',
1935
+ request_id: controlRequest.request_id,
1936
+ request_digest: controlRequest.request_digest,
1937
+ logical_intent_digest: controlIntentDigest(controlRequest),
1938
+ recompile_request_digest: recompileRequest.request_digest,
1939
+ predecessor_pointer_digest: active.pointer.pointer_digest,
1940
+ predecessor_bundle_digest: active.bundle.bundle_digest,
1941
+ successor_bundle_digest: staged.bundle_digest,
1942
+ successor_epoch: nextEpoch,
1943
+ queue_head_digest: controlRequest.payload.expected_queue_digest,
1944
+ queue_replay_commit: queuedReplayCommit,
1945
+ state: 'prepared', committed_pointer_digest: null, gate_digest: null,
1946
+ recovered_todo_ids: [],
1947
+ recovery_error: null,
1948
+ transaction_digest: '',
1949
+ };
1950
+ activationTransaction.transaction_digest = selfDigest(activationTransaction, 'transaction_digest');
1951
+ await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', activationTransaction);
1952
+ const stagedLease = (todoId, packetDigest, owner) => {
1953
+ const lease = { schema: 'lattice.runtime_write_lease.v1',
1954
+ lease_id: `lease-${recompileRequest.request_id}-${todoId}`, run_id: active.meta.run_id,
1955
+ todo_id: todoId, plan_epoch: nextEpoch, packet_digest: packetDigest,
1956
+ controller_registration_digest: owner.registration.registration_digest,
1957
+ supervisor_session_nonce_digest: nonceDigest, state: 'staged',
1958
+ ttl_ms: owner.controllerDescriptor.heartbeat.ttl_ms,
1959
+ issued_control_digest: issuedControlDigest, lease_digest: '' };
1960
+ lease.lease_digest = selfDigest(lease, 'lease_digest');
1961
+ return lease;
1962
+ };
1963
+ const rebound = new Set(Object.keys(rebindPackets));
1964
+ const rebindEvidence = new Map();
1965
+ const preparedSuccessors = new Set();
1966
+ for (const [todoId, packet] of Object.entries(rebindPackets)) {
1967
+ const owner = controllerForTodo(todoId);
1968
+ const dispatch = projectRuntimeState({ events }).dispatches[todoId];
1969
+ const evidence = await managedSupervisor.rebindController({ controllerId: owner.controllerDescriptor.controller_id, rebindPacket: packet,
1970
+ stagedLease: stagedLease(todoId, packet.packet_digest, owner), expected: { todo_id: todoId,
1971
+ executor_handle: dispatch.payload.executor_handle, worktree_id: dispatch.payload.worktree_id,
1972
+ predecessor_packet_digest: active.bundle.executor_packets[todoId].packet_digest,
1973
+ rebind_packet_digest: packet.packet_digest } });
1974
+ rebindEvidence.set(todoId, { ...evidence,
1975
+ controller_registration_digest: owner.registration.registration_digest });
1976
+ }
1977
+ for (const todoId of core.planDiff.redispatched) {
1978
+ const successors = recompileRequest.task_migration.entries
1979
+ .find((entry) => entry.predecessor_task_id === todoId)?.successor_task_ids ?? [];
1980
+ for (const successorId of successors) {
1981
+ if (rebound.has(successorId)) continue;
1982
+ const packet = executorPackets[successorId];
1983
+ const owner = controllerForTodo(successorId);
1984
+ if (packet !== undefined) await managedSupervisor.prepareController({
1985
+ controllerId: owner.controllerDescriptor.controller_id,
1986
+ executorPacket: packet, stagedLease: stagedLease(successorId, packet.packet_digest, owner) });
1987
+ if (packet !== undefined) preparedSuccessors.add(successorId);
1988
+ }
1989
+ }
1990
+ const acceptedCheckpointDigests = new Set(planDiff.accepted_checkpoints);
1991
+ const acceptedTodoIds = new Set(projectRuntimeState({ events }).receipts
1992
+ .filter((receipt) => receipt.accepted_sequence !== null
1993
+ && acceptedCheckpointDigests.has(receipt.payload?.checkpoint_digest))
1994
+ .map((receipt) => receipt.todo_id));
1995
+ const expectedLiveTodoIds = newPlan.nodes.map((node) => node.todo_id)
1996
+ .filter((todoId) => !acceptedTodoIds.has(todoId)).sort();
1997
+ const activatedTodoIds = [...new Set([...rebound, ...preparedSuccessors])].sort();
1998
+ if (canonicalizeArtifact(expectedLiveTodoIds) !== canonicalizeArtifact(activatedTodoIds)) {
1999
+ throw new ManagedRuntimeError('EPOCH_REBIND_INCOMPLETE', 'successor live taskのrebind/prepare集合が完全でない');
2000
+ }
2001
+ // 全direct ack後だけepoch_reboundをexact-once batchで保存する。
2002
+ const reboundRecordedAt = canonicalNow();
2003
+ const proposedReboundBatch = [];
2004
+ let proposedReboundEvents = [...events];
2005
+ for (const [todoId, packet] of Object.entries(rebindPackets)
2006
+ .sort(([left], [right]) => left.localeCompare(right))) {
2007
+ const event = buildNextRunEvent({ events: proposedReboundEvents,
2008
+ runId: active.meta.run_id, kind: 'epoch_rebound', planEpoch: nextEpoch,
2009
+ subject: { kind: 'todo', ref: todoId }, payload: { ...packet,
2010
+ rebind_ack_digest: rebindEvidence.get(todoId).ack.ack_digest,
2011
+ control_event_digest: rebindEvidence.get(todoId).control_event_digest,
2012
+ controller_registration_digest: rebindEvidence.get(todoId).controller_registration_digest },
2013
+ recordedAt: reboundRecordedAt });
2014
+ proposedReboundEvents.push(event);
2015
+ proposedReboundBatch.push(event);
2016
+ }
2017
+ events = await publishRuntimeEventBatch({ runDir,
2018
+ transactionId: recompileRequest.request_id, phase: 'epoch_rebound', planEpoch: nextEpoch,
2019
+ bindingDigest: digestArtifact({ successor_epoch: nextEpoch,
2020
+ successor_bundle_digest: staged.bundle_digest,
2021
+ ordered_rebind_packet_digests: Object.entries(rebindPackets)
2022
+ .sort(([left], [right]) => left.localeCompare(right))
2023
+ .map(([, packet]) => packet.packet_digest) }),
2024
+ currentEvents: events, proposedBatch: proposedReboundBatch, crashInjector });
2025
+ controlEvents = await eventStore.readEvents();
2026
+ const committed = await commitStagedSuccessorEpoch({ runDir,
2027
+ transactionId: staged.transaction_id,
2028
+ activationRunEventDigest: events.at(-1).event_digest,
2029
+ activationControlEventDigest: controlEvents.at(-1).event_digest,
2030
+ readActivationControlEvents: () => eventStore.readEvents() });
2031
+ if (typeof crashInjector === 'function') await crashInjector('after_successor_pointer_rename', {
2032
+ request_id: controlRequest.request_id, pointer_digest: committed.pointer.pointer_digest,
2033
+ });
2034
+ activationTransaction.state = 'pointer_committed';
2035
+ activationTransaction.committed_pointer_digest = committed.pointer.pointer_digest;
2036
+ activationTransaction.transaction_digest = selfDigest(activationTransaction, 'transaction_digest');
2037
+ await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', activationTransaction);
2038
+ if (typeof crashInjector === 'function') await crashInjector('after_successor_pointer_commit', {
2039
+ request_id: controlRequest.request_id, pointer_digest: committed.pointer.pointer_digest,
2040
+ });
2041
+ const activationDigest = digestArtifact({ pointer_digest: committed.pointer.pointer_digest,
2042
+ staged_bundle_digest: staged.bundle_digest });
2043
+ const activated = await managedSupervisor.commitWriteGate({ planEpoch: nextEpoch,
2044
+ committedEpochDigest: committed.pointer.pointer_digest, activationDigest,
2045
+ commitReleaseBarrier: (barrier) => commitReleaseEpochBarrier({ runDir, barrier }),
2046
+ afterControllerRelease: async (released) => {
2047
+ if (released.release_index === 1 && typeof crashInjector === 'function') {
2048
+ await crashInjector('after_first_controller_release', released);
2049
+ }
2050
+ },
2051
+ committedAt: canonicalNow() });
2052
+ if (queuedReplayCommit !== null) {
2053
+ await replaceCanonicalJsonAtomically(runDir, 'queued-events.json', queuedReplayCommit);
2054
+ }
2055
+ activationTransaction.state = 'gate_committed';
2056
+ activationTransaction.gate_digest = activated.gate.gate_digest;
2057
+ activationTransaction.transaction_digest = selfDigest(activationTransaction, 'transaction_digest');
2058
+ await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', activationTransaction);
2059
+ events.push(buildNextRunEvent({ events, runId: active.meta.run_id, kind: 'intake_resumed',
2060
+ planEpoch: nextEpoch, subject: { kind: 'runtime_plan', ref: newPlan.plan_ref },
2061
+ payload: { plan_diff_digest: planDiff.diff_digest,
2062
+ write_gate_digest: activated.gate.gate_digest }, recordedAt: canonicalNow() }));
2063
+ await replaceEventsAtomically(runDir, events);
2064
+ if (typeof crashInjector === 'function') await crashInjector('after_successor_intake_resume', {
2065
+ request_id: controlRequest.request_id, gate_digest: activated.gate.gate_digest,
2066
+ event_digest: events.at(-1).event_digest,
2067
+ });
2068
+ const result = buildControlResult({ operation: 'recompile', outcome: 'recompiled',
2069
+ eventHeadDigest: events.at(-1).event_digest,
2070
+ controlHeadDigest: (await eventStore.readEvents()).at(-1).event_digest,
2071
+ activeEpoch: nextEpoch, stagedEpoch: null });
2072
+ return buildControlResponse(controlRequest, 'completed', result, result.control_head_digest);
2073
+ }
2074
+ if (controlRequest.operation === 'reprocess') {
2075
+ const active = await readCommittedEpochStore(runDir);
2076
+ const queue = await readBoundedJson(path.join(runDir, 'queued-events.json'), 'runtime queue');
2077
+ if (!validateRuntimeQueue(queue, active.meta.run_id, active.pointer.plan_epoch)
2078
+ || queue.queue_digest !== controlRequest.payload.expected_queue_digest) {
2079
+ throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED', 'reprocess queue binding不正');
2080
+ }
2081
+ const pending = await readBoundedJson(path.join(runDir, 'pending-recompile.json'),
2082
+ 'pending recompile').catch(() => null);
2083
+ if (pending?.schema !== 'lattice.runtime_pending_recompile.v1'
2084
+ || pending.pending_digest !== selfDigest(pending, 'pending_digest')
2085
+ || pending.predecessor_epoch !== active.pointer.plan_epoch) {
2086
+ throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED', 'same staging recompile binding不足');
2087
+ }
2088
+ const recompileRequest = pending.recompile_request;
2089
+ const treatment = recompileRequest.mode === 'seam_split'
2090
+ ? recompileRequest.seam_split : recompileRequest.intentional_serial;
2091
+ if (queue.entries.some((entry, index) => entry.sequence !== index + 1
2092
+ || !['finding', 'conflict_found'].includes(entry.kind)
2093
+ || entry.artifact_digest !== treatment.finding_digest)) {
2094
+ throw new ManagedRuntimeError('QUEUED_CONFLICT_REMAINS', 'queue headをpending treatmentで解決できない');
2095
+ }
2096
+ pending.reprocess_request_id = controlRequest.request_id;
2097
+ pending.reprocess_queue_digest = queue.queue_digest;
2098
+ pending.pending_digest = selfDigest(pending, 'pending_digest');
2099
+ await replaceCanonicalJsonAtomically(runDir, 'pending-recompile.json', pending);
2100
+ const resumeRequest = createRuntimeControlRequest({
2101
+ requestId: pending.control_request_id, runId: active.meta.run_id,
2102
+ operation: 'recompile', sessionNonce, payload: controlOperationPayload({
2103
+ operation: 'recompile', runRef: controlRequest.payload.run_ref,
2104
+ artifact: recompileRequest, artifactDigest: digestArtifact(recompileRequest),
2105
+ checkpointDigest: null, expectedEpoch: active.pointer.plan_epoch,
2106
+ expectedQueueDigest: queue.queue_digest, shutdownReason: null,
2107
+ }),
2108
+ });
2109
+ const resumed = await executeControl(resumeRequest);
2110
+ if (resumed.outcome !== 'completed') {
2111
+ throw new ManagedRuntimeError(resumed.result?.unmet?.[0] ?? 'RUN_RECOVERY_REQUIRED',
2112
+ resumed.result?.unmet?.[1] ?? 'same staging recompile再開失敗');
2113
+ }
2114
+ const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
2115
+ const nowActive = await readCommittedEpochStore(runDir);
2116
+ const resumedControlHead = (await eventStore.readEvents()).at(-1).event_digest;
2117
+ const result = buildControlResult({ operation: 'reprocess', outcome: 'reprocessed',
2118
+ eventHeadDigest: events.at(-1).event_digest,
2119
+ controlHeadDigest: resumedControlHead,
2120
+ activeEpoch: nowActive.pointer.plan_epoch, stagedEpoch: null });
2121
+ return buildControlResponse(controlRequest, 'completed', result,
2122
+ resumedControlHead);
2123
+ }
2124
+ if (['close', 'abandon'].includes(controlRequest.operation)) {
2125
+ let events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
2126
+ const active = await readCommittedEpochStore(runDir);
2127
+ const state = projectRuntimeState({ events });
2128
+ const shutdownReason = controlRequest.payload.shutdown_reason;
2129
+ if (typeof shutdownReason !== 'string' || shutdownReason.length === 0) {
2130
+ throw new ManagedRuntimeError('MANAGED_SHUTDOWN_INCOMPLETE', 'shutdown reason不足');
2131
+ }
2132
+ let proposed;
2133
+ if (controlRequest.operation === 'close') {
2134
+ proposed = closeRunIfComplete({ runId: request.request_id, plan: active.bundle.plan,
2135
+ events, recordedAt: canonicalNow() });
2136
+ if (!proposed.closed) throw new ManagedRuntimeError('RUN_NOT_COMPLETE', '全TODO完了前はcloseできない');
2137
+ }
2138
+ const shutdown = await managedSupervisor.shutdownManaged({ mode: controlRequest.operation,
2139
+ reason: shutdownReason, barrierId: `shutdown-${randomUUID()}`,
2140
+ frozenEventDigest: events.at(-1).event_digest, recordedAt: canonicalNow() });
2141
+ if (controlRequest.operation === 'close') events = proposed.events;
2142
+ else {
2143
+ events = [...events, buildNextRunEvent({ events, runId: request.request_id, kind: 'run_closed',
2144
+ planEpoch: active.pointer.plan_epoch,
2145
+ subject: { kind: 'runtime_plan', ref: active.bundle.plan.plan_ref },
2146
+ payload: { outcome: 'abandoned', reason: shutdownReason, accepted: state.accepted },
2147
+ recordedAt: canonicalNow() })];
2148
+ }
2149
+ await replaceEventsAtomically(runDir, events);
2150
+ await appendControl({ run_id: request.request_id, kind: 'supervisor_stopped',
2151
+ session_nonce_digest: digestArtifact(sessionNonce),
2152
+ payload: { shutdown_result_digest: shutdown.result_digest } });
2153
+ const result = buildControlResult({ operation: controlRequest.operation,
2154
+ outcome: controlRequest.operation === 'close' ? 'closed' : 'abandoned',
2155
+ eventHeadDigest: events.at(-1).event_digest,
2156
+ controlHeadDigest: controlEvents.at(-1).event_digest,
2157
+ activeEpoch: active.pointer.plan_epoch });
2158
+ setTimeout(() => {
2159
+ server?.close(() => {
2160
+ resolveActiveRuntimePaths({ runDir }).then((active) => Promise.all([
2161
+ rm(socketPath, { force: true }), rm(active.sessionPath, { force: true }),
2162
+ rm(path.join(supervisorDir, 'session'), { force: true }),
2163
+ ])).catch(() => {});
2164
+ });
2165
+ }, 200);
2166
+ return buildControlResponse(controlRequest, 'completed', result, controlEvents.at(-1).event_digest);
2167
+ }
2168
+ throw new ManagedRuntimeError('RUN_NOT_MANAGED', `未知のmanaged operation: ${controlRequest.operation}`);
2169
+ } catch (error) {
2170
+ const code = error?.code ?? 'ADAPTER_CONTROLLER_UNAVAILABLE';
2171
+ let events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events').catch(() => []);
2172
+ const result = buildControlResult({ operation: controlRequest.operation, outcome: 'rejected',
2173
+ eventHeadDigest: events.at(-1)?.event_digest ?? null,
2174
+ controlHeadDigest: controlEvents.at(-1)?.event_digest ?? null, activeEpoch: 1,
2175
+ unmet: [code, String(error?.message ?? error)] });
2176
+ if (controlRequest.operation === 'activate' && !activationCommitted) {
2177
+ setTimeout(() => {
2178
+ server?.close(() => {
2179
+ Promise.all([
2180
+ activation?.disposeController?.(),
2181
+ candidateDir ? rm(candidateDir, { recursive: true, force: true }) : Promise.resolve(),
2182
+ candidateControllerDir ? rm(candidateControllerDir, { recursive: true, force: true }) : Promise.resolve(),
2183
+ rm(socketPath, { force: true }),
2184
+ restarting ? Promise.resolve() : rm(path.join(runDir, 'control-request-ledger.json'), { force: true }),
2185
+ restarting ? Promise.resolve() : rm(supervisorDir, { recursive: true, force: true }),
2186
+ ]).catch(() => {});
2187
+ });
2188
+ }, 200);
2189
+ }
2190
+ return buildControlResponse(controlRequest, 'rejected', result, controlEvents.at(-1)?.event_digest ?? null);
2191
+ }
2192
+ };
2193
+ const handler = async (controlRequest) => {
2194
+ let known = await requestStore.readRequest(controlRequest);
2195
+ let sessionRecoveryResponse = null;
2196
+ if (known?.state === 'completed'
2197
+ && ['recompile', 'reprocess'].includes(controlRequest.operation)) {
2198
+ const recoveryEvents = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
2199
+ const recoveryCommitted = await readCommittedEpochStore(runDir);
2200
+ const expectedSuccessor = controlRequest.payload.expected_epoch + 1;
2201
+ if (recoveryCommitted?.pointer.plan_epoch === expectedSuccessor
2202
+ && !(await isActiveSupervisorGateCommitted(runDir, recoveryEvents, expectedSuccessor))) {
2203
+ sessionRecoveryResponse = known.response;
2204
+ known = { ...known, state: 'in_progress', response: null };
2205
+ }
2206
+ }
2207
+ if (known !== null && controlRequest.operation === 'activate') {
2208
+ const active = await resolveActiveRuntimePaths({ runDir }).catch(() => null);
2209
+ if (active?.pointer?.activation_request_id === controlRequest.request_id
2210
+ && active.pointer.activation_intent_digest === controlIntentDigest(controlRequest)) {
2211
+ const priorResponse = await readBoundedJson(path.join(path.dirname(active.descriptorPath),
2212
+ 'activation-response.json'), 'activation response');
2213
+ if (!validateRuntimeControlResponse(priorResponse, controlRequest.operation)
2214
+ || priorResponse.response_digest !== active.pointer.activation_response_digest) {
2215
+ throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED', 'committed activation response binding不正');
2216
+ }
2217
+ const currentActivation = active.pointer.activation_request_digest
2218
+ === controlRequest.request_digest
2219
+ && active.pointer.session_nonce_digest === digestArtifact(sessionNonce)
2220
+ && known.request_digest === controlRequest.request_digest
2221
+ && managedSupervisor !== null;
2222
+ if (currentActivation) {
2223
+ if (known.state === 'completed') {
2224
+ if (known.response.response_digest !== priorResponse.response_digest) {
2225
+ throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED',
2226
+ 'current activation ledger/pointer response binding不正');
2227
+ }
2228
+ return known.response;
2229
+ }
2230
+ return requestStore.replaceCompletedActivationRequest(controlRequest, priorResponse);
2231
+ }
2232
+ }
2233
+ let activationLock;
2234
+ try {
2235
+ activationLock = await acquireRuntimeLifecycleLock({ runDir,
2236
+ sessionNonceDigest: digestArtifact(sessionNonce), operation: controlRequest.operation,
2237
+ requestId: controlRequest.request_id, timeoutMs: 0 });
2238
+ const freshResponse = await executeControl(controlRequest);
2239
+ if (freshResponse.outcome !== 'completed') return freshResponse;
2240
+ return requestStore.replaceCompletedActivationRequest(controlRequest, freshResponse);
2241
+ } finally {
2242
+ await activationLock?.release();
2243
+ }
2244
+ }
2245
+ if (known?.state === 'completed') return known.response;
2246
+ if (known?.state === 'in_progress' && controlRequest.operation === 'reprocess'
2247
+ && sessionRecoveryResponse === null) {
2248
+ const recovered = await readBoundedJson(path.join(runDir,
2249
+ `reprocess-operation-response-${controlRequest.request_id}.json`),
2250
+ 'reprocess operation response').catch(() => null);
2251
+ if (validateRuntimeControlResponse(recovered, 'reprocess')
2252
+ && recovered.request_id === controlRequest.request_id) {
2253
+ return requestStore.completeRequest(controlRequest, recovered);
2254
+ }
2255
+ }
2256
+ const staleInProgress = known?.state === 'in_progress'
2257
+ && known.request_digest !== controlRequest.request_digest;
2258
+ if (controlRequest.run_id !== request.request_id
2259
+ || controlRequest.session_nonce !== sessionNonce || staleInProgress
2260
+ || sessionRecoveryResponse !== null) {
2261
+ let events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events').catch(() => []);
2262
+ const active = await resolveActiveRuntimePaths({ runDir }).catch(() => null);
2263
+ const journal = active === null ? []
2264
+ : await readBoundedJson(active.controlEventsPath, 'runtime control events').catch(() => []);
2265
+ let outcome = 'rejected';
2266
+ let operationOutcome = 'rejected';
2267
+ let unmet = ['RUN_RECOVERY_REQUIRED', '旧session requestはdurable side effectを導出できない'];
2268
+ if (known?.state === 'in_progress') {
2269
+ let holdReceipt = controlRequest.operation === 'hold'
2270
+ ? await readBoundedJson(path.join(runDir, 'hold-operation-receipt.json'),
2271
+ 'hold operation receipt').catch(() => null)
2272
+ : null;
2273
+ if (controlRequest.operation === 'hold' && holdReceipt === null) {
2274
+ const reconstructed = reconstructHoldResultFromJournal({ journal,
2275
+ runId: request.request_id, requestId: controlRequest.request_id,
2276
+ intentDigest: controlIntentDigest(controlRequest) });
2277
+ if (reconstructed !== null) {
2278
+ await replaceCanonicalJsonAtomically(runDir, 'hold-result.json', reconstructed);
2279
+ holdReceipt = {
2280
+ schema: 'lattice.runtime_control_operation_receipt.v1',
2281
+ request_id: controlRequest.request_id,
2282
+ logical_intent_digest: controlIntentDigest(controlRequest),
2283
+ operation: 'hold', outcome: 'held', effect_digest: reconstructed.result_digest,
2284
+ receipt_digest: '',
2285
+ };
2286
+ holdReceipt.receipt_digest = selfDigest(holdReceipt, 'receipt_digest');
2287
+ await replaceCanonicalJsonAtomically(runDir, 'hold-operation-receipt.json', holdReceipt);
2288
+ }
2289
+ }
2290
+ const held = holdReceipt?.schema === 'lattice.runtime_control_operation_receipt.v1'
2291
+ && holdReceipt.request_id === controlRequest.request_id
2292
+ && holdReceipt.logical_intent_digest === controlIntentDigest(controlRequest)
2293
+ && holdReceipt.operation === 'hold' && holdReceipt.outcome === 'held'
2294
+ && holdReceipt.receipt_digest === selfDigest(holdReceipt, 'receipt_digest');
2295
+ if (held && !events.some((event) => event.kind === 'hold_decided'
2296
+ && event.payload?.finding?.finding_digest
2297
+ === journal.find((event) => event.kind === 'hold_prepared'
2298
+ && event.payload.request_id === controlRequest.request_id)?.payload.finding_digest)) {
2299
+ const heldResult = await readBoundedJson(path.join(runDir, 'hold-result.json'),
2300
+ 'hold result');
2301
+ const evidenceEvents = journal.filter((event) => event.kind === 'executor_quiesced'
2302
+ && heldResult.quiescence_ack_digests.includes(event.payload?.ack?.ack_digest));
2303
+ for (const evidence of evidenceEvents) {
2304
+ if (events.some((event) => event.kind === 'checkpoint_observed'
2305
+ && event.payload?.barrier_evidence_digest === evidence.payload.evidence_digest)) continue;
2306
+ events.push(buildNextRunEvent({ events, runId: request.request_id,
2307
+ kind: 'checkpoint_observed', planEpoch: controlRequest.payload.expected_epoch,
2308
+ subject: { kind: 'todo', ref: evidence.payload.todo_id },
2309
+ payload: { ...structuredClone(evidence.payload.direct_observation.checkpoint),
2310
+ barrier_final: true, barrier_evidence_digest: evidence.payload.evidence_digest },
2311
+ recordedAt: canonicalNow() }));
2312
+ }
2313
+ const activeEpoch = await readCommittedEpochStore(runDir);
2314
+ const decided = decideHoldAndCarryOver({ runId: request.request_id,
2315
+ request: activeEpoch.bundle.request, plan: activeEpoch.bundle.plan,
2316
+ manifests: activeEpoch.bundle.manifests, packets: activeEpoch.bundle.executor_packets,
2317
+ events, recordedAt: canonicalNow() });
2318
+ events = decided.events;
2319
+ await replaceEventsAtomically(runDir, events);
2320
+ }
2321
+ const found = controlRequest.operation === 'conflict'
2322
+ && events.some((event) => event.kind === 'conflict_found'
2323
+ && event.payload?.finding_digest === controlRequest.payload.artifact_digest);
2324
+ let recordedFinding = null;
2325
+ if (controlRequest.operation === 'finding_record') {
2326
+ const names = await readdir(path.join(runDir, 'findings')).catch(() => []);
2327
+ for (const name of names.filter((entry) => /^[0-9a-f]{64}\.json$/u.test(entry))) {
2328
+ const candidate = await readBoundedJson(path.join(runDir, 'findings', name),
2329
+ 'runtime finding').catch(() => null);
2330
+ const input = controlRequest.payload.artifact;
2331
+ if (validateRuntimeFindingRecord(candidate)
2332
+ && candidate.source_checkpoint_digest === controlRequest.payload.checkpoint_digest
2333
+ && candidate.finding.kind === input.proposed_kind
2334
+ && canonicalizeArtifact(candidate.finding.todo_ids) === canonicalizeArtifact(input.todo_ids)
2335
+ && candidate.finding.path === input.path
2336
+ && candidate.finding.resource_id === input.resource_id
2337
+ && input.evidence_digests.every((digest) => candidate.finding.evidence_digests.includes(digest))
2338
+ && candidate.finding.evidence_digests.includes(controlRequest.payload.checkpoint_digest)) {
2339
+ recordedFinding = candidate; break;
2340
+ }
2341
+ }
2342
+ }
2343
+ let committed = await readCommittedEpochStore(runDir).catch(() => null);
2344
+ let prePointerRecovered = false;
2345
+ if (controlRequest.operation === 'recompile'
2346
+ && committed?.pointer.plan_epoch === controlRequest.payload.expected_epoch
2347
+ && controlRequest.payload.artifact?.predecessor_epoch === committed.pointer.plan_epoch) {
2348
+ const recoveredResponse = await executeControl(controlRequest);
2349
+ if (recoveredResponse.outcome === 'completed') {
2350
+ events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
2351
+ prePointerRecovered = true;
2352
+ }
2353
+ }
2354
+ let recompiled = controlRequest.operation === 'recompile'
2355
+ && (prePointerRecovered || (committed?.pointer.plan_epoch === controlRequest.payload.expected_epoch + 1
2356
+ && committed.bundle.request.request_digest
2357
+ === controlRequest.payload.artifact?.successor_request?.request_digest
2358
+ && !staleInProgress && managedSupervisor.frozen === false
2359
+ && !(await isManagedRunFrozen(runDir, events))));
2360
+ if (controlRequest.operation === 'recompile' && committed !== null
2361
+ && committed.pointer.plan_epoch === controlRequest.payload.expected_epoch + 1
2362
+ && committed.bundle.request.request_digest
2363
+ === controlRequest.payload.artifact?.successor_request?.request_digest
2364
+ && (staleInProgress || managedSupervisor.frozen
2365
+ || await isManagedRunFrozen(runDir, events))) {
2366
+ const transaction = await readBoundedJson(path.join(runDir,
2367
+ 'epoch-activation-transaction.json'), 'epoch activation transaction').catch(() => null);
2368
+ if (transaction?.schema !== 'lattice.runtime_epoch_activation_transaction.v1'
2369
+ || transaction.request_id !== controlRequest.request_id
2370
+ || transaction.logical_intent_digest !== controlIntentDigest(controlRequest)
2371
+ || transaction.successor_bundle_digest !== committed.bundle.bundle_digest
2372
+ || !([null, committed.pointer.pointer_digest].includes(transaction.committed_pointer_digest))
2373
+ || (transaction.committed_pointer_digest === null
2374
+ && (transaction.state !== 'prepared'
2375
+ || transaction.predecessor_bundle_digest !== committed.bundle.predecessor_bundle_digest))
2376
+ || transaction.transaction_digest !== selfDigest(transaction, 'transaction_digest')) {
2377
+ throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED', 'activation transaction binding不正');
2378
+ }
2379
+ if (transaction.committed_pointer_digest === null) {
2380
+ transaction.state = 'pointer_committed';
2381
+ transaction.committed_pointer_digest = committed.pointer.pointer_digest;
2382
+ transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
2383
+ await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
2384
+ }
2385
+ const recoveryControllers = [activation, ...additionalActivations];
2386
+ const recoveryTodoIds = committed.bundle.plan.nodes.map((node) => node.todo_id).sort();
2387
+ const recoveryControllerForTodo = (todoId) => recoveryControllers[
2388
+ Math.max(0, recoveryTodoIds.indexOf(todoId)) % recoveryControllers.length];
2389
+ const nonceDigest = digestArtifact(sessionNonce);
2390
+ const issuedControlDigest = journal.at(-1)?.event_digest;
2391
+ transaction.state = 'recovery_preparing';
2392
+ transaction.recovered_todo_ids = [];
2393
+ transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
2394
+ await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
2395
+ const recoveryLease = (todoId, packetDigest, owner) => {
2396
+ const lease = { schema: 'lattice.runtime_write_lease.v1',
2397
+ lease_id: `lease-recovery-${controlRequest.request_id}-${todoId}`,
2398
+ run_id: committed.meta.run_id, todo_id: todoId,
2399
+ plan_epoch: committed.pointer.plan_epoch, packet_digest: packetDigest,
2400
+ controller_registration_digest: owner.registration.registration_digest,
2401
+ supervisor_session_nonce_digest: nonceDigest, state: 'staged',
2402
+ ttl_ms: owner.controllerDescriptor.heartbeat.ttl_ms,
2403
+ issued_control_digest: issuedControlDigest, lease_digest: '' };
2404
+ lease.lease_digest = selfDigest(lease, 'lease_digest');
2405
+ return lease;
2406
+ };
2407
+ const predecessorBundle = await readBoundedJson(path.join(runDir, 'epochs',
2408
+ String(committed.pointer.plan_epoch - 1).padStart(8, '0'), 'epoch-bundle.json'),
2409
+ 'predecessor epoch bundle');
2410
+ const recoveryRebindEvidence = new Map();
2411
+ for (const [todoId, rebindPacket] of Object.entries(committed.bundle.rebind_packets)) {
2412
+ const owner = recoveryControllerForTodo(todoId);
2413
+ const dispatch = projectRuntimeState({ events }).dispatches[todoId];
2414
+ if (dispatch === undefined) throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED',
2415
+ `carry rebind predecessor dispatch不足: ${todoId}`);
2416
+ const evidence = await managedSupervisor.rebindController({ controllerId: owner.controllerDescriptor.controller_id, rebindPacket,
2417
+ stagedLease: recoveryLease(todoId, rebindPacket.packet_digest, owner), expected: {
2418
+ todo_id: todoId, executor_handle: dispatch.payload.executor_handle,
2419
+ worktree_id: dispatch.payload.worktree_id,
2420
+ predecessor_packet_digest: predecessorBundle.executor_packets[todoId]?.packet_digest
2421
+ ?? rebindPacket.packet_digest,
2422
+ rebind_packet_digest: rebindPacket.packet_digest,
2423
+ } });
2424
+ recoveryRebindEvidence.set(todoId, { ...evidence,
2425
+ controller_registration_digest: owner.registration.registration_digest });
2426
+ transaction.recovered_todo_ids.push(todoId);
2427
+ transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
2428
+ await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
2429
+ }
2430
+ for (const predecessorId of committed.bundle.plan_diff.redispatched) {
2431
+ const successors = committed.bundle.task_migration.entries
2432
+ .find((entry) => entry.predecessor_task_id === predecessorId)?.successor_task_ids ?? [];
2433
+ for (const todoId of successors) {
2434
+ if (Object.hasOwn(committed.bundle.rebind_packets, todoId)) continue;
2435
+ const packet = committed.bundle.executor_packets[todoId];
2436
+ if (packet === undefined) continue;
2437
+ const owner = recoveryControllerForTodo(todoId);
2438
+ try {
2439
+ await managedSupervisor.prepareController({ controllerId: owner.controllerDescriptor.controller_id,
2440
+ executorPacket: packet, stagedLease: recoveryLease(todoId, packet.packet_digest, owner) });
2441
+ } catch (error) {
2442
+ transaction.recovery_error = `${error?.code ?? 'ERROR'}:${error?.message ?? error}`;
2443
+ transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
2444
+ await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
2445
+ throw error;
2446
+ }
2447
+ transaction.recovered_todo_ids.push(todoId);
2448
+ transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
2449
+ await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
2450
+ }
2451
+ }
2452
+ const recoveryReboundAt = canonicalNow();
2453
+ const recoveryReboundBatch = [];
2454
+ let recoveryReboundEvents = [...events];
2455
+ for (const [todoId, rebindPacket] of Object.entries(committed.bundle.rebind_packets)
2456
+ .sort(([left], [right]) => left.localeCompare(right))) {
2457
+ const evidence = recoveryRebindEvidence.get(todoId);
2458
+ const event = buildNextRunEvent({ events: recoveryReboundEvents,
2459
+ runId: committed.meta.run_id, kind: 'epoch_rebound',
2460
+ planEpoch: committed.pointer.plan_epoch,
2461
+ subject: { kind: 'todo', ref: todoId }, payload: { ...rebindPacket,
2462
+ rebind_ack_digest: evidence.ack.ack_digest,
2463
+ control_event_digest: evidence.control_event_digest,
2464
+ controller_registration_digest: evidence.controller_registration_digest },
2465
+ recordedAt: recoveryReboundAt });
2466
+ recoveryReboundEvents.push(event);
2467
+ recoveryReboundBatch.push(event);
2468
+ }
2469
+ events = await publishRuntimeEventBatch({ runDir,
2470
+ transactionId: transaction.request_id,
2471
+ phase: `epoch_rebound_recovery_${nonceDigest}`,
2472
+ planEpoch: committed.pointer.plan_epoch,
2473
+ bindingDigest: digestArtifact({ successor_bundle_digest: committed.bundle.bundle_digest,
2474
+ supervisor_session_nonce_digest: nonceDigest,
2475
+ ordered_rebind_packet_digests: Object.entries(committed.bundle.rebind_packets)
2476
+ .sort(([left], [right]) => left.localeCompare(right))
2477
+ .map(([, packet]) => packet.packet_digest) }),
2478
+ currentEvents: events, proposedBatch: recoveryReboundBatch, crashInjector });
2479
+ transaction.state = 'recovery_ready';
2480
+ transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
2481
+ await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
2482
+ const activationDigest = digestArtifact({ pointer_digest: committed.pointer.pointer_digest,
2483
+ staged_bundle_digest: committed.bundle.bundle_digest });
2484
+ let activated;
2485
+ try {
2486
+ activated = await managedSupervisor.commitWriteGate({
2487
+ planEpoch: committed.pointer.plan_epoch,
2488
+ committedEpochDigest: committed.pointer.pointer_digest, activationDigest,
2489
+ commitReleaseBarrier: (barrier) => commitReleaseEpochBarrier({ runDir, barrier }),
2490
+ afterControllerRelease: async (released) => {
2491
+ if (released.release_index === 1 && typeof crashInjector === 'function') {
2492
+ await crashInjector('after_first_controller_release', released);
2493
+ }
2494
+ },
2495
+ committedAt: canonicalNow(),
2496
+ });
2497
+ } catch (error) {
2498
+ transaction.recovery_error = `${error?.code ?? 'ERROR'}:${error?.message ?? error}`;
2499
+ transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
2500
+ await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
2501
+ throw error;
2502
+ }
2503
+ if (transaction.queue_replay_commit !== null) {
2504
+ await replaceCanonicalJsonAtomically(runDir, 'queued-events.json',
2505
+ transaction.queue_replay_commit);
2506
+ }
2507
+ if (!events.some((event) => event.kind === 'intake_resumed'
2508
+ && event.plan_epoch === committed.pointer.plan_epoch
2509
+ && event.payload?.write_gate_digest === activated.gate.gate_digest)) {
2510
+ events.push(buildNextRunEvent({ events, runId: committed.meta.run_id,
2511
+ kind: 'intake_resumed', planEpoch: committed.pointer.plan_epoch,
2512
+ subject: { kind: 'runtime_plan', ref: committed.bundle.plan.plan_ref },
2513
+ payload: { plan_diff_digest: committed.bundle.plan_diff.diff_digest,
2514
+ write_gate_digest: activated.gate.gate_digest }, recordedAt: canonicalNow() }));
2515
+ await replaceEventsAtomically(runDir, events);
2516
+ }
2517
+ transaction.state = 'gate_committed';
2518
+ transaction.gate_digest = activated.gate.gate_digest;
2519
+ transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
2520
+ await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
2521
+ if (typeof crashInjector === 'function') await crashInjector('after_successor_intake_resume', {
2522
+ request_id: controlRequest.request_id, gate_digest: activated.gate.gate_digest,
2523
+ event_digest: events.at(-1).event_digest,
2524
+ });
2525
+ recompiled = true;
2526
+ }
2527
+ const queue = controlRequest.operation === 'reprocess'
2528
+ ? await readBoundedJson(path.join(runDir, 'queued-events.json'), 'runtime queue').catch(() => null)
2529
+ : null;
2530
+ let resumedReprocess = false;
2531
+ if (controlRequest.operation === 'reprocess' && committed !== null
2532
+ && committed.pointer.plan_epoch >= controlRequest.payload.expected_epoch) {
2533
+ const pending = await readBoundedJson(path.join(runDir, 'pending-recompile.json'),
2534
+ 'pending recompile').catch(() => null);
2535
+ if (pending?.schema === 'lattice.runtime_pending_recompile.v1'
2536
+ && pending.pending_digest === selfDigest(pending, 'pending_digest')) {
2537
+ const resumeRequest = createRuntimeControlRequest({
2538
+ requestId: pending.control_request_id, runId: committed.meta.run_id,
2539
+ operation: 'recompile', sessionNonce, payload: controlOperationPayload({
2540
+ operation: 'recompile', runRef: controlRequest.payload.run_ref,
2541
+ artifact: pending.recompile_request,
2542
+ artifactDigest: digestArtifact(pending.recompile_request), checkpointDigest: null,
2543
+ expectedEpoch: pending.predecessor_epoch,
2544
+ expectedQueueDigest: pending.reprocess_queue_digest, shutdownReason: null,
2545
+ }),
2546
+ });
2547
+ let resumed;
2548
+ if (committed.pointer.plan_epoch === pending.predecessor_epoch) {
2549
+ resumed = await executeControl(resumeRequest);
2550
+ } else {
2551
+ try {
2552
+ resumed = await handler(resumeRequest);
2553
+ } catch (error) {
2554
+ pending.recovery_response_outcome = `nested_error:${error?.code ?? 'ERROR'}:${error?.message ?? error}`;
2555
+ pending.pending_digest = selfDigest(pending, 'pending_digest');
2556
+ await replaceCanonicalJsonAtomically(runDir, 'pending-recompile.json', pending);
2557
+ throw error;
2558
+ }
2559
+ }
2560
+ pending.recovery_response_outcome = resumed.outcome;
2561
+ pending.pending_digest = selfDigest(pending, 'pending_digest');
2562
+ await replaceCanonicalJsonAtomically(runDir, 'pending-recompile.json', pending);
2563
+ if (resumed.outcome === 'completed') {
2564
+ events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
2565
+ committed = await readCommittedEpochStore(runDir);
2566
+ resumedReprocess = true;
2567
+ const currentJournal = await eventStore.readEvents();
2568
+ const outerResult = buildControlResult({ operation: 'reprocess',
2569
+ outcome: 'reprocessed', eventHeadDigest: events.at(-1)?.event_digest ?? null,
2570
+ controlHeadDigest: currentJournal.at(-1)?.event_digest ?? null,
2571
+ activeEpoch: committed.pointer.plan_epoch, stagedEpoch: null });
2572
+ const outerResponse = buildControlResponse(controlRequest, 'completed', outerResult,
2573
+ currentJournal.at(-1)?.event_digest ?? null);
2574
+ await replaceCanonicalJsonAtomically(runDir,
2575
+ `reprocess-operation-response-${controlRequest.request_id}.json`, outerResponse);
2576
+ if (sessionRecoveryResponse?.outcome === 'completed') return sessionRecoveryResponse;
2577
+ return sessionRecoveryResponse?.outcome === 'rejected'
2578
+ ? requestStore.recoverCompletedRequest(controlRequest, outerResponse)
2579
+ : requestStore.completeRequest(controlRequest, outerResponse);
2580
+ }
2581
+ }
2582
+ }
2583
+ const reprocessed = controlRequest.operation === 'reprocess'
2584
+ && (resumedReprocess || (committed?.pointer.plan_epoch === controlRequest.payload.expected_epoch + 1
2585
+ && managedSupervisor.frozen === false
2586
+ && !(await isManagedRunFrozen(runDir, events))));
2587
+ const closed = events.findLast((event) => event.kind === 'run_closed');
2588
+ const terminal = held || found || recordedFinding !== null || recompiled || reprocessed
2589
+ || (controlRequest.operation === 'close' && closed?.payload?.outcome === 'completed')
2590
+ || (controlRequest.operation === 'abandon' && closed?.payload?.outcome === 'abandoned');
2591
+ if (terminal) {
2592
+ outcome = 'completed';
2593
+ operationOutcome = held ? 'held' : found ? 'frozen'
2594
+ : recordedFinding !== null ? 'recorded'
2595
+ : recompiled ? 'recompiled' : reprocessed ? 'reprocessed'
2596
+ : controlRequest.operation === 'close' ? 'closed' : 'abandoned';
2597
+ unmet = [];
2598
+ }
2599
+ }
2600
+ const result = buildControlResult({ operation: controlRequest.operation,
2601
+ outcome: operationOutcome, eventHeadDigest: events.at(-1)?.event_digest ?? null,
2602
+ controlHeadDigest: journal.at(-1)?.event_digest ?? null,
2603
+ activeEpoch: (await readCommittedEpochStore(runDir).catch(() => null))?.pointer.plan_epoch ?? 1,
2604
+ unmet });
2605
+ if (operationOutcome === 'recorded') {
2606
+ const names = await readdir(path.join(runDir, 'findings')).catch(() => []);
2607
+ const recovered = await Promise.all(names.filter((entry) => /^[0-9a-f]{64}\.json$/u.test(entry))
2608
+ .map((entry) => readBoundedJson(path.join(runDir, 'findings', entry), 'runtime finding').catch(() => null)));
2609
+ const candidate = recovered.find((entry) => validateRuntimeFindingRecord(entry)
2610
+ && entry.source_checkpoint_digest === controlRequest.payload.checkpoint_digest);
2611
+ if (candidate !== undefined) {
2612
+ result.finding_digest = candidate.finding_digest;
2613
+ const body = { ...result }; delete body.result_digest;
2614
+ result.result_digest = digestArtifact(body);
2615
+ }
2616
+ }
2617
+ const response = buildControlResponse(controlRequest, outcome, result,
2618
+ journal.at(-1)?.event_digest ?? null);
2619
+ if (sessionRecoveryResponse !== null) {
2620
+ if (outcome !== 'completed') return response;
2621
+ return sessionRecoveryResponse.outcome === 'completed' ? sessionRecoveryResponse
2622
+ : requestStore.recoverCompletedRequest(controlRequest, response);
2623
+ }
2624
+ return known?.state === 'in_progress'
2625
+ ? requestStore.completeRequest(controlRequest, response)
2626
+ : response;
2627
+ }
2628
+ if (known?.state === 'in_progress') {
2629
+ const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events').catch(() => []);
2630
+ const journal = await eventStore.readEvents();
2631
+ const result = buildControlResult({ operation: controlRequest.operation, outcome: 'unknown',
2632
+ eventHeadDigest: events.at(-1)?.event_digest ?? null,
2633
+ controlHeadDigest: journal.at(-1)?.event_digest ?? null, activeEpoch: 1,
2634
+ unmet: ['RUN_OUTCOME_UNKNOWN', '同一request_idの処理結果はまだ確定していない'] });
2635
+ return buildControlResponse(controlRequest, 'unknown', result,
2636
+ journal.at(-1)?.event_digest ?? null);
2637
+ }
2638
+ let lock;
2639
+ try {
2640
+ lock = await acquireRuntimeLifecycleLock({ runDir,
2641
+ sessionNonceDigest: digestArtifact(sessionNonce), operation: controlRequest.operation,
2642
+ requestId: controlRequest.request_id, timeoutMs: 0 });
2643
+ } catch (error) {
2644
+ const code = error?.code ?? 'RUN_BUSY';
2645
+ const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events').catch(() => []);
2646
+ const journal = await eventStore.readEvents().catch(() => []);
2647
+ const result = buildControlResult({ operation: controlRequest.operation, outcome: 'rejected',
2648
+ eventHeadDigest: events.at(-1)?.event_digest ?? null,
2649
+ controlHeadDigest: journal.at(-1)?.event_digest ?? null, activeEpoch: 1,
2650
+ unmet: [code, String(error?.message ?? error)] });
2651
+ return buildControlResponse(controlRequest, 'rejected', result,
2652
+ journal.at(-1)?.event_digest ?? null);
2653
+ }
2654
+ try {
2655
+ const begun = await requestStore.beginRequest(controlRequest);
2656
+ if (begun.disposition === 'completed') return begun.response;
2657
+ if (begun.disposition === 'in_progress') {
2658
+ throw new ManagedRuntimeError('RUN_OUTCOME_UNKNOWN', '同一request_idが処理中');
2659
+ }
2660
+ const response = await executeControl(controlRequest);
2661
+ return requestStore.completeRequest(controlRequest, response);
2662
+ } finally {
2663
+ await lock.release();
2664
+ }
2665
+ };
2666
+ server = await serveRuntimeControlSocket({ socketPath, handler });
2667
+ registerDaemonCleanup(async (signal) => {
2668
+ if (activationCommitted && activation !== null) {
2669
+ await appendControl({ run_id: request.request_id, kind: 'supervisor_stopped',
2670
+ session_nonce_digest: digestArtifact(sessionNonce), payload: { signal } });
2671
+ }
2672
+ await activation?.disposeController?.();
2673
+ await Promise.all(additionalActivations.map((entry) => entry.disposeController?.()));
2674
+ if (server?.listening) await new Promise((resolve) => server.close(resolve));
2675
+ await rm(socketPath, { force: true });
2676
+ const active = await resolveActiveRuntimePaths({ runDir }).catch(() => null);
2677
+ if (active !== null) await rm(active.sessionPath, { force: true });
2678
+ await rm(path.join(supervisorDir, 'session'), { force: true });
2679
+ });
2680
+ onReady(socketPath);
2681
+ await new Promise((resolve) => server.once('close', resolve));
2682
+ }
2683
+
405
2684
  async function eventVerify({ runDir, stdout }) {
406
2685
  const { events, compileArtifact } = await readRunStore(runDir);
407
2686
  const chain = verifyRunEventChain({ events });
@@ -438,13 +2717,14 @@ async function eventVerify({ runDir, stdout }) {
438
2717
  output.result_digest = digestArtifact(output);
439
2718
  stdout.write(`${JSON.stringify(output)}\n`);
440
2719
  if (failed.length > 0) {
441
- throw new CliContractError('EVENT_VERIFICATION_FAILED', 'event検証が失敗', failed);
2720
+ // ADR 0052 Decision 1: failed_conditionsはstdoutのevent_verification.v1が既に持つ=detailで二重化しない。
2721
+ throw new CliContractError('EVENT_VERIFICATION_FAILED', 'event検証が失敗');
442
2722
  }
443
2723
  return 0;
444
2724
  }
445
2725
 
446
2726
  /**
447
- * `--version`/`doctor --json`以外の全argvを受け、exit codeを返す。
2727
+ * `--version`以外の全argvを受け、exit codeを返す(`doctor --json`はADR 0052で退役済み)。
448
2728
  * exact argument contract: 位置・順序・件数の完全一致だけを受理する。
449
2729
  */
450
2730
  export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
@@ -452,6 +2732,18 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
452
2732
  || typeof stdout?.write !== 'function' || typeof stderr?.write !== 'function') {
453
2733
  throw new TypeError('runRuntimeCli optionsが不正');
454
2734
  }
2735
+ let requestIdOverride = null;
2736
+ if (argv.length >= 2 && argv.at(-2) === '--request-id') {
2737
+ if (!/^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/u.test(argv.at(-1) ?? '')) {
2738
+ return typedFailure(stderr, 'INVALID_REQUEST_ID', 'request-idは128文字以下の識別子でなければならない');
2739
+ }
2740
+ requestIdOverride = argv.at(-1);
2741
+ argv = argv.slice(0, -2);
2742
+ if (argv[0] !== 'run' || !['activate', 'close', 'abandon', 'conflict', 'hold', 'recompile', 'reprocess'].includes(argv[1])
2743
+ && !(argv[0] === 'run' && argv[1] === 'finding' && argv[2] === 'record')) {
2744
+ return typedFailure(stderr, 'INVALID_REQUEST_ID', '--request-idはrun mutationだけで指定できる');
2745
+ }
2746
+ }
455
2747
  let action = null;
456
2748
  if (argv.length === 4
457
2749
  && argv[0] === 'plan' && argv[1] === 'compile' && argv[2] === '--request'
@@ -478,16 +2770,97 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
478
2770
  stdout,
479
2771
  });
480
2772
  } else if (argv.length === 4
481
- && argv[0] === 'run' && (argv[1] === 'observe' || argv[1] === 'status')
2773
+ && argv[0] === 'run' && argv[1] === 'activate'
482
2774
  && argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0) {
483
- const runDir = path.resolve(cwd, argv[3]);
484
- action = argv[1] === 'observe'
485
- ? () => runObserve({ runDir, stdout })
486
- : () => runStatus({ runDir, stdout });
2775
+ action = async () => {
2776
+ const { repoRoot, runDir, runRef } = await resolveRunStore(cwd, argv[3]);
2777
+ return runActivate({ runDir, runRef, repoRoot, stdout, requestId: requestIdOverride });
2778
+ };
2779
+ } else if (argv.length === 4
2780
+ && argv[0] === 'run' && ['observe', 'status', 'resume', 'close'].includes(argv[1])
2781
+ && argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0) {
2782
+ action = async () => {
2783
+ const { repoRoot, runDir } = await resolveRunStore(cwd, argv[3]);
2784
+ if (argv[1] === 'observe') return runObserve({ runDir, stdout });
2785
+ if (argv[1] === 'status') return runStatus({ runDir, stdout });
2786
+ if (argv[1] === 'resume') return runResume({ runDir, repoRoot, stdout });
2787
+ return runClose({ runDir, repoRoot, stdout, requestId: requestIdOverride });
2788
+ };
2789
+ } else if (argv.length === 6
2790
+ && argv[0] === 'run' && argv[1] === 'abandon'
2791
+ && argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0
2792
+ && argv[4] === '--reason' && typeof argv[5] === 'string' && argv[5].length > 0) {
2793
+ action = async () => {
2794
+ const { runDir } = await resolveRunStore(cwd, argv[3]);
2795
+ return runAbandon({ runDir, runRef: argv[3], reason: argv[5], stdout,
2796
+ requestId: requestIdOverride });
2797
+ };
2798
+ } else if (argv.length === 6
2799
+ && argv[0] === 'run' && argv[1] === 'conflict'
2800
+ && argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0
2801
+ && argv[4] === '--finding' && /^[0-9a-f]{64}$/u.test(argv[5])) {
2802
+ action = async () => {
2803
+ const { runDir, runRef } = await resolveRunStore(cwd, argv[3]);
2804
+ return runManagedControl({
2805
+ runDir, runRef, operation: 'conflict', artifactDigest: argv[5], stdout,
2806
+ requestId: requestIdOverride,
2807
+ });
2808
+ };
2809
+ } else if (argv.length === 8
2810
+ && argv[0] === 'run' && argv[1] === 'finding' && argv[2] === 'record'
2811
+ && argv[3] === '--run' && typeof argv[4] === 'string' && argv[4].length > 0
2812
+ && argv[5] === '--checkpoint' && /^[0-9a-f]{64}$/u.test(argv[6])
2813
+ && argv[7] === '--input') {
2814
+ // exact argvにはinput valueが必要なため、この8要素形は必ずusage拒否される。
2815
+ action = null;
2816
+ } else if (argv.length === 9
2817
+ && argv[0] === 'run' && argv[1] === 'finding' && argv[2] === 'record'
2818
+ && argv[3] === '--run' && typeof argv[4] === 'string' && argv[4].length > 0
2819
+ && argv[5] === '--checkpoint' && /^[0-9a-f]{64}$/u.test(argv[6])
2820
+ && argv[7] === '--input' && typeof argv[8] === 'string' && argv[8].length > 0) {
2821
+ action = async () => {
2822
+ const { runDir, runRef } = await resolveRunStore(cwd, argv[4]);
2823
+ if (await readCommittedEpochStore(runDir) === null) {
2824
+ throw new CliContractError('RUN_NOT_MANAGED', 'runがmanaged storeへactivateされていない');
2825
+ }
2826
+ const artifact = await readBoundedJson(path.resolve(cwd, argv[8]), 'runtime finding candidate');
2827
+ return runManagedControl({ runDir, runRef, operation: 'finding_record', artifact,
2828
+ artifactDigest: digestArtifact(artifact), checkpointDigest: argv[6], stdout,
2829
+ requestId: requestIdOverride });
2830
+ };
2831
+ } else if (argv.length === 6
2832
+ && argv[0] === 'run' && argv[1] === 'recompile'
2833
+ && argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0
2834
+ && argv[4] === '--input' && typeof argv[5] === 'string' && argv[5].length > 0) {
2835
+ action = async () => {
2836
+ const { runDir, runRef } = await resolveRunStore(cwd, argv[3]);
2837
+ if (await readCommittedEpochStore(runDir) === null) {
2838
+ throw new CliContractError('RUN_NOT_MANAGED', 'runがmanaged storeへactivateされていない');
2839
+ }
2840
+ const artifact = await readBoundedJson(path.resolve(cwd, argv[5]), 'runtime recompile request');
2841
+ return runManagedControl({ runDir, runRef, operation: 'recompile', artifact,
2842
+ artifactDigest: digestArtifact(artifact), stdout, requestId: requestIdOverride });
2843
+ };
2844
+ } else if (argv.length === 4
2845
+ && argv[0] === 'run' && ['hold', 'reprocess'].includes(argv[1])
2846
+ && argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0) {
2847
+ action = async () => {
2848
+ const { runDir, runRef } = await resolveRunStore(cwd, argv[3]);
2849
+ return runManagedControl({
2850
+ runDir, runRef, operation: argv[1], artifactDigest: null, stdout,
2851
+ requestId: requestIdOverride,
2852
+ });
2853
+ };
2854
+ } else if (argv.length === 3
2855
+ && argv[0] === 'run' && argv[1] === 'list' && argv[2] === '--json') {
2856
+ action = () => runList({ cwd, stdout });
487
2857
  } else if (argv.length === 4
488
2858
  && argv[0] === 'event' && argv[1] === 'verify'
489
2859
  && argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0) {
490
- action = () => eventVerify({ runDir: path.resolve(cwd, argv[3]), stdout });
2860
+ action = async () => {
2861
+ const { runDir } = await resolveRunStore(cwd, argv[3]);
2862
+ return eventVerify({ runDir, stdout });
2863
+ };
491
2864
  }
492
2865
  if (action === null) return usageFailure(stderr, argv);
493
2866
  try {
@@ -499,6 +2872,12 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
499
2872
  if (error instanceof TypeError) {
500
2873
  return typedFailure(stderr, 'CONTRACT_VIOLATION', error.message);
501
2874
  }
2875
+ if (error instanceof RuntimeEpochStoreError) {
2876
+ return typedFailure(stderr, error.code, error.message);
2877
+ }
2878
+ if (error instanceof ManagedRuntimeError) {
2879
+ return typedFailure(stderr, error.code, error.message);
2880
+ }
502
2881
  throw error;
503
2882
  }
504
2883
  }