@evandu/super-pi 0.1.146 → 0.1.148

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 (64) hide show
  1. package/agents/super-pi-task/core.ts +223 -2
  2. package/agents/super-pi-task/index.ts +50 -0
  3. package/dist/.super-pi-check-inputs.json +2 -2
  4. package/dist/client/.super-pi-client-sources.json +8 -8
  5. package/dist/client/assets/{CodeMirrorEditor-CEwBCVM6.js → CodeMirrorEditor-DQCjEnxG.js} +2 -2
  6. package/dist/client/assets/{dist-DC1IocrS.js → dist-1Innu17E.js} +1 -1
  7. package/dist/client/assets/{dist-CSCUT1PQ.js → dist-1lynFOc5.js} +1 -1
  8. package/dist/client/assets/{dist-Do52MRwm.js → dist-B1qLZVMK.js} +1 -1
  9. package/dist/client/assets/{dist-CAOcjKAQ.js → dist-B4pz_xdd.js} +1 -1
  10. package/dist/client/assets/{dist-B0RXZeXQ.js → dist-B6KwDpeI.js} +1 -1
  11. package/dist/client/assets/{dist-DFMtP3wG.js → dist-BJVI1Unv.js} +1 -1
  12. package/dist/client/assets/{dist-oLq6HXLG.js → dist-BWvzD8Pe.js} +1 -1
  13. package/dist/client/assets/{dist-9F1wL-T5.js → dist-BayWssd0.js} +1 -1
  14. package/dist/client/assets/{dist-DBUqwiWG.js → dist-Bk_pkBVH.js} +1 -1
  15. package/dist/client/assets/{dist-CbVf2Cjn.js → dist-BpeClAv0.js} +1 -1
  16. package/dist/client/assets/{dist-DftD4ibj.js → dist-BsmJg5PX.js} +1 -1
  17. package/dist/client/assets/{dist-CUB_Chdh.js → dist-Bx7s4rni.js} +1 -1
  18. package/dist/client/assets/{dist-Bh8Q7Y_n.js → dist-CDBKM9tL.js} +1 -1
  19. package/dist/client/assets/{dist-BoyICDlL.js → dist-CGzXCzCG.js} +1 -1
  20. package/dist/client/assets/{dist-BCNaDgNW.js → dist-CRxCwn-Z.js} +1 -1
  21. package/dist/client/assets/{dist-76ArZ5M4.js → dist-CTDgUnBG.js} +1 -1
  22. package/dist/client/assets/{dist-BXBSIuTD.js → dist-D1VfMWmf.js} +1 -1
  23. package/dist/client/assets/{dist-DN0GlnnZ.js → dist-DTZLNEci.js} +1 -1
  24. package/dist/client/assets/{dist-JvQIIdqB.js → dist-DuUi29km.js} +1 -1
  25. package/dist/client/assets/{dist-CHt8PX8R.js → dist-F3S0t1oM.js} +1 -1
  26. package/dist/client/assets/{dist-BuR5Iagd.js → dist-QZ-FAVJV.js} +1 -1
  27. package/dist/client/assets/{dist-Bx6ZWYUW.js → dist-u0nBg0fZ.js} +1 -1
  28. package/dist/client/assets/index-BMpqviwU.css +1 -0
  29. package/dist/client/assets/{index-E7yaLa2C.js → index-Dzyp_7sf.js} +43 -43
  30. package/dist/client/index.html +2 -2
  31. package/dist/server/active-memory.d.ts +17 -0
  32. package/dist/server/active-memory.js +39 -0
  33. package/dist/server/agents.d.ts +1 -1
  34. package/dist/server/agents.js +1 -1
  35. package/dist/server/application/conversation-service.js +6 -0
  36. package/dist/server/application/run-service.d.ts +16 -0
  37. package/dist/server/application/run-service.js +175 -26
  38. package/dist/server/application/session-orchestrator-service.d.ts +103 -1
  39. package/dist/server/application/session-orchestrator-service.js +308 -8
  40. package/dist/server/application/session-watchdog.d.ts +159 -0
  41. package/dist/server/application/session-watchdog.js +244 -0
  42. package/dist/server/http-server.js +136 -76
  43. package/dist/server/index.js +3 -0
  44. package/dist/server/knowledge-vectors.d.ts +15 -0
  45. package/dist/server/knowledge-vectors.js +118 -37
  46. package/dist/server/knowledge.d.ts +7 -8
  47. package/dist/server/knowledge.js +133 -55
  48. package/dist/server/length-continuation.d.ts +50 -0
  49. package/dist/server/length-continuation.js +55 -0
  50. package/dist/server/pi-session-events.js +5 -1
  51. package/dist/server/pi-session-pool.d.ts +2 -0
  52. package/dist/server/pi-session-pool.js +16 -0
  53. package/dist/server/runtime.js +5 -4
  54. package/dist/server/session-tree-scope.d.ts +125 -0
  55. package/dist/server/session-tree-scope.js +337 -0
  56. package/dist/server/store.d.ts +13 -0
  57. package/dist/server/store.js +37 -4
  58. package/dist/server/task-tool.d.ts +4 -1
  59. package/dist/server/task-tool.js +25 -1
  60. package/dist/server/turn-failure.d.ts +26 -0
  61. package/dist/server/turn-failure.js +51 -0
  62. package/dist/server/types.d.ts +15 -1
  63. package/package.json +20 -3
  64. package/dist/client/assets/index-D-OxlUxN.css +0 -1
