@evandu/super-pi 0.1.135 → 0.1.137
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 +5 -5
- package/dist/client/assets/{CodeMirrorEditor-D6awRUZO.js → CodeMirrorEditor-CkA_5D5r.js} +2 -2
- package/dist/client/assets/{dist-BGgidsaF.js → dist-9Fm8XKu4.js} +1 -1
- package/dist/client/assets/{dist-spB2Q2CG.js → dist-B02Erd0t.js} +1 -1
- package/dist/client/assets/{dist-DoDmoWTn.js → dist-B3Mfuv1D.js} +1 -1
- package/dist/client/assets/{dist-yTTCht0J.js → dist-BfEmujEC.js} +1 -1
- package/dist/client/assets/{dist-WkVUFX28.js → dist-BqGjijI6.js} +1 -1
- package/dist/client/assets/{dist-DOwrSiuY.js → dist-BsqzrZ5R.js} +1 -1
- package/dist/client/assets/{dist-BYMYC_2I.js → dist-BySjislr.js} +1 -1
- package/dist/client/assets/{dist-Cwb-Lb5-.js → dist-C2tSd-AY.js} +1 -1
- package/dist/client/assets/{dist-DCiuVvGU.js → dist-CrxmpY93.js} +1 -1
- package/dist/client/assets/{dist-1jSyapdW.js → dist-CsNytPug.js} +1 -1
- package/dist/client/assets/{dist-mPR8uuzk.js → dist-D-LtG92p.js} +1 -1
- package/dist/client/assets/{dist-DeZsNbFx.js → dist-DGhdkud4.js} +1 -1
- package/dist/client/assets/{dist-BkVaPA_q.js → dist-DL4Q05ts.js} +1 -1
- package/dist/client/assets/{dist-CNuKauOq.js → dist-D_umYm0o.js} +1 -1
- package/dist/client/assets/{dist-CubYGjAk.js → dist-DbovsYeH.js} +1 -1
- package/dist/client/assets/{dist-C46474ys.js → dist-DcdzfM-n.js} +1 -1
- package/dist/client/assets/{dist-BoeM5xW7.js → dist-DdVs8Y0X.js} +1 -1
- package/dist/client/assets/{dist-DHBWUQhM.js → dist-MC2XuZgq.js} +1 -1
- package/dist/client/assets/{dist-D0Iw5XQA.js → dist-b9CfAgzG.js} +1 -1
- package/dist/client/assets/{dist-lZUWPQbT.js → dist-j9X7j381.js} +1 -1
- package/dist/client/assets/{dist-CCkdCH6a.js → dist-k6bOz9RS.js} +1 -1
- package/dist/client/assets/{dist-Dk3e6RfY.js → dist-ufRHdQ8G.js} +1 -1
- package/dist/client/assets/{index-Dyta-HkQ.js → index-DVmE8H4Y.js} +12 -12
- package/dist/client/assets/index-FxTrUapG.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 +76 -8
- package/dist/server/application/catalog-service.js +8 -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/http-server.d.ts +0 -3
- package/dist/server/http-server.js +131 -87
- package/dist/server/model-filter.d.ts +13 -3
- package/dist/server/model-filter.js +7 -5
- package/dist/server/store.d.ts +27 -0
- package/dist/server/store.js +44 -0
- package/dist/server/subagents.js +10 -2
- package/package.json +9 -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();
|
|
@@ -132,13 +139,19 @@ export class ActiveMemory {
|
|
|
132
139
|
if (this.indexedWorkspaces.has(workspaceId))
|
|
133
140
|
return;
|
|
134
141
|
const index = new Map();
|
|
135
|
-
|
|
142
|
+
const store = this.store;
|
|
143
|
+
const conversations = store.conversationsForWorkspace
|
|
144
|
+
? store.conversationsForWorkspace(workspaceId)
|
|
145
|
+
: store.snapshot().conversations.filter(c => c.workspaceId === workspaceId);
|
|
146
|
+
for (const conversation of conversations)
|
|
136
147
|
for (const message of conversation.messages) {
|
|
137
148
|
if (message.role !== "user" && message.role !== "assistant")
|
|
138
149
|
continue;
|
|
139
150
|
const value = { conversationId: conversation.id, title: conversation.title, messageId: message.id, role: message.role, content: message.content };
|
|
140
151
|
this.indexedMessages.add(message.id);
|
|
141
|
-
|
|
152
|
+
const terms = message.content.toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? [];
|
|
153
|
+
this.termsByMessage.set(message.id, { workspaceId, terms });
|
|
154
|
+
for (const word of terms) {
|
|
142
155
|
let bucket = index.get(word);
|
|
143
156
|
if (!bucket)
|
|
144
157
|
index.set(word, bucket = new Map());
|
|
@@ -149,7 +162,7 @@ export class ActiveMemory {
|
|
|
149
162
|
this.indexedWorkspaces.add(workspaceId);
|
|
150
163
|
}
|
|
151
164
|
/**
|
|
152
|
-
* Drops
|
|
165
|
+
* Drops exactly the given messages from the search index, leaving the rest of the workspace indexed.
|
|
153
166
|
*
|
|
154
167
|
* WHY THIS EXISTS. `ensureIndex` builds once per workspace and `addToIndex` only ever adds, so the
|
|
155
168
|
* index outlives the messages it indexed: a revert removes messages from the store while the index
|
|
@@ -158,17 +171,54 @@ export class ActiveMemory {
|
|
|
158
171
|
* history" after a revert every file-level assertion proved had really truncated. The store is the
|
|
159
172
|
* source of truth; the index is a cache of it, and a cache that survives an event that invalidated
|
|
160
173
|
* its source is wrong by definition.
|
|
174
|
+
*
|
|
175
|
+
* WHY SURGICAL. The caller (revert) knows precisely which message ids the store dropped, and this
|
|
176
|
+
* method trusts that list rather than re-deriving it: the ids not in `termsByMessage` were never
|
|
177
|
+
* indexed (no search had built the index, or a role the index skips), so a no-op for them is
|
|
178
|
+
* correct, and the next search - if the index was never built - builds from the post-revert store
|
|
179
|
+
* anyway. Dropping the whole workspace index instead would force the next search to rebuild every
|
|
180
|
+
* surviving conversation's entries for no reason, and the recall gap in between would answer
|
|
181
|
+
* searches with nothing at all.
|
|
161
182
|
*/
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
183
|
+
removeFromIndex(messageIds) {
|
|
184
|
+
for (const messageId of messageIds) {
|
|
185
|
+
const entry = this.termsByMessage.get(messageId);
|
|
186
|
+
if (!entry)
|
|
187
|
+
continue;
|
|
188
|
+
const terms = this.inverted.get(entry.workspaceId);
|
|
189
|
+
if (terms) {
|
|
190
|
+
for (const term of entry.terms) {
|
|
191
|
+
const bucket = terms.get(term);
|
|
192
|
+
if (!bucket)
|
|
193
|
+
continue;
|
|
194
|
+
bucket.delete(messageId);
|
|
195
|
+
if (!bucket.size)
|
|
196
|
+
terms.delete(term);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
this.termsByMessage.delete(messageId);
|
|
200
|
+
this.indexedMessages.delete(messageId);
|
|
201
|
+
}
|
|
165
202
|
}
|
|
166
203
|
addToIndex(conversation, message) {
|
|
167
204
|
if (!this.indexedWorkspaces.has(conversation.workspaceId) || (message.role !== "user" && message.role !== "assistant"))
|
|
168
205
|
return;
|
|
169
206
|
let terms = this.inverted.get(conversation.workspaceId);
|
|
170
207
|
const value = { conversationId: conversation.id, title: conversation.title, messageId: message.id, role: message.role, content: message.content };
|
|
171
|
-
|
|
208
|
+
const messageTerms = message.content.toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? [];
|
|
209
|
+
// A REFRESH MUST ALSO FORGET THE OLD TERMS. The buckets map a term to the message that contains
|
|
210
|
+
// it, and a term that only the OLD content had would keep pointing at this message with a stale
|
|
211
|
+
// copy - so a search for a word the model has since dropped would still hand the reader the old
|
|
212
|
+
// text. The terms this message had are remembered exactly so they can be dropped here.
|
|
213
|
+
const previous = this.termsByMessage.get(message.id);
|
|
214
|
+
if (previous)
|
|
215
|
+
for (const term of previous.terms) {
|
|
216
|
+
const bucket = terms.get(term);
|
|
217
|
+
if (bucket)
|
|
218
|
+
bucket.delete(message.id);
|
|
219
|
+
}
|
|
220
|
+
this.termsByMessage.set(message.id, { workspaceId: conversation.workspaceId, terms: messageTerms });
|
|
221
|
+
for (const term of messageTerms) {
|
|
172
222
|
let bucket = terms.get(term);
|
|
173
223
|
if (!bucket)
|
|
174
224
|
terms.set(term, bucket = new Map());
|
|
@@ -270,6 +320,21 @@ export class ActiveMemory {
|
|
|
270
320
|
this.cancel(id); }
|
|
271
321
|
async waitForIdle() { await Promise.allSettled([...this.workspaceReviews.values()]); await this.serial; }
|
|
272
322
|
async completed(conversation) {
|
|
323
|
+
// REFRESH THIS TURN'S MESSAGES, DO NOT JUST FILL IN THE UNSEEN ONES.
|
|
324
|
+
//
|
|
325
|
+
// The index is a CACHE of the store's content, and an assistant message's content changes while
|
|
326
|
+
// the model is still writing it: the entry was built from the empty placeholder the turn opened
|
|
327
|
+
// with, so it holds the answer BEFORE the answer. "I have seen this message" is not "I have seen
|
|
328
|
+
// its final content" - treating the two as the same froze this turn's answer in the index, and a
|
|
329
|
+
// frozen answer is an answer that can never be recalled (measured by
|
|
330
|
+
// scripts/active-memory-index-content.test.mjs: the recall came back `undefined`).
|
|
331
|
+
//
|
|
332
|
+
// Only THIS TURN's messages are refreshed - the ones after the last user message. The rest of the
|
|
333
|
+
// transcript was indexed when it was final, and re-tokenizing a whole conversation on every turn
|
|
334
|
+
// end is exactly the cost this file is about.
|
|
335
|
+
const lastUserIndex = conversation.messages.map((message) => message.role).lastIndexOf("user");
|
|
336
|
+
for (const message of lastUserIndex >= 0 ? conversation.messages.slice(lastUserIndex + 1) : conversation.messages)
|
|
337
|
+
this.addToIndex(conversation, message);
|
|
273
338
|
for (const message of conversation.messages)
|
|
274
339
|
if (!this.indexedMessages.has(message.id))
|
|
275
340
|
this.addToIndex(conversation, message);
|
|
@@ -301,7 +366,10 @@ export class ActiveMemory {
|
|
|
301
366
|
const settings = await this.configuration();
|
|
302
367
|
if (!settings.enabled || !settings.reviewInterval)
|
|
303
368
|
return;
|
|
304
|
-
const
|
|
369
|
+
const store = this.store;
|
|
370
|
+
const workspace = store.workspace
|
|
371
|
+
? store.workspace(snapshot.workspaceId)
|
|
372
|
+
: store.snapshot().workspaces.find(w => w.id === snapshot.workspaceId);
|
|
305
373
|
if (!workspace)
|
|
306
374
|
return;
|
|
307
375
|
const agent = await this.runtime.agentFor?.(snapshot);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertModelAllowed
|
|
1
|
+
import { assertModelAllowed } from "../model-filter.js";
|
|
2
2
|
import { readAgents, resolveAgentDirectories } from "../agents.js";
|
|
3
3
|
import { loadPromptTemplateSummaries } from "../pi-session.js";
|
|
4
4
|
function summarize(agent) {
|
|
@@ -75,10 +75,12 @@ export class CatalogService {
|
|
|
75
75
|
const catalog = await this.runtime.catalog(selection);
|
|
76
76
|
if (!conversationId || !this.store)
|
|
77
77
|
return catalog;
|
|
78
|
-
const
|
|
79
|
-
|
|
78
|
+
const workspace = this.store.workspaceForConversation(conversationId);
|
|
79
|
+
if (!workspace)
|
|
80
|
+
throw new Error("会话所属工作区不存在");
|
|
81
|
+
const rules = this.store.modelFilterRules();
|
|
80
82
|
return { ...catalog, models: catalog.models.filter(model => { try {
|
|
81
|
-
assertModelAllowed(
|
|
83
|
+
assertModelAllowed(rules, workspace, model);
|
|
82
84
|
return true;
|
|
83
85
|
}
|
|
84
86
|
catch {
|
|
@@ -108,7 +110,7 @@ export class CatalogService {
|
|
|
108
110
|
throw new Error("Store unavailable");
|
|
109
111
|
await this.store.setModelFilterRules(rules);
|
|
110
112
|
this.broadcaster.broadcastState();
|
|
111
|
-
for (const conversation of this.store.
|
|
113
|
+
for (const conversation of this.store.catalogConversationTargets()) {
|
|
112
114
|
if (!conversation.archivedAt) {
|
|
113
115
|
try {
|
|
114
116
|
await this.broadcastCatalog(conversation.model, conversation.id);
|
|
@@ -182,7 +184,7 @@ export class CatalogService {
|
|
|
182
184
|
const cached = this.promptCatalogs.get(workspaceId);
|
|
183
185
|
if (cached && cached.expiresAt > Date.now())
|
|
184
186
|
return cached.catalog;
|
|
185
|
-
const workspace = this.store?.
|
|
187
|
+
const workspace = this.store?.workspace(workspaceId);
|
|
186
188
|
if (!workspace)
|
|
187
189
|
return { source: "unavailable", prompts: [], workspaceId, fetchedAt: new Date().toISOString(), error: "workspace not found" };
|
|
188
190
|
try {
|
|
@@ -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 {
|
|
@@ -30,7 +30,4 @@ export interface HttpServerOptions {
|
|
|
30
30
|
context: ServerContext;
|
|
31
31
|
onShutdown: (reason: string) => Promise<void>;
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Creates the HTTP server providing static client assets and all `/api/*` endpoints.
|
|
35
|
-
*/
|
|
36
33
|
export declare function createHttpServer(options: HttpServerOptions): NodeHttpServer;
|