@ouro.bot/cli 0.1.0-alpha.673 → 0.1.0-alpha.675

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.
Files changed (65) hide show
  1. package/README.md +11 -5
  2. package/changelog.json +19 -0
  3. package/dist/arc/evolution.js +13 -10
  4. package/dist/arc/obligations.js +1 -1
  5. package/dist/heart/active-work.js +3 -3
  6. package/dist/heart/agent-entry.js +8 -8
  7. package/dist/heart/commitments.js +1 -1
  8. package/dist/heart/core.js +45 -23
  9. package/dist/heart/daemon/agent-config-check.js +24 -0
  10. package/dist/heart/daemon/agent-discovery.js +50 -0
  11. package/dist/heart/daemon/agent-service.js +6 -6
  12. package/dist/heart/daemon/await-private-wake.js +59 -0
  13. package/dist/heart/daemon/cli-exec.js +180 -24
  14. package/dist/heart/daemon/cli-help.js +20 -2
  15. package/dist/heart/daemon/cli-parse.js +45 -2
  16. package/dist/heart/daemon/cli-render.js +1 -0
  17. package/dist/heart/daemon/daemon-entry.js +126 -19
  18. package/dist/heart/daemon/daemon.js +223 -20
  19. package/dist/heart/daemon/habit-private-wake.js +56 -0
  20. package/dist/heart/daemon/health-monitor.js +6 -2
  21. package/dist/heart/daemon/inner-status.js +3 -3
  22. package/dist/heart/daemon/message-router.js +39 -3
  23. package/dist/heart/daemon/process-manager.js +1 -0
  24. package/dist/heart/daemon/pulse.js +43 -7
  25. package/dist/heart/daemon/socket-client.js +14 -2
  26. package/dist/heart/daemon/startup-tui.js +9 -1
  27. package/dist/heart/daemon/thoughts.js +39 -39
  28. package/dist/heart/habits/habit-scheduler.js +24 -4
  29. package/dist/heart/mailbox/mailbox-view.js +1 -1
  30. package/dist/heart/mailbox/readers/agent-machine.js +7 -7
  31. package/dist/heart/primitives.js +1 -1
  32. package/dist/heart/private-runtime/decision-renderer.js +156 -0
  33. package/dist/heart/private-runtime/index.js +18 -0
  34. package/dist/heart/private-runtime/ledger.js +218 -0
  35. package/dist/heart/private-runtime/policy.js +205 -0
  36. package/dist/heart/private-runtime/types.js +2 -0
  37. package/dist/heart/progress-story.js +1 -1
  38. package/dist/heart/session-transcript.js +1 -1
  39. package/dist/heart/tool-activity-callbacks.js +1 -1
  40. package/dist/heart/tool-description.js +2 -2
  41. package/dist/heart/tool-loop.js +2 -2
  42. package/dist/heart/turn-context.js +5 -5
  43. package/dist/mailbox-ui/assets/index-J8zDwHmE.js +1 -0
  44. package/dist/mailbox-ui/index.html +1 -1
  45. package/dist/mailroom/attention.js +1 -1
  46. package/dist/mind/context.js +2 -2
  47. package/dist/mind/pending.js +12 -5
  48. package/dist/mind/prompt.js +32 -25
  49. package/dist/repertoire/coding/feedback.js +25 -4
  50. package/dist/repertoire/tools-awaiting.js +2 -2
  51. package/dist/repertoire/tools-flow.js +4 -4
  52. package/dist/repertoire/tools-record.js +1 -1
  53. package/dist/repertoire/tools-session.js +33 -58
  54. package/dist/repertoire/tools-surface.js +3 -3
  55. package/dist/senses/cli.js +2 -2
  56. package/dist/senses/inner-dialog-worker.js +13 -519
  57. package/dist/senses/inner-dialog.js +10 -961
  58. package/dist/senses/mail.js +21 -5
  59. package/dist/senses/pipeline.js +1 -1
  60. package/dist/senses/private-runtime-worker.js +578 -0
  61. package/dist/senses/private-runtime.js +1145 -0
  62. package/dist/senses/shared-turn.js +1 -1
  63. package/dist/senses/trust-gate.js +2 -2
  64. package/package.json +1 -1
  65. package/dist/mailbox-ui/assets/index-CaTIFDmv.js +0 -1
@@ -60,9 +60,12 @@ const daemon_tombstone_1 = require("./daemon-tombstone");
60
60
  const agent_config_check_1 = require("./agent-config-check");
61
61
  const pulse_1 = require("./pulse");
62
62
  const socket_client_1 = require("./socket-client");
63
+ const await_private_wake_1 = require("./await-private-wake");
64
+ const habit_private_wake_1 = require("./habit-private-wake");
63
65
  const bundle_manifest_1 = require("../../mind/bundle-manifest");
