@evandu/super-pi 0.1.3 → 0.1.5
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/client/assets/{index-BhFeQ1E-.css → index-DP3T9VzH.css} +1 -1
- package/dist/client/assets/index-O9hPIaf6.js +43 -0
- package/dist/client/index.html +2 -2
- package/dist/server/application/catalog-service.d.ts +1 -1
- package/dist/server/application/catalog-service.js +2 -2
- package/dist/server/application/conversation-service.d.ts +6 -1
- package/dist/server/application/conversation-service.js +40 -15
- package/dist/server/application/run-service.js +6 -1
- package/dist/server/command-handler.js +17 -3
- package/dist/server/http-server.js +3 -1
- package/dist/server/index.js +3 -1
- package/dist/server/runtime.d.ts +5 -6
- package/dist/server/runtime.js +12 -12
- package/dist/server/store.d.ts +13 -4
- package/dist/server/store.js +47 -10
- package/dist/server/types.d.ts +23 -0
- package/dist/server/version.d.ts +10 -0
- package/dist/server/version.js +39 -0
- package/dist/server/ws-server.d.ts +2 -0
- package/dist/server/ws-server.js +12 -0
- package/package.json +5 -3
- package/dist/client/assets/index-D_VwFuME.js +0 -43
package/dist/client/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<meta name="theme-color" content="#101828" />
|
|
7
7
|
<title>Super Pi</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-O9hPIaf6.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DP3T9VzH.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
|
@@ -36,7 +36,7 @@ export declare class CatalogService {
|
|
|
36
36
|
/**
|
|
37
37
|
* Broadcasts the model catalog to connected clients.
|
|
38
38
|
*/
|
|
39
|
-
broadcastCatalog(selection?: ModelSelection): Promise<void>;
|
|
39
|
+
broadcastCatalog(selection?: ModelSelection, conversationId?: string): Promise<void>;
|
|
40
40
|
/**
|
|
41
41
|
* Reads one catalog filtered by mode at source.
|
|
42
42
|
*/
|
|
@@ -74,9 +74,9 @@ export class CatalogService {
|
|
|
74
74
|
/**
|
|
75
75
|
* Broadcasts the model catalog to connected clients.
|
|
76
76
|
*/
|
|
77
|
-
async broadcastCatalog(selection) {
|
|
77
|
+
async broadcastCatalog(selection, conversationId) {
|
|
78
78
|
const catalog = await this.getCatalog(selection);
|
|
79
|
-
this.broadcaster.broadcast({ type: "model.catalog", catalog });
|
|
79
|
+
this.broadcaster.broadcast({ type: "model.catalog", catalog, conversationId });
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
82
|
* Reads one catalog filtered by mode at source.
|
|
@@ -37,6 +37,7 @@ export declare class ConversationService {
|
|
|
37
37
|
/**
|
|
38
38
|
* Selects a conversation and broadcasts the catalog for that conversation's model.
|
|
39
39
|
*/
|
|
40
|
+
getConversationCatalog(conversationId: string): Promise<import("../types.js").ModelCatalog>;
|
|
40
41
|
selectConversation(conversationId: string): Promise<void>;
|
|
41
42
|
/**
|
|
42
43
|
* Archives a conversation.
|
|
@@ -62,11 +63,15 @@ export declare class ConversationService {
|
|
|
62
63
|
* Configures or clears the primary agent assigned to a conversation.
|
|
63
64
|
*/
|
|
64
65
|
setAgent(conversationId: string, agentId: string | null): Promise<void>;
|
|
66
|
+
private resolveAgentConfiguration;
|
|
65
67
|
/**
|
|
66
68
|
* Reverts conversation history to a designated message boundary, truncating both
|
|
67
69
|
* Pi's transcript and state.json.
|
|
68
70
|
*/
|
|
69
|
-
revertMessage(conversationId: string, messageId: string): Promise<
|
|
71
|
+
revertMessage(conversationId: string, messageId: string, withdraw?: boolean): Promise<{
|
|
72
|
+
content: string;
|
|
73
|
+
images: import("../types.js").MessageImage[] | undefined;
|
|
74
|
+
} | undefined>;
|
|
70
75
|
/**
|
|
71
76
|
* Forks an existing conversation into a new parallel branch, cloning the Pi session.
|
|
72
77
|
*/
|
|
@@ -29,15 +29,27 @@ export class ConversationService {
|
|
|
29
29
|
* Creates a new conversation in a workspace.
|
|
30
30
|
*/
|
|
31
31
|
async createConversation(workspaceId) {
|
|
32
|
-
|
|
32
|
+
const workspace = this.store.snapshot().workspaces.find(item => item.id === workspaceId);
|
|
33
|
+
if (!workspace)
|
|
34
|
+
throw new Error("Workspace not found");
|
|
35
|
+
const defaults = workspace.defaultAgentId
|
|
36
|
+
? await this.resolveAgentConfiguration({ id: workspaceId, model: workspace.defaultModel, thinkingLevel: workspace.defaultThinkingLevel }, workspace.defaultAgentId)
|
|
37
|
+
: undefined;
|
|
38
|
+
await this.store.createConversation(workspaceId, defaults);
|
|
33
39
|
this.broadcaster.broadcastState();
|
|
34
40
|
}
|
|
35
41
|
/**
|
|
36
42
|
* Selects a conversation and broadcasts the catalog for that conversation's model.
|
|
37
43
|
*/
|
|
44
|
+
async getConversationCatalog(conversationId) {
|
|
45
|
+
const conversation = this.store.snapshot().conversations.find(item => item.id === conversationId);
|
|
46
|
+
if (!conversation)
|
|
47
|
+
throw new Error("Conversation not found");
|
|
48
|
+
return this.catalogService.getCatalog(conversation.model);
|
|
49
|
+
}
|
|
38
50
|
async selectConversation(conversationId) {
|
|
39
51
|
const conversation = this.store.snapshot().conversations.find((item) => item.id === conversationId);
|
|
40
|
-
await this.catalogService.broadcastCatalog(conversation?.model);
|
|
52
|
+
await this.catalogService.broadcastCatalog(conversation?.model, conversationId);
|
|
41
53
|
}
|
|
42
54
|
/**
|
|
43
55
|
* Archives a conversation.
|
|
@@ -76,7 +88,7 @@ export class ConversationService {
|
|
|
76
88
|
: { provider, modelId, label: modelId };
|
|
77
89
|
await this.store.setConversationModel(conversationId, selection);
|
|
78
90
|
this.broadcaster.broadcastState();
|
|
79
|
-
await this.catalogService.broadcastCatalog(selection);
|
|
91
|
+
await this.catalogService.broadcastCatalog(selection, conversationId);
|
|
80
92
|
}
|
|
81
93
|
/**
|
|
82
94
|
* Sets the thinking level on a conversation, validating against the model's available levels.
|
|
@@ -102,42 +114,47 @@ export class ConversationService {
|
|
|
102
114
|
this.broadcaster.broadcastState();
|
|
103
115
|
return;
|
|
104
116
|
}
|
|
117
|
+
const settings = await this.resolveAgentConfiguration(conversation, agentId);
|
|
118
|
+
await this.store.setConversationConfiguration(conversationId, settings);
|
|
119
|
+
this.broadcaster.broadcastState();
|
|
120
|
+
await this.catalogService.broadcastCatalog(this.store.snapshot().conversations.find((item) => item.id === conversationId)?.model, conversationId);
|
|
121
|
+
}
|
|
122
|
+
async resolveAgentConfiguration(conversation, agentId) {
|
|
105
123
|
const agent = await this.runtime.agentFor?.({ id: conversation.id, agentId });
|
|
106
124
|
if (!agent) {
|
|
107
125
|
const dirsLabel = Array.isArray(this.agentsDir) ? this.agentsDir.join(", ") : this.agentsDir;
|
|
108
126
|
throw new Error(`Agent ${JSON.stringify(agentId)} 不存在或不是可用的 primary agent(目录:${dirsLabel})`);
|
|
109
127
|
}
|
|
128
|
+
let selection = conversation.model;
|
|
110
129
|
if (agent.model) {
|
|
111
130
|
const catalog = await this.runtime.catalog();
|
|
112
131
|
if (catalog.source === "pi" && !catalog.models.some((model) => model.provider === agent.model?.provider && model.modelId === agent.model?.modelId)) {
|
|
113
132
|
throw this.catalogService.notOffered(`Agent ${agent.name} 指定的模型 ${agent.model.provider}/${agent.model.modelId}`, catalog);
|
|
114
133
|
}
|
|
115
134
|
const option = catalog.models.find((model) => model.provider === agent.model?.provider && model.modelId === agent.model?.modelId);
|
|
116
|
-
|
|
135
|
+
selection = option
|
|
117
136
|
? { provider: option.provider, modelId: option.modelId, label: option.label }
|
|
118
|
-
: { provider: agent.model.provider, modelId: agent.model.modelId, label: `${agent.model.provider}/${agent.model.modelId}` }
|
|
137
|
+
: { provider: agent.model.provider, modelId: agent.model.modelId, label: `${agent.model.provider}/${agent.model.modelId}` };
|
|
119
138
|
}
|
|
120
139
|
if (agent.thinkingLevel) {
|
|
121
|
-
const catalog = await this.runtime.catalog(
|
|
140
|
+
const catalog = await this.runtime.catalog(selection);
|
|
122
141
|
if (catalog.source === "pi" && !catalog.levels.includes(agent.thinkingLevel)) {
|
|
123
142
|
throw this.catalogService.notOffered(`Agent ${agent.name} 指定的思考等级 ${agent.thinkingLevel}`, catalog);
|
|
124
143
|
}
|
|
125
|
-
await this.store.setConversationThinking(conversationId, agent.thinkingLevel);
|
|
126
144
|
}
|
|
127
|
-
|
|
128
|
-
this.broadcaster.broadcastState();
|
|
129
|
-
await this.catalogService.broadcastCatalog(this.store.snapshot().conversations.find((item) => item.id === conversationId)?.model);
|
|
145
|
+
return { agentId: agent.id, model: selection, thinkingLevel: agent.thinkingLevel ?? conversation.thinkingLevel };
|
|
130
146
|
}
|
|
131
147
|
/**
|
|
132
148
|
* Reverts conversation history to a designated message boundary, truncating both
|
|
133
149
|
* Pi's transcript and state.json.
|
|
134
150
|
*/
|
|
135
|
-
async revertMessage(conversationId, messageId) {
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
async revertMessage(conversationId, messageId, withdraw = false) {
|
|
152
|
+
let plan = this.store.planRevert(conversationId, messageId, withdraw);
|
|
153
|
+
let conversation = this.store.snapshot().conversations.find((item) => item.id === conversationId);
|
|
138
154
|
if (!conversation)
|
|
139
155
|
throw new Error("会话不存在");
|
|
140
|
-
const
|
|
156
|
+
const workspaceId = conversation.workspaceId;
|
|
157
|
+
const workspace = this.store.snapshot().workspaces.find((item) => item.id === workspaceId);
|
|
141
158
|
if (!workspace)
|
|
142
159
|
throw new Error(`会话所属的工作区 ${conversation.workspaceId} 已不存在,拒绝回退`);
|
|
143
160
|
if (!this.runtime.revert) {
|
|
@@ -146,6 +163,11 @@ export class ConversationService {
|
|
|
146
163
|
if (this.runService.isRunning(conversationId)) {
|
|
147
164
|
this.extensionUiService.cancelPendingUi(conversationId, "aborted");
|
|
148
165
|
await this.runService.stopRun(conversationId);
|
|
166
|
+
// The stopping turn may have recorded the user's Pi entry while it drained.
|
|
167
|
+
plan = this.store.planRevert(conversationId, messageId, withdraw);
|
|
168
|
+
conversation = this.store.snapshot().conversations.find(item => item.id === conversationId);
|
|
169
|
+
if (!conversation)
|
|
170
|
+
throw new Error("会话不存在");
|
|
149
171
|
}
|
|
150
172
|
for (const runId of plan.doomedRunIds) {
|
|
151
173
|
if (this.subagents.stop(runId)) {
|
|
@@ -156,12 +178,15 @@ export class ConversationService {
|
|
|
156
178
|
conversation,
|
|
157
179
|
cwd: workspace.path,
|
|
158
180
|
boundaryEntryId: plan.boundaryEntryId,
|
|
181
|
+
beforeBoundary: withdraw && Boolean(conversation.messages.find(item => item.id === messageId)?.piEntryId),
|
|
159
182
|
});
|
|
160
|
-
const applied = await this.store.revertMessages(conversationId, messageId);
|
|
183
|
+
const applied = await this.store.revertMessages(conversationId, messageId, withdraw);
|
|
161
184
|
console.log(`Super Pi: reverted conversation ${conversationId} to ${applied.boundaryIndex + 1} message(s): ` +
|
|
162
185
|
`${plan.removedMessageIds.length} message(s) and ${applied.doomedRunIds.length} sub-agent run(s) dropped from state.json; ` +
|
|
163
186
|
`Pi's file ${outcome.sessionFile} kept ${outcome.linesKept} line(s) and lost ${outcome.entriesDropped}.`);
|
|
164
187
|
this.broadcaster.broadcastState();
|
|
188
|
+
const withdrawn = conversation.messages.find(item => item.id === messageId);
|
|
189
|
+
return withdraw && withdrawn ? { content: withdrawn.content, images: withdrawn.images } : undefined;
|
|
165
190
|
}
|
|
166
191
|
/**
|
|
167
192
|
* Forks an existing conversation into a new parallel branch, cloning the Pi session.
|
|
@@ -96,8 +96,13 @@ export class RunService {
|
|
|
96
96
|
* Opens a turn by creating user and assistant messages in store.
|
|
97
97
|
*/
|
|
98
98
|
async openTurn(conversationId, prompt, images) {
|
|
99
|
+
const conversation = this.store.snapshot().conversations.find(item => item.id === conversationId);
|
|
100
|
+
if (!conversation)
|
|
101
|
+
throw new Error("Conversation not found");
|
|
102
|
+
const agent = await this.runtime.agentFor?.(conversation);
|
|
103
|
+
const identity = agent ? { id: agent.id, name: agent.name } : { name: "Pi" };
|
|
99
104
|
const user = await this.store.addUserMessage(conversationId, prompt ?? "", images);
|
|
100
|
-
const assistant = await this.store.beginAssistantMessage(conversationId);
|
|
105
|
+
const assistant = await this.store.beginAssistantMessage(conversationId, identity);
|
|
101
106
|
this.broadcaster.broadcastState();
|
|
102
107
|
return { user, assistant };
|
|
103
108
|
}
|
|
@@ -43,7 +43,10 @@ export class CommandHandler {
|
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
if (command.type === "conversation.select") {
|
|
46
|
-
|
|
46
|
+
if (sendToSocket)
|
|
47
|
+
sendToSocket({ type: "model.catalog", catalog: await this.conversationService.getConversationCatalog(command.conversationId), conversationId: command.conversationId });
|
|
48
|
+
else
|
|
49
|
+
await this.conversationService.selectConversation(command.conversationId);
|
|
47
50
|
return;
|
|
48
51
|
}
|
|
49
52
|
if (command.type === "conversation.archive") {
|
|
@@ -94,7 +97,15 @@ export class CommandHandler {
|
|
|
94
97
|
return;
|
|
95
98
|
}
|
|
96
99
|
if (command.type === "message.revert") {
|
|
97
|
-
|
|
100
|
+
try {
|
|
101
|
+
const withdrawn = await this.conversationService.revertMessage(command.conversationId, command.messageId, command.withdraw);
|
|
102
|
+
sendToSocket?.({ type: "message.revert.result", conversationId: command.conversationId, ok: true, withdrawn });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (!sendToSocket)
|
|
106
|
+
throw error;
|
|
107
|
+
sendToSocket({ type: "message.revert.result", conversationId: command.conversationId, ok: false, message: error instanceof Error ? error.message : String(error) });
|
|
108
|
+
}
|
|
98
109
|
return;
|
|
99
110
|
}
|
|
100
111
|
if (command.type === "run.stop") {
|
|
@@ -125,7 +136,10 @@ export class CommandHandler {
|
|
|
125
136
|
return;
|
|
126
137
|
}
|
|
127
138
|
if (command.type === "catalog.fetch") {
|
|
128
|
-
|
|
139
|
+
if (sendToSocket)
|
|
140
|
+
sendToSocket({ type: "model.catalog", catalog: await this.catalogService.getCatalog() });
|
|
141
|
+
else
|
|
142
|
+
await this.catalogService.broadcastCatalog();
|
|
129
143
|
return;
|
|
130
144
|
}
|
|
131
145
|
if (command.type === "agent.catalog.fetch") {
|
|
@@ -4,6 +4,7 @@ import { createServer } from "node:http";
|
|
|
4
4
|
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { describe as describeFilesError, fileMeta, listDirectory, MAX_FILE_BYTES, readWorkspaceFile, resolveInsideRoot, searchByName, toRelative, writeWorkspaceFile, } from "./files.js";
|
|
6
6
|
import { findUploadFile, MAX_UPLOAD_BODY_BYTES, saveUpload } from "./upload.js";
|
|
7
|
+
import { checkDiskVersion } from "./version.js";
|
|
7
8
|
const MIME_TYPES = {
|
|
8
9
|
".css": "text/css; charset=utf-8",
|
|
9
10
|
".html": "text/html; charset=utf-8",
|
|
@@ -51,8 +52,9 @@ export function createHttpServer(options) {
|
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
54
|
if (url.pathname === "/api/health") {
|
|
55
|
+
const version = checkDiskVersion(rootDir);
|
|
54
56
|
response.writeHead(200, { "content-type": "application/json; charset=utf-8" })
|
|
55
|
-
.end(JSON.stringify({ status: "ok", service: "super-pi" }));
|
|
57
|
+
.end(JSON.stringify({ status: "ok", service: "super-pi", ...version }));
|
|
56
58
|
return;
|
|
57
59
|
}
|
|
58
60
|
if (url.pathname === "/api/subagent/dispatch") {
|
package/dist/server/index.js
CHANGED
|
@@ -17,8 +17,10 @@ import { WorkspaceService } from "./application/workspace-service.js";
|
|
|
17
17
|
import { CommandHandler } from "./command-handler.js";
|
|
18
18
|
import { createHttpServer } from "./http-server.js";
|
|
19
19
|
import { WsBroadcaster, WsServer } from "./ws-server.js";
|
|
20
|
+
import { initRunningVersion } from "./version.js";
|
|
20
21
|
// ------------------------------------------------------------- Environment & paths
|
|
21
22
|
const rootDir = resolve(import.meta.dirname, "../..");
|
|
23
|
+
initRunningVersion(rootDir);
|
|
22
24
|
const port = Number.parseInt(process.env.SUPER_PI_PORT ?? "3080", 10);
|
|
23
25
|
const host = process.env.SUPER_PI_HOST ?? "127.0.0.1";
|
|
24
26
|
const defaultDataDir = !rootDir.includes("node_modules") && existsSync(join(rootDir, ".super-pi"))
|
|
@@ -135,7 +137,7 @@ process.on("SIGINT", () => shutdown("SIGINT"));
|
|
|
135
137
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
136
138
|
// ------------------------------------------------------------- Transports & startup
|
|
137
139
|
const httpServer = createHttpServer({ context: serverContext, onShutdown: shutdown });
|
|
138
|
-
const wsServer = new WsServer({ httpServer, broadcaster, commandHandler, catalogService, accessToken });
|
|
140
|
+
const wsServer = new WsServer({ httpServer, broadcaster, commandHandler, catalogService, accessToken, rootDir });
|
|
139
141
|
httpServer.listen(port, host, () => {
|
|
140
142
|
console.log(`Super Pi is running at http://${host}:${port}`);
|
|
141
143
|
});
|
package/dist/server/runtime.d.ts
CHANGED
|
@@ -112,6 +112,7 @@ export type RevertRequest = {
|
|
|
112
112
|
cwd: string;
|
|
113
113
|
/** Pi's entry id of the last message that SURVIVES. Everything written after it is dropped. */
|
|
114
114
|
boundaryEntryId: string;
|
|
115
|
+
beforeBoundary?: boolean;
|
|
115
116
|
};
|
|
116
117
|
export type RevertOutcome = {
|
|
117
118
|
/** The file that was truncated, so a caller can log what was rewritten. */
|
|
@@ -439,7 +440,7 @@ export declare function killTree(pid: number): Promise<void>;
|
|
|
439
440
|
* EXPORTED so it can be tested against a fabricated session file in a temp directory, without Pi and
|
|
440
441
|
* without touching anybody's real transcript.
|
|
441
442
|
*/
|
|
442
|
-
export declare function truncateSessionFile(path: string, boundaryEntryId: string, expectedSessionId: string): Promise<{
|
|
443
|
+
export declare function truncateSessionFile(path: string, boundaryEntryId: string, expectedSessionId: string, beforeBoundary?: boolean): Promise<{
|
|
443
444
|
linesKept: number;
|
|
444
445
|
entriesDropped: number;
|
|
445
446
|
}>;
|
|
@@ -620,10 +621,8 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
620
621
|
/**
|
|
621
622
|
* The agent a conversation runs as, or undefined for Pi's own defaults.
|
|
622
623
|
*
|
|
623
|
-
* A
|
|
624
|
-
*
|
|
625
|
-
* `subagent` definition is also ignored here - those are invoked BY a primary agent, never chosen
|
|
626
|
-
* as the conversation's own role.
|
|
624
|
+
* A selected role must remain valid. Missing or invalid definitions are refused before
|
|
625
|
+
* spawning Pi so their prompt and tool restrictions cannot silently disappear.
|
|
627
626
|
*/
|
|
628
627
|
agentFor(conversation: Pick<Conversation, "id" | "agentId">): Promise<AgentDefinition | undefined>;
|
|
629
628
|
setAgentsDir(dir: string | string[]): void;
|
|
@@ -713,7 +712,7 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
713
712
|
* revert is a rare, deliberate act, and a second Pi answers in well under a second. It buys the
|
|
714
713
|
* elimination of an entire class of "we truncated the wrong file and nothing said so" failures.
|
|
715
714
|
*/
|
|
716
|
-
revert({ conversation, cwd, boundaryEntryId }: RevertRequest): Promise<RevertOutcome>;
|
|
715
|
+
revert({ conversation, cwd, boundaryEntryId, beforeBoundary }: RevertRequest): Promise<RevertOutcome>;
|
|
717
716
|
/**
|
|
718
717
|
* Copies Pi's own transcript for one conversation into a second, independent file under a new id.
|
|
719
718
|
*
|
package/dist/server/runtime.js
CHANGED
|
@@ -298,7 +298,7 @@ export function killTree(pid) {
|
|
|
298
298
|
* EXPORTED so it can be tested against a fabricated session file in a temp directory, without Pi and
|
|
299
299
|
* without touching anybody's real transcript.
|
|
300
300
|
*/
|
|
301
|
-
export async function truncateSessionFile(path, boundaryEntryId, expectedSessionId) {
|
|
301
|
+
export async function truncateSessionFile(path, boundaryEntryId, expectedSessionId, beforeBoundary = false) {
|
|
302
302
|
const raw = await readFile(path, "utf8");
|
|
303
303
|
// Split, then drop the empty segment a trailing newline leaves behind. Probe A measured that the file
|
|
304
304
|
// DOES end with a newline, so `split` yields one empty tail element; keeping it would append a blank
|
|
@@ -337,9 +337,12 @@ export async function truncateSessionFile(path, boundaryEntryId, expectedSession
|
|
|
337
337
|
throw new Error(`Pi 的会话文件属于会话 ${JSON.stringify(parsed[0]?.id ?? null)},不是要回退的会话 ${JSON.stringify(expectedSessionId)}。拒绝截断别人的会话。`);
|
|
338
338
|
if (!raw.endsWith("\n"))
|
|
339
339
|
throw new Error(`Pi 的会话文件末尾没有换行,格式已经变了,拒绝回退:${path}`);
|
|
340
|
-
|
|
340
|
+
let boundary = boundaryEntryId === "" ? 0 : parsed.findIndex((entry, index) => index > 0 && entry?.id === boundaryEntryId);
|
|
341
341
|
if (boundary < 0)
|
|
342
342
|
throw new Error(`在这份 Pi 会话记录里找不到要保留的那条消息(entry ${JSON.stringify(boundaryEntryId)})。这条消息可能是更早的版本写的,Super Pi 当时没有记下它对应 Pi 的哪一条。`);
|
|
343
|
+
const expectedLastId = beforeBoundary ? parsed[boundary - 1]?.id : parsed[boundary]?.id;
|
|
344
|
+
if (beforeBoundary)
|
|
345
|
+
boundary -= 1;
|
|
343
346
|
// The `parentId` property probe A measured, enforced at the boundary that matters: every entry still
|
|
344
347
|
// being KEPT must point at something also being kept. A prefix that violates this is not a session,
|
|
345
348
|
// and writing it would produce a file Pi either rejects or silently misreads.
|
|
@@ -368,7 +371,7 @@ export async function truncateSessionFile(path, boundaryEntryId, expectedSession
|
|
|
368
371
|
catch {
|
|
369
372
|
lastEntry = undefined;
|
|
370
373
|
}
|
|
371
|
-
if (verified.length !== boundary + 1 || lastEntry?.id !==
|
|
374
|
+
if (verified.length !== boundary + 1 || lastEntry?.id !== expectedLastId) {
|
|
372
375
|
throw new Error(`截断后的 Pi 会话文件没有以要保留的那条消息结尾(写到 ${verified.length} 行,期望 ${boundary + 1} 行,末行 id=${JSON.stringify(lastEntry?.id ?? null)})。原始文件仍在 ${backupPath}。`);
|
|
373
376
|
}
|
|
374
377
|
return { linesKept: boundary + 1, entriesDropped: lines.length - (boundary + 1) };
|
|
@@ -619,21 +622,18 @@ export class PiRpcRuntime {
|
|
|
619
622
|
/**
|
|
620
623
|
* The agent a conversation runs as, or undefined for Pi's own defaults.
|
|
621
624
|
*
|
|
622
|
-
* A
|
|
623
|
-
*
|
|
624
|
-
* `subagent` definition is also ignored here - those are invoked BY a primary agent, never chosen
|
|
625
|
-
* as the conversation's own role.
|
|
625
|
+
* A selected role must remain valid. Missing or invalid definitions are refused before
|
|
626
|
+
* spawning Pi so their prompt and tool restrictions cannot silently disappear.
|
|
626
627
|
*/
|
|
627
628
|
async agentFor(conversation) {
|
|
628
629
|
if (!conversation.agentId)
|
|
629
630
|
return undefined;
|
|
630
|
-
const { agents
|
|
631
|
+
const { agents } = await readAgents(this.agents.dir);
|
|
631
632
|
const agent = agents.find((item) => item.id === conversation.agentId);
|
|
632
633
|
if (agent)
|
|
633
634
|
return agent;
|
|
634
635
|
const dirLabel = Array.isArray(this.agents.dir) ? this.agents.dir.join(", ") : this.agents.dir;
|
|
635
|
-
|
|
636
|
-
return undefined;
|
|
636
|
+
throw new Error(`Agent ${JSON.stringify(conversation.agentId)} 已失效或不是可用的 primary agent(目录:${dirLabel})。请重新选择 Agent,或选择“跟随 Pi 默认”后重试。`);
|
|
637
637
|
}
|
|
638
638
|
setAgentsDir(dir) {
|
|
639
639
|
this.agents.dir = dir;
|
|
@@ -1064,7 +1064,7 @@ export class PiRpcRuntime {
|
|
|
1064
1064
|
* revert is a rare, deliberate act, and a second Pi answers in well under a second. It buys the
|
|
1065
1065
|
* elimination of an entire class of "we truncated the wrong file and nothing said so" failures.
|
|
1066
1066
|
*/
|
|
1067
|
-
async revert({ conversation, cwd, boundaryEntryId }) {
|
|
1067
|
+
async revert({ conversation, cwd, boundaryEntryId, beforeBoundary }) {
|
|
1068
1068
|
if (this.runs.has(conversation.id))
|
|
1069
1069
|
throw new Error("这个会话还有一轮任务在跑,先停止它再回退");
|
|
1070
1070
|
if (!this.pi.found)
|
|
@@ -1120,7 +1120,7 @@ export class PiRpcRuntime {
|
|
|
1120
1120
|
// reported, because the alternative is a revert that appears to work and changes nothing.
|
|
1121
1121
|
if (!exists?.isFile())
|
|
1122
1122
|
throw new Error(`Pi 还没有为这个会话建立记录文件(${sessionFile}),没有可以回退的历史。`);
|
|
1123
|
-
const result = await truncateSessionFile(sessionFile, boundaryEntryId, conversation.id);
|
|
1123
|
+
const result = await truncateSessionFile(sessionFile, boundaryEntryId, conversation.id, beforeBoundary);
|
|
1124
1124
|
return { sessionFile, linesKept: result.linesKept, entriesDropped: result.entriesDropped };
|
|
1125
1125
|
}
|
|
1126
1126
|
/**
|
package/dist/server/store.d.ts
CHANGED
|
@@ -100,7 +100,11 @@ export declare class Store {
|
|
|
100
100
|
agentId?: string | null;
|
|
101
101
|
}): Promise<Workspace>;
|
|
102
102
|
deleteWorkspace(workspaceId: string): Promise<string[]>;
|
|
103
|
-
createConversation(workspaceId: string
|
|
103
|
+
createConversation(workspaceId: string, configuration?: {
|
|
104
|
+
agentId?: string;
|
|
105
|
+
model?: ModelSelection;
|
|
106
|
+
thinkingLevel?: string;
|
|
107
|
+
}): Promise<Conversation>;
|
|
104
108
|
/**
|
|
105
109
|
* Copies a conversation into a NEW one, under an id the caller has already minted.
|
|
106
110
|
*
|
|
@@ -195,7 +199,12 @@ export declare class Store {
|
|
|
195
199
|
* START of the next turn, so the turn in flight is unaffected.
|
|
196
200
|
*/
|
|
197
201
|
setConversationAgent(conversationId: string, agentId: string | undefined): Promise<Conversation>;
|
|
198
|
-
|
|
202
|
+
setConversationConfiguration(conversationId: string, settings: {
|
|
203
|
+
agentId?: string;
|
|
204
|
+
model?: ModelSelection;
|
|
205
|
+
thinkingLevel?: string;
|
|
206
|
+
}): Promise<Conversation>;
|
|
207
|
+
beginAssistantMessage(conversationId: string, agentIdentity?: Message["agentIdentity"]): Promise<Message>;
|
|
199
208
|
appendAssistantDelta(conversationId: string, messageId: string, delta: string): Promise<void>;
|
|
200
209
|
/**
|
|
201
210
|
* Appends one tool call to the message it belongs to, and returns the message's whole `toolCalls`
|
|
@@ -241,7 +250,7 @@ export declare class Store {
|
|
|
241
250
|
}): Promise<ToolCallBlock[]>;
|
|
242
251
|
setStatus(conversationId: string, status: Conversation["status"]): Promise<void>;
|
|
243
252
|
/** The plan a revert would carry out, without carrying it out. The server reads this to stop children first. */
|
|
244
|
-
planRevert(conversationId: string, messageId: string): RevertPlan;
|
|
253
|
+
planRevert(conversationId: string, messageId: string, withdraw?: boolean): RevertPlan;
|
|
245
254
|
/**
|
|
246
255
|
* Records which Pi entry a message became, as reported by Pi while it was still running.
|
|
247
256
|
*
|
|
@@ -269,7 +278,7 @@ export declare class Store {
|
|
|
269
278
|
* plan is recomputed here rather than passed in - two callers cannot disagree about what a message id
|
|
270
279
|
* means, and the recomputation is a pure scan of a small array.
|
|
271
280
|
*/
|
|
272
|
-
revertMessages(conversationId: string, messageId: string): Promise<RevertPlan>;
|
|
281
|
+
revertMessages(conversationId: string, messageId: string, withdraw?: boolean): Promise<RevertPlan>;
|
|
273
282
|
private requireConversation;
|
|
274
283
|
/**
|
|
275
284
|
* This conversation's waiting list, oldest first.
|
package/dist/server/store.js
CHANGED
|
@@ -363,7 +363,7 @@ function reconcileQueuedMessages(state) {
|
|
|
363
363
|
* own message for "that message is not in this conversation" - and a revert that silently did nothing is
|
|
364
364
|
* the failure this whole feature exists to avoid.
|
|
365
365
|
*/
|
|
366
|
-
function planRevert(state, conversationId, messageId) {
|
|
366
|
+
function planRevert(state, conversationId, messageId, withdraw = false) {
|
|
367
367
|
const conversation = state.conversations.find((item) => item?.id === conversationId);
|
|
368
368
|
if (!conversation)
|
|
369
369
|
throw new Error("会话不存在");
|
|
@@ -371,9 +371,11 @@ function planRevert(state, conversationId, messageId) {
|
|
|
371
371
|
if (boundaryIndex < 0)
|
|
372
372
|
throw new Error("要回退的那条消息不在这个会话里");
|
|
373
373
|
const boundary = conversation.messages[boundaryIndex];
|
|
374
|
-
if (!boundary?.piEntryId)
|
|
374
|
+
if (!withdraw && !boundary?.piEntryId)
|
|
375
375
|
throw new Error("这条消息没有对应的 Pi 记录,不能作为回退位置。请在它之后的任意一条消息上回退。");
|
|
376
|
-
|
|
376
|
+
if (withdraw && boundary.role !== "user")
|
|
377
|
+
throw new Error("Only user messages can be withdrawn");
|
|
378
|
+
const removed = conversation.messages.slice(boundaryIndex + (withdraw ? 0 : 1));
|
|
377
379
|
// Unparseable stamps are compared as 0, i.e. "older than everything", which keeps a corrupted run
|
|
378
380
|
// record ALIVE rather than deleting a child that may still be the user's most recent work. Deleting on a
|
|
379
381
|
// guess is the irreversible direction; keeping a stale record costs one card that startup reconciliation
|
|
@@ -382,7 +384,7 @@ function planRevert(state, conversationId, messageId) {
|
|
|
382
384
|
const doomedRunIds = state.subagentRuns
|
|
383
385
|
.filter((run) => run?.conversationId === conversationId && (Date.parse(run.startedAt) || 0) >= firstRemovedAt)
|
|
384
386
|
.map((run) => run.id);
|
|
385
|
-
return { boundaryIndex, boundaryEntryId: boundary.piEntryId, removedMessageIds: removed.map((item) => item.id), doomedRunIds };
|
|
387
|
+
return { boundaryIndex, boundaryEntryId: boundary.piEntryId ?? conversation.messages.slice(0, boundaryIndex).reverse().find(item => item.piEntryId)?.piEntryId ?? "", removedMessageIds: removed.map((item) => item.id), doomedRunIds };
|
|
386
388
|
}
|
|
387
389
|
/**
|
|
388
390
|
* Brings `state.todos` back to a shape the browser can render, on boot.
|
|
@@ -551,7 +553,7 @@ export class Store {
|
|
|
551
553
|
await this.persist();
|
|
552
554
|
return [...doomed];
|
|
553
555
|
}
|
|
554
|
-
async createConversation(workspaceId) {
|
|
556
|
+
async createConversation(workspaceId, configuration) {
|
|
555
557
|
const workspace = this.state.workspaces.find((item) => item.id === workspaceId);
|
|
556
558
|
if (!workspace)
|
|
557
559
|
throw new Error("Workspace not found");
|
|
@@ -567,6 +569,14 @@ export class Store {
|
|
|
567
569
|
conversation.thinkingLevel = workspace.defaultThinkingLevel;
|
|
568
570
|
if (workspace.defaultAgentId)
|
|
569
571
|
conversation.agentId = workspace.defaultAgentId;
|
|
572
|
+
if (configuration) {
|
|
573
|
+
if (configuration.model)
|
|
574
|
+
conversation.model = { ...configuration.model };
|
|
575
|
+
if (configuration.thinkingLevel)
|
|
576
|
+
conversation.thinkingLevel = configuration.thinkingLevel;
|
|
577
|
+
if (configuration.agentId)
|
|
578
|
+
conversation.agentId = configuration.agentId;
|
|
579
|
+
}
|
|
570
580
|
this.state.conversations.unshift(conversation);
|
|
571
581
|
await this.persist();
|
|
572
582
|
return structuredClone(conversation);
|
|
@@ -772,7 +782,34 @@ export class Store {
|
|
|
772
782
|
await this.persist();
|
|
773
783
|
return structuredClone(conversation);
|
|
774
784
|
}
|
|
775
|
-
async
|
|
785
|
+
async setConversationConfiguration(conversationId, settings) {
|
|
786
|
+
const conversation = this.requireConversation(conversationId);
|
|
787
|
+
const previous = { agentId: conversation.agentId, model: conversation.model, thinkingLevel: conversation.thinkingLevel };
|
|
788
|
+
const apply = (value) => {
|
|
789
|
+
if (value.agentId === undefined)
|
|
790
|
+
delete conversation.agentId;
|
|
791
|
+
else
|
|
792
|
+
conversation.agentId = value.agentId;
|
|
793
|
+
if (value.model === undefined)
|
|
794
|
+
delete conversation.model;
|
|
795
|
+
else
|
|
796
|
+
conversation.model = { provider: value.model.provider, modelId: value.model.modelId, label: value.model.label };
|
|
797
|
+
if (value.thinkingLevel === undefined)
|
|
798
|
+
delete conversation.thinkingLevel;
|
|
799
|
+
else
|
|
800
|
+
conversation.thinkingLevel = value.thinkingLevel;
|
|
801
|
+
};
|
|
802
|
+
apply(settings);
|
|
803
|
+
try {
|
|
804
|
+
await this.persist();
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
apply(previous);
|
|
808
|
+
throw error;
|
|
809
|
+
}
|
|
810
|
+
return structuredClone(conversation);
|
|
811
|
+
}
|
|
812
|
+
async beginAssistantMessage(conversationId, agentIdentity) { const conversation = this.requireConversation(conversationId); const message = { id: id("message"), role: "assistant", content: "", createdAt: now(), ...(agentIdentity ? { agentIdentity: structuredClone(agentIdentity) } : {}) }; conversation.messages.push(message); await this.persist(); return structuredClone(message); }
|
|
776
813
|
async appendAssistantDelta(conversationId, messageId, delta) { const conversation = this.requireConversation(conversationId); const message = conversation.messages.find((item) => item.id === messageId); if (!message)
|
|
777
814
|
throw new Error("Assistant message not found"); message.content += delta; conversation.updatedAt = now(); await this.persist(); }
|
|
778
815
|
/**
|
|
@@ -842,7 +879,7 @@ export class Store {
|
|
|
842
879
|
}
|
|
843
880
|
async setStatus(conversationId, status) { const conversation = this.requireConversation(conversationId); conversation.status = status; conversation.updatedAt = now(); await this.persist(); }
|
|
844
881
|
/** The plan a revert would carry out, without carrying it out. The server reads this to stop children first. */
|
|
845
|
-
planRevert(conversationId, messageId) { return planRevert(this.state, conversationId, messageId); }
|
|
882
|
+
planRevert(conversationId, messageId, withdraw = false) { return planRevert(this.state, conversationId, messageId, withdraw); }
|
|
846
883
|
/**
|
|
847
884
|
* Records which Pi entry a message became, as reported by Pi while it was still running.
|
|
848
885
|
*
|
|
@@ -879,10 +916,10 @@ export class Store {
|
|
|
879
916
|
* plan is recomputed here rather than passed in - two callers cannot disagree about what a message id
|
|
880
917
|
* means, and the recomputation is a pure scan of a small array.
|
|
881
918
|
*/
|
|
882
|
-
async revertMessages(conversationId, messageId) {
|
|
883
|
-
const plan = planRevert(this.state, conversationId, messageId);
|
|
919
|
+
async revertMessages(conversationId, messageId, withdraw = false) {
|
|
920
|
+
const plan = planRevert(this.state, conversationId, messageId, withdraw);
|
|
884
921
|
const conversation = this.requireConversation(conversationId);
|
|
885
|
-
conversation.messages.splice(plan.boundaryIndex + 1);
|
|
922
|
+
conversation.messages.splice(plan.boundaryIndex + (withdraw ? 0 : 1));
|
|
886
923
|
if (plan.doomedRunIds.length) {
|
|
887
924
|
const doomed = new Set(plan.doomedRunIds);
|
|
888
925
|
this.state.subagentRuns = this.state.subagentRuns.filter((run) => !doomed.has(run.id));
|
package/dist/server/types.d.ts
CHANGED
|
@@ -57,6 +57,10 @@ export type Message = {
|
|
|
57
57
|
piEntryId?: string;
|
|
58
58
|
toolCalls?: ToolCallBlock[];
|
|
59
59
|
images?: MessageImage[];
|
|
60
|
+
agentIdentity?: {
|
|
61
|
+
id?: string;
|
|
62
|
+
name: string;
|
|
63
|
+
};
|
|
60
64
|
};
|
|
61
65
|
/**
|
|
62
66
|
* ONE PARAMETER OF A TOOL CALL, as a call block lists it, and the boundary that keeps Pi's arguments out
|
|
@@ -746,6 +750,7 @@ export type ClientCommand = {
|
|
|
746
750
|
type: "message.revert";
|
|
747
751
|
conversationId: string;
|
|
748
752
|
messageId: string;
|
|
753
|
+
withdraw?: boolean;
|
|
749
754
|
}
|
|
750
755
|
/**
|
|
751
756
|
* Drops ONE queued message, by Super Pi's id.
|
|
@@ -827,7 +832,15 @@ export type ClientCommand = {
|
|
|
827
832
|
conversationId: string;
|
|
828
833
|
requestId: string;
|
|
829
834
|
} & ExtensionUiAnswer);
|
|
835
|
+
export type ServerVersionInfo = {
|
|
836
|
+
runningVersion: string;
|
|
837
|
+
diskVersion: string;
|
|
838
|
+
needsRestart: boolean;
|
|
839
|
+
};
|
|
830
840
|
export type ServerEvent = {
|
|
841
|
+
type: "server.version";
|
|
842
|
+
version: ServerVersionInfo;
|
|
843
|
+
} | {
|
|
831
844
|
type: "state";
|
|
832
845
|
state: AppState;
|
|
833
846
|
} | {
|
|
@@ -902,9 +915,19 @@ export type ServerEvent = {
|
|
|
902
915
|
conversationId: string;
|
|
903
916
|
requestId: string;
|
|
904
917
|
reason: ExtensionUiCloseReason;
|
|
918
|
+
} | {
|
|
919
|
+
type: "message.revert.result";
|
|
920
|
+
conversationId: string;
|
|
921
|
+
ok: boolean;
|
|
922
|
+
message?: string;
|
|
923
|
+
withdrawn?: {
|
|
924
|
+
content: string;
|
|
925
|
+
images?: MessageImage[];
|
|
926
|
+
};
|
|
905
927
|
} | {
|
|
906
928
|
type: "model.catalog";
|
|
907
929
|
catalog: ModelCatalog;
|
|
930
|
+
conversationId?: string;
|
|
908
931
|
} | {
|
|
909
932
|
type: "agent.catalog";
|
|
910
933
|
catalog: AgentCatalog;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ServerVersionInfo } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Initializes the version captured when this server process booted up.
|
|
4
|
+
*/
|
|
5
|
+
export declare function initRunningVersion(rootDir: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Checks whether the package.json version on disk has been updated beyond
|
|
8
|
+
* the version currently executing in this process.
|
|
9
|
+
*/
|
|
10
|
+
export declare function checkDiskVersion(rootDir: string): ServerVersionInfo;
|