instar 1.3.387 → 1.3.388
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/feedback-factory/cutoverReadiness.d.ts +23 -0
- package/dist/feedback-factory/cutoverReadiness.d.ts.map +1 -1
- package/dist/feedback-factory/cutoverReadiness.js +32 -2
- package/dist/feedback-factory/cutoverReadiness.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.388.md +23 -0
- package/upgrades/side-effects/cutover-parity-lock-maxhold.md +95 -0
|
@@ -115,11 +115,25 @@ export interface CutoverReadinessDeps {
|
|
|
115
115
|
runImportDryRun?: (() => Promise<ImportRunResult>) | null;
|
|
116
116
|
/** Freshness bound for the parity window (default 6h). */
|
|
117
117
|
maxPassStalenessMs?: number;
|
|
118
|
+
/**
|
|
119
|
+
* Wall-clock max-hold for a single live fetch (parity pass OR import dry-run),
|
|
120
|
+
* enforced at the single-flight-lock boundary (default 12m). This is a
|
|
121
|
+
* defense-in-depth backstop ON TOP OF whatever internal timeout the injected
|
|
122
|
+
* fetch carries: if that fetch ever fails to settle (e.g. an AbortSignal that
|
|
123
|
+
* doesn't fire on a stalled socket, or a compare step with no timeout of its
|
|
124
|
+
* own), the lock would otherwise be held forever and every subsequent pass +
|
|
125
|
+
* dry-run is refused with "already in flight" — exactly the ~85-minute stuck
|
|
126
|
+
* lock observed in #948. The max-hold guarantees the lock releases within a
|
|
127
|
+
* bounded time so the next sweep can retry; it sits comfortably above
|
|
128
|
+
* HttpParitySource's 600s total budget so it only fires when that inner budget
|
|
129
|
+
* has genuinely failed, never on a legitimately-slow-but-working pass. */
|
|
130
|
+
maxLiveFetchMs?: number;
|
|
118
131
|
now?: () => number;
|
|
119
132
|
}
|
|
120
133
|
export declare class CutoverReadiness {
|
|
121
134
|
private readonly d;
|
|
122
135
|
private readonly maxStaleMs;
|
|
136
|
+
private readonly maxLiveFetchMs;
|
|
123
137
|
/**
|
|
124
138
|
* Single-flight guard over the LIVE SOURCE FETCH — shared by the parity pass
|
|
125
139
|
* and the import dry-run because both page the full live source. Without it,
|
|
@@ -133,6 +147,15 @@ export declare class CutoverReadiness {
|
|
|
133
147
|
private liveFetchInFlight;
|
|
134
148
|
constructor(deps: CutoverReadinessDeps);
|
|
135
149
|
private nowMs;
|
|
150
|
+
/**
|
|
151
|
+
* Race an injected live-fetch promise against a wall-clock max-hold so the
|
|
152
|
+
* single-flight lock can NEVER be held indefinitely by a fetch that fails to
|
|
153
|
+
* settle (#948). On timeout this rejects (the caller's `finally` then releases
|
|
154
|
+
* the lock and the next sweep retries); the orphaned work is abandoned — a
|
|
155
|
+
* re-attempt is cheap and idempotent, an unreleasable lock is not. Uses real
|
|
156
|
+
* wall-clock time (NOT `nowMs()`), since the bug is a promise that never
|
|
157
|
+
* settles in real time regardless of an injected clock. */
|
|
158
|
+
private withMaxHold;
|
|
136
159
|
/** Persist an import IntegrityReport (server-side import tooling only). */
|
|
137
160
|
recordIntegrityReport(report: IntegrityReport, generatedAt?: string): void;
|
|
138
161
|
integrityStatus(): IntegrityStatus;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cutoverReadiness.d.ts","sourceRoot":"","sources":["../../src/feedback-factory/cutoverReadiness.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"cutoverReadiness.d.ts","sourceRoot":"","sources":["../../src/feedback-factory/cutoverReadiness.ts"],"names":[],"mappings":"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"}
|
|
@@ -24,9 +24,13 @@
|
|
|
24
24
|
import fs from 'node:fs';
|
|
25
25
|
import path from 'node:path';
|
|
26
26
|
const DEFAULT_MAX_PASS_STALENESS_MS = 6 * 60 * 60 * 1000;
|
|
27
|
+
/** 12m — above HttpParitySource's 600s total budget + a working pass's compare,
|
|
28
|
+
* so the lock-boundary max-hold is a true backstop, not a false-abort risk. */
|
|
29
|
+
const DEFAULT_MAX_LIVE_FETCH_MS = 12 * 60 * 1000;
|
|
27
30
|
export class CutoverReadiness {
|
|
28
31
|
d;
|
|
29
32
|
maxStaleMs;
|
|
33
|
+
maxLiveFetchMs;
|
|
30
34
|
/**
|
|
31
35
|
* Single-flight guard over the LIVE SOURCE FETCH — shared by the parity pass
|
|
32
36
|
* and the import dry-run because both page the full live source. Without it,
|
|
@@ -46,10 +50,36 @@ export class CutoverReadiness {
|
|
|
46
50
|
}
|
|
47
51
|
this.d = deps;
|
|
48
52
|
this.maxStaleMs = deps.maxPassStalenessMs ?? DEFAULT_MAX_PASS_STALENESS_MS;
|
|
53
|
+
this.maxLiveFetchMs = deps.maxLiveFetchMs ?? DEFAULT_MAX_LIVE_FETCH_MS;
|
|
49
54
|
}
|
|
50
55
|
nowMs() {
|
|
51
56
|
return this.d.now ? this.d.now() : Date.now();
|
|
52
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Race an injected live-fetch promise against a wall-clock max-hold so the
|
|
60
|
+
* single-flight lock can NEVER be held indefinitely by a fetch that fails to
|
|
61
|
+
* settle (#948). On timeout this rejects (the caller's `finally` then releases
|
|
62
|
+
* the lock and the next sweep retries); the orphaned work is abandoned — a
|
|
63
|
+
* re-attempt is cheap and idempotent, an unreleasable lock is not. Uses real
|
|
64
|
+
* wall-clock time (NOT `nowMs()`), since the bug is a promise that never
|
|
65
|
+
* settles in real time regardless of an injected clock. */
|
|
66
|
+
async withMaxHold(work, op) {
|
|
67
|
+
let timer;
|
|
68
|
+
const guard = new Promise((_resolve, reject) => {
|
|
69
|
+
timer = setTimeout(() => {
|
|
70
|
+
reject(new Error(`live ${op} fetch exceeded the max-hold budget (${this.maxLiveFetchMs}ms) — releasing the single-flight lock so a later sweep can retry; the underlying fetch did not settle within budget (see #948)`));
|
|
71
|
+
}, this.maxLiveFetchMs);
|
|
72
|
+
if (typeof timer.unref === 'function')
|
|
73
|
+
timer.unref();
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
return await Promise.race([work, guard]);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
if (timer)
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
53
83
|
/** Persist an import IntegrityReport (server-side import tooling only). */
|
|
54
84
|
recordIntegrityReport(report, generatedAt) {
|
|
55
85
|
const envelope = {
|
|
@@ -109,7 +139,7 @@ export class CutoverReadiness {
|
|
|
109
139
|
this.liveFetchInFlight = { op: 'parity-pass', startedAt: new Date(this.nowMs()).toISOString() };
|
|
110
140
|
let result;
|
|
111
141
|
try {
|
|
112
|
-
result = await this.d.runParityCheck();
|
|
142
|
+
result = await this.withMaxHold(this.d.runParityCheck(), 'parity-pass');
|
|
113
143
|
}
|
|
114
144
|
catch (err) {
|
|
115
145
|
return { ok: false, reason: `parity check failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
@@ -142,7 +172,7 @@ export class CutoverReadiness {
|
|
|
142
172
|
this.liveFetchInFlight = { op: 'import-dry-run', startedAt: new Date(this.nowMs()).toISOString() };
|
|
143
173
|
let result;
|
|
144
174
|
try {
|
|
145
|
-
result = await this.d.runImportDryRun();
|
|
175
|
+
result = await this.withMaxHold(this.d.runImportDryRun(), 'import-dry-run');
|
|
146
176
|
}
|
|
147
177
|
catch (err) {
|
|
148
178
|
return { ok: false, reason: `import dry-run failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cutoverReadiness.js","sourceRoot":"","sources":["../../src/feedback-factory/cutoverReadiness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"cutoverReadiness.js","sourceRoot":"","sources":["../../src/feedback-factory/cutoverReadiness.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-07T00:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-07T00:43:22.312Z",
|
|
5
|
+
"instarVersion": "1.3.388",
|
|
6
6
|
"entryCount": 199,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Fixed a liveness bug in the cutover-readiness checker (#948): `CutoverReadiness`'s single-flight lock over the live source fetch (used by both the parity pass and the import dry-run) could be held **indefinitely** if the injected fetch never settled. In production the lock stuck for ~85 minutes (`logs/server.log`), so every subsequent parity pass and dry-run was refused with "live source fetch already in flight" — the parity-pass feeder kept no-opping and the cutover parity window went stale and never refreshed.
|
|
9
|
+
|
|
10
|
+
`src/feedback-factory/cutoverReadiness.ts` now wraps the awaited live fetch in a wall-clock **max-hold guard** (`withMaxHold`, new optional `maxLiveFetchMs` dep, default 12 minutes) at the lock boundary. The lock is released in a `finally`, but a `finally` can't run while the awaited promise never settles — so the guard races the fetch against a timer and guarantees the lock releases within a bounded time regardless of *why* the fetch stalled (an `AbortSignal.timeout` that didn't fire on a stalled socket, or the un-timed compare step). The default sits comfortably above `HttpParitySource`'s 600s total budget, so it only fires when that inner budget has genuinely failed — never on a slow-but-working pass.
|
|
11
|
+
|
|
12
|
+
## What to Tell Your User
|
|
13
|
+
|
|
14
|
+
If you were ever told the feedback-migration "cutover readiness" check was stuck or its parity window had gone stale, this fixes the underlying cause: a single slow/hung check could jam the checker until the next restart. Now a check that takes too long is abandoned cleanly and the next one runs on schedule. It ships on by default with a safe 12-minute backstop and needs no configuration.
|
|
15
|
+
|
|
16
|
+
## Summary of New Capabilities
|
|
17
|
+
|
|
18
|
+
- A wall-clock max-hold backstop on the cutover-readiness live-fetch single-flight lock — a stalled parity pass or import dry-run can no longer hang the checker indefinitely.
|
|
19
|
+
- New optional `maxLiveFetchMs` knob (default 12m); the default IS the fix, so existing agents get it automatically on update with no config change.
|
|
20
|
+
|
|
21
|
+
## Evidence
|
|
22
|
+
|
|
23
|
+
Before: a single stalled live fetch held the single-flight lock until the next server restart (~85 min observed), starving the parity feeder. After: the lock releases within `maxLiveFetchMs` and the next sweep retries. Pinned by a Tier-1 regression (`tests/unit/cutover-readiness.test.ts`): a `runParityCheck` (and `runImportDryRun`) that never settles now returns `ok:false` AND a subsequent pass on the same instance is NOT refused as in-flight — it runs and records normally. 22 cutover-readiness unit tests green (2 new + 20 existing through the new path); `tsc --noEmit` clean.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Side-Effects Review — Cutover-readiness live-fetch max-hold (lock-boundary backstop, #948)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `cutover-parity-lock-maxhold`
|
|
4
|
+
**Date:** `2026-06-07`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `self-review (single-file behavioral hardening; both branches — stalled and healthy — covered by Tier-1 tests over the real CutoverReadiness)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Fixes #948: `CutoverReadiness`'s single-flight lock (`liveFetchInFlight`) could be held
|
|
11
|
+
indefinitely when an injected live fetch (parity pass or import dry-run) never settled —
|
|
12
|
+
observed ~85 minutes in production (`logs/server.log`), permanently refusing every
|
|
13
|
+
subsequent parity pass + dry-run with "live source fetch already in flight" and starving
|
|
14
|
+
the parity feeder (so the cutover window went stale and never refreshed).
|
|
15
|
+
|
|
16
|
+
Root: the lock is released in a `finally`, which is correct — but a `finally` cannot run
|
|
17
|
+
while the awaited promise never settles. `HttpParitySource.prepare()` has per-page +
|
|
18
|
+
between-page bounds, so the stall is either an `AbortSignal.timeout` that doesn't fire on a
|
|
19
|
+
particular socket stall, or the un-timed compare step. Rather than chase the sub-cause, the
|
|
20
|
+
fix adds a wall-clock **max-hold guard at the lock boundary** (`withMaxHold`, default 12m):
|
|
21
|
+
the awaited fetch is raced against a timer, so the lock ALWAYS releases within a bounded
|
|
22
|
+
time regardless of why the fetch stalled. Structure > willpower — the lock-holder enforces
|
|
23
|
+
its own max-hold, independent of the injected fetch's internal timeouts.
|
|
24
|
+
|
|
25
|
+
Files: `src/feedback-factory/cutoverReadiness.ts` (+ `tests/unit/cutover-readiness.test.ts`).
|
|
26
|
+
|
|
27
|
+
## Decision-point inventory
|
|
28
|
+
|
|
29
|
+
- `withMaxHold` — add — a private timeout-race helper. NO decision authority: it only
|
|
30
|
+
bounds how long the single-flight lock can be held; it never decides parity/integrity.
|
|
31
|
+
- `maxLiveFetchMs` config (default 12m) — add — a backstop dial, set comfortably above
|
|
32
|
+
`HttpParitySource`'s 600s total budget so it only fires when that inner budget genuinely
|
|
33
|
+
failed (never a false-abort of a slow-but-working pass).
|
|
34
|
+
|
|
35
|
+
## 1. Over-block
|
|
36
|
+
|
|
37
|
+
The only thing the max-hold can "block" is a live fetch that exceeds 12 minutes. A healthy
|
|
38
|
+
parity pass (~15s/page, 600s inner budget) is far under that, so a working pass is never
|
|
39
|
+
aborted. Default chosen above the inner budget specifically to avoid over-block.
|
|
40
|
+
|
|
41
|
+
## 2. Under-block
|
|
42
|
+
|
|
43
|
+
If a stall somehow resolved at exactly the budget edge, the worst case is one wasted sweep
|
|
44
|
+
(recorded nothing) and a retry next tick — strictly better than the pre-fix infinite hold.
|
|
45
|
+
|
|
46
|
+
## 3. Level-of-abstraction fit
|
|
47
|
+
|
|
48
|
+
Correct. The guard sits at the single-flight-lock boundary it protects, not inside the
|
|
49
|
+
fetch (which already has its own, evidently-insufficient, internal timeouts). It is a
|
|
50
|
+
backstop, not a replacement — the inner budgets still do the primary bounding.
|
|
51
|
+
|
|
52
|
+
## 4. Signal vs authority compliance
|
|
53
|
+
|
|
54
|
+
- [x] No block/allow authority. The change only ensures a lock releases; it never gates a
|
|
55
|
+
parity/integrity decision. `runParityPass` still records nothing on failure (a max-hold
|
|
56
|
+
timeout is a failed check = absence of evidence, exactly as a fetch error already was).
|
|
57
|
+
|
|
58
|
+
## 5. Interactions
|
|
59
|
+
|
|
60
|
+
- **Orphaned work:** when the max-hold fires, the abandoned fetch promise keeps running but
|
|
61
|
+
is harmless — its later settlement is ignored (control already left the `await`; the
|
|
62
|
+
`recordResult` line after it is never reached for the timed-out call), so it cannot
|
|
63
|
+
double-record or re-acquire the lock. It holds at most one connection until it aborts or
|
|
64
|
+
the process restarts — a bounded, acceptable cost versus an unreleasable lock.
|
|
65
|
+
- **Double-fire:** none — `Promise.race` settles once; `clearTimeout` in `finally` disarms
|
|
66
|
+
the timer on the happy path.
|
|
67
|
+
- **Clock:** the guard uses real `setTimeout` (NOT the injected `now()`), because the bug is
|
|
68
|
+
a promise that never settles in REAL time; an injected test clock must not disarm it.
|
|
69
|
+
|
|
70
|
+
## 6. External surfaces
|
|
71
|
+
|
|
72
|
+
- **Install base:** `maxLiveFetchMs` is a new OPTIONAL dep with a safe default → existing
|
|
73
|
+
agents get the backstop automatically on update, no config change required (Migration
|
|
74
|
+
Parity: the default IS the fix; nothing to migrate).
|
|
75
|
+
- **Persistent state:** none new.
|
|
76
|
+
- **Other agents / attention queue:** unchanged.
|
|
77
|
+
|
|
78
|
+
## 7. Rollback cost
|
|
79
|
+
|
|
80
|
+
Pure additive hardening. Back-out = revert the commit. No persistent-state cleanup; healthy
|
|
81
|
+
passes behave identically with or without the guard.
|
|
82
|
+
|
|
83
|
+
## Conclusion
|
|
84
|
+
|
|
85
|
+
A wall-clock backstop on a single-flight lock, proven by a Tier-1 regression that a
|
|
86
|
+
never-settling fetch now releases the lock (a later pass proceeds) instead of hanging it
|
|
87
|
+
for ~85 minutes. No new authority; safe default; auto-applies to existing agents.
|
|
88
|
+
|
|
89
|
+
## Evidence pointers
|
|
90
|
+
|
|
91
|
+
- Tier 1: `tests/unit/cutover-readiness.test.ts` — 2 new (#948 regression for both
|
|
92
|
+
`runParityPass` and `runImportDryRunPass`: a never-settling fetch hits the max-hold,
|
|
93
|
+
returns `ok:false`, and the lock releases so the next pass records normally) + the 20
|
|
94
|
+
existing cutover tests still green through the new code path (22 total).
|
|
95
|
+
- tsc `--noEmit` clean.
|