agent-conveyor 0.1.20 → 0.1.22

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
@@ -168,8 +168,9 @@ conveyor plugin-status
168
168
 
169
169
  The per-project default ledger for operator sessions is
170
170
  `.codex-workers/workerctl.db`. The initial included skills are
171
- `conveyor-create-pair`, `conveyor-create-worker-set`, and
172
- `conveyor-check-status`.
171
+ `conveyor-app-wake-relay`, `conveyor-create-pair`,
172
+ `conveyor-create-worker-set`, `conveyor-check-status`, and
173
+ `conveyor-whats-next-nudger`.
173
174
 
174
175
  After install, the intended Codex app entry point is natural language. Open a
175
176
  new Codex app session in the target repo and say:
@@ -204,6 +205,15 @@ print `CONVEYOR POLL`, `CONVEYOR RECEIVED`, `WORK`, `CONVEYOR SEND`, and
204
205
  audit proof, not a replacement for the live session story. Idle polls may be a
205
206
  single `CONVEYOR IDLE` line.
206
207
 
208
+ For bounded follow-up passes after the first worker result, use
209
+ `Use the conveyor-whats-next-nudger skill`.
210
+
211
+ For stale Codex app roles, use `Use the conveyor-app-wake-relay skill`. It
212
+ runs `app-wakeup-dispatch`, sends only prepared `send_ready=true` prompts with
213
+ Codex app thread tools, and records `sent`, `skipped`, or `blocked` delivery
214
+ receipts with `app-wakeup-record-delivery`. The app-thread prompt wakes the
215
+ session to poll; it is not durable task truth.
216
+
207
217
  Dispatch is core infrastructure for supervised worker/manager pairs. The
208
218
  `pair` workflow starts a detached Dispatch watch process by default so worker
209
219
  completion is routed to the bound manager mechanically. For manually bound
@@ -512,7 +522,8 @@ stay out of receipts.
512
522
  [--json]` — Record what the Codex app/operator layer did with a wake action.
513
523
  `sent` is accepted only when the referenced `app-wakeup-dispatch` receipt has
514
524
  a matching `ready_to_send` action with `send_ready=true` and the same thread
515
- id; healthy and blocked roles must be recorded as `skipped` or `blocked`.
525
+ id; healthy roles must be recorded as `skipped`; missing-thread blockers and
526
+ failed app-thread sends must be recorded as `blocked` with a reason.
516
527
  - `app-autopilot start|stop|status TASK [--dispatcher-id ID]
517
528
  [--interval SECONDS] [--watch-iterations N] [--stale-after N]
518
529
  [--quiet-after N] [--json]` —
@@ -804,6 +815,15 @@ stay out of receipts.
804
815
  - `worker-inbox <task> [--consume-next] [--wait] [--timeout N] [--interval N]
