oomi-ai 0.10.0 → 0.10.2

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.
@@ -132,6 +132,8 @@ The legacy persona-app commands below remain compatibility tools; use Spaces for
132
132
 
133
133
  Use this when your owner asks you to let another agent (Claude, Codex, a Grok bot, or another Oomi agent) talk with you directly. For example: "make a link so Claude can ask you how to set up Obsidian".
134
134
 
135
+ **Your only job is to create the link and then answer.** Your owner gives the link to the other agent, and that agent joins from wherever it already runs, usually another computer. Do not install, launch, sign in to, or message that agent yourself, even if a copy of it is installed on your computer. Never ask your owner for passwords, sign-in links or one-time codes for it. You don't need the other agent to be ready first: create the link right away.
136
+
135
137
  1. Create a channel with a specific purpose: `oomi channel create --purpose "Help Claude set up Obsidian on this computer"`. If useful, add `--expires-minutes N` (default 120, at most 1440) or `--greeting "..."`.
136
138
  2. Give your owner the returned `link`. Tell them it works for one agent only, until `joinBy`. Never post the link publicly or share your device token. The link is all the other agent needs.
137
139
  3. Each message from the other agent arrives as its own turn in a separate conversation (`agent:main:oomi:channel:ID`), headed "Oomi agent channel". Your final reply text is sent back automatically. Do not also send that reply with `oomi channel send`.
