instar 1.3.569 → 1.3.571
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/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +127 -53
- package/dist/commands/server.js.map +1 -1
- package/dist/core/CoherenceJournal.d.ts +46 -0
- package/dist/core/CoherenceJournal.d.ts.map +1 -1
- package/dist/core/CoherenceJournal.js +94 -2
- package/dist/core/CoherenceJournal.js.map +1 -1
- package/dist/core/JournalSyncApplier.d.ts +22 -0
- package/dist/core/JournalSyncApplier.d.ts.map +1 -1
- package/dist/core/JournalSyncApplier.js +39 -2
- package/dist/core/JournalSyncApplier.js.map +1 -1
- package/dist/core/MachinePoolRegistry.d.ts.map +1 -1
- package/dist/core/MachinePoolRegistry.js +26 -4
- package/dist/core/MachinePoolRegistry.js.map +1 -1
- package/dist/core/PeerPresencePuller.d.ts +39 -0
- package/dist/core/PeerPresencePuller.d.ts.map +1 -1
- package/dist/core/PeerPresencePuller.js +46 -1
- package/dist/core/PeerPresencePuller.js.map +1 -1
- package/dist/core/ReplicatedPeerStreamReader.d.ts +97 -0
- package/dist/core/ReplicatedPeerStreamReader.d.ts.map +1 -0
- package/dist/core/ReplicatedPeerStreamReader.js +274 -0
- package/dist/core/ReplicatedPeerStreamReader.js.map +1 -0
- package/dist/core/ReplicatedRecordEmitter.d.ts +102 -0
- package/dist/core/ReplicatedRecordEmitter.d.ts.map +1 -0
- package/dist/core/ReplicatedRecordEmitter.js +122 -0
- package/dist/core/ReplicatedRecordEmitter.js.map +1 -0
- package/dist/core/ws2SendWiring.d.ts +52 -0
- package/dist/core/ws2SendWiring.d.ts.map +1 -0
- package/dist/core/ws2SendWiring.js +71 -0
- package/dist/core/ws2SendWiring.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.570.md +69 -0
- package/upgrades/1.3.571.md +64 -0
- package/upgrades/side-effects/statesync-peer-advert-propagation-fix.md +125 -0
- package/upgrades/side-effects/ws2-send-side-emission.md +40 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws2SendWiring.js","sourceRoot":"","sources":["../../src/core/ws2SendWiring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;+CAE+C;AAC/C,MAAM,CAAC,MAAM,qBAAqB,GAA0B,MAAM,CAAC,MAAM,CAAC;IACxE,WAAW;CACZ,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAA0B,MAAM,CAAC,MAAM,CAAC;IAC1E,eAAe;IACf,WAAW;IACX,kBAAkB;IAClB,cAAc;IACd,eAAe;IACf,aAAa;CACd,CAAC,CAAC;AAKH,wDAAwD;AACxD,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAI,qBAAqB,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IAC1D,IAAI,uBAAuB,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC9D,OAAO,cAAc,CAAC;AACxB,CAAC;AAWD;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,gBAAuC;IACxE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,KAAK,MAAM,KAAK,IAAI,gBAAgB,EAAE,CAAC;QACrC,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,MAAM,KAAK,OAAO;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;aACrC,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;;YAC9C,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;AACzE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-15T09:28:36.949Z",
|
|
5
|
+
"instarVersion": "1.3.571",
|
|
6
6
|
"entryCount": 201,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
When an agent runs on more than one machine, each machine advertises which kinds of
|
|
9
|
+
records it can durably RECEIVE (its `stateSyncReceive` capability, carried inside
|
|
10
|
+
`seamlessnessFlags` on the machine-pool heartbeat). Before a machine replicates a record
|
|
11
|
+
to a peer, the flag-coherence gate checks that the peer advertises it can receive that
|
|
12
|
+
kind — otherwise it withholds, because the journal applier silently drops unknown kinds.
|
|
13
|
+
|
|
14
|
+
That peer advert was being DROPPED on the receive side, so every machine saw its peers as
|
|
15
|
+
having zero receive capability and the gate blocked replication in both directions.
|
|
16
|
+
Root-caused live on a Laptop↔Mac-Mini pair (2026-06-14): each machine's `GET /pool` showed
|
|
17
|
+
itself with its full receive-key set but every peer with 0.
|
|
18
|
+
|
|
19
|
+
The fix is three receive-side changes, plus a regression guard:
|
|
20
|
+
|
|
21
|
+
- **`src/core/PeerPresencePuller.ts`** — extracted the peer `session-status` narrowing into a
|
|
22
|
+
single exported pure helper, `narrowSessionStatusToPeerCapacity`, that now passes
|
|
23
|
+
`seamlessnessFlags` through (it had been dropped, exactly like commitmentsAdvert in #930,
|
|
24
|
+
quotaState in A2/#804, and preferencesAdvert in WS2.1 — this is the fourth instance of the
|
|
25
|
+
same "narrowing return forgets a field" class). Added the field to the `PeerCapacity`
|
|
26
|
+
interface and the `recordHeartbeat` dependency signature, and forwarded it in `pullOnce`.
|
|
27
|
+
Added an exported `SESSION_STATUS_ADVERT_FIELDS` registry that drives the regression test.
|
|
28
|
+
- **`src/commands/server.ts`** — `fetchPeerCapacity` now delegates to that shared helper
|
|
29
|
+
(keeping only the closure-bound journal-advert unwrap), so production and the round-trip
|
|
30
|
+
integration test run ONE mapping instead of a hand-copied mirror.
|
|
31
|
+
- **`src/core/MachinePoolRegistry.ts`** — `recordHeartbeat` now carries a peer's
|
|
32
|
+
`seamlessnessFlags` forward when a beat omits it (the same pattern guardPosture already
|
|
33
|
+
uses), so the 30-second sparse liveness heartbeat can no longer wipe an HTTP-pulled advert.
|
|
34
|
+
A genuine withdrawal (a present object with a flag flipped) still propagates.
|
|
35
|
+
|
|
36
|
+
The sender side already emitted the advert (via `getCapacity(self)`), so no sender change was
|
|
37
|
+
needed. The whole path ships behind the already-merged WS2 stateSync substrate; a
|
|
38
|
+
single-machine install is a strict no-op.
|
|
39
|
+
|
|
40
|
+
## What to Tell Your User
|
|
41
|
+
|
|
42
|
+
If you run your agent on more than one computer — say a laptop and a desktop — it is supposed
|
|
43
|
+
to share what it learns between them. That sharing was silently blocked: each machine could
|
|
44
|
+
not see that the other was ready to receive, so neither ever sent. This release fixes that.
|
|
45
|
+
Each machine now correctly sees its peers' readiness, and the pool view shows real peer
|
|
46
|
+
capability instead of a blank. If you only run on one machine, nothing changes for you. After
|
|
47
|
+
updating on both machines, you can confirm it works by saving a learning on one machine and
|
|
48
|
+
reading it on the other.
|
|
49
|
+
|
|
50
|
+
## Summary of New Capabilities
|
|
51
|
+
|
|
52
|
+
No new user-facing capability or configuration — this is a correctness fix to an existing,
|
|
53
|
+
default-off multi-machine feature. It removes a silent block so cross-machine memory
|
|
54
|
+
replication can actually take effect once the feature is enabled. The pool view now shows each
|
|
55
|
+
peer's real receive-capability set instead of zero.
|
|
56
|
+
|
|
57
|
+
## Evidence
|
|
58
|
+
|
|
59
|
+
- Unit: `tests/unit/peer-presence-puller.test.ts` — the shared helper preserves every advert
|
|
60
|
+
field in `SESSION_STATUS_ADVERT_FIELDS` (a forgotten field fails loudly); presence-guard,
|
|
61
|
+
not truthiness, so an all-disabled object survives; the puller forwards `seamlessnessFlags`.
|
|
62
|
+
- Unit: `tests/unit/MachinePoolRegistry.test.ts` — a sparse liveness beat does not wipe a
|
|
63
|
+
pulled `seamlessnessFlags`; a genuine withdrawal still propagates; the carry-forward is
|
|
64
|
+
scoped to `seamlessnessFlags` (quotaState still clears) and is per-peer (correct for N≥1).
|
|
65
|
+
- Integration: `tests/integration/peer-presence-roundtrip.test.ts` — over a real signed
|
|
66
|
+
`/mesh/rpc` round-trip using the SHARED production mapping, a peer's advertised
|
|
67
|
+
`stateSyncReceive` lands in the puller's pool registry and SURVIVES a subsequent sparse beat.
|
|
68
|
+
- Spec + convergence: `docs/specs/STATESYNC-PEER-ADVERT-PROPAGATION-FIX-SPEC.md`
|
|
69
|
+
(review-convergence, approved; codex-cli gpt-5.5 + gemini-cli gemini-2.5-pro external passes).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: minor -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The send half of multi-machine memory replication is now wired. The WS2 substrate
|
|
9
|
+
shipped the replicated-kind registry, the receive/apply machinery, and the
|
|
10
|
+
capability advert, but the journal-backed emitter that the per-store managers' write
|
|
11
|
+
hooks call was never constructed — so memory records were never written to the
|
|
12
|
+
coherence-journal own-streams, and a peer had nothing to pull (root-caused live on a
|
|
13
|
+
Laptop↔Mac-Mini pair).
|
|
14
|
+
|
|
15
|
+
This change adds the generic, store-agnostic `ReplicatedRecordEmitter` (HLC tick +
|
|
16
|
+
last-writer-witness + the store's record builder + journal append, behind the
|
|
17
|
+
per-store dark gate), closes the three matching gaps so a record crosses end-to-end —
|
|
18
|
+
the journal can now append/validate a `*-record` kind, the applier accepts a peer's
|
|
19
|
+
`*-record` instead of suspect-flagging the stream, and the union read materializes
|
|
20
|
+
own + peer journal streams (so a received record is readable) — and replaces the
|
|
21
|
+
snapshot-serve `loadOwnEntries` stub with a real own-stream loader. The first kind,
|
|
22
|
+
`learnings`, is wired end-to-end; the remaining memory stores follow on the same
|
|
23
|
+
machinery (tracked in the send-wiring manifest). A wiring-integrity ratchet now fails
|
|
24
|
+
CI if a future replicated kind is added receive-only with no send path.
|
|
25
|
+
|
|
26
|
+
Everything is gated behind the per-store multi-machine memory-sync switch (off by
|
|
27
|
+
default; the development-agent gate flips it live on a dev agent only). Records ride
|
|
28
|
+
the existing journal-sync tail and pull — no new HTTP route or mesh verb.
|
|
29
|
+
|
|
30
|
+
## What to Tell Your User
|
|
31
|
+
|
|
32
|
+
- **Cross-machine memory (lessons) now actually crosses**: "If you run me on more than
|
|
33
|
+
one machine, a lesson I learn on one is now known on the others — one shared learning
|
|
34
|
+
registry instead of one per machine. A copy from another machine is always treated as
|
|
35
|
+
a hint, never as the boss: it never silently overwrites what this machine already
|
|
36
|
+
believes, and if two machines disagree I surface both and flag it for you. It stays
|
|
37
|
+
off until you ask me to turn on multi-machine memory sync."
|
|
38
|
+
|
|
39
|
+
## Summary of New Capabilities
|
|
40
|
+
|
|
41
|
+
| Capability | How to Use |
|
|
42
|
+
|-----------|-----------|
|
|
43
|
+
| Cross-machine replication of learned lessons (learnings) | Automatic once multi-machine memory sync is enabled for the store (off by default) |
|
|
44
|
+
| A received peer memory is readable through the no-clobber union read | Automatic (read path) |
|
|
45
|
+
| Snapshot serve returns real own-stream entries | Automatic (bootstrap path) |
|
|
46
|
+
| Wiring-integrity ratchet — no new kind can ship receive-only | CI test (automatic) |
|
|
47
|
+
|
|
48
|
+
## Evidence
|
|
49
|
+
|
|
50
|
+
The live gap: after the receive-advert fix (#1167) deployed to both machines, a
|
|
51
|
+
learning written on the Laptop never appeared on the Mac-Mini after 5+ minutes; the
|
|
52
|
+
Laptop's coherence-journal meta listed only the 5 lifecycle kinds — none of the 7 WS2
|
|
53
|
+
record kinds — proving no record was ever emitted to an own-stream.
|
|
54
|
+
|
|
55
|
+
Verified by a two-instance in-process E2E
|
|
56
|
+
(tests/e2e/ws2-learnings-cross-instance.test.ts): a learning written on instance A,
|
|
57
|
+
shipped over the real journal serve/apply path, is read back on instance B through the
|
|
58
|
+
bypass-proof union reader as a foreign-origin record; a markApplied edit replicates and
|
|
59
|
+
the latest wins; the same lesson learned on both machines collapses to one record key
|
|
60
|
+
across origins. Before this change B's union read returned null (the reproduced bug);
|
|
61
|
+
after, it returns A's record. Plus integration (serve→apply→read, forged-batch
|
|
62
|
+
rejection, snapshot serve returns entries) and unit coverage (emitter dark-gate /
|
|
63
|
+
witness-order / throw-isolation, journal record append + op-key dedup + 80 KB cap, peer
|
|
64
|
+
stream materialization). 32 new tests, all green.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Side-Effects Review — Fix: peer stateSync receive-advert dropped → cross-machine memory replication never crosses
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `statesync-peer-advert-propagation-fix`
|
|
4
|
+
**Date:** `2026-06-15`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `echo (independent reviewer subagent — Phase 5; touches a coherence gate's input)`
|
|
7
|
+
**Spec:** `docs/specs/STATESYNC-PEER-ADVERT-PROPAGATION-FIX-SPEC.md` (review-convergence + approved; codex/gemini external passes)
|
|
8
|
+
**Parent principle:** Cross-Machine Coherence — One Agent, Robust Under Degraded Conditions
|
|
9
|
+
|
|
10
|
+
## Summary of the change
|
|
11
|
+
|
|
12
|
+
A peer's `seamlessnessFlags` advert (the carrier of `stateSyncReceive` — which replicated
|
|
13
|
+
kinds a machine can durably receive) was emitted by the sender but DROPPED on the receive
|
|
14
|
+
side, so the flag-coherence gate read every peer as "cannot receive" and blocked
|
|
15
|
+
cross-machine replication in both directions (root-caused live Laptop↔Mini, 2026-06-14).
|
|
16
|
+
Three receive-side files: `src/core/PeerPresencePuller.ts` (new exported pure helper
|
|
17
|
+
`narrowSessionStatusToPeerCapacity` + `PeerCapacity` field + `recordHeartbeat` dep +
|
|
18
|
+
`pullOnce` forward + `SESSION_STATUS_ADVERT_FIELDS` registry), `src/commands/server.ts`
|
|
19
|
+
(`fetchPeerCapacity` delegates to the shared helper), `src/core/MachinePoolRegistry.ts`
|
|
20
|
+
(`recordHeartbeat` carries `seamlessnessFlags` forward on a beat that omits it). No new
|
|
21
|
+
decision point — this FEEDS the existing `checkPoolFlagCoherence` / `shouldEmitToPeer` gate
|
|
22
|
+
with the data it was missing.
|
|
23
|
+
|
|
24
|
+
## Decision-point inventory
|
|
25
|
+
|
|
26
|
+
- `checkPoolFlagCoherence` / `shouldEmitToPeer` (`src/core/ReplicatedRecordEnvelope.ts`) — **pass-through (feeds, does not modify)** — the gate already exists and already reads `peer.stateSyncReceive`; this change makes that field actually arrive. No gate logic changed.
|
|
27
|
+
- `MachinePoolRegistry.recordHeartbeat` carry-forward — **modify (data retention)** — carries the last pulled `seamlessnessFlags` across a beat that omits it; not a block/allow decision, a freshness-of-observation rule mirroring the existing `guardPosture` carry-forward.
|
|
28
|
+
- `narrowSessionStatusToPeerCapacity` — **add (pure data-narrowing helper, no decision logic)** — extracted from the inline `fetchPeerCapacity` so production + tests share one mapping.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. Over-block
|
|
33
|
+
|
|
34
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
35
|
+
|
|
36
|
+
No block/allow surface is added. The change REMOVES an unintended over-block: before it, a
|
|
37
|
+
peer that was genuinely receive-capable was treated as not, so legitimate replication was
|
|
38
|
+
withheld. The carry-forward never fabricates capability where none was pulled (the "no prior
|
|
39
|
+
pull → sparse beat does not fabricate" unit test pins this), so it cannot over-advertise.
|
|
40
|
+
|
|
41
|
+
## 2. Under-block
|
|
42
|
+
|
|
43
|
+
**What failure modes does this still miss?**
|
|
44
|
+
|
|
45
|
+
`quotaState` and `inboundQueue` ride the same light-beat clobber and are NOT carry-forwarded
|
|
46
|
+
here — but both are fail-open (absent = not-blocked / depth-unknown), so a transient wipe is
|
|
47
|
+
benign (bounded to ~one 30s beat, self-correcting on the next rich pull). Scoping the
|
|
48
|
+
carry-forward to the fail-CLOSED `seamlessnessFlags` is deliberate; the fail-open siblings are
|
|
49
|
+
tracked (`<!-- tracked: CMT-statesync-lightbeat-carryforward -->`). A genuinely offline peer
|
|
50
|
+
still ages out of `online` via `routerReceivedAtMs`, so a stale carried advert is never acted
|
|
51
|
+
on as a live one.
|
|
52
|
+
|
|
53
|
+
## 3. Level-of-abstraction fit
|
|
54
|
+
|
|
55
|
+
**Is this at the right layer?**
|
|
56
|
+
|
|
57
|
+
Yes. The narrowing belongs in the receive mapping (`PeerPresencePuller`), the carry-forward
|
|
58
|
+
belongs in the registry that owns the per-machine observation (`MachinePoolRegistry`, beside
|
|
59
|
+
the identical `guardPosture` carry-forward), and the gate that consumes the advert
|
|
60
|
+
(`ReplicatedRecordEnvelope`) is untouched. Extracting the inline narrowing into a shared pure
|
|
61
|
+
helper raises a hard-to-test closure into a unit-testable function — the correct layer for the
|
|
62
|
+
regression guard the recurring bug class demands.
|
|
63
|
+
|
|
64
|
+
## 4. Signal vs authority compliance
|
|
65
|
+
|
|
66
|
+
**Does this hold blocking authority with brittle logic, or feed a smart gate?**
|
|
67
|
+
|
|
68
|
+
It FEEDS. The change adds no blocking authority. The flag-coherence gate
|
|
69
|
+
(`checkPoolFlagCoherence` / `shouldEmitToPeer`) is the existing authority; this change only
|
|
70
|
+
delivers the peer-advert signal it was already designed to read. Per
|
|
71
|
+
`docs/signal-vs-authority.md`, this is a signal-delivery fix, not a new gate. The helper is a
|
|
72
|
+
pure data map; the carry-forward is a freshness rule, not a decision.
|
|
73
|
+
|
|
74
|
+
## 5. Interactions
|
|
75
|
+
|
|
76
|
+
**Does it shadow another check, get shadowed, double-fire, race?**
|
|
77
|
+
|
|
78
|
+
Two writers race on each peer's registry entry every 30s: the rich `PeerPresencePuller.pullOnce`
|
|
79
|
+
and the sparse `refreshPool` liveness echo. Before the fix the sparse beat clobbered the rich
|
|
80
|
+
advert; the carry-forward resolves that race deterministically (omission → keep prior). It does
|
|
81
|
+
NOT shadow `guardPosture` (separate field, separate carry-forward) or `quotaState` (intentionally
|
|
82
|
+
still clears). The shared helper means `server.ts:fetchPeerCapacity` and the round-trip test can
|
|
83
|
+
never drift apart (the gap that let this bug hide from the existing test).
|
|
84
|
+
|
|
85
|
+
## 6. External surfaces
|
|
86
|
+
|
|
87
|
+
**Does it change anything visible to other agents/users/systems?**
|
|
88
|
+
|
|
89
|
+
`GET /pool` now shows each peer's real `seamlessnessFlags.stateSyncReceive` instead of an empty
|
|
90
|
+
0 — a more-correct read, no schema change (the field was already in the `MachineCapacity` type
|
|
91
|
+
and the assemble output; it was simply always undefined for peers). No new route, no new MeshRpc
|
|
92
|
+
verb. The peer advert is fixed-size booleans from an Ed25519-authenticated registered peer,
|
|
93
|
+
stored as-is exactly like its `quotaState`/`guardPosture` siblings — no new untrusted surface.
|
|
94
|
+
Old peers that never advertise are treated as non-participants (the conservative side), unchanged.
|
|
95
|
+
|
|
96
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
97
|
+
|
|
98
|
+
This change IS the cross-machine path. Posture: **replicated-capability advert** — the peer's
|
|
99
|
+
receive capability crosses via the existing signed `session-status` mesh pull and is recorded
|
|
100
|
+
per-peer in `MachinePoolRegistry`. Correct for N ≥ 1 peers: `checkPoolFlagCoherence` iterates all
|
|
101
|
+
online peers with no pairwise/"exactly 2" assumption; each peer is an independent registry Map
|
|
102
|
+
entry (pinned by the per-peer carry-forward unit test). No LAN assumption, no broadcast. No
|
|
103
|
+
user-facing notice, no durable-state-on-transfer, no generated URL involved.
|
|
104
|
+
|
|
105
|
+
## 8. Rollback cost
|
|
106
|
+
|
|
107
|
+
Low. Pure runtime code fix in three files, no config/schema/migration. Back-out is a plain
|
|
108
|
+
revert of the commit — the prior behavior (peers read as non-receiving, replication withheld)
|
|
109
|
+
is the safe-but-broken state this fixes, so reverting cannot cause data loss, only re-disable
|
|
110
|
+
the (default-off) replication path. No data migration, no agent-state repair.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Second-pass review
|
|
115
|
+
|
|
116
|
+
**Concur with the review.** Independent reviewer audited the artifact against the actual
|
|
117
|
+
`git diff src/ tests/` and verified: (1) no new gate/block logic — `ReplicatedRecordEnvelope.ts`
|
|
118
|
+
untouched, the change only feeds the existing flag-coherence gate; (2) the carry-forward in
|
|
119
|
+
`MachinePoolRegistry.recordHeartbeat` is correctly scoped to `seamlessnessFlags` only and does
|
|
120
|
+
not alter `quotaState`/`guardPosture`/`inboundQueue` handling (`quotaState` still clears on a
|
|
121
|
+
sparse beat); (3) the shared helper uses `!== undefined` presence guards so an all-disabled
|
|
122
|
+
`{ stateSyncReceive: {} }` survives, and preserves all six advert fields with the same shape as
|
|
123
|
+
the old inline mapping (none dropped or added); (4) `server.ts` preserves the `journalAdvert`
|
|
124
|
+
unwrap (computed locally with `machineId` context, then passed to the helper and re-emitted
|
|
125
|
+
unconditionally, identical to the old inline return). No missed side effect identified.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Side-Effects Review — WS2 send-side emission (wire the journal-backed replicated-record emitter)
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/specs/WS2-SEND-SIDE-EMISSION-SPEC.md (converged + approved — operator pre-approval, Justin topic 13481, the multi-machine memory-replication headline). **Parent:** Cross-Machine Coherence — One Agent, Robust Under Degraded Conditions.
|
|
4
|
+
**Ships DARK** behind `multiMachine.stateSync.<store>.enabled` (default false; the dev-agent gate flips it live on a dev agent only). Single-machine installs, and any agent with the stores off, are a strict no-op.
|
|
5
|
+
**Files:** src/core/ReplicatedRecordEmitter.ts (new), src/core/ReplicatedPeerStreamReader.ts (new), src/core/ws2SendWiring.ts (new), src/core/CoherenceJournal.ts, src/core/JournalSyncApplier.ts, src/commands/server.ts.
|
|
6
|
+
|
|
7
|
+
## What changed
|
|
8
|
+
|
|
9
|
+
1. **ReplicatedRecordEmitter.ts (new):** the generic, store-agnostic, journal-backed emitter the per-store managers' existing `emitPut`/`emitDelete` hooks call. One `emit(store, recordKey, build)` path: dark gate → degenerate-key guard → `observed` witness → HLC tick → store builder → `journal.emitReplicatedRecord`. Never throws into the manager (a builder/journal fault is a counted no-op). Single-origin by construction (stamps `origin = this machine`).
|
|
10
|
+
2. **ReplicatedPeerStreamReader.ts (new):** materializes the union's per-origin records from the OWN journal stream + every peer replica stream (`peers/<M>.<kind>.jsonl`, quarantine/meta excluded), validated through `validateReplicatedEnvelope` + the store schema, folded to the latest per (origin, recordKey) by HLC. Supplies three seams: `loadOriginRecords`/`listRecordKeys` (the union reader), `loadWitness` (the emitter's `observed` source), and `loadOwnEntries` (the snapshot-serve source — replaces the `() => ({})` stub).
|
|
11
|
+
3. **ws2SendWiring.ts (new):** the send-wiring manifest (WIRED vs PENDING stores) + the `auditWs2SendWiring` ratchet — every registered replicated kind must be consciously classified, so a future kind cannot be added receive-only with a silent no-op SEND half.
|
|
12
|
+
4. **CoherenceJournal.ts:** optional `setReplicatedKindRegistry`; new public `emitReplicatedRecord(kind, data)` (validates a registered `*-record` kind through the generic envelope validator, op-key = `recordKey:hlcKey`); the `validate()` switch gains ONE branch delegating a registered replicated kind to `validateReplicatedEnvelope`; the per-entry byte cap is per-kind (80 KB for `*-record` kinds, the 8 KB lifecycle cap unchanged). Absent registry ⇒ byte-identical prior behavior.
|
|
13
|
+
5. **JournalSyncApplier.ts:** optional `setReplicatedKindRegistry` (+ config option); `validateData()` delegates a registered replicated kind to `validateReplicatedEnvelope`; the per-entry size cap is per-kind (matching the writer). Without the registry a peer's `*-record` would `invalid`-flag the stream — the receive-only gap; with it, the record applies on the existing tail transport.
|
|
14
|
+
6. **server.ts:** constructs (when the coherence journal is live) the peer-stream reader + a persisted HLC clock + the generic emitter; injects the now-populated registry into BOTH the journal writer and the applier; replaces the `loadOwnEntries` stub with the reader; switches the `learnings` union reader to read own + peer journal streams; attaches the learnings emitter adapter to `EvolutionManager.setLearningReplicationEmitter`.
|
|
15
|
+
|
|
16
|
+
## Blast radius
|
|
17
|
+
|
|
18
|
+
- **Config-gated, not wiring-gated.** With `multiMachine.stateSync.learnings.enabled` false (the fleet default), the emitter is a strict no-op (the dark gate returns before any tick/append), so no `*-record` stream is ever written and the union read is byte-identical to today. The seams are always constructed (so the feature turns on without a restart-to-rewire) but do nothing until the store is enabled.
|
|
19
|
+
- **No new HTTP route, no new MeshRpc verb.** Records ride the EXISTING `journal-sync` tail (`buildServeBatch` serve + `apply` receive) and the EXISTING receiver-driven `PeerPresencePuller.driveJournalDelta` (which already pulls every advertised kind). The `state-snapshot` serve verb (already wired) now returns real entries via the real `loadOwnEntries`.
|
|
20
|
+
- **Single-origin + first-hop binding unchanged.** The emitter stamps `origin = this machine`; the applier's first-hop binding (`entry.machine === sender`) still rejects a forged cross-origin record. A peer's record lands only in its own `peers/<M>.<kind>.jsonl` namespace; the union read keeps origins separate and never writes a foreign record back into the local manager store (read-only union, no origin laundering).
|
|
21
|
+
- **No-clobber read.** A received record is read through the existing `ReplicatedStoreReader` + `UnionReader` (append-both-and-flag for high-impact); a replicated record never clobbers a divergent local one. The conflict ledger + dropped-origin exclusion are untouched.
|
|
22
|
+
|
|
23
|
+
## Risk + mitigation
|
|
24
|
+
|
|
25
|
+
- **Risk:** a replication emit fault breaks a local memory write. **Mitigation:** the emitter catches every builder/journal throw (counted in stats), and the managers' hooks were already try-wrapped — the durable local write is persisted before the emit hook runs. Proven by the "catches a builder/journal throw" unit tests.
|
|
26
|
+
- **Risk:** a wrong `observed` witness marks a genuinely-concurrent pair as sequential (a silent clobber). **Mitigation:** the witness is the MAX HLC over records PROVABLY on disk, read BEFORE the tick — it can only ever under-witness (a not-yet-pulled peer version is absent ⇒ the pair flags concurrent, the err-toward-flag safe direction). Proven by the witness-order unit test.
|
|
27
|
+
- **Risk:** a fat-but-legal learning is dropped as oversize. **Mitigation:** the per-entry byte cap is raised to 80 KB for `*-record` kinds on BOTH the writer and the applier (the store builders already cap `data` at 64 KB), so a record the writer emits is never rejected on receive. Proven by the 20 KB-description journal test.
|
|
28
|
+
- **Risk:** a future kind is added receive-only again (the exact original gap). **Mitigation:** the wiring-integrity ratchet (`auditWs2SendWiring`) fails CI if any registered store is neither send-wired nor explicitly send-pending.
|
|
29
|
+
|
|
30
|
+
## Migration parity
|
|
31
|
+
|
|
32
|
+
- No agent-installed files change. The feature is server-internal and activates on the next restart of an agent whose `multiMachine.stateSync.<store>` is enabled (the dev-agent gate decides for a dev agent). No `migrateConfig` / `migrateClaudeMd` / `migrateHooks` change is required for the dark slice; CLAUDE.md awareness lands when a kind is flipped on for the fleet (a later rollout step).
|
|
33
|
+
|
|
34
|
+
## Dark-gate line-map
|
|
35
|
+
|
|
36
|
+
- UNCHANGED. This PR adds no new `enabled:` line to `ConfigDefaults.ts` — the per-store stateSync flags already exist there (omitted `enabled`, resolved by the dev-agent gate, shipped in the WS2.x substrate PRs). The emitter reads the SAME resolved `_stateSyncStoresResolved` map. `node scripts/lint-dev-agent-dark-gate.js` stays clean.
|
|
37
|
+
|
|
38
|
+
## Rollback
|
|
39
|
+
|
|
40
|
+
- Revert the PR (or set `multiMachine.stateSync.learnings.enabled: false`). The emitter goes dark, no `*-record` streams are written, the union read returns to own-only/no-op, and any already-replicated peer replica files age out under the journal's per-kind retention. No durable migration to unwind.
|