@ouro.bot/cli 0.1.0-alpha.723 → 0.1.0-alpha.725

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 (55) hide show
  1. package/changelog.json +295 -275
  2. package/dist/heart/active-work.js +10 -3
  3. package/dist/heart/core.js +343 -77
  4. package/dist/heart/daemon/cli-exec.js +132 -15
  5. package/dist/heart/daemon/cli-help.js +13 -2
  6. package/dist/heart/daemon/cli-parse.js +82 -1
  7. package/dist/heart/daemon/daemon.js +286 -33
  8. package/dist/heart/daemon/doctor.js +82 -6
  9. package/dist/heart/daemon/habit-private-wake.js +6 -1
  10. package/dist/heart/habits/habit-cancel.js +810 -0
  11. package/dist/heart/habits/habit-lifecycle.js +1327 -0
  12. package/dist/heart/habits/habit-migration.js +28 -23
  13. package/dist/heart/habits/habit-parser.js +72 -8
  14. package/dist/heart/habits/habit-runtime-state.js +3 -24
  15. package/dist/heart/habits/habit-scheduler.js +268 -50
  16. package/dist/heart/habits/habit-session.js +16 -0
  17. package/dist/heart/hatch/hatch-flow.js +6 -4
  18. package/dist/heart/mailbox/readers/runtime-readers.js +26 -27
  19. package/dist/heart/orientation-frame.js +58 -1
  20. package/dist/heart/provider-attempt.js +4 -1
  21. package/dist/heart/providers/anthropic.js +23 -2
  22. package/dist/heart/start-of-turn-packet.js +14 -11
  23. package/dist/heart/streaming.js +565 -185
  24. package/dist/mind/obligation-steering.js +15 -9
  25. package/dist/mind/prompt.js +58 -26
  26. package/dist/nerves/coverage/run-artifacts.js +55 -35
  27. package/dist/nerves/review/cli.js +2 -2
  28. package/dist/nerves/review/core.js +14 -7
  29. package/dist/repertoire/tools-base.js +2 -0
  30. package/dist/repertoire/tools-habits.js +103 -0
  31. package/dist/repertoire/tools.js +18 -3
  32. package/dist/rsvp/cli.js +126 -20
  33. package/dist/rsvp/diagnostics.js +12 -2
  34. package/dist/rsvp/habit-policy.js +7 -0
  35. package/dist/rsvp/habit-stage.js +19 -4
  36. package/dist/rsvp/native-habit-runner.js +151 -11
  37. package/dist/rsvp/outbound-state.js +360 -0
  38. package/dist/senses/bluebubbles/client.js +90 -17
  39. package/dist/senses/bluebubbles/inbound-log.js +1 -0
  40. package/dist/senses/bluebubbles/index.js +662 -425
  41. package/dist/senses/bluebubbles/model.js +57 -13
  42. package/dist/senses/bluebubbles/mutation-log.js +21 -10
  43. package/dist/senses/bluebubbles/reaction-policy.js +55 -0
  44. package/dist/senses/bluebubbles/semantic-receipts.js +1818 -0
  45. package/dist/senses/cli/tui-store.js +1 -0
  46. package/dist/senses/cli.js +1 -0
  47. package/dist/senses/habit-lifecycle-guard.js +24 -0
  48. package/dist/senses/ingress-evidence.js +2 -0
  49. package/dist/senses/mail.js +129 -0
  50. package/dist/senses/pipeline.js +90 -24
  51. package/dist/senses/private-runtime-worker.js +21 -20
  52. package/dist/senses/private-runtime.js +79 -29
  53. package/dist/senses/shared-turn.js +1 -0
  54. package/dist/senses/teams.js +1 -0
  55. package/package.json +6 -1
@@ -41,6 +41,7 @@ const habit_runtime_state_1 = require("./habit-runtime-state");
41
41
  const cadence_1 = require("../daemon/cadence");
42
42
  const habit_policy_1 = require("../../rsvp/habit-policy");
43
43
  const WATCH_DEBOUNCE_MS = 200;
