@evandu/super-pi 0.1.128 → 0.1.130
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 +4 -3
- package/agents/super-pi-memory/index.ts +5 -4
- package/agents/super-pi-task/core.ts +8 -7
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +5 -5
- package/dist/client/assets/{CodeMirrorEditor-BhxCTVTL.js → CodeMirrorEditor-BNVBZZSS.js} +2 -2
- package/dist/client/assets/{dist-DERv1ty2.js → dist-B2nPVdKh.js} +1 -1
- package/dist/client/assets/{dist-BAmT9jUI.js → dist-B61RSa-K.js} +1 -1
- package/dist/client/assets/{dist-BPBlgusB.js → dist-BBW2SE_R.js} +1 -1
- package/dist/client/assets/{dist-BQK4xccN.js → dist-BDygQgZD.js} +1 -1
- package/dist/client/assets/{dist-C-hIztMW.js → dist-BKp4AWEJ.js} +1 -1
- package/dist/client/assets/{dist-Cl87QYqE.js → dist-BhJGK8-b.js} +1 -1
- package/dist/client/assets/{dist-Bplw4N5j.js → dist-BykVFExd.js} +1 -1
- package/dist/client/assets/{dist-K96Y1Tut.js → dist-C90MPTnk.js} +1 -1
- package/dist/client/assets/{dist-DVBpWkDV.js → dist-CMnvChDX.js} +1 -1
- package/dist/client/assets/{dist-BfwwrK33.js → dist-CRzRB9k3.js} +1 -1
- package/dist/client/assets/{dist-BeYwU1xB.js → dist-CWYRjnj-.js} +1 -1
- package/dist/client/assets/{dist-AtHUPHQe.js → dist-CZ9I2JCd.js} +1 -1
- package/dist/client/assets/{dist-8-xcbQPp.js → dist-ChszWGDv.js} +1 -1
- package/dist/client/assets/{dist-Bof_dYLf.js → dist-CiSGSVBZ.js} +1 -1
- package/dist/client/assets/{dist-Dc6xSTJL.js → dist-Cxerc7Bb.js} +1 -1
- package/dist/client/assets/{dist-DobX-p1U.js → dist-DB5M5fZG.js} +1 -1
- package/dist/client/assets/{dist-YUFYdtGJ.js → dist-DZnUERoW.js} +1 -1
- package/dist/client/assets/{dist-BJZUzzzf.js → dist-DmAOqr0M.js} +1 -1
- package/dist/client/assets/{dist-Bw8QOdGP.js → dist-Dq4-QFAJ.js} +1 -1
- package/dist/client/assets/{dist-icIMVISS.js → dist-GkIV1LTX.js} +1 -1
- package/dist/client/assets/{dist-DTXHzd8a.js → dist-HdeIBl2J.js} +1 -1
- package/dist/client/assets/{dist-5LsaarOt.js → dist-Ur37340K.js} +1 -1
- package/dist/client/assets/index-CXB6U5mz.css +1 -0
- package/dist/client/assets/{index-BvQ3umhR.js → index-D_c-CYAr.js} +35 -37
- package/dist/client/index.html +2 -2
- package/dist/server/active-memory.d.ts +20 -3
- package/dist/server/active-memory.js +69 -2
- package/dist/server/application/conversation-service.js +5 -0
- package/dist/server/application/run-service.js +10 -1
- package/dist/server/git.d.ts +87 -0
- package/dist/server/git.js +301 -2
- package/dist/server/http-server.js +39 -7
- package/dist/server/pi-session.d.ts +25 -6
- package/dist/server/pi-session.js +33 -3
- package/dist/server/runtime.d.ts +12 -0
- package/dist/server/runtime.js +6 -0
- package/dist/server/store.js +16 -5
- package/dist/server/ws-server.js +3 -0
- package/package.json +4 -1
- package/scripts/diagnose.mjs +8 -3
- package/dist/client/assets/index-B8GInEf1.css +0 -1
|
@@ -7,7 +7,7 @@ import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
|
7
7
|
import { describe as describeFilesError, fileMeta, listDirectory, MAX_FILE_BYTES, readWorkspaceFile, resolveInsideRoot, toRelative, writeWorkspaceFile, } from "./files.js";
|
|
8
8
|
import { findUploadFile, MAX_UPLOAD_BODY_BYTES, saveUpload } from "./upload.js";
|
|
9
9
|
import { checkDiskVersion } from "./version.js";
|
|
10
|
-
import { gitDiff, gitLog, gitStatus } from "./git.js";
|
|
10
|
+
import { gitBlame, gitDiff, gitLog, gitMutate, gitStatus } from "./git.js";
|
|
11
11
|
import { computeFileDiff } from "./file-diff.js";
|
|
12
12
|
import { MAX_KNOWLEDGE_REQUEST_BYTES } from "./knowledge.js";
|
|
13
13
|
import { MAX_TOOL_BODY_BYTES } from "./tool-registry.js";
|
|
@@ -797,10 +797,6 @@ export function createHttpServer(options) {
|
|
|
797
797
|
if (url.pathname === "/api/git") {
|
|
798
798
|
const json = (status, body) => response.writeHead(status, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify(body));
|
|
799
799
|
try {
|
|
800
|
-
if (request.method !== "GET") {
|
|
801
|
-
json(405, { error: "Git 视图需要 GET", code: "method" });
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
800
|
const workspaceId = url.searchParams.get("workspaceId");
|
|
805
801
|
const workspace = context.store.snapshot().workspaces.find((item) => item.id === workspaceId);
|
|
806
802
|
if (!workspace) {
|
|
@@ -808,6 +804,24 @@ export function createHttpServer(options) {
|
|
|
808
804
|
return;
|
|
809
805
|
}
|
|
810
806
|
const op = url.searchParams.get("op") ?? "status";
|
|
807
|
+
// Blame is a read with a GET shape: every other verb is refused HERE, before the POST branch
|
|
808
|
+
// could ever route the op into gitMutate and before any body is read.
|
|
809
|
+
if (op === "blame" && request.method !== "GET") {
|
|
810
|
+
json(405, { error: "Git 请求方法不支持", code: "method" });
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
if (request.method === "POST") {
|
|
814
|
+
let raw = "";
|
|
815
|
+
for await (const chunk of request)
|
|
816
|
+
raw += chunk;
|
|
817
|
+
const body = raw ? JSON.parse(raw) : {};
|
|
818
|
+
json(200, await gitMutate(workspace.path, op, body.path, body.message));
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (request.method !== "GET") {
|
|
822
|
+
json(405, { error: "Git 请求方法不支持", code: "method" });
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
811
825
|
if (op === "status")
|
|
812
826
|
json(200, await gitStatus(workspace.path));
|
|
813
827
|
else if (op === "log")
|
|
@@ -820,6 +834,20 @@ export function createHttpServer(options) {
|
|
|
820
834
|
}
|
|
821
835
|
json(200, await gitDiff(workspace.path, path, url.searchParams.get("staged") === "1"));
|
|
822
836
|
}
|
|
837
|
+
else if (op === "blame") {
|
|
838
|
+
const path = url.searchParams.get("path");
|
|
839
|
+
if (!path) {
|
|
840
|
+
json(400, { error: "blame 需要 path", code: "bad_path" });
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const startLine = url.searchParams.get("startLine");
|
|
844
|
+
const endLine = url.searchParams.get("endLine");
|
|
845
|
+
json(200, await gitBlame(workspace.path, path, {
|
|
846
|
+
revision: url.searchParams.get("revision") ?? undefined,
|
|
847
|
+
startLine: startLine === null ? undefined : Number(startLine),
|
|
848
|
+
endLine: endLine === null ? undefined : Number(endLine),
|
|
849
|
+
}));
|
|
850
|
+
}
|
|
823
851
|
else
|
|
824
852
|
json(400, { error: "未知 Git 操作", code: "bad_op" });
|
|
825
853
|
}
|
|
@@ -830,6 +858,10 @@ export function createHttpServer(options) {
|
|
|
830
858
|
json(404, { error: "此工作区不是 git 仓库", code: "not_a_repo" });
|
|
831
859
|
else if (code === "path")
|
|
832
860
|
json(400, { error: message, code: "bad_path" });
|
|
861
|
+
else if (code === "not_found")
|
|
862
|
+
json(404, { error: message, code: "not_found" });
|
|
863
|
+
else if (code === "bad_range" || code === "bad_revision")
|
|
864
|
+
json(400, { error: message, code });
|
|
833
865
|
else
|
|
834
866
|
json(400, { error: message, code: "git_error" });
|
|
835
867
|
}
|
|
@@ -900,7 +932,7 @@ export function createHttpServer(options) {
|
|
|
900
932
|
json(400, { error: "请求体不是合法的 JSON。" });
|
|
901
933
|
return;
|
|
902
934
|
}
|
|
903
|
-
const { cmd, cwd, conversationId, capability, toolCallId } = body;
|
|
935
|
+
const { cmd, description, cwd, conversationId, capability, toolCallId } = body;
|
|
904
936
|
if (!cmd || !cwd || !conversationId) {
|
|
905
937
|
json(400, { error: "需要 cmd、cwd 和 conversationId。" });
|
|
906
938
|
return;
|
|
@@ -929,7 +961,7 @@ export function createHttpServer(options) {
|
|
|
929
961
|
await store.setBackgroundTask({
|
|
930
962
|
id: sessionId,
|
|
931
963
|
conversationId,
|
|
932
|
-
title: cmd.length > 60 ? cmd.slice(0, 60) + "…" : cmd,
|
|
964
|
+
title: description?.trim() || (cmd.length > 60 ? cmd.slice(0, 60) + "…" : cmd),
|
|
933
965
|
agentId: "exec",
|
|
934
966
|
status: "running",
|
|
935
967
|
startedAt: new Date().toISOString(),
|
|
@@ -184,6 +184,13 @@ export interface ExtensionEnvironment {
|
|
|
184
184
|
* concurrent session's environment would otherwise land in the middle of this one.
|
|
185
185
|
*/
|
|
186
186
|
export declare function withEnvironment<T>(values: Record<string, string>, work: () => Promise<T>): Promise<T>;
|
|
187
|
+
/**
|
|
188
|
+
* `withEnvironment` for one session's extension load, serialized across all session creations.
|
|
189
|
+
*
|
|
190
|
+
* The extension factories run inside `work` and capture the values into their closures; after the
|
|
191
|
+
* window closes they never read `process.env` again, so only the WINDOW needs to be exclusive.
|
|
192
|
+
*/
|
|
193
|
+
export declare function withIsolatedSessionEnv<T>(values: Record<string, string>, work: () => Promise<T>): Promise<T>;
|
|
187
194
|
/**
|
|
188
195
|
* The live session for one conversation.
|
|
189
196
|
*
|
|
@@ -210,6 +217,15 @@ export interface PiSessionLike {
|
|
|
210
217
|
* message for later", and the caller falls back to answering it as a turn of its own.
|
|
211
218
|
*/
|
|
212
219
|
followUp?(message: string, images?: unknown[]): Promise<"queued" | "handled">;
|
|
220
|
+
sendCustomMessage?(message: {
|
|
221
|
+
customType: string;
|
|
222
|
+
content: unknown;
|
|
223
|
+
display?: string;
|
|
224
|
+
details?: unknown;
|
|
225
|
+
}, options?: {
|
|
226
|
+
triggerTurn?: boolean;
|
|
227
|
+
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
228
|
+
}): Promise<void>;
|
|
213
229
|
/**
|
|
214
230
|
* Removes every message Pi is holding in its steering and follow-up queues, and returns what was
|
|
215
231
|
* removed.
|
|
@@ -354,14 +370,17 @@ export interface SessionCreation {
|
|
|
354
370
|
*
|
|
355
371
|
* The order of operations here IS the isolation mechanism, and each step is load-bearing:
|
|
356
372
|
*
|
|
357
|
-
* 1. The
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
373
|
+
* 1. The loader is created with Super Pi's extension paths, for PI to load - through its jiti
|
|
374
|
+
* transpiler and virtual modules, the same machinery the spawned CLI used. Nothing here imports a
|
|
375
|
+
* Super Pi extension file directly; see the comment at the loader below for why that must never
|
|
376
|
+
* come back.
|
|
377
|
+
* 2. The reload runs inside this session's env window (`withIsolatedSessionEnv`), which is when Pi
|
|
378
|
+
* calls the extension factories. The factories CAPTURE the values; they do not read them later.
|
|
361
379
|
* 3. The session is created with that loader.
|
|
362
380
|
*
|
|
363
|
-
* Step 2 happening outside
|
|
364
|
-
*
|
|
381
|
+
* Step 2 happening outside its env window would give every session the same configuration, and the
|
|
382
|
+
* window would give every session no configuration at all if two creations could interleave - which
|
|
383
|
+
* the serialization in `withIsolatedSessionEnv` prevents.
|
|
365
384
|
*
|
|
366
385
|
* THE FACTORY IS INJECTABLE. A test needs to drive a session without a real Pi behind it - to check what
|
|
367
386
|
* a session was BUILT with (its working directory, its allowlist) rather than what it eventually
|
|
@@ -225,13 +225,40 @@ export async function withEnvironment(values, work) {
|
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* The env window of the session currently being built, so the next one queues behind it.
|
|
230
|
+
*
|
|
231
|
+
* `withEnvironment` alone is not safe here: its window is process-global, and two sessions created
|
|
232
|
+
* concurrently would interleave - A applies its capability, B applies its own, and A's extension
|
|
233
|
+
* factory captures B's credential. Chaining the windows means a concurrent creation waits for the
|
|
234
|
+
* other's `reload()` - milliseconds - instead of silently wearing its capability.
|
|
235
|
+
*/
|
|
236
|
+
let extensionEnvWindow = Promise.resolve();
|
|
237
|
+
/**
|
|
238
|
+
* `withEnvironment` for one session's extension load, serialized across all session creations.
|
|
239
|
+
*
|
|
240
|
+
* The extension factories run inside `work` and capture the values into their closures; after the
|
|
241
|
+
* window closes they never read `process.env` again, so only the WINDOW needs to be exclusive.
|
|
242
|
+
*/
|
|
243
|
+
export function withIsolatedSessionEnv(values, work) {
|
|
244
|
+
const run = extensionEnvWindow.then(() => withEnvironment(values, work));
|
|
245
|
+
extensionEnvWindow = run.then(() => undefined, () => undefined);
|
|
246
|
+
return run;
|
|
247
|
+
}
|
|
228
248
|
export async function createPiSession(creation) {
|
|
229
249
|
const startedAt = performance.now();
|
|
230
250
|
traceSession(creation.sessionId, "sdk.load.begin", startedAt);
|
|
231
251
|
const sdk = await loadSdk();
|
|
232
252
|
traceSession(creation.sessionId, "sdk.load.end", startedAt);
|
|
233
253
|
const agentDir = creation.agentDir ?? defaultAgentDir();
|
|
234
|
-
|
|
254
|
+
// THE EXTENSIONS ARE PI'S TO LOAD. Every path goes to Pi's own resource loader, which imports them
|
|
255
|
+
// through its jiti transpiler with its virtual modules - and both of those are load-bearing:
|
|
256
|
+
// `@sinclair/typebox` is a Pi virtual module that NO node_modules anywhere can resolve, and Node's
|
|
257
|
+
// type stripping refuses .ts under any `node_modules` segment (which is where a published package
|
|
258
|
+
// lives). A native `import()` here survived neither - it broke every installed layout the moment it
|
|
259
|
+
// shipped, invisibly, because nothing in `npm run check` runs from an installed layout.
|
|
260
|
+
// `scripts/installed-layout.test.mjs` exists to keep that class of breakage visible.
|
|
261
|
+
return (async () => {
|
|
235
262
|
// THE AGENT'S PROMPT. Pi's loader takes the same two shapes the command line took: a `systemPrompt`
|
|
236
263
|
// that REPLACES Pi's own, and an `appendSystemPrompt` list of files that are ADDED to it. An agent
|
|
237
264
|
// that says `promptMode: replace` gets the first, and one that says nothing gets the second - the
|
|
@@ -248,7 +275,10 @@ export async function createPiSession(creation) {
|
|
|
248
275
|
? { appendSystemPrompt: [creation.systemPromptFile] }
|
|
249
276
|
: {}),
|
|
250
277
|
});
|
|
251
|
-
|
|
278
|
+
// The extension factories run inside `reload()` and capture this session's env values into their
|
|
279
|
+
// closures; the window is serialized (`withIsolatedSessionEnv`) so a concurrent creation cannot
|
|
280
|
+
// land its own capability in the middle.
|
|
281
|
+
await withIsolatedSessionEnv(creation.extensionEnv, () => loader.reload());
|
|
252
282
|
traceSession(creation.sessionId, "sdk.loader.reload", startedAt, `extensions=${creation.extensionPaths.length}`);
|
|
253
283
|
const sessionManager = creation.persist === false
|
|
254
284
|
? sdk.SessionManager.inMemory(creation.cwd, { id: creation.sessionId })
|
|
@@ -298,7 +328,7 @@ export async function createPiSession(creation) {
|
|
|
298
328
|
return found;
|
|
299
329
|
};
|
|
300
330
|
return live;
|
|
301
|
-
});
|
|
331
|
+
})();
|
|
302
332
|
}
|
|
303
333
|
function transcriptBytes(sessionManager) {
|
|
304
334
|
const file = sessionManager.getSessionFile?.();
|
package/dist/server/runtime.d.ts
CHANGED
|
@@ -272,6 +272,12 @@ export interface AgentRuntime {
|
|
|
272
272
|
* own waiting list - Pi is holding a copy, not the only copy.
|
|
273
273
|
*/
|
|
274
274
|
followUp?(conversationId: string, message: string, images?: MessageImage[]): Promise<SteerDisposition>;
|
|
275
|
+
sendCustomMessage?(conversationId: string, message: {
|
|
276
|
+
customType: string;
|
|
277
|
+
content: string;
|
|
278
|
+
display?: string;
|
|
279
|
+
details: Record<string, unknown>;
|
|
280
|
+
}): Promise<void>;
|
|
275
281
|
/**
|
|
276
282
|
* Empties Pi's steering and follow-up queues, and returns what was in them.
|
|
277
283
|
*
|
|
@@ -1067,6 +1073,12 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
1067
1073
|
* was busy means).
|
|
1068
1074
|
*/
|
|
1069
1075
|
followUp(conversationId: string, message: string, images?: MessageImage[]): Promise<SteerDisposition>;
|
|
1076
|
+
sendCustomMessage(conversationId: string, message: {
|
|
1077
|
+
customType: string;
|
|
1078
|
+
content: string;
|
|
1079
|
+
display?: string;
|
|
1080
|
+
details: Record<string, unknown>;
|
|
1081
|
+
}): Promise<void>;
|
|
1070
1082
|
/**
|
|
1071
1083
|
* Empties Pi's steering and follow-up queues and returns what was in them.
|
|
1072
1084
|
*
|
package/dist/server/runtime.js
CHANGED
|
@@ -2184,6 +2184,12 @@ export class PiRpcRuntime {
|
|
|
2184
2184
|
// do anyway.
|
|
2185
2185
|
throw new SteerUnavailableError(`会话 ${conversationId} 当前没有可以接手的 Pi 会话,无法排队`);
|
|
2186
2186
|
}
|
|
2187
|
+
async sendCustomMessage(conversationId, message) {
|
|
2188
|
+
const session = await this.sessionFor(conversationId);
|
|
2189
|
+
if (!session.sendCustomMessage)
|
|
2190
|
+
throw new SteerUnavailableError(`会话 ${conversationId} 的 Pi 不支持 custom message`);
|
|
2191
|
+
await session.sendCustomMessage(message, { triggerTurn: true, deliverAs: "followUp" });
|
|
2192
|
+
}
|
|
2187
2193
|
/**
|
|
2188
2194
|
* Empties Pi's steering and follow-up queues and returns what was in them.
|
|
2189
2195
|
*
|
package/dist/server/store.js
CHANGED
|
@@ -636,13 +636,17 @@ export class Store {
|
|
|
636
636
|
console.warn(`Super Pi: state file ${repository.filePath} could not be loaded (${reason}) and could not be moved aside either (${moveError instanceof Error ? moveError.message : String(moveError)}) - the server starts with a fresh state; the unreadable file is still at ${repository.filePath}.`);
|
|
637
637
|
}
|
|
638
638
|
}
|
|
639
|
-
const createdAt = now();
|
|
640
639
|
// NO WRITE HERE, AND THAT IS AN INVARIANT RATHER THAN AN OMISSION: a store with no file behind it
|
|
641
640
|
// stays lazy, so opening one never creates a file and never has to un-create one. `persistence-
|
|
642
641
|
// repository.test.mjs` asserts it by reading the path and expecting ENOENT. Repairing an EXISTING
|
|
643
642
|
// file is the other case and is handled above; this branch invented its contents and says nothing
|
|
644
643
|
// until something actually happens.
|
|
645
|
-
|
|
644
|
+
//
|
|
645
|
+
// A fresh state starts with NO workspace, and that is a constraint rather than a gap: this branch
|
|
646
|
+
// used to seed one named after process.cwd(), so whatever directory the server happened to be
|
|
647
|
+
// launched from silently became a workspace. The first workspace is always an explicit decision -
|
|
648
|
+
// the client's zero-workspace empty state (App.tsx) asks for one instead.
|
|
649
|
+
return new Store(repository, { workspaces: [], conversations: [], subagentRuns: [], backgroundTasks: [] });
|
|
646
650
|
}
|
|
647
651
|
}
|
|
648
652
|
async setModelFilterRules(rules) {
|
|
@@ -697,7 +701,13 @@ export class Store {
|
|
|
697
701
|
throw error;
|
|
698
702
|
}
|
|
699
703
|
}
|
|
700
|
-
snapshot() {
|
|
704
|
+
snapshot() {
|
|
705
|
+
const startedAt = process.env.SUPER_PI_TRACE_LATENCY === "1" ? performance.now() : 0;
|
|
706
|
+
const value = structuredClone(this.state);
|
|
707
|
+
if (startedAt)
|
|
708
|
+
console.warn(`[super-pi latency] phase=state.snapshot bytes=${JSON.stringify(value).length} cloneMs=${Math.round(performance.now() - startedAt)}`);
|
|
709
|
+
return value;
|
|
710
|
+
}
|
|
701
711
|
/**
|
|
702
712
|
* Whether a conversation already holds a message with this id.
|
|
703
713
|
*
|
|
@@ -796,8 +806,9 @@ export class Store {
|
|
|
796
806
|
async deleteWorkspace(workspaceId) {
|
|
797
807
|
if (!this.state.workspaces.some((item) => item.id === workspaceId))
|
|
798
808
|
throw new Error("找不到这个工作区。");
|
|
799
|
-
|
|
800
|
-
|
|
809
|
+
// Deleting the LAST workspace is allowed: a fresh store starts with zero (see `openRepository`), so
|
|
810
|
+
// "at least one" was an artifact of the old first-run seed, and the client's zero-workspace empty
|
|
811
|
+
// state is the onboarding path back in.
|
|
801
812
|
// Cascade: workspaces and conversations live in one state.json, so this single atomic persist removes
|
|
802
813
|
// both together. A waiting message needs no sweep of its own any more - it is a message inside one of
|
|
803
814
|
// the conversations being removed, so it goes with its container, which is the whole reason it is a
|
package/dist/server/ws-server.js
CHANGED
|
@@ -73,7 +73,10 @@ export class WsBroadcaster {
|
|
|
73
73
|
* Stream deltas stay per-event: they are coalesced per socket, so there is no single payload to
|
|
74
74
|
* share, and their cost is one small string.
|
|
75
75
|
*/
|
|
76
|
+
const startedAt = performance.now();
|
|
76
77
|
const payload = streaming ? undefined : JSON.stringify(event);
|
|
78
|
+
if (!streaming && event.type === "state" && process.env.SUPER_PI_TRACE_LATENCY === "1")
|
|
79
|
+
console.warn(`[super-pi latency] phase=state.broadcast bytes=${payload?.length ?? 0} sockets=${this.sockets.size} serializeMs=${Math.round(performance.now() - startedAt)}`);
|
|
77
80
|
for (const socket of this.sockets) {
|
|
78
81
|
if (socket.readyState !== WebSocket.OPEN)
|
|
79
82
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evandu/super-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.130",
|
|
4
4
|
"description": "Web control plane and companion for Pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
"scripts/stream-checkpoint.test.mjs",
|
|
44
44
|
"scripts/stream-transport.test.mjs",
|
|
45
45
|
"scripts/pi-session.test.mjs",
|
|
46
|
+
"scripts/pi-session-isolation.test.mjs",
|
|
47
|
+
"scripts/installed-layout.test.mjs",
|
|
46
48
|
"scripts/pi-model-switch.test.mjs",
|
|
47
49
|
"scripts/pi-thinking-levels.test.mjs",
|
|
48
50
|
"scripts/diagnose-cli.test.mjs",
|
|
@@ -101,6 +103,7 @@
|
|
|
101
103
|
"scripts/send-retry.test.mjs",
|
|
102
104
|
"scripts/error-messages.test.mjs",
|
|
103
105
|
"scripts/git-diff-view.test.mjs",
|
|
106
|
+
"scripts/git-blame.test.mjs",
|
|
104
107
|
"scripts/fork-server.test.mjs",
|
|
105
108
|
"scripts/styles-no-dangling.test.mjs",
|
|
106
109
|
"scripts/theme-no-hardcoded-colours.test.mjs",
|
package/scripts/diagnose.mjs
CHANGED
|
@@ -229,8 +229,13 @@ export async function diagnose({ repositoryRoot, turns = 2, onProgress = () => {
|
|
|
229
229
|
throw new Error(`timed out waiting for ${label}`);
|
|
230
230
|
};
|
|
231
231
|
|
|
232
|
+
// A fresh store starts with no workspace, so the diagnosis creates the one it measures against -
|
|
233
|
+
// before any measured span, so workspace setup never lands inside a number.
|
|
234
|
+
send({ type: "workspace.create", name: "diagnose", path: process.cwd() });
|
|
235
|
+
const workspaceId = (await waitFor((e) => e.type === "state" && e.state.workspaces.some((w) => w.name === "diagnose"), "the diagnose workspace")).state.workspaces.find((w) => w.name === "diagnose").id;
|
|
236
|
+
|
|
232
237
|
onProgress("creating a conversation...");
|
|
233
|
-
send({ type: "conversation.create", workspaceId
|
|
238
|
+
send({ type: "conversation.create", workspaceId });
|
|
234
239
|
await waitFor((e) => e.type === "state" && e.state.conversations.length > 0, "a conversation");
|
|
235
240
|
const conversation = events.filter((e) => e.type === "state").at(-1).state.conversations[0];
|
|
236
241
|
|
|
@@ -258,13 +263,13 @@ export async function diagnose({ repositoryRoot, turns = 2, onProgress = () => {
|
|
|
258
263
|
onProgress("setting a workspace default model, then creating a conversation...");
|
|
259
264
|
send({
|
|
260
265
|
type: "workspace.settings",
|
|
261
|
-
workspaceId
|
|
266
|
+
workspaceId,
|
|
262
267
|
model: { provider: defaultModel.provider, modelId: defaultModel.modelId },
|
|
263
268
|
thinkingLevel: null,
|
|
264
269
|
});
|
|
265
270
|
await sleep(400);
|
|
266
271
|
const withDefaultStart = Date.now();
|
|
267
|
-
send({ type: "conversation.create", workspaceId
|
|
272
|
+
send({ type: "conversation.create", workspaceId });
|
|
268
273
|
const withDefault = await waitFor((e) => e.type === "state"
|
|
269
274
|
&& e.state.conversations.length >= 2, "a conversation with the workspace default");
|
|
270
275
|
result.conversationWithDefaultMs = Date.now() - withDefaultStart;
|