agent-conveyor 0.1.11 → 0.1.12

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
@@ -404,6 +404,15 @@ tmux attach -t codex-live-test
404
404
  `sent` is accepted only when the referenced `app-wakeup-dispatch` receipt has
405
405
  a matching `ready_to_send` action with `send_ready=true` and the same thread
406
406
  id; healthy and blocked roles must be recorded as `skipped` or `blocked`.
407
+ - `app-autopilot start|stop|status TASK [--dispatcher-id ID]
408
+ [--interval SECONDS] [--watch-iterations N] [--stale-after N] [--json]` —
409
+ Manage the pair-level app-native heartbeat policy for the active
410
+ manager/worker binding. `start` and `stop` write telemetry receipts and emit
411
+ the exact manager/worker Codex app heartbeat automation specs plus the
412
+ bounded Dispatch watch command. A plain shell CLI cannot call Codex app
413
+ thread tools, so create/pause those heartbeat automations from a Codex app
414
+ operator session using the emitted specs; Conveyor remains the durable source
415
+ of truth through Dispatch, inboxes, wake receipts, and app heartbeat status.
407
416
  - `discover [QUERY] [--all] [--limit N]` / `search [QUERY]` — Search tasks,
408
417
  registered sessions, active bindings, and recent telemetry in one JSON result.
409
418
  Use this for conversational setup when a manager or Codex session needs to
