brane 0.1.1 → 0.3.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/AGENTS.md +362 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +91 -0
- package/DESIGN.md +402 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +205 -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 +116 -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 +332 -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 +1094 -0
- package/dist/commands/token.js +153 -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 +6349 -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 +633 -0
- package/dist/db/tokens.js +583 -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/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 +82 -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 +197 -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/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 +49 -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 +262 -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 +85 -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 +137 -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 +115 -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/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +587 -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 +581 -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/tls.js +148 -0
- package/dist/tmux.js +202 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +141 -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 +53 -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 +211 -0
- package/docs/token-rotation.md +413 -0
- package/docs/view-urls.md +114 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e56efc1291dca7258ea00b8108b033a4.js +1097 -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 +48 -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,633 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { randomBase36 } from "../ids.js";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { mkdirSync, existsSync, readFileSync, chmodSync } from "node:fs";
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
let _cachedVersion;
|
|
9
|
+
/** Read the package version (major.minor) from package.json */
|
|
10
|
+
export function getPackageVersion() {
|
|
11
|
+
if (!_cachedVersion) {
|
|
12
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
|
|
13
|
+
const parts = pkg.version.split(".");
|
|
14
|
+
_cachedVersion = `${parts[0]}.${parts[1]}`;
|
|
15
|
+
}
|
|
16
|
+
return _cachedVersion;
|
|
17
|
+
}
|
|
18
|
+
const BRANE_DIR = join(homedir(), ".brane");
|
|
19
|
+
const DB_PATH = join(BRANE_DIR, "brane.db");
|
|
20
|
+
export const SCHEMA_SQL = `
|
|
21
|
+
-- Brane instance identity and configuration
|
|
22
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
23
|
+
key TEXT PRIMARY KEY,
|
|
24
|
+
value TEXT NOT NULL
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
-- Registered folds (spawn points)
|
|
28
|
+
CREATE TABLE IF NOT EXISTS folds (
|
|
29
|
+
path TEXT PRIMARY KEY,
|
|
30
|
+
created_at INTEGER NOT NULL,
|
|
31
|
+
last_spawned INTEGER,
|
|
32
|
+
archived INTEGER DEFAULT 0
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- Narratives (agent sessions)
|
|
36
|
+
CREATE TABLE IF NOT EXISTS narratives (
|
|
37
|
+
id TEXT PRIMARY KEY,
|
|
38
|
+
brane_id TEXT NOT NULL,
|
|
39
|
+
fold_path TEXT,
|
|
40
|
+
agent_type TEXT,
|
|
41
|
+
role TEXT,
|
|
42
|
+
created_at INTEGER NOT NULL,
|
|
43
|
+
status TEXT DEFAULT 'active',
|
|
44
|
+
agent_session_id TEXT,
|
|
45
|
+
title TEXT,
|
|
46
|
+
summary TEXT,
|
|
47
|
+
launch_args TEXT,
|
|
48
|
+
bind_token TEXT,
|
|
49
|
+
session_ids TEXT
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
-- Narrative events (append-only log)
|
|
53
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
54
|
+
narrative_id TEXT NOT NULL,
|
|
55
|
+
seq INTEGER NOT NULL,
|
|
56
|
+
time INTEGER NOT NULL,
|
|
57
|
+
type TEXT NOT NULL,
|
|
58
|
+
content TEXT,
|
|
59
|
+
content_type TEXT,
|
|
60
|
+
source TEXT,
|
|
61
|
+
re TEXT,
|
|
62
|
+
payloads TEXT,
|
|
63
|
+
meta TEXT,
|
|
64
|
+
PRIMARY KEY (narrative_id, seq)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
-- Daemon bearer tokens, by hash only (src/db/tokens.ts). Never deleted:
|
|
68
|
+
-- a token is revoked by stamping revoked_at.
|
|
69
|
+
CREATE TABLE IF NOT EXISTS tokens (
|
|
70
|
+
id TEXT PRIMARY KEY,
|
|
71
|
+
hash TEXT NOT NULL UNIQUE,
|
|
72
|
+
kind TEXT NOT NULL,
|
|
73
|
+
label TEXT NOT NULL,
|
|
74
|
+
-- 'minted' by this daemon, or 'adopted' from a plaintext token that
|
|
75
|
+
-- predates hashing (and may have been logged).
|
|
76
|
+
origin TEXT NOT NULL DEFAULT 'minted',
|
|
77
|
+
created_at INTEGER NOT NULL,
|
|
78
|
+
last_used_at INTEGER,
|
|
79
|
+
revoked_at INTEGER
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
-- Index for efficient event polling
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_events_narrative_seq
|
|
84
|
+
ON events (narrative_id, seq);
|
|
85
|
+
`;
|
|
86
|
+
function generateBraneId() {
|
|
87
|
+
const slug = randomBase36(6);
|
|
88
|
+
return `brane-${slug}`;
|
|
89
|
+
}
|
|
90
|
+
export function ensureBraneDir() {
|
|
91
|
+
if (!existsSync(BRANE_DIR)) {
|
|
92
|
+
mkdirSync(BRANE_DIR, { recursive: true, mode: 0o700 });
|
|
93
|
+
}
|
|
94
|
+
// Everything here is this user's alone: the database, the daemon's socket,
|
|
95
|
+
// the payloads, the TLS key. `mode` only applies at creation, so the chmod is
|
|
96
|
+
// what tightens a directory an older brane made 0755.
|
|
97
|
+
try {
|
|
98
|
+
chmodSync(BRANE_DIR, 0o700);
|
|
99
|
+
}
|
|
100
|
+
catch { }
|
|
101
|
+
return BRANE_DIR;
|
|
102
|
+
}
|
|
103
|
+
/** `.daemon_token` beside a database: where older branes kept this machine's
|
|
104
|
+
* local token. Retired: the daemon reads it once at start, to revoke what it
|
|
105
|
+
* holds, then empties it, and nothing presents it (`bootstrapTokens`,
|
|
106
|
+
* src/db/tokens.ts). */
|
|
107
|
+
export function getTokenFilePath(db) {
|
|
108
|
+
return join(dirname(db.name), ".daemon_token");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Bring a database created from `SCHEMA_SQL` up to the current shape.
|
|
112
|
+
*
|
|
113
|
+
* `SCHEMA_SQL` is the original table set; everything added since arrives here as
|
|
114
|
+
* an idempotent `ALTER TABLE`/backfill. Kept separate from `openDatabase()` so
|
|
115
|
+
* that *every* database — production and test fixtures alike — is built by the
|
|
116
|
+
* same two steps. A column that exists only in this ladder is otherwise a column
|
|
117
|
+
* that works in production and throws in tests.
|
|
118
|
+
*
|
|
119
|
+
* Safe to run on any database, any number of times.
|
|
120
|
+
*/
|
|
121
|
+
export function applyMigrations(db) {
|
|
122
|
+
// Migrate: add title and summary columns if missing
|
|
123
|
+
const cols = db.pragma("table_info(narratives)");
|
|
124
|
+
const colNames = new Set(cols.map((c) => c.name));
|
|
125
|
+
if (!colNames.has("title")) {
|
|
126
|
+
db.exec("ALTER TABLE narratives ADD COLUMN title TEXT");
|
|
127
|
+
}
|
|
128
|
+
if (!colNames.has("summary")) {
|
|
129
|
+
db.exec("ALTER TABLE narratives ADD COLUMN summary TEXT");
|
|
130
|
+
}
|
|
131
|
+
if (!colNames.has("archived")) {
|
|
132
|
+
db.exec("ALTER TABLE narratives ADD COLUMN archived INTEGER DEFAULT 0");
|
|
133
|
+
}
|
|
134
|
+
if (!colNames.has("agent_session_id")) {
|
|
135
|
+
db.exec("ALTER TABLE narratives ADD COLUMN agent_session_id TEXT");
|
|
136
|
+
}
|
|
137
|
+
if (!colNames.has("launch_args")) {
|
|
138
|
+
db.exec("ALTER TABLE narratives ADD COLUMN launch_args TEXT");
|
|
139
|
+
}
|
|
140
|
+
// Per-narrative bind secret: authenticates the MCP bridge's claim to this
|
|
141
|
+
// narrative id on /ws/mcp, so a crossed BRANE_NARRATIVE_ID (or a second
|
|
142
|
+
// process reusing an nid) can't bind as a narrative it doesn't own. Nullable;
|
|
143
|
+
// rows created before this column can no longer bind at all (see
|
|
144
|
+
// resolveMcpBind); revive mints one for such a row before relaunching it.
|
|
145
|
+
if (!colNames.has("bind_token")) {
|
|
146
|
+
db.exec("ALTER TABLE narratives ADD COLUMN bind_token TEXT");
|
|
147
|
+
}
|
|
148
|
+
// Every agent session id a narrative has ever bound (JSON array). A narrative
|
|
149
|
+
// rolls through multiple sessions over its life (revive, twin/agent-switch,
|
|
150
|
+
// context compaction), and total cost is the sum across all of them — but
|
|
151
|
+
// agent_session_id only ever holds the *current* one. session_ids is the
|
|
152
|
+
// cumulative set so cost sampling can aggregate every session's usage.
|
|
153
|
+
if (!colNames.has("session_ids")) {
|
|
154
|
+
db.exec("ALTER TABLE narratives ADD COLUMN session_ids TEXT");
|
|
155
|
+
// Backfill: seed the set from the current session so no existing narrative
|
|
156
|
+
// loses its reading. Session ids are UUIDs ([0-9a-f-]), so the string-built
|
|
157
|
+
// JSON array needs no escaping. Idempotent (guarded on session_ids IS NULL).
|
|
158
|
+
db.exec(`UPDATE narratives SET session_ids = '["' || agent_session_id || '"]'
|
|
159
|
+
WHERE session_ids IS NULL AND agent_session_id IS NOT NULL`);
|
|
160
|
+
}
|
|
161
|
+
// Cumulative session cost, sampled from the agent's own usage log. Unit is
|
|
162
|
+
// agent-defined ("USD", "AIC", …); cost_tokens is an optional JSON breakdown.
|
|
163
|
+
if (!colNames.has("cost_amount")) {
|
|
164
|
+
db.exec("ALTER TABLE narratives ADD COLUMN cost_amount REAL");
|
|
165
|
+
}
|
|
166
|
+
if (!colNames.has("cost_unit")) {
|
|
167
|
+
db.exec("ALTER TABLE narratives ADD COLUMN cost_unit TEXT");
|
|
168
|
+
}
|
|
169
|
+
if (!colNames.has("cost_tokens")) {
|
|
170
|
+
db.exec("ALTER TABLE narratives ADD COLUMN cost_tokens TEXT");
|
|
171
|
+
}
|
|
172
|
+
// Durable per-unit-of-work usage ledger. The transcripts the cost readers
|
|
173
|
+
// parse belong to the harnesses, which may purge them; this table keeps what
|
|
174
|
+
// was read. `key` is the dedup: an entry-level row ("<agent>:<message id>")
|
|
175
|
+
// is inserted once and credited to the first narrative that saw it, so a
|
|
176
|
+
// fork-copied message never counts twice even after the original file is
|
|
177
|
+
// gone. A session-level row ("session:<agent>:<session id>") holds a
|
|
178
|
+
// high-water mark for harnesses that only expose a cumulative total. Rows
|
|
179
|
+
// are never deleted, and never re-priced implicitly: only an explicit
|
|
180
|
+
// `brane cost reprice` rewrites `amount`, so between reprices a narrative's
|
|
181
|
+
// ledger sum is monotonic by construction. narratives.cost_* caches that sum.
|
|
182
|
+
// `cache_write_1h` (of `cache_write`, the 1-hour share) and `speed` are what
|
|
183
|
+
// a reprice needs beyond model and tokens; rows written before they existed
|
|
184
|
+
// have them recovered by the first reprice (src/cost/reprice.ts).
|
|
185
|
+
db.exec(`
|
|
186
|
+
CREATE TABLE IF NOT EXISTS usage_ledger (
|
|
187
|
+
key TEXT PRIMARY KEY,
|
|
188
|
+
narrative_id TEXT NOT NULL,
|
|
189
|
+
session_id TEXT,
|
|
190
|
+
agent_type TEXT,
|
|
191
|
+
unit TEXT NOT NULL,
|
|
192
|
+
amount REAL NOT NULL,
|
|
193
|
+
model TEXT,
|
|
194
|
+
input INTEGER,
|
|
195
|
+
output INTEGER,
|
|
196
|
+
cache_read INTEGER,
|
|
197
|
+
cache_write INTEGER,
|
|
198
|
+
first_seen INTEGER NOT NULL,
|
|
199
|
+
cache_write_1h INTEGER,
|
|
200
|
+
speed TEXT
|
|
201
|
+
);
|
|
202
|
+
CREATE INDEX IF NOT EXISTS usage_ledger_narrative ON usage_ledger(narrative_id);
|
|
203
|
+
`);
|
|
204
|
+
const ledgerCols = new Set(db.pragma("table_info(usage_ledger)").map((c) => c.name));
|
|
205
|
+
if (!ledgerCols.has("model")) {
|
|
206
|
+
db.exec("ALTER TABLE usage_ledger ADD COLUMN model TEXT");
|
|
207
|
+
}
|
|
208
|
+
if (!ledgerCols.has("cache_write_1h")) {
|
|
209
|
+
db.exec("ALTER TABLE usage_ledger ADD COLUMN cache_write_1h INTEGER");
|
|
210
|
+
}
|
|
211
|
+
if (!ledgerCols.has("speed")) {
|
|
212
|
+
db.exec("ALTER TABLE usage_ledger ADD COLUMN speed TEXT");
|
|
213
|
+
}
|
|
214
|
+
// Migrate: add meta column to events if missing
|
|
215
|
+
const eventCols = db.pragma("table_info(events)");
|
|
216
|
+
if (!eventCols.some((c) => c.name === "meta")) {
|
|
217
|
+
db.exec("ALTER TABLE events ADD COLUMN meta TEXT");
|
|
218
|
+
}
|
|
219
|
+
// Migrate: add brane_id to folds if missing
|
|
220
|
+
const foldCols = db.pragma("table_info(folds)");
|
|
221
|
+
if (!foldCols.some((c) => c.name === "brane_id")) {
|
|
222
|
+
db.exec("ALTER TABLE folds ADD COLUMN brane_id TEXT");
|
|
223
|
+
}
|
|
224
|
+
// Migrate: add archived to folds if missing. Archived folds are hidden from
|
|
225
|
+
// the spawn dropdown but never deleted (see Golden Rule) — "prune" is soft.
|
|
226
|
+
if (!foldCols.some((c) => c.name === "archived")) {
|
|
227
|
+
db.exec("ALTER TABLE folds ADD COLUMN archived INTEGER DEFAULT 0");
|
|
228
|
+
}
|
|
229
|
+
// Backfill any folds missing brane_id to home brane (idempotent)
|
|
230
|
+
db.exec("UPDATE folds SET brane_id = (SELECT value FROM meta WHERE key = 'brane_id') WHERE brane_id IS NULL");
|
|
231
|
+
// Fix organelle-imported narratives that kept their original role instead of "absorbed"
|
|
232
|
+
db.exec("UPDATE narratives SET role = 'absorbed' WHERE agent_session_id LIKE 'organelle:%' AND role != 'absorbed'");
|
|
233
|
+
// Tool calls an agent made, alongside the narrative rather than inside it.
|
|
234
|
+
//
|
|
235
|
+
// Deliberately NOT events. `nid:seq` is a published permalink — it appears in
|
|
236
|
+
// `re` back-references from other narratives and in exported HTML anchors that
|
|
237
|
+
// have already left this machine — so existing seqs can never move. Backfilled
|
|
238
|
+
// tool calls (from a session log or an absorb) would need to slot *between*
|
|
239
|
+
// existing seqs, which is impossible without renumbering. Keeping them out of
|
|
240
|
+
// the sequence also means a burst of 200 tool calls can't drown a `tail: 50`
|
|
241
|
+
// context load or wake every blocked `listen`.
|
|
242
|
+
//
|
|
243
|
+
// Three keys, three jobs:
|
|
244
|
+
// after_seq + started_at — where a row renders (between event N and N+1).
|
|
245
|
+
// Anchoring to a seq rather than to wall time keeps placement deterministic
|
|
246
|
+
// when the hook process, the daemon, and an imported vendor file disagree
|
|
247
|
+
// about the clock. Reads the seq space; never writes to it.
|
|
248
|
+
// rev — the sync cursor. A row mutates (pending → completed) without moving,
|
|
249
|
+
// so a client resuming from a display key would miss every update.
|
|
250
|
+
// dedup_key — identity. Vendor tool_use_id where available, else a content
|
|
251
|
+
// hash. In the primary key so the same call arriving twice (live by hook,
|
|
252
|
+
// later by session-log backfill) collapses to one row.
|
|
253
|
+
//
|
|
254
|
+
// Most columns are nullable on purpose: agents report wildly different amounts
|
|
255
|
+
// (some emit only a pre-call event, with no result, duration, or outcome ever).
|
|
256
|
+
// One row shape for every agent, with gaps where a vendor can't fill them in.
|
|
257
|
+
// status: pending | allowed | denied | completed | failed | unknown
|
|
258
|
+
// ("unknown" = started and will never resolve — the renderer must not spin.)
|
|
259
|
+
// source: hook | session_log | absorb — distinguishes "the agent didn't do
|
|
260
|
+
// that" from "this agent can't report it".
|
|
261
|
+
db.exec(`
|
|
262
|
+
CREATE TABLE IF NOT EXISTS tool_uses (
|
|
263
|
+
narrative_id TEXT NOT NULL,
|
|
264
|
+
dedup_key TEXT NOT NULL,
|
|
265
|
+
after_seq INTEGER NOT NULL,
|
|
266
|
+
started_at INTEGER NOT NULL,
|
|
267
|
+
rev INTEGER NOT NULL,
|
|
268
|
+
name TEXT NOT NULL,
|
|
269
|
+
input TEXT,
|
|
270
|
+
status TEXT NOT NULL,
|
|
271
|
+
output TEXT,
|
|
272
|
+
duration_ms INTEGER,
|
|
273
|
+
error TEXT,
|
|
274
|
+
decided_by TEXT,
|
|
275
|
+
decided_at INTEGER,
|
|
276
|
+
decision_note TEXT,
|
|
277
|
+
source TEXT NOT NULL,
|
|
278
|
+
agent_type TEXT,
|
|
279
|
+
parent_id TEXT,
|
|
280
|
+
PRIMARY KEY (narrative_id, dedup_key)
|
|
281
|
+
)
|
|
282
|
+
`);
|
|
283
|
+
db.exec("CREATE INDEX IF NOT EXISTS tool_uses_order ON tool_uses (narrative_id, after_seq, started_at)");
|
|
284
|
+
db.exec("CREATE INDEX IF NOT EXISTS tool_uses_sync ON tool_uses (narrative_id, rev)");
|
|
285
|
+
// Mentions. `@claude-4c7289d1` in one narrative addresses another, and the
|
|
286
|
+
// whole point is to get an agent's attention *without commenting in its
|
|
287
|
+
// narrative* — so a mention is deliberately not an event. That also keeps
|
|
288
|
+
// `nid:seq` clean (see the tool_uses note above), makes a re-scan idempotent,
|
|
289
|
+
// and avoids the recursion an event would invite by quoting the handle that
|
|
290
|
+
// created it.
|
|
291
|
+
//
|
|
292
|
+
// Each row carries both the mention and what became of it:
|
|
293
|
+
//
|
|
294
|
+
// The mention itself — mentioned_nid, src_narrative, src_seq, author, time
|
|
295
|
+
// — is derived data. A re-scan of the event log reproduces it exactly,
|
|
296
|
+
// because UNIQUE makes a second insert a no-op (the same trick as
|
|
297
|
+
// usage_ledger's INSERT OR IGNORE keys). Rebuild freely.
|
|
298
|
+
//
|
|
299
|
+
// sent_at and seen_at are not derivable from anything. They record what
|
|
300
|
+
// passed between the daemon and one agent, and they answer two different
|
|
301
|
+
// questions that a single mark would conflate:
|
|
302
|
+
//
|
|
303
|
+
// sent_at — this mention has been handed over. Governs whether a parked
|
|
304
|
+
// `listen` resolves right now, so a mention wakes an agent exactly
|
|
305
|
+
// once. Without it, "pending" would both resolve the listen and
|
|
306
|
+
// survive the resolve, and the next listen would return instantly,
|
|
307
|
+
// forever, at inference speed.
|
|
308
|
+
//
|
|
309
|
+
// seen_at — the agent has demonstrably attended to it, evidenced by
|
|
310
|
+
// reading the narrative it was written in (ackMentionsByRead in
|
|
311
|
+
// db/mentions.ts). Governs what the receipt keeps showing, so a
|
|
312
|
+
// delivered-but-unread mention is a standing reminder rather than a
|
|
313
|
+
// loop.
|
|
314
|
+
//
|
|
315
|
+
// Per row rather than a high-water mark per narrative, because acknowledgment
|
|
316
|
+
// is per *source*: reading narrative B says nothing about a mention written
|
|
317
|
+
// in C. A scalar watermark over a shared id space settles every lower id
|
|
318
|
+
// regardless of where it came from, silently marking unread mentions read —
|
|
319
|
+
// exactly the drop this design exists to prevent. NULL → timestamp also
|
|
320
|
+
// makes monotonicity structural: there is no value that could rewind.
|
|
321
|
+
//
|
|
322
|
+
// Rows are never deleted (Golden Rule), which is what lets `id` be a plain
|
|
323
|
+
// rowid, and what makes a rebuild safe only when it is *additive*. A
|
|
324
|
+
// delete-and-repopulate would reassign ids and discard sent_at/seen_at,
|
|
325
|
+
// re-delivering settled mentions. Note memory resets itself with
|
|
326
|
+
// `DELETE FROM memory_cursors` (memory/index.ts); the two look alike and
|
|
327
|
+
// must not be reset alike.
|
|
328
|
+
db.exec(`
|
|
329
|
+
CREATE TABLE IF NOT EXISTS mentions (
|
|
330
|
+
id INTEGER PRIMARY KEY,
|
|
331
|
+
mentioned_nid TEXT NOT NULL,
|
|
332
|
+
src_narrative TEXT NOT NULL,
|
|
333
|
+
src_seq INTEGER NOT NULL,
|
|
334
|
+
author TEXT NOT NULL,
|
|
335
|
+
time INTEGER NOT NULL,
|
|
336
|
+
sent_at INTEGER,
|
|
337
|
+
seen_at INTEGER,
|
|
338
|
+
UNIQUE (mentioned_nid, src_narrative, src_seq)
|
|
339
|
+
)
|
|
340
|
+
`);
|
|
341
|
+
db.exec("CREATE INDEX IF NOT EXISTS mentions_pending ON mentions (mentioned_nid, id)");
|
|
342
|
+
// The reverse lookup: which mentions ride in these specific events. Used to
|
|
343
|
+
// annotate events as they are returned, and to settle exactly the mentions a
|
|
344
|
+
// read actually delivered — the UNIQUE constraint's index leads with
|
|
345
|
+
// mentioned_nid and can't answer either.
|
|
346
|
+
db.exec("CREATE INDEX IF NOT EXISTS mentions_src ON mentions (src_narrative, src_seq)");
|
|
347
|
+
// Absorb's memory of what it has already read, for sources that can be
|
|
348
|
+
// re-read: a local agent's session store grows while brane runs, and
|
|
349
|
+
// `brane absorb claude` is meant to be run again and again.
|
|
350
|
+
//
|
|
351
|
+
// absorb_messages — provider-issued message id → where it landed. The
|
|
352
|
+
// dedup that makes a re-run idempotent *per message*, not just per
|
|
353
|
+
// conversation: Claude's `--fork-session` copies a predecessor's
|
|
354
|
+
// records, ids included, into the successor's file, so a count-based
|
|
355
|
+
// append would import the prefix twice under two narratives. Same
|
|
356
|
+
// trick as usage_ledger's keyed INSERT OR IGNORE — the first narrative
|
|
357
|
+
// to hold a key keeps it, and a later reading of the same key elsewhere
|
|
358
|
+
// becomes a cross-narrative `re` instead of a copy.
|
|
359
|
+
//
|
|
360
|
+
// absorb_files — (size, mtime) of each session file at its last absorb,
|
|
361
|
+
// and the parser generation that read it. A file that has not changed
|
|
362
|
+
// since a parser as new as this one read it is skipped on the stat
|
|
363
|
+
// alone, which is what keeps a re-run over hundreds of unchanged
|
|
364
|
+
// sessions instant. It is an optimisation only: a changed file is
|
|
365
|
+
// re-read in full and absorb_messages catches every duplicate, so a
|
|
366
|
+
// stale or missing row costs time, never correctness.
|
|
367
|
+
//
|
|
368
|
+
// Both are keyed by **brane** as well as provider. `--brane` names a
|
|
369
|
+
// destination, and a destination is independent of every other one: the
|
|
370
|
+
// same store imported into two branes is two complete copies, each with
|
|
371
|
+
// its own idempotent re-runs. The first cut keyed on provider alone, so a
|
|
372
|
+
// second brane saw every file as already absorbed and read nothing
|
|
373
|
+
// (claude-xiy9e8wq:38). The `managed` check in classifySessions stays
|
|
374
|
+
// global on purpose — a live narrative owns its session whatever brane it
|
|
375
|
+
// is in.
|
|
376
|
+
//
|
|
377
|
+
// Neither is ever deleted. Both are derivable from the files and the log,
|
|
378
|
+
// and additive by construction. The provider-only tables from before the
|
|
379
|
+
// brane column are carried over by joining each row to the narrative it
|
|
380
|
+
// named, and kept under `_v1` names rather than dropped.
|
|
381
|
+
db.exec(`
|
|
382
|
+
CREATE TABLE IF NOT EXISTS absorb_messages (
|
|
383
|
+
brane_id TEXT NOT NULL,
|
|
384
|
+
provider TEXT NOT NULL,
|
|
385
|
+
key TEXT NOT NULL,
|
|
386
|
+
narrative_id TEXT NOT NULL,
|
|
387
|
+
seq INTEGER NOT NULL,
|
|
388
|
+
PRIMARY KEY (brane_id, provider, key)
|
|
389
|
+
)
|
|
390
|
+
`);
|
|
391
|
+
db.exec(`
|
|
392
|
+
CREATE TABLE IF NOT EXISTS absorb_files (
|
|
393
|
+
brane_id TEXT NOT NULL,
|
|
394
|
+
provider TEXT NOT NULL,
|
|
395
|
+
external_id TEXT NOT NULL,
|
|
396
|
+
path TEXT NOT NULL,
|
|
397
|
+
bytes INTEGER NOT NULL,
|
|
398
|
+
mtime INTEGER NOT NULL,
|
|
399
|
+
absorbed_at INTEGER NOT NULL,
|
|
400
|
+
parser_version INTEGER NOT NULL DEFAULT 0,
|
|
401
|
+
PRIMARY KEY (brane_id, provider, external_id)
|
|
402
|
+
)
|
|
403
|
+
`);
|
|
404
|
+
migrateAbsorbLedgersToBraneScope(db);
|
|
405
|
+
// Which parser read the file (`LocalSource.parserVersion`). A row from
|
|
406
|
+
// before the column existed reads as 0, older than any parser, so each
|
|
407
|
+
// such file is read once more by the current one.
|
|
408
|
+
const fileCols = new Set(db.pragma("table_info(absorb_files)").map((c) => c.name));
|
|
409
|
+
if (!fileCols.has("parser_version")) {
|
|
410
|
+
db.exec("ALTER TABLE absorb_files ADD COLUMN parser_version INTEGER NOT NULL DEFAULT 0");
|
|
411
|
+
}
|
|
412
|
+
// What a token was first presented from, so the Devices list can tell rows
|
|
413
|
+
// apart (`DbTokenStore.find`, src/db/tokens.ts). Written once and never
|
|
414
|
+
// overwritten: a row keeps naming the device that claimed it.
|
|
415
|
+
const tokenCols = new Set(db.pragma("table_info(tokens)").map((c) => c.name));
|
|
416
|
+
if (!tokenCols.has("user_agent")) {
|
|
417
|
+
db.exec("ALTER TABLE tokens ADD COLUMN user_agent TEXT");
|
|
418
|
+
}
|
|
419
|
+
if (!tokenCols.has("remote_addr")) {
|
|
420
|
+
db.exec("ALTER TABLE tokens ADD COLUMN remote_addr TEXT");
|
|
421
|
+
}
|
|
422
|
+
// The latest sighting beside the first, and when the token was last
|
|
423
|
+
// presented from an address other than its first: what shows a copy.
|
|
424
|
+
if (!tokenCols.has("last_user_agent")) {
|
|
425
|
+
db.exec("ALTER TABLE tokens ADD COLUMN last_user_agent TEXT");
|
|
426
|
+
}
|
|
427
|
+
if (!tokenCols.has("last_remote_addr")) {
|
|
428
|
+
db.exec("ALTER TABLE tokens ADD COLUMN last_remote_addr TEXT");
|
|
429
|
+
}
|
|
430
|
+
if (!tokenCols.has("elsewhere_at")) {
|
|
431
|
+
db.exec("ALTER TABLE tokens ADD COLUMN elsewhere_at INTEGER");
|
|
432
|
+
}
|
|
433
|
+
// Which token's request minted this one. Written at mint and impossible to
|
|
434
|
+
// reconstruct later, so it is recorded before anything uses it.
|
|
435
|
+
if (!tokenCols.has("minted_by")) {
|
|
436
|
+
db.exec("ALTER TABLE tokens ADD COLUMN minted_by TEXT");
|
|
437
|
+
}
|
|
438
|
+
// The narrative an `agent` token belongs to: the only one it may bind as.
|
|
439
|
+
if (!tokenCols.has("owner_nid")) {
|
|
440
|
+
db.exec("ALTER TABLE tokens ADD COLUMN owner_nid TEXT");
|
|
441
|
+
}
|
|
442
|
+
// Which token's request last launched a narrative's process (spawn, run or
|
|
443
|
+
// revive), so revoking a device can say which agents it started. Recorded
|
|
444
|
+
// from now on; there is nothing to backfill it from.
|
|
445
|
+
const launchCols = new Set(db.pragma("table_info(narratives)").map((c) => c.name));
|
|
446
|
+
if (!launchCols.has("launched_by")) {
|
|
447
|
+
db.exec("ALTER TABLE narratives ADD COLUMN launched_by TEXT");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/** Carry provider-only `absorb_messages` / `absorb_files` rows (the shape
|
|
451
|
+
* #456 shipped) into the brane-scoped tables. Detected by the old table
|
|
452
|
+
* lacking a `brane_id` column; runs once, since the old table is renamed
|
|
453
|
+
* to `<name>_v1` afterwards and never touched again. Idempotent on a
|
|
454
|
+
* database that has already been migrated or never had the old shape. */
|
|
455
|
+
function migrateAbsorbLedgersToBraneScope(db) {
|
|
456
|
+
const cols = (table) => new Set(db.pragma(`table_info(${table})`).map((c) => c.name));
|
|
457
|
+
// The CREATE TABLE IF NOT EXISTS above is a no-op when the old table
|
|
458
|
+
// exists, so an old-shape table is one that is present without brane_id.
|
|
459
|
+
if (!cols("absorb_messages").has("brane_id")) {
|
|
460
|
+
db.exec("ALTER TABLE absorb_messages RENAME TO absorb_messages_v1");
|
|
461
|
+
db.exec(`
|
|
462
|
+
CREATE TABLE absorb_messages (
|
|
463
|
+
brane_id TEXT NOT NULL,
|
|
464
|
+
provider TEXT NOT NULL,
|
|
465
|
+
key TEXT NOT NULL,
|
|
466
|
+
narrative_id TEXT NOT NULL,
|
|
467
|
+
seq INTEGER NOT NULL,
|
|
468
|
+
PRIMARY KEY (brane_id, provider, key)
|
|
469
|
+
)
|
|
470
|
+
`);
|
|
471
|
+
// A message's brane is its narrative's; a row whose narrative is gone
|
|
472
|
+
// has nothing to be scoped to and stays only in the _v1 table.
|
|
473
|
+
db.exec(`
|
|
474
|
+
INSERT OR IGNORE INTO absorb_messages (brane_id, provider, key, narrative_id, seq)
|
|
475
|
+
SELECT n.brane_id, m.provider, m.key, m.narrative_id, m.seq
|
|
476
|
+
FROM absorb_messages_v1 m JOIN narratives n ON n.id = m.narrative_id
|
|
477
|
+
`);
|
|
478
|
+
}
|
|
479
|
+
if (!cols("absorb_files").has("brane_id")) {
|
|
480
|
+
db.exec("ALTER TABLE absorb_files RENAME TO absorb_files_v1");
|
|
481
|
+
// Kept in step with the CREATE in applyMigrations; carried rows take
|
|
482
|
+
// parser_version 0, so each is read once more by the current parser.
|
|
483
|
+
db.exec(`
|
|
484
|
+
CREATE TABLE absorb_files (
|
|
485
|
+
brane_id TEXT NOT NULL,
|
|
486
|
+
provider TEXT NOT NULL,
|
|
487
|
+
external_id TEXT NOT NULL,
|
|
488
|
+
path TEXT NOT NULL,
|
|
489
|
+
bytes INTEGER NOT NULL,
|
|
490
|
+
mtime INTEGER NOT NULL,
|
|
491
|
+
absorbed_at INTEGER NOT NULL,
|
|
492
|
+
parser_version INTEGER NOT NULL DEFAULT 0,
|
|
493
|
+
PRIMARY KEY (brane_id, provider, external_id)
|
|
494
|
+
)
|
|
495
|
+
`);
|
|
496
|
+
// A file's brane is that of the absorbed narrative holding its session
|
|
497
|
+
// id. A file whose narrative is gone (or that parsed to nothing and so
|
|
498
|
+
// has no narrative) is simply re-read next time — the stat-skip is an
|
|
499
|
+
// optimisation, and losing it costs one parse.
|
|
500
|
+
db.exec(`
|
|
501
|
+
INSERT OR IGNORE INTO absorb_files (brane_id, provider, external_id, path, bytes, mtime, absorbed_at)
|
|
502
|
+
SELECT n.brane_id, f.provider, f.external_id, f.path, f.bytes, f.mtime, f.absorbed_at
|
|
503
|
+
FROM absorb_files_v1 f
|
|
504
|
+
JOIN narratives n ON n.agent_session_id = f.external_id
|
|
505
|
+
AND n.agent_type = f.provider AND n.role = 'absorbed'
|
|
506
|
+
`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
export function openDatabase() {
|
|
510
|
+
// Under `node --test` there is no operator: the suite builds its databases
|
|
511
|
+
// with `createTestDb()`, and code under test is handed one. A path that
|
|
512
|
+
// reaches this instead is about to open the operator's live `~/.brane` —
|
|
513
|
+
// and run the migration ladder on it — from a test run, including the one
|
|
514
|
+
// `brane doctor` starts on an installed machine. Refuse loudly so the path
|
|
515
|
+
// gets a `db` threaded to it rather than quietly touching real data.
|
|
516
|
+
if (process.env.NODE_TEST_CONTEXT) {
|
|
517
|
+
throw new Error("openDatabase() called under node --test: pass the test's db (createTestDb) instead of opening ~/.brane");
|
|
518
|
+
}
|
|
519
|
+
ensureBraneDir();
|
|
520
|
+
const db = new Database(DB_PATH);
|
|
521
|
+
for (const f of [DB_PATH, `${DB_PATH}-wal`, `${DB_PATH}-shm`]) {
|
|
522
|
+
try {
|
|
523
|
+
chmodSync(f, 0o600);
|
|
524
|
+
}
|
|
525
|
+
catch { }
|
|
526
|
+
}
|
|
527
|
+
db.pragma("journal_mode = WAL");
|
|
528
|
+
db.pragma("foreign_keys = ON");
|
|
529
|
+
db.exec(SCHEMA_SQL);
|
|
530
|
+
applyMigrations(db);
|
|
531
|
+
// Ensure brane_id exists
|
|
532
|
+
const existing = db.prepare("SELECT value FROM meta WHERE key = 'brane_id'").get();
|
|
533
|
+
if (!existing) {
|
|
534
|
+
const braneId = generateBraneId();
|
|
535
|
+
db.prepare("INSERT INTO meta (key, value) VALUES ('brane_id', ?)").run(braneId);
|
|
536
|
+
}
|
|
537
|
+
return db;
|
|
538
|
+
}
|
|
539
|
+
export function getBraneId(db) {
|
|
540
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = 'brane_id'").get();
|
|
541
|
+
return row.value;
|
|
542
|
+
}
|
|
543
|
+
/** Where a running daemon listens, or null if none is. Cleans up stale
|
|
544
|
+
* entries. For callers that need the address but no credential — a hook
|
|
545
|
+
* script carries its narrative's bind secret, never the daemon token. */
|
|
546
|
+
export function getDaemonAddress(db) {
|
|
547
|
+
const portRow = db.prepare("SELECT value FROM meta WHERE key = 'daemon_port'").get();
|
|
548
|
+
const pidRow = db.prepare("SELECT value FROM meta WHERE key = 'daemon_pid'").get();
|
|
549
|
+
if (!portRow || !pidRow)
|
|
550
|
+
return null;
|
|
551
|
+
try {
|
|
552
|
+
process.kill(parseInt(pidRow.value, 10), 0);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
// Process is dead — clean up stale entry
|
|
556
|
+
db.prepare("DELETE FROM meta WHERE key IN ('daemon_port', 'daemon_token', 'daemon_pid', 'daemon_version', 'daemon_socket')").run();
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const versionRow = db.prepare("SELECT value FROM meta WHERE key = 'daemon_version'").get();
|
|
560
|
+
return { port: portRow.value, version: versionRow?.value };
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Atomically claim the daemon role using an exclusive SQLite transaction.
|
|
564
|
+
* Returns { claimed: true } if we won, or { claimed: false, pid } if
|
|
565
|
+
* another daemon is already running.
|
|
566
|
+
*/
|
|
567
|
+
export function claimDaemon(db) {
|
|
568
|
+
// BEGIN EXCLUSIVE takes a database-level lock, preventing any other
|
|
569
|
+
// connection from reading or writing until we commit/rollback.
|
|
570
|
+
db.exec("BEGIN EXCLUSIVE");
|
|
571
|
+
try {
|
|
572
|
+
const pidRow = db.prepare("SELECT value FROM meta WHERE key = 'daemon_pid'").get();
|
|
573
|
+
if (pidRow) {
|
|
574
|
+
const existingPid = parseInt(pidRow.value, 10);
|
|
575
|
+
try {
|
|
576
|
+
process.kill(existingPid, 0);
|
|
577
|
+
// Process is alive — another daemon owns the role
|
|
578
|
+
db.exec("ROLLBACK");
|
|
579
|
+
return { claimed: false, pid: existingPid };
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
// Process is dead — clean up stale entry and claim
|
|
583
|
+
db.prepare("DELETE FROM meta WHERE key IN ('daemon_token', 'daemon_pid', 'daemon_version', 'daemon_tls', 'daemon_socket')").run();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Claim: write our PID and version. No token: the database holds only
|
|
587
|
+
// token hashes, and this machine's CLI presents none.
|
|
588
|
+
const version = getPackageVersion();
|
|
589
|
+
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('daemon_pid', ?)").run(String(process.pid));
|
|
590
|
+
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('daemon_version', ?)").run(version);
|
|
591
|
+
db.exec("COMMIT");
|
|
592
|
+
return { claimed: true };
|
|
593
|
+
}
|
|
594
|
+
catch (err) {
|
|
595
|
+
try {
|
|
596
|
+
db.exec("ROLLBACK");
|
|
597
|
+
}
|
|
598
|
+
catch { }
|
|
599
|
+
throw err;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
export function getDbPath() {
|
|
603
|
+
return DB_PATH;
|
|
604
|
+
}
|
|
605
|
+
export function getBraneDir() {
|
|
606
|
+
return BRANE_DIR;
|
|
607
|
+
}
|
|
608
|
+
/** Get (and ensure exists) the payloads directory for a narrative */
|
|
609
|
+
export function getPayloadsDir(narrativeId) {
|
|
610
|
+
const dir = join(BRANE_DIR, "payloads", narrativeId);
|
|
611
|
+
mkdirSync(dir, { recursive: true });
|
|
612
|
+
return dir;
|
|
613
|
+
}
|
|
614
|
+
// Convert an absolute path to ~/relative if under home directory
|
|
615
|
+
export function toPortablePath(absolutePath) {
|
|
616
|
+
const home = homedir();
|
|
617
|
+
if (absolutePath === home)
|
|
618
|
+
return "~";
|
|
619
|
+
if (absolutePath.startsWith(home + "/")) {
|
|
620
|
+
return "~/" + absolutePath.slice(home.length + 1);
|
|
621
|
+
}
|
|
622
|
+
return absolutePath;
|
|
623
|
+
}
|
|
624
|
+
// Resolve a ~/relative or absolute path to an absolute path
|
|
625
|
+
export function fromPortablePath(portablePath) {
|
|
626
|
+
if (portablePath === "~")
|
|
627
|
+
return homedir();
|
|
628
|
+
if (portablePath.startsWith("~/")) {
|
|
629
|
+
return join(homedir(), portablePath.slice(2));
|
|
630
|
+
}
|
|
631
|
+
return portablePath;
|
|
632
|
+
}
|
|
633
|
+
//# sourceMappingURL=schema.js.map
|