pi-subagents 0.81.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 +38 -0
- package/bundle/agents.js +3 -3
- package/bundle/child-tool-plan.js +2 -2
- package/bundle/{chunk-LSRUTHT7.js → chunk-43SCHIMD.js} +1 -1
- package/bundle/{chunk-G5DKFLLI.js → chunk-4SDU2BZX.js} +15 -10
- package/bundle/{chunk-V2JWDB5U.js → chunk-52UBALJL.js} +2 -2
- package/bundle/{chunk-3DC5KBVP.js → chunk-5TYMSZX3.js} +1 -1
- package/bundle/{chunk-CEKS3TFO.js → chunk-7IVSUMQQ.js} +6 -6
- package/bundle/{chunk-KSWQVGDW.js → chunk-7PCKZHR5.js} +225 -18
- package/bundle/{chunk-563BHAZ5.js → chunk-7YMXOYEU.js} +33 -25
- package/bundle/{chunk-XMFLCKMQ.js → chunk-B5EKXDHE.js} +336 -130
- package/bundle/{chunk-ZBC44JJ3.js → chunk-CIZQXJ5Q.js} +22 -9
- package/bundle/{chunk-L5ZHE4L4.js → chunk-GC4DSPMH.js} +4 -4
- package/bundle/{chunk-ARRKBB2B.js → chunk-GJECHSAR.js} +2 -2
- package/bundle/{chunk-2ZSNH26H.js → chunk-M2CTFYT3.js} +7 -7
- package/bundle/{chunk-TUWLXTAM.js → chunk-MHZ4G23A.js} +681 -320
- package/bundle/{chunk-WSBNENFS.js → chunk-NGGAFDL6.js} +14 -4
- package/bundle/{chunk-VJHOELQP.js → chunk-NRQ75MGH.js} +160 -78
- package/bundle/{chunk-BJN5LUQY.js → chunk-OYF6AFDY.js} +1 -1
- package/bundle/{chunk-EYNJ6VL6.js → chunk-PLCCBLPY.js} +2 -2
- package/bundle/{chunk-74RT5YQE.js → chunk-RMQFMMAT.js} +123 -33
- package/bundle/{chunk-V6MR5TOQ.js → chunk-SIJ4S3EN.js} +25 -19
- package/bundle/{chunk-NNLASTQE.js → chunk-VH5OAWVG.js} +19 -5
- package/bundle/{chunk-BPS2AGBZ.js → chunk-W2SZ22EM.js} +93 -52
- package/bundle/{chunk-V3BIUPLU.js → chunk-WVD4UEL4.js} +256 -52
- package/bundle/{chunk-ZP3HODFH.js → chunk-XSWOTIA3.js} +1 -1
- package/bundle/{chunk-3IF2PGW7.js → chunk-YKEWIB4M.js} +1 -1
- package/bundle/{chunk-26C3WLYQ.js → chunk-ZLINIYTP.js} +497 -272
- package/bundle/{chunk-2XYWN7QJ.js → chunk-ZQGSDHL6.js} +2 -2
- package/bundle/control-channel.js +2 -2
- package/bundle/index.js +1 -1
- package/bundle/intercom-bridge.js +2 -2
- package/bundle/preflight.js +8 -8
- package/bundle/project-panes.js +3 -3
- package/docs/agents.md +31 -0
- package/docs/configuration.md +9 -1
- package/docs/tool-reference.md +3 -1
- package/docs/workflows.md +16 -0
- package/package.json +2 -2
- 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 +2 -0
- package/src/agents/frontmatter.js +15 -1
- package/src/agents/skills.js +2 -1
- package/src/extension/config.js +3 -0
- package/src/extension/herdr-pi-bridge.js +17 -10
- package/src/extension/index.js +82 -2
- 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 +1 -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 +52 -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 +6 -3
- 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/shared/completion-owner.d.ts +5 -0
- package/src/shared/completion-owner.js +5 -0
- package/src/shared/session-identity.d.ts +2 -0
- package/src/shared/session-identity.js +26 -0
- package/src/shared/types.d.ts +45 -6
- package/src/tui/fleet.d.ts +1 -0
- package/src/tui/fleet.js +24 -15
- 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/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-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
package/src/extension/index.js
CHANGED
|
@@ -73,10 +73,14 @@ import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./
|
|
|
73
73
|
import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.js";
|
|
74
74
|
import { formatWorkflowPreflightSummary, normalizeWorkflowPreflight } from "../workflows/workflow-preflight.js";
|
|
75
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";
|
|
76
79
|
import { finalizeToolResult } from "./tool-result.js";
|
|
77
80
|
import { flattenSubagentToolOptions } from "./subagent-options.js";
|
|
78
81
|
import { collectGoalContinuationNotices } from "../missions/goal-driver.js";
|
|
79
82
|
import { restoreForegroundRunHistory } from "../runs/foreground/foreground-history.js";
|
|
83
|
+
import { createInterruptedElsewhereCollector } from "../runs/background/interrupted-elsewhere.js";
|
|
80
84
|
import { resolveMissionStoreLocation } from "../missions/store.js";
|
|
81
85
|
import { listRetainedChildren } from "../runs/background/retained-children.js";
|
|
82
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";
|
|
@@ -434,6 +438,7 @@ export default function registerSubagentExtension(pi) {
|
|
|
434
438
|
refreshResultDelivery();
|
|
435
439
|
},
|
|
436
440
|
onJobCleanup: (asyncId) => deliveredRunIds.delete(asyncId),
|
|
441
|
+
deliveryOwnerId: resultDeliveryOwnership.deliveryOwnerId,
|
|
437
442
|
supervisorRequestState: supervisorChannel.getSupervisorRequestState,
|
|
438
443
|
onJobsChanged: programStatus.sync,
|
|
439
444
|
});
|
|
@@ -597,6 +602,60 @@ export default function registerSubagentExtension(pi) {
|
|
|
597
602
|
await waitForAdvertisement();
|
|
598
603
|
return (await getExecutor()).executePublic(id, params, signal, onUpdate, ctx);
|
|
599
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
|
+
};
|
|
600
659
|
const executeSubagentCollapsed = (id, params, signal, onUpdate, ctx) => {
|
|
601
660
|
if (ctx.hasUI)
|
|
602
661
|
ctx.ui.setToolsExpanded(false);
|
|
@@ -873,12 +932,31 @@ export default function registerSubagentExtension(pi) {
|
|
|
873
932
|
resetJobs(ctx);
|
|
874
933
|
logSlowPhase("reset-jobs", phaseStartedAt);
|
|
875
934
|
phaseStartedAt = Date.now();
|
|
876
|
-
|
|
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 });
|
|
877
938
|
logSlowPhase("foreground-history", phaseStartedAt);
|
|
878
939
|
phaseStartedAt = Date.now();
|
|
879
|
-
restoreActiveJobs(ctx);
|
|
940
|
+
restoreActiveJobs(ctx, interruptedElsewhere?.observeStatus);
|
|
880
941
|
logSlowPhase("active-job-restore", phaseStartedAt);
|
|
881
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();
|
|
882
960
|
scheduledRunManager.bindSession(ctx);
|
|
883
961
|
logSlowPhase("scheduled-runs", phaseStartedAt);
|
|
884
962
|
phaseStartedAt = Date.now();
|
|
@@ -903,6 +981,8 @@ export default function registerSubagentExtension(pi) {
|
|
|
903
981
|
if (runtimeCleaned)
|
|
904
982
|
return;
|
|
905
983
|
runtimeCleaned = true;
|
|
984
|
+
// Synchronously, so a reload's new runtime finds these runs unclaimed when it scans.
|
|
985
|
+
cancelPendingWorkflowAutoResumes((entry) => entry.owner === autoResumeOwner);
|
|
906
986
|
programStatus.dispose(shutdownReason);
|
|
907
987
|
releaseHostSessionLiveness();
|
|
908
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";
|
|
@@ -37,6 +37,9 @@ function isAsyncStatusIsolationError(asyncDir, error) {
|
|
|
37
37
|
|| message.startsWith(`Invalid host step '${statusPath}`)
|
|
38
38
|
|| /^(workflowChildren|Invalid workflowChildren)/.test(message);
|
|
39
39
|
}
|
|
40
|
+
// Status path -> last logged signature. A corrupt run is re-scanned on every
|
|
41
|
+
// status poll, so the line repeats only when the file or the outcome changes.
|
|
42
|
+
const reportedCorruptStatus = new Map();
|
|
40
43
|
function isolateCorruptActiveRun(asyncDir, runId, error, now) {
|
|
41
44
|
const statusPath = path.join(asyncDir, "status.json");
|
|
42
45
|
const processTerminal = readProcessTerminal(asyncDir, { runId });
|
|
@@ -61,7 +64,20 @@ function isolateCorruptActiveRun(asyncDir, runId, error, now) {
|
|
|
61
64
|
markerAction = `failed to release active marker: ${getErrorMessage(releaseError)}`;
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
|
-
|
|
67
|
+
const line = `[pi-subagents] Skipping corrupt active async run '${runId}' at '${statusPath}': ${getErrorMessage(error)}; ${markerAction}.`;
|
|
68
|
+
let fileSignature = "";
|
|
69
|
+
try {
|
|
70
|
+
const stat = fs.statSync(statusPath);
|
|
71
|
+
fileSignature = `${stat.size}:${stat.mtimeMs}`;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// A missing file still gets the line once; its message carries the cause.
|
|
75
|
+
}
|
|
76
|
+
const signature = `${fileSignature}\n${line}`;
|
|
77
|
+
if (reportedCorruptStatus.get(statusPath) === signature)
|
|
78
|
+
return;
|
|
79
|
+
reportedCorruptStatus.set(statusPath, signature);
|
|
80
|
+
console.error(line);
|
|
65
81
|
}
|
|
66
82
|
function isNotFoundError(error) {
|
|
67
83
|
return typeof error === "object"
|
|
@@ -425,16 +441,19 @@ export function listAsyncRuns(asyncDirRoot, options = {}, observeStatus) {
|
|
|
425
441
|
let status;
|
|
426
442
|
try {
|
|
427
443
|
// Reconciliation can rewrite state; session-scoped discovery does not own foreign runs.
|
|
444
|
+
let owned = false;
|
|
428
445
|
if (owners) {
|
|
429
446
|
const stored = readStatus(asyncDir);
|
|
430
447
|
if (stored && !owners.has(stored.sessionId)) {
|
|
431
448
|
observeStatus?.(stored);
|
|
432
449
|
continue;
|
|
433
450
|
}
|
|
451
|
+
owned = stored !== null;
|
|
434
452
|
}
|
|
453
|
+
// Only a run this session owns may adopt the caller's completion owner.
|
|
435
454
|
const reconciliation = options.reconcile === false
|
|
436
455
|
? undefined
|
|
437
|
-
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now }, observeStatus);
|
|
456
|
+
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now, adoptCompletionOwnerId: owned ? options.adoptCompletionOwnerId : undefined }, observeStatus);
|
|
438
457
|
status = (reconciliation?.status ?? readStatus(asyncDir));
|
|
439
458
|
if (options.reconcile === false)
|
|
440
459
|
observeStatus?.(status);
|
|
@@ -2,9 +2,8 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import { readStatus } from "../../shared/utils.js";
|
|
3
3
|
import { workflowAwaitedAsyncResultPath } from "./async-execution.js";
|
|
4
4
|
import { waitForImportedAsyncRoot } from "./chain-root-attachment.js";
|
|
5
|
-
import { currentPidNamespaceScope } from "./pid-namespace.js";
|
|
6
5
|
import { resultPayloadPathForSessionRun, resultSnapshotReplaced, withResultRunLease } from "./result-files.js";
|
|
7
|
-
import {
|
|
6
|
+
import { checkRecordedPidLiveness } from "./stale-run-reconciler.js";
|
|
8
7
|
function errorMessage(error) {
|
|
9
8
|
return error instanceof Error ? error.message : String(error);
|
|
10
9
|
}
|
|
@@ -13,11 +12,8 @@ function runnerExitedWithoutResult(asyncDir, runId, resultPath) {
|
|
|
13
12
|
const status = readStatus(asyncDir);
|
|
14
13
|
if (!status || (status.state !== "running" && status.state !== "queued") || typeof status.pid !== "number")
|
|
15
14
|
return false;
|
|
16
|
-
|
|
17
|
-
if (status.pidNamespaceScope
|
|
18
|
-
return false;
|
|
19
|
-
const pidScopeVerified = status.pidNamespaceScope !== undefined && status.pidNamespaceScope === observedScope;
|
|
20
|
-
if (checkPidLiveness(status.pid, undefined, pidScopeVerified) !== "dead")
|
|
15
|
+
// A pid from another namespace never reads as dead here, so the runner is not presumed gone.
|
|
16
|
+
if (checkRecordedPidLiveness({ pid: status.pid, pidNamespaceScope: status.pidNamespaceScope, bootId: status.bootId }) !== "dead")
|
|
21
17
|
return false;
|
|
22
18
|
return !fs.existsSync(resultPath) && !(status.sessionId && resultPayloadPathForSessionRun(asyncDir, status.sessionId, runId));
|
|
23
19
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RecordedPid } from "./stale-run-reconciler.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Identity of the current machine boot, read once per process. A pid recorded under a
|
|
4
|
+
* different boot id cannot still be running. Undefined when the platform has none.
|
|
5
|
+
*/
|
|
6
|
+
export declare function currentBootId(): string | undefined;
|
|
7
|
+
/** This process as a recorded pid, without the fields the platform does not have. */
|
|
8
|
+
export declare function currentRecordedPid(): RecordedPid;
|
|
9
|
+
/** Fixes the boot id without reading the OS; runners take Pi's value from their config. */
|
|
10
|
+
export declare function primeBootId(value: string | undefined): void;
|
|
11
|
+
/** Clears the cache so the next read calls `nextReader` (default: the OS reader). */
|
|
12
|
+
export declare function resetBootIdForTesting(nextReader?: () => string | undefined): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { currentPidNamespaceScope } from "./pid-namespace.js";
|
|
4
|
+
function readBootIdFromOs() {
|
|
5
|
+
try {
|
|
6
|
+
if (process.platform === "linux")
|
|
7
|
+
return fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf-8").trim() || undefined;
|
|
8
|
+
if (process.platform === "darwin") {
|
|
9
|
+
return execFileSync("sysctl", ["-n", "kern.bootsessionuuid"], { encoding: "utf-8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"] }).trim() || undefined;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// An unreadable boot id only means liveness falls back to probing the pid.
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
// Process-wide, so the bundled extension and the compiled runner modules share one value.
|
|
18
|
+
const BOOT_ID_KEY = Symbol.for("pi-subagents.boot-id.v1");
|
|
19
|
+
const runtime = globalThis;
|
|
20
|
+
let reader = readBootIdFromOs;
|
|
21
|
+
/**
|
|
22
|
+
* Identity of the current machine boot, read once per process. A pid recorded under a
|
|
23
|
+
* different boot id cannot still be running. Undefined when the platform has none.
|
|
24
|
+
*/
|
|
25
|
+
export function currentBootId() {
|
|
26
|
+
const cached = runtime[BOOT_ID_KEY];
|
|
27
|
+
if (cached)
|
|
28
|
+
return cached.value;
|
|
29
|
+
const value = reader();
|
|
30
|
+
runtime[BOOT_ID_KEY] = { value };
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
/** This process as a recorded pid, without the fields the platform does not have. */
|
|
34
|
+
export function currentRecordedPid() {
|
|
35
|
+
const pidNamespaceScope = currentPidNamespaceScope();
|
|
36
|
+
const bootId = currentBootId();
|
|
37
|
+
return { pid: process.pid, ...(pidNamespaceScope ? { pidNamespaceScope } : {}), ...(bootId ? { bootId } : {}) };
|
|
38
|
+
}
|
|
39
|
+
/** Fixes the boot id without reading the OS; runners take Pi's value from their config. */
|
|
40
|
+
export function primeBootId(value) {
|
|
41
|
+
runtime[BOOT_ID_KEY] = { value };
|
|
42
|
+
}
|
|
43
|
+
/** Clears the cache so the next read calls `nextReader` (default: the OS reader). */
|
|
44
|
+
export function resetBootIdForTesting(nextReader) {
|
|
45
|
+
reader = nextReader ?? readBootIdFromOs;
|
|
46
|
+
delete runtime[BOOT_ID_KEY];
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ForegroundResumeRun } from "../../shared/types.ts";
|
|
2
|
+
import type { RawDrainStatusObserver } from "../shared/readonly-drain-observation.ts";
|
|
3
|
+
interface InterruptedElsewhereCollector {
|
|
4
|
+
/** Observer for the async statuses session start already reads; keeps foreign ones that may be interrupted. */
|
|
5
|
+
observeStatus: RawDrainStatusObserver;
|
|
6
|
+
/** Receives the foreground records of other sessions that session start already reads. */
|
|
7
|
+
observeForegroundRun: (run: ForegroundResumeRun) => void;
|
|
8
|
+
/** One line per other session that can be reopened and has runs whose Pi is dead. */
|
|
9
|
+
lines: () => string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Collects runs of other sessions in this folder that were cut off because their Pi died, from what
|
|
13
|
+
* session start reads anyway. Read-only: nothing is written to those runs or sessions. The cheap
|
|
14
|
+
* filters run as records arrive; `lines` reads each session file's header once, then probes each
|
|
15
|
+
* candidate's pid once.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createInterruptedElsewhereCollector(input: {
|
|
18
|
+
currentSessionId: string;
|
|
19
|
+
cwd: string;
|
|
20
|
+
now: number;
|
|
21
|
+
}): InterruptedElsewhereCollector;
|
|
22
|
+
export {};
|