@evandu/super-pi 0.1.137 → 0.1.139

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.
Files changed (62) hide show
  1. package/agents/super-pi-knowledge/index.ts +11 -13
  2. package/agents/super-pi-memory/index.ts +11 -5
  3. package/bin/super-pi.js +7 -11
  4. package/dist/.super-pi-check-inputs.json +2 -2
  5. package/dist/client/.super-pi-client-sources.json +6 -6
  6. package/dist/client/assets/{CodeMirrorEditor-CkA_5D5r.js → CodeMirrorEditor-H7SHGhdS.js} +2 -2
  7. package/dist/client/assets/{dist-DcdzfM-n.js → dist-2XXLO-IZ.js} +1 -1
  8. package/dist/client/assets/{dist-ufRHdQ8G.js → dist-BEM6tttY.js} +1 -1
  9. package/dist/client/assets/{dist-9Fm8XKu4.js → dist-BfUeRJKr.js} +1 -1
  10. package/dist/client/assets/{dist-CrxmpY93.js → dist-BokzcFLE.js} +1 -1
  11. package/dist/client/assets/{dist-BfEmujEC.js → dist-BoucVzKA.js} +1 -1
  12. package/dist/client/assets/{dist-B02Erd0t.js → dist-BxTGjU84.js} +1 -1
  13. package/dist/client/assets/{dist-DGhdkud4.js → dist-C-MUXK_t.js} +1 -1
  14. package/dist/client/assets/{dist-k6bOz9RS.js → dist-C8NllalC.js} +1 -1
  15. package/dist/client/assets/{dist-C2tSd-AY.js → dist-CSdzvQnf.js} +1 -1
  16. package/dist/client/assets/{dist-BySjislr.js → dist-CUNctZ5v.js} +1 -1
  17. package/dist/client/assets/{dist-B3Mfuv1D.js → dist-CZ9Krsno.js} +1 -1
  18. package/dist/client/assets/{dist-DbovsYeH.js → dist-C_ftUQZN.js} +1 -1
  19. package/dist/client/assets/{dist-j9X7j381.js → dist-CuMnZOGE.js} +1 -1
  20. package/dist/client/assets/{dist-D-LtG92p.js → dist-CukPKrEn.js} +1 -1
  21. package/dist/client/assets/{dist-b9CfAgzG.js → dist-D3Ta0y4e.js} +1 -1
  22. package/dist/client/assets/{dist-BsqzrZ5R.js → dist-DJ8SwQl_.js} +1 -1
  23. package/dist/client/assets/{dist-BqGjijI6.js → dist-DLKVFZhO.js} +1 -1
  24. package/dist/client/assets/{dist-MC2XuZgq.js → dist-Jw-GQ-re.js} +1 -1
  25. package/dist/client/assets/{dist-D_umYm0o.js → dist-NlopeJ5R.js} +1 -1
  26. package/dist/client/assets/{dist-CsNytPug.js → dist-POoQ4j29.js} +1 -1
  27. package/dist/client/assets/{dist-DdVs8Y0X.js → dist-cP2vnTP_.js} +1 -1
  28. package/dist/client/assets/{dist-DL4Q05ts.js → dist-zUJqdbfo.js} +1 -1
  29. package/dist/client/assets/{index-DVmE8H4Y.js → index-B32VZVYx.js} +13 -13
  30. package/dist/client/assets/index-Bdym8BPo.css +1 -0
  31. package/dist/client/index.html +2 -2
  32. package/dist/server/application/conversation-service.d.ts +13 -4
  33. package/dist/server/application/conversation-service.js +112 -61
  34. package/dist/server/application/run-service.d.ts +13 -0
  35. package/dist/server/application/run-service.js +255 -69
  36. package/dist/server/application/scheduled-task-service.js +8 -1
  37. package/dist/server/application/workspace-service.js +16 -1
  38. package/dist/server/feishu-cli.js +15 -4
  39. package/dist/server/file-index.d.ts +30 -0
  40. package/dist/server/file-index.js +49 -4
  41. package/dist/server/files.d.ts +21 -0
  42. package/dist/server/files.js +38 -0
  43. package/dist/server/http-server.js +1455 -1412
  44. package/dist/server/index.js +3 -4
  45. package/dist/server/logging.js +56 -3
  46. package/dist/server/pi-session-pool.d.ts +2 -0
  47. package/dist/server/pi-session-pool.js +1 -0
  48. package/dist/server/pi-session.d.ts +118 -0
  49. package/dist/server/pi-session.js +132 -10
  50. package/dist/server/runtime.d.ts +47 -0
  51. package/dist/server/runtime.js +183 -84
  52. package/dist/server/store.d.ts +44 -1
  53. package/dist/server/store.js +114 -12
  54. package/dist/server/subagent-runs.js +18 -7
  55. package/dist/server/subagents.js +7 -0
  56. package/dist/server/tool-runner.js +13 -4
  57. package/dist/server/types.d.ts +27 -0
  58. package/dist/server/ws-server.d.ts +34 -5
  59. package/dist/server/ws-server.js +170 -46
  60. package/package.json +15 -1
  61. package/scripts/diagnose.mjs +161 -58
  62. package/dist/client/assets/index-FxTrUapG.css +0 -1
