@ouro.bot/cli 0.1.0-alpha.16 → 0.1.0-alpha.161

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 (168) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +3 -2
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +3 -3
  3. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  4. package/README.md +187 -191
  5. package/changelog.json +1088 -0
  6. package/dist/heart/active-work.js +633 -0
  7. package/dist/heart/bridges/manager.js +358 -0
  8. package/dist/heart/bridges/state-machine.js +135 -0
  9. package/dist/heart/bridges/store.js +123 -0
  10. package/dist/heart/commitments.js +105 -0
  11. package/dist/heart/config.js +40 -30
  12. package/dist/heart/core.js +608 -123
  13. package/dist/heart/cross-chat-delivery.js +146 -0
  14. package/dist/heart/daemon/agent-discovery.js +81 -0
  15. package/dist/heart/daemon/agent-service.js +360 -0
  16. package/dist/heart/daemon/auth-flow.js +477 -0
  17. package/dist/heart/daemon/cadence.js +70 -0
  18. package/dist/heart/daemon/daemon-cli.js +2467 -205
  19. package/dist/heart/daemon/daemon-entry.js +131 -4
  20. package/dist/heart/daemon/daemon-health.js +136 -0
  21. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  22. package/dist/heart/daemon/daemon-tombstone.js +123 -0
  23. package/dist/heart/daemon/daemon.js +445 -17
  24. package/dist/heart/daemon/habit-migration.js +181 -0
  25. package/dist/heart/daemon/habit-parser.js +119 -0
  26. package/dist/heart/daemon/habit-scheduler.js +369 -0
  27. package/dist/heart/daemon/hatch-animation.js +10 -3
  28. package/dist/heart/daemon/hatch-flow.js +30 -111
  29. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  30. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  31. package/dist/heart/daemon/inner-status.js +89 -0
  32. package/dist/heart/daemon/launchd.js +159 -0
  33. package/dist/heart/daemon/log-tailer.js +4 -3
  34. package/dist/heart/daemon/mcp-server.js +591 -0
  35. package/dist/heart/daemon/message-router.js +17 -8
  36. package/dist/heart/daemon/os-cron-deps.js +134 -0
  37. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  38. package/dist/heart/daemon/ouro-path-installer.js +78 -34
  39. package/dist/heart/daemon/ouro-version-manager.js +171 -0
  40. package/dist/heart/daemon/outlook-http.js +369 -0
  41. package/dist/heart/daemon/outlook-read.js +1281 -0
  42. package/dist/heart/daemon/outlook-render.js +1032 -0
  43. package/dist/heart/daemon/outlook-types.js +7 -0
  44. package/dist/heart/daemon/outlook-view.js +194 -0
  45. package/dist/heart/daemon/process-manager.js +75 -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 +219 -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 +50 -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/specialist-orchestrator.js +2 -2
  56. package/dist/heart/daemon/specialist-prompt.js +13 -8
  57. package/dist/heart/daemon/specialist-tools.js +53 -4
  58. package/dist/heart/daemon/staged-restart.js +114 -0
  59. package/dist/heart/daemon/task-scheduler.js +3 -25
  60. package/dist/heart/daemon/thoughts.js +507 -0
  61. package/dist/heart/daemon/update-checker.js +111 -0
  62. package/dist/heart/daemon/update-hooks.js +138 -0
  63. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  64. package/dist/heart/delegation.js +62 -0
  65. package/dist/heart/identity.js +136 -55
  66. package/dist/heart/kicks.js +2 -20
  67. package/dist/heart/migrate-config.js +127 -0
  68. package/dist/heart/model-capabilities.js +48 -0
  69. package/dist/heart/obligations.js +197 -0
  70. package/dist/heart/progress-story.js +42 -0
  71. package/dist/heart/provider-failover.js +88 -0
  72. package/dist/heart/provider-ping.js +172 -0
  73. package/dist/heart/providers/anthropic-token.js +163 -0
  74. package/dist/heart/providers/anthropic.js +196 -36
  75. package/dist/heart/providers/azure.js +116 -10
  76. package/dist/heart/providers/github-copilot.js +157 -0
  77. package/dist/heart/providers/minimax.js +33 -3
  78. package/dist/heart/providers/openai-codex.js +51 -16
  79. package/dist/heart/session-activity.js +172 -0
  80. package/dist/heart/session-recall.js +216 -0
  81. package/dist/heart/streaming.js +117 -33
  82. package/dist/heart/target-resolution.js +127 -0
  83. package/dist/heart/tool-activity-callbacks.js +36 -0
  84. package/dist/heart/tool-description.js +135 -0
  85. package/dist/heart/tool-loop.js +200 -0
  86. package/dist/heart/turn-coordinator.js +28 -0
  87. package/dist/mind/associative-recall.js +122 -32
  88. package/dist/mind/bundle-manifest.js +14 -0
  89. package/dist/mind/context.js +125 -14
  90. package/dist/mind/{memory.js → diary.js} +70 -56
  91. package/dist/mind/first-impressions.js +16 -2
  92. package/dist/mind/friends/channel.js +56 -0
  93. package/dist/mind/friends/group-context.js +144 -0
  94. package/dist/mind/friends/store-file.js +19 -0
  95. package/dist/mind/friends/trust-explanation.js +74 -0
  96. package/dist/mind/friends/types.js +8 -0
  97. package/dist/mind/journal-index.js +161 -0
  98. package/dist/mind/obligation-steering.js +221 -0
  99. package/dist/mind/obligations.js +134 -0
  100. package/dist/mind/pending.js +76 -9
  101. package/dist/mind/phrases.js +1 -0
  102. package/dist/mind/prompt.js +651 -91
  103. package/dist/mind/token-estimate.js +8 -12
  104. package/dist/nerves/cli-logging.js +15 -2
  105. package/dist/nerves/coverage/run-artifacts.js +1 -1
  106. package/dist/nerves/event-buffer.js +111 -0
  107. package/dist/nerves/index.js +63 -3
  108. package/dist/nerves/observation.js +20 -0
  109. package/dist/nerves/runtime.js +5 -1
  110. package/dist/outlook-ui/assets/index-CXQG0mVx.js +61 -0
  111. package/dist/outlook-ui/assets/index-Gq7EdfkO.css +1 -0
  112. package/dist/outlook-ui/index.html +15 -0
  113. package/dist/repertoire/ado-semantic.js +6 -0
  114. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  115. package/dist/repertoire/coding/context-pack.js +254 -0
  116. package/dist/repertoire/coding/feedback.js +301 -0
  117. package/dist/repertoire/coding/index.js +4 -1
  118. package/dist/repertoire/coding/manager.js +210 -4
  119. package/dist/repertoire/coding/spawner.js +58 -12
  120. package/dist/repertoire/coding/tools.js +176 -4
  121. package/dist/repertoire/guardrails.js +291 -0
  122. package/dist/repertoire/mcp-client.js +254 -0
  123. package/dist/repertoire/mcp-manager.js +198 -0
  124. package/dist/repertoire/skills.js +14 -23
  125. package/dist/repertoire/tasks/board.js +43 -5
  126. package/dist/repertoire/tasks/fix.js +182 -0
  127. package/dist/repertoire/tasks/index.js +28 -10
  128. package/dist/repertoire/tasks/lifecycle.js +2 -2
  129. package/dist/repertoire/tasks/parser.js +3 -2
  130. package/dist/repertoire/tasks/scanner.js +194 -37
  131. package/dist/repertoire/tasks/transitions.js +11 -28
  132. package/dist/repertoire/tools-base.js +992 -273
  133. package/dist/repertoire/tools-bluebubbles.js +94 -0
  134. package/dist/repertoire/tools-github.js +1 -6
  135. package/dist/repertoire/tools-teams.js +12 -59
  136. package/dist/repertoire/tools.js +281 -101
  137. package/dist/scripts/claude-code-hook.js +41 -0
  138. package/dist/scripts/claude-code-stop-hook.js +47 -0
  139. package/dist/senses/attention-queue.js +97 -0
  140. package/dist/senses/bluebubbles-client.js +258 -5
  141. package/dist/senses/bluebubbles-entry.js +2 -0
  142. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  143. package/dist/senses/bluebubbles-media.js +339 -0
  144. package/dist/senses/bluebubbles-model.js +12 -4
  145. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  146. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  147. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  148. package/dist/senses/bluebubbles.js +1040 -45
  149. package/dist/senses/cli-entry.js +1 -1
  150. package/dist/senses/cli-layout.js +187 -0
  151. package/dist/senses/cli.js +419 -152
  152. package/dist/senses/commands.js +38 -1
  153. package/dist/senses/continuity.js +94 -0
  154. package/dist/senses/habit-turn-message.js +108 -0
  155. package/dist/senses/inner-dialog-worker.js +112 -19
  156. package/dist/senses/inner-dialog.js +592 -84
  157. package/dist/senses/pipeline.js +502 -0
  158. package/dist/senses/shared-turn.js +196 -0
  159. package/dist/senses/surface-tool.js +95 -0
  160. package/dist/senses/teams.js +667 -120
  161. package/dist/senses/trust-gate.js +112 -2
  162. package/package.json +13 -5
  163. package/skills/configure-dev-tools.md +81 -0
  164. package/subagents/README.md +4 -70
  165. package/dist/heart/daemon/subagent-installer.js +0 -134
  166. package/subagents/work-doer.md +0 -233
  167. package/subagents/work-merger.md +0 -624
  168. package/subagents/work-planner.md +0 -373
