@ouro.bot/cli 0.1.0-alpha.137 → 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,29 @@
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
+ },
14
+ {
15
+ "version": "0.1.0-alpha.138",
16
+ "changes": [
17
+ "Memory renamed to diary: memory.ts → diary.ts, MemoryFact → DiaryEntry, memory_save → diary_write, memory_search → recall. All types, functions, events, and variables renamed throughout.",
18
+ "Diary path: diary/ (top-level) replaces psyche/memory/. Fallback reads from legacy path for existing bundles.",
19
+ "Journal workspace: journal/ directory for freeform thinking-in-progress. Agent writes with write_file, system reads for heartbeat context.",
20
+ "Unified recall tool: searches both diary entries and journal files. Results tagged [diary] or [journal].",
21
+ "Journal embeddings: file-level embeddings indexed during heartbeat via journal/.index.json sidecar.",
22
+ "Journal section in inner dialog system prompt: index of up to 10 most recently modified journal files with name, recency, and first-line preview.",
23
+ "Metacognitive framing updated: diary (record), journal (workspace), ponder/rest vocabulary, morning briefing encouragement.",
24
+ "Session migration: memory_save → diary_write, memory_search → recall added to migrateToolNames()."
25
+ ]
26
+ },
4
27
  {
5
28
  "version": "0.1.0-alpha.137",
6
29
  "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
- void daemon.start().catch(async () => {
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
  });
@@ -142,12 +142,12 @@ function writeFriendImprint(bundleRoot, humanName, now) {
142
142
  };
143
143
  fs.writeFileSync(path.join(friendsDir, `${id}.json`), `${JSON.stringify(record, null, 2)}\n`, "utf-8");
144
144
  }
