instar 1.3.552 → 1.3.554
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 +71 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +35 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +10 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +60 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +6 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/types.d.ts +50 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +4 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/scheduler/JobLeaseClaimStore.d.ts +105 -0
- package/dist/scheduler/JobLeaseClaimStore.d.ts.map +1 -0
- package/dist/scheduler/JobLeaseClaimStore.js +165 -0
- package/dist/scheduler/JobLeaseClaimStore.js.map +1 -0
- package/dist/scheduler/JobLeaseCutoverGate.d.ts +73 -0
- package/dist/scheduler/JobLeaseCutoverGate.d.ts.map +1 -0
- package/dist/scheduler/JobLeaseCutoverGate.js +66 -0
- package/dist/scheduler/JobLeaseCutoverGate.js.map +1 -0
- package/dist/scheduler/JobScheduler.d.ts +53 -0
- package/dist/scheduler/JobScheduler.d.ts.map +1 -1
- package/dist/scheduler/JobScheduler.js +136 -20
- package/dist/scheduler/JobScheduler.js.map +1 -1
- package/dist/server/AgentServer.d.ts +3 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +3 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/PoolPollCache.d.ts +136 -0
- package/dist/server/PoolPollCache.d.ts.map +1 -0
- package/dist/server/PoolPollCache.js +163 -0
- package/dist/server/PoolPollCache.js.map +1 -0
- package/dist/server/routes.d.ts +9 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +51 -8
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +65 -65
- package/src/scaffold/templates.ts +4 -0
- package/upgrades/1.3.553.md +63 -0
- package/upgrades/1.3.554.md +79 -0
- package/upgrades/side-effects/multi-machine-seamlessness-ws44-pool-cache.md +115 -0
- package/upgrades/side-effects/ws43-journal-lease-cutover.md +132 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
- **Scheduled-job claims can upgrade from the best-effort AgentBus broadcast to a
|
|
9
|
+
durable, epoch-fenced lease over the replicated journal.** The decision is made by
|
|
10
|
+
a single pure gate (`JobLeaseCutoverGate.decideClaimPath`): the journal-lease path
|
|
11
|
+
engages ONLY when the flag is on AND the pool is flag-coherent — every ONLINE peer
|
|
12
|
+
advertises the `ws43JournalLease` capability in its heartbeat. An older peer (no
|
|
13
|
+
flags field) or a peer with the flag off keeps the WHOLE pool on the legacy bus.
|
|
14
|
+
- **The cutover guarantees the two claim mechanisms are NEVER both live for the same
|
|
15
|
+
job set** — the named migration hazard the spec's "Cutover discipline" rule closes
|
|
16
|
+
(one machine leasing via the journal while a peer broadcasts via the bus). Each
|
|
17
|
+
scheduler evaluation returns exactly one path; the release seam idempotently closes
|
|
18
|
+
whichever store took the claim.
|
|
19
|
+
- **Epoch fencing** (`JobLeaseClaimStore`): a claim carries the coordinator's lease
|
|
20
|
+
epoch; a stale-epoch peer record is rejected on apply and a same/older-epoch peer
|
|
21
|
+
lease fences our claim out — so a demoted machine's late claim can't steal a job
|
|
22
|
+
from the current lease-holder, and a partition double-run is structurally prevented
|
|
23
|
+
when the journal is reachable. Claim records store METADATA ONLY (machine, slug,
|
|
24
|
+
epoch, timestamps) — never job payloads.
|
|
25
|
+
- Ships **dark** behind `multiMachine.seamlessness.ws43JournalLease` (default false)
|
|
26
|
+
with `ws43JournalLeaseDryRun` default true — the first rollout rung logs intended
|
|
27
|
+
journal claims while the legacy bus path still runs, so a dry-run pool never
|
|
28
|
+
half-migrates. Flag-off / mixed-pool / single-machine = byte-for-byte today's
|
|
29
|
+
behavior (the bus path). The advert is only `true` when the flag is on AND not
|
|
30
|
+
dry-run. Plain seamlessness booleans (read live at the claim boundary), so absence
|
|
31
|
+
yields the safe default without a migration — no migrateConfig entry needed.
|
|
32
|
+
|
|
33
|
+
This completes the WS4.3 workstream alongside the merged jobs read-side (#1104) and
|
|
34
|
+
role-guard-at-spawn (#1147). <!-- tracked: CMT-1416 -->
|
|
35
|
+
|
|
36
|
+
## What to Tell Your User
|
|
37
|
+
|
|
38
|
+
This is internal multi-machine plumbing that ships turned off, so you will not
|
|
39
|
+
see a change. When it is eventually enabled on a fleet where all your machines
|
|
40
|
+
support it, your scheduled jobs get a more reliable way to avoid two machines
|
|
41
|
+
running the same job at once — a durable, fenced claim instead of a best-effort
|
|
42
|
+
broadcast. Until then, and on any single-machine setup or a mixed fleet, job
|
|
43
|
+
scheduling behaves exactly as it does today. There is nothing to configure and no
|
|
44
|
+
action needed.
|
|
45
|
+
|
|
46
|
+
## Summary of New Capabilities
|
|
47
|
+
|
|
48
|
+
- WS4.3 journal-lease cutover: scheduled-job claims can upgrade from the AgentBus
|
|
49
|
+
broadcast to a durable, epoch-fenced lease over the replicated journal, engaged
|
|
50
|
+
only when every online peer advertises the capability (flag coherence). A single
|
|
51
|
+
gate guarantees the two claim mechanisms are never both live for one job set.
|
|
52
|
+
Ships dark behind multiMachine.seamlessness.ws43JournalLease (dry-run default);
|
|
53
|
+
single-machine and mixed-pool installs are a strict no-op.
|
|
54
|
+
|
|
55
|
+
## Evidence
|
|
56
|
+
|
|
57
|
+
- `tests/unit/job-lease-cutover-gate.test.ts` (9): the full decision matrix —
|
|
58
|
+
flag-off → bus; single-machine / all-peers-offline → no-op bus; coherent →
|
|
59
|
+
journal; ONE non-advertising peer → whole pool on bus; older peer (absent flags)
|
|
60
|
+
counts as non-advertising; an offline incoherent peer does NOT block coherence;
|
|
61
|
+
dry-run → bus with journalDryRun flagged; and the explicit "NEVER returns both
|
|
62
|
+
mechanisms" invariant.
|
|
63
|
+
- `tests/unit/job-lease-claim-store.test.ts` (8): take/idempotent-reclaim; a peer's
|
|
64
|
+
live lease blocks our same/older-epoch claim; our strictly-newer epoch supersedes;
|
|
65
|
+
applyRemote rejects a stale-epoch record; no self-spoof; completion releases;
|
|
66
|
+
expired peer lease no longer blocks; durable round-trip across restart.
|
|
67
|
+
- `tests/integration/scheduler-journal-lease-cutover.test.ts` (6): the scheduler
|
|
68
|
+
routes through the gate — coherent→journal (bus NOT called), mixed→bus (journal
|
|
69
|
+
NOT taken), dry-run→bus, single-machine no-op, remote-journal-lease→skip, and a
|
|
70
|
+
live flag flip at the boundary changing the path immediately.
|
|
71
|
+
- `tests/e2e/scheduler-journal-lease-cutover-alive.test.ts` (4): the EXACT server.ts
|
|
72
|
+
provider closure (real config + real MultiMachineCoordinator epoch + real
|
|
73
|
+
MachinePoolRegistry fed by a real heartbeat) drives a real triggerJob — coherent
|
|
74
|
+
takes the journal lease and never the bus; mixed takes the bus and never the
|
|
75
|
+
journal; flag-off no-op; single-machine no-op. Proves the wiring is live, not a stub.
|
|
76
|
+
- `tsc --noEmit` clean; no-silent-fallbacks, the dev-agent dark-gate line-map (+18
|
|
77
|
+
recompute via the attributor), and feature-delivery-completeness all green;
|
|
78
|
+
docs-coverage class floor improved (new symbols documented in under-the-hood.md +
|
|
79
|
+
multi-machine.md).
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Side-Effects Review — WS4.4(f): global pool-cache unification
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `multi-machine-seamlessness-ws44-pool-cache`
|
|
4
|
+
**Date:** `2026-06-13`
|
|
5
|
+
**Author:** `Instar Agent (echo)`
|
|
6
|
+
**Spec:** `docs/specs/MULTI-MACHINE-SEAMLESSNESS-SPEC.md` §WS4.4 clause (f) (converged + approved; deferred second half of WS4.4, tracked CMT-1416)
|
|
7
|
+
**Second-pass reviewer:** not-required — pure read-side efficiency/observability; no new authority, no credential-bearing code, no destructive op, no third-party spend.
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
WS4.4 clause (f), the deferred second half of WS4.4: every pool-scope dashboard
|
|
12
|
+
surface (`/sessions?scope=pool`, `/jobs?scope=pool`, `/attention?scope=pool`,
|
|
13
|
+
`/guards?scope=pool`, …) used to fan out to every peer machine INDEPENDENTLY — once
|
|
14
|
+
per surface, per client, per poll interval. With a dashboard polling several tabs at
|
|
15
|
+
once, the same peer is hit N times per interval. This unifies all of them onto ONE
|
|
16
|
+
shared per-peer poll cache: one fan-out per interval feeds every pool-scope surface,
|
|
17
|
+
and under CPU load-shed the cache serves last-cached peer data tagged `stale: true`
|
|
18
|
+
instead of re-fanning (load-shed, honestly labeled).
|
|
19
|
+
|
|
20
|
+
New module (pure + dependency-injected, unit-testable):
|
|
21
|
+
- `src/server/PoolPollCache.ts` — `fetchPeer(peerMachineId, routePath, fetcher)`:
|
|
22
|
+
within-TTL cache hit (one fan-out feeds two surfaces), single-flight coalescing of
|
|
23
|
+
concurrent callers, CPU load-shed stale-serve (never from an empty cache — a first
|
|
24
|
+
read always fetches), a failed fetch is NEVER cached (transient errors don't stick),
|
|
25
|
+
per-(peer, route) keying, and a read-only `snapshot()` of live load + counters.
|
|
26
|
+
|
|
27
|
+
Wiring (all behind the dark flag):
|
|
28
|
+
- `routes.ts` — the `/jobs?scope=pool` per-peer fetch routes through `ctx.poolPollCache`
|
|
29
|
+
when wired; `null` (flag dark / single-machine) ⇒ today's direct per-peer fetch
|
|
30
|
+
byte-for-byte. New read-only `GET /pool/poll-cache` observability route (503 when
|
|
31
|
+
dark — the ships-dark contract). A stale-served peer body surfaces an honest
|
|
32
|
+
`pool.stale: true` tag on the merged response.
|
|
33
|
+
- `commands/server.ts` + `AgentServer.ts` — dev-gated construction (resolveDevAgentGate)
|
|
34
|
+
in the mesh-setup block; `seamlessnessFlags.ws44PoolCache` capability advert.
|
|
35
|
+
- `types.ts` / `ConfigDefaults.ts` / `devGatedFeatures.ts` — flag (`ws44PoolCache`,
|
|
36
|
+
DELIBERATELY OMITTED so the dev-gate decides) + `ws44PoolCacheTtlMs` tunable +
|
|
37
|
+
`MachineCapacity.ws44PoolCache` advert field.
|
|
38
|
+
- `PostUpdateMigrator.ts` + `templates.ts` — Migration Parity (config strip-default +
|
|
39
|
+
CLAUDE.md section + shadow-capability markers) + Agent Awareness.
|
|
40
|
+
|
|
41
|
+
## Decision-point inventory
|
|
42
|
+
|
|
43
|
+
- `/jobs?scope=pool` per-peer fetch through the shared cache — **add** — wired only
|
|
44
|
+
when the dark flag is on; else the existing direct fetch is unchanged.
|
|
45
|
+
- `GET /pool/poll-cache` observability route — **add** — 503 while dark (ships-dark
|
|
46
|
+
contract, like `/pool/queue`); 200 with the snapshot when wired.
|
|
47
|
+
- CPU load-shed stale-serve — **add** — over the load-per-core threshold, serve
|
|
48
|
+
last-cached tagged `stale: true` rather than re-fanning; an empty cache still fetches.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 1. Over-block
|
|
53
|
+
Flag-off (default fleet): zero new behavior — every pool-scope surface keeps its direct
|
|
54
|
+
per-peer fetch byte-for-byte (verified by the "NOT WIRED (cache null)" integration
|
|
55
|
+
test). Flag-on: the 3s default TTL matches the per-route pool caches already in use, so
|
|
56
|
+
no legitimate fresh read is starved. Load-shed only triggers at/above the load-per-core
|
|
57
|
+
threshold (default 1.5, the SessionReaper critical default) AND only when something is
|
|
58
|
+
already cached — a first read is never load-shed.
|
|
59
|
+
|
|
60
|
+
## 2. Under-block
|
|
61
|
+
Not an authority — this gates nothing. It only decides whether to reuse a recently
|
|
62
|
+
fetched peer body or re-fetch. The worst case is serving a body up to one TTL (or, under
|
|
63
|
+
load-shed, the last-cached) older than a fresh fan-out would produce — and load-shed is
|
|
64
|
+
HONESTLY tagged (`stale: true` on the body, `pool.stale: true` on the merge) so a stale
|
|
65
|
+
serve is never silent. A failed fetch is never cached, so a transient peer error cannot
|
|
66
|
+
stick for the whole TTL.
|
|
67
|
+
|
|
68
|
+
## 3. Level-of-abstraction fit
|
|
69
|
+
Right layer. `PoolPollCache` is a pure primitive (injected clock + load reader); the
|
|
70
|
+
route change is a thin branch that calls the fetcher directly when the cache is null.
|
|
71
|
+
It rides the existing pool fan-out pattern (resolvePeerUrls + per-peer timeout + failed
|
|
72
|
+
markers) rather than inventing new transport.
|
|
73
|
+
|
|
74
|
+
## 4. Signal vs authority compliance
|
|
75
|
+
Everything here is a SIGNAL, never an authority: the cache hit / load-shed staleness is
|
|
76
|
+
observability + an efficiency choice; it never gates serving anything, never mutates
|
|
77
|
+
state, and never caches private end-user content (the pool-scope surfaces are
|
|
78
|
+
operator-Bearer reads of mesh METADATA — `/view/:id` private bodies are handled by
|
|
79
|
+
WS4.4 pool-links, which explicitly never caches them).
|
|
80
|
+
|
|
81
|
+
## 5. Interactions
|
|
82
|
+
- Flag-off path leaves every pool-scope surface identical (no shadowing of the direct
|
|
83
|
+
fetch).
|
|
84
|
+
- Coexists with the per-route payload caches (`jobsPoolCache`, etc.) — those dampen two
|
|
85
|
+
rapid calls to the SAME surface; this shared cache dampens the cross-surface,
|
|
86
|
+
cross-client per-peer fan-out. The integration test proves the shared cache is
|
|
87
|
+
genuinely in the path by asserting on ITS OWN counters (`stats.fetches`,
|
|
88
|
+
`cachedKeys`), not the maskable per-route "1 peer hit".
|
|
89
|
+
- Single-machine installs have no peers ⇒ a strict no-op (the cache is never
|
|
90
|
+
constructed, surfaces never call it).
|
|
91
|
+
|
|
92
|
+
## 6. Rollback
|
|
93
|
+
Set/omit `multiMachine.seamlessness.ws44PoolCache` (dev-gated; dark on the fleet by
|
|
94
|
+
default). When off, the cache is never constructed and `/pool/poll-cache` 503s — fully
|
|
95
|
+
reverting to the direct per-peer fetch. No data migration, no persisted state to undo.
|
|
96
|
+
|
|
97
|
+
## Evidence
|
|
98
|
+
|
|
99
|
+
- `tests/unit/pool-poll-cache.test.ts` (9): both sides of every decision boundary —
|
|
100
|
+
within-TTL hit vs expired re-fetch; single-flight coalescing; load-shed over/under
|
|
101
|
+
threshold; load-shed with an EMPTY cache still fetches; a failed fetch is never
|
|
102
|
+
cached; per-(peer, route) keying; snapshot wiring + boundary flip at the threshold.
|
|
103
|
+
- `tests/integration/ws44-pool-cache-route.test.ts` (4): real wiring (real second-server
|
|
104
|
+
peer that COUNTS its `/jobs` hits) — the fan-out routes THROUGH the shared cache
|
|
105
|
+
(asserted via the cache's OWN `stats.fetches` + `cachedKeys`, not the maskable
|
|
106
|
+
per-route "1 hit"); a second poll past the per-route window is served from the shared
|
|
107
|
+
cache WITHOUT re-hitting the peer; the merged body is byte-identical wired vs unwired;
|
|
108
|
+
`GET /pool/poll-cache` 503-when-dark / 200-when-wired.
|
|
109
|
+
- `tests/e2e/pool-poll-cache-alive.test.ts` (3): the feature is ALIVE (200, not 503) on
|
|
110
|
+
the real AgentServer stack; sits behind auth (no Bearer → 401/403); the ships-dark
|
|
111
|
+
default 503s with `{ enabled: false }`.
|
|
112
|
+
- Gate checks green: `tsc --noEmit`; `docs-coverage --check`; `no-silent-fallbacks`
|
|
113
|
+
(the one new fail-closed construction-catch is tagged `@silent-fallback-ok`);
|
|
114
|
+
`feature-delivery-completeness` (new featureSection + both shadow-marker variants);
|
|
115
|
+
`lint-dev-agent-dark-gate` (line-map recomputed for the +17 ConfigDefaults shift).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Side-Effects Review — WS4.3 journal-lease cutover
|
|
2
|
+
|
|
3
|
+
**Spec:** `docs/specs/MULTI-MACHINE-SEAMLESSNESS-SPEC.md` §WS4.3 ("Cutover discipline")
|
|
4
|
+
**Slug:** ws43-journal-lease-cutover
|
|
5
|
+
**Tier:** 2 (multi-machine correctness invariant; safety-invariant risk floor)
|
|
6
|
+
**Flag:** `multiMachine.seamlessness.ws43JournalLease` (default false), `ws43JournalLeaseDryRun` (default true)
|
|
7
|
+
|
|
8
|
+
## What changed
|
|
9
|
+
|
|
10
|
+
Scheduled-job claim deduplication can now upgrade from the legacy best-effort
|
|
11
|
+
AgentBus broadcast (`JobClaimManager`) to a durable, epoch-fenced lease over the
|
|
12
|
+
replicated journal (`JobLeaseClaimStore`). A single pure decision point
|
|
13
|
+
(`JobLeaseCutoverGate.decideClaimPath`) selects which mechanism a job uses, and
|
|
14
|
+
the scheduler routes its three claim seams (remote-claim check, claim-before-spawn,
|
|
15
|
+
release-on-complete) through that decision. New files: `src/scheduler/
|
|
16
|
+
JobLeaseCutoverGate.ts`, `src/scheduler/JobLeaseClaimStore.ts`. Wired into
|
|
17
|
+
`JobScheduler` (new `setJournalLeaseCutover` injector + `resolveClaimPath`/
|
|
18
|
+
`releaseClaim` helpers) and `server.ts` (the production provider closure reading
|
|
19
|
+
the flag + coordinator lease epoch + pool peers' advertised capability). Config
|
|
20
|
+
defaults + types + the `seamlessnessFlags` heartbeat advert added.
|
|
21
|
+
|
|
22
|
+
## Phase 1 — Principle check (signal vs authority)
|
|
23
|
+
|
|
24
|
+
This IS a decision point (it gates which claim mechanism runs). It is correctly
|
|
25
|
+
an **authority** because dedup is a correctness mechanism, not a signal — but the
|
|
26
|
+
authority is NARROW and STRUCTURAL: the gate keys ONLY on objective facts (flag
|
|
27
|
+
state, online-peer presence, the boolean capability each peer advertises in its
|
|
28
|
+
authenticated heartbeat) — never on interpreting message content or any brittle
|
|
29
|
+
heuristic. It cannot block a user action; the worst case is "stay on the legacy
|
|
30
|
+
bus path," which is byte-for-byte today's behavior. The gate fails toward the
|
|
31
|
+
conservative side (bus) on any ambiguity (older peer, offline peer, throwing gate
|
|
32
|
+
read). This complies with `docs/signal-vs-authority.md`: a structural ownership/
|
|
33
|
+
coherence check, not a brittle content gate with blocking authority.
|
|
34
|
+
|
|
35
|
+
## Phase 4 — Review questions
|
|
36
|
+
|
|
37
|
+
1. **Over-block** — The gate never blocks a job; it only chooses a dedup
|
|
38
|
+
mechanism. The journal lease's epoch fence could in principle refuse OUR claim
|
|
39
|
+
if a peer holds a strictly-newer-epoch lease — but that is the correct
|
|
40
|
+
behavior (the newer-epoch holder is the current lease-holder; we must yield).
|
|
41
|
+
A same/older-epoch peer claim is fenced OUT, so we are never wrongly blocked by
|
|
42
|
+
a stale peer. No legitimate run is wrongly rejected.
|
|
43
|
+
|
|
44
|
+
2. **Under-block** — The journal-lease path relies on the replicated journal
|
|
45
|
+
actually carrying claim records between machines (`applyRemote`). This PR ships
|
|
46
|
+
the gate + the local epoch-fenced store + the cutover wiring; the journal
|
|
47
|
+
EMISSION/APPLY transport for the claim kind is the existing replication
|
|
48
|
+
substrate's responsibility and is gated behind the same flag-coherence advert
|
|
49
|
+
so an older peer that can't apply the kind keeps the WHOLE pool on the bus (it
|
|
50
|
+
never advertises `ws43JournalLease`). Until the flag is flipped on a fully
|
|
51
|
+
coherent pool, dedup behavior is IDENTICAL to today (bus). The dry-run default
|
|
52
|
+
means even a flag-on coherent pool stays on the bus while logging intended
|
|
53
|
+
claims — so under-block risk during rollout is zero (the bus is the baseline).
|
|
54
|
+
|
|
55
|
+
3. **Level-of-abstraction fit** — Correct layer. The cutover decision lives in
|
|
56
|
+
the scheduler (which owns the claim seams), the gate is a pure function (no IO,
|
|
57
|
+
independently testable), and the durable lease is its own store mirroring the
|
|
58
|
+
existing `JobClaimManager` shape. The flag-coherence read reuses the existing
|
|
59
|
+
`seamlessnessFlags` heartbeat advert + registry the WS1.1/WS4.4 gates already
|
|
60
|
+
use — no parallel coherence machinery.
|
|
61
|
+
|
|
62
|
+
4. **Signal vs authority compliance** — Compliant (see Phase 1). Structural
|
|
63
|
+
authority keyed on objective facts; fails conservative; no content heuristics;
|
|
64
|
+
cannot block a user action.
|
|
65
|
+
|
|
66
|
+
5. **Interactions** — The gate guarantees the journal lease and the bus broadcast
|
|
67
|
+
are NEVER both consulted for the same job in the same evaluation (exactly one
|
|
68
|
+
`path` per decision) — the named migration hazard. The release seam calls
|
|
69
|
+
BOTH stores' `completeClaim` idempotently (each is a no-op when this machine
|
|
70
|
+
doesn't own the slug's record), so a mid-run flag flip between claim and
|
|
71
|
+
complete cannot strand a lease. It composes cleanly with the WS4.3 role-guard
|
|
72
|
+
(a separate spawn-boundary refusal that runs BEFORE the claim seam) — the two
|
|
73
|
+
are orthogonal. No double-fire: `resolveClaimPath` is evaluated once at the
|
|
74
|
+
check seam and the result reused at the claim seam.
|
|
75
|
+
|
|
76
|
+
6. **External surfaces** — Adds one boolean field (`ws43JournalLease`) to the
|
|
77
|
+
`seamlessnessFlags` heartbeat advert. Additive — older peers omit it (read as
|
|
78
|
+
non-participant, the conservative side). No new HTTP route. No user-visible
|
|
79
|
+
message. The advert is only `true` when the flag is on AND not dry-run, so a
|
|
80
|
+
dry-run machine never advertises participation it isn't actually performing.
|
|
81
|
+
|
|
82
|
+
7. **Multi-machine posture** — This feature IS multi-machine machinery.
|
|
83
|
+
Posture: **machine-local decision fed by a proxied-on-read coherence view.**
|
|
84
|
+
Each machine independently runs the gate over the peers' advertised capability
|
|
85
|
+
(read live from the registry's heartbeat data), and the journal lease store is
|
|
86
|
+
per-machine but its records replicate via the journal. Invariant-5 flag
|
|
87
|
+
coherence is enforced: the cutover engages pool-wide ONLY when every online
|
|
88
|
+
peer advertises the capability — never a window where one machine leases while
|
|
89
|
+
a peer broadcasts. Single-machine (no peers) is a strict no-op (the gate
|
|
90
|
+
returns `bus`/`single-machine` and the journal store is never touched). The
|
|
91
|
+
epoch fence (coordinator lease epoch) carries ownership across a demotion so a
|
|
92
|
+
demoted machine's stale claim can't steal a job. Designed for N machines, no
|
|
93
|
+
2-peer assumption.
|
|
94
|
+
|
|
95
|
+
8. **Rollback cost** — Trivial. The feature is dark (flag default false). Flip
|
|
96
|
+
`multiMachine.seamlessness.ws43JournalLease` off (or leave it absent) and the
|
|
97
|
+
scheduler reverts to the legacy bus path with no migration, no data repair —
|
|
98
|
+
the journal lease store is just an inert local JSON ledger. Dry-run (default)
|
|
99
|
+
is an additional safe rung: flag on but no real cutover. The off-switch is read
|
|
100
|
+
live at each claim boundary, so a config edit + session restart fully reverts.
|
|
101
|
+
|
|
102
|
+
## Phase 5 — Second-pass review (high-risk: touches scheduler claim/dedup + a "gate")
|
|
103
|
+
|
|
104
|
+
Concern lens applied: could the cutover ever let TWO machines run the same job?
|
|
105
|
+
- Within ONE machine's evaluation, the gate returns exactly one path — verified by
|
|
106
|
+
the `NEVER returns both mechanisms` unit test and the integration/e2e
|
|
107
|
+
`never-both` assertions (coherent → journal only; mixed → bus only).
|
|
108
|
+
- ACROSS machines: the cutover engages pool-wide only under flag coherence (every
|
|
109
|
+
online peer advertises it), so the pool is either all-journal or all-bus for a
|
|
110
|
+
job set — never split. A peer that can't participate keeps everyone on the bus.
|
|
111
|
+
- A demotion race is fenced by the lease epoch (stale-epoch applyRemote rejected;
|
|
112
|
+
same/older-epoch tryClaim refused) — covered by the epoch-fence unit tests.
|
|
113
|
+
|
|
114
|
+
Concur with the review. The load-bearing invariant (no double-run across the
|
|
115
|
+
cutover; no stranded timers on a demoted machine) is structurally enforced by the
|
|
116
|
+
single-decision gate + epoch fence and is covered at all three test tiers.
|
|
117
|
+
|
|
118
|
+
## Tests
|
|
119
|
+
|
|
120
|
+
- Unit: `tests/unit/job-lease-cutover-gate.test.ts` (9), `tests/unit/
|
|
121
|
+
job-lease-claim-store.test.ts` (8) — gate decision matrix incl. never-both +
|
|
122
|
+
flag coherence + single-machine no-op; epoch fencing + durability.
|
|
123
|
+
- Integration: `tests/integration/scheduler-journal-lease-cutover.test.ts` (6) —
|
|
124
|
+
the scheduler routes through the gate; coherent→journal, mixed→bus, dry-run,
|
|
125
|
+
single-machine, remote-lease skip, live flag flip.
|
|
126
|
+
- E2E "feature is alive": `tests/e2e/scheduler-journal-lease-cutover-alive.test.ts`
|
|
127
|
+
(4) — the EXACT server.ts provider closure (real config + real coordinator
|
|
128
|
+
epoch + real MachinePoolRegistry heartbeat) drives a real triggerJob to take
|
|
129
|
+
the journal lease vs the bus; proves the wiring is live, not a stub.
|
|
130
|
+
|
|
131
|
+
All 27 new tests pass; tsc clean; no-silent-fallbacks, dark-gate line-map, and
|
|
132
|
+
feature-delivery-completeness gates pass; docs-coverage class floor improved.
|