@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.
- package/agents/super-pi-knowledge/index.ts +11 -13
- package/agents/super-pi-memory/index.ts +11 -5
- package/bin/super-pi.js +7 -11
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +6 -6
- package/dist/client/assets/{CodeMirrorEditor-CkA_5D5r.js → CodeMirrorEditor-H7SHGhdS.js} +2 -2
- package/dist/client/assets/{dist-DcdzfM-n.js → dist-2XXLO-IZ.js} +1 -1
- package/dist/client/assets/{dist-ufRHdQ8G.js → dist-BEM6tttY.js} +1 -1
- package/dist/client/assets/{dist-9Fm8XKu4.js → dist-BfUeRJKr.js} +1 -1
- package/dist/client/assets/{dist-CrxmpY93.js → dist-BokzcFLE.js} +1 -1
- package/dist/client/assets/{dist-BfEmujEC.js → dist-BoucVzKA.js} +1 -1
- package/dist/client/assets/{dist-B02Erd0t.js → dist-BxTGjU84.js} +1 -1
- package/dist/client/assets/{dist-DGhdkud4.js → dist-C-MUXK_t.js} +1 -1
- package/dist/client/assets/{dist-k6bOz9RS.js → dist-C8NllalC.js} +1 -1
- package/dist/client/assets/{dist-C2tSd-AY.js → dist-CSdzvQnf.js} +1 -1
- package/dist/client/assets/{dist-BySjislr.js → dist-CUNctZ5v.js} +1 -1
- package/dist/client/assets/{dist-B3Mfuv1D.js → dist-CZ9Krsno.js} +1 -1
- package/dist/client/assets/{dist-DbovsYeH.js → dist-C_ftUQZN.js} +1 -1
- package/dist/client/assets/{dist-j9X7j381.js → dist-CuMnZOGE.js} +1 -1
- package/dist/client/assets/{dist-D-LtG92p.js → dist-CukPKrEn.js} +1 -1
- package/dist/client/assets/{dist-b9CfAgzG.js → dist-D3Ta0y4e.js} +1 -1
- package/dist/client/assets/{dist-BsqzrZ5R.js → dist-DJ8SwQl_.js} +1 -1
- package/dist/client/assets/{dist-BqGjijI6.js → dist-DLKVFZhO.js} +1 -1
- package/dist/client/assets/{dist-MC2XuZgq.js → dist-Jw-GQ-re.js} +1 -1
- package/dist/client/assets/{dist-D_umYm0o.js → dist-NlopeJ5R.js} +1 -1
- package/dist/client/assets/{dist-CsNytPug.js → dist-POoQ4j29.js} +1 -1
- package/dist/client/assets/{dist-DdVs8Y0X.js → dist-cP2vnTP_.js} +1 -1
- package/dist/client/assets/{dist-DL4Q05ts.js → dist-zUJqdbfo.js} +1 -1
- package/dist/client/assets/{index-DVmE8H4Y.js → index-B32VZVYx.js} +13 -13
- package/dist/client/assets/index-Bdym8BPo.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/application/conversation-service.d.ts +13 -4
- package/dist/server/application/conversation-service.js +112 -61
- package/dist/server/application/run-service.d.ts +13 -0
- package/dist/server/application/run-service.js +255 -69
- package/dist/server/application/scheduled-task-service.js +8 -1
- package/dist/server/application/workspace-service.js +16 -1
- package/dist/server/feishu-cli.js +15 -4
- package/dist/server/file-index.d.ts +30 -0
- package/dist/server/file-index.js +49 -4
- package/dist/server/files.d.ts +21 -0
- package/dist/server/files.js +38 -0
- package/dist/server/http-server.js +1455 -1412
- package/dist/server/index.js +3 -4
- package/dist/server/logging.js +56 -3
- package/dist/server/pi-session-pool.d.ts +2 -0
- package/dist/server/pi-session-pool.js +1 -0
- package/dist/server/pi-session.d.ts +118 -0
- package/dist/server/pi-session.js +132 -10
- package/dist/server/runtime.d.ts +47 -0
- package/dist/server/runtime.js +183 -84
- package/dist/server/store.d.ts +44 -1
- package/dist/server/store.js +114 -12
- package/dist/server/subagent-runs.js +18 -7
- package/dist/server/subagents.js +7 -0
- package/dist/server/tool-runner.js +13 -4
- package/dist/server/types.d.ts +27 -0
- package/dist/server/ws-server.d.ts +34 -5
- package/dist/server/ws-server.js +170 -46
- package/package.json +15 -1
- package/scripts/diagnose.mjs +161 -58
- package/dist/client/assets/index-FxTrUapG.css +0 -1
package/dist/server/ws-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
+
this.broadcaster.sendState(socket);
|
|
188
299
|
if (this.rootDir) {
|
|
189
|
-
socket
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
284
|
-
socket
|
|
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
|
|
421
|
+
safeSend(socket, JSON.stringify({ type: "operation.result", requestId: command.operationRequestId, ok: false, message: reason }));
|
|
298
422
|
return;
|
|
299
423
|
}
|
|
300
|
-
socket
|
|
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.
|
|
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",
|