codex-toys 0.135.0 → 0.136.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 +3 -0
- package/dist/app-server/generated/AutoCompactTokenLimitScope.d.ts +6 -0
- package/dist/app-server/generated/AutoCompactTokenLimitScope.d.ts.map +1 -0
- package/dist/app-server/generated/AutoCompactTokenLimitScope.js +3 -0
- package/dist/app-server/generated/AutoCompactTokenLimitScope.js.map +1 -0
- package/dist/app-server/generated/ClientRequest.d.ts +15 -0
- package/dist/app-server/generated/ClientRequest.d.ts.map +1 -1
- package/dist/app-server/generated/FunctionCallOutputContentItem.d.ts +3 -0
- package/dist/app-server/generated/FunctionCallOutputContentItem.d.ts.map +1 -1
- package/dist/app-server/generated/ImageDetail.d.ts +1 -1
- package/dist/app-server/generated/ImageDetail.d.ts.map +1 -1
- package/dist/app-server/generated/ServerNotification.d.ts +4 -0
- package/dist/app-server/generated/ServerNotification.d.ts.map +1 -1
- package/dist/app-server/generated/index.d.ts +1 -0
- package/dist/app-server/generated/index.d.ts.map +1 -1
- package/dist/app-server/generated/index.js.map +1 -1
- package/dist/app-server/generated/v2/ActivePermissionProfile.d.ts +2 -2
- package/dist/app-server/generated/v2/AdditionalContextEntry.d.ts +6 -0
- package/dist/app-server/generated/v2/AdditionalContextEntry.d.ts.map +1 -0
- package/dist/app-server/generated/v2/AdditionalContextEntry.js +3 -0
- package/dist/app-server/generated/v2/AdditionalContextEntry.js.map +1 -0
- package/dist/app-server/generated/v2/AdditionalContextKind.d.ts +2 -0
- package/dist/app-server/generated/v2/AdditionalContextKind.d.ts.map +1 -0
- package/dist/app-server/generated/v2/AdditionalContextKind.js +3 -0
- package/dist/app-server/generated/v2/AdditionalContextKind.js.map +1 -0
- package/dist/app-server/generated/v2/CommandExecParams.d.ts +2 -3
- package/dist/app-server/generated/v2/CommandExecParams.d.ts.map +1 -1
- package/dist/app-server/generated/v2/ComputerUseRequirements.d.ts +4 -0
- package/dist/app-server/generated/v2/ComputerUseRequirements.d.ts.map +1 -0
- package/dist/app-server/generated/v2/ComputerUseRequirements.js +3 -0
- package/dist/app-server/generated/v2/ComputerUseRequirements.js.map +1 -0
- package/dist/app-server/generated/v2/Config.d.ts +2 -5
- package/dist/app-server/generated/v2/Config.d.ts.map +1 -1
- package/dist/app-server/generated/v2/ConfigReadParams.d.ts +1 -1
- package/dist/app-server/generated/v2/ConfigReadParams.d.ts.map +1 -1
- package/dist/app-server/generated/v2/ConfigRequirements.d.ts +4 -0
- package/dist/app-server/generated/v2/ConfigRequirements.d.ts.map +1 -1
- package/dist/app-server/generated/v2/FeedbackUploadParams.d.ts +1 -1
- package/dist/app-server/generated/v2/FeedbackUploadParams.d.ts.map +1 -1
- package/dist/app-server/generated/v2/FileSystemAccessMode.d.ts +1 -1
- package/dist/app-server/generated/v2/GetAccountParams.d.ts +1 -1
- package/dist/app-server/generated/v2/GetAccountParams.d.ts.map +1 -1
- package/dist/app-server/generated/v2/HookEventName.d.ts +1 -1
- package/dist/app-server/generated/v2/HookEventName.d.ts.map +1 -1
- package/dist/app-server/generated/v2/ListMcpServerStatusParams.d.ts +1 -0
- package/dist/app-server/generated/v2/ListMcpServerStatusParams.d.ts.map +1 -1
- package/dist/app-server/generated/v2/ManagedHooksRequirements.d.ts +2 -0
- package/dist/app-server/generated/v2/ManagedHooksRequirements.d.ts.map +1 -1
- package/dist/app-server/generated/v2/Model.d.ts +4 -0
- package/dist/app-server/generated/v2/Model.d.ts.map +1 -1
- package/dist/app-server/generated/v2/PermissionProfileListParams.d.ts +15 -0
- package/dist/app-server/generated/v2/PermissionProfileListParams.d.ts.map +1 -0
- package/dist/app-server/generated/v2/PermissionProfileListParams.js +3 -0
- package/dist/app-server/generated/v2/PermissionProfileListParams.js.map +1 -0
- package/dist/app-server/generated/v2/PermissionProfileListResponse.d.ts +10 -0
- package/dist/app-server/generated/v2/PermissionProfileListResponse.d.ts.map +1 -0
- package/dist/app-server/generated/v2/PermissionProfileListResponse.js +3 -0
- package/dist/app-server/generated/v2/PermissionProfileListResponse.js.map +1 -0
- package/dist/app-server/generated/v2/PermissionProfileSummary.d.ts +11 -0
- package/dist/app-server/generated/v2/PermissionProfileSummary.d.ts.map +1 -0
- package/dist/app-server/generated/v2/PermissionProfileSummary.js +3 -0
- package/dist/app-server/generated/v2/PermissionProfileSummary.js.map +1 -0
- package/dist/app-server/generated/v2/PluginListMarketplaceKind.d.ts +1 -1
- package/dist/app-server/generated/v2/PluginListMarketplaceKind.d.ts.map +1 -1
- package/dist/app-server/generated/v2/ThreadForkParams.d.ts +3 -2
- package/dist/app-server/generated/v2/ThreadForkParams.d.ts.map +1 -1
- package/dist/app-server/generated/v2/ThreadItem.d.ts +1 -0
- package/dist/app-server/generated/v2/ThreadItem.d.ts.map +1 -1
- package/dist/app-server/generated/v2/ThreadReadParams.d.ts +1 -1
- package/dist/app-server/generated/v2/ThreadReadParams.d.ts.map +1 -1
- package/dist/app-server/generated/v2/ThreadResumeParams.d.ts +11 -4
- package/dist/app-server/generated/v2/ThreadResumeParams.d.ts.map +1 -1
- package/dist/app-server/generated/v2/ThreadSearchParams.d.ts +36 -0
- package/dist/app-server/generated/v2/ThreadSearchParams.d.ts.map +1 -0
- package/dist/app-server/generated/v2/ThreadSearchParams.js +3 -0
- package/dist/app-server/generated/v2/ThreadSearchParams.js.map +1 -0
- package/dist/app-server/generated/v2/ThreadSearchResponse.d.ts +17 -0
- package/dist/app-server/generated/v2/ThreadSearchResponse.d.ts.map +1 -0
- package/dist/app-server/generated/v2/ThreadSearchResponse.js +3 -0
- package/dist/app-server/generated/v2/ThreadSearchResponse.js.map +1 -0
- package/dist/app-server/generated/v2/ThreadSearchResult.d.ts +6 -0
- package/dist/app-server/generated/v2/ThreadSearchResult.d.ts.map +1 -0
- package/dist/app-server/generated/v2/ThreadSearchResult.js +3 -0
- package/dist/app-server/generated/v2/ThreadSearchResult.js.map +1 -0
- package/dist/app-server/generated/v2/ThreadSettings.d.ts +24 -0
- package/dist/app-server/generated/v2/ThreadSettings.d.ts.map +1 -0
- package/dist/app-server/generated/v2/ThreadSettings.js +3 -0
- package/dist/app-server/generated/v2/ThreadSettings.js.map +1 -0
- package/dist/app-server/generated/v2/ThreadSettingsUpdateParams.d.ts +60 -0
- package/dist/app-server/generated/v2/ThreadSettingsUpdateParams.d.ts.map +1 -0
- package/dist/app-server/generated/v2/ThreadSettingsUpdateParams.js +3 -0
- package/dist/app-server/generated/v2/ThreadSettingsUpdateParams.js.map +1 -0
- package/dist/app-server/generated/v2/ThreadSettingsUpdateResponse.d.ts +2 -0
- package/dist/app-server/generated/v2/ThreadSettingsUpdateResponse.d.ts.map +1 -0
- package/dist/app-server/generated/v2/ThreadSettingsUpdateResponse.js +3 -0
- package/dist/app-server/generated/v2/ThreadSettingsUpdateResponse.js.map +1 -0
- package/dist/app-server/generated/v2/ThreadSettingsUpdatedNotification.d.ts +6 -0
- package/dist/app-server/generated/v2/ThreadSettingsUpdatedNotification.d.ts.map +1 -0
- package/dist/app-server/generated/v2/ThreadSettingsUpdatedNotification.js +3 -0
- package/dist/app-server/generated/v2/ThreadSettingsUpdatedNotification.js.map +1 -0
- package/dist/app-server/generated/v2/ThreadStartParams.d.ts +2 -2
- package/dist/app-server/generated/v2/ThreadStartParams.d.ts.map +1 -1
- package/dist/app-server/generated/v2/TurnStartParams.d.ts +7 -0
- package/dist/app-server/generated/v2/TurnStartParams.d.ts.map +1 -1
- package/dist/app-server/generated/v2/TurnSteerParams.d.ts +7 -0
- package/dist/app-server/generated/v2/TurnSteerParams.d.ts.map +1 -1
- package/dist/app-server/generated/v2/index.d.ts +13 -0
- package/dist/app-server/generated/v2/index.d.ts.map +1 -1
- package/dist/bin/codex-toys-proxy.js +17 -17
- package/dist/cli/args.d.ts +50 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +97 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/index.js +134 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/toybox.d.ts.map +1 -1
- package/dist/cli/toybox.js +7 -1
- package/dist/cli/toybox.js.map +1 -1
- package/dist/cli/workspace-autonomy.d.ts +119 -0
- package/dist/cli/workspace-autonomy.d.ts.map +1 -1
- package/dist/cli/workspace-autonomy.js +676 -6
- package/dist/cli/workspace-autonomy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/toybox/deferred-run-methods.d.ts +17 -0
- package/dist/toybox/deferred-run-methods.d.ts.map +1 -0
- package/dist/toybox/deferred-run-methods.js +150 -0
- package/dist/toybox/deferred-run-methods.js.map +1 -0
- package/dist/toybox/index.d.ts +1 -0
- package/dist/toybox/index.d.ts.map +1 -1
- package/dist/toybox/index.js +1 -0
- package/dist/toybox/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, open, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { parse as parseToml } from "smol-toml";
|
|
7
|
-
import { createTurnAutomationHost, resolveTurnAutomationTarget, runTurnAutomationScript, } from "./turn-automation.js";
|
|
7
|
+
import { createTurnAutomationHost, resolveTurnAutomationTarget, runTurnAutomationScript, startAutomationTurnWithRequest, waitAutomationTurnWithRequest, } from "./turn-automation.js";
|
|
8
8
|
import { parseJsonText } from "./json.js";
|
|
9
9
|
export async function discoverWorkspaceRoot(start = process.cwd()) {
|
|
10
10
|
let current = path.resolve(start);
|
|
@@ -120,6 +120,10 @@ export async function collectWorkspaceDoctorInfo(context) {
|
|
|
120
120
|
}
|
|
121
121
|
const runs = await readRuns(context);
|
|
122
122
|
const latestRun = runs.sort((a, b) => b.startedAt.localeCompare(a.startedAt))[0];
|
|
123
|
+
const deferredRuns = await listDeferredRunIntents(context);
|
|
124
|
+
const now = new Date();
|
|
125
|
+
const latestDeferredRun = deferredRuns
|
|
126
|
+
.toSorted((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
|
|
123
127
|
const failingCount = countFailingTasks(config?.tasks ?? [], runs);
|
|
124
128
|
return {
|
|
125
129
|
mode: context.mode,
|
|
@@ -139,7 +143,12 @@ export async function collectWorkspaceDoctorInfo(context) {
|
|
|
139
143
|
taskCount: config?.tasks.length ?? 0,
|
|
140
144
|
dueCount: dueTasks(config?.tasks ?? [], runs, new Date()).length,
|
|
141
145
|
failingCount,
|
|
146
|
+
deferredCount: deferredRuns.length,
|
|
147
|
+
deferredDueCount: deferredRuns.filter((intent) => isDeferredIntentDue(intent, now)).length,
|
|
148
|
+
deferredRunningCount: deferredRuns.filter((intent) => intent.status === "running").length,
|
|
149
|
+
deferredFailedCount: deferredRuns.filter((intent) => intent.status === "failed").length,
|
|
142
150
|
latestRun,
|
|
151
|
+
latestDeferredRun,
|
|
143
152
|
surfaces: config?.surfaces ?? [],
|
|
144
153
|
errors: workspaceDoctorErrors(context),
|
|
145
154
|
};
|
|
@@ -158,6 +167,16 @@ export function formatWorkspaceDoctorInfo(info) {
|
|
|
158
167
|
["workspace memories", `${info.workspaceMemoryRoot}${info.workspaceMemorySummaryExists ? " (summary)" : ""}`],
|
|
159
168
|
["tasks", `${info.taskCount} configured, ${info.dueCount} due, ${info.failingCount} failing`],
|
|
160
169
|
["latest run", info.latestRun ? `${info.latestRun.status} ${info.latestRun.taskId} ${info.latestRun.finishedAt}` : "none"],
|
|
170
|
+
[
|
|
171
|
+
"deferred runs",
|
|
172
|
+
`${info.deferredCount} total, ${info.deferredDueCount} due, ${info.deferredRunningCount} running, ${info.deferredFailedCount} failed`,
|
|
173
|
+
],
|
|
174
|
+
[
|
|
175
|
+
"latest deferred",
|
|
176
|
+
info.latestDeferredRun
|
|
177
|
+
? `${info.latestDeferredRun.status} ${info.latestDeferredRun.id} ${info.latestDeferredRun.updatedAt}`
|
|
178
|
+
: "none",
|
|
179
|
+
],
|
|
161
180
|
];
|
|
162
181
|
for (const error of info.errors) {
|
|
163
182
|
rows.push(["error", error]);
|
|
@@ -185,10 +204,19 @@ export async function tickWorkspace(context, options) {
|
|
|
185
204
|
await ensureStateDirs(context);
|
|
186
205
|
const config = await loadWorkspaceConfig(context);
|
|
187
206
|
const previousRuns = await readRuns(context);
|
|
188
|
-
const
|
|
207
|
+
const previousIntents = await listDeferredRunIntents(context);
|
|
208
|
+
const now = new Date();
|
|
209
|
+
const due = dueTasks(config.tasks, previousRuns, now, previousIntents);
|
|
189
210
|
const runs = [];
|
|
190
211
|
for (const task of due) {
|
|
191
|
-
|
|
212
|
+
await createScheduledWorkspaceTaskIntent(context, task, now);
|
|
213
|
+
}
|
|
214
|
+
const executions = await runDueDeferredRuns(context, options);
|
|
215
|
+
for (const execution of executions.executions) {
|
|
216
|
+
const workspaceRun = record(execution.output).workspaceRun;
|
|
217
|
+
if (isWorkspaceRunRecord(workspaceRun)) {
|
|
218
|
+
runs.push(workspaceRun);
|
|
219
|
+
}
|
|
192
220
|
}
|
|
193
221
|
const allRuns = [...previousRuns, ...runs];
|
|
194
222
|
for (const rule of config.reactive.filter((item) => item.enabled)) {
|
|
@@ -233,6 +261,168 @@ export async function commitActionsWorkspaceState(context, options = {}) {
|
|
|
233
261
|
output: commit.stdout || commit.stderr,
|
|
234
262
|
};
|
|
235
263
|
}
|
|
264
|
+
export async function createDeferredRunIntent(context, params) {
|
|
265
|
+
await ensureDeferredRunDirs(context);
|
|
266
|
+
const input = parseDeferredRunCreateParams(params);
|
|
267
|
+
const now = new Date().toISOString();
|
|
268
|
+
const runAt = input.runAt ?? now;
|
|
269
|
+
const intent = compactUndefined({
|
|
270
|
+
id: input.id ?? deferredRunId(now),
|
|
271
|
+
status: "pending",
|
|
272
|
+
mode: context.mode,
|
|
273
|
+
runAt,
|
|
274
|
+
target: input.target,
|
|
275
|
+
createdAt: now,
|
|
276
|
+
updatedAt: now,
|
|
277
|
+
createdBy: input.createdBy,
|
|
278
|
+
reason: input.reason,
|
|
279
|
+
source: input.source,
|
|
280
|
+
attemptIds: [],
|
|
281
|
+
});
|
|
282
|
+
await writeNewJsonFile(deferredIntentPath(context, intent.id), intent);
|
|
283
|
+
return intent;
|
|
284
|
+
}
|
|
285
|
+
export async function listDeferredRunIntents(context, options = {}) {
|
|
286
|
+
const dir = deferredIntentDir(context);
|
|
287
|
+
try {
|
|
288
|
+
const entries = await readdir(dir);
|
|
289
|
+
const intents = [];
|
|
290
|
+
for (const entry of entries) {
|
|
291
|
+
if (!entry.endsWith(".json")) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const intent = normalizeDeferredRunIntent(parseJsonText(await readFile(path.join(dir, entry), "utf8"), path.join(dir, entry)));
|
|
296
|
+
if (!options.status || intent.status === options.status) {
|
|
297
|
+
intents.push(intent);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch { }
|
|
301
|
+
}
|
|
302
|
+
const sorted = intents.sort((left, right) => left.runAt.localeCompare(right.runAt) || left.createdAt.localeCompare(right.createdAt));
|
|
303
|
+
return sorted.slice(0, clampLimit(options.limit, 500));
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
export async function readDeferredRun(context, intentId) {
|
|
310
|
+
const intent = await readDeferredRunIntent(context, intentId);
|
|
311
|
+
const attempts = await readDeferredRunAttempts(context, intent.attemptIds);
|
|
312
|
+
return { intent, attempts };
|
|
313
|
+
}
|
|
314
|
+
export async function cancelDeferredRunIntent(context, intentId) {
|
|
315
|
+
const intent = await readDeferredRunIntent(context, intentId);
|
|
316
|
+
if (intent.status !== "pending") {
|
|
317
|
+
throw new Error(`Only pending deferred runs can be canceled: ${intentId}`);
|
|
318
|
+
}
|
|
319
|
+
const now = new Date().toISOString();
|
|
320
|
+
const canceled = {
|
|
321
|
+
...intent,
|
|
322
|
+
status: "canceled",
|
|
323
|
+
updatedAt: now,
|
|
324
|
+
canceledAt: now,
|
|
325
|
+
};
|
|
326
|
+
await writeJsonFileAtomic(deferredIntentPath(context, intentId), canceled);
|
|
327
|
+
return canceled;
|
|
328
|
+
}
|
|
329
|
+
export async function runDueDeferredRuns(context, options) {
|
|
330
|
+
await ensureStateDirs(context);
|
|
331
|
+
await ensureDeferredRunDirs(context);
|
|
332
|
+
const now = options.now ?? new Date();
|
|
333
|
+
const due = (await listDeferredRunIntents(context))
|
|
334
|
+
.filter((intent) => isDeferredIntentDue(intent, now))
|
|
335
|
+
.slice(0, clampLimit(options.limit, 100));
|
|
336
|
+
const executions = [];
|
|
337
|
+
for (const intent of due) {
|
|
338
|
+
const claim = await claimDeferredRunIntent(context, intent, {
|
|
339
|
+
now,
|
|
340
|
+
leaseMs: options.leaseMs ?? 30 * 60 * 1000,
|
|
341
|
+
});
|
|
342
|
+
if (!claim) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const outputPath = path.join(deferredOutputDir(context), `${claim.attempt.id}.json`);
|
|
347
|
+
await writeJsonFileAtomic(deferredAttemptPath(context, claim.attempt.id), claim.attempt);
|
|
348
|
+
const result = await executeDeferredRunTarget(context, claim.intent, {
|
|
349
|
+
callToybox: options.callToybox,
|
|
350
|
+
automationCwd: options.automationCwd,
|
|
351
|
+
});
|
|
352
|
+
await writeJsonFileAtomic(outputPath, result.output);
|
|
353
|
+
const finishedAt = new Date().toISOString();
|
|
354
|
+
const attempt = compactUndefined({
|
|
355
|
+
...claim.attempt,
|
|
356
|
+
status: result.status,
|
|
357
|
+
finishedAt,
|
|
358
|
+
outputPath,
|
|
359
|
+
error: result.error,
|
|
360
|
+
});
|
|
361
|
+
const completedIntent = compactUndefined({
|
|
362
|
+
...claim.intent,
|
|
363
|
+
status: result.status,
|
|
364
|
+
updatedAt: finishedAt,
|
|
365
|
+
attemptIds: [...new Set([...claim.intent.attemptIds, attempt.id])],
|
|
366
|
+
lease: undefined,
|
|
367
|
+
completedAt: result.status === "completed" ? finishedAt : undefined,
|
|
368
|
+
error: result.error,
|
|
369
|
+
});
|
|
370
|
+
await writeJsonFileAtomic(deferredAttemptPath(context, attempt.id), attempt);
|
|
371
|
+
await writeJsonFileAtomic(deferredIntentPath(context, completedIntent.id), completedIntent);
|
|
372
|
+
executions.push({
|
|
373
|
+
intent: completedIntent,
|
|
374
|
+
attempt,
|
|
375
|
+
output: result.output,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
await releaseDeferredRunClaim(context, claim.intent.id);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return { mode: context.mode, executions };
|
|
383
|
+
}
|
|
384
|
+
export async function pruneDeferredRunHistory(context, options) {
|
|
385
|
+
if (!Number.isInteger(options.olderThanDays) || options.olderThanDays <= 0) {
|
|
386
|
+
throw new Error("olderThanDays must be a positive integer");
|
|
387
|
+
}
|
|
388
|
+
const now = options.now ?? new Date();
|
|
389
|
+
const cutoff = new Date(now.getTime() - options.olderThanDays * 24 * 60 * 60 * 1000).toISOString();
|
|
390
|
+
const intents = await listDeferredRunIntents(context);
|
|
391
|
+
const pruned = [];
|
|
392
|
+
for (const intent of intents) {
|
|
393
|
+
if (!isTerminalDeferredRunStatus(intent.status) || intent.updatedAt >= cutoff) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const attempts = await readDeferredRunAttempts(context, intent.attemptIds);
|
|
397
|
+
const outputPaths = attempts.flatMap((attempt) => attempt.outputPath ? [attempt.outputPath] : []);
|
|
398
|
+
pruned.push({
|
|
399
|
+
id: intent.id,
|
|
400
|
+
status: intent.status,
|
|
401
|
+
updatedAt: intent.updatedAt,
|
|
402
|
+
attemptIds: attempts.map((attempt) => attempt.id),
|
|
403
|
+
outputPaths,
|
|
404
|
+
});
|
|
405
|
+
if (options.dryRun === true) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
await releaseDeferredRunClaim(context, intent.id);
|
|
409
|
+
for (const outputPath of outputPaths) {
|
|
410
|
+
await rm(outputPath, { force: true });
|
|
411
|
+
}
|
|
412
|
+
for (const attempt of attempts) {
|
|
413
|
+
await rm(deferredAttemptPath(context, attempt.id), { force: true });
|
|
414
|
+
}
|
|
415
|
+
await rm(deferredIntentPath(context, intent.id), { force: true });
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
mode: context.mode,
|
|
419
|
+
cutoff,
|
|
420
|
+
dryRun: options.dryRun === true,
|
|
421
|
+
inspected: intents.length,
|
|
422
|
+
pruned: pruned.length,
|
|
423
|
+
intents: pruned,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
236
426
|
async function runWorkspaceTask(context, config, task, options) {
|
|
237
427
|
const startedAt = new Date().toISOString();
|
|
238
428
|
const runId = workspaceRunId(task.id, startedAt);
|
|
@@ -297,6 +487,89 @@ async function runAutomationTask(context, config, task, runId, startedAt, option
|
|
|
297
487
|
});
|
|
298
488
|
return scriptRun.result;
|
|
299
489
|
}
|
|
490
|
+
async function executeDeferredRunTarget(context, intent, options) {
|
|
491
|
+
try {
|
|
492
|
+
const target = intent.target;
|
|
493
|
+
if (target.kind === "workspace-task") {
|
|
494
|
+
const config = await loadWorkspaceConfig(context);
|
|
495
|
+
const task = config.tasks.find((item) => item.id === target.taskId);
|
|
496
|
+
if (!task) {
|
|
497
|
+
throw new Error(`Unknown workspace task: ${target.taskId}`);
|
|
498
|
+
}
|
|
499
|
+
const workspaceRun = await runWorkspaceTask(context, config, task, options);
|
|
500
|
+
return {
|
|
501
|
+
status: workspaceRun.status === "failed" ? "failed" : "completed",
|
|
502
|
+
output: { workspaceRun },
|
|
503
|
+
error: workspaceRun.error,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (target.kind === "automation") {
|
|
507
|
+
const config = await loadWorkspaceConfig(context).catch(() => ({
|
|
508
|
+
name: path.basename(context.repoRoot),
|
|
509
|
+
surfaces: [],
|
|
510
|
+
tasks: [],
|
|
511
|
+
reactive: [],
|
|
512
|
+
path: context.configPath,
|
|
513
|
+
}));
|
|
514
|
+
const result = await runAutomationDeferredTarget(context, config, { ...intent, target }, options);
|
|
515
|
+
return { status: "completed", output: result };
|
|
516
|
+
}
|
|
517
|
+
if (target.kind === "turn") {
|
|
518
|
+
const started = await startAutomationTurnWithRequest("workspace", target, async (method, params) => await options.callToybox("app.call", {
|
|
519
|
+
method,
|
|
520
|
+
params,
|
|
521
|
+
}));
|
|
522
|
+
const snapshot = await waitAutomationTurnWithRequest("workspace", async (method, params) => await options.callToybox("app.call", {
|
|
523
|
+
method,
|
|
524
|
+
params,
|
|
525
|
+
}), started);
|
|
526
|
+
return { status: "completed", output: { turn: snapshot } };
|
|
527
|
+
}
|
|
528
|
+
return exhaustiveTarget(target);
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
return {
|
|
532
|
+
status: "failed",
|
|
533
|
+
output: { error: errorMessage(error) },
|
|
534
|
+
error: errorMessage(error),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
async function runAutomationDeferredTarget(context, config, intent, options) {
|
|
539
|
+
const target = await resolveTurnAutomationTarget(intent.target.automation, {
|
|
540
|
+
cwd: context.repoRoot,
|
|
541
|
+
});
|
|
542
|
+
const startedAt = new Date().toISOString();
|
|
543
|
+
const event = deferredAutomationEvent(config, intent, startedAt);
|
|
544
|
+
const prompt = intent.target.prompt ?? target.prompt;
|
|
545
|
+
const cwd = intent.target.cwd ?? options.automationCwd ?? target.cwd ?? context.repoRoot;
|
|
546
|
+
const scriptRun = await runTurnAutomationScript({
|
|
547
|
+
scriptPath: target.scriptPath,
|
|
548
|
+
automation: target.automation,
|
|
549
|
+
event,
|
|
550
|
+
prompt,
|
|
551
|
+
cwd,
|
|
552
|
+
timeoutMs: 90_000,
|
|
553
|
+
host: createTurnAutomationHost({
|
|
554
|
+
via: "workspace",
|
|
555
|
+
appRequest: async (method, params) => await options.callToybox("app.call", {
|
|
556
|
+
method,
|
|
557
|
+
params,
|
|
558
|
+
}),
|
|
559
|
+
workspaceRequest: options.callToybox,
|
|
560
|
+
defaults: {
|
|
561
|
+
prompt,
|
|
562
|
+
cwd,
|
|
563
|
+
skills: target.skills,
|
|
564
|
+
model: intent.target.model,
|
|
565
|
+
sandbox: intent.target.sandbox,
|
|
566
|
+
approvalPolicy: intent.target.approvalPolicy,
|
|
567
|
+
permissions: intent.target.permissions,
|
|
568
|
+
},
|
|
569
|
+
}),
|
|
570
|
+
});
|
|
571
|
+
return scriptRun.result;
|
|
572
|
+
}
|
|
300
573
|
function workspaceAutomationEvent(config, task, runId, startedAt) {
|
|
301
574
|
const event = task.event ?? {};
|
|
302
575
|
const payload = isRecord(event.payload) ? event.payload : {};
|
|
@@ -313,6 +586,25 @@ function workspaceAutomationEvent(config, task, runId, startedAt) {
|
|
|
313
586
|
},
|
|
314
587
|
};
|
|
315
588
|
}
|
|
589
|
+
function deferredAutomationEvent(config, intent, startedAt) {
|
|
590
|
+
const event = intent.target.event ?? {};
|
|
591
|
+
const payload = isRecord(event.payload) ? event.payload : {};
|
|
592
|
+
return {
|
|
593
|
+
...event,
|
|
594
|
+
id: stringValue(event.id, `deferred:${config.name}:${intent.id}`),
|
|
595
|
+
type: stringValue(event.type, intent.target.automation),
|
|
596
|
+
source: stringValue(event.source, config.name),
|
|
597
|
+
occurredAt: stringValue(event.occurredAt, intent.runAt),
|
|
598
|
+
receivedAt: stringValue(event.receivedAt, startedAt),
|
|
599
|
+
payload: {
|
|
600
|
+
deferredRunId: intent.id,
|
|
601
|
+
...payload,
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function exhaustiveTarget(value) {
|
|
606
|
+
throw new Error(`Unsupported deferred run target: ${JSON.stringify(value)}`);
|
|
607
|
+
}
|
|
316
608
|
async function runReactiveRule(context, rule) {
|
|
317
609
|
const startedAt = new Date().toISOString();
|
|
318
610
|
const runId = `${startedAt.replace(/[:.]/g, "-")}-${rule.id}`;
|
|
@@ -454,7 +746,227 @@ async function readRuns(context) {
|
|
|
454
746
|
return [];
|
|
455
747
|
}
|
|
456
748
|
}
|
|
457
|
-
function
|
|
749
|
+
async function createScheduledWorkspaceTaskIntent(context, task, now) {
|
|
750
|
+
try {
|
|
751
|
+
return await createDeferredRunIntent(context, {
|
|
752
|
+
id: scheduledDeferredRunId(task.id, now),
|
|
753
|
+
runAt: now.toISOString(),
|
|
754
|
+
target: {
|
|
755
|
+
kind: "workspace-task",
|
|
756
|
+
taskId: task.id,
|
|
757
|
+
},
|
|
758
|
+
createdBy: "workspace-schedule",
|
|
759
|
+
reason: `Scheduled workspace task ${task.id}`,
|
|
760
|
+
source: {
|
|
761
|
+
kind: "workspace-task-schedule",
|
|
762
|
+
taskId: task.id,
|
|
763
|
+
schedule: task.schedule,
|
|
764
|
+
date: now.toISOString().slice(0, 10),
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
if (isAlreadyExistsError(error)) {
|
|
770
|
+
return undefined;
|
|
771
|
+
}
|
|
772
|
+
throw error;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async function readDeferredRunIntent(context, intentId) {
|
|
776
|
+
const intentPath = deferredIntentPath(context, intentId);
|
|
777
|
+
try {
|
|
778
|
+
return normalizeDeferredRunIntent(parseJsonText(await readFile(intentPath, "utf8"), intentPath));
|
|
779
|
+
}
|
|
780
|
+
catch (error) {
|
|
781
|
+
if (isNotFoundError(error)) {
|
|
782
|
+
throw new Error(`Unknown deferred run: ${intentId}`);
|
|
783
|
+
}
|
|
784
|
+
throw error;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
async function readDeferredRunAttempts(context, attemptIds) {
|
|
788
|
+
const attempts = [];
|
|
789
|
+
for (const attemptId of attemptIds) {
|
|
790
|
+
const attemptPath = deferredAttemptPath(context, attemptId);
|
|
791
|
+
try {
|
|
792
|
+
attempts.push(normalizeDeferredRunAttempt(parseJsonText(await readFile(attemptPath, "utf8"), attemptPath)));
|
|
793
|
+
}
|
|
794
|
+
catch { }
|
|
795
|
+
}
|
|
796
|
+
return attempts.sort((left, right) => left.startedAt.localeCompare(right.startedAt));
|
|
797
|
+
}
|
|
798
|
+
async function claimDeferredRunIntent(context, intent, options) {
|
|
799
|
+
const current = await readDeferredRunIntent(context, intent.id);
|
|
800
|
+
if (!isDeferredIntentDue(current, options.now)) {
|
|
801
|
+
return undefined;
|
|
802
|
+
}
|
|
803
|
+
const claimPath = deferredClaimPath(context, current.id);
|
|
804
|
+
const claimedAt = options.now.toISOString();
|
|
805
|
+
const attemptId = deferredAttemptId(current.id, claimedAt);
|
|
806
|
+
const leaseExpiresAt = new Date(options.now.getTime() + options.leaseMs).toISOString();
|
|
807
|
+
const executorId = `${process.pid}:${randomUUID()}`;
|
|
808
|
+
const claim = { intentId: current.id, attemptId, claimedAt, leaseExpiresAt, executorId };
|
|
809
|
+
try {
|
|
810
|
+
await writeNewJsonFile(claimPath, claim);
|
|
811
|
+
}
|
|
812
|
+
catch (error) {
|
|
813
|
+
if (!isAlreadyExistsError(error)) {
|
|
814
|
+
throw error;
|
|
815
|
+
}
|
|
816
|
+
const existing = await readClaimFile(claimPath);
|
|
817
|
+
if (!existing || existing.leaseExpiresAt > options.now.toISOString()) {
|
|
818
|
+
return undefined;
|
|
819
|
+
}
|
|
820
|
+
await rm(claimPath, { force: true });
|
|
821
|
+
try {
|
|
822
|
+
await writeNewJsonFile(claimPath, claim);
|
|
823
|
+
}
|
|
824
|
+
catch (retryError) {
|
|
825
|
+
if (isAlreadyExistsError(retryError)) {
|
|
826
|
+
return undefined;
|
|
827
|
+
}
|
|
828
|
+
throw retryError;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const attempt = {
|
|
832
|
+
id: attemptId,
|
|
833
|
+
intentId: current.id,
|
|
834
|
+
status: "running",
|
|
835
|
+
mode: context.mode,
|
|
836
|
+
startedAt: claimedAt,
|
|
837
|
+
executorId,
|
|
838
|
+
leaseExpiresAt,
|
|
839
|
+
};
|
|
840
|
+
const running = {
|
|
841
|
+
...current,
|
|
842
|
+
status: "running",
|
|
843
|
+
updatedAt: claimedAt,
|
|
844
|
+
lease: {
|
|
845
|
+
attemptId,
|
|
846
|
+
claimedAt,
|
|
847
|
+
expiresAt: leaseExpiresAt,
|
|
848
|
+
executorId,
|
|
849
|
+
},
|
|
850
|
+
};
|
|
851
|
+
await writeJsonFileAtomic(deferredIntentPath(context, current.id), running);
|
|
852
|
+
return { intent: running, attempt };
|
|
853
|
+
}
|
|
854
|
+
async function releaseDeferredRunClaim(context, intentId) {
|
|
855
|
+
await rm(deferredClaimPath(context, intentId), { force: true });
|
|
856
|
+
}
|
|
857
|
+
async function readClaimFile(file) {
|
|
858
|
+
try {
|
|
859
|
+
const parsed = parseJsonText(await readFile(file, "utf8"), file);
|
|
860
|
+
return isRecord(parsed) && typeof parsed.leaseExpiresAt === "string"
|
|
861
|
+
? { leaseExpiresAt: parsed.leaseExpiresAt }
|
|
862
|
+
: undefined;
|
|
863
|
+
}
|
|
864
|
+
catch {
|
|
865
|
+
return undefined;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function parseDeferredRunCreateParams(value) {
|
|
869
|
+
const input = record(value);
|
|
870
|
+
const runAt = optionalString(input.runAt);
|
|
871
|
+
if (runAt && Number.isNaN(Date.parse(runAt))) {
|
|
872
|
+
throw new Error(`Deferred run runAt must be an ISO-compatible date: ${runAt}`);
|
|
873
|
+
}
|
|
874
|
+
const target = parseDeferredRunTarget(input.target);
|
|
875
|
+
const source = recordOrUndefined(input.source);
|
|
876
|
+
return compactUndefined({
|
|
877
|
+
id: optionalString(input.id),
|
|
878
|
+
runAt,
|
|
879
|
+
target,
|
|
880
|
+
createdBy: optionalString(input.createdBy),
|
|
881
|
+
reason: optionalString(input.reason),
|
|
882
|
+
source,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
function parseDeferredRunTarget(value) {
|
|
886
|
+
const target = record(value);
|
|
887
|
+
const kind = requiredString(target.kind, "deferred run target kind");
|
|
888
|
+
if (kind === "workspace-task") {
|
|
889
|
+
return {
|
|
890
|
+
kind,
|
|
891
|
+
taskId: requiredString(target.taskId, "deferred run workspace-task taskId"),
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
if (kind === "automation") {
|
|
895
|
+
return compactUndefined({
|
|
896
|
+
kind,
|
|
897
|
+
automation: requiredString(target.automation, "deferred run automation target automation"),
|
|
898
|
+
event: recordOrUndefined(target.event),
|
|
899
|
+
prompt: optionalString(target.prompt),
|
|
900
|
+
cwd: optionalString(target.cwd),
|
|
901
|
+
model: optionalString(target.model),
|
|
902
|
+
sandbox: sandboxValue(target.sandbox, "deferred run automation target sandbox"),
|
|
903
|
+
approvalPolicy: approvalPolicyValue(target.approvalPolicy, "deferred run automation target approvalPolicy"),
|
|
904
|
+
permissions: optionalString(target.permissions),
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
if (kind === "turn") {
|
|
908
|
+
const prompt = requiredString(target.prompt, "deferred run turn target prompt");
|
|
909
|
+
return compactUndefined({
|
|
910
|
+
kind,
|
|
911
|
+
prompt,
|
|
912
|
+
threadId: optionalString(target.threadId),
|
|
913
|
+
cwd: optionalString(target.cwd),
|
|
914
|
+
model: optionalString(target.model),
|
|
915
|
+
serviceTier: optionalString(target.serviceTier),
|
|
916
|
+
sandbox: sandboxValue(target.sandbox, "deferred run turn target sandbox"),
|
|
917
|
+
approvalPolicy: approvalPolicyValue(target.approvalPolicy, "deferred run turn target approvalPolicy"),
|
|
918
|
+
permissions: optionalString(target.permissions),
|
|
919
|
+
responsesapiClientMetadata: stringRecord(target.responsesapiClientMetadata),
|
|
920
|
+
outputSchema: target.outputSchema,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
throw new Error(`Invalid deferred run target kind: ${kind}`);
|
|
924
|
+
}
|
|
925
|
+
function normalizeDeferredRunIntent(value) {
|
|
926
|
+
const input = record(value);
|
|
927
|
+
return {
|
|
928
|
+
id: requiredString(input.id, "deferred run id"),
|
|
929
|
+
status: deferredRunStatus(input.status),
|
|
930
|
+
mode: workspaceMode(input.mode),
|
|
931
|
+
runAt: requiredString(input.runAt, "deferred run runAt"),
|
|
932
|
+
target: parseDeferredRunTarget(input.target),
|
|
933
|
+
createdAt: requiredString(input.createdAt, "deferred run createdAt"),
|
|
934
|
+
updatedAt: requiredString(input.updatedAt, "deferred run updatedAt"),
|
|
935
|
+
createdBy: optionalString(input.createdBy),
|
|
936
|
+
reason: optionalString(input.reason),
|
|
937
|
+
source: recordOrUndefined(input.source),
|
|
938
|
+
attemptIds: Array.isArray(input.attemptIds)
|
|
939
|
+
? input.attemptIds.filter((entry) => typeof entry === "string")
|
|
940
|
+
: [],
|
|
941
|
+
lease: isRecord(input.lease)
|
|
942
|
+
? {
|
|
943
|
+
attemptId: requiredString(input.lease.attemptId, "deferred run lease attemptId"),
|
|
944
|
+
claimedAt: requiredString(input.lease.claimedAt, "deferred run lease claimedAt"),
|
|
945
|
+
expiresAt: requiredString(input.lease.expiresAt, "deferred run lease expiresAt"),
|
|
946
|
+
executorId: requiredString(input.lease.executorId, "deferred run lease executorId"),
|
|
947
|
+
}
|
|
948
|
+
: undefined,
|
|
949
|
+
completedAt: optionalString(input.completedAt),
|
|
950
|
+
canceledAt: optionalString(input.canceledAt),
|
|
951
|
+
error: optionalString(input.error),
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
function normalizeDeferredRunAttempt(value) {
|
|
955
|
+
const input = record(value);
|
|
956
|
+
return {
|
|
957
|
+
id: requiredString(input.id, "deferred run attempt id"),
|
|
958
|
+
intentId: requiredString(input.intentId, "deferred run attempt intentId"),
|
|
959
|
+
status: deferredAttemptStatus(input.status),
|
|
960
|
+
mode: workspaceMode(input.mode),
|
|
961
|
+
startedAt: requiredString(input.startedAt, "deferred run attempt startedAt"),
|
|
962
|
+
finishedAt: optionalString(input.finishedAt),
|
|
963
|
+
executorId: requiredString(input.executorId, "deferred run attempt executorId"),
|
|
964
|
+
leaseExpiresAt: requiredString(input.leaseExpiresAt, "deferred run attempt leaseExpiresAt"),
|
|
965
|
+
outputPath: optionalString(input.outputPath),
|
|
966
|
+
error: optionalString(input.error),
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
function dueTasks(tasks, runs, now, intents = []) {
|
|
458
970
|
return tasks.filter((task) => {
|
|
459
971
|
if (!task.enabled) {
|
|
460
972
|
return false;
|
|
@@ -462,7 +974,9 @@ function dueTasks(tasks, runs, now) {
|
|
|
462
974
|
if (!task.schedule) {
|
|
463
975
|
return false;
|
|
464
976
|
}
|
|
465
|
-
return isScheduleDue(task.schedule, now) &&
|
|
977
|
+
return isScheduleDue(task.schedule, now) &&
|
|
978
|
+
!hasRunForDate(task.id, runs, now) &&
|
|
979
|
+
!hasScheduledIntentForDate(task.id, intents, now);
|
|
466
980
|
});
|
|
467
981
|
}
|
|
468
982
|
function isScheduleDue(schedule, now) {
|
|
@@ -487,6 +1001,32 @@ function hasRunForDate(taskId, runs, now) {
|
|
|
487
1001
|
const today = now.toISOString().slice(0, 10);
|
|
488
1002
|
return runs.some((run) => run.taskId === taskId && run.startedAt.startsWith(today));
|
|
489
1003
|
}
|
|
1004
|
+
function hasScheduledIntentForDate(taskId, intents, now) {
|
|
1005
|
+
const expected = scheduledDeferredRunId(taskId, now);
|
|
1006
|
+
return intents.some((intent) => intent.id === expected ||
|
|
1007
|
+
(intent.target.kind === "workspace-task" &&
|
|
1008
|
+
intent.target.taskId === taskId &&
|
|
1009
|
+
intent.source?.kind === "workspace-task-schedule" &&
|
|
1010
|
+
intent.source.date === now.toISOString().slice(0, 10)));
|
|
1011
|
+
}
|
|
1012
|
+
function isDeferredIntentDue(intent, now) {
|
|
1013
|
+
if (intent.status === "pending") {
|
|
1014
|
+
return intent.runAt <= now.toISOString();
|
|
1015
|
+
}
|
|
1016
|
+
if (intent.status === "running" && intent.lease?.expiresAt) {
|
|
1017
|
+
return intent.lease.expiresAt <= now.toISOString();
|
|
1018
|
+
}
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
function isTerminalDeferredRunStatus(status) {
|
|
1022
|
+
return status === "completed" || status === "failed" || status === "canceled";
|
|
1023
|
+
}
|
|
1024
|
+
function isWorkspaceRunRecord(value) {
|
|
1025
|
+
const input = record(value);
|
|
1026
|
+
return typeof input.id === "string" &&
|
|
1027
|
+
typeof input.taskId === "string" &&
|
|
1028
|
+
(input.status === "completed" || input.status === "failed" || input.status === "skipped");
|
|
1029
|
+
}
|
|
490
1030
|
function countFailingTasks(tasks, runs) {
|
|
491
1031
|
return tasks.filter((task) => consecutiveFailures(task.id, runs) > 0).length;
|
|
492
1032
|
}
|
|
@@ -527,6 +1067,69 @@ async function ensureStateDirs(context) {
|
|
|
527
1067
|
await mkdir(path.join(context.stateRoot, name), { recursive: true });
|
|
528
1068
|
}
|
|
529
1069
|
}
|
|
1070
|
+
async function ensureDeferredRunDirs(context) {
|
|
1071
|
+
for (const dir of [
|
|
1072
|
+
deferredIntentDir(context),
|
|
1073
|
+
deferredAttemptDir(context),
|
|
1074
|
+
deferredOutputDir(context),
|
|
1075
|
+
deferredClaimDir(context),
|
|
1076
|
+
]) {
|
|
1077
|
+
await mkdir(dir, { recursive: true });
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function deferredRoot(context) {
|
|
1081
|
+
return path.join(context.stateRoot, "deferred");
|
|
1082
|
+
}
|
|
1083
|
+
function deferredIntentDir(context) {
|
|
1084
|
+
return path.join(deferredRoot(context), "intents");
|
|
1085
|
+
}
|
|
1086
|
+
function deferredAttemptDir(context) {
|
|
1087
|
+
return path.join(deferredRoot(context), "attempts");
|
|
1088
|
+
}
|
|
1089
|
+
function deferredOutputDir(context) {
|
|
1090
|
+
return path.join(deferredRoot(context), "outputs");
|
|
1091
|
+
}
|
|
1092
|
+
function deferredClaimDir(context) {
|
|
1093
|
+
return path.join(deferredRoot(context), "claims");
|
|
1094
|
+
}
|
|
1095
|
+
function deferredIntentPath(context, intentId) {
|
|
1096
|
+
return path.join(deferredIntentDir(context), `${safeFileSegment(intentId)}.json`);
|
|
1097
|
+
}
|
|
1098
|
+
function deferredAttemptPath(context, attemptId) {
|
|
1099
|
+
return path.join(deferredAttemptDir(context), `${safeFileSegment(attemptId)}.json`);
|
|
1100
|
+
}
|
|
1101
|
+
function deferredClaimPath(context, intentId) {
|
|
1102
|
+
return path.join(deferredClaimDir(context), `${safeFileSegment(intentId)}.json`);
|
|
1103
|
+
}
|
|
1104
|
+
function deferredRunId(createdAt) {
|
|
1105
|
+
return `deferred-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
1106
|
+
}
|
|
1107
|
+
function deferredAttemptId(intentId, startedAt) {
|
|
1108
|
+
return `${startedAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}-${safeFileSegment(intentId).slice(0, 48)}`;
|
|
1109
|
+
}
|
|
1110
|
+
function scheduledDeferredRunId(taskId, now) {
|
|
1111
|
+
return `scheduled-${safeFileSegment(taskId)}-${now.toISOString().slice(0, 10)}`;
|
|
1112
|
+
}
|
|
1113
|
+
function safeFileSegment(value) {
|
|
1114
|
+
const safe = value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1115
|
+
return safe.slice(0, 120) || "deferred-run";
|
|
1116
|
+
}
|
|
1117
|
+
async function writeNewJsonFile(file, value) {
|
|
1118
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
1119
|
+
const handle = await open(file, "wx");
|
|
1120
|
+
try {
|
|
1121
|
+
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`);
|
|
1122
|
+
}
|
|
1123
|
+
finally {
|
|
1124
|
+
await handle.close();
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
async function writeJsonFileAtomic(file, value) {
|
|
1128
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
1129
|
+
const tmpPath = `${file}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
1130
|
+
await writeFile(tmpPath, `${JSON.stringify(value, null, 2)}\n`);
|
|
1131
|
+
await rename(tmpPath, file);
|
|
1132
|
+
}
|
|
530
1133
|
async function writeScaffoldFile(workspaceRoot, relativePath, content, overwrite) {
|
|
531
1134
|
const file = path.join(workspaceRoot, relativePath);
|
|
532
1135
|
const normalizedContent = content.endsWith("\n") ? content : `${content}\n`;
|
|
@@ -720,6 +1323,9 @@ function requiredString(value, label) {
|
|
|
720
1323
|
function optionalString(value) {
|
|
721
1324
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
722
1325
|
}
|
|
1326
|
+
function recordOrUndefined(value) {
|
|
1327
|
+
return isRecord(value) ? value : undefined;
|
|
1328
|
+
}
|
|
723
1329
|
function record(value) {
|
|
724
1330
|
return isRecord(value) ? value : {};
|
|
725
1331
|
}
|
|
@@ -741,6 +1347,58 @@ function booleanValue(value, label) {
|
|
|
741
1347
|
}
|
|
742
1348
|
return value;
|
|
743
1349
|
}
|
|
1350
|
+
function stringRecord(value) {
|
|
1351
|
+
if (!isRecord(value)) {
|
|
1352
|
+
return undefined;
|
|
1353
|
+
}
|
|
1354
|
+
const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
|
|
1355
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
1356
|
+
}
|
|
1357
|
+
function sandboxValue(value, label) {
|
|
1358
|
+
if (value === "danger-full-access" ||
|
|
1359
|
+
value === "read-only" ||
|
|
1360
|
+
value === "workspace-write") {
|
|
1361
|
+
return value;
|
|
1362
|
+
}
|
|
1363
|
+
if (value !== undefined) {
|
|
1364
|
+
throw new Error(`${label} must be danger-full-access, workspace-write, or read-only`);
|
|
1365
|
+
}
|
|
1366
|
+
return undefined;
|
|
1367
|
+
}
|
|
1368
|
+
function approvalPolicyValue(value, label) {
|
|
1369
|
+
if (value === "never" ||
|
|
1370
|
+
value === "on-failure" ||
|
|
1371
|
+
value === "on-request" ||
|
|
1372
|
+
value === "untrusted") {
|
|
1373
|
+
return value;
|
|
1374
|
+
}
|
|
1375
|
+
if (value !== undefined) {
|
|
1376
|
+
throw new Error(`${label} must be never, on-failure, on-request, or untrusted`);
|
|
1377
|
+
}
|
|
1378
|
+
return undefined;
|
|
1379
|
+
}
|
|
1380
|
+
function deferredRunStatus(value) {
|
|
1381
|
+
if (value === "pending" ||
|
|
1382
|
+
value === "running" ||
|
|
1383
|
+
value === "completed" ||
|
|
1384
|
+
value === "failed" ||
|
|
1385
|
+
value === "canceled") {
|
|
1386
|
+
return value;
|
|
1387
|
+
}
|
|
1388
|
+
throw new Error(`Invalid deferred run status: ${String(value)}`);
|
|
1389
|
+
}
|
|
1390
|
+
function deferredAttemptStatus(value) {
|
|
1391
|
+
if (value === "running" || value === "completed" || value === "failed") {
|
|
1392
|
+
return value;
|
|
1393
|
+
}
|
|
1394
|
+
throw new Error(`Invalid deferred run attempt status: ${String(value)}`);
|
|
1395
|
+
}
|
|
1396
|
+
function workspaceMode(value) {
|
|
1397
|
+
if (value === "local" || value === "actions") {
|
|
1398
|
+
return value;
|
|
1399
|
+
}
|
|
1400
|
+
throw new Error(`Invalid workspace mode: ${String(value)}`);
|
|
1401
|
+
}
|
|
744
1402
|
function positiveInteger(value, label) {
|
|
745
1403
|
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
746
1404
|
throw new Error(`${label} must be a positive integer`);
|
|
@@ -750,6 +1408,18 @@ function positiveInteger(value, label) {
|
|
|
750
1408
|
function isRecord(value) {
|
|
751
1409
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
752
1410
|
}
|
|
1411
|
+
function clampLimit(value, fallback) {
|
|
1412
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
1413
|
+
return fallback;
|
|
1414
|
+
}
|
|
1415
|
+
return Math.max(1, Math.min(1_000, Math.trunc(value)));
|
|
1416
|
+
}
|
|
1417
|
+
function isNotFoundError(error) {
|
|
1418
|
+
return isRecord(error) && error.code === "ENOENT";
|
|
1419
|
+
}
|
|
1420
|
+
function isAlreadyExistsError(error) {
|
|
1421
|
+
return isRecord(error) && error.code === "EEXIST";
|
|
1422
|
+
}
|
|
753
1423
|
async function exists(file) {
|
|
754
1424
|
try {
|
|
755
1425
|
await stat(file);
|