agent-conveyor 0.1.7 → 0.1.9
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 +85 -19
- package/dist/cli/typescript-runtime.js +393 -16
- package/dist/cli/typescript-runtime.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime/app-autonomy.d.ts +109 -0
- package/dist/runtime/app-autonomy.js +263 -0
- package/dist/runtime/app-autonomy.js.map +1 -0
- package/dist/runtime/codex-session.d.ts +6 -0
- package/dist/runtime/codex-session.js +27 -5
- package/dist/runtime/codex-session.js.map +1 -1
- package/dist/runtime/notifications.js +102 -1
- package/dist/runtime/notifications.js.map +1 -1
- package/dist/state/database.js +162 -24
- package/dist/state/database.js.map +1 -1
- package/dist/state/schema-v23.js +4 -2
- package/dist/state/schema-v23.js.map +1 -1
- package/dist/state/sqlite-contract.d.ts +1 -1
- package/dist/state/sqlite-contract.js +1 -1
- package/docs/landing-page.html +393 -40
- package/docs/manager-recipes.md +108 -0
- package/docs/typescript-migration/cli-contract.md +11 -14
- package/docs/typescript-migration/package-install-contract.md +1 -1
- package/docs/typescript-migration/qa-gate-matrix.md +7 -13
- package/docs/typescript-migration/sqlite-state-contract.md +2 -3
- package/docs/typescript-migration/t005-runtime-parity.md +12 -12
- package/package.json +1 -1
- package/skills/manage-codex-workers/SKILL.md +89 -17
|
@@ -5,6 +5,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
5
5
|
import { dirname, join, relative, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { taskAuditSync } from "../runtime/audit.js";
|
|
8
|
+
import { appLoopStatusSync, appWakeupDispatchPlanSync, appWakeupPlanSync, directInboxPollCommand, } from "../runtime/app-autonomy.js";
|
|
8
9
|
import { classifyBusyWait, classifyStartupOutput } from "../runtime/classify.js";
|
|
9
10
|
import { exportTaskSync } from "../runtime/export.js";
|
|
10
11
|
import { ingestSessionSync } from "../runtime/ingest.js";
|
|
@@ -118,6 +119,18 @@ export function runTypescriptRuntimeCommand(options) {
|
|
|
118
119
|
if (parsed.command === "loop-status") {
|
|
119
120
|
return runLoopStatusCommand(parsed, options);
|
|
120
121
|
}
|
|
122
|
+
if (parsed.command === "app-heartbeat") {
|
|
123
|
+
return runAppHeartbeatCommand(parsed, options);
|
|
124
|
+
}
|
|
125
|
+
if (parsed.command === "app-loop-status") {
|
|
126
|
+
return runAppLoopStatusCommand(parsed, options);
|
|
127
|
+
}
|
|
128
|
+
if (parsed.command === "app-wakeup-plan") {
|
|
129
|
+
return runAppWakeupPlanCommand(parsed, options);
|
|
130
|
+
}
|
|
131
|
+
if (parsed.command === "app-wakeup-dispatch") {
|
|
132
|
+
return runAppWakeupDispatchCommand(parsed, options);
|
|
133
|
+
}
|
|
121
134
|
if (parsed.command === "qa-plan") {
|
|
122
135
|
return runQaPlanCommand(parsed);
|
|
123
136
|
}
|
|
@@ -417,6 +430,7 @@ function parseRuntimeArgs(args, env) {
|
|
|
417
430
|
includeLegacy: false,
|
|
418
431
|
help: false,
|
|
419
432
|
redactIdentityToken: false,
|
|
433
|
+
appStaleAfterSeconds: 180,
|
|
420
434
|
active: false,
|
|
421
435
|
add: false,
|
|
422
436
|
apply: false,
|
|
@@ -425,6 +439,10 @@ function parseRuntimeArgs(args, env) {
|
|
|
425
439
|
candidate: null,
|
|
426
440
|
check: null,
|
|
427
441
|
classifyPrompt: null,
|
|
442
|
+
workerCodexAppThreadId: null,
|
|
443
|
+
workerCodexAppThreadTitle: null,
|
|
444
|
+
managerCodexAppThreadId: null,
|
|
445
|
+
managerCodexAppThreadTitle: null,
|
|
428
446
|
codexSession: null,
|
|
429
447
|
create: null,
|
|
430
448
|
createRun: null,
|
|
@@ -1465,7 +1483,14 @@ function parseRuntimeArgs(args, env) {
|
|
|
1465
1483
|
index += 1;
|
|
1466
1484
|
}
|
|
1467
1485
|
else if (arg === "--dispatcher-id") {
|
|
1468
|
-
if (command !== "pair"
|
|
1486
|
+
if (command !== "pair"
|
|
1487
|
+
&& command !== "dispatch"
|
|
1488
|
+
&& command !== "qa-run"
|
|
1489
|
+
&& command !== "dashboard"
|
|
1490
|
+
&& command !== "app-heartbeat"
|
|
1491
|
+
&& command !== "app-loop-status"
|
|
1492
|
+
&& command !== "app-wakeup-plan"
|
|
1493
|
+
&& command !== "app-wakeup-dispatch") {
|
|
1469
1494
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --dispatcher-id", explicit, flags, task };
|
|
1470
1495
|
}
|
|
1471
1496
|
const value = valueAfter(queue, index, arg);
|
|
@@ -1947,6 +1972,31 @@ function parseRuntimeArgs(args, env) {
|
|
|
1947
1972
|
flags.manager = value.value;
|
|
1948
1973
|
index += 1;
|
|
1949
1974
|
}
|
|
1975
|
+
else if (arg === "--worker-codex-app-thread-id"
|
|
1976
|
+
|| arg === "--worker-codex-app-thread-title"
|
|
1977
|
+
|| arg === "--manager-codex-app-thread-id"
|
|
1978
|
+
|| arg === "--manager-codex-app-thread-title") {
|
|
1979
|
+
if (command !== "create-disposable-binding") {
|
|
1980
|
+
return { command, enabled, error: `Unsupported TypeScript runtime option: ${arg}`, explicit, flags, task };
|
|
1981
|
+
}
|
|
1982
|
+
const value = valueAfter(queue, index, arg);
|
|
1983
|
+
if (value.error) {
|
|
1984
|
+
return { command, enabled, error: value.error, explicit, flags, task };
|
|
1985
|
+
}
|
|
1986
|
+
if (arg === "--worker-codex-app-thread-id") {
|
|
1987
|
+
flags.workerCodexAppThreadId = value.value;
|
|
1988
|
+
}
|
|
1989
|
+
else if (arg === "--worker-codex-app-thread-title") {
|
|
1990
|
+
flags.workerCodexAppThreadTitle = value.value;
|
|
1991
|
+
}
|
|
1992
|
+
else if (arg === "--manager-codex-app-thread-id") {
|
|
1993
|
+
flags.managerCodexAppThreadId = value.value;
|
|
1994
|
+
}
|
|
1995
|
+
else {
|
|
1996
|
+
flags.managerCodexAppThreadTitle = value.value;
|
|
1997
|
+
}
|
|
1998
|
+
index += 1;
|
|
1999
|
+
}
|
|
1950
2000
|
else if (arg === "--template") {
|
|
1951
2001
|
if (command !== "create-disposable-binding" && command !== "loop-templates") {
|
|
1952
2002
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --template", explicit, flags, task };
|
|
@@ -2373,6 +2423,21 @@ function parseRuntimeArgs(args, env) {
|
|
|
2373
2423
|
flags.statusStaleSeconds = value;
|
|
2374
2424
|
index += 1;
|
|
2375
2425
|
}
|
|
2426
|
+
else if (arg === "--stale-after") {
|
|
2427
|
+
if (command !== "app-heartbeat" && command !== "app-loop-status" && command !== "app-wakeup-plan" && command !== "app-wakeup-dispatch") {
|
|
2428
|
+
return { command, enabled, error: "Unsupported TypeScript runtime option: --stale-after", explicit, flags, task };
|
|
2429
|
+
}
|
|
2430
|
+
const parsedValue = valueAfter(queue, index, arg);
|
|
2431
|
+
if (parsedValue.error) {
|
|
2432
|
+
return { command, enabled, error: parsedValue.error, explicit, flags, task };
|
|
2433
|
+
}
|
|
2434
|
+
const value = Number(parsedValue.value);
|
|
2435
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
2436
|
+
return { command, enabled, error: "--stale-after must be a non-negative number.", explicit, flags, task };
|
|
2437
|
+
}
|
|
2438
|
+
flags.appStaleAfterSeconds = value;
|
|
2439
|
+
index += 1;
|
|
2440
|
+
}
|
|
2376
2441
|
else if (arg === "--terminal-stale-seconds") {
|
|
2377
2442
|
if (command !== "idle-check") {
|
|
2378
2443
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --terminal-stale-seconds", explicit, flags, task };
|
|
@@ -3318,6 +3383,233 @@ function runLoopStatusCommand(parsed, options) {
|
|
|
3318
3383
|
database.close();
|
|
3319
3384
|
}
|
|
3320
3385
|
}
|
|
3386
|
+
function runAppHeartbeatCommand(parsed, options) {
|
|
3387
|
+
const taskName = requireTask(parsed);
|
|
3388
|
+
if (parsed.flags.role !== "manager" && parsed.flags.role !== "worker") {
|
|
3389
|
+
return errorResult("app-heartbeat requires --role manager|worker");
|
|
3390
|
+
}
|
|
3391
|
+
const role = parsed.flags.role;
|
|
3392
|
+
const database = openRuntimeDatabase(parsed, options);
|
|
3393
|
+
try {
|
|
3394
|
+
const task = taskRowForDiagnostics(database, taskName);
|
|
3395
|
+
const session = boundAppSessionForRoleSync(database, { role, taskId: task.id });
|
|
3396
|
+
const timestamp = nowIsoSeconds(options);
|
|
3397
|
+
const dbPath = runtimeDbPath(parsed, options);
|
|
3398
|
+
database.prepare("update sessions set last_heartbeat_at = ? where id = ?").run(timestamp, session.id);
|
|
3399
|
+
emitTelemetrySync(database, {
|
|
3400
|
+
actor: role,
|
|
3401
|
+
attributes: {
|
|
3402
|
+
direct_inbox_command: directInboxPollCommand(role, task.name, dbPath),
|
|
3403
|
+
role,
|
|
3404
|
+
session_id: session.id,
|
|
3405
|
+
task: task.name,
|
|
3406
|
+
},
|
|
3407
|
+
correlation: { command: "app-heartbeat" },
|
|
3408
|
+
eventType: "app_heartbeat",
|
|
3409
|
+
severity: "info",
|
|
3410
|
+
summary: `${role} app heartbeat for ${task.name}.`,
|
|
3411
|
+
taskId: task.id,
|
|
3412
|
+
timestamp,
|
|
3413
|
+
});
|
|
3414
|
+
const status = appLoopStatusSync(database, {
|
|
3415
|
+
dbPath,
|
|
3416
|
+
dispatcherId: parsed.flags.dispatcherId ?? "dispatch-local",
|
|
3417
|
+
heartbeatStaleSeconds: parsed.flags.appStaleAfterSeconds,
|
|
3418
|
+
now: timestamp,
|
|
3419
|
+
taskName: task.name,
|
|
3420
|
+
});
|
|
3421
|
+
const roleStatus = role === "manager" ? status.manager : status.worker;
|
|
3422
|
+
const output = {
|
|
3423
|
+
heartbeat: {
|
|
3424
|
+
recorded_at: timestamp,
|
|
3425
|
+
state: "recorded",
|
|
3426
|
+
},
|
|
3427
|
+
next: {
|
|
3428
|
+
direct_inbox_command: roleStatus.direct_inbox_command,
|
|
3429
|
+
poll_command: roleStatus.poll_command,
|
|
3430
|
+
},
|
|
3431
|
+
role,
|
|
3432
|
+
status,
|
|
3433
|
+
task: { id: task.id, name: task.name },
|
|
3434
|
+
};
|
|
3435
|
+
if (parsed.flags.json) {
|
|
3436
|
+
return jsonResult(output);
|
|
3437
|
+
}
|
|
3438
|
+
return {
|
|
3439
|
+
exitCode: 0,
|
|
3440
|
+
handled: true,
|
|
3441
|
+
stdout: `${role} heartbeat recorded for ${task.name}\nNext: ${roleStatus.direct_inbox_command ?? roleStatus.poll_command ?? "(none)"}\n`,
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
finally {
|
|
3445
|
+
database.close();
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
function runAppLoopStatusCommand(parsed, options) {
|
|
3449
|
+
const taskName = requireTask(parsed);
|
|
3450
|
+
const database = openRuntimeDatabase(parsed, options);
|
|
3451
|
+
try {
|
|
3452
|
+
const status = appLoopStatusSync(database, {
|
|
3453
|
+
dbPath: runtimeDbPath(parsed, options),
|
|
3454
|
+
dispatcherId: parsed.flags.dispatcherId ?? "dispatch-local",
|
|
3455
|
+
heartbeatStaleSeconds: parsed.flags.appStaleAfterSeconds,
|
|
3456
|
+
now: nowIsoSeconds(options),
|
|
3457
|
+
taskName,
|
|
3458
|
+
});
|
|
3459
|
+
if (parsed.flags.json) {
|
|
3460
|
+
return jsonResult(status);
|
|
3461
|
+
}
|
|
3462
|
+
return {
|
|
3463
|
+
exitCode: 0,
|
|
3464
|
+
handled: true,
|
|
3465
|
+
stdout: renderAppLoopStatusText(status),
|
|
3466
|
+
};
|
|
3467
|
+
}
|
|
3468
|
+
finally {
|
|
3469
|
+
database.close();
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
function runAppWakeupPlanCommand(parsed, options) {
|
|
3473
|
+
const taskName = requireTask(parsed);
|
|
3474
|
+
const database = openRuntimeDatabase(parsed, options);
|
|
3475
|
+
try {
|
|
3476
|
+
const plan = appWakeupPlanSync(database, {
|
|
3477
|
+
dbPath: runtimeDbPath(parsed, options),
|
|
3478
|
+
dispatcherId: parsed.flags.dispatcherId ?? "dispatch-local",
|
|
3479
|
+
heartbeatStaleSeconds: parsed.flags.appStaleAfterSeconds,
|
|
3480
|
+
now: nowIsoSeconds(options),
|
|
3481
|
+
taskName,
|
|
3482
|
+
});
|
|
3483
|
+
if (parsed.flags.json) {
|
|
3484
|
+
return jsonResult(plan);
|
|
3485
|
+
}
|
|
3486
|
+
return {
|
|
3487
|
+
exitCode: 0,
|
|
3488
|
+
handled: true,
|
|
3489
|
+
stdout: renderAppWakeupPlanText(plan),
|
|
3490
|
+
};
|
|
3491
|
+
}
|
|
3492
|
+
finally {
|
|
3493
|
+
database.close();
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
function runAppWakeupDispatchCommand(parsed, options) {
|
|
3497
|
+
const taskName = requireTask(parsed);
|
|
3498
|
+
const database = openRuntimeDatabase(parsed, options);
|
|
3499
|
+
try {
|
|
3500
|
+
const timestamp = nowIsoSeconds(options);
|
|
3501
|
+
const dispatch = appWakeupDispatchPlanSync(database, {
|
|
3502
|
+
dbPath: runtimeDbPath(parsed, options),
|
|
3503
|
+
dispatcherId: parsed.flags.dispatcherId ?? "dispatch-local",
|
|
3504
|
+
heartbeatStaleSeconds: parsed.flags.appStaleAfterSeconds,
|
|
3505
|
+
now: timestamp,
|
|
3506
|
+
taskName,
|
|
3507
|
+
});
|
|
3508
|
+
const eventId = emitTelemetrySync(database, {
|
|
3509
|
+
actor: "manager",
|
|
3510
|
+
attributes: {
|
|
3511
|
+
actions: dispatch.actions.map((action) => ({
|
|
3512
|
+
blocker: action.blocker,
|
|
3513
|
+
reason: action.reason,
|
|
3514
|
+
role: action.role,
|
|
3515
|
+
send_ready: action.send_ready,
|
|
3516
|
+
status: action.status,
|
|
3517
|
+
thread_id: action.thread.id,
|
|
3518
|
+
thread_title: action.thread.title,
|
|
3519
|
+
})),
|
|
3520
|
+
dispatcher: dispatch.dispatcher,
|
|
3521
|
+
status_ok: dispatch.status.ok,
|
|
3522
|
+
summary: dispatch.summary,
|
|
3523
|
+
},
|
|
3524
|
+
correlation: {
|
|
3525
|
+
command: "app-wakeup-dispatch",
|
|
3526
|
+
dispatcher_id: dispatch.status.dispatch.dispatcher_id,
|
|
3527
|
+
},
|
|
3528
|
+
eventType: "app_wakeup_dispatch_planned",
|
|
3529
|
+
severity: dispatch.summary.blocked > 0 || dispatch.dispatcher.required ? "warning" : "info",
|
|
3530
|
+
summary: `App wakeup dispatch planned for ${dispatch.status.task.name}.`,
|
|
3531
|
+
taskId: dispatch.status.task.id,
|
|
3532
|
+
timestamp,
|
|
3533
|
+
});
|
|
3534
|
+
const output = {
|
|
3535
|
+
...dispatch,
|
|
3536
|
+
receipt: {
|
|
3537
|
+
event_id: eventId,
|
|
3538
|
+
event_type: "app_wakeup_dispatch_planned",
|
|
3539
|
+
recorded_at: timestamp,
|
|
3540
|
+
},
|
|
3541
|
+
};
|
|
3542
|
+
if (parsed.flags.json) {
|
|
3543
|
+
return jsonResult(output);
|
|
3544
|
+
}
|
|
3545
|
+
return {
|
|
3546
|
+
exitCode: 0,
|
|
3547
|
+
handled: true,
|
|
3548
|
+
stdout: renderAppWakeupDispatchText(output),
|
|
3549
|
+
};
|
|
3550
|
+
}
|
|
3551
|
+
finally {
|
|
3552
|
+
database.close();
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
function renderAppLoopStatusText(status) {
|
|
3556
|
+
const lines = [
|
|
3557
|
+
`App loop ${status.task.name}: ${status.ok ? "ok" : "attention required"}`,
|
|
3558
|
+
`Dispatch ${status.dispatch.dispatcher_id}: ${status.dispatch.state}`,
|
|
3559
|
+
`Manager ${status.manager.name ?? "(missing)"}: ${status.manager.lease.state}`,
|
|
3560
|
+
`Worker ${status.worker.name ?? "(missing)"}: ${status.worker.lease.state}`,
|
|
3561
|
+
];
|
|
3562
|
+
for (const action of status.next_actions) {
|
|
3563
|
+
lines.push(`Next: ${action.kind} - ${action.reason}`);
|
|
3564
|
+
}
|
|
3565
|
+
return `${lines.join("\n")}\n`;
|
|
3566
|
+
}
|
|
3567
|
+
function renderAppWakeupDispatchText(plan) {
|
|
3568
|
+
const lines = [
|
|
3569
|
+
`App wakeup dispatch for ${plan.status.task.name}: ${plan.status.ok ? "ok" : "attention required"}`,
|
|
3570
|
+
`Dispatch: ${plan.dispatcher.state}${plan.dispatcher.required ? ` (${plan.dispatcher.command})` : ""}`,
|
|
3571
|
+
`Receipt: ${plan.receipt.event_type} ${plan.receipt.event_id}`,
|
|
3572
|
+
];
|
|
3573
|
+
for (const action of plan.actions) {
|
|
3574
|
+
lines.push(`${action.role}: ${action.status} - ${action.reason}`);
|
|
3575
|
+
if (action.blocker) {
|
|
3576
|
+
lines.push(`Blocker: ${action.blocker}`);
|
|
3577
|
+
}
|
|
3578
|
+
if (action.send_ready && action.thread.id) {
|
|
3579
|
+
lines.push(`Thread: ${action.thread.title ?? "(untitled)"} ${action.thread.id}`);
|
|
3580
|
+
}
|
|
3581
|
+
if (action.prompt) {
|
|
3582
|
+
lines.push(action.prompt);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
return `${lines.join("\n")}\n`;
|
|
3586
|
+
}
|
|
3587
|
+
function renderAppWakeupPlanText(plan) {
|
|
3588
|
+
const lines = [
|
|
3589
|
+
`App wakeup plan for ${plan.status.task.name}: ${plan.status.ok ? "ok" : "attention required"}`,
|
|
3590
|
+
`Dispatch: ${plan.dispatcher.state}${plan.dispatcher.required ? ` (${plan.dispatcher.command})` : ""}`,
|
|
3591
|
+
];
|
|
3592
|
+
for (const wakeup of plan.wakeups) {
|
|
3593
|
+
lines.push(`Wake ${wakeup.role}${wakeup.thread.title ? ` (${wakeup.thread.title})` : ""}: ${wakeup.reason}`);
|
|
3594
|
+
lines.push(wakeup.prompt);
|
|
3595
|
+
}
|
|
3596
|
+
return `${lines.join("\n")}\n`;
|
|
3597
|
+
}
|
|
3598
|
+
function boundAppSessionForRoleSync(database, options) {
|
|
3599
|
+
const sessionJoin = options.role === "manager" ? "manager_session_id" : "worker_session_id";
|
|
3600
|
+
const row = database.prepare(`
|
|
3601
|
+
select s.id, s.name
|
|
3602
|
+
from bindings b
|
|
3603
|
+
join sessions s on s.id = b.${sessionJoin}
|
|
3604
|
+
where b.task_id = ? and b.state in ('active', 'ending') and s.role = ? and s.state = 'active'
|
|
3605
|
+
order by b.created_at desc
|
|
3606
|
+
limit 1
|
|
3607
|
+
`).get(options.taskId, options.role);
|
|
3608
|
+
if (!row) {
|
|
3609
|
+
throw new Error(`No active bound ${options.role} session for task.`);
|
|
3610
|
+
}
|
|
3611
|
+
return row;
|
|
3612
|
+
}
|
|
3321
3613
|
function runQaPlanCommand(parsed) {
|
|
3322
3614
|
const unsupported = unsupportedLoopCommandOptions(parsed, {
|
|
3323
3615
|
allowedFlags: new Set(["json", "subtype"]),
|
|
@@ -3968,10 +4260,10 @@ function qaRunBuildClearLoop(context) {
|
|
|
3968
4260
|
const artifactDir = qaArtifactDir(context, "build-clear-loop", slug, run.id);
|
|
3969
4261
|
const buildReceipt = join(artifactDir, "build-passed.json");
|
|
3970
4262
|
mkdirSync(dirname(buildReceipt), { recursive: true });
|
|
3971
|
-
writeFileSync(buildReceipt, `${JSON.stringify(sortJson({ command: "
|
|
4263
|
+
writeFileSync(buildReceipt, `${JSON.stringify(sortJson({ command: "npm test -- --runInBand", result: "pass", status: "build_passed" }), null, 2)}\n`);
|
|
3972
4264
|
qaRecordLoopEvidence(context, task, run.id, "build_passed", "qa-run-build-clear-build-passed", {
|
|
3973
4265
|
artifactPath: buildReceipt,
|
|
3974
|
-
metadata: { command: "
|
|
4266
|
+
metadata: { command: "npm test -- --runInBand", result: "Focused build/test command passed before retry." },
|
|
3975
4267
|
});
|
|
3976
4268
|
enqueueQaContinue(context, task, run.id, "qa-run-build-clear-build-only", "Run after build evidence only.");
|
|
3977
4269
|
const buildOnlyDispatch = qaDispatchContinueOnce(context, "qa-run-build-clear-build-only");
|
|
@@ -4884,19 +5176,14 @@ function dispatchWatchCommand(workerctlPath, dispatcherId, dbPath) {
|
|
|
4884
5176
|
}
|
|
4885
5177
|
function installableSkillSources() {
|
|
4886
5178
|
const root = packageRootFromRuntimeModule();
|
|
4887
|
-
const
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
.map((name) => ({ name, source: join(candidate, name) }))
|
|
4894
|
-
.filter((skill) => existsSync(join(skill.source, "SKILL.md")));
|
|
4895
|
-
if (skills.length === 2) {
|
|
4896
|
-
return skills;
|
|
4897
|
-
}
|
|
5179
|
+
const candidate = join(root, "skills");
|
|
5180
|
+
const skills = ["manage-codex-workers", "codex-review"]
|
|
5181
|
+
.map((name) => ({ name, source: join(candidate, name) }))
|
|
5182
|
+
.filter((skill) => existsSync(join(skill.source, "SKILL.md")));
|
|
5183
|
+
if (skills.length === 2) {
|
|
5184
|
+
return skills;
|
|
4898
5185
|
}
|
|
4899
|
-
throw new Error("Bundled Agent Conveyor skills not found in skills
|
|
5186
|
+
throw new Error("Bundled Agent Conveyor skills not found in skills/.");
|
|
4900
5187
|
}
|
|
4901
5188
|
function runBindCommand(parsed, options) {
|
|
4902
5189
|
const unsupported = unsupportedBindOptions(parsed);
|
|
@@ -4997,6 +5284,8 @@ function runCreateDisposableBindingCommand(parsed, options) {
|
|
|
4997
5284
|
const workerRollout = writeDisposableRollout(sessionDir, workerName, cwd);
|
|
4998
5285
|
const managerRollout = writeDisposableRollout(sessionDir, managerName, cwd);
|
|
4999
5286
|
const worker = registerSessionSync(database, {
|
|
5287
|
+
codexAppThreadId: parsed.flags.workerCodexAppThreadId,
|
|
5288
|
+
codexAppThreadTitle: parsed.flags.workerCodexAppThreadTitle,
|
|
5000
5289
|
codexSessionPath: workerRollout.path,
|
|
5001
5290
|
cwd,
|
|
5002
5291
|
name: workerName,
|
|
@@ -5005,6 +5294,8 @@ function runCreateDisposableBindingCommand(parsed, options) {
|
|
|
5005
5294
|
tmuxSession: null,
|
|
5006
5295
|
});
|
|
5007
5296
|
const manager = registerSessionSync(database, {
|
|
5297
|
+
codexAppThreadId: parsed.flags.managerCodexAppThreadId,
|
|
5298
|
+
codexAppThreadTitle: parsed.flags.managerCodexAppThreadTitle,
|
|
5008
5299
|
codexSessionPath: managerRollout.path,
|
|
5009
5300
|
cwd,
|
|
5010
5301
|
name: managerName,
|
|
@@ -5043,6 +5334,8 @@ function runCreateDisposableBindingCommand(parsed, options) {
|
|
|
5043
5334
|
db_path: dbPath,
|
|
5044
5335
|
manager: {
|
|
5045
5336
|
communication: disposableSessionCommunication("manager", task.name, dbPath),
|
|
5337
|
+
codex_app_thread_id: manager.codex_app_thread_id,
|
|
5338
|
+
codex_app_thread_title: manager.codex_app_thread_title,
|
|
5046
5339
|
id: manager.session_id,
|
|
5047
5340
|
name: managerName,
|
|
5048
5341
|
rollout_path: managerRollout.path,
|
|
@@ -5068,11 +5361,14 @@ function runCreateDisposableBindingCommand(parsed, options) {
|
|
|
5068
5361
|
},
|
|
5069
5362
|
worker: {
|
|
5070
5363
|
communication: disposableSessionCommunication("worker", task.name, dbPath),
|
|
5364
|
+
codex_app_thread_id: worker.codex_app_thread_id,
|
|
5365
|
+
codex_app_thread_title: worker.codex_app_thread_title,
|
|
5071
5366
|
id: worker.session_id,
|
|
5072
5367
|
name: workerName,
|
|
5073
5368
|
rollout_path: workerRollout.path,
|
|
5074
5369
|
tmux_session: null,
|
|
5075
5370
|
},
|
|
5371
|
+
heartbeat_recommendations: disposableHeartbeatRecommendations(task.name, dbPath),
|
|
5076
5372
|
worker_handoff: disposableWorkerHandoff(task.name, run?.name ?? null, dbPath),
|
|
5077
5373
|
};
|
|
5078
5374
|
if (parsed.flags.json) {
|
|
@@ -13590,6 +13886,10 @@ function isDefaultRuntimeCommand(command) {
|
|
|
13590
13886
|
|| command === "runs"
|
|
13591
13887
|
|| command === "loop-evidence"
|
|
13592
13888
|
|| command === "loop-status"
|
|
13889
|
+
|| command === "app-heartbeat"
|
|
13890
|
+
|| command === "app-loop-status"
|
|
13891
|
+
|| command === "app-wakeup-plan"
|
|
13892
|
+
|| command === "app-wakeup-dispatch"
|
|
13593
13893
|
|| command === "loop-templates"
|
|
13594
13894
|
|| command === "loop-triggers"
|
|
13595
13895
|
|| command === "ralph-loop-presets"
|
|
@@ -16581,6 +16881,9 @@ function disposableWorkerHandoff(taskName, runName, dbPath) {
|
|
|
16581
16881
|
`You are the worker for task ${taskName}${loopClause}.`,
|
|
16582
16882
|
"Keep polling your Conveyor worker inbox until there are no items left or the loop reaches max_iterations. Consume the next item now, treat each consumed item as the manager's next instruction, complete the requested work, and report changed files, exact commands run, evidence, and any residual risk.",
|
|
16583
16883
|
"",
|
|
16884
|
+
"Because this is a pull-required Codex app/no-tmux session, autonomous operation requires a heartbeat/wake layer that repeats this worker inbox poll while the thread is idle. If no heartbeat automation is available, report the loop as manual-poll only.",
|
|
16885
|
+
"Do not delete, pause, or disable heartbeat automation just because an inbox poll is idle; the manager or operator owns terminal loop teardown.",
|
|
16886
|
+
"",
|
|
16584
16887
|
`Run: ${sessionPollCommand("worker", taskName, dbPath)}`,
|
|
16585
16888
|
].join("\n");
|
|
16586
16889
|
}
|
|
@@ -16595,14 +16898,87 @@ function renderDisposableBindingText(result) {
|
|
|
16595
16898
|
}
|
|
16596
16899
|
lines.push("Replay commands:");
|
|
16597
16900
|
lines.push(...result.replay_commands.map((command) => ` ${command}`));
|
|
16901
|
+
if (result.heartbeat_recommendations) {
|
|
16902
|
+
lines.push("Heartbeat recommendations:");
|
|
16903
|
+
lines.push(` interval: every ${result.heartbeat_recommendations.interval_minutes} minutes`);
|
|
16904
|
+
lines.push(` manager: ${result.heartbeat_recommendations.manager.poll_command}`);
|
|
16905
|
+
lines.push(` worker: ${result.heartbeat_recommendations.worker.poll_command}`);
|
|
16906
|
+
lines.push(` status: ${result.heartbeat_recommendations.status_command}`);
|
|
16907
|
+
lines.push(` wakeup plan: ${result.heartbeat_recommendations.wakeup_plan_command}`);
|
|
16908
|
+
lines.push(` teardown: ${result.heartbeat_recommendations.teardown_policy.idle_poll}`);
|
|
16909
|
+
lines.push(` closeout: ${result.heartbeat_recommendations.teardown_policy.terminal_closeout_command}`);
|
|
16910
|
+
}
|
|
16598
16911
|
lines.push("Worker handoff:");
|
|
16599
16912
|
lines.push(result.worker_handoff);
|
|
16600
16913
|
return `${lines.join("\n")}\n`;
|
|
16601
16914
|
}
|
|
16915
|
+
function disposableHeartbeatRecommendations(taskName, dbPath) {
|
|
16916
|
+
const terminalCloseoutCommand = `${conveyorPollInvocation()} finish-task ${shellQuote(taskName)} --reason ${shellQuote("Verified terminal closeout")} --require-criteria-audit --path ${shellQuote(dbPath)}`;
|
|
16917
|
+
const managerHeartbeatCommand = disposableAppHeartbeatCommand("manager", taskName, dbPath);
|
|
16918
|
+
const workerHeartbeatCommand = disposableAppHeartbeatCommand("worker", taskName, dbPath);
|
|
16919
|
+
const managerInboxCommand = sessionPollCommand("manager", taskName, dbPath);
|
|
16920
|
+
const workerInboxCommand = sessionPollCommand("worker", taskName, dbPath);
|
|
16921
|
+
return {
|
|
16922
|
+
applies_when: {
|
|
16923
|
+
can_receive_push: false,
|
|
16924
|
+
delivery_mode: "pull_required",
|
|
16925
|
+
receive_style: "pull",
|
|
16926
|
+
session_kind: "codex_app",
|
|
16927
|
+
},
|
|
16928
|
+
interval_minutes: 2,
|
|
16929
|
+
note: "Dispatch can deliver pull-required inbox items, but Codex app/no-tmux sessions still need a heartbeat or operator wake-up to poll while idle.",
|
|
16930
|
+
status_command: `${conveyorPollInvocation()} app-loop-status ${shellQuote(taskName)} --path ${shellQuote(dbPath)} --json`,
|
|
16931
|
+
teardown_policy: {
|
|
16932
|
+
idle_poll: "Never delete, pause, or disable a manager or worker heartbeat because an inbox poll returned no item; that is only a quiet poll interval.",
|
|
16933
|
+
owner: "manager_or_operator",
|
|
16934
|
+
terminal_closeout: "Only the manager or operator should tear down heartbeats, and only after a terminal manager decision plus verified task closeout, or after explicit operator instruction.",
|
|
16935
|
+
terminal_closeout_command: terminalCloseoutCommand,
|
|
16936
|
+
worker_rule: "The worker must not own loop teardown and must not remove heartbeat automation based on idle polling.",
|
|
16937
|
+
},
|
|
16938
|
+
wakeup_plan_command: `${conveyorPollInvocation()} app-wakeup-plan ${shellQuote(taskName)} --path ${shellQuote(dbPath)} --json`,
|
|
16939
|
+
manager: {
|
|
16940
|
+
direct_inbox_command: managerInboxCommand,
|
|
16941
|
+
kind: "thread_heartbeat",
|
|
16942
|
+
poll_command: managerHeartbeatCommand,
|
|
16943
|
+
prompt: [
|
|
16944
|
+
"Use the manage-codex-workers skill.",
|
|
16945
|
+
`Run the manager app heartbeat for task ${taskName}.`,
|
|
16946
|
+
`Run: ${managerHeartbeatCommand}`,
|
|
16947
|
+
`If the heartbeat output asks for direct inbox polling, run: ${managerInboxCommand}`,
|
|
16948
|
+
"If an item is consumed, execute only that manager instruction, verify worker claims before recording conclusions, update Conveyor state as appropriate, and produce exactly one next worker task.",
|
|
16949
|
+
"If no item is consumed, stop after a one-line idle receipt.",
|
|
16950
|
+
"Do not delete, pause, or disable manager or worker heartbeat automation after an idle poll; an idle poll is only a quiet interval.",
|
|
16951
|
+
`If all accepted criteria are satisfied, deferred, or rejected and there is no next worker task, record the terminal manager decision, run or report the result of: ${terminalCloseoutCommand}`,
|
|
16952
|
+
"After verified task closeout, explicitly report heartbeat teardown status; if the task remains managed/active, report that as a control-plane blocker instead of calling the loop complete.",
|
|
16953
|
+
].join("\n"),
|
|
16954
|
+
},
|
|
16955
|
+
worker: {
|
|
16956
|
+
direct_inbox_command: workerInboxCommand,
|
|
16957
|
+
kind: "thread_heartbeat",
|
|
16958
|
+
poll_command: workerHeartbeatCommand,
|
|
16959
|
+
prompt: [
|
|
16960
|
+
"Use the manage-codex-workers skill.",
|
|
16961
|
+
`Run the worker app heartbeat for task ${taskName}.`,
|
|
16962
|
+
`Run: ${workerHeartbeatCommand}`,
|
|
16963
|
+
`If the heartbeat output asks for direct inbox polling, run: ${workerInboxCommand}`,
|
|
16964
|
+
"If an item is consumed, execute only that single worker instruction and return exact commands, compact evidence, blockers/residual risk, and exactly one next recommended worker task.",
|
|
16965
|
+
"If no item is consumed, stop after a one-line idle receipt.",
|
|
16966
|
+
"Do not delete, pause, or disable worker heartbeat automation after an idle poll; the manager or operator owns terminal loop teardown.",
|
|
16967
|
+
].join("\n"),
|
|
16968
|
+
},
|
|
16969
|
+
};
|
|
16970
|
+
}
|
|
16971
|
+
function disposableAppHeartbeatCommand(role, taskName, dbPath) {
|
|
16972
|
+
return `${conveyorPollInvocation()} app-heartbeat ${shellQuote(taskName)} --role ${role} --path ${shellQuote(dbPath)} --json`;
|
|
16973
|
+
}
|
|
16602
16974
|
function sessionPollCommand(role, taskName, dbPath) {
|
|
16603
16975
|
const inbox = role === "worker" ? "worker-inbox" : "manager-inbox";
|
|
16604
16976
|
const task = taskName ? shellQuote(taskName) : "<task>";
|
|
16605
|
-
return
|
|
16977
|
+
return `${conveyorPollInvocation()} ${inbox} ${task} --consume-next --wait --timeout 60 --path ${shellQuote(dbPath)} --json`;
|
|
16978
|
+
}
|
|
16979
|
+
function conveyorPollInvocation() {
|
|
16980
|
+
const binDir = join(packageRootFromRuntimeModule(), "bin");
|
|
16981
|
+
return pathIsExecutable(join(binDir, "conveyor")) ? `PATH=${shellQuote(binDir)}:$PATH conveyor` : "conveyor";
|
|
16606
16982
|
}
|
|
16607
16983
|
function resolveCodexStartupOptions(options) {
|
|
16608
16984
|
const defaults = {
|
|
@@ -16850,6 +17226,7 @@ function emitTelemetrySync(database, options) {
|
|
|
16850
17226
|
)
|
|
16851
17227
|
values (?, ?, ?, ?, ?, ?, ?)
|
|
16852
17228
|
`).run(eventId, options.taskId ?? null, options.runId ?? null, options.actor, options.eventType, options.summary, attributesJson);
|
|
17229
|
+
return eventId;
|
|
16853
17230
|
}
|
|
16854
17231
|
function latestCodexEventsForSession(database, options) {
|
|
16855
17232
|
const clauses = ["session_id = ?"];
|