@rryando/arcs 4.2.0 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/arcs-flash.d.ts +1 -1
  3. package/dist/cli/arcs-flash.d.ts.map +1 -1
  4. package/dist/cli/arcs-flash.js +3 -5
  5. package/dist/cli/arcs-flash.js.map +1 -1
  6. package/dist/cli/arcs-orchestrate.d.ts +1 -1
  7. package/dist/cli/arcs-orchestrate.d.ts.map +1 -1
  8. package/dist/cli/commands/index.d.ts +0 -1
  9. package/dist/cli/commands/index.d.ts.map +1 -1
  10. package/dist/cli/commands/index.js +0 -1
  11. package/dist/cli/commands/index.js.map +1 -1
  12. package/dist/cli/commands/project.js +1 -30
  13. package/dist/cli/commands/project.js.map +1 -1
  14. package/dist/cli/commands/proposal-doc.d.ts +2 -0
  15. package/dist/cli/commands/proposal-doc.d.ts.map +1 -0
  16. package/dist/cli/commands/proposal-doc.js +381 -0
  17. package/dist/cli/commands/proposal-doc.js.map +1 -0
  18. package/dist/cli/commands/web.js +4 -10
  19. package/dist/cli/commands/web.js.map +1 -1
  20. package/dist/cli/orchestrator-shared-blocks.d.ts +11 -10
  21. package/dist/cli/orchestrator-shared-blocks.d.ts.map +1 -1
  22. package/dist/cli/orchestrator-shared-blocks.js +21 -16
  23. package/dist/cli/orchestrator-shared-blocks.js.map +1 -1
  24. package/dist/utils/graphify-knowledge.d.ts +22 -0
  25. package/dist/utils/graphify-knowledge.d.ts.map +1 -0
  26. package/dist/utils/graphify-knowledge.js +47 -0
  27. package/dist/utils/graphify-knowledge.js.map +1 -0
  28. package/dist/utils/graphify.d.ts +104 -0
  29. package/dist/utils/graphify.d.ts.map +1 -0
  30. package/dist/utils/graphify.js +439 -0
  31. package/dist/utils/graphify.js.map +1 -0
  32. package/dist/utils/session-store.d.ts +24 -2
  33. package/dist/utils/session-store.d.ts.map +1 -1
  34. package/dist/utils/session-store.js +16 -7
  35. package/dist/utils/session-store.js.map +1 -1
  36. package/dist/utils/storage-utils.d.ts +1 -1
  37. package/dist/utils/storage-utils.d.ts.map +1 -1
  38. package/dist/utils/storage-utils.js +1 -1
  39. package/dist/utils/storage-utils.js.map +1 -1
  40. package/dist/web-client/assets/{GraphCanvas-BPDgvsyT.js → GraphCanvas-Dw6EcoDb.js} +1 -1
  41. package/dist/web-client/assets/{MarkdownEditor-D7TLp78z.js → MarkdownEditor-Cz5_44Ej.js} +1 -1
  42. package/dist/web-client/assets/{abnfDiagram-VRR7QNED-CyuP2N9t.js → abnfDiagram-VRR7QNED-CFJzLuew.js} +1 -1
  43. package/dist/web-client/assets/architecture-TIHT7OUA-CoHvhex9.js +1 -0
  44. package/dist/web-client/assets/{architectureDiagram-ZJ3FMSHR-DZ0ul9QX.js → architectureDiagram-ZJ3FMSHR-FRlSgnnW.js} +1 -1
  45. package/dist/web-client/assets/{blockDiagram-677ZJIJ3-LLGzlc9l.js → blockDiagram-677ZJIJ3-WUkkunPZ.js} +1 -1
  46. package/dist/web-client/assets/{c4Diagram-LMCZKHZV-CViu3CTc.js → c4Diagram-LMCZKHZV-BKeAsL6F.js} +1 -1
  47. package/dist/web-client/assets/channel-D8xMXC5_.js +1 -0
  48. package/dist/web-client/assets/{chunk-32BRIVSS-Bw_IuJCM.js → chunk-32BRIVSS-B0b9kUcF.js} +1 -1
  49. package/dist/web-client/assets/{chunk-52WLFC77-C29h440W.js → chunk-52WLFC77-i6Y-vfL3.js} +1 -1
  50. package/dist/web-client/assets/{chunk-C7G6YPKG-hhOrvw5w.js → chunk-C7G6YPKG-atYWm6iP.js} +1 -1
  51. package/dist/web-client/assets/{chunk-EX3LRPZG-COMzol-M.js → chunk-EX3LRPZG-CZD6y0UU.js} +1 -1
  52. package/dist/web-client/assets/{chunk-FWX5IMBZ-6vdX9EUn.js → chunk-FWX5IMBZ-CVErR-UZ.js} +2 -2
  53. package/dist/web-client/assets/{chunk-HOUHSVGY-DWDW6sxp.js → chunk-HOUHSVGY-BlHO2iQ3.js} +1 -1
  54. package/dist/web-client/assets/{chunk-ICXQ74PX-BdMYglo2.js → chunk-ICXQ74PX-Ar8kfXfa.js} +1 -1
  55. package/dist/web-client/assets/{chunk-MOJQB5TN-C0LAX_dC.js → chunk-MOJQB5TN-DKrK867P.js} +1 -1
  56. package/dist/web-client/assets/{chunk-OGEWGWER-CBx8MB7f.js → chunk-OGEWGWER-BmGykUeg.js} +1 -1
  57. package/dist/web-client/assets/{chunk-PUDLZKDR-DKssR1nf.js → chunk-PUDLZKDR-Bq23pnso.js} +1 -1
  58. package/dist/web-client/assets/{chunk-Q4XR5HBZ-B3kcxFE-.js → chunk-Q4XR5HBZ-g15qvFAN.js} +1 -1
  59. package/dist/web-client/assets/{chunk-V7JOEXUC-CAlymndy.js → chunk-V7JOEXUC-uyXA7S9r.js} +1 -1
  60. package/dist/web-client/assets/{chunk-VAUOI2AC-BowfsmTW.js → chunk-VAUOI2AC-WofZ-2M0.js} +1 -1
  61. package/dist/web-client/assets/{chunk-VR4S4FIN-BBOydgvt.js → chunk-VR4S4FIN-DtTkK86v.js} +1 -1
  62. package/dist/web-client/assets/{chunk-WYO6CB5R-DcymFbES.js → chunk-WYO6CB5R-zVyUwJq3.js} +1 -1
  63. package/dist/web-client/assets/{chunk-ZGVPDNZ5--uKFP-Lr.js → chunk-ZGVPDNZ5-CA1Oe3TK.js} +1 -1
  64. package/dist/web-client/assets/classDiagram-OUVF2IWQ-Doj1_hvZ.js +1 -0
  65. package/dist/web-client/assets/classDiagram-v2-EOCWNBFH-Doj1_hvZ.js +1 -0
  66. package/dist/web-client/assets/{cynefin-VYW2F7L2-CjboUOMA.js → cynefin-VYW2F7L2-ivJY1h_9.js} +1 -1
  67. package/dist/web-client/assets/{cynefinDiagram-TSTJHNR4-BcxygBP7.js → cynefinDiagram-TSTJHNR4-D2DstShq.js} +1 -1
  68. package/dist/web-client/assets/{dagre-VKFMJZFB-D-tiERQE.js → dagre-VKFMJZFB-CwnMRy0K.js} +1 -1
  69. package/dist/web-client/assets/{diagram-FQU43EPY-ChPXczaS.js → diagram-FQU43EPY-BKJv6Cvw.js} +1 -1
  70. package/dist/web-client/assets/{diagram-G47NLZAW-CVL3Y91h.js → diagram-G47NLZAW-CwCXcgU5.js} +1 -1
  71. package/dist/web-client/assets/{diagram-NH7WQ7WH-DsaNA9Lh.js → diagram-NH7WQ7WH-CkghU1-6.js} +1 -1
  72. package/dist/web-client/assets/{diagram-OA4YK3LP-CXhrhdhU.js → diagram-OA4YK3LP-D3siQIGu.js} +1 -1
  73. package/dist/web-client/assets/{diagram-WEI45ONY-BTVPnk4E.js → diagram-WEI45ONY-CSs9xTBn.js} +1 -1
  74. package/dist/web-client/assets/{ebnfDiagram-CCIWWBDH-BAyrRBtM.js → ebnfDiagram-CCIWWBDH--i52vMiv.js} +1 -1
  75. package/dist/web-client/assets/{erDiagram-Q63AITRT-Qm24Wepm.js → erDiagram-Q63AITRT-q2hgOBY1.js} +1 -1
  76. package/dist/web-client/assets/eventmodeling-45OFAUF4-ESVuFkJJ.js +1 -0
  77. package/dist/web-client/assets/flowDiagram-23GEKE2U-DN9SdR9f.js +1 -0
  78. package/dist/web-client/assets/{ganttDiagram-NO4QXBWP-D8h7l3XJ.js → ganttDiagram-NO4QXBWP-BZ2rBbTe.js} +1 -1
  79. package/dist/web-client/assets/{gitGraph-TEB2WS4Q-DIBml1SB.js → gitGraph-TEB2WS4Q-OnJ8tHgt.js} +1 -1
  80. package/dist/web-client/assets/{gitGraphDiagram-IHSO6WYX-CtkYoXjn.js → gitGraphDiagram-IHSO6WYX-CPNExFAr.js} +1 -1
  81. package/dist/web-client/assets/{index-DOSH4Q9H.js → index-DCBn-YrR.js} +38 -38
  82. package/dist/web-client/assets/{info-DKCQHKI2-DLEUtV5Q.js → info-DKCQHKI2-DGuJbmdY.js} +1 -1
  83. package/dist/web-client/assets/{infoDiagram-FWYZ7A6U-BJQ7aQux.js → infoDiagram-FWYZ7A6U-ADIAn-P5.js} +1 -1
  84. package/dist/web-client/assets/{ishikawaDiagram-FXEZZL3T-BPM11FvG.js → ishikawaDiagram-FXEZZL3T-CUx-RGfg.js} +1 -1
  85. package/dist/web-client/assets/{journeyDiagram-5HDEW3XC-C0aX2z3c.js → journeyDiagram-5HDEW3XC-BH7-tBBy.js} +1 -1
  86. package/dist/web-client/assets/{kanban-definition-HUTT4EX6-C56F29Ib.js → kanban-definition-HUTT4EX6-BGEXBnI2.js} +1 -1
  87. package/dist/web-client/assets/{line-BLFHLF2N.js → line-CC-5ezU8.js} +1 -1
  88. package/dist/web-client/assets/{mermaid-parser.core-BLC8FhgU.js → mermaid-parser.core-Dw18Fjbq.js} +3 -3
  89. package/dist/web-client/assets/{mermaid.core-BBqkKuXt.js → mermaid.core-CMQBgE43.js} +3 -3
  90. package/dist/web-client/assets/{mindmap-definition-LN4V7U3C-aVZbsoPc.js → mindmap-definition-LN4V7U3C-CGtjw8tB.js} +1 -1
  91. package/dist/web-client/assets/{packet-7NZHBO7P-D4aqSQfB.js → packet-7NZHBO7P-w71Y4Hzd.js} +1 -1
  92. package/dist/web-client/assets/{pegDiagram-2B236MQR-DjfyNI0U.js → pegDiagram-2B236MQR-DWtfEm4T.js} +1 -1
  93. package/dist/web-client/assets/{pie-RZYD4A2V-ChCwYsYj.js → pie-RZYD4A2V-S9ztgnWs.js} +1 -1
  94. package/dist/web-client/assets/{pieDiagram-ENE6RG2P-BeHLKkXC.js → pieDiagram-ENE6RG2P-BWZQPN4m.js} +1 -1
  95. package/dist/web-client/assets/{quadrantDiagram-ABIIQ3AL-stga3gvq.js → quadrantDiagram-ABIIQ3AL-DINDwblH.js} +1 -1
  96. package/dist/web-client/assets/{radar-I7S5WNFK-DOGheiwT.js → radar-I7S5WNFK-BOGl9krB.js} +1 -1
  97. package/dist/web-client/assets/{railroad-3IZDKUUU-_JnU7M6L.js → railroad-3IZDKUUU-CR9kuYSZ.js} +1 -1
  98. package/dist/web-client/assets/railroad-abnf-AHOZXSZD-BvbCHrlR.js +1 -0
  99. package/dist/web-client/assets/railroad-ebnf-EBAXGLYW-D869lbCL.js +1 -0
  100. package/dist/web-client/assets/railroad-peg-LSFZ7HO6-r4TXaJPI.js +1 -0
  101. package/dist/web-client/assets/{railroadDiagram-RFXS5EU6-C0CkMsOd.js → railroadDiagram-RFXS5EU6-byLCs9hp.js} +1 -1
  102. package/dist/web-client/assets/{requirementDiagram-TGXJPOKE-DuImwoRD.js → requirementDiagram-TGXJPOKE-Dn_FtbqW.js} +1 -1
  103. package/dist/web-client/assets/{sankeyDiagram-HTMAVEWB-kprq0XF9.js → sankeyDiagram-HTMAVEWB-DUjRrCeC.js} +1 -1
  104. package/dist/web-client/assets/{sequenceDiagram-DBY2YBRQ-DiXKJMF6.js → sequenceDiagram-DBY2YBRQ-BmBz8Wpq.js} +1 -1
  105. package/dist/web-client/assets/{stateDiagram-2N3HPSRC-D5qbVStE.js → stateDiagram-2N3HPSRC-BAYHjmqB.js} +1 -1
  106. package/dist/web-client/assets/stateDiagram-v2-6OUMAXLB-Bl-1K1zV.js +1 -0
  107. package/dist/web-client/assets/{swimlanes-5IMT3BWC-DCbw389c.js → swimlanes-5IMT3BWC-Bm942AR-.js} +1 -1
  108. package/dist/web-client/assets/swimlanesDiagram-G3AALYLV-DHU7bsfA.js +8 -0
  109. package/dist/web-client/assets/{timeline-definition-FHXFAJF6-CQeaYN_9.js → timeline-definition-FHXFAJF6-CHAZHhQk.js} +1 -1
  110. package/dist/web-client/assets/{treeView-QDETBFTQ-Cf7Sq3qo.js → treeView-QDETBFTQ-DHHkLsSy.js} +1 -1
  111. package/dist/web-client/assets/{treemap-6X3UGDF4-BovzvoTU.js → treemap-6X3UGDF4-BUxZ8fhY.js} +1 -1
  112. package/dist/web-client/assets/{vennDiagram-L72KCM5P-CZsJy139.js → vennDiagram-L72KCM5P-BL9TRcUU.js} +1 -1
  113. package/dist/web-client/assets/{wardley-OPB4EBWU-DJ7MS6XZ.js → wardley-OPB4EBWU-CfJ5mO-y.js} +1 -1
  114. package/dist/web-client/assets/{wardleyDiagram-EHGQE667-rqhcmsbM.js → wardleyDiagram-EHGQE667-BlH1TSjy.js} +1 -1
  115. package/dist/web-client/assets/{xychartDiagram-FW5EYKEG-HuK4Seps.js → xychartDiagram-FW5EYKEG-BJN6wtrV.js} +1 -1
  116. package/dist/web-client/index.html +1 -1
  117. package/dist/web-server/app.d.ts.map +1 -1
  118. package/dist/web-server/app.js +1 -7
  119. package/dist/web-server/app.js.map +1 -1
  120. package/dist/web-server/claude-runner.d.ts +11 -4
  121. package/dist/web-server/claude-runner.d.ts.map +1 -1
  122. package/dist/web-server/claude-runner.js +10 -14
  123. package/dist/web-server/claude-runner.js.map +1 -1
  124. package/dist/web-server/index.d.ts.map +1 -1
  125. package/dist/web-server/index.js +4 -1
  126. package/dist/web-server/index.js.map +1 -1
  127. package/dist/web-server/opencode-client.d.ts +123 -0
  128. package/dist/web-server/opencode-client.d.ts.map +1 -0
  129. package/dist/web-server/opencode-client.js +514 -0
  130. package/dist/web-server/opencode-client.js.map +1 -0
  131. package/dist/web-server/prompt-assembly.d.ts.map +1 -1
  132. package/dist/web-server/prompt-assembly.js +1 -2
  133. package/dist/web-server/prompt-assembly.js.map +1 -1
  134. package/dist/web-server/routes/sessions.d.ts +9 -6
  135. package/dist/web-server/routes/sessions.d.ts.map +1 -1
  136. package/dist/web-server/routes/sessions.js +272 -205
  137. package/dist/web-server/routes/sessions.js.map +1 -1
  138. package/dist/web-server/run-driver.d.ts +113 -0
  139. package/dist/web-server/run-driver.d.ts.map +1 -0
  140. package/dist/web-server/run-driver.js +214 -0
  141. package/dist/web-server/run-driver.js.map +1 -0
  142. package/dist/web-server/run-event-log.d.ts +23 -1
  143. package/dist/web-server/run-event-log.d.ts.map +1 -1
  144. package/dist/web-server/run-event-log.js +29 -5
  145. package/dist/web-server/run-event-log.js.map +1 -1
  146. package/dist/web-server/web-auth.d.ts +3 -5
  147. package/dist/web-server/web-auth.d.ts.map +1 -1
  148. package/dist/web-server/web-auth.js +6 -11
  149. package/dist/web-server/web-auth.js.map +1 -1
  150. package/dist/web-server/web-token.d.ts +1 -2
  151. package/dist/web-server/web-token.d.ts.map +1 -1
  152. package/dist/web-server/web-token.js +1 -2
  153. package/dist/web-server/web-token.js.map +1 -1
  154. package/opencode/arcs/bundle-runtime.json +0 -3
  155. package/opencode/arcs/prompts/arcs-docs.txt +4 -4
  156. package/opencode/arcs/prompts/arcs-flash.txt +15 -21
  157. package/opencode/arcs/prompts/arcs-orchestrate-caveman.txt +17 -15
  158. package/opencode/arcs/prompts/arcs-orchestrate.txt +17 -15
  159. package/opencode/arcs/prompts/code-reviewer.txt +4 -4
  160. package/opencode/arcs/prompts/graph-explorer.txt +4 -4
  161. package/opencode/arcs/prompts/software-engineer.txt +2 -2
  162. package/opencode/arcs/prompts/tech-architect.txt +4 -4
  163. package/opencode/arcs/skills/brainstorming/SKILL.md +4 -4
  164. package/opencode/arcs/skills/enriching-codegraph-proposals/SKILL.md +1 -1
  165. package/opencode/arcs/skills/implementation/SKILL.md +6 -6
  166. package/opencode/arcs/skills/init-project/SKILL.md +4 -4
  167. package/opencode/arcs/skills/systematic-debugging/SKILL.md +2 -2
  168. package/opencode/arcs/skills/test-driven-development/SKILL.md +3 -3
  169. package/opencode/arcs/skills/to-diagram/SKILL.md +2 -2
  170. package/opencode/arcs/skills/writing-knowledge/SKILL.md +2 -2
  171. package/opencode/arcs/skills/writing-plans/SKILL.md +2 -2
  172. package/package.json +1 -1
  173. package/dist/web-client/assets/architecture-TIHT7OUA-Bdo2Yvm9.js +0 -1
  174. package/dist/web-client/assets/channel-DBNmizpo.js +0 -1
  175. package/dist/web-client/assets/classDiagram-OUVF2IWQ-CB3HiA1_.js +0 -1
  176. package/dist/web-client/assets/classDiagram-v2-EOCWNBFH-CB3HiA1_.js +0 -1
  177. package/dist/web-client/assets/eventmodeling-45OFAUF4-DoTBIvl5.js +0 -1
  178. package/dist/web-client/assets/flowDiagram-23GEKE2U-BEH23L1A.js +0 -1
  179. package/dist/web-client/assets/railroad-abnf-AHOZXSZD-nhNub7LE.js +0 -1
  180. package/dist/web-client/assets/railroad-ebnf-EBAXGLYW-BlQYe7Yf.js +0 -1
  181. package/dist/web-client/assets/railroad-peg-LSFZ7HO6-B3E8pRVN.js +0 -1
  182. package/dist/web-client/assets/stateDiagram-v2-6OUMAXLB-DWwTAG1r.js +0 -1
  183. package/dist/web-client/assets/swimlanesDiagram-G3AALYLV-DabrCsjZ.js +0 -8
  184. package/opencode/arcs/skills/install-claude-code-hook/SKILL.md +0 -23
  185. package/scripts/claude-code-session-hook.mjs +0 -146
