instar 1.3.1007 → 1.3.1009

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.
@@ -0,0 +1,142 @@
1
+ # Side-Effects Review — Bounded mentor-onboarding retries
2
+
3
+ **Version / slug:** `bounded-mentor-onboarding-retries`
4
+ **Date:** `2026-07-27`
5
+ **Author:** `Instar Agent (instar-codey)`
6
+ **Review tier:** Tier 1, user-directed
7
+
8
+ ## Summary of the change
9
+
10
+ `OutstandingPromptTracker` extends its existing durable correlation ledger with a content-keyed retry ledger. Live mentor delivery reserves an attempt before transport, permits at most three attempts for the same unanswered normalized content, keeps new content independent, and persists a one-shot exhaustion escalation. `runMentorTick` now awaits the delivery callback and exposes the delivery layer's structured reason.
11
+
12
+ The tracker persists only SHA-256 content keys plus mentee, attempt, timestamp, and escalation metadata. The ledger holds at most 64 unresolved content keys. Existing version-1 files load and upgrade on the next write.
13
+
14
+ ## Decision-point inventory
15
+
16
+ - `OutstandingPromptTracker.reserveSend` — **new authority inside an existing authority** — permits or refuses one mentor send based on in-flight state, content-key attempts, storage availability, and the fixed ledger capacity.
17
+ - `OutstandingPromptTracker.clearByCorr` — **modified** — a correlated reply now closes both the outstanding correlation and that content's retry episode.
18
+ - `OutstandingPromptTracker.markDeliveryFailed` — **new transition** — removes the in-flight correlation while retaining the consumed attempt.
19
+ - `AgentServer.deliverToMentee` — **modified actuation boundary** — durably reserves before transport and returns a structured delivery outcome.
20
+ - `runMentorTick` — **modified observer** — awaits the delivery boundary and records its reason; it does not decide the retry policy.
21
+ - `DegradationReporter.report` — **existing signal path** — emits one delivery-exhaustion event after the durable latch admits it.
22
+
23
+ ---
24
+
25
+ ## 1. Over-block
26
+
27
+ The brake can suppress a prompt whose wording is identical even if the mentor intended a semantically separate repetition. That only happens after three sends without a correlated reply. A confirmed reply deletes the content episode, so later reuse after a real conversation is allowed. A genuinely different agenda item hashes separately and remains eligible.
28
+
29
+ Whitespace is normalized before hashing. Cosmetic spacing cannot evade the brake, but punctuation or wording changes produce a new key. This is deliberate: the gate refuses exact normalized repetition, not semantic similarity judged by a brittle classifier.
30
+
31
+ The 64-key ledger cap can refuse a novel prompt after 64 unresolved content episodes. That is a fail-closed capacity bound. It prefers pausing the autonomous mentor over allowing either unbounded state growth or eviction that would silently reopen old breakers.
32
+
33
+ ---
34
+
35
+ ## 2. Under-block
36
+
37
+ An LLM can paraphrase the same intent and receive a new content key. This patch closes the observed identical-content loop, not semantic-repetition detection. Semantic dedupe would require judgment and risks suppressing legitimately revised questions; it is intentionally outside this Tier 1 correction.
38
+
39
+ A corrupt version-2 file follows the tracker's existing corruption behavior and starts with fresh in-memory state. Normal restarts over valid state preserve the brake, which is the requested failure mode. Hardening corrupt-state recovery into a fail-closed operator repair state is a broader storage-policy change and is not smuggled into this patch.
40
+
41
+ Version-1 outstanding entries have no historical content key. They preserve the existing in-flight refusal but cannot retroactively consume a content attempt. The first post-upgrade content reservation begins the new bounded history.
42
+
43
+ ---
44
+
45
+ ## 3. Level-of-abstraction fit
46
+
47
+ The retry authority belongs in `OutstandingPromptTracker`, which already owns the durable question “may this mentor send another prompt to this mentee?” Adding a second disconnected store would create competing truth for the same actuation boundary.
48
+
49
+ Content normalization and hashing live beside the attempt state. `AgentServer` owns orchestration: reserve, call transport, mark immediate failure, append sent history, and emit degradation. `runMentorTick` only awaits and surfaces the structured outcome. No transport adapter is taught mentor-specific policy.
50
+
51
+ ---
52
+
53
+ ## 4. Signal vs authority compliance
54
+
55
+ **Required reference:** `docs/signal-vs-authority.md`
56
+
57
+ The static retry count is appropriate hard authority because “never perform identical autonomous sends without a bound” is a safety invariant, not a competing-signals judgment. The gate uses exact normalized content, explicit counters, and durable state. It does not guess user intent, message quality, or semantic equivalence.
58
+
59
+ The degradation event is signal-only. It cannot reopen the breaker, alter the agenda, change transport routing, or authorize another send. The durable tracker remains the sole authority.
60
+
61
+ ---
62
+
63
+ ## 4b. Judgment-point check
64
+
65
+ No LLM or heuristic decides whether the retry budget is exhausted. The decision inputs are a stable content key, integer attempt count, fixed maximum, outstanding correlation, and store health.
66
+
67
+ The number three is a conservative structural ceiling for an existing retry loop that previously had no ceiling. It is not exposed as configuration in this patch, avoiding a migration surface and preventing an operator from accidentally configuring the invariant away.
68
+
69
+ ---
70
+
71
+ ## 5. Interactions
72
+
73
+ - **Outstanding timeout:** an expired correlation is still swept, but its content attempt remains. Expiry permits evaluation of the next attempt; it no longer resets history.
74
+ - **Immediate transport refusal:** the pre-send reservation remains counted while the outstanding correlation is removed, so a dead transport reaches the same breaker without waiting twenty minutes per attempt.
75
+ - **Successful delivery:** the correlation stays outstanding until its matching reply or timeout.
76
+ - **Matching reply:** clears the outstanding row and content retry episode.
77
+ - **Late reply after timeout:** remains classified by the existing late/unknown-correlation behavior and does not silently reopen an exhausted content key.
78
+ - **Restart:** entries, retry counts, and `escalatedAt` reload from one atomic JSON file.
79
+ - **Concurrent tick:** the runner's existing single in-flight guard plus the tracker's per-mentee outstanding check prevents overlapping reservations in this process.
80
+ - **Multi-machine:** state remains machine-local, matching the existing mentor runner's single active process. This patch adds no replicated authority.
81
+ - **Escalation flood:** `recordRetryExhaustionEscalated` persists the latch before the degradation call. Repeated ticks see the open breaker and decline the signal.
82
+ - **State growth:** 64 unresolved keys is a hard ceiling; raw message content is absent from this store.
83
+
84
+ ---
85
+
86
+ ## 6. External surfaces
87
+
88
+ `GET /mentor/status.lastResult` can now include `deliveryReason` for a live tick. Existing fields and status codes remain unchanged; JSON omits the optional field when no delivery-layer reason exists.
89
+
90
+ Operators may receive one existing-style degradation alert under the new feature name `mentor.delivery-unconfirmed-retry-exhausted`. Its content describes a delivery failure and the automatic suppression. It does not ask the operator to edit files, run commands, or diagnose raw internals.
91
+
92
+ No Telegram API call shape, Threadline identity rule, bot token handling, topic selection, endpoint, config key, or dashboard surface changes.
93
+
94
+ ---
95
+
96
+ ## 6b. Operator-surface quality
97
+
98
+ The only operator-facing addition is the existing degradation channel's plain-language event. It leads with the condition and effect: prompt delivery could not be confirmed, so identical sends stopped. It exposes no hash, correlation id, state path, token, or stack trace as primary content. There is no destructive action. The message is short enough for phone width and introduces no form or technical input.
99
+
100
+ ---
101
+
102
+ ## 7. Multi-machine posture
103
+
104
+ **Posture: intentionally machine-local.** The mentor runner and its outstanding-prompt tracker already live on the machine executing Echo's mentor tick. A send is reserved and actuated by that same process. Replicating attempt authority without a cross-machine single-writer contract would create two counters that could each admit three sends.
105
+
106
+ This patch preserves the existing one-process ownership boundary. If mentor execution becomes active-active in the future, the tracker must move behind a shared claim/lease before that rollout; this change does not claim cross-machine safety for a topology that is not currently enabled.
107
+
108
+ ---
109
+
110
+ ## 8. Rollback cost
111
+
112
+ Reverting the code restores version-1 behavior. Older code reads the version-2 file as unsupported and starts fresh, so rollback removes the brake until the prior version is restored. No database migration or user-data conversion is required. The file contains only machine-local mentor retry metadata and can be regenerated.
113
+
114
+ The additive `deliveryReason` field disappears on rollback. Existing consumers already tolerate its absence.
115
+
116
+ ---
117
+
118
+ ## Class-Closure Declaration
119
+
120
+ **Defect class:** `unbounded-self-action`
121
+ **Closure:** `guard`
122
+
123
+ The guard is the durable pre-transport reservation plus the three-attempt content-key breaker. Unit tests prove the first send, cap, independent novel content, restart persistence, one-shot escalation, normalization, and capacity bound. The production-wiring E2E test drives three refused transport attempts, requires the fourth identical attempt to be suppressed, and requires a new message to retain an independent attempt.
124
+
125
+ Steady state for one unanswered content key is therefore: at most three outbound attempts, one exhaustion signal, then permanent suppression in valid durable state. The escalation path cannot recursively send another mentor prompt.
126
+
127
+ ---
128
+
129
+ ## Evidence pointers
130
+
131
+ - Refusal-first: four new tracker tests failed against unmodified source with `reserveSend is not a function`; ten pre-existing tracker tests remained green.
132
+ - Focused proof after implementation: 77/77 across tracker, tick, runner, route integration, production lifecycle, and mentor config hot-read coverage.
133
+ - Static validation: `npm run build` and `npm run lint` pass.
134
+ - No route, config, transport identity, LLM breaker, cadence, budget, or agenda change.
135
+
136
+ ---
137
+
138
+ ## Causal autopsy
139
+
140
+ **Origin:** latent.
141
+
142
+ The correlation-scoped anti-ping-pong guard was correct within one reply window but treated timeout expiry as the end of all retry history. That latent reset became an unbounded loop when Telegram visibly accepted bot-authored posts that the mentee bot could not ingest. Every correlation expired normally, so the system repeatedly regenerated and resent identical content while believing each attempt was new.
@@ -0,0 +1,120 @@
1
+ # Side-effects review — `GET /completion-claim/stats`
2
+
3
+ **Change:** adds a read-only counters endpoint for the completion-claim verifier, and corrects
4
+ the `rollout-evidence-ref` in `docs/specs/claim-verification-sentinel.md` from
5
+ `/completion-claim-verification/stats` (a prefix that does not exist) to `/completion-claim/stats`.
6
+
7
+ **Discovered:** 2026-07-27, while checking — before proposing a new feature — whether a
8
+ claim-review capability already existed. It did, and it could not graduate.
9
+
10
+ ## The condition
11
+
12
+ `docs/specs/claim-verification-sentinel.md` is `status: approved`, `rollout-disposition: active`,
13
+ `rollout-evidence-type: endpoint`, with a graduation metric of
14
+ `classified-completion-claims >= 1`. Verified on `main`:
15
+
16
+ - `CompletionClaimVerifier` exists and **is** constructed (`src/server/AgentServer.ts:2572`).
17
+ - `monitoring.completionClaimVerification` config is present with `dryRun: true`.
18
+ - `POST /completion-claim/observe` and `GET /completion-claim/audit` are live —
19
+ `/completion-claim/audit` returns 200 and was **actively recording** (records dated
20
+ `2026-07-27T17:46:20.971Z`, `dryRun: true`, verdicts such as `uncorroborated-unknown`).
21
+ - `CompletionClaimVerifier.stats()` is implemented (`src/monitoring/CompletionClaimVerifier.ts:231`)
22
+ and **called by no route**.
23
+ - `/completion-claim/stats` → 404. `/completion-claim-verification/stats` (the spec's ref) → 404.
24
+
25
+ So the feature runs, observes, and records — while the evidence its own spec nominates for
26
+ graduation is unreadable. It cannot progress, and nothing surfaces that it is stuck.
27
+
28
+ **Correction of record:** an earlier filing of this said "the route does not exist anywhere."
29
+ That was wrong — the observe and audit routes exist under a different prefix. The accurate
30
+ finding is narrower: no *stats* surface exists at any prefix, and the spec's ref points at a
31
+ prefix that never did. Recorded rather than quietly amended.
32
+
33
+ ## 1. Over-block — what legitimate input does this reject?
34
+
35
+ None. New read-only route; rejects nothing that previously worked. It 503s when the verifier is
36
+ absent, which is the same contract the sibling audit route already uses — deliberately matched so
37
+ "feature off" reads identically across both surfaces rather than one 404ing and one 503ing.
38
+
39
+ ## 2. Under-block — what does this still miss?
40
+
41
+ - **It does not make the feature graduate.** It makes graduation *evaluable*. Whether the
42
+ criterion is met is a separate, later, evidence-driven decision that belongs to the operator.
43
+ - **Local scope only.** The audit route already owns the pool projection; adding a second
44
+ cross-machine fan-out for the same underlying data would duplicate a surface rather than serve
45
+ one. A pool view of counters is a follow-up if it is ever wanted, not a gap this change creates.
46
+ - **It does not audit the wider class.** This is one instance of "a spec nominates rollout
47
+ evidence that was never built". Whether other `rollout-evidence-ref` values resolve is an
48
+ open question this change does not answer. <!-- tracked: ACT-1394 -->
49
+
50
+ ## 3. Level-of-abstraction fit
51
+
52
+ Correct layer, and deliberately the *thinnest* one. `stats()` already existed with the right
53
+ shape; this exposes it and adds nothing else. The alternative — computing counters in the route —
54
+ would have created a second source of truth for numbers the verifier already maintains.
55
+
56
+ The spec-ref correction belongs in the same change because a stats route at a path the spec does
57
+ not name would leave the rollout check just as unevaluable as before, only less obviously.
58
+
59
+ ## 4. Signal vs authority compliance
60
+
61
+ Pure signal, and structurally incapable of being anything else: a GET that reads a counters
62
+ snapshot. It gates nothing, blocks nothing, and mutates nothing. `stats()` returns a deep copy
63
+ (`JSON.parse(JSON.stringify(...))`), so a caller cannot reach through the response into live
64
+ counters. A test asserts the route touches no mutation entry point — `recordDisposition` and
65
+ `recordCanaryDrift` throw if called during the request.
66
+
67
+ ## 5. Interactions
68
+
69
+ - **Sibling route.** Shares the 503-when-absent contract with `/completion-claim/audit`; no
70
+ shared state, no ordering dependency, and reading stats does not disturb the audit ring.
71
+ - **Rollout tracking.** The corrected `rollout-evidence-ref` is what the rollout machinery reads.
72
+ Before this change it pointed at a 404, so any automated evidence check would have failed or
73
+ silently found nothing; after it, the check resolves.
74
+ - **Metric naming.** The response carries both `stats.classifiedTurns` (the internal field) and
75
+ `classified-completion-claims` (the spec's name) so a rollout check does not need to know the
76
+ internal field name. Duplication is deliberate and one-directional — the spec name mirrors the
77
+ counter, never the reverse.
78
+
79
+ ## 6. External surfaces
80
+
81
+ One new authenticated GET. No wire-format change to any existing route, no schema, no migration,
82
+ no config, no persisted state. The only non-route change is a single frontmatter line in a spec.
83
+
84
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
85
+
86
+ **Machine-local by design, and correctly so — no durable state is introduced.** The counters
87
+ describe what *this* server process has observed; they are in-memory, per-process, and reset on
88
+ restart, which is a property of the existing verifier and not something this change alters. There
89
+ is nothing here to replicate: a peer's counters are a different fact about a different process,
90
+ not a stale copy of the same fact. `scope: 'local'` is stated explicitly in the response so a
91
+ reader never mistakes it for a pool figure, and the pool projection continues to live on the audit
92
+ route where the underlying records are durable.
93
+
94
+ ## 8. Rollback cost
95
+
96
+ Delete the route and revert one frontmatter line. Nothing persists, nothing migrates, no consumer
97
+ exists yet that could break. The feature returns to exactly its present state: running, observing,
98
+ and unable to graduate.
99
+
100
+ ## Evidence
101
+
102
+ Tests verified to fail against unmodified source before the fix was applied:
103
+
104
+ ```
105
+ # route reverted
106
+ Tests 5 failed (5)
107
+ ✗ 503s when the verifier is absent
108
+ ✗ returns the raw counters when present
109
+ ✗ surfaces the spec's graduation metric name
110
+ ✗ reports zero honestly rather than omitting the metric
111
+ ✗ is read-only
112
+
113
+ # route applied
114
+ Tests 5 passed (5)
115
+ tsc --noEmit — clean
116
+ ```
117
+
118
+ The "reports zero honestly" case is the one worth keeping: a dark feature that has classified
119
+ nothing must read as `0`, never as an absent field, because absent is indistinguishable from
120
+ "the endpoint does not work" — which is precisely the condition this route exists to end.