@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.
- package/changelog.json +9 -0
- package/dist/heart/autonomy-budget.js +434 -0
- package/dist/heart/core.js +11 -0
- package/dist/heart/daemon/cadence.js +110 -0
- package/dist/heart/daemon/cli-exec.js +20 -0
- package/dist/heart/daemon/cli-help.js +74 -0
- package/dist/heart/daemon/cli-parse.js +466 -0
- package/dist/heart/daemon/daemon.js +24 -3
- package/dist/heart/daemon/doctor.js +151 -0
- package/dist/heart/daemon/stale-bundle-prune.js +23 -6
- package/dist/heart/habits/habit-parser.js +4 -1
- package/dist/heart/habits/habit-scheduler.js +46 -38
- package/dist/heart/habits/habit-session.js +2 -3
- package/dist/heart/run-ledger.js +247 -0
- package/dist/heart/runtime-credentials.js +12 -3
- package/dist/mind/prompt-budget.js +380 -0
- package/dist/repertoire/tools-base.js +2 -0
- package/dist/repertoire/tools-rsvp.js +139 -0
- package/dist/rsvp/aisleplanner-client.js +354 -0
- package/dist/rsvp/cli.js +679 -0
- package/dist/rsvp/config.js +327 -0
- package/dist/rsvp/cutover.js +431 -0
- package/dist/rsvp/diagnostics.js +222 -0
- package/dist/rsvp/diff-renderer.js +159 -0
- package/dist/rsvp/habit-policy.js +85 -0
- package/dist/rsvp/habit-stage.js +93 -0
- package/dist/rsvp/incident-bundle.js +97 -0
- package/dist/rsvp/migration.js +204 -0
- package/dist/rsvp/outbound-state.js +254 -0
- package/dist/rsvp/query.js +118 -0
- package/dist/rsvp/replay.js +233 -0
- package/dist/rsvp/snapshot.js +232 -0
- package/dist/senses/bluebubbles/client.js +13 -7
- package/dist/senses/bluebubbles/index.js +227 -22
- package/dist/senses/bluebubbles/outbound-state.js +304 -0
- package/dist/senses/bluebubbles/replay.js +147 -0
- package/dist/senses/context-packet-ledger.js +245 -0
- package/dist/senses/context-packets.js +154 -0
- package/dist/senses/pipeline.js +91 -1
- package/dist/senses/private-runtime-worker.js +170 -31
- package/dist/senses/private-runtime.js +13 -1
- package/dist/senses/voice/twilio-phone.js +9 -5
- 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
|
|
43
|
-
*
|
|
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.
|
|
48
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
101
|
-
if (cadenceMs === null)
|
|
101
|
+
if (this.rejectInvalidRsvpHabit(habit))
|
|
102
102
|
continue;
|
|
103
103
|
const nowMs = this.deps.now();
|
|
104
|
-
|
|
105
|
-
|
|
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:
|
|
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
|
-
|
|
154
|
-
if (cadenceMs === null)
|
|
142
|
+
if (this.rejectInvalidRsvpHabit(habit))
|
|
155
143
|
continue;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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.
|
|
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,
|
|
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,
|
|
305
|
+
occurrenceId: this.timerOccurrenceId(habitName, cadence),
|
|
323
306
|
});
|
|
324
307
|
schedule();
|
|
325
|
-
},
|
|
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,
|
|
407
|
-
const
|
|
408
|
-
|
|
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 (
|
|
457
|
+
if (!Number.isFinite(endedMs))
|
|
459
458
|
return null;
|
|
460
|
-
return
|
|
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
|
+
}
|