@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
|
@@ -4,11 +4,13 @@ import { newSubagentRunId } from "../subagents.js";
|
|
|
4
4
|
import { readAgents } from "../agents.js";
|
|
5
5
|
import { verifyCapability } from "../capability.js";
|
|
6
6
|
import { capText, validateWorkflowPlan, workflowSummary } from "../workflow.js";
|
|
7
|
-
import { ensureNameIndex } from "../file-name-index.js";
|
|
8
|
-
import { formatTurnFailure } from "../turn-failure.js";
|
|
7
|
+
import { ensureNameIndex, recordWorkspacePath } from "../file-name-index.js";
|
|
8
|
+
import { formatTurnFailure, isLengthExceededError } from "../turn-failure.js";
|
|
9
|
+
import { isLengthContinuation, lengthContinuationText } from "../length-continuation.js";
|
|
9
10
|
import { describeThrownWithStack } from "../failure-text.js";
|
|
10
11
|
import { toWorkspaceRelative } from "../files.js";
|
|
11
12
|
import { refreshWorkspaceIndex } from "../file-index.js";
|
|
13
|
+
import { resolveSessionCwd } from "../session-cwd.js";
|
|
12
14
|
import { execFile } from "node:child_process";
|
|
13
15
|
import { promisify } from "node:util";
|
|
14
16
|
import { randomUUID } from "node:crypto";
|
|
@@ -56,10 +58,38 @@ async function expandAtReferences(store, workspace, content) {
|
|
|
56
58
|
* The envelope and the prose are NOT redundant: the envelope is what the reader sees in the transcript
|
|
57
59
|
* column, what they cancel or reorder; the prose is what leaves the server for Pi.
|
|
58
60
|
*/
|
|
61
|
+
/**
|
|
62
|
+
* THE TEXT THE MODEL IS HANDED WHEN A 后台回传 BECOMES ITS TURN.
|
|
63
|
+
*
|
|
64
|
+
* Pi's SDK has ONE entry point for a new turn - `session.prompt(text)` - and every text sent
|
|
65
|
+
* through it is a USER MESSAGE in Pi's transcript. There is no "the agent said this" channel to
|
|
66
|
+
* use. So a delivered result has to travel as prose, and the only question left is what that prose
|
|
67
|
+
* SAYS.
|
|
68
|
+
*
|
|
69
|
+
* IT USED TO SAY `[后台任务「...」已完成]`, which reads as a thing the PERSON said - a bracketed
|
|
70
|
+
* label, then the body, with nothing saying who is speaking. The model therefore answered it as a
|
|
71
|
+
* user would be answered ("已收到:[后台任务「...」]"), and the reader saw their own result coming
|
|
72
|
+
* back in the user's voice, inside the user's own turn. The delivery had become a quotation.
|
|
73
|
+
*
|
|
74
|
+
* So the prose now says WHO IS SPEAKING and that this is information, not an instruction:
|
|
75
|
+
* 「子会话「...」已完成,结果如下」. The model is told what happened and what it received, and it
|
|
76
|
+
* answers about THAT - which is what a primary agent should do with a child's result.
|
|
77
|
+
*/
|
|
59
78
|
function composePrompt(message) {
|
|
60
79
|
if (message.role !== "tool")
|
|
61
80
|
return message.content;
|
|
62
81
|
const content = message.content;
|
|
82
|
+
// A CONTINUATION ORDER IS NOT AN ORDER FROM A PARENT AGENT. It is the system telling the session
|
|
83
|
+
// to finish an answer that was cut short, and the model has to hear THAT - "here is a task you
|
|
84
|
+
// were given" would make it start the work over rather than finish what it had already said.
|
|
85
|
+
if (isLengthContinuation(content)) {
|
|
86
|
+
return `(上一条回答因达到输出长度上限被截断,请接着写完)\n\n${lengthContinuationText(content)}`;
|
|
87
|
+
}
|
|
88
|
+
// A DISPATCHED ORDER IS NOT A DELIVERED RESULT. The two both travel as a `tool` message, and the
|
|
89
|
+
// envelope tells them apart: a result carries `<task ...>`, an order does not.
|
|
90
|
+
if (!/<task id="[^"]+" state="[^"]+"/.test(content)) {
|
|
91
|
+
return `上级智能体下发的任务:\n\n${content}`;
|
|
92
|
+
}
|
|
63
93
|
const summaryMatch = /<summary>([\s\S]*?)<\/summary>/.exec(content);
|
|
64
94
|
const summary = summaryMatch ? summaryMatch[1].trim() : "后台任务";
|
|
65
95
|
const stateMatch = /<task id="[^"]+" state="([^"]+)"/.exec(content);
|
|
@@ -74,7 +104,10 @@ function composePrompt(message) {
|
|
|
74
104
|
const bodyMatch = /<task_(?:result|error)>\n?([\s\S]*)\n?<\/(?:task_result|task_error)>/.exec(content);
|
|
75
105
|
const body = bodyMatch ? bodyMatch[1] : content;
|
|
76
106
|
const stateLabel = state === "completed" ? "已完成" : state === "error" ? "失败" : state;
|
|
77
|
-
|
|
107
|
+
// THE SPEAKER IS NAMED, AND IT IS NOT THE READER. A line that starts 「[」 looks like a note the
|
|
108
|
+
// person pasted in; a line that starts 「子会话」 or 「后台任务」 says the SYSTEM is reporting.
|
|
109
|
+
const speaker = /kind="session"/.test(content) ? "子会话" : "后台任务";
|
|
110
|
+
return `${speaker}「${summary}」${stateLabel},结果如下:\n\n${body}`;
|
|
78
111
|
}
|
|
79
112
|
/**
|
|
80
113
|
* Service managing execution of conversation turns, message queueing,
|
|
@@ -286,20 +319,20 @@ export class RunService {
|
|
|
286
319
|
// "[后台任务 X 失败]" is part of the same "after the current answer finishes, the result is
|
|
287
320
|
// processed automatically" contract, and the auto-triggered turn goes through the same
|
|
288
321
|
// `composePrompt` path the success case does.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
// goes into Pi's follow-up queue any more.
|
|
322
|
+
//
|
|
323
|
+
// BUSY: steer the result into the running turn so Pi drains it at the very next tool batch
|
|
324
|
+
// boundary instead of waiting for the whole turn to complete.
|
|
325
|
+
void this.dispatchBackgroundResult(task.conversationId, queued.id).catch(async (error) => {
|
|
326
|
+
console.warn(`Super Pi: dispatch background exec result failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
327
|
+
await this.settleBackgroundStatus(task.conversationId);
|
|
328
|
+
});
|
|
297
329
|
}
|
|
298
330
|
catch (error) {
|
|
299
331
|
console.warn(`Super Pi: exec result could not be appended as a tool message: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
300
332
|
}
|
|
301
333
|
finally {
|
|
302
334
|
await this.store.removeBackgroundTask(sessionId);
|
|
335
|
+
await this.settleBackgroundStatus(task.conversationId);
|
|
303
336
|
this.broadcaster.broadcastState();
|
|
304
337
|
}
|
|
305
338
|
})());
|
|
@@ -345,13 +378,68 @@ export class RunService {
|
|
|
345
378
|
});
|
|
346
379
|
if (!queued)
|
|
347
380
|
return;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
});
|
|
352
|
-
}
|
|
381
|
+
void this.dispatchBackgroundResult(parentId, queued.id).catch((error) => {
|
|
382
|
+
console.warn(`Super Pi: dispatch child session result failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
383
|
+
});
|
|
353
384
|
})());
|
|
354
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Dispatches a background result (exec or child session) into the conversation.
|
|
388
|
+
*
|
|
389
|
+
* IDLE: starts a fresh turn via `processWaitingMessage`.
|
|
390
|
+
* BUSY: attempts to steer the message into the running turn so it is delivered at the
|
|
391
|
+
* very next tool batch boundary rather than waiting for the entire turn to complete.
|
|
392
|
+
* INTERRUPTING: quietly leaves the message waiting; the interrupted or next turn will pick it up.
|
|
393
|
+
*/
|
|
394
|
+
async dispatchBackgroundResult(conversationId, messageId) {
|
|
395
|
+
if (this.interruptTargets.has(conversationId))
|
|
396
|
+
return;
|
|
397
|
+
if (!this.activeRuns.has(conversationId)) {
|
|
398
|
+
await this.processWaitingMessage(conversationId, messageId);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
// Conversation is busy. Attempt to steer the background result directly into the active turn.
|
|
402
|
+
try {
|
|
403
|
+
// THE LANE IS RELEASED BEFORE THE FALLBACK. `processWaitingMessage` acquires this same lane,
|
|
404
|
+
// so calling it from inside the serialized callback re-enters the lane and deadlocks: the
|
|
405
|
+
// outer work waits for the inner one, and the inner one waits for the outer one.
|
|
406
|
+
const startWaiting = await this.serializeConversation(conversationId, async () => {
|
|
407
|
+
if (this.interruptTargets.has(conversationId))
|
|
408
|
+
return false;
|
|
409
|
+
// `processWaitingMessage` acquires this lane, so the fallback must run after serialization exits.
|
|
410
|
+
if (!this.activeRuns.has(conversationId))
|
|
411
|
+
return true;
|
|
412
|
+
if (!this.runtime.steer)
|
|
413
|
+
return false;
|
|
414
|
+
if (this.runtime.canSteer && !this.runtime.canSteer(conversationId))
|
|
415
|
+
return false;
|
|
416
|
+
const waiting = this.store.listWaitingMessages(conversationId);
|
|
417
|
+
const item = waiting.find((m) => m.id === messageId && m.delivery === "waiting");
|
|
418
|
+
if (!item)
|
|
419
|
+
return false;
|
|
420
|
+
await this.moveAheadOfUnclaimed(conversationId, messageId);
|
|
421
|
+
this.broadcaster.broadcastState();
|
|
422
|
+
const claimed = await this.store.claimWaitingMessage(conversationId, messageId);
|
|
423
|
+
this.broadcaster.broadcastState();
|
|
424
|
+
try {
|
|
425
|
+
await this.runtime.steer(conversationId, composePrompt(claimed), claimed.images);
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
await this.store.releaseWaitingMessage(conversationId, claimed.id);
|
|
429
|
+
this.broadcaster.broadcastState();
|
|
430
|
+
if (!(error instanceof SteerUnavailableError)) {
|
|
431
|
+
console.warn(`Super Pi: steer background result failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return false;
|
|
435
|
+
});
|
|
436
|
+
if (startWaiting)
|
|
437
|
+
await this.processWaitingMessage(conversationId, messageId);
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
console.warn(`Super Pi: steer background result failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
355
443
|
/**
|
|
356
444
|
* Stops an active background task (spawned process or detached subagent) on user request.
|
|
357
445
|
*
|
|
@@ -382,8 +470,29 @@ export class RunService {
|
|
|
382
470
|
}
|
|
383
471
|
}
|
|
384
472
|
await this.store.removeBackgroundTask(taskId);
|
|
473
|
+
if (task?.conversationId) {
|
|
474
|
+
await this.settleBackgroundStatus(task.conversationId);
|
|
475
|
+
}
|
|
385
476
|
this.broadcaster.broadcastState();
|
|
386
477
|
}
|
|
478
|
+
hasActiveBackgroundTasks(conversationId) {
|
|
479
|
+
const tasks = this.store.snapshot().backgroundTasks ?? [];
|
|
480
|
+
return tasks.some((t) => t.conversationId === conversationId && (t.status === "running" || !t.status));
|
|
481
|
+
}
|
|
482
|
+
async settleBackgroundStatus(conversationId) {
|
|
483
|
+
const conv = this.store.conversation(conversationId);
|
|
484
|
+
if (!conv || conv.status !== "awaiting_background")
|
|
485
|
+
return;
|
|
486
|
+
if (this.activeRuns.has(conversationId))
|
|
487
|
+
return;
|
|
488
|
+
if (this.hasActiveBackgroundTasks(conversationId))
|
|
489
|
+
return;
|
|
490
|
+
const hasWaiting = this.store.listWaitingMessages(conversationId).some(m => m.delivery === "waiting");
|
|
491
|
+
if (!hasWaiting) {
|
|
492
|
+
await this.store.setStatus(conversationId, "completed");
|
|
493
|
+
this.broadcaster.broadcastState();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
387
496
|
/** Per-conversation command lane. All queue mutations and run claims use the same lane, so
|
|
388
497
|
* two browser tabs cannot both observe an idle conversation and start duplicate turns. */
|
|
389
498
|
conversationOps = new Map();
|
|
@@ -563,9 +672,9 @@ export class RunService {
|
|
|
563
672
|
/**
|
|
564
673
|
* Resolves conversation and workspace working directory for subagent dispatch.
|
|
565
674
|
*/
|
|
566
|
-
dispatchContext(conversationId) {
|
|
675
|
+
async dispatchContext(conversationId) {
|
|
567
676
|
const { conversation, workspace } = this.turnContext(conversationId);
|
|
568
|
-
return { conversation, cwd: workspace.path };
|
|
677
|
+
return { conversation, cwd: await resolveSessionCwd(workspace.path, conversation.cwd) };
|
|
569
678
|
}
|
|
570
679
|
/**
|
|
571
680
|
* Checks whether a conversation has an active turn running.
|
|
@@ -663,6 +772,17 @@ export class RunService {
|
|
|
663
772
|
// the SDK's five-second backstop. Do this before any await so a stale stop cannot reach new work.
|
|
664
773
|
this.subagents.stopConversation(conversationId);
|
|
665
774
|
this.workflows.stopConversation(conversationId);
|
|
775
|
+
const bgTasks = (this.store.snapshot().backgroundTasks ?? []).filter(t => t.conversationId === conversationId);
|
|
776
|
+
for (const task of bgTasks) {
|
|
777
|
+
if (this.execService) {
|
|
778
|
+
try {
|
|
779
|
+
this.execService.kill(task.id);
|
|
780
|
+
}
|
|
781
|
+
catch { /* ignore if already exited */ }
|
|
782
|
+
}
|
|
783
|
+
this.subagents.stop(task.id);
|
|
784
|
+
await this.store.removeBackgroundTask(task.id);
|
|
785
|
+
}
|
|
666
786
|
// Cancel human-facing waits BEFORE asking Pi to become idle. A session can be blocked inside an
|
|
667
787
|
// extension UI bridge or the question tool; Pi's idle promise cannot settle until that wait is
|
|
668
788
|
// released, so cancelling after `runtime.stop()` turns a click into the full idle backstop.
|
|
@@ -1205,13 +1325,15 @@ export class RunService {
|
|
|
1205
1325
|
// does not exist.
|
|
1206
1326
|
const relative = toWorkspaceRelative(workspace.path, path);
|
|
1207
1327
|
tracker.record(workspace.id, relative, { root: workspace.path, conversationId: conversation.id });
|
|
1208
|
-
refreshWorkspaceIndex(this.fileIndexes, workspace.id, relative);
|
|
1328
|
+
refreshWorkspaceIndex(this.fileIndexes, workspace.id, relative, workspace.path);
|
|
1209
1329
|
// The name index learns about the write as well, so the file the agent just created is
|
|
1210
1330
|
// findable by name straight away rather than after the next background scan. It goes through
|
|
1211
1331
|
// the SAME `ensureNameIndex` the HTTP layer uses, so there is one index per workspace rather
|
|
1212
1332
|
// than one per layer.
|
|
1213
|
-
if (this.nameIndexes)
|
|
1214
|
-
ensureNameIndex(this.nameIndexes, workspace.id, workspace.path)
|
|
1333
|
+
if (this.nameIndexes) {
|
|
1334
|
+
ensureNameIndex(this.nameIndexes, workspace.id, workspace.path);
|
|
1335
|
+
recordWorkspacePath(this.nameIndexes, workspace.id, workspace.path, relative);
|
|
1336
|
+
}
|
|
1215
1337
|
if (toolCallId)
|
|
1216
1338
|
fileWritePaths.set(toolCallId, relative);
|
|
1217
1339
|
this.broadcastFileChanges(workspace.id);
|
|
@@ -1294,9 +1416,11 @@ export class RunService {
|
|
|
1294
1416
|
const writtenPath = call.id ? fileWritePaths.get(call.id) : undefined;
|
|
1295
1417
|
if (writtenPath) {
|
|
1296
1418
|
this.fileChanges?.confirm(workspace.id, writtenPath, workspace.path);
|
|
1297
|
-
refreshWorkspaceIndex(this.fileIndexes, workspace.id, writtenPath);
|
|
1298
|
-
if (this.nameIndexes)
|
|
1299
|
-
ensureNameIndex(this.nameIndexes, workspace.id, workspace.path)
|
|
1419
|
+
refreshWorkspaceIndex(this.fileIndexes, workspace.id, writtenPath, workspace.path);
|
|
1420
|
+
if (this.nameIndexes) {
|
|
1421
|
+
ensureNameIndex(this.nameIndexes, workspace.id, workspace.path);
|
|
1422
|
+
recordWorkspacePath(this.nameIndexes, workspace.id, workspace.path, writtenPath);
|
|
1423
|
+
}
|
|
1300
1424
|
if (call.id)
|
|
1301
1425
|
fileWritePaths.delete(call.id);
|
|
1302
1426
|
this.broadcastFileChanges(workspace.id);
|
|
@@ -1377,7 +1501,7 @@ export class RunService {
|
|
|
1377
1501
|
conversation,
|
|
1378
1502
|
prompt,
|
|
1379
1503
|
signal: controller.signal,
|
|
1380
|
-
cwd: workspace.path,
|
|
1504
|
+
cwd: await resolveSessionCwd(workspace.path, conversation.cwd),
|
|
1381
1505
|
host: {
|
|
1382
1506
|
notify: (request) => { if (acceptsEvent("ui.notify"))
|
|
1383
1507
|
uiHost.notify(request); },
|
|
@@ -1394,6 +1518,10 @@ export class RunService {
|
|
|
1394
1518
|
if (acceptsEvent("transcript.file", true))
|
|
1395
1519
|
await this.store.setConversationPiSessionFile(conversation.id, path);
|
|
1396
1520
|
},
|
|
1521
|
+
onUsage: async (readout) => {
|
|
1522
|
+
if (acceptsEvent("usage", true))
|
|
1523
|
+
await this.store.recordConversationUsage(conversation.id, readout);
|
|
1524
|
+
},
|
|
1397
1525
|
onToolCall,
|
|
1398
1526
|
onFileWrite,
|
|
1399
1527
|
onToolPreparing,
|
|
@@ -1551,6 +1679,7 @@ export class RunService {
|
|
|
1551
1679
|
try {
|
|
1552
1680
|
const agent = await this.runtime.agentFor?.(conversation);
|
|
1553
1681
|
const identity = agent ? { id: agent.id, name: agent.name } : { name: "Pi" };
|
|
1682
|
+
await this.assertModelSupportsImages(conversation, Boolean(initialImages?.length), firstMessageId);
|
|
1554
1683
|
const opened = await this.openTurn(conversation.id, firstMessageId);
|
|
1555
1684
|
await this.streamTurn(conversation, workspace, prompt, opened.assistant, controller, opened.user.id, initialImages, identity);
|
|
1556
1685
|
for (;;) {
|
|
@@ -1563,6 +1692,7 @@ export class RunService {
|
|
|
1563
1692
|
// answers them with a turn of their own, which is the behaviour there was before Pi's queue was
|
|
1564
1693
|
// used at all. Nothing is lost, and nothing is delivered twice.
|
|
1565
1694
|
await this.clearPiQueues(conversation.id);
|
|
1695
|
+
await this.store.releaseSendingMessages(conversation.id);
|
|
1566
1696
|
// Taken AND opened, in that order, by two separate writes. The take is what bounds this loop -
|
|
1567
1697
|
// every pass strictly shrinks the waiting set - and it is what makes a crash honest, because a
|
|
1568
1698
|
// server that dies between the two has a message that is neither waiting nor answered, and the
|
|
@@ -1570,24 +1700,49 @@ export class RunService {
|
|
|
1570
1700
|
const next = await this.store.takeNextWaitingMessage(conversation.id);
|
|
1571
1701
|
if (!next)
|
|
1572
1702
|
break;
|
|
1703
|
+
const current = this.turnContext(conversation.id).conversation;
|
|
1704
|
+
await this.assertModelSupportsImages(current, Boolean(next.images?.length), next.id);
|
|
1573
1705
|
const followUp = await this.store.openTurnOnMessage(conversation.id, next.id, identity);
|
|
1574
1706
|
this.broadcaster.broadcastState();
|
|
1575
|
-
const
|
|
1707
|
+
const activeConversation = this.turnContext(conversation.id).conversation;
|
|
1576
1708
|
// `composePrompt` synthesises the prose for a 后台回传 (the `<task>` envelope is what the transcript
|
|
1577
1709
|
// column reads from; the prose is what leaves the server for Pi). For user messages it is a no-op.
|
|
1578
|
-
await this.streamTurn(
|
|
1710
|
+
await this.streamTurn(activeConversation, workspace, composePrompt(next), followUp.assistant, controller, followUp.user.id, next.images, identity);
|
|
1579
1711
|
}
|
|
1580
1712
|
}
|
|
1581
1713
|
finally {
|
|
1582
1714
|
await this.store.releaseSendingMessages(conversation.id);
|
|
1583
1715
|
}
|
|
1584
1716
|
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Refuses a turn when the selected model cannot accept image input, either because the incoming
|
|
1719
|
+
* message carries images or because previous messages in the conversation contain images.
|
|
1720
|
+
*/
|
|
1721
|
+
async assertModelSupportsImages(conversation, currentTurnHasImages, excludingMessageId) {
|
|
1722
|
+
const hasHistoryImages = conversation.messages.some((m) => m.id !== excludingMessageId && Boolean(m.images && m.images.length > 0));
|
|
1723
|
+
if (!currentTurnHasImages && !hasHistoryImages)
|
|
1724
|
+
return;
|
|
1725
|
+
const catalog = await this.runtime.catalog(conversation.model);
|
|
1726
|
+
const currentModel = catalog.models.find((model) => model.provider === conversation.model?.provider && model.modelId === conversation.model?.modelId)
|
|
1727
|
+
?? catalog.models.find((m) => m.provider === catalog.defaultModel?.provider && m.modelId === catalog.defaultModel?.modelId);
|
|
1728
|
+
if (currentModel && !currentModel.imageInput) {
|
|
1729
|
+
if (currentTurnHasImages) {
|
|
1730
|
+
throw new Error(`当前选择的模型「${currentModel.label || currentModel.modelId}」不支持图片输入`);
|
|
1731
|
+
}
|
|
1732
|
+
throw new Error(`会话历史包含图片,当前模型「${currentModel.label || currentModel.modelId}」不支持图片输入;请切回支持图文的模型继续对话`);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1585
1735
|
/** Keep admission owned through final persistence and recheck the durable queue before releasing. */
|
|
1586
1736
|
async runUntilDrained(conversation, workspace, prompt, controller, messageId, images) {
|
|
1587
1737
|
for (;;) {
|
|
1588
1738
|
if (!controller.signal.aborted)
|
|
1589
1739
|
await this.runConversationTurns(conversation, workspace, prompt, controller, messageId, images);
|
|
1590
|
-
|
|
1740
|
+
// The `runConversationTurns` loop above drains every waiting message before returning, so a
|
|
1741
|
+
// turn that ends with `hasBg` true really is idle - it is waiting on a background task, not
|
|
1742
|
+
// about to start another turn.
|
|
1743
|
+
const hasBg = this.hasActiveBackgroundTasks(conversation.id);
|
|
1744
|
+
const finalStatus = controller.signal.aborted ? "stopped" : hasBg ? "awaiting_background" : "completed";
|
|
1745
|
+
await this.store.setStatus(conversation.id, finalStatus);
|
|
1591
1746
|
if (controller.signal.aborted)
|
|
1592
1747
|
return;
|
|
1593
1748
|
// No await between the last empty observation and releasing the slot.
|
|
@@ -1608,6 +1763,90 @@ export class RunService {
|
|
|
1608
1763
|
/**
|
|
1609
1764
|
* Enqueues or starts execution for a user message.
|
|
1610
1765
|
*/
|
|
1766
|
+
/**
|
|
1767
|
+
* STARTS A TURN FROM A SENDER THAT IS NOT THE READER.
|
|
1768
|
+
*
|
|
1769
|
+
* `enqueueAndRun` below is the person's path: it writes the words under the reader's own name, in
|
|
1770
|
+
* the reader's bubble, so the reader can see that they said it. The orchestrator's path is the
|
|
1771
|
+
* same machinery with a different speaker - a primary agent handing a task to a child session -
|
|
1772
|
+
* and writing THAT under the reader's name was the bug the reader reported: the child's
|
|
1773
|
+
* transcript showed an instruction they never typed, wearing their avatar.
|
|
1774
|
+
*
|
|
1775
|
+
* So the write goes through `addAgentMessage` instead, which records the words as the system's,
|
|
1776
|
+
* and the turn runs on them exactly as before. `composePrompt` is what tells the model who is
|
|
1777
|
+
* speaking, so the model is addressed in the same words.
|
|
1778
|
+
*/
|
|
1779
|
+
async enqueueAndRunAsAgent(conversationId, content) {
|
|
1780
|
+
this.assertHistoryAvailable(conversationId);
|
|
1781
|
+
this.memory?.cancel(conversationId);
|
|
1782
|
+
const trimmed = content.trim();
|
|
1783
|
+
if (!trimmed)
|
|
1784
|
+
return;
|
|
1785
|
+
const { conversation, workspace } = this.turnContext(conversationId);
|
|
1786
|
+
const active = this.activeRuns.get(conversation.id);
|
|
1787
|
+
if (active?.signal.aborted)
|
|
1788
|
+
await this.awaitRunSlotReleased(conversation.id, false);
|
|
1789
|
+
this.assertHistoryAvailable(conversationId);
|
|
1790
|
+
const busy = this.activeRuns.has(conversation.id) || this.interruptTargets.has(conversation.id);
|
|
1791
|
+
const reservedController = busy ? undefined : new AbortController();
|
|
1792
|
+
if (reservedController) {
|
|
1793
|
+
this.cancelledQuestionConversations.delete(conversation.id);
|
|
1794
|
+
this.claimRunSlot(conversation.id, reservedController);
|
|
1795
|
+
}
|
|
1796
|
+
let message;
|
|
1797
|
+
try {
|
|
1798
|
+
message = await this.store.addAgentMessage(conversationId, trimmed, { ...(busy ? { delivery: "waiting" } : {}) });
|
|
1799
|
+
}
|
|
1800
|
+
catch (error) {
|
|
1801
|
+
if (reservedController)
|
|
1802
|
+
this.releaseRunSlot(conversation.id, reservedController);
|
|
1803
|
+
throw error;
|
|
1804
|
+
}
|
|
1805
|
+
this.broadcaster.broadcastState();
|
|
1806
|
+
if (!reservedController) {
|
|
1807
|
+
const waiting = this.store.listWaitingMessages(conversationId).find((item) => item.id === message.id && item.delivery === "waiting");
|
|
1808
|
+
if (!waiting)
|
|
1809
|
+
return;
|
|
1810
|
+
if (!this.activeRuns.has(conversationId) && !this.interruptTargets.has(conversationId))
|
|
1811
|
+
await this.processWaitingMessage(conversationId, message.id);
|
|
1812
|
+
else
|
|
1813
|
+
await this.submitToPiQueue(conversationId, message);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
const controller = reservedController;
|
|
1817
|
+
try {
|
|
1818
|
+
if (controller.signal.aborted) {
|
|
1819
|
+
await this.store.setStatus(conversation.id, "stopped");
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
await this.assertModelSupportsImages(conversation, false, message.id);
|
|
1823
|
+
await this.runUntilDrained(conversation, workspace, composePrompt(message), controller, message.id);
|
|
1824
|
+
}
|
|
1825
|
+
catch (error) {
|
|
1826
|
+
// A TRUNCATED ANSWER IS NOT A DEAD CHILD. The model filled its single-response output budget
|
|
1827
|
+
// and stopped mid-sentence; the work it was doing is not over. The conversation goes back to
|
|
1828
|
+
// `idle` - usable, not failed - and the truncation is recorded as a WARNING, so the orchestrator
|
|
1829
|
+
// above can see that the child needs to be told to carry on rather than reported as broken.
|
|
1830
|
+
if (isLengthExceededError(error)) {
|
|
1831
|
+
await this.store.setStatus(conversation.id, "idle");
|
|
1832
|
+
await this.store.setWarningReason(conversationId, formatTurnFailure(error));
|
|
1833
|
+
console.warn("Super Pi agent-dispatched turn was truncated by the output limit:", describeThrownWithStack(error));
|
|
1834
|
+
throw error;
|
|
1835
|
+
}
|
|
1836
|
+
await this.store.setStatus(conversation.id, controller.signal.aborted ? "stopped" : "failed");
|
|
1837
|
+
if (controller.signal.aborted)
|
|
1838
|
+
return;
|
|
1839
|
+
await this.store.setFailureReason(conversationId, formatTurnFailure(error));
|
|
1840
|
+
console.error("Super Pi agent-dispatched turn failed:", describeThrownWithStack(error));
|
|
1841
|
+
throw error;
|
|
1842
|
+
}
|
|
1843
|
+
finally {
|
|
1844
|
+
this.releaseRunSlot(conversation.id, controller);
|
|
1845
|
+
if (!this.activeRuns.has(conversation.id))
|
|
1846
|
+
this.extensionUiService.cancelPendingUi(conversation.id, "expired");
|
|
1847
|
+
this.broadcaster.broadcastState();
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1611
1850
|
async enqueueAndRun(conversationId, content, images, attachments, clientId) {
|
|
1612
1851
|
this.assertHistoryAvailable(conversationId);
|
|
1613
1852
|
/**
|
|
@@ -1696,14 +1935,8 @@ export class RunService {
|
|
|
1696
1935
|
}
|
|
1697
1936
|
let expanded;
|
|
1698
1937
|
try {
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
const currentModel = catalog.models.find((model) => model.provider === conversation.model?.provider && model.modelId === conversation.model?.modelId)
|
|
1702
|
-
?? catalog.models.find((m) => m.provider === catalog.defaultModel?.provider && m.modelId === catalog.defaultModel?.modelId);
|
|
1703
|
-
if (currentModel && !currentModel.imageInput) {
|
|
1704
|
-
throw new Error(`当前选择的模型「${currentModel.label || currentModel.modelId}」不支持图片输入`);
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1938
|
+
const currentConv = this.turnContext(conversationId).conversation;
|
|
1939
|
+
await this.assertModelSupportsImages(currentConv, Boolean(validImages?.length), message.id);
|
|
1707
1940
|
expanded = await expandAtReferences(this.store, workspace, trimmed);
|
|
1708
1941
|
}
|
|
1709
1942
|
catch (error) {
|
|
@@ -1770,6 +2003,25 @@ export class RunService {
|
|
|
1770
2003
|
if (error?.__superPiPreflight) {
|
|
1771
2004
|
throw error;
|
|
1772
2005
|
}
|
|
2006
|
+
// A TRUNCATED ANSWER IS NOT A FAILED TURN. The model filled its single-response output budget
|
|
2007
|
+
// and stopped mid-sentence: everything it said is on screen, and the conversation is still
|
|
2008
|
+
// usable - the reader's next move is to let it carry on, not to retry from scratch.
|
|
2009
|
+
//
|
|
2010
|
+
// So the conversation goes back to `idle` rather than `failed`, and the fact that the answer
|
|
2011
|
+
// was cut short is recorded as a WARNING. Marking it `failed` is what made an orchestrated
|
|
2012
|
+
// child session die: the orchestrator read the status, told the parent the task had failed,
|
|
2013
|
+
// and a task that was one turn from done was thrown away.
|
|
2014
|
+
//
|
|
2015
|
+
// AND THE TURN DOES NOT THROW. The reader's message WAS accepted and WAS answered - partially.
|
|
2016
|
+
// Throwing here would tell the browser the send failed, and the browser's response to a failed
|
|
2017
|
+
// send is to offer a retry, which is the wrong move: retrying asks for the same answer again
|
|
2018
|
+
// instead of asking for the rest of this one.
|
|
2019
|
+
if (isLengthExceededError(error)) {
|
|
2020
|
+
await this.store.setStatus(conversation.id, "idle");
|
|
2021
|
+
await this.store.setWarningReason(conversation.id, formatTurnFailure(error));
|
|
2022
|
+
console.warn("Super Pi turn was truncated by the output limit:", describeThrownWithStack(error));
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
1773
2025
|
await this.store.setStatus(conversation.id, controller.signal.aborted ? "stopped" : "failed");
|
|
1774
2026
|
if (controller.signal.aborted)
|
|
1775
2027
|
return;
|
|
@@ -1907,6 +2159,7 @@ export class RunService {
|
|
|
1907
2159
|
const message = conversation.messages.find((item) => item.id === messageId);
|
|
1908
2160
|
if (!message)
|
|
1909
2161
|
throw new Error("消息不存在或已被撤回");
|
|
2162
|
+
await this.assertModelSupportsImages(conversation, Boolean(message.images?.length), message.id);
|
|
1910
2163
|
// runConversationTurns opens this first message; do not create a second assistant placeholder.
|
|
1911
2164
|
await this.runUntilDrained(conversation, workspace, composePrompt(message), controller, message.id, message.images);
|
|
1912
2165
|
}
|
|
@@ -2004,7 +2257,7 @@ export class RunService {
|
|
|
2004
2257
|
const task = params.task.trim();
|
|
2005
2258
|
if (!task)
|
|
2006
2259
|
throw new Error("子 agent 的任务不能为空");
|
|
2007
|
-
const { conversation, cwd } = this.dispatchContext(params.conversationId);
|
|
2260
|
+
const { conversation, cwd } = await this.dispatchContext(params.conversationId);
|
|
2008
2261
|
void this.trackDetached(this.subagents.dispatch({
|
|
2009
2262
|
conversationId: conversation.id,
|
|
2010
2263
|
agentId: params.agentId,
|
|
@@ -2039,7 +2292,7 @@ export class RunService {
|
|
|
2039
2292
|
if (body.depth !== undefined)
|
|
2040
2293
|
checkSubagentDepth(body.depth);
|
|
2041
2294
|
const signal = this.activeRuns.get(conversationId)?.signal;
|
|
2042
|
-
const { conversation, cwd } = this.dispatchContext(conversationId);
|
|
2295
|
+
const { conversation, cwd } = await this.dispatchContext(conversationId);
|
|
2043
2296
|
const parent = body.parentRunId ? this.store.snapshot().subagentRuns.find((run) => run.id === body.parentRunId) : undefined;
|
|
2044
2297
|
if (body.parentRunId && (!parent || parent.conversationId !== conversation.id)) {
|
|
2045
2298
|
throw new Error("Invalid parentRunId: dispatch ancestry is not recorded for this conversation");
|
|
@@ -2100,8 +2353,9 @@ export class RunService {
|
|
|
2100
2353
|
return row;
|
|
2101
2354
|
});
|
|
2102
2355
|
if (!signal?.aborted && run.status !== "stopped" && !this.activeRuns.has(conversation.id) && !this.interruptTargets.has(conversation.id)) {
|
|
2103
|
-
void this.processWaitingMessage(conversation.id, queued.id).catch((error) => {
|
|
2356
|
+
void this.processWaitingMessage(conversation.id, queued.id).catch(async (error) => {
|
|
2104
2357
|
console.warn(`Super Pi: auto-trigger for background sub-agent result failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
2358
|
+
await this.settleBackgroundStatus(conversation.id);
|
|
2105
2359
|
});
|
|
2106
2360
|
}
|
|
2107
2361
|
}
|
|
@@ -2109,6 +2363,7 @@ export class RunService {
|
|
|
2109
2363
|
console.warn(`Super Pi: sub-agent result could not be appended as a tool message: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
2110
2364
|
}
|
|
2111
2365
|
await this.store.removeBackgroundTask(run.id);
|
|
2366
|
+
await this.settleBackgroundStatus(conversation.id);
|
|
2112
2367
|
this.broadcaster.broadcastState();
|
|
2113
2368
|
}).catch(async (error) => {
|
|
2114
2369
|
const message = `<task id="${backgroundId}" state="error" kind="subagent">\n<summary>Background task failed: ${body.description?.trim() || subagentType}</summary>\n<task_error>Background task failed before completion: ${error instanceof Error ? error.message : "unknown error"}</task_error>\n</task>`;
|
|
@@ -2126,6 +2381,7 @@ export class RunService {
|
|
|
2126
2381
|
console.warn(`Super Pi: sub-agent failure could not be appended as a tool message: ${innerError instanceof Error ? innerError.message : "unknown error"}`);
|
|
2127
2382
|
}
|
|
2128
2383
|
await this.store.removeBackgroundTask(backgroundId);
|
|
2384
|
+
await this.settleBackgroundStatus(conversationId);
|
|
2129
2385
|
this.broadcaster.broadcastState();
|
|
2130
2386
|
console.warn(`Super Pi: background sub-agent dispatch failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
2131
2387
|
}));
|
|
@@ -2173,7 +2429,7 @@ export class RunService {
|
|
|
2173
2429
|
const capability = this.verifyPrimaryCapability(conversationId, body.capability);
|
|
2174
2430
|
if (!capability.ok)
|
|
2175
2431
|
throw new Error(capability.message);
|
|
2176
|
-
const { conversation, cwd } = this.dispatchContext(conversationId);
|
|
2432
|
+
const { conversation, cwd } = await this.dispatchContext(conversationId);
|
|
2177
2433
|
const signal = this.activeRuns.get(conversationId)?.signal;
|
|
2178
2434
|
// Validated against the roster THIS CALL reads from disk, for the same freshness reason the task tool
|
|
2179
2435
|
// builds its roster per turn: a sub-agent added while the server is running is dispatchable on the next
|
|
@@ -98,7 +98,20 @@ export declare class ScheduledTaskService {
|
|
|
98
98
|
update(id: string, input: ScheduledTaskInput): Promise<ScheduledTask>;
|
|
99
99
|
/** Turns a task on or off. */
|
|
100
100
|
setEnabled(id: string, enabled: boolean): Promise<ScheduledTask>;
|
|
101
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Removes a task. Its conversation is ARCHIVED, not deleted.
|
|
103
|
+
*
|
|
104
|
+
* THE ANSWERS ARE WORTH KEEPING, so the session is not thrown away - and archiving is the keeping
|
|
105
|
+
* that can be undone: the session moves into the sidebar's 已归档 group and can be restored from
|
|
106
|
+
* there. The old behaviour was to leave it in the LIVE list, and that was the defect: a 「定时:xxx」
|
|
107
|
+
* session with no task behind it is an orphan the reader cannot explain (there is no delete-session
|
|
108
|
+
* control at all) and cannot get rid of.
|
|
109
|
+
*
|
|
110
|
+
* THE DELETE ITSELF IS NEVER REFUSED. The task is removed first and the archive is best-effort: a
|
|
111
|
+
* conversation that is still running cannot be archived, and a reader who deletes a task must not be
|
|
112
|
+
* told "no" because a session happens to be busy. The session simply stays where it is, and the
|
|
113
|
+
* reason is in the server log rather than in the reader's face.
|
|
114
|
+
*/
|
|
102
115
|
remove(id: string): Promise<void>;
|
|
103
116
|
/**
|
|
104
117
|
* Runs a task NOW, once.
|
|
@@ -186,10 +186,35 @@ export class ScheduledTaskService {
|
|
|
186
186
|
return task;
|
|
187
187
|
return this.update(id, { enabled });
|
|
188
188
|
}
|
|
189
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* Removes a task. Its conversation is ARCHIVED, not deleted.
|
|
191
|
+
*
|
|
192
|
+
* THE ANSWERS ARE WORTH KEEPING, so the session is not thrown away - and archiving is the keeping
|
|
193
|
+
* that can be undone: the session moves into the sidebar's 已归档 group and can be restored from
|
|
194
|
+
* there. The old behaviour was to leave it in the LIVE list, and that was the defect: a 「定时:xxx」
|
|
195
|
+
* session with no task behind it is an orphan the reader cannot explain (there is no delete-session
|
|
196
|
+
* control at all) and cannot get rid of.
|
|
197
|
+
*
|
|
198
|
+
* THE DELETE ITSELF IS NEVER REFUSED. The task is removed first and the archive is best-effort: a
|
|
199
|
+
* conversation that is still running cannot be archived, and a reader who deletes a task must not be
|
|
200
|
+
* told "no" because a session happens to be busy. The session simply stays where it is, and the
|
|
201
|
+
* reason is in the server log rather than in the reader's face.
|
|
202
|
+
*/
|
|
190
203
|
async remove(id) {
|
|
191
|
-
this.require(id);
|
|
204
|
+
const task = this.require(id);
|
|
192
205
|
await this.store.removeScheduledTask(id);
|
|
206
|
+
const conversationId = task.conversationId;
|
|
207
|
+
if (conversationId) {
|
|
208
|
+
const conversation = this.store.conversation(conversationId);
|
|
209
|
+
if (conversation && !conversation.archivedAt) {
|
|
210
|
+
try {
|
|
211
|
+
await this.conversationService.archiveConversation(conversationId);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
console.warn(`Super Pi: the deleted task's conversation ${conversationId} could not be archived:`, error instanceof Error ? error.message : String(error));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
193
218
|
this.broadcaster.broadcastState();
|
|
194
219
|
}
|
|
195
220
|
/**
|