agent-conveyor 0.1.9 → 0.1.11

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 CHANGED
@@ -372,9 +372,13 @@ tmux attach -t codex-live-test
372
372
  `heartbeat_recommendations` with role-specific poll prompts; Dispatch can
373
373
  deliver into those inboxes, but a heartbeat or operator wake-up is still
374
374
  required to make an idle app thread poll autonomously. Those recommendations
375
- include a `teardown_policy`: an idle poll is only a quiet interval, not a
376
- reason to delete or pause heartbeat automation; heartbeat teardown belongs to
377
- the manager/operator after terminal closeout or explicit operator instruction.
375
+ also include `wakeup_dispatch_command` and `delivery_receipt_commands` for
376
+ app-thread wake recovery. Use them to record sent, skipped, and blocked wake
377
+ outcomes after `app-wakeup-dispatch`; an app-thread send is not task
378
+ completion. The recommendations include a `teardown_policy`: an idle poll is
379
+ only a quiet interval, not a reason to delete or pause heartbeat automation;
380
+ heartbeat teardown belongs to the manager/operator after terminal closeout or
381
+ explicit operator instruction.
378
382
  The optional
379
383
  Codex app thread metadata is normally supplied after a Codex app manager has
380
384
  used `create_thread` and `set_thread_title`; terminal-only users can omit it
@@ -394,6 +398,12 @@ tmux attach -t codex-live-test
394
398
  for prepared, skipped, and blocked role wakeups. This command does not send
395
399
  `send_message_to_thread`; the Codex app/operator layer sends prompts whose
396
400
  actions report `send_ready=true`.
401
+ - `app-wakeup-record-delivery TASK --role manager|worker --dispatch-receipt ID
402
+ --delivery-status sent|skipped|blocked [--thread-id ID] [--reason TEXT]
403
+ [--json]` — Record what the Codex app/operator layer did with a wake action.
404
+ `sent` is accepted only when the referenced `app-wakeup-dispatch` receipt has
405
+ a matching `ready_to_send` action with `send_ready=true` and the same thread
406
+ id; healthy and blocked roles must be recorded as `skipped` or `blocked`.
397
407
  - `discover [QUERY] [--all] [--limit N]` / `search [QUERY]` — Search tasks,
398
408
  registered sessions, active bindings, and recent telemetry in one JSON result.
399
409
  Use this for conversational setup when a manager or Codex session needs to
@@ -1039,10 +1049,12 @@ Current dispatch state:
1039
1049
  reaches `max_iterations`. For no-tmux Codex app sessions, treat
1040
1050
  `communication.requires_polling=true` as requiring a heartbeat/wake layer:
1041
1051
  a delivered pull inbox item does not by itself wake an idle app thread. Do
1042
- not delete or pause heartbeats because an inbox poll is idle. A terminal
1043
- manager decision should be followed by `finish-task --require-criteria-audit`
1044
- or by an explicit blocker explaining why the task/binding still appears
1045
- active.
1052
+ not delete or pause heartbeats because an inbox poll is idle. Use generated
1053
+ `heartbeat_recommendations.wakeup_dispatch_command` and
1054
+ `heartbeat_recommendations.delivery_receipt_commands` for stale-thread wake
1055
+ recovery receipts. A terminal manager decision should be followed by
1056
+ `finish-task --require-criteria-audit` or by an explicit blocker explaining
1057
+ why the task/binding still appears active.
1046
1058
  - `register-worker`, `register-manager`, `sessions`, `discover`, and
1047
1059
  `create-disposable-binding --json` expose a `communication` block per
1048
1060
  session. Treat `session_kind='tmux'` plus `receive_style='push'` as direct