44
+ const MAX_CRON_VERIFICATION_ATTEMPTS = 3;
44
45
  class HabitScheduler {
45
46
  agent;
46
47
  habitsDir;
@@ -53,8 +54,10 @@ class HabitScheduler {
53
54
  debounceTimer = null;
54
55
  parseErrors = [];
55
56
  timerFallbacks = new Map();
57
+ timerFallbackGenerations = new Map();
56
58
  degradedHabitNames = new Map();
57
59
  periodicTimer = null;
60
+ habitSnapshots = new Map();
58
61
  constructor(options) {
59
62
  this.agent = options.agent;
60
63
  this.habitsDir = options.habitsDir;
@@ -71,11 +74,7 @@ class HabitScheduler {
71
74
  message: "habit scheduler starting",
72
75
  meta: { agent: this.agent, habitsDir: this.habitsDir },
73
76
  });
74
- const habits = this.scanHabits();
75
- const jobs = this.buildJobs(habits);
76
- this.osCronManager.sync(jobs);
77
- this.verifyCronAndCreateFallbacks(jobs);
78
- this.fireOverdueHabits(habits);
77
+ this.runSchedulingCycle();
79
78
  }
80
79
  reconcile() {
81
80
  (0, runtime_1.emitNervesEvent)({
@@ -86,21 +85,36 @@ class HabitScheduler {
86
85
  });
87
86
  // Clear ALL existing timers FIRST to prevent overlap window
88
87
  this.clearAllTimerFallbacks();
89
- const habits = this.scanHabits();
90
- const jobs = this.buildJobs(habits);
91
- this.osCronManager.sync(jobs);
92
- this.verifyCronAndCreateFallbacks(jobs);
93
- this.fireOverdueHabits(habits);
88
+ this.runSchedulingCycle();
94
89
  }
95
- fireOverdueHabits(habits) {
96
- for (const habit of habits) {
97
- if (habit.status !== "active")
98
- continue;
99
- if (!habit.cadence)
100
- continue;
101
- if (this.rejectInvalidRsvpHabit(habit))
90
+ runSchedulingCycle() {
91
+ const scannedHabits = this.scanHabits();
92
+ let state = this.revalidateState({
93
+ habits: scannedHabits,
94
+ jobs: this.buildJobs(scannedHabits),
95
+ });
96
+ if (!this.syncJobsFailClosed(state.jobs))
97
+ return;
98
+ const afterSync = this.revalidateAndCorrectJobs(state);
99
+ if (afterSync === null)
100
+ return;
101
+ state = afterSync;
102
+ const afterVerification = this.verifyCronAndCreateFallbacks(state);
103
+ if (afterVerification === null)
104
+ return;
105
+ state = afterVerification;
106
+ const beforeDispatch = this.revalidateAndCorrectJobs(state);
107
+ if (beforeDispatch === null)
108
+ return;
109
+ this.fireOverdueHabits(beforeDispatch);
110
+ }
111
+ fireOverdueHabits(state) {
112
+ for (const expectedJob of state.jobs) {
113
+ const habit = this.revalidateHabit(expectedJob.taskId);
114
+ if (habit === null)
102
115
  continue;
103
- if (this.rejectReservedCronNamespaceHabit(habit))
116
+ const currentJob = this.buildJobs([habit]).find((job) => job.id === expectedJob.id);
117
+ if (!currentJob || !this.jobEqual(expectedJob, currentJob))
104
118
  continue;
105
119
  const nowMs = this.deps.now();
106
120
  const dueState = (0, cadence_1.evaluateCadenceDue)(habit.cadence, habit.lastRun, nowMs);
@@ -174,14 +188,21 @@ class HabitScheduler {
174
188
  if (!job) {
175
189
  return { ok: false, message: `unknown habit job: ${jobId}` };
176
190
  }
177
- this.onHabitFire(job.taskId, trigger, {
178
- occurrenceId: this.jobOccurrenceId(job, trigger),
191
+ const currentHabit = this.revalidateHabit(job.taskId);
192
+ const currentJob = currentHabit === null
193
+ ? undefined
194
+ : this.buildJobs([currentHabit]).find((candidate) => candidate.id === jobId);
195
+ if (!currentJob) {
196
+ return { ok: false, message: `unknown habit job: ${jobId}` };
197
+ }
198
+ this.onHabitFire(currentJob.taskId, trigger, {
199
+ occurrenceId: this.jobOccurrenceId(currentJob, trigger),
179
200
  });
180
201
  (0, runtime_1.emitNervesEvent)({
181
202
  component: "daemon",
182
203
  event: "daemon.habit_job_triggered",
183
204
  message: "habit scheduler job triggered",
184
- meta: { agent: job.agent, habitName: job.taskId, jobId, trigger },
205
+ meta: { agent: currentJob.agent, habitName: currentJob.taskId, jobId, trigger },
185
206
  });
186
207
  return { ok: true, message: `triggered habit ${jobId}` };
187
208
  }
@@ -246,31 +267,69 @@ class HabitScheduler {
246
267
  this.scheduleNextReconciliation(intervalMs);
247
268
  }, intervalMs);
248
269
  }
249
- verifyCronAndCreateFallbacks(jobs) {
250
- if (!this.execForVerify)
251
- return;
252
- const verifiedLabels = this.verifyCronEntries();
253
- for (const job of jobs) {
254
- const label = `bot.ouro.${job.agent}.${job.taskId}`;
255
- const isVerified = this.platform === "darwin"
256
- ? verifiedLabels.has(label)
257
- : verifiedLabels.has(job.taskId);
258
- if (!isVerified) {
259
- (0, runtime_1.emitNervesEvent)({
260
- component: "daemon",
261
- event: "daemon.habit_cron_verification_failed",
262
- message: `cron verification failed for habit: ${job.taskId}`,
263
- meta: { habitName: job.taskId, agent: job.agent, label },
264
- });
265
- // Parse cadence from the original habit file for timer interval
266
- const habitFile = this.getHabitFile(job.taskId);
267
- const ms = habitFile?.cadence ? (0, cadence_1.cadenceFallbackDelayMs)(habitFile.cadence, this.deps.now()) : null;
268
- if (ms !== null) {
269
- this.createTimerFallback(job.taskId, habitFile.cadence, ms);
270
+ verifyCronAndCreateFallbacks(state) {
271
+ if (!this.execForVerify || state.jobs.length === 0)
272
+ return state;
273
+ let currentState = state;
274
+ for (let attempt = 1; attempt <= MAX_CRON_VERIFICATION_ATTEMPTS; attempt += 1) {
275
+ const verifiedLabels = this.verifyCronEntries();
276
+ const revalidatedState = this.revalidateAndCorrectJobs(currentState);
277
+ if (revalidatedState === null)
278
+ return null;
279
+ if (!this.jobsEqual(currentState.jobs, revalidatedState.jobs)) {
280
+ currentState = revalidatedState;
281
+ if (currentState.jobs.length === 0)
282
+ return currentState;
283
+ continue;
284
+ }
285
+ currentState = revalidatedState;
286
+ for (const job of currentState.jobs) {
287
+ const label = `bot.ouro.${job.agent}.${job.taskId}`;
288
+ const isVerified = this.platform === "darwin"
289
+ ? verifiedLabels.has(label)
290
+ : verifiedLabels.has(job.taskId);
291
+ if (!isVerified) {
292
+ (0, runtime_1.emitNervesEvent)({
293
+ component: "daemon",
294
+ event: "daemon.habit_cron_verification_failed",
295
+ message: `cron verification failed for habit: ${job.taskId}`,
296
+ meta: { habitName: job.taskId, agent: job.agent, label },
297
+ });
298
+ const habitFile = currentState.habits.find((habit) => habit.name === job.taskId);
299
+ const cadence = habitFile.cadence;
300
+ const ms = (0, cadence_1.cadenceFallbackDelayMs)(cadence, this.deps.now());
301
+ if (ms !== null) {
302
+ this.createTimerFallback(job.taskId, cadence, ms);
303
+ this.degradedHabitNames.set(job.taskId, "cron registration failed — using timer fallback");
304
+ }
305
+ else {
306
+ this.degradedHabitNames.set(job.taskId, "cron registration failed — no timer fallback available");
307
+ }
270
308
  }
271
- this.degradedHabitNames.set(job.taskId, "cron registration failed — using timer fallback");
272
309
  }
310
+ return currentState;
311
+ }
312
+ this.clearAllTimerFallbacks();
313
+ let cleanupError = null;
314
+ try {
315
+ this.osCronManager.removeAll();
316
+ }
317
+ catch (error) {
318
+ cleanupError = this.errorMessage(error);
273
319
  }
320
+ (0, runtime_1.emitNervesEvent)({
321
+ level: "error",
322
+ component: "daemon",
323
+ event: "daemon.habit_cron_verification_unstable",
324
+ message: "habit scheduler cron verification did not stabilize; fail-closed cleanup attempted",
325
+ meta: {
326
+ agent: this.agent,
327
+ attempts: MAX_CRON_VERIFICATION_ATTEMPTS,
328
+ jobCount: currentState.jobs.length,
329
+ cleanupError,
330
+ },
331
+ });
332
+ return null;
274
333
  }
275
334
  verifyCronEntries() {
276
335
  const verified = new Set();
@@ -302,25 +361,156 @@ class HabitScheduler {
302
361
  return verified;
303
362
  }
304
363
  createTimerFallback(habitName, cadence, firstDelayMs) {
364
+ if (this.timerFallbacks.has(habitName))
365
+ this.clearTimerFallback(habitName);
366
+ const generation = this.nextTimerFallbackGeneration(habitName);
305
367
  const schedule = () => {
306
368
  const delayMs = (0, cadence_1.cadenceFallbackDelayMs)(cadence, this.deps.now()) ?? firstDelayMs;
307
369
  const timer = setTimeout(() => {
370
+ const owner = this.timerFallbacks.get(habitName);
371
+ if (!owner || owner.handle !== timer || owner.generation !== generation)
372
+ return;
373
+ const currentHabit = this.revalidateHabit(habitName);
374
+ const currentJob = currentHabit === null
375
+ ? undefined
376
+ : this.buildJobs([currentHabit]).find((job) => job.taskId === habitName);
377
+ if (!currentJob || currentHabit?.cadence !== cadence) {
378
+ this.clearTimerFallback(habitName);
379
+ return;
380
+ }
308
381
  this.onHabitFire(habitName, "overdue", {
309
382
  occurrenceId: this.timerOccurrenceId(habitName, cadence),
310
383
  });
384
+ const currentOwner = this.timerFallbacks.get(habitName);
385
+ if (!currentOwner || currentOwner.handle !== timer || currentOwner.generation !== generation)
386
+ return;
311
387
  schedule();
312
388
  }, delayMs);
313
- this.timerFallbacks.set(habitName, timer);
389
+ this.timerFallbacks.set(habitName, { handle: timer, cadence, generation });
314
390
  };
315
391
  schedule();
316
392
  }
317
393
  clearAllTimerFallbacks() {
318
- for (const timer of this.timerFallbacks.values()) {
319
- clearTimeout(timer);
320
- }
321
- this.timerFallbacks.clear();
394
+ for (const name of [...this.timerFallbacks.keys()])
395
+ this.clearTimerFallback(name);
322
396
  this.degradedHabitNames.clear();
323
397
  }
398
+ clearTimerFallback(name) {
399
+ const owner = this.timerFallbacks.get(name);
400
+ clearTimeout(owner.handle);
401
+ this.timerFallbacks.delete(name);
402
+ this.degradedHabitNames.delete(name);
403
+ this.nextTimerFallbackGeneration(name);
404
+ }
405
+ nextTimerFallbackGeneration(name) {
406
+ const generation = (this.timerFallbackGenerations.get(name) ?? 0) + 1;
407
+ this.timerFallbackGenerations.set(name, generation);
408
+ return generation;
409
+ }
410
+ revalidateState(state) {
411
+ const habits = [];
412
+ for (const habit of state.habits) {
413
+ const currentHabit = this.revalidateHabit(habit.name);
414
+ if (currentHabit !== null)
415
+ habits.push(currentHabit);
416
+ }
417
+ return { habits, jobs: this.buildJobs(habits) };
418
+ }
419
+ revalidateAndCorrectJobs(state) {
420
+ const currentState = this.revalidateState(state);
421
+ this.pruneTimerFallbacks(state, currentState);
422
+ if (this.jobsEqual(state.jobs, currentState.jobs))
423
+ return currentState;
424
+ return this.syncJobsFailClosed(currentState.jobs) ? currentState : null;
425
+ }
426
+ revalidateHabit(name) {
427
+ const file = `${name}.md`;
428
+ const filePath = path.join(this.habitsDir, file);
429
+ let content;
430
+ try {
431
+ content = this.deps.readFile(filePath, "utf-8");
432
+ }
433
+ catch (error) {
434
+ this.habitSnapshots.delete(name);
435
+ this.recordHabitParseError(file, `habit definition unreadable during scheduler revalidation: ${this.errorMessage(error)}`);
436
+ return null;
437
+ }
438
+ try {
439
+ const cached = this.habitSnapshots.get(name);
440
+ const definition = cached?.content === content
441
+ ? cached.definition
442
+ : (0, habit_parser_1.parseHabitFile)(content, filePath);
443
+ this.habitSnapshots.set(name, { content, definition });
444
+ const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(path.dirname(this.habitsDir), definition);
445
+ this.recordDegradedHabitError(file, habit);
446
+ return habit;
447
+ }
448
+ catch (error) {
449
+ this.habitSnapshots.delete(name);
450
+ this.recordHabitParseError(file, this.errorMessage(error));
451
+ return null;
452
+ }
453
+ }
454
+ syncJobsFailClosed(jobs) {
455
+ try {
456
+ this.osCronManager.sync(jobs);
457
+ return true;
458
+ }
459
+ catch (error) {
460
+ this.clearAllTimerFallbacks();
461
+ let cleanupError = null;
462
+ try {
463
+ this.osCronManager.removeAll();
464
+ }
465
+ catch (cleanupFailure) {
466
+ cleanupError = this.errorMessage(cleanupFailure);
467
+ }
468
+ (0, runtime_1.emitNervesEvent)({
469
+ level: "error",
470
+ component: "daemon",
471
+ event: "daemon.habit_scheduler_sync_error",
472
+ message: "habit scheduler cron synchronization failed; fail-closed cleanup attempted",
473
+ meta: {
474
+ agent: this.agent,
475
+ error: this.errorMessage(error),
476
+ cleanupError,
477
+ jobCount: jobs.length,
478
+ },
479
+ });
480
+ return false;
481
+ }
482
+ }
483
+ pruneTimerFallbacks(previousState, currentState) {
484
+ for (const [name, owner] of this.timerFallbacks) {
485
+ const previousJob = previousState.jobs.find((job) => job.taskId === name);
486
+ const currentJob = currentState.jobs.find((job) => job.taskId === name);
487
+ const currentHabit = currentState.habits.find((habit) => habit.name === name);
488
+ if (previousJob
489
+ && currentJob
490
+ && currentHabit?.cadence === owner.cadence
491
+ && this.jobEqual(previousJob, currentJob))
492
+ continue;
493
+ this.clearTimerFallback(name);
494
+ }
495
+ }
496
+ jobsEqual(left, right) {
497
+ if (left.length !== right.length)
498
+ return false;
499
+ return left.every((job, index) => this.jobEqual(job, right[index]));
500
+ }
501
+ jobEqual(left, right) {
502
+ return right !== undefined
503
+ && left.id === right.id
504
+ && left.agent === right.agent
505
+ && left.taskId === right.taskId
506
+ && left.schedule === right.schedule
507
+ && left.lastRun === right.lastRun
508
+ && left.command === right.command
509
+ && left.taskPath === right.taskPath;
510
+ }
511
+ errorMessage(error) {
512
+ return error instanceof Error ? error.message : String(error);
513
+ }
324
514
  scanHabits() {
325
515
  let files;
326
516
  try {
@@ -328,21 +518,36 @@ class HabitScheduler {
328
518
  }
329
519
  catch {
330
520
  this.parseErrors = [];
521
+ this.habitSnapshots.clear();
331
522
  return [];
332
523
  }
333
524
  const habits = [];
334
525
  const errors = [];
526
+ const snapshots = new Map();
335
527
  for (const file of files) {
336
528
  if (!file.endsWith(".md"))
337
529
  continue;
338
530
  const filePath = path.join(this.habitsDir, file);
339
531
  try {
340
532
  const content = this.deps.readFile(filePath, "utf-8");
341
- const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(path.dirname(this.habitsDir), (0, habit_parser_1.parseHabitFile)(content, filePath));
533
+ const definition = (0, habit_parser_1.parseHabitFile)(content, filePath);
534
+ const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(path.dirname(this.habitsDir), definition);
535
+ snapshots.set(habit.name, { content, definition });
342
536
  habits.push(habit);
537
+ if (habit.status === "degraded") {
538
+ const error = `habit definition degraded: ${habit.degradedReason}`;
539
+ errors.push({ file, error });
540
+ (0, runtime_1.emitNervesEvent)({
541
+ level: "error",
542
+ component: "daemon",
543
+ event: "daemon.habit_parse_error",
544
+ message: "failed to parse habit file",
545
+ meta: { file, error, agent: this.agent },
546
+ });
547
+ }
343
548
  }
344
549
  catch (error) {
345
- const errorMessage = error instanceof Error ? error.message : String(error);
550
+ const errorMessage = this.errorMessage(error);
346
551
  errors.push({ file, error: errorMessage });
347
552
  (0, runtime_1.emitNervesEvent)({
348
553
  level: "error",
@@ -358,6 +563,7 @@ class HabitScheduler {
358
563
  }
359
564
  }
360
565
  this.parseErrors = errors;
566
+ this.habitSnapshots = snapshots;
361
567
  return habits;
362
568
  }
363
569
  buildJobs(habits) {
@@ -398,6 +604,18 @@ class HabitScheduler {
398
604
  this.recordHabitParseError(`${habit.name}.md`, "habit names cannot start with reserved cron namespace 'await.'");
399
605
  return true;
400
606
  }
607
+ recordDegradedHabitError(file, habit) {
608
+ const prefix = "habit definition degraded: ";
609
+ if (habit.status !== "degraded") {
610
+ this.parseErrors = this.parseErrors.filter((entry) => entry.file !== file || !entry.error.startsWith(prefix));
611
+ return;
612
+ }
613
+ const error = `${prefix}${habit.degradedReason}`;
614
+ if (this.parseErrors.some((entry) => entry.file === file && entry.error === error))
615
+ return;
616
+ this.parseErrors = this.parseErrors.filter((entry) => entry.file !== file || !entry.error.startsWith(prefix));
617
+ this.recordHabitParseError(file, error);
618
+ }
401
619
  recordHabitParseError(file, error) {
402
620
  if (this.parseErrors.some((existing) => existing.file === file && existing.error === error))
403
621
  return;
@@ -254,6 +254,22 @@ function parseExtraRouteSpec(spec) {
254
254
  return { recipient: parts[0], channel: parts[1], key: parts[2] };
255
255
  }
256
256
  async function normalizeHabitPermissionEnvelope(habit, options) {
257
+ if (habit.status !== "active") {
258
+ const envelope = {
259
+ schemaVersion: 1,
260
+ canMessageOutward: false,
261
+ returnRoutes: [],
262
+ deniedTools: ["send_message", "surface"],
263
+ warnings: [`habit status ${habit.status} is non-executable`],
264
+ };
265
+ (0, runtime_1.emitNervesEvent)({
266
+ component: "daemon",
267
+ event: "daemon.habit_permission_envelope_normalized",
268
+ message: "habit permission envelope normalized",
269
+ meta: { agentRoot: options.agentRoot, habitName: habit.name, routes: 0, canMessageOutward: false },
270
+ });
271
+ return envelope;
272
+ }
257
273
  const returnRoutes = [];
258
274
  const warnings = [];
259
275
  if (habit.surface.family) {
@@ -44,6 +44,7 @@ const runtime_1 = require("../../nerves/runtime");
44
44
  const auth_flow_1 = require("../auth/auth-flow");
45
45
  const provider_models_1 = require("../provider-models");
46
46
  const habit_parser_1 = require("../habits/habit-parser");
47
+ const habit_lifecycle_1 = require("../habits/habit-lifecycle");
47
48
  const bundle_manifest_1 = require("../../mind/bundle-manifest");
48
49
  const record_paths_1 = require("../../mind/record-paths");
49
50
  const hatch_specialist_1 = require("./hatch-specialist");
@@ -77,16 +78,17 @@ function writeReadme(dir, purpose) {
77
78
  }
78
79
  }
79
80
  function writeHeartbeatHabit(bundleRoot, now) {
80
- const habitsDir = path.join(bundleRoot, "habits");
81
- fs.mkdirSync(habitsDir, { recursive: true });
82
- const filePath = path.join(habitsDir, "heartbeat.md");
83
81
  const content = (0, habit_parser_1.renderHabitFile)({
84
82
  title: "Heartbeat check-in",
85
83
  cadence: "30m",
86
84
  status: "active",
87
85
  created: now.toISOString(),
88
86
  }, "Run a lightweight heartbeat cycle. Review task board and inbox.\nCheck on pending obligations. Write important durable outputs to Arc or Desk record.");
89
- fs.writeFileSync(filePath, content, "utf-8");
87
+ (0, habit_lifecycle_1.publishNewHabitDefinition)({
88
+ agentRoot: bundleRoot,
89
+ habitId: "heartbeat",
90
+ bytes: content,
91
+ });
90
92
  }
91
93
  function writeFriendImprint(bundleRoot, humanName, now) {
92
94
  const friendsDir = path.join(bundleRoot, "friends");
@@ -568,38 +568,37 @@ function readHabitView(agentRoot, options = {}) {
568
568
  for (const file of (0, shared_1.safeReaddir)(habitsDir)) {
569
569
  if (!file.endsWith(".md"))
570
570
  continue;
571
+ const filePath = path.join(habitsDir, file);
572
+ let habit;
571
573
  try {
572
- const filePath = path.join(habitsDir, file);
573
574
  const raw = fs.readFileSync(filePath, "utf-8");
574
- if (!/^---\r?\n[\s\S]*?\r?\n---/.test(raw))
575
- continue;
576
- const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(raw, filePath));
577
- const cadenceMs = parseCadenceMs(habit.cadence);
578
- let isOverdue = false;
579
- let overdueMs = null;
580
- if (habit.status === "active" && habit.lastRun && cadenceMs) {
581
- const elapsed = now.getTime() - Date.parse(habit.lastRun);
582
- if (elapsed > cadenceMs) {
583
- isOverdue = true;
584
- overdueMs = elapsed - cadenceMs;
585
- }
586
- }
587
- items.push({
588
- name: habit.name,
589
- title: habit.title,
590
- cadence: habit.cadence,
591
- status: habit.status,
592
- lastRun: habit.lastRun,
593
- bodyExcerpt: (0, shared_1.truncateExcerpt)(habit.body, 120),
594
- isDegraded: false,
595
- degradedReason: null,
596
- isOverdue,
597
- overdueMs,
598
- });
575
+ habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(raw, filePath));
599
576
  }
600
577
  catch {
601
- // skip unparseable habit files
578
+ habit = (0, habit_parser_1.createDegradedHabitFile)(filePath, "read_error");
602
579
  }
580
+ const cadenceMs = parseCadenceMs(habit.cadence);
581
+ let isOverdue = false;
582
+ let overdueMs = null;
583
+ if (habit.status === "active" && habit.lastRun && cadenceMs) {
584
+ const elapsed = now.getTime() - Date.parse(habit.lastRun);
585
+ if (elapsed > cadenceMs) {
586
+ isOverdue = true;
587
+ overdueMs = elapsed - cadenceMs;
588
+ }
589
+ }
590
+ items.push({
591
+ name: habit.name,
592
+ title: habit.title,
593
+ cadence: habit.cadence,
594
+ status: habit.status,
595
+ lastRun: habit.lastRun,
596
+ bodyExcerpt: (0, shared_1.truncateExcerpt)(habit.body, 120),
597
+ isDegraded: habit.status === "degraded",
598
+ degradedReason: habit.status === "degraded" ? habit.degradedReason : null,
599
+ isOverdue,
600
+ overdueMs,
601
+ });
603
602
  }
604
603
  items.sort((a, b) => {
605
604
  if (a.isOverdue && !b.isOverdue)
@@ -1,11 +1,43 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EXPLICIT_PRIOR_WORK_RESUME_INSTRUCTION = exports.BACKGROUND_ONLY_INSTRUCTION = void 0;
4
+ exports.priorWorkInstruction = priorWorkInstruction;
5
+ exports.labelPriorWorkSurface = labelPriorWorkSurface;
3
6
  exports.extractMessageText = extractMessageText;
4
7
  exports.extractOrderedListReferents = extractOrderedListReferents;
5
8
  exports.buildOrientationFrame = buildOrientationFrame;
6
9
  exports.renderOrientationFrame = renderOrientationFrame;
7
10
  const runtime_1 = require("../nerves/runtime");
8
11
  const structured_output_1 = require("./structured-output");
12
+ exports.BACKGROUND_ONLY_INSTRUCTION = "Background only; do not execute.";
13
+ exports.EXPLICIT_PRIOR_WORK_RESUME_INSTRUCTION = "Prior work explicitly resumed by the current trigger.";
14
+ function priorWorkInstruction(resumePriorWork) {
15
+ return resumePriorWork
16
+ ? exports.EXPLICIT_PRIOR_WORK_RESUME_INSTRUCTION
17
+ : exports.BACKGROUND_ONLY_INSTRUCTION;
18
+ }
19
+ /**
20
+ * Put the prior-work authority label directly under a section heading when one
21
+ * exists, otherwise before the surface. This keeps the label adjacent to the
22
+ * exact stale-work surface it governs instead of relying on a distant prompt
23
+ * preamble.
24
+ */
25
+ function labelPriorWorkSurface(surface, resumePriorWork) {
26
+ if (!surface)
27
+ return "";
28
+ const instruction = priorWorkInstruction(resumePriorWork);
29
+ const inlineBoldHeading = /^(\*\*[^*]+:\*\*)\s+([\s\S]+)$/.exec(surface);
30
+ if (inlineBoldHeading) {
31
+ return `${inlineBoldHeading[1]}\n${instruction}\n${inlineBoldHeading[2]}`;
32
+ }
33
+ const newlineIndex = surface.indexOf("\n");
34
+ const firstLine = newlineIndex === -1 ? surface : surface.slice(0, newlineIndex);
35
+ if (firstLine.startsWith("## ") || /^\*\*[^*]+:\*\*$/.test(firstLine)) {
36
+ const remainder = newlineIndex === -1 ? "" : surface.slice(newlineIndex + 1);
37
+ return [firstLine, instruction, remainder].filter((part) => part.length > 0).join("\n");
38
+ }
39
+ return `${instruction}\n${surface}`;
40
+ }
9
41
  function isRecord(value) {
10
42
  return !!value && typeof value === "object" && !Array.isArray(value);
11
43
  }
@@ -221,7 +253,7 @@ function buildOrientationFrame(input) {
221
253
  }
222
254
  function renderOrientationFrame(frame) {
223
255
  const lines = [
224
- "## orientation frame",
256
+ "## Current trigger (authoritative)",
225
257
  `channel: ${frame.channel}`,
226
258
  `action policy: ${frame.actionPolicy.mode}`,
227
259
  ];
@@ -240,6 +272,31 @@ function renderOrientationFrame(frame) {
240
272
  if (frame.source) {
241
273
  lines.push("source:");
242
274
  lines.push(`- kind: ${frame.source.kind}`);
275
+ if (frame.source.authority === "presentation_only") {
276
+ lines.push("- source authority: presentation only; never tool authority");
277
+ }
278
+ if (frame.source.conversationKind) {
279
+ lines.push(`- conversation kind: ${frame.source.conversationKind}`);
280
+ }
281
+ if (frame.source.event) {
282
+ lines.push(`- event: ${frame.source.event.kind} (${frame.source.event.sourceEventType}; from me: ${String(frame.source.event.fromMe)})`);
283
+ }
284
+ if (frame.source.actor) {
285
+ const actorLabel = frame.source.actor.displayName?.trim() || frame.source.actor.externalId;
286
+ lines.push(`- observed actor: ${actorLabel} [${frame.source.actor.provider}:${frame.source.actor.externalId}]`);
287
+ }
288
+ if (frame.source.participants) {
289
+ for (const participant of frame.source.participants) {
290
+ const participantLabel = participant.displayName?.trim() || participant.externalId;
291
+ lines.push(`- group participant only: ${participantLabel} [${participant.provider}:${participant.externalId}]`);
292
+ }
293
+ }
294
+ if (frame.source.conversationKind === "group") {
295
+ lines.push("- participant membership is not evidence that someone spoke, read, requested, or authored a reaction target.");
296
+ }
297
+ if (frame.source.target) {
298
+ lines.push(`- target: ${frame.source.target.messageGuid} (authorship: ${frame.source.target.authorship})`);
299
+ }
243
300
  if (frame.source.lane)
244
301
  lines.push(`- lane: ${frame.source.lane}`);
245
302
  if (frame.source.defaultReplyTarget)
@@ -44,6 +44,9 @@ function httpStatus(error) {
44
44
  function delayForAttempt(policy, attempt) {
45
45
  return policy.baseDelayMs * Math.pow(policy.backoffMultiplier, attempt - 1);
46
46
  }
47
+ function isRetryable(error) {
48
+ return error.retryable !== false;
49
+ }
47
50
  async function runProviderAttempt(input) {
48
51
  const policy = normalizePolicy(input.policy);
49
52
  const maxAttempts = Math.max(1, Math.floor(policy.maxAttempts));
@@ -74,7 +77,7 @@ async function runProviderAttempt(input) {
74
77
  throw caught;
75
78
  const error = toError(caught);
76
79
  const classification = classify(caught, input.classifyError);
77
- const willRetry = attempt < maxAttempts;
80
+ const willRetry = isRetryable(error) && attempt < maxAttempts;
78
81
  const delayMs = willRetry ? delayForAttempt(policy, attempt) : undefined;
79
82
  const record = {
80
83
  attempt,