@evandu/super-pi 0.1.144 → 0.1.146

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.
Files changed (75) hide show
  1. package/agents/super-pi-task/core.ts +15 -2
  2. package/agents/super-pi-task/index.ts +3 -0
  3. package/dist/.super-pi-check-inputs.json +2 -2
  4. package/dist/client/.super-pi-client-sources.json +9 -9
  5. package/dist/client/assets/{CodeMirrorEditor-DfQHX1Hy.js → CodeMirrorEditor-CEwBCVM6.js} +2 -2
  6. package/dist/client/assets/{dist-Cu3B0JQX.js → dist-76ArZ5M4.js} +1 -1
  7. package/dist/client/assets/{dist-BEbEv2KM.js → dist-9F1wL-T5.js} +1 -1
  8. package/dist/client/assets/{dist-BRjyQs_L.js → dist-B0RXZeXQ.js} +1 -1
  9. package/dist/client/assets/{dist-SzZEAk24.js → dist-BCNaDgNW.js} +1 -1
  10. package/dist/client/assets/{dist-qAUj_2Ue.js → dist-BXBSIuTD.js} +1 -1
  11. package/dist/client/assets/{dist-BGBinEie.js → dist-Bh8Q7Y_n.js} +1 -1
  12. package/dist/client/assets/{dist-DKkMk3XX.js → dist-BoyICDlL.js} +1 -1
  13. package/dist/client/assets/{dist-ISoxXlmW.js → dist-BuR5Iagd.js} +1 -1
  14. package/dist/client/assets/{dist-LRNzDUVb.js → dist-Bx6ZWYUW.js} +1 -1
  15. package/dist/client/assets/{dist-DOvzruAQ.js → dist-CAOcjKAQ.js} +1 -1
  16. package/dist/client/assets/{dist-CDtFD-PW.js → dist-CHt8PX8R.js} +1 -1
  17. package/dist/client/assets/{dist-BuPimjY-.js → dist-CSCUT1PQ.js} +1 -1
  18. package/dist/client/assets/{dist-BsMikSTM.js → dist-CUB_Chdh.js} +1 -1
  19. package/dist/client/assets/{dist-KQJCVwxy.js → dist-CbVf2Cjn.js} +1 -1
  20. package/dist/client/assets/{dist-Dnru8_uX.js → dist-DBUqwiWG.js} +1 -1
  21. package/dist/client/assets/{dist-B8oH6dsc.js → dist-DC1IocrS.js} +1 -1
  22. package/dist/client/assets/{dist-D7aAMRTb.js → dist-DFMtP3wG.js} +1 -1
  23. package/dist/client/assets/{dist-Cw_KCDH9.js → dist-DN0GlnnZ.js} +1 -1
  24. package/dist/client/assets/{dist-BCLhQXYY.js → dist-DftD4ibj.js} +1 -1
  25. package/dist/client/assets/{dist-CgQ7zaWY.js → dist-Do52MRwm.js} +1 -1
  26. package/dist/client/assets/{dist-BS-FBUKW.js → dist-JvQIIdqB.js} +1 -1
  27. package/dist/client/assets/{dist-C5BQsmB7.js → dist-oLq6HXLG.js} +1 -1
  28. package/dist/client/assets/{index-Xaui0uUI.css → index-D-OxlUxN.css} +1 -1
  29. package/dist/client/assets/index-E7yaLa2C.js +102 -0
  30. package/dist/client/index.html +2 -2
  31. package/dist/server/agents.d.ts +1 -1
  32. package/dist/server/agents.js +3 -1
  33. package/dist/server/application/conversation-service.d.ts +1 -1
  34. package/dist/server/application/conversation-service.js +6 -4
  35. package/dist/server/application/run-service.d.ts +16 -2
  36. package/dist/server/application/run-service.js +121 -14
  37. package/dist/server/application/scheduled-task-service.d.ts +14 -1
  38. package/dist/server/application/scheduled-task-service.js +27 -2
  39. package/dist/server/application/session-orchestrator-service.d.ts +11 -2
  40. package/dist/server/application/session-orchestrator-service.js +103 -27
  41. package/dist/server/command-handler.js +1 -1
  42. package/dist/server/conversation-usage.d.ts +19 -0
  43. package/dist/server/conversation-usage.js +40 -0
  44. package/dist/server/file-diff.d.ts +3 -0
  45. package/dist/server/file-diff.js +4 -4
  46. package/dist/server/file-index.d.ts +4 -2
  47. package/dist/server/file-index.js +14 -9
  48. package/dist/server/file-name-index.d.ts +5 -2
  49. package/dist/server/file-name-index.js +22 -5
  50. package/dist/server/git.d.ts +3 -3
  51. package/dist/server/git.js +10 -9
  52. package/dist/server/http-server.js +195 -33
  53. package/dist/server/model-context-window.d.ts +46 -0
  54. package/dist/server/model-context-window.js +121 -0
  55. package/dist/server/pi-session.d.ts +6 -0
  56. package/dist/server/pi-session.js +15 -1
  57. package/dist/server/runtime.d.ts +3 -0
  58. package/dist/server/runtime.js +21 -6
  59. package/dist/server/session-cwd.d.ts +17 -0
  60. package/dist/server/session-cwd.js +43 -0
  61. package/dist/server/session-ledger.d.ts +7 -0
  62. package/dist/server/store.d.ts +22 -0
  63. package/dist/server/store.js +52 -0
  64. package/dist/server/task-tool.d.ts +2 -1
  65. package/dist/server/task-tool.js +3 -1
  66. package/dist/server/terminal.d.ts +1 -0
  67. package/dist/server/terminal.js +5 -3
  68. package/dist/server/turn-failure.js +16 -1
  69. package/dist/server/types.d.ts +18 -0
  70. package/dist/server/version.d.ts +25 -2
  71. package/dist/server/version.js +98 -26
  72. package/dist/server/ws-server.d.ts +1 -1
  73. package/dist/server/ws-server.js +7 -5
  74. package/package.json +20 -3
  75. package/dist/client/assets/index-BLWr70ej.js +0 -100