@@ -1,6 +1,48 @@
1
1
  import { WebSocket, WebSocketServer } from "ws";
2
2
  import { isAuthorized } from "./http-server.js";
3
3
  import { describeThrown, describeThrownWithStack } from "./failure-text.js";
4
+ /**
5
+ * A WebSocket is a live transport, not an unbounded queue. `ws.send()` only
6
+ * copies bytes into the socket's userland buffer; a browser that is frozen or
7
+ * a network peer that has disappeared can therefore make the buffer grow
8
+ * without throwing. Once this threshold is crossed we drop that connection
9
+ * and let the normal close path clean up its resources.
10
+ */
11
+ export const MAX_WS_BUFFERED_BYTES = 8 * 1024 * 1024;
12
+ /** Maximum size of one inbound WebSocket message. Attachments are uploaded
13
+ * over HTTP and commands contain references, so multi-megabyte WS frames are
14
+ * almost certainly malformed or abusive. */
15
+ export const MAX_WS_PAYLOAD_BYTES = 4 * 1024 * 1024;
16
+ /**
17
+ * Send one frame without allowing a race with close/error to escape as an
18
+ * uncaught exception. The `ws` package can throw synchronously after the
19
+ * readyState check (for example when a close happens between the two calls),
20
+ * and an unhandled `error` event can otherwise terminate the Node process.
21
+ */
22
+ export function safeSend(socket, payload) {
23
+ if (socket.readyState !== WebSocket.OPEN)
24
+ return false;
25
+ const bufferedAmount = Number(socket.bufferedAmount ?? 0);
26
+ if (Number.isFinite(bufferedAmount) && bufferedAmount > MAX_WS_BUFFERED_BYTES) {
27
+ try {
28
+ socket.terminate();
29
+ }
30
+ catch { /* close races are already being cleaned up */ }
31
+ return false;
32
+ }
33
+ try {
34
+ socket.send(payload);
35
+ return true;
36
+ }
37
+ catch (error) {
38
+ console.warn(`Super Pi: WebSocket send failed: ${error instanceof Error ? error.message : String(error)}`);
39
+ try {
40
+ socket.terminate();
41
+ }
42
+ catch { /* close races are already being cleaned up */ }
43
+ return false;
44
+ }
45
+ }
4
46
  /**
5
47
  * Concrete implementation of EventBroadcaster delivering server events
6
48
  * across active WebSocket connections.
@@ -8,16 +50,19 @@ import { describeThrown, describeThrownWithStack } from "./failure-text.js";
8
50
  export class WsBroadcaster {
9
51
  getState;
10
52
  sockets = new Set();
53
+ // Undefined preserves the original full-state protocol. An object, including {}, opts into
54
+ // conversation-focused snapshots from the very first frame of the connection.
55
+ scopes = new Map();
56
+ fileIndexScopes = new Map();
11
57
  coalescedSockets = new Set();
12
58
  pendingStreams = new Map();
13
- cachedState;
14
- cachedStatePayload;
15
59
  stateFlushScheduled = false;
16
60
  constructor(getState) {
17
61
  this.getState = getState;
18
62
  }
19
- register(socket) {
63
+ register(socket, scope) {
20
64
  this.sockets.add(socket);
65
+ this.scopes.set(socket, scope);
21
66
  }
22
67
  unregister(socket) {
23
68
  this.coalescedSockets.delete(socket);
@@ -25,8 +70,49 @@ export class WsBroadcaster {
25
70
  if (pending)
26
71
  clearTimeout(pending.timer);
27
72
  this.pendingStreams.delete(socket);
73
+ this.scopes.delete(socket);
74
+ this.fileIndexScopes.delete(socket);
28
75
  this.sockets.delete(socket);
29
76
  }
77
+ subscribe(socket, conversationId) {
78
+ // Finish the old transcript before replacing it. A later delta then belongs to the new scope,
79
+ // and the authoritative snapshot reconstructs anything not streamed while it was unselected.
80
+ this.flushStreaming(socket);
81
+ this.scopes.set(socket, conversationId ? { conversationId } : {});
82
+ this.sendState(socket);
83
+ }
84
+ sendState(socket) {
85
+ if (socket.readyState !== WebSocket.OPEN)
86
+ return;
87
+ this.flushStreaming(socket);
88
+ safeSend(socket, JSON.stringify({ type: "state", state: this.getState(this.scopes.get(socket)) }));
89
+ }
90
+ subscribeFileIndex(socket, workspaceId) {
91
+ if (workspaceId)
92
+ this.fileIndexScopes.set(socket, workspaceId);
93
+ else
94
+ this.fileIndexScopes.delete(socket);
95
+ }
96
+ receives(socket, event) {
97
+ const scope = this.scopes.get(socket);
98
+ if (!scope)
99
+ return true;
100
+ switch (event.type) {
101
+ case "files.index":
102
+ return this.fileIndexScopes.get(socket) === event.workspaceId;
103
+ case "message.delta":
104
+ case "message.thinking":
105
+ case "message.tools":
106
+ return event.conversationId === scope.conversationId;
107
+ case "subagent.event":
108
+ return event.run.conversationId === scope.conversationId;
109
+ case "workflow.event":
110
+ return event.workflow.conversationId === scope.conversationId;
111
+ default:
112
+ // Lifecycle and summary changes still reach the sidebar for every conversation.
113
+ return true;
114
+ }
115
+ }
30
116
  configureStreaming(socket, coalesce) {
31
117
  if (coalesce)
32
118
  this.coalescedSockets.add(socket);
@@ -42,8 +128,7 @@ export class WsBroadcaster {
42
128
  clearTimeout(pending.timer);
43
129
  this.pendingStreams.delete(socket);
44
130
  for (const event of pending.events.values())
45
- if (socket.readyState === WebSocket.OPEN)
46
- socket.send(JSON.stringify(event));
131
+ safeSend(socket, JSON.stringify(event));
47
132
  }
48
133
  enqueueStreaming(socket, event) {
49
134
  let pending = this.pendingStreams.get(socket);
@@ -67,29 +152,24 @@ export class WsBroadcaster {
67
152
  return this.sockets;
68
153
  }
69
154
  broadcast(event) {
155
+ if (event.type === "state") {
156
+ this.broadcastState();
157
+ return;
158
+ }
70
159
  const streaming = event.type === "message.delta" || event.type === "message.thinking";
71
- /**
72
- * SERIALIZED ONCE, NOT ONCE PER SOCKET. A `state` frame carries the whole application state, so
73
- * `JSON.stringify` on it is the most expensive thing this method does - and every open tab used
74
- * to pay for its own copy of the same string. The bytes are identical (no socket-specific field),
75
- * so the string is built once and handed to every socket.
76
- *
77
- * Stream deltas stay per-event: they are coalesced per socket, so there is no single payload to
78
- * share, and their cost is one small string.
79
- */
80
- const startedAt = performance.now();
81
- const payload = streaming ? undefined : JSON.stringify(event);
82
- if (!streaming && event.type === "state" && process.env.SUPER_PI_TRACE_LATENCY === "1")
83
- console.warn(`[super-pi latency] phase=state.broadcast bytes=${payload?.length ?? 0} sockets=${this.sockets.size} serializeMs=${Math.round(performance.now() - startedAt)}`);
160
+ // Serialize common events once, after finding a recipient. Coalesced text stays per socket;
161
+ // conversation snapshots are projected and serialized once per scope in broadcastState().
162
+ let payload;
84
163
  for (const socket of this.sockets) {
85
- if (socket.readyState !== WebSocket.OPEN)
164
+ if (socket.readyState !== WebSocket.OPEN || !this.receives(socket, event))
86
165
  continue;
87
166
  if (streaming && this.coalescedSockets.has(socket))
88
167
  this.enqueueStreaming(socket, event);
89
168
  else {
90
169
  // A state/tool/lifecycle event is a transcript boundary. Flush its preceding text first.
91
170
  this.flushStreaming(socket);
92
- socket.send(payload ?? JSON.stringify(event));
171
+ payload ??= JSON.stringify(event);
172
+ safeSend(socket, payload);
93
173
  }
94
174
  }
