@evandu/super-pi 0.1.128 → 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-BhxCTVTL.js → CodeMirrorEditor-BNVBZZSS.js} +2 -2
  7. package/dist/client/assets/{dist-DERv1ty2.js → dist-B2nPVdKh.js} +1 -1
  8. package/dist/client/assets/{dist-BAmT9jUI.js → dist-B61RSa-K.js} +1 -1
  9. package/dist/client/assets/{dist-BPBlgusB.js → dist-BBW2SE_R.js} +1 -1
  10. package/dist/client/assets/{dist-BQK4xccN.js → dist-BDygQgZD.js} +1 -1
  11. package/dist/client/assets/{dist-C-hIztMW.js → dist-BKp4AWEJ.js} +1 -1
  12. package/dist/client/assets/{dist-Cl87QYqE.js → dist-BhJGK8-b.js} +1 -1
  13. package/dist/client/assets/{dist-Bplw4N5j.js → dist-BykVFExd.js} +1 -1
  14. package/dist/client/assets/{dist-K96Y1Tut.js → dist-C90MPTnk.js} +1 -1
  15. package/dist/client/assets/{dist-DVBpWkDV.js → dist-CMnvChDX.js} +1 -1
  16. package/dist/client/assets/{dist-BfwwrK33.js → dist-CRzRB9k3.js} +1 -1
  17. package/dist/client/assets/{dist-BeYwU1xB.js → dist-CWYRjnj-.js} +1 -1
  18. package/dist/client/assets/{dist-AtHUPHQe.js → dist-CZ9I2JCd.js} +1 -1
  19. package/dist/client/assets/{dist-8-xcbQPp.js → dist-ChszWGDv.js} +1 -1
  20. package/dist/client/assets/{dist-Bof_dYLf.js → dist-CiSGSVBZ.js} +1 -1
  21. package/dist/client/assets/{dist-Dc6xSTJL.js → dist-Cxerc7Bb.js} +1 -1
  22. package/dist/client/assets/{dist-DobX-p1U.js → dist-DB5M5fZG.js} +1 -1
  23. package/dist/client/assets/{dist-YUFYdtGJ.js → dist-DZnUERoW.js} +1 -1
  24. package/dist/client/assets/{dist-BJZUzzzf.js → dist-DmAOqr0M.js} +1 -1
  25. package/dist/client/assets/{dist-Bw8QOdGP.js → dist-Dq4-QFAJ.js} +1 -1
  26. package/dist/client/assets/{dist-icIMVISS.js → dist-GkIV1LTX.js} +1 -1
  27. package/dist/client/assets/{dist-DTXHzd8a.js → dist-HdeIBl2J.js} +1 -1
  28. package/dist/client/assets/{dist-5LsaarOt.js → dist-Ur37340K.js} +1 -1
  29. package/dist/client/assets/index-CXB6U5mz.css +1 -0
  30. package/dist/client/assets/{index-BvQ3umhR.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 +6 -0
  42. package/dist/server/store.js +16 -5
  43. package/dist/server/ws-server.js +3 -0
  44. package/package.json +2 -1
  45. package/scripts/diagnose.mjs +8 -3
  46. package/dist/client/assets/index-B8GInEf1.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
  *
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.128",
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;