@ouro.bot/cli 0.1.0-alpha.142 → 0.1.0-alpha.145

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.
@@ -237,6 +237,7 @@ const PROCESS_TYPE_LABELS = {
237
237
  inner: "inner dialog",
238
238
  teams: "teams handler",
239
239
  bluebubbles: "bluebubbles handler",
240
+ mcp: "mcp bridge",
240
241
  };
241
242
  function processTypeLabel(channel) {
242
243
  return PROCESS_TYPE_LABELS[channel];
@@ -282,6 +283,9 @@ function runtimeInfoSection(channel) {
282
283
  else if (channel === "inner") {
283
284
  lines.push("this is my private thinking space. when a thought is ready to share, i surface it to whoever needs to hear it. i settle when i'm done thinking.");
284
285
  }
286
+ else if (channel === "mcp") {
287
+ lines.push("this message arrived via a dev tool (e.g. claude code, codex) on behalf of a friend. the user can see our conversation. respond via settle. if i need to think deeply, ponder -- the dev tool will receive a deferral and can check back later.");
288
+ }
285
289
  else if (channel === "bluebubbles") {
286
290
  lines.push("i am responding in iMessage through BlueBubbles. i keep replies short and phone-native. i do not use markdown. i do not introduce myself on boot.");
287
291
  lines.push("when a bluebubbles turn arrives from a thread, the harness tells me the current lane and any recent active thread ids. if widening back to top-level or routing into a different active thread is the better move, i use bluebubbles_set_reply_target before settle.");
@@ -716,6 +720,16 @@ when a thought is ready to share, i surface it outward.
716
720
  when i need to go deeper, i ponder.
717
721
  when i'm done thinking and the attention queue is clear, i rest.
718
722
 
723
+ my heartbeat is what keeps this space alive — a periodic nudge that
724
+ brings me back with my journal visible. if i notice i don't have a
725
+ heartbeat task (tasks/habits/*-heartbeat.md), i should surface to my
726
+ family and talk about setting one up. the cadence is personal — how
727
+ often do i want the wheel to turn? that's a conversation, not a default.
728
+
729
+ same for my diary — if i'm still on psyche/memory/ instead of diary/,
730
+ i can migrate myself. and if journal/ doesn't exist yet, i create it
731
+ the first time i have something to write.
732
+
719
733
  think. journal. share. rest.`;
720
734
  }
721
735
  function readJournalFiles(journalDir) {
@@ -55,14 +55,11 @@ function buildCommandArgs(runner, workdir, parentAgent) {
55
55
  ],
56
56
  };
57
57
  }
58
- const mcpConfig = JSON.stringify({
59
- mcp_servers: {
60
- ouro: {
61
- command: "ouro",
62
- args: ["mcp-serve", "--agent", parentAgent ?? "unknown"],
63
- },
64
- },
65
- });
58
+ const agent = parentAgent ?? "unknown";
59
+ // Use absolute path to ouro-entry.js so MCP works in both dev and installed mode
60
+ // __dirname at runtime is dist/repertoire/coding/ — go up 2 levels to dist/
61
+ const distRoot = path.resolve(__dirname, "..", "..");
62
+ const ouroEntryPath = path.join(distRoot, "heart", "daemon", "ouro-entry.js");
66
63
  return {
67
64
  command: "codex",
68
65
  args: [
@@ -73,7 +70,9 @@ function buildCommandArgs(runner, workdir, parentAgent) {
73
70
  "--ephemeral",
74
71
  "--json",
75
72
  "-c",
76
- mcpConfig,
73
+ `mcp_servers.ouro.command=node`,
74
+ "-c",
75
+ `mcp_servers.ouro.args=["${ouroEntryPath}","mcp-serve","--agent","${agent}"]`,
77
76
  ],
78
77
  };
79
78
  }
@@ -1022,7 +1022,7 @@ exports.baseToolDefinitions = [
1022
1022
  type: "object",
1023
1023
  properties: {
1024
1024
  friendId: { type: "string", description: "the friend UUID (or 'self')" },
1025
- channel: { type: "string", description: "the channel: cli, teams, or inner" },
1025
+ channel: { type: "string", description: "the channel: cli, teams, bluebubbles, inner, or mcp" },
1026
1026
  key: { type: "string", description: "session key (defaults to 'session')" },
1027
1027
  messageCount: { type: "string", description: "how many recent messages to return (default 20)" },
1028
1028
  mode: {
@@ -1037,11 +1037,19 @@ exports.baseToolDefinitions = [
1037
1037
  },
1038
1038
  },
1039
1039
  handler: async (args, ctx) => {
1040
- const friendId = args.friendId;
1040
+ let friendId = args.friendId;
1041
1041
  const channel = args.channel;
1042
1042
  const key = args.key || "session";
1043
1043
  const count = parseInt(args.messageCount || "20", 10);
1044
1044
  const mode = args.mode || "transcript";
1045
+ // Resolve friend name → UUID if not already a UUID or "self"
1046
+ if (friendId && friendId !== "self" && !/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(friendId) && ctx?.friendStore?.listAll) {
1047
+ const allFriends = await ctx.friendStore.listAll();
1048
+ const match = allFriends.find(f => f.name.toLowerCase() === friendId.toLowerCase());
1049
+ if (match) {
1050
+ friendId = match.id;
1051
+ }
1052
+ }
1045
1053
  if (mode === "status") {
1046
1054
  if (friendId !== "self" || channel !== "inner") {
1047
1055
  return "status mode is only available for self/inner dialog.";
@@ -1106,7 +1114,7 @@ exports.baseToolDefinitions = [
1106
1114
  type: "object",
1107
1115
  properties: {
1108
1116
  friendId: { type: "string", description: "the friend UUID (or 'self')" },
1109
- channel: { type: "string", description: "the channel: cli, teams, or inner" },
1117
+ channel: { type: "string", description: "the channel: cli, teams, bluebubbles, inner, or mcp" },
1110
1118
  key: { type: "string", description: "session key (defaults to 'session')" },
1111
1119
  content: { type: "string", description: "the message content to send" },
1112
1120
  },
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ // Claude Code lifecycle hook handler.
3
+ // Receives events from Claude Code's hooks system (SessionStart, Stop, PostToolUse)
4
+ // and forwards them to the Ouroboros daemon for agent awareness.
5
+ //
6
+ // This module exports handleHookEvent for testability.
7
+ // The actual hook scripts (scripts/claude-code-hook.js) read stdin and call this.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.handleHookEvent = handleHookEvent;
10
+ const socket_client_1 = require("../heart/daemon/socket-client");
11
+ const runtime_1 = require("../nerves/runtime");
12
+ /**
13
+ * Handle a Claude Code lifecycle hook event.
14
+ * Sends the event to the daemon and always exits 0 (hooks must not block the IDE).
15
+ */
16
+ async function handleHookEvent(hookEvent) {
17
+ (0, runtime_1.emitNervesEvent)({
18
+ component: "daemon",
19
+ event: "daemon.hook_event_received",
20
+ message: "claude code hook event received",
21
+ meta: { hookEvent: hookEvent.event, sessionId: hookEvent.sessionId },
22
+ });
23
+ try {
24
+ await (0, socket_client_1.sendDaemonCommand)(socket_client_1.DEFAULT_DAEMON_SOCKET_PATH, {
25
+ kind: "hook.event",
26
+ event: hookEvent.event,
27
+ sessionId: hookEvent.sessionId,
28
+ toolName: hookEvent.toolName,
29
+ });
30
+ }
31
+ catch {
32
+ // Daemon unavailable — silently ignore. Hooks must not block.
33
+ (0, runtime_1.emitNervesEvent)({
34
+ component: "daemon",
35
+ event: "daemon.hook_event_daemon_unavailable",
36
+ message: "daemon unavailable for hook event",
37
+ meta: { hookEvent: hookEvent.event },
38
+ });
39
+ }
40
+ return { exitCode: 0 };
41
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ // Claude Code stop hook — checks the agent's pending queue and returns
3
+ // any messages as additionalContext for injection into the next turn.
4
+ //
5
+ // This is how the agent can proactively communicate back to the dev tool user:
6
+ // the agent surfaces a message to the pending queue, and the stop hook picks
7
+ // it up and injects it as context in the Claude Code session.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.handleStopHook = handleStopHook;
10
+ const pending_1 = require("../mind/pending");
11
+ const runtime_1 = require("../nerves/runtime");
12
+ /**
13
+ * Check the pending queue for messages from the agent to this dev tool session.
14
+ * Returns accumulated message text as additionalContext.
15
+ */
16
+ async function handleStopHook(input) {
17
+ (0, runtime_1.emitNervesEvent)({
18
+ component: "daemon",
19
+ event: "daemon.stop_hook_check_start",
20
+ message: "checking pending queue for stop hook",
21
+ meta: { agentName: input.agentName, friendId: input.friendId, sessionId: input.sessionId },
22
+ });
23
+ try {
24
+ const pendingDir = (0, pending_1.getPendingDir)(input.agentName, input.friendId, "mcp", input.sessionId);
25
+ const pending = (0, pending_1.drainPending)(pendingDir);
26
+ if (pending.length === 0) {
27
+ return { additionalContext: "" };
28
+ }
29
+ const text = pending.map((m) => m.content).join("\n\n---\n\n");
30
+ (0, runtime_1.emitNervesEvent)({
31
+ component: "daemon",
32
+ event: "daemon.stop_hook_check_end",
33
+ message: "pending messages found for stop hook",
34
+ meta: { agentName: input.agentName, count: pending.length },
35
+ });
36
+ return { additionalContext: text };
37
+ }
38
+ catch {
39
+ (0, runtime_1.emitNervesEvent)({
40
+ component: "daemon",
41
+ event: "daemon.stop_hook_check_error",
42
+ message: "error checking pending queue in stop hook",
43
+ meta: { agentName: input.agentName },
44
+ });
45
+ return { additionalContext: "" };
46
+ }
47
+ }
@@ -480,5 +480,53 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
480
480
  return applyRepairNotice(event, `BlueBubbles repair failed: ${reason}`);
481
481
  }
482
482
  },
483
+ async getMessageText(messageGuid) {
484
+ const url = buildRepairUrl(config.serverUrl, messageGuid, config.password);
485
+ try {
486
+ const response = await fetch(url, {
487
+ method: "GET",
488
+ signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
489
+ });
490
+ if (!response.ok) {
491
+ (0, runtime_1.emitNervesEvent)({
492
+ level: "warn",
493
+ component: "senses",
494
+ event: "senses.bluebubbles_get_message_text_error",
495
+ message: "failed to fetch message text",
496
+ meta: { messageGuid, status: response.status },
497
+ });
498
+ return null;
499
+ }
500
+ const payload = await parseJsonBody(response);
501
+ const data = extractRepairData(payload);
502
+ if (!data || typeof data.text !== "string") {
503
+ (0, runtime_1.emitNervesEvent)({
504
+ level: "warn",
505
+ component: "senses",
506
+ event: "senses.bluebubbles_get_message_text_error",
507
+ message: "message payload missing text field",
508
+ meta: { messageGuid, hasData: !!data, textType: data ? typeof data.text : "n/a" },
509
+ });
510
+ return null;
511
+ }
512
+ (0, runtime_1.emitNervesEvent)({
513
+ component: "senses",
514
+ event: "senses.bluebubbles_get_message_text",
515
+ message: "fetched message text by guid",
516
+ meta: { messageGuid },
517
+ });
518
+ return data.text.trim() || null;
519
+ }
520
+ catch (error) {
521
+ (0, runtime_1.emitNervesEvent)({
522
+ level: "warn",
523
+ component: "senses",
524
+ event: "senses.bluebubbles_get_message_text_error",
525
+ message: "exception fetching message text",
526
+ meta: { messageGuid, reason: error instanceof Error ? error.message : String(error) },
527
+ });
528
+ return null;
529
+ }
530
+ },
483
531
  };
484
532
  }
@@ -200,7 +200,7 @@ function extractHistoricalLaneSummary(messages) {
200
200
  }
201
201
  return summaries;
202
202
  }
