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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +3 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/AutoUpdater.d.ts +12 -0
- package/dist/core/AutoUpdater.d.ts.map +1 -1
- package/dist/core/AutoUpdater.js +21 -2
- package/dist/core/AutoUpdater.js.map +1 -1
- package/dist/core/CircuitBreakingIntelligenceProvider.d.ts +26 -0
- package/dist/core/CircuitBreakingIntelligenceProvider.d.ts.map +1 -1
- package/dist/core/CircuitBreakingIntelligenceProvider.js +56 -5
- package/dist/core/CircuitBreakingIntelligenceProvider.js.map +1 -1
- package/dist/core/CoherenceGate.d.ts.map +1 -1
- package/dist/core/CoherenceGate.js +21 -1
- package/dist/core/CoherenceGate.js.map +1 -1
- package/dist/core/CoherenceReviewer.d.ts +6 -0
- package/dist/core/CoherenceReviewer.d.ts.map +1 -1
- package/dist/core/CoherenceReviewer.js +3 -0
- package/dist/core/CoherenceReviewer.js.map +1 -1
- package/dist/core/LlmCircuitBreaker.d.ts +52 -2
- package/dist/core/LlmCircuitBreaker.d.ts.map +1 -1
- package/dist/core/LlmCircuitBreaker.js +134 -11
- package/dist/core/LlmCircuitBreaker.js.map +1 -1
- package/dist/core/MessagingToneGate.d.ts.map +1 -1
- package/dist/core/MessagingToneGate.js +7 -0
- package/dist/core/MessagingToneGate.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +17 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/UnjustifiedStopGate.d.ts.map +1 -1
- package/dist/core/UnjustifiedStopGate.js +9 -0
- package/dist/core/UnjustifiedStopGate.js.map +1 -1
- package/dist/core/UpdateGate.d.ts +17 -0
- package/dist/core/UpdateGate.d.ts.map +1 -1
- package/dist/core/UpdateGate.js +23 -0
- package/dist/core/UpdateGate.js.map +1 -1
- package/dist/core/types.d.ts +14 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/FeatureMetricsLedger.d.ts +101 -0
- package/dist/monitoring/FeatureMetricsLedger.d.ts.map +1 -0
- package/dist/monitoring/FeatureMetricsLedger.js +185 -0
- package/dist/monitoring/FeatureMetricsLedger.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +5 -0
- 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 +27 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +11 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +18 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/TopicLinkageHandler.d.ts.map +1 -1
- package/dist/threadline/TopicLinkageHandler.js +9 -4
- package/dist/threadline/TopicLinkageHandler.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates.ts +5 -0
- package/upgrades/1.3.179.md +68 -0
- package/upgrades/1.3.180.md +133 -0
- package/upgrades/side-effects/llm-feature-metrics-phase1b.md +83 -0
- package/upgrades/side-effects/llm-feature-metrics.md +82 -0
- package/upgrades/side-effects/llm-ratelimit-bounded-wait.md +46 -0
- 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.
|