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
|
@@ -49,7 +49,7 @@ import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDi
|
|
|
49
49
|
import { isContextOverflow } from "../shared/model-resolution.js";
|
|
50
50
|
import { processTerminalPath, writeProcessTerminalCandidate } from "./process-terminal.js";
|
|
51
51
|
import { persistRunnerStartupFailure } from "./runner-startup-failure.js";
|
|
52
|
-
import {
|
|
52
|
+
import { currentRecordedPid } from "./boot-id.js";
|
|
53
53
|
import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, unconsumedSteerReason, updateSteeringTarget } from "./steering.js";
|
|
54
54
|
import { PROMPT_REDACTED, SUBAGENT_STOPPED_MESSAGE, SUBAGENT_TIMED_OUT_MESSAGE, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getAgentDir, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.js";
|
|
55
55
|
import { planAbortRecovery } from "../shared/abort-recovery.js";
|
|
@@ -77,6 +77,7 @@ import { resolveCodexExecLaunch } from "../shared/codex-exec-adapter.js";
|
|
|
77
77
|
import { resolveCursorAgentLaunch } from "../shared/cursor-agent-adapter.js";
|
|
78
78
|
import { resolveExternalCliRunnerStatus } from "../shared/external-cli-contract.js";
|
|
79
79
|
import { formatHerdrMachineHint, prepareHerdrMachineExternalCliRun } from "../shared/herdr-machine.js";
|
|
80
|
+
import { isRexLocalMachine } from "../shared/rex-machine.js";
|
|
80
81
|
import { HerdrExternalNeedsAttentionError, createHerdrExternalAdapter, prepareInternalHerdrExternalAdapter } from "../shared/herdr-external-adapters.js";
|
|
81
82
|
import { runExternalJob } from "../shared/external-job-runner.js";
|
|
82
83
|
import { createOrcaProgressTab } from "../shared/orca-progress-tabs.js";
|
|
@@ -522,6 +523,7 @@ export async function runSingleStepInner(step, ctx) {
|
|
|
522
523
|
capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
|
|
523
524
|
inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
|
|
524
525
|
requiredExtensions: step.requiredExtensions ?? ctx.inheritedChildRuntime?.requiredExtensions,
|
|
526
|
+
agentName: step.agent,
|
|
525
527
|
permissionRules: step.permissionRules,
|
|
526
528
|
}));
|
|
527
529
|
}
|
|
@@ -608,6 +610,8 @@ export async function runSingleStepInner(step, ctx) {
|
|
|
608
610
|
return omitUndefinedProperties({ agent: step.agent, context: step.context, output: message, error: message, exitCode: 1, stopped: stopped || undefined, timedOut: stopped ? undefined : true });
|
|
609
611
|
}
|
|
610
612
|
if (step.machine) {
|
|
613
|
+
if (step.machine.provider !== "herdr")
|
|
614
|
+
throw new Error("Rex supports native Pi children only.");
|
|
611
615
|
const runner = resolveExternalCliRunnerStatus({ ...step.runner, machine: step.machine });
|
|
612
616
|
const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
|
|
613
617
|
const placedRunId = `${ctx.id}-${ctx.flatIndex}`;
|
|
@@ -781,7 +785,8 @@ export async function runSingleStepInner(step, ctx) {
|
|
|
781
785
|
let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
|
|
782
786
|
let toolBudgetBlocked = false;
|
|
783
787
|
let actualLaunchContractDigest = step.launchContractDigest;
|
|
784
|
-
|
|
788
|
+
// A machine 'local' child edits this disk, so its local Git evidence is authoritative, as for an unplaced run.
|
|
789
|
+
const mutationSnapshot = step.machine && !isRexLocalMachine(step.machine) ? { source: "tracked-files", trackedOnly: true, cwd: step.cwd ?? ctx.cwd, dirtyFiles: [], fingerprints: {}, unavailable: "Local Git evidence is not authoritative for a pane-native remote run." } : snapshotTrackedMutations(step.cwd ?? ctx.cwd);
|
|
785
790
|
let finalMutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
|
|
786
791
|
let contextOverflow = false;
|
|
787
792
|
let launchWarningsEmitted = false;
|
|
@@ -1004,7 +1009,7 @@ export async function runSingleStepInner(step, ctx) {
|
|
|
1004
1009
|
? formatEmptyTerminalAssistantResponseError(run.messages)
|
|
1005
1010
|
: undefined;
|
|
1006
1011
|
const remoteGitChanged = run.nativeMachine?.initialGit && run.nativeMachine.finalGit ? run.nativeMachine.initialGit.head !== run.nativeMachine.finalGit.head || run.nativeMachine.initialGit.dirty !== run.nativeMachine.finalGit.dirty : undefined;
|
|
1007
|
-
const mutationEvidence = run.nativeMachine ? { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
|
|
1012
|
+
const mutationEvidence = run.nativeMachine && !isRexLocalMachine(step.machine) ? { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
|
|
1008
1013
|
finalMutationEvidence = mutationEvidence;
|
|
1009
1014
|
const mutationAttemptObserved = run.observedMutationAttempt === true || mutationEvidence.attemptedMutation === true;
|
|
1010
1015
|
// Validated structured output is persistable file content even when the final text is only
|
|
@@ -1669,6 +1674,7 @@ export async function runSubagent(config, childSessions) {
|
|
|
1669
1674
|
...(config.toolCallId ? { toolCallId: config.toolCallId } : {}),
|
|
1670
1675
|
...(config.sessionId ? { sessionId: config.sessionId } : {}),
|
|
1671
1676
|
...(config.completionOwnerId ? { completionOwnerId: config.completionOwnerId } : {}),
|
|
1677
|
+
...(config.completionOwnerProcess ? { completionOwnerProcess: config.completionOwnerProcess } : {}),
|
|
1672
1678
|
mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
|
|
1673
1679
|
...(config.nestedSelf ? { isNested: true } : {}),
|
|
1674
1680
|
state: "running",
|
|
@@ -1680,8 +1686,7 @@ export async function runSubagent(config, childSessions) {
|
|
|
1680
1686
|
...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
|
|
1681
1687
|
...(config.toolBudget ? { toolBudget: initialToolBudgetState(config.toolBudget) } : {}),
|
|
1682
1688
|
...(config.usageBudget ? { usageBudget: usageBudgetState(config.usageBudget, undefined) } : {}),
|
|
1683
|
-
|
|
1684
|
-
pidNamespaceScope: currentPidNamespaceScope(),
|
|
1689
|
+
...currentRecordedPid(),
|
|
1685
1690
|
cwd,
|
|
1686
1691
|
currentStep: 0,
|
|
1687
1692
|
chainStepCount: steps.length,
|
|
@@ -1909,6 +1914,8 @@ export async function runSubagent(config, childSessions) {
|
|
|
1909
1914
|
cwd,
|
|
1910
1915
|
sessionId: config.sessionId,
|
|
1911
1916
|
completionOwnerId: config.completionOwnerId,
|
|
1917
|
+
completionOwnerProcess: config.completionOwnerProcess,
|
|
1918
|
+
parentWorkflowRunId: config.parentWorkflowRunId,
|
|
1912
1919
|
sessionFile: statusPayload.sessionFile ?? latestSessionFile,
|
|
1913
1920
|
}), (filePath, payload) => publishRunResult(filePath, () => writePendingAsyncResultFile(filePath, payload)));
|
|
1914
1921
|
};
|
|
@@ -4797,6 +4804,7 @@ export async function runSubagent(config, childSessions) {
|
|
|
4797
4804
|
runtimeAcknowledgedExtensions: singleRuntimeAcknowledgedExtensions,
|
|
4798
4805
|
sessionId: config.sessionId,
|
|
4799
4806
|
completionOwnerId: config.completionOwnerId,
|
|
4807
|
+
completionOwnerProcess: config.completionOwnerProcess,
|
|
4800
4808
|
sessionFile: effectiveSessionFile,
|
|
4801
4809
|
intercomTarget: config.controlIntercomTarget,
|
|
4802
4810
|
shareUrl,
|
|
@@ -54,7 +54,7 @@ export interface SubagentWaitParams {
|
|
|
54
54
|
all?: boolean;
|
|
55
55
|
/** Give up after this many milliseconds. Defaults to waitTool.defaultTimeoutMs, then 30 minutes. */
|
|
56
56
|
timeoutMs?: number;
|
|
57
|
-
/** False keeps a blocking wait open through
|
|
57
|
+
/** False keeps a blocking wait open through all attention except supervisor/contact requests, such as idle time, a tool held open, or repeated tool failures. */
|
|
58
58
|
stopOnAttention?: boolean;
|
|
59
59
|
}
|
|
60
60
|
/** Minimal event-bus surface wait subscribes to (matches pi.events). */
|
|
@@ -707,8 +707,13 @@ export async function waitForSubagents(params, signal, deps) {
|
|
|
707
707
|
const supervisorAttentionHint = relevantAttention.some(hasSupervisorTool)
|
|
708
708
|
? " Reply to any pending supervisor request. If subagent_supervisor({ action: \"pending\" }) is empty, check intercom({ action: \"pending\" }) because an external intercom tool may own the request."
|
|
709
709
|
: "";
|
|
710
|
+
// Only when watchdog attention alone ended the wait: stopOnAttention: false would have kept blocking.
|
|
711
|
+
const keepWaitingHint = stopOnAttention && supervisorAttentionHint === ""
|
|
712
|
+
&& (waitForAll || finishedAsyncCount + providerFinishedCount === 0)
|
|
713
|
+
? " These runs are still running and none is waiting on a supervisor reply. To keep waiting through this kind of attention, call bg_wait again with stopOnAttention: false; supervisor requests still end that wait."
|
|
714
|
+
: "";
|
|
710
715
|
const attentionNote = relevantAttention.length > 0
|
|
711
|
-
? ` ${relevantAttention.length} run(s) need attention: ${relevantAttention.map((run) => run.id).join(", ")} —${supervisorAttentionHint} inspect with subagent({ action: "status" }) then steer a top-level live async child, resume a paused/completed/failed child, or interrupt explicitly
|
|
716
|
+
? ` ${relevantAttention.length} run(s) need attention: ${relevantAttention.map((run) => run.id).join(", ")} —${supervisorAttentionHint} inspect with subagent({ action: "status" }) then steer a top-level live async child, resume a paused/completed/failed child, or interrupt explicitly.${keepWaitingHint}`
|
|
712
717
|
: "";
|
|
713
718
|
const stillRunning = active.filter((run) => initialAsyncIds.has(run.id)).length
|
|
714
719
|
+ providerActive.filter((item) => initialProviderIds.has(backgroundWorkIdentity(item))).length;
|
|
@@ -99,8 +99,10 @@ function sessionDirs(asyncDirRoot, sessionId) {
|
|
|
99
99
|
throw error;
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
function recentMarkerFiles(dirs, limit) {
|
|
102
|
+
function recentMarkerFiles(dirs, limit, endedAfter) {
|
|
103
103
|
return dirs.flatMap(markerFiles)
|
|
104
|
+
// The name starts with the run's end time, so an old run is skipped without reading its marker.
|
|
105
|
+
.filter((marker) => endedAfter === undefined || !(Number(marker.name.slice(0, TIMESTAMP_WIDTH)) < endedAfter))
|
|
104
106
|
.sort((left, right) => right.name.localeCompare(left.name))
|
|
105
107
|
.slice(0, limit)
|
|
106
108
|
.map((marker) => path.join(marker.dir, marker.name));
|
|
@@ -135,7 +137,7 @@ export function readTerminalRunToolCallIndex(asyncDirRoot, toolCallId) {
|
|
|
135
137
|
}
|
|
136
138
|
export function readRecentTerminalRunIndex(asyncDirRoot, options = {}) {
|
|
137
139
|
const limit = options.limit === undefined ? Number.POSITIVE_INFINITY : Math.max(0, Math.floor(options.limit));
|
|
138
|
-
const candidates = recentMarkerFiles(sessionDirs(asyncDirRoot, options.sessionId), limit);
|
|
140
|
+
const candidates = recentMarkerFiles(sessionDirs(asyncDirRoot, options.sessionId), limit, options.endedAfter);
|
|
139
141
|
const runIds = [];
|
|
140
142
|
const seen = new Set();
|
|
141
143
|
for (const marker of candidates) {
|
|
@@ -44,6 +44,8 @@ function sealPausedRun(asyncDir, status, resultsDir) {
|
|
|
44
44
|
sessionId: status.sessionId,
|
|
45
45
|
asyncDir,
|
|
46
46
|
...(status.completionOwnerId ? { completionOwnerId: status.completionOwnerId } : {}),
|
|
47
|
+
...(status.completionOwnerProcess ? { completionOwnerProcess: status.completionOwnerProcess } : {}),
|
|
48
|
+
...(status.parentWorkflowRunId ? { parentWorkflowRunId: status.parentWorkflowRunId } : {}),
|
|
47
49
|
...(status.toolCallId ? { toolCallId: status.toolCallId } : {}),
|
|
48
50
|
results: (status.steps ?? []).map((step) => ({
|
|
49
51
|
agent: step.agent,
|
|
@@ -146,7 +148,7 @@ export function stopAsyncRun(state, runId, kill, location, childId) {
|
|
|
146
148
|
if (!target)
|
|
147
149
|
return null;
|
|
148
150
|
const status = reconcileAsyncRun(target.asyncDir, { kill }).status;
|
|
149
|
-
// A
|
|
151
|
+
// A relaunched workflow stops the exact child run it re-attached from its relaunch chain, whichever session launched it.
|
|
150
152
|
if (state.currentSessionId && !ownsRun(state.currentSessionId, status?.sessionId) && location?.reattachedRunId !== target.asyncId) {
|
|
151
153
|
return {
|
|
152
154
|
content: [{ type: "text", text: `Async run '${target.asyncId}' was not found in the active session.` }],
|
|
@@ -46,6 +46,7 @@ import { buildInProcessChildLaunch, createReportedChildSessionInput } from "../s
|
|
|
46
46
|
import { childSessionFactory, childSessionHasQueuedMessages, projectChildSessionEventForJson } from "../shared/child-session.js";
|
|
47
47
|
import { reconcileAttemptUsage } from "../shared/usage-reconciliation.js";
|
|
48
48
|
import { clearProviderRetry, observeProviderDiagnostics, snapshotProviderDiagnostics } from "../../shared/provider-diagnostics.js";
|
|
49
|
+
import { isRexLocalMachine } from "../shared/rex-machine.js";
|
|
49
50
|
const artifactOutputByResult = new WeakMap();
|
|
50
51
|
const acceptanceOutputByResult = new WeakMap();
|
|
51
52
|
function emptyUsage() {
|
|
@@ -441,7 +442,9 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
|
|
|
441
442
|
};
|
|
442
443
|
return result;
|
|
443
444
|
}
|
|
444
|
-
|
|
445
|
+
// A machine 'local' child edits this disk, so its local Git evidence is authoritative, as for an unplaced run.
|
|
446
|
+
const mutationCwd = isRexLocalMachine(options.machine) ? options.machine.cwd : options.cwd ?? runtimeCwd;
|
|
447
|
+
const mutationSnapshot = options.machine && !isRexLocalMachine(options.machine) ? { source: "tracked-files", trackedOnly: true, cwd: mutationCwd, dirtyFiles: [], fingerprints: {}, unavailable: "Local Git evidence is not authoritative for a pane-native remote run." } : snapshotTrackedMutations(mutationCwd);
|
|
445
448
|
let structuredOutputToolInvoked = false;
|
|
446
449
|
let structuredOutputMessageStartIndex;
|
|
447
450
|
let toolAvailabilityError;
|
|
@@ -1259,7 +1262,7 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
|
|
|
1259
1262
|
toolAvailabilityError = toolDiagnosticError;
|
|
1260
1263
|
result.runtimeAcknowledgedExtensions = capture.runtimeAcknowledgedExtensions();
|
|
1261
1264
|
if (session?.machineEvidence)
|
|
1262
|
-
result.nativeMachine = { provider:
|
|
1265
|
+
result.nativeMachine = { provider: session.machineEvidence.provider, machineId: session.machineEvidence.machineId, ...(session.machineEvidence.initial ? { initialGit: session.machineEvidence.initial } : {}), ...(session.machineEvidence.final ? { finalGit: session.machineEvidence.final } : {}) };
|
|
1263
1266
|
let closeError = result.error ?? toolDiagnosticError ?? assistantError;
|
|
1264
1267
|
const promptErrorMessage = promptError === undefined ? undefined : promptError instanceof Error ? promptError.message : String(promptError);
|
|
1265
1268
|
if (promptError !== undefined)
|
|
@@ -1533,7 +1536,7 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
|
|
|
1533
1536
|
durationMs: progress.durationMs,
|
|
1534
1537
|
};
|
|
1535
1538
|
const remoteGitChanged = result.nativeMachine?.initialGit && result.nativeMachine.finalGit ? result.nativeMachine.initialGit.head !== result.nativeMachine.finalGit.head || result.nativeMachine.initialGit.dirty !== result.nativeMachine.finalGit.dirty : undefined;
|
|
1536
|
-
const mutationEvidence = result.nativeMachine ? { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot,
|
|
1539
|
+
const mutationEvidence = result.nativeMachine && !isRexLocalMachine(options.machine) ? { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot, mutationCwd);
|
|
1537
1540
|
const acceptanceOutput = getFinalOutput(result.messages ?? []);
|
|
1538
1541
|
const structuredText = result.structuredOutput === undefined ? undefined : JSON.stringify(result.structuredOutput, null, 2);
|
|
1539
1542
|
let fullOutput = stripAcceptanceReport(acceptanceOutput);
|
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
import type { SubagentState } from "../../shared/types.ts";
|
|
1
|
+
import type { ForegroundResumeChild, ForegroundResumeRun, SubagentState } from "../../shared/types.ts";
|
|
2
2
|
export declare const MAX_REMEMBERED_FOREGROUND_RUNS = 50;
|
|
3
|
-
export declare
|
|
3
|
+
export declare const FOREGROUND_OWNER_EXITED_STOP_ERROR = "Subagent stopped because the Pi process that ran it exited before the run finished.";
|
|
4
|
+
/**
|
|
5
|
+
* Saves one foreground run to its own history file. A run with a `running` child is a launch-time
|
|
6
|
+
* record; the run's final save replaces it.
|
|
7
|
+
*/
|
|
8
|
+
export declare function persistForegroundRun(run: ForegroundResumeRun, options?: {
|
|
4
9
|
resultsDir?: string;
|
|
5
10
|
limit?: number;
|
|
6
11
|
}): void;
|
|
12
|
+
/** Drops this process's `running` record for a launch that threw, so it never loads as interrupted. */
|
|
13
|
+
export declare function discardRunningForegroundRun(runId: string, options?: {
|
|
14
|
+
resultsDir?: string;
|
|
15
|
+
}): void;
|
|
16
|
+
/** For a record with a `running` child: its owning Pi process is gone. */
|
|
17
|
+
export declare function ownerConfirmedDead(run: ForegroundResumeRun): boolean;
|
|
18
|
+
/** State text for a remembered child; a child cut off by a Pi exit names the resume target. */
|
|
19
|
+
export declare function foregroundChildStateLabel(run: Pick<ForegroundResumeRun, "runId">, child: Pick<ForegroundResumeChild, "status" | "error" | "sessionFile">): string;
|
|
7
20
|
export declare function restoreForegroundRunHistory(state: SubagentState, options?: {
|
|
8
21
|
resultsDir?: string;
|
|
9
22
|
sessionId?: string | null;
|
|
10
23
|
limit?: number;
|
|
24
|
+
observeForeign?: (run: ForegroundResumeRun) => void;
|
|
11
25
|
}): number;
|
|
@@ -5,12 +5,18 @@ import { writePrivateAtomicJson } from "../../shared/atomic-json.js";
|
|
|
5
5
|
import { utf8Tail } from "../../shared/utf8.js";
|
|
6
6
|
import { validateAcceptanceInput } from "../shared/acceptance.js";
|
|
7
7
|
import { validateModelResponseAliases } from "../../shared/model-response-aliases.js";
|
|
8
|
+
import { checkRecordedPidLiveness, isRecordedPid } from "../background/stale-run-reconciler.js";
|
|
9
|
+
import { encodeIndexSegment } from "../background/index-segment.js";
|
|
8
10
|
export const MAX_REMEMBERED_FOREGROUND_RUNS = 50;
|
|
9
|
-
const
|
|
11
|
+
export const FOREGROUND_OWNER_EXITED_STOP_ERROR = "Subagent stopped because the Pi process that ran it exited before the run finished.";
|
|
10
12
|
const MAX_INLINE_OUTPUT_BYTES = 64 * 1024;
|
|
11
13
|
const MAX_RESUME_CONTRACT_BYTES = 64 * 1024;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
// One file per run, so Pi processes sharing the results dir never rewrite each other's records.
|
|
15
|
+
function historyDir(resultsDir) {
|
|
16
|
+
return path.join(resultsDir, "foreground-history");
|
|
17
|
+
}
|
|
18
|
+
function runHistoryPath(resultsDir, runId) {
|
|
19
|
+
return path.join(historyDir(resultsDir), `${encodeIndexSegment(runId)}.json`);
|
|
14
20
|
}
|
|
15
21
|
function boundedTail(value) {
|
|
16
22
|
return utf8Tail(value, MAX_INLINE_OUTPUT_BYTES).text;
|
|
@@ -57,6 +63,13 @@ function compactChild(child) {
|
|
|
57
63
|
function isRestorableForegroundStatus(status) {
|
|
58
64
|
return status === "completed" || status === "failed" || status === "paused" || status === "stopped";
|
|
59
65
|
}
|
|
66
|
+
function hasRunningChild(run) {
|
|
67
|
+
return run.children.some((child) => child.status === "running");
|
|
68
|
+
}
|
|
69
|
+
/** A `running` record is kept only for a single run whose owning Pi process can be probed later. */
|
|
70
|
+
function isAdmittedChildStatus(run, status) {
|
|
71
|
+
return isRestorableForegroundStatus(status) || (status === "running" && run.mode === "single" && isRecordedPid(run.owner));
|
|
72
|
+
}
|
|
60
73
|
function isRestorableResumeContract(value) {
|
|
61
74
|
if (value === undefined)
|
|
62
75
|
return true;
|
|
@@ -86,8 +99,9 @@ function isRestorableResumeContract(value) {
|
|
|
86
99
|
function compactRun(run) {
|
|
87
100
|
if (!run.sessionId)
|
|
88
101
|
return undefined;
|
|
89
|
-
if (run.children.length === 0 || !run.children.every((child) =>
|
|
102
|
+
if (run.children.length === 0 || !run.children.every((child) => isAdmittedChildStatus(run, child.status) && isRestorableResumeContract(child.resumeContract)))
|
|
90
103
|
return undefined;
|
|
104
|
+
const running = hasRunningChild(run);
|
|
91
105
|
return {
|
|
92
106
|
runId: run.runId,
|
|
93
107
|
mode: run.mode,
|
|
@@ -95,23 +109,26 @@ function compactRun(run) {
|
|
|
95
109
|
sessionId: run.sessionId,
|
|
96
110
|
updatedAt: run.updatedAt,
|
|
97
111
|
children: run.children.map(compactChild),
|
|
112
|
+
...(running ? { owner: run.owner } : {}),
|
|
113
|
+
...(run.parentWorkflowRunId ? { parentWorkflowRunId: run.parentWorkflowRunId } : {}),
|
|
98
114
|
};
|
|
99
115
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (!fs.existsSync(filePath))
|
|
103
|
-
return { version: HISTORY_VERSION, runs: [] };
|
|
116
|
+
/** One saved run, or undefined when the file is missing, unreadable or invalid. */
|
|
117
|
+
function readRunFile(filePath) {
|
|
104
118
|
try {
|
|
105
119
|
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
return isRestorableRun(parsed) ? parsed : undefined;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function historyFiles(dir) {
|
|
127
|
+
try {
|
|
128
|
+
return fs.readdirSync(dir).filter((name) => name.endsWith(".json"));
|
|
112
129
|
}
|
|
113
130
|
catch {
|
|
114
|
-
return
|
|
131
|
+
return [];
|
|
115
132
|
}
|
|
116
133
|
}
|
|
117
134
|
function isRestorableRun(value) {
|
|
@@ -125,40 +142,125 @@ function isRestorableRun(value) {
|
|
|
125
142
|
&& typeof run.updatedAt === "number" && Number.isFinite(run.updatedAt)
|
|
126
143
|
&& Array.isArray(run.children)
|
|
127
144
|
&& run.children.length > 0
|
|
145
|
+
&& (run.owner === undefined || isRecordedPid(run.owner))
|
|
146
|
+
&& (run.parentWorkflowRunId === undefined || typeof run.parentWorkflowRunId === "string")
|
|
128
147
|
&& run.children.every((child) => Boolean(child && typeof child === "object" && !Array.isArray(child)
|
|
129
148
|
&& typeof child.agent === "string"
|
|
130
149
|
&& typeof child.index === "number"
|
|
131
150
|
&& isRestorableResumeContract(child.resumeContract)
|
|
132
|
-
&&
|
|
151
|
+
&& isAdmittedChildStatus(run, child.status)));
|
|
133
152
|
}
|
|
134
|
-
|
|
135
|
-
|
|
153
|
+
/** Deletes the oldest final run files beyond the cap; running records stay, so the cap can be exceeded while runs are live. */
|
|
154
|
+
function pruneHistory(dir, limit) {
|
|
155
|
+
const files = historyFiles(dir);
|
|
156
|
+
if (files.length <= limit)
|
|
157
|
+
return;
|
|
158
|
+
const byAge = files.flatMap((name) => {
|
|
159
|
+
try {
|
|
160
|
+
return [{ name, mtimeMs: fs.statSync(path.join(dir, name)).mtimeMs }];
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
}).sort((left, right) => left.mtimeMs - right.mtimeMs);
|
|
166
|
+
for (const { name, mtimeMs } of byAge.slice(0, byAge.length - limit)) {
|
|
167
|
+
const filePath = path.join(dir, name);
|
|
168
|
+
// A running record is replaced later by its run's final save, possibly in another Pi process;
|
|
169
|
+
// once its owner is confirmed dead nothing can replace it, so it is final.
|
|
170
|
+
const run = readRunFile(filePath);
|
|
171
|
+
if (run && hasRunningChild(run) && !ownerConfirmedDead(run))
|
|
172
|
+
continue;
|
|
173
|
+
// A file rewritten since the snapshot is a fresh record, not the old one judged here.
|
|
174
|
+
try {
|
|
175
|
+
if (fs.statSync(filePath).mtimeMs !== mtimeMs)
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
fs.rmSync(filePath, { force: true });
|
|
182
|
+
}
|
|
136
183
|
}
|
|
137
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Saves one foreground run to its own history file. A run with a `running` child is a launch-time
|
|
186
|
+
* record; the run's final save replaces it.
|
|
187
|
+
*/
|
|
188
|
+
export function persistForegroundRun(run, options = {}) {
|
|
189
|
+
const compact = compactRun(run);
|
|
190
|
+
if (!compact)
|
|
191
|
+
return;
|
|
138
192
|
const resultsDir = options.resultsDir ?? DIRS.results;
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (compact)
|
|
145
|
-
merged.set(compact.runId, compact);
|
|
193
|
+
const dir = historyDir(resultsDir);
|
|
194
|
+
if (!fs.existsSync(dir)) {
|
|
195
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
196
|
+
// The single shared file used before per-run files is not read or migrated.
|
|
197
|
+
fs.rmSync(path.join(resultsDir, "foreground-history.json"), { force: true });
|
|
146
198
|
}
|
|
147
|
-
|
|
148
|
-
|
|
199
|
+
writePrivateAtomicJson(runHistoryPath(resultsDir, compact.runId), compact);
|
|
200
|
+
pruneHistory(dir, options.limit ?? MAX_REMEMBERED_FOREGROUND_RUNS);
|
|
201
|
+
}
|
|
202
|
+
/** Drops this process's `running` record for a launch that threw, so it never loads as interrupted. */
|
|
203
|
+
export function discardRunningForegroundRun(runId, options = {}) {
|
|
204
|
+
const filePath = runHistoryPath(options.resultsDir ?? DIRS.results, runId);
|
|
205
|
+
const run = readRunFile(filePath);
|
|
206
|
+
if (run && hasRunningChild(run) && run.owner?.pid === process.pid)
|
|
207
|
+
fs.rmSync(filePath, { force: true });
|
|
208
|
+
}
|
|
209
|
+
/** For a record with a `running` child: its owning Pi process is gone. */
|
|
210
|
+
export function ownerConfirmedDead(run) {
|
|
211
|
+
// readRunFile admits a running child only with a valid owner.
|
|
212
|
+
return checkRecordedPidLiveness(run.owner) === "dead";
|
|
213
|
+
}
|
|
214
|
+
/** A run whose owning Pi process is gone loads as stopped and resumable; one whose owner may be alive is not loaded. */
|
|
215
|
+
function restoreInterruptedRun(run) {
|
|
216
|
+
if (!ownerConfirmedDead(run))
|
|
217
|
+
return undefined;
|
|
218
|
+
const { owner: _owner, ...rest } = run;
|
|
219
|
+
return {
|
|
220
|
+
...rest,
|
|
221
|
+
children: run.children.map((child) => {
|
|
222
|
+
if (child.status !== "running")
|
|
223
|
+
return child;
|
|
224
|
+
const { activityState: _activityState, currentTool: _currentTool, currentToolStartedAt: _currentToolStartedAt, currentPath: _currentPath, sessionFile, ...stopped } = child;
|
|
225
|
+
return {
|
|
226
|
+
...stopped,
|
|
227
|
+
status: "stopped",
|
|
228
|
+
exitCode: 1,
|
|
229
|
+
error: FOREGROUND_OWNER_EXITED_STOP_ERROR,
|
|
230
|
+
finalOutput: FOREGROUND_OWNER_EXITED_STOP_ERROR,
|
|
231
|
+
...(sessionFile && fs.existsSync(sessionFile) ? { sessionFile } : {}),
|
|
232
|
+
};
|
|
233
|
+
}),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/** State text for a remembered child; a child cut off by a Pi exit names the resume target. */
|
|
237
|
+
export function foregroundChildStateLabel(run, child) {
|
|
238
|
+
if (child.status !== "stopped" || child.error !== FOREGROUND_OWNER_EXITED_STOP_ERROR)
|
|
239
|
+
return child.status;
|
|
240
|
+
// Without a transcript there is nothing to resume from.
|
|
241
|
+
return child.sessionFile ? `interrupted when Pi exited · resume ${run.runId}` : "interrupted when Pi exited";
|
|
149
242
|
}
|
|
150
243
|
export function restoreForegroundRunHistory(state, options = {}) {
|
|
151
244
|
const sessionId = options.sessionId ?? state.currentSessionId;
|
|
152
245
|
if (!sessionId)
|
|
153
246
|
return 0;
|
|
154
|
-
const
|
|
155
|
-
const
|
|
247
|
+
const dir = historyDir(options.resultsDir ?? DIRS.results);
|
|
248
|
+
const saved = historyFiles(dir).flatMap((name) => readRunFile(path.join(dir, name)) ?? []);
|
|
249
|
+
// Other sessions' records that may have been cut off, for a read-only pointer to them.
|
|
250
|
+
if (options.observeForeign)
|
|
251
|
+
for (const run of saved)
|
|
252
|
+
if (run.sessionId !== sessionId && hasRunningChild(run))
|
|
253
|
+
options.observeForeign(run);
|
|
254
|
+
const runs = saved.filter((run) => run.sessionId === sessionId).sort((left, right) => right.updatedAt - left.updatedAt).slice(0, options.limit ?? MAX_REMEMBERED_FOREGROUND_RUNS);
|
|
156
255
|
state.foregroundRuns ??= new Map();
|
|
157
256
|
let restored = 0;
|
|
158
257
|
for (const run of runs) {
|
|
159
258
|
if (state.foregroundRuns.has(run.runId))
|
|
160
259
|
continue;
|
|
161
|
-
|
|
260
|
+
const loaded = hasRunningChild(run) ? restoreInterruptedRun(run) : run;
|
|
261
|
+
if (!loaded)
|
|
262
|
+
continue;
|
|
263
|
+
state.foregroundRuns.set(run.runId, loaded);
|
|
162
264
|
restored += 1;
|
|
163
265
|
}
|
|
164
266
|
return restored;
|
|
@@ -15,6 +15,7 @@ import { steerAsyncRun } from "./async-steering-action.ts";
|
|
|
15
15
|
import type { ChildRuntimeConfig } from "../shared/child-runtime-config.ts";
|
|
16
16
|
import { type MissionLaunchBinding } from "../../missions/lifecycle.ts";
|
|
17
17
|
import { type WorkflowSteerOptions, type WorkflowSteerResult } from "../../workflows/scripted-workflow.ts";
|
|
18
|
+
import { type PreparedWorkflowResume, type WorkflowResumeRefusalReason } from "../../workflows/workflow-resume.ts";
|
|
18
19
|
import { type AcceptanceInput, type AgentContract, type ControlConfig, type Details, type ExtensionConfig, type IntercomBridgeConfig, type JsonSchemaObject, type MaxOutputConfig, type ResolvedControlConfig, type RunFanoutBudgetDescriptor, type SteeringRecoveryDescriptor, type SingleResult, type ToolBudgetConfig, type UsageBudgetConfig, type SubagentState, type ScheduleOrigin } from "../../shared/types.ts";
|
|
19
20
|
export declare function unknownSubagentActionMessage(action: string, disabled?: DisabledFeatureSurface): string;
|
|
20
21
|
interface TaskParam {
|
|
@@ -287,6 +288,14 @@ export declare function resolveRevivalControlConfig(input: {
|
|
|
287
288
|
requestedControl?: ControlConfig;
|
|
288
289
|
recoveryControlConfig?: ResolvedControlConfig;
|
|
289
290
|
}): ResolvedControlConfig;
|
|
291
|
+
type WorkflowResumeOutcome = {
|
|
292
|
+
result: AgentToolResult<Details>;
|
|
293
|
+
} & ({
|
|
294
|
+
launched: true;
|
|
295
|
+
} | {
|
|
296
|
+
launched: false;
|
|
297
|
+
refusal: WorkflowResumeRefusalReason;
|
|
298
|
+
});
|
|
290
299
|
export declare function createSubagentExecutor(deps: ExecutorDeps): {
|
|
291
300
|
execute: (id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((r: AgentToolResult<Details>) => void) | undefined, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>;
|
|
292
301
|
/** Public/model-facing execution boundary. Internal direct launch primitives use execute or executeDelegated. */
|
|
@@ -299,6 +308,8 @@ export declare function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
299
308
|
executeDelegated: (id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((r: AgentToolResult<Details>) => void) | undefined, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>;
|
|
300
309
|
/** Scheduled launches retain their owning context without replacing the live active session. */
|
|
301
310
|
executeScheduled: (id: string, params: SubagentParamsLike, signal: AbortSignal, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>;
|
|
311
|
+
/** Relaunches a stopped workflow claimed by prepareWorkflowResume (resume by id and auto-resume); a launch that did not start gives up its claim. */
|
|
312
|
+
executeWorkflowResume: (id: string, prepared: PreparedWorkflowResume, signal: AbortSignal, ctx: ExtensionContext) => Promise<WorkflowResumeOutcome>;
|
|
302
313
|
/** Scheduled state visible to the current runtime supervisor owner only. */
|
|
303
314
|
getCurrentSupervisorOwnerStates: () => Iterable<SubagentState>;
|
|
304
315
|
};
|