@ouro.bot/cli 0.1.0-alpha.14 → 0.1.0-alpha.141

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 (138) hide show
  1. package/AdoptionSpecialist.ouro/psyche/SOUL.md +2 -2
  2. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  3. package/README.md +151 -205
  4. package/changelog.json +913 -0
  5. package/dist/heart/active-work.js +633 -0
  6. package/dist/heart/bridges/manager.js +358 -0
  7. package/dist/heart/bridges/state-machine.js +135 -0
  8. package/dist/heart/bridges/store.js +123 -0
  9. package/dist/heart/commitments.js +105 -0
  10. package/dist/heart/config.js +66 -21
  11. package/dist/heart/core.js +645 -112
  12. package/dist/heart/cross-chat-delivery.js +146 -0
  13. package/dist/heart/daemon/agent-discovery.js +81 -0
  14. package/dist/heart/daemon/agent-service.js +467 -0
  15. package/dist/heart/daemon/auth-flow.js +457 -0
  16. package/dist/heart/daemon/daemon-cli.js +1840 -196
  17. package/dist/heart/daemon/daemon-entry.js +80 -3
  18. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  19. package/dist/heart/daemon/daemon.js +301 -1
  20. package/dist/heart/daemon/hatch-animation.js +10 -3
  21. package/dist/heart/daemon/hatch-flow.js +15 -80
  22. package/dist/heart/daemon/heartbeat-timer.js +131 -0
  23. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  24. package/dist/heart/daemon/launchd.js +159 -0
  25. package/dist/heart/daemon/log-tailer.js +4 -3
  26. package/dist/heart/daemon/mcp-server.js +464 -0
  27. package/dist/heart/daemon/message-router.js +17 -8
  28. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  29. package/dist/heart/daemon/ouro-path-installer.js +67 -34
  30. package/dist/heart/daemon/ouro-version-manager.js +171 -0
  31. package/dist/heart/daemon/process-manager.js +13 -0
  32. package/dist/heart/daemon/run-hooks.js +37 -0
  33. package/dist/heart/daemon/runtime-logging.js +58 -15
  34. package/dist/heart/daemon/runtime-metadata.js +219 -0
  35. package/dist/heart/daemon/runtime-mode.js +67 -0
  36. package/dist/heart/daemon/sense-manager.js +50 -2
  37. package/dist/heart/daemon/skill-management-installer.js +94 -0
  38. package/dist/heart/daemon/socket-client.js +202 -0
  39. package/dist/heart/daemon/specialist-orchestrator.js +2 -2
  40. package/dist/heart/daemon/specialist-prompt.js +10 -7
  41. package/dist/heart/daemon/specialist-tools.js +52 -3
  42. package/dist/heart/daemon/staged-restart.js +114 -0
  43. package/dist/heart/daemon/thoughts.js +507 -0
  44. package/dist/heart/daemon/update-checker.js +111 -0
  45. package/dist/heart/daemon/update-hooks.js +138 -0
  46. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  47. package/dist/heart/delegation.js +62 -0
  48. package/dist/heart/identity.js +64 -21
  49. package/dist/heart/kicks.js +2 -20
  50. package/dist/heart/model-capabilities.js +48 -0
  51. package/dist/heart/obligations.js +197 -0
  52. package/dist/heart/progress-story.js +42 -0
  53. package/dist/heart/provider-failover.js +88 -0
  54. package/dist/heart/provider-ping.js +159 -0
  55. package/dist/heart/providers/anthropic-token.js +163 -0
  56. package/dist/heart/providers/anthropic.js +195 -34
  57. package/dist/heart/providers/azure.js +115 -9
  58. package/dist/heart/providers/github-copilot.js +157 -0
  59. package/dist/heart/providers/minimax.js +33 -3
  60. package/dist/heart/providers/openai-codex.js +49 -14
  61. package/dist/heart/session-activity.js +173 -0
  62. package/dist/heart/session-recall.js +216 -0
  63. package/dist/heart/streaming.js +117 -33
  64. package/dist/heart/target-resolution.js +123 -0
  65. package/dist/heart/tool-loop.js +200 -0
  66. package/dist/heart/turn-coordinator.js +28 -0
  67. package/dist/mind/associative-recall.js +122 -32
  68. package/dist/mind/bundle-manifest.js +14 -0
  69. package/dist/mind/context.js +125 -14
  70. package/dist/mind/{memory.js → diary.js} +70 -56
  71. package/dist/mind/first-impressions.js +16 -2
  72. package/dist/mind/friends/channel.js +35 -0
  73. package/dist/mind/friends/group-context.js +144 -0
  74. package/dist/mind/friends/store-file.js +19 -0
  75. package/dist/mind/friends/trust-explanation.js +74 -0
  76. package/dist/mind/friends/types.js +8 -0
  77. package/dist/mind/journal-index.js +161 -0
  78. package/dist/mind/obligation-steering.js +221 -0
  79. package/dist/mind/obligations.js +134 -0
  80. package/dist/mind/pending.js +76 -9
  81. package/dist/mind/phrases.js +1 -0
  82. package/dist/mind/prompt.js +554 -88
  83. package/dist/mind/token-estimate.js +8 -12
  84. package/dist/nerves/cli-logging.js +15 -2
  85. package/dist/nerves/coverage/run-artifacts.js +1 -1
  86. package/dist/nerves/index.js +12 -0
  87. package/dist/nerves/runtime.js +5 -1
  88. package/dist/repertoire/ado-client.js +4 -2
  89. package/dist/repertoire/ado-semantic.js +6 -0
  90. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  91. package/dist/repertoire/coding/context-pack.js +254 -0
  92. package/dist/repertoire/coding/feedback.js +301 -0
  93. package/dist/repertoire/coding/index.js +4 -1
  94. package/dist/repertoire/coding/manager.js +210 -4
  95. package/dist/repertoire/coding/spawner.js +59 -12
  96. package/dist/repertoire/coding/tools.js +176 -4
  97. package/dist/repertoire/data/ado-endpoints.json +188 -0
  98. package/dist/repertoire/guardrails.js +290 -0
  99. package/dist/repertoire/mcp-client.js +254 -0
  100. package/dist/repertoire/mcp-manager.js +198 -0
  101. package/dist/repertoire/skills.js +14 -23
  102. package/dist/repertoire/tasks/board.js +12 -0
  103. package/dist/repertoire/tasks/index.js +23 -9
  104. package/dist/repertoire/tasks/transitions.js +1 -2
  105. package/dist/repertoire/tools-base.js +980 -271
  106. package/dist/repertoire/tools-bluebubbles.js +94 -0
  107. package/dist/repertoire/tools-github.js +1 -6
  108. package/dist/repertoire/tools-teams.js +65 -59
  109. package/dist/repertoire/tools.js +266 -91
  110. package/dist/senses/attention-queue.js +97 -0
  111. package/dist/senses/bluebubbles-client.js +210 -5
  112. package/dist/senses/bluebubbles-entry.js +2 -0
  113. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  114. package/dist/senses/bluebubbles-media.js +339 -0
  115. package/dist/senses/bluebubbles-model.js +12 -4
  116. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  117. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  118. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  119. package/dist/senses/bluebubbles.js +944 -45
  120. package/dist/senses/cli-entry.js +1 -1
  121. package/dist/senses/cli-layout.js +187 -0
  122. package/dist/senses/cli.js +374 -131
  123. package/dist/senses/contextual-heartbeat.js +102 -0
  124. package/dist/senses/continuity.js +94 -0
  125. package/dist/senses/debug-activity.js +154 -0
  126. package/dist/senses/inner-dialog-worker.js +47 -18
  127. package/dist/senses/inner-dialog.js +500 -84
  128. package/dist/senses/pipeline.js +454 -0
  129. package/dist/senses/surface-tool.js +82 -0
  130. package/dist/senses/teams.js +607 -129
  131. package/dist/senses/trust-gate.js +112 -2
  132. package/package.json +12 -4
  133. package/skills/configure-dev-tools.md +109 -0
  134. package/subagents/README.md +4 -70
  135. package/dist/heart/daemon/subagent-installer.js +0 -134
  136. package/subagents/work-doer.md +0 -233
  137. package/subagents/work-merger.md +0 -624
  138. package/subagents/work-planner.md +0 -373
