@evandu/super-pi 0.0.0-stage → 0.1.1
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/README.md +79 -2
- package/agents/coder.md +10 -0
- package/agents/critic.md +20 -0
- package/agents/patcher.md +23 -0
- package/agents/planner.md +18 -0
- package/agents/reviewer.md +13 -0
- package/agents/scout.md +23 -0
- package/agents/super-pi-task/core.ts +485 -0
- package/agents/super-pi-task/index.ts +141 -0
- package/agents/verifier.md +21 -0
- package/bin/super-pi.js +17 -0
- package/dist/client/assets/index-BMQsEdio.js +24 -0
- package/dist/client/assets/index-CsbZzuMy.css +1 -0
- package/dist/client/index.html +14 -0
- package/dist/extension.d.ts +15 -0
- package/dist/extension.js +111 -0
- package/dist/server/agents.d.ts +177 -0
- package/dist/server/agents.js +484 -0
- package/dist/server/application/catalog-service.d.ts +44 -0
- package/dist/server/application/catalog-service.js +94 -0
- package/dist/server/application/conversation-service.d.ts +73 -0
- package/dist/server/application/conversation-service.js +202 -0
- package/dist/server/application/extension-ui-service.d.ts +28 -0
- package/dist/server/application/extension-ui-service.js +73 -0
- package/dist/server/application/run-service.d.ts +115 -0
- package/dist/server/application/run-service.js +341 -0
- package/dist/server/application/workspace-service.d.ts +45 -0
- package/dist/server/application/workspace-service.js +106 -0
- package/dist/server/command-handler.d.ts +22 -0
- package/dist/server/command-handler.js +137 -0
- package/dist/server/files.d.ts +202 -0
- package/dist/server/files.js +629 -0
- package/dist/server/http-server.d.ts +20 -0
- package/dist/server/http-server.js +313 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +103 -0
- package/dist/server/pi-command.d.ts +65 -0
- package/dist/server/pi-command.js +183 -0
- package/dist/server/runtime.d.ts +819 -0
- package/dist/server/runtime.js +1470 -0
- package/dist/server/server-context.d.ts +46 -0
- package/dist/server/server-context.js +1 -0
- package/dist/server/store.d.ts +424 -0
- package/dist/server/store.js +1121 -0
- package/dist/server/subagent-runs.d.ts +142 -0
- package/dist/server/subagent-runs.js +315 -0
- package/dist/server/subagents.d.ts +194 -0
- package/dist/server/subagents.js +542 -0
- package/dist/server/task-tool.d.ts +196 -0
- package/dist/server/task-tool.js +285 -0
- package/dist/server/tool-calls.d.ts +62 -0
- package/dist/server/tool-calls.js +141 -0
- package/dist/server/types.d.ts +920 -0
- package/dist/server/types.js +11 -0
- package/dist/server/upload.d.ts +37 -0
- package/dist/server/upload.js +169 -0
- package/dist/server/ws-server.d.ts +45 -0
- package/dist/server/ws-server.js +116 -0
- package/package.json +115 -4
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
import type { AgentRuntime } from "./runtime.js";
|
|
3
|
+
import type { SubagentRunRecorder } from "./subagent-runs.js";
|
|
4
|
+
import type { ServerEvent } from "./types.js";
|
|
5
|
+
import type { WorkspaceService } from "./application/workspace-service.js";
|
|
6
|
+
import type { ConversationService } from "./application/conversation-service.js";
|
|
7
|
+
import type { RunService } from "./application/run-service.js";
|
|
8
|
+
import type { ExtensionUiService } from "./application/extension-ui-service.js";
|
|
9
|
+
import type { CatalogService } from "./application/catalog-service.js";
|
|
10
|
+
/**
|
|
11
|
+
* Common filesystem paths used across the server.
|
|
12
|
+
*/
|
|
13
|
+
export interface ServerPaths {
|
|
14
|
+
dataDir: string;
|
|
15
|
+
rootDir: string;
|
|
16
|
+
agentsDir: string;
|
|
17
|
+
agentPromptDir?: string;
|
|
18
|
+
uploadsDir: string;
|
|
19
|
+
clientDir: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* EventBroadcaster abstracts the WebSocket transport's broadcast mechanism
|
|
23
|
+
* so application services do not directly couple to WebSocket connections.
|
|
24
|
+
*/
|
|
25
|
+
export interface EventBroadcaster {
|
|
26
|
+
/** Broadcast a server event to all connected clients. */
|
|
27
|
+
broadcast(event: ServerEvent): void;
|
|
28
|
+
/** Broadcast the current state snapshot to all connected clients. */
|
|
29
|
+
broadcastState(): void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Central server context containing infrastructure dependencies and application services.
|
|
33
|
+
*/
|
|
34
|
+
export interface ServerContext {
|
|
35
|
+
paths: ServerPaths;
|
|
36
|
+
store: Store;
|
|
37
|
+
runtime: AgentRuntime;
|
|
38
|
+
subagents: SubagentRunRecorder;
|
|
39
|
+
broadcaster: EventBroadcaster;
|
|
40
|
+
accessToken?: string;
|
|
41
|
+
workspaceService: WorkspaceService;
|
|
42
|
+
conversationService: ConversationService;
|
|
43
|
+
runService: RunService;
|
|
44
|
+
extensionUiService: ExtensionUiService;
|
|
45
|
+
catalogService: CatalogService;
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import type { AppState, Conversation, Message, MessageImage, ModelSelection, QueuedMessage, SubagentRun, SubagentRunUpdate, TodoItem, ToolCallBlock, Workspace } from "./types.js";
|
|
2
|
+
/** Mints the id a forked conversation will have, BEFORE its Pi transcript exists.
|
|
3
|
+
*
|
|
4
|
+
* Exported because the ordering demands it and no other arrangement provides it. Pi's copied file's header
|
|
5
|
+
* `id` has to BE the new conversation's id (it is what `findLocalSessionByExactId` matches), so the id has
|
|
6
|
+
* to be decided before the file is written; and the conversation must not exist until the file is safely on
|
|
7
|
+
* disk, or a failed write would leave a conversation whose model context is silently empty. The runtime is
|
|
8
|
+
* therefore handed the id and the store is handed the same one back. See `ForkRequest.forkedConversationId`. */
|
|
9
|
+
export declare function newConversationId(): string;
|
|
10
|
+
/**
|
|
11
|
+
* How many SETTLED sub-agent runs `state.json` keeps, so the file cannot grow without bound.
|
|
12
|
+
*
|
|
13
|
+
* WHY A BOUND IS NEEDED AT ALL. `beginSubagentRun` unshifts and nothing ever removes a run, so a long
|
|
14
|
+
* session that dispatches sub-agents - which is exactly what this feature encourages - would otherwise
|
|
15
|
+
* accumulate records forever. The file is not a log on disk that costs nothing to grow: it is rewritten
|
|
16
|
+
* in full on EVERY text delta of EVERY running child (see `SubagentRunRecorder.publish`), cloned into
|
|
17
|
+
* the browser on every `state` broadcast, and read at startup. A run record is bounded but not small -
|
|
18
|
+
* `output` is the last assistant message, up to the 50 KB cap of §6, plus the task text and one label
|
|
19
|
+
* per tool call - so an unbounded array makes every one of those operations slower for everybody.
|
|
20
|
+
*
|
|
21
|
+
* WHY 100. It has to be big enough that a person can scroll back through a working day without a card
|
|
22
|
+
* going missing, and small enough that the worst case stays in the range a JSON file and a browser
|
|
23
|
+
* snapshot handle comfortably. At 100 settled runs, and a deliberately pessimistic 50 KB per output,
|
|
24
|
+
* the array is ~5 MB; at 1000 it would be ~50 MB, which is enough to be felt as the app hanging on
|
|
25
|
+
* every broadcast. 100 is roughly a day's worth of sub-agent work for a person using this as a tool
|
|
26
|
+
* rather than as a load generator, and a run older than that is still recoverable in full through Pi -
|
|
27
|
+
* the record carries `sessionId`, and `switch_session` or `export_html` reads the real transcript. So
|
|
28
|
+
* what the bound costs is a scrollback affordance, not history.
|
|
29
|
+
*
|
|
30
|
+
* Exported so a test can assert the bound against the same number the store uses rather than
|
|
31
|
+
* re-deriving it.
|
|
32
|
+
*/
|
|
33
|
+
export declare const SUBAGENT_RUN_RETENTION = 100;
|
|
34
|
+
/**
|
|
35
|
+
* Everything a revert decided, before anything is changed.
|
|
36
|
+
*
|
|
37
|
+
* TWO CALLERS NEED IT, and they must agree exactly: the server has to know which sub-agent children to
|
|
38
|
+
* STOP before it cuts the history out from under them, and the store has to know which records to delete
|
|
39
|
+
* afterwards. Two independently written answers to "what does this revert remove" would eventually
|
|
40
|
+
* disagree, and the shape of that disagreement is the worst one available - a stopped child whose record
|
|
41
|
+
* survives, or a deleted record for a child that is still running and still streaming into a store it is
|
|
42
|
+
* no longer in. So both read this one function, which touches nothing.
|
|
43
|
+
*/
|
|
44
|
+
export type RevertPlan = {
|
|
45
|
+
/** Index of the message that SURVIVES. Everything from `boundaryIndex + 1` is removed. */
|
|
46
|
+
boundaryIndex: number;
|
|
47
|
+
/** Pi's entry id of the surviving message - the line Pi's file is cut at. */
|
|
48
|
+
boundaryEntryId: string;
|
|
49
|
+
/** The messages that go away, oldest first. */
|
|
50
|
+
removedMessageIds: string[];
|
|
51
|
+
/**
|
|
52
|
+
* Sub-agent runs dispatched by a turn that is being removed, so their records point at a transcript
|
|
53
|
+
* that no longer exists (research §5 pitfall 7). Runs from surviving turns are deliberately NOT here:
|
|
54
|
+
* `trimSubagentRuns` refuses to drop a live conversation's settled runs (`store.ts:183-195`), so a
|
|
55
|
+
* revert has to drop these explicitly or they become permanent orphans.
|
|
56
|
+
*/
|
|
57
|
+
doomedRunIds: string[];
|
|
58
|
+
};
|
|
59
|
+
export declare class Store {
|
|
60
|
+
private readonly filePath;
|
|
61
|
+
private state;
|
|
62
|
+
private constructor();
|
|
63
|
+
static open(filePath: string): Promise<Store>;
|
|
64
|
+
snapshot(): AppState;
|
|
65
|
+
createWorkspace(name: string, path?: string): Promise<Workspace>;
|
|
66
|
+
renameWorkspace(workspaceId: string, name: string): Promise<Workspace>;
|
|
67
|
+
/**
|
|
68
|
+
* Replaces the workspace's two defaults. `undefined` DELETES the key rather than storing Pi's
|
|
69
|
+
* default as this app's state - the same convention as setConversationThinking, and for the same
|
|
70
|
+
* reason: "follow Pi" must be the absence of a value, not a frozen copy of whatever Pi's default
|
|
71
|
+
* happened to be when the dialog was saved. Only conversations created AFTER this call ever see
|
|
72
|
+
* the new values; existing ones are not migrated, by design.
|
|
73
|
+
*/
|
|
74
|
+
/**
|
|
75
|
+
* Replaces the workspace's defaults. `model`/`thinkingLevel` are ALWAYS sent whole (null clears one of
|
|
76
|
+
* them); `agentId` is tri-state, which is why it is not part of that pair: a string sets it, `null`
|
|
77
|
+
* clears it, and `undefined` leaves it alone. Bundling a tri-state field into an "absent means delete"
|
|
78
|
+
* pair would make any caller that only wanted to change the model silently drop the default agent.
|
|
79
|
+
*
|
|
80
|
+
* ONE persist for all three, deliberately. Two writes would publish an intermediate state - the pair
|
|
81
|
+
* saved but not the agent - and the window between them is observable: a client that reopens the
|
|
82
|
+
* settings dialog after seeing the first write on disk would mount from a broadcast that has not been
|
|
83
|
+
* sent yet, and its draft would then be frozen at the stale value. One write, one broadcast, no window.
|
|
84
|
+
*/
|
|
85
|
+
setWorkspaceDefaults(workspaceId: string, defaults: {
|
|
86
|
+
model?: ModelSelection;
|
|
87
|
+
thinkingLevel?: string;
|
|
88
|
+
agentId?: string | null;
|
|
89
|
+
}): Promise<Workspace>;
|
|
90
|
+
deleteWorkspace(workspaceId: string): Promise<string[]>;
|
|
91
|
+
createConversation(workspaceId: string): Promise<Conversation>;
|
|
92
|
+
/**
|
|
93
|
+
* Copies a conversation into a NEW one, under an id the caller has already minted.
|
|
94
|
+
*
|
|
95
|
+
* THE ID IS A PARAMETER, and that is the whole reason this method does not simply mint one: the caller had
|
|
96
|
+
* to write Pi's copied transcript under that id first (see `ForkRequest.forkedConversationId`), so by the
|
|
97
|
+
* time this runs the id is already the one on disk. Minting here as well would produce a conversation whose
|
|
98
|
+
* Pi file belongs to a different, nonexistent conversation - the exact "Pi opens an empty session and the
|
|
99
|
+
* history has vanished with no error anywhere" failure probe B was about.
|
|
100
|
+
*
|
|
101
|
+
* WHAT IS COPIED, FIELD BY FIELD AND NEVER BY A SPREAD, for the same reason `createConversation` writes out
|
|
102
|
+
* three fields individually: the type declares exactly these, so a spread would quietly start persisting
|
|
103
|
+
* whatever the next person adds to `Conversation`. The workspace, the model, the thinking level and the
|
|
104
|
+
* agent are all copied, because a fork that came back on a different model or with no role would not be
|
|
105
|
+
* the conversation the user forked.
|
|
106
|
+
*
|
|
107
|
+
* `title` is copied VERBATIM, unchanged. Not a reason, not a suffix: the fork IS this conversation, and
|
|
108
|
+
* inventing a name is a product decision the request did not delegate. Renaming it is one click and always
|
|
109
|
+
* available, which is better than a label nobody chose.
|
|
110
|
+
*
|
|
111
|
+
* MESSAGES ARE COPIED WITH FRESH `id`s AND THE ORIGINAL `piEntryId`s. Both halves matter:
|
|
112
|
+
*
|
|
113
|
+
* - Fresh ids, because `Message.id` is this store's own handle (`beginAssistantMessage`,
|
|
114
|
+
* `appendToolCall` and `appendAssistantDelta` all find a message by it) and a forked conversation that
|
|
115
|
+
* answered to its source's ids would be one object wearing two names. Fork the same conversation twice
|
|
116
|
+
* and the two copies are distinct records.
|
|
117
|
+
* - The ORIGINAL `piEntryId`, because that is the whole reason the fork is a fork. Pi's copied file is
|
|
118
|
+
* byte-identical apart from two header fields (see `forkSessionFile`), so the source's entry ids are
|
|
119
|
+
* present and valid in the fork - and a copied message keeps the ability to be a REVERT boundary in the
|
|
120
|
+
* conversation it was copied into. Dropping the ids would produce a fork whose history cannot be
|
|
121
|
+
* reverted at all, which is a silent loss of a feature rather than an honest absence.
|
|
122
|
+
*
|
|
123
|
+
* `status` is `"idle"` unconditionally, even when the source is running, because the caller refuses a
|
|
124
|
+
* running source outright (`runtime.fork`) and a conversation that has never run a turn must not be born
|
|
125
|
+
* claiming to be in one. `error` is NOT copied: it describes a run, and this conversation has had none.
|
|
126
|
+
*
|
|
127
|
+
* NOT COPIED, and both omissions are deliberate and both are the same judgement - they belong to a RUN,
|
|
128
|
+
* and no run was copied: `subagentRuns` are filtered by `conversationId`, so a run dispatched by the source
|
|
129
|
+
* stays with the source; a run dispatched by the FORK will be filed under the fork's own id and work as
|
|
130
|
+
* usual. `queuedMessages` likewise, for the same reason (research pitfall 7): a message waiting for a turn
|
|
131
|
+
* that was never copied must not be re-sent into a different conversation.
|
|
132
|
+
*
|
|
133
|
+
* POSITION IS NOT CHOSEN HERE. `unshift` plus the client rendering the sidebar by `updatedAt`
|
|
134
|
+
* (`byRecency`) puts a fork at the TOP of its workspace group, because `updatedAt` is now - which is the
|
|
135
|
+
* same position a newly created conversation takes, and the same reason.
|
|
136
|
+
*/
|
|
137
|
+
forkConversation(sourceConversationId: string, forkedConversationId: string): Promise<Conversation>;
|
|
138
|
+
/**
|
|
139
|
+
* Archive is a marker, not a move: the conversation keeps its id, workspace, messages and position.
|
|
140
|
+
* `updatedAt` is deliberately NOT touched, because it drives the sidebar's live timestamp and recency -
|
|
141
|
+
* bumping it would teleport the row to the top of the list the instant it was archived.
|
|
142
|
+
*
|
|
143
|
+
* A running conversation is rejected rather than archived. An archived conversation cannot be opened,
|
|
144
|
+
* so allowing one would leave a run streaming into a pane the user can no longer reach, along with any
|
|
145
|
+
* Extension UI question it is blocked on. Refusing keeps the rule honest and leaves stop-then-archive
|
|
146
|
+
* to the user, which is a visible choice instead of a silent one.
|
|
147
|
+
*/
|
|
148
|
+
archiveConversation(conversationId: string): Promise<void>;
|
|
149
|
+
/** Restoring deletes the key rather than setting it to null, so a fully restored conversation serializes
|
|
150
|
+
* byte-identically to one that was never archived. `delete` is legal here because the field is optional. */
|
|
151
|
+
unarchiveConversation(conversationId: string): Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Rename edits the title and NOTHING else. In particular `updatedAt` is deliberately not touched,
|
|
154
|
+
* for exactly the reason archive is: `updatedAt` is the sidebar's recency key, so bumping it would
|
|
155
|
+
* teleport the row to the top of the live list because someone retitled it. Renaming is not activity.
|
|
156
|
+
*
|
|
157
|
+
* There is no uniqueness check, and there is no status check on purpose. `createWorkspace` does not
|
|
158
|
+
* reject a duplicate name either, so rejecting one here would make the two paths disagree about the
|
|
159
|
+
* same question; two rows reading "notes" is a cosmetic problem the user can see and fix. And a
|
|
160
|
+
* RUNNING conversation stays renameable: the title is display-only, the run is unaffected, and
|
|
161
|
+
* refusing would mean a conversation could be permanently un-renameable while the agent works.
|
|
162
|
+
*
|
|
163
|
+
* Archived conversations are renameable too - archiving folds a row away, it does not delete it, and
|
|
164
|
+
* the Archived group renders the same name.
|
|
165
|
+
*/
|
|
166
|
+
renameConversation(conversationId: string, name: string): Promise<Conversation>;
|
|
167
|
+
addUserMessage(conversationId: string, content: string, images?: MessageImage[]): Promise<Message>;
|
|
168
|
+
/**
|
|
169
|
+
* Records the model a conversation runs on. `updatedAt` is deliberately NOT touched, for the same
|
|
170
|
+
* reason rename does not touch it: it is the sidebar's recency key, and switching model is not
|
|
171
|
+
* activity in the conversation. No status check either - changing the model of a RUNNING
|
|
172
|
+
* conversation is allowed, because the selection is read at the START of the next turn, so the
|
|
173
|
+
* turn in flight is unaffected and refusing would strand a conversation on a model the user can
|
|
174
|
+
* no longer change until it settles.
|
|
175
|
+
*/
|
|
176
|
+
setConversationModel(conversationId: string, model: ModelSelection): Promise<Conversation>;
|
|
177
|
+
/** Restoring Pi's own default deletes the key rather than storing Pi's default as this app's state. */
|
|
178
|
+
setConversationThinking(conversationId: string, level: string | undefined): Promise<Conversation>;
|
|
179
|
+
/**
|
|
180
|
+
* Records the agent a conversation runs as. Same three rules as `setConversationModel`: `undefined`
|
|
181
|
+
* DELETES the key rather than freezing "no agent" as a value, `updatedAt` is untouched because
|
|
182
|
+
* changing role is not activity in the conversation, and no status check - the agent is read at the
|
|
183
|
+
* START of the next turn, so the turn in flight is unaffected.
|
|
184
|
+
*/
|
|
185
|
+
setConversationAgent(conversationId: string, agentId: string | undefined): Promise<Conversation>;
|
|
186
|
+
beginAssistantMessage(conversationId: string): Promise<Message>;
|
|
187
|
+
appendAssistantDelta(conversationId: string, messageId: string, delta: string): Promise<void>;
|
|
188
|
+
/**
|
|
189
|
+
* Appends one tool call to the message it belongs to, and returns the message's whole `toolCalls`
|
|
190
|
+
* array so the caller can broadcast it as one snapshot.
|
|
191
|
+
*
|
|
192
|
+
* WHY AN APPEND AND NOT AN UPDATE, and why the returned array is the whole truth. A turn's calls arrive
|
|
193
|
+
* one at a time and each one REWRITES a field on a row it has already broadcast - `status` and
|
|
194
|
+
* `durationMs` land on `tool_execution_end`, minutes or seconds after the row was published by
|
|
195
|
+
* `tool_execution_start`. So the only thing that can be cheap and correct is: the store owns the array,
|
|
196
|
+
* the caller sends the array. Handing back the array rather than the one row is what stops the two from
|
|
197
|
+
* drifting - the caller has nothing to merge, so there is no merge to get wrong, and the event it sends
|
|
198
|
+
* is the same shape `state` carries on the next broadcast.
|
|
199
|
+
*
|
|
200
|
+
* PERSISTED ON EVERY CALL, for the same reason `appendAssistantDelta` is: a block that was on screen
|
|
201
|
+
* when the browser reloaded has to come back, and a reload that showed a `read` but not the `bash` that
|
|
202
|
+
* followed it would be worse than showing neither.
|
|
203
|
+
*
|
|
204
|
+
* `toolCalls` is CREATED LAZILY, so a message whose turn used no tools stores no empty array - which is
|
|
205
|
+
* what makes a `state.json` written before call blocks existed read as "this turn used no tools"
|
|
206
|
+
* instead of as a field it does not understand.
|
|
207
|
+
*/
|
|
208
|
+
appendToolCall(conversationId: string, messageId: string, call: ToolCallBlock): Promise<ToolCallBlock[]>;
|
|
209
|
+
/**
|
|
210
|
+
* Fills in the outcome of the call at `index`, or returns the array UNCHANGED when there is no such
|
|
211
|
+
* index.
|
|
212
|
+
*
|
|
213
|
+
* ADDRESSED BY POSITION rather than by Pi's `toolCallId`, and that is the whole reason the caller has
|
|
214
|
+
* to hold a map. `toolCallId` is an identity only Pi can resolve - it is never persisted on
|
|
215
|
+
* `ToolCallBlock` - so the caller resolves each end back to the row it started, which is exactly what
|
|
216
|
+
* `SubagentRunRecorder` does for a card.
|
|
217
|
+
*
|
|
218
|
+
* A MISMATCH IS NOT AN ERROR. `tool_execution_end` with no matching start, or one that arrives after the
|
|
219
|
+
* turn ended, must not fail the turn: the alternative is a store method that throws inside a drain loop,
|
|
220
|
+
* and the reader would lose the whole turn's transcript over one unresolvable id. Returning the array
|
|
221
|
+
* unchanged keeps the caller broadcasting what it actually knows.
|
|
222
|
+
*
|
|
223
|
+
* Only `status` and `durationMs` are written, and only when they are present on the incoming block - a
|
|
224
|
+
* second end for a row that already has an outcome must not erase it with an absent field.
|
|
225
|
+
*/
|
|
226
|
+
settleToolCall(conversationId: string, messageId: string, index: number, outcome: {
|
|
227
|
+
status?: "ok" | "error";
|
|
228
|
+
durationMs?: number;
|
|
229
|
+
}): Promise<ToolCallBlock[]>;
|
|
230
|
+
setStatus(conversationId: string, status: Conversation["status"]): Promise<void>;
|
|
231
|
+
/** The plan a revert would carry out, without carrying it out. The server reads this to stop children first. */
|
|
232
|
+
planRevert(conversationId: string, messageId: string): RevertPlan;
|
|
233
|
+
/**
|
|
234
|
+
* Records which Pi entry a message became, as reported by Pi while it was still running.
|
|
235
|
+
*
|
|
236
|
+
* Only ever set, never cleared: a message that already has an id keeps the one Pi gave it for that
|
|
237
|
+
* turn, and a second report for the same message is ignored rather than allowed to move the boundary -
|
|
238
|
+
* a boundary that moved under a user who is looking at it is exactly the silent data loss revert must not
|
|
239
|
+
* be able to cause. An unknown message id is refused for the same reason `appendAssistantDelta` refuses
|
|
240
|
+
* one: it means the caller and the store disagree about the conversation, and guessing is worse.
|
|
241
|
+
*/
|
|
242
|
+
setMessagePiEntryId(conversationId: string, messageId: string, piEntryId: string): Promise<void>;
|
|
243
|
+
/**
|
|
244
|
+
* Reverts the conversation to the state right after `messageId`, in `state.json` only.
|
|
245
|
+
*
|
|
246
|
+
* SCOPE, DELIBERATELY NARROW: this touches `messages` and `subagentRuns` and nothing else. It does not
|
|
247
|
+
* touch the workspace (that is not this process's to touch), it does not create or modify a conversation
|
|
248
|
+
* (the user's hard requirement: still the same session, same id, same row in the sidebar), and it does
|
|
249
|
+
* not record a "revert happened" marker - there is no undo to restore one for.
|
|
250
|
+
*
|
|
251
|
+
* NOT `updatedAt`. For the same reason rename and archive leave it alone: it is the sidebar's recency key,
|
|
252
|
+
* so bumping it would teleport the conversation to the top of the list as a side effect of editing its
|
|
253
|
+
* history. What a person did to a transcript is not activity IN the conversation, and the timestamps of
|
|
254
|
+
* the surviving messages are untouched, so the pane's own ordering does not move either.
|
|
255
|
+
*
|
|
256
|
+
* `planRevert` has already run by the time this is called (the server reads it to stop children), so the
|
|
257
|
+
* plan is recomputed here rather than passed in - two callers cannot disagree about what a message id
|
|
258
|
+
* means, and the recomputation is a pure scan of a small array.
|
|
259
|
+
*/
|
|
260
|
+
revertMessages(conversationId: string, messageId: string): Promise<RevertPlan>;
|
|
261
|
+
private requireConversation;
|
|
262
|
+
/**
|
|
263
|
+
* This conversation's waiting list, oldest first.
|
|
264
|
+
*
|
|
265
|
+
* Cloned like every other value this store hands out, because the caller broadcasts it straight to
|
|
266
|
+
* browsers and the array is mutated in place by the very next enqueue.
|
|
267
|
+
*/
|
|
268
|
+
listQueuedMessages(conversationId: string): QueuedMessage[];
|
|
269
|
+
/**
|
|
270
|
+
* Adds one message to the waiting list and returns it.
|
|
271
|
+
*
|
|
272
|
+
* The id is minted here and is the entry's identity for its whole life. Pi matches its own queue by
|
|
273
|
+
* message TEXT (`agent-session.ts:1123`), so two identical messages are one entry to Pi and two here;
|
|
274
|
+
* `cancel` and `steer` name the id, which is the only reason "cancel the second of two identical
|
|
275
|
+
* messages" is expressible at all. `crypto.randomUUID`, like every other id in this store.
|
|
276
|
+
*/
|
|
277
|
+
enqueueMessage(conversationId: string, content: string, images?: MessageImage[]): Promise<QueuedMessage>;
|
|
278
|
+
/**
|
|
279
|
+
* Removes one waiting message. Returns it, or `undefined` when it is already gone.
|
|
280
|
+
*
|
|
281
|
+
* "Already gone" is a no-op rather than an error, and the reason is the same as `SubagentRun.stop`:
|
|
282
|
+
* the click that sent this was almost always triggered by a dock row the browser has not been told
|
|
283
|
+
* about yet, so an id that has already been delivered or cancelled is the ordinary case and must not
|
|
284
|
+
* surface as a failure.
|
|
285
|
+
*
|
|
286
|
+
* A `sending` entry REFUSES, because the text is already in a live Pi child's stdin and Pi has no way
|
|
287
|
+
* to forget one item (its only queue command is `clear_queue`, which clears every one of them,
|
|
288
|
+
* including follow-ups that have nothing to do with this click). Removing it from this list would not
|
|
289
|
+
* stop Pi from delivering it - it would make the model act on a message the user believed they had
|
|
290
|
+
* withdrawn.
|
|
291
|
+
*/
|
|
292
|
+
cancelQueuedMessage(conversationId: string, queueId: string): Promise<QueuedMessage | undefined>;
|
|
293
|
+
/**
|
|
294
|
+
* Claims one waiting message for delivery to the live Pi process: `queued` becomes `sending`, and the
|
|
295
|
+
* entry STAYS in the list until Pi reports it delivered.
|
|
296
|
+
*
|
|
297
|
+
* The read and the write are adjacent statements with no `await` between them, which is what makes
|
|
298
|
+
* the claim atomic here. This store has no mutex, and it does not need one: JavaScript runs this
|
|
299
|
+
* synchronous run-to-completion, so a second caller arriving in the same tick observes the write above
|
|
300
|
+
* and cannot claim the same entry. Anything that needed to await between reading and writing would
|
|
301
|
+
* need real serialization instead.
|
|
302
|
+
*
|
|
303
|
+
* Throws for an unknown id and for one that is already `sending`, because both mean the caller is
|
|
304
|
+
* acting on a state the store has already moved past - and the second one means a second steer of the
|
|
305
|
+
* same message would put the text into Pi twice.
|
|
306
|
+
*/
|
|
307
|
+
claimQueuedMessage(conversationId: string, queueId: string): Promise<QueuedMessage>;
|
|
308
|
+
/**
|
|
309
|
+
* Puts a `sending` entry back into the queue.
|
|
310
|
+
*
|
|
311
|
+
* The one legal use, and it is narrow on purpose: this is called ONLY when we can prove the message
|
|
312
|
+
* never reached a model - the turn ended while Pi still held it, or the write to Pi's stdin failed
|
|
313
|
+
* before it left. Pi drains steering before its loop can end and reports the removal before the
|
|
314
|
+
* delivered message, so a message still `sending` when the child is gone provably was not delivered,
|
|
315
|
+
* and returning it is a fact rather than a guess. It is deliberately NOT used for "Pi has not answered
|
|
316
|
+
* yet" (see `steer` in `index.ts`), where the outcome is genuinely unknown and the honest state is
|
|
317
|
+
* still `sending`.
|
|
318
|
+
*/
|
|
319
|
+
releaseQueuedMessage(queueId: string): Promise<void>;
|
|
320
|
+
/**
|
|
321
|
+
* Removes the OLDEST `count` messages this conversation has in `sending`, and returns them.
|
|
322
|
+
*
|
|
323
|
+
* Driven by a COUNT rather than by matching text, and that is the whole point. Pi's `queue_update`
|
|
324
|
+
* reports only the strings it still holds, and Pi itself identifies which of its entries a delivered
|
|
325
|
+
* message came from with `indexOf` on that text - so with two identical steered messages, "which one
|
|
326
|
+
* was delivered" is exactly the question Pi cannot answer. It does not need to be answered: Pi's
|
|
327
|
+
* steering queue is FIFO and Super Pi only ever steers from the front of this list, so a drop in
|
|
328
|
+
* `steering.length` from N to N-k means Super Pi's oldest k `sending` entries were delivered, in
|
|
329
|
+
* order, whatever their text was.
|
|
330
|
+
*/
|
|
331
|
+
dropSendingMessages(conversationId: string, count: number): Promise<QueuedMessage[]>;
|
|
332
|
+
/**
|
|
333
|
+
* Takes the oldest still-`queued` message of this conversation OUT of the list and returns it, for the
|
|
334
|
+
* next turn to send as its prompt.
|
|
335
|
+
*
|
|
336
|
+
* Removing it here, rather than marking it and removing it after the turn, is what bounds the loop:
|
|
337
|
+
* every pass through the follow-up loop strictly shrinks the list, so the loop cannot run forever
|
|
338
|
+
* even if a turn succeeds without consuming anything. And removing it BEFORE the spawn is what makes a
|
|
339
|
+
* crash honest - a server that dies mid-turn has already taken the message out of the file, and
|
|
340
|
+
* `open` does not put it back, because it may well have been delivered.
|
|
341
|
+
*/
|
|
342
|
+
takeNextQueuedMessage(conversationId: string): Promise<QueuedMessage | undefined>;
|
|
343
|
+
/**
|
|
344
|
+
* Returns every `sending` message of this conversation to the queue, for the end of a turn.
|
|
345
|
+
*
|
|
346
|
+
* See `releaseQueuedMessage` for why this is a fact and not a guess, and `reconcileQueuedMessages` for
|
|
347
|
+
* why the same treatment is right after a crash. Called both when a turn ends and when the user stops
|
|
348
|
+
* it: in the stop case the child is killed mid-turn and a steer it was still holding was never
|
|
349
|
+
* delivered either, so putting it back is what lets the user decide what to do with it instead of
|
|
350
|
+
* silently dropping a message they typed.
|
|
351
|
+
*/
|
|
352
|
+
releaseSendingMessages(conversationId: string): Promise<void>;
|
|
353
|
+
/**
|
|
354
|
+
* Records a sub-agent run as `running`, BEFORE the child is spawned.
|
|
355
|
+
*
|
|
356
|
+
* The ordering is the whole point of this method existing separately from the spawn. A record created
|
|
357
|
+
* after the child starts cannot survive the child failing to start, and one created only at the end
|
|
358
|
+
* leaves nothing behind when the SERVER dies - which is the orphan startup reconciliation exists to
|
|
359
|
+
* clean up. Writing the claim first is what makes that orphan possible, and therefore recoverable.
|
|
360
|
+
*
|
|
361
|
+
* `unshift`, like `createConversation`: the newest run is the one a reader is looking for.
|
|
362
|
+
*
|
|
363
|
+
* Trimmed HERE rather than at read time, because the array in memory and the file on disk have to
|
|
364
|
+
* be the same thing - a store that kept evicted rows until the next unrelated write would put them
|
|
365
|
+
* back on disk, and one that trimmed on read would show a browser a roster the file still holds.
|
|
366
|
+
*/
|
|
367
|
+
beginSubagentRun(run: SubagentRun): Promise<SubagentRun>;
|
|
368
|
+
setBackgroundTask(summary: AppState["backgroundTasks"][number]): Promise<void>;
|
|
369
|
+
removeBackgroundTask(id: string): Promise<void>;
|
|
370
|
+
/**
|
|
371
|
+
* Applies the fields a run's life can change, and nothing else.
|
|
372
|
+
*
|
|
373
|
+
* `Object.assign` is safe HERE in a way it is not in `setConversationModel`, and the difference is the
|
|
374
|
+
* patch's own type: `SubagentRunUpdate` is a named list of six already-narrowed fields, so there is no
|
|
375
|
+
* wider object for a spread to smuggle extra keys in from. `undefined` means "no such value" and is
|
|
376
|
+
* dropped by JSON.stringify, which is how `output`/`error`/`outputTruncated` come to be absent rather
|
|
377
|
+
* than stored as an empty or null one.
|
|
378
|
+
*
|
|
379
|
+
* PERSISTED ON EVERY CALL, exactly as `appendAssistantDelta` is: the record a reader sees in a
|
|
380
|
+
* `state` broadcast and the record on disk are then never different, and a run that is in flight is
|
|
381
|
+
* as durable as the conversation it belongs to.
|
|
382
|
+
*/
|
|
383
|
+
setSubagentRun(runId: string, changes: SubagentRunUpdate): Promise<SubagentRun>;
|
|
384
|
+
/**
|
|
385
|
+
* Replaces the model's task list, and persists it.
|
|
386
|
+
*
|
|
387
|
+
* WHY IT IS A FILE AND NOT PI'S MEMORY, stated once here because it is the whole justification: Pi is
|
|
388
|
+
* killed unconditionally at the end of every turn (`runtime.ts`) and never writes a tool result it was
|
|
389
|
+
* given by an extension anywhere durable except the session JSONL, so a list held only in the child
|
|
390
|
+
* would be gone before a person could look at it - and the sidebar would empty itself at every turn
|
|
391
|
+
* boundary. The list is therefore part of `state.json` and rides the existing broadcast, which is also
|
|
392
|
+
* why the browser needs no channel of its own: it already repaints from `state` on every text delta.
|
|
393
|
+
*
|
|
394
|
+
* THE PARAMETER IS RE-NORMALIZED HERE, not trusted because the endpoint normalized it. Those are two
|
|
395
|
+
* different things: `/api/todos` validates a request that crossed a socket, and this validates a value
|
|
396
|
+
* handed to `setTodos` by any future caller inside this process. A method whose own contract is "call me
|
|
397
|
+
* with anything" is the only version of this that cannot be broken by the next call site.
|
|
398
|
+
*
|
|
399
|
+
* AN EMPTY LIST IS STORED AS ABSENCE, not as `[]`. `normalizeTodos` returns `[]` for a list the model
|
|
400
|
+
* emptied deliberately, and the two must not be distinguishable: the browser renders `todos: []` and
|
|
401
|
+
* `todos: undefined` as the same empty state on purpose, and persisting `[]` for a store that has never
|
|
402
|
+
* seen the tool would make a fresh install claim the model had considered a plan and found none. So the
|
|
403
|
+
* key is deleted, which also keeps `state.json` free of a field no build before this one wrote.
|
|
404
|
+
*/
|
|
405
|
+
setTodos(raw: unknown): Promise<{
|
|
406
|
+
todos: TodoItem[];
|
|
407
|
+
dropped: number;
|
|
408
|
+
}>;
|
|
409
|
+
/**
|
|
410
|
+
* Writes the whole state atomically: a temp file, then a rename over the real one.
|
|
411
|
+
*
|
|
412
|
+
* SERIALIZED, because two conversations can genuinely run at once (see workspace-cwd.test.mjs), and
|
|
413
|
+
* this method was not safe for that: every write used the SAME `${filePath}.tmp`, so two overlapping
|
|
414
|
+
* persists raced and the loser's rename failed with ENOENT on a file the winner had already moved.
|
|
415
|
+
* A unique name per write would hide that ENOENT while leaving the real bug in place - whichever rename
|
|
416
|
+
* landed last would win, and a rename of an OLDER snapshot last would silently discard a newer one.
|
|
417
|
+
* Queueing keeps both properties: one writer at a time, and last write wins with the newest state.
|
|
418
|
+
*
|
|
419
|
+
* A rejected write must not poison the queue, or one failure would reject every later persist as well.
|
|
420
|
+
*/
|
|
421
|
+
private persistQueue;
|
|
422
|
+
private persist;
|
|
423
|
+
}
|
|
424
|
+
export declare function defaultStorePath(dataDir: string): string;
|