@evandu/super-pi 0.1.127 → 0.1.129

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 (46) hide show
  1. package/agents/super-pi-knowledge/index.ts +4 -3
  2. package/agents/super-pi-memory/index.ts +5 -4
  3. package/agents/super-pi-task/core.ts +8 -7
  4. package/dist/.super-pi-check-inputs.json +2 -2
  5. package/dist/client/.super-pi-client-sources.json +5 -5
  6. package/dist/client/assets/{CodeMirrorEditor-DpbDVB1W.js → CodeMirrorEditor-BNVBZZSS.js} +2 -2
  7. package/dist/client/assets/{dist-C30Mvvko.js → dist-B2nPVdKh.js} +1 -1
  8. package/dist/client/assets/{dist-3JbohZwT.js → dist-B61RSa-K.js} +1 -1
  9. package/dist/client/assets/{dist-Dx5uAXkp.js → dist-BBW2SE_R.js} +1 -1
  10. package/dist/client/assets/{dist-CbkhMDE7.js → dist-BDygQgZD.js} +1 -1
  11. package/dist/client/assets/{dist-DhVx85Hi.js → dist-BKp4AWEJ.js} +1 -1
  12. package/dist/client/assets/{dist-zedNmYPO.js → dist-BhJGK8-b.js} +1 -1
  13. package/dist/client/assets/{dist-NOyudbxD.js → dist-BykVFExd.js} +1 -1
  14. package/dist/client/assets/{dist-CBqTq6Rd.js → dist-C90MPTnk.js} +1 -1
  15. package/dist/client/assets/{dist-kPkvm0f9.js → dist-CMnvChDX.js} +1 -1
  16. package/dist/client/assets/{dist-CZ_MzICk.js → dist-CRzRB9k3.js} +1 -1
  17. package/dist/client/assets/{dist-XSLGDk_-.js → dist-CWYRjnj-.js} +1 -1
  18. package/dist/client/assets/{dist-DkWOTrCe.js → dist-CZ9I2JCd.js} +1 -1
  19. package/dist/client/assets/{dist-ik9WKwIy.js → dist-ChszWGDv.js} +1 -1
  20. package/dist/client/assets/{dist-DL2rVK6z.js → dist-CiSGSVBZ.js} +1 -1
  21. package/dist/client/assets/{dist-CKY9oRoB.js → dist-Cxerc7Bb.js} +1 -1
  22. package/dist/client/assets/{dist-j9nf5NbD.js → dist-DB5M5fZG.js} +1 -1
  23. package/dist/client/assets/{dist-Cv9yhljh.js → dist-DZnUERoW.js} +1 -1
  24. package/dist/client/assets/{dist-NukDmwGc.js → dist-DmAOqr0M.js} +1 -1
  25. package/dist/client/assets/{dist-BTxdkV1I.js → dist-Dq4-QFAJ.js} +1 -1
  26. package/dist/client/assets/{dist-qTKaDyIl.js → dist-GkIV1LTX.js} +1 -1
  27. package/dist/client/assets/{dist-H1xOj1sR.js → dist-HdeIBl2J.js} +1 -1
  28. package/dist/client/assets/{dist-Uvt7BHub.js → dist-Ur37340K.js} +1 -1
  29. package/dist/client/assets/index-CXB6U5mz.css +1 -0
  30. package/dist/client/assets/{index-DvxaTOG8.js → index-D_c-CYAr.js} +35 -37
  31. package/dist/client/index.html +2 -2
  32. package/dist/server/active-memory.d.ts +8 -3
  33. package/dist/server/active-memory.js +44 -1
  34. package/dist/server/application/run-service.js +10 -1
  35. package/dist/server/git.d.ts +87 -0
  36. package/dist/server/git.js +301 -2
  37. package/dist/server/http-server.js +39 -7
  38. package/dist/server/pi-session.d.ts +9 -0
  39. package/dist/server/pi-session.js +12 -3
  40. package/dist/server/runtime.d.ts +12 -0
  41. package/dist/server/runtime.js +9 -19
  42. package/dist/server/store.js +16 -5
  43. package/dist/server/ws-server.js +6 -1
  44. package/package.json +2 -1
  45. package/scripts/diagnose.mjs +8 -3
  46. package/dist/client/assets/index-BAaymrG2.css +0 -1