@@ -153,7 +153,7 @@ function reconcileConversations(state) {
153
153
  // was sent) and the server died before any settled state was recorded. A conversation that was in
154
154
  // either of those two states when the process went away has a half-open turn whose outcome nobody
155
155
  // can determine from the file alone - exactly the situation this sweep exists to remove.
156
- if (!conversation || (conversation.status !== "running" && conversation.status !== "waiting"))
156
+ if (!conversation || (conversation.status !== "running" && conversation.status !== "waiting" && conversation.status !== "awaiting_background"))
157
157
  continue;
158
158
  conversation.status = "failed";
159
159
  conversation.error = ORPHANED_CONVERSATION_ERROR;
@@ -722,6 +722,10 @@ export class Store {
722
722
  const workspace = this.state.workspaces.find((item) => item.id === workspaceId);
723
723
  return workspace ? { ...workspace } : undefined;
724
724
  }
725
+ /** Read every workspace as detached shallow copies. */
726
+ workspaces() {
727
+ return this.state.workspaces.map((item) => ({ ...item }));
728
+ }
725
729
  /** Read the app-wide default without cloning conversations or transcripts. */
726
730
  defaultModel() {
727
731
  const model = this.state.defaultModel;
@@ -1234,7 +1238,7 @@ export class Store {
1234
1238
  const conversation = this.requireConversation(conversationId);
1235
1239
  if (conversation.archivedAt)
1236
1240
  return;
1237
- if (conversation.status === "running")
1241
+ if (conversation.status === "running" || conversation.status === "awaiting_background")
1238
1242
  throw new Error("运行中的会话无法归档,请先停止运行");
1239
1243
  conversation.archivedAt = now();
1240
1244
  await this.persistMetadata(conversationId);
@@ -1370,7 +1374,6 @@ export class Store {
1370
1374
  * reader sees who spoke.
1371
1375
  */
1372
1376
  async addAgentMessage(conversationId, content, options) {
1373
- console.error("STORE addAgentMessage conv=" + conversationId + " content=" + content.slice(0, 60));
1374
1377
  const conversation = this.requireConversation(conversationId);
1375
1378
  if (conversation.archivedAt)
1376
1379
  throw new Error("无法向已归档的会话下发指令,请先恢复该会话");
@@ -1422,6 +1425,10 @@ export class Store {
1422
1425
  // the new turn starts: leaving it up would show "was interrupted, marked failed" above a conversation
1423
1426
  // that is visibly working again.
1424
1427
  delete conversation.error;
1428
+ // THE SAME IS TRUE OF THE TRUNCATION WARNING. It describes the answer that was cut short, and a
1429
+ // new turn is the reader answering that situation - so the notice belongs to the turn that just
1430
+ // ended, not to the conversation from now on.
1431
+ delete conversation.warning;
1425
1432
  // AN INCREMENTAL WRITE, NOT A FULL ONE. This line was the last full-state write on the hot path
1426
1433
  // of every message, and it was in front of the broadcast that tells the browser a turn has
1427
1434
  // started - so the size of the WHOLE state was the delay between clicking send and the stream
@@ -1460,7 +1467,6 @@ export class Store {
1460
1467
  * state it also enters `waitingMessages` and `runConversationTurns` will pick it up next.
1461
1468
  */
1462
1469
  async addToolMessage(conversationId, content, options) {
1463
- console.error("STORE addToolMessage conv=" + conversationId + " content=" + content.slice(0, 60));
1464
1470
  const conversation = this.requireConversation(conversationId);
1465
1471
  if (conversation.archivedAt)
1466
1472
  throw new Error("无法向已归档的会话追加后台回传,请先恢复该会话");
@@ -1912,6 +1918,25 @@ export class Store {
1912
1918
  conversation.updatedAt = now();
1913
1919
  await this.persistMetadata(conversationId);
1914
1920
  }
1921
+ /**
1922
+ * Records WHY THIS CONVERSATION'S LAST ANSWER IS INCOMPLETE, in a way the reader can see.
1923
+ *
1924
+ * `Conversation.warning` is a different fact from `Conversation.error`: the run did not break, the
1925
+ * answer was cut short by the model's output budget. The reader's next move is different too - a
1926
+ * failed turn asks them to retry, a truncated one asks them to let the model continue - so the two
1927
+ * are kept apart rather than sharing one field and one colour.
1928
+ *
1929
+ * Set to `undefined` to clear, which the next `addUserMessage` does anyway.
1930
+ */
1931
+ async setWarningReason(conversationId, reason) {
1932
+ const conversation = this.requireConversation(conversationId);
1933
+ if (reason === undefined)
1934
+ delete conversation.warning;
1935
+ else
1936
+ conversation.warning = reason;
1937
+ conversation.updatedAt = now();
1938
+ await this.persistMetadata(conversationId);
1939
+ }
1915
1940
  /** The plan a revert would carry out, without carrying it out. The server reads this to stop children first. */
1916
1941
  planRevert(conversationId, messageId, withdraw = false) { return planRevert(this.state, conversationId, messageId, withdraw); }
1917
1942
  /**
@@ -2392,6 +2417,14 @@ export class Store {
2392
2417
  Object.assign(existing, summary);
2393
2418
  else
2394
2419
  this.state.backgroundTasks.unshift(summary);
2420
+ if (summary.conversationId && summary.status === "running") {
2421
+ const conv = this.state.conversations.find((c) => c.id === summary.conversationId);
2422
+ if (conv && conv.status !== "running" && conv.status !== "waiting") {
2423
+ conv.status = "awaiting_background";
2424
+ conv.updatedAt = now();
2425
+ await this.persistMetadata(conv.id);
2426
+ }
2427
+ }
2395
2428
  await this.persistCollections("backgroundTasks");
2396
2429
  }
2397
2430
  async removeBackgroundTask(id) {
@@ -257,11 +257,14 @@ export declare const SESSION_SPAWN_TOOL_NAME = "session_spawn";
257
257
  export declare const SESSION_STATUS_TOOL_NAME = "session_status";
258
258
  export declare const SESSION_DISPATCH_TOOL_NAME = "session_dispatch";
259
259
  export declare const SESSION_STOP_TOOL_NAME = "session_stop";
260
+ export declare const SESSION_REPORT_TOOL_NAME = "session_report";
260
261
  /** Model-facing description for session_spawn. */
261
- 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.\n5. The new session always belongs to the calling session's workspace. Pass 'cwd' as a workspace-relative subdirectory (for example, 'packages/a') when it should work below the workspace root; absolute paths and traversal are rejected.";
262
+ 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.\n5. The new session always belongs to the calling session's workspace. Pass 'cwd' as a workspace-relative subdirectory (for example, 'packages/a') when it should work below the workspace root; absolute paths and traversal are rejected.\n6. Optionally pass 'model' to choose which model the NEW session runs on: {\"provider\":\"...\",\"modelId\":\"...\"} - the same \"provider/modelId\" pair the model picker shows, and BOTH halves are required. Leave 'model' out and the new session starts on the workspace default model.\n7. Optionally pass 'watchdogTimeoutSeconds' to declare how long YOU are willing to wait for a report before you want to be reminded: a positive number of seconds. If the new session stays silent that long, you receive a '[Watchdog \u63D0\u9192]' message naming it. A report from the child (session_report) restarts the clock; the watchdog never stops the child. Leave it out to run without a watchdog.";
262
263
  /** Model-facing description for session_status. */
263
264
  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.";
264
265
  /** Model-facing description for session_dispatch. */
265
266
  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.";
266
267
  /** Model-facing description for session_stop. */
267
268
  export declare const SESSION_STOP_DESCRIPTION = "Stop execution in a sibling conversation.\n\nWHEN TO USE THIS:\n- When a sibling session has gone down the wrong path, is stuck in an unwanted loop, or when the overall goal has changed and remaining work should be aborted.\n- To clear pending queued messages before they start running.\n\nUsage notes:\n1. 'mode' controls what to stop:\n - 'all' (default): stops the currently running turn and cancels all pending queued messages in the session.\n - 'active': only interrupts the active running turn; leaves any pending queued messages untouched.\n - 'queued': only cancels pending queued messages; lets the currently running turn complete naturally.\n2. Use 'messageId' if you want to cancel or interrupt one specific queued message instead of all queued messages.";
269
+ /** Model-facing description for session_report. */
270
+ export declare const SESSION_REPORT_DESCRIPTION = "Report progress from THIS session to the parent session that spawned it.\n\nWHEN TO USE THIS:\n- You are a spawned sibling session, and the parent is waiting to hear from you: a milestone is done, a roadblock appeared, or the task is finished.\n- The parent set a watchdog (session_spawn's 'watchdogTimeoutSeconds'): a report is what restarts that clock, and silence is what triggers the reminder.\n- The task is over and the parent needs the outcome NOW rather than after the automatic end-of-turn result.\n\nWHEN NOT TO USE THIS:\n- This session has no parent (the user opened it directly): there is nobody to report to, and the server refuses the call.\n- You want to ASK the parent something or send it new instructions: that is session_dispatch, and only the parent can send it. A report is one-way - the parent receives it as a message.\n\nUsage notes:\n1. 'content' is required: it is the text the parent receives. Write it for the parent - what is done, what is left, what you need.\n2. 'status' says what the report MEANS:\n - 'in_progress' (default): an interim update. The task continues, and the parent's watchdog clock restarts.\n - 'completed': the task is finished and this is your final word.\n - 'failed': the task could not be finished, and this says why.\n3. A 'completed' or 'failed' report tells the server the work is over, so the parent is NOT also sent the automatic end-of-turn result. Send it once, at the end, with the final answer in 'content'.\n4. Reporting does NOT end your turn. If the task is not done, keep working afterwards - the parent is waiting on the work, not on the report.\n5. The report reaches your DIRECT parent only. A sibling, a cousin, and the user's own session are all out of reach, and the server refuses those.";
@@ -362,6 +362,7 @@ export const SESSION_SPAWN_TOOL_NAME = "session_spawn";
362
362
  export const SESSION_STATUS_TOOL_NAME = "session_status";
363
363
  export const SESSION_DISPATCH_TOOL_NAME = "session_dispatch";
364
364
  export const SESSION_STOP_TOOL_NAME = "session_stop";
365
+ export const SESSION_REPORT_TOOL_NAME = "session_report";
365
366
  /** Model-facing description for session_spawn. */
366
367
  export const SESSION_SPAWN_DESCRIPTION = `Create a new sibling conversation in the current workspace and immediately dispatch its initial task.
367
368
 
@@ -384,7 +385,9 @@ Usage notes:
384
385
  2. The initial task should provide clear context and goals since the new session starts with a fresh conversation history.
385
386
  3. Use session_status with the returned conversationId to check progress, or session_dispatch to send further messages.
386
387
  4. The new session reports completion to its DIRECT parent automatically. 'parentConversationId' optionally selects that parent; by default it is the calling session.
387
- 5. The new session always belongs to the calling session's workspace. Pass 'cwd' as a workspace-relative subdirectory (for example, 'packages/a') when it should work below the workspace root; absolute paths and traversal are rejected.`;
388
+ 5. The new session always belongs to the calling session's workspace. Pass 'cwd' as a workspace-relative subdirectory (for example, 'packages/a') when it should work below the workspace root; absolute paths and traversal are rejected.
389
+ 6. Optionally pass 'model' to choose which model the NEW session runs on: {"provider":"...","modelId":"..."} - the same "provider/modelId" pair the model picker shows, and BOTH halves are required. Leave 'model' out and the new session starts on the workspace default model.
390
+ 7. Optionally pass 'watchdogTimeoutSeconds' to declare how long YOU are willing to wait for a report before you want to be reminded: a positive number of seconds. If the new session stays silent that long, you receive a '[Watchdog 提醒]' message naming it. A report from the child (session_report) restarts the clock; the watchdog never stops the child. Leave it out to run without a watchdog.`;
388
391
  /** Model-facing description for session_status. */
389
392
  export const SESSION_STATUS_DESCRIPTION = `Inspect the progress, run state, and latest activity of sibling conversations created by the orchestrator.
390
393
 
@@ -423,3 +426,24 @@ Usage notes:
423
426
  - 'active': only interrupts the active running turn; leaves any pending queued messages untouched.
424
427
  - 'queued': only cancels pending queued messages; lets the currently running turn complete naturally.
425
428
  2. Use 'messageId' if you want to cancel or interrupt one specific queued message instead of all queued messages.`;
429
+ /** Model-facing description for session_report. */
430
+ export const SESSION_REPORT_DESCRIPTION = `Report progress from THIS session to the parent session that spawned it.
431
+
432
+ WHEN TO USE THIS:
433
+ - You are a spawned sibling session, and the parent is waiting to hear from you: a milestone is done, a roadblock appeared, or the task is finished.
434
+ - The parent set a watchdog (session_spawn's 'watchdogTimeoutSeconds'): a report is what restarts that clock, and silence is what triggers the reminder.
435
+ - The task is over and the parent needs the outcome NOW rather than after the automatic end-of-turn result.
436
+
437
+ WHEN NOT TO USE THIS:
438
+ - This session has no parent (the user opened it directly): there is nobody to report to, and the server refuses the call.
439
+ - You want to ASK the parent something or send it new instructions: that is session_dispatch, and only the parent can send it. A report is one-way - the parent receives it as a message.
440
+
441
+ Usage notes:
442
+ 1. 'content' is required: it is the text the parent receives. Write it for the parent - what is done, what is left, what you need.
443
+ 2. 'status' says what the report MEANS:
444
+ - 'in_progress' (default): an interim update. The task continues, and the parent's watchdog clock restarts.
445
+ - 'completed': the task is finished and this is your final word.
446
+ - 'failed': the task could not be finished, and this says why.
447
+ 3. A 'completed' or 'failed' report tells the server the work is over, so the parent is NOT also sent the automatic end-of-turn result. Send it once, at the end, with the final answer in 'content'.
448
+ 4. Reporting does NOT end your turn. If the task is not done, keep working afterwards - the parent is waiting on the work, not on the report.
449
+ 5. The report reaches your DIRECT parent only. A sibling, a cousin, and the user's own session are all out of reach, and the server refuses those.`;
@@ -19,4 +19,30 @@
19
19
  * needs no server, socket or Pi process.
20
20
  */
21
21
  export declare function formatTurnFailure(error: unknown): string;
22
+ /**
23
+ * THE MODEL FILLED ITS OUTPUT BUDGET AND STOPPED MID-ANSWER.
24
+ *
25
+ * A length stop is NOT a crash. The provider answered, streamed real text, and stopped because it
26
+ * ran out of `max_tokens` - which is what happens every time an answer is longer than one output
27
+ * budget. Treating it as a failure is what made an orchestrated child session die: the run service
28
+ * stamped the conversation `failed`, the orchestrator reported a failure to the parent, and a task
29
+ * that was one sentence from done was thrown away.
30
+ *
31
+ * It carries a CLASS so the layers above can tell it apart from a provider error by identity rather
32
+ * than by matching a Chinese substring - the message is written for a reader, and a reader-facing
33
+ * sentence is not a reliable machine contract.
34
+ */
35
+ export declare class LengthExceededError extends Error {
36
+ readonly stopReason: "length";
37
+ constructor(message?: string);
38
+ }
39
+ /**
40
+ * Whether this failure is a length stop rather than a real failure.
41
+ *
42
+ * The class is the answer; the message match is the BACKSTOP for a throw site that never got to
43
+ * construct the class - an error that crossed a process boundary, or one that was rebuilt from its
44
+ * text somewhere along the way. Both are checked because a length stop that loses its class on the
45
+ * way up is exactly the case that used to kill a child session.
46
+ */
47
+ export declare function isLengthExceededError(error: unknown): boolean;
22
48
  export declare function controllerAborted(error: unknown): boolean;
@@ -82,11 +82,62 @@ export function formatTurnFailure(error) {
82
82
  if (lower.includes("不支持图片输入")) {
83
83
  return raw;
84
84
  }
85
+ // LENGTH LIMIT - the model stopped because it filled the output budget, NOT because anything
86
+ // broke. The answer is incomplete, but everything that was said is still on screen, so the
87
+ // sentence has to offer the way out (say "continue") instead of calling it a failure.
88
+ //
89
+ // THE SENTENCE IS BUILT, NOT TAKEN FROM THE ERROR. The error's own message says the fact
90
+ // ("模型输出达到本轮长度上限,回答未完成") and stops there - which is a statement, not an action, and
91
+ // a reader who is told a thing stopped has no idea what to do next. What they need is the way
92
+ // out, so the sentence always carries it. Pi's own words are kept only when they add something
93
+ // the sentence does not already say.
94
+ if (isLengthExceededError(error)) {
95
+ const detail = raw && !/长度上限/.test(raw) ? `(${raw})` : "";
96
+ return `模型单次输出达到长度上限,回答没有写完${detail}。可以点「继续写完」或者自己发一句「继续」,模型会从被截断的地方接着写。`;
97
+ }
85
98
  // GENERIC - the literal message is more useful than a paraphrase, but it has to be the LAST
86
99
  // branch and the message has to be non-empty (a thrown `new Error()` is empty and renders
87
100
  // nothing, which is exactly the silence this whole change exists to break).
88
101
  return raw || "Pi 未能完成本轮回答,已标记为失败。可以重发任务继续。";
89
102
  }
103
+ /**
104
+ * THE MODEL FILLED ITS OUTPUT BUDGET AND STOPPED MID-ANSWER.
105
+ *
106
+ * A length stop is NOT a crash. The provider answered, streamed real text, and stopped because it
107
+ * ran out of `max_tokens` - which is what happens every time an answer is longer than one output
108
+ * budget. Treating it as a failure is what made an orchestrated child session die: the run service
109
+ * stamped the conversation `failed`, the orchestrator reported a failure to the parent, and a task
110
+ * that was one sentence from done was thrown away.
111
+ *
112
+ * It carries a CLASS so the layers above can tell it apart from a provider error by identity rather
113
+ * than by matching a Chinese substring - the message is written for a reader, and a reader-facing
114
+ * sentence is not a reliable machine contract.
115
+ */
116
+ export class LengthExceededError extends Error {
117
+ stopReason = "length";
118
+ constructor(message) {
119
+ super(message || "模型输出达到本轮长度上限,回答未完成。");
120
+ this.name = "LengthExceededError";
121
+ }
122
+ }
123
+ /**
124
+ * Whether this failure is a length stop rather than a real failure.
125
+ *
126
+ * The class is the answer; the message match is the BACKSTOP for a throw site that never got to
127
+ * construct the class - an error that crossed a process boundary, or one that was rebuilt from its
128
+ * text somewhere along the way. Both are checked because a length stop that loses its class on the
129
+ * way up is exactly the case that used to kill a child session.
130
+ */
131
+ export function isLengthExceededError(error) {
132
+ if (!error)
133
+ return false;
134
+ if (error instanceof LengthExceededError)
135
+ return true;
136
+ if (error?.stopReason === "length")
137
+ return true;
138
+ const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "";
139
+ return msg.includes("模型输出达到本轮长度上限") || msg.includes("达到本轮长度上限") || msg.includes("单次输出达到长度上限");
140
+ }
90
141
  export function controllerAborted(error) {
91
142
  if (!error)
92
143
  return false;
@@ -18,7 +18,7 @@ import type { ScheduledTask } from "./scheduled-tasks.js";
18
18
  * `failed` is what any unhandled throw lands (with `Conversation.error` carrying the translated reason);
19
19
  * `completed` is the clean path.
20
20
  */
21
- export type RunStatus = "idle" | "waiting" | "running" | "completed" | "stopped" | "failed";
21
+ export type RunStatus = "idle" | "waiting" | "running" | "awaiting_background" | "completed" | "stopped" | "failed";
22
22
  export type MessageRole = "user" | "assistant" | "tool" | "system";
23
23
  /**
24
24
  * One message in `state.json`.
@@ -494,6 +494,20 @@ export type Conversation = {
494
494
  * would answer "what happened" with something the reader has to go and decode.
495
495
  */
496
496
  error?: string;
497
+ /**
498
+ * WHY THIS CONVERSATION'S LAST ANSWER IS INCOMPLETE - in words a person reads.
499
+ *
500
+ * A WARNING, not a failure. The model filled its single-response output budget and stopped
501
+ * mid-answer; everything it said is on screen, and the conversation is still usable. It is a
502
+ * separate field from `error` because the two mean different things to the reader: `error` says
503
+ * "this run broke, here is why", this says "this answer was cut short, and here is what to do
504
+ * about it". Collapsing them would paint a recoverable truncation as a crash - which is exactly
505
+ * what made an orchestrated child session die.
506
+ *
507
+ * Cleared by the next turn, the same way `error` is: it describes the turn that just ended and
508
+ * must never outlive it.
509
+ */
510
+ warning?: string;
497
511
  /**
498
512
  * WHY THIS CONVERSATION IS NOT ON THE MODEL THE USER SET FOR IT - in words a person reads.
499
513
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.146",
3
+ "version": "0.1.148",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -51,6 +51,7 @@
51
51
  "scripts/inline-thinking.test.mjs",
52
52
  "scripts/inline-thinking-close-tag.test.mjs",
53
53
  "scripts/pi-session.test.mjs",
54
+ "scripts/length-continuation.test.mjs",
54
55
  "scripts/pi-session-isolation.test.mjs",
55
56
  "scripts/installed-layout.test.mjs",
56
57
  "scripts/pi-model-switch.test.mjs",
@@ -192,6 +193,7 @@
192
193
  "scripts/document-write-overwrite.test.mjs",
193
194
  "scripts/feishu.test.mjs",
194
195
  "scripts/scheduled-tasks.test.mjs",
196
+ "scripts/scheduled-tasks-timezone.test.mjs",
195
197
  "scripts/scheduled-task-model-clear.test.mjs",
196
198
  "scripts/scheduled-task-scope.test.mjs",
197
199
  "scripts/scheduled-task-tool.test.mjs",
@@ -203,8 +205,19 @@
203
205
  "scripts/spawn-workspace-boundary.test.mjs",
204
206
  "scripts/cwd-agent-spawn.test.mjs",
205
207
  "scripts/capabilities-prompt.test.mjs",
208
+ "scripts/version-fallback.test.mjs",
209
+ "scripts/model-image-input-session.test.mjs",
206
210
  "scripts/conversation-usage.test.mjs",
207
- "scripts/orchestrator-observability.test.mjs"
211
+ "scripts/orchestrator-observability.test.mjs",
212
+ "scripts/session-tab-preview.test.mjs",
213
+ "scripts/knowledge-retrieval-quality.test.mjs",
214
+ "scripts/rail-ui-contract.test.mjs",
215
+ "scripts/session-tree-scope.test.mjs",
216
+ "scripts/session-spawn-model.test.mjs",
217
+ "scripts/background-result-steer.test.mjs",
218
+ "scripts/session-report-watchdog.test.mjs",
219
+ "scripts/session-report-tool.test.mjs",
220
+ "scripts/awaiting-background-status.test.mjs"
208
221
  ],
209
222
  "check:browser": [
210
223
  "scripts/interrupt-browser.test.mjs",
@@ -243,6 +256,7 @@
243
256
  "scripts/archive-restore-race-browser.test.mjs",
244
257
  "scripts/tool-call-blocks-browser.test.mjs",
245
258
  "scripts/model-notice-browser.test.mjs",
259
+ "scripts/length-warning-browser.test.mjs",
246
260
  "scripts/dead-default-agent-browser.test.mjs",
247
261
  "scripts/assistant-working-indicator-browser.test.mjs",
248
262
  "scripts/thinking-first-delta-once-browser.test.mjs",
@@ -285,7 +299,10 @@
285
299
  "scripts/orchestrated-order-role-browser.test.mjs",
286
300
  "scripts/result-file-link-browser.test.mjs",
287
301
  "scripts/git-panel-confirm-browser.test.mjs",
288
- "scripts/session-origin-badge-browser.test.mjs"
302
+ "scripts/session-origin-badge-browser.test.mjs",
303
+ "scripts/rail-ui-layout-browser.test.mjs",
304
+ "scripts/session-landing-browser.test.mjs",
305
+ "scripts/responsive-rail-drawer-browser.test.mjs"
289
306
  ],
290
307
  "dependencies": {
291
308
  "@codemirror/commands": "^6.11.1",