95
175
  }
@@ -99,21 +179,38 @@ export class WsBroadcaster {
99
179
  this.stateFlushScheduled = true;
100
180
  queueMicrotask(() => {
101
181
  this.stateFlushScheduled = false;
102
- this.cachedState = this.getState();
103
- this.cachedStatePayload = JSON.stringify({ type: "state", state: this.cachedState });
104
- const payload = this.cachedStatePayload;
182
+ const groups = new Map();
105
183
  for (const socket of this.sockets) {
106
184
  if (socket.readyState !== WebSocket.OPEN)
107
185
  continue;
108
- this.flushStreaming(socket);
109
- socket.send(payload);
186
+ const scope = this.scopes.get(socket);
187
+ const key = scope ? `focused:${JSON.stringify(scope.conversationId ?? null)}` : "legacy";
188
+ let group = groups.get(key);
189
+ if (!group) {
190
+ group = { scope, sockets: [] };
191
+ groups.set(key, group);
192
+ }
193
+ group.sockets.push(socket);
194
+ }
195
+ for (const { scope, sockets } of groups.values()) {
196
+ const snapshotStartedAt = performance.now();
197
+ const state = this.getState(scope);
198
+ const snapshotMs = performance.now() - snapshotStartedAt;
199
+ const serializeStartedAt = performance.now();
200
+ const cachedStatePayload = JSON.stringify({ type: "state", state });
201
+ const serializeMs = performance.now() - serializeStartedAt;
202
+ if (process.env.SUPER_PI_TRACE_LATENCY === "1") {
203
+ console.warn(`[super-pi latency] phase=state.snapshot.ready bytes=${Buffer.byteLength(cachedStatePayload, "utf8")} snapshotMs=${Math.round(snapshotMs)} serializeMs=${Math.round(serializeMs)} sockets=${sockets.length} scope=${scope ? "focused" : "legacy"}`);
204
+ }
205
+ for (const socket of sockets) {
206
+ this.flushStreaming(socket);
207
+ safeSend(socket, cachedStatePayload);
208
+ }
110
209
  }
111
- this.cachedState = undefined;
112
- this.cachedStatePayload = undefined;
113
210
  });
114
211
  }
