@ouro.bot/cli 0.1.0-alpha.21 → 0.1.0-alpha.210

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.
Files changed (227) hide show
  1. package/README.md +187 -191
  2. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +3 -2
  3. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +1 -1
  4. package/changelog.json +1312 -0
  5. package/dist/arc/attention-types.js +8 -0
  6. package/dist/arc/cares.js +179 -0
  7. package/dist/arc/episodes.js +119 -0
  8. package/dist/arc/intentions.js +173 -0
  9. package/dist/arc/obligations.js +321 -0
  10. package/dist/arc/presence.js +185 -0
  11. package/dist/heart/active-work.js +832 -0
  12. package/dist/heart/agent-entry.js +37 -2
  13. package/dist/heart/auth/auth-flow.js +444 -0
  14. package/dist/heart/bridges/manager.js +358 -0
  15. package/dist/heart/bridges/state-machine.js +135 -0
  16. package/dist/heart/bridges/store.js +123 -0
  17. package/dist/heart/commitments.js +111 -0
  18. package/dist/heart/config-registry.js +305 -0
  19. package/dist/heart/config.js +59 -30
  20. package/dist/heart/core.js +606 -131
  21. package/dist/heart/cross-chat-delivery.js +146 -0
  22. package/dist/heart/daemon/agent-config-check.js +149 -0
  23. package/dist/heart/daemon/agent-discovery.js +81 -0
  24. package/dist/heart/daemon/agent-service.js +360 -0
  25. package/dist/heart/daemon/cadence.js +70 -0
  26. package/dist/heart/daemon/cli-defaults.js +557 -0
  27. package/dist/heart/daemon/cli-exec.js +1847 -0
  28. package/dist/heart/daemon/cli-parse.js +759 -0
  29. package/dist/heart/daemon/cli-render.js +316 -0
  30. package/dist/heart/daemon/cli-types.js +8 -0
  31. package/dist/heart/daemon/daemon-cli.js +29 -1163
  32. package/dist/heart/daemon/daemon-entry.js +182 -4
  33. package/dist/heart/daemon/daemon-health.js +136 -0
  34. package/dist/heart/daemon/daemon-runtime-sync.js +153 -12
  35. package/dist/heart/daemon/daemon-tombstone.js +123 -0
  36. package/dist/heart/daemon/daemon.js +491 -18
  37. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  38. package/dist/heart/daemon/hooks/bundle-meta.js +206 -0
  39. package/dist/heart/daemon/inner-status.js +89 -0
  40. package/dist/heart/daemon/launchd.js +165 -0
  41. package/dist/heart/daemon/log-tailer.js +4 -3
  42. package/dist/heart/daemon/message-router.js +17 -8
  43. package/dist/heart/daemon/os-cron-deps.js +134 -0
  44. package/dist/heart/daemon/ouro-bot-entry.js +1 -1
  45. package/dist/heart/daemon/process-manager.js +123 -0
  46. package/dist/heart/daemon/run-hooks.js +39 -0
  47. package/dist/heart/daemon/runtime-logging.js +58 -15
  48. package/dist/heart/daemon/runtime-metadata.js +101 -0
  49. package/dist/heart/daemon/runtime-mode.js +67 -0
  50. package/dist/heart/daemon/safe-mode.js +161 -0
  51. package/dist/heart/daemon/sense-manager.js +71 -2
  52. package/dist/heart/daemon/session-id-resolver.js +131 -0
  53. package/dist/heart/daemon/skill-management-installer.js +94 -0
  54. package/dist/heart/daemon/socket-client.js +202 -0
  55. package/dist/heart/daemon/task-scheduler.js +3 -25
  56. package/dist/heart/daemon/thoughts.js +507 -0
  57. package/dist/heart/delegation.js +62 -0
  58. package/dist/heart/habits/habit-migration.js +181 -0
  59. package/dist/heart/habits/habit-parser.js +119 -0
  60. package/dist/heart/habits/habit-scheduler.js +369 -0
  61. package/dist/heart/{daemon → hatch}/hatch-flow.js +29 -119
  62. package/dist/heart/{daemon → hatch}/hatch-specialist.js +3 -3
  63. package/dist/heart/{daemon → hatch}/specialist-prompt.js +10 -7
  64. package/dist/heart/{daemon → hatch}/specialist-tools.js +49 -3
  65. package/dist/heart/identity.js +144 -55
  66. package/dist/heart/kicks.js +2 -20
  67. package/dist/heart/mcp/mcp-server.js +608 -0
  68. package/dist/heart/migrate-config.js +127 -0
  69. package/dist/heart/model-capabilities.js +48 -0
  70. package/dist/heart/outlook/outlook-http.js +439 -0
  71. package/dist/heart/outlook/outlook-read.js +1618 -0
  72. package/dist/heart/outlook/outlook-render.js +1032 -0
  73. package/dist/heart/outlook/outlook-types.js +7 -0
  74. package/dist/heart/outlook/outlook-view.js +194 -0
  75. package/dist/heart/progress-story.js +42 -0
  76. package/dist/heart/provider-failover.js +88 -0
  77. package/dist/heart/provider-ping.js +162 -0
  78. package/dist/heart/providers/anthropic-token.js +163 -0
  79. package/dist/heart/providers/anthropic.js +187 -41
  80. package/dist/heart/providers/azure.js +116 -10
  81. package/dist/heart/providers/github-copilot.js +157 -0
  82. package/dist/heart/providers/minimax.js +33 -3
  83. package/dist/heart/providers/openai-codex.js +51 -16
  84. package/dist/heart/session-activity.js +172 -0
  85. package/dist/heart/session-recall.js +216 -0
  86. package/dist/heart/start-of-turn-packet.js +330 -0
  87. package/dist/heart/streaming.js +36 -27
  88. package/dist/heart/sync.js +217 -0
  89. package/dist/heart/target-resolution.js +127 -0
  90. package/dist/heart/tempo.js +93 -0
  91. package/dist/heart/temporal-view.js +41 -0
  92. package/dist/heart/tool-activity-callbacks.js +36 -0
  93. package/dist/heart/tool-description.js +135 -0
  94. package/dist/heart/tool-loop.js +200 -0
  95. package/dist/heart/turn-context.js +358 -0
  96. package/dist/heart/turn-coordinator.js +28 -0
  97. package/dist/heart/versioning/ouro-bot-global-installer.js +128 -0
  98. package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
  99. package/dist/heart/{daemon → versioning}/ouro-path-installer.js +78 -35
  100. package/dist/heart/versioning/ouro-version-manager.js +171 -0
  101. package/dist/heart/versioning/staged-restart.js +138 -0
  102. package/dist/heart/versioning/update-checker.js +111 -0
  103. package/dist/heart/versioning/update-hooks.js +138 -0
  104. package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +41 -3
  105. package/dist/mind/associative-recall.js +122 -32
  106. package/dist/mind/bundle-manifest.js +19 -1
  107. package/dist/mind/context.js +125 -14
  108. package/dist/mind/{memory.js → diary.js} +69 -56
  109. package/dist/mind/file-state.js +179 -0
  110. package/dist/mind/first-impressions.js +16 -2
  111. package/dist/mind/friends/channel.js +56 -0
  112. package/dist/mind/friends/group-context.js +144 -0
  113. package/dist/mind/friends/store-file.js +36 -0
  114. package/dist/mind/friends/trust-explanation.js +74 -0
  115. package/dist/mind/friends/types.js +8 -0
  116. package/dist/mind/journal-index.js +161 -0
  117. package/dist/mind/obligation-steering.js +221 -0
  118. package/dist/mind/pending.js +76 -9
  119. package/dist/mind/phrases.js +1 -0
  120. package/dist/mind/prompt.js +795 -108
  121. package/dist/mind/scrutiny.js +173 -0
  122. package/dist/mind/token-estimate.js +8 -12
  123. package/dist/nerves/coverage/audit.js +1 -1
  124. package/dist/nerves/coverage/file-completeness.js +42 -5
  125. package/dist/nerves/coverage/run-artifacts.js +1 -1
  126. package/dist/nerves/event-buffer.js +111 -0
  127. package/dist/nerves/index.js +63 -3
  128. package/dist/nerves/observation.js +20 -0
  129. package/dist/nerves/runtime.js +5 -1
  130. package/dist/outlook-ui/assets/index-Ck8agNeO.js +61 -0
  131. package/dist/outlook-ui/assets/index-LwChZTgL.css +1 -0
  132. package/dist/outlook-ui/index.html +15 -0
  133. package/dist/repertoire/ado-semantic.js +11 -5
  134. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  135. package/dist/repertoire/coding/context-pack.js +330 -0
  136. package/dist/repertoire/coding/feedback.js +197 -30
  137. package/dist/repertoire/coding/manager.js +149 -2
  138. package/dist/repertoire/coding/spawner.js +55 -9
  139. package/dist/repertoire/coding/tools.js +170 -7
  140. package/dist/repertoire/guardrails.js +291 -0
  141. package/dist/repertoire/mcp-client.js +254 -0
  142. package/dist/repertoire/mcp-manager.js +251 -0
  143. package/dist/repertoire/shell-sessions.js +133 -0
  144. package/dist/repertoire/skills.js +14 -23
  145. package/dist/repertoire/tasks/board.js +43 -5
  146. package/dist/repertoire/tasks/fix.js +182 -0
  147. package/dist/repertoire/tasks/index.js +28 -10
  148. package/dist/repertoire/tasks/lifecycle.js +2 -2
  149. package/dist/repertoire/tasks/parser.js +3 -2
  150. package/dist/repertoire/tasks/scanner.js +194 -37
  151. package/dist/repertoire/tasks/transitions.js +11 -28
  152. package/dist/repertoire/tools-base.js +31 -771
  153. package/dist/repertoire/tools-bluebubbles.js +94 -0
  154. package/dist/repertoire/tools-bridge.js +141 -0
  155. package/dist/repertoire/tools-config.js +266 -0
  156. package/dist/repertoire/tools-continuity.js +248 -0
  157. package/dist/repertoire/tools-files.js +342 -0
  158. package/dist/repertoire/tools-flow.js +62 -0
  159. package/dist/repertoire/tools-github.js +1 -6
  160. package/dist/repertoire/tools-memory.js +347 -0
  161. package/dist/repertoire/tools-session.js +682 -0
  162. package/dist/repertoire/tools-shell.js +120 -0
  163. package/dist/repertoire/tools-surface.js +228 -0
  164. package/dist/repertoire/tools-teams.js +12 -59
  165. package/dist/repertoire/tools.js +132 -124
  166. package/dist/scripts/claude-code-hook.js +41 -0
  167. package/dist/scripts/claude-code-stop-hook.js +47 -0
  168. package/dist/senses/attention-queue.js +97 -0
  169. package/dist/senses/bluebubbles-client.js +102 -3
  170. package/dist/senses/bluebubbles-entry.js +2 -0
  171. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  172. package/dist/senses/bluebubbles-media.js +118 -23
  173. package/dist/senses/bluebubbles-model.js +12 -4
  174. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  175. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  176. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  177. package/dist/senses/bluebubbles.js +1008 -102
  178. package/dist/senses/cli/adapter.js +274 -0
  179. package/dist/senses/cli/ink-app.js +156 -0
  180. package/dist/senses/cli/inline-diff.js +64 -0
  181. package/dist/senses/cli/message-list.js +51 -0
  182. package/dist/senses/cli/ouro-tui.js +358 -0
  183. package/dist/senses/cli/spinner-imperative.js +135 -0
  184. package/dist/senses/cli/spinner.js +101 -0
  185. package/dist/senses/cli/status-line.js +60 -0
  186. package/dist/senses/cli/streaming-markdown.js +526 -0
  187. package/dist/senses/cli/tool-display.js +83 -0
  188. package/dist/senses/cli/tool-render.js +85 -0
  189. package/dist/senses/cli/tui-store.js +198 -0
  190. package/dist/senses/cli/virtual-list.js +35 -0
  191. package/dist/senses/cli-entry.js +1 -1
  192. package/dist/senses/cli-layout.js +187 -0
  193. package/dist/senses/cli.js +508 -243
  194. package/dist/senses/commands.js +65 -1
  195. package/dist/senses/continuity.js +94 -0
  196. package/dist/senses/habit-turn-message.js +108 -0
  197. package/dist/senses/inner-dialog-worker.js +112 -19
  198. package/dist/senses/inner-dialog.js +591 -84
  199. package/dist/senses/pipeline.js +559 -0
  200. package/dist/senses/shared-turn.js +196 -0
  201. package/dist/senses/surface-tool.js +68 -0
  202. package/dist/senses/teams.js +667 -120
  203. package/dist/senses/trust-gate.js +112 -2
  204. package/package.json +16 -6
  205. package/skills/configure-dev-tools.md +81 -0
  206. package/subagents/README.md +4 -70
  207. package/dist/heart/daemon/subagent-installer.js +0 -134
  208. package/dist/senses/debug-activity.js +0 -107
  209. package/subagents/work-doer.md +0 -235
  210. package/subagents/work-merger.md +0 -618
  211. package/subagents/work-planner.md +0 -382
  212. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  213. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  214. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  215. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  216. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  217. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
  218. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  219. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  220. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  221. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  222. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  223. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +0 -0
  224. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
  225. /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
  226. /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
  227. /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
