@ouro.bot/cli 0.1.0-alpha.696 → 0.1.0-alpha.697

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 (43) hide show
  1. package/changelog.json +9 -0
  2. package/dist/heart/autonomy-budget.js +434 -0
  3. package/dist/heart/core.js +11 -0
  4. package/dist/heart/daemon/cadence.js +110 -0
  5. package/dist/heart/daemon/cli-exec.js +20 -0
  6. package/dist/heart/daemon/cli-help.js +74 -0
  7. package/dist/heart/daemon/cli-parse.js +466 -0
  8. package/dist/heart/daemon/daemon.js +24 -3
  9. package/dist/heart/daemon/doctor.js +151 -0
  10. package/dist/heart/daemon/stale-bundle-prune.js +23 -6
  11. package/dist/heart/habits/habit-parser.js +4 -1
  12. package/dist/heart/habits/habit-scheduler.js +46 -38
  13. package/dist/heart/habits/habit-session.js +2 -3
  14. package/dist/heart/run-ledger.js +247 -0
  15. package/dist/heart/runtime-credentials.js +12 -3
  16. package/dist/mind/prompt-budget.js +380 -0
  17. package/dist/repertoire/tools-base.js +2 -0
  18. package/dist/repertoire/tools-rsvp.js +139 -0
  19. package/dist/rsvp/aisleplanner-client.js +354 -0
  20. package/dist/rsvp/cli.js +679 -0
  21. package/dist/rsvp/config.js +327 -0
  22. package/dist/rsvp/cutover.js +431 -0
  23. package/dist/rsvp/diagnostics.js +222 -0
  24. package/dist/rsvp/diff-renderer.js +159 -0
  25. package/dist/rsvp/habit-policy.js +85 -0
  26. package/dist/rsvp/habit-stage.js +93 -0
  27. package/dist/rsvp/incident-bundle.js +97 -0
  28. package/dist/rsvp/migration.js +204 -0
  29. package/dist/rsvp/outbound-state.js +254 -0
  30. package/dist/rsvp/query.js +118 -0
  31. package/dist/rsvp/replay.js +233 -0
  32. package/dist/rsvp/snapshot.js +232 -0
  33. package/dist/senses/bluebubbles/client.js +13 -7
  34. package/dist/senses/bluebubbles/index.js +227 -22
  35. package/dist/senses/bluebubbles/outbound-state.js +304 -0
  36. package/dist/senses/bluebubbles/replay.js +147 -0
  37. package/dist/senses/context-packet-ledger.js +245 -0
  38. package/dist/senses/context-packets.js +154 -0
  39. package/dist/senses/pipeline.js +91 -1
  40. package/dist/senses/private-runtime-worker.js +170 -31
  41. package/dist/senses/private-runtime.js +13 -1
  42. package/dist/senses/voice/twilio-phone.js +9 -5
  43. package/package.json +1 -1
@@ -13,6 +13,7 @@ exports.checkDaemon = checkDaemon;
13
13
  exports.checkAgents = checkAgents;
14
14
  exports.checkSenses = checkSenses;
15
15
  exports.checkHabits = checkHabits;
16
+ exports.checkRsvp = checkRsvp;
16
17
  exports.checkSecurity = checkSecurity;
17
18
  exports.checkTrips = checkTrips;
18
19
  exports.checkMailroom = checkMailroom;
@@ -28,6 +29,9 @@ const machine_identity_1 = require("../machine-identity");
28
29
  const habit_parser_1 = require("../habits/habit-parser");
29
30
  const cadence_1 = require("./cadence");
30
31
  const nerves_1 = require("../../nerves");
32
+ const config_1 = require("../../rsvp/config");
33
+ const cutover_1 = require("../../rsvp/cutover");
34
+ const diagnostics_1 = require("../../rsvp/diagnostics");
31
35
  const DEFAULT_BLUEBUBBLES_REQUEST_TIMEOUT_MS = 30_000;
32
36
  // ── Category checkers ──
33
37
  function checkCliPath(deps) {
@@ -397,6 +401,152 @@ function checkHabits(deps) {
397
401
  }
398
402
  return { name: "Habits", checks };
399
403
  }
