@ouro.bot/cli 0.1.0-alpha.2 → 0.1.0-alpha.200

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 (215) hide show
  1. package/README.md +187 -191
  2. package/SerpentGuide.ouro/agent.json +82 -0
  3. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +6 -3
  4. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +2 -2
  5. package/assets/ouroboros.png +0 -0
  6. package/changelog.json +1248 -0
  7. package/dist/heart/active-work.js +832 -0
  8. package/dist/heart/bridges/manager.js +358 -0
  9. package/dist/heart/bridges/state-machine.js +135 -0
  10. package/dist/heart/bridges/store.js +123 -0
  11. package/dist/heart/cares.js +179 -0
  12. package/dist/heart/commitments.js +111 -0
  13. package/dist/heart/config.js +114 -28
  14. package/dist/heart/core.js +689 -120
  15. package/dist/heart/cross-chat-delivery.js +146 -0
  16. package/dist/heart/daemon/agent-config-check.js +149 -0
  17. package/dist/heart/daemon/agent-discovery.js +81 -0
  18. package/dist/heart/daemon/agent-service.js +360 -0
  19. package/dist/heart/daemon/auth-flow.js +477 -0
  20. package/dist/heart/daemon/cadence.js +70 -0
  21. package/dist/heart/daemon/daemon-cli.js +2897 -193
  22. package/dist/heart/daemon/daemon-entry.js +194 -8
  23. package/dist/heart/daemon/daemon-health.js +136 -0
  24. package/dist/heart/daemon/daemon-runtime-sync.js +231 -0
  25. package/dist/heart/daemon/daemon-tombstone.js +123 -0
  26. package/dist/heart/daemon/daemon.js +488 -13
  27. package/dist/heart/daemon/habit-migration.js +181 -0
  28. package/dist/heart/daemon/habit-parser.js +119 -0
  29. package/dist/heart/daemon/habit-scheduler.js +369 -0
  30. package/dist/heart/daemon/hatch-animation.js +35 -0
  31. package/dist/heart/daemon/hatch-flow.js +32 -122
  32. package/dist/heart/daemon/hatch-specialist.js +7 -2
  33. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  34. package/dist/heart/daemon/hooks/bundle-meta.js +206 -0
  35. package/dist/heart/daemon/inner-status.js +89 -0
  36. package/dist/heart/daemon/launchd.js +165 -0
  37. package/dist/heart/daemon/log-tailer.js +4 -3
  38. package/dist/heart/daemon/mcp-server.js +591 -0
  39. package/dist/heart/daemon/message-router.js +17 -8
  40. package/dist/heart/daemon/os-cron-deps.js +134 -0
  41. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  42. package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
  43. package/dist/heart/daemon/ouro-path-installer.js +221 -0
  44. package/dist/heart/daemon/ouro-uti.js +11 -2
  45. package/dist/heart/daemon/ouro-version-manager.js +171 -0
  46. package/dist/heart/daemon/outlook-http.js +439 -0
  47. package/dist/heart/daemon/outlook-read.js +1618 -0
  48. package/dist/heart/daemon/outlook-render.js +1032 -0
  49. package/dist/heart/daemon/outlook-types.js +7 -0
  50. package/dist/heart/daemon/outlook-view.js +194 -0
  51. package/dist/heart/daemon/process-manager.js +94 -1
  52. package/dist/heart/daemon/run-hooks.js +39 -0
  53. package/dist/heart/daemon/runtime-logging.js +61 -14
  54. package/dist/heart/daemon/runtime-metadata.js +219 -0
  55. package/dist/heart/daemon/runtime-mode.js +67 -0
  56. package/dist/heart/daemon/safe-mode.js +161 -0
  57. package/dist/heart/daemon/sense-manager.js +314 -0
  58. package/dist/heart/daemon/session-id-resolver.js +131 -0
  59. package/dist/heart/daemon/skill-management-installer.js +94 -0
  60. package/dist/heart/daemon/socket-client.js +202 -0
  61. package/dist/heart/daemon/specialist-orchestrator.js +129 -0
  62. package/dist/heart/daemon/specialist-prompt.js +101 -0
  63. package/dist/heart/daemon/specialist-tools.js +283 -0
  64. package/dist/heart/daemon/staged-restart.js +138 -0
  65. package/dist/heart/daemon/task-scheduler.js +3 -25
  66. package/dist/heart/daemon/thoughts.js +507 -0
  67. package/dist/heart/daemon/update-checker.js +111 -0
  68. package/dist/heart/daemon/update-hooks.js +138 -0
  69. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  70. package/dist/heart/delegation.js +62 -0
  71. package/dist/heart/identity.js +220 -52
  72. package/dist/heart/intentions.js +173 -0
  73. package/dist/heart/kicks.js +2 -20
  74. package/dist/heart/migrate-config.js +127 -0
  75. package/dist/heart/model-capabilities.js +48 -0
  76. package/dist/heart/obligations.js +225 -0
  77. package/dist/heart/presence.js +185 -0
  78. package/dist/heart/progress-story.js +42 -0
  79. package/dist/heart/provider-failover.js +88 -0
  80. package/dist/heart/provider-ping.js +178 -0
  81. package/dist/heart/providers/anthropic-token.js +163 -0
  82. package/dist/heart/providers/anthropic.js +199 -36
  83. package/dist/heart/providers/azure.js +116 -10
  84. package/dist/heart/providers/github-copilot.js +157 -0
  85. package/dist/heart/providers/minimax.js +33 -3
  86. package/dist/heart/providers/openai-codex.js +51 -16
  87. package/dist/heart/sense-truth.js +61 -0
  88. package/dist/heart/session-activity.js +172 -0
  89. package/dist/heart/session-recall.js +216 -0
  90. package/dist/heart/start-of-turn-packet.js +232 -0
  91. package/dist/heart/streaming.js +120 -33
  92. package/dist/heart/sync.js +217 -0
  93. package/dist/heart/target-resolution.js +127 -0
  94. package/dist/heart/tempo.js +93 -0
  95. package/dist/heart/temporal-view.js +41 -0
  96. package/dist/heart/tool-activity-callbacks.js +36 -0
  97. package/dist/heart/tool-description.js +135 -0
  98. package/dist/heart/tool-loop.js +200 -0
  99. package/dist/heart/turn-coordinator.js +28 -0
  100. package/dist/mind/associative-recall.js +122 -32
  101. package/dist/mind/bundle-manifest.js +77 -1
  102. package/dist/mind/context.js +125 -14
  103. package/dist/mind/{memory.js → diary.js} +69 -56
  104. package/dist/mind/episodes.js +119 -0
  105. package/dist/mind/file-state.js +179 -0
  106. package/dist/mind/first-impressions.js +16 -2
  107. package/dist/mind/friends/channel.js +64 -0
  108. package/dist/mind/friends/group-context.js +144 -0
  109. package/dist/mind/friends/store-file.js +36 -0
  110. package/dist/mind/friends/trust-explanation.js +74 -0
  111. package/dist/mind/friends/types.js +9 -1
  112. package/dist/mind/journal-index.js +161 -0
  113. package/dist/mind/obligation-steering.js +221 -0
  114. package/dist/mind/obligations.js +138 -0
  115. package/dist/mind/pending.js +76 -9
  116. package/dist/mind/phrases.js +1 -0
  117. package/dist/mind/prompt.js +861 -105
  118. package/dist/mind/scrutiny.js +173 -0
  119. package/dist/mind/token-estimate.js +8 -12
  120. package/dist/nerves/cli-logging.js +15 -2
  121. package/dist/nerves/coverage/audit.js +1 -1
  122. package/dist/nerves/coverage/run-artifacts.js +1 -1
  123. package/dist/nerves/event-buffer.js +111 -0
  124. package/dist/nerves/index.js +63 -3
  125. package/dist/nerves/observation.js +20 -0
  126. package/dist/nerves/runtime.js +5 -1
  127. package/dist/outlook-ui/assets/index-Ck8agNeO.js +61 -0
  128. package/dist/outlook-ui/assets/index-LwChZTgL.css +1 -0
  129. package/dist/outlook-ui/index.html +15 -0
  130. package/dist/repertoire/ado-client.js +4 -2
  131. package/dist/repertoire/ado-semantic.js +11 -5
  132. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  133. package/dist/repertoire/coding/context-pack.js +330 -0
  134. package/dist/repertoire/coding/feedback.js +301 -0
  135. package/dist/repertoire/coding/index.js +4 -1
  136. package/dist/repertoire/coding/manager.js +210 -4
  137. package/dist/repertoire/coding/spawner.js +58 -12
  138. package/dist/repertoire/coding/tools.js +209 -7
  139. package/dist/repertoire/data/ado-endpoints.json +188 -0
  140. package/dist/repertoire/guardrails.js +291 -0
  141. package/dist/repertoire/mcp-client.js +254 -0
  142. package/dist/repertoire/mcp-manager.js +251 -0
  143. package/dist/repertoire/shell-sessions.js +133 -0
  144. package/dist/repertoire/skills.js +14 -23
  145. package/dist/repertoire/tasks/board.js +43 -5
  146. package/dist/repertoire/tasks/fix.js +182 -0
  147. package/dist/repertoire/tasks/index.js +28 -10
  148. package/dist/repertoire/tasks/lifecycle.js +2 -2
  149. package/dist/repertoire/tasks/parser.js +3 -2
  150. package/dist/repertoire/tasks/scanner.js +194 -37
  151. package/dist/repertoire/tasks/transitions.js +11 -28
  152. package/dist/repertoire/tools-base.js +1360 -271
  153. package/dist/repertoire/tools-bluebubbles.js +94 -0
  154. package/dist/repertoire/tools-github.js +1 -6
  155. package/dist/repertoire/tools-teams.js +65 -59
  156. package/dist/repertoire/tools.js +318 -91
  157. package/dist/scripts/claude-code-hook.js +41 -0
  158. package/dist/scripts/claude-code-stop-hook.js +47 -0
  159. package/dist/senses/attention-queue.js +97 -0
  160. package/dist/senses/bluebubbles-client.js +532 -0
  161. package/dist/senses/bluebubbles-entry.js +13 -0
  162. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  163. package/dist/senses/bluebubbles-media.js +339 -0
  164. package/dist/senses/bluebubbles-model.js +261 -0
  165. package/dist/senses/bluebubbles-mutation-log.js +116 -0
  166. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  167. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  168. package/dist/senses/bluebubbles.js +1327 -0
  169. package/dist/senses/cli/adapter.js +274 -0
  170. package/dist/senses/cli/ink-app.js +156 -0
  171. package/dist/senses/cli/inline-diff.js +64 -0
  172. package/dist/senses/cli/message-list.js +51 -0
  173. package/dist/senses/cli/ouro-tui.js +349 -0
  174. package/dist/senses/cli/spinner-imperative.js +135 -0
  175. package/dist/senses/cli/spinner.js +101 -0
  176. package/dist/senses/cli/status-line.js +60 -0
  177. package/dist/senses/cli/streaming-markdown.js +526 -0
  178. package/dist/senses/cli/tool-display.js +83 -0
  179. package/dist/senses/cli/tool-render.js +85 -0
  180. package/dist/senses/cli/tui-store.js +198 -0
  181. package/dist/senses/cli/virtual-list.js +35 -0
  182. package/dist/senses/cli-entry.js +1 -1
  183. package/dist/senses/cli-layout.js +187 -0
  184. package/dist/senses/cli.js +687 -262
  185. package/dist/senses/commands.js +65 -1
  186. package/dist/senses/continuity.js +94 -0
  187. package/dist/senses/habit-turn-message.js +108 -0
  188. package/dist/senses/inner-dialog-worker.js +112 -19
  189. package/dist/senses/inner-dialog.js +592 -84
  190. package/dist/senses/pipeline.js +693 -0
  191. package/dist/senses/shared-turn.js +196 -0
  192. package/dist/senses/surface-tool.js +95 -0
  193. package/dist/senses/teams.js +817 -151
  194. package/dist/senses/trust-gate.js +112 -2
  195. package/package.json +24 -7
  196. package/skills/configure-dev-tools.md +81 -0
  197. package/subagents/README.md +4 -70
  198. package/AdoptionSpecialist.ouro/agent.json +0 -20
  199. package/dist/heart/daemon/subagent-installer.js +0 -125
  200. package/dist/inner-worker-entry.js +0 -4
  201. package/subagents/work-doer.md +0 -233
  202. package/subagents/work-merger.md +0 -624
  203. package/subagents/work-planner.md +0 -373
  204. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  205. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  206. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  207. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  208. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  209. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  210. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  211. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  212. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  213. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  214. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +0 -0
  215. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