@@ -8,8 +8,8 @@
8
8
  <link rel="alternate icon" href="/favicon.ico" />
9
9
  <link rel="apple-touch-icon" href="/favicon.svg" />
10
10
  <title>Super Pi</title>
11
- <script type="module" crossorigin src="/assets/index-BLWr70ej.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-Xaui0uUI.css">
11
+ <script type="module" crossorigin src="/assets/index-E7yaLa2C.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-D-OxlUxN.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
@@ -38,7 +38,7 @@ type Frontmatter = Record<string, unknown>;
38
38
  */
39
39
  export declare const PI_TOOL_NAMES: readonly ["read", "bash", "powershell", "edit", "write", "grep", "find", "ls", "codemode", "tool_search", "list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"];
40
40
  /** Tools shipped by this package. Authors must be able to explicitly deny host extras. */
41
- export declare const SUPER_PI_TOOL_NAMES: readonly ["task", "todowrite", "workflow", "run_async", "scheduled_task", "question", "tool_register", "memory", "skill_manage", "session_search", "knowledge_search", "knowledge_import", "document_read", "document_write", "web_search", "web_fetch", "feishu_doc_read", "feishu_doc_write"];
41
+ export declare const SUPER_PI_TOOL_NAMES: readonly ["task", "todowrite", "workflow", "run_async", "scheduled_task", "question", "tool_register", "memory", "skill_manage", "session_search", "session_spawn", "session_status", "session_dispatch", "session_stop", "knowledge_search", "knowledge_import", "document_read", "document_write", "web_search", "web_fetch", "feishu_doc_read", "feishu_doc_write"];
42
42
  /**
43
43
  * Whether a name can be a real Pi tool. A name is real if it is one of `PI_TOOL_NAMES`, an MCP
44
44
  * server tool, or a PATTERN.
@@ -39,7 +39,9 @@ export const PI_TOOL_NAMES = [
39
39
  /** Tools shipped by this package. Authors must be able to explicitly deny host extras. */
40
40
  export const SUPER_PI_TOOL_NAMES = [
41
41
  "task", "todowrite", "workflow", "run_async", "scheduled_task", "question", "tool_register",
42
- "memory", "skill_manage", "session_search", "knowledge_search", "knowledge_import",
42
+ "memory", "skill_manage", "session_search",
43
+ "session_spawn", "session_status", "session_dispatch", "session_stop",
44
+ "knowledge_search", "knowledge_import",
43
45
  "document_read", "document_write", "web_search", "web_fetch", "feishu_doc_read", "feishu_doc_write",
44
46
  ];