@@ -210,6 +210,15 @@ export interface PiSessionLike {
210
210
  * message for later", and the caller falls back to answering it as a turn of its own.
211
211
  */
212
212
  followUp?(message: string, images?: unknown[]): Promise<"queued" | "handled">;
213
+ sendCustomMessage?(message: {
214
+ customType: string;
215
+ content: unknown;
216
+ display?: string;
217
+ details?: unknown;
218
+ }, options?: {
219
+ triggerTurn?: boolean;
220
+ deliverAs?: "steer" | "followUp" | "nextTurn";
221
+ }): Promise<void>;
213
222
  /**
214
223
  * Removes every message Pi is holding in its steering and follow-up queues, and returns what was
215
224
  * removed.
@@ -231,7 +231,15 @@ export async function createPiSession(creation) {
231
231
  const sdk = await loadSdk();
232
232
  traceSession(creation.sessionId, "sdk.load.end", startedAt);
233
233
  const agentDir = creation.agentDir ?? defaultAgentDir();
234
- return withEnvironment(creation.extensionEnv, async () => {
234
+ // Extension credentials are per-session capabilities. Do not temporarily mutate process.env here:
235
+ // two concurrent session creations otherwise race and can capture each other's capability.
236
+ const inlinePaths = creation.extensionPaths.filter((path) => /super-pi-(task|memory|knowledge)/.test(path.replaceAll("\\", "/")));
237
+ const externalPaths = creation.extensionPaths.filter((path) => !inlinePaths.includes(path));
238
+ const extensionFactories = await Promise.all(inlinePaths.map(async (path) => {
239
+ const mod = await import(pathToFileURL(path).href);
240
+ return async (pi) => mod.default?.(pi, creation.extensionEnv);
241
+ }));
242
+ return (async () => {
235
243
  // THE AGENT'S PROMPT. Pi's loader takes the same two shapes the command line took: a `systemPrompt`
236
244
  // that REPLACES Pi's own, and an `appendSystemPrompt` list of files that are ADDED to it. An agent
237
245
  // that says `promptMode: replace` gets the first, and one that says nothing gets the second - the
@@ -241,7 +249,8 @@ export async function createPiSession(creation) {
241
249
  const loader = new sdk.DefaultResourceLoader({
242
250
  cwd: creation.cwd,
243
251
  agentDir,
244
- ...(creation.extensionPaths.length ? { additionalExtensionPaths: creation.extensionPaths } : {}),
252
+ ...(externalPaths.length ? { additionalExtensionPaths: externalPaths } : {}),
253
+ ...(extensionFactories.length ? { extensionFactories } : {}),
245
254
  ...(creation.systemPromptFile && creation.systemPromptMode === "replace"
246
255
  ? { systemPrompt: creation.systemPromptFile }
247
256
  : creation.systemPromptFile
@@ -298,7 +307,7 @@ export async function createPiSession(creation) {
298
307
  return found;
299
308
  };
300
309
  return live;
301
- });
310
+ })();
302
311
  }
303
312
  function transcriptBytes(sessionManager) {
304
313
  const file = sessionManager.getSessionFile?.();
@@ -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
  *
@@ -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
  *
@@ -2421,26 +2427,10 @@ export class PiRpcRuntime {
2421
2427
  if (!models.length)
2422
2428
  return unavailable("Pi's SDK reported no usable models");
2423
2429
  const pick = (model) => (model ? { provider: model.provider, modelId: model.modelId, label: model.label } : undefined);
2424
- // The levels belong to the SELECTED model, so they are asked of the model in question rather
2425
- // than of whatever Pi would have defaulted to.
2426
- //
2427
- // A MODEL THIS PI DOES NOT KNOW IS NOT A REASON TO THROW THE WHOLE READ AWAY. The MODEL LIST
2428
- // does not depend on the selection at all - it is the same list for every conversation - so
2429
- // abandoning it because one model's levels are unknown turns "the model this session used was
2430
- // removed" into "the picker is empty", which is the exact opposite of what the person needs
2431
- // at that moment. Measured before this change: removing the model in use produced a catalog
2432
- // with `source: "unavailable"` and ZERO models, and the spawned fallback that produced it had
2433
- // only reached its own `set_model` rejection ("Model not found").
2434
- //
2435
- // The levels then come from Pi's default model, because that is the model a turn on a
2436
- // nonexistent model actually runs on (the session cannot resolve it and falls back), so they
2437
- // describe the model in force rather than a different one.
2430
+ // Unknown levels do not invalidate the SDK model list or imply an external Pi.
2431
+ // Keep the catalog usable without spawning a process or borrowing another model's levels.
2438
2432
  const forModel = selection ?? reader.defaultModel;
2439
- let levels = forModel ? await reader.thinkingLevelsFor(forModel) : undefined;
2440
- if (forModel && !levels && reader.defaultModel) {
2441
- levels = await reader.thinkingLevelsFor(reader.defaultModel);
2442
- }
2443
- const levelNames = levels ?? [];
2433
+ const levelNames = (forModel ? await reader.thinkingLevelsFor(forModel) : undefined) ?? [];
2444
2434
  const value = {
2445
2435
  source: "pi",
2446
2436
  models,
@@ -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
- return new Store(repository, { workspaces: [{ id: "workspace_default", name: "Super Pi", path: process.cwd(), updatedAt: createdAt }], conversations: [], subagentRuns: [], backgroundTasks: [] });
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() { return structuredClone(this.state); }
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
- if (this.state.workspaces.length <= 1)
800
- throw new Error("至少要保留一个工作区,不能把最后一个删掉。");
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
@@ -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;
@@ -255,8 +258,10 @@ export class WsServer {
255
258
  socket.send(JSON.stringify(event));
256
259
  }
257
260
  }, remoteAddress);
258
- if (command.operationRequestId)
261
+ // Void-returning configuration commands also need an ack to release optimistic UI state.
262
+ if (command.operationRequestId && socket.readyState === WebSocket.OPEN) {
259
263
  socket.send(JSON.stringify({ type: "operation.result", requestId: command.operationRequestId, ok: true, entityId }));
264
+ }
260
265
  }
261
266
  catch (error) {
262
267
  if (command?.operationRequestId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.127",
3
+ "version": "0.1.129",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -101,6 +101,7 @@
101
101
  "scripts/send-retry.test.mjs",
102
102
  "scripts/error-messages.test.mjs",
103
103
  "scripts/git-diff-view.test.mjs",
104
+ "scripts/git-blame.test.mjs",
104
105
  "scripts/fork-server.test.mjs",
105
106
  "scripts/styles-no-dangling.test.mjs",
106
107
  "scripts/theme-no-hardcoded-colours.test.mjs",
@@ -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: "workspace_default" });
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: "workspace_default",
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: "workspace_default" });
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;