@ouro.bot/cli 0.1.0-alpha.696 → 0.1.0-alpha.698
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 +18 -0
- package/dist/heart/agent-entry.js +32 -2
- 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-entry.js +104 -3
- 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/provider-credentials.js +16 -0
- 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 +704 -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 +161 -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
|
@@ -68,6 +68,7 @@ const context_loss_sentinel_1 = require("../context-loss-sentinel");
|
|
|
68
68
|
const provider_credentials_1 = require("../provider-credentials");
|
|
69
69
|
const runtime_credentials_1 = require("../runtime-credentials");
|
|
70
70
|
const machine_identity_1 = require("../machine-identity");
|
|
71
|
+
const auth_flow_1 = require("../auth/auth-flow");
|
|
71
72
|
function parseSocketPath(argv) {
|
|
72
73
|
const socketIndex = argv.indexOf("--socket");
|
|
73
74
|
if (socketIndex >= 0) {
|
|
@@ -483,16 +484,116 @@ function writeStopCommandHealthState() {
|
|
|
483
484
|
// Health writes are best-effort during shutdown.
|
|
484
485
|
}
|
|
485
486
|
}
|
|
487
|
+
function providerPreloadTargets(agent) {
|
|
488
|
+
try {
|
|
489
|
+
const { config } = (0, auth_flow_1.readAgentConfigForAgent)(agent, (0, identity_1.getAgentBundlesRoot)());
|
|
490
|
+
return [...new Set([config.humanFacing.provider, config.agentFacing.provider])];
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
(0, runtime_1.emitNervesEvent)({
|
|
494
|
+
level: "warn",
|
|
495
|
+
component: "daemon",
|
|
496
|
+
event: "daemon.provider_preload_skipped",
|
|
497
|
+
message: "skipping provider credential preload because agent config could not be read",
|
|
498
|
+
meta: {
|
|
499
|
+
agent,
|
|
500
|
+
error: error instanceof Error ? error.message : /* v8 ignore next -- defensive non-Error config-read failures @preserve */ String(error),
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function providerPreloadMissingTargets(result, providers) {
|
|
507
|
+
return providers.filter((provider) => !result.pool.providers[provider]);
|
|
508
|
+
}
|
|
486
509
|
async function preloadProviderCredentialPools() {
|
|
487
|
-
await Promise.all(managedAgents.map((agent) =>
|
|
510
|
+
await Promise.all(managedAgents.map(async (agent) => {
|
|
511
|
+
const providers = providerPreloadTargets(agent);
|
|
512
|
+
if (providers.length === 0)
|
|
513
|
+
return;
|
|
514
|
+
const result = await (0, provider_credentials_1.refreshProviderCredentialPool)(agent, { preserveCachedOnFailure: true, providers, skipCache: true });
|
|
515
|
+
if (result.ok) {
|
|
516
|
+
const missingProviders = providerPreloadMissingTargets(result, providers);
|
|
517
|
+
if (missingProviders.length > 0) {
|
|
518
|
+
(0, runtime_1.emitNervesEvent)({
|
|
519
|
+
level: "warn",
|
|
520
|
+
component: "daemon",
|
|
521
|
+
event: "daemon.provider_preload_unavailable",
|
|
522
|
+
message: "provider credential preload returned an incomplete selected provider cache",
|
|
523
|
+
meta: {
|
|
524
|
+
agent,
|
|
525
|
+
reason: "missing",
|
|
526
|
+
error: `missing selected providers: ${missingProviders.join(", ")}`,
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const records = Object.values(result.pool.providers).filter((record) => !!record);
|
|
532
|
+
(0, provider_credentials_1.cacheProviderCredentialRecords)(agent, records, new Date(result.pool.updatedAt));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
(0, runtime_1.emitNervesEvent)({
|
|
536
|
+
level: "warn",
|
|
537
|
+
component: "daemon",
|
|
538
|
+
event: "daemon.provider_preload_unavailable",
|
|
539
|
+
message: "provider credential preload could not refresh selected provider cache",
|
|
540
|
+
meta: {
|
|
541
|
+
agent,
|
|
542
|
+
reason: result.reason,
|
|
543
|
+
error: result.error,
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
}));
|
|
547
|
+
}
|
|
548
|
+
function startProviderCredentialPoolPreload() {
|
|
549
|
+
return preloadProviderCredentialPools().catch((error) => {
|
|
550
|
+
(0, runtime_1.emitNervesEvent)({
|
|
551
|
+
level: "error",
|
|
552
|
+
component: "daemon",
|
|
553
|
+
event: "daemon.provider_preload_error",
|
|
554
|
+
message: "provider credential preload failed after daemon startup",
|
|
555
|
+
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive non-Error provider preload failures @preserve */ String(error) },
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
function scheduleStartupSentinelAfterProviderPreload(agent, preload) {
|
|
560
|
+
void preload.then(async () => {
|
|
561
|
+
const result = await refreshDaemonSentinel(agent, "daemon_startup");
|
|
562
|
+
if (result.status === "critical") {
|
|
563
|
+
(0, runtime_1.emitNervesEvent)({
|
|
564
|
+
level: "error",
|
|
565
|
+
component: "daemon",
|
|
566
|
+
event: "daemon.startup_sentinel_error",
|
|
567
|
+
message: "startup Sentinel refresh reported critical after provider preload",
|
|
568
|
+
meta: {
|
|
569
|
+
agent,
|
|
570
|
+
error: result.message,
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
/* v8 ignore start -- refreshDaemonSentinel handles ordinary Sentinel failures; this is a promise-chain guard. @preserve */
|
|
575
|
+
}).catch((error) => {
|
|
576
|
+
(0, runtime_1.emitNervesEvent)({
|
|
577
|
+
level: "error",
|
|
578
|
+
component: "daemon",
|
|
579
|
+
event: "daemon.startup_sentinel_error",
|
|
580
|
+
message: "startup Sentinel refresh failed after provider preload",
|
|
581
|
+
meta: {
|
|
582
|
+
agent,
|
|
583
|
+
error: error instanceof Error ? error.message : String(error),
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
/* v8 ignore stop */
|
|
488
588
|
}
|
|
489
589
|
/* v8 ignore start -- habit wiring: lambdas delegate to processManager/fs; tested via HabitScheduler unit tests @preserve */
|
|
490
|
-
void
|
|
590
|
+
void daemon.start().then(async () => {
|
|
591
|
+
const providerPreload = startProviderCredentialPoolPreload();
|
|
491
592
|
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
492
593
|
const ouroPath = (0, os_cron_deps_1.resolveOuroBinaryPath)();
|
|
493
594
|
const osCronDeps = (0, os_cron_deps_1.createRealOsCronDeps)();
|
|
494
595
|
for (const agent of managedAgents) {
|
|
495
|
-
|
|
596
|
+
scheduleStartupSentinelAfterProviderPreload(agent, providerPreload);
|
|
496
597
|
const bundleRoot = path.join(bundlesRoot, `${agent}.ouro`);
|
|
497
598
|
const habitsDir = path.join(bundleRoot, "habits");
|
|
498
599
|
const degradedComponent = `habits:${agent}`;
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.OuroDaemon = void 0;
|
|
37
|
+
exports.messageFromHabitPokeError = messageFromHabitPokeError;
|
|
37
38
|
exports.parseOrphanPidsFromPs = parseOrphanPidsFromPs;
|
|
38
39
|
exports.filterPidfilePidsToActualOrphans = filterPidfilePidsToActualOrphans;
|
|
39
40
|
exports.mergeUniqueOrphanPids = mergeUniqueOrphanPids;
|
|
@@ -74,6 +75,7 @@ const habit_private_wake_1 = require("./habit-private-wake");
|
|
|
74
75
|
const habit_parser_1 = require("../habits/habit-parser");
|
|
75
76
|
const habit_runtime_state_1 = require("../habits/habit-runtime-state");
|
|
76
77
|
const router_1 = require("../external-events/router");
|
|
78
|
+
const habit_policy_1 = require("../../rsvp/habit-policy");
|
|
77
79
|
const PIDFILE_PATH = path.join(os.homedir(), ".ouro-cli", "daemon.pids");
|
|
78
80
|
/**
|
|
79
81
|
* Defense-in-depth: detect if we're running under vitest. The pidfile lives
|
|
@@ -95,6 +97,12 @@ function isVitestProcess() {
|
|
|
95
97
|
return false;
|
|
96
98
|
return process.argv.some((arg) => typeof arg === "string" && arg.includes("vitest"));
|
|
97
99
|
}
|
|
100
|
+
function messageFromHabitPokeError(error) {
|
|
101
|
+
if (error instanceof Error)
|
|
102
|
+
return error.message;
|
|
103
|
+
/* v8 ignore next -- defensive fallback for hostile/non-Error throws from filesystem or parsers @preserve */
|
|
104
|
+
return String(error);
|
|
105
|
+
}
|
|
98
106
|
/**
|
|
99
107
|
* Scan `ps -eo pid,ppid,command` output for daemon-owned entry points whose
|
|
100
108
|
* parent has died (PPID reparented to init/PID 1). Returns the list of PIDs
|
|
@@ -1180,13 +1188,26 @@ class OuroDaemon {
|
|
|
1180
1188
|
};
|
|
1181
1189
|
}
|
|
1182
1190
|
resolveHabitPokeOccurrenceId(command, trigger) {
|
|
1183
|
-
if (trigger === "poke" || trigger === "manual")
|
|
1184
|
-
return undefined;
|
|
1185
1191
|
const agentRoot = path.join(this.bundlesRoot, `${command.agent}.ouro`);
|
|
1186
1192
|
const habitPath = path.join(agentRoot, "habits", `${command.habitName}.md`);
|
|
1193
|
+
let parsedHabit = null;
|
|
1194
|
+
if ((0, habit_policy_1.isRsvpHabitName)(command.habitName)) {
|
|
1195
|
+
if (!fs.existsSync(habitPath))
|
|
1196
|
+
return { skipReason: "RSVP habit file not found" };
|
|
1197
|
+
try {
|
|
1198
|
+
parsedHabit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath));
|
|
1199
|
+
}
|
|
1200
|
+
catch (error) {
|
|
1201
|
+
return { skipReason: `RSVP habit metadata invalid: ${messageFromHabitPokeError(error)}` };
|
|
1202
|
+
}
|
|
1203
|
+
if (!parsedHabit.rsvp)
|
|
1204
|
+
return { skipReason: "RSVP habit metadata is required before private runtime wake" };
|
|
1205
|
+
}
|
|
1206
|
+
if (trigger === "poke" || trigger === "manual")
|
|
1207
|
+
return undefined;
|
|
1187
1208
|
if (!fs.existsSync(habitPath))
|
|
1188
1209
|
return { skipReason: "habit file not found" };
|
|
1189
|
-
const habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath));
|
|
1210
|
+
const habit = parsedHabit ?? (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath));
|
|
1190
1211
|
if (habit.cadence === null)
|
|
1191
1212
|
return { skipReason: "habit has no cadence" };
|
|
1192
1213
|
const cadence = habit.cadence;
|
|
@@ -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 ?? [];
|
|
@@ -241,6 +241,7 @@ function readProviderCredentialPool(agentName) {
|
|
|
241
241
|
}
|
|
242
242
|
async function refreshProviderCredentialPool(agentName, options = {}) {
|
|
243
243
|
const providersToRead = selectedProviders(options.providers);
|
|
244
|
+
const requiresAllSelectedProviders = !!options.providers && options.providers.length > 0;
|
|
244
245
|
try {
|
|
245
246
|
const store = (0, credential_access_1.getCredentialStore)(agentName);
|
|
246
247
|
options.onProgress?.(`reading vault items for ${agentName}...`);
|
|
@@ -266,6 +267,21 @@ async function refreshProviderCredentialPool(agentName, options = {}) {
|
|
|
266
267
|
updatedAt = record.updatedAt;
|
|
267
268
|
}
|
|
268
269
|
options.onProgress?.("parsing provider credentials...");
|
|
270
|
+
const missingRequestedProviders = requiresAllSelectedProviders
|
|
271
|
+
? providersToRead.filter((provider) => !providers[provider])
|
|
272
|
+
: [];
|
|
273
|
+
if (missingRequestedProviders.length > 0) {
|
|
274
|
+
const cached = cachedPools.get(agentName);
|
|
275
|
+
const result = {
|
|
276
|
+
ok: false,
|
|
277
|
+
reason: "missing",
|
|
278
|
+
poolPath: providerCredentialsVaultPath(agentName),
|
|
279
|
+
error: `requested provider credentials missing from ${providerCredentialsVaultPath(agentName)}: ${missingRequestedProviders.join(", ")}`,
|
|
280
|
+
};
|
|
281
|
+
if (options.preserveCachedOnFailure && cached?.ok)
|
|
282
|
+
return cached;
|
|
283
|
+
return options.skipCache ? result : cacheResult(agentName, result);
|
|
284
|
+
}
|
|
269
285
|
const pool = {
|
|
270
286
|
schemaVersion: 1,
|
|
271
287
|
updatedAt,
|