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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { normalizePublicSubagentExecution } from "../extension/public-execution.js";
|
|
4
|
+
import { reconcileAsyncRun } from "../runs/background/stale-run-reconciler.js";
|
|
5
|
+
import { ownsRun } from "../runs/shared/run-ownership.js";
|
|
6
|
+
import { readStatus } from "../shared/utils.js";
|
|
7
|
+
import { readWorkflowLaunchRecord } from "./workflow-launch-record.js";
|
|
8
|
+
import { claimWorkflowResume, refusalSentence, relaunchByHandText, resumeByHandText } from "./workflow-resume-claim.js";
|
|
9
|
+
import { workflowScriptDigest } from "./workflow-reuse.js";
|
|
10
|
+
import { isRunId } from "./workflow-revival.js";
|
|
11
|
+
import { isReusableWorkflowStopCause, WORKFLOW_STOP_CAUSE_OWNER_EXITED } from "./workflow-stop-cause.js";
|
|
12
|
+
/** A run may be resumed again after at most this many resumes that followed Pi exits (auto-resume only). */
|
|
13
|
+
const MAX_WORKFLOW_RESUME_ATTEMPTS = 3;
|
|
14
|
+
export function workflowResumeRefusal(code, reason, runId) {
|
|
15
|
+
const byHand = code === "launch-missing" || code === "launch-invalid" || code === "structured" || code === "digest" || code === "authority"
|
|
16
|
+
? relaunchByHandText(runId)
|
|
17
|
+
: code === "capacity" || code === "attempts" || code === "launch-failed" ? resumeByHandText(runId) : "";
|
|
18
|
+
return { code, reason, byHand };
|
|
19
|
+
}
|
|
20
|
+
/** The tool error for a resume by id; the resume form is not repeated to a caller that just tried it. */
|
|
21
|
+
export function manualWorkflowResumeRefusalText(runId, refusal) {
|
|
22
|
+
return `Cannot resume workflow '${runId}': ${refusalSentence(refusal.reason, refusal.byHand === relaunchByHandText(runId) ? refusal.byHand : "")}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The run id a resume action relaunches: no message, task, chain or index, and an id naming a
|
|
26
|
+
* top-level workflow run. Anything else is a child resume.
|
|
27
|
+
*/
|
|
28
|
+
export function workflowResumeTargetId(asyncDirRoot, params) {
|
|
29
|
+
if (params.message !== undefined || params.task !== undefined || params.index !== undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (Array.isArray(params.chain) && params.chain.length > 0)
|
|
32
|
+
return undefined;
|
|
33
|
+
const id = params.id ?? params.runId;
|
|
34
|
+
if (!isRunId(id))
|
|
35
|
+
return undefined;
|
|
36
|
+
let status;
|
|
37
|
+
try {
|
|
38
|
+
status = readStatus(path.join(asyncDirRoot, id));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return status?.mode === "workflow" && status.parentWorkflowRunId === undefined ? id : undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Checks a stopped workflow run can be resumed and claims it for a new run id. Synchronous and
|
|
47
|
+
* cheap: the launch itself happens in the executor.
|
|
48
|
+
* `newRunId` presets the new run's id: a reload claims a run again under the id of the launch it cancelled.
|
|
49
|
+
*/
|
|
50
|
+
export function prepareWorkflowResume(input) {
|
|
51
|
+
const { asyncDirRoot, sourceRunId, trigger } = input;
|
|
52
|
+
const refuse = (code, reason) => ({ ok: false, ...workflowResumeRefusal(code, reason, sourceRunId) });
|
|
53
|
+
const sourceAsyncDir = path.join(asyncDirRoot, sourceRunId);
|
|
54
|
+
// Only the session that owns the run (itself or one it replaced) may resume it, and nothing is
|
|
55
|
+
// written into another session's run, so this comes before the reconcile.
|
|
56
|
+
const stored = readStatus(sourceAsyncDir);
|
|
57
|
+
if (stored && !ownsRun(input.currentSessionId, stored.sessionId))
|
|
58
|
+
return refuse("not-owned", "it was started in another session; reopen that session to resume it");
|
|
59
|
+
// Session start has already reconciled this session's runs; a resume by id may come later.
|
|
60
|
+
if (trigger === "manual")
|
|
61
|
+
reconcileAsyncRun(sourceAsyncDir);
|
|
62
|
+
const status = readStatus(sourceAsyncDir);
|
|
63
|
+
if (!status || status.mode !== "workflow" || !isReusableWorkflowStopCause(status.workflow?.stopCause))
|
|
64
|
+
return refuse("not-interrupted", "not interrupted; pass message to revive one of its children");
|
|
65
|
+
// The attempt the resumed run records: only resumes that follow a Pi exit count.
|
|
66
|
+
const attempt = (status.workflow?.resumeOrigin?.attempt ?? 0) + (status.workflow?.stopCause === WORKFLOW_STOP_CAUSE_OWNER_EXITED ? 1 : 0);
|
|
67
|
+
if (trigger === "auto" && attempt > MAX_WORKFLOW_RESUME_ATTEMPTS)
|
|
68
|
+
return refuse("attempts", `${MAX_WORKFLOW_RESUME_ATTEMPTS} resumes already followed Pi exits`);
|
|
69
|
+
const read = readWorkflowLaunchRecord(sourceAsyncDir);
|
|
70
|
+
if (!read.ok)
|
|
71
|
+
return read.missing ? refuse("launch-missing", "launch.json is missing (the run started before launches were saved)") : refuse("launch-invalid", `launch.json is invalid: ${read.error}`);
|
|
72
|
+
const record = read.record;
|
|
73
|
+
if (record.source === "structured")
|
|
74
|
+
return refuse("structured", "launched from tasks/chain");
|
|
75
|
+
if (record.source === "script" && (typeof record.params.workflowScript !== "string" || workflowScriptDigest(record.params.workflowScript) !== record.scriptDigest)) {
|
|
76
|
+
return refuse("launch-invalid", "launch.json is invalid: its script does not match its scriptDigest");
|
|
77
|
+
}
|
|
78
|
+
const normalized = normalizePublicSubagentExecution(record.params);
|
|
79
|
+
if (!normalized.ok)
|
|
80
|
+
return refuse("launch-invalid", `launch.json is invalid: ${normalized.error}`);
|
|
81
|
+
const newRunId = input.newRunId ?? randomUUID();
|
|
82
|
+
const claim = claimWorkflowResume(sourceAsyncDir, { newRunId, trigger, now: input.now });
|
|
83
|
+
if (claim.kind === "resumed")
|
|
84
|
+
return refuse("claim-resumed", `resumed as ${claim.newRunId}; wait on it or stop it`);
|
|
85
|
+
if (claim.kind === "busy")
|
|
86
|
+
return refuse("claim-busy", `being resumed by pid ${claim.pid}`);
|
|
87
|
+
if (claim.kind === "unreadable")
|
|
88
|
+
return refuse("claim-busy", "its resume claim cannot be read; wait or remove it");
|
|
89
|
+
return {
|
|
90
|
+
ok: true,
|
|
91
|
+
prepared: {
|
|
92
|
+
sourceRunId,
|
|
93
|
+
sourceAsyncDir,
|
|
94
|
+
newRunId,
|
|
95
|
+
trigger,
|
|
96
|
+
params: {
|
|
97
|
+
...record.params,
|
|
98
|
+
async: true,
|
|
99
|
+
reuseFrom: sourceRunId,
|
|
100
|
+
...(status.admissionCapabilityCeiling ? { capabilityCeiling: status.admissionCapabilityCeiling } : {}),
|
|
101
|
+
},
|
|
102
|
+
expectedScriptDigest: record.scriptDigest,
|
|
103
|
+
resumeOrigin: { sourceRunId, attempt },
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { type WorkflowResumeClaimOutcome } from "./workflow-resume-claim.ts";
|
|
1
2
|
import { type WorkflowScriptChildResult } from "./scripted-workflow.ts";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export declare function workflowRuntimeReplacedRelaunchNotice(runId: string): string;
|
|
3
|
+
import { WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED } from "./workflow-stop-cause.ts";
|
|
4
|
+
/** What a runtime replacement left; the notice's resume line says how the run continues. */
|
|
5
|
+
export declare const WORKFLOW_RUNTIME_REPLACED_STOP_NOTICE = "Async children that were still running keep running, and finished children are kept.";
|
|
6
6
|
/** Abort reason for workflow controllers torn down by runtime replacement; carries the cause as data, not text. */
|
|
7
7
|
export declare function runtimeReplacedAbortReason(): Error;
|
|
8
|
-
export declare function workflowStopCause(reason: unknown):
|
|
8
|
+
export declare function workflowStopCause(reason: unknown): typeof WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED | undefined;
|
|
9
9
|
export declare function workflowScriptDigest(script: string): string;
|
|
10
10
|
/** Hash of the canonical launch params the script host uses to detect duplicate keys. */
|
|
11
11
|
export declare function workflowChildFingerprint(params: Record<string, unknown>): string;
|
|
@@ -40,16 +40,22 @@ type WorkflowReuseMatch = {
|
|
|
40
40
|
kind: "started";
|
|
41
41
|
runId: string;
|
|
42
42
|
};
|
|
43
|
+
/** Why a relaunch of `runId` is refused when another relaunch already continues it. */
|
|
44
|
+
export declare function claimedWorkflowRunRefusal(runId: string, claim: Exclude<WorkflowResumeClaimOutcome, {
|
|
45
|
+
kind: "owner";
|
|
46
|
+
}>, named: boolean): string;
|
|
43
47
|
/**
|
|
44
48
|
* The newest terminal workflow of this session with the same script and args,
|
|
45
|
-
* when that run stopped because its runtime was replaced.
|
|
49
|
+
* when that run stopped because its runtime was replaced or its Pi process exited.
|
|
50
|
+
* Throws when another relaunch already continues that run.
|
|
46
51
|
*/
|
|
47
52
|
export declare function findWorkflowReuseSource(asyncDirRoot: string, sessionId: string, scriptDigest: string, argsDigest: string | undefined): WorkflowReuseSource | undefined;
|
|
48
53
|
/**
|
|
49
|
-
* The
|
|
50
|
-
* must match: each child is still reused only when its key and launch params match.
|
|
54
|
+
* The interrupted workflow run named by `options.reuseFrom`, from any session. Only the script
|
|
55
|
+
* must match: each child is still reused only when its key and launch params match. The run is
|
|
56
|
+
* reconciled first, so a run whose Pi process died is marked `owner-exited` before it is read.
|
|
51
57
|
*/
|
|
52
|
-
export declare function readNamedWorkflowReuseSource(asyncDirRoot: string, runId: string, scriptDigest: string, cwd: string): WorkflowReuseSource;
|
|
58
|
+
export declare function readNamedWorkflowReuseSource(asyncDirRoot: string, runId: string, scriptDigest: string, cwd: string, claimOwnerRunId?: string): WorkflowReuseSource;
|
|
53
59
|
/** A settled match is reusable only when that child succeeded; failed or stopped children run again. */
|
|
54
60
|
export declare function matchWorkflowReuse(source: WorkflowReuseSource, key: string, fingerprint: string): WorkflowReuseMatch | undefined;
|
|
55
61
|
export {};
|
|
@@ -2,14 +2,15 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { readRecentTerminalRunIndex } from "../runs/background/terminal-run-index.js";
|
|
5
|
+
import { reconcileAsyncRun } from "../runs/background/stale-run-reconciler.js";
|
|
5
6
|
import { readStatus } from "../shared/utils.js";
|
|
7
|
+
import { workflowResumeClaimState } from "./workflow-resume-claim.js";
|
|
8
|
+
import { isRunId } from "./workflow-revival.js";
|
|
6
9
|
import { workflowRunParamsFingerprint } from "./scripted-workflow.js";
|
|
7
|
-
|
|
8
|
-
const WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED = "runtime-replaced";
|
|
10
|
+
import { isReusableWorkflowStopCause, WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED } from "./workflow-stop-cause.js";
|
|
9
11
|
const WORKFLOW_CHILD_JOURNAL_FILE = "workflow-children.jsonl";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
12
|
+
/** What a runtime replacement left; the notice's resume line says how the run continues. */
|
|
13
|
+
export const WORKFLOW_RUNTIME_REPLACED_STOP_NOTICE = "Async children that were still running keep running, and finished children are kept.";
|
|
13
14
|
/** Abort reason for workflow controllers torn down by runtime replacement; carries the cause as data, not text. */
|
|
14
15
|
export function runtimeReplacedAbortReason() {
|
|
15
16
|
return Object.assign(new Error("Workflow stopped because the extension session was replaced or reloaded."), { workflowStopCause: WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED });
|
|
@@ -67,9 +68,63 @@ function readWorkflowChildJournal(runId, workflowAsyncDir) {
|
|
|
67
68
|
}
|
|
68
69
|
return source;
|
|
69
70
|
}
|
|
71
|
+
const WORKFLOW_REUSE_CHAIN_MAX_LINKS = 16;
|
|
72
|
+
/**
|
|
73
|
+
* The head run's journal plus the journals of the runs it was relaunched from (`workflow.reusedFrom`).
|
|
74
|
+
* A child's start record is written only once it gets a concurrency slot, so a key the head never
|
|
75
|
+
* reached is still in an earlier run's journal. For each key the newest run that recorded it wins. A
|
|
76
|
+
* status file is parsed, not validated, so a corrupt `reusedFrom` may be any JSON value.
|
|
77
|
+
*/
|
|
78
|
+
function readWorkflowReuseChain(asyncDirRoot, headRunId, headStatus) {
|
|
79
|
+
const source = readWorkflowChildJournal(headRunId, path.join(asyncDirRoot, headRunId));
|
|
80
|
+
const visited = new Set([headRunId]);
|
|
81
|
+
let next = headStatus.workflow?.reusedFrom;
|
|
82
|
+
for (let links = 0; isRunId(next) && links < WORKFLOW_REUSE_CHAIN_MAX_LINKS && !visited.has(next); links++) {
|
|
83
|
+
visited.add(next);
|
|
84
|
+
const asyncDir = path.join(asyncDirRoot, next);
|
|
85
|
+
let status;
|
|
86
|
+
try {
|
|
87
|
+
status = readStatus(asyncDir);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
if (status?.mode !== "workflow" || status.workflow?.scriptDigest !== headStatus.workflow?.scriptDigest)
|
|
93
|
+
break;
|
|
94
|
+
const link = readWorkflowChildJournal(next, asyncDir);
|
|
95
|
+
for (const key of new Set([...link.started.keys(), ...link.settled.keys()])) {
|
|
96
|
+
if (source.started.has(key) || source.settled.has(key))
|
|
97
|
+
continue;
|
|
98
|
+
const started = link.started.get(key);
|
|
99
|
+
const settled = link.settled.get(key);
|
|
100
|
+
if (started)
|
|
101
|
+
source.started.set(key, started);
|
|
102
|
+
if (settled)
|
|
103
|
+
source.settled.set(key, settled);
|
|
104
|
+
}
|
|
105
|
+
next = status.workflow?.reusedFrom;
|
|
106
|
+
}
|
|
107
|
+
return source;
|
|
108
|
+
}
|
|
109
|
+
/** Why a relaunch of `runId` is refused when another relaunch already continues it. */
|
|
110
|
+
export function claimedWorkflowRunRefusal(runId, claim, named) {
|
|
111
|
+
const run = `${named ? "options.reuseFrom: workflow" : "Workflow"} run '${runId}'`;
|
|
112
|
+
if (claim.kind === "resumed")
|
|
113
|
+
return `${run} was resumed as ${claim.newRunId}; wait on it or stop it.`;
|
|
114
|
+
if (claim.kind === "busy")
|
|
115
|
+
return `${run} is being resumed by pid ${claim.pid}; wait for it.`;
|
|
116
|
+
return `${run} has a resume claim that cannot be read; wait or remove it.`;
|
|
117
|
+
}
|
|
118
|
+
function assertWorkflowRunNotClaimed(asyncDir, runId, named, claimOwnerRunId) {
|
|
119
|
+
const claim = workflowResumeClaimState(asyncDir);
|
|
120
|
+
if (claim.kind === "none" || (claim.kind !== "unreadable" && claim.newRunId === claimOwnerRunId))
|
|
121
|
+
return;
|
|
122
|
+
throw new Error(claimedWorkflowRunRefusal(runId, claim, named));
|
|
123
|
+
}
|
|
70
124
|
/**
|
|
71
125
|
* The newest terminal workflow of this session with the same script and args,
|
|
72
|
-
* when that run stopped because its runtime was replaced.
|
|
126
|
+
* when that run stopped because its runtime was replaced or its Pi process exited.
|
|
127
|
+
* Throws when another relaunch already continues that run.
|
|
73
128
|
*/
|
|
74
129
|
export function findWorkflowReuseSource(asyncDirRoot, sessionId, scriptDigest, argsDigest) {
|
|
75
130
|
let runIds;
|
|
@@ -90,28 +145,36 @@ export function findWorkflowReuseSource(asyncDirRoot, sessionId, scriptDigest, a
|
|
|
90
145
|
}
|
|
91
146
|
if (status?.mode !== "workflow" || status.workflow?.scriptDigest !== scriptDigest || status.workflow.argsDigest !== argsDigest)
|
|
92
147
|
continue;
|
|
93
|
-
|
|
148
|
+
if (!isReusableWorkflowStopCause(status.workflow.stopCause))
|
|
149
|
+
return undefined;
|
|
150
|
+
assertWorkflowRunNotClaimed(asyncDir, runId, false);
|
|
151
|
+
return readWorkflowReuseChain(asyncDirRoot, runId, status);
|
|
94
152
|
}
|
|
95
153
|
return undefined;
|
|
96
154
|
}
|
|
97
155
|
/**
|
|
98
|
-
* The
|
|
99
|
-
* must match: each child is still reused only when its key and launch params match.
|
|
156
|
+
* The interrupted workflow run named by `options.reuseFrom`, from any session. Only the script
|
|
157
|
+
* must match: each child is still reused only when its key and launch params match. The run is
|
|
158
|
+
* reconciled first, so a run whose Pi process died is marked `owner-exited` before it is read.
|
|
100
159
|
*/
|
|
101
|
-
export function readNamedWorkflowReuseSource(asyncDirRoot, runId, scriptDigest, cwd) {
|
|
160
|
+
export function readNamedWorkflowReuseSource(asyncDirRoot, runId, scriptDigest, cwd, claimOwnerRunId) {
|
|
102
161
|
// A user-supplied id: it must name one run directory, never a path.
|
|
103
|
-
const
|
|
162
|
+
const runName = isRunId(runId);
|
|
163
|
+
if (runName)
|
|
164
|
+
reconcileAsyncRun(path.join(asyncDirRoot, runId));
|
|
165
|
+
const status = runName ? readStatus(path.join(asyncDirRoot, runId)) : null;
|
|
104
166
|
if (!status)
|
|
105
167
|
throw new Error(`options.reuseFrom: workflow run '${runId}' was not found.`);
|
|
106
168
|
if (status.mode !== "workflow")
|
|
107
169
|
throw new Error(`options.reuseFrom: run '${runId}' is not a workflow run.`);
|
|
108
|
-
if (status.workflow?.stopCause
|
|
109
|
-
throw new Error(`options.reuseFrom: workflow run '${runId}' did not stop because its session was replaced, so its children cannot be reused.`);
|
|
170
|
+
if (!isReusableWorkflowStopCause(status.workflow?.stopCause))
|
|
171
|
+
throw new Error(`options.reuseFrom: workflow run '${runId}' did not stop because its session was replaced or its Pi process exited, so its children cannot be reused.`);
|
|
110
172
|
if (status.workflow.scriptDigest !== scriptDigest)
|
|
111
173
|
throw new Error(`options.reuseFrom: workflow run '${runId}' ran a different workflow script.`);
|
|
112
174
|
if (status.cwd !== cwd)
|
|
113
175
|
throw new Error(`options.reuseFrom: workflow run '${runId}' ran in '${status.cwd}', not '${cwd}'.`);
|
|
114
|
-
|
|
176
|
+
assertWorkflowRunNotClaimed(path.join(asyncDirRoot, runId), runId, true, claimOwnerRunId);
|
|
177
|
+
return readWorkflowReuseChain(asyncDirRoot, runId, status);
|
|
115
178
|
}
|
|
116
179
|
/** A settled match is reusable only when that child succeeded; failed or stopped children run again. */
|
|
117
180
|
export function matchWorkflowReuse(source, key, fingerprint) {
|
|
@@ -9,6 +9,8 @@ export interface WorkflowKeyRevival {
|
|
|
9
9
|
/** State of the latest revived run; undefined when its status is unreadable. */
|
|
10
10
|
state?: AsyncStatus["state"];
|
|
11
11
|
}
|
|
12
|
+
/** A run directory name, never a path. */
|
|
13
|
+
export declare function isRunId(value: unknown): value is string;
|
|
12
14
|
/**
|
|
13
15
|
* Links a detached revival to the workflow key of its source run when the source failed and is
|
|
14
16
|
* an async workflow child or an earlier revival of one. Throws when the source status cannot be
|
|
@@ -13,7 +13,8 @@ export const REVIVAL_ORIGIN_FILE = "workflow-revival-origin.json";
|
|
|
13
13
|
/** Recording refuses a deeper link, so readers bounded by this always reach the chain's end. */
|
|
14
14
|
const MAX_REVIVAL_DEPTH = 16;
|
|
15
15
|
const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
16
|
-
|
|
16
|
+
/** A run directory name, never a path. */
|
|
17
|
+
export function isRunId(value) {
|
|
17
18
|
return typeof value === "string" && value.length > 0 && value.length <= 255 && path.basename(value) === value && value !== "." && value !== "..";
|
|
18
19
|
}
|
|
19
20
|
function readLink(file) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The extension runtime that owned the workflow was replaced (/reload, resume, project switch). */
|
|
2
|
+
export declare const WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED = "runtime-replaced";
|
|
3
|
+
/** Stale-run reconciliation found the Pi process that ran the workflow dead (crash, kill, reboot). */
|
|
4
|
+
export declare const WORKFLOW_STOP_CAUSE_OWNER_EXITED = "owner-exited";
|
|
5
|
+
export type WorkflowStopCause = typeof WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED | typeof WORKFLOW_STOP_CAUSE_OWNER_EXITED;
|
|
6
|
+
export declare function isReusableWorkflowStopCause(cause: unknown): cause is WorkflowStopCause;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Leaf module with no imports, so any module can use it without an import cycle.
|
|
2
|
+
/** The extension runtime that owned the workflow was replaced (/reload, resume, project switch). */
|
|
3
|
+
export const WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED = "runtime-replaced";
|
|
4
|
+
/** Stale-run reconciliation found the Pi process that ran the workflow dead (crash, kill, reboot). */
|
|
5
|
+
export const WORKFLOW_STOP_CAUSE_OWNER_EXITED = "owner-exited";
|
|
6
|
+
export function isReusableWorkflowStopCause(cause) {
|
|
7
|
+
return cause === WORKFLOW_STOP_CAUSE_RUNTIME_REPLACED || cause === WORKFLOW_STOP_CAUSE_OWNER_EXITED;
|
|
8
|
+
}
|