@ouro.bot/cli 0.1.0-alpha.147 → 0.1.0-alpha.149
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 +27 -0
- package/dist/heart/daemon/cadence.js +70 -0
- package/dist/heart/daemon/daemon-cli.js +141 -17
- package/dist/heart/daemon/daemon-entry.js +67 -17
- package/dist/heart/daemon/daemon-tombstone.js +106 -0
- package/dist/heart/daemon/daemon.js +71 -2
- package/dist/heart/daemon/habit-migration.js +181 -0
- package/dist/heart/daemon/habit-parser.js +119 -0
- package/dist/heart/daemon/habit-scheduler.js +244 -0
- package/dist/heart/daemon/hatch-flow.js +13 -31
- package/dist/heart/daemon/os-cron-deps.js +134 -0
- package/dist/heart/daemon/process-manager.js +51 -0
- package/dist/heart/daemon/specialist-tools.js +1 -1
- package/dist/heart/daemon/task-scheduler.js +3 -25
- package/dist/mind/prompt.js +9 -5
- package/dist/nerves/index.js +47 -2
- package/dist/repertoire/tasks/parser.js +0 -2
- package/dist/repertoire/tasks/transitions.js +0 -18
- package/dist/senses/contextual-heartbeat.js +5 -1
- package/dist/senses/inner-dialog-worker.js +84 -20
- package/dist/senses/inner-dialog.js +145 -39
- package/package.json +1 -1
- package/dist/heart/daemon/heartbeat-timer.js +0 -132
|
@@ -1,33 +1,63 @@
|
|
|
1
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.createInnerDialogWorker = createInnerDialogWorker;
|
|
4
37
|
exports.startInnerDialogWorker = startInnerDialogWorker;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
5
40
|
const inner_dialog_1 = require("./inner-dialog");
|
|
6
41
|
const runtime_1 = require("../nerves/runtime");
|
|
7
42
|
const identity_1 = require("../heart/identity");
|
|
8
43
|
const pending_1 = require("../mind/pending");
|
|
44
|
+
const habit_parser_1 = require("../heart/daemon/habit-parser");
|
|
9
45
|
function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runInnerDialogTurn)(options), hasPendingWork = () => (0, pending_1.hasPendingMessages)((0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)()))) {
|
|
10
46
|
let running = false;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
let rerunTaskId;
|
|
14
|
-
async function run(reason, taskId) {
|
|
47
|
+
const queue = [];
|
|
48
|
+
async function run(reason, taskId, habitName) {
|
|
15
49
|
if (running) {
|
|
16
|
-
|
|
17
|
-
rerunReason = reason;
|
|
18
|
-
if (taskId !== undefined) {
|
|
19
|
-
rerunTaskId = taskId;
|
|
20
|
-
}
|
|
50
|
+
queue.push({ reason, taskId, habitName });
|
|
21
51
|
return;
|
|
22
52
|
}
|
|
23
53
|
running = true;
|
|
24
54
|
try {
|
|
25
55
|
let nextReason = reason;
|
|
26
56
|
let nextTaskId = taskId;
|
|
57
|
+
let nextHabitName = habitName;
|
|
27
58
|
do {
|
|
28
|
-
rerunRequested = false;
|
|
29
59
|
try {
|
|
30
|
-
await runTurn({ reason: nextReason, taskId: nextTaskId });
|
|
60
|
+
await runTurn({ reason: nextReason, taskId: nextTaskId, habitName: nextHabitName });
|
|
31
61
|
}
|
|
32
62
|
catch (error) {
|
|
33
63
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -41,15 +71,44 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
|
|
|
41
71
|
},
|
|
42
72
|
});
|
|
43
73
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
74
|
+
// Update lastRun in habit frontmatter after a habit turn
|
|
75
|
+
if (nextReason === "habit" && nextHabitName) {
|
|
76
|
+
try {
|
|
77
|
+
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
78
|
+
const habitFilePath = path.join(agentRoot, "habits", `${nextHabitName}.md`);
|
|
79
|
+
const content = fs.readFileSync(habitFilePath, "utf-8");
|
|
80
|
+
const parsed = (0, habit_parser_1.parseHabitFile)(content, habitFilePath);
|
|
81
|
+
const frontmatter = {
|
|
82
|
+
title: parsed.title,
|
|
83
|
+
cadence: parsed.cadence,
|
|
84
|
+
status: parsed.status,
|
|
85
|
+
lastRun: new Date().toISOString(),
|
|
86
|
+
created: parsed.created,
|
|
87
|
+
};
|
|
88
|
+
const rendered = (0, habit_parser_1.renderHabitFile)(frontmatter, parsed.body);
|
|
89
|
+
fs.writeFileSync(habitFilePath, rendered, "utf-8");
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Habit file may have been deleted during the turn — skip gracefully
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Drain queue first
|
|
96
|
+
if (queue.length > 0) {
|
|
97
|
+
const next = queue.shift();
|
|
98
|
+
nextReason = next.reason;
|
|
99
|
+
nextTaskId = next.taskId;
|
|
100
|
+
nextHabitName = next.habitName;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// Then check hasPendingWork fallback
|
|
104
|
+
if (hasPendingWork()) {
|
|
105
|
+
nextReason = "instinct";
|
|
106
|
+
nextTaskId = undefined;
|
|
107
|
+
nextHabitName = undefined;
|
|
108
|
+
continue;
|
|
47
109
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
rerunReason = "instinct";
|
|
51
|
-
rerunTaskId = undefined;
|
|
52
|
-
} while (rerunRequested);
|
|
110
|
+
break;
|
|
111
|
+
} while (true);
|
|
53
112
|
}
|
|
54
113
|
finally {
|
|
55
114
|
running = false;
|
|
@@ -59,8 +118,13 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
|
|
|
59
118
|
if (!message || typeof message !== "object")
|
|
60
119
|
return;
|
|
61
120
|
const maybeMessage = message;
|
|
121
|
+
if (maybeMessage.type === "habit") {
|
|
122
|
+
await run("habit", undefined, maybeMessage.habitName);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
62
125
|
if (maybeMessage.type === "heartbeat") {
|
|
63
|
-
|
|
126
|
+
// Backward compatibility: heartbeat -> habit/heartbeat
|
|
127
|
+
await run("habit", undefined, "heartbeat");
|
|
64
128
|
return;
|
|
65
129
|
}
|
|
66
130
|
if (maybeMessage.type === "poke") {
|
|
@@ -43,6 +43,7 @@ exports.deriveResumeCheckpoint = deriveResumeCheckpoint;
|
|
|
43
43
|
exports.innerDialogSessionPath = innerDialogSessionPath;
|
|
44
44
|
exports.enrichDelegatedFromWithBridge = enrichDelegatedFromWithBridge;
|
|
45
45
|
exports.routeDelegatedCompletion = routeDelegatedCompletion;
|
|
46
|
+
exports.buildParseErrorNudge = buildParseErrorNudge;
|
|
46
47
|
exports.runInnerDialogTurn = runInnerDialogTurn;
|
|
47
48
|
const fs = __importStar(require("fs"));
|
|
48
49
|
const path = __importStar(require("path"));
|
|
@@ -68,6 +69,8 @@ const bluebubbles_1 = require("./bluebubbles");
|
|
|
68
69
|
const obligations_2 = require("../heart/obligations");
|
|
69
70
|
const contextual_heartbeat_1 = require("./contextual-heartbeat");
|
|
70
71
|
const journal_index_1 = require("../mind/journal-index");
|
|
72
|
+
const habit_parser_1 = require("../heart/daemon/habit-parser");
|
|
73
|
+
const cadence_1 = require("../heart/daemon/cadence");
|
|
71
74
|
const DEFAULT_INNER_DIALOG_INSTINCTS = [
|
|
72
75
|
{
|
|
73
76
|
id: "heartbeat_checkin",
|
|
@@ -127,9 +130,10 @@ function buildInstinctUserMessage(instincts, _reason, state) {
|
|
|
127
130
|
return lines.join("\n");
|
|
128
131
|
}
|
|
129
132
|
function readTaskFile(agentRoot, taskId) {
|
|
130
|
-
// Task files live in collection subdirectories (one-shots, ongoing
|
|
133
|
+
// Task files live in collection subdirectories (one-shots, ongoing).
|
|
131
134
|
// Try each collection, then fall back to root tasks/ for legacy layout.
|
|
132
|
-
|
|
135
|
+
// Habits are no longer in tasks/ — they live at bundle root habits/.
|
|
136
|
+
const collections = ["one-shots", "ongoing", ""];
|
|
133
137
|
for (const collection of collections) {
|
|
134
138
|
try {
|
|
135
139
|
return fs.readFileSync(path.join(agentRoot, "tasks", collection, `${taskId}.md`), "utf8").trim();
|
|
@@ -460,9 +464,57 @@ function createNoOpFriendStore() {
|
|
|
460
464
|
findByExternalId: async () => null,
|
|
461
465
|
};
|
|
462
466
|
}
|
|
467
|
+
function buildParseErrorNudge(parseErrors) {
|
|
468
|
+
if (parseErrors.length === 0)
|
|
469
|
+
return "";
|
|
470
|
+
const lines = parseErrors.map((e) => `I noticed my habit file \`${e.file}\` has invalid frontmatter — I should fix it. (${e.error})`);
|
|
471
|
+
return lines.join("\n");
|
|
472
|
+
}
|
|
473
|
+
function buildAlsoDueLine(agentRoot, currentHabitName, now) {
|
|
474
|
+
const habitsDir = path.join(agentRoot, "habits");
|
|
475
|
+
let files;
|
|
476
|
+
try {
|
|
477
|
+
files = fs.readdirSync(habitsDir);
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return "";
|
|
481
|
+
}
|
|
482
|
+
const nowMs = now().getTime();
|
|
483
|
+
const alsoDue = [];
|
|
484
|
+
for (const file of files) {
|
|
485
|
+
if (!file.endsWith(".md"))
|
|
486
|
+
continue;
|
|
487
|
+
const stem = file.replace(/\.md$/, "");
|
|
488
|
+
if (stem === currentHabitName)
|
|
489
|
+
continue;
|
|
490
|
+
try {
|
|
491
|
+
const content = fs.readFileSync(path.join(habitsDir, file), "utf-8");
|
|
492
|
+
const habit = (0, habit_parser_1.parseHabitFile)(content, path.join(habitsDir, file));
|
|
493
|
+
if (habit.status !== "active" || !habit.cadence)
|
|
494
|
+
continue;
|
|
495
|
+
const cadenceMs = (0, cadence_1.parseCadenceToMs)(habit.cadence);
|
|
496
|
+
if (cadenceMs === null)
|
|
497
|
+
continue;
|
|
498
|
+
if (habit.lastRun === null) {
|
|
499
|
+
alsoDue.push(stem);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const lastRunMs = new Date(habit.lastRun).getTime();
|
|
503
|
+
if (nowMs - lastRunMs >= cadenceMs) {
|
|
504
|
+
alsoDue.push(stem);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
// skip unreadable habits
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (alsoDue.length === 0)
|
|
512
|
+
return "";
|
|
513
|
+
return `also due: ${alsoDue.join(", ")}`;
|
|
514
|
+
}
|
|
463
515
|
async function runInnerDialogTurn(options) {
|
|
464
516
|
const now = options?.now ?? (() => new Date());
|
|
465
|
-
const reason = options?.reason ?? "
|
|
517
|
+
const reason = options?.reason ?? "instinct";
|
|
466
518
|
const sessionFilePath = innerDialogSessionPath();
|
|
467
519
|
const agentName = (0, identity_1.getAgentName)();
|
|
468
520
|
writeInnerDialogRuntimeState(sessionFilePath, {
|
|
@@ -500,47 +552,101 @@ async function runInnerDialogTurn(options) {
|
|
|
500
552
|
const taskContent = readTaskFile((0, identity_1.getAgentRoot)(), options.taskId);
|
|
501
553
|
userContent = buildTaskTriggeredMessage(options.taskId, taskContent, state.checkpoint);
|
|
502
554
|
}
|
|
503
|
-
else if (reason === "
|
|
555
|
+
else if (reason === "habit" && options?.habitName) {
|
|
504
556
|
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
// Read
|
|
508
|
-
let
|
|
557
|
+
const habitName = options.habitName;
|
|
558
|
+
const habitFilePath = path.join(agentRoot, "habits", `${habitName}.md`);
|
|
559
|
+
// Read and parse the habit file
|
|
560
|
+
let habitBody;
|
|
561
|
+
let habitLastRun = null;
|
|
509
562
|
try {
|
|
510
|
-
const
|
|
511
|
-
const
|
|
512
|
-
|
|
563
|
+
const habitContent = fs.readFileSync(habitFilePath, "utf-8");
|
|
564
|
+
const parsed = (0, habit_parser_1.parseHabitFile)(habitContent, habitFilePath);
|
|
565
|
+
habitBody = parsed.body;
|
|
566
|
+
habitLastRun = parsed.lastRun;
|
|
513
567
|
}
|
|
514
568
|
catch {
|
|
515
|
-
//
|
|
569
|
+
// Habit file missing or unreadable
|
|
570
|
+
}
|
|
571
|
+
if (habitName === "heartbeat") {
|
|
572
|
+
// Heartbeat habit: use contextual heartbeat builder with habit body
|
|
573
|
+
const journalDir = path.join(agentRoot, "journal");
|
|
574
|
+
const runtimeStatePath = innerDialogRuntimeStatePath(sessionFilePath);
|
|
575
|
+
let lastCompletedAt;
|
|
576
|
+
try {
|
|
577
|
+
const raw = fs.readFileSync(runtimeStatePath, "utf-8");
|
|
578
|
+
const runtimeState = JSON.parse(raw);
|
|
579
|
+
lastCompletedAt = typeof runtimeState.lastCompletedAt === "string" ? runtimeState.lastCompletedAt : undefined;
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
// no runtime state — will get cold start fallback
|
|
583
|
+
}
|
|
584
|
+
const obligations = (0, obligations_1.listActiveObligations)(agentName);
|
|
585
|
+
const nowMs = now().getTime();
|
|
586
|
+
const pendingObligations = obligations.map((o) => ({
|
|
587
|
+
id: o.id,
|
|
588
|
+
content: o.delegatedContent,
|
|
589
|
+
friendName: o.origin.friendId,
|
|
590
|
+
timestamp: o.createdAt,
|
|
591
|
+
staleness: nowMs - o.createdAt,
|
|
592
|
+
}));
|
|
593
|
+
const heartbeatContent = (0, contextual_heartbeat_1.buildContextualHeartbeat)({
|
|
594
|
+
journalDir,
|
|
595
|
+
lastCompletedAt,
|
|
596
|
+
pendingObligations,
|
|
597
|
+
lastSurfaceAt: undefined,
|
|
598
|
+
checkpoint: state.checkpoint,
|
|
599
|
+
now,
|
|
600
|
+
readJournalDir: () => (0, prompt_1.readJournalFiles)(journalDir),
|
|
601
|
+
habitBody,
|
|
602
|
+
});
|
|
603
|
+
// Build also-due line
|
|
604
|
+
const alsoDue = buildAlsoDueLine(agentRoot, habitName, now);
|
|
605
|
+
userContent = alsoDue ? `${heartbeatContent}\n\n${alsoDue}` : heartbeatContent;
|
|
606
|
+
// Parse error nudge
|
|
607
|
+
const parseErrorNudge = buildParseErrorNudge(options?.parseErrors ?? []);
|
|
608
|
+
if (parseErrorNudge)
|
|
609
|
+
userContent = `${userContent}\n\n${parseErrorNudge}`;
|
|
610
|
+
// Piggyback journal embedding indexing (best-effort, fire-and-forget)
|
|
611
|
+
/* v8 ignore start -- journal indexing piggyback: embedding provider may not be available; tested via journal-index unit tests @preserve */
|
|
612
|
+
void (0, journal_index_1.indexJournalFiles)(journalDir, path.join(journalDir, ".index.json"), {
|
|
613
|
+
embed: async () => [],
|
|
614
|
+
}).catch(() => {
|
|
615
|
+
// swallowed: indexing failure must never block heartbeat
|
|
616
|
+
});
|
|
617
|
+
/* v8 ignore stop */
|
|
618
|
+
}
|
|
619
|
+
else if (habitBody !== undefined) {
|
|
620
|
+
// Non-heartbeat habit: basic context + body
|
|
621
|
+
const sections = [];
|
|
622
|
+
// Elapsed time since last run
|
|
623
|
+
if (habitLastRun) {
|
|
624
|
+
const lastRunMs = new Date(habitLastRun).getTime();
|
|
625
|
+
const elapsed = now().getTime() - lastRunMs;
|
|
626
|
+
const minutes = Math.floor(elapsed / 60000);
|
|
627
|
+
if (minutes < 60) {
|
|
628
|
+
sections.push(`it's been ${minutes} ${minutes === 1 ? "minute" : "minutes"} since ${habitName} last ran.`);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
const hours = Math.floor(minutes / 60);
|
|
632
|
+
sections.push(`it's been ${hours} ${hours === 1 ? "hour" : "hours"} since ${habitName} last ran.`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
sections.push(`## habit: ${habitName}\n${habitBody}`);
|
|
636
|
+
// Also due line
|
|
637
|
+
const alsoDue = buildAlsoDueLine(agentRoot, habitName, now);
|
|
638
|
+
if (alsoDue)
|
|
639
|
+
sections.push(alsoDue);
|
|
640
|
+
// Parse error nudge
|
|
641
|
+
const parseErrorNudge = buildParseErrorNudge(options?.parseErrors ?? []);
|
|
642
|
+
if (parseErrorNudge)
|
|
643
|
+
sections.push(parseErrorNudge);
|
|
644
|
+
userContent = sections.join("\n\n");
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
// Habit file not found
|
|
648
|
+
userContent = `habit "${habitName}" could not be read (file not found or unreadable). check habits/${habitName}.md exists.`;
|
|
516
649
|
}
|
|
517
|
-
// Gather pending obligations for display
|
|
518
|
-
const obligations = (0, obligations_1.listActiveObligations)(agentName);
|
|
519
|
-
const nowMs = now().getTime();
|
|
520
|
-
const pendingObligations = obligations.map((o) => ({
|
|
521
|
-
id: o.id,
|
|
522
|
-
content: o.delegatedContent,
|
|
523
|
-
friendName: o.origin.friendId,
|
|
524
|
-
timestamp: o.createdAt,
|
|
525
|
-
staleness: nowMs - o.createdAt,
|
|
526
|
-
}));
|
|
527
|
-
userContent = (0, contextual_heartbeat_1.buildContextualHeartbeat)({
|
|
528
|
-
journalDir,
|
|
529
|
-
lastCompletedAt,
|
|
530
|
-
pendingObligations,
|
|
531
|
-
lastSurfaceAt: undefined, // TODO: wire when surface tracking is added
|
|
532
|
-
checkpoint: state.checkpoint,
|
|
533
|
-
now,
|
|
534
|
-
readJournalDir: () => (0, prompt_1.readJournalFiles)(journalDir),
|
|
535
|
-
});
|
|
536
|
-
// Piggyback journal embedding indexing (best-effort, fire-and-forget)
|
|
537
|
-
/* v8 ignore start -- journal indexing piggyback: embedding provider may not be available; tested via journal-index unit tests @preserve */
|
|
538
|
-
void (0, journal_index_1.indexJournalFiles)(journalDir, path.join(journalDir, ".index.json"), {
|
|
539
|
-
embed: async () => [],
|
|
540
|
-
}).catch(() => {
|
|
541
|
-
// swallowed: indexing failure must never block heartbeat
|
|
542
|
-
});
|
|
543
|
-
/* v8 ignore stop */
|
|
544
650
|
}
|
|
545
651
|
else {
|
|
546
652
|
userContent = buildInstinctUserMessage(instincts, reason, state);
|
package/package.json
CHANGED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HeartbeatTimer = exports.DEFAULT_CADENCE_MS = void 0;
|
|
4
|
-
exports.parseCadenceMs = parseCadenceMs;
|
|
5
|
-
const runtime_1 = require("../../nerves/runtime");
|
|
6
|
-
const parser_1 = require("../../repertoire/tasks/parser");
|
|
7
|
-
exports.DEFAULT_CADENCE_MS = 30 * 60 * 1000; // 30 minutes
|
|
8
|
-
function parseCadenceMs(raw) {
|
|
9
|
-
if (typeof raw !== "string")
|
|
10
|
-
return null;
|
|
11
|
-
const value = raw.trim();
|
|
12
|
-
if (!value)
|
|
13
|
-
return null;
|
|
14
|
-
const match = /^(\d+)(m|h|d)$/.exec(value);
|
|
15
|
-
if (!match)
|
|
16
|
-
return null;
|
|
17
|
-
const interval = Number.parseInt(match[1], 10);
|
|
18
|
-
if (!Number.isFinite(interval) || interval <= 0)
|
|
19
|
-
return null;
|
|
20
|
-
const unit = match[2];
|
|
21
|
-
if (unit === "m")
|
|
22
|
-
return interval * 60 * 1000;
|
|
23
|
-
if (unit === "h")
|
|
24
|
-
return interval * 60 * 60 * 1000;
|
|
25
|
-
return interval * 24 * 60 * 60 * 1000;
|
|
26
|
-
}
|
|
27
|
-
class HeartbeatTimer {
|
|
28
|
-
agent;
|
|
29
|
-
sendToAgent;
|
|
30
|
-
readFileSync;
|
|
31
|
-
readdirSync;
|
|
32
|
-
heartbeatTaskDir;
|
|
33
|
-
runtimeStatePath;
|
|
34
|
-
now;
|
|
35
|
-
pendingTimer = null;
|
|
36
|
-
constructor(options) {
|
|
37
|
-
this.agent = options.agent;
|
|
38
|
-
this.sendToAgent = options.sendToAgent;
|
|
39
|
-
this.readFileSync = options.deps.readFileSync;
|
|
40
|
-
this.readdirSync = options.deps.readdirSync;
|
|
41
|
-
this.heartbeatTaskDir = options.deps.heartbeatTaskDir;
|
|
42
|
-
this.runtimeStatePath = options.deps.runtimeStatePath;
|
|
43
|
-
this.now = options.deps.now ?? (() => Date.now());
|
|
44
|
-
}
|
|
45
|
-
start() {
|
|
46
|
-
this.scheduleNext();
|
|
47
|
-
}
|
|
48
|
-
stop() {
|
|
49
|
-
if (this.pendingTimer !== null) {
|
|
50
|
-
clearTimeout(this.pendingTimer);
|
|
51
|
-
this.pendingTimer = null;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
scheduleNext() {
|
|
55
|
-
const cadenceMs = this.readCadence();
|
|
56
|
-
const lastCompletedAt = this.readLastCompletedAt();
|
|
57
|
-
const nowMs = this.now();
|
|
58
|
-
let delay;
|
|
59
|
-
if (lastCompletedAt === null) {
|
|
60
|
-
// Never run before — fire once after a full cadence (not immediately, to avoid spin loop
|
|
61
|
-
// when the agent has no inner dialog state to write lastCompletedAt)
|
|
62
|
-
delay = cadenceMs;
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
const elapsed = nowMs - lastCompletedAt;
|
|
66
|
-
delay = Math.max(0, cadenceMs - elapsed);
|
|
67
|
-
}
|
|
68
|
-
this.pendingTimer = setTimeout(() => {
|
|
69
|
-
this.fire();
|
|
70
|
-
}, delay);
|
|
71
|
-
}
|
|
72
|
-
fire() {
|
|
73
|
-
this.pendingTimer = null;
|
|
74
|
-
(0, runtime_1.emitNervesEvent)({
|
|
75
|
-
component: "daemon",
|
|
76
|
-
event: "daemon.heartbeat_fire",
|
|
77
|
-
message: "heartbeat timer fired",
|
|
78
|
-
meta: { agent: this.agent },
|
|
79
|
-
});
|
|
80
|
-
this.sendToAgent(this.agent, { type: "heartbeat" });
|
|
81
|
-
this.scheduleNext();
|
|
82
|
-
}
|
|
83
|
-
readCadence() {
|
|
84
|
-
// Scan habits dir for *-heartbeat.md
|
|
85
|
-
let files;
|
|
86
|
-
try {
|
|
87
|
-
files = this.readdirSync(this.heartbeatTaskDir);
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
return exports.DEFAULT_CADENCE_MS;
|
|
91
|
-
}
|
|
92
|
-
const heartbeatFile = files.find((f) => f.endsWith("-heartbeat.md"));
|
|
93
|
-
if (!heartbeatFile)
|
|
94
|
-
return exports.DEFAULT_CADENCE_MS;
|
|
95
|
-
try {
|
|
96
|
-
const content = this.readFileSync(`${this.heartbeatTaskDir}/${heartbeatFile}`, "utf-8");
|
|
97
|
-
const cadence = this.extractCadenceFromTaskFile(content);
|
|
98
|
-
return cadence ?? exports.DEFAULT_CADENCE_MS;
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
return exports.DEFAULT_CADENCE_MS;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
extractCadenceFromTaskFile(content) {
|
|
105
|
-
// Parse frontmatter from task file to get cadence value
|
|
106
|
-
const lines = content.split(/\r?\n/);
|
|
107
|
-
if (lines[0]?.trim() !== "---")
|
|
108
|
-
return null;
|
|
109
|
-
const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
110
|
-
if (closing === -1)
|
|
111
|
-
return null;
|
|
112
|
-
const rawFrontmatter = lines.slice(1, closing).join("\n");
|
|
113
|
-
const frontmatter = (0, parser_1.parseFrontmatter)(rawFrontmatter);
|
|
114
|
-
return parseCadenceMs(frontmatter.cadence);
|
|
115
|
-
}
|
|
116
|
-
readLastCompletedAt() {
|
|
117
|
-
try {
|
|
118
|
-
const raw = this.readFileSync(this.runtimeStatePath, "utf-8");
|
|
119
|
-
const state = JSON.parse(raw);
|
|
120
|
-
if (typeof state.lastCompletedAt !== "string")
|
|
121
|
-
return null;
|
|
122
|
-
const ms = new Date(state.lastCompletedAt).getTime();
|
|
123
|
-
if (Number.isNaN(ms))
|
|
124
|
-
return null;
|
|
125
|
-
return ms;
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
exports.HeartbeatTimer = HeartbeatTimer;
|