instar 1.3.747 → 1.3.749
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 +136 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/core/HttpLeaseTransport.d.ts +22 -0
- package/dist/core/HttpLeaseTransport.d.ts.map +1 -1
- package/dist/core/HttpLeaseTransport.js +37 -3
- package/dist/core/HttpLeaseTransport.js.map +1 -1
- package/dist/core/LeaseCoordinator.d.ts +22 -0
- package/dist/core/LeaseCoordinator.d.ts.map +1 -1
- package/dist/core/LeaseCoordinator.js +11 -0
- package/dist/core/LeaseCoordinator.js.map +1 -1
- package/dist/core/MachinePoolRegistry.d.ts +11 -0
- package/dist/core/MachinePoolRegistry.d.ts.map +1 -1
- package/dist/core/MachinePoolRegistry.js +29 -1
- package/dist/core/MachinePoolRegistry.js.map +1 -1
- package/dist/core/PeerPresencePuller.d.ts +26 -1
- package/dist/core/PeerPresencePuller.d.ts.map +1 -1
- package/dist/core/PeerPresencePuller.js +15 -1
- package/dist/core/PeerPresencePuller.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +16 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +80 -5
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +16 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/conversationBindGate.d.ts +62 -0
- package/dist/core/conversationBindGate.d.ts.map +1 -0
- package/dist/core/conversationBindGate.js +83 -0
- package/dist/core/conversationBindGate.js.map +1 -0
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +12 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/machineCoherenceAdvert.d.ts +108 -0
- package/dist/core/machineCoherenceAdvert.d.ts.map +1 -0
- package/dist/core/machineCoherenceAdvert.js +160 -0
- package/dist/core/machineCoherenceAdvert.js.map +1 -0
- package/dist/core/machineCoherenceManifest.d.ts +133 -0
- package/dist/core/machineCoherenceManifest.d.ts.map +1 -0
- package/dist/core/machineCoherenceManifest.js +293 -0
- package/dist/core/machineCoherenceManifest.js.map +1 -0
- package/dist/core/types.d.ts +21 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/CommitmentTracker.d.ts.map +1 -1
- package/dist/monitoring/CommitmentTracker.js +4 -1
- package/dist/monitoring/CommitmentTracker.js.map +1 -1
- package/dist/monitoring/MachineCoherenceSentinel.d.ts +240 -0
- package/dist/monitoring/MachineCoherenceSentinel.d.ts.map +1 -0
- package/dist/monitoring/MachineCoherenceSentinel.js +336 -0
- package/dist/monitoring/MachineCoherenceSentinel.js.map +1 -0
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +25 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/monitoring/machineCoherenceEpisode.d.ts +152 -0
- package/dist/monitoring/machineCoherenceEpisode.d.ts.map +1 -0
- package/dist/monitoring/machineCoherenceEpisode.js +92 -0
- package/dist/monitoring/machineCoherenceEpisode.js.map +1 -0
- package/dist/monitoring/machineCoherenceEpisodeManager.d.ts +210 -0
- package/dist/monitoring/machineCoherenceEpisodeManager.d.ts.map +1 -0
- package/dist/monitoring/machineCoherenceEpisodeManager.js +724 -0
- package/dist/monitoring/machineCoherenceEpisodeManager.js.map +1 -0
- package/dist/monitoring/machineCoherenceEvaluate.d.ts +129 -0
- package/dist/monitoring/machineCoherenceEvaluate.d.ts.map +1 -0
- package/dist/monitoring/machineCoherenceEvaluate.js +183 -0
- package/dist/monitoring/machineCoherenceEvaluate.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +7 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +1 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +2 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +141 -88
- 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 +7 -1
- package/upgrades/{1.3.747.md → 1.3.748.md} +16 -0
- package/upgrades/1.3.749.md +35 -0
- package/upgrades/side-effects/action-claim-dryrun-audit-mkdir-fix.md +45 -0
- package/upgrades/side-effects/machine-coherence-guard.md +229 -0
- package/upgrades/side-effects/slack-followthrough-generalization.md +49 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
A new multi-machine safety guard that answers "are my machines running as the same me?". When an agent runs across more than one machine, the guard compares — across its OWN online machines, riding the existing 30-second presence-pull (no new timer) — the coherence-critical dimensions: instar version, resolved safety-flags, mesh protocol version, and manifest generation. When the pool DIVERGES on something that halves a cross-machine guarantee (for example, the conversation-move pair enabled on one machine and dark on the other), exactly ONE elected machine opens a deduped, episode-scoped attention item.
|
|
9
|
+
|
|
10
|
+
The guard is pure signal — it never blocks, equalizes, or restarts anything on its own. This increment lands the full detection + episode lifecycle:
|
|
11
|
+
|
|
12
|
+
- Manifest-driven skew detection across the four dimensions, with confirmation counters (a flapping reading never accumulates) and update-wave suppression (a rolling version update is grace-gated so it does not cry wolf).
|
|
13
|
+
- The episode state machine: open, join, suspend/resume (a machine that goes offline or unreadable holds the episode open honestly), and a close taxonomy where only a genuine restoration is ever reported as restored.
|
|
14
|
+
- Bounded notification surface: a per-day item cap and a shared per-episode append budget so a flapping pool can never flood the operator, giving up loudly rather than going silent.
|
|
15
|
+
- The proposed-remedy lifecycle: the item names a concrete change the agent performs on the operator's one-word approval, or holds the episode open without nagging when the operator declines — gated on the topic's verified operator (an unverified sender can never approve).
|
|
16
|
+
- A read-only status surface at `GET /pool/machine-coherence`.
|
|
17
|
+
|
|
18
|
+
Rollout posture: dev-gated dark on the fleet (`monitoring.machineCoherence.enabled` is omitted so the dev-agent gate decides), dry-run FIRST even on a development agent (the detector runs and records would-raise, but raises no item until a deliberate `dryRun:false`), and a strict no-op on a single-machine agent.
|
|
19
|
+
|
|
20
|
+
## What to Tell Your User
|
|
21
|
+
|
|
22
|
+
- **Multi-machine coherence**: "When I run on more than one of your machines, I now watch whether they are running as the same me — same version, same safety settings. If they drift apart in a way that would silently break moving a conversation between them, I raise one clear heads-up and offer to fix it, rather than letting the two quietly disagree."
|
|
23
|
+
- **No noise by default**: "This is experimental and off on your fleet — it only runs on a development agent, in observe-only mode, until it has soaked."
|
|
24
|
+
|
|
25
|
+
## Summary of New Capabilities
|
|
26
|
+
|
|
27
|
+
| Capability | How to Use |
|
|
28
|
+
|-----------|-----------|
|
|
29
|
+
| Machine-coherence status | `GET /pool/machine-coherence` (503 when the guard is dark on this agent) |
|
|
30
|
+
| Skew alarm + proposed fix | Automatic — one elected machine raises one HIGH episode item naming the fix; reply "fix it" / "leave it" |
|
|
31
|
+
| Transition log | `logs/machine-coherence.jsonl` (transition-only) |
|
|
32
|
+
|
|
33
|
+
## Evidence
|
|
34
|
+
|
|
35
|
+
Not reproducible in dev — this is a NEW capability (a multi-machine coherence detector + its episode/alarm machinery), not a bug fix, so there is no prior broken behavior to reproduce. It ships dev-gated dark on the fleet and dry-run FIRST on a development agent, so it changes no existing behavior. Verification is by the layered test suite: unit tests over the manifest, advert transport, evaluator, confirmation engine, episode state machine (open/join/suspend/resume/close taxonomy), the shared append-budget burst invariant, and the pendingFix approval lifecycle; a Tier-2 "feature-alive" integration test over `GET /pool/machine-coherence` (503 when dark, 200 with a live sentinel that has opened an episode); and a migration-parity test for the CLAUDE.md awareness backfill.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Side-effects review — action-claim dryRun audit mkdir fix
|
|
2
|
+
|
|
3
|
+
## Change
|
|
4
|
+
|
|
5
|
+
`src/server/routes.ts`, `POST /action-claim/observe` dry-run branch (minted-id
|
|
6
|
+
Slack lane, §8.1): add `fs.mkdirSync(path.dirname(logPath), { recursive: true })`
|
|
7
|
+
immediately before the best-effort `fs.appendFileSync` that writes the
|
|
8
|
+
`would-register` audit line to `logs/action-claim-observe.jsonl`.
|
|
9
|
+
|
|
10
|
+
## Files touched
|
|
11
|
+
|
|
12
|
+
- `src/server/routes.ts` (one added line inside the existing dry-run `try` block)
|
|
13
|
+
|
|
14
|
+
## Side-effects analysis
|
|
15
|
+
|
|
16
|
+
- **Filesystem**: now creates the `logs/` directory if absent (idempotent,
|
|
17
|
+
`recursive: true`). Previously the append silently failed via the swallowing
|
|
18
|
+
`catch` when the directory did not exist. No new files beyond the audit log
|
|
19
|
+
the feature already intended to write.
|
|
20
|
+
- **Response contract**: unchanged. The route returns the identical
|
|
21
|
+
`{ observed: true, registered: false, dryRun: true, wouldRegister: true, ... }`
|
|
22
|
+
body. No status-code change; never-500 guarantee preserved (the `mkdirSync`
|
|
23
|
+
is inside the same `try/catch`).
|
|
24
|
+
- **Live path**: untouched — the non-dry-run branch calls `record()` and never
|
|
25
|
+
reaches this code.
|
|
26
|
+
- **Migration / config / hooks**: none. No config keys, no settings, no CLAUDE.md
|
|
27
|
+
template surface, no operator-facing change.
|
|
28
|
+
- **Security**: none. Directory is created under the agent's own state tree with
|
|
29
|
+
default permissions, same as the sibling audit writers.
|
|
30
|
+
- **Concurrency**: `mkdirSync(..., { recursive: true })` is safe to call
|
|
31
|
+
repeatedly and from concurrent requests (no-op when the dir exists).
|
|
32
|
+
|
|
33
|
+
## Test impact
|
|
34
|
+
|
|
35
|
+
Fixes `tests/integration/action-claim-route.test.ts` → "dryRun → would-register
|
|
36
|
+
audit line, NO row", which failed in clean CI environments because
|
|
37
|
+
`<os-tmpdir>/logs/` did not pre-exist. No test was modified or skipped; the code
|
|
38
|
+
was fixed to satisfy the existing assertion.
|
|
39
|
+
|
|
40
|
+
## Causal autopsy
|
|
41
|
+
|
|
42
|
+
Origin: new-code. The dry-run audit-write branch was introduced by this same PR
|
|
43
|
+
(slack-followthrough-generalization) and shipped with the missing `mkdirSync`.
|
|
44
|
+
The failure is a latent bug in that new code, masked locally by a leaked
|
|
45
|
+
`logs/` directory in the OS temp dir.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Side-Effects Review — Machine-Coherence Guard (roadmap 4.1, F4/P0-1)
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/specs/machine-coherence-guard.md (CONVERGED round 6 — 0 CRITICAL / 0 MAJOR / 0 MINOR; `review-convergence: 2026-07-03`; `approved: true` under the standing Session-A operator preapproval, topic 29836). **ELI16:** docs/specs/machine-coherence-guard.eli16.md. **Parent principle:** Cross-Machine Coherence — One Agent, Robust Under Degraded Conditions. **Constitution:** Cross-Store Coherence Is an Invariant; A Dark Feature Guards Nothing; Bounded Notification Surface; Structure beats Willpower; Agent Proposes, Operator Approves.
|
|
4
|
+
|
|
5
|
+
**What this build delivers (three things, per the spec's opening):**
|
|
6
|
+
1. The **machine-coherence guard** — a manifest-driven skew detector over version / resolved-flag / protocol / manifest-generation, riding the existing 30s presence-pull, raising ONE deduped episode-scoped attention item from exactly ONE elected machine. Signal-only.
|
|
7
|
+
2. The **`awakeMachineCount` telemetry fix** (§5, ships live — corrects a lying surface, not a new behavior).
|
|
8
|
+
3. Updater coordination is Phase 2 (§8) — designed at sketch level, explicitly NOT built.
|
|
9
|
+
|
|
10
|
+
**Rollout posture:** the EVALUATOR + episode/alarm machinery are dev-gated dark-on-fleet / live-on-dev, dryRun-FIRST (`monitoring.machineCoherence`, `enabled` OMITTED from ConfigDefaults so `resolveDevAgentGate` decides — the #1001 anti-mechanism). The §5 bug-fixes AND the §3.2 advert EMISSION ship LIVE unconditionally (a gated advert would make the guard misdiagnose the F4 topology itself — §3.2 rationale + a pinned unit test).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Build increments (committed early + often per the ~1.5h/fragile-quota directive)
|
|
15
|
+
|
|
16
|
+
### Increment A — the coherence-critical manifest (§3.1) — LANDED
|
|
17
|
+
|
|
18
|
+
**What changed:**
|
|
19
|
+
1. **`src/core/machineCoherenceManifest.ts` (NEW, ~360 lines)** — the pure, deterministic manifest:
|
|
20
|
+
- `CoherenceCriticalFlag` interface + `COHERENCE_CRITICAL_FLAGS` (the §3.1 table): the F4 pair (ws13PinReplicate + ws13Reconcile), ws43JournalLease, ws44PoolLinks/PoolCache, the 7 WS2 stateSync stores, pollFollowsLease, sessionPool.stage (live-read, M8), exactlyOnceIngress (resolved), meshTransport.enabled, developmentAgent, and the guard's OWN posture row (N2).
|
|
21
|
+
- `resolveFlagValue` — deterministic effective-value resolution per `resolution` mode (raw / dev-gate / dev-gate+dryRun) reading each entry the way its real consumer does (`readSource: boot|live`, M8) via a `CoherenceConfigView { boot, liveGet }`. Values clamped to the §3.1 alphabet.
|
|
22
|
+
- `computeManifestHash` / `selfManifestHash` — sha256 over sorted ENTRIES (key+resolution+readSource, M7), 64 lowercase hex.
|
|
23
|
+
- `COHERENCE_MANIFEST_EXCLUSIONS` — the explicit membership-drift exclusion list (every `multiMachine.*` DEV_GATED_FEATURES entry not in the manifest, each with a one-line reason).
|
|
24
|
+
- The §3.1 clamp + byte-bound constants (`MC_MAX_ENTRIES`, `MC_FLAGS_BYTES_MAX`, `MC_MARKER_BYTES_MAX`, `MC_BLOCK_BYTES_MAX`, `MC_MARKER_ROWS_MAX`, format regexes).
|
|
25
|
+
2. **`tests/unit/machine-coherence-manifest.test.ts` (NEW, 21 tests)** — membership + hash determinism; the **N5 size-ratchet** (entry cap + the reference advert INCLUDING a worst-case 72-row alarm marker within the byte budgets, R3-N1); the **N5 membership-drift guard** (every `multiMachine.*` dev-gated flag is in the manifest OR explicitly excluded — a new coherence-relevant flag added without a decision fails HERE); resolution semantics both sides (dev-gate live/off, the F4 omitted-flag case, dryRun fold, force-dark false, live-read via `liveGet` M8, exactlyOnceIngress derivation, pollFollowsLease raw fold).
|
|
26
|
+
|
|
27
|
+
**Blast radius:** pure module, zero runtime wiring yet — nothing imports it in production until increment B/C. The unit test is the only consumer. No config, no route, no behavior change on the fleet.
|
|
28
|
+
|
|
29
|
+
### Increment D₁ — version telemetry populated (§5a) — LANDED
|
|
30
|
+
|
|
31
|
+
**What changed:** `src/commands/server.ts` — the sole `captureHardware()` callsite (~:17458) now passes `ProcessIntegrity.getInstance()?.runningVersion`. When ProcessIntegrity is unavailable the argument is `undefined` (OMITTED), so `hardware.instarVersion` stays honestly absent (L1) — never a possibly-stale `config.version`.
|
|
32
|
+
|
|
33
|
+
**Blast radius:** ships LIVE (a bug-fix — correcting a field that was structurally always `undefined`, not a new behavior). Retroactively activates the already-written consumer at `src/server/routes.ts:6645/6671` (the peer-version annotation on `/guards?scope=pool` failure rows). Additive: `hardware.instarVersion` was optional and always undefined before; now populated on machines running this version. No shape change (that is §5b's D5, still pending).
|
|
34
|
+
|
|
35
|
+
### Increment B — advert transport (§3.2) — LANDED
|
|
36
|
+
|
|
37
|
+
**What changed:**
|
|
38
|
+
1. **`src/core/machineCoherenceAdvert.ts` (NEW)** — the §3.2 advert block:
|
|
39
|
+
- `CoherenceAdvert` / `CoherenceAlarmMarker` / `CoherenceAdvertRejection` types (instarVersion, protocolVersion, manifestHash, guard posture N2, FORENSIC-ONLY beatSeq R2-L1, manifest-resolved flags, optional alarm marker R2-M1/M2).
|
|
40
|
+
- `buildCoherenceAdvert` — pure builder over the increment-A manifest (`buildCoherenceFlags` + `selfManifestHash` + `selfProtocolVersion`); `resolveSelfGuardPosture` maps the manifest's own-posture row to `live | dry-run | dark`.
|
|
41
|
+
- `clampCoherenceAdvert` — the **M4 receive clamp (NEW BUILD WORK per spec)** with the R5-N3 format clamps: version alphabet, 64-lowercase-hex manifestHash, guard enum, numeric protocol/beatSeq, flag key/value alphabets + entry cap, the §3.1 byte budgets measured on the REBUILT serialization (R4-L4). Failure directions per spec: malformed ADVERT → named rejection (rejected ≠ absent); malformed alarm MARKER (episodeId format R3-N9 / row-hash format) → marker dropped with named reason, advert STANDS; >72-row marker → truncated + `rowsTruncated` (honesty only — truncation never grants coverage, R3-M4).
|
|
42
|
+
2. **`src/core/types.ts`** — `MachineCapacity` gains `coherenceAdvert` + `coherenceAdvertReceivedAt` (receiver-stamped, M5) + `coherenceAdvertRejected` (M4), siblings to `seamlessnessFlags`/`guardPosture`.
|
|
43
|
+
3. **`src/core/MachinePoolRegistry.ts`** — `HeartbeatObservation` gains both fields; `recordHeartbeat` tracks advert receipt SEPARATELY (the posture pattern): an advert-carrying beat stamps a fresh receipt + clears any standing rejection; a rejection-carrying beat REPLACES the advert for evaluation (last-good retained internally for forensics only); a sparse beat carries BOTH forward unchanged — carry-forward can never impersonate freshness (M5). `assemble` exposes rejection-over-advert.
|
|
44
|
+
4. **`src/core/PeerPresencePuller.ts`** — `PeerCapacity` gains both fields; `coherenceAdvert` added to `SESSION_STATUS_ADVERT_FIELDS` (the ratchet covers the narrowing from day one); `narrowSessionStatusToPeerCapacity` applies the clamp AT the narrowing step (the spec's designated clamp point); the **R2-N1 second enumeration** closed — `pullOnce`'s hand-maintained `recordHeartbeat` spread + its deps type carry both fields (the #930-class 5th-instance guard).
|
|
45
|
+
5. **`src/commands/server.ts`** — `refreshPool` emits the advert on EVERY self beat, **UNCONDITIONALLY (M3, normative)** — no dev-gate on emission; `beatSeq` closure counter; `instarVersion` from `ProcessIntegrity.runningVersion`; `liveGet` wired to `liveConfig.get` so `readSource:'live'` entries (sessionPool.stage, exactlyOnceIngress) re-advertise within one beat of a PATCH /config with no restart (M8). Builder faults degrade to an advert-less beat (never a dead heartbeat). Alarm marker is increment C's to attach (no episode machinery exists yet).
|
|
46
|
+
6. **Tests (green):** `tests/unit/machine-coherence-advert.test.ts` (NEW, 22 tests — M3 fleet-config emission, guard-posture ladder, clamp accept/reject matrix incl. marker-drop + truncation, registry receipt/carry-forward/rejection-replace semantics) + `tests/unit/peer-presence-puller.test.ts` extended (ratchet fixture carries a clean advert; R2-N1 spread tests both directions; M4 narrowing clamp pass/reject).
|
|
47
|
+
|
|
48
|
+
**Blast radius:** the advert is an ADDITIVE optional heartbeat field (the same additive-advert path every prior field took — peers without the code ignore it). Ships LIVE by design (M3): every machine on this version emits ~1–2 KB more per 30s self-beat and stores peers' clamped adverts in the in-memory registry. No route, no alarm, no evaluator yet — nothing CONSUMES the advert until increment C, so fleet behavior is unchanged beyond the heartbeat payload. Rejection markers are data-at-rest only until C classifies them.
|
|
49
|
+
|
|
50
|
+
### Increment C₀ — evaluator PURE helpers (§3.3 classification + N1 identity) — LANDED
|
|
51
|
+
|
|
52
|
+
**What changed:** `src/monitoring/machineCoherenceEvaluate.ts` (NEW, pure, zero wiring — the increment-A pattern): `classifyPeer` (compared / unknown / advert-stale / advert-rejected, each class's pinned handling; rejection wins over a co-existing advert; an unparseable receipt time degrades — never trusts), `skewRowIdentity` (the N1 canonical `dimension|key|sorted(machineId=valueClass)` key that confirmation counters, episode membership, the §4.5 damper, and the latches all key on), `rowIdentityHash` (the §3.2 marker wire format — first 16 lowercase hex of sha256; content-free), `classifyVersionSkew` (none / patch-only-grace-gated / major-minor; an unparseable-but-differing version takes the QUIETER grace path — never cries wolf), `electRaiser` (§3.4: lease-holder-if-candidate else lowest machineId; zero candidates → null; pure over shared inputs so every machine computes the same result). Tier 0 supervision (N6) noted in-module. 24 unit tests (`tests/unit/machine-coherence-evaluate.test.ts`) — both sides of every boundary incl. the at-bound staleness edge and the no-mutation election property.
|
|
53
|
+
|
|
54
|
+
**Blast radius:** pure module; the tests are the only consumer until the sentinel (C₁) composes it.
|
|
55
|
+
|
|
56
|
+
### Increment C₁a — sentinel evaluator core: config + gates + classification tick + election — LANDED
|
|
57
|
+
|
|
58
|
+
**What changed:**
|
|
59
|
+
1. **`src/monitoring/MachineCoherenceSentinel.ts` (NEW)** — the evaluator core composing the C₀ pure helpers:
|
|
60
|
+
- `resolveMachineCoherenceConfig` — the full §7 config surface with code-side `??` defaults; `enabled` resolves through `resolveDevAgentGate` (OMITTED from ConfigDefaults — the #1001 anti-mechanism; explicit value wins); `dryRun` defaults TRUE (dry-run FIRST even on dev). `selfPostureOf` → live/dry-run/dark.
|
|
61
|
+
- `tick()` — rides the caller's cadence (the 30s peerPresenceTick, wiring pending): single-machine STRICT no-op (short-circuits below 2 online members BEFORE touching state), per-machine classification via `classifyPeer` (M11 universe honesty — every online machine accounted), §3.4 election via `electRaiser` (self candidacy from LOCAL resolved config — authoritative over its own advert echo; peers from their adverts' `guard`), fail-toward-silence (any error → counter, no emit). `inWarmup()` exposes the N8 window for the Session-B confirmation counters.
|
|
62
|
+
- `status()` — the §6 snapshot core (enabled/dryRun/lastTickAt/universe counts/classification counts/raiser/openEpisode:null/counters).
|
|
63
|
+
2. **`src/core/devGatedFeatures.ts`** — `machineCoherence` registered (`monitoring.machineCoherence.enabled`), auto-covered by the both-sides wiring test (145 green). Justification: signal-only, Tier 0, no spend/egress, dry-run-first, single-machine no-op.
|
|
64
|
+
3. **`src/monitoring/guardManifest.ts`** — GUARD_MANIFEST entry (`monitoring.machineCoherence.enabled`, `component: 'MachineCoherenceSentinel'`, NOT loadBearing per D6, `expectRuntime: false` FOR NOW — C₁b adds the server-boot construction + `guardRegistry.register` callsite and flips it to true; the lint-guard-manifest classification standard required the entry the moment the guard-shaped component existed).
|
|
65
|
+
4. **Tests:** `tests/unit/MachineCoherenceSentinel.test.ts` (NEW, 13) — gate ladder both sides + explicit-wins, §7 defaults + overrides, single-machine no-op, offline exclusion, M11 classification counts, holder-vs-standby SAME-raiser property, dry-run self/peer non-candidacy, fail-toward-silence, warm-up window, openEpisode-never-fabricated.
|
|
66
|
+
|
|
67
|
+
**Blast radius:** pure module + one registry data entry; NOTHING constructs the sentinel in production yet (server wiring is C₁b). Fleet behavior unchanged.
|
|
68
|
+
|
|
69
|
+
### Increment C₁b-i — server-boot wiring + peerPresenceTick rider — LANDED
|
|
70
|
+
|
|
71
|
+
**What changed:**
|
|
72
|
+
1. **`src/monitoring/MachineCoherenceSentinel.ts`** — added `guardStatus()`: the synchronous GuardRegistry runtime getter (`{ enabled, dryRun, lastTickAt }`, GUARD-POSTURE-ENDPOINT-SPEC §2.1). `lastTickAt` is 0 before the first tick so a constructed-but-never-ticking guard reads `on-stale`, never "on".
|
|
73
|
+
2. **`src/commands/server.ts`** — server-boot construction on the mesh peer-presence path (right before the `peerPresenceTick` closure, where `machinePoolRegistry` / `meshSelfId` / `leaseCoordinatorRef` / `guardRegistry` are all in scope, mirroring the StrandedTopicSentinel exemplar): constructs the sentinel ONLY when `resolveMachineCoherenceConfig(config).enabled` (dark guard → never constructed → route 503s, no tick), wires deps (`listCapacities`→pool registry, `selfMachineId`→`meshSelfId`, `leaseHolderMachineId`→`leaseCoordinatorRef.currentHolder()` else null, `now`→`Date.now`), `guardRegistry.register('monitoring.machineCoherence.enabled', …guardStatus())`, and a §6 boot line (`enabled dryRun=… manifestHash=… flags=…`). The `peerPresenceTick` closure now calls `machineCoherenceSentinel?.tick()` inside its own try/catch (fail toward silence) — the 30s rider, no new timer. Own try/catch around construction: a wiring failure is logged non-fatal (never crashes boot).
|
|
74
|
+
3. **`src/monitoring/guardManifest.ts`** — flipped the machineCoherence entry `expectRuntime: false → true` (the register callsite now exists). Comment pins the safety: `missing` requires `configEnabled === true` (guardPostureView precedence), so a dark FLEET agent (gate → false) never constructs, never registers, and is never falsely graded `missing` — the ws13Reconcile/holdForStability `expectRuntime:true` precedent.
|
|
75
|
+
4. **Tests:** `tests/unit/MachineCoherenceSentinel.test.ts` +3 (guardStatus shape + GuardRuntimeStatus conformance; lastTickAt 0→NOW after a tick; dev dry-run-first / fleet-dark posture mirror) = 16. `tests/unit/lint-guard-manifest.test.ts` +3 (single classification, `expectRuntime:true` + fleet-default-false + 30s cadence + not-loadBearing, real lint stays clean) = 17. The both-sides dev-gate wiring test (`devGatedFeatures-wiring.test.ts`) already auto-covers the machineCoherence entry (145 green).
|
|
76
|
+
|
|
77
|
+
**Blast radius:** dev-agent only (gate live-on-dev / dark-on-fleet). On a dev agent with mesh, the sentinel now constructs, registers in `/guards` (graded `on-dry-run` while `dryRun:true`), and ticks every 30s — classification + election only (no confirmation, no episode, no item yet; those are C₁b-ii onward). Fleet + single-machine-non-mesh agents unchanged (never constructed).
|
|
78
|
+
|
|
79
|
+
### Increment C₁b-ii — dimension comparison + confirmation counters (§3.3 R2-L3 + M6) — LANDED
|
|
80
|
+
|
|
81
|
+
**What changed:**
|
|
82
|
+
1. **`src/monitoring/machineCoherenceEvaluate.ts`** — `SkewRow` interface + `computeDivergentRows(compared)`: the PURE per-tick dimension comparator. Emits one `SkewRow` per divergent dimension across the COMPARED machines: **flag** (manifest-INTERSECTION keys only — a key on one side alone is version skew, not flag skew), **version** (`instarVersion` not all equal; severity = `major-minor` if ANY pair differs in major.minor else `patch-only`), **manifest** (`manifestHash` differs WHILE every `instarVersion` is identical — M7 same-version dirty-dist), **protocol** (`protocolVersion` differs). Each row's `identity` is the N1 `skewRowIdentity` (content-free: ids + clamped value classes) so two evaluators over the same shared adverts compute the SAME identity (the §3.4 duplicate-reconciliation / marker-key convergence property). `clampValueClass` clamps every scalar to `[a-z0-9-]{≤32}`.
|
|
83
|
+
2. **`src/monitoring/MachineCoherenceSentinel.ts`** — the §3.3 confirmation engine wired into `tick()` (`updateConfirmation(nowMs)` after the election):
|
|
84
|
+
- Per-row `RowConfirmState` map keyed on the N1 identity (`consecutiveTicks`, `firstSeenAtMs` grace clock, `confirmed`).
|
|
85
|
+
- **R2-L3 consecutive rule**: a row's counter increments ONLY when its identity was present LAST tick (`lastTickRowIds`); a row whose identity was absent (re)starts at 1 with a fresh grace clock. Because a participant dropping out (offline/unknown/stale) or a pair equalizing either vanishes or changes the identity, one flapping reading can never accumulate toward confirmation. Rows absent this tick are dropped.
|
|
86
|
+
- **Confirmation predicate**: patch-only version rows confirm on the `versionSkewGraceMs` CLOCK (continuous skew ≥ 45 min); every other dimension (flag / major-minor version / manifest-class / protocol) confirms at `flagConfirmTicks` (default 2) consecutive ticks.
|
|
87
|
+
- **M6 update-wave suppression**: FLAG rows are filtered out while ANY `instarVersion` skew is present among the compared machines (an update that shifts a flag's resolved default would else alarm HIGH mid-wave and auto-resolve); once versions agree, residual flag skew confirms normally.
|
|
88
|
+
- **Fail-toward-silence hardening**: an error tick leaves the confirmation state INTACT (a transient pool-read fault must not fabricate a participant-drop reset); the single-machine no-op clears the whole engine (below 2 members, no divergence is possible).
|
|
89
|
+
- `confirmedSkewRows()` / `pendingSkewRows()` pure reads (Session B's episode machinery + the status route consume these); `status().counters` gains `skewsConfirmed` (cumulative transitions), `confirmedRows` + `pendingRows` (live gauges).
|
|
90
|
+
3. **Tests (green):** `tests/unit/machine-coherence-evaluate.test.ts` +9 (`computeDivergentRows` describe — <2 no-op, all-agree, flag row = skewRowIdentity, intersection-only flags, version severities both sides, manifest-only-when-versions-identical, protocol, N1 determinism); `tests/unit/MachineCoherenceSentinel.test.ts` +5 confirmation-engine (2-tick flag confirm, R2-L3 flap reset, M6 suppress-then-confirm, patch-only grace-clock, below-2 clears) + the two counter-shape updates. 54 targeted tests green; `tsc --noEmit` clean.
|
|
91
|
+
|
|
92
|
+
**Blast radius:** dev-agent only (the sentinel is only constructed on a live-gate dev agent from C₁b-i). Adds per-tick divergence classification + confirmation accounting to the 30s rider — still SIGNAL-ONLY (no episode, no attention item, no alarm marker; those are C₁b-iii onward). Fleet + single-machine unchanged (never constructed).
|
|
93
|
+
|
|
94
|
+
### Increment C₁b-iii-a — episode DURABLE state layer (§4.1 + §4.6) — LANDED
|
|
95
|
+
|
|
96
|
+
**What changed:**
|
|
97
|
+
1. **`src/monitoring/machineCoherenceEpisode.ts` (NEW, pure types + fs primitives)** — the durable state layer the §4 state machine consumes (the established pure-module-first rhythm; nothing raises/transitions here):
|
|
98
|
+
- Types faithful to §4.1–§4.6: `EpisodeState` (episodeId, openedAtMs, skewRowIdentities N1 set, itemRaisedAt R4-M1, attentionItemId, predecessorEpisodeId R2-M2, durable `suspended`/`suspendReason`, durable `operatorAck` R4-N2, `pendingFix`, `escalationAppended`, `reopenCount`), `PendingFix` (the three-state `proposed`/`approved-holding`/`executing-verifying` lifecycle R3-M6, proposal hash+message-id AUTHORITY, verify-clock anchors + `accumulatedSuspendedMs` R5-L1), `RecurrenceBlock` (the R2-N2 sibling that OUTLIVES episode close — newItemTimestamps per-day cap, recentlyClosed reopen-window memory, reopenLatch, shared appendBudget), `EpisodeCloseReason` (the §4.3 taxonomy — only `restored` claims restoration), `EpisodeFile` on-disk shape.
|
|
99
|
+
- `mintEpisodeId(openedAtMs)` → `mc-<ms>` (N4); `episodeStatePath(stateDir)` → `<stateDir>/state/machine-coherence-episode.json` (N7 per-agent, never global); `emptyRecurrence()`.
|
|
100
|
+
- `readEpisodeFile(stateDir)` → `{ status: 'absent' | 'ok' | 'corrupt' }` — distinguishes missing (fresh) from structurally-invalid (§4.6 re-baseline gate, the GuardPostureProbe pattern): bad JSON / wrong version / missing-or-malformed shape returns a NAMED corrupt reason, never a throw and never a silent `{}`.
|
|
101
|
+
- `writeEpisodeFile(stateDir, file)` — atomic tmp+rename mirroring `writeConfigAtomic` (`BootSelfKnowledge.ts:112`); creates the `state/` subdir; callers write on TRANSITIONS only (R2-N3).
|
|
102
|
+
2. **Tests (green):** `tests/unit/machine-coherence-episode.test.ts` (NEW, 12) — episodeId + path shape, absent/ok/corrupt matrix (invalid-json, bad-version, missing-recurrence, episode-shape, recurrence-shape), between-episodes round-trip (episode:null + persisted recurrence), atomic write (subdir creation, no tmp leftover, last-writer-wins). 12 green; `tsc --noEmit` clean.
|
|
103
|
+
|
|
104
|
+
**Blast radius:** pure module + its test; NOTHING wires it in production yet (the state machine that reads/writes it is the next sub-unit). Fleet + single-machine unchanged.
|
|
105
|
+
|
|
106
|
+
### Increment C₁b-iii-b1 — episode STATE MACHINE core + §4.2 verbatim item render — LANDED
|
|
107
|
+
|
|
108
|
+
**What changed:**
|
|
109
|
+
1. **`src/monitoring/machineCoherenceEpisodeManager.ts` (NEW)** — `MachineCoherenceEpisodeManager`, the §4 state machine consuming the C₁b-iii-a durable layer + the confirmation engine's confirmed rows. `reconcile(input)` drives the lifecycle and returns EFFECTS the caller executes (the manager does no telegram I/O — only its own durable file + jsonl):
|
|
110
|
+
- **Open** (§4.1) on the first confirmed row; **join** newly-confirmed rows into the OPEN episode with one append (never a 2nd item — the named anti-pattern).
|
|
111
|
+
- **Suspend/resume** (§4.3): a skew participant leaving the VERIFIABLE set suspends (`peer-offline` when it drops offline, `peer-unverifiable` when online-but-not-compared) with an honest append; resume is silent (same item), then re-evaluates.
|
|
112
|
+
- **Close taxonomy** (§4.3): `restored` (skew clear for `resolveTicks`; ONLY this claims restoration, note names the held ticks), `expired-peer-gone` (`expireIfStale`, suspended past `suspendedEpisodeExpiryMs`), `manifest-changed` (a flag key retired from the manifest intersection). Every close recorded in the recurrence memory (R2-N2, outlives close).
|
|
113
|
+
- **§4.4 escalation**: one append past `escalateAfterMs` (unsuspended), suppressed by the durable operator **"leave it"** ack (`setOperatorAck`, R4-N2).
|
|
114
|
+
- **Effect gating**: raise/append/resolve emit ONLY when `enabled && !dryRun && raiser === self` (`speaks()`); dry-run + non-raiser run the full machine + jsonl + `wouldRaise` counter, never speak.
|
|
115
|
+
- **§4.6 corrupt re-baseline** on construction (the GuardPostureProbe pattern — bad file → fresh baseline + a `rebaseline` jsonl row, never a crash; drops any pendingFix R3-L3).
|
|
116
|
+
- **§4.2 VERBATIM body render** (`renderBody`, pure): impact-first (manifest `guarantee` per row, by nickname), the approve-to-execute fix with the direction ALWAYS named (§4.2.1-ii — pool-majority else lease-holder), the two divergent-machine cases word-for-word (self+lease-holder → the named-failover clause; any-other-machine → "from my own hands there"), the "leave it" line, and the technical block last. Peer strings (nicknames, value classes) rendered as data.
|
|
117
|
+
- `logs/machine-coherence.jsonl` transition-only, byte-cap safety rotation on append.
|
|
118
|
+
2. **Tests (green):** `tests/unit/machine-coherence-episode-manager.test.ts` (NEW, 16) — open + raiser/live/dry-run/non-raiser gating, join, suspend both reasons, resume→restored, restored-only-claims-restoration, escalation once + ack suppression, expired-peer-gone, corrupt re-baseline, both verbatim body branches. 16 green; `tsc --noEmit` clean.
|
|
119
|
+
|
|
120
|
+
**Blast radius:** NEW module + its test; NOT wired into the sentinel/server yet (the sentinel-tick wiring + refreshPool alarm-marker attach + the status route are the wiring slice). Fleet + single-machine unchanged.
|
|
121
|
+
|
|
122
|
+
**Documented partials (honest, not silent):**
|
|
123
|
+
- The precise **30-day time-based** jsonl prune (SessionWatchdog `rotateLog` shape) is deferred to the wiring slice as a periodic call on the sentinel cadence; this slice uses the byte-cap `maybeRotateJsonl` safety rotation (bounded growth — the real hazard) on append.
|
|
124
|
+
- `expireIfStale` uses `openedAtMs` as the suspended-since anchor (no explicit suspend-start timestamp in this slice); the precise suspend-start anchor + suspended-time accumulator land with the §4.5 recurrence slice (b2).
|
|
125
|
+
- §4.6 corrupt-path **adopt-or-resolve** of a locally-held stale item is handled minimally (fresh episode opens a new item id; a stale different-id item is left for operator ack — a bounded duplicate inside §0(b)'s envelope); the full adopt-or-resolve rides the wiring slice where the live attention store is in scope.
|
|
126
|
+
|
|
127
|
+
### Increment C₁b-iii-b2 — §4.5 recurrence damper + per-day cap + shared append budget — LANDED
|
|
128
|
+
|
|
129
|
+
**What changed:**
|
|
130
|
+
1. **`src/monitoring/machineCoherenceEpisode.ts`** — widened `RecurrenceBlock` with the damper's durable bookkeeping: `recentlyClosed[].itemId?` (so a reopen reuses the SAME item/topic), `appendBudget.reservedSuspendResumeAtMs?` (R4-L6 reserved slot), `capGiveupAtMs?` (once-per-24h give-up). All ride the same atomic file + §4.6 corrupt handling.
|
|
131
|
+
2. **`src/monitoring/machineCoherenceEpisodeManager.ts`** — the §4.5 brakes on the budget-exempt HIGH path (M2):
|
|
132
|
+
- **Reopen damper**: a newly-confirmed skew whose N1 set intersects a `recentlyClosed` entry within `reopenWindowMs` RE-OPENS it — same item un-resolved + one "this divergence is back — re-opening" append, NO new item, and it does NOT count toward the per-day cap. `reopenCount` carried.
|
|
133
|
+
- **Per-day cap**: at most `maxEpisodeItemsPerDay` NEW items per rolling 24h (`newItemTimestamps`); past the cap, further episodes are jsonl-only + counted, and ONE give-up note fires (once per window) — "coherence is flapping faster than I'll alarm … see /pool/machine-coherence" (P19 give-up-loudly).
|
|
134
|
+
- **Shared per-episode append budget (R3-M5)**: ALL intra-episode FLAP-class appends (row-join, suspend/resume) share ONE rolling `episodeAppendBudget` per `episodeAppendWindowMs` — `pushFlapAppend` bounds them to `budget + 1` (the one "flapping — recording silently" note), then jsonl-only until the rolling count falls back below budget (latch exit, R4-N3/L7). ONE slot is RESERVED per window for the first suspend/resume (R4-L6 — the clock-changing note never crowded out). Structural appends (escalation, cap give-up) do NOT ride this budget.
|
|
135
|
+
- **Latched-flapping reopen mode**: after `flappingLatchReopens` re-opens in the window → latched (one note, then jsonl-only); the window resets after `reopenWindowMs`.
|
|
136
|
+
- Rolling-window eviction is LAZY (`pruneRecurrence`, R3-L2 — never triggers a write on its own).
|
|
137
|
+
3. **Tests (green):** `tests/unit/machine-coherence-episode-manager.test.ts` +4 (now 20) — per-day cap + once-loud give-up, reopen reuses the SAME item id (no new item, cap-exempt), the burst invariant (10 flap transitions → exactly `budget + 1` appends), the reserved suspend/resume slot surviving a spent budget. 20 green; `tsc --noEmit` clean.
|
|
138
|
+
|
|
139
|
+
**Blast radius:** additive to the (still-unwired) EpisodeManager + its durable shape; NOTHING constructs it in production yet. Fleet + single-machine unchanged.
|
|
140
|
+
|
|
141
|
+
**Documented partial:** the precise suspend-since anchor + suspended-time accumulator (§4.2.1-v executing-verifying clock pause) land with the pendingFix slice (b3), which introduces the executing-verifying state those clocks belong to; this slice's `expireIfStale` still uses `openedAtMs` as the suspend anchor (noted in b1).
|
|
142
|
+
|
|
143
|
+
### Increment C₁b-iii-b4 — sentinel wiring + effect execution + status route (the feature is ALIVE) — LANDED
|
|
144
|
+
|
|
145
|
+
**What changed:**
|
|
146
|
+
1. **`src/monitoring/MachineCoherenceSentinel.ts`** — the sentinel now OWNS the EpisodeManager when a `stateDir` dep is provided (absent in pure unit tests → classification-only, as before). New deps `stateDir?()` + `nicknameOf?()`. In `tick()`, after confirmation + election (and once past the N8 warm-up), it assembles the reconcile input (confirmedRows + compared/online sets + self/raiser/leaseHolder + nicknameOf) and calls `reconcile` + `expireIfStale`, queuing the returned effects. `drainPendingEffects()` returns + clears them (keeps the tick sync + Tier-0 — the caller executes the telegram I/O). `status()` now surfaces the real `openEpisode` + `episodeCounters`. `setOperatorAck` passthrough (for the b3 reply path).
|
|
147
|
+
2. **`src/monitoring/machineCoherenceEpisodeManager.ts`** — jsonl path corrected to the agent-root `logs/` dir (`stateDir/../logs`, matching the house convention) from the b1 `stateDir/logs`.
|
|
148
|
+
3. **`src/commands/server.ts`** — the C₁b-i sentinel construction now passes `stateDir: () => config.stateDir` + `nicknameOf` (from `machinePoolRegistry` capacities). After `tick()`, the `peerPresenceTick` drains the effects and executes them ASYNC + best-effort: `raise` → `telegram.createAttentionItem` (id `machine-coherence:<episodeId>`, HIGH, category `machine-coherence`), `append` → `telegram.getAttentionItem(id).topicId` + `sendToTopic`, `resolve` → topic note + `updateAttentionStatus(id,'DONE')`. A telegram fault never crashes the shared tick (fail toward silence). Module-level `_machineCoherenceSentinel` exposes it to the route; wired into the AgentServer ctx as `getMachineCoherence`.
|
|
149
|
+
4. **`src/server/AgentServer.ts` + `src/server/routes.ts`** — threaded `getMachineCoherence` through the options → routeCtx → RoutesContext, and registered **`GET /pool/machine-coherence`**: 503 when the guard is dark (getter null — dev-gated, never constructed on the fleet), else the §6 `status()` snapshot (enabled/dryRun/universe counts/classifications/raiser/openEpisode/counters + episodeCounters).
|
|
150
|
+
5. **Tests (green):** `tests/unit/MachineCoherenceSentinel.test.ts` +2 wiring-integrity (a stateDir-wired sentinel drives the EpisodeManager and `drainPendingEffects` returns a REAL raise + `openEpisode` populated; without a stateDir it's classification-only, empty drain) = 23. `tests/integration/machine-coherence-route.test.ts` (NEW, 2 — the "feature-alive" Tier-2: 503 dark, 200 with a live sentinel that opened an episode). 295 across the coherence + wiring + route sweep green; `tsc --noEmit` clean.
|
|
151
|
+
|
|
152
|
+
**Blast radius:** the guard is now CONSTRUCTED + TICKING + RAISING on a live-gate dev agent (dryRun defaults TRUE → runs the full machine + `wouldRaise` counters, raises NO item until a deliberate `dryRun:false`); the route serves its status. Fleet: `enabled` gate → false → never constructed → route 503s → unchanged. Single-machine: `tick()` strict no-op → no episode.
|
|
153
|
+
|
|
154
|
+
### Increment C₁b-iii-b3-core — §4.2.1 pendingFix STATE MACHINE — LANDED
|
|
155
|
+
|
|
156
|
+
**What changed:**
|
|
157
|
+
1. **`src/monitoring/machineCoherenceEpisodeManager.ts`** — the §4.2.1 fix lifecycle (the spec's "ONLY action in this build"):
|
|
158
|
+
- **Propose** (`maybeProposeFix`, on open): records the FIRST auto-proposable row as a `proposed` pendingFix — rowIdentity/key/targetMachineId (§4.2.1-ii direction: pool-majority else lease-holder)/targetValue + a `proposalHash` (sha256 over `episodeId|key|target|value` — the display-integrity authority). Cardinality: ONE at a time (R3-N8).
|
|
159
|
+
- **Exclusions** (§4.2.1-iii): a version/manifest/protocol row (no config override) and the two root classes (`developmentAgent`, `monitoring.machineCoherence`) are NEVER auto-proposed — `renderBody` renders a MANUAL decision block instead ("this one I won't touch on my own … do nothing until you say").
|
|
160
|
+
- **Approve** (`approveFix`, uid-gated): the caller passes `verifiedOperator` (Know Your Principal — the reply path verifies the sender is the topic's verified operator) + the `proposalHash`. Refuses an unverified sender (`not-verified-operator`), a lapsed hash (`proposal-lapsed`), or a second approval in flight (`already-in-flight`, single-flight R4-N4). On success: `targetMachine === self` → `executing-verifying` + an `execute-fix` effect (the LOCAL config write + self-restart the caller runs, §4.2.1-iv mechanized); else → `approved-holding` + an honest "from my own hands on <nickname>" append (§4.2.1-iv held).
|
|
161
|
+
- **Invalidation** (§4.2.1-i): a skew-set change (row join) or suspension INVALIDATES a proposed/approved-holding fix (an `executing-verifying` fix is exempt — its durable write already happened, R5-N2); suspension of an approved-holding fix emits the named "the fix you approved is paused" note.
|
|
162
|
+
- **Verify/failure** (§4.2.1-v): an `executing-verifying` fix whose row hasn't cleared within `fixVerifyTicks` fires ONE loud "the fix didn't take" append + clears (retry needs fresh approval); the episode STAYS OPEN (closure belongs to §4.3 alone). A row that clears silently drops the fix.
|
|
163
|
+
- New `execute-fix` EpisodeEffect + `FixApprovalResult`; `status().openEpisode.pendingFix` + `currentProposalHash()` surface the proposal.
|
|
164
|
+
2. **`src/monitoring/MachineCoherenceSentinel.ts`** — `approveFix` passthrough (dark/unwired guard → `guard-not-active` refusal) + the `openEpisode.pendingFix` shape on the status snapshot.
|
|
165
|
+
3. **Tests (green):** `tests/unit/machine-coherence-episode-manager.test.ts` +10 (now 30) — propose/exclusions, manual block, Know-Your-Principal refusal, lapsed-hash refusal, both approval directions, single-flight, suspend-invalidation, verify-failure-episode-stays-open. 30 green; `tsc` clean; existing sentinel/route tests unaffected.
|
|
166
|
+
|
|
167
|
+
**Blast radius:** additive to the (wired) EpisodeManager. The approve/execute path is only reachable when the guard SPEAKS (raiser && live && `dryRun:false`) — dev-gated dark + dry-run-first inheritance holds. NO route yet consumes `approveFix` and NO config write/restart executes yet — that is b3-wiring (next). Fleet + single-machine unchanged.
|
|
168
|
+
|
|
169
|
+
### Increment E-part1 — Agent Awareness + Migration Parity for the status route — LANDED
|
|
170
|
+
|
|
171
|
+
**What changed:**
|
|
172
|
+
1. **`src/scaffold/templates.ts` (`generateClaudeMd`)** — a "Machine-Coherence Guard" awareness section (dev-gated-dark noted, maturity-tagged ⚗️): what the guard does, the `GET /pool/machine-coherence` read surface (503-when-dark honesty), and the proactive trigger ("why did I get a machine-coherence alarm?" / "are my machines in sync?"). NEW installs get it.
|
|
173
|
+
2. **`src/core/PostUpdateMigrator.ts` (`migrateClaudeMd`)** — the content-sniffed (`!content.includes('Machine-Coherence Guard')`), idempotent backfill so EXISTING agents receive the same section on update (Migration Parity — a feature that only works for new agents is broken). Uses the configured `${port}`.
|
|
174
|
+
3. **Tests (green):** `tests/unit/PostUpdateMigrator-machineCoherence.test.ts` (NEW, 2 — adds the section with the route + configured port when absent; idempotent on re-run). Mirrors the house awareness-migration test harness.
|
|
175
|
+
|
|
176
|
+
**Blast radius:** template/awareness only — no runtime behavior change. Satisfies the Agent Awareness + Migration Parity obligations the new route incurred.
|
|
177
|
+
|
|
178
|
+
### Increment C₁b (remainder) + D₂b + Tier-3 e2e — PENDING (documented remaining)
|
|
179
|
+
Remaining: (b3-WIRING) the fix ACTUATION — per §4.2.1-iv this is a SERVER-SIDE inbound-message interceptor (ScopeAccretionRatifier shape, "no new HTTP surface"): recognize "fix it"/"leave it" in the episode topic, verify the sender uid against TopicOperatorStore (Know Your Principal), call `sentinel.approveFix`, execute the `execute-fix` effect (config write via `writeConfigAtomic` + self-restart via `writeLifelineRestartSignal`, write-ahead outcome). Deferred as a focused unit — a config-write + self-restart is the single most dangerous action-bearing mechanism in the build and must not be rushed. The pendingFix STATE MACHINE (b3-core) that this actuates is DONE + tested. (b3-wiring) §3.2 alarm-marker attach into refreshPool's advert + §3.4 cross-machine takeover/reconciliation. (D₂b) awakeMachineCount counting rule + D5 shape sweep. (Tier-3) feature-alive e2e over the real AgentServer (dev→200, fleet→503, single-machine→machinesCompared:1); the 30-day jsonl time-prune; release fragment. (proposal → approved-holding → executing-verifying in the conversational path, single-flight, invalidation, executing-verifying suspend-pause clocks); the §3.2 alarm-MARKER attach into refreshPool's advert + §3.4 cross-machine takeover/fallback/reconciliation; the precise 30-day jsonl time-prune; the §6 boot-line already exists (C₁b-i) — extend it if desired. D₂b (awakeMachineCount counting rule + D5 shape sweep) and E (CLAUDE.md template mention of the route + "why did I get a machine-coherence alarm?" trigger via generateClaudeMd/migrateClaudeMd, Tier-3 e2e over real AgentServer, release fragment).
|
|
180
|
+
### Increment D₂a — per-peer lease-observation map (§5b's NEW retained state) — LANDED
|
|
181
|
+
|
|
182
|
+
**What changed:**
|
|
183
|
+
1. **`src/core/HttpLeaseTransport.ts`** — `lastPulledByPeer: Map<peerMachineId, { lease, observedAtMs }>` recorded inside `pullPeer()` from the SAME dials it already makes (zero new network traffic), keyed on the DIALED peer's machine-auth-verified registry id — NEVER the response body's holder claim (a pulled lease naming a third machine is hearsay; it never mints a row for the third machine). A confirmed no-lease pull records an honest `null`; an UNCONFIRMED dial records nothing (the stale entry ages out via the counting rule's freshness bound). Pruned in `pullAllPeers()` when a peer leaves the peer set. Exposed as `observedByPeer()` (a copy).
|
|
184
|
+
2. **`src/core/LeaseCoordinator.ts`** — `LeaseTransport` interface gains optional `observedByPeer?()`; surfaced as `LeaseCoordinator.peerLeaseObservations()` (empty map on a git-only mesh — the counting rule degrades to `'registry-roles'` there).
|
|
185
|
+
3. **Tests:** `tests/unit/HttpLeaseTransport.test.ts` +5 (§5b describe): dialed-id keying vs third-machine hearsay, honest-null observation, unconfirmed-dial non-refresh, de-pair pruning, copy semantics.
|
|
186
|
+
|
|
187
|
+
**Blast radius:** additive retained state + two read surfaces; NOTHING consumes them yet (the counting-rule rework is D₂b). Advisory data only (L4/SEC-4) — never demotion authority.
|
|
188
|
+
|
|
189
|
+
### Increment D₂b — the awakeMachineCount counting rule + shape rework (§5b, D5) — PENDING
|
|
190
|
+
(The counting rule in `MultiMachineCoordinator.getSyncStatus` — self `holdsLease()` + fresh/live/self-claim peers; `number | null` + `awakeMachineCountSource` tag; the FULL consumer/test/template/docstring sweep the D5 decision mandates in the same PR: docstring, /health + GET /pool serializers + the two other route callers, multimachine-syncstatus + split-brain e2e + pool-routes tests, the two CLAUDE.md template mentions via generateClaudeMd + migrateClaudeMd, doctor + machine list labels (M12), upgrade-guide entry.)
|
|
191
|
+
### Increment E — integration + e2e + CLAUDE.md template + release fragment — PENDING
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Deviations from the spec (explicit, never silent — the standard the brief mandates)
|
|
196
|
+
|
|
197
|
+
1. **`dryRunConfigPath?` added to `CoherenceCriticalFlag`.** The spec's §3.1 interface sketch enumerates `{ key, configPath, resolution, readSource, guarantee }` but the `dev-gate+dryRun` resolution (and the "(+ dryRun)" raw rows) inherently need to know WHERE the dry-run flag lives to fold it into the effective value. Added as an optional field, documented in the module header. Behavior matches the spec's resolution semantics exactly; this is a faithful elaboration, not a semantic change.
|
|
198
|
+
2. **The F4 pair is TWO manifest entries, not one compound row.** The spec §3.1 table renders `ws13PinReplicate + ws13Reconcile` as one row sharing a guarantee. This build ships them as two independent entries with the same `guarantee` string, so each key is compared on its own — strictly finer-grained (never coarser) than a compound row. If either diverges independently the guard still names it.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Migration parity
|
|
203
|
+
|
|
204
|
+
- ConfigDefaults OMITS the `monitoring.machineCoherence` block (the gate decides) — nothing for `migrateConfig()` to add for an omitted-`enabled` dev-gated feature with code-side `??` fallbacks. The real parity artifacts (the both-sides wiring test entry, the CLAUDE.md template triggers, the M10 awakeMachineCount template sweep) land in increments C/D/E.
|
|
205
|
+
|
|
206
|
+
## Rollback
|
|
207
|
+
|
|
208
|
+
- Increment A is inert (pure module + its test). Reverting the two new files is a clean no-op; nothing else references them yet.
|
|
209
|
+
|
|
210
|
+
## Continuation note (for a session resuming this build)
|
|
211
|
+
|
|
212
|
+
- **Landed:** increment A (manifest module + unit tests, green).
|
|
213
|
+
- **Next, in order:** (B) `coherenceAdvert` type on `PeerCapacity` (src/core/types.ts near the `seamlessnessFlags` block ~2035) + `SESSION_STATUS_ADVERT_FIELDS` addition + the PeerPresencePuller SECOND-enumeration fix (recordHeartbeat spread `:254` + deps type `:172` — the R2-N1 named build work) + the M4 receive clamp; emission in `refreshPool` (src/commands/server.ts:17203) UNCONDITIONAL. (C) `src/monitoring/MachineCoherenceSentinel.ts` evaluator + episode state machine + §3.4 election + §4.2.1 three-state pendingFix. (D) §5a one-liner at server.ts:17094 + §5b per-peer lease-observation map in HttpLeaseTransport + the awakeMachineCount shape sweep (D5). (E) integration/e2e + CLAUDE.md template + release fragment + architecture page.
|
|
214
|
+
- The spec's §9 test rows are the binding checklist; the burst-invariant (P17 flapping participant) + dead-adapter + held-offline walks are the load-bearing correctness tests.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## CI-fix (2026-07-03) — green the PR #1360 shards (no behavior change)
|
|
219
|
+
|
|
220
|
+
The committed tip left three unit-shard-red gates; all three are line-shift/parity artifacts of the machine-coherence wiring, not logic defects. Fixed genuinely (no test bypass/skip):
|
|
221
|
+
|
|
222
|
+
1. **`tests/unit/mesh-coherence-wiring.test.ts` (4 failing).** The machine-coherence rider (`machineCoherenceSentinel.tick()` + the async episode-effects executor) had been inlined at the START of `peerPresenceTick`, pushing the mesh-coherence-live recheck code past the fixed 4200-char source-slice window the wiring test asserts against (mesh call offset 4491 > 4200). **Fix (behavior-preserving refactor):** the episode-effects executor is extracted to a named `executeMachineCoherenceEffects(sentinel, tg)` helper defined ABOVE `peerPresenceTick`, and the whole machine-coherence rider is moved to run AFTER the mesh recheck inside the tick. The tick still runs `pullOnce()` → mesh recheck → `tick()` + drain/execute effects every 30s cadence, unchanged; only the mesh block's window offset shrinks (now 2065/2543, safely < 4200). An inline comment records WHY the ordering is load-bearing so a future refactor doesn't silently re-break it.
|
|
223
|
+
|
|
224
|
+
2. **`tests/unit/no-silent-fallbacks.test.ts` (ratchet 493 > baseline 491).** Two intentional fail-toward-silence catches in the machine-coherence liveness/tick path were newly matched by the 20-line fallback heuristic (one pre-existing on the branch — the advert-builder `catch { return {}; }` "self degrades to advert-less"; one a shift artifact of the refactor above — the tick catch). Both are deliberate degradation guards (a sentinel/builder fault must never crash the shared presence tick or the liveness beat). **Fix:** annotated both with the ratchet's sanctioned `@silent-fallback-ok` marker + reason. Count back to 491 (== baseline). No new baseline bump.
|
|
225
|
+
|
|
226
|
+
3. **`tests/unit/feature-delivery-completeness.test.ts` (markers[] parity).** `Machine-Coherence Guard` is a dev-gated-dark `GET /pool/machine-coherence` READ surface + behavioral trigger with template+migrator parity but NO `migrateFrameworkShadowCapabilities` marker (a server-side monitoring guard, not a user-invokable capability the Codex/Gemini frameworks shadow). **Fix:** moved it from `featureSections` (full tri-parity list) to `legacyMigratorSections` — the correct class, matching its siblings `Write Admission` / `Autonomous Liveness Reconciler`. Satisfies the migrateClaudeMd auto-detect without falsely requiring a shadow marker.
|
|
227
|
+
|
|
228
|
+
### Rollback
|
|
229
|
+
Revert the CI-fix commit. The `executeMachineCoherenceEffects` extraction is a straight inline↔helper move (git revert is clean); the two `@silent-fallback-ok` comments and the test-list reclassification are comment/array-membership only. No runtime behavior, config, route, or migration changes.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Side-Effects Review — Slack Follow-Through Generalization (Phase 2.3)
|
|
2
|
+
|
|
3
|
+
Spec: `docs/specs/slack-followthrough-generalization.md` (converged + approved). ELI16: `docs/specs/slack-followthrough-generalization.eli16.md`.
|
|
4
|
+
|
|
5
|
+
**What shipped:** the registration TRIGGER that makes a promise born in a Slack conversation register a durable commitment (bound to the conversation's minted id), so the already-live increment-2 PromiseBeacon + `deliverToConversation` funnel carry the follow-through across restarts — the way Telegram already works. Six code seams: (1) `detectTimePromise` hedge fix; (2) a shared `verifyConversationBind` §7 helper (refactor — `POST /commitments` now calls it, behavior-identical, golden test green); (3) `/action-claim/observe` gains the negative-id bind gate + slack dev-gate/dryRun read + Lane-A precedence + Lane-B time-promise predicate + both-prefix cap + dryRun audit sink; (4) `INSTAR_CONVERSATION_ID` spawn env with the 1:1 lifeline guard; (5) the generalized Stop hook (INSTAR_CONVERSATION_ID-only, bind-token header, no ≥20-char floor, 16KB clamp); (6) `messaging.actionClaim.slack` dev-gate registration + array-safe migration + CLAUDE.md template (both scaffold + migrator).
|
|
6
|
+
|
|
7
|
+
## Signal-vs-authority (Phase 1 answer)
|
|
8
|
+
**Does this involve a decision point?** YES — two. (a) The **registration trigger** is a DECISION to open durable state, but it is **SIGNAL-ONLY**: it runs at turn end, AFTER the Slack reply already went out; it can never block/delay/rewrite a message. It produces a durable commitment (a signal the existing beacon consumes). (b) The **bind gate** is a real blocking AUTHORITY, but it is NOT new brittle logic — it REUSES the deployed §7 `verifyConversationBind` (MAC-verified token, deterministic), factored into one helper both routes share. No brittle check gained blocking authority. Compliant.
|
|
9
|
+
|
|
10
|
+
## The 8 questions
|
|
11
|
+
|
|
12
|
+
1. **Over-block (rejects legitimate input it shouldn't?)** — The bind gate refuses a minted-id registration whose token is missing/invalid/foreign. The only "legitimate" caller of a minted id is the owning Slack session (its spawn-env token) — a refusal there means the caller genuinely lacks authority (fail-closed by design). A refusal is signal-only (the hook ignores the response) → a refused registration is an untracked promise (status-quo), never a user-visible error. No legitimate message is blocked (registration is post-send).
|
|
13
|
+
|
|
14
|
+
2. **Under-block (failure modes still missed?)** — By design (accepted recall trade, precision-over-recall): (a) Slack DMs folded into the shared `lifeline` session are deliberately untracked (they carry no `INSTAR_CONVERSATION_ID`); (b) a promise phrased outside the two deterministic classifiers; (c) a promise buried past the 16KB hook clamp; (d) a legacy dedicated session spawned before this change (no `INSTAR_CONVERSATION_ID`) until it respawns. All are safe misses (untracked = status quo), never mis-delivery. §10-Q1/Q5 track recall follow-ups.
|
|
15
|
+
|
|
16
|
+
3. **Level-of-abstraction fit** — Correct layer: the responding session's own post-turn Stop hook (generalizes the Telegram model exactly), server-side classify+record+bind on the owning machine, delivery on the existing funnel. Alternatives (in-process send-chokepoint classifier; the `commitment-detection` job) were considered and rejected in spec §11 — they add new machinery and don't generalize the Telegram model. No smarter gate is bypassed.
|
|
17
|
+
|
|
18
|
+
4. **Signal vs authority compliance** — Yes (see above). The trigger is signal-only; the authority is the reused deterministic §7 gate. `docs/signal-vs-authority.md` honored.
|
|
19
|
+
|
|
20
|
+
5. **Interactions (shadow / shadowed / double-fire / race)** — (a) **Lane A precedence** prevents double-registration: a dual-signal turn ("I'll deploy in 10 min") registers ONE actionclaim row (integration-tested). (b) **Shared cap** now counts BOTH `actionclaim:` + `timepromise:` prefixes against one budget (was actionclaim-only) — a Lane-B row can no longer escape the cap. (c) The `POST /commitments` refactor is behavior-identical (the §7 golden test `conversation-registry-routes.test.ts` passes unchanged). (d) Lane B relies on `record()`'s INTERNAL auto-arm; the route runs `detectTimePromise` on the SAME `message.slice(0,500)` `record()` sees, so "predicate fired" ⟺ "beacon armed" — no divergence. (e) The generalized hook is shared with the Telegram path; adding Lane B improves Telegram time-promise recall once the master flag graduates (spec §8.4, intended).
|
|
21
|
+
|
|
22
|
+
6. **External surfaces** — (a) A registered commitment's PromiseBeacon heartbeat is delivered into the user's Slack thread — but ONLY when `conversationIdentity.followThrough` graduates (delivery gate, separate); until then the §6.1 dark-window attention item marks it undeliverable (existing machinery). (b) The hook posts a bind-token header now (all sessions) — validated by the shared helper; a token-bearing positive (Telegram) id rides the existing R6-minor-4 arm (a strict improvement, no regression). (c) A new best-effort `logs/action-claim-observe.jsonl` dryRun audit file. Timing/state: the whole feature is dev-gated dark (`messaging.actionClaim.slack`) AND requires the master `messaging.actionClaim.enabled` — dark on the fleet.
|
|
23
|
+
|
|
24
|
+
7. **Multi-machine posture (Cross-Machine Coherence)** — **Machine-local BY DESIGN, structurally.** Registration happens on the machine that OWNS the minted conversation (the Slack-fronting Mini): the Slack session spawns there, its Stop hook posts to that machine's localhost, its bind token verifies only against that machine's secret (a peer would 403 — different secret), and `record()`'s binder resolves the minted id against the LOCAL registry (a peer throws `conversation-bind-unresolvable` → 409). Write-admission refuses a minted-id write on a non-owner. A conversation that later moves machines carries its denormalized `boundTuple`; PromiseBeacon re-resolves the live owner at speak time (increment-2 machinery, unchanged). No new replication path needed — registration correctly follows the owner.
|
|
25
|
+
|
|
26
|
+
8. **Rollback cost** — Trivial, no data migration. Kill switches (read live, no restart): `messaging.actionClaim.slack.dryRun:true` → observe-only; `messaging.actionClaim.enabled:false` (fleet default) → the hook `exit(0)`s before any POST. The `POST /commitments` refactor is behavior-identical (revertible in isolation). No durable schema change (commitments are existing rows; the only new externalKey prefix is `timepromise:`, which auto-expires). Back-out = revert the branch or flip the flag.
|
|
27
|
+
|
|
28
|
+
## Reconciliation notes (grounded deviations from the spec, all documented)
|
|
29
|
+
- **No `messaging.actionClaim.slack.dryRun` config write (spec §8.5 asked for one).** Real installs have `messaging` as an ARRAY of adapter configs, so `messaging.actionClaim.slack.enabled` resolves `undefined` — exactly what `resolveDevAgentGate` wants (live-on-dev/dark-fleet) — and the route defaults `dryRun` to `true` when absent. Correct dev-gated-dryRun-first behavior is delivered with ZERO config write; writing a dotted key into an array would corrupt it. Migration parity is satisfied by construction; the added `migrateConfigActionClaimSlackDevGate` is a defensive, array-safe #1001 strip (a no-op on the array shape).
|
|
30
|
+
- **Inherited (not introduced) risk:** the action-claim sentinel reads its master flag at `messaging.actionClaim.enabled`, but real `messaging` is an array — so the master flag is enabled via an object-shaped override (as the tests do). This is the EXISTING sentinel's config path, unchanged by this build; flagged for operator awareness before the dev soak.
|
|
31
|
+
- **Keystone doc-fix owed (§10-Q6):** durable-conversation-identity's minor-3 mislabels "the action-claim observer" as in-process; the reconciliation is in spec §4.3. A one-line keystone edit is owed when the build PR touches the shared helper.
|
|
32
|
+
|
|
33
|
+
## Tests (all three tiers, both sides of every boundary)
|
|
34
|
+
- Unit: `detect-time-promise-hedge` (9), `conversation-bind-gate` (9, all arms), `action-claim-hook-slack` (7, hook semantics), `migrate-actionclaim-slack-devgate` (5).
|
|
35
|
+
- Integration: `action-claim-route` Slack lane (7 new: register/bind, no-token 403+attention, foreign 403, lane precedence single-row, shared cap both-prefix, dryRun audit, slack-lane-dark, positive-unaffected).
|
|
36
|
+
- E2E: `action-claim-lifecycle` Slack minted-id feature-ALIVE (bound + beacon-armed) + no-token-403.
|
|
37
|
+
- Regression: §7 golden test (`conversation-registry-routes`) green (refactor parity); `generated-hooks-parse` green (hook still parses); `devGatedFeatures-wiring` (145) green (new gate entry conforms). tsc clean, lint clean.
|
|
38
|
+
|
|
39
|
+
## Phase 5 — Second-pass review (independent reviewer)
|
|
40
|
+
|
|
41
|
+
An independent reviewer audited all five high-risk areas against the ACTUAL code (not the prose):
|
|
42
|
+
1. **Cross-channel mis-delivery** — clean: the hook keys ONLY on `INSTAR_CONVERSATION_ID` (no Telegram fallback; the other `INSTAR_TELEGRAM_TOPIC` refs belong to *different* hooks); `SessionManager.ts` injects the env only when `name !== 'lifeline'`, so a Slack DM folded into `lifeline` carries no env → registers nothing (safe miss).
|
|
43
|
+
2. **Signal vs authority** — registration is post-send (`exit(0)` always); the bind gate faithfully reproduces the old inline §7 branches (skip / minted-fail-closed / positive-token / legacy-fail-open + straggler), identical 403 shape. `POST /commitments` refactor behavior-identical.
|
|
44
|
+
3. **Double-registration / cap escape** — Lane A returns before Lane B (one row for "I'll deploy in 10 min"); cap counts both prefixes.
|
|
45
|
+
4. **Beacon arming** — Lane B passes no beacon fields; auto-arm fires (`beaconEnabled===undefined`); route runs `detectTimePromise` on the same `slice(0,500)`.
|
|
46
|
+
5. **Fail-closed on minted ids** — negative id requires a valid in-set token; the gate runs before the dryRun return AND `record()`.
|
|
47
|
+
Also confirmed the hedge regex uses a NON-capturing group so `numeric[1]/[2]` indices are unshifted.
|
|
48
|
+
|
|
49
|
+
**Verdict: Concur with the review.**
|