agent-conveyor 0.1.8 → 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 +20 -4
- package/dist/cli/typescript-runtime.js +290 -7
- 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/docs/manager-recipes.md +36 -6
- package/package.json +1 -1
- package/skills/manage-codex-workers/SKILL.md +41 -24
package/README.md
CHANGED
|
@@ -379,6 +379,21 @@ tmux attach -t codex-live-test
|
|
|
379
379
|
Codex app thread metadata is normally supplied after a Codex app manager has
|
|
380
380
|
used `create_thread` and `set_thread_title`; terminal-only users can omit it
|
|
381
381
|
and still use the manual no-tmux handoff.
|
|
382
|
+
- `app-heartbeat TASK --role manager|worker [--dispatcher-id ID]
|
|
383
|
+
[--stale-after N] [--json]` — Record a Codex app/no-tmux manager or worker
|
|
384
|
+
heartbeat for the active binding and return that role's next poll commands.
|
|
385
|
+
Use this as the recurring heartbeat command for pull-required app sessions.
|
|
386
|
+
- `app-loop-status TASK [--dispatcher-id ID] [--stale-after N] [--json]` —
|
|
387
|
+
Summarize manager lease, worker lease, Dispatch heartbeat, and next required
|
|
388
|
+
actions for an app-native Conveyor loop.
|
|
389
|
+
- `app-wakeup-plan TASK [--dispatcher-id ID] [--stale-after N] [--json]` —
|
|
390
|
+
Emit exact manager and worker thread wake prompts, including app thread ids
|
|
391
|
+
and titles when present, for an operator or Codex app automation to send.
|
|
392
|
+
- `app-wakeup-dispatch TASK [--dispatcher-id ID] [--stale-after N] [--json]` —
|
|
393
|
+
Prepare adapter-ready Codex app wake actions and record a telemetry receipt
|
|
394
|
+
for prepared, skipped, and blocked role wakeups. This command does not send
|
|
395
|
+
`send_message_to_thread`; the Codex app/operator layer sends prompts whose
|
|
396
|
+
actions report `send_ready=true`.
|
|
382
397
|
- `discover [QUERY] [--all] [--limit N]` / `search [QUERY]` — Search tasks,
|
|
383
398
|
registered sessions, active bindings, and recent telemetry in one JSON result.
|
|
384
399
|
Use this for conversational setup when a manager or Codex session needs to
|
|
@@ -1017,10 +1032,11 @@ Current dispatch state:
|
|
|
1017
1032
|
in `routed_notifications`, and threaded with `correlation_id`.
|
|
1018
1033
|
- The session inbox is the same `routed_notifications` stream addressed by
|
|
1019
1034
|
`target_session_id`: tmux push is optional transport. Codex app-based sessions
|
|
1020
|
-
should
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1035
|
+
should run `app-heartbeat` as their recurring heartbeat, using the direct
|
|
1036
|
+
`communication.poll_command` only as a fallback or when returned in heartbeat
|
|
1037
|
+
guidance. For disposable Ralph loops, use the generated `worker_handoff`
|
|
1038
|
+
prompt so the worker keeps polling until no inbox item remains or the loop
|
|
1039
|
+
reaches `max_iterations`. For no-tmux Codex app sessions, treat
|
|
1024
1040
|
`communication.requires_polling=true` as requiring a heartbeat/wake layer:
|
|
1025
1041
|
a delivered pull inbox item does not by itself wake an idle app thread. Do
|
|
1026
1042
|
not delete or pause heartbeats because an inbox poll is idle. A terminal
|
|
@@ -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,
|
|
@@ -1469,7 +1483,14 @@ function parseRuntimeArgs(args, env) {
|
|
|
1469
1483
|
index += 1;
|
|
1470
1484
|
}
|
|
1471
1485
|
else if (arg === "--dispatcher-id") {
|
|
1472
|
-
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") {
|
|
1473
1494
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --dispatcher-id", explicit, flags, task };
|
|
1474
1495
|
}
|
|
1475
1496
|
const value = valueAfter(queue, index, arg);
|
|
@@ -2402,6 +2423,21 @@ function parseRuntimeArgs(args, env) {
|
|
|
2402
2423
|
flags.statusStaleSeconds = value;
|
|
2403
2424
|
index += 1;
|
|
2404
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
|
+
}
|
|
2405
2441
|
else if (arg === "--terminal-stale-seconds") {
|
|
2406
2442
|
if (command !== "idle-check") {
|
|
2407
2443
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --terminal-stale-seconds", explicit, flags, task };
|
|
@@ -3347,6 +3383,233 @@ function runLoopStatusCommand(parsed, options) {
|
|
|
3347
3383
|
database.close();
|
|
3348
3384
|
}
|
|
3349
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
|
+
}
|
|
3350
3613
|
function runQaPlanCommand(parsed) {
|
|
3351
3614
|
const unsupported = unsupportedLoopCommandOptions(parsed, {
|
|
3352
3615
|
allowedFlags: new Set(["json", "subtype"]),
|
|
@@ -13623,6 +13886,10 @@ function isDefaultRuntimeCommand(command) {
|
|
|
13623
13886
|
|| command === "runs"
|
|
13624
13887
|
|| command === "loop-evidence"
|
|
13625
13888
|
|| command === "loop-status"
|
|
13889
|
+
|| command === "app-heartbeat"
|
|
13890
|
+
|| command === "app-loop-status"
|
|
13891
|
+
|| command === "app-wakeup-plan"
|
|
13892
|
+
|| command === "app-wakeup-dispatch"
|
|
13626
13893
|
|| command === "loop-templates"
|
|
13627
13894
|
|| command === "loop-triggers"
|
|
13628
13895
|
|| command === "ralph-loop-presets"
|
|
@@ -16636,6 +16903,8 @@ function renderDisposableBindingText(result) {
|
|
|
16636
16903
|
lines.push(` interval: every ${result.heartbeat_recommendations.interval_minutes} minutes`);
|
|
16637
16904
|
lines.push(` manager: ${result.heartbeat_recommendations.manager.poll_command}`);
|
|
16638
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}`);
|
|
16639
16908
|
lines.push(` teardown: ${result.heartbeat_recommendations.teardown_policy.idle_poll}`);
|
|
16640
16909
|
lines.push(` closeout: ${result.heartbeat_recommendations.teardown_policy.terminal_closeout_command}`);
|
|
16641
16910
|
}
|
|
@@ -16645,6 +16914,10 @@ function renderDisposableBindingText(result) {
|
|
|
16645
16914
|
}
|
|
16646
16915
|
function disposableHeartbeatRecommendations(taskName, dbPath) {
|
|
16647
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);
|
|
16648
16921
|
return {
|
|
16649
16922
|
applies_when: {
|
|
16650
16923
|
can_receive_push: false,
|
|
@@ -16654,6 +16927,7 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
|
|
|
16654
16927
|
},
|
|
16655
16928
|
interval_minutes: 2,
|
|
16656
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`,
|
|
16657
16931
|
teardown_policy: {
|
|
16658
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.",
|
|
16659
16933
|
owner: "manager_or_operator",
|
|
@@ -16661,13 +16935,16 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
|
|
|
16661
16935
|
terminal_closeout_command: terminalCloseoutCommand,
|
|
16662
16936
|
worker_rule: "The worker must not own loop teardown and must not remove heartbeat automation based on idle polling.",
|
|
16663
16937
|
},
|
|
16938
|
+
wakeup_plan_command: `${conveyorPollInvocation()} app-wakeup-plan ${shellQuote(taskName)} --path ${shellQuote(dbPath)} --json`,
|
|
16664
16939
|
manager: {
|
|
16940
|
+
direct_inbox_command: managerInboxCommand,
|
|
16665
16941
|
kind: "thread_heartbeat",
|
|
16666
|
-
poll_command:
|
|
16942
|
+
poll_command: managerHeartbeatCommand,
|
|
16667
16943
|
prompt: [
|
|
16668
16944
|
"Use the manage-codex-workers skill.",
|
|
16669
|
-
`
|
|
16670
|
-
`Run: ${
|
|
16945
|
+
`Run the manager app heartbeat for task ${taskName}.`,
|
|
16946
|
+
`Run: ${managerHeartbeatCommand}`,
|
|
16947
|
+
`If the heartbeat output asks for direct inbox polling, run: ${managerInboxCommand}`,
|
|
16671
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.",
|
|
16672
16949
|
"If no item is consumed, stop after a one-line idle receipt.",
|
|
16673
16950
|
"Do not delete, pause, or disable manager or worker heartbeat automation after an idle poll; an idle poll is only a quiet interval.",
|
|
@@ -16676,12 +16953,14 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
|
|
|
16676
16953
|
].join("\n"),
|
|
16677
16954
|
},
|
|
16678
16955
|
worker: {
|
|
16956
|
+
direct_inbox_command: workerInboxCommand,
|
|
16679
16957
|
kind: "thread_heartbeat",
|
|
16680
|
-
poll_command:
|
|
16958
|
+
poll_command: workerHeartbeatCommand,
|
|
16681
16959
|
prompt: [
|
|
16682
16960
|
"Use the manage-codex-workers skill.",
|
|
16683
|
-
`
|
|
16684
|
-
`Run: ${
|
|
16961
|
+
`Run the worker app heartbeat for task ${taskName}.`,
|
|
16962
|
+
`Run: ${workerHeartbeatCommand}`,
|
|
16963
|
+
`If the heartbeat output asks for direct inbox polling, run: ${workerInboxCommand}`,
|
|
16685
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.",
|
|
16686
16965
|
"If no item is consumed, stop after a one-line idle receipt.",
|
|
16687
16966
|
"Do not delete, pause, or disable worker heartbeat automation after an idle poll; the manager or operator owns terminal loop teardown.",
|
|
@@ -16689,6 +16968,9 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
|
|
|
16689
16968
|
},
|
|
16690
16969
|
};
|
|
16691
16970
|
}
|
|
16971
|
+
function disposableAppHeartbeatCommand(role, taskName, dbPath) {
|
|
16972
|
+
return `${conveyorPollInvocation()} app-heartbeat ${shellQuote(taskName)} --role ${role} --path ${shellQuote(dbPath)} --json`;
|
|
16973
|
+
}
|
|
16692
16974
|
function sessionPollCommand(role, taskName, dbPath) {
|
|
16693
16975
|
const inbox = role === "worker" ? "worker-inbox" : "manager-inbox";
|
|
16694
16976
|
const task = taskName ? shellQuote(taskName) : "<task>";
|
|
@@ -16944,6 +17226,7 @@ function emitTelemetrySync(database, options) {
|
|
|
16944
17226
|
)
|
|
16945
17227
|
values (?, ?, ?, ?, ?, ?, ?)
|
|
16946
17228
|
`).run(eventId, options.taskId ?? null, options.runId ?? null, options.actor, options.eventType, options.summary, attributesJson);
|
|
17229
|
+
return eventId;
|
|
16947
17230
|
}
|
|
16948
17231
|
function latestCodexEventsForSession(database, options) {
|
|
16949
17232
|
const clauses = ["session_id = ?"];
|