brainclaw 1.9.0 → 1.10.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/README.md +631 -499
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +18 -1
- package/dist/commands/code-map.js +129 -0
- package/dist/commands/codev.js +7 -0
- package/dist/commands/harvest.js +1 -1
- package/dist/commands/hooks.js +73 -73
- package/dist/commands/init.js +1 -1
- package/dist/commands/install-hooks.js +78 -78
- package/dist/commands/mcp-read-handlers.js +57 -14
- package/dist/commands/mcp.js +200 -13
- package/dist/commands/run-profile.js +3 -2
- package/dist/commands/switch.js +125 -93
- package/dist/commands/version.js +1 -1
- package/dist/core/agent-capability.js +19 -4
- package/dist/core/agent-files.js +131 -119
- package/dist/core/code-map/backend.js +123 -0
- package/dist/core/code-map/core.js +81 -0
- package/dist/core/code-map/drafts.js +2 -0
- package/dist/core/code-map/extractor.js +29 -0
- package/dist/core/code-map/finalizer.js +191 -0
- package/dist/core/code-map/freshness.js +108 -0
- package/dist/core/code-map/ids.js +0 -0
- package/dist/core/code-map/importable.js +35 -0
- package/dist/core/code-map/indexes.js +197 -0
- package/dist/core/code-map/lang/java/imports.scm +17 -0
- package/dist/core/code-map/lang/java/index.js +254 -0
- package/dist/core/code-map/lang/java/tags.scm +48 -0
- package/dist/core/code-map/lang/php/imports.scm +21 -0
- package/dist/core/code-map/lang/php/index.js +251 -0
- package/dist/core/code-map/lang/php/tags.scm +44 -0
- package/dist/core/code-map/lang/provider.js +9 -0
- package/dist/core/code-map/lang/providers.js +24 -0
- package/dist/core/code-map/lang/python/imports.scm +90 -0
- package/dist/core/code-map/lang/python/index.js +364 -0
- package/dist/core/code-map/lang/python/tags.scm +81 -0
- package/dist/core/code-map/lang/query-runtime.js +374 -0
- package/dist/core/code-map/lang/registry.js +125 -0
- package/dist/core/code-map/lang/typescript/imports.scm +90 -0
- package/dist/core/code-map/lang/typescript/index.js +306 -0
- package/dist/core/code-map/lang/typescript/tags.js.scm +106 -0
- package/dist/core/code-map/lang/typescript/tags.scm +151 -0
- package/dist/core/code-map/lock.js +210 -0
- package/dist/core/code-map/materialized.js +51 -0
- package/dist/core/code-map/memory-reader.js +59 -0
- package/dist/core/code-map/paths.js +53 -0
- package/dist/core/code-map/query.js +568 -0
- package/dist/core/code-map/refresh.js +0 -0
- package/dist/core/code-map/resolve.js +177 -0
- package/dist/core/code-map/store.js +206 -0
- package/dist/core/code-map/types.js +288 -0
- package/dist/core/code-map/vocabulary.js +57 -0
- package/dist/core/code-map/wasm-loader.js +294 -0
- package/dist/core/code-map/work-section.js +206 -0
- package/dist/core/codev-prompts.js +38 -38
- package/dist/core/codev-rounds.js +4 -0
- package/dist/core/default-profiles/doctor.yaml +11 -11
- package/dist/core/default-profiles/janitor.yaml +11 -11
- package/dist/core/default-profiles/onboarder.yaml +11 -11
- package/dist/core/default-profiles/reviewer.yaml +13 -13
- package/dist/core/dispatcher.js +1 -1
- package/dist/core/entity-operations.js +29 -3
- package/dist/core/execution-adapters.js +11 -10
- package/dist/core/execution-profile.js +58 -0
- package/dist/core/execution.js +1 -1
- package/dist/core/facade-schema.js +9 -0
- package/dist/core/instruction-templates.js +2 -0
- package/dist/core/loops/verbs.js +0 -1
- package/dist/core/mcp-command-resolution.js +3 -1
- package/dist/core/messaging.js +2 -2
- package/dist/core/protocol-skills.js +164 -164
- package/dist/core/runtime-signals.js +1 -1
- package/dist/core/search.js +19 -2
- package/dist/core/security-guard.js +207 -207
- package/dist/core/spawn-check.js +16 -2
- package/dist/core/staleness.js +1 -1
- package/dist/core/store-resolution.js +67 -11
- package/dist/core/worktree.js +18 -18
- package/dist/facts.js +9 -5
- package/dist/facts.json +8 -4
- package/dist/vendor/web-tree-sitter/tree-sitter.js +3980 -0
- package/dist/vendor/web-tree-sitter/tree-sitter.wasm +0 -0
- package/dist/wasm/tree-sitter-java.wasm +0 -0
- package/dist/wasm/tree-sitter-javascript.wasm +0 -0
- package/dist/wasm/tree-sitter-php.wasm +0 -0
- package/dist/wasm/tree-sitter-python.wasm +0 -0
- package/dist/wasm/tree-sitter-tsx.wasm +0 -0
- package/dist/wasm/tree-sitter-typescript.wasm +0 -0
- package/dist/wasm/tree-sitter.wasm +0 -0
- package/docs/PROTOCOL.md +1 -1
- package/docs/adapters/openclaw.md +43 -43
- package/docs/architecture/project-refs.md +328 -328
- package/docs/cli.md +2131 -2093
- package/docs/code-map.md +198 -0
- package/docs/concepts/coordination.md +52 -52
- package/docs/concepts/coordinator-runbook.md +129 -129
- package/docs/concepts/dispatch-lifecycle.md +245 -245
- package/docs/concepts/event-log-store.md +928 -928
- package/docs/concepts/ideation-loop.md +317 -317
- package/docs/concepts/loop-engine.md +520 -511
- package/docs/concepts/mcp-governance.md +268 -268
- package/docs/concepts/memory.md +84 -84
- package/docs/concepts/multi-agent-workflows.md +167 -167
- package/docs/concepts/observer-protocol.md +361 -361
- package/docs/concepts/plans-and-claims.md +217 -217
- package/docs/concepts/project-md-convention.md +35 -35
- package/docs/concepts/runtime-notes.md +38 -38
- package/docs/concepts/troubleshooting.md +254 -254
- package/docs/concepts/workspace-bootstrapping.md +142 -142
- package/docs/context-format-changelog.md +35 -35
- package/docs/context-format.md +48 -48
- package/docs/index.md +65 -65
- package/docs/integrations/agents.md +158 -158
- package/docs/integrations/claude-code.md +23 -23
- package/docs/integrations/cline.md +77 -77
- package/docs/integrations/continue.md +55 -55
- package/docs/integrations/copilot.md +68 -68
- package/docs/integrations/cursor.md +23 -23
- package/docs/integrations/kilocode.md +72 -72
- package/docs/integrations/mcp.md +385 -378
- package/docs/integrations/mistral-vibe.md +122 -122
- package/docs/integrations/openclaw.md +92 -92
- package/docs/integrations/opencode.md +84 -84
- package/docs/integrations/overview.md +115 -115
- package/docs/integrations/roo.md +71 -71
- package/docs/integrations/windsurf.md +77 -77
- package/docs/mcp-schema-changelog.md +364 -356
- package/docs/playbooks/integration/index.md +121 -121
- package/docs/playbooks/orchestration.md +37 -0
- package/docs/playbooks/productivity/index.md +99 -99
- package/docs/playbooks/team/index.md +117 -117
- package/docs/product/agent-first-model.md +184 -184
- package/docs/product/entity-model-audit.md +462 -462
- package/docs/product/positioning.md +86 -86
- package/docs/quickstart-existing-project.md +107 -107
- package/docs/quickstart.md +183 -183
- package/docs/release-maintenance.md +79 -79
- package/docs/reputation.md +52 -52
- package/docs/review.md +45 -45
- package/docs/security.md +212 -212
- package/docs/server-operations.md +118 -118
- package/docs/storage.md +106 -106
- package/package.json +86 -66
- package/docs/concepts/event-log-store-critique-A.md +0 -333
- package/docs/concepts/event-log-store-critique-B.md +0 -353
- package/docs/concepts/event-log-store-phase0-measurements.md +0 -58
- package/docs/concepts/event-log-store-proposal-A.md +0 -365
- package/docs/concepts/event-log-store-proposal-B.md +0 -404
- package/docs/concepts/identity-model-proposal.md +0 -371
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
# Event-Log Store — Proposal A (slot A, round 1)
|
|
2
|
-
|
|
3
|
-
> Ideation artifact for lop_3bf55b9492e0d96c (pln_2290bc70 / pln#543 step 1).
|
|
4
|
-
> Independent proposal — not yet cross-critiqued. Status: DRAFT.
|
|
5
|
-
|
|
6
|
-
## 0. Position summary
|
|
7
|
-
|
|
8
|
-
Evolve `src/core/event-log.ts` from a best-effort notification stream into a
|
|
9
|
-
**write-ahead journal of full-entity snapshots**, organized as **immutable
|
|
10
|
-
sealed segments + checkpoint records**, with the existing per-entity JSON
|
|
11
|
-
directories demoted to **lazily reconciled projections** (pln#496 pattern).
|
|
12
|
-
Ordering comes from a **store-global sequence number assigned under the
|
|
13
|
-
existing store lock** — never from timestamps. Migration is a four-phase,
|
|
14
|
-
flag-gated dual-write with a backfill genesis and a park-don't-delete rollback.
|
|
15
|
-
|
|
16
|
-
The three deliberate "boring" choices, argued below:
|
|
17
|
-
|
|
18
|
-
1. **Full snapshot payloads, not diffs** (§2) — correctness over bytes.
|
|
19
|
-
2. **The store lock is the concurrency primitive, O_APPEND is the seatbelt**
|
|
20
|
-
(§3) — no new locking protocol.
|
|
21
|
-
3. **One journal per store, not per entity** (§5) — global order for free.
|
|
22
|
-
|
|
23
|
-
## 1. Seed analysis — what exists and the exact gap
|
|
24
|
-
|
|
25
|
-
What `src/core/event-log.ts` + call sites give us today:
|
|
26
|
-
|
|
27
|
-
| Capability | State today | Gap to source-of-truth |
|
|
28
|
-
|---|---|---|
|
|
29
|
-
| Append on every mutation | `appendEvent` wired into `persistStateUnlocked` (state.ts:200) and ~30 verb sites | Events carry no payload — state NOT reconstructible |
|
|
30
|
-
| Envelope | `{ts, agent, agent_id?, user?, action, item_type, item_id?, summary?}` | No seq, no writer identity, no payload, no schema version |
|
|
31
|
-
| Durability | `appendFileSync` inside try/catch that **swallows all errors** (event-log.ts:94-96) | A journal that may silently drop writes cannot be authoritative |
|
|
32
|
-
| Rotation | At 10MB: rename file away, **delete all cursors** (event-log.ts:211-236) | History unreachable by readers; cursor reset re-notifies nothing (offset semantics gone) |
|
|
33
|
-
| Readers | `readAllEvents` (full parse), `readUnseenEvents` (byte-offset cursors), `buildNotificationSummary` | Cursors are bare byte offsets into a file that rotation deletes |
|
|
34
|
-
| Concurrency | All mutations serialize via `mutate()` → store-wide lock with token ownership + refresh (lock.ts, hardened sprint 1.5) | appendEvent itself is also called outside any documented invariant; lock-expiry break creates a 2-writer window |
|
|
35
|
-
|
|
36
|
-
Adjacent precedent worth noting: **loops already run a payload-carrying
|
|
37
|
-
per-loop journal** (`src/core/loops/store.ts` `appendEvent` →
|
|
38
|
-
`loops/<id>/events.jsonl`, zod-validated `LoopEventSchema`, thread file as
|
|
39
|
-
projection). The store journal should converge on the same shape; unifying
|
|
40
|
-
the loop journals into it is explicitly a **non-goal** of this spec (future
|
|
41
|
-
work, §10).
|
|
42
|
-
|
|
43
|
-
## 2. Event payload format: full entity snapshot
|
|
44
|
-
|
|
45
|
-
### Decision
|
|
46
|
-
|
|
47
|
-
Every state-changing event carries the **complete post-image of the entity**
|
|
48
|
-
(`payload` = the same zod-validated document that today lands in
|
|
49
|
-
`<entity>/<id>.json`). Deletions are explicit tombstones (`payload: null`,
|
|
50
|
-
`action: "delete"`). No JSON-patch, no field-deltas.
|
|
51
|
-
|
|
52
|
-
```jsonc
|
|
53
|
-
{
|
|
54
|
-
"v": 2, // envelope schema version
|
|
55
|
-
"seq": 18204, // store-global, assigned under lock (§4)
|
|
56
|
-
"ts": "2026-06-10T14:02:11.318Z",// informational ONLY, never ordering
|
|
57
|
-
"writer": "w_1a2b3c", // writer instance id: pid + start-nonce
|
|
58
|
-
"agent": "claude-code",
|
|
59
|
-
"agent_id": "agt_...",
|
|
60
|
-
"user": "jberdah",
|
|
61
|
-
"action": "update", // existing EventAction union
|
|
62
|
-
"item_type": "plan",
|
|
63
|
-
"item_id": "pln_2290bc70",
|
|
64
|
-
"entity_rev": 7, // per-entity monotonic revision (§6)
|
|
65
|
-
"payload": { /* full post-image, schema-versioned doc */ },
|
|
66
|
-
"summary": "step 1 completed" // kept for notification consumers
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Why snapshots win
|
|
71
|
-
|
|
72
|
-
- **Replay is a trivially correct reducer.** Rebuild = single pass, last
|
|
73
|
-
event per `item_id` wins, tombstones remove. No patch-base tracking, no
|
|
74
|
-
op-ordering semantics, no "patch against missing base" failure class.
|
|
75
|
-
- **Zero-dep constraint.** JSON-patch (RFC 6902) means either a new runtime
|
|
76
|
-
dep or a hand-rolled implementation — hand-rolled patch appliers are
|
|
77
|
-
exactly the kind of subtle code that produced trp_d5595086 (silent data
|
|
78
|
-
loss via load-swallow). Snapshots reuse the existing zod schemas as the
|
|
79
|
-
only validation layer.
|
|
80
|
-
- **Partial history loss is survivable.** With diffs, one torn/lost event
|
|
81
|
-
poisons every later diff for that entity. With snapshots, the next event
|
|
82
|
-
for that entity self-heals the projection.
|
|
83
|
-
- **Size is bounded by compaction, not by the payload format.** Entities are
|
|
84
|
-
1–10KB. The worst case (a plan updated 50× in a session) costs ~500KB of
|
|
85
|
-
journal — and §5's checkpointing makes old fat segments irrelevant to
|
|
86
|
-
replay cost. Optimizing bytes here buys nothing measurable and sells
|
|
87
|
-
correctness.
|
|
88
|
-
|
|
89
|
-
### Trade-off acknowledged
|
|
90
|
-
|
|
91
|
-
Snapshot payloads make the journal **chatty in git diffs** (every event line
|
|
92
|
-
embeds the whole doc). Mitigation: the journal lives under
|
|
93
|
-
`.brainclaw/journal/` and the *projections* stay git-diffable as today —
|
|
94
|
-
store identity for human review remains the per-entity JSON files +
|
|
95
|
-
PROJECT.md. The journal is diffable too (append-only = pure line additions),
|
|
96
|
-
just verbose. OPEN QUESTION Q1 (§11) flags whether `payload` should be
|
|
97
|
-
elided for `summary`-only event kinds (session_start, run_* lifecycle) —
|
|
98
|
-
proposed: yes, payload is required only for actions that mutate a persisted
|
|
99
|
-
entity.
|
|
100
|
-
|
|
101
|
-
## 3. Atomicity & append guarantees
|
|
102
|
-
|
|
103
|
-
### Append protocol (replaces the swallow)
|
|
104
|
-
|
|
105
|
-
1. All journal appends happen **inside the store mutation lock** — the
|
|
106
|
-
journal becomes part of the `mutate()` critical section, ordered
|
|
107
|
-
**journal-first** (write-ahead): append event(s) → apply projections →
|
|
108
|
-
release lock. Crash between append and projection write = projection is
|
|
109
|
-
stale-but-rebuildable; the lazy reconciler (§6) converges it on next read.
|
|
110
|
-
2. One event = **one `fs.writeSync` call** on a fd opened with flag `'a'`
|
|
111
|
-
(O_APPEND on POSIX; FILE_APPEND_DATA on Windows). Single-syscall writes
|
|
112
|
-
of a full line ≤ a few KB do not interleave on local filesystems on
|
|
113
|
-
either platform. This is the **seatbelt**, not the primary guarantee —
|
|
114
|
-
the lock is. It matters precisely in the lock-expiry race (lock.ts O2
|
|
115
|
-
residual: a breaker can briefly coexist with a stale-but-alive owner):
|
|
116
|
-
two appenders may both write, but neither corrupts the other's line, and
|
|
117
|
-
`(writer, seq)` lets the reader detect the overlap (§4).
|
|
118
|
-
3. **Failures are loud.** If the journal is source of truth, a failed append
|
|
119
|
-
is a failed mutation: `appendEvent` throws inside `mutate()`, the
|
|
120
|
-
mutation reports failure to the caller, projections are not written.
|
|
121
|
-
The current `logger.debug` swallow is removed in journal-mode. (During
|
|
122
|
-
the dual-write phase the legacy stream keeps best-effort semantics; the
|
|
123
|
-
new journal is strict from day one — see §8.)
|
|
124
|
-
|
|
125
|
-
### fsync policy
|
|
126
|
-
|
|
127
|
-
Default: **no fsync per event.** Node's `writeSync` hands data to the OS;
|
|
128
|
-
on OS crash / power loss we may lose the tail. That is acceptable because
|
|
129
|
-
(a) projections exist on disk as a second copy, (b) the store also commits
|
|
130
|
-
to git (`commitMemoryChange`), and (c) the recovery rule below makes a torn
|
|
131
|
-
tail non-fatal. Config escape hatch: `journal.fsync: always | rotate | off`
|
|
132
|
-
(default `rotate` — fsync on segment seal and checkpoint write only).
|
|
133
|
-
Per-event fsync on Windows is brutal (~ms each) and would violate the
|
|
134
|
-
"MCP worker-per-call stays cheap" constraint.
|
|
135
|
-
|
|
136
|
-
### Torn-tail recovery (read side)
|
|
137
|
-
|
|
138
|
-
A reader hitting the active segment applies one rule: **a final line not
|
|
139
|
-
terminated by `\n`, or that fails `JSON.parse`, is a torn write — ignore
|
|
140
|
-
it and treat the previous line as head.** The next writer, before
|
|
141
|
-
appending, checks the last byte of the file; if it is not `\n`, it moves
|
|
142
|
-
the torn bytes into `journal/quarantine/<segment>-<ts>.torn` and appends a
|
|
143
|
-
`journal_repair` event recording what was quarantined. Torn data is parked,
|
|
144
|
-
never deleted (house rule). Mid-file malformed lines (should be impossible
|
|
145
|
-
under the lock) are surfaced via `logger.warn` + `brainclaw doctor`, never
|
|
146
|
-
silently skipped — that is the trp_d5595086 lesson applied to the journal.
|
|
147
|
-
|
|
148
|
-
### Windows/POSIX divergence note
|
|
149
|
-
|
|
150
|
-
All of the above must land with tests run on both families — the sprint-1
|
|
151
|
-
release-gate trap (trp_e85e9fbe) showed `shell:true` spawn and path-
|
|
152
|
-
normalization behaviors diverge between local Windows, CI Windows, and
|
|
153
|
-
Linux. The append-atomicity seatbelt specifically needs a two-process
|
|
154
|
-
stress test (spawn N children appending K events; assert no interleaved
|
|
155
|
-
bytes, no lost `(writer, seq)` pairs) on both platforms.
|
|
156
|
-
|
|
157
|
-
## 4. Ordering: store-global seq under the lock; ts is decoration
|
|
158
|
-
|
|
159
|
-
- `seq` is a **store-global monotonic integer**, persisted in
|
|
160
|
-
`journal/meta.json` (`next_seq`), incremented under the mutation lock.
|
|
161
|
-
Since every legitimate append already holds the lock, this is trivially
|
|
162
|
-
total-ordered. File order and seq order coincide in the normal case.
|
|
163
|
-
- `writer` (pid + random start nonce) disambiguates the abnormal case: the
|
|
164
|
-
lock-expiry break window can produce two writers that both read the same
|
|
165
|
-
`next_seq`. Readers treat `(seq, writer)` as the identity; a duplicate
|
|
166
|
-
`seq` from different writers is a **detected anomaly** — the reducer
|
|
167
|
-
applies both in file order and emits a doctor warning. Snapshot payloads
|
|
168
|
-
(§2) make this safe: the later file line wins wholesale, no diff
|
|
169
|
-
corruption.
|
|
170
|
-
- **Timestamps never order anything.** Clock skew across agent shells,
|
|
171
|
-
WSL, and containers is real; `ts` is for humans and notification
|
|
172
|
-
summaries only.
|
|
173
|
-
- **Federation hook (informative, not normative here):** the journal is the
|
|
174
|
-
Pull-and-Materialize substrate. Remote events are imported preserving
|
|
175
|
-
`origin: {store_id, seq}` and assigned fresh local seqs; per-entity
|
|
176
|
-
conflict detection uses `entity_rev` (§6). Cross-store merge semantics
|
|
177
|
-
(entity-level LWW vs. manual conflict surfacing) is OPEN QUESTION Q2 —
|
|
178
|
-
it needs the federation architecture's owner, not this spec.
|
|
179
|
-
|
|
180
|
-
## 5. Segments + checkpoints replace lossy rotation
|
|
181
|
-
|
|
182
|
-
### Layout
|
|
183
|
-
|
|
184
|
-
```
|
|
185
|
-
.brainclaw/journal/
|
|
186
|
-
meta.json # next_seq, active segment id, checkpoint ref
|
|
187
|
-
active.jsonl # current segment, append-only
|
|
188
|
-
segments/
|
|
189
|
-
seg-000001-000812.jsonl # sealed, IMMUTABLE (seq range in name)
|
|
190
|
-
seg-000813-001650.jsonl
|
|
191
|
-
checkpoints/
|
|
192
|
-
ckpt-001650.json # full-state snapshot manifest at seq 1650
|
|
193
|
-
quarantine/ # torn tails, unparseable lines (parked)
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### Seal & checkpoint protocol (under the lock)
|
|
197
|
-
|
|
198
|
-
When `active.jsonl` crosses the size threshold (keep 10MB):
|
|
199
|
-
|
|
200
|
-
1. Write `ckpt-<seq>.json`: a manifest containing the full materialized
|
|
201
|
-
state at head seq (entity id → post-image, or — cheaper — entity id →
|
|
202
|
-
`{file hash, entity_rev}` referencing the projection files; OPEN
|
|
203
|
-
QUESTION Q3 picks between self-contained vs. referencing checkpoints).
|
|
204
|
-
2. fsync checkpoint, then rename `active.jsonl` →
|
|
205
|
-
`segments/seg-<first>-<last>.jsonl` (sealed segments are immutable
|
|
206
|
-
forever after).
|
|
207
|
-
3. Create fresh `active.jsonl` whose first record is a `checkpoint_ref`
|
|
208
|
-
event pointing at the checkpoint.
|
|
209
|
-
4. Update `meta.json` last (atomic write via existing `writeFileAtomic`).
|
|
210
|
-
|
|
211
|
-
**Rebuild cost is bounded:** latest checkpoint + replay of `active.jsonl`
|
|
212
|
-
only (≤10MB ⇒ ~5–15k events ⇒ well under the 1s cold-read target on the
|
|
213
|
-
hardware we run). A 100k-event store replays the same ≤10MB tail; the other
|
|
214
|
-
90k events sit in sealed segments that only `doctor --verify-journal`,
|
|
215
|
-
backfill audits, or federation ever read.
|
|
216
|
-
|
|
217
|
-
### Cursors survive rotation
|
|
218
|
-
|
|
219
|
-
`AgentCursor` becomes `{segment_id, offset, last_read}`. Sealing does not
|
|
220
|
-
touch cursors: a cursor pointing into a now-sealed segment is still valid
|
|
221
|
-
(the file still exists, immutable). `readUnseenEvents` walks: remainder of
|
|
222
|
-
cursor's segment → subsequent sealed segments → active. The current
|
|
223
|
-
"rotation deletes all cursors" behavior — and its silent re-notification
|
|
224
|
-
loss — disappears entirely.
|
|
225
|
-
|
|
226
|
-
### Retention
|
|
227
|
-
|
|
228
|
-
Sealed segments and checkpoints are **never auto-deleted**. `brainclaw gc`
|
|
229
|
-
may archive segments older than the N-th checkpoint into
|
|
230
|
-
`.brainclaw/gc-backups/` (existing park location). Rationale: house rule
|
|
231
|
-
park-don't-delete, plus federation may need deep history.
|
|
232
|
-
|
|
233
|
-
## 6. Projections: entity dirs become lazily reconciled caches
|
|
234
|
-
|
|
235
|
-
### Mechanics
|
|
236
|
-
|
|
237
|
-
- Each projection family (constraints, decisions, traps, handoffs, plans,
|
|
238
|
-
and later assignments/runs/claims) tracks `last_applied_seq` in
|
|
239
|
-
`journal/meta.json` (single file, one stat to check everything).
|
|
240
|
-
- **Read path** (`loadState`, entity getters): stat-compare
|
|
241
|
-
`meta.next_seq - 1` vs `last_applied_seq`. Equal → serve projection files
|
|
242
|
-
as today (zero added cost beyond one small JSON read — keeps the MCP
|
|
243
|
-
worker-per-call re-import model cheap). Behind → acquire lock, replay the
|
|
244
|
-
delta events into the projection files, bump `last_applied_seq`, serve.
|
|
245
|
-
This is exactly the pln#496 lazy-reconcile pattern; **no daemon**.
|
|
246
|
-
- **Write path migration** is the key ergonomic question. Today
|
|
247
|
-
`mutateState(fn)` lets callers imperatively poke a full `State`. End
|
|
248
|
-
state: writers emit **events**, and projections are a pure
|
|
249
|
-
`apply(state, event)` reducer. Transition without rewriting ~all call
|
|
250
|
-
sites: `persistStateUnlocked` computes an **id-level diff** (loaded state
|
|
251
|
-
vs. mutated state per entity collection: created / content-changed /
|
|
252
|
-
removed) and synthesizes the corresponding snapshot events automatically.
|
|
253
|
-
Callers keep their API; the journal gets correct per-entity events.
|
|
254
|
-
Verb-level code (assignments.ts, agentruns.ts, loops) migrates to
|
|
255
|
-
explicit event emission opportunistically afterwards.
|
|
256
|
-
- `syncDirectory(deleteMissing)` keeps its trp_d5595086 guard during dual-
|
|
257
|
-
write. In journal-primary mode, deletion authority moves to tombstone
|
|
258
|
-
events: a projection file is only unlinked when a tombstone for its id is
|
|
259
|
-
applied — "absent from in-memory state" stops being a deletion signal at
|
|
260
|
-
all, which closes that bug class structurally rather than defensively.
|
|
261
|
-
|
|
262
|
-
### entity_rev
|
|
263
|
-
|
|
264
|
-
Each entity carries a monotonic `entity_rev` bumped on every event for that
|
|
265
|
-
id. Uses: cheap "did it change" checks for projections, optimistic-
|
|
266
|
-
concurrency guard for future API writes, and the conflict-detection
|
|
267
|
-
primitive federation needs. Stored in the event envelope, not inside the
|
|
268
|
-
document (keeps entity schemas untouched during migration).
|
|
269
|
-
|
|
270
|
-
## 7. Crash-matrix (invariants self-attack, round-1 honesty)
|
|
271
|
-
|
|
272
|
-
| Scenario | Outcome under this design |
|
|
273
|
-
|---|---|
|
|
274
|
-
| Crash mid-append | Torn tail quarantined on next read/write (§3); at most the in-flight mutation lost; projections untouched (journal-first means they were never written) |
|
|
275
|
-
| Crash between append and projection write | Journal ahead of projection; lazy reconcile converges on next read |
|
|
276
|
-
| Crash during seal (steps 1–4) | Ordered writes + meta-last: worst case is a checkpoint file with no meta ref (orphan, harmless) or active not yet renamed (retry seal next time); each step idempotent-checkable |
|
|
277
|
-
| Two writers (lock-expiry break, O2 residual) | O_APPEND prevents byte interleaving; duplicate seq detected via `(seq, writer)`; snapshot payloads make double-apply convergent; doctor warning emitted |
|
|
278
|
-
| Rotation during concurrent read | Sealed segments immutable; reader holding an fd on the just-sealed file still reads valid data (POSIX) — **Windows caveat:** rename of an open file can fail (EPERM/EBUSY); seal must retry/defer if the rename fails, never copy-then-delete. Flagged for test coverage |
|
|
279
|
-
| Clock skew | Irrelevant — ts never orders (§4) |
|
|
280
|
-
| 100k-event store | Replay bounded to active segment by checkpoints (§5); cold read target <1s holds |
|
|
281
|
-
| Journal disabled/absent (worktree without .brainclaw, trp_26e9634b) | Same failure mode as today's store access — out of scope here, but the spec must not make CLI hang harder on missing journal; ENOENT → clear error |
|
|
282
|
-
|
|
283
|
-
## 8. Migration plan (flag: `journal.mode = off | dual | primary`)
|
|
284
|
-
|
|
285
|
-
- **Phase 0 — substrate (no reader change):** envelope v2, loud appends,
|
|
286
|
-
seq/meta, segments+checkpoints, cursor format migration. Journal still
|
|
287
|
-
consumed only by notifications. Old `events.jsonl` (17k events) and any
|
|
288
|
-
`events.<ts>.jsonl` archives are **parked** under
|
|
289
|
-
`journal/legacy/` — they carry no payloads and cannot be upgraded; they
|
|
290
|
-
remain audit history, not replayable state.
|
|
291
|
-
- **Phase 1 — dual-write (`dual`):** genesis backfill emits one
|
|
292
|
-
`backfill`-action snapshot event per existing entity (current state =
|
|
293
|
-
seq 1..N), then `persistStateUnlocked` diff-emission (§6) runs alongside
|
|
294
|
-
the unchanged file writes. State dirs remain authoritative. Rollback =
|
|
295
|
-
set `off`; nothing depended on the journal yet.
|
|
296
|
-
- **Phase 2 — verification:** `brainclaw doctor --verify-journal` rebuilds
|
|
297
|
-
state from checkpoint+journal in a temp dir and diffs against the live
|
|
298
|
-
projections; runs in CI on both OS families and in dogfooding for a full
|
|
299
|
-
sprint. Exit criterion: zero divergence across a sprint of real
|
|
300
|
-
multi-agent traffic, including dispatch worktree churn.
|
|
301
|
-
- **Phase 3 — primary (`primary`):** reads serve projections via lazy
|
|
302
|
-
reconcile; deletion authority moves to tombstones; `mutateState` callers
|
|
303
|
-
unchanged. **Rollback path:** projections are at all times a full
|
|
304
|
-
materialized state in the exact legacy format — flipping back to `dual`
|
|
305
|
-
or `off` requires no data transformation, only re-arming the legacy
|
|
306
|
-
delete semantics. Backup of `.brainclaw/` taken at each phase flip
|
|
307
|
-
(upgrade-style, park-don't-delete).
|
|
308
|
-
- **Perf gates (measured, not asserted):** `bclaw_work` cold read < 1s;
|
|
309
|
-
single-entity op cost independent of store size (O(1) append + O(1)
|
|
310
|
-
projection write + O(delta) reconcile); MCP per-call overhead delta
|
|
311
|
-
< 50ms vs. today.
|
|
312
|
-
|
|
313
|
-
## 9. Hard-constraint compliance check
|
|
314
|
-
|
|
315
|
-
- Zero new runtime deps: JSONL + zod + node:fs only. ✓
|
|
316
|
-
- Windows + POSIX: O_APPEND/FILE_APPEND_DATA seatbelt, rename-of-open-file
|
|
317
|
-
caveat handled, dual-platform CI mandated (trp_e85e9fbe). ✓
|
|
318
|
-
- File-based, git-diffable identity: projections unchanged; journal is
|
|
319
|
-
append-only text. ✓
|
|
320
|
-
- No daemon: reconciliation only at read paths under lock. ✓ (dec'd in
|
|
321
|
-
federation architecture)
|
|
322
|
-
- MCP worker-per-call cheap: clean-state read path adds one small meta read. ✓
|
|
323
|
-
- Does not regress sprint-1 lock hardening: journal lives *inside* the
|
|
324
|
-
existing `mutate()` critical section; no new lock protocol introduced. ✓
|
|
325
|
-
|
|
326
|
-
## 10. Non-goals (this spec)
|
|
327
|
-
|
|
328
|
-
- Unifying per-loop journals (`loops/<id>/events.jsonl`) into the store
|
|
329
|
-
journal — convergence candidate after primary mode stabilizes.
|
|
330
|
-
- Federation merge semantics (consumes this journal; owned by federation
|
|
331
|
-
spec).
|
|
332
|
-
- Audit-log (`audit.ts`) consolidation.
|
|
333
|
-
- Query/index layer over the journal (projections are the query surface).
|
|
334
|
-
|
|
335
|
-
## 11. OPEN QUESTIONS (for cross-critique, Codex schema review, Juan)
|
|
336
|
-
|
|
337
|
-
- **Q1 (schema):** which `EventAction`s are exempt from carrying `payload`?
|
|
338
|
-
Proposal: payload required iff the action mutates a persisted entity;
|
|
339
|
-
lifecycle/notification actions stay payload-free. Codex: please attack
|
|
340
|
-
the action-union → payload-requirement mapping for holes.
|
|
341
|
-
- **Q2 (product/federation):** cross-store conflict policy when two stores
|
|
342
|
-
edited the same entity between pulls — entity-level LWW by pull order,
|
|
343
|
-
or surface a conflict artifact for the operator? Affects whether
|
|
344
|
-
`entity_rev` needs a vector component. **Juan's call.**
|
|
345
|
-
- **Q3 (schema/perf):** checkpoints self-contained (full post-images,
|
|
346
|
-
bigger but standalone) vs. referencing (hashes of projection files,
|
|
347
|
-
small but couples checkpoint validity to projection integrity)?
|
|
348
|
-
Proposal leans self-contained; needs a size estimate at 20-agent scale.
|
|
349
|
-
- **Q4 (scope):** do assignments/agent_runs/claims (currently written via
|
|
350
|
-
their own stores, not `State`) enter the journal in Phase 1 or in a
|
|
351
|
-
Phase 1.5? They are the highest-churn entities — biggest win, biggest
|
|
352
|
-
blast radius.
|
|
353
|
-
- **Q5 (ops):** exact threshold/policy for `gc` archiving of sealed
|
|
354
|
-
segments — count-based, age-based, or never until federation needs
|
|
355
|
-
defining?
|
|
356
|
-
|
|
357
|
-
## 12. Memory citations (per loop protocol)
|
|
358
|
-
|
|
359
|
-
Relied on: trp_d5595086 (silent-data-loss via load-swallow — drove §3 loud
|
|
360
|
-
failures, §6 tombstone deletion authority), feedback_lazy_reconcile_pattern
|
|
361
|
-
/ pln#496 (drove §6 read-path reconciliation, no daemon), trp_e85e9fbe
|
|
362
|
-
(dual-platform CI gates in §3/§7), trp_26e9634b (worktree-without-store
|
|
363
|
-
failure mode noted in §7), feedback_no_init_force / park-don't-delete house
|
|
364
|
-
rule (§5 retention, §8 rollback), architecture decisions: federation
|
|
365
|
-
Pull-and-Materialize + no-daemon (§4 federation hook, §9 compliance).
|