instar 1.3.498 → 1.3.499
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 +68 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +8 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +86 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/monitoring/CommitmentTracker.d.ts.map +1 -1
- package/dist/monitoring/CommitmentTracker.js +7 -1
- package/dist/monitoring/CommitmentTracker.js.map +1 -1
- package/dist/monitoring/PresenceProxy.d.ts +10 -0
- package/dist/monitoring/PresenceProxy.d.ts.map +1 -1
- package/dist/monitoring/PresenceProxy.js +11 -0
- package/dist/monitoring/PresenceProxy.js.map +1 -1
- package/dist/monitoring/PromiseBeacon.d.ts +8 -0
- package/dist/monitoring/PromiseBeacon.d.ts.map +1 -1
- package/dist/monitoring/PromiseBeacon.js +13 -2
- package/dist/monitoring/PromiseBeacon.js.map +1 -1
- package/dist/monitoring/SpeakerElection.d.ts +75 -0
- package/dist/monitoring/SpeakerElection.d.ts.map +1 -0
- package/dist/monitoring/SpeakerElection.js +132 -0
- package/dist/monitoring/SpeakerElection.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +19 -19
- package/upgrades/1.3.499.md +58 -0
- package/upgrades/side-effects/multi-machine-seamlessness-ws3.md +125 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Side-Effects Review — WS3.1/WS3.2 one-voice gates (SpeakerElection)
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/specs/MULTI-MACHINE-SEAMLESSNESS-SPEC.md (converged 2026-06-12, approved; merged to main in PR #1083)
|
|
4
|
+
**Change:** new `src/monitoring/SpeakerElection.ts` (deterministic speaker election with
|
|
5
|
+
lease-stability dwell); PresenceProxy gate at its single send chokepoint (closes F18 —
|
|
6
|
+
it had NO machine gate); PromiseBeacon gate upgraded to live owner re-resolution with
|
|
7
|
+
the commitment stamp as fallback (closes F19); CommitmentTracker stamps
|
|
8
|
+
`ownerMachineId` at creation (defaulting to originMachineId); PostUpdateMigrator
|
|
9
|
+
backfills existing open commitments; dark config flag
|
|
10
|
+
`multiMachine.seamlessness.ws3OneVoice` (default false) + `ws3DwellMs`.
|
|
11
|
+
|
|
12
|
+
## 1. Over-block — what legitimate emissions could this wrongly silence?
|
|
13
|
+
|
|
14
|
+
The known hazard (round-1 adversarial finding, addressed in the converged spec): a
|
|
15
|
+
gate keyed on "am I the owner?" can conclude "no" on EVERY machine and silence the
|
|
16
|
+
pool. The election is built against that direction of error:
|
|
17
|
+
- Unknown ownership NEVER yields pool-wide silence: lease-holder speaks; no/offline
|
|
18
|
+
lease-holder → deterministic lowest-online-id tiebreak; flapping lease → bounded
|
|
19
|
+
defer then tiebreak. The exactly-one-speaks invariant test asserts ≥1 as well as ≤1,
|
|
20
|
+
including under a flapping lease.
|
|
21
|
+
- Every guard failure (flag off, no machine id, pool < 2, deps unbound during the
|
|
22
|
+
boot window) returns "speak" — fail toward speech.
|
|
23
|
+
- PromiseBeacon's silent verdicts RE-ARM the commitment (schedule), never drop it; the
|
|
24
|
+
owning machine's beacon carries the heartbeat.
|
|
25
|
+
Residual over-block: a STALE local placement view could say "owner is the other
|
|
26
|
+
machine" while that machine is dead — its election (not running) can't speak, ours
|
|
27
|
+
stays silent for that topic until placement converges. Mitigation: ownership records
|
|
28
|
+
key the election only when status is 'active'; a dead machine's records converge via
|
|
29
|
+
the existing failover path, and PresenceProxy's cadence re-evaluates each tick.
|
|
30
|
+
Accepted as bounded-by-failover; noted for the WS1.3 reconcile build which shortens
|
|
31
|
+
exactly this window. Second residual (independent reviewer finding): a held silent
|
|
32
|
+
verdict can outlive the chosen speaker's departure for up to one dwell window
|
|
33
|
+
(default 60s) plus one cadence tick on a doubly-degraded path (speaker offline
|
|
34
|
+
mid-dwell + unknown ownership + no online lease-holder) — bounded, self-healing
|
|
35
|
+
(re-election after dwell expiry), and no emission is dropped (both consumers re-arm).
|
|
36
|
+
|
|
37
|
+
## 2. Under-block — what double-voice does this still miss?
|
|
38
|
+
|
|
39
|
+
- Both gates only cover PresenceProxy + PromiseBeacon (the F18/F19 surfaces named in
|
|
40
|
+
the spec). Other notice paths (rate-limit sentinel, recovery notices — F23/WS3.3)
|
|
41
|
+
are explicitly a separate spec item with episode-key dedup; not silently claimed.
|
|
42
|
+
- The ≥1/≤1 invariant holds when machines agree on the replicated inputs. During a
|
|
43
|
+
partition with divergent placement views, two machines can transiently both believe
|
|
44
|
+
they own a topic and both speak — the same exposure as today (no regression), fully
|
|
45
|
+
closed only by the WS1 epoch-fenced reconcile family.
|
|
46
|
+
|
|
47
|
+
## 3. Level-of-abstraction fit
|
|
48
|
+
|
|
49
|
+
One shared deterministic module consumed by both sentinels at their existing
|
|
50
|
+
chokepoints — not two bespoke gates (the F18 finding showed exactly how per-sentinel
|
|
51
|
+
divergence happens). The election reads only local replicated state (ownership
|
|
52
|
+
registry, capacity registry, lease status) — never a mesh call on the emission path
|
|
53
|
+
(spec hot-path rule). The dwell lives in the module so every consumer inherits it.
|
|
54
|
+
|
|
55
|
+
## 4. Signal vs authority compliance
|
|
56
|
+
|
|
57
|
+
The election holds NARROW authority: it gates WHO SPEAKS a duplicate-prone sentinel
|
|
58
|
+
notice — never recovery actions, never message content, never anything user-initiated
|
|
59
|
+
(spec "Decision points touched": WS3 gates sentinel SPEECH only). Its logic is
|
|
60
|
+
deterministic over replicated state (not brittle pattern-matching), it fails open to
|
|
61
|
+
"speak" on every uncertainty, ships dark, and every verdict is observable (onVerdict
|
|
62
|
+
hook → server log; P7). This is the blocking-authority shape the converged spec
|
|
63
|
+
explicitly reviewed and approved across three rounds.
|
|
64
|
+
|
|
65
|
+
## 5. Interactions
|
|
66
|
+
|
|
67
|
+
- The per-topic proxy mutex (PromiseBeacon/PresenceProxy double-post guard, spec A10)
|
|
68
|
+
is untouched and still applies AFTER the election — the two compose (election picks
|
|
69
|
+
the machine, mutex serializes emitters within it).
|
|
70
|
+
- PromiseBeacon's legacy static gate remains for election-absent construction
|
|
71
|
+
(back-compat for tests/embedders); the election branch supersedes it when wired.
|
|
72
|
+
- The CommitmentTracker stamp default cannot collide with cross-machine commitment
|
|
73
|
+
sync (P1.5): originMachineId was already recorded; ownerMachineId now mirrors it at
|
|
74
|
+
creation and remains mutable via the existing CAS mutate path.
|
|
75
|
+
- The migration touches state/commitments.json directly at update time (server down/
|
|
76
|
+
pre-boot — same pattern as every other PostUpdateMigrator state migration); it
|
|
77
|
+
stamps only missing fields on 'pending' records.
|
|
78
|
+
|
|
79
|
+
## 6. External surfaces
|
|
80
|
+
|
|
81
|
+
None new. No routes, no message formats, no cross-machine wire changes. The flag is
|
|
82
|
+
config-local. Single-machine agents: structurally inert (no SpeakerElection
|
|
83
|
+
engagement below 2 online machines; CommitmentTracker stamp is absent without an
|
|
84
|
+
originMachineId — both tested).
|
|
85
|
+
|
|
86
|
+
## 7. Rollback cost
|
|
87
|
+
|
|
88
|
+
Flag-flip: `multiMachine.seamlessness.ws3OneVoice: false` (the default) restores
|
|
89
|
+
legacy verdicts instantly at the next decision — no restart-ordering hazard since the
|
|
90
|
+
flag is read live per decision. The backfilled ownerMachineId stamps are inert data
|
|
91
|
+
when the flag is off (the legacy gate only acts when currentMachineId is ALSO set,
|
|
92
|
+
i.e. multi-machine pools) and are correct-by-construction on single-machine agents;
|
|
93
|
+
no data rollback needed.
|
|
94
|
+
|
|
95
|
+
## Second-pass review
|
|
96
|
+
|
|
97
|
+
REQUIRED (sentinel surface). Reviewer response appended below.
|
|
98
|
+
|
|
99
|
+
<!-- second-pass reviewer response appended below by the independent reviewer -->
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Second-pass reviewer response (independent subagent, 2026-06-12)
|
|
104
|
+
|
|
105
|
+
Concur with the review
|
|
106
|
+
|
|
107
|
+
1. **Fail-toward-speech verified on every uncertain path.** Both consumer gates skip the send ONLY when `!verdict.speak` (PromiseBeacon.ts:360, PresenceProxy.ts:1847), and `SpeakerElection.decideInner` returns `speak:true` on every legacy/no-op/ambiguous branch: flag-off (`legacy-disabled`), no machine id, pool<2 (`single-machine`), owner-self, stamp-self, lease-holder-self, and the deterministic `tiebreak-lowest-id` whenever the lease holder is null or offline. The guards are ordered legacy/no-op FIRST, so none of the election machinery runs when disabled. The boot-window no-op is real: `poolMachineIds()` returns `[]` until `ws3PoolDeps` binds, yielding `single-machine`/speak. The flag is read live (`() => ws3Cfg().ws3OneVoice === true`) so the rollback flip takes effect at the next decision with no restart — artifact §7 is accurate.
|
|
108
|
+
|
|
109
|
+
2. **Silent verdicts re-arm, never drop.** PromiseBeacon calls `this.schedule(c)` on a non-speak verdict, which re-arms at the commitment cadence (default 10min, 1s floor — no busy-loop); PresenceProxy logs and returns, and its own cadence re-evaluates. No emission is lost on a silent/defer verdict. The legacy static gate is preserved for election-absent construction as the artifact §5 claims.
|
|
110
|
+
|
|
111
|
+
3. **Migration direction-of-error is safe and idempotent.** `migrateCommitmentOwnerBackfill` stamps ONLY `status==='pending'` records lacking `ownerMachineId`, with THIS machine's own identity.json machineId — and crucially, when no identity exists it skips WITHOUT writing the marker, so it retries on a later update rather than permanently no-op'ing. The stamp is only a fallback; live placement re-resolution at speak time means a stale backfilled stamp racing a transfer cannot wedge the gate (live owner beats stamp; tested). The single-machine no-op is genuinely byte-identical: no `originMachineId` → `ownerMachineId` stays absent (tested).
|
|
112
|
+
|
|
113
|
+
4. **No authority beyond sentinel speech; cannot gate recovery/delivery/user flows.** The election is consumed at exactly two chokepoints — `PresenceProxy.sendProxyMessage` and `PromiseBeacon.fire` — both of which emit duplicate-prone 🔭/heartbeat notices. It is injected as an optional dep; it touches no recovery action, no message content, no user-initiated path, and `resolveTopicOwner` reads only local replicated state with no mesh/fetch call on the hot path (asserted by test). The `onVerdict` hook is try/catch-swallowed so observability can never gate. I agree with the artifact's "no issue identified" answers in §3–§6.
|
|
114
|
+
|
|
115
|
+
5. **Bounded memory; ≤1 invariant holds.** The `held` map is opportunistically swept past the dwell window at size>512 and `unstableSince` entries are deleted on every decisive path — no unbounded growth. Two machines cannot BOTH speak for a topic under agreed replicated inputs: live owner is single-valued, the stamp comparison is to one id, and the tiebreak picks one `sort()[0]`. (Divergent-placement double-voice during a partition is correctly scoped to WS1 and disclosed as no-regression in §2.)
|
|
116
|
+
|
|
117
|
+
**One verification note for the record (bounded, not a blocker):** the speaker-identity dwell can HOLD a silent verdict, and the held branch returns it irrespective of current pool state. If the chosen speaker machine drops offline mid-dwell while ownership is unknown AND there is no online lease holder AND the remaining pool is still ≥2, the surviving machines can each return a fresh held-silent verdict, producing a transient ALL-silent window for that topic. This does NOT violate the spec's ≥1 framing ("never pool-wide / never unbounded silence"): it is bounded by `dwellMs` (default 60s) plus one cadence tick, it self-heals (after dwell expiry the new lowest-id re-elects to `tiebreak-lowest-id` and speaks), no emission is dropped (both consumers re-arm), and the `single-machine` guard correctly rescues the pool-of-1 case by firing before the dwell check. It is strictly no worse in outcome than the partition exposure already disclosed in §2, and the gate holds no authority beyond speech — an acceptable residual on a doubly-degraded path, not a defect. Recorded in §1's residual list.
|
|
118
|
+
|
|
119
|
+
## Addendum — no-silent-fallbacks ratchet (CI round 1)
|
|
120
|
+
|
|
121
|
+
The three new fail-open guards in server.ts (identity read, pool-ids read, ownership
|
|
122
|
+
read) tripped the repo-wide silent-fallback ratchet (466 > 463). Each now carries the
|
|
123
|
+
sanctioned `@silent-fallback-ok` exemption with its reason inline: every one degrades
|
|
124
|
+
to the election's designed fail-toward-speech/no-op verdict — the documented safe
|
|
125
|
+
direction, not a silent loss. Ratchet count restored to baseline.
|