@evandu/super-pi 0.1.142 → 0.1.144
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 -7
- package/dist/client/assets/{CodeMirrorEditor-iLUdQy9q.js → CodeMirrorEditor-DfQHX1Hy.js} +2 -2
- package/dist/client/assets/{dist-Bt8G7e7e.js → dist-B8oH6dsc.js} +1 -1
- package/dist/client/assets/{dist-DbLEngKN.js → dist-BCLhQXYY.js} +1 -1
- package/dist/client/assets/{dist-DBAoCOnn.js → dist-BEbEv2KM.js} +1 -1
- package/dist/client/assets/{dist-Dxq1s4j3.js → dist-BGBinEie.js} +1 -1
- package/dist/client/assets/{dist-CWBm3q4a.js → dist-BRjyQs_L.js} +1 -1
- package/dist/client/assets/{dist-JuLMNOUt.js → dist-BS-FBUKW.js} +1 -1
- package/dist/client/assets/{dist-Cfa_e2VZ.js → dist-BsMikSTM.js} +1 -1
- package/dist/client/assets/{dist-0dCNvYEE.js → dist-BuPimjY-.js} +1 -1
- package/dist/client/assets/{dist-rIYPNzbY.js → dist-C5BQsmB7.js} +1 -1
- package/dist/client/assets/{dist-B8FbKhnf.js → dist-CDtFD-PW.js} +1 -1
- package/dist/client/assets/{dist-ic-MopV2.js → dist-CgQ7zaWY.js} +1 -1
- package/dist/client/assets/{dist-F634gv7V.js → dist-Cu3B0JQX.js} +1 -1
- package/dist/client/assets/{dist-Bj2C-QqM.js → dist-Cw_KCDH9.js} +1 -1
- package/dist/client/assets/{dist-BA6EsWzo.js → dist-D7aAMRTb.js} +1 -1
- package/dist/client/assets/{dist-B4JqBNQI.js → dist-DKkMk3XX.js} +1 -1
- package/dist/client/assets/{dist-CoTJns5x.js → dist-DOvzruAQ.js} +1 -1
- package/dist/client/assets/{dist-Nt-BWmBB.js → dist-Dnru8_uX.js} +1 -1
- package/dist/client/assets/{dist-DHyWJ9kA.js → dist-ISoxXlmW.js} +1 -1
- package/dist/client/assets/{dist-DEUl3Yqk.js → dist-KQJCVwxy.js} +1 -1
- package/dist/client/assets/{dist-Dq6y_nK3.js → dist-LRNzDUVb.js} +1 -1
- package/dist/client/assets/{dist-CtCiGxne.js → dist-SzZEAk24.js} +1 -1
- package/dist/client/assets/{dist-Df9zcCXs.js → dist-qAUj_2Ue.js} +1 -1
- package/dist/client/assets/{index-CX5gWDve.js → index-BLWr70ej.js} +22 -22
- package/dist/client/assets/{index-DMhigTiY.css → index-Xaui0uUI.css} +1 -1
- package/dist/client/index.html +2 -2
- 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.js +7 -1
- 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 +9 -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 +331 -325
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-BLWr70ej.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Xaui0uUI.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -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.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]
|
|
@@ -71,6 +71,8 @@ export declare const TASK_EXEC_ENDPOINT_ENV = "SUPER_PI_EXEC_ENDPOINT";
|
|
|
71
71
|
export declare const TASK_ROSTER_ENV = "SUPER_PI_TASK_ROSTER";
|
|
72
72
|
/** The conversation the dispatch belongs to. The server resolves the workspace from this itself. */
|
|
73
73
|
export declare const TASK_CONVERSATION_ENV = "SUPER_PI_TASK_CONVERSATION";
|
|
74
|
+
/** The conversation that spawned this one. Empty means a top-level conversation. */
|
|
75
|
+
export declare const PARENT_CONVERSATION_ENV = "SUPER_PI_PARENT_CONVERSATION";
|
|
74
76
|
/** How many task hops deep this process already is. A primary is `0`. See `DEFAULT_SUBAGENT_DEPTH`. */
|
|
75
77
|
export declare const TASK_DEPTH_ENV = "SUPER_PI_TASK_DEPTH";
|
|
76
78
|
/**
|
|
@@ -255,9 +257,9 @@ export declare const SESSION_STATUS_TOOL_NAME = "session_status";
|
|
|
255
257
|
export declare const SESSION_DISPATCH_TOOL_NAME = "session_dispatch";
|
|
256
258
|
export declare const SESSION_STOP_TOOL_NAME = "session_stop";
|
|
257
259
|
/** Model-facing description for session_spawn. */
|
|
258
|
-
export declare const SESSION_SPAWN_DESCRIPTION = "Create a new sibling conversation in the current workspace and immediately dispatch its initial task.\n\nDIFFERENCE FROM THE 'task' TOOL:\n- The 'task' tool delegates to a transient, one-shot subagent process (scout, critic, patcher) that runs once, returns its final text, and terminates. It has no conversation history and does not appear in the workspace sidebar.\n- 'session_spawn' creates a real, persistent sibling CONVERSATION in Super Pi. It appears in the sidebar, retains multi-turn message history, can receive follow-up messages via session_dispatch, can be inspected via session_status, and can be stopped via session_stop. The user can also switch to it in the Web UI.\n\nWHEN TO USE THIS:\n- Long-running, multi-phase tracks of work that require ongoing context, multiple turns of dialogue, or independent user visibility.\n- Parallel work streams (e.g., Track A: backend refactor, Track B: frontend component update) where both need full conversational capabilities.\n- When the user asks you to act as an orchestrator/manager who delegates to multiple sessions.\n\nWHEN NOT TO USE THIS:\n- For one-off, focused delegation with a single answer: use 'task' (with scout/critic/patcher). It is faster, cheaper, and returns directly to your transcript.\n- For running shell commands or build watchers: use 'run_async'.\n- For a linear multi-step subagent pipeline: use 'workflow'.\n\nUsage notes:\n1. Spawning is ASYNCHRONOUS by default: it creates the conversation, enqueues the initial task, and returns the new conversationId immediately. The sibling session runs in the background.\n2. The initial task should provide clear context and goals since the new session starts with a fresh conversation history.\n3. Use session_status with the returned conversationId to check progress, or session_dispatch to send further messages.";
|
|
260
|
+
export declare const SESSION_SPAWN_DESCRIPTION = "Create a new sibling conversation in the current workspace and immediately dispatch its initial task.\n\nDIFFERENCE FROM THE 'task' TOOL:\n- The 'task' tool delegates to a transient, one-shot subagent process (scout, critic, patcher) that runs once, returns its final text, and terminates. It has no conversation history and does not appear in the workspace sidebar.\n- 'session_spawn' creates a real, persistent sibling CONVERSATION in Super Pi. It appears in the sidebar, retains multi-turn message history, can receive follow-up messages via session_dispatch, can be inspected via session_status, and can be stopped via session_stop. The user can also switch to it in the Web UI.\n\nWHEN TO USE THIS:\n- Long-running, multi-phase tracks of work that require ongoing context, multiple turns of dialogue, or independent user visibility.\n- Parallel work streams (e.g., Track A: backend refactor, Track B: frontend component update) where both need full conversational capabilities.\n- When the user asks you to act as an orchestrator/manager who delegates to multiple sessions.\n\nWHEN NOT TO USE THIS:\n- For one-off, focused delegation with a single answer: use 'task' (with scout/critic/patcher). It is faster, cheaper, and returns directly to your transcript.\n- For running shell commands or build watchers: use 'run_async'.\n- For a linear multi-step subagent pipeline: use 'workflow'.\n\nUsage notes:\n1. Spawning is ASYNCHRONOUS by default: it creates the conversation, enqueues the initial task, and returns the new conversationId immediately. The sibling session runs in the background.\n2. The initial task should provide clear context and goals since the new session starts with a fresh conversation history.\n3. Use session_status with the returned conversationId to check progress, or session_dispatch to send further messages.\n4. The new session reports completion to its DIRECT parent automatically. 'parentConversationId' optionally selects that parent; by default it is the calling session.";
|
|
259
261
|
/** Model-facing description for session_status. */
|
|
260
|
-
export declare const SESSION_STATUS_DESCRIPTION = "Inspect the progress, run state, and latest activity of sibling conversations created by the orchestrator.\n\nWHEN TO USE THIS:\n- To check whether a spawned sibling conversation is still running, has finished (idle), is stopped, or encountered an error.\n- To read the latest assistant progress snippet without bloating your context with the session's entire raw message transcript.\n- To list all sibling conversations under your coordination when conversationId is omitted.\n\nUsage notes:\n1. When 'conversationId' is provided: returns a compact summary for that specific session, including its run status ('idle', 'running', 'waiting', 'stopped', 'failed'), messageCount, turnCount, lastAssistantSnippet, lastUserSnippet, activeTool, and error (if any).\n2. When 'conversationId' is omitted: returns a summary list of
|
|
262
|
+
export declare const SESSION_STATUS_DESCRIPTION = "Inspect the progress, run state, and latest activity of sibling conversations created by the orchestrator.\n\nWHEN TO USE THIS:\n- To check whether a spawned sibling conversation is still running, has finished (idle), is stopped, or encountered an error.\n- To read the latest assistant progress snippet without bloating your context with the session's entire raw message transcript.\n- To list all sibling conversations under your coordination when conversationId is omitted.\n\nUsage notes:\n1. When 'conversationId' is provided: returns a compact summary for that specific session, including its run status ('idle', 'running', 'waiting', 'stopped', 'failed'), messageCount, turnCount, lastAssistantSnippet, lastUserSnippet, activeTool, and error (if any).\n2. When 'conversationId' is omitted: returns a summary list of sibling conversations whose direct parent is 'parentConversationId' (the calling session by default).\n3. By default it never returns full transcripts; concise summaries keep your context window lean. Set 'includeOutput' with a conversationId to read the final output from its result file, capped to the last 64Ki characters with an explicit truncation notice.";
|
|
261
263
|
/** Model-facing description for session_dispatch. */
|
|
262
264
|
export declare const SESSION_DISPATCH_DESCRIPTION = "Send a message, follow-up instructions, or steering guidance to an existing sibling conversation.\n\nWHEN TO USE THIS:\n- To follow up with a sibling session after reading its progress with session_status.\n- To give feedback, provide additional requirements, or answer a question from a sibling session.\n- To kick off a new turn on an idle or stopped sibling session.\n\nUsage notes:\n1. 'conversationId' is required and must identify an existing sibling conversation.\n2. 'content' is the text instruction sent to that session, just as if typed by a user in that session's chat.\n3. Dispatch is ASYNCHRONOUS by default: it enqueues the message into the session's queue and returns immediately.\n4. If the sibling session is currently busy running a turn, the dispatched message is placed in its waiting queue and processed when the active turn completes.";
|
|
263
265
|
/** Model-facing description for session_stop. */
|
package/dist/server/task-tool.js
CHANGED
|
@@ -70,6 +70,8 @@ export const TASK_EXEC_ENDPOINT_ENV = "SUPER_PI_EXEC_ENDPOINT";
|
|
|
70
70
|
export const TASK_ROSTER_ENV = "SUPER_PI_TASK_ROSTER";
|
|
71
71
|
/** The conversation the dispatch belongs to. The server resolves the workspace from this itself. */
|
|
72
72
|
export const TASK_CONVERSATION_ENV = "SUPER_PI_TASK_CONVERSATION";
|
|
73
|
+
/** The conversation that spawned this one. Empty means a top-level conversation. */
|
|
74
|
+
export const PARENT_CONVERSATION_ENV = "SUPER_PI_PARENT_CONVERSATION";
|
|
73
75
|
/** How many task hops deep this process already is. A primary is `0`. See `DEFAULT_SUBAGENT_DEPTH`. */
|
|
74
76
|
export const TASK_DEPTH_ENV = "SUPER_PI_TASK_DEPTH";
|
|
75
77
|
/**
|
|
@@ -379,7 +381,8 @@ WHEN NOT TO USE THIS:
|
|
|
379
381
|
Usage notes:
|
|
380
382
|
1. Spawning is ASYNCHRONOUS by default: it creates the conversation, enqueues the initial task, and returns the new conversationId immediately. The sibling session runs in the background.
|
|
381
383
|
2. The initial task should provide clear context and goals since the new session starts with a fresh conversation history.
|
|
382
|
-
3. Use session_status with the returned conversationId to check progress, or session_dispatch to send further messages
|
|
384
|
+
3. Use session_status with the returned conversationId to check progress, or session_dispatch to send further messages.
|
|
385
|
+
4. The new session reports completion to its DIRECT parent automatically. 'parentConversationId' optionally selects that parent; by default it is the calling session.`;
|
|
383
386
|
/** Model-facing description for session_status. */
|
|
384
387
|
export const SESSION_STATUS_DESCRIPTION = `Inspect the progress, run state, and latest activity of sibling conversations created by the orchestrator.
|
|
385
388
|
|
|
@@ -390,8 +393,8 @@ WHEN TO USE THIS:
|
|
|
390
393
|
|
|
391
394
|
Usage notes:
|
|
392
395
|
1. When 'conversationId' is provided: returns a compact summary for that specific session, including its run status ('idle', 'running', 'waiting', 'stopped', 'failed'), messageCount, turnCount, lastAssistantSnippet, lastUserSnippet, activeTool, and error (if any).
|
|
393
|
-
2. When 'conversationId' is omitted: returns a summary list of
|
|
394
|
-
3.
|
|
396
|
+
2. When 'conversationId' is omitted: returns a summary list of sibling conversations whose direct parent is 'parentConversationId' (the calling session by default).
|
|
397
|
+
3. By default it never returns full transcripts; concise summaries keep your context window lean. Set 'includeOutput' with a conversationId to read the final output from its result file, capped to the last 64Ki characters with an explicit truncation notice.`;
|
|
395
398
|
/** Model-facing description for session_dispatch. */
|
|
396
399
|
export const SESSION_DISPATCH_DESCRIPTION = `Send a message, follow-up instructions, or steering guidance to an existing sibling conversation.
|
|
397
400
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CLOCK THIS PROCESS SCHEDULES BY.
|
|
3
|
+
*
|
|
4
|
+
* A scheduled task's 「每天 09:00」 is a WALL CLOCK, and the wall clock it means is the one this
|
|
5
|
+
* process runs on - the machine holding the server. The browser that reads the result may be on a
|
|
6
|
+
* different clock entirely, so a moment computed here and rendered there says one thing in one place
|
|
7
|
+
* and another thing in the other.
|
|
8
|
+
*
|
|
9
|
+
* The name is the IANA zone rather than an offset, because an offset is ambiguous twice a year: it
|
|
10
|
+
* does not say whether 「09:00」 is the summer or the winter offset, and a task that drifts by an
|
|
11
|
+
* hour twice a year is the kind of bug nobody reports.
|
|
12
|
+
*/
|
|
13
|
+
export declare function serverTimeZone(): string;
|
|
14
|
+
/**
|
|
15
|
+
* The zone's short name for the moment, the way a person would say it: 「GMT+8」, 「UTC」.
|
|
16
|
+
*
|
|
17
|
+
* Used in the panel next to a time, so the reader can tell WHICH clock they are looking at without
|
|
18
|
+
* having to know what 「Asia/Shanghai」 means.
|
|
19
|
+
*/
|
|
20
|
+
export declare function timeZoneLabel(at?: number): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CLOCK THIS PROCESS SCHEDULES BY.
|
|
3
|
+
*
|
|
4
|
+
* A scheduled task's 「每天 09:00」 is a WALL CLOCK, and the wall clock it means is the one this
|
|
5
|
+
* process runs on - the machine holding the server. The browser that reads the result may be on a
|
|
6
|
+
* different clock entirely, so a moment computed here and rendered there says one thing in one place
|
|
7
|
+
* and another thing in the other.
|
|
8
|
+
*
|
|
9
|
+
* The name is the IANA zone rather than an offset, because an offset is ambiguous twice a year: it
|
|
10
|
+
* does not say whether 「09:00」 is the summer or the winter offset, and a task that drifts by an
|
|
11
|
+
* hour twice a year is the kind of bug nobody reports.
|
|
12
|
+
*/
|
|
13
|
+
export function serverTimeZone() {
|
|
14
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The zone's short name for the moment, the way a person would say it: 「GMT+8」, 「UTC」.
|
|
18
|
+
*
|
|
19
|
+
* Used in the panel next to a time, so the reader can tell WHICH clock they are looking at without
|
|
20
|
+
* having to know what 「Asia/Shanghai」 means.
|
|
21
|
+
*/
|
|
22
|
+
export function timeZoneLabel(at = Date.now()) {
|
|
23
|
+
const zone = serverTimeZone();
|
|
24
|
+
try {
|
|
25
|
+
const parts = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: "shortOffset" }).formatToParts(at);
|
|
26
|
+
const name = parts.find((part) => part.type === "timeZoneName")?.value;
|
|
27
|
+
return name ?? zone;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return zone;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/server/types.d.ts
CHANGED
|
@@ -888,6 +888,12 @@ export type AppState = {
|
|
|
888
888
|
* "nothing collapsed", which is the correct reading of a preference that did not exist yet.
|
|
889
889
|
*/
|
|
890
890
|
collapsedConversationIds?: string[];
|
|
891
|
+
/**
|
|
892
|
+
* THE CLOCK THIS SERVER SCHEDULES BY. A scheduled task's 「每天 09:00」 is the SERVER's wall clock,
|
|
893
|
+
* and the panel has to say so, because the browser rendering 「下次」 may be on a different clock
|
|
894
|
+
* entirely. Without this the two lines in one row quietly mean two different things.
|
|
895
|
+
*/
|
|
896
|
+
serverTimeZone?: string;
|
|
891
897
|
};
|
|
892
898
|
export type TodoItem = {
|
|
893
899
|
id?: string;
|
|
@@ -917,6 +923,9 @@ export type OrchestratorSessionSummary = {
|
|
|
917
923
|
updatedAt: string;
|
|
918
924
|
createdAt?: number;
|
|
919
925
|
lastProgress?: string;
|
|
926
|
+
output?: string;
|
|
927
|
+
outputPath?: string;
|
|
928
|
+
outputTruncated?: boolean;
|
|
920
929
|
};
|
|
921
930
|
/**
|
|
922
931
|
* What Pi reports about ITS OWN queue, forwarded to the browser as it arrives.
|