@@ -140,6 +142,10 @@ Use this when your owner asks you to let another agent (Claude, Codex, a Grok bo
140
142
 
141
143
  To review a channel, use `oomi channel list` and `oomi channel show ID`.
142
144
 
145
+ **When you can't answer.** Your AI provider might reject your sign-in, hit a limit or be unreachable. If so, oomi records the reason, tells your owner what to fix and tells the other side the message wasn't answered. Tell your owner plainly which AI provider failed and what it said. Ask them to sign in again on this computer themselves, in a terminal or the provider's app here. Never ask them to paste passwords or one-time codes into a chat. `oomi verify --connection NAME` checks that you can answer again. `oomi channel create` also returns `warnings` when your bridge can't answer; tell your owner before they share the link.
146
+
147
+ **Use your own connection.** Turns started by the bridge already use it. Otherwise, pass `--connection NAME` (`oomi bridge list` shows the names). If oomi says it used a connection that isn't linked, rerun with your own. Don't create a new Auth Link unless your owner asked to connect you.
148
+
143
149
  To join another agent's link as a guest:
144
150
 
145
151
  1. Run `oomi channel join LINK`.
package/bin/oomi-ai.js CHANGED
@@ -20,6 +20,7 @@ import { handleSpacesCommand, compactHealth } from '../lib/spaces.js';
20
20
  import { handleObjectivesCommand } from '../lib/objectives.js';
21
21
  import { handleRoutinesCommand, startRoutineEventWorker } from '../lib/routines.js';
22
22
  import { handleChannelCommand, startAgentChannelWorker } from '../lib/agentChannels.js';
23
+ import { classifyRuntimeFailure } from '../lib/runtimeFailure.js';
23
24
  import { createPersonaApiClient } from '../lib/personaApiClient.js';
24
25
  import { inferSpokenMetadataFromContent, normalizeSpokenMetadata } from '../lib/spokenMetadata.js';
25
26
  import {
@@ -2650,7 +2651,9 @@ async function startOpenclawBridge(flags) {
2650
2651
  state: { ...bridgeState, brokerHttp, deviceToken },
2651
2652
  connection: bridgeConnectionName,
2652
2653
  deliver: deliverAgentEvent,
2653
- onError: (error) => updateBridgeStatus({ agentChannelError: `Agent channel delivery: ${error.message}` }),
2654
+ onError: (error) => updateBridgeStatus({ agentChannelError: error.failure
2655
+ ? `Couldn't answer a chat-link message: ${error.failure.summary}. ${error.failure.hint}`
2656
+ : `Agent channel delivery: ${error.message}` }),
2654
2657
  onDelivered: () => updateBridgeStatus({ agentChannelError: '' }),
2655
2658
  });
2656
2659
  console.log(`Local gateway: ${gateway.gatewayUrl}`);
@@ -4450,6 +4453,14 @@ function connectionDirectory() {
4450
4453
  return path.join(process.env.OOMI_CONNECTIONS_DIR || path.join(os.homedir(), '.oomi', 'connections'), bridgeConnectionName);
4451
4454
  }
4452
4455
 
4456
+ // A failed connection check usually means the agent's own AI access is broken.
4457
+ // Say that plainly, with the fix, instead of a bare runtime error.
4458
+ function explainVerificationFailure(err) {
4459
+ const failure = classifyRuntimeFailure(err);
4460
+ if (failure.code === 'runtime_failed') return err;
4461
+ return Object.assign(new Error(`This agent couldn't answer the connection check: ${failure.summary}. ${failure.hint} Oomi itself is fine; nothing else needs reconnecting. (Details: ${failure.detail})`), { failure });
4462
+ }
4463
+
4453
4464
  async function verifyLocalAgent(state) {
4454
4465
  if (state.openclawHome) process.env.OPENCLAW_HOME = state.openclawHome;
4455
4466
  let result;
@@ -4519,8 +4530,9 @@ async function connectLocalAgent(flags) {
4519
4530
  const verified = await verifyLocalAgent(state);
4520
4531
  state.verifiedAt = verified.verifiedAt;
4521
4532
  } catch (err) {
4522
- updateBridgeStatus({ runtime, runtimeReady: false, status: 'degraded', lastErrorMessage: err.message });
4523
- throw err;
4533
+ const explained = explainVerificationFailure(err);
4534
+ updateBridgeStatus({ runtime, runtimeReady: false, status: 'degraded', lastErrorMessage: explained.message });
4535
+ throw explained;
4524
4536
  }
4525
4537
  } else {
4526
4538
  console.log(`Reusing the verified ${runtime} runtime. No model test message sent. Use oomi verify for an explicit paid-model probe.`);
@@ -4676,8 +4688,9 @@ async function main() {
4676
4688
  updateBridgeStatus({ runtimeReady: true, verifiedAt: result.verifiedAt, lastErrorMessage: '' });
4677
4689
  console.log(JSON.stringify(result, null, 2));
4678
4690
  } catch (err) {
4679
- updateBridgeStatus({ runtimeReady: false, status: 'degraded', lastErrorMessage: err.message });
4680
- throw err;
4691
+ const explained = explainVerificationFailure(err);
4692
+ updateBridgeStatus({ runtimeReady: false, status: 'degraded', lastErrorMessage: explained.message });
4693
+ throw explained;
4681
4694
  }
4682
4695
  return;
4683
4696
  }
@@ -1,7 +1,9 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { randomUUID } from 'node:crypto';
4
5
  import { readJson, writePrivateJson } from './localAgent.js';
6
+ import { classifyRuntimeFailure } from './runtimeFailure.js';
5
7
 
6
8
  // Owner-visible conversations between this agent and one other agent.
7
9
  // Host side uses the device token against the Oomi API. Guest side (after
@@ -42,6 +44,46 @@ export function parseChannelLink(link) {
42
44
  return { origin: url.origin, token: match[1] };
43
45
  }
44
46
 
47
+ function connectionsRoot() {
48
+ return process.env.OOMI_CONNECTIONS_DIR || path.join(os.homedir(), '.oomi', 'connections');
49
+ }
50
+
51
+ function localConnections() {
52
+ try {
53
+ return fs.readdirSync(connectionsRoot(), { withFileTypes: true })
54
+ .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(connectionsRoot(), entry.name, 'oomi-bridge.json')))
55
+ .map((entry) => entry.name);
56
+ } catch { return []; }
57
+ }
58
+
59
+ // Agents that ran a command without --connection were sent to a stale default
60
+ // connection and then created Auth Links for it. Say which connection was used
61
+ // and which others exist, and steer away from reconnecting.
62
+ export function explainConnectionError(error, directory) {
63
+ const notLinked = error?.status === 403 && /not linked/i.test(error.message || '');
64
+ const missing = /Connect this agent to Oomi first/.test(error?.message || '');
65
+ if (!notLinked && !missing) return error;
66
+ const named = directory && path.resolve(path.dirname(directory)) === path.resolve(connectionsRoot());
67
+ const label = named ? `the connection "${path.basename(directory)}"` : 'the default OpenClaw connection';
68
+ const others = localConnections().filter((name) => !named || name !== path.basename(directory));
69
+ const problem = missing ? `found no saved login for ${label}` : `used ${label}, but that connection isn't linked to an oomi account`;
70
+ const next = others.length
71
+ ? `Connections on this computer: ${others.join(', ')}. If one of them is you, rerun with --connection NAME.`
72
+ : 'No other oomi connections were found on this computer.';
73
+ return Object.assign(new Error(`oomi ${problem}. ${next} Don't create a new Auth Link or reconnect unless your owner asked to connect this agent.`), { status: error.status, reasonCode: error.reasonCode });
74
+ }
75
+
76
+ // A link only helps if this agent can answer; warn before handing one out.
77
+ export function bridgeWarnings(directory) {
78
+ if (!directory) return [];
79
+ const status = readJson(path.join(directory, 'oomi-bridge-status.json'), null);
80
+ if (!status) return ['No oomi bridge status was found for this connection. Run oomi bridge repair for it, or guest messages will wait unanswered.'];
81
+ const warnings = [];
82
+ if (['error', 'degraded', 'failed', 'stopped'].includes(status.status)) warnings.push(`Your oomi bridge reports "${status.status}"${status.lastErrorMessage ? `: ${String(status.lastErrorMessage).replace(/\.\s*$/, '')}` : ''}.`);
83
+ if (status.agentChannelError) warnings.push(status.agentChannelError);
84
+ return warnings;
85
+ }
86
+
45
87
  function guestRecordPath(directory, channelId) {
46
88
  return path.join(directory, 'agent-channels', `${channelId}.json`);
47
89
  }
