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,404 +0,0 @@
|
|
|
1
|
-
# Event-Log Store — Proposal B (slot B, round 1)
|
|
2
|
-
|
|
3
|
-
Status: ideation draft — loop lop_3bf55b9492e0d96c, pln_2290bc70 / pln#543 step 1.
|
|
4
|
-
Author: claude-code (slot B), independent round-1 proposal.
|
|
5
|
-
|
|
6
|
-
## Thesis
|
|
7
|
-
|
|
8
|
-
Evolve `src/core/event-log.ts` from a best-effort notification stream into the
|
|
9
|
-
**source of truth**, and re-cast the existing per-entity JSON files as a
|
|
10
|
-
**materialized projection** of that journal. The JSON files do not disappear —
|
|
11
|
-
they become a derived cache that is *also* the git-diffable, human-readable,
|
|
12
|
-
MCP-cheap representation. This single move satisfies the three hard constraints
|
|
13
|
-
that look contradictory at first glance (journal as truth / git-diffable store /
|
|
14
|
-
cheap worker-per-call reads), because the projection on disk *is* what today's
|
|
15
|
-
readers already consume.
|
|
16
|
-
|
|
17
|
-
Core design choices, argued below:
|
|
18
|
-
|
|
19
|
-
| Question | Choice | One-line rationale |
|
|
20
|
-
|---|---|---|
|
|
21
|
-
| Payload format | **Full entity snapshot per event** | Self-contained, gap-tolerant, zero-dep, trivial compaction |
|
|
22
|
-
| Ordering authority | **Global monotonic `seq`, assigned under the store lock** | Clock skew neutralized; journal order = mutation order |
|
|
23
|
-
| Append safety | O_APPEND single-write + newline framing + torn-tail tolerance | Atomic enough on NTFS+POSIX local FS; reader heals |
|
|
24
|
-
| Durability | One `fsync` per `mutate()` (not per event), configurable | Journal-first means tail loss is bounded and healable |
|
|
25
|
-
| Rotation | **Immutable segments + checkpoint records**, never rename-away | Rebuildability preserved; cursors survive rotation |
|
|
26
|
-
| Projections | Lazy reconciliation at read paths, `seq`-watermark dirty check | House pattern (pln#496); no daemon |
|
|
27
|
-
| Migration | Flag-gated dual-write, genesis-snapshot backfill, park-don't-delete | Rollback = flip the flag; JSON files are always valid |
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## 1. Event payload format
|
|
32
|
-
|
|
33
|
-
### 1.1 Decision: full entity snapshot per event
|
|
34
|
-
|
|
35
|
-
Each state-changing event carries the **complete post-mutation entity**, not a
|
|
36
|
-
diff. Record shape (v2; existing events are retroactively "v1"):
|
|
37
|
-
|
|
38
|
-
```jsonc
|
|
39
|
-
{
|
|
40
|
-
"v": 2,
|
|
41
|
-
"seq": 18342, // global monotonic, assigned under store lock
|
|
42
|
-
"ts": "2026-06-10T14:03:22.114Z", // informational only — never an ordering key
|
|
43
|
-
"writer": "claude-code", // agent name
|
|
44
|
-
"writer_id": "agt_...", // optional stable agent id
|
|
45
|
-
"pid": 31416, // diagnostic
|
|
46
|
-
"action": "update", // existing EventAction union
|
|
47
|
-
"item_type": "plan",
|
|
48
|
-
"item_id": "pln_2290bc70",
|
|
49
|
-
"summary": "step 1 started", // human-facing, optional
|
|
50
|
-
"payload": { /* full entity object, schema-valid */ },
|
|
51
|
-
"deleted": false // true => payload omitted, tombstone
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Deletes are tombstones (`deleted: true`, no payload). Non-entity events
|
|
56
|
-
(`session_start`, `run_*` notifications without state change) keep
|
|
57
|
-
`payload`-less form — they are observability, not state.
|
|
58
|
-
|
|
59
|
-
### 1.2 Why snapshots beat diffs here
|
|
60
|
-
|
|
61
|
-
Considered alternatives:
|
|
62
|
-
|
|
63
|
-
- **JSON-Patch (RFC 6902)** — smallest payloads, but: (a) zero-runtime-deps
|
|
64
|
-
constraint means hand-rolling a patch engine with its own bug surface;
|
|
65
|
-
(b) every event becomes **load-bearing**: one corrupt/torn/skipped line
|
|
66
|
-
poisons all subsequent state for that entity. Replay must be perfect or
|
|
67
|
-
fail loudly. That is the wrong failure mode for a file-based store that
|
|
68
|
-
agents, tests, and humans touch directly.
|
|
69
|
-
- **Field-delta (shallow `{changed_fields}`)** — cheaper than full snapshot,
|
|
70
|
-
no library needed, but same load-bearing problem for any field ever touched,
|
|
71
|
-
plus ambiguity between "field absent = unchanged" and "field removed".
|
|
72
|
-
- **Full snapshot** — every event is **idempotent and self-contained**.
|
|
73
|
-
Rebuilding an entity = take the latest event for that `item_id`. Rebuilding
|
|
74
|
-
the store = one backward scan keeping first-seen per id. A lost or corrupt
|
|
75
|
-
line costs *at most the window until the next write of that entity*, never
|
|
76
|
-
the entity's integrity. Compaction is trivial (§3). Federation merge is
|
|
77
|
-
trivial (§5).
|
|
78
|
-
|
|
79
|
-
### 1.3 Size math (is snapshot affordable?)
|
|
80
|
-
|
|
81
|
-
Entities today are ~1–4 KB pretty-printed; compact JSON in the journal ≈
|
|
82
|
-
0.5–2 KB. Current observed rate: ~17k events / ~2 months. Even if v2 events
|
|
83
|
-
average 2 KB, that is ~34 MB/2 months ≈ a 10 MB segment roll every ~2–3 weeks.
|
|
84
|
-
With checkpoint-based compaction (§3) the *live* journal stays bounded by
|
|
85
|
-
(entities × snapshot size) + recent tail. Verdict: snapshot cost is noise at
|
|
86
|
-
brainclaw's write rates; we are not building Kafka.
|
|
87
|
-
|
|
88
|
-
**Mitigation if an entity type grows large** (e.g. long plan bodies): per-type
|
|
89
|
-
opt-in to `payload_ref` pointing at the projection file + content hash — but
|
|
90
|
-
this is explicitly **deferred**; do not build it until a real entity exceeds
|
|
91
|
-
~64 KB. (OPEN QUESTION Q1.)
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
## 2. Append atomicity & durability
|
|
96
|
-
|
|
97
|
-
### 2.1 What we get from the OS
|
|
98
|
-
|
|
99
|
-
`fs.appendFileSync` opens `O_APPEND` (POSIX) / `FILE_APPEND_DATA` (Windows).
|
|
100
|
-
For **local** filesystems (NTFS, ext4, APFS), a single `write()` of one buffer
|
|
101
|
-
to an O_APPEND fd positions atomically; interleaving between processes
|
|
102
|
-
produces *concatenated* records, not *interleaved bytes*, for the record sizes
|
|
103
|
-
we write. Caveats we must encode in the spec, not just assume:
|
|
104
|
-
|
|
105
|
-
- Network filesystems (SMB shares, NFS) do **not** guarantee O_APPEND
|
|
106
|
-
atomicity. Spec stance: journal correctness is guaranteed on local FS only;
|
|
107
|
-
`bclaw doctor` warns when the store sits on a network mount.
|
|
108
|
-
- A crash mid-`write()` can leave a **torn tail** (partial line, no `\n`).
|
|
109
|
-
|
|
110
|
-
### 2.2 Framing: leading + trailing newline
|
|
111
|
-
|
|
112
|
-
Each record is written as **one buffer**: `"\n" + JSON + "\n"`.
|
|
113
|
-
|
|
114
|
-
The leading `\n` is the cheap, high-value trick: if the previous append tore
|
|
115
|
-
(no trailing newline), our leading newline *terminates the torn fragment as
|
|
116
|
-
its own malformed line* instead of letting our valid record be absorbed into
|
|
117
|
-
it. Damage from a torn write is thereby capped at **exactly one event** — the
|
|
118
|
-
torn one — never two. Readers already `split('\n').filter(Boolean)`, so empty
|
|
119
|
-
lines from double-`\n` are free.
|
|
120
|
-
|
|
121
|
-
Reader rules (normative):
|
|
122
|
-
1. Split on `\n`; skip empty lines.
|
|
123
|
-
2. A line that fails `JSON.parse` or schema validation: skip, count, surface
|
|
124
|
-
via `bclaw doctor` (never silently — extend the current swallow).
|
|
125
|
-
3. A torn **tail** (last line, no trailing newline in file) is *expected* after
|
|
126
|
-
a crash: skip without warning, but record `torn_tail: true` in doctor output.
|
|
127
|
-
|
|
128
|
-
### 2.3 Write path and the journal-first invariant
|
|
129
|
-
|
|
130
|
-
All state-mutating appends happen **inside `mutate()`** (they already do —
|
|
131
|
-
`appendEvent` is called from `persistStateUnlocked`). The store lock therefore
|
|
132
|
-
serializes mutating appenders; O_APPEND atomicity is the backstop for the
|
|
133
|
-
non-mutating observability events (session/run notifications) that may append
|
|
134
|
-
without the lock.
|
|
135
|
-
|
|
136
|
-
**Order inversion required.** Today `persistStateUnlocked` writes entity JSON
|
|
137
|
-
files first, then appends the event (state.ts:196→200). As source of truth the
|
|
138
|
-
journal must be written **first**:
|
|
139
|
-
|
|
140
|
-
```
|
|
141
|
-
mutate(): append v2 event(s) [+ fsync] → update projection files → bump watermark
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Crash between append and projection write → projection is *stale*, lazy
|
|
145
|
-
reconcile heals forward. The reverse order would allow a projection *from the
|
|
146
|
-
future* — an event-less state the journal can never explain, and which a
|
|
147
|
-
reconciler would wrongly **regress**. Journal-first is the single most
|
|
148
|
-
important invariant in this spec.
|
|
149
|
-
|
|
150
|
-
### 2.4 fsync policy
|
|
151
|
-
|
|
152
|
-
- Default: **one `fs.fsyncSync(fd)` per `mutate()` call**, after the last
|
|
153
|
-
append, before projection writes. Mutations are user-action-frequency, not
|
|
154
|
-
hot-loop; one fsync per mutation is affordable on NTFS.
|
|
155
|
-
- `appendEvent` outside a mutation (observability events): no fsync.
|
|
156
|
-
- Config escape hatch `store.journal.fsync: "mutation" | "never"` for
|
|
157
|
-
pathological filesystems. No `"always"` tier until someone demonstrates a
|
|
158
|
-
need. (OPEN QUESTION Q2: default for CI/test envs.)
|
|
159
|
-
- The current `try/catch` that **swallows append errors must go** for v2
|
|
160
|
-
state events: if the journal append fails inside a mutation, the mutation
|
|
161
|
-
fails. A source of truth that silently drops writes is not a source of truth.
|
|
162
|
-
Observability-only events may keep best-effort semantics.
|
|
163
|
-
|
|
164
|
-
---
|
|
165
|
-
|
|
166
|
-
## 3. Non-lossy rotation: segments + checkpoints
|
|
167
|
-
|
|
168
|
-
### 3.1 Layout
|
|
169
|
-
|
|
170
|
-
```
|
|
171
|
-
.brainclaw/events/
|
|
172
|
-
HEAD.json # { next_seq, active_segment } — written under store lock
|
|
173
|
-
seg-00000001.jsonl # immutable once rolled; name = first seq it contains
|
|
174
|
-
seg-00018000.jsonl # active segment (append target)
|
|
175
|
-
archive/
|
|
176
|
-
events.v1.jsonl # the parked legacy notification log (never deleted)
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
- Segment file name encodes the **first seq** in it → locating seq N is a
|
|
180
|
-
directory listing + binary search by name, no index file needed.
|
|
181
|
-
- Roll when active segment ≥ 10 MB: under the store lock, write a fresh
|
|
182
|
-
segment, update `HEAD.json`. **Never rename the active file** — readers
|
|
183
|
-
holding it open (Windows!) are unaffected; rolled segments are immutable.
|
|
184
|
-
- `HEAD.json` is small and rewritten atomically (temp+rename, as
|
|
185
|
-
`writeFileAtomic` already does). It is a cache: if missing/corrupt it is
|
|
186
|
-
rebuilt by listing segments and tail-reading the last one.
|
|
187
|
-
|
|
188
|
-
### 3.2 Checkpoints make old segments prunable
|
|
189
|
-
|
|
190
|
-
A **checkpoint** is a special record (or run of records) appended like any
|
|
191
|
-
event: `{v:2, seq, action:"checkpoint", item_type:"state", payload:{...full
|
|
192
|
-
snapshot refs...}}` — concretely, a checkpoint emits one snapshot event per
|
|
193
|
-
live entity plus a terminator record. After a checkpoint at seq C, every
|
|
194
|
-
segment whose *last* seq < first-snapshot-of-C is **redundant for state
|
|
195
|
-
rebuild** and moves to `archive/` (park, never delete — house rule).
|
|
196
|
-
|
|
197
|
-
Checkpoint triggers (lazy, no daemon): on segment roll, and on
|
|
198
|
-
`bclaw doctor --compact`. Cost: O(live entities), bounded, runs under the
|
|
199
|
-
store lock.
|
|
200
|
-
|
|
201
|
-
### 3.3 Cursors become seq watermarks
|
|
202
|
-
|
|
203
|
-
Replace `{offset, last_read}` byte cursors with `{last_seq, last_read}`.
|
|
204
|
-
This fixes the existing rotation bug class outright: rotation/compaction
|
|
205
|
-
**cannot invalidate a seq watermark**. `readUnseenEvents(agent)` = find
|
|
206
|
-
segment containing `last_seq+1` (filename binary search), stream forward.
|
|
207
|
-
If the watermark predates the oldest non-archived segment, the reader gets
|
|
208
|
-
`{gap: true}` and a summary built from the checkpoint instead of replaying
|
|
209
|
-
archaeology — notifications degrade gracefully, state rebuild does not need
|
|
210
|
-
them.
|
|
211
|
-
|
|
212
|
-
---
|
|
213
|
-
|
|
214
|
-
## 4. Projections: lazy materialization
|
|
215
|
-
|
|
216
|
-
### 4.1 What is a projection
|
|
217
|
-
|
|
218
|
-
Exactly the files we have today: `constraints/*.json`, `plans/*.json`, etc.,
|
|
219
|
-
written by `saveVersionedJsonFile` (atomic, pretty-printed, git-diffable).
|
|
220
|
-
Plus one new manifest:
|
|
221
|
-
|
|
222
|
-
```
|
|
223
|
-
.brainclaw/projections.json # { applied_seq: 18342 }
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
### 4.2 Dirty detection = watermark comparison (no dirty flags, no daemon)
|
|
227
|
-
|
|
228
|
-
Read path (`loadState`, single-entity gets):
|
|
229
|
-
|
|
230
|
-
1. Read `projections.json.applied_seq` and `HEAD.json.next_seq` — two tiny
|
|
231
|
-
file reads, this is the staleness check and it is O(1).
|
|
232
|
-
2. Equal (the overwhelmingly common case: same-process read-after-write, or
|
|
233
|
-
MCP worker spawned after a clean mutation) → serve projection files
|
|
234
|
-
directly. **This is why worker-per-call stays cheap**: the fresh-path adds
|
|
235
|
-
two small reads to today's behavior, nothing else.
|
|
236
|
-
3. Behind → acquire store lock, replay events `(applied_seq, head)` onto the
|
|
237
|
-
projection (apply snapshot / tombstone per record), bump `applied_seq`,
|
|
238
|
-
release. Replay cost is proportional to the *gap*, not the store.
|
|
239
|
-
4. Lock unavailable (reader racing a writer) → serve the stale projection
|
|
240
|
-
with a `stale: true` annotation rather than block reads. (OPEN QUESTION
|
|
241
|
-
Q3: is stale-read acceptable for claims? Probably not — claims may need
|
|
242
|
-
read-through-journal.)
|
|
243
|
-
|
|
244
|
-
This is precisely the pln#496 lazy-reconcile pattern: convergence at read
|
|
245
|
-
paths, no background process.
|
|
246
|
-
|
|
247
|
-
### 4.3 How `mutateState` / `persistState` migrate
|
|
248
|
-
|
|
249
|
-
End-state shape (phase 3, §6):
|
|
250
|
-
|
|
251
|
-
```ts
|
|
252
|
-
mutateState(fn):
|
|
253
|
-
mutate(lock):
|
|
254
|
-
state = loadState() // projection, reconciled if behind
|
|
255
|
-
result = fn(state)
|
|
256
|
-
events = diffToEvents(prev, state) // snapshot events for changed entities
|
|
257
|
-
appendEvents(events) + fsync
|
|
258
|
-
applyToProjection(events) // same writes syncDirectory does today
|
|
259
|
-
writeWatermark(applied_seq = last seq)
|
|
260
|
-
return result
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
`diffToEvents` compares by entity identity + shallow equality — note this
|
|
264
|
-
replaces today's "rewrite every file on every mutation" with **write only what
|
|
265
|
-
changed**, which is where the "single-entity ops not O(store)" perf target is
|
|
266
|
-
actually won. `syncDirectory`'s deleteMissing semantics map to tombstone
|
|
267
|
-
events; the trp_d5595086 guard (never unlink unparseable files) carries over
|
|
268
|
-
unchanged on the projection side.
|
|
269
|
-
|
|
270
|
-
The full-state-load in `mutateState` stays in phase 1–2 (it's correct, just
|
|
271
|
-
not optimal); per-entity loads land in phase 3 once registries
|
|
272
|
-
(assignments/runs/loops, which already do per-entity files) and State entities
|
|
273
|
-
share the journal apply path.
|
|
274
|
-
|
|
275
|
-
---
|
|
276
|
-
|
|
277
|
-
## 5. Causal ordering & multi-writer
|
|
278
|
-
|
|
279
|
-
### 5.1 Inside one store
|
|
280
|
-
|
|
281
|
-
- **`seq` is the only ordering authority.** Assigned under the store lock from
|
|
282
|
-
`HEAD.json.next_seq`. Since every state mutation already runs under that
|
|
283
|
-
lock (mutation-pipeline invariant), seq assignment adds zero new
|
|
284
|
-
coordination.
|
|
285
|
-
- `ts` is diagnostic. Clock skew, DST, multi-machine clones — none of it can
|
|
286
|
-
reorder state, because nothing orders by `ts`.
|
|
287
|
-
- Observability events appended outside the lock get `seq: null` and are
|
|
288
|
-
excluded from state rebuild; they order best-effort by file position. If
|
|
289
|
-
this proves annoying, they can cheaply take the lock — but do not let
|
|
290
|
-
notification traffic contend with mutations by default.
|
|
291
|
-
- Per-writer `(writer_id, writer_seq)` is **also** recorded (a tiny counter in
|
|
292
|
-
the agent's cursor file) — unused locally, load-bearing for federation.
|
|
293
|
-
|
|
294
|
-
### 5.2 Federation (Pull-and-Materialize consumes the journal)
|
|
295
|
-
|
|
296
|
-
The journal is the sync substrate the federation decisions assumed:
|
|
297
|
-
|
|
298
|
-
- A remote pulls segments (append-only files = rsync/git/dumb-bus friendly,
|
|
299
|
-
no daemon).
|
|
300
|
-
- Materialization replays *foreign* snapshot events into the local store as
|
|
301
|
-
**signaling entities** (candidates/handoffs) per the
|
|
302
|
-
cross-project-signaling decision — foreign events do not directly mutate
|
|
303
|
-
local execution entities.
|
|
304
|
-
- Idempotency key for merge = `(origin_store_id, seq)`; duplicate pulls are
|
|
305
|
-
no-ops. `origin_store_id` is stamped once per segment header record, not
|
|
306
|
-
per event, to save bytes.
|
|
307
|
-
- Conflict semantics (same entity edited in two stores): **out of scope
|
|
308
|
-
here**; the journal guarantees each side's history is complete and
|
|
309
|
-
replayable, which is the precondition. (OPEN QUESTION Q4 — Codex schema
|
|
310
|
-
review territory.)
|
|
311
|
-
|
|
312
|
-
### 5.3 Adversarial scenarios (pre-answered for the critique round)
|
|
313
|
-
|
|
314
|
-
| Attack | Outcome under this design |
|
|
315
|
-
|---|---|
|
|
316
|
-
| Crash mid-append | Torn tail; leading-`\n` framing caps loss at 1 event; reader skips; doctor reports. Mutation that crashed never confirmed → caller retries; projection never ahead of journal (journal-first). |
|
|
317
|
-
| Two mutating writers | Impossible by construction — store lock (hardened today, token-owned, refresh-padded) serializes them; seq assignment is inside the lock. |
|
|
318
|
-
| Stray appender without lock | O_APPEND keeps records intact; record has `seq:null` → cannot corrupt state order. |
|
|
319
|
-
| Rotation during read | Rolled segments are immutable; active segment never renamed; seq watermarks survive. The v1 bug (rename + cursor reset) is structurally removed. |
|
|
320
|
-
| Clock skew / ts collision | Irrelevant to state: seq orders. |
|
|
321
|
-
| 100k-event store | Reads never replay history: fresh-path is O(1) check + projection read; stale-path replays only the gap; rebuild-from-zero bounded by last checkpoint. |
|
|
322
|
-
| Corrupt line mid-segment | Snapshot self-containment: only that event lost; later snapshot of same entity supersedes. Doctor flags the segment. |
|
|
323
|
-
| `HEAD.json` corrupt/lost | Rebuilt from segment listing + tail read; it is a cache, not truth. |
|
|
324
|
-
| `projections.json` lost | Worst case: full replay from last checkpoint. Truth intact. |
|
|
325
|
-
|
|
326
|
-
---
|
|
327
|
-
|
|
328
|
-
## 6. Migration plan
|
|
329
|
-
|
|
330
|
-
Flag: `store.journal_v2: off | dual | primary` (config.yaml), default `off`.
|
|
331
|
-
|
|
332
|
-
**Phase 0 — ship the format, change nothing.** Land record schema (zod),
|
|
333
|
-
segment reader/writer, doctor checks. v1 `events.jsonl` untouched.
|
|
334
|
-
|
|
335
|
-
**Phase 1 — `dual`: journal-first dual-write.**
|
|
336
|
-
- `bclaw migrate journal` (one-shot, upgrade-style): backup store; emit
|
|
337
|
-
**genesis checkpoint** = one snapshot event per current entity built from
|
|
338
|
-
the projection files (the only truth we have — the 17k v1 events are not
|
|
339
|
-
reconstructible and are not translated, just parked to
|
|
340
|
-
`events/archive/events.v1.jsonl`, still readable for history/forensics);
|
|
341
|
-
initialize `HEAD.json`, `projections.json`.
|
|
342
|
-
- `persistStateUnlocked` reordered: append v2 events → existing
|
|
343
|
-
`writeStateDirectories` → watermark. Notifications switch to seq cursors.
|
|
344
|
-
- **Rollback:** set flag `off`, restore nothing — projection files were
|
|
345
|
-
written on every mutation and are exactly today's format. Park the
|
|
346
|
-
`events/` dir. Zero data loss in either direction. This must not regress
|
|
347
|
-
today's lock/mutation hardening: the only change inside the lock is
|
|
348
|
-
ordering + two small file writes.
|
|
349
|
-
|
|
350
|
-
**Phase 2 — `primary`: reads heal from journal.** Lazy reconcile on read
|
|
351
|
-
paths (§4.2). Acceptance gate: kill -9 storms in tests (crash between append
|
|
352
|
-
and projection) always converge.
|
|
353
|
-
|
|
354
|
-
**Phase 3 — per-entity ops.** Single-entity mutations append + patch one
|
|
355
|
-
projection file without full-store load/rewrite; registries unify on the same
|
|
356
|
-
path. Perf gates: `bclaw_work` cold read < 1 s on a 100k-event store;
|
|
357
|
-
single-entity op cost independent of store size.
|
|
358
|
-
|
|
359
|
-
Each phase ships dark behind the flag, soak-tested by dogfooding (this repo's
|
|
360
|
-
own store is the canary, ~17k events of realistic traffic).
|
|
361
|
-
|
|
362
|
-
---
|
|
363
|
-
|
|
364
|
-
## 7. Hard-constraint checklist
|
|
365
|
-
|
|
366
|
-
- **Zero new runtime deps**: snapshots need no diff lib; zod (already present)
|
|
367
|
-
validates records; segments are plain JSONL. ✔
|
|
368
|
-
- **Windows + POSIX**: O_APPEND/FILE_APPEND_DATA local-FS semantics; no rename
|
|
369
|
-
of open files; no fsync assumptions beyond Node built-ins; paths via
|
|
370
|
-
existing `memoryDir`. ✔
|
|
371
|
-
- **Git-diffable store identity**: projection files unchanged in format and
|
|
372
|
-
location; segments are append-only (clean diffs); archives parked. ✔
|
|
373
|
-
- **No daemon**: all convergence at read/write paths under the existing lock. ✔
|
|
374
|
-
- **Cheap MCP worker-per-call**: fresh-path = two extra tiny reads. ✔
|
|
375
|
-
|
|
376
|
-
## 8. Open questions (for Codex review / Juan product call)
|
|
377
|
-
|
|
378
|
-
- **Q1** Large-payload escape hatch (`payload_ref`): spec now or explicitly
|
|
379
|
-
defer? (I say defer; flagging because it changes the record schema if added.)
|
|
380
|
-
- **Q2** fsync default in tests/CI — `"never"` to keep suite fast, or same as
|
|
381
|
-
prod to keep fidelity? (Test-runner env contamination history says: same as
|
|
382
|
-
prod, measure first.)
|
|
383
|
-
- **Q3** Are stale projection reads acceptable when the lock is contended, or
|
|
384
|
-
must claims/locks-adjacent entities read through the journal? Product call:
|
|
385
|
-
consistency vs. never-blocking reads.
|
|
386
|
-
- **Q4** Federation conflict semantics (LWW per entity vs field merge vs
|
|
387
|
-
always-surface-as-candidate). Needs Codex's schema-review pass; journal
|
|
388
|
-
design above is agnostic.
|
|
389
|
-
- **Q5** Should journal segments be committed by `commitMemoryChange`
|
|
390
|
-
(memory-git) or gitignored within the store's internal repo? Append-only
|
|
391
|
-
files diff well, but double-storing 10 MB segments in git history may bloat;
|
|
392
|
-
checkpoint-only commits are a middle ground.
|
|
393
|
-
- **Q6** Observability events: keep them in the same journal (current
|
|
394
|
-
proposal) or split notification stream from state journal entirely? Same
|
|
395
|
-
file is simpler; split avoids notification traffic inflating segments.
|
|
396
|
-
|
|
397
|
-
## Memory ids relied upon
|
|
398
|
-
|
|
399
|
-
- feedback_lazy_reconcile_pattern (pln#496) — projection reconciliation shape (§4).
|
|
400
|
-
- trp_d5595086 — never-unlink-unparseable guard carried into projection apply (§4.3); load-swallow lesson behind §2.2 reader rules.
|
|
401
|
-
- federation_architecture_decisions / cross_project_signaling_vs_execution — no daemon, Pull-and-Materialize, signaling-only foreign writes (§5.2).
|
|
402
|
-
- trp_e85e9fbe — Windows/POSIX divergence discipline (§2.1, §7); CI-on-both-platforms gate for phases.
|
|
403
|
-
- trp_09988deb — config/backup hygiene → upgrade-style backup + park-don't-delete in migration (§6).
|
|
404
|
-
- feedback_bisect_state_before_code — motivates doctor-visible counters over silent skips (§2.2).
|