instar 1.3.690 → 1.3.691
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 +9 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +7 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/MultiMachineCoordinator.d.ts +40 -0
- package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
- package/dist/core/MultiMachineCoordinator.js +96 -1
- package/dist/core/MultiMachineCoordinator.js.map +1 -1
- package/dist/core/nobodyPollingActuator.d.ts +61 -0
- package/dist/core/nobodyPollingActuator.d.ts.map +1 -0
- package/dist/core/nobodyPollingActuator.js +61 -0
- package/dist/core/nobodyPollingActuator.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +3 -3
- package/upgrades/1.3.691.md +28 -0
- package/upgrades/side-effects/mesh-self-heal-g2-enforce.md +38 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MESH-SELF-HEAL G2 — enforce-mode ACTUATOR (dependency-injected, testable).
|
|
3
|
+
*
|
|
4
|
+
* Spec: MESH-SELF-HEAL-SPEC §3.2; build plan: MESH-SELF-HEAL-G2-BUILD.md.
|
|
5
|
+
*
|
|
6
|
+
* This is the ENFORCE half of G2: given a `claim` decision from
|
|
7
|
+
* `decideNobodyPollingClaim`, actually take poll-ownership — win the fenced
|
|
8
|
+
* epoch-CAS, RE-VERIFY live local poll-success (CAS-win is necessary-not-sufficient,
|
|
9
|
+
* Adv2-F1), then either start polling (drive the existing poll-follows-lease lever)
|
|
10
|
+
* or relinquish + self-exclude. The delicate cross-machine authority (the exact
|
|
11
|
+
* code whose hasty version caused the 2026-06-27 tug-of-war) is isolated behind
|
|
12
|
+
* INJECTED PORTS so it is unit-testable WITHOUT a live coordinator/lifeline, and so
|
|
13
|
+
* the dryRun gate is a single, auditable chokepoint.
|
|
14
|
+
*
|
|
15
|
+
* Ships DARK + dryRun-first. In dryRun it records "would actuate" and performs ZERO
|
|
16
|
+
* side effects (no CAS acquire, no poll-lever write) — pure observation. Only an
|
|
17
|
+
* explicit `dryRun:false` (the enforce promotion) ever touches the lease or polling.
|
|
18
|
+
*
|
|
19
|
+
* SECOND-PASS REVIEW REQUIRED before merge (this holds poll-ownership authority).
|
|
20
|
+
*/
|
|
21
|
+
import { decidePostCasSelfReverify } from './nobodyPollingRecovery.js';
|
|
22
|
+
/**
|
|
23
|
+
* Actuate a G2 claim decision. Pure control-flow over injected ports; the only
|
|
24
|
+
* authority is exercised through the ports, and ONLY when `dryRun` is false and the
|
|
25
|
+
* decision is a genuine self-claim. Idempotent per call.
|
|
26
|
+
*/
|
|
27
|
+
export async function applyNobodyPollingRecovery(args) {
|
|
28
|
+
const { decision, dryRun, ports, ledger, nowIso, log } = args;
|
|
29
|
+
// Only a genuine self-claim actuates. stand-down / veto / fail-closed / hold /
|
|
30
|
+
// escalate / await / no-op are non-actuating (the decision recorder already
|
|
31
|
+
// counted them); the actuator must NEVER touch the lease for those.
|
|
32
|
+
if (!decision.selfClaims || decision.action !== 'claim') {
|
|
33
|
+
return { result: 'no-action', reason: `decision=${decision.action}` };
|
|
34
|
+
}
|
|
35
|
+
// DARK / dryRun: observe only. Record the counterfactual; perform NO side effect.
|
|
36
|
+
if (dryRun) {
|
|
37
|
+
log?.(`[g2-actuator] DRY-RUN would claim poll-ownership (epoch n+1) — no CAS, no poll-lever write`);
|
|
38
|
+
return { result: 'dry-run-would-claim', reason: 'dry-run-observe-only' };
|
|
39
|
+
}
|
|
40
|
+
// ENFORCE: win the fenced epoch-CAS. Losing means a peer won the same episode —
|
|
41
|
+
// stand down (the single-claimant invariant holds via the fence, not our guess).
|
|
42
|
+
const won = await ports.acquireFencedCas();
|
|
43
|
+
if (!won) {
|
|
44
|
+
return { result: 'cas-lost', reason: 'another-machine-won-the-fenced-epoch' };
|
|
45
|
+
}
|
|
46
|
+
// CAS-win is necessary but NOT sufficient (Adv2-F1): re-verify OWN live poll
|
|
47
|
+
// freshness (current+local) before committing to serve.
|
|
48
|
+
const reverify = decidePostCasSelfReverify({ localPollSucceededFresh: ports.localPollSucceededFresh() });
|
|
49
|
+
if (!reverify.commit) {
|
|
50
|
+
ports.relinquishAndSelfExclude();
|
|
51
|
+
ledger.recordSelfExclusion(nowIso);
|
|
52
|
+
log?.(`[g2-actuator] won CAS but self-unfit on re-verify → relinquished + self-excluded`);
|
|
53
|
+
return { result: 'self-excluded', reason: reverify.reason };
|
|
54
|
+
}
|
|
55
|
+
// Committed: drive the poll-follows-lease lever to START polling at the won epoch.
|
|
56
|
+
// (The caller's post-claim live-verify confirms lifeline-poll-active.json advances.)
|
|
57
|
+
ports.startPolling(ports.currentEpoch());
|
|
58
|
+
log?.(`[g2-actuator] claimed poll-ownership + started polling at epoch ${ports.currentEpoch()}`);
|
|
59
|
+
return { result: 'claimed-serving', reason: 'cas-won-self-reverified-fit' };
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=nobodyPollingActuator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nobodyPollingActuator.js","sourceRoot":"","sources":["../../src/core/nobodyPollingActuator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAoCvE;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAAC,IAOhD;IACC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAE9D,+EAA+E;IAC/E,4EAA4E;IAC5E,oEAAoE;IACpE,IAAI,CAAC,QAAQ,CAAC,UAAU,IAAI,QAAQ,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;QACxD,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;IACxE,CAAC;IAED,kFAAkF;IAClF,IAAI,MAAM,EAAE,CAAC;QACX,GAAG,EAAE,CAAC,4FAA4F,CAAC,CAAC;QACpG,OAAO,EAAE,MAAM,EAAE,qBAAqB,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;IAC3E,CAAC;IAED,gFAAgF;IAChF,iFAAiF;IACjF,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,gBAAgB,EAAE,CAAC;IAC3C,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,sCAAsC,EAAE,CAAC;IAChF,CAAC;IAED,6EAA6E;IAC7E,wDAAwD;IACxD,MAAM,QAAQ,GAAG,yBAAyB,CAAC,EAAE,uBAAuB,EAAE,KAAK,CAAC,uBAAuB,EAAE,EAAE,CAAC,CAAC;IACzG,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QACrB,KAAK,CAAC,wBAAwB,EAAE,CAAC;QACjC,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC;QACnC,GAAG,EAAE,CAAC,kFAAkF,CAAC,CAAC;QAC1F,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;IAC9D,CAAC;IAED,mFAAmF;IACnF,qFAAqF;IACrF,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC;IACzC,GAAG,EAAE,CAAC,mEAAmE,KAAK,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IACjG,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,6BAA6B,EAAE,CAAC;AAC9E,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-28T09:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-28T09:47:08.176Z",
|
|
5
|
+
"instarVersion": "1.3.691",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -1618,7 +1618,7 @@
|
|
|
1618
1618
|
"type": "subsystem",
|
|
1619
1619
|
"domain": "coordination",
|
|
1620
1620
|
"sourcePath": "src/core/MultiMachineCoordinator.ts",
|
|
1621
|
-
"contentHash": "
|
|
1621
|
+
"contentHash": "db0a3a53e3b3fc6d8c9593918944cba27c1faccd5a369d71bf1e555cf20bb028",
|
|
1622
1622
|
"since": "2025-01-01"
|
|
1623
1623
|
},
|
|
1624
1624
|
"subsystem:backup-manager": {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Mesh Self-Heal **G2 enforce** (MESH-SELF-HEAL-SPEC §3.2) — the take-over actuation for the nobody-serving alarm. When NO machine is polling Telegram (the zombie-lease-holder state behind the message-drop incidents), exactly ONE fit machine now takes over poll-ownership — never zero (drops), never two (the Telegram 409 poll-war).
|
|
9
|
+
|
|
10
|
+
- `src/core/nobodyPollingActuator.ts` (`applyNobodyPollingRecovery`) — dependency-injected actuator: win the fenced epoch-CAS → re-verify own poll-freshness (CAS-win is necessary-not-sufficient) → start polling, else relinquish + self-exclude.
|
|
11
|
+
- `MultiMachineCoordinator.evaluateNobodyPolling()` — wires the detector + single-claimant election to the actuator with the real lease ports (`acquireIfEligible` / `currentEpoch` / `writeLeasePollIntent` / `relinquishAndBroadcast`), debounced (confirm=3) and reentrancy-guarded.
|
|
12
|
+
- `server.ts` `refreshPool` — runs it on the existing 30s pool cadence, fire-and-forget.
|
|
13
|
+
|
|
14
|
+
Ships **dark + dryRun-first** (`multiMachine.nobodyPollingRecovery`, OMITS `enabled` → dev-gated). In dryRun it detects + elects + records the soak counterfactual but performs ZERO side effects (no lease acquire, no poll-lever write) — verified by an independent second-pass review (the high-risk lease-authority gate). Flipping `dryRun:false` is the deliberate enforce promotion, gated on two tracked prerequisites: the real pollSucceeded watermark + the peer-confirmed global-outage plumbing (see MESH-SELF-HEAL-G2-BUILD.md).
|
|
15
|
+
|
|
16
|
+
## What to Tell Your User
|
|
17
|
+
|
|
18
|
+
When this is turned on (it ships off, in observe-only mode for now), it fixes the "nobody is polling, so my messages silently drop" problem on multi-machine setups: if the in-charge machine goes quiet, exactly one healthy machine automatically takes over polling — no tug-of-war, no duplicates, no action needed from you. Single-machine setups are unaffected.
|
|
19
|
+
|
|
20
|
+
## Summary of New Capabilities
|
|
21
|
+
|
|
22
|
+
- `multiMachine.nobodyPollingRecovery` `{ dryRun }` — opt-in (dev-gated) nobody-polling take-over: detect → elect one machine → acquire the fenced lease → start polling. Dark + dryRun by default (observe-only; zero side effects until a deliberate enforce promotion).
|
|
23
|
+
|
|
24
|
+
## Evidence
|
|
25
|
+
|
|
26
|
+
- `tests/unit/nobodyPollingActuator.test.ts` — 5 tests, both sides of every boundary incl. the critical dryRun=zero-side-effects safety invariant (no CAS, no poll-write) and CAS-lost / self-excluded / claimed-serving.
|
|
27
|
+
- Builds on the merged G2 core (18 unit) + observe route (4 integration). Dark-gate lint golden map updated for the +7 line shift. Typecheck clean; full G2 suite green.
|
|
28
|
+
- Independent Phase-5 second-pass review: CONCUR (dryRun invariant holds; single-claimant is the fenced CAS; no tug-of-war). Its one minor concern (reentrancy guard) was fixed in this commit.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Side-Effects Review — Mesh Self-Heal G2 (enforce actuation)
|
|
2
|
+
|
|
3
|
+
**Change:** The ENFORCE half of G2 (MESH-SELF-HEAL-SPEC §3.2) — actually taking over poll-ownership when nobody is polling. HIGH-RISK: it holds cross-machine poll-ownership authority (the area whose hasty version caused the 2026-06-27 tug-of-war). Pieces:
|
|
4
|
+
- `src/core/nobodyPollingActuator.ts` (`applyNobodyPollingRecovery`) — dependency-injected actuator: dryRun gate → `acquireFencedCas` → `decidePostCasSelfReverify` → `startPolling` / `relinquishAndSelfExclude`. 5 unit tests.
|
|
5
|
+
- `MultiMachineCoordinator.evaluateNobodyPolling()` — the wiring: gate (`nobodyPollingRecoveryCfg`) → B5 verdict → debounce (`_g2SilenceStreak`, confirm=3) → `decideNobodyPollingClaim` → record → actuate (real ports: `leaseCoordinator.acquireIfEligible` / `currentEpoch` / `writeLeasePollIntent(true)` / `relinquishAndBroadcast`). Reentrancy-guarded (`_g2Evaluating`).
|
|
6
|
+
- `server.ts` `refreshPool` — the 30s cadence (fire-and-forget, gated inside the coordinator).
|
|
7
|
+
- `ConfigDefaults`: `multiMachine.nobodyPollingRecovery: { dryRun: true }` (OMITS `enabled` → dev-gated). Dark-gate lint golden map updated (+7 line shift).
|
|
8
|
+
|
|
9
|
+
**Decision point?** YES — it acquires the fenced lease + starts polling. Signal-vs-authority + the dryRun safety invariant are the load-bearing concerns.
|
|
10
|
+
|
|
11
|
+
## 1. Over-block
|
|
12
|
+
N/A (it grants poll-ownership, never blocks). It biases hard against acting: only a confirmed real silence where this machine is the deterministic single claimant AND wins the fenced CAS AND re-verifies its own poll-freshness ever serves.
|
|
13
|
+
|
|
14
|
+
## 2. Under-block
|
|
15
|
+
Ships dark+dryRun (default), so nothing actuates until a deliberate `dryRun:false`. **Enforce-ENABLE prerequisites (TRACKED, not orphan-deferred — see MESH-SELF-HEAL-G2-BUILD.md + the enforce note below):** (1) the real `pollSucceededMonoMs`/serve-progress watermark for `localPollSucceededFresh` (currently a lifeline-liveness approximation — consulted ONLY when dryRun:false); (2) peer-confirmed `globalOutageEvidence` plumbing (currently hard-coded false → a confirmed silence proceeds to elect, the local-failure-safe direction). Both are inert under dryRun.
|
|
16
|
+
|
|
17
|
+
## 3. Level-of-abstraction fit
|
|
18
|
+
The authority lives in the coordinator (it owns the leaseCoordinator); the server only supplies the cadence + capacities. The actuator isolates the delicate flow behind injected ports (testable without a live coordinator; the dryRun gate is a single chokepoint). Reuses `acquireIfEligible` (the SAME fence `tickLease` uses) — not a parallel acquire path.
|
|
19
|
+
|
|
20
|
+
## 4. Signal vs authority compliance
|
|
21
|
+
COMPLIANT with the safety design: the only authority (acquire CAS + start polling) is exercised through the ports ONLY when `dryRun:false` AND the decision is a genuine self-claim. The election is deterministic + machine-agnostic, but it is NOT the authority — the **fenced CAS is the single-claimant gate** (even if two machines diverge on the election under partition, `acquireIfEligible` admits exactly one; the loser gets `cas-lost` and stands down). `decidePostCasSelfReverify` enforces "CAS-win necessary-not-sufficient."
|
|
22
|
+
|
|
23
|
+
## 5. Interactions
|
|
24
|
+
- vs `tickLease`: a won CAS makes this machine the holder → `tickLease`'s non-holder acquire converges (doesn't fight); a successful claim flips the verdict to `ok` (self-terminating). Debounce + post-CAS reverify damp flap.
|
|
25
|
+
- Reentrancy: `_g2Evaluating` prevents a >30s eval overlapping the 30s cadence (which would inflate the silence streak pre-await). Added per the 2nd-pass review.
|
|
26
|
+
- Cadence: fire-and-forget with `.catch()` — a slow/failed eval never blocks or fails `refreshPool`.
|
|
27
|
+
|
|
28
|
+
## 6. External surfaces
|
|
29
|
+
No new route (the observe `/mesh-selfheal/g2` from the prior PR reads the same ledger). New config path `multiMachine.nobodyPollingRecovery` (dev-gated). When `dryRun:false` it WRITES the lease (poll-ownership) — the reason this is dark-first + 2nd-pass-gated.
|
|
30
|
+
|
|
31
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
32
|
+
This IS the cross-machine recovery actuator. Coherence is the deterministic election + the fenced CAS (one winner, pool-wide). The watermarks it reads are machine-local (skew-immune). Single-machine = strict no-op (no leaseCoordinator/peers → `skipped`).
|
|
33
|
+
|
|
34
|
+
## 8. Rollback cost
|
|
35
|
+
Trivial — ships dark+dryRun (strict no-op until a deliberate enable). Revert the commit, or leave the flag off. No migration, no state. Even enabled-in-dryRun performs zero side effects.
|
|
36
|
+
|
|
37
|
+
## Second-Pass Review (REQUIRED — high-risk: lease/poll-ownership authority)
|
|
38
|
+
An independent reviewer audited the actuator + coordinator method + cadence. **Verdict: CONCUR.** Verified: (a) the dryRun invariant HOLDS — the gate sits strictly before every port call; default `dryRun:true` (`?? true`) + dev-gated dark-on-fleet; test pins zero side-effects; (b) single-claimant is genuinely the fenced CAS, not the election guess — a partition divergence still admits exactly one (loser → `cas-lost`); (c) no tug-of-war — the won CAS converges with `tickLease`, debounce + self-reverify damp flap; (d) fail directions correct, hardcoded `globalOutageEvidence:false` is the documented local-safe direction + inert under dryRun. **One concern raised (minor, non-blocking given dark+dryRun): no reentrancy guard** on `evaluateNobodyPolling` — a >30s eval could overlap the 30s tick and inflate `_g2SilenceStreak`. **RESOLVED in this commit** (added `_g2Evaluating` guard, mirroring `leaseTicking`). The reviewer's two enforce-ENABLE prerequisites (the pollSucceeded watermark; the globalOutageEvidence plumbing) are tracked in MESH-SELF-HEAL-G2-BUILD.md as the gate before `dryRun:false`.
|