instar 1.3.570 → 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 +116 -28
- 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/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.571.md +64 -0
- package/upgrades/side-effects/ws2-send-side-emission.md +40 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReplicatedRecordEmitter — the GENERIC journal-backed SEND emitter (WS2 send-side).
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/WS2-SEND-SIDE-EMISSION-SPEC.md §4; the substrate it rides is
|
|
5
|
+
* docs/specs/multi-machine-replicated-store-foundation.md §4 (the envelope +
|
|
6
|
+
* flag-gated emission) and §7.2 (the `observed` last-writer-witness).
|
|
7
|
+
*
|
|
8
|
+
* THE GAP THIS CLOSES: every memory manager already calls an internal
|
|
9
|
+
* `emitter.emitPut(record)` / `emitter.emitDelete(...)` hook on each write, behind a
|
|
10
|
+
* `*ReplicationEmitter | null` seam — but server.ts never constructed the concrete
|
|
11
|
+
* emitter those hooks call (it was deferred to "a later rollout stage" that never
|
|
12
|
+
* came), so the hooks fired into a no-op and nothing reached the journal own-streams.
|
|
13
|
+
* This class IS that concrete emitter; server.ts builds ONE and adapts it to each
|
|
14
|
+
* manager's emit seam (a tiny per-store adapter that names the store's
|
|
15
|
+
* `build*RecordData`).
|
|
16
|
+
*
|
|
17
|
+
* It is store-AGNOSTIC: `emit(store, recordKey, build)` resolves the store's journal
|
|
18
|
+
* kind from the injected `ReplicatedKindRegistry`, gates on
|
|
19
|
+
* `multiMachine.stateSync.<store>.enabled` (dark by default), stamps the HLC, derives
|
|
20
|
+
* the `observed` witness, asks the store's builder for the disclosure-minimized
|
|
21
|
+
* envelope `data`, and appends it via `CoherenceJournal.emitReplicatedRecord`. ONE
|
|
22
|
+
* generic path serves all 7 kinds; the only per-store thing is the builder closure.
|
|
23
|
+
*
|
|
24
|
+
* SAFETY: NEVER throws into the caller (the manager hooks are best-effort). A disabled
|
|
25
|
+
* store, an unregistered store, a degenerate (null) recordKey, a builder that returns
|
|
26
|
+
* null or throws (e.g. a record over its per-entry byte cap) — every one is a counted
|
|
27
|
+
* no-op, never an exception that would break the local write the agent is performing.
|
|
28
|
+
* Emission is SINGLE-ORIGIN by construction: it stamps `origin = this machine` and the
|
|
29
|
+
* journal writes only this machine's own stream (§6.1 anti-forgery holds end-to-end).
|
|
30
|
+
*/
|
|
31
|
+
import { isStoreEmissionEnabled, } from './ReplicatedRecordEnvelope.js';
|
|
32
|
+
export class ReplicatedRecordEmitter {
|
|
33
|
+
seams;
|
|
34
|
+
stats = {
|
|
35
|
+
emitted: 0,
|
|
36
|
+
storeDisabled: 0,
|
|
37
|
+
skipped: 0,
|
|
38
|
+
errors: 0,
|
|
39
|
+
};
|
|
40
|
+
constructor(seams) {
|
|
41
|
+
// Wiring-integrity preconditions: the seams MUST be real, not null/no-op.
|
|
42
|
+
if (!seams)
|
|
43
|
+
throw new Error('ReplicatedRecordEmitter: seams are required');
|
|
44
|
+
if (!seams.journal || typeof seams.journal.emitReplicatedRecord !== 'function') {
|
|
45
|
+
throw new Error('ReplicatedRecordEmitter: journal.emitReplicatedRecord seam must be a function (not a no-op)');
|
|
46
|
+
}
|
|
47
|
+
if (!seams.clock || typeof seams.clock.tick !== 'function') {
|
|
48
|
+
throw new Error('ReplicatedRecordEmitter: clock.tick seam must be a function');
|
|
49
|
+
}
|
|
50
|
+
if (!seams.registry)
|
|
51
|
+
throw new Error('ReplicatedRecordEmitter: registry seam is required (not null)');
|
|
52
|
+
if (typeof seams.origin !== 'string' || seams.origin.length === 0) {
|
|
53
|
+
throw new Error('ReplicatedRecordEmitter: origin must be a non-empty string');
|
|
54
|
+
}
|
|
55
|
+
if (typeof seams.stores !== 'function')
|
|
56
|
+
throw new Error('ReplicatedRecordEmitter: stores seam must be a function');
|
|
57
|
+
if (typeof seams.loadWitness !== 'function')
|
|
58
|
+
throw new Error('ReplicatedRecordEmitter: loadWitness seam must be a function (not a no-op)');
|
|
59
|
+
this.seams = seams;
|
|
60
|
+
}
|
|
61
|
+
/** Read-only stats (for observability + the wiring-integrity assertions). */
|
|
62
|
+
getStats() {
|
|
63
|
+
return { ...this.stats };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Emit one replicated record for (store, recordKey). The single generic path for a
|
|
67
|
+
* put OR a delete — the only difference is the `build` closure (a put builder vs a
|
|
68
|
+
* tombstone builder). Never throws into the caller.
|
|
69
|
+
*
|
|
70
|
+
* Order (§4): dark gate → degenerate guard → witness → tick → build → append.
|
|
71
|
+
*/
|
|
72
|
+
emit(store, recordKey, build) {
|
|
73
|
+
try {
|
|
74
|
+
// 1. Dark gate (the default). A disabled store emits NOTHING — a strict no-op.
|
|
75
|
+
if (!isStoreEmissionEnabled(this.seams.stores(), store)) {
|
|
76
|
+
this.stats.storeDisabled++;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// 2. Only a registered store has a journal kind to ride.
|
|
80
|
+
const reg = this.seams.registry.getByStore(store);
|
|
81
|
+
if (!reg) {
|
|
82
|
+
this.stats.skipped++;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// 3. Degenerate guard — a null/empty recordKey has no stable identity surface.
|
|
86
|
+
if (typeof recordKey !== 'string' || recordKey.length === 0) {
|
|
87
|
+
this.stats.skipped++;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// 4. Witness BEFORE the tick — the HLC this machine had already merged for the
|
|
91
|
+
// key (own prior + applied peers). Best-effort; a witness read fault degrades
|
|
92
|
+
// to "no witness" (flag-on-conflict, the safe direction), never a throw.
|
|
93
|
+
let observed;
|
|
94
|
+
try {
|
|
95
|
+
observed = this.seams.loadWitness(store, recordKey);
|
|
96
|
+
}
|
|
97
|
+
catch (e) { /* @silent-fallback-ok: a witness read fault degrades to no-witness ⇒ flag-on-conflict (the safe merge direction, §7.2); never blocks the emit. */
|
|
98
|
+
observed = undefined;
|
|
99
|
+
this.log('witness-read-failed', { store, error: e?.message });
|
|
100
|
+
}
|
|
101
|
+
// 5. Tick AFTER the witness so hlc > observed (a clean sequential position).
|
|
102
|
+
const hlc = this.seams.clock.tick();
|
|
103
|
+
// 6. Build the disclosure-minimized envelope data (store-specific).
|
|
104
|
+
const data = build(hlc, this.seams.origin, observed);
|
|
105
|
+
if (data === null || data === undefined) {
|
|
106
|
+
this.stats.skipped++;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// 7. Append. The journal validates + op-key-dedupes + enqueues (non-blocking).
|
|
110
|
+
this.seams.journal.emitReplicatedRecord(reg.kind, data);
|
|
111
|
+
this.stats.emitted++;
|
|
112
|
+
}
|
|
113
|
+
catch (e) { /* @silent-fallback-ok: the manager's write must NEVER fail because replication did — a builder throw (e.g. over-cap) / journal fault is a counted no-op, surfaced via stats + the log, never propagated (Structure > Willpower: the safety is structural, not per-caller). */
|
|
114
|
+
this.stats.errors++;
|
|
115
|
+
this.log('emit-failed', { store, error: e?.message });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
log(event, detail) {
|
|
119
|
+
this.seams.log?.(event, detail);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=ReplicatedRecordEmitter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReplicatedRecordEmitter.js","sourceRoot":"","sources":["../../src/core/ReplicatedRecordEmitter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ws2SendWiring — the SEND-side wiring manifest + the wiring-integrity ratchet
|
|
3
|
+
* (WS2 send-side, docs/specs/WS2-SEND-SIDE-EMISSION-SPEC.md §6/§7).
|
|
4
|
+
*
|
|
5
|
+
* THE INVARIANT THIS ENFORCES: every replicated store registered in the
|
|
6
|
+
* `ReplicatedKindRegistry` (the RECEIVE/advert half) must be CONSCIOUSLY classified
|
|
7
|
+
* as either SEND-WIRED (its manager's emit hooks are attached to the journal-backed
|
|
8
|
+
* emitter) or SEND-PENDING (a known, enumerated follow-up). A new replicated kind
|
|
9
|
+
* added to the registry WITHOUT placing it in one of these sets fails the ratchet —
|
|
10
|
+
* which is the EXACT gap this workstream fixes: a kind shipped receive-only (advert +
|
|
11
|
+
* apply machinery) with the SEND half silently a no-op. The ratchet makes that a CI
|
|
12
|
+
* failure, not a memory item (Structure > Willpower).
|
|
13
|
+
*
|
|
14
|
+
* Pure data + a pure check — no I/O, no deps. The server's wiring + the
|
|
15
|
+
* wiring-integrity test BOTH read these sets, so they cannot drift.
|
|
16
|
+
*/
|
|
17
|
+
/** Stores whose manager emit hooks ARE attached to the journal-backed emitter (their
|
|
18
|
+
* records actually cross). The learnings slice ships first; the other seamed stores
|
|
19
|
+
* follow on the SAME emitter (WS2-SEND-2). */
|
|
20
|
+
export declare const WS2_SEND_WIRED_STORES: ReadonlyArray<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Stores registered for RECEIVE but whose SEND wiring is a KNOWN, enumerated
|
|
23
|
+
* follow-up — NOT a silent omission. Each is here for a stated reason:
|
|
24
|
+
* - relationships / knowledge / evolutionActions / userRegistry: fully seamed
|
|
25
|
+
* managers (emitPut + emitDelete); table-row wiring onto the same emitter (WS2-SEND-2).
|
|
26
|
+
* - topicOperator: seamed put-only (no emitDelete in its manager interface yet) (WS2-SEND-3).
|
|
27
|
+
* - preferences: NO manager emit seam yet — it rode the deprecated `preferences-sync`
|
|
28
|
+
* verb; needs a manager emit hook before it can be wired (WS2-SEND-3).
|
|
29
|
+
*/
|
|
30
|
+
export declare const WS2_SEND_PENDING_STORES: ReadonlyArray<string>;
|
|
31
|
+
/** A registered store's send-wiring classification. */
|
|
32
|
+
export type Ws2SendStatus = 'wired' | 'pending' | 'unclassified';
|
|
33
|
+
/** Classify a registered store's send-wiring status. */
|
|
34
|
+
export declare function ws2SendStatus(store: string): Ws2SendStatus;
|
|
35
|
+
/** The ratchet result: any registered store that is neither wired nor pending. */
|
|
36
|
+
export interface Ws2SendWiringAudit {
|
|
37
|
+
wired: string[];
|
|
38
|
+
pending: string[];
|
|
39
|
+
/** Registered stores in NEITHER set — the failure condition (a silent receive-only kind). */
|
|
40
|
+
unclassified: string[];
|
|
41
|
+
ok: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Audit a set of registered store keys against the wiring manifest. `ok` is false iff
|
|
45
|
+
* any registered store is unclassified (neither wired nor pending) — the exact
|
|
46
|
+
* receive-only gap this workstream closes. Also flags a store in BOTH sets (a manifest
|
|
47
|
+
* authoring error) as unclassified-style failure via a thrown precondition is avoided;
|
|
48
|
+
* instead WIRED takes precedence and the overlap is surfaced by the caller's own
|
|
49
|
+
* disjointness assertion in the test.
|
|
50
|
+
*/
|
|
51
|
+
export declare function auditWs2SendWiring(registeredStores: ReadonlyArray<string>): Ws2SendWiringAudit;
|
|
52
|
+
//# sourceMappingURL=ws2SendWiring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws2SendWiring.d.ts","sourceRoot":"","sources":["../../src/core/ws2SendWiring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;+CAE+C;AAC/C,eAAO,MAAM,qBAAqB,EAAE,aAAa,CAAC,MAAM,CAEtD,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,MAAM,CAOxD,CAAC;AAEH,uDAAuD;AACvD,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,cAAc,CAAC;AAEjE,wDAAwD;AACxD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAI1D;AAED,kFAAkF;AAClF,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6FAA6F;IAC7F,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,EAAE,EAAE,OAAO,CAAC;CACb;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,kBAAkB,CAW9F"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ws2SendWiring — the SEND-side wiring manifest + the wiring-integrity ratchet
|
|
3
|
+
* (WS2 send-side, docs/specs/WS2-SEND-SIDE-EMISSION-SPEC.md §6/§7).
|
|
4
|
+
*
|
|
5
|
+
* THE INVARIANT THIS ENFORCES: every replicated store registered in the
|
|
6
|
+
* `ReplicatedKindRegistry` (the RECEIVE/advert half) must be CONSCIOUSLY classified
|
|
7
|
+
* as either SEND-WIRED (its manager's emit hooks are attached to the journal-backed
|
|
8
|
+
* emitter) or SEND-PENDING (a known, enumerated follow-up). A new replicated kind
|
|
9
|
+
* added to the registry WITHOUT placing it in one of these sets fails the ratchet —
|
|
10
|
+
* which is the EXACT gap this workstream fixes: a kind shipped receive-only (advert +
|
|
11
|
+
* apply machinery) with the SEND half silently a no-op. The ratchet makes that a CI
|
|
12
|
+
* failure, not a memory item (Structure > Willpower).
|
|
13
|
+
*
|
|
14
|
+
* Pure data + a pure check — no I/O, no deps. The server's wiring + the
|
|
15
|
+
* wiring-integrity test BOTH read these sets, so they cannot drift.
|
|
16
|
+
*/
|
|
17
|
+
/** Stores whose manager emit hooks ARE attached to the journal-backed emitter (their
|
|
18
|
+
* records actually cross). The learnings slice ships first; the other seamed stores
|
|
19
|
+
* follow on the SAME emitter (WS2-SEND-2). */
|
|
20
|
+
export const WS2_SEND_WIRED_STORES = Object.freeze([
|
|
21
|
+
'learnings',
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* Stores registered for RECEIVE but whose SEND wiring is a KNOWN, enumerated
|
|
25
|
+
* follow-up — NOT a silent omission. Each is here for a stated reason:
|
|
26
|
+
* - relationships / knowledge / evolutionActions / userRegistry: fully seamed
|
|
27
|
+
* managers (emitPut + emitDelete); table-row wiring onto the same emitter (WS2-SEND-2).
|
|
28
|
+
* - topicOperator: seamed put-only (no emitDelete in its manager interface yet) (WS2-SEND-3).
|
|
29
|
+
* - preferences: NO manager emit seam yet — it rode the deprecated `preferences-sync`
|
|
30
|
+
* verb; needs a manager emit hook before it can be wired (WS2-SEND-3).
|
|
31
|
+
*/
|
|
32
|
+
export const WS2_SEND_PENDING_STORES = Object.freeze([
|
|
33
|
+
'relationships',
|
|
34
|
+
'knowledge',
|
|
35
|
+
'evolutionActions',
|
|
36
|
+
'userRegistry',
|
|
37
|
+
'topicOperator',
|
|
38
|
+
'preferences',
|
|
39
|
+
]);
|
|
40
|
+
/** Classify a registered store's send-wiring status. */
|
|
41
|
+
export function ws2SendStatus(store) {
|
|
42
|
+
if (WS2_SEND_WIRED_STORES.includes(store))
|
|
43
|
+
return 'wired';
|
|
44
|
+
if (WS2_SEND_PENDING_STORES.includes(store))
|
|
45
|
+
return 'pending';
|
|
46
|
+
return 'unclassified';
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Audit a set of registered store keys against the wiring manifest. `ok` is false iff
|
|
50
|
+
* any registered store is unclassified (neither wired nor pending) — the exact
|
|
51
|
+
* receive-only gap this workstream closes. Also flags a store in BOTH sets (a manifest
|
|
52
|
+
* authoring error) as unclassified-style failure via a thrown precondition is avoided;
|
|
53
|
+
* instead WIRED takes precedence and the overlap is surfaced by the caller's own
|
|
54
|
+
* disjointness assertion in the test.
|
|
55
|
+
*/
|
|
56
|
+
export function auditWs2SendWiring(registeredStores) {
|
|
57
|
+
const wired = [];
|
|
58
|
+
const pending = [];
|
|
59
|
+
const unclassified = [];
|
|
60
|
+
for (const store of registeredStores) {
|
|
61
|
+
const status = ws2SendStatus(store);
|
|
62
|
+
if (status === 'wired')
|
|
63
|
+
wired.push(store);
|
|
64
|
+
else if (status === 'pending')
|
|
65
|
+
pending.push(store);
|
|
66
|
+
else
|
|
67
|
+
unclassified.push(store);
|
|
68
|
+
}
|
|
69
|
+
return { wired, pending, unclassified, ok: unclassified.length === 0 };
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=ws2SendWiring.js.map
|
|
@@ -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,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,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.
|