@ouro.bot/cli 0.1.0-alpha.595 → 0.1.0-alpha.597

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.
@@ -0,0 +1,377 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AwaitScheduler = exports.AWAIT_CRON_LABEL_PREFIX_MARKER = void 0;
37
+ const path = __importStar(require("path"));
38
+ const runtime_1 = require("../../nerves/runtime");
39
+ const await_parser_1 = require("./await-parser");
40
+ const await_runtime_state_1 = require("./await-runtime-state");
41
+ const cadence_1 = require("../daemon/cadence");
42
+ const WATCH_DEBOUNCE_MS = 200;
43
+ /** Cron-label namespace prefix to avoid collision with habits. */
44
+ exports.AWAIT_CRON_LABEL_PREFIX_MARKER = "await";
45
+ class AwaitScheduler {
46
+ agent;
47
+ awaitsDir;
48
+ osCronManager;
49
+ onAwaitFire;
50
+ onAwaitExpire;
51
+ deps;
52
+ execForVerify;
53
+ platform;
54
+ watcher = null;
55
+ debounceTimer = null;
56
+ parseErrors = [];
57
+ timerFallbacks = new Map();
58
+ degradedAwaitNames = new Map();
59
+ periodicTimer = null;
60
+ constructor(options) {
61
+ this.agent = options.agent;
62
+ this.awaitsDir = options.awaitsDir;
63
+ this.osCronManager = options.osCronManager;
64
+ this.onAwaitFire = options.onAwaitFire;
65
+ this.onAwaitExpire = options.onAwaitExpire;
66
+ this.deps = options.deps;
67
+ this.execForVerify = options.execForVerify;
68
+ this.platform = options.platform ?? process.platform;
69
+ }
70
+ start() {
71
+ (0, runtime_1.emitNervesEvent)({
72
+ component: "daemon",
73
+ event: "daemon.await_scheduler_start",
74
+ message: "await scheduler starting",
75
+ meta: { agent: this.agent, awaitsDir: this.awaitsDir },
76
+ });
77
+ // Ensure the awaits dir exists before scanning + attaching the file watcher.
78
+ // Without this, fs.watch ENOENTs on cold start and the watcher is never installed,
79
+ // so a freshly-filed first-ever await has to wait for the next periodic reconcile.
80
+ this.deps.mkdir(this.awaitsDir);
81
+ const awaits = this.scanAwaits();
82
+ this.expireOverdueByMaxAge(awaits);
83
+ const remaining = awaits.filter((a) => a.status === "pending" && !this.isExpiredByMaxAge(a));
84
+ const jobs = this.buildJobs(remaining);
85
+ this.osCronManager.sync(jobs);
86
+ this.verifyCronAndCreateFallbacks(jobs);
87
+ this.fireOverdueAwaits(remaining);
88
+ }
89
+ reconcile() {
90
+ (0, runtime_1.emitNervesEvent)({
91
+ component: "daemon",
92
+ event: "daemon.await_scheduler_reconcile",
93
+ message: "await scheduler reconciling",
94
+ meta: { agent: this.agent },
95
+ });
96
+ this.clearAllTimerFallbacks();
97
+ const awaits = this.scanAwaits();
98
+ this.expireOverdueByMaxAge(awaits);
99
+ const remaining = awaits.filter((a) => a.status === "pending" && !this.isExpiredByMaxAge(a));
100
+ const jobs = this.buildJobs(remaining);
101
+ this.osCronManager.sync(jobs);
102
+ this.verifyCronAndCreateFallbacks(jobs);
103
+ this.fireOverdueAwaits(remaining);
104
+ }
105
+ stop() {
106
+ (0, runtime_1.emitNervesEvent)({
107
+ component: "daemon",
108
+ event: "daemon.await_scheduler_end",
109
+ message: "await scheduler stopping",
110
+ meta: { agent: this.agent },
111
+ });
112
+ this.stopPeriodicReconciliation();
113
+ this.clearAllTimerFallbacks();
114
+ this.osCronManager.removeAll();
115
+ }
116
+ getParseErrors() {
117
+ return [...this.parseErrors];
118
+ }
119
+ getDegradedAwaits() {
120
+ const out = [];
121
+ for (const [name, reason] of this.degradedAwaitNames)
122
+ out.push({ name, reason });
123
+ return out;
124
+ }
125
+ getAwaitFile(name) {
126
+ const filePath = path.join(this.awaitsDir, `${name}.md`);
127
+ try {
128
+ const content = this.deps.readFile(filePath, "utf-8");
129
+ return (0, await_runtime_state_1.applyAwaitRuntimeState)(path.dirname(this.awaitsDir), (0, await_parser_1.parseAwaitFile)(content, filePath));
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ }
135
+ watchForChanges() {
136
+ const watchFn = this.deps.watch;
137
+ if (!watchFn)
138
+ return;
139
+ try {
140
+ this.watcher = watchFn(this.awaitsDir, (_event, _filename) => {
141
+ if (this.debounceTimer !== null)
142
+ clearTimeout(this.debounceTimer);
143
+ this.debounceTimer = setTimeout(() => {
144
+ this.debounceTimer = null;
145
+ this.reconcile();
146
+ }, WATCH_DEBOUNCE_MS);
147
+ });
148
+ /* v8 ignore start — ENOENT catch requires real missing directory @preserve */
149
+ }
150
+ catch {
151
+ // awaits dir may not exist yet — skip watching silently
152
+ }
153
+ /* v8 ignore stop */
154
+ }
155
+ stopWatching() {
156
+ if (this.debounceTimer !== null) {
157
+ clearTimeout(this.debounceTimer);
158
+ this.debounceTimer = null;
159
+ }
160
+ if (this.watcher !== null) {
161
+ this.watcher.close();
162
+ this.watcher = null;
163
+ }
164
+ }
165
+ static DEFAULT_PERIODIC_INTERVAL_MS = 300_000;
166
+ static INITIAL_RECONCILIATION_DELAY_MS = 30_000;
167
+ startPeriodicReconciliation(intervalMs) {
168
+ const interval = intervalMs ?? AwaitScheduler.DEFAULT_PERIODIC_INTERVAL_MS;
169
+ this.periodicTimer = setTimeout(() => {
170
+ this.reconcile();
171
+ this.scheduleNextReconciliation(interval);
172
+ }, AwaitScheduler.INITIAL_RECONCILIATION_DELAY_MS);
173
+ }
174
+ stopPeriodicReconciliation() {
175
+ if (this.periodicTimer !== null) {
176
+ clearTimeout(this.periodicTimer);
177
+ this.periodicTimer = null;
178
+ }
179
+ }
180
+ scheduleNextReconciliation(intervalMs) {
181
+ this.periodicTimer = setTimeout(() => {
182
+ this.reconcile();
183
+ this.scheduleNextReconciliation(intervalMs);
184
+ }, intervalMs);
185
+ }
186
+ fireOverdueAwaits(awaits) {
187
+ for (const a of awaits) {
188
+ if (!a.cadence)
189
+ continue;
190
+ const cadenceMs = (0, cadence_1.parseCadenceToMs)(a.cadence);
191
+ if (cadenceMs === null)
192
+ continue;
193
+ const nowMs = this.deps.now();
194
+ const lastChecked = a.last_checked ?? null;
195
+ if (lastChecked === null) {
196
+ (0, runtime_1.emitNervesEvent)({
197
+ component: "daemon",
198
+ event: "daemon.await_fire",
199
+ message: "firing overdue await (never checked)",
200
+ meta: { awaitName: a.name, agent: this.agent },
201
+ });
202
+ this.onAwaitFire(a.name);
203
+ continue;
204
+ }
205
+ const lastCheckedMs = new Date(lastChecked).getTime();
206
+ const elapsed = nowMs - lastCheckedMs;
207
+ if (elapsed >= cadenceMs) {
208
+ (0, runtime_1.emitNervesEvent)({
209
+ component: "daemon",
210
+ event: "daemon.await_fire",
211
+ message: "firing overdue await",
212
+ meta: { awaitName: a.name, agent: this.agent, elapsedMs: elapsed },
213
+ });
214
+ this.onAwaitFire(a.name);
215
+ }
216
+ }
217
+ }
218
+ isExpiredByMaxAge(a) {
219
+ if (a.status !== "pending")
220
+ return false;
221
+ if (!a.max_age || !a.created_at)
222
+ return false;
223
+ const ageMs = (0, cadence_1.parseCadenceToMs)(a.max_age);
224
+ if (ageMs === null)
225
+ return false;
226
+ const createdMs = new Date(a.created_at).getTime();
227
+ if (!Number.isFinite(createdMs))
228
+ return false;
229
+ return this.deps.now() - createdMs >= ageMs;
230
+ }
231
+ expireOverdueByMaxAge(awaits) {
232
+ for (const a of awaits) {
233
+ if (this.isExpiredByMaxAge(a)) {
234
+ (0, runtime_1.emitNervesEvent)({
235
+ component: "daemon",
236
+ event: "daemon.await_expire",
237
+ message: "await max_age elapsed; expiring",
238
+ meta: { awaitName: a.name, agent: this.agent, max_age: a.max_age, created_at: a.created_at },
239
+ });
240
+ this.onAwaitExpire(a.name);
241
+ }
242
+ }
243
+ }
244
+ verifyCronAndCreateFallbacks(jobs) {
245
+ if (!this.execForVerify)
246
+ return;
247
+ const verifiedLabels = this.verifyCronEntries();
248
+ for (const job of jobs) {
249
+ // job.taskId is already namespaced as "await.<name>". The bare name is
250
+ // what we pass to `--await` and what shows up in the crontab regex
251
+ // capture, so we strip the prefix for linux verification.
252
+ const bareName = job.taskId.startsWith(`${exports.AWAIT_CRON_LABEL_PREFIX_MARKER}.`)
253
+ ? job.taskId.slice(exports.AWAIT_CRON_LABEL_PREFIX_MARKER.length + 1)
254
+ : /* v8 ignore next -- defensive: buildJobs always namespaces taskId @preserve */
255
+ job.taskId;
256
+ const label = `bot.ouro.${job.agent}.${job.taskId}`;
257
+ const isVerified = this.platform === "darwin"
258
+ ? verifiedLabels.has(label)
259
+ : verifiedLabels.has(bareName);
260
+ if (!isVerified) {
261
+ (0, runtime_1.emitNervesEvent)({
262
+ component: "daemon",
263
+ event: "daemon.await_cron_verification_failed",
264
+ message: `cron verification failed for await: ${bareName}`,
265
+ meta: { awaitName: bareName, agent: job.agent, label },
266
+ });
267
+ const awaitFile = this.getAwaitFile(bareName);
268
+ const ms = awaitFile?.cadence ? (0, cadence_1.parseCadenceToMs)(awaitFile.cadence) : null;
269
+ if (ms !== null)
270
+ this.createTimerFallback(bareName, ms);
271
+ this.degradedAwaitNames.set(bareName, "cron registration failed — using timer fallback");
272
+ }
273
+ }
274
+ }
275
+ verifyCronEntries() {
276
+ const verified = new Set();
277
+ try {
278
+ if (this.platform === "darwin") {
279
+ const output = this.execForVerify("launchctl list");
280
+ const lines = output.split("\n");
281
+ for (const line of lines) {
282
+ const match = line.match(/bot\.ouro\.\S+\.await\.\S+/);
283
+ if (match)
284
+ verified.add(match[0]);
285
+ }
286
+ }
287
+ else {
288
+ const output = this.execForVerify("crontab -l");
289
+ const lines = output.split("\n");
290
+ for (const line of lines) {
291
+ const match = line.match(/ouro poke \S+ --await (\S+)/);
292
+ if (match)
293
+ verified.add(match[1]);
294
+ }
295
+ }
296
+ }
297
+ catch {
298
+ // best-effort
299
+ }
300
+ return verified;
301
+ }
302
+ createTimerFallback(awaitName, cadenceMs) {
303
+ const schedule = () => {
304
+ const timer = setTimeout(() => {
305
+ this.onAwaitFire(awaitName);
306
+ schedule();
307
+ }, cadenceMs);
308
+ this.timerFallbacks.set(awaitName, timer);
309
+ };
310
+ schedule();
311
+ }
312
+ clearAllTimerFallbacks() {
313
+ for (const timer of this.timerFallbacks.values())
314
+ clearTimeout(timer);
315
+ this.timerFallbacks.clear();
316
+ this.degradedAwaitNames.clear();
317
+ }
318
+ scanAwaits() {
319
+ let files;
320
+ try {
321
+ files = this.deps.readdir(this.awaitsDir);
322
+ }
323
+ catch {
324
+ this.parseErrors = [];
325
+ return [];
326
+ }
327
+ const awaits = [];
328
+ const errors = [];
329
+ for (const file of files) {
330
+ if (!file.endsWith(".md"))
331
+ continue;
332
+ const filePath = path.join(this.awaitsDir, file);
333
+ try {
334
+ const content = this.deps.readFile(filePath, "utf-8");
335
+ const a = (0, await_runtime_state_1.applyAwaitRuntimeState)(path.dirname(this.awaitsDir), (0, await_parser_1.parseAwaitFile)(content, filePath));
336
+ awaits.push(a);
337
+ }
338
+ catch (error) {
339
+ const errorMessage = error instanceof Error ? error.message : String(error);
340
+ errors.push({ file, error: errorMessage });
341
+ (0, runtime_1.emitNervesEvent)({
342
+ level: "error",
343
+ component: "daemon",
344
+ event: "daemon.await_parse_error",
345
+ message: "failed to parse await file",
346
+ meta: { file, error: errorMessage, agent: this.agent },
347
+ });
348
+ }
349
+ }
350
+ this.parseErrors = errors;
351
+ return awaits;
352
+ }
353
+ buildJobs(awaits) {
354
+ const jobs = [];
355
+ for (const a of awaits) {
356
+ /* v8 ignore next -- defensive: callers (start/reconcile) pre-filter to pending awaits @preserve */
357
+ if (a.status !== "pending")
358
+ continue;
359
+ if (!a.cadence)
360
+ continue;
361
+ const cronSchedule = (0, cadence_1.parseCadenceToCron)(a.cadence);
362
+ if (cronSchedule === null)
363
+ continue;
364
+ jobs.push({
365
+ id: `${this.agent}:${exports.AWAIT_CRON_LABEL_PREFIX_MARKER}.${a.name}:cadence`,
366
+ agent: this.agent,
367
+ taskId: `${exports.AWAIT_CRON_LABEL_PREFIX_MARKER}.${a.name}`,
368
+ schedule: cronSchedule,
369
+ lastRun: a.last_checked ?? null,
370
+ command: `${this.deps.ouroPath} poke ${this.agent} --await ${a.name}`,
371
+ taskPath: path.join(this.awaitsDir, `${a.name}.md`),
372
+ });
373
+ }
374
+ return jobs;
375
+ }
376
+ }
377
+ exports.AwaitScheduler = AwaitScheduler;
@@ -15,10 +15,11 @@ function describeActiveObligation(obligation) {
15
15
  }
16
16
  return `i owe ${obligation.origin.friendId}: ${obligation.content} (${statusText})`;
17
17
  }
