@evandu/super-pi 0.1.135 → 0.1.136

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 (41) hide show
  1. package/dist/.super-pi-check-inputs.json +2 -2
  2. package/dist/client/.super-pi-client-sources.json +4 -4
  3. package/dist/client/assets/{CodeMirrorEditor-D6awRUZO.js → CodeMirrorEditor-CWN8JIx4.js} +2 -2
  4. package/dist/client/assets/{dist-C46474ys.js → dist-0vYGgCC5.js} +1 -1
  5. package/dist/client/assets/{dist-D0Iw5XQA.js → dist-B0vHKLg1.js} +1 -1
  6. package/dist/client/assets/{dist-DeZsNbFx.js → dist-BA6BeFaf.js} +1 -1
  7. package/dist/client/assets/{dist-DOwrSiuY.js → dist-BQjtlfpY.js} +1 -1
  8. package/dist/client/assets/{dist-DCiuVvGU.js → dist-BSOBu3lQ.js} +1 -1
  9. package/dist/client/assets/{dist-BkVaPA_q.js → dist-BtNKV30Z.js} +1 -1
  10. package/dist/client/assets/{dist-mPR8uuzk.js → dist-CTXz1R9C.js} +1 -1
  11. package/dist/client/assets/{dist-yTTCht0J.js → dist-CV9QN-qU.js} +1 -1
  12. package/dist/client/assets/{dist-1jSyapdW.js → dist-CYLUz40X.js} +1 -1
  13. package/dist/client/assets/{dist-Dk3e6RfY.js → dist-CwNZuQ4i.js} +1 -1
  14. package/dist/client/assets/{dist-CubYGjAk.js → dist-DCuAT0n-.js} +1 -1
  15. package/dist/client/assets/{dist-BYMYC_2I.js → dist-DErmxG4Q.js} +1 -1
  16. package/dist/client/assets/{dist-CNuKauOq.js → dist-DHTyhm7w.js} +1 -1
  17. package/dist/client/assets/{dist-spB2Q2CG.js → dist-DMDxxHpj.js} +1 -1
  18. package/dist/client/assets/{dist-DHBWUQhM.js → dist-Dalb5yHq.js} +1 -1
  19. package/dist/client/assets/{dist-Cwb-Lb5-.js → dist-OKarkelj.js} +1 -1
  20. package/dist/client/assets/{dist-WkVUFX28.js → dist-Qc0xt1y7.js} +1 -1
  21. package/dist/client/assets/{dist-BoeM5xW7.js → dist-WkcafMsD.js} +1 -1
  22. package/dist/client/assets/{dist-BGgidsaF.js → dist-ZvUavnTX.js} +1 -1
  23. package/dist/client/assets/{dist-CCkdCH6a.js → dist-q1T0uV1c.js} +1 -1
  24. package/dist/client/assets/{dist-lZUWPQbT.js → dist-t0oHfWqq.js} +1 -1
  25. package/dist/client/assets/{dist-DoDmoWTn.js → dist-yOveRczL.js} +1 -1
  26. package/dist/client/assets/{index-Dyta-HkQ.js → index-oDRgSKPX.js} +4 -4
  27. package/dist/client/assets/index-r-L_XCYs.css +1 -0
  28. package/dist/client/index.html +2 -2
  29. package/dist/server/active-memory.d.ts +17 -2
  30. package/dist/server/active-memory.js +67 -6
  31. package/dist/server/application/conversation-service.js +4 -2
  32. package/dist/server/application/scheduled-task-service.d.ts +7 -0
  33. package/dist/server/application/scheduled-task-service.js +14 -5
  34. package/dist/server/files.js +11 -1
  35. package/dist/server/model-filter.d.ts +13 -3
  36. package/dist/server/model-filter.js +7 -5
  37. package/dist/server/store.d.ts +22 -0
  38. package/dist/server/store.js +28 -0
  39. package/dist/server/subagents.js +10 -2
  40. package/package.json +6 -1
  41. package/dist/client/assets/index-V93Kzott.css +0 -1
