@tranhoangnguyen0310/pi-flow-external 3.1.0-external.0 → 3.1.1-external.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 +8 -0
- package/package.json +1 -1
- package/src/external-runs.ts +129 -24
- package/src/workflow/journal.ts +28 -6
- package/src/workflow/source.ts +17 -25
- package/src/workflow/tool.ts +29 -18
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-flow external are documented here.
|
|
4
4
|
|
|
5
|
+
## [3.1.1-external.0] - 2026-10-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- `external_runs list` no longer reports live work as `interrupted_or_uncertain` on later pages, or lists a live run twice. Live state now replaces a run's persisted row on whichever page that row falls. Live runs with no persisted row yet (for example, while evidence writes fail) are paged first and count against `limit`, so every page stays within `limit`. The cursor bookmarks the last such run in registry order, so persisting one between pages never skips another; it can only appear twice (once live, once persisted). If the bookmarked run has left the registry, the next page fails with "Run list changed while paging" instead of guessing. A run started after the listing reached its persisted rows appears in the next fresh listing.
|
|
9
|
+
- One unreadable workflow journal (an incompatible version, a corrupt start line, or a read error) no longer fails the whole `list`. It is listed as one `interrupted_or_uncertain` row with the read error; `inspect` of that ID still reports the error.
|
|
10
|
+
- A workflow launched after its session closed no longer throws with its journal left open. The journal is settled as failed, and the call returns an error result naming the closed session.
|
|
11
|
+
- Workflow resume no longer reveals that another project's run exists: it reports the same "run journal not found" message as an unknown run ID.
|
|
12
|
+
|
|
5
13
|
## [3.1.0-external.0] - 2026-10-02
|
|
6
14
|
|
|
7
15
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tranhoangnguyen0310/pi-flow-external",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1-external.0",
|
|
4
4
|
"description": "External Claude Code, Codex CLI, Antigravity, Grok Build CLI, Muse Code, and OpenCode delegation for pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
package/src/external-runs.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { renderOutputText } from "./core/subagent-render.ts";
|
|
|
12
12
|
import { SPINNER_INTERVAL_MS } from "./core/spinner.ts";
|
|
13
13
|
import { EXTERNAL_RUNS_PROMPT_SNIPPET } from "./prompts.ts";
|
|
14
14
|
import type { WorkflowToolDetails } from "./types.ts";
|
|
15
|
-
import { getSessionWorkflowDir, listWorkflowJournals, loadWorkflowJournal, type LoadedWorkflowJournal } from "./workflow/journal.ts";
|
|
15
|
+
import { getSessionWorkflowDir, listWorkflowJournals, loadWorkflowJournal, WorkflowJournalReadError, type LoadedWorkflowJournal, type UnreadableWorkflowJournal } from "./workflow/journal.ts";
|
|
16
16
|
|
|
17
17
|
const RUN_ID = /^(?:run|wf)_[A-Za-z0-9_-]{1,128}$/;
|
|
18
18
|
const WORKFLOW_ID = /^wf_[A-Za-z0-9_-]{1,128}$/;
|
|
@@ -219,6 +219,97 @@ function journalSummary(journal: LoadedWorkflowJournal, allChildren = false) {
|
|
|
219
219
|
};
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
/** A listed journal that could not be loaded: one uncertain row instead of a failed listing. */
|
|
223
|
+
function unreadableJournalSummary(journal: UnreadableWorkflowJournal) {
|
|
224
|
+
return {
|
|
225
|
+
...projectWorkflowRun({
|
|
226
|
+
runId: journal.runId,
|
|
227
|
+
live: false,
|
|
228
|
+
status: "interrupted_or_uncertain",
|
|
229
|
+
error: clip(journal.unreadable),
|
|
230
|
+
resultAvailable: false,
|
|
231
|
+
finalAvailable: false,
|
|
232
|
+
}),
|
|
233
|
+
children: [],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Mirrors the listing's own inclusion rule, so a journal is counted persisted exactly when some page lists it. */
|
|
238
|
+
async function hasWorkflowJournal(dir: string, runId: string, project: string): Promise<boolean> {
|
|
239
|
+
try {
|
|
240
|
+
return (await loadWorkflowJournal(dir, runId))?.project === project;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const owner = error instanceof WorkflowJournalReadError ? error.project : undefined;
|
|
243
|
+
return owner === undefined || owner === project;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function listScope(stream: "runs" | "workflows", sessionId: string, project: string, workflowRunId?: string): string {
|
|
248
|
+
return JSON.stringify({ stream, sessionId, project, workflowRunId });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
type UnpersistedBookmark = { after: string } | { persisted: true };
|
|
252
|
+
|
|
253
|
+
function encodeUnpersistedCursor(scope: string, bookmark: UnpersistedBookmark): string {
|
|
254
|
+
return Buffer.from(JSON.stringify({ v: 1, kind: "unpersisted-list", scope, ...bookmark })).toString("base64url");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** The bookmark of an unpersisted-stage cursor, or undefined for any other cursor (passed on to the persisted listing). */
|
|
258
|
+
function decodeUnpersistedCursor(value: string, scope: string): UnpersistedBookmark | undefined {
|
|
259
|
+
let parsed: Record<string, unknown> | undefined;
|
|
260
|
+
try {
|
|
261
|
+
parsed = record(JSON.parse(Buffer.from(value, "base64url").toString("utf8")));
|
|
262
|
+
} catch {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
if (parsed?.kind !== "unpersisted-list") return undefined;
|
|
266
|
+
const keys = Object.keys(parsed).sort().join(",");
|
|
267
|
+
const valid = parsed.v === 1 && ((keys === "after,kind,scope,v" && typeof parsed.after === "string" && RUN_ID.test(parsed.after)) || (keys === "kind,persisted,scope,v" && parsed.persisted === true));
|
|
268
|
+
if (!valid) throw new Error("Invalid list cursor");
|
|
269
|
+
if (parsed.scope !== scope) throw new Error("Cursor belongs to a different list scope");
|
|
270
|
+
return typeof parsed.after === "string" ? { after: parsed.after } : { persisted: true };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* One bounded list page: live runs with no persisted row first, then
|
|
275
|
+
* persisted rows, all within `pageSize`. The unpersisted stage bookmarks the
|
|
276
|
+
* last run it served, in registry order. Persisting a run never removes it
|
|
277
|
+
* from the registry, so a later page always resumes after the same run and
|
|
278
|
+
* none is skipped. If the bookmarked run has left the registry, continuation
|
|
279
|
+
* fails as stale rather than guessing. A run persisted between pages may
|
|
280
|
+
* appear in both stages; a run registered after the listing reached its
|
|
281
|
+
* persisted stage appears in the next fresh listing.
|
|
282
|
+
*/
|
|
283
|
+
async function pageUnpersistedFirst<L extends { runId: string }, D>(
|
|
284
|
+
scope: string,
|
|
285
|
+
cursor: string | undefined,
|
|
286
|
+
pageSize: number,
|
|
287
|
+
candidates: L[],
|
|
288
|
+
isUnpersisted: (candidate: L) => Promise<boolean>,
|
|
289
|
+
persisted: (limit: number, cursor: string | undefined) => Promise<{ items: D[]; nextCursor?: string }>,
|
|
290
|
+
): Promise<{ live: L[]; items: D[]; nextCursor?: string }> {
|
|
291
|
+
const bookmark = cursor === undefined ? undefined : decodeUnpersistedCursor(cursor, scope);
|
|
292
|
+
if (cursor !== undefined && bookmark === undefined) return { live: [], ...await persisted(pageSize, cursor) };
|
|
293
|
+
if (bookmark && "persisted" in bookmark) return { live: [], ...await persisted(pageSize, undefined) };
|
|
294
|
+
let start = 0;
|
|
295
|
+
if (bookmark) {
|
|
296
|
+
const index = candidates.findIndex((candidate) => candidate.runId === bookmark.after);
|
|
297
|
+
if (index < 0) throw new Error("Run list changed while paging; restart the listing without a cursor");
|
|
298
|
+
start = index + 1;
|
|
299
|
+
}
|
|
300
|
+
// Find one more than fits, to know whether the unpersisted stage continues.
|
|
301
|
+
const live: L[] = [];
|
|
302
|
+
for (let index = start; index < candidates.length && live.length <= pageSize; index++) {
|
|
303
|
+
if (await isUnpersisted(candidates[index]!)) live.push(candidates[index]!);
|
|
304
|
+
}
|
|
305
|
+
if (live.length > pageSize) {
|
|
306
|
+
const served = live.slice(0, pageSize);
|
|
307
|
+
return { live: served, items: [], nextCursor: encodeUnpersistedCursor(scope, { after: served.at(-1)!.runId }) };
|
|
308
|
+
}
|
|
309
|
+
if (live.length === pageSize) return { live, items: [], nextCursor: encodeUnpersistedCursor(scope, { persisted: true }) };
|
|
310
|
+
return { live, ...await persisted(pageSize - live.length, undefined) };
|
|
311
|
+
}
|
|
312
|
+
|
|
222
313
|
function projectionRevision(text: string): string {
|
|
223
314
|
return createHash("sha256").update(text).digest("hex");
|
|
224
315
|
}
|
|
@@ -685,30 +776,44 @@ export function createExternalRunsTool(
|
|
|
685
776
|
|
|
686
777
|
if (params.action === "list") {
|
|
687
778
|
if (params.workflowRunId !== undefined && !WORKFLOW_ID.test(params.workflowRunId)) throw new Error("Invalid workflow run ID");
|
|
688
|
-
const page = params.workflowCursor && !params.cursor
|
|
689
|
-
? { items: [] }
|
|
690
|
-
: await listRunRecords({
|
|
691
|
-
runsDirectory,
|
|
692
|
-
sessionId,
|
|
693
|
-
project,
|
|
694
|
-
workflowRunId: params.workflowRunId,
|
|
695
|
-
limit: params.limit,
|
|
696
|
-
cursor: params.cursor,
|
|
697
|
-
});
|
|
698
779
|
const workflowDir = getSessionWorkflowDir(ctx);
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
780
|
+
// Live state overlays its persisted row on whichever page that row
|
|
781
|
+
// falls, so a live run is never shown as interrupted or listed twice.
|
|
782
|
+
// Live runs with no persisted row yet are paged first, inside the same
|
|
783
|
+
// limit, before the persisted rows begin.
|
|
784
|
+
const registered = new Map(options.registry.list(sessionId, project).map((entry) => [entry.runId, entry]));
|
|
785
|
+
const runScope = listScope("runs", sessionId, project, params.workflowRunId);
|
|
786
|
+
const page = params.workflowCursor && !params.cursor
|
|
787
|
+
? { live: [], items: [] }
|
|
788
|
+
: await pageUnpersistedFirst(runScope, params.cursor, Math.max(1, Math.min(100, Math.floor(params.limit ?? 50))),
|
|
789
|
+
[...registered.values()].filter((entry) => entry.kind === "agent" && (params.workflowRunId === undefined || entry.workflowRunId === params.workflowRunId)),
|
|
790
|
+
async (entry) => {
|
|
791
|
+
const persisted = await getRunRecord(runsDirectory, entry.runId);
|
|
792
|
+
return !persisted || persisted.parentSessionId !== sessionId || persisted.project !== project
|
|
793
|
+
|| (params.workflowRunId !== undefined && persisted.workflowRunId !== params.workflowRunId);
|
|
794
|
+
},
|
|
795
|
+
(limit, cursor) => listRunRecords({ runsDirectory, sessionId, project, workflowRunId: params.workflowRunId, limit, cursor }));
|
|
796
|
+
const historical = params.workflowRunId
|
|
797
|
+
? { live: [], items: [] }
|
|
798
|
+
: await pageUnpersistedFirst(listScope("workflows", sessionId, project), params.workflowCursor, Math.max(1, Math.min(100, Math.floor(params.limit ?? 100))),
|
|
799
|
+
[...registered.values()].filter((entry) => entry.kind === "workflow"),
|
|
800
|
+
async (entry) => !workflowDir || !await hasWorkflowJournal(workflowDir, entry.runId, project),
|
|
801
|
+
async (limit, cursor) => workflowDir ? listWorkflowJournals(workflowDir, project, limit, cursor) : { items: [] });
|
|
802
|
+
const workflows = [
|
|
803
|
+
...historical.live.map((entry) => liveSummary(entry)),
|
|
804
|
+
...historical.items.map((journal) => {
|
|
805
|
+
const live = registered.get(journal.runId);
|
|
806
|
+
if (live?.kind === "workflow") return liveSummary(live);
|
|
807
|
+
return "unreadable" in journal ? unreadableJournalSummary(journal) : journalSummary(journal);
|
|
808
|
+
}),
|
|
809
|
+
];
|
|
810
|
+
const runs = [
|
|
811
|
+
...page.live.map(listedAgent),
|
|
812
|
+
...page.items.map((item) => {
|
|
813
|
+
const live = registered.get(item.runId);
|
|
814
|
+
return live?.kind === "agent" ? listedAgent(live) : historicalAgent(item);
|
|
815
|
+
}),
|
|
816
|
+
];
|
|
712
817
|
const list = { workflows, runs, nextCursor: page.nextCursor, nextWorkflowCursor: historical.nextCursor };
|
|
713
818
|
return result(JSON.stringify(list), list);
|
|
714
819
|
}
|
package/src/workflow/journal.ts
CHANGED
|
@@ -41,6 +41,14 @@ export interface LoadedWorkflowJournal {
|
|
|
41
41
|
children: Array<{ index: number; runId?: string; label?: string; status: "queued" | "done" | "error" | "aborted"; outcome?: ChildRunOutcome; error?: unknown }>;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** A journal that exists but cannot be loaded; `project` is set when its own run_start line names one. */
|
|
45
|
+
export class WorkflowJournalReadError extends Error {
|
|
46
|
+
constructor(message: string, readonly project?: string) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "WorkflowJournalReadError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
export interface WorkflowJournalWriter {
|
|
45
53
|
runId: string;
|
|
46
54
|
path: string;
|
|
@@ -128,8 +136,9 @@ export async function loadWorkflowJournal(dir: string, runId: string): Promise<L
|
|
|
128
136
|
}
|
|
129
137
|
if (entry.type === "run_start") {
|
|
130
138
|
if (entry.version !== JOURNAL_VERSION || entry.apiVersion !== WORKFLOW_API_VERSION) {
|
|
131
|
-
throw new
|
|
139
|
+
throw new WorkflowJournalReadError(
|
|
132
140
|
`Workflow journal ${path} uses an incompatible API contract; recompose with meta.apiVersion: ${WORKFLOW_API_VERSION}. No children were launched`,
|
|
141
|
+
typeof entry.project === "string" ? entry.project : undefined,
|
|
133
142
|
);
|
|
134
143
|
}
|
|
135
144
|
seenRunStart = entry.runId === runId;
|
|
@@ -190,13 +199,19 @@ export async function loadWorkflowJournal(dir: string, runId: string): Promise<L
|
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
if (!seenRunStart) {
|
|
193
|
-
throw new
|
|
202
|
+
throw new WorkflowJournalReadError(`Workflow journal ${path} does not match run id ${runId}`, project);
|
|
194
203
|
}
|
|
195
204
|
return { launch, runId, path, agentResults, name, source, project, status, outcome, result, error: terminalError, children: children.filter(Boolean) };
|
|
196
205
|
}
|
|
197
206
|
|
|
207
|
+
/** A listed journal that failed to load: shown as its own row instead of failing the whole listing. */
|
|
208
|
+
export interface UnreadableWorkflowJournal {
|
|
209
|
+
runId: string;
|
|
210
|
+
unreadable: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
198
213
|
export interface WorkflowJournalPage {
|
|
199
|
-
items: LoadedWorkflowJournal
|
|
214
|
+
items: Array<LoadedWorkflowJournal | UnreadableWorkflowJournal>;
|
|
200
215
|
nextCursor?: string;
|
|
201
216
|
}
|
|
202
217
|
|
|
@@ -228,12 +243,19 @@ export async function listWorkflowJournals(dir: string, project: string, limit =
|
|
|
228
243
|
}
|
|
229
244
|
const start = workflowListStart(names, cursor);
|
|
230
245
|
const pageLimit = Math.max(1, Math.min(100, limit));
|
|
231
|
-
const journals:
|
|
246
|
+
const journals: WorkflowJournalPage["items"] = [];
|
|
232
247
|
let index = start;
|
|
233
248
|
for (; index < names.length && index < start + 200; index++) {
|
|
234
249
|
const name = names[index]!;
|
|
235
|
-
const
|
|
236
|
-
|
|
250
|
+
const runId = name.slice(4, -6);
|
|
251
|
+
try {
|
|
252
|
+
const journal = await loadWorkflowJournal(dir, runId);
|
|
253
|
+
if (journal?.project === project) journals.push(journal);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
// The directory is this session's own; a journal whose project cannot be read is still listed so it is never silently dropped.
|
|
256
|
+
const owner = error instanceof WorkflowJournalReadError ? error.project : undefined;
|
|
257
|
+
if (owner === undefined || owner === project) journals.push({ runId, unreadable: error instanceof Error ? error.message : String(error) });
|
|
258
|
+
}
|
|
237
259
|
if (journals.length >= pageLimit) {
|
|
238
260
|
index++;
|
|
239
261
|
break;
|
package/src/workflow/source.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
createWorkflowJournalWriter,
|
|
7
7
|
createWorkflowRunIdentity,
|
|
8
8
|
getSessionWorkflowDir,
|
|
9
|
+
WorkflowJournalReadError,
|
|
9
10
|
loadWorkflowJournal,
|
|
10
11
|
persistWorkflowScript,
|
|
11
12
|
type WorkflowJournalWriter,
|
|
@@ -270,19 +271,23 @@ export async function prepareWorkflowToolSource(
|
|
|
270
271
|
try {
|
|
271
272
|
journal = await loadWorkflowJournal(sessionWorkflowDir, resumeFromRunId);
|
|
272
273
|
} catch (error) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
274
|
+
// Another project's unreadable journal is reported exactly like a missing one.
|
|
275
|
+
if (!(error instanceof WorkflowJournalReadError) || error.project === undefined || error.project === project) {
|
|
276
|
+
const message = `Cannot resume workflow: ${error instanceof Error ? error.message : String(error)}`;
|
|
277
|
+
return sourceError(message, {
|
|
278
|
+
name: metaName,
|
|
279
|
+
error: message,
|
|
280
|
+
logs: source.warnings,
|
|
281
|
+
source: source.source,
|
|
282
|
+
sourcePath: source.sourcePath,
|
|
283
|
+
scriptPath,
|
|
284
|
+
runId: identity.runId,
|
|
285
|
+
resumeFromRunId,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
284
288
|
}
|
|
285
|
-
|
|
289
|
+
// Another project's run is indistinguishable from an unknown one.
|
|
290
|
+
if (!journal || journal.project !== project) {
|
|
286
291
|
const message = `Cannot resume workflow: run journal not found for ${resumeFromRunId}.`;
|
|
287
292
|
return sourceError(message, {
|
|
288
293
|
name: metaName,
|
|
@@ -295,19 +300,6 @@ export async function prepareWorkflowToolSource(
|
|
|
295
300
|
resumeFromRunId,
|
|
296
301
|
});
|
|
297
302
|
}
|
|
298
|
-
if (journal.project !== project) {
|
|
299
|
-
const message = `Cannot resume workflow: ${resumeFromRunId} belongs to a different project. No children were launched.`;
|
|
300
|
-
return sourceError(message, {
|
|
301
|
-
name: metaName,
|
|
302
|
-
error: message,
|
|
303
|
-
logs: source.warnings,
|
|
304
|
-
source: source.source,
|
|
305
|
-
sourcePath: source.sourcePath,
|
|
306
|
-
scriptPath,
|
|
307
|
-
runId: identity.runId,
|
|
308
|
-
resumeFromRunId,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
303
|
resumeAgentResults = journal.agentResults;
|
|
312
304
|
}
|
|
313
305
|
|
package/src/workflow/tool.ts
CHANGED
|
@@ -18,7 +18,8 @@ import { CHILD_EXCLUDED_TOOLS, spawnSubagent } from "../core/spawn.ts";
|
|
|
18
18
|
import { createRunRecord } from "../core/run-record.ts";
|
|
19
19
|
import { runRecordsDirectory } from "../core/retention.ts";
|
|
20
20
|
import { captureParentContext } from "../core/parent-context.ts";
|
|
21
|
-
import { RunRegistry } from "../core/run-registry.ts";
|
|
21
|
+
import { RunRegistry, type RegisteredRunHandle } from "../core/run-registry.ts";
|
|
22
|
+
import { ExpectedFlowError } from "../core/errors.ts";
|
|
22
23
|
import { OUTPUT_PREVIEW_CHARS } from "../core/progress.ts";
|
|
23
24
|
import { loadExternalCatalog, resolveExternalProfile, selectorHarness, unknownExternalProfileMessage } from "../profiles.ts";
|
|
24
25
|
import { resolveExecutionProfile } from '../execution-config.ts';
|
|
@@ -585,23 +586,33 @@ export function createWorkflowTool(
|
|
|
585
586
|
}
|
|
586
587
|
};
|
|
587
588
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
outcome:
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
? {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
589
|
+
let registered: RegisteredRunHandle<ReturnType<typeof workflowResult>>;
|
|
590
|
+
try {
|
|
591
|
+
registered = options.registry.start({
|
|
592
|
+
runId: identity.runId,
|
|
593
|
+
kind: "workflow",
|
|
594
|
+
sessionId,
|
|
595
|
+
sessionVersion,
|
|
596
|
+
project,
|
|
597
|
+
...(background ? {} : { signal }),
|
|
598
|
+
run: executeWorkflow,
|
|
599
|
+
outcome: (result, workflowSignal) => ({
|
|
600
|
+
status: result.details.status === "completed" ? "done" : result.details.status === "aborted" ? "aborted" : "error",
|
|
601
|
+
outcome: result.details.outcome ?? (result.details.status === "completed" ? "succeeded" : result.details.status === "aborted" ? "cancelled" : "failed"),
|
|
602
|
+
...(result.details.result !== undefined ? { result: result.details.result } : {}),
|
|
603
|
+
...(workflowSignal.aborted && workflowSignal.reason !== undefined
|
|
604
|
+
? { error: workflowSignal.reason instanceof Error ? workflowSignal.reason.message : String(workflowSignal.reason) }
|
|
605
|
+
: result.details.error ? { error: result.details.error } : {}),
|
|
606
|
+
}),
|
|
607
|
+
});
|
|
608
|
+
} catch (error) {
|
|
609
|
+
// The run_start journal line is already written; settle it so the
|
|
610
|
+
// never-started run does not read as interrupted forever.
|
|
611
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
612
|
+
await journalWriter?.fail(message).catch(() => undefined);
|
|
613
|
+
if (!(error instanceof ExpectedFlowError)) throw error;
|
|
614
|
+
return workflowError(`Workflow "${metaName}" was not started: ${message}`, { name: metaName, error: message, status: "error", outcome: "failed", runId: identity.runId, journalPath: journalWriter?.path });
|
|
615
|
+
}
|
|
605
616
|
emit();
|
|
606
617
|
if (!background) return await registered.result;
|
|
607
618
|
return workflowResult(
|