115
- getStateSnapshot() {
116
- return this.getState();
212
+ getStateSnapshot(scope) {
213
+ return this.getState(scope);
117
214
  }
118
215
  }
119
216
  import { checkDiskVersion } from "./version.js";
@@ -135,7 +232,11 @@ export class WsServer {
135
232
  this.accessToken = options.accessToken;
136
233
  this.rootDir = options.rootDir;
137
234
  this.onSocketClose = options.onSocketClose;
138
- this.websocketServer = new WebSocketServer({ noServer: true });
235
+ // Remote clients are commonly separated from the server by a high-latency link. State snapshots can
236
+ // contain a long transcript, so sending them uncompressed turns every lifecycle event into a multi-MB
237
+ // transfer. Per-message deflate keeps the protocol unchanged while making the wire cost proportional
238
+ // to the actual information in the snapshot. The threshold avoids spending CPU on token-sized events.
239
+ this.websocketServer = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD_BYTES, perMessageDeflate: { threshold: 1024, serverNoContextTakeover: true } });
139
240
  this.setupUpgrade(options.httpServer);
140
241
  this.setupConnectionHandling();
141
242
  }
@@ -167,7 +268,17 @@ export class WsServer {
167
268
  }
168
269
  setupConnectionHandling() {
169
270
  this.websocketServer.on("connection", (socket, request) => {
170
- this.broadcaster.register(socket);
271
+ const url = new URL(request.url ?? "/ws", "http://localhost");
272
+ const scope = url.searchParams.get("state") === "focused"
273
+ ? { conversationId: url.searchParams.get("conversationId") || undefined } : undefined;
274
+ this.broadcaster.register(socket, scope);
275
+ socket.on("error", (error) => {
276
+ console.warn(`Super Pi: WebSocket error: ${error instanceof Error ? error.message : String(error)}`);
277
+ try {
278
+ socket.terminate();
279
+ }
280
+ catch { /* already closed */ }
281
+ });
171
282
  /**
172
283
  * The peer address, captured ONCE per connection rather than read per message.
173
284
  *
@@ -184,18 +295,18 @@ export class WsServer {
184
295
  * only the shells belonging to the socket that went away.
185
296
  */
186
297
  const terminalSessionIds = new Set();
187
- socket.send(JSON.stringify({ type: "state", state: this.broadcaster.getStateSnapshot() }));
298
+ this.broadcaster.sendState(socket);
188
299
  if (this.rootDir) {
189
- socket.send(JSON.stringify({ type: "server.version", version: checkDiskVersion(this.rootDir) }));
300
+ safeSend(socket, JSON.stringify({ type: "server.version", version: checkDiskVersion(this.rootDir) }));
190
301
  }
191
302
  void this.catalogService.agentCatalog().then((catalog) => {
192
303
  if (socket.readyState === WebSocket.OPEN) {
193
- socket.send(JSON.stringify({ type: "agent.catalog", catalog }));
304
+ safeSend(socket, JSON.stringify({ type: "agent.catalog", catalog }));
194
305
  }
195
306
  }).catch((error) => console.warn(`Super Pi: agent catalog for a new connection failed: ${error instanceof Error ? error.message : "unknown error"}`));
196
307
  void this.catalogService.subagentCatalog().then((catalog) => {
197
308
  if (socket.readyState === WebSocket.OPEN) {
198
- socket.send(JSON.stringify({ type: "subagent.catalog", catalog }));
309
+ safeSend(socket, JSON.stringify({ type: "subagent.catalog", catalog }));
199
310
  }
200
311
  }).catch((error) => console.warn(`Super Pi: sub-agent catalog for a new connection failed: ${error instanceof Error ? error.message : "unknown error"}`));
201
312
  /**
@@ -219,7 +330,7 @@ export class WsServer {
219
330
  if (this.rootDir) {
220
331
  const v = checkDiskVersion(this.rootDir);
221
332
  if (v.needsRestart) {
222
- socket.send(JSON.stringify({ type: "server.version", version: v }));
333
+ safeSend(socket, JSON.stringify({ type: "server.version", version: v }));
223
334
  }
224
335
  }
225
336
  }, 30_000);
@@ -249,18 +360,34 @@ export class WsServer {
249
360
  }
250
361
  catch {
251
362
  console.error("Super Pi command error: received a frame that is not valid JSON");
252
- socket.send(JSON.stringify({ type: "error", message: "收到一条无法解析的命令,已忽略。" }));
363
+ safeSend(socket, JSON.stringify({ type: "error", message: "收到一条无法解析的命令,已忽略。" }));
364
+ return;
365
+ }
366
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || typeof parsed.type !== "string") {
367
+ safeSend(socket, JSON.stringify({ type: "error", message: "收到一条格式无效的命令,已忽略。" }));
368
+ return;
369
+ }
370
+ if (parsed.type === "files.index.subscribe") {
371
+ this.broadcaster.subscribeFileIndex(socket, typeof parsed.workspaceId === "string" ? parsed.workspaceId : undefined);
253
372
  return;
254
373
  }
255
374
  if (parsed.type === "stream.configure") {
256
375
  this.broadcaster.configureStreaming(socket, parsed.coalesce);
257
376
  return;
258
377
  }
378
+ if (parsed.type === "state.subscribe") {
379
+ if (parsed.conversationId !== undefined && typeof parsed.conversationId !== "string") {
380
+ safeSend(socket, JSON.stringify({ type: "error", message: "会话订阅标识必须是字符串。" }));
381
+ return;
382
+ }
383
+ this.broadcaster.subscribe(socket, parsed.conversationId);
384
+ return;
385
+ }
259
386
  // A send or selected queued turn is a streaming operation. It must leave the socket's command
260
387
  // chain immediately: waiting for the model turn would block later priority/cancel commands and
261
388
  // make a second send miss the durable queue. The service owns per-conversation run claims.
262
389
  const runIndependently = parsed.type === "message.send" || parsed.type === "message.sendQueued"
263
- || parsed.type === "message.interrupt" || parsed.type === "run.stop";
390
+ || parsed.type === "message.interrupt" || parsed.type === "run.stop" || parsed.type === "message.revert";
264
391
  // ...WHICH MEANS A SEND CAN OVERTAKE A REVERT SENT JUST BEFORE IT, because the revert below is
265
392
  // queued behind whatever this socket was already doing. The service only learns it is cutting
266
393
  // when the command EXECUTES, so a send handled in between would find the conversation open and
@@ -275,14 +402,11 @@ export class WsServer {
275
402
  if (command.type === "terminal.open")
276
403
  terminalSessionIds.add(command.sessionId);
277
404
  const entityId = await this.commandHandler.handleCommand(command, (event) => {
278
- if (socket.readyState === WebSocket.OPEN) {
279
- socket.send(JSON.stringify(event));
280
- }
405
+ safeSend(socket, JSON.stringify(event));
281
406
  }, remoteAddress);
282
407
  // Void-returning configuration commands also need an ack to release optimistic UI state.
283
- if (command.operationRequestId && socket.readyState === WebSocket.OPEN) {
284
- socket.send(JSON.stringify({ type: "operation.result", requestId: command.operationRequestId, ok: true, entityId }));
285
- }
408
+ if (command.operationRequestId)
409
+ safeSend(socket, JSON.stringify({ type: "operation.result", requestId: command.operationRequestId, ok: true, entityId }));
286
410
  }
287
411
  catch (error) {
288
412
  // A FAILED COMMAND WRITES ITSELF DOWN BEFORE IT ANSWERS.
@@ -294,10 +418,10 @@ export class WsServer {
294
418
  console.error("Super Pi command error:", describeThrownWithStack(error));
295
419
  const reason = describeThrown(error);
296
420
  if (command?.operationRequestId) {
297
- socket.send(JSON.stringify({ type: "operation.result", requestId: command.operationRequestId, ok: false, message: reason }));
421
+ safeSend(socket, JSON.stringify({ type: "operation.result", requestId: command.operationRequestId, ok: false, message: reason }));
298
422
  return;
299
423
  }
300
- socket.send(JSON.stringify({
424
+ safeSend(socket, JSON.stringify({
301
425
  type: "error",
302
426
  message: reason || "服务端没有说明这次失败的原因。",
303
427
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.137",
3
+ "version": "0.1.139",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -46,6 +46,8 @@
46
46
  "scripts/test-runner.test.mjs",
47
47
  "scripts/stream-checkpoint.test.mjs",
48
48
  "scripts/stream-transport.test.mjs",
49
+ "scripts/remote-transport.test.mjs",
50
+ "scripts/browser-safe-port.test.mjs",
49
51
  "scripts/inline-thinking.test.mjs",
50
52
  "scripts/inline-thinking-close-tag.test.mjs",
51
53
  "scripts/pi-session.test.mjs",
@@ -97,6 +99,13 @@
97
99
  "scripts/background-result-one-per-turn.test.mjs",
98
100
  "scripts/thinking-status-promotion.test.mjs",
99
101
  "scripts/thinking-delivery-once.test.mjs",
102
+ "scripts/thinking-tool-order.test.mjs",
103
+ "scripts/question-behind-body.test.mjs",
104
+ "scripts/workspace-default-model-rules.test.mjs",
105
+ "scripts/tool-call-persist-count.test.mjs",
106
+ "scripts/conversation-service-narrow-reads.test.mjs",
107
+ "scripts/revert-evicts-live-session.test.mjs",
108
+ "scripts/cdp-retry-bounded.test.mjs",
100
109
  "scripts/question-call-session-path.test.mjs",
101
110
  "scripts/withdrawn-message-persistence.test.mjs",
102
111
  "scripts/git-write-endpoint-guard.test.mjs",
@@ -116,6 +125,7 @@
116
125
  "scripts/files-missing-path.test.mjs",
117
126
  "scripts/file-changes.test.mjs",
118
127
  "scripts/file-index.test.mjs",
128
+ "scripts/file-index-write-refresh.test.mjs",
119
129
  "scripts/file-name-index.test.mjs",
120
130
  "scripts/file-content-search.test.mjs",
121
131
  "scripts/file-diff-client.test.mjs",
@@ -188,6 +198,7 @@
188
198
  "scripts/visual-defects-browser.test.mjs",
189
199
  "scripts/session-state-animation-browser.test.mjs",
190
200
  "scripts/session-state-delivery-browser.test.mjs",
201
+ "scripts/transcript-render-scaling-browser.test.mjs",
191
202
  "scripts/question-awaiting-signal-browser.test.mjs",
192
203
  "scripts/withdraw-then-send-browser.test.mjs",
193
204
  "scripts/agent-vis-card-browser.test.mjs",
@@ -198,15 +209,18 @@
198
209
  "scripts/rail-workspace-follows-session-browser.test.mjs",
199
210
  "scripts/git-tab-browser.test.mjs",
200
211
  "scripts/workspace-reopen-browser.test.mjs",
212
+ "scripts/workspace-identity-browser.test.mjs",
201
213
  "scripts/sidebar-tools-browser.test.mjs",
202
214
  "scripts/tool-runtime-browser.test.mjs",
203
215
  "scripts/session-context-menu-browser.test.mjs",
204
216
  "scripts/tool-call-blocks-browser.test.mjs",
217
+ "scripts/model-notice-browser.test.mjs",
205
218
  "scripts/assistant-working-indicator-browser.test.mjs",
206
219
  "scripts/thinking-first-delta-once-browser.test.mjs",
207
220
  "scripts/files-tab-browser.test.mjs",
208
221
  "scripts/editor-blame-browser.test.mjs",
209
222
  "scripts/file-panel-external-poll-browser.test.mjs",
223
+ "scripts/editor-save-race-browser.test.mjs",
210
224
  "scripts/file-quickopen-browser.test.mjs",
211
225
  "scripts/file-content-search-browser.test.mjs",
212
226
  "scripts/file-changes-browser.test.mjs",