Recover moved supervisors through OpenGoalWorker

Require explicit human takeover confirmation before releasing an inherited or stopped worker binding. Preserve saved history and reviews, then open the replacement through the correlated worker path.\n\nCo-Authored-By: PI/OpenAI <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
wassname
2026-09-20 07:32:11 +08:00
parent e76f388cf9
commit 981c7eceaf
4 changed files with 31 additions and 10 deletions
+1 -1
View File
@@ -159,7 +159,7 @@ Supervisors and workers can use ordinary stock async helpers, with one writer pe
`OpenGoalWorker` supplies startup only to a newly created stock Pi context. An existing live binding receives no message, so opening it does not replace its conversation or editor draft. The new worker calls `AttachGoalPlan`, reports its exact Intercom identity/model/saved-session path, and waits for a direct parent assignment. Revisions use that same session. A model preference is an instruction for agent-led configuration and verification, not a CLI override; later human changes take precedence.
There is no custom fresh/recover operation. Inspect stopped workers' saved history and partial results, preserve drafts/queued input, and confirm the exact writer stopped before stock `project.close`/`project.open`. Stock close checks ownership and idle state, but cannot establish editor-draft safety. If uncertain, retain the pane and inspect it. Preserve history and completed work. Automatic ownership transfer remains unresolved. — Pi/OpenAI
When a moved/cloned supervisor session inherits a worker binding, or a recorded worker has stopped, call `OpenGoalWorker`. It asks the human to confirm that no other supervisor or writer remains active; only then does it release the runtime binding and use stock open for a newly correlated worker. Saved history and reviews remain. Missing panes or roster entries never trigger automatic takeover, and raw `project.open` is not a goals-worker recovery path. — Pi/OpenAI
`/goals attach` now rejects a plan that is not already current in this context, including `attach <path> solo` and reattachment after Clear. The public roster cannot establish its supervisor's ownership; a checkbox or missing roster row is not proof. The command leaves current authority unchanged and provides read-only inspection controls. Keep the original supervisor context when available rather than clearing it to reconnect. Same-current-plan refresh and its separate explicit stopped-writer confirmation for solo recovery remain available. This guard does not solve generic adoption or cross-parent transfer. — Pi/OpenAI
+13 -6
View File
@@ -336,7 +336,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
const stamp = generation;
const revision = workerRevision;
const confirmation = "Worker confirmed stopped";
const choice = await ctx.ui.select("Confirm all other writers for the current and target plans are stopped (inspect Intercom and their native panes). A missing handle is not proof. Take over in this session?", [confirmation, "Cancel"]);
const choice = await ctx.ui.select("Confirm no other supervisor is active and all other writers for the current and target plans are stopped (inspect Intercom and their native panes). A missing handle is not proof. Take over in this session?", [confirmation, "Cancel"]);
if (stamp !== generation || revision !== workerRevision) return false;
if (choice !== confirmation) return false;
if (readFileSync(target, "utf8") !== text) { ctx.ui.notify("Plan changed during takeover; confirm again.", "warning"); return false; }
@@ -857,17 +857,24 @@ export default function mainSupervisor(pi: ExtensionAPI) {
async execute(_id, params, signal, _update, ctx) {
if (state.child || state.mode !== "supervising" || !state.plan) return result(goalToolBlocked(state.mode));
// TODO(2026-11+, Pi): Recheck pi-subagents/project-panes v1's one-pane-per-cwd limit before adding multiple visible workers.
if (opening || state.worker) return result(nativeMessages.alreadyRecorded);
if (opening) return result(nativeMessages.alreadyRecorded);
if (!params.task.trim()) return result(nativeMessages.taskRequired);
const preference = params.model?.trim() || notedPlanValue("preferred worker model");
const model = preference && (preference.includes("/") || !/^(?:none|\(none|default|inherit|not stated)\b/i.test(preference)) ? preference : undefined;
if (!channel?.snapshot().connected || !channel.snapshot().supported) return result(nativeMessages.intercomNotReady);
const stamp = generation, plan = state.plan;
const preflight = generation;
const peers = await channel.listSessions().catch(() => undefined);
if (!peers) return result(nativeMessages.intercomNotReady);
const self = peers.filter(peer => peer.pid === process.pid);
if (self.length !== 1) return result(nativeMessages.noIdentity);
if (stamp !== generation || opening || state.worker || signal?.aborted) return result(messages.cancelled);
if (preflight !== generation || opening || signal?.aborted) return result(messages.cancelled);
if (state.worker) {
const recorded = JSON.stringify(state.worker);
if (!(await confirmOwnership(ctx, state.plan, planText())) || JSON.stringify(state.worker) !== recorded) return result(nativeMessages.alreadyRecorded);
state.worker = undefined; state.workerStopped = true; workerRevision++; generation++; save(); refresh(ctx);
}
const preference = params.model?.trim() || notedPlanValue("preferred worker model");
const model = preference && (preference.includes("/") || !/^(?:none|\(none|default|inherit|not stated)\b/i.test(preference)) ? preference : undefined;
const plan = state.plan;
if (opening || state.worker || signal?.aborted) return result(messages.cancelled);
const requestId = randomUUID();
state.worker = { requestId, parentId: self[0].id, task: params.task }; state.workerStopped = false; workerRevision++; opening = true; save();
try {
+3 -3
View File
@@ -161,7 +161,7 @@ const helperGuidance = "Use ordinary stock async helpers when useful, not anothe
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 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. If live, steer that exact Intercom session; do not replace its conversation. If stopped, preserve history and drafts. Do not bypass worker ownership with project.open; use a bounded stock helper if the recorded pane cannot be safely reused.`
? `Inspect recorded history ${notedWorker} and actual writer state. If live, steer that exact Intercom session; do not replace its conversation. If stopped, preserve history and drafts, then use OpenGoalWorker; its human confirmation releases the old runtime binding before replacement. Do not bypass worker ownership with project.open.`
: `Use OpenGoalWorker with a bounded proposed task for '${workerName}'. It uses stock project.open, not subagent execution. A new worker attaches and waits; after inspecting its report, send the authorized task through exact-session Intercom.`;
return `[pi-goals: approval — Ready]\nReady approved this plan: ${planPath}. Stay here as supervisor. ${launch} Confirm your actual Intercom UUID with status/list; your Pi session ID ${supervisorId} is a distinct field. Await explicit worker attachment and a report with actual Intercom UUID, saved-session path and resolved model. worker_view must show the correlated saved session before assignment. Never create a replacement goals-worker through raw Intercom openProjectPaneIfMissing or subagent project.open: those panes are not parent-owned and their automatic stop events cannot be supervised. Use OpenGoalWorker for an interactive worker, or a bounded stock helper for authorized non-pane work. Inspect results and steer corrections in the same owned session. A receipt, roster row or idle pane is not attachment, writer exit or completion.\n\n${quotedPlan(planPath, foldPlan(plan), "working set before Log")}`;
}
@@ -185,7 +185,7 @@ Humour is a reflective meta-learning mechanism, not decoration. At natural check
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
Use OpenGoalWorker for the native project pane and stock Intercom only for exact-session assignment/report/steering after correlated attachment. Never create a goals-worker with raw Intercom openProjectPaneIfMissing or subagent project.open. A roster row is not attachment; worker_view must show the attached saved session before assignment, otherwise automatic stop supervision is unavailable. Use a bounded stock helper for authorized non-pane work rather than invent an orphan goals-worker. Do not use subagent as a second goals-worker backend. Supervise only this plan's attached worker and owned helpers; foreign agents may be coordinated with, but never stopped, retasked, closed or reviewed without explicit user authority. ${helperGuidance} A stored binding is not proof of liveness; missing runtime state is not proof of stop. Verify actual Intercom identities with list/status; your Pi session ID is ${supervisorId}, a distinct field. Require artifact paths, saved verification and blocker/error reports. When the worker stops for any reason, inspect actual artifacts and saved messages before approving or correcting it in the same open session. A recap or receipt alone sends no instruction and proves no action. Record actual pane identity, '- worker session:' and '- worker intercom session:' with provenance. CompleteGoal belongs only to this parent or explicitly confirmed solo self-verification.
Keep normal tools and honor human model changes. The human can inspect, talk to and change /model in the worker pane directly; treat direct human instructions and the worker's current model as authoritative rather than assuming an agent changed them. Do not revert either unless the human asks. Inherit by default. If the user supplies a model preference to the supervisor, pass it explicitly to the agent through OpenGoalWorker's model instruction or exact-session Intercom steering; let the agent configure it through supported controls and verify its actual choice. project.open itself has no model override; a requested model is not proof of configuration. Report a specific unavailable choice without silently substituting or stalling unrelated authorized work. After compaction reread the plan. Lost connection or exhausted credits does not erase work. Preserve drafts and saved sessions; confirm other writers stopped before solo takeover. Revisions use ordinary Intercom in the same context. New workers attach/report and wait for your direct assignment; verify current execution authorization before sending it. For stopped-worker replacement, inspect saved history and partial work, preserve any editor draft/queued input and confirm the exact writer stopped before stock project.close/project.open. Idle or absence alone cannot establish draft safety or writer exit. If safety is unobservable, retain the pane and inspect it; do not invent recovery controls. Do not reapply historical preferences over later human choices. Never replace an unreviewed conversation or start a duplicate writer.`;
Keep normal tools and honor human model changes. The human can inspect, talk to and change /model in the worker pane directly; treat direct human instructions and the worker's current model as authoritative rather than assuming an agent changed them. Do not revert either unless the human asks. Inherit by default. If the user supplies a model preference to the supervisor, pass it explicitly to the agent through OpenGoalWorker's model instruction or exact-session Intercom steering; let the agent configure it through supported controls and verify its actual choice. project.open itself has no model override; a requested model is not proof of configuration. Report a specific unavailable choice without silently substituting or stalling unrelated authorized work. After compaction reread the plan. Lost connection or exhausted credits does not erase work. Preserve drafts and saved sessions; confirm other writers stopped before solo takeover. Revisions use ordinary Intercom in the same context. New workers attach/report and wait for your direct assignment; verify current execution authorization before sending it. For stopped-worker replacement or a cloned/moved supervisor session, inspect saved history and partial work, preserve any editor draft/queued input, then call OpenGoalWorker and let its human confirmation release the old runtime binding. Never replace through raw project.open. Idle or absence alone cannot establish draft safety or writer exit. If safety is unobservable, retain the pane and inspect it; do not invent recovery controls. Do not reapply historical preferences over later human choices. Never replace an unreviewed conversation or start a duplicate writer.`;
}
// Routine notices quote only selected goal lines; full context stops at Log.
const goalLines = (text: string) => foldPlan(text).split("\n").filter(line => GOAL_LINE.test(line)).join("\n");
@@ -295,7 +295,7 @@ export const nativeMessages = {
workerPause: (paused: boolean) => `Worker ${paused ? "paused" : "unpaused"} locally; no new task submitted and no approval authority granted.`,
taskRequired: "Supply an explicit bounded proposed task for a new worker context.",
modelDescription: "User preference for agent-led configuration and verification, not a launch override.",
openDescription: "After Ready, open a native project pane with a bounded proposed task. Stock open sends startup only into a newly created Pi context; existing panes receive no message. The worker must AttachGoalPlan and report, then wait for an explicit exact-session assignment. Inherit model defaults unless the user supplies a preference for agent-led configuration. Receipts are not attachment. Revisions use Intercom; stopped-worker replacement uses stock controls after inspected history, preserved drafts and confirmed stop, not this tool.",
openDescription: "After Ready, open a native project pane with a bounded proposed task. Stock open sends startup only into a newly created Pi context; existing panes receive no message. The worker must AttachGoalPlan and report, then wait for an explicit exact-session assignment. Inherit model defaults unless the user supplies a preference for agent-led configuration. Receipts are not attachment. Revisions use Intercom. If this supervisor session was cloned or a worker is already recorded, this tool requires explicit human confirmation that the original supervisor and all writers stopped before it releases the runtime binding and opens a replacement; saved history and reviews remain.",
disconnected: "Intercom disconnected; liveness and stop are unconfirmed. Inspect the saved session and pane; do not launch a replacement.",
shuttingDown: "Worker session shutting down; inspect its last saved messages. No goal sign-off inferred.",
noAssistant: "Worker run ended without an assistant result; inspect saved messages.",
+14
View File
@@ -1220,6 +1220,20 @@ it("opens no-focus, records explicit attachment only, and wakes review only for
expect(f.messages).toHaveLength(cleared);
});
it("releases a confirmed-stopped inherited binding through OpenGoalWorker", 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" });
const oldRequest = f.entries.at(-1).data.worker.requestId;
f.ctx.ui.select.mockResolvedValueOnce("Worker confirmed stopped");
const opened = await f.tools.get("OpenGoalWorker").execute("replacement", { task: "Continue the approved plan" }, undefined, undefined, f.ctx);
expect(opened.content[0].text).toContain('"disposition":"opened"');
const replacement = f.entries.at(-1).data.worker;
expect(replacement).toMatchObject({ paneId: "native-pane", parentId: "parent-intercom", task: "Continue the approved plan" });
expect(replacement.requestId).not.toBe(oldRequest);
expect(f.entries.some(entry => entry.data.worker?.intercomId === "old-worker")).toBe(true); // saved history is retained
expect(openProjectPane).toHaveBeenCalledTimes(2);
});
it("automatically reports worker turn end and pauses a rejected attachment", async () => {
const f = fixture(); const path = join(f.ctx.cwd, "supplied.md"); writeFileSync(path, f.plan);
await f.tools.get("AttachGoalPlan").execute("attach", { path, parent: "live-parent", requestId: "owned-request" }, undefined, undefined, f.ctx);