mirror of
https://github.com/wassname/pi-plan.git
synced 2026-09-26 14:10:23 +08:00
Let supervisors choose stop reviews
Replace automatic worker review debt with supervisor-selected stop review, restore compact incremental VCC worker views, and remove blank goal-message bubbles.\n\nCo-Authored-By: PI/OpenAI <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
@@ -22,6 +22,7 @@ The supervisor should:
|
||||
- Let the human see and intervene in both worker and supervisor as native Pi panels in Herdr. We keep workers open so the human can check their outputs and final review, usually in the final or penultimate message.
|
||||
|
||||
- Keep supervisor inspection tools. It inspects actual results, delegates implementation and must not weaken the user's goal to accept worker output.
|
||||
- Keep `worker_view` as compact VCC Markdown: summarize current process/subagent presence, do not dump transcripts, raw JSON or repeated compaction, and request detail only when needed. — wassname (Pi wording/spelling edits)
|
||||
- Put all model-facing prompts in `src/prompts.ts`, in conversation order. Preserve the user's verbatim requirements.
|
||||
- `/goals` opens actions. New plan starts a discussion without an objective form. Unknown commands never start planning. A changed settled draft opens the approval dialogue; unchanged discussion does not repeatedly reopen it.
|
||||
- Keep goal titles/status in widgets; omit subtask text. Tasks and evidence remain in the plan.
|
||||
|
||||
@@ -153,7 +153,7 @@ pi
|
||||
|
||||
### Native worker lifecycle and limits
|
||||
|
||||
The parent and worker keep separate native conversations. Worker attachment and stop notices use stock Intercom extension channels; assignments, reports and corrections remain visible Pi messages. Attachment metadata is saved and displayed without requesting a model acknowledgement (at a safe turn boundary when busy); blocked, done and error reports still request supervisor review. A pane-open receipt, idle state or delivery receipt does not approve a goal.
|
||||
The parent and worker keep separate native conversations. Worker attachment and stop notices use stock Intercom extension channels; assignments, reports and corrections remain visible Pi messages. Attachment metadata is saved without requesting a model acknowledgement. A worker stop, blocker or completion is a visible event, not automatic review debt: the supervisor steers or permits an in-flight plan edit directly, and chooses full evidence review only when it may allow the worker to stop. A pane-open receipt, idle state or delivery receipt does not approve a goal.
|
||||
|
||||
Supervisors and workers can use ordinary stock async helpers, with one writer per cwd. These are headless subagents, not additional interactive goals-workers; goal lifecycle hooks stay off even when they inherit forked history. The owning session follows results and failures through stock controls; an optional stock inspector only displays their work. Pausing blocks new owner launch/resume requests while keeping inspection and stop/interrupt available. Already-dispatched workflows may continue until stopped through their owner. <!-- Pi/OpenAI -->
|
||||
|
||||
@@ -165,9 +165,9 @@ Supervisors and workers can use ordinary stock async helpers, with one writer pe
|
||||
|
||||
## Context delivery
|
||||
|
||||
`worker_view` reads the attached worker's history, or the worker's own history. `Ctrl+O` expands bounded Markdown with paired calls/results and background-control references. Review and stop contexts include the same view; routine progress and receipts stay short. Saved launches and watches do not establish current job status: check the native owner before waiting or intervening. This uses VCC's compiler only, without loading its extension runtime. <!-- Pi/OpenAI -->
|
||||
`worker_view` reads the attached worker's saved history, or the worker's own history. Its default is incremental VCC Markdown plus current Intercom model/context status, unanswered calls and a child-process count. Tool calls remain VCC one-line summaries; late, failed and background-control results get bounded one-line outcomes. Raw result bodies, JSON, transcript dumps and repeated compaction summaries stay out of the view. Use `detail: "diagnostic"` for bounded IDs and process commands. Detached queues still require their native owner. <!-- Pi/OpenAI -->
|
||||
|
||||
Routine injected `[pi-goals]` prompts are one compact custom message; `Ctrl+O` expands the exact text, and Pi converts it to the same user-role model input without an empty user bubble. Role-changing transitions retain normal prompt preparation so the new supervisor/worker role applies before the turn. — Pi/OpenAI
|
||||
Routine injected `[pi-goals]` prompts are one compact custom message; `Ctrl+O` expands the exact text. Role-changing transitions remain normal user prompts so the new role applies before the turn, but render as one nonempty compact line. — Pi/OpenAI
|
||||
|
||||
Startup, attachment/resume, session restore, successful compaction and changed requirements restore the active plan above Log at the next ordinary prompt. This includes current preferences and User voice, but leaves historical Log on disk. Routine context and requested reviews quote unfinished or unreviewed goal lines. After eight unchanged turns, the next ordinary prompt carries an upkeep reminder with its reason, those goal lines and the plan path. It omits preferences and role prose; occasional rotating perspective quotations accompany supervision upkeep. Reviewed, cancelled and paused work receives no periodic upkeep; manual ticks remain unreviewed. A fresh plan refresh replaces pending upkeep; edits, pause, exit and session navigation invalidate obsolete reminders. Failed or cancelled compaction does not schedule a refresh. Missing plans are retried. Compaction still uses Pi's configured threshold.
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Review brief: supervisor stop decisions and worker VCC view
|
||||
|
||||
## Question
|
||||
|
||||
Does the uncommitted implementation preserve the intended supervisor workflow and produce a useful compact worker view without hiding state needed for judgment? Find correctness, lifecycle, persistence, identity, and information-loss bugs. Review only; do not edit files.
|
||||
|
||||
## User intent
|
||||
|
||||
> “if a worker tried to stop or change the plan it can 1) steer 2) allow an edit to plan (as long as worker didn't stop) 3) full review see if it should stop. So the paper work is only for the end (or potential end).”
|
||||
|
||||
> “the whole point is to not dump the transcript or json”
|
||||
|
||||
> “The worker view should be compact VCC Markdown: summarize current process/subagent presence; do not dump transcripts, raw JSON or repeated compaction, and request detail only when needed.”
|
||||
|
||||
The supervisor, not the worker event kind, chooses whether formal review is warranted. A recoverable failure should normally get a direct Intercom steer. Acceptance of a formal stop review does not complete a plan goal; `CompleteGoal` remains separate.
|
||||
|
||||
## Changed behavior
|
||||
|
||||
Review flow:
|
||||
|
||||
- Every incoming worker event is first saved as `pi-goals-worker-event`.
|
||||
- `review_request`, `blocker`, `completion`, `aborted`, and `unclassified` wake the supervisor but create no formal report.
|
||||
- `review_subagent(eventId=...)` accepts only those potential-stop events (or an already selected legacy report). After evidence validation it writes `pi-goals-report`, then sends the review. A delivery failure leaves that explicitly selected review pending.
|
||||
- `decision`, progress, waits, receipts, and in-flight plan edits require no review form.
|
||||
|
||||
Worker view:
|
||||
|
||||
- `worker_view` now returns incremental VCC Markdown rather than raw paired tool-call/result dumps.
|
||||
- Intercom supplies connection status, model, context use, and worker PID.
|
||||
- A `ps` snapshot summarizes recursive child processes and exact `pi` children.
|
||||
- VCC keeps compact one-line tool calls and omits raw tool-result bodies.
|
||||
- Native compaction summaries are not printed.
|
||||
- `detail: "diagnostic"` adds bounded IDs/process commands, still without raw results.
|
||||
- A cursor in pi-goals state records turns already shown.
|
||||
|
||||
Blank-message fix included in the same working tree:
|
||||
|
||||
- role-changing prompts render as a compact nonempty normal user message;
|
||||
- passive status messages use one visible custom message;
|
||||
- exact model input remains saved.
|
||||
|
||||
## Evidence and scope
|
||||
|
||||
Read complete relevant files, not only this summary:
|
||||
|
||||
- `AGENTS.md`
|
||||
- `src/index.ts`
|
||||
- `src/prompts.ts`
|
||||
- `src/worker-view.ts`
|
||||
- `src/notice-display.ts`
|
||||
- `test/goals.test.ts`
|
||||
- `test/notice-display.test.ts`
|
||||
- `test/rpc-review.test.ts`
|
||||
- `git diff -- AGENTS.md README.md src test`
|
||||
|
||||
Ignore unrelated uncommitted `RESEARCH_JOURNAL.md` and `docs/human_journal.md`.
|
||||
|
||||
Current local checks: 136 tests passed; typecheck and lint passed before this review brief was written.
|
||||
|
||||
Please prioritize subtle failures over style. In particular, reconstruct whether event deduplication, offline delivery retry, reload, cursor persistence, stale worker identity, missing tool results, process-tree limits, and compact prompt restoration behave as claimed. Distinguish implementation correctness from whether the view is empirically useful.
|
||||
|
||||
— PI/OpenAI
|
||||
@@ -0,0 +1,41 @@
|
||||
# Review findings: stop decisions and worker VCC view
|
||||
|
||||
## Quick oracle
|
||||
|
||||
Source: `pi-quick-oracles` child run `39dbb784-0523-4da7-ab28-8fbadb666f4c`; saved answer:
|
||||
`/home/code/.pi/agent/sessions/--home-code-.pi-agent-git-github.com-wassname-pi-goals--/subagent-artifacts/outputs/bb2764b1-20df-48e2-805d-32e1b1098339/quick-oracle-review.md`
|
||||
|
||||
### Finding 1: compaction could skip unseen turns
|
||||
|
||||
> “The cursor needs an entry identity or compaction-generation anchor, not only a count.”
|
||||
|
||||
Confirmed from the implementation. Fixed by persisting both the compaction boundary ID and the last shown message entry ID. A missing anchor restarts from the new boundary. The regression test now covers view → compaction → twelve new turns → view.
|
||||
|
||||
### Finding 2: a result arriving after its call could disappear
|
||||
|
||||
> “View B gives VCC only the isolated `toolResult`, without the originating call or arguments. The call also vanishes from ‘unanswered tool calls.’”
|
||||
|
||||
Confirmed. VCC intentionally omits tool-result bodies. The view now adds bounded one-line outcomes for late results, failures and background-control results. Raw result bodies and JSON remain omitted. The regression test covers a process result arriving after its call was shown.
|
||||
|
||||
### Finding 3: disconnect episodes could deduplicate forever
|
||||
|
||||
> “Repeated disconnects after the same last assistant entry also reuse the same ID.”
|
||||
|
||||
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
|
||||
|
||||
The Anthropic review child failed before producing findings:
|
||||
|
||||
> `429 ... Usage credits are required for this model ... org_level_disabled_until`
|
||||
|
||||
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.
|
||||
|
||||
## Verification after fixes
|
||||
|
||||
- `npm test`: 137 passed
|
||||
- `npm run typecheck`: passed
|
||||
- `npm run lint`: passed
|
||||
- `git diff --check`: passed
|
||||
|
||||
— PI/OpenAI
|
||||
+57
-48
@@ -52,22 +52,21 @@ import {
|
||||
upkeep,
|
||||
workerAssignment,
|
||||
workerAttachment,
|
||||
workerReview,
|
||||
workerStatus,
|
||||
workerViewDescription,
|
||||
workerViewPresence,
|
||||
workerViewText,
|
||||
workerViewUnavailable,
|
||||
} from "./prompts.js";
|
||||
import { buildWorkerView, viewClip } from "./worker-view.js";
|
||||
import { buildWorkerView, descendantProcesses, viewClip } from "./worker-view.js";
|
||||
|
||||
const STATE = "pi-goals-main-supervisor-v1";
|
||||
const WORKER = "goals-worker";
|
||||
const REPORT = "pi-goals-report", REVIEW = "pi-goals-report-review", REVIEW_DRAFT = "pi-goals-review-draft", REVIEW_REMINDER = "pi-goals-review-reminder";
|
||||
const RUN = "pi-goals-worker-run", STOP = "pi-goals-worker-stop", WORKER_EVENT = "pi-goals-worker-event", WORKER_RELEASE = "pi-goals-worker-release";
|
||||
type GoalEventKind = "review_request" | "decision" | "blocker" | "completion" | "progress" | "running" | "waiting" | "receipt" | "no_change" | "aborted" | "unclassified";
|
||||
const REVIEWABLE_EVENTS = new Set<GoalEventKind>(["review_request", "blocker", "completion"]);
|
||||
const ATTENTION_EVENTS = new Set<GoalEventKind>(["decision", "aborted", "unclassified"]);
|
||||
// Formal review is the supervisor's choice only for a potential stop; steering and in-flight plan edits need no form. — wassname
|
||||
const POTENTIAL_STOP_EVENTS = new Set<GoalEventKind>(["review_request", "blocker", "completion", "aborted", "unclassified"]);
|
||||
const ATTENTION_EVENTS = new Set<GoalEventKind>([...POTENTIAL_STOP_EVENTS, "decision"]);
|
||||
interface WorkerStop { type: "stopped"; entryId: string; to: string; requestId: string; plan: string; text: string; identity: Peer; kind?: GoalEventKind; }
|
||||
interface Report { id: string; plan: string; session: string; sessionFile: string; requestId: string; task?: string; text: string; kind: GoalEventKind; supersedes?: string; }
|
||||
type WorkerEvent = Report;
|
||||
@@ -82,13 +81,14 @@ interface Peer {
|
||||
interface State {
|
||||
mode: Mode;
|
||||
plan?: string;
|
||||
worker?: { sessionFile?: string; intercomId?: string; paneId?: string; requestId?: string; parentId?: string; task?: string; identity?: Peer };
|
||||
worker?: { sessionFile?: string; intercomId?: string; paneId?: string; requestId?: string; parentId?: string; task?: string; identity?: Peer; disconnectRevision?: number };
|
||||
parent?: { intercomId: string; requestId: string };
|
||||
workerStopped?: boolean;
|
||||
pausedFrom?: "solo" | "supervising";
|
||||
finalReview?: { planDigest: string };
|
||||
child?: boolean;
|
||||
pausedCheckIns?: Record<string, string>;
|
||||
workerView?: { sessionFile: string; boundary: string; through: string; turns: number; progressKey: string; stale: number };
|
||||
}
|
||||
interface CheckInTask { id: string; name?: string; action?: string; type?: string; scope?: string; sessionFile?: string; prompt?: string; disabledAt?: string; }
|
||||
const SCHEDULER_SOURCE = fileURLToPath(import.meta.resolve("@jl1990/pi-scheduler/extensions/scheduler/index.ts"));
|
||||
@@ -327,13 +327,11 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
if (triggerTurn) {
|
||||
const prompt = `[pi-goals]\n${content}`;
|
||||
if (prepareRole || preparedRole !== roleKey()) {
|
||||
notices.mirror(prompt);
|
||||
notices.compact(prompt);
|
||||
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
||||
} else notices.prompt(prompt);
|
||||
} else {
|
||||
if (collapse) notices.mirror(content);
|
||||
pi.sendMessage({ customType: "pi-goals-supervision", content, display: !collapse }, { deliverAs: "nextTurn" });
|
||||
}
|
||||
} else if (collapse) notices.passive(content);
|
||||
else pi.sendMessage({ customType: "pi-goals-supervision", content, display: true }, { deliverAs: "nextTurn" });
|
||||
}
|
||||
async function confirmOwnership(ctx: ExtensionContext, target: string, text: string): Promise<boolean> {
|
||||
if (target !== state.plan || state.mode === "chat") { ctx.ui.notify(nativeMessages.externalOwnershipUnknown(target, state.worker), "warning"); return false; }
|
||||
@@ -389,29 +387,23 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
const reportLabel = (report: Report) => {
|
||||
const revision = report.id.split(":").at(-1)!.slice(0, 8);
|
||||
const plainTask = report.task?.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[*_`~<>]/g, "").trim().replace(/\s+/g, " ").slice(0, 100);
|
||||
return `revision ${revision}${plainTask ? ` — ${plainTask}` : ""} (reportId ${report.id})`;
|
||||
return `event ${revision}${plainTask ? ` — ${plainTask}` : ""} (eventId ${report.id})`;
|
||||
};
|
||||
function savedWorkerView(ctx: ExtensionContext, source: { runtimeId?: string; sessionFile?: string; task?: string }, presence?: string) {
|
||||
function savedWorkerView(ctx: ExtensionContext, source: { runtimeId?: string; sessionFile?: string; task?: string }, runtime: Parameters<typeof buildWorkerView>[3] = {}, diagnostic = false) {
|
||||
if (!source.sessionFile) return workerViewUnavailable(workerViewText.noHistory);
|
||||
try {
|
||||
const history = source.runtimeId === ctx.sessionManager.getSessionId() ? ctx.sessionManager : savedSession(source.sessionFile);
|
||||
if (source.runtimeId && history.getSessionId() !== source.runtimeId) return workerViewUnavailable(workerViewText.identityMismatch);
|
||||
return buildWorkerView(history.getBranch(), source.sessionFile, source.task ?? "", presence);
|
||||
const view = buildWorkerView(history.getBranch(), source.sessionFile, source.task ?? "", runtime, state.workerView, diagnostic);
|
||||
state.workerView = view.cursor; save();
|
||||
return view.text;
|
||||
} catch (error) { return workerViewUnavailable(viewClip(String(error), 300)); }
|
||||
}
|
||||
function recordReport(ctx: ExtensionContext, report: Report, wake = true) {
|
||||
if (records<Report>(ctx, REPORT).some(saved => saved.id === report.id)) return;
|
||||
pi.appendEntry(REPORT, report);
|
||||
send(workerReview(report.plan, report.session, `${report.id}\n${report.text}\n\n${savedWorkerView(ctx, { ...report, runtimeId: state.worker?.identity?.sessionId })}`), false, true);
|
||||
if (wake) remindReports(ctx);
|
||||
}
|
||||
function recordWorkerEvent(ctx: ExtensionContext, event: WorkerEvent, wake = true) {
|
||||
if (REVIEWABLE_EVENTS.has(event.kind)) { recordReport(ctx, event, wake); return; }
|
||||
if (records<Report>(ctx, REPORT).some(saved => saved.id === event.id) || records<WorkerEvent>(ctx, WORKER_EVENT).some(saved => saved.id === event.id)) return;
|
||||
pi.appendEntry(WORKER_EVENT, event);
|
||||
const context = ["waiting", "aborted", "unclassified"].includes(event.kind) ? `${event.text}\n\n${savedWorkerView(ctx, { ...event, runtimeId: state.worker?.identity?.sessionId })}` : event.text;
|
||||
const needsDirectAttention = ATTENTION_EVENTS.has(event.kind) && wake;
|
||||
send(workerStatus(event.plan, event.session, event.id, event.kind, context), needsDirectAttention, true);
|
||||
send(workerStatus(event.plan, event.session, event.id, event.kind, event.text), needsDirectAttention, true);
|
||||
}
|
||||
function remindReports(ctx: ExtensionContext) {
|
||||
if (state.child || state.mode !== "supervising") return;
|
||||
@@ -428,7 +420,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
if (expanded) return new Markdown(review.content, 0, 0, getMarkdownTheme());
|
||||
const revision = reviewedReportId(review)?.split(":").at(-1)?.slice(0, 8) ?? "unknown";
|
||||
return {
|
||||
render: (width) => [truncateToWidth(theme.fg("muted", `[pi-goals] Worker review: ${review.verdict} · revision ${revision} · ${keyHint("app.tools.expand", "expand")}`), width)],
|
||||
render: (width) => [truncateToWidth(theme.fg("muted", `[pi-goals] Worker stop review: ${review.verdict} · event ${revision} · ${keyHint("app.tools.expand", "expand")}`), width)],
|
||||
invalidate() {},
|
||||
};
|
||||
});
|
||||
@@ -439,17 +431,23 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
onReady: value => { channel = value; },
|
||||
onEvent: event => {
|
||||
if (event.type === "session_left" && event.sessionId === state.worker?.intercomId && state.plan) {
|
||||
let entryId = "disconnected", text = nativeMessages.disconnected, kind: GoalEventKind = "unclassified";
|
||||
const worker = state.worker;
|
||||
worker.disconnectRevision = (worker.disconnectRevision ?? 0) + 1; save();
|
||||
const episode = `disconnect-${worker.disconnectRevision}`;
|
||||
let entryId = episode, text = nativeMessages.disconnected, kind: GoalEventKind = "unclassified";
|
||||
try {
|
||||
const branch = savedSession(state.worker.sessionFile!).getBranch();
|
||||
const branch = savedSession(worker.sessionFile!).getBranch();
|
||||
const run = branch.filter(entry => entry.type === "custom" && entry.customType === RUN).at(-1);
|
||||
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}:disconnected`; kind = "blocker"; }
|
||||
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 entryId = branch.filter(entry => entry.type === "message" && entry.message.role === "assistant").at(-1)?.id || entryId;
|
||||
else {
|
||||
const assistant = branch.filter(entry => entry.type === "message" && entry.message.role === "assistant").at(-1)?.id;
|
||||
if (assistant) entryId = `${assistant}:${episode}`;
|
||||
}
|
||||
} catch { /* Unknown history remains visible without inventing completion or review debt. */ }
|
||||
recordWorkerEvent(ctx, { id: `${event.sessionId}:${entryId}`, plan: state.plan, session: event.sessionId, sessionFile: state.worker.sessionFile || "", requestId: state.worker.requestId!, task: state.worker.task, text, kind });
|
||||
recordWorkerEvent(ctx, { id: `${event.sessionId}:${entryId}`, plan: state.plan, session: event.sessionId, sessionFile: worker.sessionFile || "", requestId: worker.requestId!, task: worker.task, text, kind });
|
||||
}
|
||||
if (event.type !== "message" || !event.payload || typeof event.payload !== "object") return;
|
||||
const data = event.payload as { type?: string; to?: string; requestId?: string; plan?: string; sessionFile?: string; text?: string; identity?: Peer; entryId?: string; kind?: GoalEventKind; review?: ReportReview };
|
||||
@@ -466,7 +464,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
if (!prior) {
|
||||
pi.appendEntry(REVIEW, review);
|
||||
if (review.verdict === "changes_requested") {
|
||||
notices.hide(review.content);
|
||||
notices.compact(review.content);
|
||||
pi.sendUserMessage(review.content, { deliverAs: "followUp" });
|
||||
}
|
||||
}
|
||||
@@ -501,7 +499,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
}
|
||||
if (data.type === "stopped" && event.fromSessionId === worker.intercomId && typeof data.entryId === "string" && data.entryId && typeof data.text === "string") {
|
||||
const id = `${event.fromSessionId}:${data.entryId}`;
|
||||
if (records<Report>(ctx, REPORT).some(report => report.id === id) || !REVIEWABLE_EVENTS.has(data.kind ?? "unclassified") && records<WorkerEvent>(ctx, WORKER_EVENT).some(saved => saved.id === id)) return;
|
||||
if (records<Report>(ctx, REPORT).some(report => report.id === id) || records<WorkerEvent>(ctx, WORKER_EVENT).some(saved => saved.id === id)) return;
|
||||
if (data.identity) { worker.identity = data.identity; worker.sessionFile = data.identity.sessionFile; save(); }
|
||||
const report: Report = { id, plan: state.plan, session: event.fromSessionId, sessionFile: worker.sessionFile!, requestId: data.requestId, task: worker.task, text: data.text, kind: data.kind ?? "unclassified" };
|
||||
recordWorkerEvent(ctx, report);
|
||||
@@ -537,7 +535,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
const ended = records<WorkerStop>(liveContext, STOP).filter(inRun).at(-1);
|
||||
if (ended) return ended;
|
||||
}
|
||||
const type = automatic || REVIEWABLE_EVENTS.has(kind) ? STOP : WORKER_EVENT;
|
||||
const type = automatic || POTENTIAL_STOP_EVENTS.has(kind) ? STOP : WORKER_EVENT;
|
||||
const entryId = `${runId}:${digest(`${state.parent.requestId}:${state.plan}:${kind}:${text}`)}`;
|
||||
let stopped = records<WorkerStop>(liveContext, type).find(saved => saved.entryId === entryId);
|
||||
if (!stopped) {
|
||||
@@ -730,7 +728,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
`Plan: ${state.plan ?? "none"}`,
|
||||
`Preferred worker model (plan, not configuration): ${notedPlanValue("preferred worker model") ?? "inherit"}`,
|
||||
`Last observed worker model: ${state.worker?.identity?.model ?? "unconfirmed"}; verify current choice before claiming configuration.`,
|
||||
`Pending worker revision reviews: ${pendingReports(ctx).map(reportLabel).join("; ") || "none"}`,
|
||||
`Selected worker-stop reviews pending delivery: ${pendingReports(ctx).map(reportLabel).join("; ") || "none"}`,
|
||||
`Latest worker status event: ${records<WorkerEvent>(ctx, WORKER_EVENT).at(-1)?.kind ?? "none"}`,
|
||||
`Recorded worker session: ${state.worker?.sessionFile ?? "not recorded"}`,
|
||||
`Worker Intercom: ${state.worker?.intercomId ?? "unconfirmed"}; native pane: ${state.worker?.identity?.paneId || state.worker?.paneId || "unconfirmed"}`,
|
||||
@@ -839,22 +837,30 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
});
|
||||
pi.registerTool({
|
||||
name: "worker_view", label: "Worker view", description: workerViewDescription,
|
||||
parameters: Type.Object({}),
|
||||
parameters: Type.Object({ detail: Type.Optional(Type.String({ enum: ["summary", "diagnostic"] })) }),
|
||||
renderResult(output, { expanded }) {
|
||||
const body = output.content.filter(part => part.type === "text").map(part => part.text).join("\n");
|
||||
return new Markdown(expanded ? body : `${body.split("\n").slice(0, 5).join("\n")}\n${keyHint("app.tools.expand", workerViewText.expand)}`, 0, 0, getMarkdownTheme());
|
||||
},
|
||||
async execute(_id, _params, _signal, _update, ctx) {
|
||||
async execute(_id, params, _signal, _update, ctx) {
|
||||
const worker = state.worker, intercomId = worker?.intercomId, stamp = generation;
|
||||
const source = state.child ? { runtimeId: ctx.sessionManager.getSessionId(), sessionFile: ctx.sessionManager.getSessionFile() }
|
||||
: { runtimeId: worker?.identity?.sessionId, sessionFile: worker?.sessionFile, task: worker?.task };
|
||||
let presence: string | undefined;
|
||||
if (!state.child && worker?.intercomId && channel?.snapshot().connected && channel.snapshot().supported) {
|
||||
const peers = await channel.listSessions().catch(() => undefined);
|
||||
if (peers?.some(peer => peer.id === intercomId)) presence = workerViewPresence(new Date().toISOString());
|
||||
const peers = channel?.snapshot().connected && channel.snapshot().supported ? await channel.listSessions().catch(() => undefined) : undefined;
|
||||
const peer = state.child ? peers?.find(peer => peer.pid === process.pid) : peers?.find(peer => peer.id === intercomId);
|
||||
const runtime: Parameters<typeof buildWorkerView>[3] = {
|
||||
connected: peers ? Boolean(peer) : undefined,
|
||||
status: peer?.status,
|
||||
model: peer?.model,
|
||||
contextPct: peer?.contextPct,
|
||||
pid: peer?.pid,
|
||||
};
|
||||
if (peer?.pid) {
|
||||
try { runtime.processes = descendantProcesses(peer.pid); }
|
||||
catch (error) { runtime.processError = viewClip(String(error), 160); }
|
||||
}
|
||||
if (stamp !== generation || worker !== state.worker || intercomId !== state.worker?.intercomId || (!state.child && source.sessionFile !== state.worker?.sessionFile)) return result(messages.cancelled);
|
||||
return result(savedWorkerView(ctx, source, presence));
|
||||
return result(savedWorkerView(ctx, source, runtime, params.detail === "diagnostic"));
|
||||
},
|
||||
});
|
||||
pi.registerTool({
|
||||
@@ -941,22 +947,24 @@ export default function mainSupervisor(pi: ExtensionAPI) {
|
||||
if (!params.summary.trim()) throw new Error("Supply the canonical event summary and exact artifact paths when applicable.");
|
||||
const stopped = reportStop(params.summary.trim(), kind);
|
||||
if (!stopped) throw new Error("No active worker run is available for this event.");
|
||||
return result(REVIEWABLE_EVENTS.has(kind) ? "Recorded one canonical event for parent review." : "Recorded one visible status event without formal review.");
|
||||
return result(POTENTIAL_STOP_EVENTS.has(kind) ? "Recorded one visible stop request for supervisor judgment; no formal review was created." : "Recorded one visible status event without formal review.");
|
||||
},
|
||||
});
|
||||
const sourceQuote = Type.Object({ path: Type.String({ minLength: 1 }), entryId: Type.Optional(Type.String()), quote: Type.String({ minLength: 1 }) });
|
||||
pi.registerTool({
|
||||
name: "review_subagent", label: "Review worker report", description: reportReviewDescription,
|
||||
name: "review_subagent", label: "Review potential worker stop", description: reportReviewDescription,
|
||||
parameters: Type.Object({
|
||||
reportId: Type.String({ minLength: 1 }), goal: sourceQuote,
|
||||
eventId: Type.String({ minLength: 1 }), goal: sourceQuote,
|
||||
evidence: Type.Array(Type.Object({ path: Type.String({ minLength: 1 }), entryId: Type.Optional(Type.String()), quote: Type.Optional(Type.String()), observation: Type.String({ minLength: 1 }) }), { minItems: 1 }),
|
||||
observation: Type.String({ minLength: 1 }), unmet: Type.String({ minLength: 1 }),
|
||||
verdict: Type.String({ enum: ["accepted", "changes_requested", "blocked"] }), continuation: Type.Optional(Type.String()),
|
||||
}),
|
||||
async execute(_id, params, signal, _update, ctx) {
|
||||
if (state.child || state.mode !== "supervising") throw new Error("Only the active parent supervisor can review worker reports.");
|
||||
const report = records<Report>(ctx, REPORT).find(report => report.id === params.reportId);
|
||||
if (!report) throw new Error("Unknown owned report ID; use the reportId shown in /goals status.");
|
||||
if (state.child || state.mode !== "supervising") throw new Error("Only the active parent supervisor can review a potential worker stop.");
|
||||
const selected = records<Report>(ctx, REPORT).find(report => report.id === params.eventId);
|
||||
const event = records<WorkerEvent>(ctx, WORKER_EVENT).find(event => event.id === params.eventId && POTENTIAL_STOP_EVENTS.has(event.kind));
|
||||
const report = selected ?? event;
|
||||
if (!report) throw new Error("Unknown owned potential-stop event ID; use the exact Event shown in the worker status.");
|
||||
if (!["accepted", "changes_requested", "blocked"].includes(params.verdict) || !params.goal.quote.trim() || !params.observation.trim() || !params.unmet.trim() || params.verdict === "changes_requested" && !params.continuation?.trim()) throw new Error("Supply the verdict, inspection, unmet requirements (or none), and a concrete continuation for changes_requested.");
|
||||
const sources = [params.goal, ...params.evidence].map(source => {
|
||||
const gitEvidence = /^git:([0-9a-f]{7,64}):(.+)$/i.exec(source.path);
|
||||
@@ -984,8 +992,9 @@ 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 || "" };
|
||||
if (records<ReportReview>(ctx, REVIEW).some(saved => reviewedReportId(saved) === report.id)) return result("This worker revision already has a delivered review; a later stop report is a new revision.");
|
||||
if (!channel?.snapshot().connected || !channel.snapshot().supported || signal?.aborted) throw new Error("Review delivery unavailable; report remains pending.");
|
||||
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);
|
||||
|
||||
+31
-10
@@ -4,20 +4,28 @@ import { Markdown, truncateToWidth } from "@earendil-works/pi-tui";
|
||||
|
||||
const NOTICE = "pi-goals-notice";
|
||||
const PROMPT = "pi-goals-prompt";
|
||||
const COMPACT = "pi-goals-compact-prompt";
|
||||
|
||||
function noticeLabel(content: string) {
|
||||
return content.includes("\nPlan changed") ? "Plan changed · review requested"
|
||||
: content.includes("## Selected worker-stop reviews") ? "Selected worker-stop reviews"
|
||||
: content.includes("## Worker stop review:") ? "Worker stop review"
|
||||
: content.includes("## Worker status:") ? "Worker status"
|
||||
: "Goal instructions";
|
||||
}
|
||||
|
||||
export function noticeDisplay(pi: ExtensionAPI) {
|
||||
const mirrored = new Set<string>();
|
||||
pi.registerMarkdownTransformer((markdown, context) =>
|
||||
context.messageType === "user" && mirrored.has(markdown) ? "" : markdown);
|
||||
const compacted = new Set<string>();
|
||||
pi.registerMarkdownTransformer((markdown, context) => {
|
||||
if (context.messageType !== "user") return markdown;
|
||||
if (compacted.has(markdown)) return `[pi-goals] ${noticeLabel(markdown)}`;
|
||||
return mirrored.has(markdown) ? "" : markdown;
|
||||
});
|
||||
const render = (content: string, expanded: boolean, theme: { fg(color: string, text: string): string }) => {
|
||||
const label = content.includes("\nPlan changed") ? "Plan changed · review requested"
|
||||
: content.includes("## Worker revision reviews") ? "Worker revisions · review requested"
|
||||
: content.includes("## Worker revision report") ? "Worker revision report"
|
||||
: content.includes("## Worker status:") ? "Worker status"
|
||||
: "Goal instructions";
|
||||
if (expanded) return new Markdown(content, 0, 0, getMarkdownTheme());
|
||||
return {
|
||||
render: (width: number) => [truncateToWidth(theme.fg("muted", `[pi-goals] ${label} · ${keyHint("app.tools.expand", "expand")}`), width)],
|
||||
render: (width: number) => [truncateToWidth(theme.fg("muted", `[pi-goals] ${noticeLabel(content)} · ${keyHint("app.tools.expand", "expand")}`), width)],
|
||||
invalidate() {},
|
||||
};
|
||||
};
|
||||
@@ -27,6 +35,9 @@ export function noticeDisplay(pi: ExtensionAPI) {
|
||||
prompt(content: string) {
|
||||
pi.sendMessage({ customType: PROMPT, content, display: true }, { triggerTurn: true, deliverAs: "followUp" });
|
||||
},
|
||||
passive(content: string) {
|
||||
pi.sendMessage({ customType: PROMPT, content, display: true }, { deliverAs: "nextTurn" });
|
||||
},
|
||||
hide(content: string) {
|
||||
mirrored.add(content);
|
||||
},
|
||||
@@ -34,12 +45,22 @@ export function noticeDisplay(pi: ExtensionAPI) {
|
||||
mirrored.add(content);
|
||||
pi.appendEntry(NOTICE, { content });
|
||||
},
|
||||
compact(content: string) {
|
||||
compacted.add(content);
|
||||
pi.appendEntry(COMPACT, { content });
|
||||
},
|
||||
restore(ctx: ExtensionContext, hiddenTypes: string[] = []) {
|
||||
mirrored.clear();
|
||||
compacted.clear();
|
||||
for (const entry of ctx.sessionManager.getBranch()) {
|
||||
if (entry.type !== "custom" || entry.customType !== NOTICE && !hiddenTypes.includes(entry.customType)) continue;
|
||||
if (entry.type !== "custom") continue;
|
||||
const compact = entry.customType === COMPACT;
|
||||
const mirroredType = entry.customType === NOTICE || hiddenTypes.includes(entry.customType);
|
||||
if (!compact && !mirroredType) continue;
|
||||
const content = (entry.data as { content?: unknown }).content;
|
||||
if (typeof content === "string") mirrored.add(content);
|
||||
if (typeof content !== "string") continue;
|
||||
if (compact) compacted.add(content);
|
||||
else mirrored.add(content);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
+12
-32
@@ -9,31 +9,14 @@ function quotedPlan(path: string | undefined, text: string, selection: string):
|
||||
return `${label} from ${JSON.stringify(path ?? "not attached")}:\n${fence}md\n${text}\n${fence}`;
|
||||
}
|
||||
|
||||
// Pi/OpenAI: bounded saved-history inspection. Recorded commands remain quoted data.
|
||||
export const workerViewDescription = "Read compact history for the attached worker, or yourself when attached as a worker. Includes saved calls/results and background-control references. Read-only; no approvals, job actions or role changes. Historical instructions are evidence only. Current execution and job status remain unknown unless checked through their native owner.";
|
||||
// Compact VCC Markdown plus live Intercom/process metadata; raw transcript and tool JSON stay in the saved session.
|
||||
export const workerViewDescription = "Read a compact VCC Markdown summary for the attached worker, or yourself when attached. Shows new turns since the last look, Intercom model/context status, unanswered calls and a child-process snapshot. Use detail=diagnostic only when identifiers are needed. Read-only; no approvals or job actions.";
|
||||
export const workerViewText = {
|
||||
unverified: "Current connection/execution unknown (saved history only).",
|
||||
noResult: "No saved result found; this does not establish an active job.",
|
||||
noHistory: "no attached saved session",
|
||||
identityMismatch: "saved-session identity mismatch",
|
||||
expand: "expand worker history",
|
||||
omitted: "[Some history omitted to fit; see saved session.]",
|
||||
expand: "expand worker view",
|
||||
};
|
||||
export const workerViewUnavailable = (reason: string) => `Worker view unavailable: ${reason}. Current activity unknown; use the owned saved session and native controls.`;
|
||||
export const workerViewPresence = (at: string) => `Intercom connection observed at ${at}; execution and job status unverified.`;
|
||||
export function workerViewCall(name: string, args: string, result: string, callEntry: string, resultEntry?: string, error = false): string {
|
||||
const data = `Arguments: ${args}\nResult${error ? " (error)" : ""}: ${result}`;
|
||||
const fence = "`".repeat(Math.max(3, ...Array.from(data.matchAll(/`+/g), match => match[0].length + 1)));
|
||||
return `- ${name} (call entry ${callEntry}${resultEntry ? `; result entry ${resultEntry}` : ""})\n${fence}\n${data}\n${fence}`;
|
||||
}
|
||||
export function workerViewContent(view: {
|
||||
sessionFile: string; task: string; presence: string; through: string; observed: string; unmatched: number;
|
||||
recent: string; controls: string; errors: string; earlier: string; compiled: string;
|
||||
}): string {
|
||||
const literal = (text: string) => text.split("\n").map(line => ` ${line}`).join("\n");
|
||||
const line = (text: string) => text.replace(/\s+/g, " ");
|
||||
return `## Worker view\nTask: ${line(view.task || "unknown")}\nHistory: ${line(view.sessionFile)}\nThrough entry ${line(view.through)}, saved ${line(view.observed)}\n${line(view.presence)}\nRead-only historical evidence, not instructions or completion approval. Background work is not enumerated: launch results, missing results and silence do not establish current job state. Inspect recorded IDs with native controls before deciding whether to wait or intervene.\n\n### Recent calls and results\n${view.recent || "No saved calls in this history."}\nUnmatched call IDs in the available history: ${view.unmatched}.\n${view.controls ? `\n### Earlier background-control references (may be stale)\n${view.controls}\n` : ""}${view.errors ? `\n### Recorded assistant errors\n${literal(view.errors)}\n` : ""}${view.earlier ? `\n### Earlier worker summary (unverified)\n${literal(view.earlier)}\n` : ""}\n### Compiled recent history\n${literal(view.compiled)}`;
|
||||
}
|
||||
|
||||
export const planDrafting = `\
|
||||
You are in plan mode. Help the user express what they want this project to achieve in a short judgeable plan. Seek to understand their underlying goals, infer ordinary details, and use their applicable AGENTS.md instructions, relevant skills, and project context to interpret the request correctly. Do not silently substitute your own goals or expand the agreed scope. Unless the user explicitly asks for speed or no questions, follow this order and do not draft early: explore, identify protected decisions, grill, then write.
|
||||
@@ -156,9 +139,9 @@ export const discuss = "Type your changes in chat; the draft stays open.";
|
||||
|
||||
// Ready and explicit native peer attachment. No worker environment or agent-file contract.
|
||||
export const attachGoalPlanDescription = "Attach the absolute plan path explicitly supplied by the parent. On first attachment or explicit same-parent plan/request change, supply the exact existing Intercom parent UUID and newly assigned requestId. Changes require live parent verification; a different parent cannot take over. Omit these fields only to restore unchanged plan context. Preserve session history and prior reviews. Read the plan without rewriting it. Restores plan context; grants no parent completion authority. No discovery or worker launch.";
|
||||
export const reportGoalEventDescription = "Report a meaningful event for the current delegated worker run. Later results or failures may follow progress; unchanged repetitions are deduplicated. review_request, blocker and completion create a formal parent review obligation. decision requests prompt supervisor attention and direct steering without review paperwork. progress, running, waiting, receipt and no_change stay visible without formal review. Use review_request only for a bounded artifact that needs approval; completion only when the assigned task is complete; blocker only when autonomous progress cannot continue and formally allowing the worker to stop may be justified. If a direct steer, retry or restart can continue the work, use decision or progress instead of blocker. Routine intermediate work and queued jobs are progress or waiting.";
|
||||
export const reportGoalEventDescription = "Report one meaningful event for the current delegated worker run. Events remain visible and deduplicated but never create formal review by themselves. Use review_request when asking to stop for approval, completion when the assigned task appears complete, blocker when autonomous progress cannot continue, and decision when parent judgment is needed while work can remain open. The supervisor may steer or permit an in-flight plan edit without a form; only the supervisor can choose full review when allowing a stop may be justified. Routine work and queued jobs are progress or waiting.";
|
||||
const helperGuidance = "Use ordinary stock async helpers when useful, not another interactive goals-worker. Check stock capabilities before launch, including external-CLI runner availability. Keep one writer per cwd or isolated worktree and follow results/failures through the owning session. Supervise only the worker attached to this plan and helpers launched by its owner. Other agents, panes, jobs and schedules are foreign: coordinate when useful, but do not retask, pause, stop, close or review them unless the user explicitly assigns that authority. Pause blocks new owner launch/resume requests; already-dispatched owned workflows may continue, so inspect or stop them through their owner. When tooling, pane, subagent or harness infrastructure fails, inspect the exact native state, understand and fix the cause when practical, and report any remaining loss of visibility or control. Do not claim to wait for a pane unless native status shows that exact pane exists and is closing. Continue unaffected authorized work; a stale binding or unavailable pane need not block a bounded stock helper in an isolated worktree, with the parent retaining goal authority. If the requested model is unavailable, use another model only when the plan or user already approved it and verify the actual model. Infrastructure becomes a blocker only after authorized stock alternatives fail or the fallback would change a protected decision, ownership, spending or the user-visible result. Never silently switch to CLI or foreground fallback.";
|
||||
export const childPlanRole = "You are the delegated implementation worker. Save evidence and report progress for your delegated work; leave plan maintenance to the parent. Preserve agreed goals, requirements and discriminators; the supervisor owns goal-status changes and completion approval. Do not launch a second writer. Call AttachGoalPlan with the explicit plan path in your task before implementation (also after reconnect if unbound). Immediately report your actual Intercom UUID, saved-session path and current provider/model to the supplied supervisor ID. Identify unavailable fields as unknown; do not equate runtime IDs, session filenames and Intercom IDs. Call ReportGoalEvent when there is a meaningful result or status change, including a later blocker or completion after progress. Do not repeat unchanged events. review_request, blocker and completion require formal parent review. decision asks the parent to choose or steer directly without that form. progress, running, waiting, receipt and no_change do not require review; use progress when work changed but the correct instruction is simply to continue. Treat any proposed change to protected project intent, editorial/publication authority, core research design or evaluation principles as a decision, not an ordinary implementation choice. Put the canonical summary and exact artifact paths in the event. When waiting, name the child/job you await, its owner or handle, and what will wake you. Ending a turn while followed work continues is not task completion. Then stay open for live messages. Do not exit or use caller_ping; unsent editor drafts are not visible in model context." + " " + helperGuidance;
|
||||
export const childPlanRole = "You are the delegated implementation worker. Save evidence and report progress for your delegated work; leave plan maintenance to the parent. Preserve agreed goals, requirements and discriminators; the supervisor owns goal-status changes and completion approval. Do not launch a second writer. Call AttachGoalPlan with the explicit plan path in your task before implementation (also after reconnect if unbound). Immediately report your actual Intercom UUID, saved-session path and current provider/model to the supplied supervisor ID. Identify unavailable fields as unknown; do not equate runtime IDs, session filenames and Intercom IDs. Call ReportGoalEvent when there is a meaningful result or status change, including a later blocker or completion after progress. Do not repeat unchanged events. No event creates review paperwork by itself: the parent normally steers, retries or permits an in-flight plan edit, and chooses full review only when it may allow you to stop. Treat any proposed change to protected project intent, editorial/publication authority, core research design or evaluation principles as a decision, not an ordinary implementation choice. Put the canonical summary and exact artifact paths in the event. When waiting, name the child/job you await, its owner or handle, and what will wake you. Ending a turn while followed work continues is not task completion. Then stay open for live messages. Do not exit or use caller_ping; unsent editor drafts are not visible in model context." + " " + helperGuidance;
|
||||
export function readyApproved(workerName: string, planPath: string, notedWorker: string | undefined, plan: string, supervisorId: string): string {
|
||||
const launch = notedWorker
|
||||
? `Inspect recorded history ${notedWorker} and actual writer state. Reuse it when useful. When replacement is the better route, call OpenGoalWorker: stock decides whether a pane is new, and pi-goals preserves/supersedes the recorded binding only for a newly opened replacement. Coordinate possible concurrent writers through normal supervisor judgment and stock controls; pi-goals does not gate generic writer concurrency. Do not bypass worker correlation with raw project.open.`
|
||||
@@ -180,7 +163,7 @@ Reassess your cadence: edit the existing owned check-in, slower for reliable lon
|
||||
const supervisorJob = "Your job is to be an autonomous research partner and supervisor with responsibility for the user's goals. Keep perspective, bring diligence, and use research taste and wisdom to sustain work overnight and keep it on track. Resolve routine implementation decisions yourself; ask the user only when their judgment or authorization is needed. At each check-in, start from the user-visible result, inspect the plan and workers for drift, loops and stuck/stopped/blocked work, and ensure follow-up. Give a busy-reader update in five short fields when something materially changed: Goal, Changed, Judgment, Next, Need from you. Coalesce review and transport details into that update; omit IDs unless they matter. If nothing changed, say so in one line and slow the next check-in for a reliable followed job rather than repeat the recap.";
|
||||
export function supervisor(workerName: string, planPath: string, supervisorId: string): string {
|
||||
return `You are the goal supervisor in the main chat for ${planPath}. ${supervisorJob}\n${waitingGuidance}\nInspect actual artifacts, saved verification, applicable AGENTS.md and skills yourself; delegate implementation to '${workerName}'. Keep authorized work moving to the requested outcome, not merely approval paperwork. Use worker_view for compact saved history. Investigate blocked/waiting/done claims using recent saved tool calls with arguments and results, then current child/job status when needed. History proves a launch or watch at that time, not current liveness. A worker ending its turn may still await work; verify follow-up and change ineffective instructions. Give brief visible assessments with judgment. Infer protected decisions from User voice, applicable instructions and prior choices: publication or editorial approval, the core experiment, evaluation principles, scope and spending are examples, not a fixed list. Put a proposed change to one first, explain its effect and get explicit user approval. You may maintain the plan but must not weaken or change the goal to accept worker output.
|
||||
Use full review_subagent evidence only for completion, a bounded artifact needing approval, or a genuine blocker where formally allowing the worker to stop may be justified. If a status or decision only needs a steer, retry or restart, send that exact instruction to the same worker and keep moving without review paperwork. Tooling and harness recovery serve the goal, not the reverse: diagnose the actual state, fix or raise the defect, then continue through an already authorized stock helper or approved model when ownership and the requested result remain unchanged. Never wait on an inferred or nonexistent pane.
|
||||
When a worker asks to stop or reports completion/blockage, choose among: steer/retry in the same session; permit an in-flight plan edit while work remains open; or use full review_subagent evidence because you may allow the worker to stop. Only your third choice creates review paperwork. Tooling and harness recovery serve the goal, not the reverse: diagnose the actual state, fix or raise the defect, then continue through an already authorized stock helper or approved model when ownership and the requested result remain unchanged. Never wait on an inferred or nonexistent pane.
|
||||
Humour is a reflective meta-learning mechanism, not decoration. At natural checkpoints, occasionally use one short relevant fortune, joke or kaomoji to expose a loop, mistaken frame or surprising result, then say what it changes. Keep it sparse; never put it in formal evidence or force cheerfulness. (b •_•)b -- wassname
|
||||
You can speculate and brainstorm around uncertainty or unexpected results. Label guesses as guesses, consider alternative explanations, and look for a useful way to tell them apart. Keep exploration brief, open-minded and fun: take a step back, play with surprising ideas, question the current framing, and enjoy exploring the broader perspective while staying connected to the agreed goal.
|
||||
Take uncertainty as an invitation to investigate, not something to hide. Have room to play with ideas, question yourself and the worker, and appreciate a good surprise. Investigate surprising results, find mistaken assumptions, make complicated ideas simpler, and disagree usefully rather than agree politely. Keep the work moving without turning supervision into paperwork. A little affectionate teasing is welcome when it fits, and workers can push back too. Keep the humor friendly and the criticism specific. -- Pi/Astra
|
||||
@@ -210,15 +193,12 @@ export function planChangedReview(planPath: string, text = ""): string {
|
||||
export function workerAttachment(plan: string, session: string, text: string): string {
|
||||
return `Worker attachment for ${plan}, exact Intercom session ${session}:\n${text}\nMetadata only; no acknowledgement or review turn requested.`;
|
||||
}
|
||||
// Pi/OpenAI: supervisor-authored report reviews, separate from goal completion.
|
||||
export const reportReviewDescription = "Review one owned formal worker revision: completion, a bounded artifact needing approval, or a genuine blocker where allowing the worker to stop may be justified. Direct steering, retries and restarts use Intercom without this form. After inspecting actual artifacts, use the exact reportId shown in /goals status, not report prose. Quote the assigned goal/task and evidence from files; git:<commit>:<path> reads an immutable tracked revision. An optional saved-session entryId selects decoded message text. State observations and unmet requirements; use accepted, changes_requested or blocked. Changes requested need a concrete continuation. Text quotes are checked, not their relevance or quality. Non-text evidence needs a nonempty capture and specific observation. Delivery stays pending until the worker saves the visible review. Acceptance never completes a goal or wakes/closes the worker.";
|
||||
export const reportReviewContent = (report: string, sessionFile: string, sources: string[], observation: string, unmet: string, verdict: string, continuation: string) => `## Worker review: ${verdict}\n\n- Report: \`${report}\`\n- Saved session: \`${sessionFile}\`\n\n### Assigned goal/task\n\n${sources[0]}\n\n### Evidence\n\n${sources.slice(1).join("\n\n")}\n\n### Review\n\n- Inspected: ${observation}\n- Unmet: ${unmet}\n- Continuation: ${continuation || "none"}\n\nThis is a report review, not CompleteGoal.\n\n— Pi supervisor`;
|
||||
export const pendingReportReviews = (reports: string[]) => `## Formal worker revision reviews\n\nInspect each completion, bounded artifact approval or genuine blocker and its actual artifacts, then use review_subagent with its reportId. Direct steering, retries and restarts use Intercom without this form. Independent authorized work may continue; attachment receipts and ordinary Intercom messages are not review obligations.\n\n${reports.map(report => `- ${report}`).join("\n")}`;
|
||||
export function workerReview(plan: string, session: string, text: string): string {
|
||||
return `[pi-goals: worker review]\n## Worker revision report\n\n- Plan: \`${plan}\`\n- Intercom session: \`${session}\`\n\n### Report\n\n${text}\n\nThis is a report, not completion approval. Inspect actual artifacts and saved messages; if correction is needed, send it to the same session. Preserve its visible review conversation. Respect pauses; do not reply merely to acknowledge.`;
|
||||
}
|
||||
// Pi/OpenAI: the supervisor creates this record only when it may allow a worker to stop.
|
||||
export const reportReviewDescription = "Choose full evidence review for one owned potential-stop event. Use this only after deciding not to steer/retry or permit an in-flight plan edit without paperwork. Inspect actual artifacts, then pass the exact eventId shown in worker status. Quote the assigned goal/task and evidence from files; git:<commit>:<path> reads an immutable tracked revision. An optional saved-session entryId selects decoded message text. State observations and unmet requirements; use accepted, changes_requested or blocked. Text quotes are checked, not their relevance or quality. Non-text evidence needs a nonempty capture and specific observation. Delivery stays pending until the worker saves the visible review. Acceptance never completes a goal or wakes/closes the worker.";
|
||||
export const reportReviewContent = (event: string, sessionFile: string, sources: string[], observation: string, unmet: string, verdict: string, continuation: string) => `## Worker stop review: ${verdict}\n\n- Event: \`${event}\`\n- Saved session: \`${sessionFile}\`\n\n### Assigned goal/task\n\n${sources[0]}\n\n### Evidence\n\n${sources.slice(1).join("\n\n")}\n\n### Review\n\n- Inspected: ${observation}\n- Unmet: ${unmet}\n- Continuation: ${continuation || "none"}\n\nThis is a worker-stop review, not CompleteGoal.\n\n— Pi supervisor`;
|
||||
export const pendingReportReviews = (reports: string[]) => `## Selected worker-stop reviews\n\nThese reviews were explicitly selected but delivery is not yet verified. Inspect the actual artifacts and retry review_subagent with the eventId. Steering, retries and in-flight plan edits do not use this form.\n\n${reports.map(report => `- ${report}`).join("\n")}`;
|
||||
export function workerStatus(plan: string, session: string, eventId: string, kind: string, text: string): string {
|
||||
return `[pi-goals: worker status]\n## Worker status: ${kind}\n\n- Plan: \`${plan}\`\n- Intercom session: \`${session}\`\n- Event: \`${eventId}\`\n\n${text}\n\nVisible diagnostic only; no review_subagent obligation was created.`;
|
||||
return `[pi-goals: worker status]\n## Worker status: ${kind}\n\n- Plan: \`${plan}\`\n- Intercom session: \`${session}\`\n- Event: \`${eventId}\`\n\n${text}\n\nNo formal review was created. Steer or permit an in-flight plan edit directly; use review_subagent with this event only if you may allow the worker to stop.`;
|
||||
}
|
||||
|
||||
export function manualReview(planPath: string, text: string): string {
|
||||
@@ -229,7 +209,7 @@ export function finalReview(planPath: string, text: string): string {
|
||||
}
|
||||
|
||||
// Check-ins. The installed scheduler owns storage/timing/UI; only new default wakes are one line.
|
||||
export const goalCheckInWake = "Goal check-in: only while supervising unfinished goals, start from the user-visible result, read the attached plan and inspect worker_view if available, otherwise the saved worker history. Check for drift, loops and stuck/stopped/blocked work; ensure follow-up. Verify current child/job status when needed; an ended turn may still await work. Directly steer, retry or restart when that is all the work needs; reserve formal review for completion, bounded artifact approval or a genuine accepted blocker. When something materially changed, give a busy-reader update: Goal, Changed, Judgment, Next, Need from you. Coalesce protocol details. If nothing changed, say so in one line and slow the cadence for a reliable followed job. Occasionally use brief relevant humour or a kaomoji to gain perspective, not as decoration. Otherwise do not resume work. Never create a timer from this wake.";
|
||||
export const goalCheckInWake = "Goal check-in: only while supervising unfinished goals, start from the user-visible result, read the attached plan and inspect worker_view if available. Check for drift and stuck/stopped/blocked work; ensure follow-up. A stopped worker normally needs a direct steer, retry or permitted in-flight plan edit. Use formal review only when you choose to allow it to stop. When something materially changed, give a busy-reader update: Goal, Changed, Judgment, Next, Need from you. Coalesce protocol details. If nothing changed, say so in one line and slow the cadence for a reliable followed job. Occasionally use brief relevant humour or a kaomoji to gain perspective, not as decoration. Otherwise do not resume work. Never create a timer from this wake.";
|
||||
export const schedulerMessages = {
|
||||
unconfirmed: "Owned check-in removal unconfirmed: no fresh scheduler result could be observed in this saved session. The request is cancelled; later results will not trigger removal. Inspect /schedules all and use exact owned IDs with /schedule-remove.",
|
||||
unavailable: "Owned check-in removal unavailable: verified @jl1990/pi-scheduler commands are not loaded. No model turn or replacement timer was started. Inspect /schedules all.",
|
||||
|
||||
+170
-76
@@ -1,89 +1,183 @@
|
||||
// Adapted from wassname/pi-intercom-supervisor's view.ts: compiler-only, no worker runtime.
|
||||
// Adapted from wassname/pi-intercom-supervisor's VCC Markdown view; raw history stays in the saved session.
|
||||
|
||||
import { inspect } from "node:util";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
||||
import { compile } from "@sting8k/pi-vcc/src/core/summarize.js";
|
||||
import { workerViewCall, workerViewContent, workerViewText } from "./prompts.js";
|
||||
|
||||
export const MAX_WORKER_VIEW_BYTES = 12_000;
|
||||
// Bound input as well as output. Full arguments/results remain in the linked saved session.
|
||||
export const MAX_WORKER_VIEW_BYTES = 8_000;
|
||||
const RECALL = /\n*-*\n*Use `vcc_recall`[\s\S]*$/;
|
||||
|
||||
export interface WorkerProcess {
|
||||
pid: number;
|
||||
ppid: number;
|
||||
command: string;
|
||||
args: string;
|
||||
}
|
||||
|
||||
export interface WorkerViewRuntime {
|
||||
connected?: boolean;
|
||||
status?: string;
|
||||
model?: string;
|
||||
contextPct?: number;
|
||||
pid?: number;
|
||||
processes?: WorkerProcess[];
|
||||
processError?: string;
|
||||
}
|
||||
|
||||
export interface WorkerViewCursor {
|
||||
sessionFile: string;
|
||||
boundary: string;
|
||||
through: string;
|
||||
turns: number;
|
||||
progressKey: string;
|
||||
stale: number;
|
||||
}
|
||||
|
||||
export function viewClip(text: string, bytes: number): string {
|
||||
if (Buffer.byteLength(text) <= bytes) return text;
|
||||
return Buffer.from(text).subarray(0, bytes - 32).toString("utf8") + "\n[truncated; see history]";
|
||||
return Buffer.from(text).subarray(0, bytes - 32).toString("utf8") + "\n[truncated; see saved session]";
|
||||
}
|
||||
const preview = (value: unknown, bytes: number) => viewClip(inspect(value, { depth: 3, maxArrayLength: 5, maxStringLength: 200, breakLength: Infinity, compact: true }), bytes);
|
||||
// Reserve an omission notice; retain whole blocks newest-first, then restore chronology.
|
||||
function fitBlocks(blocks: string[], bytes: number): string {
|
||||
const complete = blocks.join("\n\n");
|
||||
if (Buffer.byteLength(complete) <= bytes) return complete;
|
||||
const selected: string[] = [];
|
||||
let remaining = bytes - Buffer.byteLength(workerViewText.omitted) - 2;
|
||||
for (const block of [...blocks].reverse()) {
|
||||
const size = Buffer.byteLength(block) + 2;
|
||||
if (size > remaining) continue;
|
||||
selected.unshift(block); remaining -= size;
|
||||
}
|
||||
return [selected.length < blocks.length ? workerViewText.omitted : "", ...selected].filter(Boolean).join("\n\n");
|
||||
}
|
||||
const controls = new Set(["subagent", "process", "bg_wait", "schedule_task", "manage_scheduled_task", "ReportGoalEvent"]);
|
||||
|
||||
export function buildWorkerView(entries: SessionEntry[], sessionFile: string, task: string, presence = workerViewText.unverified): string {
|
||||
function messageRows(entries: SessionEntry[]) {
|
||||
const boundary = entries.map(entry => entry.type).lastIndexOf("compaction");
|
||||
const checkpoint = entries[boundary];
|
||||
const rows = entries.flatMap((entry, index) => entry.type === "message" ? [{ id: entry.id, index, message: entry.message }]
|
||||
: entry.type === "custom_message" && !entry.customType.startsWith("pi-goals-") ? [{ id: entry.id, index, message: { role: "user" as const, content: entry.content, timestamp: Date.parse(entry.timestamp) } }] : []);
|
||||
const kept = checkpoint?.type === "compaction" ? entries.findIndex(entry => entry.id === checkpoint.firstKeptEntryId) : 0;
|
||||
const fresh = rows.filter(row => row.index >= (kept < 0 ? boundary : kept)).slice(-40).map(({ message }) => {
|
||||
if (message.role === "bashExecution") return { role: message.role, command: viewClip(message.command, 2000), output: viewClip(message.output, 4000) };
|
||||
if (!("content" in message)) return { role: message.role };
|
||||
const content = typeof message.content === "string" ? viewClip(message.content, 4000) : message.content.filter(block => block.type === "text" || block.type === "toolCall").slice(-8).map(block =>
|
||||
block.type === "text" ? { type: block.type, text: viewClip(block.text, 4000) }
|
||||
: { type: block.type, id: block.id, name: block.name, arguments: Object.fromEntries(Object.entries(block.arguments).slice(0, 16).map(([key, value]) => [key, typeof value === "string" ? viewClip(value, 2000) : value && typeof value === "object" ? preview(value, 1000) : value])) }); // No raw reasoning or image payloads.
|
||||
return { role: message.role, content, ...(message.role === "toolResult" ? { toolName: message.toolName, toolCallId: message.toolCallId, isError: message.isError } : {}) };
|
||||
});
|
||||
// Keep the prior summary separate: the reference documents VCC's headerless merge loss.
|
||||
let budget = 32_000;
|
||||
const bounded = fresh.reverse().filter(message => { const size = Buffer.byteLength(JSON.stringify(message)); if (size > budget) return false; budget -= size; return true; }).reverse();
|
||||
let compiled = compile({ messages: bounded });
|
||||
const recall = compiled.lastIndexOf("\n\n---\n\nUse `vcc_recall`");
|
||||
if (recall >= 0) compiled = compiled.slice(0, recall);
|
||||
|
||||
type Row = typeof rows[number];
|
||||
type Call = { id: string; name: string; args: unknown; row: Row };
|
||||
const calls = new Map<string, Call>(), results = new Map<string, Row>();
|
||||
for (const row of rows) {
|
||||
const message = row.message;
|
||||
if (message.role === "assistant") for (const block of message.content) {
|
||||
if (block.type === "toolCall") calls.set(block.id, { id: block.id, name: block.name, args: block.arguments, row });
|
||||
const boundaryId = boundary < 0 ? "root" : entries[boundary].id;
|
||||
const rows = entries.slice(boundary + 1).flatMap(entry => {
|
||||
if (entry.type === "message") return [{ id: entry.id, timestamp: entry.timestamp, message: entry.message }];
|
||||
if (entry.type === "custom_message" && !entry.customType.startsWith("pi-goals-")) {
|
||||
return [{ id: entry.id, timestamp: entry.timestamp, message: { role: "user" as const, content: entry.content, timestamp: Date.parse(entry.timestamp) } }];
|
||||
}
|
||||
if (message.role === "toolResult") results.set(message.toolCallId, row);
|
||||
return [];
|
||||
});
|
||||
return { boundary: boundaryId ?? `compaction-${boundary}`, rows };
|
||||
}
|
||||
|
||||
function cleanCompile(messages: unknown[]): string {
|
||||
return compile({ messages }).replace(RECALL, "").trim();
|
||||
}
|
||||
|
||||
function section(summary: string, name: string): string {
|
||||
const start = summary.indexOf(`[${name}]`);
|
||||
if (start < 0) return "";
|
||||
const tail = summary.slice(start);
|
||||
const next = tail.slice(1).search(/\n\[[^\]]+\]/);
|
||||
const separator = tail.indexOf("\n\n---\n\n");
|
||||
const ends = [next < 0 ? -1 : next + 1, separator].filter(index => index > 0);
|
||||
return tail.slice(0, ends.length ? Math.min(...ends) : undefined).trim();
|
||||
}
|
||||
|
||||
function compactSummary(summary: string): string {
|
||||
if (!summary) return "No new saved turns since the last look.";
|
||||
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}`;
|
||||
}
|
||||
|
||||
function age(timestamp: string | undefined, now = Date.now()): string {
|
||||
if (!timestamp) return "unknown";
|
||||
const seconds = Math.max(0, Math.round((now - Date.parse(timestamp)) / 1000));
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
const minutes = Math.round(seconds / 60);
|
||||
return minutes < 60 ? `${minutes}m` : `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, "0")}m`;
|
||||
}
|
||||
|
||||
function callsAndResults(rows: ReturnType<typeof messageRows>["rows"]) {
|
||||
const calls = new Map<string, { name: string; index: number }>();
|
||||
const results = new Set<string>();
|
||||
for (const [index, row] of rows.entries()) {
|
||||
const message = row.message;
|
||||
if (message.role === "assistant" && Array.isArray(message.content)) {
|
||||
for (const block of message.content) if (block.type === "toolCall") calls.set(block.id, { name: block.name, index });
|
||||
}
|
||||
if (message.role === "toolResult") results.add(message.toolCallId);
|
||||
}
|
||||
const ordered = [...calls.values()].sort((a, b) => Math.max(a.row.index, results.get(a.id)?.index ?? -1) - Math.max(b.row.index, results.get(b.id)?.index ?? -1));
|
||||
const render = (call: Call) => {
|
||||
const result = results.get(call.id), message = result?.message;
|
||||
const body = message?.role === "toolResult" ? preview({ content: message.content.filter(block => block.type === "text"), details: message.details }, 700) : workerViewText.noResult;
|
||||
return workerViewCall(viewClip(call.name, 100), preview(call.args, 500), body, viewClip(call.row.id, 100), result && viewClip(result.id, 100), message?.role === "toolResult" && message.isError);
|
||||
};
|
||||
const recent = ordered.slice(-6);
|
||||
// Historical hints, not a job registry: a returned launch is not proof that work finished.
|
||||
const earlierControls = ordered.filter(call => controls.has(call.name) && !recent.includes(call)).slice(-3);
|
||||
const errors = rows.flatMap(row => row.message.role === "assistant" && row.message.errorMessage ? [`${row.id}: ${row.message.errorMessage}`] : []).slice(-3);
|
||||
const view = {
|
||||
sessionFile: viewClip(sessionFile, 1000), task: viewClip(task, 400), presence: viewClip(presence, 400), through: viewClip(entries.at(-1)?.id ?? "unknown", 100),
|
||||
observed: viewClip(entries.at(-1)?.timestamp ?? "unknown", 100), unmatched: [...calls.keys()].filter(id => !results.has(id)).length,
|
||||
recent: "",
|
||||
controls: "",
|
||||
errors: viewClip(errors.join("\n"), 800),
|
||||
earlier: checkpoint?.type === "compaction" ? viewClip(checkpoint.summary, 1000) : "",
|
||||
compiled: Buffer.byteLength(compiled) <= 1600 ? compiled : `${viewClip(compiled, 500)}\n${Buffer.from(compiled).subarray(-1000).toString("utf8")}`,
|
||||
};
|
||||
// Prose excerpts are quoted by the formatter. Never byte-cut assembled Markdown.
|
||||
if (Buffer.byteLength(workerViewContent(view)) > MAX_WORKER_VIEW_BYTES - 6000) {
|
||||
view.earlier = ""; view.compiled = workerViewText.omitted;
|
||||
return { calls, results };
|
||||
}
|
||||
|
||||
function outstandingCalls(rows: ReturnType<typeof messageRows>["rows"]) {
|
||||
const { calls, results } = callsAndResults(rows);
|
||||
return [...calls].flatMap(([id, call]) => results.has(id) ? [] : [{ id, name: call.name }]);
|
||||
}
|
||||
|
||||
const CONTROL_RESULTS = new Set(["process", "subagent", "bg_wait", "schedule_task", "manage_scheduled_task"]);
|
||||
function newResultSummaries(rows: ReturnType<typeof messageRows>["rows"], since: number): string[] {
|
||||
const { calls } = callsAndResults(rows);
|
||||
return rows.slice(since).flatMap(row => {
|
||||
const message = row.message;
|
||||
if (message.role !== "toolResult") return [];
|
||||
const call = calls.get(message.toolCallId), name = call?.name ?? message.toolName;
|
||||
if (!message.isError && call?.index !== undefined && call.index >= since && !CONTROL_RESULTS.has(name)) return [];
|
||||
const raw = typeof message.content === "string" ? message.content : message.content.filter(block => block.type === "text").map(block => block.text).join(" ");
|
||||
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[] {
|
||||
if (!Number.isSafeInteger(rootPid) || rootPid <= 0 || !["darwin", "linux"].includes(process.platform)) return [];
|
||||
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 descendants: WorkerProcess[] = [];
|
||||
const parents = new Set([rootPid]);
|
||||
for (;;) {
|
||||
const found = processes.filter(item => parents.has(item.ppid) && !parents.has(item.pid));
|
||||
if (!found.length) break;
|
||||
for (const item of found) { descendants.push(item); parents.add(item.pid); }
|
||||
}
|
||||
const available = () => MAX_WORKER_VIEW_BYTES - Buffer.byteLength(workerViewContent(view));
|
||||
view.recent = fitBlocks(recent.map(render), Math.min(6000, available() - (earlierControls.length ? 200 : 0)));
|
||||
// Allow for the optional section heading as well as its complete blocks.
|
||||
view.controls = fitBlocks(earlierControls.map(render), Math.min(1600, available() - 100));
|
||||
return workerViewContent(view);
|
||||
return descendants;
|
||||
}
|
||||
|
||||
export function buildWorkerView(
|
||||
entries: SessionEntry[],
|
||||
sessionFile: string,
|
||||
task: string,
|
||||
runtime: WorkerViewRuntime = {},
|
||||
previous?: WorkerViewCursor,
|
||||
diagnostic = false,
|
||||
): { text: string; cursor: WorkerViewCursor } {
|
||||
const current = messageRows(entries), rows = current.rows;
|
||||
const sameHistory = previous?.sessionFile === sessionFile && previous.boundary === current.boundary;
|
||||
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 processes = runtime.processes;
|
||||
const piChildren = processes?.filter(item => item.command === "pi") ?? [];
|
||||
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}`
|
||||
: 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.");
|
||||
}
|
||||
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 } };
|
||||
}
|
||||
|
||||
+54
-38
@@ -90,54 +90,61 @@ it("puts exploration, protected decisions and grilling before the plan draft", (
|
||||
expect(planDrafting).toContain("publication approval or editorial voice");
|
||||
});
|
||||
|
||||
it("reserves formal reviews for approvals, completion and genuine blockers", () => {
|
||||
expect(reportGoalEventDescription).toContain("review_request, blocker and completion create a formal parent review obligation");
|
||||
expect(reportGoalEventDescription).toContain("decision requests prompt supervisor attention and direct steering without review paperwork");
|
||||
it("leaves stop events informal until the supervisor chooses full review", () => {
|
||||
expect(reportGoalEventDescription).toContain("never create formal review by themselves");
|
||||
expect(reportGoalEventDescription).toContain("only the supervisor can choose full review");
|
||||
const role = supervisor("worker", "/tmp/plan.md", "parent");
|
||||
expect(role).toContain("Goal, Changed, Judgment, Next, Need from you");
|
||||
expect(role).toContain("never stopped, retasked, closed or reviewed without explicit user authority");
|
||||
expect(role).toContain("Humour is a reflective meta-learning mechanism");
|
||||
expect(role).toContain("The human can inspect, talk to and change /model in the worker pane directly");
|
||||
expect(role).toContain("pi-goals owns attachment/report correlation, not generic writer concurrency");
|
||||
expect(role).not.toContain("human confirmation");
|
||||
expect(role).not.toContain("replaceStopped");
|
||||
expect(role).toContain("Only your third choice creates review paperwork");
|
||||
expect(role).toContain("Never wait on an inferred or nonexistent pane");
|
||||
expect(goalCheckInWake).toContain("reserve formal review for completion, bounded artifact approval or a genuine accepted blocker");
|
||||
expect(goalCheckInWake).toContain("Use formal review only when you choose to allow it to stop");
|
||||
});
|
||||
|
||||
it("reads bounded worker history without changing it, and expands native Markdown", async () => {
|
||||
it("shows incremental VCC Markdown without raw tool results or compaction dumps", async () => {
|
||||
initTheme("dark");
|
||||
const f = fixture(true), history = f.ctx.sessionManager.getBranch(), timestamp = new Date().toISOString();
|
||||
const entry = (id: string, message: object) => ({ type: "message", id, timestamp, parentId: null, message });
|
||||
history.push(entry("kept", { role: "user", content: "Retained heading requirement" }),
|
||||
{ type: "compaction", id: "checkpoint", timestamp, firstKeptEntryId: "kept", summary: "Earlier output was completed." },
|
||||
{ type: "compaction", id: "checkpoint", timestamp, firstKeptEntryId: "kept", summary: "COMPACTION_DUMP_MUST_STAY_HIDDEN" },
|
||||
entry("failed-call", { role: "assistant", content: [{ type: "thinking", thinking: "PRIVATE_REASONING_SENTINEL" }, { type: "toolCall", id: "read-1", name: "read", arguments: { path: "missing.txt" } }] }),
|
||||
entry("failed-result", { role: "toolResult", toolCallId: "read-1", toolName: "read", isError: true, content: [{ type: "text", text: "Permission denied" }] }),
|
||||
entry("failed-result", { role: "toolResult", toolCallId: "read-1", toolName: "read", isError: true, content: [{ type: "text", text: `Permission denied while reading missing.txt. ${"x".repeat(500)} RESULT_TAIL_MUST_STAY_HIDDEN` }] }),
|
||||
entry("pending-call", { role: "assistant", content: [{ type: "toolCall", id: "job-1", name: "process", arguments: { action: "start", command: "long job", notify: { onSuccess: "turn" }, nested: Array(20).fill({ payload: "x".repeat(100_000) }) } }] }));
|
||||
const before = JSON.stringify(history), published = f.channel.publish.mock.calls.length;
|
||||
const published = f.channel.publish.mock.calls.length;
|
||||
const tool = f.tools.get("worker_view"), output = await tool.execute("view", {}, undefined, undefined, f.ctx), text = output.content[0].text;
|
||||
expect(Buffer.byteLength(text)).toBeLessThanOrEqual(12_000);
|
||||
for (const value of ["Permission denied", "onSuccess", "No saved result", "Retained heading requirement", "Earlier output was completed"]) expect(text).toContain(value);
|
||||
expect(text).not.toContain("PRIVATE_REASONING_SENTINEL"); expect(text).not.toContain("vcc_recall");
|
||||
expect(JSON.stringify(history)).toBe(before); expect(f.channel.publish).toHaveBeenCalledTimes(published);
|
||||
expect(Buffer.byteLength(text)).toBeLessThanOrEqual(8_000);
|
||||
expect(text).toContain("### VCC summary of new turns");
|
||||
expect(text).toContain("* read \"missing.txt\"");
|
||||
expect(text).toContain("unanswered tool calls: process");
|
||||
expect(text).toContain("read failed: Permission denied while reading missing.txt");
|
||||
for (const hidden of ["RESULT_TAIL_MUST_STAY_HIDDEN", "COMPACTION_DUMP_MUST_STAY_HIDDEN", "PRIVATE_REASONING_SENTINEL", "onSuccess", "Recent calls and results", "vcc_recall"]) expect(text).not.toContain(hidden);
|
||||
expect(f.channel.publish).toHaveBeenCalledTimes(published);
|
||||
for (const width of [40, 80]) {
|
||||
const collapsed = tool.renderResult(output, { expanded: false }).render(width), expanded = tool.renderResult(output, { expanded: true }).render(width);
|
||||
expect(expanded.length).toBeGreaterThan(collapsed.length);
|
||||
expect(collapsed.join("\n")).not.toContain("Permission denied"); expect(expanded.join("\n")).toContain("Permission denied");
|
||||
for (const line of expanded) expect(visibleWidth(line)).toBeLessThanOrEqual(width);
|
||||
}
|
||||
// Older verbosity must not hide the newest failure or leave half a fenced block.
|
||||
for (let i = 0; i < 6; i++) history.push(
|
||||
entry(`verbose-call-${i}`, { role: "assistant", content: [{ type: "toolCall", id: `verbose-${i}`, name: "bash", arguments: { command: `diagnostic-${i}`, a: "a".repeat(200), b: "b".repeat(200), c: "c".repeat(200) } }] }),
|
||||
entry(`verbose-result-${i}`, { role: "toolResult", toolCallId: `verbose-${i}`, toolName: "bash", isError: i === 5, content: [{ type: "text", text: i === 5 ? "NEWEST_FAILURE: diagnostic failed" : "Older diagnostic output" }], details: { a: "a".repeat(200), b: "b".repeat(200), c: "c".repeat(200) } }));
|
||||
const fullBefore = JSON.stringify(history), bounded = (await tool.execute("view-again", {}, undefined, undefined, f.ctx)).content[0].text;
|
||||
expect(Buffer.byteLength(bounded)).toBeLessThanOrEqual(12_000);
|
||||
expect(bounded).toContain("NEWEST_FAILURE: diagnostic failed"); expect(bounded).toContain("Some history omitted");
|
||||
const retained = [...bounded.matchAll(/call entry verbose-call-(\d)/g)].map(match => Number(match[1]));
|
||||
expect(retained.at(-1)).toBe(5); expect(retained).toEqual([...retained].sort()); expect(retained.length).toBeLessThan(6);
|
||||
let open: string | undefined;
|
||||
for (const fence of bounded.match(/^`{3,}$/gm) ?? []) { if (open) { expect(fence).toBe(open); open = undefined; } else open = fence; }
|
||||
expect(open).toBeUndefined(); expect(JSON.stringify(history)).toBe(fullBefore);
|
||||
history.push(
|
||||
entry("late-result", { role: "toolResult", toolCallId: "job-1", toolName: "process", isError: false, content: [{ type: "text", text: "Process 1820 exited successfully with saved output." }] }),
|
||||
entry("new-work", { role: "assistant", content: [{ type: "text", text: "Implemented the correction." }, { type: "toolCall", id: "edit-1", name: "edit", arguments: { path: "src/a.ts" } }] }));
|
||||
const next = (await tool.execute("view-again", {}, undefined, undefined, f.ctx)).content[0].text;
|
||||
expect(next).toContain("Implemented the correction");
|
||||
expect(next).toContain("src/a.ts");
|
||||
expect(next).toContain("process returned: Process 1820 exited successfully");
|
||||
expect(next).not.toContain("missing.txt");
|
||||
history.push({ type: "compaction", id: "later-checkpoint", timestamp, summary: "SECOND_COMPACTION_DUMP_MUST_STAY_HIDDEN" });
|
||||
for (let i = 0; i < 12; i++) history.push(entry(`post-compaction-${i}`, { role: "assistant", content: [{ type: "text", text: `POST_COMPACTION_TURN_${i}` }] }));
|
||||
const afterCompaction = (await tool.execute("after-compaction", {}, undefined, undefined, f.ctx)).content[0].text;
|
||||
expect(afterCompaction).toContain("POST_COMPACTION_TURN_0");
|
||||
expect(afterCompaction).toContain("POST_COMPACTION_TURN_11");
|
||||
expect(afterCompaction).not.toContain("SECOND_COMPACTION_DUMP_MUST_STAY_HIDDEN");
|
||||
const diagnostic = (await tool.execute("diagnostic", { detail: "diagnostic" }, undefined, undefined, f.ctx)).content[0].text;
|
||||
expect(diagnostic).toContain("### Diagnostics");
|
||||
expect(diagnostic).toContain("Saved session:");
|
||||
expect(diagnostic).not.toContain("RESULT_TAIL_MUST_STAY_HIDDEN");
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -1164,7 +1171,7 @@ it("passive pause is visible immediately while its model notice waits safely for
|
||||
});
|
||||
|
||||
// The native surface has one project binding; these replace old launch-schema/helper tests.
|
||||
it("opens no-focus, records explicit attachment only, and wakes review only for the exact worker", async () => {
|
||||
it("opens no-focus, records exact-worker stop events without automatic review debt", async () => {
|
||||
const f = fixture(); await f.draft(); await f.command("ready");
|
||||
for (const event of [
|
||||
{ toolName: "intercom", input: { action: "send", cwd: "/tmp/other", openProjectPaneIfMissing: true } },
|
||||
@@ -1193,12 +1200,11 @@ it("opens no-focus, records explicit attachment only, and wakes review only for
|
||||
f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, entryId: undefined } });
|
||||
expect(f.messages).toHaveLength(count);
|
||||
f.event({ type: "message", fromSessionId: "worker-id", payload: notice });
|
||||
expect(f.messages.at(-2)).toMatchObject({ message: { customType: "pi-goals-supervision", display: false, content: expect.stringContaining("## Worker revision report") } });
|
||||
expect(f.messages.at(-2)?.message.content).toContain("Blocked: input missing");
|
||||
expect(f.ctx.sessionManager.getBranch().some((entry: any) => entry.customType === "pi-goals-notice" && entry.data.content.includes("## Worker revision report"))).toBe(true);
|
||||
expect(f.messages.at(-1)?.message.content).toContain("## Formal worker revision reviews");
|
||||
expect(f.messages.at(-1)?.message.content).toContain("cached interruption audit");
|
||||
expect(f.messages.at(-1)?.message.content).not.toContain("](");
|
||||
expect(f.messages.at(-1)?.message.content).toContain("## Worker status: blocker");
|
||||
expect(f.messages.at(-1)?.message.content).toContain("Blocked: input missing");
|
||||
expect(f.messages.at(-1)?.message.content).toContain("No formal review was created");
|
||||
expect(f.ctx.sessionManager.getBranch().filter((entry: any) => entry.customType === "pi-goals-report")).toHaveLength(0);
|
||||
expect(f.ctx.sessionManager.getBranch().some((entry: any) => entry.customType === "pi-goals-worker-event" && entry.data.id.endsWith(":revision-1"))).toBe(true);
|
||||
const afterFirstRevision = f.messages.length;
|
||||
for (const text of ["Done: output.txt", "Error: execution failed"]) f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, text } });
|
||||
expect(f.messages).toHaveLength(afterFirstRevision);
|
||||
@@ -1220,13 +1226,22 @@ it("opens no-focus, records explicit attachment only, and wakes review only for
|
||||
expect(f.ctx.ui.notify.mock.lastCall?.[0]).not.toContain("automatic-stop");
|
||||
expect(readFileSync(f.path, "utf8")).not.toContain("[✓]");
|
||||
await f.command("stop");
|
||||
f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, entryId: "revision-2", text: "New report during pause" } });
|
||||
expect(f.messages.at(-1).options).toEqual({ deliverAs: "nextTurn" });
|
||||
f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, entryId: "revision-2", text: "New stop request during pause" } });
|
||||
expect(f.messages.at(-1)?.message.content).toContain("New stop request during pause");
|
||||
await f.command("clear");
|
||||
const cleared = f.messages.length; f.event({ type: "message", fromSessionId: "worker-id", payload: { ...notice, entryId: "revision-3" } });
|
||||
expect(f.messages).toHaveLength(cleared);
|
||||
});
|
||||
|
||||
it("records separate disconnect episodes when saved worker history is unavailable", async () => {
|
||||
const f = fixture(); await f.draft(); await f.command("ready");
|
||||
await f.launch({ id: "worker-id", sessionFile: "/tmp/missing-worker-history.jsonl" });
|
||||
f.event({ type: "session_left", sessionId: "worker-id" });
|
||||
f.event({ type: "session_left", sessionId: "worker-id" });
|
||||
const disconnects = f.ctx.sessionManager.getBranch().filter((entry: any) => entry.customType === "pi-goals-worker-event" && entry.data.id.includes("disconnect-"));
|
||||
expect(disconnects.map((entry: any) => entry.data.id)).toEqual(["worker-id:disconnect-1", "worker-id:disconnect-2"]);
|
||||
});
|
||||
|
||||
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" });
|
||||
@@ -1374,12 +1389,13 @@ it.each(["inherit", "plan", "explicit"])("hands off %s model policy without clai
|
||||
const view = await f.tools.get("worker_view").execute("view", {}, undefined, undefined, f.ctx);
|
||||
expect(view.content[0].text).toContain("Distinct runtime and Intercom identity evidence");
|
||||
f.event({ type: "message", fromSessionId: "worker", payload: { type: "stopped", to: worker.parentId, requestId: worker.requestId, plan: f.path, entryId: "view-inspection", kind: "unclassified", text: "Inspection ended." } });
|
||||
expect(f.messages.at(-1)?.message.content).toContain("Distinct runtime and Intercom identity evidence");
|
||||
expect(f.messages.at(-1)?.message.content).toContain("Inspection ended.");
|
||||
expect(f.messages.at(-1)?.message.content).not.toContain("Distinct runtime and Intercom identity evidence");
|
||||
if (model) {
|
||||
f.event({ type: "message", fromSessionId: "worker", payload: { type: "stopped", to: worker.parentId, requestId: worker.requestId, plan: f.path, entryId: "model-unavailable", kind: "progress", text: "Requested missing/unavailable is unavailable; unrelated work can continue." } });
|
||||
expect(f.messages.at(-1)?.message.content).toContain("## Worker status: progress");
|
||||
await f.command("status");
|
||||
expect(f.ctx.ui.notify.mock.lastCall?.[0]).toContain("Pending worker revision reviews: none");
|
||||
expect(f.ctx.ui.notify.mock.lastCall?.[0]).toContain("Selected worker-stop reviews pending delivery: none");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -18,6 +18,10 @@ it("collapses mirrored prompts only in the UI, expands the exact text, and resto
|
||||
expect(transform(content, { messageType: "user" })).toBe("");
|
||||
expect(transform(content, { messageType: "assistant" })).toBe(content);
|
||||
expect(transform("[pi-goals]\nA human quotation", { messageType: "user" })).toBe("[pi-goals]\nA human quotation");
|
||||
const rolePrompt = "[pi-goals]\nFor this plan, continue as the supervisor.";
|
||||
display.compact(rolePrompt);
|
||||
expect(pi.appendEntry).toHaveBeenLastCalledWith("pi-goals-compact-prompt", { content: rolePrompt });
|
||||
expect(transform(rolePrompt, { messageType: "user" })).toBe("[pi-goals] Goal instructions");
|
||||
display.prompt(content);
|
||||
expect(pi.sendMessage).toHaveBeenCalledExactlyOnceWith({ customType: "pi-goals-prompt", content, display: true }, { triggerTurn: true, deliverAs: "followUp" });
|
||||
const prompt = { role: "custom", customType: "pi-goals-prompt", content, display: true };
|
||||
@@ -35,24 +39,27 @@ it("collapses mirrored prompts only in the UI, expands the exact text, and resto
|
||||
const expanded = render(entry, { expanded: true }, theme);
|
||||
expect(expanded.render(80)).toEqual(new Markdown(content, 0, 0, getMarkdownTheme()).render(80));
|
||||
|
||||
const review = "[pi-goals]\n## Worker revision reviews\n\n- revision run-1: output ready (reportId worker:run-1)";
|
||||
const review = "[pi-goals]\n## Selected worker-stop reviews\n\n- event run-1: output ready (eventId worker:run-1)";
|
||||
display.mirror(review);
|
||||
const reviewCollapsed = render({ type: "custom", customType: "pi-goals-notice", data: { content: review } }, { expanded: false }, theme);
|
||||
expect(reviewCollapsed.render(80).join("\n")).toContain("Worker revisions · review requested");
|
||||
expect(reviewCollapsed.render(80).join("\n")).toContain("Selected worker-stop reviews");
|
||||
|
||||
const status = "[pi-goals: worker status]\n## Worker status: waiting\n\nPueue 1552 is running.";
|
||||
display.mirror(status);
|
||||
const statusCollapsed = render({ type: "custom", customType: "pi-goals-notice", data: { content: status } }, { expanded: false }, theme);
|
||||
expect(statusCollapsed.render(80).join("\n")).toContain("Worker status");
|
||||
|
||||
const deliveredReview = "## Worker review: changes_requested\n\nCorrect output.txt.";
|
||||
const deliveredReview = "## Worker stop review: changes_requested\n\nCorrect output.txt.";
|
||||
display.hide(deliveredReview);
|
||||
expect(transform(deliveredReview, { messageType: "user" })).toBe("");
|
||||
display.restore({ sessionManager: { getBranch: () => [] } } as unknown as ExtensionContext, ["pi-goals-report-review"]);
|
||||
expect(transform(content, { messageType: "user" })).toBe(content);
|
||||
expect(transform(rolePrompt, { messageType: "user" })).toBe(rolePrompt);
|
||||
const reviewEntry = { type: "custom", customType: "pi-goals-report-review", data: { content: deliveredReview } };
|
||||
display.restore({ sessionManager: { getBranch: () => [entry, reviewEntry] } } as unknown as ExtensionContext, ["pi-goals-report-review"]);
|
||||
const compactEntry = { type: "custom", customType: "pi-goals-compact-prompt", data: { content: rolePrompt } };
|
||||
display.restore({ sessionManager: { getBranch: () => [entry, reviewEntry, compactEntry] } } as unknown as ExtensionContext, ["pi-goals-report-review"]);
|
||||
expect(transform(content, { messageType: "user" })).toBe("");
|
||||
expect(transform(deliveredReview, { messageType: "user" })).toBe("");
|
||||
expect(transform(rolePrompt, { messageType: "user" })).toBe("[pi-goals] Goal instructions");
|
||||
expect(entry.data.content).toBe(content);
|
||||
});
|
||||
|
||||
+40
-38
@@ -130,8 +130,8 @@ it("plans and reviews the same worker across failure, delivery retry and reload"
|
||||
}
|
||||
const entries = (path: string) => readFileSync(path, "utf8").trim().split("\n").map(line => JSON.parse(line));
|
||||
const records = (path: string, type: string) => entries(path).filter(e => e.type === "custom" && e.customType === type).map(e => e.data);
|
||||
async function report(parent: RpcClient, after: number) {
|
||||
const event = await parent.waitFor(m => m.type === "entry_appended" && (m.entry as any)?.customType === "pi-goals-report", after);
|
||||
async function workerEvent(parent: RpcClient, after: number) {
|
||||
const event = await parent.waitFor(m => m.type === "entry_appended" && (m.entry as any)?.customType === "pi-goals-worker-event", after);
|
||||
await parent.waitFor(m => m.type === "agent_settled", parent.messages.indexOf(event));
|
||||
return (event.entry as any).data;
|
||||
}
|
||||
@@ -192,8 +192,8 @@ it("plans and reviews the same worker across failure, delivery retry and reload"
|
||||
expect(JSON.stringify(requests.parent.at(-1)!.messages)).toContain("Worker status: unclassified");
|
||||
const workerState = await state(worker), workerFile = workerState.sessionFile;
|
||||
expect(records(parentState.sessionFile, "pi-goals-worker-event").map(event => event.kind)).toEqual(["receipt", "unclassified"]); // turn end is independently observable
|
||||
const receiptNotice = entries(parentState.sessionFile).find(entry => entry.customType === "pi-goals-notice" && String(entry.data?.content).includes("Attached and waiting."));
|
||||
expect(Buffer.byteLength(receiptNotice.data.content)).toBeLessThan(1500); // routine status does not carry a history dump
|
||||
const receiptNotice = entries(parentState.sessionFile).find(entry => entry.customType === "pi-goals-prompt" && String(entry.content).includes("Attached and waiting."));
|
||||
expect(Buffer.byteLength(receiptNotice.content)).toBeLessThan(1500); // routine status does not carry a history dump
|
||||
// Real native scheduler commands continue after the worker turn. Hold their HTTP
|
||||
// response so inspection observes actual running work, not a fabricated job record.
|
||||
const started = ["followed", "unfollowed"].map(name => once(server, `job-${name}`, { signal: AbortSignal.timeout(8_000) }));
|
||||
@@ -208,9 +208,10 @@ it("plans and reviews the same worker across failure, delivery retry and reload"
|
||||
await run(parent, "parent", call("worker_view", {}));
|
||||
const viewText = (after: number) => (parent.messages.slice(after).find(m => m.type === "tool_execution_end" && m.toolName === "worker_view") as any).result.content.map((part: any) => part.text ?? "").join("\n");
|
||||
const runningView = viewText(viewAt);
|
||||
expect(runningView).toContain("wakeOn"); expect(runningView).toContain("success"); expect(runningView).toContain("never");
|
||||
expect(runningView).toContain("Intercom connection observed"); expect(runningView).toContain("execution and job status unverified");
|
||||
expect(runningView).not.toContain("vcc_recall"); expect(Buffer.byteLength(runningView)).toBeLessThanOrEqual(12_000);
|
||||
expect(runningView).toContain("### VCC summary of new turns");
|
||||
expect(runningView).toContain("Status:"); expect(runningView).toContain("Model:"); expect(runningView).toContain("Background:");
|
||||
expect(runningView).not.toContain("wakeOn"); expect(runningView).not.toContain("Recent calls and results");
|
||||
expect(runningView).not.toContain("vcc_recall"); expect(Buffer.byteLength(runningView)).toBeLessThanOrEqual(8_000);
|
||||
expect(readFileSync(workerFile, "utf8")).toBe(unchangedHistory); expect(readFileSync(planPath, "utf8")).toBe(approved);
|
||||
const quietWorker = requests.worker.length, quietParent = requests.parent.length;
|
||||
const allTasks = () => JSON.parse(readFileSync(join(cwd, "scheduler.json"), "utf8")).tasks;
|
||||
@@ -287,53 +288,54 @@ it("plans and reviews the same worker across failure, delivery retry and reload"
|
||||
} finally { releaseWorker(); }
|
||||
await worker.waitFor(m => m.type === "agent_settled", workerAt);
|
||||
parent = start("parent", parentState.sessionFile); await state(parent);
|
||||
const failure = records(parentState.sessionFile, "pi-goals-report").at(-1);
|
||||
await run(parent, "parent");
|
||||
expect(systemText(requests.parent.at(-1)!)).toContain(failure.id);
|
||||
expect(failure.kind).toBe("blocker"); expect(failure.text).toContain("Fixture execution failed after progress");
|
||||
const failure = records(parentState.sessionFile, "pi-goals-worker-event").find(event => event.kind === "blocker");
|
||||
expect(failure.text).toContain("Fixture execution failed after progress");
|
||||
expect(records(parentState.sessionFile, "pi-goals-report")).toHaveLength(0); // a blocker does not choose review for the supervisor
|
||||
const failedViewAt = parent.messages.length;
|
||||
await run(parent, "parent", call("worker_view", {}));
|
||||
expect(viewText(failedViewAt)).toContain("Fixture execution failed after progress");
|
||||
expect(viewText(failedViewAt)).toContain("greeting.txt");
|
||||
expect(entries(parentState.sessionFile).some(entry => entry.type === "custom_message" && String(entry.content).includes("## Worker view"))).toBe(true);
|
||||
expect(records(parentState.sessionFile, "pi-goals-report")).toHaveLength(1); // receipts/progress/normal stops stayed quiet
|
||||
expect(viewText(failedViewAt)).not.toContain("Recent calls and results");
|
||||
|
||||
// The supervisor steers the recoverable failure directly. No review form is created.
|
||||
const correctionAt = parent.messages.length, correctionWorkerAt = worker.messages.length;
|
||||
const statusCount = records(parentState.sessionFile, "pi-goals-worker-event").length;
|
||||
const correctedEvent = { kind: "completion", summary: `Corrected artifact: ${greeting}` };
|
||||
replies.worker.push(call("write", { path: greeting, content: "hello\n" }), call("read", { path: greeting }), call("ReportGoalEvent", correctedEvent), call("ReportGoalEvent", correctedEvent));
|
||||
await run(parent, "parent", call("intercom", { action: "send", to: workerId, message: "Replace greeting.txt with hello followed by one newline, read it back, and report completion." }));
|
||||
const correction = await workerEvent(parent, correctionAt);
|
||||
await worker.waitFor(m => m.type === "agent_settled", correctionWorkerAt);
|
||||
expect(records(parentState.sessionFile, "pi-goals-worker-event").length).toBeGreaterThan(statusCount);
|
||||
expect(correction.id).not.toBe(failure.id); expect(correction.kind).toBe("completion"); expect(correction.sessionFile).toBe(workerFile);
|
||||
expect(readFileSync(greeting, "utf8")).toBe("hello\n");
|
||||
expect(records(parentState.sessionFile, "pi-goals-report")).toHaveLength(0);
|
||||
|
||||
const git = (...args: string[]) => execFileSync("git", args, { cwd, encoding: "utf8" }).trim();
|
||||
git("init", "--quiet"); git("add", "greeting.txt");
|
||||
git("-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", "commit", "-qm", "Initial artifact");
|
||||
const revision = git("rev-parse", "HEAD");
|
||||
const form = { reportId: failure.id, goal: { path: planPath, quote: "goal: deliver greeting" }, evidence: [{ path: `git:${revision}:greeting.txt`, quote: "helo", observation: "Read the incorrect greeting" }], observation: "The greeting is missing a letter", unmet: "Expected hello", verdict: "changes_requested", continuation: "Replace greeting.txt with hello followed by one newline, read it back, and report the corrected artifact." };
|
||||
await run(parent, "parent", call("read", { path: greeting }), call("review_subagent", { ...form, evidence: [{ ...form.evidence[0], quote: "invented bytes" }] }));
|
||||
expect(records(parentState.sessionFile, "pi-goals-report-review")).toHaveLength(0);
|
||||
await stop(worker); // exact exit, not disappearance; failed delivery must remain pending
|
||||
const form = { eventId: correction.id, goal: { path: planPath, quote: "goal: deliver greeting" }, evidence: [{ path: `git:${revision}:greeting.txt`, quote: "hello", observation: "Read the corrected greeting" }], observation: "Matches the requested greeting", unmet: "none", verdict: "accepted", continuation: "" };
|
||||
await run(parent, "parent", call("review_subagent", { ...form, evidence: [{ ...form.evidence[0], quote: "invented bytes" }] }));
|
||||
expect(records(parentState.sessionFile, "pi-goals-report")).toHaveLength(0); // invalid evidence does not select formal review
|
||||
await stop(worker);
|
||||
await run(parent, "parent", call("review_subagent", form));
|
||||
expect(records(parentState.sessionFile, "pi-goals-report-review")).toHaveLength(0);
|
||||
expect(records(parentState.sessionFile, "pi-goals-report").map(report => report.id)).toEqual([correction.id]);
|
||||
expect(records(parentState.sessionFile, "pi-goals-report-review")).toHaveLength(0); // selected review remains pending while delivery is unavailable
|
||||
worker = start("worker", workerFile); await state(worker);
|
||||
const inspectionAt = parent.messages.length;
|
||||
await run(worker, "worker", call("intercom", { action: "list" }));
|
||||
await parent.waitFor(m => m.type === "entry_appended" && (m.entry as any)?.customType === "pi-goals-worker-event", inspectionAt);
|
||||
const correctionAt = parent.messages.length, correctionWorkerAt = worker.messages.length;
|
||||
const statusCount = records(parentState.sessionFile, "pi-goals-worker-event").length;
|
||||
const correctedEvent = { kind: "review_request", summary: `Corrected artifact: ${greeting}` };
|
||||
replies.worker.push(call("write", { path: greeting, content: "hello\n" }), call("read", { path: greeting }), call("ReportGoalEvent", correctedEvent), call("ReportGoalEvent", correctedEvent));
|
||||
const workerCount = requests.worker.length, reviewAt = worker.messages.length;
|
||||
await run(parent, "parent", call("review_subagent", form));
|
||||
const correction = await report(parent, correctionAt);
|
||||
await worker.waitFor(m => m.type === "agent_settled", correctionWorkerAt);
|
||||
expect(records(parentState.sessionFile, "pi-goals-worker-event")).toHaveLength(statusCount);
|
||||
expect(correction.id).not.toBe(failure.id); expect(correction.sessionFile).toBe(workerFile);
|
||||
expect(readFileSync(greeting, "utf8")).toBe("hello\n");
|
||||
const workerCount = requests.worker.length;
|
||||
await run(parent, "parent", call("read", { path: greeting }), call("review_subagent", { ...form, reportId: correction.id, evidence: [{ path: greeting, quote: "hello", observation: "Read corrected greeting" }], observation: "Matches the requested greeting", unmet: "none", verdict: "accepted", continuation: "" }));
|
||||
await worker.waitFor(message => message.type === "entry_appended" && (message.entry as any)?.customType === "pi-goals-report-review", reviewAt);
|
||||
await command(parent, "/goals status");
|
||||
expect(requests.worker).toHaveLength(workerCount); // acceptance does not wake or close worker
|
||||
expect(worker.process.exitCode).toBeNull(); expect(worker.process.signalCode).toBeNull();
|
||||
const savedReviews = records(workerFile, "pi-goals-report-review");
|
||||
expect(savedReviews.map(r => r.verdict)).toEqual(["changes_requested", "accepted"]);
|
||||
expect(savedReviews.map(r => r.report)).toEqual([failure.id, correction.id]); // old consumers key this wire field
|
||||
const reviewedStatusCount = records(parentState.sessionFile, "pi-goals-worker-event").length;
|
||||
expect(savedReviews.map(r => r.verdict)).toEqual(["accepted"]);
|
||||
expect(savedReviews.map(r => r.report)).toEqual([correction.id]); // retained wire field
|
||||
expect(records(parentState.sessionFile, "pi-goals-report-review")).toEqual(savedReviews);
|
||||
const upkeepAt = requests.parent.length, ordinaryTurns = 17; // two eight-turn periods plus prompt preparation
|
||||
for (let turn = 0; turn < ordinaryTurns; turn++) await run(parent, "parent");
|
||||
expect(requests.parent).toHaveLength(upkeepAt + ordinaryTurns); // nudges join ordinary prompts, never create a turn
|
||||
expect(requests.parent.length).toBeGreaterThan(upkeepAt); // queued status delivery may coalesce ordinary prompts
|
||||
expect(requests.worker).toHaveLength(workerCount);
|
||||
const notes = entries(parentState.sessionFile).filter(entry => entry.type === "custom_message" && entry.customType === "pi-goals-upkeep");
|
||||
const delivered = upkeepNudges.filter(nudge => notes.some(entry => entry.content.includes(nudge)));
|
||||
@@ -341,7 +343,7 @@ it("plans and reviews the same worker across failure, delivery retry and reload"
|
||||
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
|
||||
expect(requests.worker).toHaveLength(workerCount);
|
||||
expect(records(parentState.sessionFile, "pi-goals-worker-event")).toHaveLength(statusCount);
|
||||
expect(records(parentState.sessionFile, "pi-goals-worker-event")).toHaveLength(reviewedStatusCount);
|
||||
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; });
|
||||
@@ -350,10 +352,10 @@ it("plans and reviews the same worker across failure, delivery retry and reload"
|
||||
worker.send({ type: "prompt", id: "interrupted", message: "Wait for the interruption fixture." });
|
||||
try { await abortRequested; worker.send({ type: "abort", id: "abort" }); await worker.waitFor(m => m.type === "agent_settled", abortAt); } finally { releaseAbort(); }
|
||||
await parent.waitFor(m => m.type === "entry_appended" && (m.entry as any)?.customType === "pi-goals-worker-event" && (m.entry as any).data.kind === "aborted", abortParentAt);
|
||||
expect(records(parentState.sessionFile, "pi-goals-report")).toHaveLength(2); // intentional interruption is not another formal review
|
||||
expect(records(parentState.sessionFile, "pi-goals-report")).toHaveLength(1); // intentional interruption is not another formal review
|
||||
await command(parent, "/fixture-legacy-supersession");
|
||||
await run(parent, "parent");
|
||||
expect(systemText(requests.parent.at(-1)!)).not.toContain("Pending worker revision reviews:");
|
||||
expect(systemText(requests.parent.at(-1)!)).not.toContain("Selected worker-stop reviews");
|
||||
expect(readFileSync(planPath, "utf8")).toBe(approved); // reviews never CompleteGoal
|
||||
|
||||
// The Ready-created owned timer survived the existing reconnect/reload story.
|
||||
|
||||
Reference in New Issue
Block a user