instar 1.3.758 → 1.3.760

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.
Files changed (43) hide show
  1. package/dist/commands/machine.d.ts.map +1 -1
  2. package/dist/commands/machine.js +28 -5
  3. package/dist/commands/machine.js.map +1 -1
  4. package/dist/commands/server.js +2 -2
  5. package/dist/commands/server.js.map +1 -1
  6. package/dist/core/LeaseCoordinator.d.ts +30 -0
  7. package/dist/core/LeaseCoordinator.d.ts.map +1 -1
  8. package/dist/core/LeaseCoordinator.js +45 -0
  9. package/dist/core/LeaseCoordinator.js.map +1 -1
  10. package/dist/core/MultiMachineCoordinator.d.ts +24 -5
  11. package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
  12. package/dist/core/MultiMachineCoordinator.js +58 -11
  13. package/dist/core/MultiMachineCoordinator.js.map +1 -1
  14. package/dist/core/PostUpdateMigrator.d.ts +15 -0
  15. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  16. package/dist/core/PostUpdateMigrator.js +45 -1
  17. package/dist/core/PostUpdateMigrator.js.map +1 -1
  18. package/dist/messaging/TelegramAdapter.d.ts +2 -0
  19. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  20. package/dist/messaging/TelegramAdapter.js +65 -25
  21. package/dist/messaging/TelegramAdapter.js.map +1 -1
  22. package/dist/scaffold/templates.js +1 -1
  23. package/dist/server/routes.d.ts.map +1 -1
  24. package/dist/server/routes.js +1 -0
  25. package/dist/server/routes.js.map +1 -1
  26. package/dist/testing/selfActionRegistry.d.ts +88 -0
  27. package/dist/testing/selfActionRegistry.d.ts.map +1 -0
  28. package/dist/testing/selfActionRegistry.js +202 -0
  29. package/dist/testing/selfActionRegistry.js.map +1 -0
  30. package/package.json +2 -2
  31. package/scripts/class-closure-declare.mjs +69 -4
  32. package/scripts/class-closure-lint.mjs +61 -5
  33. package/scripts/instar-dev-precommit.js +100 -2
  34. package/scripts/lib/self-action-detect.mjs +162 -0
  35. package/scripts/lint-no-unregistered-self-action.js +204 -0
  36. package/skills/instar-dev/templates/side-effects-artifact.md +8 -1
  37. package/src/data/builtin-manifest.json +64 -64
  38. package/src/scaffold/templates.ts +1 -1
  39. package/upgrades/{1.3.758.md → 1.3.759.md} +51 -0
  40. package/upgrades/1.3.760.md +100 -0
  41. package/upgrades/side-effects/machine-coherence-guard-5b-awake-count.md +105 -0
  42. package/upgrades/side-effects/self-action-convergence.md +62 -0
  43. package/upgrades/side-effects/telegram-conservatism-pass.md +83 -0