404
+ function hasRsvpHabitSignal(deps, agentPath) {
405
+ const habitsDir = `${agentPath}/habits`;
406
+ if (!deps.existsSync(habitsDir))
407
+ return false;
408
+ try {
409
+ return deps.readdirSync(habitsDir).some((name) => name.toLowerCase().includes("rsvp") && name.endsWith(".md"));
410
+ }
411
+ catch {
412
+ return false;
413
+ }
414
+ }
415
+ function rsvpDoctorLabel(agentDir, checkId) {
416
+ const suffix = {
417
+ "rsvp.native_config": "RSVP native config",
418
+ "rsvp.aisleplanner_source": "RSVP AislePlanner source",
419
+ "rsvp.aisleplanner_credentials": "RSVP AislePlanner credentials",
420
+ "rsvp.bluebubbles_route": "RSVP BlueBubbles route",
421
+ "rsvp.bluebubbles_attachment": "RSVP BlueBubbles attachment",
422
+ };
423
+ return `${agentDir} ${suffix[checkId]}`;
424
+ }
425
+ function rsvpDoctorId(checkId) {
426
+ const ids = {
427
+ "rsvp.native_config": "rsvp.native_config",
428
+ "rsvp.aisleplanner_source": "rsvp.aisleplanner.source",
429
+ "rsvp.aisleplanner_credentials": "rsvp.aisleplanner.credentials",
430
+ "rsvp.bluebubbles_route": "rsvp.bluebubbles.route",
431
+ "rsvp.bluebubbles_attachment": "rsvp.bluebubbles.attachment",
432
+ };
433
+ return ids[checkId];
434
+ }
435
+ function rsvpDoctorDetail(readinessCheck) {
436
+ return readinessCheck.id === "rsvp.bluebubbles_route" && readinessCheck.status === "pass"
437
+ ? "configured"
438
+ : readinessCheck.detail;
439
+ }
440
+ function doctorStatus(status) {
441
+ return status;
442
+ }
443
+ function discoverRsvpCutoverLegacyRoot(deps, config) {
444
+ const configured = typeof config?.cutover?.legacyRoot === "string" ? config.cutover.legacyRoot.trim() : "";
445
+ if (configured)
446
+ return configured;
447
+ if (deps.rsvpCutoverLegacyRoot)
448
+ return deps.rsvpCutoverLegacyRoot;
449
+ const candidate = `${deps.homedir}/Projects/rsvp-tracker`;
450
+ return deps.existsSync(`${candidate}/config.json`) ? candidate : null;
451
+ }
452
+ function rsvpCutoverDetail(checks, sendAllowed, denialCount) {
453
+ return [
454
+ `sendAllowed=${sendAllowed}`,
455
+ `launchAgentInactive=${checks.launchAgentInactive}`,
456
+ `legacyProcessInactive=${checks.legacyProcessInactive}`,
457
+ `legacyConfigSendInactive=${checks.legacyConfigSendInactive}`,
458
+ `legacyLiveSendInactive=${checks.legacyLiveSendInactive}`,
459
+ `nativeBlueBubblesCredentialHealthy=${checks.nativeBlueBubblesCredentialHealthy}`,
460
+ `denialCount=${denialCount}`,
461
+ ].join("; ");
462
+ }
463
+ async function checkRsvp(deps) {
464
+ const checks = [];
465
+ const agents = discoverAgents(deps);
466
+ if (agents.length === 0) {
467
+ checks.push({ label: "RSVP", status: "warn", detail: "no agent bundles found" });
468
+ return { name: "RSVP", checks };
469
+ }
470
+ for (const agentDir of agents) {
471
+ const agentName = agentDir.replace(/\.ouro$/, "");
472
+ const agentPath = `${deps.bundlesRoot}/${agentDir}`;
473
+ const config = (0, config_1.readRsvpConfig)(agentPath);
474
+ const hasRsvpSignal = config.ok || hasRsvpHabitSignal(deps, agentPath);
475
+ if (!hasRsvpSignal) {
476
+ checks.push({ label: `${agentDir} RSVP`, status: "pass", detail: "not configured" });
477
+ continue;
478
+ }
479
+ const machineId = (0, machine_identity_1.loadOrCreateMachineIdentity)({ homeDir: deps.homedir }).machineId;
480
+ const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agentName, { preserveCachedOnFailure: true });
481
+ const machineRuntimeConfig = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agentName, machineId, { preserveCachedOnFailure: true });
482
+ const readiness = (0, config_1.validateRsvpReadiness)({
483
+ agent: agentName,
484
+ agentRoot: agentPath,
485
+ runtimeConfig,
486
+ machineRuntimeConfig,
487
+ config,
488
+ });
489
+ for (const readinessCheck of readiness.checks) {
490
+ checks.push({
491
+ id: rsvpDoctorId(readinessCheck.id),
492
+ label: rsvpDoctorLabel(agentDir, readinessCheck.id),
493
+ status: readinessCheck.status === "pass" ? "pass" : "fail",
494
+ detail: rsvpDoctorDetail(readinessCheck),
495
+ });
496
+ }
497
+ const attachment = readiness.checks.find((check) => check.id === "rsvp.bluebubbles_attachment");
498
+ if (attachment) {
499
+ checks.push({
500
+ id: "rsvp.bluebubbles.attachment_identity",
501
+ label: `${agentDir} RSVP BlueBubbles attachment identity`,
502
+ status: attachment.status === "pass" ? "pass" : "fail",
503
+ detail: attachment.status === "pass" ? "machine attachment credential present" : attachment.detail,
504
+ });
505
+ }
506
+ const diagnostics = (0, diagnostics_1.collectRsvpDiagnostics)(agentPath, deps);
507
+ checks.push({
508
+ id: "rsvp.context_packet_ledger",
509
+ label: `${agentDir} RSVP context packet ledger`,
510
+ status: doctorStatus(diagnostics.contextPacketLedger.status),
511
+ detail: diagnostics.contextPacketLedger.detail,
512
+ }, {
513
+ id: "rsvp.habit.schedule",
514
+ label: `${agentDir} RSVP habit schedule`,
515
+ status: doctorStatus(diagnostics.habitSchedule.status),
516
+ detail: diagnostics.habitSchedule.detail,
517
+ }, {
518
+ id: "rsvp.latest_fetch",
519
+ label: `${agentDir} RSVP latest fetch`,
520
+ status: doctorStatus(diagnostics.latestFetch.status),
521
+ detail: diagnostics.latestFetch.detail,
522
+ }, {
523
+ id: "rsvp.delivery.reconciliation",
524
+ label: `${agentDir} RSVP delivery reconciliation`,
525
+ status: doctorStatus(diagnostics.deliveryReconciliation.status),
526
+ detail: diagnostics.deliveryReconciliation.detail,
527
+ }, {
528
+ id: "rsvp.spend_timeline",
529
+ label: `${agentDir} RSVP spend timeline`,
530
+ status: doctorStatus(diagnostics.spendTimeline.status),
531
+ detail: diagnostics.spendTimeline.detail,
532
+ });
533
+ const legacyRoot = discoverRsvpCutoverLegacyRoot(deps, config.ok ? config.config : undefined);
534
+ if (config.ok && legacyRoot) {
535
+ const cutover = await (0, cutover_1.checkRsvpCutover)({
536
+ agent: agentName,
537
+ legacyRoot,
538
+ ...(deps.rsvpCutoverDeps ? { deps: deps.rsvpCutoverDeps } : {}),
539
+ });
540
+ checks.push({
541
+ id: "rsvp.cutover.live_send_preflight",
542
+ label: `${agentDir} RSVP legacy live-send preflight`,
543
+ status: cutover.sendAllowed ? "pass" : "fail",
544
+ detail: rsvpCutoverDetail(cutover.checks, cutover.sendAllowed, cutover.denialReasons.length),
545
+ });
546
+ }
547
+ }
548
+ return { name: "RSVP", checks };
549
+ }
400
550
  function checkSecurity(deps) {
401
551
  const checks = [];
402
552
  const agents = discoverAgents(deps);
@@ -934,6 +1084,7 @@ const CATEGORY_CHECKERS = [
934
1084
  { name: "Agents", fn: checkAgents },
935
1085
  { name: "Senses", fn: checkSenses },
936
1086
  { name: "Habits", fn: checkHabits },
1087
+ { name: "RSVP", fn: checkRsvp },
937
1088
  { name: "Security", fn: checkSecurity },
938
1089
  { name: "Trips", fn: checkTrips },
939
1090
  { name: "Mailroom", fn: checkMailroom },
@@ -39,13 +39,16 @@ const path = __importStar(require("path"));
39
39
  const identity_1 = require("../identity");
40
40
  const runtime_1 = require("../../nerves/runtime");
41
41
  /**
42
- * Scan the bundles root for `.ouro` directories that have no `agent.json`
43
- * file (definitively dead ephemeral bundles) and delete them.
42
+ * Scan the bundles root for `.ouro` directories that have no bundle contract
43
+ * or durable state markers (definitively dead ephemeral bundles) and delete
44
+ * them.
44
45
  *
45
46
  * Returns a list of pruned bundle directory names (e.g. `["stale.ouro"]`)
46
47
  * for display purposes. Bundles that have `agent.json` -- even if disabled
47
- * -- are never deleted. Errors on individual bundles are swallowed so that
48
- * one permission-denied doesn't block pruning the rest.
48
+ * -- are never deleted. Task-doc-only bundles are also durable work state, so
49
+ * directories with state roots such as `tasks`, `desk`, `arc`, `state`, or
50
+ * `.git` are preserved. Errors on individual bundles are swallowed so that one
51
+ * permission-denied doesn't block pruning the rest.
49
52
  */
50
53
  function pruneStaleEphemeralBundles(deps = {}) {
51
54
  const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
@@ -64,8 +67,7 @@ function pruneStaleEphemeralBundles(deps = {}) {
64
67
  if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
65
68
  continue;
66
69
  const bundlePath = path.join(bundlesRoot, entry.name);
67
- const agentJsonPath = path.join(bundlePath, "agent.json");
68
- if (existsSync(agentJsonPath))
70
+ if (hasDurableBundleMarker(bundlePath, existsSync))
69
71
  continue;
70
72
  try {
71
73
  rmSync(bundlePath, { recursive: true, force: true });
@@ -94,3 +96,18 @@ function pruneStaleEphemeralBundles(deps = {}) {
94
96
  }
95
97
  return pruned;
96
98
  }
99
+ const DURABLE_BUNDLE_MARKERS = [
100
+ "agent.json",
101
+ "tasks",
102
+ "desk",
103
+ "arc",
104
+ "state",
105
+ "habits",
106
+ "friends",
107
+ "facts",
108
+ "vault",
109
+ ".git",
110
+ ];
111
+ function hasDurableBundleMarker(bundlePath, existsSync) {
112
+ return DURABLE_BUNDLE_MARKERS.some((marker) => existsSync(path.join(bundlePath, marker)));
113
+ }
@@ -38,6 +38,7 @@ exports.renderHabitFile = renderHabitFile;
38
38
  const path = __importStar(require("path"));
39
39
  const frontmatter_1 = require("../../util/frontmatter");
40
40
  const runtime_1 = require("../../nerves/runtime");
41
+ const habit_policy_1 = require("../../rsvp/habit-policy");
41
42
  function isHabitStatus(value) {
42
43
  return value === "active" || value === "paused";
43
44
  }
@@ -157,7 +158,8 @@ function parseHabitFile(content, filePath) {
157
158
  const lastRun = typeof rawLastRun === "string" && rawLastRun.length > 0 ? rawLastRun : null;
158
159
  const rawCreated = frontmatter.created;
159
160
  const created = typeof rawCreated === "string" && rawCreated.length > 0 ? rawCreated : null;
160
- const tools = parseToolsField(frontmatter.tools);
161
+ const rsvp = (0, habit_policy_1.parseRsvpHabitMetadata)(frontmatter.rsvp);
162
+ const tools = rsvp ? [...habit_policy_1.RSVP_HABIT_ALLOWED_TOOLS] : parseToolsField(frontmatter.tools);
161
163
  const origin = parseOrigin(frontmatter.origin);
162
164
  const surface = parseSurface(frontmatter.surface);
163
165
  const continuity = parseContinuity(frontmatter.continuity);
@@ -169,6 +171,7 @@ function parseHabitFile(content, filePath) {
169
171
  lastRun,
170
172
  created,
171
173
  tools,
174
+ ...(rsvp ? { rsvp } : {}),
172
175
  origin,
173
176
  surface,
174
177
  continuity,
@@ -39,6 +39,7 @@ const runtime_1 = require("../../nerves/runtime");
39
39
  const habit_parser_1 = require("./habit-parser");
40
40
  const habit_runtime_state_1 = require("./habit-runtime-state");
41
41
  const cadence_1 = require("../daemon/cadence");
42
+ const habit_policy_1 = require("../../rsvp/habit-policy");
42
43
  const WATCH_DEBOUNCE_MS = 200;
43
44
  class HabitScheduler {
44
45
  agent;
@@ -97,33 +98,21 @@ class HabitScheduler {
97
98
  continue;
98
99
  if (!habit.cadence)
99
100
  continue;
100
- const cadenceMs = (0, cadence_1.parseCadenceToMs)(habit.cadence);
101
- if (cadenceMs === null)
101
+ if (this.rejectInvalidRsvpHabit(habit))
102
102
  continue;
103
103
  const nowMs = this.deps.now();
104
- if (habit.lastRun === null) {
105
- (0, runtime_1.emitNervesEvent)({
106
- component: "daemon",
107
- event: "daemon.habit_fire",
108
- message: "firing overdue habit (never run)",
109
- meta: { habitName: habit.name, agent: this.agent },
110
- });
111
- this.onHabitFire(habit.name, "overdue", {
112
- occurrenceId: this.overdueOccurrenceId(habit),
113
- });
104
+ const dueState = (0, cadence_1.evaluateCadenceDue)(habit.cadence, habit.lastRun, nowMs);
105
+ if (dueState === null)
114
106
  continue;
115
- }
116
- const lastRunMs = new Date(habit.lastRun).getTime();
117
- const elapsed = nowMs - lastRunMs;
118
- if (elapsed >= cadenceMs) {
107
+ if (dueState.due) {
119
108
  (0, runtime_1.emitNervesEvent)({
120
109
  component: "daemon",
121
110
  event: "daemon.habit_fire",
122
- message: "firing overdue habit",
123
- meta: { habitName: habit.name, agent: this.agent, elapsedMs: elapsed },
111
+ message: habit.lastRun === null ? "firing overdue habit (never run)" : "firing overdue habit",
112
+ meta: { habitName: habit.name, agent: this.agent, elapsedMs: dueState.elapsedMs },
124
113
  });
125
114
  this.onHabitFire(habit.name, "overdue", {
126
- occurrenceId: this.overdueOccurrenceId(habit),
115
+ occurrenceId: dueState.occurrenceId ?? this.overdueOccurrenceId(habit),
127
116
  });
128
117
  }
129
118
  }
@@ -150,18 +139,11 @@ class HabitScheduler {
150
139
  continue;
151
140
  if (!habit.cadence)
152
141
  continue;
153
- const cadenceMs = (0, cadence_1.parseCadenceToMs)(habit.cadence);
154
- if (cadenceMs === null)
142
+ if (this.rejectInvalidRsvpHabit(habit))
155
143
  continue;
156
- if (habit.lastRun === null) {
157
- overdue.push({ name: habit.name, elapsedMs: Infinity });
158
- continue;
159
- }
160
- const lastRunMs = new Date(habit.lastRun).getTime();
161
- const elapsed = nowMs - lastRunMs;
162
- if (elapsed >= cadenceMs) {
163
- overdue.push({ name: habit.name, elapsedMs: elapsed });
164
- }
144
+ const dueState = (0, cadence_1.evaluateCadenceDue)(habit.cadence, habit.lastRun, nowMs);
145
+ if (dueState?.due)
146
+ overdue.push({ name: habit.name, elapsedMs: dueState.elapsedMs });
165
147
  }
166
148
  return overdue;
167
149
  }
@@ -278,9 +260,9 @@ class HabitScheduler {
278
260
  });
279
261
  // Parse cadence from the original habit file for timer interval
280
262
  const habitFile = this.getHabitFile(job.taskId);
281
- const ms = habitFile?.cadence ? (0, cadence_1.parseCadenceToMs)(habitFile.cadence) : null;
263
+ const ms = habitFile?.cadence ? (0, cadence_1.cadenceFallbackDelayMs)(habitFile.cadence, this.deps.now()) : null;
282
264
  if (ms !== null) {
283
- this.createTimerFallback(job.taskId, ms);
265
+ this.createTimerFallback(job.taskId, habitFile.cadence, ms);
284
266
  }
285
267
  this.degradedHabitNames.set(job.taskId, "cron registration failed — using timer fallback");
286
268
  }
@@ -315,14 +297,15 @@ class HabitScheduler {
315
297
  }
316
298
  return verified;
317
299
  }
318
- createTimerFallback(habitName, cadenceMs) {
300
+ createTimerFallback(habitName, cadence, firstDelayMs) {
319
301
  const schedule = () => {
302
+ const delayMs = (0, cadence_1.cadenceFallbackDelayMs)(cadence, this.deps.now()) ?? firstDelayMs;
320
303
  const timer = setTimeout(() => {
321
304
  this.onHabitFire(habitName, "overdue", {
322
- occurrenceId: this.timerOccurrenceId(habitName, cadenceMs),
305
+ occurrenceId: this.timerOccurrenceId(habitName, cadence),
323
306
  });
324
307
  schedule();
325
- }, cadenceMs);
308
+ }, delayMs);
326
309
  this.timerFallbacks.set(habitName, timer);
327
310
  };
328
311
  schedule();
@@ -380,6 +363,8 @@ class HabitScheduler {
380
363
  continue;
381
364
  if (!habit.cadence)
382
365
  continue;
366
+ if (this.rejectInvalidRsvpHabit(habit))
367
+ continue;
383
368
  const cronSchedule = (0, cadence_1.parseCadenceToCron)(habit.cadence);
384
369
  if (cronSchedule === null)
385
370
  continue;
@@ -395,6 +380,24 @@ class HabitScheduler {
395
380
  }
396
381
  return jobs;
397
382
  }
383
+ rejectInvalidRsvpHabit(habit) {
384
+ if (!(0, habit_policy_1.isRsvpHabitName)(habit.name) || habit.rsvp)
385
+ return false;
386
+ this.recordHabitParseError(`${habit.name}.md`, "RSVP habit metadata is required before scheduling");
387
+ return true;
388
+ }
389
+ recordHabitParseError(file, error) {
390
+ if (this.parseErrors.some((existing) => existing.file === file && existing.error === error))
391
+ return;
392
+ this.parseErrors.push({ file, error });
393
+ (0, runtime_1.emitNervesEvent)({
394
+ level: "error",
395
+ component: "daemon",
396
+ event: "daemon.habit_parse_error",
397
+ message: "failed to parse habit file",
398
+ meta: { file, error, agent: this.agent },
399
+ });
400
+ }
398
401
  overdueOccurrenceId(habit) {
399
402
  if (habit.lastRun === null)
400
403
  return `overdue:first-run:${habit.cadence}`;
@@ -403,9 +406,14 @@ class HabitScheduler {
403
406
  jobOccurrenceId(job, trigger) {
404
407
  return `job:${job.id}:${trigger}:last-run:${job.lastRun ?? "never"}`;
405
408
  }
406
- timerOccurrenceId(habitName, cadenceMs) {
407
- const slot = Math.floor(this.deps.now() / cadenceMs);
408
- return `timer:${habitName}:cadence-ms:${cadenceMs}:slot:${slot}`;
409
+ timerOccurrenceId(habitName, cadence) {
410
+ const cadenceMs = (0, cadence_1.parseCadenceToMs)(cadence);
411
+ if (cadenceMs !== null) {
412
+ const slot = Math.floor(this.deps.now() / cadenceMs);
413
+ return `timer:${habitName}:cadence-ms:${cadenceMs}:slot:${slot}`;
414
+ }
415
+ const slot = (0, cadence_1.nextCadenceRunAt)(cadence, this.deps.now()) ?? "unknown";
416
+ return `timer:${habitName}:cadence:${cadence}:slot:${slot}`;
409
417
  }
410
418
  }
411
419
  exports.HabitScheduler = HabitScheduler;
@@ -453,11 +453,10 @@ function filterHabitToolsForEnvelope(definitions, requestedTools, envelope, risk
453
453
  function computeNextRunAt(habit, endedAt) {
454
454
  if (habit.status !== "active" || !habit.cadence)
455
455
  return null;
456
- const cadenceMs = (0, cadence_1.parseCadenceToMs)(habit.cadence);
457
456
  const endedMs = Date.parse(endedAt);
458
- if (cadenceMs === null || !Number.isFinite(endedMs))
457
+ if (!Number.isFinite(endedMs))
459
458
  return null;
460
- return new Date(endedMs + cadenceMs).toISOString();
459
+ return (0, cadence_1.nextCadenceRunAt)(habit.cadence, endedMs);
461
460
  }
462
461
  function defaultSummarySnapshot(input) {
463
462
  const errors = input.errors ?? [];
@@ -0,0 +1,247 @@
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.runLedgerHash = runLedgerHash;
37
+ exports.usageMetadataFromUsageData = usageMetadataFromUsageData;
38
+ exports.deriveRunLedgerIds = deriveRunLedgerIds;
39
+ exports.createRunLedgerRecord = createRunLedgerRecord;
40
+ exports.runLedgerPath = runLedgerPath;
41
+ exports.appendRunLedgerRecord = appendRunLedgerRecord;
42
+ exports.appendRunLedgerRecordNonFatal = appendRunLedgerRecordNonFatal;
43
+ exports.readRunLedger = readRunLedger;
44
+ const node_crypto_1 = require("node:crypto");
45
+ const fs = __importStar(require("node:fs"));
46
+ const path = __importStar(require("node:path"));
47
+ const runtime_1 = require("../nerves/runtime");
48
+ function sha256Hex(input) {
49
+ return (0, node_crypto_1.createHash)("sha256").update(input).digest("hex");
50
+ }
51
+ function canonicalJson(value) {
52
+ if (Array.isArray(value)) {
53
+ return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
54
+ }
55
+ if (value && typeof value === "object") {
56
+ const record = value;
57
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
58
+ }
59
+ return JSON.stringify(value);
60
+ }
61
+ function hashValue(value) {
62
+ return sha256Hex(canonicalJson(value));
63
+ }
64
+ function runLedgerHash(value) {
65
+ return `sha256:${hashValue(value)}`;
66
+ }
67
+ function stableUnique(values) {
68
+ return [...new Set((values ?? []).filter((value) => typeof value === "string" && value.trim().length > 0))];
69
+ }
70
+ function numberOrZero(value) {
71
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
72
+ }
73
+ function usageMetadataFromUsageData(usage, source) {
74
+ return {
75
+ source,
76
+ inputTokens: numberOrZero(usage?.input_tokens),
77
+ outputTokens: numberOrZero(usage?.output_tokens),
78
+ reasoningTokens: numberOrZero(usage?.reasoning_tokens),
79
+ totalTokens: numberOrZero(usage?.total_tokens),
80
+ };
81
+ }
82
+ function deriveRunLedgerIds(input) {
83
+ const targetHash = runLedgerHash(input.target);
84
+ const idempotencyKey = `idem_${hashValue({
85
+ agent: input.agent,
86
+ triggerType: input.triggerType,
87
+ sourceKind: input.sourceKind,
88
+ senseOrHabit: input.senseOrHabit,
89
+ scope: input.idempotencyScope ?? input.target,
90
+ }).slice(0, 32)}`;
91
+ const runId = `run_${hashValue({
92
+ agent: input.agent,
93
+ triggerType: input.triggerType,
94
+ sourceKind: input.sourceKind,
95
+ senseOrHabit: input.senseOrHabit,
96
+ targetHash,
97
+ idempotencyKey,
98
+ parentRunId: input.parentRunId ?? null,
99
+ }).slice(0, 24)}`;
100
+ return {
101
+ runId,
102
+ rootRunId: input.rootRunId ?? input.parentRunId ?? runId,
103
+ ...(input.parentRunId ? { parentRunId: input.parentRunId } : {}),
104
+ idempotencyKey,
105
+ targetHash,
106
+ };
107
+ }
108
+ function createRunLedgerRecord(input) {
109
+ const ids = deriveRunLedgerIds(input);
110
+ return {
111
+ schemaVersion: 1,
112
+ recordedAt: new Date().toISOString(),
113
+ ...ids,
114
+ agent: input.agent,
115
+ triggerType: input.triggerType,
116
+ sourceKind: input.sourceKind,
117
+ senseOrHabit: input.senseOrHabit,
118
+ lifecycle: input.lifecycle,
119
+ startedAt: input.startedAt,
120
+ ...(input.endedAt ? { endedAt: input.endedAt } : {}),
121
+ ...(input.usage ? { usage: input.usage } : {}),
122
+ ...(input.provider ? { provider: input.provider } : {}),
123
+ ...(input.model ? { model: input.model } : {}),
124
+ ...(input.sessionRef ? { sessionRef: input.sessionRef } : {}),
125
+ contextPacketIds: stableUnique(input.contextPacketIds),
126
+ contentStored: false,
127
+ ...(input.errorName ? { errorName: input.errorName } : {}),
128
+ ...(input.errorCode ? { errorCode: input.errorCode } : {}),
129
+ };
130
+ }
131
+ function runLedgerPath(agentRoot) {
132
+ return path.join(agentRoot, "state", "run-ledger", "runs.jsonl");
133
+ }
134
+ function appendRunLedgerRecord(agentRoot, record) {
135
+ const filePath = runLedgerPath(agentRoot);
136
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
137
+ fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`, "utf-8");
138
+ (0, runtime_1.emitNervesEvent)({
139
+ component: "heart",
140
+ event: "heart.run_ledger_recorded",
141
+ message: "recorded causal run ledger row",
142
+ meta: {
143
+ agent: record.agent,
144
+ runId: record.runId,
145
+ triggerType: record.triggerType,
146
+ sourceKind: record.sourceKind,
147
+ lifecycle: record.lifecycle,
148
+ senseOrHabit: record.senseOrHabit,
149
+ },
150
+ });
151
+ }
152
+ function appendRunLedgerRecordNonFatal(agentRoot, record) {
153
+ try {
154
+ appendRunLedgerRecord(agentRoot, record);
155
+ return true;
156
+ }
157
+ catch (error) {
158
+ (0, runtime_1.emitNervesEvent)({
159
+ level: "error",
160
+ component: "heart",
161
+ event: "heart.run_ledger_record_error",
162
+ message: "failed to record causal run ledger row",
163
+ meta: {
164
+ agent: record.agent,
165
+ runId: record.runId,
166
+ lifecycle: record.lifecycle,
167
+ error: String(error),
168
+ },
169
+ });
170
+ return false;
171
+ }
172
+ }
173
+ function isStringArray(value) {
174
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
175
+ }
176
+ function isUsageMetadata(value) {
177
+ const usage = value;
178
+ return !!usage
179
+ && (usage.source === "provider" || usage.source === "none" || usage.source === "reported-unavailable")
180
+ && typeof usage.inputTokens === "number"
181
+ && typeof usage.outputTokens === "number"
182
+ && typeof usage.reasoningTokens === "number"
183
+ && typeof usage.totalTokens === "number";
184
+ }
185
+ function isSessionRef(value) {
186
+ const ref = value;
187
+ return !!ref
188
+ && typeof ref.channel === "string"
189
+ && typeof ref.keyHash === "string";
190
+ }
191
+ function isRunLedgerRecord(value) {
192
+ const record = value;
193
+ return !!record
194
+ && record.schemaVersion === 1
195
+ && typeof record.recordedAt === "string"
196
+ && typeof record.runId === "string"
197
+ && typeof record.rootRunId === "string"
198
+ && (record.parentRunId === undefined || typeof record.parentRunId === "string")
199
+ && typeof record.idempotencyKey === "string"
200
+ && typeof record.agent === "string"
201
+ && (record.triggerType === "inbound" || record.triggerType === "habit" || record.triggerType === "reload" || record.triggerType === "recovery" || record.triggerType === "manual")
202
+ && (record.sourceKind === "sense" || record.sourceKind === "private-runtime" || record.sourceKind === "daemon" || record.sourceKind === "cli")
203
+ && typeof record.senseOrHabit === "string"
204
+ && typeof record.targetHash === "string"
205
+ && (record.lifecycle === "started" || record.lifecycle === "completed" || record.lifecycle === "skipped" || record.lifecycle === "blocked" || record.lifecycle === "error")
206
+ && typeof record.startedAt === "string"
207
+ && (record.endedAt === undefined || typeof record.endedAt === "string")
208
+ && (record.usage === undefined || isUsageMetadata(record.usage))
209
+ && (record.provider === undefined || typeof record.provider === "string")
210
+ && (record.model === undefined || typeof record.model === "string")
211
+ && (record.sessionRef === undefined || isSessionRef(record.sessionRef))
212
+ && isStringArray(record.contextPacketIds)
213
+ && record.contentStored === false
214
+ && (record.errorName === undefined || typeof record.errorName === "string")
215
+ && (record.errorCode === undefined || typeof record.errorCode === "string");
216
+ }
217
+ function emitMalformedRunLedgerRow(lineNumber, reason) {
218
+ (0, runtime_1.emitNervesEvent)({
219
+ level: "warn",
220
+ component: "heart",
221
+ event: "heart.run_ledger_malformed",
222
+ message: "skipped malformed causal run ledger row",
223
+ meta: { lineNumber, reason },
224
+ });
225
+ }
226
+ function readRunLedger(agentRoot) {
227
+ const filePath = runLedgerPath(agentRoot);
228
+ if (!fs.existsSync(filePath))
229
+ return [];
230
+ const rows = [];
231
+ const lines = fs.readFileSync(filePath, "utf-8").split("\n").filter((line) => line.trim().length > 0);
232
+ lines.forEach((line, index) => {
233
+ try {
234
+ const parsed = JSON.parse(line);
235
+ if (isRunLedgerRecord(parsed)) {
236
+ rows.push(parsed);
237
+ }
238
+ else {
239
+ emitMalformedRunLedgerRow(index + 1, "invalid shape");
240
+ }
241
+ }
242
+ catch (error) {
243
+ emitMalformedRunLedgerRow(index + 1, String(error));
244
+ }
245
+ });
246
+ return rows;
247
+ }