@loompad/cli 0.1.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/ARCHITECTURE.md +211 -0
- package/CHANGELOG.md +470 -0
- package/LICENSE +21 -0
- package/README.md +630 -0
- package/dist/adapters/base.d.ts +71 -0
- package/dist/adapters/base.js +187 -0
- package/dist/adapters/base.js.map +1 -0
- package/dist/adapters/bridges/antigravity.d.ts +53 -0
- package/dist/adapters/bridges/antigravity.js +106 -0
- package/dist/adapters/bridges/antigravity.js.map +1 -0
- package/dist/adapters/bridges/cdp.d.ts +82 -0
- package/dist/adapters/bridges/cdp.js +222 -0
- package/dist/adapters/bridges/cdp.js.map +1 -0
- package/dist/adapters/bridges/gui-chat.d.ts +113 -0
- package/dist/adapters/bridges/gui-chat.js +498 -0
- package/dist/adapters/bridges/gui-chat.js.map +1 -0
- package/dist/adapters/bridges/kiro.d.ts +19 -0
- package/dist/adapters/bridges/kiro.js +22 -0
- package/dist/adapters/bridges/kiro.js.map +1 -0
- package/dist/adapters/bridges/profiles.d.ts +82 -0
- package/dist/adapters/bridges/profiles.js +125 -0
- package/dist/adapters/bridges/profiles.js.map +1 -0
- package/dist/adapters/claude-code.d.ts +26 -0
- package/dist/adapters/claude-code.js +209 -0
- package/dist/adapters/claude-code.js.map +1 -0
- package/dist/adapters/codex.d.ts +51 -0
- package/dist/adapters/codex.js +291 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/echo.d.ts +20 -0
- package/dist/adapters/echo.js +79 -0
- package/dist/adapters/echo.js.map +1 -0
- package/dist/adapters/grok.d.ts +61 -0
- package/dist/adapters/grok.js +217 -0
- package/dist/adapters/grok.js.map +1 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.js +33 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/opencode.d.ts +64 -0
- package/dist/adapters/opencode.js +426 -0
- package/dist/adapters/opencode.js.map +1 -0
- package/dist/cli/common.d.ts +11 -0
- package/dist/cli/common.js +40 -0
- package/dist/cli/common.js.map +1 -0
- package/dist/cli/doctor.d.ts +12 -0
- package/dist/cli/doctor.js +181 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +641 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/tui-model.d.ts +73 -0
- package/dist/cli/tui-model.js +312 -0
- package/dist/cli/tui-model.js.map +1 -0
- package/dist/cli/tui.d.ts +15 -0
- package/dist/cli/tui.js +451 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/cli/ui.d.ts +10 -0
- package/dist/cli/ui.js +135 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/core/ades.d.ts +79 -0
- package/dist/core/ades.js +130 -0
- package/dist/core/ades.js.map +1 -0
- package/dist/core/baton.d.ts +37 -0
- package/dist/core/baton.js +92 -0
- package/dist/core/baton.js.map +1 -0
- package/dist/core/brain-extract.d.ts +111 -0
- package/dist/core/brain-extract.js +248 -0
- package/dist/core/brain-extract.js.map +1 -0
- package/dist/core/brain-index.d.ts +128 -0
- package/dist/core/brain-index.js +338 -0
- package/dist/core/brain-index.js.map +1 -0
- package/dist/core/brain.d.ts +214 -0
- package/dist/core/brain.js +425 -0
- package/dist/core/brain.js.map +1 -0
- package/dist/core/claude-cli.d.ts +12 -0
- package/dist/core/claude-cli.js +47 -0
- package/dist/core/claude-cli.js.map +1 -0
- package/dist/core/distill.d.ts +19 -0
- package/dist/core/distill.js +88 -0
- package/dist/core/distill.js.map +1 -0
- package/dist/core/eventlog.d.ts +43 -0
- package/dist/core/eventlog.js +211 -0
- package/dist/core/eventlog.js.map +1 -0
- package/dist/core/git.d.ts +203 -0
- package/dist/core/git.js +460 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/logbook.d.ts +59 -0
- package/dist/core/logbook.js +118 -0
- package/dist/core/logbook.js.map +1 -0
- package/dist/core/memory.d.ts +34 -0
- package/dist/core/memory.js +124 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/notify.d.ts +11 -0
- package/dist/core/notify.js +30 -0
- package/dist/core/notify.js.map +1 -0
- package/dist/core/projection.d.ts +20 -0
- package/dist/core/projection.js +96 -0
- package/dist/core/projection.js.map +1 -0
- package/dist/core/registry.d.ts +76 -0
- package/dist/core/registry.js +147 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/router.d.ts +42 -0
- package/dist/core/router.js +99 -0
- package/dist/core/router.js.map +1 -0
- package/dist/core/routes.d.ts +93 -0
- package/dist/core/routes.js +399 -0
- package/dist/core/routes.js.map +1 -0
- package/dist/core/search.d.ts +63 -0
- package/dist/core/search.js +0 -0
- package/dist/core/search.js.map +1 -0
- package/dist/core/setup.d.ts +70 -0
- package/dist/core/setup.js +250 -0
- package/dist/core/setup.js.map +1 -0
- package/dist/core/suggestions.d.ts +15 -0
- package/dist/core/suggestions.js +38 -0
- package/dist/core/suggestions.js.map +1 -0
- package/dist/core/worktree.d.ts +32 -0
- package/dist/core/worktree.js +83 -0
- package/dist/core/worktree.js.map +1 -0
- package/dist/daemon/app-page.d.ts +32 -0
- package/dist/daemon/app-page.js +6363 -0
- package/dist/daemon/app-page.js.map +1 -0
- package/dist/daemon/auth.d.ts +51 -0
- package/dist/daemon/auth.js +135 -0
- package/dist/daemon/auth.js.map +1 -0
- package/dist/daemon/board.d.ts +117 -0
- package/dist/daemon/board.js +202 -0
- package/dist/daemon/board.js.map +1 -0
- package/dist/daemon/brand-icons.d.ts +29 -0
- package/dist/daemon/brand-icons.js +44 -0
- package/dist/daemon/brand-icons.js.map +1 -0
- package/dist/daemon/client.d.ts +127 -0
- package/dist/daemon/client.js +237 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/geist-font.d.ts +10 -0
- package/dist/daemon/geist-font.js +783 -0
- package/dist/daemon/geist-font.js.map +1 -0
- package/dist/daemon/guards.d.ts +18 -0
- package/dist/daemon/guards.js +40 -0
- package/dist/daemon/guards.js.map +1 -0
- package/dist/daemon/linear.d.ts +59 -0
- package/dist/daemon/linear.js +117 -0
- package/dist/daemon/linear.js.map +1 -0
- package/dist/daemon/push.d.ts +22 -0
- package/dist/daemon/push.js +58 -0
- package/dist/daemon/push.js.map +1 -0
- package/dist/daemon/runtime.d.ts +299 -0
- package/dist/daemon/runtime.js +1016 -0
- package/dist/daemon/runtime.js.map +1 -0
- package/dist/daemon/server.d.ts +125 -0
- package/dist/daemon/server.js +1728 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/tasks.d.ts +153 -0
- package/dist/daemon/tasks.js +264 -0
- package/dist/daemon/tasks.js.map +1 -0
- package/dist/daemon/terminals.d.ts +82 -0
- package/dist/daemon/terminals.js +372 -0
- package/dist/daemon/terminals.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/sdk.d.ts +17 -0
- package/dist/sdk.js +17 -0
- package/dist/sdk.js.map +1 -0
- package/dist/types.d.ts +236 -0
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -0
- package/docs/SETUP.md +265 -0
- package/docs/adapters.md +132 -0
- package/docs/design-system.md +245 -0
- package/docs/img/board-linear.png +0 -0
- package/docs/img/board-projects.png +0 -0
- package/docs/img/brain.png +0 -0
- package/docs/img/settings.png +0 -0
- package/docs/img/workspace.png +0 -0
- package/docs/integration-notes.md +76 -0
- package/docs/proposals/brain.md +262 -0
- package/package.json +84 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Loom design system — "quiet graphite"
|
|
2
|
+
|
|
3
|
+
Loom's UI adopts the design language of [Orca](https://github.com/stablyai/orca)
|
|
4
|
+
(MIT), the AI-orchestrator desktop app: **monochrome and quiet** — neutral
|
|
5
|
+
surfaces carry the chrome, and color is reserved for *state*. Loom keeps its two
|
|
6
|
+
brand accents inside that rule: **thread** cyan (`#67e8f9`) marks live activity,
|
|
7
|
+
**shuttle** magenta (`#e879f9`) marks the baton moving between agents. Everything
|
|
8
|
+
else is neutral.
|
|
9
|
+
|
|
10
|
+
Canonical token sources:
|
|
11
|
+
|
|
12
|
+
| Surface | Token file |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| Web app (daemon-served) + Electron shell | `src/daemon/app-page.ts` (`:root` light / `.dark`) |
|
|
15
|
+
| Phone app (Expo RN) | `app/src/theme.ts` |
|
|
16
|
+
|
|
17
|
+
## Color tokens — web / desktop (from Orca desktop)
|
|
18
|
+
|
|
19
|
+
Paired *surface + foreground* roles, both themes. Dark is the default; light is
|
|
20
|
+
a first-class theme behind the in-app toggle (persisted as `loomTheme`).
|
|
21
|
+
|
|
22
|
+
| Role | Light | Dark |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| `--background` / `--foreground` | `#fff` / `#0a0a0a` | `#0a0a0a` / `#fafafa` |
|
|
25
|
+
| `--card` / `--card-foreground` | `#fff` / `#0a0a0a` | `#171717` / `#fafafa` |
|
|
26
|
+
| `--popover` | `#fff` | `#171717` |
|
|
27
|
+
| `--primary` / `--primary-foreground` | `#171717` / `#fafafa` | `#e5e5e5` / `#171717` |
|
|
28
|
+
| `--secondary`, `--muted`, `--accent` | `#f5f5f5` | `#262626` / `#262626` / `#404040` |
|
|
29
|
+
| `--muted-foreground` | `#737373` | `#a1a1a1` |
|
|
30
|
+
| `--destructive` | `#e40014` | `#ff6568` |
|
|
31
|
+
| `--border` | `#e5e5e5` | `rgb(255 255 255 / 0.07)` |
|
|
32
|
+
| `--input` | `#e5e5e5` | `rgb(255 255 255 / 0.15)` |
|
|
33
|
+
| `--ring` | `#a1a1a1` | `#737373` |
|
|
34
|
+
| `--sidebar` / `--sidebar-accent` | `#fafafa` / `#f5f5f5` | `#171717` / `#262626` |
|
|
35
|
+
| state: `--thread` / `--shuttle` | `#67e8f9` / `#e879f9` | same |
|
|
36
|
+
| state: `--ok` / `--warn` / `--err` | `#15803d` / `#f59e0b` / `#e40014` | `#10b981` / `#eab308` / `#ff6568` |
|
|
37
|
+
|
|
38
|
+
Rules:
|
|
39
|
+
|
|
40
|
+
- **Hairlines everywhere.** 1px `--border`; in dark mode borders are 7% white —
|
|
41
|
+
never brighter.
|
|
42
|
+
- **Tints via `color-mix`** against an existing token, never a new hex.
|
|
43
|
+
- **Three elevation tiers only:** hairline border (default) → border + tiny
|
|
44
|
+
shadow (cards) → floating `0 10px 24px rgba(0,0,0,.18)` + glass (popovers,
|
|
45
|
+
sheets, toasts). Floating surfaces use the Orca glass recipe: translucent
|
|
46
|
+
popover bg + `backdrop-filter: blur()` + inset top highlight.
|
|
47
|
+
- Hover/active list rows use `--accent` (`--sidebar-accent` inside the rail);
|
|
48
|
+
the persistent current row also carries `data-current="true"`.
|
|
49
|
+
|
|
50
|
+
## Typography
|
|
51
|
+
|
|
52
|
+
- **Sans:** `Geist` (variable woff2, weight 100–900), served by the daemon at
|
|
53
|
+
`/app/fonts/geist.woff2` (embedded in `src/daemon/geist-font.ts`; SIL OFL 1.1).
|
|
54
|
+
Fallback `-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`.
|
|
55
|
+
Body letter-spacing `0.01em`, antialiased.
|
|
56
|
+
- **Mono:** `'SF Mono', SFMono-Regular, ui-monospace, 'Cascadia Code', Menlo,
|
|
57
|
+
Consolas, 'Liberation Mono', monospace` — paths, branch names, meta, anything
|
|
58
|
+
literal.
|
|
59
|
+
- **Scale:** 11px uppercase meta (600, tracking .05em) · 12px secondary ·
|
|
60
|
+
13px list rows · 14px body · 15px+ titles only.
|
|
61
|
+
|
|
62
|
+
## Radius, controls
|
|
63
|
+
|
|
64
|
+
- Base radius `10px`; `sm 6 / md 8 / lg 10 / xl 14`. Buttons + inputs `8px`;
|
|
65
|
+
cards `14px`; badges/chips full.
|
|
66
|
+
- Buttons: default h-36px, sm h-32, xs h-24; variants `primary` (near-white on
|
|
67
|
+
dark / near-black on light), `secondary`, `outline`, `ghost`, `destructive`.
|
|
68
|
+
- Focus: `border-color: var(--ring)` + 3px ring at 50% — on every interactive.
|
|
69
|
+
- Inputs h-36px, transparent bg (dark: 15%-white wash), placeholder at 60%
|
|
70
|
+
muted-foreground.
|
|
71
|
+
- Icons: inline SVG, 16px, stroke 2, `currentColor`. Never emoji.
|
|
72
|
+
|
|
73
|
+
## Workspace shell (desktop web + Electron, ≥900px)
|
|
74
|
+
|
|
75
|
+
The Orca workspace layout, drawn from Loom's real model. Every column is
|
|
76
|
+
drag-resizable and persisted; double-click a handle to reset
|
|
77
|
+
(`loomSbW` / `loomRailW` / `loomDockW` / `loomTermH`).
|
|
78
|
+
|
|
79
|
+
- **Left sidebar (264px)** — New task / New project + Search, then project
|
|
80
|
+
groups with nested **chat** rows (a project holds many conversations; they
|
|
81
|
+
share one brain, one baton, one working tree). Hover a chat for its ×,
|
|
82
|
+
double-click to rename; `+ New chat` at the end. Agents are NOT here — they
|
|
83
|
+
work the whole project, so they live in the rail's roster.
|
|
84
|
+
- **Tab strip (40px)** — *is* the window chrome: project context, then
|
|
85
|
+
Thread | Board | Brain, then the terminal / right-panel / theme toggles.
|
|
86
|
+
The active tab merges into the canvas; the composer docks under Thread only.
|
|
87
|
+
- **Diff dock (right of the chat, closed by default)** — opens only when you
|
|
88
|
+
click a change: an `Update(n files)` card in the thread, or a file in
|
|
89
|
+
Explorer / Search / Source Control. Per-file diff cards on
|
|
90
|
+
`--editor-surface` with add/delete washes, hunk headers and old/new line
|
|
91
|
+
gutters; Loom's `.loom/` state files filtered. Closes with an X.
|
|
92
|
+
- **Right rail (304px, `loomRail`, open by default on Explorer)** — four
|
|
93
|
+
views behind an icon switcher (`loomRailView`):
|
|
94
|
+
- *Explorer* — lazy project file tree; a file opens its diff if changed,
|
|
95
|
+
else a read-only preview.
|
|
96
|
+
- *Search* — filename find across the project.
|
|
97
|
+
- *Source Control* — branch, changed files with colored status letters,
|
|
98
|
+
live route and needs-input cards.
|
|
99
|
+
- *Agents* — New task + the roster: status dot (amber pulse = working),
|
|
100
|
+
`baton` badge, brand mark, and a role you can click and retype (roles are
|
|
101
|
+
free text — planner/executor/reviewer are suggestions, not a menu).
|
|
102
|
+
Bridges appear here too, badged and unclickable.
|
|
103
|
+
- **Terminal dock** — a bottom, resizable strip of real shells (`loomTerm`,
|
|
104
|
+
Ctrl+backtick). See *Terminal* below.
|
|
105
|
+
- **Tasks pane** — the project's GitHub issues/PRs, read through the user's own
|
|
106
|
+
`gh` CLI (never a token of ours). Provider mark, `Issues`/`PRs` segmented
|
|
107
|
+
control, repo pill, `Open` / `Assigned to me` chips, a mono query box taking
|
|
108
|
+
GitHub's search syntax verbatim, and a fixed-column table — id pill, title +
|
|
109
|
+
author + labels *in the colours GitHub reports*, assignee stack, status
|
|
110
|
+
badge, relative time, and a `Start →` that drafts a task from the issue.
|
|
111
|
+
Colour rule holds: the only colour is state (open/closed/merged/draft) and
|
|
112
|
+
GitHub's own label hues, which are data, not chrome. Every unavailable
|
|
113
|
+
reason (`no-cli` / `no-auth` / `no-remote`) gets a written panel — an empty
|
|
114
|
+
table would claim "no issues", which is a different fact.
|
|
115
|
+
- **Board pane** — Orca's board, on Loom's data. Four columns (working / needs
|
|
116
|
+
you / in review / ready to merge), each a card list; a card is a status dot +
|
|
117
|
+
label, the agent (with its brand mark), title, branch, and PR link or "no PR
|
|
118
|
+
yet". Colour is state only: amber working/blocked, red CI, grey review, green
|
|
119
|
+
ready. **Cards are derived, never stored** — dragging one pins it in a column
|
|
120
|
+
(`loomBoardPins:<pid>`) and nothing more, so the badge always reports what
|
|
121
|
+
GitHub and the daemon say. Dropping a card in its computed column clears the
|
|
122
|
+
pin. Replaced the Routes tab; routes live on in the New Task modal (named
|
|
123
|
+
pipelines, or several agents = custom steps) and the Source Control rail
|
|
124
|
+
(live state + abort).
|
|
125
|
+
- **Brand marks** — ADE logos from @lobehub/icons (MIT), frozen into an SVG
|
|
126
|
+
sprite by `scripts/gen-brand-icons.mjs` and instantiated with
|
|
127
|
+
`<use href="#brand-<kind>">`. They are the one exception to the monochrome
|
|
128
|
+
rule: a logo is the agent's identity, not our state palette, so it keeps its
|
|
129
|
+
own colours. Keyed by adapter **kind**; an unknown kind keeps the hue
|
|
130
|
+
monogram rather than wearing another vendor's mark. A sprite, not inline
|
|
131
|
+
copies, because Antigravity and Codex carry internal ids.
|
|
132
|
+
- **New Task modal** — Orca's Create Worktree, mapped to Loom: project + task
|
|
133
|
+
+ **one agent, or several** (several = a pipeline through them, in the order
|
|
134
|
+
you picked). Scrim + glass panel; `n` opens, Cmd/Ctrl+Enter submits, Esc
|
|
135
|
+
closes.
|
|
136
|
+
- **New Project modal** — same chrome; `p` opens. Folder + optional name, and
|
|
137
|
+
a native picker (`window.loomNative.pickFolder`, exposed by the Electron
|
|
138
|
+
preload) when the shell can offer one — a browser types the path, since the
|
|
139
|
+
daemon may be on another host.
|
|
140
|
+
- **Status bar (25px)** — websocket liveness, daemon host, baton holder,
|
|
141
|
+
spend meter (project share of total), working-agent count, project count,
|
|
142
|
+
total spend.
|
|
143
|
+
|
|
144
|
+
### Terminal
|
|
145
|
+
|
|
146
|
+
Each tab owns a long-lived shell in the project directory. There are two
|
|
147
|
+
backends behind one interface (`src/daemon/terminals.ts`); the daemon reports
|
|
148
|
+
which is live via `/api/health` and the WebSocket hello.
|
|
149
|
+
|
|
150
|
+
**`pty` — the real thing** (when `node-pty` is available). The shell is on a
|
|
151
|
+
tty, so it draws its own prompt, echoes, and has job control: `^C`/`^Z`,
|
|
152
|
+
`less`, `vim`, `htop`, and a real window size all work. The front end is
|
|
153
|
+
xterm.js, served from `node_modules` at `/app/vendor/*` — no bundler, no CDN.
|
|
154
|
+
|
|
155
|
+
- Keystrokes go up the WebSocket; a POST per keystroke can't carry a tty.
|
|
156
|
+
- FitAddon + a ResizeObserver keep the pty's window matched to the pane.
|
|
157
|
+
Fitting is refused while the pane has no box: measuring a hidden element
|
|
158
|
+
yields a 1×1 grid, and the pty would be sized to match.
|
|
159
|
+
- Terminals open only after the socket is listening, or the shell's first
|
|
160
|
+
output (its prompt) is broadcast to nobody.
|
|
161
|
+
- The palette is mapped from the design tokens and repaints on theme toggle.
|
|
162
|
+
- Cmd/Ctrl+C copies with a selection, interrupts without; Cmd/Ctrl+V pastes.
|
|
163
|
+
|
|
164
|
+
**`pipe` — the fallback** (no node-pty). `node-pty` is a native module, so it
|
|
165
|
+
is an *optionalDependency* and ships prebuilds only for macOS/Windows; Loom
|
|
166
|
+
runs headless on Linux boxes that may have no toolchain, and `npm i -g
|
|
167
|
+
threadloom` must never break. A long-lived shell on plain pipes:
|
|
168
|
+
|
|
169
|
+
- `cd` and exported variables still persist; tabs are isolated.
|
|
170
|
+
- No tty, so the daemon reports each command's exit code and cwd out of band:
|
|
171
|
+
the shell prints a sentinel, which the daemon strips from the stream
|
|
172
|
+
(holding back partial matches across chunk boundaries). That's how the
|
|
173
|
+
client draws the prompt and surfaces failures.
|
|
174
|
+
- **Ctrl+C** signals the shell's process group. The shell survives because it
|
|
175
|
+
is sent a no-op `INT` trap at open — handled signals reset to default in
|
|
176
|
+
children, so the foreground job still dies. Real `^C → exit 130 → prompt`.
|
|
177
|
+
- No echo or job control, so the client drives it a line at a time and renders
|
|
178
|
+
ANSI SGR itself against the theme tokens.
|
|
179
|
+
|
|
180
|
+
`FORCE_COLOR` and a `cat` pager are set in both, so tools colourise and never
|
|
181
|
+
block. `LOOM_NO_PTY=1` forces the fallback — CI runs the suite both ways so it
|
|
182
|
+
can't rot.
|
|
183
|
+
|
|
184
|
+
One packaging wrinkle worth knowing: node-pty's prebuilt `spawn-helper` ships
|
|
185
|
+
without the executable bit, and node-pty then fails every spawn with a bare
|
|
186
|
+
"posix_spawnp failed". The loader repairs the mode, and proves it can spawn
|
|
187
|
+
before trusting it — a load isn't proof.
|
|
188
|
+
|
|
189
|
+
### The file sandbox
|
|
190
|
+
|
|
191
|
+
Explorer endpoints (`/files`, `/file`, `/find`) hand file contents to any
|
|
192
|
+
paired client, so containment is checked twice: lexically (stops `../`, and
|
|
193
|
+
covers paths that don't exist yet) and again via `realpath` (stops a symlink
|
|
194
|
+
*inside* the project pointing out of it — `path.resolve` resolves straight
|
|
195
|
+
through links). Both are covered by `test/workspace.test.ts`.
|
|
196
|
+
|
|
197
|
+
Mobile home mirrors the Orca companion: a Welcome-back hero, three stat
|
|
198
|
+
tiles (Projects / Agents / Spend), a Daemon card (host + counts), a Resume
|
|
199
|
+
card for the active project, hue-glyph project cards, and quick-action
|
|
200
|
+
pills.
|
|
201
|
+
|
|
202
|
+
Mobile (<900px) keeps the single-column thread: chips row, glass sheets, and
|
|
203
|
+
the docked composer.
|
|
204
|
+
|
|
205
|
+
## Window chrome (Electron)
|
|
206
|
+
|
|
207
|
+
Matches Orca: `titleBarStyle: hiddenInset` (macOS) with traffic lights at
|
|
208
|
+
`{x:16, y:12}`, min 600×400, canvas-colored background (`#0a0a0a`). The web app
|
|
209
|
+
draws 36px draggable title strips (`-webkit-app-region: drag`) keyed off the
|
|
210
|
+
`data-electron` attribute set by the preload; macOS reserves an 80px
|
|
211
|
+
traffic-light gutter. Sidebar strip wears `--sidebar` with an inset 1px seam.
|
|
212
|
+
|
|
213
|
+
## Phone app (from Orca mobile — graphite, dark-only)
|
|
214
|
+
|
|
215
|
+
`app/src/theme.ts`:
|
|
216
|
+
|
|
217
|
+
- Surfaces: `bgBase #111111`, `bgPanel #1a1a1a`, `bgRaised #242424`,
|
|
218
|
+
border `#2a2a2a`, editor `#1e1e1e`.
|
|
219
|
+
- Text: `#e0e0e0` / `#888888` / `#555555`; primary CTA is **near-white**
|
|
220
|
+
(`#f5f5f5`) with dark text — the single loudest thing on any screen.
|
|
221
|
+
- State: green `#22c55e`, amber `#f59e0b`, red `#ef4444`, thread `#67e8f9`,
|
|
222
|
+
shuttle `#e879f9`; `accentBlue #3b82f6` for links/selection only.
|
|
223
|
+
- Spacing `4/8/12/16/24`; radii: rows+buttons+inputs `6`, cards `14`.
|
|
224
|
+
- Active/selected states are **neutral grey** (raised bg or 2px grey left
|
|
225
|
+
border), not colored.
|
|
226
|
+
- Composer is a command dock: `bgPanel` bar, raised 6px-radius input, circular
|
|
227
|
+
34px send button with an ↑ glyph.
|
|
228
|
+
- Diffs: added `rgba(129,184,139,.1)` / deleted `rgba(199,78,57,.11)` washes
|
|
229
|
+
with green/red gutters, mono 11–12px.
|
|
230
|
+
|
|
231
|
+
## The weave, kept
|
|
232
|
+
|
|
233
|
+
Loom's identity survives as state, per Orca's own rule:
|
|
234
|
+
|
|
235
|
+
- **Selvage edges** — agent messages carry a 2px left border in a per-agent hue
|
|
236
|
+
(`hsl(hash(agent), 50%, 52%)`).
|
|
237
|
+
- **Shuttle handoff** — `a ⟿ b` rows in shuttle magenta.
|
|
238
|
+
- **Woven loader** — staggered warp bars shimmering in thread cyan.
|
|
239
|
+
- **Warp ground** — a near-invisible vertical-thread texture on the dark canvas.
|
|
240
|
+
- Live/needs-input dots pulse in thread/amber.
|
|
241
|
+
|
|
242
|
+
## Credits
|
|
243
|
+
|
|
244
|
+
Design system adapted from [Orca](https://github.com/stablyai/orca) (MIT,
|
|
245
|
+
© Lovecast Inc.). Geist typeface © Vercel, SIL OFL 1.1.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Integration surfaces — verified live
|
|
2
|
+
|
|
3
|
+
These are not guesses: each surface below was verified against the locally installed
|
|
4
|
+
tool before the adapter was written. Re-verify when versions move.
|
|
5
|
+
|
|
6
|
+
## Claude Code (verified: v2.1.83)
|
|
7
|
+
|
|
8
|
+
Headless invocation per turn:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
claude -p "<prompt>" \
|
|
12
|
+
--output-format stream-json --verbose \
|
|
13
|
+
[--resume <session-id>] \
|
|
14
|
+
[--append-system-prompt "<loom briefing>"] \
|
|
15
|
+
--permission-mode acceptEdits
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- `--output-format stream-json` — newline-delimited JSON events on stdout:
|
|
19
|
+
- `{"type":"system","subtype":"init","session_id":...}` → capture session id
|
|
20
|
+
- `{"type":"assistant","message":{"content":[{"type":"text"|"tool_use",...}]}}`
|
|
21
|
+
- `{"type":"result","subtype":"success","total_cost_usd":...}` → run complete
|
|
22
|
+
- `--resume <session-id>` — continue the same conversation across turns.
|
|
23
|
+
- `--append-system-prompt` — **this is how Loom injects the handoff briefing** without
|
|
24
|
+
touching user files.
|
|
25
|
+
- `--permission-mode` — `acceptEdits` default for baton holders (configurable).
|
|
26
|
+
- Interrupt = SIGINT to the child process (escalate SIGKILL).
|
|
27
|
+
|
|
28
|
+
## OpenCode (verified: v1.17.20)
|
|
29
|
+
|
|
30
|
+
`opencode serve --port <p> --hostname 127.0.0.1` per project dir, then HTTP:
|
|
31
|
+
|
|
32
|
+
| Purpose | Route |
|
|
33
|
+
|---|---|
|
|
34
|
+
| Health | `GET /api/health` |
|
|
35
|
+
| Create session | `POST /api/session` `{}` → `{ id: "ses…" }` |
|
|
36
|
+
| Send prompt | `POST /api/session/{id}/prompt` `{ "prompt": { "text": "…" } }` (async-admitted) |
|
|
37
|
+
| Wait for idle | `/api/session/{id}/wait` |
|
|
38
|
+
| Interrupt | `POST /api/session/{id}/interrupt` |
|
|
39
|
+
| Live events | `GET /event` (SSE) |
|
|
40
|
+
| Message detail | `GET /api/session/{id}/message/{messageID}` |
|
|
41
|
+
|
|
42
|
+
SSE event types Loom maps:
|
|
43
|
+
- `message.part.updated` (TextPart / ToolPart / PatchPart) → message, tool_call, file_edit
|
|
44
|
+
- `message.updated` with assistant `time.completed` → turn complete
|
|
45
|
+
- `permission.asked` / `question.asked` → **needs_input** (drives notifications)
|
|
46
|
+
|
|
47
|
+
Notes:
|
|
48
|
+
- Older docs say `POST /session/:id/message` and `/abort` — **wrong for 1.17.x**; it's
|
|
49
|
+
`/prompt` and `/interrupt`.
|
|
50
|
+
- No per-prompt system-prompt field, so Loom prepends the handoff briefing to the first
|
|
51
|
+
prompt after a handoff, clearly delimited.
|
|
52
|
+
|
|
53
|
+
Dogfood findings (verified live on 1.17.20):
|
|
54
|
+
- **`/wait` returns 503** `{"_tag":"ServiceUnavailableError","message":"Session wait is
|
|
55
|
+
not available yet"}` — it's in the OpenAPI spec but stubbed. Loom's adapter therefore
|
|
56
|
+
detects turn completion by **polling the message list** for a new completed assistant
|
|
57
|
+
message (SSE remains the live-streaming fast path), and reconciles any text the SSE
|
|
58
|
+
stream missed from the message detail.
|
|
59
|
+
- **Turns can end in `finish: "error"`** with an `error.message` (e.g. a provider
|
|
60
|
+
rejecting headless auth). The adapter surfaces these as Loom error events.
|
|
61
|
+
- **`serve` sessions don't inherit your TUI's model.** A session created with `{}` used
|
|
62
|
+
`github-copilot/gpt-5.6-luna` (which fails headless: "Personal Access Tokens are not
|
|
63
|
+
supported") while the TUI default was `opencode/minimax-m2.5`. Set the model
|
|
64
|
+
explicitly in the agent options: `{ "model": "opencode/minimax-m2.5" }`.
|
|
65
|
+
- Loom strips inherited `CLAUDE_CODE_*` / session `ANTHROPIC_BASE_URL` env before
|
|
66
|
+
spawning agents — running `loom` from inside a Claude Code terminal otherwise poisons
|
|
67
|
+
nested agent auth.
|
|
68
|
+
|
|
69
|
+
## Antigravity (bridge, best-effort)
|
|
70
|
+
|
|
71
|
+
Launched with a debug port it exposes a Chromium DevTools endpoint:
|
|
72
|
+
- `GET http://127.0.0.1:{port}/json/version` — presence check
|
|
73
|
+
- `GET http://127.0.0.1:{port}/json` — target list (read-only visibility)
|
|
74
|
+
|
|
75
|
+
No stable send/interrupt/memory surface → **Bridge tier** (never holds the baton),
|
|
76
|
+
receives projections via `.loom/memory/antigravity.md`.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# The Brain — an ADE memory layer for Loom
|
|
2
|
+
|
|
3
|
+
**Status:** proposal
|
|
4
|
+
**Reference:** `references/mem0` @ `739534c` (mem0ai/mem0, Apache-2.0), read at depth
|
|
5
|
+
**Note:** that checkout is a **v3 "additive" fork**. The classic ADD/UPDATE/DELETE memory-manager loop and the Neo4j graph layer are both *gone* from the code — `mem0/graphs/` and `graph_memory.py` do not exist, and `grep -n graph mem0/memory/main.py` returns zero hits. mem0's own `CLAUDE.md` still documents the graph; it is stale. Read the code, not the docs.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. What mem0 actually does
|
|
10
|
+
|
|
11
|
+
Four ideas. The vector database is the least interesting one.
|
|
12
|
+
|
|
13
|
+
**Idea 1 — memory is an addressable unit, not a document.** One short self-contained statement, with an id, an embedding, an md5 hash, scoping ids, and a payload. Not a transcript.
|
|
14
|
+
|
|
15
|
+
**Idea 2 — the write path reconciles; it does not append.** `main.py:872-1160`:
|
|
16
|
+
|
|
17
|
+
| Phase | What | Line |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| 0 | Last 10 messages of session context | `:876` |
|
|
20
|
+
| 1 | **Vector-search the store with the raw new conversation as the query** — pull the top 10 memories that might collide | `:882` |
|
|
21
|
+
| 2 | **One** LLM call seeing new messages *and* those candidates → facts already deduped | `:912` |
|
|
22
|
+
| 3 | Batch-embed | `:950` |
|
|
23
|
+
| 4-5 | md5 dedup vs existing hashes and within the batch | `:976` |
|
|
24
|
+
| 6 | Batch persist | `:1007` |
|
|
25
|
+
| 7 | Batch entity linking | `:1042` |
|
|
26
|
+
| 8 | Save raw messages regardless | `:1148` |
|
|
27
|
+
|
|
28
|
+
The older explicit form (`prompts.py:176`) states the intent plainly: per fact, return **ADD / UPDATE / DELETE / NONE** against the existing set. Memory that *self-corrects*. "Likes cheese pizza" + "loves chicken pizza" → one UPDATE, not two rows contradicting each other forever.
|
|
29
|
+
|
|
30
|
+
**Idea 3 — never let the LLM touch a real id.** `main.py:889-894` maps UUIDs to `"0"`, `"1"`, `"2"` before showing candidates, and maps back after. The comment is literally `# Map UUIDs to integers (anti-hallucination)`. A hallucinated `"7"` is detectable; a hallucinated UUID is silent corruption.
|
|
31
|
+
|
|
32
|
+
**Idea 4 — every change is append-only and auditable.** `storage.py` — a `history` table whose only writes are INSERT. Delete appends a tombstone (`is_deleted=1`); prior rows survive. You can always ask *why does it think that*.
|
|
33
|
+
|
|
34
|
+
### The read path, and the trap in it
|
|
35
|
+
|
|
36
|
+
`_search_vector_store` (`:1584`): lemmatize + extract query entities → embed → dense search over-fetching `max(limit*4, 60)` → BM25 (`keyword_search`, real, via a `Qdrant/bm25` sparse vector) → entity boosts → combine → shape.
|
|
37
|
+
|
|
38
|
+
**The trap, and the single most valuable thing in this whole reference:** at `:1622-1633` the candidate set is built **from the dense results only**. BM25 and entity boosts can only *re-rank what vector search already returned*. A memory that BM25 matches perfectly but dense search misses **cannot be retrieved, at any score**. It is not RRF, not a union — it is "dense recall, hybrid precision," and it is a recall bug wearing a hybrid-search costume. Do not copy this.
|
|
39
|
+
|
|
40
|
+
Scoring (`utils/scoring.py`) is additive with a **query-dependent divisor**: `min((semantic + bm25 + entity) / max_possible, 1.0)`, where `max_possible` is 1.0/1.5/2.0/2.5 depending on which signals fired *anywhere in the query*. So a doc scoring `semantic=0.9, bm25=0` gets 0.45 if some *other* doc matched keywords, and 0.9 if none did. Scores are not comparable across queries. And `threshold` gates the **raw semantic score before combining** (`:110-112`) — so it's a pure dense-recall gate that silently overrides the hybrid signals. Also don't copy.
|
|
41
|
+
|
|
42
|
+
Worth stealing outright: `memory_count_weight = 1/(1 + 0.001*(n-1)²)` (`:1758`) — a cheap inverse-quadratic IDF surrogate. An entity linked to 1 memory weighs 1.0; to 32 memories, 0.5; to 100, 0.09. Common entities self-suppress. Ten lines, no corpus statistics.
|
|
43
|
+
|
|
44
|
+
### The entity store is not a graph
|
|
45
|
+
|
|
46
|
+
`main.py:544-692`. It is a **bipartite inverted index wearing a vector-store costume**: a second collection where each row is `{data, entity_type, linked_memory_ids[]}`, vector = embedding of the entity text. There are **no entity↔entity edges and no traversal**. Lookup is fuzzy (≥0.5 similarity to boost, ≥0.95 to merge as an alias); the price is `list(top_k=10000)` full scans on every upsert (`:550`).
|
|
47
|
+
|
|
48
|
+
**And entities are extracted by spaCy, not an LLM** (`utils/entity_extraction.py:751`) — NER plus proper-noun/quoted/topic/identifier heuristics, resolved by span-overlap rejection against heavy stoplists. This matters for §3.
|
|
49
|
+
|
|
50
|
+
One more tell: the extraction prompt carefully elicits `linked_memory_ids` — memory→memory relations (`prompts.py:854`) — and `_add_to_vector_store` reads only `text` and `attributed_to` off each object (`:971-995`). **The memory graph is elicited and then thrown away.** If you don't store it, don't ask for it.
|
|
51
|
+
|
|
52
|
+
**What mem0 requires to exist:** Qdrant, an OpenAI key, SQLAlchemy, an embedding model, spaCy. Three services, a credential, and a 500MB model.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 2. What Loom's Brain actually is today
|
|
57
|
+
|
|
58
|
+
Honest inventory. A proposal aimed at an imaginary starting point is worthless.
|
|
59
|
+
|
|
60
|
+
**It is a read-only tab over a document that is recomputed from scratch on every GET. There is no brain store. There is no memory unit anywhere in the codebase.**
|
|
61
|
+
|
|
62
|
+
- `GET /api/projects/:id/memory` → `rt.unifiedMemory()` (`server.ts:721`). Every field is derived on the spot from (a) `decision` events in the log and (b) a **live filesystem read** of each ADE's native memory file.
|
|
63
|
+
- `src/core/memory.ts` — `NATIVE_MEMORY` (`:35`) maps every ADE to its real memory paths (`CLAUDE.md`, `AGENTS.md`, `.kiro/steering/*.md`, `.antigravity/memory.md`, `.windsurfrules`). `readNativeMemory()` reads and truncates each at 8k chars; `buildUnifiedMemory()` concatenates them with `slice(-40)` decisions into one markdown string. **This cross-ADE import is Loom's real insight and the thing no isolation-first tool can structurally own.** But merging is all it does.
|
|
64
|
+
- `importMemories()` (`runtime.ts:446`) is a **dedup ledger, not a store**: it appends a `memory_import` event carrying `{file, kind, chars, hash}`. The content is never stored — only its sha1 and length. Re-reading is always from disk.
|
|
65
|
+
- `src/core/projection.ts` — `buildProjection()` is a **recency window**: `slice(-30)` messages @300 chars, `slice(-20)` files, `slice(-5)` handoffs, all decisions @300 chars. `buildBriefing()` is the same at `slice(-8)` / `slice(-6)` @200 chars.
|
|
66
|
+
- `src/core/distill.ts` — optionally sends that window to Claude Haiku for prose, falling back to the template on any failure. **The precedent that matters: Loom already uses a logged-in CLI as an LLM** (`claudeText()`, `claude-cli.ts:17`). No API key involved.
|
|
67
|
+
- The event log (`eventlog.ts`) is append-only over **`node:sqlite` at `.loom/log.db`** (verified available — Node 25.8.2), JSONL only as fallback. 22 event kinds. `decision`, `turn_diff` (`{files, added, removed, patch}` per agent turn, from a real git snapshot), `file_edit`, and `memory_import` already exist. `types.ts` opens with the stated philosophy: *"The event log is the source of truth; everything else is a projection of it."*
|
|
68
|
+
|
|
69
|
+
### What's broken today, found while reading
|
|
70
|
+
|
|
71
|
+
These aren't hypotheticals; they're in the code right now.
|
|
72
|
+
|
|
73
|
+
1. **Nothing reads `.loom/memory/<id>.md`.** Zero readers in `src/`. `injectMemory()` has exactly one implementation (`base.ts:46`) — write the file — and **no adapter overrides it**. The mechanism is a pointer sentence in the briefing (`projection.ts:112`) plus a hope the agent runs Read. `injectMemory: true` in the capabilities means "writes a file," not "reaches the model."
|
|
74
|
+
2. **Only claude-code has guaranteed injection.** It gets `--append-system-prompt` (`claude-code.ts:86`). codex, grok, and opencode prepend the briefing to the user text because none of them has a system-prompt flag. **Bridges get nothing injected at all** — `send: false`, they never hold the baton, so the briefing never reaches Antigravity or Kiro; they only get the file written.
|
|
75
|
+
3. **`pendingBriefings` is an in-memory `Map`** (`runtime.ts:641`). A daemon restart between handoff and first message **silently drops the briefing**. The durability of the only guaranteed-injection path is "the process is still alive."
|
|
76
|
+
4. **The projection is chat-blind.** `handoff()` calls `log.list({limit: 400})` with no `chat` filter (`:680`) while `sendMessage` scopes per-chat (`:603`). Memory crosses conversation boundaries the thread UI keeps separate.
|
|
77
|
+
5. **Decisions are the only human-authored memory**, unstructured strings, append-only — no dedup, no edit, no delete path.
|
|
78
|
+
6. **Tokens are never recorded.** Only claude-code emits cost, and only as `{state:"turn_cost", costUsd}` into an in-memory map that resets on restart. There is no budget signal to size an injection against.
|
|
79
|
+
|
|
80
|
+
**The gap, in one sentence:**
|
|
81
|
+
|
|
82
|
+
> Loom's brain is a sliding window over recent chat plus a pile of hand-typed decisions; nothing is ever *learned* from a turn, nothing is ever *reconciled*, nothing is ever *retrieved by relevance*, and nothing is ever *forgotten* — and growth is bounded by truncation, which discards the durable architectural facts first and keeps the last 30 lines of chatter.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 3. The three constraints that force a different design
|
|
87
|
+
|
|
88
|
+
Loom is not mem0 with different nouns.
|
|
89
|
+
|
|
90
|
+
**A. No API key, no vector DB, no embedder.** Loom's deps: `express`, `ws`, `commander`, `ink`, `xterm`, `react`. Requiring an OpenAI key to have a memory would break the product's central promise — *your agents are already logged in; Loom just connects them*. **But Loom has what mem0 doesn't: six logged-in frontier models reachable headlessly.** `codex exec --json`, `claude -p`, `opencode run`. The extractor is already installed and already paid for, and `distill.ts` proves the pattern in production.
|
|
91
|
+
|
|
92
|
+
**B. The domain has literal, verifiable entities.** mem0 must run spaCy NER over prose and then fuzzy-match embeddings at 0.95 to decide "Osteria Francescana" is the same restaurant it saw last week. Loom's entities are `src/core/git.ts`, `AgentBase`, `PR #42`, `claude-code`, `EADDRINUSE` — exact strings that appear verbatim in both the conversation and the repo, and that can be **validated against `git ls-files`**. No NER model, no fuzzy alias matching, no 10k-row scans. A regex and a set. This is why lexical retrieval is viable here and isn't for mem0.
|
|
93
|
+
|
|
94
|
+
**C. Six consumers, not one.** mem0 hands memories to one app. Loom must reach six ADEs through four different mechanisms, two of which currently receive nothing at all. The read path has to *render per ADE*.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 4. The design
|
|
99
|
+
|
|
100
|
+
Keep `memory.ts`'s cross-ADE import — it's the good idea. Keep `projection.ts` as the **renderer**. Put a real memory layer underneath.
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
run_complete (turn boundary)
|
|
104
|
+
│
|
|
105
|
+
▼
|
|
106
|
+
┌─────────────┐ candidates ┌──────────────┐
|
|
107
|
+
│ Extractor │◄────────────────│ Retriever │
|
|
108
|
+
│ (an ADE, │ │ entities ∪ │
|
|
109
|
+
│ headless) │ │ BM25 │
|
|
110
|
+
└──────┬──────┘ └──────▲───────┘
|
|
111
|
+
│ ADD / UPDATE / FORGET / NONE │
|
|
112
|
+
▼ │
|
|
113
|
+
┌──────────────────────────────────────┴──────┐
|
|
114
|
+
│ The event log — memory ops ARE events │
|
|
115
|
+
│ fold → state. filter → history. (exists) │
|
|
116
|
+
└──────┬──────────────────────────────────────┘
|
|
117
|
+
│ retrieve(task, files, agent)
|
|
118
|
+
▼
|
|
119
|
+
┌─────────────┐
|
|
120
|
+
│ Compiler │ → per-ADE brief → the four injection mechanisms
|
|
121
|
+
└─────────────┘
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 4.1 The store: memories are events
|
|
125
|
+
|
|
126
|
+
**Do not build `brain.jsonl`. Do not add SQLite.** Add three event kinds to the log that already exists:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
| "memory_add" // payload: Memory
|
|
130
|
+
| "memory_update" // payload: { id, text?, confidence?, by, evidence }
|
|
131
|
+
| "memory_forget" // payload: { id, reason, by }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The Brain is then a **fold over the log**, cached in memory and kept live by `EventLog.onEvent` (`eventlog.ts:266`), which already exists. History is a filter over the same log — mem0 needs a whole second SQLite store for what Loom gets free. Ordering, durability, the sqlite/JSONL fallback, chat scoping, and the `since`/`kinds` query API are all already built and tested.
|
|
135
|
+
|
|
136
|
+
This is not a clever trick; it is what `types.ts:1-6` already says the architecture is. Everything else is a projection of the log. The Brain should be too.
|
|
137
|
+
|
|
138
|
+
Put `retrieve`/`add`/`forget` behind a `Brain` interface anyway, so a vector store can slot in later without touching callers. Don't build that now.
|
|
139
|
+
|
|
140
|
+
### 4.2 The unit
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
export type MemoryKind =
|
|
144
|
+
| "constraint" // a rule reality imposes: "a raw backtick in app-page.ts closes the template literal"
|
|
145
|
+
| "decision" // a choice and its reason: "chose CDP over an extension because Antigravity ships no API"
|
|
146
|
+
| "convention" // how this repo does things: "tests live in test/, *.test.ts, vitest"
|
|
147
|
+
| "fact" // how it is: "the daemon serves the app from one template literal, no build step"
|
|
148
|
+
| "failure" // what did not work, and why: "probed the Antigravity Manager for hours — the IDE is a different app"
|
|
149
|
+
| "task"; // ephemeral, run-scoped
|
|
150
|
+
|
|
151
|
+
export interface Memory {
|
|
152
|
+
id: string;
|
|
153
|
+
kind: MemoryKind;
|
|
154
|
+
/** One self-contained sentence. Must read true with zero surrounding context. */
|
|
155
|
+
text: string;
|
|
156
|
+
/** Literal strings: file paths, symbols, agent ids, error codes. Validated against the repo. */
|
|
157
|
+
entities: string[];
|
|
158
|
+
scope: { chat?: string; agent?: string; task?: string };
|
|
159
|
+
/** One click back to the turn that produced it. */
|
|
160
|
+
provenance: { agentId: string; eventId: number; ts: number };
|
|
161
|
+
/** The extractor's own confidence. Below the floor: shown in the UI, never injected. */
|
|
162
|
+
confidence: number;
|
|
163
|
+
/** The verbatim span it was extracted from. Verified to appear in the turn. */
|
|
164
|
+
evidence: string;
|
|
165
|
+
lemmas: string; // for BM25; never displayed
|
|
166
|
+
hash: string; // md5(text) — dedup before spending an LLM call
|
|
167
|
+
createdAt: number;
|
|
168
|
+
updatedAt: number;
|
|
169
|
+
expiresAt?: number; // task memories expire; constraints and failures never do
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The taxonomy is where this must diverge hardest from mem0, whose categories are personal (preferences, relationships, health). Loom's are engineering. **`failure` is the one nobody builds and the one that matters most** — negative knowledge is the most expensive to re-derive and the least likely to be written down. Every hour lost to "we already tried that" is a `failure` memory that didn't exist. This repo's own history is full of them: the Antigravity Manager, the empty DMG, `restore --staged` in a repo with no commits.
|
|
174
|
+
|
|
175
|
+
### 4.3 The write path — steal mem0's shape, fix its bug
|
|
176
|
+
|
|
177
|
+
On `run_complete` (turn boundary — *not* per message; that's the cost control):
|
|
178
|
+
|
|
179
|
+
1. **Retrieve candidates** — entity lookup over the turn's `turn_diff` files ∪ BM25 over its text → top ~10 that might collide. (mem0 phase 1, lexical.)
|
|
180
|
+
2. **Hash-dedup first.** mem0 dedups at phase 4, *after* the LLM call. Do it before: if the turn produces nothing whose hash is new, skip the call entirely. Free.
|
|
181
|
+
3. **One extractor call** — show it the turn, the candidates **integer-mapped, never real ids** (`main.py:889`), and the taxonomy. Returns ADD/UPDATE/FORGET/NONE with `text`, `kind`, `entities`, `confidence`, `evidence`.
|
|
182
|
+
4. **Verify in code, not in the prompt.** The `evidence` span must literally appear in the turn — `turn.includes(evidence)`. Drop the memory if not. One `includes()` call, and it's the cheapest hallucination guard available. Likewise every entity must appear in `git ls-files` or the turn text, or it's dropped.
|
|
183
|
+
5. **Append the ops** as events. The raw turn is already in the log — mem0's phase 8 is free.
|
|
184
|
+
|
|
185
|
+
Don't elicit what you won't store (mem0's discarded `linked_memory_ids`). If memory→memory links aren't in the schema, they aren't in the prompt.
|
|
186
|
+
|
|
187
|
+
The extractor is an ADE, chosen by `brain.extractor: "auto" | <agentId> | "off"`, defaulting to any available adapter **not currently holding the baton**. It runs **async and non-blocking** — a slow or broken extractor must never delay a handoff, exactly as `distill.ts` already refuses to let a broken Claude block one.
|
|
188
|
+
|
|
189
|
+
### 4.4 The read path — a union, not a re-rank
|
|
190
|
+
|
|
191
|
+
`retrieve({ query, files, agent, task, limit })`:
|
|
192
|
+
|
|
193
|
+
1. **Entity postings** — exact match on file paths and symbols from the task and `turn_diff`. High precision, zero cost.
|
|
194
|
+
2. **BM25** over `lemmas` (~150 lines of pure TS, no dependency).
|
|
195
|
+
3. **Union the two candidate sets.** This is the one place to consciously diverge from the reference: mem0's `:1622` restricts candidates to the dense hits, so a perfect keyword match that dense search missed is unreachable. With no dense channel at all, Loom's recall *is* entities ∪ BM25. Union them.
|
|
196
|
+
4. **Score with a fixed divisor**, not mem0's query-dependent one — scores should be comparable across queries. Apply `memory_count_weight` (`:1758`) to entity boosts; it's free IDF.
|
|
197
|
+
5. **Filter** — scope, confidence floor, expiry. And unlike mem0, expire *before* the top-k cut, not after: mem0 filters expired rows at `:1626` after over-fetching, so dead rows silently eat the recall budget.
|
|
198
|
+
6. **Rank by kind** at equal score: `constraint` and `failure` outrank `fact`. Getting burned again is worse than not knowing a detail.
|
|
199
|
+
|
|
200
|
+
No embeddings. If recall proves inadequate — **measure it against this repo's own log**, don't guess — the interface has room for a local embedder as an *optional* dependency, the way `node-pty` already is. Never a required key.
|
|
201
|
+
|
|
202
|
+
### 4.5 The injection — the part mem0 never had to solve
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
compile(memories, ade): string
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
| ADE | Mechanism | Today |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| claude-code | `--append-system-prompt` | ✅ guaranteed |
|
|
211
|
+
| codex / opencode / grok-code | prepend to user text (no system-prompt flag exists) | ✅ works |
|
|
212
|
+
| antigravity / kiro | no system prompt, never holds the baton | ❌ **nothing reaches the model** |
|
|
213
|
+
|
|
214
|
+
`memory.ts:35` already knows every native path. `injectMemory()` + `pendingBriefings` is already the seam. **What changes is only what goes in — retrieved, relevant, bounded memories instead of the last 30 lines of chat.** Three fixes ride along and should be scoped in:
|
|
215
|
+
|
|
216
|
+
- **Persist `pendingBriefings`** into `.loom/state.json` (it's a `Map` today; a restart drops it).
|
|
217
|
+
- **Give bridges a real path** — they can't take a system prompt, but their chat text can carry a compiled brief.
|
|
218
|
+
- **Scope the projection by chat**, matching `sendMessage`.
|
|
219
|
+
|
|
220
|
+
And it closes a loop that is currently open: Loom *reads* `CLAUDE.md` and never writes back. The Brain should offer to **write learned constraints into the repo's own `CLAUDE.md`/`AGENTS.md`** — user-gated, because it edits tracked files. That makes Loom's memory useful *even when Loom isn't running*, which is the strongest argument the product has.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 5. Phasing, honestly
|
|
225
|
+
|
|
226
|
+
| Phase | What | LLM? | Effort |
|
|
227
|
+
|---|---|---|---|
|
|
228
|
+
| **0** | Types, three event kinds, the fold, `add/get/list/forget` | no | S |
|
|
229
|
+
| **1** | Entity index + BM25 + `retrieve()` — deterministic, pure functions | no | M |
|
|
230
|
+
| **2** | Extractor via an ADE, reconciliation, int-id mapping, evidence + entity verification | **yes** | L |
|
|
231
|
+
| **3** | `compile()` per ADE; replace the blob at the handoff; keep the template fallback; fix the three injection bugs | no | M |
|
|
232
|
+
| **4** | Brain tab: real memories, provenance links, forget button, confidence, filters | no | M |
|
|
233
|
+
| **5** | Write-back to `CLAUDE.md`/`AGENTS.md`, user-gated | no | S |
|
|
234
|
+
| **6** | *Only if measured:* embeddings behind the interface | no | — |
|
|
235
|
+
|
|
236
|
+
Phases 0-1 are pure functions over a log that already exists — trivially unit-testable, no risk, no tokens. **Phase 2 is where both the money and the danger are.** Phase 3 is where the user first feels it.
|
|
237
|
+
|
|
238
|
+
**Suggested first cut: 0 + 1 + 4**, with memories seeded only from existing `decision` events and `turn_diff` files. That yields a working, honest, LLM-free Brain tab — real units, real retrieval, real forgetting — and proves the store and the recall before spending a single token. Then add 2 and measure it against the log this repo already has.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 6. Risks, stated plainly
|
|
243
|
+
|
|
244
|
+
**A wrong memory is worse than no memory.** This is the whole risk and everything else is a footnote. A hallucinated "constraint" gets injected into six agents, is believed by all of them, and compounds. mem0's blast radius is a bad restaurant recommendation; Loom's is every agent confidently building on a fiction. The mitigations are load-bearing, not nice-to-have: verified evidence spans (code, not prompt); entities validated against `git ls-files`; provenance on every memory with one click to the source turn; a confidence floor for injection; every memory visible and deletable in the Brain tab; and an append-only log, so `forget` is always available and always honest.
|
|
245
|
+
|
|
246
|
+
**Token cost per turn.** One extractor call per turn boundary. Mitigated by hash-dedup *before* the call, `run_complete`-only triggering, async execution, and shipping `brain.extractor: "off"` as a real supported mode. Note there's no token accounting today (§2.6) — if cost control matters, that gets fixed first or you're flying blind.
|
|
247
|
+
|
|
248
|
+
**Extractor variance.** Six ADEs will extract at six different qualities. Pin the extractor per project; never rotate silently.
|
|
249
|
+
|
|
250
|
+
**BM25 recall — the honest unknown.** Mitigated by the domain (literal entities) and by short extractor-written statements, but it must be *measured* against held-out turns from this repo's log before anyone declares victory. mem0 ships a benchmark suite for exactly this reason.
|
|
251
|
+
|
|
252
|
+
**Scope creep into a graph.** mem0's entity store is already a soft graph, and even mem0 deleted its real one. Resist. The entity index is enough until proven otherwise.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 7. Why this is worth building
|
|
257
|
+
|
|
258
|
+
mem0 gives one assistant a memory of one user.
|
|
259
|
+
|
|
260
|
+
Loom's thesis is different and larger: **six ADEs, one brain.** `memory.ts` already imports every agent's native memory and merges it — a real insight, and one that no isolation-first tool (separate worktrees, separate contexts) can structurally own. What's missing is that the merged thing isn't a memory yet. It's a pile, truncated by recency.
|
|
261
|
+
|
|
262
|
+
The gap between "everyone's notes, concatenated and cut off at 8k" and "a reconciled, retrievable, self-correcting store that every agent inherits and contributes to" is exactly the gap mem0 closed for personal assistants. Nobody has closed it for coding agents. And the pieces are already in this repo: an append-only sqlite log, per-turn file attribution via `turn_diff`, six headless frontier models, native memory paths for every ADE, an injection seam, and a stated architecture that already says everything is a projection of the log.
|