@evandu/super-pi 0.1.147 → 0.1.149

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.
@@ -110,7 +110,7 @@ description: 用脚本清洗、转换、统计 Excel/CSV 数据
110
110
  mode: primary
111
111
  ---
112
112
  (正文从这里开始,写它的职责和工作方式。)保存后下一条消息即可使用;修改已有的自定义 Agent 就是编辑同一个目录里对应的 .md 文件。
113
- - 用户要多个 agent 分工协作:单个子任务用 task 工具委托,有先后依赖的流程用 workflow 工具编排;同一流程要按明确结论行反复执行时用 loop 节点,要对 primary 自己列出的一批同构对象逐项执行时用 foreach 节点,两者可以任意嵌套。需要拆分多条独立并行工作线、多会话持续跟踪管理、或用户要求创建多个会话并行时,用 session_spawn 创建兄弟会话,并用 session_status / session_dispatch / session_stop 监督管理。`;
113
+ - 用户要多个 agent 分工协作:单个子任务用 task 工具委托,有先后依赖的流程用 workflow 工具编排;同一流程要按明确结论行反复执行时用 loop 节点,要对 primary 自己列出的一批同构对象逐项执行时用 foreach 节点,两者可以任意嵌套。需要拆分多条独立并行工作线、多会话持续跟踪管理、或用户要求创建多个会话并行时,用 session_spawn 创建兄弟会话,并用 session_status / session_dispatch / session_stop 监督管理。父会话可在 session_spawn 时用 watchdogTimeoutSeconds 声明「多久没收到汇报就提醒我一次」;被创建出来的子会话用 session_report 主动向自己的父会话汇报(status 用 in_progress 表示进行中、completed 表示完成、failed 表示失败)。`;
114
114
 
115
115
  /** The parameters, OpenCode's `tool/todo.ts:6-8` minus its schema wrapper. */
116
116
  export type TodoParams = { todos: TodoEntry[] };
@@ -1140,6 +1140,7 @@ export const SESSION_SPAWN_TOOL_NAME = "session_spawn";
1140
1140
  export const SESSION_STATUS_TOOL_NAME = "session_status";
1141
1141
  export const SESSION_DISPATCH_TOOL_NAME = "session_dispatch";
1142
1142
  export const SESSION_STOP_TOOL_NAME = "session_stop";
1143
+ export const SESSION_REPORT_TOOL_NAME = "session_report";
1143
1144
 
1144
1145
  /** Model-facing description for session_spawn. */
1145
1146
  export const SESSION_SPAWN_DESCRIPTION = `Create a new sibling conversation in the current workspace and immediately dispatch its initial task.
@@ -1164,7 +1165,8 @@ Usage notes:
1164
1165
  3. Use session_status with the returned conversationId to check progress, or session_dispatch to send further messages.
1165
1166
  4. The new session reports completion to its DIRECT parent automatically. 'parentConversationId' optionally selects that parent; by default it is the calling session.
1166
1167
  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.
1167
- 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.`;
1168
+ 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.
1169
+ 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.`;
1168
1170
 
1169
1171
  /** Model-facing description for session_status. */
1170
1172
  export const SESSION_STATUS_DESCRIPTION = `Inspect the progress, run state, and latest activity of sibling conversations created by the orchestrator.
@@ -1207,6 +1209,28 @@ Usage notes:
1207
1209
  - 'queued': only cancels pending queued messages; lets the currently running turn complete naturally.
