@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.
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +4 -4
- package/dist/client/assets/{CodeMirrorEditor-D6awRUZO.js → CodeMirrorEditor-CWN8JIx4.js} +2 -2
- package/dist/client/assets/{dist-C46474ys.js → dist-0vYGgCC5.js} +1 -1
- package/dist/client/assets/{dist-D0Iw5XQA.js → dist-B0vHKLg1.js} +1 -1
- package/dist/client/assets/{dist-DeZsNbFx.js → dist-BA6BeFaf.js} +1 -1
- package/dist/client/assets/{dist-DOwrSiuY.js → dist-BQjtlfpY.js} +1 -1
- package/dist/client/assets/{dist-DCiuVvGU.js → dist-BSOBu3lQ.js} +1 -1
- package/dist/client/assets/{dist-BkVaPA_q.js → dist-BtNKV30Z.js} +1 -1
- package/dist/client/assets/{dist-mPR8uuzk.js → dist-CTXz1R9C.js} +1 -1
- package/dist/client/assets/{dist-yTTCht0J.js → dist-CV9QN-qU.js} +1 -1
- package/dist/client/assets/{dist-1jSyapdW.js → dist-CYLUz40X.js} +1 -1
- package/dist/client/assets/{dist-Dk3e6RfY.js → dist-CwNZuQ4i.js} +1 -1
- package/dist/client/assets/{dist-CubYGjAk.js → dist-DCuAT0n-.js} +1 -1
- package/dist/client/assets/{dist-BYMYC_2I.js → dist-DErmxG4Q.js} +1 -1
- package/dist/client/assets/{dist-CNuKauOq.js → dist-DHTyhm7w.js} +1 -1
- package/dist/client/assets/{dist-spB2Q2CG.js → dist-DMDxxHpj.js} +1 -1
- package/dist/client/assets/{dist-DHBWUQhM.js → dist-Dalb5yHq.js} +1 -1
- package/dist/client/assets/{dist-Cwb-Lb5-.js → dist-OKarkelj.js} +1 -1
- package/dist/client/assets/{dist-WkVUFX28.js → dist-Qc0xt1y7.js} +1 -1
- package/dist/client/assets/{dist-BoeM5xW7.js → dist-WkcafMsD.js} +1 -1
- package/dist/client/assets/{dist-BGgidsaF.js → dist-ZvUavnTX.js} +1 -1
- package/dist/client/assets/{dist-CCkdCH6a.js → dist-q1T0uV1c.js} +1 -1
- package/dist/client/assets/{dist-lZUWPQbT.js → dist-t0oHfWqq.js} +1 -1
- package/dist/client/assets/{dist-DoDmoWTn.js → dist-yOveRczL.js} +1 -1
- package/dist/client/assets/{index-Dyta-HkQ.js → index-oDRgSKPX.js} +4 -4
- package/dist/client/assets/index-r-L_XCYs.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/active-memory.d.ts +17 -2
- package/dist/server/active-memory.js +67 -6
- package/dist/server/application/conversation-service.js +4 -2
- package/dist/server/application/scheduled-task-service.d.ts +7 -0
- package/dist/server/application/scheduled-task-service.js +14 -5
- package/dist/server/files.js +11 -1
- package/dist/server/model-filter.d.ts +13 -3
- package/dist/server/model-filter.js +7 -5
- package/dist/server/store.d.ts +22 -0
- package/dist/server/store.js +28 -0
- package/dist/server/subagents.js +10 -2
- package/package.json +6 -1
- 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
|
-
|
|
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
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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.
|
|
363
|
-
this
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
await this.conversationService.
|
|
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) {
|
package/dist/server/files.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/server/store.d.ts
CHANGED
|
@@ -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.
|
package/dist/server/store.js
CHANGED
|
@@ -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("模型过滤规则格式不合法");
|
package/dist/server/subagents.js
CHANGED
|
@@ -421,8 +421,16 @@ export class SubagentSupervisor {
|
|
|
421
421
|
}
|
|
422
422
|
if (!record)
|
|
423
423
|
break;
|
|
424
|
-
|
|
425
|
-
|
|
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.
|
|
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",
|