@@ -1,13 +1,16 @@
1
1
  /**
2
2
  * Session routes — full CRUD over the per-project session index.
3
3
  *
4
- * Sessions are runtime records for claude-code agent sessions attached to a
5
- * project. All mutations go through the locked session-store, so concurrent
6
- * writers (the hook-event bridge and the web UI) cannot clobber each other.
4
+ * Sessions are runtime records for agent threads attached to a project. Every
5
+ * record this module mints is ARCS-origin ("arcs"): a thread ARCS drives itself
6
+ * through one-shot runs, `opencode run` for the default opencode runtime and
7
+ * headless `claude -p` for legacy claude-code threads. All mutations go through
8
+ * the locked session-store, so concurrent writers cannot clobber each other.
7
9
  *
8
- * One route reaches outside the store: `POST /sessions/:id/turns` RUNS a
9
- * headless `claude -p` turn against a thread ARCS owns, forking an observed
10
- * session into such a thread on first contact. Every route here is
10
+ * One route reaches outside the store: `POST /sessions/:id/turns` RUNS a turn.
11
+ * The RUNTIME policy argv shapes and wire format lives in run-driver.ts
12
+ * adapters; the generic lifecycle (spawn, concurrency slot, durable event log,
13
+ * timeout) stays in claude-runner.ts and run-event-log.ts. Every route here is
11
14
  * browser-facing and therefore already behind the global loopback-only
12
15
  * `secureLocalRequest` middleware — no per-route auth.
13
16
  */