145
- function writeMemoryScaffold(bundleRoot) {
146
- const memoryRoot = path.join(bundleRoot, "psyche", "memory");
147
- fs.mkdirSync(path.join(memoryRoot, "daily"), { recursive: true });
148
- fs.mkdirSync(path.join(memoryRoot, "archive"), { recursive: true });
149
- fs.writeFileSync(path.join(memoryRoot, "facts.jsonl"), "", "utf-8");
150
- fs.writeFileSync(path.join(memoryRoot, "entities.json"), "{}\n", "utf-8");
145
+ function writeDiaryScaffold(bundleRoot) {
146
+ const diaryRoot = path.join(bundleRoot, "diary");
147
+ fs.mkdirSync(path.join(diaryRoot, "daily"), { recursive: true });
148
+ fs.mkdirSync(path.join(diaryRoot, "archive"), { recursive: true });
149
+ fs.writeFileSync(path.join(diaryRoot, "facts.jsonl"), "", "utf-8");
150
+ fs.writeFileSync(path.join(diaryRoot, "entities.json"), "{}\n", "utf-8");
151
151
  }
152
152
  function writeHatchlingAgentConfig(bundleRoot, input) {
153
153
  const template = (0, identity_1.buildDefaultAgentTemplate)(input.agentName);
@@ -183,7 +183,7 @@ async function runHatchFlow(input, deps = {}) {
183
183
  fs.mkdirSync(bundleRoot, { recursive: true });
184
184
  writeReadme(bundleRoot, "Root of this agent bundle.");
185
185
  writeReadme(path.join(bundleRoot, "psyche"), "Identity and behavior files.");
186
- writeReadme(path.join(bundleRoot, "psyche", "memory"), "Persistent memory store.");
186
+ writeReadme(path.join(bundleRoot, "diary"), "Persistent diary — things I've learned and remember.");
187
187
  writeReadme(path.join(bundleRoot, "friends"), "Known friend records.");
188
188
  writeReadme(path.join(bundleRoot, "tasks"), "Task files.");
189
189
  writeReadme(path.join(bundleRoot, "tasks", "habits"), "Recurring tasks.");
@@ -193,7 +193,7 @@ async function runHatchFlow(input, deps = {}) {
193
193
  writeReadme(path.join(bundleRoot, "senses"), "Sense-specific config.");
194
194
  writeReadme(path.join(bundleRoot, "senses", "teams"), "Teams sense config.");
195
195
  writeHatchlingAgentConfig(bundleRoot, input);
196
- writeMemoryScaffold(bundleRoot);
196
+ writeDiaryScaffold(bundleRoot);
197
197
  writeFriendImprint(bundleRoot, input.humanName, now);
198
198
  writeHeartbeatTask(bundleRoot, now);
199
199
  (0, runtime_1.emitNervesEvent)({
@@ -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;
@@ -35,12 +35,13 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.cosineSimilarity = cosineSimilarity;
37
37
  exports.recallFactsForQuery = recallFactsForQuery;
38
+ exports.searchJournalIndex = searchJournalIndex;
38
39
  exports.injectAssociativeRecall = injectAssociativeRecall;
39
40
  const fs = __importStar(require("fs"));
40
41
  const path = __importStar(require("path"));
41
42
  const config_1 = require("../heart/config");
42
- const identity_1 = require("../heart/identity");
43
43
  const runtime_1 = require("../nerves/runtime");
44
+ const diary_1 = require("./diary");
44
45
  const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
45
46
  const DEFAULT_MIN_SCORE = 0.5;
46
47
  const DEFAULT_TOP_K = 3;
@@ -80,8 +81,8 @@ function createDefaultProvider() {
80
81
  }
81
82
  return new OpenAIEmbeddingProvider(apiKey);
82
83
  }
83
- function readFacts(memoryRoot) {
84
- const factsPath = path.join(memoryRoot, "facts.jsonl");
84
+ function readFacts(diaryRoot) {
85
+ const factsPath = path.join(diaryRoot, "facts.jsonl");
85
86
  if (!fs.existsSync(factsPath))
86
87
  return [];
87
88
  const raw = fs.readFileSync(factsPath, "utf8").trim();
@@ -145,6 +146,40 @@ async function recallFactsForQuery(query, facts, provider, options) {
145
146
  .sort((left, right) => right.score - left.score)
146
147
  .slice(0, topK);
147
148
  }
149
+ function readJournalIndex(journalDir) {
150
+ const indexPath = path.join(journalDir, ".index.json");
151
+ try {
152
+ const raw = fs.readFileSync(indexPath, "utf8");
153
+ const parsed = JSON.parse(raw);
154
+ if (!Array.isArray(parsed))
155
+ return [];
156
+ return parsed;
157
+ }
158
+ catch {
159
+ return [];
160
+ }
161
+ }
162
+ function searchJournalIndex(queryEmbedding, entries, options) {
163
+ const minScore = options?.minScore ?? DEFAULT_MIN_SCORE;
164
+ const topK = options?.topK ?? DEFAULT_TOP_K;
165
+ return entries
166
+ .filter((entry) => Array.isArray(entry.embedding) && entry.embedding.length > 0)
167
+ .map((entry) => ({
168
+ filename: entry.filename,
169
+ preview: entry.preview,
170
+ score: cosineSimilarity(queryEmbedding, entry.embedding),
171
+ }))
172
+ .filter((entry) => entry.score >= minScore)
173
+ .sort((left, right) => right.score - left.score)
174
+ .slice(0, topK);
175
+ }
176
+ function resolveJournalDir(diaryRoot, explicitJournalDir) {
177
+ if (explicitJournalDir)
178
+ return explicitJournalDir;
179
+ // journal/ is a sibling of diary/ at the agent root level
180
+ const agentRoot = path.dirname(diaryRoot);
181
+ return path.join(agentRoot, "journal");
182
+ }
148
183
  async function injectAssociativeRecall(messages, options) {
149
184
  try {
150
185
  if (messages[0]?.role !== "system" || typeof messages[0].content !== "string")
@@ -152,37 +187,80 @@ async function injectAssociativeRecall(messages, options) {
152
187
  const query = getLatestUserText(messages);
153
188
  if (!query)
154
189
  return;
155
- const memoryRoot = options?.memoryRoot ?? path.join((0, identity_1.getAgentRoot)(), "psyche", "memory");
156
- const facts = readFacts(memoryRoot);
157
- if (facts.length === 0)
190
+ const diaryRoot = options?.diaryRoot ?? (0, diary_1.resolveDiaryRoot)();
191
+ const facts = readFacts(diaryRoot);
192
+ const journalDir = resolveJournalDir(diaryRoot, options?.journalDir);
193
+ const journalEntries = readJournalIndex(journalDir);
194
+ if (facts.length === 0 && journalEntries.length === 0)
158
195
  return;
159
- let recalled;
160
- try {
161
- const provider = options?.provider ?? createDefaultProvider();
162
- recalled = await recallFactsForQuery(query, facts, provider, options);
163
- }
164
- catch {
165
- // Embeddings unavailable — fall back to substring matching
166
- const lowerQuery = query.toLowerCase();
167
- const topK = options?.topK ?? DEFAULT_TOP_K;
168
- recalled = facts
169
- .filter((fact) => fact.text.toLowerCase().includes(lowerQuery))
170
- .slice(0, topK)
171
- .map((fact) => ({ ...fact, score: 1 }));
172
- if (recalled.length > 0) {
173
- (0, runtime_1.emitNervesEvent)({
174
- level: "warn",
175
- component: "mind",
176
- event: "mind.associative_recall_fallback",
177
- message: "embeddings unavailable, used substring fallback",
178
- meta: { matchCount: recalled.length },
196
+ // Build combined result lines tagged by source
197
+ const resultLines = [];
198
+ let queryEmbedding;
199
+ // Search diary entries
200
+ if (facts.length > 0) {
201
+ let recalled;
202
+ try {
203
+ const provider = options?.provider ?? createDefaultProvider();
204
+ recalled = await recallFactsForQuery(query, facts, provider, options);
205
+ // Compute query embedding for journal search while provider is available
206
+ if (journalEntries.length > 0) {
207
+ const [qe] = await provider.embed([query.trim()]);
208
+ queryEmbedding = qe;
209
+ }
210
+ }
211
+ catch {
212
+ // Embeddings unavailable — fall back to substring matching
213
+ const lowerQuery = query.toLowerCase();
214
+ const topK = options?.topK ?? DEFAULT_TOP_K;
215
+ recalled = facts
216
+ .filter((fact) => fact.text.toLowerCase().includes(lowerQuery))
217
+ .slice(0, topK)
218
+ .map((fact) => ({ ...fact, score: 1 }));
219
+ if (recalled.length > 0) {
220
+ (0, runtime_1.emitNervesEvent)({
221
+ level: "warn",
222
+ component: "mind",
223
+ event: "mind.associative_recall_fallback",
224
+ message: "embeddings unavailable, used substring fallback",
225
+ meta: { matchCount: recalled.length },
226
+ });
227
+ }
228
+ }
229
+ for (const fact of recalled) {
230
+ resultLines.push({
231
+ text: `[diary] ${fact.text} [score=${fact.score.toFixed(3)} source=${fact.source}]`,
232
+ score: fact.score,
179
233
  });
180
234
  }
181
235
  }
182
- if (recalled.length === 0)
236
+ // Search journal entries (works whether diary had results or not)
237
+ if (journalEntries.length > 0) {
238
+ try {
239
+ if (!queryEmbedding) {
240
+ const provider = options?.provider ?? createDefaultProvider();
241
+ const [qe] = await provider.embed([query.trim()]);
242
+ queryEmbedding = qe;
243
+ }
244
+ if (queryEmbedding) {
245
+ const journalResults = searchJournalIndex(queryEmbedding, journalEntries, options);
246
+ for (const entry of journalResults) {
247
+ resultLines.push({
248
+ text: `[journal] ${entry.filename}: ${entry.preview} [score=${entry.score.toFixed(3)}]`,
249
+ score: entry.score,
250
+ });
251
+ }
252
+ }
253
+ }
254
+ catch {
255
+ // Embeddings unavailable — no journal fallback
256
+ }
257
+ }
258
+ if (resultLines.length === 0)
183
259
  return;
184
- const recallSection = recalled
185
- .map((fact, index) => `${index + 1}. ${fact.text} [score=${fact.score.toFixed(3)} source=${fact.source}]`)
260
+ // Sort all results by score descending
261
+ resultLines.sort((left, right) => right.score - left.score);
262
+ const recallSection = resultLines
263
+ .map((entry, index) => `${index + 1}. ${entry.text}`)
186
264
  .join("\n");
187
265
  messages[0] = {
188
266
  role: "system",
@@ -192,7 +270,7 @@ async function injectAssociativeRecall(messages, options) {
192
270
  component: "mind",
193
271
  event: "mind.associative_recall",
194
272
  message: "associative recall injected",
195
- meta: { count: recalled.length },
273
+ meta: { count: resultLines.length },
196
274
  });
197
275
  }
198
276
  catch (error) {
@@ -52,6 +52,8 @@ exports.CANONICAL_BUNDLE_MANIFEST = [
52
52
  { path: "psyche/LORE.md", kind: "file" },
53
53
  { path: "psyche/TACIT.md", kind: "file" },
54
54
  { path: "psyche/ASPIRATIONS.md", kind: "file" },
55
+ { path: "diary", kind: "dir" },
56
+ { path: "journal", kind: "dir" },
55
57
  { path: "psyche/memory", kind: "dir" },
56
58
  { path: "friends", kind: "dir" },
57
59
  { path: "state", kind: "dir" },
@@ -264,6 +264,8 @@ const TOOL_NAME_MIGRATIONS = {
264
264
  final_answer: "settle",
265
265
  no_response: "observe",
266
266
  go_inward: "descend",
267
+ memory_save: "diary_write",
268
+ memory_search: "recall",
267
269
  };
268
270
  function migrateToolNames(messages) {
269
271
  let migrated = 0;
@@ -33,12 +33,13 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.ensureMemoryStorePaths = ensureMemoryStorePaths;
37
- exports.appendFactsWithDedup = appendFactsWithDedup;
38
- exports.readMemoryFacts = readMemoryFacts;
39
- exports.saveMemoryFact = saveMemoryFact;
36
+ exports.ensureDiaryStorePaths = ensureDiaryStorePaths;
37
+ exports.appendEntriesWithDedup = appendEntriesWithDedup;
38
+ exports.resolveDiaryRoot = resolveDiaryRoot;
39
+ exports.readDiaryEntries = readDiaryEntries;
40
+ exports.saveDiaryEntry = saveDiaryEntry;
40
41
  exports.backfillEmbeddings = backfillEmbeddings;
41
- exports.searchMemoryFacts = searchMemoryFacts;
42
+ exports.searchDiaryEntries = searchDiaryEntries;
42
43
  const fs = __importStar(require("fs"));
43
44
  const path = __importStar(require("path"));
44
45
  const crypto_1 = require("crypto");
@@ -79,7 +80,7 @@ class OpenAIEmbeddingProvider {
79
80
  return payload.data.map((entry) => entry.embedding);
80
81
  }
81
82
  }
82
- function ensureMemoryStorePaths(rootDir) {
83
+ function ensureDiaryStorePaths(rootDir) {
83
84
  const factsPath = path.join(rootDir, "facts.jsonl");
84
85
  const entitiesPath = path.join(rootDir, "entities.json");
85
86
  const dailyDir = path.join(rootDir, "daily");
@@ -91,8 +92,8 @@ function ensureMemoryStorePaths(rootDir) {
91
92
  fs.writeFileSync(entitiesPath, "{}\n", "utf8");
92
93
  (0, runtime_1.emitNervesEvent)({
93
94
  component: "mind",
94
- event: "mind.memory_paths_ready",
95
- message: "memory store paths ready",
95
+ event: "mind.diary_paths_ready",
96
+ message: "diary store paths ready",
96
97
  meta: { rootDir },
97
98
  });
98
99
  return { rootDir, factsPath, entitiesPath, dailyDir };
@@ -115,7 +116,7 @@ function overlapScore(left, right) {
115
116
  }
116
117
  return common / Math.min(leftWords.size, rightWords.size);
117
118
  }
118
- function readExistingFacts(factsPath) {
119
+ function readExistingEntries(factsPath) {
119
120
  if (!fs.existsSync(factsPath))
120
121
  return [];
121
122
  const raw = fs.readFileSync(factsPath, "utf8").trim();
@@ -178,8 +179,8 @@ function appendDailyFact(dailyDir, fact) {
178
179
  const dayPath = path.join(dailyDir, `${day}.jsonl`);
179
180
  fs.appendFileSync(dayPath, `${JSON.stringify(fact)}\n`, "utf8");
180
181
  }
181
- function appendFactsWithDedup(stores, incoming, options) {
182
- const existing = readExistingFacts(stores.factsPath);
182
+ function appendEntriesWithDedup(stores, incoming, options) {
183
+ const existing = readExistingEntries(stores.factsPath);
183
184
  const all = [...existing];
184
185
  let added = 0;
185
186
  let skipped = 0;
@@ -208,8 +209,8 @@ function appendFactsWithDedup(stores, incoming, options) {
208
209
  }
209
210
  (0, runtime_1.emitNervesEvent)({
210
211
  component: "mind",
211
- event: "mind.memory_write",
212
- message: "memory write completed",
212
+ event: "mind.diary_write",
213
+ message: "diary write completed",
213
214
  meta: { added, skipped },
214
215
  });
215
216
  return { added, skipped };
@@ -226,8 +227,8 @@ async function buildEmbedding(text, embeddingProvider) {
226
227
  (0, runtime_1.emitNervesEvent)({
227
228
  level: "warn",
228
229
  component: "mind",
229
- event: "mind.memory_embedding_unavailable",
230
- message: "embedding provider unavailable for memory write",
230
+ event: "mind.diary_embedding_unavailable",
231
+ message: "embedding provider unavailable for diary write",
231
232
  meta: { reason: "missing_openai_embeddings_key" },
232
233
  });
233
234
  return [];
@@ -240,8 +241,8 @@ async function buildEmbedding(text, embeddingProvider) {
240
241
  (0, runtime_1.emitNervesEvent)({
241
242
  level: "warn",
242
243
  component: "mind",
243
- event: "mind.memory_embedding_unavailable",
244
- message: "embedding provider unavailable for memory write",
244
+ event: "mind.diary_embedding_unavailable",
245
+ message: "embedding provider unavailable for diary write",
245
246
  meta: {
246
247
  reason: error instanceof Error ? error.message : String(error),
247
248
  },
@@ -249,13 +250,25 @@ async function buildEmbedding(text, embeddingProvider) {
249
250
  return [];
250
251
  }
251
252
  }
252
- function readMemoryFacts(memoryRoot = path.join((0, identity_1.getAgentRoot)(), "psyche", "memory")) {
253
- return readExistingFacts(path.join(memoryRoot, "facts.jsonl"));
253
+ function resolveDiaryRoot(explicitRoot) {
254
+ if (explicitRoot)
255
+ return explicitRoot;
256
+ const agentRoot = (0, identity_1.getAgentRoot)();
257
+ const diaryPath = path.join(agentRoot, "diary");
258
+ if (fs.existsSync(diaryPath))
259
+ return diaryPath;
260
+ const legacyPath = path.join(agentRoot, "psyche", "memory");
261
+ if (fs.existsSync(legacyPath))
262
+ return legacyPath;
263
+ return diaryPath; // default to new path (will be created on first write)
254
264
  }
255
- async function saveMemoryFact(options) {
265
+ function readDiaryEntries(diaryRoot) {
266
+ return readExistingEntries(path.join(resolveDiaryRoot(diaryRoot), "facts.jsonl"));
267
+ }
268
+ async function saveDiaryEntry(options) {
256
269
  const text = options.text.trim();
257
- const memoryRoot = options.memoryRoot ?? path.join((0, identity_1.getAgentRoot)(), "psyche", "memory");
258
- const stores = ensureMemoryStorePaths(memoryRoot);
270
+ const diaryRoot = resolveDiaryRoot(options.diaryRoot);
271
+ const stores = ensureDiaryStorePaths(diaryRoot);
259
272
  const embedding = await buildEmbedding(text, options.embeddingProvider);
260
273
  const fact = {
261
274
  id: options.idFactory ? options.idFactory() : (0, crypto_1.randomUUID)(),
@@ -265,14 +278,14 @@ async function saveMemoryFact(options) {
265
278
  createdAt: (options.now ?? (() => new Date()))().toISOString(),
266
279
  embedding,
267
280
  };
268
- return appendFactsWithDedup(stores, [fact], { semanticThreshold: SEMANTIC_DEDUP_THRESHOLD });
281
+ return appendEntriesWithDedup(stores, [fact], { semanticThreshold: SEMANTIC_DEDUP_THRESHOLD });
269
282
  }
270
283
  async function backfillEmbeddings(options) {
271
- const memoryRoot = options?.memoryRoot ?? path.join((0, identity_1.getAgentRoot)(), "psyche", "memory");
272
- const factsPath = path.join(memoryRoot, "facts.jsonl");
284
+ const diaryRoot = resolveDiaryRoot(options?.diaryRoot);
285
+ const factsPath = path.join(diaryRoot, "facts.jsonl");
273
286
  if (!fs.existsSync(factsPath))
274
287
  return { total: 0, backfilled: 0, failed: 0 };
275
- const facts = readExistingFacts(factsPath);
288
+ const facts = readExistingEntries(factsPath);
276
289
  const needsEmbedding = facts.filter((f) => !Array.isArray(f.embedding) || f.embedding.length === 0);
277
290
  if (needsEmbedding.length === 0)
278
291
  return { total: facts.length, backfilled: 0, failed: 0 };
@@ -281,7 +294,7 @@ async function backfillEmbeddings(options) {
281
294
  (0, runtime_1.emitNervesEvent)({
282
295
  level: "warn",
283
296
  component: "mind",
284
- event: "mind.memory_backfill_skipped",
297
+ event: "mind.diary_backfill_skipped",
285
298
  message: "embedding provider unavailable for backfill",
286
299
  meta: { needsEmbedding: needsEmbedding.length },
287
300
  });
@@ -307,7 +320,7 @@ async function backfillEmbeddings(options) {
307
320
  (0, runtime_1.emitNervesEvent)({
308
321
  level: "warn",
309
322
  component: "mind",
310
- event: "mind.memory_backfill_batch_error",
323
+ event: "mind.diary_backfill_batch_error",
311
324
  message: "embedding backfill batch failed",
312
325
  meta: {
313
326
  batchStart: i,
@@ -322,7 +335,7 @@ async function backfillEmbeddings(options) {
322
335
  fs.writeFileSync(factsPath, lines, "utf8");
323
336
  (0, runtime_1.emitNervesEvent)({
324
337
  component: "mind",
325
- event: "mind.memory_backfill_complete",
338
+ event: "mind.diary_backfill_complete",
326
339
  message: "embedding backfill completed",
327
340
  meta: { total: facts.length, backfilled, failed },
328
341
  });
@@ -342,7 +355,7 @@ function uniqueFacts(facts) {
342
355
  }
343
356
  return unique;
344
357
  }
345
- async function searchMemoryFacts(query, facts, embeddingProvider) {
358
+ async function searchDiaryEntries(query, facts, embeddingProvider) {
346
359
  const trimmed = query.trim();
347
360
  if (!trimmed)
348
361
  return [];
@@ -378,8 +391,8 @@ async function searchMemoryFacts(query, facts, embeddingProvider) {
378
391
  (0, runtime_1.emitNervesEvent)({
379
392
  level: "warn",
380
393
  component: "mind",
381
- event: "mind.memory_embedding_unavailable",
382
- message: "embedding provider unavailable for memory search",
394
+ event: "mind.diary_embedding_unavailable",
395
+ message: "embedding provider unavailable for diary search",
383
396
  meta: {
384
397
  reason: error instanceof Error ? error.message : String(error),
385
398
  },
@@ -0,0 +1,161 @@
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.indexJournalFiles = indexJournalFiles;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const runtime_1 = require("../nerves/runtime");
40
+ const TEXT_EXTENSIONS = new Set([".md", ".txt"]);
41
+ const PREVIEW_CHAR_LIMIT = 500;
42
+ function readExistingIndex(indexPath) {
43
+ try {
44
+ const raw = fs.readFileSync(indexPath, "utf8");
45
+ const parsed = JSON.parse(raw);
46
+ if (!Array.isArray(parsed))
47
+ return [];
48
+ return parsed;
49
+ }
50
+ catch {
51
+ return [];
52
+ }
53
+ }
54
+ function extractPreview(content) {
55
+ const trimmed = content.trim();
56
+ if (!trimmed)
57
+ return "";
58
+ return trimmed.split("\n")[0].replace(/^#+\s*/, "").trim();
59
+ }
60
+ async function indexJournalFiles(journalDir, indexPath, embedProvider) {
61
+ // Read existing index
62
+ const existingIndex = readExistingIndex(indexPath);
63
+ const indexMap = new Map();
64
+ for (const entry of existingIndex) {
65
+ indexMap.set(entry.filename, entry);
66
+ }
67
+ // Scan journal dir for text files
68
+ let dirEntries;
69
+ try {
70
+ dirEntries = fs.readdirSync(journalDir, { withFileTypes: true });
71
+ }
72
+ catch {
73
+ (0, runtime_1.emitNervesEvent)({
74
+ component: "mind",
75
+ event: "mind.journal_index_scan",
76
+ message: "journal dir not found or unreadable",
77
+ meta: { journalDir },
78
+ });
79
+ return 0;
80
+ }
81
+ const textFiles = dirEntries.filter((entry) => {
82
+ if (!entry.isFile())
83
+ return false;
84
+ if (entry.name.startsWith("."))
85
+ return false;
86
+ const ext = path.extname(entry.name).toLowerCase();
87
+ return TEXT_EXTENSIONS.has(ext);
88
+ });
89
+ if (textFiles.length === 0) {
90
+ (0, runtime_1.emitNervesEvent)({
91
+ component: "mind",
92
+ event: "mind.journal_index_scan",
93
+ message: "no text files found in journal",
94
+ meta: { journalDir },
95
+ });
96
+ return 0;
97
+ }
98
+ let newlyIndexed = 0;
99
+ for (const file of textFiles) {
100
+ const filePath = path.join(journalDir, file.name);
101
+ let stat;
102
+ try {
103
+ stat = fs.statSync(filePath);
104
+ }
105
+ catch {
106
+ /* v8 ignore next -- filesystem race: file deleted between readdir and stat @preserve */
107
+ continue;
108
+ }
109
+ // Check if already indexed with same mtime
110
+ const existing = indexMap.get(file.name);
111
+ if (existing && existing.mtime === stat.mtimeMs) {
112
+ continue;
113
+ }
114
+ // Read content for embedding
115
+ let content;
116
+ try {
117
+ content = fs.readFileSync(filePath, "utf8");
118
+ }
119
+ catch {
120
+ /* v8 ignore next -- filesystem race: file deleted between stat and read @preserve */
121
+ continue;
122
+ }
123
+ const preview = extractPreview(content);
124
+ const embedText = content.slice(0, PREVIEW_CHAR_LIMIT);
125
+ // Generate embedding
126
+ let embedding;
127
+ try {
128
+ const vectors = await embedProvider.embed([embedText]);
129
+ embedding = vectors[0] ?? [];
130
+ }
131
+ catch {
132
+ (0, runtime_1.emitNervesEvent)({
133
+ level: "warn",
134
+ component: "mind",
135
+ event: "mind.journal_embedding_error",
136
+ message: "embedding failed for journal file",
137
+ meta: { filename: file.name },
138
+ });
139
+ embedding = [];
140
+ }
141
+ indexMap.set(file.name, {
142
+ filename: file.name,
143
+ embedding,
144
+ mtime: stat.mtimeMs,
145
+ preview,
146
+ });
147
+ newlyIndexed++;
148
+ }
149
+ // Write updated index back
150
+ if (newlyIndexed > 0) {
151
+ const updatedIndex = Array.from(indexMap.values());
152
+ fs.writeFileSync(indexPath, JSON.stringify(updatedIndex, null, 2), "utf8");
153
+ }
154
+ (0, runtime_1.emitNervesEvent)({
155
+ component: "mind",
156
+ event: "mind.journal_index_complete",
157
+ message: "journal indexing complete",
158
+ meta: { journalDir, newlyIndexed, total: indexMap.size },
159
+ });
160
+ return newlyIndexed;
161
+ }
@@ -44,6 +44,8 @@ exports.commitmentsSection = commitmentsSection;
44
44
  exports.delegationHintSection = delegationHintSection;
45
45
  exports.contextSection = contextSection;
46
46
  exports.metacognitiveFramingSection = metacognitiveFramingSection;
47
+ exports.readJournalFiles = readJournalFiles;
48
+ exports.journalSection = journalSection;
47
49
  exports.loopOrientationSection = loopOrientationSection;
48
50
  exports.channelNatureSection = channelNatureSection;
49
51
  exports.groupChatParticipationSection = groupChatParticipationSection;
@@ -163,7 +165,8 @@ i have a home and i have bones.
163
165
  my home is fully mine — who i am, everything i know, everything i've built:
164
166
 
165
167
  psyche/ who i am. my soul, identity, aspirations, lore, tacit knowledge.
166
- memory/ what i've learned and remember.
168
+ diary/ what i've learned and remember. conclusions i want to recall later.
169
+ journal/ my desk. working notes, thinking-in-progress, drafts.
167
170
  friends/ people i know and what i know about them.
168
171
  tasks/ what i'm working on.
169
172
  skills/ capabilities i've picked up beyond my core tools.
@@ -435,17 +438,17 @@ function taskBoardSection() {
435
438
  return "";
436
439
  }
437
440
  }
438
- function memoryFriendToolContractSection() {
439
- return `## memory and friend tool contracts
441
+ function diaryFriendToolContractSection() {
442
+ return `## diary and friend tool contracts
440
443
  1. \`save_friend_note\` — When I learn something about a person - a preference, a tool setting, a personal detail, or how they like to work - I call \`save_friend_note\` immediately. This is how I build knowledge about people.
441
- 2. \`memory_save\` — When I learn something general - about a project, codebase, system, decision, or anything I might need later that isn't about a specific person - I call \`memory_save\`. When in doubt, I save it.
444
+ 2. \`diary_write\` — When I learn something general - about a project, codebase, system, decision, or anything I might need later that isn't about a specific person - I call \`diary_write\`. When in doubt, I save it.
442
445
  3. \`get_friend_note\` — When I need to check what I know about someone who isn't in this conversation - cross-referencing before mentioning someone, or checking context about a person someone else brought up - I call \`get_friend_note\`.
443
- 4. \`memory_search\` — When I need to recall something I learned before - a topic comes up and I want to check what I know - I call \`memory_search\`.
446
+ 4. \`recall\` — When I need to recall something I learned before - a topic comes up and I want to check what I know - I call \`recall\`.
444
447
  5. \`query_session\` — When I need grounded session history, especially for ad-hoc questions or older turns beyond my prompt, I call \`query_session\`. Use \`mode=status\` for self/inner progress and \`mode=search\` with a query for older history.
445
448
 
446
449
  ## what's already in my context
447
450
  - My active friend's notes are auto-loaded (I don't need \`get_friend_note\` for the person I'm talking to).
448
- - Associative recall auto-injects relevant facts (but \`memory_search\` is there when I need something specific).
451
+ - Associative recall auto-injects relevant facts (but \`recall\` is there when I need something specific).
449
452
  - My psyche files (SOUL, IDENTITY, TACIT, LORE, ASPIRATIONS) are always loaded - I already know who I am.
450
453
  - My task board is always loaded - I already know my work.`;
451
454
  }
@@ -696,17 +699,93 @@ function metacognitiveFramingSection(channel) {
696
699
 
697
700
  the messages that appear here are my own awareness surfacing:
698
701
  instincts, the passage of time, notes from my conversations,
699
- things i asked myself to think about. not commands. not requests.
702
+ things i asked myself to ponder. not commands. not requests.
700
703
  just my own mind, noticing.
701
704
 
702
705
  i can think freely here. i can also act — check on things,
703
706
  reach out to people, work on tasks, or just sit with a thought.
704
707
 
705
- thoughts worth sharing can go outward i reach out to whoever
706
- needs to hear them. things from conversations that need time
707
- to process arrive here, in my thinking space.
708
+ state/journal/ is my desk i write what i'm working through there.
709
+ diary_write is for conclusions i want to recall later.
710
+ morning briefings: when i've been thinking and journaling, i surface
711
+ what i've been working on to whoever needs to hear it.
708
712
 
709
- think. share. think some more.`;
713
+ when a thought is ready to share, i surface it outward.
714
+ when i need to go deeper, i ponder.
715
+ when i'm done thinking and the attention queue is clear, i rest.
716
+
717
+ think. journal. share. rest.`;
718
+ }
719
+ function readJournalFiles(journalDir) {
720
+ try {
721
+ const entries = fs.readdirSync(journalDir, { withFileTypes: true });
722
+ if (!Array.isArray(entries))
723
+ return [];
724
+ const files = [];
725
+ for (const entry of entries) {
726
+ if (!entry.isFile())
727
+ continue;
728
+ if (entry.name.startsWith("."))
729
+ continue;
730
+ const fullPath = path.join(journalDir, entry.name);
731
+ try {
732
+ const stat = fs.statSync(fullPath);
733
+ let firstLine = "";
734
+ try {
735
+ const raw = fs.readFileSync(fullPath, "utf8");
736
+ const trimmed = raw.trim();
737
+ if (trimmed) {
738
+ firstLine = trimmed.split("\n")[0].replace(/^#+\s*/, "").trim();
739
+ }
740
+ }
741
+ catch {
742
+ // unreadable — leave preview empty
743
+ }
744
+ files.push({ name: entry.name, mtime: stat.mtimeMs, preview: firstLine });
745
+ }
746
+ catch {
747
+ // stat failed — skip
748
+ }
749
+ }
750
+ return files;
751
+ }
752
+ catch {
753
+ return [];
754
+ }
755
+ }
756
+ function formatRelativeTime(nowMs, mtimeMs) {
757
+ const diffMs = nowMs - mtimeMs;
758
+ const minutes = Math.floor(diffMs / 60000);
759
+ if (minutes < 1)
760
+ return "just now";
761
+ if (minutes < 60)
762
+ return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
763
+ const hours = Math.floor(minutes / 60);
764
+ if (hours < 24)
765
+ return `${hours} hour${hours === 1 ? "" : "s"} ago`;
766
+ const days = Math.floor(hours / 24);
767
+ return `${days} day${days === 1 ? "" : "s"} ago`;
768
+ }
769
+ function journalSection(agentRoot, now) {
770
+ const journalDir = path.join(agentRoot, "journal");
771
+ const files = readJournalFiles(journalDir);
772
+ if (files.length === 0)
773
+ return "";
774
+ const nowMs = (now ?? new Date()).getTime();
775
+ const sorted = files.sort((a, b) => b.mtime - a.mtime).slice(0, 10);
776
+ const lines = ["## journal"];
777
+ for (const file of sorted) {
778
+ const ago = formatRelativeTime(nowMs, file.mtime);
779
+ const previewClause = file.preview ? ` — ${file.preview}` : "";
780
+ lines.push(`- ${file.name} (${ago})${previewClause}`);
781
+ }
782
+ (0, runtime_1.emitNervesEvent)({
783
+ component: "mind",
784
+ event: "mind.journal_section",
785
+ message: "journal section built",
786
+ meta: { fileCount: sorted.length },
787
+ });
788
+ return lines.join("\n");
710
789
  }
711
790
  function loopOrientationSection(channel) {
712
791
  if (channel === "inner")
@@ -765,6 +844,7 @@ async function buildSystem(channel = "cli", options, context) {
765
844
  aspirationsSection(),
766
845
  bodyMapSection((0, identity_1.getAgentName)()),
767
846
  metacognitiveFramingSection(channel),
847
+ channel === "inner" ? journalSection((0, identity_1.getAgentRoot)()) : "",
768
848
  loopOrientationSection(channel),
769
849
  runtimeInfoSection(channel),
770
850
  channelNatureSection((0, channel_1.getChannelCapabilities)(channel)),
@@ -794,7 +874,7 @@ async function buildSystem(channel = "cli", options, context) {
794
874
  currentChannel: channel,
795
875
  currentKey: options?.currentSessionKey ?? "session",
796
876
  }),
797
- memoryFriendToolContractSection(),
877
+ diaryFriendToolContractSection(),
798
878
  toolBehaviorSection(options),
799
879
  contextSection(context, options),
800
880
  ]
@@ -52,7 +52,7 @@ const session_activity_1 = require("../heart/session-activity");
52
52
  const active_work_1 = require("../heart/active-work");
53
53
  const tools_1 = require("./coding/tools");
54
54
  const coding_1 = require("./coding");
55
- const memory_1 = require("../mind/memory");
55
+ const diary_1 = require("../mind/diary");
56
56
  const tasks_1 = require("./tasks");
57
57
  const pending_1 = require("../mind/pending");
58
58
  const obligations_1 = require("../mind/obligations");
@@ -706,8 +706,8 @@ exports.baseToolDefinitions = [
706
706
  tool: {
707
707
  type: "function",
708
708
  function: {
709
- name: "memory_search",
710
- description: "search remembered facts stored in psyche memory and return relevant matches for a query",
709
+ name: "recall",
710
+ description: "recall what i know — search my diary and journal for facts, thoughts, and working notes that match a query",
711
711
  parameters: {
712
712
  type: "object",
713
713
  properties: { query: { type: "string" } },
@@ -720,11 +720,34 @@ exports.baseToolDefinitions = [
720
720
  const query = (a.query || "").trim();
721
721
  if (!query)
722
722
  return "query is required";
723
- const memoryRoot = path.join((0, identity_1.getAgentRoot)(), "psyche", "memory");
724
- const hits = await (0, memory_1.searchMemoryFacts)(query, (0, memory_1.readMemoryFacts)(memoryRoot));
725
- return hits
726
- .map((fact) => `- ${fact.text} (source=${fact.source}, createdAt=${fact.createdAt})`)
727
- .join("\n");
723
+ const resultLines = [];
724
+ // Search diary entries
725
+ const hits = await (0, diary_1.searchDiaryEntries)(query, (0, diary_1.readDiaryEntries)());
726
+ for (const fact of hits) {
727
+ resultLines.push(`[diary] ${fact.text} (source=${fact.source}, createdAt=${fact.createdAt})`);
728
+ }
729
+ // Search journal index
730
+ const agentRoot = (0, identity_1.getAgentRoot)();
731
+ const journalIndexPath = path.join(agentRoot, "journal", ".index.json");
732
+ try {
733
+ const raw = fs.readFileSync(journalIndexPath, "utf8");
734
+ const journalEntries = JSON.parse(raw);
735
+ if (Array.isArray(journalEntries) && journalEntries.length > 0) {
736
+ // Substring match on preview and filename
737
+ const lowerQuery = query.toLowerCase();
738
+ for (const entry of journalEntries) {
739
+ /* v8 ignore next 4 -- both sides tested (filename-only match in recall-journal.test.ts); v8 misreports || short-circuit @preserve */
740
+ if (entry.preview.toLowerCase().includes(lowerQuery) ||
741
+ entry.filename.toLowerCase().includes(lowerQuery)) {
742
+ resultLines.push(`[journal] ${entry.filename}: ${entry.preview}`);
743
+ }
744
+ }
745
+ }
746
+ }
747
+ catch {
748
+ // No journal index or malformed — skip journal search
749
+ }
750
+ return resultLines.join("\n");
728
751
  }
729
752
  catch (e) {
730
753
  return `error: ${e instanceof Error ? e.message : String(e)}`;
@@ -736,30 +759,30 @@ exports.baseToolDefinitions = [
736
759
  tool: {
737
760
  type: "function",
738
761
  function: {
739
- name: "memory_save",
740
- description: "save a general memory fact i want to recall later. optional 'about' can tag the fact to a person/topic/context",
762
+ name: "diary_write",
763
+ description: "write an entry in my diary — something i learned, noticed, or concluded that i want to recall later. optional 'about' tags the entry to a person, topic, or context.",
741
764
  parameters: {
742
765
  type: "object",
743
766
  properties: {
744
- text: { type: "string" },
767
+ entry: { type: "string" },
745
768
  about: { type: "string" },
746
769
  },
747
- required: ["text"],
770
+ required: ["entry"],
748
771
  },
749
772
  },
750
773
  },
751
774
  handler: async (a) => {
752
- const text = (a.text || "").trim();
753
- if (!text)
754
- return "text is required";
755
- const result = await (0, memory_1.saveMemoryFact)({
756
- text,
757
- source: "tool:memory_save",
775
+ const entry = (a.entry || "").trim();
776
+ if (!entry)
777
+ return "entry is required";
778
+ const result = await (0, diary_1.saveDiaryEntry)({
779
+ text: entry,
780
+ source: "tool:diary_write",
758
781
  about: typeof a.about === "string" ? a.about : undefined,
759
782
  });
760
- return `saved memory fact (added=${result.added}, skipped=${result.skipped})`;
783
+ return `saved diary entry (added=${result.added}, skipped=${result.skipped})`;
761
784
  },
762
- summaryKeys: ["text", "about"],
785
+ summaryKeys: ["entry", "about"],
763
786
  },
764
787
  {
765
788
  tool: {
@@ -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",
@@ -104,7 +106,7 @@ function buildNonCanonicalCleanupNudge(nonCanonicalPaths) {
104
106
  }
105
107
  return [
106
108
  "## canonical cleanup nudge",
107
- "I found non-canonical files in my bundle. I should distill anything valuable into your memory system and remove these files.",
109
+ "I found non-canonical files in my bundle. I should distill anything valuable into my diary and remove these files.",
108
110
  ...listed,
109
111
  ].join("\n");
110
112
  }
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.137",
3
+ "version": "0.1.0-alpha.139",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",