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.
- package/AGENTS.md +367 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +101 -0
- package/DESIGN.md +405 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +213 -0
- package/dist/absorb/brane-id.js +11 -0
- package/dist/absorb/chatgpt.js +161 -0
- package/dist/absorb/claude-code.js +504 -0
- package/dist/absorb/index.js +543 -0
- package/dist/absorb/organelle.js +345 -0
- package/dist/absorb/run.js +520 -0
- package/dist/absorb/sources.js +168 -0
- package/dist/absorb/types.js +3 -0
- package/dist/agent-env.js +45 -0
- package/dist/agents.js +1168 -0
- package/dist/claude-live-session.js +124 -0
- package/dist/cli.js +50 -0
- package/dist/codex-tool-log.js +144 -0
- package/dist/commands/absorb.js +291 -0
- package/dist/commands/attach.js +31 -0
- package/dist/commands/certify.js +62 -0
- package/dist/commands/config.js +133 -0
- package/dist/commands/cost.js +126 -0
- package/dist/commands/daemon.js +350 -0
- package/dist/commands/doctor.js +262 -0
- package/dist/commands/fold.js +95 -0
- package/dist/commands/list.js +89 -0
- package/dist/commands/mcp.js +84 -0
- package/dist/commands/meld.js +360 -0
- package/dist/commands/memory.js +215 -0
- package/dist/commands/revive.js +74 -0
- package/dist/commands/run.js +85 -0
- package/dist/commands/spawn.js +1101 -0
- package/dist/commands/token.js +154 -0
- package/dist/copilot-tool-log.js +145 -0
- package/dist/cost/pricing.js +116 -0
- package/dist/cost/reprice.js +136 -0
- package/dist/crush-tool-log.js +293 -0
- package/dist/daemon/agent-scope.js +116 -0
- package/dist/daemon/awaiting.js +54 -0
- package/dist/daemon/cert-manager.js +349 -0
- package/dist/daemon/client.js +262 -0
- package/dist/daemon/gated-routes.js +112 -0
- package/dist/daemon/keep-awake.js +103 -0
- package/dist/daemon/local-socket.js +145 -0
- package/dist/daemon/presence-gate.js +206 -0
- package/dist/daemon/server.js +6353 -0
- package/dist/daemon/signal-source.js +35 -0
- package/dist/daemon/tailscale-rebind.js +58 -0
- package/dist/daemon/token-ring.js +263 -0
- package/dist/daemon/token-routes.js +190 -0
- package/dist/db/events.js +152 -0
- package/dist/db/mentions.js +179 -0
- package/dist/db/schema.js +643 -0
- package/dist/db/tokens.js +624 -0
- package/dist/db/tool-uses.js +647 -0
- package/dist/diagram/index.js +85 -0
- package/dist/diagram/label.js +455 -0
- package/dist/diagram/layout.js +476 -0
- package/dist/diagram/links.js +50 -0
- package/dist/diagram/metrics.js +152 -0
- package/dist/diagram/parse.js +487 -0
- package/dist/diagram/svg.js +389 -0
- package/dist/diagram/types.js +9 -0
- package/dist/export.js +629 -0
- package/dist/files/archive-types.js +2 -0
- package/dist/files/archive.js +182 -0
- package/dist/files/browse.js +327 -0
- package/dist/files/bytes.js +162 -0
- package/dist/files/codecs.js +33 -0
- package/dist/files/find.js +212 -0
- package/dist/files/tar.js +223 -0
- package/dist/files/zip.js +209 -0
- package/dist/folds.js +72 -0
- package/dist/grammars/ebnf.js +92 -0
- package/dist/highlight.js +250 -0
- package/dist/ids.js +24 -0
- package/dist/markdown-diagnostics.js +233 -0
- package/dist/math.js +168 -0
- package/dist/mcp/server.js +886 -0
- package/dist/memory/embedder.js +145 -0
- package/dist/memory/embedding-worker.js +71 -0
- package/dist/memory/embeddings.js +91 -0
- package/dist/memory/index.js +232 -0
- package/dist/memory/storage.js +587 -0
- package/dist/memory/wiki.js +688 -0
- package/dist/memory/wordpiece.js +247 -0
- package/dist/mentions-scan.js +40 -0
- package/dist/presence.js +259 -0
- package/dist/session-affinity.js +185 -0
- package/dist/shared/api-request.js +124 -0
- package/dist/shared/app-origin.js +26 -0
- package/dist/shared/block-glyphs.js +114 -0
- package/dist/shared/diagram-css.js +17 -0
- package/dist/shared/export-selection.js +101 -0
- package/dist/shared/find-in-text.js +110 -0
- package/dist/shared/glow-hues.js +225 -0
- package/dist/shared/hex-dump.js +62 -0
- package/dist/shared/hex-pages.js +52 -0
- package/dist/shared/hit-drum.js +79 -0
- package/dist/shared/known-nids.js +58 -0
- package/dist/shared/list-scroll-keys.js +56 -0
- package/dist/shared/markdown-lists.js +72 -0
- package/dist/shared/markdown-math.js +485 -0
- package/dist/shared/markdown-mentions.js +79 -0
- package/dist/shared/markdown-nids.js +41 -0
- package/dist/shared/markdown-protocols.js +53 -0
- package/dist/shared/markdown-refs.js +123 -0
- package/dist/shared/membrane-invasion.js +342 -0
- package/dist/shared/membrane-motion.js +83 -0
- package/dist/shared/membrane-palette.js +253 -0
- package/dist/shared/membrane-terrain.js +212 -0
- package/dist/shared/message-selection.js +123 -0
- package/dist/shared/mo-glyph.js +57 -0
- package/dist/shared/narrative-format.js +237 -0
- package/dist/shared/narrative-menu.js +84 -0
- package/dist/shared/narrative-sort.js +47 -0
- package/dist/shared/narrative-status.js +52 -0
- package/dist/shared/no-pan.js +85 -0
- package/dist/shared/pane-layout.js +50 -0
- package/dist/shared/payload-inline.js +83 -0
- package/dist/shared/speaker-color.js +61 -0
- package/dist/shared/token-rotation.js +14 -0
- package/dist/shared/view-url.js +216 -0
- package/dist/shared/wiki-links.js +70 -0
- package/dist/shared/wiki-window.js +4 -0
- package/dist/tailscale.js +55 -0
- package/dist/test/absorb-claude.test.js +892 -0
- package/dist/test/agent-scope.test.js +193 -0
- package/dist/test/agents.test.js +55 -0
- package/dist/test/api-request.test.js +122 -0
- package/dist/test/app-host-literal.test.js +53 -0
- package/dist/test/awaiting-glow.test.js +54 -0
- package/dist/test/awaiting.test.js +122 -0
- package/dist/test/bind-token.test.js +88 -0
- package/dist/test/block-glyphs.test.js +68 -0
- package/dist/test/cert-manager.test.js +262 -0
- package/dist/test/claude-live-session.test.js +110 -0
- package/dist/test/codex-tool-log.test.js +568 -0
- package/dist/test/config.test.js +71 -0
- package/dist/test/copilot-tool-log.test.js +93 -0
- package/dist/test/cors-preflight.test.js +56 -0
- package/dist/test/cost-linger.test.js +31 -0
- package/dist/test/cost-pricing.test.js +72 -0
- package/dist/test/cost-reprice.test.js +139 -0
- package/dist/test/cost-sessions.test.js +301 -0
- package/dist/test/crush-tool-log.test.js +289 -0
- package/dist/test/daemon-client.test.js +109 -0
- package/dist/test/daemon-socket.test.js +293 -0
- package/dist/test/diagram-arrows.test.js +205 -0
- package/dist/test/diagram-export.test.js +210 -0
- package/dist/test/diagram-labels.test.js +313 -0
- package/dist/test/diagram-parse.test.js +191 -0
- package/dist/test/diagram-render.test.js +175 -0
- package/dist/test/diagram-review.test.js +288 -0
- package/dist/test/diagram-self-loop-labels.test.js +303 -0
- package/dist/test/embedder.test.js +84 -0
- package/dist/test/events.test.js +123 -0
- package/dist/test/export-selection.test.js +102 -0
- package/dist/test/export.test.js +505 -0
- package/dist/test/files-browse.test.js +393 -0
- package/dist/test/files-find.test.js +272 -0
- package/dist/test/find-in-text.test.js +86 -0
- package/dist/test/first-paint-viewport.test.js +59 -0
- package/dist/test/fixtures/embedding-golden.js +23 -0
- package/dist/test/fixtures/wordpiece-golden.js +167 -0
- package/dist/test/glow-hues.test.js +202 -0
- package/dist/test/harness-cost.test.js +423 -0
- package/dist/test/helpers.js +21 -0
- package/dist/test/hex-pages.test.js +68 -0
- package/dist/test/highlight.test.js +69 -0
- package/dist/test/hit-drum.test.js +71 -0
- package/dist/test/hook-config.test.js +299 -0
- package/dist/test/json-gzip.test.js +70 -0
- package/dist/test/key-routing.test.js +58 -0
- package/dist/test/known-nids.test.js +106 -0
- package/dist/test/link-preview.test.js +67 -0
- package/dist/test/list-scroll-keys.test.js +66 -0
- package/dist/test/listen-own-words.test.js +147 -0
- package/dist/test/listen-timeout.test.js +33 -0
- package/dist/test/markdown-diagnostics.test.js +121 -0
- package/dist/test/markdown-lists.test.js +182 -0
- package/dist/test/markdown-math.test.js +256 -0
- package/dist/test/markdown-nids.test.js +146 -0
- package/dist/test/markdown-protocols.test.js +139 -0
- package/dist/test/mcp-bridge-cancel.test.js +98 -0
- package/dist/test/mcp-bridge-reconnect.test.js +121 -0
- package/dist/test/mcp-narrative.test.js +193 -0
- package/dist/test/mcp.test.js +548 -0
- package/dist/test/meld.test.js +143 -0
- package/dist/test/membrane-invasion.test.js +482 -0
- package/dist/test/membrane-motion.test.js +101 -0
- package/dist/test/membrane-palette.test.js +166 -0
- package/dist/test/membrane-terrain.test.js +170 -0
- package/dist/test/memory-log.test.js +202 -0
- package/dist/test/mentions-e2e.test.js +261 -0
- package/dist/test/mentions-ledger.test.js +265 -0
- package/dist/test/mentions-listen.test.js +265 -0
- package/dist/test/mentions.test.js +238 -0
- package/dist/test/message-selection.test.js +114 -0
- package/dist/test/mo-glyph.test.js +22 -0
- package/dist/test/model-resolution.test.js +166 -0
- package/dist/test/narrate-validate.test.js +77 -0
- package/dist/test/narrate-wait.test.js +160 -0
- package/dist/test/narrative-format.test.js +219 -0
- package/dist/test/narrative-menu.test.js +150 -0
- package/dist/test/narrative-sort.test.js +75 -0
- package/dist/test/no-pan.test.js +60 -0
- package/dist/test/pane-layout.test.js +153 -0
- package/dist/test/paths.test.js +37 -0
- package/dist/test/payload-inline.test.js +91 -0
- package/dist/test/precache-fonts.test.js +72 -0
- package/dist/test/presence-gate.test.js +440 -0
- package/dist/test/presence.test.js +160 -0
- package/dist/test/read-events.test.js +220 -0
- package/dist/test/remote-meld.test.js +128 -0
- package/dist/test/revive-cli.test.js +93 -0
- package/dist/test/search-floor.test.js +206 -0
- package/dist/test/search-fusion.test.js +168 -0
- package/dist/test/search.test.js +205 -0
- package/dist/test/security.test.js +48 -0
- package/dist/test/selection-wiring.test.js +61 -0
- package/dist/test/session-affinity.test.js +295 -0
- package/dist/test/signal-source.test.js +32 -0
- package/dist/test/socket-account.test.js +268 -0
- package/dist/test/spawn-toml.test.js +72 -0
- package/dist/test/speaker-color.test.js +90 -0
- package/dist/test/tailscale-rebind.test.js +79 -0
- package/dist/test/terminal-cursor.test.js +61 -0
- package/dist/test/terminal-resync.test.js +23 -0
- package/dist/test/terminal-size.test.js +36 -0
- package/dist/test/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +646 -0
- package/dist/test/token-lapse.test.js +94 -0
- package/dist/test/token-ring.test.js +188 -0
- package/dist/test/token-rotate-cli.test.js +58 -0
- package/dist/test/token-routes.test.js +95 -0
- package/dist/test/tool-uses.test.js +1006 -0
- package/dist/test/usage-ledger.test.js +126 -0
- package/dist/test/view-url.test.js +627 -0
- package/dist/test/wiki-links.test.js +57 -0
- package/dist/test/wiki-sources.test.js +85 -0
- package/dist/test/wiki-window.test.js +31 -0
- package/dist/test/wiki.test.js +399 -0
- package/dist/test/wordpiece.test.js +68 -0
- package/dist/test/wrangler-route.test.js +27 -0
- package/dist/tls.js +148 -0
- package/dist/tmux.js +213 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +154 -0
- package/docs/README.md +3 -0
- package/docs/absorb.md +231 -0
- package/docs/cost-ledger.md +125 -0
- package/docs/daemon-api.md +189 -0
- package/docs/diagrams.md +163 -0
- package/docs/doctor.md +36 -0
- package/docs/export-scope.md +69 -0
- package/docs/files-view.md +133 -0
- package/docs/link-previews.md +38 -0
- package/docs/math.md +32 -0
- package/docs/mcp.md +69 -0
- package/docs/memory-search.md +75 -0
- package/docs/mentions.md +101 -0
- package/docs/multi-brane.md +14 -0
- package/docs/passkeys.md +165 -0
- package/docs/presence.md +202 -0
- package/docs/releasing.md +104 -0
- package/docs/rendering-warnings.md +22 -0
- package/docs/request-layer.md +72 -0
- package/docs/selection-store.md +50 -0
- package/docs/session-affinity.md +61 -0
- package/docs/terminal-mirroring.md +243 -0
- package/docs/token-rotation.md +431 -0
- package/docs/view-urls.md +134 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
- package/expo/dist/apple-touch-icon.png +0 -0
- package/expo/dist/favicon.svg +3 -0
- package/expo/dist/index.html +177 -0
- package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/expo/dist/katex/katex.min.css +1 -0
- package/expo/dist/manifest.json +16 -0
- package/expo/dist/metadata.json +1 -0
- package/expo/dist/og/membrane.v1.png +0 -0
- package/expo/dist/pdf.min.mjs +28 -0
- package/expo/dist/pdf.worker.min.mjs +28 -0
- package/expo/dist/sw.js +154 -0
- package/package.json +79 -27
- package/protocols/LIVE_loop.md +407 -0
- package/protocols/bebrief.md +41 -0
- package/protocols/digest.md +231 -0
- package/protocols/fly.md +266 -0
- package/protocols/idle.md +81 -0
- package/protocols/keeper.md +829 -0
- package/protocols/markdown.md +80 -0
- package/protocols/memory.md +419 -0
- package/protocols/mentions.md +93 -0
- package/protocols/payloads.md +53 -0
- package/protocols/prelude.md +91 -0
- package/protocols/ramify.md +105 -0
- package/protocols/review.md +356 -0
- package/protocols/rrr.md +232 -0
- package/protocols/scribe.md +429 -0
- package/protocols/summarize.md +203 -0
- package/protocols/twin.md +174 -0
- package/protocols/wiki.md +438 -0
- package/protocols/worker.md +38 -0
- package/wiki/shared/append-only-event-log.md +148 -0
- package/wiki/shared/index.md +53 -0
- package/wiki/shared/owning-your-narratives.md +161 -0
- package/wiki/shared/premature-ontologies.md +435 -0
- package/wiki/shared/sedimentary-superintelligence.md +128 -0
- package/wiki/shared/sequestering-the-narrative.md +143 -0
- package/wiki/shared/why-wikis-work.md +216 -0
- package/.travis.yml +0 -7
- package/lib/brane.cjs.js +0 -12
- package/lib/brane.cjs.js.map +0 -1
- package/lib/brane.d.ts +0 -11
- package/lib/brane.js +0 -6
- package/lib/brane.js.map +0 -1
- package/lib/tests.d.ts +0 -1
- package/lib/tests.js +0 -8
- package/lib/tests.js.map +0 -1
- package/rollup.config.js +0 -21
- package/scripts/test.sh +0 -8
- package/src/brane.ts +0 -21
- package/src/tests.ts +0 -8
- 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
|