@@ -26,12 +29,22 @@ import { isRunLive, liveRunPid, resolveTimeoutMs, runClaudeJob, } from "../claud
26
29
  import { buildPermissionArgv, RUN_INTENTS } from "../permission-policy.js";
27
30
  import { buildStagedEnvironment, planStageRefresh, renderReferences } from "../prompt-assembly.js";
28
31
  import { fail, parseBody, requireProjectDir, respond } from "../respond.js";
32
+ import { getRunDriver } from "../run-driver.js";
29
33
  import { foldRunEventLog, pruneRunEventLogs, RUN_EVENT_LOG_MAX_BYTES, runEventLogPath, } from "../run-event-log.js";
30
34
  import { isProcessAlive, reconcileSessionPhases } from "../session-reconciler.js";
31
35
  export const sessionsRoute = new Hono();
36
+ /**
37
+ * Payload for POST /sessions — one ARCS-owned thread record.
38
+ *
39
+ * `runtimeType` defaults to "opencode": the runtime the run-driver seam exists
40
+ * for. `runtimeSessionId` is optional and only ever NAMES the record when
41
+ * given; the runtime-native id of an opencode thread is unknowable until its
42
+ * first settled run harvests one, so a minted thread starts without it.
43
+ * Creation spawns nothing — the first POST /turns does.
44
+ */
32
45
  const createSessionSchema = z.object({
33
- runtimeType: z.enum(SESSION_RUNTIME_TYPES),
34
- runtimeSessionId: z.string().min(1),
46
+ runtimeType: z.enum(SESSION_RUNTIME_TYPES).default("opencode"),
47
+ runtimeSessionId: z.string().min(1).optional(),
35
48
  status: z.enum(SESSION_STATUSES).optional(),
36
49
  startedAt: z.string().optional(),
37
50
  lastMessageAt: z.string().optional(),
@@ -129,12 +142,13 @@ const sessionReferenceSchema = z
129
142
  * and permission mode `buildPermissionArgv` emits (`ask` → read-only + plan,
130
143
  * `change` → the edit surface + acceptEdits) and decides nothing else.
131
144
  *
132
- * `threadRef` names an ARCS thread RECORD to continue — never a claude uuid,
133
- * and never an observed session (that is what adoption is for). `refs` are the
134
- * turn's references: they render into the user-facing prompt AND land on the
135
- * sidecar. `guards` is validated and then deliberately ignored here; the
136
- * change-intent preflight that reads it is a separate task, and accepting the
137
- * key now keeps that task from being a breaking payload change.
145
+ * `threadRef` names an ARCS thread RECORD to continue — never a runtime-native
146
+ * session id, and never a record ARCS does not own (that is refused, not
147
+ * claimed). `refs` are the turn's references: they render into the user-facing
148
+ * prompt AND land on the sidecar. `guards` is validated and then deliberately
149
+ * ignored here; the change-intent preflight that reads it is a separate task,
150
+ * and accepting the key now keeps that task from being a breaking payload
151
+ * change.
138
152
  */
139
153
  const turnSchema = z.object({
140
154
  intent: z.enum(RUN_INTENTS),
@@ -307,12 +321,10 @@ const THREAD_UNKNOWN_PATTERN = /No conversation found with session ID/i;
307
321
  * - "already in use" — claude HAS the id ARCS thought it had not handed over.
308
322
  * The flag was a false negative; set it and the next turn resumes.
309
323
  * - "No conversation found with session ID" — claude does NOT have the id ARCS
310
- * resumed. Clearing the flag alone would re-seed the SAME uuid, so the uuid
311
- * is re-minted with it. `adoptedClaudeSessionId` is cleared in the same
312
- * write: it is the id claude could not find, and keeping it would re-issue
313
- * the identical doomed `--resume` on every later turn — the exact wedge this
314
- * repair exists to prevent. `adoptedFrom` stays, because the record's
315
- * provenance is still true and never reaches argv.
324
+ * resumed. Clearing the flag alone would re-seed the SAME uuid, so a fresh
325
+ * one is minted with it: keeping the old id would re-issue the identical
326
+ * doomed `--resume` on every later turn the exact wedge this repair exists
327
+ * to prevent.
316
328
  */
317
329
  function repairThreadSeed(record) {
318
330
  const error = typeof record.error === "string" ? record.error : "";
@@ -327,7 +339,6 @@ function repairThreadSeed(record) {
327
339
  metadata: {
328
340
  threadInitialized: false,
329
341
  claudeSessionId: randomUUID(),
330
- adoptedClaudeSessionId: "",
331
342
  },
332
343
  };
333
344
  }
@@ -340,14 +351,23 @@ function repairThreadSeed(record) {
340
351
  *
341
352
  * Every write target is an ARCS-owned thread, so there is exactly one sidecar
342
353
  * discipline: `appendSessionTurn`-owned, never mirrored. The run's own event log
343
- * folds down first (assistant text plus one turn per tool_use, every turn tagged
344
- * with the run id so a second fold is a no-op). Only when that fold produced no
345
- * assistant text no log, an empty log, a child that spoke only through the
346
- * terminal `result` envelope does the captured reply get appended as an
347
- * assistant turn on a success outcome; error/timeout outcomes append nothing.
354
+ * folds down first (assistant text plus one turn per tool call, every turn
355
+ * tagged with the run id so a second fold is a no-op) through the write
356
+ * target's own driver normalizer when it has one, so an opencode log's
357
+ * `{type, sessionID, part}` lines fold instead of being read as claude events.
358
+ * Only for claude-code, and only when that fold produced no assistant text — no
359
+ * log, an empty log, a child that spoke only through the terminal `result`
360
+ * envelope — does the captured reply get appended as an assistant turn on a
361
+ * success outcome; error/timeout outcomes append nothing. A driver-driven run
362
+ * never appends the captured reply: the runner's reader does not speak that
363
+ * wire format, so its fallback "reply" is raw NDJSON, while the durable log the
364
+ * fold just read IS the reply when there was one.
348
365
  *
349
- * The observed session an adopted thread was forked from is never touched here:
350
- * `ctx.writeTarget` is the fork, and the fork's transcript is its own.
366
+ * When the fold harvested a runtime-native session id onto a thread that has
367
+ * none (an opencode first turn), it is persisted BEFORE the settle releases the
368
+ * claim — the next turn must continue this runtime session, not fork a fresh
369
+ * one. The thread's `lastMessageAt` moves to the settle too, and a non-terminal
370
+ * status is re-stamped active: the thread was just driven.
351
371
  *
352
372
  * Every path finalizes metadata.run with the settled record (pid/startedAt/
353
373
  * mode plus endedAt/outcome/error/replyChars) so the panel shows the true
@@ -366,8 +386,24 @@ async function writeBackRun(projectDir, ctx, record) {
366
386
  // Fold the run's durable event log down into the sidecar first. Idempotent by
367
387
  // its own output — every folded turn carries the run id, and a run already
368
388
  // represented there folds to nothing.
369
- const fold = await foldRunEventLog(projectDir, ctx.writeTarget.normalizedId, ctx.runId);
370
- if (!fold.assistantTextFolded && record.outcome === "success" && record.replyText !== undefined) {
389
+ const fold = await foldRunEventLog(projectDir, ctx.writeTarget.normalizedId, ctx.runId, {
390
+ runtimeType: ctx.writeTarget.runtimeType,
391
+ });
392
+ // The harvested runtime session id lands before the settle: from the moment
393
+ // the claim is released the next turn is accepted, and it has to see the id
394
+ // or it mints a fresh runtime thread instead of continuing this one.
395
+ if (fold.runtimeSessionId !== undefined &&
396
+ ctx.writeTarget.runtimeSessionId.trim() === "" &&
397
+ ctx.writeTarget.normalizedId !== fold.runtimeSessionId) {
398
+ await updateSession(projectDir, {
399
+ id: ctx.writeTarget.normalizedId,
400
+ runtimeSessionId: fold.runtimeSessionId,
401
+ });
402
+ }
403
+ if (!fold.assistantTextFolded &&
404
+ record.outcome === "success" &&
405
+ record.replyText !== undefined &&
406
+ ctx.writeTarget.runtimeType === "claude-code") {
371
407
  // Nothing in the log spoke for this run (no log at all, or only tool
372
408
  // turns): the captured reply lands in the sidecar as an assistant turn,
373
409
  // minted in the shared negative id space after the user turn and any
@@ -413,9 +449,14 @@ async function writeBackRun(projectDir, ctx, record) {
413
449
  // Typed, so the panel can act on the failure rather than render opaque
414
450
  // CLI text at the user.
415
451
  ...(repair.errorCode !== undefined && { errorCode: repair.errorCode }),
416
- ...(record.replyChars !== undefined && { replyChars: record.replyChars }),
452
+ // Reply size and reader drift are CLAUDE READER observations — the
453
+ // built-in reader does not speak a driver runtime's wire format, so its
454
+ // fallback reply length and skip count would only lie about one.
455
+ ...(ctx.writeTarget.runtimeType === "claude-code" &&
456
+ record.replyChars !== undefined && { replyChars: record.replyChars }),
417
457
  ...(record.firstTokenAt !== undefined && { firstTokenAt: record.firstTokenAt }),
418
- ...(record.skippedLines !== undefined && { skippedLines: record.skippedLines }),
458
+ ...(ctx.writeTarget.runtimeType === "claude-code" &&
459
+ record.skippedLines !== undefined && { skippedLines: record.skippedLines }),
419
460
  ...(record.eventLogLines !== undefined && { eventLogLines: record.eventLogLines }),
420
461
  // Whether the log is the WHOLE stream. `eventLogLines` alone cannot say:
421
462
  // a log capped on its first chunk reports zero lines, the same number a
@@ -430,6 +471,17 @@ async function writeBackRun(projectDir, ctx, record) {
430
471
  },
431
472
  ...(Object.keys(repair.metadata).length > 0 && { metadata: repair.metadata }),
432
473
  });
474
+ // The thread was just driven: its last message is this run's end, and a
475
+ // non-terminal status is re-stamped active. Terminal statuses are never
476
+ // reopened by a run — the same rule the phase derivation enforces.
477
+ const terminal = ctx.writeTarget.status === "completed" ||
478
+ ctx.writeTarget.status === "failed" ||
479
+ ctx.writeTarget.status === "disconnected";
480
+ await updateSession(projectDir, {
481
+ id: ctx.writeTarget.normalizedId,
482
+ lastMessageAt: new Date(record.endedAt ?? Date.now()).toISOString(),
483
+ ...(terminal ? {} : { status: "active" }),
484
+ });
433
485
  }
434
486
  sessionsRoute.get("/api/p/:slug/sessions", async (c) => respond(c, async () => {
435
487
  const projectDir = requireProjectDir(c.req.param("slug"));
@@ -437,9 +489,30 @@ sessionsRoute.get("/api/p/:slug/sessions", async (c) => respond(c, async () => {
437
489
  return { sessions: await withPhases(projectDir, sessions) };
438
490
  }));
439
491
  sessionsRoute.post("/api/p/:slug/sessions", async (c) => respond(c, async () => {
440
- const projectDir = requireProjectDir(c.req.param("slug"));
492
+ const slug = c.req.param("slug");
493
+ const projectDir = requireProjectDir(slug);
441
494
  const input = await parseBody(c, createSessionSchema);
442
- return createSession(projectDir, input);
495
+ // ARCS-origin only, and the name is minted unless the caller supplies
496
+ // one: provenance is never client-settable, and a thread without a
497
+ // runtime-native id still needs a stable record key from birth. The
498
+ // supplied name keys the RECORD only — it never seeds
499
+ // `runtimeSessionId`, which stays blank until the runtime itself
500
+ // produces one (a harvested opencode session id) or the claude path
501
+ // mints its uuid into metadata at first spawn.
502
+ const threadName = input.runtimeSessionId ?? `arcs-thread-${slug}-${randomUUID()}`;
503
+ // The workspace is resolved NOW so every later turn spawns in the same
504
+ // directory even if the project's registered paths change in between.
505
+ const directory = metadataString(input.metadata?.directory) ?? (await primaryWorkspacePath(projectDir, slug));
506
+ return createSession(projectDir, {
507
+ runtimeType: input.runtimeType,
508
+ recordName: threadName,
509
+ origin: "arcs",
510
+ status: input.status,
511
+ startedAt: input.startedAt,
512
+ lastMessageAt: input.lastMessageAt,
513
+ userEmail: input.userEmail,
514
+ metadata: { control: "arcs-owned", directory, ...input.metadata },
515
+ });
443
516
  }, 201));
444
517
  sessionsRoute.get("/api/p/:slug/sessions/:id", async (c) => respond(c, async () => {
445
518
  const projectDir = requireProjectDir(c.req.param("slug"));
@@ -460,11 +533,10 @@ sessionsRoute.patch("/api/p/:slug/sessions/:id", async (c) => respond(c, async (
460
533
  * folds an unreadable index into an empty one (`readJsonSafe` swallows every
461
534
  * error class), so an EACCES or an EISDIR on a live index arrives wearing the
462
535
  * deleted record's code. Minting on that answer would upsert a fresh ARCS
463
- * thread over a name that already belongs to something else and worse, would
464
- * seed a uuid onto a thread claude already knows. So a not-found is believed
465
- * only when a DIRECT read of the index says this record is not listed (or that
466
- * there is no index at all); anything else is reported as unavailable and the
467
- * caller retries.
536
+ * thread over a name that already belongs to something else. So a not-found is
537
+ * believed only when a DIRECT read of the index says this record is not listed
538
+ * (or that there is no index at all); anything else is reported as unavailable
539
+ * and the caller retries.
468
540
  */
469
541
  async function readThreadRecord(projectDir, threadRef) {
470
542
  try {
@@ -477,111 +549,72 @@ async function readThreadRecord(projectDir, threadRef) {
477
549
  if (await sessionIndexAnswered(projectDir, normalizeIdentifier(threadRef)))
478
550
  return undefined;
479
551
  throw new DagError("SESSION_INDEX_UNAVAILABLE", `cannot tell whether thread "${threadRef}" exists — the session index did not answer for ` +
480
- `it, and minting a second record over that name would re-seed a uuid claude may already ` +
481
- `hold. Retry once the index reads.`);
552
+ `it, and minting a second record over that name would collide with whatever holds the ` +
553
+ `name today. Retry once the index reads.`);
482
554
  }
483
555
  /**
484
- * Resolves the record this turn writes to, the uuid claude is told, and whether
485
- * this spawn claims that uuid or continues it.
556
+ * Mints a fresh ARCS thread for a `threadRef` that names nothing yet. The
557
+ * runtime defaults to "opencode" the same default POST /sessions applies — so
558
+ * every minted-by-turn thread is drivable by the run-driver seam from birth.
559
+ */
560
+ async function mintThread(projectDir, slug, threadName) {
561
+ const dir = await primaryWorkspacePath(projectDir, slug);
562
+ return upsertSession(projectDir, {
563
+ runtimeType: "opencode",
564
+ recordName: threadName,
565
+ origin: "arcs",
566
+ metadata: { control: "arcs-owned", directory: dir },
567
+ });
568
+ }
569
+ /**
570
+ * Resolves the record this turn writes to.
486
571
  *
487
- * Three branches, and only these three:
572
+ * Two branches, and only these two:
488
573
  *
489
574
  * 1. `threadRef` — the caller names an ARCS thread RECORD to continue. It may
490
- * name one that does not exist yet (a fresh thread is minted for it), but
491
- * never an observed session: that is refused rather than quietly claimed,
492
- * because ARCS writing run state onto a record with a live terminal behind
493
- * it is exactly what adoption exists to avoid.
494
- * 2. the referenced session IS an ARCS thread — continue it.
495
- * 3. ADOPTION — the referenced session is one ARCS merely observes, so a NEW
496
- * thread is minted and its first spawn forks the observed session:
497
- * `--resume <observed uuid> --session-id <fresh uuid> --fork-session`.
498
- * Probed on claude 2.1.223: the fork inherits the observed context and
499
- * writes a SEPARATE transcript, leaving the original untouched — so
500
- * adoption preserves continuity without needing to read a claude-chosen id
501
- * back off the stream.
575
+ * name one that does not exist yet (a fresh thread is minted for it), but a
576
+ * record that exists and is not ARCS-owned is refused rather than claimed.
577
+ * 2. no `threadRef` the addressed session itself must be an ARCS-owned
578
+ * thread; it is continued in place.
502
579
  *
503
- * Adoption is deliberately NOT idempotent: every turn addressed to the observed
504
- * session forks it again, from whatever state the human has left it in. The
505
- * 202 names the write target, and continuing THAT thread (by id, or by
506
- * `threadRef`) is how a conversation accumulates in one sidecar.
580
+ * There is no third branch. Turns used to ADOPT an observed session by forking
581
+ * it into a new thread (`--resume <observed> --session-id <fresh>
582
+ * --fork-session`); with the hook bridge gone there are no observed sessions
583
+ * left to adopt, so anything that is not an ARCS thread is refused outright.
507
584
  */
508
585
  async function resolveTurnTarget(projectDir, slug, session, threadRef) {
509
- let existing;
510
- let threadId;
511
- /** Provenance, written only on the upsert that MINTS an adopted thread. */
512
- let adoptedFrom;
513
- let adoptedClaudeSessionId;
586
+ let writeTarget;
514
587
  if (threadRef !== undefined) {
515
- threadId = threadRef;
516
- existing = await readThreadRecord(projectDir, threadRef);
588
+ const existing = await readThreadRecord(projectDir, threadRef);
517
589
  if (existing !== undefined && existing.origin !== "arcs") {
518
- throw new DagError("TURN_THREAD_NOT_OWNED", `cannot continue thread "${existing.normalizedId}": it is a session ARCS observes, not ` +
519
- `an ARCS-owned thread — omit threadRef to fork it into one instead`);
590
+ throw new DagError("TURN_THREAD_NOT_OWNED", `cannot continue thread "${existing.normalizedId}": it is not an ARCS-owned thread`);
520
591
  }
521
- }
522
- else if (session.origin === "arcs") {
523
- threadId = session.runtimeSessionId;
524
- existing = session;
592
+ writeTarget = existing ?? (await mintThread(projectDir, slug, threadRef));
525
593
  }
526
594
  else {
527
- threadId = `arcs-thread-${slug}-${randomUUID()}`;
528
- adoptedFrom = session.normalizedId;
529
- adoptedClaudeSessionId = session.runtimeSessionId;
595
+ if (session.origin !== "arcs") {
596
+ throw new DagError("TURN_THREAD_NOT_OWNED", `cannot run a turn on "${session.normalizedId}": it is not an ARCS-owned thread — ` +
597
+ `create one with POST /api/p/${slug}/sessions and address turns to it`);
598
+ }
599
+ writeTarget = session;
530
600
  }
531
- const meta = existing?.metadata;
532
- const persistedUuid = metadataString(meta?.claudeSessionId);
533
- // The SEED DECISION. `threadInitialized` means "ARCS has already handed this
534
- // uuid to --session-id" and is persisted at spawn, so a thread whose first
535
- // run timed out (or whose server died) resumes on the next turn instead of
536
- // re-seeding an id claude has already registered.
537
- const seeding = meta?.threadInitialized !== true || persistedUuid === undefined;
538
- const claudeSessionId = persistedUuid ?? randomUUID();
539
- // A fork is only ever the spawn that CLAIMS the new id; once the thread is
540
- // seeded it continues on its own uuid and the observed session is done with.
541
- const forkFrom = seeding
542
- ? (adoptedClaudeSessionId ?? metadataString(meta?.adoptedClaudeSessionId))
543
- : undefined;
544
- // HARD SAFETY ASSERTION. `--resume A --session-id A --fork-session` is
545
- // ACCEPTED by claude 2.1.223 — exit 0, empty stderr — and appends to the
546
- // ORIGINAL transcript, hijacking the human's live terminal thread (measured:
547
- // the source grew 11 -> 20 lines). There is no error to notice it by, so the
548
- // only thing standing between a corrupt record and a hijacked session is this
549
- // check. Unreachable from a freshly minted uuid; reachable from a hand-edited
550
- // or half-repaired index, which is precisely why it is enforced here.
551
- if (forkFrom !== undefined && forkFrom === claudeSessionId) {
552
- throw new DagError("CLAUDE_FORK_ID_COLLISION", `refusing to fork thread "${threadId}": its claude session id and the session it would ` +
553
- `fork are the same id ("${forkFrom}"), which claude accepts silently and appends to the ` +
554
- `ORIGINAL session — re-mint metadata.claudeSessionId before running this thread again`);
601
+ const persistedDir = sessionDirectory(writeTarget);
602
+ const dir = persistedDir ?? (await primaryWorkspacePath(projectDir, slug));
603
+ if (persistedDir === undefined) {
604
+ // Pin the workspace on first contact so later turns spawn in the same
605
+ // directory even if the project's registered paths change in between.
606
+ await updateSession(projectDir, { id: writeTarget.normalizedId, metadata: { directory: dir } });
607
+ writeTarget = { ...writeTarget, metadata: { ...writeTarget.metadata, directory: dir } };
555
608
  }
556
- const dir = sessionDirectory(existing ?? session) ?? (await primaryWorkspacePath(projectDir, slug));
557
- const writeTarget = await upsertSession(projectDir, {
558
- runtimeType: "claude-code",
559
- runtimeSessionId: threadId,
560
- // Create-only in the store: an upsert never rewrites provenance, so this
561
- // marks a freshly minted thread and is inert on one that already exists.
562
- origin: "arcs",
563
- metadata: {
564
- control: "arcs-owned",
565
- directory: dir,
566
- claudeSessionId,
567
- ...(adoptedFrom !== undefined && { adoptedFrom, adoptedClaudeSessionId }),
568
- },
569
- });
570
- return {
571
- writeTarget,
572
- claudeSessionId,
573
- ...(forkFrom !== undefined && { forkFrom }),
574
- seeding,
575
- dir,
576
- };
609
+ return { writeTarget, dir };
577
610
  }
578
611
  /**
579
612
  * The turn's user-facing prompt: the message, then its rendered reference block.
580
613
  *
581
- * References ride the PROMPT, never `--append-system-prompt`. The system tier is
582
- * the STABLE one — byte-identical across turns is what makes the prompt cache
583
- * pay — while a reference belongs to the turn that sent it and to no other.
584
- * Staging them would break the cache on every send and leave the pointer in the
614
+ * References ride the PROMPT, never a system tier. The system tier is the
615
+ * STABLE one — byte-identical across turns is what makes the prompt cache pay —
616
+ * while a reference belongs to the turn that sent it and to no other. Staging
617
+ * them would break the cache on every send and leave the pointer in the
585
618
  * conversation long after the turn it was meant for.
586
619
  */
587
620
  function turnPrompt(message, refs) {
@@ -589,59 +622,52 @@ function turnPrompt(message, refs) {
589
622
  return block === "" ? message : `${message}\n\n${block}`;
590
623
  }
591
624
  /**
592
- * Targeting tokens for one spawn — everything before the permission segment.
593
- *
594
- * Exactly three shapes, and the fork's ORDER is the probed one:
625
+ * Targeting tokens for one legacy claude-code spawn — everything before the
626
+ * permission segment. Exactly two shapes:
595
627
  * - fresh thread seed: -p <prompt> --session-id <new> --output-format json
596
628
  * - thread resume: -p <prompt> --resume <own> --output-format json
597
- * - observed adoption: -p <prompt> --resume <observed> --session-id <new>
598
- * --fork-session --output-format json
599
629
  *
600
- * `--session-id` alongside `--resume` without `--fork-session` exits 1 at flag
601
- * validation ("--session-id can only be used with --continue or --resume if
602
- * --fork-session is also specified"), so the three tokens are never split.
630
+ * (`--resume <observed> --fork-session` the adoption fork — is gone with the
631
+ * observed sessions it forked.)
603
632
  */
604
- function turnTargetingArgv(prompt, target) {
633
+ function turnTargetingArgv(prompt, seeding, claudeSessionId) {
605
634
  const argv = ["-p", prompt];
606
- if (target.seeding && target.forkFrom !== undefined) {
607
- argv.push("--resume", target.forkFrom, "--session-id", target.claudeSessionId, "--fork-session");
608
- }
609
- else if (target.seeding) {
610
- argv.push("--session-id", target.claudeSessionId);
611
- }
612
- else {
613
- argv.push("--resume", target.claudeSessionId);
614
- }
635
+ argv.push(seeding ? "--session-id" : "--resume", claudeSessionId);
615
636
  argv.push("--output-format", "json");
616
637
  return argv;
617
638
  }
618
639
  /**
619
- * One turn of a headless `claude -p` conversation. Answers 202 with the run's
620
- * id, the stream to tail it on, and the record it writes to — the acceptance,
621
- * not the result: the run proceeds out-of-band in the runner, whose exit-time
622
- * write-back settles it.
640
+ * One turn of a headless conversation. Answers 202 with the run's id, the
641
+ * stream to tail it on, and the record it writes to — the acceptance, not the
642
+ * result: the run proceeds out-of-band in the runner, whose exit-time write-back
643
+ * settles it.
623
644
  *
624
645
  * WHAT THE CALLER CHOOSES is an INTENT (`ask` | `change`), never a targeting
625
646
  * mode. Where the turn lands is derived from the record it is addressed to (see
626
- * `resolveTurnTarget`): an ARCS thread continues, an observed session is forked
627
- * into a new one.
647
+ * `resolveTurnTarget`): an ARCS thread continues in place.
648
+ *
649
+ * RUNTIME SELECTION is the write target's own `runtimeType`, read through the
650
+ * run-driver registry: a thread whose runtime has a registered adapter (the
651
+ * default, opencode) is driven one-shot through that adapter — its argv, its
652
+ * binary, its wire format; a thread without one (legacy claude-code) keeps the
653
+ * claude path below.
628
654
  *
629
- * ARGV OWNERSHIP, which is the safety property: every tool and permission token
630
- * comes from `buildPermissionArgv` and this route builds none. It keeps only
631
- * the targeting tokens above, and the permission segment is appended LAST —
632
- * `--tools` is variadic (it eats following tokens until the next dash-leading
633
- * one) and `--append-system-prompt` consumes exactly one following token, so a
634
- * segment placed before `-p` would swallow the prompt or the staged text. The
635
- * staged environment reaches the child through that segment's
655
+ * ARGV OWNERSHIP, which is the safety property on the claude path: every tool
656
+ * and permission token comes from `buildPermissionArgv` and this route builds
657
+ * none. It keeps only the targeting tokens above, and the permission segment is
658
+ * appended LAST — `--tools` is variadic (it eats following tokens until the next
659
+ * dash-leading one) and `--append-system-prompt` consumes exactly one following
660
+ * token, so a segment placed before `-p` would swallow the prompt or the staged
661
+ * text. The staged environment reaches the child through that segment's
636
662
  * `stagedSystemPrompt` slot and nowhere else; a second direct push would emit
637
- * the flag twice.
663
+ * the flag twice. A driver-driven run carries NO permission segment — those
664
+ * flags are claude's vocabulary, and the adapter's argv is complete on its own.
638
665
  *
639
666
  * The user turn (and one reference turn per `refs` entry) is appended to the
640
667
  * write target's sidecar immediately, so the panel shows the prompt before the
641
668
  * run ends, with delivery-first ordering.
642
669
  *
643
- * Staged environment (W2): a spawn that STARTS a conversation — a fresh seed and
644
- * an adoption fork alike, since the forked context has never seen ARCS's block —
670
+ * Staged environment (W2): claude-code only. A spawn that STARTS a conversation
645
671
  * always carries it; a spawn that CONTINUES one carries it only on a restage.
646
672
  *
647
673
  * Concurrency: one live run per write-target. The runner's beginRun is the
@@ -656,10 +682,12 @@ sessionsRoute.post("/api/p/:slug/sessions/:id/turns", async (c) => respond(c, as
656
682
  const { intent, message, refs, threadRef } = await parseBody(c, turnSchema);
657
683
  const session = await getSession(projectDir, c.req.param("id"));
658
684
  const target = await resolveTurnTarget(projectDir, slug, session, threadRef);
659
- const { writeTarget, dir, seeding } = target;
660
- // One live run per write-target — refuse before appending anything.
685
+ const { writeTarget, dir } = target;
686
+ // One live run per write-target — refuse before appending anything. The
687
+ // CODE is historical (claude was the only drivable runtime when it was
688
+ // minted); both runtimes share it so clients keep one overlap signal.
661
689
  if (isRunLive(writeTarget.normalizedId)) {
662
- throw new DagError("CLAUDE_RUN_IN_PROGRESS", `a claude run for "${writeTarget.normalizedId}" is already in progress`);
690
+ throw new DagError("CLAUDE_RUN_IN_PROGRESS", `a run for "${writeTarget.normalizedId}" is already in progress`);
663
691
  }
664
692
  await appendSessionTurn(projectDir, writeTarget.normalizedId, {
665
693
  type: "user",
@@ -668,39 +696,84 @@ sessionsRoute.post("/api/p/:slug/sessions/:id/turns", async (c) => respond(c, as
668
696
  for (const reference of refs ?? []) {
669
697
  await appendReference(projectDir, writeTarget.normalizedId, reference);
670
698
  }
671
- // NOTE: no "--cwd" flag — claude >= 2.x rejects it ("error: unknown
672
- // option '--cwd'"), settling every headless run as outcome:error. The
673
- // spawn applies the working directory via options.cwd below instead.
674
- const argv = turnTargetingArgv(turnPrompt(message, refs), target);
675
- // --- Staged environment (W2) -----------------------------------------
676
- //
677
- // Keyed on the WRITE TARGET, never on the referenced session: the write
678
- // target is the record the run lands on and the record `metadata.stage`
679
- // is persisted to, so the fingerprint compared next turn describes the
680
- // same node the text was built from.
681
- const stageOpts = { workspaceRoot: dir };
682
- const refresh = await planStageRefresh(projectDir, slug, writeTarget, stageOpts);
683
- const staged = refresh.staged ??
684
- (seeding
685
- ? await buildStagedEnvironment(projectDir, slug, writeTarget, {
686
- ...stageOpts,
687
- // The same watermark planStageRefresh stamps with, so this record
688
- // stays mtime-comparable even though this path never persists it.
689
- now: refresh.probedAt,
690
- })
691
- : undefined);
692
- // LAST, and the only source of tool/permission tokens. The staged text is
693
- // handed over as this segment's value slot rather than pushed directly —
694
- // one flag, one emitter.
695
- argv.push(...buildPermissionArgv({
696
- intent,
697
- ...(staged !== undefined && { stagedSystemPrompt: staged.text }),
698
- }));
699
699
  // The run's own ceiling, resolved HERE so the deadline persisted with the
700
700
  // claim is the same number the runner arms its kill timer with (it
701
701
  // prefers this over its own env/default lookup).
702
702
  const runId = randomUUID();
703
703
  const timeoutMs = resolveTimeoutMs(undefined, process.env);
704
+ // --- Per-runtime child shape -------------------------------------------
705
+ //
706
+ // A registered driver adapter owns everything runtime-specific about the
707
+ // spawn: argv shape, binary, wire format. What stays here is the generic
708
+ // lifecycle — deadline, claim, durable log, write-back — identical for
709
+ // every runtime.
710
+ const driver = getRunDriver(writeTarget.runtimeType);
711
+ let argv;
712
+ let runnerOptions;
713
+ /** Sibling metadata persisted with the deadline below, per runtime. */
714
+ let spawnMetadata = {};
715
+ if (driver !== undefined) {
716
+ // One-shot driver runtime (opencode): FRESH when no runtime session id
717
+ // has been harvested yet, `-s` continuation once one has. No permission
718
+ // segment and no staged tier — the adapter's argv is complete policy,
719
+ // and the runner must not rewrite it onto its stream-json contract.
720
+ const runtimeSessionId = writeTarget.runtimeSessionId.trim();
721
+ argv = driver.buildArgv({
722
+ message: turnPrompt(message, refs),
723
+ title: metadataString(writeTarget.metadata?.title),
724
+ ...(runtimeSessionId !== "" && { runtimeSessionId }),
725
+ });
726
+ runnerOptions = { binary: driver.binary };
727
+ }
728
+ else {
729
+ // Legacy claude-code thread — seed-or-resume decision, targeting tokens,
730
+ // staged environment, then the permission segment LAST.
731
+ const meta = writeTarget.metadata;
732
+ const persistedUuid = metadataString(meta?.claudeSessionId);
733
+ // The SEED DECISION. `threadInitialized` means "ARCS has already handed
734
+ // this uuid to --session-id" and is persisted at spawn, so a thread
735
+ // whose first run timed out (or whose server died) resumes on the next
736
+ // turn instead of re-seeding an id claude has already registered.
737
+ const seeding = meta?.threadInitialized !== true || persistedUuid === undefined;
738
+ const claudeSessionId = persistedUuid ?? randomUUID();
739
+ // NOTE: no "--cwd" flag — claude >= 2.x rejects it ("error: unknown
740
+ // option '--cwd'"), settling every headless run as outcome:error. The
741
+ // spawn applies the working directory via options.cwd below instead.
742
+ argv = turnTargetingArgv(turnPrompt(message, refs), seeding, claudeSessionId);
743
+ // Keyed on the WRITE TARGET, never on anything else: the write target
744
+ // is the record the run lands on and the record `metadata.stage` is
745
+ // persisted to, so the fingerprint compared next turn describes the
746
+ // same node the text was built from.
747
+ const stageOpts = { workspaceRoot: dir };
748
+ const refresh = await planStageRefresh(projectDir, slug, writeTarget, stageOpts);
749
+ const staged = refresh.staged ??
750
+ (seeding
751
+ ? await buildStagedEnvironment(projectDir, slug, writeTarget, {
752
+ ...stageOpts,
753
+ // The same watermark planStageRefresh stamps with, so this record
754
+ // stays mtime-comparable even though this path never persists it.
755
+ now: refresh.probedAt,
756
+ })
757
+ : undefined);
758
+ // LAST, and the only source of tool/permission tokens. The staged text
759
+ // is handed over as this segment's value slot rather than pushed
760
+ // directly — one flag, one emitter.
761
+ argv.push(...buildPermissionArgv({
762
+ intent,
763
+ ...(staged !== undefined && { stagedSystemPrompt: staged.text }),
764
+ }));
765
+ spawnMetadata = {
766
+ // Minted once here and persisted AT SPAWN, so a crash before the
767
+ // settle still leaves the thread resuming the uuid it was seeded
768
+ // with rather than re-seeding a second one.
769
+ claudeSessionId,
770
+ ...(seeding && { threadInitialized: true }),
771
+ // Written EXACTLY when the refresh asks for it. On the cheap exit
772
+ // nothing was rebuilt, so re-stamping the record would move the very
773
+ // watermark the next turn's freshness decision is made against.
774
+ ...(refresh.persist && refresh.stage ? { stage: refresh.stage } : {}),
775
+ };
776
+ }
704
777
  // Persisted next to the claim rather than inside metadata.run, which
705
778
  // `beginSessionRun` replaces wholesale: as sibling keys these cannot be
706
779
  // clobbered by the claim, nor the claim by them.
@@ -708,16 +781,7 @@ sessionsRoute.post("/api/p/:slug/sessions/:id/turns", async (c) => respond(c, as
708
781
  id: writeTarget.normalizedId,
709
782
  metadata: {
710
783
  runDeadlineAt: Date.now() + timeoutMs,
711
- // AT SPAWN, not at settle. The uuid is in argv from this point on, so
712
- // "ARCS has handed it over" is true now and stays true through a
713
- // timeout, an error, or a server that dies before the settle runs —
714
- // the three ways the old settle-time write left a thread re-seeding
715
- // forever into "Session ID … is already in use".
716
- ...(seeding && { threadInitialized: true }),
717
- // Written EXACTLY when the refresh asks for it. On the cheap exit
718
- // nothing was rebuilt, so re-stamping the record would move the very
719
- // watermark the next turn's freshness decision is made against.
720
- ...(refresh.persist && refresh.stage ? { stage: refresh.stage } : {}),
784
+ ...spawnMetadata,
721
785
  },
722
786
  });
723
787
  // Claim the record BEFORE the child exists: from here on, a server that
@@ -740,11 +804,14 @@ sessionsRoute.post("/api/p/:slug/sessions/:id/turns", async (c) => respond(c, as
740
804
  cwd: dir,
741
805
  timeoutMs,
742
806
  writeTargetKey: writeTarget.normalizedId,
807
+ // A driver runtime owns its own wire format: its argv reaches the
808
+ // child verbatim, never rewritten onto the claude output contract.
809
+ ...(driver !== undefined && { streamJsonArgv: false }),
743
810
  // The SAME runId the claim above persisted as currentRunId — the log's
744
811
  // filename and the session record can never name different runs.
745
812
  eventLog: { projectDir, sessionId: writeTarget.normalizedId, runId },
746
813
  onSettled: (record) => writeBackRun(projectDir, { intent, writeTarget, runId, claimed }, record),
747
- }).catch(() => {
814
+ }, runnerOptions).catch(() => {
748
815
  // Best-effort — the write-back lives inside the runner's onSettled.
749
816
  });
750
817
  // runClaudeJob spawns synchronously (nothing is awaited before its
@@ -768,10 +835,10 @@ sessionsRoute.post("/api/p/:slug/sessions/:id/turns", async (c) => respond(c, as
768
835
  }
769
836
  return {
770
837
  runId,
771
- // Keyed on the WRITE TARGET's id, never on the path `:id`: under
772
- // adoption they are different records, and a stream URL built from the
773
- // path id answers 200 and then emits nothing — indistinguishable from a
774
- // child that never spoke.
838
+ // Keyed on the WRITE TARGET's id, never on the path `:id`: when
839
+ // `threadRef` mints a thread they can differ, and a stream URL built
840
+ // from the path id answers 200 and then emits nothing —
841
+ // indistinguishable from a child that never spoke.
775
842
  streamUrl: `/api/p/${slug}/sessions/${writeTarget.normalizedId}/runs/${runId}/stream`,
776
843
  writeTargetId: writeTarget.normalizedId,
777
844
  };