@ouro.bot/cli 0.1.0-alpha.15 → 0.1.0-alpha.150

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 (150) hide show
  1. package/AdoptionSpecialist.ouro/psyche/SOUL.md +2 -2
  2. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  3. package/README.md +151 -205
  4. package/changelog.json +985 -0
  5. package/dist/heart/active-work.js +633 -0
  6. package/dist/heart/bridges/manager.js +358 -0
  7. package/dist/heart/bridges/state-machine.js +135 -0
  8. package/dist/heart/bridges/store.js +123 -0
  9. package/dist/heart/commitments.js +105 -0
  10. package/dist/heart/config.js +66 -21
  11. package/dist/heart/core.js +645 -112
  12. package/dist/heart/cross-chat-delivery.js +146 -0
  13. package/dist/heart/daemon/agent-discovery.js +81 -0
  14. package/dist/heart/daemon/agent-service.js +360 -0
  15. package/dist/heart/daemon/auth-flow.js +457 -0
  16. package/dist/heart/daemon/cadence.js +70 -0
  17. package/dist/heart/daemon/daemon-cli.js +2301 -205
  18. package/dist/heart/daemon/daemon-entry.js +131 -4
  19. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  20. package/dist/heart/daemon/daemon-tombstone.js +106 -0
  21. package/dist/heart/daemon/daemon.js +372 -3
  22. package/dist/heart/daemon/habit-migration.js +181 -0
  23. package/dist/heart/daemon/habit-parser.js +119 -0
  24. package/dist/heart/daemon/habit-scheduler.js +252 -0
  25. package/dist/heart/daemon/hatch-animation.js +10 -3
  26. package/dist/heart/daemon/hatch-flow.js +28 -111
  27. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  28. package/dist/heart/daemon/inner-status.js +89 -0
  29. package/dist/heart/daemon/launchd.js +159 -0
  30. package/dist/heart/daemon/log-tailer.js +4 -3
  31. package/dist/heart/daemon/mcp-server.js +591 -0
  32. package/dist/heart/daemon/message-router.js +17 -8
  33. package/dist/heart/daemon/os-cron-deps.js +134 -0
  34. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  35. package/dist/heart/daemon/ouro-path-installer.js +78 -34
  36. package/dist/heart/daemon/ouro-version-manager.js +171 -0
  37. package/dist/heart/daemon/process-manager.js +75 -0
  38. package/dist/heart/daemon/run-hooks.js +37 -0
  39. package/dist/heart/daemon/runtime-logging.js +58 -15
  40. package/dist/heart/daemon/runtime-metadata.js +219 -0
  41. package/dist/heart/daemon/runtime-mode.js +67 -0
  42. package/dist/heart/daemon/sense-manager.js +50 -2
  43. package/dist/heart/daemon/session-id-resolver.js +131 -0
  44. package/dist/heart/daemon/skill-management-installer.js +94 -0
  45. package/dist/heart/daemon/socket-client.js +202 -0
  46. package/dist/heart/daemon/specialist-orchestrator.js +2 -2
  47. package/dist/heart/daemon/specialist-prompt.js +10 -7
  48. package/dist/heart/daemon/specialist-tools.js +53 -4
  49. package/dist/heart/daemon/staged-restart.js +114 -0
  50. package/dist/heart/daemon/task-scheduler.js +3 -25
  51. package/dist/heart/daemon/thoughts.js +507 -0
  52. package/dist/heart/daemon/update-checker.js +111 -0
  53. package/dist/heart/daemon/update-hooks.js +138 -0
  54. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  55. package/dist/heart/delegation.js +62 -0
  56. package/dist/heart/identity.js +64 -21
  57. package/dist/heart/kicks.js +2 -20
  58. package/dist/heart/model-capabilities.js +48 -0
  59. package/dist/heart/obligations.js +197 -0
  60. package/dist/heart/progress-story.js +42 -0
  61. package/dist/heart/provider-failover.js +88 -0
  62. package/dist/heart/provider-ping.js +159 -0
  63. package/dist/heart/providers/anthropic-token.js +163 -0
  64. package/dist/heart/providers/anthropic.js +195 -34
  65. package/dist/heart/providers/azure.js +115 -9
  66. package/dist/heart/providers/github-copilot.js +157 -0
  67. package/dist/heart/providers/minimax.js +33 -3
  68. package/dist/heart/providers/openai-codex.js +49 -14
  69. package/dist/heart/session-activity.js +173 -0
  70. package/dist/heart/session-recall.js +216 -0
  71. package/dist/heart/streaming.js +117 -33
  72. package/dist/heart/target-resolution.js +127 -0
  73. package/dist/heart/tool-loop.js +200 -0
  74. package/dist/heart/turn-coordinator.js +28 -0
  75. package/dist/mind/associative-recall.js +122 -32
  76. package/dist/mind/bundle-manifest.js +14 -0
  77. package/dist/mind/context.js +125 -14
  78. package/dist/mind/{memory.js → diary.js} +70 -56
  79. package/dist/mind/first-impressions.js +16 -2
  80. package/dist/mind/friends/channel.js +44 -0
  81. package/dist/mind/friends/group-context.js +144 -0
  82. package/dist/mind/friends/store-file.js +19 -0
  83. package/dist/mind/friends/trust-explanation.js +74 -0
  84. package/dist/mind/friends/types.js +8 -0
  85. package/dist/mind/journal-index.js +161 -0
  86. package/dist/mind/obligation-steering.js +221 -0
  87. package/dist/mind/obligations.js +134 -0
  88. package/dist/mind/pending.js +76 -9
  89. package/dist/mind/phrases.js +1 -0
  90. package/dist/mind/prompt.js +607 -88
  91. package/dist/mind/token-estimate.js +8 -12
  92. package/dist/nerves/cli-logging.js +15 -2
  93. package/dist/nerves/coverage/run-artifacts.js +1 -1
  94. package/dist/nerves/index.js +59 -2
  95. package/dist/nerves/runtime.js +5 -1
  96. package/dist/repertoire/ado-client.js +4 -2
  97. package/dist/repertoire/ado-semantic.js +6 -0
  98. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  99. package/dist/repertoire/coding/context-pack.js +254 -0
  100. package/dist/repertoire/coding/feedback.js +301 -0
  101. package/dist/repertoire/coding/index.js +4 -1
  102. package/dist/repertoire/coding/manager.js +210 -4
  103. package/dist/repertoire/coding/spawner.js +58 -12
  104. package/dist/repertoire/coding/tools.js +176 -4
  105. package/dist/repertoire/data/ado-endpoints.json +188 -0
  106. package/dist/repertoire/guardrails.js +290 -0
  107. package/dist/repertoire/mcp-client.js +254 -0
  108. package/dist/repertoire/mcp-manager.js +198 -0
  109. package/dist/repertoire/skills.js +14 -23
  110. package/dist/repertoire/tasks/board.js +12 -0
  111. package/dist/repertoire/tasks/index.js +23 -9
  112. package/dist/repertoire/tasks/parser.js +0 -2
  113. package/dist/repertoire/tasks/transitions.js +1 -20
  114. package/dist/repertoire/tools-base.js +990 -273
  115. package/dist/repertoire/tools-bluebubbles.js +94 -0
  116. package/dist/repertoire/tools-github.js +1 -6
  117. package/dist/repertoire/tools-teams.js +65 -59
  118. package/dist/repertoire/tools.js +281 -91
  119. package/dist/scripts/claude-code-hook.js +41 -0
  120. package/dist/scripts/claude-code-stop-hook.js +47 -0
  121. package/dist/senses/attention-queue.js +97 -0
  122. package/dist/senses/bluebubbles-client.js +258 -5
  123. package/dist/senses/bluebubbles-entry.js +2 -0
  124. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  125. package/dist/senses/bluebubbles-media.js +339 -0
  126. package/dist/senses/bluebubbles-model.js +12 -4
  127. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  128. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  129. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  130. package/dist/senses/bluebubbles.js +981 -45
  131. package/dist/senses/cli-entry.js +1 -1
  132. package/dist/senses/cli-layout.js +187 -0
  133. package/dist/senses/cli.js +374 -131
  134. package/dist/senses/contextual-heartbeat.js +106 -0
  135. package/dist/senses/continuity.js +94 -0
  136. package/dist/senses/debug-activity.js +154 -0
  137. package/dist/senses/inner-dialog-worker.js +112 -19
  138. package/dist/senses/inner-dialog.js +621 -84
  139. package/dist/senses/pipeline.js +463 -0
  140. package/dist/senses/shared-turn.js +196 -0
  141. package/dist/senses/surface-tool.js +95 -0
  142. package/dist/senses/teams.js +796 -141
  143. package/dist/senses/trust-gate.js +112 -2
  144. package/package.json +12 -4
  145. package/skills/configure-dev-tools.md +81 -0
  146. package/subagents/README.md +4 -70
  147. package/dist/heart/daemon/subagent-installer.js +0 -134
  148. package/subagents/work-doer.md +0 -233
  149. package/subagents/work-merger.md +0 -624
  150. package/subagents/work-planner.md +0 -373
