@aexol/spectral 0.9.210 → 0.9.212
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/agent/agents.d.ts.map +1 -1
- package/dist/agent/agents.js +40 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +24 -4
- package/dist/extensions/browser/tools/console.d.ts.map +1 -1
- package/dist/extensions/browser/tools/console.js +22 -2
- package/dist/extensions/browser/tools/evaluate.d.ts.map +1 -1
- package/dist/extensions/browser/tools/evaluate.js +28 -4
- package/dist/extensions/browser/tools/network.d.ts.map +1 -1
- package/dist/extensions/browser/tools/network.js +22 -2
- package/dist/extensions/seo/tools/fetch.d.ts.map +1 -1
- package/dist/extensions/seo/tools/fetch.js +59 -8
- package/dist/extensions/web/tools/fetch.d.ts.map +1 -1
- package/dist/extensions/web/tools/fetch.js +28 -15
- package/dist/generated/zeus/const.d.ts.map +1 -1
- package/dist/generated/zeus/const.js +27 -0
- package/dist/generated/zeus/index.d.ts +122 -0
- package/dist/generated/zeus/index.d.ts.map +1 -1
- package/dist/memory/tool-output-compressor.d.ts.map +1 -1
- package/dist/memory/tool-output-compressor.js +53 -2
- package/dist/relay/client.d.ts +11 -0
- package/dist/relay/client.d.ts.map +1 -1
- package/dist/relay/client.js +24 -6
- package/dist/relay/history-projection.d.ts.map +1 -1
- package/dist/relay/history-projection.js +7 -0
- package/dist/sdk/coding-agent/core/tools/offload.d.ts +102 -0
- package/dist/sdk/coding-agent/core/tools/offload.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/tools/offload.js +207 -0
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +21 -0
- package/dist/server/offload-gc.d.ts +21 -0
- package/dist/server/offload-gc.d.ts.map +1 -0
- package/dist/server/offload-gc.js +111 -0
- package/dist/server/session-stream.d.ts +2 -0
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +166 -9
- package/package.json +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Garbage collection for large-result offload files.
|
|
3
|
+
*
|
|
4
|
+
* Offloads live under `<configDir>/offloads/<sessionId>/*.log`. This sweep
|
|
5
|
+
* removes per-session directories older than `SPECTRAL_OFFLOAD_TTL_DAYS`
|
|
6
|
+
* (default 7) and, when the total still exceeds `SPECTRAL_OFFLOAD_MAX_MB`
|
|
7
|
+
* (default 500), evicts the oldest sessions first.
|
|
8
|
+
*
|
|
9
|
+
* Best-effort and synchronous: callers fire-and-forget it during preflight.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { getConfigDir } from "../config.js";
|
|
14
|
+
import { envPositiveInt } from "../sdk/coding-agent/core/tools/offload.js";
|
|
15
|
+
export const DEFAULT_OFFLOAD_TTL_DAYS = 7;
|
|
16
|
+
export const DEFAULT_OFFLOAD_MAX_MB = 500;
|
|
17
|
+
export function getOffloadTtlMs() {
|
|
18
|
+
const days = envPositiveInt("SPECTRAL_OFFLOAD_TTL_DAYS", DEFAULT_OFFLOAD_TTL_DAYS);
|
|
19
|
+
return days * 24 * 60 * 60 * 1000;
|
|
20
|
+
}
|
|
21
|
+
export function getOffloadMaxBytes() {
|
|
22
|
+
const mb = envPositiveInt("SPECTRAL_OFFLOAD_MAX_MB", DEFAULT_OFFLOAD_MAX_MB);
|
|
23
|
+
return mb * 1024 * 1024;
|
|
24
|
+
}
|
|
25
|
+
function dirSizeBytes(dir) {
|
|
26
|
+
let total = 0;
|
|
27
|
+
let entries;
|
|
28
|
+
try {
|
|
29
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const full = join(dir, entry.name);
|
|
36
|
+
try {
|
|
37
|
+
if (entry.isDirectory())
|
|
38
|
+
total += dirSizeBytes(full);
|
|
39
|
+
else if (entry.isFile())
|
|
40
|
+
total += statSync(full).size;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Skip unreadable entries — GC is best-effort.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return total;
|
|
47
|
+
}
|
|
48
|
+
/** Remove stale/over-budget offload directories. Never throws. */
|
|
49
|
+
export function sweepOffloads(root) {
|
|
50
|
+
const result = { removedSessions: 0, removedBytes: 0 };
|
|
51
|
+
let base;
|
|
52
|
+
try {
|
|
53
|
+
base = root ?? join(getConfigDir(), "offloads");
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
if (!existsSync(base))
|
|
59
|
+
return result;
|
|
60
|
+
let sessionDirs;
|
|
61
|
+
try {
|
|
62
|
+
sessionDirs = readdirSync(base);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
const ttlMs = getOffloadTtlMs();
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const removeDir = (dir) => {
|
|
70
|
+
try {
|
|
71
|
+
result.removedBytes += dirSizeBytes(dir);
|
|
72
|
+
rmSync(dir, { recursive: true, force: true });
|
|
73
|
+
result.removedSessions++;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Best-effort.
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
// Pass 1: TTL expiry by directory mtime.
|
|
80
|
+
const survivors = [];
|
|
81
|
+
for (const name of sessionDirs) {
|
|
82
|
+
const dir = join(base, name);
|
|
83
|
+
let stat;
|
|
84
|
+
try {
|
|
85
|
+
stat = statSync(dir);
|
|
86
|
+
if (!stat.isDirectory())
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (now - stat.mtimeMs > ttlMs)
|
|
93
|
+
removeDir(dir);
|
|
94
|
+
else
|
|
95
|
+
survivors.push({ dir, mtime: stat.mtimeMs, size: dirSizeBytes(dir) });
|
|
96
|
+
}
|
|
97
|
+
// Pass 2: total-size budget, oldest first.
|
|
98
|
+
let total = survivors.reduce((sum, s) => sum + s.size, 0);
|
|
99
|
+
const maxBytes = getOffloadMaxBytes();
|
|
100
|
+
if (total <= maxBytes)
|
|
101
|
+
return result;
|
|
102
|
+
survivors.sort((a, b) => a.mtime - b.mtime);
|
|
103
|
+
for (const s of survivors) {
|
|
104
|
+
if (total <= maxBytes)
|
|
105
|
+
break;
|
|
106
|
+
const before = s.size;
|
|
107
|
+
removeDir(s.dir);
|
|
108
|
+
total -= before;
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
@@ -471,6 +471,8 @@ export declare class SessionStreamManager {
|
|
|
471
471
|
private scheduleIntervalTick;
|
|
472
472
|
private clearIntervalTimer;
|
|
473
473
|
private clearCurrentTurnSilenceTimer;
|
|
474
|
+
/** Clear subagent-in-flight bookkeeping (used by the silence safety net). */
|
|
475
|
+
private resetSubagentTracking;
|
|
474
476
|
private armCurrentTurnSilenceTimer;
|
|
475
477
|
private handleCurrentTurnSilenceTimeout;
|
|
476
478
|
private intervalTick;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-stream.d.ts","sourceRoot":"","sources":["../../src/server/session-stream.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"session-stream.d.ts","sourceRoot":"","sources":["../../src/server/session-stream.ts"],"names":[],"mappings":"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"}
|
|
@@ -84,8 +84,68 @@ const MAX_LOOP_ITERATIONS = 100;
|
|
|
84
84
|
// `stream.currentTurn` stays non-null forever. On reconnect, `session_ready`
|
|
85
85
|
// then reports `currentTurn != null`, so landing shows a phantom "streaming"
|
|
86
86
|
// status. This timer clears the stale turn and emits a synthetic `agent_end`
|
|
87
|
-
// after
|
|
88
|
-
|
|
87
|
+
// after 30 minutes of silence on streaming-meaningful events.
|
|
88
|
+
//
|
|
89
|
+
// Configurable via `SPECTRAL_TURN_SILENCE_TIMEOUT_MS`:
|
|
90
|
+
// * unset / empty / not-a-number / negative -> 30 minutes (default)
|
|
91
|
+
// * `0` -> DISABLED, the timer is never armed (official opt-out)
|
|
92
|
+
// * a positive fraction (e.g. `0.5`) is rounded UP to 1ms so it can never
|
|
93
|
+
// accidentally disable the safety net
|
|
94
|
+
// * anything larger than Node's max delay is clamped (see below)
|
|
95
|
+
const DEFAULT_TURN_SILENCE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
96
|
+
/**
|
|
97
|
+
* Node emits `TimeoutOverflowWarning` and fires immediately for delays above
|
|
98
|
+
* this value, so anything larger is clamped to it.
|
|
99
|
+
*/
|
|
100
|
+
const MAX_TIMEOUT_DELAY_MS = 2_147_483_647;
|
|
101
|
+
/**
|
|
102
|
+
* Upper bound on how many times the silence timer may be re-armed instead of
|
|
103
|
+
* closing the turn while a subagent is still running. Guards against a
|
|
104
|
+
* counter stuck at >0 forever (e.g. a `subagent_end` that never arrives after
|
|
105
|
+
* a crash) turning the safety net off permanently.
|
|
106
|
+
* `SPECTRAL_TURN_SILENCE_MAX_REARM=0` means unlimited re-arms.
|
|
107
|
+
*/
|
|
108
|
+
const DEFAULT_TURN_SILENCE_MAX_REARM = 8;
|
|
109
|
+
/**
|
|
110
|
+
* Sanity clamp for `SPECTRAL_TURN_SILENCE_MAX_REARM`. Even with the shortest
|
|
111
|
+
* useful timeout this is days of silence, so a typo like `1e10` cannot keep a
|
|
112
|
+
* hung turn open effectively forever.
|
|
113
|
+
*/
|
|
114
|
+
const MAX_TURN_SILENCE_MAX_REARM = 1000;
|
|
115
|
+
function resolveTurnSilenceTimeoutMs() {
|
|
116
|
+
const raw = process.env.SPECTRAL_TURN_SILENCE_TIMEOUT_MS;
|
|
117
|
+
if (raw === undefined || raw.trim() === "") {
|
|
118
|
+
return DEFAULT_TURN_SILENCE_TIMEOUT_MS;
|
|
119
|
+
}
|
|
120
|
+
const parsed = Number(raw);
|
|
121
|
+
// `0` is the documented way to switch the safety net off entirely.
|
|
122
|
+
if (parsed === 0)
|
|
123
|
+
return 0;
|
|
124
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
125
|
+
return DEFAULT_TURN_SILENCE_TIMEOUT_MS;
|
|
126
|
+
}
|
|
127
|
+
// `Math.max(1, ...)`: a fractional value such as `0.5` must floor to 1ms,
|
|
128
|
+
// not to 0 — flooring to 0 would silently switch the safety net off.
|
|
129
|
+
return Math.min(Math.max(1, Math.floor(parsed)), MAX_TIMEOUT_DELAY_MS);
|
|
130
|
+
}
|
|
131
|
+
function resolveTurnSilenceMaxRearm() {
|
|
132
|
+
const raw = process.env.SPECTRAL_TURN_SILENCE_MAX_REARM;
|
|
133
|
+
// Empty / whitespace-only must NOT fall through to `Number(...)`, which
|
|
134
|
+
// coerces `""` to `0` and would therefore mean "unlimited re-arms" —
|
|
135
|
+
// i.e. the original "phantom streaming forever" bug.
|
|
136
|
+
if (raw === undefined || raw.trim() === "") {
|
|
137
|
+
return DEFAULT_TURN_SILENCE_MAX_REARM;
|
|
138
|
+
}
|
|
139
|
+
const parsed = Number(raw);
|
|
140
|
+
// `0` is the documented way to allow unlimited re-arms (opt-out of the
|
|
141
|
+
// re-arm budget).
|
|
142
|
+
if (parsed === 0)
|
|
143
|
+
return 0;
|
|
144
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
145
|
+
return DEFAULT_TURN_SILENCE_MAX_REARM;
|
|
146
|
+
}
|
|
147
|
+
return Math.min(Math.max(1, Math.floor(parsed)), MAX_TURN_SILENCE_MAX_REARM);
|
|
148
|
+
}
|
|
89
149
|
// Streaming-meaningful events that reset the silence timer while a turn is
|
|
90
150
|
// active. Mirrors Fix C on the landing side, but EXCLUDES `token_usage`
|
|
91
151
|
// (arrives post-turn) and excludes terminal/control events handled by their
|
|
@@ -102,7 +162,17 @@ const STREAMING_MEANINGFUL_EVENT_TYPES = new Set([
|
|
|
102
162
|
"tool_result",
|
|
103
163
|
"message_end",
|
|
104
164
|
"subagent_start",
|
|
165
|
+
// WARNING: `subagent_progress` only resets the timer if it is ALSO
|
|
166
|
+
// replayable. The re-arm below lives inside `if (isReplayable(event))`,
|
|
167
|
+
// and `subagent_progress` is deliberately live-only (see `isReplayable`),
|
|
168
|
+
// so this entry is currently inert — `subagent_tool_progress` is the
|
|
169
|
+
// subagent heartbeat that actually works. Do not rely on this entry until
|
|
170
|
+
// the re-arm is hoisted out of the `isReplayable` gate.
|
|
105
171
|
"subagent_progress",
|
|
172
|
+
// `subagent_tool_progress` is the only live signal a subagent emits while
|
|
173
|
+
// it sits inside one long tool call (bash build/test). Without it a busy
|
|
174
|
+
// but "quiet" subagent looks like silence and the turn gets closed.
|
|
175
|
+
"subagent_tool_progress",
|
|
106
176
|
"subagent_end",
|
|
107
177
|
]);
|
|
108
178
|
/**
|
|
@@ -1207,6 +1277,9 @@ export class SessionStreamManager {
|
|
|
1207
1277
|
// next-turn events (including the upcoming `message_start`) are not
|
|
1208
1278
|
// rejected. The fence is re-armed if this turn later closes.
|
|
1209
1279
|
stream.closedTurnGeneration = null;
|
|
1280
|
+
// Fresh run: drop any subagent tracking left over from a previous turn
|
|
1281
|
+
// that ended without a matching `subagent_end`.
|
|
1282
|
+
this.resetSubagentTracking(stream);
|
|
1210
1283
|
// New logical run. This is the only place `busy` transitions
|
|
1211
1284
|
// false -> true (loop iterations and auto-dequeue also go through
|
|
1212
1285
|
// `prompt()`). Retries/continuations do NOT bump the generation.
|
|
@@ -1324,6 +1397,9 @@ export class SessionStreamManager {
|
|
|
1324
1397
|
// the synthetic agent_end below isn't later double-emitted by a stale
|
|
1325
1398
|
// timer.
|
|
1326
1399
|
this.clearCurrentTurnSilenceTimer(stream);
|
|
1400
|
+
// The bridge is being torn down, so no `subagent_end` will arrive for any
|
|
1401
|
+
// subagent that is still running. Drop the tracking state.
|
|
1402
|
+
this.resetSubagentTracking(stream);
|
|
1327
1403
|
// Dispose the agent bridge immediately — this tears down spectral's session and
|
|
1328
1404
|
// unsubscribe. The bridge's own event handler is detached; no further
|
|
1329
1405
|
// events will flow. We broadcast agent_end ourselves below.
|
|
@@ -1523,11 +1599,21 @@ export class SessionStreamManager {
|
|
|
1523
1599
|
stream.currentTurnSilenceTimer = null;
|
|
1524
1600
|
}
|
|
1525
1601
|
}
|
|
1602
|
+
/** Clear subagent-in-flight bookkeeping (used by the silence safety net). */
|
|
1603
|
+
resetSubagentTracking(stream) {
|
|
1604
|
+
stream.activeSubagentToolCallIds.clear();
|
|
1605
|
+
stream.currentTurnSilenceRearmCount = 0;
|
|
1606
|
+
}
|
|
1526
1607
|
armCurrentTurnSilenceTimer(stream) {
|
|
1527
1608
|
this.clearCurrentTurnSilenceTimer(stream);
|
|
1609
|
+
const timeoutMs = resolveTurnSilenceTimeoutMs();
|
|
1610
|
+
// `0` (or any disabled value) means the safety net is switched off: never
|
|
1611
|
+
// arm the timer at all.
|
|
1612
|
+
if (timeoutMs <= 0)
|
|
1613
|
+
return;
|
|
1528
1614
|
stream.currentTurnSilenceTimer = setTimeout(() => {
|
|
1529
1615
|
this.handleCurrentTurnSilenceTimeout(stream);
|
|
1530
|
-
},
|
|
1616
|
+
}, timeoutMs);
|
|
1531
1617
|
}
|
|
1532
1618
|
handleCurrentTurnSilenceTimeout(stream) {
|
|
1533
1619
|
stream.currentTurnSilenceTimer = null;
|
|
@@ -1538,8 +1624,36 @@ export class SessionStreamManager {
|
|
|
1538
1624
|
if (stream.currentTurn == null)
|
|
1539
1625
|
return;
|
|
1540
1626
|
const sessionId = stream.sessionId;
|
|
1627
|
+
const maxRearm = resolveTurnSilenceMaxRearm();
|
|
1628
|
+
// Set when the re-arm budget is exhausted while subagents are still
|
|
1629
|
+
// tracked in flight: the turn is closed anyway and the UI needs a
|
|
1630
|
+
// distinguishable signal (see the `error` broadcast below).
|
|
1631
|
+
let stuckSubagentCount = 0;
|
|
1632
|
+
// A subagent is still running. Closing the turn now would flush the
|
|
1633
|
+
// buffer and set `closedTurnGeneration`, which silently discards every
|
|
1634
|
+
// replayable event the subagent emits afterwards (`subagent_end`,
|
|
1635
|
+
// `tool_result`, `message_end`) while it keeps burning tokens. Re-arm
|
|
1636
|
+
// instead, up to a bounded number of times.
|
|
1637
|
+
if (stream.activeSubagentToolCallIds.size > 0) {
|
|
1638
|
+
const unlimited = maxRearm === 0;
|
|
1639
|
+
if (unlimited || stream.currentTurnSilenceRearmCount < maxRearm) {
|
|
1640
|
+
stream.currentTurnSilenceRearmCount += 1;
|
|
1641
|
+
console.warn(`[session-stream] currentTurn silence timeout deferred: ` +
|
|
1642
|
+
`${stream.activeSubagentToolCallIds.size} subagent(s) still active ` +
|
|
1643
|
+
`in session ${sessionId}, re-arming ` +
|
|
1644
|
+
`(${stream.currentTurnSilenceRearmCount}/${unlimited ? "unlimited" : maxRearm})`);
|
|
1645
|
+
this.armCurrentTurnSilenceTimer(stream);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
stuckSubagentCount = stream.activeSubagentToolCallIds.size;
|
|
1649
|
+
console.warn(`[session-stream] currentTurn silence timeout reached max re-arms ` +
|
|
1650
|
+
`(${maxRearm}) with ${stream.activeSubagentToolCallIds.size} ` +
|
|
1651
|
+
`subagent(s) still active in session ${sessionId}; ` +
|
|
1652
|
+
`closing the turn anyway`);
|
|
1653
|
+
}
|
|
1654
|
+
stream.currentTurnSilenceRearmCount = 0;
|
|
1541
1655
|
console.warn(`[session-stream] currentTurn silence timeout fired ` +
|
|
1542
|
-
`(${
|
|
1656
|
+
`(${resolveTurnSilenceTimeoutMs() / 1000}s without a ` +
|
|
1543
1657
|
`streaming-meaningful event), emitting synthetic agent_end ` +
|
|
1544
1658
|
`for session ${sessionId}`);
|
|
1545
1659
|
const finishedTurn = stream.currentTurn;
|
|
@@ -1547,6 +1661,22 @@ export class SessionStreamManager {
|
|
|
1547
1661
|
stream.lastFlushedEventCount = 0;
|
|
1548
1662
|
stream.currentTurn = null;
|
|
1549
1663
|
stream.closedTurnGeneration = stream.bridgeTurnGeneration;
|
|
1664
|
+
// Distinguish "gave up on a still-running subagent" from a clean
|
|
1665
|
+
// synthetic `agent_end`: without this the UI sees an ordinary turn end
|
|
1666
|
+
// and the subagent's result (which is dropped on the
|
|
1667
|
+
// `closedTurnGeneration` fence) disappears without any explanation.
|
|
1668
|
+
if (stuckSubagentCount > 0) {
|
|
1669
|
+
console.warn(`[session-stream] turn silence timeout exhausted after ${maxRearm} ` +
|
|
1670
|
+
`re-arm(s) while ${stuckSubagentCount} subagent(s) were still ` +
|
|
1671
|
+
`active in session ${sessionId}; their remaining events will be ` +
|
|
1672
|
+
`dropped on the closed-turn fence`);
|
|
1673
|
+
this.broadcast(stream, {
|
|
1674
|
+
type: "error",
|
|
1675
|
+
message: `Turn silence timeout exhausted after ${maxRearm} re-arm(s) while ` +
|
|
1676
|
+
`${stuckSubagentCount} subagent(s) were still active. The turn was ` +
|
|
1677
|
+
`closed and any result they emit from now on will be discarded.`,
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1550
1680
|
// Reuse the normal broadcast path so relay subscribers (landing
|
|
1551
1681
|
// WebSocket clients) receive agent_end and stop showing "streaming".
|
|
1552
1682
|
this.broadcast(stream, { type: "agent_end" });
|
|
@@ -1706,6 +1836,8 @@ export class SessionStreamManager {
|
|
|
1706
1836
|
currentTurn: null,
|
|
1707
1837
|
bridgeTurnGeneration: 0,
|
|
1708
1838
|
closedTurnGeneration: null,
|
|
1839
|
+
activeSubagentToolCallIds: new Set(),
|
|
1840
|
+
currentTurnSilenceRearmCount: 0,
|
|
1709
1841
|
runState: {
|
|
1710
1842
|
runId: "",
|
|
1711
1843
|
generation: 0,
|
|
@@ -1889,6 +2021,24 @@ export class SessionStreamManager {
|
|
|
1889
2021
|
if (dcpLiteEvent) {
|
|
1890
2022
|
stream.lastDcpEvent = dcpLiteEvent;
|
|
1891
2023
|
}
|
|
2024
|
+
// Track in-flight subagents (tracked BEFORE the `closedTurnGeneration`
|
|
2025
|
+
// fence below so a late `subagent_end` can still clear the counter).
|
|
2026
|
+
// The silence timer uses this to avoid closing a turn out from under a
|
|
2027
|
+
// still-working subagent.
|
|
2028
|
+
if (event.type === "subagent_start") {
|
|
2029
|
+
// Runtime payloads from the SDK are not typechecked. Without this guard
|
|
2030
|
+
// a `toolCallId` of `undefined`/`""` would leave the set size pinned at
|
|
2031
|
+
// 1 forever (the matching `subagent_end` could never delete it), which
|
|
2032
|
+
// permanently disables the silence safety net for the whole session.
|
|
2033
|
+
if (event.toolCallId) {
|
|
2034
|
+
stream.activeSubagentToolCallIds.add(event.toolCallId);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
else if (event.type === "subagent_end") {
|
|
2038
|
+
if (event.toolCallId) {
|
|
2039
|
+
stream.activeSubagentToolCallIds.delete(event.toolCallId);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
1892
2042
|
// Buffer replayable events into the in-flight turn. We intentionally
|
|
1893
2043
|
// accept events even if currentTurn is null (rare race: spectral emits before
|
|
1894
2044
|
// prompt() opened the turn), in which case we open one defensively so
|
|
@@ -1957,11 +2107,12 @@ export class SessionStreamManager {
|
|
|
1957
2107
|
BATCH_FLUSH_INTERVAL) {
|
|
1958
2108
|
this.flushInFlightTurn(stream);
|
|
1959
2109
|
}
|
|
1960
|
-
// Fix E: reset the
|
|
1961
|
-
//
|
|
1962
|
-
//
|
|
1963
|
-
//
|
|
1964
|
-
//
|
|
2110
|
+
// Fix E: reset the turn silence timer (default 30 minutes, see
|
|
2111
|
+
// `SPECTRAL_TURN_SILENCE_TIMEOUT_MS` / `resolveTurnSilenceTimeoutMs`)
|
|
2112
|
+
// whenever a streaming-meaningful event arrives while a turn is
|
|
2113
|
+
// active. As long as the LLM keeps streaming (even slowly), the timer
|
|
2114
|
+
// keeps resetting. `token_usage` is intentionally excluded (arrives
|
|
2115
|
+
// post-turn and would prevent cleanup of a hung turn).
|
|
1965
2116
|
if (stream.currentTurn &&
|
|
1966
2117
|
STREAMING_MEANINGFUL_EVENT_TYPES.has(event.type)) {
|
|
1967
2118
|
this.armCurrentTurnSilenceTimer(stream);
|
|
@@ -2137,6 +2288,9 @@ export class SessionStreamManager {
|
|
|
2137
2288
|
// Fix E: natural agent_end arrived — cancel the safety-net timer so we
|
|
2138
2289
|
// don't double-emit.
|
|
2139
2290
|
this.clearCurrentTurnSilenceTimer(stream);
|
|
2291
|
+
// Defensive: a `subagent_end` may never arrive (crashed subagent), so
|
|
2292
|
+
// never let the counter stay >0 into the next turn.
|
|
2293
|
+
this.resetSubagentTracking(stream);
|
|
2140
2294
|
// Keep the run busy across the agent_end -> continuation gap when the
|
|
2141
2295
|
// SDK signals it will re-enter the loop (retry, length continuation,
|
|
2142
2296
|
// compact-and-retry). The deferred settle below re-resolves the final
|
|
@@ -2249,6 +2403,9 @@ export class SessionStreamManager {
|
|
|
2249
2403
|
// Fix E: error terminates the turn — cancel the safety-net timer so we
|
|
2250
2404
|
// don't double-emit.
|
|
2251
2405
|
this.clearCurrentTurnSilenceTimer(stream);
|
|
2406
|
+
// Defensive: same as agent_end — a failed turn can leave subagents
|
|
2407
|
+
// without a matching `subagent_end`.
|
|
2408
|
+
this.resetSubagentTracking(stream);
|
|
2252
2409
|
this.runTurnCleanup(failedTurn);
|
|
2253
2410
|
this.settleRunState(stream);
|
|
2254
2411
|
}
|