@ouro.bot/cli 0.1.0-alpha.138 → 0.1.0-alpha.139
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
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.139",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Daemon-internal heartbeat timer: self-correcting setTimeout loop reads cadence from heartbeat task file, computes delay from lastCompletedAt, fires IPC {type: heartbeat}. One timer per managed agent.",
|
|
8
|
+
"Heartbeat fires immediately when agent is overdue (elapsed > cadence) or has never run (no runtime state).",
|
|
9
|
+
"Contextual heartbeat message: journal index (up to 10 files with recency and preview), elapsed time since last turn, pending attention count, journal entries since last surface, stale obligation alerts (>30 min).",
|
|
10
|
+
"Bare instinct prompt now cold-start fallback only; resumed heartbeat sessions get contextual message.",
|
|
11
|
+
"Journal embedding indexing piggybacked during heartbeat turns (best-effort, fire-and-forget)."
|
|
12
|
+
]
|
|
13
|
+
},
|
|
4
14
|
{
|
|
5
15
|
"version": "0.1.0-alpha.138",
|
|
6
16
|
"changes": [
|
|
@@ -47,6 +47,7 @@ const sense_manager_1 = require("./sense-manager");
|
|
|
47
47
|
const agent_discovery_1 = require("./agent-discovery");
|
|
48
48
|
const identity_1 = require("../identity");
|
|
49
49
|
const runtime_mode_1 = require("./runtime-mode");
|
|
50
|
+
const heartbeat_timer_1 = require("./heartbeat-timer");
|
|
50
51
|
function parseSocketPath(argv) {
|
|
51
52
|
const socketIndex = argv.indexOf("--socket");
|
|
52
53
|
if (socketIndex >= 0) {
|
|
@@ -115,7 +116,27 @@ const daemon = new daemon_1.OuroDaemon({
|
|
|
115
116
|
router,
|
|
116
117
|
mode,
|
|
117
118
|
});
|
|
118
|
-
|
|
119
|
+
const heartbeatTimers = [];
|
|
120
|
+
/* v8 ignore start -- heartbeat wiring: lambdas delegate to processManager/fs; tested via HeartbeatTimer unit tests @preserve */
|
|
121
|
+
void daemon.start().then(() => {
|
|
122
|
+
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
123
|
+
for (const agent of managedAgents) {
|
|
124
|
+
const bundleRoot = path.join(bundlesRoot, `${agent}.ouro`);
|
|
125
|
+
const timer = new heartbeat_timer_1.HeartbeatTimer({
|
|
126
|
+
agent,
|
|
127
|
+
sendToAgent: (a, msg) => processManager.sendToAgent(a, msg),
|
|
128
|
+
deps: {
|
|
129
|
+
readFileSync: (p, enc) => fs.readFileSync(p, enc),
|
|
130
|
+
readdirSync: (p) => fs.readdirSync(p).map((e) => (typeof e === "string" ? e : e)),
|
|
131
|
+
heartbeatTaskDir: path.join(bundleRoot, "tasks", "habits"),
|
|
132
|
+
runtimeStatePath: path.join(bundleRoot, "state", "sessions", "self", "inner", "runtime.json"),
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
timer.start();
|
|
136
|
+
heartbeatTimers.push(timer);
|
|
137
|
+
}
|
|
138
|
+
}).catch(async () => {
|
|
139
|
+
/* v8 ignore stop */
|
|
119
140
|
(0, runtime_1.emitNervesEvent)({
|
|
120
141
|
level: "error",
|
|
121
142
|
component: "daemon",
|
|
@@ -127,8 +148,12 @@ void daemon.start().catch(async () => {
|
|
|
127
148
|
process.exit(1);
|
|
128
149
|
});
|
|
129
150
|
process.on("SIGINT", () => {
|
|
151
|
+
for (const timer of heartbeatTimers)
|
|
152
|
+
timer.stop();
|
|
130
153
|
void daemon.stop().then(() => process.exit(0));
|
|
131
154
|
});
|
|
132
155
|
process.on("SIGTERM", () => {
|
|
156
|
+
for (const timer of heartbeatTimers)
|
|
157
|
+
timer.stop();
|
|
133
158
|
void daemon.stop().then(() => process.exit(0));
|
|
134
159
|
});
|
|
@@ -0,0 +1,131 @@
|
|
|
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 immediately
|
|
61
|
+
delay = 0;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const elapsed = nowMs - lastCompletedAt;
|
|
65
|
+
delay = Math.max(0, cadenceMs - elapsed);
|
|
66
|
+
}
|
|
67
|
+
this.pendingTimer = setTimeout(() => {
|
|
68
|
+
this.fire();
|
|
69
|
+
}, delay);
|
|
70
|
+
}
|
|
71
|
+
fire() {
|
|
72
|
+
this.pendingTimer = null;
|
|
73
|
+
(0, runtime_1.emitNervesEvent)({
|
|
74
|
+
component: "daemon",
|
|
75
|
+
event: "daemon.heartbeat_fire",
|
|
76
|
+
message: "heartbeat timer fired",
|
|
77
|
+
meta: { agent: this.agent },
|
|
78
|
+
});
|
|
79
|
+
this.sendToAgent(this.agent, { type: "heartbeat" });
|
|
80
|
+
this.scheduleNext();
|
|
81
|
+
}
|
|
82
|
+
readCadence() {
|
|
83
|
+
// Scan habits dir for *-heartbeat.md
|
|
84
|
+
let files;
|
|
85
|
+
try {
|
|
86
|
+
files = this.readdirSync(this.heartbeatTaskDir);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return exports.DEFAULT_CADENCE_MS;
|
|
90
|
+
}
|
|
91
|
+
const heartbeatFile = files.find((f) => f.endsWith("-heartbeat.md"));
|
|
92
|
+
if (!heartbeatFile)
|
|
93
|
+
return exports.DEFAULT_CADENCE_MS;
|
|
94
|
+
try {
|
|
95
|
+
const content = this.readFileSync(`${this.heartbeatTaskDir}/${heartbeatFile}`, "utf-8");
|
|
96
|
+
const cadence = this.extractCadenceFromTaskFile(content);
|
|
97
|
+
return cadence ?? exports.DEFAULT_CADENCE_MS;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return exports.DEFAULT_CADENCE_MS;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
extractCadenceFromTaskFile(content) {
|
|
104
|
+
// Parse frontmatter from task file to get cadence value
|
|
105
|
+
const lines = content.split(/\r?\n/);
|
|
106
|
+
if (lines[0]?.trim() !== "---")
|
|
107
|
+
return null;
|
|
108
|
+
const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
109
|
+
if (closing === -1)
|
|
110
|
+
return null;
|
|
111
|
+
const rawFrontmatter = lines.slice(1, closing).join("\n");
|
|
112
|
+
const frontmatter = (0, parser_1.parseFrontmatter)(rawFrontmatter);
|
|
113
|
+
return parseCadenceMs(frontmatter.cadence);
|
|
114
|
+
}
|
|
115
|
+
readLastCompletedAt() {
|
|
116
|
+
try {
|
|
117
|
+
const raw = this.readFileSync(this.runtimeStatePath, "utf-8");
|
|
118
|
+
const state = JSON.parse(raw);
|
|
119
|
+
if (typeof state.lastCompletedAt !== "string")
|
|
120
|
+
return null;
|
|
121
|
+
const ms = new Date(state.lastCompletedAt).getTime();
|
|
122
|
+
if (Number.isNaN(ms))
|
|
123
|
+
return null;
|
|
124
|
+
return ms;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
exports.HeartbeatTimer = HeartbeatTimer;
|
package/dist/mind/prompt.js
CHANGED
|
@@ -44,6 +44,7 @@ exports.commitmentsSection = commitmentsSection;
|
|
|
44
44
|
exports.delegationHintSection = delegationHintSection;
|
|
45
45
|
exports.contextSection = contextSection;
|
|
46
46
|
exports.metacognitiveFramingSection = metacognitiveFramingSection;
|
|
47
|
+
exports.readJournalFiles = readJournalFiles;
|
|
47
48
|
exports.journalSection = journalSection;
|
|
48
49
|
exports.loopOrientationSection = loopOrientationSection;
|
|
49
50
|
exports.channelNatureSection = channelNatureSection;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildContextualHeartbeat = buildContextualHeartbeat;
|
|
4
|
+
const runtime_1 = require("../nerves/runtime");
|
|
5
|
+
const STALE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
|
|
6
|
+
const MAX_JOURNAL_FILES = 10;
|
|
7
|
+
function formatElapsed(ms) {
|
|
8
|
+
const minutes = Math.floor(ms / 60000);
|
|
9
|
+
if (minutes < 60) {
|
|
10
|
+
return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
|
|
11
|
+
}
|
|
12
|
+
const hours = Math.floor(minutes / 60);
|
|
13
|
+
return `${hours} ${hours === 1 ? "hour" : "hours"}`;
|
|
14
|
+
}
|
|
15
|
+
function formatRelativeTime(nowMs, mtimeMs) {
|
|
16
|
+
const diffMs = nowMs - mtimeMs;
|
|
17
|
+
const minutes = Math.floor(diffMs / 60000);
|
|
18
|
+
if (minutes < 1)
|
|
19
|
+
return "just now";
|
|
20
|
+
if (minutes < 60)
|
|
21
|
+
return `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`;
|
|
22
|
+
const hours = Math.floor(minutes / 60);
|
|
23
|
+
if (hours < 24)
|
|
24
|
+
return `${hours} ${hours === 1 ? "hour" : "hours"} ago`;
|
|
25
|
+
const days = Math.floor(hours / 24);
|
|
26
|
+
return `${days} ${days === 1 ? "day" : "days"} ago`;
|
|
27
|
+
}
|
|
28
|
+
function buildContextualHeartbeat(options) {
|
|
29
|
+
const { lastCompletedAt, pendingObligations, lastSurfaceAt, checkpoint, now, readJournalDir, } = options;
|
|
30
|
+
const nowDate = now();
|
|
31
|
+
const nowMs = nowDate.getTime();
|
|
32
|
+
const journalFiles = readJournalDir();
|
|
33
|
+
// Cold start: no journal files and no runtime state
|
|
34
|
+
if (journalFiles.length === 0 && !lastCompletedAt) {
|
|
35
|
+
const lines = ["...time passing. anything stirring?"];
|
|
36
|
+
if (checkpoint) {
|
|
37
|
+
lines.push(`\nlast i remember: ${checkpoint}`);
|
|
38
|
+
}
|
|
39
|
+
(0, runtime_1.emitNervesEvent)({
|
|
40
|
+
component: "senses",
|
|
41
|
+
event: "senses.contextual_heartbeat_built",
|
|
42
|
+
message: "contextual heartbeat built (cold start)",
|
|
43
|
+
meta: { coldStart: true },
|
|
44
|
+
});
|
|
45
|
+
return lines.join("\n");
|
|
46
|
+
}
|
|
47
|
+
const sections = [];
|
|
48
|
+
// Elapsed time since last turn
|
|
49
|
+
if (lastCompletedAt) {
|
|
50
|
+
const lastMs = new Date(lastCompletedAt).getTime();
|
|
51
|
+
const elapsed = nowMs - lastMs;
|
|
52
|
+
sections.push(`it's been ${formatElapsed(elapsed)} since your last turn.`);
|
|
53
|
+
}
|
|
54
|
+
// Pending attention count
|
|
55
|
+
if (pendingObligations.length > 0) {
|
|
56
|
+
const count = pendingObligations.length;
|
|
57
|
+
sections.push(`you're holding ${count} ${count === 1 ? "thought" : "thoughts"}.`);
|
|
58
|
+
}
|
|
59
|
+
// Journal index
|
|
60
|
+
if (journalFiles.length > 0) {
|
|
61
|
+
const sorted = [...journalFiles].sort((a, b) => b.mtime - a.mtime).slice(0, MAX_JOURNAL_FILES);
|
|
62
|
+
const indexLines = ["## journal"];
|
|
63
|
+
for (const file of sorted) {
|
|
64
|
+
const ago = formatRelativeTime(nowMs, file.mtime);
|
|
65
|
+
const previewClause = file.preview ? ` — ${file.preview}` : "";
|
|
66
|
+
indexLines.push(`- ${file.name} (${ago})${previewClause}`);
|
|
67
|
+
}
|
|
68
|
+
sections.push(indexLines.join("\n"));
|
|
69
|
+
}
|
|
70
|
+
// Journal entries since last surface
|
|
71
|
+
if (lastSurfaceAt && journalFiles.length > 0) {
|
|
72
|
+
const surfaceMs = new Date(lastSurfaceAt).getTime();
|
|
73
|
+
const entriesSinceSurface = journalFiles.filter((f) => f.mtime > surfaceMs).length;
|
|
74
|
+
if (entriesSinceSurface > 0) {
|
|
75
|
+
const surfaceElapsed = formatElapsed(nowMs - surfaceMs);
|
|
76
|
+
sections.push(`${entriesSinceSurface} journal ${entriesSinceSurface === 1 ? "entry" : "entries"} since you last surfaced, ${surfaceElapsed} ago.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Stale obligation alerts
|
|
80
|
+
const staleObligations = pendingObligations.filter((o) => o.staleness >= STALE_THRESHOLD_MS);
|
|
81
|
+
if (staleObligations.length > 0) {
|
|
82
|
+
for (const obligation of staleObligations) {
|
|
83
|
+
sections.push(`this has been sitting for ${formatElapsed(obligation.staleness)}: ${obligation.content}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Checkpoint
|
|
87
|
+
if (checkpoint) {
|
|
88
|
+
sections.push(`last i remember: ${checkpoint}`);
|
|
89
|
+
}
|
|
90
|
+
(0, runtime_1.emitNervesEvent)({
|
|
91
|
+
component: "senses",
|
|
92
|
+
event: "senses.contextual_heartbeat_built",
|
|
93
|
+
message: "contextual heartbeat built",
|
|
94
|
+
meta: {
|
|
95
|
+
hasJournal: journalFiles.length > 0,
|
|
96
|
+
hasLastCompleted: !!lastCompletedAt,
|
|
97
|
+
obligationCount: pendingObligations.length,
|
|
98
|
+
staleCount: staleObligations.length,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
return sections.join("\n\n");
|
|
102
|
+
}
|
|
@@ -66,6 +66,8 @@ const manager_1 = require("../heart/bridges/manager");
|
|
|
66
66
|
const session_activity_1 = require("../heart/session-activity");
|
|
67
67
|
const bluebubbles_1 = require("./bluebubbles");
|
|
68
68
|
const obligations_2 = require("../heart/obligations");
|
|
69
|
+
const contextual_heartbeat_1 = require("./contextual-heartbeat");
|
|
70
|
+
const journal_index_1 = require("../mind/journal-index");
|
|
69
71
|
const DEFAULT_INNER_DIALOG_INSTINCTS = [
|
|
70
72
|
{
|
|
71
73
|
id: "heartbeat_checkin",
|
|
@@ -483,6 +485,48 @@ async function runInnerDialogTurn(options) {
|
|
|
483
485
|
const taskContent = readTaskFile((0, identity_1.getAgentRoot)(), options.taskId);
|
|
484
486
|
userContent = buildTaskTriggeredMessage(options.taskId, taskContent, state.checkpoint);
|
|
485
487
|
}
|
|
488
|
+
else if (reason === "heartbeat") {
|
|
489
|
+
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
490
|
+
const journalDir = path.join(agentRoot, "journal");
|
|
491
|
+
const runtimeStatePath = innerDialogRuntimeStatePath(sessionFilePath);
|
|
492
|
+
// Read lastCompletedAt from current runtime state
|
|
493
|
+
let lastCompletedAt;
|
|
494
|
+
try {
|
|
495
|
+
const raw = fs.readFileSync(runtimeStatePath, "utf-8");
|
|
496
|
+
const runtimeState = JSON.parse(raw);
|
|
497
|
+
lastCompletedAt = typeof runtimeState.lastCompletedAt === "string" ? runtimeState.lastCompletedAt : undefined;
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
// no runtime state — will get cold start fallback
|
|
501
|
+
}
|
|
502
|
+
// Gather pending obligations for display
|
|
503
|
+
const obligations = (0, obligations_1.listActiveObligations)(agentName);
|
|
504
|
+
const nowMs = now().getTime();
|
|
505
|
+
const pendingObligations = obligations.map((o) => ({
|
|
506
|
+
id: o.id,
|
|
507
|
+
content: o.delegatedContent,
|
|
508
|
+
friendName: o.origin.friendId,
|
|
509
|
+
timestamp: o.createdAt,
|
|
510
|
+
staleness: nowMs - o.createdAt,
|
|
511
|
+
}));
|
|
512
|
+
userContent = (0, contextual_heartbeat_1.buildContextualHeartbeat)({
|
|
513
|
+
journalDir,
|
|
514
|
+
lastCompletedAt,
|
|
515
|
+
pendingObligations,
|
|
516
|
+
lastSurfaceAt: undefined, // TODO: wire when surface tracking is added
|
|
517
|
+
checkpoint: state.checkpoint,
|
|
518
|
+
now,
|
|
519
|
+
readJournalDir: () => (0, prompt_1.readJournalFiles)(journalDir),
|
|
520
|
+
});
|
|
521
|
+
// Piggyback journal embedding indexing (best-effort, fire-and-forget)
|
|
522
|
+
/* v8 ignore start -- journal indexing piggyback: embedding provider may not be available; tested via journal-index unit tests @preserve */
|
|
523
|
+
void (0, journal_index_1.indexJournalFiles)(journalDir, path.join(journalDir, ".index.json"), {
|
|
524
|
+
embed: async () => [],
|
|
525
|
+
}).catch(() => {
|
|
526
|
+
// swallowed: indexing failure must never block heartbeat
|
|
527
|
+
});
|
|
528
|
+
/* v8 ignore stop */
|
|
529
|
+
}
|
|
486
530
|
else {
|
|
487
531
|
userContent = buildInstinctUserMessage(instincts, reason, state);
|
|
488
532
|
}
|