instar 1.3.178 → 1.3.180

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 (68) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +3 -0
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/AutoUpdater.d.ts +12 -0
  5. package/dist/core/AutoUpdater.d.ts.map +1 -1
  6. package/dist/core/AutoUpdater.js +21 -2
  7. package/dist/core/AutoUpdater.js.map +1 -1
  8. package/dist/core/CircuitBreakingIntelligenceProvider.d.ts +26 -0
  9. package/dist/core/CircuitBreakingIntelligenceProvider.d.ts.map +1 -1
  10. package/dist/core/CircuitBreakingIntelligenceProvider.js +56 -5
  11. package/dist/core/CircuitBreakingIntelligenceProvider.js.map +1 -1
  12. package/dist/core/CoherenceGate.d.ts.map +1 -1
  13. package/dist/core/CoherenceGate.js +21 -1
  14. package/dist/core/CoherenceGate.js.map +1 -1
  15. package/dist/core/CoherenceReviewer.d.ts +6 -0
  16. package/dist/core/CoherenceReviewer.d.ts.map +1 -1
  17. package/dist/core/CoherenceReviewer.js +3 -0
  18. package/dist/core/CoherenceReviewer.js.map +1 -1
  19. package/dist/core/LlmCircuitBreaker.d.ts +52 -2
  20. package/dist/core/LlmCircuitBreaker.d.ts.map +1 -1
  21. package/dist/core/LlmCircuitBreaker.js +134 -11
  22. package/dist/core/LlmCircuitBreaker.js.map +1 -1
  23. package/dist/core/MessagingToneGate.d.ts.map +1 -1
  24. package/dist/core/MessagingToneGate.js +7 -0
  25. package/dist/core/MessagingToneGate.js.map +1 -1
  26. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  27. package/dist/core/PostUpdateMigrator.js +17 -0
  28. package/dist/core/PostUpdateMigrator.js.map +1 -1
  29. package/dist/core/UnjustifiedStopGate.d.ts.map +1 -1
  30. package/dist/core/UnjustifiedStopGate.js +9 -0
  31. package/dist/core/UnjustifiedStopGate.js.map +1 -1
  32. package/dist/core/UpdateGate.d.ts +17 -0
  33. package/dist/core/UpdateGate.d.ts.map +1 -1
  34. package/dist/core/UpdateGate.js +23 -0
  35. package/dist/core/UpdateGate.js.map +1 -1
  36. package/dist/core/types.d.ts +14 -0
  37. package/dist/core/types.d.ts.map +1 -1
  38. package/dist/core/types.js.map +1 -1
  39. package/dist/monitoring/FeatureMetricsLedger.d.ts +101 -0
  40. package/dist/monitoring/FeatureMetricsLedger.d.ts.map +1 -0
  41. package/dist/monitoring/FeatureMetricsLedger.js +185 -0
  42. package/dist/monitoring/FeatureMetricsLedger.js.map +1 -0
  43. package/dist/scaffold/templates.d.ts.map +1 -1
  44. package/dist/scaffold/templates.js +5 -0
  45. package/dist/scaffold/templates.js.map +1 -1
  46. package/dist/server/AgentServer.d.ts +1 -0
  47. package/dist/server/AgentServer.d.ts.map +1 -1
  48. package/dist/server/AgentServer.js +27 -0
  49. package/dist/server/AgentServer.js.map +1 -1
  50. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  51. package/dist/server/CapabilityIndex.js +11 -0
  52. package/dist/server/CapabilityIndex.js.map +1 -1
  53. package/dist/server/routes.d.ts +1 -0
  54. package/dist/server/routes.d.ts.map +1 -1
  55. package/dist/server/routes.js +18 -0
  56. package/dist/server/routes.js.map +1 -1
  57. package/dist/threadline/TopicLinkageHandler.d.ts.map +1 -1
  58. package/dist/threadline/TopicLinkageHandler.js +9 -4
  59. package/dist/threadline/TopicLinkageHandler.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/data/builtin-manifest.json +64 -64
  62. package/src/scaffold/templates.ts +5 -0
  63. package/upgrades/1.3.179.md +68 -0
  64. package/upgrades/1.3.180.md +133 -0
  65. package/upgrades/side-effects/llm-feature-metrics-phase1b.md +83 -0
  66. package/upgrades/side-effects/llm-feature-metrics.md +82 -0
  67. package/upgrades/side-effects/llm-ratelimit-bounded-wait.md +46 -0
  68. package/upgrades/side-effects/restart-immediately.md +96 -0