805
816
  [--limit N] [--json]` — Resolve the task's bound worker session and read its
806
817
  dispatcher inbox.
818
+ - `inbox-ack <task> --notification-id N [--json]` — Read durable
819
+ role-authored acknowledgement history for one routed notification.
820
+ - `inbox-ack <task> --notification-id N --role manager|worker
821
+ --status received|accepted|blocked --from-stdin [--correlation-id C]
822
+ [--json]` — Record that the addressed role saw a delivered notification and
823
+ either received, accepted, or blocked on it. `received` requires delivery;
824
+ `accepted` and `blocked` require consumption. Use this after visible Codex app
825
+ sessions print `CONVEYOR RECEIVED`; do not use direct app-thread text as the
826
+ durable receipt.
807
827
 
808
828
  ### Actuation
809
829
 
@@ -18,7 +18,7 @@ import { executeDispatchCommandSync } from "../runtime/dispatch.js";
18
18
  import { deregisterSessionSync, discoverRegistrySync, findRolloutPathForPid, listRegisteredSessionsSync, registerSessionSync, readSessionMeta, sessionRow, } from "../runtime/codex-session.js";
19
19
  import { managerConfigPermissionAllowed, managerConfigSync } from "../runtime/manager-config.js";
20
20
  import { canonicalManagerPermissionNames, flattenManagerPermissions, normalizeManagerPermissions, } from "../runtime/manager-permissions.js";
21
- import { deferRoutedNotificationBeforeSideEffectSync, deliveryModeForTargetSessionSync, finishRoutedNotificationSync, insertRoutedNotificationSync, markRoutedNotificationSideEffectStartedSync, consumeNextSessionInboxItemSync, routedNotificationsSync, sessionInboxSync, } from "../runtime/notifications.js";
21
+ import { deferRoutedNotificationBeforeSideEffectSync, deliveryModeForTargetSessionSync, finishRoutedNotificationSync, insertRoutedNotificationSync, insertNotificationAcknowledgementSync, latestNotificationAcknowledgementSync, markRoutedNotificationSideEffectStartedSync, notificationAcknowledgementsSync, consumeNextSessionInboxItemSync, routedNotificationsSync, sessionInboxSync, } from "../runtime/notifications.js";
22
22
  import { activeBindingForTaskSync, bindSessionsSync, createTaskSync, listTasksSync, unbindTaskSync, } from "../runtime/tasks.js";
23
23
  import { captureTmuxTargetWithRunner, captureTranscriptTmuxTargetWithRunner, currentPaneIdWithRunner, killTmuxSessionWithRunner, sendEnterToTmuxSessionWithRunner, sendTextToSessionWithRunner, sessionExists, startTmuxSessionWithRunner, tmuxCommandFailureMessage, tmuxSession, tmuxSessionRunning, } from "../runtime/tmux.js";
24
24
  import { captureMetaPath, configPath, defaultDbPath, eventsPath, loadJsonSync, stateRoot, statusPath, transcriptPath, validateWorkerName, workerDir, writeJsonSync, } from "../state/files.js";
@@ -261,6 +261,9 @@ export function runTypescriptRuntimeCommand(options) {
261
261
  if (parsed.command === "manager-ack") {
262
262
  return runTaskAckCommand(parsed, options, "manager");
263
263
  }
264
+ if (parsed.command === "inbox-ack") {
265
+ return runInboxAckCommand(parsed, options);
266
+ }
264
267
  if (parsed.command === "session-inbox") {
265
268
  return runSessionInboxCommand(parsed, options, "session");
266
269
  }
@@ -420,6 +423,15 @@ function commandHelpText(program, command) {
420
423
  "Example JSON:",
421
424
  ` {"task":"my-task","manager_session":"mgr","supervision_contract":"I will supervise through Conveyor and verify criteria before finishing.","will_not_edit_project_files":true}`,
422
425
  ],
426
+ "inbox-ack": [
427
+ `usage: ${program} inbox-ack <task> --notification-id N --role manager|worker --status received|accepted|blocked --from-stdin ${path} [--json]`,
428
+ `usage: ${program} inbox-ack <task> --notification-id N --json ${path}`,
429
+ "",
430
+ "Record or read a notification-scoped manager/worker acknowledgement.",
431
+ "",
432
+ "Example JSON:",
433
+ ` {"summary":"Received manager instruction and will run the bounded task.","evidence":[]}`,
434
+ ],
423
435
  nudge: [
424
436
  `usage: ${program} nudge <worker-or-session> <message> ${path} [--dry-run]`,
425
437
  `usage: ${program} session-nudge <session> <message> ${path} [--dry-run]`,
@@ -584,6 +596,7 @@ function parseRuntimeArgs(args, env) {
584
596
  nextAction: null,
585
597
  noFollowup: false,
586
598
  nextSteps: [],
599
+ notificationId: null,
587
600
  output: null,
588
601
  path: null,
589
602
  pid: null,
@@ -1502,7 +1515,7 @@ function parseRuntimeArgs(args, env) {
1502
1515
  index += 1;
1503
1516
  }
1504
1517
  else if (arg === "--from-stdin") {
1505
- if (command !== "criteria-plan" && command !== "continuation" && command !== "worker-ack" && command !== "manager-ack") {
1518
+ if (command !== "criteria-plan" && command !== "continuation" && command !== "worker-ack" && command !== "manager-ack" && command !== "inbox-ack") {
1506
1519
  return { command, enabled, error: "Unsupported TypeScript runtime option: --from-stdin", explicit, flags, task };
1507
1520
  }
1508
1521
  flags.fromStdin = true;
@@ -2373,7 +2386,7 @@ function parseRuntimeArgs(args, env) {
2373
2386
  flags.epilogueStatus = true;
2374
2387
  continue;
2375
2388
  }
2376
- if (command !== "criteria" && command !== "runs" && command !== "loop-evidence" && command !== "campaign") {
2389
+ if (command !== "criteria" && command !== "runs" && command !== "loop-evidence" && command !== "campaign" && command !== "inbox-ack") {
2377
2390
  return { command, enabled, error: "Unsupported TypeScript runtime option: --status", explicit, flags, task };
2378
2391
  }
2379
2392
  const parsedValue = valueAfter(queue, index, arg);
@@ -2383,11 +2396,29 @@ function parseRuntimeArgs(args, env) {
2383
2396
  if (command === "criteria") {
2384
2397
  flags.statuses.push(parsedValue.value);
2385
2398
  }
2399
+ else if (command === "inbox-ack") {
2400
+ flags.statusState = parsedValue.value;
2401
+ }
2386
2402
  else {
2387
2403
  flags.statusState = parsedValue.value;
2388
2404
  }
2389
2405
  index += 1;
2390
2406
  }
2407
+ else if (arg === "--notification-id") {
2408
+ if (command !== "inbox-ack") {
2409
+ return { command, enabled, error: "Unsupported TypeScript runtime option: --notification-id", explicit, flags, task };
2410
+ }
2411
+ const parsedValue = valueAfter(queue, index, arg);
2412
+ if (parsedValue.error) {
2413
+ return { command, enabled, error: parsedValue.error, explicit, flags, task };
2414
+ }
2415
+ const value = Number(parsedValue.value);
2416
+ if (!Number.isInteger(value) || value <= 0) {
2417
+ return { command, enabled, error: "--notification-id must be a positive integer.", explicit, flags, task };
2418
+ }
2419
+ flags.notificationId = value;
2420
+ index += 1;
2421
+ }
2391
2422
  else if (arg === "--type") {
2392
2423
  const value = valueAfter(queue, index, arg);
2393
2424
  if (value.error) {
@@ -2537,6 +2568,7 @@ function parseRuntimeArgs(args, env) {
2537
2568
  && command !== "enqueue-continue-iteration"
2538
2569
  && command !== "worker-ack"
2539
2570
  && command !== "manager-ack"
2571
+ && command !== "inbox-ack"
2540
2572
  && command !== "loop-evidence"
2541
2573
  && command !== "continuation"
2542
2574
  && command !== "continuation-reviewer"
@@ -3863,7 +3895,7 @@ function runAppWakeupRecordDeliveryCommand(parsed, options) {
3863
3895
  if (!action) {
3864
3896
  throw new Error(`Receipt ${source.id} has no ${role} wake action.`);
3865
3897
  }
3866
- validateAppWakeupDelivery({ action, deliveryStatus, role, threadId: parsed.flags.threadId });
3898
+ validateAppWakeupDelivery({ action, deliveryStatus, reason: parsed.flags.reason, role, threadId: parsed.flags.threadId });
3867
3899
  const timestamp = nowIsoSeconds(options);
3868
3900
  const eventId = emitTelemetrySync(database, {
3869
3901
  actor: "manager",
@@ -4383,9 +4415,16 @@ function validateAppWakeupDelivery(options) {
4383
4415
  }
4384
4416
  return;
4385
4417
  }
4386
- if (sourceStatus !== "blocked_missing_thread") {
4387
- throw new Error(`Cannot record blocked wakeup for ${options.role}; source action is ${sourceStatus}.`);
4418
+ if (sourceStatus === "blocked_missing_thread") {
4419
+ return;
4420
+ }
4421
+ if (sourceStatus === "ready_to_send" && options.action.send_ready === true) {
4422
+ if (!options.reason?.trim()) {
4423
+ throw new Error(`Cannot record blocked wakeup for ${options.role}; ready-to-send source actions require --reason.`);
4424
+ }
4425
+ return;
4388
4426
  }
4427
+ throw new Error(`Cannot record blocked wakeup for ${options.role}; source action is ${sourceStatus}.`);
4389
4428
  }
4390
4429
  function parseAppWakeupSourceActionStatus(value) {
4391
4430
  if (value === "ready_to_send" || value === "skipped_healthy" || value === "blocked_missing_thread") {
@@ -6697,7 +6736,13 @@ function runInstallSkillsCommand(parsed, options) {
6697
6736
  return { exitCode: 0, handled: true, stdout: `${lines.join("\n")}\n` };
6698
6737
  }
6699
6738
  const AGENT_CONVEYOR_PLUGIN_NAME = "agent-conveyor";
6700
- const AGENT_CONVEYOR_PLUGIN_SKILLS = ["conveyor-create-pair", "conveyor-create-worker-set", "conveyor-check-status"];
6739
+ const AGENT_CONVEYOR_PLUGIN_SKILLS = [
6740
+ "conveyor-app-wake-relay",
6741
+ "conveyor-create-pair",
6742
+ "conveyor-create-worker-set",
6743
+ "conveyor-check-status",
6744
+ "conveyor-whats-next-nudger",
6745
+ ];
6701
6746
  function resolveCodexHome(parsed, options) {
6702
6747
  return resolve(expandUserPath(parsed.flags.codexHome ?? options.env?.CODEX_HOME ?? join(homedir(), ".codex")));
6703
6748
  }
@@ -9087,6 +9132,94 @@ function runTaskAckCommand(parsed, options, role) {
9087
9132
  database.close();
9088
9133
  }
9089
9134
  }
9135
+ function runInboxAckCommand(parsed, options) {
9136
+ const unsupported = unsupportedInboxAckOptions(parsed);
9137
+ if (unsupported) {
9138
+ return unsupportedRuntimeResult(parsed, unsupported);
9139
+ }
9140
+ const taskName = requireTask(parsed);
9141
+ const notificationId = parsed.flags.notificationId;
9142
+ if (notificationId === null) {
9143
+ return errorResult("inbox-ack requires --notification-id.");
9144
+ }
9145
+ const database = openRuntimeDatabase(parsed, options);
9146
+ try {
9147
+ const task = taskRowForLifecycle(database, taskName);
9148
+ if (task === null) {
9149
+ throw new Error(`Unknown task: ${taskName}`);
9150
+ }
9151
+ const binding = activeBindingForTaskSync(database, task.name);
9152
+ const notification = notificationForInboxAckSync(database, {
9153
+ notificationId,
9154
+ taskId: task.id,
9155
+ taskName: task.name,
9156
+ });
9157
+ if (notification.binding_id !== binding.binding_id) {
9158
+ throw new Error(`Notification ${notificationId} does not belong to the active binding for task ${task.name}.`);
9159
+ }
9160
+ if (!parsed.flags.fromStdin) {
9161
+ const acknowledgements = notificationAcknowledgementsSync(database, { notificationId, taskId: task.id });
9162
+ return jsonResult({
9163
+ acknowledgements,
9164
+ latest: latestNotificationAcknowledgementSync(database, { notificationId, taskId: task.id }),
9165
+ notification: inboxAckNotificationPayload(notification),
9166
+ task: { id: task.id, name: task.name },
9167
+ });
9168
+ }
9169
+ const role = parsed.flags.role;
9170
+ const status = parseNotificationAcknowledgementStatus(parsed.flags.statusState);
9171
+ if (status instanceof Error) {
9172
+ return errorResult(status.message);
9173
+ }
9174
+ validateInboxAcknowledgement({ notification, role, status });
9175
+ const payload = parseStdinJsonObject(options.stdin);
9176
+ const ack = insertNotificationAcknowledgementSync(database, {
9177
+ bindingId: notification.binding_id,
9178
+ correlationId: parsed.flags.correlationId,
9179
+ notificationId,
9180
+ now: nowIsoSeconds(options),
9181
+ payload,
9182
+ role,
9183
+ status,
9184
+ taskId: task.id,
9185
+ });
9186
+ const eventId = emitTelemetrySync(database, {
9187
+ actor: role,
9188
+ attributes: {
9189
+ binding_id: notification.binding_id,
9190
+ notification_id: notificationId,
9191
+ payload_keys: Object.keys(payload).sort(),
9192
+ role,
9193
+ signal_type: notification.signal_type,
9194
+ status,
9195
+ },
9196
+ correlation: {
9197
+ command: "inbox-ack",
9198
+ correlation_id: parsed.flags.correlationId,
9199
+ notification_id: notificationId,
9200
+ role,
9201
+ },
9202
+ eventType: "dispatch_inbox_ack_recorded",
9203
+ severity: status === "blocked" ? "warning" : "info",
9204
+ summary: `${role} recorded ${status} acknowledgement for notification ${notificationId}.`,
9205
+ taskId: task.id,
9206
+ timestamp: ack.created_at,
9207
+ });
9208
+ return jsonResult({
9209
+ acknowledgement: ack,
9210
+ notification: inboxAckNotificationPayload(notification),
9211
+ receipt: {
9212
+ event_id: eventId,
9213
+ event_type: "dispatch_inbox_ack_recorded",
9214
+ recorded_at: ack.created_at,
9215
+ },
9216
+ task: { id: task.id, name: task.name },
9217
+ });
9218
+ }
9219
+ finally {
9220
+ database.close();
9221
+ }
9222
+ }
9090
9223
  function runSessionInboxCommand(parsed, options, kind) {
9091
9224
  const unsupported = unsupportedSessionInboxOptions(parsed, kind);
9092
9225
  if (unsupported) {
@@ -9336,6 +9469,59 @@ function latestTaskAcknowledgementSync(database, options) {
9336
9469
  task_id: row.task_id,
9337
9470
  };
9338
9471
  }
9472
+ function notificationForInboxAckSync(database, options) {
9473
+ const row = database.prepare(`
9474
+ select rn.id, rn.task_id, rn.binding_id, rn.signal_type, rn.delivered_at,
9475
+ rn.consumed_at, rn.delivery_mode, rn.state, rn.target_session_id,
9476
+ ts.name as target_session_name, ts.role as target_session_role
9477
+ from routed_notifications rn
9478
+ join sessions ts on ts.id = rn.target_session_id
9479
+ where rn.id = ? and rn.task_id = ?
9480
+ limit 1
9481
+ `).get(options.notificationId, options.taskId);
9482
+ if (!row) {
9483
+ throw new Error(`Unknown routed notification for task ${options.taskName}: ${options.notificationId}`);
9484
+ }
9485
+ return row;
9486
+ }
9487
+ function inboxAckNotificationPayload(notification) {
9488
+ return {
9489
+ binding_id: notification.binding_id,
9490
+ consumed_at: notification.consumed_at,
9491
+ delivered_at: notification.delivered_at,
9492
+ delivery_mode: notification.delivery_mode,
9493
+ id: notification.id,
9494
+ signal_type: notification.signal_type,
9495
+ state: notification.state,
9496
+ target_session_id: notification.target_session_id,
9497
+ target_session_name: notification.target_session_name,
9498
+ target_session_role: notification.target_session_role,
9499
+ task_id: notification.task_id,
9500
+ };
9501
+ }
9502
+ function parseNotificationAcknowledgementStatus(value) {
9503
+ if (value === "received" || value === "accepted" || value === "blocked") {
9504
+ return value;
9505
+ }
9506
+ return new Error("inbox-ack --status must be received, accepted, or blocked.");
9507
+ }
9508
+ function validateInboxAcknowledgement(options) {
9509
+ if (options.notification.target_session_role !== options.role) {
9510
+ throw new Error(`Cannot record ${options.role} acknowledgement for notification ${options.notification.id}; target role is ${options.notification.target_session_role}.`);
9511
+ }
9512
+ if (options.notification.state !== "delivered") {
9513
+ throw new Error(`Cannot acknowledge notification ${options.notification.id}; state is ${options.notification.state}, expected delivered.`);
9514
+ }
9515
+ if (options.status === "received") {
9516
+ if (!options.notification.delivered_at) {
9517
+ throw new Error(`Cannot record received acknowledgement for notification ${options.notification.id}; it has no delivered_at receipt.`);
9518
+ }
9519
+ return;
9520
+ }
9521
+ if (!options.notification.consumed_at) {
9522
+ throw new Error(`Cannot record ${options.status} acknowledgement for notification ${options.notification.id}; it has not been consumed by the target session.`);
9523
+ }
9524
+ }
9339
9525
  function sessionInboxResponse(database, options) {
9340
9526
  if (options.timeoutSeconds < 0) {
9341
9527
  throw new Error("--timeout must be non-negative");
@@ -15201,6 +15387,26 @@ function unsupportedTaskAckOptions(parsed) {
15201
15387
  }
15202
15388
  return null;
15203
15389
  }
15390
+ function unsupportedInboxAckOptions(parsed) {
15391
+ if (!parsed.task) {
15392
+ return "inbox-ack requires a task.";
15393
+ }
15394
+ if (parsed.flags.notificationId === null) {
15395
+ return "inbox-ack requires --notification-id.";
15396
+ }
15397
+ if (!parsed.flags.fromStdin && !parsed.flags.json) {
15398
+ return "inbox-ack requires --from-stdin to write or --json to read.";
15399
+ }
15400
+ if (parsed.flags.fromStdin) {
15401
+ if (parsed.flags.role !== "manager" && parsed.flags.role !== "worker") {
15402
+ return "inbox-ack write requires --role manager|worker.";
15403
+ }
15404
+ if (parsed.flags.statusState === null) {
15405
+ return "inbox-ack write requires --status received|accepted|blocked.";
15406
+ }
15407
+ }
15408
+ return null;
15409
+ }
15204
15410
  function unsupportedSessionInboxOptions(parsed, kind) {
15205
15411
  if (!parsed.task) {
15206
15412
  return kind === "session" ? "session-inbox requires a session name." : `${kind}-inbox requires a task.`;
@@ -15718,6 +15924,7 @@ function isDefaultRuntimeCommand(command) {
15718
15924
  || command === "nudge"
15719
15925
  || command === "worker-ack"
15720
15926
  || command === "manager-ack"
15927
+ || command === "inbox-ack"
15721
15928
  || command === "session-inbox"
15722
15929
  || command === "manager-inbox"
15723
15930
  || command === "worker-inbox"