@rryando/arcs 3.11.0 → 4.0.0
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/dist/cli/commands/hooks.d.ts +46 -6
- package/dist/cli/commands/hooks.d.ts.map +1 -1
- package/dist/cli/commands/hooks.js +190 -52
- package/dist/cli/commands/hooks.js.map +1 -1
- package/dist/cli/commands/web.js +14 -4
- package/dist/cli/commands/web.js.map +1 -1
- package/dist/cli/config.d.ts +14 -3
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +39 -38
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/instructions.d.ts +13 -1
- package/dist/cli/instructions.d.ts.map +1 -1
- package/dist/cli/instructions.js +38 -3
- package/dist/cli/instructions.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +76 -11
- package/dist/cli/setup.js.map +1 -1
- package/dist/shared/session-vocabulary.d.ts +88 -0
- package/dist/shared/session-vocabulary.d.ts.map +1 -0
- package/dist/shared/session-vocabulary.js +114 -0
- package/dist/shared/session-vocabulary.js.map +1 -0
- package/dist/utils/claude-code-hook-install.d.ts +1 -1
- package/dist/utils/claude-code-hook-install.d.ts.map +1 -1
- package/dist/utils/claude-code-hook-install.js +68 -3
- package/dist/utils/claude-code-hook-install.js.map +1 -1
- package/dist/utils/claude-transcript.d.ts +81 -2
- package/dist/utils/claude-transcript.d.ts.map +1 -1
- package/dist/utils/claude-transcript.js +33 -2
- package/dist/utils/claude-transcript.js.map +1 -1
- package/dist/utils/git.d.ts +21 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +65 -1
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/hook-contract.d.ts +36 -0
- package/dist/utils/hook-contract.d.ts.map +1 -0
- package/dist/utils/hook-contract.js +35 -0
- package/dist/utils/hook-contract.js.map +1 -0
- package/dist/utils/hook-token-store.d.ts +39 -0
- package/dist/utils/hook-token-store.d.ts.map +1 -1
- package/dist/utils/hook-token-store.js +46 -3
- package/dist/utils/hook-token-store.js.map +1 -1
- package/dist/utils/session-store.d.ts +226 -0
- package/dist/utils/session-store.d.ts.map +1 -1
- package/dist/utils/session-store.js +266 -1
- package/dist/utils/session-store.js.map +1 -1
- package/dist/web-client/assets/{GraphCanvas-dNyZ458L.js → GraphCanvas-CTyf_XXQ.js} +1 -1
- package/dist/web-client/assets/{MarkdownEditor-BmU9mdkN.js → MarkdownEditor-af2vwQOX.js} +1 -1
- package/dist/web-client/assets/{abnfDiagram-VRR7QNED-D1BFBoeF.js → abnfDiagram-VRR7QNED-CdxcKX9t.js} +1 -1
- package/dist/web-client/assets/architecture-TIHT7OUA-Bt2-DmfV.js +1 -0
- package/dist/web-client/assets/{architectureDiagram-ZJ3FMSHR-CA8hTWUK.js → architectureDiagram-ZJ3FMSHR-DOISDv6o.js} +1 -1
- package/dist/web-client/assets/{blockDiagram-677ZJIJ3-CQpb_KwX.js → blockDiagram-677ZJIJ3-DKwtbttM.js} +1 -1
- package/dist/web-client/assets/{c4Diagram-LMCZKHZV-BhpHX84V.js → c4Diagram-LMCZKHZV-CilqK-Mm.js} +1 -1
- package/dist/web-client/assets/channel-C2mb9np_.js +1 -0
- package/dist/web-client/assets/{chunk-32BRIVSS-BUusQQa_.js → chunk-32BRIVSS-C79m1mkG.js} +1 -1
- package/dist/web-client/assets/{chunk-52WLFC77-nNYwlftl.js → chunk-52WLFC77-C6WelGWJ.js} +1 -1
- package/dist/web-client/assets/{chunk-C7G6YPKG-D0a-yqnK.js → chunk-C7G6YPKG-DLg7ryWI.js} +1 -1
- package/dist/web-client/assets/{chunk-EX3LRPZG-IDuRMN-4.js → chunk-EX3LRPZG-DjWgo4gL.js} +1 -1
- package/dist/web-client/assets/{chunk-FWX5IMBZ-D_spTcqi.js → chunk-FWX5IMBZ-BgS9p_zy.js} +2 -2
- package/dist/web-client/assets/{chunk-HOUHSVGY-C-bcxwZS.js → chunk-HOUHSVGY-DuVR7dZO.js} +1 -1
- package/dist/web-client/assets/{chunk-ICXQ74PX-CjmK2bAM.js → chunk-ICXQ74PX-Y8DlnIJM.js} +1 -1
- package/dist/web-client/assets/{chunk-MOJQB5TN-Bkc08KWY.js → chunk-MOJQB5TN-BEM3QgeD.js} +1 -1
- package/dist/web-client/assets/{chunk-OGEWGWER-BAAYYvG9.js → chunk-OGEWGWER-DlM8LxGr.js} +1 -1
- package/dist/web-client/assets/{chunk-PUDLZKDR-WXbPY7NM.js → chunk-PUDLZKDR-c6cqNVTx.js} +1 -1
- package/dist/web-client/assets/{chunk-Q4XR5HBZ-DcbnjxQE.js → chunk-Q4XR5HBZ-C5lNmcka.js} +1 -1
- package/dist/web-client/assets/{chunk-V7JOEXUC-C6t75PAp.js → chunk-V7JOEXUC-nSswxvSG.js} +1 -1
- package/dist/web-client/assets/{chunk-VAUOI2AC-DVrJ0Ic7.js → chunk-VAUOI2AC-CqJkCkT0.js} +1 -1
- package/dist/web-client/assets/{chunk-VR4S4FIN-MOOFvGS0.js → chunk-VR4S4FIN-D3pFchin.js} +1 -1
- package/dist/web-client/assets/{chunk-WYO6CB5R-sK7Y2NZD.js → chunk-WYO6CB5R-BI9c-NzI.js} +1 -1
- package/dist/web-client/assets/{chunk-ZGVPDNZ5-a13RQsku.js → chunk-ZGVPDNZ5-CKTF2kLR.js} +1 -1
- package/dist/web-client/assets/classDiagram-OUVF2IWQ-CPd_w3Zr.js +1 -0
- package/dist/web-client/assets/classDiagram-v2-EOCWNBFH-CPd_w3Zr.js +1 -0
- package/dist/web-client/assets/{cynefin-VYW2F7L2-D8xaH-wO.js → cynefin-VYW2F7L2-BrJrnfh1.js} +1 -1
- package/dist/web-client/assets/{cynefinDiagram-TSTJHNR4-Bt__EqJW.js → cynefinDiagram-TSTJHNR4-9jYzPWJZ.js} +1 -1
- package/dist/web-client/assets/{dagre-VKFMJZFB-Bwgjwflz.js → dagre-VKFMJZFB-B705Djpu.js} +1 -1
- package/dist/web-client/assets/{diagram-FQU43EPY-CaPDVUq2.js → diagram-FQU43EPY-BfQAlUlJ.js} +1 -1
- package/dist/web-client/assets/{diagram-G47NLZAW-BQLB9YYA.js → diagram-G47NLZAW-s1YDe06A.js} +1 -1
- package/dist/web-client/assets/{diagram-NH7WQ7WH-BWo84w8Y.js → diagram-NH7WQ7WH-Dy3z11Hc.js} +1 -1
- package/dist/web-client/assets/{diagram-OA4YK3LP-bKn6Pz5s.js → diagram-OA4YK3LP-Bius2xUN.js} +1 -1
- package/dist/web-client/assets/{diagram-WEI45ONY-BRqV5Oy6.js → diagram-WEI45ONY-D7x4VcHM.js} +1 -1
- package/dist/web-client/assets/{ebnfDiagram-CCIWWBDH-DhUXL1-7.js → ebnfDiagram-CCIWWBDH-CBg1xrmD.js} +1 -1
- package/dist/web-client/assets/{erDiagram-Q63AITRT-HXAQQ-_F.js → erDiagram-Q63AITRT-CJRvTFvd.js} +1 -1
- package/dist/web-client/assets/eventmodeling-45OFAUF4-CTKRC2Yj.js +1 -0
- package/dist/web-client/assets/flowDiagram-23GEKE2U-CaZjFyU1.js +1 -0
- package/dist/web-client/assets/{ganttDiagram-NO4QXBWP-D-Ddf_Ii.js → ganttDiagram-NO4QXBWP-C_LsypZ4.js} +1 -1
- package/dist/web-client/assets/{gitGraph-TEB2WS4Q-BmHzs0uF.js → gitGraph-TEB2WS4Q-Dx2XxdGk.js} +1 -1
- package/dist/web-client/assets/{gitGraphDiagram-IHSO6WYX-YnQWrlh_.js → gitGraphDiagram-IHSO6WYX-DiKWkGWQ.js} +1 -1
- package/dist/web-client/assets/{index-DCWxuIeQ.js → index-CYwhkPtc.js} +38 -36
- package/dist/web-client/assets/index-wSzUPvml.css +2 -0
- package/dist/web-client/assets/{info-DKCQHKI2-DCT_B7RN.js → info-DKCQHKI2-DORwHenK.js} +1 -1
- package/dist/web-client/assets/{infoDiagram-FWYZ7A6U-D-le1Zhq.js → infoDiagram-FWYZ7A6U-CQecXS1E.js} +1 -1
- package/dist/web-client/assets/{ishikawaDiagram-FXEZZL3T-Jr1x2VJB.js → ishikawaDiagram-FXEZZL3T-CWhj60Zp.js} +1 -1
- package/dist/web-client/assets/{journeyDiagram-5HDEW3XC-BF9ELxj-.js → journeyDiagram-5HDEW3XC-Cj3z2U8u.js} +1 -1
- package/dist/web-client/assets/{kanban-definition-HUTT4EX6-C4fJqAxu.js → kanban-definition-HUTT4EX6-DFmBRenP.js} +1 -1
- package/dist/web-client/assets/{line-7N7ikFxa.js → line-C_Hxz9xb.js} +1 -1
- package/dist/web-client/assets/{mermaid-parser.core-QbC1icPt.js → mermaid-parser.core-E3xRdzPn.js} +3 -3
- package/dist/web-client/assets/{mermaid.core-C26d_UJm.js → mermaid.core-C2_CcotH.js} +3 -3
- package/dist/web-client/assets/{mindmap-definition-LN4V7U3C-D6TV1JDf.js → mindmap-definition-LN4V7U3C-Bs7VxPid.js} +1 -1
- package/dist/web-client/assets/{packet-7NZHBO7P-CR1vrGj3.js → packet-7NZHBO7P-D2nqso7i.js} +1 -1
- package/dist/web-client/assets/{pegDiagram-2B236MQR-xOMBBtfV.js → pegDiagram-2B236MQR-Cepo1sIu.js} +1 -1
- package/dist/web-client/assets/{pie-RZYD4A2V-BbWuhjwy.js → pie-RZYD4A2V-Sx0IGbXt.js} +1 -1
- package/dist/web-client/assets/{pieDiagram-ENE6RG2P-MsfnsqgW.js → pieDiagram-ENE6RG2P-D-PMDNhk.js} +1 -1
- package/dist/web-client/assets/{quadrantDiagram-ABIIQ3AL-BoI7zKXF.js → quadrantDiagram-ABIIQ3AL-BNGC7Fms.js} +1 -1
- package/dist/web-client/assets/{radar-I7S5WNFK-CbYXKToJ.js → radar-I7S5WNFK-CFf_-2OC.js} +1 -1
- package/dist/web-client/assets/{railroad-3IZDKUUU-6LxHDkLe.js → railroad-3IZDKUUU-BvZeVRGh.js} +1 -1
- package/dist/web-client/assets/railroad-abnf-AHOZXSZD-DnGh6x2O.js +1 -0
- package/dist/web-client/assets/railroad-ebnf-EBAXGLYW-CGfgCoaE.js +1 -0
- package/dist/web-client/assets/railroad-peg-LSFZ7HO6-BzQxYdqp.js +1 -0
- package/dist/web-client/assets/{railroadDiagram-RFXS5EU6-D6RUoUki.js → railroadDiagram-RFXS5EU6-D0iK_k84.js} +1 -1
- package/dist/web-client/assets/{requirementDiagram-TGXJPOKE-B6k4BDpE.js → requirementDiagram-TGXJPOKE-BPdsw9tz.js} +1 -1
- package/dist/web-client/assets/{sankeyDiagram-HTMAVEWB-BUDF-UFr.js → sankeyDiagram-HTMAVEWB-maA6ukOJ.js} +1 -1
- package/dist/web-client/assets/{sequenceDiagram-DBY2YBRQ-D6GqcsUi.js → sequenceDiagram-DBY2YBRQ-DKmAjmgr.js} +1 -1
- package/dist/web-client/assets/{stateDiagram-2N3HPSRC-WfJCQAK5.js → stateDiagram-2N3HPSRC-esxmzk3w.js} +1 -1
- package/dist/web-client/assets/stateDiagram-v2-6OUMAXLB-hcBqlOLc.js +1 -0
- package/dist/web-client/assets/{swimlanes-5IMT3BWC-BtMo82mC.js → swimlanes-5IMT3BWC-DFd-wGBv.js} +1 -1
- package/dist/web-client/assets/swimlanesDiagram-G3AALYLV-DbSrPBYr.js +8 -0
- package/dist/web-client/assets/{timeline-definition-FHXFAJF6-CoAmv2Sn.js → timeline-definition-FHXFAJF6-C7SG8MQH.js} +1 -1
- package/dist/web-client/assets/{treeView-QDETBFTQ-BWsKzE1s.js → treeView-QDETBFTQ-M0rhxbs2.js} +1 -1
- package/dist/web-client/assets/{treemap-6X3UGDF4-i_qGtB3o.js → treemap-6X3UGDF4-BQfkQxSh.js} +1 -1
- package/dist/web-client/assets/{vennDiagram-L72KCM5P-DYkiLe-P.js → vennDiagram-L72KCM5P-CyIlb8ju.js} +1 -1
- package/dist/web-client/assets/{wardley-OPB4EBWU-Daaqr1Vp.js → wardley-OPB4EBWU-EE-7q8M1.js} +1 -1
- package/dist/web-client/assets/{wardleyDiagram-EHGQE667-Bofbsg3J.js → wardleyDiagram-EHGQE667-bY8XdYYS.js} +1 -1
- package/dist/web-client/assets/{xychartDiagram-FW5EYKEG-KNF4VTfL.js → xychartDiagram-FW5EYKEG-AhuXJ3de.js} +1 -1
- package/dist/web-client/index.html +2 -2
- package/dist/web-server/app.d.ts +4 -1
- package/dist/web-server/app.d.ts.map +1 -1
- package/dist/web-server/app.js +19 -0
- package/dist/web-server/app.js.map +1 -1
- package/dist/web-server/claude-runner.d.ts +109 -7
- package/dist/web-server/claude-runner.d.ts.map +1 -1
- package/dist/web-server/claude-runner.js +329 -50
- package/dist/web-server/claude-runner.js.map +1 -1
- package/dist/web-server/index.d.ts +2 -2
- package/dist/web-server/index.d.ts.map +1 -1
- package/dist/web-server/index.js +3 -2
- package/dist/web-server/index.js.map +1 -1
- package/dist/web-server/permission-policy.d.ts +46 -0
- package/dist/web-server/permission-policy.d.ts.map +1 -0
- package/dist/web-server/permission-policy.js +96 -0
- package/dist/web-server/permission-policy.js.map +1 -0
- package/dist/web-server/prompt-assembly.d.ts +454 -0
- package/dist/web-server/prompt-assembly.d.ts.map +1 -0
- package/dist/web-server/prompt-assembly.js +1122 -0
- package/dist/web-server/prompt-assembly.js.map +1 -0
- package/dist/web-server/routes/hook-events.d.ts +28 -3
- package/dist/web-server/routes/hook-events.d.ts.map +1 -1
- package/dist/web-server/routes/hook-events.js +209 -24
- package/dist/web-server/routes/hook-events.js.map +1 -1
- package/dist/web-server/routes/sessions.d.ts +7 -5
- package/dist/web-server/routes/sessions.d.ts.map +1 -1
- package/dist/web-server/routes/sessions.js +1030 -174
- package/dist/web-server/routes/sessions.js.map +1 -1
- package/dist/web-server/routes/workspace.d.ts +27 -0
- package/dist/web-server/routes/workspace.d.ts.map +1 -0
- package/dist/web-server/routes/workspace.js +280 -0
- package/dist/web-server/routes/workspace.js.map +1 -0
- package/dist/web-server/run-event-log.d.ts +167 -0
- package/dist/web-server/run-event-log.d.ts.map +1 -0
- package/dist/web-server/run-event-log.js +468 -0
- package/dist/web-server/run-event-log.js.map +1 -0
- package/dist/web-server/session-reconciler.d.ts +162 -0
- package/dist/web-server/session-reconciler.d.ts.map +1 -0
- package/dist/web-server/session-reconciler.js +363 -0
- package/dist/web-server/session-reconciler.js.map +1 -0
- package/dist/web-server/static.d.ts +7 -0
- package/dist/web-server/static.d.ts.map +1 -1
- package/dist/web-server/static.js +46 -3
- package/dist/web-server/static.js.map +1 -1
- package/dist/web-server/web-auth.d.ts +17 -0
- package/dist/web-server/web-auth.d.ts.map +1 -0
- package/dist/web-server/web-auth.js +33 -0
- package/dist/web-server/web-auth.js.map +1 -0
- package/dist/web-server/web-token.d.ts +39 -0
- package/dist/web-server/web-token.d.ts.map +1 -0
- package/dist/web-server/web-token.js +71 -0
- package/dist/web-server/web-token.js.map +1 -0
- package/opencode/arcs/manifest.json +8 -8
- package/package.json +1 -1
- package/scripts/claude-code-session-hook.mjs +49 -3
- package/scripts/deploy-claudecode-bundle.mjs +24 -3
- package/dist/web-client/assets/architecture-TIHT7OUA-CJqI5wNI.js +0 -1
- package/dist/web-client/assets/channel-C8DlmyVe.js +0 -1
- package/dist/web-client/assets/classDiagram-OUVF2IWQ-p32N1P_G.js +0 -1
- package/dist/web-client/assets/classDiagram-v2-EOCWNBFH-p32N1P_G.js +0 -1
- package/dist/web-client/assets/eventmodeling-45OFAUF4-Bj5P8mZJ.js +0 -1
- package/dist/web-client/assets/flowDiagram-23GEKE2U-37BztFri.js +0 -1
- package/dist/web-client/assets/index-3mNPVkix.css +0 -2
- package/dist/web-client/assets/railroad-abnf-AHOZXSZD-2Dg9wu0J.js +0 -1
- package/dist/web-client/assets/railroad-ebnf-EBAXGLYW-C_E2ot0R.js +0 -1
- package/dist/web-client/assets/railroad-peg-LSFZ7HO6-Bs9UQR1b.js +0 -1
- package/dist/web-client/assets/stateDiagram-v2-6OUMAXLB-BCMWGnsJ.js +0 -1
- package/dist/web-client/assets/swimlanesDiagram-G3AALYLV-DmudmLcK.js +0 -8
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-run stdout event log — the durable spine of a headless `claude -p` run.
|
|
3
|
+
*
|
|
4
|
+
* Owns `sessions/{normalizedId}.run-{runId}.events.jsonl`: every byte the child
|
|
5
|
+
* writes to stdout lands here VERBATIM, one file line per stdout line, BEFORE
|
|
6
|
+
* anything parses it. That ordering is the whole point — the log is the source
|
|
7
|
+
* of truth and the runner's NDJSON reader is merely its first consumer, so a
|
|
8
|
+
* line the reader cannot parse (drifted wire format, truncated JSON, an event
|
|
9
|
+
* type that did not exist when this shipped) is still on disk, and a process
|
|
10
|
+
* that dies mid-run still leaves behind everything that arrived.
|
|
11
|
+
*
|
|
12
|
+
* Writes are synchronous (`writeSync` against an fd opened with "a") for
|
|
13
|
+
* exactly that reason: an async queue would only ever have *issued* the write
|
|
14
|
+
* before the parse, and a hard kill would lose whatever was still in flight.
|
|
15
|
+
* The chunks are small and few (one per stdout chunk of a single child), so the
|
|
16
|
+
* event-loop cost is a rounding error against a run measured in minutes.
|
|
17
|
+
*
|
|
18
|
+
* The `runId` in the filename is the SAME id the route persists as
|
|
19
|
+
* `currentRunId`, so the log's name and the session record can never disagree
|
|
20
|
+
* about which run produced it.
|
|
21
|
+
*
|
|
22
|
+
* Total by construction, like the reader it feeds: nothing here throws. A log
|
|
23
|
+
* that cannot be opened or written reports itself through `RunEventLogStats`
|
|
24
|
+
* and the run proceeds untouched — a logging failure must never fail a run.
|
|
25
|
+
* Reporting is the whole point: a log that hit its size cap or lost bytes to a
|
|
26
|
+
* short write says so on `truncated`, which rides all the way out to
|
|
27
|
+
* `metadata.run.eventLogTruncated`. A silent fallback here would make an empty
|
|
28
|
+
* log and a capped one look identical to every consumer downstream.
|
|
29
|
+
*
|
|
30
|
+
* At settle the log folds down into the transcript sidecar (assistant text plus
|
|
31
|
+
* one turn per `tool_use`) through `appendSessionTurn`, and older logs for the
|
|
32
|
+
* same session are pruned so the sessions dir cannot grow without limit.
|
|
33
|
+
*/
|
|
34
|
+
import { closeSync, mkdirSync, openSync, writeSync } from "node:fs";
|
|
35
|
+
import { readdir, readFile, stat, unlink } from "node:fs/promises";
|
|
36
|
+
import { dirname, join } from "node:path";
|
|
37
|
+
import { appendSessionTurn, readSessionTurns } from "../utils/claude-transcript.js";
|
|
38
|
+
import { normalizeIdentifier } from "../utils/slug.js";
|
|
39
|
+
/**
|
|
40
|
+
* Ceiling on one run's log — a runaway child cannot fill the disk.
|
|
41
|
+
*
|
|
42
|
+
* The finished file is never LARGER than this, terminator included: the writer
|
|
43
|
+
* refuses a chunk that would take it TO the cap (`>=`, not `>`), leaving the one
|
|
44
|
+
* byte `flush()` may still need. The off-by-one that leaves matters because
|
|
45
|
+
* `foldRunEventLog` refuses to read anything past this size — a writer allowed
|
|
46
|
+
* to land on exactly the cap and then add a terminator produces a file its own
|
|
47
|
+
* fold silently discards.
|
|
48
|
+
*
|
|
49
|
+
* Reaching it does not close the log: only the crossing chunk is refused, later
|
|
50
|
+
* chunks that still fit are still written, and `RunEventLogStats.truncated`
|
|
51
|
+
* reports that bytes are missing. The file always ends on a line boundary, so an
|
|
52
|
+
* offset-based tail never reads half a record.
|
|
53
|
+
*/
|
|
54
|
+
export const RUN_EVENT_LOG_MAX_BYTES = 32 * 1024 * 1024;
|
|
55
|
+
/**
|
|
56
|
+
* How many event logs a single session keeps. Count-based rather than
|
|
57
|
+
* age-based: a count is a hard bound (N logs per session, at most
|
|
58
|
+
* RUN_EVENT_LOG_MAX_BYTES each), while an age window still admits unbounded
|
|
59
|
+
* growth from a session that runs in a tight loop.
|
|
60
|
+
*/
|
|
61
|
+
export const RUN_EVENT_LOG_RETENTION = 5;
|
|
62
|
+
const LOG_SUFFIX = ".events.jsonl";
|
|
63
|
+
const NEWLINE = 0x0a;
|
|
64
|
+
/** The only byte the log ever adds of its own: a line terminator. */
|
|
65
|
+
const TERMINATOR = Buffer.from("\n", "utf-8");
|
|
66
|
+
/**
|
|
67
|
+
* Filename-safe form of a run id. The id reaches a path, so it is sanitized
|
|
68
|
+
* rather than trusted: anything outside `[A-Za-z0-9_-]` (`/`, `.`, `..`) becomes
|
|
69
|
+
* `-` before it can traverse out of the sessions dir.
|
|
70
|
+
*/
|
|
71
|
+
export function runEventLogSegment(runId) {
|
|
72
|
+
const safe = runId.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 64);
|
|
73
|
+
return safe === "" ? "unknown" : safe;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Log path for one run. Keys on the session's canonical `normalizedId` exactly
|
|
77
|
+
* as the transcript sidecar does, so both files for a session sort together and
|
|
78
|
+
* neither can be derived from a raw route id.
|
|
79
|
+
*/
|
|
80
|
+
export function runEventLogPath(projectDir, sessionId, runId) {
|
|
81
|
+
const name = `${normalizeIdentifier(sessionId)}.run-${runEventLogSegment(runId)}${LOG_SUFFIX}`;
|
|
82
|
+
return join(projectDir, "sessions", name);
|
|
83
|
+
}
|
|
84
|
+
/** The handle a run with no log target gets — every method is a no-op. */
|
|
85
|
+
const NOOP_HANDLE = {
|
|
86
|
+
path: "",
|
|
87
|
+
push() { },
|
|
88
|
+
flush() { },
|
|
89
|
+
close() {
|
|
90
|
+
return { lines: 0, bytes: 0, truncated: false };
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Opens (creating) the run's log and returns a write-through handle.
|
|
95
|
+
*
|
|
96
|
+
* The file is created up front, so a run that produces no stdout at all — a
|
|
97
|
+
* child killed at its deadline before it spoke, an immediate non-zero exit —
|
|
98
|
+
* still leaves the log behind. `undefined` target yields the no-op handle, which
|
|
99
|
+
* is how the runner stays usable without a project dir (tests, and any caller
|
|
100
|
+
* that does not want a log).
|
|
101
|
+
*/
|
|
102
|
+
export function openRunEventLog(target) {
|
|
103
|
+
if (target === undefined)
|
|
104
|
+
return NOOP_HANDLE;
|
|
105
|
+
const path = runEventLogPath(target.projectDir, target.sessionId, target.runId);
|
|
106
|
+
let fd = null;
|
|
107
|
+
let lines = 0;
|
|
108
|
+
let bytes = 0;
|
|
109
|
+
let truncated = false;
|
|
110
|
+
let closed = false;
|
|
111
|
+
let endsWithNewline = true;
|
|
112
|
+
/**
|
|
113
|
+
* Bytes were dropped part-way through a record, so the stream now continues
|
|
114
|
+
* INSIDE a line this file does not have. Appending those bytes would splice
|
|
115
|
+
* the tail of a record the log never took onto whatever precedes it and emit
|
|
116
|
+
* a line the child never wrote, so they are dropped until the next newline.
|
|
117
|
+
*/
|
|
118
|
+
let resyncing = false;
|
|
119
|
+
let error;
|
|
120
|
+
const note = (err) => {
|
|
121
|
+
if (error === undefined)
|
|
122
|
+
error = String(err);
|
|
123
|
+
};
|
|
124
|
+
try {
|
|
125
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
126
|
+
// "a" — the kernel appends at every write, so nothing here has to track a
|
|
127
|
+
// file position and a concurrent reader (the tail) can never see a hole.
|
|
128
|
+
fd = openSync(path, "a");
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
note(err);
|
|
132
|
+
}
|
|
133
|
+
const stats = () => ({
|
|
134
|
+
lines,
|
|
135
|
+
bytes,
|
|
136
|
+
truncated,
|
|
137
|
+
...(error !== undefined && { error }),
|
|
138
|
+
});
|
|
139
|
+
/**
|
|
140
|
+
* Appends `buf` whole, returning how many of its bytes actually landed.
|
|
141
|
+
*
|
|
142
|
+
* `writeSync` may return SHORT — a signal interrupting the syscall (EINTR), a
|
|
143
|
+
* filesystem that ran out of room mid-buffer (ENOSPC) — and its return value
|
|
144
|
+
* is the only place that ever shows. Discarding it emits half a record while
|
|
145
|
+
* counting a whole one, which breaks "always ends on a line boundary" exactly
|
|
146
|
+
* when the disk is full. So the loop keeps writing while the fd makes
|
|
147
|
+
* progress, and reports precisely how far it got. `n <= 0` ends the loop
|
|
148
|
+
* rather than spinning on an fd that has stopped accepting bytes.
|
|
149
|
+
*/
|
|
150
|
+
const writeAll = (handle, buf) => {
|
|
151
|
+
let written = 0;
|
|
152
|
+
while (written < buf.length) {
|
|
153
|
+
let n;
|
|
154
|
+
try {
|
|
155
|
+
n = writeSync(handle, buf, written, buf.length - written);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
note(err);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
if (n <= 0)
|
|
162
|
+
break;
|
|
163
|
+
written += n;
|
|
164
|
+
}
|
|
165
|
+
return written;
|
|
166
|
+
};
|
|
167
|
+
/** Counts what landed: bytes, whole lines, and where the file now ends. */
|
|
168
|
+
const account = (buf) => {
|
|
169
|
+
if (buf.length === 0)
|
|
170
|
+
return;
|
|
171
|
+
bytes += buf.length;
|
|
172
|
+
// Native indexOf rather than a per-byte loop: a megabyte chunk must not
|
|
173
|
+
// cost a million iterations on the event loop.
|
|
174
|
+
for (let at = buf.indexOf(NEWLINE); at !== -1; at = buf.indexOf(NEWLINE, at + 1)) {
|
|
175
|
+
lines += 1;
|
|
176
|
+
}
|
|
177
|
+
endsWithNewline = buf[buf.length - 1] === NEWLINE;
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Closes a line the child left open. The only byte the log adds of its own,
|
|
181
|
+
* and only when the file would otherwise not end on a line boundary — an
|
|
182
|
+
* offset-based tail would mis-frame the last record without it.
|
|
183
|
+
*
|
|
184
|
+
* Runs at flush() AND the instant a chunk is refused or lands short, not just
|
|
185
|
+
* at close: after a gap in the stream the next accepted chunk must start its
|
|
186
|
+
* own line rather than extend the fragment before the gap.
|
|
187
|
+
*
|
|
188
|
+
* The terminator write can itself fail, and failing SILENTLY is how a
|
|
189
|
+
* fabricated line survives every other guard here: the file is left
|
|
190
|
+
* un-line-closed with `resyncing` unset, so an fd that recovers splices the
|
|
191
|
+
* next chunk onto the open fragment. A terminator that does not land therefore
|
|
192
|
+
* sets BOTH flags — `truncated` because the file is no longer line-framed, and
|
|
193
|
+
* `resyncing` because nothing may extend that fragment ever again.
|
|
194
|
+
*/
|
|
195
|
+
const terminate = (handle) => {
|
|
196
|
+
if (endsWithNewline || bytes === 0)
|
|
197
|
+
return;
|
|
198
|
+
if (writeAll(handle, TERMINATOR) !== TERMINATOR.length) {
|
|
199
|
+
truncated = true;
|
|
200
|
+
resyncing = true;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
account(TERMINATOR);
|
|
204
|
+
};
|
|
205
|
+
return {
|
|
206
|
+
path,
|
|
207
|
+
push(chunk) {
|
|
208
|
+
if (fd === null || closed)
|
|
209
|
+
return;
|
|
210
|
+
let buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8");
|
|
211
|
+
if (buf.length === 0)
|
|
212
|
+
return;
|
|
213
|
+
if (resyncing) {
|
|
214
|
+
const at = buf.indexOf(NEWLINE);
|
|
215
|
+
// Still inside the record whose bytes were dropped — all of it belongs
|
|
216
|
+
// to that record, none of it is a line this file may claim to hold.
|
|
217
|
+
if (at === -1)
|
|
218
|
+
return;
|
|
219
|
+
buf = buf.subarray(at + 1);
|
|
220
|
+
resyncing = false;
|
|
221
|
+
// The stream is back at a record start, but the FILE may not be: a
|
|
222
|
+
// terminator that failed AT the gap left an open fragment behind, and
|
|
223
|
+
// knowing where the stream resumed does nothing about it. Close it here,
|
|
224
|
+
// before any of these bytes can land on it — the two questions are
|
|
225
|
+
// different and only this one keeps the next whole record off the
|
|
226
|
+
// fragment. A no-op whenever the file already ends on a boundary.
|
|
227
|
+
terminate(fd);
|
|
228
|
+
// Still open: the fd will not take even one byte, so stay resynced (a
|
|
229
|
+
// failed terminate re-arms it) and drop this record too. A fabricated
|
|
230
|
+
// line is not detectable downstream; a missing one is.
|
|
231
|
+
if (resyncing)
|
|
232
|
+
return;
|
|
233
|
+
if (buf.length === 0)
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (bytes + buf.length >= RUN_EVENT_LOG_MAX_BYTES) {
|
|
237
|
+
// The CROSSING chunk is refused whole — never half a record — but the
|
|
238
|
+
// log stays open. Returning early here for the rest of the run turned
|
|
239
|
+
// one oversized chunk into a 0-byte file; now every later chunk that
|
|
240
|
+
// still fits is still kept, and `truncated` is what says the file is no
|
|
241
|
+
// longer the whole stream. `>=` reserves the byte terminate() may add,
|
|
242
|
+
// so the finished file never exceeds the size the fold agrees to read.
|
|
243
|
+
truncated = true;
|
|
244
|
+
// The dropped bytes end mid-record unless the chunk's own last byte was
|
|
245
|
+
// a terminator; only then does the next chunk start a fresh line.
|
|
246
|
+
resyncing = buf[buf.length - 1] !== NEWLINE;
|
|
247
|
+
terminate(fd);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const written = writeAll(fd, buf);
|
|
251
|
+
account(buf.subarray(0, written));
|
|
252
|
+
if (written === buf.length)
|
|
253
|
+
return;
|
|
254
|
+
// A short write: this record is on disk in part only and the stream
|
|
255
|
+
// continues from bytes that are gone. Same treatment as the cap — say the
|
|
256
|
+
// file is incomplete, close the half-written line, and skip to the next
|
|
257
|
+
// boundary instead of splicing the next chunk onto a fragment.
|
|
258
|
+
truncated = true;
|
|
259
|
+
// The lost bytes are `buf[written..]`, so the stream stands exactly where
|
|
260
|
+
// it does after a refusal: at `buf`'s last byte. Same rule as the cap,
|
|
261
|
+
// deliberately — hardcoding a resync here is safe in direction but throws
|
|
262
|
+
// away a whole record that begins at a record boundary, for nothing.
|
|
263
|
+
resyncing = buf[buf.length - 1] !== NEWLINE;
|
|
264
|
+
terminate(fd);
|
|
265
|
+
},
|
|
266
|
+
flush() {
|
|
267
|
+
if (fd === null || closed)
|
|
268
|
+
return;
|
|
269
|
+
terminate(fd);
|
|
270
|
+
},
|
|
271
|
+
close() {
|
|
272
|
+
if (fd !== null && !closed) {
|
|
273
|
+
try {
|
|
274
|
+
closeSync(fd);
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
note(err);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
closed = true;
|
|
281
|
+
return stats();
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const EMPTY_FOLD = {
|
|
286
|
+
appended: 0,
|
|
287
|
+
alreadyFolded: false,
|
|
288
|
+
assistantTextFolded: false,
|
|
289
|
+
};
|
|
290
|
+
function isBlock(value) {
|
|
291
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Turns one stream-json `assistant` event yields, in content-block order.
|
|
295
|
+
*
|
|
296
|
+
* Consecutive text blocks coalesce into one turn; every `tool_use` block gets
|
|
297
|
+
* its own turn (unlike the transcript mirror, which keeps only the first) so a
|
|
298
|
+
* message that fans out to three tools reads as three rows. Only completed
|
|
299
|
+
* `assistant` messages are folded: `stream_event` deltas repeat their text and
|
|
300
|
+
* the terminal `result` repeats the final message, so both would duplicate.
|
|
301
|
+
*/
|
|
302
|
+
function foldAssistantEvent(event) {
|
|
303
|
+
const message = event.message;
|
|
304
|
+
if (!isBlock(message))
|
|
305
|
+
return [];
|
|
306
|
+
const content = message.content;
|
|
307
|
+
if (typeof content === "string")
|
|
308
|
+
return content === "" ? [] : [{ text: content }];
|
|
309
|
+
if (!Array.isArray(content))
|
|
310
|
+
return [];
|
|
311
|
+
const turns = [];
|
|
312
|
+
let text = "";
|
|
313
|
+
const flushText = () => {
|
|
314
|
+
if (text === "")
|
|
315
|
+
return;
|
|
316
|
+
turns.push({ text });
|
|
317
|
+
text = "";
|
|
318
|
+
};
|
|
319
|
+
for (const block of content) {
|
|
320
|
+
if (!isBlock(block))
|
|
321
|
+
continue;
|
|
322
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
323
|
+
text += block.text;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (block.type === "tool_use" && typeof block.name === "string") {
|
|
327
|
+
flushText();
|
|
328
|
+
turns.push({ text: "", tool: { name: block.name } });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
flushText();
|
|
332
|
+
return turns;
|
|
333
|
+
}
|
|
334
|
+
/** Every turn the log implies, in the order the child emitted them. */
|
|
335
|
+
function foldTurns(raw) {
|
|
336
|
+
const turns = [];
|
|
337
|
+
for (const line of raw.split("\n")) {
|
|
338
|
+
const text = (line.endsWith("\r") ? line.slice(0, -1) : line).trim();
|
|
339
|
+
if (text === "")
|
|
340
|
+
continue;
|
|
341
|
+
let value;
|
|
342
|
+
try {
|
|
343
|
+
value = JSON.parse(text);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
continue; // unparsable lines stay in the log; they just fold to nothing
|
|
347
|
+
}
|
|
348
|
+
if (!isBlock(value) || value.type !== "assistant")
|
|
349
|
+
continue;
|
|
350
|
+
turns.push(...foldAssistantEvent(value));
|
|
351
|
+
}
|
|
352
|
+
return turns;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Folds a run's event log down into the session's transcript sidecar: assistant
|
|
356
|
+
* text plus one turn per `tool_use`, appended through `appendSessionTurn` in
|
|
357
|
+
* stream order, each tagged with the run id.
|
|
358
|
+
*
|
|
359
|
+
* Idempotence mechanism: the fold's OWN OUTPUT is the marker. Every appended
|
|
360
|
+
* turn carries `run: <runId>`, and a fold refuses as soon as the sidecar holds
|
|
361
|
+
* any turn tagged with that id. This was chosen over a separate marker (a
|
|
362
|
+
* `foldedAt` on the run record, a `.folded` sentinel file) precisely because a
|
|
363
|
+
* separate marker has a window: a crash between "turns appended" and "marker
|
|
364
|
+
* written" would re-fold and duplicate the whole run on the next settle. Here
|
|
365
|
+
* there is no window — the evidence and the mark are the same bytes. The cost
|
|
366
|
+
* is at-most-once rather than exactly-once under a crash MID-fold (the turns
|
|
367
|
+
* already appended are tagged, so the remainder is never completed), which is
|
|
368
|
+
* the safe direction: a short transcript, never a doubled one.
|
|
369
|
+
*
|
|
370
|
+
* Never throws — a fold that cannot read the log or write the sidecar reports
|
|
371
|
+
* an empty result and the settle continues.
|
|
372
|
+
*/
|
|
373
|
+
export async function foldRunEventLog(projectDir, sessionId, runId) {
|
|
374
|
+
try {
|
|
375
|
+
const path = runEventLogPath(projectDir, sessionId, runId);
|
|
376
|
+
const info = await stat(path);
|
|
377
|
+
if (!info.isFile() || info.size > RUN_EVENT_LOG_MAX_BYTES)
|
|
378
|
+
return EMPTY_FOLD;
|
|
379
|
+
// The mark IS the data: any turn already tagged with this run id means a
|
|
380
|
+
// previous fold got there first.
|
|
381
|
+
const existing = await readSessionTurns(projectDir, sessionId);
|
|
382
|
+
const tagged = existing.filter((turn) => turn.run === runId);
|
|
383
|
+
if (tagged.length > 0) {
|
|
384
|
+
return {
|
|
385
|
+
appended: 0,
|
|
386
|
+
alreadyFolded: true,
|
|
387
|
+
assistantTextFolded: tagged.some((turn) => turn.type === "assistant" && turn.text !== ""),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const turns = foldTurns(await readFile(path, "utf-8"));
|
|
391
|
+
if (turns.length === 0)
|
|
392
|
+
return EMPTY_FOLD;
|
|
393
|
+
let appended = 0;
|
|
394
|
+
let assistantTextFolded = false;
|
|
395
|
+
for (const turn of turns) {
|
|
396
|
+
// Sequential on purpose: `appendSessionTurn` mints its negative id from
|
|
397
|
+
// what is already on disk, so concurrent appends would collide.
|
|
398
|
+
await appendSessionTurn(projectDir, sessionId, {
|
|
399
|
+
type: "assistant",
|
|
400
|
+
text: turn.text,
|
|
401
|
+
run: runId,
|
|
402
|
+
...(turn.tool !== undefined && { tool: turn.tool }),
|
|
403
|
+
});
|
|
404
|
+
appended += 1;
|
|
405
|
+
if (turn.tool === undefined && turn.text !== "")
|
|
406
|
+
assistantTextFolded = true;
|
|
407
|
+
}
|
|
408
|
+
return { appended, alreadyFolded: false, assistantTextFolded };
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// Missing log, unreadable sidecar, a full disk — all fold to nothing.
|
|
412
|
+
return EMPTY_FOLD;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
// Retention
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
/**
|
|
419
|
+
* Keeps the newest `keep` event logs for one session and deletes the rest.
|
|
420
|
+
*
|
|
421
|
+
* Called at EVERY settle (after the fold), so the log that just settled is
|
|
422
|
+
* always among the survivors and the sessions dir is bounded at `keep` logs per
|
|
423
|
+
* session no matter how many runs a session accumulates. "Every settle" means
|
|
424
|
+
* the startup orphan sweep too (`settleOrphanedRuns`): a server that dies
|
|
425
|
+
* mid-run never reaches the route's write-back, so without that call site a
|
|
426
|
+
* session whose runs are always interrupted grows one capped log per run
|
|
427
|
+
* forever. `keep: 0` drops them all — what session deletion uses.
|
|
428
|
+
*
|
|
429
|
+
* Never throws; returns how many files it removed.
|
|
430
|
+
*/
|
|
431
|
+
export async function pruneRunEventLogs(projectDir, sessionId, keep = RUN_EVENT_LOG_RETENTION) {
|
|
432
|
+
try {
|
|
433
|
+
const sessionsDir = join(projectDir, "sessions");
|
|
434
|
+
const prefix = `${normalizeIdentifier(sessionId)}.run-`;
|
|
435
|
+
const names = (await readdir(sessionsDir)).filter((name) => name.startsWith(prefix) &&
|
|
436
|
+
name.endsWith(LOG_SUFFIX) &&
|
|
437
|
+
name.length > prefix.length + LOG_SUFFIX.length);
|
|
438
|
+
if (names.length <= keep)
|
|
439
|
+
return 0;
|
|
440
|
+
const dated = await Promise.all(names.map(async (name) => {
|
|
441
|
+
try {
|
|
442
|
+
return { name, mtimeMs: (await stat(join(sessionsDir, name))).mtimeMs };
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
// Vanished under us — sort it to the front so it is dropped first.
|
|
446
|
+
return { name, mtimeMs: 0 };
|
|
447
|
+
}
|
|
448
|
+
}));
|
|
449
|
+
// Newest first; the name breaks mtime ties (same-millisecond runs) so the
|
|
450
|
+
// ordering is total and the prune is deterministic.
|
|
451
|
+
dated.sort((a, b) => b.mtimeMs - a.mtimeMs || (a.name < b.name ? 1 : -1));
|
|
452
|
+
let pruned = 0;
|
|
453
|
+
for (const entry of dated.slice(Math.max(keep, 0))) {
|
|
454
|
+
try {
|
|
455
|
+
await unlink(join(sessionsDir, entry.name));
|
|
456
|
+
pruned += 1;
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
// Already gone or locked — retention is best-effort.
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return pruned;
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
return 0;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
//# sourceMappingURL=run-event-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-event-log.js","sourceRoot":"","sources":["../../src/web-server/run-event-log.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session liveness reconciliation — the world's opinion on top of the store's.
|
|
3
|
+
*
|
|
4
|
+
* `deriveSessionPhase` (session-store) answers "what does this record claim" from
|
|
5
|
+
* persisted evidence alone. This module answers "is that still true", using the
|
|
6
|
+
* two liveness sources that exist outside the DAG:
|
|
7
|
+
*
|
|
8
|
+
* - a run claim's pid, probed with signal 0. ARCS-driven runs are headless
|
|
9
|
+
* `claude -p` children ARCS spawned itself, so their liveness is a local
|
|
10
|
+
* process fact — and the persisted pid is what makes it readable from a
|
|
11
|
+
* server process that did not spawn them.
|
|
12
|
+
* - `claude agents --json`, which lists the live interactive/background
|
|
13
|
+
* sessions a terminal is driving. That is the only way to tell a session ARCS
|
|
14
|
+
* merely observes from one whose terminal has since been closed.
|
|
15
|
+
*
|
|
16
|
+
* Two shapes of work, deliberately separated:
|
|
17
|
+
*
|
|
18
|
+
* - `settleOrphanedRuns` WRITES: a claim whose process is gone settles as
|
|
19
|
+
* outcome `interrupted`, the claim is cleared and the session's run event
|
|
20
|
+
* logs are pruned. Runs once at startup (see app.ts), because a claim
|
|
21
|
+
* persisted by a previous process can never be live in this one unless its
|
|
22
|
+
* child outlived the restart.
|
|
23
|
+
* - `reconcileSessionPhases` READS: it returns demoted phases and never writes.
|
|
24
|
+
* On-demand only — there is no timer here, and no poller.
|
|
25
|
+
*
|
|
26
|
+
* Everything that touches the outside world is tolerant by construction: the
|
|
27
|
+
* probe is a subprocess of a binary that may not exist, running a command whose
|
|
28
|
+
* output shape is claude's to change. A missing binary, a non-zero exit, an
|
|
29
|
+
* empty stdout or unparsable JSON all degrade to the store-derived phase and
|
|
30
|
+
* NEVER throw — a reconciler that can fail would take the sessions UI down with
|
|
31
|
+
* every claude release.
|
|
32
|
+
*/
|
|
33
|
+
import { type SessionMeta, type SessionPhase } from "../utils/session-store.js";
|
|
34
|
+
/**
|
|
35
|
+
* One live agent as reported by `claude agents --json` (verified on claude
|
|
36
|
+
* 2.1.223: live interactive and background sessions, `{pid, cwd, sessionId,
|
|
37
|
+
* name, state|status}`). Every field is optional because the wire shape belongs
|
|
38
|
+
* to claude — a release that renames one must degrade this module, not break it.
|
|
39
|
+
*/
|
|
40
|
+
export interface LiveAgent {
|
|
41
|
+
pid?: number;
|
|
42
|
+
cwd?: string;
|
|
43
|
+
sessionId?: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
/** `status`, falling back to `state` — reported, never branched on. */
|
|
46
|
+
status?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Raw probe result: `ok:false` covers missing binary, non-zero exit, timeout. */
|
|
49
|
+
export type AgentsProbeResult = {
|
|
50
|
+
ok: true;
|
|
51
|
+
stdout: string;
|
|
52
|
+
} | {
|
|
53
|
+
ok: false;
|
|
54
|
+
};
|
|
55
|
+
export type AgentsProbe = () => Promise<AgentsProbeResult>;
|
|
56
|
+
/**
|
|
57
|
+
* Parses `claude agents --json` stdout into live agents.
|
|
58
|
+
*
|
|
59
|
+
* `null` means "the probe answered nothing usable" (empty output, non-JSON, a
|
|
60
|
+
* JSON shape carrying no array) and is NOT the same as `[]`, which means "the
|
|
61
|
+
* probe worked and no agent is live". Only the second may demote a session:
|
|
62
|
+
* conflating them would mark every observed session idle the day claude changes
|
|
63
|
+
* its output.
|
|
64
|
+
*/
|
|
65
|
+
export declare function parseLiveAgents(stdout: string): LiveAgent[] | null;
|
|
66
|
+
/**
|
|
67
|
+
* Runs the probe and parses it. `null` on every degraded path — missing binary,
|
|
68
|
+
* non-zero exit, timeout, unparsable output, or a probe that threw.
|
|
69
|
+
*/
|
|
70
|
+
export declare function listLiveClaudeAgents(options?: {
|
|
71
|
+
probe?: AgentsProbe;
|
|
72
|
+
}): Promise<LiveAgent[] | null>;
|
|
73
|
+
/**
|
|
74
|
+
* Whether a pid is a live process, via signal 0 (no signal delivered).
|
|
75
|
+
*
|
|
76
|
+
* SHORTCUT: pids are reused, so a recycled pid reads alive and keeps an orphan
|
|
77
|
+
* claim standing; upgrade to a start-time check (`/proc/<pid>/stat` field 22 or
|
|
78
|
+
* `ps -o lstart`) when a stuck claim is ever actually observed.
|
|
79
|
+
*/
|
|
80
|
+
export declare function isProcessAlive(pid: number): boolean;
|
|
81
|
+
export interface ReconcileOptions {
|
|
82
|
+
/** Injectable `claude agents --json` probe (tests). */
|
|
83
|
+
probe?: AgentsProbe;
|
|
84
|
+
/** Injectable pid liveness (tests). */
|
|
85
|
+
isAlive?: (pid: number) => boolean;
|
|
86
|
+
/** Clock in epoch ms; defaults to `Date.now()`. */
|
|
87
|
+
now?: number;
|
|
88
|
+
}
|
|
89
|
+
export interface SessionPhaseView {
|
|
90
|
+
sessionId: string;
|
|
91
|
+
phase: SessionPhase;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The store-derived phase, demoted when the world disagrees.
|
|
95
|
+
*
|
|
96
|
+
* Demotion is one-way: this can turn `running` into `idle`, never the reverse.
|
|
97
|
+
* Nothing outside the record may promote a session — the DAG stays the source
|
|
98
|
+
* of truth and the probe is only ever allowed to take evidence away.
|
|
99
|
+
*
|
|
100
|
+
* Which evidence applies depends on what the record claims, not on its origin:
|
|
101
|
+
* - a run claim is checked against its pid. `claude agents` never lists a
|
|
102
|
+
* headless `claude -p` child, so asking it about an ARCS run would demote
|
|
103
|
+
* every healthy run.
|
|
104
|
+
* - everything else is checked against the agent list. `agents === null` (the
|
|
105
|
+
* probe degraded) leaves the `lastCheckpointAt`-derived phase untouched.
|
|
106
|
+
*/
|
|
107
|
+
export declare function reconcilePhase(session: SessionMeta, agents: LiveAgent[] | null, options?: ReconcileOptions): SessionPhase;
|
|
108
|
+
/**
|
|
109
|
+
* Reconciled phase for every session in a project. Read-only: no writes, no
|
|
110
|
+
* timer, and at most ONE `claude agents --json` for the whole index — never one
|
|
111
|
+
* per session.
|
|
112
|
+
*
|
|
113
|
+
* The probe is LAZY: `claude agents --json` costs ~0.35s of wall clock and a
|
|
114
|
+
* whole subprocess, and its answer is read at a single branch of
|
|
115
|
+
* `reconcilePhase`. Every terminal record, every idle one and every ARCS run
|
|
116
|
+
* holding a claim decides without it, so a probe fired before the phases are
|
|
117
|
+
* derived is a subprocess whose result is thrown away — on EVERY sessions
|
|
118
|
+
* request, list and detail alike, at whatever rate the watcher invalidates the
|
|
119
|
+
* query. So the derivation runs first and the probe only follows when some
|
|
120
|
+
* record actually reaches that branch.
|
|
121
|
+
*
|
|
122
|
+
* Skipping it passes `null` — the same value a degraded probe yields, and the
|
|
123
|
+
* value no session left in this pass can read: reaching the `agents` branch is
|
|
124
|
+
* precisely the condition that would have run the probe.
|
|
125
|
+
*/
|
|
126
|
+
export declare function reconcileSessionPhases(projectDir: string, options?: ReconcileOptions): Promise<SessionPhaseView[]>;
|
|
127
|
+
export interface SettledOrphan {
|
|
128
|
+
sessionId: string;
|
|
129
|
+
runId: string;
|
|
130
|
+
pid?: number;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Settles every run claim in a project whose process is gone.
|
|
134
|
+
*
|
|
135
|
+
* A claim only survives when its pid is still alive — a run spawned by a
|
|
136
|
+
* previous server process either outlived the restart (leave it be; it still
|
|
137
|
+
* holds the write-target) or died with it, and a dead claim that is never
|
|
138
|
+
* cleared is a session stuck on `running` for good. The settled run keeps
|
|
139
|
+
* whatever `metadata.run` already held and gains `outcome: "interrupted"`,
|
|
140
|
+
* which no live run can ever produce.
|
|
141
|
+
*
|
|
142
|
+
* A settle here also prunes the session's run event logs, exactly as the route's
|
|
143
|
+
* own write-back does — this is the ONLY settle an interrupted run ever gets, so
|
|
144
|
+
* it is the only place that can bound the logs of a session whose runs never
|
|
145
|
+
* settle normally.
|
|
146
|
+
*
|
|
147
|
+
* Never throws: a project whose index cannot be read is skipped, and a record
|
|
148
|
+
* deleted mid-sweep is simply not settled.
|
|
149
|
+
*/
|
|
150
|
+
export declare function settleOrphanedRuns(projectDir: string, options?: ReconcileOptions): Promise<SettledOrphan[]>;
|
|
151
|
+
/**
|
|
152
|
+
* Startup sweep across every registered project.
|
|
153
|
+
*
|
|
154
|
+
* Composition-time work (app.ts calls it once, fire-and-forget) but the logic
|
|
155
|
+
* lives here. One pass, no timer: liveness questions after startup are answered
|
|
156
|
+
* on demand by `reconcileSessionPhases`.
|
|
157
|
+
*
|
|
158
|
+
* Never throws — a data dir without a readable meta.json settles nothing and
|
|
159
|
+
* reports it, rather than taking the server down at boot.
|
|
160
|
+
*/
|
|
161
|
+
export declare function settleOrphanedRunsOnStartup(dataDir?: string, options?: ReconcileOptions): Promise<SettledOrphan[]>;
|
|
162
|
+
//# sourceMappingURL=session-reconciler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-reconciler.d.ts","sourceRoot":"","sources":["../../src/web-server/session-reconciler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAMH,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,YAAY,EAGlB,MAAM,2BAA2B,CAAC;AAQnC;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,kFAAkF;AAClF,MAAM,MAAM,iBAAiB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC;AAC7E,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAoF3D;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,IAAI,CAiBlE;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,WAAW,CAAA;CAAO,GACpC,OAAO,CAAC,SAAS,EAAE,GAAG,IAAI,CAAC,CAU7B;AAMD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CASnD;AAMD,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,uCAAuC;IACvC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACnC,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,YAAY,CAAC;CACrB;AAcD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,EAC1B,OAAO,GAAE,gBAAqB,GAC7B,YAAY,CAad;AAeD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAa7B;AAMD,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAQD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,aAAa,EAAE,CAAC,CAqC1B;AAED;;;;;;;;;GASG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,GAAE,MAAqB,EAC9B,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,aAAa,EAAE,CAAC,CAa1B"}
|