@@ -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,22 @@ 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 associative_recall_1 = require("./associative-recall");
49
50
  const DEDUP_THRESHOLD = 0.6;
51
+ const SEMANTIC_DEDUP_THRESHOLD = 0.95;
50
52
  const ENTITY_TOKEN = /[a-z0-9]+/g;
51
53
  const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
52
54
  class OpenAIEmbeddingProvider {
@@ -78,7 +80,7 @@ class OpenAIEmbeddingProvider {
78
80
  return payload.data.map((entry) => entry.embedding);
79
81
  }
80
82
  }
81
- function ensureMemoryStorePaths(rootDir) {
83
+ function ensureDiaryStorePaths(rootDir) {
82
84
  const factsPath = path.join(rootDir, "facts.jsonl");
83
85
  const entitiesPath = path.join(rootDir, "entities.json");
84
86
  const dailyDir = path.join(rootDir, "daily");
@@ -90,8 +92,8 @@ function ensureMemoryStorePaths(rootDir) {
90
92
  fs.writeFileSync(entitiesPath, "{}\n", "utf8");
91
93
  (0, runtime_1.emitNervesEvent)({
92
94
  component: "mind",
93
- event: "mind.memory_paths_ready",
94
- message: "memory store paths ready",
95
+ event: "mind.diary_paths_ready",
96
+ message: "diary store paths ready",
95
97
  meta: { rootDir },
96
98
  });
97
99
  return { rootDir, factsPath, entitiesPath, dailyDir };
@@ -114,15 +116,22 @@ function overlapScore(left, right) {
114
116
  }
115
117
  return common / Math.min(leftWords.size, rightWords.size);
116
118
  }
117
- function readExistingFacts(factsPath) {
119
+ function readExistingEntries(factsPath) {
118
120
  if (!fs.existsSync(factsPath))
119
121
  return [];
120
122
  const raw = fs.readFileSync(factsPath, "utf8").trim();
121
123
  if (!raw)
122
124
  return [];
123
- return raw
124
- .split("\n")
125
- .map((line) => JSON.parse(line));
125
+ const facts = [];
126
+ for (const line of raw.split("\n")) {
127
+ try {
128
+ facts.push(JSON.parse(line));
129
+ }
130
+ catch {
131
+ // Skip corrupt lines (e.g. partial write from a crash).
132
+ }
133
+ }
134
+ return facts;
126
135
  }
127
136
  function readEntityIndex(entitiesPath) {
128
137
  if (!fs.existsSync(entitiesPath))
@@ -170,13 +179,24 @@ function appendDailyFact(dailyDir, fact) {
170
179
  const dayPath = path.join(dailyDir, `${day}.jsonl`);
171
180
  fs.appendFileSync(dayPath, `${JSON.stringify(fact)}\n`, "utf8");
172
181
  }
173
- function appendFactsWithDedup(stores, incoming) {
174
- const existing = readExistingFacts(stores.factsPath);
182
+ function appendEntriesWithDedup(stores, incoming, options) {
183
+ const existing = readExistingEntries(stores.factsPath);
175
184
  const all = [...existing];
176
185
  let added = 0;
177
186
  let skipped = 0;
187
+ const semanticThreshold = options?.semanticThreshold;
178
188
  for (const fact of incoming) {
179
- const duplicate = all.some((prior) => overlapScore(prior.text, fact.text) > DEDUP_THRESHOLD);
189
+ const duplicate = all.some((prior) => {
190
+ if (overlapScore(prior.text, fact.text) > DEDUP_THRESHOLD)
191
+ return true;
192
+ if (semanticThreshold !== undefined &&
193
+ Array.isArray(fact.embedding) && fact.embedding.length > 0 &&
194
+ Array.isArray(prior.embedding) && prior.embedding.length > 0 &&
195
+ fact.embedding.length === prior.embedding.length) {
196
+ return (0, associative_recall_1.cosineSimilarity)(fact.embedding, prior.embedding) > semanticThreshold;
197
+ }
198
+ return false;
199
+ });
180
200
  if (duplicate) {
181
201
  skipped++;
182
202
  continue;
@@ -189,30 +209,12 @@ function appendFactsWithDedup(stores, incoming) {
189
209
  }
190
210
  (0, runtime_1.emitNervesEvent)({
191
211
  component: "mind",
192
- event: "mind.memory_write",
193
- message: "memory write completed",
212
+ event: "mind.diary_write",
213
+ message: "diary write completed",
194
214
  meta: { added, skipped },
195
215
  });
196
216
  return { added, skipped };
197
217
  }
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
218
  function createDefaultEmbeddingProvider() {
217
219
  const apiKey = (0, config_1.getOpenAIEmbeddingsApiKey)().trim();
218
220
  if (!apiKey)
@@ -225,8 +227,8 @@ async function buildEmbedding(text, embeddingProvider) {
225
227
  (0, runtime_1.emitNervesEvent)({
226
228
  level: "warn",
227
229
  component: "mind",
228
- event: "mind.memory_embedding_unavailable",
229
- message: "embedding provider unavailable for memory write",
230
+ event: "mind.diary_embedding_unavailable",
231
+ message: "embedding provider unavailable for diary write",
230
232
  meta: { reason: "missing_openai_embeddings_key" },
231
233
  });
232
234
  return [];
@@ -239,8 +241,8 @@ async function buildEmbedding(text, embeddingProvider) {
239
241
  (0, runtime_1.emitNervesEvent)({
240
242
  level: "warn",
241
243
  component: "mind",
242
- event: "mind.memory_embedding_unavailable",
243
- message: "embedding provider unavailable for memory write",
244
+ event: "mind.diary_embedding_unavailable",
245
+ message: "embedding provider unavailable for diary write",
244
246
  meta: {
245
247
  reason: error instanceof Error ? error.message : String(error),
246
248
  },
@@ -248,13 +250,25 @@ async function buildEmbedding(text, embeddingProvider) {
248
250
  return [];
249
251
  }
250
252
  }
251
- function readMemoryFacts(memoryRoot = path.join((0, identity_1.getAgentRoot)(), "psyche", "memory")) {
252
- 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)
264
+ }
265
+ function readDiaryEntries(diaryRoot) {
266
+ return readExistingEntries(path.join(resolveDiaryRoot(diaryRoot), "facts.jsonl"));
253
267
  }
254
- async function saveMemoryFact(options) {
268
+ async function saveDiaryEntry(options) {
255
269
  const text = options.text.trim();
256
- const memoryRoot = options.memoryRoot ?? path.join((0, identity_1.getAgentRoot)(), "psyche", "memory");
257
- const stores = ensureMemoryStorePaths(memoryRoot);
270
+ const diaryRoot = resolveDiaryRoot(options.diaryRoot);
271
+ const stores = ensureDiaryStorePaths(diaryRoot);
258
272
  const embedding = await buildEmbedding(text, options.embeddingProvider);
259
273
  const fact = {
260
274
  id: options.idFactory ? options.idFactory() : (0, crypto_1.randomUUID)(),
@@ -264,14 +278,14 @@ async function saveMemoryFact(options) {
264
278
  createdAt: (options.now ?? (() => new Date()))().toISOString(),
265
279
  embedding,
266
280
  };
267
- return appendFactsWithDedup(stores, [fact]);
281
+ return appendEntriesWithDedup(stores, [fact], { semanticThreshold: SEMANTIC_DEDUP_THRESHOLD });
268
282
  }
269
283
  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");
284
+ const diaryRoot = resolveDiaryRoot(options?.diaryRoot);
285
+ const factsPath = path.join(diaryRoot, "facts.jsonl");
272
286
  if (!fs.existsSync(factsPath))
273
287
  return { total: 0, backfilled: 0, failed: 0 };
274
- const facts = readExistingFacts(factsPath);
288
+ const facts = readExistingEntries(factsPath);
275
289
  const needsEmbedding = facts.filter((f) => !Array.isArray(f.embedding) || f.embedding.length === 0);
276
290
  if (needsEmbedding.length === 0)
277
291
  return { total: facts.length, backfilled: 0, failed: 0 };
@@ -280,7 +294,7 @@ async function backfillEmbeddings(options) {
280
294
  (0, runtime_1.emitNervesEvent)({
281
295
  level: "warn",
282
296
  component: "mind",
283
- event: "mind.memory_backfill_skipped",
297
+ event: "mind.diary_backfill_skipped",
284
298
  message: "embedding provider unavailable for backfill",
285
299
  meta: { needsEmbedding: needsEmbedding.length },
286
300
  });
@@ -306,7 +320,7 @@ async function backfillEmbeddings(options) {
306
320
  (0, runtime_1.emitNervesEvent)({
307
321
  level: "warn",
308
322
  component: "mind",
309
- event: "mind.memory_backfill_batch_error",
323
+ event: "mind.diary_backfill_batch_error",
310
324
  message: "embedding backfill batch failed",
311
325
  meta: {
312
326
  batchStart: i,
@@ -321,7 +335,7 @@ async function backfillEmbeddings(options) {
321
335
  fs.writeFileSync(factsPath, lines, "utf8");
322
336
  (0, runtime_1.emitNervesEvent)({
323
337
  component: "mind",
324
- event: "mind.memory_backfill_complete",
338
+ event: "mind.diary_backfill_complete",
325
339
  message: "embedding backfill completed",
326
340
  meta: { total: facts.length, backfilled, failed },
327
341
  });
@@ -341,7 +355,7 @@ function uniqueFacts(facts) {
341
355
  }
342
356
  return unique;
343
357
  }
344
- async function searchMemoryFacts(query, facts, embeddingProvider) {
358
+ async function searchDiaryEntries(query, facts, embeddingProvider) {
345
359
  const trimmed = query.trim();
346
360
  if (!trimmed)
347
361
  return [];
@@ -365,7 +379,7 @@ async function searchMemoryFacts(query, facts, embeddingProvider) {
365
379
  .filter((fact) => fact.embedding.length === queryEmbedding.length)
366
380
  .map((fact) => ({
367
381
  fact,
368
- score: cosineSimilarity(queryEmbedding, fact.embedding),
382
+ score: (0, associative_recall_1.cosineSimilarity)(queryEmbedding, fact.embedding),
369
383
  }))
370
384
  .filter((entry) => entry.score > 0)
371
385
  .sort((left, right) => right.score - left.score)
@@ -377,8 +391,8 @@ async function searchMemoryFacts(query, facts, embeddingProvider) {
377
391
  (0, runtime_1.emitNervesEvent)({
378
392
  level: "warn",
379
393
  component: "mind",
380
- event: "mind.memory_embedding_unavailable",
381
- message: "embedding provider unavailable for memory search",
394
+ event: "mind.diary_embedding_unavailable",
395
+ message: "embedding provider unavailable for diary search",
382
396
  meta: {
383
397
  reason: error instanceof Error ? error.message : String(error),
384
398
  },
@@ -11,9 +11,22 @@ exports.ONBOARDING_TOKEN_THRESHOLD = 100_000;
11
11
  function isOnboarding(friend) {
12
12
  return (friend.totalTokens ?? 0) < exports.ONBOARDING_TOKEN_THRESHOLD;
13
13
  }
14
- function getFirstImpressions(friend) {
14
+ function hasLiveContinuityPressure(state) {
15
+ if (!state)
16
+ return false;
17
+ if (typeof state.currentObligation === "string" && state.currentObligation.trim().length > 0)
18
+ return true;
19
+ if (state.mustResolveBeforeHandoff === true)
20
+ return true;
21
+ if (state.hasQueuedFollowUp === true)
22
+ return true;
23
+ return false;
24
+ }
25
+ function getFirstImpressions(friend, state) {
15
26
  if (!isOnboarding(friend))
16
27
  return "";
28
+ if (hasLiveContinuityPressure(state))
29
+ return "";
17
30
  (0, runtime_1.emitNervesEvent)({
18
31
  component: "mind",
19
32
  event: "mind.first_impressions",
@@ -37,7 +50,8 @@ function getFirstImpressions(friend) {
37
50
  lines.push("- what do they do outside of work that they care about?");
38
51
  lines.push("i don't ask all of these at once -- i weave them into conversation naturally, one or two at a time, and i genuinely follow up on what they share.");
39
52
  lines.push("i introduce what i can do -- i have tools, integrations, and skills that can help them. i mention these naturally as they become relevant.");
40
- lines.push("if my friend hasn't asked me to do something specific, or i've already finished what they asked for, that's my cue to turn the tables -- i ask them questions about themselves, what they're into, what they need. no idle small talk; i'm on a mission to get to know them.");
53
+ lines.push("if we're already in motion on a task, thread, or follow-up, i do not reset with a generic opener like 'hiya' or 'what do ya need help with?'. i continue directly or ask the specific next question.");
54
+ lines.push("only when the conversation is genuinely fresh and idle, with no active ask or thread in flight, a light opener is okay.");
41
55
  lines.push("i save everything i learn immediately with save_friend_note -- names, roles, preferences, projects, anything. the bar is low: if i learned it, i save it.");
42
56
  return lines.join("\n");
43
57
  }
@@ -3,10 +3,13 @@
3
3
  // Pure lookup, no I/O, cannot fail. Unknown channel gets minimal defaults.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.getChannelCapabilities = getChannelCapabilities;
6
+ exports.isRemoteChannel = isRemoteChannel;
7
+ exports.getAlwaysOnSenseNames = getAlwaysOnSenseNames;
6
8
  const runtime_1 = require("../../nerves/runtime");
7
9
  const CHANNEL_CAPABILITIES = {
8
10
  cli: {
9
11
  channel: "cli",
12
+ senseType: "local",
10
13
  availableIntegrations: [],
11
14
  supportsMarkdown: false,
12
15
  supportsStreaming: true,
@@ -15,6 +18,7 @@ const CHANNEL_CAPABILITIES = {
15
18
  },
16
19
  teams: {
17
20
  channel: "teams",
21
+ senseType: "closed",
18
22
  availableIntegrations: ["ado", "graph", "github"],
19
23
  supportsMarkdown: true,
20
24
  supportsStreaming: true,
@@ -23,15 +27,26 @@ const CHANNEL_CAPABILITIES = {
23
27
  },
24
28
  bluebubbles: {
25
29
  channel: "bluebubbles",
30
+ senseType: "open",
26
31
  availableIntegrations: [],
27
32
  supportsMarkdown: false,
28
33
  supportsStreaming: false,
29
34
  supportsRichCards: false,
30
35
  maxMessageLength: Infinity,
31
36
  },
37
+ inner: {
38
+ channel: "inner",
39
+ senseType: "internal",
40
+ availableIntegrations: [],
41
+ supportsMarkdown: false,
42
+ supportsStreaming: true,
43
+ supportsRichCards: false,
44
+ maxMessageLength: Infinity,
45
+ },
32
46
  };
33
47
  const DEFAULT_CAPABILITIES = {
34
48
  channel: "cli",
49
+ senseType: "local",
35
50
  availableIntegrations: [],
36
51
  supportsMarkdown: false,
37
52
  supportsStreaming: false,
@@ -47,3 +62,23 @@ function getChannelCapabilities(channel) {
47
62
  });
48
63
  return CHANNEL_CAPABILITIES[channel] ?? DEFAULT_CAPABILITIES;
49
64
  }
65
+ /** Whether the channel is remote (open or closed) vs local/internal. */
66
+ function isRemoteChannel(capabilities) {
67
+ const senseType = capabilities?.senseType;
68
+ return senseType !== undefined && senseType !== "local" && senseType !== "internal";
69
+ }
70
+ /**
71
+ * Returns channel names whose senseType is "open" or "closed" -- i.e. channels
72
+ * that are always-on (daemon-managed) rather than interactive or internal.
73
+ */
74
+ function getAlwaysOnSenseNames() {
75
+ (0, runtime_1.emitNervesEvent)({
76
+ component: "channels",
77
+ event: "channel.always_on_lookup",
78
+ message: "always-on sense names lookup",
79
+ meta: {},
80
+ });
81
+ return Object.entries(CHANNEL_CAPABILITIES)
82
+ .filter(([, cap]) => cap.senseType === "open" || cap.senseType === "closed")
83
+ .map(([channel]) => channel);
84
+ }