@@ -90,7 +132,10 @@ function optionalInteger(value, name) {
90
132
  return number;
91
133
  }
92
134
 
93
- export async function handleChannelCommand(state, action, flags = {}, positionals = [], { directory, request = channelRequest, guest = guestRequest, print = (value) => console.log(JSON.stringify(value, null, 2)) } = {}) {
135
+ export async function handleChannelCommand(state, action, flags = {}, positionals = [], { directory, request: rawRequest = channelRequest, guest = guestRequest, print = (value) => console.log(JSON.stringify(value, null, 2)) } = {}) {
136
+ const request = async (...args) => {
137
+ try { return await rawRequest(...args); } catch (error) { throw explainConnectionError(error, directory); }
138
+ };
94
139
  const id = positionals[0] || flags.id;
95
140
  const requireId = () => { if (typeof id !== 'string' || !CHANNEL_ID.test(id)) throw new Error('Provide a channel ID (ach_...) from oomi channel list.'); };
96
141
  let result;
@@ -110,7 +155,9 @@ export async function handleChannelCommand(state, action, flags = {}, positional
110
155
  } });
111
156
  result = { ...result, next: audience === 'person'
112
157
  ? 'Give your owner both links: the chat link to send to the person, and the watchUrl where they read along, approve requests and end the chat. The person\'s messages arrive as separate Oomi chat turns; your reply text goes back to them.'
113
- : 'Give the link to the other agent (or your owner to pass on). Its messages arrive as separate Oomi channel turns; your reply text goes back to it. Your owner can watch and end the channel in Oomi.' };
158
+ : 'Give this link to your owner to pass to the other agent. That agent joins from wherever it runs; do not start, install, sign in to or message it yourself, and never ask your owner for sign-in codes for it. Its messages arrive as separate Oomi channel turns; your reply text goes back to it. Your owner can watch and end the channel in Oomi.' };
159
+ const warnings = bridgeWarnings(directory);
160
+ if (warnings.length) result.warnings = [...warnings, 'Tell your owner about these before they share the link: the other side\'s messages will not be answered until they are fixed.'];
114
161
  break;
115
162
  }
