mirror of
https://github.com/wassname/pi-goals.git
synced 2026-09-25 14:01:03 +08:00
Preserve compact worker evidence
Co-Authored-By: PI/OpenAI <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
@@ -23,17 +23,55 @@ Confirmed. VCC intentionally omits tool-result bodies. The view now adds bounded
|
||||
|
||||
Confirmed for missing history and assistant-derived fallback IDs. The worker binding now persists a disconnect revision and includes it in fallback disconnect IDs. Structured stop IDs remain canonical and deduplicated. The regression test covers two disconnect episodes with unavailable history.
|
||||
|
||||
## Independent-family limitation
|
||||
## OpenRouter Fable review
|
||||
|
||||
The Anthropic review child failed before producing findings:
|
||||
Source: OpenRouter `anthropic/claude-fable-5.1`, medium reasoning, run with `moa` after the user explicitly selected OpenRouter. The first direct-Anthropic child had failed with `credits_required`; it produced no review evidence.
|
||||
|
||||
> `429 ... Usage credits are required for this model ... org_level_disabled_until`
|
||||
Saved answer: `slop/reviews/2026-09-20_claude-fable-5.1_stop-flow-vcc-postfix.answer.md` (generated working artifact, not tracked).
|
||||
|
||||
No cross-family verdict was available. It was not replaced with another route because the requested panel had already been launched as one bounded workflow and silent route substitution would weaken provenance.
|
||||
### Finding 1: a changed Intercom UUID would reject reconnection
|
||||
|
||||
> “If Intercom assigns a fresh UUID on reconnect, a selected review can never be delivered and the worker is detached.”
|
||||
|
||||
Conditional and not changed. `pi-intercom` uses the Pi session ID by default, so reload of the same saved session keeps the UUID. A cloned or replacement Pi session is a different worker identity and must not silently inherit ownership using only the old request ID. Explicit replacement correlation remains required.
|
||||
|
||||
### Finding 2: size validation happened after formal selection
|
||||
|
||||
> “An oversized review throws after the REPORT exists.”
|
||||
|
||||
Confirmed and fixed. The complete Intercom payload is now constructed and checked against 16 KiB before `pi-goals-report` is appended. A regression test supplies a 17 KiB quote and confirms that no formal report is created.
|
||||
|
||||
### Finding 3: disconnect after a structured stop was deduplicated
|
||||
|
||||
> “The supervisor never learns the worker actually left.”
|
||||
|
||||
Confirmed. The original stop stays canonical; a later disconnect now records a separate non-reviewable `receipt` with an episode ID. It does not wake the supervisor or reopen formal review.
|
||||
|
||||
### Finding 4: the cursor advanced past content omitted by size limits
|
||||
|
||||
> “The cursor advances past all rows, making omitted info unavailable from worker_view.”
|
||||
|
||||
Confirmed and fixed. The view now summarizes a chronological prefix that fits the bounded view, advances only through the last represented entry, and states how many newer saved turns remain. Result summaries are no longer silently limited to the last five. A single oversized turn is explicitly marked as partial and remains available in the saved session.
|
||||
|
||||
### Finding 5: child Pi process detection assumed the executable name was `pi`
|
||||
|
||||
> “`comm=` ... is `node`/`bun` for a Pi CLI.”
|
||||
|
||||
Confirmed and fixed conservatively. The summary now counts probable child Pi processes from either the command or a `pi`/`pi-coding-agent` executable path. Only the exact snapshot command is excluded; unrelated `ps` children remain visible.
|
||||
|
||||
### Finding 6: exact JSON serialization made review acknowledgement brittle
|
||||
|
||||
> “Key-order/serialization drift would fail the same way.”
|
||||
|
||||
Confirmed for key order. Saved review verification now compares the review ID, report ID, verdict, content and continuation explicitly. It still requires the parent’s existing draft and exact worker/request/plan ownership; an unsolicited historical review cannot acquire authority.
|
||||
|
||||
### Minor findings
|
||||
|
||||
`supersedes` remains a legacy-compatible field used by pending-report projection but is not written by the current one-event-per-stop path. Empty disconnect session paths were not changed: an attached worker necessarily supplied an absolute saved-session path before its Intercom ID became authoritative.
|
||||
|
||||
## Verification after fixes
|
||||
|
||||
- `npm test`: 137 passed
|
||||
- `npm test`: 140 passed
|
||||
- `npm run typecheck`: passed
|
||||
- `npm run lint`: passed
|
||||
- `git diff --check`: passed
|
||||
|
||||
+13
-4
@@ -441,7 +441,11 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
const stop = branch.filter(entry => entry.type === "custom" && entry.customType === STOP).at(-1);
|
||||
const stopped = stop?.type === "custom" ? stop.data as WorkerStop : undefined;
|
||||
if (run && (!stopped || stopped.entryId !== run.id && !stopped.entryId.startsWith(`${run.id}:`))) { entryId = `${run.id}:${episode}`; kind = "blocker"; }
|
||||
else if (stopped) { entryId = stopped.entryId; text = stopped.text; kind = stopped.kind ?? "unclassified"; }
|
||||
else if (stopped) {
|
||||
entryId = `${stopped.entryId}:${episode}`;
|
||||
text = `Worker disconnected after its recorded ${stopped.kind ?? "unclassified"} event ${stopped.entryId}.`;
|
||||
kind = "receipt";
|
||||
}
|
||||
else {
|
||||
const assistant = branch.filter(entry => entry.type === "message" && entry.message.role === "assistant").at(-1)?.id;
|
||||
if (assistant) entryId = `${assistant}:${episode}`;
|
||||
@@ -476,7 +480,12 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
const report = records<Report>(ctx, REPORT).find(report => report.id === (draft && reviewedReportId(draft)));
|
||||
if (!draft || !report || event.fromSessionId !== report.session || data.requestId !== report.requestId || !records<State>(ctx, STATE).some(saved => saved.worker && saved.worker.parentId === data.to && saved.worker.requestId === report.requestId && saved.worker.intercomId === report.session) || data.plan !== report.plan) return;
|
||||
try {
|
||||
const saved = savedSession(report.sessionFile).getBranch().some(entry => entry.type === "custom" && entry.customType === REVIEW && JSON.stringify(entry.data) === JSON.stringify(draft));
|
||||
const saved = savedSession(report.sessionFile).getBranch().some(entry => {
|
||||
if (entry.type !== "custom" || entry.customType !== REVIEW) return false;
|
||||
const review = entry.data as ReportReview;
|
||||
return review.id === draft.id && reviewedReportId(review) === report.id && review.verdict === draft.verdict
|
||||
&& review.content === draft.content && review.continuation === draft.continuation;
|
||||
});
|
||||
if (saved && !records<ReportReview>(ctx, REVIEW).some(review => review.id === draft.id)) pi.appendEntry(REVIEW, draft);
|
||||
} catch { ctx.ui.notify("Worker review delivery remains unverified; inspect its saved session and retry review_subagent.", "warning"); }
|
||||
return;
|
||||
@@ -992,11 +1001,11 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
});
|
||||
const content = reportReviewContent(report.id, report.sessionFile, sources, params.observation, params.unmet, params.verdict, params.continuation || "");
|
||||
const review: ReportReview = { id: digest(content), reportId: report.id, report: report.id, verdict: params.verdict, content, continuation: params.continuation || "" };
|
||||
const payload = { type: "review", to: report.session, sessionFile: report.sessionFile, requestId: report.requestId, plan: report.plan, review };
|
||||
if (Buffer.byteLength(JSON.stringify(payload)) > 16000) throw new Error("Review exceeds Intercom's 16 KiB limit; shorten the quotes and retain source references.");
|
||||
if (records<ReportReview>(ctx, REVIEW).some(saved => reviewedReportId(saved) === report.id)) return result("This worker stop already has a delivered review; a later stop is a new event.");
|
||||
if (!selected) pi.appendEntry(REPORT, report);
|
||||
if (!channel?.snapshot().connected || !channel.snapshot().supported || signal?.aborted) throw new Error("Review delivery unavailable; the selected stop remains pending.");
|
||||
const payload = { type: "review", to: report.session, sessionFile: report.sessionFile, requestId: report.requestId, plan: report.plan, review };
|
||||
if (Buffer.byteLength(JSON.stringify(payload)) > 16000) throw new Error("Review exceeds Intercom's 16 KiB limit; shorten the quotes and retain source references.");
|
||||
if (!records<ReportReview>(ctx, REVIEW_DRAFT).some(saved => saved.id === review.id)) pi.appendEntry(REVIEW_DRAFT, review);
|
||||
channel.publish(payload, { audience: "capable" });
|
||||
return result("Review sent for saving in the worker conversation. Obligation remains pending until the exact saved review is verified; use /goals status to inspect delivery.");
|
||||
|
||||
+52
-35
@@ -71,7 +71,7 @@ function compactSummary(summary: string): string {
|
||||
if (Buffer.byteLength(summary) <= 5_500) return summary;
|
||||
const head = Buffer.from(summary).subarray(0, 1_800).toString("utf8");
|
||||
const tail = Buffer.from(summary).subarray(-3_500).toString("utf8");
|
||||
return `${head}\n\n[earlier VCC lines omitted]\n\n${tail}`;
|
||||
return `${head}\n\n[part of this single saved turn omitted; inspect the saved session for exact content]\n\n${tail}`;
|
||||
}
|
||||
|
||||
function age(timestamp: string | undefined, now = Date.now()): string {
|
||||
@@ -101,9 +101,9 @@ function outstandingCalls(rows: ReturnType<typeof messageRows>["rows"]) {
|
||||
}
|
||||
|
||||
const CONTROL_RESULTS = new Set(["process", "subagent", "bg_wait", "schedule_task", "manage_scheduled_task"]);
|
||||
function newResultSummaries(rows: ReturnType<typeof messageRows>["rows"], since: number): string[] {
|
||||
function newResultSummaries(rows: ReturnType<typeof messageRows>["rows"], since: number, through: number): string[] {
|
||||
const { calls } = callsAndResults(rows);
|
||||
return rows.slice(since).flatMap(row => {
|
||||
return rows.slice(since, through).flatMap(row => {
|
||||
const message = row.message;
|
||||
if (message.role !== "toolResult") return [];
|
||||
const call = calls.get(message.toolCallId), name = call?.name ?? message.toolName;
|
||||
@@ -112,7 +112,7 @@ function newResultSummaries(rows: ReturnType<typeof messageRows>["rows"], since:
|
||||
const text = raw.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
||||
const outcome = message.isError ? "failed" : "returned";
|
||||
return [`${name} ${outcome}${text ? `: ${text.slice(0, 220)}` : ""}`];
|
||||
}).slice(-5);
|
||||
});
|
||||
}
|
||||
|
||||
export function descendantProcesses(rootPid: number): WorkerProcess[] {
|
||||
@@ -120,7 +120,8 @@ export function descendantProcesses(rootPid: number): WorkerProcess[] {
|
||||
const stdout = execFileSync("ps", ["-eo", "pid=,ppid=,comm=,args="], { encoding: "utf8" });
|
||||
const processes = stdout.trim().split("\n").flatMap(line => {
|
||||
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s*(.*)$/.exec(line);
|
||||
return match && match[3] !== "ps" ? [{ pid: Number(match[1]), ppid: Number(match[2]), command: match[3], args: match[4] }] : [];
|
||||
const ownSnapshot = match?.[3] === "ps" && match[4].includes("-eo pid=,ppid=,comm=,args=");
|
||||
return match && !ownSnapshot ? [{ pid: Number(match[1]), ppid: Number(match[2]), command: match[3], args: match[4] }] : [];
|
||||
});
|
||||
const descendants: WorkerProcess[] = [];
|
||||
const parents = new Set([rootPid]);
|
||||
@@ -145,39 +146,55 @@ export function buildWorkerView(
|
||||
const anchor = sameHistory && previous.through ? rows.findIndex(row => row.id === previous.through) : -1;
|
||||
const since = anchor >= 0 ? anchor + 1 : 0;
|
||||
const fresh = rows.slice(since);
|
||||
const summary = compactSummary(cleanCompile(fresh.map(row => row.message)));
|
||||
const results = newResultSummaries(rows, since);
|
||||
const allSummary = cleanCompile(rows.map(row => row.message));
|
||||
const progress = [section(allSummary, "Files And Changes"), section(allSummary, "Commits")].filter(Boolean).join("\n\n");
|
||||
const progressKey = createHash("sha256").update(progress).digest("hex");
|
||||
const stale = fresh.length && sameHistory && previous.progressKey === progressKey ? previous.stale + 1 : 0;
|
||||
const pending = outstandingCalls(rows);
|
||||
const pendingNames = pending.length ? `${pending.slice(0, 8).map(call => call.name).join(", ")}${pending.length > 8 ? ` (+${pending.length - 8} more)` : ""}` : "none";
|
||||
const processes = runtime.processes;
|
||||
const piChildren = processes?.filter(item => item.command === "pi") ?? [];
|
||||
const piChildren = processes?.filter(item => item.command === "pi" || /(?:^|\/)(?:pi|pi-coding-agent)(?:[\s/]|$)/.test(item.args)) ?? [];
|
||||
const lastTimestamp = rows.at(-1)?.timestamp;
|
||||
const status = runtime.connected === false ? "disconnected" : runtime.status || (runtime.connected ? "connected" : "saved history only");
|
||||
const model = runtime.model ? `${runtime.model}${runtime.contextPct === undefined ? "" : `, ${runtime.contextPct}% context used`}` : "unknown";
|
||||
const background = runtime.processError ? `process snapshot unavailable: ${runtime.processError}`
|
||||
const status = runtime.connected === false ? "disconnected" : (runtime.status || (runtime.connected ? "connected" : "saved history only")).slice(0, 120);
|
||||
const modelName = runtime.model?.slice(0, 160);
|
||||
const model = modelName ? `${modelName}${runtime.contextPct === undefined ? "" : `, ${runtime.contextPct}% context used`}` : "unknown";
|
||||
const background = runtime.processError ? `process snapshot unavailable: ${viewClip(runtime.processError, 300)}`
|
||||
: processes === undefined ? "process snapshot not available"
|
||||
: `${processes.length} child OS process${processes.length === 1 ? "" : "es"}; ${piChildren.length} child Pi process${piChildren.length === 1 ? "" : "es"}`;
|
||||
const lines = [
|
||||
"## Worker view",
|
||||
`Task: ${task.replace(/\s+/g, " ").trim().slice(0, 400) || "unknown"}`,
|
||||
`Status: ${status}; last saved activity ${age(lastTimestamp)} ago`,
|
||||
`Model: ${model}`,
|
||||
`Background: ${background}; unanswered tool calls: ${pending.length ? pending.map(call => call.name).join(", ") : "none"}`,
|
||||
...(stale ? [`Progress: no new file or commit for ${stale} view${stale === 1 ? "" : "s"} with new turns`] : []),
|
||||
"",
|
||||
"### VCC summary of new turns",
|
||||
summary,
|
||||
...(results.length ? ["", "### New result summaries", ...results.map(result => `- ${result}`)] : []),
|
||||
];
|
||||
if (diagnostic) {
|
||||
lines.push("", "### Diagnostics", `Saved session: ${sessionFile}`, `Through entry: ${entries.at(-1)?.id ?? "unknown"}`,
|
||||
`Unanswered calls: ${pending.length ? pending.map(call => `${call.name} (${call.id})`).join(", ") : "none"}`,
|
||||
`Child processes: ${processes?.length ? processes.slice(0, 8).map(item => `${item.pid} ${item.command} ${viewClip(item.args, 120)}`).join("; ") : runtime.processError || "none observed"}`,
|
||||
"Detached queues and jobs are not inferred from the process tree; check their native owner when the saved turns name one.");
|
||||
: `${processes.length} child OS process${processes.length === 1 ? "" : "es"}; ${piChildren.length} probable child Pi process${piChildren.length === 1 ? "" : "es"}`;
|
||||
let consumed = fresh;
|
||||
let text = "";
|
||||
let progressKey = previous?.progressKey ?? createHash("sha256").update("").digest("hex");
|
||||
let stale = 0;
|
||||
for (;;) {
|
||||
const through = since + consumed.length;
|
||||
const compiled = cleanCompile(consumed.map(row => row.message));
|
||||
const summary = compactSummary(compiled);
|
||||
const results = newResultSummaries(rows, since, through);
|
||||
const visibleSummary = cleanCompile(rows.slice(0, through).map(row => row.message));
|
||||
const progress = [section(visibleSummary, "Files And Changes"), section(visibleSummary, "Commits")].filter(Boolean).join("\n\n");
|
||||
progressKey = createHash("sha256").update(progress).digest("hex");
|
||||
stale = consumed.length && sameHistory && previous?.progressKey === progressKey ? previous.stale + 1 : 0;
|
||||
const remaining = fresh.length - consumed.length;
|
||||
const lines = [
|
||||
"## Worker view",
|
||||
`Task: ${task.replace(/\s+/g, " ").trim().slice(0, 400) || "unknown"}`,
|
||||
`Status: ${status}; last saved activity ${age(lastTimestamp)} ago`,
|
||||
`Model: ${model}`,
|
||||
`Background: ${background}; unanswered tool calls: ${pendingNames}`,
|
||||
...(stale ? [`Progress: no new file or commit for ${stale} view${stale === 1 ? "" : "s"} with new turns`] : []),
|
||||
"",
|
||||
"### VCC summary of new turns",
|
||||
summary,
|
||||
...(results.length ? ["", "### New result summaries", ...results.map(result => `- ${result}`)] : []),
|
||||
...(remaining ? ["", `${remaining} newer saved turn${remaining === 1 ? "" : "s"} remain; call worker_view again.`] : []),
|
||||
];
|
||||
if (diagnostic) {
|
||||
lines.push("", "### Diagnostics", `Saved session: ${sessionFile}`, `Summary through entry: ${consumed.at(-1)?.id ?? previous?.through ?? "none"}`, `Latest saved entry: ${entries.at(-1)?.id ?? "unknown"}`,
|
||||
`Unanswered calls: ${pending.length ? `${pending.slice(0, 8).map(call => `${call.name} (${call.id})`).join(", ")}${pending.length > 8 ? ` (+${pending.length - 8} more)` : ""}` : "none"}`,
|
||||
`Child processes: ${processes?.length ? processes.slice(0, 8).map(item => `${item.pid} ${item.command} ${viewClip(item.args, 120)}`).join("; ") : runtime.processError || "none observed"}`,
|
||||
"Detached queues and jobs are not inferred from the process tree; check their native owner when the saved turns name one.");
|
||||
}
|
||||
text = lines.join("\n");
|
||||
if (consumed.length <= 1 || Buffer.byteLength(compiled) <= 5_500 && Buffer.byteLength(text) <= MAX_WORKER_VIEW_BYTES) break;
|
||||
consumed = fresh.slice(0, Math.max(1, Math.floor(consumed.length / 2)));
|
||||
}
|
||||
const text = viewClip(lines.join("\n"), MAX_WORKER_VIEW_BYTES);
|
||||
return { text, cursor: { sessionFile, boundary: current.boundary, through: rows.at(-1)?.id ?? "", turns: rows.length, progressKey, stale } };
|
||||
text = viewClip(text, MAX_WORKER_VIEW_BYTES);
|
||||
const through = consumed.at(-1)?.id ?? (sameHistory ? previous?.through : "") ?? "";
|
||||
return { text, cursor: { sessionFile, boundary: current.boundary, through, turns: since + consumed.length, progressKey, stale } };
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { openProjectPane } from "pi-subagents/project-panes";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import goalsExtension from "../src/index.js";
|
||||
import { goalCheckInWake, planDrafting, reportGoalEventDescription, supervisor } from "../src/prompts.js";
|
||||
import { buildWorkerView } from "../src/worker-view.js";
|
||||
|
||||
vi.mock("pi-subagents/project-panes", () => ({ openProjectPane: vi.fn(async () => ({ ok: true, data: { bindingPath: "/project/.pi/subagents/project-pane.json", disposition: "opened", binding: { paneId: "native-pane", projectRoot: "/project", command: "pi" } } })) }));
|
||||
|
||||
@@ -147,6 +148,23 @@ it("shows incremental VCC Markdown without raw tool results or compaction dumps"
|
||||
expect(diagnostic).not.toContain("RESULT_TAIL_MUST_STAY_HIDDEN");
|
||||
});
|
||||
|
||||
it("paginates oversized worker history without advancing past omitted turns", () => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const entries = Array.from({ length: 48 }, (_, index) => ({
|
||||
type: "message" as const, id: `large-${index}`, parentId: index ? `large-${index - 1}` : null, timestamp,
|
||||
message: { role: "assistant" as const, content: [{ type: "text" as const, text: `TURN_${index} ${String(index).repeat(900)}` }], stopReason: "stop" as const, timestamp: Date.now() },
|
||||
}));
|
||||
const runtime = { connected: true, processes: [{ pid: 7, ppid: 1, command: "node", args: "node /opt/pi-coding-agent/dist/cli.js" }] };
|
||||
let view = buildWorkerView(entries as any, "/tmp/worker.jsonl", "large history", runtime);
|
||||
expect(view.text).toContain("newer saved turns remain");
|
||||
expect(view.text).toContain("1 probable child Pi process");
|
||||
expect(view.cursor.through).not.toBe("large-47");
|
||||
const first = view.cursor.through;
|
||||
for (let page = 0; page < 64 && view.cursor.through !== "large-47"; page++) view = buildWorkerView(entries as any, "/tmp/worker.jsonl", "large history", runtime, view.cursor);
|
||||
expect(view.cursor.through).toBe("large-47");
|
||||
expect(view.cursor.through).not.toBe(first);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["chat", ["new", "attach", "help", "quit"], undefined],
|
||||
["planning", ["edit", "discuss", "ready", "model", "help", "quit"], "📝 planning"],
|
||||
@@ -1242,6 +1260,43 @@ it("records separate disconnect episodes when saved worker history is unavailabl
|
||||
expect(disconnects.map((entry: any) => entry.data.id)).toEqual(["worker-id:disconnect-1", "worker-id:disconnect-2"]);
|
||||
});
|
||||
|
||||
it("records a passive disconnect receipt after an already-visible stop", async () => {
|
||||
const f = fixture(); await f.draft(); await f.command("ready");
|
||||
const sessionFile = join(f.ctx.cwd, "worker.jsonl");
|
||||
const timestamp = new Date().toISOString();
|
||||
writeFileSync(sessionFile, [
|
||||
{ type: "session", version: 3, id: "worker-id", timestamp, cwd: f.ctx.cwd },
|
||||
{ type: "custom", id: "saved-stop", parentId: null, timestamp, customType: "pi-goals-worker-stop", data: { type: "stopped", entryId: "run:completion", to: "parent-intercom", requestId: "placeholder", plan: f.path, text: "Finished output", kind: "completion" } },
|
||||
].map(entry => JSON.stringify(entry)).join("\n") + "\n");
|
||||
await f.launch({ id: "worker-id", sessionFile });
|
||||
const worker = f.entries.at(-1).data.worker;
|
||||
const saved = SessionManager.open(sessionFile);
|
||||
const stop = saved.getBranch().find((entry: any) => entry.customType === "pi-goals-worker-stop") as any;
|
||||
stop.data.requestId = worker.requestId;
|
||||
stop.data.to = worker.parentId;
|
||||
writeFileSync(sessionFile, [saved.getHeader(), stop].map(entry => JSON.stringify(entry)).join("\n") + "\n");
|
||||
const before = f.messages.length;
|
||||
f.event({ type: "session_left", sessionId: "worker-id" });
|
||||
const receipt = f.ctx.sessionManager.getBranch().find((entry: any) => entry.customType === "pi-goals-worker-event" && entry.data.id.includes("run:completion:disconnect-1"));
|
||||
expect(receipt?.data).toMatchObject({ kind: "receipt", text: expect.stringContaining("disconnected after its recorded completion event") });
|
||||
expect(f.messages).toHaveLength(before + 1);
|
||||
expect(f.messages.at(-1)?.options).toEqual({ deliverAs: "nextTurn" });
|
||||
});
|
||||
|
||||
it("rejects an oversized review before selecting formal review", async () => {
|
||||
const f = fixture(); await f.draft(); await f.command("ready");
|
||||
await f.launch({ id: "worker-id", sessionFile: join(f.ctx.cwd, "worker.jsonl") });
|
||||
const worker = f.entries.at(-1).data.worker;
|
||||
const eventId = "worker-id:completion-oversized";
|
||||
f.event({ type: "message", fromSessionId: "worker-id", payload: { type: "stopped", to: worker.parentId, requestId: worker.requestId, plan: f.path, entryId: "completion-oversized", kind: "completion", text: "Potential completion" } });
|
||||
const quote = "q".repeat(17_000); writeFileSync(join(f.ctx.cwd, "proof.txt"), quote);
|
||||
await expect(f.tools.get("review_subagent").execute("review", {
|
||||
eventId, goal: { path: f.path, quote: "goal: first output" }, evidence: [{ path: "proof.txt", quote, observation: "Read exact proof" }],
|
||||
observation: "Inspected proof", unmet: "none", verdict: "accepted", continuation: "",
|
||||
}, undefined, undefined, f.ctx)).rejects.toThrow("16 KiB");
|
||||
expect(f.ctx.sessionManager.getBranch().filter((entry: any) => entry.customType === "pi-goals-report")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("supersedes an inherited worker binding when the supervisor opens a replacement", async () => {
|
||||
const f = fixture(); await f.draft(); await f.command("ready");
|
||||
await f.launch({ id: "old-worker", sessionFile: "/tmp/old-worker.jsonl", task: "Old task" });
|
||||
|
||||
@@ -341,9 +341,11 @@ it("plans and reviews the same worker across failure, delivery retry and reload"
|
||||
const delivered = upkeepNudges.filter(nudge => notes.some(entry => entry.content.includes(nudge)));
|
||||
expect(delivered.length).toBeGreaterThan(1);
|
||||
for (const nudge of delivered) expect(JSON.stringify(requests.parent)).toContain(nudge);
|
||||
await command(worker, "/fixture-reload"); // real shutdown/start after a formal event stays quiet
|
||||
await command(worker, "/fixture-reload"); // reload records liveness without waking or reopening formal review
|
||||
expect(requests.worker).toHaveLength(workerCount);
|
||||
expect(records(parentState.sessionFile, "pi-goals-worker-event")).toHaveLength(reviewedStatusCount);
|
||||
const postReloadEvents = records(parentState.sessionFile, "pi-goals-worker-event");
|
||||
expect(postReloadEvents).toHaveLength(reviewedStatusCount + 1);
|
||||
expect(postReloadEvents.at(-1)).toMatchObject({ kind: "receipt", text: expect.stringContaining("disconnected after its recorded") });
|
||||
expect(records(workerFile, "pi-goals-report-review")).toEqual(savedReviews);
|
||||
const abortAt = worker.messages.length, abortParentAt = parent.messages.length;
|
||||
let releaseAbort!: () => void; const abortedRequest = new Promise<void>(done => { releaseAbort = done; });
|
||||
|
||||
Reference in New Issue
Block a user