@@ -9,6 +9,13 @@ export class ActiveMemory {
9
9
  inverted = new Map();
10
10
  indexedWorkspaces = new Set();
11
11
  indexedMessages = new Set();
12
+ /**
13
+ * messageId -> which workspace's index holds it and under which terms. This is the reverse side of
14
+ * `inverted`, and it is what makes removal surgical: a revert knows exactly which messages it cut
15
+ * (the store's `plan.removedMessageIds`), so exactly those postings can be dropped - the rest of the
16
+ * workspace stays indexed instead of paying a full rebuild on the next search.
17
+ */
18
+ termsByMessage = new Map();
12
19
  notes;
13
20
  capabilities = new Map();
14
21
  reviews = new Map();
@@ -138,7 +145,9 @@ export class ActiveMemory {
138
145
  continue;
139
146
  const value = { conversationId: conversation.id, title: conversation.title, messageId: message.id, role: message.role, content: message.content };
140
147
  this.indexedMessages.add(message.id);
141
- for (const word of message.content.toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? []) {
148
+ const terms = message.content.toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? [];
149
+ this.termsByMessage.set(message.id, { workspaceId, terms });
150
+ for (const word of terms) {
142
151
  let bucket = index.get(word);
143
152
  if (!bucket)
144
153
  index.set(word, bucket = new Map());
@@ -149,7 +158,7 @@ export class ActiveMemory {
149
158
  this.indexedWorkspaces.add(workspaceId);
150
159
  }
151
160
  /**
152
- * Drops a workspace's search index so the next search rebuilds it from the store.
161
+ * Drops exactly the given messages from the search index, leaving the rest of the workspace indexed.
153
162
  *
154
163
  * WHY THIS EXISTS. `ensureIndex` builds once per workspace and `addToIndex` only ever adds, so the
155
164
  * index outlives the messages it indexed: a revert removes messages from the store while the index
@@ -158,17 +167,54 @@ export class ActiveMemory {
158
167
  * history" after a revert every file-level assertion proved had really truncated. The store is the
159
168
  * source of truth; the index is a cache of it, and a cache that survives an event that invalidated
160
169
  * its source is wrong by definition.
170
+ *
171
+ * WHY SURGICAL. The caller (revert) knows precisely which message ids the store dropped, and this
172
+ * method trusts that list rather than re-deriving it: the ids not in `termsByMessage` were never
173
+ * indexed (no search had built the index, or a role the index skips), so a no-op for them is
174
+ * correct, and the next search - if the index was never built - builds from the post-revert store
175
+ * anyway. Dropping the whole workspace index instead would force the next search to rebuild every
176
+ * surviving conversation's entries for no reason, and the recall gap in between would answer
177
+ * searches with nothing at all.
161
178
  */
162
- invalidateSearchIndex(workspaceId) {
163
- this.indexedWorkspaces.delete(workspaceId);
164
- this.inverted.delete(workspaceId);
179
+ removeFromIndex(messageIds) {
180
+ for (const messageId of messageIds) {
181
+ const entry = this.termsByMessage.get(messageId);
182
+ if (!entry)
183
+ continue;
184
+ const terms = this.inverted.get(entry.workspaceId);
185
+ if (terms) {
186
+ for (const term of entry.terms) {
187
+ const bucket = terms.get(term);
188
+ if (!bucket)
189
+ continue;
190
+ bucket.delete(messageId);
191
+ if (!bucket.size)
192
+ terms.delete(term);
193
+ }
194
+ }
195
+ this.termsByMessage.delete(messageId);
196
+ this.indexedMessages.delete(messageId);
197
+ }
165
198
  }
166
199
  addToIndex(conversation, message) {
167
200
  if (!this.indexedWorkspaces.has(conversation.workspaceId) || (message.role !== "user" && message.role !== "assistant"))
168
201
  return;
169
202
  let terms = this.inverted.get(conversation.workspaceId);
170
203
  const value = { conversationId: conversation.id, title: conversation.title, messageId: message.id, role: message.role, content: message.content };
171
- for (const term of message.content.toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? []) {
204
+ const messageTerms = message.content.toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? [];
205
+ // A REFRESH MUST ALSO FORGET THE OLD TERMS. The buckets map a term to the message that contains
206
+ // it, and a term that only the OLD content had would keep pointing at this message with a stale
207
+ // copy - so a search for a word the model has since dropped would still hand the reader the old
208
+ // text. The terms this message had are remembered exactly so they can be dropped here.
209
+ const previous = this.termsByMessage.get(message.id);
210
+ if (previous)
211
+ for (const term of previous.terms) {
212
+ const bucket = terms.get(term);
213
+ if (bucket)
214
+ bucket.delete(message.id);
215
+ }
216
+ this.termsByMessage.set(message.id, { workspaceId: conversation.workspaceId, terms: messageTerms });
217
+ for (const term of messageTerms) {
172
218
  let bucket = terms.get(term);
173
219
  if (!bucket)
174
220
  terms.set(term, bucket = new Map());
@@ -270,6 +316,21 @@ export class ActiveMemory {
270
316
  this.cancel(id); }
271
317
  async waitForIdle() { await Promise.allSettled([...this.workspaceReviews.values()]); await this.serial; }
272
318
  async completed(conversation) {
319
+ // REFRESH THIS TURN'S MESSAGES, DO NOT JUST FILL IN THE UNSEEN ONES.
320
+ //
321
+ // The index is a CACHE of the store's content, and an assistant message's content changes while
322
+ // the model is still writing it: the entry was built from the empty placeholder the turn opened
323
+ // with, so it holds the answer BEFORE the answer. "I have seen this message" is not "I have seen
324
+ // its final content" - treating the two as the same froze this turn's answer in the index, and a
325
+ // frozen answer is an answer that can never be recalled (measured by
326
+ // scripts/active-memory-index-content.test.mjs: the recall came back `undefined`).
327
+ //
328
+ // Only THIS TURN's messages are refreshed - the ones after the last user message. The rest of the
329
+ // transcript was indexed when it was final, and re-tokenizing a whole conversation on every turn
330
+ // end is exactly the cost this file is about.
331
+ const lastUserIndex = conversation.messages.map((message) => message.role).lastIndexOf("user");
332
+ for (const message of lastUserIndex >= 0 ? conversation.messages.slice(lastUserIndex + 1) : conversation.messages)
333
+ this.addToIndex(conversation, message);
273
334
  for (const message of conversation.messages)
274
335
  if (!this.indexedMessages.has(message.id))
275
336
  this.addToIndex(conversation, message);
@@ -359,8 +359,10 @@ export class ConversationService {
359
359
  // The recall index is a cache of the store's messages, and the store just lost several: left alone it
360
360
  // keeps answering the memory extension's searches with the withdrawn turns, so the model is told about
361
361
  // history the person reverted away even though the transcript, Pi's file and the memory store all
362
- // agree it is gone. Invalidate it and the next search rebuilds from what actually survived.
363
- this.memory?.invalidateSearchIndex(workspaceId);
362
+ // agree it is gone. The store's plan says exactly which message ids were dropped, so exactly those
363
+ // postings leave the index - the surviving conversations of this workspace stay indexed and the next
364
+ // search pays no rebuild.
365
+ this.memory?.removeFromIndex(applied.removedMessageIds);
364
366
  console.log(`Super Pi: reverted conversation ${conversationId} to ${applied.boundaryIndex + 1} message(s): ` +
365
367
  `${plan.removedMessageIds.length} message(s) and ${applied.doomedRunIds.length} sub-agent run(s) dropped from state.json; ` +
366
368
  `Pi's file ${outcome.sessionFile} kept ${outcome.linesKept} line(s) and lost ${outcome.entriesDropped}; ` +
@@ -147,6 +147,13 @@ export declare class ScheduledTaskService {
147
147
  * one that has to use it. A failure here is the task's failure - a model that Pi no longer offers
148
148
  * cannot be silently ignored, because the run would then happen on some other model and the panel
149
149
  * would say it succeeded.
150
+ *
151
+ * AND WHEN THERE IS NO OVERRIDE, THE DEFAULTS BEHIND IT GO ON THE CONVERSATION TOO. Clearing the
152
+ * override in the panel means 「跟着工作区默认走」, and the only way to honour that on a conversation
153
+ * that already ran with the override is to PUT THE DEFAULT BACK. The conversation's model is a
154
+ * copy-at-birth value (AGENTS.md: 建会话时把默认值盖到会话字段上,之后永不回查工作区), so "do
155
+ * nothing when there is no override" keeps the WITHDRAWN override pinned for good - and the panel
156
+ * says the run succeeded on a model the user just took away.
150
157
  */
151
158
  private applyConfiguration;
152
159
  /** Writes down how the run went, and tells the panel. */
@@ -302,14 +302,23 @@ export class ScheduledTaskService {
302
302
  * one that has to use it. A failure here is the task's failure - a model that Pi no longer offers
303
303
  * cannot be silently ignored, because the run would then happen on some other model and the panel
304
304
  * would say it succeeded.
305
+ *
306
+ * AND WHEN THERE IS NO OVERRIDE, THE DEFAULTS BEHIND IT GO ON THE CONVERSATION TOO. Clearing the
307
+ * override in the panel means 「跟着工作区默认走」, and the only way to honour that on a conversation
308
+ * that already ran with the override is to PUT THE DEFAULT BACK. The conversation's model is a
309
+ * copy-at-birth value (AGENTS.md: 建会话时把默认值盖到会话字段上,之后永不回查工作区), so "do
310
+ * nothing when there is no override" keeps the WITHDRAWN override pinned for good - and the panel
311
+ * says the run succeeded on a model the user just took away.
305
312
  */
306
313
  async applyConfiguration(task, conversationId) {
307
- if (task.model) {
308
- await this.conversationService.setModel(conversationId, task.model.provider, task.model.modelId);
309
- }
310
- if (task.agentId) {
311
- await this.conversationService.setAgent(conversationId, task.agentId);
314
+ const state = this.store.snapshot();
315
+ const workspace = state.workspaces.find((item) => item.id === task.workspaceId);
316
+ const model = task.model ?? workspace?.defaultModel ?? state.defaultModel;
317
+ if (model) {
318
+ await this.conversationService.setModel(conversationId, model.provider, model.modelId);
312
319
  }
320
+ const agentId = task.agentId ?? workspace?.defaultAgentId ?? null;
321
+ await this.conversationService.setAgent(conversationId, agentId);
313
322
  }
314
323
  /** Writes down how the run went, and tells the panel. */
315
324
  async record(task, outcome) {
@@ -650,7 +650,17 @@ export async function writeWorkspaceFile(root, requested, content, baseEtag) {
650
650
  throw new FilesError(415, "拒绝写入:内容包含二进制字节(\\0)", "binary");
651
651
  const original = await readFile(target);
652
652
  const hadBom = original.length >= 3 && original[0] === 0xef && original[1] === 0xbb && original[2] === 0xbf;
653
- const output = hadBom ? `\uFEFF${content}` : content;
653
+ // THE LINE ENDINGS THE FILE HAD, NOT THE ONES THE EDITOR NORMALIZED.
654
+ //
655
+ // CodeMirror's document is always LF (it splits lines on `/\r\n?|\n/`), so a CRLF file comes back
656
+ // from the editor as LF text - and writing that back is how one edited character turns into a
657
+ // whole-file diff, on Windows, in every repository with `core.autocrlf=true`. The contract in this
658
+ // file already says it ("a CRLF file is saved back as CRLF instead of silently rewritten to LF");
659
+ // it just was not implemented HERE: the BOM's byte-level convention was honoured two lines above
660
+ // and the EOL's was dropped.
661
+ const eol = original.includes("\r\n") ? "\r\n" : "\n";
662
+ const body = eol === "\r\n" ? content.replace(/\r?\n/g, "\r\n") : content.replace(/\r\n/g, "\n");
663
+ const output = hadBom ? `\uFEFF${body}` : body;
654
664
  const temporary = await mkdtemp(`${target}.super-pi-${process.pid}-`);
655
665
  const temporaryFile = join(temporary, basename(target));
656
666
  try {
@@ -6,8 +6,18 @@ export declare function applicableRules(rules: ModelFilterRule[] | undefined, wo
6
6
  export declare function assertModelAllowed(rules: ModelFilterRule[] | undefined, workspace: Workspace, model: ModelSelection | undefined): void;
7
7
  export declare function conversationWorkspace(state: AppState, conversationId: string): Workspace;
8
8
  /** Resolve Pi's actual global default when no model was selected; never guess a permitted replacement. */
9
- export declare function validateConversationModel(store: {
10
- snapshot(): AppState;
11
- }, runtime: {
9
+ /**
10
+ * THE STORE READS THIS NEEDS, AND NOTHING ELSE.
11
+ *
12
+ * The check wants two things: the rules, and the workspace the conversation is in. Both now have a
13
+ * narrow reader on the store (`modelFilterRules()` / `workspaceForConversation()`), so this path
14
+ * clones NOTHING - it used to deep-clone the whole application three times over, on the way to
15
+ * sending a message.
16
+ */
17
+ export interface ModelFilterStore {
18
+ modelFilterRules(): import("./types.js").ModelFilterRule[];
19
+ workspaceForConversation(conversationId: string): Workspace | undefined;
20
+ }
21
+ export declare function validateConversationModel(store: ModelFilterStore, runtime: {
12
22
  catalog(selection?: ModelSelection): Promise<import("./types.js").ModelCatalog>;
13
23
  }, conversationId: string, selection?: ModelSelection): Promise<void>;
@@ -29,18 +29,20 @@ export function conversationWorkspace(state, conversationId) {
29
29
  throw new Error("会话所属工作区不存在");
30
30
  return workspace;
31
31
  }
32
- /** Resolve Pi's actual global default when no model was selected; never guess a permitted replacement. */
33
32
  export async function validateConversationModel(store, runtime, conversationId, selection) {
34
- const state = store.snapshot();
35
- const workspace = conversationWorkspace(state, conversationId);
36
- if (!applicableRules(state.modelFilterRules, workspace).length)
33
+ const workspace = store.workspaceForConversation(conversationId);
34
+ if (!workspace)
35
+ throw new Error("会话所属工作区不存在");
36
+ if (!applicableRules(store.modelFilterRules(), workspace).length)
37
37
  return;
38
38
  const startedAt = performance.now();
39
39
  if (process.env.SUPER_PI_TRACE_LATENCY === "1")
40
40
  console.warn(`[super-pi latency] conversation=${conversationId} phase=model.validate.begin selection=${selection ? `${selection.provider}/${selection.modelId}` : "default"}`);
41
41
  const catalog = await runtime.catalog();
42
42
  const model = selection ? catalog.models.find(item => item.provider === selection.provider && item.modelId === selection.modelId) ?? selection : catalog.defaultModel;
43
- assertModelAllowed(store.snapshot().modelFilterRules, conversationWorkspace(store.snapshot(), conversationId), model);
43
+ // READ AGAIN, NOT CACHED: the catalog fetch above is an await, and a rule edited during it must
44
+ // still be enforced for this message. The narrow readers make that re-read cheap.
45
+ assertModelAllowed(store.modelFilterRules(), store.workspaceForConversation(conversationId), model);
44
46
  if (process.env.SUPER_PI_TRACE_LATENCY === "1")
45
47
  console.warn(`[super-pi latency] conversation=${conversationId} phase=model.validate.end elapsedMs=${Math.round(performance.now() - startedAt)}`);
46
48
  }
@@ -96,6 +96,28 @@ export declare class Store {
96
96
  static open(filePath: string): Promise<Store>;
97
97
  /** Opens an injected repository, retaining domain reconciliation here. */
98
98
  static openRepository(repository: AppStateRepository): Promise<Store>;
99
+ /**
100
+ * THE MODEL FILTER RULES, WITHOUT CLONING THE STATE TO READ THEM.
101
+ *
102
+ * `snapshot()` deep-clones the WHOLE application - every workspace, every conversation, every
103
+ * message of every conversation - and the model filter check needs ONE small array. Reading that
104
+ * array through a full clone is the difference between a few hundred bytes and the entire
105
+ * transcript of the app, on the path that runs every time a message is sent.
106
+ *
107
+ * So the rules get their own narrow reader. It returns a COPY of the array (the caller may keep
108
+ * it, and the store's own state must not change under it), and the individual rules are treated
109
+ * as read-only - which is what the filter does with them.
110
+ */
111
+ modelFilterRules(): import("./types.js").ModelFilterRule[];
112
+ /**
113
+ * THE WORKSPACE A CONVERSATION BELONGS TO, WITHOUT CLONING THE STATE TO FIND ONE ROW.
114
+ *
115
+ * The same argument as the rules above: the filter needs to know WHICH workspace the conversation
116
+ * is in (the rules are per workspace), and the only way to know that today is to clone everything
117
+ * and look. This returns a shallow copy of the one workspace - cheap, and detached enough that a
118
+ * caller cannot reach into the store's state through it.
119
+ */
120
+ workspaceForConversation(conversationId: string): Workspace | undefined;
99
121
  setModelFilterRules(rules: import("./types.js").ModelFilterRule[]): Promise<void>;
100
122
  /**
101
123
  * Sets (or clears) the app-wide default model.
@@ -689,6 +689,34 @@ export class Store {
689
689
  return new Store(repository, { workspaces: [], conversations: [], subagentRuns: [], backgroundTasks: [] });
690
690
  }
691
691
  }
692
+ /**
693
+ * THE MODEL FILTER RULES, WITHOUT CLONING THE STATE TO READ THEM.
694
+ *
695
+ * `snapshot()` deep-clones the WHOLE application - every workspace, every conversation, every
696
+ * message of every conversation - and the model filter check needs ONE small array. Reading that
697
+ * array through a full clone is the difference between a few hundred bytes and the entire
698
+ * transcript of the app, on the path that runs every time a message is sent.
699
+ *
700
+ * So the rules get their own narrow reader. It returns a COPY of the array (the caller may keep
701
+ * it, and the store's own state must not change under it), and the individual rules are treated
702
+ * as read-only - which is what the filter does with them.
703
+ */
704
+ modelFilterRules() {
705
+ return this.state.modelFilterRules ? [...this.state.modelFilterRules] : [];
706
+ }
707
+ /**
708
+ * THE WORKSPACE A CONVERSATION BELONGS TO, WITHOUT CLONING THE STATE TO FIND ONE ROW.
709
+ *
710
+ * The same argument as the rules above: the filter needs to know WHICH workspace the conversation
711
+ * is in (the rules are per workspace), and the only way to know that today is to clone everything
712
+ * and look. This returns a shallow copy of the one workspace - cheap, and detached enough that a
713
+ * caller cannot reach into the store's state through it.
714
+ */
715
+ workspaceForConversation(conversationId) {
716
+ const conversation = this.state.conversations.find((item) => item.id === conversationId);
717
+ const workspace = this.state.workspaces.find((item) => item.id === conversation?.workspaceId);
718
+ return workspace ? { ...workspace } : undefined;
719
+ }
692
720
  async setModelFilterRules(rules) {
693
721
  if (!Array.isArray(rules) || rules.some(rule => !rule || typeof rule.id !== "string" || typeof rule.workspacePattern !== "string" || typeof rule.modelPattern !== "string" || (rule.label !== undefined && typeof rule.label !== "string")))
694
722
  throw new Error("模型过滤规则格式不合法");
@@ -421,8 +421,16 @@ export class SubagentSupervisor {
421
421
  }
422
422
  if (!record)
423
423
  break;
424
- if (record.type === "response" && record.id === "prompt" && record.success === false)
425
- throw new Error(`Pi rejected the sub-agent task: ${record.error ?? "no reason given"}`);
424
+ // THE RECEIPT FOR OUR OWN PROMPT IS A KNOWN RECORD, NOT AN UNKNOWN ONE. Pi answers every
425
+ // command with `type:"response"`, so the successful receipt for the prompt we just wrote is
426
+ // part of the protocol - and treating it as unrecognised made every ordinary sub-agent run
427
+ // log a false alarm ("unhandled child record response - consumed without action"). A FAILED
428
+ // receipt is the run's failure and is thrown; a successful one is simply consumed.
429
+ if (record.type === "response" && record.id === "prompt") {
430
+ if (record.success === false)
431
+ throw new Error(`Pi rejected the sub-agent task: ${record.error ?? "no reason given"}`);
432
+ continue;
433
+ }
426
434
  if (record.type === "message_update") {
427
435
  const event = record.assistantMessageEvent;
428
436
  // `text_delta` is the only inner event that is output. `start` and `done` are the boundaries
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.135",
3
+ "version": "0.1.136",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -76,6 +76,7 @@
76
76
  "scripts/workspace-cwd.test.mjs",
77
77
  "scripts/subagents.test.mjs",
78
78
  "scripts/subagent-noise.test.mjs",
79
+ "scripts/subagent-response-receipt.test.mjs",
79
80
  "scripts/subagent-runs.test.mjs",
80
81
  "scripts/conversation-reconcile.test.mjs",
81
82
  "scripts/subagent-dispatch.test.mjs",
@@ -110,6 +111,7 @@
110
111
  "scripts/todo-tool.test.mjs",
111
112
  "scripts/tool-calls.test.mjs",
112
113
  "scripts/files-api.test.mjs",
114
+ "scripts/files-eol-roundtrip.test.mjs",
113
115
  "scripts/files-missing-path.test.mjs",
114
116
  "scripts/file-changes.test.mjs",
115
117
  "scripts/file-index.test.mjs",
@@ -138,6 +140,7 @@
138
140
  "scripts/provider-pi-reads.test.mjs",
139
141
  "scripts/session-extension-credentials.test.mjs",
140
142
  "scripts/active-memory.test.mjs",
143
+ "scripts/active-memory-index-content.test.mjs",
141
144
  "scripts/knowledge-base.test.mjs",
142
145
  "scripts/knowledge-import-path.test.mjs",
143
146
  "scripts/knowledge-import-identity.test.mjs",
@@ -164,6 +167,7 @@
164
167
  "scripts/document-write-overwrite.test.mjs",
165
168
  "scripts/feishu.test.mjs",
166
169
  "scripts/scheduled-tasks.test.mjs",
170
+ "scripts/scheduled-task-model-clear.test.mjs",
167
171
  "scripts/scheduled-task-scope.test.mjs",
168
172
  "scripts/scheduled-task-tool.test.mjs",
169
173
  "scripts/run-state-sync.test.mjs",
@@ -182,6 +186,7 @@
182
186
  "scripts/conversation-reconcile-browser.test.mjs",
183
187
  "scripts/visual-defects-browser.test.mjs",
184
188
  "scripts/session-state-animation-browser.test.mjs",
189
+ "scripts/session-state-delivery-browser.test.mjs",
185
190
  "scripts/agent-vis-card-browser.test.mjs",
186
191
  "scripts/queue-dock.test.mjs",
187
192
  "scripts/scheduled-tasks-browser.test.mjs",