@@ -131,6 +131,9 @@ export function runTypescriptRuntimeCommand(options) {
131
131
  if (parsed.command === "app-wakeup-dispatch") {
132
132
  return runAppWakeupDispatchCommand(parsed, options);
133
133
  }
134
+ if (parsed.command === "app-wakeup-record-delivery") {
135
+ return runAppWakeupRecordDeliveryCommand(parsed, options);
136
+ }
134
137
  if (parsed.command === "qa-plan") {
135
138
  return runQaPlanCommand(parsed);
136
139
  }
@@ -546,9 +549,12 @@ function parseRuntimeArgs(args, env) {
546
549
  captureTranscriptLines: DEFAULT_HISTORY_LINES,
547
550
  captureTranscriptMode: "segment",
548
551
  decisionId: null,
552
+ deliveryStatus: null,
553
+ dispatchReceipt: null,
549
554
  force: false,
550
555
  message: null,
551
556
  reason: null,
557
+ threadId: null,
552
558
  consumeNext: false,
553
559
  wait: false,
554
560
  requireAcks: false,
@@ -1833,7 +1839,7 @@ function parseRuntimeArgs(args, env) {
1833
1839
  index = queue.length;
1834
1840
  }
1835
1841
  else if (arg === "--reason") {
1836
- if (command !== "finish-task" && command !== "stop-task" && command !== "record-decision" && command !== "compact-worker") {
1842
+ if (command !== "finish-task" && command !== "stop-task" && command !== "record-decision" && command !== "compact-worker" && command !== "app-wakeup-record-delivery") {
1837
1843
  return { command, enabled, error: "Unsupported TypeScript runtime option: --reason", explicit, flags, task };
1838
1844
  }
1839
1845
  const value = valueAfter(queue, index, arg);
@@ -1843,6 +1849,39 @@ function parseRuntimeArgs(args, env) {
1843
1849
  flags.reason = value.value;
1844
1850
  index += 1;
1845
1851
  }
1852
+ else if (arg === "--dispatch-receipt") {
1853
+ if (command !== "app-wakeup-record-delivery") {
1854
+ return { command, enabled, error: "Unsupported TypeScript runtime option: --dispatch-receipt", explicit, flags, task };
1855
+ }
1856
+ const value = valueAfter(queue, index, arg);
1857
+ if (value.error) {
1858
+ return { command, enabled, error: value.error, explicit, flags, task };
1859
+ }
1860
+ flags.dispatchReceipt = value.value;
1861
+ index += 1;
1862
+ }
1863
+ else if (arg === "--delivery-status") {
1864
+ if (command !== "app-wakeup-record-delivery") {
1865
+ return { command, enabled, error: "Unsupported TypeScript runtime option: --delivery-status", explicit, flags, task };
1866
+ }
1867
+ const value = valueAfter(queue, index, arg);
1868
+ if (value.error) {
1869
+ return { command, enabled, error: value.error, explicit, flags, task };
1870
+ }
1871
+ flags.deliveryStatus = value.value;
1872
+ index += 1;
1873
+ }
1874
+ else if (arg === "--thread-id") {
1875
+ if (command !== "app-wakeup-record-delivery") {
1876
+ return { command, enabled, error: "Unsupported TypeScript runtime option: --thread-id", explicit, flags, task };
1877
+ }
1878
+ const value = valueAfter(queue, index, arg);
1879
+ if (value.error) {
1880
+ return { command, enabled, error: value.error, explicit, flags, task };
1881
+ }
1882
+ flags.threadId = value.value;
1883
+ index += 1;
1884
+ }
1846
1885
  else if (arg === "--message") {
1847
1886
  if (command !== "finish-task"
1848
1887
  && command !== "stop-task"
@@ -3552,6 +3591,175 @@ function runAppWakeupDispatchCommand(parsed, options) {
3552
3591
  database.close();
3553
3592
  }
3554
3593
  }
3594
+ function runAppWakeupRecordDeliveryCommand(parsed, options) {
3595
+ const taskName = requireTask(parsed);
3596
+ if (parsed.flags.role !== "manager" && parsed.flags.role !== "worker") {
3597
+ return errorResult("app-wakeup-record-delivery requires --role manager|worker");
3598
+ }
3599
+ const role = parsed.flags.role;
3600
+ if (!parsed.flags.dispatchReceipt) {
3601
+ return errorResult("app-wakeup-record-delivery requires --dispatch-receipt");
3602
+ }
3603
+ const deliveryStatus = parseAppWakeupDeliveryStatus(parsed.flags.deliveryStatus);
3604
+ if (deliveryStatus instanceof Error) {
3605
+ return errorResult(deliveryStatus.message);
3606
+ }
3607
+ const database = openRuntimeDatabase(parsed, options);
3608
+ try {
3609
+ const task = taskRowForDiagnostics(database, taskName);
3610
+ const source = database.prepare(`
3611
+ select id, task_id, event_type, attributes_json
3612
+ from telemetry_events
3613
+ where id = ?
3614
+ limit 1
3615
+ `).get(parsed.flags.dispatchReceipt);
3616
+ if (!source) {
3617
+ throw new Error(`Unknown app wakeup dispatch receipt: ${parsed.flags.dispatchReceipt}`);
3618
+ }
3619
+ if (source.event_type !== "app_wakeup_dispatch_planned") {
3620
+ throw new Error(`Receipt ${source.id} is ${source.event_type}, expected app_wakeup_dispatch_planned.`);
3621
+ }
3622
+ if (source.task_id !== task.id) {
3623
+ throw new Error(`Receipt ${source.id} does not belong to task ${task.name}.`);
3624
+ }
3625
+ const attributes = parseAppWakeupDispatchAttributes(source.attributes_json, source.id);
3626
+ const action = attributes.actions.find((candidate) => candidate.role === role);
3627
+ if (!action) {
3628
+ throw new Error(`Receipt ${source.id} has no ${role} wake action.`);
3629
+ }
3630
+ validateAppWakeupDelivery({ action, deliveryStatus, role, threadId: parsed.flags.threadId });
3631
+ const timestamp = nowIsoSeconds(options);
3632
+ const eventId = emitTelemetrySync(database, {
3633
+ actor: "manager",
3634
+ attributes: {
3635
+ delivery_status: deliveryStatus,
3636
+ dispatch_receipt: source.id,
3637
+ dispatch_required: attributes.summary.dispatcher_required,
3638
+ reason: parsed.flags.reason,
3639
+ role,
3640
+ source_action_status: action.status,
3641
+ source_send_ready: action.send_ready === true,
3642
+ thread_id: parsed.flags.threadId ?? action.thread_id ?? null,
3643
+ thread_title: action.thread_title ?? null,
3644
+ },
3645
+ correlation: {
3646
+ command: "app-wakeup-record-delivery",
3647
+ dispatch_receipt: source.id,
3648
+ role,
3649
+ thread_id: parsed.flags.threadId ?? action.thread_id ?? null,
3650
+ },
3651
+ eventType: "app_wakeup_delivery_recorded",
3652
+ severity: deliveryStatus === "sent" ? "info" : "warning",
3653
+ summary: `App wakeup delivery ${deliveryStatus} for ${role} on ${task.name}.`,
3654
+ taskId: task.id,
3655
+ timestamp,
3656
+ });
3657
+ const output = {
3658
+ delivery: {
3659
+ reason: parsed.flags.reason,
3660
+ role,
3661
+ source_action_status: action.status,
3662
+ source_send_ready: action.send_ready === true,
3663
+ status: deliveryStatus,
3664
+ thread_id: parsed.flags.threadId ?? action.thread_id ?? null,
3665
+ },
3666
+ receipt: {
3667
+ event_id: eventId,
3668
+ event_type: "app_wakeup_delivery_recorded",
3669
+ recorded_at: timestamp,
3670
+ },
3671
+ source: {
3672
+ dispatch_receipt: source.id,
3673
+ dispatch_required: attributes.summary.dispatcher_required,
3674
+ dispatcher: attributes.dispatcher,
3675
+ },
3676
+ task: { id: task.id, name: task.name },
3677
+ };
3678
+ if (parsed.flags.json) {
3679
+ return jsonResult(output);
3680
+ }
3681
+ return {
3682
+ exitCode: 0,
3683
+ handled: true,
3684
+ stdout: [
3685
+ `App wakeup delivery ${deliveryStatus} recorded for ${role} on ${task.name}.`,
3686
+ `Source receipt: ${source.id}`,
3687
+ `Receipt: ${eventId}`,
3688
+ attributes.summary.dispatcher_required ? "Dispatch still required by source receipt." : "Dispatch healthy in source receipt.",
3689
+ ].join("\n") + "\n",
3690
+ };
3691
+ }
3692
+ finally {
3693
+ database.close();
3694
+ }
3695
+ }
3696
+ function parseAppWakeupDeliveryStatus(value) {
3697
+ if (value === "sent" || value === "skipped" || value === "blocked") {
3698
+ return value;
3699
+ }
3700
+ return new Error("app-wakeup-record-delivery requires --delivery-status sent|skipped|blocked");
3701
+ }
3702
+ function parseAppWakeupDispatchAttributes(value, receiptId) {
3703
+ let parsed;
3704
+ try {
3705
+ parsed = JSON.parse(value);
3706
+ }
3707
+ catch {
3708
+ throw new Error(`Receipt ${receiptId} has invalid attributes JSON.`);
3709
+ }
3710
+ if (!parsed || typeof parsed !== "object") {
3711
+ throw new Error(`Receipt ${receiptId} has invalid attributes shape.`);
3712
+ }
3713
+ const attributes = parsed;
3714
+ if (!Array.isArray(attributes.actions)) {
3715
+ throw new Error(`Receipt ${receiptId} is missing actions.`);
3716
+ }
3717
+ const summary = attributes.summary;
3718
+ if (!summary || typeof summary !== "object" || typeof summary.dispatcher_required !== "boolean") {
3719
+ throw new Error(`Receipt ${receiptId} is missing dispatcher_required summary.`);
3720
+ }
3721
+ const dispatcher = attributes.dispatcher && typeof attributes.dispatcher === "object"
3722
+ ? attributes.dispatcher
3723
+ : {};
3724
+ return {
3725
+ actions: attributes.actions,
3726
+ dispatcher,
3727
+ summary: { dispatcher_required: Boolean(summary.dispatcher_required) },
3728
+ };
3729
+ }
3730
+ function validateAppWakeupDelivery(options) {
3731
+ const sourceStatus = parseAppWakeupSourceActionStatus(options.action.status);
3732
+ if (sourceStatus instanceof Error) {
3733
+ throw sourceStatus;
3734
+ }
3735
+ if (options.deliveryStatus === "sent") {
3736
+ if (sourceStatus !== "ready_to_send" || options.action.send_ready !== true) {
3737
+ throw new Error(`Cannot record sent wakeup for ${options.role}; source action is ${sourceStatus}.`);
3738
+ }
3739
+ if (!options.threadId) {
3740
+ throw new Error("app-wakeup-record-delivery --delivery-status sent requires --thread-id.");
3741
+ }
3742
+ if (options.action.thread_id !== options.threadId) {
3743
+ throw new Error(`Thread id mismatch for ${options.role}; source action targets ${options.action.thread_id ?? "(none)"}.`);
3744
+ }
3745
+ return;
3746
+ }
3747
+ if (options.deliveryStatus === "skipped") {
3748
+ if (sourceStatus !== "skipped_healthy") {
3749
+ throw new Error(`Cannot record skipped wakeup for ${options.role}; source action is ${sourceStatus}.`);
3750
+ }
3751
+ return;
3752
+ }
3753
+ if (sourceStatus !== "blocked_missing_thread") {
3754
+ throw new Error(`Cannot record blocked wakeup for ${options.role}; source action is ${sourceStatus}.`);
3755
+ }
3756
+ }
3757
+ function parseAppWakeupSourceActionStatus(value) {
3758
+ if (value === "ready_to_send" || value === "skipped_healthy" || value === "blocked_missing_thread") {
3759
+ return value;
3760
+ }
3761
+ return new Error(`Unsupported app wakeup source action status: ${value ?? "(missing)"}.`);
3762
+ }
3555
3763
  function renderAppLoopStatusText(status) {
3556
3764
  const lines = [
3557
3765
  `App loop ${status.task.name}: ${status.ok ? "ok" : "attention required"}`,
@@ -13890,6 +14098,7 @@ function isDefaultRuntimeCommand(command) {
13890
14098
  || command === "app-loop-status"
13891
14099
  || command === "app-wakeup-plan"
13892
14100
  || command === "app-wakeup-dispatch"
14101
+ || command === "app-wakeup-record-delivery"
13893
14102
  || command === "loop-templates"
13894
14103
  || command === "loop-triggers"
13895
14104
  || command === "ralph-loop-presets"
@@ -16905,6 +17114,8 @@ function renderDisposableBindingText(result) {
16905
17114
  lines.push(` worker: ${result.heartbeat_recommendations.worker.poll_command}`);
16906
17115
  lines.push(` status: ${result.heartbeat_recommendations.status_command}`);
16907
17116
  lines.push(` wakeup plan: ${result.heartbeat_recommendations.wakeup_plan_command}`);
17117
+ lines.push(` wakeup dispatch: ${result.heartbeat_recommendations.wakeup_dispatch_command}`);
17118
+ lines.push(` delivery sent: ${result.heartbeat_recommendations.delivery_receipt_commands.sent}`);
16908
17119
  lines.push(` teardown: ${result.heartbeat_recommendations.teardown_policy.idle_poll}`);
16909
17120
  lines.push(` closeout: ${result.heartbeat_recommendations.teardown_policy.terminal_closeout_command}`);
16910
17121
  }
@@ -16918,6 +17129,8 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
16918
17129
  const workerHeartbeatCommand = disposableAppHeartbeatCommand("worker", taskName, dbPath);
16919
17130
  const managerInboxCommand = sessionPollCommand("manager", taskName, dbPath);
16920
17131
  const workerInboxCommand = sessionPollCommand("worker", taskName, dbPath);
17132
+ const wakeupDispatchCommand = `${conveyorPollInvocation()} app-wakeup-dispatch ${shellQuote(taskName)} --path ${shellQuote(dbPath)} --json`;
17133
+ const deliveryReceiptCommands = disposableDeliveryReceiptCommands(taskName, dbPath);
16921
17134
  return {
16922
17135
  applies_when: {
16923
17136
  can_receive_push: false,
@@ -16925,6 +17138,7 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
16925
17138
  receive_style: "pull",
16926
17139
  session_kind: "codex_app",
16927
17140
  },
17141
+ delivery_receipt_commands: deliveryReceiptCommands,
16928
17142
  interval_minutes: 2,
16929
17143
  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
17144
  status_command: `${conveyorPollInvocation()} app-loop-status ${shellQuote(taskName)} --path ${shellQuote(dbPath)} --json`,
@@ -16935,6 +17149,7 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
16935
17149
  terminal_closeout_command: terminalCloseoutCommand,
16936
17150
  worker_rule: "The worker must not own loop teardown and must not remove heartbeat automation based on idle polling.",
16937
17151
  },
17152
+ wakeup_dispatch_command: wakeupDispatchCommand,
16938
17153
  wakeup_plan_command: `${conveyorPollInvocation()} app-wakeup-plan ${shellQuote(taskName)} --path ${shellQuote(dbPath)} --json`,
16939
17154
  manager: {
16940
17155
  direct_inbox_command: managerInboxCommand,
@@ -16945,6 +17160,11 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
16945
17160
  `Run the manager app heartbeat for task ${taskName}.`,
16946
17161
  `Run: ${managerHeartbeatCommand}`,
16947
17162
  `If the heartbeat output asks for direct inbox polling, run: ${managerInboxCommand}`,
17163
+ `For stale app-thread recovery with an auditable receipt, run: ${wakeupDispatchCommand}`,
17164
+ "Send app-thread wake prompts only for actions where `send_ready=true`; direct app-thread delivery is not task completion.",
17165
+ `After a successful app-thread send, record it with: ${deliveryReceiptCommands.sent}`,
17166
+ `For healthy skipped actions, record: ${deliveryReceiptCommands.skipped}`,
17167
+ `For missing-thread blocked actions, record: ${deliveryReceiptCommands.blocked}`,
16948
17168
  "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
17169
  "If no item is consumed, stop after a one-line idle receipt.",
16950
17170
  "Do not delete, pause, or disable manager or worker heartbeat automation after an idle poll; an idle poll is only a quiet interval.",
@@ -16961,13 +17181,23 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
16961
17181
  `Run the worker app heartbeat for task ${taskName}.`,
16962
17182
  `Run: ${workerHeartbeatCommand}`,
16963
17183
  `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.",
17184
+ "If an item is consumed, execute only that single worker instruction and return exact commands, compact evidence for any completion claim, blockers/residual risk, and exactly one next recommended worker task.",
16965
17185
  "If no item is consumed, stop after a one-line idle receipt.",
16966
17186
  "Do not delete, pause, or disable worker heartbeat automation after an idle poll; the manager or operator owns terminal loop teardown.",
16967
17187
  ].join("\n"),
16968
17188
  },
16969
17189
  };
16970
17190
  }
17191
+ function disposableDeliveryReceiptCommands(taskName, dbPath) {
17192
+ const base = `${conveyorPollInvocation()} app-wakeup-record-delivery ${shellQuote(taskName)} --role <role> --dispatch-receipt <receipt.event_id>`;
17193
+ const pathAndJson = ` --path ${shellQuote(dbPath)} --json`;
17194
+ return {
17195
+ blocked: `${base} --delivery-status blocked${pathAndJson}`,
17196
+ note: "Run these only after app-wakeup-dispatch. Replace <role>, <receipt.event_id>, and <action.thread.id> from the dispatch JSON; sent is valid only for send_ready=true actions.",
17197
+ sent: `${base} --delivery-status sent --thread-id <action.thread.id>${pathAndJson}`,
17198
+ skipped: `${base} --delivery-status skipped${pathAndJson}`,
17199
+ };
17200
+ }
16971
17201
  function disposableAppHeartbeatCommand(role, taskName, dbPath) {
16972
17202
  return `${conveyorPollInvocation()} app-heartbeat ${shellQuote(taskName)} --role ${role} --path ${shellQuote(dbPath)} --json`;
16973
17203
  }