instar 1.3.1313 → 1.3.1315

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,207 @@
1
+ # Side-Effects Review — out of tick time is a clean stop; the drain job reads slow runs
2
+
3
+ **Version / slug:** `feedback-drain-degraded-label`
4
+ **Date:** `2026-10-01`
5
+ **Author:** `Echo`
6
+ **Second-pass reviewer:** `independent reviewer subagent (see below)`
7
+
8
+ ## Summary of the change
9
+
10
+ Live on 2026-10-01 (Mac Studio, v1.3.1314, authority gen 4 `maxBatch` 50, 90 s tick), drain
11
+ runs ef0f6cbf and 10674e04 each ran 70 s. Each made two good readiness calls: ~32 s and ~62 s
12
+ in, 10 + 9 and 10 + 6 candidates. Then each started a ONE-candidate call with 7.6 s / 7.8 s
13
+ left. The pace-based sizing (`remaining / (1.25 × ms per candidate)`) assumes a call costs
14
+ only its per-candidate time. Live calls also pay about 9 s of start-up (6 candidates 23.2 s,
15
+ 9 candidates 30.2 s, 10 candidates 32.2 s). So the service's own `withStageBudget` timer cut
16
+ the call at exactly 70.0 s, the readiness deadline. `failed()` then marked the row
17
+ `readiness-authority-failed` and the run `degraded`, with zero provider errors in
18
+ `/metrics/features`.
19
+
20
+ `FeedbackDrainService.reviewReadinessInChunks`:
21
+ - A call after the first needs at least `MIN_LATER_READINESS_CALL_MS` (25 s) left. Below
22
+ that, the loop stops.
23
+ - If the stage timer (`StageBudgetExceeded`, a new private Error subclass with the same
24
+ message) cuts a LATER call whose budget was shortened by the tick clock
25
+ (`callBudgetMs < readinessStageBudgetMs`), that is out of time, not a provider failure.
26
+ The loop stops, and the call's rows stay due, untouched.
27
+ - In both cases, after at least one successful call, the tick records `out.note =
28
+ 'readiness-time-exhausted-rest-due'`. `FeedbackDrainTickResult.note` is a new optional,
29
+ informational field: it never degrades the run. `transitionRun` stores `reason ?? note`,
30
+ so a `succeeded` row can carry the note in its reason column.
31
+ - Unchanged: no call made at all → degraded `readiness-wall-clock-exhausted`. A first call
32
+ that times out, a provider timeout or error (`CodexExecJsonTimeoutError` etc.), a contract
33
+ violation and the spend brake are all still degraded.
34
+
35
+ The `feedback-factory-process` job body (`src/scaffold/templates/jobs/instar/feedback-factory-process.md`):
36
+ - The installed body WAS the new script (PR #2111). Its 60 s poll ended before the 70 s runs
37
+ finished, and it printed `still in flight; the next cadence observes it` (both job
38
+ transcripts show this) and exited as a success.
39
+ - The scheduler's declared-failure file (`INSTAR_JOB_FAILURE_FILE` → `readJobDeclaredFailure`
40
+ in `JobScheduler.notifyJobComplete`) is wired correctly. It was never written.
41
+ - Now: the default poll is 180 s (above the service's 115 s wall-clock ceiling). A run still
42
+ in flight after the poll FAILS the job. `expectedDurationMinutes` goes from 2 to 5, so the
43
+ session budget covers a 180 s poll plus model start-up.
44
+
45
+ ## Decision-point inventory
46
+
47
+ - Drain readiness loop stop/continue (`FeedbackDrainService`): modify. One extra stop
48
+ condition (a time floor for later calls), and a stage-timer cut of a time-shortened later
49
+ call is reclassified as a clean stop.
50
+ - Run outcome labelling: modify. A tick that stops for time after a successful call is
51
+ `succeeded` with a note, not `degraded`.
52
+ - Job tier-1 supervision (`feedback-factory-process` script): modify. "Still in flight at
53
+ the poll limit" goes from success to a declared failure, and the poll is 3× longer.
54
+
55
+ ---
56
+
57
+ ## 1. Over-block
58
+
59
+ - The job now fails on a run still in flight after 180 s. The service throws
60
+ `feedback drain wall-clock budget exceeded` past 115 s, and the run lease is 120 s, so a
61
+ run alive at 180 s is stuck. Failing the job surfaces it. One false failure is possible if
62
+ the status route itself is unreachable for the whole poll. That is a real fault too.
63
+ - The 25 s floor can leave a few candidates unreviewed in a tick where a short call might
64
+ just have fitted. They stay due and go first next tick (due order `next_review_at,
65
+ cluster_id`). It costs about 1-2 candidates per tick and never rejects anything.
66
+
67
+ ---
68
+
69
+ ## 2. Under-block
70
+
71
+ - A later call cut by the tick clock after an earlier success no longer counts toward
72
+ degradation. If the provider is really slow on that one call, it is hidden in that tick.
73
+ The next tick's first call has the full budget, and if it fails it is degraded as before.
74
+ A provider that keeps getting slow therefore still surfaces.
75
+ - The reclassification requires `StageBudgetExceeded` AND a shortened budget AND an earlier
76
+ success. A provider timeout raises its own error class, so it is never reclassified.
77
+
78
+ ---
79
+
80
+ ## 3. Level-of-abstraction fit
81
+
82
+ This is the right layer. The tick budget and its run labelling belong to the drain service.
83
+ The job script is the tier-1 supervisor of exactly this run outcome. No new gate or table is
84
+ added.
85
+
86
+ ---
87
+
88
+ ## 4. Signal vs authority compliance
89
+
90
+ - [x] No — this change has no block/allow surface.
91
+
92
+ These are resource and timekeeping bounds plus honest outcome labelling. The readiness
93
+ judgment stays with the registered LLM authority.
94
+
95
+ ---
96
+
97
+ ## 4b. Judgment-point check (Judgment Within Floors standard)
98
+
99
+ No new heuristic at a competing-signals decision point. The 25 s floor is a measured resource
100
+ bound. Fitted to the live calls, a call costs ~9 s plus ~2.4 s per candidate. The pace estimate
101
+ (with its 25% margin) only stays safe with about 23 s or more left; the reviewer derived this,
102
+ and the floor was raised from 15 s to 25 s to match.
103
+
104
+ ---
105
+
106
+ ## 5. Interactions
107
+
108
+ - `onRecoverableStall` fired on every degraded run. These two live runs called it. A
109
+ time-stopped run no longer does.
110
+ - `pruneOperationalHistory` runs only for non-degraded runs, so it now also runs after a
111
+ time-stopped tick. That is the same as any succeeded tick.
112
+ - Transient-failure counter: unchanged. Any successful call clears it, and a clean stop is
113
+ not a failure.
114
+ - A cut later call's codex child keeps running until its own 60 s timeout (named in PR #2112
115
+ as well). The 25 s floor makes this rarer. 30-minute ticks never overlap.
116
+ - `FeedbackDrainTickProxy` and the status route pass the run's `reason` through. A succeeded
117
+ run can now show the note there. The job script reads `reason` only for non-success
118
+ states.
119
+
120
+ ---
121
+
122
+ ## 6. External surfaces
123
+
124
+ - The job body and its manifest are replaced on update by `installBuiltinJobs`. That covers
125
+ the body, `expectedDurationMinutes` and the operator's `enabled`, which is preserved
126
+ (Migration Parity is covered by the existing body-refresh test). No config, route, schema
127
+ or CLAUDE.md template change.
128
+ - Model spend: slightly lower, because the doomed last call is no longer made.
129
+
130
+ ---
131
+
132
+ ## 6b. Operator-surface quality
133
+
134
+ Not applicable. No operator surface is touched (the run reason is already shown as-is).
135
+
136
+ ---
137
+
138
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
139
+
140
+ Unchanged. Readiness runs only on the canonical owner. A proxied tick still finishes the job
141
+ at once ("the owner records its outcome").
142
+
143
+ ---
144
+
145
+ ## 8. Rollback cost
146
+
147
+ Low. Revert the commit. There are no data-shape changes. A `succeeded` drain_runs row may
148
+ hold the note in `reason`, which older code reads as an ordinary bounded string.
149
+
150
+ ---
151
+
152
+ ## Conclusion
153
+
154
+ The run label now tells the truth in both directions. A tick whose calls all succeeded but
155
+ ran out of time is `succeeded` (with a note), and the rest stays due. A degraded run reaches
156
+ the operator, because the job now waits past the drain's wall clock instead of reporting
157
+ success at 60 s.
158
+
159
+ ## Second-pass review (if required)
160
+
161
+ **Reviewer:** independent reviewer subagent
162
+ **Independent read of the artifact: concur**
163
+
164
+ The reviewer ran the targeted tests: unit 15/15, integration 17/17, e2e 1/1. Its notes, and
165
+ what was done with each:
166
+ - The 15 s floor was below the ~23 s at which the start-up-blind pace estimate is safe.
167
+ Applied: the floor is now 25 s.
168
+ - Nothing tested that our timer cutting a later call that had its FULL budget stays degraded.
169
+ Applied: a new unit case.
170
+ - One test name said "full budget" when the budget was shortened. Applied: renamed. That case
171
+ proves a provider error is never reclassified.
172
+ - The job's in-flight failure did not say what it last saw. Applied: it now includes the last
173
+ `lastRun` id and state, or the HTTP code.
174
+ - Left as is (named): the real-timer `setTimeout` race path is not separately unit-tested.
175
+ Both paths throw the same class, and the post-check path is covered.
176
+
177
+ ## Evidence pointers
178
+
179
+ - Live store (read-only `.backup` at /tmp/fac-degraded/drain.db): drain_runs ef0f6cbf
180
+ (22:49:40) and 10674e04 (23:00:24). Readiness rows were stamped at +32.2 s / +62.4 s /
181
+ +70.0 s and +39.0 s / +62.2 s / +70.0 s. Exactly one `readiness-authority-failed` row per
182
+ run, at the 70 s deadline.
183
+ - Job transcripts: `FEEDBACK_DRAIN_RESULT run run:ef0f6cbf-… still in flight; the next
184
+ cadence observes it` (and the same for 10674e04).
185
+ - `tests/unit/feedback-factory/drain-readiness-chunking.test.ts`: five new cases cover the
186
+ live shape (9 s + 2.33 s per candidate → calls [10, 9], succeeded with the note, 31 rows
187
+ untouched), a stage-timer cut of a later call (clean), the same cut on the first call
188
+ (degraded), a provider timeout with a shortened budget (degraded), and our timer cutting a
189
+ later call that had its full budget (degraded). Three existing
190
+ expectations move from [10, 10, 2] to [10, 10] because of the floor.
191
+ - `tests/integration/feedback-drain-time-exhausted.test.ts`: production wiring + HTTP status
192
+ report `succeeded` with the note. It fails on the pre-fix source.
193
+ - `tests/integration/feedback-factory-process-job-body.test.ts`: four new cases cover the
194
+ default poll above 115 s, a slow degraded run that still fails the job, a slow succeeded
195
+ run with the note counted as success, and in flight after the poll → failure. Two fail on
196
+ the pre-fix body.
197
+ - `tests/e2e/feedback-drain-time-exhausted.test.ts`: the shipped job script against the
198
+ production AgentServer with the live latency shape. Pre-fix calls are [5, 4, 1] (the live
199
+ shape) and the run is degraded. Post-fix calls are [5, 4], the run succeeded with the note,
200
+ and the job records success.
201
+
202
+ ## Class-Closure Declaration (display-only mirror)
203
+
204
+ No agent-authored-artifact defect class: the defects are a missing fixed-cost term in a
205
+ TypeScript time estimate, and a poll shorter than the thing it polls. Convergence of the
206
+ modified loop: at most `ceil(maxBatch / readinessChunkSize)` calls per tick. It stops at the
207
+ floor or the deadline, and any non-time failure still ends the readiness stage.