@evandu/super-pi 0.1.138 → 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-CVImD0nh.js → CodeMirrorEditor-H7SHGhdS.js} +2 -2
  7. package/dist/client/assets/{dist-At94VK1P.js → dist-2XXLO-IZ.js} +1 -1
  8. package/dist/client/assets/{dist-DhEa5hHR.js → dist-BEM6tttY.js} +1 -1
  9. package/dist/client/assets/{dist-CKI1xVeE.js → dist-BfUeRJKr.js} +1 -1
  10. package/dist/client/assets/{dist-1H_gIENV.js → dist-BokzcFLE.js} +1 -1
  11. package/dist/client/assets/{dist-DSCjkO-y.js → dist-BoucVzKA.js} +1 -1
  12. package/dist/client/assets/{dist-CUACXe1Y.js → dist-BxTGjU84.js} +1 -1
  13. package/dist/client/assets/{dist-yONALz0V.js → dist-C-MUXK_t.js} +1 -1
  14. package/dist/client/assets/{dist-YB4GJD3u.js → dist-C8NllalC.js} +1 -1
  15. package/dist/client/assets/{dist-quQ6u6Rw.js → dist-CSdzvQnf.js} +1 -1
  16. package/dist/client/assets/{dist-Cocw5XJ_.js → dist-CUNctZ5v.js} +1 -1
  17. package/dist/client/assets/{dist-4uPxkRdw.js → dist-CZ9Krsno.js} +1 -1
  18. package/dist/client/assets/{dist-B75OT6A9.js → dist-C_ftUQZN.js} +1 -1
  19. package/dist/client/assets/{dist-pnGHl3nq.js → dist-CuMnZOGE.js} +1 -1
  20. package/dist/client/assets/{dist-CiEW7LQW.js → dist-CukPKrEn.js} +1 -1
  21. package/dist/client/assets/{dist-CNP-Q5Gs.js → dist-D3Ta0y4e.js} +1 -1
  22. package/dist/client/assets/{dist-BqblSnWL.js → dist-DJ8SwQl_.js} +1 -1
  23. package/dist/client/assets/{dist-DozjvPAZ.js → dist-DLKVFZhO.js} +1 -1
  24. package/dist/client/assets/{dist-amySbADh.js → dist-Jw-GQ-re.js} +1 -1
  25. package/dist/client/assets/{dist-BwQM8_Mk.js → dist-NlopeJ5R.js} +1 -1
  26. package/dist/client/assets/{dist-BIgRwYsI.js → dist-POoQ4j29.js} +1 -1
  27. package/dist/client/assets/{dist-faxwvvOU.js → dist-cP2vnTP_.js} +1 -1
  28. package/dist/client/assets/{dist-Clf26yld.js → dist-zUJqdbfo.js} +1 -1
  29. package/dist/client/assets/{index-CNoiRdJC.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 +14 -1
  61. package/scripts/diagnose.mjs +161 -58
  62. package/dist/client/assets/index-FxTrUapG.css +0 -1
@@ -219,7 +219,14 @@ export class ScheduledTaskService {
219
219
  if (this.stopped)
220
220
  return;
221
221
  const now = Date.now();
222
- const due = (this.store.snapshot().scheduledTasks ?? []).filter((task) => task.enabled && task.nextRunAt !== undefined && task.nextRunAt <= now);
222
+ // READ THROUGH THE NARROW READER, NOT THROUGH A CLONE OF THE WHOLE APPLICATION.
223
+ //
224
+ // This sweep runs every fifteen seconds for the whole life of the server. It used to read its
225
+ // tasks through `store.snapshot()` - a deep clone of every workspace, every conversation and every
226
+ // message of every conversation - to find the zero-to-three tasks that are due. On a server
227
+ // holding a long transcript that is a full copy of the app four times a minute, for a list that is
228
+ // usually empty. The narrow reader returns the tasks themselves and nothing else.
229
+ const due = this.store.scheduledTasks().filter((task) => task.enabled && task.nextRunAt !== undefined && task.nextRunAt <= now);
223
230
  for (const task of due) {
224
231
  if (this.inFlight.has(task.id))
225
232
  continue;
@@ -108,7 +108,22 @@ export class WorkspaceService {
108
108
  agentToStore = agent.id;
109
109
  }
110
110
  }
111
- assertModelAllowed(this.store.snapshot().modelFilterRules, workspace, selection ?? (await this.runtime.catalog()).defaultModel);
111
+ // THE MODEL RULES HOLD THE MODEL THE USER JUST SET. The check is on `selection` ALONE, and
112
+ // `selection` is undefined when the update did not touch the model at all - which is a
113
+ // DIFFERENT thing from 「the model is Pi's default」.
114
+ //
115
+ // It used to read `selection ?? (await this.runtime.catalog()).defaultModel`, so changing the
116
+ // THINKING LEVEL - a setting that has nothing to do with models - was checked against Pi's own
117
+ // default model, and a workspace whose rules allow only its own default could not change its
118
+ // thinking level at all. The refusal even named a model the user had never chosen: 「当前模型
119
+ // 「Pi 默认 (prov/model-zzz)」不匹配允许的模型正则」.
120
+ //
121
+ // What the rules have to hold is the model this workspace's conversations will RUN ON, and the
122
+ // only model the user could have set here is `selection`. When the update does not name a model,
123
+ // the workspace's existing default (or the absence of one) is what stands, and that is checked
124
+ // where it is stamped onto a conversation - at birth, in `conversation-service.ts`.
125
+ if (selection)
126
+ assertModelAllowed(this.store.modelFilterRules(), workspace, selection);
112
127
  await this.store.setWorkspaceDefaults(workspaceId, {
113
128
  model: selection,
114
129
  thinkingLevel: settings.thinkingLevel ?? undefined,
@@ -60,8 +60,15 @@ const defaultSpawn = (command, args, options) => new Promise((resolve, reject) =
60
60
  });
61
61
  let stdout = "";
62
62
  let stderr = "";
63
- child.stdout?.on("data", (chunk) => { stdout += String(chunk); });
64
- child.stderr?.on("data", (chunk) => { stderr += String(chunk); });
63
+ // A PIPE CARRIES BYTES, AND A READ BOUNDARY CAN LAND INSIDE A CHARACTER. A Chinese character is
64
+ // three bytes, so decoding one chunk at a time turns every character that straddles a boundary
65
+ // into U+FFFD - which for this CLI means a JSON envelope whose text is damaged, and an envelope
66
+ // that may not parse at all. The stream decodes instead, and Node keeps an incomplete
67
+ // character until the rest of its bytes arrive.
68
+ child.stdout?.setEncoding("utf8");
69
+ child.stderr?.setEncoding("utf8");
70
+ child.stdout?.on("data", (chunk) => { stdout += chunk; });
71
+ child.stderr?.on("data", (chunk) => { stderr += chunk; });
65
72
  const timer = setTimeout(() => { child.kill(); }, options.timeoutMs);
66
73
  child.once("error", (error) => {
67
74
  clearTimeout(timer);
@@ -91,8 +98,12 @@ const defaultBackgroundSpawn = (command, args, options) => {
91
98
  });
92
99
  return {
93
100
  onOutput: (listener) => {
94
- child.stdout?.on("data", (chunk) => listener(String(chunk)));
95
- child.stderr?.on("data", (chunk) => listener(String(chunk)));
101
+ // The same rule as the one-shot spawn: the stream decodes, so a character that straddles a
102
+ // read boundary stays whole.
103
+ child.stdout?.setEncoding("utf8");
104
+ child.stderr?.setEncoding("utf8");
105
+ child.stdout?.on("data", (chunk) => listener(chunk));
106
+ child.stderr?.on("data", (chunk) => listener(chunk));
96
107
  },
97
108
  done: new Promise((resolve) => {
98
109
  child.once("close", () => resolve());
@@ -46,6 +46,19 @@ export declare class FileIndex {
46
46
  start(root: string): void;
47
47
  stop(): void;
48
48
  snapshot(since?: number): FileIndexSnapshot;
49
+ /**
50
+ * ONE FILE, REFRESHED.
51
+ *
52
+ * The path arrives from a MODEL - from the arguments of the `write`/`edit` tool - so it may be
53
+ * absolute, and an absolute path inside the workspace is the same file as its relative spelling.
54
+ * `toWorkspaceRelative` translates that spelling; the guard then decides containment, exactly as it
55
+ * does for every other path in this module.
56
+ *
57
+ * A FILE THAT IS GONE IS NOT A FAILURE. `ENOENT` means the agent deleted it, and the index says so
58
+ * by removing it. Anything else is a real problem and is thrown to the caller - which is why the
59
+ * callers must handle it (see `refreshWorkspaceIndex` in `run-service.ts`): a background index
60
+ * refresh must never take a turn down with it.
61
+ */
49
62
  refresh(path: string): Promise<void>;
50
63
  /** Record metadata already obtained by opening, saving, or polling an open file. No second stat: the
51
64
  * caller's file API operation has already passed the sole containment guard and read this metadata. */
@@ -54,3 +67,20 @@ export declare class FileIndex {
54
67
  private scan;
55
68
  private publish;
56
69
  }
70
+ /**
71
+ * REFRESH THE WORKSPACE'S FILE INDEX AFTER THE AGENT WROTE SOMETHING - AND NEVER AT THE COST OF THE
72
+ * TURN.
73
+ *
74
+ * WHY THIS IS A FUNCTION AND NOT A `void` CALL. `FileIndex.refresh` refuses a path it cannot contain,
75
+ * and a refusal is a REJECTED PROMISE. Calling it with `void` and nothing else leaves that rejection
76
+ * without a handler, and Node reports an unhandled rejection - which this server used to log as a
77
+ * `fatal` line. That is exactly what happened in the field: one absolute-path write per agent edit
78
+ * produced one `[fatal] unhandledRejection: FilesError` in the server log - a log that reads like a
79
+ * crash report and is not one - while the Files panel went on showing the file the agent had just
80
+ * written as if it had never been touched.
81
+ *
82
+ * A REFUSED PATH IS A FACT ABOUT THE PATH, NOT A FAULT OF THE TURN. So the refusal is caught here,
83
+ * said out loud, and the turn carries on. The index is a convenience; it must never be able to end a
84
+ * conversation.
85
+ */
86
+ export declare function refreshWorkspaceIndex(indexes: Map<string, FileIndex> | undefined, workspaceId: string, path: string): void;
@@ -1,5 +1,5 @@
1
1
  import { readdir, stat } from "node:fs/promises";
2
- import { SEARCH_SKIP_DIRS, createRootGuard, fileMeta, isTextName, toRelative } from "./files.js";
2
+ import { SEARCH_SKIP_DIRS, createRootGuard, fileMeta, isTextName, toRelative, toWorkspaceRelative } from "./files.js";
3
3
  const MAX_DIRECTORIES = 4000;
4
4
  const MAX_FILES = 30000;
5
5
  const MAX_MS = 4000;
@@ -41,14 +41,35 @@ export class FileIndex {
41
41
  changed: valid ? deltas.flatMap((item) => item.changed) : [...this.entries.values()],
42
42
  removed: valid ? deltas.flatMap((item) => item.removed) : [] };
43
43
  }
44
+ /**
45
+ * ONE FILE, REFRESHED.
46
+ *
47
+ * The path arrives from a MODEL - from the arguments of the `write`/`edit` tool - so it may be
48
+ * absolute, and an absolute path inside the workspace is the same file as its relative spelling.
49
+ * `toWorkspaceRelative` translates that spelling; the guard then decides containment, exactly as it
50
+ * does for every other path in this module.
51
+ *
52
+ * A FILE THAT IS GONE IS NOT A FAILURE. `ENOENT` means the agent deleted it, and the index says so
53
+ * by removing it. Anything else is a real problem and is thrown to the caller - which is why the
54
+ * callers must handle it (see `refreshWorkspaceIndex` in `run-service.ts`): a background index
55
+ * refresh must never take a turn down with it.
56
+ */
44
57
  async refresh(path) {
45
58
  if (!this.root)
46
59
  return;
47
60
  const guard = createRootGuard(this.root);
61
+ const requested = toWorkspaceRelative(this.root, path);
62
+ // The key the index stores paths under. It starts as the translated path and is replaced by the
63
+ // canonical relative form once the guard has resolved it, so a removed file is removed under the
64
+ // name the index actually knows it by.
65
+ let key = requested;
48
66
  try {
49
- const absolute = await guard(path);
50
- const info = await stat(absolute);
67
+ const absolute = await guard(requested);
68
+ // The canonical relative name is settled BEFORE the stat, so a file that is already gone is
69
+ // still removed under the name the index knows it by (`src/a.ts`, not `src\a.ts`).
51
70
  const rel = toRelative(this.root, absolute);
71
+ key = rel;
72
+ const info = await stat(absolute);
52
73
  if (info.isDirectory())
53
74
  this.publish([{ path: rel, kind: "directory", size: null, mtimeMs: null, etag: null, text: null }], []);
54
75
  else {
@@ -59,7 +80,7 @@ export class FileIndex {
59
80
  catch (error) {
60
81
  const code = error.code;
61
82
  if (code === "ENOENT" || code === "ENOTDIR")
62
- this.remove(path);
83
+ this.remove(key);
63
84
  else
64
85
  throw error;
65
86
  }
@@ -148,3 +169,27 @@ export class FileIndex {
148
169
  this.broadcast(event);
149
170
  }
150
171
  }
172
+ /**
173
+ * REFRESH THE WORKSPACE'S FILE INDEX AFTER THE AGENT WROTE SOMETHING - AND NEVER AT THE COST OF THE
174
+ * TURN.
175
+ *
176
+ * WHY THIS IS A FUNCTION AND NOT A `void` CALL. `FileIndex.refresh` refuses a path it cannot contain,
177
+ * and a refusal is a REJECTED PROMISE. Calling it with `void` and nothing else leaves that rejection
178
+ * without a handler, and Node reports an unhandled rejection - which this server used to log as a
179
+ * `fatal` line. That is exactly what happened in the field: one absolute-path write per agent edit
180
+ * produced one `[fatal] unhandledRejection: FilesError` in the server log - a log that reads like a
181
+ * crash report and is not one - while the Files panel went on showing the file the agent had just
182
+ * written as if it had never been touched.
183
+ *
184
+ * A REFUSED PATH IS A FACT ABOUT THE PATH, NOT A FAULT OF THE TURN. So the refusal is caught here,
185
+ * said out loud, and the turn carries on. The index is a convenience; it must never be able to end a
186
+ * conversation.
187
+ */
188
+ export function refreshWorkspaceIndex(indexes, workspaceId, path) {
189
+ const index = indexes?.get(workspaceId);
190
+ if (!index)
191
+ return;
192
+ void index.refresh(path).catch((error) => {
193
+ console.warn(`Super Pi: the file index did not refresh ${path}: ${error instanceof Error ? error.message : "unknown error"}`);
194
+ });
195
+ }
@@ -158,6 +158,27 @@ export type DirListing = {
158
158
  };
159
159
  /** The `/`-separated relative form used on the wire and in every comparison. */
160
160
  export declare function toRelative(root: string, absolute: string): string;
161
+ /**
162
+ * A PATH A MODEL WROTE, AS THE WORKSPACE SPELLS IT.
163
+ *
164
+ * WHO SPELLS PATHS THIS WAY. Pi's `write`/`edit` tools carry whatever path the MODEL produced, and a
165
+ * model that has been told where the workspace is writes an absolute path: `/home/me/project/src/a.ts`
166
+ * is the same file as `src/a.ts`. The model is not trying to escape anything - it is repeating the
167
+ * workspace root it was given.
168
+ *
169
+ * WHY THIS IS HERE AND NOT IN THE CALLER. Two layers need the same translation and neither may own
170
+ * it: the file-change tracker (which records what the agent wrote) and the file index (which refreshes
171
+ * what the agent wrote). Before this function existed, the index refused an absolute path outright -
172
+ * and because the refusal was thrown out of a promise nobody was awaiting, one absolute-path write
173
+ * produced one `[fatal] unhandledRejection` in the server log and left the Files panel showing a file
174
+ * the agent had just written as if it had never been touched.
175
+ *
176
+ * AN ABSOLUTE PATH OUTSIDE THE WORKSPACE IS REFUSED, EXACTLY AS `../` IS REFUSED. The containment rule
177
+ * belongs to this module and has exactly one owner; this function does not get a second, weaker copy of
178
+ * it. It translates a spelling and then hands the result to the guard, which is what actually decides
179
+ * containment - including the symlink case, which a string comparison can never see.
180
+ */
181
+ export declare function toWorkspaceRelative(root: string, requested: string): string;
161
182
  /** One directory's contents. Directories sort before files, then case-insensitively by name with numeric
162
183
  * ordering, so `src2` lands between `src` and `src10` rather than after every `srcN`. */
163
184
  export declare function listDirectory(root: string, requested: string | null | undefined): Promise<DirListing>;
@@ -396,6 +396,44 @@ export function isDocumentName(name) {
396
396
  export function toRelative(root, absolute) {
397
397
  return relative(root, absolute).split(sep).join("/");
398
398
  }
399
+ /**
400
+ * A PATH A MODEL WROTE, AS THE WORKSPACE SPELLS IT.
401
+ *
402
+ * WHO SPELLS PATHS THIS WAY. Pi's `write`/`edit` tools carry whatever path the MODEL produced, and a
403
+ * model that has been told where the workspace is writes an absolute path: `/home/me/project/src/a.ts`
404
+ * is the same file as `src/a.ts`. The model is not trying to escape anything - it is repeating the
405
+ * workspace root it was given.
406
+ *
407
+ * WHY THIS IS HERE AND NOT IN THE CALLER. Two layers need the same translation and neither may own
408
+ * it: the file-change tracker (which records what the agent wrote) and the file index (which refreshes
409
+ * what the agent wrote). Before this function existed, the index refused an absolute path outright -
410
+ * and because the refusal was thrown out of a promise nobody was awaiting, one absolute-path write
411
+ * produced one `[fatal] unhandledRejection` in the server log and left the Files panel showing a file
412
+ * the agent had just written as if it had never been touched.
413
+ *
414
+ * AN ABSOLUTE PATH OUTSIDE THE WORKSPACE IS REFUSED, EXACTLY AS `../` IS REFUSED. The containment rule
415
+ * belongs to this module and has exactly one owner; this function does not get a second, weaker copy of
416
+ * it. It translates a spelling and then hands the result to the guard, which is what actually decides
417
+ * containment - including the symlink case, which a string comparison can never see.
418
+ */
419
+ export function toWorkspaceRelative(root, requested) {
420
+ const raw = requested.trim();
421
+ // A RELATIVE PATH IS ALREADY THE WORKSPACE'S SPELLING. It is passed through untouched so that the
422
+ // guard sees exactly what the caller asked for - including `../`, which the guard is there to refuse.
423
+ if (!isAbsolute(raw))
424
+ return raw;
425
+ const base = resolve(root);
426
+ const rel = relative(base, resolve(raw));
427
+ if (rel === "")
428
+ return "";
429
+ // `relative` answers "what is left to walk from base to target". When the target is on another drive
430
+ // (Windows) or outside the root (anywhere), the answer is an absolute path or a chain of `..`, and
431
+ // both mean the same thing: this path is not inside the workspace.
432
+ if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`) || rel.startsWith("../")) {
433
+ throw new FilesError(403, "拒绝绝对路径:请给出相对于工作区根目录的路径", "outside_workspace");
434
+ }
435
+ return rel.split(sep).join("/");
436
+ }
399
437
  /** One directory's contents. Directories sort before files, then case-insensitively by name with numeric
400
438
  * ordering, so `src2` lands between `src` and `src10` rather than after every `srcN`. */
401
439
  export async function listDirectory(root, requested) {