@ouro.bot/cli 0.1.0-alpha.482 → 0.1.0-alpha.484

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 (34) hide show
  1. package/changelog.json +14 -0
  2. package/dist/heart/active-work.js +46 -0
  3. package/dist/heart/background-operations.js +27 -3
  4. package/dist/heart/core.js +15 -0
  5. package/dist/heart/daemon/cli-exec.js +104 -15
  6. package/dist/heart/daemon/cli-help.js +10 -4
  7. package/dist/heart/daemon/cli-parse.js +11 -5
  8. package/dist/heart/daemon/cli-render.js +1 -1
  9. package/dist/heart/daemon/thoughts.js +22 -8
  10. package/dist/heart/mail-import-discovery.js +318 -0
  11. package/dist/heart/outlook/outlook-http-routes.js +1 -1
  12. package/dist/heart/outlook/outlook-http-static.js +4 -0
  13. package/dist/heart/outlook/outlook-http.js +2 -2
  14. package/dist/heart/outlook/outlook-types.js +1 -1
  15. package/dist/heart/outlook/outlook-view.js +3 -3
  16. package/dist/heart/outlook/readers/agent-machine.js +34 -11
  17. package/dist/heart/outlook/readers/continuity-readers.js +5 -1
  18. package/dist/heart/outlook/readers/mail.js +3 -3
  19. package/dist/heart/provider-failover.js +35 -0
  20. package/dist/heart/session-events.js +91 -5
  21. package/dist/heart/turn-context.js +11 -0
  22. package/dist/mind/context.js +1 -1
  23. package/dist/mind/prompt.js +6 -3
  24. package/dist/nerves/coverage/file-completeness.js +1 -0
  25. package/dist/nerves/observation.js +2 -2
  26. package/dist/outlook-ui/assets/{index-CPfhbn13.js → index-Cm51CY9W.js} +1 -1
  27. package/dist/outlook-ui/index.html +2 -2
  28. package/dist/repertoire/tools-mail.js +2 -2
  29. package/dist/repertoire/tools-session.js +5 -2
  30. package/dist/senses/cli/ouro-tui.js +1 -1
  31. package/dist/senses/inner-dialog.js +5 -7
  32. package/dist/senses/mail.js +149 -18
  33. package/dist/senses/pipeline.js +70 -7
  34. package/package.json +1 -1
@@ -4,9 +4,9 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <meta name="color-scheme" content="dark" />
7
- <title>Ouro Outlook</title>
7
+ <title>Ouro Mailbox</title>
8
8
  <meta name="description" content="The daemon-hosted shared orientation surface for agents alive on this machine." />
9
- <script type="module" crossorigin src="/assets/index-CPfhbn13.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-Cm51CY9W.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="/assets/index-BPr5vNuM.css">
11
11
  </head>
12
12
  <body>
@@ -262,12 +262,12 @@ async function renderEmptyMailResult(input) {
262
262
  `mail onboarding status: Mailroom is provisioned for ${input.config.mailboxAddress}, but this agent's encrypted store has 0 messages.`,
263
263
  ...renderSourceGrantStatus(input.config, input.agentId),
264
264
  "interpretation: this is not evidence that the human's HEY inbox is empty; Agent Mail has not yet received or imported mail visible to this agent.",
265
- `agent next move: guide setup from docs/agent-mail-setup.md. If HEY mail is needed, ensure the delegated hey alias exists, ask the human for the exported MBOX file path, run ouro mail import-mbox --agent ${input.agentId} --owner-email <human-email> --source hey --file <mbox-path>, then verify with mail_recent/mail_search/Ouro Outlook.`,
265
+ `agent next move: guide setup from docs/agent-mail-setup.md. If HEY mail is needed, ensure the delegated hey alias exists, first try ouro mail import-mbox --agent ${input.agentId} --owner-email <human-email> --source hey --discover so Ouro can find a browser-downloaded export in .playwright-mcp or Downloads. Only ask the human for a file path if discovery cannot find a unique MBOX, then run ouro mail import-mbox --agent ${input.agentId} --owner-email <human-email> --source hey --file <mbox-path>. Verify with mail_recent/mail_search/Ouro Mailbox.`,
266
266
  "validation golden paths before claiming setup works:",
267
267
  "1. HEY archive to work object: import the human-provided HEY MBOX and use delegated mail to update a real work object, such as travel plans.",
268
268
  "2. Native mail and Screener: send and receive agent-native mail, confirm unknown senders enter Screener, get family authorization for allow/discard, verify sender policy, and confirm discarded mail is recoverable.",
269
269
  "3. Cross-sense reaction: use a mail-derived update or decision to trigger another configured sense, such as texting the family member on iMessage when BlueBubbles is available.",
270
- "4. Ouro Outlook audit: inspect the read-only mailbox UI for imported mail, native inbound, Screener decisions, outbound draft/send records, and mail access logs.",
270
+ "4. Ouro Mailbox audit: inspect the read-only mailbox UI for imported mail, native inbound, Screener decisions, outbound draft/send records, and mail access logs.",
271
271
  "supporting diagnostics are separate evidence inside those paths, not additional paths; never answer a golden-path question with command names, tool names, or status checks.",
272
272
  ].join("\n");
