Archived
fix(subsessions): omit oversized completion output
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@jmfederico/pi-web": patch
|
||||
---
|
||||
|
||||
Omit oversized tracked-subsession output from parent completion notices, directing the parent to retrieve the full result with `check_subsession` instead of duplicating a truncated preview in context.
|
||||
+5
-3
@@ -629,15 +629,17 @@
|
||||
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
|
||||
<code>yield_to_subsessions</code> 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 <code>check_subsession</code>.
|
||||
</p>
|
||||
<p>
|
||||
<code>list_subsessions</code>, <code>check_subsession</code>, and <code>read_subsession</code> never yield
|
||||
or change control flow. They are for deliberate inspection or recovery, not completion polling. While a
|
||||
child works, agent-facing <code>check_subsession</code> and <code>read_subsession</code> 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.
|
||||
</p>
|
||||
<p>
|
||||
In <strong>Settings → Session daemon</strong>, these keys are saved on the selected machine. Restart the
|
||||
|
||||
+2
-2
@@ -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.
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user