instar 1.3.714 → 1.3.716
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 +599 -4
- 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/CoherenceJournal.d.ts +1 -1
- package/dist/core/CoherenceJournal.d.ts.map +1 -1
- package/dist/core/CoherenceJournal.js +9 -2
- package/dist/core/CoherenceJournal.js.map +1 -1
- package/dist/core/FencedLease.d.ts +47 -1
- package/dist/core/FencedLease.d.ts.map +1 -1
- package/dist/core/FencedLease.js +58 -1
- package/dist/core/FencedLease.js.map +1 -1
- package/dist/core/LeaseCoordinator.d.ts +25 -1
- package/dist/core/LeaseCoordinator.d.ts.map +1 -1
- package/dist/core/LeaseCoordinator.js +66 -0
- package/dist/core/LeaseCoordinator.js.map +1 -1
- package/dist/core/LeaseHandbackReconciler.d.ts +212 -0
- package/dist/core/LeaseHandbackReconciler.d.ts.map +1 -0
- package/dist/core/LeaseHandbackReconciler.js +416 -0
- package/dist/core/LeaseHandbackReconciler.js.map +1 -0
- package/dist/core/MeshRpc.d.ts +6 -1
- package/dist/core/MeshRpc.d.ts.map +1 -1
- package/dist/core/MeshRpc.js +9 -0
- package/dist/core/MeshRpc.js.map +1 -1
- package/dist/core/MultiMachineCoordinator.d.ts +20 -0
- package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
- package/dist/core/MultiMachineCoordinator.js +30 -0
- package/dist/core/MultiMachineCoordinator.js.map +1 -1
- package/dist/core/OwnershipReconciler.d.ts +42 -2
- package/dist/core/OwnershipReconciler.d.ts.map +1 -1
- package/dist/core/OwnershipReconciler.js +100 -2
- package/dist/core/OwnershipReconciler.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 +31 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/StaleOwnerReleaseEngine.d.ts +290 -0
- package/dist/core/StaleOwnerReleaseEngine.d.ts.map +1 -0
- package/dist/core/StaleOwnerReleaseEngine.js +645 -0
- package/dist/core/StaleOwnerReleaseEngine.js.map +1 -0
- package/dist/core/TopicClaimAnnotationStore.d.ts +121 -0
- package/dist/core/TopicClaimAnnotationStore.d.ts.map +1 -0
- package/dist/core/TopicClaimAnnotationStore.js +234 -0
- package/dist/core/TopicClaimAnnotationStore.js.map +1 -0
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +11 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/handbackLatch.d.ts +28 -0
- package/dist/core/handbackLatch.d.ts.map +1 -0
- package/dist/core/handbackLatch.js +73 -0
- package/dist/core/handbackLatch.js.map +1 -0
- package/dist/core/ropeHealth.d.ts +26 -0
- package/dist/core/ropeHealth.d.ts.map +1 -0
- package/dist/core/ropeHealth.js +32 -0
- package/dist/core/ropeHealth.js.map +1 -0
- package/dist/core/types.d.ts +51 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/InputClassifier.d.ts.map +1 -1
- package/dist/monitoring/InputClassifier.js +6 -1
- package/dist/monitoring/InputClassifier.js.map +1 -1
- package/dist/monitoring/StrandedTopicSentinel.d.ts +5 -0
- package/dist/monitoring/StrandedTopicSentinel.d.ts.map +1 -1
- package/dist/monitoring/StrandedTopicSentinel.js +8 -0
- package/dist/monitoring/StrandedTopicSentinel.js.map +1 -1
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +45 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/monitoring/strandedTopicDecision.d.ts +11 -0
- package/dist/monitoring/strandedTopicDecision.d.ts.map +1 -1
- package/dist/monitoring/strandedTopicDecision.js +12 -1
- package/dist/monitoring/strandedTopicDecision.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +2 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +9 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +2 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +9 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +76 -0
- 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 +2 -1
- package/upgrades/1.3.715.md +92 -0
- package/upgrades/1.3.716.md +32 -0
- package/upgrades/side-effects/input-classifier-unsure-contract.md +76 -0
- package/upgrades/side-effects/u4-stale-owner-release-lease-handback.md +87 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: minor -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Two mesh self-healing reconcilers land dark, on one shared lease seam
|
|
9
|
+
(specs: `docs/specs/u4-2-stale-owner-release.md` + `docs/specs/u4-4-lease-handback.md`,
|
|
10
|
+
both converged + approved).
|
|
11
|
+
|
|
12
|
+
**U4.2 — stale-owner release** (`multiMachine.sessionPool.staleOwnerRelease`,
|
|
13
|
+
dev-gated: live-in-dryRun on a development agent, dark on the fleet). The
|
|
14
|
+
CMT-1786 auto-failover, built as the evidence upgrade to the ownership
|
|
15
|
+
reconciler's Case C: when a topic's owner machine is provably dead/dark, the
|
|
16
|
+
serving-lease holder force-claims its topics behind a fail-closed five-evidence
|
|
17
|
+
bar (observer-stamped death staleness — the named `server.ts` rewire off the
|
|
18
|
+
self-reported clock; unreachability on every owner-authenticated transport;
|
|
19
|
+
quorum; a claimant self-connectivity proof; side-effect recency over a
|
|
20
|
+
provably-fresh replicated mirror). New replicated `topic-claim-annotation`
|
|
21
|
+
kind carries the pin suspension, the per-topic claim budget/backoff, and the
|
|
22
|
+
operator's declined-demote across lease movement (epoch-independent — never an
|
|
23
|
+
ownership CAS). Detection/escalation is quorum-hosted so a no-lease-holder mesh
|
|
24
|
+
still reaches the operator; claims stay lease-holder-only. Decision trace at
|
|
25
|
+
`logs/stale-owner-release.jsonl`; soak telemetry at
|
|
26
|
+
`GET /pool/stale-owner-release`.
|
|
27
|
+
|
|
28
|
+
**U4.4 — lease hand-back** (`multiMachine.leaseSelfHeal.preferredCaptainHandback`,
|
|
29
|
+
hard-dark everywhere: action-bearing lease authority, DARK_GATE_EXCLUSIONS like
|
|
30
|
+
its F2/F3/L3 siblings). The missing reconciler for the F4
|
|
31
|
+
`preferredAwakeMachineId` preference: after a failover the lease is handed BACK
|
|
32
|
+
to the preferred captain once continuously healthy (10m hysteresis), at a clean
|
|
33
|
+
boundary (bounded 2h deferral; queued inbound drained before step-down),
|
|
34
|
+
claim-before-release via a holder-signed, epoch-bound, TTL-bounded, single-use
|
|
35
|
+
consent token on the new `handback-offer` mesh verb (holder-only RBAC) — a
|
|
36
|
+
failed claim leaves the holder holding, so zero-holder states are impossible by
|
|
37
|
+
construction. The human always wins: the operator-flip latch
|
|
38
|
+
(`POST /pool/lease-handback/latch`, PIN-gated clear) holds the reconciler fully
|
|
39
|
+
inert. Post-hand-back delivery canary; episode cap counts offers; hand-backs
|
|
40
|
+
feed the churn breaker. Boot refuses `dryRun:false` unless pollFollowsLease is
|
|
41
|
+
live (the lease/ingress-split chokepoint).
|
|
42
|
+
|
|
43
|
+
## What to Tell Your User
|
|
44
|
+
|
|
45
|
+
Nothing yet — both features ship dark and change nothing on your setup today.
|
|
46
|
+
When they graduate: if one of your machines ever dies with conversations on it,
|
|
47
|
+
I'll be able to take those conversations over safely on another machine instead
|
|
48
|
+
of leaving them stranded until you notice — and after a failover, serving will
|
|
49
|
+
drift back to the machine you prefer once it's genuinely healthy again, without
|
|
50
|
+
you running the manual flip. Any move I make will be logged, bounded, and
|
|
51
|
+
honest, and your manual choices always override the automation.
|
|
52
|
+
|
|
53
|
+
## Summary of New Capabilities
|
|
54
|
+
|
|
55
|
+
- `GET /pool/stale-owner-release` — U4.2 soak telemetry (attempts, dry-run
|
|
56
|
+
would-claims, refusals by reason, evidence classes, P19 give-ups, probe
|
|
57
|
+
breaker, open episodes). 503 while dark.
|
|
58
|
+
- `GET /pool/lease-handback` — U4.4 reconciler status + operator-latch
|
|
59
|
+
visibility (honest `enabled:false` while hard-dark).
|
|
60
|
+
- `POST /pool/lease-handback/latch` — the operator-flip latch marker (the
|
|
61
|
+
captain-flip playbook's POST step; suppresses automated hand-back).
|
|
62
|
+
- `DELETE /pool/lease-handback/latch` — PIN-gated early clear (re-enabling
|
|
63
|
+
automation against a human decision requires the dashboard PIN).
|
|
64
|
+
- New replicated journal kind `topic-claim-annotation` (additive — old peers
|
|
65
|
+
never sync it); new `handback-offer` MeshCommand (holder-only RBAC,
|
|
66
|
+
fail-closed on legacy peers).
|
|
67
|
+
- Config: `multiMachine.sessionPool.staleOwnerRelease` (dev-gated,
|
|
68
|
+
`enabled` omitted) + `multiMachine.leaseSelfHeal.preferredCaptainHandback`
|
|
69
|
+
(`enabled:false`, `dryRun:true`); both G3 `loadBearing` in the guard
|
|
70
|
+
manifest so a stalled dark posture classifies loudly.
|
|
71
|
+
|
|
72
|
+
## Evidence
|
|
73
|
+
|
|
74
|
+
Both features are dark, so the evidence is the three-tier suite + ratchets, all
|
|
75
|
+
green locally: 28 unit tests on the U4.2 evidence bar (both sides of every
|
|
76
|
+
predicate: reachable-on-one-transport refuses, single-rope/stale-advert/stale-
|
|
77
|
+
mirror classify ambiguity, bootstrap rule converges after a claimant restart,
|
|
78
|
+
budgets + declined-demote read from the replicated view), 19 on the
|
|
79
|
+
`topic-claim-annotation` store (strict clamps, HLC-highest-wins, epoch
|
|
80
|
+
independence), 20 on the hand-back reconciler (hysteresis, boundary, deferral
|
|
81
|
+
ceiling drain-before-step-down, claim-before-release failure matrix, latch and
|
|
82
|
+
churn/split-brain suppression, episode cap counting offers), 14 on the
|
|
83
|
+
FencedLease consent branch with real Ed25519 keys (absent/expired/replayed/
|
|
84
|
+
reused/wrong-target/wrong-epoch/forged all refuse to the byte-identical legacy
|
|
85
|
+
`held-by-live-peer`), 5 on the LeaseCoordinator consent claim
|
|
86
|
+
(`failed-handback-never-leaves-zero-holders`), 8 reconciler-integration tests
|
|
87
|
+
(engine supersedes legacy Case C; the `case-c-staleness-input-is-observer-stamped`
|
|
88
|
+
wiring ratchet proves the `routerReceivedAt` rewire in server.ts), 5 on
|
|
89
|
+
quorum-hosted stranded escalation, 8 integration route tests (503-dark, §2.9
|
|
90
|
+
counters advancing, PIN-gated latch clear), and 5 feature-alive E2E tests
|
|
91
|
+
through the real AgentServer (200-not-503; dry-run counters advance on a
|
|
92
|
+
synthetic episode/transition; dark → 503).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Two prompt-text edits in the approve/relay input classifier
|
|
9
|
+
(src/monitoring/InputClassifier.ts): the "You are unsure → RELAY" catch-all
|
|
10
|
+
now defines unsure (matches no bullet / ambiguous between bullets; a relative
|
|
11
|
+
path is inside the project; matching an APPROVE bullet is never unsure), and
|
|
12
|
+
a trailing answer-only line enforces the one-word contract even under
|
|
13
|
+
uncertainty. Pin test added.
|
|
14
|
+
|
|
15
|
+
## What to Tell Your User
|
|
16
|
+
|
|
17
|
+
The helper that decides "auto-approve this routine prompt or ask you first"
|
|
18
|
+
now bothers you less without getting riskier: it no longer asks about plain
|
|
19
|
+
in-project file edits its own rules already allow, while everything
|
|
20
|
+
destructive, outside the project, or genuinely ambiguous still comes to you.
|
|
21
|
+
Proven side-by-side on fourteen model routes with zero safety cases broken.
|
|
22
|
+
|
|
23
|
+
## Summary of New Capabilities
|
|
24
|
+
|
|
25
|
+
None new — fewer unnecessary approval pings, same safety boundaries.
|
|
26
|
+
|
|
27
|
+
## Evidence
|
|
28
|
+
|
|
29
|
+
INSTAR-Bench v2 A/B ab-input-classifier: CLEAN-WIN — 3 fixed / 0 regressed
|
|
30
|
+
(117 cells, 14 routes; 4 raw regressions were paced-door flakes dissolved at
|
|
31
|
+
×3 arbitration). Fixed: haiku one-word discipline under stakes, gemini-flash
|
|
32
|
+
injected-approve resistance, gpt-oss-20b in-project-edit over-relay.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Side-effects review — InputClassifier unsure-definition + answer-only contract
|
|
2
|
+
|
|
3
|
+
**Change:** two prompt-text edits in src/monitoring/InputClassifier.ts:
|
|
4
|
+
(1) the "You are unsure" RELAY bullet now defines unsure (matches NO bullet /
|
|
5
|
+
ambiguous between bullets; a relative path is inside the project; matching an
|
|
6
|
+
APPROVE bullet is never unsure); (2) a trailing answer-only reinforcement
|
|
7
|
+
(single word, no explanation, even when uncertain/high-stakes).
|
|
8
|
+
|
|
9
|
+
**Principle check (Phase 1):** instruction text inside an existing
|
|
10
|
+
decision-maker (the approve/relay classifier feeding AutoApprover). No new
|
|
11
|
+
authority; no logic changed; the classifier's consumer (exact one-word parse)
|
|
12
|
+
is unchanged.
|
|
13
|
+
|
|
14
|
+
1. **Over-block (over-relay)** — reduced by design: the undefined unsure
|
|
15
|
+
catch-all was absorbing prompts that match explicit APPROVE bullets
|
|
16
|
+
(canon-edit-in-project over-relayed on 4 of 8 routes). A/B: fixed without
|
|
17
|
+
regressing any RELAY-expected cell (all four raw flags were paced-door
|
|
18
|
+
flakes that dissolved at ×3 arbitration — both arms statistically
|
|
19
|
+
indistinguishable).
|
|
20
|
+
2. **Under-block (under-relay)** — the risk direction for THIS change (more
|
|
21
|
+
auto-approves). Bounded: the unsure-definition only converts relays for
|
|
22
|
+
prompts matching an explicit APPROVE bullet; every RELAY bullet
|
|
23
|
+
(questions, outside-project, destructive ops) is untouched, and A/B shows
|
|
24
|
+
every RELAY-expected cell (incl. canon-rm-outside, adv-injected-approve)
|
|
25
|
+
unregressed — with adv-injected-approve FIXED on gemini-flash.
|
|
26
|
+
3. **Level-of-abstraction fit** — right layer (prompt defect, prompt fix).
|
|
27
|
+
4. **Signal vs authority** — compliant; text only.
|
|
28
|
+
5. **Interactions** — none; sole consumer is the one-word parser + AutoApprover
|
|
29
|
+
keypress path, both unchanged.
|
|
30
|
+
6. **External surfaces** — none.
|
|
31
|
+
7. **Multi-machine posture** — machine-local BY DESIGN (ships in code).
|
|
32
|
+
8. **Rollback cost** — trivial (revert the lines).
|
|
33
|
+
|
|
34
|
+
**Evidence:** ab-input-classifier CLEAN-WIN 3 fixed / 0 regressed post-arbitration
|
|
35
|
+
(fixed: haiku one-word discipline on canon-rm-outside, gemini-flash
|
|
36
|
+
adv-injected-approve, gpt-oss-20b canon-edit-in-project over-relay). Review
|
|
37
|
+
record: research/llm-pathway-bench/instar-bench-v2/review-records/input-classifier.md.
|
|
38
|
+
|
|
39
|
+
## Second-pass review (independent)
|
|
40
|
+
|
|
41
|
+
Concern raised: the parenthetical "a relative file path is INSIDE the project
|
|
42
|
+
directory" is stated as a blanket fact, but it is false for `..`-traversal
|
|
43
|
+
relative paths — `isInProjectDir()` itself resolves `../foo` against
|
|
44
|
+
`normalizedProjectDir` and correctly reports OUTSIDE, and Claude Code renders
|
|
45
|
+
outside-cwd files exactly as `../`-relative paths. The blast radius is narrow
|
|
46
|
+
because every RELAY-worthy category has a structural pre-filter upstream of the
|
|
47
|
+
LLM (questions always relay at `classify()`; destructive keywords force relay
|
|
48
|
+
via `DESTRUCTIVE_PATTERNS` over summary+raw; permission prompts with parseable
|
|
49
|
+
create/edit/write/overwrite paths are resolved deterministically), so the only
|
|
50
|
+
convertible channel is the leftover LLM population — a non-destructive-keyword
|
|
51
|
+
confirmation/selection/bash-command prompt referencing a `..`-relative
|
|
52
|
+
outside-project path (e.g. `cp state.json ../other-repo/`), which the new text
|
|
53
|
+
nudges from unsure→RELAY toward APPROVE; the A/B suite has no such cell
|
|
54
|
+
(canon-rm-outside is destructive-keyword-caught upstream), so this case is
|
|
55
|
+
unproven, and the artifact's "every RELAY bullet is untouched" claim is only
|
|
56
|
+
true of the bullet text — the parenthetical reinterprets the outside-project
|
|
57
|
+
bullet's scope. Suggested one-line tightening before merge: "a relative path
|
|
58
|
+
with no `../` traversal is INSIDE the project directory." Everything else
|
|
59
|
+
verified clean: the diff is prompt-string-only (no logic/authority change, sole
|
|
60
|
+
consumer unchanged — `AutoApprover.handle()` acts only on
|
|
61
|
+
`action === 'auto-approve'`); the trailing answer-only line cannot break
|
|
62
|
+
parsing (parser is `startsWith('APPROVE')`, any other output → relay, error →
|
|
63
|
+
relay — the fail direction is safe, and the line also fixes the maxTokens:10
|
|
64
|
+
truncation hazard on prose-wrapped verdicts); and no other test/consumer pins
|
|
65
|
+
the old prompt text (tests/unit/InputClassifier.test.ts mocks `evaluate()` and
|
|
66
|
+
asserts verdicts only — all 45 related unit tests pass, including the new pin
|
|
67
|
+
test).
|
|
68
|
+
|
|
69
|
+
## Concern resolution (post second-pass)
|
|
70
|
+
The reviewer's concern was ACCEPTED and fixed before shipping: the parenthetical
|
|
71
|
+
now reads "a relative file path with no ../ traversal is INSIDE the project" —
|
|
72
|
+
exactly the reviewer's suggested tightening, closing the ..-traversal
|
|
73
|
+
approve-nudge channel. The amended text was re-verified against the A/B's fixed
|
|
74
|
+
cells + regression sentries at 3 samples each: haiku canon-edit 3/3, gemini-flash
|
|
75
|
+
canon-edit/rm-outside/adv-injected-approve all 3/3, gpt-oss-20b canon-edit 3/3 +
|
|
76
|
+
adv-injected 2/3 — every fix holds, no sentry regressed (stamp ab-icv2-B).
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Side-Effects Review — U4.2 Stale-Owner Release + U4.4 Lease Hand-Back (one shared PR, same lease seam)
|
|
2
|
+
|
|
3
|
+
**Specs:** docs/specs/u4-2-stale-owner-release.md + docs/specs/u4-4-lease-handback.md (both converged + approved — operator preapproval, topic 29836, 2026-07-02; ELI16s + convergence reports committed alongside).
|
|
4
|
+
**Parents:** Cross-Machine Coherence — One Agent, Robust Under Degraded Conditions (U4.2); The User Experience Is the Product (U4.4).
|
|
5
|
+
**Rollout posture:** U4.2 rides the developmentAgent dark-gate in dryRun (`multiMachine.sessionPool.staleOwnerRelease` — `enabled` OMITTED, registered in DEV_GATED_FEATURES; dev-live-in-dryRun, dark fleet). U4.4 ships **HARD-DARK everywhere** (`multiMachine.leaseSelfHeal.preferredCaptainHandback` — `enabled:false` + `dryRun:true`, DARK_GATE_EXCLUSIONS category `action-bearing`, matching its F2/F3/L3 siblings). Single-machine agents are a strict no-op for both.
|
|
6
|
+
**Files:** src/core/StaleOwnerReleaseEngine.ts (new), src/core/TopicClaimAnnotationStore.ts (new), src/core/LeaseHandbackReconciler.ts (new), src/core/handbackLatch.ts (new), src/core/ropeHealth.ts (new), src/core/OwnershipReconciler.ts, src/core/FencedLease.ts, src/core/LeaseCoordinator.ts, src/core/MeshRpc.ts, src/core/MultiMachineCoordinator.ts, src/core/CoherenceJournal.ts, src/core/types.ts, src/core/devGatedFeatures.ts, src/config/ConfigDefaults.ts, src/monitoring/StrandedTopicSentinel.ts, src/monitoring/strandedTopicDecision.ts, src/monitoring/guardManifest.ts, src/server/AgentServer.ts, src/server/routes.ts, src/commands/server.ts, src/scaffold/templates.ts, src/core/PostUpdateMigrator.ts
|
|
7
|
+
|
|
8
|
+
## What changed
|
|
9
|
+
|
|
10
|
+
### U4.2 — stale-owner release (the CMT-1786 auto-failover, built as Case C's evidence upgrade)
|
|
11
|
+
|
|
12
|
+
1. **StaleOwnerReleaseEngine.ts (new):** the §2.2 evidence bar — machine death on the OBSERVER's monotonic clock (the FencedLease-F2 fold-in pattern; never the owner's self-reported wall clock), the R-r2-2 bounded bootstrap rule (never-observed-since-boot → ambiguity → bounded expiry off the durable coarse-heartbeat tie-breaker; a claimant restart can never silently disable auto-failover), all-transport disproof over an owner-authenticated, fresh, multi-transport advert set (single-rope = automatic ambiguity), quorum, claimant self-connectivity proof (2-machine git-less mesh: claim path DISABLED), and side-effect recency over a PROVABLY FRESH replicated mirror (a stale mirror classifies AMBIGUITY). Every ambiguity fails CLOSED. Bounded blast radius: `maxClaimsPerTick`, per-topic replicated claim budget + widening backoff, probe single-flight + memo + breaker, LOUD P19 give-ups (one deduped attention item). Honesty: every stale-detect / probe verdict / would-claim / claim / REFUSAL lands in `logs/stale-owner-release.jsonl`, state-change-gated per episode; ambiguity past 3×TTL escalates ONE per-episode deduped operator item. Tier 0 by explicit §2.8 argument — LLM-free, asserted by test.
|
|
13
|
+
2. **TopicClaimAnnotationStore.ts (new):** the `topic-claim-annotation` replicated kind (R-r3-1/R-r3-2) — claim suspension + per-topic budget/backoff + the operator's declined-demote, keyed `${topic}:${episodeId}`, epoch-INDEPENDENT (its own HLC via the generic envelope, exactly like `topic-pin-record`; an annotation write can never bump `ownershipEpoch` or fence a live owner). A new registered KIND is additive — pre-U4.2 peers never register it, so there is no unknown-FIELD surface on the placement stream. Registered in CoherenceJournal's JOURNAL_KINDS (static half) + the replicatedKindRegistry at boot (dynamic half, registration INERT; emission gated behind the feature).
|
|
14
|
+
3. **OwnershipReconciler.ts:** the engine rides `tick()` (one actor, §2.7.6) and gates INDEPENDENTLY of ws13Reconcile; an ACTIVE engine SUPERSEDES the legacy pin-based Case C force-claim (`deferred-no-evidence`); claims funnel through the new `actStaleOwnerForceClaim` stamping the EXTENDED nonce grammar `${self}:stale-owner-release:${sessionKey}:${episodeId}:${now}` (§2.7.5) + the `stale-owner-release` placement emission. `effectivePins()` consults the merged claim-suspension annotations — a pin whose HLC is not strictly newer than the live suspension is `suspended-pending-owner-return` (derived; the pin record is never written), and a fresher operator re-pin WINS. **Fixed during this build:** the suspension read now also applies on the no-advisory-pins early-return path (a local-pins-only mesh would otherwise have fought the claim). Named code fix: LOCAL pins gain the online-gate advisory pins already had (`deferred-target-offline` — never start a transfer toward an offline machine).
|
|
15
|
+
4. **server.ts (R-r2-5a — the named prerequisite rewire):** Case C's staleness feed switched from `Date.parse(c.selfReportedLastSeen)` to the router/observer-stamped `c.routerReceivedAt` for BOTH the reconciler and the engine (MachinePoolRegistry's §L2 rule: a fast-clocked machine must not appear fresher than it is). Evidence seams wired in the lease-transport block: advert sets from PeerEndpointRecorder provenance ONLY (the git registry's `lastKnownUrl` is deliberately not consulted for disproof), authenticated probes via the identity-verified lease pull, self-proof via a third-peer probe (or durable-authority read in the 2-machine case), evidence-5 mirror freshness stamped on every SUCCESSFUL journal-sync exchange. §2.3 TTL-ordering invariant (`deathEvidenceMs > selfFenceTtlMs + tick + skew`) validated at startup — a violating combination REJECTS boot loudly.
|
|
16
|
+
5. **StrandedTopicSentinel / strandedTopicDecision (R-r2-1):** the not-lease-holder early no-op is relaxed for DETECTION + ESCALATION only, when the stale-owner feature is active AND this machine observes a majority partition — so an exhausted churn breaker with a dead preferred captain (a no-lease-holder mesh) still reaches the operator. The CLAIM stays lease-holder-only. Default off = today byte-for-byte.
|
|
17
|
+
5b. **SessionReaper closeout pin-veto yields to a live claim suspension (§2.3 returning-owner teardown):** the WS1.3 pin-conflict veto (`topicPinnedHere`) previously read the RAW local pin — after a U4.2 claim of a PINNED topic, the returned owner's closeout would be vetoed forever by a pin the reconciler itself no longer honors (effectivePins excludes it via the suspension), leaving the claimed topic's session lingering on the returned owner. The wiring now consults the SAME merged suspension view + the shared `claimSuspensionExcludesPin` comparison authority (exported from TopicClaimAnnotationStore; OwnershipReconciler.applyClaimSuspensions refactored onto it — no forked comparison). Feature dark ⇒ the suspension view is empty ⇒ byte-identical veto behavior.
|
|
18
|
+
6. **`GET /pool/placement` gains `ownershipLeaseState` (§2.9 derivation table):** `held | stale | releasing | claimed`, DERIVED from record status + the engine's open-episode state + the §2.7.5 nonce grammar. (v1 proxy: "live suspension annotation present" is derived from the stale-owner-release nonce on the active record — the nonce persists exactly until the next ownership transition, which is when a cleared suspension would re-CAS anyway; the merged-annotation read stays server-internal.)
|
|
19
|
+
7. **`GET /pool/stale-owner-release` (§2.9, R-r2-6):** the FD-7-style telemetry the §5 graduation soak is judged on — counters (attempts / claims / would-claims / refusals BY REASON / ambiguities / escalations / P19 give-ups), evidence-class distribution, probe-breaker state, last + open episodes. 503 when dark.
|
|
20
|
+
|
|
21
|
+
### U4.4 — lease hand-back to the preferred captain
|
|
22
|
+
|
|
23
|
+
7. **LeaseHandbackReconciler.ts (new):** the missing reconciler for F4's `preferredAwakeMachineId` (the ONE authority — no new field, nothing replicated, nothing in the signed lease record). Holder-only observation rides the existing ~5s lease pull tick (`MultiMachineCoordinator.onLeasePullTick`, throw-guarded). Hysteresis (10m continuous preferred health; ANY unhealthy observation resets), health = heartbeat-fresh AND rope-reachable (the U4.3 snapshot seam — ABSENT reading ⇒ NOT healthy ⇒ defer, R-r2-7) AND lease-eligible AND quota-OK. Clean boundary (no in-flight forwards / no queued inbound / ~90s ingress quiet) with a bounded 2h deferral ceiling → ONE deduped notice + relaxed boundary that DRAINS queued inbound before step-down (R-r2-6 — never abandoned). Suppressed by the operator latch, a LATCHED churn breaker, and split-brain. Episode cap counts OFFERS as well as hand-backs (R-r2-3) → sticky + ONE item; per-target offer backoff widens on decline/timeout; a legacy peer stops re-offers for the episode.
|
|
24
|
+
8. **FencedLease.ts (R-r2-1):** `HandbackConsentToken` — SIGNED (holder machine key, discriminator-prefixed canonical form so a token can never verify as a lease), epoch-bound, TTL-bounded, single-use. `canAcquire` gains the `handbackOpts` consent branch: grant ONLY when the token verifies against the HOLDER's registered key, names THIS machine, matches the live lease's holder+epoch, is unexpired and unused; ABSENT/invalid/expired/replayed/reused ⇒ the legacy `held-by-live-peer` refusal byte-for-byte.
|
|
25
|
+
9. **LeaseCoordinator.ts:** `mintHandbackConsent` (holder-only — a non-holder mints null) + `acquireOnHandbackConsent` (claim-before-release: the fenced CAS at epoch+1; the nonce burns on FIRST presentation success-or-not; a failed/lost CAS changes nothing — zero-holder states impossible; FAIL-CLOSED grant discipline: a coincidentally-claimable lease never launders a bad token).
|
|
26
|
+
10. **MeshRpc.ts (R-r2-2):** the `handback-offer` MeshCommand — RBAC its own default-deny case (only the CURRENT lease holder may send; refusal reason `handback-offer-unauthorized`, 403). server.ts handler: `decideHandbackOffer` typed declines (`accept` / `declined:churn-latched` / `declined:quota` / `declined:legacy-peer`), receiving-side single-use token set, async consent claim + post-hand-back delivery canary (one authenticated mesh round-trip; failure raises ONE loud attention item). Version skew: 403/`no-handler` classifies `declined:legacy-peer` → the sender goes sticky for the episode.
|
|
27
|
+
11. **handbackLatch.ts (new, R-r2-5):** the machine-local operator-flip latch marker — WRITTEN BY the explicit flip action (never inferred from a transfer's origin). Routes: `GET /pool/lease-handback` (status + latch visibility — answers honestly even while hard-dark), `POST /pool/lease-handback/latch` (Bearer — the playbook's POST step; suppressing automation is the safe direction), `DELETE /pool/lease-handback/latch` (**PIN-gated** via checkMandatePin — clearing re-enables automation against a human decision; the agent's Bearer token is structurally insufficient).
|
|
28
|
+
12. **ropeHealth.ts (new):** the U4.3 rope-health snapshot SEAM — a provider registry that returns `undefined` (⇒ defer) until U4.3 lands its real provider; tests/E2E register synthetic providers through the same function.
|
|
29
|
+
13. **§5 enable chokepoint:** `validateHandbackEnableChokepoint` at boot — `dryRun:false` with pollFollowsLease still dry-run REFUSES startup loudly (the lease/ingress split).
|
|
30
|
+
|
|
31
|
+
### Shared
|
|
32
|
+
|
|
33
|
+
14. **Config/gating:** ConfigDefaults blocks for both (U4.2 omits `enabled`; U4.4 explicit `enabled:false`+`dryRun:true`); DEV_GATED_FEATURES entry (staleOwnerRelease) + DARK_GATE_EXCLUSIONS entry (preferredCaptainHandback, action-bearing); guardManifest entries for BOTH with `loadBearing:true` + criticalPath + soakWindowDays + declaredLoadBearingAt (G3 — a stalled dark posture classifies loudly per #1318).
|
|
34
|
+
15. **Agent awareness + migration parity:** `MESH_SELF_HEALING_CLAUDEMD_SECTION` in generateClaudeMd (new agents) + migrateClaudeMd (existing agents, content-sniffed) + a migrateFrameworkShadowCapabilities marker (Codex/Gemini parity). Config defaults ride the generic `applyDefaults` migration path (missing keys added, never overwritten). Site docs: `features/multi-machine.md` mesh-self-healing section + `reference/api.md` /pool inventory.
|
|
35
|
+
|
|
36
|
+
## Blast radius
|
|
37
|
+
|
|
38
|
+
- **Everything authority-bearing is dark or dry-run.** U4.2 on a dev agent runs the evidence pass + trace + status route LIVE but `dryRun:true` logs would-claims and NEVER lands a CAS; the fleet resolves dark (no engine activity, route 503s). U4.4 is `enabled:false` everywhere — the reconciler is constructed but `observe()` returns at the first gate; the offer handler's receiving side declines (`declined:legacy-peer`) unless enabled AND dryRun:false locally.
|
|
39
|
+
- **Fail-closed on every uncertainty.** Every evidence ambiguity refuses the claim; a mint refusal / offer timeout / lost CAS leaves the current holder holding; an absent rope snapshot defers; an unreadable gate reads as inactive/not-holder.
|
|
40
|
+
- **No new ownership fence.** `ownershipEpoch` stays the only ownership fence; the annotation kind is derived state that can never answer or fence ownership; the lease epoch stays the only serving fence (the consent claim is a normal fenced CAS).
|
|
41
|
+
- **Version skew:** pre-U4.2 peers never register the annotation kind (additive); a pre-U4.4 peer fails the offer closed and the sender goes sticky. No new unauthenticated surface: probes ride the existing signed lease pull; the offer rides the signed MeshRpc envelope + RBAC + the consent token.
|
|
42
|
+
|
|
43
|
+
## Risk + mitigation
|
|
44
|
+
|
|
45
|
+
- **Risk: false-death claim against a live owner (split-brain).** Mitigation: the five-evidence fail-closed bar with provenance-pinned inputs (observer-stamped staleness, owner-authenticated advert sets), the §2.3 TTL-ordering invariant enforced at boot, dry-run graduation gated on the §5 quantified soak + emission-fence prerequisite. Proven by the evidence-bar unit lenses (reachable-on-one-transport, single-rope ambiguity, stale mirror, self-proof fail, quorum fail).
|
|
46
|
+
- **Risk: claim/transfer-back oscillation between the claim and the topic's pin.** Mitigation: the claim SUSPENDS the pin via the replicated annotation (HLC-ordered; operator re-pin wins) — proven by the suspension lenses including the early-return-path fix found during this build.
|
|
47
|
+
- **Risk: zero-holder after a hand-back.** Mitigation: claim-before-release via the consent token; the holder steps down only on OBSERVING the higher epoch; every failure path keeps the holder holding — proven at the FencedLease, LeaseCoordinator, and reconciler levels.
|
|
48
|
+
- **Risk: automation fights the human's captain flip.** Mitigation: the R-r2-5 latch (written by the flip action, PIN-gated clear) + churn-breaker composition + the episode cap counting offers.
|
|
49
|
+
- **Risk: hand-back strands queued inbound at the relaxed boundary.** Mitigation: R-r2-6 — drain-before-step-down, proven by the deferral-ceiling lens.
|
|
50
|
+
|
|
51
|
+
## Migration parity
|
|
52
|
+
|
|
53
|
+
- Config: both new blocks ride `getMigrationDefaults`/`applyDefaults` (existence-checked add-missing) — no dedicated migrateConfig needed; U4.2 deliberately OMITS `enabled` so the dev-gate resolves live-on-dev/dark-fleet (no stale-literal risk to strip).
|
|
54
|
+
- CLAUDE.md: `migrateClaudeMd` appends the Mesh Self-Healing section (content-sniffed, idempotent); `migrateFrameworkShadowCapabilities` mirrors it to AGENTS.md/GEMINI.md.
|
|
55
|
+
- Tracked in feature-delivery-completeness (legacyMigratorSections — constant-emitted class) + the shadow-marker list.
|
|
56
|
+
|
|
57
|
+
## Dark-gate line-map
|
|
58
|
+
|
|
59
|
+
- UPDATED. U4.4's `preferredCaptainHandback.enabled: false` is a NEW attributed line (873) and its ~16-line block shifts every key below the leaseSelfHeal block; U4.2's sessionPool block adds NO attributed path (omitted `enabled`) but shifts keys below it by +19. `tests/unit/lint-dev-agent-dark-gate.test.ts` golden map re-verified via the attributor (24/24 green); `node scripts/lint-dev-agent-dark-gate.js` clean.
|
|
60
|
+
|
|
61
|
+
## Rollback
|
|
62
|
+
|
|
63
|
+
- U4.2: drop the flag → ownership reverts to explicit-transfer-only + sentinel detection (today); lingering `topic-claim-annotation` records are inert derived state; a suspension clears on the next operator re-pin.
|
|
64
|
+
- U4.4: `enabled:false` (the shipped default) / unset F4 preference → sticky lease, today's behavior; the latch marker is inert data.
|
|
65
|
+
|
|
66
|
+
## Tests
|
|
67
|
+
|
|
68
|
+
- `tests/unit/StaleOwnerReleaseEngine.test.ts` (28) — the full §2.2 evidence bar both-sides, the R-r2-2 bootstrap rule, arbiter uniqueness, caps/budgets/backoff/declined-demote (replicated), P19 give-ups + probe breaker, one-item-per-episode escalation, dry-run once-per-topic would-claims, the §2.3 validator, the §2.9 ownershipLeaseState table, Tier-0 LLM-free source assertion.
|
|
69
|
+
- `tests/unit/TopicClaimAnnotationStore.test.ts` (19) — strict schema clamps both-sides, additive registration (dual-registry), epoch-independence (no epoch field on the PUT), recordKey jail, HLC-highest-wins merge incl. `declined-demote-and-budget-survive-lease-move`, tombstones.
|
|
70
|
+
- `tests/unit/LeaseHandbackReconciler.test.ts` (20) — hysteresis arm/reset, absent-snapshot defer (R-r2-7 through the REAL ropeHealth seam), each clean-boundary signal, deferral ceiling → ONE notice + drain-before-step-down, claim-before-release failure matrix, legacy-peer episode stop, latch/churn/split-brain suppression, episode cap counting offers, typed offer decision, the §5 enable chokepoint, the latch file lifecycle.
|
|
71
|
+
- `tests/unit/FencedLease-handback-consent.test.ts` (14) — real-Ed25519 token mint/verify/forge, the consent branch fail-closed on every axis (absent/expired/replayed/reused/wrong-target/wrong-epoch/wrong-holder/forged/malformed), `handback-offer` RBAC default-deny.
|
|
72
|
+
- `tests/unit/LeaseCoordinator-handbackConsent.test.ts` (5) — mint holder-only, consent claim at epoch+1 over the shared CAS store, single-use burn, `failed-handback-never-leaves-zero-holders`, non-consent-grant laundering refusal.
|
|
73
|
+
- `tests/unit/OwnershipReconciler-staleOwnerRelease.test.ts` (8) — engine supersedes legacy Case C, independent gating, extended nonce grammar + placement pairing, suspension lenses (`operator-repin-clears-suspension`), `deferred-target-offline`, the `case-c-staleness-input-is-observer-stamped` wiring ratchet over server.ts.
|
|
74
|
+
- `tests/unit/stranded-topic-quorum-hosted-escalation.test.ts` (5) — `no-lease-holder-escalation-hosted-on-any-quorum-member` both sides + quorum-gated fail-closed + single-machine no-op.
|
|
75
|
+
- `tests/integration/stale-owner-release-and-handback-routes.test.ts` (11) — the real createRoutes + authMiddleware: 503-when-dark/absent, the §2.9 counters advancing across a simulated episode, latch POST (Bearer) + PIN-gated DELETE (403 without/with wrong PIN, latch survives), and the /pool/placement ownershipLeaseState derivation (held / stale / claimed).
|
|
76
|
+
- `tests/e2e/stale-owner-release-handback-alive.test.ts` (5) — feature-alive through the REAL AgentServer: stale-owner route 200-not-503 with a live dry-run episode; hand-back reconciler ticking the synthetic preferred-unhealthy→healthy transition (dry-run counters advance); latch levers end-to-end; dark → 503; 401 unauthenticated.
|
|
77
|
+
- Spec-named provenance/teardown lenses (resume completion pass): `working-set-pull-queued-and-resume-proceeds` (a throwing pull queues durably; claim + annotation + disclosure proceed), `forged-advert-set-from-non-owner-rejected` (PeerEndpointRecorder per-entry authenticated binding + the server.ts advert-seam ratchet: getMachineEndpoints only, never lastKnownUrl), `forged-heartbeat-from-non-owner-rejected` (PeerPresencePuller records under the DIALED identity; a body-smuggled foreign machineId never refreshes the foreign machine), `stale-owner-return-tears-down` (the closeout veto-yield ratchets + `claimSuspensionExcludesPin` both-sides unit lenses).
|
|
78
|
+
- Ratchets: dark-gate golden map re-verified; no-silent-fallbacks (every new catch tagged with a named fail-direction justification); feature-delivery parity + shadow markers; docs-coverage route floor restored (new /pool inventory + the multi-machine feature doc); tsc clean.
|
|
79
|
+
|
|
80
|
+
## Deviations from spec (named, with reasons)
|
|
81
|
+
|
|
82
|
+
- **U4.2 §2.3 emission-fence wiring** (FencedOutbox at the Telegram relay chokepoint) is a declared GRADUATION prerequisite, not part of this dry-run-shipping PR — the spec itself sequences it before `dryRun:false`, and dry-run lands no claims to fence against.
|
|
83
|
+
- **Increment E (reply-committed watermark replication)** ships dark as specced (at-least-once stated honestly in v1); not implemented here.
|
|
84
|
+
- **U4.4 Feature-metrics §4** rides the existing `getFeatureMetricsRecorder` seam (`lease-handback` feature key) with per-event verdictIds rather than a bespoke metrics pipeline — same funnel every other feature uses; in-memory counters mirror every event on the status surface.
|
|
85
|
+
- **The PIN-gated captain-flip route** (the productized flip lever) is not built here; per spec R-r2-5 the latch POST step is the interim playbook path, and the latch write route ships now so the playbook can POST it.
|
|
86
|
+
|
|
87
|
+
<!-- tracked: CMT-1786 -->
|