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.
Files changed (135) hide show
  1. package/README.md +3 -0
  2. package/dist/app-server/generated/AutoCompactTokenLimitScope.d.ts +6 -0
  3. package/dist/app-server/generated/AutoCompactTokenLimitScope.d.ts.map +1 -0
  4. package/dist/app-server/generated/AutoCompactTokenLimitScope.js +3 -0
  5. package/dist/app-server/generated/AutoCompactTokenLimitScope.js.map +1 -0
  6. package/dist/app-server/generated/ClientRequest.d.ts +15 -0
  7. package/dist/app-server/generated/ClientRequest.d.ts.map +1 -1
  8. package/dist/app-server/generated/FunctionCallOutputContentItem.d.ts +3 -0
  9. package/dist/app-server/generated/FunctionCallOutputContentItem.d.ts.map +1 -1
  10. package/dist/app-server/generated/ImageDetail.d.ts +1 -1
  11. package/dist/app-server/generated/ImageDetail.d.ts.map +1 -1
  12. package/dist/app-server/generated/ServerNotification.d.ts +4 -0
  13. package/dist/app-server/generated/ServerNotification.d.ts.map +1 -1
  14. package/dist/app-server/generated/index.d.ts +1 -0
  15. package/dist/app-server/generated/index.d.ts.map +1 -1
  16. package/dist/app-server/generated/index.js.map +1 -1
  17. package/dist/app-server/generated/v2/ActivePermissionProfile.d.ts +2 -2
  18. package/dist/app-server/generated/v2/AdditionalContextEntry.d.ts +6 -0
  19. package/dist/app-server/generated/v2/AdditionalContextEntry.d.ts.map +1 -0
  20. package/dist/app-server/generated/v2/AdditionalContextEntry.js +3 -0
  21. package/dist/app-server/generated/v2/AdditionalContextEntry.js.map +1 -0
  22. package/dist/app-server/generated/v2/AdditionalContextKind.d.ts +2 -0
  23. package/dist/app-server/generated/v2/AdditionalContextKind.d.ts.map +1 -0
  24. package/dist/app-server/generated/v2/AdditionalContextKind.js +3 -0
  25. package/dist/app-server/generated/v2/AdditionalContextKind.js.map +1 -0
  26. package/dist/app-server/generated/v2/CommandExecParams.d.ts +2 -3
  27. package/dist/app-server/generated/v2/CommandExecParams.d.ts.map +1 -1
  28. package/dist/app-server/generated/v2/ComputerUseRequirements.d.ts +4 -0
  29. package/dist/app-server/generated/v2/ComputerUseRequirements.d.ts.map +1 -0
  30. package/dist/app-server/generated/v2/ComputerUseRequirements.js +3 -0
  31. package/dist/app-server/generated/v2/ComputerUseRequirements.js.map +1 -0
  32. package/dist/app-server/generated/v2/Config.d.ts +2 -5
  33. package/dist/app-server/generated/v2/Config.d.ts.map +1 -1
  34. package/dist/app-server/generated/v2/ConfigReadParams.d.ts +1 -1
  35. package/dist/app-server/generated/v2/ConfigReadParams.d.ts.map +1 -1
  36. package/dist/app-server/generated/v2/ConfigRequirements.d.ts +4 -0
  37. package/dist/app-server/generated/v2/ConfigRequirements.d.ts.map +1 -1
  38. package/dist/app-server/generated/v2/FeedbackUploadParams.d.ts +1 -1
  39. package/dist/app-server/generated/v2/FeedbackUploadParams.d.ts.map +1 -1
  40. package/dist/app-server/generated/v2/FileSystemAccessMode.d.ts +1 -1
  41. package/dist/app-server/generated/v2/GetAccountParams.d.ts +1 -1
  42. package/dist/app-server/generated/v2/GetAccountParams.d.ts.map +1 -1
  43. package/dist/app-server/generated/v2/HookEventName.d.ts +1 -1
  44. package/dist/app-server/generated/v2/HookEventName.d.ts.map +1 -1
  45. package/dist/app-server/generated/v2/ListMcpServerStatusParams.d.ts +1 -0
  46. package/dist/app-server/generated/v2/ListMcpServerStatusParams.d.ts.map +1 -1
  47. package/dist/app-server/generated/v2/ManagedHooksRequirements.d.ts +2 -0
  48. package/dist/app-server/generated/v2/ManagedHooksRequirements.d.ts.map +1 -1
  49. package/dist/app-server/generated/v2/Model.d.ts +4 -0
  50. package/dist/app-server/generated/v2/Model.d.ts.map +1 -1
  51. package/dist/app-server/generated/v2/PermissionProfileListParams.d.ts +15 -0
  52. package/dist/app-server/generated/v2/PermissionProfileListParams.d.ts.map +1 -0
  53. package/dist/app-server/generated/v2/PermissionProfileListParams.js +3 -0
  54. package/dist/app-server/generated/v2/PermissionProfileListParams.js.map +1 -0
  55. package/dist/app-server/generated/v2/PermissionProfileListResponse.d.ts +10 -0
  56. package/dist/app-server/generated/v2/PermissionProfileListResponse.d.ts.map +1 -0
  57. package/dist/app-server/generated/v2/PermissionProfileListResponse.js +3 -0
  58. package/dist/app-server/generated/v2/PermissionProfileListResponse.js.map +1 -0
  59. package/dist/app-server/generated/v2/PermissionProfileSummary.d.ts +11 -0
  60. package/dist/app-server/generated/v2/PermissionProfileSummary.d.ts.map +1 -0
  61. package/dist/app-server/generated/v2/PermissionProfileSummary.js +3 -0
  62. package/dist/app-server/generated/v2/PermissionProfileSummary.js.map +1 -0
  63. package/dist/app-server/generated/v2/PluginListMarketplaceKind.d.ts +1 -1
  64. package/dist/app-server/generated/v2/PluginListMarketplaceKind.d.ts.map +1 -1
  65. package/dist/app-server/generated/v2/ThreadForkParams.d.ts +3 -2
  66. package/dist/app-server/generated/v2/ThreadForkParams.d.ts.map +1 -1
  67. package/dist/app-server/generated/v2/ThreadItem.d.ts +1 -0
  68. package/dist/app-server/generated/v2/ThreadItem.d.ts.map +1 -1
  69. package/dist/app-server/generated/v2/ThreadReadParams.d.ts +1 -1
  70. package/dist/app-server/generated/v2/ThreadReadParams.d.ts.map +1 -1
  71. package/dist/app-server/generated/v2/ThreadResumeParams.d.ts +11 -4
  72. package/dist/app-server/generated/v2/ThreadResumeParams.d.ts.map +1 -1
  73. package/dist/app-server/generated/v2/ThreadSearchParams.d.ts +36 -0
  74. package/dist/app-server/generated/v2/ThreadSearchParams.d.ts.map +1 -0
  75. package/dist/app-server/generated/v2/ThreadSearchParams.js +3 -0
  76. package/dist/app-server/generated/v2/ThreadSearchParams.js.map +1 -0
  77. package/dist/app-server/generated/v2/ThreadSearchResponse.d.ts +17 -0
  78. package/dist/app-server/generated/v2/ThreadSearchResponse.d.ts.map +1 -0
  79. package/dist/app-server/generated/v2/ThreadSearchResponse.js +3 -0
  80. package/dist/app-server/generated/v2/ThreadSearchResponse.js.map +1 -0
  81. package/dist/app-server/generated/v2/ThreadSearchResult.d.ts +6 -0
  82. package/dist/app-server/generated/v2/ThreadSearchResult.d.ts.map +1 -0
  83. package/dist/app-server/generated/v2/ThreadSearchResult.js +3 -0
  84. package/dist/app-server/generated/v2/ThreadSearchResult.js.map +1 -0
  85. package/dist/app-server/generated/v2/ThreadSettings.d.ts +24 -0
  86. package/dist/app-server/generated/v2/ThreadSettings.d.ts.map +1 -0
  87. package/dist/app-server/generated/v2/ThreadSettings.js +3 -0
  88. package/dist/app-server/generated/v2/ThreadSettings.js.map +1 -0
  89. package/dist/app-server/generated/v2/ThreadSettingsUpdateParams.d.ts +60 -0
  90. package/dist/app-server/generated/v2/ThreadSettingsUpdateParams.d.ts.map +1 -0
  91. package/dist/app-server/generated/v2/ThreadSettingsUpdateParams.js +3 -0
  92. package/dist/app-server/generated/v2/ThreadSettingsUpdateParams.js.map +1 -0
  93. package/dist/app-server/generated/v2/ThreadSettingsUpdateResponse.d.ts +2 -0
  94. package/dist/app-server/generated/v2/ThreadSettingsUpdateResponse.d.ts.map +1 -0
  95. package/dist/app-server/generated/v2/ThreadSettingsUpdateResponse.js +3 -0
  96. package/dist/app-server/generated/v2/ThreadSettingsUpdateResponse.js.map +1 -0
  97. package/dist/app-server/generated/v2/ThreadSettingsUpdatedNotification.d.ts +6 -0
  98. package/dist/app-server/generated/v2/ThreadSettingsUpdatedNotification.d.ts.map +1 -0
  99. package/dist/app-server/generated/v2/ThreadSettingsUpdatedNotification.js +3 -0
  100. package/dist/app-server/generated/v2/ThreadSettingsUpdatedNotification.js.map +1 -0
  101. package/dist/app-server/generated/v2/ThreadStartParams.d.ts +2 -2
  102. package/dist/app-server/generated/v2/ThreadStartParams.d.ts.map +1 -1
  103. package/dist/app-server/generated/v2/TurnStartParams.d.ts +7 -0
  104. package/dist/app-server/generated/v2/TurnStartParams.d.ts.map +1 -1
  105. package/dist/app-server/generated/v2/TurnSteerParams.d.ts +7 -0
  106. package/dist/app-server/generated/v2/TurnSteerParams.d.ts.map +1 -1
  107. package/dist/app-server/generated/v2/index.d.ts +13 -0
  108. package/dist/app-server/generated/v2/index.d.ts.map +1 -1
  109. package/dist/bin/codex-toys-proxy.js +17 -17
  110. package/dist/cli/args.d.ts +50 -0
  111. package/dist/cli/args.d.ts.map +1 -1
  112. package/dist/cli/args.js +97 -0
  113. package/dist/cli/args.js.map +1 -1
  114. package/dist/cli/index.js +134 -1
  115. package/dist/cli/index.js.map +1 -1
  116. package/dist/cli/toybox.d.ts.map +1 -1
  117. package/dist/cli/toybox.js +7 -1
  118. package/dist/cli/toybox.js.map +1 -1
  119. package/dist/cli/workspace-autonomy.d.ts +119 -0
  120. package/dist/cli/workspace-autonomy.d.ts.map +1 -1
  121. package/dist/cli/workspace-autonomy.js +676 -6
  122. package/dist/cli/workspace-autonomy.js.map +1 -1
  123. package/dist/index.d.ts +1 -1
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/index.js +1 -1
  126. package/dist/index.js.map +1 -1
  127. package/dist/toybox/deferred-run-methods.d.ts +17 -0
  128. package/dist/toybox/deferred-run-methods.d.ts.map +1 -0
  129. package/dist/toybox/deferred-run-methods.js +150 -0
  130. package/dist/toybox/deferred-run-methods.js.map +1 -0
  131. package/dist/toybox/index.d.ts +1 -0
  132. package/dist/toybox/index.d.ts.map +1 -1
  133. package/dist/toybox/index.js +1 -0
  134. package/dist/toybox/index.js.map +1 -1
  135. 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 due = dueTasks(config.tasks, previousRuns, new Date());
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
- runs.push(await runWorkspaceTask(context, config, task, options));
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 dueTasks(tasks, runs, now) {
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) && !hasRunForDate(task.id, runs, 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);