@ouro.bot/cli 0.1.0-alpha.352 → 0.1.0-alpha.354

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,8 @@ Ouroboros is a TypeScript harness for daemon-managed agents that live in externa
11
11
  - `ouro up` starts the daemon from the installed production version, syncs the launcher, installs workflow helpers, and reconciles stale runtime state.
12
12
  - `ouro dev` starts the daemon from a local repo build. It auto-builds from source, disables launchd auto-restart (so the installed daemon doesn't respawn underneath you), persists the repo path in `~/.ouro-cli/dev-config.json` for next time, and force-restarts the daemon. If you run `ouro dev` from inside the repo, it detects the CWD automatically. Run `ouro up` to return to production mode (this also cleans up `dev-config.json`).
13
13
  - Agent bundles live outside the repo at `~/AgentBundles/<agent>.ouro/`.
14
- - Secrets live outside the repo at `~/.agentsecrets/<agent>/secrets.json`.
14
+ - Provider credentials live outside the repo at `~/.agentsecrets/providers.json`.
15
+ - Sense-specific secrets live outside the repo at `~/.agentsecrets/<agent>/secrets.json`.
15
16
  - Machine-scoped test and runtime spillover lives under `~/.agentstate/...`.
16
17
 
17
18
  Current first-class senses:
@@ -90,8 +91,10 @@ Task docs do not live in this repo anymore. Planning and doing docs live in the
90
91
 
91
92
  ## Runtime Truths
92
93
 
93
- - `agent.json` is the source of truth for provider+model selection per facing (`humanFacing` and `agentFacing`), phrase pools, context settings, and enabled senses.
94
- - `configPath` must point to `~/.agentsecrets/<agent>/secrets.json`.
94
+ - `agent.json` is the source of truth for phrase pools, context settings, enabled senses, and the agent's `configPath`. Legacy `humanFacing`/`agentFacing` values are bootstrap inputs, not live machine fallback.
95
+ - `configPath` must point to `~/.agentsecrets/<agent>/secrets.json` for sense-specific secrets.
96
+ - `state/providers.json` is the local source of truth for provider+model selection on this machine. It has two lanes: `outward` for CLI, Teams, and BlueBubbles turns, and `inner` for inner dialogue.
97
+ - `~/.agentsecrets/providers.json` is the machine credential pool. It stores provider credentials only, records which agent contributed them, and is shared by all agents on that machine.
95
98
  - The daemon discovers bundles dynamically from `~/AgentBundles`.
96
99
  - `ouro status` reports version, last-updated time, discovered agents, senses, and workers.
97
100
  - `bundle-meta.json` tracks the runtime version that last touched a bundle.
@@ -104,17 +107,22 @@ Task docs do not live in this repo anymore. Planning and doing docs live in the
104
107
  - `running`
105
108
  - `error`
106
109
 
107
- When a model provider needs first-time setup, reauth, or an explicit switch, use:
110
+ When a model provider needs first-time setup or reauth, use:
108
111
 
109
112
  ```bash
110
113
  ouro auth --agent <name>
111
114
  ouro auth --agent <name> --provider <provider>
112
- ouro auth --agent <name> --facing agent --provider <provider>
113
115
  ```
114
116
 
115
- The default form reauths the human-facing provider already selected in `agent.json`. The
116
- `--provider` form is for adding or switching providers. The `--facing` flag (values: `human`
117
- or `agent`) controls which facing gets updated; it defaults to `human` when omitted.
117
+ `ouro auth` stores credentials only. It does not switch a lane or write provider/model selection.
118
+
119
+ When you want this machine to use a provider/model for a lane, use:
120
+
121
+ ```bash
122
+ ouro use --agent <name> --lane <outward|inner> --provider <provider> --model <model>
123
+ ```
124
+
125
+ The outward lane handles user-facing senses. The inner lane handles the agent's private thinking. `ouro use` performs the provider/model check before committing the lane, so a broken local choice fails fast with a repair path instead of surprising the next turn.
118
126
 
119
127
  ## Quickstart
120
128
 
@@ -159,6 +167,7 @@ ouro logs
159
167
  ouro stop
160
168
  ouro auth --agent <name>
161
169
  ouro auth --agent <name> --provider <provider>
170
+ ouro use --agent <name> --lane <outward|inner> --provider <provider> --model <model>
162
171
  ouro hatch
163
172
  ouro chat <agent>
164
173
  ouro msg --to <agent> [--session <id>] [--task <ref>] <message>
package/changelog.json CHANGED
@@ -1,6 +1,19 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.354",
6
+ "changes": [
7
+ "Provider visibility now surfaces the effective local outward/inner lanes across start-of-turn context, system prompts, pulse, Outlook, and daemon status, using the shared provider binding resolver with safe credential provenance and no raw secret exposure.",
8
+ "Provider auth docs now state the runtime model plainly: `state/providers.json` selects provider/model per machine, `~/.agentsecrets/providers.json` stores machine credentials, `ouro use` switches lanes, and `ouro auth` stores credentials only."
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.1.0-alpha.353",
13
+ "changes": [
14
+ "Provider failover now uses the machine-wide credential pool and local provider-state lanes: terminal errors build ready-provider choices with safe credential provenance, `switch to <provider>` carries lane/model/revision context, and the retry path updates only the failed `state/providers.json` lane instead of mutating both synced `agent.json` facings."
15
+ ]
16
+ },
4
17
  {
5
18
  "version": "0.1.0-alpha.352",
6
19
  "changes": [
@@ -9,6 +22,16 @@
9
22
  "Autonomous execution prompt contract added: when told to work autonomously, agents use ponder to absorb new messages and continue using tools, settling only with the final result."
10
23
  ]
11
24
  },
25
+ {
26
+ "version": "0.1.0-alpha.351",
27
+ "changes": [
28
+ "Surface tool description rewritten from 'surface progress' to 'send a message to someone' — makes it clear the tool is for interpersonal messaging, not status reporting.",
29
+ "Inner dialog prompt contract now guides agents to use rest(note) for heartbeat state and ponder(reflection) for deeper thoughts, keeping surface strictly for words meant for another person.",
30
+ "Removed [surfaced from inner dialog] prefix from synthetic session messages — provenance is tracked via captureKind: 'synthetic', the prefix was redundant and created echo loops.",
31
+ "Obligation summaries and attention queue headers reframed as structured internal data ([internal] tags) instead of surface-ready prose.",
32
+ "Shared proactive-content-guard module blocks internal content (heartbeat, check-in, task board, obligation status, meta markers) from BlueBubbles and Teams proactive sends."
33
+ ]
34
+ },
12
35
  {
13
36
  "version": "0.1.0-alpha.350",
14
37
  "changes": [
@@ -75,15 +75,18 @@ function parseStatusPayload(data) {
75
75
  const workers = raw.workers;
76
76
  const sync = raw.sync;
77
77
  const agents = raw.agents;
78
+ const providers = raw.providers;
78
79
  if (!overview || typeof overview !== "object" || Array.isArray(overview))
79
80
  return null;
80
81
  if (!Array.isArray(senses) || !Array.isArray(workers))
81
82
  return null;
82
- // sync and agents are optional for backward compatibility — older daemons may omit them
83
+ // sync, agents, and providers are optional for backward compatibility — older daemons may omit them
83
84
  if (sync !== undefined && !Array.isArray(sync))
84
85
  return null;
85
86
  if (agents !== undefined && !Array.isArray(agents))
86
87
  return null;
88
+ if (providers !== undefined && !Array.isArray(providers))
89
+ return null;
87
90
  const parsedOverview = {
88
91
  daemon: stringField(overview.daemon) ?? "unknown",
89
92
  health: stringField(overview.health) ?? "unknown",
@@ -172,10 +175,38 @@ function parseStatusPayload(data) {
172
175
  return null;
173
176
  return { name, enabled };
174
177
  });
178
+ const parsedProviders = (providers ?? []).map((entry) => {
179
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
180
+ return null;
181
+ const row = entry;
182
+ const agent = stringField(row.agent);
183
+ const lane = stringField(row.lane);
184
+ const provider = stringField(row.provider);
185
+ const model = stringField(row.model);
186
+ const source = stringField(row.source);
187
+ const readiness = stringField(row.readiness);
188
+ const credential = stringField(row.credential);
189
+ if (!agent || !lane || !provider || !model || !source || !readiness || !credential)
190
+ return null;
191
+ const parsed = {
192
+ agent,
193
+ lane,
194
+ provider,
195
+ model,
196
+ source,
197
+ readiness,
198
+ credential,
199
+ };
200
+ const detail = stringField(row.detail);
201
+ if (detail !== null)
202
+ parsed.detail = detail;
203
+ return parsed;
204
+ });
175
205
  if (parsedSenses.some((row) => row === null) ||
176
206
  parsedWorkers.some((row) => row === null) ||
177
207
  parsedSync.some((row) => row === null) ||
178
- parsedAgents.some((row) => row === null))
208
+ parsedAgents.some((row) => row === null) ||
209
+ parsedProviders.some((row) => row === null))
179
210
  return null;
180
211
  return {
181
212
  overview: parsedOverview,
@@ -183,6 +214,7 @@ function parseStatusPayload(data) {
183
214
  workers: parsedWorkers,
184
215
  sync: parsedSync,
185
216
  agents: parsedAgents,
217
+ providers: parsedProviders,
186
218
  };
187
219
  }
188
220
  // ── ANSI color helpers (private) ──
@@ -208,12 +240,15 @@ function statusDot(status) {
208
240
  case "ok":
209
241
  case "interactive":
210
242
  case "enabled":
243
+ case "ready":
211
244
  return green("●");
212
245
  case "crashed":
213
246
  case "warn":
214
247
  case "error":
248
+ case "failed":
215
249
  return red("●");
216
250
  case "needs_config":
251
+ case "stale":
217
252
  return yellow("●");
218
253
  case "disabled":
219
254
  case "stopped":
@@ -291,6 +326,18 @@ function formatDaemonStatusOutput(response, fallback) {
291
326
  }
292
327
  lines.push("");
293
328
  }
329
+ // ── Providers ──
330
+ if (payload.providers.length > 0) {
331
+ lines.push(` ${teal("──")} ${bold("Providers")} ${teal("─".repeat(34))}`);
332
+ const agentLaneWidth = Math.max(16, ...payload.providers.map((r) => `${r.agent} ${r.lane}`.length));
333
+ for (const row of payload.providers) {
334
+ const agentLane = `${row.agent} ${row.lane}`.padEnd(agentLaneWidth);
335
+ const model = `${row.provider} / ${row.model}`;
336
+ const detail = [row.readiness, row.detail, row.source, row.credential].filter(Boolean).join("; ");
337
+ lines.push(` ${agentLane} ${statusDot(row.readiness)} ${model} ${dim(detail)}`);
338
+ }
339
+ lines.push("");
340
+ }
294
341
  // ── Senses ──
295
342
  if (payload.senses.length > 0) {
296
343
  lines.push(` ${teal("──")} ${bold("Senses")} ${teal("─".repeat(37))}`);
@@ -421,6 +468,7 @@ function buildStoppedStatusPayload(socketPath, syncRows = [], agentRows = []) {
421
468
  workers: [],
422
469
  sync: syncRows,
423
470
  agents: agentRows,
471
+ providers: [],
424
472
  };
425
473
  }
426
474
  function daemonUnavailableStatusOutput(socketPath, healthFilePath) {
@@ -64,6 +64,7 @@ const outlook_http_1 = require("../outlook/outlook-http");
64
64
  const outlook_types_1 = require("../outlook/outlook-types");
65
65
  const outlook_read_1 = require("../outlook/outlook-read");
66
66
  const outlook_view_1 = require("../outlook/outlook-view");
67
+ const provider_visibility_1 = require("../provider-visibility");
67
68
  const PIDFILE_PATH = path.join(os.homedir(), ".ouro-cli", "daemon.pids");
68
69
  /**
69
70
  * Defense-in-depth: detect if we're running under vitest. The pidfile lives
@@ -427,6 +428,10 @@ class OuroDaemon {
427
428
  const repoRoot = (0, identity_1.getRepoRoot)();
428
429
  const sync = (0, agent_discovery_1.listBundleSyncRows)({ bundlesRoot: this.bundlesRoot });
429
430
  const agents = (0, agent_discovery_1.listAllBundleAgents)({ bundlesRoot: this.bundlesRoot });
431
+ const providers = agents.flatMap((agent) => (0, provider_visibility_1.providerVisibilityStatusRows)((0, provider_visibility_1.buildAgentProviderVisibility)({
432
+ agentName: agent.name,
433
+ agentRoot: path.join(this.bundlesRoot, `${agent.name}.ouro`),
434
+ })));
430
435
  return {
431
436
  overview: {
432
437
  daemon: "running",
@@ -443,6 +448,7 @@ class OuroDaemon {
443
448
  senses,
444
449
  sync,
445
450
  agents,
451
+ ...(providers.length > 0 ? { providers } : {}),
446
452
  };
447
453
  }
448
454
  async start() {
@@ -52,6 +52,7 @@ const fs = __importStar(require("fs"));
52
52
  const os = __importStar(require("os"));
53
53
  const path = __importStar(require("path"));
54
54
  const runtime_1 = require("../../nerves/runtime");
55
+ const provider_visibility_1 = require("../provider-visibility");
55
56
  /* v8 ignore next 3 -- path defaults: tests always inject @preserve */
56
57
  function defaultPulsePath() {
57
58
  return path.join(os.homedir(), ".ouro-cli", "pulse.json");
@@ -136,7 +137,7 @@ function readAgentActivity(bundlePath, readFile = (p) => fs.readFileSync(p, "utf
136
137
  * directly, we just compute where it lives so sibling agents have a
137
138
  * starting point if they want to navigate there manually.
138
139
  */
139
- function buildPulseState(snapshots, bundlesRoot, daemonVersion, now, readActivity = readAgentActivity) {
140
+ function buildPulseState(snapshots, bundlesRoot, daemonVersion, now, readActivity = readAgentActivity, readProviderVisibility = () => null) {
140
141
  const agents = snapshots.map((snap) => {
141
142
  const errorReason = snap.errorReason;
142
143
  const bundlePath = path.join(bundlesRoot, `${snap.name}.ouro`);
@@ -151,6 +152,7 @@ function buildPulseState(snapshots, bundlesRoot, daemonVersion, now, readActivit
151
152
  // Only read activity for agents that are actually running. For
152
153
  // crashed/stopped agents, the runtime.json is stale at best.
153
154
  currentActivity: snap.status === "running" ? readActivity(bundlePath) : null,
155
+ providerVisibility: readProviderVisibility(snap.name, bundlePath),
154
156
  };
155
157
  });
156
158
  return {
@@ -294,7 +296,17 @@ function readPulse(deps = {}) {
294
296
  return {
295
297
  generatedAt: parsed.generatedAt,
296
298
  daemonVersion: parsed.daemonVersion,
297
- agents: parsed.agents.filter(isValidPulseAgentEntry),
299
+ agents: parsed.agents.filter(isValidPulseAgentEntry).map((agent) => {
300
+ const rawAgent = agent;
301
+ if (!Object.prototype.hasOwnProperty.call(rawAgent, "providerVisibility"))
302
+ return agent;
303
+ return {
304
+ ...agent,
305
+ providerVisibility: (0, provider_visibility_1.isAgentProviderVisibility)(rawAgent.providerVisibility)
306
+ ? rawAgent.providerVisibility
307
+ : null,
308
+ };
309
+ }),
298
310
  };
299
311
  }
300
312
  catch {
@@ -390,7 +402,7 @@ function pruneDeliveredState(delivered, state) {
390
402
  * for production callers.
391
403
  */
392
404
  function flushPulse(deps) {
393
- const state = buildPulseState(deps.snapshots, deps.bundlesRoot, deps.daemonVersion, deps.now);
405
+ const state = buildPulseState(deps.snapshots, deps.bundlesRoot, deps.daemonVersion, deps.now, readAgentActivity, (agentName, bundlePath) => (0, provider_visibility_1.buildAgentProviderVisibility)({ agentName, agentRoot: bundlePath }));
394
406
  /* v8 ignore start -- dep defaults: production daemon path; the arrow functions only fire when the corresponding dep is omitted, which only happens in production code paths. Tests inject all deps explicitly. @preserve */
395
407
  const readPrev = deps.readPrev ?? (() => readPulse());
396
408
  const writeNext = deps.writeNext ?? ((s) => writePulse(s));
@@ -173,6 +173,7 @@ function buildOutlookAgentView(input) {
173
173
  agentRoot: input.agent.agentRoot,
174
174
  enabled: input.agent.enabled,
175
175
  provider: input.agent.provider,
176
+ providers: input.agent.providers ?? null,
176
177
  senses: input.agent.senses,
177
178
  freshness: input.agent.freshness,
178
179
  degraded: input.agent.degraded,
@@ -46,6 +46,7 @@ const session_activity_1 = require("../../session-activity");
46
46
  const identity_1 = require("../../identity");
47
47
  const agent_discovery_1 = require("../../daemon/agent-discovery");
48
48
  const runtime_metadata_1 = require("../../daemon/runtime-metadata");
49
+ const provider_visibility_1 = require("../../provider-visibility");
49
50
  const thoughts_1 = require("../../daemon/thoughts");
50
51
  const outlook_types_1 = require("../outlook-types");
51
52
  const shared_1 = require("./shared");
@@ -266,6 +267,7 @@ function summarizeAgent(state) {
266
267
  return {
267
268
  agentName: state.agentName,
268
269
  enabled: state.enabled,
270
+ providers: state.providers,
269
271
  freshness: state.freshness,
270
272
  degraded: state.degraded,
271
273
  tasks: {
@@ -296,6 +298,7 @@ function readOutlookAgentState(agentName, options = {}) {
296
298
  issues.push(...inner.issues);
297
299
  const coding = readCodingSummary(agentRoot);
298
300
  issues.push(...coding.issues);
301
+ const providers = (0, provider_visibility_1.buildAgentProviderVisibility)({ agentName, agentRoot, homeDir: options.homeDir });
299
302
  const latestActivityAt = collectLatestActivityTimestamps({
300
303
  obligations: obligations.items,
301
304
  sessions: sessions.items,
@@ -308,6 +311,7 @@ function readOutlookAgentState(agentName, options = {}) {
308
311
  agentRoot,
309
312
  enabled: config.summary.enabled,
310
313
  provider: config.summary.provider,
314
+ providers,
311
315
  senses: config.summary.senses,
312
316
  freshness: summarizeFreshness(latestActivityAt, now),
313
317
  degraded: summarizeDegraded(issues),
@@ -1,8 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatCredentialProvenanceLabel = formatCredentialProvenanceLabel;
4
+ exports.formatReadyProviderLabel = formatReadyProviderLabel;
3
5
  exports.buildFailoverContext = buildFailoverContext;
4
6
  exports.handleFailoverReply = handleFailoverReply;
7
+ exports.runMachineProviderFailoverInventory = runMachineProviderFailoverInventory;
8
+ const identity_1 = require("./identity");
9
+ const provider_ping_1 = require("./provider-ping");
5
10
  const provider_models_1 = require("./provider-models");
11
+ const provider_credential_pool_1 = require("./provider-credential-pool");
6
12
  const runtime_1 = require("../nerves/runtime");
7
13
  const CLASSIFICATION_LABELS = {
8
14
  "auth-failure": "authentication failed",
@@ -26,6 +32,23 @@ function formatErrorDetail(errorMessage, errorSummary) {
26
32
  return "";
27
33
  return detail.length > 300 ? `${detail.slice(0, 297)}...` : detail;
28
34
  }
35
+ function formatCredentialProvenanceLabel(candidate) {
36
+ if (candidate.contributedByAgent && candidate.source) {
37
+ return `credentials from ${candidate.contributedByAgent} via ${candidate.source}`;
38
+ }
39
+ if (candidate.contributedByAgent) {
40
+ return `credentials from ${candidate.contributedByAgent}`;
41
+ }
42
+ if (candidate.source) {
43
+ return `credentials from this machine via ${candidate.source}`;
44
+ }
45
+ return undefined;
46
+ }
47
+ function formatReadyProviderLabel(candidate) {
48
+ const provenance = formatCredentialProvenanceLabel(candidate);
49
+ const provenanceSuffix = provenance ? `; ${provenance}` : "";
50
+ return `${candidate.provider} (${candidate.model}${provenanceSuffix})`;
51
+ }
29
52
  function formatFailingProviderLine(provider, classification, agentName) {
30
53
  const authCommand = `ouro auth --agent ${agentName} --provider ${provider}`;
31
54
  switch (classification) {
@@ -43,27 +66,64 @@ function formatFailingProviderLine(provider, classification, agentName) {
43
66
  return ` - ${provider}: could not be reached. Run \`${authCommand}\` if credentials may be stale.`;
44
67
  }
45
68
  }
46
- function buildFailoverContext(errorMessage, classification, currentProvider, currentModel, agentName, inventory, providerModels) {
47
- const label = CLASSIFICATION_LABELS[classification];
48
- const providerWithModel = formatProviderWithModel(currentProvider, currentModel);
49
- const errorSummary = `${providerWithModel} ${label}`;
50
- const errorDetail = formatErrorDetail(errorMessage, errorSummary);
51
- const modelMismatch = (0, provider_models_1.getProviderModelMismatchMessage)(currentProvider, currentModel);
52
- const workingProviders = [];
53
- const unconfiguredProviders = [];
54
- const failingProviders = [];
69
+ function isProviderFailoverInventory(inventory) {
70
+ const candidate = inventory;
71
+ return Array.isArray(candidate.ready) && Array.isArray(candidate.unavailable) && Array.isArray(candidate.unconfigured);
72
+ }
73
+ function normalizeLegacyInventory(inventory, providerModels) {
74
+ const ready = [];
75
+ const unavailable = [];
76
+ const unconfigured = [];
55
77
  for (const [provider, result] of Object.entries(inventory)) {
56
78
  if (result.ok) {
57
- workingProviders.push(provider);
79
+ ready.push({
80
+ provider,
81
+ model: (0, provider_models_1.resolveModelForProviderDisplay)(provider, providerModels[provider]),
82
+ result,
83
+ });
58
84
  }
59
85
  else if (result.classification === "auth-failure" && result.message === "no credentials configured") {
60
- unconfiguredProviders.push(provider);
86
+ unconfigured.push(provider);
61
87
  }
62
88
  else {
63
89
  // Configured but ping failed (expired token, provider also down, etc.)
64
- failingProviders.push({ provider, classification: result.classification });
90
+ unavailable.push({
91
+ provider,
92
+ model: (0, provider_models_1.resolveModelForProviderDisplay)(provider, providerModels[provider]),
93
+ result,
94
+ });
65
95
  }
66
96
  }
97
+ return { ready, unavailable, unconfigured };
98
+ }
99
+ function normalizeFailoverInventory(inventory, providerModels) {
100
+ if (!isProviderFailoverInventory(inventory)) {
101
+ return normalizeLegacyInventory(inventory, providerModels);
102
+ }
103
+ return {
104
+ ready: inventory.ready.map((candidate) => ({
105
+ ...candidate,
106
+ model: (0, provider_models_1.resolveModelForProviderDisplay)(candidate.provider, candidate.model),
107
+ })),
108
+ unavailable: inventory.unavailable.map((candidate) => ({
109
+ ...candidate,
110
+ model: candidate.model ? (0, provider_models_1.resolveModelForProviderDisplay)(candidate.provider, candidate.model) : undefined,
111
+ })),
112
+ unconfigured: [...inventory.unconfigured],
113
+ };
114
+ }
115
+ function buildFailoverContext(errorMessage, classification, currentProvider, currentModel, agentName, inventory, providerModels, options = {}) {
116
+ const currentLane = options.currentLane ?? "outward";
117
+ const label = CLASSIFICATION_LABELS[classification];
118
+ const providerWithModel = formatProviderWithModel(currentProvider, currentModel);
119
+ const errorSummary = `${providerWithModel} ${label}`;
120
+ const errorDetail = formatErrorDetail(errorMessage, errorSummary);
121
+ const modelMismatch = (0, provider_models_1.getProviderModelMismatchMessage)(currentProvider, currentModel);
122
+ const normalizedInventory = normalizeFailoverInventory(inventory, providerModels);
123
+ const readyProviders = normalizedInventory.ready;
124
+ const workingProviders = readyProviders.map((candidate) => candidate.provider);
125
+ const unconfiguredProviders = normalizedInventory.unconfigured;
126
+ const failingProviders = normalizedInventory.unavailable;
67
127
  const lines = [`${errorSummary}.`];
68
128
  if (errorDetail) {
69
129
  lines.push(`provider detail: ${errorDetail}`);
@@ -79,22 +139,20 @@ function buildFailoverContext(errorMessage, classification, currentProvider, cur
79
139
  lines.push("Config warning:");
80
140
  lines.push(` - ${modelMismatch}`);
81
141
  lines.push(" - Repair the configured model with:");
82
- lines.push(` \`ouro config model --agent ${agentName} --facing human ${defaultModel}\``);
83
- lines.push(` \`ouro config model --agent ${agentName} --facing agent ${defaultModel}\``);
142
+ lines.push(` \`ouro use --agent ${agentName} --lane ${currentLane} --provider ${currentProvider} --model ${defaultModel}\``);
84
143
  }
85
- if (workingProviders.length > 0) {
144
+ if (readyProviders.length > 0) {
86
145
  lines.push("");
87
146
  lines.push("Ready providers:");
88
- for (const provider of workingProviders) {
89
- const model = (0, provider_models_1.resolveModelForProviderDisplay)(provider, providerModels[provider]);
90
- lines.push(` - ${provider} (${model}): reply "switch to ${provider}"`);
147
+ for (const candidate of readyProviders) {
148
+ lines.push(` - ${formatReadyProviderLabel(candidate)}: reply "switch to ${candidate.provider}"`);
91
149
  }
92
150
  }
93
151
  if (failingProviders.length > 0) {
94
152
  lines.push("");
95
153
  lines.push("Configured but unavailable:");
96
- for (const { provider, classification } of failingProviders) {
97
- lines.push(formatFailingProviderLine(provider, classification, agentName));
154
+ for (const candidate of failingProviders) {
155
+ lines.push(formatFailingProviderLine(candidate.provider, candidate.result.classification, agentName));
98
156
  }
99
157
  }
100
158
  if (unconfiguredProviders.length > 0) {
@@ -112,24 +170,105 @@ function buildFailoverContext(errorMessage, classification, currentProvider, cur
112
170
  component: "engine",
113
171
  event: "engine.failover_context_built",
114
172
  message: "built provider failover context",
115
- meta: { currentProvider, classification, workingProviders, unconfiguredProviders },
173
+ meta: { currentProvider, currentLane, classification, workingProviders, unconfiguredProviders },
116
174
  });
117
175
  return {
118
176
  errorSummary,
119
177
  classification,
120
178
  currentProvider,
179
+ currentLane,
121
180
  agentName,
122
181
  workingProviders,
182
+ readyProviders,
123
183
  unconfiguredProviders,
124
184
  userMessage: lines.join("\n"),
125
185
  };
126
186
  }
127
187
  function handleFailoverReply(reply, context) {
128
188
  const lower = reply.toLowerCase().trim();
129
- for (const provider of context.workingProviders) {
130
- if (lower.includes(`switch to ${provider}`) || lower === provider) {
131
- return { action: "switch", provider };
189
+ const readyProviders = context.readyProviders ?? context.workingProviders.map((provider) => ({
190
+ provider,
191
+ model: (0, provider_models_1.resolveModelForProviderDisplay)(provider),
192
+ }));
193
+ const currentLane = context.currentLane ?? "outward";
194
+ for (const candidate of readyProviders) {
195
+ if (lower.includes(`switch to ${candidate.provider}`) || lower === candidate.provider) {
196
+ return {
197
+ action: "switch",
198
+ provider: candidate.provider,
199
+ model: candidate.model,
200
+ lane: currentLane,
201
+ ...(candidate.credentialRevision ? { credentialRevision: candidate.credentialRevision } : {}),
202
+ ...(candidate.source ? { source: candidate.source } : {}),
203
+ ...(candidate.contributedByAgent ? { contributedByAgent: candidate.contributedByAgent } : {}),
204
+ };
132
205
  }
133
206
  }
134
207
  return { action: "dismiss" };
135
208
  }
209
+ function candidateFromCredentialRecord(record) {
210
+ return {
211
+ provider: record.provider,
212
+ credentialRevision: record.revision,
213
+ source: record.provenance.source,
214
+ contributedByAgent: record.provenance.contributedByAgent,
215
+ };
216
+ }
217
+ async function runMachineProviderFailoverInventory(agentName, currentProvider, options = {}) {
218
+ const ping = options.ping ?? provider_ping_1.pingProvider;
219
+ const poolResult = (0, provider_credential_pool_1.readProviderCredentialPool)(options.homeDir);
220
+ const providers = Object.keys(identity_1.PROVIDER_CREDENTIALS).filter((provider) => provider !== currentProvider);
221
+ const inventory = { ready: [], unavailable: [], unconfigured: [] };
222
+ if (!poolResult.ok) {
223
+ inventory.unconfigured.push(...providers);
224
+ (0, runtime_1.emitNervesEvent)({
225
+ component: "engine",
226
+ event: "engine.machine_failover_inventory_built",
227
+ message: "built machine provider failover inventory",
228
+ meta: { agentName, currentProvider, credentialPoolStatus: poolResult.reason, readyCount: 0, unavailableCount: 0, unconfiguredCount: inventory.unconfigured.length },
229
+ });
230
+ return inventory;
231
+ }
232
+ const results = await Promise.all(providers.map(async (provider) => {
233
+ const record = poolResult.pool.providers[provider];
234
+ if (!record)
235
+ return { provider, record: undefined, result: undefined };
236
+ const model = (0, provider_models_1.getDefaultModelForProvider)(provider);
237
+ const config = { ...record.credentials, ...record.config };
238
+ const result = await ping(provider, config, { model });
239
+ return { provider, record, model, result };
240
+ }));
241
+ for (const entry of results) {
242
+ if (!entry.record) {
243
+ inventory.unconfigured.push(entry.provider);
244
+ }
245
+ else if (entry.result.ok) {
246
+ inventory.ready.push({
247
+ ...candidateFromCredentialRecord(entry.record),
248
+ model: entry.model,
249
+ result: entry.result,
250
+ });
251
+ }
252
+ else {
253
+ inventory.unavailable.push({
254
+ ...candidateFromCredentialRecord(entry.record),
255
+ model: entry.model,
256
+ result: entry.result,
257
+ });
258
+ }
259
+ }
260
+ (0, runtime_1.emitNervesEvent)({
261
+ component: "engine",
262
+ event: "engine.machine_failover_inventory_built",
263
+ message: "built machine provider failover inventory",
264
+ meta: {
265
+ agentName,
266
+ currentProvider,
267
+ credentialPoolStatus: "present",
268
+ readyCount: inventory.ready.length,
269
+ unavailableCount: inventory.unavailable.length,
270
+ unconfiguredCount: inventory.unconfigured.length,
271
+ },
272
+ });
273
+ return inventory;
274
+ }
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildAgentProviderVisibility = buildAgentProviderVisibility;
4
+ exports.formatProviderVisibilityLine = formatProviderVisibilityLine;
5
+ exports.formatAgentProviderVisibilityForPrompt = formatAgentProviderVisibilityForPrompt;
6
+ exports.formatAgentProviderVisibilityForStartOfTurn = formatAgentProviderVisibilityForStartOfTurn;
7
+ exports.formatAgentProviderVisibilityForPulse = formatAgentProviderVisibilityForPulse;
8
+ exports.providerVisibilityStatusRows = providerVisibilityStatusRows;
9
+ exports.isAgentProviderVisibility = isAgentProviderVisibility;
10
+ const runtime_1 = require("../nerves/runtime");
11
+ const provider_binding_resolver_1 = require("./provider-binding-resolver");
12
+ const LANES = ["outward", "inner"];
13
+ function credentialVisibility(binding) {
14
+ const credential = binding.credential;
15
+ if (credential.status === "present") {
16
+ return {
17
+ status: "present",
18
+ source: credential.source,
19
+ contributedByAgent: credential.contributedByAgent,
20
+ revision: credential.revision,
21
+ };
22
+ }
23
+ return {
24
+ status: credential.status,
25
+ repairCommand: credential.repair.command,
26
+ };
27
+ }
28
+ function readinessVisibility(binding) {
29
+ return {
30
+ status: binding.readiness.status,
31
+ ...(binding.readiness.checkedAt ? { checkedAt: binding.readiness.checkedAt } : {}),
32
+ ...(binding.readiness.error ? { error: binding.readiness.error } : {}),
33
+ ...(binding.readiness.reason ? { reason: binding.readiness.reason } : {}),
34
+ ...(binding.readiness.attempts !== undefined ? { attempts: binding.readiness.attempts } : {}),
35
+ };
36
+ }
37
+ function visibilityForLane(input, lane) {
38
+ const resolved = (0, provider_binding_resolver_1.resolveEffectiveProviderBinding)({ ...input, lane });
39
+ if (!resolved.ok) {
40
+ return {
41
+ lane,
42
+ status: "unconfigured",
43
+ provider: "unconfigured",
44
+ model: "-",
45
+ source: "missing",
46
+ readiness: {
47
+ status: "unknown",
48
+ reason: resolved.reason,
49
+ },
50
+ credential: {
51
+ status: "missing",
52
+ repairCommand: resolved.repair.command,
53
+ },
54
+ repairCommand: resolved.repair.command,
55
+ reason: resolved.reason,
56
+ warnings: resolved.warnings.map((warning) => warning.message),
57
+ };
58
+ }
59
+ return {
60
+ lane,
61
+ status: "configured",
62
+ provider: resolved.binding.provider,
63
+ model: resolved.binding.model,
64
+ source: resolved.binding.source,
65
+ readiness: readinessVisibility(resolved.binding),
66
+ credential: credentialVisibility(resolved.binding),
67
+ warnings: resolved.binding.warnings.map((warning) => warning.message),
68
+ };
69
+ }
70
+ function buildAgentProviderVisibility(input) {
71
+ const visibility = {
72
+ agentName: input.agentName,
73
+ lanes: LANES.map((lane) => visibilityForLane(input, lane)),
74
+ };
75
+ (0, runtime_1.emitNervesEvent)({
76
+ component: "config/identity",
77
+ event: "config.provider_visibility_built",
78
+ message: "built provider visibility summary",
79
+ meta: {
80
+ agentName: input.agentName,
81
+ laneStatuses: visibility.lanes.map((lane) => `${lane.lane}:${lane.status}`).join(","),
82
+ },
83
+ });
84
+ return visibility;
85
+ }
86
+ function credentialLabel(credential) {
87
+ if (credential.status === "present") {
88
+ const source = credential.source ?? "unknown";
89
+ return credential.contributedByAgent ? `${source} from ${credential.contributedByAgent}` : source;
90
+ }
91
+ if (credential.status === "invalid-pool")
92
+ return "invalid pool";
93
+ return "missing";
94
+ }
95
+ function readinessLabel(readiness) {
96
+ if (readiness.status === "failed") {
97
+ return readiness.error ? `failed: ${readiness.error}` : "failed";
98
+ }
99
+ if (readiness.status === "stale") {
100
+ return readiness.reason ? `stale: ${readiness.reason}` : "stale";
101
+ }
102
+ if (readiness.status === "unknown") {
103
+ return readiness.reason ? `unknown: ${readiness.reason}` : "unknown";
104
+ }
105
+ return readiness.status;
106
+ }
107
+ function formatProviderVisibilityLine(lane) {
108
+ if (lane.status === "unconfigured") {
109
+ return `${lane.lane}: unconfigured (${lane.reason}); repair: ${lane.repairCommand}`;
110
+ }
111
+ const parts = [
112
+ readinessLabel(lane.readiness),
113
+ `source: ${lane.source}`,
114
+ `credentials: ${credentialLabel(lane.credential)}`,
115
+ ];
116
+ if (lane.credential.revision)
117
+ parts.push(`revision: ${lane.credential.revision}`);
118
+ if (lane.credential.repairCommand)
119
+ parts.push(`repair: ${lane.credential.repairCommand}`);
120
+ if (lane.warnings.length > 0)
121
+ parts.push(`warnings: ${lane.warnings.join("; ")}`);
122
+ return `${lane.lane}: ${lane.provider} / ${lane.model} [${parts.join("; ")}]`;
123
+ }
124
+ function formatAgentProviderVisibilityForPrompt(visibility) {
125
+ if (visibility.lanes.every((lane) => lane.status === "unconfigured")) {
126
+ return [
127
+ "provider bindings are not configured on this machine.",
128
+ ...visibility.lanes.map((lane) => `- ${formatProviderVisibilityLine(lane)}`),
129
+ ].join("\n");
130
+ }
131
+ return [
132
+ "runtime uses local provider bindings for this machine:",
133
+ ...visibility.lanes.map((lane) => `- ${formatProviderVisibilityLine(lane)}`),
134
+ ].join("\n");
135
+ }
136
+ function formatAgentProviderVisibilityForStartOfTurn(visibility) {
137
+ return visibility.lanes.map((lane) => `- ${formatProviderVisibilityLine(lane)}`).join("\n");
138
+ }
139
+ function formatAgentProviderVisibilityForPulse(visibility) {
140
+ return visibility.lanes.map((lane) => formatProviderVisibilityLine(lane)).join("; ");
141
+ }
142
+ function providerVisibilityStatusRows(visibility) {
143
+ return visibility.lanes.map((lane) => {
144
+ if (lane.status === "unconfigured") {
145
+ return {
146
+ agent: visibility.agentName,
147
+ lane: lane.lane,
148
+ provider: "unconfigured",
149
+ model: "-",
150
+ source: "missing",
151
+ readiness: "unknown",
152
+ detail: lane.repairCommand,
153
+ credential: "missing",
154
+ };
155
+ }
156
+ return {
157
+ agent: visibility.agentName,
158
+ lane: lane.lane,
159
+ provider: lane.provider,
160
+ model: lane.model,
161
+ source: lane.source,
162
+ readiness: lane.readiness.status,
163
+ ...(lane.readiness.error ? { detail: lane.readiness.error } : {}),
164
+ credential: credentialLabel(lane.credential),
165
+ };
166
+ });
167
+ }
168
+ function isAgentProviderVisibility(value) {
169
+ if (!value || typeof value !== "object" || Array.isArray(value))
170
+ return false;
171
+ const record = value;
172
+ if (typeof record.agentName !== "string")
173
+ return false;
174
+ if (!Array.isArray(record.lanes))
175
+ return false;
176
+ return record.lanes.every((lane) => {
177
+ if (!lane || typeof lane !== "object" || Array.isArray(lane))
178
+ return false;
179
+ const laneRecord = lane;
180
+ return (laneRecord.lane === "outward" || laneRecord.lane === "inner")
181
+ && (laneRecord.status === "configured" || laneRecord.status === "unconfigured");
182
+ });
183
+ }
@@ -228,6 +228,7 @@ function renderStartOfTurnPacket(packet) {
228
228
  // are actionable "fix your git" signals. bundleState is preferred
229
229
  // because it's structured (array of enum values) while syncFailure is
230
230
  // a legacy free-form string; both render when populated.
231
+ { label: "provider", content: packet.providerState ?? "", priority: 8 },
231
232
  { label: "bundleState", content: (0, bundle_state_1.renderBundleStateHint)(packet.bundleState ?? []), priority: 7 },
232
233
  { label: "syncFailure", content: packet.syncFailure ?? "", priority: 7 },
233
234
  { label: "resume", content: packet.resumeHint, priority: 6 },
@@ -305,6 +306,9 @@ function formatSections(sections) {
305
306
  case "bundleState":
306
307
  parts.push(`**Bundle:** ${section.content}`);
307
308
  break;
309
+ case "provider":
310
+ parts.push(`**Provider:**\n${section.content}`);
311
+ break;
308
312
  }
309
313
  }
310
314
  return parts.join("\n\n");
@@ -59,6 +59,7 @@ const cares_1 = require("../arc/cares");
59
59
  const config_1 = require("./config");
60
60
  const daemon_health_1 = require("./daemon/daemon-health");
61
61
  const prompt_1 = require("../mind/prompt");
62
+ const provider_visibility_1 = require("./provider-visibility");
62
63
  // ── Helpers ─────────────────────────────────────────────────────────
63
64
  const DAEMON_SOCKET_PATH = "/tmp/ouroboros-daemon.sock";
64
65
  function isLiveCodingSessionStatus(status) {
@@ -306,6 +307,7 @@ async function buildTurnContext(input) {
306
307
  /* v8 ignore stop */
307
308
  }
308
309
  const bundleMeta = readBundleMetaFile();
310
+ const providerVisibility = (0, provider_visibility_1.buildAgentProviderVisibility)({ agentName, agentRoot });
309
311
  let daemonHealth = null;
310
312
  try {
311
313
  daemonHealth = (0, daemon_health_1.readHealth)((0, daemon_health_1.getDefaultHealthPath)());
@@ -333,6 +335,7 @@ async function buildTurnContext(input) {
333
335
  bridgeCount: activeBridges.length,
334
336
  codingSessionCount: codingSessions.length,
335
337
  episodeCount: recentEpisodes.length,
338
+ providerLaneCount: providerVisibility.lanes.length,
336
339
  },
337
340
  });
338
341
  return {
@@ -349,6 +352,7 @@ async function buildTurnContext(input) {
349
352
  activeCares,
350
353
  syncConfig,
351
354
  syncFailure: undefined, // Set by pipeline after preTurnPull
355
+ providerVisibility,
352
356
  daemonRunning,
353
357
  senseStatusLines,
354
358
  bundleMeta,
@@ -78,6 +78,7 @@ const obligation_steering_1 = require("./obligation-steering");
78
78
  const daemon_health_1 = require("../heart/daemon/daemon-health");
79
79
  const scrutiny_1 = require("./scrutiny");
80
80
  const pulse_1 = require("../heart/daemon/pulse");
81
+ const provider_visibility_1 = require("../heart/provider-visibility");
81
82
  // Lazy-loaded psyche text cache
82
83
  let _psycheCache = null;
83
84
  let _senseStatusLinesCache = null;
@@ -459,7 +460,10 @@ function senseRuntimeGuidance(channel, preReadStatusLines) {
459
460
  }
460
461
  return lines;
461
462
  }
462
- function providerSection(channel) {
463
+ function providerSection(channel, options) {
464
+ if (options?.providerVisibility) {
465
+ return `## my provider\n${(0, provider_visibility_1.formatAgentProviderVisibilityForPrompt)(options.providerVisibility)}`;
466
+ }
463
467
  return `## my provider\n${(0, core_1.getProviderDisplayLabel)((0, channel_1.channelToFacing)(channel))}`;
464
468
  }
465
469
  function dateSection() {
@@ -727,6 +731,8 @@ function pulseSection(channel = "cli") {
727
731
  if (sib.fixHint)
728
732
  lines.push(` fix: ${sib.fixHint}`);
729
733
  lines.push(` bundle: \`${sib.bundlePath}\``);
734
+ if (sib.providerVisibility)
735
+ lines.push(` provider: ${(0, provider_visibility_1.formatAgentProviderVisibilityForPulse)(sib.providerVisibility)}`);
730
736
  }
731
737
  lines.push("");
732
738
  }
@@ -737,6 +743,8 @@ function pulseSection(channel = "cli") {
737
743
  for (const sib of healthy) {
738
744
  const activity = sib.currentActivity ? ` — ${sib.currentActivity}` : "";
739
745
  lines.push(`- **${sib.name}** is running${activity}. bundle: \`${sib.bundlePath}\``);
746
+ if (sib.providerVisibility)
747
+ lines.push(` provider: ${(0, provider_visibility_1.formatAgentProviderVisibilityForPulse)(sib.providerVisibility)}`);
740
748
  }
741
749
  lines.push("");
742
750
  }
@@ -744,6 +752,8 @@ function pulseSection(channel = "cli") {
744
752
  lines.push("**idle siblings** — configured but not currently running:");
745
753
  for (const sib of idle) {
746
754
  lines.push(`- **${sib.name}** (status: ${sib.status}). bundle: \`${sib.bundlePath}\``);
755
+ if (sib.providerVisibility)
756
+ lines.push(` provider: ${(0, provider_visibility_1.formatAgentProviderVisibilityForPulse)(sib.providerVisibility)}`);
747
757
  }
748
758
  lines.push("");
749
759
  }
@@ -1260,7 +1270,7 @@ async function buildSystem(channel = "cli", options, context) {
1260
1270
  runtimeInfoSection(channel, options),
1261
1271
  rhythmStatusSection(options?.daemonHealth),
1262
1272
  channelNatureSection((0, channel_1.getChannelCapabilities)(channel)),
1263
- providerSection(channel),
1273
+ providerSection(channel, options),
1264
1274
  dateSection(),
1265
1275
  // Group 3: my tools & capabilities
1266
1276
  "# my tools & capabilities",
@@ -52,9 +52,7 @@ const active_work_1 = require("../heart/active-work");
52
52
  const delegation_1 = require("../heart/delegation");
53
53
  const obligations_1 = require("../arc/obligations");
54
54
  const provider_failover_1 = require("../heart/provider-failover");
55
- const provider_ping_1 = require("../heart/provider-ping");
56
- const auth_flow_1 = require("../heart/auth/auth-flow");
57
- const provider_models_1 = require("../heart/provider-models");
55
+ const provider_state_1 = require("../heart/provider-state");
58
56
  const tempo_1 = require("../heart/tempo");
59
57
  const temporal_view_1 = require("../heart/temporal-view");
60
58
  const start_of_turn_packet_1 = require("../heart/start-of-turn-packet");
@@ -65,6 +63,7 @@ const session_events_1 = require("../heart/session-events");
65
63
  const presence_1 = require("../arc/presence");
66
64
  const episodes_1 = require("../arc/episodes");
67
65
  const turn_context_1 = require("../heart/turn-context");
66
+ const provider_visibility_1 = require("../heart/provider-visibility");
68
67
  /**
69
68
  * Emit episodes for obligation state transitions detected during a turn.
70
69
  * Exported for direct testability (avoids v8 coverage merge issues in multi-file test suites).
@@ -85,6 +84,52 @@ function emitObligationTransitionEpisodes(agentRoot, preTurnObligationIds, postT
85
84
  }
86
85
  }
87
86
  }
87
+ function providerLaneForChannel(channel) {
88
+ return channel === "inner" ? "inner" : "outward";
89
+ }
90
+ function resolveCurrentFailoverBinding(agentName, lane) {
91
+ const stateResult = (0, provider_state_1.readProviderState)((0, identity_1.getAgentRoot)(agentName));
92
+ if (stateResult.ok) {
93
+ const binding = stateResult.state.lanes[lane];
94
+ return { provider: binding.provider, model: binding.model };
95
+ }
96
+ const agentConfig = (0, identity_1.loadAgentConfig)();
97
+ const fallback = lane === "inner" ? agentConfig.agentFacing : agentConfig.humanFacing;
98
+ return { provider: fallback.provider, model: fallback.model };
99
+ }
100
+ function writeFailoverProviderStateSwitch(agentName, action) {
101
+ const agentRoot = (0, identity_1.getAgentRoot)(agentName);
102
+ const stateResult = (0, provider_state_1.readProviderState)(agentRoot);
103
+ if (!stateResult.ok) {
104
+ throw new Error(`Cannot switch ${action.lane} lane for ${agentName}: ${stateResult.error}`);
105
+ }
106
+ const updatedAt = new Date().toISOString();
107
+ const lanes = { ...stateResult.state.lanes };
108
+ lanes[action.lane] = {
109
+ provider: action.provider,
110
+ model: action.model,
111
+ source: "local",
112
+ updatedAt,
113
+ };
114
+ const readiness = { ...stateResult.state.readiness };
115
+ readiness[action.lane] = {
116
+ status: "ready",
117
+ provider: action.provider,
118
+ model: action.model,
119
+ checkedAt: updatedAt,
120
+ ...(action.credentialRevision ? { credentialRevision: action.credentialRevision } : {}),
121
+ };
122
+ (0, provider_state_1.writeProviderState)(agentRoot, {
123
+ ...stateResult.state,
124
+ updatedAt,
125
+ lanes,
126
+ readiness,
127
+ });
128
+ }
129
+ function formatFailoverSwitchLabel(action) {
130
+ const provenance = (0, provider_failover_1.formatCredentialProvenanceLabel)(action);
131
+ return `${action.provider} (${action.model}${provenance ? `; ${provenance}` : ""})`;
132
+ }
88
133
  function prependTurnSections(message, sections) {
89
134
  /* v8 ignore next -- defensive: only user messages with non-empty sections reach here @preserve */
90
135
  if (message.role !== "user" || sections.length === 0)
@@ -133,8 +178,7 @@ async function handleInboundTurn(input) {
133
178
  if (failoverAction.action === "switch") {
134
179
  let switchSucceeded = false;
135
180
  try {
136
- (0, auth_flow_1.writeAgentProviderSelection)(failoverAgentName, "human", failoverAction.provider);
137
- (0, auth_flow_1.writeAgentProviderSelection)(failoverAgentName, "agent", failoverAction.provider);
181
+ writeFailoverProviderStateSwitch(failoverAgentName, failoverAction);
138
182
  switchSucceeded = true;
139
183
  /* v8 ignore start -- defensive: write failure during provider switch @preserve */
140
184
  }
@@ -143,8 +187,8 @@ async function handleInboundTurn(input) {
143
187
  level: "error",
144
188
  component: "senses",
145
189
  event: "senses.failover_switch_error",
146
- message: `failed to switch provider to ${failoverAction.provider}`,
147
- meta: { agentName: failoverAgentName, provider: failoverAction.provider, error: switchError instanceof Error ? switchError.message : String(switchError) },
190
+ message: `failed to switch ${failoverAction.lane} provider lane to ${failoverAction.provider}`,
191
+ meta: { agentName: failoverAgentName, lane: failoverAction.lane, provider: failoverAction.provider, model: failoverAction.model, error: switchError instanceof Error ? switchError.message : String(switchError) },
148
192
  });
149
193
  }
150
194
  /* v8 ignore stop */
@@ -153,28 +197,24 @@ async function handleInboundTurn(input) {
153
197
  (0, runtime_1.emitNervesEvent)({
154
198
  component: "senses",
155
199
  event: "senses.failover_switch",
156
- message: `switched provider to ${failoverAction.provider} via failover`,
157
- meta: { agentName: failoverAgentName, provider: failoverAction.provider },
200
+ message: `switched ${failoverAction.lane} provider lane to ${failoverAction.provider} via failover`,
201
+ meta: {
202
+ agentName: failoverAgentName,
203
+ lane: failoverAction.lane,
204
+ provider: failoverAction.provider,
205
+ model: failoverAction.model,
206
+ credentialRevision: failoverAction.credentialRevision,
207
+ source: failoverAction.source,
208
+ contributedByAgent: failoverAction.contributedByAgent,
209
+ },
158
210
  });
159
211
  // Replace "switch to <provider>" with a context message for the agent.
160
212
  // The session already has the user's original question from the failed turn.
161
213
  // The agent needs to know what happened so it can respond appropriately.
162
- const newProviderSecrets = (() => {
163
- try {
164
- const { secrets } = (0, auth_flow_1.loadAgentSecrets)(failoverAgentName);
165
- const cfg = secrets.providers[failoverAction.provider];
166
- const hint = cfg?.model ?? cfg?.modelName;
167
- return (0, provider_models_1.resolveModelForProviderDisplay)(failoverAction.provider, typeof hint === "string" ? hint : "");
168
- /* v8 ignore next 2 -- defensive: secrets read failure @preserve */
169
- }
170
- catch {
171
- return (0, provider_models_1.resolveModelForProviderDisplay)(failoverAction.provider);
172
- }
173
- })();
174
- const newProviderLabel = `${failoverAction.provider} (${newProviderSecrets})`;
214
+ const newProviderLabel = formatFailoverSwitchLabel(failoverAction);
175
215
  input.messages = [{
176
216
  role: "user",
177
- content: `[provider switch: ${pendingContext.errorSummary}. switched to ${newProviderLabel}. your conversation history is intact — respond to the user's last message.]`,
217
+ content: `[provider switch: ${pendingContext.errorSummary}. switched ${failoverAction.lane} lane to ${newProviderLabel}. your conversation history is intact — respond to the user's last message.]`,
178
218
  }];
179
219
  input.switchedProvider = failoverAction.provider;
180
220
  }
@@ -396,6 +436,9 @@ async function handleInboundTurn(input) {
396
436
  if (syncFailure) {
397
437
  startOfTurnPacket.syncFailure = syncFailure;
398
438
  }
439
+ if (ctx.providerVisibility) {
440
+ startOfTurnPacket.providerState = (0, provider_visibility_1.formatAgentProviderVisibilityForStartOfTurn)(ctx.providerVisibility);
441
+ }
399
442
  // Structured bundle state detection — surfaces discrete issues the
400
443
  // agent can remediate via the bundle_* tools. Runs independently of
401
444
  // syncFailure so the two signals coexist during the transition away
@@ -450,6 +493,7 @@ async function handleInboundTurn(input) {
450
493
  bundleMeta: ctx.bundleMeta,
451
494
  daemonHealth: ctx.daemonHealth,
452
495
  journalFiles: ctx.journalFiles,
496
+ ...(ctx.providerVisibility ? { providerVisibility: ctx.providerVisibility } : {}),
453
497
  toolContext: {
454
498
  /* v8 ignore next -- default no-op signin satisfies interface; real signin injected by sense adapter @preserve */
455
499
  signin: async () => undefined,
@@ -465,23 +509,15 @@ async function handleInboundTurn(input) {
465
509
  if (result.outcome === "errored" && input.failoverState) {
466
510
  try {
467
511
  const agentName = (0, identity_1.getAgentName)();
468
- const agentConfig = (0, identity_1.loadAgentConfig)();
469
- const currentProvider = agentConfig.humanFacing.provider;
512
+ const currentLane = providerLaneForChannel(input.channel);
513
+ const currentBinding = resolveCurrentFailoverBinding(agentName, currentLane);
514
+ const currentProvider = currentBinding.provider;
470
515
  /* v8 ignore next -- defensive: errorClassification always set when errored @preserve */
471
516
  const classification = result.errorClassification ?? "unknown";
472
- const inventory = await (0, provider_ping_1.runHealthInventory)(agentName, currentProvider);
473
- const { secrets } = (0, auth_flow_1.loadAgentSecrets)(agentName);
474
- const providerModels = {};
475
- for (const [p, cfg] of Object.entries(secrets.providers)) {
476
- const model = cfg.model ?? cfg.modelName;
477
- if (typeof model === "string" && model)
478
- providerModels[p] = model;
479
- }
480
- // Use agent.json model (source of truth), not secrets model (may be stale)
481
- const currentModel = agentConfig.humanFacing.model;
517
+ const inventory = await (0, provider_failover_1.runMachineProviderFailoverInventory)(agentName, currentProvider);
482
518
  const failoverContext = (0, provider_failover_1.buildFailoverContext)(
483
519
  /* v8 ignore next -- defensive: error always set when errored @preserve */
484
- result.error?.message ?? "unknown error", classification, currentProvider, currentModel, agentName, inventory, providerModels);
520
+ result.error?.message ?? "unknown error", classification, currentProvider, currentBinding.model, agentName, inventory, {}, { currentLane });
485
521
  input.failoverState.pending = failoverContext;
486
522
  input.postTurn(sessionMessages, session.sessionPath, result.usage);
487
523
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.352",
3
+ "version": "0.1.0-alpha.354",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",