instar 1.3.224 → 1.3.225
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 +9 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/core/uncaughtExceptionPolicy.d.ts +3 -0
- package/dist/core/uncaughtExceptionPolicy.d.ts.map +1 -1
- package/dist/core/uncaughtExceptionPolicy.js +38 -0
- package/dist/core/uncaughtExceptionPolicy.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.225.md +46 -0
- package/upgrades/side-effects/uncaught-suppressed-stack-capture.md +56 -0
- package/upgrades/uncaught-suppressed-stack-capture.eli16.md +22 -0
|
@@ -5,4 +5,7 @@
|
|
|
5
5
|
* safe default).
|
|
6
6
|
*/
|
|
7
7
|
export declare function isNonFatalUncaught(err: unknown): boolean;
|
|
8
|
+
export declare function shouldLogStackForUncaught(err: unknown): boolean;
|
|
9
|
+
/** Test-only: reset the dedup memory so each test starts from a clean slate. */
|
|
10
|
+
export declare function __resetUncaughtStackDedupeForTests(): void;
|
|
8
11
|
//# sourceMappingURL=uncaughtExceptionPolicy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uncaughtExceptionPolicy.d.ts","sourceRoot":"","sources":["../../src/core/uncaughtExceptionPolicy.ts"],"names":[],"mappings":"AA2CA;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAIxD"}
|
|
1
|
+
{"version":3,"file":"uncaughtExceptionPolicy.d.ts","sourceRoot":"","sources":["../../src/core/uncaughtExceptionPolicy.ts"],"names":[],"mappings":"AA2CA;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAIxD;AAyBD,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAQ/D;AAED,gFAAgF;AAChF,wBAAgB,kCAAkC,IAAI,IAAI,CAEzD"}
|
|
@@ -52,4 +52,42 @@ export function isNonFatalUncaught(err) {
|
|
|
52
52
|
return false;
|
|
53
53
|
return NON_FATAL_UNCAUGHT_PATTERNS.some((p) => msg.includes(p));
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Decides whether to attach the FULL stack trace when logging a suppressed
|
|
57
|
+
* non-fatal uncaught exception. These isolated races recur (the HTTP
|
|
58
|
+
* double-response one — "Cannot set headers after they are sent" — fires
|
|
59
|
+
* ~10-20x/hour), so logging the stack on EVERY occurrence would flood the log.
|
|
60
|
+
* Yet without ANY stack the offending call site is undiagnosable: the throw
|
|
61
|
+
* originates in node's http internals, so the suppressed message alone carries
|
|
62
|
+
* no location — the route that double-responded is invisible.
|
|
63
|
+
*
|
|
64
|
+
* The fix: log the full stack the FIRST time a given stack is seen, then
|
|
65
|
+
* message-only for repeats. That surfaces each distinct originating call path
|
|
66
|
+
* exactly once (enough to find + fix the real double-send) without the flood.
|
|
67
|
+
*
|
|
68
|
+
* Dedup key is the full `err.stack` (not just the top frames): the distinguishing
|
|
69
|
+
* frame for an HTTP double-response is the application route DEEP in the stack —
|
|
70
|
+
* the top frames are always the same node internals — so only the whole stack
|
|
71
|
+
* separates one origin from another. Process-local + bounded (cleared past
|
|
72
|
+
* MAX_TRACKED_STACKS so a pathological variety of stacks can't grow it without
|
|
73
|
+
* limit). Returns false for non-Error / stackless input.
|
|
74
|
+
*/
|
|
75
|
+
const seenUncaughtStacks = new Set();
|
|
76
|
+
const MAX_TRACKED_STACKS = 200;
|
|
77
|
+
export function shouldLogStackForUncaught(err) {
|
|
78
|
+
if (!(err instanceof Error) || !err.stack)
|
|
79
|
+
return false;
|
|
80
|
+
if (seenUncaughtStacks.has(err.stack))
|
|
81
|
+
return false;
|
|
82
|
+
// Bound memory: if a pathological variety of distinct stacks accumulates,
|
|
83
|
+
// reset rather than grow without limit (re-surfacing after the reset is fine).
|
|
84
|
+
if (seenUncaughtStacks.size >= MAX_TRACKED_STACKS)
|
|
85
|
+
seenUncaughtStacks.clear();
|
|
86
|
+
seenUncaughtStacks.add(err.stack);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/** Test-only: reset the dedup memory so each test starts from a clean slate. */
|
|
90
|
+
export function __resetUncaughtStackDedupeForTests() {
|
|
91
|
+
seenUncaughtStacks.clear();
|
|
92
|
+
}
|
|
55
93
|
//# sourceMappingURL=uncaughtExceptionPolicy.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uncaughtExceptionPolicy.js","sourceRoot":"","sources":["../../src/core/uncaughtExceptionPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,2BAA2B,GAAG;IAClC,0EAA0E;IAC1E,8EAA8E;IAC9E,wCAAwC;IACxC,iBAAiB;IACjB,uBAAuB;IACvB,4BAA4B;IAC5B,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,4EAA4E;IAC5E,6EAA6E;IAC7E,4EAA4E;IAC5E,uBAAuB;IACvB,6EAA6E;IAC7E,sEAAsE;IACtE,8EAA8E;IAC9E,uEAAuE;IACvE,2EAA2E;IAC3E,4EAA4E;IAC5E,6EAA6E;IAC7E,gFAAgF;IAChF,6EAA6E;IAC7E,yDAAyD;IACzD,2BAA2B;CAC5B,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACpF,IAAI,CAAC,GAAG;QAAE,OAAO,KAAK,CAAC;IACvB,OAAO,2BAA2B,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC"}
|
|
1
|
+
{"version":3,"file":"uncaughtExceptionPolicy.js","sourceRoot":"","sources":["../../src/core/uncaughtExceptionPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,2BAA2B,GAAG;IAClC,0EAA0E;IAC1E,8EAA8E;IAC9E,wCAAwC;IACxC,iBAAiB;IACjB,uBAAuB;IACvB,4BAA4B;IAC5B,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,4EAA4E;IAC5E,6EAA6E;IAC7E,4EAA4E;IAC5E,uBAAuB;IACvB,6EAA6E;IAC7E,sEAAsE;IACtE,8EAA8E;IAC9E,uEAAuE;IACvE,2EAA2E;IAC3E,4EAA4E;IAC5E,6EAA6E;IAC7E,gFAAgF;IAChF,6EAA6E;IAC7E,yDAAyD;IACzD,2BAA2B;CAC5B,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACpF,IAAI,CAAC,GAAG;QAAE,OAAO,KAAK,CAAC;IACvB,OAAO,2BAA2B,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAAU,CAAC;AAC7C,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAE/B,MAAM,UAAU,yBAAyB,CAAC,GAAY;IACpD,IAAI,CAAC,CAAC,GAAG,YAAY,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACxD,IAAI,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACpD,0EAA0E;IAC1E,+EAA+E;IAC/E,IAAI,kBAAkB,CAAC,IAAI,IAAI,kBAAkB;QAAE,kBAAkB,CAAC,KAAK,EAAE,CAAC;IAC9E,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,kCAAkC;IAChD,kBAAkB,CAAC,KAAK,EAAE,CAAC;AAC7B,CAAC"}
|
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-03T14:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-03T14:47:08.272Z",
|
|
5
|
+
"instarVersion": "1.3.225",
|
|
6
6
|
"entryCount": 196,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Makes suppressed non-fatal uncaught exceptions **diagnosable** by logging their stack
|
|
9
|
+
trace once per origin.
|
|
10
|
+
|
|
11
|
+
**The problem.** The server's `uncaughtException` handler logs a small allowlist of
|
|
12
|
+
isolated/recoverable errors and continues (instead of crashing) — chiefly the HTTP
|
|
13
|
+
double-response race `Cannot set headers after they are sent`. But it logged only
|
|
14
|
+
`err.message`, no stack. Observed live: echo (~34×/2h) and codey (~26×/2h) emit
|
|
15
|
+
`[WARN] Non-fatal uncaught exception (suppressed): Cannot set headers after they are sent`
|
|
16
|
+
with **no location** — and since that error is thrown deep in node's http internals, the
|
|
17
|
+
message alone can't tell you which route double-responded. The real double-send bug is
|
|
18
|
+
therefore un-findable from the logs.
|
|
19
|
+
|
|
20
|
+
**The fix.** A new `shouldLogStackForUncaught(err)` in `uncaughtExceptionPolicy.ts`: the
|
|
21
|
+
handler now attaches the **full stack the first time a given stack is seen**, then logs
|
|
22
|
+
message-only for repeats. That surfaces each distinct offending call path exactly once —
|
|
23
|
+
enough to find and fix the real double-send — without flooding the log (these races recur
|
|
24
|
+
~10–20×/hour). The dedup key is the whole `err.stack` (the distinguishing route frame is
|
|
25
|
+
deep, not at the top); the tracking set is bounded (cleared past 200 distinct stacks).
|
|
26
|
+
|
|
27
|
+
This is a **diagnostic** change — it does not alter which exceptions are suppressed vs.
|
|
28
|
+
fatal (`isNonFatalUncaught` is unchanged), only how the suppressed ones are logged.
|
|
29
|
+
|
|
30
|
+
## What to Tell Your User
|
|
31
|
+
|
|
32
|
+
Nothing required — internal observability (agent-only). The next time the harmless-but-noisy
|
|
33
|
+
"Cannot set headers" race fires, the log will include the originating stack once, so the
|
|
34
|
+
underlying double-respond can be located and fixed.
|
|
35
|
+
|
|
36
|
+
## Summary of New Capabilities
|
|
37
|
+
|
|
38
|
+
None — observability/diagnostic only. No new routes, config, or surfaces.
|
|
39
|
+
|
|
40
|
+
## Evidence
|
|
41
|
+
|
|
42
|
+
- Live: echo/codey `logs/server.log` — `[WARN] Non-fatal uncaught exception (suppressed):
|
|
43
|
+
Cannot set headers after they are sent` recurring with no stack.
|
|
44
|
+
- `src/commands/server.ts` uncaughtException handler now calls `shouldLogStackForUncaught`.
|
|
45
|
+
- Tests: `tests/unit/uncaughtExceptionPolicy.test.ts` — first-logs-then-suppresses,
|
|
46
|
+
distinct-origin-surfaced-once, non-Error/stackless → false, reset re-surfaces, cap bound.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Side-Effects Review — Suppressed-uncaught stack capture
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `uncaught-suppressed-stack-capture`
|
|
4
|
+
**Date:** `2026-06-03`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required (Tier-1 — additive diagnostic helper + one log-line change; no control-flow/decision change)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
The server's `uncaughtException` handler suppresses an allowlist of isolated errors
|
|
11
|
+
(HTTP double-response, Slack reconnect, standby read-only write) and logs `err.message`.
|
|
12
|
+
For the recurring `Cannot set headers after they are sent` race the message has no location
|
|
13
|
+
(thrown in node internals), so the offending route is un-findable. Added
|
|
14
|
+
`shouldLogStackForUncaught(err)` to `uncaughtExceptionPolicy.ts` — logs the full stack the
|
|
15
|
+
first time a given stack is seen, message-only thereafter. The handler appends that stack
|
|
16
|
+
when the helper returns true. `isNonFatalUncaught` (the suppress-vs-crash decision) is
|
|
17
|
+
unchanged.
|
|
18
|
+
|
|
19
|
+
## Decision-point inventory
|
|
20
|
+
|
|
21
|
+
1. **Dedup key = full `err.stack` (not top-N frames).** For an HTTP double-response the
|
|
22
|
+
top frames are always node's http internals; only the deep application frame separates
|
|
23
|
+
one route from another, so the whole stack is the right key. Errs toward surfacing more
|
|
24
|
+
distinct origins (good for diagnosis), not fewer.
|
|
25
|
+
2. **First-occurrence-only logging (vs. always / rate-limited count).** These races fire
|
|
26
|
+
~10–20×/hour; logging the stack every time would flood. Once-per-origin gives the route
|
|
27
|
+
exactly once (sufficient to fix the real bug). Chosen over a time-window rate-limit
|
|
28
|
+
because the goal is "reveal each origin once," not "sample volume."
|
|
29
|
+
3. **Bounded tracking (clear past 200).** A pathological variety of distinct stacks can't
|
|
30
|
+
grow the Set without limit; clearing re-surfaces stacks after the cap, which is benign
|
|
31
|
+
(a little extra logging) and far better than unbounded memory.
|
|
32
|
+
|
|
33
|
+
## 1. Behavior / safety
|
|
34
|
+
|
|
35
|
+
No change to which exceptions crash vs. continue — `isNonFatalUncaught` and the FATAL
|
|
36
|
+
branch are untouched. The only effect is additional (bounded) log output on the
|
|
37
|
+
already-suppressed path. Cannot introduce a crash (the helper is pure + guarded for
|
|
38
|
+
non-Error/stackless input → returns false).
|
|
39
|
+
|
|
40
|
+
## 2. Log volume
|
|
41
|
+
|
|
42
|
+
Net REDUCES noise risk vs. a naive "always log stack": one stack per origin, then
|
|
43
|
+
message-only. The first-occurrence stacks are a small, finite set (the handful of
|
|
44
|
+
double-respond origins + the other allowlisted errors).
|
|
45
|
+
|
|
46
|
+
## 3. Blast radius / reversibility
|
|
47
|
+
|
|
48
|
+
One new pure function + one log-line edit in the handler. Fully reversible (revert both).
|
|
49
|
+
No schema/config/route/external surface. Memory: one process-local bounded `Set<string>`.
|
|
50
|
+
|
|
51
|
+
## 4. Tests
|
|
52
|
+
|
|
53
|
+
`tests/unit/uncaughtExceptionPolicy.test.ts` (existing isNonFatalUncaught suite unchanged) +
|
|
54
|
+
a new `shouldLogStackForUncaught` block covering: first-logs-then-suppresses, same-stack
|
|
55
|
+
fresh-Error still deduped, distinct-origin each surfaced once, non-Error/stackless → false,
|
|
56
|
+
reset re-surfaces, and the cap-bound clear path. 10 tests pass.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Suppressed-crash stack capture (#72) — ELI16
|
|
2
|
+
|
|
3
|
+
Viewable private artifact:
|
|
4
|
+
https://echo.dawn-tunnel.dev/view/311d1657-692e-48ca-a112-04c82fa74dec?sig=6e66a9515e16c2e28156a94169f7737cb86dd0c661440267eed93dc730ebd632
|
|
5
|
+
|
|
6
|
+
The agent server has a short list of harmless-but-annoying errors it deliberately swallows
|
|
7
|
+
instead of crashing — the main one is an HTTP race called "Cannot set headers after they are
|
|
8
|
+
sent" (two replies sent for one web request). Swallowing it is correct: the request is already
|
|
9
|
+
handled, and crashing the whole agent would be far worse.
|
|
10
|
+
|
|
11
|
+
The snag: it only logged the error MESSAGE, not WHERE it came from. And that error is thrown
|
|
12
|
+
deep inside Node's plumbing, so the message alone never says which piece of code double-replied.
|
|
13
|
+
It fires 10–20 times an hour on each agent, completely un-findable.
|
|
14
|
+
|
|
15
|
+
This change logs the full stack trace the FIRST time each distinct origin appears, then goes
|
|
16
|
+
quiet (message-only) for the repeats. So the offending code reveals itself once — enough to
|
|
17
|
+
actually fix the real double-reply — without flooding the log. Nothing about what gets swallowed
|
|
18
|
+
vs. crashed changes; only how the swallowed ones are logged.
|
|
19
|
+
|
|
20
|
+
_Patch · branch `echo/double-send-stack-capture` · found by scanning live fleet logs (the
|
|
21
|
+
see-what-breaks loop). Step 1 (make it findable); the real double-send fix follows once the next
|
|
22
|
+
occurrence reveals the route._
|