1208
1210
  2. Use 'messageId' if you want to cancel or interrupt one specific queued message instead of all queued messages.`;
1209
1211
 
1212
+ /** Model-facing description for session_report. */
1213
+ export const SESSION_REPORT_DESCRIPTION = `Report progress from THIS session to the parent session that spawned it.
1214
+
1215
+ WHEN TO USE THIS:
1216
+ - 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.
1217
+ - The parent set a watchdog (session_spawn's 'watchdogTimeoutSeconds'): a report is what restarts that clock, and silence is what triggers the reminder.
1218
+ - The task is over and the parent needs the outcome NOW rather than after the automatic end-of-turn result.
1219
+
1220
+ WHEN NOT TO USE THIS:
1221
+ - This session has no parent (the user opened it directly): there is nobody to report to, and the server refuses the call.
1222
+ - 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.
1223
+
1224
+ Usage notes:
1225
+ 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.
1226
+ 2. 'status' says what the report MEANS:
1227
+ - 'in_progress' (default): an interim update. The task continues, and the parent's watchdog clock restarts.
1228
+ - 'completed': the task is finished and this is your final word.
1229
+ - 'failed': the task could not be finished, and this says why.
1230
+ 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'.
1231
+ 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.
1232
+ 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.`;
1233
+
1210
1234
  /** Compact summary returned for an orchestrated session. */
1211
1235
  export type OrchestratorSessionSummary = {
1212
1236
  conversationId: string;
@@ -1271,6 +1295,13 @@ export type SessionSpawnParams = {
1271
1295
  wait?: boolean;
1272
1296
  /** Optional subdirectory (relative to the resolved workspace root) the new session's cwd should be rooted in. */
1273
1297
  cwd?: string;
1298
+ /**
1299
+ * How long the PARENT is willing to wait for a report before it wants to be reminded, in seconds.
1300
+ *
1301
+ * The parent declares it once, at spawn time, and the watchdog never stops the child - it only
1302
+ * tells the parent that the child has gone quiet. A report from the child restarts the clock.
1303
+ */
1304
+ watchdogTimeoutSeconds?: number;
1274
1305
  };
1275
1306
 
1276
1307
  export type SessionStatusParams = {
@@ -1291,6 +1322,18 @@ export type SessionStopParams = {
1291
1322
  messageId?: string;
1292
1323
  };
1293
1324
 
1325
+ /**
1326
+ * What this session is allowed to tell its parent.
1327
+ *
1328
+ * `status` is the model's word for what the report MEANS, and the server turns it into two
1329
+ * different things: an interim report restarts the watchdog clock, a terminal one cancels it and
1330
+ * silences the automatic end-of-turn result. The three values are the server's own vocabulary.
1331
+ */
1332
+ export type SessionReportParams = {
1333
+ content: string;
1334
+ status?: "in_progress" | "completed" | "failed";
1335
+ };
1336
+
1294
1337
  /**
1295
1338
  * THE FORMAT THE MODEL IS TOLD TO USE. The client hands its model picker a single
1296
1339
  * "provider/modelId" string, so this is the shape the model already knows how to read.
@@ -1303,6 +1346,34 @@ const SPAWN_MODEL_MISSING_PROVIDER_REFUSAL = `session_spawn's \`model\` paramete
1303
1346
 
1304
1347
  const SPAWN_MODEL_MISSING_MODEL_ID_REFUSAL = `session_spawn's \`model\` parameter is missing \`modelId\`. It needs BOTH \`provider\` and \`modelId\` - ${SPAWN_MODEL_FORMAT}. Provide both, or leave \`model\` out entirely to use the workspace default model.`;
1305
1348
 
1349
+ /**
1350
+ * The silence watchdog the parent declares, or the reason it cannot be used.
1351
+ *
1352
+ * A NUMBER OF SECONDS, AND NOTHING ELSE. The server clamps whatever arrives into the range it
1353
+ * accepts, so a nonsense value would arrive as a real watchdog with a duration nobody chose - a
1354
+ * parent that asked for "about five minutes" and got three seconds of tolerance would be reminded
1355
+ * before the child had said anything. So a value that is not a positive number is refused HERE,
1356
+ * where the model is told exactly what to write instead.
1357
+ *
1358
+ * An ABSENT value is not a case here: it means "no watchdog", which is what a spawn without one has
1359
+ * always meant, and the body then carries no watchdog field at all.
1360
+ */
1361
+ const SPAWN_WATCHDOG_REFUSAL = `session_spawn's \`watchdogTimeoutSeconds\` parameter must be a positive number of seconds - how long the parent is willing to wait for a report before it wants to be reminded. Provide a number such as 300, or leave the parameter out to run without a watchdog.`;
1362
+
1363
+ function readSpawnWatchdog(raw: unknown): { seconds?: number; error?: string } {
1364
+ if (raw === undefined || raw === null) return {};
1365
+ // A NUMBER, OR A STRING THAT IS ONE. Anything else - a boolean, an object, an array - is not a
1366
+ // duration, and `Number(true)` being 1 is exactly the kind of accident a duration must not have.
1367
+ const value = typeof raw === "number"
1368
+ ? raw
1369
+ : (typeof raw === "string" && raw.trim() ? Number(raw) : Number.NaN);
1370
+ if (!Number.isFinite(value) || value <= 0) return { error: SPAWN_WATCHDOG_REFUSAL };
1371
+ // THE SERVER'S OWN FLOOR. A watchdog shorter than a second is not a watchdog, and the server
1372
+ // clamps it up to one second - so the tool does the same, and a parent that asked for "half a
1373
+ // second" gets a second rather than a watchdog that never fires.
1374
+ return { seconds: Math.max(1, Math.floor(value)) };
1375
+ }
1376
+
1306
1377
  /**
1307
1378
  * The model selection the model asked for, or the reason it cannot be used.
1308
1379
  *
@@ -1359,6 +1430,13 @@ export async function executeSessionSpawn(
1359
1430
  if (selection.error) {
1360
1431
  return text(selection.error, true);
1361
1432
  }
1433
+ // THE SAME TREATMENT FOR THE WATCHDOG. A silence timeout that is not a number would reach the
1434
+ // server as a duration nobody chose, and the server would clamp it into a real watchdog anyway -
1435
+ // so the mistake is refused here, where the model can fix it in one call.
1436
+ const watchdog = readSpawnWatchdog(params.watchdogTimeoutSeconds);
1437
+ if (watchdog.error) {
1438
+ return text(watchdog.error, true);
1439
+ }
1362
1440
 
1363
1441
  const send = deps.fetch ?? (globalFetch as unknown as TaskFetch);
1364
1442
  try {
@@ -1381,6 +1459,9 @@ export async function executeSessionSpawn(
1381
1459
  thinkingLevel: params.thinkingLevel?.trim() || undefined,
1382
1460
  wait: Boolean(params.wait),
1383
1461
  cwd: params.cwd?.trim() || undefined,
1462
+ // ONLY WHEN A DURATION WAS GIVEN. An absent watchdog leaves the field out entirely, and the
1463
+ // child then runs unwatched - which is what a spawn without one has always meant.
1464
+ ...(watchdog.seconds ? { watchdogTimeoutSeconds: watchdog.seconds } : {}),
1384
1465
  callerConversationId: config.conversationId || undefined,
1385
1466
  parentConversationId: params.parentConversationId?.trim() || config.conversationId || undefined,
1386
1467
  capability: config.capability,
@@ -1616,3 +1697,84 @@ export async function executeSessionStop(
1616
1697
  return text(`session_stop network error: ${error instanceof Error ? error.message : String(error)}`, true);
1617
1698
  }
1618
1699
  }
1700
+
1701
+ /**
1702
+ * Execute session_report: THIS session tells its direct parent what is going on.
1703
+ *
1704
+ * WHO CALLS IT. The child. The parent has no use for it - the parent's half of this conversation is
1705
+ * `session_dispatch`, `session_status` and `session_stop`. The child has no other way to speak at
1706
+ * all: without this tool, everything a child knows stays in the child's transcript until the turn
1707
+ * ends, and a parent waiting on a long task has nothing to read in the meantime.
1708
+ *
1709
+ * WHY THE CALLER ID AND NOT A TARGET. The body carries THIS session's id and no target at all. The
1710
+ * server reads the parent off the conversation's own tree edge, so the report can only ever reach
1711
+ * the session that spawned this one: a sibling, a cousin, and the user's own session are out of
1712
+ * reach by construction rather than by a rule that could be forgotten. The server owns that rule and
1713
+ * refuses what does not belong to the tree.
1714
+ *
1715
+ * WHY A REPORT DOES NOT END THE TURN. The parent asked to hear from this child WHILE it worked; a
1716
+ * report that ended the child's turn would turn every interim update into a completion. So the
1717
+ * report is a message, and the child's turn continues.
1718
+ */
1719
+ export async function executeSessionReport(
1720
+ params: SessionReportParams,
1721
+ config: OrchestratorToolConfig,
1722
+ deps: { signal?: AbortSignal; fetch?: TaskFetch } = {},
1723
+ ): Promise<TaskToolResult> {
1724
+ const text = (body: string, isError?: boolean): TaskToolResult => ({
1725
+ content: [{ type: "text", text: body }],
1726
+ details: undefined,
1727
+ ...(isError ? { isError: true } : {}),
1728
+ });
1729
+
1730
+ if (!config.endpoint) {
1731
+ return text("The session_report tool is not available in this session: SUPER_PI_ORCHESTRATOR_ENDPOINT is not set.", true);
1732
+ }
1733
+ if (!config.conversationId) {
1734
+ return text("The session_report tool is not available in this session: SUPER_PI_TASK_CONVERSATION is not set.", true);
1735
+ }
1736
+ if (!params.content || !params.content.trim()) {
1737
+ return text("session_report requires a non-empty `content` message.", true);
1738
+ }
1739
+ // THE STATUS IS THE MODEL'S OWN WORD, AND THE THREE VALUES ARE THE SERVER'S VOCABULARY. An
1740
+ // unknown value would reach the server as an interim report - the least consequential of the
1741
+ // three - so it is normalized here rather than travelling as something the server never saw.
1742
+ const status = params.status === "completed" || params.status === "failed" ? params.status : "in_progress";
1743
+
1744
+ const send = deps.fetch ?? (globalFetch as unknown as TaskFetch);
1745
+ try {
1746
+ const response = await send(config.endpoint, {
1747
+ method: "POST",
1748
+ headers: {
1749
+ "content-type": "application/json",
1750
+ ...(config.token ? { authorization: `Bearer ${config.token}` } : {}),
1751
+ },
1752
+ body: JSON.stringify({
1753
+ action: "report",
1754
+ content: params.content.trim(),
1755
+ status,
1756
+ // THE CALLER IS THIS SESSION. There is no target field: the parent is the server's to
1757
+ // decide, and it decides from the tree edge.
1758
+ callerConversationId: config.conversationId,
1759
+ capability: config.capability,
1760
+ }),
1761
+ ...(deps.signal ? { signal: deps.signal } : {}),
1762
+ });
1763
+
1764
+ const bodyText = await response.text();
1765
+ let payload: { ok?: boolean; delivered?: boolean; parentConversationId?: string; error?: string };
1766
+ try {
1767
+ payload = JSON.parse(bodyText);
1768
+ } catch {
1769
+ return text(`The orchestrator endpoint returned HTTP ${response.status}: ${bodyText}`, true);
1770
+ }
1771
+
1772
+ if (!response.ok || !payload.ok || !payload.delivered) {
1773
+ return text(`Failed to report to the parent session: ${payload.error ?? `HTTP ${response.status}`}`, true);
1774
+ }
1775
+
1776
+ return text(`Report delivered to parent session ${payload.parentConversationId ?? "(unknown)"}.`);
1777
+ } catch (error) {
1778
+ return text(`session_report network error: ${error instanceof Error ? error.message : String(error)}`, true);
1779
+ }
1780
+ }
@@ -41,19 +41,23 @@ import {
41
41
  SESSION_STATUS_TOOL_NAME,
42
42
  SESSION_DISPATCH_TOOL_NAME,
43
43
  SESSION_STOP_TOOL_NAME,
44
+ SESSION_REPORT_TOOL_NAME,
44
45
  SESSION_SPAWN_DESCRIPTION,
45
46
  SESSION_STATUS_DESCRIPTION,
46
47
  SESSION_DISPATCH_DESCRIPTION,
47
48
  SESSION_STOP_DESCRIPTION,
49
+ SESSION_REPORT_DESCRIPTION,
48
50
  executeSessionSpawn,
49
51
  executeSessionStatus,
50
52
  executeSessionDispatch,
51
53
  executeSessionStop,
54
+ executeSessionReport,
52
55
  readOrchestratorConfig,
53
56
  type SessionSpawnParams,
54
57
  type SessionStatusParams,
55
58
  type SessionDispatchParams,
56
59
  type SessionStopParams,
60
+ type SessionReportParams,
57
61
  } from "./core.ts";
58
62
 
59
63
  /**
@@ -471,6 +475,9 @@ const extension = (pi: ExtensionApi): void => {
471
475
  cwd: Type.Optional(Type.String({
472
476
  description: "Optional workspace-root-relative subdirectory (for example, packages/a). Absolute paths, '..' traversal, UNC paths, and escaping symlinks are rejected; omit it to use the workspace root.",
473
477
  })),
478
+ watchdogTimeoutSeconds: Type.Optional(Type.Number({
479
+ description: "How long YOU are willing to wait for a report from the new session before you want to be reminded, in seconds (for example 300 for five minutes). A report from the child restarts the clock; the watchdog never stops the child. Omit it to run without a watchdog.",
480
+ })),
474
481
  });
475
482
 
476
483
  const SessionStatusParameters = Type.Object({
@@ -513,6 +520,27 @@ const extension = (pi: ExtensionApi): void => {
513
520
  })),
514
521
  });
515
522
 
523
+ /**
524
+ * The `session_report` parameter schema.
525
+ *
526
+ * `content` is required, because a report with nothing in it is a report that says nothing - and
527
+ * the parent reads it as if it had been told something. `status` is a closed union of the three
528
+ * words the server understands, so a model cannot invent a fourth one that arrives as an
529
+ * unintended interim update.
530
+ */
531
+ const SessionReportParameters = Type.Object({
532
+ content: Type.String({
533
+ description: "The text the parent receives. Write it for the parent: what is done, what is left, what you need.",
534
+ }),
535
+ status: Type.Optional(Type.Union([
536
+ Type.Literal("in_progress", { description: "An interim update; the task continues and the parent's watchdog clock restarts (default)" }),
537
+ Type.Literal("completed", { description: "The task is finished and this is the final report" }),
538
+ Type.Literal("failed", { description: "The task could not be finished; say why" }),
539
+ ], {
540
+ description: "What this report means: 'in_progress' (default), 'completed', or 'failed'.",
541
+ })),
542
+ });
543
+
516
544
  if (orchestratorConfig.endpoint) {
517
545
  pi.registerTool({
518
546
  name: SESSION_SPAWN_TOOL_NAME,
@@ -553,6 +581,24 @@ const extension = (pi: ExtensionApi): void => {
553
581
  execute: async (_toolCallId, params, signal) =>
554
582
  await executeSessionStop(params as SessionStopParams, orchestratorConfig, { signal: signal ?? undefined }),
555
583
  });
584
+
585
+ /**
586
+ * `session_report`, registered AFTER the four tools that act ON OTHER SESSIONS.
587
+ *
588
+ * The order is the reading order, and this one is the odd one out: the other four are a
589
+ * PARENT's tools - they reach out and manage somebody else's session - while this one is the
590
+ * CHILD's own voice. A model reading the list top to bottom meets the manager's instruments
591
+ * first and the worker's mouth last, which is the order the relationship actually has.
592
+ */
593
+ pi.registerTool({
594
+ name: SESSION_REPORT_TOOL_NAME,
595
+ label: "Report to Parent",
596
+ description: SESSION_REPORT_DESCRIPTION,
597
+ promptSnippet: "report progress or a blocker to the parent session that spawned this one",
598
+ parameters: SessionReportParameters,
599
+ execute: async (_toolCallId, params, signal) =>
600
+ await executeSessionReport(params as SessionReportParams, orchestratorConfig, { signal: signal ?? undefined }),
601
+ });
556
602
  }
557
603
  };
558
604
 
@@ -35,5 +35,5 @@
35
35
  "src/client/types.ts": "7c06189b32890009d28a371a64d9f29161c14a349a5b399d51d1314d38fa27ac",
36
36
  "src/client/vite-env.d.ts": "65996936fbb042915f7b74a200fcdde7e410f32a669b1ab9597cfaa4b0faddb5"
37
37
  },