@@ -36,7 +36,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.trimMessages = trimMessages;
37
37
  exports.validateSessionMessages = validateSessionMessages;
38
38
  exports.repairSessionMessages = repairSessionMessages;
39
+ exports.migrateToolNames = migrateToolNames;
39
40
  exports.saveSession = saveSession;
41
+ exports.appendSyntheticAssistantMessage = appendSyntheticAssistantMessage;
40
42
  exports.loadSession = loadSession;
41
43
  exports.postTurn = postTurn;
42
44
  exports.deleteSession = deleteSession;
@@ -50,17 +52,17 @@ function buildTrimmableBlocks(messages) {
50
52
  let i = 0;
51
53
  while (i < messages.length) {
52
54
  const msg = messages[i];
53
- if (msg?.role === "system") {
55
+ if (msg.role === "system") {
54
56
  i++;
55
57
  continue;
56
58
  }
57
59
  // Tool coherence block: assistant message with tool_calls + immediately following tool results
58
- if (msg?.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
60
+ if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
59
61
  const indices = [i];
60
62
  i++;
61
63
  while (i < messages.length) {
62
64
  const next = messages[i];
63
- if (next?.role !== "tool")
65
+ if (next.role !== "tool")
64
66
  break;
65
67
  indices.push(i);
66
68
  i++;
@@ -78,13 +80,13 @@ function buildTrimmableBlocks(messages) {
78
80
  function getSystemMessageIndices(messages) {
79
81
  const indices = [];
80
82
  for (let i = 0; i < messages.length; i++) {
81
- if (messages[i]?.role === "system")
83
+ if (messages[i].role === "system")
82
84
  indices.push(i);
83
85
  }
84
86
  return indices;
85
87
  }
86
88
  function buildTrimmedMessages(messages, kept) {
87
- return messages.filter((m, idx) => m?.role === "system" || kept.has(idx));
89
+ return messages.filter((m, idx) => m.role === "system" || kept.has(idx));
88
90
  }
89
91
  function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
90
92
  const targetTokens = Math.floor(maxTokens * (1 - contextMargin / 100));
@@ -132,7 +134,7 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
132
134
  let remaining = actualTokenCount;
133
135
  const kept = new Set();
134
136
  for (let i = 0; i < messages.length; i++) {
135
- if (messages[i]?.role !== "system")
137
+ if (messages[i].role !== "system")
136
138
  kept.add(i);
137
139
  }
138
140
  // Drop oldest blocks until we fall under target.
@@ -146,7 +148,7 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
146
148
  let trimmed = buildTrimmedMessages(messages, kept);
147
149
  // If we're still above budget after dropping everything trimmable, preserve system only.
148
150
  if (remaining > targetTokens) {
149
- trimmed = messages.filter((m) => m?.role === "system");
151
+ trimmed = messages.filter((m) => m.role === "system");
150
152
  }
151
153
  const estimatedAfter = (0, token_estimate_1.estimateTokensForMessages)(trimmed);
152
154
  (0, runtime_1.emitNervesEvent)({
@@ -219,7 +221,7 @@ function repairSessionMessages(messages) {
219
221
  result.push(msg);
220
222
  }
221
223
  (0, runtime_1.emitNervesEvent)({
222
- level: "warn",
224
+ level: "info",
223
225
  event: "mind.session_invariant_repair",
224
226
  component: "mind",
225
227
  message: "repaired session invariant violations",
@@ -227,11 +229,79 @@ function repairSessionMessages(messages) {
227
229
  });
228
230
  return result;
229
231
  }
230
- function saveSession(filePath, messages, lastUsage) {
232
+ function stripOrphanedToolResults(messages) {
233
+ const validCallIds = new Set();
234
+ for (const msg of messages) {
235
+ if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls))
236
+ continue;
237
+ for (const toolCall of msg.tool_calls)
238
+ validCallIds.add(toolCall.id);
239
+ }
240
+ let removed = 0;
241
+ const repaired = messages.filter((msg) => {
242
+ if (msg.role !== "tool")
243
+ return true;
244
+ const keep = validCallIds.has(msg.tool_call_id);
245
+ if (!keep)
246
+ removed++;
247
+ return keep;
248
+ });
249
+ if (removed > 0) {
250
+ (0, runtime_1.emitNervesEvent)({
251
+ level: "info",
252
+ event: "mind.session_orphan_tool_result_repair",
253
+ component: "mind",
254
+ message: "removed orphaned tool results from session history",
255
+ meta: { removed },
256
+ });
257
+ }
258
+ return repaired;
259
+ }
260
+ // Tool renames that have shipped. Old names in session history confuse the
261
+ // model into calling tools that no longer exist. Applied on session load so
262
+ // the transcript uses the current vocabulary.
263
+ const TOOL_NAME_MIGRATIONS = {
264
+ final_answer: "settle",
265
+ no_response: "observe",
266
+ go_inward: "ponder",
267
+ descend: "ponder",
268
+ memory_save: "diary_write",
269
+ memory_search: "recall",
270
+ };
271
+ function migrateToolNames(messages) {
272
+ let migrated = 0;
273
+ const result = messages.map((msg) => {
274
+ if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls) || msg.tool_calls.length === 0)
275
+ return msg;
276
+ let changed = false;
277
+ const updatedCalls = msg.tool_calls.map((tc) => {
278
+ if (tc.type !== "function")
279
+ return tc;
280
+ const newName = TOOL_NAME_MIGRATIONS[tc.function.name];
281
+ if (!newName)
282
+ return tc;
283
+ changed = true;
284
+ migrated++;
285
+ return { ...tc, function: { ...tc.function, name: newName } };
286
+ });
287
+ return changed ? { ...msg, tool_calls: updatedCalls } : msg;
288
+ });
289
+ if (migrated > 0) {
290
+ (0, runtime_1.emitNervesEvent)({
291
+ level: "info",
292
+ event: "mind.session_tool_name_migration",
293
+ component: "mind",
294
+ message: "migrated deprecated tool names in session history",
295
+ meta: { migrated },
296
+ });
297
+ }
298
+ return result;
299
+ }
300
+ function saveSession(filePath, messages, lastUsage, state) {
231
301
  const violations = validateSessionMessages(messages);
232
302
  if (violations.length > 0) {
233
303
  (0, runtime_1.emitNervesEvent)({
234
- level: "warn",
304
+ level: "info",
235
305
  event: "mind.session_invariant_violation",
236
306
  component: "mind",
237
307
  message: "session invariant violated on save",
@@ -239,12 +309,41 @@ function saveSession(filePath, messages, lastUsage) {
239
309
  });
240
310
  messages = repairSessionMessages(messages);
241
311
  }
312
+ messages = stripOrphanedToolResults(messages);
242
313
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
243
314
  const envelope = { version: 1, messages };
244
315
  if (lastUsage)
245
316
  envelope.lastUsage = lastUsage;
317
+ if (state?.mustResolveBeforeHandoff === true || typeof state?.lastFriendActivityAt === "string") {
318
+ envelope.state = {
319
+ ...(state?.mustResolveBeforeHandoff === true ? { mustResolveBeforeHandoff: true } : {}),
320
+ ...(typeof state?.lastFriendActivityAt === "string" ? { lastFriendActivityAt: state.lastFriendActivityAt } : {}),
321
+ };
322
+ }
246
323
  fs.writeFileSync(filePath, JSON.stringify(envelope, null, 2));
247
324
  }
325
+ function appendSyntheticAssistantMessage(filePath, content) {
326
+ try {
327
+ if (!fs.existsSync(filePath))
328
+ return false;
329
+ const raw = fs.readFileSync(filePath, "utf-8");
330
+ const data = JSON.parse(raw);
331
+ if (data.version !== 1 || !Array.isArray(data.messages))
332
+ return false;
333
+ data.messages.push({ role: "assistant", content });
334
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
335
+ (0, runtime_1.emitNervesEvent)({
336
+ component: "mind",
337
+ event: "mind.session_synthetic_message_appended",
338
+ message: "appended synthetic assistant message to session",
339
+ meta: { path: filePath, contentLength: content.length },
340
+ });
341
+ return true;
342
+ }
343
+ catch {
344
+ return false;
345
+ }
346
+ }
248
347
  function loadSession(filePath) {
249
348
  try {
250
349
  const raw = fs.readFileSync(filePath, "utf-8");
@@ -255,7 +354,7 @@ function loadSession(filePath) {
255
354
  const violations = validateSessionMessages(messages);
256
355
  if (violations.length > 0) {
257
356
  (0, runtime_1.emitNervesEvent)({
258
- level: "warn",
357
+ level: "info",
259
358
  event: "mind.session_invariant_violation",
260
359
  component: "mind",
261
360
  message: "session invariant violated on load",
@@ -263,13 +362,25 @@ function loadSession(filePath) {
263
362
  });
264
363
  messages = repairSessionMessages(messages);
265
364
  }
266
- return { messages, lastUsage: data.lastUsage };
365
+ messages = stripOrphanedToolResults(messages);
366
+ messages = migrateToolNames(messages);
367
+ const rawState = data?.state && typeof data.state === "object" && data.state !== null
368
+ ? data.state
369
+ : undefined;
370
+ const state = rawState && (rawState.mustResolveBeforeHandoff === true
371
+ || typeof rawState.lastFriendActivityAt === "string")
372
+ ? {
373
+ ...(rawState.mustResolveBeforeHandoff === true ? { mustResolveBeforeHandoff: true } : {}),
374
+ ...(typeof rawState.lastFriendActivityAt === "string" ? { lastFriendActivityAt: rawState.lastFriendActivityAt } : {}),
375
+ }
376
+ : undefined;
377
+ return { messages, lastUsage: data.lastUsage, state };
267
378
  }
268
379
  catch {
269
380
  return null;
270
381
  }
271
382
  }
272
- function postTurn(messages, sessPath, usage, hooks) {
383
+ function postTurn(messages, sessPath, usage, hooks, state) {
273
384
  if (hooks?.beforeTrim) {
274
385
  try {
275
386
  hooks.beforeTrim([...messages]);
@@ -289,7 +400,7 @@ function postTurn(messages, sessPath, usage, hooks) {
289
400
  const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
290
401
  const trimmed = trimMessages(messages, maxTokens, contextMargin, usage?.input_tokens);
291
402
  messages.splice(0, messages.length, ...trimmed);
292
- saveSession(sessPath, messages, usage);
403
+ saveSession(sessPath, messages, usage, state);
293
404
  }
294
405
  function deleteSession(filePath) {
295
406
  try {
@@ -33,20 +33,23 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.__memoryTestUtils = void 0;
37
- exports.ensureMemoryStorePaths = ensureMemoryStorePaths;
38
- exports.appendFactsWithDedup = appendFactsWithDedup;
39
- exports.readMemoryFacts = readMemoryFacts;
40
- exports.saveMemoryFact = saveMemoryFact;
36
+ exports.ensureDiaryStorePaths = ensureDiaryStorePaths;
37
+ exports.appendEntriesWithDedup = appendEntriesWithDedup;
38
+ exports.resolveDiaryRoot = resolveDiaryRoot;
39
+ exports.readDiaryEntries = readDiaryEntries;
40
+ exports.saveDiaryEntry = saveDiaryEntry;
41
41
  exports.backfillEmbeddings = backfillEmbeddings;
42
- exports.searchMemoryFacts = searchMemoryFacts;
42
+ exports.searchDiaryEntries = searchDiaryEntries;
43
43
  const fs = __importStar(require("fs"));
44
44
  const path = __importStar(require("path"));
45
45
  const crypto_1 = require("crypto");
46
46
  const config_1 = require("../heart/config");
47
47
  const identity_1 = require("../heart/identity");
48
48
  const runtime_1 = require("../nerves/runtime");
49
+ const sync_1 = require("../heart/sync");
50
+ const associative_recall_1 = require("./associative-recall");
49
51
  const DEDUP_THRESHOLD = 0.6;
52
+ const SEMANTIC_DEDUP_THRESHOLD = 0.95;
50
53
  const ENTITY_TOKEN = /[a-z0-9]+/g;
51
54
  const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
52
55
  class OpenAIEmbeddingProvider {
@@ -78,7 +81,7 @@ class OpenAIEmbeddingProvider {
78
81
  return payload.data.map((entry) => entry.embedding);
79
82
  }
80
83
  }
81
- function ensureMemoryStorePaths(rootDir) {
84
+ function ensureDiaryStorePaths(rootDir) {
82
85
  const factsPath = path.join(rootDir, "facts.jsonl");
83
86
  const entitiesPath = path.join(rootDir, "entities.json");
84
87
  const dailyDir = path.join(rootDir, "daily");
@@ -90,8 +93,8 @@ function ensureMemoryStorePaths(rootDir) {
90
93
  fs.writeFileSync(entitiesPath, "{}\n", "utf8");
91
94
  (0, runtime_1.emitNervesEvent)({
92
95
  component: "mind",
93
- event: "mind.memory_paths_ready",
94
- message: "memory store paths ready",
96
+ event: "mind.diary_paths_ready",
97
+ message: "diary store paths ready",
95
98
  meta: { rootDir },
96
99
  });
97
100
  return { rootDir, factsPath, entitiesPath, dailyDir };
@@ -114,15 +117,22 @@ function overlapScore(left, right) {
114
117
  }
115
118
  return common / Math.min(leftWords.size, rightWords.size);
116
119
  }
117
- function readExistingFacts(factsPath) {
120
+ function readExistingEntries(factsPath) {
118
121
  if (!fs.existsSync(factsPath))
119
122
  return [];
120
123
  const raw = fs.readFileSync(factsPath, "utf8").trim();
121
124
  if (!raw)
122
125
  return [];
123
- return raw
124
- .split("\n")
125
- .map((line) => JSON.parse(line));
126
+ const facts = [];
127
+ for (const line of raw.split("\n")) {
128
+ try {
129
+ facts.push(JSON.parse(line));
130
+ }
131
+ catch {
132
+ // Skip corrupt lines (e.g. partial write from a crash).
133
+ }
134
+ }
135
+ return facts;
126
136
  }
127
137
  function readEntityIndex(entitiesPath) {
128
138
  if (!fs.existsSync(entitiesPath))
@@ -170,13 +180,24 @@ function appendDailyFact(dailyDir, fact) {
170
180
  const dayPath = path.join(dailyDir, `${day}.jsonl`);
171
181
  fs.appendFileSync(dayPath, `${JSON.stringify(fact)}\n`, "utf8");
172
182
  }
173
- function appendFactsWithDedup(stores, incoming) {
174
- const existing = readExistingFacts(stores.factsPath);
183
+ function appendEntriesWithDedup(stores, incoming, options) {
184
+ const existing = readExistingEntries(stores.factsPath);
175
185
  const all = [...existing];
176
186
  let added = 0;
177
187
  let skipped = 0;
188
+ const semanticThreshold = options?.semanticThreshold;
178
189
  for (const fact of incoming) {
179
- const duplicate = all.some((prior) => overlapScore(prior.text, fact.text) > DEDUP_THRESHOLD);
190
+ const duplicate = all.some((prior) => {
191
+ if (overlapScore(prior.text, fact.text) > DEDUP_THRESHOLD)
192
+ return true;
193
+ if (semanticThreshold !== undefined &&
194
+ Array.isArray(fact.embedding) && fact.embedding.length > 0 &&
195
+ Array.isArray(prior.embedding) && prior.embedding.length > 0 &&
196
+ fact.embedding.length === prior.embedding.length) {
197
+ return (0, associative_recall_1.cosineSimilarity)(fact.embedding, prior.embedding) > semanticThreshold;
198
+ }
199
+ return false;
200
+ });
180
201
  if (duplicate) {
181
202
  skipped++;
182
203
  continue;
@@ -184,35 +205,20 @@ function appendFactsWithDedup(stores, incoming) {
184
205
  all.push(fact);
185
206
  added++;
186
207
  fs.appendFileSync(stores.factsPath, `${JSON.stringify(fact)}\n`, "utf8");
208
+ (0, sync_1.trackSyncWrite)(stores.factsPath);
187
209
  updateEntityIndex(stores.entitiesPath, fact);
210
+ (0, sync_1.trackSyncWrite)(stores.entitiesPath);
188
211
  appendDailyFact(stores.dailyDir, fact);
212
+ (0, sync_1.trackSyncWrite)(path.join(stores.dailyDir, `${(fact.createdAt.slice(0, 10) || "unknown")}.jsonl`));
189
213
  }
190
214
  (0, runtime_1.emitNervesEvent)({
191
215
  component: "mind",
192
- event: "mind.memory_write",
193
- message: "memory write completed",
216
+ event: "mind.diary_write",
217
+ message: "diary write completed",
194
218
  meta: { added, skipped },
195
219
  });
196
220
  return { added, skipped };
197
221
  }
198
- function cosineSimilarity(left, right) {
199
- if (left.length === 0 || right.length === 0 || left.length !== right.length)
200
- return 0;
201
- let dot = 0;
202
- let leftNorm = 0;
203
- let rightNorm = 0;
204
- for (let i = 0; i < left.length; i += 1) {
205
- dot += left[i] * right[i];
206
- leftNorm += left[i] * left[i];
207
- rightNorm += right[i] * right[i];
208
- }
209
- if (leftNorm === 0 || rightNorm === 0)
210
- return 0;
211
- return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
212
- }
213
- exports.__memoryTestUtils = {
214
- cosineSimilarity,
215
- };
216
222
  function createDefaultEmbeddingProvider() {
217
223
  const apiKey = (0, config_1.getOpenAIEmbeddingsApiKey)().trim();
218
224
  if (!apiKey)
@@ -225,8 +231,8 @@ async function buildEmbedding(text, embeddingProvider) {
225
231
  (0, runtime_1.emitNervesEvent)({
226
232
  level: "warn",
227
233
  component: "mind",
228
- event: "mind.memory_embedding_unavailable",
229
- message: "embedding provider unavailable for memory write",
234
+ event: "mind.diary_embedding_unavailable",
235
+ message: "embedding provider unavailable for diary write",
230
236
  meta: { reason: "missing_openai_embeddings_key" },
231
237
  });
232
238
  return [];
@@ -239,8 +245,8 @@ async function buildEmbedding(text, embeddingProvider) {
239
245
  (0, runtime_1.emitNervesEvent)({
240
246
  level: "warn",
241
247
  component: "mind",
242
- event: "mind.memory_embedding_unavailable",
243
- message: "embedding provider unavailable for memory write",
248
+ event: "mind.diary_embedding_unavailable",
249
+ message: "embedding provider unavailable for diary write",
244
250
  meta: {
245
251
  reason: error instanceof Error ? error.message : String(error),
246
252
  },
@@ -248,13 +254,19 @@ async function buildEmbedding(text, embeddingProvider) {
248
254
  return [];
249
255
  }
250
256
  }
251
- function readMemoryFacts(memoryRoot = path.join((0, identity_1.getAgentRoot)(), "psyche", "memory")) {
252
- return readExistingFacts(path.join(memoryRoot, "facts.jsonl"));
257
+ function resolveDiaryRoot(explicitRoot) {
258
+ if (explicitRoot)
259
+ return explicitRoot;
260
+ const agentRoot = (0, identity_1.getAgentRoot)();
261
+ return path.join(agentRoot, "diary");
262
+ }
263
+ function readDiaryEntries(diaryRoot) {
264
+ return readExistingEntries(path.join(resolveDiaryRoot(diaryRoot), "facts.jsonl"));
253
265
  }
254
- async function saveMemoryFact(options) {
266
+ async function saveDiaryEntry(options) {
255
267
  const text = options.text.trim();
256
- const memoryRoot = options.memoryRoot ?? path.join((0, identity_1.getAgentRoot)(), "psyche", "memory");
257
- const stores = ensureMemoryStorePaths(memoryRoot);
268
+ const diaryRoot = resolveDiaryRoot(options.diaryRoot);
269
+ const stores = ensureDiaryStorePaths(diaryRoot);
258
270
  const embedding = await buildEmbedding(text, options.embeddingProvider);
259
271
  const fact = {
260
272
  id: options.idFactory ? options.idFactory() : (0, crypto_1.randomUUID)(),
@@ -264,14 +276,14 @@ async function saveMemoryFact(options) {
264
276
  createdAt: (options.now ?? (() => new Date()))().toISOString(),
265
277
  embedding,
266
278
  };
267
- return appendFactsWithDedup(stores, [fact]);
279
+ return appendEntriesWithDedup(stores, [fact], { semanticThreshold: SEMANTIC_DEDUP_THRESHOLD });
268
280
  }
269
281
  async function backfillEmbeddings(options) {
270
- const memoryRoot = options?.memoryRoot ?? path.join((0, identity_1.getAgentRoot)(), "psyche", "memory");
271
- const factsPath = path.join(memoryRoot, "facts.jsonl");
282
+ const diaryRoot = resolveDiaryRoot(options?.diaryRoot);
283
+ const factsPath = path.join(diaryRoot, "facts.jsonl");
272
284
  if (!fs.existsSync(factsPath))
273
285
  return { total: 0, backfilled: 0, failed: 0 };
274
- const facts = readExistingFacts(factsPath);
286
+ const facts = readExistingEntries(factsPath);
275
287
  const needsEmbedding = facts.filter((f) => !Array.isArray(f.embedding) || f.embedding.length === 0);
276
288
  if (needsEmbedding.length === 0)
277
289
  return { total: facts.length, backfilled: 0, failed: 0 };
@@ -280,7 +292,7 @@ async function backfillEmbeddings(options) {
280
292
  (0, runtime_1.emitNervesEvent)({
281
293
  level: "warn",
282
294
  component: "mind",
283
- event: "mind.memory_backfill_skipped",
295
+ event: "mind.diary_backfill_skipped",
284
296
  message: "embedding provider unavailable for backfill",
285
297
  meta: { needsEmbedding: needsEmbedding.length },
286
298
  });
@@ -306,7 +318,7 @@ async function backfillEmbeddings(options) {
306
318
  (0, runtime_1.emitNervesEvent)({
307
319
  level: "warn",
308
320
  component: "mind",
309
- event: "mind.memory_backfill_batch_error",
321
+ event: "mind.diary_backfill_batch_error",
310
322
  message: "embedding backfill batch failed",
311
323
  meta: {
312
324
  batchStart: i,
@@ -319,9 +331,10 @@ async function backfillEmbeddings(options) {
319
331
  // Rewrite facts file with updated embeddings
320
332
  const lines = facts.map((f) => JSON.stringify(f)).join("\n") + "\n";
321
333
  fs.writeFileSync(factsPath, lines, "utf8");
334
+ (0, sync_1.trackSyncWrite)(factsPath);
322
335
  (0, runtime_1.emitNervesEvent)({
323
336
  component: "mind",
324
- event: "mind.memory_backfill_complete",
337
+ event: "mind.diary_backfill_complete",
325
338
  message: "embedding backfill completed",
326
339
  meta: { total: facts.length, backfilled, failed },
327
340
  });
@@ -341,7 +354,7 @@ function uniqueFacts(facts) {
341
354
  }
342
355
  return unique;
343
356
  }
344
- async function searchMemoryFacts(query, facts, embeddingProvider) {
357
+ async function searchDiaryEntries(query, facts, embeddingProvider) {
345
358
  const trimmed = query.trim();
346
359
  if (!trimmed)
347
360
  return [];
@@ -365,7 +378,7 @@ async function searchMemoryFacts(query, facts, embeddingProvider) {
365
378
  .filter((fact) => fact.embedding.length === queryEmbedding.length)
366
379
  .map((fact) => ({
367
380
  fact,
368
- score: cosineSimilarity(queryEmbedding, fact.embedding),
381
+ score: (0, associative_recall_1.cosineSimilarity)(queryEmbedding, fact.embedding),
369
382
  }))
370
383
  .filter((entry) => entry.score > 0)
371
384
  .sort((left, right) => right.score - left.score)
@@ -377,8 +390,8 @@ async function searchMemoryFacts(query, facts, embeddingProvider) {
377
390
  (0, runtime_1.emitNervesEvent)({
378
391
  level: "warn",
379
392
  component: "mind",
380
- event: "mind.memory_embedding_unavailable",
381
- message: "embedding provider unavailable for memory search",
393
+ event: "mind.diary_embedding_unavailable",
394
+ message: "embedding provider unavailable for diary search",
382
395
  meta: {
383
396
  reason: error instanceof Error ? error.message : String(error),
384
397
  },
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fileStateCache = exports.FileStateCache = void 0;
4
+ exports.contentHash = contentHash;
5
+ const crypto_1 = require("crypto");
6
+ const fs_1 = require("fs");
7
+ const runtime_1 = require("../nerves/runtime");
8
+ /** Compute sha256 hex hash of content */
9
+ function contentHash(content) {
10
+ return (0, crypto_1.createHash)("sha256").update(content).digest("hex");
11
+ }
12
+ /**
13
+ * Session-scoped LRU cache tracking file reads.
14
+ * Stores content hashes (not full content) to limit memory.
15
+ * Keyed by absolute file path.
16
+ *
17
+ * Also maintains a separate snapshot list for future rewind support.
18
+ * Snapshots are indexed by content hash and linked to conversation messages.
19
+ */
20
+ class FileStateCache {
21
+ entries;
22
+ maxSize;
23
+ snapshots = [];
24
+ maxSnapshots = 100;
25
+ constructor(maxSize = 50) {
26
+ this.entries = new Map();
27
+ this.maxSize = maxSize;
28
+ }
29
+ /**
30
+ * Record a file read. Computes content hash and stores metadata.
31
+ */
32
+ record(filePath, content, mtime, offset, limit, messageId) {
33
+ // If key already exists, delete it so re-insertion moves it to end (most recent)
34
+ if (this.entries.has(filePath)) {
35
+ this.entries.delete(filePath);
36
+ }
37
+ const hash = contentHash(content);
38
+ const fullRead = offset === undefined && limit === undefined;
39
+ this.entries.set(filePath, {
40
+ hash,
41
+ mtime,
42
+ offset: fullRead ? undefined : offset,
43
+ limit: fullRead ? undefined : limit,
44
+ fullRead,
45
+ recordedAt: Date.now(),
46
+ messageId,
47
+ });
48
+ (0, runtime_1.emitNervesEvent)({
49
+ component: "mind",
50
+ event: "mind.file_state.record",
51
+ message: "recorded file state",
52
+ meta: { path: filePath, hash, fullRead },
53
+ });
54
+ // Evict LRU (first entry in Map iteration order) if over capacity
55
+ if (this.entries.size > this.maxSize) {
56
+ const firstKey = this.entries.keys().next().value;
57
+ this.entries.delete(firstKey);
58
+ }
59
+ }
60
+ /**
61
+ * Get the cached state for a file path. Also promotes it in LRU order.
62
+ */
63
+ get(filePath) {
64
+ const entry = this.entries.get(filePath);
65
+ if (entry === undefined)
66
+ return undefined;
67
+ // Promote to most-recently-used by re-inserting
68
+ this.entries.delete(filePath);
69
+ this.entries.set(filePath, entry);
70
+ (0, runtime_1.emitNervesEvent)({
71
+ component: "mind",
72
+ event: "mind.file_state.get",
73
+ message: "retrieved file state",
74
+ meta: { path: filePath, hash: entry.hash },
75
+ });
76
+ return entry;
77
+ }
78
+ /**
79
+ * Check if a file has been modified since the last recorded read.
80
+ * Uses mtime as primary signal, content hash as fallback for cloud sync / touch scenarios.
81
+ * Returns { stale: false } if the path is not in cache or the file cannot be stat'd.
82
+ */
83
+ isStale(filePath) {
84
+ const entry = this.entries.get(filePath);
85
+ if (entry === undefined)
86
+ return { stale: false };
87
+ let currentMtime;
88
+ try {
89
+ currentMtime = (0, fs_1.statSync)(filePath).mtimeMs;
90
+ }
91
+ catch {
92
+ // File doesn't exist or can't be stat'd -- no basis for staleness
93
+ return { stale: false };
94
+ }
95
+ // Fast path: mtime unchanged means not stale
96
+ if (currentMtime === entry.mtime)
97
+ return { stale: false };
98
+ // mtime differs -- check content hash as fallback (handles touch / cloud sync)
99
+ try {
100
+ const currentContent = (0, fs_1.readFileSync)(filePath, "utf-8");
101
+ const currentHash = contentHash(currentContent);
102
+ if (currentHash === entry.hash)
103
+ return { stale: false };
104
+ (0, runtime_1.emitNervesEvent)({
105
+ component: "mind",
106
+ event: "mind.file_state.stale_detected",
107
+ message: "file staleness detected",
108
+ meta: { path: filePath, previousHash: entry.hash, currentHash },
109
+ });
110
+ return { stale: true, reason: `file modified since last read (mtime and content differ)` };
111
+ }
112
+ catch {
113
+ // Can't read file -- treat as not stale (file may have been deleted)
114
+ return { stale: false };
115
+ }
116
+ }
117
+ /**
118
+ * Create a pre-edit snapshot of the current cache state for a file.
119
+ * Snapshots are stored separately from the LRU cache for future rewind support.
120
+ * Returns undefined if the path is not in cache.
121
+ */
122
+ snapshot(filePath) {
123
+ const entry = this.entries.get(filePath);
124
+ if (entry === undefined)
125
+ return undefined;
126
+ const snap = {
127
+ filePath,
128
+ hash: entry.hash,
129
+ mtime: entry.mtime,
130
+ messageId: entry.messageId,
131
+ createdAt: Date.now(),
132
+ };
133
+ this.snapshots.push(snap);
134
+ (0, runtime_1.emitNervesEvent)({
135
+ component: "mind",
136
+ event: "mind.file_state.snapshot_created",
137
+ message: "created file state snapshot",
138
+ meta: { path: filePath, hash: snap.hash },
139
+ });
140
+ // Evict oldest snapshots if over capacity
141
+ if (this.snapshots.length > this.maxSnapshots) {
142
+ this.snapshots = this.snapshots.slice(this.snapshots.length - this.maxSnapshots);
143
+ }
144
+ return snap;
145
+ }
146
+ /**
147
+ * Get all snapshots in creation order.
148
+ */
149
+ getSnapshots() {
150
+ return this.snapshots;
151
+ }
152
+ /**
153
+ * Look up a snapshot by content hash. Returns the first match.
154
+ */
155
+ lookupSnapshotByHash(hash) {
156
+ return this.snapshots.find(s => s.hash === hash);
157
+ }
158
+ /**
159
+ * Clear all snapshots.
160
+ */
161
+ clearSnapshots() {
162
+ this.snapshots = [];
163
+ }
164
+ /**
165
+ * Clear all cached entries (does not clear snapshots).
166
+ */
167
+ clear() {
168
+ (0, runtime_1.emitNervesEvent)({
169
+ component: "mind",
170
+ event: "mind.file_state.clear",
171
+ message: "cleared file state cache",
172
+ meta: { entryCount: this.entries.size },
173
+ });
174
+ this.entries.clear();
175
+ }
176
+ }
177
+ exports.FileStateCache = FileStateCache;
178
+ /** Session-scoped singleton instance used by tool handlers */
179
+ exports.fileStateCache = new FileStateCache();