agent-conveyor 0.1.12 → 0.1.13

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
@@ -174,7 +174,10 @@ thread identity through `--worker-codex-app-thread-id` and
174
174
  deliver the generated `worker_handoff` bootstrap prompt. The raw terminal
175
175
  `conveyor` CLI does not create Codex app threads by itself; if app thread tools
176
176
  are unavailable, open a separate Codex app worker manually and paste the
177
- `worker_handoff` prompt.
177
+ `worker_handoff` prompt. After a worker consumes a manager instruction, its
178
+ completion or blocker report must go back through the generated
179
+ `enqueue-notify-manager` command and a bounded Dispatch watch tick; a direct
180
+ Codex app final answer is not a durable manager receipt.
178
181
 
179
182
  Dispatch is core infrastructure for supervised worker/manager pairs. The
180
183
  `pair` workflow starts a detached Dispatch watch process by default so worker
@@ -371,8 +374,12 @@ tmux attach -t codex-live-test
371
374
  Codex app sessions, the JSON output also includes
372
375
  `heartbeat_recommendations` with role-specific poll prompts; Dispatch can
373
376
  deliver into those inboxes, but a heartbeat or operator wake-up is still
374
- required to make an idle app thread poll autonomously. Those recommendations
375
- also include `wakeup_dispatch_command` and `delivery_receipt_commands` for
377
+ required to make an idle app thread poll autonomously. The worker handoff and
378
+ worker heartbeat prompt also include the exact durable
379
+ `enqueue-notify-manager` and one-iteration `dispatch --watch` commands that a
380
+ worker must run after completing or blocking on a consumed item. Those
381
+ recommendations also include `wakeup_dispatch_command` and
382
+ `delivery_receipt_commands` for
376
383
  app-thread wake recovery. Use them to record sent, skipped, and blocked wake
377
384
  outcomes after `app-wakeup-dispatch`; an app-thread send is not task
378
385
  completion. The recommendations include a `teardown_policy`: an idle poll is
@@ -405,7 +412,8 @@ tmux attach -t codex-live-test
405
412
  a matching `ready_to_send` action with `send_ready=true` and the same thread
406
413
  id; healthy and blocked roles must be recorded as `skipped` or `blocked`.
407
414
  - `app-autopilot start|stop|status TASK [--dispatcher-id ID]
408
- [--interval SECONDS] [--watch-iterations N] [--stale-after N] [--json]` —
415
+ [--interval SECONDS] [--watch-iterations N] [--stale-after N]
416
+ [--quiet-after N] [--json]` —
409
417
  Manage the pair-level app-native heartbeat policy for the active
410
418
  manager/worker binding. `start` and `stop` write telemetry receipts and emit
411
419
  the exact manager/worker Codex app heartbeat automation specs plus the
@@ -413,6 +421,11 @@ tmux attach -t codex-live-test
413
421
  thread tools, so create/pause those heartbeat automations from a Codex app
414
422
  operator session using the emitted specs; Conveyor remains the durable source
415
423
  of truth through Dispatch, inboxes, wake receipts, and app heartbeat status.
424
+ `status` also reports `plan.quiescence`: when the loop is healthy, has no
425
+ `next_actions`, and both roles have produced `--quiet-after` paired
426
+ heartbeats since the last command or inbox-consumption receipt, it recommends
427
+ `stop_autopilot` so operators can quiesce blocked/no-progress loops instead
428
+ of repeating idle pulses.
416
429
  - `discover [QUERY] [--all] [--limit N]` / `search [QUERY]` — Search tasks,
417
430
  registered sessions, active bindings, and recent telemetry in one JSON result.
418
431
  Use this for conversational setup when a manager or Codex session needs to
@@ -640,7 +653,10 @@ tmux attach -t codex-live-test
640
653
  recovery; `--once` performs one pass.
641
654
  - `enqueue-notify-manager <task> --message "..." [--correlation-id C]
642
655
  [--required-permission P] [--idempotency-key K] [--json]` — Queue a `notify_manager` command row for
643
- Dispatch to claim and deliver to the bound manager.
656
+ Dispatch to claim and deliver to the bound manager. Codex app/no-tmux
657
+ workers must use this route for completion and blocker reports after
658
+ consuming a manager instruction; direct app-thread final answers are local
659
+ text, not manager inbox receipts.
644
660
  - `enqueue-nudge-worker <task> --message "..." [--correlation-id C]