38
- "recordedAt": "2026-11-01T06:41:55.741Z"
38
+ "recordedAt": "2026-11-01T14:53:57.934Z"
39
39
  }
@@ -38,7 +38,7 @@ type Frontmatter = Record<string, unknown>;
38
38
  */
39
39
  export declare const PI_TOOL_NAMES: readonly ["read", "bash", "powershell", "edit", "write", "grep", "find", "ls", "codemode", "tool_search", "list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"];
40
40
  /** Tools shipped by this package. Authors must be able to explicitly deny host extras. */
41
- export declare const SUPER_PI_TOOL_NAMES: readonly ["task", "todowrite", "workflow", "run_async", "scheduled_task", "question", "tool_register", "memory", "skill_manage", "session_search", "session_spawn", "session_status", "session_dispatch", "session_stop", "knowledge_search", "knowledge_import", "document_read", "document_write", "web_search", "web_fetch", "feishu_doc_read", "feishu_doc_write"];
41
+ export declare const SUPER_PI_TOOL_NAMES: readonly ["task", "todowrite", "workflow", "run_async", "scheduled_task", "question", "tool_register", "memory", "skill_manage", "session_search", "session_spawn", "session_status", "session_dispatch", "session_stop", "session_report", "knowledge_search", "knowledge_import", "document_read", "document_write", "web_search", "web_fetch", "feishu_doc_read", "feishu_doc_write"];
42
42
  /**
43
43
  * Whether a name can be a real Pi tool. A name is real if it is one of `PI_TOOL_NAMES`, an MCP
44
44
  * server tool, or a PATTERN.
@@ -40,7 +40,7 @@ export const PI_TOOL_NAMES = [
40
40
  export const SUPER_PI_TOOL_NAMES = [
41
41
  "task", "todowrite", "workflow", "run_async", "scheduled_task", "question", "tool_register",
42
42
  "memory", "skill_manage", "session_search",
43
- "session_spawn", "session_status", "session_dispatch", "session_stop",
43
+ "session_spawn", "session_status", "session_dispatch", "session_stop", "session_report",
44
44
  "knowledge_search", "knowledge_import",
45
45
  "document_read", "document_write", "web_search", "web_fetch", "feishu_doc_read", "feishu_doc_write",
46
46
  ];
@@ -110,16 +110,27 @@ export declare class SessionOrchestratorService {
110
110
  */