@@ -0,0 +1,133 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ **Agent-to-agent reply-waits no longer flood the user's chat with "⏳ awaiting
9
+ reply" heartbeats.** When an agent sent a Threadline message to another agent,
10
+ `TopicLinkageHandler` created the reply-tracking commitment with
11
+ `beaconEnabled: true` **and** the user's topic id attached. `PromiseBeacon` then
12
+ fired cadenced `⏳ … awaiting reply` heartbeats straight into the user's Telegram
13
+ topic for what is purely an agent-to-agent conversation — observed in the wild as
14
+ dozens of heartbeats burying one topic, making real work invisible and reading as
15
+ "stuck."
16
+
17
+ The reply already routes back on its own: the threadline-reply commitment carries
18
+ `relatedThreadId` + `topicId`, and the inbound dispatch matches on those when the
19
+ reply lands — entirely independent of the beacon. So the heartbeat was pure
20
+ housekeeping noise. The fix sets `beaconEnabled: false` on these reply-wait
21
+ commitments. This is a direct application of the **Near-Silent Notifications**
22
+ standard: routine agent-to-agent status belongs in logs, never the user's chat.
23
+
24
+ The per-feature LLM metrics now actually collect data. Phase 1a added the ledger
25
+ + `GET /metrics/features`; **Phase 1b adds the funnel tap** — the one shared LLM
26
+ call (`CircuitBreakingIntelligenceProvider.evaluate`) now records, per gate/
27
+ sentinel, its latency, whether it had to wait out a rate-limit window, and
28
+ success/error. So `/metrics/features` goes from empty to live as your checks run.
29
+
30
+ It's pure observability: a single side-channel `record()` per call, in a
31
+ swallow-all try/catch, with the breaker/rate-limit control flow byte-identical.
32
+ No gate changes behavior.
33
+
34
+ New read-only observability: **per-feature LLM metrics**. A new
35
+ `FeatureMetricsLedger` (SQLite, like the token ledger) plus `GET
36
+ /metrics/features` let you see what each LLM-driven gate/sentinel actually
37
+ costs (tokens, latency) and how often it fires — so tuning them is evidence-
38
+ based (which to thin, which to strengthen) instead of guessed.
39
+
40
+ This is **Phase 1a**: the store + endpoint. It changes no existing gate's
41
+ behavior and writes nothing in production yet — the single funnel tap that
42
+ feeds it lands in **Phase 1b**, on top of the in-flight rate-limit-resilience
43
+ change (#638), so the two don't collide.
44
+
45
+ New per-agent, opt-in config flag **`updates.restartImmediately`** (default
46
+ **false**). When true, that agent's update restarts are **never deferred** — not
47
+ for active sessions, not for the restart window — so it always rolls onto the
48
+ latest version as soon as it is downloaded.
49
+
50
+ This is intended for the instar developer's own agent (always-current is
51
+ required when you build and dogfood the fleet). It is **off by default**, so the
52
+ fleet's existing session-aware + window-aware restart deferral is unchanged.
53
+
54
+ ## What to Tell Your User
55
+
56
+ Nothing to configure. If your agent talks to other agents over Threadline, it will
57
+ no longer spam your chat with "⏳ awaiting reply" heartbeats while it waits for a
58
+ peer — the peer's reply still arrives and routes to the right place exactly as
59
+ before. Beacons that were already running before this update auto-pause on their
60
+ own; new ones won't be created.
61
+
62
+ Nothing required. If asked: I can now show real per-check cost and timing, how
63
+ often each check runs, and how often it hits a rate-limit wait — the data that
64
+ lets us tune the checks with evidence.
65
+
66
+ Nothing required. If asked: I can now report, per safety check, how much it costs
67
+ and how often it fires — which is what lets us tune the checks with real data
68
+ instead of guesses.
69
+
70
+ Nothing for almost everyone — this is off by default and changes nothing unless
71
+ you explicitly turn it on. If you run the kind of agent that must always be on the
72
+ latest build, I can switch it to restart right away whenever an update lands. A
73
+ restart does not close your sessions — they resume right where they left off; the
74
+ only cost is a brief pause in messaging while the server bounces. Just ask me and
75
+ I can turn it on and confirm it's active.
76
+
77
+ ## Summary of New Capabilities
78
+
79
+ - `TopicLinkageHandler` creates threadline-reply commitments with
80
+ `beaconEnabled: false` — the reply-routing (relatedThreadId + topicId) is
81
+ preserved; only the user-facing heartbeat is suppressed.
82
+ - No change to user-facing PromiseBeacons for genuine commitments *to the user*;
83
+ this scopes the suppression to agent-to-agent reply-waits only.
84
+
85
+ - `CircuitBreakingIntelligenceProvider` instruments every LLM call into the
86
+ `FeatureMetricsLedger` via a module-level recorder (`setFeatureMetricsRecorder`),
87
+ wired once in the server so it covers all current and future LLM features.
88
+ - Per-feature data: call-count, latency (p50/p95), rate-limit wait-rate, error-rate.
89
+ (Fired-vs-noop verdict + token attribution are Phase 2.)
90
+
91
+ - `GET /metrics/features` (`?sinceHours=` / `?feature=`) — per-feature rollup:
92
+ calls, tokens, fired/no-op, fire-rate, p50/p95 latency, wait-stats.
93
+ - `FeatureMetricsLedger` — read-only per-feature LLM observability store.
94
+ - Agents learn about it via the CLAUDE.md template (new) + migration (existing).
95
+
96
+ - `updates.restartImmediately` config flag (default false). `UpdateGate` gains
97
+ `alwaysRestartImmediately` (short-circuits `canRestart` to allow, never starts
98
+ the deferral clock) + a runtime `setAlwaysRestartImmediately` setter; the
99
+ `AutoUpdater` constructs the gate with it, skips the restart-window wait when
100
+ set, and re-reads the flag each tick so a live config edit takes effect without
101
+ a restart. The same-version cooldown + cascade dampener (loop protection) are
102
+ preserved.
103
+ - Observability: both `UpdateGate.getStatus()` and `AutoUpdater.getStatus()` /
104
+ `GET /updates/status` surface the active value.
105
+
106
+ ## Evidence
107
+
108
+ - `tests/unit/TopicLinkageHandler.test.ts` — the threadline-reply commitment now
109
+ asserts `beaconEnabled === false` (regression pin).
110
+ - `tests/unit/CommitmentTracker-threadline-reply.test.ts` — the tracker still
111
+ honors an explicit `beaconEnabled: true` (mechanism unchanged); stale "matches
112
+ call-site" comment corrected. 32/32 green across both files.
113
+
114
+ - Spec: `docs/specs/llm-feature-metrics-spec.md` (Phase 1b; approved Telegram 13435).
115
+ - Tests: `tests/unit/CircuitBreaking-feature-metrics-tap.test.ts` (+8, incl. an
116
+ end-to-end feed into a real ledger); all 74 existing CircuitBreaking/breaker tests
117
+ pass unchanged; `npm run lint` clean.
118
+
119
+ - Spec: `docs/specs/llm-feature-metrics-spec.md` (+ `.eli16.md`), review-convergence
120
+ + approved by Justin (Telegram 13435, 2026-05-31).
121
+ - Tests (3-tier): `tests/unit/FeatureMetricsLedger.test.ts`,
122
+ `tests/unit/PostUpdateMigrator-metricsFeatures.test.ts`,
123
+ `tests/integration/metrics-features-routes.test.ts`,
124
+ `tests/e2e/metrics-features-lifecycle.test.ts` (feature-is-alive: 200 not 503).
125
+ `npm run lint` clean.
126
+
127
+ - Spec: `docs/specs/restart-immediately-spec.md` (approved Telegram 13435,
128
+ 2026-06-01).
129
+ - Tests: `tests/unit/UpdateGate.test.ts` (+7: allow-despite-healthy-session,
130
+ pure-no-deferral, monitor-not-consulted, default-still-blocks, runtime
131
+ toggle both directions) and `tests/unit/AutoUpdater.test.ts` (+2: default
132
+ false; `restartImmediately:true` reflected in status via the real gate). All
133
+ 19 UpdateGate + 18 AutoUpdater tests pass; `npm run lint` (tsc) clean.
@@ -0,0 +1,83 @@
1
+ # Side-Effects Review — Per-feature LLM metrics, Phase 1b (the funnel tap)
2
+
3
+ **Slug:** `llm-feature-metrics-phase1b`
4
+ **Date:** 2026-06-01
5
+ **Author:** echo
6
+ **Spec:** `docs/specs/llm-feature-metrics-spec.md` (Phase 1b; approved by Justin, Telegram 13435)
7
+
8
+ ## Summary of the change
9
+
10
+ Phase 1a (#639) shipped the `FeatureMetricsLedger` + `/metrics/features` but nothing
11
+ wrote to it in production. Phase 1b adds the **funnel tap**: the one shared
12
+ `CircuitBreakingIntelligenceProvider.evaluate()` now records, per call, the calling
13
+ feature + latency + whether a rate-limit bounded-wait was engaged + success/error —
14
+ so every LLM-driven gate/sentinel is measured through a single instrumentation point
15
+ (built on top of #638's now-merged `evaluate()`, measuring its wait-behavior).
16
+
17
+ **Files changed (source):**
18
+ - `src/core/CircuitBreakingIntelligenceProvider.ts`:
19
+ - new `FeatureMetricsRecorder` **interface** (kept local so core/ never imports
20
+ monitoring/ — no dependency cycle) + module-level recorder + `setFeatureMetricsRecorder()` /
21
+ `getFeatureMetricsRecorder()`.
22
+ - `evaluate()` records on all three exits — success (`noop`), inner failure (`error`),
23
+ and the circuit-open skip (`noop`, marking `waited`) — via a private `recordMetric()`
24
+ that **swallows all errors** (observability must never break the LLM path).
25
+ - `src/server/AgentServer.ts`: after constructing the `FeatureMetricsLedger`, call
26
+ `setFeatureMetricsRecorder(this.featureMetricsLedger)` once — a single injection point
27
+ that instruments every wrapped provider (current and future).
28
+
29
+ **Files changed (tests):**
30
+ - `tests/unit/CircuitBreaking-feature-metrics-tap.test.ts` — +8: success→noop, unlabeled
31
+ bucketing, failure→error+rethrow, the rate-limit wait path (`waited`+`waitMs`), the
32
+ circuit-open skip (no inner call, throws `LlmCircuitOpenError`), safe no-op with no
33
+ recorder, a throwing recorder never breaks the call, and an **end-to-end** test feeding
34
+ a real `FeatureMetricsLedger` and asserting the queryable rollup.
35
+
36
+ ## Blast radius
37
+
38
+ The hot LLM path gains one side-channel `record()` call per `evaluate()`. It is fully
39
+ isolated: `recordMetric()` is in a try/catch that swallows everything, and a null
40
+ recorder (CLI / no server) is a clean no-op. The breaker/rate-limit control flow is
41
+ **byte-identical** to before — all 74 existing CircuitBreaking + breaker-wait tests pass
42
+ unchanged. No route, config, or behavior of any gate changes.
43
+
44
+ ## Behavior delta
45
+
46
+ | Scenario | Before (1a) | After (1b) |
47
+ |---|---|---|
48
+ | an LLM gate calls `evaluate()` | nothing recorded | one per-feature row (latency, waited, success/error) |
49
+ | `/metrics/features` | alive but empty | populated as gates run |
50
+ | rate-limit bounded wait (#638) | invisible | recorded (`waited`/`waitMs`) |
51
+ | no recorder set (CLI) | n/a | no-op (no record) |
52
+ | metrics write throws | n/a | swallowed; LLM call unaffected |
53
+ | breaker / rate-limit control flow | as-is | **unchanged** (byte-identical) |
54
+
55
+ ## Outcome semantics (honest scoping)
56
+
57
+ At the funnel, `outcome` is `noop` for a completed call and `error` for a failure — the
58
+ **fired-vs-noop verdict** is the *caller's* interpretation of the result string, not
59
+ visible here, and **tokens** live in TokenLedger (the provider returns a string). So
60
+ Phase 1b delivers per-feature **call-count, latency (p50/p95), wait-rate, error-rate**;
61
+ the verdict + token-join are **Phase 2** (caller-side `recordEvent` / TokenLedger
62
+ attribution join). This is stated in the spec and the metric is labeled accordingly.
63
+
64
+ ## Risks considered
65
+
66
+ - **Dependency cycle (core→monitoring)?** Avoided — the provider depends only on a local
67
+ `FeatureMetricsRecorder` interface; the concrete ledger is injected at runtime.
68
+ - **Hot-path safety?** `recordMetric` swallows all errors; an end-to-end + a throwing-recorder
69
+ test prove the LLM path is never broken.
70
+ - **Coverage of all providers?** The module-level recorder is read by every
71
+ `CircuitBreakingIntelligenceProvider` instance (all created via the same wrap), so one
72
+ `setFeatureMetricsRecorder()` covers all current and future LLM features — no per-call-site wiring.
73
+
74
+ ## Migration parity
75
+
76
+ None needed — no agent-installed file changes (no hook/config/skill/CLAUDE.md template).
77
+ The `/metrics/features` awareness shipped in Phase 1a (#639); Phase 1b only makes the
78
+ existing endpoint produce data.
79
+
80
+ ## Tests / lint
81
+
82
+ 8 new + 74 existing CircuitBreaking/breaker tests pass; `npm run lint` (tsc +
83
+ destructive/LLM/URL-log/codex-drift) clean.
@@ -0,0 +1,82 @@
1
+ # Side-Effects Review — Per-feature LLM metrics (Phase 1a)
2
+
3
+ **Slug:** `llm-feature-metrics`
4
+ **Date:** 2026-05-31
5
+ **Author:** echo
6
+ **Spec:** `docs/specs/llm-feature-metrics-spec.md` (review-convergence + approved by Justin, Telegram 13435)
7
+
8
+ ## Summary of the change
9
+
10
+ Adds a read-only per-feature LLM observability ledger + endpoint, so every gate/
11
+ sentinel's cost (tokens, latency) and hit-rate becomes a tracked number and tuning
12
+ is evidence-based. **Phase 1a: the store + route only — no funnel edit, so zero
13
+ collision with the open #638** (which restructures the same `evaluate()` with
14
+ bounded-wait). Phase 1b (after #638) adds the ~3-line funnel tap that feeds it.
15
+
16
+ **Files changed (source):**
17
+ - `src/monitoring/FeatureMetricsLedger.ts` — NEW. SQLite-backed, read-mostly, modeled
18
+ on TokenLedger (opened via `NativeModuleHealer.openWithHealSync` for ABI resilience).
19
+ `record()` / `recordEvent()` append; `byFeature()` / `summary()` roll up per feature
20
+ (calls, tokens, fired/noop, fire-rate, p50/p95 latency, wait-stats). Never gates.
21
+ - `src/server/AgentServer.ts` — construct the ledger in its OWN try/catch inside the
22
+ stateDir block (cascade-isolated, mirroring FrameworkIssueLedger); add the field +
23
+ import; pass `featureMetricsLedger` into the route context.
24
+ - `src/server/routes.ts` — `GET /metrics/features` (mirrors `/tokens/summary`):
25
+ 503-stub on a null ledger; `?sinceHours=` / `?feature=` filters; add the ctx type field.
26
+ - `src/scaffold/templates.ts` — Agent Awareness: a "Per-Feature LLM Metrics" read-surface
27
+ blurb in the template Capabilities (new agents).
28
+ - `src/core/PostUpdateMigrator.ts` — Migration Parity: an idempotent, content-sniffed
29
+ (`/metrics/features`) `migrateClaudeMd` block (existing agents). Marker shared with the
30
+ template → no double-patch.
31
+ - `src/server/CapabilityIndex.ts` — classify the new `/metrics` route prefix as a
32
+ `featureMetrics` capability (surfaces in `GET /capabilities`), as the
33
+ capabilities-discoverability lint requires for every registered route prefix.
34
+
35
+ **Files changed (docs/tests):**
36
+ - `docs/specs/llm-feature-metrics-spec.md` (+ `.eli16.md`).
37
+ - `tests/unit/FeatureMetricsLedger.test.ts` (+8), `tests/unit/PostUpdateMigrator-metricsFeatures.test.ts` (+3),
38
+ `tests/integration/metrics-features-routes.test.ts` (+3), `tests/e2e/metrics-features-lifecycle.test.ts` (+3),
39
+ and the `/metrics/features` registration in `tests/unit/feature-delivery-completeness.test.ts`.
40
+
41
+ ## Blast radius
42
+
43
+ Confined to a new ledger + one new read route + CLAUDE.md awareness text. **No existing
44
+ gate, sentinel, job, or flow changes behavior.** The ledger is read-only observability
45
+ (same guarantee as TokenLedger); in Phase 1a nothing even writes to it in production yet
46
+ (the funnel tap is Phase 1b), so the live effect is purely: a new `/metrics/features`
47
+ endpoint that returns an empty-but-alive rollup.
48
+
49
+ ## Behavior delta
50
+
51
+ | Scenario | Before | After |
52
+ |---|---|---|
53
+ | `GET /metrics/features` | 404 (no route) | 200 + per-feature rollup (empty until Phase 1b) |
54
+ | ledger init fails / no stateDir | — | route 503-stubs (graceful, like /tokens) |
55
+ | existing LLM gates (tone, coherence, stop) | run as-is | **unchanged** (no funnel edit) |
56
+ | new agent init | no metrics awareness | template Capabilities mentions /metrics/features |
57
+ | existing agent update | no metrics awareness | migrateClaudeMd appends it once (idempotent, no double-patch) |
58
+
59
+ ## Risks considered
60
+
61
+ - **Collision with #638?** None — Phase 1a does not touch `CircuitBreakingIntelligenceProvider`
62
+ or any gate file #638 edits. The tap (Phase 1b) is built on top of #638's merged funnel.
63
+ - **Observability breaking the path it observes?** No — `record()` swallows write errors;
64
+ the ledger is a side-channel, never a gate (Close the Loop: measurement must not become a blocker).
65
+ - **ABI / native-module?** Opened via the same `NativeModuleHealer` path as TokenLedger.
66
+ - **Double-patch a fresh agent?** No — template + migration share the `/metrics/features`
67
+ content-sniff marker; covered by an explicit test.
68
+ - **Unbounded growth?** Same retention approach as TokenLedger (follow-up); Phase 1a writes
69
+ nothing in production yet.
70
+
71
+ ## Migration parity
72
+
73
+ Handled: template (new agents) + content-sniffed `migrateClaudeMd` (existing agents),
74
+ registered in `feature-delivery-completeness` `legacyMigratorSections` (a read-surface like
75
+ `/codex/usage` / `/tokens`, not a framework-shadowed user capability). No config default,
76
+ hook, or skill changed.
77
+
78
+ ## Tests
79
+
80
+ 3-tier, all green: unit (ledger + migration), integration (route 200/503/filter), E2E
81
+ (feature-is-alive on the real AgentServer init: 200 not 503, auth-gated, read-only).
82
+ `npm run lint` (tsc + destructive/LLM/URL-log/codex-drift) clean.
@@ -0,0 +1,46 @@
1
+ # Side effects — coherence-critical LLM rate-limit bounded wait
2
+
3
+ ## What changes at runtime
4
+
5
+ When the shared LLM circuit breaker is open (account rate-limited), a small set
6
+ of coherence-critical guards now WAIT (bounded) for the window to clear before
7
+ falling back, instead of instantly failing open. Everything else is unchanged.
8
+
9
+ ## Who is affected
10
+
11
+ - **Callers that do NOT set `rateLimitWaitMs`** (the vast majority — every
12
+ best-effort/observability LLM consumer): **zero behavior change.** When the
13
+ breaker is open they still throw `LlmCircuitOpenError` immediately and run
14
+ their existing fail-open fallback. Proven by a dedicated test (instant throw,
15
+ zero sleeps, inner provider never called).
16
+ - **MessagingToneGate** (outbound gate): when rate-limited, waits up to 120s for
17
+ the window, then falls back to its current pass-through. Net effect: under rate
18
+ limit, an outbound message may be delayed up to 2 min to be properly reviewed,
19
+ rather than sent unreviewed.
20
+ - **UnjustifiedStopGate**: waits up to 8s (short — it's on the Stop critical
21
+ path), then its existing fail-open behavior. A stop may be delayed up to 8s
22
+ under rate limit.
23
+ - **CoherenceGate high-stakes reviewers** (value-alignment, claim-provenance,
24
+ capability-accuracy, information-leakage): wait up to 60s. Other reviewers
25
+ unchanged (instant fail-open).
26
+
27
+ ## Blast radius
28
+
29
+ - 7 files, all in `src/core/`. No config keys, no new env, no migration (the
30
+ change is compiled source, not an agent-installed file/hook/skill).
31
+ - The breaker remains a single process-global singleton; no new global state
32
+ beyond a per-trip window length field.
33
+ - `isRateLimitError` detection is byte-identical (reimplemented as
34
+ `classifyRateLimit(m).isLimit`).
35
+
36
+ ## Failure modes considered
37
+
38
+ - **Latency under rate limit:** bounded by `maxWaitMs` per callsite; the
39
+ Stop-path wait is deliberately small (8s). A wait can never hang indefinitely.
40
+ - **Thundering herd on window-close:** `acquireOrWait` re-consults `acquire()`
41
+ so waiters serialize behind the single half-open probe; exactly one call
42
+ probes the provider on recovery.
43
+ - **Retry-after parse error:** parsed values are clamped to [1s, 15min]; any
44
+ unparseable/absurd value falls back to the flat 15-min default window.
45
+ - **Load shedding preserved:** best-effort high-volume callers keep failing open,
46
+ so they don't consume the scarce probe slot or re-trip the breaker.
@@ -0,0 +1,96 @@
1
+ # Side-Effects Review — Primary-developer mode (`updates.restartImmediately`)
2
+
3
+ **Slug:** `restart-immediately`
4
+ **Date:** 2026-06-01
5
+ **Author:** echo
6
+ **Spec:** `docs/specs/restart-immediately-spec.md` (approved by Justin, Telegram topic 13435)
7
+
8
+ ## Summary of the change
9
+
10
+ A per-agent, opt-in config flag `updates.restartImmediately` (default **false**).
11
+ When true, the agent's update restarts are never deferred — not for active
12
+ sessions (UpdateGate) and not for the restart window (AutoUpdater) — so it always
13
+ rolls onto the latest version as soon as it is downloaded. Intended for the
14
+ instar developer's own agent; explicitly **not** a fleet default.
15
+
16
+ **Files changed (source):**
17
+ - `src/core/UpdateGate.ts`:
18
+ - `UpdateGateConfig.alwaysRestartImmediately` (default false) + a constructor
19
+ default + `UpdateGateStatus.alwaysRestartImmediately`.
20
+ - `canRestart()` short-circuits at the very top when the flag is set:
21
+ `reset()` + return `{ allowed: true }` *before* listing sessions, so the
22
+ deferral clock never starts and the session monitor is never consulted.
23
+ - new `setAlwaysRestartImmediately(value)` — runtime toggle (clears an
24
+ in-flight deferral when turned on) for live `LiveConfig` edits.
25
+ - `src/core/AutoUpdater.ts`:
26
+ - `AutoUpdaterConfig.restartImmediately` (default false) + constructor default;
27
+ constructs `UpdateGate` with `{ alwaysRestartImmediately: this.config.restartImmediately }`.
28
+ - `gatedRestart()` skips the restart-window deferral when the flag is set
29
+ (session gate already satisfied inside `UpdateGate`).
30
+ - `reloadDynamicConfig()` re-reads `updates.restartImmediately` (both the
31
+ `LiveConfig` path and the file-read fallback) and pushes changes into the
32
+ gate via the setter.
33
+ - `AutoUpdaterStatus.restartImmediately` (sourced from the gate's status).
34
+ - `src/commands/server.ts`: maps `config.updates?.restartImmediately ?? false`
35
+ into the `AutoUpdater` config at construction.
36
+ - `src/core/types.ts`: `UpdateConfig.restartImmediately?: boolean` (typed).
37
+
38
+ **Files changed (tests):**
39
+ - `tests/unit/UpdateGate.test.ts` — +7 (new `alwaysRestartImmediately` describe):
40
+ allow-despite-healthy-active-session (with a baseline assertion that the same
41
+ fixture blocks by default), pure-no-deferral-clock, monitor-never-consulted,
42
+ default-false-still-blocks, runtime setter true→allowed (clears deferral) and
43
+ false→blocks again.
44
+ - `tests/unit/AutoUpdater.test.ts` — +2: default `restartImmediately` false;
45
+ `restartImmediately:true` reflected in `getStatus().restartImmediately`
46
+ (sourced from `gate.getStatus()` — proves the flag reached the real gate).
47
+
48
+ ## Blast radius
49
+
50
+ Default false ⇒ **zero fleet impact**. With the flag unset, `canRestart` runs
51
+ exactly as before (the new branch is skipped), `gatedRestart`'s window check is
52
+ unchanged (`!this.config.restartImmediately` is true), and the gate is
53
+ constructed with `alwaysRestartImmediately: false`. All 19 existing UpdateGate +
54
+ 18 existing AutoUpdater tests pass unmodified.
55
+
56
+ ## Behavior delta
57
+
58
+ | Scenario | Before | After (flag OFF — default) | After (flag ON) |
59
+ |---|---|---|---|
60
+ | healthy active session + update ready | defer indefinitely | defer indefinitely (unchanged) | restart now |
61
+ | outside restart window + update ready | defer to window | defer to window (unchanged) | restart now |
62
+ | same version within 30 min | cooldown skip | cooldown skip (unchanged) | cooldown skip (preserved) |
63
+ | two releases within ~15 min | dampener coalesces | dampener coalesces (unchanged) | dampener coalesces (preserved) |
64
+ | sessions during restart | survive (CONTINUATION) | survive | survive (unchanged) |
65
+ | `GET /updates/status` | no field | `restartImmediately: false` | `restartImmediately: true` |
66
+
67
+ ## Risks considered
68
+
69
+ - **Does it kill sessions?** No. A server restart bounces the server process;
70
+ tmux sessions survive and resume via CONTINUATION. The flag changes *when* the
71
+ server restarts, never *whether* sessions survive.
72
+ - **Restart loop / storm?** The same-version 30-min cooldown and the cascade
73
+ dampener are deliberately preserved — only genuinely back-to-back distinct
74
+ releases coalesce (≤15 min), and a flapping single version cannot re-restart.
75
+ - **Accidental fleet-wide enablement?** Default false + no `migrateConfig` entry
76
+ ⇒ existing and new agents are false-by-absence; only an explicit per-agent
77
+ config edit turns it on. This is intentional (the directive forbids a fleet
78
+ default).
79
+ - **Stale cached flag after a live edit?** `reloadDynamicConfig()` re-reads it
80
+ each tick and pushes it into the gate, and the setter clears any in-flight
81
+ deferral — so enabling it on disk takes effect at the next tick without a
82
+ restart.
83
+
84
+ ## Migration parity
85
+
86
+ No agent-installed file changes (no hook/skill/CLAUDE.md template). A new config
87
+ default is normally added to `migrateConfig()` — **intentionally not done here**:
88
+ the flag must default to false for the fleet, and absence already yields false in
89
+ code. Adding a fleet migration that set it true would violate the directive
90
+ ("not for all instar agents"). Echo's own `.instar/config.json` is set directly
91
+ (operational, outside this PR).
92
+
93
+ ## Tests / lint
94
+
95
+ 19 UpdateGate (+7 new) + 18 AutoUpdater (+2 new) tests pass; `npm run lint`
96
+ (tsc + destructive/LLM/URL-log/codex-drift) clean.