instar 1.3.671 → 1.3.673
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 +76 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/core/MessagingToneGate.d.ts +44 -0
- package/dist/core/MessagingToneGate.d.ts.map +1 -1
- package/dist/core/MessagingToneGate.js +92 -18
- package/dist/core/MessagingToneGate.js.map +1 -1
- package/dist/monitoring/EnforcedTerminationWatchdog.d.ts +91 -0
- package/dist/monitoring/EnforcedTerminationWatchdog.d.ts.map +1 -0
- package/dist/monitoring/EnforcedTerminationWatchdog.js +172 -0
- package/dist/monitoring/EnforcedTerminationWatchdog.js.map +1 -0
- package/dist/monitoring/enforcedTermination.d.ts +105 -0
- package/dist/monitoring/enforcedTermination.d.ts.map +1 -0
- package/dist/monitoring/enforcedTermination.js +105 -0
- package/dist/monitoring/enforcedTermination.js.map +1 -0
- package/dist/monitoring/enforcedTerminationWiring.d.ts +18 -0
- package/dist/monitoring/enforcedTerminationWiring.d.ts.map +1 -0
- package/dist/monitoring/enforcedTerminationWiring.js +80 -0
- package/dist/monitoring/enforcedTerminationWiring.js.map +1 -0
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +10 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/server/AgentServer.d.ts +3 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/outboundGateBudget.d.ts +1 -1
- package/dist/server/outboundGateBudget.d.ts.map +1 -1
- package/dist/server/outboundGateBudget.js +15 -1
- package/dist/server/outboundGateBudget.js.map +1 -1
- package/dist/server/routes.d.ts +3 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +33 -11
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +47 -47
- package/upgrades/1.3.672.md +63 -0
- package/upgrades/1.3.673.md +50 -0
- package/upgrades/side-effects/enforced-termination-watchdog.md +95 -0
- package/upgrades/side-effects/tone-gate-graceful-degradation.md +211 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Side-Effects Review — Tone-Gate Graceful Degradation (Postmortem F4)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `tone-gate-graceful-degradation`
|
|
4
|
+
**Date:** `2026-06-25`
|
|
5
|
+
**Author:** `Echo (instar-dev agent)`
|
|
6
|
+
**Second-pass reviewer:** `required (outbound block/allow decision + "gate") — verdict + resolution appended at the end`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`MessagingToneGate.review()` is the outbound block/allow authority on the Telegram
|
|
11
|
+
reply path. Its provider-exhaustion catch branch was **fail-CLOSED by default**:
|
|
12
|
+
when the LLM backend was unavailable (breaker-open / rate-limit / transport error),
|
|
13
|
+
it HELD every message unconditionally. Tonight that turned a `claude -p` rate-limit
|
|
14
|
+
into a multi-hour silent outbound outage — the user saw delivery receipts but no
|
|
15
|
+
replies. This change makes the outage path **degrade to an in-process deterministic
|
|
16
|
+
leak floor** (`detectGateSignals` B1–B7 + `detectInternalIdLeak`, both pre-existing
|
|
17
|
+
pure detectors): a clean message SENDS, a leaked artifact still HOLDS.
|
|
18
|
+
|
|
19
|
+
The same rate-limit outage has **two manifestations**, and this change covers BOTH:
|
|
20
|
+
the FAST throw (breaker open → `review()` rejects) degrades inside `review()`; the
|
|
21
|
+
SLOW stall (the gate waits up to 120s for a rate-limit window and overruns the
|
|
22
|
+
outbound route budget — the DOCUMENTED 2026-06-08 production failure) degrades at
|
|
23
|
+
the route seam in `reviewWithinBudget`, via the shared `buildDegradedToneResult`.
|
|
24
|
+
The capacity-shed (fork-bomb P3) path and the discipline-failure path are UNCHANGED.
|
|
25
|
+
|
|
26
|
+
Files: `src/core/MessagingToneGate.ts` (the shared `buildDegradedToneResult` +
|
|
27
|
+
`degradedToDeterministic` result flag + `DegradeReason`), `src/server/outboundGateBudget.ts`
|
|
28
|
+
(budget-timeout degrade callback), `src/server/routes.ts` (three-valued route disposition),
|
|
29
|
+
`tests/unit/MessagingToneGate.test.ts`, `tests/unit/spawn-cap-fail-closed-gates.test.ts`,
|
|
30
|
+
`tests/unit/outbound-gate-budget.test.ts`, plus the spec + ELI16 + this artifact.
|
|
31
|
+
|
|
32
|
+
## Decision-point inventory
|
|
33
|
+
|
|
34
|
+
- `MessagingToneGate.review()` provider-exhaustion catch branch — **modify** — was pure-hold; now degrades to the deterministic floor by default (clean sends, leak holds).
|
|
35
|
+
- `reviewWithinBudget` budget-timeout disposition (route seam) — **modify** — the slow-stall timeout was pure-hold (`GATE_TIMEOUT`) under the default; now degrades to the SAME floor by default (the slow sibling of the fast throw). Pure-hold/fail-open overrides still reachable.
|
|
36
|
+
- `MessagingToneGate.review()` capacity-shed branch (`isCapacityUnavailable`) — **pass-through** — still pure-hold (fork-bomb P3 invariant preserved).
|
|
37
|
+
- `MessagingToneGate.review()` discipline-failure branch (unparseable/contradictory after one re-prompt) — **pass-through** — still pure-hold (a model that produced an unusable verdict is not an infra outage).
|
|
38
|
+
- `messaging.toneGate.failClosedOnExhaustion` config flag — **modify** — was 2-valued (default-true hold / false open); now 3-valued (unset = degrade, true = pure-hold, false = open) on BOTH the throw and the budget-timeout paths.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 1. Over-block
|
|
43
|
+
|
|
44
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
45
|
+
|
|
46
|
+
The change strictly REDUCES over-block on the provider-exhaustion path: messages
|
|
47
|
+
that were previously held during a backend outage now send if they carry no
|
|
48
|
+
deterministic leak. The only inputs still held on the degraded path are those
|
|
49
|
+
carrying a real B1–B7 artifact (a literal CLI command, file path, config key,
|
|
50
|
+
code block, API endpoint, env var, cron/slug) or a B20 internal-id leak — the
|
|
51
|
+
exact high-stakes content that must never escape. A message that legitimately
|
|
52
|
+
*discusses* such an artifact in prose (e.g. "I'll run the migration for you")
|
|
53
|
+
does NOT trip the detectors (verified by the GateSignalDetectors negative tests)
|
|
54
|
+
and sends normally.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 2. Under-block
|
|
59
|
+
|
|
60
|
+
**What failure modes does this still miss?**
|
|
61
|
+
|
|
62
|
+
On the degraded path the behavioral rules (B11–B20 tone/self-stop/false-blocker/
|
|
63
|
+
parked-on-user/jargon) are NOT evaluated — they require LLM judgment that is, by
|
|
64
|
+
definition, unavailable during the outage that triggers this path. So during a
|
|
65
|
+
backend outage a slightly-off-tone or self-stop-shaped message could reach the
|
|
66
|
+
user unchecked. This is a deliberate, bounded trade: a leak (the dangerous class)
|
|
67
|
+
is still caught deterministically; a tone slip (the recoverable class) reaching
|
|
68
|
+
the user beats silence (the F4 goal). When the backend recovers, full LLM review
|
|
69
|
+
resumes automatically. Operators who want the strict behavior restore pure-hold
|
|
70
|
+
with `failClosedOnExhaustion: true`.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 3. Level-of-abstraction fit
|
|
75
|
+
|
|
76
|
+
The degrade floor is the right layer: it REUSES the existing low-level
|
|
77
|
+
deterministic detectors (`detectGateSignals`, `detectInternalIdLeak`) that the
|
|
78
|
+
gate already calls to build its LLM prompt — it does not re-implement detection.
|
|
79
|
+
The authority (send/hold) stays where it belongs, inside `review()`. No higher
|
|
80
|
+
gate exists that should own this; `review()` IS the outbound authority. The
|
|
81
|
+
change feeds an existing primitive into an existing authority's fallback path —
|
|
82
|
+
no new abstraction introduced.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 4. Signal vs authority compliance
|
|
87
|
+
|
|
88
|
+
**Required reference:** `docs/signal-vs-authority.md`
|
|
89
|
+
|
|
90
|
+
- [x] No — this change reuses EXISTING signal-producers (`detectGateSignals`,
|
|
91
|
+
`detectInternalIdLeak`) feeding the EXISTING smart authority (`review()`). It
|
|
92
|
+
adds no new brittle blocking check.
|
|
93
|
+
|
|
94
|
+
The deterministic detectors are pure signal-producers; the gate is the authority.
|
|
95
|
+
On the degraded path the authority consults those signals deterministically
|
|
96
|
+
(LLM unavailable) and fails CLOSED on any positive signal. This is the
|
|
97
|
+
signal-vs-authority pattern applied exactly as intended — the brittle detectors
|
|
98
|
+
never gained new authority; they inform a fallback disposition that defaults to
|
|
99
|
+
holding on any leak signal.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 5. Interactions
|
|
104
|
+
|
|
105
|
+
- **Shadowing:** the degrade branch runs ONLY in the provider-exhaustion catch,
|
|
106
|
+
after the LLM call throws. It cannot shadow the normal-verdict path (that path
|
|
107
|
+
`return`s before the catch) or the capacity-shed branch (which `return`s first
|
|
108
|
+
inside the catch). Ordering verified: capacity-shed check precedes the
|
|
109
|
+
provider-error disposition.
|
|
110
|
+
- **Double-fire:** none — a single `review()` call returns exactly one
|
|
111
|
+
disposition.
|
|
112
|
+
- **Races:** none — `review()` is a pure per-call function over its `text` +
|
|
113
|
+
live-read config; it shares no mutable state with concurrent calls.
|
|
114
|
+
- **Feedback loops:** none — the floor reads the candidate text only; it does not
|
|
115
|
+
feed any system that feeds back into the gate.
|
|
116
|
+
- The downstream retry path (`failClosedOnExhaustion` hold → queued for retry) is
|
|
117
|
+
unchanged for the held cases; a now-SENT clean message simply skips that queue.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 6. External surfaces
|
|
122
|
+
|
|
123
|
+
- **Other agents / users:** none directly. The install-base effect is the intended
|
|
124
|
+
one: during a backend outage, outbound replies degrade-and-send instead of
|
|
125
|
+
silently holding.
|
|
126
|
+
- **External systems (Telegram):** a clean message now reaches Telegram during an
|
|
127
|
+
outage that previously held it. The held-leak case is byte-identical to before
|
|
128
|
+
(still `pass:false`, queued for retry).
|
|
129
|
+
- **Persistent state:** none — no new state written.
|
|
130
|
+
- **Operator surface:** no NEW operator surface. The change extends the value
|
|
131
|
+
semantics of the existing `messaging.toneGate.failClosedOnExhaustion` config
|
|
132
|
+
flag (a config edit, already documented) — it adds the `unset = degrade`
|
|
133
|
+
default and keeps `true`/`false` working. No new PIN-gated route or dashboard
|
|
134
|
+
form.
|
|
135
|
+
- **Result shape:** adds an OPTIONAL `degradedToDeterministic?: boolean` to
|
|
136
|
+
`ToneReviewResult`. Existing consumers that don't read it are unaffected (the
|
|
137
|
+
`pass`/`rule`/`issue`/`failedClosed` contract is preserved).
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 6b. Operator-surface quality
|
|
142
|
+
|
|
143
|
+
No operator surface — not applicable. The change touches no dashboard renderer,
|
|
144
|
+
approval page, or grant/revoke/secret-drop form. The only operator-facing element
|
|
145
|
+
is the pre-existing `failClosedOnExhaustion` config flag (a JSON edit), whose
|
|
146
|
+
default and overrides are documented in the spec.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
151
|
+
|
|
152
|
+
**machine-local BY DESIGN.** The tone gate is a pure, stateless, per-message
|
|
153
|
+
function evaluated on whichever machine is serving that outbound reply. It holds
|
|
154
|
+
no durable state, emits no cross-machine notice, and generates no URL. Given the
|
|
155
|
+
same candidate text + the same `failClosedOnExhaustion` config, the disposition
|
|
156
|
+
is identical on every machine — so there is nothing to replicate or proxy. It
|
|
157
|
+
does not strand on topic transfer (no state), needs no one-voice gating (it does
|
|
158
|
+
not itself emit a user notice; it gates the agent's own reply, which is already
|
|
159
|
+
single-voiced per topic), and creates no machine-bound link.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 8. Rollback cost
|
|
164
|
+
|
|
165
|
+
Cheap and instant, two independent levers:
|
|
166
|
+
1. **Config (no deploy):** set `messaging.toneGate.failClosedOnExhaustion: true`
|
|
167
|
+
to restore the exact legacy pure-hold behavior on the provider-error path.
|
|
168
|
+
Read live per review — no restart.
|
|
169
|
+
2. **Code:** revert the single commit. The change is isolated to one method's
|
|
170
|
+
catch branch + one shared helper + the route seam + one optional result field;
|
|
171
|
+
no migration, no state, no schema. No data repair or agent-state repair needed.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Phase 5 — Second-pass review (independent reviewer)
|
|
176
|
+
|
|
177
|
+
An independent reviewer subagent audited the change (code diff, `GateSignalDetectors`,
|
|
178
|
+
`internal-id-leak`, the artifact, both test files, and ran the suites).
|
|
179
|
+
|
|
180
|
+
**Verdict:** Concur with the core change — leak-safe, control-flow-correct, tests
|
|
181
|
+
honest — with ONE substantive concern.
|
|
182
|
+
|
|
183
|
+
- **Leak safety — PASS.** The degraded floor (B1–B7 + B20) is exactly the tone
|
|
184
|
+
gate's high-stakes artifact-leak class; everything else is behavioral/tone
|
|
185
|
+
(recoverable). Secrets/API-keys are redacted separately on BOTH paths (not a
|
|
186
|
+
regression). The floor is strictly *stricter* than the LLM on the leak class
|
|
187
|
+
(holds on any positive detection), so degrade can only over-hold, never
|
|
188
|
+
under-send a leak.
|
|
189
|
+
- **Control flow — PASS.** Capacity-shed returns before the provider-error
|
|
190
|
+
disposition; the degrade branch cannot be reached for a discipline failure
|
|
191
|
+
(that path returns inside the `try`).
|
|
192
|
+
- **3-valued flag — PASS.** `true`→hold, `false`→open, `undefined`→degrade; a
|
|
193
|
+
throwing config getter yields `{}`→`undefined`→degrade (safe default).
|
|
194
|
+
- **Tests — PASS.** Non-vacuous inputs; the override test holds a message that
|
|
195
|
+
would otherwise send.
|
|
196
|
+
|
|
197
|
+
**Concern raised (resolved in this change):** the original revision fixed only the
|
|
198
|
+
provider-*throw* manifestation; the provider-*slow* (budget-timeout) sibling — the
|
|
199
|
+
DOCUMENTED 2026-06-08 production failure — still pure-held under the default, so
|
|
200
|
+
the F4 goal was only partially met and the artifact overstated the fix.
|
|
201
|
+
|
|
202
|
+
**Resolution:** rather than defer (no-deferrals standard), the slow path was closed
|
|
203
|
+
in this same change. `reviewWithinBudget` now takes a `budgetDegrade` callback; the
|
|
204
|
+
route passes it under the default disposition so a budget timeout degrades to the
|
|
205
|
+
SAME `buildDegradedToneResult` floor (clean sends, leak holds). Two new tests in
|
|
206
|
+
`outbound-gate-budget.test.ts` prove the slow-path clean-send and leak-hold. The
|
|
207
|
+
artifact summary + decision inventory above were corrected to describe both paths.
|
|
208
|
+
The reviewer's two minor notes (dead capacity-arm in the degrade helper; B12
|
|
209
|
+
health-jargon passing on degrade) were also addressed — the dead arm was removed in
|
|
210
|
+
the `buildDegradedToneResult` refactor, and the B12 behavioral drop is documented as
|
|
211
|
+
accepted in §2.
|