diff --git a/.changeset/yield-to-tracked-subsessions.md b/.changeset/yield-to-tracked-subsessions.md index ebdf265..29e8089 100644 --- a/.changeset/yield-to-tracked-subsessions.md +++ b/.changeset/yield-to-tracked-subsessions.md @@ -2,4 +2,4 @@ "@jmfederico/pi-web": patch --- -Add explicit tracked-subsession yielding, with wake-up notices that identify children still working so parents can continue or yield again without polling or reading partial output. +Add explicit tracked-subsession yielding with no-poll wake-up guidance, remaining-child status, and clear boundaries around child output. diff --git a/docs/config.html b/docs/config.html index d191ec6..7bbc07d 100644 --- a/docs/config.html +++ b/docs/config.html @@ -552,7 +552,8 @@ 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. + 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.

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 3c63b28..34570e3 100644 --- a/docs/config.md +++ b/docs/config.md @@ -189,7 +189,7 @@ At a join point, after finishing its independent work, the parent calls `yield_t 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. -`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. +`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. 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 acd4b74..3b9f678 100644 --- a/src/server/sessions/piSessionService.spawnSubsession.test.ts +++ b/src/server/sessions/piSessionService.spawnSubsession.test.ts @@ -804,7 +804,7 @@ describe("PiSessionService", () => { await new Promise((resolve) => setTimeout(resolve, 20)); expect(parent.calls.sendCustomMessage[0]?.message.content).toBe( - "Subsession child-1 stopped working (idle). Latest output:\n\n(no output)\n\nStill working: child-2. Continue working, or call yield_to_subsessions alone and last at the next join point. Further completion notices arrive automatically; do not poll.", + "Subsession child-1 stopped working (idle).\nStill working: child-2. Continue working, or call yield_to_subsessions alone and last at the next join point. Further completion notices arrive automatically; do not poll.\n\n--- SUBSESSION OUTPUT: child-1 ---\n(no output)", ); second.session.isStreaming = false; @@ -812,7 +812,7 @@ describe("PiSessionService", () => { await new Promise((resolve) => setTimeout(resolve, 20)); expect(parent.calls.sendCustomMessage[1]?.message.content).toBe( - "Subsession child-2 stopped working (idle). Latest output:\n\n(no output)\n\nNo other tracked subsessions are working.", + "Subsession child-2 stopped working (idle).\nNo other tracked subsessions are working.\n\n--- SUBSESSION OUTPUT: child-2 ---\n(no output)", ); await service.dispose(); }); diff --git a/src/server/sessions/piSessionService.ts b/src/server/sessions/piSessionService.ts index 9b28ed9..d14de8c 100644 --- a/src/server/sessions/piSessionService.ts +++ b/src/server/sessions/piSessionService.ts @@ -931,7 +931,7 @@ export class PiSessionService { 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}). Latest output:\n\n${preview}\n\n${next}`; + const text = `Subsession ${childId} stopped working (${status}).\n${next}\n\n--- SUBSESSION OUTPUT: ${childId} ---\n${preview}`; void this.notifyParentOfSubsession(link.parentSessionId, childId, text); } diff --git a/src/server/sessions/spawnSubsessionTool.test.ts b/src/server/sessions/spawnSubsessionTool.test.ts index 853517c..c68396f 100644 --- a/src/server/sessions/spawnSubsessionTool.test.ts +++ b/src/server/sessions/spawnSubsessionTool.test.ts @@ -183,7 +183,7 @@ describe("createSubsessionToolDefinitions", () => { expect(check).toHaveBeenCalledWith("parent-1", "child-1", "/sessions/parent-1.jsonl"); expect(result.details).toMatchObject({ sessionId: "child-1", status: "idle", finalText: "all done" }); - expect(firstText(result.content)).toContain("all done"); + expect(firstText(result.content)).toBe("Subsession child-1 [idle].\n\n--- SUBSESSION OUTPUT: child-1 ---\nall done"); expect(result.terminate).toBeUndefined(); }); @@ -208,7 +208,7 @@ describe("createSubsessionToolDefinitions", () => { const result = await checkTool.execute("call-error-check", { sessionId: "child-1" }, undefined, undefined, ctxFor("parent-1", undefined)); - expect(firstText(result.content)).toBe("Subsession child-1 [error]:\n\nchild failed"); + expect(firstText(result.content)).toBe("Subsession child-1 [error].\n\n--- SUBSESSION OUTPUT: child-1 ---\nchild failed"); expect(result.terminate).toBeUndefined(); }); @@ -224,15 +224,15 @@ describe("createSubsessionToolDefinitions", () => { const read = vi.fn(() => Promise.resolve({ sessionId: "child-1", cwd: "/repos/a", status: "idle" as const, entries: [{ index: 2, role: "assistant" as const, parts: [{ kind: "text" as const, text: "the answer" }] }], - total: 5, matched: 1, start: 2, hasMore: false, + total: 5, matched: 2, start: 2, hasMore: true, })); const { read: readTool } = tools({ read }); - const result = await readTool.execute("call-6", { sessionId: "child-1", roles: ["assistant"], maxChars: 200 }, undefined, undefined, ctxFor("parent-1", "/sessions/parent-1.jsonl")); + const result = await readTool.execute("call-6", { sessionId: "child-1", roles: ["assistant"], maxChars: 200, limit: 1 }, undefined, undefined, ctxFor("parent-1", "/sessions/parent-1.jsonl")); - expect(read).toHaveBeenCalledWith("parent-1", "child-1", { roles: ["assistant"], maxChars: 200 }, "/sessions/parent-1.jsonl"); - expect(result.details).toMatchObject({ sessionId: "child-1", matched: 1 }); - expect(firstText(result.content)).toContain("the answer"); + expect(read).toHaveBeenCalledWith("parent-1", "child-1", { roles: ["assistant"], maxChars: 200, limit: 1 }, "/sessions/parent-1.jsonl"); + expect(result.details).toMatchObject({ sessionId: "child-1", matched: 2 }); + expect(firstText(result.content)).toBe("Subsession child-1 [idle] — messages 2–2 of 5 (2 matched). Earlier matching messages exist before index 2.\n\n--- SUBSESSION TRANSCRIPT: child-1 ---\n#2 assistant\nthe answer"); expect(result.terminate).toBeUndefined(); }); diff --git a/src/server/sessions/spawnSubsessionTool.ts b/src/server/sessions/spawnSubsessionTool.ts index 7995edd..1bb7357 100644 --- a/src/server/sessions/spawnSubsessionTool.ts +++ b/src/server/sessions/spawnSubsessionTool.ts @@ -158,7 +158,7 @@ function renderTranscript(result: SubsessionReadResult): string { ? "no messages matched your filters" : `no messages in this window (${String(result.matched)} matched outside it)`) : `messages ${String(result.start)}–${String(last.index)} of ${String(result.total)} (${String(result.matched)} matched)`; - const more = result.hasMore ? `\n\nEarlier matching messages exist before index ${String(result.start)}.` : ""; + const more = result.hasMore ? ` Earlier matching messages exist before index ${String(result.start)}.` : ""; // Empty entries with matches means the `before` cursor excluded every match // (they all sit at index >= before): the agent paged too far back and should // raise `before` or omit it, not page back further. @@ -167,7 +167,7 @@ function renderTranscript(result: SubsessionReadResult): string { : (result.matched === 0 ? "(no messages matched the filters)" : `(no messages before index ${String(result.start)}; all ${String(result.matched)} matches have later indexes)`); - return `Subsession ${result.sessionId} [${result.status}] — ${range}:\n\n${body}${more}`; + return `Subsession ${result.sessionId} [${result.status}] — ${range}.${more}\n\n--- SUBSESSION TRANSCRIPT: ${result.sessionId} ---\n${body}`; } /** @@ -235,7 +235,7 @@ export function createSubsessionToolDefinitions(spawningCwd: string, deps: Subse const body = result.finalText === "" ? "(no output yet)" : result.finalText; const text = result.status === "working" ? workingInspectionGuidance(result.sessionId) - : `Subsession ${result.sessionId} [${result.status}]:\n\n${body}`; + : `Subsession ${result.sessionId} [${result.status}].\n\n--- SUBSESSION OUTPUT: ${result.sessionId} ---\n${body}`; return { content: [{ type: "text", text }], details: result,