18
- function deriveCommitments(activeWorkFrame, innerJob, pendingObligations) {
18
+ function deriveCommitments(activeWorkFrame, innerJob, pendingObligations, pendingAwaits) {
19
19
  const committedTo = [];
20
20
  const completionCriteria = [];
21
21
  const safeToIgnore = [];
22
+ const awaiting = pendingAwaits ? [...pendingAwaits] : [];
22
23
  // Persistent obligations from the obligation store
23
24
  // Sort by status priority: investigating/waiting/updating before pending
24
25
  if (pendingObligations && pendingObligations.length > 0) {
@@ -87,11 +88,29 @@ function deriveCommitments(activeWorkFrame, innerJob, pendingObligations) {
87
88
  component: "engine",
88
89
  event: "engine.commitments_derive",
89
90
  message: "derived commitments frame",
90
- meta: { committedCount: committedTo.length, criteriaCount: completionCriteria.length },
91
+ meta: { committedCount: committedTo.length, criteriaCount: completionCriteria.length, awaitingCount: awaiting.length },
91
92
  });
92
- return { committedTo, completionCriteria, safeToIgnore };
93
+ return { committedTo, completionCriteria, safeToIgnore, awaiting };
93
94
  }
94
- function formatCommitments(commitments) {
95
+ function formatRelativeAge(lastCheckedAt, now) {
96
+ if (!lastCheckedAt)
97
+ return "never checked";
98
+ const lastMs = new Date(lastCheckedAt).getTime();
99
+ if (!Number.isFinite(lastMs))
100
+ return "never checked";
101
+ const elapsedMs = now().getTime() - lastMs;
102
+ if (elapsedMs < 60_000)
103
+ return "<1m ago";
104
+ const minutes = Math.floor(elapsedMs / 60_000);
105
+ if (minutes < 60)
106
+ return `${minutes}m ago`;
107
+ const hours = Math.floor(minutes / 60);
108
+ if (hours < 24)
109
+ return `${hours}h ago`;
110
+ const days = Math.floor(hours / 24);
111
+ return `${days}d ago`;
112
+ }
113
+ function formatCommitments(commitments, now = () => new Date()) {
95
114
  const sections = [];
96
115
  if (commitments.committedTo.length === 0) {
97
116
  sections.push("i'm not holding anything specific right now. i'm free to be present.");
@@ -107,5 +126,17 @@ function formatCommitments(commitments) {
107
126
  sections.push("");
108
127
  sections.push("## what i can let go of");
109
128
  sections.push(commitments.safeToIgnore.map((c) => `- ${c}`).join("\n"));
129
+ const awaiting = commitments.awaiting ?? [];
130
+ if (awaiting.length > 0) {
131
+ sections.push("");
132
+ sections.push("## what i'm waiting on");
133
+ for (const a of awaiting) {
134
+ sections.push(`- ${a.name}: ${a.condition}`);
135
+ const obs = a.lastObservation && a.lastObservation.trim().length > 0
136
+ ? `: "${a.lastObservation.trim()}"`
137
+ : "";
138
+ sections.push(` (checked ${a.checkedCount}x, last ${formatRelativeAge(a.lastCheckedAt, now)}${obs})`);
139
+ }
140
+ }
110
141
  return sections.join("\n");
111
142
  }
@@ -179,6 +179,7 @@ function parsePokeCommand(args) {
179
179
  throw new Error(`Usage\n${usage()}`);
180
180
  let taskId;
181
181
  let habitName;
182
+ let awaitName;
182
183
  for (let i = 1; i < args.length; i += 1) {
183
184
  if (args[i] === "--task") {
184
185
  taskId = args[i + 1];
@@ -188,8 +189,14 @@ function parsePokeCommand(args) {
188
189
  habitName = args[i + 1];
189
190
  i += 1;
190
191
  }
192
+ if (args[i] === "--await") {
193
+ awaitName = args[i + 1];
194
+ i += 1;
195
+ }
191
196
  }
192
- // --habit takes priority over --task
197
+ // Priority order: --await > --habit > --task
198
+ if (awaitName)
199
+ return { kind: "await.poke", agent, awaitName };
193
200
  if (habitName)
194
201
  return { kind: "habit.poke", agent, habitName };
195
202
  if (!taskId)
@@ -52,6 +52,8 @@ const identity_1 = require("../identity");
52
52
  const runtime_mode_1 = require("./runtime-mode");
53
53
  const habit_scheduler_1 = require("../habits/habit-scheduler");
54
54
  const habit_migration_1 = require("../habits/habit-migration");
55
+ const await_scheduler_1 = require("../awaiting/await-scheduler");
56
+ const await_expiry_1 = require("../awaiting/await-expiry");
55
57
  const os_cron_deps_1 = require("./os-cron-deps");
56
58
  const os_cron_1 = require("./os-cron");
57
59
  const daemon_tombstone_1 = require("./daemon-tombstone");
@@ -173,6 +175,7 @@ const healthMonitor = new health_monitor_1.HealthMonitor({
173
175
  },
174
176
  });
175
177
  const habitSchedulers = [];
178
+ const awaitSchedulers = [];
176
179
  let entryRuntimeStopping = false;
177
180
  let stopCommandExitScheduled = false;
178
181
  function stopEntryRuntime() {
@@ -183,6 +186,10 @@ function stopEntryRuntime() {
183
186
  s.stopWatching();
184
187
  s.stop();
185
188
  }
189
+ for (const s of awaitSchedulers) {
190
+ s.stopWatching();
191
+ s.stop();
192
+ }
186
193
  healthMonitor.stopPeriodicChecks();
187
194
  }
188
195
  function scheduleCleanProcessExitAfterStopCommand() {
@@ -314,6 +321,16 @@ function emitHabitSetupError(agent, error) {
314
321
  meta: { agent, error: normalized.message },
315
322
  });
316
323
  }
324
+ function emitAwaitSetupError(agent, error) {
325
+ const normalized = error instanceof Error ? error : new Error(String(error));
326
+ (0, runtime_1.emitNervesEvent)({
327
+ level: "error",
328
+ component: "daemon",
329
+ event: "daemon.await_setup_error",
330
+ message: `await setup failed for agent ${agent}`,
331
+ meta: { agent, error: normalized.message },
332
+ });
333
+ }
317
334
  /* v8 ignore start — daemon health writer wiring, tested via daemon-health.test.ts @preserve */
318
335
  const healthWriter = new daemon_health_1.DaemonHealthWriter((0, daemon_health_1.getDefaultHealthPath)());
319
336
  const healthSink = (0, daemon_health_1.createHealthNervesSink)(healthWriter, buildDaemonHealthState);
@@ -396,6 +413,87 @@ void daemon.start().then(() => {
396
413
  guidance: `fix ${agent} habits or cron setup and rerun ouro up to restore habit automation`,
397
414
  });
398
415
  }
416
+ // Parallel await-condition scheduler. Uses its own OS cron manager so
417
+ // habits and awaits don't share label namespace and stale removals can't
418
+ // collide.
419
+ const awaitsDir = path.join(bundleRoot, "awaiting");
420
+ const awaitDegradedComponent = `awaits:${agent}`;
421
+ try {
422
+ const awaitOsCronManager = new os_cron_1.LaunchdCronManager(osCronDeps);
423
+ const awaitScheduler = new await_scheduler_1.AwaitScheduler({
424
+ agent,
425
+ awaitsDir,
426
+ osCronManager: awaitOsCronManager,
427
+ onAwaitFire: (awaitName) => {
428
+ processManager.sendToAgent(agent, { type: "await", awaitName });
429
+ },
430
+ onAwaitExpire: (awaitName) => {
431
+ void (0, await_expiry_1.archiveAndAlertExpiredAwait)({
432
+ agentRoot: bundleRoot,
433
+ agentName: agent,
434
+ awaitName,
435
+ deliveryDeps: {
436
+ agentName: agent,
437
+ queuePending: () => {
438
+ // Best-effort: queue inner-dialog wake so the agent processes the alert path
439
+ (0, socket_client_1.sendDaemonCommand)(socketPath, { kind: "inner.wake", agent }).catch(() => { });
440
+ },
441
+ },
442
+ }).catch((err) => {
443
+ (0, runtime_1.emitNervesEvent)({
444
+ level: "error",
445
+ component: "daemon",
446
+ event: "daemon.await_expire_error",
447
+ message: "await expiry handler threw",
448
+ meta: { agent, awaitName, error: err instanceof Error ? err.message : String(err) },
449
+ });
450
+ });
451
+ },
452
+ deps: {
453
+ readdir: (dir) => fs.readdirSync(dir),
454
+ readFile: (p, enc) => fs.readFileSync(p, enc),
455
+ existsSync: (p) => fs.existsSync(p),
456
+ mkdir: (dir) => { fs.mkdirSync(dir, { recursive: true }); },
457
+ now: () => Date.now(),
458
+ ouroPath,
459
+ watch: (dir, cb) => fs.watch(dir, cb),
460
+ },
461
+ });
462
+ try {
463
+ awaitScheduler.start();
464
+ awaitScheduler.startPeriodicReconciliation();
465
+ awaitScheduler.watchForChanges();
466
+ awaitSchedulers.push(awaitScheduler);
467
+ }
468
+ catch (error) {
469
+ try {
470
+ awaitScheduler.stopWatching();
471
+ awaitScheduler.stop();
472
+ }
473
+ catch {
474
+ // best-effort cleanup
475
+ }
476
+ emitAwaitSetupError(agent, error);
477
+ recordRecoverableBootstrapFailure({
478
+ agent,
479
+ component: awaitDegradedComponent,
480
+ habitsDir: awaitsDir,
481
+ error,
482
+ guidance: `fix ${agent} awaits or cron setup and rerun ouro up to restore await automation`,
483
+ });
484
+ }
485
+ }
486
+ catch (err) {
487
+ const error = err instanceof Error ? err : new Error(String(err));
488
+ emitAwaitSetupError(agent, error);
489
+ recordRecoverableBootstrapFailure({
490
+ agent,
491
+ component: awaitDegradedComponent,
492
+ habitsDir: awaitsDir,
493
+ error,
494
+ guidance: `fix ${agent} awaits or cron setup and rerun ouro up to restore await automation`,
495
+ });
496
+ }
399
497
  }
400
498
  healthMonitor.startPeriodicChecks(60_000);
401
499
  /* v8 ignore start -- startup failure + signal handlers: call process.exit, untestable in vitest @preserve */
@@ -1199,6 +1199,13 @@ class OuroDaemon {
1199
1199
  message: `poked habit ${command.habitName} for ${command.agent}`,
1200
1200
  };
1201
1201
  }
1202
+ case "await.poke": {
1203
+ this.processManager.sendToAgent?.(command.agent, { type: "await", awaitName: command.awaitName });
1204
+ return {
1205
+ ok: true,
1206
+ message: `poked await ${command.awaitName} for ${command.agent}`,
1207
+ };
1208
+ }
1202
1209
  case "mcp.list": {
1203
1210
  const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)();
1204
1211
  if (!mcpManager) {
@@ -80,6 +80,7 @@ const tasks_1 = require("../repertoire/tasks");
80
80
  const session_activity_1 = require("../heart/session-activity");
81
81
  const active_work_1 = require("../heart/active-work");
82
82
  const commitments_1 = require("../heart/commitments");
83
+ const await_loader_1 = require("../heart/awaiting/await-loader");
83
84
  const obligation_steering_1 = require("./obligation-steering");
84
85
  const daemon_health_1 = require("../heart/daemon/daemon-health");
85
86
  const scrutiny_1 = require("./scrutiny");
@@ -979,8 +980,18 @@ function commitmentsSection(options) {
979
980
  const job = options.activeWorkFrame.inner?.job;
980
981
  if (!job)
981
982
  return "";
982
- const commitments = (0, commitments_1.deriveCommitments)(options.activeWorkFrame, job, options.activeWorkFrame.pendingObligations);
983
- if (commitments.committedTo.length === 0)
983
+ let awaits = options.pendingAwaits ?? [];
984
+ if (!options.pendingAwaits) {
985
+ try {
986
+ awaits = (0, await_loader_1.loadPendingAwaitsForCommitments)((0, identity_1.getAgentRoot)());
987
+ }
988
+ catch {
989
+ // Identity not configured (test/eager-call contexts) — proceed without awaits
990
+ awaits = [];
991
+ }
992
+ }
993
+ const commitments = (0, commitments_1.deriveCommitments)(options.activeWorkFrame, job, options.activeWorkFrame.pendingObligations, awaits);
994
+ if (commitments.committedTo.length === 0 && awaits.length === 0)
984
995
  return "";
985
996
  return `## my commitments\n\n${(0, commitments_1.formatCommitments)(commitments)}`;
986
997
  }