645
661
  [--required-permission P] [--idempotency-key K] [--json]` — Queue a `nudge_worker` command row for
646
662
  Dispatch to claim and deliver to the bound worker. Use this dispatcher-backed
@@ -793,7 +809,10 @@ same-project `create_thread` worker plus `set_thread_title` before creating the
793
809
  binding, then pass the worker thread id/title into Conveyor. Use `fork_thread`
794
810
  only when the user explicitly asks to fork or resume this conversation. If app
795
811
  thread tools are unavailable, create the binding anyway and paste the returned
796
- `worker_handoff` prompt into a manually opened worker session.
812
+ `worker_handoff` prompt into a manually opened worker session. The handoff
813
+ requires a worker to report completion/blockers through
814
+ `enqueue-notify-manager` plus a bounded Dispatch watch run before treating the
815
+ manager as notified.
797
816
  - `enqueue-continue-iteration TASK --loop-run RUN --requested-iteration N` —
798
817
  Queue a manager-requested next loop pass for Dispatch. The command refuses
799
818
  same/current iteration requests before they become pending queue rows, while
@@ -389,7 +389,7 @@ function commandHelpText(program, command) {
389
389
  ` ${program} dispatch --once --type nudge_worker --path /tmp/work/workerctl.db`,
390
390
  ],
391
391
  "app-autopilot": [
392
- `usage: ${program} app-autopilot start|stop|status <task> [--dispatcher-id ID] [--interval SECONDS] [--watch-iterations N] [--stale-after N] ${path} [--json]`,
392
+ `usage: ${program} app-autopilot start|stop|status <task> [--dispatcher-id ID] [--interval SECONDS] [--watch-iterations N] [--stale-after N] [--quiet-after N] ${path} [--json]`,
393
393
  "",
394
394
  "Manage the app-native heartbeat policy for a bound manager/worker pair.",
395
395
  "The CLI records policy receipts and emits Codex app heartbeat automation specs; app-thread automation creation still happens through Codex app tools.",
@@ -528,6 +528,7 @@ function parseRuntimeArgs(args, env) {
528
528
  source: null,
529
529
  proof: null,
530
530
  purpose: null,
531
+ quietAfterCycles: 3,
531
532
  questions: false,
532
533
  rationale: null,
533
534
  receiptOutput: null,
@@ -2496,6 +2497,21 @@ function parseRuntimeArgs(args, env) {
2496
2497
  flags.appStaleAfterSeconds = value;
2497
2498
  index += 1;
2498
2499
  }
2500
+ else if (arg === "--quiet-after") {
2501
+ if (command !== "app-autopilot") {
2502
+ return { command, enabled, error: "Unsupported TypeScript runtime option: --quiet-after", explicit, flags, task };
2503
+ }
2504
+ const parsedValue = valueAfter(queue, index, arg);
2505
+ if (parsedValue.error) {
2506
+ return { command, enabled, error: parsedValue.error, explicit, flags, task };
2507
+ }
2508
+ const value = Number(parsedValue.value);
2509
+ if (!Number.isInteger(value) || value < 0) {
2510
+ return { command, enabled, error: "--quiet-after must be a non-negative integer.", explicit, flags, task };
2511
+ }
2512
+ flags.quietAfterCycles = value;
2513
+ index += 1;
2514
+ }
2499
2515
  else if (arg === "--terminal-stale-seconds") {
2500
2516
  if (command !== "idle-check") {
2501
2517
  return { command, enabled, error: "Unsupported TypeScript runtime option: --terminal-stale-seconds", explicit, flags, task };
@@ -3738,6 +3754,7 @@ function runAppAutopilotCommand(parsed, options) {
3738
3754
  heartbeatIntervalMinutes: 2,
3739
3755
  heartbeatStaleSeconds: parsed.flags.appStaleAfterSeconds,
3740
3756
  now: timestamp,
3757
+ quietAfterCycles: parsed.flags.quietAfterCycles,
3741
3758
  taskName,
3742
3759
  watchIterations: parsed.flags.watchIterations ?? 1000000,
3743
3760
  });
@@ -3759,6 +3776,7 @@ function runAppAutopilotCommand(parsed, options) {
3759
3776
  dispatcher_command: plan.control.dispatcher_command,
3760
3777
  dispatcher_id: dispatcherId,
3761
3778
  interval_minutes: plan.interval_minutes,
3779
+ quiescence: plan.quiescence,
3762
3780
  summary: plan.summary,
3763
3781
  },
3764
3782
  correlation: {
@@ -3918,6 +3936,13 @@ function renderAppAutopilotText(result) {
3918
3936
  `Dispatch command: ${result.plan.control.dispatcher_command}`,
3919
3937
  `Wake dispatch: ${result.plan.control.wakeup_dispatch_command}`,
3920
3938
  ];
3939
+ if (result.plan.quiescence.recommended_action === "stop_autopilot") {
3940
+ lines.push(`Quiescence: stop recommended - ${result.plan.quiescence.reason}`);
3941
+ lines.push(`Stop command: ${result.plan.control.stop_command}`);
3942
+ }
3943
+ else {
3944
+ lines.push(`Quiescence: ${result.plan.quiescence.state} (${result.plan.quiescence.quiet_cycles}/${result.plan.quiescence.threshold_cycles} quiet cycles)`);
3945
+ }
3921
3946
  if (result.receipt) {
3922
3947
  lines.push(`Receipt: ${result.receipt.event_type} ${result.receipt.event_id}`);
3923
3948
  }
@@ -17218,11 +17243,17 @@ function disposableWorkerHandoff(taskName, runName, dbPath) {
17218
17243
  const loopClause = runName
17219
17244
  ? ` for Ralph loop run ${runName}`
17220
17245
  : " for this disposable no-tmux binding";
17246
+ const notifyCommand = durableWorkerNotifyManagerCommand(taskName, dbPath);
17247
+ const dispatchCommand = durableWorkerNotifyDispatchCommand(dbPath);
17221
17248
  return [
17222
17249
  "Use the manage-codex-workers skill.",
17223
17250
  "",
17224
17251
  `You are the worker for task ${taskName}${loopClause}.`,
17225
17252
  "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.",
17253
+ "After completing or blocking on a consumed item, send the manager a durable Conveyor notification before your final answer. A direct app-thread final answer is not a manager receipt and is not task completion.",
17254
+ `Run: ${notifyCommand}`,
17255
+ `Then run: ${dispatchCommand}`,
17256
+ "If either notify/dispatch command fails, include that failure as the blocker and do not claim the manager was notified.",
17226
17257
  "",
17227
17258
  "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.",
17228
17259
  "Do not delete, pause, or disable heartbeat automation just because an inbox poll is idle; the manager or operator owns terminal loop teardown.",
@@ -17263,6 +17294,8 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
17263
17294
  const workerHeartbeatCommand = disposableAppHeartbeatCommand("worker", taskName, dbPath);
17264
17295
  const managerInboxCommand = sessionPollCommand("manager", taskName, dbPath);
17265
17296
  const workerInboxCommand = sessionPollCommand("worker", taskName, dbPath);
17297
+ const workerNotifyCommand = durableWorkerNotifyManagerCommand(taskName, dbPath);
17298
+ const workerNotifyDispatchCommand = durableWorkerNotifyDispatchCommand(dbPath);
17266
17299
  const wakeupDispatchCommand = `${conveyorPollInvocation()} app-wakeup-dispatch ${shellQuote(taskName)} --path ${shellQuote(dbPath)} --json`;
17267
17300
  const deliveryReceiptCommands = disposableDeliveryReceiptCommands(taskName, dbPath);
17268
17301
  return {
@@ -17316,6 +17349,10 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
17316
17349
  `Run: ${workerHeartbeatCommand}`,
17317
17350
  `If the heartbeat output asks for direct inbox polling, run: ${workerInboxCommand}`,
17318
17351
  "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.",
17352
+ "Before your final answer after any consumed item, notify the manager durably; a direct app-thread final answer is not a manager receipt and is not task completion.",
17353
+ `Run: ${workerNotifyCommand}`,
17354
+ `Then run: ${workerNotifyDispatchCommand}`,
17355
+ "If either notify/dispatch command fails, include that failure as the blocker and do not claim the manager was notified.",
17319
17356
  "If no item is consumed, stop after a one-line idle receipt.",
17320
17357
  "Do not delete, pause, or disable worker heartbeat automation after an idle poll; the manager or operator owns terminal loop teardown.",
17321
17358
  ].join("\n"),
@@ -17335,6 +17372,12 @@ function disposableDeliveryReceiptCommands(taskName, dbPath) {
17335
17372
  function disposableAppHeartbeatCommand(role, taskName, dbPath) {
17336
17373
  return `${conveyorPollInvocation()} app-heartbeat ${shellQuote(taskName)} --role ${role} --path ${shellQuote(dbPath)} --json`;
17337
17374
  }
17375
+ function durableWorkerNotifyManagerCommand(taskName, dbPath) {
17376
+ return `${conveyorPollInvocation()} enqueue-notify-manager ${shellQuote(taskName)} --message ${shellQuote("<compact completion/blocker report with files, commands, evidence, residual risk, and next recommended worker task>")} --correlation-id ${shellQuote("<worker-result-id>")} --path ${shellQuote(dbPath)} --json`;
17377
+ }
17378
+ function durableWorkerNotifyDispatchCommand(dbPath) {
17379
+ return `${conveyorPollInvocation()} dispatch --watch --watch-iterations 1 --interval 2 --dispatcher-id dispatch-local --path ${shellQuote(dbPath)} --json`;
17380
+ }
17338
17381
  function sessionPollCommand(role, taskName, dbPath) {
17339
17382
  const inbox = role === "worker" ? "worker-inbox" : "manager-inbox";
17340
17383
  const task = taskName ? shellQuote(taskName) : "<task>";