111
111
  private readonly lengthContinuations;
112
112
  /**
113
- * THE CHILDREN THAT HAVE ALREADY REPORTED THEIR OWN RESULT TO THEIR PARENT.
113
+ * THE CHILDREN THAT HAVE ALREADY REPORTED THEIR OWN RESULT TO THEIR PARENT, AND WHEN.
114
114
  *
115
115
  * A child can report explicitly - the `report` action - and that report is the child's own
116
116
  * statement of what it did. When the child's turn then ends, the automatic completion report
117
117
  * would deliver the same thing a second time, so the automatic path steps aside for a child that
118
- * is in this set.
118
+ * is in this map.
119
119
  *
120
120
  * The entry is added only by a TERMINAL report (`completed` / `failed`), because an interim
121
121
  * update is not the child's final word: the task is still running, and the parent will want to
122
122
  * hear how it ended.
123
+ *
124
+ * WHY THE MAP HOLDS A TIMESTAMP AND NOT A FLAG. A flag says "this child reported", and that is
125
+ * not the same question as "this child reported ABOUT THE TURN THAT JUST ENDED". A flag stays
126
+ * true for the rest of the child's life, so it silences every automatic report the child ever
127
+ * produces again: the parent gives the same child a second task, the child finishes, and the
128
+ * result is swallowed because of a report the parent received about the FIRST task. The parent
129
+ * waits for a result that was written and never arrives, and nothing in the transcript says why.
130
+ *
131
+ * The timestamp answers the real question: the report was made INSIDE this turn, so it speaks for
132
+ * this turn. A report made before the turn started speaks for an earlier one, and this turn's
133
+ * result is still its own.
123
134
  */
