@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.
- package/README.md +2 -2
- package/dist/cli/arcs-flash.d.ts +1 -1
- package/dist/cli/arcs-flash.d.ts.map +1 -1
- package/dist/cli/arcs-flash.js +3 -5
- package/dist/cli/arcs-flash.js.map +1 -1
- package/dist/cli/arcs-orchestrate.d.ts +1 -1
- package/dist/cli/arcs-orchestrate.d.ts.map +1 -1
- package/dist/cli/commands/index.d.ts +0 -1
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +0 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/project.js +1 -30
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/proposal-doc.d.ts +2 -0
- package/dist/cli/commands/proposal-doc.d.ts.map +1 -0
- package/dist/cli/commands/proposal-doc.js +381 -0
- package/dist/cli/commands/proposal-doc.js.map +1 -0
- package/dist/cli/commands/web.js +4 -10
- package/dist/cli/commands/web.js.map +1 -1
- package/dist/cli/orchestrator-shared-blocks.d.ts +11 -10
- package/dist/cli/orchestrator-shared-blocks.d.ts.map +1 -1
- package/dist/cli/orchestrator-shared-blocks.js +21 -16
- package/dist/cli/orchestrator-shared-blocks.js.map +1 -1
- package/dist/utils/graphify-knowledge.d.ts +22 -0
- package/dist/utils/graphify-knowledge.d.ts.map +1 -0
- package/dist/utils/graphify-knowledge.js +47 -0
- package/dist/utils/graphify-knowledge.js.map +1 -0
- package/dist/utils/graphify.d.ts +104 -0
- package/dist/utils/graphify.d.ts.map +1 -0
- package/dist/utils/graphify.js +439 -0
- package/dist/utils/graphify.js.map +1 -0
- package/dist/utils/session-store.d.ts +24 -2
- package/dist/utils/session-store.d.ts.map +1 -1
- package/dist/utils/session-store.js +16 -7
- package/dist/utils/session-store.js.map +1 -1
- package/dist/utils/storage-utils.d.ts +1 -1
- package/dist/utils/storage-utils.d.ts.map +1 -1
- package/dist/utils/storage-utils.js +1 -1
- package/dist/utils/storage-utils.js.map +1 -1
- package/dist/web-client/assets/{GraphCanvas-BPDgvsyT.js → GraphCanvas-Dw6EcoDb.js} +1 -1
- package/dist/web-client/assets/{MarkdownEditor-D7TLp78z.js → MarkdownEditor-Cz5_44Ej.js} +1 -1
- package/dist/web-client/assets/{abnfDiagram-VRR7QNED-CyuP2N9t.js → abnfDiagram-VRR7QNED-CFJzLuew.js} +1 -1
- package/dist/web-client/assets/architecture-TIHT7OUA-CoHvhex9.js +1 -0
- package/dist/web-client/assets/{architectureDiagram-ZJ3FMSHR-DZ0ul9QX.js → architectureDiagram-ZJ3FMSHR-FRlSgnnW.js} +1 -1
- package/dist/web-client/assets/{blockDiagram-677ZJIJ3-LLGzlc9l.js → blockDiagram-677ZJIJ3-WUkkunPZ.js} +1 -1
- package/dist/web-client/assets/{c4Diagram-LMCZKHZV-CViu3CTc.js → c4Diagram-LMCZKHZV-BKeAsL6F.js} +1 -1
- package/dist/web-client/assets/channel-D8xMXC5_.js +1 -0
- package/dist/web-client/assets/{chunk-32BRIVSS-Bw_IuJCM.js → chunk-32BRIVSS-B0b9kUcF.js} +1 -1
- package/dist/web-client/assets/{chunk-52WLFC77-C29h440W.js → chunk-52WLFC77-i6Y-vfL3.js} +1 -1
- package/dist/web-client/assets/{chunk-C7G6YPKG-hhOrvw5w.js → chunk-C7G6YPKG-atYWm6iP.js} +1 -1
- package/dist/web-client/assets/{chunk-EX3LRPZG-COMzol-M.js → chunk-EX3LRPZG-CZD6y0UU.js} +1 -1
- package/dist/web-client/assets/{chunk-FWX5IMBZ-6vdX9EUn.js → chunk-FWX5IMBZ-CVErR-UZ.js} +2 -2
- package/dist/web-client/assets/{chunk-HOUHSVGY-DWDW6sxp.js → chunk-HOUHSVGY-BlHO2iQ3.js} +1 -1
- package/dist/web-client/assets/{chunk-ICXQ74PX-BdMYglo2.js → chunk-ICXQ74PX-Ar8kfXfa.js} +1 -1
- package/dist/web-client/assets/{chunk-MOJQB5TN-C0LAX_dC.js → chunk-MOJQB5TN-DKrK867P.js} +1 -1
- package/dist/web-client/assets/{chunk-OGEWGWER-CBx8MB7f.js → chunk-OGEWGWER-BmGykUeg.js} +1 -1
- package/dist/web-client/assets/{chunk-PUDLZKDR-DKssR1nf.js → chunk-PUDLZKDR-Bq23pnso.js} +1 -1
- package/dist/web-client/assets/{chunk-Q4XR5HBZ-B3kcxFE-.js → chunk-Q4XR5HBZ-g15qvFAN.js} +1 -1
- package/dist/web-client/assets/{chunk-V7JOEXUC-CAlymndy.js → chunk-V7JOEXUC-uyXA7S9r.js} +1 -1
- package/dist/web-client/assets/{chunk-VAUOI2AC-BowfsmTW.js → chunk-VAUOI2AC-WofZ-2M0.js} +1 -1
- package/dist/web-client/assets/{chunk-VR4S4FIN-BBOydgvt.js → chunk-VR4S4FIN-DtTkK86v.js} +1 -1
- package/dist/web-client/assets/{chunk-WYO6CB5R-DcymFbES.js → chunk-WYO6CB5R-zVyUwJq3.js} +1 -1
- package/dist/web-client/assets/{chunk-ZGVPDNZ5--uKFP-Lr.js → chunk-ZGVPDNZ5-CA1Oe3TK.js} +1 -1
- package/dist/web-client/assets/classDiagram-OUVF2IWQ-Doj1_hvZ.js +1 -0
- package/dist/web-client/assets/classDiagram-v2-EOCWNBFH-Doj1_hvZ.js +1 -0
- package/dist/web-client/assets/{cynefin-VYW2F7L2-CjboUOMA.js → cynefin-VYW2F7L2-ivJY1h_9.js} +1 -1
- package/dist/web-client/assets/{cynefinDiagram-TSTJHNR4-BcxygBP7.js → cynefinDiagram-TSTJHNR4-D2DstShq.js} +1 -1
- package/dist/web-client/assets/{dagre-VKFMJZFB-D-tiERQE.js → dagre-VKFMJZFB-CwnMRy0K.js} +1 -1
- package/dist/web-client/assets/{diagram-FQU43EPY-ChPXczaS.js → diagram-FQU43EPY-BKJv6Cvw.js} +1 -1
- package/dist/web-client/assets/{diagram-G47NLZAW-CVL3Y91h.js → diagram-G47NLZAW-CwCXcgU5.js} +1 -1
- package/dist/web-client/assets/{diagram-NH7WQ7WH-DsaNA9Lh.js → diagram-NH7WQ7WH-CkghU1-6.js} +1 -1
- package/dist/web-client/assets/{diagram-OA4YK3LP-CXhrhdhU.js → diagram-OA4YK3LP-D3siQIGu.js} +1 -1
- package/dist/web-client/assets/{diagram-WEI45ONY-BTVPnk4E.js → diagram-WEI45ONY-CSs9xTBn.js} +1 -1
- package/dist/web-client/assets/{ebnfDiagram-CCIWWBDH-BAyrRBtM.js → ebnfDiagram-CCIWWBDH--i52vMiv.js} +1 -1
- package/dist/web-client/assets/{erDiagram-Q63AITRT-Qm24Wepm.js → erDiagram-Q63AITRT-q2hgOBY1.js} +1 -1
- package/dist/web-client/assets/eventmodeling-45OFAUF4-ESVuFkJJ.js +1 -0
- package/dist/web-client/assets/flowDiagram-23GEKE2U-DN9SdR9f.js +1 -0
- package/dist/web-client/assets/{ganttDiagram-NO4QXBWP-D8h7l3XJ.js → ganttDiagram-NO4QXBWP-BZ2rBbTe.js} +1 -1
- package/dist/web-client/assets/{gitGraph-TEB2WS4Q-DIBml1SB.js → gitGraph-TEB2WS4Q-OnJ8tHgt.js} +1 -1
- package/dist/web-client/assets/{gitGraphDiagram-IHSO6WYX-CtkYoXjn.js → gitGraphDiagram-IHSO6WYX-CPNExFAr.js} +1 -1
- package/dist/web-client/assets/{index-DOSH4Q9H.js → index-DCBn-YrR.js} +38 -38
- package/dist/web-client/assets/{info-DKCQHKI2-DLEUtV5Q.js → info-DKCQHKI2-DGuJbmdY.js} +1 -1
- package/dist/web-client/assets/{infoDiagram-FWYZ7A6U-BJQ7aQux.js → infoDiagram-FWYZ7A6U-ADIAn-P5.js} +1 -1
- package/dist/web-client/assets/{ishikawaDiagram-FXEZZL3T-BPM11FvG.js → ishikawaDiagram-FXEZZL3T-CUx-RGfg.js} +1 -1
- package/dist/web-client/assets/{journeyDiagram-5HDEW3XC-C0aX2z3c.js → journeyDiagram-5HDEW3XC-BH7-tBBy.js} +1 -1
- package/dist/web-client/assets/{kanban-definition-HUTT4EX6-C56F29Ib.js → kanban-definition-HUTT4EX6-BGEXBnI2.js} +1 -1
- package/dist/web-client/assets/{line-BLFHLF2N.js → line-CC-5ezU8.js} +1 -1
- package/dist/web-client/assets/{mermaid-parser.core-BLC8FhgU.js → mermaid-parser.core-Dw18Fjbq.js} +3 -3
- package/dist/web-client/assets/{mermaid.core-BBqkKuXt.js → mermaid.core-CMQBgE43.js} +3 -3
- package/dist/web-client/assets/{mindmap-definition-LN4V7U3C-aVZbsoPc.js → mindmap-definition-LN4V7U3C-CGtjw8tB.js} +1 -1
- package/dist/web-client/assets/{packet-7NZHBO7P-D4aqSQfB.js → packet-7NZHBO7P-w71Y4Hzd.js} +1 -1
- package/dist/web-client/assets/{pegDiagram-2B236MQR-DjfyNI0U.js → pegDiagram-2B236MQR-DWtfEm4T.js} +1 -1
- package/dist/web-client/assets/{pie-RZYD4A2V-ChCwYsYj.js → pie-RZYD4A2V-S9ztgnWs.js} +1 -1
- package/dist/web-client/assets/{pieDiagram-ENE6RG2P-BeHLKkXC.js → pieDiagram-ENE6RG2P-BWZQPN4m.js} +1 -1
- package/dist/web-client/assets/{quadrantDiagram-ABIIQ3AL-stga3gvq.js → quadrantDiagram-ABIIQ3AL-DINDwblH.js} +1 -1
- package/dist/web-client/assets/{radar-I7S5WNFK-DOGheiwT.js → radar-I7S5WNFK-BOGl9krB.js} +1 -1
- package/dist/web-client/assets/{railroad-3IZDKUUU-_JnU7M6L.js → railroad-3IZDKUUU-CR9kuYSZ.js} +1 -1
- package/dist/web-client/assets/railroad-abnf-AHOZXSZD-BvbCHrlR.js +1 -0
- package/dist/web-client/assets/railroad-ebnf-EBAXGLYW-D869lbCL.js +1 -0
- package/dist/web-client/assets/railroad-peg-LSFZ7HO6-r4TXaJPI.js +1 -0
- package/dist/web-client/assets/{railroadDiagram-RFXS5EU6-C0CkMsOd.js → railroadDiagram-RFXS5EU6-byLCs9hp.js} +1 -1
- package/dist/web-client/assets/{requirementDiagram-TGXJPOKE-DuImwoRD.js → requirementDiagram-TGXJPOKE-Dn_FtbqW.js} +1 -1
- package/dist/web-client/assets/{sankeyDiagram-HTMAVEWB-kprq0XF9.js → sankeyDiagram-HTMAVEWB-DUjRrCeC.js} +1 -1
- package/dist/web-client/assets/{sequenceDiagram-DBY2YBRQ-DiXKJMF6.js → sequenceDiagram-DBY2YBRQ-BmBz8Wpq.js} +1 -1
- package/dist/web-client/assets/{stateDiagram-2N3HPSRC-D5qbVStE.js → stateDiagram-2N3HPSRC-BAYHjmqB.js} +1 -1
- package/dist/web-client/assets/stateDiagram-v2-6OUMAXLB-Bl-1K1zV.js +1 -0
- package/dist/web-client/assets/{swimlanes-5IMT3BWC-DCbw389c.js → swimlanes-5IMT3BWC-Bm942AR-.js} +1 -1
- package/dist/web-client/assets/swimlanesDiagram-G3AALYLV-DHU7bsfA.js +8 -0
- package/dist/web-client/assets/{timeline-definition-FHXFAJF6-CQeaYN_9.js → timeline-definition-FHXFAJF6-CHAZHhQk.js} +1 -1
- package/dist/web-client/assets/{treeView-QDETBFTQ-Cf7Sq3qo.js → treeView-QDETBFTQ-DHHkLsSy.js} +1 -1
- package/dist/web-client/assets/{treemap-6X3UGDF4-BovzvoTU.js → treemap-6X3UGDF4-BUxZ8fhY.js} +1 -1
- package/dist/web-client/assets/{vennDiagram-L72KCM5P-CZsJy139.js → vennDiagram-L72KCM5P-BL9TRcUU.js} +1 -1
- package/dist/web-client/assets/{wardley-OPB4EBWU-DJ7MS6XZ.js → wardley-OPB4EBWU-CfJ5mO-y.js} +1 -1
- package/dist/web-client/assets/{wardleyDiagram-EHGQE667-rqhcmsbM.js → wardleyDiagram-EHGQE667-BlH1TSjy.js} +1 -1
- package/dist/web-client/assets/{xychartDiagram-FW5EYKEG-HuK4Seps.js → xychartDiagram-FW5EYKEG-BJN6wtrV.js} +1 -1
- package/dist/web-client/index.html +1 -1
- package/dist/web-server/app.d.ts.map +1 -1
- package/dist/web-server/app.js +1 -7
- package/dist/web-server/app.js.map +1 -1
- package/dist/web-server/claude-runner.d.ts +11 -4
- package/dist/web-server/claude-runner.d.ts.map +1 -1
- package/dist/web-server/claude-runner.js +10 -14
- package/dist/web-server/claude-runner.js.map +1 -1
- package/dist/web-server/index.d.ts.map +1 -1
- package/dist/web-server/index.js +4 -1
- package/dist/web-server/index.js.map +1 -1
- package/dist/web-server/opencode-client.d.ts +123 -0
- package/dist/web-server/opencode-client.d.ts.map +1 -0
- package/dist/web-server/opencode-client.js +514 -0
- package/dist/web-server/opencode-client.js.map +1 -0
- package/dist/web-server/prompt-assembly.d.ts.map +1 -1
- package/dist/web-server/prompt-assembly.js +1 -2
- package/dist/web-server/prompt-assembly.js.map +1 -1
- package/dist/web-server/routes/sessions.d.ts +9 -6
- package/dist/web-server/routes/sessions.d.ts.map +1 -1
- package/dist/web-server/routes/sessions.js +272 -205
- package/dist/web-server/routes/sessions.js.map +1 -1
- package/dist/web-server/run-driver.d.ts +113 -0
- package/dist/web-server/run-driver.d.ts.map +1 -0
- package/dist/web-server/run-driver.js +214 -0
- package/dist/web-server/run-driver.js.map +1 -0
- package/dist/web-server/run-event-log.d.ts +23 -1
- package/dist/web-server/run-event-log.d.ts.map +1 -1
- package/dist/web-server/run-event-log.js +29 -5
- package/dist/web-server/run-event-log.js.map +1 -1
- package/dist/web-server/web-auth.d.ts +3 -5
- package/dist/web-server/web-auth.d.ts.map +1 -1
- package/dist/web-server/web-auth.js +6 -11
- package/dist/web-server/web-auth.js.map +1 -1
- package/dist/web-server/web-token.d.ts +1 -2
- package/dist/web-server/web-token.d.ts.map +1 -1
- package/dist/web-server/web-token.js +1 -2
- package/dist/web-server/web-token.js.map +1 -1
- package/opencode/arcs/bundle-runtime.json +0 -3
- package/opencode/arcs/prompts/arcs-docs.txt +4 -4
- package/opencode/arcs/prompts/arcs-flash.txt +15 -21
- package/opencode/arcs/prompts/arcs-orchestrate-caveman.txt +17 -15
- package/opencode/arcs/prompts/arcs-orchestrate.txt +17 -15
- package/opencode/arcs/prompts/code-reviewer.txt +4 -4
- package/opencode/arcs/prompts/graph-explorer.txt +4 -4
- package/opencode/arcs/prompts/software-engineer.txt +2 -2
- package/opencode/arcs/prompts/tech-architect.txt +4 -4
- package/opencode/arcs/skills/brainstorming/SKILL.md +4 -4
- package/opencode/arcs/skills/enriching-codegraph-proposals/SKILL.md +1 -1
- package/opencode/arcs/skills/implementation/SKILL.md +6 -6
- package/opencode/arcs/skills/init-project/SKILL.md +4 -4
- package/opencode/arcs/skills/systematic-debugging/SKILL.md +2 -2
- package/opencode/arcs/skills/test-driven-development/SKILL.md +3 -3
- package/opencode/arcs/skills/to-diagram/SKILL.md +2 -2
- package/opencode/arcs/skills/writing-knowledge/SKILL.md +2 -2
- package/opencode/arcs/skills/writing-plans/SKILL.md +2 -2
- package/package.json +1 -1
- package/dist/web-client/assets/architecture-TIHT7OUA-Bdo2Yvm9.js +0 -1
- package/dist/web-client/assets/channel-DBNmizpo.js +0 -1
- package/dist/web-client/assets/classDiagram-OUVF2IWQ-CB3HiA1_.js +0 -1
- package/dist/web-client/assets/classDiagram-v2-EOCWNBFH-CB3HiA1_.js +0 -1
- package/dist/web-client/assets/eventmodeling-45OFAUF4-DoTBIvl5.js +0 -1
- package/dist/web-client/assets/flowDiagram-23GEKE2U-BEH23L1A.js +0 -1
- package/dist/web-client/assets/railroad-abnf-AHOZXSZD-nhNub7LE.js +0 -1
- package/dist/web-client/assets/railroad-ebnf-EBAXGLYW-BlQYe7Yf.js +0 -1
- package/dist/web-client/assets/railroad-peg-LSFZ7HO6-B3E8pRVN.js +0 -1
- package/dist/web-client/assets/stateDiagram-v2-6OUMAXLB-DWwTAG1r.js +0 -1
- package/dist/web-client/assets/swimlanesDiagram-G3AALYLV-DabrCsjZ.js +0 -8
- package/opencode/arcs/skills/install-claude-code-hook/SKILL.md +0 -23
- 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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
|
133
|
-
* and never
|
|
134
|
-
* turn's references: they render into the user-facing
|
|
135
|
-
* sidecar. `guards` is validated and then deliberately
|
|
136
|
-
* change-intent preflight that reads it is a separate task,
|
|
137
|
-
* key now keeps that task from being a breaking payload
|
|
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
|
|
311
|
-
* is
|
|
312
|
-
*
|
|
313
|
-
*
|
|
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
|
|
344
|
-
* with the run id so a second fold is a no-op)
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
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
|
-
*
|
|
350
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
|
492
|
+
const slug = c.req.param("slug");
|
|
493
|
+
const projectDir = requireProjectDir(slug);
|
|
441
494
|
const input = await parseBody(c, createSessionSchema);
|
|
442
|
-
|
|
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
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
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
|
|
481
|
-
`
|
|
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
|
-
*
|
|
485
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
492
|
-
*
|
|
493
|
-
* it is
|
|
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
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
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
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
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
|
|
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
|
-
* `--
|
|
601
|
-
*
|
|
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,
|
|
633
|
+
function turnTargetingArgv(prompt, seeding, claudeSessionId) {
|
|
605
634
|
const argv = ["-p", prompt];
|
|
606
|
-
|
|
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
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
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
|
|
627
|
-
*
|
|
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
|
|
630
|
-
* comes from `buildPermissionArgv` and this route builds
|
|
631
|
-
* the targeting tokens above, and the permission segment is
|
|
632
|
-
* `--tools` is variadic (it eats following tokens until the next
|
|
633
|
-
* one) and `--append-system-prompt` consumes exactly one following
|
|
634
|
-
* segment placed before `-p` would swallow the prompt or the staged
|
|
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):
|
|
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
|
|
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
|
|
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
|
-
|
|
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`:
|
|
772
|
-
//
|
|
773
|
-
// path id answers 200 and then emits nothing —
|
|
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
|
};
|