@aexol/spectral 0.9.260 → 0.9.263
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/index.d.ts +26 -0
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +240 -14
- package/dist/generated/zeus/const.d.ts.map +1 -1
- package/dist/generated/zeus/const.js +9 -4
- package/dist/generated/zeus/index.d.ts +68 -43
- package/dist/generated/zeus/index.d.ts.map +1 -1
- package/dist/memory/tool-output-compressor.d.ts +31 -1
- package/dist/memory/tool-output-compressor.d.ts.map +1 -1
- package/dist/memory/tool-output-compressor.js +64 -22
- package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/agent-session.js +28 -6
- package/dist/sdk/coding-agent/core/session-manager.d.ts +6 -2
- package/dist/sdk/coding-agent/core/session-manager.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/session-manager.js +6 -2
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +7 -1
- package/dist/server/session-stream.d.ts +23 -0
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +83 -15
- package/package.json +1 -1
|
@@ -104,6 +104,23 @@ const UI_HISTORY_TAIL_MESSAGE_LIMIT = 60;
|
|
|
104
104
|
* at most the last `BATCH_FLUSH_INTERVAL` events, not the entire turn.
|
|
105
105
|
*/
|
|
106
106
|
const BATCH_FLUSH_INTERVAL = 10;
|
|
107
|
+
/**
|
|
108
|
+
* Auto-recovery threshold (FIX 4): a `bridge.prompt()` rejection that
|
|
109
|
+
* settles within this window is "fast" — it indicates the bridge/SDK
|
|
110
|
+
* session state is broken (synchronous throws settle in single-digit ms)
|
|
111
|
+
* rather than a slow provider failure. Legitimate turn failures (model
|
|
112
|
+
* timeouts, tool errors) take seconds, so they never count toward the
|
|
113
|
+
* streak.
|
|
114
|
+
*/
|
|
115
|
+
const FAST_PROMPT_REJECTION_THRESHOLD_MS = 500;
|
|
116
|
+
/**
|
|
117
|
+
* Auto-recovery threshold (FIX 4): after this many CONSECUTIVE fast
|
|
118
|
+
* `bridge.prompt()` rejections while the stream is idle (no compaction,
|
|
119
|
+
* no active turn), the bridge is disposed and marked for rebuild so the
|
|
120
|
+
* next `prompt()` constructs a fresh bridge from the SQLite history —
|
|
121
|
+
* the same recovery `cancelTurn` performed, but automatic.
|
|
122
|
+
*/
|
|
123
|
+
const AUTO_REBUILD_AFTER_FAST_REJECTIONS = 3;
|
|
107
124
|
export class SessionStreamManager {
|
|
108
125
|
store;
|
|
109
126
|
cwd;
|
|
@@ -1121,13 +1138,47 @@ export class SessionStreamManager {
|
|
|
1121
1138
|
// (e.g. disposed bridge, missing session) arrive as a rejected promise
|
|
1122
1139
|
// we handle here so the frontend sees the error rather than a silent
|
|
1123
1140
|
// dead turn.
|
|
1124
|
-
|
|
1141
|
+
const promptStartedAt = Date.now();
|
|
1142
|
+
stream.bridge
|
|
1143
|
+
.prompt(annotatedContent, images)
|
|
1144
|
+
.then(() => {
|
|
1145
|
+
// The turn completed — a fast-rejection streak (if any) is over.
|
|
1146
|
+
stream.consecutiveFastPromptRejections = 0;
|
|
1147
|
+
})
|
|
1148
|
+
.catch((err) => {
|
|
1125
1149
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1126
1150
|
console.error(`[spectral] error: bridge.prompt failed: ${msg}`);
|
|
1127
1151
|
this.broadcast(stream, {
|
|
1128
1152
|
type: "error",
|
|
1129
1153
|
message: humanizeProviderError(err),
|
|
1130
1154
|
});
|
|
1155
|
+
// FIX 4 auto-recovery: a broken SDK session state makes
|
|
1156
|
+
// `bridge.prompt()` reject synchronously (~ms) on EVERY attempt while
|
|
1157
|
+
// the stream itself returns to idle — the bridge's own catch emits
|
|
1158
|
+
// `agent_end`, which clears `currentTurn`. Without recovery the dead
|
|
1159
|
+
// bridge is reused forever until the user manually presses Stop
|
|
1160
|
+
// (cancelTurn). Count consecutive FAST rejections observed while the
|
|
1161
|
+
// stream is idle (no compaction, no active turn); after a short streak
|
|
1162
|
+
// dispose the bridge and mark it for rebuild so the next `prompt()`
|
|
1163
|
+
// constructs a fresh bridge from the SQLite history. The user message
|
|
1164
|
+
// was persisted BEFORE `bridge.prompt()`, so the rebuild loses nothing.
|
|
1165
|
+
const fast = Date.now() - promptStartedAt <
|
|
1166
|
+
FAST_PROMPT_REJECTION_THRESHOLD_MS;
|
|
1167
|
+
if (!stream.compacting && stream.currentTurn == null && fast) {
|
|
1168
|
+
stream.consecutiveFastPromptRejections += 1;
|
|
1169
|
+
if (stream.consecutiveFastPromptRejections
|
|
1170
|
+
>= AUTO_REBUILD_AFTER_FAST_REJECTIONS) {
|
|
1171
|
+
const streak = stream.consecutiveFastPromptRejections;
|
|
1172
|
+
stream.consecutiveFastPromptRejections = 0;
|
|
1173
|
+
console.warn(`[spectral] warn: auto-rebuilding bridge for ${sessionId}: ${streak} consecutive fast prompt rejections (<${FAST_PROMPT_REJECTION_THRESHOLD_MS}ms) while idle — disposing bridge to rebuild from SQLite history. Last error: ${msg}`);
|
|
1174
|
+
this.disposeAndMarkForRebuild(stream, `Auto-rebuild after ${streak} fast prompt rejections: ${msg}`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
// Slow rejection, compaction in flight, or a turn still active:
|
|
1179
|
+
// no evidence of a broken-bridge streak — reset the counter.
|
|
1180
|
+
stream.consecutiveFastPromptRejections = 0;
|
|
1181
|
+
}
|
|
1131
1182
|
});
|
|
1132
1183
|
}
|
|
1133
1184
|
/**
|
|
@@ -1181,6 +1232,35 @@ export class SessionStreamManager {
|
|
|
1181
1232
|
}
|
|
1182
1233
|
return n;
|
|
1183
1234
|
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Shared teardown for "the bridge is dead — rebuild it from SQLite on the
|
|
1237
|
+
* next prompt": disposes the current bridge and marks the stream with a
|
|
1238
|
+
* `startError` so `dispatchPrompt` reconstructs a fresh bridge from the
|
|
1239
|
+
* persisted session history (subscribers, cwd and stream metadata are
|
|
1240
|
+
* preserved — only the bridge is replaced).
|
|
1241
|
+
*
|
|
1242
|
+
* Used by `cancelTurn` (the user pressed Stop) and by the FIX 4
|
|
1243
|
+
* auto-recovery path in the `bridge.prompt().catch` handler (repeated
|
|
1244
|
+
* fast rejections from a broken SDK session state). The user message of
|
|
1245
|
+
* a failed prompt is persisted BEFORE `bridge.prompt()` is invoked, so
|
|
1246
|
+
* the rebuild loses nothing.
|
|
1247
|
+
*/
|
|
1248
|
+
disposeAndMarkForRebuild(stream, reason) {
|
|
1249
|
+
try {
|
|
1250
|
+
stream.bridge.dispose();
|
|
1251
|
+
}
|
|
1252
|
+
catch {
|
|
1253
|
+
// ignore — dispose failures must not mask the rebuild marker
|
|
1254
|
+
}
|
|
1255
|
+
// Set startError so any late `await stream.ready` rejects cleanly
|
|
1256
|
+
// instead of hanging. Also prevents the next prompt() from waiting on
|
|
1257
|
+
// a dead bridge's ready promise.
|
|
1258
|
+
stream.startError = new Error(reason);
|
|
1259
|
+
// Clear batch-persist tracking so the next turn doesn't accidentally
|
|
1260
|
+
// flush events against a stale messageId.
|
|
1261
|
+
stream.currentMessageId = null;
|
|
1262
|
+
stream.lastFlushedEventCount = 0;
|
|
1263
|
+
}
|
|
1184
1264
|
/**
|
|
1185
1265
|
* Cancel the in-flight turn for a session (user pressed Stop in the UI).
|
|
1186
1266
|
* Disposes the agent bridge and broadcasts `agent_end` so all subscribers
|
|
@@ -1214,20 +1294,7 @@ export class SessionStreamManager {
|
|
|
1214
1294
|
// Dispose the agent bridge immediately — this tears down spectral's session and
|
|
1215
1295
|
// unsubscribe. The bridge's own event handler is detached; no further
|
|
1216
1296
|
// events will flow. We broadcast agent_end ourselves below.
|
|
1217
|
-
|
|
1218
|
-
stream.bridge.dispose();
|
|
1219
|
-
}
|
|
1220
|
-
catch {
|
|
1221
|
-
// ignore
|
|
1222
|
-
}
|
|
1223
|
-
// Set startError so any late `await stream.ready` rejects cleanly
|
|
1224
|
-
// instead of hanging. Also prevents the next prompt() from waiting on
|
|
1225
|
-
// a dead bridge's ready promise.
|
|
1226
|
-
stream.startError = new Error("Turn cancelled");
|
|
1227
|
-
// Clear batch-persist tracking so the next turn doesn't accidentally
|
|
1228
|
-
// flush events against a stale messageId.
|
|
1229
|
-
stream.currentMessageId = null;
|
|
1230
|
-
stream.lastFlushedEventCount = 0;
|
|
1297
|
+
this.disposeAndMarkForRebuild(stream, "Turn cancelled");
|
|
1231
1298
|
// Broadcast agent_end so all subscribers close their open turn and
|
|
1232
1299
|
// re-enable their composers. Use the pre-disposal flag — dispose()
|
|
1233
1300
|
// may have cleared currentTurn via an error event from the torn-down
|
|
@@ -1660,6 +1727,7 @@ export class SessionStreamManager {
|
|
|
1660
1727
|
contextWindowUsed: null,
|
|
1661
1728
|
contextWindowMax: null,
|
|
1662
1729
|
lastDcpEvent: null,
|
|
1730
|
+
consecutiveFastPromptRejections: 0,
|
|
1663
1731
|
};
|
|
1664
1732
|
const memorySnapshot = this.store.getSessionMemorySnapshot(sessionId);
|
|
1665
1733
|
const bridgeHistory = selectHistoryForBridge(history, memorySnapshot != null);
|