@ouro.bot/cli 0.1.0-alpha.44 → 0.1.0-alpha.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/changelog.json CHANGED
@@ -1,6 +1,27 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.46",
6
+ "changes": [
7
+ "Inner dialog now knows which task triggered it: taskId flows from daemon poke through the worker into the turn, and the agent gets the full task file content instead of a generic heartbeat prompt.",
8
+ "Inner dialog boot message includes aspirations and state summary instead of a vacuous placeholder, so the agent wakes up with context about what matters and what's happening.",
9
+ "Vestigial `drainInbox` removed from inner dialog — pipeline already handles pending drain correctly.",
10
+ "Inner dialog nerves events now include assistant response preview, tool call names, token usage, and taskId for meaningful observability.",
11
+ "`ouro thoughts` command reads and formats inner dialog session turns with `--last`, `--json`, `--follow`, and `--agent` flags — humans can now see what the agent has been thinking.",
12
+ "`readTaskFile` searches collection subdirectories (one-shots, ongoing, habits) since the scheduler sends bare task stems without collection prefixes.",
13
+ "`ouro reminder create` accepts `--requester` to track who requested a reminder for notification round-trip.",
14
+ "Response extraction handles `tool_choice=required` models by falling back to `final_answer` tool call arguments when assistant message content is empty."
15
+ ]
16
+ },
17
+ {
18
+ "version": "0.1.0-alpha.45",
19
+ "changes": [
20
+ "`ouro up` now persists a boot-time launch agent with `RunAtLoad`, so the daemon comes back reliably after reboot instead of only starting for the current session.",
21
+ "BlueBubbles sense status is now truthful about upstream health: it probes the real BlueBubbles API, records runtime state, and surfaces `error` when the webhook listener is alive but the upstream server is unreachable.",
22
+ "BlueBubbles intake now replays recoverable read/delivery mutation backlogs through the normal inbound agent path, with a small dedupe ledger so missed messages can be recovered without silent drops or duplicate delivery."
23
+ ]
24
+ },
4
25
  {
5
26
  "version": "0.1.0-alpha.44",
6
27
  "changes": [
@@ -62,7 +62,9 @@ const update_hooks_1 = require("./update-hooks");
62
62
  const bundle_meta_1 = require("./hooks/bundle-meta");
63
63
  const bundle_manifest_1 = require("../../mind/bundle-manifest");
64
64
  const tasks_1 = require("../../repertoire/tasks");
65
+ const thoughts_1 = require("./thoughts");
65
66
  const ouro_bot_global_installer_1 = require("./ouro-bot-global-installer");
67
+ const launchd_1 = require("./launchd");
66
68
  function stringField(value) {
67
69
  return typeof value === "string" ? value : null;
68
70
  }
@@ -257,6 +259,7 @@ function usage() {
257
259
  " ouro friend list [--agent <name>]",
258
260
  " ouro friend show <id> [--agent <name>]",
259
261
  " ouro friend create --name <name> [--trust <level>] [--agent <name>]",
262
+ " ouro thoughts [--last <n>] [--json] [--follow] [--agent <name>]",
260
263
  " ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
261
264
  " ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
262
265
  " ouro whoami [--agent <name>]",
@@ -519,6 +522,7 @@ function parseReminderCommand(args) {
519
522
  let scheduledAt;
520
523
  let cadence;
521
524
  let category;
525
+ let requester;
522
526
  for (let i = 1; i < rest.length; i++) {
523
527
  if (rest[i] === "--body" && rest[i + 1]) {
524
528
  body = rest[i + 1];
@@ -536,6 +540,10 @@ function parseReminderCommand(args) {
536
540
  category = rest[i + 1];
537
541
  i += 1;
538
542
  }
543
+ else if (rest[i] === "--requester" && rest[i + 1]) {
544
+ requester = rest[i + 1];
545
+ i += 1;
546
+ }
539
547
  }
540
548
  if (!body)
541
549
  throw new Error(`Usage\n${usage()}`);
@@ -548,6 +556,7 @@ function parseReminderCommand(args) {
548
556
  ...(scheduledAt ? { scheduledAt } : {}),
549
557
  ...(cadence ? { cadence } : {}),
550
558
  ...(category ? { category } : {}),
559
+ ...(requester ? { requester } : {}),
551
560
  ...(agent ? { agent } : {}),
552
561
  };
553
562
  }
@@ -562,6 +571,23 @@ function parseSessionCommand(args) {
562
571
  return { kind: "session.list", ...(agent ? { agent } : {}) };
563
572
  throw new Error(`Usage\n${usage()}`);
564
573
  }
574
+ function parseThoughtsCommand(args) {
575
+ const { agent, rest: cleaned } = extractAgentFlag(args);
576
+ let last;
577
+ let json = false;
578
+ let follow = false;
579
+ for (let i = 0; i < cleaned.length; i++) {
580
+ if (cleaned[i] === "--last" && i + 1 < cleaned.length) {
581
+ last = Number.parseInt(cleaned[i + 1], 10);
582
+ i++;
583
+ }
584
+ if (cleaned[i] === "--json")
585
+ json = true;
586
+ if (cleaned[i] === "--follow" || cleaned[i] === "-f")
587
+ follow = true;
588
+ }
589
+ return { kind: "thoughts", ...(agent ? { agent } : {}), ...(last ? { last } : {}), ...(json ? { json } : {}), ...(follow ? { follow } : {}) };
590
+ }
565
591
  function parseFriendCommand(args) {
566
592
  const { agent, rest: cleaned } = extractAgentFlag(args);
567
593
  const [sub, ...rest] = cleaned;
@@ -629,6 +655,8 @@ function parseOuroCommand(args) {
629
655
  }
630
656
  if (head === "session")
631
657
  return parseSessionCommand(args.slice(1));
658
+ if (head === "thoughts")
659
+ return parseThoughtsCommand(args.slice(1));
632
660
  if (head === "chat") {
633
661
  if (!second)
634
662
  throw new Error(`Usage\n${usage()}`);
@@ -761,6 +789,25 @@ function defaultFallbackPendingMessage(command) {
761
789
  });
762
790
  return pendingPath;
763
791
  }
792
+ function defaultEnsureDaemonBootPersistence(socketPath) {
793
+ if (process.platform !== "darwin") {
794
+ return;
795
+ }
796
+ const homeDir = os.homedir();
797
+ const launchdDeps = {
798
+ writeFile: (filePath, content) => fs.writeFileSync(filePath, content, "utf-8"),
799
+ mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
800
+ homeDir,
801
+ };
802
+ const entryPath = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
803
+ const logDir = path.join(homeDir, ".agentstate", "daemon", "logs");
804
+ (0, launchd_1.writeLaunchAgentPlist)(launchdDeps, {
805
+ nodePath: process.execPath,
806
+ entryPath,
807
+ socketPath,
808
+ logDir,
809
+ });
810
+ }
764
811
  async function defaultInstallSubagents() {
765
812
  return (0, subagent_installer_1.installSubagentsForAvailableCli)({
766
813
  repoRoot: (0, identity_1.getRepoRoot)(),
@@ -1045,6 +1092,7 @@ function createDefaultOuroCliDeps(socketPath = "/tmp/ouroboros-daemon.sock") {
1045
1092
  registerOuroBundleType: ouro_uti_1.registerOuroBundleUti,
1046
1093
  installOuroCommand: ouro_path_installer_1.installOuroCommand,
1047
1094
  syncGlobalOuroBotWrapper: ouro_bot_global_installer_1.syncGlobalOuroBotWrapper,
1095
+ ensureDaemonBootPersistence: defaultEnsureDaemonBootPersistence,
1048
1096
  /* v8 ignore next 3 -- integration: launches interactive CLI session @preserve */
1049
1097
  startChat: async (agentName) => {
1050
1098
  const { main } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
@@ -1317,6 +1365,7 @@ function executeReminderCommand(command, taskMod) {
1317
1365
  body: command.body,
1318
1366
  scheduledAt: command.scheduledAt,
1319
1367
  cadence: command.cadence,
1368
+ requester: command.requester,
1320
1369
  });
1321
1370
  return `created: ${created}`;
1322
1371
  }
@@ -1406,6 +1455,20 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1406
1455
  });
1407
1456
  if (command.kind === "daemon.up") {
1408
1457
  await performSystemSetup(deps);
1458
+ if (deps.ensureDaemonBootPersistence) {
1459
+ try {
1460
+ await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
1461
+ }
1462
+ catch (error) {
1463
+ (0, runtime_1.emitNervesEvent)({
1464
+ level: "warn",
1465
+ component: "daemon",
1466
+ event: "daemon.system_setup_launchd_error",
1467
+ message: "failed to persist daemon boot startup",
1468
+ meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
1469
+ });
1470
+ }
1471
+ }
1409
1472
  // Run update hooks before starting daemon so user sees the output
1410
1473
  (0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
1411
1474
  const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
@@ -1501,6 +1564,49 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1501
1564
  }
1502
1565
  /* v8 ignore stop */
1503
1566
  }
1567
+ // ── thoughts (local, no daemon socket needed) ──
1568
+ if (command.kind === "thoughts") {
1569
+ try {
1570
+ const agentName = command.agent ?? (0, identity_1.getAgentName)();
1571
+ const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`);
1572
+ const sessionFilePath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
1573
+ if (command.json) {
1574
+ try {
1575
+ const raw = fs.readFileSync(sessionFilePath, "utf-8");
1576
+ deps.writeStdout(raw);
1577
+ return raw;
1578
+ }
1579
+ catch {
1580
+ const message = "no inner dialog session found";
1581
+ deps.writeStdout(message);
1582
+ return message;
1583
+ }
1584
+ }
1585
+ const turns = (0, thoughts_1.parseInnerDialogSession)(sessionFilePath);
1586
+ const message = (0, thoughts_1.formatThoughtTurns)(turns, command.last ?? 10);
1587
+ deps.writeStdout(message);
1588
+ if (command.follow) {
1589
+ deps.writeStdout("\n\n--- following (ctrl+c to stop) ---\n");
1590
+ /* v8 ignore start -- callback tested via followThoughts unit tests @preserve */
1591
+ const stop = (0, thoughts_1.followThoughts)(sessionFilePath, (formatted) => {
1592
+ deps.writeStdout("\n" + formatted);
1593
+ });
1594
+ /* v8 ignore stop */
1595
+ // Block until process exit; cleanup watcher on SIGINT/SIGTERM
1596
+ return new Promise((resolve) => {
1597
+ const cleanup = () => { stop(); resolve(message); };
1598
+ process.once("SIGINT", cleanup);
1599
+ process.once("SIGTERM", cleanup);
1600
+ });
1601
+ }
1602
+ return message;
1603
+ }
1604
+ catch {
1605
+ const message = "error: no agent context — use --agent <name> to specify";
1606
+ deps.writeStdout(message);
1607
+ return message;
1608
+ }
1609
+ }
1504
1610
  // ── session list (local, no daemon socket needed) ──
1505
1611
  if (command.kind === "session.list") {
1506
1612
  /* v8 ignore start -- production default: requires full identity setup @preserve */
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.DAEMON_PLIST_LABEL = void 0;
37
37
  exports.generateDaemonPlist = generateDaemonPlist;
38
+ exports.writeLaunchAgentPlist = writeLaunchAgentPlist;
38
39
  exports.installLaunchAgent = installLaunchAgent;
39
40
  exports.uninstallLaunchAgent = uninstallLaunchAgent;
40
41
  exports.isDaemonInstalled = isDaemonInstalled;
@@ -65,6 +66,8 @@ function generateDaemonPlist(options) {
65
66
  ` <string>--socket</string>`,
66
67
  ` <string>${options.socketPath}</string>`,
67
68
  ` </array>`,
69
+ ` <key>RunAtLoad</key>`,
70
+ ` <true/>`,
68
71
  ` <key>KeepAlive</key>`,
69
72
  ` <true/>`,
70
73
  ];
@@ -74,6 +77,23 @@ function generateDaemonPlist(options) {
74
77
  lines.push(`</dict>`, `</plist>`, ``);
75
78
  return lines.join("\n");
76
79
  }
80
+ function writeLaunchAgentPlist(deps, options) {
81
+ const launchAgentsDir = path.join(deps.homeDir, "Library", "LaunchAgents");
82
+ deps.mkdirp(launchAgentsDir);
83
+ if (options.logDir) {
84
+ deps.mkdirp(options.logDir);
85
+ }
86
+ const fullPath = plistFilePath(deps.homeDir);
87
+ const xml = generateDaemonPlist(options);
88
+ deps.writeFile(fullPath, xml);
89
+ (0, runtime_1.emitNervesEvent)({
90
+ component: "daemon",
91
+ event: "daemon.launchd_plist_written",
92
+ message: "daemon launch agent plist written",
93
+ meta: { plistPath: fullPath, entryPath: options.entryPath, socketPath: options.socketPath },
94
+ });
95
+ return fullPath;
96
+ }
77
97
  function installLaunchAgent(deps, options) {
78
98
  (0, runtime_1.emitNervesEvent)({
79
99
  component: "daemon",
@@ -81,8 +101,6 @@ function installLaunchAgent(deps, options) {
81
101
  message: "installing launch agent",
82
102
  meta: { entryPath: options.entryPath, socketPath: options.socketPath },
83
103
  });
84
- const launchAgentsDir = path.join(deps.homeDir, "Library", "LaunchAgents");
85
- deps.mkdirp(launchAgentsDir);
86
104
  const fullPath = plistFilePath(deps.homeDir);
87
105
  // Unload existing (best effort) for idempotent re-install
88
106
  if (deps.existsFile(fullPath)) {
@@ -91,8 +109,7 @@ function installLaunchAgent(deps, options) {
91
109
  }
92
110
  catch { /* best effort */ }
93
111
  }
94
- const xml = generateDaemonPlist(options);
95
- deps.writeFile(fullPath, xml);
112
+ writeLaunchAgentPlist(deps, options);
96
113
  deps.exec(`launchctl load "${fullPath}"`);
97
114
  (0, runtime_1.emitNervesEvent)({
98
115
  component: "daemon",
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
38
38
  const os = __importStar(require("os"));
39
39
  const path = __importStar(require("path"));
40
40
  const runtime_1 = require("../../nerves/runtime");
41
+ const bluebubbles_runtime_state_1 = require("../../senses/bluebubbles-runtime-state");
41
42
  const identity_1 = require("../identity");
42
43
  const sense_truth_1 = require("../sense-truth");
43
44
  const process_manager_1 = require("./process-manager");
@@ -175,12 +176,29 @@ function runtimeInfoFor(status) {
175
176
  return { runtime: "running" };
176
177
  return { runtime: "error" };
177
178
  }
179
+ function readBlueBubblesRuntimeFacts(agent, bundlesRoot, snapshot) {
180
+ const agentRoot = path.join(bundlesRoot, `${agent}.ouro`);
181
+ const runtimePath = path.join(agentRoot, "state", "senses", "bluebubbles", "runtime.json");
182
+ if (snapshot?.runtime !== "running" || !fs.existsSync(runtimePath)) {
183
+ return { runtime: snapshot?.runtime };
184
+ }
185
+ const state = (0, bluebubbles_runtime_state_1.readBlueBubblesRuntimeState)(agent, agentRoot);
186
+ if (state.upstreamStatus === "error") {
187
+ return {
188
+ runtime: "error",
189
+ detail: state.detail,
190
+ };
191
+ }
192
+ return { runtime: snapshot.runtime };
193
+ }
178
194
  class DaemonSenseManager {
179
195
  processManager;
180
196
  contexts;
197
+ bundlesRoot;
181
198
  constructor(options) {
182
199
  const bundlesRoot = options.bundlesRoot ?? path.join(os.homedir(), "AgentBundles");
183
200
  const secretsRoot = options.secretsRoot ?? path.join(os.homedir(), ".agentsecrets");
201
+ this.bundlesRoot = bundlesRoot;
184
202
  this.contexts = new Map(options.agents.map((agent) => {
185
203
  const senses = readAgentSenses(path.join(bundlesRoot, `${agent}.ouro`, "agent.json"));
186
204
  const facts = senseFactsFromSecrets(agent, senses, path.join(secretsRoot, agent, "secrets.json"));
@@ -227,6 +245,7 @@ class DaemonSenseManager {
227
245
  runtime.set(parsed.agent, current);
228
246
  }
229
247
  const rows = [...this.contexts.entries()].flatMap(([agent, context]) => {
248
+ const blueBubblesRuntimeFacts = readBlueBubblesRuntimeFacts(agent, this.bundlesRoot, runtime.get(agent)?.bluebubbles);
230
249
  const runtimeInfo = {
231
250
  cli: { configured: true },
232
251
  teams: {
@@ -235,7 +254,7 @@ class DaemonSenseManager {
235
254
  },
236
255
  bluebubbles: {
237
256
  configured: context.facts.bluebubbles.configured,
238
- ...(runtime.get(agent)?.bluebubbles ?? {}),
257
+ ...blueBubblesRuntimeFacts,
239
258
  },
240
259
  };
241
260
  const inventory = (0, sense_truth_1.getSenseInventory)({ senses: context.senses }, runtimeInfo);
@@ -245,7 +264,12 @@ class DaemonSenseManager {
245
264
  label: entry.label,
246
265
  enabled: entry.enabled,
247
266
  status: entry.status,
248
- detail: entry.enabled ? context.facts[entry.sense].detail : "not enabled in agent.json",
267
+ detail: entry.enabled
268
+ ? entry.sense === "bluebubbles"
269
+ ? blueBubblesRuntimeFacts.detail
270
+ ?? context.facts[entry.sense].detail
271
+ : context.facts[entry.sense].detail
272
+ : "not enabled in agent.json",
249
273
  }));
250
274
  });
251
275
  (0, runtime_1.emitNervesEvent)({
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ // Formats inner dialog session turns for human consumption.
3
+ // Used by `ouro thoughts` CLI command to show what the agent has been thinking.
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || (function () {
21
+ var ownKeys = function(o) {
22
+ ownKeys = Object.getOwnPropertyNames || function (o) {
23
+ var ar = [];
24
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
25
+ return ar;
26
+ };
27
+ return ownKeys(o);
28
+ };
29
+ return function (mod) {
30
+ if (mod && mod.__esModule) return mod;
31
+ var result = {};
32
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
33
+ __setModuleDefault(result, mod);
34
+ return result;
35
+ };
36
+ })();
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.parseInnerDialogSession = parseInnerDialogSession;
39
+ exports.formatThoughtTurns = formatThoughtTurns;
40
+ exports.getInnerDialogSessionPath = getInnerDialogSessionPath;
41
+ exports.followThoughts = followThoughts;
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const runtime_1 = require("../../nerves/runtime");
45
+ function contentToText(content) {
46
+ if (typeof content === "string")
47
+ return content;
48
+ if (!Array.isArray(content))
49
+ return "";
50
+ return content
51
+ .map((part) => {
52
+ if (typeof part === "string")
53
+ return part;
54
+ if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
55
+ return part.text;
56
+ }
57
+ return "";
58
+ })
59
+ .join("\n");
60
+ }
61
+ function classifyTurn(userText) {
62
+ if (userText.includes("waking up."))
63
+ return { type: "boot" };
64
+ const taskMatch = /## task: (.+)$/m.exec(userText);
65
+ if (taskMatch)
66
+ return { type: "task", taskId: taskMatch[1] };
67
+ return { type: "heartbeat" };
68
+ }
69
+ function extractToolNames(messages) {
70
+ const names = [];
71
+ for (const msg of messages) {
72
+ if (msg.role === "assistant" && Array.isArray(msg.tool_calls)) {
73
+ for (const tc of msg.tool_calls) {
74
+ if (tc.function?.name && tc.function.name !== "final_answer")
75
+ names.push(tc.function.name);
76
+ }
77
+ }
78
+ }
79
+ return names;
80
+ }
81
+ /** Extract text from a final_answer tool call's arguments. */
82
+ function extractFinalAnswer(messages) {
83
+ for (let k = messages.length - 1; k >= 0; k--) {
84
+ const msg = messages[k];
85
+ if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls))
86
+ continue;
87
+ for (const tc of msg.tool_calls) {
88
+ if (tc.function?.name !== "final_answer")
89
+ continue;
90
+ try {
91
+ const parsed = JSON.parse(tc.function.arguments ?? "{}");
92
+ if (typeof parsed.answer === "string" && parsed.answer.trim())
93
+ return parsed.answer.trim();
94
+ }
95
+ catch {
96
+ // malformed arguments — skip
97
+ }
98
+ }
99
+ }
100
+ return "";
101
+ }
102
+ function parseInnerDialogSession(sessionPath) {
103
+ (0, runtime_1.emitNervesEvent)({
104
+ component: "daemon",
105
+ event: "daemon.thoughts_parse",
106
+ message: "parsing inner dialog session",
107
+ meta: { sessionPath },
108
+ });
109
+ let raw;
110
+ try {
111
+ raw = fs.readFileSync(sessionPath, "utf-8");
112
+ }
113
+ catch {
114
+ return [];
115
+ }
116
+ let data;
117
+ try {
118
+ data = JSON.parse(raw);
119
+ }
120
+ catch {
121
+ return [];
122
+ }
123
+ if (data.version !== 1 || !Array.isArray(data.messages))
124
+ return [];
125
+ const turns = [];
126
+ const messages = data.messages;
127
+ // Walk messages, pairing user → (tool calls) → assistant sequences
128
+ let i = 0;
129
+ while (i < messages.length) {
130
+ const msg = messages[i];
131
+ if (msg.role === "system") {
132
+ i++;
133
+ continue;
134
+ }
135
+ if (msg.role !== "user") {
136
+ i++;
137
+ continue;
138
+ }
139
+ const userText = contentToText(msg.content);
140
+ const classification = classifyTurn(userText);
141
+ // Collect all messages until the next user message (or end)
142
+ const turnMessages = [];
143
+ let j = i + 1;
144
+ while (j < messages.length && messages[j].role !== "user") {
145
+ turnMessages.push(messages[j]);
146
+ j++;
147
+ }
148
+ // Find the last assistant text response in this turn.
149
+ // With tool_choice="required", the response may be inside a final_answer tool call.
150
+ const assistantMsgs = turnMessages.filter((m) => m.role === "assistant");
151
+ const lastAssistant = assistantMsgs.reverse().find((m) => contentToText(m.content).trim().length > 0);
152
+ const response = lastAssistant
153
+ ? contentToText(lastAssistant.content).trim()
154
+ : extractFinalAnswer(turnMessages);
155
+ const tools = extractToolNames(turnMessages);
156
+ turns.push({
157
+ type: classification.type,
158
+ prompt: userText.trim(),
159
+ response,
160
+ tools,
161
+ ...(classification.taskId ? { taskId: classification.taskId } : {}),
162
+ });
163
+ i = j;
164
+ }
165
+ return turns;
166
+ }
167
+ function formatThoughtTurns(turns, lastN) {
168
+ if (turns.length === 0)
169
+ return "no inner dialog activity";
170
+ const selected = lastN > 0 ? turns.slice(-lastN) : turns;
171
+ /* v8 ignore next -- unreachable: turns.length > 0 checked above, slice always returns ≥1 @preserve */
172
+ if (selected.length === 0)
173
+ return "no inner dialog activity";
174
+ const lines = [];
175
+ for (const turn of selected) {
176
+ const typeLabel = turn.type === "task" && turn.taskId
177
+ ? `task: ${turn.taskId}`
178
+ : turn.type;
179
+ lines.push(`--- ${typeLabel} ---`);
180
+ if (turn.tools.length > 0) {
181
+ lines.push(`tools: ${turn.tools.join(", ")}`);
182
+ }
183
+ if (turn.response) {
184
+ lines.push(turn.response);
185
+ }
186
+ else {
187
+ lines.push("(no response)");
188
+ }
189
+ lines.push("");
190
+ }
191
+ return lines.join("\n").trim();
192
+ }
193
+ function getInnerDialogSessionPath(agentRoot) {
194
+ return path.join(agentRoot, "state", "sessions", "self", "inner", "dialog.json");
195
+ }
196
+ /**
197
+ * Watch a session file and emit new turns as they appear.
198
+ * Returns a cleanup function that stops the watcher.
199
+ */
200
+ function followThoughts(sessionPath, onNewTurns, pollIntervalMs = 1000) {
201
+ let displayedCount = parseInnerDialogSession(sessionPath).length;
202
+ (0, runtime_1.emitNervesEvent)({
203
+ component: "daemon",
204
+ event: "daemon.thoughts_follow_start",
205
+ message: "started following inner dialog session",
206
+ meta: { sessionPath, initialTurns: displayedCount },
207
+ });
208
+ fs.watchFile(sessionPath, { interval: pollIntervalMs }, () => {
209
+ const turns = parseInnerDialogSession(sessionPath);
210
+ if (turns.length > displayedCount) {
211
+ const newTurns = turns.slice(displayedCount);
212
+ onNewTurns(formatThoughtTurns(newTurns, 0));
213
+ displayedCount = turns.length;
214
+ }
215
+ });
216
+ return () => {
217
+ fs.unwatchFile(sessionPath);
218
+ (0, runtime_1.emitNervesEvent)({
219
+ component: "daemon",
220
+ event: "daemon.thoughts_follow_stop",
221
+ message: "stopped following inner dialog session",
222
+ meta: { sessionPath, totalTurns: displayedCount },
223
+ });
224
+ };
225
+ }
@@ -170,6 +170,12 @@ function applyRepairNotice(event, notice) {
170
170
  repairNotice: notice,
171
171
  };
172
172
  }
173
+ function hasRecoverableMessageContent(event) {
174
+ return event.kind === "message"
175
+ && (event.textForAgent.trim().length > 0
176
+ || event.attachments.length > 0
177
+ || event.hasPayloadData);
178
+ }
173
179
  function hydrateTextForAgent(event, rawData) {
174
180
  if (event.kind !== "message") {
175
181
  return { ...event, requiresRepair: false };
@@ -323,6 +329,40 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
323
329
  throw new Error(`BlueBubbles read failed (${response.status}): ${errorText || "unknown"}`);
324
330
  }
325
331
  },
332
+ async checkHealth() {
333
+ const url = buildBlueBubblesApiUrl(config.serverUrl, "/api/v1/message/count", config.password);
334
+ (0, runtime_1.emitNervesEvent)({
335
+ component: "senses",
336
+ event: "senses.bluebubbles_healthcheck_start",
337
+ message: "probing bluebubbles upstream health",
338
+ meta: { serverUrl: config.serverUrl },
339
+ });
340
+ const response = await fetch(url, {
341
+ method: "GET",
342
+ signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
343
+ });
344
+ if (!response.ok) {
345
+ const errorText = await response.text().catch(() => "");
346
+ (0, runtime_1.emitNervesEvent)({
347
+ level: "warn",
348
+ component: "senses",
349
+ event: "senses.bluebubbles_healthcheck_error",
350
+ message: "bluebubbles upstream health probe failed",
351
+ meta: {
352
+ serverUrl: config.serverUrl,
353
+ status: response.status,
354
+ reason: errorText || "unknown",
355
+ },
356
+ });
357
+ throw new Error(`BlueBubbles upstream health check failed (${response.status}): ${errorText || "unknown"}`);
358
+ }
359
+ (0, runtime_1.emitNervesEvent)({
360
+ component: "senses",
361
+ event: "senses.bluebubbles_healthcheck_end",
362
+ message: "bluebubbles upstream health probe succeeded",
363
+ meta: { serverUrl: config.serverUrl },
364
+ });
365
+ },
326
366
  async repairEvent(event) {
327
367
  if (!event.requiresRepair) {
328
368
  (0, runtime_1.emitNervesEvent)({
@@ -387,7 +427,16 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
387
427
  type: event.eventType,
388
428
  data,
389
429
  });
390
- let hydrated = hydrateTextForAgent(normalized, data);
430
+ const recoveredMessage = event.kind === "mutation"
431
+ && !event.shouldNotifyAgent
432
+ ? (0, bluebubbles_model_1.normalizeBlueBubblesEvent)({
433
+ type: "new-message",
434
+ data,
435
+ })
436
+ : null;
437
+ let hydrated = recoveredMessage && hasRecoverableMessageContent(recoveredMessage)
438
+ ? hydrateTextForAgent(recoveredMessage, data)
439
+ : hydrateTextForAgent(normalized, data);
391
440
  if (hydrated.kind === "message" &&
392
441
  hydrated.balloonBundleId !== "com.apple.messages.URLBalloonProvider" &&
393
442
  hydrated.attachments.length > 0) {
@@ -411,6 +460,7 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
411
460
  kind: hydrated.kind,
412
461
  messageGuid: hydrated.messageGuid,
413
462
  repairedFrom: event.kind,
463
+ promotedFromMutation: event.kind === "mutation" && hydrated.kind === "message",
414
464
  },
415
465
  });
416
466
  return hydrated;
@@ -8,4 +8,6 @@ if (!process.argv.includes("--agent")) {
8
8
  process.exit(1);
9
9
  }
10
10
  const bluebubbles_1 = require("./bluebubbles");
11
+ const runtime_logging_1 = require("../heart/daemon/runtime-logging");
12
+ (0, runtime_logging_1.configureDaemonRuntimeLogger)("bluebubbles");
11
13
  (0, bluebubbles_1.startBlueBubblesApp)();