From 15d25d8c2e0aad473b0bb007680803f6f3ec62a9 Mon Sep 17 00:00:00 2001
From: Federico Jaramillo Martinez yield_to_subsessions again at the next join point. Further notices arrive automatically; do
- not poll.
+ not poll. The notice includes the child's final output when it fits. If that output is too long, PI WEB
+ omits it entirely instead of adding a truncated duplicate to the parent's context and directs the parent
+ to retrieve it with check_subsession.
list_subsessions, check_subsession, and read_subsession never yield
or change control flow. They are for deliberate inspection or recovery, not completion polling. While a
child works, agent-facing check_subsession and read_subsession withhold partial
output and direct the parent to continue independent work or yield at the join point. Output becomes
- available when the child stops. In notices and inspection results, PI WEB guidance precedes a labeled
- marker and the child output or transcript always comes last.
+ available when the child stops. Included output and transcripts follow a labeled marker and come last,
+ after PI WEB guidance.
In Settings → Session daemon, these keys are saved on the selected machine. Restart the diff --git a/docs/config.md b/docs/config.md index 8819de0..61b70b1 100644 --- a/docs/config.md +++ b/docs/config.md @@ -216,9 +216,9 @@ Tracked subsessions are join-oriented. Calling `spawn_subsession` returns immedi At a join point, after finishing its independent work, the parent calls `yield_to_subsessions` alone as the final action in its tool batch. Pi ends a tool batch early only when every result in that batch is terminating. If any tracked child is still working, the action ends the current agent run so the parent becomes idle. If none are working, it does not end the run and clearly reports that there is nothing to wait for. -A completion notice wakes an idle parent or queues behind in-flight work. Each notice lists any other tracked children still working, so the parent can continue work or call `yield_to_subsessions` again at the next join point. Further notices arrive automatically; do not poll. +A completion notice wakes an idle parent or queues behind in-flight work. Each notice lists any other tracked children still working, so the parent can continue work or call `yield_to_subsessions` again at the next join point. Further notices arrive automatically; do not poll. The notice includes the child's final output when it fits. If that output is too long, PI WEB omits it entirely instead of adding a truncated duplicate to the parent's context and directs the parent to retrieve it with `check_subsession`. -`list_subsessions`, `check_subsession`, and `read_subsession` never yield or change control flow. They are for deliberate inspection or recovery, not completion polling. While a child works, agent-facing `check_subsession` and `read_subsession` withhold partial output and direct the parent to continue independent work or yield at the join point. Output becomes available when the child stops. In notices and inspection results, PI WEB guidance precedes a labeled marker and the child output or transcript always comes last. +`list_subsessions`, `check_subsession`, and `read_subsession` never yield or change control flow. They are for deliberate inspection or recovery, not completion polling. While a child works, agent-facing `check_subsession` and `read_subsession` withhold partial output and direct the parent to continue independent work or yield at the join point. Output becomes available when the child stops. Included output and transcripts follow a labeled marker and come last, after PI WEB guidance. In **Settings → Session daemon**, these keys are saved on the selected machine. Restart the session daemon on that machine after changing them. diff --git a/src/server/sessions/piSessionService.spawnSubsession.test.ts b/src/server/sessions/piSessionService.spawnSubsession.test.ts index 2c8a69a..1eb11b3 100644 --- a/src/server/sessions/piSessionService.spawnSubsession.test.ts +++ b/src/server/sessions/piSessionService.spawnSubsession.test.ts @@ -782,6 +782,9 @@ describe("PiSessionService", () => { it("notifies the parent once when the tracked child stops working", async () => { const { parent, child, service } = subsessionService({ allowed: true, cwd: "/workspace-feature" }); + child.session.sessionManager.getBranch = () => [ + { type: "message", message: { role: "assistant", content: "all done" } }, + ]; await service.start("/workspace"); await service.spawnSubsession({ spawningCwd: "/workspace", parentSessionId: "parent-1", parentSessionFile: "/tmp/parent-1.jsonl", prompt: "go", cwd: "/workspace-feature" }); parent.calls.prompt.length = 0; // ignore the spawn prompt to the child; focus on the parent notification @@ -795,12 +798,36 @@ describe("PiSessionService", () => { expect(parent.calls.sendCustomMessage).toHaveLength(1); expect(parent.calls.sendCustomMessage[0]?.message.content).toContain("Subsession child-1 stopped working"); + expect(parent.calls.sendCustomMessage[0]?.message.content).toContain("--- SUBSESSION OUTPUT: child-1 ---\nall done"); expect(parent.calls.sendCustomMessage[0]?.message.customType).toBe("subsession.completion"); expect(parent.calls.sendCustomMessage[0]?.options).toEqual({ triggerTurn: true, deliverAs: "followUp" }); expect(parent.calls.prompt).toHaveLength(0); // not a user-authored message await service.dispose(); }); + it("omits oversized output from the completion notice while keeping it available for inspection", async () => { + const { parent, child, service } = subsessionService({ allowed: true, cwd: "/workspace-feature" }); + const longOutput = `BEGIN_LONG_OUTPUT\n${"x".repeat(2100)}\nEND_LONG_OUTPUT`; + child.session.sessionManager.getBranch = () => [ + { type: "message", message: { role: "assistant", content: longOutput } }, + ]; + await service.start("/workspace"); + await service.spawnSubsession({ spawningCwd: "/workspace", parentSessionId: "parent-1", parentSessionFile: "/tmp/parent-1.jsonl", prompt: "go", cwd: "/workspace-feature" }); + + child.session.isStreaming = true; + child.emit({ type: "agent_start" }); + child.session.isStreaming = false; + child.emit({ type: "agent_end" }); + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect(parent.calls.sendCustomMessage[0]?.message.content).toBe( + "Subsession child-1 stopped working (idle).\nNo other tracked subsessions are working.\n\nOutput from subsession child-1 was too long for this completion notice and was omitted. Call check_subsession with sessionId \"child-1\" to retrieve the final output.", + ); + expect(parent.calls.sendCustomMessage[0]?.message.content).not.toContain("BEGIN_LONG_OUTPUT"); + await expect(service.checkSubsession("parent-1", "child-1", "/tmp/parent-1.jsonl")).resolves.toMatchObject({ finalText: longOutput }); + await service.dispose(); + }); + it("reports other working children in each completion notice", async () => { const { parent, children, service } = subsessionService( { allowed: true, cwd: "/workspace-feature" }, diff --git a/src/server/sessions/piSessionService.ts b/src/server/sessions/piSessionService.ts index cdb7f22..eeaf385 100644 --- a/src/server/sessions/piSessionService.ts +++ b/src/server/sessions/piSessionService.ts @@ -924,12 +924,12 @@ export class PiSessionService implements SessionRouteService { this.subsessionNotifyArmed.set(childId, false); const status: SubsessionStatus = this.activities.get(childId)?.phase === "error" ? "error" : "idle"; const finalText = finalAssistantText(historyMessages(session)); - const preview = finalText === "" ? "(no output)" : truncateForNotification(finalText); + const outputSection = formatSubsessionNotificationOutput(childId, finalText); const workingIds = this.workingSubsessionIds(link.parentSessionId); const next = workingIds.length === 0 ? "No other tracked subsessions are working." : `Still working: ${workingIds.join(", ")}. Continue working, or call yield_to_subsessions alone and last at the next join point. Further completion notices arrive automatically; do not poll.`; - const text = `Subsession ${childId} stopped working (${status}).\n${next}\n\n--- SUBSESSION OUTPUT: ${childId} ---\n${preview}`; + const text = `Subsession ${childId} stopped working (${status}).\n${next}\n\n${outputSection}`; void this.notifyParentOfSubsession(link.parentSessionId, childId, text); } @@ -2489,11 +2489,14 @@ const SUBSESSION_CHILD_LINK_CUSTOM_TYPE = "pi-web.subsession.spawned"; /** customType marking a parent-facing subsession-completion notice. */ const SUBSESSION_NOTIFICATION_CUSTOM_TYPE = "subsession.completion"; -const SUBSESSION_NOTIFICATION_PREVIEW_CHARS = 2000; +const SUBSESSION_NOTIFICATION_MAX_OUTPUT_CHARS = 2000; -function truncateForNotification(text: string): string { - if (text.length <= SUBSESSION_NOTIFICATION_PREVIEW_CHARS) return text; - return `${text.slice(0, SUBSESSION_NOTIFICATION_PREVIEW_CHARS)}…`; +/** Avoid duplicating a partial result in context when deliberate inspection can return the full output. */ +function formatSubsessionNotificationOutput(childSessionId: string, text: string): string { + if (text.length > SUBSESSION_NOTIFICATION_MAX_OUTPUT_CHARS) { + return `Output from subsession ${childSessionId} was too long for this completion notice and was omitted. Call check_subsession with sessionId "${childSessionId}" to retrieve the final output.`; + } + return `--- SUBSESSION OUTPUT: ${childSessionId} ---\n${text === "" ? "(no output)" : text}`; } /** Most recent assistant text from a history message list, or "" if none. */