codex-toys 0.140.2 → 0.140.4
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 +6 -1
- package/dist/app-server/stdio-transport.js +0 -2
- package/dist/app-server/stdio-transport.js.map +1 -1
- package/dist/bin/codex-toys-proxy.js +16 -4
- package/dist/cli/args.d.ts +194 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +461 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/index.js +446 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/pack.d.ts +1 -3
- package/dist/cli/pack.d.ts.map +1 -1
- package/dist/cli/pack.js +3 -138
- package/dist/cli/pack.js.map +1 -1
- package/dist/cli/toybox.d.ts.map +1 -1
- package/dist/cli/toybox.js +24 -2
- package/dist/cli/toybox.js.map +1 -1
- package/dist/cli/turn-automation.d.ts +4 -0
- package/dist/cli/turn-automation.d.ts.map +1 -1
- package/dist/cli/turn-automation.js +17 -0
- package/dist/cli/turn-automation.js.map +1 -1
- package/dist/cli/workspace-autonomy.d.ts +135 -0
- package/dist/cli/workspace-autonomy.d.ts.map +1 -1
- package/dist/cli/workspace-autonomy.js +476 -11
- package/dist/cli/workspace-autonomy.js.map +1 -1
- package/dist/host-overview.d.ts +112 -0
- package/dist/host-overview.d.ts.map +1 -0
- package/dist/host-overview.js +406 -0
- package/dist/host-overview.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +11 -1
- package/dist/proxy.js.map +1 -1
- package/dist/toybox/deferred-run-methods.d.ts +15 -0
- package/dist/toybox/deferred-run-methods.d.ts.map +1 -1
- package/dist/toybox/deferred-run-methods.js +261 -1
- package/dist/toybox/deferred-run-methods.js.map +1 -1
- package/dist/toybox/index.d.ts +2 -1
- package/dist/toybox/index.d.ts.map +1 -1
- package/dist/toybox/index.js +2 -1
- package/dist/toybox/index.js.map +1 -1
- package/dist/toybox/protocol.d.ts +1 -1
- package/dist/toybox/protocol.d.ts.map +1 -1
- package/dist/toybox/protocol.js +1 -0
- package/dist/toybox/protocol.js.map +1 -1
- package/dist/workspace-overview.d.ts +153 -0
- package/dist/workspace-overview.d.ts.map +1 -0
- package/dist/workspace-overview.js +584 -0
- package/dist/workspace-overview.js.map +1 -0
- package/package.json +1 -1
|
@@ -125,6 +125,7 @@ export async function collectWorkspaceDoctorInfo(context, options = {}) {
|
|
|
125
125
|
const now = new Date();
|
|
126
126
|
const latestDeferredRun = deferredRuns
|
|
127
127
|
.toSorted((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
|
|
128
|
+
const deferredDueFlags = await Promise.all(deferredRuns.map(async (intent) => await isDeferredIntentDue(context, intent, now)));
|
|
128
129
|
const failingCount = countFailingTasks(config?.tasks ?? [], runs);
|
|
129
130
|
const includeRunner = options.includeRunner === true || options.runnerProbe !== undefined;
|
|
130
131
|
const runner = includeRunner
|
|
@@ -149,7 +150,7 @@ export async function collectWorkspaceDoctorInfo(context, options = {}) {
|
|
|
149
150
|
dueCount: dueTasks(config?.tasks ?? [], runs, new Date()).length,
|
|
150
151
|
failingCount,
|
|
151
152
|
deferredCount: deferredRuns.length,
|
|
152
|
-
deferredDueCount:
|
|
153
|
+
deferredDueCount: deferredDueFlags.filter(Boolean).length,
|
|
153
154
|
deferredRunningCount: deferredRuns.filter((intent) => intent.status === "running").length,
|
|
154
155
|
deferredFailedCount: deferredRuns.filter((intent) => intent.status === "failed").length,
|
|
155
156
|
latestRun,
|
|
@@ -292,6 +293,7 @@ export async function createDeferredRunIntent(context, params) {
|
|
|
292
293
|
createdBy: input.createdBy,
|
|
293
294
|
reason: input.reason,
|
|
294
295
|
source: input.source,
|
|
296
|
+
dependsOn: input.dependsOn,
|
|
295
297
|
attemptIds: [],
|
|
296
298
|
});
|
|
297
299
|
await writeNewJsonFile(deferredIntentPath(context, intent.id), intent);
|
|
@@ -329,12 +331,141 @@ export async function readDeferredRun(context, intentId, options = {}) {
|
|
|
329
331
|
: undefined;
|
|
330
332
|
return compactUndefined({ intent, attempts, outputs });
|
|
331
333
|
}
|
|
334
|
+
export async function enqueuePromptQueueIntent(context, params) {
|
|
335
|
+
const input = parsePromptQueueEnqueueParams(params);
|
|
336
|
+
const after = input.afterIntentId
|
|
337
|
+
? {
|
|
338
|
+
kind: "deferred-run",
|
|
339
|
+
intentId: input.afterIntentId,
|
|
340
|
+
status: input.afterStatus,
|
|
341
|
+
}
|
|
342
|
+
: undefined;
|
|
343
|
+
return await createDeferredRunIntent(context, {
|
|
344
|
+
id: input.id,
|
|
345
|
+
runAt: input.runAt,
|
|
346
|
+
target: compactUndefined({
|
|
347
|
+
kind: "turn",
|
|
348
|
+
prompt: input.prompt,
|
|
349
|
+
threadId: input.threadId,
|
|
350
|
+
cwd: input.cwd,
|
|
351
|
+
model: input.model,
|
|
352
|
+
serviceTier: input.serviceTier,
|
|
353
|
+
effort: input.effort,
|
|
354
|
+
sandbox: input.sandbox,
|
|
355
|
+
approvalPolicy: input.approvalPolicy,
|
|
356
|
+
permissions: input.permissions,
|
|
357
|
+
responsesapiClientMetadata: input.responsesapiClientMetadata,
|
|
358
|
+
outputSchema: input.outputSchema,
|
|
359
|
+
}),
|
|
360
|
+
createdBy: input.createdBy ?? "workspace-prompt-queue",
|
|
361
|
+
reason: input.reason ?? input.title,
|
|
362
|
+
source: promptQueueSource(input, after),
|
|
363
|
+
dependsOn: after ? [after] : undefined,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
export async function listPromptQueueIntents(context, options = {}) {
|
|
367
|
+
const intents = await listDeferredRunIntents(context, {
|
|
368
|
+
status: options.status,
|
|
369
|
+
});
|
|
370
|
+
return intents
|
|
371
|
+
.filter((intent) => isPromptQueueIntent(intent, options.queue))
|
|
372
|
+
.slice(0, clampLimit(options.limit, 500));
|
|
373
|
+
}
|
|
374
|
+
export async function collectPromptQueueRuns(context, options = {}) {
|
|
375
|
+
return await collectDeferredRuns(context, {
|
|
376
|
+
cursor: options.cursor,
|
|
377
|
+
defaultCursor: "prompt-queue",
|
|
378
|
+
now: options.now,
|
|
379
|
+
filter: (intent) => isPromptQueueIntent(intent, options.queue),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
export async function runDuePromptQueueIntents(context, options) {
|
|
383
|
+
return await runDueDeferredRuns(context, {
|
|
384
|
+
...options,
|
|
385
|
+
filter: (intent) => isPromptQueueIntent(intent, options.queue),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
export async function enqueueLocalHandoffIntent(context, params) {
|
|
389
|
+
const input = parseLocalHandoffEnqueueParams(params);
|
|
390
|
+
const after = input.afterIntentId
|
|
391
|
+
? {
|
|
392
|
+
kind: "deferred-run",
|
|
393
|
+
intentId: input.afterIntentId,
|
|
394
|
+
status: input.afterStatus,
|
|
395
|
+
}
|
|
396
|
+
: undefined;
|
|
397
|
+
return await createDeferredRunIntent(context, {
|
|
398
|
+
id: input.id,
|
|
399
|
+
runAt: input.runAt,
|
|
400
|
+
target: compactUndefined({
|
|
401
|
+
kind: "turn",
|
|
402
|
+
prompt: input.prompt,
|
|
403
|
+
threadId: input.threadId,
|
|
404
|
+
cwd: input.cwd,
|
|
405
|
+
model: input.model,
|
|
406
|
+
serviceTier: input.serviceTier,
|
|
407
|
+
effort: input.effort,
|
|
408
|
+
sandbox: input.sandbox,
|
|
409
|
+
approvalPolicy: input.approvalPolicy,
|
|
410
|
+
permissions: input.permissions,
|
|
411
|
+
responsesapiClientMetadata: input.responsesapiClientMetadata,
|
|
412
|
+
outputSchema: input.outputSchema,
|
|
413
|
+
}),
|
|
414
|
+
createdBy: input.createdBy ?? "workspace-local-handoff",
|
|
415
|
+
reason: input.reason ?? input.title,
|
|
416
|
+
source: localHandoffSource(input, after),
|
|
417
|
+
dependsOn: after ? [after] : undefined,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
export async function listLocalHandoffIntents(context, options = {}) {
|
|
421
|
+
const intents = await listDeferredRunIntents(context, {
|
|
422
|
+
status: options.status,
|
|
423
|
+
});
|
|
424
|
+
return intents
|
|
425
|
+
.filter((intent) => isLocalHandoffIntent(intent, options))
|
|
426
|
+
.slice(0, clampLimit(options.limit, 500));
|
|
427
|
+
}
|
|
428
|
+
export async function collectLocalHandoffRuns(context, options = {}) {
|
|
429
|
+
return await collectDeferredRuns(context, {
|
|
430
|
+
cursor: options.cursor,
|
|
431
|
+
defaultCursor: "local-handoff",
|
|
432
|
+
now: options.now,
|
|
433
|
+
filter: (intent) => isLocalHandoffIntent(intent, options),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
export async function drainLocalHandoffQueue(context, options) {
|
|
437
|
+
const action = options.action ?? "run";
|
|
438
|
+
const result = await runDueDeferredRuns(context, {
|
|
439
|
+
...options,
|
|
440
|
+
includeLocalHandoffs: true,
|
|
441
|
+
localHandoffMaterialize: action === "materialize"
|
|
442
|
+
? { queue: options.promptQueue }
|
|
443
|
+
: undefined,
|
|
444
|
+
filter: (intent) => isLocalHandoffIntent(intent, {
|
|
445
|
+
queue: options.queue,
|
|
446
|
+
targetHost: options.hostId ? undefined : "local-controller",
|
|
447
|
+
hostId: options.hostId,
|
|
448
|
+
capabilities: options.capabilities ?? [],
|
|
449
|
+
}),
|
|
450
|
+
});
|
|
451
|
+
return {
|
|
452
|
+
mode: result.mode,
|
|
453
|
+
action,
|
|
454
|
+
executions: result.executions,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
332
457
|
export async function collectDeferredRuns(context, options = {}) {
|
|
333
458
|
await ensureDeferredRunDirs(context);
|
|
334
|
-
const cursor = deferredCollectCursorName(options.cursor);
|
|
459
|
+
const cursor = deferredCollectCursorName(options.cursor, options.defaultCursor);
|
|
335
460
|
const previousCursor = await readDeferredRunCollectCursor(context, cursor);
|
|
336
461
|
const collectedAt = (options.now ?? new Date()).toISOString();
|
|
337
|
-
const
|
|
462
|
+
const filteredIntents = [];
|
|
463
|
+
for (const intent of await listDeferredRunIntents(context)) {
|
|
464
|
+
if (!options.filter || await options.filter(intent)) {
|
|
465
|
+
filteredIntents.push(intent);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const terminalIntents = filteredIntents
|
|
338
469
|
.filter((intent) => isTerminalDeferredRunStatus(intent.status))
|
|
339
470
|
.toSorted((left, right) => left.updatedAt.localeCompare(right.updatedAt) || left.id.localeCompare(right.id))
|
|
340
471
|
.filter((intent) => isAfterDeferredRunCollectCursor(intent, previousCursor));
|
|
@@ -371,13 +502,59 @@ export async function cancelDeferredRunIntent(context, intentId) {
|
|
|
371
502
|
await writeJsonFileAtomic(deferredIntentPath(context, intentId), canceled);
|
|
372
503
|
return canceled;
|
|
373
504
|
}
|
|
505
|
+
export async function retryDeferredRunIntent(context, intentId, params = {}, options = {}) {
|
|
506
|
+
await ensureDeferredRunDirs(context);
|
|
507
|
+
const originalIntent = await readDeferredRunIntent(context, intentId);
|
|
508
|
+
if (!isTerminalDeferredRunStatus(originalIntent.status)) {
|
|
509
|
+
throw new Error(`Only terminal deferred runs can be retried: ${intentId} is ${originalIntent.status}`);
|
|
510
|
+
}
|
|
511
|
+
const input = parseDeferredRunRetryParams(params);
|
|
512
|
+
const now = (options.now ?? new Date()).toISOString();
|
|
513
|
+
const retrySource = retryDeferredRunSource(originalIntent, input.source);
|
|
514
|
+
let id = input.id ?? deferredRetryRunId(originalIntent.id, now);
|
|
515
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
516
|
+
const intent = compactUndefined({
|
|
517
|
+
id,
|
|
518
|
+
status: "pending",
|
|
519
|
+
mode: context.mode,
|
|
520
|
+
runAt: input.runAt ?? now,
|
|
521
|
+
target: originalIntent.target,
|
|
522
|
+
createdAt: now,
|
|
523
|
+
updatedAt: now,
|
|
524
|
+
createdBy: input.createdBy ?? "workspace-deferred-retry",
|
|
525
|
+
reason: input.reason ?? `Retry deferred run ${originalIntent.id}`,
|
|
526
|
+
source: retrySource,
|
|
527
|
+
dependsOn: originalIntent.dependsOn,
|
|
528
|
+
attemptIds: [],
|
|
529
|
+
});
|
|
530
|
+
try {
|
|
531
|
+
await writeNewJsonFile(deferredIntentPath(context, intent.id), intent);
|
|
532
|
+
return { intent, originalIntent };
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
if (!isAlreadyExistsError(error)) {
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
id = deferredRetryRunId(originalIntent.id, now);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
throw new Error(`Could not allocate retry deferred run id for ${intentId}`);
|
|
542
|
+
}
|
|
374
543
|
export async function runDueDeferredRuns(context, options) {
|
|
375
544
|
await ensureStateDirs(context);
|
|
376
545
|
await ensureDeferredRunDirs(context);
|
|
377
546
|
const now = options.now ?? new Date();
|
|
378
|
-
const due =
|
|
379
|
-
|
|
380
|
-
|
|
547
|
+
const due = [];
|
|
548
|
+
for (const intent of await listDeferredRunIntents(context)) {
|
|
549
|
+
if (await isDeferredIntentDue(context, intent, now) &&
|
|
550
|
+
(options.includeLocalHandoffs === true || !isLocalHandoffIntent(intent)) &&
|
|
551
|
+
(!options.filter || await options.filter(intent))) {
|
|
552
|
+
due.push(intent);
|
|
553
|
+
}
|
|
554
|
+
if (due.length >= clampLimit(options.limit, 100)) {
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
381
558
|
const executions = [];
|
|
382
559
|
for (const intent of due) {
|
|
383
560
|
const claim = await claimDeferredRunIntent(context, intent, {
|
|
@@ -393,6 +570,7 @@ export async function runDueDeferredRuns(context, options) {
|
|
|
393
570
|
const result = await executeDeferredRunTarget(context, claim.intent, {
|
|
394
571
|
callToybox: options.callToybox,
|
|
395
572
|
automationCwd: options.automationCwd,
|
|
573
|
+
localHandoffMaterialize: options.localHandoffMaterialize,
|
|
396
574
|
});
|
|
397
575
|
await writeJsonFileAtomic(outputPath, result.output);
|
|
398
576
|
const finishedAt = new Date().toISOString();
|
|
@@ -535,6 +713,13 @@ async function runAutomationTask(context, config, task, runId, startedAt, option
|
|
|
535
713
|
async function executeDeferredRunTarget(context, intent, options) {
|
|
536
714
|
try {
|
|
537
715
|
const target = intent.target;
|
|
716
|
+
if (options.localHandoffMaterialize && isLocalHandoffIntent(intent)) {
|
|
717
|
+
const materialized = await materializeLocalHandoffIntent(context, intent, options.localHandoffMaterialize);
|
|
718
|
+
return {
|
|
719
|
+
status: "completed",
|
|
720
|
+
output: { localHandoff: materialized },
|
|
721
|
+
};
|
|
722
|
+
}
|
|
538
723
|
if (target.kind === "workspace-task") {
|
|
539
724
|
const config = await loadWorkspaceConfig(context);
|
|
540
725
|
const task = config.tasks.find((item) => item.id === target.taskId);
|
|
@@ -580,6 +765,42 @@ async function executeDeferredRunTarget(context, intent, options) {
|
|
|
580
765
|
};
|
|
581
766
|
}
|
|
582
767
|
}
|
|
768
|
+
async function materializeLocalHandoffIntent(context, intent, options) {
|
|
769
|
+
if (intent.target.kind !== "turn") {
|
|
770
|
+
throw new Error(`Local handoff can only materialize turn targets: ${intent.id}`);
|
|
771
|
+
}
|
|
772
|
+
const source = recordOrUndefined(intent.source) ?? {};
|
|
773
|
+
const queue = options.queue ?? optionalString(source.queue) ?? "local";
|
|
774
|
+
const promptIntent = await enqueuePromptQueueIntent(context, {
|
|
775
|
+
prompt: intent.target.prompt,
|
|
776
|
+
title: optionalString(source.title) ?? intent.reason,
|
|
777
|
+
queue,
|
|
778
|
+
labels: stringArray(source.labels),
|
|
779
|
+
threadId: intent.target.threadId,
|
|
780
|
+
cwd: intent.target.cwd,
|
|
781
|
+
model: intent.target.model,
|
|
782
|
+
serviceTier: intent.target.serviceTier,
|
|
783
|
+
effort: intent.target.effort,
|
|
784
|
+
sandbox: intent.target.sandbox,
|
|
785
|
+
approvalPolicy: intent.target.approvalPolicy,
|
|
786
|
+
permissions: intent.target.permissions,
|
|
787
|
+
responsesapiClientMetadata: intent.target.responsesapiClientMetadata,
|
|
788
|
+
outputSchema: intent.target.outputSchema,
|
|
789
|
+
createdBy: "workspace-local-handoff-drain",
|
|
790
|
+
reason: intent.reason,
|
|
791
|
+
source: compactUndefined({
|
|
792
|
+
kind: "local-handoff-materialized",
|
|
793
|
+
handoffIntentId: intent.id,
|
|
794
|
+
handoffSource: source,
|
|
795
|
+
}),
|
|
796
|
+
});
|
|
797
|
+
return {
|
|
798
|
+
action: "materialized",
|
|
799
|
+
handoffIntentId: intent.id,
|
|
800
|
+
promptIntentId: promptIntent.id,
|
|
801
|
+
queue,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
583
804
|
async function runAutomationDeferredTarget(context, config, intent, options) {
|
|
584
805
|
const target = await resolveTurnAutomationTarget(intent.target.automation, {
|
|
585
806
|
cwd: context.repoRoot,
|
|
@@ -889,7 +1110,7 @@ async function readDeferredRunCollectCursor(context, cursor) {
|
|
|
889
1110
|
}
|
|
890
1111
|
async function claimDeferredRunIntent(context, intent, options) {
|
|
891
1112
|
const current = await readDeferredRunIntent(context, intent.id);
|
|
892
|
-
if (!isDeferredIntentDue(current, options.now)) {
|
|
1113
|
+
if (!await isDeferredIntentDue(context, current, options.now)) {
|
|
893
1114
|
return undefined;
|
|
894
1115
|
}
|
|
895
1116
|
const claimPath = deferredClaimPath(context, current.id);
|
|
@@ -972,8 +1193,185 @@ function parseDeferredRunCreateParams(value) {
|
|
|
972
1193
|
createdBy: optionalString(input.createdBy),
|
|
973
1194
|
reason: optionalString(input.reason),
|
|
974
1195
|
source,
|
|
1196
|
+
dependsOn: parseDeferredRunDependencies(input.dependsOn),
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
function parsePromptQueueEnqueueParams(value) {
|
|
1200
|
+
const input = record(value);
|
|
1201
|
+
const runAt = optionalString(input.runAt);
|
|
1202
|
+
if (runAt && Number.isNaN(Date.parse(runAt))) {
|
|
1203
|
+
throw new Error(`Prompt queue runAt must be an ISO-compatible date: ${runAt}`);
|
|
1204
|
+
}
|
|
1205
|
+
const afterStatus = deferredDependencyStatusValue(input.afterStatus, "prompt queue afterStatus");
|
|
1206
|
+
return compactUndefined({
|
|
1207
|
+
id: optionalString(input.id),
|
|
1208
|
+
runAt,
|
|
1209
|
+
prompt: requiredString(input.prompt, "prompt queue prompt"),
|
|
1210
|
+
title: optionalString(input.title),
|
|
1211
|
+
queue: optionalString(input.queue),
|
|
1212
|
+
labels: stringArray(input.labels),
|
|
1213
|
+
threadId: optionalString(input.threadId),
|
|
1214
|
+
cwd: optionalString(input.cwd),
|
|
1215
|
+
model: optionalString(input.model),
|
|
1216
|
+
serviceTier: optionalString(input.serviceTier),
|
|
1217
|
+
effort: reasoningEffortValue(input.effort, "prompt queue effort"),
|
|
1218
|
+
sandbox: sandboxValue(input.sandbox, "prompt queue sandbox"),
|
|
1219
|
+
approvalPolicy: approvalPolicyValue(input.approvalPolicy, "prompt queue approvalPolicy"),
|
|
1220
|
+
permissions: optionalString(input.permissions),
|
|
1221
|
+
responsesapiClientMetadata: stringRecord(input.responsesapiClientMetadata),
|
|
1222
|
+
outputSchema: input.outputSchema,
|
|
1223
|
+
afterIntentId: optionalString(input.afterIntentId),
|
|
1224
|
+
afterStatus,
|
|
1225
|
+
createdBy: optionalString(input.createdBy),
|
|
1226
|
+
reason: optionalString(input.reason),
|
|
1227
|
+
source: recordOrUndefined(input.source),
|
|
975
1228
|
});
|
|
976
1229
|
}
|
|
1230
|
+
function parseLocalHandoffEnqueueParams(value) {
|
|
1231
|
+
const input = record(value);
|
|
1232
|
+
const runAt = optionalString(input.runAt);
|
|
1233
|
+
if (runAt && Number.isNaN(Date.parse(runAt))) {
|
|
1234
|
+
throw new Error(`Local handoff runAt must be an ISO-compatible date: ${runAt}`);
|
|
1235
|
+
}
|
|
1236
|
+
const afterStatus = deferredDependencyStatusValue(input.afterStatus, "local handoff afterStatus");
|
|
1237
|
+
return compactUndefined({
|
|
1238
|
+
id: optionalString(input.id),
|
|
1239
|
+
runAt,
|
|
1240
|
+
prompt: requiredString(input.prompt, "local handoff prompt"),
|
|
1241
|
+
title: optionalString(input.title),
|
|
1242
|
+
queue: optionalString(input.queue),
|
|
1243
|
+
labels: stringArray(input.labels),
|
|
1244
|
+
targetHost: optionalString(input.targetHost),
|
|
1245
|
+
requiredCapabilities: stringArray(input.requiredCapabilities),
|
|
1246
|
+
requesterHost: optionalString(input.requesterHost),
|
|
1247
|
+
requesterThreadId: optionalString(input.requesterThreadId),
|
|
1248
|
+
threadId: optionalString(input.threadId),
|
|
1249
|
+
cwd: optionalString(input.cwd),
|
|
1250
|
+
model: optionalString(input.model),
|
|
1251
|
+
serviceTier: optionalString(input.serviceTier),
|
|
1252
|
+
effort: reasoningEffortValue(input.effort, "local handoff effort"),
|
|
1253
|
+
sandbox: sandboxValue(input.sandbox, "local handoff sandbox"),
|
|
1254
|
+
approvalPolicy: approvalPolicyValue(input.approvalPolicy, "local handoff approvalPolicy"),
|
|
1255
|
+
permissions: optionalString(input.permissions),
|
|
1256
|
+
responsesapiClientMetadata: stringRecord(input.responsesapiClientMetadata),
|
|
1257
|
+
outputSchema: input.outputSchema,
|
|
1258
|
+
afterIntentId: optionalString(input.afterIntentId),
|
|
1259
|
+
afterStatus,
|
|
1260
|
+
createdBy: optionalString(input.createdBy),
|
|
1261
|
+
reason: optionalString(input.reason),
|
|
1262
|
+
source: recordOrUndefined(input.source),
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
function parseDeferredRunDependencies(value) {
|
|
1266
|
+
if (!Array.isArray(value)) {
|
|
1267
|
+
return undefined;
|
|
1268
|
+
}
|
|
1269
|
+
const dependencies = value.map(parseDeferredRunDependency);
|
|
1270
|
+
return dependencies.length > 0 ? dependencies : undefined;
|
|
1271
|
+
}
|
|
1272
|
+
function parseDeferredRunDependency(value) {
|
|
1273
|
+
const input = record(value);
|
|
1274
|
+
const kind = requiredString(input.kind, "deferred run dependency kind");
|
|
1275
|
+
if (kind !== "deferred-run") {
|
|
1276
|
+
throw new Error(`Invalid deferred run dependency kind: ${kind}`);
|
|
1277
|
+
}
|
|
1278
|
+
return compactUndefined({
|
|
1279
|
+
kind: "deferred-run",
|
|
1280
|
+
intentId: requiredString(input.intentId, "deferred run dependency intentId"),
|
|
1281
|
+
status: deferredDependencyStatusValue(input.status, "deferred run dependency status"),
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
function parseDeferredRunRetryParams(value) {
|
|
1285
|
+
const input = record(value);
|
|
1286
|
+
const runAt = optionalString(input.runAt);
|
|
1287
|
+
if (runAt && Number.isNaN(Date.parse(runAt))) {
|
|
1288
|
+
throw new Error(`Deferred run retry runAt must be an ISO-compatible date: ${runAt}`);
|
|
1289
|
+
}
|
|
1290
|
+
return compactUndefined({
|
|
1291
|
+
id: optionalString(input.id),
|
|
1292
|
+
runAt,
|
|
1293
|
+
createdBy: optionalString(input.createdBy),
|
|
1294
|
+
reason: optionalString(input.reason),
|
|
1295
|
+
source: recordOrUndefined(input.source),
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
function retryDeferredRunSource(originalIntent, source) {
|
|
1299
|
+
const originalSource = recordOrUndefined(originalIntent.source) ?? {};
|
|
1300
|
+
return compactUndefined({
|
|
1301
|
+
...originalSource,
|
|
1302
|
+
kind: optionalString(originalSource.kind) ?? "deferred-retry",
|
|
1303
|
+
retry: compactUndefined({
|
|
1304
|
+
kind: "deferred-retry",
|
|
1305
|
+
originalIntentId: originalIntent.id,
|
|
1306
|
+
originalStatus: originalIntent.status,
|
|
1307
|
+
originalRunAt: originalIntent.runAt,
|
|
1308
|
+
originalUpdatedAt: originalIntent.updatedAt,
|
|
1309
|
+
details: source,
|
|
1310
|
+
}),
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
function promptQueueSource(input, after) {
|
|
1314
|
+
return compactUndefined({
|
|
1315
|
+
kind: "prompt-queue",
|
|
1316
|
+
queue: input.queue ?? "default",
|
|
1317
|
+
title: input.title,
|
|
1318
|
+
labels: input.labels,
|
|
1319
|
+
after,
|
|
1320
|
+
details: input.source,
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
function localHandoffSource(input, after) {
|
|
1324
|
+
const requester = compactUndefined({
|
|
1325
|
+
host: input.requesterHost,
|
|
1326
|
+
threadId: input.requesterThreadId,
|
|
1327
|
+
});
|
|
1328
|
+
return compactUndefined({
|
|
1329
|
+
kind: "local-handoff",
|
|
1330
|
+
queue: input.queue ?? "local",
|
|
1331
|
+
title: input.title,
|
|
1332
|
+
labels: input.labels,
|
|
1333
|
+
targetHost: input.targetHost ?? "local-controller",
|
|
1334
|
+
requiredCapabilities: input.requiredCapabilities,
|
|
1335
|
+
requester: Object.keys(requester).length > 0 ? requester : undefined,
|
|
1336
|
+
after,
|
|
1337
|
+
details: input.source,
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
function isPromptQueueIntent(intent, queue) {
|
|
1341
|
+
if (intent.target.kind !== "turn") {
|
|
1342
|
+
return false;
|
|
1343
|
+
}
|
|
1344
|
+
const source = recordOrUndefined(intent.source);
|
|
1345
|
+
if (source?.kind !== "prompt-queue") {
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1348
|
+
return !queue || source.queue === queue;
|
|
1349
|
+
}
|
|
1350
|
+
function isLocalHandoffIntent(intent, options = {}) {
|
|
1351
|
+
if (intent.target.kind !== "turn") {
|
|
1352
|
+
return false;
|
|
1353
|
+
}
|
|
1354
|
+
const source = recordOrUndefined(intent.source);
|
|
1355
|
+
if (source?.kind !== "local-handoff") {
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
if (options.queue && source.queue !== options.queue) {
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
const targetHost = optionalString(source.targetHost) ?? "local-controller";
|
|
1362
|
+
if (options.targetHost && targetHost !== options.targetHost) {
|
|
1363
|
+
return false;
|
|
1364
|
+
}
|
|
1365
|
+
if (options.hostId && targetHost !== "local-controller" && targetHost !== options.hostId) {
|
|
1366
|
+
return false;
|
|
1367
|
+
}
|
|
1368
|
+
if (options.capabilities) {
|
|
1369
|
+
const requiredCapabilities = stringArray(source.requiredCapabilities) ?? [];
|
|
1370
|
+
const availableCapabilities = new Set(options.capabilities);
|
|
1371
|
+
return requiredCapabilities.every((capability) => availableCapabilities.has(capability));
|
|
1372
|
+
}
|
|
1373
|
+
return true;
|
|
1374
|
+
}
|
|
977
1375
|
function parseDeferredRunTarget(value) {
|
|
978
1376
|
const target = record(value);
|
|
979
1377
|
const kind = requiredString(target.kind, "deferred run target kind");
|
|
@@ -1005,6 +1403,7 @@ function parseDeferredRunTarget(value) {
|
|
|
1005
1403
|
cwd: optionalString(target.cwd),
|
|
1006
1404
|
model: optionalString(target.model),
|
|
1007
1405
|
serviceTier: optionalString(target.serviceTier),
|
|
1406
|
+
effort: reasoningEffortValue(target.effort, "deferred run turn target effort"),
|
|
1008
1407
|
sandbox: sandboxValue(target.sandbox, "deferred run turn target sandbox"),
|
|
1009
1408
|
approvalPolicy: approvalPolicyValue(target.approvalPolicy, "deferred run turn target approvalPolicy"),
|
|
1010
1409
|
permissions: optionalString(target.permissions),
|
|
@@ -1027,6 +1426,7 @@ function normalizeDeferredRunIntent(value) {
|
|
|
1027
1426
|
createdBy: optionalString(input.createdBy),
|
|
1028
1427
|
reason: optionalString(input.reason),
|
|
1029
1428
|
source: recordOrUndefined(input.source),
|
|
1429
|
+
dependsOn: parseDeferredRunDependencies(input.dependsOn),
|
|
1030
1430
|
attemptIds: Array.isArray(input.attemptIds)
|
|
1031
1431
|
? input.attemptIds.filter((entry) => typeof entry === "string")
|
|
1032
1432
|
: [],
|
|
@@ -1110,15 +1510,44 @@ function hasScheduledIntentForDate(taskId, intents, now) {
|
|
|
1110
1510
|
intent.source?.kind === "workspace-task-schedule" &&
|
|
1111
1511
|
intent.source.date === now.toISOString().slice(0, 10)));
|
|
1112
1512
|
}
|
|
1113
|
-
function isDeferredIntentDue(intent, now) {
|
|
1513
|
+
async function isDeferredIntentDue(context, intent, now) {
|
|
1114
1514
|
if (intent.status === "pending") {
|
|
1115
|
-
return intent.runAt <= now.toISOString()
|
|
1515
|
+
return intent.runAt <= now.toISOString() &&
|
|
1516
|
+
await areDeferredRunDependenciesSatisfied(context, intent.dependsOn);
|
|
1116
1517
|
}
|
|
1117
1518
|
if (intent.status === "running" && intent.lease?.expiresAt) {
|
|
1118
1519
|
return intent.lease.expiresAt <= now.toISOString();
|
|
1119
1520
|
}
|
|
1120
1521
|
return false;
|
|
1121
1522
|
}
|
|
1523
|
+
async function areDeferredRunDependenciesSatisfied(context, dependencies) {
|
|
1524
|
+
if (!dependencies || dependencies.length === 0) {
|
|
1525
|
+
return true;
|
|
1526
|
+
}
|
|
1527
|
+
for (const dependency of dependencies) {
|
|
1528
|
+
if (dependency.kind !== "deferred-run") {
|
|
1529
|
+
return false;
|
|
1530
|
+
}
|
|
1531
|
+
let intent;
|
|
1532
|
+
try {
|
|
1533
|
+
intent = await readDeferredRunIntent(context, dependency.intentId);
|
|
1534
|
+
}
|
|
1535
|
+
catch {
|
|
1536
|
+
return false;
|
|
1537
|
+
}
|
|
1538
|
+
const status = dependency.status ?? "completed";
|
|
1539
|
+
if (status === "terminal") {
|
|
1540
|
+
if (!isTerminalDeferredRunStatus(intent.status)) {
|
|
1541
|
+
return false;
|
|
1542
|
+
}
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
if (intent.status !== status) {
|
|
1546
|
+
return false;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return true;
|
|
1550
|
+
}
|
|
1122
1551
|
function isTerminalDeferredRunStatus(status) {
|
|
1123
1552
|
return status === "completed" || status === "failed" || status === "canceled";
|
|
1124
1553
|
}
|
|
@@ -1395,8 +1824,8 @@ function deferredClaimPath(context, intentId) {
|
|
|
1395
1824
|
function deferredCollectCursorPath(context, cursor) {
|
|
1396
1825
|
return path.join(deferredCollectCursorDir(context), `${safeFileSegment(cursor)}.json`);
|
|
1397
1826
|
}
|
|
1398
|
-
function deferredCollectCursorName(value) {
|
|
1399
|
-
const cursor = value?.trim() ||
|
|
1827
|
+
function deferredCollectCursorName(value, defaultCursor = "default") {
|
|
1828
|
+
const cursor = value?.trim() || defaultCursor;
|
|
1400
1829
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(cursor)) {
|
|
1401
1830
|
throw new Error(`Invalid deferred collect cursor: ${cursor}`);
|
|
1402
1831
|
}
|
|
@@ -1405,6 +1834,9 @@ function deferredCollectCursorName(value) {
|
|
|
1405
1834
|
function deferredRunId(createdAt) {
|
|
1406
1835
|
return `deferred-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
1407
1836
|
}
|
|
1837
|
+
function deferredRetryRunId(originalIntentId, createdAt) {
|
|
1838
|
+
return `retry-${safeFileSegment(originalIntentId).slice(0, 40)}-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
1839
|
+
}
|
|
1408
1840
|
function deferredAttemptId(intentId, startedAt) {
|
|
1409
1841
|
return `${startedAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}-${safeFileSegment(intentId).slice(0, 48)}`;
|
|
1410
1842
|
}
|
|
@@ -1664,6 +2096,13 @@ function stringRecord(value) {
|
|
|
1664
2096
|
const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
|
|
1665
2097
|
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
1666
2098
|
}
|
|
2099
|
+
function stringArray(value) {
|
|
2100
|
+
if (!Array.isArray(value)) {
|
|
2101
|
+
return undefined;
|
|
2102
|
+
}
|
|
2103
|
+
const entries = value.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
2104
|
+
return entries.length > 0 ? entries : undefined;
|
|
2105
|
+
}
|
|
1667
2106
|
function sandboxValue(value, label) {
|
|
1668
2107
|
if (value === "danger-full-access" ||
|
|
1669
2108
|
value === "read-only" ||
|
|
@@ -1687,6 +2126,32 @@ function approvalPolicyValue(value, label) {
|
|
|
1687
2126
|
}
|
|
1688
2127
|
return undefined;
|
|
1689
2128
|
}
|
|
2129
|
+
function reasoningEffortValue(value, label) {
|
|
2130
|
+
if (value === "none" ||
|
|
2131
|
+
value === "minimal" ||
|
|
2132
|
+
value === "low" ||
|
|
2133
|
+
value === "medium" ||
|
|
2134
|
+
value === "high" ||
|
|
2135
|
+
value === "xhigh") {
|
|
2136
|
+
return value;
|
|
2137
|
+
}
|
|
2138
|
+
if (value !== undefined) {
|
|
2139
|
+
throw new Error(`${label} must be none, minimal, low, medium, high, or xhigh`);
|
|
2140
|
+
}
|
|
2141
|
+
return undefined;
|
|
2142
|
+
}
|
|
2143
|
+
function deferredDependencyStatusValue(value, label) {
|
|
2144
|
+
if (value === "completed" ||
|
|
2145
|
+
value === "failed" ||
|
|
2146
|
+
value === "canceled" ||
|
|
2147
|
+
value === "terminal") {
|
|
2148
|
+
return value;
|
|
2149
|
+
}
|
|
2150
|
+
if (value !== undefined) {
|
|
2151
|
+
throw new Error(`${label} must be completed, failed, canceled, or terminal`);
|
|
2152
|
+
}
|
|
2153
|
+
return undefined;
|
|
2154
|
+
}
|
|
1690
2155
|
function deferredRunStatus(value) {
|
|
1691
2156
|
if (value === "pending" ||
|
|
1692
2157
|
value === "running" ||
|