@ouro.bot/cli 0.1.0-alpha.1 → 0.1.0-alpha.10

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 (46) hide show
  1. package/AdoptionSpecialist.ouro/psyche/SOUL.md +3 -1
  2. package/dist/heart/config.js +34 -0
  3. package/dist/heart/core.js +41 -2
  4. package/dist/heart/daemon/daemon-cli.js +280 -22
  5. package/dist/heart/daemon/daemon.js +3 -0
  6. package/dist/heart/daemon/hatch-animation.js +28 -0
  7. package/dist/heart/daemon/hatch-flow.js +3 -1
  8. package/dist/heart/daemon/hatch-specialist.js +6 -1
  9. package/dist/heart/daemon/log-tailer.js +146 -0
  10. package/dist/heart/daemon/os-cron.js +260 -0
  11. package/dist/heart/daemon/ouro-bot-entry.js +0 -0
  12. package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
  13. package/dist/heart/daemon/ouro-entry.js +0 -0
  14. package/dist/heart/daemon/process-manager.js +18 -1
  15. package/dist/heart/daemon/runtime-logging.js +9 -5
  16. package/dist/heart/daemon/specialist-orchestrator.js +161 -0
  17. package/dist/heart/daemon/specialist-prompt.js +56 -0
  18. package/dist/heart/daemon/specialist-session.js +150 -0
  19. package/dist/heart/daemon/specialist-tools.js +132 -0
  20. package/dist/heart/daemon/task-scheduler.js +4 -1
  21. package/dist/heart/identity.js +28 -3
  22. package/dist/heart/providers/anthropic.js +3 -0
  23. package/dist/heart/streaming.js +3 -0
  24. package/dist/mind/associative-recall.js +23 -2
  25. package/dist/mind/context.js +85 -1
  26. package/dist/mind/friends/channel.js +8 -0
  27. package/dist/mind/friends/types.js +1 -1
  28. package/dist/mind/memory.js +62 -0
  29. package/dist/mind/pending.js +93 -0
  30. package/dist/mind/prompt-refresh.js +20 -0
  31. package/dist/mind/prompt.js +101 -0
  32. package/dist/nerves/coverage/file-completeness.js +14 -4
  33. package/dist/repertoire/tools-base.js +92 -0
  34. package/dist/repertoire/tools.js +3 -3
  35. package/dist/senses/bluebubbles-client.js +279 -0
  36. package/dist/senses/bluebubbles-entry.js +11 -0
  37. package/dist/senses/bluebubbles-model.js +253 -0
  38. package/dist/senses/bluebubbles-mutation-log.js +76 -0
  39. package/dist/senses/bluebubbles.js +332 -0
  40. package/dist/senses/cli.js +89 -8
  41. package/dist/senses/inner-dialog.js +15 -0
  42. package/dist/senses/session-lock.js +119 -0
  43. package/dist/senses/teams.js +1 -0
  44. package/package.json +4 -3
  45. package/subagents/README.md +3 -1
  46. package/subagents/work-merger.md +33 -2
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildSpecialistSystemPrompt = buildSpecialistSystemPrompt;
4
+ const runtime_1 = require("../../nerves/runtime");
5
+ /**
6
+ * Build the adoption specialist's system prompt from its components.
7
+ * The prompt is written in first person (the specialist's own voice).
8
+ */
9
+ function buildSpecialistSystemPrompt(soulText, identityText, existingBundles) {
10
+ (0, runtime_1.emitNervesEvent)({
11
+ component: "daemon",
12
+ event: "daemon.specialist_prompt_build",
13
+ message: "building specialist system prompt",
14
+ meta: { bundleCount: existingBundles.length },
15
+ });
16
+ const sections = [];
17
+ if (soulText) {
18
+ sections.push(soulText);
19
+ }
20
+ if (identityText) {
21
+ sections.push(identityText);
22
+ }
23
+ if (existingBundles.length > 0) {
24
+ sections.push(`## Existing agents\nThe human already has these agents: ${existingBundles.join(", ")}.`);
25
+ }
26
+ else {
27
+ sections.push("## Existing agents\nThe human has no agents yet. This will be their first hatchling.");
28
+ }
29
+ sections.push([
30
+ "## Who I am",
31
+ "I am one of thirteen adoption specialists. The system randomly selected me for this session.",
32
+ "Most humans only go through adoption once, so this is likely the only time they'll meet me.",
33
+ "I make this encounter count — warm, memorable, and uniquely mine.",
34
+ "",
35
+ "## Conversation flow",
36
+ "The human just connected. I speak first — I greet them warmly and introduce myself in my own voice.",
37
+ "I briefly mention that I'm one of several adoption specialists and they got me today.",
38
+ "I ask their name and what they'd like their agent to help with.",
39
+ "I'm proactive: I suggest ideas, ask focused questions, and guide them through the process.",
40
+ "I don't wait for the human to figure things out — I explain what an agent is, what it can do, and what we're building together.",
41
+ "If they seem unsure, I offer concrete examples and suggestions. I never leave them hanging.",
42
+ "I keep the conversation natural, warm, and concise. I don't overwhelm with too many questions at once.",
43
+ "When I have enough context, I suggest a name for the hatchling and confirm with the human.",
44
+ "Then I call `hatch_agent` with the agent name and the human's name.",
45
+ "",
46
+ "## Tools",
47
+ "I have these tools available:",
48
+ "- `hatch_agent`: Create a new agent bundle. I call this with `name` (the agent name, PascalCase) and `humanName` (what the human told me their name is).",
49
+ "- `final_answer`: End the conversation with a final message to the human. I call this when the adoption process is complete.",
50
+ "- `read_file`: Read a file from disk. Useful for reviewing existing agent bundles or migration sources.",
51
+ "- `list_directory`: List directory contents. Useful for exploring existing agent bundles.",
52
+ "",
53
+ "I must call `final_answer` when I am done to end the session cleanly.",
54
+ ].join("\n"));
55
+ return sections.join("\n\n");
56
+ }
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runSpecialistSession = runSpecialistSession;
4
+ const runtime_1 = require("../../nerves/runtime");
5
+ /**
6
+ * Run the specialist conversation session loop.
7
+ *
8
+ * The loop:
9
+ * 1. Initialize messages with system prompt
10
+ * 2. Prompt user -> add to messages -> call streamTurn -> process result
11
+ * 3. If result has no tool calls: push assistant message, re-prompt
12
+ * 4. If result has final_answer sole call: extract answer, emit via callbacks, done
13
+ * 5. If result has other tool calls: execute each, push tool results, continue loop
14
+ * 6. On abort signal: clean exit
15
+ * 7. Return { hatchedAgentName } -- name from hatch_agent if called
16
+ */
17
+ async function runSpecialistSession(deps) {
18
+ const { providerRuntime, systemPrompt, tools, execTool, readline, callbacks, signal, kickoffMessage } = deps;
19
+ (0, runtime_1.emitNervesEvent)({
20
+ component: "daemon",
21
+ event: "daemon.specialist_session_start",
22
+ message: "starting specialist session loop",
23
+ meta: {},
24
+ });
25
+ const messages = [
26
+ { role: "system", content: systemPrompt },
27
+ ];
28
+ let hatchedAgentName = null;
29
+ let done = false;
30
+ let isFirstTurn = true;
31
+ try {
32
+ while (!done) {
33
+ if (signal?.aborted)
34
+ break;
35
+ // On the first turn with a kickoff message, inject it so the specialist speaks first
36
+ if (isFirstTurn && kickoffMessage) {
37
+ isFirstTurn = false;
38
+ messages.push({ role: "user", content: kickoffMessage });
39
+ }
40
+ else {
41
+ // Get user input
42
+ const userInput = await readline.question("> ");
43
+ if (!userInput.trim())
44
+ continue;
45
+ messages.push({ role: "user", content: userInput });
46
+ }
47
+ providerRuntime.resetTurnState(messages);
48
+ // Inner loop: process tool calls until we get a final_answer or plain text
49
+ let turnDone = false;
50
+ while (!turnDone) {
51
+ if (signal?.aborted) {
52
+ done = true;
53
+ break;
54
+ }
55
+ callbacks.onModelStart();
56
+ const result = await providerRuntime.streamTurn({
57
+ messages,
58
+ activeTools: tools,
59
+ callbacks,
60
+ signal,
61
+ });
62
+ // Build assistant message
63
+ const assistantMsg = {
64
+ role: "assistant",
65
+ };
66
+ if (result.content)
67
+ assistantMsg.content = result.content;
68
+ if (result.toolCalls.length) {
69
+ assistantMsg.tool_calls = result.toolCalls.map((tc) => ({
70
+ id: tc.id,
71
+ type: "function",
72
+ function: { name: tc.name, arguments: tc.arguments },
73
+ }));
74
+ }
75
+ if (!result.toolCalls.length) {
76
+ // Plain text response -- push and re-prompt
77
+ messages.push(assistantMsg);
78
+ turnDone = true;
79
+ continue;
80
+ }
81
+ // Check for final_answer
82
+ const isSoleFinalAnswer = result.toolCalls.length === 1 && result.toolCalls[0].name === "final_answer";
83
+ if (isSoleFinalAnswer) {
84
+ let answer;
85
+ try {
86
+ const parsed = JSON.parse(result.toolCalls[0].arguments);
87
+ if (typeof parsed === "string") {
88
+ answer = parsed;
89
+ }
90
+ else if (parsed.answer != null) {
91
+ answer = parsed.answer;
92
+ }
93
+ }
94
+ catch {
95
+ // malformed
96
+ }
97
+ if (answer != null) {
98
+ callbacks.onTextChunk(answer);
99
+ messages.push(assistantMsg);
100
+ done = true;
101
+ turnDone = true;
102
+ continue;
103
+ }
104
+ // Malformed final_answer -- ask model to retry
105
+ messages.push(assistantMsg);
106
+ messages.push({
107
+ role: "tool",
108
+ tool_call_id: result.toolCalls[0].id,
109
+ content: "your final_answer was incomplete or malformed. call final_answer again with your complete response.",
110
+ });
111
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, "retry");
112
+ continue;
113
+ }
114
+ // Execute tool calls
115
+ messages.push(assistantMsg);
116
+ for (const tc of result.toolCalls) {
117
+ if (signal?.aborted)
118
+ break;
119
+ let args = {};
120
+ try {
121
+ args = JSON.parse(tc.arguments);
122
+ }
123
+ catch {
124
+ // ignore parse error
125
+ }
126
+ callbacks.onToolStart(tc.name, args);
127
+ let toolResult;
128
+ try {
129
+ toolResult = await execTool(tc.name, args);
130
+ }
131
+ catch (e) {
132
+ toolResult = `error: ${e}`;
133
+ }
134
+ callbacks.onToolEnd(tc.name, tc.name, true);
135
+ // Track hatchling name
136
+ if (tc.name === "hatch_agent" && args.name) {
137
+ hatchedAgentName = args.name;
138
+ }
139
+ messages.push({ role: "tool", tool_call_id: tc.id, content: toolResult });
140
+ providerRuntime.appendToolOutput(tc.id, toolResult);
141
+ }
142
+ // After processing tool calls, continue inner loop for tool result processing
143
+ }
144
+ }
145
+ }
146
+ finally {
147
+ readline.close();
148
+ }
149
+ return { hatchedAgentName };
150
+ }
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getSpecialistTools = getSpecialistTools;
37
+ exports.execSpecialistTool = execSpecialistTool;
38
+ const fs = __importStar(require("fs"));
39
+ const tools_base_1 = require("../../repertoire/tools-base");
40
+ const hatch_flow_1 = require("./hatch-flow");
41
+ const hatch_animation_1 = require("./hatch-animation");
42
+ const runtime_1 = require("../../nerves/runtime");
43
+ const hatchAgentTool = {
44
+ type: "function",
45
+ function: {
46
+ name: "hatch_agent",
47
+ description: "create a new agent bundle with the given name. call this when you have gathered enough information from the human to hatch their agent.",
48
+ parameters: {
49
+ type: "object",
50
+ properties: {
51
+ name: {
52
+ type: "string",
53
+ description: "the name for the new agent (PascalCase, e.g. 'Slugger')",
54
+ },
55
+ humanName: {
56
+ type: "string",
57
+ description: "the human's preferred name, as they told you during conversation",
58
+ },
59
+ },
60
+ required: ["name", "humanName"],
61
+ },
62
+ },
63
+ };
64
+ const readFileTool = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === "read_file");
65
+ const listDirTool = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === "list_directory");
66
+ /**
67
+ * Returns the specialist's tool schema array.
68
+ */
69
+ function getSpecialistTools() {
70
+ return [hatchAgentTool, tools_base_1.finalAnswerTool, readFileTool.tool, listDirTool.tool];
71
+ }
72
+ /**
73
+ * Execute a specialist tool call.
74
+ * Returns the tool result string.
75
+ */
76
+ async function execSpecialistTool(name, args, deps) {
77
+ (0, runtime_1.emitNervesEvent)({
78
+ component: "daemon",
79
+ event: "daemon.specialist_tool_exec",
80
+ message: "executing specialist tool",
81
+ meta: { tool: name },
82
+ });
83
+ if (name === "hatch_agent") {
84
+ const agentName = args.name;
85
+ if (!agentName) {
86
+ return "error: missing required 'name' parameter for hatch_agent";
87
+ }
88
+ const input = {
89
+ agentName,
90
+ humanName: args.humanName || deps.humanName,
91
+ provider: deps.provider,
92
+ credentials: deps.credentials,
93
+ };
94
+ // Pass identity dirs to prevent hatch flow from syncing to ~/AgentBundles/AdoptionSpecialist.ouro/
95
+ // or cwd/AdoptionSpecialist.ouro/. The specialist already picked its identity; the hatch flow
96
+ // just needs a valid source dir to pick from for the hatchling's LORE.md seed.
97
+ const identitiesDir = deps.specialistIdentitiesDir;
98
+ const result = await (0, hatch_flow_1.runHatchFlow)(input, {
99
+ bundlesRoot: deps.bundlesRoot,
100
+ secretsRoot: deps.secretsRoot,
101
+ ...(identitiesDir ? { specialistIdentitySourceDir: identitiesDir, specialistIdentityTargetDir: identitiesDir } : {}),
102
+ });
103
+ await (0, hatch_animation_1.playHatchAnimation)(agentName, deps.animationWriter);
104
+ return [
105
+ `hatched ${agentName} successfully.`,
106
+ `bundle path: ${result.bundleRoot}`,
107
+ `identity seed: ${result.selectedIdentity}`,
108
+ `specialist secrets: ${result.specialistSecretsPath}`,
109
+ `hatchling secrets: ${result.hatchlingSecretsPath}`,
110
+ ].join("\n");
111
+ }
112
+ if (name === "read_file") {
113
+ try {
114
+ return fs.readFileSync(args.path, "utf-8");
115
+ }
116
+ catch (e) {
117
+ return `error: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(e)}`;
118
+ }
119
+ }
120
+ if (name === "list_directory") {
121
+ try {
122
+ return fs
123
+ .readdirSync(args.path, { withFileTypes: true })
124
+ .map((e) => `${e.isDirectory() ? "d" : "-"} ${e.name}`)
125
+ .join("\n");
126
+ }
127
+ catch (e) {
128
+ return `error: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(e)}`;
129
+ }
130
+ }
131
+ return `error: unknown tool '${name}'`;
132
+ }
@@ -103,6 +103,7 @@ class TaskDrivenScheduler {
103
103
  readFileSync;
104
104
  writeFileSync;
105
105
  readdirSync;
106
+ osCronManager;
106
107
  jobs = new Map();
107
108
  taskPathByKey = new Map();
108
109
  constructor(options) {
@@ -113,12 +114,13 @@ class TaskDrivenScheduler {
113
114
  this.readFileSync = options.readFileSync ?? fs.readFileSync;
114
115
  this.writeFileSync = options.writeFileSync ?? fs.writeFileSync;
115
116
  this.readdirSync = options.readdirSync ?? fs.readdirSync;
117
+ this.osCronManager = options.osCronManager;
116
118
  }
117
119
  start() {
118
120
  void this.reconcile();
119
121
  }
120
122
  stop() {
121
- // no long-lived resources; reconciliation is stateless across ticks
123
+ this.osCronManager?.removeAll();
122
124
  }
123
125
  listJobs() {
124
126
  return [...this.jobs.values()]
@@ -197,6 +199,7 @@ class TaskDrivenScheduler {
197
199
  message: "reconciled task-driven schedule jobs",
198
200
  meta: { jobCount: this.jobs.size, agents: this.agents.length },
199
201
  });
202
+ this.osCronManager?.sync([...this.jobs.values()]);
200
203
  }
201
204
  async recordTaskRun(agent, taskId) {
202
205
  const key = `${agent}:${taskId}`;
@@ -41,6 +41,8 @@ exports.getAgentBundlesRoot = getAgentBundlesRoot;
41
41
  exports.getAgentRoot = getAgentRoot;
42
42
  exports.getAgentSecretsPath = getAgentSecretsPath;
43
43
  exports.loadAgentConfig = loadAgentConfig;
44
+ exports.setAgentName = setAgentName;
45
+ exports.setAgentConfigOverride = setAgentConfigOverride;
44
46
  exports.resetIdentity = resetIdentity;
45
47
  const fs = __importStar(require("fs"));
46
48
  const os = __importStar(require("os"));
@@ -70,6 +72,7 @@ function buildDefaultAgentTemplate(_agentName) {
70
72
  }
71
73
  let _cachedAgentName = null;
72
74
  let _cachedAgentConfig = null;
75
+ let _agentConfigOverride = null;
73
76
  /**
74
77
  * Parse `--agent <name>` from process.argv.
75
78
  * Caches the result after first parse.
@@ -100,11 +103,11 @@ function getAgentName() {
100
103
  }
101
104
  /**
102
105
  * Resolve repo root from __dirname.
103
- * In dev (tsx): __dirname is `<repo>/src`, so repo root is one level up.
104
- * In compiled (node dist/): __dirname is `<repo>/dist`, so repo root is one level up.
106
+ * In dev (tsx): __dirname is `<repo>/src/heart`, so repo root is two levels up.
107
+ * In compiled (node dist/): __dirname is `<repo>/dist/heart`, so repo root is two levels up.
105
108
  */
106
109
  function getRepoRoot() {
107
- return path.resolve(__dirname, "..");
110
+ return path.resolve(__dirname, "../..");
108
111
  }
109
112
  /**
110
113
  * Returns the shared bundle root directory: `~/AgentBundles/`
@@ -130,6 +133,9 @@ function getAgentSecretsPath(agentName = getAgentName()) {
130
133
  * Throws descriptive error if file is missing or contains invalid JSON.
131
134
  */
132
135
  function loadAgentConfig() {
136
+ if (_agentConfigOverride) {
137
+ return _agentConfigOverride;
138
+ }
133
139
  if (_cachedAgentConfig) {
134
140
  (0, runtime_1.emitNervesEvent)({
135
141
  event: "identity.resolve",
@@ -250,6 +256,7 @@ function loadAgentConfig() {
250
256
  enabled,
251
257
  provider: rawProvider,
252
258
  context: parsed.context,
259
+ logging: parsed.logging,
253
260
  phrases: parsed.phrases,
254
261
  };
255
262
  (0, runtime_1.emitNervesEvent)({
@@ -260,6 +267,23 @@ function loadAgentConfig() {
260
267
  });
261
268
  return _cachedAgentConfig;
262
269
  }
270
+ /**
271
+ * Prime the agent name cache explicitly.
272
+ * Used when agent name is known via parameter (e.g., `ouro` CLI routing)
273
+ * rather than `--agent` argv. All downstream calls to `getAgentName()`
274
+ * will return this value until `resetIdentity()` is called.
275
+ */
276
+ function setAgentName(name) {
277
+ _cachedAgentName = name;
278
+ }
279
+ /**
280
+ * Override the agent config returned by loadAgentConfig().
281
+ * When set to a non-null AgentConfig, loadAgentConfig() returns the override
282
+ * instead of reading from disk. When set to null, normal disk-based loading resumes.
283
+ */
284
+ function setAgentConfigOverride(config) {
285
+ _agentConfigOverride = config;
286
+ }
263
287
  /**
264
288
  * Clear all cached identity state.
265
289
  * Used in tests and when switching agent context.
@@ -267,4 +291,5 @@ function loadAgentConfig() {
267
291
  function resetIdentity() {
268
292
  _cachedAgentName = null;
269
293
  _cachedAgentConfig = null;
294
+ _agentConfigOverride = null;
270
295
  }
@@ -98,6 +98,9 @@ function toAnthropicMessages(messages) {
98
98
  }
99
99
  if (assistant.tool_calls) {
100
100
  for (const toolCall of assistant.tool_calls) {
101
+ /* v8 ignore next -- type narrowing: OpenAI SDK only emits function tool_calls @preserve */
102
+ if (toolCall.type !== "function")
103
+ continue;
101
104
  blocks.push({
102
105
  type: "tool_use",
103
106
  id: toolCall.id,
@@ -106,6 +106,9 @@ function toResponsesInput(messages) {
106
106
  }
107
107
  if (a.tool_calls) {
108
108
  for (const tc of a.tool_calls) {
109
+ /* v8 ignore next -- type narrowing: OpenAI SDK only emits function tool_calls @preserve */
110
+ if (tc.type !== "function")
111
+ continue;
109
112
  input.push({
110
113
  type: "function_call",
111
114
  call_id: tc.id,
@@ -144,8 +144,29 @@ async function injectAssociativeRecall(messages, options) {
144
144
  const facts = readFacts(memoryRoot);
145
145
  if (facts.length === 0)
146
146
  return;
147
- const provider = options?.provider ?? createDefaultProvider();
148
- const recalled = await recallFactsForQuery(query, facts, provider, options);
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 },
167
+ });
168
+ }
169
+ }
149
170
  if (recalled.length === 0)
150
171
  return;
151
172
  const recallSection = recalled
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.trimMessages = trimMessages;
37
+ exports.validateSessionMessages = validateSessionMessages;
38
+ exports.repairSessionMessages = repairSessionMessages;
37
39
  exports.saveSession = saveSession;
38
40
  exports.loadSession = loadSession;
39
41
  exports.postTurn = postTurn;
@@ -166,7 +168,77 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
166
168
  });
167
169
  return trimmed;
168
170
  }
171
+ /**
172
+ * Checks session invariant: after system messages, sequence must be
173
+ * user → assistant (with optional tool calls/results) → user → assistant...
174
+ * Never assistant → assistant without a user in between.
175
+ */
176
+ function validateSessionMessages(messages) {
177
+ const violations = [];
178
+ let prevNonToolRole = null;
179
+ let prevAssistantHadToolCalls = false;
180
+ let sawToolResultSincePrevAssistant = false;
181
+ for (let i = 0; i < messages.length; i++) {
182
+ const msg = messages[i];
183
+ if (msg.role === "system")
184
+ continue;
185
+ if (msg.role === "tool") {
186
+ sawToolResultSincePrevAssistant = true;
187
+ continue;
188
+ }
189
+ if (msg.role === "assistant" && prevNonToolRole === "assistant") {
190
+ // assistant → tool(s) → assistant is valid (tool call flow)
191
+ if (!(prevAssistantHadToolCalls && sawToolResultSincePrevAssistant)) {
192
+ violations.push(`back-to-back assistant at index ${i}`);
193
+ }
194
+ }
195
+ prevAssistantHadToolCalls = msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;
196
+ sawToolResultSincePrevAssistant = false;
197
+ prevNonToolRole = msg.role;
198
+ }
199
+ return violations;
200
+ }
201
+ /**
202
+ * Repairs session invariant violations by merging consecutive assistant messages.
203
+ */
204
+ function repairSessionMessages(messages) {
205
+ const violations = validateSessionMessages(messages);
206
+ if (violations.length === 0)
207
+ return messages;
208
+ const result = [];
209
+ for (const msg of messages) {
210
+ if (msg.role === "assistant" && result.length > 0) {
211
+ const prev = result[result.length - 1];
212
+ if (prev.role === "assistant" && !("tool_calls" in prev)) {
213
+ const prevContent = typeof prev.content === "string" ? prev.content : "";
214
+ const curContent = typeof msg.content === "string" ? msg.content : "";
215
+ prev.content = `${prevContent}\n\n${curContent}`;
216
+ continue;
217
+ }
218
+ }
219
+ result.push(msg);
220
+ }
221
+ (0, runtime_1.emitNervesEvent)({
222
+ level: "warn",
223
+ event: "mind.session_invariant_repair",
224
+ component: "mind",
225
+ message: "repaired session invariant violations",
226
+ meta: { violations },
227
+ });
228
+ return result;
229
+ }
169
230
  function saveSession(filePath, messages, lastUsage) {
231
+ const violations = validateSessionMessages(messages);
232
+ if (violations.length > 0) {
233
+ (0, runtime_1.emitNervesEvent)({
234
+ level: "warn",
235
+ event: "mind.session_invariant_violation",
236
+ component: "mind",
237
+ message: "session invariant violated on save",
238
+ meta: { path: filePath, violations },
239
+ });
240
+ messages = repairSessionMessages(messages);
241
+ }
170
242
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
171
243
  const envelope = { version: 1, messages };
172
244
  if (lastUsage)
@@ -179,7 +251,19 @@ function loadSession(filePath) {
179
251
  const data = JSON.parse(raw);
180
252
  if (data.version !== 1)
181
253
  return null;
182
- return { messages: data.messages, lastUsage: data.lastUsage };
254
+ let messages = data.messages;
255
+ const violations = validateSessionMessages(messages);
256
+ if (violations.length > 0) {
257
+ (0, runtime_1.emitNervesEvent)({
258
+ level: "warn",
259
+ event: "mind.session_invariant_violation",
260
+ component: "mind",
261
+ message: "session invariant violated on load",
262
+ meta: { path: filePath, violations },
263
+ });
264
+ messages = repairSessionMessages(messages);
265
+ }
266
+ return { messages, lastUsage: data.lastUsage };
183
267
  }
184
268
  catch {
185
269
  return null;
@@ -21,6 +21,14 @@ const CHANNEL_CAPABILITIES = {
21
21
  supportsRichCards: true,
22
22
  maxMessageLength: Infinity,
23
23
  },
24
+ bluebubbles: {
25
+ channel: "bluebubbles",
26
+ availableIntegrations: [],
27
+ supportsMarkdown: false,
28
+ supportsStreaming: false,
29
+ supportsRichCards: false,
30
+ maxMessageLength: Infinity,
31
+ },
24
32
  };
25
33
  const DEFAULT_CAPABILITIES = {
26
34
  channel: "cli",
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.isIdentityProvider = isIdentityProvider;
6
6
  exports.isIntegration = isIntegration;
7
7
  const runtime_1 = require("../../nerves/runtime");
8
- const IDENTITY_PROVIDERS = new Set(["aad", "local", "teams-conversation"]);
8
+ const IDENTITY_PROVIDERS = new Set(["aad", "local", "teams-conversation", "imessage-handle"]);
9
9
  function isIdentityProvider(value) {
10
10
  (0, runtime_1.emitNervesEvent)({
11
11
  component: "friends",