@@ -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,14 +81,26 @@ 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();
88
89
  if (!raw)
89
90
  return [];
90
- return raw.split("\n").map((line) => JSON.parse(line));
91
+ const facts = [];
92
+ for (const line of raw.split("\n")) {
93
+ const trimmed = line.trim();
94
+ if (!trimmed)
95
+ continue;
96
+ try {
97
+ facts.push(JSON.parse(trimmed));
98
+ }
99
+ catch {
100
+ // Skip corrupt lines (e.g. partial write from a crash).
101
+ }
102
+ }
103
+ return facts;
91
104
  }
92
105
  function getLatestUserText(messages) {
93
106
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -133,6 +146,40 @@ async function recallFactsForQuery(query, facts, provider, options) {
133
146
  .sort((left, right) => right.score - left.score)
134
147
  .slice(0, topK);
135
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
+ }
136
183
  async function injectAssociativeRecall(messages, options) {
137
184
  try {
138
185
  if (messages[0]?.role !== "system" || typeof messages[0].content !== "string")
@@ -140,37 +187,80 @@ async function injectAssociativeRecall(messages, options) {
140
187
  const query = getLatestUserText(messages);
141
188
  if (!query)
142
189
  return;
143
- const memoryRoot = options?.memoryRoot ?? path.join((0, identity_1.getAgentRoot)(), "psyche", "memory");
144
- const facts = readFacts(memoryRoot);
145
- 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)
146
195
  return;
147
- let recalled;
148
- try {
149
- const provider = options?.provider ?? createDefaultProvider();
150
- recalled = await recallFactsForQuery(query, facts, provider, options);
151
- }
152
- catch {
153
- // Embeddings unavailable — fall back to substring matching
154
- const lowerQuery = query.toLowerCase();
155
- const topK = options?.topK ?? DEFAULT_TOP_K;
156
- recalled = facts
157
- .filter((fact) => fact.text.toLowerCase().includes(lowerQuery))
158
- .slice(0, topK)
159
- .map((fact) => ({ ...fact, score: 1 }));
160
- if (recalled.length > 0) {
161
- (0, runtime_1.emitNervesEvent)({
162
- level: "warn",
163
- component: "mind",
164
- event: "mind.associative_recall_fallback",
165
- message: "embeddings unavailable, used substring fallback",
166
- 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,
167
233
  });
168
234
  }
169
235
  }
170
- 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)
171
259
  return;
172
- const recallSection = recalled
173
- .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}`)
174
264
  .join("\n");
175
265
  messages[0] = {
176
266
  role: "system",
@@ -180,7 +270,7 @@ async function injectAssociativeRecall(messages, options) {
180
270
  component: "mind",
181
271
  event: "mind.associative_recall",
182
272
  message: "associative recall injected",
183
- meta: { count: recalled.length },
273
+ meta: { count: resultLines.length },
184
274
  });
185
275
  }
186
276
  catch (error) {
@@ -190,7 +280,7 @@ async function injectAssociativeRecall(messages, options) {
190
280
  event: "mind.associative_recall_error",
191
281
  message: "associative recall failed",
192
282
  meta: {
193
- reason: error instanceof Error ? error.message : String(error),
283
+ reason: error instanceof Error ? error.message : /* v8 ignore start -- defensive: non-Error catch branch @preserve */ String(error) /* v8 ignore stop */,
194
284
  },
195
285
  });
196
286
  }
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CANONICAL_BUNDLE_MANIFEST = void 0;
37
+ exports.getChangelogPath = getChangelogPath;
37
38
  exports.getPackageVersion = getPackageVersion;
38
39
  exports.createBundleMeta = createBundleMeta;
39
40
  exports.backfillBundleMeta = backfillBundleMeta;
@@ -51,13 +52,26 @@ exports.CANONICAL_BUNDLE_MANIFEST = [
51
52
  { path: "psyche/LORE.md", kind: "file" },
52
53
  { path: "psyche/TACIT.md", kind: "file" },
53
54
  { path: "psyche/ASPIRATIONS.md", kind: "file" },
55
+ { path: "diary", kind: "dir" },
56
+ { path: "journal", kind: "dir" },
54
57
  { path: "psyche/memory", kind: "dir" },
55
58
  { path: "friends", kind: "dir" },
59
+ { path: "state", kind: "dir" },
56
60
  { path: "tasks", kind: "dir" },
57
61
  { path: "skills", kind: "dir" },
58
62
  { path: "senses", kind: "dir" },
59
63
  { path: "senses/teams", kind: "dir" },
60
64
  ];
65
+ function getChangelogPath() {
66
+ const changelogPath = path.resolve(__dirname, "../../changelog.json");
67
+ (0, runtime_1.emitNervesEvent)({
68
+ component: "mind",
69
+ event: "mind.changelog_path_resolved",
70
+ message: "resolved changelog path",
71
+ meta: { path: changelogPath },
72
+ });
73
+ return changelogPath;
74
+ }
61
75
  function getPackageVersion() {
62
76
  const packageJsonPath = path.resolve(__dirname, "../../package.json");
63
77
  const raw = fs.readFileSync(packageJsonPath, "utf-8");
@@ -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 {