@evandu/super-pi 0.1.139 → 0.1.140

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 (48) hide show
  1. package/agents/super-pi-task/core.ts +418 -1
  2. package/agents/super-pi-task/index.ts +115 -0
  3. package/dist/.super-pi-check-inputs.json +2 -2
  4. package/dist/client/.super-pi-client-sources.json +6 -5
  5. package/dist/client/assets/{CodeMirrorEditor-H7SHGhdS.js → CodeMirrorEditor-DvZeOLny.js} +2 -2
  6. package/dist/client/assets/{dist-CukPKrEn.js → dist-3BuyVG3H.js} +1 -1
  7. package/dist/client/assets/{dist-cP2vnTP_.js → dist-BCUfH4nP.js} +1 -1
  8. package/dist/client/assets/{dist-C-MUXK_t.js → dist-BIIMtU9W.js} +1 -1
  9. package/dist/client/assets/{dist-BfUeRJKr.js → dist-C-QmRwbd.js} +1 -1
  10. package/dist/client/assets/{dist-CZ9Krsno.js → dist-CTkzLBdb.js} +1 -1
  11. package/dist/client/assets/{dist-BxTGjU84.js → dist-C_VuSpx8.js} +1 -1
  12. package/dist/client/assets/{dist-NlopeJ5R.js → dist-CaUsvSz1.js} +1 -1
  13. package/dist/client/assets/{dist-zUJqdbfo.js → dist-Cb1gxS57.js} +1 -1
  14. package/dist/client/assets/{dist-C8NllalC.js → dist-CqDSeoUk.js} +1 -1
  15. package/dist/client/assets/{dist-DLKVFZhO.js → dist-D7yfnKNC.js} +1 -1
  16. package/dist/client/assets/{dist-CUNctZ5v.js → dist-DA8yW4lK.js} +1 -1
  17. package/dist/client/assets/{dist-Jw-GQ-re.js → dist-DAcS4Kj8.js} +1 -1
  18. package/dist/client/assets/{dist-C_ftUQZN.js → dist-DN0M4DAX.js} +1 -1
  19. package/dist/client/assets/{dist-2XXLO-IZ.js → dist-DRgWZqFH.js} +1 -1
  20. package/dist/client/assets/{dist-POoQ4j29.js → dist-DdfW8Gjv.js} +1 -1
  21. package/dist/client/assets/{dist-BEM6tttY.js → dist-DlQ8PMvv.js} +1 -1
  22. package/dist/client/assets/{dist-CSdzvQnf.js → dist-Dvca0LeR.js} +1 -1
  23. package/dist/client/assets/{dist-DJ8SwQl_.js → dist-HJdP1yxN.js} +1 -1
  24. package/dist/client/assets/{dist-CuMnZOGE.js → dist-gBAr9yXF.js} +1 -1
  25. package/dist/client/assets/{dist-BokzcFLE.js → dist-gyzurUDR.js} +1 -1
  26. package/dist/client/assets/{dist-BoucVzKA.js → dist-qG53hT1b.js} +1 -1
  27. package/dist/client/assets/{dist-D3Ta0y4e.js → dist-r4Kq1ABA.js} +1 -1
  28. package/dist/client/assets/{index-B32VZVYx.js → index-CNbh1fL2.js} +9 -9
  29. package/dist/client/assets/index-DCSFD4VV.css +1 -0
  30. package/dist/client/index.html +2 -2
  31. package/dist/server/application/session-orchestrator-service.d.ts +113 -0
  32. package/dist/server/application/session-orchestrator-service.js +234 -0
  33. package/dist/server/http-server.js +135 -0
  34. package/dist/server/index.js +20 -2
  35. package/dist/server/runtime.d.ts +1 -0
  36. package/dist/server/runtime.js +6 -2
  37. package/dist/server/server-context.d.ts +2 -0
  38. package/dist/server/session-ledger.d.ts +60 -0
  39. package/dist/server/session-ledger.js +76 -0
  40. package/dist/server/subagent-runs.d.ts +31 -1
  41. package/dist/server/subagent-runs.js +30 -13
  42. package/dist/server/task-tool.d.ts +18 -0
  43. package/dist/server/task-tool.js +85 -0
  44. package/dist/server/types.d.ts +23 -0
  45. package/dist/server/ws-server.d.ts +52 -1
  46. package/dist/server/ws-server.js +151 -35
  47. package/package.json +3 -2
  48. package/dist/client/assets/index-Bdym8BPo.css +0 -1
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Orchestrator service coordinating sibling and child conversations.
3
+ *
4
+ * Provides lifecycle methods (spawn, dispatch, status, list, stop, resume) without
5
+ * duplicating message history or inflating memory.
6
+ */
7
+ export class SessionOrchestratorService {
8
+ store;
9
+ conversationService;
10
+ runService;
11
+ ledger;
12
+ broadcaster;
13
+ constructor(store, conversationService, runService, ledger, broadcaster) {
14
+ this.store = store;
15
+ this.conversationService = conversationService;
16
+ this.runService = runService;
17
+ this.ledger = ledger;
18
+ this.broadcaster = broadcaster;
19
+ }
20
+ /**
21
+ * Spawns a new sibling conversation and immediately dispatches its initial task.
22
+ */
23
+ async spawn(options) {
24
+ let workspaceId = options.workspaceId;
25
+ if (!workspaceId && options.parentConversationId) {
26
+ const parent = this.store.conversation(options.parentConversationId);
27
+ workspaceId = parent?.workspaceId;
28
+ }
29
+ if (!workspaceId) {
30
+ throw new Error("创建兄弟会话需要指定工作区(或提供有效的父会话 ID)。");
31
+ }
32
+ const conversationId = await this.conversationService.createConversation(workspaceId);
33
+ // Set title if requested or derive from task
34
+ if (options.title) {
35
+ await this.conversationService.renameConversation(conversationId, options.title);
36
+ }
37
+ else if (options.task) {
38
+ const titleSnippet = options.task.trim().replace(/\s+/g, " ").slice(0, 30);
39
+ await this.conversationService.renameConversation(conversationId, `任务:${titleSnippet}`);
40
+ }
41
+ // Apply configuration overrides if provided
42
+ if (options.agentId) {
43
+ await this.conversationService.setAgent(conversationId, options.agentId);
44
+ }
45
+ if (options.model) {
46
+ await this.conversationService.setModel(conversationId, options.model.provider, options.model.modelId);
47
+ }
48
+ if (options.thinkingLevel) {
49
+ await this.conversationService.setThinking(conversationId, options.thinkingLevel);
50
+ }
51
+ // Register in relationship ledger
52
+ this.ledger.register({
53
+ conversationId,
54
+ workspaceId,
55
+ origin: options.parentConversationId ? "orchestrator" : "user",
56
+ parentConversationId: options.parentConversationId,
57
+ task: options.task,
58
+ status: "idle",
59
+ });
60
+ // Immediately dispatch initial work
61
+ const prompt = options.initialPrompt ?? options.task;
62
+ if (prompt && prompt.trim()) {
63
+ await this.dispatch(conversationId, prompt, { wait: options.wait });
64
+ }
65
+ return { conversationId };
66
+ }
67
+ /**
68
+ * Dispatches a message to an arbitrary session (server-to-server).
69
+ * By default dispatches asynchronously so the orchestrator caller is not blocked.
70
+ */
71
+ async dispatch(conversationId, content, options) {
72
+ const conversation = this.store.conversation(conversationId);
73
+ if (!conversation) {
74
+ throw new Error(`找不到会话:${conversationId}`);
75
+ }
76
+ this.ledger.update(conversationId, {
77
+ status: "running",
78
+ });
79
+ const runPromise = this.runService.enqueueAndRun(conversationId, content);
80
+ if (options?.wait) {
81
+ await runPromise;
82
+ }
83
+ else {
84
+ void runPromise.catch((error) => {
85
+ console.warn(`Super Pi: dispatch failed for conversation ${conversationId}:`, error instanceof Error ? error.message : String(error));
86
+ this.ledger.update(conversationId, { status: "failed" });
87
+ });
88
+ }
89
+ return { conversationId };
90
+ }
91
+ /**
92
+ * Returns a high-level summary of a session's progress without returning full transcripts.
93
+ * Uses narrow store reader `store.conversation(id)` to avoid cloning the entire state.
94
+ */
95
+ async status(conversationId) {
96
+ const conversation = this.store.conversation(conversationId);
97
+ if (!conversation) {
98
+ throw new Error(`找不到会话:${conversationId}`);
99
+ }
100
+ const ledgerEntry = this.ledger.get(conversationId);
101
+ const messages = conversation.messages ?? [];
102
+ let turnCount = 0;
103
+ let lastAssistantSnippet;
104
+ let lastUserSnippet;
105
+ let activeTool;
106
+ for (let i = messages.length - 1; i >= 0; i--) {
107
+ const msg = messages[i];
108
+ if (!lastAssistantSnippet && msg.role === "assistant") {
109
+ const raw = msg.content || (msg.thinkingBlocks ? msg.thinkingBlocks.map((b) => b.text).join(" ") : "") || "";
110
+ lastAssistantSnippet = raw.slice(0, 300);
111
+ }
112
+ if (!lastUserSnippet && msg.role === "user") {
113
+ lastUserSnippet = (msg.content || "").slice(0, 300);
114
+ }
115
+ if (msg.role === "assistant") {
116
+ turnCount++;
117
+ }
118
+ }
119
+ if (conversation.status === "running" || conversation.status === "waiting") {
120
+ const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
121
+ if (lastAssistant?.toolCalls?.length) {
122
+ const lastCall = lastAssistant.toolCalls[lastAssistant.toolCalls.length - 1];
123
+ if (!lastCall.status || lastCall.live || lastCall.preparing) {
124
+ activeTool = { label: lastCall.label, detail: lastCall.detail };
125
+ }
126
+ }
127
+ }
128
+ const hasPendingQuestion = this.store.pendingQuestions(conversationId).length > 0;
129
+ // Sync ledger status if present
130
+ if (ledgerEntry && ledgerEntry.status !== conversation.status) {
131
+ this.ledger.update(conversationId, {
132
+ status: conversation.status,
133
+ lastProgress: lastAssistantSnippet ?? ledgerEntry.lastProgress,
134
+ });
135
+ }
136
+ return {
137
+ conversationId: conversation.id,
138
+ workspaceId: conversation.workspaceId,
139
+ title: conversation.title,
140
+ status: conversation.status,
141
+ origin: ledgerEntry?.origin ?? "user",
142
+ parentConversationId: ledgerEntry?.parentConversationId,
143
+ task: ledgerEntry?.task,
144
+ messageCount: messages.length,
145
+ turnCount,
146
+ lastAssistantSnippet,
147
+ lastUserSnippet,
148
+ hasPendingQuestion,
149
+ activeTool,
150
+ error: conversation.error,
151
+ updatedAt: conversation.updatedAt,
152
+ createdAt: ledgerEntry?.createdAt,
153
+ lastProgress: ledgerEntry?.lastProgress ?? lastAssistantSnippet,
154
+ };
155
+ }
156
+ /**
157
+ * Lists summaries of all sessions, optionally filtered by parent conversation.
158
+ */
159
+ async list(parentConversationId) {
160
+ const entries = parentConversationId
161
+ ? this.ledger.listByParent(parentConversationId)
162
+ : this.ledger.listAll();
163
+ const summaries = [];
164
+ for (const entry of entries) {
165
+ try {
166
+ const summary = await this.status(entry.conversationId);
167
+ summaries.push(summary);
168
+ }
169
+ catch {
170
+ // Conversation was removed from store; prune ledger
171
+ this.ledger.delete(entry.conversationId);
172
+ }
173
+ }
174
+ return summaries;
175
+ }
176
+ /**
177
+ * Stops a target session.
178
+ * Can distinguish between interrupting an active turn, cancelling queued messages, or both.
179
+ */
180
+ async stop(conversationId, options) {
181
+ const conversation = this.store.conversation(conversationId);
182
+ if (!conversation) {
183
+ throw new Error(`找不到会话:${conversationId}`);
184
+ }
185
+ let mode = options?.mode ?? "all";
186
+ if (options?.messageId) {
187
+ await this.runService.interruptQueued(conversationId, options.messageId);
188
+ mode = "queued-interrupt";
189
+ }
190
+ else if (mode === "queued") {
191
+ const waiting = this.store.listWaitingMessages(conversationId);
192
+ for (const msg of waiting) {
193
+ await this.runService.cancelQueued(conversationId, msg.id);
194
+ }
195
+ mode = "queued-cancel";
196
+ }
197
+ else if (mode === "active") {
198
+ await this.runService.stopRun(conversationId);
199
+ mode = "active-stop";
200
+ }
201
+ else {
202
+ // mode === "all"
203
+ await this.runService.stopRun(conversationId);
204
+ const waiting = this.store.listWaitingMessages(conversationId);
205
+ for (const msg of waiting) {
206
+ await this.runService.cancelQueued(conversationId, msg.id);
207
+ }
208
+ mode = "all";
209
+ }
210
+ this.ledger.update(conversationId, { status: "stopped" });
211
+ return { stopped: true, mode };
212
+ }
213
+ /**
214
+ * Resumes execution in a conversation.
215
+ * If prompt is provided, sends it as a new turn.
216
+ * Otherwise, if there is a waiting message in the queue, kicks off its execution.
217
+ */
218
+ async resume(conversationId, prompt, options) {
219
+ const conversation = this.store.conversation(conversationId);
220
+ if (!conversation) {
221
+ throw new Error(`找不到会话:${conversationId}`);
222
+ }
223
+ if (prompt && prompt.trim()) {
224
+ return this.dispatch(conversationId, prompt, options);
225
+ }
226
+ const waiting = this.store.listWaitingMessages(conversationId).find((item) => item.delivery === "waiting");
227
+ if (waiting) {
228
+ await this.runService.sendQueued(conversationId, waiting.id);
229
+ this.ledger.update(conversationId, { status: "running" });
230
+ return { conversationId };
231
+ }
232
+ throw new Error("该会话没有排队的消息可恢复,请提供 prompt 发送新消息。");
233
+ }
234
+ }
@@ -1452,6 +1452,141 @@ export function createHttpServer(options) {
1452
1452
  }
