brane 0.1.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/AGENTS.md +367 -0
  2. package/CHANGELOG.md +66 -0
  3. package/CONTRIBUTING.md +101 -0
  4. package/DESIGN.md +405 -0
  5. package/Dockerfile +61 -0
  6. package/LICENSE +656 -17
  7. package/README.md +174 -2
  8. package/SECURITY.md +213 -0
  9. package/dist/absorb/brane-id.js +11 -0
  10. package/dist/absorb/chatgpt.js +161 -0
  11. package/dist/absorb/claude-code.js +504 -0
  12. package/dist/absorb/index.js +543 -0
  13. package/dist/absorb/organelle.js +345 -0
  14. package/dist/absorb/run.js +520 -0
  15. package/dist/absorb/sources.js +168 -0
  16. package/dist/absorb/types.js +3 -0
  17. package/dist/agent-env.js +45 -0
  18. package/dist/agents.js +1168 -0
  19. package/dist/claude-live-session.js +124 -0
  20. package/dist/cli.js +50 -0
  21. package/dist/codex-tool-log.js +144 -0
  22. package/dist/commands/absorb.js +291 -0
  23. package/dist/commands/attach.js +31 -0
  24. package/dist/commands/certify.js +62 -0
  25. package/dist/commands/config.js +133 -0
  26. package/dist/commands/cost.js +126 -0
  27. package/dist/commands/daemon.js +350 -0
  28. package/dist/commands/doctor.js +262 -0
  29. package/dist/commands/fold.js +95 -0
  30. package/dist/commands/list.js +89 -0
  31. package/dist/commands/mcp.js +84 -0
  32. package/dist/commands/meld.js +360 -0
  33. package/dist/commands/memory.js +215 -0
  34. package/dist/commands/revive.js +74 -0
  35. package/dist/commands/run.js +85 -0
  36. package/dist/commands/spawn.js +1101 -0
  37. package/dist/commands/token.js +154 -0
  38. package/dist/copilot-tool-log.js +145 -0
  39. package/dist/cost/pricing.js +116 -0
  40. package/dist/cost/reprice.js +136 -0
  41. package/dist/crush-tool-log.js +293 -0
  42. package/dist/daemon/agent-scope.js +116 -0
  43. package/dist/daemon/awaiting.js +54 -0
  44. package/dist/daemon/cert-manager.js +349 -0
  45. package/dist/daemon/client.js +262 -0
  46. package/dist/daemon/gated-routes.js +112 -0
  47. package/dist/daemon/keep-awake.js +103 -0
  48. package/dist/daemon/local-socket.js +145 -0
  49. package/dist/daemon/presence-gate.js +206 -0
  50. package/dist/daemon/server.js +6353 -0
  51. package/dist/daemon/signal-source.js +35 -0
  52. package/dist/daemon/tailscale-rebind.js +58 -0
  53. package/dist/daemon/token-ring.js +263 -0
  54. package/dist/daemon/token-routes.js +190 -0
  55. package/dist/db/events.js +152 -0
  56. package/dist/db/mentions.js +179 -0
  57. package/dist/db/schema.js +643 -0
  58. package/dist/db/tokens.js +624 -0
  59. package/dist/db/tool-uses.js +647 -0
  60. package/dist/diagram/index.js +85 -0
  61. package/dist/diagram/label.js +455 -0
  62. package/dist/diagram/layout.js +476 -0
  63. package/dist/diagram/links.js +50 -0
  64. package/dist/diagram/metrics.js +152 -0
  65. package/dist/diagram/parse.js +487 -0
  66. package/dist/diagram/svg.js +389 -0
  67. package/dist/diagram/types.js +9 -0
  68. package/dist/export.js +629 -0
  69. package/dist/files/archive-types.js +2 -0
  70. package/dist/files/archive.js +182 -0
  71. package/dist/files/browse.js +327 -0
  72. package/dist/files/bytes.js +162 -0
  73. package/dist/files/codecs.js +33 -0
  74. package/dist/files/find.js +212 -0
  75. package/dist/files/tar.js +223 -0
  76. package/dist/files/zip.js +209 -0
  77. package/dist/folds.js +72 -0
  78. package/dist/grammars/ebnf.js +92 -0
  79. package/dist/highlight.js +250 -0
  80. package/dist/ids.js +24 -0
  81. package/dist/markdown-diagnostics.js +233 -0
  82. package/dist/math.js +168 -0
  83. package/dist/mcp/server.js +886 -0
  84. package/dist/memory/embedder.js +145 -0
  85. package/dist/memory/embedding-worker.js +71 -0
  86. package/dist/memory/embeddings.js +91 -0
  87. package/dist/memory/index.js +232 -0
  88. package/dist/memory/storage.js +587 -0
  89. package/dist/memory/wiki.js +688 -0
  90. package/dist/memory/wordpiece.js +247 -0
  91. package/dist/mentions-scan.js +40 -0
  92. package/dist/presence.js +259 -0
  93. package/dist/session-affinity.js +185 -0
  94. package/dist/shared/api-request.js +124 -0
  95. package/dist/shared/app-origin.js +26 -0
  96. package/dist/shared/block-glyphs.js +114 -0
  97. package/dist/shared/diagram-css.js +17 -0
  98. package/dist/shared/export-selection.js +101 -0
  99. package/dist/shared/find-in-text.js +110 -0
  100. package/dist/shared/glow-hues.js +225 -0
  101. package/dist/shared/hex-dump.js +62 -0
  102. package/dist/shared/hex-pages.js +52 -0
  103. package/dist/shared/hit-drum.js +79 -0
  104. package/dist/shared/known-nids.js +58 -0
  105. package/dist/shared/list-scroll-keys.js +56 -0
  106. package/dist/shared/markdown-lists.js +72 -0
  107. package/dist/shared/markdown-math.js +485 -0
  108. package/dist/shared/markdown-mentions.js +79 -0
  109. package/dist/shared/markdown-nids.js +41 -0
  110. package/dist/shared/markdown-protocols.js +53 -0
  111. package/dist/shared/markdown-refs.js +123 -0
  112. package/dist/shared/membrane-invasion.js +342 -0
  113. package/dist/shared/membrane-motion.js +83 -0
  114. package/dist/shared/membrane-palette.js +253 -0
  115. package/dist/shared/membrane-terrain.js +212 -0
  116. package/dist/shared/message-selection.js +123 -0
  117. package/dist/shared/mo-glyph.js +57 -0
  118. package/dist/shared/narrative-format.js +237 -0
  119. package/dist/shared/narrative-menu.js +84 -0
  120. package/dist/shared/narrative-sort.js +47 -0
  121. package/dist/shared/narrative-status.js +52 -0
  122. package/dist/shared/no-pan.js +85 -0
  123. package/dist/shared/pane-layout.js +50 -0
  124. package/dist/shared/payload-inline.js +83 -0
  125. package/dist/shared/speaker-color.js +61 -0
  126. package/dist/shared/token-rotation.js +14 -0
  127. package/dist/shared/view-url.js +216 -0
  128. package/dist/shared/wiki-links.js +70 -0
  129. package/dist/shared/wiki-window.js +4 -0
  130. package/dist/tailscale.js +55 -0
  131. package/dist/test/absorb-claude.test.js +892 -0
  132. package/dist/test/agent-scope.test.js +193 -0
  133. package/dist/test/agents.test.js +55 -0
  134. package/dist/test/api-request.test.js +122 -0
  135. package/dist/test/app-host-literal.test.js +53 -0
  136. package/dist/test/awaiting-glow.test.js +54 -0
  137. package/dist/test/awaiting.test.js +122 -0
  138. package/dist/test/bind-token.test.js +88 -0
  139. package/dist/test/block-glyphs.test.js +68 -0
  140. package/dist/test/cert-manager.test.js +262 -0
  141. package/dist/test/claude-live-session.test.js +110 -0
  142. package/dist/test/codex-tool-log.test.js +568 -0
  143. package/dist/test/config.test.js +71 -0
  144. package/dist/test/copilot-tool-log.test.js +93 -0
  145. package/dist/test/cors-preflight.test.js +56 -0
  146. package/dist/test/cost-linger.test.js +31 -0
  147. package/dist/test/cost-pricing.test.js +72 -0
  148. package/dist/test/cost-reprice.test.js +139 -0
  149. package/dist/test/cost-sessions.test.js +301 -0
  150. package/dist/test/crush-tool-log.test.js +289 -0
  151. package/dist/test/daemon-client.test.js +109 -0
  152. package/dist/test/daemon-socket.test.js +293 -0
  153. package/dist/test/diagram-arrows.test.js +205 -0
  154. package/dist/test/diagram-export.test.js +210 -0
  155. package/dist/test/diagram-labels.test.js +313 -0
  156. package/dist/test/diagram-parse.test.js +191 -0
  157. package/dist/test/diagram-render.test.js +175 -0
  158. package/dist/test/diagram-review.test.js +288 -0
  159. package/dist/test/diagram-self-loop-labels.test.js +303 -0
  160. package/dist/test/embedder.test.js +84 -0
  161. package/dist/test/events.test.js +123 -0
  162. package/dist/test/export-selection.test.js +102 -0
  163. package/dist/test/export.test.js +505 -0
  164. package/dist/test/files-browse.test.js +393 -0
  165. package/dist/test/files-find.test.js +272 -0
  166. package/dist/test/find-in-text.test.js +86 -0
  167. package/dist/test/first-paint-viewport.test.js +59 -0
  168. package/dist/test/fixtures/embedding-golden.js +23 -0
  169. package/dist/test/fixtures/wordpiece-golden.js +167 -0
  170. package/dist/test/glow-hues.test.js +202 -0
  171. package/dist/test/harness-cost.test.js +423 -0
  172. package/dist/test/helpers.js +21 -0
  173. package/dist/test/hex-pages.test.js +68 -0
  174. package/dist/test/highlight.test.js +69 -0
  175. package/dist/test/hit-drum.test.js +71 -0
  176. package/dist/test/hook-config.test.js +299 -0
  177. package/dist/test/json-gzip.test.js +70 -0
  178. package/dist/test/key-routing.test.js +58 -0
  179. package/dist/test/known-nids.test.js +106 -0
  180. package/dist/test/link-preview.test.js +67 -0
  181. package/dist/test/list-scroll-keys.test.js +66 -0
  182. package/dist/test/listen-own-words.test.js +147 -0
  183. package/dist/test/listen-timeout.test.js +33 -0
  184. package/dist/test/markdown-diagnostics.test.js +121 -0
  185. package/dist/test/markdown-lists.test.js +182 -0
  186. package/dist/test/markdown-math.test.js +256 -0
  187. package/dist/test/markdown-nids.test.js +146 -0
  188. package/dist/test/markdown-protocols.test.js +139 -0
  189. package/dist/test/mcp-bridge-cancel.test.js +98 -0
  190. package/dist/test/mcp-bridge-reconnect.test.js +121 -0
  191. package/dist/test/mcp-narrative.test.js +193 -0
  192. package/dist/test/mcp.test.js +548 -0
  193. package/dist/test/meld.test.js +143 -0
  194. package/dist/test/membrane-invasion.test.js +482 -0
  195. package/dist/test/membrane-motion.test.js +101 -0
  196. package/dist/test/membrane-palette.test.js +166 -0
  197. package/dist/test/membrane-terrain.test.js +170 -0
  198. package/dist/test/memory-log.test.js +202 -0
  199. package/dist/test/mentions-e2e.test.js +261 -0
  200. package/dist/test/mentions-ledger.test.js +265 -0
  201. package/dist/test/mentions-listen.test.js +265 -0
  202. package/dist/test/mentions.test.js +238 -0
  203. package/dist/test/message-selection.test.js +114 -0
  204. package/dist/test/mo-glyph.test.js +22 -0
  205. package/dist/test/model-resolution.test.js +166 -0
  206. package/dist/test/narrate-validate.test.js +77 -0
  207. package/dist/test/narrate-wait.test.js +160 -0
  208. package/dist/test/narrative-format.test.js +219 -0
  209. package/dist/test/narrative-menu.test.js +150 -0
  210. package/dist/test/narrative-sort.test.js +75 -0
  211. package/dist/test/no-pan.test.js +60 -0
  212. package/dist/test/pane-layout.test.js +153 -0
  213. package/dist/test/paths.test.js +37 -0
  214. package/dist/test/payload-inline.test.js +91 -0
  215. package/dist/test/precache-fonts.test.js +72 -0
  216. package/dist/test/presence-gate.test.js +440 -0
  217. package/dist/test/presence.test.js +160 -0
  218. package/dist/test/read-events.test.js +220 -0
  219. package/dist/test/remote-meld.test.js +128 -0
  220. package/dist/test/revive-cli.test.js +93 -0
  221. package/dist/test/search-floor.test.js +206 -0
  222. package/dist/test/search-fusion.test.js +168 -0
  223. package/dist/test/search.test.js +205 -0
  224. package/dist/test/security.test.js +48 -0
  225. package/dist/test/selection-wiring.test.js +61 -0
  226. package/dist/test/session-affinity.test.js +295 -0
  227. package/dist/test/signal-source.test.js +32 -0
  228. package/dist/test/socket-account.test.js +268 -0
  229. package/dist/test/spawn-toml.test.js +72 -0
  230. package/dist/test/speaker-color.test.js +90 -0
  231. package/dist/test/tailscale-rebind.test.js +79 -0
  232. package/dist/test/terminal-cursor.test.js +61 -0
  233. package/dist/test/terminal-resync.test.js +23 -0
  234. package/dist/test/terminal-size.test.js +36 -0
  235. package/dist/test/thin-cli.test.js +382 -0
  236. package/dist/test/token-bootstrap.test.js +272 -0
  237. package/dist/test/token-devices.test.js +646 -0
  238. package/dist/test/token-lapse.test.js +94 -0
  239. package/dist/test/token-ring.test.js +188 -0
  240. package/dist/test/token-rotate-cli.test.js +58 -0
  241. package/dist/test/token-routes.test.js +95 -0
  242. package/dist/test/tool-uses.test.js +1006 -0
  243. package/dist/test/usage-ledger.test.js +126 -0
  244. package/dist/test/view-url.test.js +627 -0
  245. package/dist/test/wiki-links.test.js +57 -0
  246. package/dist/test/wiki-sources.test.js +85 -0
  247. package/dist/test/wiki-window.test.js +31 -0
  248. package/dist/test/wiki.test.js +399 -0
  249. package/dist/test/wordpiece.test.js +68 -0
  250. package/dist/test/wrangler-route.test.js +27 -0
  251. package/dist/tls.js +148 -0
  252. package/dist/tmux.js +213 -0
  253. package/dist/user-agent.js +46 -0
  254. package/docker-entrypoint.sh +154 -0
  255. package/docs/README.md +3 -0
  256. package/docs/absorb.md +231 -0
  257. package/docs/cost-ledger.md +125 -0
  258. package/docs/daemon-api.md +189 -0
  259. package/docs/diagrams.md +163 -0
  260. package/docs/doctor.md +36 -0
  261. package/docs/export-scope.md +69 -0
  262. package/docs/files-view.md +133 -0
  263. package/docs/link-previews.md +38 -0
  264. package/docs/math.md +32 -0
  265. package/docs/mcp.md +69 -0
  266. package/docs/memory-search.md +75 -0
  267. package/docs/mentions.md +101 -0
  268. package/docs/multi-brane.md +14 -0
  269. package/docs/passkeys.md +165 -0
  270. package/docs/presence.md +202 -0
  271. package/docs/releasing.md +104 -0
  272. package/docs/rendering-warnings.md +22 -0
  273. package/docs/request-layer.md +72 -0
  274. package/docs/selection-store.md +50 -0
  275. package/docs/session-affinity.md +61 -0
  276. package/docs/terminal-mirroring.md +243 -0
  277. package/docs/token-rotation.md +431 -0
  278. package/docs/view-urls.md +134 -0
  279. package/docs/web-app.md +23 -0
  280. package/docs/wiki.md +43 -0
  281. package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
  282. package/expo/dist/apple-touch-icon.png +0 -0
  283. package/expo/dist/favicon.svg +3 -0
  284. package/expo/dist/index.html +177 -0
  285. package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  286. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  287. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  288. package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  289. package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  290. package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  291. package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  292. package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  293. package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  294. package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  295. package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  296. package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  297. package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  298. package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  299. package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  300. package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  301. package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  302. package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  303. package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  304. package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  305. package/expo/dist/katex/katex.min.css +1 -0
  306. package/expo/dist/manifest.json +16 -0
  307. package/expo/dist/metadata.json +1 -0
  308. package/expo/dist/og/membrane.v1.png +0 -0
  309. package/expo/dist/pdf.min.mjs +28 -0
  310. package/expo/dist/pdf.worker.min.mjs +28 -0
  311. package/expo/dist/sw.js +154 -0
  312. package/package.json +79 -27
  313. package/protocols/LIVE_loop.md +407 -0
  314. package/protocols/bebrief.md +41 -0
  315. package/protocols/digest.md +231 -0
  316. package/protocols/fly.md +266 -0
  317. package/protocols/idle.md +81 -0
  318. package/protocols/keeper.md +829 -0
  319. package/protocols/markdown.md +80 -0
  320. package/protocols/memory.md +419 -0
  321. package/protocols/mentions.md +93 -0
  322. package/protocols/payloads.md +53 -0
  323. package/protocols/prelude.md +91 -0
  324. package/protocols/ramify.md +105 -0
  325. package/protocols/review.md +356 -0
  326. package/protocols/rrr.md +232 -0
  327. package/protocols/scribe.md +429 -0
  328. package/protocols/summarize.md +203 -0
  329. package/protocols/twin.md +174 -0
  330. package/protocols/wiki.md +438 -0
  331. package/protocols/worker.md +38 -0
  332. package/wiki/shared/append-only-event-log.md +148 -0
  333. package/wiki/shared/index.md +53 -0
  334. package/wiki/shared/owning-your-narratives.md +161 -0
  335. package/wiki/shared/premature-ontologies.md +435 -0
  336. package/wiki/shared/sedimentary-superintelligence.md +128 -0
  337. package/wiki/shared/sequestering-the-narrative.md +143 -0
  338. package/wiki/shared/why-wikis-work.md +216 -0
  339. package/.travis.yml +0 -7
  340. package/lib/brane.cjs.js +0 -12
  341. package/lib/brane.cjs.js.map +0 -1
  342. package/lib/brane.d.ts +0 -11
  343. package/lib/brane.js +0 -6
  344. package/lib/brane.js.map +0 -1
  345. package/lib/tests.d.ts +0 -1
  346. package/lib/tests.js +0 -8
  347. package/lib/tests.js.map +0 -1
  348. package/rollup.config.js +0 -21
  349. package/scripts/test.sh +0 -8
  350. package/src/brane.ts +0 -21
  351. package/src/tests.ts +0 -8
  352. package/tsconfig.json +0 -16
