agent-conveyor 0.1.21 → 0.1.23

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.
@@ -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";
@@ -144,6 +144,9 @@ export function runTypescriptRuntimeCommand(options) {
144
144
  if (parsed.command === "app-autopilot") {
145
145
  return runAppAutopilotCommand(parsed, options);
146
146
  }
147
+ if (parsed.command === "app-smoke") {
148
+ return runAppSmokeCommand(parsed, options);
149
+ }
147
150
  if (parsed.command === "qa-plan") {
148
151
  return runQaPlanCommand(parsed);
149
152
  }
@@ -261,6 +264,9 @@ export function runTypescriptRuntimeCommand(options) {
261
264
  if (parsed.command === "manager-ack") {
262
265
  return runTaskAckCommand(parsed, options, "manager");
263
266
  }
267
+ if (parsed.command === "inbox-ack") {
268
+ return runInboxAckCommand(parsed, options);
269
+ }
264
270
  if (parsed.command === "session-inbox") {
265
271
  return runSessionInboxCommand(parsed, options, "session");
266
272
  }
@@ -420,6 +426,15 @@ function commandHelpText(program, command) {
420
426
  "Example JSON:",
421
427
  ` {"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
428
  ],
429
+ "inbox-ack": [
430
+ `usage: ${program} inbox-ack <task> --notification-id N --role manager|worker --status received|accepted|blocked --from-stdin ${path} [--json]`,
431
+ `usage: ${program} inbox-ack <task> --notification-id N --json ${path}`,
432
+ "",
433
+ "Record or read a notification-scoped manager/worker acknowledgement.",
434
+ "",
435
+ "Example JSON:",
436
+ ` {"summary":"Received manager instruction and will run the bounded task.","evidence":[]}`,
437
+ ],
423
438
  nudge: [
424
439
  `usage: ${program} nudge <worker-or-session> <message> ${path} [--dry-run]`,
425
440
  `usage: ${program} session-nudge <session> <message> ${path} [--dry-run]`,
@@ -439,6 +454,18 @@ function commandHelpText(program, command) {
439
454
  ` ${program} app-autopilot status dogfood --path /tmp/work/workerctl.db`,
440
455
  ` ${program} app-autopilot stop dogfood --path /tmp/work/workerctl.db --json`,
441
456
  ],
457
+ "app-smoke": [
458
+ `usage: ${program} app-smoke preflight|start|record|status <task> [--mode required|advisory|skip] [--scope pair|worker-set] [--smoke-id ID] [--nonce NONCE] [--role manager|worker] [--status sent|skipped|received|accepted|blocked] [--thread-id ID] [--notification-id N] [--worker-count N] ${path} [--from-stdin] [--json]`,
459
+ "",
460
+ "Manage a blocking Codex app connection smoke lifecycle for manager/worker bindings.",
461
+ "The CLI records durable smoke receipts; the Codex app/plugin layer still performs live app-thread sends.",
462
+ "",
463
+ "Examples:",
464
+ ` ${program} app-smoke preflight dogfood --mode required --path /tmp/work/workerctl.db --json`,
465
+ ` ${program} app-smoke start dogfood --mode required --path /tmp/work/workerctl.db --json`,
466
+ ` ${program} app-smoke record dogfood --smoke-id smoke-123 --nonce nonce --role worker --status accepted --thread-id thread-worker --from-stdin --path /tmp/work/workerctl.db --json`,
467
+ ` ${program} app-smoke status dogfood --path /tmp/work/workerctl.db --json`,
468
+ ],
442
469
  "app-worker-rotation-plan": [
443
470
  `usage: ${program} app-worker-rotation-plan <task> --old-worker-thread-id ID [--require-handoff] [--reason TEXT] ${path} [--json]`,
444
471
  "",
@@ -584,6 +611,7 @@ function parseRuntimeArgs(args, env) {
584
611
  nextAction: null,
585
612
  noFollowup: false,
586
613
  nextSteps: [],
614
+ notificationId: null,
587
615
  output: null,
588
616
  path: null,
589
617
  pid: null,
@@ -617,6 +645,10 @@ function parseRuntimeArgs(args, env) {
617
645
  subtype: null,
618
646
  summary: null,
619
647
  source: null,
648
+ smokeId: null,
649
+ smokeMode: null,
650
+ smokeNonce: null,
651
+ smokeScope: null,
620
652
  objective: null,
621
653
  promptSummary: null,
622
654
  proof: null,
@@ -722,6 +754,7 @@ function parseRuntimeArgs(args, env) {
722
754
  telemetryView: null,
723
755
  telemetryViewTask: null,
724
756
  workerName: null,
757
+ workerCount: null,
725
758
  search: null,
726
759
  severity: null,
727
760
  staleCycleSeconds: 3600.0,
@@ -1502,7 +1535,7 @@ function parseRuntimeArgs(args, env) {
1502
1535
  index += 1;
1503
1536
  }
1504
1537
  else if (arg === "--from-stdin") {
1505
- if (command !== "criteria-plan" && command !== "continuation" && command !== "worker-ack" && command !== "manager-ack") {
1538
+ if (command !== "criteria-plan" && command !== "continuation" && command !== "worker-ack" && command !== "manager-ack" && command !== "inbox-ack" && command !== "app-smoke") {
1506
1539
  return { command, enabled, error: "Unsupported TypeScript runtime option: --from-stdin", explicit, flags, task };
1507
1540
  }
1508
1541
  flags.fromStdin = true;
@@ -1636,7 +1669,8 @@ function parseRuntimeArgs(args, env) {
1636
1669
  && command !== "app-loop-status"
1637
1670
  && command !== "app-wakeup-plan"
1638
1671
  && command !== "app-wakeup-dispatch"
1639
- && command !== "app-autopilot") {
1672
+ && command !== "app-autopilot"
1673
+ && command !== "app-smoke") {
1640
1674
  return { command, enabled, error: "Unsupported TypeScript runtime option: --dispatcher-id", explicit, flags, task };
1641
1675
  }
1642
1676
  const value = valueAfter(queue, index, arg);
@@ -2034,7 +2068,7 @@ function parseRuntimeArgs(args, env) {
2034
2068
  index += 1;
2035
2069
  }
2036
2070
  else if (arg === "--thread-id") {
2037
- if (command !== "app-wakeup-record-delivery" && command !== "campaign") {
2071
+ if (command !== "app-wakeup-record-delivery" && command !== "campaign" && command !== "app-smoke") {
2038
2072
  return { command, enabled, error: "Unsupported TypeScript runtime option: --thread-id", explicit, flags, task };
2039
2073
  }
2040
2074
  const value = valueAfter(queue, index, arg);
@@ -2312,6 +2346,14 @@ function parseRuntimeArgs(args, env) {
2312
2346
  index += 1;
2313
2347
  continue;
2314
2348
  }
2349
+ if (command === "app-smoke") {
2350
+ if (value !== "required" && value !== "advisory" && value !== "skip") {
2351
+ return { command, enabled, error: `Unsupported app-smoke mode: ${value}`, explicit, flags, task };
2352
+ }
2353
+ flags.smokeMode = value;
2354
+ index += 1;
2355
+ continue;
2356
+ }
2315
2357
  if (!isTranscriptCaptureMode(value)) {
2316
2358
  return { command, enabled, error: `Unsupported transcript capture mode: ${value}`, explicit, flags, task };
2317
2359
  }
@@ -2373,7 +2415,7 @@ function parseRuntimeArgs(args, env) {
2373
2415
  flags.epilogueStatus = true;
2374
2416
  continue;
2375
2417
  }
2376
- if (command !== "criteria" && command !== "runs" && command !== "loop-evidence" && command !== "campaign") {
2418
+ if (command !== "criteria" && command !== "runs" && command !== "loop-evidence" && command !== "campaign" && command !== "inbox-ack" && command !== "app-smoke") {
2377
2419
  return { command, enabled, error: "Unsupported TypeScript runtime option: --status", explicit, flags, task };
2378
2420
  }
2379
2421
  const parsedValue = valueAfter(queue, index, arg);
@@ -2383,11 +2425,66 @@ function parseRuntimeArgs(args, env) {
2383
2425
  if (command === "criteria") {
2384
2426
  flags.statuses.push(parsedValue.value);
2385
2427
  }
2428
+ else if (command === "inbox-ack" || command === "app-smoke") {
2429
+ flags.statusState = parsedValue.value;
2430
+ }
2386
2431
  else {
2387
2432
  flags.statusState = parsedValue.value;
2388
2433
  }
2389
2434
  index += 1;
2390
2435
  }
2436
+ else if (arg === "--notification-id") {
2437
+ if (command !== "inbox-ack" && command !== "app-smoke") {
2438
+ return { command, enabled, error: "Unsupported TypeScript runtime option: --notification-id", explicit, flags, task };
2439
+ }
2440
+ const parsedValue = valueAfter(queue, index, arg);
2441
+ if (parsedValue.error) {
2442
+ return { command, enabled, error: parsedValue.error, explicit, flags, task };
2443
+ }
2444
+ const value = Number(parsedValue.value);
2445
+ if (!Number.isInteger(value) || value <= 0) {
2446
+ return { command, enabled, error: "--notification-id must be a positive integer.", explicit, flags, task };
2447
+ }
2448
+ flags.notificationId = value;
2449
+ index += 1;
2450
+ }
2451
+ else if (arg === "--smoke-id" || arg === "--nonce" || arg === "--scope") {
2452
+ if (command !== "app-smoke") {
2453
+ return { command, enabled, error: `Unsupported TypeScript runtime option: ${arg}`, explicit, flags, task };
2454
+ }
2455
+ const parsedValue = valueAfter(queue, index, arg);
2456
+ if (parsedValue.error) {
2457
+ return { command, enabled, error: parsedValue.error, explicit, flags, task };
2458
+ }
2459
+ if (arg === "--smoke-id") {
2460
+ flags.smokeId = parsedValue.value;
2461
+ }
2462
+ else if (arg === "--nonce") {
2463
+ flags.smokeNonce = parsedValue.value;
2464
+ }
2465
+ else {
2466
+ if (parsedValue.value !== "pair" && parsedValue.value !== "worker-set") {
2467
+ return { command, enabled, error: `Unsupported app-smoke scope: ${parsedValue.value}`, explicit, flags, task };
2468
+ }
2469
+ flags.smokeScope = parsedValue.value;
2470
+ }
2471
+ index += 1;
2472
+ }
2473
+ else if (arg === "--worker-count") {
2474
+ if (command !== "app-smoke") {
2475
+ return { command, enabled, error: "Unsupported TypeScript runtime option: --worker-count", explicit, flags, task };
2476
+ }
2477
+ const parsedValue = valueAfter(queue, index, arg);
2478
+ if (parsedValue.error) {
2479
+ return { command, enabled, error: parsedValue.error, explicit, flags, task };
2480
+ }
2481
+ const value = Number(parsedValue.value);
2482
+ if (!Number.isInteger(value) || value <= 0) {
2483
+ return { command, enabled, error: "--worker-count must be a positive integer.", explicit, flags, task };
2484
+ }
2485
+ flags.workerCount = value;
2486
+ index += 1;
2487
+ }
2391
2488
  else if (arg === "--type") {
2392
2489
  const value = valueAfter(queue, index, arg);
2393
2490
  if (value.error) {
@@ -2537,10 +2634,12 @@ function parseRuntimeArgs(args, env) {
2537
2634
  && command !== "enqueue-continue-iteration"
2538
2635
  && command !== "worker-ack"
2539
2636
  && command !== "manager-ack"
2637
+ && command !== "inbox-ack"
2540
2638
  && command !== "loop-evidence"
2541
2639
  && command !== "continuation"
2542
2640
  && command !== "continuation-reviewer"
2543
- && command !== "epilogue") {
2641
+ && command !== "epilogue"
2642
+ && command !== "app-smoke") {
2544
2643
  return { command, enabled, error: "Unsupported TypeScript runtime option: --correlation-id", explicit, flags, task };
2545
2644
  }
2546
2645
  const value = valueAfter(queue, index, arg);
@@ -3036,6 +3135,12 @@ function parseRuntimeArgs(args, env) {
3036
3135
  }
3037
3136
  flags.action = arg;
3038
3137
  }
3138
+ else if (command === "app-smoke" && flags.action === null) {
3139
+ if (!["preflight", "record", "start", "status"].includes(arg)) {
3140
+ return { command, enabled, error: `Unsupported app-smoke action: ${arg}`, explicit, flags, task };
3141
+ }
3142
+ flags.action = arg;
3143
+ }
3039
3144
  else if (command === "campaign" && flags.action === null) {
3040
3145
  if (!CAMPAIGN_ACTIONS.has(arg)) {
3041
3146
  return { command, enabled, error: unsupportedCampaignActionMessage(arg), explicit, flags, task };
@@ -3863,7 +3968,7 @@ function runAppWakeupRecordDeliveryCommand(parsed, options) {
3863
3968
  if (!action) {
3864
3969
  throw new Error(`Receipt ${source.id} has no ${role} wake action.`);
3865
3970
  }
3866
- validateAppWakeupDelivery({ action, deliveryStatus, role, threadId: parsed.flags.threadId });
3971
+ validateAppWakeupDelivery({ action, deliveryStatus, reason: parsed.flags.reason, role, threadId: parsed.flags.threadId });
3867
3972
  const timestamp = nowIsoSeconds(options);
3868
3973
  const eventId = emitTelemetrySync(database, {
3869
3974
  actor: "manager",
@@ -4326,6 +4431,407 @@ function runAppAutopilotCommand(parsed, options) {
4326
4431
  database.close();
4327
4432
  }
4328
4433
  }
4434
+ function runAppSmokeCommand(parsed, options) {
4435
+ const action = parsed.flags.action;
4436
+ if (action !== "preflight" && action !== "record" && action !== "start" && action !== "status") {
4437
+ return errorResult("app-smoke requires an action: preflight, start, record, or status");
4438
+ }
4439
+ const taskName = requireTask(parsed);
4440
+ const database = openRuntimeDatabase(parsed, options);
4441
+ try {
4442
+ const timestamp = nowIsoSeconds(options);
4443
+ const dbPath = runtimeDbPath(parsed, options);
4444
+ const task = taskRowForDiagnostics(database, taskName);
4445
+ if (action === "preflight") {
4446
+ const output = appSmokePreflightSync(database, {
4447
+ dbPath,
4448
+ mode: parseAppSmokeMode(parsed.flags.smokeMode),
4449
+ scope: parseAppSmokeScope(parsed.flags.smokeScope),
4450
+ task,
4451
+ workerCount: parsed.flags.workerCount ?? 1,
4452
+ });
4453
+ return parsed.flags.json ? jsonResult(output) : { exitCode: 0, handled: true, stdout: renderAppSmokeStatusText(output) };
4454
+ }
4455
+ if (action === "start") {
4456
+ const mode = parseAppSmokeMode(parsed.flags.smokeMode);
4457
+ const scope = parseAppSmokeScope(parsed.flags.smokeScope);
4458
+ const binding = activeBindingForTaskSync(database, task.name);
4459
+ const smokeId = parsed.flags.smokeId ?? `smoke-${randomUUID()}`;
4460
+ const nonce = parsed.flags.smokeNonce ?? `nonce-${randomUUID()}`;
4461
+ const eventId = emitTelemetrySync(database, {
4462
+ actor: "operator",
4463
+ attributes: {
4464
+ binding_id: binding.binding_id,
4465
+ mode,
4466
+ nonce,
4467
+ scope,
4468
+ smoke_id: smokeId,
4469
+ worker_count: parsed.flags.workerCount ?? 1,
4470
+ },
4471
+ correlation: {
4472
+ command: "app-smoke",
4473
+ smoke_id: smokeId,
4474
+ },
4475
+ eventType: mode === "skip" ? "app_smoke_skipped" : "app_smoke_started",
4476
+ severity: mode === "skip" ? "warning" : "info",
4477
+ summary: mode === "skip" ? `App smoke skipped for ${task.name}.` : `App smoke started for ${task.name}.`,
4478
+ taskId: task.id,
4479
+ timestamp,
4480
+ });
4481
+ const status = appSmokeStatusSync(database, {
4482
+ dbPath,
4483
+ now: timestamp,
4484
+ smokeId,
4485
+ staleAfterSeconds: parsed.flags.appStaleAfterSeconds,
4486
+ task,
4487
+ });
4488
+ const output = {
4489
+ receipt: {
4490
+ event_id: eventId,
4491
+ event_type: mode === "skip" ? "app_smoke_skipped" : "app_smoke_started",
4492
+ recorded_at: timestamp,
4493
+ },
4494
+ smoke: {
4495
+ id: smokeId,
4496
+ mode,
4497
+ nonce,
4498
+ scope,
4499
+ worker_count: parsed.flags.workerCount ?? 1,
4500
+ },
4501
+ status,
4502
+ task: { id: task.id, name: task.name },
4503
+ };
4504
+ return parsed.flags.json ? jsonResult(output) : { exitCode: 0, handled: true, stdout: renderAppSmokeStatusText(status) };
4505
+ }
4506
+ if (action === "record") {
4507
+ const smokeId = requiredStringFlag(parsed.flags.smokeId, "--smoke-id");
4508
+ const nonce = requiredStringFlag(parsed.flags.smokeNonce, "--nonce");
4509
+ const role = parseAppSmokeRole(parsed.flags.role);
4510
+ const status = parseAppSmokeRecordStatus(parsed.flags.statusState);
4511
+ if (role instanceof Error) {
4512
+ return errorResult(role.message);
4513
+ }
4514
+ if (status instanceof Error) {
4515
+ return errorResult(status.message);
4516
+ }
4517
+ const session = latestAppSmokeSessionSync(database, { smokeId, taskId: task.id });
4518
+ if (session.nonce !== nonce) {
4519
+ throw new Error(`Smoke nonce mismatch for ${smokeId}; expected ${session.nonce}.`);
4520
+ }
4521
+ validateAppSmokeRecordThread(database, {
4522
+ role,
4523
+ status,
4524
+ taskId: task.id,
4525
+ taskName: task.name,
4526
+ threadId: parsed.flags.threadId,
4527
+ });
4528
+ const payload = parsed.flags.fromStdin ? parseStdinJsonObject(options.stdin) : {};
4529
+ const eventId = emitTelemetrySync(database, {
4530
+ actor: role,
4531
+ attributes: {
4532
+ notification_id: parsed.flags.notificationId,
4533
+ nonce,
4534
+ payload,
4535
+ payload_keys: Object.keys(payload).sort(),
4536
+ role,
4537
+ smoke_id: smokeId,
4538
+ status,
4539
+ thread_id: parsed.flags.threadId,
4540
+ },
4541
+ correlation: {
4542
+ command: "app-smoke",
4543
+ correlation_id: parsed.flags.correlationId,
4544
+ nonce,
4545
+ role,
4546
+ smoke_id: smokeId,
4547
+ },
4548
+ eventType: "app_smoke_receipt_recorded",
4549
+ severity: status === "blocked" ? "warning" : "info",
4550
+ summary: `App smoke ${status} recorded for ${role} on ${task.name}.`,
4551
+ taskId: task.id,
4552
+ timestamp,
4553
+ });
4554
+ const output = {
4555
+ receipt: {
4556
+ event_id: eventId,
4557
+ event_type: "app_smoke_receipt_recorded",
4558
+ recorded_at: timestamp,
4559
+ },
4560
+ smoke: {
4561
+ id: smokeId,
4562
+ nonce,
4563
+ },
4564
+ task: { id: task.id, name: task.name },
4565
+ };
4566
+ return parsed.flags.json ? jsonResult(output) : { exitCode: 0, handled: true, stdout: `App smoke ${status} recorded for ${role}.\n` };
4567
+ }
4568
+ const status = appSmokeStatusSync(database, {
4569
+ dbPath,
4570
+ now: timestamp,
4571
+ smokeId: parsed.flags.smokeId,
4572
+ staleAfterSeconds: parsed.flags.appStaleAfterSeconds,
4573
+ task,
4574
+ });
4575
+ return parsed.flags.json ? jsonResult(status) : { exitCode: 0, handled: true, stdout: renderAppSmokeStatusText(status) };
4576
+ }
4577
+ finally {
4578
+ database.close();
4579
+ }
4580
+ }
4581
+ function appSmokePreflightSync(database, options) {
4582
+ let binding = null;
4583
+ let bindingError = null;
4584
+ try {
4585
+ binding = activeBindingForTaskSync(database, options.task.name);
4586
+ }
4587
+ catch (error) {
4588
+ bindingError = error instanceof Error ? error.message : String(error);
4589
+ }
4590
+ const bound = binding ? appSmokeBoundSessionsSync(database, options.task.id) : null;
4591
+ const checks = [
4592
+ { ok: true, name: "ledger_writable", path: options.dbPath },
4593
+ { ok: true, name: "package_cli_available", command: "app-smoke" },
4594
+ {
4595
+ detail: bindingError,
4596
+ ok: binding !== null,
4597
+ name: "active_binding",
4598
+ },
4599
+ {
4600
+ ok: Boolean(bound?.manager.thread_id),
4601
+ name: "manager_thread_metadata",
4602
+ thread_id: bound?.manager.thread_id ?? null,
4603
+ },
4604
+ {
4605
+ ok: Boolean(bound?.worker.thread_id),
4606
+ name: "worker_thread_metadata",
4607
+ thread_id: bound?.worker.thread_id ?? null,
4608
+ },
4609
+ {
4610
+ ok: false,
4611
+ name: "codex_app_thread_tools",
4612
+ note: "Plain CLI cannot call Codex app thread tools; the plugin skill must perform live sends.",
4613
+ required_in_plugin: true,
4614
+ },
4615
+ ];
4616
+ const blockers = options.mode === "required"
4617
+ ? checks.filter((check) => !check.ok && check.name !== "codex_app_thread_tools").map((check) => `${check.name}${check.detail ? `: ${check.detail}` : ""}`)
4618
+ : [];
4619
+ return {
4620
+ blockers,
4621
+ checks,
4622
+ mode: options.mode,
4623
+ ok: blockers.length === 0,
4624
+ real_work_allowed: options.mode !== "required" || blockers.length === 0,
4625
+ scope: options.scope,
4626
+ task: { id: options.task.id, name: options.task.name },
4627
+ worker_count: options.workerCount,
4628
+ };
4629
+ }
4630
+ function appSmokeStatusSync(database, options) {
4631
+ const smoke = latestAppSmokeSessionSync(database, { smokeId: options.smokeId, taskId: options.task.id });
4632
+ const bound = appSmokeBoundSessionsSync(database, options.task.id);
4633
+ const receipts = appSmokeReceiptsSync(database, { smokeId: smoke.smoke_id, taskId: options.task.id });
4634
+ const manager = appSmokeRoleStatus("manager", bound.manager, receipts, smoke, options);
4635
+ const worker = appSmokeRoleStatus("worker", bound.worker, receipts, smoke, options);
4636
+ const blockers = smoke.mode === "skip" ? [] : [
4637
+ ...appSmokeScopeBlockers(smoke),
4638
+ ...appSmokeRoleBlockers("manager", manager),
4639
+ ...appSmokeRoleBlockers("worker", worker),
4640
+ ];
4641
+ const ok = blockers.length === 0;
4642
+ return {
4643
+ blockers,
4644
+ ok,
4645
+ real_work_allowed: smoke.mode === "skip" || smoke.mode === "advisory" || ok,
4646
+ roles: {
4647
+ manager,
4648
+ worker,
4649
+ },
4650
+ smoke: {
4651
+ id: smoke.smoke_id,
4652
+ mode: smoke.mode,
4653
+ nonce: smoke.nonce,
4654
+ recorded_at: smoke.recorded_at,
4655
+ scope: smoke.scope,
4656
+ worker_count: smoke.worker_count,
4657
+ },
4658
+ task: { id: options.task.id, name: options.task.name },
4659
+ };
4660
+ }
4661
+ function appSmokeRoleStatus(role, session, receipts, smoke, options) {
4662
+ const roleReceipts = receipts.filter((receipt) => receipt.role === role && receipt.nonce === smoke.nonce);
4663
+ const sent = roleReceipts.some((receipt) => receipt.status === "sent");
4664
+ const received = roleReceipts.some((receipt) => receipt.status === "received");
4665
+ const accepted = roleReceipts.some((receipt) => receipt.status === "accepted");
4666
+ const blockedReceipt = roleReceipts.find((receipt) => receipt.status === "blocked");
4667
+ const heartbeatFresh = session.last_heartbeat_at !== null
4668
+ && session.last_heartbeat_at >= smoke.recorded_at
4669
+ && secondsBetweenIso(session.last_heartbeat_at, options.now) <= options.staleAfterSeconds;
4670
+ return {
4671
+ accepted,
4672
+ blocked: Boolean(blockedReceipt),
4673
+ blocker: typeof blockedReceipt?.payload.summary === "string" ? blockedReceipt.payload.summary : null,
4674
+ heartbeat_fresh: heartbeatFresh,
4675
+ last_heartbeat_at: session.last_heartbeat_at,
4676
+ received,
4677
+ sent,
4678
+ session_id: session.session_id,
4679
+ session_name: session.session_name,
4680
+ thread_id: session.thread_id,
4681
+ };
4682
+ }
4683
+ function appSmokeRoleBlockers(role, status) {
4684
+ const blockers = [];
4685
+ if (!status.thread_id) {
4686
+ blockers.push(`${role} has no Codex app thread id`);
4687
+ }
4688
+ if (!status.sent) {
4689
+ blockers.push(`${role} smoke prompt has not been recorded as sent`);
4690
+ }
4691
+ if (!status.heartbeat_fresh) {
4692
+ blockers.push(`${role} heartbeat is not fresh after smoke start`);
4693
+ }
4694
+ if (role === "worker" && !status.received) {
4695
+ blockers.push("worker has not recorded smoke received");
4696
+ }
4697
+ if (!status.accepted) {
4698
+ blockers.push(`${role} has not accepted smoke`);
4699
+ }
4700
+ if (status.blocked) {
4701
+ blockers.push(`${role} smoke blocked${status.blocker ? `: ${status.blocker}` : ""}`);
4702
+ }
4703
+ return blockers;
4704
+ }
4705
+ function appSmokeScopeBlockers(smoke) {
4706
+ if (smoke.scope === "worker-set" && smoke.worker_count > 1) {
4707
+ return [`worker-set smoke for one task proves 1 of ${smoke.worker_count} workers; run per-worker smoke and aggregate in the plugin skill`];
4708
+ }
4709
+ return [];
4710
+ }
4711
+ function latestAppSmokeSessionSync(database, options) {
4712
+ const params = [options.taskId];
4713
+ const smokeClause = options.smokeId ? "and json_extract(attributes_json, '$.smoke_id') = ?" : "";
4714
+ if (options.smokeId) {
4715
+ params.push(options.smokeId);
4716
+ }
4717
+ const row = database.prepare(`
4718
+ select id, timestamp, event_type, attributes_json
4719
+ from telemetry_events
4720
+ where task_id = ?
4721
+ and event_type in ('app_smoke_started', 'app_smoke_skipped')
4722
+ ${smokeClause}
4723
+ order by timestamp desc, id desc
4724
+ limit 1
4725
+ `).get(...params);
4726
+ if (!row) {
4727
+ throw new Error(options.smokeId ? `Unknown app smoke session: ${options.smokeId}` : "No app smoke session has been started for this task.");
4728
+ }
4729
+ const attributes = JSON.parse(row.attributes_json);
4730
+ return {
4731
+ event_id: row.id,
4732
+ mode: attributes.mode,
4733
+ nonce: attributes.nonce,
4734
+ recorded_at: row.timestamp,
4735
+ scope: attributes.scope,
4736
+ smoke_id: attributes.smoke_id,
4737
+ worker_count: attributes.worker_count,
4738
+ };
4739
+ }
4740
+ function appSmokeReceiptsSync(database, options) {
4741
+ const rows = database.prepare(`
4742
+ select attributes_json
4743
+ from telemetry_events
4744
+ where task_id = ?
4745
+ and event_type = 'app_smoke_receipt_recorded'
4746
+ and json_extract(attributes_json, '$.smoke_id') = ?
4747
+ order by timestamp, id
4748
+ `).all(options.taskId, options.smokeId);
4749
+ return rows.map((row) => JSON.parse(row.attributes_json));
4750
+ }
4751
+ function appSmokeBoundSessionsSync(database, taskId) {
4752
+ const row = database.prepare(`
4753
+ select
4754
+ ms.id as manager_session_id,
4755
+ ms.name as manager_session_name,
4756
+ ms.codex_app_thread_id as manager_thread_id,
4757
+ ms.codex_app_thread_title as manager_thread_title,
4758
+ ms.last_heartbeat_at as manager_last_heartbeat_at,
4759
+ ws.id as worker_session_id,
4760
+ ws.name as worker_session_name,
4761
+ ws.codex_app_thread_id as worker_thread_id,
4762
+ ws.codex_app_thread_title as worker_thread_title,
4763
+ ws.last_heartbeat_at as worker_last_heartbeat_at
4764
+ from bindings b
4765
+ join sessions ms on ms.id = b.manager_session_id
4766
+ join sessions ws on ws.id = b.worker_session_id
4767
+ where b.task_id = ?
4768
+ and b.state in ('active', 'ending')
4769
+ order by b.created_at desc
4770
+ limit 1
4771
+ `).get(taskId);
4772
+ if (!row) {
4773
+ throw new Error("No active app smoke binding for task.");
4774
+ }
4775
+ return {
4776
+ manager: {
4777
+ last_heartbeat_at: row.manager_last_heartbeat_at,
4778
+ session_id: row.manager_session_id,
4779
+ session_name: row.manager_session_name,
4780
+ thread_id: row.manager_thread_id,
4781
+ thread_title: row.manager_thread_title,
4782
+ },
4783
+ worker: {
4784
+ last_heartbeat_at: row.worker_last_heartbeat_at,
4785
+ session_id: row.worker_session_id,
4786
+ session_name: row.worker_session_name,
4787
+ thread_id: row.worker_thread_id,
4788
+ thread_title: row.worker_thread_title,
4789
+ },
4790
+ };
4791
+ }
4792
+ function validateAppSmokeRecordThread(database, options) {
4793
+ const bound = appSmokeBoundSessionsSync(database, options.taskId);
4794
+ const expected = options.role === "manager" ? bound.manager.thread_id : bound.worker.thread_id;
4795
+ if (options.status === "sent" && !options.threadId) {
4796
+ throw new Error("app-smoke record --status sent requires --thread-id.");
4797
+ }
4798
+ if (options.threadId && expected !== options.threadId) {
4799
+ throw new Error(`Thread id mismatch for ${options.role}; active binding for ${options.taskName} targets ${expected ?? "(missing)"}.`);
4800
+ }
4801
+ }
4802
+ function parseAppSmokeMode(value) {
4803
+ return value === "advisory" || value === "skip" ? value : "required";
4804
+ }
4805
+ function parseAppSmokeScope(value) {
4806
+ return value === "worker-set" ? "worker-set" : "pair";
4807
+ }
4808
+ function parseAppSmokeRole(value) {
4809
+ if (value === "manager" || value === "worker") {
4810
+ return value;
4811
+ }
4812
+ return new Error("app-smoke record requires --role manager|worker");
4813
+ }
4814
+ function parseAppSmokeRecordStatus(value) {
4815
+ if (value === "accepted" || value === "blocked" || value === "received" || value === "sent" || value === "skipped") {
4816
+ return value;
4817
+ }
4818
+ return new Error("app-smoke record requires --status sent|skipped|received|accepted|blocked");
4819
+ }
4820
+ function secondsBetweenIso(left, right) {
4821
+ return Math.max(0, (Date.parse(right) - Date.parse(left)) / 1000);
4822
+ }
4823
+ function renderAppSmokeStatusText(status) {
4824
+ const smoke = isPlainRecord(status.smoke) ? status.smoke : {};
4825
+ const blockers = Array.isArray(status.blockers) ? status.blockers.map(String) : [];
4826
+ const lines = [
4827
+ `App smoke ${String(smoke.id ?? "(preflight)")}: ${status.ok ? "ok" : "blocked"}`,
4828
+ `Real work allowed: ${String(status.real_work_allowed ?? false)}`,
4829
+ ];
4830
+ for (const blocker of blockers) {
4831
+ lines.push(`Blocker: ${blocker}`);
4832
+ }
4833
+ return `${lines.join("\n")}\n`;
4834
+ }
4329
4835
  function parseAppWakeupDeliveryStatus(value) {
4330
4836
  if (value === "sent" || value === "skipped" || value === "blocked") {
4331
4837
  return value;
@@ -4383,9 +4889,16 @@ function validateAppWakeupDelivery(options) {
4383
4889
  }
4384
4890
  return;
4385
4891
  }
4386
- if (sourceStatus !== "blocked_missing_thread") {
4387
- throw new Error(`Cannot record blocked wakeup for ${options.role}; source action is ${sourceStatus}.`);
4892
+ if (sourceStatus === "blocked_missing_thread") {
4893
+ return;
4894
+ }
4895
+ if (sourceStatus === "ready_to_send" && options.action.send_ready === true) {
4896
+ if (!options.reason?.trim()) {
4897
+ throw new Error(`Cannot record blocked wakeup for ${options.role}; ready-to-send source actions require --reason.`);
4898
+ }
4899
+ return;
4388
4900
  }
4901
+ throw new Error(`Cannot record blocked wakeup for ${options.role}; source action is ${sourceStatus}.`);
4389
4902
  }
4390
4903
  function parseAppWakeupSourceActionStatus(value) {
4391
4904
  if (value === "ready_to_send" || value === "skipped_healthy" || value === "blocked_missing_thread") {
@@ -6698,6 +7211,8 @@ function runInstallSkillsCommand(parsed, options) {
6698
7211
  }
6699
7212
  const AGENT_CONVEYOR_PLUGIN_NAME = "agent-conveyor";
6700
7213
  const AGENT_CONVEYOR_PLUGIN_SKILLS = [
7214
+ "conveyor-app-wake-relay",
7215
+ "conveyor-smoke-app-connections",
6701
7216
  "conveyor-create-pair",
6702
7217
  "conveyor-create-worker-set",
6703
7218
  "conveyor-check-status",
@@ -9092,6 +9607,94 @@ function runTaskAckCommand(parsed, options, role) {
9092
9607
  database.close();
9093
9608
  }
9094
9609
  }
9610
+ function runInboxAckCommand(parsed, options) {
9611
+ const unsupported = unsupportedInboxAckOptions(parsed);
9612
+ if (unsupported) {
9613
+ return unsupportedRuntimeResult(parsed, unsupported);
9614
+ }
9615
+ const taskName = requireTask(parsed);
9616
+ const notificationId = parsed.flags.notificationId;
9617
+ if (notificationId === null) {
9618
+ return errorResult("inbox-ack requires --notification-id.");
9619
+ }
9620
+ const database = openRuntimeDatabase(parsed, options);
9621
+ try {
9622
+ const task = taskRowForLifecycle(database, taskName);
9623
+ if (task === null) {
9624
+ throw new Error(`Unknown task: ${taskName}`);
9625
+ }
9626
+ const binding = activeBindingForTaskSync(database, task.name);
9627
+ const notification = notificationForInboxAckSync(database, {
9628
+ notificationId,
9629
+ taskId: task.id,
9630
+ taskName: task.name,
9631
+ });
9632
+ if (notification.binding_id !== binding.binding_id) {
9633
+ throw new Error(`Notification ${notificationId} does not belong to the active binding for task ${task.name}.`);
9634
+ }
9635
+ if (!parsed.flags.fromStdin) {
9636
+ const acknowledgements = notificationAcknowledgementsSync(database, { notificationId, taskId: task.id });
9637
+ return jsonResult({
9638
+ acknowledgements,
9639
+ latest: latestNotificationAcknowledgementSync(database, { notificationId, taskId: task.id }),
9640
+ notification: inboxAckNotificationPayload(notification),
9641
+ task: { id: task.id, name: task.name },
9642
+ });
9643
+ }
9644
+ const role = parsed.flags.role;
9645
+ const status = parseNotificationAcknowledgementStatus(parsed.flags.statusState);
9646
+ if (status instanceof Error) {
9647
+ return errorResult(status.message);
9648
+ }
9649
+ validateInboxAcknowledgement({ notification, role, status });
9650
+ const payload = parseStdinJsonObject(options.stdin);
9651
+ const ack = insertNotificationAcknowledgementSync(database, {
9652
+ bindingId: notification.binding_id,
9653
+ correlationId: parsed.flags.correlationId,
9654
+ notificationId,
9655
+ now: nowIsoSeconds(options),
9656
+ payload,
9657
+ role,
9658
+ status,
9659
+ taskId: task.id,
9660
+ });
9661
+ const eventId = emitTelemetrySync(database, {
9662
+ actor: role,
9663
+ attributes: {
9664
+ binding_id: notification.binding_id,
9665
+ notification_id: notificationId,
9666
+ payload_keys: Object.keys(payload).sort(),
9667
+ role,
9668
+ signal_type: notification.signal_type,
9669
+ status,
9670
+ },
9671
+ correlation: {
9672
+ command: "inbox-ack",
9673
+ correlation_id: parsed.flags.correlationId,
9674
+ notification_id: notificationId,
9675
+ role,
9676
+ },
9677
+ eventType: "dispatch_inbox_ack_recorded",
9678
+ severity: status === "blocked" ? "warning" : "info",
9679
+ summary: `${role} recorded ${status} acknowledgement for notification ${notificationId}.`,
9680
+ taskId: task.id,
9681
+ timestamp: ack.created_at,
9682
+ });
9683
+ return jsonResult({
9684
+ acknowledgement: ack,
9685
+ notification: inboxAckNotificationPayload(notification),
9686
+ receipt: {
9687
+ event_id: eventId,
9688
+ event_type: "dispatch_inbox_ack_recorded",
9689
+ recorded_at: ack.created_at,
9690
+ },
9691
+ task: { id: task.id, name: task.name },
9692
+ });
9693
+ }
9694
+ finally {
9695
+ database.close();
9696
+ }
9697
+ }
9095
9698
  function runSessionInboxCommand(parsed, options, kind) {
9096
9699
  const unsupported = unsupportedSessionInboxOptions(parsed, kind);
9097
9700
  if (unsupported) {
@@ -9341,6 +9944,59 @@ function latestTaskAcknowledgementSync(database, options) {
9341
9944
  task_id: row.task_id,
9342
9945
  };
9343
9946
  }
9947
+ function notificationForInboxAckSync(database, options) {
9948
+ const row = database.prepare(`
9949
+ select rn.id, rn.task_id, rn.binding_id, rn.signal_type, rn.delivered_at,
9950
+ rn.consumed_at, rn.delivery_mode, rn.state, rn.target_session_id,
9951
+ ts.name as target_session_name, ts.role as target_session_role
9952
+ from routed_notifications rn
9953
+ join sessions ts on ts.id = rn.target_session_id
9954
+ where rn.id = ? and rn.task_id = ?
9955
+ limit 1
9956
+ `).get(options.notificationId, options.taskId);
9957
+ if (!row) {
9958
+ throw new Error(`Unknown routed notification for task ${options.taskName}: ${options.notificationId}`);
9959
+ }
9960
+ return row;
9961
+ }
9962
+ function inboxAckNotificationPayload(notification) {
9963
+ return {
9964
+ binding_id: notification.binding_id,
9965
+ consumed_at: notification.consumed_at,
9966
+ delivered_at: notification.delivered_at,
9967
+ delivery_mode: notification.delivery_mode,
9968
+ id: notification.id,
9969
+ signal_type: notification.signal_type,
9970
+ state: notification.state,
9971
+ target_session_id: notification.target_session_id,
9972
+ target_session_name: notification.target_session_name,
9973
+ target_session_role: notification.target_session_role,
9974
+ task_id: notification.task_id,
9975
+ };
9976
+ }
9977
+ function parseNotificationAcknowledgementStatus(value) {
9978
+ if (value === "received" || value === "accepted" || value === "blocked") {
9979
+ return value;
9980
+ }
9981
+ return new Error("inbox-ack --status must be received, accepted, or blocked.");
9982
+ }
9983
+ function validateInboxAcknowledgement(options) {
9984
+ if (options.notification.target_session_role !== options.role) {
9985
+ throw new Error(`Cannot record ${options.role} acknowledgement for notification ${options.notification.id}; target role is ${options.notification.target_session_role}.`);
9986
+ }
9987
+ if (options.notification.state !== "delivered") {
9988
+ throw new Error(`Cannot acknowledge notification ${options.notification.id}; state is ${options.notification.state}, expected delivered.`);
9989
+ }
9990
+ if (options.status === "received") {
9991
+ if (!options.notification.delivered_at) {
9992
+ throw new Error(`Cannot record received acknowledgement for notification ${options.notification.id}; it has no delivered_at receipt.`);
9993
+ }
9994
+ return;
9995
+ }
9996
+ if (!options.notification.consumed_at) {
9997
+ throw new Error(`Cannot record ${options.status} acknowledgement for notification ${options.notification.id}; it has not been consumed by the target session.`);
9998
+ }
9999
+ }
9344
10000
  function sessionInboxResponse(database, options) {
9345
10001
  if (options.timeoutSeconds < 0) {
9346
10002
  throw new Error("--timeout must be non-negative");
@@ -15206,6 +15862,26 @@ function unsupportedTaskAckOptions(parsed) {
15206
15862
  }
15207
15863
  return null;
15208
15864
  }
15865
+ function unsupportedInboxAckOptions(parsed) {
15866
+ if (!parsed.task) {
15867
+ return "inbox-ack requires a task.";
15868
+ }
15869
+ if (parsed.flags.notificationId === null) {
15870
+ return "inbox-ack requires --notification-id.";
15871
+ }
15872
+ if (!parsed.flags.fromStdin && !parsed.flags.json) {
15873
+ return "inbox-ack requires --from-stdin to write or --json to read.";
15874
+ }
15875
+ if (parsed.flags.fromStdin) {
15876
+ if (parsed.flags.role !== "manager" && parsed.flags.role !== "worker") {
15877
+ return "inbox-ack write requires --role manager|worker.";
15878
+ }
15879
+ if (parsed.flags.statusState === null) {
15880
+ return "inbox-ack write requires --status received|accepted|blocked.";
15881
+ }
15882
+ }
15883
+ return null;
15884
+ }
15209
15885
  function unsupportedSessionInboxOptions(parsed, kind) {
15210
15886
  if (!parsed.task) {
15211
15887
  return kind === "session" ? "session-inbox requires a session name." : `${kind}-inbox requires a task.`;
@@ -15677,6 +16353,7 @@ function isDefaultRuntimeCommand(command) {
15677
16353
  || command === "app-worker-rotation-plan"
15678
16354
  || command === "app-worker-rotation-record"
15679
16355
  || command === "app-autopilot"
16356
+ || command === "app-smoke"
15680
16357
  || command === "loop-templates"
15681
16358
  || command === "loop-triggers"
15682
16359
  || command === "ralph-loop-presets"
@@ -15723,6 +16400,7 @@ function isDefaultRuntimeCommand(command) {
15723
16400
  || command === "nudge"
15724
16401
  || command === "worker-ack"
15725
16402
  || command === "manager-ack"
16403
+ || command === "inbox-ack"
15726
16404
  || command === "session-inbox"
15727
16405
  || command === "manager-inbox"
15728
16406
  || command === "worker-inbox"