@@ -0,0 +1,463 @@
1
+ "use strict";
2
+ // Shared per-turn pipeline for all senses.
3
+ // Senses are thin transport adapters; this module owns the common lifecycle:
4
+ // resolve friend -> trust gate -> load session -> drain pending -> runAgent -> postTurn -> token accumulation.
5
+ //
6
+ // Transport-level concerns (BB API calls, Teams cards, readline) stay in sense adapters.
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.handleInboundTurn = handleInboundTurn;
9
+ const runtime_1 = require("../nerves/runtime");
10
+ const continuity_1 = require("./continuity");
11
+ const manager_1 = require("../heart/bridges/manager");
12
+ const identity_1 = require("../heart/identity");
13
+ const tasks_1 = require("../repertoire/tasks");
14
+ const coding_1 = require("../repertoire/coding");
15
+ const session_activity_1 = require("../heart/session-activity");
16
+ const socket_client_1 = require("../heart/daemon/socket-client");
17
+ const active_work_1 = require("../heart/active-work");
18
+ const delegation_1 = require("../heart/delegation");
19
+ const target_resolution_1 = require("../heart/target-resolution");
20
+ const thoughts_1 = require("../heart/daemon/thoughts");
21
+ const pending_1 = require("../mind/pending");
22
+ const obligations_1 = require("../heart/obligations");
23
+ const obligations_2 = require("../mind/obligations");
24
+ const provider_failover_1 = require("../heart/provider-failover");
25
+ const provider_ping_1 = require("../heart/provider-ping");
26
+ const auth_flow_1 = require("../heart/daemon/auth-flow");
27
+ function emptyTaskBoard() {
28
+ return {
29
+ compact: "",
30
+ full: "",
31
+ byStatus: {
32
+ drafting: [],
33
+ processing: [],
34
+ validating: [],
35
+ collaborating: [],
36
+ paused: [],
37
+ blocked: [],
38
+ done: [],
39
+ },
40
+ actionRequired: [],
41
+ unresolvedDependencies: [],
42
+ activeSessions: [],
43
+ activeBridges: [],
44
+ };
45
+ }
46
+ function isLiveCodingSessionStatus(status) {
47
+ return status === "spawning"
48
+ || status === "running"
49
+ || status === "waiting_input"
50
+ || status === "stalled";
51
+ }
52
+ function prependTurnSections(message, sections) {
53
+ if (message.role !== "user" || sections.length === 0)
54
+ return message;
55
+ const prefix = sections.join("\n\n");
56
+ if (typeof message.content === "string") {
57
+ return {
58
+ ...message,
59
+ content: `${prefix}\n\n${message.content}`,
60
+ };
61
+ }
62
+ return {
63
+ ...message,
64
+ content: [
65
+ { type: "text", text: `${prefix}\n\n` },
66
+ ...message.content,
67
+ ],
68
+ };
69
+ }
70
+ function readInnerWorkState() {
71
+ const defaultJob = {
72
+ status: "idle",
73
+ content: null,
74
+ origin: null,
75
+ mode: "reflect",
76
+ obligationStatus: null,
77
+ surfacedResult: null,
78
+ queuedAt: null,
79
+ startedAt: null,
80
+ surfacedAt: null,
81
+ };
82
+ try {
83
+ const agentRoot = (0, identity_1.getAgentRoot)();
84
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
85
+ const sessionPath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
86
+ const { pendingMessages, turns, runtimeState } = (0, thoughts_1.readInnerDialogRawData)(sessionPath, pendingDir);
87
+ const dialogStatus = (0, thoughts_1.deriveInnerDialogStatus)(pendingMessages, turns, runtimeState);
88
+ const job = (0, thoughts_1.deriveInnerJob)(pendingMessages, turns, runtimeState);
89
+ // Derive obligationPending from both the pending message field and the obligation store
90
+ const storeObligationPending = (0, obligations_1.readPendingObligations)(agentRoot).length > 0;
91
+ return {
92
+ status: dialogStatus.processing === "started" ? "running" : "idle",
93
+ hasPending: dialogStatus.queue !== "clear",
94
+ origin: dialogStatus.origin,
95
+ contentSnippet: dialogStatus.contentSnippet,
96
+ obligationPending: dialogStatus.obligationPending || storeObligationPending,
97
+ job,
98
+ };
99
+ }
100
+ catch {
101
+ return {
102
+ status: "idle",
103
+ hasPending: false,
104
+ job: defaultJob,
105
+ };
106
+ }
107
+ }
108
+ // ── Pipeline ──────────────────────────────────────────────────────
109
+ async function handleInboundTurn(input) {
110
+ // Step 0: Check for pending failover reply
111
+ if (input.failoverState?.pending) {
112
+ const userText = input.messages
113
+ .filter((m) => m.role === "user")
114
+ .map((m) => typeof m.content === "string" ? m.content : /* v8 ignore next -- defensive: multipart content fallback @preserve */ "")
115
+ .join(" ")
116
+ .trim();
117
+ const pendingContext = input.failoverState.pending;
118
+ const failoverAction = (0, provider_failover_1.handleFailoverReply)(userText, pendingContext);
119
+ const failoverAgentName = pendingContext.agentName;
120
+ input.failoverState.pending = null; // always clear before acting
121
+ if (failoverAction.action === "switch") {
122
+ let switchSucceeded = false;
123
+ try {
124
+ (0, auth_flow_1.writeAgentProviderSelection)(failoverAgentName, failoverAction.provider);
125
+ switchSucceeded = true;
126
+ /* v8 ignore start -- defensive: write failure during provider switch @preserve */
127
+ }
128
+ catch (switchError) {
129
+ (0, runtime_1.emitNervesEvent)({
130
+ level: "error",
131
+ component: "senses",
132
+ event: "senses.failover_switch_error",
133
+ message: `failed to switch provider to ${failoverAction.provider}`,
134
+ meta: { agentName: failoverAgentName, provider: failoverAction.provider, error: switchError instanceof Error ? switchError.message : String(switchError) },
135
+ });
136
+ }
137
+ /* v8 ignore stop */
138
+ /* v8 ignore next -- false branch: write-failure fallthrough @preserve */
139
+ if (switchSucceeded) {
140
+ (0, runtime_1.emitNervesEvent)({
141
+ component: "senses",
142
+ event: "senses.failover_switch",
143
+ message: `switched provider to ${failoverAction.provider} via failover`,
144
+ meta: { agentName: failoverAgentName, provider: failoverAction.provider },
145
+ });
146
+ // Replace "switch to <provider>" with a context message for the agent.
147
+ // The session already has the user's original question from the failed turn.
148
+ // The agent needs to know what happened so it can respond appropriately.
149
+ const newProviderSecrets = (() => {
150
+ try {
151
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(failoverAgentName);
152
+ const cfg = secrets.providers[failoverAction.provider];
153
+ return cfg?.model ?? cfg?.modelName ?? "";
154
+ /* v8 ignore next 2 -- defensive: secrets read failure @preserve */
155
+ }
156
+ catch {
157
+ return "";
158
+ }
159
+ })();
160
+ const newProviderLabel = newProviderSecrets ? `${failoverAction.provider} (${newProviderSecrets})` : failoverAction.provider;
161
+ input.messages = [{
162
+ role: "user",
163
+ content: `[provider switch: ${pendingContext.errorSummary}. switched to ${newProviderLabel}. your conversation history is intact — respond to the user's last message.]`,
164
+ }];
165
+ input.switchedProvider = failoverAction.provider;
166
+ }
167
+ // Switch failed OR succeeded — either way, fall through to normal processing.
168
+ }
169
+ }
170
+ // Step 1: Resolve friend
171
+ const resolvedContext = await input.friendResolver.resolve();
172
+ (0, runtime_1.emitNervesEvent)({
173
+ component: "senses",
174
+ event: "senses.pipeline_start",
175
+ message: "inbound turn pipeline started",
176
+ meta: {
177
+ channel: input.channel,
178
+ friendId: resolvedContext.friend.id,
179
+ senseType: input.capabilities.senseType,
180
+ },
181
+ });
182
+ // Step 2: Trust gate
183
+ const gateInput = {
184
+ friend: resolvedContext.friend,
185
+ provider: input.provider ?? "local",
186
+ externalId: input.externalId ?? "",
187
+ tenantId: input.tenantId,
188
+ channel: input.channel,
189
+ senseType: input.capabilities.senseType,
190
+ isGroupChat: input.isGroupChat ?? false,
191
+ groupHasFamilyMember: input.groupHasFamilyMember ?? false,
192
+ hasExistingGroupWithFamily: input.hasExistingGroupWithFamily ?? false,
193
+ };
194
+ const gateResult = input.enforceTrustGate(gateInput);
195
+ // Gate rejection: return early, no agent turn
196
+ if (!gateResult.allowed) {
197
+ (0, runtime_1.emitNervesEvent)({
198
+ component: "senses",
199
+ event: "senses.pipeline_gate_reject",
200
+ message: "trust gate rejected inbound turn",
201
+ meta: {
202
+ channel: input.channel,
203
+ friendId: resolvedContext.friend.id,
204
+ reason: gateResult.reason,
205
+ },
206
+ });
207
+ return {
208
+ resolvedContext,
209
+ gateResult,
210
+ };
211
+ }
212
+ // Step 3: Load/create session
213
+ const session = await input.sessionLoader.loadOrCreate();
214
+ const sessionMessages = session.messages;
215
+ let mustResolveBeforeHandoff = (0, continuity_1.resolveMustResolveBeforeHandoff)(session.state?.mustResolveBeforeHandoff === true, input.continuityIngressTexts);
216
+ const lastFriendActivityAt = input.channel === "inner"
217
+ ? session.state?.lastFriendActivityAt
218
+ : new Date().toISOString();
219
+ const currentObligation = input.continuityIngressTexts
220
+ ?.map((text) => text.trim())
221
+ .filter((text) => text.length > 0)
222
+ .at(-1);
223
+ const currentSession = {
224
+ friendId: resolvedContext.friend.id,
225
+ channel: input.channel,
226
+ key: input.sessionKey ?? "session",
227
+ sessionPath: session.sessionPath,
228
+ };
229
+ const activeBridges = (0, manager_1.createBridgeManager)().findBridgesForSession({
230
+ friendId: currentSession.friendId,
231
+ channel: currentSession.channel,
232
+ key: currentSession.key,
233
+ });
234
+ const bridgeContext = (0, manager_1.formatBridgeContext)(activeBridges) || undefined;
235
+ let sessionActivity = [];
236
+ try {
237
+ const agentRoot = (0, identity_1.getAgentRoot)();
238
+ sessionActivity = (0, session_activity_1.listSessionActivity)({
239
+ sessionsDir: `${agentRoot}/state/sessions`,
240
+ friendsDir: `${agentRoot}/friends`,
241
+ agentName: (0, identity_1.getAgentName)(),
242
+ currentSession: {
243
+ friendId: currentSession.friendId,
244
+ channel: currentSession.channel,
245
+ key: currentSession.key,
246
+ },
247
+ });
248
+ }
249
+ catch {
250
+ sessionActivity = [];
251
+ }
252
+ let targetCandidates = [];
253
+ try {
254
+ if (input.channel !== "inner") {
255
+ const agentRoot = (0, identity_1.getAgentRoot)();
256
+ targetCandidates = await (0, target_resolution_1.listTargetSessionCandidates)({
257
+ sessionsDir: `${agentRoot}/state/sessions`,
258
+ friendsDir: `${agentRoot}/friends`,
259
+ agentName: (0, identity_1.getAgentName)(),
260
+ currentSession: {
261
+ friendId: currentSession.friendId,
262
+ channel: currentSession.channel,
263
+ key: currentSession.key,
264
+ },
265
+ friendStore: input.friendStore,
266
+ });
267
+ }
268
+ }
269
+ catch {
270
+ targetCandidates = [];
271
+ }
272
+ let pendingObligations = [];
273
+ try {
274
+ pendingObligations = (0, obligations_1.readPendingObligations)((0, identity_1.getAgentRoot)());
275
+ }
276
+ catch {
277
+ pendingObligations = [];
278
+ }
279
+ let codingSessions = [];
280
+ let otherCodingSessions = [];
281
+ try {
282
+ const liveCodingSessions = (0, coding_1.getCodingSessionManager)()
283
+ .listSessions()
284
+ .filter((session) => isLiveCodingSessionStatus(session.status) && Boolean(session.originSession));
285
+ codingSessions = liveCodingSessions.filter((session) => session.originSession?.friendId === currentSession.friendId
286
+ && session.originSession.channel === currentSession.channel
287
+ && session.originSession.key === currentSession.key);
288
+ otherCodingSessions = liveCodingSessions.filter((session) => !(session.originSession?.friendId === currentSession.friendId
289
+ && session.originSession.channel === currentSession.channel
290
+ && session.originSession.key === currentSession.key));
291
+ }
292
+ catch {
293
+ codingSessions = [];
294
+ otherCodingSessions = [];
295
+ }
296
+ const activeWorkFrame = (0, active_work_1.buildActiveWorkFrame)({
297
+ currentSession,
298
+ currentObligation,
299
+ mustResolveBeforeHandoff,
300
+ inner: readInnerWorkState(),
301
+ bridges: activeBridges,
302
+ codingSessions,
303
+ otherCodingSessions,
304
+ pendingObligations,
305
+ taskBoard: (() => {
306
+ try {
307
+ return (0, tasks_1.getTaskModule)().getBoard();
308
+ }
309
+ catch {
310
+ return emptyTaskBoard();
311
+ }
312
+ })(),
313
+ friendActivity: sessionActivity,
314
+ targetCandidates,
315
+ innerReturnObligations: (() => {
316
+ try {
317
+ return (0, obligations_2.listActiveObligations)((0, identity_1.getAgentName)());
318
+ }
319
+ catch {
320
+ return [];
321
+ }
322
+ })(),
323
+ });
324
+ const delegationDecision = (0, delegation_1.decideDelegation)({
325
+ channel: input.channel,
326
+ ingressTexts: input.continuityIngressTexts ?? [],
327
+ activeWork: activeWorkFrame,
328
+ mustResolveBeforeHandoff,
329
+ });
330
+ // Step 4: Drain deferred friend returns, then ordinary per-session pending.
331
+ const deferredReturns = input.channel === "inner"
332
+ ? []
333
+ : (input.drainDeferredReturns?.(resolvedContext.friend.id) ?? []);
334
+ const sessionPending = input.drainPending(input.pendingDir);
335
+ const pending = [...deferredReturns, ...sessionPending];
336
+ // Assemble messages: session messages + attention queue + live world-state checkpoint + pending + inbound user messages
337
+ const extraPrefixSections = input.onPendingDrained?.(pending) ?? [];
338
+ const prefixSections = [...extraPrefixSections, (0, active_work_1.formatLiveWorldStateCheckpoint)(activeWorkFrame)];
339
+ if (pending.length > 0) {
340
+ const pendingSection = pending
341
+ .map((msg) => `[pending from ${msg.from}]: ${msg.content}`)
342
+ .join("\n");
343
+ prefixSections.push(`## pending messages\n${pendingSection}`);
344
+ }
345
+ if (input.messages.length > 0) {
346
+ input.messages[0] = prependTurnSections(input.messages[0], prefixSections);
347
+ }
348
+ // Append user messages from the inbound turn
349
+ for (const msg of input.messages) {
350
+ sessionMessages.push(msg);
351
+ }
352
+ // Step 5: runAgent
353
+ const existingToolContext = input.runAgentOptions?.toolContext;
354
+ const runAgentOptions = {
355
+ ...input.runAgentOptions,
356
+ bridgeContext,
357
+ activeWorkFrame,
358
+ delegationDecision,
359
+ currentSessionKey: currentSession.key,
360
+ currentObligation,
361
+ mustResolveBeforeHandoff,
362
+ setMustResolveBeforeHandoff: (value) => {
363
+ mustResolveBeforeHandoff = value;
364
+ },
365
+ toolContext: {
366
+ /* v8 ignore next -- default no-op signin satisfies interface; real signin injected by sense adapter @preserve */
367
+ signin: async () => undefined,
368
+ ...existingToolContext,
369
+ context: resolvedContext,
370
+ friendStore: input.friendStore,
371
+ currentSession,
372
+ activeBridges,
373
+ },
374
+ };
375
+ const result = await input.runAgent(sessionMessages, input.callbacks, input.channel, input.signal, runAgentOptions);
376
+ // Step 5b: Failover on terminal error
377
+ if (result.outcome === "errored" && input.failoverState) {
378
+ try {
379
+ const agentName = (0, identity_1.getAgentName)();
380
+ const agentConfig = (0, identity_1.loadAgentConfig)();
381
+ const currentProvider = agentConfig.provider;
382
+ /* v8 ignore next -- defensive: errorClassification always set when errored @preserve */
383
+ const classification = result.errorClassification ?? "unknown";
384
+ const inventory = await (0, provider_ping_1.runHealthInventory)(agentName, currentProvider);
385
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(agentName);
386
+ const providerModels = {};
387
+ for (const [p, cfg] of Object.entries(secrets.providers)) {
388
+ const model = cfg.model ?? cfg.modelName;
389
+ if (typeof model === "string" && model)
390
+ providerModels[p] = model;
391
+ }
392
+ /* v8 ignore next -- defensive: current provider always in secrets @preserve */
393
+ const currentModel = providerModels[currentProvider] ?? "";
394
+ const failoverContext = (0, provider_failover_1.buildFailoverContext)(
395
+ /* v8 ignore next -- defensive: error always set when errored @preserve */
396
+ result.error?.message ?? "unknown error", classification, currentProvider, currentModel, agentName, inventory, providerModels);
397
+ input.failoverState.pending = failoverContext;
398
+ input.postTurn(sessionMessages, session.sessionPath, result.usage);
399
+ return {
400
+ resolvedContext,
401
+ gateResult,
402
+ usage: result.usage,
403
+ turnOutcome: result.outcome,
404
+ sessionPath: session.sessionPath,
405
+ messages: sessionMessages,
406
+ drainedPending: pending,
407
+ failoverMessage: failoverContext.userMessage,
408
+ };
409
+ /* v8 ignore start -- failover catch: tested via pipeline failover sequence throws test but v8 under-reports catch coverage @preserve */
410
+ }
411
+ catch (failoverError) {
412
+ (0, runtime_1.emitNervesEvent)({
413
+ level: "warn",
414
+ component: "senses",
415
+ event: "senses.failover_error",
416
+ message: "failover sequence failed, falling through",
417
+ meta: { error: failoverError instanceof Error ? failoverError.message : String(failoverError) },
418
+ });
419
+ }
420
+ /* v8 ignore stop */
421
+ }
422
+ // Step 6: postTurn
423
+ const continuingState = {
424
+ ...(mustResolveBeforeHandoff ? { mustResolveBeforeHandoff: true } : {}),
425
+ ...(typeof lastFriendActivityAt === "string" ? { lastFriendActivityAt } : {}),
426
+ };
427
+ const nextState = result.outcome === "settled" || result.outcome === "blocked" || result.outcome === "superseded" || result.outcome === "observed"
428
+ ? (typeof lastFriendActivityAt === "string"
429
+ ? { lastFriendActivityAt }
430
+ : undefined)
431
+ : (Object.keys(continuingState).length > 0 ? continuingState : undefined);
432
+ input.postTurn(sessionMessages, session.sessionPath, result.usage, undefined, nextState);
433
+ // Step 7: Token accumulation
434
+ await input.accumulateFriendTokens(input.friendStore, resolvedContext.friend.id, result.usage);
435
+ (0, runtime_1.emitNervesEvent)({
436
+ component: "senses",
437
+ event: "senses.pipeline_end",
438
+ message: "inbound turn pipeline completed",
439
+ meta: {
440
+ channel: input.channel,
441
+ friendId: resolvedContext.friend.id,
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
+ }
452
+ return {
453
+ resolvedContext,
454
+ gateResult,
455
+ usage: result.usage,
456
+ turnOutcome: result.outcome,
457
+ completion: result.completion,
458
+ sessionPath: session.sessionPath,
459
+ messages: sessionMessages,
460
+ drainedPending: pending,
461
+ ...(input.switchedProvider ? { switchedProvider: input.switchedProvider } : {}),
462
+ };
463
+ }
@@ -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
+ }