@@ -0,0 +1,293 @@
1
+ /** Reading tool calls out of crush's own session log.
2
+ *
3
+ * Every other harness brane captures reports its tool calls by running a hook.
4
+ * crush cannot: it has a `PreToolUse` hook and no `PostToolUse` event at all,
5
+ * and its pre payload carries no call id — so the two halves of a call could
6
+ * never share a `dedup_key` even if both fired, and every call would strand as
7
+ * `pending`. What crush does have is a database that records both halves, and
8
+ * records the first one *while the call is still running*: a `tool_call` part
9
+ * is persisted with `finished: false` the moment the call starts (verified
10
+ * live against a 25s sleep by crush-cdcwzh2b, claude-pasen1co:24). That is the
11
+ * same pending→completed shape the hook path produces, arriving by a different
12
+ * road, which is what the `source: "session_log"` lane was declared for.
13
+ *
14
+ * Everything here is read-only against a database another process owns.
15
+ */
16
+ import Database from "better-sqlite3";
17
+ import { join } from "node:path";
18
+ import { statSync } from "node:fs";
19
+ import { recordToolUse } from "./db/tool-uses.js";
20
+ /** crush keeps one database per project, under the working directory. The data
21
+ * directory is configurable (`options.data_directory`), and a fold that moved
22
+ * it will simply read as "no log", which degrades to no capture. */
23
+ export function crushDbPath(foldPath) {
24
+ return join(foldPath, ".crush", "crush.db");
25
+ }
26
+ /** crush's own timestamp columns are in **seconds**, despite the schema comment
27
+ * on them saying milliseconds — the live rows read `created_at = 1786904402`,
28
+ * and its `AFTER UPDATE` trigger stamps `strftime('%s','now')`, which is
29
+ * seconds by definition. The `start_time`/`end_time` inside a tool result's
30
+ * metadata really are milliseconds. Mixing the two units in one file is easy
31
+ * and silent: a `created_at >= Date.now()` comparison matches nothing at all,
32
+ * forever, and looks exactly like a session with no tool calls. */
33
+ function toCrushSeconds(epochMs) {
34
+ return Math.floor(epochMs / 1000);
35
+ }
36
+ /** Session ids crush mints for its own bookkeeping rather than for a
37
+ * conversation: `title-<parentId>` for summary generation, and
38
+ * `<messageID>$$<toolCallID>` for a sub-agent invoked through its `agent`
39
+ * tool. Neither is the session a narrative is having. */
40
+ function isSyntheticSessionId(id) {
41
+ return id.startsWith("title-") || id.includes("$$");
42
+ }
43
+ /** The tool call that spawned a sub-agent session, from the session's own id.
44
+ * crush encodes it as `<messageID>$$<toolCallID>`, which is exactly the
45
+ * `parent_id` edge `tool_uses` records — so a sub-agent's calls nest under the
46
+ * call that started it instead of arriving as orphans. */
47
+ function parentToolCallId(sessionId) {
48
+ const marker = sessionId.indexOf("$$");
49
+ return marker === -1 ? undefined : sessionId.slice(marker + 2) || undefined;
50
+ }
51
+ /** crush double-encodes: `tool_call.input` and `tool_result.metadata` are JSON
52
+ * *strings* inside a JSON document. Parse them when they are, pass them
53
+ * through when they aren't, and never throw on either. */
54
+ function parseMaybeJson(value) {
55
+ if (typeof value !== "string")
56
+ return value;
57
+ try {
58
+ return JSON.parse(value);
59
+ }
60
+ catch {
61
+ return value;
62
+ }
63
+ }
64
+ export function newCrushLogCursor() {
65
+ return { updatedAt: 0, written: new Map(), signature: null };
66
+ }
67
+ /** A cheap fingerprint of the log's contents: size and mtime of the database
68
+ * *and of its `-wal` sibling*.
69
+ *
70
+ * The sibling is the whole point. In WAL mode a write lands in `-wal` and the
71
+ * main file's mtime does not move until a checkpoint — so a gate that stats
72
+ * only the database would go quiet during exactly the burst of activity it
73
+ * exists to catch, and look like a session that stopped making tool calls.
74
+ *
75
+ * Returns null when there is no log at all, which is also "nothing to do".
76
+ * This is the poller's whole idle cost: two stats, no open, no query. */
77
+ export function crushLogSignature(foldPath) {
78
+ const path = crushDbPath(foldPath);
79
+ let signature;
80
+ try {
81
+ const main = statSync(path);
82
+ signature = `${main.size}:${main.mtimeMs}`;
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ try {
88
+ const wal = statSync(`${path}-wal`);
89
+ signature += `|${wal.size}:${wal.mtimeMs}`;
90
+ }
91
+ catch {
92
+ // No -wal: either checkpointed away or the journal mode changed. Absence
93
+ // is itself a stable part of the signature.
94
+ signature += "|none";
95
+ }
96
+ return signature;
97
+ }
98
+ /** Which crush session belongs to this narrative.
99
+ *
100
+ * Prefers the id brane recorded at spawn. The fallback — newest real session
101
+ * created at or after the narrative was — is a heuristic with a known failure
102
+ * mode: two crush instances started in one fold at the same moment can be
103
+ * cross-assigned. That is the same risk the codex adapter's discovery already
104
+ * accepts, and it disappears the moment `agent_session_id` is populated. */
105
+ export function resolveCrushSessionIds(crush, opts) {
106
+ const recorded = (opts.recorded ?? []).filter((id) => id && !isSyntheticSessionId(id));
107
+ if (recorded.length > 0)
108
+ return recorded;
109
+ // The synthetic-id exclusion belongs in the query, not after it. Filtering a
110
+ // `LIMIT 1` result in JS asks for the newest row and then throws it away if
111
+ // it turns out to be bookkeeping — reporting "no session" while a perfectly
112
+ // good one sits directly underneath it. `parent_session_id IS NULL` is not
113
+ // enough on its own: it happens to exclude sub-agent rows in the logs seen so
114
+ // far, but nothing guarantees every kind of synthetic row is parented.
115
+ const row = crush
116
+ .prepare(`SELECT id FROM sessions
117
+ WHERE created_at >= ?
118
+ AND parent_session_id IS NULL
119
+ AND id NOT LIKE 'title-%'
120
+ AND id NOT LIKE '%$$%'
121
+ ORDER BY created_at DESC LIMIT 1`)
122
+ .get(toCrushSeconds(opts.narrativeCreatedAtMs));
123
+ return row ? [row.id] : [];
124
+ }
125
+ /** A session and every sub-agent session under it, so calls made by a
126
+ * sub-agent are captured too rather than vanishing into a child row. */
127
+ function sessionFamily(crush, sessionIds) {
128
+ if (sessionIds.length === 0)
129
+ return [];
130
+ const placeholders = sessionIds.map(() => "?").join(", ");
131
+ const children = crush
132
+ .prepare(`SELECT id FROM sessions WHERE parent_session_id IN (${placeholders})`)
133
+ .all(...sessionIds);
134
+ const family = new Set(sessionIds);
135
+ for (const child of children) {
136
+ // Title-generation sessions summarize; they make no tool calls worth
137
+ // showing and would only add rows the operator never asked about.
138
+ if (!child.id.startsWith("title-"))
139
+ family.add(child.id);
140
+ }
141
+ return [...family];
142
+ }
143
+ /** Translate one message's `parts` into record calls.
144
+ *
145
+ * A `tool_call` part is the call as announced — crush persists it with
146
+ * `finished: false` before the tool runs — so it maps to `pending` no matter
147
+ * what `finished` says by the time we read it. `finished: true` means the call
148
+ * returned, not that it succeeded; the outcome is in the `tool_result` part,
149
+ * which may not have been written yet. Letting `finished` imply success would
150
+ * mark failed calls completed.
151
+ *
152
+ * Exported for tests: the payload shapes below are crush's, and pinning them
153
+ * in a pure function is cheaper than standing up a crush. */
154
+ export function toolUsesFromCrushParts(partsJson, ctx) {
155
+ let parts;
156
+ try {
157
+ parts = JSON.parse(partsJson);
158
+ }
159
+ catch {
160
+ return [];
161
+ }
162
+ if (!Array.isArray(parts))
163
+ return [];
164
+ const out = [];
165
+ for (const part of parts) {
166
+ if (typeof part !== "object" || part === null)
167
+ continue;
168
+ const { type, data } = part;
169
+ if (typeof data !== "object" || data === null)
170
+ continue;
171
+ const d = data;
172
+ if (type === "tool_call") {
173
+ if (typeof d.id !== "string" || typeof d.name !== "string")
174
+ continue;
175
+ out.push({
176
+ dedupKey: d.id,
177
+ name: d.name,
178
+ input: parseMaybeJson(d.input),
179
+ status: "pending",
180
+ source: "session_log",
181
+ agentType: "crush",
182
+ parentId: ctx.parentId,
183
+ });
184
+ continue;
185
+ }
186
+ if (type === "tool_result") {
187
+ if (typeof d.tool_call_id !== "string")
188
+ continue;
189
+ const metadata = parseMaybeJson(d.metadata);
190
+ const times = typeof metadata === "object" && metadata !== null
191
+ ? metadata
192
+ : {};
193
+ const start = typeof times.start_time === "number" ? times.start_time : undefined;
194
+ const end = typeof times.end_time === "number" ? times.end_time : undefined;
195
+ out.push({
196
+ dedupKey: d.tool_call_id,
197
+ // The result names the tool again, but a pending row already has it;
198
+ // recordToolUse ignores a name it already holds.
199
+ name: typeof d.name === "string" ? d.name : "unknown",
200
+ status: d.is_error === true ? "failed" : "completed",
201
+ output: d.content,
202
+ // These are milliseconds, unlike the columns around them.
203
+ startedAt: start,
204
+ durationMs: start !== undefined && end !== undefined && end > start ? end - start : undefined,
205
+ source: "session_log",
206
+ agentType: "crush",
207
+ parentId: ctx.parentId,
208
+ });
209
+ }
210
+ }
211
+ return out;
212
+ }
213
+ /** Read whatever is new in a crush session log and record it.
214
+ *
215
+ * Returns the rows actually written, for the caller to push. An unchanged row
216
+ * is not rewritten: every write bumps `rev`, which is the sync cursor, so
217
+ * re-recording a settled call would re-push it to every client on every poll.
218
+ *
219
+ * Opens the log read-only. crush runs in WAL mode, so a concurrent reader is
220
+ * safe while its TUI is live, and read-only means this can never be the
221
+ * process that damages someone else's session. Any failure — no log, a schema
222
+ * that moved, a locked file — is swallowed into "nothing to record": capture
223
+ * is a bystander and must never take a narrative down with it. */
224
+ export function pollCrushToolLog(db, narrativeId, opts, cursor) {
225
+ // Nothing has changed since the last look — and most looks find nothing,
226
+ // since an agent is idle far more often than it is calling a tool. Costing
227
+ // an idle narrative two stats instead of a database open is what makes a
228
+ // 2-second interval defensible at all.
229
+ const signature = crushLogSignature(opts.foldPath);
230
+ if (signature === null || signature === cursor.signature)
231
+ return [];
232
+ const path = crushDbPath(opts.foldPath);
233
+ let crush = null;
234
+ const written = [];
235
+ try {
236
+ crush = new Database(path, { readonly: true, fileMustExist: true });
237
+ const sessions = sessionFamily(crush, resolveCrushSessionIds(crush, {
238
+ recorded: opts.sessionIds,
239
+ narrativeCreatedAtMs: opts.narrativeCreatedAtMs,
240
+ }));
241
+ if (sessions.length === 0)
242
+ return [];
243
+ const placeholders = sessions.map(() => "?").join(", ");
244
+ // `>=`, not `>`: crush's timestamps have one-second resolution, so a strict
245
+ // cursor would skip a message written in the same second as the last one
246
+ // read. Re-reading a second costs an upsert that the `written` map then
247
+ // discards.
248
+ const rows = crush
249
+ .prepare(`SELECT id, session_id, parts, created_at, updated_at FROM messages
250
+ WHERE session_id IN (${placeholders}) AND updated_at >= ?
251
+ ORDER BY updated_at, created_at`)
252
+ .all(...sessions, cursor.updatedAt);
253
+ for (const row of rows) {
254
+ if (row.updated_at > cursor.updatedAt)
255
+ cursor.updatedAt = row.updated_at;
256
+ for (const opt of toolUsesFromCrushParts(row.parts, {
257
+ parentId: parentToolCallId(row.session_id),
258
+ })) {
259
+ const key = opt.dedupKey;
260
+ const prior = cursor.written.get(key);
261
+ if (prior === opt.status)
262
+ continue;
263
+ // The same message rows are re-read whenever the cursor's second is
264
+ // revisited, so a settled call's `tool_call` part comes back around
265
+ // after its `tool_result` has already completed the row. recordToolUse
266
+ // refuses the status regression, but it still bumps `rev` and so still
267
+ // re-pushes the row to every client — every poll, forever. The rule
268
+ // that matters is the same one the upsert applies: a call already seen
269
+ // never goes back to pending.
270
+ if (prior !== undefined && opt.status === "pending")
271
+ continue;
272
+ cursor.written.set(key, opt.status);
273
+ written.push(recordToolUse(db, narrativeId, opt));
274
+ }
275
+ }
276
+ // Only on a clean read: a poll that threw halfway must look again rather
277
+ // than treat the log as consumed.
278
+ cursor.signature = signature;
279
+ }
280
+ catch (err) {
281
+ console.error(`[crush-log] reading ${path} for ${narrativeId} failed: ${err}`);
282
+ }
283
+ finally {
284
+ try {
285
+ crush?.close();
286
+ }
287
+ catch {
288
+ // Closing a read-only handle can't lose data; nothing to do about it.
289
+ }
290
+ }
291
+ return written;
292
+ }
293
+ //# sourceMappingURL=crush-tool-log.js.map
@@ -0,0 +1,116 @@
1
+ /**
2
+ * What an agent's token may reach. Default-deny: a route is open to an agent
3
+ * only if it is listed here, so a route added later is closed to agents
4
+ * until someone decides otherwise.
5
+ *
6
+ * Open: reading brane (narratives, events, files, wiki, search), the pure
7
+ * renderers, and the session lifecycle agents drive themselves — spawn and
8
+ * revive (/ramify, reviving a peer) — and the memory index. Closed:
9
+ *
10
+ * - `/ws/terminal/:nid` and `/ws/control`, the app's channels: keystrokes
11
+ * into any session, and signals. (tmux still lets one agent reach
12
+ * another's pane; that is known and accepted, SECURITY.md.)
13
+ * - The operator's housekeeping: registering and archiving folds,
14
+ * archiving narratives, absorb, cost backfill and reprice, the Devices
15
+ * list. An operator who wants one of these from a phone runs it in a
16
+ * utility session (the spawn dialog's Utility side), whose env carries
17
+ * no agent token.
18
+ * - `POST /api/run`: a utility session's env carries no agent token, so
19
+ * starting one would hand an agent this account's standing in a single
20
+ * call. Only the app starts those; there is no CLI command for it.
21
+ * - `human_signal` (src/daemon/signal-source.ts) and the gated acts
22
+ * (src/daemon/presence-gate.ts), which refuse agents with their own
23
+ * messages; they are closed here too, so the list stays the whole story.
24
+ *
25
+ * Hygiene, not containment: an agent runs as the operator's Unix account
26
+ * and can drop its token and use `daemon.sock` (SECURITY.md, "What an agent
27
+ * token does not do"). What this stops is an agent that follows the rules
28
+ * overreaching, and a leaked agent token reaching any of the above.
29
+ */
30
+ /** Routes an agent token may use, as `METHOD path-pattern`. `:x` matches
31
+ * one path segment, `*` the rest of the path. */
32
+ export const AGENT_ROUTES = [
33
+ "GET /api/status",
34
+ "GET /api/branes",
35
+ "GET /api/protocols",
36
+ "GET /api/folds",
37
+ "GET /api/narratives",
38
+ "GET /api/narratives/recent",
39
+ "GET /api/narratives/:id",
40
+ "GET /api/narratives/:id/events",
41
+ "GET /api/narratives/:id/export",
42
+ "GET /api/narratives/:id/tool_uses",
43
+ "GET /api/narratives/:id/tool_uses/:key",
44
+ "GET /api/payloads/:nid/*",
45
+ "GET /api/files",
46
+ "GET /api/files/list",
47
+ "GET /api/files/find",
48
+ "GET /api/wiki/read",
49
+ "GET /api/wiki/asset",
50
+ "GET /api/spawn-history",
51
+ "GET /api/run-history",
52
+ "POST /api/search",
53
+ "POST /api/wiki/search",
54
+ "POST /api/diagram",
55
+ "POST /api/highlight",
56
+ "POST /api/memory/index",
57
+ "POST /api/spawn",
58
+ "POST /api/revive/:nid",
59
+ // Bound to the token's own narrative where the upgrade is handled.
60
+ "GET /ws/mcp",
61
+ ];
62
+ /** What an agent is told when a route is closed to it. */
63
+ export const AGENT_SCOPE_REFUSAL = "An agent's token cannot use this route; it is the operator's. From a phone, run the command in a utility session (the spawn dialog's Utility side), which carries no agent token";
64
+ const MATCHERS = AGENT_ROUTES.map((route) => {
65
+ const [method, pattern] = route.split(" ");
66
+ return { method, segments: pattern.split("/").slice(1) };
67
+ });
68
+ function matches(m, method, segments) {
69
+ if (m.method !== method)
70
+ return false;
71
+ for (let i = 0; i < m.segments.length; i++) {
72
+ const want = m.segments[i];
73
+ if (want === "*")
74
+ return i < segments.length && segments.slice(i).every((s) => s !== "");
75
+ if (i >= segments.length)
76
+ return false;
77
+ if (want.startsWith(":")) {
78
+ if (segments[i] === "")
79
+ return false;
80
+ continue;
81
+ }
82
+ if (want !== segments[i])
83
+ return false;
84
+ }
85
+ return m.segments.length === segments.length;
86
+ }
87
+ export const AGENT_LOOPBACK_REFUSAL = "An agent's token works only from this machine";
88
+ /** Why a request made with an agent's token is refused, or null if it may
89
+ * go on. An agent's token works only from this machine — over
90
+ * `daemon.sock` or loopback — so a copy that leaks off it opens nothing.
91
+ * That costs no client anything: a spawned agent's MCP bridge and an
92
+ * external client's (`brane mcp`, started from its `.mcp.json`) both run
93
+ * here and dial the socket. */
94
+ export function agentRefusal(req) {
95
+ if (!req.loopback)
96
+ return AGENT_LOOPBACK_REFUSAL;
97
+ return agentMayUse(req.method, req.path) ? null : AGENT_SCOPE_REFUSAL;
98
+ }
99
+ export const AGENT_REVIVE_REFUSAL = "An agent cannot revive or prompt a utility session: it carries no agent token, so typing into it would act as this account";
100
+ /** Why an agent may not revive (or prompt) a narrative with this role, or
101
+ * null if it may. A `utility` narrative is a session the app started
102
+ * through `POST /api/run`: its env carries no agent token, so a prompt
103
+ * typed into it — or its command replayed — runs as this account, the
104
+ * standing closing that route withholds.
105
+ * Reviving or prompting another agent stays open: tmux already lets agents
106
+ * reach each other, which is accepted. */
107
+ export function agentReviveRefusal(role) {
108
+ return role === "utility" ? AGENT_REVIVE_REFUSAL : null;
109
+ }
110
+ /** Whether an agent's token may make this request. `path` is the URL's
111
+ * pathname, without the query string. A WebSocket upgrade is a `GET`. */
112
+ export function agentMayUse(method, path) {
113
+ const segments = path.split("/").slice(1);
114
+ return MATCHERS.some((m) => matches(m, method.toUpperCase(), segments));
115
+ }
116
+ //# sourceMappingURL=agent-scope.js.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Which narratives are parked waiting for direction.
3
+ *
4
+ * An agent that has gone back to a blocking `listen` — or to a
5
+ * `narrate({ wait: true })`, which parks through the same machinery — is, as
6
+ * a rule, done with what it was doing and waiting to be told what next. The
7
+ * daemon holds that fact exactly: the call sits in its bridge's
8
+ * `pendingListens` until something resolves it. This turns the parks and
9
+ * resolutions into one boolean per narrative, and reports its edges.
10
+ *
11
+ * The boolean means "a call is parked right now", and every way a call ends
12
+ * clears it: `delivered` (the agent has something to act on), `closed` (its
13
+ * bridge is gone), `cancelled` (the harness abandoned the call: Esc, a killed
14
+ * task) and `timeout`. A timeout clears it too, though the agent will usually
15
+ * park again a model turn later. The glow goes out for that turn, and that is
16
+ * accurate: the agent is re-reading its context, not hanging on the operator's
17
+ * next word. (A 120 s grace used to hold it on across the gap; it made the
18
+ * glow a guess exactly when the agent had left the loop at the timeout.)
19
+ *
20
+ * Counted per narrative, not per call: a narrative can have more than one
21
+ * bridge (a reconnect, a subagent's), and it is awaiting while any of them
22
+ * holds a parked call.
23
+ */
24
+ export class AwaitingTracker {
25
+ emit;
26
+ parkedCount = new Map();
27
+ constructor(emit) {
28
+ this.emit = emit;
29
+ }
30
+ isAwaiting(nid) {
31
+ return (this.parkedCount.get(nid) ?? 0) > 0;
32
+ }
33
+ /** A blocking call parked for this narrative. */
34
+ parked(nid) {
35
+ const count = this.parkedCount.get(nid) ?? 0;
36
+ this.parkedCount.set(nid, count + 1);
37
+ if (count === 0)
38
+ this.emit(nid, true);
39
+ }
40
+ /** A parked call for this narrative stopped waiting. The ending no longer
41
+ * changes what happens; it is kept so every call site says why. */
42
+ resolved(nid, _ending) {
43
+ const count = this.parkedCount.get(nid) ?? 0;
44
+ if (count === 0)
45
+ return; // not one we saw park; nothing to undo
46
+ if (count > 1) {
47
+ this.parkedCount.set(nid, count - 1);
48
+ return;
49
+ }
50
+ this.parkedCount.delete(nid);
51
+ this.emit(nid, false);
52
+ }
53
+ }
54
+ //# sourceMappingURL=awaiting.js.map