@acpus/runtime 0.8.0 → 0.9.0-alpha.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/dist/.tsbuildinfo +1 -0
- package/dist/admission/input.d.ts +5 -0
- package/dist/admission/input.d.ts.map +1 -0
- package/dist/admission/input.js +17 -0
- package/dist/admission/input.js.map +1 -0
- package/dist/artifacts/rewrite.d.ts +7 -0
- package/dist/artifacts/rewrite.d.ts.map +1 -0
- package/dist/artifacts/rewrite.js +28 -0
- package/dist/artifacts/rewrite.js.map +1 -0
- package/dist/control/agent-overrides.d.ts +4 -0
- package/dist/control/agent-overrides.d.ts.map +1 -0
- package/dist/control/agent-overrides.js +40 -0
- package/dist/control/agent-overrides.js.map +1 -0
- package/dist/daemon/loop.d.ts +16 -0
- package/dist/daemon/loop.d.ts.map +1 -0
- package/dist/daemon/loop.js +191 -0
- package/dist/daemon/loop.js.map +1 -0
- package/dist/daemon/sessions.d.ts +23 -0
- package/dist/daemon/sessions.d.ts.map +1 -0
- package/dist/daemon/sessions.js +190 -0
- package/dist/daemon/sessions.js.map +1 -0
- package/dist/daemon/socket.d.ts +82 -0
- package/dist/daemon/socket.d.ts.map +1 -0
- package/dist/daemon/socket.js +341 -0
- package/dist/daemon/socket.js.map +1 -0
- package/dist/daemon/tick.d.ts +9 -0
- package/dist/daemon/tick.d.ts.map +1 -0
- package/dist/daemon/tick.js +18 -0
- package/dist/daemon/tick.js.map +1 -0
- package/dist/evaluation/admissible.d.ts +2 -0
- package/dist/evaluation/admissible.d.ts.map +1 -0
- package/dist/evaluation/admissible.js +50 -0
- package/dist/evaluation/admissible.js.map +1 -0
- package/dist/evaluation/evaluator.d.ts +14 -0
- package/dist/evaluation/evaluator.d.ts.map +1 -0
- package/dist/evaluation/evaluator.js +34 -0
- package/dist/evaluation/evaluator.js.map +1 -0
- package/dist/evaluation/loop-limit.d.ts +4 -0
- package/dist/evaluation/loop-limit.d.ts.map +1 -0
- package/dist/evaluation/loop-limit.js +9 -0
- package/dist/evaluation/loop-limit.js.map +1 -0
- package/dist/evaluation/schema.d.ts +4 -0
- package/dist/evaluation/schema.d.ts.map +1 -0
- package/dist/evaluation/schema.js +92 -0
- package/dist/evaluation/schema.js.map +1 -0
- package/dist/execution/advance.d.ts +18 -0
- package/dist/execution/advance.d.ts.map +1 -0
- package/dist/execution/advance.js +57 -0
- package/dist/execution/advance.js.map +1 -0
- package/dist/execution/agent-node.d.ts +23 -0
- package/dist/execution/agent-node.d.ts.map +1 -0
- package/dist/execution/agent-node.js +512 -0
- package/dist/execution/agent-node.js.map +1 -0
- package/dist/execution/duration.d.ts +2 -0
- package/dist/execution/duration.d.ts.map +1 -0
- package/dist/execution/duration.js +15 -0
- package/dist/execution/duration.js.map +1 -0
- package/dist/execution/ir.d.ts +3 -0
- package/dist/execution/ir.d.ts.map +1 -0
- package/dist/execution/ir.js +46 -0
- package/dist/execution/ir.js.map +1 -0
- package/dist/execution/scheduler.d.ts +59 -0
- package/dist/execution/scheduler.d.ts.map +1 -0
- package/dist/execution/scheduler.js +306 -0
- package/dist/execution/scheduler.js.map +1 -0
- package/dist/execution/task-executor.d.ts +14 -0
- package/dist/execution/task-executor.d.ts.map +1 -0
- package/dist/execution/task-executor.js +186 -0
- package/dist/execution/task-executor.js.map +1 -0
- package/dist/hooks/config.d.ts +32 -0
- package/dist/hooks/config.d.ts.map +1 -0
- package/dist/hooks/config.js +112 -0
- package/dist/hooks/config.js.map +1 -0
- package/dist/hooks/context.d.ts +50 -0
- package/dist/hooks/context.d.ts.map +1 -0
- package/dist/hooks/context.js +116 -0
- package/dist/hooks/context.js.map +1 -0
- package/dist/hooks/events.d.ts +22 -0
- package/dist/hooks/events.d.ts.map +1 -0
- package/dist/hooks/events.js +41 -0
- package/dist/hooks/events.js.map +1 -0
- package/dist/hooks/journal.d.ts +21 -19
- package/dist/hooks/journal.d.ts.map +1 -1
- package/dist/hooks/journal.js +1 -48
- package/dist/hooks/journal.js.map +1 -1
- package/dist/hooks/loader.d.ts +33 -39
- package/dist/hooks/loader.d.ts.map +1 -1
- package/dist/hooks/loader.js +79 -95
- package/dist/hooks/loader.js.map +1 -1
- package/dist/hooks/runner.d.ts +14 -41
- package/dist/hooks/runner.d.ts.map +1 -1
- package/dist/hooks/runner.js +177 -145
- package/dist/hooks/runner.js.map +1 -1
- package/dist/index.d.ts +13 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -33
- package/dist/index.js.map +1 -1
- package/dist/runs/advance-runtime.d.ts +50 -0
- package/dist/runs/advance-runtime.d.ts.map +1 -0
- package/dist/runs/advance-runtime.js +82 -0
- package/dist/runs/advance-runtime.js.map +1 -0
- package/dist/runs/use-cases.d.ts +111 -0
- package/dist/runs/use-cases.d.ts.map +1 -0
- package/dist/runs/use-cases.js +539 -0
- package/dist/runs/use-cases.js.map +1 -0
- package/dist/scheduler/advance.d.ts +65 -0
- package/dist/scheduler/advance.d.ts.map +1 -0
- package/dist/scheduler/advance.js +435 -0
- package/dist/scheduler/advance.js.map +1 -0
- package/dist/scheduler/control.d.ts +49 -0
- package/dist/scheduler/control.d.ts.map +1 -0
- package/dist/scheduler/control.js +212 -0
- package/dist/scheduler/control.js.map +1 -0
- package/dist/scheduler/event-codec.d.ts +4 -0
- package/dist/scheduler/event-codec.d.ts.map +1 -0
- package/dist/scheduler/event-codec.js +57 -0
- package/dist/scheduler/event-codec.js.map +1 -0
- package/dist/scheduler/events.d.ts +193 -0
- package/dist/scheduler/events.d.ts.map +1 -0
- package/dist/scheduler/events.js +13 -0
- package/dist/scheduler/events.js.map +1 -0
- package/dist/scheduler/fork-seed.d.ts +38 -0
- package/dist/scheduler/fork-seed.d.ts.map +1 -0
- package/dist/scheduler/fork-seed.js +590 -0
- package/dist/scheduler/fork-seed.js.map +1 -0
- package/dist/scheduler/identity.d.ts +8 -0
- package/dist/scheduler/identity.d.ts.map +1 -0
- package/dist/scheduler/identity.js +49 -0
- package/dist/scheduler/identity.js.map +1 -0
- package/dist/scheduler/ir-walk.d.ts +3 -0
- package/dist/scheduler/ir-walk.d.ts.map +1 -0
- package/dist/scheduler/ir-walk.js +25 -0
- package/dist/scheduler/ir-walk.js.map +1 -0
- package/dist/scheduler/limiter.d.ts +7 -0
- package/dist/scheduler/limiter.d.ts.map +1 -0
- package/dist/scheduler/limiter.js +10 -0
- package/dist/scheduler/limiter.js.map +1 -0
- package/dist/scheduler/materialize.d.ts +7 -0
- package/dist/scheduler/materialize.d.ts.map +1 -0
- package/dist/scheduler/materialize.js +756 -0
- package/dist/scheduler/materialize.js.map +1 -0
- package/dist/scheduler/membership.d.ts +12 -0
- package/dist/scheduler/membership.d.ts.map +1 -0
- package/dist/scheduler/membership.js +74 -0
- package/dist/scheduler/membership.js.map +1 -0
- package/dist/scheduler/node-executor.d.ts +15 -0
- package/dist/scheduler/node-executor.d.ts.map +1 -0
- package/dist/scheduler/node-executor.js +69 -0
- package/dist/scheduler/node-executor.js.map +1 -0
- package/dist/scheduler/runtime-runner.d.ts +44 -0
- package/dist/scheduler/runtime-runner.d.ts.map +1 -0
- package/dist/scheduler/runtime-runner.js +155 -0
- package/dist/scheduler/runtime-runner.js.map +1 -0
- package/dist/scheduler/scope.d.ts +6 -0
- package/dist/scheduler/scope.d.ts.map +1 -0
- package/dist/scheduler/scope.js +56 -0
- package/dist/scheduler/scope.js.map +1 -0
- package/dist/scheduler/store-port.d.ts +211 -0
- package/dist/scheduler/store-port.d.ts.map +1 -0
- package/dist/scheduler/store-port.js +28 -0
- package/dist/scheduler/store-port.js.map +1 -0
- package/dist/scheduler/transitions.d.ts +89 -0
- package/dist/scheduler/transitions.d.ts.map +1 -0
- package/dist/scheduler/transitions.js +967 -0
- package/dist/scheduler/transitions.js.map +1 -0
- package/dist/scheduler/types.d.ts +146 -0
- package/dist/scheduler/types.d.ts.map +1 -0
- package/dist/{executors → scheduler}/types.js.map +1 -1
- package/dist/store/store.d.ts +390 -0
- package/dist/store/store.d.ts.map +1 -0
- package/dist/store/store.js +3053 -0
- package/dist/store/store.js.map +1 -0
- package/dist/visualization/overlay.d.ts +86 -0
- package/dist/visualization/overlay.d.ts.map +1 -0
- package/dist/visualization/overlay.js +161 -0
- package/dist/visualization/overlay.js.map +1 -0
- package/package.json +19 -37
- package/README.md +0 -7
- package/dist/agent-telemetry.d.ts +0 -56
- package/dist/agent-telemetry.d.ts.map +0 -1
- package/dist/agent-telemetry.js +0 -304
- package/dist/agent-telemetry.js.map +0 -1
- package/dist/artifacts.d.ts +0 -45
- package/dist/artifacts.d.ts.map +0 -1
- package/dist/artifacts.js +0 -163
- package/dist/artifacts.js.map +0 -1
- package/dist/attempt-artifacts.d.ts +0 -35
- package/dist/attempt-artifacts.d.ts.map +0 -1
- package/dist/attempt-artifacts.js +0 -90
- package/dist/attempt-artifacts.js.map +0 -1
- package/dist/client.d.ts +0 -82
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -200
- package/dist/client.js.map +0 -1
- package/dist/evaluator.d.ts +0 -36
- package/dist/evaluator.d.ts.map +0 -1
- package/dist/evaluator.js +0 -101
- package/dist/evaluator.js.map +0 -1
- package/dist/executors/agent.d.ts +0 -103
- package/dist/executors/agent.d.ts.map +0 -1
- package/dist/executors/agent.js +0 -516
- package/dist/executors/agent.js.map +0 -1
- package/dist/executors/mock-program.d.ts +0 -26
- package/dist/executors/mock-program.d.ts.map +0 -1
- package/dist/executors/mock-program.js +0 -100
- package/dist/executors/mock-program.js.map +0 -1
- package/dist/executors/output-preview.d.ts +0 -2
- package/dist/executors/output-preview.d.ts.map +0 -1
- package/dist/executors/output-preview.js +0 -20
- package/dist/executors/output-preview.js.map +0 -1
- package/dist/executors/program.d.ts +0 -30
- package/dist/executors/program.d.ts.map +0 -1
- package/dist/executors/program.js +0 -230
- package/dist/executors/program.js.map +0 -1
- package/dist/executors/types.d.ts +0 -32
- package/dist/executors/types.d.ts.map +0 -1
- package/dist/fork.d.ts +0 -74
- package/dist/fork.d.ts.map +0 -1
- package/dist/fork.js +0 -278
- package/dist/fork.js.map +0 -1
- package/dist/hooks/payload.d.ts +0 -17
- package/dist/hooks/payload.d.ts.map +0 -1
- package/dist/hooks/payload.js +0 -45
- package/dist/hooks/payload.js.map +0 -1
- package/dist/interpreter.d.ts +0 -168
- package/dist/interpreter.d.ts.map +0 -1
- package/dist/interpreter.js +0 -1747
- package/dist/interpreter.js.map +0 -1
- package/dist/keys.d.ts +0 -77
- package/dist/keys.d.ts.map +0 -1
- package/dist/keys.js +0 -382
- package/dist/keys.js.map +0 -1
- package/dist/run-control.d.ts +0 -45
- package/dist/run-control.d.ts.map +0 -1
- package/dist/run-control.js +0 -255
- package/dist/run-control.js.map +0 -1
- package/dist/run-id.d.ts +0 -3
- package/dist/run-id.d.ts.map +0 -1
- package/dist/run-id.js +0 -10
- package/dist/run-id.js.map +0 -1
- package/dist/state-machine.d.ts +0 -31
- package/dist/state-machine.d.ts.map +0 -1
- package/dist/state-machine.js +0 -102
- package/dist/state-machine.js.map +0 -1
- package/dist/store.d.ts +0 -92
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -420
- package/dist/store.js.map +0 -1
- package/dist/supervisor-app.d.ts +0 -27
- package/dist/supervisor-app.d.ts.map +0 -1
- package/dist/supervisor-app.js +0 -694
- package/dist/supervisor-app.js.map +0 -1
- package/dist/supervisor-discovery.d.ts +0 -18
- package/dist/supervisor-discovery.d.ts.map +0 -1
- package/dist/supervisor-discovery.js +0 -180
- package/dist/supervisor-discovery.js.map +0 -1
- package/dist/supervisor-entry.d.ts +0 -11
- package/dist/supervisor-entry.d.ts.map +0 -1
- package/dist/supervisor-entry.js +0 -50
- package/dist/supervisor-entry.js.map +0 -1
- package/dist/supervisor-lock.d.ts +0 -26
- package/dist/supervisor-lock.d.ts.map +0 -1
- package/dist/supervisor-lock.js +0 -49
- package/dist/supervisor-lock.js.map +0 -1
- package/dist/supervisor-runner.d.ts +0 -11
- package/dist/supervisor-runner.d.ts.map +0 -1
- package/dist/supervisor-runner.js +0 -128
- package/dist/supervisor-runner.js.map +0 -1
- package/dist/types.d.ts +0 -375
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -7
- package/dist/types.js.map +0 -1
- package/dist/validate-input.d.ts +0 -27
- package/dist/validate-input.d.ts.map +0 -1
- package/dist/validate-input.js +0 -56
- package/dist/validate-input.js.map +0 -1
- package/dist/validate-signal.d.ts +0 -14
- package/dist/validate-signal.d.ts.map +0 -1
- package/dist/validate-signal.js +0 -31
- package/dist/validate-signal.js.map +0 -1
- package/dist/workflow-context.d.ts +0 -4
- package/dist/workflow-context.d.ts.map +0 -1
- package/dist/workflow-context.js +0 -11
- package/dist/workflow-context.js.map +0 -1
- package/dist/workflow-outputs.d.ts +0 -9
- package/dist/workflow-outputs.d.ts.map +0 -1
- package/dist/workflow-outputs.js +0 -60
- package/dist/workflow-outputs.js.map +0 -1
- /package/dist/{executors → scheduler}/types.js +0 -0
package/dist/supervisor-app.js
DELETED
|
@@ -1,694 +0,0 @@
|
|
|
1
|
-
import { Hono } from "hono";
|
|
2
|
-
import { WorkflowInterpreter, generateRunId } from "./interpreter.js";
|
|
3
|
-
import { InputValidationFailure } from "./validate-input.js";
|
|
4
|
-
import { SignalPayloadValidationError } from "./validate-signal.js";
|
|
5
|
-
import { applyAgentOverrides, compileWorkflow, emptyAgentOverrideResult, optionalSubmissionMetadata, parseWorkflowSpecForOverrides, serializeWorkflowSpecForOverrides, workflowSourceResolver } from "@acpus/core";
|
|
6
|
-
import { resolve } from "node:path";
|
|
7
|
-
import { isRunTerminal } from "./types.js";
|
|
8
|
-
import { ForkError, materializeForkedRun, planForkedRun } from "./fork.js";
|
|
9
|
-
import { ArtifactReferences } from "./artifacts.js";
|
|
10
|
-
import { HookConfigLoader } from "./hooks/loader.js";
|
|
11
|
-
import { HookRunner } from "./hooks/runner.js";
|
|
12
|
-
import { isUnsafeRunId } from "./run-id.js";
|
|
13
|
-
/**
|
|
14
|
-
* Creates a Hono HTTP app for the acpus Run Supervisor.
|
|
15
|
-
*
|
|
16
|
-
* Note: Node keys contain "/" characters (e.g. "workflow/step-a"), which are
|
|
17
|
-
* incompatible with URL path segments. Node-addressed routes accept the nodeKey
|
|
18
|
-
* as a query parameter instead.
|
|
19
|
-
*
|
|
20
|
-
* Returns the Hono app plus control handles for the runner to query
|
|
21
|
-
* idle-shutdown state and inject health-overrides after listen.
|
|
22
|
-
*/
|
|
23
|
-
export function createSupervisorApp(config, store, agentExecutor, programExecutor) {
|
|
24
|
-
const app = new Hono();
|
|
25
|
-
// Active interpreter instances
|
|
26
|
-
const interpreters = new Map();
|
|
27
|
-
// In-flight runToCompletion promises, keyed by runId, to prevent concurrent
|
|
28
|
-
// execution races during Run-level resume or retry.
|
|
29
|
-
const inFlightRuns = new Map();
|
|
30
|
-
// Mutex guard: runIds currently being resumed or retried. Prevents concurrent
|
|
31
|
-
// resume or retry requests from starting duplicate runToCompletion executions.
|
|
32
|
-
const resumingRunIds = new Set();
|
|
33
|
-
// Lease tracking for idle shutdown
|
|
34
|
-
const clientLeases = new Map();
|
|
35
|
-
let lastActiveAt = Date.now();
|
|
36
|
-
// Health overrides set by the runner after the server binds to a port
|
|
37
|
-
let healthStartedAt = "";
|
|
38
|
-
let healthEndpoint = "";
|
|
39
|
-
const workspaceRoot = resolve(config.workspace ?? process.cwd());
|
|
40
|
-
const sourceResolver = workflowSourceResolver(workspaceRoot);
|
|
41
|
-
const hookConfigLoader = new HookConfigLoader(workspaceRoot);
|
|
42
|
-
/** Refresh lease on every request with client headers. */
|
|
43
|
-
function refreshLease(clientId, clientKind) {
|
|
44
|
-
if (!clientId || !clientKind)
|
|
45
|
-
return;
|
|
46
|
-
clientLeases.set(clientId, { kind: clientKind, lastSeen: Date.now() });
|
|
47
|
-
lastActiveAt = Date.now();
|
|
48
|
-
}
|
|
49
|
-
/** Expire stale leases (TTL 2s). */
|
|
50
|
-
function expireLeases() {
|
|
51
|
-
const now = Date.now();
|
|
52
|
-
for (const [id, lease] of clientLeases) {
|
|
53
|
-
if (now - lease.lastSeen > 2000) {
|
|
54
|
-
clientLeases.delete(id);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
/** Count of non-expired leases. */
|
|
59
|
-
function activeClientCount() {
|
|
60
|
-
expireLeases();
|
|
61
|
-
return clientLeases.size;
|
|
62
|
-
}
|
|
63
|
-
/** Count of Runs with status === "running". */
|
|
64
|
-
function runningCount() {
|
|
65
|
-
let count = 0;
|
|
66
|
-
for (const runId of store.listRunIds()) {
|
|
67
|
-
const meta = store.readRunMeta(runId);
|
|
68
|
-
if (meta?.status === "running")
|
|
69
|
-
count++;
|
|
70
|
-
}
|
|
71
|
-
return count;
|
|
72
|
-
}
|
|
73
|
-
/** Get the most recent active timestamp (for idle shutdown). */
|
|
74
|
-
function getLastActiveAt() {
|
|
75
|
-
expireLeases();
|
|
76
|
-
let maxLease = 0;
|
|
77
|
-
for (const lease of clientLeases.values()) {
|
|
78
|
-
if (lease.lastSeen > maxLease)
|
|
79
|
-
maxLease = lease.lastSeen;
|
|
80
|
-
}
|
|
81
|
-
return Math.max(lastActiveAt, maxLease);
|
|
82
|
-
}
|
|
83
|
-
/** Create an interpreter bound to this supervisor's store + executors. */
|
|
84
|
-
function newInterpreter(runId) {
|
|
85
|
-
const nowTimestamp = runId ? store.readRunMeta(runId)?.createdAt : undefined;
|
|
86
|
-
// Rebuild the frozen hook runner from the Run's persisted snapshot so retry,
|
|
87
|
-
// resume, and fork executions all reuse the same frozen configuration.
|
|
88
|
-
const snapshot = runId ? store.readHookConfig(runId) : undefined;
|
|
89
|
-
const hookRunner = snapshot ? new HookRunner(snapshot.mergedConfig) : undefined;
|
|
90
|
-
return new WorkflowInterpreter(store, agentExecutor, programExecutor, { nowTimestamp, hookRunner });
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Freeze the merged hook configuration for a newly-created Run. Writes
|
|
94
|
-
* hook-config.json and records the hash on Run metadata when any hooks apply;
|
|
95
|
-
* otherwise leaves no file and no hash so the Run incurs zero hook overhead.
|
|
96
|
-
*/
|
|
97
|
-
function freezeHookConfig(runId) {
|
|
98
|
-
const snapshot = hookConfigLoader.freeze();
|
|
99
|
-
if (!snapshot)
|
|
100
|
-
return;
|
|
101
|
-
store.writeHookConfig(runId, snapshot);
|
|
102
|
-
const meta = store.readRunMeta(runId);
|
|
103
|
-
if (meta) {
|
|
104
|
-
meta.hookConfigHash = snapshot.hash;
|
|
105
|
-
store.writeRunMeta(runId, meta);
|
|
106
|
-
return meta;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Interpreter for a forward-progress control action (resume or retry). When the
|
|
111
|
-
* supervisor has restarted (in-memory map empty) but the Run persists, lazily
|
|
112
|
-
* recover one and reset stale `running` nodes so the action can proceed.
|
|
113
|
-
* Returns `undefined` only when the Run does not exist on disk.
|
|
114
|
-
*/
|
|
115
|
-
function getOrRecoverInterpreter(runId) {
|
|
116
|
-
const live = interpreters.get(runId);
|
|
117
|
-
if (live)
|
|
118
|
-
return live;
|
|
119
|
-
if (!store.hasRun(runId))
|
|
120
|
-
return undefined;
|
|
121
|
-
const recovered = newInterpreter(runId);
|
|
122
|
-
recovered.recoverStaleNodes(runId);
|
|
123
|
-
interpreters.set(runId, recovered);
|
|
124
|
-
return recovered;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Read-only interpreter used by replay. Lazily creates one for cross-process
|
|
128
|
-
* access but NEVER mutates persisted state (no crash recovery). Returns
|
|
129
|
-
* `undefined` only when the Run does not exist on disk.
|
|
130
|
-
*/
|
|
131
|
-
function getReadOnlyInterpreter(runId) {
|
|
132
|
-
const live = interpreters.get(runId);
|
|
133
|
-
if (live)
|
|
134
|
-
return live;
|
|
135
|
-
if (!store.hasRun(runId))
|
|
136
|
-
return undefined;
|
|
137
|
-
return newInterpreter(runId);
|
|
138
|
-
}
|
|
139
|
-
/** Prune interpreters for terminal Runs from the in-memory map. */
|
|
140
|
-
function pruneTerminalInterpreters() {
|
|
141
|
-
for (const [runId] of interpreters) {
|
|
142
|
-
const meta = store.readRunMeta(runId);
|
|
143
|
-
if (meta && isRunTerminal(meta.status)) {
|
|
144
|
-
interpreters.delete(runId);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// ─── Lease refresh middleware (runs before route handlers) ────────
|
|
149
|
-
app.use("*", async (c, next) => {
|
|
150
|
-
const clientId = c.req.header("x-acpus-client-id");
|
|
151
|
-
const clientKind = c.req.header("x-acpus-client-kind");
|
|
152
|
-
refreshLease(clientId, clientKind);
|
|
153
|
-
await next();
|
|
154
|
-
});
|
|
155
|
-
// ─── runId path-traversal guard ──────────────────────────────────
|
|
156
|
-
app.use("/runs/:runId/*", async (c, next) => {
|
|
157
|
-
const runId = c.req.param("runId");
|
|
158
|
-
if (isUnsafeRunId(runId)) {
|
|
159
|
-
return c.json({ error: "Invalid runId format" }, 400);
|
|
160
|
-
}
|
|
161
|
-
await next();
|
|
162
|
-
});
|
|
163
|
-
/** Fire-and-forget runToCompletion with proper tracking and cleanup. */
|
|
164
|
-
function startRunExecution(interpreter, ir, input, runId) {
|
|
165
|
-
const promise = interpreter.runToCompletion(ir, { input }, runId)
|
|
166
|
-
.catch(() => {
|
|
167
|
-
// runToCompletion always resolves with the RunState (never rejects
|
|
168
|
-
// with undefined), but the catch silences any unexpected throw.
|
|
169
|
-
const meta = store.readRunMeta(runId);
|
|
170
|
-
return meta ?? {
|
|
171
|
-
runId,
|
|
172
|
-
workflowName: "",
|
|
173
|
-
status: "failed",
|
|
174
|
-
irDigest: "",
|
|
175
|
-
inputDigest: "",
|
|
176
|
-
createdAt: "",
|
|
177
|
-
updatedAt: "",
|
|
178
|
-
runAttempt: 1
|
|
179
|
-
};
|
|
180
|
-
})
|
|
181
|
-
.finally(() => {
|
|
182
|
-
inFlightRuns.delete(runId);
|
|
183
|
-
pruneTerminalInterpreters();
|
|
184
|
-
if (runningCount() === 0)
|
|
185
|
-
lastActiveAt = Date.now();
|
|
186
|
-
});
|
|
187
|
-
inFlightRuns.set(runId, promise);
|
|
188
|
-
}
|
|
189
|
-
function startPersistedRunExecution(interpreter, runId) {
|
|
190
|
-
const ir = store.readIr(runId);
|
|
191
|
-
const input = store.readInput(runId);
|
|
192
|
-
if (ir && input) {
|
|
193
|
-
startRunExecution(interpreter, ir, input, runId);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Node-level retry may run for a long time. Wait one event-loop turn so
|
|
198
|
-
* synchronous validation errors surface as 409s, then detach once execution
|
|
199
|
-
* has started so interactive clients are not kept alive by the request.
|
|
200
|
-
*/
|
|
201
|
-
function settleImmediate(promise) {
|
|
202
|
-
return Promise.race([
|
|
203
|
-
promise.then((value) => ({ kind: "settled", value })).catch((error) => ({ kind: "rejected", error })),
|
|
204
|
-
new Promise((resolve) => setImmediate(() => resolve({ kind: "pending" })))
|
|
205
|
-
]);
|
|
206
|
-
}
|
|
207
|
-
function consumeDetachedControl(promise) {
|
|
208
|
-
void promise.catch(() => undefined);
|
|
209
|
-
}
|
|
210
|
-
// ─── Health ──────────────────────────────────────────────────────
|
|
211
|
-
app.get("/health", (c) => {
|
|
212
|
-
const health = {
|
|
213
|
-
ok: true,
|
|
214
|
-
schemaVersion: 1,
|
|
215
|
-
workspace: workspaceRoot,
|
|
216
|
-
pid: process.pid,
|
|
217
|
-
endpoint: healthEndpoint,
|
|
218
|
-
startedAt: healthStartedAt,
|
|
219
|
-
version: "0.1.0",
|
|
220
|
-
runningCount: runningCount(),
|
|
221
|
-
activeClients: activeClientCount()
|
|
222
|
-
};
|
|
223
|
-
return c.json(health);
|
|
224
|
-
});
|
|
225
|
-
// ─── Runs ────────────────────────────────────────────────────────
|
|
226
|
-
app.post("/runs", async (c) => {
|
|
227
|
-
const body = await c.req.json();
|
|
228
|
-
if (!body.spec) {
|
|
229
|
-
return c.json({ error: "spec is required" }, 400);
|
|
230
|
-
}
|
|
231
|
-
let sourcePath;
|
|
232
|
-
if (body.sourcePath) {
|
|
233
|
-
try {
|
|
234
|
-
sourcePath = sourceResolver.validateSourcePath(body.sourcePath);
|
|
235
|
-
}
|
|
236
|
-
catch (error) {
|
|
237
|
-
return c.json({ error: errorMessage(error) }, 400);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
let effectiveSource = body.spec;
|
|
241
|
-
let submitMetadata = emptyAgentOverrideResult();
|
|
242
|
-
try {
|
|
243
|
-
if (body.agentOverrides !== undefined) {
|
|
244
|
-
const parsedSpec = parseWorkflowSpecForOverrides(body.spec);
|
|
245
|
-
submitMetadata = applyAgentOverrides(parsedSpec, body.agentOverrides);
|
|
246
|
-
effectiveSource = serializeWorkflowSpecForOverrides(submitMetadata.effectiveSpec);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
catch (error) {
|
|
250
|
-
return c.json({ error: errorMessage(error) }, 400);
|
|
251
|
-
}
|
|
252
|
-
const result = compileWorkflow(effectiveSource, {
|
|
253
|
-
sourcePath,
|
|
254
|
-
includeResolver: sourceResolver.createIncludeResolver(sourcePath)
|
|
255
|
-
});
|
|
256
|
-
if (!result.ok || !result.ir) {
|
|
257
|
-
return c.json({ error: "Compilation failed", diagnostics: result.diagnostics }, 400);
|
|
258
|
-
}
|
|
259
|
-
const initInterpreter = newInterpreter();
|
|
260
|
-
let runState;
|
|
261
|
-
try {
|
|
262
|
-
runState = initInterpreter.initRun(result.ir, {
|
|
263
|
-
input: body.input ?? {},
|
|
264
|
-
workflowRef: body.workflowRef,
|
|
265
|
-
workflowSourcePath: sourcePath,
|
|
266
|
-
...optionalSubmissionMetadata(submitMetadata),
|
|
267
|
-
skipHooks: Boolean(body.skipHooks)
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
catch (error) {
|
|
271
|
-
if (error instanceof InputValidationFailure) {
|
|
272
|
-
return c.json({ error: "Input validation failed", validationErrors: error.errors }, 400);
|
|
273
|
-
}
|
|
274
|
-
throw error;
|
|
275
|
-
}
|
|
276
|
-
// Freeze hook configuration once at Run creation unless this submission
|
|
277
|
-
// explicitly disabled hooks. Retry/resume reuse the Run's frozen state.
|
|
278
|
-
if (!body.skipHooks)
|
|
279
|
-
runState = freezeHookConfig(runState.runId) ?? runState;
|
|
280
|
-
const interpreter = newInterpreter(runState.runId);
|
|
281
|
-
interpreters.set(runState.runId, interpreter);
|
|
282
|
-
lastActiveAt = Date.now();
|
|
283
|
-
// initRun has already validated and filled defaults; read the validated
|
|
284
|
-
// input from the store rather than re-using the raw body.input.
|
|
285
|
-
const validatedInput = store.readInput(runState.runId) ?? body.input ?? {};
|
|
286
|
-
startRunExecution(interpreter, result.ir, validatedInput, runState.runId);
|
|
287
|
-
return c.json(runState, 201);
|
|
288
|
-
});
|
|
289
|
-
app.get("/runs", (c) => {
|
|
290
|
-
const runIds = store.listRunIds();
|
|
291
|
-
const summaries = [];
|
|
292
|
-
for (const id of runIds) {
|
|
293
|
-
const meta = store.readRunMeta(id);
|
|
294
|
-
if (meta) {
|
|
295
|
-
summaries.push({
|
|
296
|
-
runId: meta.runId,
|
|
297
|
-
workflowName: meta.workflowName,
|
|
298
|
-
workflowRef: meta.workflowRef,
|
|
299
|
-
workflowSourcePath: meta.workflowSourcePath,
|
|
300
|
-
status: meta.status,
|
|
301
|
-
createdAt: meta.createdAt,
|
|
302
|
-
updatedAt: meta.updatedAt,
|
|
303
|
-
lineage: meta.lineage
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
// Skip corrupt Run metadata deterministically (don't crash)
|
|
307
|
-
}
|
|
308
|
-
// Sort by updatedAt descending, limit to most recent 50
|
|
309
|
-
summaries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
310
|
-
return c.json(summaries.slice(0, 50));
|
|
311
|
-
});
|
|
312
|
-
app.get("/runs/:runId", (c) => {
|
|
313
|
-
const runId = c.req.param("runId");
|
|
314
|
-
const meta = store.readRunMeta(runId);
|
|
315
|
-
if (!meta) {
|
|
316
|
-
return c.json({ error: "Run not found" }, 404);
|
|
317
|
-
}
|
|
318
|
-
const nodes = store.listNodeStates(runId);
|
|
319
|
-
return c.json({ ...meta, nodes });
|
|
320
|
-
});
|
|
321
|
-
app.post("/runs/clean", async (c) => {
|
|
322
|
-
pruneTerminalInterpreters();
|
|
323
|
-
const body = await c.req.json().catch(() => ({}));
|
|
324
|
-
const result = store.cleanTerminalRuns({ dryRun: Boolean(body.dryRun) });
|
|
325
|
-
if (!body.dryRun) {
|
|
326
|
-
for (const item of result.deleted) {
|
|
327
|
-
interpreters.delete(item.runId);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
lastActiveAt = Date.now();
|
|
331
|
-
return c.json(result);
|
|
332
|
-
});
|
|
333
|
-
// ─── Nodes ───────────────────────────────────────────────────────
|
|
334
|
-
app.get("/runs/:runId/nodes", (c) => {
|
|
335
|
-
const runId = c.req.param("runId");
|
|
336
|
-
if (!store.hasRun(runId)) {
|
|
337
|
-
return c.json({ error: "Run not found" }, 404);
|
|
338
|
-
}
|
|
339
|
-
return c.json(store.listNodeStates(runId));
|
|
340
|
-
});
|
|
341
|
-
app.get("/runs/:runId/ir", (c) => {
|
|
342
|
-
const runId = c.req.param("runId");
|
|
343
|
-
if (!store.hasRun(runId)) {
|
|
344
|
-
return c.json({ error: "Run not found" }, 404);
|
|
345
|
-
}
|
|
346
|
-
const ir = store.readIr(runId);
|
|
347
|
-
if (!ir) {
|
|
348
|
-
return c.json({ error: "IR not found" }, 404);
|
|
349
|
-
}
|
|
350
|
-
return c.json(ir);
|
|
351
|
-
});
|
|
352
|
-
app.get("/runs/:runId/input", (c) => {
|
|
353
|
-
const runId = c.req.param("runId");
|
|
354
|
-
if (!store.hasRun(runId)) {
|
|
355
|
-
return c.json({ error: "Run not found" }, 404);
|
|
356
|
-
}
|
|
357
|
-
const input = store.readInput(runId);
|
|
358
|
-
if (!input) {
|
|
359
|
-
return c.json({ error: "Input not found" }, 404);
|
|
360
|
-
}
|
|
361
|
-
return c.json({ input });
|
|
362
|
-
});
|
|
363
|
-
app.get("/runs/:runId/node", (c) => {
|
|
364
|
-
const runId = c.req.param("runId");
|
|
365
|
-
const nodeKey = c.req.query("key");
|
|
366
|
-
if (!nodeKey) {
|
|
367
|
-
return c.json({ error: "key query parameter is required" }, 400);
|
|
368
|
-
}
|
|
369
|
-
const state = store.readNodeState(runId, nodeKey);
|
|
370
|
-
if (!state) {
|
|
371
|
-
return c.json({ error: "Node not found" }, 404);
|
|
372
|
-
}
|
|
373
|
-
return c.json(state);
|
|
374
|
-
});
|
|
375
|
-
app.get("/runs/:runId/artifact-path", (c) => {
|
|
376
|
-
const runId = c.req.param("runId");
|
|
377
|
-
const uri = c.req.query("uri");
|
|
378
|
-
if (!uri) {
|
|
379
|
-
return c.json({ error: "uri query parameter is required" }, 400);
|
|
380
|
-
}
|
|
381
|
-
if (!store.hasRun(runId)) {
|
|
382
|
-
return c.json({ error: "Run not found" }, 404);
|
|
383
|
-
}
|
|
384
|
-
// Validate that the URI's runId matches the route's :runId to prevent
|
|
385
|
-
// cross-run artifact path resolution.
|
|
386
|
-
const parsed = ArtifactReferences.tryParse(uri);
|
|
387
|
-
if (parsed && parsed.runId !== runId) {
|
|
388
|
-
return c.json({ error: "Artifact URI runId does not match route runId" }, 400);
|
|
389
|
-
}
|
|
390
|
-
const absPath = store.resolveArtifactPath(uri);
|
|
391
|
-
if (!absPath) {
|
|
392
|
-
return c.json({ error: "Invalid artifact uri" }, 400);
|
|
393
|
-
}
|
|
394
|
-
return c.json({ absPath });
|
|
395
|
-
});
|
|
396
|
-
// ─── Control ────────────────────────────────────────────────────
|
|
397
|
-
app.post("/runs/:runId/pause", async (c) => {
|
|
398
|
-
const runId = c.req.param("runId");
|
|
399
|
-
// Run-level pause
|
|
400
|
-
const meta = store.readRunMeta(runId);
|
|
401
|
-
if (!meta)
|
|
402
|
-
return c.json({ error: "Run not found" }, 404);
|
|
403
|
-
if (meta.status !== "running")
|
|
404
|
-
return c.json({ error: `Cannot pause a run in state '${meta.status}'` }, 409);
|
|
405
|
-
const interpreter = interpreters.get(runId);
|
|
406
|
-
if (!interpreter)
|
|
407
|
-
return c.json({ error: "Run is not actively executing; pause requires an in-flight run" }, 409);
|
|
408
|
-
interpreter.pauseRun(runId);
|
|
409
|
-
const updated = store.readRunMeta(runId);
|
|
410
|
-
return c.json(updated);
|
|
411
|
-
});
|
|
412
|
-
app.post("/runs/:runId/resume", async (c) => {
|
|
413
|
-
const runId = c.req.param("runId");
|
|
414
|
-
// Run-level resume
|
|
415
|
-
if (resumingRunIds.has(runId)) {
|
|
416
|
-
return c.json({ error: "Run is already being resumed or retried" }, 409);
|
|
417
|
-
}
|
|
418
|
-
const meta = store.readRunMeta(runId);
|
|
419
|
-
if (!meta)
|
|
420
|
-
return c.json({ error: "Run not found" }, 404);
|
|
421
|
-
if (meta.status !== "paused")
|
|
422
|
-
return c.json({ error: `Cannot resume a run in state '${meta.status}'` }, 409);
|
|
423
|
-
// Wait for any in-flight runToCompletion to settle before resuming,
|
|
424
|
-
// preventing concurrent execution race on the same interpreter.
|
|
425
|
-
const inFlight = inFlightRuns.get(runId);
|
|
426
|
-
if (inFlight)
|
|
427
|
-
await inFlight;
|
|
428
|
-
resumingRunIds.add(runId);
|
|
429
|
-
try {
|
|
430
|
-
const interpreter = getOrRecoverInterpreter(runId);
|
|
431
|
-
if (!interpreter)
|
|
432
|
-
return c.json({ error: "Run not found" }, 404);
|
|
433
|
-
await interpreter.resumeRun(runId);
|
|
434
|
-
interpreters.set(runId, interpreter);
|
|
435
|
-
lastActiveAt = Date.now();
|
|
436
|
-
startPersistedRunExecution(interpreter, runId);
|
|
437
|
-
const updated = store.readRunMeta(runId);
|
|
438
|
-
return c.json(updated);
|
|
439
|
-
}
|
|
440
|
-
finally {
|
|
441
|
-
resumingRunIds.delete(runId);
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
app.post("/runs/:runId/cancel", async (c) => {
|
|
445
|
-
const runId = c.req.param("runId");
|
|
446
|
-
// Run-level cancel
|
|
447
|
-
const meta = store.readRunMeta(runId);
|
|
448
|
-
if (!meta)
|
|
449
|
-
return c.json({ error: "Run not found" }, 404);
|
|
450
|
-
if (meta.status !== "running" && meta.status !== "paused") {
|
|
451
|
-
return c.json({ error: `Cannot cancel a run in state '${meta.status}'` }, 409);
|
|
452
|
-
}
|
|
453
|
-
const interpreter = interpreters.get(runId) ?? getOrRecoverInterpreter(runId);
|
|
454
|
-
if (!interpreter)
|
|
455
|
-
return c.json({ error: "Run not found" }, 404);
|
|
456
|
-
interpreter.cancelRun(runId);
|
|
457
|
-
// Wait for in-flight runToCompletion to settle after cancel, so its catch
|
|
458
|
-
// block sees the "cancelled" abort intent and writes "cancelled" status
|
|
459
|
-
// instead of overwriting with "paused" from a prior pauseRun.
|
|
460
|
-
const inFlight = inFlightRuns.get(runId);
|
|
461
|
-
if (inFlight)
|
|
462
|
-
await inFlight;
|
|
463
|
-
pruneTerminalInterpreters();
|
|
464
|
-
const updated = store.readRunMeta(runId);
|
|
465
|
-
return c.json(updated);
|
|
466
|
-
});
|
|
467
|
-
app.post("/runs/:runId/retry", async (c) => {
|
|
468
|
-
const runId = c.req.param("runId");
|
|
469
|
-
const nodeKey = c.req.query("key");
|
|
470
|
-
if (nodeKey) {
|
|
471
|
-
const interpreter = getOrRecoverInterpreter(runId);
|
|
472
|
-
if (!interpreter)
|
|
473
|
-
return c.json({ error: "Run not found" }, 404);
|
|
474
|
-
const retry = interpreter.retryNode(runId, nodeKey);
|
|
475
|
-
const immediate = await settleImmediate(retry);
|
|
476
|
-
if (immediate.kind === "rejected") {
|
|
477
|
-
const err = immediate.error;
|
|
478
|
-
return c.json({ error: err instanceof Error ? err.message : String(err) }, 409);
|
|
479
|
-
}
|
|
480
|
-
if (immediate.kind === "pending")
|
|
481
|
-
consumeDetachedControl(retry);
|
|
482
|
-
lastActiveAt = Date.now();
|
|
483
|
-
const state = store.readNodeState(runId, nodeKey);
|
|
484
|
-
return c.json(state);
|
|
485
|
-
}
|
|
486
|
-
// Run-level retry
|
|
487
|
-
if (resumingRunIds.has(runId)) {
|
|
488
|
-
return c.json({ error: "Run is already being resumed or retried" }, 409);
|
|
489
|
-
}
|
|
490
|
-
const meta = store.readRunMeta(runId);
|
|
491
|
-
if (!meta)
|
|
492
|
-
return c.json({ error: "Run not found" }, 404);
|
|
493
|
-
if (meta.status !== "failed") {
|
|
494
|
-
return c.json({ error: `Cannot retry a run in state '${meta.status}'` }, 409);
|
|
495
|
-
}
|
|
496
|
-
const inFlight = inFlightRuns.get(runId);
|
|
497
|
-
if (inFlight)
|
|
498
|
-
await inFlight;
|
|
499
|
-
resumingRunIds.add(runId);
|
|
500
|
-
try {
|
|
501
|
-
const interpreter = getOrRecoverInterpreter(runId);
|
|
502
|
-
if (!interpreter)
|
|
503
|
-
return c.json({ error: "Run not found" }, 404);
|
|
504
|
-
interpreter.retryRun(runId);
|
|
505
|
-
interpreters.set(runId, interpreter);
|
|
506
|
-
lastActiveAt = Date.now();
|
|
507
|
-
startPersistedRunExecution(interpreter, runId);
|
|
508
|
-
const updated = store.readRunMeta(runId);
|
|
509
|
-
return c.json(updated);
|
|
510
|
-
}
|
|
511
|
-
finally {
|
|
512
|
-
resumingRunIds.delete(runId);
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
// ─── Fork ────────────────────────────────────────────────────────
|
|
516
|
-
//
|
|
517
|
-
// Derive a new Run from a terminal source Run by supplying a possibly-
|
|
518
|
-
// modified Workflow Spec. Inheritance is keyed by Node Definition Hash; the
|
|
519
|
-
// first divergence becomes the default Fork Origin. `dryRun: true` returns
|
|
520
|
-
// the plan without creating a Run.
|
|
521
|
-
app.post("/runs/:runId/fork", async (c) => {
|
|
522
|
-
const runId = c.req.param("runId");
|
|
523
|
-
const body = await c.req.json().catch(() => ({}));
|
|
524
|
-
const priorMeta = store.readRunMeta(runId);
|
|
525
|
-
if (!priorMeta)
|
|
526
|
-
return c.json({ error: "Run not found" }, 404);
|
|
527
|
-
if (!body.spec) {
|
|
528
|
-
return c.json({ error: "spec is required" }, 400);
|
|
529
|
-
}
|
|
530
|
-
let sourcePath;
|
|
531
|
-
if (body.sourcePath) {
|
|
532
|
-
try {
|
|
533
|
-
sourcePath = sourceResolver.validateSourcePath(body.sourcePath);
|
|
534
|
-
}
|
|
535
|
-
catch (error) {
|
|
536
|
-
return c.json({ error: errorMessage(error) }, 400);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
let submitMetadata = emptyAgentOverrideResult();
|
|
540
|
-
let effectiveSource = body.spec;
|
|
541
|
-
try {
|
|
542
|
-
if (body.agentOverrides !== undefined || Object.keys(priorMeta.agentOverrides ?? {}).length > 0) {
|
|
543
|
-
const parsedSpec = parseWorkflowSpecForOverrides(body.spec);
|
|
544
|
-
submitMetadata = applyAgentOverrides(parsedSpec, body.agentOverrides, {
|
|
545
|
-
inherited: priorMeta.agentOverrides ?? {}
|
|
546
|
-
});
|
|
547
|
-
effectiveSource = serializeWorkflowSpecForOverrides(submitMetadata.effectiveSpec);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
catch (error) {
|
|
551
|
-
return c.json({ kind: "fork-rejected", error: errorMessage(error) }, 400);
|
|
552
|
-
}
|
|
553
|
-
const compileResult = compileWorkflow(effectiveSource, {
|
|
554
|
-
sourcePath,
|
|
555
|
-
includeResolver: sourceResolver.createIncludeResolver(sourcePath)
|
|
556
|
-
});
|
|
557
|
-
if (!compileResult.ok || !compileResult.ir) {
|
|
558
|
-
return c.json({ kind: "fork-rejected", error: "Compilation failed", diagnostics: compileResult.diagnostics }, 400);
|
|
559
|
-
}
|
|
560
|
-
let plan;
|
|
561
|
-
try {
|
|
562
|
-
plan = planForkedRun(store, {
|
|
563
|
-
sourceRunId: runId,
|
|
564
|
-
ir: compileResult.ir,
|
|
565
|
-
overrideOriginNodeKey: body.overrideOriginNodeKey
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
catch (error) {
|
|
569
|
-
if (error instanceof ForkError) {
|
|
570
|
-
return c.json({ kind: "fork-rejected", error: error.message }, 409);
|
|
571
|
-
}
|
|
572
|
-
throw error;
|
|
573
|
-
}
|
|
574
|
-
if (body.dryRun) {
|
|
575
|
-
return c.json({
|
|
576
|
-
dryRun: true,
|
|
577
|
-
plan,
|
|
578
|
-
agentOverrides: submitMetadata.agentOverrides,
|
|
579
|
-
submissionWarnings: submitMetadata.warnings
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
const forkRunId = generateRunId();
|
|
583
|
-
let materialized;
|
|
584
|
-
try {
|
|
585
|
-
materialized = materializeForkedRun(store, {
|
|
586
|
-
sourceRunId: runId,
|
|
587
|
-
forkRunId,
|
|
588
|
-
ir: compileResult.ir,
|
|
589
|
-
input: body.input,
|
|
590
|
-
workflowRef: body.workflowRef ?? priorMeta.workflowRef,
|
|
591
|
-
workflowSourcePath: sourcePath ?? priorMeta.workflowSourcePath,
|
|
592
|
-
plan,
|
|
593
|
-
...optionalSubmissionMetadata(submitMetadata),
|
|
594
|
-
skipHooks: priorMeta.skipHooks
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
catch (error) {
|
|
598
|
-
if (error instanceof InputValidationFailure) {
|
|
599
|
-
return c.json({ error: "Input validation failed", validationErrors: error.errors }, 400);
|
|
600
|
-
}
|
|
601
|
-
throw error;
|
|
602
|
-
}
|
|
603
|
-
const interpreter = newInterpreter(forkRunId);
|
|
604
|
-
interpreters.set(forkRunId, interpreter);
|
|
605
|
-
lastActiveAt = Date.now();
|
|
606
|
-
startRunExecution(interpreter, compileResult.ir, materialized.input, forkRunId);
|
|
607
|
-
return c.json({ run: materialized.run, plan: materialized.plan }, 201);
|
|
608
|
-
});
|
|
609
|
-
// ─── Replay ──────────────────────────────────────────────────────
|
|
610
|
-
app.post("/runs/:runId/replay", (c) => {
|
|
611
|
-
const runId = c.req.param("runId");
|
|
612
|
-
const interpreter = getReadOnlyInterpreter(runId);
|
|
613
|
-
if (!interpreter)
|
|
614
|
-
return c.json({ error: "Run not found" }, 404);
|
|
615
|
-
const result = interpreter.replay(runId);
|
|
616
|
-
return c.json(result);
|
|
617
|
-
});
|
|
618
|
-
// ─── Signal ───────────────────────────────────────────────────────
|
|
619
|
-
//
|
|
620
|
-
// Deliver an external decision payload to a Signal Node that is currently
|
|
621
|
-
// `awaiting`. Node-addressed only (`?key=` required). The request body is the
|
|
622
|
-
// payload object directly. The resolver lives in the in-memory interpreter,
|
|
623
|
-
// so a live in-flight run is required (409 otherwise). A payload that fails
|
|
624
|
-
// the node's declared output schema is rejected with 422 and the node stays
|
|
625
|
-
// `awaiting`.
|
|
626
|
-
app.post("/runs/:runId/signal", async (c) => {
|
|
627
|
-
const runId = c.req.param("runId");
|
|
628
|
-
const nodeKey = c.req.query("key");
|
|
629
|
-
if (!nodeKey) {
|
|
630
|
-
return c.json({ error: "key query parameter is required (signals are node-level)" }, 400);
|
|
631
|
-
}
|
|
632
|
-
const payload = await c.req.json().catch(() => undefined);
|
|
633
|
-
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
634
|
-
return c.json({ error: "Signal payload must be a JSON object" }, 400);
|
|
635
|
-
}
|
|
636
|
-
const interpreter = interpreters.get(runId);
|
|
637
|
-
if (!interpreter) {
|
|
638
|
-
return store.hasRun(runId)
|
|
639
|
-
? c.json({ error: "Run is not actively executing; signal requires an in-flight run" }, 409)
|
|
640
|
-
: c.json({ error: "Run not found" }, 404);
|
|
641
|
-
}
|
|
642
|
-
try {
|
|
643
|
-
interpreter.submitSignal(runId, nodeKey, payload);
|
|
644
|
-
}
|
|
645
|
-
catch (err) {
|
|
646
|
-
// A non-conforming payload is a request-body problem (422); the node
|
|
647
|
-
// stays `awaiting`. A missing resolver ("not awaiting") is a state
|
|
648
|
-
// conflict (409).
|
|
649
|
-
if (err instanceof SignalPayloadValidationError) {
|
|
650
|
-
return c.json({ error: err.message }, 422);
|
|
651
|
-
}
|
|
652
|
-
return c.json({ error: err instanceof Error ? err.message : String(err) }, 409);
|
|
653
|
-
}
|
|
654
|
-
// submitSignal resolves the in-memory promise; the awaiting → completed
|
|
655
|
-
// write happens in the executeNode continuation on a later microtask. Wait
|
|
656
|
-
// briefly for the node to leave `awaiting` so we return the settled state.
|
|
657
|
-
let state = store.readNodeState(runId, nodeKey);
|
|
658
|
-
for (let i = 0; i < 100 && state?.state === "awaiting"; i++) {
|
|
659
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
660
|
-
state = store.readNodeState(runId, nodeKey);
|
|
661
|
-
}
|
|
662
|
-
return c.json(state);
|
|
663
|
-
});
|
|
664
|
-
// ─── Output & Artifacts ──────────────────────────────────────────
|
|
665
|
-
app.get("/runs/:runId/output", (c) => {
|
|
666
|
-
const runId = c.req.param("runId");
|
|
667
|
-
const meta = store.readRunMeta(runId);
|
|
668
|
-
if (!meta)
|
|
669
|
-
return c.json({ error: "Run not found" }, 404);
|
|
670
|
-
const output = meta.status === "completed" ? (meta.output ?? {}) : {};
|
|
671
|
-
const body = { status: meta.status, output };
|
|
672
|
-
if (meta.status === "failed" && meta.error)
|
|
673
|
-
body.error = meta.error;
|
|
674
|
-
return c.json(body);
|
|
675
|
-
});
|
|
676
|
-
return {
|
|
677
|
-
app,
|
|
678
|
-
/** Expose for the runner to read idle-shutdown state. */
|
|
679
|
-
getLastActiveAt: () => getLastActiveAt(),
|
|
680
|
-
/** Expose for the runner to check how many Runs are still active. */
|
|
681
|
-
runningCount: () => runningCount(),
|
|
682
|
-
/** Expose for the runner to write the startedAt and endpoint after listen. */
|
|
683
|
-
setHealthOverrides: (overrides) => {
|
|
684
|
-
if (overrides.startedAt)
|
|
685
|
-
healthStartedAt = overrides.startedAt;
|
|
686
|
-
if (overrides.endpoint)
|
|
687
|
-
healthEndpoint = overrides.endpoint;
|
|
688
|
-
}
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
function errorMessage(error) {
|
|
692
|
-
return error instanceof Error ? error.message : String(error);
|
|
693
|
-
}
|
|
694
|
-
//# sourceMappingURL=supervisor-app.js.map
|