@@ -0,0 +1,100 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Maintainer-only development machinery — the first product-code member of the
9
+ class-closure program (#1347), and the constitution standard it enforces.
10
+
11
+ - **New defect class `unbounded-self-action`** in `docs/defect-classes.json`
12
+ (severity `critical`, seeded-closed at the 20 already-fixed instances). A
13
+ post-seed recurrence fires the deterministic re-raise at lint time.
14
+ - **The live guard** — `tests/unit/self-action-convergence.test.ts` drives every
15
+ controller in the new `src/testing/selfActionRegistry.ts` registry N ticks
16
+ under a pinned sustained-pressure fixture (all accounts hot / veto never clears
17
+ / no progress) and asserts the action count SETTLES to a small bound that does
18
+ NOT scale with the horizon. Seeded with faithful convergence models of four
19
+ real controllers (proactive swap monitor, age-kill backoff, promise-beacon
20
+ notify, and a declared Eternal-Sentinel liveness heartbeat exercising the P19
21
+ exemption).
22
+ - **The forcing lint** — `scripts/lint-no-unregistered-self-action.js`
23
+ (emit-anchored, fail-closed once enforcing) refuses an unregistered self-action
24
+ controller emit so a NEW self-action inherits the invariant. **Report-only by
25
+ default** — it reports the population of unregistered controllers and exits 0
26
+ until `prGate.classClosure.dryRun:false`.
27
+ - **The detector + scope** — `scripts/lib/self-action-detect.mjs` is the single
28
+ shared source (emit set + controller-shape predicate + the gate decision). It
29
+ widens the class-closure CI lint's scope (`scripts/class-closure-lint.mjs`) so
30
+ a self-action diff requires a `classClosure` declaration, adds a
31
+ convergence-addressed check (a per-tick-cap-only `howCaught` is flagged), and
32
+ powers `assertSelfActionDeclared` in `scripts/instar-dev-precommit.js` — the
33
+ early arm at the LIGHT commit path #1035 (swap-thrash) slipped through. Both new
34
+ hard-violation conditions land report-only under the existing
35
+ `prGate.classClosure.dryRun` default.
36
+ - **`scripts/class-closure-declare.mjs`** gains `--to-trace` (write the block into
37
+ the instar-dev trace, the E3 declaration host) and an explicit negative
38
+ declaration (`--closure n/a --reason`), the trace-level analog of the lint's
39
+ allowlist.
40
+ - **The standard** "Capacity Safety — No Unbounded Self-Action" added to
41
+ `docs/STANDARDS-REGISTRY.md` (operator-ratified 2026-07-04) — BBR's temporal
42
+ twin: BBR bounds instantaneous MASS, this bounds steady-state FREQUENCY under
43
+ feedback.
44
+ - **Migration Parity** — `migrateClassClosureTemplateSelfActionClause` extends the
45
+ deployed side-effects template's Class-Closure trigger with the self-action
46
+ clause for existing agents.
47
+
48
+ Named follow-on increments (own specs, NOT built here): the unified default-on
49
+ self-action backpressure primitive (the obfuscation-resistant funnel), and the
50
+ swap decoupling / live credential re-pointing (the durable swap fix).
51
+ <!-- tracked: CMT-1911 -->
52
+
53
+ ## What to Tell Your User
54
+
55
+ This is maintainer-only development machinery — there is nothing for you to do or
56
+ notice in day-to-day use. If your operator asks "why do we keep fighting the same
57
+ runaway loops (account swaps, notification floods, the reaper firing thousands of
58
+ kill requests)?" — the answer is: those were 20 instances of ONE unnamed defect
59
+ class, and this change names it (unbounded self-action) and builds the automatic
60
+ CI test that proves any self-triggered action settles under sustained pressure
61
+ instead of running away. It ships report-only first (it logs what it would flag)
62
+ and only starts blocking after a clean soak, per the class-closure gate's own dark
63
+ default — so it changes nothing about how I respond to you today.
64
+
65
+ ## Summary of New Capabilities
66
+
67
+ - **A new defect class + its guard.** "Unbounded / oscillating self-action" is now
68
+ a registered class with a live CI ratchet (`tests/unit/self-action-convergence.test.ts`)
69
+ that drives every self-triggered controller under worst-case pressure and fails
70
+ the build if the action count doesn't converge — the temporal twin of Bounded
71
+ Blast Radius (which caps how many run at once; this caps how often one fires).
72
+ - **A forcing lint + a shared detector** so a NEW self-triggered action inherits
73
+ the convergence check instead of earning a bespoke brake after it breaks
74
+ something — caught at both the commit gate and CI.
75
+ - **A ratified constitution standard** — "Capacity Safety — No Unbounded
76
+ Self-Action" (`docs/STANDARDS-REGISTRY.md`).
77
+ - No user-facing surface, no routes, no config to set. Report-only until the
78
+ operator flips enforcing.
79
+
80
+ ## Evidence
81
+
82
+ - `tests/unit/self-action-convergence.test.ts` — the ratchet: 26 cases, every
83
+ registered controller settles (count ≤ boundK, horizon-independent at 2×ticks,
84
+ no single target thrashed), the Eternal-Sentinel honors its P19 rate floor,
85
+ wiring-integrity (no vacuous/no-op controllers), verb-superset coherence, and
86
+ grader-parity (the three new guards grade ratchet/lint/gate).
87
+ - `tests/unit/self-action-detect.test.ts` — the shared detector + the pure gate
88
+ decision (both precommit call sites): emit true-positives, comment/prose
89
+ false, fail-open on empty diff, all declaration branches.
90
+ - `tests/unit/lint-no-unregistered-self-action.test.ts` — the forcing lint's pure
91
+ evaluator: unmarked → violation, marked+registered → clean, marked-unregistered
92
+ → violation, allowlisted → clean, no-emit / non-shape → skipped.
93
+ - `tests/integration/class-closure-lint-self-action.test.ts` — the CI lint's
94
+ self-action scope arm over synthetic diffs: good guard declaration → clean; no
95
+ declaration → hard violation (report-only exit 0, enforcing exit 1); a
96
+ per-tick-cap-only `howCaught` → convergence-addressed violation.
97
+ - `tests/unit/migrate-class-closure-self-action-clause.test.ts` — the migration:
98
+ stock template updated, idempotent, customized-left-untouched, no-op when absent.
99
+ - Full lint chain (`npm run lint`) green — including the new forcing lint
100
+ (report-only) and the class-closure grader-parity. `tsc --noEmit` clean.
@@ -0,0 +1,105 @@
1
+ # Side-Effects Review — machine-coherence-guard §5b: lease-live `awakeMachineCount`
2
+
3
+ **Version / slug:** `machine-coherence-guard-5b-awake-count`
4
+ **Date:** `2026-07-04`
5
+ **Author:** Echo (autonomous)
6
+ **Spec:** `docs/specs/machine-coherence-guard.md` §5b (converged 2026-07-03, approved)
7
+ **Second-pass reviewer:** not-required (Tier 2; a read-only telemetry-derivation fix — no gate, no demotion authority, no mutation)
8
+
9
+ ## Summary of the change
10
+
11
+ `/health.multiMachine.syncStatus.awakeMachineCount` reported **0** while both the
12
+ Laptop and the Mac Mini were online and the Mini correctly held the lease
13
+ (`leaseHolder` named the Mini). Root cause, confirmed in code: `getSyncStatus()`
14
+ derived the count SOLELY by counting `registry.machines[].role === 'awake'` rows
15
+ (`MultiMachineCoordinator.ts`) — a git-synced SYMBOL that lags the authoritative
16
+ lease and that a dead Cloudflare rope or a slow registry push can leave stale — so
17
+ it read 0 even though the lease (the same signal `leaseHolder` uses) named the
18
+ holder. The multi-transport hedge was NOT the wedge: it carries the lease over the
19
+ healthy Tailscale/LAN ropes (that is why `leaseHolder` was correct). The count
20
+ simply consulted the wrong source. This implements the spec's already-designed
21
+ §5b remedy: derive the count from LIVE lease observations, source-tagged and
22
+ honest.
23
+
24
+ Files modified:
25
+ - `src/core/LeaseCoordinator.ts` — new `deriveLiveAwakeCount(staleMs)`: `(self
26
+ holds ? 1 : 0)` + distinct peers whose most-recent observation is FRESH, LIVE
27
+ (not expired on our clock), and a SELF-CLAIM (`lease.holder === peerId`). The
28
+ per-peer observation seam (`peerLeaseObservations()` / transport `observedByPeer()`)
29
+ already existed; this wires the counting rule onto it. Advisory only.
30
+ - `src/core/MultiMachineCoordinator.ts` — `getSyncStatus()` rewritten to prefer
31
+ the lease-live basis (when a lease coordinator with pull capability is attached),
32
+ degrade to the registry-role basis on a git-only mesh, and yield `null` on a read
33
+ failure; a new `leaseObservationStaleMs()` (3× lease-pull interval, floor 30s).
34
+ `MultiMachineSyncStatus.awakeMachineCount` becomes `number | null` and gains
35
+ `awakeMachineCountSource: 'lease-live' | 'registry-roles' | 'unavailable'`.
36
+ `splitBrainState` now null-safely reads `count > 1` (degrades to the pull-contest
37
+ latch alone when the count is null).
38
+ - `src/server/routes.ts` — `/pool` router block carries `awakeMachineCountSource`
39
+ (`/health` spreads the whole object, so it flows automatically).
40
+ - `src/commands/machine.ts` — `instar doctor` labels the registry-role check as a
41
+ "may-lag" VIEW and, when the server is reachable, adds a **Live lease view**
42
+ check that prints the authoritative count + source and names any divergence.
43
+ - `src/scaffold/templates.ts` + `src/core/PostUpdateMigrator.ts` — the two deployed
44
+ CLAUDE.md mentions updated to the new shape (Agent Awareness + Migration Parity).
45
+
46
+ Tests: `tests/unit/multimachine-awake-count-lease-live.test.ts` (new; the
47
+ load-bearing "Cloudflare dead, Tailscale/LAN alive, Mini holds lease → count 1
48
+ lease-live, NOT 0" case plus stale/expired/hearsay/no-lease/registry-fallback/
49
+ unavailable), updated `tests/unit/multimachine-syncstatus.test.ts`,
50
+ `tests/integration/pool-routes.test.ts` (+ two other mocks), and an extended
51
+ `tests/e2e/multi-machine-lease-split-brain.test.ts` (lease-live convergence).
52
+
53
+ ## Roll-up across the seven review dimensions
54
+
55
+ 1. **Over-block**: none. Nothing is blocked, delayed, or rewritten — this is a
56
+ read-only observability derivation. It NEVER drives a demotion (demotion
57
+ authority stays exclusively with the strictly-higher-epoch supersede gate and
58
+ the operator flow), so it cannot mis-fence a healthy holder.
59
+ 2. **Under-block**: none. Genuine split-brain is still detected: `count > 1` OR
60
+ the pull-contest latch → `splitBrainState: 'contested'`. The freshness (3×
61
+ pull-interval), liveness (not-expired), and self-claim (no third-machine
62
+ hearsay) gates prevent both over- and under-count.
63
+ 3. **Level-of-abstraction fit**: correct. The count derivation lives in
64
+ `LeaseCoordinator` (where the lease/clock/expiry logic already lives) behind a
65
+ pure method; `getSyncStatus()` only chooses the basis and tags it.
66
+ 4. **Signal-vs-authority compliance**: fully signal-only. Peer lease claims are
67
+ self-asserted advisory data (L4/SEC-4); the count feeds dashboards + the
68
+ existing human-decision attention flow, never an automatic action. No silent
69
+ fallback: each `catch` yields an honest `null`+`'unavailable'` (tagged
70
+ `@silent-fallback-ok` as a read-only health field), never a fabricated 0.
71
+ 5. **Interactions**: the `MultiMachineSyncStatus` shape change (`number → number |
72
+ null` + source tag) is swept across every consumer — `/health`, `/pool`, the
73
+ two other route callers (leaseHolder-only, unaffected), the unit/integration/e2e
74
+ fixtures, `instar doctor`, and both CLAUDE.md templates. `splitBrainState`'s
75
+ three in-code consumers (ingress suppression, speaker-election, rope-health
76
+ `splitBrainItemOpen`) keep their exact semantics.
77
+ 6. **External surfaces**: none added. No new network path, no new egress. `instar
78
+ doctor` adds a Bearer'd localhost `/health` read (the same host it already
79
+ contacts) to fetch the live count.
80
+ 7. **Rollback cost**: low. Additive method + a localized `getSyncStatus()` rewrite;
81
+ revertable in one commit. No persistent state, no migration data. The
82
+ `registry-roles` basis remains as the automatic git-only-mesh degrade (there is
83
+ deliberately no config lever back to the old always-registry behavior — that
84
+ would preserve the documented defect).
85
+
86
+ ## Clock assumption
87
+
88
+ Lease liveness (`expiresAt` vs now) is judged on the OBSERVER's clock. A
89
+ clock-drifted machine could misjudge a peer lease's expiry and skew its own
90
+ published count; the mesh's existing per-peer clock-skew gate
91
+ (`MachinePoolRegistry`) marks a divergent machine placement-ineligible, and the
92
+ freshness bound caps how long any one misjudged observation can distort the count.
93
+ The count inherits the mesh's clock-sanity envelope rather than adding its own.
94
+
95
+ ## Evidence pointers
96
+
97
+ - `npx tsc --noEmit` — clean.
98
+ - `npm run lint` — exit 0 (all ratchets green, including no-silent-fallbacks).
99
+ - `npm run build` — exit 0.
100
+ - `npx vitest run tests/unit/multimachine-awake-count-lease-live.test.ts` — 8/8
101
+ pass (load-bearing Cloudflare-dead case asserts count 1 + `lease-live`, NOT 0).
102
+ - `npx vitest run` across the mesh/lease suites (MultiMachineCoordinator-leasePull,
103
+ multi-machine-coordinator, HttpLeaseTransport, multimachine-syncstatus,
104
+ lease-contested-resolution, pool-routes, multi-machine-lease-split-brain) — all
105
+ green (59 + 9 pass).
@@ -0,0 +1,62 @@
1
+ # Side-Effects Review — Self-Action Convergence (the class-closure guard for self-inflicted loops)
2
+
3
+ **Version / slug:** `self-action-convergence`
4
+ **Date:** `2026-07-04`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required — additive dev-tooling (a defect-class entry, a test ratchet, two lints, a shared detector, a precommit arm, a template clause, a migration, a constitution standard). No runtime decision surface, no routes, no user-facing behavior, no data migration. Everything ships report-only under the pre-existing prGate.classClosure dark default.`
7
+
8
+ ## Summary of the change
9
+
10
+ Registers `unbounded-self-action` as a formal defect class in the class-closure registry (#1347) — the FIRST product-code member of that program — and builds the live guard that ends it: an N-tick sustained-pressure convergence ratchet (`tests/unit/self-action-convergence.test.ts` + the `src/testing/selfActionRegistry.ts` controller registry), a fail-closed forcing lint (`scripts/lint-no-unregistered-self-action.js`), a shared detector (`scripts/lib/self-action-detect.mjs`), a scope+convergence arm added to the class-closure CI lint (`scripts/class-closure-lint.mjs`), an early-detection arm at instar-dev commit time (`assertSelfActionDeclared` in `scripts/instar-dev-precommit.js`), a `--to-trace`/negative-declaration mode in `scripts/class-closure-declare.mjs`, a one-line side-effects-template clause, its migration (`migrateClassClosureTemplateSelfActionClause`), and the ratified constitution standard "Capacity Safety — No Unbounded Self-Action."
11
+
12
+ This is the structural fix for the entire self-inflicted-loop class (swap-thrash being the presenting case). It is BBR's temporal twin: Bounded Blast Radius bounds instantaneous MASS; this bounds steady-state FREQUENCY under feedback.
13
+
14
+ Files touched: `docs/defect-classes.json`, `docs/STANDARDS-REGISTRY.md`, `docs/specs/self-action-convergence.md` (+ `.eli16.md`), `scripts/lib/self-action-detect.mjs` (new), `src/testing/selfActionRegistry.ts` (new), `tests/unit/self-action-convergence.test.ts` (new), `scripts/lint-no-unregistered-self-action.js` (new), `scripts/class-closure-lint.mjs`, `scripts/class-closure-declare.mjs`, `scripts/instar-dev-precommit.js`, `skills/instar-dev/templates/side-effects-artifact.md`, `src/core/PostUpdateMigrator.ts`, `package.json`, plus tests (`tests/unit/self-action-detect.test.ts`, `tests/unit/lint-no-unregistered-self-action.test.ts`, `tests/unit/migrate-class-closure-self-action-clause.test.ts`, `tests/integration/class-closure-lint-self-action.test.ts`).
15
+
16
+ ## Decision-point inventory
17
+
18
+ - **`unbounded-self-action` defect class** — add (registry semantics) — a protected-path registry entry. Governs fix-time declarations; does not gate runtime.
19
+ - **Convergence ratchet + controller registry** — add (CI test) — drives modeled controllers under a pinned worst-case fixture; fails the build (once green) if a registered controller does not settle. Zero runtime authority.
20
+ - **Forcing lint** — add (CI lint) — refuses an unregistered self-action controller emit. **Report-only by default** (exits 0 unless `prGate.classClosure.enabled && !dryRun`).
21
+ - **Class-closure CI lint scope arm + convergence check** — modify (CI lint) — a self-action diff now requires a declaration; a per-tick-cap-only `howCaught` is flagged. Hard-violations recorded; **fail the build only when enforcing** (dark default).
22
+ - **`assertSelfActionDeclared` precommit arm** — add (commit gate) — blocks a self-action src/ change with no declaration/negative. FAIL-OPEN on tooling failure; a genuine one-shot costs one attested negative line.
23
+ - **Shared detector** — add (pure library) — the single source for the emit set + scope predicate + the gate decision. Deterministic; never LLM-guesses.
24
+ - **Side-effects template clause + migration** — modify (skill template) — the Class-Closure Declaration trigger now names the self-action case; the migration reaches existing agents (Migration Parity).
25
+ - **Constitution standard** — add (standards text) — "Capacity Safety — No Unbounded Self-Action," ratified by the operator 2026-07-04.
26
+ - **All existing gates** — pass-through — the tone gate, coherence gate, external-operation gate, class-closure gate's existing behavior are unchanged; this composes with #1347, it does not fork it.
27
+
28
+ ## 1. Over-block
29
+
30
+ **No runtime block/allow surface — over-block N/A.** The only new blocking paths are DEV-TIME gates (the precommit arm + the CI lints), and both ship report-only under the pre-existing `prGate.classClosure` dark default. The precommit arm fails OPEN on any tooling failure (empty diff, no src/ file, unreadable artifact) and offers an explicit one-line negative-declaration escape, so a genuine one-shot / user-driven action is never unescapably blocked. The forcing lint is scoped to controller-SHAPE files to avoid flagging every `retry(` in src/.
31
+
32
+ ## 2. Under-block
33
+
34
+ **No runtime under-block surface.** The completeness guarantee (every controller registered) is report-only until the operator flips enforcing on measured declaration population, per #1347's own criterion. Until then a NEW self-action can still ship un-registered — the honest, deliberate soak posture; the E2 CI lint + E3 precommit arm are the earlier backstops, and the string-based scope is named as an accident-deterrent (the obfuscation-resistant closure is the follow-on funnel, Part B).
35
+
36
+ ## 3. Ordering / idempotency
37
+
38
+ The migration is idempotent (marker short-circuit). The declare `--to-trace` mode is idempotent (identical block → no change). The ratchet + lints are pure re-runnable evaluations. No persistent runtime state, no ordering dependency.
39
+
40
+ ## 4. Signal vs. Authority
41
+
42
+ Compliant. The detector is DETERMINISTIC — it forces a declaration, it never LLM-guesses (Signal vs. Authority). The ratchet + lints are structure, not willpower. The convergence proof is a CI ratchet, not discretionary prose. No new blocking authority is granted to any LLM path.
43
+
44
+ ## 5. Feedback loops
45
+
46
+ **This spec IS the answer to the feedback-loop question** the swap-thrash review never answered. The change adds NO new runtime control loop — it adds a CI/commit-time invariant that PROVES other control loops converge. It cannot itself spiral (it is a stateless test/lint run per CI invocation).
47
+
48
+ ## 6. External surfaces
49
+
50
+ None. No HTTP routes, no external API calls, no network egress. All new surfaces are local dev-tooling (a test, two lints, a shared library, a commit-gate arm, a registry entry, a template clause, a migration, a standard).
51
+
52
+ ## 7. Multi-machine posture
53
+
54
+ `unified` (repo-resident). The registry entry, the lints, the detector, the ratchet, and the standard all ride the git repo itself (the replication medium), reaching every checkout on `git pull` — matching #1347's declared posture. The template clause reaches existing deployed agents through the PostUpdateMigrator migration. No machine-local state, no per-machine divergence.
55
+
56
+ ## Class-Closure Declaration (display-only mirror)
57
+
58
+ - **`defectClass`** — `unbounded-self-action` (this change REGISTERS the class AND builds its closing guard; the self-referential canonical closure).
59
+ - **`closure`** — `guard`.
60
+ - **`guardEvidence`** — `{ enforcementType: ratchet, citation: tests/unit/self-action-convergence.test.ts, howCaught: the ratchet drives every registered self-action controller N ticks under a pinned sustained-pressure fixture (all accounts hot / veto never clears / no progress) and asserts the action count SETTLES to a small bound that does NOT scale with the horizon — steady-state convergence via each controller's settling brake (all-hot / projected-load / exponential-backoff+breaker / suppress-unchanged), with the declared Eternal-Sentinel exemption asserting a P19 rate floor instead. A per-tick cap would NOT satisfy this — it bounds one pass, never the loop. }`
61
+
62
+ The machine-readable counterpart lives in the commit's decision-audit entry (persisted from the instar-dev trace by `writeDecisionAudit`), which the CI class-closure lint reads. This mirror is display-only and is never summed with it (C1).
@@ -0,0 +1,83 @@
1
+ # Side-Effects Review — Telegram Conservatism Pass
2
+
3
+ **Version / slug:** `telegram-conservatism-pass`
4
+ **Date:** `2026-07-04`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `echo (guard-adjacent; independent re-read appended)`
7
+
8
+ ## Summary of the change
9
+
10
+ Closes the `origin:'system'`/`'user'` bypass in the auto-topic flood ceiling. `TelegramAdapter.createForumTopic` previously enforced its last-resort budget ONLY for `origin:'auto'`; a bare `origin:'system'` skipped it entirely, and `createAttentionItem` used `origin:'system'` for every HIGH/URGENT item (never coalesced) → an unbounded per-item topic path. The change: (1) the ceiling now applies to every origin except an explicit human request (`origin:'user'`) or a caller that declares its topic cardinality-bounded (`opts.bounded === true`); (2) the genuine create-once system topics are marked `bounded:true`; (3) critical attention topics get a distinct budget label and coalesce (never drop) on ceiling-refusal. Files: `src/messaging/TelegramAdapter.ts`, `src/commands/server.ts`, `tests/integration/notification-flood-burst-invariant.test.ts`. Decision point touched: the topic-creation flood ceiling (a delivery shaper).
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `TelegramAdapter.createForumTopic` flood-ceiling exemption — **modify** — exemption narrowed from `origin ∈ {user,system}` to `origin===user || bounded===true`.
15
+ - `TelegramAdapter.createAttentionItem` critical-topic path — **modify** — critical items now ride a distinct budget label and coalesce on refusal instead of degrading.
16
+
17
+ ---
18
+
19
+ ## 1. Over-block
20
+
21
+ A create-once system topic caller that a FUTURE author forgets to mark `bounded:true` would be budgeted against the ceiling (12/10min global). Because these topics are low-volume create-once, the ceiling is not approached in normal operation; a refusal self-heals on the next window (the topic recreates). No user message and no attention item is ever rejected — only a redundant per-item topic is withheld, and even then the item is coalesced into the single notices topic. Conservative-direction residual.
22
+
23
+ ---
24
+
25
+ ## 2. Under-block
26
+
27
+ The raw-API lifeline topic creation (`lifeline/TelegramLifeline.ts:2685`) bypasses the adapter chokepoint entirely (separate process, create-once) and is NOT covered by this change — tracked follow-up `<!-- tracked: CMT-1901 -->`. It is create-once and cannot flood, so the miss is bounded. `origin:'user'` remains fully exempt by design (a human explicitly asking for a topic is the sanctioned exception).
28
+
29
+ ---
30
+
31
+ ## 3. Level-of-abstraction fit
32
+
33
+ Correct layer. The change lives in the ONE function where topics are born (`createForumTopic`) and reuses the existing `topicCreationGuard` (an `AttentionTopicGuard` instance) and `routeToFloodNotice` — it does not add a parallel guard or a new primitive. The ceiling is exactly the chokepoint the *Bounded Notification Surface* standard designates; this change fixes a hole in it rather than bolting on a new layer.
34
+
35
+ ---
36
+
37
+ ## 4. Signal vs authority compliance
38
+
39
+ - [x] No — this change has no NEW block/allow authority over agent behavior or information flow; it tunes an existing delivery shaper.
40
+
41
+ The `topicCreationBudget` ceiling and `AttentionTopicGuard` are DELIVERY SHAPERS (the rate-counter carve-out in `docs/signal-vs-authority.md`): they change the FORM of delivery (one coalesced topic + a log line instead of a new topic per item) and never withhold a critical notice or drop an item. This change narrows an over-broad exemption and adds a coalesce branch — strictly more conservative, no new brittle blocking logic.
42
+
43
+ ---
44
+
45
+ ## 5. Interactions
46
+
47
+ - **Shadowing:** the critical path now uses a distinct budget label (`attention-item-critical`) so LOW/NORMAL attention topics (`attention-item`) do not starve the critical budget and vice-versa. No shadowing.
48
+ - **Double-fire:** none — one `decide()` call per creation attempt (unchanged cardinality).
49
+ - **Races:** the guard is a per-process in-memory counter (no shared state); concurrent creations share the existing `floodNoticePending` in-flight map so the coalesce topic is created once. Unchanged.
50
+ - **Feedback loops:** the overflow surfaces (flood-notice, agent-health lane) are marked `bounded:true`, so the ceiling can NEVER refuse the very topic the coalesce path needs — no infinite regress. Verified in the design and by the passing 1,000-item critical-flood test (which creates exactly one coalesced topic).
51
+
52
+ ---
53
+
54
+ ## 6. External surfaces
55
+
56
+ - **Telegram:** fewer topics under a flood — the intended user-visible effect. A genuine lone emergency still gets its own topic (verified by the retained lone-emergency test).
57
+ - **Persistent state:** none changed. Coalesced items are still written to the attention store + `state/attention-suppressed.jsonl` (existing paths).
58
+ - **Other agents / users:** none — internal delivery shaping.
59
+ - **Operator surface (Mobile-Complete):** no operator-facing actions added or touched. Not applicable.
60
+
61
+ ---
62
+
63
+ ## 6b. Operator-surface quality
64
+
65
+ No operator surface (no `dashboard/*`, approval, or grant/secret form touched) — not applicable.
66
+
67
+ ---
68
+
69
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
70
+
71
+ **machine-local BY DESIGN.** The topic-creation budget is a per-`TelegramAdapter`-instance rate counter, and only the machine fronting Telegram polling creates topics. A per-machine budget is correct: it bounds the topics THAT machine creates, which is the only machine creating any. No durable state is introduced (nothing to strand on a topic transfer), no user-facing notice needs one-voice gating beyond what already exists (the change reduces notices), and no URLs are generated. Same posture as the existing `AttentionTopicGuard`.
72
+
73
+ ---
74
+
75
+ ## 8. Rollback cost
76
+
77
+ Pure code, no migration, no durable-state change. Back-out = revert the diff in a single hot-fix release. The new `bounded` opt is falsy-by-default, so an un-reverted partial state can only be MORE conservative, never a flood. Lowest-cost rollback class.
78
+
79
+ ---
80
+
81
+ ## Second-pass review (guard-adjacent — independent re-read)
82
+
83
+ Re-read the diff cold against the artifact. The one real behavior change is that a FLOOD (8+ in 10min per label, or 12+ global) of HIGH/URGENT attention items now coalesces instead of each spawning a topic. This contradicts the OLD "critical never coalesced" invariant — but that invariant was itself the unbounded hole (it made "mark it HIGH" a flood bypass), and the operator directive explicitly prefers a single alert topic under flood. The item is never dropped (coalesced + stored + audited). The generous per-label ceiling means a normal handful of genuine emergencies is unaffected. **Concur with the review.** No concern raised.