64
66
  const mcp_canary_1 = require("./mcp-canary");
65
67
  const context_loss_sentinel_1 = require("../context-loss-sentinel");
68
+ const provider_credentials_1 = require("../provider-credentials");
66
69
  function parseSocketPath(argv) {
67
70
  const socketIndex = argv.indexOf("--socket");
68
71
  if (socketIndex >= 0) {
@@ -93,6 +96,10 @@ if (mode === "dev") {
93
96
  });
94
97
  }
95
98
  const managedAgents = (0, agent_discovery_1.listEnabledBundleAgents)();
99
+ const managedPrivateRuntimes = managedAgents.map((agent) => ({
100
+ agent,
101
+ config: (0, agent_discovery_1.readPrivateRuntimeConfig)(agent),
102
+ }));
96
103
  function sentinelHealthStatus(receipt) {
97
104
  if (receipt.verdict === "ready")
98
105
  return "ok";
@@ -118,18 +125,65 @@ async function refreshDaemonSentinel(agent, trigger, daemonHealthResults = []) {
118
125
  };
119
126
  }
120
127
  }
128
+ function emitAwaitPrivateWakeDispatchError(options) {
129
+ (0, runtime_1.emitNervesEvent)({
130
+ level: "error",
131
+ component: "daemon",
132
+ event: "daemon.await_private_wake_dispatch_error",
133
+ message: "failed to dispatch await private-runtime wake",
134
+ meta: {
135
+ agent: options.agent,
136
+ awaitName: options.awaitName,
137
+ triggerSource: options.triggerSource,
138
+ socketPath: options.socketPath,
139
+ error: options.error instanceof Error ? options.error.message : String(options.error),
140
+ },
141
+ });
142
+ }
143
+ function emitPulsePrivateWakeDispatchError(options) {
144
+ (0, runtime_1.emitNervesEvent)({
145
+ level: "error",
146
+ component: "daemon",
147
+ event: "daemon.pulse_private_wake_dispatch_error",
148
+ message: "failed to dispatch pulse private-runtime wake",
149
+ meta: {
150
+ agent: options.request.agent,
151
+ triggerSource: options.request.triggerSource,
152
+ idempotencyKey: options.request.idempotencyKey,
153
+ originRefs: options.request.originRefs,
154
+ socketPath: options.socketPath,
155
+ error: options.error instanceof Error ? options.error.message : String(options.error),
156
+ },
157
+ });
158
+ }
159
+ function emitHabitPrivateWakeDispatchError(options) {
160
+ (0, runtime_1.emitNervesEvent)({
161
+ level: "error",
162
+ component: "daemon",
163
+ event: "daemon.habit_private_wake_dispatch_error",
164
+ message: "failed to dispatch habit private-runtime wake",
165
+ meta: {
166
+ agent: options.agent,
167
+ habitName: options.habitName,
168
+ trigger: options.trigger,
169
+ triggerSource: options.triggerSource,
170
+ socketPath: options.socketPath,
171
+ error: options.error instanceof Error ? options.error.message : String(options.error),
172
+ },
173
+ });
174
+ }
121
175
  const processManager = new process_manager_1.DaemonProcessManager({
122
- agents: managedAgents.map((agent) => ({
176
+ agents: managedPrivateRuntimes.map(({ agent, config }) => ({
123
177
  name: agent,
124
178
  entry: "heart/agent-entry.js",
125
- channel: "inner-dialog",
126
- autoStart: true,
179
+ channel: "private-runtime",
180
+ autoStart: config.autoStart,
127
181
  })),
128
182
  existsSync: fs.existsSync,
129
- /* v8 ignore next 4 -- wiring: delegates to checkAgentConfigWithProviderHealth which has full unit tests @preserve */
183
+ /* v8 ignore next 4 -- wiring: delegates to checkAgentConfig which has full unit tests @preserve */
130
184
  configCheck: async (agent) => {
131
185
  const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
132
- return (0, agent_config_check_1.checkAgentConfigWithProviderHealth)(agent, bundlesRoot);
186
+ return (0, agent_config_check_1.checkAgentConfig)(agent, bundlesRoot);
133
187
  },
134
188
  /* v8 ignore start -- pulse flush wiring: integration code; flushPulse itself has full unit tests @preserve */
135
189
  onSnapshotChange: () => {
@@ -138,12 +192,20 @@ const processManager = new process_manager_1.DaemonProcessManager({
138
192
  bundlesRoot: (0, identity_1.getAgentBundlesRoot)(),
139
193
  daemonVersion: (0, bundle_manifest_1.getPackageVersion)(),
140
194
  now: new Date(),
141
- // Default I/O wired into pulse.ts (writePulse, readPulse, etc.)
142
- // Wake recipient: send inner.wake over the daemon's own socket so
143
- // the recipient agent runs an inner-dialog turn that picks up the
144
- // pulse alert. Catch errors silently — pulse is best-effort.
145
- fireInnerWake: (agent) => {
146
- (0, socket_client_1.sendDaemonCommand)(socketPath, { kind: "inner.wake", agent }).catch(() => { });
195
+ // Default I/O wired into pulse.ts (writePulse, readPulse, etc.).
196
+ // Pulse alerts queue canonical private-runtime wakes so policy decides
197
+ // whether a model-backed notification turn may run.
198
+ firePrivateWake: (request) => {
199
+ (0, socket_client_1.sendDaemonCommand)(socketPath, {
200
+ kind: "private.wake",
201
+ ...request,
202
+ }).catch((error) => {
203
+ emitPulsePrivateWakeDispatchError({
204
+ request,
205
+ socketPath,
206
+ error,
207
+ });
208
+ });
147
209
  },
148
210
  });
149
211
  },
@@ -266,7 +328,7 @@ const degradedComponents = [];
266
328
  function buildDaemonHealthState() {
267
329
  const snapshots = processManager.listAgentSnapshots();
268
330
  const agentDegradedComponents = snapshots
269
- .filter((snapshot) => snapshot.status !== "running")
331
+ .filter((snapshot) => snapshot.status !== "running" && snapshot.autoStart !== false)
270
332
  .map((snapshot) => {
271
333
  const reasonParts = [
272
334
  snapshot.errorReason ?? `${snapshot.channel} is ${snapshot.status}`,
@@ -305,7 +367,9 @@ function buildDaemonHealthState() {
305
367
  const rollupStatus = (0, daemon_rollup_1.computeDaemonRollup)({
306
368
  enabledAgents: snapshots.map((snapshot) => ({
307
369
  name: snapshot.name,
308
- status: snapshot.status,
370
+ // A parked non-autostart worker is intentionally not serving; it should
371
+ // remain visible in worker status but should not degrade daemon health.
372
+ status: snapshot.autoStart === false ? "running" : snapshot.status,
309
373
  })),
310
374
  bootstrapDegraded: degradedComponents,
311
375
  safeMode: false,
@@ -394,8 +458,11 @@ function writeStopCommandHealthState() {
394
458
  // Health writes are best-effort during shutdown.
395
459
  }
396
460
  }
461
+ async function preloadProviderCredentialPools() {
462
+ await Promise.all(managedAgents.map((agent) => (0, provider_credentials_1.refreshProviderCredentialPool)(agent, { preserveCachedOnFailure: true })));
463
+ }
397
464
  /* v8 ignore start -- habit wiring: lambdas delegate to processManager/fs; tested via HabitScheduler unit tests @preserve */
398
- void daemon.start().then(async () => {
465
+ void preloadProviderCredentialPools().then(() => daemon.start()).then(async () => {
399
466
  const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
400
467
  const ouroPath = (0, os_cron_deps_1.resolveOuroBinaryPath)();
401
468
  const osCronDeps = (0, os_cron_deps_1.createRealOsCronDeps)();
@@ -412,8 +479,24 @@ void daemon.start().then(async () => {
412
479
  agent,
413
480
  habitsDir,
414
481
  osCronManager,
415
- onHabitFire: (habitName, trigger) => {
416
- processManager.sendToAgent(agent, { type: "habit", habitName, trigger });
482
+ onHabitFire: (habitName, trigger, context) => {
483
+ const command = (0, habit_private_wake_1.buildHabitPrivateWakeCommand)({
484
+ agent,
485
+ habitName,
486
+ trigger,
487
+ sourceRef: { kind: "daemon-entry", id: "habit-scheduler" },
488
+ occurrenceId: context?.occurrenceId,
489
+ });
490
+ (0, socket_client_1.sendDaemonCommand)(socketPath, command).catch((error) => {
491
+ emitHabitPrivateWakeDispatchError({
492
+ agent,
493
+ habitName,
494
+ trigger,
495
+ triggerSource: command.triggerSource,
496
+ socketPath,
497
+ error,
498
+ });
499
+ });
417
500
  },
418
501
  deps: {
419
502
  readdir: (dir) => fs.readdirSync(dir),
@@ -472,7 +555,19 @@ void daemon.start().then(async () => {
472
555
  awaitsDir,
473
556
  osCronManager: awaitOsCronManager,
474
557
  onAwaitFire: (awaitName) => {
475
- processManager.sendToAgent(agent, { type: "await", awaitName });
558
+ (0, socket_client_1.sendDaemonCommand)(socketPath, (0, await_private_wake_1.buildAwaitPrivateWakeCommand)({
559
+ agent,
560
+ awaitName,
561
+ triggerSource: "await-scheduler",
562
+ })).catch((error) => {
563
+ emitAwaitPrivateWakeDispatchError({
564
+ agent,
565
+ awaitName,
566
+ triggerSource: "await-scheduler",
567
+ socketPath,
568
+ error,
569
+ });
570
+ });
476
571
  },
477
572
  onAwaitExpire: (awaitName) => {
478
573
  void (0, await_expiry_1.archiveAndAlertExpiredAwait)({
@@ -482,8 +577,20 @@ void daemon.start().then(async () => {
482
577
  deliveryDeps: {
483
578
  agentName: agent,
484
579
  queuePending: () => {
485
- // Best-effort: queue inner-dialog wake so the agent processes the alert path
486
- (0, socket_client_1.sendDaemonCommand)(socketPath, { kind: "inner.wake", agent }).catch(() => { });
580
+ // Best-effort: queue private-runtime wake so the agent processes the alert path.
581
+ (0, socket_client_1.sendDaemonCommand)(socketPath, (0, await_private_wake_1.buildAwaitPrivateWakeCommand)({
582
+ agent,
583
+ awaitName,
584
+ triggerSource: "await-expiry",
585
+ })).catch((error) => {
586
+ emitAwaitPrivateWakeDispatchError({
587
+ agent,
588
+ awaitName,
589
+ triggerSource: "await-expiry",
590
+ socketPath,
591
+ error,
592
+ });
593
+ });
487
594
  },
488
595
  },
489
596
  }).catch((err) => {
@@ -66,8 +66,13 @@ const mailbox_types_1 = require("../mailbox/mailbox-types");
66
66
  const mailbox_read_1 = require("../mailbox/mailbox-read");
67
67
  const mailbox_view_1 = require("../mailbox/mailbox-view");
68
68
  const provider_visibility_1 = require("../provider-visibility");
69
+ const private_runtime_1 = require("../private-runtime");
69
70
  const socket_client_1 = require("./socket-client");
70
71
  const flight_recorder_1 = require("../../arc/flight-recorder");
72
+ const await_private_wake_1 = require("./await-private-wake");
73
+ const habit_private_wake_1 = require("./habit-private-wake");
74
+ const habit_parser_1 = require("../habits/habit-parser");
75
+ const habit_runtime_state_1 = require("../habits/habit-runtime-state");
71
76
  const PIDFILE_PATH = path.join(os.homedir(), ".ouro-cli", "daemon.pids");
72
77
  /**
73
78
  * Defense-in-depth: detect if we're running under vitest. The pidfile lives
@@ -374,6 +379,7 @@ function buildWorkerRows(snapshots) {
374
379
  return snapshots.map((snapshot) => ({
375
380
  agent: snapshot.name,
376
381
  worker: snapshot.channel,
382
+ autoStart: snapshot.autoStart ?? true,
377
383
  status: snapshot.status,
378
384
  pid: snapshot.pid,
379
385
  restartCount: snapshot.restartCount,
@@ -395,11 +401,17 @@ function unhealthySenseRows(senses) {
395
401
  return true;
396
402
  });
397
403
  }
404
+ function isParkedWorker(row) {
405
+ return row.autoStart === false && row.status === "stopped";
406
+ }
407
+ function unhealthyWorkerRows(workers) {
408
+ return workers.filter((row) => !isParkedWorker(row) && row.status !== "running");
409
+ }
398
410
  function unhealthyHealthChecks(healthChecks) {
399
411
  return healthChecks.filter((row) => row.status !== "ok");
400
412
  }
401
413
  function overviewHealth(workers, senses, healthChecks = []) {
402
- if (!workers.every((worker) => worker.status === "running"))
414
+ if (unhealthyWorkerRows(workers).length > 0)
403
415
  return "warn";
404
416
  if (unhealthySenseRows(senses).length > 0)
405
417
  return "warn";
@@ -412,8 +424,7 @@ function formatStatusSummary(payload) {
412
424
  return "no managed agents";
413
425
  }
414
426
  const degraded = [
415
- ...payload.workers
416
- .filter((row) => row.status !== "running")
427
+ ...unhealthyWorkerRows(payload.workers)
417
428
  .map((row) => `worker:${row.agent}/${row.worker}:${row.status}`),
418
429
  ...unhealthySenseRows(payload.senses)
419
430
  .map((row) => `sense:${row.agent}/${row.sense}:${row.status}`),
@@ -456,6 +467,27 @@ function isValidSenseReviveCommand(command) {
456
467
  && typeof command.sense === "string"
457
468
  && typeof command.reason === "string";
458
469
  }
470
+ function isValidTaskPokeCommand(command) {
471
+ return typeof command.agent === "string"
472
+ && command.agent.trim().length > 0
473
+ && typeof command.taskId === "string"
474
+ && command.taskId.trim().length > 0;
475
+ }
476
+ function isValidHabitPokeCommand(command) {
477
+ return typeof command.agent === "string"
478
+ && command.agent.trim().length > 0
479
+ && typeof command.habitName === "string"
480
+ && command.habitName.trim().length > 0;
481
+ }
482
+ function taskIdFromTaskPokePrivateWakeCommand(command) {
483
+ if (command.kind !== "private.wake" || command.triggerSource !== "task-poke")
484
+ return null;
485
+ const taskRef = command.originRefs?.find((ref) => ref.kind === "task" && typeof ref.id === "string");
486
+ if (!taskRef || typeof taskRef.id !== "string")
487
+ return null;
488
+ const trimmed = taskRef.id.trim();
489
+ return trimmed.length > 0 ? trimmed : null;
490
+ }
459
491
  /**
460
492
  * Handle agent.senseTurn command: runs a full agent turn via the daemon process.
461
493
  * Dynamic import lazy-loads shared-turn. Hot-reload works because ouro dev
@@ -523,6 +555,7 @@ class OuroDaemon {
523
555
  socketIdentity = null;
524
556
  senseAutostartTimer = null;
525
557
  mailboxServerFactory;
558
+ privateRuntimePolicyDeps;
526
559
  onStopCommandComplete;
527
560
  constructor(options) {
528
561
  this.socketPath = options.socketPath;
@@ -534,6 +567,7 @@ class OuroDaemon {
534
567
  this.bundlesRoot = options.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
535
568
  this.mode = options.mode ?? "production";
536
569
  this.mailboxServerFactory = options.mailboxServerFactory ?? this.createDefaultMailboxServer.bind(this);
570
+ this.privateRuntimePolicyDeps = options.privateRuntimePolicyDeps ?? {};
537
571
  this.onStopCommandComplete = options.onStopCommandComplete ?? null;
538
572
  }
539
573
  /* v8 ignore start -- default mailbox server wiring: production-only path, tests inject mailboxServerFactory stub instead. startMailboxHttpServer itself has full coverage in mailbox-http.test.ts @preserve */
@@ -1078,6 +1112,123 @@ class OuroDaemon {
1078
1112
  }
1079
1113
  /* v8 ignore stop */
1080
1114
  }
1115
+ hasManagedPrivateRuntime(agent) {
1116
+ return this.processManager.listAgentSnapshots().some((snapshot) => snapshot.name === agent && snapshot.channel === "private-runtime");
1117
+ }
1118
+ buildPrivateRuntimeWakeRequest(command) {
1119
+ if (command.kind === "inner.wake") {
1120
+ return {
1121
+ agent: command.agent,
1122
+ origin: "daemon.private.wake",
1123
+ reason: "legacy inner.wake compatibility alias",
1124
+ providerLane: "inner",
1125
+ triggerSource: "legacy",
1126
+ budgetClass: "interactive",
1127
+ originRefs: [{ kind: "daemon-command", id: "inner.wake" }],
1128
+ };
1129
+ }
1130
+ return {
1131
+ agent: command.agent,
1132
+ origin: "daemon.private.wake",
1133
+ reason: command.reason ?? "manual private-runtime wake",
1134
+ providerLane: "inner",
1135
+ triggerSource: command.triggerSource ?? "manual",
1136
+ budgetClass: command.budgetClass ?? "interactive",
1137
+ ...(command.idempotencyKey ? { idempotencyKey: command.idempotencyKey } : {}),
1138
+ originRefs: command.originRefs ?? [{ kind: "daemon-command", id: "private.wake" }],
1139
+ };
1140
+ }
1141
+ buildAwaitPrivateWakeCommand(command) {
1142
+ return (0, await_private_wake_1.buildAwaitPrivateWakeCommand)({
1143
+ agent: command.agent,
1144
+ awaitName: command.awaitName,
1145
+ triggerSource: "await-poke",
1146
+ });
1147
+ }
1148
+ buildTaskPokePrivateWakeCommand(command, receiptId) {
1149
+ return {
1150
+ kind: "private.wake",
1151
+ agent: command.agent,
1152
+ reason: `task poke ${command.taskId}`,
1153
+ triggerSource: "task-poke",
1154
+ budgetClass: "scheduled",
1155
+ idempotencyKey: `task-poke:${command.agent}:${command.taskId}:${receiptId}`,
1156
+ originRefs: [
1157
+ { kind: "task", id: command.taskId },
1158
+ { kind: "queue-receipt", id: receiptId },
1159
+ { kind: "daemon-command", id: "task.poke" },
1160
+ ],
1161
+ };
1162
+ }
1163
+ resolveHabitPokeOccurrenceId(command, trigger) {
1164
+ if (trigger === "poke" || trigger === "manual")
1165
+ return undefined;
1166
+ const agentRoot = path.join(this.bundlesRoot, `${command.agent}.ouro`);
1167
+ const habitPath = path.join(agentRoot, "habits", `${command.habitName}.md`);
1168
+ if (!fs.existsSync(habitPath))
1169
+ return { skipReason: "habit file not found" };
1170
+ const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath));
1171
+ if (habit.cadence === null)
1172
+ return { skipReason: "habit has no cadence" };
1173
+ const cadence = habit.cadence;
1174
+ if (habit.lastRun === null)
1175
+ return `${trigger}:first-run:${cadence}`;
1176
+ return `${trigger}:last-run:${habit.lastRun}:cadence:${cadence}`;
1177
+ }
1178
+ buildPrivateRuntimeWorkerWakeMessage(command, decision) {
1179
+ const awaitName = (0, await_private_wake_1.awaitNameFromPrivateWakeCommand)(command);
1180
+ if (awaitName) {
1181
+ return {
1182
+ type: "await",
1183
+ awaitName,
1184
+ privateTurnDecision: decision,
1185
+ };
1186
+ }
1187
+ const taskId = taskIdFromTaskPokePrivateWakeCommand(command);
1188
+ if (taskId) {
1189
+ return {
1190
+ type: "poke",
1191
+ taskId,
1192
+ privateTurnDecision: decision,
1193
+ };
1194
+ }
1195
+ const habitMessage = (0, habit_private_wake_1.habitMessageFromPrivateWakeCommand)(command);
1196
+ if (habitMessage) {
1197
+ return {
1198
+ type: "habit",
1199
+ habitName: habitMessage.habitName,
1200
+ trigger: habitMessage.trigger,
1201
+ privateTurnDecision: decision,
1202
+ };
1203
+ }
1204
+ return { type: "message", privateTurnDecision: decision };
1205
+ }
1206
+ async handlePrivateRuntimeWake(command) {
1207
+ if (!this.hasManagedPrivateRuntime(command.agent)) {
1208
+ return {
1209
+ ok: false,
1210
+ error: `No managed agent '${command.agent}' is registered with daemon-managed private runtime.`,
1211
+ };
1212
+ }
1213
+ const decision = await (0, private_runtime_1.requestPrivateTurnDecision)(this.buildPrivateRuntimeWakeRequest(command), {
1214
+ bundlesRoot: this.bundlesRoot,
1215
+ ...this.privateRuntimePolicyDeps,
1216
+ });
1217
+ if (!decision.executable) {
1218
+ return {
1219
+ ok: true,
1220
+ message: `private-runtime wake denied for ${command.agent}: ${decision.deniedReason}`,
1221
+ data: { decision },
1222
+ };
1223
+ }
1224
+ await this.processManager.startAgent(command.agent);
1225
+ this.processManager.sendToAgent?.(command.agent, this.buildPrivateRuntimeWorkerWakeMessage(command, decision));
1226
+ return {
1227
+ ok: true,
1228
+ message: `woke private runtime for ${command.agent}`,
1229
+ data: { decision },
1230
+ };
1231
+ }
1081
1232
  async handleCommandInner(command) {
1082
1233
  switch (command.kind) {
1083
1234
  case "daemon.start":
@@ -1252,11 +1403,11 @@ class OuroDaemon {
1252
1403
  // (cli-exec.ts) intentionally sends only message.send for tool-use
1253
1404
  // events to avoid waking the agent on every tool call. The hook's
1254
1405
  // intent was completely defeated by this handler calling
1255
- // `sendToAgent({type: "message"})`, which woke the inner-dialog
1406
+ // `sendToAgent({type: "message"})`, which woke the private runtime
1256
1407
  // worker on EVERY message.send anyway. ~30 message.send/min × the
1257
1408
  // 3-turn instinct-loop cap = ~90 turns/min sustained for hours.
1258
1409
  //
1259
- // Callers that want immediate processing must send `inner.wake`
1410
+ // Callers that want immediate processing must send `private.wake`
1260
1411
  // explicitly after message.send. The CLI `ouro msg` does so
1261
1412
  // (lifecycle-boundary delivery should wake); the hook does so
1262
1413
  // only on session-start / stop, not per tool-use; the API does
@@ -1279,13 +1430,42 @@ class OuroDaemon {
1279
1430
  data: messages,
1280
1431
  };
1281
1432
  }
1282
- case "inner.wake":
1283
- await this.processManager.startAgent(command.agent);
1284
- this.processManager.sendToAgent?.(command.agent, { type: "message" });
1433
+ case "private.decisions": {
1434
+ const requestedLimit = Number.isInteger(command.limit) && command.limit && command.limit > 0 ? command.limit : 20;
1435
+ const limit = Math.min(requestedLimit, 1000);
1436
+ const ledgerPath = (0, private_runtime_1.privateTurnLedgerPath)(command.agent, { bundlesRoot: this.bundlesRoot });
1437
+ const ledgerExists = fs.existsSync(ledgerPath);
1438
+ let decisions;
1439
+ try {
1440
+ decisions = ledgerExists ? (0, private_runtime_1.readPrivateTurnLedger)(ledgerPath) : [];
1441
+ }
1442
+ catch {
1443
+ return {
1444
+ ok: false,
1445
+ error: `private-runtime decision ledger is malformed: ${ledgerPath}; repair or remove malformed JSONL rows before reading decisions`,
1446
+ };
1447
+ }
1448
+ const guidance = ledgerExists
1449
+ ? undefined
1450
+ : `No private-runtime decision ledger exists for ${command.agent}; run an explicit private-runtime trigger or check ${path.dirname(ledgerPath)}.`;
1451
+ const payload = (0, private_runtime_1.buildPrivateDecisionReadPayload)({
1452
+ agent: command.agent,
1453
+ ledgerPath,
1454
+ decisions,
1455
+ limit,
1456
+ ...(guidance ? { guidance } : {}),
1457
+ });
1285
1458
  return {
1286
1459
  ok: true,
1287
- message: `woke inner dialog for ${command.agent}`,
1460
+ summary: (0, private_runtime_1.privateDecisionCountSummary)(payload.decisions.length),
1461
+ ...(guidance ? { message: guidance } : {}),
1462
+ data: payload,
1288
1463
  };
1464
+ }
1465
+ case "inner.wake":
1466
+ return this.handlePrivateRuntimeWake(command);
1467
+ case "private.wake":
1468
+ return this.handlePrivateRuntimeWake(command);
1289
1469
  case "chat.connect":
1290
1470
  await this.processManager.startAgent(command.agent);
1291
1471
  return {
@@ -1293,6 +1473,12 @@ class OuroDaemon {
1293
1473
  message: `connected to ${command.agent}`,
1294
1474
  };
1295
1475
  case "task.poke": {
1476
+ if (!isValidTaskPokeCommand(command)) {
1477
+ return {
1478
+ ok: false,
1479
+ error: "Invalid task.poke payload: expected non-empty string fields 'agent' and 'taskId'.",
1480
+ };
1481
+ }
1296
1482
  const receipt = await this.router.send({
1297
1483
  from: "ouro-poke",
1298
1484
  to: command.agent,
@@ -1301,7 +1487,7 @@ class OuroDaemon {
1301
1487
  taskRef: command.taskId,
1302
1488
  });
1303
1489
  await this.scheduler.recordTaskRun?.(command.agent, command.taskId);
1304
- this.processManager.sendToAgent?.(command.agent, { type: "poke", taskId: command.taskId });
1490
+ await this.handlePrivateRuntimeWake(this.buildTaskPokePrivateWakeCommand(command, receipt.id));
1305
1491
  return {
1306
1492
  ok: true,
1307
1493
  message: `queued poke ${receipt.id}`,
@@ -1309,22 +1495,39 @@ class OuroDaemon {
1309
1495
  };
1310
1496
  }
1311
1497
  case "habit.poke": {
1498
+ if (!isValidHabitPokeCommand(command)) {
1499
+ return {
1500
+ ok: false,
1501
+ error: "Invalid habit.poke payload: expected non-empty string fields 'agent' and 'habitName'.",
1502
+ };
1503
+ }
1312
1504
  const trigger = command.trigger ?? "poke";
1313
1505
  if (!(0, flight_recorder_1.isHabitRunTrigger)(trigger)) {
1314
1506
  return { ok: false, error: `invalid habit trigger: ${String(trigger)}` };
1315
1507
  }
1316
- this.processManager.sendToAgent?.(command.agent, { type: "habit", habitName: command.habitName, trigger });
1317
- return {
1318
- ok: true,
1319
- message: `poked habit ${command.habitName} for ${command.agent}`,
1320
- };
1508
+ if (!this.hasManagedPrivateRuntime(command.agent)) {
1509
+ return {
1510
+ ok: false,
1511
+ error: `No managed agent '${command.agent}' is registered with daemon-managed private runtime.`,
1512
+ };
1513
+ }
1514
+ const occurrenceId = this.resolveHabitPokeOccurrenceId(command, trigger);
1515
+ if (typeof occurrenceId === "object") {
1516
+ return {
1517
+ ok: true,
1518
+ message: `skipped scheduled habit ${command.habitName} for ${command.agent}: ${occurrenceId.skipReason}`,
1519
+ };
1520
+ }
1521
+ return this.handlePrivateRuntimeWake((0, habit_private_wake_1.buildHabitPrivateWakeCommand)({
1522
+ agent: command.agent,
1523
+ habitName: command.habitName,
1524
+ trigger,
1525
+ sourceRef: { kind: "daemon-command", id: "habit.poke" },
1526
+ occurrenceId,
1527
+ }));
1321
1528
  }
1322
1529
  case "await.poke": {
1323
- this.processManager.sendToAgent?.(command.agent, { type: "await", awaitName: command.awaitName });
1324
- return {
1325
- ok: true,
1326
- message: `poked await ${command.awaitName} for ${command.agent}`,
1327
- };
1530
+ return this.handlePrivateRuntimeWake(this.buildAwaitPrivateWakeCommand(command));
1328
1531
  }
1329
1532
  case "mcp.list": {
1330
1533
  (0, identity_1.setAgentName)(command.agent ?? "default");
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildHabitPrivateWakeCommand = buildHabitPrivateWakeCommand;
4
+ exports.habitMessageFromPrivateWakeCommand = habitMessageFromPrivateWakeCommand;
5
+ const flight_recorder_1 = require("../../arc/flight-recorder");
6
+ const runtime_1 = require("../../nerves/runtime");
7
+ function buildHabitPrivateWakeCommand(options) {
8
+ const firedAt = (options.now ?? (() => new Date()))().toISOString();
9
+ const occurrenceId = options.occurrenceId ?? firedAt;
10
+ const triggerSource = `habit-${options.trigger}`;
11
+ (0, runtime_1.emitNervesEvent)({
12
+ component: "daemon",
13
+ event: "daemon.habit_private_wake_built",
14
+ message: "built habit private-runtime wake command",
15
+ meta: {
16
+ agent: options.agent,
17
+ habitName: options.habitName,
18
+ trigger: options.trigger,
19
+ triggerSource,
20
+ occurrenceId,
21
+ },
22
+ });
23
+ return {
24
+ kind: "private.wake",
25
+ agent: options.agent,
26
+ reason: `habit ${options.habitName} fired by ${options.trigger}`,
27
+ triggerSource,
28
+ budgetClass: "scheduled",
29
+ idempotencyKey: `habit:${options.agent}:${options.habitName}:${options.trigger}:${occurrenceId}`,
30
+ originRefs: [
31
+ { kind: "habit", id: options.habitName },
32
+ { kind: "habit-trigger", id: options.trigger },
33
+ { kind: "habit-occurrence", id: occurrenceId },
34
+ options.sourceRef,
35
+ ],
36
+ };
37
+ }
38
+ function habitMessageFromPrivateWakeCommand(command) {
39
+ if (command.kind !== "private.wake")
40
+ return null;
41
+ if (typeof command.triggerSource !== "string" || !command.triggerSource.startsWith("habit-"))
42
+ return null;
43
+ const habitRef = command.originRefs?.find((ref) => ref.kind === "habit" && typeof ref.id === "string");
44
+ const triggerRef = command.originRefs?.find((ref) => ref.kind === "habit-trigger" && typeof ref.id === "string");
45
+ if (!habitRef || typeof habitRef.id !== "string" || !triggerRef || typeof triggerRef.id !== "string")
46
+ return null;
47
+ const habitName = habitRef.id.trim();
48
+ const trigger = triggerRef.id.trim();
49
+ if (habitName.length === 0)
50
+ return null;
51
+ if (!(0, flight_recorder_1.isHabitRunTrigger)(trigger))
52
+ return null;
53
+ if (command.triggerSource !== `habit-${trigger}`)
54
+ return null;
55
+ return { habitName, trigger };
56
+ }
@@ -69,7 +69,8 @@ class HealthMonitor {
69
69
  async runChecks() {
70
70
  const results = [];
71
71
  const snapshots = this.processManager.listAgentSnapshots();
72
- const unhealthy = snapshots.filter((snapshot) => snapshot.status !== "running");
72
+ const unhealthy = snapshots.filter((snapshot) => snapshot.status !== "running" && snapshot.autoStart !== false);
73
+ const parked = snapshots.filter((snapshot) => snapshot.status !== "running" && snapshot.autoStart === false);
73
74
  if (unhealthy.length > 0) {
74
75
  const unhealthySummary = unhealthy.map((item) => {
75
76
  const detail = [
@@ -105,7 +106,10 @@ class HealthMonitor {
105
106
  }
106
107
  }
107
108
  else {
108
- results.push({ name: "agent-processes", status: "ok", message: "all managed agents running" });
109
+ const message = parked.length > 0
110
+ ? `all auto-start managed agents running; parked agents: ${parked.map((item) => item.name).join(", ")}`
111
+ : "all managed agents running";
112
+ results.push({ name: "agent-processes", status: "ok", message });
109
113
  }
110
114
  const jobs = this.scheduler.listJobs();
111
115
  const neverRan = jobs.filter((job) => !job.lastRun);