@hasna/workflows 0.1.3 → 0.2.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/README.md +152 -98
- package/dist/agent-tools.js +152 -959
- package/dist/cli/index.js +4130 -2329
- package/dist/handlers.js +3287 -55
- package/dist/index.js +1534 -370
- package/dist/mcp/index.js +1741 -1660
- package/dist/sdk.js +1620 -0
- package/dist/serve/admin.js +4248 -0
- package/dist/serve/index.js +3416 -1350
- package/dist/service.js +1302 -286
- package/hasna.contract.json +17 -17
- package/package.json +16 -13
- package/types/agent-tools.d.ts +3 -17
- package/types/client-error.d.ts +4 -0
- package/types/client-tools.d.ts +4 -0
- package/types/client.d.ts +60 -0
- package/types/daemon.d.ts +18 -9
- package/types/db/postgres-store.d.ts +54 -0
- package/types/graph.d.ts +7 -0
- package/types/handlers.d.ts +2 -1
- package/types/identity.d.ts +7 -0
- package/types/lanes/types.d.ts +12 -3
- package/types/openapi.d.ts +7 -0
- package/types/paths.d.ts +10 -1
- package/types/sdk/generated-api.d.ts +159 -0
- package/types/sdk.d.ts +6 -0
- package/types/serve/admin.d.ts +2 -0
- package/types/service.d.ts +75 -4
- package/types/session.d.ts +31 -8
- package/types/setup.d.ts +71 -0
- package/types/store.d.ts +35 -0
- package/types/tool-definitions.d.ts +18 -0
- package/types/types.d.ts +8 -0
- package/types/version.d.ts +1 -0
- package/types/wal.d.ts +9 -1
package/dist/agent-tools.js
CHANGED
|
@@ -29,10 +29,30 @@ function binaryOnPath(binary) {
|
|
|
29
29
|
}
|
|
30
30
|
return false;
|
|
31
31
|
}
|
|
32
|
-
function
|
|
32
|
+
function subscriptionEnvironment(overrides = {}) {
|
|
33
|
+
const paid = /^(OPENAI_API_KEY|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|CODEX_API_KEY|CURSOR_API_KEY)$/;
|
|
34
|
+
const sensitive = /(?:API[_-]?KEY|SECRET|PASSWORD|TOKEN|DATABASE_URL|CREDENTIAL)/i;
|
|
35
|
+
const env = {};
|
|
36
|
+
for (const [name, value] of Object.entries(process.env)) {
|
|
37
|
+
if (value !== undefined && !paid.test(name) && (!sensitive.test(name) || name === "CLAUDE_CODE_OAUTH_TOKEN"))
|
|
38
|
+
env[name] = value;
|
|
39
|
+
}
|
|
40
|
+
for (const [name, value] of Object.entries(overrides)) {
|
|
41
|
+
if (paid.test(name) || sensitive.test(name))
|
|
42
|
+
throw new Error(`lane environment field ${name} requires a supported scoped credential consumer`);
|
|
43
|
+
env[name] = value;
|
|
44
|
+
}
|
|
45
|
+
return env;
|
|
46
|
+
}
|
|
47
|
+
function subscriptionSdkEnvironment(overrides = {}) {
|
|
48
|
+
const safe = subscriptionEnvironment(overrides);
|
|
49
|
+
return { ...Object.fromEntries(Object.keys(process.env).map((name) => [name, undefined])), ...safe };
|
|
50
|
+
}
|
|
51
|
+
function withTimeout(promise, timeoutMs, what, cancel) {
|
|
33
52
|
return new Promise((resolve, reject) => {
|
|
34
53
|
const timer = setTimeout(() => {
|
|
35
|
-
|
|
54
|
+
cancel?.();
|
|
55
|
+
reject(new LaneTimeoutError(what, timeoutMs));
|
|
36
56
|
}, timeoutMs);
|
|
37
57
|
promise.then((value) => {
|
|
38
58
|
clearTimeout(timer);
|
|
@@ -43,15 +63,16 @@ function withTimeout(promise, timeoutMs, what) {
|
|
|
43
63
|
});
|
|
44
64
|
});
|
|
45
65
|
}
|
|
46
|
-
function spawnCli(cmd, opts) {
|
|
66
|
+
async function spawnCli(cmd, opts) {
|
|
47
67
|
const started = Date.now();
|
|
48
68
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
|
|
49
69
|
let proc;
|
|
50
70
|
try {
|
|
51
|
-
proc = Bun.
|
|
71
|
+
proc = Bun.spawn({
|
|
52
72
|
cmd,
|
|
53
73
|
cwd: opts.cwd,
|
|
54
|
-
env:
|
|
74
|
+
env: subscriptionEnvironment(opts.env),
|
|
75
|
+
stdin: "ignore",
|
|
55
76
|
stdout: "pipe",
|
|
56
77
|
stderr: "pipe",
|
|
57
78
|
timeout: timeoutMs
|
|
@@ -63,15 +84,16 @@ function spawnCli(cmd, opts) {
|
|
|
63
84
|
}
|
|
64
85
|
return { ok: false, exitCode: 1, output: "", durationMs: Date.now() - started, error: message };
|
|
65
86
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
88
|
+
new Response(proc.stdout).text(),
|
|
89
|
+
new Response(proc.stderr).text(),
|
|
90
|
+
proc.exited
|
|
91
|
+
]);
|
|
69
92
|
const output = stderr ? `${stdout}
|
|
70
93
|
${stderr}`.trim() : stdout.trim();
|
|
71
|
-
|
|
72
|
-
return { ok: exitCode === 0, exitCode, output, durationMs };
|
|
94
|
+
return { ok: exitCode === 0, exitCode, output, durationMs: Date.now() - started };
|
|
73
95
|
}
|
|
74
|
-
var LANE_KINDS2, LaneDependencyMissingError, LaneAdapterShapeError, DEFAULT_LANE_TIMEOUT_MS = 120000;
|
|
96
|
+
var LANE_KINDS2, LaneDependencyMissingError, LaneAdapterShapeError, DEFAULT_LANE_TIMEOUT_MS = 120000, LaneTimeoutError;
|
|
75
97
|
var init_types = __esm(() => {
|
|
76
98
|
LANE_KINDS2 = ["claude", "codex", "cursor", "grok"];
|
|
77
99
|
LaneDependencyMissingError = class LaneDependencyMissingError extends Error {
|
|
@@ -86,6 +108,12 @@ var init_types = __esm(() => {
|
|
|
86
108
|
this.name = "LaneAdapterShapeError";
|
|
87
109
|
}
|
|
88
110
|
};
|
|
111
|
+
LaneTimeoutError = class LaneTimeoutError extends Error {
|
|
112
|
+
constructor(what, timeoutMs) {
|
|
113
|
+
super(`${what} exceeded the ${timeoutMs}ms budget`);
|
|
114
|
+
this.name = "LaneTimeoutError";
|
|
115
|
+
}
|
|
116
|
+
};
|
|
89
117
|
});
|
|
90
118
|
|
|
91
119
|
// src/lanes/claude.ts
|
|
@@ -127,11 +155,12 @@ function createClaudeAdapter(deps = {}) {
|
|
|
127
155
|
throw new LaneAdapterShapeError("@anthropic-ai/claude-agent-sdk exports no query() function");
|
|
128
156
|
}
|
|
129
157
|
const prompt = job.prompt;
|
|
158
|
+
const abortController = new AbortController;
|
|
130
159
|
try {
|
|
131
160
|
const { output, isError } = await withTimeout((async () => {
|
|
132
161
|
const generator = await query({
|
|
133
162
|
prompt,
|
|
134
|
-
options: { model: job.model, maxTurns: 32,
|
|
163
|
+
options: { model: job.model, cwd: job.cwd, maxTurns: 32, abortController, env: subscriptionSdkEnvironment(job.env) }
|
|
135
164
|
});
|
|
136
165
|
let output2 = "";
|
|
137
166
|
let isError2 = false;
|
|
@@ -150,19 +179,19 @@ function createClaudeAdapter(deps = {}) {
|
|
|
150
179
|
isError2 = true;
|
|
151
180
|
}
|
|
152
181
|
return { output: output2, isError: isError2 };
|
|
153
|
-
})(), timeoutMs,
|
|
182
|
+
})(), timeoutMs, "claude lane", () => abortController.abort());
|
|
154
183
|
const durationMs = Date.now() - started;
|
|
155
184
|
return { ok: !isError, exitCode: isError ? 1 : 0, output: output.trim(), durationMs };
|
|
156
185
|
} catch (err) {
|
|
157
186
|
const durationMs = Date.now() - started;
|
|
158
|
-
return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err) };
|
|
187
|
+
return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
|
|
159
188
|
}
|
|
160
189
|
}
|
|
161
190
|
};
|
|
162
191
|
}
|
|
163
|
-
function runClaudeCli(job, cliPath, started, timeoutMs) {
|
|
192
|
+
async function runClaudeCli(job, cliPath, started, timeoutMs) {
|
|
164
193
|
try {
|
|
165
|
-
const result = spawnCli([cliPath, "-p", job.prompt], {
|
|
194
|
+
const result = await spawnCli([cliPath, "-p", job.prompt], {
|
|
166
195
|
binaryName: cliPath,
|
|
167
196
|
packageHint: "@anthropic-ai/claude-agent-sdk or the claude CLI",
|
|
168
197
|
timeoutMs,
|
|
@@ -219,29 +248,30 @@ function createCodexAdapter(deps = {}) {
|
|
|
219
248
|
if (typeof CodexClass !== "function") {
|
|
220
249
|
throw new LaneAdapterShapeError("@openai/codex-sdk exports no Codex class");
|
|
221
250
|
}
|
|
251
|
+
const abortController = new AbortController;
|
|
222
252
|
try {
|
|
223
253
|
const client = await withTimeout(Promise.resolve(new CodexClass({
|
|
224
254
|
config: { model: job.model },
|
|
225
|
-
env: job.env
|
|
226
|
-
})), timeoutMs,
|
|
227
|
-
const thread = client.startThread?.();
|
|
255
|
+
env: subscriptionEnvironment(job.env)
|
|
256
|
+
})), timeoutMs, "codex lane");
|
|
257
|
+
const thread = client.startThread?.({ model: job.model, workingDirectory: job.cwd });
|
|
228
258
|
if (!thread || typeof thread.run !== "function") {
|
|
229
259
|
throw new LaneAdapterShapeError("@openai/codex-sdk Codex.startThread() returned no run()");
|
|
230
260
|
}
|
|
231
|
-
const turn = await withTimeout(thread.run(job.prompt), timeoutMs,
|
|
261
|
+
const turn = await withTimeout(thread.run(job.prompt, { signal: abortController.signal }), timeoutMs, "codex lane", () => abortController.abort());
|
|
232
262
|
const output = turn?.finalResponse ?? "";
|
|
233
263
|
const durationMs = Date.now() - started;
|
|
234
264
|
return { ok: true, exitCode: 0, output, durationMs };
|
|
235
265
|
} catch (err) {
|
|
236
266
|
const durationMs = Date.now() - started;
|
|
237
|
-
return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err) };
|
|
267
|
+
return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
|
|
238
268
|
}
|
|
239
269
|
}
|
|
240
270
|
};
|
|
241
271
|
}
|
|
242
|
-
function runCodexCli(job, cliPath, started, timeoutMs) {
|
|
272
|
+
async function runCodexCli(job, cliPath, started, timeoutMs) {
|
|
243
273
|
try {
|
|
244
|
-
const result = spawnCli([cliPath, "exec", "--", job.prompt], {
|
|
274
|
+
const result = await spawnCli([cliPath, "exec", "--", job.prompt], {
|
|
245
275
|
binaryName: cliPath,
|
|
246
276
|
packageHint: "@openai/codex-sdk or the codex CLI",
|
|
247
277
|
timeoutMs,
|
|
@@ -285,6 +315,8 @@ function createCursorAdapter(deps = {}) {
|
|
|
285
315
|
if (!job.prompt) {
|
|
286
316
|
return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "cursor lane requires a prompt" };
|
|
287
317
|
}
|
|
318
|
+
if (!deps.sdkLoader && binaryOnPath(deps.cliPath ?? "cursor-agent"))
|
|
319
|
+
return runCursorCli(job, deps.cliPath ?? "cursor-agent", Date.now(), job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS);
|
|
288
320
|
const started = Date.now();
|
|
289
321
|
const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
|
|
290
322
|
const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
|
|
@@ -294,38 +326,63 @@ function createCursorAdapter(deps = {}) {
|
|
|
294
326
|
} catch {
|
|
295
327
|
return runCursorCli(job, deps.cliPath ?? "cursor-agent", started, timeoutMs);
|
|
296
328
|
}
|
|
329
|
+
let cancel;
|
|
297
330
|
try {
|
|
298
|
-
|
|
331
|
+
subscriptionEnvironment(job.env);
|
|
332
|
+
if (!deps.sdkLoader && process.env.CURSOR_API_KEY)
|
|
333
|
+
throw new Error("cursor lane refuses ambient API billing; use the native subscription CLI");
|
|
334
|
+
const run = await withTimeout(runLocalAgent(sdk, job, (callback) => {
|
|
335
|
+
cancel = callback;
|
|
336
|
+
}), timeoutMs, "cursor lane", () => cancel?.());
|
|
299
337
|
const durationMs = Date.now() - started;
|
|
300
|
-
const status = run
|
|
338
|
+
const status = run.status;
|
|
301
339
|
const ok = status === "finished";
|
|
302
|
-
return { ok, exitCode: ok ? 0 : 1, output: run
|
|
340
|
+
return { ok, exitCode: ok ? 0 : 1, output: run.result, durationMs };
|
|
303
341
|
} catch (err) {
|
|
342
|
+
if (err instanceof LaneAdapterShapeError)
|
|
343
|
+
throw err;
|
|
304
344
|
const durationMs = Date.now() - started;
|
|
305
|
-
return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err) };
|
|
345
|
+
return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
|
|
306
346
|
}
|
|
307
347
|
}
|
|
308
348
|
};
|
|
309
349
|
}
|
|
310
|
-
async function runLocalAgent(sdk, job) {
|
|
350
|
+
async function runLocalAgent(sdk, job, onCancel) {
|
|
311
351
|
const AgentClass = sdk.Agent;
|
|
312
352
|
const CursorClass = sdk.Cursor;
|
|
313
353
|
if (typeof AgentClass !== "function" && typeof CursorClass !== "function") {
|
|
314
354
|
throw new LaneAdapterShapeError("@cursor/sdk exports no Agent/Cursor class");
|
|
315
355
|
}
|
|
316
|
-
const
|
|
356
|
+
const create = AgentClass.create;
|
|
357
|
+
const agent = typeof create === "function" ? await create.call(AgentClass, { local: { cwd: job.cwd, enableAgentRetries: false } }) : typeof AgentClass === "function" ? new AgentClass : undefined;
|
|
317
358
|
if (!agent || typeof agent.send !== "function") {
|
|
318
359
|
throw new LaneAdapterShapeError("@cursor/sdk Agent has no send()");
|
|
319
360
|
}
|
|
320
|
-
|
|
361
|
+
let run = await agent.send(job.prompt, { model: job.model });
|
|
362
|
+
if (run && typeof run === "object") {
|
|
363
|
+
const handle = run;
|
|
364
|
+
if (typeof handle.cancel === "function")
|
|
365
|
+
onCancel(() => {
|
|
366
|
+
handle.cancel().catch(() => {});
|
|
367
|
+
});
|
|
368
|
+
if (typeof handle.wait === "function")
|
|
369
|
+
run = await handle.wait();
|
|
370
|
+
}
|
|
321
371
|
if (!run || typeof run !== "object") {
|
|
322
372
|
throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned no Run");
|
|
323
373
|
}
|
|
324
|
-
|
|
374
|
+
const candidate = run;
|
|
375
|
+
if (typeof candidate.status !== "string" || !["finished", "error", "cancelled"].includes(candidate.status)) {
|
|
376
|
+
throw new LaneAdapterShapeError(`@cursor/sdk Agent.send() returned a Run without a terminal status (got ${JSON.stringify(candidate.status)}; expected finished|error|cancelled)`);
|
|
377
|
+
}
|
|
378
|
+
if (typeof candidate.result !== "string") {
|
|
379
|
+
throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned a Run without a string result");
|
|
380
|
+
}
|
|
381
|
+
return { status: candidate.status, result: candidate.result };
|
|
325
382
|
}
|
|
326
|
-
function runCursorCli(job, cliPath, started, timeoutMs) {
|
|
383
|
+
async function runCursorCli(job, cliPath, started, timeoutMs) {
|
|
327
384
|
try {
|
|
328
|
-
const result = spawnCli([cliPath, "
|
|
385
|
+
const result = await spawnCli([cliPath, "--print", job.prompt, ...job.model ? ["--model", job.model] : []], {
|
|
329
386
|
binaryName: cliPath,
|
|
330
387
|
packageHint: "@cursor/sdk (local mode) or the cursor-agent CLI",
|
|
331
388
|
timeoutMs,
|
|
@@ -368,7 +425,7 @@ function createGrokAdapter(deps = {}) {
|
|
|
368
425
|
const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
|
|
369
426
|
const cliPath = deps.cliPath ?? "grok";
|
|
370
427
|
try {
|
|
371
|
-
const result = spawnCli([cliPath, job.prompt], {
|
|
428
|
+
const result = await spawnCli([cliPath, job.prompt], {
|
|
372
429
|
binaryName: cliPath,
|
|
373
430
|
packageHint: "the grok CLI (no xAI Grok SDK exists on npm \u2014 measured 2026-08-25)",
|
|
374
431
|
timeoutMs,
|
|
@@ -807,16 +864,49 @@ function findCycles(graph) {
|
|
|
807
864
|
}
|
|
808
865
|
function validateGraph(graph) {
|
|
809
866
|
const issues = [];
|
|
867
|
+
if (graph === null || typeof graph !== "object" || Array.isArray(graph)) {
|
|
868
|
+
return { ok: false, issues: [{ path: "graph", message: "graph must be an object" }] };
|
|
869
|
+
}
|
|
810
870
|
if (typeof graph.name !== "string" || graph.name.trim() === "") {
|
|
811
871
|
issue(issues, "name", "graph requires a non-empty name");
|
|
812
872
|
}
|
|
813
873
|
if (typeof graph.version !== "string" || graph.version.trim() === "") {
|
|
814
874
|
issue(issues, "version", "graph requires a version string");
|
|
815
875
|
}
|
|
876
|
+
if (graph.memoWatch !== undefined) {
|
|
877
|
+
if (!Array.isArray(graph.memoWatch) || graph.memoWatch.length === 0) {
|
|
878
|
+
issue(issues, "memoWatch", "memoWatch must be a non-empty array of file paths or globs");
|
|
879
|
+
} else {
|
|
880
|
+
for (const entry of graph.memoWatch) {
|
|
881
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
882
|
+
issue(issues, "memoWatch", "each memoWatch entry must be a non-empty path or glob string");
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
816
888
|
if (!Array.isArray(graph.nodes) || graph.nodes.length === 0) {
|
|
817
889
|
issue(issues, "nodes", "graph requires at least one node");
|
|
818
890
|
return { ok: false, issues };
|
|
819
891
|
}
|
|
892
|
+
for (let index = 0;index < graph.nodes.length; index++) {
|
|
893
|
+
const node = graph.nodes[index];
|
|
894
|
+
if (node === null || typeof node !== "object" || Array.isArray(node)) {
|
|
895
|
+
issue(issues, `nodes.${index}`, "node must be an object");
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
if (!GRAPH_NODE_TYPES.includes(node.type))
|
|
899
|
+
issue(issues, `nodes.${index}.type`, "type must be start, step, decision, while, or end");
|
|
900
|
+
for (const key of ["next", "then", "else", "prompt", "command"]) {
|
|
901
|
+
const value = node[key];
|
|
902
|
+
if (value !== undefined && (typeof value !== "string" || value.trim() === ""))
|
|
903
|
+
issue(issues, `nodes.${index}.${key}`, `${key} must be a non-empty string`);
|
|
904
|
+
}
|
|
905
|
+
if (node.type === "while" && (!Array.isArray(node.body) || node.body.some((id) => typeof id !== "string")))
|
|
906
|
+
issue(issues, `nodes.${index}.body`, "body must be an array of node ids");
|
|
907
|
+
}
|
|
908
|
+
if (issues.length)
|
|
909
|
+
return { ok: false, issues };
|
|
820
910
|
const starts = graph.nodes.filter((n) => n.type === "start");
|
|
821
911
|
if (starts.length === 0)
|
|
822
912
|
issue(issues, "nodes", "graph requires exactly one start node (found none)");
|
|
@@ -878,6 +968,8 @@ function nodeEdges(node) {
|
|
|
878
968
|
return node.next ? [{ kind: "next", target: node.next }] : [];
|
|
879
969
|
case "end":
|
|
880
970
|
return [];
|
|
971
|
+
default:
|
|
972
|
+
return [];
|
|
881
973
|
}
|
|
882
974
|
}
|
|
883
975
|
function validateNode(graph, node, issues) {
|
|
@@ -971,906 +1063,15 @@ function validateNode(graph, node, issues) {
|
|
|
971
1063
|
}
|
|
972
1064
|
}
|
|
973
1065
|
|
|
974
|
-
// src/daemon.ts
|
|
975
|
-
import { createHash as createHash2 } from "crypto";
|
|
976
|
-
|
|
977
|
-
// src/secrets.ts
|
|
978
|
-
class SecretsGateError extends Error {
|
|
979
|
-
constructor(message) {
|
|
980
|
-
super(message);
|
|
981
|
-
this.name = "SecretsGateError";
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
var p = (...parts) => parts.join("");
|
|
985
|
-
var VALUE_PATTERNS = [
|
|
986
|
-
{ detector: "anthropic_api_key", re: new RegExp(p("sk-", "ant-") + "[A-Za-z0-9_-]{10,}") },
|
|
987
|
-
{ detector: "openai_api_key", re: new RegExp(p("sk-", "proj-") + "[A-Za-z0-9_-]{10,}") },
|
|
988
|
-
{ detector: "npm_token", re: /npm_[A-Za-z0-9]{20,}/ },
|
|
989
|
-
{ detector: "github_token", re: /gh[pousr]_[A-Za-z0-9]{20,}/ },
|
|
990
|
-
{ detector: "aws_access_key", re: /AKIA[0-9A-Z]{16}/ },
|
|
991
|
-
{ detector: "google_api_key", re: /AIza[0-9A-Za-z_-]{20,}/ },
|
|
992
|
-
{ detector: "xai_api_key", re: new RegExp(p("xai-") + "[A-Za-z0-9]{20,}") },
|
|
993
|
-
{ detector: "codex_token", re: new RegExp(p("ctx7", "sk-") + "[A-Za-z0-9]{10,}") },
|
|
994
|
-
{ detector: "private_key_block", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/ },
|
|
995
|
-
{ detector: "bearer_token", re: /Bearer [A-Za-z0-9._~+/=-]{20,}/ }
|
|
996
|
-
];
|
|
997
|
-
var CREDENTIAL_KEY_RE = /^(token|api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|secret[_-]?key|refresh[_-]?token|session[_-]?token|bot[_-]?token)$/i;
|
|
998
|
-
function scanString(value, path, findings) {
|
|
999
|
-
for (const { detector, re } of VALUE_PATTERNS) {
|
|
1000
|
-
if (re.test(value)) {
|
|
1001
|
-
findings.push({ path, detector });
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
function isTrivial(value) {
|
|
1007
|
-
const v = value.trim();
|
|
1008
|
-
if (v.length === 0)
|
|
1009
|
-
return true;
|
|
1010
|
-
const upper = v.toUpperCase();
|
|
1011
|
-
if (upper.startsWith("REPLACE") || upper.startsWith("CHANGE") || upper.startsWith("YOUR_") || upper.startsWith("EXAMPLE") || upper.startsWith("PLACEHOLDER"))
|
|
1012
|
-
return true;
|
|
1013
|
-
if (v.length < 8)
|
|
1014
|
-
return true;
|
|
1015
|
-
return false;
|
|
1016
|
-
}
|
|
1017
|
-
function scanForSecrets(value, path = "$") {
|
|
1018
|
-
const findings = [];
|
|
1019
|
-
if (value === null || value === undefined)
|
|
1020
|
-
return findings;
|
|
1021
|
-
if (typeof value === "string") {
|
|
1022
|
-
scanString(value, path, findings);
|
|
1023
|
-
return findings;
|
|
1024
|
-
}
|
|
1025
|
-
if (Array.isArray(value)) {
|
|
1026
|
-
value.forEach((item, index) => {
|
|
1027
|
-
findings.push(...scanForSecrets(item, `${path}[${index}]`));
|
|
1028
|
-
});
|
|
1029
|
-
return findings;
|
|
1030
|
-
}
|
|
1031
|
-
if (typeof value === "object") {
|
|
1032
|
-
const record = value;
|
|
1033
|
-
for (const [key, child] of Object.entries(record)) {
|
|
1034
|
-
if (CREDENTIAL_KEY_RE.test(key) && typeof child === "string" && !isTrivial(child)) {
|
|
1035
|
-
findings.push({ path: `${path}.${key}`, detector: "credential_key_field" });
|
|
1036
|
-
} else {
|
|
1037
|
-
findings.push(...scanForSecrets(child, `${path}.${key}`));
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
return findings;
|
|
1042
|
-
}
|
|
1043
|
-
function assertNoSecrets(value, what) {
|
|
1044
|
-
const findings = scanForSecrets(value);
|
|
1045
|
-
if (findings.length === 0)
|
|
1046
|
-
return;
|
|
1047
|
-
const detail = findings.map((f) => `${f.path} (${f.detector})`).join(", ");
|
|
1048
|
-
throw new SecretsGateError(`secrets write-gate refused to persist ${what}: credential-shaped value at ${detail}`);
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// src/session.ts
|
|
1052
|
-
import { createHash } from "crypto";
|
|
1053
|
-
var DEFAULT_MAX_ATTEMPTS = 3;
|
|
1054
|
-
function repairTornRuns(store, wal, opts = {}) {
|
|
1055
|
-
const maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
1056
|
-
const replay = wal.replay();
|
|
1057
|
-
const liveClaims = replay.liveClaims(opts.now);
|
|
1058
|
-
const report = { interrupted: 0, requeued: 0, failed: 0 };
|
|
1059
|
-
const running = store.listRuns({ status: "running", limit: 1000 });
|
|
1060
|
-
for (const run of running) {
|
|
1061
|
-
if (liveClaims.has(run.id))
|
|
1062
|
-
continue;
|
|
1063
|
-
report.interrupted++;
|
|
1064
|
-
const nodes = store.listRunNodes(run.id);
|
|
1065
|
-
if (run.attempts < maxAttempts) {
|
|
1066
|
-
store.bumpAttempts(run.id);
|
|
1067
|
-
store.setRunStatus(run.id, "interrupted", {
|
|
1068
|
-
error: `torn run interrupted after claim loss (attempt ${run.attempts + 1}/${maxAttempts})`
|
|
1069
|
-
});
|
|
1070
|
-
for (const node of nodes) {
|
|
1071
|
-
if (node.status === "running") {
|
|
1072
|
-
store.setRunNodeStatus(node.id, "pending", { error: undefined });
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
report.requeued++;
|
|
1076
|
-
} else {
|
|
1077
|
-
store.setRunStatus(run.id, "failed", {
|
|
1078
|
-
error: `torn run failed after ${maxAttempts} interrupted attempts`
|
|
1079
|
-
});
|
|
1080
|
-
for (const node of nodes) {
|
|
1081
|
-
if (node.status === "running") {
|
|
1082
|
-
store.setRunNodeStatus(node.id, "failed", { error: "run interrupted beyond retry budget" });
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
report.failed++;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
return report;
|
|
1089
|
-
}
|
|
1090
|
-
function inputHash(payload) {
|
|
1091
|
-
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
1092
|
-
}
|
|
1093
|
-
function memoKey(graphName, nodeId, input) {
|
|
1094
|
-
return `${graphName}:${nodeId}:${inputHash(input)}`;
|
|
1095
|
-
}
|
|
1096
|
-
function tryMemoHit(store, graphName, nodeId, input) {
|
|
1097
|
-
const key = memoKey(graphName, nodeId, input);
|
|
1098
|
-
const memo = store.memoGet(key);
|
|
1099
|
-
if (!memo)
|
|
1100
|
-
return;
|
|
1101
|
-
store.memoHit(key);
|
|
1102
|
-
return memo;
|
|
1103
|
-
}
|
|
1104
|
-
function recordMemo(store, graphName, nodeId, input, output) {
|
|
1105
|
-
const key = memoKey(graphName, nodeId, input);
|
|
1106
|
-
store.memoPut(key, graphName, nodeId, inputHash(input), JSON.stringify(output));
|
|
1107
|
-
}
|
|
1108
|
-
function findRunByIdempotencyKey(store, key) {
|
|
1109
|
-
const runs = store.listRuns({ limit: 1000 });
|
|
1110
|
-
for (const run of runs) {
|
|
1111
|
-
try {
|
|
1112
|
-
const context = JSON.parse(run.contextJson);
|
|
1113
|
-
if (context?.__wf?.idempotencyKey === key)
|
|
1114
|
-
return run;
|
|
1115
|
-
} catch {}
|
|
1116
|
-
}
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
// src/daemon.ts
|
|
1121
|
-
init_types();
|
|
1122
|
-
var DEFAULT_LEASE_TTL_MS = 120000;
|
|
1123
|
-
var DEFAULT_MAX_ATTEMPTS2 = 3;
|
|
1124
|
-
var DEFAULT_MAX_DISPATCH = 4;
|
|
1125
|
-
var DEFAULT_MAX_STEPS = 25;
|
|
1126
|
-
var DEFAULT_TIMEOUT_MS = 120000;
|
|
1127
|
-
function nowIso() {
|
|
1128
|
-
return new Date().toISOString();
|
|
1129
|
-
}
|
|
1130
|
-
function stripRunState(context) {
|
|
1131
|
-
const { __wf: _wf, steps: _steps, ...user } = context;
|
|
1132
|
-
return user;
|
|
1133
|
-
}
|
|
1134
|
-
function runCommand(job) {
|
|
1135
|
-
if (!job.command) {
|
|
1136
|
-
return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "no command" };
|
|
1137
|
-
}
|
|
1138
|
-
const started = Date.now();
|
|
1139
|
-
const timeoutMs = job.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1140
|
-
const proc = Bun.spawnSync({
|
|
1141
|
-
cmd: ["/bin/sh", "-c", job.command],
|
|
1142
|
-
cwd: job.cwd,
|
|
1143
|
-
env: { ...process.env, ...job.env ?? {} },
|
|
1144
|
-
stdout: "pipe",
|
|
1145
|
-
stderr: "pipe",
|
|
1146
|
-
timeout: timeoutMs
|
|
1147
|
-
});
|
|
1148
|
-
const durationMs = Date.now() - started;
|
|
1149
|
-
const stdout = proc.stdout ? new TextDecoder().decode(proc.stdout) : "";
|
|
1150
|
-
const stderr = proc.stderr ? new TextDecoder().decode(proc.stderr) : "";
|
|
1151
|
-
const output = stderr ? `${stdout}
|
|
1152
|
-
${stderr}`.trim() : stdout.trim();
|
|
1153
|
-
const exitCode = proc.exitCode ?? (proc.signalCode ? 1 : 1);
|
|
1154
|
-
return { ok: exitCode === 0, exitCode, output, durationMs };
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
class WorkflowsDaemon {
|
|
1158
|
-
store;
|
|
1159
|
-
wal;
|
|
1160
|
-
worker;
|
|
1161
|
-
leaseTtlMs;
|
|
1162
|
-
maxAttempts;
|
|
1163
|
-
maxDispatchPerCycle;
|
|
1164
|
-
maxStepsPerCycle;
|
|
1165
|
-
time;
|
|
1166
|
-
laneRunner;
|
|
1167
|
-
env;
|
|
1168
|
-
leases = new Map;
|
|
1169
|
-
fencingCounter = new Map;
|
|
1170
|
-
graphCache = new Map;
|
|
1171
|
-
isolatedRunId = null;
|
|
1172
|
-
constructor(store, wal, options = {}) {
|
|
1173
|
-
this.store = store;
|
|
1174
|
-
this.wal = wal;
|
|
1175
|
-
this.worker = options.worker ?? "local";
|
|
1176
|
-
this.leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
|
|
1177
|
-
this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS2;
|
|
1178
|
-
this.maxDispatchPerCycle = options.maxDispatchPerCycle ?? DEFAULT_MAX_DISPATCH;
|
|
1179
|
-
this.maxStepsPerCycle = options.maxStepsPerCycle ?? DEFAULT_MAX_STEPS;
|
|
1180
|
-
this.time = options.time ?? Date.now;
|
|
1181
|
-
this.env = options.env ?? {};
|
|
1182
|
-
this.laneRunner = options.laneRunner ?? defaultLaneRunner;
|
|
1183
|
-
this.isolatedRunId = options.isolatedRunId ?? null;
|
|
1184
|
-
this.replayRegisteredGraphs();
|
|
1185
|
-
}
|
|
1186
|
-
isolateRun(runId) {
|
|
1187
|
-
this.isolatedRunId = runId;
|
|
1188
|
-
}
|
|
1189
|
-
replayRegisteredGraphs() {
|
|
1190
|
-
const entries = this.wal.replay().entries;
|
|
1191
|
-
for (const entry of entries) {
|
|
1192
|
-
const op = entry.op;
|
|
1193
|
-
if (op.op === "graph_registered") {
|
|
1194
|
-
this.graphCache.set(op.name, op.graph);
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
liveLeases() {
|
|
1199
|
-
const now = this.time();
|
|
1200
|
-
for (const [runId, lease] of this.leases) {
|
|
1201
|
-
if (lease.expiresAtMs <= now)
|
|
1202
|
-
this.leases.delete(runId);
|
|
1203
|
-
}
|
|
1204
|
-
return this.leases;
|
|
1205
|
-
}
|
|
1206
|
-
claim(runId) {
|
|
1207
|
-
const now = this.time();
|
|
1208
|
-
const existing = this.leases.get(runId);
|
|
1209
|
-
if (existing && existing.worker !== this.worker && existing.expiresAtMs > now) {
|
|
1210
|
-
return null;
|
|
1211
|
-
}
|
|
1212
|
-
const liveClaims = this.wal.replay().liveClaims(now);
|
|
1213
|
-
const foreign = liveClaims.get(runId);
|
|
1214
|
-
if (foreign && foreign.worker !== this.worker && foreign.expiresAtMs > now) {
|
|
1215
|
-
return null;
|
|
1216
|
-
}
|
|
1217
|
-
const fencing = (this.fencingCounter.get(runId) ?? 0) + 1;
|
|
1218
|
-
this.fencingCounter.set(runId, fencing);
|
|
1219
|
-
const lease = { runId, worker: this.worker, expiresAtMs: now + this.leaseTtlMs, fencing };
|
|
1220
|
-
this.leases.set(runId, lease);
|
|
1221
|
-
this.wal.append({
|
|
1222
|
-
op: "claim_acquired",
|
|
1223
|
-
runId,
|
|
1224
|
-
worker: this.worker,
|
|
1225
|
-
expiresAt: new Date(lease.expiresAtMs).toISOString(),
|
|
1226
|
-
fencing,
|
|
1227
|
-
at: nowIso()
|
|
1228
|
-
});
|
|
1229
|
-
return lease;
|
|
1230
|
-
}
|
|
1231
|
-
heartbeat(runId, fencing) {
|
|
1232
|
-
const lease = this.leases.get(runId);
|
|
1233
|
-
if (!lease || lease.fencing !== fencing)
|
|
1234
|
-
return false;
|
|
1235
|
-
lease.expiresAtMs = this.time() + this.leaseTtlMs;
|
|
1236
|
-
return true;
|
|
1237
|
-
}
|
|
1238
|
-
release(runId, fencing) {
|
|
1239
|
-
const lease = this.leases.get(runId);
|
|
1240
|
-
if (!lease || lease.fencing !== fencing)
|
|
1241
|
-
return false;
|
|
1242
|
-
this.leases.delete(runId);
|
|
1243
|
-
this.wal.append({ op: "claim_released", runId, fencing, at: nowIso() });
|
|
1244
|
-
return true;
|
|
1245
|
-
}
|
|
1246
|
-
async reap() {
|
|
1247
|
-
const report = { expired: 0, interrupted: 0, requeued: 0, failed: 0, dispatched: 0, advanced: 0, completed: 0 };
|
|
1248
|
-
const now = this.time();
|
|
1249
|
-
for (const [runId, lease] of this.leases) {
|
|
1250
|
-
if (lease.expiresAtMs <= now) {
|
|
1251
|
-
this.leases.delete(runId);
|
|
1252
|
-
report.expired++;
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
if (this.isolatedRunId === null) {
|
|
1256
|
-
const repair = repairTornRuns(this.store, this.wal, { maxAttempts: this.maxAttempts, now });
|
|
1257
|
-
report.interrupted += repair.interrupted;
|
|
1258
|
-
report.requeued += repair.requeued;
|
|
1259
|
-
report.failed += repair.failed;
|
|
1260
|
-
}
|
|
1261
|
-
const pending = this.store.listRuns({ status: "pending", limit: this.maxDispatchPerCycle }).concat(this.store.listRuns({ status: "interrupted", limit: this.maxDispatchPerCycle })).sort((a, b) => a.createdAt === b.createdAt ? a.id < b.id ? 1 : -1 : a.createdAt < b.createdAt ? 1 : -1).slice(0, this.maxDispatchPerCycle);
|
|
1262
|
-
for (const run of pending) {
|
|
1263
|
-
if (this.isolatedRunId !== null && run.id !== this.isolatedRunId)
|
|
1264
|
-
continue;
|
|
1265
|
-
const lease = this.claim(run.id);
|
|
1266
|
-
if (!lease)
|
|
1267
|
-
continue;
|
|
1268
|
-
this.store.setRunStatus(run.id, "running");
|
|
1269
|
-
this.wal.append({ op: "run_started", runId: run.id, at: nowIso() });
|
|
1270
|
-
report.dispatched++;
|
|
1271
|
-
}
|
|
1272
|
-
const running = this.store.listRuns({ status: "running", limit: this.maxStepsPerCycle });
|
|
1273
|
-
let steps = 0;
|
|
1274
|
-
for (const run of running) {
|
|
1275
|
-
if (this.isolatedRunId !== null && run.id !== this.isolatedRunId)
|
|
1276
|
-
continue;
|
|
1277
|
-
if (steps >= this.maxStepsPerCycle)
|
|
1278
|
-
break;
|
|
1279
|
-
const outcome = await this.advanceRun(run.id);
|
|
1280
|
-
steps++;
|
|
1281
|
-
if (outcome.kind === "completed")
|
|
1282
|
-
report.completed++;
|
|
1283
|
-
if (outcome.kind === "failed")
|
|
1284
|
-
report.failed++;
|
|
1285
|
-
if (outcome.kind === "advanced")
|
|
1286
|
-
report.advanced++;
|
|
1287
|
-
}
|
|
1288
|
-
return report;
|
|
1289
|
-
}
|
|
1290
|
-
startRun(graph, context) {
|
|
1291
|
-
const validation = validateGraph(graph);
|
|
1292
|
-
if (!validation.ok) {
|
|
1293
|
-
throw new Error(`graph validation failed: ${validation.issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
|
|
1294
|
-
}
|
|
1295
|
-
assertNoSecrets(context, "run context");
|
|
1296
|
-
assertNoSecrets(graph, "graph definition");
|
|
1297
|
-
const existing = this.graphCache.get(graph.name);
|
|
1298
|
-
const serialized = JSON.stringify(graph);
|
|
1299
|
-
if (!existing || JSON.stringify(existing) !== serialized) {
|
|
1300
|
-
this.graphCache.set(graph.name, graph);
|
|
1301
|
-
this.wal.append({ op: "graph_registered", name: graph.name, version: graph.version, graph, at: nowIso() });
|
|
1302
|
-
} else {
|
|
1303
|
-
this.graphCache.set(graph.name, existing);
|
|
1304
|
-
}
|
|
1305
|
-
return this.store.createRun({ graphName: graph.name, graphVersion: graph.version, context: context ?? {} });
|
|
1306
|
-
}
|
|
1307
|
-
async advanceRun(runId) {
|
|
1308
|
-
const run = this.store.getRun(runId);
|
|
1309
|
-
if (!run || run.status !== "running")
|
|
1310
|
-
return { kind: "noop" };
|
|
1311
|
-
const lease = this.leases.get(runId);
|
|
1312
|
-
if (!lease || lease.expiresAtMs <= this.time())
|
|
1313
|
-
return { kind: "blocked" };
|
|
1314
|
-
const graph = this.loadGraph(run.graphName);
|
|
1315
|
-
if (!graph) {
|
|
1316
|
-
this.failRun(run, `graph ${run.graphName} not found in daemon graph cache`);
|
|
1317
|
-
return { kind: "failed", error: "graph not found" };
|
|
1318
|
-
}
|
|
1319
|
-
const context = JSON.parse(run.contextJson);
|
|
1320
|
-
const rawWf = context.__wf ?? {};
|
|
1321
|
-
const wf = {
|
|
1322
|
-
...rawWf,
|
|
1323
|
-
cursor: typeof rawWf.cursor === "string" ? rawWf.cursor : undefined,
|
|
1324
|
-
loops: rawWf.loops ?? {},
|
|
1325
|
-
completedLoops: rawWf.completedLoops ?? {}
|
|
1326
|
-
};
|
|
1327
|
-
try {
|
|
1328
|
-
const cursor = wf.cursor;
|
|
1329
|
-
const node = cursor === undefined ? graph.nodes.find((n) => n.type === "start") : findNode(graph, cursor);
|
|
1330
|
-
if (!node) {
|
|
1331
|
-
this.failRun(run, `cursor ${cursor ?? "<start>"} resolves to no node`);
|
|
1332
|
-
return { kind: "failed", error: "no node at cursor" };
|
|
1333
|
-
}
|
|
1334
|
-
switch (node.type) {
|
|
1335
|
-
case "start": {
|
|
1336
|
-
wf.cursor = node.next;
|
|
1337
|
-
this.persistContext(run, context, wf);
|
|
1338
|
-
return { kind: "advanced", nodeId: node.id };
|
|
1339
|
-
}
|
|
1340
|
-
case "step": {
|
|
1341
|
-
return await this.executeStep(run, graph, context, wf, node);
|
|
1342
|
-
}
|
|
1343
|
-
case "decision": {
|
|
1344
|
-
const value = evaluateExpr(node.condition, this.conditionScope(context));
|
|
1345
|
-
const next = value ? node.then : node.else;
|
|
1346
|
-
if (!next) {
|
|
1347
|
-
this.failRun(run, `decision ${node.id} evaluated ${value} but no ${value ? "then" : "else"} edge`);
|
|
1348
|
-
return { kind: "failed", error: "missing decision edge" };
|
|
1349
|
-
}
|
|
1350
|
-
wf.cursor = next;
|
|
1351
|
-
this.persistContext(run, context, wf);
|
|
1352
|
-
return { kind: "advanced", nodeId: node.id };
|
|
1353
|
-
}
|
|
1354
|
-
case "while": {
|
|
1355
|
-
return this.executeWhile(run, graph, context, wf, node);
|
|
1356
|
-
}
|
|
1357
|
-
case "end": {
|
|
1358
|
-
this.completeRun(run, context);
|
|
1359
|
-
return { kind: "completed" };
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
} catch (err) {
|
|
1363
|
-
if (err instanceof ExprEvalError || err instanceof ExprSyntaxError) {
|
|
1364
|
-
this.failRun(run, `condition error at ${wf.cursor ?? "<start>"}: ${err.message}`);
|
|
1365
|
-
return { kind: "failed", error: err.message };
|
|
1366
|
-
}
|
|
1367
|
-
throw err;
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
loadGraph(name) {
|
|
1371
|
-
return this.graphCache.get(name);
|
|
1372
|
-
}
|
|
1373
|
-
conditionScope(context) {
|
|
1374
|
-
return { ...context, steps: context.steps ?? {} };
|
|
1375
|
-
}
|
|
1376
|
-
persistContext(run, context, wf) {
|
|
1377
|
-
context.__wf = wf;
|
|
1378
|
-
this.store.setRunContext(run.id, context);
|
|
1379
|
-
}
|
|
1380
|
-
async executeStep(run, graph, context, wf, node) {
|
|
1381
|
-
const nodeId = node.id;
|
|
1382
|
-
const existing = this.store.listRunNodes(run.id).find((n) => n.nodeId === nodeId);
|
|
1383
|
-
const nodeRow = existing ?? this.store.createRunNode({ runId: run.id, nodeId, lane: node.lane ?? "claude" });
|
|
1384
|
-
if (node.memo) {
|
|
1385
|
-
const memoInput = { command: node.command, prompt: node.prompt, context: stripRunState(context) };
|
|
1386
|
-
const hit = tryMemoHit(this.store, run.graphName, nodeId, memoInput);
|
|
1387
|
-
if (hit) {
|
|
1388
|
-
const cached = JSON.parse(hit.outputJson);
|
|
1389
|
-
this.store.setRunNodeStatus(nodeRow.id, "completed", { exitCode: Number(cached.exitCode ?? 0), output: { ...cached, memoHit: true } });
|
|
1390
|
-
this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "completed", at: nowIso() });
|
|
1391
|
-
this.recordStepResult(context, nodeId, { ...cached, memoHit: true });
|
|
1392
|
-
return this.afterNode(run, context, wf, node.next);
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
if (nodeRow.status !== "running") {
|
|
1396
|
-
this.store.setRunNodeStatus(nodeRow.id, "running");
|
|
1397
|
-
this.wal.append({ op: "node_started", runId: run.id, nodeId, at: nowIso() });
|
|
1398
|
-
}
|
|
1399
|
-
const job = {
|
|
1400
|
-
lane: node.lane ?? "claude",
|
|
1401
|
-
prompt: node.prompt,
|
|
1402
|
-
command: node.command,
|
|
1403
|
-
timeoutMs: node.timeoutMs,
|
|
1404
|
-
env: this.env
|
|
1405
|
-
};
|
|
1406
|
-
let result;
|
|
1407
|
-
try {
|
|
1408
|
-
result = await this.laneRunner(job);
|
|
1409
|
-
} catch (err) {
|
|
1410
|
-
result = { ok: false, exitCode: 1, output: "", durationMs: 0, error: String(err instanceof Error ? err.message : err) };
|
|
1411
|
-
}
|
|
1412
|
-
const output = { ok: result.ok, exitCode: result.exitCode, output: result.output, durationMs: result.durationMs };
|
|
1413
|
-
if (!result.ok) {
|
|
1414
|
-
const shouldRetry = nodeRow.attempts < (node.maxRetries ?? 0);
|
|
1415
|
-
this.store.bumpAttemptsNode(nodeRow.id);
|
|
1416
|
-
if (shouldRetry) {
|
|
1417
|
-
this.store.setRunNodeStatus(nodeRow.id, "pending", { error: result.error ?? `exit ${result.exitCode}` });
|
|
1418
|
-
return { kind: "advanced", nodeId };
|
|
1419
|
-
}
|
|
1420
|
-
const error = result.error ?? `step ${nodeId} failed with exit ${result.exitCode}`;
|
|
1421
|
-
this.store.setRunNodeStatus(nodeRow.id, "failed", { exitCode: result.exitCode, error });
|
|
1422
|
-
this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "failed", at: nowIso() });
|
|
1423
|
-
this.failRun(run, error);
|
|
1424
|
-
return { kind: "failed", error };
|
|
1425
|
-
}
|
|
1426
|
-
try {
|
|
1427
|
-
assertNoSecrets(output, `node ${nodeId} output`);
|
|
1428
|
-
} catch (err) {
|
|
1429
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1430
|
-
this.store.setRunNodeStatus(nodeRow.id, "failed", { error: message });
|
|
1431
|
-
this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "failed", at: nowIso() });
|
|
1432
|
-
this.failRun(run, message);
|
|
1433
|
-
return { kind: "failed", error: message };
|
|
1434
|
-
}
|
|
1435
|
-
this.store.setRunNodeStatus(nodeRow.id, "completed", { exitCode: result.exitCode, output });
|
|
1436
|
-
this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "completed", at: nowIso() });
|
|
1437
|
-
if (node.memo) {
|
|
1438
|
-
recordMemo(this.store, run.graphName, nodeId, { command: node.command, prompt: node.prompt, context: stripRunState(context) }, output);
|
|
1439
|
-
this.wal.append({ op: "memo_set", key: `${run.graphName}:${nodeId}:${createHash2("sha256").update(JSON.stringify({ command: node.command, prompt: node.prompt, context: stripRunState(context) })).digest("hex")}`, at: nowIso() });
|
|
1440
|
-
}
|
|
1441
|
-
this.recordStepResult(context, nodeId, output);
|
|
1442
|
-
return this.afterNode(run, context, wf, node.next);
|
|
1443
|
-
}
|
|
1444
|
-
afterNode(run, context, wf, next) {
|
|
1445
|
-
if (next) {
|
|
1446
|
-
wf.cursor = next;
|
|
1447
|
-
this.persistContext(run, context, wf);
|
|
1448
|
-
return { kind: "advanced", nodeId: wf.cursor };
|
|
1449
|
-
}
|
|
1450
|
-
const cursor = wf.cursor;
|
|
1451
|
-
const graph = this.loadGraph(run.graphName);
|
|
1452
|
-
const whileNode = graph.nodes.find((n) => n.type === "while" && n.body.includes(cursor ?? ""));
|
|
1453
|
-
if (whileNode) {
|
|
1454
|
-
const index = whileNode.body.indexOf(cursor ?? "");
|
|
1455
|
-
if (index >= 0 && index < whileNode.body.length - 1) {
|
|
1456
|
-
wf.cursor = whileNode.body[index + 1];
|
|
1457
|
-
this.persistContext(run, context, wf);
|
|
1458
|
-
return { kind: "advanced", nodeId: wf.cursor };
|
|
1459
|
-
}
|
|
1460
|
-
wf.loops[whileNode.id] = (wf.loops[whileNode.id] ?? 0) + 1;
|
|
1461
|
-
wf.cursor = whileNode.id;
|
|
1462
|
-
this.persistContext(run, context, wf);
|
|
1463
|
-
return { kind: "advanced", nodeId: whileNode.id };
|
|
1464
|
-
}
|
|
1465
|
-
this.completeRun(run, context);
|
|
1466
|
-
return { kind: "completed" };
|
|
1467
|
-
}
|
|
1468
|
-
executeWhile(run, graph, context, wf, node) {
|
|
1469
|
-
const iteration = wf.loops[node.id] ?? 0;
|
|
1470
|
-
if (iteration >= node.maxIterations) {
|
|
1471
|
-
const error = `while node ${node.id} exceeded maxIterations (${node.maxIterations})`;
|
|
1472
|
-
this.failRun(run, error);
|
|
1473
|
-
return { kind: "failed", error };
|
|
1474
|
-
}
|
|
1475
|
-
const scope = { ...this.conditionScope(context), i: iteration };
|
|
1476
|
-
const keepGoing = evaluateExpr(node.condition, scope);
|
|
1477
|
-
if (!keepGoing) {
|
|
1478
|
-
wf.completedLoops[node.id] = iteration;
|
|
1479
|
-
delete wf.loops[node.id];
|
|
1480
|
-
wf.cursor = node.next;
|
|
1481
|
-
this.persistContext(run, context, wf);
|
|
1482
|
-
return { kind: "advanced", nodeId: node.id };
|
|
1483
|
-
}
|
|
1484
|
-
wf.cursor = node.body[0];
|
|
1485
|
-
this.persistContext(run, context, wf);
|
|
1486
|
-
return { kind: "advanced", nodeId: node.id };
|
|
1487
|
-
}
|
|
1488
|
-
recordStepResult(context, nodeId, output) {
|
|
1489
|
-
const steps = context.steps ?? {};
|
|
1490
|
-
steps[nodeId] = output;
|
|
1491
|
-
context.steps = steps;
|
|
1492
|
-
}
|
|
1493
|
-
completeRun(run, context) {
|
|
1494
|
-
const result = {
|
|
1495
|
-
steps: context.steps ?? {},
|
|
1496
|
-
iterations: context.__wf?.completedLoops ?? {}
|
|
1497
|
-
};
|
|
1498
|
-
try {
|
|
1499
|
-
assertNoSecrets(result, "run result");
|
|
1500
|
-
} catch (err) {
|
|
1501
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1502
|
-
this.failRun(run, message);
|
|
1503
|
-
return;
|
|
1504
|
-
}
|
|
1505
|
-
this.store.setRunStatus(run.id, "completed", { result, finishedAt: nowIso(), error: null });
|
|
1506
|
-
this.wal.append({ op: "run_finished", runId: run.id, status: "completed", at: nowIso() });
|
|
1507
|
-
const lease = this.leases.get(run.id);
|
|
1508
|
-
if (lease)
|
|
1509
|
-
this.release(run.id, lease.fencing);
|
|
1510
|
-
}
|
|
1511
|
-
failRun(run, error) {
|
|
1512
|
-
this.store.setRunStatus(run.id, "failed", { error, finishedAt: nowIso() });
|
|
1513
|
-
this.wal.append({ op: "run_finished", runId: run.id, status: "failed", at: nowIso() });
|
|
1514
|
-
const lease = this.leases.get(run.id);
|
|
1515
|
-
if (lease)
|
|
1516
|
-
this.release(run.id, lease.fencing);
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
var defaultLaneRunner = async (job) => {
|
|
1520
|
-
if (job.command)
|
|
1521
|
-
return runCommand(job);
|
|
1522
|
-
const { runLaneJob: runLaneJob2 } = await Promise.resolve().then(() => (init_lanes(), exports_lanes));
|
|
1523
|
-
return runLaneJob2(job);
|
|
1524
|
-
};
|
|
1525
|
-
async function runGraphToCompletion(store, wal, graph, context, options = {}) {
|
|
1526
|
-
const daemon = new WorkflowsDaemon(store, wal, {
|
|
1527
|
-
worker: "local",
|
|
1528
|
-
laneRunner: options.laneRunner,
|
|
1529
|
-
time: options.time,
|
|
1530
|
-
env: options.env,
|
|
1531
|
-
isolatedRunId: options.isolatedRunId
|
|
1532
|
-
});
|
|
1533
|
-
const run = daemon.startRun(graph, context);
|
|
1534
|
-
daemon.isolateRun(run.id);
|
|
1535
|
-
const maxCycles = options.maxCycles ?? 500;
|
|
1536
|
-
for (let cycle = 0;cycle < maxCycles; cycle++) {
|
|
1537
|
-
const report = await daemon.reap();
|
|
1538
|
-
const current = store.getRun(run.id);
|
|
1539
|
-
if (current.status !== "running")
|
|
1540
|
-
return current;
|
|
1541
|
-
if (report.advanced === 0 && report.dispatched === 0) {
|
|
1542
|
-
return store.getRun(run.id);
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
return store.getRun(run.id);
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
1066
|
// src/agent-tools.ts
|
|
1549
1067
|
init_lanes();
|
|
1550
1068
|
|
|
1551
|
-
// src/
|
|
1552
|
-
import { Database } from "bun:sqlite";
|
|
1553
|
-
import { mkdirSync } from "fs";
|
|
1554
|
-
import { join } from "path";
|
|
1555
|
-
import { randomUUID } from "crypto";
|
|
1556
|
-
function nowIso2() {
|
|
1557
|
-
return new Date().toISOString();
|
|
1558
|
-
}
|
|
1559
|
-
function openStore(dataDir) {
|
|
1560
|
-
mkdirSync(dataDir, { recursive: true });
|
|
1561
|
-
const db = new Database(join(dataDir, "workflows.db"));
|
|
1562
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
1563
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
1564
|
-
db.exec(`
|
|
1565
|
-
CREATE TABLE IF NOT EXISTS runs (
|
|
1566
|
-
id TEXT PRIMARY KEY,
|
|
1567
|
-
graph_name TEXT NOT NULL,
|
|
1568
|
-
graph_version TEXT NOT NULL,
|
|
1569
|
-
status TEXT NOT NULL,
|
|
1570
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
1571
|
-
context_json TEXT NOT NULL,
|
|
1572
|
-
result_json TEXT,
|
|
1573
|
-
error TEXT,
|
|
1574
|
-
started_at TEXT,
|
|
1575
|
-
finished_at TEXT,
|
|
1576
|
-
created_at TEXT NOT NULL,
|
|
1577
|
-
updated_at TEXT NOT NULL
|
|
1578
|
-
);
|
|
1579
|
-
CREATE TABLE IF NOT EXISTS run_nodes (
|
|
1580
|
-
id TEXT PRIMARY KEY,
|
|
1581
|
-
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
1582
|
-
node_id TEXT NOT NULL,
|
|
1583
|
-
status TEXT NOT NULL,
|
|
1584
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
1585
|
-
lane TEXT,
|
|
1586
|
-
exit_code INTEGER,
|
|
1587
|
-
output_json TEXT,
|
|
1588
|
-
error TEXT,
|
|
1589
|
-
started_at TEXT,
|
|
1590
|
-
finished_at TEXT
|
|
1591
|
-
);
|
|
1592
|
-
CREATE TABLE IF NOT EXISTS memos (
|
|
1593
|
-
key TEXT PRIMARY KEY,
|
|
1594
|
-
graph_name TEXT NOT NULL,
|
|
1595
|
-
node_id TEXT NOT NULL,
|
|
1596
|
-
input_hash TEXT NOT NULL,
|
|
1597
|
-
output_json TEXT NOT NULL,
|
|
1598
|
-
hit_count INTEGER NOT NULL DEFAULT 0,
|
|
1599
|
-
created_at TEXT NOT NULL
|
|
1600
|
-
);
|
|
1601
|
-
CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
|
|
1602
|
-
CREATE INDEX IF NOT EXISTS idx_runs_created ON runs(created_at);
|
|
1603
|
-
CREATE INDEX IF NOT EXISTS idx_run_nodes_run ON run_nodes(run_id);
|
|
1604
|
-
`);
|
|
1605
|
-
const rowToRun = (r) => ({
|
|
1606
|
-
id: String(r.id),
|
|
1607
|
-
graphName: String(r.graph_name),
|
|
1608
|
-
graphVersion: String(r.graph_version),
|
|
1609
|
-
status: r.status,
|
|
1610
|
-
attempts: Number(r.attempts),
|
|
1611
|
-
contextJson: String(r.context_json),
|
|
1612
|
-
resultJson: r.result_json === null ? null : String(r.result_json),
|
|
1613
|
-
error: r.error === null ? null : String(r.error),
|
|
1614
|
-
startedAt: r.started_at === null ? null : String(r.started_at),
|
|
1615
|
-
finishedAt: r.finished_at === null ? null : String(r.finished_at),
|
|
1616
|
-
createdAt: String(r.created_at),
|
|
1617
|
-
updatedAt: String(r.updated_at)
|
|
1618
|
-
});
|
|
1619
|
-
const rowToNode = (r) => ({
|
|
1620
|
-
id: String(r.id),
|
|
1621
|
-
runId: String(r.run_id),
|
|
1622
|
-
nodeId: String(r.node_id),
|
|
1623
|
-
status: r.status,
|
|
1624
|
-
attempts: Number(r.attempts),
|
|
1625
|
-
lane: r.lane === null ? null : String(r.lane),
|
|
1626
|
-
exitCode: r.exit_code === null ? null : Number(r.exit_code),
|
|
1627
|
-
outputJson: r.output_json === null ? null : String(r.output_json),
|
|
1628
|
-
error: r.error === null ? null : String(r.error),
|
|
1629
|
-
startedAt: r.started_at === null ? null : String(r.started_at),
|
|
1630
|
-
finishedAt: r.finished_at === null ? null : String(r.finished_at)
|
|
1631
|
-
});
|
|
1632
|
-
const rowToMemo = (r) => ({
|
|
1633
|
-
key: String(r.key),
|
|
1634
|
-
graphName: String(r.graph_name),
|
|
1635
|
-
nodeId: String(r.node_id),
|
|
1636
|
-
inputHash: String(r.input_hash),
|
|
1637
|
-
outputJson: String(r.output_json),
|
|
1638
|
-
hitCount: Number(r.hit_count),
|
|
1639
|
-
createdAt: String(r.created_at)
|
|
1640
|
-
});
|
|
1641
|
-
return {
|
|
1642
|
-
close() {
|
|
1643
|
-
db.close();
|
|
1644
|
-
},
|
|
1645
|
-
listTables() {
|
|
1646
|
-
const rows = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all();
|
|
1647
|
-
return rows.map((r) => r.name);
|
|
1648
|
-
},
|
|
1649
|
-
createRun(input) {
|
|
1650
|
-
const id = randomUUID();
|
|
1651
|
-
const now = nowIso2();
|
|
1652
|
-
db.query(`INSERT INTO runs (id, graph_name, graph_version, status, attempts, context_json, created_at, updated_at)
|
|
1653
|
-
VALUES (?, ?, ?, 'pending', 0, ?, ?, ?)`).run(id, input.graphName, input.graphVersion, JSON.stringify(input.context ?? {}), now, now);
|
|
1654
|
-
return this.getRun(id);
|
|
1655
|
-
},
|
|
1656
|
-
getRun(id) {
|
|
1657
|
-
const row = db.query(`SELECT * FROM runs WHERE id = ?`).get(id);
|
|
1658
|
-
return row ? rowToRun(row) : undefined;
|
|
1659
|
-
},
|
|
1660
|
-
listRuns(opts = {}) {
|
|
1661
|
-
const limit = Math.min(opts.limit ?? 100, 1000);
|
|
1662
|
-
if (opts.status) {
|
|
1663
|
-
const rows2 = db.query(`SELECT * FROM runs WHERE status = ? ORDER BY created_at DESC, id DESC LIMIT ?`).all(opts.status, limit);
|
|
1664
|
-
return rows2.map((r) => rowToRun(r));
|
|
1665
|
-
}
|
|
1666
|
-
const rows = db.query(`SELECT * FROM runs ORDER BY created_at DESC, id DESC LIMIT ?`).all(limit);
|
|
1667
|
-
return rows.map((r) => rowToRun(r));
|
|
1668
|
-
},
|
|
1669
|
-
setRunStatus(id, status, patch = {}) {
|
|
1670
|
-
const now = nowIso2();
|
|
1671
|
-
const existing = this.getRun(id);
|
|
1672
|
-
const resultJson = patch.result !== undefined ? JSON.stringify(patch.result) : existing?.resultJson ?? null;
|
|
1673
|
-
const error = patch.error !== undefined ? patch.error : existing?.error ?? null;
|
|
1674
|
-
const startedAt = patch.startedAt !== undefined ? patch.startedAt : status === "running" && existing?.startedAt === null ? now : existing?.startedAt ?? null;
|
|
1675
|
-
const terminal = status === "completed" || status === "failed" || status === "cancelled" || status === "interrupted";
|
|
1676
|
-
const finishedAt = patch.finishedAt !== undefined ? patch.finishedAt : terminal && existing?.finishedAt === null ? now : existing?.finishedAt ?? null;
|
|
1677
|
-
db.query(`UPDATE runs SET status = ?, result_json = ?, error = ?, started_at = ?, finished_at = ?, updated_at = ? WHERE id = ?`).run(status, resultJson, error, startedAt, finishedAt, now, id);
|
|
1678
|
-
},
|
|
1679
|
-
bumpAttempts(id) {
|
|
1680
|
-
db.query(`UPDATE runs SET attempts = attempts + 1, updated_at = ? WHERE id = ?`).run(nowIso2(), id);
|
|
1681
|
-
},
|
|
1682
|
-
setRunContext(id, context) {
|
|
1683
|
-
db.query(`UPDATE runs SET context_json = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(context), nowIso2(), id);
|
|
1684
|
-
},
|
|
1685
|
-
createRunNode(input) {
|
|
1686
|
-
const id = randomUUID();
|
|
1687
|
-
db.query(`INSERT INTO run_nodes (id, run_id, node_id, status, attempts, lane)
|
|
1688
|
-
VALUES (?, ?, ?, 'pending', 0, ?)`).run(id, input.runId, input.nodeId, input.lane ?? null);
|
|
1689
|
-
return this.getRunNode(id);
|
|
1690
|
-
},
|
|
1691
|
-
getRunNode(id) {
|
|
1692
|
-
const row = db.query(`SELECT * FROM run_nodes WHERE id = ?`).get(id);
|
|
1693
|
-
return row ? rowToNode(row) : undefined;
|
|
1694
|
-
},
|
|
1695
|
-
listRunNodes(runId) {
|
|
1696
|
-
const rows = db.query(`SELECT * FROM run_nodes WHERE run_id = ? ORDER BY rowid ASC`).all(runId);
|
|
1697
|
-
return rows.map((r) => rowToNode(r));
|
|
1698
|
-
},
|
|
1699
|
-
setRunNodeStatus(id, status, patch = {}) {
|
|
1700
|
-
const now = nowIso2();
|
|
1701
|
-
const existing = this.getRunNode(id);
|
|
1702
|
-
if (!existing)
|
|
1703
|
-
return;
|
|
1704
|
-
const exitCode = patch.exitCode !== undefined ? patch.exitCode : existing.exitCode;
|
|
1705
|
-
const outputJson = patch.output !== undefined ? JSON.stringify(patch.output) : existing.outputJson;
|
|
1706
|
-
const error = patch.error !== undefined ? patch.error : existing.error;
|
|
1707
|
-
const startedAt = patch.startedAt !== undefined ? patch.startedAt : status === "running" && existing.startedAt === null ? now : existing.startedAt;
|
|
1708
|
-
const terminal = status === "completed" || status === "failed" || status === "skipped";
|
|
1709
|
-
const finishedAt = patch.finishedAt !== undefined ? patch.finishedAt : terminal && existing.finishedAt === null ? now : existing.finishedAt;
|
|
1710
|
-
db.query(`UPDATE run_nodes SET status = ?, exit_code = ?, output_json = ?, error = ?, started_at = ?, finished_at = ? WHERE id = ?`).run(status, exitCode, outputJson, error, startedAt, finishedAt, id);
|
|
1711
|
-
},
|
|
1712
|
-
bumpAttemptsNode(id) {
|
|
1713
|
-
db.query(`UPDATE run_nodes SET attempts = attempts + 1 WHERE id = ?`).run(id);
|
|
1714
|
-
},
|
|
1715
|
-
memoPut(key, graphName, nodeId, inputHash2, outputJson) {
|
|
1716
|
-
db.query(`INSERT INTO memos (key, graph_name, node_id, input_hash, output_json, hit_count, created_at)
|
|
1717
|
-
VALUES (?, ?, ?, ?, ?, 0, ?)
|
|
1718
|
-
ON CONFLICT(key) DO UPDATE SET output_json = excluded.output_json`).run(key, graphName, nodeId, inputHash2, outputJson, nowIso2());
|
|
1719
|
-
},
|
|
1720
|
-
memoGet(key) {
|
|
1721
|
-
const row = db.query(`SELECT * FROM memos WHERE key = ?`).get(key);
|
|
1722
|
-
return row ? rowToMemo(row) : undefined;
|
|
1723
|
-
},
|
|
1724
|
-
memoHit(key) {
|
|
1725
|
-
db.query(`UPDATE memos SET hit_count = hit_count + 1 WHERE key = ?`).run(key);
|
|
1726
|
-
},
|
|
1727
|
-
memoList() {
|
|
1728
|
-
const rows = db.query(`SELECT * FROM memos ORDER BY created_at DESC LIMIT 1000`).all();
|
|
1729
|
-
return rows.map((r) => rowToMemo(r));
|
|
1730
|
-
},
|
|
1731
|
-
memoClear() {
|
|
1732
|
-
db.exec(`DELETE FROM memos`);
|
|
1733
|
-
}
|
|
1734
|
-
};
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
// src/wal.ts
|
|
1738
|
-
import { createHash as createHash3 } from "crypto";
|
|
1739
|
-
import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync, renameSync, truncateSync, writeFileSync } from "fs";
|
|
1740
|
-
import { join as join2 } from "path";
|
|
1741
|
-
var WAL_FILE_NAME = "session.wal";
|
|
1742
|
-
var SESSIONS_DIR_NAME = "sessions";
|
|
1743
|
-
var MAX_WAL_BYTES = 256 * 1024 * 1024;
|
|
1744
|
-
function checksumOf(op) {
|
|
1745
|
-
if (op === undefined || op === null)
|
|
1746
|
-
return "";
|
|
1747
|
-
return createHash3("sha256").update(JSON.stringify(op)).digest("hex");
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
class SessionWAL {
|
|
1751
|
-
filePath;
|
|
1752
|
-
constructor(filePath) {
|
|
1753
|
-
this.filePath = filePath;
|
|
1754
|
-
}
|
|
1755
|
-
static open(dataDir) {
|
|
1756
|
-
mkdirSync2(join2(dataDir, SESSIONS_DIR_NAME), { recursive: true });
|
|
1757
|
-
const canonical = join2(dataDir, SESSIONS_DIR_NAME, WAL_FILE_NAME);
|
|
1758
|
-
const legacy = join2(dataDir, WAL_FILE_NAME);
|
|
1759
|
-
if (existsSync(legacy) && !existsSync(canonical)) {
|
|
1760
|
-
try {
|
|
1761
|
-
renameSync(legacy, canonical);
|
|
1762
|
-
} catch {
|
|
1763
|
-
return new SessionWAL(legacy);
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
return new SessionWAL(canonical);
|
|
1767
|
-
}
|
|
1768
|
-
static openAt(filePath) {
|
|
1769
|
-
return new SessionWAL(filePath);
|
|
1770
|
-
}
|
|
1771
|
-
append(op) {
|
|
1772
|
-
const at = op.at ?? new Date().toISOString();
|
|
1773
|
-
const full = { ...op, at };
|
|
1774
|
-
const nextSeq = this.nextSeq();
|
|
1775
|
-
const entry = { seq: nextSeq, at, checksum: checksumOf(full), op: full };
|
|
1776
|
-
appendFileSync(this.filePath, JSON.stringify(entry) + `
|
|
1777
|
-
`, "utf8");
|
|
1778
|
-
return entry;
|
|
1779
|
-
}
|
|
1780
|
-
nextSeq() {
|
|
1781
|
-
const replay = this.replay();
|
|
1782
|
-
return replay.entries.length + 1;
|
|
1783
|
-
}
|
|
1784
|
-
replay() {
|
|
1785
|
-
const entries = [];
|
|
1786
|
-
let torn = false;
|
|
1787
|
-
let repaired = false;
|
|
1788
|
-
let content;
|
|
1789
|
-
try {
|
|
1790
|
-
content = readFileSync(this.filePath, "utf8");
|
|
1791
|
-
} catch {
|
|
1792
|
-
return { entries, torn, repaired, liveClaims: () => new Map };
|
|
1793
|
-
}
|
|
1794
|
-
if (content.length > MAX_WAL_BYTES) {
|
|
1795
|
-
throw new Error(`session WAL exceeds ${MAX_WAL_BYTES} bytes; refusing to replay a truncated journal`);
|
|
1796
|
-
}
|
|
1797
|
-
if (content === "") {
|
|
1798
|
-
return { entries, torn, repaired, liveClaims: () => new Map };
|
|
1799
|
-
}
|
|
1800
|
-
const lines = content.split(`
|
|
1801
|
-
`);
|
|
1802
|
-
let badOffset = null;
|
|
1803
|
-
let expectedSeq = 1;
|
|
1804
|
-
for (let i = 0;i < lines.length; i++) {
|
|
1805
|
-
const line = lines[i];
|
|
1806
|
-
if (line === "")
|
|
1807
|
-
continue;
|
|
1808
|
-
if (badOffset !== null)
|
|
1809
|
-
break;
|
|
1810
|
-
const lineOffset = cumulativeOffset(lines, i);
|
|
1811
|
-
let entry;
|
|
1812
|
-
try {
|
|
1813
|
-
entry = JSON.parse(line);
|
|
1814
|
-
} catch {
|
|
1815
|
-
badOffset = lineOffset;
|
|
1816
|
-
break;
|
|
1817
|
-
}
|
|
1818
|
-
if (typeof entry?.seq !== "number" || entry.seq !== expectedSeq || entry.checksum !== checksumOf(entry.op)) {
|
|
1819
|
-
badOffset = lineOffset;
|
|
1820
|
-
break;
|
|
1821
|
-
}
|
|
1822
|
-
entries.push(entry);
|
|
1823
|
-
expectedSeq++;
|
|
1824
|
-
}
|
|
1825
|
-
if (badOffset !== null) {
|
|
1826
|
-
torn = true;
|
|
1827
|
-
try {
|
|
1828
|
-
truncateSync(this.filePath, badOffset);
|
|
1829
|
-
repaired = true;
|
|
1830
|
-
} catch {}
|
|
1831
|
-
}
|
|
1832
|
-
return {
|
|
1833
|
-
entries,
|
|
1834
|
-
torn,
|
|
1835
|
-
repaired,
|
|
1836
|
-
liveClaims: (nowMs) => reconstructLiveClaims(entries, nowMs)
|
|
1837
|
-
};
|
|
1838
|
-
}
|
|
1839
|
-
close() {}
|
|
1840
|
-
}
|
|
1841
|
-
function cumulativeOffset(lines, index) {
|
|
1842
|
-
let offset = 0;
|
|
1843
|
-
for (let i = 0;i < index; i++) {
|
|
1844
|
-
offset += Buffer.byteLength(lines[i]) + 1;
|
|
1845
|
-
}
|
|
1846
|
-
return offset;
|
|
1847
|
-
}
|
|
1848
|
-
function reconstructLiveClaims(entries, nowMs = Date.now()) {
|
|
1849
|
-
const claims = new Map;
|
|
1850
|
-
for (const entry of entries) {
|
|
1851
|
-
const op = entry.op;
|
|
1852
|
-
if (op.op === "claim_acquired") {
|
|
1853
|
-
claims.set(op.runId, { worker: op.worker, fencing: op.fencing, expiresAtMs: Date.parse(op.expiresAt) });
|
|
1854
|
-
} else if (op.op === "claim_released") {
|
|
1855
|
-
const existing = claims.get(op.runId);
|
|
1856
|
-
if (existing && existing.fencing === op.fencing) {
|
|
1857
|
-
claims.delete(op.runId);
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
for (const [runId, claim] of claims) {
|
|
1862
|
-
if (claim.expiresAtMs <= nowMs)
|
|
1863
|
-
claims.delete(runId);
|
|
1864
|
-
}
|
|
1865
|
-
return claims;
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
// src/agent-tools.ts
|
|
1069
|
+
// src/tool-definitions.ts
|
|
1869
1070
|
function textResult(payload) {
|
|
1870
1071
|
return { content: [{ type: "text", text: JSON.stringify(payload) }], isError: false };
|
|
1871
1072
|
}
|
|
1872
1073
|
function workflowsTools(_service) {
|
|
1873
|
-
|
|
1074
|
+
const tools = [
|
|
1874
1075
|
{
|
|
1875
1076
|
name: "workflows_version",
|
|
1876
1077
|
description: "Report the installed @hasna/workflows version.",
|
|
@@ -1883,7 +1084,7 @@ function workflowsTools(_service) {
|
|
|
1883
1084
|
},
|
|
1884
1085
|
{
|
|
1885
1086
|
name: "workflows_ready",
|
|
1886
|
-
description: "Report the workflows service readiness
|
|
1087
|
+
description: "Report the hosted workflows service readiness.",
|
|
1887
1088
|
inputSchema: { type: "object", properties: {} }
|
|
1888
1089
|
},
|
|
1889
1090
|
{
|
|
@@ -1902,14 +1103,16 @@ function workflowsTools(_service) {
|
|
|
1902
1103
|
type: "object",
|
|
1903
1104
|
properties: {
|
|
1904
1105
|
graph: { type: "object", description: "the workflow graph" },
|
|
1905
|
-
context: { type: "object", description: "optional run context" }
|
|
1106
|
+
context: { type: "object", description: "optional run context" },
|
|
1107
|
+
idempotencyKey: { type: "string", description: "stable request key" },
|
|
1108
|
+
maxCycles: { type: "integer", minimum: 1, maximum: 2000 }
|
|
1906
1109
|
},
|
|
1907
|
-
required: ["graph"]
|
|
1110
|
+
required: ["graph", "idempotencyKey"]
|
|
1908
1111
|
}
|
|
1909
1112
|
},
|
|
1910
1113
|
{
|
|
1911
1114
|
name: "workflows_runs_list",
|
|
1912
|
-
description: "List recent runs from the
|
|
1115
|
+
description: "List recent runs from the configured hosted service.",
|
|
1913
1116
|
inputSchema: {
|
|
1914
1117
|
type: "object",
|
|
1915
1118
|
properties: {
|
|
@@ -1924,7 +1127,13 @@ function workflowsTools(_service) {
|
|
|
1924
1127
|
inputSchema: { type: "object", properties: {} }
|
|
1925
1128
|
}
|
|
1926
1129
|
];
|
|
1130
|
+
return tools.map((tool) => ({ ...tool, inputSchema: { ...tool.inputSchema, properties: {
|
|
1131
|
+
...tool.inputSchema.properties,
|
|
1132
|
+
agent: { type: "string", description: "calling agent identity" }
|
|
1133
|
+
} } }));
|
|
1927
1134
|
}
|
|
1135
|
+
|
|
1136
|
+
// src/agent-tools.ts
|
|
1928
1137
|
async function callWorkflowTool(service, name, args) {
|
|
1929
1138
|
switch (name) {
|
|
1930
1139
|
case "workflows_version":
|
|
@@ -1932,42 +1141,26 @@ async function callWorkflowTool(service, name, args) {
|
|
|
1932
1141
|
case "workflows_health":
|
|
1933
1142
|
return textResult(service.health());
|
|
1934
1143
|
case "workflows_ready":
|
|
1935
|
-
return textResult(service.ready());
|
|
1144
|
+
return textResult(await service.ready());
|
|
1936
1145
|
case "workflows_validate": {
|
|
1937
1146
|
const graph = args.graph;
|
|
1938
1147
|
const result = validateGraph(graph);
|
|
1939
1148
|
return textResult(result);
|
|
1940
1149
|
}
|
|
1941
|
-
case "workflows_run":
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
result: final.resultJson ? JSON.parse(final.resultJson) : null
|
|
1952
|
-
});
|
|
1953
|
-
} finally {
|
|
1954
|
-
store.close();
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
case "workflows_runs_list": {
|
|
1958
|
-
const store = openStore(service.config.dataDir);
|
|
1959
|
-
try {
|
|
1960
|
-
const status = typeof args.status === "string" ? args.status : undefined;
|
|
1961
|
-
const limit = typeof args.limit === "number" ? args.limit : 100;
|
|
1962
|
-
return textResult(store.listRuns({ status, limit }));
|
|
1963
|
-
} finally {
|
|
1964
|
-
store.close();
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1150
|
+
case "workflows_run":
|
|
1151
|
+
return textResult(await service.triggerRun(args.graph, args.context ?? {}, {
|
|
1152
|
+
idempotencyKey: typeof args.idempotencyKey === "string" ? args.idempotencyKey : undefined,
|
|
1153
|
+
maxCycles: typeof args.maxCycles === "number" ? args.maxCycles : undefined
|
|
1154
|
+
}));
|
|
1155
|
+
case "workflows_runs_list":
|
|
1156
|
+
return textResult(await service.listRuns({
|
|
1157
|
+
status: typeof args.status === "string" ? args.status : undefined,
|
|
1158
|
+
limit: typeof args.limit === "number" ? args.limit : 100
|
|
1159
|
+
}));
|
|
1967
1160
|
case "workflows_lanes_list":
|
|
1968
1161
|
return textResult(await laneInventory());
|
|
1969
1162
|
default:
|
|
1970
|
-
throw new Error(
|
|
1163
|
+
throw new Error("Unknown workflows tool");
|
|
1971
1164
|
}
|
|
1972
1165
|
}
|
|
1973
1166
|
export {
|