@evandu/super-pi 0.1.145 → 0.1.147
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-task/core.ts +74 -2
- package/agents/super-pi-task/index.ts +7 -0
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +11 -11
- package/dist/client/assets/{CodeMirrorEditor-A9KvbIkm.js → CodeMirrorEditor-DQCjEnxG.js} +2 -2
- package/dist/client/assets/{dist-DM4Olwfx.js → dist-1Innu17E.js} +1 -1
- package/dist/client/assets/{dist-bIDce5j-.js → dist-1lynFOc5.js} +1 -1
- package/dist/client/assets/{dist-BlxwDnWr.js → dist-B1qLZVMK.js} +1 -1
- package/dist/client/assets/{dist-BTVbZigv.js → dist-B4pz_xdd.js} +1 -1
- package/dist/client/assets/{dist-UJ46aPS3.js → dist-B6KwDpeI.js} +1 -1
- package/dist/client/assets/{dist-CK2KFFtT.js → dist-BJVI1Unv.js} +1 -1
- package/dist/client/assets/{dist-CRnm1rnO.js → dist-BWvzD8Pe.js} +1 -1
- package/dist/client/assets/{dist-DSHM-PAs.js → dist-BayWssd0.js} +1 -1
- package/dist/client/assets/{dist-B9DE7sc5.js → dist-Bk_pkBVH.js} +1 -1
- package/dist/client/assets/{dist-D-o7iQ8c.js → dist-BpeClAv0.js} +1 -1
- package/dist/client/assets/{dist-CQFASxyb.js → dist-BsmJg5PX.js} +1 -1
- package/dist/client/assets/{dist-Ds5gfeAi.js → dist-Bx7s4rni.js} +1 -1
- package/dist/client/assets/{dist-UPJPMjWv.js → dist-CDBKM9tL.js} +1 -1
- package/dist/client/assets/{dist-CIgjifH-.js → dist-CGzXCzCG.js} +1 -1
- package/dist/client/assets/{dist-BAiV8I_O.js → dist-CRxCwn-Z.js} +1 -1
- package/dist/client/assets/{dist-Br-AuQUG.js → dist-CTDgUnBG.js} +1 -1
- package/dist/client/assets/{dist-4JbFTwg2.js → dist-D1VfMWmf.js} +1 -1
- package/dist/client/assets/{dist-BYOpvW02.js → dist-DTZLNEci.js} +1 -1
- package/dist/client/assets/{dist-CQV5kOCE.js → dist-DuUi29km.js} +1 -1
- package/dist/client/assets/{dist-CjszAoXk.js → dist-F3S0t1oM.js} +1 -1
- package/dist/client/assets/{dist-2boAf_pN.js → dist-QZ-FAVJV.js} +1 -1
- package/dist/client/assets/{dist-B1p4YF9C.js → dist-u0nBg0fZ.js} +1 -1
- package/dist/client/assets/index-BMpqviwU.css +1 -0
- package/dist/client/assets/index-Dzyp_7sf.js +102 -0
- package/dist/client/index.html +2 -2
- package/dist/server/active-memory.d.ts +17 -0
- package/dist/server/active-memory.js +39 -0
- package/dist/server/agents.d.ts +1 -1
- package/dist/server/agents.js +3 -1
- package/dist/server/application/conversation-service.d.ts +1 -1
- package/dist/server/application/conversation-service.js +12 -4
- package/dist/server/application/run-service.d.ts +32 -2
- package/dist/server/application/run-service.js +296 -40
- package/dist/server/application/scheduled-task-service.d.ts +14 -1
- package/dist/server/application/scheduled-task-service.js +27 -2
- package/dist/server/application/session-orchestrator-service.d.ts +114 -3
- package/dist/server/application/session-orchestrator-service.js +406 -35
- package/dist/server/application/session-watchdog.d.ts +159 -0
- package/dist/server/application/session-watchdog.js +244 -0
- package/dist/server/command-handler.js +1 -1
- package/dist/server/conversation-usage.d.ts +19 -0
- package/dist/server/conversation-usage.js +40 -0
- package/dist/server/file-diff.d.ts +3 -0
- package/dist/server/file-diff.js +4 -4
- package/dist/server/file-index.d.ts +4 -2
- package/dist/server/file-index.js +14 -9
- package/dist/server/file-name-index.d.ts +5 -2
- package/dist/server/file-name-index.js +22 -5
- package/dist/server/git.d.ts +3 -3
- package/dist/server/git.js +10 -9
- package/dist/server/http-server.js +256 -34
- package/dist/server/index.js +3 -0
- package/dist/server/knowledge-vectors.d.ts +15 -0
- package/dist/server/knowledge-vectors.js +118 -37
- package/dist/server/knowledge.d.ts +7 -8
- package/dist/server/knowledge.js +133 -55
- package/dist/server/length-continuation.d.ts +50 -0
- package/dist/server/length-continuation.js +55 -0
- package/dist/server/model-context-window.d.ts +46 -0
- package/dist/server/model-context-window.js +121 -0
- package/dist/server/pi-session-events.js +5 -1
- package/dist/server/pi-session-pool.d.ts +2 -0
- package/dist/server/pi-session-pool.js +16 -0
- package/dist/server/pi-session.d.ts +6 -0
- package/dist/server/pi-session.js +15 -1
- package/dist/server/runtime.d.ts +3 -0
- package/dist/server/runtime.js +23 -7
- package/dist/server/session-cwd.d.ts +17 -0
- package/dist/server/session-cwd.js +43 -0
- package/dist/server/session-ledger.d.ts +7 -0
- package/dist/server/session-tree-scope.d.ts +125 -0
- package/dist/server/session-tree-scope.js +337 -0
- package/dist/server/store.d.ts +35 -0
- package/dist/server/store.js +87 -2
- package/dist/server/task-tool.d.ts +2 -1
- package/dist/server/task-tool.js +4 -1
- package/dist/server/terminal.d.ts +1 -0
- package/dist/server/terminal.js +5 -3
- package/dist/server/turn-failure.d.ts +26 -0
- package/dist/server/turn-failure.js +67 -1
- package/dist/server/types.d.ts +33 -1
- package/dist/server/version.d.ts +25 -2
- package/dist/server/version.js +98 -26
- package/dist/server/ws-server.d.ts +1 -1
- package/dist/server/ws-server.js +7 -5
- package/package.json +36 -3
- package/dist/client/assets/index-BGsP8pt_.js +0 -100
- package/dist/client/assets/index-CiIc734l.css +0 -1
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { capText } from "../workflow.js";
|
|
4
|
+
import { assertSessionCwdInsideWorkspace } from "../session-cwd.js";
|
|
5
|
+
import { SessionWatchdog } from "./session-watchdog.js";
|
|
6
|
+
import { isLengthExceededError } from "../turn-failure.js";
|
|
7
|
+
import { LENGTH_CONTINUATION_PROMPT } from "../length-continuation.js";
|
|
4
8
|
export const SESSION_OUTPUT_CHAR_LIMIT = 64 * 1024;
|
|
5
9
|
export const sessionResultRelativePath = (conversationId) => `.super-pi/session-results/${conversationId}.md`;
|
|
10
|
+
/**
|
|
11
|
+
* HOW MANY TIMES A CHILD MAY BE TOLD TO CONTINUE AFTER ITS OUTPUT WAS TRUNCATED.
|
|
12
|
+
*
|
|
13
|
+
* The bound is not about tokens - it is about a model that truncates on EVERY response. Three
|
|
14
|
+
* continuations cover the ordinary case (a long answer split across a few responses) and stop the
|
|
15
|
+
* pathological one. Past the bound the child is reported as unfinished, which is a true sentence
|
|
16
|
+
* about what happened, instead of a loop that never ends.
|
|
17
|
+
*/
|
|
18
|
+
export const MAX_LENGTH_CONTINUATIONS = 3;
|
|
6
19
|
/**
|
|
7
20
|
* Orchestrator service coordinating sibling and child conversations.
|
|
8
21
|
*
|
|
@@ -15,25 +28,90 @@ export class SessionOrchestratorService {
|
|
|
15
28
|
runService;
|
|
16
29
|
ledger;
|
|
17
30
|
broadcaster;
|
|
18
|
-
|
|
31
|
+
watchdog;
|
|
32
|
+
/**
|
|
33
|
+
* HOW MANY TIMES EACH CHILD HAS BEEN TOLD TO CONTINUE AFTER A TRUNCATION.
|
|
34
|
+
*
|
|
35
|
+
* Per child rather than per dispatch, so a child that is given several tasks cannot extend itself
|
|
36
|
+
* indefinitely by being handed a new task each time it runs out of output budget. The entry is
|
|
37
|
+
* dropped as soon as the child finishes, so a child that continues successfully starts fresh on
|
|
38
|
+
* its next task.
|
|
39
|
+
*/
|
|
40
|
+
lengthContinuations = new Map();
|
|
41
|
+
/**
|
|
42
|
+
* THE CHILDREN THAT HAVE ALREADY REPORTED THEIR OWN RESULT TO THEIR PARENT.
|
|
43
|
+
*
|
|
44
|
+
* A child can report explicitly - the `report` action - and that report is the child's own
|
|
45
|
+
* statement of what it did. When the child's turn then ends, the automatic completion report
|
|
46
|
+
* would deliver the same thing a second time, so the automatic path steps aside for a child that
|
|
47
|
+
* is in this set.
|
|
48
|
+
*
|
|
49
|
+
* The entry is added only by a TERMINAL report (`completed` / `failed`), because an interim
|
|
50
|
+
* update is not the child's final word: the task is still running, and the parent will want to
|
|
51
|
+
* hear how it ended.
|
|
52
|
+
*/
|
|
53
|
+
reportedChildren = new Set();
|
|
54
|
+
constructor(store, conversationService, runService, ledger, broadcaster, watchdog) {
|
|
19
55
|
this.store = store;
|
|
20
56
|
this.conversationService = conversationService;
|
|
21
57
|
this.runService = runService;
|
|
22
58
|
this.ledger = ledger;
|
|
23
59
|
this.broadcaster = broadcaster;
|
|
60
|
+
this.watchdog = watchdog ?? new SessionWatchdog({
|
|
61
|
+
onTimeout: async (event) => {
|
|
62
|
+
await this.handleWatchdogTimeout(event);
|
|
63
|
+
},
|
|
64
|
+
workspacePath: (workspaceId) => this.store.workspace(workspaceId)?.path,
|
|
65
|
+
workspaceIds: () => this.store.workspaces().map((w) => w.id),
|
|
66
|
+
isChildWorking: (childConversationId) => {
|
|
67
|
+
const conversation = this.store.conversation(childConversationId);
|
|
68
|
+
return conversation ? (conversation.status === "running" || conversation.status === "waiting") : false;
|
|
69
|
+
},
|
|
70
|
+
warn: (message) => console.warn(message),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The watchdog timer fired: the child has been silent for longer than the parent declared.
|
|
75
|
+
* Delivers a reminder message to the parent session. Does NOT interrupt or abort the child session.
|
|
76
|
+
*/
|
|
77
|
+
async handleWatchdogTimeout(event) {
|
|
78
|
+
const { childConversationId, parentConversationId, timeoutSeconds, elapsedSeconds, childWorking } = event;
|
|
79
|
+
const conversation = this.store.conversation(childConversationId);
|
|
80
|
+
const workspace = conversation ? this.store.workspace(conversation.workspaceId) : undefined;
|
|
81
|
+
const statusText = childWorking ? "运行中" : "空闲";
|
|
82
|
+
const minutes = Math.max(1, Math.round(elapsedSeconds / 60));
|
|
83
|
+
const output = `[Watchdog 提醒] 子会话 ${childConversationId} 已经超过 ${elapsedSeconds} 秒(约 ${minutes} 分钟)未汇报。当前状态:${statusText}。如果需要,你可以主动查看子会话状态或打断其执行。`;
|
|
84
|
+
const resultPath = sessionResultRelativePath(childConversationId);
|
|
85
|
+
if (workspace) {
|
|
86
|
+
try {
|
|
87
|
+
const resultDirectory = join(workspace.path, ".super-pi", "session-results");
|
|
88
|
+
await mkdir(resultDirectory, { recursive: true });
|
|
89
|
+
await writeFile(join(resultDirectory, `${childConversationId}.md`), output, "utf8");
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.warn(`Super Pi: writing watchdog result file failed for ${childConversationId}:`, error instanceof Error ? error.message : String(error));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
await this.runService.enqueueSessionResult({
|
|
96
|
+
conversationId: childConversationId,
|
|
97
|
+
parentConversationId,
|
|
98
|
+
output,
|
|
99
|
+
resultPath,
|
|
100
|
+
});
|
|
24
101
|
}
|
|
25
102
|
/**
|
|
26
103
|
* Spawns a new sibling conversation and immediately dispatches its initial task.
|
|
27
104
|
*/
|
|
28
105
|
async spawn(options) {
|
|
29
|
-
|
|
30
|
-
if (!workspaceId && options.parentConversationId) {
|
|
31
|
-
const parent = this.store.conversation(options.parentConversationId);
|
|
32
|
-
workspaceId = parent?.workspaceId;
|
|
33
|
-
}
|
|
106
|
+
const workspaceId = options.workspaceId?.trim();
|
|
34
107
|
if (!workspaceId) {
|
|
35
|
-
throw new Error("
|
|
108
|
+
throw new Error("创建兄弟会话必须显式指定 workspaceId;父会话只记录树形关系,不能代替目标工作区。");
|
|
36
109
|
}
|
|
110
|
+
const workspace = this.store.workspace(workspaceId);
|
|
111
|
+
if (!workspace)
|
|
112
|
+
throw new Error("找不到这个工作区。");
|
|
113
|
+
await assertSessionCwdInsideWorkspace(workspace.path, options.cwd);
|
|
114
|
+
const cwd = options.cwd;
|
|
37
115
|
// THE PARENTAGE IS WRITTEN AT CREATION TIME, onto the conversation itself - not only into the
|
|
38
116
|
// in-memory ledger below. The ledger is task/status/progress metadata and dies with the process;
|
|
39
117
|
// the conversation's parentConversationId/origin is what the sidebar's session tree redraws
|
|
@@ -41,7 +119,7 @@ export class SessionOrchestratorService {
|
|
|
41
119
|
const conversationId = await this.conversationService.createConversation(workspaceId, {
|
|
42
120
|
parentConversationId: options.parentConversationId,
|
|
43
121
|
origin: options.parentConversationId ? "orchestrator" : "user",
|
|
44
|
-
});
|
|
122
|
+
}, cwd);
|
|
45
123
|
// Set title if requested or derive from task
|
|
46
124
|
if (options.title) {
|
|
47
125
|
await this.conversationService.renameConversation(conversationId, options.title);
|
|
@@ -69,6 +147,16 @@ export class SessionOrchestratorService {
|
|
|
69
147
|
task: options.task,
|
|
70
148
|
status: "idle",
|
|
71
149
|
});
|
|
150
|
+
// Arm silence watchdog if configured and session has a direct parent
|
|
151
|
+
const watchdogTimeout = options.watchdogTimeoutSeconds ?? options.watchdog_timeout_seconds ?? options.watchdogSeconds;
|
|
152
|
+
if (watchdogTimeout && watchdogTimeout > 0 && options.parentConversationId) {
|
|
153
|
+
await this.watchdog.arm({
|
|
154
|
+
childConversationId: conversationId,
|
|
155
|
+
parentConversationId: options.parentConversationId,
|
|
156
|
+
workspaceId,
|
|
157
|
+
timeoutSeconds: watchdogTimeout,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
72
160
|
// Immediately dispatch initial work
|
|
73
161
|
const prompt = options.initialPrompt ?? options.task;
|
|
74
162
|
if (prompt && prompt.trim()) {
|
|
@@ -88,7 +176,11 @@ export class SessionOrchestratorService {
|
|
|
88
176
|
this.ledger.update(conversationId, {
|
|
89
177
|
status: "running",
|
|
90
178
|
});
|
|
91
|
-
|
|
179
|
+
// THE ORDER IS THE SYSTEM'S, NOT THE READER'S. It used to go through `enqueueAndRun`, which
|
|
180
|
+
// writes the words under the reader's own name - so the child's transcript showed an
|
|
181
|
+
// instruction the reader never typed, wearing the reader's avatar, and a reader who looked at
|
|
182
|
+
// the child session saw their own name on somebody else's order.
|
|
183
|
+
const runPromise = this.runService.enqueueAndRunAsAgent(conversationId, content);
|
|
92
184
|
const completion = (async () => {
|
|
93
185
|
let failure;
|
|
94
186
|
try {
|
|
@@ -100,17 +192,62 @@ export class SessionOrchestratorService {
|
|
|
100
192
|
failure = error;
|
|
101
193
|
await this.runService.waitForConversationIdle(conversationId);
|
|
102
194
|
}
|
|
195
|
+
// A TRUNCATED ANSWER IS NOT A DEAD CHILD. The model filled its single-response output budget
|
|
196
|
+
// and stopped mid-sentence; everything it said is already on the child's transcript, and the
|
|
197
|
+
// work it was doing is NOT over. Treating that as a failure is what killed the child: the run
|
|
198
|
+
// service stamped the conversation `failed`, the orchestrator reported a failure to the
|
|
199
|
+
// parent, and a task that was one turn from done was thrown away.
|
|
200
|
+
//
|
|
201
|
+
// So a length stop is CONTINUED, not reported. The child is told to pick up where the output
|
|
202
|
+
// stopped, and it runs again. The bound is real: a model that truncates forever would
|
|
203
|
+
// otherwise loop until the watchdog gave up, and a task that cannot finish is better REPORTED
|
|
204
|
+
// as unfinished than retried until the server falls over.
|
|
205
|
+
//
|
|
206
|
+
// A CONTINUED CHILD IS NOT A FAILED ONE, SO THE FAILURE IS CLEARED. Everything below reads
|
|
207
|
+
// `failure` to decide what the parent is told and what the ledger records; a child that
|
|
208
|
+
// finished its answer has neither a failure to report nor a result to hand over - the parent
|
|
209
|
+
// reads the child's session, exactly as it does for a child that finished normally.
|
|
210
|
+
if (failure !== undefined && isLengthExceededError(failure)) {
|
|
211
|
+
const continued = await this.continueAfterLengthStop(conversationId, failure);
|
|
212
|
+
if (continued)
|
|
213
|
+
failure = undefined;
|
|
214
|
+
}
|
|
215
|
+
// THE RESULT FILE IS ALWAYS WRITTEN, AND THE PARENT IS ALWAYS TOLD.
|
|
216
|
+
//
|
|
217
|
+
// The file is the child's product and the reader may open it. The report is a message to the
|
|
218
|
+
// parent, and it goes out WHETHER THE CHILD SUCCEEDED OR FAILED - because a parent that
|
|
219
|
+
// delegated a task has to be told how it went. A parent that heard nothing would wait forever,
|
|
220
|
+
// or would have to poll the child's session to find out, which is exactly what the automatic
|
|
221
|
+
// report exists to avoid.
|
|
222
|
+
//
|
|
223
|
+
// The envelope already tells the two apart: `state="completed"` with `<task_result>` for a
|
|
224
|
+
// child that finished, `state="error"` with `<task_error>` for one that broke. Reporting only
|
|
225
|
+
// the failures was a real defect - it left a parent that had successfully delegated a task
|
|
226
|
+
// staring at an empty transcript, and the child looking finished to everyone except the one
|
|
227
|
+
// person waiting for it.
|
|
103
228
|
try {
|
|
104
229
|
const result = await this.persistSessionResult(conversationId, failure);
|
|
105
230
|
if (result) {
|
|
106
231
|
const current = this.store.conversation(conversationId);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
232
|
+
// AN INTERRUPTED CHILD HAS NO RESULT TO REPORT. "stopped" means somebody pressed stop, and
|
|
233
|
+
// the half-finished answer that was on screen is not a result anybody stated. The parent is
|
|
234
|
+
// told nothing, which is right: the child can be asked again, and then it reports.
|
|
235
|
+
const interrupted = current?.status === "stopped";
|
|
236
|
+
// A CHILD THAT REPORTED ITSELF IS NOT REPORTED TWICE. The explicit report is the child's
|
|
237
|
+
// own statement of what it did, so the automatic completion steps aside for it.
|
|
238
|
+
const alreadyReported = this.reportedChildren.has(conversationId);
|
|
239
|
+
if (!interrupted && !alreadyReported) {
|
|
240
|
+
await this.runService.enqueueSessionResult({
|
|
241
|
+
conversationId,
|
|
242
|
+
parentConversationId: current?.parentConversationId,
|
|
243
|
+
output: result.output,
|
|
244
|
+
resultPath: result.resultPath,
|
|
245
|
+
// THE ERROR GOES ONLY WHEN THERE IS ONE. `enqueueSessionResult` reads the presence of
|
|
246
|
+
// `error` as "this failed", so a successful report must NOT carry the key - carrying it
|
|
247
|
+
// with the output would tell the parent the child failed.
|
|
248
|
+
...(failure !== undefined ? { error: result.output } : {}),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
114
251
|
}
|
|
115
252
|
}
|
|
116
253
|
catch (error) {
|
|
@@ -130,11 +267,174 @@ export class SessionOrchestratorService {
|
|
|
130
267
|
void completion.catch(() => undefined);
|
|
131
268
|
return { conversationId };
|
|
132
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* THE CHILD'S OUTPUT WAS CUT OFF. TELL IT TO CARRY ON, AND LET IT.
|
|
272
|
+
*
|
|
273
|
+
* This is the recovery for a length stop, and it exists because a truncated answer is not a
|
|
274
|
+
* finished one. The child has already said most of what it had to say - that text is on its
|
|
275
|
+
* transcript - so the work left to do is the tail of the answer, not the whole of it.
|
|
276
|
+
*
|
|
277
|
+
* WHY THE CONTINUATION IS A NEW TURN RATHER THAN A RESUME OF THE SAME ONE. Pi has already settled
|
|
278
|
+
* the turn that was truncated: the model stopped, the message was written, the run ended. There is
|
|
279
|
+
* no live turn to resume, so the only way to get more output is to ask again - and asking again in
|
|
280
|
+
* the SAME conversation is what makes the continuation work, because the child's own partial
|
|
281
|
+
* answer is in its context and the model can see exactly where it stopped.
|
|
282
|
+
*
|
|
283
|
+
* WHY IT IS AN AGENT-SENT ORDER RATHER THAN A USER MESSAGE. The person never typed this, and the
|
|
284
|
+
* child's transcript must not show words under their name that they never said - the same reason
|
|
285
|
+
* `dispatch` uses `enqueueAndRunAsAgent` in the first place.
|
|
286
|
+
*
|
|
287
|
+
* Returns true when the child was continued (and the caller must NOT report a failure), and false
|
|
288
|
+
* when the continuation could not be made or the bound was reached - in which case the caller
|
|
289
|
+
* reports the original failure, which is then the honest description of what happened.
|
|
290
|
+
*/
|
|
291
|
+
async continueAfterLengthStop(conversationId, failure) {
|
|
292
|
+
if (!this.store.conversation(conversationId))
|
|
293
|
+
return false;
|
|
294
|
+
// THE LOOP, BECAUSE A CONTINUATION CAN BE TRUNCATED TOO. A long answer often needs more than
|
|
295
|
+
// one continuation: each response fills its own output budget, so a three-part answer runs out
|
|
296
|
+
// of room twice before it is done. The bound counts every continuation this child has been
|
|
297
|
+
// given, so the loop cannot run forever - a model that truncates on EVERY response reaches
|
|
298
|
+
// the bound and is reported as unfinished, which is the honest sentence about what happened.
|
|
299
|
+
for (;;) {
|
|
300
|
+
const used = this.lengthContinuations.get(conversationId) ?? 0;
|
|
301
|
+
if (used >= MAX_LENGTH_CONTINUATIONS) {
|
|
302
|
+
console.warn(`Super Pi: session ${conversationId} hit the output limit ${MAX_LENGTH_CONTINUATIONS} times; reporting it as unfinished.`);
|
|
303
|
+
this.lengthContinuations.delete(conversationId);
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
this.lengthContinuations.set(conversationId, used + 1);
|
|
307
|
+
const reason = failure instanceof Error ? failure.message : String(failure);
|
|
308
|
+
console.warn(`Super Pi: session ${conversationId} was truncated (${reason}); asking it to continue (${used + 1}/${MAX_LENGTH_CONTINUATIONS}).`);
|
|
309
|
+
let continuationFailure;
|
|
310
|
+
try {
|
|
311
|
+
// THE ORDER GOES IN AS A SYSTEM MESSAGE, AND THE RUN IS WAITED FOR. The child must be given
|
|
312
|
+
// the chance to finish before anything is reported to the parent - otherwise the parent would
|
|
313
|
+
// receive a result that is missing the tail the child was about to write.
|
|
314
|
+
await this.runService.enqueueAndRunAsAgent(conversationId, LENGTH_CONTINUATION_PROMPT);
|
|
315
|
+
await this.runService.waitForConversationIdle(conversationId);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
continuationFailure = error;
|
|
319
|
+
}
|
|
320
|
+
if (continuationFailure !== undefined) {
|
|
321
|
+
// A CONTINUATION THAT IS ITSELF TRUNCATED IS THE SAME SITUATION AGAIN, AND THE LOOP
|
|
322
|
+
// CONTINUES. It is only a different thing when the continuation failed for a reason that
|
|
323
|
+
// is NOT the output limit - a provider error, a refused tool - and those are the child's
|
|
324
|
+
// real failure, reported through the normal path below.
|
|
325
|
+
if (isLengthExceededError(continuationFailure)) {
|
|
326
|
+
failure = continuationFailure;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
console.warn(`Super Pi: continuing session ${conversationId} failed:`, continuationFailure instanceof Error ? continuationFailure.message : String(continuationFailure));
|
|
330
|
+
this.lengthContinuations.delete(conversationId);
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
// THE CHILD SETTLED WITHOUT ANOTHER TRUNCATION. Its work is done: the answer on its transcript
|
|
334
|
+
// is now complete, and there is nothing to report to the parent - the parent reads the child's
|
|
335
|
+
// result from the session itself, exactly as it does for a child that finished normally.
|
|
336
|
+
//
|
|
337
|
+
// THE WARNING IS CLEARED HERE. It described the answer that was cut short, and the child has
|
|
338
|
+
// now finished that answer; leaving it up would tell the reader the answer is still truncated
|
|
339
|
+
// when it is not.
|
|
340
|
+
await this.store.setWarningReason(conversationId, undefined);
|
|
341
|
+
this.lengthContinuations.delete(conversationId);
|
|
342
|
+
this.ledger.update(conversationId, { status: "completed" });
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Explicit report from a child session to its direct parent.
|
|
348
|
+
* Can be called multiple times at any point during execution.
|
|
349
|
+
*/
|
|
350
|
+
async report(childConversationId, options) {
|
|
351
|
+
const conversation = this.store.conversation(childConversationId);
|
|
352
|
+
if (!conversation) {
|
|
353
|
+
throw new Error(`找不到会话:${childConversationId}`);
|
|
354
|
+
}
|
|
355
|
+
const parentConversationId = conversation.parentConversationId;
|
|
356
|
+
if (!parentConversationId) {
|
|
357
|
+
throw new Error(`会话 ${childConversationId} 没有直属父会话,无法汇报`);
|
|
358
|
+
}
|
|
359
|
+
const parent = this.store.conversation(parentConversationId);
|
|
360
|
+
if (!parent) {
|
|
361
|
+
throw new Error(`直属父会话 ${parentConversationId} 不存在`);
|
|
362
|
+
}
|
|
363
|
+
const content = options.content ?? "";
|
|
364
|
+
const status = options.status ?? "in_progress";
|
|
365
|
+
const failed = status === "failed";
|
|
366
|
+
const workspace = this.store.workspace(conversation.workspaceId);
|
|
367
|
+
const resultPath = sessionResultRelativePath(childConversationId);
|
|
368
|
+
if (workspace) {
|
|
369
|
+
const resultDirectory = join(workspace.path, ".super-pi", "session-results");
|
|
370
|
+
await mkdir(resultDirectory, { recursive: true });
|
|
371
|
+
await writeFile(join(resultDirectory, `${childConversationId}.md`), content, "utf8");
|
|
372
|
+
}
|
|
373
|
+
await this.runService.enqueueSessionResult({
|
|
374
|
+
conversationId: childConversationId,
|
|
375
|
+
parentConversationId,
|
|
376
|
+
output: content,
|
|
377
|
+
resultPath,
|
|
378
|
+
...(failed ? { error: content } : {}),
|
|
379
|
+
});
|
|
380
|
+
// THE CHILD HAS NOW SPOKEN FOR ITSELF. The automatic report that fires when the child's turn
|
|
381
|
+
// ends would otherwise repeat what was just delivered - the parent would receive the same
|
|
382
|
+
// answer twice, once as the child's own report and once as the automatic completion.
|
|
383
|
+
//
|
|
384
|
+
// An EXPLICIT report is the child's own statement of what it did, so it is the better of the
|
|
385
|
+
// two and the automatic one steps aside. A report that is NOT terminal (an interim update)
|
|
386
|
+
// does not silence the automatic report: the child is still working, and the parent will want
|
|
387
|
+
// to hear how the task ended.
|
|
388
|
+
if (status === "completed" || status === "failed") {
|
|
389
|
+
this.reportedChildren.add(childConversationId);
|
|
390
|
+
await this.watchdog.cancel(childConversationId);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
await this.watchdog.noteSignal(childConversationId);
|
|
394
|
+
}
|
|
395
|
+
return { delivered: true, parentConversationId };
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Sets or updates a silence watchdog timer on a child session.
|
|
399
|
+
*/
|
|
400
|
+
async setWatchdog(options) {
|
|
401
|
+
const child = this.store.conversation(options.childConversationId);
|
|
402
|
+
if (!child) {
|
|
403
|
+
throw new Error(`找不到会话:${options.childConversationId}`);
|
|
404
|
+
}
|
|
405
|
+
await this.watchdog.arm({
|
|
406
|
+
childConversationId: options.childConversationId,
|
|
407
|
+
parentConversationId: options.parentConversationId,
|
|
408
|
+
workspaceId: child.workspaceId,
|
|
409
|
+
timeoutSeconds: options.timeoutSeconds,
|
|
410
|
+
});
|
|
411
|
+
return { ok: true, childConversationId: options.childConversationId, timeoutSeconds: options.timeoutSeconds };
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Cancels a silence watchdog timer on a child session.
|
|
415
|
+
*/
|
|
416
|
+
async cancelWatchdog(childConversationId) {
|
|
417
|
+
await this.watchdog.cancel(childConversationId);
|
|
418
|
+
return { ok: true };
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Re-arms persisted watchdogs from disk.
|
|
422
|
+
*/
|
|
423
|
+
async restoreWatchdogs() {
|
|
424
|
+
await this.watchdog.restore();
|
|
425
|
+
}
|
|
133
426
|
async persistSessionResult(conversationId, failure) {
|
|
134
427
|
const conversation = this.store.conversation(conversationId);
|
|
135
428
|
const workspace = conversation ? this.store.workspace(conversation.workspaceId) : undefined;
|
|
136
429
|
if (!conversation || !workspace)
|
|
137
430
|
return undefined;
|
|
431
|
+
// THE FILE IS THE CHILD'S PRODUCT, AND THE REPORT USES IT. They used to be the same thing, and
|
|
432
|
+
// the split is what the user asked for:
|
|
433
|
+
// - the FILE is written on every turn, so the reader can open it and `status(includeOutput)`
|
|
434
|
+
// can return what the child actually produced;
|
|
435
|
+
// - the REPORT to the parent carries that same text, because the parent is waiting for an
|
|
436
|
+
// answer. A failure reports the DIAGNOSIS rather than whatever the model managed to say,
|
|
437
|
+
// because a broken turn's last words are not a diagnosis of why it broke.
|
|
138
438
|
const output = failure !== undefined
|
|
139
439
|
? (failure instanceof Error ? failure.message : String(failure))
|
|
140
440
|
: ([...conversation.messages].reverse().find((message) => message.role === "assistant")?.content ?? "");
|
|
@@ -175,11 +475,22 @@ export class SessionOrchestratorService {
|
|
|
175
475
|
if (conversation.status === "running" || conversation.status === "waiting") {
|
|
176
476
|
const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
|
|
177
477
|
if (lastAssistant?.toolCalls?.length) {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
478
|
+
const isCompleted = (t) => t.status === "ok" || t.status === "error" || t.status === "done" || t.status === "failed";
|
|
479
|
+
const isRunning = (t) => !isCompleted(t) && (Boolean(t.preparing) || Boolean(t.live) || !t.status || t.status === "running");
|
|
480
|
+
const runningCall = [...lastAssistant.toolCalls].reverse().find(isRunning)
|
|
481
|
+
?? (!isCompleted(lastAssistant.toolCalls[lastAssistant.toolCalls.length - 1])
|
|
482
|
+
? lastAssistant.toolCalls[lastAssistant.toolCalls.length - 1]
|
|
483
|
+
: undefined);
|
|
484
|
+
if (runningCall) {
|
|
485
|
+
activeTool = {
|
|
486
|
+
label: runningCall.label || runningCall.name || "tool",
|
|
487
|
+
detail: runningCall.detail || runningCall.live,
|
|
488
|
+
};
|
|
181
489
|
}
|
|
182
490
|
}
|
|
491
|
+
if (!activeTool && ledgerEntry?.activeTool) {
|
|
492
|
+
activeTool = { ...ledgerEntry.activeTool };
|
|
493
|
+
}
|
|
183
494
|
}
|
|
184
495
|
const hasPendingQuestion = this.store.pendingQuestions(conversationId).length > 0;
|
|
185
496
|
let output;
|
|
@@ -200,54 +511,113 @@ export class SessionOrchestratorService {
|
|
|
200
511
|
}
|
|
201
512
|
}
|
|
202
513
|
}
|
|
514
|
+
const effectiveStatus = conversation.error
|
|
515
|
+
? "failed"
|
|
516
|
+
: conversation.status;
|
|
517
|
+
const finalProgress = lastAssistantSnippet ?? ledgerEntry?.lastProgress ?? lastUserSnippet;
|
|
203
518
|
// Sync ledger status if present
|
|
204
|
-
if (ledgerEntry && ledgerEntry.status !==
|
|
519
|
+
if (ledgerEntry && (ledgerEntry.status !== effectiveStatus || ledgerEntry.activeTool !== activeTool || ledgerEntry.lastProgress !== finalProgress)) {
|
|
205
520
|
this.ledger.update(conversationId, {
|
|
206
|
-
status:
|
|
207
|
-
lastProgress:
|
|
521
|
+
status: effectiveStatus,
|
|
522
|
+
lastProgress: finalProgress,
|
|
523
|
+
activeTool,
|
|
524
|
+
...(conversation.error ? { error: conversation.error } : {}),
|
|
208
525
|
});
|
|
209
526
|
}
|
|
210
527
|
return {
|
|
211
528
|
conversationId: conversation.id,
|
|
212
529
|
workspaceId: conversation.workspaceId,
|
|
213
530
|
title: conversation.title,
|
|
214
|
-
status:
|
|
215
|
-
origin: ledgerEntry?.origin ?? "user",
|
|
531
|
+
status: effectiveStatus,
|
|
532
|
+
origin: ledgerEntry?.origin ?? conversation.origin ?? "user",
|
|
216
533
|
parentConversationId: conversation.parentConversationId ?? ledgerEntry?.parentConversationId,
|
|
217
|
-
task: ledgerEntry?.task,
|
|
534
|
+
task: ledgerEntry?.task ?? (conversation.title && conversation.title !== "新会话" ? conversation.title : undefined),
|
|
218
535
|
messageCount: messages.length,
|
|
219
536
|
turnCount,
|
|
220
537
|
lastAssistantSnippet,
|
|
221
538
|
lastUserSnippet,
|
|
222
539
|
hasPendingQuestion,
|
|
223
540
|
activeTool,
|
|
224
|
-
error: conversation.error,
|
|
541
|
+
error: conversation.error ?? ledgerEntry?.error,
|
|
225
542
|
updatedAt: conversation.updatedAt,
|
|
226
|
-
createdAt: ledgerEntry?.createdAt,
|
|
227
|
-
lastProgress:
|
|
543
|
+
createdAt: ledgerEntry?.createdAt ?? (conversation.messages?.[0]?.createdAt ? new Date(conversation.messages[0].createdAt).getTime() : (conversation.updatedAt ? new Date(conversation.updatedAt).getTime() : Date.now())),
|
|
544
|
+
lastProgress: finalProgress,
|
|
228
545
|
...(options?.includeOutput ? { outputPath: resultPath } : {}),
|
|
229
546
|
...(output !== undefined ? { output, outputTruncated } : {}),
|
|
230
547
|
};
|
|
231
548
|
}
|
|
232
549
|
/**
|
|
233
550
|
* Lists summaries of all sessions, optionally filtered by parent conversation.
|
|
551
|
+
*
|
|
552
|
+
* Persisted store conversations are the authoritative source for session tree
|
|
553
|
+
* parent-child relationships across server restarts, while SessionLedger
|
|
554
|
+
* enriches live in-memory task, progress, and activeTool state.
|
|
234
555
|
*/
|
|
235
556
|
async list(parentConversationId) {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
557
|
+
const targetParentId = parentConversationId?.trim();
|
|
558
|
+
const candidateIds = new Set();
|
|
559
|
+
if (targetParentId) {
|
|
560
|
+
// 1. Check parent in store to know parent's workspace
|
|
561
|
+
const parentConv = this.store.conversation(targetParentId);
|
|
562
|
+
if (parentConv) {
|
|
563
|
+
const workspaceChildren = this.store.conversationsForWorkspace(parentConv.workspaceId);
|
|
564
|
+
for (const child of workspaceChildren) {
|
|
565
|
+
if (child.parentConversationId === targetParentId) {
|
|
566
|
+
candidateIds.add(child.id);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
// Fallback: check all conversations in store
|
|
572
|
+
const allConversations = this.store.snapshot().conversations;
|
|
573
|
+
for (const conv of allConversations) {
|
|
574
|
+
if (conv.parentConversationId === targetParentId) {
|
|
575
|
+
candidateIds.add(conv.id);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// 2. Also check in-memory ledger entries
|
|
580
|
+
const ledgerEntries = this.ledger.listByParent(targetParentId);
|
|
581
|
+
for (const entry of ledgerEntries) {
|
|
582
|
+
candidateIds.add(entry.conversationId);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
// No parent specified: list all orchestrator / child sessions
|
|
587
|
+
for (const conv of this.store.snapshot().conversations) {
|
|
588
|
+
if (conv.parentConversationId || conv.origin === "orchestrator") {
|
|
589
|
+
candidateIds.add(conv.id);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
for (const entry of this.ledger.listAll()) {
|
|
593
|
+
candidateIds.add(entry.conversationId);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const parentConv = targetParentId ? this.store.conversation(targetParentId) : undefined;
|
|
239
597
|
const summaries = [];
|
|
240
|
-
for (const
|
|
598
|
+
for (const conversationId of candidateIds) {
|
|
241
599
|
try {
|
|
242
|
-
const summary = await this.status(
|
|
600
|
+
const summary = await this.status(conversationId);
|
|
601
|
+
// Workspace boundary enforcement: do not leak sessions across different workspaces
|
|
602
|
+
if (parentConv && summary.workspaceId !== parentConv.workspaceId) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (targetParentId && summary.parentConversationId !== targetParentId) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
243
608
|
summaries.push(summary);
|
|
244
609
|
}
|
|
245
610
|
catch {
|
|
246
611
|
// Conversation was removed from store; prune ledger
|
|
247
|
-
this.ledger.delete(
|
|
612
|
+
this.ledger.delete(conversationId);
|
|
248
613
|
}
|
|
249
614
|
}
|
|
250
|
-
|
|
615
|
+
// Sort newest first by creation time or updated timestamp
|
|
616
|
+
return summaries.sort((a, b) => {
|
|
617
|
+
const timeA = a.createdAt ?? (a.updatedAt ? new Date(a.updatedAt).getTime() : 0);
|
|
618
|
+
const timeB = b.createdAt ?? (b.updatedAt ? new Date(b.updatedAt).getTime() : 0);
|
|
619
|
+
return timeB - timeA;
|
|
620
|
+
});
|
|
251
621
|
}
|
|
252
622
|
/**
|
|
253
623
|
* Stops a target session.
|
|
@@ -284,6 +654,7 @@ export class SessionOrchestratorService {
|
|
|
284
654
|
mode = "all";
|
|
285
655
|
}
|
|
286
656
|
this.ledger.update(conversationId, { status: "stopped" });
|
|
657
|
+
await this.watchdog.cancel(conversationId);
|
|
287
658
|
return { stopped: true, mode };
|
|
288
659
|
}
|
|
289
660
|
/**
|