45
47
  const KNOWN_TOOL_NAMES = [...PI_TOOL_NAMES, ...SUPER_PI_TOOL_NAMES];
@@ -48,7 +48,7 @@ export declare class ConversationService {
48
48
  createConversation(workspaceId: string, parentage?: {
49
49
  parentConversationId?: string;
50
50
  origin?: "user" | "orchestrator";
51
- }): Promise<string>;
51
+ }, cwd?: string): Promise<string>;
52
52
  /**
53
53
  * Name a conversation from what was said in it. Returns the title; the CALLER applies it, because the
54
54
  * rename is a second command with its own optimistic path and its own failure toast, and a service that
@@ -1,5 +1,6 @@
1
1
  import { assertModelAllowed } from "../model-filter.js";
2
2
  import { newConversationId } from "../store.js";
3
+ import { normalizeSessionCwd, resolveSessionCwd } from "../session-cwd.js";
3
4
  function isMissingPrimaryAgent(error, agentId) {
4
5
  const message = error instanceof Error ? error.message : String(error);
5
6
  const id = JSON.stringify(agentId);
@@ -50,7 +51,7 @@ export class ConversationService {
50
51
  * button and every other caller create a top-level conversation by omission, which is the same
51
52
  * "absence means top level" rule the pre-tree state.json files read under.
52
53
  */