@@ -0,0 +1,693 @@
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
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.emitObligationTransitionEpisodes = emitObligationTransitionEpisodes;
42
+ exports.handleInboundTurn = handleInboundTurn;
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const runtime_1 = require("../nerves/runtime");
46
+ const commands_1 = require("./commands");
47
+ const continuity_1 = require("./continuity");
48
+ const manager_1 = require("../heart/bridges/manager");
49
+ const identity_1 = require("../heart/identity");
50
+ const tasks_1 = require("../repertoire/tasks");
51
+ const coding_1 = require("../repertoire/coding");
52
+ const session_activity_1 = require("../heart/session-activity");
53
+ const socket_client_1 = require("../heart/daemon/socket-client");
54
+ const active_work_1 = require("../heart/active-work");
55
+ const delegation_1 = require("../heart/delegation");
56
+ const target_resolution_1 = require("../heart/target-resolution");
57
+ const thoughts_1 = require("../heart/daemon/thoughts");
58
+ const pending_1 = require("../mind/pending");
59
+ const obligations_1 = require("../heart/obligations");
60
+ const obligations_2 = require("../mind/obligations");
61
+ const provider_failover_1 = require("../heart/provider-failover");
62
+ const provider_ping_1 = require("../heart/provider-ping");
63
+ const auth_flow_1 = require("../heart/daemon/auth-flow");
64
+ const tempo_1 = require("../heart/tempo");
65
+ const temporal_view_1 = require("../heart/temporal-view");
66
+ const start_of_turn_packet_1 = require("../heart/start-of-turn-packet");
67
+ const sync_1 = require("../heart/sync");
68
+ const config_1 = require("../heart/config");
69
+ const presence_1 = require("../heart/presence");
70
+ const episodes_1 = require("../mind/episodes");
71
+ const cares_1 = require("../heart/cares");
72
+ /**
73
+ * Emit episodes for obligation state transitions detected during a turn.
74
+ * Exported for direct testability (avoids v8 coverage merge issues in multi-file test suites).
75
+ */
76
+ function emitObligationTransitionEpisodes(agentRoot, preTurnObligationIds, postTurnObligations, preTurnObligations) {
77
+ const postTurnObligationIds = new Set(postTurnObligations.map((ob) => `${ob.id}:${ob.status}`));
78
+ for (const key of preTurnObligationIds) {
79
+ if (!postTurnObligationIds.has(key)) {
80
+ const [obId] = key.split(":");
81
+ const matchedOb = postTurnObligations.find((ob) => ob.id === obId) ?? preTurnObligations.find((ob) => ob.id === obId);
82
+ (0, episodes_1.emitEpisode)(agentRoot, {
83
+ kind: "obligation_shift",
84
+ summary: `obligation "${matchedOb?.content ?? obId}" status changed`,
85
+ whyItMattered: "obligation state transition detected during turn",
86
+ relatedEntities: [`obligation:${obId}`],
87
+ salience: "medium",
88
+ });
89
+ }
90
+ }
91
+ }
92
+ function emptyTaskBoard() {
93
+ return {
94
+ compact: "",
95
+ full: "",
96
+ byStatus: {
97
+ drafting: [],
98
+ processing: [],
99
+ validating: [],
100
+ collaborating: [],
101
+ paused: [],
102
+ blocked: [],
103
+ done: [],
104
+ cancelled: [],
105
+ },
106
+ issues: [],
107
+ actionRequired: [],
108
+ unresolvedDependencies: [],
109
+ activeSessions: [],
110
+ activeBridges: [],
111
+ };
112
+ }
113
+ function isLiveCodingSessionStatus(status) {
114
+ return status === "spawning"
115
+ || status === "running"
116
+ || status === "waiting_input"
117
+ || status === "stalled";
118
+ }
119
+ function prependTurnSections(message, sections) {
120
+ /* v8 ignore next -- defensive: only user messages with non-empty sections reach here @preserve */
121
+ if (message.role !== "user" || sections.length === 0)
122
+ return message;
123
+ const prefix = sections.join("\n\n");
124
+ /* v8 ignore start -- defensive: multipart content branch; non-string user messages are rare @preserve */
125
+ if (typeof message.content === "string") {
126
+ return {
127
+ ...message,
128
+ content: `${prefix}\n\n${message.content}`,
129
+ };
130
+ }
131
+ return {
132
+ ...message,
133
+ content: [
134
+ { type: "text", text: `${prefix}\n\n` },
135
+ ...message.content,
136
+ ],
137
+ };
138
+ /* v8 ignore stop */
139
+ }
140
+ function readInnerWorkState() {
141
+ const defaultJob = {
142
+ status: "idle",
143
+ content: null,
144
+ origin: null,
145
+ mode: "reflect",
146
+ obligationStatus: null,
147
+ surfacedResult: null,
148
+ queuedAt: null,
149
+ startedAt: null,
150
+ surfacedAt: null,
151
+ };
152
+ try {
153
+ const agentRoot = (0, identity_1.getAgentRoot)();
154
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
155
+ const sessionPath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
156
+ const { pendingMessages, turns, runtimeState } = (0, thoughts_1.readInnerDialogRawData)(sessionPath, pendingDir);
157
+ const dialogStatus = (0, thoughts_1.deriveInnerDialogStatus)(pendingMessages, turns, runtimeState);
158
+ const job = (0, thoughts_1.deriveInnerJob)(pendingMessages, turns, runtimeState);
159
+ // Derive obligationPending from both the pending message field and the obligation store
160
+ const storeObligationPending = (0, obligations_1.readPendingObligations)(agentRoot).length > 0;
161
+ return {
162
+ status: dialogStatus.processing === "started" ? "running" : "idle",
163
+ hasPending: dialogStatus.queue !== "clear",
164
+ origin: dialogStatus.origin,
165
+ contentSnippet: dialogStatus.contentSnippet,
166
+ obligationPending: dialogStatus.obligationPending || storeObligationPending,
167
+ job,
168
+ };
169
+ }
170
+ catch {
171
+ return {
172
+ status: "idle",
173
+ hasPending: false,
174
+ job: defaultJob,
175
+ };
176
+ }
177
+ }
178
+ // ── Pipeline ──────────────────────────────────────────────────────
179
+ let _lastSessionKey = null;
180
+ async function handleInboundTurn(input) {
181
+ return (0, sync_1.runWithSyncContext)(async () => {
182
+ // Reset session-scoped state when the session changes
183
+ const sessionKey = `${input.channel}/${input.sessionKey ?? "session"}`;
184
+ if (sessionKey !== _lastSessionKey) {
185
+ _lastSessionKey = sessionKey;
186
+ // Reset file-state cache and scrutiny tracking for the new session
187
+ const { fileStateCache } = await Promise.resolve().then(() => __importStar(require("../mind/file-state")));
188
+ const { resetSessionModifiedFiles } = await Promise.resolve().then(() => __importStar(require("../mind/scrutiny")));
189
+ fileStateCache.clear();
190
+ resetSessionModifiedFiles();
191
+ }
192
+ // Step 0: Check for pending failover reply
193
+ if (input.failoverState?.pending) {
194
+ const userText = input.messages
195
+ .filter((m) => m.role === "user")
196
+ .map((m) => typeof m.content === "string" ? m.content : /* v8 ignore next -- defensive: multipart content fallback @preserve */ "")
197
+ .join(" ")
198
+ .trim();
199
+ const pendingContext = input.failoverState.pending;
200
+ const failoverAction = (0, provider_failover_1.handleFailoverReply)(userText, pendingContext);
201
+ const failoverAgentName = pendingContext.agentName;
202
+ input.failoverState.pending = null; // always clear before acting
203
+ if (failoverAction.action === "switch") {
204
+ let switchSucceeded = false;
205
+ try {
206
+ (0, auth_flow_1.writeAgentProviderSelection)(failoverAgentName, "human", failoverAction.provider);
207
+ (0, auth_flow_1.writeAgentProviderSelection)(failoverAgentName, "agent", failoverAction.provider);
208
+ switchSucceeded = true;
209
+ /* v8 ignore start -- defensive: write failure during provider switch @preserve */
210
+ }
211
+ catch (switchError) {
212
+ (0, runtime_1.emitNervesEvent)({
213
+ level: "error",
214
+ component: "senses",
215
+ event: "senses.failover_switch_error",
216
+ message: `failed to switch provider to ${failoverAction.provider}`,
217
+ meta: { agentName: failoverAgentName, provider: failoverAction.provider, error: switchError instanceof Error ? switchError.message : String(switchError) },
218
+ });
219
+ }
220
+ /* v8 ignore stop */
221
+ /* v8 ignore next -- false branch: write-failure fallthrough @preserve */
222
+ if (switchSucceeded) {
223
+ (0, runtime_1.emitNervesEvent)({
224
+ component: "senses",
225
+ event: "senses.failover_switch",
226
+ message: `switched provider to ${failoverAction.provider} via failover`,
227
+ meta: { agentName: failoverAgentName, provider: failoverAction.provider },
228
+ });
229
+ // Replace "switch to <provider>" with a context message for the agent.
230
+ // The session already has the user's original question from the failed turn.
231
+ // The agent needs to know what happened so it can respond appropriately.
232
+ const newProviderSecrets = (() => {
233
+ try {
234
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(failoverAgentName);
235
+ const cfg = secrets.providers[failoverAction.provider];
236
+ return cfg?.model ?? cfg?.modelName ?? "";
237
+ /* v8 ignore next 2 -- defensive: secrets read failure @preserve */
238
+ }
239
+ catch {
240
+ return "";
241
+ }
242
+ })();
243
+ const newProviderLabel = newProviderSecrets ? `${failoverAction.provider} (${newProviderSecrets})` : failoverAction.provider;
244
+ input.messages = [{
245
+ role: "user",
246
+ content: `[provider switch: ${pendingContext.errorSummary}. switched to ${newProviderLabel}. your conversation history is intact — respond to the user's last message.]`,
247
+ }];
248
+ input.switchedProvider = failoverAction.provider;
249
+ }
250
+ // Switch failed OR succeeded — either way, fall through to normal processing.
251
+ }
252
+ }
253
+ // Step 0b: Slash command interception (before friend resolution / agent turn)
254
+ {
255
+ const firstUserMsg = input.messages.find((m) => m.role === "user");
256
+ const userText = firstUserMsg
257
+ ? (typeof firstUserMsg.content === "string"
258
+ ? firstUserMsg.content
259
+ : Array.isArray(firstUserMsg.content)
260
+ ? firstUserMsg.content.find((p) => p.type === "text")?.text ?? ""
261
+ : /* v8 ignore next -- defensive: content is always string or array @preserve */ "")
262
+ : "";
263
+ const parsed = (0, commands_1.parseSlashCommand)(userText);
264
+ if (parsed) {
265
+ const registry = (0, commands_1.getSharedCommandRegistry)();
266
+ const dispatchResult = registry.dispatch(parsed.command, { channel: input.channel });
267
+ if (dispatchResult.handled && dispatchResult.result) {
268
+ (0, runtime_1.emitNervesEvent)({
269
+ component: "senses",
270
+ event: "senses.pipeline_command",
271
+ message: `slash command intercepted: /${parsed.command}`,
272
+ meta: { command: parsed.command, channel: input.channel },
273
+ });
274
+ if (dispatchResult.result.message) {
275
+ input.callbacks.onTextChunk(dispatchResult.result.message);
276
+ }
277
+ // Return a minimal result — no agent turn, no session write
278
+ const resolvedContext = await input.friendResolver.resolve();
279
+ return {
280
+ resolvedContext,
281
+ gateResult: { allowed: true },
282
+ turnOutcome: "command",
283
+ commandAction: dispatchResult.result.action,
284
+ };
285
+ }
286
+ }
287
+ }
288
+ // Step 1: Resolve friend
289
+ const resolvedContext = await input.friendResolver.resolve();
290
+ (0, runtime_1.emitNervesEvent)({
291
+ component: "senses",
292
+ event: "senses.pipeline_start",
293
+ message: "inbound turn pipeline started",
294
+ meta: {
295
+ channel: input.channel,
296
+ friendId: resolvedContext.friend.id,
297
+ senseType: input.capabilities.senseType,
298
+ },
299
+ });
300
+ // Step 2: Trust gate
301
+ const gateInput = {
302
+ friend: resolvedContext.friend,
303
+ provider: input.provider ?? "local",
304
+ externalId: input.externalId ?? "",
305
+ tenantId: input.tenantId,
306
+ channel: input.channel,
307
+ senseType: input.capabilities.senseType,
308
+ isGroupChat: input.isGroupChat ?? false,
309
+ groupHasFamilyMember: input.groupHasFamilyMember ?? false,
310
+ hasExistingGroupWithFamily: input.hasExistingGroupWithFamily ?? false,
311
+ };
312
+ const gateResult = input.enforceTrustGate(gateInput);
313
+ // Gate rejection: return early, no agent turn
314
+ if (!gateResult.allowed) {
315
+ (0, runtime_1.emitNervesEvent)({
316
+ component: "senses",
317
+ event: "senses.pipeline_gate_reject",
318
+ message: "trust gate rejected inbound turn",
319
+ meta: {
320
+ channel: input.channel,
321
+ friendId: resolvedContext.friend.id,
322
+ reason: gateResult.reason,
323
+ },
324
+ });
325
+ return {
326
+ resolvedContext,
327
+ gateResult,
328
+ };
329
+ }
330
+ // Step 3: Load/create session
331
+ const session = await input.sessionLoader.loadOrCreate();
332
+ const sessionMessages = session.messages;
333
+ let mustResolveBeforeHandoff = (0, continuity_1.resolveMustResolveBeforeHandoff)(session.state?.mustResolveBeforeHandoff === true, input.continuityIngressTexts);
334
+ const lastFriendActivityAt = input.channel === "inner"
335
+ ? session.state?.lastFriendActivityAt
336
+ : new Date().toISOString();
337
+ const currentObligation = input.continuityIngressTexts
338
+ ?.map((text) => text.trim())
339
+ .filter((text) => text.length > 0)
340
+ .at(-1);
341
+ const currentSession = {
342
+ friendId: resolvedContext.friend.id,
343
+ channel: input.channel,
344
+ key: input.sessionKey ?? "session",
345
+ sessionPath: session.sessionPath,
346
+ };
347
+ // Step 3b: Pre-turn sync pull (opt-in) — MUST happen before any continuity state reads
348
+ // so that obligations, episodes, cares, etc. reflect the latest remote state.
349
+ let syncFailure;
350
+ let syncConfig = { enabled: false, remote: "origin" };
351
+ try {
352
+ syncConfig = (0, config_1.getSyncConfig)();
353
+ }
354
+ catch { /* config not available */ }
355
+ // Wrap the turn body in try/finally so drainSyncWrites always runs — even on
356
+ // error or early-return failover paths. This prevents writes from leaking into
357
+ // the next turn's accumulator.
358
+ try {
359
+ /* v8 ignore start -- sync-enabled branches tested in sync.test.ts, pipeline tests mock at module boundary @preserve */
360
+ if (syncConfig.enabled) {
361
+ const pullResult = (0, sync_1.preTurnPull)((0, identity_1.getAgentRoot)(), syncConfig);
362
+ if (!pullResult.ok) {
363
+ syncFailure = pullResult.error;
364
+ }
365
+ // Check for pending-sync from a prior failed push
366
+ if (!syncFailure) {
367
+ const pendingSyncPath = path.join((0, identity_1.getAgentRoot)(), "state", "pending-sync.json");
368
+ try {
369
+ if (fs.existsSync(pendingSyncPath)) {
370
+ const pendingSync = JSON.parse(fs.readFileSync(pendingSyncPath, "utf-8"));
371
+ syncFailure = `prior sync push failed: ${pendingSync.error ?? "unknown"}`;
372
+ fs.unlinkSync(pendingSyncPath);
373
+ }
374
+ }
375
+ catch {
376
+ // Ignore read errors for pending-sync
377
+ }
378
+ }
379
+ }
380
+ /* v8 ignore stop */
381
+ const activeBridges = (0, manager_1.createBridgeManager)().findBridgesForSession({
382
+ friendId: currentSession.friendId,
383
+ channel: currentSession.channel,
384
+ key: currentSession.key,
385
+ });
386
+ const bridgeContext = (0, manager_1.formatBridgeContext)(activeBridges) || undefined;
387
+ let sessionActivity = [];
388
+ try {
389
+ const agentRoot = (0, identity_1.getAgentRoot)();
390
+ sessionActivity = (0, session_activity_1.listSessionActivity)({
391
+ sessionsDir: `${agentRoot}/state/sessions`,
392
+ friendsDir: `${agentRoot}/friends`,
393
+ agentName: (0, identity_1.getAgentName)(),
394
+ currentSession: {
395
+ friendId: currentSession.friendId,
396
+ channel: currentSession.channel,
397
+ key: currentSession.key,
398
+ },
399
+ });
400
+ }
401
+ catch {
402
+ sessionActivity = [];
403
+ }
404
+ let targetCandidates = [];
405
+ try {
406
+ if (input.channel !== "inner") {
407
+ const agentRoot = (0, identity_1.getAgentRoot)();
408
+ targetCandidates = await (0, target_resolution_1.listTargetSessionCandidates)({
409
+ sessionsDir: `${agentRoot}/state/sessions`,
410
+ friendsDir: `${agentRoot}/friends`,
411
+ agentName: (0, identity_1.getAgentName)(),
412
+ currentSession: {
413
+ friendId: currentSession.friendId,
414
+ channel: currentSession.channel,
415
+ key: currentSession.key,
416
+ },
417
+ friendStore: input.friendStore,
418
+ });
419
+ }
420
+ }
421
+ catch {
422
+ targetCandidates = [];
423
+ }
424
+ let pendingObligations = [];
425
+ try {
426
+ pendingObligations = (0, obligations_1.readPendingObligations)((0, identity_1.getAgentRoot)());
427
+ }
428
+ catch {
429
+ pendingObligations = [];
430
+ }
431
+ let codingSessions = [];
432
+ let otherCodingSessions = [];
433
+ try {
434
+ const liveCodingSessions = (0, coding_1.getCodingSessionManager)()
435
+ .listSessions()
436
+ .filter((session) => isLiveCodingSessionStatus(session.status) && Boolean(session.originSession));
437
+ codingSessions = liveCodingSessions.filter((session) => session.originSession?.friendId === currentSession.friendId
438
+ && session.originSession.channel === currentSession.channel
439
+ && session.originSession.key === currentSession.key);
440
+ otherCodingSessions = liveCodingSessions.filter((session) => !(session.originSession?.friendId === currentSession.friendId
441
+ && session.originSession.channel === currentSession.channel
442
+ && session.originSession.key === currentSession.key));
443
+ }
444
+ catch {
445
+ codingSessions = [];
446
+ otherCodingSessions = [];
447
+ }
448
+ const activeWorkFrame = (0, active_work_1.buildActiveWorkFrame)({
449
+ currentSession,
450
+ currentObligation,
451
+ mustResolveBeforeHandoff,
452
+ inner: readInnerWorkState(),
453
+ bridges: activeBridges,
454
+ codingSessions,
455
+ otherCodingSessions,
456
+ pendingObligations,
457
+ taskBoard: (() => {
458
+ try {
459
+ return (0, tasks_1.getTaskModule)().getBoard();
460
+ }
461
+ catch {
462
+ return emptyTaskBoard();
463
+ }
464
+ })(),
465
+ friendActivity: sessionActivity,
466
+ targetCandidates,
467
+ innerReturnObligations: (() => {
468
+ try {
469
+ return (0, obligations_2.listActiveObligations)((0, identity_1.getAgentName)());
470
+ }
471
+ catch {
472
+ return [];
473
+ }
474
+ })(),
475
+ });
476
+ const delegationDecision = (0, delegation_1.decideDelegation)({
477
+ channel: input.channel,
478
+ ingressTexts: input.continuityIngressTexts ?? [],
479
+ activeWork: activeWorkFrame,
480
+ mustResolveBeforeHandoff,
481
+ });
482
+ // Step 4: Drain deferred friend returns, then ordinary per-session pending.
483
+ const deferredReturns = input.channel === "inner"
484
+ ? []
485
+ : (input.drainDeferredReturns?.(resolvedContext.friend.id) ?? []);
486
+ const sessionPending = input.drainPending(input.pendingDir);
487
+ const pending = [...deferredReturns, ...sessionPending];
488
+ // Assemble messages: session messages + pending + inbound user messages
489
+ // NOTE: live world-state checkpoint and pending messages are rendered via buildSystem (system prompt sections)
490
+ const extraPrefixSections = input.onPendingDrained?.(pending) ?? [];
491
+ // extraPrefixSections from onPendingDrained still prepend to user message (e.g., inner dialog wakes)
492
+ if (extraPrefixSections.length > 0 && input.messages.length > 0) {
493
+ input.messages[0] = prependTurnSections(input.messages[0], extraPrefixSections);
494
+ }
495
+ // Append user messages from the inbound turn
496
+ for (const msg of input.messages) {
497
+ sessionMessages.push(msg);
498
+ }
499
+ // Step 4b: Continuity pipeline — derive tempo, build start-of-turn packet, snapshot obligations
500
+ let renderedStartOfTurnPacket;
501
+ const preTurnObligationIds = new Set(pendingObligations.map((ob) => `${ob.id}:${ob.status}`));
502
+ try {
503
+ const agentRoot = (0, identity_1.getAgentRoot)();
504
+ const agentName = (0, identity_1.getAgentName)();
505
+ const recentEpisodes = (0, episodes_1.readRecentEpisodes)(agentRoot, { limit: 20 });
506
+ const activeCares = (0, cares_1.readActiveCares)(agentRoot);
507
+ const tempoState = (0, tempo_1.deriveTempo)({
508
+ activeSessions: sessionActivity.length + 1,
509
+ openObligations: pendingObligations.length,
510
+ recentEpisodeCount: recentEpisodes.length,
511
+ lastActivityAgeMs: sessionActivity.length > 0
512
+ ? Date.now() - new Date(sessionActivity[0].lastActivityAt).getTime()
513
+ : 0,
514
+ hasBlockers: false, // obligations use specific statuses, not "blocked"
515
+ highSalienceEpisodes: recentEpisodes.filter((ep) => ep.salience === "high" || ep.salience === "critical").length,
516
+ activeCareCount: activeCares.length,
517
+ atRiskCareCount: activeCares.filter((c) => c.currentRisk != null).length,
518
+ });
519
+ const temporalView = (0, temporal_view_1.buildTemporalView)(agentRoot, {
520
+ tempo: tempoState.mode,
521
+ preloaded: {
522
+ recentEpisodes,
523
+ activeObligations: pendingObligations,
524
+ activeCares,
525
+ },
526
+ });
527
+ const startOfTurnPacket = (0, start_of_turn_packet_1.buildStartOfTurnPacket)(temporalView, {
528
+ canonicalObligations: {
529
+ primary: activeWorkFrame.primaryObligation,
530
+ all: activeWorkFrame.pendingObligations,
531
+ },
532
+ });
533
+ /* v8 ignore next 3 -- syncFailure propagation tested in sync.test.ts @preserve */
534
+ if (syncFailure) {
535
+ startOfTurnPacket.syncFailure = syncFailure;
536
+ }
537
+ renderedStartOfTurnPacket = (0, start_of_turn_packet_1.renderStartOfTurnPacket)(startOfTurnPacket);
538
+ if (!renderedStartOfTurnPacket)
539
+ renderedStartOfTurnPacket = undefined;
540
+ // Update self-presence
541
+ const presence = (0, presence_1.derivePresence)(agentRoot, agentName, {
542
+ activeSessions: sessionActivity.length + 1,
543
+ openObligations: pendingObligations.length,
544
+ activeBridges: activeBridges.length,
545
+ codingLanes: codingSessions.length,
546
+ currentTempo: tempoState.mode,
547
+ });
548
+ (0, presence_1.writePresence)(agentRoot, agentName, presence);
549
+ }
550
+ catch (continuityError) {
551
+ (0, runtime_1.emitNervesEvent)({
552
+ level: "warn",
553
+ component: "senses",
554
+ event: "senses.continuity_error",
555
+ message: "continuity pipeline failed, continuing without start-of-turn packet",
556
+ meta: { error: continuityError instanceof Error ? continuityError.message : String(continuityError) },
557
+ });
558
+ }
559
+ // Step 5: runAgent
560
+ const existingToolContext = input.runAgentOptions?.toolContext;
561
+ const runAgentOptions = {
562
+ ...input.runAgentOptions,
563
+ bridgeContext,
564
+ activeWorkFrame,
565
+ delegationDecision,
566
+ startOfTurnPacket: renderedStartOfTurnPacket,
567
+ pendingMessages: pending.length > 0 ? pending.map((msg) => ({ from: msg.from, content: msg.content })) : undefined,
568
+ currentSessionKey: currentSession.key,
569
+ currentObligation,
570
+ mustResolveBeforeHandoff,
571
+ setMustResolveBeforeHandoff: (value) => {
572
+ mustResolveBeforeHandoff = value;
573
+ },
574
+ toolContext: {
575
+ /* v8 ignore next -- default no-op signin satisfies interface; real signin injected by sense adapter @preserve */
576
+ signin: async () => undefined,
577
+ ...existingToolContext,
578
+ context: resolvedContext,
579
+ friendStore: input.friendStore,
580
+ currentSession,
581
+ activeBridges,
582
+ },
583
+ };
584
+ const result = await input.runAgent(sessionMessages, input.callbacks, input.channel, input.signal, runAgentOptions);
585
+ // Step 5b: Failover on terminal error
586
+ if (result.outcome === "errored" && input.failoverState) {
587
+ try {
588
+ const agentName = (0, identity_1.getAgentName)();
589
+ const agentConfig = (0, identity_1.loadAgentConfig)();
590
+ const currentProvider = agentConfig.humanFacing.provider;
591
+ /* v8 ignore next -- defensive: errorClassification always set when errored @preserve */
592
+ const classification = result.errorClassification ?? "unknown";
593
+ const inventory = await (0, provider_ping_1.runHealthInventory)(agentName, currentProvider);
594
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(agentName);
595
+ const providerModels = {};
596
+ for (const [p, cfg] of Object.entries(secrets.providers)) {
597
+ const model = cfg.model ?? cfg.modelName;
598
+ if (typeof model === "string" && model)
599
+ providerModels[p] = model;
600
+ }
601
+ // Use agent.json model (source of truth), not secrets model (may be stale)
602
+ const currentModel = agentConfig.humanFacing.model;
603
+ const failoverContext = (0, provider_failover_1.buildFailoverContext)(
604
+ /* v8 ignore next -- defensive: error always set when errored @preserve */
605
+ result.error?.message ?? "unknown error", classification, currentProvider, currentModel, agentName, inventory, providerModels);
606
+ input.failoverState.pending = failoverContext;
607
+ input.postTurn(sessionMessages, session.sessionPath, result.usage);
608
+ return {
609
+ resolvedContext,
610
+ gateResult,
611
+ usage: result.usage,
612
+ turnOutcome: result.outcome,
613
+ sessionPath: session.sessionPath,
614
+ messages: sessionMessages,
615
+ drainedPending: pending,
616
+ failoverMessage: failoverContext.userMessage,
617
+ };
618
+ /* v8 ignore start -- failover catch: tested via pipeline failover sequence throws test but v8 under-reports catch coverage @preserve */
619
+ }
620
+ catch (failoverError) {
621
+ (0, runtime_1.emitNervesEvent)({
622
+ level: "warn",
623
+ component: "senses",
624
+ event: "senses.failover_error",
625
+ message: "failover sequence failed, falling through",
626
+ meta: { error: failoverError instanceof Error ? failoverError.message : String(failoverError) },
627
+ });
628
+ }
629
+ /* v8 ignore stop */
630
+ }
631
+ // Step 5c: Emit episodes for obligation state transitions
632
+ try {
633
+ const agentRoot = (0, identity_1.getAgentRoot)();
634
+ const postTurnObligations = (0, obligations_1.readPendingObligations)(agentRoot);
635
+ emitObligationTransitionEpisodes(agentRoot, preTurnObligationIds, postTurnObligations, pendingObligations);
636
+ }
637
+ catch {
638
+ // Episode emission is non-fatal
639
+ }
640
+ // Step 6: postTurn
641
+ const continuingState = {
642
+ ...(mustResolveBeforeHandoff ? { mustResolveBeforeHandoff: true } : {}),
643
+ ...(typeof lastFriendActivityAt === "string" ? { lastFriendActivityAt } : {}),
644
+ };
645
+ const nextState = result.outcome === "settled" || result.outcome === "blocked" || result.outcome === "superseded" || result.outcome === "observed"
646
+ ? (typeof lastFriendActivityAt === "string"
647
+ ? { lastFriendActivityAt }
648
+ : undefined)
649
+ : (Object.keys(continuingState).length > 0 ? continuingState : undefined);
650
+ input.postTurn(sessionMessages, session.sessionPath, result.usage, undefined, nextState);
651
+ // Step 7: Token accumulation
652
+ await input.accumulateFriendTokens(input.friendStore, resolvedContext.friend.id, result.usage);
653
+ (0, runtime_1.emitNervesEvent)({
654
+ component: "senses",
655
+ event: "senses.pipeline_end",
656
+ message: "inbound turn pipeline completed",
657
+ meta: {
658
+ channel: input.channel,
659
+ friendId: resolvedContext.friend.id,
660
+ },
661
+ });
662
+ // DRY cross-session awareness: notify inner dialog that activity happened on another channel
663
+ // Inner dialog's next checkpoint will include this session's state
664
+ if (input.channel !== "inner") {
665
+ try {
666
+ (0, socket_client_1.requestInnerWake)((0, identity_1.getAgentName)()).catch(/* v8 ignore next */ () => { });
667
+ }
668
+ catch { /* getAgentName may fail in test environments */ }
669
+ }
670
+ return {
671
+ resolvedContext,
672
+ gateResult,
673
+ usage: result.usage,
674
+ turnOutcome: result.outcome,
675
+ completion: result.completion,
676
+ sessionPath: session.sessionPath,
677
+ messages: sessionMessages,
678
+ drainedPending: pending,
679
+ ...(input.switchedProvider ? { switchedProvider: input.switchedProvider } : {}),
680
+ };
681
+ }
682
+ finally {
683
+ // Step 6b: Post-turn sync push (opt-in, turn-scoped writes only).
684
+ // In a finally block so writes are always drained — even on error or early-return
685
+ // failover paths. This prevents writes from leaking into the next turn.
686
+ const writtenPaths = (0, sync_1.drainSyncWrites)();
687
+ /* v8 ignore next 3 -- postTurnPush integration tested in sync.test.ts @preserve */
688
+ if (syncConfig.enabled && writtenPaths.length > 0) {
689
+ (0, sync_1.postTurnPush)((0, identity_1.getAgentRoot)(), syncConfig, writtenPaths);
690
+ }
691
+ }
692
+ }); // end runWithSyncContext
693
+ }