1453
1453
  return;
1454
1454
  }
1455
+ if (url.pathname === "/api/orchestrator") {
1456
+ if (request.method !== "POST" || !isLocalOrPrivateAddress(remote)) {
1457
+ response.writeHead(403, { "content-type": "text/plain; charset=utf-8" }).end("Orchestrator requires a loopback or private network POST");
1458
+ return;
1459
+ }
1460
+ const asked = await readRequestText(request, MAX_FILE_BYTES * 2);
1461
+ if (asked.tooLarge) {
1462
+ response.writeHead(413, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "请求体过大", code: "too_large" }));
1463
+ return;
1464
+ }
1465
+ const json = (status, body) => {
1466
+ response.writeHead(status, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify(body));
1467
+ };
1468
+ try {
1469
+ const body = JSON.parse(asked.text);
1470
+ const service = context.sessionOrchestratorService;
1471
+ if (!service) {
1472
+ json(503, { error: "编排服务未就绪" });
1473
+ return;
1474
+ }
1475
+ if (body.capability && body.parentConversationId) {
1476
+ const verdict = runService.verifyPrimaryCapability(body.parentConversationId, body.capability);
1477
+ if (!verdict.ok) {
1478
+ json(403, { error: verdict.message });
1479
+ return;
1480
+ }
1481
+ }
1482
+ else if (context.accessToken && !isAuthorized(request, context.accessToken)) {
1483
+ json(401, { error: "未授权" });
1484
+ return;
1485
+ }
1486
+ const action = (body.action || "").trim();
1487
+ if (action === "spawn") {
1488
+ if (!body.task)
1489
+ throw new Error("spawn requires task");
1490
+ const result = await service.spawn({
1491
+ task: body.task,
1492
+ parentConversationId: body.parentConversationId,
1493
+ workspaceId: body.workspaceId,
1494
+ title: body.title,
1495
+ agentId: body.agentId,
1496
+ model: body.model,
1497
+ thinkingLevel: body.thinkingLevel,
1498
+ initialPrompt: body.initialPrompt,
1499
+ wait: body.wait,
1500
+ });
1501
+ json(200, { ok: true, ...result });
1502
+ return;
1503
+ }
1504
+ if (action === "dispatch") {
1505
+ if (!body.conversationId)
1506
+ throw new Error("dispatch requires conversationId");
1507
+ if (!body.content)
1508
+ throw new Error("dispatch requires content");
1509
+ const result = await service.dispatch(body.conversationId, body.content, { wait: body.wait });
1510
+ json(200, { ok: true, ...result });
1511
+ return;
1512
+ }
1513
+ if (action === "status") {
1514
+ if (!body.conversationId)
1515
+ throw new Error("status requires conversationId");
1516
+ const summary = await service.status(body.conversationId);
1517
+ json(200, { ok: true, summary });
1518
+ return;
1519
+ }
1520
+ if (action === "list") {
1521
+ const sessions = await service.list(body.parentConversationId);
1522
+ json(200, { ok: true, sessions });
1523
+ return;
1524
+ }
1525
+ if (action === "stop") {
1526
+ if (!body.conversationId)
1527
+ throw new Error("stop requires conversationId");
1528
+ const result = await service.stop(body.conversationId, { mode: body.mode, messageId: body.messageId });
1529
+ json(200, { ok: true, ...result });
1530
+ return;
1531
+ }
1532
+ if (action === "resume") {
1533
+ if (!body.conversationId)
1534
+ throw new Error("resume requires conversationId");
1535
+ const result = await service.resume(body.conversationId, body.prompt, { wait: body.wait });
1536
+ json(200, { ok: true, ...result });
1537
+ return;
1538
+ }
1539
+ json(400, { error: `不支持的编排操作:${action}` });
1540
+ }
1541
+ catch (error) {
1542
+ json(400, { error: error instanceof Error ? error.message : "orchestrator request failed" });
1543
+ }
1544
+ return;
1545
+ }
1546
+ if (url.pathname === "/api/orchestrator/status" && request.method === "GET") {
1547
+ if (!isLocalOrPrivateAddress(remote)) {
1548
+ response.writeHead(403).end();
1549
+ return;
1550
+ }
1551
+ const service = context.sessionOrchestratorService;
1552
+ if (!service) {
1553
+ response.writeHead(503, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "编排服务未就绪" }));
1554
+ return;
1555
+ }
1556
+ const conversationId = url.searchParams.get("conversationId");
1557
+ if (!conversationId) {
1558
+ response.writeHead(400, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "缺少 conversationId" }));
1559
+ return;
1560
+ }
1561
+ try {
1562
+ const summary = await service.status(conversationId);
1563
+ response.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ ok: true, summary }));
1564
+ }
1565
+ catch (error) {
1566
+ response.writeHead(400, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: error instanceof Error ? error.message : "status failed" }));
1567
+ }
1568
+ return;
1569
+ }
1570
+ if (url.pathname === "/api/orchestrator/list" && request.method === "GET") {
1571
+ if (!isLocalOrPrivateAddress(remote)) {
1572
+ response.writeHead(403).end();
1573
+ return;
1574
+ }
1575
+ const service = context.sessionOrchestratorService;
1576
+ if (!service) {
1577
+ response.writeHead(503, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "编排服务未就绪" }));
1578
+ return;
1579
+ }
1580
+ const parentId = url.searchParams.get("parentId") || undefined;
1581
+ try {
1582
+ const sessions = await service.list(parentId);
1583
+ response.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ ok: true, sessions }));
1584
+ }
1585
+ catch (error) {
1586
+ response.writeHead(400, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: error instanceof Error ? error.message : "list failed" }));
1587
+ }
1588
+ return;
1589
+ }
1455
1590
  if (url.pathname === "/api/question") {
1456
1591
  if (request.method !== "POST" || !isLocalOrPrivateAddress(remote)) {
1457
1592
  response.writeHead(403).end();
@@ -24,6 +24,8 @@ import { RunService } from "./application/run-service.js";
24
24
  import { FileChangeTracker } from "./file-changes.js";
25
25
  import { ConversationService } from "./application/conversation-service.js";
26
26
  import { ScheduledTaskService } from "./application/scheduled-task-service.js";
27
+ import { SessionLedger } from "./session-ledger.js";
28
+ import { SessionOrchestratorService } from "./application/session-orchestrator-service.js";
27
29
  import { WorkspaceService } from "./application/workspace-service.js";
28
30
  import { CommandHandler } from "./command-handler.js";
29
31
  import { TerminalService, terminalDisabledReason } from "./terminal.js";
@@ -114,6 +116,7 @@ const workflowEndpoint = `http://${internalHost}:${port}/api/workflow/run`;
114
116
  const questionEndpoint = `http://${internalHost}:${port}/api/question`;
115
117
  const toolsEndpoint = `http://${internalHost}:${port}/api/tools`;
116
118
  const documentsEndpoint = `http://${internalHost}:${port}/api/documents/tool`;
119
+ const orchestratorEndpoint = `http://${internalHost}:${port}/api/orchestrator`;
117
120
  // ------------------------------------------------------------- Core singletons
118
121
  const store = await openStateStore(dataDir, process.env.SUPER_PI_PERSISTENCE ?? "sqlite");
119
122
  const toolRegistry = new ToolRegistry(dataDir);
@@ -136,7 +139,7 @@ const paths = {
136
139
  };
137
140
  const runtime = process.env.SUPER_PI_RUNTIME === "welcome"
138
141
  ? new WelcomeRuntime()
139
- : new PiRpcRuntime(join(dataDir, "pi-sessions"), undefined, { dir: initialAgentDirs, promptDir: agentPromptDir }, { endpoint: taskEndpoint, execEndpoint, todoEndpoint, workflowEndpoint, questionEndpoint, toolsEndpoint, documentsEndpoint, scheduledTaskEndpoint, uploadsDir });
142
+ : new PiRpcRuntime(join(dataDir, "pi-sessions"), undefined, { dir: initialAgentDirs, promptDir: agentPromptDir }, { endpoint: taskEndpoint, execEndpoint, todoEndpoint, workflowEndpoint, questionEndpoint, toolsEndpoint, documentsEndpoint, scheduledTaskEndpoint, orchestratorEndpoint, uploadsDir });
140
143
  // Closed workspaces stay in the store for reopening. Focused sockets receive all visible sidebar
141
144
  // summaries and only their selected transcript; legacy sockets retain the complete visible state.
142
145
  const broadcaster = new WsBroadcaster((scope) => store.clientState(scope));
@@ -173,13 +176,24 @@ supervisor.setMemory(memory);
173
176
  const validateModel = (conversationId, selection) => validateConversationModel(store, runtime, conversationId, selection);
174
177
  runtime.setModelValidator?.(validateModel);
175
178
  supervisor.setModelValidator(validateModel);
179
+ /**
180
+ * DIAGNOSTIC ONLY - reproduces the pre-fix flood wire-for-wire (a full `state` broadcast per
181
+ * `subagent.event`, uncapped live output) so probes and regression tests can A/B the fix. Never set
182
+ * outside a probe; see docs/SUBAGENT-WS-FLOOD-FIX-2026-10-29.md.
183
+ */
184
+ const legacySubagentFlood = process.env.SUPER_PI_LEGACY_SUBAGENT_FLOOD === "1";
176
185
  const subagents = new SubagentRunRecorder(supervisor, {
177
186
  store,
178
187
  broadcast: (event) => {
179
188
  broadcaster.broadcast(event);
180
- if (event.type === "subagent.event")
189
+ if (legacySubagentFlood && event.type === "subagent.event")
181
190
  broadcaster.broadcastState();
182
191
  },
192
+ // The full state broadcast moved from "every delta" to "on settle": a browser that reloads mid-run
193
+ // rebuilds the card from the store (written per delta); the settle frame is what puts the FINAL
194
+ // record into `state` so a reload after the finish sees it too.
195
+ onSettled: legacySubagentFlood ? undefined : () => broadcaster.broadcastState(),
196
+ liveTailCap: legacySubagentFlood ? Number.POSITIVE_INFINITY : undefined,
183
197
  host: (conversationId, runId) => extensionUiService.createHost(conversationId, runId),
184
198
  });
185
199
  /**
@@ -275,6 +289,8 @@ runService.setExecService(execService);
275
289
  * message takes.
276
290
  */
277
291
  const scheduledTaskService = new ScheduledTaskService(store, runService, conversationService, broadcaster);
292
+ const sessionLedger = new SessionLedger();
293
+ const sessionOrchestratorService = new SessionOrchestratorService(store, conversationService, runService, sessionLedger, broadcaster);
278
294
  const serverContext = {
279
295
  memory,
280
296
  knowledge,
@@ -294,6 +310,8 @@ const serverContext = {
294
310
  catalogService,
295
311
  providerService,
296
312
  scheduledTaskService,
313
+ sessionLedger,
314
+ sessionOrchestratorService,
297
315
  execService,
298
316
  toolRegistry,
299
317
  toolRunner,
@@ -742,6 +742,7 @@ export declare class PiRpcRuntime implements AgentRuntime {
742
742
  toolsEndpoint?: string;
743
743
  documentsEndpoint?: string;
744
744
  scheduledTaskEndpoint?: string;
745
+ orchestratorEndpoint?: string;
745
746
  uploadsDir?: string;
746
747
  sessionFactory?: PiSessionFactory;
747
748
  sessionMode?: "sdk" | "oneshot";
@@ -21,7 +21,7 @@ import { describeDeliveredUserMessage, describePiQueue, handleSessionEvent, Turn
21
21
  // paths recognise a delivery identically. Re-exported because they were part of this module's surface.
22
22
  export { describeDeliveredUserMessage, describePiQueue } from "./pi-session-events.js";
23
23
  import { createPiSession, createSdkCatalog, piSdkAvailable, resolveTranscriptFile, superPiExtensionPaths } from "./pi-session.js";
24
- import { TASK_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_ENDPOINT_ENV, TASK_ROSTER_ENV, TASK_TOOL_NAME, TODO_ENDPOINT_ENV, TODO_TOOL_NAME, SCHEDULED_TASK_ENDPOINT_ENV, SCHEDULED_TASK_TOOL_NAME, WORKFLOW_ENDPOINT_ENV, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, rosterPayload, } from "./task-tool.js";
24
+ import { TASK_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_ENDPOINT_ENV, TASK_ROSTER_ENV, TASK_TOOL_NAME, TODO_ENDPOINT_ENV, TODO_TOOL_NAME, SCHEDULED_TASK_ENDPOINT_ENV, SCHEDULED_TASK_TOOL_NAME, WORKFLOW_ENDPOINT_ENV, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, ORCHESTRATOR_ENDPOINT_ENV, SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, rosterPayload, } from "./task-tool.js";
25
25
  import { BLOCKING_UI_METHODS, EXTENSION_UI_METHODS, } from "./types.js";
26
26
  import { loadConversationImages } from "./upload.js";
27
27
  import { describeToolCall, describeToolCallArgs, describeToolCallInProgress, extractPartialResultText, extractToolResultText, readQuestionCall } from "./tool-calls.js";
@@ -709,6 +709,8 @@ export class PiRpcRuntime {
709
709
  [TODO_ENDPOINT_ENV]: this.options.todoEndpoint ?? "",
710
710
  [SCHEDULED_TASK_ENDPOINT_ENV]: this.options.scheduledTaskEndpoint ?? "",
711
711
  [WORKFLOW_ENDPOINT_ENV]: this.options.workflowEndpoint ?? "",
712
+ [ORCHESTRATOR_ENDPOINT_ENV]: this.options.orchestratorEndpoint ?? "",
713
+ SUPER_PI_ORCHESTRATOR_CONVERSATION: conversationId,
712
714
  SUPER_PI_QUESTION_ENDPOINT: this.options.questionEndpoint ?? "",
713
715
  SUPER_PI_TOOLS_ENDPOINT: this.options.toolsEndpoint ?? "",
714
716
  // The document tools' endpoint and the conversation they act for. The CREDENTIAL is not here:
@@ -1240,6 +1242,8 @@ export class PiRpcRuntime {
1240
1242
  // loaded-but-undeclared tool - broken with no error to explain it.
1241
1243
  if (process.env.SUPER_PI_FEISHU_ENDPOINT)
1242
1244
  names.push(FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME);
1245
+ if (this.options.orchestratorEndpoint)
1246
+ names.push(SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME);
1243
1247
  return names;
1244
1248
  }
1245
1249
  /**
@@ -1458,7 +1462,7 @@ export class PiRpcRuntime {
1458
1462
  // allowlist and naming a tool Pi has not registered would change the spawn line for every other
1459
1463
  // mode. The prompt asks for a bare JSON array and nothing else; if the model spends a turn on a
1460
1464
  // tool call anyway the reranker times out and falls back to the lexical order.
1461
- const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : knowledgeRerank ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, SCHEDULED_TASK_TOOL_NAME, "question", "tool_register"] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : []), ...(knowledgeEnabled ? [KNOWLEDGE_TOOL_NAME, KNOWLEDGE_IMPORT_TOOL_NAME] : []), ...(documentsEnabled ? [DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME] : []), ...(webEnabled ? [WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME] : []), ...(feishuEnabled ? [FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME] : [])] });
1465
+ const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : knowledgeRerank ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, SCHEDULED_TASK_TOOL_NAME, "question", "tool_register", ...(this.options.orchestratorEndpoint ? [SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME] : [])] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : []), ...(knowledgeEnabled ? [KNOWLEDGE_TOOL_NAME, KNOWLEDGE_IMPORT_TOOL_NAME] : []), ...(documentsEnabled ? [DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME] : []), ...(webEnabled ? [WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME] : []), ...(feishuEnabled ? [FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME] : [])] });
1462
1466
  // Refused here, with the reason, rather than handed to the spawn: an ENOENT from `spawn` reads as
1463
1467
  // "the app is broken" and hides the one fact that matters, which is where it looked for Pi.
1464
1468
  if (!this.pi.found)
@@ -71,6 +71,8 @@ export interface ServerContext {
71
71
  * task of its own.
72
72
  */
73
73
  scheduledTaskService?: import("./application/scheduled-task-service.js").ScheduledTaskService;
74
+ sessionLedger?: import("./session-ledger.js").SessionLedger;
75
+ sessionOrchestratorService?: import("./application/session-orchestrator-service.js").SessionOrchestratorService;
74
76
  execService: ExecService;
75
77
  toolRegistry: ToolRegistry;
76
78
  toolRunner: ToolRunner;
@@ -0,0 +1,60 @@
1
+ export type SessionOrigin = "user" | "orchestrator";
2
+ export type SessionLedgerStatus = "idle" | "running" | "waiting" | "stopped" | "failed" | "completed";
3
+ /**
4
+ * Metadata entry for a conversation managed or observed by the orchestrator.
5
+ * Crucially, NO transcript or message history is retained here to keep memory footprint bounded.
6
+ */
7
+ export interface SessionLedgerEntry {
8
+ conversationId: string;
9
+ workspaceId: string;
10
+ /** Whether created manually by user or by orchestrator agent */
11
+ origin: SessionOrigin;
12
+ /** If created by orchestrator: the parent conversation that dispatched it */
13
+ parentConversationId?: string;
14
+ /** One-line task description assigned to this session */
15
+ task?: string;
16
+ /** Current status */
17
+ status: SessionLedgerStatus;
18
+ createdAt: number;
19
+ updatedAt: number;
20
+ /** High-level progress snippet or latest outcome */
21
+ lastProgress?: string;
22
+ }
23
+ /**
24
+ * In-memory relationship ledger for sibling and child sessions.
25
+ * Holds only metadata (parent-child relationships, tasks, statuses) - never transcripts.
26
+ */
27
+ export declare class SessionLedger {
28
+ private readonly entries;
29
+ /**
30
+ * Registers a session into the ledger.
31
+ */
32
+ register(entry: Omit<SessionLedgerEntry, "createdAt" | "updatedAt"> & {
33
+ createdAt?: number;
34
+ updatedAt?: number;
35
+ }): SessionLedgerEntry;
36
+ /**
37
+ * Updates metadata for an existing session in the ledger.
38
+ */
39
+ update(conversationId: string, patch: Partial<Omit<SessionLedgerEntry, "conversationId" | "createdAt">>): SessionLedgerEntry | undefined;
40
+ /**
41
+ * Retrieves metadata for one session.
42
+ */
43
+ get(conversationId: string): SessionLedgerEntry | undefined;
44
+ /**
45
+ * Lists all sessions dispatched by a given parent conversation, sorted newest first.
46
+ */
47
+ listByParent(parentConversationId: string): SessionLedgerEntry[];
48
+ /**
49
+ * Lists all recorded sessions in the ledger, sorted newest first.
50
+ */
51
+ listAll(): SessionLedgerEntry[];
52
+ /**
53
+ * Removes a session from the ledger.
54
+ */
55
+ delete(conversationId: string): boolean;
56
+ /**
57
+ * Clears all ledger entries (primarily for test cleanup).
58
+ */
59
+ clear(): void;
60
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * In-memory relationship ledger for sibling and child sessions.
3
+ * Holds only metadata (parent-child relationships, tasks, statuses) - never transcripts.
4
+ */
5
+ export class SessionLedger {
6
+ entries = new Map();
7
+ /**
8
+ * Registers a session into the ledger.
9
+ */
10
+ register(entry) {
11
+ const now = Date.now();
12
+ const fullEntry = {
13
+ ...entry,
14
+ createdAt: entry.createdAt ?? now,
15
+ updatedAt: entry.updatedAt ?? now,
16
+ };
17
+ this.entries.set(entry.conversationId, fullEntry);
18
+ return { ...fullEntry };
19
+ }
20
+ /**
21
+ * Updates metadata for an existing session in the ledger.
22
+ */
23
+ update(conversationId, patch) {
24
+ const existing = this.entries.get(conversationId);
25
+ if (!existing)
26
+ return undefined;
27
+ const updated = {
28
+ ...existing,
29
+ ...patch,
30
+ updatedAt: Date.now(),
31
+ };
32
+ this.entries.set(conversationId, updated);
33
+ return { ...updated };
34
+ }
35
+ /**
36
+ * Retrieves metadata for one session.
37
+ */
38
+ get(conversationId) {
39
+ const entry = this.entries.get(conversationId);
40
+ return entry ? { ...entry } : undefined;
41
+ }
42
+ /**
43
+ * Lists all sessions dispatched by a given parent conversation, sorted newest first.
44
+ */
45
+ listByParent(parentConversationId) {
46
+ const result = [];
47
+ for (const entry of this.entries.values()) {
48
+ if (entry.parentConversationId === parentConversationId) {
49
+ result.push({ ...entry });
50
+ }
51
+ }
52
+ return result.sort((a, b) => b.createdAt - a.createdAt);
53
+ }
54
+ /**
55
+ * Lists all recorded sessions in the ledger, sorted newest first.
56
+ */
57
+ listAll() {
58
+ const result = [];
59
+ for (const entry of this.entries.values()) {
60
+ result.push({ ...entry });
61
+ }
62
+ return result.sort((a, b) => b.createdAt - a.createdAt);
63
+ }
64
+ /**
65
+ * Removes a session from the ledger.
66
+ */
67
+ delete(conversationId) {
68
+ return this.entries.delete(conversationId);
69
+ }
70
+ /**
71
+ * Clears all ledger entries (primarily for test cleanup).
72
+ */
73
+ clear() {
74
+ this.entries.clear();
75
+ }
76
+ }