codex-toys 0.140.11 → 0.140.13
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 +35 -15
- package/dist/cli/actions.d.ts +1 -1
- package/dist/cli/actions.d.ts.map +1 -1
- package/dist/cli/actions.js +1 -0
- package/dist/cli/actions.js.map +1 -1
- package/dist/cli/args.d.ts +25 -20
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +67 -33
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +18 -16
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/index.js +68 -57
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/toybox.js +3 -3
- package/dist/internal/feed/index.d.ts +26 -1
- package/dist/internal/feed/index.d.ts.map +1 -1
- package/dist/internal/feed/index.js +102 -1
- package/dist/internal/feed/index.js.map +1 -1
- package/dist/internal/package.json +5 -0
- package/dist/internal/workbench/{deferred-run-methods.d.ts → dispatch-run-methods.d.ts} +12 -12
- package/dist/internal/workbench/{deferred-run-methods.d.ts.map → dispatch-run-methods.d.ts.map} +1 -1
- package/dist/internal/workbench/{deferred-run-methods.js → dispatch-run-methods.js} +61 -61
- package/dist/internal/workbench/{deferred-run-methods.js.map → dispatch-run-methods.js.map} +1 -1
- package/dist/internal/workbench/fetch.js +1 -1
- package/dist/internal/workbench/fetch.js.map +1 -1
- package/dist/internal/workbench/index.d.ts +1 -1
- package/dist/internal/workbench/index.js +1 -1
- package/dist/internal/workbench/workbench-overview.d.ts +9 -9
- package/dist/internal/workbench/workbench-overview.js +12 -12
- package/dist/internal/workbench/workbench-runtime.d.ts +75 -120
- package/dist/internal/workbench/workbench-runtime.d.ts.map +1 -1
- package/dist/internal/workbench/workbench-runtime.js +293 -621
- package/dist/internal/workbench/workbench-runtime.js.map +1 -1
- package/dist/internal/workbench/workflow.d.ts.map +1 -1
- package/dist/internal/workbench/workflow.js +106 -11
- package/dist/internal/workbench/workflow.js.map +1 -1
- package/docs/pages/{reference → components}/cli.md +13 -14
- package/docs/pages/{primitives → components}/toybox.md +4 -4
- package/docs/pages/guides/capability-kit-setup.md +135 -0
- package/docs/pages/guides/dashboard-over-toybox.md +145 -0
- package/docs/pages/guides/delegated-repo-work.md +119 -0
- package/docs/pages/guides/feed-to-workflow.md +138 -0
- package/docs/pages/guides/local-scheduled-workbench.md +149 -0
- package/docs/pages/guides/remote-codex-workbench.md +147 -0
- package/docs/pages/guides/repository-autonomy.md +163 -0
- package/docs/pages/index.md +35 -11
- package/docs/pages/primitives/{deferred-queues.md → dispatch-queues.md} +21 -21
- package/docs/pages/primitives/feed.md +18 -9
- package/docs/pages/primitives/workbench.md +26 -21
- package/docs/pages/primitives/workflow.md +6 -5
- package/docs/pages/reference/packages.md +14 -2
- package/package.json +1 -1
- /package/docs/pages/{primitives → components}/kits.md +0 -0
- /package/docs/pages/{primitives → components}/proxy.md +0 -0
|
@@ -6,6 +6,7 @@ import { spawn } from "node:child_process";
|
|
|
6
6
|
import { parse as parseToml } from "smol-toml";
|
|
7
7
|
import { createWorkflowHost, resolveWorkflowTarget, runWorkflowScript, startWorkflowTurnWithRequest, waitWorkflowTurnWithRequest, } from "./workflow.js";
|
|
8
8
|
import { parseJsonText } from "../bridge/json.js";
|
|
9
|
+
export const defaultActionsRunnerImage = "ghcr.io/peezy-tech/codex-toys-actions:latest";
|
|
9
10
|
export async function discoverWorkbenchRoot(start = process.cwd()) {
|
|
10
11
|
let current = path.resolve(start);
|
|
11
12
|
let firstDotCodexRoot;
|
|
@@ -86,7 +87,9 @@ export async function loadWorkbenchConfig(context) {
|
|
|
86
87
|
const workbench = isRecord(parsed.workbench) ? parsed.workbench : undefined;
|
|
87
88
|
const surfacesInput = Array.isArray(workbench?.surfaces) ? workbench.surfaces : [];
|
|
88
89
|
const tasksInput = Array.isArray(workbench?.tasks) ? workbench.tasks : [];
|
|
89
|
-
|
|
90
|
+
if (workbench?.reactive !== undefined) {
|
|
91
|
+
throw new Error("workbench.reactive has been removed; run explicit tasks or dispatch queues from systemd or Actions schedules");
|
|
92
|
+
}
|
|
90
93
|
const tasks = tasksInput.map(parseTask);
|
|
91
94
|
const ids = new Set();
|
|
92
95
|
for (const task of tasks) {
|
|
@@ -99,11 +102,10 @@ export async function loadWorkbenchConfig(context) {
|
|
|
99
102
|
name: stringValue(workbench?.name, path.basename(context.repoRoot)),
|
|
100
103
|
surfaces: surfacesInput.map(parseSurface),
|
|
101
104
|
tasks,
|
|
102
|
-
reactive: reactiveInput.map(parseReactiveRule),
|
|
103
105
|
path: context.configPath,
|
|
104
106
|
};
|
|
105
107
|
}
|
|
106
|
-
export async function collectWorkbenchDoctorInfo(context
|
|
108
|
+
export async function collectWorkbenchDoctorInfo(context) {
|
|
107
109
|
let config;
|
|
108
110
|
let configExists = true;
|
|
109
111
|
try {
|
|
@@ -122,16 +124,12 @@ export async function collectWorkbenchDoctorInfo(context, options = {}) {
|
|
|
122
124
|
}
|
|
123
125
|
const runs = await readRuns(context);
|
|
124
126
|
const latestRun = runs.sort((a, b) => b.startedAt.localeCompare(a.startedAt))[0];
|
|
125
|
-
const
|
|
127
|
+
const dispatchRuns = await listDispatchRunIntents(context);
|
|
126
128
|
const now = new Date();
|
|
127
|
-
const
|
|
129
|
+
const latestDispatchRun = dispatchRuns
|
|
128
130
|
.toSorted((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
|
|
129
|
-
const
|
|
131
|
+
const dispatchDueFlags = await Promise.all(dispatchRuns.map(async (intent) => await isDispatchIntentDue(context, intent, now)));
|
|
130
132
|
const failingCount = countFailingTasks(config?.tasks ?? [], runs);
|
|
131
|
-
const includeRunner = options.includeRunner === true || options.runnerProbe !== undefined;
|
|
132
|
-
const runner = includeRunner
|
|
133
|
-
? await collectWorkbenchRunnerInfo(context, config?.tasks ?? [], deferredRuns, options.runnerProbe ?? runSystemctlUser)
|
|
134
|
-
: undefined;
|
|
135
133
|
return {
|
|
136
134
|
mode: context.mode,
|
|
137
135
|
requestedMode: context.requestedMode,
|
|
@@ -148,15 +146,13 @@ export async function collectWorkbenchDoctorInfo(context, options = {}) {
|
|
|
148
146
|
globalMemorySummaryExists: await exists(path.join(context.globalCodexHome, "memories", "memory_summary.md")),
|
|
149
147
|
workbenchMemorySummaryExists: await exists(path.join(context.workbenchCodexHome, "memories", "memory_summary.md")),
|
|
150
148
|
taskCount: config?.tasks.length ?? 0,
|
|
151
|
-
dueCount: dueTasks(config?.tasks ?? [], runs, new Date()).length,
|
|
152
149
|
failingCount,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
150
|
+
dispatchCount: dispatchRuns.length,
|
|
151
|
+
dispatchDueCount: dispatchDueFlags.filter(Boolean).length,
|
|
152
|
+
dispatchRunningCount: dispatchRuns.filter((intent) => intent.status === "running").length,
|
|
153
|
+
dispatchFailedCount: dispatchRuns.filter((intent) => intent.status === "failed").length,
|
|
157
154
|
latestRun,
|
|
158
|
-
|
|
159
|
-
runner,
|
|
155
|
+
latestDispatchRun,
|
|
160
156
|
surfaces: config?.surfaces ?? [],
|
|
161
157
|
errors: workbenchDoctorErrors(context),
|
|
162
158
|
};
|
|
@@ -173,23 +169,19 @@ export function formatWorkbenchDoctorInfo(info) {
|
|
|
173
169
|
["actions state", info.actionsStateRoot],
|
|
174
170
|
["global memories", `${info.globalMemoryRoot}${info.globalMemorySummaryExists ? " (summary)" : ""}`],
|
|
175
171
|
["workbench memories", `${info.workbenchMemoryRoot}${info.workbenchMemorySummaryExists ? " (summary)" : ""}`],
|
|
176
|
-
["tasks", `${info.taskCount} configured, ${info.
|
|
172
|
+
["tasks", `${info.taskCount} configured, ${info.failingCount} failing`],
|
|
177
173
|
["latest run", info.latestRun ? `${info.latestRun.status} ${info.latestRun.taskId} ${info.latestRun.finishedAt}` : "none"],
|
|
178
174
|
[
|
|
179
|
-
"
|
|
180
|
-
`${info.
|
|
175
|
+
"dispatch runs",
|
|
176
|
+
`${info.dispatchCount} total, ${info.dispatchDueCount} due, ${info.dispatchRunningCount} running, ${info.dispatchFailedCount} failed`,
|
|
181
177
|
],
|
|
182
178
|
[
|
|
183
|
-
"latest
|
|
184
|
-
info.
|
|
185
|
-
? `${info.
|
|
179
|
+
"latest dispatch",
|
|
180
|
+
info.latestDispatchRun
|
|
181
|
+
? `${info.latestDispatchRun.status} ${info.latestDispatchRun.id} ${info.latestDispatchRun.updatedAt}`
|
|
186
182
|
: "none",
|
|
187
183
|
],
|
|
188
|
-
["runner", formatWorkbenchRunnerInfo(info.runner)],
|
|
189
184
|
];
|
|
190
|
-
if (info.runner?.warning) {
|
|
191
|
-
rows.push(["runner warning", info.runner.warning]);
|
|
192
|
-
}
|
|
193
185
|
for (const error of info.errors) {
|
|
194
186
|
rows.push(["error", error]);
|
|
195
187
|
}
|
|
@@ -198,47 +190,21 @@ export function formatWorkbenchDoctorInfo(info) {
|
|
|
198
190
|
export async function scaffoldActionsWorkbench(options = {}) {
|
|
199
191
|
const workbenchRoot = path.resolve(options.workbenchRoot ?? await discoverWorkbenchRoot());
|
|
200
192
|
const files = [];
|
|
193
|
+
const runnerImage = options.runnerImage === undefined ? defaultActionsRunnerImage : options.runnerImage;
|
|
201
194
|
const write = async (relativePath, content) => {
|
|
202
195
|
files.push(await writeScaffoldFile(workbenchRoot, relativePath, content, options.overwrite === true));
|
|
203
196
|
};
|
|
204
197
|
await write(".codex/workbench.toml", workbenchTomlTemplate(workbenchRoot));
|
|
205
198
|
await write(".codex/config.toml", codexConfigTemplate());
|
|
206
199
|
if (options.forgejo) {
|
|
207
|
-
await write(".forgejo/workflows/codex-toys-actions.yml", actionsWorkflowTemplate("forgejo"));
|
|
200
|
+
await write(".forgejo/workflows/codex-toys-actions.yml", actionsWorkflowTemplate("forgejo", runnerImage));
|
|
208
201
|
}
|
|
209
202
|
if (options.github || !options.forgejo) {
|
|
210
|
-
await write(".github/workflows/codex-toys-actions.yml", actionsWorkflowTemplate("github"));
|
|
203
|
+
await write(".github/workflows/codex-toys-actions.yml", actionsWorkflowTemplate("github", runnerImage));
|
|
211
204
|
}
|
|
212
205
|
files.push(await appendGitignoreEntries(workbenchRoot, actionsGitignoreEntries(), retiredActionsGitignoreEntries()));
|
|
213
206
|
return { workbenchRoot, files };
|
|
214
207
|
}
|
|
215
|
-
export async function tickWorkbench(context, options) {
|
|
216
|
-
await ensureStateDirs(context);
|
|
217
|
-
const config = await loadWorkbenchConfig(context);
|
|
218
|
-
const previousRuns = await readRuns(context);
|
|
219
|
-
const previousIntents = await listDeferredRunIntents(context);
|
|
220
|
-
const now = new Date();
|
|
221
|
-
const due = dueTasks(config.tasks, previousRuns, now, previousIntents);
|
|
222
|
-
const runs = [];
|
|
223
|
-
for (const task of due) {
|
|
224
|
-
await createScheduledWorkbenchTaskIntent(context, task, now);
|
|
225
|
-
}
|
|
226
|
-
const executions = await runDueDeferredRuns(context, options);
|
|
227
|
-
for (const execution of executions.executions) {
|
|
228
|
-
const workbenchRun = record(execution.output).workbenchRun;
|
|
229
|
-
if (isWorkbenchRunRecord(workbenchRun)) {
|
|
230
|
-
runs.push(workbenchRun);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
const allRuns = [...previousRuns, ...runs];
|
|
234
|
-
for (const rule of config.reactive.filter((item) => item.enabled)) {
|
|
235
|
-
const targets = config.tasks.filter((task) => rule.task === "*" ? true : task.id === rule.task);
|
|
236
|
-
if (targets.some((task) => consecutiveFailures(task.id, allRuns) >= rule.consecutiveFailuresGte)) {
|
|
237
|
-
runs.push(await runReactiveRule(context, rule));
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return { mode: context.mode, due: due.map((task) => task.id), runs };
|
|
241
|
-
}
|
|
242
208
|
export async function runWorkbenchTaskById(context, taskId, options) {
|
|
243
209
|
await ensureStateDirs(context);
|
|
244
210
|
const config = await loadWorkbenchConfig(context);
|
|
@@ -269,15 +235,18 @@ export async function commitActionsWorkbenchState(context, options = {}) {
|
|
|
269
235
|
await runGit(context.repoRoot, ["add", "-A", "-f", "--", sessionsPath]);
|
|
270
236
|
}
|
|
271
237
|
const staged = await runGit(context.repoRoot, ["diff", "--cached", "--name-only", "--", ...relativePaths]);
|
|
272
|
-
|
|
238
|
+
const stagedPaths = staged.stdout.trim().split(/\r?\n/).filter(Boolean);
|
|
239
|
+
if (stagedPaths.length === 0) {
|
|
273
240
|
return { attempted: true, committed: false, paths: context.actionsCommitPaths };
|
|
274
241
|
}
|
|
242
|
+
await runGit(context.repoRoot, ["config", "user.name", "codex-toys-actions"]);
|
|
243
|
+
await runGit(context.repoRoot, ["config", "user.email", "codex-toys-actions@users.noreply.github.com"]);
|
|
275
244
|
const commit = await runGit(context.repoRoot, [
|
|
276
245
|
"commit",
|
|
277
246
|
"-m",
|
|
278
247
|
options.message ?? "Update Codex workbench state",
|
|
279
248
|
"--",
|
|
280
|
-
...
|
|
249
|
+
...stagedPaths,
|
|
281
250
|
]);
|
|
282
251
|
return {
|
|
283
252
|
attempted: true,
|
|
@@ -286,13 +255,13 @@ export async function commitActionsWorkbenchState(context, options = {}) {
|
|
|
286
255
|
output: commit.stdout || commit.stderr,
|
|
287
256
|
};
|
|
288
257
|
}
|
|
289
|
-
export async function
|
|
290
|
-
await
|
|
291
|
-
const input =
|
|
258
|
+
export async function createDispatchRunIntent(context, params) {
|
|
259
|
+
await ensureDispatchRunDirs(context);
|
|
260
|
+
const input = parseDispatchRunCreateParams(params);
|
|
292
261
|
const now = new Date().toISOString();
|
|
293
262
|
const runAt = input.runAt ?? now;
|
|
294
263
|
const intent = compactUndefined({
|
|
295
|
-
id: input.id ??
|
|
264
|
+
id: input.id ?? dispatchRunId(now),
|
|
296
265
|
status: "pending",
|
|
297
266
|
mode: context.mode,
|
|
298
267
|
runAt,
|
|
@@ -305,11 +274,11 @@ export async function createDeferredRunIntent(context, params) {
|
|
|
305
274
|
dependsOn: input.dependsOn,
|
|
306
275
|
attemptIds: [],
|
|
307
276
|
});
|
|
308
|
-
await writeNewJsonFile(
|
|
277
|
+
await writeNewJsonFile(dispatchIntentPath(context, intent.id), intent);
|
|
309
278
|
return intent;
|
|
310
279
|
}
|
|
311
|
-
export async function
|
|
312
|
-
const dir =
|
|
280
|
+
export async function listDispatchRunIntents(context, options = {}) {
|
|
281
|
+
const dir = dispatchIntentDir(context);
|
|
313
282
|
try {
|
|
314
283
|
const entries = await readdir(dir);
|
|
315
284
|
const intents = [];
|
|
@@ -318,7 +287,7 @@ export async function listDeferredRunIntents(context, options = {}) {
|
|
|
318
287
|
continue;
|
|
319
288
|
}
|
|
320
289
|
try {
|
|
321
|
-
const intent =
|
|
290
|
+
const intent = normalizeDispatchRunIntent(parseJsonText(await readFile(path.join(dir, entry), "utf8"), path.join(dir, entry)));
|
|
322
291
|
if (!options.status || intent.status === options.status) {
|
|
323
292
|
intents.push(intent);
|
|
324
293
|
}
|
|
@@ -332,11 +301,11 @@ export async function listDeferredRunIntents(context, options = {}) {
|
|
|
332
301
|
return [];
|
|
333
302
|
}
|
|
334
303
|
}
|
|
335
|
-
export async function
|
|
336
|
-
const intent = await
|
|
337
|
-
const attempts = await
|
|
304
|
+
export async function readDispatchRun(context, intentId, options = {}) {
|
|
305
|
+
const intent = await readDispatchRunIntent(context, intentId);
|
|
306
|
+
const attempts = await readDispatchRunAttempts(context, intent.attemptIds);
|
|
338
307
|
const outputs = options.includeOutput
|
|
339
|
-
? await
|
|
308
|
+
? await readDispatchRunAttemptOutputs(attempts)
|
|
340
309
|
: undefined;
|
|
341
310
|
return compactUndefined({ intent, attempts, outputs });
|
|
342
311
|
}
|
|
@@ -344,12 +313,12 @@ export async function enqueuePromptQueueIntent(context, params) {
|
|
|
344
313
|
const input = parsePromptQueueEnqueueParams(params);
|
|
345
314
|
const after = input.afterIntentId
|
|
346
315
|
? {
|
|
347
|
-
kind: "
|
|
316
|
+
kind: "dispatch-run",
|
|
348
317
|
intentId: input.afterIntentId,
|
|
349
318
|
status: input.afterStatus,
|
|
350
319
|
}
|
|
351
320
|
: undefined;
|
|
352
|
-
return await
|
|
321
|
+
return await createDispatchRunIntent(context, {
|
|
353
322
|
id: input.id,
|
|
354
323
|
runAt: input.runAt,
|
|
355
324
|
target: compactUndefined({
|
|
@@ -373,7 +342,7 @@ export async function enqueuePromptQueueIntent(context, params) {
|
|
|
373
342
|
});
|
|
374
343
|
}
|
|
375
344
|
export async function listPromptQueueIntents(context, options = {}) {
|
|
376
|
-
const intents = await
|
|
345
|
+
const intents = await listDispatchRunIntents(context, {
|
|
377
346
|
status: options.status,
|
|
378
347
|
});
|
|
379
348
|
return intents
|
|
@@ -393,10 +362,10 @@ function comparePromptQueueIntents(left, right) {
|
|
|
393
362
|
left.id.localeCompare(right.id);
|
|
394
363
|
}
|
|
395
364
|
function dependsOnIntent(intent, dependencyId) {
|
|
396
|
-
return (intent.dependsOn ?? []).some((dependency) => dependency.kind === "
|
|
365
|
+
return (intent.dependsOn ?? []).some((dependency) => dependency.kind === "dispatch-run" && dependency.intentId === dependencyId);
|
|
397
366
|
}
|
|
398
367
|
export async function collectPromptQueueRuns(context, options = {}) {
|
|
399
|
-
return await
|
|
368
|
+
return await collectDispatchRuns(context, {
|
|
400
369
|
cursor: options.cursor,
|
|
401
370
|
defaultCursor: "prompt-queue",
|
|
402
371
|
now: options.now,
|
|
@@ -404,7 +373,7 @@ export async function collectPromptQueueRuns(context, options = {}) {
|
|
|
404
373
|
});
|
|
405
374
|
}
|
|
406
375
|
export async function runDuePromptQueueIntents(context, options) {
|
|
407
|
-
return await
|
|
376
|
+
return await runDueDispatchRuns(context, {
|
|
408
377
|
...options,
|
|
409
378
|
filter: (intent) => isPromptQueueIntent(intent, options.queue),
|
|
410
379
|
});
|
|
@@ -413,12 +382,12 @@ export async function enqueueLocalHandoffIntent(context, params) {
|
|
|
413
382
|
const input = parseLocalHandoffEnqueueParams(params);
|
|
414
383
|
const after = input.afterIntentId
|
|
415
384
|
? {
|
|
416
|
-
kind: "
|
|
385
|
+
kind: "dispatch-run",
|
|
417
386
|
intentId: input.afterIntentId,
|
|
418
387
|
status: input.afterStatus,
|
|
419
388
|
}
|
|
420
389
|
: undefined;
|
|
421
|
-
return await
|
|
390
|
+
return await createDispatchRunIntent(context, {
|
|
422
391
|
id: input.id,
|
|
423
392
|
runAt: input.runAt,
|
|
424
393
|
target: compactUndefined({
|
|
@@ -442,7 +411,7 @@ export async function enqueueLocalHandoffIntent(context, params) {
|
|
|
442
411
|
});
|
|
443
412
|
}
|
|
444
413
|
export async function listLocalHandoffIntents(context, options = {}) {
|
|
445
|
-
const intents = await
|
|
414
|
+
const intents = await listDispatchRunIntents(context, {
|
|
446
415
|
status: options.status,
|
|
447
416
|
});
|
|
448
417
|
return intents
|
|
@@ -450,7 +419,7 @@ export async function listLocalHandoffIntents(context, options = {}) {
|
|
|
450
419
|
.slice(0, clampLimit(options.limit, 500));
|
|
451
420
|
}
|
|
452
421
|
export async function collectLocalHandoffRuns(context, options = {}) {
|
|
453
|
-
return await
|
|
422
|
+
return await collectDispatchRuns(context, {
|
|
454
423
|
cursor: options.cursor,
|
|
455
424
|
defaultCursor: "local-handoff",
|
|
456
425
|
now: options.now,
|
|
@@ -459,7 +428,7 @@ export async function collectLocalHandoffRuns(context, options = {}) {
|
|
|
459
428
|
}
|
|
460
429
|
export async function drainLocalHandoffQueue(context, options) {
|
|
461
430
|
const action = options.action ?? "run";
|
|
462
|
-
const result = await
|
|
431
|
+
const result = await runDueDispatchRuns(context, {
|
|
463
432
|
...options,
|
|
464
433
|
includeLocalHandoffs: true,
|
|
465
434
|
localHandoffMaterialize: action === "materialize"
|
|
@@ -478,22 +447,22 @@ export async function drainLocalHandoffQueue(context, options) {
|
|
|
478
447
|
executions: result.executions,
|
|
479
448
|
};
|
|
480
449
|
}
|
|
481
|
-
export async function
|
|
482
|
-
await
|
|
483
|
-
const cursor =
|
|
484
|
-
const previousCursor = await
|
|
450
|
+
export async function collectDispatchRuns(context, options = {}) {
|
|
451
|
+
await ensureDispatchRunDirs(context);
|
|
452
|
+
const cursor = dispatchCollectCursorName(options.cursor, options.defaultCursor);
|
|
453
|
+
const previousCursor = await readDispatchRunCollectCursor(context, cursor);
|
|
485
454
|
const collectedAt = (options.now ?? new Date()).toISOString();
|
|
486
455
|
const filteredIntents = [];
|
|
487
|
-
for (const intent of await
|
|
456
|
+
for (const intent of await listDispatchRunIntents(context)) {
|
|
488
457
|
if (!options.filter || await options.filter(intent)) {
|
|
489
458
|
filteredIntents.push(intent);
|
|
490
459
|
}
|
|
491
460
|
}
|
|
492
461
|
const terminalIntents = filteredIntents
|
|
493
|
-
.filter((intent) =>
|
|
462
|
+
.filter((intent) => isTerminalDispatchRunStatus(intent.status))
|
|
494
463
|
.toSorted((left, right) => left.updatedAt.localeCompare(right.updatedAt) || left.id.localeCompare(right.id))
|
|
495
|
-
.filter((intent) =>
|
|
496
|
-
const intents = await Promise.all(terminalIntents.map(async (intent) => await
|
|
464
|
+
.filter((intent) => isAfterDispatchRunCollectCursor(intent, previousCursor));
|
|
465
|
+
const intents = await Promise.all(terminalIntents.map(async (intent) => await readDispatchRun(context, intent.id, { includeOutput: true })));
|
|
497
466
|
const last = terminalIntents.at(-1);
|
|
498
467
|
const cursorState = compactUndefined({
|
|
499
468
|
cursor,
|
|
@@ -501,7 +470,7 @@ export async function collectDeferredRuns(context, options = {}) {
|
|
|
501
470
|
lastUpdatedAt: last?.updatedAt ?? previousCursor?.lastUpdatedAt,
|
|
502
471
|
lastIntentId: last?.id ?? previousCursor?.lastIntentId,
|
|
503
472
|
});
|
|
504
|
-
await writeJsonFileAtomic(
|
|
473
|
+
await writeJsonFileAtomic(dispatchCollectCursorPath(context, cursor), cursorState);
|
|
505
474
|
return compactUndefined({
|
|
506
475
|
mode: context.mode,
|
|
507
476
|
cursor,
|
|
@@ -511,10 +480,10 @@ export async function collectDeferredRuns(context, options = {}) {
|
|
|
511
480
|
intents,
|
|
512
481
|
});
|
|
513
482
|
}
|
|
514
|
-
export async function
|
|
515
|
-
const intent = await
|
|
483
|
+
export async function cancelDispatchRunIntent(context, intentId) {
|
|
484
|
+
const intent = await readDispatchRunIntent(context, intentId);
|
|
516
485
|
if (intent.status !== "pending") {
|
|
517
|
-
throw new Error(`Only pending
|
|
486
|
+
throw new Error(`Only pending dispatch runs can be canceled: ${intentId}`);
|
|
518
487
|
}
|
|
519
488
|
const now = new Date().toISOString();
|
|
520
489
|
const canceled = {
|
|
@@ -523,19 +492,19 @@ export async function cancelDeferredRunIntent(context, intentId) {
|
|
|
523
492
|
updatedAt: now,
|
|
524
493
|
canceledAt: now,
|
|
525
494
|
};
|
|
526
|
-
await writeJsonFileAtomic(
|
|
495
|
+
await writeJsonFileAtomic(dispatchIntentPath(context, intentId), canceled);
|
|
527
496
|
return canceled;
|
|
528
497
|
}
|
|
529
|
-
export async function
|
|
530
|
-
await
|
|
531
|
-
const originalIntent = await
|
|
532
|
-
if (!
|
|
533
|
-
throw new Error(`Only terminal
|
|
498
|
+
export async function retryDispatchRunIntent(context, intentId, params = {}, options = {}) {
|
|
499
|
+
await ensureDispatchRunDirs(context);
|
|
500
|
+
const originalIntent = await readDispatchRunIntent(context, intentId);
|
|
501
|
+
if (!isTerminalDispatchRunStatus(originalIntent.status)) {
|
|
502
|
+
throw new Error(`Only terminal dispatch runs can be retried: ${intentId} is ${originalIntent.status}`);
|
|
534
503
|
}
|
|
535
|
-
const input =
|
|
504
|
+
const input = parseDispatchRunRetryParams(params);
|
|
536
505
|
const now = (options.now ?? new Date()).toISOString();
|
|
537
|
-
const retrySource =
|
|
538
|
-
let id = input.id ??
|
|
506
|
+
const retrySource = retryDispatchRunSource(originalIntent, input.source);
|
|
507
|
+
let id = input.id ?? dispatchRetryRunId(originalIntent.id, now);
|
|
539
508
|
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
540
509
|
const intent = compactUndefined({
|
|
541
510
|
id,
|
|
@@ -545,32 +514,32 @@ export async function retryDeferredRunIntent(context, intentId, params = {}, opt
|
|
|
545
514
|
target: originalIntent.target,
|
|
546
515
|
createdAt: now,
|
|
547
516
|
updatedAt: now,
|
|
548
|
-
createdBy: input.createdBy ?? "workbench-
|
|
549
|
-
reason: input.reason ?? `Retry
|
|
517
|
+
createdBy: input.createdBy ?? "workbench-dispatch-retry",
|
|
518
|
+
reason: input.reason ?? `Retry dispatch run ${originalIntent.id}`,
|
|
550
519
|
source: retrySource,
|
|
551
520
|
dependsOn: originalIntent.dependsOn,
|
|
552
521
|
attemptIds: [],
|
|
553
522
|
});
|
|
554
523
|
try {
|
|
555
|
-
await writeNewJsonFile(
|
|
524
|
+
await writeNewJsonFile(dispatchIntentPath(context, intent.id), intent);
|
|
556
525
|
return { intent, originalIntent };
|
|
557
526
|
}
|
|
558
527
|
catch (error) {
|
|
559
528
|
if (!isAlreadyExistsError(error)) {
|
|
560
529
|
throw error;
|
|
561
530
|
}
|
|
562
|
-
id =
|
|
531
|
+
id = dispatchRetryRunId(originalIntent.id, now);
|
|
563
532
|
}
|
|
564
533
|
}
|
|
565
|
-
throw new Error(`Could not allocate retry
|
|
534
|
+
throw new Error(`Could not allocate retry dispatch run id for ${intentId}`);
|
|
566
535
|
}
|
|
567
|
-
export async function
|
|
536
|
+
export async function runDueDispatchRuns(context, options) {
|
|
568
537
|
await ensureStateDirs(context);
|
|
569
|
-
await
|
|
538
|
+
await ensureDispatchRunDirs(context);
|
|
570
539
|
const now = options.now ?? new Date();
|
|
571
540
|
const due = [];
|
|
572
|
-
for (const intent of await
|
|
573
|
-
if (await
|
|
541
|
+
for (const intent of await listDispatchRunIntents(context)) {
|
|
542
|
+
if (await isDispatchIntentDue(context, intent, now) &&
|
|
574
543
|
(options.includeLocalHandoffs === true || !isLocalHandoffIntent(intent)) &&
|
|
575
544
|
(!options.filter || await options.filter(intent))) {
|
|
576
545
|
due.push(intent);
|
|
@@ -581,7 +550,7 @@ export async function runDueDeferredRuns(context, options) {
|
|
|
581
550
|
}
|
|
582
551
|
const executions = [];
|
|
583
552
|
for (const intent of due) {
|
|
584
|
-
const claim = await
|
|
553
|
+
const claim = await claimDispatchRunIntent(context, intent, {
|
|
585
554
|
now,
|
|
586
555
|
leaseMs: options.leaseMs ?? 30 * 60 * 1000,
|
|
587
556
|
});
|
|
@@ -589,9 +558,9 @@ export async function runDueDeferredRuns(context, options) {
|
|
|
589
558
|
continue;
|
|
590
559
|
}
|
|
591
560
|
try {
|
|
592
|
-
const outputPath = path.join(
|
|
593
|
-
await writeJsonFileAtomic(
|
|
594
|
-
const result = await
|
|
561
|
+
const outputPath = path.join(dispatchOutputDir(context), `${claim.attempt.id}.json`);
|
|
562
|
+
await writeJsonFileAtomic(dispatchAttemptPath(context, claim.attempt.id), claim.attempt);
|
|
563
|
+
const result = await executeDispatchRunTarget(context, claim.intent, {
|
|
595
564
|
callToybox: options.callToybox,
|
|
596
565
|
workflowCwd: options.workflowCwd,
|
|
597
566
|
localHandoffMaterialize: options.localHandoffMaterialize,
|
|
@@ -614,8 +583,8 @@ export async function runDueDeferredRuns(context, options) {
|
|
|
614
583
|
completedAt: result.status === "completed" ? finishedAt : undefined,
|
|
615
584
|
error: result.error,
|
|
616
585
|
});
|
|
617
|
-
await writeJsonFileAtomic(
|
|
618
|
-
await writeJsonFileAtomic(
|
|
586
|
+
await writeJsonFileAtomic(dispatchAttemptPath(context, attempt.id), attempt);
|
|
587
|
+
await writeJsonFileAtomic(dispatchIntentPath(context, completedIntent.id), completedIntent);
|
|
619
588
|
executions.push({
|
|
620
589
|
intent: completedIntent,
|
|
621
590
|
attempt,
|
|
@@ -623,24 +592,24 @@ export async function runDueDeferredRuns(context, options) {
|
|
|
623
592
|
});
|
|
624
593
|
}
|
|
625
594
|
finally {
|
|
626
|
-
await
|
|
595
|
+
await releaseDispatchRunClaim(context, claim.intent.id);
|
|
627
596
|
}
|
|
628
597
|
}
|
|
629
598
|
return { mode: context.mode, executions };
|
|
630
599
|
}
|
|
631
|
-
export async function
|
|
600
|
+
export async function pruneDispatchRunHistory(context, options) {
|
|
632
601
|
if (!Number.isInteger(options.olderThanDays) || options.olderThanDays <= 0) {
|
|
633
602
|
throw new Error("olderThanDays must be a positive integer");
|
|
634
603
|
}
|
|
635
604
|
const now = options.now ?? new Date();
|
|
636
605
|
const cutoff = new Date(now.getTime() - options.olderThanDays * 24 * 60 * 60 * 1000).toISOString();
|
|
637
|
-
const intents = await
|
|
606
|
+
const intents = await listDispatchRunIntents(context);
|
|
638
607
|
const pruned = [];
|
|
639
608
|
for (const intent of intents) {
|
|
640
|
-
if (!
|
|
609
|
+
if (!isTerminalDispatchRunStatus(intent.status) || intent.updatedAt >= cutoff) {
|
|
641
610
|
continue;
|
|
642
611
|
}
|
|
643
|
-
const attempts = await
|
|
612
|
+
const attempts = await readDispatchRunAttempts(context, intent.attemptIds);
|
|
644
613
|
const outputPaths = attempts.flatMap((attempt) => attempt.outputPath ? [attempt.outputPath] : []);
|
|
645
614
|
pruned.push({
|
|
646
615
|
id: intent.id,
|
|
@@ -652,14 +621,14 @@ export async function pruneDeferredRunHistory(context, options) {
|
|
|
652
621
|
if (options.dryRun === true) {
|
|
653
622
|
continue;
|
|
654
623
|
}
|
|
655
|
-
await
|
|
624
|
+
await releaseDispatchRunClaim(context, intent.id);
|
|
656
625
|
for (const outputPath of outputPaths) {
|
|
657
626
|
await rm(outputPath, { force: true });
|
|
658
627
|
}
|
|
659
628
|
for (const attempt of attempts) {
|
|
660
|
-
await rm(
|
|
629
|
+
await rm(dispatchAttemptPath(context, attempt.id), { force: true });
|
|
661
630
|
}
|
|
662
|
-
await rm(
|
|
631
|
+
await rm(dispatchIntentPath(context, intent.id), { force: true });
|
|
663
632
|
}
|
|
664
633
|
return {
|
|
665
634
|
mode: context.mode,
|
|
@@ -673,13 +642,13 @@ export async function pruneDeferredRunHistory(context, options) {
|
|
|
673
642
|
async function runWorkbenchTask(context, config, task, options) {
|
|
674
643
|
const startedAt = new Date().toISOString();
|
|
675
644
|
const runId = workbenchRunId(task.id, startedAt);
|
|
676
|
-
const outputPath =
|
|
645
|
+
const outputPath = workbenchTaskOutputPath(context, task, runId);
|
|
677
646
|
try {
|
|
678
647
|
let result;
|
|
679
648
|
if (!task.enabled) {
|
|
680
649
|
result = { skipped: "disabled" };
|
|
681
650
|
const run = runRecord(context, runId, task.id, task.kind, startedAt, "skipped", outputPath);
|
|
682
|
-
await persistRun(context, run, result);
|
|
651
|
+
await persistRun(context, task, run, result);
|
|
683
652
|
return run;
|
|
684
653
|
}
|
|
685
654
|
if (task.kind === "workflow") {
|
|
@@ -692,18 +661,23 @@ async function runWorkbenchTask(context, config, task, options) {
|
|
|
692
661
|
result = await runSkill(task, context);
|
|
693
662
|
}
|
|
694
663
|
const run = runRecord(context, runId, task.id, task.kind, startedAt, "completed", outputPath);
|
|
695
|
-
await persistRun(context, run, result);
|
|
664
|
+
await persistRun(context, task, run, result);
|
|
696
665
|
return run;
|
|
697
666
|
}
|
|
698
667
|
catch (error) {
|
|
699
668
|
const run = runRecord(context, runId, task.id, task.kind, startedAt, "failed", outputPath, errorMessage(error));
|
|
700
|
-
await persistRun(context, run, { error: errorMessage(error) });
|
|
669
|
+
await persistRun(context, task, run, { error: errorMessage(error) });
|
|
701
670
|
return run;
|
|
702
671
|
}
|
|
703
672
|
}
|
|
704
673
|
function workbenchRunId(taskId, startedAt) {
|
|
705
674
|
return `${startedAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}-${taskId}`;
|
|
706
675
|
}
|
|
676
|
+
function workbenchTaskOutputPath(context, task, runId) {
|
|
677
|
+
return task.history === "latest"
|
|
678
|
+
? path.join(latestOutputDir(context), `${safeFileSegment(task.id)}.json`)
|
|
679
|
+
: path.join(context.stateRoot, "outputs", `${runId}.json`);
|
|
680
|
+
}
|
|
707
681
|
async function runWorkflowTask(context, config, task, runId, startedAt, options) {
|
|
708
682
|
const target = await resolveWorkflowTarget(task.workflow, {
|
|
709
683
|
cwd: context.repoRoot,
|
|
@@ -734,7 +708,7 @@ async function runWorkflowTask(context, config, task, runId, startedAt, options)
|
|
|
734
708
|
});
|
|
735
709
|
return scriptRun.result;
|
|
736
710
|
}
|
|
737
|
-
async function
|
|
711
|
+
async function executeDispatchRunTarget(context, intent, options) {
|
|
738
712
|
try {
|
|
739
713
|
const target = intent.target;
|
|
740
714
|
if (options.localHandoffMaterialize && isLocalHandoffIntent(intent)) {
|
|
@@ -762,10 +736,9 @@ async function executeDeferredRunTarget(context, intent, options) {
|
|
|
762
736
|
name: path.basename(context.repoRoot),
|
|
763
737
|
surfaces: [],
|
|
764
738
|
tasks: [],
|
|
765
|
-
reactive: [],
|
|
766
739
|
path: context.configPath,
|
|
767
740
|
}));
|
|
768
|
-
const result = await
|
|
741
|
+
const result = await runWorkflowDispatchTarget(context, config, { ...intent, target }, options);
|
|
769
742
|
return { status: "completed", output: result };
|
|
770
743
|
}
|
|
771
744
|
if (target.kind === "turn") {
|
|
@@ -825,12 +798,12 @@ async function materializeLocalHandoffIntent(context, intent, options) {
|
|
|
825
798
|
queue,
|
|
826
799
|
};
|
|
827
800
|
}
|
|
828
|
-
async function
|
|
801
|
+
async function runWorkflowDispatchTarget(context, config, intent, options) {
|
|
829
802
|
const target = await resolveWorkflowTarget(intent.target.workflow, {
|
|
830
803
|
cwd: context.repoRoot,
|
|
831
804
|
});
|
|
832
805
|
const startedAt = new Date().toISOString();
|
|
833
|
-
const event =
|
|
806
|
+
const event = dispatchWorkflowEvent(config, intent, startedAt);
|
|
834
807
|
const prompt = intent.target.prompt ?? target.prompt;
|
|
835
808
|
const cwd = intent.target.cwd ?? options.workflowCwd ?? target.cwd ?? context.repoRoot;
|
|
836
809
|
const scriptRun = await runWorkflowScript({
|
|
@@ -876,46 +849,24 @@ function workbenchWorkflowEvent(config, task, runId, startedAt) {
|
|
|
876
849
|
},
|
|
877
850
|
};
|
|
878
851
|
}
|
|
879
|
-
function
|
|
852
|
+
function dispatchWorkflowEvent(config, intent, startedAt) {
|
|
880
853
|
const event = intent.target.event ?? {};
|
|
881
854
|
const payload = isRecord(event.payload) ? event.payload : {};
|
|
882
855
|
return {
|
|
883
856
|
...event,
|
|
884
|
-
id: stringValue(event.id, `
|
|
857
|
+
id: stringValue(event.id, `dispatch:${config.name}:${intent.id}`),
|
|
885
858
|
type: stringValue(event.type, intent.target.workflow),
|
|
886
859
|
source: stringValue(event.source, config.name),
|
|
887
860
|
occurredAt: stringValue(event.occurredAt, intent.runAt),
|
|
888
861
|
receivedAt: stringValue(event.receivedAt, startedAt),
|
|
889
862
|
payload: {
|
|
890
|
-
|
|
863
|
+
dispatchRunId: intent.id,
|
|
891
864
|
...payload,
|
|
892
865
|
},
|
|
893
866
|
};
|
|
894
867
|
}
|
|
895
868
|
function exhaustiveTarget(value) {
|
|
896
|
-
throw new Error(`Unsupported
|
|
897
|
-
}
|
|
898
|
-
async function runReactiveRule(context, rule) {
|
|
899
|
-
const startedAt = new Date().toISOString();
|
|
900
|
-
const runId = `${startedAt.replace(/[:.]/g, "-")}-${rule.id}`;
|
|
901
|
-
const outputPath = path.join(context.stateRoot, "outputs", `${runId}.json`);
|
|
902
|
-
try {
|
|
903
|
-
const result = await runSkill({
|
|
904
|
-
id: rule.id,
|
|
905
|
-
enabled: rule.enabled,
|
|
906
|
-
kind: "skill",
|
|
907
|
-
skill: rule.skill,
|
|
908
|
-
var: `repair failures for ${rule.task}`,
|
|
909
|
-
}, context);
|
|
910
|
-
const run = runRecord(context, runId, rule.id, "reactive", startedAt, "completed", outputPath);
|
|
911
|
-
await persistRun(context, run, result);
|
|
912
|
-
return run;
|
|
913
|
-
}
|
|
914
|
-
catch (error) {
|
|
915
|
-
const run = runRecord(context, runId, rule.id, "reactive", startedAt, "failed", outputPath, errorMessage(error));
|
|
916
|
-
await persistRun(context, run, { error: errorMessage(error) });
|
|
917
|
-
return run;
|
|
918
|
-
}
|
|
869
|
+
throw new Error(`Unsupported dispatch run target: ${JSON.stringify(value)}`);
|
|
919
870
|
}
|
|
920
871
|
async function runSkill(task, context) {
|
|
921
872
|
const skillPath = path.join(context.runtimeCodexHome, "skills", task.skill, "SKILL.md");
|
|
@@ -971,18 +922,6 @@ async function runGit(cwd, args) {
|
|
|
971
922
|
}
|
|
972
923
|
return { stdout, stderr };
|
|
973
924
|
}
|
|
974
|
-
async function runSystemctlUser(args) {
|
|
975
|
-
const proc = spawn("systemctl", ["--user", ...args]);
|
|
976
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
977
|
-
collectText(proc.stdout),
|
|
978
|
-
collectText(proc.stderr),
|
|
979
|
-
exitCodeFor(proc),
|
|
980
|
-
]);
|
|
981
|
-
if (exitCode !== 0) {
|
|
982
|
-
throw new Error(`systemctl --user ${args.join(" ")} failed (${exitCode}): ${stderr || stdout}`);
|
|
983
|
-
}
|
|
984
|
-
return stdout;
|
|
985
|
-
}
|
|
986
925
|
function collectText(stream) {
|
|
987
926
|
return new Promise((resolve, reject) => {
|
|
988
927
|
let output = "";
|
|
@@ -1004,16 +943,20 @@ function exitCodeFor(child) {
|
|
|
1004
943
|
child.once("exit", (code) => resolve(code));
|
|
1005
944
|
});
|
|
1006
945
|
}
|
|
1007
|
-
async function persistRun(context, run, output) {
|
|
946
|
+
async function persistRun(context, task, run, output) {
|
|
1008
947
|
await ensureStateDirs(context);
|
|
1009
948
|
if (run.outputPath) {
|
|
1010
949
|
await writeFile(run.outputPath, `${JSON.stringify(output, null, 2)}\n`);
|
|
1011
950
|
}
|
|
1012
|
-
|
|
951
|
+
const runPath = task.history === "latest"
|
|
952
|
+
? path.join(latestRunDir(context), `${safeFileSegment(task.id)}.json`)
|
|
953
|
+
: path.join(context.stateRoot, "runs", `${run.id}.json`);
|
|
954
|
+
await writeFile(runPath, `${JSON.stringify(run, null, 2)}\n`);
|
|
1013
955
|
await writeHealth(context, run);
|
|
1014
956
|
}
|
|
1015
957
|
async function writeHealth(context, run) {
|
|
1016
|
-
const runs = [...await readRuns(context), run]
|
|
958
|
+
const runs = uniqueWorkbenchRuns([...await readRuns(context), run])
|
|
959
|
+
.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
1017
960
|
const health = {
|
|
1018
961
|
updatedAt: new Date().toISOString(),
|
|
1019
962
|
latestRun: run,
|
|
@@ -1025,79 +968,58 @@ async function writeHealth(context, run) {
|
|
|
1025
968
|
await writeFile(path.join(context.stateRoot, "health", "summary.json"), `${JSON.stringify(health, null, 2)}\n`);
|
|
1026
969
|
}
|
|
1027
970
|
async function readRuns(context) {
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
971
|
+
const dirs = [path.join(context.stateRoot, "runs"), latestRunDir(context)];
|
|
972
|
+
const runs = [];
|
|
973
|
+
for (const dir of dirs) {
|
|
974
|
+
try {
|
|
975
|
+
const entries = await readdir(dir);
|
|
976
|
+
for (const entry of entries) {
|
|
977
|
+
if (!entry.endsWith(".json")) {
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
const runPath = path.join(dir, entry);
|
|
982
|
+
const parsed = parseJsonText(await readFile(runPath, "utf8"), runPath);
|
|
983
|
+
if (isWorkbenchRunRecord(parsed)) {
|
|
984
|
+
runs.push(parsed);
|
|
985
|
+
}
|
|
1041
986
|
}
|
|
987
|
+
catch { }
|
|
1042
988
|
}
|
|
1043
|
-
catch { }
|
|
1044
989
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
catch {
|
|
1048
|
-
return [];
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
async function createScheduledWorkbenchTaskIntent(context, task, now) {
|
|
1052
|
-
try {
|
|
1053
|
-
return await createDeferredRunIntent(context, {
|
|
1054
|
-
id: scheduledDeferredRunId(task.id, now),
|
|
1055
|
-
runAt: now.toISOString(),
|
|
1056
|
-
target: {
|
|
1057
|
-
kind: "workbench-task",
|
|
1058
|
-
taskId: task.id,
|
|
1059
|
-
},
|
|
1060
|
-
createdBy: "workbench-schedule",
|
|
1061
|
-
reason: `Scheduled workbench task ${task.id}`,
|
|
1062
|
-
source: {
|
|
1063
|
-
kind: "workbench-task-schedule",
|
|
1064
|
-
taskId: task.id,
|
|
1065
|
-
schedule: task.schedule,
|
|
1066
|
-
date: now.toISOString().slice(0, 10),
|
|
1067
|
-
},
|
|
1068
|
-
});
|
|
1069
|
-
}
|
|
1070
|
-
catch (error) {
|
|
1071
|
-
if (isAlreadyExistsError(error)) {
|
|
1072
|
-
return undefined;
|
|
990
|
+
catch {
|
|
991
|
+
continue;
|
|
1073
992
|
}
|
|
1074
|
-
throw error;
|
|
1075
993
|
}
|
|
994
|
+
return uniqueWorkbenchRuns(runs);
|
|
995
|
+
}
|
|
996
|
+
function uniqueWorkbenchRuns(runs) {
|
|
997
|
+
return [...new Map(runs.map((run) => [run.id, run])).values()];
|
|
1076
998
|
}
|
|
1077
|
-
async function
|
|
1078
|
-
const intentPath =
|
|
999
|
+
async function readDispatchRunIntent(context, intentId) {
|
|
1000
|
+
const intentPath = dispatchIntentPath(context, intentId);
|
|
1079
1001
|
try {
|
|
1080
|
-
return
|
|
1002
|
+
return normalizeDispatchRunIntent(parseJsonText(await readFile(intentPath, "utf8"), intentPath));
|
|
1081
1003
|
}
|
|
1082
1004
|
catch (error) {
|
|
1083
1005
|
if (isNotFoundError(error)) {
|
|
1084
|
-
throw new Error(`Unknown
|
|
1006
|
+
throw new Error(`Unknown dispatch run: ${intentId}`);
|
|
1085
1007
|
}
|
|
1086
1008
|
throw error;
|
|
1087
1009
|
}
|
|
1088
1010
|
}
|
|
1089
|
-
async function
|
|
1011
|
+
async function readDispatchRunAttempts(context, attemptIds) {
|
|
1090
1012
|
const attempts = [];
|
|
1091
1013
|
for (const attemptId of attemptIds) {
|
|
1092
|
-
const attemptPath =
|
|
1014
|
+
const attemptPath = dispatchAttemptPath(context, attemptId);
|
|
1093
1015
|
try {
|
|
1094
|
-
attempts.push(
|
|
1016
|
+
attempts.push(normalizeDispatchRunAttempt(parseJsonText(await readFile(attemptPath, "utf8"), attemptPath)));
|
|
1095
1017
|
}
|
|
1096
1018
|
catch { }
|
|
1097
1019
|
}
|
|
1098
1020
|
return attempts.sort((left, right) => left.startedAt.localeCompare(right.startedAt));
|
|
1099
1021
|
}
|
|
1100
|
-
async function
|
|
1022
|
+
async function readDispatchRunAttemptOutputs(attempts) {
|
|
1101
1023
|
const outputs = [];
|
|
1102
1024
|
for (const attempt of attempts) {
|
|
1103
1025
|
if (!attempt.outputPath) {
|
|
@@ -1120,10 +1042,10 @@ async function readDeferredRunAttemptOutputs(attempts) {
|
|
|
1120
1042
|
}
|
|
1121
1043
|
return outputs;
|
|
1122
1044
|
}
|
|
1123
|
-
async function
|
|
1124
|
-
const file =
|
|
1045
|
+
async function readDispatchRunCollectCursor(context, cursor) {
|
|
1046
|
+
const file = dispatchCollectCursorPath(context, cursor);
|
|
1125
1047
|
try {
|
|
1126
|
-
return
|
|
1048
|
+
return normalizeDispatchRunCollectCursor(parseJsonText(await readFile(file, "utf8"), file), cursor);
|
|
1127
1049
|
}
|
|
1128
1050
|
catch (error) {
|
|
1129
1051
|
if (isNotFoundError(error)) {
|
|
@@ -1132,14 +1054,14 @@ async function readDeferredRunCollectCursor(context, cursor) {
|
|
|
1132
1054
|
throw error;
|
|
1133
1055
|
}
|
|
1134
1056
|
}
|
|
1135
|
-
async function
|
|
1136
|
-
const current = await
|
|
1137
|
-
if (!await
|
|
1057
|
+
async function claimDispatchRunIntent(context, intent, options) {
|
|
1058
|
+
const current = await readDispatchRunIntent(context, intent.id);
|
|
1059
|
+
if (!await isDispatchIntentDue(context, current, options.now)) {
|
|
1138
1060
|
return undefined;
|
|
1139
1061
|
}
|
|
1140
|
-
const claimPath =
|
|
1062
|
+
const claimPath = dispatchClaimPath(context, current.id);
|
|
1141
1063
|
const claimedAt = options.now.toISOString();
|
|
1142
|
-
const attemptId =
|
|
1064
|
+
const attemptId = dispatchAttemptId(current.id, claimedAt);
|
|
1143
1065
|
const leaseExpiresAt = new Date(options.now.getTime() + options.leaseMs).toISOString();
|
|
1144
1066
|
const executorId = `${process.pid}:${randomUUID()}`;
|
|
1145
1067
|
const claim = { intentId: current.id, attemptId, claimedAt, leaseExpiresAt, executorId };
|
|
@@ -1185,11 +1107,11 @@ async function claimDeferredRunIntent(context, intent, options) {
|
|
|
1185
1107
|
executorId,
|
|
1186
1108
|
},
|
|
1187
1109
|
};
|
|
1188
|
-
await writeJsonFileAtomic(
|
|
1110
|
+
await writeJsonFileAtomic(dispatchIntentPath(context, current.id), running);
|
|
1189
1111
|
return { intent: running, attempt };
|
|
1190
1112
|
}
|
|
1191
|
-
async function
|
|
1192
|
-
await rm(
|
|
1113
|
+
async function releaseDispatchRunClaim(context, intentId) {
|
|
1114
|
+
await rm(dispatchClaimPath(context, intentId), { force: true });
|
|
1193
1115
|
}
|
|
1194
1116
|
async function readClaimFile(file) {
|
|
1195
1117
|
try {
|
|
@@ -1202,13 +1124,13 @@ async function readClaimFile(file) {
|
|
|
1202
1124
|
return undefined;
|
|
1203
1125
|
}
|
|
1204
1126
|
}
|
|
1205
|
-
function
|
|
1127
|
+
function parseDispatchRunCreateParams(value) {
|
|
1206
1128
|
const input = record(value);
|
|
1207
1129
|
const runAt = optionalString(input.runAt);
|
|
1208
1130
|
if (runAt && Number.isNaN(Date.parse(runAt))) {
|
|
1209
|
-
throw new Error(`
|
|
1131
|
+
throw new Error(`Dispatch run runAt must be an ISO-compatible date: ${runAt}`);
|
|
1210
1132
|
}
|
|
1211
|
-
const target =
|
|
1133
|
+
const target = parseDispatchRunTarget(input.target);
|
|
1212
1134
|
const source = recordOrUndefined(input.source);
|
|
1213
1135
|
return compactUndefined({
|
|
1214
1136
|
id: optionalString(input.id),
|
|
@@ -1217,7 +1139,7 @@ function parseDeferredRunCreateParams(value) {
|
|
|
1217
1139
|
createdBy: optionalString(input.createdBy),
|
|
1218
1140
|
reason: optionalString(input.reason),
|
|
1219
1141
|
source,
|
|
1220
|
-
dependsOn:
|
|
1142
|
+
dependsOn: parseDispatchRunDependencies(input.dependsOn),
|
|
1221
1143
|
});
|
|
1222
1144
|
}
|
|
1223
1145
|
function parsePromptQueueEnqueueParams(value) {
|
|
@@ -1226,7 +1148,7 @@ function parsePromptQueueEnqueueParams(value) {
|
|
|
1226
1148
|
if (runAt && Number.isNaN(Date.parse(runAt))) {
|
|
1227
1149
|
throw new Error(`Prompt queue runAt must be an ISO-compatible date: ${runAt}`);
|
|
1228
1150
|
}
|
|
1229
|
-
const afterStatus =
|
|
1151
|
+
const afterStatus = dispatchDependencyStatusValue(input.afterStatus, "prompt queue afterStatus");
|
|
1230
1152
|
return compactUndefined({
|
|
1231
1153
|
id: optionalString(input.id),
|
|
1232
1154
|
runAt,
|
|
@@ -1257,7 +1179,7 @@ function parseLocalHandoffEnqueueParams(value) {
|
|
|
1257
1179
|
if (runAt && Number.isNaN(Date.parse(runAt))) {
|
|
1258
1180
|
throw new Error(`Local handoff runAt must be an ISO-compatible date: ${runAt}`);
|
|
1259
1181
|
}
|
|
1260
|
-
const afterStatus =
|
|
1182
|
+
const afterStatus = dispatchDependencyStatusValue(input.afterStatus, "local handoff afterStatus");
|
|
1261
1183
|
return compactUndefined({
|
|
1262
1184
|
id: optionalString(input.id),
|
|
1263
1185
|
runAt,
|
|
@@ -1286,30 +1208,30 @@ function parseLocalHandoffEnqueueParams(value) {
|
|
|
1286
1208
|
source: recordOrUndefined(input.source),
|
|
1287
1209
|
});
|
|
1288
1210
|
}
|
|
1289
|
-
function
|
|
1211
|
+
function parseDispatchRunDependencies(value) {
|
|
1290
1212
|
if (!Array.isArray(value)) {
|
|
1291
1213
|
return undefined;
|
|
1292
1214
|
}
|
|
1293
|
-
const dependencies = value.map(
|
|
1215
|
+
const dependencies = value.map(parseDispatchRunDependency);
|
|
1294
1216
|
return dependencies.length > 0 ? dependencies : undefined;
|
|
1295
1217
|
}
|
|
1296
|
-
function
|
|
1218
|
+
function parseDispatchRunDependency(value) {
|
|
1297
1219
|
const input = record(value);
|
|
1298
|
-
const kind = requiredString(input.kind, "
|
|
1299
|
-
if (kind !== "
|
|
1300
|
-
throw new Error(`Invalid
|
|
1220
|
+
const kind = requiredString(input.kind, "dispatch run dependency kind");
|
|
1221
|
+
if (kind !== "dispatch-run") {
|
|
1222
|
+
throw new Error(`Invalid dispatch run dependency kind: ${kind}`);
|
|
1301
1223
|
}
|
|
1302
1224
|
return compactUndefined({
|
|
1303
|
-
kind: "
|
|
1304
|
-
intentId: requiredString(input.intentId, "
|
|
1305
|
-
status:
|
|
1225
|
+
kind: "dispatch-run",
|
|
1226
|
+
intentId: requiredString(input.intentId, "dispatch run dependency intentId"),
|
|
1227
|
+
status: dispatchDependencyStatusValue(input.status, "dispatch run dependency status"),
|
|
1306
1228
|
});
|
|
1307
1229
|
}
|
|
1308
|
-
function
|
|
1230
|
+
function parseDispatchRunRetryParams(value) {
|
|
1309
1231
|
const input = record(value);
|
|
1310
1232
|
const runAt = optionalString(input.runAt);
|
|
1311
1233
|
if (runAt && Number.isNaN(Date.parse(runAt))) {
|
|
1312
|
-
throw new Error(`
|
|
1234
|
+
throw new Error(`Dispatch run retry runAt must be an ISO-compatible date: ${runAt}`);
|
|
1313
1235
|
}
|
|
1314
1236
|
return compactUndefined({
|
|
1315
1237
|
id: optionalString(input.id),
|
|
@@ -1319,13 +1241,13 @@ function parseDeferredRunRetryParams(value) {
|
|
|
1319
1241
|
source: recordOrUndefined(input.source),
|
|
1320
1242
|
});
|
|
1321
1243
|
}
|
|
1322
|
-
function
|
|
1244
|
+
function retryDispatchRunSource(originalIntent, source) {
|
|
1323
1245
|
const originalSource = recordOrUndefined(originalIntent.source) ?? {};
|
|
1324
1246
|
return compactUndefined({
|
|
1325
1247
|
...originalSource,
|
|
1326
|
-
kind: optionalString(originalSource.kind) ?? "
|
|
1248
|
+
kind: optionalString(originalSource.kind) ?? "dispatch-retry",
|
|
1327
1249
|
retry: compactUndefined({
|
|
1328
|
-
kind: "
|
|
1250
|
+
kind: "dispatch-retry",
|
|
1329
1251
|
originalIntentId: originalIntent.id,
|
|
1330
1252
|
originalStatus: originalIntent.status,
|
|
1331
1253
|
originalRunAt: originalIntent.runAt,
|
|
@@ -1396,30 +1318,30 @@ function isLocalHandoffIntent(intent, options = {}) {
|
|
|
1396
1318
|
}
|
|
1397
1319
|
return true;
|
|
1398
1320
|
}
|
|
1399
|
-
function
|
|
1321
|
+
function parseDispatchRunTarget(value) {
|
|
1400
1322
|
const target = record(value);
|
|
1401
|
-
const kind = requiredString(target.kind, "
|
|
1323
|
+
const kind = requiredString(target.kind, "dispatch run target kind");
|
|
1402
1324
|
if (kind === "workbench-task") {
|
|
1403
1325
|
return {
|
|
1404
1326
|
kind,
|
|
1405
|
-
taskId: requiredString(target.taskId, "
|
|
1327
|
+
taskId: requiredString(target.taskId, "dispatch run workbench-task taskId"),
|
|
1406
1328
|
};
|
|
1407
1329
|
}
|
|
1408
1330
|
if (kind === "workflow") {
|
|
1409
1331
|
return compactUndefined({
|
|
1410
1332
|
kind,
|
|
1411
|
-
workflow: requiredString(target.workflow, "
|
|
1333
|
+
workflow: requiredString(target.workflow, "dispatch run workflow target workflow"),
|
|
1412
1334
|
event: recordOrUndefined(target.event),
|
|
1413
1335
|
prompt: optionalString(target.prompt),
|
|
1414
1336
|
cwd: optionalString(target.cwd),
|
|
1415
1337
|
model: optionalString(target.model),
|
|
1416
|
-
sandbox: sandboxValue(target.sandbox, "
|
|
1417
|
-
approvalPolicy: approvalPolicyValue(target.approvalPolicy, "
|
|
1338
|
+
sandbox: sandboxValue(target.sandbox, "dispatch run workflow target sandbox"),
|
|
1339
|
+
approvalPolicy: approvalPolicyValue(target.approvalPolicy, "dispatch run workflow target approvalPolicy"),
|
|
1418
1340
|
permissions: optionalString(target.permissions),
|
|
1419
1341
|
});
|
|
1420
1342
|
}
|
|
1421
1343
|
if (kind === "turn") {
|
|
1422
|
-
const prompt = requiredString(target.prompt, "
|
|
1344
|
+
const prompt = requiredString(target.prompt, "dispatch run turn target prompt");
|
|
1423
1345
|
return compactUndefined({
|
|
1424
1346
|
kind,
|
|
1425
1347
|
prompt,
|
|
@@ -1427,39 +1349,39 @@ function parseDeferredRunTarget(value) {
|
|
|
1427
1349
|
cwd: optionalString(target.cwd),
|
|
1428
1350
|
model: optionalString(target.model),
|
|
1429
1351
|
serviceTier: optionalString(target.serviceTier),
|
|
1430
|
-
effort: reasoningEffortValue(target.effort, "
|
|
1431
|
-
sandbox: sandboxValue(target.sandbox, "
|
|
1432
|
-
approvalPolicy: approvalPolicyValue(target.approvalPolicy, "
|
|
1352
|
+
effort: reasoningEffortValue(target.effort, "dispatch run turn target effort"),
|
|
1353
|
+
sandbox: sandboxValue(target.sandbox, "dispatch run turn target sandbox"),
|
|
1354
|
+
approvalPolicy: approvalPolicyValue(target.approvalPolicy, "dispatch run turn target approvalPolicy"),
|
|
1433
1355
|
permissions: optionalString(target.permissions),
|
|
1434
1356
|
responsesapiClientMetadata: stringRecord(target.responsesapiClientMetadata),
|
|
1435
1357
|
outputSchema: target.outputSchema,
|
|
1436
1358
|
});
|
|
1437
1359
|
}
|
|
1438
|
-
throw new Error(`Invalid
|
|
1360
|
+
throw new Error(`Invalid dispatch run target kind: ${kind}`);
|
|
1439
1361
|
}
|
|
1440
|
-
function
|
|
1362
|
+
function normalizeDispatchRunIntent(value) {
|
|
1441
1363
|
const input = record(value);
|
|
1442
1364
|
return {
|
|
1443
|
-
id: requiredString(input.id, "
|
|
1444
|
-
status:
|
|
1365
|
+
id: requiredString(input.id, "dispatch run id"),
|
|
1366
|
+
status: dispatchRunStatus(input.status),
|
|
1445
1367
|
mode: workbenchMode(input.mode),
|
|
1446
|
-
runAt: requiredString(input.runAt, "
|
|
1447
|
-
target:
|
|
1448
|
-
createdAt: requiredString(input.createdAt, "
|
|
1449
|
-
updatedAt: requiredString(input.updatedAt, "
|
|
1368
|
+
runAt: requiredString(input.runAt, "dispatch run runAt"),
|
|
1369
|
+
target: parseDispatchRunTarget(input.target),
|
|
1370
|
+
createdAt: requiredString(input.createdAt, "dispatch run createdAt"),
|
|
1371
|
+
updatedAt: requiredString(input.updatedAt, "dispatch run updatedAt"),
|
|
1450
1372
|
createdBy: optionalString(input.createdBy),
|
|
1451
1373
|
reason: optionalString(input.reason),
|
|
1452
1374
|
source: recordOrUndefined(input.source),
|
|
1453
|
-
dependsOn:
|
|
1375
|
+
dependsOn: parseDispatchRunDependencies(input.dependsOn),
|
|
1454
1376
|
attemptIds: Array.isArray(input.attemptIds)
|
|
1455
1377
|
? input.attemptIds.filter((entry) => typeof entry === "string")
|
|
1456
1378
|
: [],
|
|
1457
1379
|
lease: isRecord(input.lease)
|
|
1458
1380
|
? {
|
|
1459
|
-
attemptId: requiredString(input.lease.attemptId, "
|
|
1460
|
-
claimedAt: requiredString(input.lease.claimedAt, "
|
|
1461
|
-
expiresAt: requiredString(input.lease.expiresAt, "
|
|
1462
|
-
executorId: requiredString(input.lease.executorId, "
|
|
1381
|
+
attemptId: requiredString(input.lease.attemptId, "dispatch run lease attemptId"),
|
|
1382
|
+
claimedAt: requiredString(input.lease.claimedAt, "dispatch run lease claimedAt"),
|
|
1383
|
+
expiresAt: requiredString(input.lease.expiresAt, "dispatch run lease expiresAt"),
|
|
1384
|
+
executorId: requiredString(input.lease.executorId, "dispatch run lease executorId"),
|
|
1463
1385
|
}
|
|
1464
1386
|
: undefined,
|
|
1465
1387
|
completedAt: optionalString(input.completedAt),
|
|
@@ -1467,101 +1389,58 @@ function normalizeDeferredRunIntent(value) {
|
|
|
1467
1389
|
error: optionalString(input.error),
|
|
1468
1390
|
};
|
|
1469
1391
|
}
|
|
1470
|
-
function
|
|
1392
|
+
function normalizeDispatchRunAttempt(value) {
|
|
1471
1393
|
const input = record(value);
|
|
1472
1394
|
return {
|
|
1473
|
-
id: requiredString(input.id, "
|
|
1474
|
-
intentId: requiredString(input.intentId, "
|
|
1475
|
-
status:
|
|
1395
|
+
id: requiredString(input.id, "dispatch run attempt id"),
|
|
1396
|
+
intentId: requiredString(input.intentId, "dispatch run attempt intentId"),
|
|
1397
|
+
status: dispatchAttemptStatus(input.status),
|
|
1476
1398
|
mode: workbenchMode(input.mode),
|
|
1477
|
-
startedAt: requiredString(input.startedAt, "
|
|
1399
|
+
startedAt: requiredString(input.startedAt, "dispatch run attempt startedAt"),
|
|
1478
1400
|
finishedAt: optionalString(input.finishedAt),
|
|
1479
|
-
executorId: requiredString(input.executorId, "
|
|
1480
|
-
leaseExpiresAt: requiredString(input.leaseExpiresAt, "
|
|
1401
|
+
executorId: requiredString(input.executorId, "dispatch run attempt executorId"),
|
|
1402
|
+
leaseExpiresAt: requiredString(input.leaseExpiresAt, "dispatch run attempt leaseExpiresAt"),
|
|
1481
1403
|
outputPath: optionalString(input.outputPath),
|
|
1482
1404
|
error: optionalString(input.error),
|
|
1483
1405
|
};
|
|
1484
1406
|
}
|
|
1485
|
-
function
|
|
1407
|
+
function normalizeDispatchRunCollectCursor(value, fallbackCursor) {
|
|
1486
1408
|
const input = record(value);
|
|
1487
1409
|
return compactUndefined({
|
|
1488
1410
|
cursor: optionalString(input.cursor) ?? fallbackCursor,
|
|
1489
|
-
updatedAt: requiredString(input.updatedAt, "
|
|
1411
|
+
updatedAt: requiredString(input.updatedAt, "dispatch run collect cursor updatedAt"),
|
|
1490
1412
|
lastUpdatedAt: optionalString(input.lastUpdatedAt),
|
|
1491
1413
|
lastIntentId: optionalString(input.lastIntentId),
|
|
1492
1414
|
});
|
|
1493
1415
|
}
|
|
1494
|
-
function
|
|
1495
|
-
return tasks.filter((task) => {
|
|
1496
|
-
if (!task.enabled) {
|
|
1497
|
-
return false;
|
|
1498
|
-
}
|
|
1499
|
-
if (!task.schedule) {
|
|
1500
|
-
return false;
|
|
1501
|
-
}
|
|
1502
|
-
return isScheduleDue(task.schedule, now) &&
|
|
1503
|
-
!hasRunForDate(task.id, runs, now) &&
|
|
1504
|
-
!hasScheduledIntentForDate(task.id, intents, now);
|
|
1505
|
-
});
|
|
1506
|
-
}
|
|
1507
|
-
function isScheduleDue(schedule, now) {
|
|
1508
|
-
const parts = schedule.trim().split(/\s+/);
|
|
1509
|
-
if (parts.length !== 5) {
|
|
1510
|
-
throw new Error(`Invalid workbench task schedule: ${schedule}`);
|
|
1511
|
-
}
|
|
1512
|
-
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
1513
|
-
return cronPartMatches(minute, now.getUTCMinutes()) &&
|
|
1514
|
-
cronPartMatches(hour, now.getUTCHours()) &&
|
|
1515
|
-
cronPartMatches(dayOfMonth, now.getUTCDate()) &&
|
|
1516
|
-
cronPartMatches(month, now.getUTCMonth() + 1) &&
|
|
1517
|
-
cronPartMatches(dayOfWeek, now.getUTCDay());
|
|
1518
|
-
}
|
|
1519
|
-
function cronPartMatches(part, value) {
|
|
1520
|
-
if (!part || part === "*") {
|
|
1521
|
-
return true;
|
|
1522
|
-
}
|
|
1523
|
-
return part.split(",").some((item) => Number.parseInt(item, 10) === value);
|
|
1524
|
-
}
|
|
1525
|
-
function hasRunForDate(taskId, runs, now) {
|
|
1526
|
-
const today = now.toISOString().slice(0, 10);
|
|
1527
|
-
return runs.some((run) => run.taskId === taskId && run.startedAt.startsWith(today));
|
|
1528
|
-
}
|
|
1529
|
-
function hasScheduledIntentForDate(taskId, intents, now) {
|
|
1530
|
-
const expected = scheduledDeferredRunId(taskId, now);
|
|
1531
|
-
return intents.some((intent) => intent.id === expected ||
|
|
1532
|
-
(intent.target.kind === "workbench-task" &&
|
|
1533
|
-
intent.target.taskId === taskId &&
|
|
1534
|
-
intent.source?.kind === "workbench-task-schedule" &&
|
|
1535
|
-
intent.source.date === now.toISOString().slice(0, 10)));
|
|
1536
|
-
}
|
|
1537
|
-
async function isDeferredIntentDue(context, intent, now) {
|
|
1416
|
+
async function isDispatchIntentDue(context, intent, now) {
|
|
1538
1417
|
if (intent.status === "pending") {
|
|
1539
1418
|
return intent.runAt <= now.toISOString() &&
|
|
1540
|
-
await
|
|
1419
|
+
await areDispatchRunDependenciesSatisfied(context, intent.dependsOn);
|
|
1541
1420
|
}
|
|
1542
1421
|
if (intent.status === "running" && intent.lease?.expiresAt) {
|
|
1543
1422
|
return intent.lease.expiresAt <= now.toISOString();
|
|
1544
1423
|
}
|
|
1545
1424
|
return false;
|
|
1546
1425
|
}
|
|
1547
|
-
async function
|
|
1426
|
+
async function areDispatchRunDependenciesSatisfied(context, dependencies) {
|
|
1548
1427
|
if (!dependencies || dependencies.length === 0) {
|
|
1549
1428
|
return true;
|
|
1550
1429
|
}
|
|
1551
1430
|
for (const dependency of dependencies) {
|
|
1552
|
-
if (dependency.kind !== "
|
|
1431
|
+
if (dependency.kind !== "dispatch-run") {
|
|
1553
1432
|
return false;
|
|
1554
1433
|
}
|
|
1555
1434
|
let intent;
|
|
1556
1435
|
try {
|
|
1557
|
-
intent = await
|
|
1436
|
+
intent = await readDispatchRunIntent(context, dependency.intentId);
|
|
1558
1437
|
}
|
|
1559
1438
|
catch {
|
|
1560
1439
|
return false;
|
|
1561
1440
|
}
|
|
1562
1441
|
const status = dependency.status ?? "completed";
|
|
1563
1442
|
if (status === "terminal") {
|
|
1564
|
-
if (!
|
|
1443
|
+
if (!isTerminalDispatchRunStatus(intent.status)) {
|
|
1565
1444
|
return false;
|
|
1566
1445
|
}
|
|
1567
1446
|
continue;
|
|
@@ -1572,10 +1451,10 @@ async function areDeferredRunDependenciesSatisfied(context, dependencies) {
|
|
|
1572
1451
|
}
|
|
1573
1452
|
return true;
|
|
1574
1453
|
}
|
|
1575
|
-
function
|
|
1454
|
+
function isTerminalDispatchRunStatus(status) {
|
|
1576
1455
|
return status === "completed" || status === "failed" || status === "canceled";
|
|
1577
1456
|
}
|
|
1578
|
-
function
|
|
1457
|
+
function isAfterDispatchRunCollectCursor(intent, cursor) {
|
|
1579
1458
|
if (!cursor?.lastUpdatedAt) {
|
|
1580
1459
|
return true;
|
|
1581
1460
|
}
|
|
@@ -1603,182 +1482,6 @@ function workbenchDoctorErrors(context) {
|
|
|
1603
1482
|
}
|
|
1604
1483
|
return [];
|
|
1605
1484
|
}
|
|
1606
|
-
async function collectWorkbenchRunnerInfo(context, tasks, deferredRuns, probe) {
|
|
1607
|
-
const workbenchRoot = path.resolve(context.repoRoot);
|
|
1608
|
-
const hasScheduledWork = tasks.some((task) => task.enabled && task.schedule);
|
|
1609
|
-
const hasPendingDeferredWork = deferredRuns.some((intent) => intent.status === "pending" || intent.status === "running");
|
|
1610
|
-
const hasRunnableWork = hasScheduledWork || hasPendingDeferredWork;
|
|
1611
|
-
const base = {
|
|
1612
|
-
kind: "systemd-user",
|
|
1613
|
-
workbenchRoot,
|
|
1614
|
-
candidates: [],
|
|
1615
|
-
};
|
|
1616
|
-
if (context.mode !== "local") {
|
|
1617
|
-
return {
|
|
1618
|
-
...base,
|
|
1619
|
-
status: "unsupported",
|
|
1620
|
-
warning: hasRunnableWork
|
|
1621
|
-
? "Runner visibility currently checks local systemd user timers only."
|
|
1622
|
-
: undefined,
|
|
1623
|
-
};
|
|
1624
|
-
}
|
|
1625
|
-
if (os.platform() !== "linux") {
|
|
1626
|
-
return {
|
|
1627
|
-
...base,
|
|
1628
|
-
status: "unsupported",
|
|
1629
|
-
warning: hasRunnableWork
|
|
1630
|
-
? "No local systemd user timer check is available on this platform."
|
|
1631
|
-
: undefined,
|
|
1632
|
-
};
|
|
1633
|
-
}
|
|
1634
|
-
try {
|
|
1635
|
-
const timerRows = parseSystemdTimerRows(await probe(["list-timers", "--all", "--no-legend", "--no-pager"]));
|
|
1636
|
-
const candidates = [];
|
|
1637
|
-
for (const row of timerRows) {
|
|
1638
|
-
const serviceShow = parseSystemdShow(await probe([
|
|
1639
|
-
"show",
|
|
1640
|
-
row.service,
|
|
1641
|
-
"--property=ExecStart",
|
|
1642
|
-
"--property=ActiveState",
|
|
1643
|
-
"--property=UnitFileState",
|
|
1644
|
-
"--no-pager",
|
|
1645
|
-
]));
|
|
1646
|
-
const command = normalizeSystemdCommand(serviceShow.ExecStart);
|
|
1647
|
-
if (!command.includes("codex-toys")) {
|
|
1648
|
-
continue;
|
|
1649
|
-
}
|
|
1650
|
-
const runsWorkbenchTick = /\bworkbench\s+tick\b/.test(command);
|
|
1651
|
-
const runsDeferredOnly = /\bworkbench\s+deferred\s+run-due\b/.test(command);
|
|
1652
|
-
if (!runsWorkbenchTick && !runsDeferredOnly) {
|
|
1653
|
-
continue;
|
|
1654
|
-
}
|
|
1655
|
-
const timerShow = parseSystemdShow(await probe([
|
|
1656
|
-
"show",
|
|
1657
|
-
row.timer,
|
|
1658
|
-
"--property=ActiveState",
|
|
1659
|
-
"--property=UnitFileState",
|
|
1660
|
-
"--property=NextElapseUSecRealtime",
|
|
1661
|
-
"--property=LastTriggerUSec",
|
|
1662
|
-
"--no-pager",
|
|
1663
|
-
]));
|
|
1664
|
-
const runnerWorkbenchRoot = extractWorkbenchRootFromCommand(command);
|
|
1665
|
-
const matchesWorkbench = runnerWorkbenchRoot
|
|
1666
|
-
? path.resolve(runnerWorkbenchRoot) === workbenchRoot
|
|
1667
|
-
: command.includes(workbenchRoot);
|
|
1668
|
-
candidates.push(compactUndefined({
|
|
1669
|
-
kind: "systemd-user",
|
|
1670
|
-
timer: row.timer,
|
|
1671
|
-
service: row.service,
|
|
1672
|
-
command,
|
|
1673
|
-
activeState: serviceShow.ActiveState,
|
|
1674
|
-
unitFileState: serviceShow.UnitFileState,
|
|
1675
|
-
timerActiveState: timerShow.ActiveState,
|
|
1676
|
-
timerUnitFileState: timerShow.UnitFileState,
|
|
1677
|
-
nextTrigger: timerShow.NextElapseUSecRealtime,
|
|
1678
|
-
lastTrigger: timerShow.LastTriggerUSec,
|
|
1679
|
-
workbenchRoot: runnerWorkbenchRoot,
|
|
1680
|
-
runsWorkbenchTick,
|
|
1681
|
-
runsDeferredOnly,
|
|
1682
|
-
matchesWorkbench,
|
|
1683
|
-
}));
|
|
1684
|
-
}
|
|
1685
|
-
const selected = candidates.find((candidate) => candidate.matchesWorkbench &&
|
|
1686
|
-
candidate.runsWorkbenchTick &&
|
|
1687
|
-
candidate.timerActiveState === "active") ?? candidates.find((candidate) => candidate.matchesWorkbench &&
|
|
1688
|
-
candidate.timerActiveState === "active") ?? candidates.find((candidate) => candidate.matchesWorkbench &&
|
|
1689
|
-
candidate.runsWorkbenchTick) ?? candidates.find((candidate) => candidate.matchesWorkbench);
|
|
1690
|
-
if (!selected) {
|
|
1691
|
-
return {
|
|
1692
|
-
...base,
|
|
1693
|
-
status: "missing",
|
|
1694
|
-
candidates,
|
|
1695
|
-
warning: hasRunnableWork
|
|
1696
|
-
? "No matching local runner was found; due work needs a manual tick or another scheduler."
|
|
1697
|
-
: undefined,
|
|
1698
|
-
};
|
|
1699
|
-
}
|
|
1700
|
-
const status = selected.timerActiveState === "active" ? "active" : "inactive";
|
|
1701
|
-
return {
|
|
1702
|
-
...base,
|
|
1703
|
-
status,
|
|
1704
|
-
selected,
|
|
1705
|
-
candidates,
|
|
1706
|
-
warning: selected.runsDeferredOnly
|
|
1707
|
-
? "The matching runner only runs deferred work; scheduled tasks need workbench tick."
|
|
1708
|
-
: status === "inactive" && hasRunnableWork
|
|
1709
|
-
? "The matching local runner is not active; due work needs a manual tick or another scheduler."
|
|
1710
|
-
: undefined,
|
|
1711
|
-
};
|
|
1712
|
-
}
|
|
1713
|
-
catch (error) {
|
|
1714
|
-
return {
|
|
1715
|
-
...base,
|
|
1716
|
-
status: "unknown",
|
|
1717
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1718
|
-
warning: hasRunnableWork
|
|
1719
|
-
? "Could not inspect local runner status; due work may need a manual tick or another scheduler."
|
|
1720
|
-
: undefined,
|
|
1721
|
-
};
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
function parseSystemdTimerRows(output) {
|
|
1725
|
-
const rows = [];
|
|
1726
|
-
for (const line of output.split(/\r?\n/)) {
|
|
1727
|
-
const trimmed = line.trim();
|
|
1728
|
-
if (!trimmed) {
|
|
1729
|
-
continue;
|
|
1730
|
-
}
|
|
1731
|
-
const fields = trimmed.split(/\s+/);
|
|
1732
|
-
const timerIndex = fields.findIndex((field) => field.endsWith(".timer"));
|
|
1733
|
-
if (timerIndex < 0) {
|
|
1734
|
-
continue;
|
|
1735
|
-
}
|
|
1736
|
-
const timer = fields[timerIndex];
|
|
1737
|
-
const service = fields[timerIndex + 1];
|
|
1738
|
-
if (!timer || !service?.endsWith(".service")) {
|
|
1739
|
-
continue;
|
|
1740
|
-
}
|
|
1741
|
-
rows.push({ timer, service });
|
|
1742
|
-
}
|
|
1743
|
-
return rows;
|
|
1744
|
-
}
|
|
1745
|
-
function parseSystemdShow(output) {
|
|
1746
|
-
const result = {};
|
|
1747
|
-
for (const line of output.split(/\r?\n/)) {
|
|
1748
|
-
const index = line.indexOf("=");
|
|
1749
|
-
if (index <= 0) {
|
|
1750
|
-
continue;
|
|
1751
|
-
}
|
|
1752
|
-
result[line.slice(0, index)] = line.slice(index + 1);
|
|
1753
|
-
}
|
|
1754
|
-
return result;
|
|
1755
|
-
}
|
|
1756
|
-
function normalizeSystemdCommand(value) {
|
|
1757
|
-
return (value ?? "")
|
|
1758
|
-
.replace(/\\x([0-9a-fA-F]{2})/g, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
|
|
1759
|
-
.replace(/\s+/g, " ")
|
|
1760
|
-
.trim();
|
|
1761
|
-
}
|
|
1762
|
-
function extractWorkbenchRootFromCommand(command) {
|
|
1763
|
-
const match = command.match(/--workbench-root(?:=|\s+)(?:"([^"]+)"|'([^']+)'|([^\s;]+))/);
|
|
1764
|
-
return match?.[1] ?? match?.[2] ?? match?.[3];
|
|
1765
|
-
}
|
|
1766
|
-
function formatWorkbenchRunnerInfo(runner) {
|
|
1767
|
-
if (!runner) {
|
|
1768
|
-
return "not checked";
|
|
1769
|
-
}
|
|
1770
|
-
if (runner.selected) {
|
|
1771
|
-
const command = runner.selected.runsWorkbenchTick ? "workbench tick" : "workbench deferred run-due";
|
|
1772
|
-
return `${runner.status} ${runner.selected.timer} -> ${runner.selected.service} (${command})`;
|
|
1773
|
-
}
|
|
1774
|
-
if (runner.status === "unsupported") {
|
|
1775
|
-
return "unsupported";
|
|
1776
|
-
}
|
|
1777
|
-
if (runner.status === "unknown") {
|
|
1778
|
-
return `unknown${runner.error ? ` (${runner.error})` : ""}`;
|
|
1779
|
-
}
|
|
1780
|
-
return `${runner.status}${runner.candidates.length > 0 ? ` (${runner.candidates.length} codex-toys runner candidates)` : ""}`;
|
|
1781
|
-
}
|
|
1782
1485
|
function consecutiveFailures(taskId, runs) {
|
|
1783
1486
|
let count = 0;
|
|
1784
1487
|
for (const run of runs.filter((item) => item.taskId === taskId).sort((a, b) => b.startedAt.localeCompare(a.startedAt))) {
|
|
@@ -1803,73 +1506,76 @@ function runRecord(context, id, taskId, kind, startedAt, status, outputPath, err
|
|
|
1803
1506
|
};
|
|
1804
1507
|
}
|
|
1805
1508
|
async function ensureStateDirs(context) {
|
|
1806
|
-
for (const name of ["state", "runs", "outputs", "health"]) {
|
|
1509
|
+
for (const name of ["state", "runs", "outputs", "latest-runs", "latest-outputs", "health"]) {
|
|
1807
1510
|
await mkdir(path.join(context.stateRoot, name), { recursive: true });
|
|
1808
1511
|
}
|
|
1809
1512
|
}
|
|
1810
|
-
async function
|
|
1513
|
+
async function ensureDispatchRunDirs(context) {
|
|
1811
1514
|
for (const dir of [
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1515
|
+
dispatchIntentDir(context),
|
|
1516
|
+
dispatchAttemptDir(context),
|
|
1517
|
+
dispatchOutputDir(context),
|
|
1518
|
+
dispatchClaimDir(context),
|
|
1519
|
+
dispatchCollectCursorDir(context),
|
|
1817
1520
|
]) {
|
|
1818
1521
|
await mkdir(dir, { recursive: true });
|
|
1819
1522
|
}
|
|
1820
1523
|
}
|
|
1821
|
-
function
|
|
1822
|
-
return path.join(context.stateRoot, "
|
|
1524
|
+
function dispatchRoot(context) {
|
|
1525
|
+
return path.join(context.stateRoot, "dispatch");
|
|
1823
1526
|
}
|
|
1824
|
-
function
|
|
1825
|
-
return path.join(
|
|
1527
|
+
function dispatchIntentDir(context) {
|
|
1528
|
+
return path.join(dispatchRoot(context), "intents");
|
|
1826
1529
|
}
|
|
1827
|
-
function
|
|
1828
|
-
return path.join(
|
|
1530
|
+
function dispatchAttemptDir(context) {
|
|
1531
|
+
return path.join(dispatchRoot(context), "attempts");
|
|
1829
1532
|
}
|
|
1830
|
-
function
|
|
1831
|
-
return path.join(
|
|
1533
|
+
function dispatchOutputDir(context) {
|
|
1534
|
+
return path.join(dispatchRoot(context), "outputs");
|
|
1832
1535
|
}
|
|
1833
|
-
function
|
|
1834
|
-
return path.join(
|
|
1536
|
+
function dispatchClaimDir(context) {
|
|
1537
|
+
return path.join(dispatchRoot(context), "claims");
|
|
1835
1538
|
}
|
|
1836
|
-
function
|
|
1837
|
-
return path.join(
|
|
1539
|
+
function dispatchCollectCursorDir(context) {
|
|
1540
|
+
return path.join(dispatchRoot(context), "collect-cursors");
|
|
1838
1541
|
}
|
|
1839
|
-
function
|
|
1840
|
-
return path.join(
|
|
1542
|
+
function dispatchIntentPath(context, intentId) {
|
|
1543
|
+
return path.join(dispatchIntentDir(context), `${safeFileSegment(intentId)}.json`);
|
|
1841
1544
|
}
|
|
1842
|
-
function
|
|
1843
|
-
return path.join(
|
|
1545
|
+
function dispatchAttemptPath(context, attemptId) {
|
|
1546
|
+
return path.join(dispatchAttemptDir(context), `${safeFileSegment(attemptId)}.json`);
|
|
1844
1547
|
}
|
|
1845
|
-
function
|
|
1846
|
-
return path.join(
|
|
1548
|
+
function dispatchClaimPath(context, intentId) {
|
|
1549
|
+
return path.join(dispatchClaimDir(context), `${safeFileSegment(intentId)}.json`);
|
|
1847
1550
|
}
|
|
1848
|
-
function
|
|
1849
|
-
return path.join(
|
|
1551
|
+
function dispatchCollectCursorPath(context, cursor) {
|
|
1552
|
+
return path.join(dispatchCollectCursorDir(context), `${safeFileSegment(cursor)}.json`);
|
|
1850
1553
|
}
|
|
1851
|
-
function
|
|
1554
|
+
function dispatchCollectCursorName(value, defaultCursor = "default") {
|
|
1852
1555
|
const cursor = value?.trim() || defaultCursor;
|
|
1853
1556
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(cursor)) {
|
|
1854
|
-
throw new Error(`Invalid
|
|
1557
|
+
throw new Error(`Invalid dispatch collect cursor: ${cursor}`);
|
|
1855
1558
|
}
|
|
1856
1559
|
return cursor;
|
|
1857
1560
|
}
|
|
1858
|
-
function
|
|
1859
|
-
return `
|
|
1561
|
+
function dispatchRunId(createdAt) {
|
|
1562
|
+
return `dispatch-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
1860
1563
|
}
|
|
1861
|
-
function
|
|
1564
|
+
function dispatchRetryRunId(originalIntentId, createdAt) {
|
|
1862
1565
|
return `retry-${safeFileSegment(originalIntentId).slice(0, 40)}-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
1863
1566
|
}
|
|
1864
|
-
function
|
|
1567
|
+
function dispatchAttemptId(intentId, startedAt) {
|
|
1865
1568
|
return `${startedAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}-${safeFileSegment(intentId).slice(0, 48)}`;
|
|
1866
1569
|
}
|
|
1867
|
-
function
|
|
1868
|
-
return
|
|
1570
|
+
function latestRunDir(context) {
|
|
1571
|
+
return path.join(context.stateRoot, "latest-runs");
|
|
1572
|
+
}
|
|
1573
|
+
function latestOutputDir(context) {
|
|
1574
|
+
return path.join(context.stateRoot, "latest-outputs");
|
|
1869
1575
|
}
|
|
1870
1576
|
function safeFileSegment(value) {
|
|
1871
1577
|
const safe = value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1872
|
-
return safe.slice(0, 120) || "
|
|
1578
|
+
return safe.slice(0, 120) || "dispatch-run";
|
|
1873
1579
|
}
|
|
1874
1580
|
async function writeNewJsonFile(file, value) {
|
|
1875
1581
|
await mkdir(path.dirname(file), { recursive: true });
|
|
@@ -1944,10 +1650,10 @@ function codexConfigTemplate() {
|
|
|
1944
1650
|
"",
|
|
1945
1651
|
].join("\n");
|
|
1946
1652
|
}
|
|
1947
|
-
function actionsWorkflowTemplate(provider) {
|
|
1653
|
+
function actionsWorkflowTemplate(provider, runnerImage) {
|
|
1948
1654
|
const checkout = provider === "github" ? "actions/checkout@v4" : "actions/checkout@v4";
|
|
1949
1655
|
const setupNode = provider === "github" ? "actions/setup-node@v4" : "actions/setup-node@v4";
|
|
1950
|
-
|
|
1656
|
+
const lines = [
|
|
1951
1657
|
"name: Codex Toys Actions",
|
|
1952
1658
|
"",
|
|
1953
1659
|
"on:",
|
|
@@ -1958,38 +1664,19 @@ function actionsWorkflowTemplate(provider) {
|
|
|
1958
1664
|
"jobs:",
|
|
1959
1665
|
" workbench:",
|
|
1960
1666
|
" runs-on: ubuntu-latest",
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
"
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
"
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
"
|
|
1971
|
-
|
|
1972
|
-
" CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}",
|
|
1973
|
-
|
|
1974
|
-
" - run: vp dlx codex-toys workbench tick --mode actions",
|
|
1975
|
-
" - if: always()",
|
|
1976
|
-
" run: vp dlx codex-toys actions cleanup",
|
|
1977
|
-
" - if: always()",
|
|
1978
|
-
" run: |",
|
|
1979
|
-
" git add -- .codex/memories .codex/workbench/actions",
|
|
1980
|
-
" if [ -d .codex/feed/actions ]; then",
|
|
1981
|
-
" git add -- .codex/feed/actions",
|
|
1982
|
-
" fi",
|
|
1983
|
-
" if [ -d .codex/sessions ]; then",
|
|
1984
|
-
" git add -A -f -- .codex/sessions",
|
|
1985
|
-
" fi",
|
|
1986
|
-
" git diff --cached --quiet && exit 0",
|
|
1987
|
-
" git config user.name codex-toys-actions",
|
|
1988
|
-
" git config user.email codex-toys-actions@users.noreply.github.com",
|
|
1989
|
-
" git commit -m \"Update Codex workbench state\"",
|
|
1990
|
-
" git push",
|
|
1991
|
-
"",
|
|
1992
|
-
].join("\n");
|
|
1667
|
+
];
|
|
1668
|
+
if (runnerImage) {
|
|
1669
|
+
lines.push(" container:", ` image: ${runnerImage}`);
|
|
1670
|
+
}
|
|
1671
|
+
lines.push(" permissions:", " contents: write", " steps:", ` - uses: ${checkout}`);
|
|
1672
|
+
if (runnerImage) {
|
|
1673
|
+
lines.push(" - run: git config --global --add safe.directory \"${GITHUB_WORKSPACE:-$PWD}\"", " - run: codex-toys actions prepare-auth");
|
|
1674
|
+
}
|
|
1675
|
+
else {
|
|
1676
|
+
lines.push(` - uses: ${setupNode}`, " with:", " node-version: 24", " - run: npm install -g vite-plus", " - run: vp dlx codex-toys actions prepare-auth");
|
|
1677
|
+
}
|
|
1678
|
+
lines.push(" env:", " CODEX_AUTH_JSON_B64: ${{ secrets.CODEX_AUTH_JSON_B64 }}", " CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}", " OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}", ` - run: ${runnerImage ? "codex-toys" : "vp dlx codex-toys"} workbench dispatch run-due --mode actions`, " - if: always()", ` run: ${runnerImage ? "codex-toys" : "vp dlx codex-toys"} actions cleanup`, " - if: always()", " run: |", " git add -- .codex/memories .codex/workbench/actions", " if [ -d .codex/feed/actions ]; then", " git add -- .codex/feed/actions", " fi", " if [ -d .codex/sessions ]; then", " git add -A -f -- .codex/sessions", " fi", " if git diff --cached --quiet; then", " upstream=\"$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || true)\"", " if [ -z \"${upstream}\" ] || [ \"$(git rev-list --count \"${upstream}..HEAD\")\" = \"0\" ]; then", " exit 0", " fi", " else", " git config user.name codex-toys-actions", " git config user.email codex-toys-actions@users.noreply.github.com", " git commit -m \"Update Codex workbench state\"", " fi", " git push", "");
|
|
1679
|
+
return lines.join("\n");
|
|
1993
1680
|
}
|
|
1994
1681
|
function actionsGitignoreEntries() {
|
|
1995
1682
|
return [
|
|
@@ -2031,12 +1718,12 @@ function parseTask(input) {
|
|
|
2031
1718
|
const id = requiredTaskId(input.id);
|
|
2032
1719
|
const enabled = input.enabled === undefined ? true : booleanValue(input.enabled, `workbench task ${id} enabled`);
|
|
2033
1720
|
const kind = requiredString(input.kind, `workbench task ${id} kind`);
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
isScheduleDue(schedule, new Date());
|
|
1721
|
+
if (input.schedule !== undefined) {
|
|
1722
|
+
throw new Error(`workbench task ${id} schedule has been removed; run explicit codex-toys commands from systemd or Actions schedules`);
|
|
2037
1723
|
}
|
|
1724
|
+
const history = workbenchTaskHistoryValue(input.history, `workbench task ${id} history`);
|
|
2038
1725
|
if (kind === "skill") {
|
|
2039
|
-
return { id, enabled, kind, skill: requiredString(input.skill, `workbench task ${id} skill`),
|
|
1726
|
+
return { id, enabled, kind, skill: requiredString(input.skill, `workbench task ${id} skill`), history, var: optionalString(input.var) };
|
|
2040
1727
|
}
|
|
2041
1728
|
if (kind === "workflow") {
|
|
2042
1729
|
return {
|
|
@@ -2044,7 +1731,7 @@ function parseTask(input) {
|
|
|
2044
1731
|
enabled,
|
|
2045
1732
|
kind,
|
|
2046
1733
|
workflow: requiredString(input.workflow, `workbench task ${id} workflow`),
|
|
2047
|
-
|
|
1734
|
+
history,
|
|
2048
1735
|
event: isRecord(input.event) ? input.event : undefined,
|
|
2049
1736
|
prompt: optionalString(input.prompt),
|
|
2050
1737
|
cwd: optionalString(input.cwd),
|
|
@@ -2054,28 +1741,10 @@ function parseTask(input) {
|
|
|
2054
1741
|
if (!Array.isArray(input.command) || !input.command.every((item) => typeof item === "string")) {
|
|
2055
1742
|
throw new Error(`workbench task ${id} command must be an array of strings`);
|
|
2056
1743
|
}
|
|
2057
|
-
return { id, enabled, kind, command: input.command,
|
|
1744
|
+
return { id, enabled, kind, command: input.command, history };
|
|
2058
1745
|
}
|
|
2059
1746
|
throw new Error(`Invalid workbench task kind for ${id}: ${kind}`);
|
|
2060
1747
|
}
|
|
2061
|
-
function parseReactiveRule(input) {
|
|
2062
|
-
if (!isRecord(input)) {
|
|
2063
|
-
throw new Error("workbench.reactive entries must be tables");
|
|
2064
|
-
}
|
|
2065
|
-
const id = requiredTaskId(input.id);
|
|
2066
|
-
const kind = requiredString(input.kind, `workbench reactive ${id} kind`);
|
|
2067
|
-
if (kind !== "skill") {
|
|
2068
|
-
throw new Error(`Invalid workbench reactive kind for ${id}: ${kind}`);
|
|
2069
|
-
}
|
|
2070
|
-
return {
|
|
2071
|
-
id,
|
|
2072
|
-
enabled: input.enabled === undefined ? true : booleanValue(input.enabled, `workbench reactive ${id} enabled`),
|
|
2073
|
-
task: requiredString(input.task, `workbench reactive ${id} task`),
|
|
2074
|
-
consecutiveFailuresGte: positiveInteger(input.consecutive_failures_gte, `workbench reactive ${id} consecutive_failures_gte`),
|
|
2075
|
-
kind,
|
|
2076
|
-
skill: requiredString(input.skill, `workbench reactive ${id} skill`),
|
|
2077
|
-
};
|
|
2078
|
-
}
|
|
2079
1748
|
function requiredTaskId(value) {
|
|
2080
1749
|
const id = requiredString(value, "workbench task id");
|
|
2081
1750
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(id)) {
|
|
@@ -2116,6 +1785,15 @@ function booleanValue(value, label) {
|
|
|
2116
1785
|
}
|
|
2117
1786
|
return value;
|
|
2118
1787
|
}
|
|
1788
|
+
function workbenchTaskHistoryValue(value, label) {
|
|
1789
|
+
if (value === undefined) {
|
|
1790
|
+
return "full";
|
|
1791
|
+
}
|
|
1792
|
+
if (value === "full" || value === "latest") {
|
|
1793
|
+
return value;
|
|
1794
|
+
}
|
|
1795
|
+
throw new Error(`${label} must be full or latest`);
|
|
1796
|
+
}
|
|
2119
1797
|
function stringRecord(value) {
|
|
2120
1798
|
if (!isRecord(value)) {
|
|
2121
1799
|
return undefined;
|
|
@@ -2167,7 +1845,7 @@ function reasoningEffortValue(value, label) {
|
|
|
2167
1845
|
}
|
|
2168
1846
|
return undefined;
|
|
2169
1847
|
}
|
|
2170
|
-
function
|
|
1848
|
+
function dispatchDependencyStatusValue(value, label) {
|
|
2171
1849
|
if (value === "completed" ||
|
|
2172
1850
|
value === "failed" ||
|
|
2173
1851
|
value === "canceled" ||
|
|
@@ -2179,7 +1857,7 @@ function deferredDependencyStatusValue(value, label) {
|
|
|
2179
1857
|
}
|
|
2180
1858
|
return undefined;
|
|
2181
1859
|
}
|
|
2182
|
-
function
|
|
1860
|
+
function dispatchRunStatus(value) {
|
|
2183
1861
|
if (value === "pending" ||
|
|
2184
1862
|
value === "running" ||
|
|
2185
1863
|
value === "completed" ||
|
|
@@ -2187,13 +1865,13 @@ function deferredRunStatus(value) {
|
|
|
2187
1865
|
value === "canceled") {
|
|
2188
1866
|
return value;
|
|
2189
1867
|
}
|
|
2190
|
-
throw new Error(`Invalid
|
|
1868
|
+
throw new Error(`Invalid dispatch run status: ${String(value)}`);
|
|
2191
1869
|
}
|
|
2192
|
-
function
|
|
1870
|
+
function dispatchAttemptStatus(value) {
|
|
2193
1871
|
if (value === "running" || value === "completed" || value === "failed") {
|
|
2194
1872
|
return value;
|
|
2195
1873
|
}
|
|
2196
|
-
throw new Error(`Invalid
|
|
1874
|
+
throw new Error(`Invalid dispatch run attempt status: ${String(value)}`);
|
|
2197
1875
|
}
|
|
2198
1876
|
function workbenchMode(value) {
|
|
2199
1877
|
if (value === "local" || value === "actions") {
|
|
@@ -2201,12 +1879,6 @@ function workbenchMode(value) {
|
|
|
2201
1879
|
}
|
|
2202
1880
|
throw new Error(`Invalid workbench mode: ${String(value)}`);
|
|
2203
1881
|
}
|
|
2204
|
-
function positiveInteger(value, label) {
|
|
2205
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
2206
|
-
throw new Error(`${label} must be a positive integer`);
|
|
2207
|
-
}
|
|
2208
|
-
return value;
|
|
2209
|
-
}
|
|
2210
1882
|
function isRecord(value) {
|
|
2211
1883
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2212
1884
|
}
|