124
135
  private readonly reportedChildren;
125
136
  constructor(store: Store, conversationService: ConversationService, runService: RunService, ledger: SessionLedger, broadcaster?: EventBroadcaster | undefined, watchdog?: SessionWatchdog);
@@ -39,18 +39,29 @@ export class SessionOrchestratorService {
39
39
  */
40
40
  lengthContinuations = new Map();
41
41
  /**
42
- * THE CHILDREN THAT HAVE ALREADY REPORTED THEIR OWN RESULT TO THEIR PARENT.
42
+ * THE CHILDREN THAT HAVE ALREADY REPORTED THEIR OWN RESULT TO THEIR PARENT, AND WHEN.
43
43
  *
44
44
  * A child can report explicitly - the `report` action - and that report is the child's own
45
45
  * statement of what it did. When the child's turn then ends, the automatic completion report
46
46
  * would deliver the same thing a second time, so the automatic path steps aside for a child that
47
- * is in this set.
47
+ * is in this map.
48
48
  *
49
49
  * The entry is added only by a TERMINAL report (`completed` / `failed`), because an interim
50
50
  * update is not the child's final word: the task is still running, and the parent will want to
51
51
  * hear how it ended.
52
+ *
53
+ * WHY THE MAP HOLDS A TIMESTAMP AND NOT A FLAG. A flag says "this child reported", and that is
54
+ * not the same question as "this child reported ABOUT THE TURN THAT JUST ENDED". A flag stays
55
+ * true for the rest of the child's life, so it silences every automatic report the child ever
56
+ * produces again: the parent gives the same child a second task, the child finishes, and the
57
+ * result is swallowed because of a report the parent received about the FIRST task. The parent
58
+ * waits for a result that was written and never arrives, and nothing in the transcript says why.
59
+ *
60
+ * The timestamp answers the real question: the report was made INSIDE this turn, so it speaks for
61
+ * this turn. A report made before the turn started speaks for an earlier one, and this turn's
62
+ * result is still its own.
52
63
  */
53
- reportedChildren = new Set();
64
+ reportedChildren = new Map();
54
65
  constructor(store, conversationService, runService, ledger, broadcaster, watchdog) {
55
66
  this.store = store;
56
67
  this.conversationService = conversationService;
@@ -180,6 +191,11 @@ export class SessionOrchestratorService {
180
191
  // writes the words under the reader's own name - so the child's transcript showed an
181
192
  // instruction the reader never typed, wearing the reader's avatar, and a reader who looked at
182
193
  // the child session saw their own name on somebody else's order.
194
+ //
195
+ // THE TURN'S START IS RECORDED HERE, because the question "did this child already report THIS
196
+ // turn?" is answered by comparing the report's time against it. A report made inside the turn
197
+ // speaks for the turn; a report made before it speaks for an earlier one.
198
+ const turnStartedAt = Date.now();
183
199
  const runPromise = this.runService.enqueueAndRunAsAgent(conversationId, content);
184
200
  const completion = (async () => {
185
201
  let failure;
@@ -229,14 +245,23 @@ export class SessionOrchestratorService {
229
245
  const result = await this.persistSessionResult(conversationId, failure);
230
246
  if (result) {
231
247
  const current = this.store.conversation(conversationId);
232
- // AN INTERRUPTED CHILD HAS NO RESULT TO REPORT. "stopped" means somebody pressed stop, and
233
- // the half-finished answer that was on screen is not a result anybody stated. The parent is
234
- // told nothing, which is right: the child can be asked again, and then it reports.
235
- const interrupted = current?.status === "stopped";
236
- // A CHILD THAT REPORTED ITSELF IS NOT REPORTED TWICE. The explicit report is the child's
237
- // own statement of what it did, so the automatic completion steps aside for it.
238
- const alreadyReported = this.reportedChildren.has(conversationId);
239
- if (!interrupted && !alreadyReported) {
248
+ // THE REPORT GOES OUT ONLY WHEN THE CHILD'S TURN ENDED. The statuses that mean "the turn is
249
+ // over" are `completed` and `failed`; `running` and `awaiting_background` mean work is still
250
+ // in flight and its result is not final, and `stopped` means somebody pressed stop, so the
251
+ // half-finished answer on screen is not a result anybody stated.
252
+ //
253
+ // This is what separates the two channels. The automatic report says "the task ended, here
254
+ // is what it produced"; the explicit report says "the child is telling its parent something
255
+ // now", and can be called at any point in the task's life.
256
+ const turnEnded = current?.status === "completed" || current?.status === "failed";
257
+ // A CHILD THAT REPORTED ITSELF IS NOT REPORTED TWICE - BUT ONLY FOR THE TURN IT REPORTED.
258
+ // The explicit terminal report is the child's own statement of what it did, so the
259
+ // automatic completion steps aside for it - when the report was made inside THIS turn.
260
+ // A report from an earlier turn says nothing about this one's result, and silencing this
261
+ // one because of that would swallow a result the parent is still waiting for.
262
+ const reportedAt = this.reportedChildren.get(conversationId);
263
+ const alreadyReported = reportedAt !== undefined && reportedAt >= turnStartedAt;
264
+ if (turnEnded && !alreadyReported) {
240
265
  await this.runService.enqueueSessionResult({
241
266
  conversationId,
242
267
  parentConversationId: current?.parentConversationId,
@@ -255,6 +280,16 @@ export class SessionOrchestratorService {
255
280
  // turn an already-finished child turn into a second failure.
256
281
  console.warn(`Super Pi: session result callback failed for ${conversationId}:`, error instanceof Error ? error.message : String(error));
257
282
  }
283
+ finally {
284
+ // THE RECORD IS SPENT BY THE TURN IT SPOKE FOR. Whatever it was about, that turn is over.
285
+ //
286
+ // A record left behind would be harmless - the timestamp comparison already keeps it from
287
+ // silencing a later turn - but it would keep saying "this child reported" to every future
288
+ // reader of this code, and the next thing that consults it would have to re-derive the
289
+ // timestamp rule. Dropping it here keeps one rule in one place: a report belongs to the
290
+ // turn that was running when it was made.
291
+ this.reportedChildren.delete(conversationId);
292
+ }
258
293
  if (failure !== undefined) {
259
294
  console.warn(`Super Pi: dispatch failed for conversation ${conversationId}:`, failure instanceof Error ? failure.message : String(failure));
260
295
  this.ledger.update(conversationId, { status: "failed" });
@@ -386,7 +421,10 @@ export class SessionOrchestratorService {
386
421
  // does not silence the automatic report: the child is still working, and the parent will want
387
422
  // to hear how the task ended.
388
423
  if (status === "completed" || status === "failed") {
389
- this.reportedChildren.add(childConversationId);
424
+ // THE TIME OF THE REPORT IS THE PART THAT MATTERS. It is what tells the completion path
425
+ // whether this report spoke for the turn that is ending or for an earlier one, and it is
426
+ // stamped here - where the report is made - rather than where it is looked up later.
427
+ this.reportedChildren.set(childConversationId, Date.now());
390
428
  await this.watchdog.cancel(childConversationId);
391
429
  }
392
430
  else {
@@ -24,7 +24,7 @@ import { LengthExceededError } from "./turn-failure.js";
24
24
  // paths recognise a delivery identically. Re-exported because they were part of this module's surface.
25
25
  export { describeDeliveredUserMessage, describePiQueue } from "./pi-session-events.js";
26
26
  import { createPiSession, createSdkCatalog, piSdkAvailable, resolveTranscriptFile, superPiExtensionPaths } from "./pi-session.js";
27
- 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, ORCHESTRATOR_WORKSPACE_ENV, SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, rosterPayload, } from "./task-tool.js";
27
+ 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, ORCHESTRATOR_WORKSPACE_ENV, SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, SESSION_REPORT_TOOL_NAME, rosterPayload, } from "./task-tool.js";
28
28
  import { BLOCKING_UI_METHODS, EXTENSION_UI_METHODS, } from "./types.js";
29
29
  import { loadConversationImages } from "./upload.js";
30
30
  import { describeToolCall, describeToolCallArgs, describeToolCallInProgress, extractPartialResultText, extractToolResultText, readQuestionCall } from "./tool-calls.js";
@@ -1260,7 +1260,7 @@ export class PiRpcRuntime {
1260
1260
  if (process.env.SUPER_PI_FEISHU_ENDPOINT)
1261
1261
  names.push(FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME);
1262
1262
  if (this.options.orchestratorEndpoint)
1263
- names.push(SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME);
1263
+ names.push(SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, SESSION_REPORT_TOOL_NAME);
1264
1264
  return names;
1265
1265
  }
1266
1266
  /**
@@ -1479,7 +1479,7 @@ export class PiRpcRuntime {
1479
1479
  // allowlist and naming a tool Pi has not registered would change the spawn line for every other
1480
1480
  // mode. The prompt asks for a bare JSON array and nothing else; if the model spends a turn on a
1481
1481
  // tool call anyway the reranker times out and falls back to the lexical order.
1482
- const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : knowledgeRerank ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, SCHEDULED_TASK_TOOL_NAME, "question", "tool_register", ...(this.options.orchestratorEndpoint ? [SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME] : [])] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : []), ...(knowledgeEnabled ? [KNOWLEDGE_TOOL_NAME, KNOWLEDGE_IMPORT_TOOL_NAME] : []), ...(documentsEnabled ? [DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME] : []), ...(webEnabled ? [WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME] : []), ...(feishuEnabled ? [FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME] : [])] });
1482
+ const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : knowledgeRerank ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, SCHEDULED_TASK_TOOL_NAME, "question", "tool_register", ...(this.options.orchestratorEndpoint ? [SESSION_SPAWN_TOOL_NAME, SESSION_STATUS_TOOL_NAME, SESSION_DISPATCH_TOOL_NAME, SESSION_STOP_TOOL_NAME, SESSION_REPORT_TOOL_NAME] : [])] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : []), ...(knowledgeEnabled ? [KNOWLEDGE_TOOL_NAME, KNOWLEDGE_IMPORT_TOOL_NAME] : []), ...(documentsEnabled ? [DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME] : []), ...(webEnabled ? [WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME] : []), ...(feishuEnabled ? [FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME] : [])] });
1483
1483
  // Refused here, with the reason, rather than handed to the spawn: an ENOENT from `spawn` reads as
1484
1484
  // "the app is broken" and hides the one fact that matters, which is where it looked for Pi.
1485
1485
  if (!this.pi.found)
@@ -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.\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.";
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
 
@@ -385,7 +386,8 @@ Usage notes:
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
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.
388
- 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.`;
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.`;
389
391
  /** Model-facing description for session_status. */
390
392
  export const SESSION_STATUS_DESCRIPTION = `Inspect the progress, run state, and latest activity of sibling conversations created by the orchestrator.
391
393
 
@@ -424,3 +426,24 @@ Usage notes:
424
426
  - 'active': only interrupts the active running turn; leaves any pending queued messages untouched.
425
427
  - 'queued': only cancels pending queued messages; lets the currently running turn complete naturally.
426
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.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.147",
3
+ "version": "0.1.149",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -216,6 +216,7 @@
216
216
  "scripts/session-spawn-model.test.mjs",
217
217
  "scripts/background-result-steer.test.mjs",
218
218
  "scripts/session-report-watchdog.test.mjs",
219
+ "scripts/session-report-tool.test.mjs",
219
220
  "scripts/awaiting-background-status.test.mjs"
220
221
  ],
221
222
  "check:browser": [