pi-extensible-workflows 1.0.0 → 1.0.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/dist/src/agent-execution.js +47 -7
- package/dist/src/ambient-workflow-evals.js +14 -33
- package/dist/src/doctor.js +3 -5
- package/dist/src/index.d.ts +7 -4
- package/dist/src/index.js +176 -117
- package/dist/src/persistence.d.ts +2 -0
- package/dist/src/persistence.js +27 -4
- package/dist/src/workflow-evals.d.ts +1 -0
- package/dist/src/workflow-evals.js +23 -25
- package/package.json +5 -2
- package/skills/pi-extensible-workflows/SKILL.md +6 -2
- package/src/agent-execution.ts +45 -7
- package/src/ambient-workflow-evals.ts +18 -28
- package/src/doctor.ts +2 -5
- package/src/index.ts +145 -79
- package/src/persistence.ts +23 -4
- package/src/workflow-evals.ts +13 -16
package/dist/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { fork } from "node:child_process";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync,
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir, tmpdir } from "node:os";
|
|
6
6
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -12,11 +12,10 @@ import { Value } from "typebox/value";
|
|
|
12
12
|
import { copyToClipboard, getAgentDir, parseFrontmatter, highlightCode, SessionManager, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { createNativeAgentSession, FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
|
|
14
14
|
import { transcriptLines } from "./session-inspector.js";
|
|
15
|
-
import { acquireSessionLease, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
|
|
15
|
+
import { acquireSessionLease, atomicWriteFile, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
|
|
16
16
|
export const RUN_STATES = ["queued", "running", "pausing", "paused", "awaiting_input", "completed", "failed", "stopped", "interrupted", "budget_exhausted"];
|
|
17
17
|
export const AGENT_STATES = ["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"];
|
|
18
|
-
export const
|
|
19
|
-
export const WORKFLOW_ASYNC_COMPLETE_EVENT = "workflow:async-complete";
|
|
18
|
+
export const WORKFLOW_CALL_KINDS = ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree"];
|
|
20
19
|
export const WORKFLOW_RUN_STARTED_EVENT = "workflow:run-started";
|
|
21
20
|
export const WORKFLOW_RUN_RESUMED_EVENT = "workflow:run-resumed";
|
|
22
21
|
export const WORKFLOW_RUN_STATE_CHANGED_EVENT = "workflow:run-state-changed";
|
|
@@ -188,6 +187,7 @@ export class RunLifecycle {
|
|
|
188
187
|
export const DEFAULT_SETTINGS = Object.freeze({ concurrency: 8 });
|
|
189
188
|
function fail(code, message) { throw new WorkflowError(code, message); }
|
|
190
189
|
function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
190
|
+
export { object as isObject };
|
|
191
191
|
function nonNegativeInteger(value) { return Number.isInteger(value) && value >= 0; }
|
|
192
192
|
function nonNegativeFinite(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
|
|
193
193
|
export function validateBudget(value) {
|
|
@@ -562,9 +562,7 @@ export function saveModelAliases(path = workflowSettingsPath(), aliases = {}) {
|
|
|
562
562
|
throw error;
|
|
563
563
|
}
|
|
564
564
|
mkdirSync(dirname(path), { recursive: true });
|
|
565
|
-
|
|
566
|
-
writeFileSync(temporary, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, { mode: 0o600 });
|
|
567
|
-
renameSync(temporary, path);
|
|
565
|
+
atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
|
|
568
566
|
}
|
|
569
567
|
export function parseRoleMarkdown(content, strict = false, rolePath) {
|
|
570
568
|
if (!strict) {
|
|
@@ -700,20 +698,32 @@ function parseWorkflow(script) {
|
|
|
700
698
|
function astNode(value) {
|
|
701
699
|
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
702
700
|
}
|
|
701
|
+
function astChildren(node) {
|
|
702
|
+
const children = [];
|
|
703
|
+
for (const value of Object.values(node)) {
|
|
704
|
+
if (Array.isArray(value)) {
|
|
705
|
+
for (const child of value)
|
|
706
|
+
if (astNode(child))
|
|
707
|
+
children.push(child);
|
|
708
|
+
}
|
|
709
|
+
else if (astNode(value))
|
|
710
|
+
children.push(value);
|
|
711
|
+
}
|
|
712
|
+
return children;
|
|
713
|
+
}
|
|
714
|
+
function workflowCallKind(node) {
|
|
715
|
+
if (node.type !== "CallExpression" || node.callee.type !== "Identifier")
|
|
716
|
+
return undefined;
|
|
717
|
+
const kind = node.callee.name;
|
|
718
|
+
return WORKFLOW_CALL_KINDS.includes(kind) ? kind : undefined;
|
|
719
|
+
}
|
|
703
720
|
function workflowCalls(program) {
|
|
704
721
|
const calls = [];
|
|
705
722
|
const visit = (node) => {
|
|
706
|
-
if (
|
|
723
|
+
if (workflowCallKind(node))
|
|
707
724
|
calls.push(node);
|
|
708
|
-
for (const
|
|
709
|
-
|
|
710
|
-
for (const child of value)
|
|
711
|
-
if (astNode(child))
|
|
712
|
-
visit(child);
|
|
713
|
-
}
|
|
714
|
-
else if (astNode(value))
|
|
715
|
-
visit(value);
|
|
716
|
-
}
|
|
725
|
+
for (const child of astChildren(node))
|
|
726
|
+
visit(child);
|
|
717
727
|
};
|
|
718
728
|
visit(program);
|
|
719
729
|
return calls.sort((left, right) => left.start - right.start);
|
|
@@ -728,9 +738,9 @@ function workflowCallsWithStructure(program) {
|
|
|
728
738
|
if (scope?.key === null && key)
|
|
729
739
|
current = { ...current, structure: [...current.structure.slice(0, -1), { ...scope, key }] };
|
|
730
740
|
}
|
|
731
|
-
|
|
741
|
+
const operation = workflowCallKind(node);
|
|
742
|
+
if (operation) {
|
|
732
743
|
const call = node;
|
|
733
|
-
const operation = call.callee.name;
|
|
734
744
|
const execution = operation === "parallel" ? "parallel" : operation === "pipeline" ? "sequential" : current.execution;
|
|
735
745
|
calls.push({ call, execution, structure: current.structure });
|
|
736
746
|
for (const [index, argument] of call.arguments.entries()) {
|
|
@@ -741,15 +751,8 @@ function workflowCallsWithStructure(program) {
|
|
|
741
751
|
}
|
|
742
752
|
return;
|
|
743
753
|
}
|
|
744
|
-
for (const
|
|
745
|
-
|
|
746
|
-
for (const child of value)
|
|
747
|
-
if (astNode(child))
|
|
748
|
-
visit(child, current);
|
|
749
|
-
}
|
|
750
|
-
else if (astNode(value))
|
|
751
|
-
visit(value, current);
|
|
752
|
-
}
|
|
754
|
+
for (const child of astChildren(node))
|
|
755
|
+
visit(child, current);
|
|
753
756
|
};
|
|
754
757
|
visit(program, { execution: "sequential", structure: [] });
|
|
755
758
|
return calls.sort((left, right) => left.call.start - right.call.start);
|
|
@@ -762,22 +765,15 @@ function validateDirectPrimitiveReferences(program, name) {
|
|
|
762
765
|
if (!directCall && !propertyKey)
|
|
763
766
|
fail("INVALID_METADATA", `${name} calls must use a direct ${name}(...) call; aliases and indirect calls are unsupported`);
|
|
764
767
|
}
|
|
765
|
-
for (const
|
|
766
|
-
|
|
767
|
-
for (const child of value)
|
|
768
|
-
if (astNode(child))
|
|
769
|
-
visit(child, node);
|
|
770
|
-
}
|
|
771
|
-
else if (astNode(value))
|
|
772
|
-
visit(value, node);
|
|
773
|
-
}
|
|
768
|
+
for (const child of astChildren(node))
|
|
769
|
+
visit(child, node);
|
|
774
770
|
};
|
|
775
771
|
visit(program);
|
|
776
772
|
}
|
|
777
773
|
function hasIdentifier(node, name) {
|
|
778
774
|
if (node.type === "Identifier" && node.name === name)
|
|
779
775
|
return true;
|
|
780
|
-
return
|
|
776
|
+
return astChildren(node).some((child) => hasIdentifier(child, name));
|
|
781
777
|
}
|
|
782
778
|
const INTERNAL_AGENT_NAME = "__pi_extensible_workflows_agent";
|
|
783
779
|
const INTERNAL_CONVERSATION_NAME = "__pi_extensible_workflows_conversation";
|
|
@@ -1156,7 +1152,7 @@ export class WorkflowRegistry {
|
|
|
1156
1152
|
fail("RESULT_INVALID", `Invalid replay for ${name}`);
|
|
1157
1153
|
return structuredClone(replayed);
|
|
1158
1154
|
}
|
|
1159
|
-
const result = await fn.run(deepFreeze(structuredClone(input)), Object.freeze({ run: context.run, agent: context.agent, prompt: context.prompt, parallel: context.parallel, pipeline: context.pipeline, withWorktree: context.withWorktree, checkpoint: context.checkpoint, phase: context.phase, log: context.log }));
|
|
1155
|
+
const result = await fn.run(deepFreeze(structuredClone(input)), Object.freeze({ run: context.run, invoke: context.invoke, agent: context.agent, prompt: context.prompt, parallel: context.parallel, pipeline: context.pipeline, withWorktree: context.withWorktree, checkpoint: context.checkpoint, phase: context.phase, log: context.log }));
|
|
1160
1156
|
if (!jsonValue(result) || !Value.Check(fn.output, result))
|
|
1161
1157
|
fail("RESULT_INVALID", `Invalid output from ${name}`);
|
|
1162
1158
|
const stored = structuredClone(result);
|
|
@@ -1333,8 +1329,6 @@ export function createLaunchSnapshot(input) {
|
|
|
1333
1329
|
return deepFreeze(structuredClone({ ...input, identityVersion: input.identityVersion ?? LAUNCH_SNAPSHOT_IDENTITY_VERSION }));
|
|
1334
1330
|
}
|
|
1335
1331
|
export function loadLaunchSnapshot(input) {
|
|
1336
|
-
if (object(input.settings) && Object.prototype.hasOwnProperty.call(input.settings, "maxAgentLaunches"))
|
|
1337
|
-
fail("RESUME_INCOMPATIBLE", "Persisted workflow settings contain removed maxAgentLaunches");
|
|
1338
1332
|
return deepFreeze(structuredClone(input));
|
|
1339
1333
|
}
|
|
1340
1334
|
export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
@@ -2121,30 +2115,17 @@ class WorkflowEventPublisher {
|
|
|
2121
2115
|
seen.add(this.budgetKey(event));
|
|
2122
2116
|
this.#budgetEvents.set(runId, seen);
|
|
2123
2117
|
}
|
|
2124
|
-
async runStarted(store, metadata,
|
|
2125
|
-
await this.#publish(store, metadata, WORKFLOW_RUN_STARTED_EVENT, {});
|
|
2126
|
-
if (background)
|
|
2127
|
-
await this.legacyStarted(store, metadata);
|
|
2128
|
-
}
|
|
2129
|
-
async legacyStarted(store, metadata) { await this.#publish(store, metadata, WORKFLOW_ASYNC_STARTED_EVENT, { id: store.runId, runId: store.runId, pid: process.pid, sessionId: store.sessionId, asyncDir: store.directory, agent: metadata.name }); }
|
|
2118
|
+
async runStarted(store, metadata) { await this.#publish(store, metadata, WORKFLOW_RUN_STARTED_EVENT, {}); }
|
|
2130
2119
|
async runResumed(store, metadata) { await this.#publish(store, metadata, WORKFLOW_RUN_RESUMED_EVENT, {}); }
|
|
2131
2120
|
async runState(store, metadata, previousState, state, reason) {
|
|
2132
2121
|
await this.#publish(store, metadata, WORKFLOW_RUN_STATE_CHANGED_EVENT, { previousState, state, ...(reason ? { reason } : {}), ...(ERROR_CODES.includes(reason) ? { errorCode: reason } : {}) });
|
|
2133
2122
|
if ((previousState === "paused" || previousState === "interrupted" || previousState === "budget_exhausted") && state === "running")
|
|
2134
2123
|
await this.runResumed(store, metadata);
|
|
2135
2124
|
}
|
|
2136
|
-
async runCompleted(store, metadata, resultPath,
|
|
2137
|
-
|
|
2138
|
-
if (background)
|
|
2139
|
-
await this.#publish(store, metadata, WORKFLOW_ASYNC_COMPLETE_EVENT, { id: store.runId, runId: store.runId, sessionId: store.sessionId, asyncDir: store.directory, success: true, state: "complete" });
|
|
2140
|
-
}
|
|
2141
|
-
async runFailed(store, metadata, error, background, state) {
|
|
2125
|
+
async runCompleted(store, metadata, resultPath) { await this.#publish(store, metadata, WORKFLOW_RUN_COMPLETED_EVENT, { resultPath }); }
|
|
2126
|
+
async runFailed(store, metadata, error, state) {
|
|
2142
2127
|
if (state === "failed")
|
|
2143
2128
|
await this.#publish(store, metadata, WORKFLOW_RUN_FAILED_EVENT, { error: safeEventError(error) });
|
|
2144
|
-
if (background) {
|
|
2145
|
-
const legacyState = state === "interrupted" ? "failed" : state;
|
|
2146
|
-
await this.#publish(store, metadata, WORKFLOW_ASYNC_COMPLETE_EVENT, { id: store.runId, runId: store.runId, sessionId: store.sessionId, asyncDir: store.directory, success: false, state: legacyState, ...(state === "stopped" ? { stopped: true } : {}), ...(legacyState === "budget_exhausted" || legacyState === "failed" ? { error: safeEventError(error).message } : {}) });
|
|
2147
|
-
}
|
|
2148
2129
|
}
|
|
2149
2130
|
async agentState(store, metadata, previous, agent) {
|
|
2150
2131
|
await this.#publish(store, metadata, WORKFLOW_AGENT_STATE_CHANGED_EVENT, { agentId: agent.id, displayLabel: agent.label ?? agent.name, ...(agent.role ? { role: agent.role } : {}), structuralPath: [...(agent.structuralPath ?? [])], ...(agent.parentId ? { parentId: agent.parentId } : {}), ...(agent.parentBreadcrumb ? { parentBreadcrumb: agent.parentBreadcrumb } : {}), ...(agent.worktreeOwner ? { worktreeOwner: agent.worktreeOwner } : {}), ...(previous ? { previousState: previous.state } : {}), state: agent.state, attempt: agent.attempts });
|
|
@@ -2321,46 +2302,100 @@ function nextNamedOccurrence(counters, label) {
|
|
|
2321
2302
|
function withWorkflowFunctions(bridge, store, runContext, variables, registry) {
|
|
2322
2303
|
const functionAgentOccurrences = new Map();
|
|
2323
2304
|
const functionWorktreeOccurrences = new Map();
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2305
|
+
const functionInvokeOccurrences = new Map();
|
|
2306
|
+
const invokeFunction = async (name, input, path, signal, worktreeOwner, structuralPath = [], breadcrumb) => {
|
|
2307
|
+
const replayed = await store.replay(path);
|
|
2308
|
+
let stored;
|
|
2309
|
+
const sideEffects = [];
|
|
2310
|
+
const functionBreadcrumb = breadcrumb ?? name;
|
|
2311
|
+
const context = {
|
|
2312
|
+
run: runContext,
|
|
2313
|
+
invoke: async (targetName, targetInput) => {
|
|
2314
|
+
const inherited = inheritedHostAgentPath.getStore() ?? structuralPath;
|
|
2315
|
+
const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
|
|
2316
|
+
const key = JSON.stringify([path, inherited, targetName]);
|
|
2317
|
+
const occurrence = (functionInvokeOccurrences.get(key) ?? 0) + 1;
|
|
2318
|
+
functionInvokeOccurrences.set(key, occurrence);
|
|
2319
|
+
const nestedPath = operationPath("function", "nested", path, ...inherited, targetName, `occurrence:${String(occurrence)}`);
|
|
2320
|
+
return invokeFunction(targetName, targetInput, nestedPath, signal, scopedWorktreeOwner, inherited, `${functionBreadcrumb} > ${targetName}`);
|
|
2321
|
+
},
|
|
2322
|
+
agent: async (...args) => {
|
|
2323
|
+
if (!bridge.agent || typeof args[0] !== "string")
|
|
2324
|
+
fail("AGENT_FAILED", "No agent bridge is available");
|
|
2325
|
+
const options = validateAgentOptions(args[1] === undefined ? {} : args[1]);
|
|
2326
|
+
const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
|
|
2327
|
+
const inherited = inheritedHostAgentPath.getStore() ?? [];
|
|
2328
|
+
const key = `${path}\0${JSON.stringify(inherited)}`;
|
|
2329
|
+
const occurrence = (functionAgentOccurrences.get(key) ?? 0) + 1;
|
|
2330
|
+
functionAgentOccurrences.set(key, occurrence);
|
|
2331
|
+
return bridge.agent(args[0], options, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, parentBreadcrumb: functionBreadcrumb, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
|
|
2332
|
+
},
|
|
2333
|
+
prompt: workflowPrompt,
|
|
2334
|
+
parallel: (...args) => hostParallel(args[0], args[1]),
|
|
2335
|
+
pipeline: (...args) => hostPipeline(args[0], args[1], args[2]),
|
|
2336
|
+
withWorktree: (...args) => hostWithWorktree(args, path, functionWorktreeOccurrences),
|
|
2337
|
+
checkpoint: async (...args) => {
|
|
2338
|
+
if (!bridge.checkpoint || !object(args[0]) || !jsonValue(args[0]))
|
|
2339
|
+
fail("INTERNAL_ERROR", "No checkpoint bridge is available");
|
|
2340
|
+
return bridge.checkpoint(args[0], signal);
|
|
2341
|
+
},
|
|
2342
|
+
phase: (name) => { sideEffects.push(Promise.resolve(bridge.phase?.(name))); },
|
|
2343
|
+
log: (message) => { sideEffects.push(Promise.resolve(bridge.log?.(message))); },
|
|
2344
|
+
};
|
|
2345
|
+
const result = await inheritedHostAgentPath.run([...structuralPath], async () => registry.invokeFunction(name, input, context, path, { get: () => replayed?.value, put: (_path, value) => { stored = value; } }));
|
|
2346
|
+
await Promise.all(sideEffects);
|
|
2347
|
+
if (!replayed)
|
|
2348
|
+
await store.complete(path, stored ?? result);
|
|
2349
|
+
return result;
|
|
2350
|
+
};
|
|
2351
|
+
return { ...bridge, functions: registry.globals(), variables, function: invokeFunction };
|
|
2359
2352
|
}
|
|
2360
2353
|
function projectTrusted(ctx) {
|
|
2361
2354
|
const check = object(ctx) ? ctx.isProjectTrusted : undefined;
|
|
2362
2355
|
return typeof check === "function" ? Boolean(Reflect.apply(check, ctx, [])) : true;
|
|
2363
2356
|
}
|
|
2357
|
+
function isEntryRenderer(value) { return typeof value === "function"; }
|
|
2358
|
+
function isWorkflowEventSink(value) { return object(value) && typeof value.emit === "function"; }
|
|
2359
|
+
function piHostCapabilities(pi) {
|
|
2360
|
+
if (!object(pi))
|
|
2361
|
+
return {};
|
|
2362
|
+
const registerEntryRenderer = pi.registerEntryRenderer;
|
|
2363
|
+
const events = pi.events;
|
|
2364
|
+
return { ...(isEntryRenderer(registerEntryRenderer) ? { registerEntryRenderer } : {}), ...(isWorkflowEventSink(events) ? { events } : {}) };
|
|
2365
|
+
}
|
|
2366
|
+
function isModelRegistryGetter(value) { return typeof value === "function"; }
|
|
2367
|
+
function contextHostCapabilities(ctx) {
|
|
2368
|
+
if (!object(ctx) || !object(ctx.modelRegistry))
|
|
2369
|
+
return {};
|
|
2370
|
+
const registry = ctx.modelRegistry;
|
|
2371
|
+
const getAll = registry.getAll;
|
|
2372
|
+
const getAvailable = registry.getAvailable;
|
|
2373
|
+
return { modelRegistry: { ...(isModelRegistryGetter(getAll) ? { getAll: () => getAll.call(registry) } : {}), ...(isModelRegistryGetter(getAvailable) ? { getAvailable: () => getAvailable.call(registry) } : {}) } };
|
|
2374
|
+
}
|
|
2375
|
+
function isUiSelect(value) { return typeof value === "function"; }
|
|
2376
|
+
function isUiInput(value) { return typeof value === "function"; }
|
|
2377
|
+
function isUiSetStatus(value) { return typeof value === "function"; }
|
|
2378
|
+
function uiHostCapabilities(ui) {
|
|
2379
|
+
if (!object(ui))
|
|
2380
|
+
return undefined;
|
|
2381
|
+
const select = ui.select;
|
|
2382
|
+
const input = ui.input;
|
|
2383
|
+
const setStatus = ui.setStatus;
|
|
2384
|
+
return { ...(isUiSelect(select) ? { select } : {}), ...(isUiInput(input) ? { input } : {}), ...(isUiSetStatus(setStatus) ? { setStatus } : {}) };
|
|
2385
|
+
}
|
|
2386
|
+
function tuiHostCapabilities(tui) {
|
|
2387
|
+
if (!object(tui) || !object(tui.terminal))
|
|
2388
|
+
return {};
|
|
2389
|
+
return { terminal: { ...(tui.terminal.rows === undefined ? {} : { rows: tui.terminal.rows }) } };
|
|
2390
|
+
}
|
|
2391
|
+
function tuiRows(tui) { const rows = tuiHostCapabilities(tui).terminal?.rows; return typeof rows === "number" && Number.isFinite(rows) ? rows : 24; }
|
|
2392
|
+
function isKeybindingGetter(value) { return typeof value === "function"; }
|
|
2393
|
+
function keybindingsHostCapabilities(keybindings) {
|
|
2394
|
+
if (!object(keybindings) || !isKeybindingGetter(keybindings.getKeys))
|
|
2395
|
+
return {};
|
|
2396
|
+
return { getKeys: keybindings.getKeys };
|
|
2397
|
+
}
|
|
2398
|
+
function keybindingKeys(keybindings, name) { const getKeys = keybindingsHostCapabilities(keybindings).getKeys; return typeof getKeys === "function" ? getKeys.call(keybindings, name) : undefined; }
|
|
2364
2399
|
function parseThinking(value) {
|
|
2365
2400
|
switch (value) {
|
|
2366
2401
|
case "off":
|
|
@@ -2376,7 +2411,7 @@ function parseThinking(value) {
|
|
|
2376
2411
|
export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession) {
|
|
2377
2412
|
beginWorkflowExtensionLoading();
|
|
2378
2413
|
const registry = loadingRegistry();
|
|
2379
|
-
const registerEntryRenderer = pi.registerEntryRenderer;
|
|
2414
|
+
const registerEntryRenderer = piHostCapabilities(pi).registerEntryRenderer;
|
|
2380
2415
|
registerEntryRenderer?.(WORKFLOW_LOG_ENTRY, (entry) => {
|
|
2381
2416
|
const data = entry.data;
|
|
2382
2417
|
return textBlock(data ? `Workflow ${data.workflowName}: ${data.message}` : "");
|
|
@@ -2390,7 +2425,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2390
2425
|
await lifecycle.leave();
|
|
2391
2426
|
}
|
|
2392
2427
|
};
|
|
2393
|
-
const eventPublisher = new WorkflowEventPublisher(pi.events);
|
|
2428
|
+
const eventPublisher = new WorkflowEventPublisher(piHostCapabilities(pi).events);
|
|
2394
2429
|
pi.on("resources_discover", () => {
|
|
2395
2430
|
if (!pi.getActiveTools().includes("workflow"))
|
|
2396
2431
|
return;
|
|
@@ -2399,6 +2434,28 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2399
2434
|
return skillPath ? { skillPaths: [skillPath] } : undefined;
|
|
2400
2435
|
});
|
|
2401
2436
|
const runs = new Map();
|
|
2437
|
+
const liveActivities = new Map();
|
|
2438
|
+
const setLiveActivity = (runId, agentId, activity) => {
|
|
2439
|
+
const activities = liveActivities.get(runId);
|
|
2440
|
+
if (activity) {
|
|
2441
|
+
if (activities)
|
|
2442
|
+
activities.set(agentId, activity);
|
|
2443
|
+
else
|
|
2444
|
+
liveActivities.set(runId, new Map([[agentId, activity]]));
|
|
2445
|
+
}
|
|
2446
|
+
else {
|
|
2447
|
+
activities?.delete(agentId);
|
|
2448
|
+
if (activities?.size === 0)
|
|
2449
|
+
liveActivities.delete(runId);
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
const withLiveActivities = (run) => {
|
|
2453
|
+
const activities = liveActivities.get(run.id);
|
|
2454
|
+
return activities?.size ? { ...run, agents: run.agents.map((agent) => {
|
|
2455
|
+
const activity = activities.get(agent.id);
|
|
2456
|
+
return activity ? { ...agent, activity } : agent;
|
|
2457
|
+
}) } : run;
|
|
2458
|
+
};
|
|
2402
2459
|
const conversationLocks = new Set();
|
|
2403
2460
|
const terminalRunStates = new Map();
|
|
2404
2461
|
let sessionLease;
|
|
@@ -2443,7 +2500,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2443
2500
|
return nextRun;
|
|
2444
2501
|
});
|
|
2445
2502
|
await eventPublisher.runState(store, metadata, previous, next, reason);
|
|
2446
|
-
runs.get(store.runId)?.update?.(workflowToolUpdate(persisted));
|
|
2503
|
+
runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
2447
2504
|
});
|
|
2448
2505
|
const scheduler = new FairAgentScheduler(async ({ id, runId, parentId, prompt, options, signal, setSteer }) => {
|
|
2449
2506
|
const run = runs.get(runId);
|
|
@@ -2464,7 +2521,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2464
2521
|
}
|
|
2465
2522
|
if (!runState.agents.some((agent) => agent.id === id))
|
|
2466
2523
|
return;
|
|
2467
|
-
|
|
2524
|
+
setLiveActivity(runId, id, progress.activity);
|
|
2525
|
+
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
2468
2526
|
};
|
|
2469
2527
|
const onAttempt = async (attempt) => {
|
|
2470
2528
|
await scheduler.flush();
|
|
@@ -2475,7 +2533,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2475
2533
|
const active = (await run.store.load()).run;
|
|
2476
2534
|
await eventPublisher.agentStates(run.store, run.metadata, before.agents, active.agents);
|
|
2477
2535
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
2478
|
-
run.update?.(workflowToolUpdate(persisted));
|
|
2536
|
+
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
2479
2537
|
};
|
|
2480
2538
|
const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, onProgress, onAttempt, budget, ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.thinking ? { thinking: options.thinking } : {}), ...(options.role ? { role: options.role } : {}), ...(options.role ? {} : { tools: options.tools }), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}), ...(options.conversation ? { conversation: options.conversation } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, thinking) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
|
|
2481
2539
|
const before = (await run.store.load()).run;
|
|
@@ -2483,7 +2541,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2483
2541
|
const completed = (await run.store.load()).run;
|
|
2484
2542
|
await eventPublisher.agentStates(run.store, run.metadata, before.agents, completed.agents);
|
|
2485
2543
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
2486
|
-
|
|
2544
|
+
setLiveActivity(runId, id);
|
|
2545
|
+
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
2487
2546
|
return result.value;
|
|
2488
2547
|
}
|
|
2489
2548
|
catch (error) {
|
|
@@ -2495,7 +2554,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2495
2554
|
await eventPublisher.agentStates(run.store, run.metadata, before.agents, failed.agents);
|
|
2496
2555
|
}
|
|
2497
2556
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
2498
|
-
|
|
2557
|
+
setLiveActivity(runId, id);
|
|
2558
|
+
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
2499
2559
|
throw error;
|
|
2500
2560
|
}
|
|
2501
2561
|
}, 16, async (runId, ownership) => {
|
|
@@ -2522,7 +2582,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2522
2582
|
return { ...current, agents };
|
|
2523
2583
|
});
|
|
2524
2584
|
await eventPublisher.agentStates(run.store, run.metadata, previousAgents, runState.agents);
|
|
2525
|
-
run.update?.(workflowToolUpdate(runState));
|
|
2585
|
+
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
2526
2586
|
});
|
|
2527
2587
|
const stopWorkflowRun = async (runId) => {
|
|
2528
2588
|
const run = runs.get(runId);
|
|
@@ -2756,7 +2816,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2756
2816
|
if (!["completed", "failed", "stopped"].includes(run.lifecycle.state)) {
|
|
2757
2817
|
await run.lifecycle.terminal("failed", typed.code).catch(() => undefined);
|
|
2758
2818
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, error: { code: typed.code, message: typed.message } }));
|
|
2759
|
-
await eventPublisher.runFailed(run.store, run.metadata, typed,
|
|
2819
|
+
await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed");
|
|
2760
2820
|
run.update?.(workflowToolUpdate(persisted));
|
|
2761
2821
|
}
|
|
2762
2822
|
throw typed;
|
|
@@ -2826,7 +2886,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2826
2886
|
throw new WorkflowError("BUDGET_EXHAUSTED", "Budgeted work was attempted after hard exhaustion");
|
|
2827
2887
|
const resultPath = await run.store.saveResult(value);
|
|
2828
2888
|
await run.lifecycle.terminal("completed", "completed");
|
|
2829
|
-
await eventPublisher.runCompleted(run.store, run.metadata, resultPath
|
|
2889
|
+
await eventPublisher.runCompleted(run.store, run.metadata, resultPath);
|
|
2830
2890
|
return { value, resultPath };
|
|
2831
2891
|
}).catch(async (error) => {
|
|
2832
2892
|
await scheduler.flush();
|
|
@@ -2835,7 +2895,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2835
2895
|
await run.lifecycle.terminal(typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
|
|
2836
2896
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), error: { code: typed.code, message: typed.message } }));
|
|
2837
2897
|
const state = run.lifecycle.state === "stopped" || run.lifecycle.state === "interrupted" || run.lifecycle.state === "budget_exhausted" ? run.lifecycle.state : "failed";
|
|
2838
|
-
await eventPublisher.runFailed(run.store, run.metadata, typed,
|
|
2898
|
+
await eventPublisher.runFailed(run.store, run.metadata, typed, state);
|
|
2839
2899
|
run.update?.(workflowToolUpdate(persisted));
|
|
2840
2900
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
|
|
2841
2901
|
deliverFailure(pi, run.metadata.name, error);
|
|
@@ -2962,7 +3022,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2962
3022
|
deliver(pi, budgetDecisionDelivery(loaded.snapshot.metadata, decision));
|
|
2963
3023
|
scheduler.restoreRun(runId, loaded.snapshot.settings.concurrency, loaded.snapshot.identityVersion === LAUNCH_SNAPSHOT_IDENTITY_VERSION ? await store.loadOwnership() : [], () => runs.get(runId)?.budget.checkAgentLaunch());
|
|
2964
3024
|
}
|
|
2965
|
-
const resumeSelect = ctx.ui?.select;
|
|
3025
|
+
const resumeSelect = uiHostCapabilities(ctx.ui)?.select;
|
|
2966
3026
|
if (ctx.hasUI && resumeSelect) {
|
|
2967
3027
|
const interrupted = [...runs.values()].filter((r) => r.lifecycle.state === "interrupted");
|
|
2968
3028
|
if (interrupted.length > 0) {
|
|
@@ -3014,7 +3074,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3014
3074
|
const budget = validateBudget(params.budget);
|
|
3015
3075
|
const rootModel = { provider: ctx.model.provider, model: ctx.model.id, thinking: pi.getThinkingLevel() };
|
|
3016
3076
|
const rootModelName = `${rootModel.provider}/${rootModel.model}`;
|
|
3017
|
-
const modelRegistry = ctx.modelRegistry;
|
|
3077
|
+
const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
|
|
3018
3078
|
const knownModels = new Set((modelRegistry?.getAll?.() ?? modelRegistry?.getAvailable?.() ?? [ctx.model]).map((model) => `${model.provider}/${model.id}`));
|
|
3019
3079
|
knownModels.add(rootModelName);
|
|
3020
3080
|
const availableModels = knownModels;
|
|
@@ -3116,14 +3176,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3116
3176
|
}
|
|
3117
3177
|
}, log: logBridge(lifecycle, checked.metadata.name) }, store, runContext, variables, registry), runController.signal);
|
|
3118
3178
|
runs.get(runId).execution = execution;
|
|
3119
|
-
await eventPublisher.runStarted(store, checked.metadata
|
|
3179
|
+
await eventPublisher.runStarted(store, checked.metadata);
|
|
3120
3180
|
const finish = execution.result.then(async (value) => {
|
|
3121
3181
|
await scheduler.flush();
|
|
3122
3182
|
if (budgetRuntime.hardExhausted)
|
|
3123
3183
|
throw new WorkflowError("BUDGET_EXHAUSTED", "Budgeted work was attempted after hard exhaustion");
|
|
3124
3184
|
const resultPath = await store.saveResult(value);
|
|
3125
3185
|
await lifecycle.terminal("completed", "completed");
|
|
3126
|
-
await eventPublisher.runCompleted(store, checked.metadata, resultPath
|
|
3186
|
+
await eventPublisher.runCompleted(store, checked.metadata, resultPath);
|
|
3127
3187
|
return { value, resultPath };
|
|
3128
3188
|
}).catch(async (error) => {
|
|
3129
3189
|
await scheduler.flush();
|
|
@@ -3132,7 +3192,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3132
3192
|
await lifecycle.terminal(typed.code === "CANCELLED" ? "stopped" : typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
|
|
3133
3193
|
await persistRunState(store, checked.metadata, (current) => ({ ...current, ...budgetRuntime.snapshot(), error: { code: typed.code, message: typed.message } }));
|
|
3134
3194
|
const state = lifecycle.state === "stopped" || lifecycle.state === "interrupted" || lifecycle.state === "budget_exhausted" ? lifecycle.state : "failed";
|
|
3135
|
-
await eventPublisher.runFailed(store, checked.metadata, typed,
|
|
3195
|
+
await eventPublisher.runFailed(store, checked.metadata, typed, state);
|
|
3136
3196
|
throw typed;
|
|
3137
3197
|
});
|
|
3138
3198
|
runs.get(runId).completion = finish;
|
|
@@ -3198,7 +3258,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3198
3258
|
let stores = await loadStores();
|
|
3199
3259
|
const usage = "Usage: /workflow [doctor|model-aliases], or /workflow pause|resume|stop|approve|reject|delete <run-id> [checkpoint or proposal-id]. Use workflow_resume for budget patches.";
|
|
3200
3260
|
const setWorkflowStatus = (text) => {
|
|
3201
|
-
const setStatus = ctx.ui
|
|
3261
|
+
const setStatus = uiHostCapabilities(ctx.ui)?.setStatus;
|
|
3202
3262
|
setStatus?.call(ctx.ui, "workflow-stop", text);
|
|
3203
3263
|
};
|
|
3204
3264
|
const runAction = async (actionCommand, keepContext, status = setWorkflowStatus) => {
|
|
@@ -3254,7 +3314,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3254
3314
|
return keepContext ? "dashboard" : "done";
|
|
3255
3315
|
}
|
|
3256
3316
|
if (action === "adjust" && run?.lifecycle.state === "budget_exhausted") {
|
|
3257
|
-
const input = await ctx.ui
|
|
3317
|
+
const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
|
|
3258
3318
|
if (input === undefined)
|
|
3259
3319
|
return keepContext ? "dashboard" : "done";
|
|
3260
3320
|
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input));
|
|
@@ -3292,7 +3352,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3292
3352
|
};
|
|
3293
3353
|
const manageAliases = async () => {
|
|
3294
3354
|
const settingsPath = workflowSettingsPath();
|
|
3295
|
-
const modelRegistry = ctx.modelRegistry;
|
|
3355
|
+
const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
|
|
3296
3356
|
const available = () => [...new Set((modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`))].sort();
|
|
3297
3357
|
const selectTarget = async () => {
|
|
3298
3358
|
const models = available();
|
|
@@ -3458,7 +3518,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3458
3518
|
await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
3459
3519
|
let offset = 0;
|
|
3460
3520
|
let renderedLines = [];
|
|
3461
|
-
const viewport = () => Math.max(1, (
|
|
3521
|
+
const viewport = () => Math.max(1, tuiRows(tui) - 3);
|
|
3462
3522
|
const move = (delta) => { offset = Math.max(0, Math.min(Math.max(0, renderedLines.length - viewport()), offset + delta)); };
|
|
3463
3523
|
return {
|
|
3464
3524
|
render(width) {
|
|
@@ -3615,11 +3675,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3615
3675
|
let disposed = false;
|
|
3616
3676
|
let stopRequested = false;
|
|
3617
3677
|
let stopStatus;
|
|
3618
|
-
const terminalRows = () => Math.max(1, (tui
|
|
3678
|
+
const terminalRows = () => Math.max(1, tuiRows(tui));
|
|
3619
3679
|
const keyLabels = { up: "↑", down: "↓", pageUp: "pgup", pageDown: "pgdn", escape: "esc" };
|
|
3620
3680
|
const keyLabel = (binding, fallback) => {
|
|
3621
|
-
const
|
|
3622
|
-
const keys = getKeys?.call(keybindings, binding);
|
|
3681
|
+
const keys = keybindingKeys(keybindings, binding);
|
|
3623
3682
|
return keys?.length ? keys.map((key) => keyLabels[key] ?? key).join("/") : fallback;
|
|
3624
3683
|
};
|
|
3625
3684
|
const dashboardLayout = () => {
|
|
@@ -3733,7 +3792,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3733
3792
|
const highlighted = highlightCode(view.script, "javascript");
|
|
3734
3793
|
let offset = 0;
|
|
3735
3794
|
let renderedLines = [];
|
|
3736
|
-
const viewport = () => Math.max(1, (
|
|
3795
|
+
const viewport = () => Math.max(1, tuiRows(tui) - 3);
|
|
3737
3796
|
const move = (delta) => {
|
|
3738
3797
|
const maxOffset = Math.max(0, renderedLines.length - viewport());
|
|
3739
3798
|
offset = Math.max(0, Math.min(maxOffset, offset + delta));
|
|
@@ -3799,7 +3858,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3799
3858
|
let offset = 0;
|
|
3800
3859
|
let renderedLines = [];
|
|
3801
3860
|
const layout = () => {
|
|
3802
|
-
const rows = Math.max(1, (
|
|
3861
|
+
const rows = Math.max(1, tuiRows(tui));
|
|
3803
3862
|
const compactControls = rows < 4;
|
|
3804
3863
|
const titleRows = rows >= 5 ? 1 : 0;
|
|
3805
3864
|
const hintRows = rows >= 8 ? 1 : 0;
|
|
@@ -60,6 +60,8 @@ export declare class SessionLease {
|
|
|
60
60
|
export declare function acquireSessionLease(cwd: string, sessionId: string, home?: string): Promise<SessionLease>;
|
|
61
61
|
export declare function listRunIds(cwd: string, sessionId: string, home?: string): Promise<string[]>;
|
|
62
62
|
export declare function structuralPath(...names: string[]): string;
|
|
63
|
+
export declare function atomicWriteFile(path: string, content: string): Promise<void>;
|
|
64
|
+
export declare function atomicWriteFile(path: string, content: string, sync: true): void;
|
|
63
65
|
export declare class RunStore {
|
|
64
66
|
readonly cwd: string;
|
|
65
67
|
readonly sessionId: string;
|
package/dist/src/persistence.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
+
import { renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { access, link, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
5
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
5
6
|
import { homedir } from "node:os";
|
|
@@ -182,10 +183,32 @@ export function structuralPath(...names) {
|
|
|
182
183
|
throw new WorkflowError("INVALID_METADATA", "Structural paths require non-empty explicit names");
|
|
183
184
|
return names.map((name) => encodeURIComponent(name)).join("/");
|
|
184
185
|
}
|
|
185
|
-
|
|
186
|
+
export function atomicWriteFile(path, content, sync = false) {
|
|
186
187
|
const temporary = `${path}.${String(process.pid)}.${randomUUID()}.tmp`;
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
if (sync) {
|
|
189
|
+
try {
|
|
190
|
+
writeFileSync(temporary, content, { encoding: "utf8", mode: 0o600 });
|
|
191
|
+
renameSync(temporary, path);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
try {
|
|
195
|
+
rmSync(temporary, { force: true });
|
|
196
|
+
}
|
|
197
|
+
catch { /* Preserve the original write error. */ }
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
return writeFile(temporary, content, { encoding: "utf8", mode: 0o600 }).then(() => rename(temporary, path)).catch(async (error) => {
|
|
203
|
+
try {
|
|
204
|
+
await rm(temporary, { force: true });
|
|
205
|
+
}
|
|
206
|
+
catch { /* Preserve the original write error. */ }
|
|
207
|
+
throw error;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async function atomicJson(path, value) {
|
|
211
|
+
await atomicWriteFile(path, `${JSON.stringify(value)}\n`);
|
|
189
212
|
}
|
|
190
213
|
async function json(path) { return JSON.parse(await readFile(path, "utf8")); }
|
|
191
214
|
export class RunStore {
|
|
@@ -277,7 +300,7 @@ export class RunStore {
|
|
|
277
300
|
await write;
|
|
278
301
|
}
|
|
279
302
|
async appendEvent(event) {
|
|
280
|
-
await this.updateState((run) => ({ ...run, events: [...(run.events ?? []), ...(run.events?.some((current) => current.message === event.message) ? [] : [event])] }));
|
|
303
|
+
await this.updateState((run) => ({ ...run, events: [...(run.events ?? []), ...(run.events?.some((current) => current.type === event.type && current.message === event.message) ? [] : [event])] }));
|
|
281
304
|
}
|
|
282
305
|
async saveOwnership(nodes) {
|
|
283
306
|
await atomicJson(join(this.directory, "ownership.json"), nodes);
|