116
163
  case 'list': {
@@ -244,8 +291,10 @@ export function startAgentChannelWorker({ state, deliver, connection = '', reque
244
291
  try {
245
292
  reply = await deliver({ sessionKey: item.sessionKey, idempotencyKey: item.messageId, message: channelTurnMessage(item, connection), retryInterrupted: false });
246
293
  } catch (error) {
247
- await request(state, `agent_channels/inbox/${item.messageId}/ack`, { method: 'POST', body: { outcome: 'failed', failureCode: 'host_turn_failed' } }).catch(() => {});
248
- onError(error);
294
+ // Report why, so the owner is told what to fix and the guest isn't left guessing.
295
+ const failure = classifyRuntimeFailure(error);
296
+ await request(state, `agent_channels/inbox/${item.messageId}/ack`, { method: 'POST', body: { outcome: 'failed', failureCode: failure.code, failureDetail: failure.detail } }).catch(() => {});
297
+ onError(Object.assign(error, { failure }));
249
298
  continue;
250
299
  }
251
300
  const text = typeof reply?.text === 'string' ? reply.text.trim() : '';
@@ -1,5 +1,6 @@
1
1
  import WebSocket from 'ws';
2
2
  import { randomUUID } from 'node:crypto';
3
+ import { redactDetail } from './runtimeFailure.js';
3
4
 
4
5
  // The broker's protocol is independent of the installed local gateway version.
5
6
  export const LOCAL_PROTOCOL_MIN = 3;
@@ -48,7 +49,11 @@ export function probeGateway({ gateway, deviceIdentity, prepareFrame, verifyRepl
48
49
  message: turn?.message || 'Oomi connection check. Reply with OOMI_OK only. Do not use tools or change anything.',
49
50
  } }));
50
51
  } else if (verifyReply && frame.event === 'chat' && frame.payload?.runId === runId) {
51
- if (frame.payload.state === 'error' || frame.payload.state === 'aborted') return finish(new Error('Local agent could not complete the verification message.'));
52
+ if (frame.payload.state === 'error' || frame.payload.state === 'aborted') {
53
+ // Keep the runtime's own reason (for example a rejected model login) so callers can say what went wrong.
54
+ const reason = redactDetail(frame.payload.errorMessage || frame.payload.error?.message || frame.payload.state);
55
+ return finish(Object.assign(new Error(`Local agent could not complete the message: ${reason}`), { runtimeDetail: reason }));
56
+ }
52
57
  if (frame.payload.state === 'final') {
53
58
  const content = frame.payload.message?.content;
54
59
  const text = typeof content === 'string' ? content : (Array.isArray(content) ? content.map((part) => part.text || '').join('') : '');
package/lib/localAgent.js CHANGED
@@ -7,6 +7,7 @@ import { createHash, randomUUID } from 'node:crypto';
7
7
  import { runSharedHermesTurn, hermesRefusal } from './hermesGateway.js';
8
8
  import { runHermesAcpTurn, hermesAcpAvailable, resetHermesAcp, warmHermesAcp } from './hermesAcp.js';
9
9
  import { grokbotConfig, runGrokbotTurn, completedGrokbotTurn } from './grokbotRuntime.js';
10
+ import { classifyRuntimeFailure } from './runtimeFailure.js';
10
11
 
11
12
  const IMAGE_EXTENSIONS = { 'image/jpeg': '.jpg', 'image/png': '.png', 'image/webp': '.webp', 'image/gif': '.gif' };
12
13
  const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
@@ -194,7 +195,10 @@ function runAgentProcess(config, request, { signal, timeoutMs = 180000 } = {}) {
194
195
  if (error) return reject(error);
195
196
  if (code !== 0) {
196
197
  const reason = config.runtime === 'hermes' && code === 1 && !diagnosticsTruncated && !output.trim() && diagnostics.match(/^hermes-refusal-reason:\s*([A-Z_]+)(?:\r?\n|$)/)?.[1];
197
- return reject(hermesRefusal(reason) || new Error(`Agent exited with code ${code}. Check local runtime credentials and logs.`));
198
+ // stderr can hold private tool output, so it never enters the message. Keep
199
+ // only the kind of failure its last lines show, such as a rejected provider login.
200
+ const runtimeFailureCode = classifyRuntimeFailure({ message: diagnostics.trim().split(/\r?\n/).slice(-3).join(' ') }).code;
201
+ return reject(hermesRefusal(reason) || Object.assign(new Error(`Agent exited with code ${code}. Check local runtime credentials and logs.`), { runtimeFailureCode }));
198
202
  }
199
203
  try {
200
204
  const result = config.runtime === 'hermes'
@@ -0,0 +1,43 @@
1
+ // Turns a raw agent-runtime error into something an agent, its owner and a
2
+ // guest can act on. The raw text stays on this computer except for a short,
3
+ // redacted excerpt sent to the owner.
4
+
5
+ const RULES = [
6
+ ['model_auth_failed', /\b401\b|\b403\b|unauthori[sz]ed|authenticat|invalid[ _-]?(x-)?api[ _-]?key|api key|oauth|access token|refresh token|token (has )?expired|expired token|not logged in|log ?in again|sign ?in again|please (log|sign) ?in|login (was )?rejected|invalid credentials|credentials? (are |were |have )?(invalid|expired|rejected)/i],
7
+ ['model_rate_limited', /\b429\b|rate[ _-]?limit|quota|usage limit|too many requests/i],
8
+ ['model_unavailable', /\b5\d\d\b|overloaded|unavailable|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET|network|could not reach/i],
9
+ ['runtime_timeout', /timed out|timeout/i],
10
+ ];
11
+
12
+ export const FAILURE_SUMMARIES = {
13
+ model_auth_failed: 'its AI provider rejected its sign-in',
14
+ model_rate_limited: 'its AI provider hit a usage or rate limit',
15
+ model_unavailable: "its AI provider couldn't be reached",
16
+ runtime_timeout: 'it took too long to answer',
17
+ runtime_failed: 'its agent runtime stopped with an error',
18
+ };
19
+
20
+ export const FAILURE_HINTS = {
21
+ model_auth_failed: "Ask your owner to sign in to your AI provider again on this computer themselves (in a terminal or the provider's app here). Never ask them to paste passwords or one-time codes into a chat.",
22
+ model_rate_limited: 'Wait for the provider limit to reset, or ask your owner to raise it.',
23
+ model_unavailable: 'Check this computer is online and the provider is up, then try again.',
24
+ runtime_timeout: 'Try again with a shorter request, or check the agent runtime is responsive.',
25
+ runtime_failed: 'Check the agent runtime logs on this computer, then try again.',
26
+ };
27
+
28
+ export function redactDetail(text, limit = 300) {
29
+ return String(text || '')
30
+ .replace(/Bearer\s+\S+/gi, 'Bearer [redacted]')
31
+ .replace(/\b(sk|pk|rk|oomi_gst|xox[abpr])[-_][A-Za-z0-9_-]{6,}/g, '[redacted]')
32
+ .replace(/[A-Za-z0-9_\-+/=]{32,}/g, '[redacted]')
33
+ .replace(/\s+/g, ' ')
34
+ .trim()
35
+ .slice(-limit);
36
+ }
37
+
38
+ export function classifyRuntimeFailure(error) {
39
+ const raw = [error?.runtimeDetail, error?.message, typeof error === 'string' ? error : ''].filter(Boolean).join(' | ');
40
+ // A runtime that can't share its output (stderr may be private) passes only the code.
41
+ const code = FAILURE_SUMMARIES[error?.runtimeFailureCode] ? error.runtimeFailureCode : (RULES.find(([, pattern]) => pattern.test(raw)) || ['runtime_failed'])[0];
42
+ return { code, summary: FAILURE_SUMMARIES[code], hint: FAILURE_HINTS[code], detail: redactDetail(raw) };
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oomi-ai",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "Connect OpenClaw, Hermes, Grok Bot, and other agents to Oomi with verified runtime adapters",
5
5
  "bin": {
6
6
  "oomi": "bin/oomi-ai.js"
@@ -49,7 +49,7 @@
49
49
  "url": "https://github.com/crispcode-io/oomi/issues"
50
50
  },
51
51
  "scripts": {
52
- "check": "node --check bin/oomi-ai.js && node --check lib/bridgeService.js && node --check lib/bridgeMaintenance.js && node --check lib/localAgent.js && node --check lib/hermesGateway.js && node --check lib/hermesAcp.js && node --check lib/grokbotRuntime.js && node --check lib/connectionSetup.js && node --check lib/processSupervisor.js && node --check lib/agentChannels.js",
52
+ "check": "node --check bin/oomi-ai.js && node --check lib/bridgeService.js && node --check lib/bridgeMaintenance.js && node --check lib/localAgent.js && node --check lib/hermesGateway.js && node --check lib/hermesAcp.js && node --check lib/grokbotRuntime.js && node --check lib/connectionSetup.js && node --check lib/processSupervisor.js && node --check lib/agentChannels.js && node --check lib/runtimeFailure.js",
53
53
  "test": "node --test test/*.test.mjs"
54
54
  },
55
55
  "dependencies": {
@@ -85,6 +85,7 @@
85
85
  "lib/connectionSetup.js",
86
86
  "lib/processSupervisor.js",
87
87
  "lib/agentChannels.js",
88
+ "lib/runtimeFailure.js",
88
89
  "skills/oomi-delivery/SKILL.md"
89
90
  ]
90
91
  }
@@ -158,6 +158,8 @@ The legacy persona-app commands below remain compatibility tools; use Spaces for
158
158
 
159
159
  Use this when your owner asks you to let another agent (Claude, Codex, a Grok bot, or another Oomi agent) talk with you directly. For example: "make a link so Claude can ask you how to set up Obsidian".
160
160
 
161
+ **Your only job is to create the link and then answer.** Your owner gives the link to the other agent, and that agent joins from wherever it already runs, usually another computer. Do not install, launch, sign in to, or message that agent yourself, even if a copy of it is installed on your computer. Never ask your owner for passwords, sign-in links or one-time codes for it. You don't need the other agent to be ready first: create the link right away.
162
+
161
163
  1. Create a channel with a specific purpose: `oomi channel create --purpose "Help Claude set up Obsidian on this computer"`. If useful, add `--expires-minutes N` (default 120, at most 1440) or `--greeting "..."`.
162
164
  2. Give your owner the returned `link`. Tell them it works for one agent only, until `joinBy`. Never post the link publicly or share your device token. The link is all the other agent needs.
163
165
  3. Each message from the other agent arrives as its own turn in a separate conversation (`agent:main:oomi:channel:ID`), headed "Oomi agent channel". Your final reply text is sent back automatically. Do not also send that reply with `oomi channel send`.
@@ -166,6 +168,10 @@ Use this when your owner asks you to let another agent (Claude, Codex, a Grok bo
166
168
 
167
169
  To review a channel, use `oomi channel list` and `oomi channel show ID`.
168
170
 
171
+ **When you can't answer.** Your AI provider might reject your sign-in, hit a limit or be unreachable. If so, oomi records the reason, tells your owner what to fix and tells the other side the message wasn't answered. Tell your owner plainly which AI provider failed and what it said. Ask them to sign in again on this computer themselves, in a terminal or the provider's app here. Never ask them to paste passwords or one-time codes into a chat. `oomi verify --connection NAME` checks that you can answer again. `oomi channel create` also returns `warnings` when your bridge can't answer; tell your owner before they share the link.
172
+
173
+ **Use your own connection.** Turns started by the bridge already use it. Otherwise, pass `--connection NAME` (`oomi bridge list` shows the names). If oomi says it used a connection that isn't linked, rerun with your own. Don't create a new Auth Link unless your owner asked to connect you.
174
+
169
175
  To join another agent's link as a guest:
170
176
 
171
177
  1. Run `oomi channel join LINK`.
@@ -132,6 +132,8 @@ The legacy persona-app commands below remain compatibility tools; use Spaces for
132
132
 
133
133
  Use this when your owner asks you to let another agent (Claude, Codex, a Grok bot, or another Oomi agent) talk with you directly. For example: "make a link so Claude can ask you how to set up Obsidian".
134
134
 
135
+ **Your only job is to create the link and then answer.** Your owner gives the link to the other agent, and that agent joins from wherever it already runs, usually another computer. Do not install, launch, sign in to, or message that agent yourself, even if a copy of it is installed on your computer. Never ask your owner for passwords, sign-in links or one-time codes for it. You don't need the other agent to be ready first: create the link right away.
136
+
135
137
  1. Create a channel with a specific purpose: `oomi channel create --purpose "Help Claude set up Obsidian on this computer"`. If useful, add `--expires-minutes N` (default 120, at most 1440) or `--greeting "..."`.
136
138
  2. Give your owner the returned `link`. Tell them it works for one agent only, until `joinBy`. Never post the link publicly or share your device token. The link is all the other agent needs.
137
139
  3. Each message from the other agent arrives as its own turn in a separate conversation (`agent:main:oomi:channel:ID`), headed "Oomi agent channel". Your final reply text is sent back automatically. Do not also send that reply with `oomi channel send`.
@@ -140,6 +142,10 @@ Use this when your owner asks you to let another agent (Claude, Codex, a Grok bo
140
142
 
141
143
  To review a channel, use `oomi channel list` and `oomi channel show ID`.
142
144
 
145
+ **When you can't answer.** Your AI provider might reject your sign-in, hit a limit or be unreachable. If so, oomi records the reason, tells your owner what to fix and tells the other side the message wasn't answered. Tell your owner plainly which AI provider failed and what it said. Ask them to sign in again on this computer themselves, in a terminal or the provider's app here. Never ask them to paste passwords or one-time codes into a chat. `oomi verify --connection NAME` checks that you can answer again. `oomi channel create` also returns `warnings` when your bridge can't answer; tell your owner before they share the link.
146
+
147
+ **Use your own connection.** Turns started by the bridge already use it. Otherwise, pass `--connection NAME` (`oomi bridge list` shows the names). If oomi says it used a connection that isn't linked, rerun with your own. Don't create a new Auth Link unless your owner asked to connect you.
148
+
143
149
  To join another agent's link as a guest:
144
150
 
145
151
  1. Run `oomi channel join LINK`.