pi-subagents 0.80.0 → 0.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/bundle/agents.js +18 -0
- package/bundle/background-work.js +16 -0
- package/bundle/capability-ceiling.js +22 -0
- package/bundle/child-tool-plan.js +11 -0
- package/bundle/chunk-43SCHIMD.js +57 -0
- package/bundle/chunk-4QFZZV3O.js +57 -0
- package/bundle/chunk-4SDU2BZX.js +1190 -0
- package/bundle/chunk-52UBALJL.js +753 -0
- package/bundle/chunk-5LFLVSTM.js +260 -0
- package/bundle/chunk-5TYMSZX3.js +235 -0
- package/bundle/chunk-62OCY2S4.js +14 -0
- package/bundle/chunk-7F6RKT6W.js +132 -0
- package/bundle/chunk-7IVSUMQQ.js +1337 -0
- package/bundle/chunk-7J4ZXMY7.js +18 -0
- package/bundle/chunk-7PCKZHR5.js +1234 -0
- package/bundle/chunk-7YMXOYEU.js +1373 -0
- package/bundle/chunk-B5EKXDHE.js +16870 -0
- package/bundle/chunk-CIZQXJ5Q.js +8619 -0
- package/bundle/chunk-CTPAE36Y.js +94 -0
- package/bundle/chunk-D4EHT5SJ.js +14 -0
- package/bundle/chunk-GC4DSPMH.js +946 -0
- package/bundle/chunk-GJECHSAR.js +179 -0
- package/bundle/chunk-M2CTFYT3.js +4164 -0
- package/bundle/chunk-MHZ4G23A.js +1296 -0
- package/bundle/chunk-N2BN7BF5.js +159 -0
- package/bundle/chunk-NF7IEKBH.js +490 -0
- package/bundle/chunk-NGGAFDL6.js +742 -0
- package/bundle/chunk-NIXWDZZV.js +9 -0
- package/bundle/chunk-NRQ75MGH.js +8327 -0
- package/bundle/chunk-OYF6AFDY.js +458 -0
- package/bundle/chunk-PLCCBLPY.js +526 -0
- package/bundle/chunk-RMQFMMAT.js +416 -0
- package/bundle/chunk-SIJ4S3EN.js +276 -0
- package/bundle/chunk-UKG4V45O.js +228 -0
- package/bundle/chunk-VH5OAWVG.js +4096 -0
- package/bundle/chunk-W2SZ22EM.js +5462 -0
- package/bundle/chunk-WVD4UEL4.js +9367 -0
- package/bundle/chunk-XSWOTIA3.js +175 -0
- package/bundle/chunk-YFCK7H2S.js +129 -0
- package/bundle/chunk-YKEWIB4M.js +380 -0
- package/bundle/chunk-YYREO77V.js +137 -0
- package/bundle/chunk-Z7KVHQHL.js +173 -0
- package/bundle/chunk-ZLINIYTP.js +8613 -0
- package/bundle/chunk-ZQGSDHL6.js +128 -0
- package/bundle/control-channel.js +11 -0
- package/bundle/delegation.js +14 -0
- package/bundle/external-job-provider.js +20 -0
- package/bundle/external-runs.js +21 -0
- package/bundle/index.js +30 -0
- package/bundle/inspectors.js +8 -0
- package/bundle/intercom-bridge.js +11 -0
- package/bundle/preflight.js +425 -0
- package/bundle/project-panes.js +28 -0
- package/bundle/required-child-extensions.js +6 -0
- package/bundle/shared-types.js +8 -0
- package/bundle/workflow-resources.js +7 -0
- package/docs/agents.md +31 -0
- package/docs/configuration.md +9 -1
- package/docs/extension-api.md +27 -0
- package/docs/observability.md +6 -2
- package/docs/tool-reference.md +3 -1
- package/docs/workflows.md +16 -0
- package/package.json +19 -19
- package/skills/pi-subagents/references/execution-controls.md +5 -4
- package/src/agents/agent-management.js +3 -3
- package/src/agents/agent-memory.js +3 -1
- package/src/agents/agents.js +6 -6
- package/src/agents/frontmatter.d.ts +4 -0
- package/src/agents/frontmatter.js +24 -3
- package/src/agents/skills.js +2 -1
- package/src/api/delegation.d.ts +20 -0
- package/src/extension/config.js +3 -0
- package/src/extension/herdr-pi-bridge.js +17 -10
- package/src/extension/index.js +109 -9
- package/src/extension/schemas.js +3 -3
- package/src/intercom/native-supervisor-channel.js +20 -0
- package/src/missions/lifecycle.js +7 -1
- package/src/runs/background/active-async-capacity.d.ts +1 -0
- package/src/runs/background/active-async-capacity.js +9 -3
- package/src/runs/background/async-execution.js +20 -10
- package/src/runs/background/async-job-tracker.d.ts +6 -1
- package/src/runs/background/async-job-tracker.js +9 -4
- package/src/runs/background/async-status.d.ts +2 -0
- package/src/runs/background/async-status.js +21 -2
- package/src/runs/background/await-async-run.js +3 -7
- package/src/runs/background/boot-id.d.ts +12 -0
- package/src/runs/background/boot-id.js +47 -0
- package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
- package/src/runs/background/interrupted-elsewhere.js +68 -0
- package/src/runs/background/notify.d.ts +2 -0
- package/src/runs/background/notify.js +13 -0
- package/src/runs/background/result-delivery-ownership.d.ts +6 -0
- package/src/runs/background/result-delivery-ownership.js +19 -1
- package/src/runs/background/result-watcher.d.ts +3 -1
- package/src/runs/background/result-watcher.js +24 -8
- package/src/runs/background/run-child-session.d.ts +1 -6
- package/src/runs/background/run-child-session.js +19 -1
- package/src/runs/background/run-status.js +2 -1
- package/src/runs/background/stale-run-reconciler.d.ts +25 -0
- package/src/runs/background/stale-run-reconciler.js +54 -13
- package/src/runs/background/subagent-runner-bootstrap.js +3 -0
- package/src/runs/background/subagent-runner.d.ts +4 -0
- package/src/runs/background/subagent-runner.js +13 -5
- package/src/runs/background/subagent-wait.d.ts +1 -1
- package/src/runs/background/subagent-wait.js +6 -1
- package/src/runs/background/terminal-run-index.d.ts +1 -0
- package/src/runs/background/terminal-run-index.js +4 -2
- package/src/runs/foreground/async-stop-action.js +3 -1
- package/src/runs/foreground/execution.js +45 -5
- package/src/runs/foreground/foreground-history.d.ts +16 -2
- package/src/runs/foreground/foreground-history.js +133 -31
- package/src/runs/foreground/subagent-executor.d.ts +11 -0
- package/src/runs/foreground/subagent-executor.js +277 -74
- package/src/runs/shared/child-launch.d.ts +2 -2
- package/src/runs/shared/child-session.d.ts +3 -2
- package/src/runs/shared/child-session.js +51 -8
- package/src/runs/shared/child-tool-plan.d.ts +7 -0
- package/src/runs/shared/child-tool-plan.js +21 -6
- package/src/runs/shared/herdr-connection.d.ts +17 -8
- package/src/runs/shared/herdr-connection.js +19 -7
- package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
- package/src/runs/shared/herdr-external-adapters.js +1 -1
- package/src/runs/shared/herdr-machine.d.ts +12 -2
- package/src/runs/shared/herdr-machine.js +55 -9
- package/src/runs/shared/herdr-pane-host.d.ts +88 -0
- package/src/runs/shared/herdr-pane-host.js +155 -0
- package/src/runs/shared/herdr-placed-run.d.ts +27 -150
- package/src/runs/shared/herdr-placed-run.js +48 -395
- package/src/runs/shared/local-machine-access.d.ts +5 -0
- package/src/runs/shared/local-machine-access.js +28 -0
- package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
- package/src/runs/shared/orca-progress-tabs.js +1 -24
- package/src/runs/shared/parallel-utils.d.ts +1 -1
- package/src/runs/shared/placed-run-owner.d.ts +49 -0
- package/src/runs/shared/placed-run-owner.js +93 -0
- package/src/runs/shared/placement-host.d.ts +99 -0
- package/src/runs/shared/placement-host.js +157 -0
- package/src/runs/shared/rex-machine.d.ts +25 -0
- package/src/runs/shared/rex-machine.js +83 -0
- package/src/runs/shared/rex-pane-host.d.ts +62 -0
- package/src/runs/shared/rex-pane-host.js +327 -0
- package/src/runs/shared/structured-output.d.ts +1 -0
- package/src/runs/shared/structured-output.js +1 -0
- package/src/shared/accessible-dir.js +4 -1
- package/src/shared/completion-owner.d.ts +5 -0
- package/src/shared/completion-owner.js +5 -0
- package/src/shared/provider-diagnostics.d.ts +9 -0
- package/src/shared/provider-diagnostics.js +75 -0
- package/src/shared/session-identity.d.ts +2 -0
- package/src/shared/session-identity.js +26 -0
- package/src/shared/types.d.ts +48 -6
- package/src/slash/delegation-adapters.d.ts +3 -1
- package/src/slash/delegation-adapters.js +7 -0
- package/src/slash/prompt-template-bridge.js +3 -1
- package/src/slash/subagents-admin.d.ts +1 -0
- package/src/slash/subagents-admin.js +1 -1
- package/src/tui/fleet-layout.d.ts +20 -0
- package/src/tui/fleet-layout.js +17 -0
- package/src/tui/fleet-viewport.d.ts +12 -0
- package/src/tui/fleet-viewport.js +35 -0
- package/src/tui/fleet.d.ts +6 -6
- package/src/tui/fleet.js +93 -66
- package/src/tui/render.d.ts +6 -0
- package/src/tui/render.js +3 -3
- package/src/tui/subagent-messages.js +1 -0
- package/src/workflows/scripted-workflow.d.ts +1 -1
- package/src/workflows/structured-workflow-scripts.d.ts +1 -0
- package/src/workflows/structured-workflow-scripts.js +5 -4
- package/src/workflows/workflow-auto-resume.d.ts +32 -0
- package/src/workflows/workflow-auto-resume.js +102 -0
- package/src/workflows/workflow-launch-record.d.ts +27 -0
- package/src/workflows/workflow-launch-record.js +48 -0
- package/src/workflows/workflow-resources.js +2 -2
- package/src/workflows/workflow-resume-claim.d.ts +82 -0
- package/src/workflows/workflow-resume-claim.js +210 -0
- package/src/workflows/workflow-resume.d.ts +56 -0
- package/src/workflows/workflow-resume.js +106 -0
- package/src/workflows/workflow-reuse.d.ts +15 -9
- package/src/workflows/workflow-reuse.js +77 -14
- package/src/workflows/workflow-revival.d.ts +2 -0
- package/src/workflows/workflow-revival.js +2 -1
- package/src/workflows/workflow-stop-cause.d.ts +6 -0
- package/src/workflows/workflow-stop-cause.js +8 -0
|
@@ -10,6 +10,7 @@ import { buildSkillInjection, resolveSkills } from "../agents/skills.js";
|
|
|
10
10
|
import { buildAgentMemoryInjection } from "../agents/agent-memory.js";
|
|
11
11
|
import { appendAgentRefinementOverlay } from "../agents/agent-refinements.js";
|
|
12
12
|
import { rewriteSubagentPrompt } from "../runs/shared/subagent-prompt-runtime.js";
|
|
13
|
+
import { assertNoToolModifierEntries } from "../runs/shared/child-tool-plan.js";
|
|
13
14
|
import { resolveExistingReadPaths } from "../shared/settings.js";
|
|
14
15
|
import { MODEL_ONLY_TOOL } from "../shared/extension-context.js";
|
|
15
16
|
function gitEvidence(cwd) {
|
|
@@ -37,6 +38,7 @@ export function resolveRemoteHerdrResources(cwd, resources, remoteDefaultTools =
|
|
|
37
38
|
const reads = resources.reads === undefined ? agent.defaultReads ?? false : resources.reads;
|
|
38
39
|
const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, cwd) : [];
|
|
39
40
|
const systemPrompt = appendAgentRefinementOverlay(`${agent.systemPrompt}${buildAgentMemoryInjection(agent, cwd)}${skillPrompt ? `\n\n${skillPrompt}` : ""}${readPaths.length ? `\n\n[Read from: ${readPaths.join(", ")}]` : ""}`, { cwd, agentName: agent.name });
|
|
41
|
+
assertNoToolModifierEntries(agent.name, agent.tools ?? []);
|
|
40
42
|
const denied = new Set(agent.excludeTools ?? []);
|
|
41
43
|
const tools = (agent.tools === undefined ? remoteDefaultTools : agent.tools).filter((tool) => !denied.has(tool) && (!resources.toolCeiling || resources.toolCeiling.includes(tool)));
|
|
42
44
|
if (agent.tools === undefined && !tools.length)
|
|
@@ -91,7 +93,6 @@ export default function registerHerdrPiBridge(pi) {
|
|
|
91
93
|
client.write(encodeHerdrPiFrame(record));
|
|
92
94
|
};
|
|
93
95
|
const sessionId = (ctx) => ctx.sessionManager?.getSessionId?.();
|
|
94
|
-
const activeTools = (ctx) => ctx.getActiveTools?.() ?? [];
|
|
95
96
|
pi.registerTool({
|
|
96
97
|
name: "contact_supervisor",
|
|
97
98
|
...MODEL_ONLY_TOOL,
|
|
@@ -104,11 +105,11 @@ export default function registerHerdrPiBridge(pi) {
|
|
|
104
105
|
const id = `sup_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
105
106
|
send({ type: "supervisor-request", requestId: id, reason: input.reason, message: input.message?.slice(0, 65_536) ?? "", ...(input.interview !== undefined ? { interview: input.interview } : {}), expectsReply: input.reason !== "progress_update", nativeSessionId });
|
|
106
107
|
if (input.reason === "progress_update") {
|
|
107
|
-
await new Promise((resolve) => supervisor.set(id, { reason: input.reason, resolve }));
|
|
108
|
+
await new Promise((resolve, reject) => supervisor.set(id, { reason: input.reason, resolve, reject }));
|
|
108
109
|
return { content: [{ type: "text", text: "Supervisor progress update queued." }], details: { delivered: true, requestId: id, reason: input.reason } };
|
|
109
110
|
}
|
|
110
111
|
const reason = input.reason;
|
|
111
|
-
const answer = await new Promise((resolve) => supervisor.set(id, { reason, resolve }));
|
|
112
|
+
const answer = await new Promise((resolve, reject) => supervisor.set(id, { reason, resolve, reject }));
|
|
112
113
|
const details = { requestId: id, reason };
|
|
113
114
|
if (reason === "interview_request")
|
|
114
115
|
try {
|
|
@@ -145,7 +146,10 @@ export default function registerHerdrPiBridge(pi) {
|
|
|
145
146
|
if (!entry)
|
|
146
147
|
throw new Error("Unknown supervisor request.");
|
|
147
148
|
supervisor.delete(request.supervisorId);
|
|
148
|
-
|
|
149
|
+
if (request.error !== undefined)
|
|
150
|
+
entry.reject(new Error(request.error));
|
|
151
|
+
else
|
|
152
|
+
entry.resolve(request.text ?? "");
|
|
149
153
|
}
|
|
150
154
|
send({ type: "control-ack", requestId, operation: request.operation, nativeSessionId });
|
|
151
155
|
};
|
|
@@ -165,7 +169,7 @@ export default function registerHerdrPiBridge(pi) {
|
|
|
165
169
|
if (!nativeSessionId)
|
|
166
170
|
throw new Error("Pane-native Pi bridge requires a persisted native session identity.");
|
|
167
171
|
fs.writeFileSync(manifestPath, JSON.stringify({ protocol: HERDR_PI_PROTOCOL, packageVersion: packageJson.version, runId, socketPath, nativeSessionId, pid: process.pid }), { mode: 0o600, flag: "wx" });
|
|
168
|
-
send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: ctx.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools:
|
|
172
|
+
send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: ctx.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: pi.getActiveTools(), model: ctx.model?.provider && ctx.model.id ? `${ctx.model.provider}/${ctx.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
|
|
169
173
|
});
|
|
170
174
|
on("message_end", (event) => {
|
|
171
175
|
const message = event.message;
|
|
@@ -215,11 +219,11 @@ export default function registerHerdrPiBridge(pi) {
|
|
|
215
219
|
const invalidReads = resources.reads !== undefined && resources.reads !== false && (!Array.isArray(resources.reads) || resources.reads.some((name) => typeof name !== "string"));
|
|
216
220
|
if (typeof resources.agent !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(resources.agent) || invalidSkills || invalidReads)
|
|
217
221
|
throw new Error("Remote logical resource names are invalid.");
|
|
218
|
-
const resolved = resolveRemoteHerdrResources(process.cwd(), { agent: resources.agent, ...(resources.skills ? { skills: resources.skills } : {}), ...(resources.toolCeiling ? { toolCeiling: resources.toolCeiling } : {}), ...(resources.reads !== undefined ? { reads: resources.reads } : {}) }, context ?
|
|
222
|
+
const resolved = resolveRemoteHerdrResources(process.cwd(), { agent: resources.agent, ...(resources.skills ? { skills: resources.skills } : {}), ...(resources.toolCeiling ? { toolCeiling: resources.toolCeiling } : {}), ...(resources.reads !== undefined ? { reads: resources.reads } : {}) }, context ? pi.getActiveTools() : []);
|
|
219
223
|
resolvedSystemPrompt = resolved.systemPrompt;
|
|
220
224
|
resolvedContextPolicy = resolved;
|
|
221
|
-
pi.setActiveTools
|
|
222
|
-
const acknowledged = context ?
|
|
225
|
+
pi.setActiveTools(resolved.tools);
|
|
226
|
+
const acknowledged = context ? pi.getActiveTools() : [];
|
|
223
227
|
if (acknowledged.some((tool) => !resolved.tools.includes(tool)) || resolved.tools.some((tool) => !acknowledged.includes(tool)))
|
|
224
228
|
throw new Error("Remote Pi did not apply the resolved active-tool set exactly.");
|
|
225
229
|
configured = true;
|
|
@@ -234,7 +238,10 @@ export default function registerHerdrPiBridge(pi) {
|
|
|
234
238
|
const text = frame.text;
|
|
235
239
|
if (text !== undefined && (typeof text !== "string" || Buffer.byteLength(text) > 256 * 1024))
|
|
236
240
|
throw new Error("Bridge request text is invalid or too large.");
|
|
237
|
-
|
|
241
|
+
const error = frame.error;
|
|
242
|
+
if (error !== undefined && (operation !== "supervisor-reply" || typeof error !== "string" || Buffer.byteLength(error) > 256 * 1024))
|
|
243
|
+
throw new Error("Bridge supervisor error is invalid or too large.");
|
|
244
|
+
pending.set(frame.requestId, { operation: operation, ...(text !== undefined ? { text } : {}), ...(typeof frame.supervisorId === "string" ? { supervisorId: frame.supervisorId } : {}), ...(error !== undefined ? { error } : {}) });
|
|
238
245
|
send({ type: "prepared", requestId: frame.requestId, nativeSessionId });
|
|
239
246
|
}
|
|
240
247
|
}
|
|
@@ -245,7 +252,7 @@ export default function registerHerdrPiBridge(pi) {
|
|
|
245
252
|
socket.on("close", () => { if (client === socket)
|
|
246
253
|
client = undefined; });
|
|
247
254
|
if (context && nativeSessionId)
|
|
248
|
-
send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: context.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools:
|
|
255
|
+
send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: context.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: pi.getActiveTools(), model: context.model?.provider && context.model.id ? `${context.model.provider}/${context.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
|
|
249
256
|
});
|
|
250
257
|
server.listen(socketPath, () => { fs.chmodSync(socketPath, 0o600); });
|
|
251
258
|
}
|
package/src/extension/index.js
CHANGED
|
@@ -43,6 +43,7 @@ import { createResultWatcher } from "../runs/background/result-watcher.js";
|
|
|
43
43
|
import { createResultDeliveryOwnership } from "../runs/background/result-delivery-ownership.js";
|
|
44
44
|
import { createScheduledRunManager } from "../runs/background/scheduled-runs.js";
|
|
45
45
|
import { registerSlashCommands } from "../slash/slash-commands.js";
|
|
46
|
+
import { ADMIN_MESSAGE_TYPE } from "../slash/subagents-admin.js";
|
|
46
47
|
import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template-bridge.js";
|
|
47
48
|
import { registerMainWatchdog } from "../watchdog/register-main.js";
|
|
48
49
|
import { registerSlashSubagentBridge } from "../slash/slash-bridge.js";
|
|
@@ -72,10 +73,14 @@ import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./
|
|
|
72
73
|
import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.js";
|
|
73
74
|
import { formatWorkflowPreflightSummary, normalizeWorkflowPreflight } from "../workflows/workflow-preflight.js";
|
|
74
75
|
import { runtimeReplacedAbortReason } from "../workflows/workflow-reuse.js";
|
|
76
|
+
import { autoResumeFailureText, cancelPendingWorkflowAutoResumes, finishWorkflowAutoResume, planWorkflowAutoResumes, WORKFLOW_AUTO_RESUME_MESSAGE_TYPE } from "../workflows/workflow-auto-resume.js";
|
|
77
|
+
import { recordWorkflowResumeRefusal, releaseWorkflowResumeClaim, resumedRunStarted } from "../workflows/workflow-resume-claim.js";
|
|
78
|
+
import { workflowResumeRefusal } from "../workflows/workflow-resume.js";
|
|
75
79
|
import { finalizeToolResult } from "./tool-result.js";
|
|
76
80
|
import { flattenSubagentToolOptions } from "./subagent-options.js";
|
|
77
81
|
import { collectGoalContinuationNotices } from "../missions/goal-driver.js";
|
|
78
82
|
import { restoreForegroundRunHistory } from "../runs/foreground/foreground-history.js";
|
|
83
|
+
import { createInterruptedElsewhereCollector } from "../runs/background/interrupted-elsewhere.js";
|
|
79
84
|
import { resolveMissionStoreLocation } from "../missions/store.js";
|
|
80
85
|
import { listRetainedChildren } from "../runs/background/retained-children.js";
|
|
81
86
|
import { DIRS, DEFAULT_ARTIFACT_CONFIG, SLASH_RESULT_TYPE, SLASH_TEXT_RESULT_TYPE, SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_ASYNC_STARTED_EVENT, SUBAGENT_PROCESS_TERMINAL_EVENT, SUBAGENT_CONTROL_EVENT, SUBAGENT_STEERING_NOTICE_EVENT, WIDGET_KEY, resolveMaxSubagentSpawnsPerSession, } from "../shared/types.js";
|
|
@@ -433,6 +438,7 @@ export default function registerSubagentExtension(pi) {
|
|
|
433
438
|
refreshResultDelivery();
|
|
434
439
|
},
|
|
435
440
|
onJobCleanup: (asyncId) => deliveredRunIds.delete(asyncId),
|
|
441
|
+
deliveryOwnerId: resultDeliveryOwnership.deliveryOwnerId,
|
|
436
442
|
supervisorRequestState: supervisorChannel.getSupervisorRequestState,
|
|
437
443
|
onJobsChanged: programStatus.sync,
|
|
438
444
|
});
|
|
@@ -557,13 +563,32 @@ export default function registerSubagentExtension(pi) {
|
|
|
557
563
|
return undefined;
|
|
558
564
|
return createSlashResultComponent(details, options, theme, config.mainWindowRenderer, foregroundDetachShortcut, () => state.lastUiContext?.ui.theme ?? theme);
|
|
559
565
|
});
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
566
|
+
// Pi sends custom messages to the model as plain user messages, so every model-visible
|
|
567
|
+
// pi-subagents message is wrapped in a marker naming its source. The marker depends only on
|
|
568
|
+
// the message, so history renders the same bytes every turn and cached prefixes stay valid.
|
|
569
|
+
const markedMessageTypes = new Set([...SUBAGENT_MESSAGE_TYPES, "subagent-compaction-resume", SLASH_RESULT_TYPE, SLASH_TEXT_RESULT_TYPE, ADMIN_MESSAGE_TYPE]);
|
|
570
|
+
pi.on("context", (event) => {
|
|
571
|
+
const messages = [];
|
|
572
|
+
for (const message of event.messages) {
|
|
573
|
+
if (message.role !== "custom" || !markedMessageTypes.has(message.customType)) {
|
|
574
|
+
messages.push(message);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
// Keep view-only reports in the transcript/RPC stream, but out of model context.
|
|
578
|
+
// SLASH_RESULT_TYPE carries delegated work and must remain model-visible.
|
|
579
|
+
if (message.customType === SLASH_TEXT_RESULT_TYPE && message.details?.viewOnly === true)
|
|
580
|
+
continue;
|
|
581
|
+
const open = `<pi_subagents_notice type="${message.customType}" origin="automated message from the pi-subagents extension, not typed by the user">`;
|
|
582
|
+
const close = "</pi_subagents_notice>";
|
|
583
|
+
messages.push({
|
|
584
|
+
...message,
|
|
585
|
+
content: typeof message.content === "string"
|
|
586
|
+
? `${open}\n${message.content}\n${close}`
|
|
587
|
+
: [{ type: "text", text: `${open}\n` }, ...message.content, { type: "text", text: `\n${close}` }],
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
return { messages };
|
|
591
|
+
});
|
|
567
592
|
pi.registerMessageRenderer(SLASH_TEXT_RESULT_TYPE, (message, _options, _theme) => {
|
|
568
593
|
const content = typeof message.content === "string"
|
|
569
594
|
? message.content
|
|
@@ -577,6 +602,60 @@ export default function registerSubagentExtension(pi) {
|
|
|
577
602
|
await waitForAdvertisement();
|
|
578
603
|
return (await getExecutor()).executePublic(id, params, signal, onUpdate, ctx);
|
|
579
604
|
};
|
|
605
|
+
// Names this runtime's planned auto-resumes in the process-global registry.
|
|
606
|
+
const autoResumeOwner = {};
|
|
607
|
+
const postAutoResumeFailure = (sourceRunId, refusal) => {
|
|
608
|
+
pi.sendMessage({ customType: WORKFLOW_AUTO_RESUME_MESSAGE_TYPE, content: autoResumeFailureText(sourceRunId, refusal), display: true }, { triggerTurn: false });
|
|
609
|
+
};
|
|
610
|
+
// Each session start claims its interrupted workflows synchronously, then launches them here, off
|
|
611
|
+
// session start. Cleanup and the next session start cancel the launches that have not started.
|
|
612
|
+
const launchWorkflowAutoResume = async (entry, ctx) => {
|
|
613
|
+
const { prepared } = entry;
|
|
614
|
+
try {
|
|
615
|
+
// Let the rest of session start (agent advertisement, other handlers) run first.
|
|
616
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
617
|
+
await waitForAdvertisement();
|
|
618
|
+
const executor = await getExecutor();
|
|
619
|
+
if (entry.state !== "pending")
|
|
620
|
+
return;
|
|
621
|
+
entry.state = "started";
|
|
622
|
+
const outcome = await executor.executeWorkflowResume(randomUUID(), prepared, new AbortController().signal, ctx);
|
|
623
|
+
// The executor has given back the claim and recorded any refusal; nothing below may replace them.
|
|
624
|
+
// A run launched by a runtime that was cleaned up meanwhile would keep running through its stale context.
|
|
625
|
+
if (outcome.launched && runtimeCleaned)
|
|
626
|
+
state.workflowControllers?.get(prepared.newRunId)?.abort(runtimeReplacedAbortReason());
|
|
627
|
+
// A cleaned-up runtime's pi can no longer post; the refusal stays in the run for its notice.
|
|
628
|
+
if (outcome.launched || runtimeCleaned)
|
|
629
|
+
return;
|
|
630
|
+
try {
|
|
631
|
+
postAutoResumeFailure(prepared.sourceRunId, outcome.refusal);
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
console.error(`Failed to post the auto-resume failure of workflow ${prepared.sourceRunId}:`, error);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
// A cancelled launch already gave its claim back.
|
|
639
|
+
if (entry.state === "cancelled")
|
|
640
|
+
return;
|
|
641
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
642
|
+
console.error(`Failed to auto-resume workflow ${prepared.sourceRunId}:`, error);
|
|
643
|
+
try {
|
|
644
|
+
if (resumedRunStarted(prepared.sourceAsyncDir, prepared.newRunId))
|
|
645
|
+
return;
|
|
646
|
+
releaseWorkflowResumeClaim(prepared.sourceAsyncDir, prepared.newRunId);
|
|
647
|
+
const refusal = workflowResumeRefusal("launch-failed", `launch failed: ${message}`, prepared.sourceRunId);
|
|
648
|
+
recordWorkflowResumeRefusal(prepared.sourceAsyncDir, { reason: refusal.reason, byHand: refusal.byHand, newRunId: prepared.newRunId });
|
|
649
|
+
postAutoResumeFailure(prepared.sourceRunId, refusal);
|
|
650
|
+
}
|
|
651
|
+
catch (cleanupError) {
|
|
652
|
+
console.error(`Failed to give back the auto-resume claim of workflow ${prepared.sourceRunId}:`, cleanupError);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
finally {
|
|
656
|
+
finishWorkflowAutoResume(entry);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
580
659
|
const executeSubagentCollapsed = (id, params, signal, onUpdate, ctx) => {
|
|
581
660
|
if (ctx.hasUI)
|
|
582
661
|
ctx.ui.setToolsExpanded(false);
|
|
@@ -853,12 +932,31 @@ export default function registerSubagentExtension(pi) {
|
|
|
853
932
|
resetJobs(ctx);
|
|
854
933
|
logSlowPhase("reset-jobs", phaseStartedAt);
|
|
855
934
|
phaseStartedAt = Date.now();
|
|
856
|
-
|
|
935
|
+
// Runs other sessions in this folder left when their Pi died, from what this restore reads anyway.
|
|
936
|
+
const interruptedElsewhere = ctx.hasUI ? createInterruptedElsewhereCollector({ currentSessionId: state.currentSessionId, cwd: ctx.cwd, now: Date.now() }) : undefined;
|
|
937
|
+
restoreForegroundRunHistory(state, { resultsDir: DIRS.results, observeForeign: interruptedElsewhere?.observeForegroundRun });
|
|
857
938
|
logSlowPhase("foreground-history", phaseStartedAt);
|
|
858
939
|
phaseStartedAt = Date.now();
|
|
859
|
-
restoreActiveJobs(ctx);
|
|
940
|
+
restoreActiveJobs(ctx, interruptedElsewhere?.observeStatus);
|
|
860
941
|
logSlowPhase("active-job-restore", phaseStartedAt);
|
|
861
942
|
phaseStartedAt = Date.now();
|
|
943
|
+
// Read-only and UI-only: never sent to the model, so prompts and history are unchanged.
|
|
944
|
+
if (interruptedElsewhere)
|
|
945
|
+
for (const line of interruptedElsewhere.lines())
|
|
946
|
+
ctx.ui.notify(line, "info");
|
|
947
|
+
logSlowPhase("cross-session-notice", phaseStartedAt);
|
|
948
|
+
// Before any stored result is delivered, so the interrupted run's notice can name its resume.
|
|
949
|
+
phaseStartedAt = Date.now();
|
|
950
|
+
// Launches an earlier session start of this runtime planned and has not started belong to that session.
|
|
951
|
+
cancelPendingWorkflowAutoResumes((entry) => entry.owner === autoResumeOwner);
|
|
952
|
+
// Not in a one-shot print run: its turn would then wait on the resumed workflow at settle.
|
|
953
|
+
if (recovering && ctx.hasUI && config.autoResume !== false) {
|
|
954
|
+
for (const entry of planWorkflowAutoResumes({ asyncDirRoot: DIRS.async, currentSessionId: state.currentSessionId, now: Date.now(), owner: autoResumeOwner })) {
|
|
955
|
+
void launchWorkflowAutoResume(entry, ctx);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
logSlowPhase("workflow-auto-resume", phaseStartedAt);
|
|
959
|
+
phaseStartedAt = Date.now();
|
|
862
960
|
scheduledRunManager.bindSession(ctx);
|
|
863
961
|
logSlowPhase("scheduled-runs", phaseStartedAt);
|
|
864
962
|
phaseStartedAt = Date.now();
|
|
@@ -883,6 +981,8 @@ export default function registerSubagentExtension(pi) {
|
|
|
883
981
|
if (runtimeCleaned)
|
|
884
982
|
return;
|
|
885
983
|
runtimeCleaned = true;
|
|
984
|
+
// Synchronously, so a reload's new runtime finds these runs unclaimed when it scans.
|
|
985
|
+
cancelPendingWorkflowAutoResumes((entry) => entry.owner === autoResumeOwner);
|
|
886
986
|
programStatus.dispose(shutdownReason);
|
|
887
987
|
releaseHostSessionLiveness();
|
|
888
988
|
releaseHostSessionLiveness = () => { };
|
package/src/extension/schemas.js
CHANGED
|
@@ -205,7 +205,7 @@ const SubagentParamProperties = {
|
|
|
205
205
|
description: "true: run the one ```js workflow block written in this same reply (false invalid). String with '/': script file read from request cwd. Other string: named workflow resource. Raw scripts (true or path) have no runs.host.",
|
|
206
206
|
})),
|
|
207
207
|
args: Type.Optional(Type.Unsafe({ type: "object", maxProperties: MAX_ARGS_FIELDS, additionalProperties: true, description: `Plain-JSON args for workflow; ${MAX_ARGS_FIELDS} fields/object, ${MAX_ARGS_ITEMS} items/array, depth ${MAX_ARGS_DEPTH}, ${MAX_ARGS_BYTES / 1024} KiB total; raw-script args are exposed deeply frozen and persisted, so do not include secrets.` })),
|
|
208
|
-
reuseFrom: Type.Optional(Type.String({ minLength: 1, description: "Exact id of a workflow run
|
|
208
|
+
reuseFrom: Type.Optional(Type.String({ minLength: 1, description: "Exact id of a workflow run ended by a reload, handoff or Pi exit; this async workflow reuses its finished children and re-attaches to its running ones." })),
|
|
209
209
|
globalConcurrencyLimit: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
210
210
|
maxSubagentSpawnsPerRun: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
211
211
|
preflight: Type.Optional(WorkflowPreflightOverride),
|
|
@@ -227,7 +227,7 @@ const SubagentParamProperties = {
|
|
|
227
227
|
usageBudget: Type.Optional(UsageBudgetOverride),
|
|
228
228
|
agentScope: Type.Optional(Type.String({ description: "user/project/both (default); project wins collisions." })),
|
|
229
229
|
cwd: Type.Optional(Type.String({ description: "Execution/project-pane directory." })),
|
|
230
|
-
machine: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Herdr saved machine id or label
|
|
230
|
+
machine: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Herdr saved machine id or label (runs an external CLI agent), a Rex host named in settings (runs a Pi child), or \"local\" (a Pi child in a Rex pane here). On another machine, cwd is a directory there." })),
|
|
231
231
|
artifacts: Type.Optional(Type.Boolean({ description: "Debug artifacts; default true." })),
|
|
232
232
|
includeProgress: Type.Optional(Type.Boolean({ description: "Full result progress; default false." })),
|
|
233
233
|
share: Type.Optional(Type.Boolean({ description: "Upload session to GitHub Gist; default false." })),
|
|
@@ -306,6 +306,6 @@ const SubagentWaitParamsSchema = Type.Object({
|
|
|
306
306
|
nonBlocking: Type.Optional(Type.Boolean({ description: "Subscribe to id's wake and return immediately; requires id, not with all." })),
|
|
307
307
|
all: Type.Optional(Type.Boolean({ description: "Wait for all work active at call time." })),
|
|
308
308
|
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Max wait; default waitTool.defaultTimeoutMs, then 30 min." })),
|
|
309
|
-
stopOnAttention: Type.Optional(Type.Boolean({ description: "false: keep waiting through
|
|
309
|
+
stopOnAttention: Type.Optional(Type.Boolean({ description: "false: keep waiting through all attention except supervisor requests." })),
|
|
310
310
|
});
|
|
311
311
|
export const SubagentWaitParams = keepTopLevelParameterDescriptions(SubagentWaitParamsSchema);
|
|
@@ -113,6 +113,8 @@ async function waitForReply(channelDir, requestId, initialDeadline, signal) {
|
|
|
113
113
|
if (fs.existsSync(file)) {
|
|
114
114
|
const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
115
115
|
if (parsed.type === "subagent.supervisor.reply" && parsed.requestId === requestId && typeof parsed.message === "string") {
|
|
116
|
+
if (typeof parsed.error === "string")
|
|
117
|
+
throw new Error(parsed.error);
|
|
116
118
|
return parsed;
|
|
117
119
|
}
|
|
118
120
|
}
|
|
@@ -761,6 +763,24 @@ export function createNativeSupervisorChannel(pi, state, deps = {}) {
|
|
|
761
763
|
cleanupRequestLifecycle(request, lifecycle);
|
|
762
764
|
continue;
|
|
763
765
|
}
|
|
766
|
+
if (request.expectsReply && runState(request).blockingWorkflowChildren?.has(request.runId)) {
|
|
767
|
+
// Pi cannot deliver an ask behind the workflow tool call that awaits this child.
|
|
768
|
+
// Return a tool error instead of renewing an unreachable ask until the run deadline.
|
|
769
|
+
try {
|
|
770
|
+
writePrivateAtomicJson(replyPath(channelDir, request.id), {
|
|
771
|
+
type: "subagent.supervisor.reply", requestId: request.id, createdAt: now, message: "",
|
|
772
|
+
error: "Supervisor unavailable: the parent is blocked in an async:false workflow and cannot answer this request. Report the blocker and any partial results, then stop. Relaunch the workflow with async:true to allow supervisor replies.",
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
console.error(`Failed to report unreachable supervisor request ${request.id}; will retry:`, error);
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
seenFiles.add(file);
|
|
780
|
+
observeRequestLifecycle(request, "resolved");
|
|
781
|
+
cleanupRequestLifecycle(request, "resolved");
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
764
784
|
seenFiles.add(file);
|
|
765
785
|
if (!request.expectsReply) {
|
|
766
786
|
// Progress is already visible through child activity; do not inject a
|
|
@@ -253,7 +253,13 @@ export function readMissionBinding(asyncDir) {
|
|
|
253
253
|
const bindingPath = path.join(asyncDir, MISSION_BINDING_FILE);
|
|
254
254
|
if (!fs.existsSync(bindingPath))
|
|
255
255
|
return undefined;
|
|
256
|
-
|
|
256
|
+
const raw = fs.readFileSync(bindingPath, "utf-8");
|
|
257
|
+
try {
|
|
258
|
+
return parsePersistedBinding(JSON.parse(raw), bindingPath);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
throw new Error(`Invalid mission binding file '${bindingPath}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
262
|
+
}
|
|
257
263
|
}
|
|
258
264
|
export function syncMissionFromAsyncCompletion(value) {
|
|
259
265
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -34,6 +34,7 @@ interface CapacityOptions {
|
|
|
34
34
|
abandonedSlotReleaseAfterMs?: number | false;
|
|
35
35
|
pidLiveness?: (pid: number) => PidLiveness;
|
|
36
36
|
pidNamespaceScope?: () => string | undefined;
|
|
37
|
+
bootId?: () => string | undefined;
|
|
37
38
|
afterSlotRename?: (releasedDir: string) => void;
|
|
38
39
|
writeOwner?: (filePath: string, owner: ActiveAsyncCapacityOwner) => void;
|
|
39
40
|
}
|
|
@@ -4,7 +4,8 @@ import * as path from "node:path";
|
|
|
4
4
|
import { writePrivateAtomicJson } from "../../shared/atomic-json.js";
|
|
5
5
|
import { TEMP_ROOT_DIR } from "../../shared/types.js";
|
|
6
6
|
import { readStatus } from "../../shared/utils.js";
|
|
7
|
-
import { checkPidLiveness } from "./stale-run-reconciler.js";
|
|
7
|
+
import { checkPidLiveness, checkRecordedPidLiveness } from "./stale-run-reconciler.js";
|
|
8
|
+
import { currentBootId } from "./boot-id.js";
|
|
8
9
|
import { currentPidNamespaceScope } from "./pid-namespace.js";
|
|
9
10
|
import { readProcessTerminal } from "./process-terminal.js";
|
|
10
11
|
import { isTerminalAsyncState as terminalState, readWorkflowChildEvidence, readWorkflowChildProcessEvidence } from "./workflow-terminal-proof.js";
|
|
@@ -201,9 +202,14 @@ function abandonedRunnerReleaseVerdict(status, proofState, options, anyTerminalS
|
|
|
201
202
|
if (typeof status.pid !== "number" || !Number.isInteger(status.pid) || status.pid < 1)
|
|
202
203
|
return { state: "retained", reason: `${proofReason}; runner PID is missing or invalid` };
|
|
203
204
|
// A PID from another namespace can look dead here while the runner is alive.
|
|
204
|
-
|
|
205
|
+
const observedScope = (options.pidNamespaceScope ?? currentPidNamespaceScope)();
|
|
206
|
+
if (status.pidNamespaceScope !== undefined && status.pidNamespaceScope !== observedScope)
|
|
205
207
|
return { state: "retained", reason: `${proofReason}; runner PID namespace differs from this process, so liveness is unknown` };
|
|
206
|
-
const liveness = (
|
|
208
|
+
const liveness = checkRecordedPidLiveness({ pid: status.pid, pidNamespaceScope: status.pidNamespaceScope, bootId: status.bootId }, {
|
|
209
|
+
observedScope,
|
|
210
|
+
observedBootId: (options.bootId ?? currentBootId)(),
|
|
211
|
+
probe: options.pidLiveness ?? ((pid) => checkPidLiveness(pid)),
|
|
212
|
+
});
|
|
207
213
|
if (liveness !== "dead")
|
|
208
214
|
return { state: "retained", reason: `${proofReason}; runner PID liveness is ${liveness}` };
|
|
209
215
|
const lastActivityAt = status.lastActivityAt ?? status.lastUpdate ?? status.endedAt;
|
|
@@ -13,7 +13,7 @@ import { childCacheRetentionEnv } from "../../shared/child-cache-retention.js";
|
|
|
13
13
|
import { buildEffectiveSystemPrompt } from "../shared/effective-system-prompt.js";
|
|
14
14
|
import { currentCompletionOwnerId } from "../../shared/completion-owner.js";
|
|
15
15
|
import { planChildLaunch, projectChainOutputSchemas, resolveStepBehavior, suppressProgressForReadOnlyTask } from "../shared/child-launch-plan.js";
|
|
16
|
-
import { formatHerdrMachineRunnerUnsupported,
|
|
16
|
+
import { formatHerdrMachineRunnerUnsupported, resolveMachinePlacement } from "../shared/herdr-machine.js";
|
|
17
17
|
import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.js";
|
|
18
18
|
import { assertClaudeCodeModelScope, isClaudeCodeAdapterId, resolveClaudeCodeOverride } from "../shared/claude-code-adapter.js";
|
|
19
19
|
import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.js";
|
|
@@ -51,8 +51,9 @@ import { usageBudgetState } from "../shared/usage-budget.js";
|
|
|
51
51
|
import { finalizeProcessTerminal, initializeProcessTerminal, readProcessTerminal } from "./process-terminal.js";
|
|
52
52
|
import { persistRunnerStartupFailure } from "./runner-startup-failure.js";
|
|
53
53
|
import { statusStepDescription } from "./chain-append.js";
|
|
54
|
+
import { currentBootId, currentRecordedPid } from "./boot-id.js";
|
|
54
55
|
import { currentPidNamespaceScope } from "./pid-namespace.js";
|
|
55
|
-
import {
|
|
56
|
+
import { checkRecordedPidLiveness } from "./stale-run-reconciler.js";
|
|
56
57
|
import { lookupRunnerLauncher, resolveAgentRunnerLauncher, runnerLauncherPlacementError } from "../shared/runner-launcher.js";
|
|
57
58
|
import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.js";
|
|
58
59
|
import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling } from "../shared/capability-ceiling.js";
|
|
@@ -432,7 +433,12 @@ function spawnRunner(cfg, suffix, cwd, initialStatus, initialStatusPath, launchP
|
|
|
432
433
|
const runnerProcessInstanceId = randomUUID();
|
|
433
434
|
const hasRevivalLease = typeof cfg.revivalLease === "object";
|
|
434
435
|
const launchBarrierToken = hasRevivalLease ? undefined : runnerProcessInstanceId;
|
|
435
|
-
|
|
436
|
+
// A broker launcher may start the runner outside its own process, even under another kernel, so the
|
|
437
|
+
// spawned pid is only the wrapper's and only a direct runner shares Pi's boot id. The runner reports
|
|
438
|
+
// its own identity before it is allowed to proceed.
|
|
439
|
+
const launcher = cfg.launcher;
|
|
440
|
+
const bootId = launcher ? undefined : currentBootId();
|
|
441
|
+
const launchConfig = { ...cfg, runnerProcessInstanceId, ...(launchBarrierToken ? { launchBarrierToken } : {}), ...(bootId ? { bootId } : {}) };
|
|
436
442
|
writePrivateAtomicJson(cfgPath, launchConfig);
|
|
437
443
|
const command = binaryHost ?? nodeExecutable;
|
|
438
444
|
const launchForStartup = launchConfig;
|
|
@@ -456,9 +462,6 @@ function spawnRunner(cfg, suffix, cwd, initialStatus, initialStatusPath, launchP
|
|
|
456
462
|
fs.rmSync(startupConfirmPath, { force: true });
|
|
457
463
|
if (startupProceedPath)
|
|
458
464
|
fs.rmSync(startupProceedPath, { force: true });
|
|
459
|
-
// A broker launcher may start the runner outside its own process, so the spawned pid is only the
|
|
460
|
-
// wrapper's. The runner reports its own identity before it is allowed to proceed.
|
|
461
|
-
const launcher = launchConfig.launcher;
|
|
462
465
|
const identityPath = launcher && launchBarrierToken && launchAsyncDir ? path.join(launchAsyncDir, "runner-startup.json") : undefined;
|
|
463
466
|
if (identityPath)
|
|
464
467
|
fs.rmSync(identityPath, { force: true });
|
|
@@ -550,7 +553,8 @@ function spawnRunner(cfg, suffix, cwd, initialStatus, initialStatusPath, launchP
|
|
|
550
553
|
}
|
|
551
554
|
// The runner may outlive its wrapper; only a confirmed-dead runner in this pid namespace finalizes here,
|
|
552
555
|
// otherwise status-pid reconciliation does it later.
|
|
553
|
-
if (!runnerIdentity || runnerIdentity.pidNamespaceScope !== currentPidNamespaceScope()
|
|
556
|
+
if (!runnerIdentity || runnerIdentity.pidNamespaceScope !== currentPidNamespaceScope()
|
|
557
|
+
|| checkRecordedPidLiveness(runnerIdentity) !== "dead")
|
|
554
558
|
return;
|
|
555
559
|
}
|
|
556
560
|
finalizeProcessTerminal(asyncDir, runId, {
|
|
@@ -612,7 +616,7 @@ function spawnRunner(cfg, suffix, cwd, initialStatus, initialStatusPath, launchP
|
|
|
612
616
|
try {
|
|
613
617
|
writePrivateAtomicJson(initialStatusPath, {
|
|
614
618
|
...initialStatus,
|
|
615
|
-
...(launcher ? {} : { pid: proc.pid, pidNamespaceScope: currentPidNamespaceScope() }),
|
|
619
|
+
...(launcher ? {} : { pid: proc.pid, pidNamespaceScope: currentPidNamespaceScope(), ...(bootId ? { bootId } : {}) }),
|
|
616
620
|
processTerminal: { version: 1, state: "pending", runId: initialStatus.runId, runnerProcessInstanceId },
|
|
617
621
|
});
|
|
618
622
|
// Aggregate waits must see the launch before the runner's first status update.
|
|
@@ -834,7 +838,7 @@ export function buildAsyncRunnerSteps(id, params) {
|
|
|
834
838
|
let machineEnv;
|
|
835
839
|
if (requestedMachine) {
|
|
836
840
|
try {
|
|
837
|
-
const placement =
|
|
841
|
+
const placement = resolveMachinePlacement({ machine: requestedMachine, cwd: runnerCwd, stepCwd: s.cwd ?? params.machineCwd });
|
|
838
842
|
machine = placement.machine;
|
|
839
843
|
machineEnv = placement.env;
|
|
840
844
|
}
|
|
@@ -1331,6 +1335,7 @@ export function executeAsyncChain(id, params) {
|
|
|
1331
1335
|
}
|
|
1332
1336
|
const initialStatusAt = Date.now();
|
|
1333
1337
|
const initialCompletionOwnerId = ctx.completionOwnerId ?? currentCompletionOwnerId();
|
|
1338
|
+
const completionOwnerProcess = currentRecordedPid();
|
|
1334
1339
|
const launchParentSessionId = ctx.parentSessionId ?? ctx.currentSessionId;
|
|
1335
1340
|
try {
|
|
1336
1341
|
writeRetainedRequiredChildExtensions(asyncDir, params.requiredExtensions ?? ctx.childRuntime?.requiredExtensions ?? resolveRequiredChildExtensions(launchParentSessionId ?? undefined));
|
|
@@ -1355,6 +1360,7 @@ export function executeAsyncChain(id, params) {
|
|
|
1355
1360
|
asyncDir,
|
|
1356
1361
|
sessionId: ctx.currentSessionId,
|
|
1357
1362
|
completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
|
|
1363
|
+
completionOwnerProcess,
|
|
1358
1364
|
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
1359
1365
|
piPackageRoot,
|
|
1360
1366
|
childSessionFactoryModule: childSessionFactoryModule(),
|
|
@@ -1393,6 +1399,7 @@ export function executeAsyncChain(id, params) {
|
|
|
1393
1399
|
runId: id,
|
|
1394
1400
|
...(ctx.currentSessionId ? { sessionId: ctx.currentSessionId } : {}),
|
|
1395
1401
|
...(initialCompletionOwnerId ? { completionOwnerId: initialCompletionOwnerId } : {}),
|
|
1402
|
+
completionOwnerProcess,
|
|
1396
1403
|
mode: resultMode,
|
|
1397
1404
|
state: "running",
|
|
1398
1405
|
startedAt: initialStatusAt,
|
|
@@ -1620,7 +1627,7 @@ export function executeAsyncSingle(id, params) {
|
|
|
1620
1627
|
let machineEnv;
|
|
1621
1628
|
if (requestedMachine) {
|
|
1622
1629
|
try {
|
|
1623
|
-
const placement =
|
|
1630
|
+
const placement = resolveMachinePlacement({ machine: requestedMachine, cwd: runnerCwd, stepCwd: params.machineCwd });
|
|
1624
1631
|
machine = placement.machine;
|
|
1625
1632
|
machineEnv = placement.env;
|
|
1626
1633
|
}
|
|
@@ -1895,6 +1902,7 @@ export function executeAsyncSingle(id, params) {
|
|
|
1895
1902
|
let spawnResultOrPromise = {};
|
|
1896
1903
|
const initialStatusAt = Date.now();
|
|
1897
1904
|
const initialCompletionOwnerId = ctx.completionOwnerId ?? currentCompletionOwnerId();
|
|
1905
|
+
const completionOwnerProcess = currentRecordedPid();
|
|
1898
1906
|
const launchParentSessionId = ctx.parentSessionId ?? ctx.currentSessionId;
|
|
1899
1907
|
try {
|
|
1900
1908
|
spawnResultOrPromise = spawnRunner({
|
|
@@ -1977,6 +1985,7 @@ export function executeAsyncSingle(id, params) {
|
|
|
1977
1985
|
asyncDir,
|
|
1978
1986
|
sessionId: ctx.currentSessionId,
|
|
1979
1987
|
completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
|
|
1988
|
+
completionOwnerProcess,
|
|
1980
1989
|
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
1981
1990
|
piPackageRoot,
|
|
1982
1991
|
childSessionFactoryModule: childSessionFactoryModule(),
|
|
@@ -2018,6 +2027,7 @@ export function executeAsyncSingle(id, params) {
|
|
|
2018
2027
|
runId: id,
|
|
2019
2028
|
...(ctx.currentSessionId ? { sessionId: ctx.currentSessionId } : {}),
|
|
2020
2029
|
...(initialCompletionOwnerId ? { completionOwnerId: initialCompletionOwnerId } : {}),
|
|
2030
|
+
completionOwnerProcess,
|
|
2021
2031
|
mode: "single",
|
|
2022
2032
|
...(params.toolCallId ? { toolCallId: params.toolCallId } : {}),
|
|
2023
2033
|
state: "running",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { type AsyncJobState, type AsyncWidgetLayout, type ControlEvent, type SubagentState } from "../../shared/types.ts";
|
|
4
|
+
import type { RawDrainStatusObserver } from "../shared/readonly-drain-observation.ts";
|
|
5
|
+
import type { ResultDeliveryOwnership } from "./result-delivery-ownership.ts";
|
|
4
6
|
interface AsyncJobTrackerOptions {
|
|
5
7
|
completionRetentionMs?: number;
|
|
6
8
|
/** Slow safety sweep for liveness repair when filesystem events are missed. */
|
|
@@ -12,6 +14,8 @@ interface AsyncJobTrackerOptions {
|
|
|
12
14
|
platform?: NodeJS.Platform;
|
|
13
15
|
onJobTerminal?: (job: AsyncJobState) => void;
|
|
14
16
|
onJobCleanup?: (asyncId: string) => void;
|
|
17
|
+
/** Lets a run whose launching Pi exited hold its publication in the session that owns it. */
|
|
18
|
+
deliveryOwnerId?: ResultDeliveryOwnership["deliveryOwnerId"];
|
|
15
19
|
watch?: typeof fs.watch;
|
|
16
20
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
17
21
|
now?: () => number;
|
|
@@ -26,7 +30,8 @@ export declare function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">,
|
|
|
26
30
|
handleStarted: (data: unknown) => void;
|
|
27
31
|
handleComplete: (data: unknown) => void;
|
|
28
32
|
resetJobs: (ctx?: ExtensionContext) => void;
|
|
29
|
-
|
|
33
|
+
/** `observeStatus` also sees the statuses of other sessions' runs that the listing skips. */
|
|
34
|
+
restoreActiveJobs: (ctx?: ExtensionContext, observeStatus?: RawDrainStatusObserver) => void;
|
|
30
35
|
dispose: () => void;
|
|
31
36
|
};
|
|
32
37
|
export {};
|
|
@@ -15,6 +15,7 @@ import { withWorkflowChildRunModel } from "./workflow-terminal-proof.js";
|
|
|
15
15
|
import { validHostStepNodes } from "../shared/host-step-status.js";
|
|
16
16
|
import { readProcessTerminal } from "./process-terminal.js";
|
|
17
17
|
import { withCachedUiContext } from "../../shared/extension-context.js";
|
|
18
|
+
import { OWNING_SESSION_COMPLETION_OWNER } from "../../shared/completion-owner.js";
|
|
18
19
|
const CONTROL_EVENT_READ_CHUNK_BYTES = 64 * 1024;
|
|
19
20
|
const MAX_CONTROL_EVENT_LINE_BYTES = 1024 * 1024;
|
|
20
21
|
const CONTROL_EVENT_SCAN_WINDOW_BYTES = 2 * 1024 * 1024;
|
|
@@ -560,8 +561,11 @@ export function createAsyncJobTracker(pi, state, asyncDirRoot, options = {}) {
|
|
|
560
561
|
if (isTerminalJobStatus(job.status)) {
|
|
561
562
|
let publication = terminalPublications.get(job.asyncId);
|
|
562
563
|
if (!publication && status.mode !== "workflow" && status.processTerminal?.state === "pending"
|
|
563
|
-
&& status.runId === job.asyncId && status.
|
|
564
|
-
&& status.
|
|
564
|
+
&& status.runId === job.asyncId && status.completionOwnerId
|
|
565
|
+
&& ((status.sessionId === state.currentSessionId && status.completionOwnerId === state.completionOwnerId)
|
|
566
|
+
// Also covers a session inherited after /new or /resume: deliveryOwnerId checks run ownership.
|
|
567
|
+
|| (typeof status.sessionId === "string"
|
|
568
|
+
&& options.deliveryOwnerId?.(status.sessionId, status.completionOwnerId, status.completionOwnerProcess, status.parentWorkflowRunId) === OWNING_SESSION_COMPLETION_OWNER))) {
|
|
565
569
|
publication = { instanceId: status.processTerminal.runnerProcessInstanceId, pending: true };
|
|
566
570
|
terminalPublications.set(job.asyncId, publication);
|
|
567
571
|
}
|
|
@@ -883,7 +887,7 @@ export function createAsyncJobTracker(pi, state, asyncDirRoot, options = {}) {
|
|
|
883
887
|
rerenderWidget(ctx, []);
|
|
884
888
|
}
|
|
885
889
|
};
|
|
886
|
-
const restoreActiveJobs = (ctx) => {
|
|
890
|
+
const restoreActiveJobs = (ctx, observeStatus) => {
|
|
887
891
|
if (ctx?.hasUI)
|
|
888
892
|
state.lastUiContext = ctx;
|
|
889
893
|
state.statusProjectionSessionId = null;
|
|
@@ -891,7 +895,8 @@ export function createAsyncJobTracker(pi, state, asyncDirRoot, options = {}) {
|
|
|
891
895
|
return;
|
|
892
896
|
let runs;
|
|
893
897
|
try {
|
|
894
|
-
|
|
898
|
+
// A workflow whose Pi exited is repaired here, in the session that owns it, which then delivers its notice.
|
|
899
|
+
runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], sessionId: state.currentSessionId, resultsDir, kill: options.kill, now: options.now, adoptCompletionOwnerId: state.completionOwnerId }, observeStatus);
|
|
895
900
|
}
|
|
896
901
|
catch (error) {
|
|
897
902
|
console.error(`Failed to restore active async jobs from '${asyncDirRoot}':`, error);
|
|
@@ -142,6 +142,8 @@ interface AsyncRunListOptions {
|
|
|
142
142
|
includeNested?: boolean;
|
|
143
143
|
/** Explicit repair/debug escape hatch. Normal runtime paths must not set this. */
|
|
144
144
|
repairScan?: boolean;
|
|
145
|
+
/** This process's completion owner, adopted by an owner-exited workflow repair of a run the session owns; needs `sessionId`. */
|
|
146
|
+
adoptCompletionOwnerId?: string;
|
|
145
147
|
}
|
|
146
148
|
type TargetedAsyncRunResolution = {
|
|
147
149
|
kind: "exact";
|