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.
@@ -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" && command !== "dispatch" && command !== "qa-run" && command !== "dashboard") {
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: "scripts/run-unittests-isolated -k build_clear_loop", result: "pass", status: "build_passed" }), null, 2)}\n`);
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: "scripts/run-unittests-isolated -k build_clear_loop", result: "Focused build/test command passed before retry." },
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 candidates = [
4888
- join(root, "skills"),
4889
- join(root, "workerctl", "assets", "skills"),
4890
- ];
4891
- for (const candidate of candidates) {
4892
- const skills = ["manage-codex-workers", "codex-review"]
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/ or workerctl/assets/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 `conveyor ${inbox} ${task} --consume-next --wait --timeout 60 --path ${shellQuote(dbPath)} --json`;
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 = ?"];