@evandu/super-pi 0.1.141 → 0.1.143
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 +55 -27
- package/agents/super-pi-task/index.ts +76 -4
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +7 -8
- package/dist/client/assets/{CodeMirrorEditor-Dm5IDKy9.js → CodeMirrorEditor-CXDQ0ikN.js} +2 -2
- package/dist/client/assets/{dist-BGaKp7BG.js → dist-BK_KWsQr.js} +1 -1
- package/dist/client/assets/{dist-CN1j5LO2.js → dist-BKsr7wc2.js} +1 -1
- package/dist/client/assets/{dist-C7UQGfmz.js → dist-BXQWaRci.js} +1 -1
- package/dist/client/assets/{dist-BvCNiV_z.js → dist-B_uWAP2-.js} +1 -1
- package/dist/client/assets/{dist-CNgQgdyS.js → dist-BwwacXyS.js} +1 -1
- package/dist/client/assets/{dist-GAPsnCM8.js → dist-CWd7iD1S.js} +1 -1
- package/dist/client/assets/{dist-DWj0wg8M.js → dist-CbjfwVs-.js} +1 -1
- package/dist/client/assets/{dist-PQZBepmy.js → dist-CoEavaUe.js} +1 -1
- package/dist/client/assets/{dist-BqXU6S8h.js → dist-D60BDUMe.js} +1 -1
- package/dist/client/assets/{dist-LCDxbTTI.js → dist-DBbQHcwG.js} +1 -1
- package/dist/client/assets/{dist-DtO-7WEZ.js → dist-DVEBhaUf.js} +1 -1
- package/dist/client/assets/{dist-D0eIQjoI.js → dist-Db6KH9Qv.js} +1 -1
- package/dist/client/assets/{dist-BMbwbGH9.js → dist-Dsj0Q8gO.js} +1 -1
- package/dist/client/assets/{dist-4baaMdqn.js → dist-H0dUhAgy.js} +1 -1
- package/dist/client/assets/{dist-D0-9TI_k.js → dist-MZSBP_1m.js} +1 -1
- package/dist/client/assets/{dist-B-edBMDa.js → dist-PNt22XLU.js} +1 -1
- package/dist/client/assets/{dist-M3BFsjal.js → dist-S5UwGLvb.js} +1 -1
- package/dist/client/assets/{dist-F7KOq8Xm.js → dist-_jioFcsD.js} +1 -1
- package/dist/client/assets/{dist-CSDLiQKt.js → dist-hmP8KP8-.js} +1 -1
- package/dist/client/assets/{dist-D7YZUqfA.js → dist-jV9tqAoC.js} +1 -1
- package/dist/client/assets/{dist-CMDW84Eh.js → dist-oqETQw0G.js} +1 -1
- package/dist/client/assets/{dist-DQWPdnen.js → dist-oz2WOWPd.js} +1 -1
- package/dist/client/assets/{index-BJIfLtDs.css → index-6h_u-X47.css} +1 -1
- package/dist/client/assets/{index-DHsjaI7m.js → index-iSnWwgJ-.js} +26 -26
- package/dist/client/index.html +2 -2
- package/dist/server/application/conversation-service.d.ts +2 -1
- package/dist/server/application/conversation-service.js +25 -7
- package/dist/server/application/run-service.d.ts +15 -0
- package/dist/server/application/run-service.js +59 -1
- package/dist/server/application/session-orchestrator-service.d.ts +9 -1
- package/dist/server/application/session-orchestrator-service.js +79 -10
- package/dist/server/http-server.js +18 -11
- package/dist/server/runtime.js +7 -6
- package/dist/server/store.d.ts +12 -1
- package/dist/server/store.js +77 -4
- package/dist/server/task-tool.d.ts +4 -2
- package/dist/server/task-tool.js +6 -3
- package/dist/server/time-zone.d.ts +20 -0
- package/dist/server/time-zone.js +32 -0
- package/dist/server/types.d.ts +12 -0
- package/dist/server/workflow-engine.d.ts +11 -95
- package/dist/server/workflow-engine.js +122 -134
- package/dist/server/workflow.d.ts +73 -31
- package/dist/server/workflow.js +112 -30
- package/package.json +17 -2
package/dist/client/index.html
CHANGED
|
@@ -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-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-iSnWwgJ-.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-6h_u-X47.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -74,7 +74,8 @@ export declare class ConversationService {
|
|
|
74
74
|
*/
|
|
75
75
|
archiveConversation(conversationId: string): Promise<void>;
|
|
76
76
|
/**
|
|
77
|
-
* Unarchives
|
|
77
|
+
* Unarchives an archived tree root and every archived descendant. Descendants that are
|
|
78
|
+
* already active need no write; they are already in the live tree this restore rejoins.
|
|
78
79
|
*/
|
|
79
80
|
unarchiveConversation(conversationId: string): Promise<void>;
|
|
80
81
|
/**
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { assertModelAllowed } from "../model-filter.js";
|
|
2
2
|
import { newConversationId } from "../store.js";
|
|
3
|
+
function isMissingPrimaryAgent(error, agentId) {
|
|
4
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5
|
+
const id = JSON.stringify(agentId);
|
|
6
|
+
return message.includes(`Agent ${id} 已失效或不是可用的 primary agent`)
|
|
7
|
+
|| message.includes(`Agent ${id} 不存在或不是可用的 primary agent`);
|
|
8
|
+
}
|
|
3
9
|
/**
|
|
4
10
|
* Service managing conversation lifecycle: creation, selection, archiving,
|
|
5
11
|
* model/thinking/agent configuration, message reversion, and session forking.
|
|
@@ -104,12 +110,23 @@ export class ConversationService {
|
|
|
104
110
|
thinkingLevel = undefined;
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
let agentNotice;
|
|
114
|
+
let resolved = { model, thinkingLevel };
|
|
115
|
+
if (workspace.defaultAgentId) {
|
|
116
|
+
try {
|
|
117
|
+
resolved = await this.resolveAgentConfiguration({ id: workspaceId, model, thinkingLevel }, workspace.defaultAgentId, workspaceId);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
// Only an absent definition is downgraded. A present agent whose model, thinking level or
|
|
121
|
+
// workspace rules are invalid must still refuse rather than silently dropping its configuration.
|
|
122
|
+
if (!isMissingPrimaryAgent(error, workspace.defaultAgentId))
|
|
123
|
+
throw error;
|
|
124
|
+
agentNotice = `你设的默认 Agent「${workspace.defaultAgentId}」已经不存在了,这个会话改用 Pi 默认 Agent。请在工作区设置里重新选择默认 Agent。`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
110
127
|
// `null` is intentional here: Store uses it to remove a workspace value that it
|
|
111
128
|
// would otherwise have copied before applying the resolved configuration.
|
|
112
|
-
const defaults = { ...resolved, model: resolved.model ?? null, thinkingLevel: resolved.thinkingLevel ?? null, ...(modelNotice ? { modelNotice } : {}), ...(parentage ? { parentConversationId: parentage.parentConversationId, origin: parentage.origin } : {}) };
|
|
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 } : {}) };
|
|
113
130
|
const created = await this.store.createConversation(workspaceId, defaults);
|
|
114
131
|
this.broadcaster.broadcastState();
|
|
115
132
|
return created.id;
|
|
@@ -169,14 +186,15 @@ export class ConversationService {
|
|
|
169
186
|
*/
|
|
170
187
|
async archiveConversation(conversationId) {
|
|
171
188
|
this.runService.reviewMemoryBoundary(conversationId);
|
|
172
|
-
await this.store.
|
|
189
|
+
await this.store.archiveConversationTree(conversationId);
|
|
173
190
|
this.broadcaster.broadcastState();
|
|
174
191
|
}
|
|
175
192
|
/**
|
|
176
|
-
* Unarchives
|
|
193
|
+
* Unarchives an archived tree root and every archived descendant. Descendants that are
|
|
194
|
+
* already active need no write; they are already in the live tree this restore rejoins.
|
|
177
195
|
*/
|
|
178
196
|
async unarchiveConversation(conversationId) {
|
|
179
|
-
await this.store.
|
|
197
|
+
await this.store.unarchiveConversationTree(conversationId);
|
|
180
198
|
this.broadcaster.broadcastState();
|
|
181
199
|
}
|
|
182
200
|
/**
|
|
@@ -43,6 +43,7 @@ export interface WorkflowRunRequestBody {
|
|
|
43
43
|
summary?: string;
|
|
44
44
|
onFailure?: string;
|
|
45
45
|
steps?: unknown;
|
|
46
|
+
nodes?: unknown;
|
|
46
47
|
/** The per-turn dispatch capability, presented by the primary agent's extension. See `capability.ts`. */
|
|
47
48
|
capability?: string;
|
|
48
49
|
/** Pi's id for the tool call that asked for this, and resolved the same inert way as a sub-agent
|
|
@@ -148,6 +149,18 @@ export declare class RunService {
|
|
|
148
149
|
* transcript - so a `role: "user"` is never impersonated.
|
|
149
150
|
*/
|
|
150
151
|
enqueueExecResult(sessionId: string, exitCode: number, stdout: string, stderr: string): void;
|
|
152
|
+
/**
|
|
153
|
+
* Delivers an orchestrated child session's final output to its direct parent. This intentionally uses
|
|
154
|
+
* the same waiting-queue contract as background sub-agent results: one result per turn, ahead of
|
|
155
|
+
* unclaimed user messages, with an automatic follow-up turn only when the parent is idle.
|
|
156
|
+
*/
|
|
157
|
+
enqueueSessionResult(input: {
|
|
158
|
+
conversationId: string;
|
|
159
|
+
parentConversationId?: string;
|
|
160
|
+
output: string;
|
|
161
|
+
resultPath: string;
|
|
162
|
+
error?: string;
|
|
163
|
+
}): Promise<void>;
|
|
151
164
|
/**
|
|
152
165
|
* Stops an active background task (spawned process or detached subagent) on user request.
|
|
153
166
|
*
|
|
@@ -231,6 +244,8 @@ export declare class RunService {
|
|
|
231
244
|
* Checks whether a conversation has an active turn running.
|
|
232
245
|
*/
|
|
233
246
|
isRunning(conversationId: string): boolean;
|
|
247
|
+
/** Waits for this conversation's current producer, without waiting for unrelated sessions. */
|
|
248
|
+
waitForConversationIdle(conversationId: string): Promise<void>;
|
|
234
249
|
/** Returns true while a stop has been requested and the old producer is still unwinding. */
|
|
235
250
|
private isStopping;
|
|
236
251
|
/**
|
|
@@ -3,7 +3,7 @@ import { checkSubagentDepth, parentDeniedTools, subagentRoster } from "../task-t
|
|
|
3
3
|
import { newSubagentRunId } from "../subagents.js";
|
|
4
4
|
import { readAgents } from "../agents.js";
|
|
5
5
|
import { verifyCapability } from "../capability.js";
|
|
6
|
-
import { validateWorkflowPlan, workflowSummary } from "../workflow.js";
|
|
6
|
+
import { capText, validateWorkflowPlan, workflowSummary } from "../workflow.js";
|
|
7
7
|
import { ensureNameIndex } from "../file-name-index.js";
|
|
8
8
|
import { formatTurnFailure } from "../turn-failure.js";
|
|
9
9
|
import { describeThrownWithStack } from "../failure-text.js";
|
|
@@ -304,6 +304,54 @@ export class RunService {
|
|
|
304
304
|
}
|
|
305
305
|
})());
|
|
306
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Delivers an orchestrated child session's final output to its direct parent. This intentionally uses
|
|
309
|
+
* the same waiting-queue contract as background sub-agent results: one result per turn, ahead of
|
|
310
|
+
* unclaimed user messages, with an automatic follow-up turn only when the parent is idle.
|
|
311
|
+
*/
|
|
312
|
+
async enqueueSessionResult(input) {
|
|
313
|
+
await this.trackDetached((async () => {
|
|
314
|
+
const parentId = input.parentConversationId;
|
|
315
|
+
if (!parentId)
|
|
316
|
+
return;
|
|
317
|
+
const parent = this.store.conversation(parentId);
|
|
318
|
+
if (!parent || parent.archivedAt)
|
|
319
|
+
return;
|
|
320
|
+
// A corrupt or manually edited parent chain must not bounce a result around a cycle forever.
|
|
321
|
+
const seen = new Set();
|
|
322
|
+
let ancestor = parent;
|
|
323
|
+
while (ancestor && !seen.has(ancestor.id)) {
|
|
324
|
+
if (ancestor.id === input.conversationId)
|
|
325
|
+
return;
|
|
326
|
+
seen.add(ancestor.id);
|
|
327
|
+
ancestor = ancestor.parentConversationId ? this.store.conversation(ancestor.parentConversationId) : undefined;
|
|
328
|
+
}
|
|
329
|
+
const failed = input.error !== undefined;
|
|
330
|
+
const state = failed ? "error" : "completed";
|
|
331
|
+
const tag = failed ? "task_error" : "task_result";
|
|
332
|
+
const output = failed ? input.error : input.output;
|
|
333
|
+
const inline = output.length <= 8_000 ? output : capText(output, 4_000, "子会话输出").text;
|
|
334
|
+
const summary = `Child session ${input.conversationId} ${failed ? "failed" : "completed"}. The full result is in ${input.resultPath}; use the read tool to inspect it.`;
|
|
335
|
+
const message = `<task id="${input.conversationId}" state="${state}" kind="session">\n<summary>${summary}</summary>\n<result_file>${input.resultPath}</result_file>\n<${tag}>\n${inline}\n</${tag}>\n</task>`;
|
|
336
|
+
const queued = await this.serializeConversation(parentId, async () => {
|
|
337
|
+
// The parent can disappear or become archived while this delivery waits for its command lane.
|
|
338
|
+
const current = this.store.conversation(parentId);
|
|
339
|
+
if (!current || current.archivedAt)
|
|
340
|
+
return undefined;
|
|
341
|
+
const row = await this.store.addToolMessage(parentId, message, { delivery: "waiting" });
|
|
342
|
+
await this.moveAheadOfUnclaimed(parentId, row.id);
|
|
343
|
+
this.broadcaster.broadcastState();
|
|
344
|
+
return row;
|
|
345
|
+
});
|
|
346
|
+
if (!queued)
|
|
347
|
+
return;
|
|
348
|
+
if (!this.activeRuns.has(parentId) && !this.interruptTargets.has(parentId)) {
|
|
349
|
+
void this.processWaitingMessage(parentId, queued.id).catch((error) => {
|
|
350
|
+
console.warn(`Super Pi: auto-trigger for child session result failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
})());
|
|
354
|
+
}
|
|
307
355
|
/**
|
|
308
356
|
* Stops an active background task (spawned process or detached subagent) on user request.
|
|
309
357
|
*
|
|
@@ -525,6 +573,16 @@ export class RunService {
|
|
|
525
573
|
isRunning(conversationId) {
|
|
526
574
|
return this.activeRuns.has(conversationId);
|
|
527
575
|
}
|
|
576
|
+
/** Waits for this conversation's current producer, without waiting for unrelated sessions. */
|
|
577
|
+
async waitForConversationIdle(conversationId) {
|
|
578
|
+
while (this.activeRuns.has(conversationId)) {
|
|
579
|
+
const completion = this.runCompletions.get(conversationId)?.promise;
|
|
580
|
+
if (completion)
|
|
581
|
+
await completion;
|
|
582
|
+
else
|
|
583
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
528
586
|
/** Returns true while a stop has been requested and the old producer is still unwinding. */
|
|
529
587
|
isStopping(conversationId) {
|
|
530
588
|
// Use the same identity as the slot; a separate conversation-keyed flag can leak across owners.
|
|
@@ -4,6 +4,8 @@ import type { RunService } from "./run-service.js";
|
|
|
4
4
|
import type { SessionLedger } from "../session-ledger.js";
|
|
5
5
|
import type { EventBroadcaster } from "../server-context.js";
|
|
6
6
|
import type { RunStatus } from "../types.js";
|
|
7
|
+
export declare const SESSION_OUTPUT_CHAR_LIMIT: number;
|
|
8
|
+
export declare const sessionResultRelativePath: (conversationId: string) => string;
|
|
7
9
|
export interface SpawnSessionOptions {
|
|
8
10
|
/** The high-level task to be performed by the sibling session. */
|
|
9
11
|
task: string;
|
|
@@ -54,6 +56,9 @@ export interface SessionSummary {
|
|
|
54
56
|
updatedAt: string;
|
|
55
57
|
createdAt?: number;
|
|
56
58
|
lastProgress?: string;
|
|
59
|
+
output?: string;
|
|
60
|
+
outputPath?: string;
|
|
61
|
+
outputTruncated?: boolean;
|
|
57
62
|
}
|
|
58
63
|
/**
|
|
59
64
|
* Orchestrator service coordinating sibling and child conversations.
|
|
@@ -83,11 +88,14 @@ export declare class SessionOrchestratorService {
|
|
|
83
88
|
}): Promise<{
|
|
84
89
|
conversationId: string;
|
|
85
90
|
}>;
|
|
91
|
+
private persistSessionResult;
|
|
86
92
|
/**
|
|
87
93
|
* Returns a high-level summary of a session's progress without returning full transcripts.
|
|
88
94
|
* Uses narrow store reader `store.conversation(id)` to avoid cloning the entire state.
|
|
89
95
|
*/
|
|
90
|
-
status(conversationId: string
|
|
96
|
+
status(conversationId: string, options?: {
|
|
97
|
+
includeOutput?: boolean;
|
|
98
|
+
}): Promise<SessionSummary>;
|
|
91
99
|
/**
|
|
92
100
|
* Lists summaries of all sessions, optionally filtered by parent conversation.
|
|
93
101
|
*/
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { capText } from "../workflow.js";
|
|
4
|
+
export const SESSION_OUTPUT_CHAR_LIMIT = 64 * 1024;
|
|
5
|
+
export const sessionResultRelativePath = (conversationId) => `.super-pi/session-results/${conversationId}.md`;
|
|
1
6
|
/**
|
|
2
7
|
* Orchestrator service coordinating sibling and child conversations.
|
|
3
8
|
*
|
|
@@ -84,22 +89,66 @@ export class SessionOrchestratorService {
|
|
|
84
89
|
status: "running",
|
|
85
90
|
});
|
|
86
91
|
const runPromise = this.runService.enqueueAndRun(conversationId, content);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
const completion = (async () => {
|
|
93
|
+
let failure;
|
|
94
|
+
try {
|
|
95
|
+
await runPromise;
|
|
96
|
+
// A dispatch into an already-running child returns after queueing, not after that child settles.
|
|
97
|
+
await this.runService.waitForConversationIdle(conversationId);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
failure = error;
|
|
101
|
+
await this.runService.waitForConversationIdle(conversationId);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const result = await this.persistSessionResult(conversationId, failure);
|
|
105
|
+
if (result) {
|
|
106
|
+
const current = this.store.conversation(conversationId);
|
|
107
|
+
await this.runService.enqueueSessionResult({
|
|
108
|
+
conversationId,
|
|
109
|
+
parentConversationId: current?.parentConversationId,
|
|
110
|
+
output: result.output,
|
|
111
|
+
resultPath: result.resultPath,
|
|
112
|
+
...(failure !== undefined ? { error: result.output } : {}),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
// Result reporting is best-effort: a missing/archived parent or a workspace I/O problem must not
|
|
118
|
+
// turn an already-finished child turn into a second failure.
|
|
119
|
+
console.warn(`Super Pi: session result callback failed for ${conversationId}:`, error instanceof Error ? error.message : String(error));
|
|
120
|
+
}
|
|
121
|
+
if (failure !== undefined) {
|
|
122
|
+
console.warn(`Super Pi: dispatch failed for conversation ${conversationId}:`, failure instanceof Error ? failure.message : String(failure));
|
|
93
123
|
this.ledger.update(conversationId, { status: "failed" });
|
|
94
|
-
|
|
95
|
-
|
|
124
|
+
throw failure;
|
|
125
|
+
}
|
|
126
|
+
})();
|
|
127
|
+
if (options?.wait)
|
|
128
|
+
await completion;
|
|
129
|
+
else
|
|
130
|
+
void completion.catch(() => undefined);
|
|
96
131
|
return { conversationId };
|
|
97
132
|
}
|
|
133
|
+
async persistSessionResult(conversationId, failure) {
|
|
134
|
+
const conversation = this.store.conversation(conversationId);
|
|
135
|
+
const workspace = conversation ? this.store.workspace(conversation.workspaceId) : undefined;
|
|
136
|
+
if (!conversation || !workspace)
|
|
137
|
+
return undefined;
|
|
138
|
+
const output = failure !== undefined
|
|
139
|
+
? (failure instanceof Error ? failure.message : String(failure))
|
|
140
|
+
: ([...conversation.messages].reverse().find((message) => message.role === "assistant")?.content ?? "");
|
|
141
|
+
const resultPath = sessionResultRelativePath(conversationId);
|
|
142
|
+
const resultDirectory = join(workspace.path, ".super-pi", "session-results");
|
|
143
|
+
await mkdir(resultDirectory, { recursive: true });
|
|
144
|
+
await writeFile(join(resultDirectory, `${conversationId}.md`), output, "utf8");
|
|
145
|
+
return { output, resultPath };
|
|
146
|
+
}
|
|
98
147
|
/**
|
|
99
148
|
* Returns a high-level summary of a session's progress without returning full transcripts.
|
|
100
149
|
* Uses narrow store reader `store.conversation(id)` to avoid cloning the entire state.
|
|
101
150
|
*/
|
|
102
|
-
async status(conversationId) {
|
|
151
|
+
async status(conversationId, options) {
|
|
103
152
|
const conversation = this.store.conversation(conversationId);
|
|
104
153
|
if (!conversation) {
|
|
105
154
|
throw new Error(`找不到会话:${conversationId}`);
|
|
@@ -133,6 +182,24 @@ export class SessionOrchestratorService {
|
|
|
133
182
|
}
|
|
134
183
|
}
|
|
135
184
|
const hasPendingQuestion = this.store.pendingQuestions(conversationId).length > 0;
|
|
185
|
+
let output;
|
|
186
|
+
let outputTruncated;
|
|
187
|
+
const resultPath = sessionResultRelativePath(conversationId);
|
|
188
|
+
if (options?.includeOutput) {
|
|
189
|
+
const workspace = this.store.workspace(conversation.workspaceId);
|
|
190
|
+
if (workspace) {
|
|
191
|
+
try {
|
|
192
|
+
const fullOutput = await readFile(join(workspace.path, ".super-pi", "session-results", `${conversationId}.md`), "utf8");
|
|
193
|
+
const capped = capText(fullOutput, SESSION_OUTPUT_CHAR_LIMIT, "会话输出");
|
|
194
|
+
output = capped.text;
|
|
195
|
+
outputTruncated = capped.truncated;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (error.code !== "ENOENT")
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
136
203
|
// Sync ledger status if present
|
|
137
204
|
if (ledgerEntry && ledgerEntry.status !== conversation.status) {
|
|
138
205
|
this.ledger.update(conversationId, {
|
|
@@ -146,7 +213,7 @@ export class SessionOrchestratorService {
|
|
|
146
213
|
title: conversation.title,
|
|
147
214
|
status: conversation.status,
|
|
148
215
|
origin: ledgerEntry?.origin ?? "user",
|
|
149
|
-
parentConversationId: ledgerEntry?.parentConversationId,
|
|
216
|
+
parentConversationId: conversation.parentConversationId ?? ledgerEntry?.parentConversationId,
|
|
150
217
|
task: ledgerEntry?.task,
|
|
151
218
|
messageCount: messages.length,
|
|
152
219
|
turnCount,
|
|
@@ -158,6 +225,8 @@ export class SessionOrchestratorService {
|
|
|
158
225
|
updatedAt: conversation.updatedAt,
|
|
159
226
|
createdAt: ledgerEntry?.createdAt,
|
|
160
227
|
lastProgress: ledgerEntry?.lastProgress ?? lastAssistantSnippet,
|
|
228
|
+
...(options?.includeOutput ? { outputPath: resultPath } : {}),
|
|
229
|
+
...(output !== undefined ? { output, outputTruncated } : {}),
|
|
161
230
|
};
|
|
162
231
|
}
|
|
163
232
|
/**
|
|
@@ -1472,15 +1472,10 @@ export function createHttpServer(options) {
|
|
|
1472
1472
|
json(503, { error: "编排服务未就绪" });
|
|
1473
1473
|
return;
|
|
1474
1474
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
else if (context.accessToken && !isAuthorized(request, context.accessToken)) {
|
|
1483
|
-
json(401, { error: "未授权" });
|
|
1475
|
+
const callerConversationId = body.callerConversationId ?? body.parentConversationId ?? "";
|
|
1476
|
+
const verdict = runService.verifyPrimaryCapability(callerConversationId, body.capability);
|
|
1477
|
+
if (!verdict.ok) {
|
|
1478
|
+
json(403, { error: verdict.message });
|
|
1484
1479
|
return;
|
|
1485
1480
|
}
|
|
1486
1481
|
const action = (body.action || "").trim();
|
|
@@ -1513,7 +1508,7 @@ export function createHttpServer(options) {
|
|
|
1513
1508
|
if (action === "status") {
|
|
1514
1509
|
if (!body.conversationId)
|
|
1515
1510
|
throw new Error("status requires conversationId");
|
|
1516
|
-
const summary = await service.status(body.conversationId);
|
|
1511
|
+
const summary = await service.status(body.conversationId, { includeOutput: body.includeOutput });
|
|
1517
1512
|
json(200, { ok: true, summary });
|
|
1518
1513
|
return;
|
|
1519
1514
|
}
|
|
@@ -1554,12 +1549,18 @@ export function createHttpServer(options) {
|
|
|
1554
1549
|
return;
|
|
1555
1550
|
}
|
|
1556
1551
|
const conversationId = url.searchParams.get("conversationId");
|
|
1552
|
+
const callerConversationId = url.searchParams.get("callerConversationId") ?? url.searchParams.get("parentId") ?? "";
|
|
1553
|
+
const verdict = runService.verifyPrimaryCapability(callerConversationId, url.searchParams.get("capability"));
|
|
1554
|
+
if (!verdict.ok) {
|
|
1555
|
+
response.writeHead(403, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: verdict.message }));
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1557
1558
|
if (!conversationId) {
|
|
1558
1559
|
response.writeHead(400, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "缺少 conversationId" }));
|
|
1559
1560
|
return;
|
|
1560
1561
|
}
|
|
1561
1562
|
try {
|
|
1562
|
-
const summary = await service.status(conversationId);
|
|
1563
|
+
const summary = await service.status(conversationId, { includeOutput: url.searchParams.get("includeOutput") === "true" });
|
|
1563
1564
|
response.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ ok: true, summary }));
|
|
1564
1565
|
}
|
|
1565
1566
|
catch (error) {
|
|
@@ -1578,6 +1579,12 @@ export function createHttpServer(options) {
|
|
|
1578
1579
|
return;
|
|
1579
1580
|
}
|
|
1580
1581
|
const parentId = url.searchParams.get("parentId") || undefined;
|
|
1582
|
+
const callerConversationId = url.searchParams.get("callerConversationId") ?? parentId ?? "";
|
|
1583
|
+
const verdict = runService.verifyPrimaryCapability(callerConversationId, url.searchParams.get("capability"));
|
|
1584
|
+
if (!verdict.ok) {
|
|
1585
|
+
response.writeHead(403, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: verdict.message }));
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1581
1588
|
try {
|
|
1582
1589
|
const sessions = await service.list(parentId);
|
|
1583
1590
|
response.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ ok: true, sessions }));
|
package/dist/server/runtime.js
CHANGED
|
@@ -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, ORCHESTRATOR_ENDPOINT_ENV, SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, rosterPayload, } from "./task-tool.js";
|
|
24
|
+
import { TASK_CONVERSATION_ENV, PARENT_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";
|
|
@@ -696,7 +696,7 @@ export class PiRpcRuntime {
|
|
|
696
696
|
* its environment once, when it is loaded) and a token minted per turn would be replaced before the
|
|
697
697
|
* second turn of the conversation, leaving the extension holding a value the server had forgotten.
|
|
698
698
|
*/
|
|
699
|
-
async taskExtensionEnv(conversationId) {
|
|
699
|
+
async taskExtensionEnv(conversationId, parentConversationId) {
|
|
700
700
|
const { agents, problems } = await readAgents(this.agents.dir, { mode: "subagent" });
|
|
701
701
|
for (const problem of problems)
|
|
702
702
|
console.warn(`PiRpcRuntime: ${problem.file} is not a usable sub-agent - ${problem.message}`);
|
|
@@ -705,6 +705,7 @@ export class PiRpcRuntime {
|
|
|
705
705
|
[TASK_EXEC_ENDPOINT_ENV]: this.options.execEndpoint ?? "",
|
|
706
706
|
[TASK_ROSTER_ENV]: rosterPayload(agents),
|
|
707
707
|
[TASK_CONVERSATION_ENV]: conversationId,
|
|
708
|
+
[PARENT_CONVERSATION_ENV]: parentConversationId ?? "",
|
|
708
709
|
[TASK_DEPTH_ENV]: "0",
|
|
709
710
|
[TODO_ENDPOINT_ENV]: this.options.todoEndpoint ?? "",
|
|
710
711
|
[SCHEDULED_TASK_ENDPOINT_ENV]: this.options.scheduledTaskEndpoint ?? "",
|
|
@@ -737,9 +738,9 @@ export class PiRpcRuntime {
|
|
|
737
738
|
* here. A session lives for many turns, so its credential is minted when the session is created (see
|
|
738
739
|
* `PiSessionPool.acquire`) and the server separately requires a running turn for it to be usable.
|
|
739
740
|
*/
|
|
740
|
-
async oneShotExtensionEnv(conversationId, capability) {
|
|
741
|
+
async oneShotExtensionEnv(conversationId, capability, parentConversationId) {
|
|
741
742
|
return {
|
|
742
|
-
...(await this.taskExtensionEnv(conversationId)),
|
|
743
|
+
...(await this.taskExtensionEnv(conversationId, parentConversationId)),
|
|
743
744
|
[TASK_CAPABILITY_ENV]: capability,
|
|
744
745
|
SUPER_PI_TOOLS_CAPABILITY: capability,
|
|
745
746
|
};
|
|
@@ -800,7 +801,7 @@ export class PiRpcRuntime {
|
|
|
800
801
|
...(systemPromptFile ? { systemPromptFile } : {}),
|
|
801
802
|
...(agent?.promptMode ? { systemPromptMode: agent.promptMode } : {}),
|
|
802
803
|
extensionPaths: superPiExtensionPaths(repositoryRootFrom()),
|
|
803
|
-
extensionEnv: await this.taskExtensionEnv(conversation.id),
|
|
804
|
+
extensionEnv: await this.taskExtensionEnv(conversation.id, conversation.parentConversationId),
|
|
804
805
|
...(conversation.model ? { model: { provider: conversation.model.provider, modelId: conversation.model.modelId } } : {}),
|
|
805
806
|
...(conversation.thinkingLevel ? { thinkingLevel: conversation.thinkingLevel } : {}),
|
|
806
807
|
};
|
|
@@ -1476,7 +1477,7 @@ export class PiRpcRuntime {
|
|
|
1476
1477
|
// every turn would silently replace the child's environment with whatever THIS process has at
|
|
1477
1478
|
// spawn time. Inheriting is what an omitted `env` already does, so the two paths differ in nothing
|
|
1478
1479
|
// but the four documented additions.
|
|
1479
|
-
...((extensionArgs.length || memoryEnabled || knowledgeEnabled || documentsEnabled || webEnabled || feishuEnabled) ? { env: { ...process.env, ...(oneShotCapability ? await this.oneShotExtensionEnv(conversation.id, oneShotCapability) : {}), ...(memoryEnabled ? {
|
|
1480
|
+
...((extensionArgs.length || memoryEnabled || knowledgeEnabled || documentsEnabled || webEnabled || feishuEnabled) ? { env: { ...process.env, ...(oneShotCapability ? await this.oneShotExtensionEnv(conversation.id, oneShotCapability, conversation.parentConversationId) : {}), ...(memoryEnabled ? {
|
|
1480
1481
|
SUPER_PI_MEMORY_ENDPOINT: process.env.SUPER_PI_MEMORY_ENDPOINT,
|
|
1481
1482
|
SUPER_PI_MEMORY_TOKEN: memoryToken,
|
|
1482
1483
|
SUPER_PI_MEMORY_REVIEW: memoryReview ? "1" : "0",
|
package/dist/server/store.d.ts
CHANGED
|
@@ -266,10 +266,11 @@ export declare class Store {
|
|
|
266
266
|
conversationId?: string;
|
|
267
267
|
}): AppState;
|
|
268
268
|
createConversation(workspaceId: string, configuration?: {
|
|
269
|
-
agentId?: string;
|
|
269
|
+
agentId?: string | null;
|
|
270
270
|
model?: ModelSelection | null;
|
|
271
271
|
thinkingLevel?: string | null;
|
|
272
272
|
modelNotice?: string;
|
|
273
|
+
agentNotice?: string;
|
|
273
274
|
parentConversationId?: string;
|
|
274
275
|
origin?: "user" | "orchestrator";
|
|
275
276
|
}): Promise<Conversation>;
|
|
@@ -341,6 +342,16 @@ export declare class Store {
|
|
|
341
342
|
/** Restoring deletes the key rather than setting it to null, so a fully restored conversation serializes
|
|
342
343
|
* byte-identically to one that was never archived. `delete` is legal here because the field is optional. */
|
|
343
344
|
unarchiveConversation(conversationId: string): Promise<void>;
|
|
345
|
+
/**
|
|
346
|
+
* Archives one whole tree as one state write. Only a conversation with no existing parent may
|
|
347
|
+
* start it; an archived parent is still a parent, so its active descendants cannot be archived
|
|
348
|
+
* independently. All running checks happen before the first mutation, making rejection all-or-none.
|
|
349
|
+
*/
|
|
350
|
+
archiveConversationTree(conversationId: string): Promise<void>;
|
|
351
|
+
/** Restores one archived tree in one write; active descendants are already restored by definition. */
|
|
352
|
+
unarchiveConversationTree(conversationId: string): Promise<void>;
|
|
353
|
+
/** Pre-order traversal with cycle protection; archived nodes are traversed, not treated as leaves. */
|
|
354
|
+
private conversationSubtree;
|
|
344
355
|
/**
|
|
345
356
|
* Rename edits the title and NOTHING else. In particular `updatedAt` is deliberately not touched,
|
|
346
357
|
* for exactly the reason archive is: `updatedAt` is the sidebar's recency key, so bumping it would
|
package/dist/server/store.js
CHANGED
|
@@ -2,6 +2,7 @@ import { rename } from "node:fs/promises";
|
|
|
2
2
|
import { JsonStateRepository, defaultJsonStatePath } from "./persistence/json-state-repository.js";
|
|
3
3
|
import { normalizeTodos } from "./task-tool.js";
|
|
4
4
|
import { TOOL_ARG_CAP } from "./tool-calls.js";
|
|
5
|
+
import { serverTimeZone } from "./time-zone.js";
|
|
5
6
|
function id(prefix) { return `${prefix}_${crypto.randomUUID()}`; }
|
|
6
7
|
/** Mints the id a forked conversation will have, BEFORE its Pi transcript exists.
|
|
7
8
|
*
|
|
@@ -1031,7 +1032,7 @@ export class Store {
|
|
|
1031
1032
|
// Legacy sockets still receive complete visible transcripts. Project before cloning so a closed
|
|
1032
1033
|
// workspace's retained history is never copied just to discard it afterwards.
|
|
1033
1034
|
if (scope === undefined)
|
|
1034
|
-
return structuredClone({ ...this.state, workspaces, conversations: visible });
|
|
1035
|
+
return structuredClone({ ...this.state, workspaces, conversations: visible, serverTimeZone: serverTimeZone() });
|
|
1035
1036
|
// A focused socket keeps every sidebar row, but owns only the selected transcript. An empty scope
|
|
1036
1037
|
// deliberately loads none; a missing/closed selection must not expose retained workspace history.
|
|
1037
1038
|
const selectedId = visible.find((item) => item.id === scope.conversationId)?.id;
|
|
@@ -1045,6 +1046,11 @@ export class Store {
|
|
|
1045
1046
|
...this.state,
|
|
1046
1047
|
workspaces,
|
|
1047
1048
|
conversations,
|
|
1049
|
+
// THE CLOCK THE SERVER SCHEDULES BY, NAMED. The scheduled-task panel renders 「每天 09:00」 and
|
|
1050
|
+
// 「下次」 from two different sources - a configured string and an epoch millisecond - and a
|
|
1051
|
+
// browser on another machine renders the epoch in ITS timezone. Without this field the two
|
|
1052
|
+
// lines in one row mean two different wall clocks and nothing says so.
|
|
1053
|
+
serverTimeZone: serverTimeZone(),
|
|
1048
1054
|
subagentRuns: this.state.subagentRuns.filter((run) => run.conversationId === selectedId),
|
|
1049
1055
|
...(this.state.workflows ? { workflows: this.state.workflows.filter((run) => run.conversationId === selectedId) } : {}),
|
|
1050
1056
|
...(this.state.todosByConversation ? { todosByConversation: selectedId && this.state.todosByConversation[selectedId]
|
|
@@ -1060,7 +1066,7 @@ export class Store {
|
|
|
1060
1066
|
// here and never consulted again: the session picker edits the conversation's own fields, so an
|
|
1061
1067
|
// existing conversation is untouched by later workspace settings, and a workspace edit never
|
|
1062
1068
|
// rewrites history. Field by field, never a spread, for the same reason setConversationModel
|
|
1063
|
-
// narrows:
|
|
1069
|
+
// narrows: every inherited field has an explicit birth override, including null for Pi's default.
|
|
1064
1070
|
if (workspace.defaultModel)
|
|
1065
1071
|
conversation.model = { provider: workspace.defaultModel.provider, modelId: workspace.defaultModel.modelId, label: workspace.defaultModel.label };
|
|
1066
1072
|
if (workspace.defaultThinkingLevel)
|
|
@@ -1072,6 +1078,8 @@ export class Store {
|
|
|
1072
1078
|
// rewritten: it describes how this conversation came to be, not what is true right now.
|
|
1073
1079
|
if (configuration.modelNotice !== undefined)
|
|
1074
1080
|
conversation.modelNotice = configuration.modelNotice;
|
|
1081
|
+
if (configuration.agentNotice !== undefined)
|
|
1082
|
+
conversation.agentNotice = configuration.agentNotice;
|
|
1075
1083
|
if (configuration.model !== undefined) {
|
|
1076
1084
|
if (configuration.model)
|
|
1077
1085
|
conversation.model = { ...configuration.model };
|
|
@@ -1084,8 +1092,12 @@ export class Store {
|
|
|
1084
1092
|
else
|
|
1085
1093
|
delete conversation.thinkingLevel;
|
|
1086
1094
|
}
|
|
1087
|
-
if (configuration.agentId)
|
|
1088
|
-
|
|
1095
|
+
if (configuration.agentId !== undefined) {
|
|
1096
|
+
if (configuration.agentId)
|
|
1097
|
+
conversation.agentId = configuration.agentId;
|
|
1098
|
+
else
|
|
1099
|
+
delete conversation.agentId;
|
|
1100
|
+
}
|
|
1089
1101
|
// THE SESSION TREE'S EDGE, WRITTEN AT BIRTH. Both fields describe how this conversation CAME TO
|
|
1090
1102
|
// BE, exactly like modelNotice above, so they are set once here and never rewritten: the tree
|
|
1091
1103
|
// the sidebar draws after a restart is the tree that was true when each session was opened.
|
|
@@ -1232,6 +1244,67 @@ export class Store {
|
|
|
1232
1244
|
delete conversation.archivedAt;
|
|
1233
1245
|
await this.persistMetadata(conversationId);
|
|
1234
1246
|
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Archives one whole tree as one state write. Only a conversation with no existing parent may
|
|
1249
|
+
* start it; an archived parent is still a parent, so its active descendants cannot be archived
|
|
1250
|
+
* independently. All running checks happen before the first mutation, making rejection all-or-none.
|
|
1251
|
+
*/
|
|
1252
|
+
async archiveConversationTree(conversationId) {
|
|
1253
|
+
const root = this.requireConversation(conversationId);
|
|
1254
|
+
if (root.parentConversationId && this.state.conversations.some(item => item.id === root.parentConversationId)) {
|
|
1255
|
+
throw new Error("子会话不能独立归档,请从根会话归档整棵树");
|
|
1256
|
+
}
|
|
1257
|
+
if (root.archivedAt)
|
|
1258
|
+
return;
|
|
1259
|
+
const active = this.conversationSubtree(root).filter(item => !item.archivedAt);
|
|
1260
|
+
const running = active.find(item => item.status === "running");
|
|
1261
|
+
if (running)
|
|
1262
|
+
throw new Error(`会话「${running.title}」正在运行,无法归档整棵树,请先停止运行`);
|
|
1263
|
+
const archivedAt = now();
|
|
1264
|
+
for (const conversation of active)
|
|
1265
|
+
conversation.archivedAt = archivedAt;
|
|
1266
|
+
await this.persist();
|
|
1267
|
+
}
|
|
1268
|
+
/** Restores one archived tree in one write; active descendants are already restored by definition. */
|
|
1269
|
+
async unarchiveConversationTree(conversationId) {
|
|
1270
|
+
const root = this.requireConversation(conversationId);
|
|
1271
|
+
const parent = root.parentConversationId
|
|
1272
|
+
? this.state.conversations.find(item => item.id === root.parentConversationId)
|
|
1273
|
+
: undefined;
|
|
1274
|
+
if (parent?.archivedAt)
|
|
1275
|
+
throw new Error("归档树中的子会话不能独立恢复,请从归档树的根会话恢复整棵树");
|
|
1276
|
+
if (!root.archivedAt)
|
|
1277
|
+
return;
|
|
1278
|
+
const archived = this.conversationSubtree(root).filter(item => item.archivedAt);
|
|
1279
|
+
for (const conversation of archived)
|
|
1280
|
+
delete conversation.archivedAt;
|
|
1281
|
+
await this.persist();
|
|
1282
|
+
}
|
|
1283
|
+
/** Pre-order traversal with cycle protection; archived nodes are traversed, not treated as leaves. */
|
|
1284
|
+
conversationSubtree(root) {
|
|
1285
|
+
const byParent = new Map();
|
|
1286
|
+
for (const conversation of this.state.conversations) {
|
|
1287
|
+
if (conversation.workspaceId !== root.workspaceId || !conversation.parentConversationId)
|
|
1288
|
+
continue;
|
|
1289
|
+
const siblings = byParent.get(conversation.parentConversationId);
|
|
1290
|
+
if (siblings)
|
|
1291
|
+
siblings.push(conversation);
|
|
1292
|
+
else
|
|
1293
|
+
byParent.set(conversation.parentConversationId, [conversation]);
|
|
1294
|
+
}
|
|
1295
|
+
const result = [];
|
|
1296
|
+
const visited = new Set();
|
|
1297
|
+
const walk = (conversation) => {
|
|
1298
|
+
if (visited.has(conversation.id))
|
|
1299
|
+
return;
|
|
1300
|
+
visited.add(conversation.id);
|
|
1301
|
+
result.push(conversation);
|
|
1302
|
+
for (const child of byParent.get(conversation.id) ?? [])
|
|
1303
|
+
walk(child);
|
|
1304
|
+
};
|
|
1305
|
+
walk(root);
|
|
1306
|
+
return result;
|
|
1307
|
+
}
|
|
1235
1308
|
/**
|
|
1236
1309
|
* Rename edits the title and NOTHING else. In particular `updatedAt` is deliberately not touched,
|
|
1237
1310
|
* for exactly the reason archive is: `updatedAt` is the sidebar's recency key, so bumping it would
|