53
- async createConversation(workspaceId, parentage) {
54
+ async createConversation(workspaceId, parentage, cwd) {
54
55
  const workspace = this.store.workspace(workspaceId);
55
56
  if (!workspace)
56
57
  throw new Error("找不到这个工作区。");
@@ -126,7 +127,8 @@ export class ConversationService {
126
127
  }
127
128
  // `null` is intentional here: Store uses it to remove a workspace value that it
128
129
  // would otherwise have copied before applying the resolved configuration.
129
- const defaults = { ...resolved, agentId: resolved.agentId ?? null, model: resolved.model ?? null, thinkingLevel: resolved.thinkingLevel ?? null, ...(modelNotice ? { modelNotice } : {}), ...(agentNotice ? { agentNotice } : {}), ...(parentage ? { parentConversationId: parentage.parentConversationId, origin: parentage.origin } : {}) };
130
+ const sessionCwd = await normalizeSessionCwd(workspace.path, cwd);
131
+ const defaults = { ...resolved, agentId: resolved.agentId ?? null, model: resolved.model ?? null, thinkingLevel: resolved.thinkingLevel ?? null, ...(sessionCwd ? { cwd: sessionCwd } : {}), ...(modelNotice ? { modelNotice } : {}), ...(agentNotice ? { agentNotice } : {}), ...(parentage ? { parentConversationId: parentage.parentConversationId, origin: parentage.origin } : {}) };
130
132
  const created = await this.store.createConversation(workspaceId, defaults);
131
133
  this.broadcaster.broadcastState();
132
134
  return created.id;
@@ -422,7 +424,7 @@ export class ConversationService {
422
424
  }
423
425
  const outcome = await this.runtime.revert({
424
426
  conversation,
425
- cwd: workspace.path,
427
+ cwd: await resolveSessionCwd(workspace.path, conversation.cwd),
426
428
  boundaryEntryId: plan.boundaryEntryId,
427
429
  beforeBoundary: withdraw && Boolean(conversation.messages.find(item => item.id === messageId)?.piEntryId),
428
430
  });
@@ -480,7 +482,7 @@ export class ConversationService {
480
482
  }
481
483
  try {
482
484
  const forkedId = newConversationId();
483
- const outcome = await this.runtime.fork({ conversation, cwd: workspace.path, forkedConversationId: forkedId });
485
+ const outcome = await this.runtime.fork({ conversation, cwd: await resolveSessionCwd(workspace.path, conversation.cwd), forkedConversationId: forkedId });
484
486
  const forked = await this.store.forkConversation(conversationId, forkedId);
485
487
  console.log(`Super Pi: forked conversation ${conversation.id} into ${forked.id}: ` +
486
488
  `${forked.messages.length} message(s) copied in state.json` +
@@ -236,10 +236,10 @@ export declare class RunService {
236
236
  /**
237
237
  * Resolves conversation and workspace working directory for subagent dispatch.
238
238
  */
239
- dispatchContext(conversationId: string): {
239
+ dispatchContext(conversationId: string): Promise<{
240
240
  conversation: Conversation;
241
241
  cwd: string;
242
- };
242
+ }>;
243
243
  /**
244
244
  * Checks whether a conversation has an active turn running.
245
245
  */
@@ -384,6 +384,20 @@ export declare class RunService {
384
384
  /**
385
385
  * Enqueues or starts execution for a user message.
386
386
  */
387
+ /**
388
+ * STARTS A TURN FROM A SENDER THAT IS NOT THE READER.
389
+ *
390
+ * `enqueueAndRun` below is the person's path: it writes the words under the reader's own name, in
391
+ * the reader's bubble, so the reader can see that they said it. The orchestrator's path is the
392
+ * same machinery with a different speaker - a primary agent handing a task to a child session -
393
+ * and writing THAT under the reader's name was the bug the reader reported: the child's
394
+ * transcript showed an instruction they never typed, wearing their avatar.
395
+ *
396
+ * So the write goes through `addAgentMessage` instead, which records the words as the system's,
397
+ * and the turn runs on them exactly as before. `composePrompt` is what tells the model who is
398
+ * speaking, so the model is addressed in the same words.
399
+ */
400
+ enqueueAndRunAsAgent(conversationId: string, content: string): Promise<void>;
387
401
  enqueueAndRun(conversationId: string, content: string, images?: MessageImage[], attachments?: import("../types.js").MessageAttachments, clientId?: string): Promise<void>;
388
402
  /**
389
403
  * Steers a queued message into the in-flight turn.
@@ -4,11 +4,12 @@ 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";
7
+ import { ensureNameIndex, recordWorkspacePath } from "../file-name-index.js";
8
8
  import { formatTurnFailure } from "../turn-failure.js";
9
9
  import { describeThrownWithStack } from "../failure-text.js";
10
10
  import { toWorkspaceRelative } from "../files.js";
11
11
  import { refreshWorkspaceIndex } from "../file-index.js";
12
+ import { resolveSessionCwd } from "../session-cwd.js";
12
13
  import { execFile } from "node:child_process";
13
14
  import { promisify } from "node:util";
14
15
  import { randomUUID } from "node:crypto";
@@ -56,10 +57,32 @@ async function expandAtReferences(store, workspace, content) {
56
57
  * The envelope and the prose are NOT redundant: the envelope is what the reader sees in the transcript
57
58
  * column, what they cancel or reorder; the prose is what leaves the server for Pi.
58
59
  */
60
+ /**
61
+ * THE TEXT THE MODEL IS HANDED WHEN A 后台回传 BECOMES ITS TURN.
62
+ *
63
+ * Pi's SDK has ONE entry point for a new turn - `session.prompt(text)` - and every text sent
64
+ * through it is a USER MESSAGE in Pi's transcript. There is no "the agent said this" channel to
65
+ * use. So a delivered result has to travel as prose, and the only question left is what that prose
66
+ * SAYS.
67
+ *
68
+ * IT USED TO SAY `[后台任务「...」已完成]`, which reads as a thing the PERSON said - a bracketed
69
+ * label, then the body, with nothing saying who is speaking. The model therefore answered it as a
70
+ * user would be answered ("已收到:[后台任务「...」]"), and the reader saw their own result coming
71
+ * back in the user's voice, inside the user's own turn. The delivery had become a quotation.
72
+ *
73
+ * So the prose now says WHO IS SPEAKING and that this is information, not an instruction:
74
+ * 「子会话「...」已完成,结果如下」. The model is told what happened and what it received, and it
75
+ * answers about THAT - which is what a primary agent should do with a child's result.
76
+ */
59
77
  function composePrompt(message) {
60
78
  if (message.role !== "tool")
61
79
  return message.content;
62
80
  const content = message.content;
81
+ // A DISPATCHED ORDER IS NOT A DELIVERED RESULT. The two both travel as a `tool` message, and the
82
+ // envelope tells them apart: a result carries `<task ...>`, an order does not.
83
+ if (!/<task id="[^"]+" state="[^"]+"/.test(content)) {
84
+ return `上级智能体下发的任务:\n\n${content}`;
85
+ }
63
86
  const summaryMatch = /<summary>([\s\S]*?)<\/summary>/.exec(content);
64
87
  const summary = summaryMatch ? summaryMatch[1].trim() : "后台任务";
65
88
  const stateMatch = /<task id="[^"]+" state="([^"]+)"/.exec(content);
@@ -74,7 +97,10 @@ function composePrompt(message) {
74
97
  const bodyMatch = /<task_(?:result|error)>\n?([\s\S]*)\n?<\/(?:task_result|task_error)>/.exec(content);
75
98
  const body = bodyMatch ? bodyMatch[1] : content;
76
99
  const stateLabel = state === "completed" ? "已完成" : state === "error" ? "失败" : state;
77
- return `[后台任务「${summary}」${stateLabel}]\n\n${body}`;
100
+ // THE SPEAKER IS NAMED, AND IT IS NOT THE READER. A line that starts 「[」 looks like a note the
101
+ // person pasted in; a line that starts 「子会话」 or 「后台任务」 says the SYSTEM is reporting.
102
+ const speaker = /kind="session"/.test(content) ? "子会话" : "后台任务";
103
+ return `${speaker}「${summary}」${stateLabel},结果如下:\n\n${body}`;
78
104
  }
79
105
  /**
80
106
  * Service managing execution of conversation turns, message queueing,
@@ -563,9 +589,9 @@ export class RunService {
563
589
  /**
564
590
  * Resolves conversation and workspace working directory for subagent dispatch.
565
591
  */
566
- dispatchContext(conversationId) {
592
+ async dispatchContext(conversationId) {
567
593
  const { conversation, workspace } = this.turnContext(conversationId);
568
- return { conversation, cwd: workspace.path };
594
+ return { conversation, cwd: await resolveSessionCwd(workspace.path, conversation.cwd) };
569
595
  }
570
596
  /**
571
597
  * Checks whether a conversation has an active turn running.
@@ -1205,13 +1231,15 @@ export class RunService {
1205
1231
  // does not exist.
1206
1232
  const relative = toWorkspaceRelative(workspace.path, path);
1207
1233
  tracker.record(workspace.id, relative, { root: workspace.path, conversationId: conversation.id });
1208
- refreshWorkspaceIndex(this.fileIndexes, workspace.id, relative);
1234
+ refreshWorkspaceIndex(this.fileIndexes, workspace.id, relative, workspace.path);
1209
1235
  // The name index learns about the write as well, so the file the agent just created is
1210
1236
  // findable by name straight away rather than after the next background scan. It goes through
1211
1237
  // the SAME `ensureNameIndex` the HTTP layer uses, so there is one index per workspace rather
1212
1238
  // than one per layer.
1213
- if (this.nameIndexes)
1214
- ensureNameIndex(this.nameIndexes, workspace.id, workspace.path).upsert(relative);
1239
+ if (this.nameIndexes) {
1240
+ ensureNameIndex(this.nameIndexes, workspace.id, workspace.path);
1241
+ recordWorkspacePath(this.nameIndexes, workspace.id, workspace.path, relative);
1242
+ }
1215
1243
  if (toolCallId)
1216
1244
  fileWritePaths.set(toolCallId, relative);
1217
1245
  this.broadcastFileChanges(workspace.id);
@@ -1294,9 +1322,11 @@ export class RunService {
1294
1322
  const writtenPath = call.id ? fileWritePaths.get(call.id) : undefined;
1295
1323
  if (writtenPath) {
1296
1324
  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).upsert(writtenPath);
1325
+ refreshWorkspaceIndex(this.fileIndexes, workspace.id, writtenPath, workspace.path);
1326
+ if (this.nameIndexes) {
1327
+ ensureNameIndex(this.nameIndexes, workspace.id, workspace.path);
1328
+ recordWorkspacePath(this.nameIndexes, workspace.id, workspace.path, writtenPath);
1329
+ }
1300
1330
  if (call.id)
1301
1331
  fileWritePaths.delete(call.id);
1302
1332
  this.broadcastFileChanges(workspace.id);
@@ -1377,7 +1407,7 @@ export class RunService {
1377
1407
  conversation,
1378
1408
  prompt,
1379
1409
  signal: controller.signal,
1380
- cwd: workspace.path,
1410
+ cwd: await resolveSessionCwd(workspace.path, conversation.cwd),
1381
1411
  host: {
1382
1412
  notify: (request) => { if (acceptsEvent("ui.notify"))
1383
1413
  uiHost.notify(request); },
@@ -1394,6 +1424,10 @@ export class RunService {
1394
1424
  if (acceptsEvent("transcript.file", true))
1395
1425
  await this.store.setConversationPiSessionFile(conversation.id, path);
1396
1426
  },
1427
+ onUsage: async (readout) => {
1428
+ if (acceptsEvent("usage", true))
1429
+ await this.store.recordConversationUsage(conversation.id, readout);
1430
+ },
1397
1431
  onToolCall,
1398
1432
  onFileWrite,
1399
1433
  onToolPreparing,
@@ -1608,6 +1642,79 @@ export class RunService {
1608
1642
  /**
1609
1643
  * Enqueues or starts execution for a user message.
1610
1644
  */
1645
+ /**
1646
+ * STARTS A TURN FROM A SENDER THAT IS NOT THE READER.
1647
+ *
1648
+ * `enqueueAndRun` below is the person's path: it writes the words under the reader's own name, in
1649
+ * the reader's bubble, so the reader can see that they said it. The orchestrator's path is the
1650
+ * same machinery with a different speaker - a primary agent handing a task to a child session -
1651
+ * and writing THAT under the reader's name was the bug the reader reported: the child's
1652
+ * transcript showed an instruction they never typed, wearing their avatar.
1653
+ *
1654
+ * So the write goes through `addAgentMessage` instead, which records the words as the system's,
1655
+ * and the turn runs on them exactly as before. `composePrompt` is what tells the model who is
1656
+ * speaking, so the model is addressed in the same words.
1657
+ */
1658
+ async enqueueAndRunAsAgent(conversationId, content) {
1659
+ this.assertHistoryAvailable(conversationId);
1660
+ this.memory?.cancel(conversationId);
1661
+ const trimmed = content.trim();
1662
+ if (!trimmed)
1663
+ return;
1664
+ const { conversation, workspace } = this.turnContext(conversationId);
1665
+ const active = this.activeRuns.get(conversation.id);
1666
+ if (active?.signal.aborted)
1667
+ await this.awaitRunSlotReleased(conversation.id, false);
1668
+ this.assertHistoryAvailable(conversationId);
1669
+ const busy = this.activeRuns.has(conversation.id) || this.interruptTargets.has(conversation.id);
1670
+ const reservedController = busy ? undefined : new AbortController();
1671
+ if (reservedController) {
1672
+ this.cancelledQuestionConversations.delete(conversation.id);
1673
+ this.claimRunSlot(conversation.id, reservedController);
1674
+ }
1675
+ let message;
1676
+ try {
1677
+ message = await this.store.addAgentMessage(conversationId, trimmed, { ...(busy ? { delivery: "waiting" } : {}) });
1678
+ }
1679
+ catch (error) {
1680
+ if (reservedController)
1681
+ this.releaseRunSlot(conversation.id, reservedController);
1682
+ throw error;
1683
+ }
1684
+ this.broadcaster.broadcastState();
1685
+ if (!reservedController) {
1686
+ const waiting = this.store.listWaitingMessages(conversationId).find((item) => item.id === message.id && item.delivery === "waiting");
1687
+ if (!waiting)
1688
+ return;
1689
+ if (!this.activeRuns.has(conversationId) && !this.interruptTargets.has(conversationId))
1690
+ await this.processWaitingMessage(conversationId, message.id);
1691
+ else
1692
+ await this.submitToPiQueue(conversationId, message);
1693
+ return;
1694
+ }
1695
+ const controller = reservedController;
1696
+ try {
1697
+ if (controller.signal.aborted) {
1698
+ await this.store.setStatus(conversation.id, "stopped");
1699
+ return;
1700
+ }
1701
+ await this.runUntilDrained(conversation, workspace, composePrompt(message), controller, message.id);
1702
+ }
1703
+ catch (error) {
1704
+ await this.store.setStatus(conversation.id, controller.signal.aborted ? "stopped" : "failed");
1705
+ if (controller.signal.aborted)
1706
+ return;
1707
+ await this.store.setFailureReason(conversationId, formatTurnFailure(error));
1708
+ console.error("Super Pi agent-dispatched turn failed:", describeThrownWithStack(error));
1709
+ throw error;
1710
+ }
1711
+ finally {
1712
+ this.releaseRunSlot(conversation.id, controller);
1713
+ if (!this.activeRuns.has(conversation.id))
1714
+ this.extensionUiService.cancelPendingUi(conversation.id, "expired");
1715
+ this.broadcaster.broadcastState();
1716
+ }
1717
+ }
1611
1718
  async enqueueAndRun(conversationId, content, images, attachments, clientId) {
1612
1719
  this.assertHistoryAvailable(conversationId);
1613
1720
  /**
@@ -2004,7 +2111,7 @@ export class RunService {
2004
2111
  const task = params.task.trim();
2005
2112
  if (!task)
2006
2113
  throw new Error("子 agent 的任务不能为空");
2007
- const { conversation, cwd } = this.dispatchContext(params.conversationId);
2114
+ const { conversation, cwd } = await this.dispatchContext(params.conversationId);
2008
2115
  void this.trackDetached(this.subagents.dispatch({
2009
2116
  conversationId: conversation.id,
2010
2117
  agentId: params.agentId,
@@ -2039,7 +2146,7 @@ export class RunService {
2039
2146
  if (body.depth !== undefined)
2040
2147
  checkSubagentDepth(body.depth);
2041
2148
  const signal = this.activeRuns.get(conversationId)?.signal;
2042
- const { conversation, cwd } = this.dispatchContext(conversationId);
2149
+ const { conversation, cwd } = await this.dispatchContext(conversationId);
2043
2150
  const parent = body.parentRunId ? this.store.snapshot().subagentRuns.find((run) => run.id === body.parentRunId) : undefined;
2044
2151
  if (body.parentRunId && (!parent || parent.conversationId !== conversation.id)) {
2045
2152
  throw new Error("Invalid parentRunId: dispatch ancestry is not recorded for this conversation");
@@ -2173,7 +2280,7 @@ export class RunService {
2173
2280
  const capability = this.verifyPrimaryCapability(conversationId, body.capability);
2174
2281
  if (!capability.ok)
2175
2282
  throw new Error(capability.message);
2176
- const { conversation, cwd } = this.dispatchContext(conversationId);
2283
+ const { conversation, cwd } = await this.dispatchContext(conversationId);
2177
2284
  const signal = this.activeRuns.get(conversationId)?.signal;
2178
2285
  // Validated against the roster THIS CALL reads from disk, for the same freshness reason the task tool
2179
2286
  // 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
- /** Removes a task. Its conversation stays: it holds the answers the task produced. */
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
- /** Removes a task. Its conversation stays: it holds the answers the task produced. */
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
  /**
@@ -11,8 +11,13 @@ export interface SpawnSessionOptions {
11
11
  task: string;
12
12
  /** Parent conversation initiating the orchestration, if any. */
13
13
  parentConversationId?: string;
14
- /** Workspace ID for the new session. Inherited from parent if omitted. */
15
- workspaceId?: string;
14
+ /** Explicit target workspace. A parent conversation records only the session-tree relationship. */
15
+ workspaceId: string;
16
+ /**
17
+ * Optional workspace-root-relative subdirectory for the new session. Invalid or out-of-bounds
18
+ * values are rejected before the conversation is created.
19
+ */
20
+ cwd?: string;
16
21
  /** Optional custom title for the new session. */
17
22
  title?: string;
18
23
  /** Optional agent override for the new session. */
@@ -98,6 +103,10 @@ export declare class SessionOrchestratorService {
98
103
  }): Promise<SessionSummary>;
99
104
  /**
100
105
  * Lists summaries of all sessions, optionally filtered by parent conversation.
106
+ *
107
+ * Persisted store conversations are the authoritative source for session tree
108
+ * parent-child relationships across server restarts, while SessionLedger
109
+ * enriches live in-memory task, progress, and activeTool state.
101
110
  */
102
111
  list(parentConversationId?: string): Promise<SessionSummary[]>;
103
112
  /**