@@ -5,7 +5,7 @@ import { homedir, tmpdir } from "node:os";
5
5
  import { dirname, join, relative, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { taskAuditSync } from "../runtime/audit.js";
8
- import { appLoopStatusSync, appWakeupDispatchPlanSync, appWakeupPlanSync, directInboxPollCommand, } from "../runtime/app-autonomy.js";
8
+ import { appAutopilotPlanSync, appLoopStatusSync, appWakeupDispatchPlanSync, appWakeupPlanSync, directInboxPollCommand, } from "../runtime/app-autonomy.js";
9
9
  import { classifyBusyWait, classifyStartupOutput } from "../runtime/classify.js";
10
10
  import { exportTaskSync } from "../runtime/export.js";
11
11
  import { ingestSessionSync } from "../runtime/ingest.js";
@@ -134,6 +134,9 @@ export function runTypescriptRuntimeCommand(options) {
134
134
  if (parsed.command === "app-wakeup-record-delivery") {
135
135
  return runAppWakeupRecordDeliveryCommand(parsed, options);
136
136
  }
137
+ if (parsed.command === "app-autopilot") {
138
+ return runAppAutopilotCommand(parsed, options);
139
+ }
137
140
  if (parsed.command === "qa-plan") {
138
141
  return runQaPlanCommand(parsed);
139
142
  }
@@ -385,6 +388,17 @@ function commandHelpText(program, command) {
385
388
  ` ${program} enqueue-nudge-worker my-task --message "Status and evidence?" --path /tmp/work/workerctl.db`,
386
389
  ` ${program} dispatch --once --type nudge_worker --path /tmp/work/workerctl.db`,
387
390
  ],
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]`,
393
+ "",
394
+ "Manage the app-native heartbeat policy for a bound manager/worker pair.",
395
+ "The CLI records policy receipts and emits Codex app heartbeat automation specs; app-thread automation creation still happens through Codex app tools.",
396
+ "",
397
+ "Examples:",
398
+ ` ${program} app-autopilot start dogfood --dispatcher-id dispatch-local --path /tmp/work/workerctl.db --json`,
399
+ ` ${program} app-autopilot status dogfood --path /tmp/work/workerctl.db`,
400
+ ` ${program} app-autopilot stop dogfood --path /tmp/work/workerctl.db --json`,
401
+ ],
388
402
  pair: [
389
403
  `usage: ${program} pair --task <task> --worker-name <worker> --manager-name <manager> [options] ${path}`,
390
404
  "",
@@ -1496,7 +1510,8 @@ function parseRuntimeArgs(args, env) {
1496
1510
  && command !== "app-heartbeat"
1497
1511
  && command !== "app-loop-status"
1498
1512
  && command !== "app-wakeup-plan"
1499
- && command !== "app-wakeup-dispatch") {
1513
+ && command !== "app-wakeup-dispatch"
1514
+ && command !== "app-autopilot") {
1500
1515
  return { command, enabled, error: "Unsupported TypeScript runtime option: --dispatcher-id", explicit, flags, task };
1501
1516
  }
1502
1517
  const value = valueAfter(queue, index, arg);
@@ -2463,7 +2478,11 @@ function parseRuntimeArgs(args, env) {
2463
2478
  index += 1;
2464
2479
  }
2465
2480
  else if (arg === "--stale-after") {
2466
- if (command !== "app-heartbeat" && command !== "app-loop-status" && command !== "app-wakeup-plan" && command !== "app-wakeup-dispatch") {
2481
+ if (command !== "app-heartbeat"
2482
+ && command !== "app-loop-status"
2483
+ && command !== "app-wakeup-plan"
2484
+ && command !== "app-wakeup-dispatch"
2485
+ && command !== "app-autopilot") {
2467
2486
  return { command, enabled, error: "Unsupported TypeScript runtime option: --stale-after", explicit, flags, task };
2468
2487
  }
2469
2488
  const parsedValue = valueAfter(queue, index, arg);
@@ -2676,7 +2695,7 @@ function parseRuntimeArgs(args, env) {
2676
2695
  index += 1;
2677
2696
  }
2678
2697
  else if (arg === "--interval") {
2679
- if (command !== "dispatch" && command !== "session-inbox" && command !== "manager-inbox" && command !== "worker-inbox") {
2698
+ if (command !== "dispatch" && command !== "session-inbox" && command !== "manager-inbox" && command !== "worker-inbox" && command !== "app-autopilot") {
2680
2699
  return { command, enabled, error: "Unsupported TypeScript runtime option: --interval", explicit, flags, task };
2681
2700
  }
2682
2701
  const parsedValue = valueAfter(queue, index, arg);
@@ -2691,7 +2710,7 @@ function parseRuntimeArgs(args, env) {
2691
2710
  index += 1;
2692
2711
  }
2693
2712
  else if (arg === "--watch-iterations") {
2694
- if (command !== "dispatch") {
2713
+ if (command !== "dispatch" && command !== "app-autopilot") {
2695
2714
  return { command, enabled, error: "Unsupported TypeScript runtime option: --watch-iterations", explicit, flags, task };
2696
2715
  }
2697
2716
  const parsedValue = valueAfter(queue, index, arg);
@@ -2824,6 +2843,12 @@ function parseRuntimeArgs(args, env) {
2824
2843
  }
2825
2844
  flags.subtype = arg;
2826
2845
  }
2846
+ else if (command === "app-autopilot" && flags.action === null) {
2847
+ if (!["start", "stop", "status"].includes(arg)) {
2848
+ return { command, enabled, error: `Unsupported app-autopilot action: ${arg}`, explicit, flags, task };
2849
+ }
2850
+ flags.action = arg;
2851
+ }
2827
2852
  else if ((command === "qa-plan" || command === "qa-run") && flags.subtype === null) {
2828
2853
  flags.subtype = arg;
2829
2854
  }
@@ -3693,6 +3718,88 @@ function runAppWakeupRecordDeliveryCommand(parsed, options) {
3693
3718
  database.close();
3694
3719
  }
3695
3720
  }
3721
+ function runAppAutopilotCommand(parsed, options) {
3722
+ const action = parsed.flags.action;
3723
+ if (action !== "start" && action !== "stop" && action !== "status") {
3724
+ return errorResult("app-autopilot requires an action: start, stop, or status");
3725
+ }
3726
+ const taskName = requireTask(parsed);
3727
+ const database = openRuntimeDatabase(parsed, options);
3728
+ try {
3729
+ const timestamp = nowIsoSeconds(options);
3730
+ const dbPath = runtimeDbPath(parsed, options);
3731
+ const dispatcherId = parsed.flags.dispatcherId ?? "dispatch-local";
3732
+ const desiredState = action === "start" ? "active" : action === "stop" ? "stopped" : null;
3733
+ let plan = appAutopilotPlanSync(database, {
3734
+ dbPath,
3735
+ dispatchIntervalSeconds: parsed.flags.intervalSeconds,
3736
+ dispatcherId,
3737
+ desiredState,
3738
+ heartbeatIntervalMinutes: 2,
3739
+ heartbeatStaleSeconds: parsed.flags.appStaleAfterSeconds,
3740
+ now: timestamp,
3741
+ taskName,
3742
+ watchIterations: parsed.flags.watchIterations ?? 1000000,
3743
+ });
3744
+ let receipt = null;
3745
+ if (action === "start" || action === "stop") {
3746
+ const eventType = action === "start" ? "app_autopilot_started" : "app_autopilot_stopped";
3747
+ const eventId = emitTelemetrySync(database, {
3748
+ actor: "operator",
3749
+ attributes: {
3750
+ automation_specs: plan.automation_specs.map((spec) => ({
3751
+ can_create: spec.can_create,
3752
+ interval_minutes: spec.interval_minutes,
3753
+ name: spec.name,
3754
+ role: spec.role,
3755
+ target_thread_id: spec.target_thread_id,
3756
+ target_thread_title: spec.target_thread_title,
3757
+ })),
3758
+ desired_state: desiredState,
3759
+ dispatcher_command: plan.control.dispatcher_command,
3760
+ dispatcher_id: dispatcherId,
3761
+ interval_minutes: plan.interval_minutes,
3762
+ summary: plan.summary,
3763
+ },
3764
+ correlation: {
3765
+ action,
3766
+ command: "app-autopilot",
3767
+ dispatcher_id: dispatcherId,
3768
+ },
3769
+ eventType,
3770
+ severity: plan.summary.blocked_automations > 0 ? "warning" : "info",
3771
+ summary: `App autopilot ${action} for ${plan.task.name}.`,
3772
+ taskId: plan.task.id,
3773
+ timestamp,
3774
+ });
3775
+ receipt = {
3776
+ event_id: eventId,
3777
+ event_type: eventType,
3778
+ recorded_at: timestamp,
3779
+ };
3780
+ plan = {
3781
+ ...plan,
3782
+ last_policy_event: receipt,
3783
+ };
3784
+ }
3785
+ const output = {
3786
+ action,
3787
+ plan,
3788
+ receipt,
3789
+ };
3790
+ if (parsed.flags.json) {
3791
+ return jsonResult(output);
3792
+ }
3793
+ return {
3794
+ exitCode: 0,
3795
+ handled: true,
3796
+ stdout: renderAppAutopilotText(output),
3797
+ };
3798
+ }
3799
+ finally {
3800
+ database.close();
3801
+ }
3802
+ }
3696
3803
  function parseAppWakeupDeliveryStatus(value) {
3697
3804
  if (value === "sent" || value === "skipped" || value === "blocked") {
3698
3805
  return value;
@@ -3803,6 +3910,32 @@ function renderAppWakeupPlanText(plan) {
3803
3910
  }
3804
3911
  return `${lines.join("\n")}\n`;
3805
3912
  }
3913
+ function renderAppAutopilotText(result) {
3914
+ const lines = [
3915
+ `App autopilot ${result.action} for ${result.plan.task.name}: ${result.plan.desired_state}`,
3916
+ `Loop status: ${result.plan.status.ok ? "ok" : "attention required"}`,
3917
+ `Dispatch: ${result.plan.dispatcher.state}${result.plan.dispatcher.required ? " required" : ""}`,
3918
+ `Dispatch command: ${result.plan.control.dispatcher_command}`,
3919
+ `Wake dispatch: ${result.plan.control.wakeup_dispatch_command}`,
3920
+ ];
3921
+ if (result.receipt) {
3922
+ lines.push(`Receipt: ${result.receipt.event_type} ${result.receipt.event_id}`);
3923
+ }
3924
+ else if (result.plan.last_policy_event) {
3925
+ lines.push(`Last policy: ${result.plan.last_policy_event.event_type} ${result.plan.last_policy_event.event_id}`);
3926
+ }
3927
+ else {
3928
+ lines.push("Last policy: unconfigured");
3929
+ }
3930
+ for (const spec of result.plan.automation_specs) {
3931
+ lines.push(`${spec.role} automation: ${spec.can_create ? "ready" : "blocked"} ${spec.name}`, ` thread: ${spec.target_thread_title ?? "(untitled)"} ${spec.target_thread_id ?? "(missing)"}`, ` schedule: ${spec.rrule}`);
3932
+ if (spec.blocker) {
3933
+ lines.push(` blocker: ${spec.blocker}`);
3934
+ }
3935
+ }
3936
+ lines.push(result.plan.control.note);
3937
+ return `${lines.join("\n")}\n`;
3938
+ }
3806
3939
  function boundAppSessionForRoleSync(database, options) {
3807
3940
  const sessionJoin = options.role === "manager" ? "manager_session_id" : "worker_session_id";
3808
3941
  const row = database.prepare(`
@@ -14099,6 +14232,7 @@ function isDefaultRuntimeCommand(command) {
14099
14232
  || command === "app-wakeup-plan"
14100
14233
  || command === "app-wakeup-dispatch"
14101
14234
  || command === "app-wakeup-record-delivery"
14235
+ || command === "app-autopilot"
14102
14236
  || command === "loop-templates"
14103
14237
  || command === "loop-triggers"
14104
14238
  || command === "ralph-loop-presets"