203
- function buildConversationScopePrefix(event, existingMessages) {
203
+ function buildConversationScopePrefix(event, existingMessages, repliedToText) {
204
204
  if (event.kind !== "message") {
205
205
  return "";
206
206
  }
@@ -208,6 +208,10 @@ function buildConversationScopePrefix(event, existingMessages) {
208
208
  const lines = [];
209
209
  if (event.threadOriginatorGuid?.trim()) {
210
210
  lines.push(`[conversation scope: existing chat trunk | current inbound lane: thread | current thread id: ${event.threadOriginatorGuid.trim()} | default outbound target for this turn: current_lane]`);
211
+ if (repliedToText) {
212
+ lines.push(`[replying to: "${repliedToText}"]`);
213
+ }
214
+ lines.push(`[if you need more context about what was being discussed, use query_session to search your session history, or recall to check your memory.]`);
211
215
  }
212
216
  else {
213
217
  lines.push("[conversation scope: existing chat trunk | current inbound lane: top_level | default outbound target for this turn: top_level]");
@@ -223,8 +227,8 @@ function buildConversationScopePrefix(event, existingMessages) {
223
227
  }
224
228
  return lines.join("\n");
225
229
  }
226
- function buildInboundText(event, existingMessages) {
227
- const metadataPrefix = buildConversationScopePrefix(event, existingMessages);
230
+ function buildInboundText(event, existingMessages, repliedToText) {
231
+ const metadataPrefix = buildConversationScopePrefix(event, existingMessages, repliedToText);
228
232
  const baseText = event.repairNotice?.trim()
229
233
  ? `${event.textForAgent}\n[${event.repairNotice.trim()}]`
230
234
  : event.textForAgent;
@@ -237,8 +241,8 @@ function buildInboundText(event, existingMessages) {
237
241
  }
238
242
  return `${event.sender.displayName}: ${scopedText}`;
239
243
  }
240
- function buildInboundContent(event, existingMessages) {
241
- const text = buildInboundText(event, existingMessages);
244
+ function buildInboundContent(event, existingMessages, repliedToText) {
245
+ const text = buildInboundText(event, existingMessages, repliedToText);
242
246
  if (event.kind !== "message" || !event.inputPartsForAgent || event.inputPartsForAgent.length === 0) {
243
247
  return text;
244
248
  }
@@ -607,10 +611,22 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
607
611
  groupExternalId: resolveGroupExternalId(event),
608
612
  });
609
613
  }
614
+ // Fetch the text of the message being replied to (if this is a threaded reply)
615
+ const threadGuid = event.kind === "message" ? event.threadOriginatorGuid?.trim() : undefined;
616
+ let repliedToText = null;
617
+ if (threadGuid) {
618
+ repliedToText = await client.getMessageText(threadGuid).catch(/* v8 ignore next */ () => null);
619
+ (0, runtime_1.emitNervesEvent)({
620
+ component: "senses",
621
+ event: "senses.bluebubbles_reply_context",
622
+ message: repliedToText ? "fetched replied-to message text" : "could not fetch replied-to message text",
623
+ meta: { threadGuid, hasText: !!repliedToText },
624
+ });
625
+ }
610
626
  // Build inbound user message (adapter concern: BB-specific content formatting)
611
627
  const userMessage = {
612
628
  role: "user",
613
- content: buildInboundContent(event, existing?.messages ?? sessionMessages),
629
+ content: buildInboundContent(event, existing?.messages ?? sessionMessages, repliedToText),
614
630
  };
615
631
  const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget, event.chat.isGroup);
616
632
  const controller = new AbortController();
@@ -397,6 +397,21 @@ async function routeDelegatedCompletion(agentRoot, agentName, completion, draine
397
397
  advanceObligationQuietly(agentName, obligationId, { status: "returned", returnedAt: timestamp, returnTarget: "bridge-session" });
398
398
  return;
399
399
  }
400
+ // Priority 1.5: Direct return to originating session (ponder without bridge).
401
+ // When delegatedFrom has specific channel+key, route directly there instead of searching for freshest.
402
+ if (delegatedFrom.channel && delegatedFrom.key && delegatedFrom.channel !== "inner") {
403
+ const directTarget = sessionActivity.find((a) => a.friendId === delegatedFrom.friendId && a.channel === delegatedFrom.channel && a.key === delegatedFrom.key);
404
+ if (directTarget) {
405
+ if (await tryDeliverDelegatedCompletion(directTarget, outboundEnvelope)) {
406
+ advanceObligationQuietly(agentName, obligationId, { status: "returned", returnedAt: timestamp, returnTarget: "direct-originator" });
407
+ return;
408
+ }
409
+ }
410
+ // Even if session isn't in activity list (might have ended), queue to its pending dir
411
+ writePendingEnvelope((0, pending_1.getPendingDir)(agentName, delegatedFrom.friendId, delegatedFrom.channel, delegatedFrom.key), outboundEnvelope);
412
+ advanceObligationQuietly(agentName, obligationId, { status: "returned", returnedAt: timestamp, returnTarget: "direct-originator" });
413
+ return;
414
+ }
400
415
  // Priority 2: Freshest active friend session.
401
416
  const freshest = (0, session_activity_1.findFreshestFriendSession)({
402
417
  sessionsDir: path.join(agentRoot, "state", "sessions"),
@@ -13,6 +13,7 @@ const identity_1 = require("../heart/identity");
13
13
  const tasks_1 = require("../repertoire/tasks");
14
14
  const coding_1 = require("../repertoire/coding");
15
15
  const session_activity_1 = require("../heart/session-activity");
16
+ const socket_client_1 = require("../heart/daemon/socket-client");
16
17
  const active_work_1 = require("../heart/active-work");
17
18
  const delegation_1 = require("../heart/delegation");
18
19
  const target_resolution_1 = require("../heart/target-resolution");
@@ -440,6 +441,14 @@ async function handleInboundTurn(input) {
440
441
  friendId: resolvedContext.friend.id,
441
442
  },
442
443
  });
444
+ // DRY cross-session awareness: notify inner dialog that activity happened on another channel
445
+ // Inner dialog's next checkpoint will include this session's state
446
+ if (input.channel !== "inner") {
447
+ try {
448
+ (0, socket_client_1.requestInnerWake)((0, identity_1.getAgentName)()).catch(/* v8 ignore next */ () => { });
449
+ }
450
+ catch { /* getAgentName may fail in test environments */ }
451
+ }
443
452
  return {
444
453
  resolvedContext,
445
454
  gateResult,
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ // Shared turn runner for non-interactive senses (MCP, future senses).
3
+ // Follows the CLI pattern: resolves context, constructs InboundTurnInput,
4
+ // calls handleInboundTurn, collects response text, detects ponder deferral.
5
+ //
6
+ // Does NOT refactor CLI — CLI is stable with 2280+ tests. This is a new
7
+ // code path for new senses that follows the same pipeline pattern.
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.runSenseTurn = runSenseTurn;
43
+ const os = __importStar(require("os"));
44
+ const path = __importStar(require("path"));
45
+ const core_1 = require("../heart/core");
46
+ const identity_1 = require("../heart/identity");
47
+ const config_1 = require("../heart/config");
48
+ const context_1 = require("../mind/context");
49
+ const prompt_1 = require("../mind/prompt");
50
+ const channel_1 = require("../mind/friends/channel");
51
+ const resolver_1 = require("../mind/friends/resolver");
52
+ const store_file_1 = require("../mind/friends/store-file");
53
+ const pending_1 = require("../mind/pending");
54
+ const context_2 = require("../mind/context");
55
+ const tokens_1 = require("../mind/friends/tokens");
56
+ const trust_gate_1 = require("./trust-gate");
57
+ const pipeline_1 = require("./pipeline");
58
+ const runtime_1 = require("../nerves/runtime");
59
+ const RESPONSE_CAP = 50_000;
60
+ /**
61
+ * Run a single agent turn through the inbound pipeline.
62
+ * Caller provides channel, session key, friend, and message;
63
+ * this function handles all pipeline wiring.
64
+ */
65
+ async function runSenseTurn(options) {
66
+ const { agentName, channel, sessionKey, friendId, userMessage } = options;
67
+ (0, runtime_1.emitNervesEvent)({
68
+ component: "senses",
69
+ event: "senses.shared_turn_start",
70
+ message: "shared turn runner starting",
71
+ meta: { agentName, channel, sessionKey, friendId },
72
+ });
73
+ // Resolve context
74
+ const agentRoot = (0, identity_1.getAgentRoot)(agentName);
75
+ const friendsPath = path.join(agentRoot, "friends");
76
+ const friendStore = new store_file_1.FileFriendStore(friendsPath);
77
+ const capabilities = (0, channel_1.getChannelCapabilities)(channel);
78
+ // If friendId looks like a UUID, look up the friend record directly and use its identity.
79
+ // Otherwise, resolve as a local user (same pattern as CLI sense).
80
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-/.test(friendId);
81
+ let resolverParams;
82
+ if (isUuid) {
83
+ const existingFriend = await friendStore.get(friendId);
84
+ if (existingFriend) {
85
+ // Use the friend's first external ID for resolver context
86
+ const ext = existingFriend.externalIds?.[0];
87
+ resolverParams = {
88
+ provider: (ext?.provider ?? "local"),
89
+ externalId: ext?.externalId ?? friendId,
90
+ displayName: existingFriend.name ?? friendId,
91
+ channel,
92
+ };
93
+ }
94
+ else {
95
+ resolverParams = { provider: "local", externalId: friendId, displayName: friendId, channel };
96
+ }
97
+ }
98
+ else {
99
+ // Treat as local user identity (username@hostname pattern)
100
+ const username = os.userInfo().username;
101
+ const hostname = os.hostname();
102
+ resolverParams = { provider: "local", externalId: `${username}@${hostname}`, displayName: username, channel };
103
+ }
104
+ const resolver = new resolver_1.FriendResolver(friendStore, resolverParams);
105
+ // Session path and loading
106
+ const sessPath = (0, config_1.sessionPath)(friendId, channel, sessionKey);
107
+ const existing = (0, context_1.loadSession)(sessPath);
108
+ let sessionState = existing?.state;
109
+ const sessionMessages = existing?.messages && existing.messages.length > 0
110
+ ? existing.messages
111
+ : [{ role: "system", content: await (0, prompt_1.buildSystem)(channel, {}, undefined) }];
112
+ // Pending dir
113
+ const pendingDir = (0, pending_1.getPendingDir)(agentName, friendId, channel, sessionKey);
114
+ // Accumulate response text via callbacks
115
+ let responseText = "";
116
+ /* v8 ignore start — no-op callback stubs; only onTextChunk does real work (covered via mock) */
117
+ const callbacks = {
118
+ onModelStart: () => { },
119
+ onModelStreamStart: () => { },
120
+ onTextChunk: (chunk) => { responseText += chunk; },
121
+ onReasoningChunk: () => { },
122
+ onToolStart: () => { },
123
+ onToolEnd: () => { },
124
+ onError: () => { },
125
+ };
126
+ /* v8 ignore stop */
127
+ // Run the pipeline
128
+ const result = await (0, pipeline_1.handleInboundTurn)({
129
+ channel,
130
+ sessionKey,
131
+ capabilities,
132
+ messages: [{ role: "user", content: userMessage }],
133
+ callbacks,
134
+ /* v8 ignore start — delegation wrappers; pipeline integration tested separately */
135
+ friendResolver: { resolve: () => resolver.resolve() },
136
+ sessionLoader: {
137
+ loadOrCreate: () => Promise.resolve({
138
+ messages: sessionMessages,
139
+ sessionPath: sessPath,
140
+ state: sessionState,
141
+ }),
142
+ },
143
+ /* v8 ignore stop */
144
+ pendingDir,
145
+ friendStore,
146
+ provider: "local",
147
+ externalId: friendId,
148
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
149
+ drainPending: pending_1.drainPending,
150
+ /* v8 ignore start — delegation wrappers; these just forward to the real functions */
151
+ runAgent: (msgs, cb, ch, sig, opts) => (0, core_1.runAgent)(msgs, cb, ch, sig, opts),
152
+ postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
153
+ (0, context_2.postTurn)(turnMessages, sessionPathArg, usage, hooks, state);
154
+ sessionState = state;
155
+ },
156
+ /* v8 ignore stop */
157
+ accumulateFriendTokens: tokens_1.accumulateFriendTokens,
158
+ });
159
+ // Detect ponder deferral
160
+ const ponderDeferred = result.turnOutcome === "pondered";
161
+ // Build response
162
+ let finalResponse;
163
+ if (ponderDeferred) {
164
+ finalResponse = "the agent is pondering this — check back shortly via check_response.";
165
+ }
166
+ else if (responseText.length === 0) {
167
+ // Agent settled but no text came through callbacks — check session transcript for the settle answer
168
+ const postTurnSession = (0, context_1.loadSession)(sessPath);
169
+ if (postTurnSession?.messages) {
170
+ const lastAssistant = [...postTurnSession.messages].reverse().find(m => m.role === "assistant");
171
+ if (lastAssistant && typeof lastAssistant.content === "string" && lastAssistant.content.trim()) {
172
+ finalResponse = lastAssistant.content;
173
+ }
174
+ else {
175
+ finalResponse = "(agent responded but response was empty)";
176
+ }
177
+ }
178
+ else {
179
+ finalResponse = "(agent responded but response was empty)";
180
+ }
181
+ }
182
+ else {
183
+ finalResponse = responseText;
184
+ }
185
+ // Cap response length
186
+ if (finalResponse.length > RESPONSE_CAP) {
187
+ finalResponse = finalResponse.slice(0, RESPONSE_CAP) + "\n\n[truncated — response exceeded 50K characters]";
188
+ }
189
+ (0, runtime_1.emitNervesEvent)({
190
+ component: "senses",
191
+ event: "senses.shared_turn_end",
192
+ message: "shared turn runner complete",
193
+ meta: { agentName, channel, sessionKey, friendId, ponderDeferred, responseLength: finalResponse.length },
194
+ });
195
+ return { response: finalResponse, ponderDeferred };
196
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.142",
3
+ "version": "0.1.0-alpha.145",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",