pi-subagents 0.81.0 → 0.82.1
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 +55 -0
- package/README.md +1 -1
- package/bundle/agents.js +3 -3
- package/bundle/child-tool-plan.js +2 -2
- package/bundle/{chunk-L5ZHE4L4.js → chunk-3JCDWF4V.js} +4 -4
- package/bundle/{chunk-LSRUTHT7.js → chunk-43SCHIMD.js} +1 -1
- package/bundle/{chunk-VJHOELQP.js → chunk-46XKCPJR.js} +193 -81
- package/bundle/{chunk-G5DKFLLI.js → chunk-4SDU2BZX.js} +15 -10
- package/bundle/{chunk-2ZSNH26H.js → chunk-4Y2ROR7L.js} +7 -7
- package/bundle/{chunk-V2JWDB5U.js → chunk-52UBALJL.js} +2 -2
- package/bundle/{chunk-3DC5KBVP.js → chunk-5TYMSZX3.js} +1 -1
- package/bundle/{chunk-NNLASTQE.js → chunk-6XJB7PIX.js} +20 -6
- package/bundle/{chunk-ZBC44JJ3.js → chunk-CIZQXJ5Q.js} +22 -9
- package/bundle/{chunk-KSWQVGDW.js → chunk-FXUXCX2K.js} +225 -18
- package/bundle/{chunk-563BHAZ5.js → chunk-G4GZD3OJ.js} +35 -26
- package/bundle/{chunk-ARRKBB2B.js → chunk-GJECHSAR.js} +2 -2
- package/bundle/{chunk-V3BIUPLU.js → chunk-IVAE5Z6Q.js} +256 -52
- package/bundle/{chunk-V6MR5TOQ.js → chunk-JYUKW2SY.js} +25 -19
- package/bundle/{chunk-BJN5LUQY.js → chunk-OYF6AFDY.js} +1 -1
- package/bundle/{chunk-CEKS3TFO.js → chunk-PBK2I7WY.js} +6 -6
- package/bundle/{chunk-TUWLXTAM.js → chunk-PDZWJCHD.js} +681 -320
- package/bundle/{chunk-EYNJ6VL6.js → chunk-PLCCBLPY.js} +2 -2
- package/bundle/{chunk-XMFLCKMQ.js → chunk-QE4AGIMG.js} +339 -132
- package/bundle/{chunk-74RT5YQE.js → chunk-RMQFMMAT.js} +123 -33
- package/bundle/{chunk-WSBNENFS.js → chunk-UCPP4GNU.js} +14 -4
- package/bundle/{chunk-2XYWN7QJ.js → chunk-UTT254JE.js} +2 -2
- package/bundle/{chunk-BPS2AGBZ.js → chunk-VSUSLDNW.js} +93 -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-ZIV4J4HG.js} +498 -274
- 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 +37 -7
- 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/council-mode/SKILL.md +1 -4
- package/skills/pi-subagents/SKILL.md +130 -107
- package/skills/pi-subagents/references/orchestration.md +124 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +96 -295
- 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/reply-workflow-script.js +5 -2
- package/src/extension/schemas.js +3 -3
- package/src/extension/subagent-guide.js +1 -2
- 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 +4 -0
- package/src/runs/background/async-status.js +22 -3
- 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 +2 -0
- package/src/runs/background/terminal-run-index.js +44 -3
- 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 +25 -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/skills/pi-subagents/references/constraints-and-recipes.md +0 -74
- package/skills/pi-subagents/references/execution-controls.md +0 -563
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -165
- package/skills/pi-subagents/references/multi-lane-orchestration.md +0 -61
- package/skills/pi-subagents/references/review-and-validation.md +0 -73
|
@@ -60,7 +60,7 @@ export interface WorkflowScriptChildResult {
|
|
|
60
60
|
};
|
|
61
61
|
artifactPaths: string[];
|
|
62
62
|
results?: SingleResult[];
|
|
63
|
-
/** Came from a runtime-replaced run of the same script (and args, unless named by options.reuseFrom): its saved result, or its still-running child re-attached; this run launched nothing. */
|
|
63
|
+
/** Came from a runtime-replaced or owner-exited run of the same script (and args, unless named by options.reuseFrom): its saved result, or its still-running child re-attached; this run launched nothing. */
|
|
64
64
|
reused?: boolean;
|
|
65
65
|
}
|
|
66
66
|
export interface WorkflowScriptTraceEntry {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type PreparedWorkflowResume, type WorkflowResumeRefusalReason } from "./workflow-resume.ts";
|
|
2
|
+
/** Custom message posted when an auto-resume launch did not start. */
|
|
3
|
+
export declare const WORKFLOW_AUTO_RESUME_MESSAGE_TYPE = "subagent-auto-resume";
|
|
4
|
+
/** One planned auto-resume: its claim is held from session start until the launch starts or is cancelled. */
|
|
5
|
+
export interface PendingWorkflowAutoResume {
|
|
6
|
+
readonly sessionId: string;
|
|
7
|
+
/** The extension runtime that planned it. */
|
|
8
|
+
readonly owner: object;
|
|
9
|
+
readonly prepared: PreparedWorkflowResume;
|
|
10
|
+
/** `cancelled` before it started: its run id is reused by the next scan. */
|
|
11
|
+
state: "pending" | "started" | "cancelled";
|
|
12
|
+
}
|
|
13
|
+
/** Cancels matching launches that have not started and gives their claims back, synchronously. */
|
|
14
|
+
export declare function cancelPendingWorkflowAutoResumes(match: (entry: PendingWorkflowAutoResume) => boolean): void;
|
|
15
|
+
/** A cancelled entry stays recorded until its session's next scan reuses its run id. */
|
|
16
|
+
export declare function finishWorkflowAutoResume(entry: PendingWorkflowAutoResume): void;
|
|
17
|
+
/**
|
|
18
|
+
* Claims this session's interrupted top-level workflows for auto-resume. Synchronous and bounded:
|
|
19
|
+
* one listing of the session's terminal index, reads only for runs that ended in the last 24 h,
|
|
20
|
+
* then the cheap filters before any claim. A refusal is recorded in the run, so each run gets one
|
|
21
|
+
* automatic attempt; the launch itself happens later, off session start. Launches of this session
|
|
22
|
+
* that another runtime (or an earlier session start) planned but did not start are cancelled first,
|
|
23
|
+
* so their claims do not hide the runs from this scan.
|
|
24
|
+
*/
|
|
25
|
+
export declare function planWorkflowAutoResumes(input: {
|
|
26
|
+
asyncDirRoot: string;
|
|
27
|
+
currentSessionId: string;
|
|
28
|
+
now: number;
|
|
29
|
+
owner: object;
|
|
30
|
+
}): PendingWorkflowAutoResume[];
|
|
31
|
+
/** The one line posted when an auto-resume launch did not start. */
|
|
32
|
+
export declare function autoResumeFailureText(sourceRunId: string, refusal: Pick<WorkflowResumeRefusalReason, "reason" | "byHand">): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { readRecentTerminalRunIndex } from "../runs/background/terminal-run-index.js";
|
|
3
|
+
import { readStatus } from "../shared/utils.js";
|
|
4
|
+
import { hasWorkflowResumeRefusal, recordWorkflowResumeRefusal, refusalSentence, releaseWorkflowResumeClaim, workflowResumeClaimState } from "./workflow-resume-claim.js";
|
|
5
|
+
import { prepareWorkflowResume } from "./workflow-resume.js";
|
|
6
|
+
import { isReusableWorkflowStopCause, WORKFLOW_STOP_CAUSE_OWNER_EXITED } from "./workflow-stop-cause.js";
|
|
7
|
+
/** Custom message posted when an auto-resume launch did not start. */
|
|
8
|
+
export const WORKFLOW_AUTO_RESUME_MESSAGE_TYPE = "subagent-auto-resume";
|
|
9
|
+
/** Interrupted workflows older than this are left for a resume by hand. */
|
|
10
|
+
const AUTO_RESUME_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
const AUTO_RESUME_SCAN_LIMIT = 50;
|
|
12
|
+
/** When the run was interrupted: the dead owner's last update, or the runtime replacement. */
|
|
13
|
+
function interruptedAt(status) {
|
|
14
|
+
return status.workflow?.stopCause === WORKFLOW_STOP_CAUSE_OWNER_EXITED ? status.workflow.ownerExitedAt : status.endedAt;
|
|
15
|
+
}
|
|
16
|
+
// Shared by every runtime in the process: a reload's new runtime must see the old runtime's pending claims.
|
|
17
|
+
const registryKey = Symbol.for("pi-subagents.workflow-auto-resume.v1");
|
|
18
|
+
const registryGlobal = globalThis;
|
|
19
|
+
const pendingAutoResumes = registryGlobal[registryKey] ?? (registryGlobal[registryKey] = new Set());
|
|
20
|
+
/** Cancels matching launches that have not started and gives their claims back, synchronously. */
|
|
21
|
+
export function cancelPendingWorkflowAutoResumes(match) {
|
|
22
|
+
for (const entry of pendingAutoResumes) {
|
|
23
|
+
if (entry.state !== "pending" || !match(entry))
|
|
24
|
+
continue;
|
|
25
|
+
// It stays in the set, so the next scan of its session claims the run again under the same id and
|
|
26
|
+
// a notice that already said `Auto-resumed as <id>.` stays true.
|
|
27
|
+
entry.state = "cancelled";
|
|
28
|
+
try {
|
|
29
|
+
releaseWorkflowResumeClaim(entry.prepared.sourceAsyncDir, entry.prepared.newRunId);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error(`Failed to give back the auto-resume claim of workflow ${entry.prepared.sourceRunId}:`, error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** A cancelled entry stays recorded until its session's next scan reuses its run id. */
|
|
37
|
+
export function finishWorkflowAutoResume(entry) {
|
|
38
|
+
if (entry.state !== "cancelled")
|
|
39
|
+
pendingAutoResumes.delete(entry);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Claims this session's interrupted top-level workflows for auto-resume. Synchronous and bounded:
|
|
43
|
+
* one listing of the session's terminal index, reads only for runs that ended in the last 24 h,
|
|
44
|
+
* then the cheap filters before any claim. A refusal is recorded in the run, so each run gets one
|
|
45
|
+
* automatic attempt; the launch itself happens later, off session start. Launches of this session
|
|
46
|
+
* that another runtime (or an earlier session start) planned but did not start are cancelled first,
|
|
47
|
+
* so their claims do not hide the runs from this scan.
|
|
48
|
+
*/
|
|
49
|
+
export function planWorkflowAutoResumes(input) {
|
|
50
|
+
const { asyncDirRoot, currentSessionId, now } = input;
|
|
51
|
+
cancelPendingWorkflowAutoResumes((entry) => entry.sessionId === currentSessionId);
|
|
52
|
+
const reusableRunIds = new Map();
|
|
53
|
+
for (const entry of pendingAutoResumes) {
|
|
54
|
+
if (entry.sessionId !== currentSessionId || entry.state !== "cancelled")
|
|
55
|
+
continue;
|
|
56
|
+
reusableRunIds.set(entry.prepared.sourceRunId, entry.prepared.newRunId);
|
|
57
|
+
pendingAutoResumes.delete(entry);
|
|
58
|
+
}
|
|
59
|
+
let runIds;
|
|
60
|
+
try {
|
|
61
|
+
runIds = readRecentTerminalRunIndex(asyncDirRoot, { sessionId: currentSessionId, limit: AUTO_RESUME_SCAN_LIMIT, endedAfter: now - AUTO_RESUME_MAX_AGE_MS });
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error("Failed to read interrupted workflows for auto-resume:", error);
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const planned = [];
|
|
68
|
+
for (const runId of runIds) {
|
|
69
|
+
const asyncDir = path.join(asyncDirRoot, runId);
|
|
70
|
+
try {
|
|
71
|
+
const status = readStatus(asyncDir);
|
|
72
|
+
if (!status || status.mode !== "workflow" || status.parentWorkflowRunId !== undefined || status.sessionId !== currentSessionId)
|
|
73
|
+
continue;
|
|
74
|
+
if (!isReusableWorkflowStopCause(status.workflow?.stopCause))
|
|
75
|
+
continue;
|
|
76
|
+
const at = interruptedAt(status);
|
|
77
|
+
if (at === undefined || now - at > AUTO_RESUME_MAX_AGE_MS)
|
|
78
|
+
continue;
|
|
79
|
+
if (workflowResumeClaimState(asyncDir).kind !== "none" || hasWorkflowResumeRefusal(asyncDir))
|
|
80
|
+
continue;
|
|
81
|
+
const preparation = prepareWorkflowResume({ asyncDirRoot, sourceRunId: runId, trigger: "auto", now, currentSessionId, newRunId: reusableRunIds.get(runId) });
|
|
82
|
+
if (preparation.ok) {
|
|
83
|
+
const entry = { sessionId: currentSessionId, owner: input.owner, prepared: preparation.prepared, state: "pending" };
|
|
84
|
+
pendingAutoResumes.add(entry);
|
|
85
|
+
planned.push(entry);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Another claimant continues it; nothing to record.
|
|
89
|
+
if (preparation.code === "claim-busy" || preparation.code === "claim-resumed")
|
|
90
|
+
continue;
|
|
91
|
+
recordWorkflowResumeRefusal(asyncDir, { reason: preparation.reason, byHand: preparation.byHand }, now);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error(`Failed to prepare auto-resume of workflow ${runId}:`, error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return planned;
|
|
98
|
+
}
|
|
99
|
+
/** The one line posted when an auto-resume launch did not start. */
|
|
100
|
+
export function autoResumeFailureText(sourceRunId, refusal) {
|
|
101
|
+
return `Workflow ${sourceRunId} was not auto-resumed: ${refusalSentence(refusal.reason, refusal.byHand)}`;
|
|
102
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public launch of an async top-level workflow, saved in its run directory so the run can be
|
|
3
|
+
* relaunched later. A named resource is saved by name (its `runs.host` permit cannot be saved);
|
|
4
|
+
* structured tasks/chain launches save no params, so they cannot be relaunched from this record.
|
|
5
|
+
*/
|
|
6
|
+
export type WorkflowLaunch = {
|
|
7
|
+
source: "script" | "resource";
|
|
8
|
+
params: Record<string, unknown>;
|
|
9
|
+
} | {
|
|
10
|
+
source: "structured";
|
|
11
|
+
};
|
|
12
|
+
export type WorkflowLaunchRecord = WorkflowLaunch & {
|
|
13
|
+
version: 1;
|
|
14
|
+
scriptDigest: string;
|
|
15
|
+
};
|
|
16
|
+
type WorkflowLaunchRecordRead = {
|
|
17
|
+
ok: true;
|
|
18
|
+
record: WorkflowLaunchRecord;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
missing: boolean;
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
/** Throws on a write failure; the launch must fail rather than start a run it cannot relaunch. */
|
|
25
|
+
export declare function writeWorkflowLaunchRecord(asyncDir: string, record: WorkflowLaunchRecord): void;
|
|
26
|
+
export declare function readWorkflowLaunchRecord(asyncDir: string): WorkflowLaunchRecordRead;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { writeAtomicJson } from "../shared/atomic-json.js";
|
|
4
|
+
const WORKFLOW_LAUNCH_RECORD_FILE = "launch.json";
|
|
5
|
+
function launchRecordPath(asyncDir) {
|
|
6
|
+
return path.join(asyncDir, WORKFLOW_LAUNCH_RECORD_FILE);
|
|
7
|
+
}
|
|
8
|
+
/** Throws on a write failure; the launch must fail rather than start a run it cannot relaunch. */
|
|
9
|
+
export function writeWorkflowLaunchRecord(asyncDir, record) {
|
|
10
|
+
writeAtomicJson(launchRecordPath(asyncDir), record);
|
|
11
|
+
}
|
|
12
|
+
function isPlainObject(value) {
|
|
13
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function invalid(error) {
|
|
16
|
+
return { ok: false, missing: false, error };
|
|
17
|
+
}
|
|
18
|
+
export function readWorkflowLaunchRecord(asyncDir) {
|
|
19
|
+
let text;
|
|
20
|
+
try {
|
|
21
|
+
text = fs.readFileSync(launchRecordPath(asyncDir), "utf-8");
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code === "ENOENT")
|
|
25
|
+
return { ok: false, missing: true, error: "launch.json is missing" };
|
|
26
|
+
return invalid(`could not be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
27
|
+
}
|
|
28
|
+
let value;
|
|
29
|
+
try {
|
|
30
|
+
value = JSON.parse(text);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
return invalid(`not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
34
|
+
}
|
|
35
|
+
if (!isPlainObject(value))
|
|
36
|
+
return invalid("not a JSON object");
|
|
37
|
+
if (value.version !== 1)
|
|
38
|
+
return invalid("version must be 1");
|
|
39
|
+
if (typeof value.scriptDigest !== "string" || !value.scriptDigest)
|
|
40
|
+
return invalid("scriptDigest must be a non-empty string");
|
|
41
|
+
if (value.source === "structured")
|
|
42
|
+
return { ok: true, record: { version: 1, source: "structured", scriptDigest: value.scriptDigest } };
|
|
43
|
+
if (value.source !== "script" && value.source !== "resource")
|
|
44
|
+
return invalid("source must be \"script\", \"resource\", or \"structured\"");
|
|
45
|
+
if (!isPlainObject(value.params))
|
|
46
|
+
return invalid("params must be an object");
|
|
47
|
+
return { ok: true, record: { version: 1, source: value.source, params: value.params, scriptDigest: value.scriptDigest } };
|
|
48
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type PidLiveness, type RecordedPid } from "../runs/background/stale-run-reconciler.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Who continues a stopped workflow run. Claims are generation files `resumed-by.<n>.json` in the
|
|
4
|
+
* stopped run's directory, and the highest n is the current claim. A claimant whose process is dead
|
|
5
|
+
* is superseded by creating the next generation, so a takeover never moves a file another taker
|
|
6
|
+
* may have just created. An owner whose launch did not start renames its own claim to
|
|
7
|
+
* `resumed-by.<n>.json.released-<uuid>`; a released top means no claim, and the next taker creates
|
|
8
|
+
* n+1, so claims below it are never judged again. Only the owner launches; once the claimed new
|
|
9
|
+
* run's status.json exists the claim is permanent.
|
|
10
|
+
*/
|
|
11
|
+
interface WorkflowResumeClaim extends RecordedPid {
|
|
12
|
+
version: 1;
|
|
13
|
+
claimedAt: number;
|
|
14
|
+
newRunId: string;
|
|
15
|
+
trigger: "auto" | "manual" | "reuse";
|
|
16
|
+
}
|
|
17
|
+
interface WorkflowResumeRefusal {
|
|
18
|
+
version: 1;
|
|
19
|
+
reason: string;
|
|
20
|
+
byHand: string;
|
|
21
|
+
newRunId?: string;
|
|
22
|
+
refusedAt: number;
|
|
23
|
+
}
|
|
24
|
+
export type WorkflowResumeClaimOutcome = {
|
|
25
|
+
kind: "owner";
|
|
26
|
+
} | {
|
|
27
|
+
kind: "resumed";
|
|
28
|
+
newRunId: string;
|
|
29
|
+
} | {
|
|
30
|
+
kind: "busy";
|
|
31
|
+
pid: number;
|
|
32
|
+
}
|
|
33
|
+
/** The current claim file exists but cannot be read, so nothing proves its claimant dead. */
|
|
34
|
+
| {
|
|
35
|
+
kind: "unreadable";
|
|
36
|
+
};
|
|
37
|
+
type WorkflowResumeClaimState = {
|
|
38
|
+
kind: "none";
|
|
39
|
+
} | {
|
|
40
|
+
kind: "resumed";
|
|
41
|
+
newRunId: string;
|
|
42
|
+
} | {
|
|
43
|
+
kind: "busy";
|
|
44
|
+
pid: number;
|
|
45
|
+
newRunId: string;
|
|
46
|
+
} | {
|
|
47
|
+
kind: "unreadable";
|
|
48
|
+
};
|
|
49
|
+
type ClaimLiveness = (claim: WorkflowResumeClaim) => PidLiveness;
|
|
50
|
+
export declare function readWorkflowResumeClaim(asyncDir: string): WorkflowResumeClaim | undefined;
|
|
51
|
+
/** True once the run that resumes the run in `sourceAsyncDir` as `newRunId` has written its first status. */
|
|
52
|
+
export declare function resumedRunStarted(sourceAsyncDir: string, newRunId: string): boolean;
|
|
53
|
+
/** Read-only view for reuse readers and auto-resume: a dead, corrupt, or missing claim blocks nothing. */
|
|
54
|
+
export declare function workflowResumeClaimState(asyncDir: string, options?: {
|
|
55
|
+
liveness?: ClaimLiveness;
|
|
56
|
+
}): WorkflowResumeClaimState;
|
|
57
|
+
/**
|
|
58
|
+
* Claims the right to continue the stopped run in `asyncDir` as `newRunId`. A dead claimant is
|
|
59
|
+
* superseded by the next generation file; of several takers, the one whose exclusive create
|
|
60
|
+
* succeeds owns the run, and the others see its live claim on their next read.
|
|
61
|
+
*/
|
|
62
|
+
export declare function claimWorkflowResume(asyncDir: string, input: {
|
|
63
|
+
newRunId: string;
|
|
64
|
+
trigger: WorkflowResumeClaim["trigger"];
|
|
65
|
+
now?: number;
|
|
66
|
+
liveness?: ClaimLiveness;
|
|
67
|
+
}): WorkflowResumeClaimOutcome;
|
|
68
|
+
/** Gives up this process's claim on a launch that did not start, so the run can be continued again. */
|
|
69
|
+
export declare function releaseWorkflowResumeClaim(asyncDir: string, newRunId: string): void;
|
|
70
|
+
export declare function recordWorkflowResumeRefusal(asyncDir: string, refusal: Omit<WorkflowResumeRefusal, "version" | "refusedAt">, now?: number): void;
|
|
71
|
+
export declare function resumeByHandText(runId: string): string;
|
|
72
|
+
export declare function relaunchByHandText(runId: string): string;
|
|
73
|
+
/** `<reason>. <byHand>`; some reasons quote a message that already ends in a period. */
|
|
74
|
+
export declare function refusalSentence(reason: string, byHand: string): string;
|
|
75
|
+
/** A refusal was recorded for this run; the run is not tried again automatically. */
|
|
76
|
+
export declare function hasWorkflowResumeRefusal(asyncDir: string): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* The one line an interrupted workflow's notice carries about what happens next: the recorded
|
|
79
|
+
* auto-resume refusal, else who continues the run, else how to continue it by hand.
|
|
80
|
+
*/
|
|
81
|
+
export declare function describeWorkflowResume(asyncDir: string, runId: string): string;
|
|
82
|
+
export {};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { writeAtomicJson } from "../shared/atomic-json.js";
|
|
5
|
+
import { currentBootId, currentRecordedPid } from "../runs/background/boot-id.js";
|
|
6
|
+
import { currentPidNamespaceScope } from "../runs/background/pid-namespace.js";
|
|
7
|
+
import { checkRecordedPidLiveness, isRecordedPid } from "../runs/background/stale-run-reconciler.js";
|
|
8
|
+
import { readWorkflowLaunchRecord } from "./workflow-launch-record.js";
|
|
9
|
+
const CLAIM_FILE_PATTERN = /^resumed-by\.([1-9][0-9]*)\.json$/;
|
|
10
|
+
const RELEASED_CLAIM_FILE_PATTERN = /^resumed-by\.([1-9][0-9]*)\.json\.released-/;
|
|
11
|
+
const REFUSAL_FILE = "resume-refused.json";
|
|
12
|
+
const MAX_CLAIM_ROUNDS = 8;
|
|
13
|
+
// Claims this process holds, shared by every runtime in the process so a reload keeps them.
|
|
14
|
+
const heldClaimsKey = Symbol.for("pi-subagents.workflow-resume-claims.v1");
|
|
15
|
+
const heldClaimsGlobal = globalThis;
|
|
16
|
+
const heldClaims = heldClaimsGlobal[heldClaimsKey] ?? (heldClaimsGlobal[heldClaimsKey] = new Set());
|
|
17
|
+
function claimFilePath(asyncDir, generation) {
|
|
18
|
+
return path.join(asyncDir, `resumed-by.${generation}.json`);
|
|
19
|
+
}
|
|
20
|
+
function isClaim(value) {
|
|
21
|
+
if (!isRecordedPid(value) || Array.isArray(value))
|
|
22
|
+
return false;
|
|
23
|
+
const claim = value;
|
|
24
|
+
return claim.version === 1
|
|
25
|
+
&& typeof claim.claimedAt === "number"
|
|
26
|
+
&& typeof claim.newRunId === "string" && claim.newRunId.length > 0
|
|
27
|
+
&& (claim.trigger === "auto" || claim.trigger === "manual" || claim.trigger === "reuse");
|
|
28
|
+
}
|
|
29
|
+
function readTopClaim(asyncDir, retries = 3) {
|
|
30
|
+
let generation = 0;
|
|
31
|
+
let releasedGeneration = 0;
|
|
32
|
+
try {
|
|
33
|
+
for (const name of fs.readdirSync(asyncDir)) {
|
|
34
|
+
const live = CLAIM_FILE_PATTERN.exec(name);
|
|
35
|
+
if (live)
|
|
36
|
+
generation = Math.max(generation, Number(live[1]));
|
|
37
|
+
const released = RELEASED_CLAIM_FILE_PATTERN.exec(name);
|
|
38
|
+
if (released)
|
|
39
|
+
releasedGeneration = Math.max(releasedGeneration, Number(released[1]));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return error.code === "ENOENT" ? { kind: "open", generation: 0 } : { kind: "unreadable" };
|
|
44
|
+
}
|
|
45
|
+
// A live file and a released one with the same n: the live one was created after the release.
|
|
46
|
+
if (releasedGeneration > generation || generation === 0)
|
|
47
|
+
return { kind: "open", generation: Math.max(releasedGeneration, generation) };
|
|
48
|
+
const filePath = claimFilePath(asyncDir, generation);
|
|
49
|
+
let text;
|
|
50
|
+
try {
|
|
51
|
+
text = fs.readFileSync(filePath, "utf-8");
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// A release renamed it after the listing: look again.
|
|
55
|
+
if (error.code === "ENOENT" && retries > 0)
|
|
56
|
+
return readTopClaim(asyncDir, retries - 1);
|
|
57
|
+
return { kind: "unreadable" };
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const value = JSON.parse(text);
|
|
61
|
+
// The file is created whole, so a body that does not parse is corrupt, not half-written.
|
|
62
|
+
return isClaim(value) ? { kind: "claim", generation, filePath, claim: value } : { kind: "open", generation };
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return { kind: "open", generation };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function readWorkflowResumeClaim(asyncDir) {
|
|
69
|
+
const top = readTopClaim(asyncDir);
|
|
70
|
+
return top.kind === "claim" ? top.claim : undefined;
|
|
71
|
+
}
|
|
72
|
+
/** True once the run that resumes the run in `sourceAsyncDir` as `newRunId` has written its first status. */
|
|
73
|
+
export function resumedRunStarted(sourceAsyncDir, newRunId) {
|
|
74
|
+
return path.basename(newRunId) === newRunId
|
|
75
|
+
&& fs.existsSync(path.join(path.dirname(sourceAsyncDir), newRunId, "status.json"));
|
|
76
|
+
}
|
|
77
|
+
function claimantLiveness(filePath, claim, liveness) {
|
|
78
|
+
// A reload keeps the pid, so this process's own claim counts only while one of its runtimes holds
|
|
79
|
+
// it. The same pid in another pid namespace (a container sharing the temp root) is another process.
|
|
80
|
+
if (claim.pid === process.pid && claim.bootId === currentBootId() && claim.pidNamespaceScope === currentPidNamespaceScope())
|
|
81
|
+
return heldClaims.has(filePath) ? "alive" : "dead";
|
|
82
|
+
return liveness ? liveness(claim) : checkRecordedPidLiveness(claim);
|
|
83
|
+
}
|
|
84
|
+
/** Read-only view for reuse readers and auto-resume: a dead, corrupt, or missing claim blocks nothing. */
|
|
85
|
+
export function workflowResumeClaimState(asyncDir, options = {}) {
|
|
86
|
+
const top = readTopClaim(asyncDir);
|
|
87
|
+
if (top.kind === "unreadable")
|
|
88
|
+
return { kind: "unreadable" };
|
|
89
|
+
if (top.kind !== "claim")
|
|
90
|
+
return { kind: "none" };
|
|
91
|
+
if (resumedRunStarted(asyncDir, top.claim.newRunId))
|
|
92
|
+
return { kind: "resumed", newRunId: top.claim.newRunId };
|
|
93
|
+
return claimantLiveness(top.filePath, top.claim, options.liveness) === "dead" ? { kind: "none" } : { kind: "busy", pid: top.claim.pid, newRunId: top.claim.newRunId };
|
|
94
|
+
}
|
|
95
|
+
/** Creates the claim file only if none exists, with its whole body in place at once. */
|
|
96
|
+
function createClaimFile(filePath, claim) {
|
|
97
|
+
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
98
|
+
try {
|
|
99
|
+
fs.writeFileSync(tempPath, JSON.stringify(claim, null, 2), "utf-8");
|
|
100
|
+
fs.linkSync(tempPath, filePath);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error.code === "EEXIST")
|
|
105
|
+
return false;
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
fs.rmSync(tempPath, { force: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Claims the right to continue the stopped run in `asyncDir` as `newRunId`. A dead claimant is
|
|
114
|
+
* superseded by the next generation file; of several takers, the one whose exclusive create
|
|
115
|
+
* succeeds owns the run, and the others see its live claim on their next read.
|
|
116
|
+
*/
|
|
117
|
+
export function claimWorkflowResume(asyncDir, input) {
|
|
118
|
+
const claim = {
|
|
119
|
+
version: 1,
|
|
120
|
+
...currentRecordedPid(),
|
|
121
|
+
claimedAt: input.now ?? Date.now(),
|
|
122
|
+
newRunId: input.newRunId,
|
|
123
|
+
trigger: input.trigger,
|
|
124
|
+
};
|
|
125
|
+
for (let round = 0; round < MAX_CLAIM_ROUNDS; round++) {
|
|
126
|
+
const top = readTopClaim(asyncDir);
|
|
127
|
+
if (top.kind === "unreadable")
|
|
128
|
+
return { kind: "unreadable" };
|
|
129
|
+
if (top.kind === "claim") {
|
|
130
|
+
// Already claimed for this exact new run, as a resume does before it launches.
|
|
131
|
+
if (top.claim.newRunId === input.newRunId && top.claim.pid === process.pid) {
|
|
132
|
+
heldClaims.add(top.filePath);
|
|
133
|
+
return { kind: "owner" };
|
|
134
|
+
}
|
|
135
|
+
if (resumedRunStarted(asyncDir, top.claim.newRunId))
|
|
136
|
+
return { kind: "resumed", newRunId: top.claim.newRunId };
|
|
137
|
+
if (claimantLiveness(top.filePath, top.claim, input.liveness) !== "dead")
|
|
138
|
+
return { kind: "busy", pid: top.claim.pid };
|
|
139
|
+
}
|
|
140
|
+
const filePath = claimFilePath(asyncDir, top.generation + 1);
|
|
141
|
+
if (createClaimFile(filePath, claim)) {
|
|
142
|
+
heldClaims.add(filePath);
|
|
143
|
+
return { kind: "owner" };
|
|
144
|
+
}
|
|
145
|
+
// Another taker created that generation first: decide again from its claim.
|
|
146
|
+
}
|
|
147
|
+
// Every round lost to another taker; fail closed with whatever claim is there now.
|
|
148
|
+
const top = readTopClaim(asyncDir);
|
|
149
|
+
return top.kind === "claim" ? { kind: "busy", pid: top.claim.pid } : { kind: "unreadable" };
|
|
150
|
+
}
|
|
151
|
+
/** Gives up this process's claim on a launch that did not start, so the run can be continued again. */
|
|
152
|
+
export function releaseWorkflowResumeClaim(asyncDir, newRunId) {
|
|
153
|
+
const top = readTopClaim(asyncDir);
|
|
154
|
+
if (top.kind !== "claim" || top.claim.newRunId !== newRunId || top.claim.pid !== process.pid)
|
|
155
|
+
return;
|
|
156
|
+
// The only rename in the protocol: the owner moves its own claim aside.
|
|
157
|
+
try {
|
|
158
|
+
fs.renameSync(top.filePath, `${top.filePath}.released-${randomUUID()}`);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (error.code !== "ENOENT")
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
heldClaims.delete(top.filePath);
|
|
165
|
+
}
|
|
166
|
+
export function recordWorkflowResumeRefusal(asyncDir, refusal, now = Date.now()) {
|
|
167
|
+
writeAtomicJson(path.join(asyncDir, REFUSAL_FILE), { version: 1, ...refusal, refusedAt: now });
|
|
168
|
+
}
|
|
169
|
+
function readWorkflowResumeRefusal(asyncDir) {
|
|
170
|
+
try {
|
|
171
|
+
const value = JSON.parse(fs.readFileSync(path.join(asyncDir, REFUSAL_FILE), "utf-8"));
|
|
172
|
+
return value?.version === 1 && typeof value.reason === "string" && typeof value.byHand === "string" && typeof value.refusedAt === "number"
|
|
173
|
+
&& (value.newRunId === undefined || typeof value.newRunId === "string")
|
|
174
|
+
? value
|
|
175
|
+
: undefined;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export function resumeByHandText(runId) {
|
|
182
|
+
return `Resume by hand: subagent({ action: "resume", id: "${runId}" })`;
|
|
183
|
+
}
|
|
184
|
+
export function relaunchByHandText(runId) {
|
|
185
|
+
return `Relaunch it by hand with options: { reuseFrom: "${runId}" }.`;
|
|
186
|
+
}
|
|
187
|
+
/** `<reason>. <byHand>`; some reasons quote a message that already ends in a period. */
|
|
188
|
+
export function refusalSentence(reason, byHand) {
|
|
189
|
+
return `${reason.replace(/\.$/, "")}.${byHand ? ` ${byHand}` : ""}`;
|
|
190
|
+
}
|
|
191
|
+
/** A refusal was recorded for this run; the run is not tried again automatically. */
|
|
192
|
+
export function hasWorkflowResumeRefusal(asyncDir) {
|
|
193
|
+
return fs.existsSync(path.join(asyncDir, REFUSAL_FILE));
|
|
194
|
+
}
|
|
195
|
+
const RESUMED_AS = { auto: "Auto-resumed as", manual: "Resumed as", reuse: "Relaunched as" };
|
|
196
|
+
/**
|
|
197
|
+
* The one line an interrupted workflow's notice carries about what happens next: the recorded
|
|
198
|
+
* auto-resume refusal, else who continues the run, else how to continue it by hand.
|
|
199
|
+
*/
|
|
200
|
+
export function describeWorkflowResume(asyncDir, runId) {
|
|
201
|
+
const claim = readWorkflowResumeClaim(asyncDir);
|
|
202
|
+
const refusal = readWorkflowResumeRefusal(asyncDir);
|
|
203
|
+
if (refusal && (!claim || refusal.newRunId === claim.newRunId)) {
|
|
204
|
+
return `Not auto-resumed: ${refusalSentence(refusal.reason, refusal.byHand)}`;
|
|
205
|
+
}
|
|
206
|
+
if (claim)
|
|
207
|
+
return `${RESUMED_AS[claim.trigger]} ${claim.newRunId}.`;
|
|
208
|
+
const launch = readWorkflowLaunchRecord(asyncDir);
|
|
209
|
+
return launch.ok && launch.record.source !== "structured" ? resumeByHandText(runId) : relaunchByHandText(runId);
|
|
210
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
type WorkflowResumeTrigger = "auto" | "manual";
|
|
2
|
+
export type WorkflowResumeRefusalCode = "not-owned" | "not-interrupted" | "claim-busy" | "claim-resumed" | "launch-missing" | "launch-invalid" | "structured" | "digest" | "authority" | "capacity" | "attempts" | "launch-failed";
|
|
3
|
+
export interface WorkflowResumeRefusalReason {
|
|
4
|
+
code: WorkflowResumeRefusalCode;
|
|
5
|
+
reason: string;
|
|
6
|
+
/** How to continue the run by hand; empty when there is nothing to add. */
|
|
7
|
+
byHand: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PreparedWorkflowResume {
|
|
10
|
+
sourceRunId: string;
|
|
11
|
+
sourceAsyncDir: string;
|
|
12
|
+
newRunId: string;
|
|
13
|
+
trigger: WorkflowResumeTrigger;
|
|
14
|
+
/** Public launch params for executePublic: the saved launch with async, reuseFrom and the source's ceiling. */
|
|
15
|
+
params: Record<string, unknown>;
|
|
16
|
+
expectedScriptDigest: string;
|
|
17
|
+
resumeOrigin: {
|
|
18
|
+
sourceRunId: string;
|
|
19
|
+
attempt: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
type WorkflowResumePreparation = {
|
|
23
|
+
ok: true;
|
|
24
|
+
prepared: PreparedWorkflowResume;
|
|
25
|
+
} | ({
|
|
26
|
+
ok: false;
|
|
27
|
+
} & WorkflowResumeRefusalReason);
|
|
28
|
+
export declare function workflowResumeRefusal(code: WorkflowResumeRefusalCode, reason: string, runId: string): WorkflowResumeRefusalReason;
|
|
29
|
+
/** The tool error for a resume by id; the resume form is not repeated to a caller that just tried it. */
|
|
30
|
+
export declare function manualWorkflowResumeRefusalText(runId: string, refusal: Pick<WorkflowResumeRefusalReason, "reason" | "byHand">): string;
|
|
31
|
+
/**
|
|
32
|
+
* The run id a resume action relaunches: no message, task, chain or index, and an id naming a
|
|
33
|
+
* top-level workflow run. Anything else is a child resume.
|
|
34
|
+
*/
|
|
35
|
+
export declare function workflowResumeTargetId(asyncDirRoot: string, params: {
|
|
36
|
+
id?: unknown;
|
|
37
|
+
runId?: unknown;
|
|
38
|
+
message?: unknown;
|
|
39
|
+
task?: unknown;
|
|
40
|
+
chain?: unknown;
|
|
41
|
+
index?: unknown;
|
|
42
|
+
}): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Checks a stopped workflow run can be resumed and claims it for a new run id. Synchronous and
|
|
45
|
+
* cheap: the launch itself happens in the executor.
|
|
46
|
+
* `newRunId` presets the new run's id: a reload claims a run again under the id of the launch it cancelled.
|
|
47
|
+
*/
|
|
48
|
+
export declare function prepareWorkflowResume(input: {
|
|
49
|
+
asyncDirRoot: string;
|
|
50
|
+
sourceRunId: string;
|
|
51
|
+
trigger: WorkflowResumeTrigger;
|
|
52
|
+
now: number;
|
|
53
|
+
currentSessionId: string;
|
|
54
|
+
newRunId?: string;
|
|
55
|
+
}): WorkflowResumePreparation;
|
|
56
|
+
export {};
|