273
273
  }
@@ -46,13 +46,13 @@ const manager_1 = require("../heart/bridges/manager");
46
46
  const session_transcript_1 = require("../heart/session-transcript");
47
47
  const session_activity_1 = require("../heart/session-activity");
48
48
  const active_work_1 = require("../heart/active-work");
49
- const background_operations_1 = require("../heart/background-operations");
50
49
  const coding_1 = require("./coding");
51
50
  const tasks_1 = require("./tasks");
52
51
  const pending_1 = require("../mind/pending");
53
52
  const obligations_1 = require("../arc/obligations");
54
53
  const progress_story_1 = require("../heart/progress-story");
55
54
  const cross_chat_delivery_1 = require("../heart/cross-chat-delivery");
55
+ const mail_import_discovery_1 = require("../heart/mail-import-discovery");
56
56
  const NO_SESSION_FOUND_MESSAGE = "no session found for that friend/channel/key combination.";
57
57
  const EMPTY_SESSION_MESSAGE = "session exists but has no non-system messages.";
58
58
  async function summarizeSessionTailSafely(options) {
@@ -264,9 +264,12 @@ async function buildToolActiveWorkFrame(ctx) {
264
264
  && obligation.origin.channel === currentSession.channel
265
265
  && obligation.origin.key === currentSession.key)?.content ?? null
266
266
  : null;
267
- const backgroundOperations = (0, background_operations_1.listBackgroundOperations)({
267
+ const backgroundOperations = (0, mail_import_discovery_1.listVisibleBackgroundOperations)({
268
268
  agentName: (0, identity_1.getAgentName)(),
269
269
  agentRoot,
270
+ repoRoot: process.cwd(),
271
+ homeDir: process.env.HOME,
272
+ nowMs: Date.now(),
270
273
  limit: 5,
271
274
  });
272
275
  return (0, active_work_1.buildActiveWorkFrame)({
@@ -11,7 +11,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
11
11
  * Only the "live" area (current streaming + spinner + input) re-renders.
12
12
  * This avoids the screen-clearing problem that broke the previous Ink attempt.
13
13
  *
14
- * Design language: ouroboros brand palette from ouroboros.bot / Outlook UI.
14
+ * Design language: ouroboros brand palette from ouroboros.bot / Mailbox UI.
15
15
  * ZERO business logic here — pure rendering from CliStore state.
16
16
  */
17
17
  const react_1 = require("react");
@@ -187,19 +187,17 @@ function deriveResumeCheckpoint(messages) {
187
187
  const assistantText = contentToText(lastAssistant.content);
188
188
  if (!assistantText)
189
189
  return "no prior checkpoint recorded";
190
- const explicitCheckpoint = assistantText
190
+ const cleanedLines = assistantText
191
191
  .split("\n")
192
- .map((line) => line.trim())
192
+ .map((line) => line.replace(/<\/?think>/gi, "").trim())
193
+ .filter((line) => line.length > 0);
194
+ const explicitCheckpoint = cleanedLines
193
195
  .find((line) => /^checkpoint\s*:/i.test(line));
194
196
  if (explicitCheckpoint) {
195
197
  const parsed = explicitCheckpoint.replace(/^checkpoint\s*:\s*/i, "").trim();
196
198
  return parsed || "no prior checkpoint recorded";
197
199
  }
198
- const firstLine = assistantText
199
- .split("\n")
200
- .map((line) => line.trim())
201
- .find((line) => line.length > 0);
202
- /* v8 ignore next -- unreachable: contentToText().trim() guarantees a non-empty line @preserve */
200
+ const firstLine = cleanedLines[0];
203
201
  if (!firstLine)
204
202
  return "no prior checkpoint recorded";
205
203
  if (firstLine.length <= 220)
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.scanMailImportDiscoveryAttention = scanMailImportDiscoveryAttention;
36
37
  exports.startMailSenseApp = startMailSenseApp;
37
38
  const fs = __importStar(require("node:fs"));
38
39
  const path = __importStar(require("node:path"));
@@ -40,6 +41,9 @@ const runtime_1 = require("../nerves/runtime");
40
41
  const identity_1 = require("../heart/identity");
41
42
  const runtime_credentials_1 = require("../heart/runtime-credentials");
42
43
  const pending_1 = require("../mind/pending");
44
+ const socket_client_1 = require("../heart/daemon/socket-client");
45
+ const background_operations_1 = require("../heart/background-operations");
46
+ const mail_import_discovery_1 = require("../heart/mail-import-discovery");
43
47
  const attention_1 = require("../mailroom/attention");
44
48
  const reader_1 = require("../mailroom/reader");
45
49
  const smtp_ingress_1 = require("../mailroom/smtp-ingress");
@@ -87,6 +91,9 @@ function runtimeStatePath(agentName) {
87
91
  function attentionStatePath(agentName) {
88
92
  return path.join((0, identity_1.getAgentRoot)(agentName), "state", "senses", "mail", "attention.json");
89
93
  }
94
+ function importDiscoveryStatePath(agentName) {
95
+ return path.join((0, identity_1.getAgentRoot)(agentName), "state", "senses", "mail", "import-discovery.json");
96
+ }
90
97
  function writeRuntimeState(filePath, state) {
91
98
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
92
99
  fs.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
@@ -97,6 +104,107 @@ function writeRuntimeState(filePath, state) {
97
104
  meta: { agentName: state.agentName, status: state.status, lastQueuedCount: state.lastQueuedCount },
98
105
  });
99
106
  }
107
+ function emptyImportDiscoveryState(updatedAt) {
108
+ return {
109
+ schemaVersion: 1,
110
+ lastNotifiedFingerprint: null,
111
+ updatedAt,
112
+ };
113
+ }
114
+ function readImportDiscoveryState(filePath, updatedAt) {
115
+ try {
116
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
117
+ return {
118
+ schemaVersion: 1,
119
+ lastNotifiedFingerprint: typeof parsed.lastNotifiedFingerprint === "string" && parsed.lastNotifiedFingerprint.trim().length > 0
120
+ ? parsed.lastNotifiedFingerprint
121
+ : null,
122
+ updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : updatedAt,
123
+ };
124
+ }
125
+ catch {
126
+ return emptyImportDiscoveryState(updatedAt);
127
+ }
128
+ }
129
+ function writeImportDiscoveryState(filePath, state) {
130
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
131
+ fs.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
132
+ }
133
+ function stringArray(value) {
134
+ if (!Array.isArray(value))
135
+ return [];
136
+ return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
137
+ }
138
+ function renderImportDiscoveryContent(candidatePaths) {
139
+ return [
140
+ "[Mail Import Ready]",
141
+ "A local MBOX archive is ready for delegated-mail backfill.",
142
+ "This may live in a worktree-local Playwright sandbox rather than ~/Downloads.",
143
+ "",
144
+ "recent candidates:",
145
+ ...candidatePaths.map((candidatePath) => `- ${candidatePath}`),
146
+ "",
147
+ "If this matches an expected mailbox backfill, run `ouro mail import-mbox --discover --owner-email <email> --source hey --agent <agent>` first so Ouro can pick the matching archive or report ambiguity.",
148
+ ].join("\n");
149
+ }
150
+ async function scanMailImportDiscoveryAttention(input) {
151
+ const nowMs = input.now?.() ?? Date.now();
152
+ const updatedAt = new Date(nowMs).toISOString();
153
+ const statePath = input.statePath ?? importDiscoveryStatePath(input.agentName);
154
+ const pendingDir = input.pendingDir ?? (0, pending_1.getInnerDialogPendingDir)(input.agentName);
155
+ const state = readImportDiscoveryState(statePath, updatedAt);
156
+ const existingOperations = (0, background_operations_1.listBackgroundOperations)({
157
+ agentName: input.agentName,
158
+ agentRoot: (0, identity_1.getAgentRoot)(input.agentName),
159
+ limit: 10,
160
+ });
161
+ const discovered = (0, mail_import_discovery_1.listAmbientMailImportOperations)({
162
+ agentName: input.agentName,
163
+ agentRoot: (0, identity_1.getAgentRoot)(input.agentName),
164
+ existingOperations,
165
+ repoRoot: (0, identity_1.getRepoRoot)(),
166
+ homeDir: process.env.HOME,
167
+ nowMs,
168
+ })[0] ?? null;
169
+ const fingerprint = typeof discovered?.spec?.fingerprint === "string" && discovered.spec.fingerprint.trim().length > 0
170
+ ? discovered.spec.fingerprint
171
+ : null;
172
+ const candidatePaths = stringArray(discovered?.spec?.candidatePaths);
173
+ const shouldQueue = Boolean(discovered && fingerprint && fingerprint !== state.lastNotifiedFingerprint);
174
+ if (shouldQueue) {
175
+ (0, pending_1.queuePendingMessage)(pendingDir, {
176
+ from: "mailroom",
177
+ friendId: "self",
178
+ channel: "mail",
179
+ key: "import-ready",
180
+ content: renderImportDiscoveryContent(candidatePaths),
181
+ timestamp: nowMs,
182
+ mode: "reflect",
183
+ });
184
+ await (0, socket_client_1.requestInnerWake)(input.agentName).catch(() => undefined);
185
+ }
186
+ writeImportDiscoveryState(statePath, {
187
+ schemaVersion: 1,
188
+ lastNotifiedFingerprint: shouldQueue ? fingerprint : state.lastNotifiedFingerprint,
189
+ updatedAt,
190
+ });
191
+ (0, runtime_1.emitNervesEvent)({
192
+ component: "senses",
193
+ event: "senses.mail_import_discovery_scanned",
194
+ message: "mail import discovery scanned",
195
+ meta: {
196
+ agentName: input.agentName,
197
+ queued: shouldQueue,
198
+ candidateCount: candidatePaths.length,
199
+ fingerprint,
200
+ },
201
+ });
202
+ return {
203
+ queued: shouldQueue,
204
+ fingerprint,
205
+ candidatePaths,
206
+ };
207
+ }
100
208
  function closeServer(server) {
101
209
  return new Promise((resolve) => {
102
210
  server.close(resolve);
@@ -137,34 +245,21 @@ async function startMailSenseApp(options) {
137
245
  const activeHttpPort = () => ingress ? serverPort(ingress.health) : null;
138
246
  const runtimePath = runtimeStatePath(options.agentName);
139
247
  const attentionPath = attentionStatePath(options.agentName);
248
+ const importDiscoveryPath = importDiscoveryStatePath(options.agentName);
140
249
  let lastScanAt = null;
141
250
  let lastQueuedCount = 0;
142
251
  const scan = async () => {
252
+ const scanStartedAt = new Date(now()).toISOString();
253
+ let queuedCount = 0;
143
254
  try {
144
- const scanStartedAt = new Date(now()).toISOString();
145
- const result = await (0, attention_1.scanMailScreenerAttention)({
255
+ const screener = await (0, attention_1.scanMailScreenerAttention)({
146
256
  agentName: options.agentName,
147
257
  store: resolved.store,
148
258
  pendingDir: (0, pending_1.getInnerDialogPendingDir)(options.agentName),
149
259
  statePath: attentionPath,
150
260
  now,
151
261
  });
152
- lastScanAt = scanStartedAt;
153
- lastQueuedCount = result.queued.length;
154
- writeRuntimeState(runtimePath, {
155
- schemaVersion: 1,
156
- agentName: options.agentName,
157
- status: "running",
158
- mailboxAddress: resolved.config.mailboxAddress,
159
- smtpPort: activeSmtpPort(),
160
- httpPort: activeHttpPort(),
161
- host,
162
- storeKind: resolved.storeKind,
163
- storeLabel: resolved.storeLabel,
164
- lastScanAt,
165
- lastQueuedCount,
166
- updatedAt: new Date(now()).toISOString(),
167
- });
262
+ queuedCount += screener.queued.length;
168
263
  }
169
264
  catch (error) {
170
265
  (0, runtime_1.emitNervesEvent)({
@@ -175,6 +270,42 @@ async function startMailSenseApp(options) {
175
270
  meta: { agentName: options.agentName, error: error instanceof Error ? error.message : String(error) },
176
271
  });
177
272
  }
273
+ try {
274
+ const importDiscovery = await scanMailImportDiscoveryAttention({
275
+ agentName: options.agentName,
276
+ pendingDir: (0, pending_1.getInnerDialogPendingDir)(options.agentName),
277
+ statePath: importDiscoveryPath,
278
+ now,
279
+ });
280
+ if (importDiscovery.queued) {
281
+ queuedCount += 1;
282
+ }
283
+ }
284
+ catch (error) {
285
+ (0, runtime_1.emitNervesEvent)({
286
+ level: "error",
287
+ component: "senses",
288
+ event: "senses.mail_import_discovery_scan_error",
289
+ message: "mail import discovery scan failed",
290
+ meta: { agentName: options.agentName, error: error instanceof Error ? error.message : String(error) },
291
+ });
292
+ }
293
+ lastScanAt = scanStartedAt;
294
+ lastQueuedCount = queuedCount;
295
+ writeRuntimeState(runtimePath, {
296
+ schemaVersion: 1,
297
+ agentName: options.agentName,
298
+ status: "running",
299
+ mailboxAddress: resolved.config.mailboxAddress,
300
+ smtpPort: activeSmtpPort(),
301
+ httpPort: activeHttpPort(),
302
+ host,
303
+ storeKind: resolved.storeKind,
304
+ storeLabel: resolved.storeLabel,
305
+ lastScanAt,
306
+ lastQueuedCount,
307
+ updatedAt: new Date(now()).toISOString(),
308
+ });
178
309
  };
179
310
  await scan();
180
311
  const intervalMs = Math.max(5_000, resolved.config.attentionIntervalMs ?? 30_000);
@@ -97,7 +97,23 @@ function resolveCurrentFailoverBinding(agentName, lane) {
97
97
  const fallback = lane === "inner" ? agentConfig.agentFacing : agentConfig.humanFacing;
98
98
  return { provider: fallback.provider, model: fallback.model };
99
99
  }
100
- function writeFailoverProviderStateSwitch(agentName, action) {
100
+ /**
101
+ * Apply an agent-driven failover switch to provider state, but only after
102
+ * re-pinging the candidate. The inventory ping that produced the candidate
103
+ * may be stale by the time the agent replies — without this preflight, a
104
+ * "switch to <provider>" reply can move the lane onto an unreachable provider.
105
+ *
106
+ * Returns:
107
+ * { ok: true } — preflight passed, state mutated
108
+ * { ok: false, refused } — preflight failed, state untouched, caller should
109
+ * surface the refusal to the agent
110
+ * Throws on disk errors only (caught by caller as before).
111
+ */
112
+ async function writeFailoverProviderStateSwitch(agentName, action) {
113
+ const validation = await (0, provider_failover_1.validateFailoverSwitchCandidate)(agentName, { provider: action.provider, model: action.model });
114
+ if (!validation.ok) {
115
+ return { ok: false, refused: true, classification: validation.classification, message: validation.message };
116
+ }
101
117
  const agentRoot = (0, identity_1.getAgentRoot)(agentName);
102
118
  const stateResult = (0, provider_state_1.readProviderState)(agentRoot);
103
119
  if (!stateResult.ok) {
@@ -125,11 +141,36 @@ function writeFailoverProviderStateSwitch(agentName, action) {
125
141
  lanes,
126
142
  readiness,
127
143
  });
144
+ return { ok: true };
128
145
  }
129
146
  function formatFailoverSwitchLabel(action) {
130
147
  const provenance = (0, provider_failover_1.formatCredentialProvenanceLabel)(action);
131
148
  return `${action.provider} (${action.model}${provenance ? `; ${provenance}` : ""})`;
132
149
  }
150
+ /**
151
+ * Build the operational refusal context message handed back to the agent when
152
+ * a failover switch is rejected by the preflight ping. Slugger-tested format:
153
+ * lead with the refusal + reason, restate the lane that's still standing, then
154
+ * list remaining ready alternatives so the next turn doesn't have to re-enter
155
+ * discovery mode.
156
+ */
157
+ function buildFailoverSwitchRefusedMessage(pendingContext, refusedAction, refusal) {
158
+ const refusedLabel = formatFailoverSwitchLabel(refusedAction);
159
+ const remaining = pendingContext.readyProviders.filter((candidate) => candidate.provider !== refusedAction.provider);
160
+ const alternativesLine = remaining.length > 0
161
+ ? `available verified alternatives right now: ${remaining.map((c) => `${c.provider} (${c.model})`).join(", ")}.`
162
+ : "no other verified alternatives are ready right now.";
163
+ const nextMove = remaining.length > 0
164
+ ? `next move: reply "switch to <provider>" picking one of the alternatives above, or tell the user you cannot continue and why.`
165
+ : `next move: ask the operator to repair credentials for ${refusedAction.provider} (or another provider), or tell the user you cannot continue and why.`;
166
+ return [
167
+ `[provider switch refused: tried to switch ${refusedAction.lane} lane to ${refusedLabel}.`,
168
+ `reason: preflight ping failed (${refusal.classification}: ${refusal.message}).`,
169
+ `current lane unchanged: ${pendingContext.currentProvider} / ${pendingContext.currentModel} on the ${pendingContext.currentLane} lane.`,
170
+ alternativesLine,
171
+ nextMove + "]",
172
+ ].join(" ");
173
+ }
133
174
  function prependTurnSections(message, sections) {
134
175
  /* v8 ignore next -- defensive: only user messages with non-empty sections reach here @preserve */
135
176
  if (message.role !== "user" || sections.length === 0)
@@ -176,10 +217,9 @@ async function handleInboundTurn(input) {
176
217
  const failoverAgentName = pendingContext.agentName;
177
218
  input.failoverState.pending = null; // always clear before acting
178
219
  if (failoverAction.action === "switch") {
179
- let switchSucceeded = false;
220
+ let switchOutcome = null;
180
221
  try {
181
- writeFailoverProviderStateSwitch(failoverAgentName, failoverAction);
182
- switchSucceeded = true;
222
+ switchOutcome = await writeFailoverProviderStateSwitch(failoverAgentName, failoverAction);
183
223
  /* v8 ignore start -- defensive: write failure during provider switch @preserve */
184
224
  }
185
225
  catch (switchError) {
@@ -192,8 +232,7 @@ async function handleInboundTurn(input) {
192
232
  });
193
233
  }
194
234
  /* v8 ignore stop */
195
- /* v8 ignore next -- false branch: write-failure fallthrough @preserve */
196
- if (switchSucceeded) {
235
+ if (switchOutcome?.ok) {
197
236
  (0, runtime_1.emitNervesEvent)({
198
237
  component: "senses",
199
238
  event: "senses.failover_switch",
@@ -217,6 +256,29 @@ async function handleInboundTurn(input) {
217
256
  }];
218
257
  input.switchedProvider = failoverAction.provider;
219
258
  }
259
+ else if (switchOutcome && !switchOutcome.ok) {
260
+ // Preflight refused the switch — the candidate provider is not actually
261
+ // reachable right now. Keep the existing lane intact and tell the agent
262
+ // what happened so it can pick something else next turn.
263
+ (0, runtime_1.emitNervesEvent)({
264
+ level: "warn",
265
+ component: "senses",
266
+ event: "senses.failover_switch_refused",
267
+ message: `refused failover switch of ${failoverAction.lane} lane to ${failoverAction.provider}: ${switchOutcome.message}`,
268
+ meta: {
269
+ agentName: failoverAgentName,
270
+ lane: failoverAction.lane,
271
+ provider: failoverAction.provider,
272
+ model: failoverAction.model,
273
+ classification: switchOutcome.classification,
274
+ error: switchOutcome.message,
275
+ },
276
+ });
277
+ input.messages = [{
278
+ role: "user",
279
+ content: buildFailoverSwitchRefusedMessage(pendingContext, failoverAction, switchOutcome),
280
+ }];
281
+ }
220
282
  // Switch failed OR succeeded — either way, fall through to normal processing.
221
283
  }
222
284
  }
@@ -357,7 +419,7 @@ async function handleInboundTurn(input) {
357
419
  });
358
420
  // Propagate sync failure from pre-turn pull
359
421
  ctx.syncFailure = syncFailure;
360
- const { activeBridges, sessionActivity, pendingObligations, codingSessions, otherCodingSessions } = ctx;
422
+ const { activeBridges, sessionActivity, pendingObligations, codingSessions, otherCodingSessions, backgroundOperations } = ctx;
361
423
  const bridgeContext = (0, manager_1.formatBridgeContext)(activeBridges) || undefined;
362
424
  const activeWorkFrame = (0, active_work_1.buildActiveWorkFrame)({
363
425
  currentSession,
@@ -366,6 +428,7 @@ async function handleInboundTurn(input) {
366
428
  inner: ctx.innerWorkState,
367
429
  bridges: activeBridges,
368
430
  codingSessions,
431
+ backgroundOperations,
369
432
  otherCodingSessions,
370
433
  pendingObligations,
371
434
  taskBoard: ctx.taskBoard,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.482",
3
+ "version": "0.1.0-alpha.484",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",