@evandu/super-pi 0.1.137 → 0.1.139
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/agents/super-pi-knowledge/index.ts +11 -13
- package/agents/super-pi-memory/index.ts +11 -5
- package/bin/super-pi.js +7 -11
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +6 -6
- package/dist/client/assets/{CodeMirrorEditor-CkA_5D5r.js → CodeMirrorEditor-H7SHGhdS.js} +2 -2
- package/dist/client/assets/{dist-DcdzfM-n.js → dist-2XXLO-IZ.js} +1 -1
- package/dist/client/assets/{dist-ufRHdQ8G.js → dist-BEM6tttY.js} +1 -1
- package/dist/client/assets/{dist-9Fm8XKu4.js → dist-BfUeRJKr.js} +1 -1
- package/dist/client/assets/{dist-CrxmpY93.js → dist-BokzcFLE.js} +1 -1
- package/dist/client/assets/{dist-BfEmujEC.js → dist-BoucVzKA.js} +1 -1
- package/dist/client/assets/{dist-B02Erd0t.js → dist-BxTGjU84.js} +1 -1
- package/dist/client/assets/{dist-DGhdkud4.js → dist-C-MUXK_t.js} +1 -1
- package/dist/client/assets/{dist-k6bOz9RS.js → dist-C8NllalC.js} +1 -1
- package/dist/client/assets/{dist-C2tSd-AY.js → dist-CSdzvQnf.js} +1 -1
- package/dist/client/assets/{dist-BySjislr.js → dist-CUNctZ5v.js} +1 -1
- package/dist/client/assets/{dist-B3Mfuv1D.js → dist-CZ9Krsno.js} +1 -1
- package/dist/client/assets/{dist-DbovsYeH.js → dist-C_ftUQZN.js} +1 -1
- package/dist/client/assets/{dist-j9X7j381.js → dist-CuMnZOGE.js} +1 -1
- package/dist/client/assets/{dist-D-LtG92p.js → dist-CukPKrEn.js} +1 -1
- package/dist/client/assets/{dist-b9CfAgzG.js → dist-D3Ta0y4e.js} +1 -1
- package/dist/client/assets/{dist-BsqzrZ5R.js → dist-DJ8SwQl_.js} +1 -1
- package/dist/client/assets/{dist-BqGjijI6.js → dist-DLKVFZhO.js} +1 -1
- package/dist/client/assets/{dist-MC2XuZgq.js → dist-Jw-GQ-re.js} +1 -1
- package/dist/client/assets/{dist-D_umYm0o.js → dist-NlopeJ5R.js} +1 -1
- package/dist/client/assets/{dist-CsNytPug.js → dist-POoQ4j29.js} +1 -1
- package/dist/client/assets/{dist-DdVs8Y0X.js → dist-cP2vnTP_.js} +1 -1
- package/dist/client/assets/{dist-DL4Q05ts.js → dist-zUJqdbfo.js} +1 -1
- package/dist/client/assets/{index-DVmE8H4Y.js → index-B32VZVYx.js} +13 -13
- package/dist/client/assets/index-Bdym8BPo.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/application/conversation-service.d.ts +13 -4
- package/dist/server/application/conversation-service.js +112 -61
- package/dist/server/application/run-service.d.ts +13 -0
- package/dist/server/application/run-service.js +255 -69
- package/dist/server/application/scheduled-task-service.js +8 -1
- package/dist/server/application/workspace-service.js +16 -1
- package/dist/server/feishu-cli.js +15 -4
- package/dist/server/file-index.d.ts +30 -0
- package/dist/server/file-index.js +49 -4
- package/dist/server/files.d.ts +21 -0
- package/dist/server/files.js +38 -0
- package/dist/server/http-server.js +1455 -1412
- package/dist/server/index.js +3 -4
- package/dist/server/logging.js +56 -3
- package/dist/server/pi-session-pool.d.ts +2 -0
- package/dist/server/pi-session-pool.js +1 -0
- package/dist/server/pi-session.d.ts +118 -0
- package/dist/server/pi-session.js +132 -10
- package/dist/server/runtime.d.ts +47 -0
- package/dist/server/runtime.js +183 -84
- package/dist/server/store.d.ts +44 -1
- package/dist/server/store.js +114 -12
- package/dist/server/subagent-runs.js +18 -7
- package/dist/server/subagents.js +7 -0
- package/dist/server/tool-runner.js +13 -4
- package/dist/server/types.d.ts +27 -0
- package/dist/server/ws-server.d.ts +34 -5
- package/dist/server/ws-server.js +170 -46
- package/package.json +15 -1
- package/scripts/diagnose.mjs +161 -58
- package/dist/client/assets/index-FxTrUapG.css +0 -1
package/dist/server/store.js
CHANGED
|
@@ -721,6 +721,47 @@ export class Store {
|
|
|
721
721
|
const workspace = this.state.workspaces.find((item) => item.id === workspaceId);
|
|
722
722
|
return workspace ? { ...workspace } : undefined;
|
|
723
723
|
}
|
|
724
|
+
/** Read the app-wide default without cloning conversations or transcripts. */
|
|
725
|
+
defaultModel() {
|
|
726
|
+
const model = this.state.defaultModel;
|
|
727
|
+
return model ? { ...model } : undefined;
|
|
728
|
+
}
|
|
729
|
+
/** Read one detached conversation for lifecycle operations. */
|
|
730
|
+
conversation(conversationId) {
|
|
731
|
+
const conversation = this.state.conversations.find((item) => item.id === conversationId);
|
|
732
|
+
return conversation ? structuredClone(conversation) : undefined;
|
|
733
|
+
}
|
|
734
|
+
/** Return every pending question in one conversation without cloning unrelated history. */
|
|
735
|
+
pendingQuestions(conversationId) {
|
|
736
|
+
const conversation = this.state.conversations.find((item) => item.id === conversationId);
|
|
737
|
+
if (!conversation)
|
|
738
|
+
return [];
|
|
739
|
+
const pending = [];
|
|
740
|
+
for (const message of conversation.messages) {
|
|
741
|
+
for (let index = 0; index < (message.toolCalls?.length ?? 0); index += 1) {
|
|
742
|
+
const question = message.toolCalls?.[index]?.question;
|
|
743
|
+
if (message.toolCalls?.[index]?.label === "question" && question?.status === "pending") {
|
|
744
|
+
pending.push({ messageId: message.id, index, question: structuredClone(question) });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return pending;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* THE SCHEDULED TASKS, WITHOUT CLONING THE STATE TO FIND THEM.
|
|
752
|
+
*
|
|
753
|
+
* The same argument as the readers above, with a sharper edge: the scheduled-task clock sweeps
|
|
754
|
+
* every fifteen seconds and used to read its three-to-zero tasks through `snapshot()` - a deep
|
|
755
|
+
* clone of the whole application, every fifteen seconds, whether the user has any scheduled task or
|
|
756
|
+
* not. On a server holding a long transcript that is a full copy of the app four times a minute,
|
|
757
|
+
* produced for a list that is usually empty.
|
|
758
|
+
*
|
|
759
|
+
* The tasks are returned as a shallow copy of the array: the sweep reads `enabled` and
|
|
760
|
+
* `nextRunAt` and then hands the whole task back to the store, which owns the real one.
|
|
761
|
+
*/
|
|
762
|
+
scheduledTasks() {
|
|
763
|
+
return this.state.scheduledTasks ? [...this.state.scheduledTasks] : [];
|
|
764
|
+
}
|
|
724
765
|
/** Small detached rows used when refreshing each conversation's scoped model catalog. */
|
|
725
766
|
catalogConversationTargets() {
|
|
726
767
|
return this.state.conversations.map(({ id, archivedAt, model }) => ({
|
|
@@ -788,8 +829,10 @@ export class Store {
|
|
|
788
829
|
snapshot() {
|
|
789
830
|
const startedAt = process.env.SUPER_PI_TRACE_LATENCY === "1" ? performance.now() : 0;
|
|
790
831
|
const value = structuredClone(this.state);
|
|
832
|
+
// Do not stringify here just to count bytes: that makes tracing add a second full serialization
|
|
833
|
+
// and mislabels that extra work as clone time. The broadcaster measures the actual JSON payload.
|
|
791
834
|
if (startedAt)
|
|
792
|
-
console.warn(`[super-pi latency] phase=state.snapshot
|
|
835
|
+
console.warn(`[super-pi latency] phase=state.snapshot cloneMs=${Math.round(performance.now() - startedAt)}`);
|
|
793
836
|
return value;
|
|
794
837
|
}
|
|
795
838
|
/** Cheap scalar/entity reads for hot paths. Never expose the live mutable state object. */
|
|
@@ -981,14 +1024,32 @@ export class Store {
|
|
|
981
1024
|
* Filtering HERE, in one place, rather than in every reader, is what keeps the client dumb: the
|
|
982
1025
|
* sidebar renders `state.workspaces` and never has to know that a closed workspace exists.
|
|
983
1026
|
*/
|
|
984
|
-
clientState() {
|
|
985
|
-
const
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1027
|
+
clientState(scope) {
|
|
1028
|
+
const closed = new Set(this.state.workspaces.filter((item) => item.closedAt).map((item) => item.id));
|
|
1029
|
+
const workspaces = this.state.workspaces.filter((item) => !closed.has(item.id));
|
|
1030
|
+
const visible = this.state.conversations.filter((item) => !closed.has(item.workspaceId));
|
|
1031
|
+
// Legacy sockets still receive complete visible transcripts. Project before cloning so a closed
|
|
1032
|
+
// workspace's retained history is never copied just to discard it afterwards.
|
|
1033
|
+
if (scope === undefined)
|
|
1034
|
+
return structuredClone({ ...this.state, workspaces, conversations: visible });
|
|
1035
|
+
// A focused socket keeps every sidebar row, but owns only the selected transcript. An empty scope
|
|
1036
|
+
// deliberately loads none; a missing/closed selection must not expose retained workspace history.
|
|
1037
|
+
const selectedId = visible.find((item) => item.id === scope.conversationId)?.id;
|
|
1038
|
+
const conversations = visible.map(({ messages, ...metadata }) => ({
|
|
1039
|
+
...metadata,
|
|
1040
|
+
messages: metadata.id === selectedId ? messages : [],
|
|
1041
|
+
messagesLoaded: metadata.id === selectedId,
|
|
1042
|
+
hasPendingQuestion: messages.some((message) => message.toolCalls?.some((call) => call.question?.status === "pending")),
|
|
1043
|
+
}));
|
|
1044
|
+
return structuredClone({
|
|
1045
|
+
...this.state,
|
|
1046
|
+
workspaces,
|
|
1047
|
+
conversations,
|
|
1048
|
+
subagentRuns: this.state.subagentRuns.filter((run) => run.conversationId === selectedId),
|
|
1049
|
+
...(this.state.workflows ? { workflows: this.state.workflows.filter((run) => run.conversationId === selectedId) } : {}),
|
|
1050
|
+
...(this.state.todosByConversation ? { todosByConversation: selectedId && this.state.todosByConversation[selectedId]
|
|
1051
|
+
? { [selectedId]: this.state.todosByConversation[selectedId] } : {} } : {}),
|
|
1052
|
+
});
|
|
992
1053
|
}
|
|
993
1054
|
async createConversation(workspaceId, configuration) {
|
|
994
1055
|
const workspace = this.state.workspaces.find((item) => item.id === workspaceId);
|
|
@@ -1007,6 +1068,10 @@ export class Store {
|
|
|
1007
1068
|
if (workspace.defaultAgentId)
|
|
1008
1069
|
conversation.agentId = workspace.defaultAgentId;
|
|
1009
1070
|
if (configuration) {
|
|
1071
|
+
// WHY THIS CONVERSATION IS NOT ON THE MODEL THE USER SET. Set once, at birth, and never
|
|
1072
|
+
// rewritten: it describes how this conversation came to be, not what is true right now.
|
|
1073
|
+
if (configuration.modelNotice !== undefined)
|
|
1074
|
+
conversation.modelNotice = configuration.modelNotice;
|
|
1010
1075
|
if (configuration.model !== undefined) {
|
|
1011
1076
|
if (configuration.model)
|
|
1012
1077
|
conversation.model = { ...configuration.model };
|
|
@@ -1477,7 +1542,7 @@ export class Store {
|
|
|
1477
1542
|
};
|
|
1478
1543
|
calls.push(structuredClone(placed));
|
|
1479
1544
|
conversation.updatedAt = now();
|
|
1480
|
-
await this.
|
|
1545
|
+
await this.persistToolCallMessage(conversationId, messageId);
|
|
1481
1546
|
return structuredClone(calls);
|
|
1482
1547
|
}
|
|
1483
1548
|
/**
|
|
@@ -1521,7 +1586,7 @@ export class Store {
|
|
|
1521
1586
|
// finished tool block that vanishes the moment the call ends is a block that taught the reader
|
|
1522
1587
|
// nothing. The client prefers `live` when both are present, falling back to `result`.
|
|
1523
1588
|
conversation.updatedAt = now();
|
|
1524
|
-
await this.
|
|
1589
|
+
await this.persistToolCallMessage(conversationId, messageId);
|
|
1525
1590
|
return structuredClone(calls);
|
|
1526
1591
|
}
|
|
1527
1592
|
/**
|
|
@@ -1533,6 +1598,26 @@ export class Store {
|
|
|
1533
1598
|
* Every field is `undefined`-tested for the same reason `settleToolCall` does it: `""` and `[]` are
|
|
1534
1599
|
* values a tool can genuinely produce, and treating them as "not reported" would erase a real answer.
|
|
1535
1600
|
*/
|
|
1601
|
+
/**
|
|
1602
|
+
* THE TOOL-CALL WRITES, PERSISTED AS ONE MESSAGE INSTEAD OF THE WHOLE APPLICATION.
|
|
1603
|
+
*
|
|
1604
|
+
* A tool call's lifecycle touches exactly ONE thing: the `toolCalls` array of ONE message. The
|
|
1605
|
+
* write used to be `this.persist()`, which hands the ENTIRE state to the repository - every
|
|
1606
|
+
* workspace, every conversation, every message of every conversation - to be serialized and
|
|
1607
|
+
* written. A turn that calls five tools therefore wrote the whole application ten to fifteen
|
|
1608
|
+
* times, and every one of those writes was proportional to the whole history rather than to the
|
|
1609
|
+
* one row that changed (LAT-020).
|
|
1610
|
+
*
|
|
1611
|
+
* `message.replace` is the transaction that means 「this one message now reads like this」, and it
|
|
1612
|
+
* is exactly the write this is: the row's content did not change, only its tool calls did.
|
|
1613
|
+
*/
|
|
1614
|
+
async persistToolCallMessage(conversationId, messageId) {
|
|
1615
|
+
const conversation = this.requireConversation(conversationId);
|
|
1616
|
+
const message = conversation.messages.find((item) => item.id === messageId);
|
|
1617
|
+
if (!message)
|
|
1618
|
+
return;
|
|
1619
|
+
await this.persistTransaction({ kind: "message.replace", conversationId, message: structuredClone(message), updatedAt: conversation.updatedAt });
|
|
1620
|
+
}
|
|
1536
1621
|
async updateToolCall(conversationId, messageId, index, patch) {
|
|
1537
1622
|
const conversation = this.requireConversation(conversationId);
|
|
1538
1623
|
const message = conversation.messages.find((item) => item.id === messageId);
|
|
@@ -1569,11 +1654,28 @@ export class Store {
|
|
|
1569
1654
|
// the call was being written are gone by the time it runs.
|
|
1570
1655
|
if (patch.live !== undefined)
|
|
1571
1656
|
call.live = patch.live;
|
|
1657
|
+
// WHERE THE CALL HAPPENED, corrected when the call is announced. The placeholder stamped the
|
|
1658
|
+
// offsets of the moment it was made, and that moment is not the moment the call belongs to
|
|
1659
|
+
// (UX-008).
|
|
1660
|
+
if (patch.at !== undefined)
|
|
1661
|
+
call.at = patch.at;
|
|
1662
|
+
if (patch.thinkingAt !== undefined)
|
|
1663
|
+
call.thinkingAt = patch.thinkingAt;
|
|
1664
|
+
// WHICH REASONING BLOCK THIS CALL BELONGS TO. It is written when the call is ANNOUNCED, not
|
|
1665
|
+
// when its placeholder was made, and it is written HERE because the placeholder row already
|
|
1666
|
+
// exists by then - a placeholder created while the model was still writing the arguments
|
|
1667
|
+
// cannot know whether the reasoning was over by the time the call was made (UX-008).
|
|
1668
|
+
//
|
|
1669
|
+
// `null` IS A VALUE, NOT AN ABSENCE: it says the call is outside every reasoning block, and
|
|
1670
|
+
// the browser needs to hear that difference from "not decided yet". So the check is
|
|
1671
|
+
// `!== undefined`, never a truthiness test.
|
|
1672
|
+
if (patch.thinkingBlock !== undefined)
|
|
1673
|
+
call.thinkingBlock = patch.thinkingBlock;
|
|
1572
1674
|
// The question rides on the call rather than beside it, so this is the only write it needs.
|
|
1573
1675
|
if (patch.question !== undefined)
|
|
1574
1676
|
call.question = patch.question;
|
|
1575
1677
|
conversation.updatedAt = now();
|
|
1576
|
-
await this.
|
|
1678
|
+
await this.persistToolCallMessage(conversationId, messageId);
|
|
1577
1679
|
return structuredClone(calls);
|
|
1578
1680
|
}
|
|
1579
1681
|
/**
|
|
@@ -125,7 +125,22 @@ export class SubagentRunRecorder {
|
|
|
125
125
|
// Generated HERE, not by `run()`, because the record has to exist first - see this file's header.
|
|
126
126
|
const id = input.id ?? newSubagentRunId();
|
|
127
127
|
const started = { id, conversationId, agentId, task, dispatchedBy, parentRunId: input.parentRunId, depth: input.depth ?? 0, loopId: input.loopId, role: input.role, originMessageId: input.originMessageId, originCallRow: input.originCallRow, status: "running", startedAt: new Date().toISOString(), sessionId: id };
|
|
128
|
-
await
|
|
128
|
+
// Own cancellation before the first persistence await. A conversation stop must also reach a
|
|
129
|
+
// human dispatch whose initial row is still being written, before a supervisor/child exists.
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
this.controllers.set(id, { controller, conversationId });
|
|
132
|
+
const onCallerAbort = () => controller.abort();
|
|
133
|
+
input.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
134
|
+
if (input.signal?.aborted)
|
|
135
|
+
controller.abort();
|
|
136
|
+
try {
|
|
137
|
+
await this.hooks.store.beginSubagentRun(started);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
input.signal?.removeEventListener("abort", onCallerAbort);
|
|
141
|
+
this.controllers.delete(id);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
129
144
|
// The recorder's own copy, held separately from the store's. Mutating the object `beginSubagentRun`
|
|
130
145
|
// was handed would reach into the store's state behind its back, and every snapshot it hands out
|
|
131
146
|
// would then move under its reader.
|
|
@@ -219,13 +234,9 @@ export class SubagentRunRecorder {
|
|
|
219
234
|
* run happened.
|
|
220
235
|
*/
|
|
221
236
|
const elapsedMs = () => Math.max(0, Date.now() - new Date(started.startedAt).getTime());
|
|
222
|
-
// One controller for both abort sources: the caller's signal and `abortAll` are the same button from
|
|
223
|
-
// the child's point of view, and `run()` takes exactly one.
|
|
224
|
-
const controller = new AbortController();
|
|
225
|
-
this.controllers.set(id, { controller, conversationId });
|
|
226
|
-
const onCallerAbort = () => controller.abort();
|
|
227
|
-
input.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
228
237
|
try {
|
|
238
|
+
if (controller.signal.aborted)
|
|
239
|
+
throw new SubagentCancelled(id);
|
|
229
240
|
const result = await this.supervisor.run({
|
|
230
241
|
id,
|
|
231
242
|
conversationId,
|
package/dist/server/subagents.js
CHANGED
|
@@ -265,6 +265,13 @@ export class SubagentSupervisor {
|
|
|
265
265
|
args.push("--provider", chosen.provider, "--model", chosen.modelId);
|
|
266
266
|
if (agent.thinkingLevel)
|
|
267
267
|
args.push("--thinking", agent.thinkingLevel);
|
|
268
|
+
// Cancellation may arrive during filesystem/config/model preparation, before the child or its
|
|
269
|
+
// abort listener exists. Refuse the spawn instead of starting work after the user pressed stop.
|
|
270
|
+
if (signal?.aborted) {
|
|
271
|
+
if (memoryToken)
|
|
272
|
+
this.memory?.release(memoryToken);
|
|
273
|
+
throw new SubagentCancelled(id);
|
|
274
|
+
}
|
|
268
275
|
// npm exposes global CLIs as .cmd shims on Windows; Node requires a shell to launch those shims.
|
|
269
276
|
const child = spawn(shellQuote(this.pi.command), args, { cwd: childCwd, stdio: ["pipe", "pipe", "pipe"], shell: process.platform === "win32", ...(memoryToken ? { env: { ...process.env, SUPER_PI_MEMORY_ENDPOINT: process.env.SUPER_PI_MEMORY_ENDPOINT, SUPER_PI_MEMORY_TOKEN: memoryToken, SUPER_PI_MEMORY_READ_ONLY: "1" } } : {}) });
|
|
270
277
|
if (memoryToken)
|
|
@@ -156,8 +156,15 @@ export class ToolRunner {
|
|
|
156
156
|
// -I ignores user Python configuration and user site packages. No shell means params never become code.
|
|
157
157
|
const child = spawn(python.path, ["-I", "-u", input.script], { cwd: input.cwd, shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"], env: { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot, TEMP: process.env.TEMP, TMP: process.env.TMP, PYTHONIOENCODING: "utf-8", PYTHONNOUSERSITE: "1", SUPER_PI_TOOL_ID: input.toolId } });
|
|
158
158
|
active.child = child;
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
// A PIPE CARRIES BYTES, AND A READ BOUNDARY CAN LAND INSIDE A CHARACTER. A Chinese character is
|
|
160
|
+
// three bytes, so decoding one chunk at a time turns every character that straddles a boundary
|
|
161
|
+
// into U+FFFD - the same output comes out damaged on one machine and whole on another. The
|
|
162
|
+
// stream decodes instead, and Node keeps an incomplete character until the rest of its bytes
|
|
163
|
+
// arrive.
|
|
164
|
+
child.stdout.setEncoding("utf8");
|
|
165
|
+
child.stderr.setEncoding("utf8");
|
|
166
|
+
child.stdout.on("data", (text) => { active.stdout = clipped(`${active.stdout}${text}`); });
|
|
167
|
+
child.stderr.on("data", (text) => logLine(active, text));
|
|
161
168
|
child.once("error", (error) => this.finish(id, "failed", error.message));
|
|
162
169
|
child.once("close", (code) => {
|
|
163
170
|
if (active.run.status !== "running")
|
|
@@ -210,8 +217,10 @@ function runCapture(python, args, timeoutMs, onOutput) {
|
|
|
210
217
|
}
|
|
211
218
|
catch { } resolve({ ok: false, code: null, stdout, error: `timed out after ${timeoutMs}ms` }); }, timeoutMs);
|
|
212
219
|
timer.unref();
|
|
213
|
-
child.stdout.
|
|
214
|
-
child.stderr.
|
|
220
|
+
child.stdout.setEncoding("utf8");
|
|
221
|
+
child.stderr.setEncoding("utf8");
|
|
222
|
+
child.stdout.on("data", (text) => { stdout += text; onOutput?.(text); });
|
|
223
|
+
child.stderr.on("data", (text) => { stderr += text; onOutput?.(text); });
|
|
215
224
|
child.once("error", (error) => { if (settled)
|
|
216
225
|
return; settled = true; clearTimeout(timer); resolve({ ok: false, code: null, stdout, error: error.message }); });
|
|
217
226
|
child.once("close", (code) => { if (settled)
|
package/dist/server/types.d.ts
CHANGED
|
@@ -464,6 +464,10 @@ export type Conversation = {
|
|
|
464
464
|
status: RunStatus;
|
|
465
465
|
messages: Message[];
|
|
466
466
|
archivedAt?: string;
|
|
467
|
+
/** Transport projection only: false means messages were omitted, rather than an empty transcript. */
|
|
468
|
+
messagesLoaded?: boolean;
|
|
469
|
+
/** Pending question summary, retained when a focused snapshot omits this conversation's messages. */
|
|
470
|
+
hasPendingQuestion?: boolean;
|
|
467
471
|
/** Pi's own resolved transcript path, observed from this conversation's live worker. It is only a
|
|
468
472
|
* locator: revert still verifies the file header and every retained boundary before writing. */
|
|
469
473
|
piSessionFile?: string;
|
|
@@ -484,6 +488,21 @@ export type Conversation = {
|
|
|
484
488
|
* would answer "what happened" with something the reader has to go and decode.
|
|
485
489
|
*/
|
|
486
490
|
error?: string;
|
|
491
|
+
/**
|
|
492
|
+
* WHY THIS CONVERSATION IS NOT ON THE MODEL THE USER SET FOR IT - in words a person reads.
|
|
493
|
+
*
|
|
494
|
+
* A default model can stop being usable between one session and the next: Pi may drop the model
|
|
495
|
+
* from its list, or the workspace's own model rules may forbid it. The conversation then starts on
|
|
496
|
+
* Pi's own default, which is the right behaviour - but doing it SILENTLY is not: the user set a
|
|
497
|
+
* default, they set a rule, and nobody told them the two contradicted each other (TEST-062).
|
|
498
|
+
*
|
|
499
|
+
* It lives on the conversation rather than on the workspace because that is where the decision
|
|
500
|
+
* was made: a default can be usable for one workspace and unusable for another, and two
|
|
501
|
+
* conversations of the same workspace can disagree if the rules changed in between. It is set
|
|
502
|
+
* ONCE, at birth, and never rewritten afterwards - it describes how this conversation came to be,
|
|
503
|
+
* not what is true right now.
|
|
504
|
+
*/
|
|
505
|
+
modelNotice?: string;
|
|
487
506
|
/** Absent means "Pi's own default", which is why a conversation created before the picker existed still works. */
|
|
488
507
|
model?: ModelSelection;
|
|
489
508
|
thinkingLevel?: string;
|
|
@@ -1194,6 +1213,14 @@ export type ClientCommandBody =
|
|
|
1194
1213
|
type: "stream.configure";
|
|
1195
1214
|
coalesce: boolean;
|
|
1196
1215
|
}
|
|
1216
|
+
/** Selects this socket's transcript; omitted conversationId requests sidebar summaries only. */
|
|
1217
|
+
| {
|
|
1218
|
+
type: "files.index.subscribe";
|
|
1219
|
+
workspaceId?: string;
|
|
1220
|
+
} | {
|
|
1221
|
+
type: "state.subscribe";
|
|
1222
|
+
conversationId?: string;
|
|
1223
|
+
}
|
|
1197
1224
|
/** Asks for the agent catalog read from disk. Answered by `agent.catalog`. */
|
|
1198
1225
|
| {
|
|
1199
1226
|
type: "agent.catalog.fetch";
|
|
@@ -4,6 +4,25 @@ import type { AppState, ServerEvent } from "./types.js";
|
|
|
4
4
|
import type { EventBroadcaster } from "./server-context.js";
|
|
5
5
|
import type { CommandHandler } from "./command-handler.js";
|
|
6
6
|
import type { CatalogService } from "./application/catalog-service.js";
|
|
7
|
+
/**
|
|
8
|
+
* A WebSocket is a live transport, not an unbounded queue. `ws.send()` only
|
|
9
|
+
* copies bytes into the socket's userland buffer; a browser that is frozen or
|
|
10
|
+
* a network peer that has disappeared can therefore make the buffer grow
|
|
11
|
+
* without throwing. Once this threshold is crossed we drop that connection
|
|
12
|
+
* and let the normal close path clean up its resources.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MAX_WS_BUFFERED_BYTES: number;
|
|
15
|
+
/** Maximum size of one inbound WebSocket message. Attachments are uploaded
|
|
16
|
+
* over HTTP and commands contain references, so multi-megabyte WS frames are
|
|
17
|
+
* almost certainly malformed or abusive. */
|
|
18
|
+
export declare const MAX_WS_PAYLOAD_BYTES: number;
|
|
19
|
+
/**
|
|
20
|
+
* Send one frame without allowing a race with close/error to escape as an
|
|
21
|
+
* uncaught exception. The `ws` package can throw synchronously after the
|
|
22
|
+
* readyState check (for example when a close happens between the two calls),
|
|
23
|
+
* and an unhandled `error` event can otherwise terminate the Node process.
|
|
24
|
+
*/
|
|
25
|
+
export declare function safeSend(socket: WebSocket, payload: string): boolean;
|
|
7
26
|
/**
|
|
8
27
|
* Concrete implementation of EventBroadcaster delivering server events
|
|
9
28
|
* across active WebSocket connections.
|
|
@@ -11,21 +30,31 @@ import type { CatalogService } from "./application/catalog-service.js";
|
|
|
11
30
|
export declare class WsBroadcaster implements EventBroadcaster {
|
|
12
31
|
private readonly getState;
|
|
13
32
|
private readonly sockets;
|
|
33
|
+
private readonly scopes;
|
|
34
|
+
private readonly fileIndexScopes;
|
|
14
35
|
private readonly coalescedSockets;
|
|
15
36
|
private readonly pendingStreams;
|
|
16
|
-
private cachedState?;
|
|
17
|
-
private cachedStatePayload?;
|
|
18
37
|
private stateFlushScheduled;
|
|
19
|
-
constructor(getState: (
|
|
20
|
-
|
|
38
|
+
constructor(getState: (scope?: {
|
|
39
|
+
conversationId?: string;
|
|
40
|
+
}) => AppState);
|
|
41
|
+
register(socket: WebSocket, scope?: {
|
|
42
|
+
conversationId?: string;
|
|
43
|
+
}): void;
|
|
21
44
|
unregister(socket: WebSocket): void;
|
|
45
|
+
subscribe(socket: WebSocket, conversationId?: string): void;
|
|
46
|
+
sendState(socket: WebSocket): void;
|
|
47
|
+
subscribeFileIndex(socket: WebSocket, workspaceId?: string): void;
|
|
48
|
+
private receives;
|
|
22
49
|
configureStreaming(socket: WebSocket, coalesce: boolean): void;
|
|
23
50
|
private flushStreaming;
|
|
24
51
|
private enqueueStreaming;
|
|
25
52
|
getSockets(): Set<WebSocket>;
|
|
26
53
|
broadcast(event: ServerEvent): void;
|
|
27
54
|
broadcastState(): void;
|
|
28
|
-
getStateSnapshot(
|
|
55
|
+
getStateSnapshot(scope?: {
|
|
56
|
+
conversationId?: string;
|
|
57
|
+
}): AppState;
|
|
29
58
|
}
|
|
30
59
|
export interface WsServerOptions {
|
|
31
60
|
httpServer: NodeHttpServer;
|