oomi-ai 0.8.0 → 0.8.1

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
@@ -48,9 +48,22 @@ This package exposes connection, bridge, voice, and persona app state APIs. It d
48
48
 
49
49
  Connect the agent the user is speaking to. Identify your runtime and profile first. Never choose OpenClaw merely because it is installed on the same machine as Hermes.
50
50
 
51
+ ## Updating Oomi safely
52
+
53
+ When the user asks you to update Oomi, read `oomi instructions` first; it supersedes copied setup/restart recipes. Keep the same connection on the same computer.
54
+
55
+ 1. Run `oomi update --connection NAME` (omit the selector for default OpenClaw). This only checks versions. If already current, report that and stop: do not reinstall or restart "to be sure."
56
+ 2. If an update exists, run the returned exact-version npm install command **alone**, through the runtime's normal approval flow. Do not append redirections, status checks or restarts. If installation is denied, fails, or cannot be verified, leave the bridge alone and explain the approval/error. Never disable security checks, retry through another executor, or enable blanket unattended approvals.
57
+ 3. After successful installation, check `npm list -g oomi-ai --depth=0` and `oomi update` separately. Only then run `oomi bridge repair --connection NAME`.
58
+ 4. A running 0.8.1+ supervised bridge queues its restart until active replies finish and queued phone replies are saved. If the result is `queued`, finish your reply now; do not wait for your own turn to end, poll in a loop, or claim completion. `oomi status --connection NAME --json` subsequently reports maintenance `completed`/`failed` and the running `packageVersion`. Pairing and conversations stay intact.
59
+
60
+ For packages before 0.8.1 (no `oomi update` command), read the published guide, check `npm list -g oomi-ai --depth=0` and `npm view oomi-ai version` in separate commands. If equal, stop. Otherwise install the exact returned version through normal approvals. The first restart of an older or unsupervised bridge must happen from an independent host terminal/agent session, outside the Oomi conversation it would terminate. New CLI commands refuse to stop their own older host. After that bootstrap, in-chat restarts use the deferred path.
61
+
62
+ Hermes single-query mode has no interactive approval surface. If its package/security check requires a human, tell the user to approve/run the exact install in an approval-capable host session, then continue there. A blocked install is not a reason to restart. Oomi does not override host permissions. `bridge repair` is also available for an actual connection fault when no package update is needed.
63
+
51
64
  ## Standard Connect Flow
52
65
 
53
- Install or update the local package:
66
+ For a new installation (for an existing installation, use the safe update flow below):
54
67
 
55
68
  ```bash
56
69
  npm install -g oomi-ai@latest
@@ -129,7 +142,7 @@ oomi invite --name hermes --app-url https://www.oomi.ai
129
142
 
130
143
  Do not pair again to refresh an expired Auth Link. Re-running `connect` reuses saved pairing and verification; only the first connection or an explicit `oomi verify` sends a model probe. A different command, profile, or working directory requires a new connection name, so one agent cannot silently replace another. Named connections store private state and logs under `~/.oomi/connections/<name>`; default OpenClaw keeps `~/.openclaw`. Since 0.8.0, `connect` installs a separate automatic-start service for each connection: macOS launchd or Linux systemd user. It restarts an exited worker and starts at the next login/user session. Linux servers can start at boot and remain running after logout when the host administrator enables user lingering (`loginctl enable-linger <user>`); `oomi bridge service status --name hermes` reports whether this is enabled. WSL needs systemd and a running WSL instance; native Windows does not yet have an automatic service installer. A sleeping, powered-off, or disconnected computer remains unavailable.
131
144
 
132
- Run `oomi bridge list` to find saved connection names and their startup protection on this computer. To upgrade an existing connection on **each computer**, install `oomi-ai@latest`, then run `oomi bridge repair --name <existing-name>` (omit the name for default OpenClaw). Repair installs/updates its supervisor, preserves pairing and conversations, and waits for the broker connection. It does not send a model probe or switch agents. Do not copy connection credentials between computers.
145
+ Run `oomi bridge list` to find saved connection names and their startup protection on this computer. To update an existing connection on **each computer**, follow Updating Oomi safely above. Repair preserves pairing and conversations. On a running 0.8.1+ supervised bridge it queues a safe restart and reports pending work; on a stopped bridge it installs/starts supervision and waits for the broker connection. It does not send a model probe or switch agents. Do not copy connection credentials between computers.
133
146
 
134
147
  Use `oomi bridge service status --name hermes --json` to inspect startup protection. `oomi bridge stop --name hermes` disables automatic startup and stops only that connection; `oomi bridge start --name hermes` enables it again. `oomi bridge service uninstall --name hermes` removes startup management without deleting saved state. The legacy `oomi openclaw bridge service` command remains supported.
135
148
 
@@ -333,7 +346,7 @@ STT works but the assistant does not reply:
333
346
 
334
347
  Bridge keeps using an old package after update:
335
348
 
336
- - Action: run `oomi openclaw refresh --skip-version-check`.
349
+ - Action: follow Updating Oomi safely above, then inspect `oomi status --json` for running version and maintenance result.
337
350
  - If still stale: run `oomi bridge repair` for default OpenClaw, or add the existing `--name` for a named connection.
338
351
  - On macOS service installs: run `oomi openclaw bridge service restart`.
339
352
  - Confirm with `oomi openclaw bridge ps`.
@@ -101,9 +101,22 @@ Owner-originated identity changes are a deliberate exception: one notice per ide
101
101
 
102
102
  The legacy persona-app commands below remain compatibility tools; use Spaces for new agent-composed personal views.
103
103
 
104
+ ## Updating Oomi safely
105
+
106
+ When the user asks you to update Oomi, read `oomi instructions` first; it supersedes copied setup/restart recipes. Keep the same connection on the same computer.
107
+
108
+ 1. Run `oomi update --connection NAME` (omit the selector for default OpenClaw). This only checks versions. If already current, report that and stop: do not reinstall or restart "to be sure."
109
+ 2. If an update exists, run the returned exact-version npm install command **alone**, through the runtime's normal approval flow. Do not append redirections, status checks or restarts. If installation is denied, fails, or cannot be verified, leave the bridge alone and explain the approval/error. Never disable security checks, retry through another executor, or enable blanket unattended approvals.
110
+ 3. After successful installation, check `npm list -g oomi-ai --depth=0` and `oomi update` separately. Only then run `oomi bridge repair --connection NAME`.
111
+ 4. A running 0.8.1+ supervised bridge queues its restart until active replies finish and queued phone replies are saved. If the result is `queued`, finish your reply now; do not wait for your own turn to end, poll in a loop, or claim completion. `oomi status --connection NAME --json` subsequently reports maintenance `completed`/`failed` and the running `packageVersion`. Pairing and conversations stay intact.
112
+
113
+ For packages before 0.8.1 (no `oomi update` command), read the published guide, check `npm list -g oomi-ai --depth=0` and `npm view oomi-ai version` in separate commands. If equal, stop. Otherwise install the exact returned version through normal approvals. The first restart of an older or unsupervised bridge must happen from an independent host terminal/agent session, outside the Oomi conversation it would terminate. New CLI commands refuse to stop their own older host. After that bootstrap, in-chat restarts use the deferred path.
114
+
115
+ Hermes single-query mode has no interactive approval surface. If its package/security check requires a human, tell the user to approve/run the exact install in an approval-capable host session, then continue there. A blocked install is not a reason to restart. Oomi does not override host permissions. `bridge repair` is also available for an actual connection fault when no package update is needed.
116
+
104
117
  ## Standard Connect Flow
105
118
 
106
- Install or update the local package:
119
+ For a new installation (for an existing installation, use the safe update flow below):
107
120
 
108
121
  ```bash
109
122
  npm install -g oomi-ai@latest
@@ -182,7 +195,7 @@ oomi invite --name hermes --app-url https://www.oomi.ai
182
195
 
183
196
  Do not pair again to refresh an expired Auth Link. Re-running `connect` reuses saved pairing and verification; only the first connection or an explicit `oomi verify` sends a model probe. A different command, profile, or working directory requires a new connection name, so one agent cannot silently replace another. Named connections store private state and logs under `~/.oomi/connections/<name>`; default OpenClaw keeps `~/.openclaw`. Since 0.8.0, `connect` installs a separate automatic-start service for each connection: macOS launchd or Linux systemd user. It restarts an exited worker and starts at the next login/user session. Linux servers can start at boot and remain running after logout when the host administrator enables user lingering (`loginctl enable-linger <user>`); `oomi bridge service status --name hermes` reports whether this is enabled. WSL needs systemd and a running WSL instance; native Windows does not yet have an automatic service installer. A sleeping, powered-off, or disconnected computer remains unavailable.
184
197
 
185
- Run `oomi bridge list` to find saved connection names and their startup protection on this computer. To upgrade an existing connection on **each computer**, install `oomi-ai@latest`, then run `oomi bridge repair --name <existing-name>` (omit the name for default OpenClaw). Repair installs/updates its supervisor, preserves pairing and conversations, and waits for the broker connection. It does not send a model probe or switch agents. Do not copy connection credentials between computers.
198
+ Run `oomi bridge list` to find saved connection names and their startup protection on this computer. To update an existing connection on **each computer**, follow Updating Oomi safely above. Repair preserves pairing and conversations. On a running 0.8.1+ supervised bridge it queues a safe restart and reports pending work; on a stopped bridge it installs/starts supervision and waits for the broker connection. It does not send a model probe or switch agents. Do not copy connection credentials between computers.
186
199
 
187
200
  Use `oomi bridge service status --name hermes --json` to inspect startup protection. `oomi bridge stop --name hermes` disables automatic startup and stops only that connection; `oomi bridge start --name hermes` enables it again. `oomi bridge service uninstall --name hermes` removes startup management without deleting saved state. The legacy `oomi openclaw bridge service` command remains supported.
188
201
 
package/bin/oomi-ai.js CHANGED
@@ -8,6 +8,7 @@ import net from 'net';
8
8
  import { lookup as dnsLookup } from 'dns/promises';
9
9
  import { fileURLToPath } from 'url';
10
10
  import WebSocket from 'ws';
11
+ import { planPackageUpdate, isWorkerDescendant, queueRestart, readMaintenance, createMaintenanceController } from '../lib/bridgeMaintenance.js';
11
12
  import { bridgeServiceConfig, createBridgeService, renderLaunchAgent } from '../lib/bridgeService.js';
12
13
  import { LOCAL_PROTOCOL_MIN, LOCAL_PROTOCOL_MAX, probeGateway } from '../lib/gatewayProbe.js';
13
14
  import { LocalAgentRuntime, localAgentConfig, runLocalAgent, writePrivateJson, startChannelWorker } from '../lib/localAgent.js';
@@ -193,8 +194,7 @@ async function maybeNotifyUpdate(command) {
193
194
 
194
195
  if (compareVersions(currentVersion, latestVersion) < 0) {
195
196
  console.warn(`[oomi] Update available: oomi-ai ${currentVersion} -> ${latestVersion}`);
196
- console.warn('[oomi] Update command: pnpm add -g oomi-ai@latest');
197
- console.warn('[oomi] Fallback update command: npm install -g oomi-ai@latest');
197
+ console.warn('[oomi] Read the safe update plan: oomi update');
198
198
  }
199
199
  }
200
200
 
@@ -202,6 +202,8 @@ function usage() {
202
202
  console.log(`oomi <command>
203
203
 
204
204
  Commands:
205
+ update
206
+ Check for a newer package and return a safe, approval-preserving update plan.
205
207
  connect --runtime openclaw|hermes|command [--name CONNECTION] [--agent-name "Your name"] [--app-url URL]
206
208
  Verify the selected local agent, pair once, start its bridge, and return an Auth Link.
207
209
  Hermes: --profile PROFILE and --resume SESSION select the intended agent/session.
@@ -704,6 +706,9 @@ async function handleContextHealthSyncCommand(flags = {}) {
704
706
  async function refreshBridgeForUpdate(flags = {}, options = {}) {
705
707
  const logger = options.logger || null;
706
708
  const service = currentBridgeService();
709
+ const deferred = deferLiveRestart({ print: false });
710
+ if (deferred) return { queued: true, maintenance: deferred };
711
+ assertIndependentMaintenance();
707
712
  if (service.status().installed) {
708
713
  logger?.(`Restarting supervised bridge ${service.config.target}.`);
709
714
  await stopSupervisedBridge(service);
@@ -771,6 +776,7 @@ async function handleOpenclawRefreshCommand(flags = {}) {
771
776
  return;
772
777
  }
773
778
 
779
+ if (payload.bridge.queued) { console.log(payload.bridge.maintenance.message); return; }
774
780
  console.log(`Oomi refresh complete (${payload.currentVersion})`);
775
781
  if (payload.bridge.restarted) {
776
782
  console.log(`Bridge restarted (${payload.bridge.mode}).`);
@@ -2582,15 +2588,24 @@ async function startOpenclawBridge(flags) {
2582
2588
  consecutiveFailures: 0,
2583
2589
  pid: process.pid,
2584
2590
  startedAt: bridgeNowIso(),
2591
+ packageVersion: currentPackageVersion(),
2592
+ maintenanceProtocol: 1,
2585
2593
  });
2586
2594
 
2595
+ try { refreshSelectedSkill(bridgeState); } catch (err) { console.warn('Oomi skill refresh failed:', err.message); }
2587
2596
  console.log(`Starting ${bridgeState.runtime || 'openclaw'} bridge: device=${deviceId}`);
2597
+ let backgroundTurns = 0;
2598
+ const deliverAgentEvent = async (turn) => {
2599
+ backgroundTurns++;
2600
+ try { return await (localRuntime ? localRuntime.turn(turn) : probeGateway({ gateway, deviceIdentity: gatewayDeviceIdentity, prepareFrame: prepareGatewayFrameForLocalGateway, turn, timeoutMs: 180000 })); }
2601
+ finally { backgroundTurns--; }
2602
+ };
2588
2603
  const stopChannelWorker = localRuntime ? startChannelWorker(bridgeState, localRuntime) : () => {};
2589
2604
  const stopIdentityWorker = startIdentityWorker({
2590
2605
  state: { ...bridgeState, brokerHttp, deviceToken },
2591
2606
  receiptPath: path.join(path.dirname(resolveBridgeStatePath()), 'oomi-identity-receipt.json'),
2592
2607
  // Applying a specific settings revision is idempotent, unlike a user turn.
2593
- deliver: (turn) => localRuntime ? localRuntime.turn({ ...turn, retryInterrupted: false }) : probeGateway({ gateway, deviceIdentity: gatewayDeviceIdentity, prepareFrame: prepareGatewayFrameForLocalGateway, turn, timeoutMs: 180000 }),
2608
+ deliver: deliverAgentEvent,
2594
2609
  acknowledgeCommand: (identity) => `oomi identity acknowledge${bridgeConnectionName ? ` --connection ${bridgeConnectionName}` : ''} --identity-id ${identity.identityId} --revision ${identity.revision}`,
2595
2610
  onError: () => updateBridgeStatus({ identitySyncError: 'Identity notice was interrupted. Read oomi identity get in your agent to apply and acknowledge the pending revision; no model retry was sent.' }),
2596
2611
  });
@@ -2598,7 +2613,7 @@ async function startOpenclawBridge(flags) {
2598
2613
  state: { ...bridgeState, brokerHttp, deviceToken },
2599
2614
  connection: bridgeConnectionName,
2600
2615
  receiptPath: path.join(path.dirname(resolveBridgeStatePath()), 'oomi-routine-receipt.json'),
2601
- deliver: (turn) => localRuntime ? localRuntime.turn(turn) : probeGateway({ gateway, deviceIdentity: gatewayDeviceIdentity, prepareFrame: prepareGatewayFrameForLocalGateway, turn, timeoutMs: 180000 }),
2616
+ deliver: deliverAgentEvent,
2602
2617
  onError: () => updateBridgeStatus({ routineSyncError: 'Routine updates are waiting for the runtime. They will retry.' }),
2603
2618
  });
2604
2619
  console.log(`Local gateway: ${gateway.gatewayUrl}`);
@@ -2608,6 +2623,19 @@ async function startOpenclawBridge(flags) {
2608
2623
  );
2609
2624
 
2610
2625
  const activeGatewaySockets = new Map();
2626
+ const activeGatewayRuns = new Set();
2627
+ let currentBrokerSocket;
2628
+ const maintenanceTick = createMaintenanceController({
2629
+ directory: path.dirname(resolveBridgeStatePath()),
2630
+ isIdle: () => !backgroundTurns && !localRuntime?.active.size && !localRuntime?.deliveries && !activeGatewayRuns.size
2631
+ && !currentBrokerSocket?.bufferedAmount
2632
+ && [...activeGatewaySockets.values()].every((session) => !session.pendingRequests?.size),
2633
+ isConnected: () => readBridgeStatus().status === 'connected',
2634
+ restart: () => currentBridgeService().restart(),
2635
+ });
2636
+ const maintenanceTimer = setInterval(() => {
2637
+ try { maintenanceTick(); } catch (err) { console.error('[bridge] maintenance:', err.message); }
2638
+ }, 250);
2611
2639
  const reconnectState = {
2612
2640
  attempt: 0,
2613
2641
  timer: null,
@@ -2960,6 +2988,7 @@ async function startOpenclawBridge(flags) {
2960
2988
  wsUrl.searchParams.set('token', deviceToken);
2961
2989
 
2962
2990
  const brokerSocket = new WebSocket(wsUrl.toString());
2991
+ currentBrokerSocket = brokerSocket;
2963
2992
  let actionCableHeartbeat = null;
2964
2993
 
2965
2994
  const clearChallengeTimer = (sessionBridge) => {
@@ -3322,6 +3351,12 @@ async function startOpenclawBridge(flags) {
3322
3351
  }
3323
3352
  }
3324
3353
 
3354
+ if (gatewayPayload?.type === 'res' && gatewayPayload.ok && gatewayPayload.payload?.status === 'started' && gatewayPayload.payload?.runId) {
3355
+ activeGatewayRuns.add(gatewayPayload.payload.runId);
3356
+ }
3357
+ if (gatewayPayload?.event === 'chat' && ['final', 'error', 'aborted'].includes(gatewayPayload.payload?.state)) {
3358
+ activeGatewayRuns.delete(gatewayPayload.payload.runId);
3359
+ }
3325
3360
  if (gatewayPayload?.event === 'chat' && gatewayPayload.payload?.state === 'final') {
3326
3361
  updateBridgeStatus({ runtimeReady: true, lastReplyAt: bridgeNowIso() });
3327
3362
  } else if (gatewayPayload?.event === 'chat' && gatewayPayload.payload?.state === 'error') {
@@ -3775,6 +3810,7 @@ async function startOpenclawBridge(flags) {
3775
3810
 
3776
3811
  const markStopped = async (signal) => {
3777
3812
  reconnectState.stopped = true;
3813
+ clearInterval(maintenanceTimer);
3778
3814
  stopChannelWorker();
3779
3815
  stopIdentityWorker();
3780
3816
  stopRoutineWorker();
@@ -3898,11 +3934,6 @@ async function pairAndStartOpenclawBridge(flags) {
3898
3934
  console.log('');
3899
3935
  console.log('Share this exact response with the user (no extra text):');
3900
3936
  console.log('----------------------------------------');
3901
- const skillSource = resolveSkillSource();
3902
- if (runtime === 'hermes') installSkill(skillSource, [path.join(state.localAgent.hermesHome, ...(state.localAgent.profile === 'default' ? [] : ['profiles', state.localAgent.profile]), 'skills')]);
3903
- else if (runtime === 'openclaw') installSkill(skillSource, [resolveOpenclawSkillsDir()]);
3904
- else if (flags['skills-dir']) installSkill(skillSource, [flags['skills-dir']]);
3905
- console.log('Oomi tools: run oomi capabilities, then oomi instructions. Add --connection ' + (bridgeConnectionName || '(omit for default)') + ' to commands. Passive updates do not invoke your model.');
3906
3937
  console.log('Oomi Connect Ready');
3907
3938
  console.log(`Auth Link: ${inviteUrl}`);
3908
3939
  console.log('----------------------------------------');
@@ -4019,6 +4050,7 @@ function printOpenclawBridgeStatus(flags) {
4019
4050
  },
4020
4051
  runtime: runtimeStatus,
4021
4052
  supervision: currentBridgeService().status(),
4053
+ maintenance: readMaintenance(path.dirname(resolveBridgeStatePath())),
4022
4054
  };
4023
4055
 
4024
4056
  if (jsonOutput) {
@@ -4026,6 +4058,7 @@ function printOpenclawBridgeStatus(flags) {
4026
4058
  return;
4027
4059
  }
4028
4060
 
4061
+ if (payload.maintenance) console.log(`Maintenance: ${payload.maintenance.status} — ${payload.maintenance.message || ''}`);
4029
4062
  console.log('Oomi Bridge Status');
4030
4063
  const supervision = payload.supervision;
4031
4064
  console.log(`Automatic startup: ${supervision.installed && supervision.enabled ? supervision.manager + ' (' + supervision.startsAt + ')' : 'off'}`);
@@ -4168,7 +4201,25 @@ function requireSavedBridge() {
4168
4201
  if (!readBridgeState().deviceToken) throw new Error('No saved connection. Run oomi connect with an explicit runtime first.');
4169
4202
  }
4170
4203
 
4204
+ function assertIndependentMaintenance() {
4205
+ const running = findRunningBridgeProcess();
4206
+ if (running && isWorkerDescendant(running.pid)) {
4207
+ throw new Error('This command would stop the agent carrying this conversation. Use bridge repair/restart with a running 0.8.1+ bridge, or perform the first upgrade from an independent host terminal. The bridge was left unchanged.');
4208
+ }
4209
+ }
4210
+
4211
+ function deferLiveRestart({ print = true } = {}) {
4212
+ const status = readBridgeStatus();
4213
+ const service = currentBridgeService().status();
4214
+ if (status.maintenanceProtocol !== 1 || !status.pid || !isBridgeProcess(status.pid)
4215
+ || !service.running || service.pid !== status.pid || !service.enabled) return false;
4216
+ const request = queueRestart(path.dirname(resolveBridgeStatePath()), status.pid);
4217
+ if (print) console.log(JSON.stringify(request, null, 2));
4218
+ return request;
4219
+ }
4220
+
4171
4221
  async function stopSupervisedBridge(service) {
4222
+ assertIndependentMaintenance();
4172
4223
  // Unload/disable before signalling the worker, otherwise KeepAlive immediately recreates it.
4173
4224
  service.stop();
4174
4225
  const stopped = await stopBridgeProcesses();
@@ -4187,6 +4238,8 @@ async function handleBridgeServiceCommand(actionRaw = '', flags = {}) {
4187
4238
  throw new Error('Use: oomi bridge service [install|start|stop|restart|status|uninstall] [--name NAME]');
4188
4239
  }
4189
4240
  if (['install', 'start', 'restart'].includes(action)) requireSavedBridge();
4241
+ if (['install', 'restart'].includes(action) && !isTruthyFlag(flags['no-start']) && deferLiveRestart()) return;
4242
+ if (action !== 'start') assertIndependentMaintenance();
4190
4243
  if (action === 'install') {
4191
4244
  service.install({ noStart: true });
4192
4245
  await stopSupervisedBridge(service);
@@ -4209,6 +4262,7 @@ async function handleBridgeServiceCommand(actionRaw = '', flags = {}) {
4209
4262
 
4210
4263
  async function repairBridge(flags = {}) {
4211
4264
  requireSavedBridge();
4265
+ if (!isTruthyFlag(flags['no-start']) && deferLiveRestart()) return;
4212
4266
  await handleBridgeServiceCommand('install', flags);
4213
4267
  if (!isTruthyFlag(flags['no-start'])) {
4214
4268
  await waitForBridgeConnection({});
@@ -4291,6 +4345,8 @@ async function handleBridgeLifecycleCommand(flags = {}, actionRaw = '') {
4291
4345
  return handleBridgeServiceCommand(action, flags);
4292
4346
  }
4293
4347
 
4348
+ if (['stop', 'restart'].includes(action)) assertIndependentMaintenance();
4349
+
4294
4350
  if (action === 'start' || action === 'ensure') {
4295
4351
  await startBridgeLifecycle(flags);
4296
4352
  return;
@@ -4364,7 +4420,16 @@ async function verifyLocalAgent(state) {
4364
4420
  return { runtime: state.runtime || 'openclaw', ...result };
4365
4421
  }
4366
4422
 
4423
+ function refreshSelectedSkill(state, flags = {}) {
4424
+ const source = resolveSkillSource();
4425
+ if (state.runtime === 'hermes' && state.localAgent?.hermesHome) {
4426
+ installSkill(source, [path.join(state.localAgent.hermesHome, ...(state.localAgent.profile === 'default' ? [] : ['profiles', state.localAgent.profile]), 'skills')]);
4427
+ } else if (!state.runtime || state.runtime === 'openclaw') installSkill(source, [resolveOpenclawSkillsDir()]);
4428
+ else if (flags['skills-dir']) installSkill(source, [flags['skills-dir']]);
4429
+ }
4430
+
4367
4431
  async function connectLocalAgent(flags) {
4432
+ assertIndependentMaintenance();
4368
4433
  const agentName = flags['agent-name'] === undefined ? undefined : validateAgentName(flags['agent-name']);
4369
4434
  const runtime = String(flags.runtime || '');
4370
4435
  if (!['openclaw', 'hermes', 'command'].includes(runtime)) throw new Error('Specify --runtime openclaw, hermes, or command. Never select a different agent just because it is installed.');
@@ -4436,6 +4501,7 @@ async function connectLocalAgent(flags) {
4436
4501
  console.log(unmanaged ? 'Automatic startup: off (--no-service); your host supervisor must manage recovery.'
4437
4502
  : `Automatic startup: ${service.config.platform === 'darwin' ? 'macOS login service' : 'Linux user service'}; restarts after worker exits.`);
4438
4503
  const invite = await requestConnectInviteLink({ backendHttp: state.brokerHttp, appUrl, sessionKey: state.sessionKey, deviceToken: state.deviceToken });
4504
+ refreshSelectedSkill(state, flags);
4439
4505
  console.log(`Verified agent: ${runtime}${state.localAgent?.profile ? ` (${state.localAgent.profile})` : ''}`);
4440
4506
  console.log('Oomi Connect Ready');
4441
4507
  console.log(`Auth Link: ${invite.inviteUrl}`);
@@ -4457,9 +4523,10 @@ async function main() {
4457
4523
  if (connection && !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(connection)) throw new Error('Connection name must use lowercase letters, numbers, underscores, or hyphens.');
4458
4524
  bridgeConnectionName = connection || '';
4459
4525
 
4526
+ if (command === 'update') { console.log(JSON.stringify(planPackageUpdate(currentPackageVersion(), await fetchLatestPublishedVersion('oomi-ai')), null, 2)); return; }
4460
4527
  if (command === 'instructions') { console.log(fs.readFileSync(resolveInstructionsFile(), 'utf8')); return; }
4461
4528
  if (command === 'capabilities') {
4462
- console.log(JSON.stringify({ instructions: 'oomi instructions', skill: path.join(PACKAGE_ROOT, 'skills/oomi/SKILL.md'), connectionSelector: '--connection NAME',
4529
+ console.log(JSON.stringify({ instructions: 'oomi instructions', packageUpdate: 'oomi update', skill: path.join(PACKAGE_ROOT, 'skills/oomi/SKILL.md'), connectionSelector: '--connection NAME',
4463
4530
  spaces: { catalog: 'oomi spaces catalog', list: 'oomi spaces list', create: 'oomi spaces create --file space.json', update: 'oomi spaces update ID --file change.json', revisions: 'oomi spaces revisions ID', upgrade: 'oomi spaces upgrade ID --file upgrade.json', guide: 'oomi instructions' },
4464
4531
  health: { steps: 'oomi context health --window week --metric steps --json', calendarWeek: 'oomi context health --window range --from YYYY-MM-DD --to YYYY-MM-DD --metric steps --json', largeResults: '--output new-file.json', plugins: 'oomi context plugins', missing: 'unknown, never zero', week: 'rolling seven days; use explicit range for calendar week' },
4465
4532
  objectives: { list: 'oomi objectives list', show: 'oomi objectives show ID', history: 'oomi objectives history ID [--before YYYY-MM-DD]', create: 'oomi objectives create --file goal.json', update: 'oomi objectives update ID --file change.json', respond: 'oomi objectives respond ID --file response.json', calendar: 'daily/weekly/monthly/once; stored time zone', streaks: 'optional goal references in the Space catalog' },
@@ -0,0 +1,64 @@
1
+ import path from 'node:path';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { readJson, writePrivateJson } from './localAgent.js';
5
+
6
+ export const maintenancePath = (directory) => path.join(directory, 'oomi-bridge-maintenance.json');
7
+ export const readMaintenance = (directory) => readJson(maintenancePath(directory), null);
8
+ const pending = (request) => ['queued', 'restarting'].includes(request?.status);
9
+
10
+ // This is a plan, never an installer: the runtime keeps its normal package approval gate.
11
+ export function planPackageUpdate(current, latest) {
12
+ const valid = (v) => typeof v === 'string' && /^\d+\.\d+\.\d+$/.test(v);
13
+ if (!valid(current) || !valid(latest)) throw new Error('Could not verify the published version. Leave the package and bridge unchanged; try again later.');
14
+ const a = current.split('.').map(Number), b = latest.split('.').map(Number);
15
+ const difference = b.map((v, i) => v - a[i]).find((v) => v !== 0) || 0;
16
+ return { currentVersion: current, latestVersion: latest, status: difference > 0 ? 'update_available' : 'up_to_date',
17
+ ...(difference > 0 ? { installCommand: `npm install -g oomi-ai@${latest}`,
18
+ instruction: 'Run this command alone through your normal approval flow. If blocked or unsuccessful, stop and report the approval needed; do not restart or change security settings. After success, verify the installed version, then run oomi bridge repair for this connection.' }
19
+ : { instruction: 'Already current. Do not reinstall or restart. Check oomi status only if the user also reports a connection problem.' }) };
20
+ }
21
+
22
+ export function isWorkerDescendant(workerPid, { pid = process.pid, run = spawnSync } = {}) {
23
+ if (!Number.isInteger(workerPid) || workerPid <= 0) return false;
24
+ const result = run('ps', ['-axo', 'pid=,ppid='], { encoding: 'utf8', timeout: 5000 });
25
+ if (result.status !== 0) throw new Error('Cannot establish whether this command would stop its own agent. Run maintenance from an independent host terminal.');
26
+ const parents = new Map(String(result.stdout).trim().split('\n').map((line) => line.trim().split(/\s+/).map(Number)));
27
+ const visited = new Set();
28
+ while (pid > 1 && !visited.has(pid)) {
29
+ if (pid === workerPid) return true;
30
+ visited.add(pid); pid = parents.get(pid);
31
+ }
32
+ return false;
33
+ }
34
+
35
+ export function queueRestart(directory, workerPid, now = Date.now()) {
36
+ const previous = readMaintenance(directory);
37
+ if (pending(previous) && previous.workerPid === workerPid && previous.expiresAt > now) return previous;
38
+ const request = { id: randomUUID(), status: 'queued', action: 'restart', workerPid, requestedAt: now, expiresAt: now + 300000,
39
+ message: 'Restart queued. Finish this reply; the bridge will restart after active replies are delivered. Check oomi status afterward; queued is not completed.' };
40
+ writePrivateJson(maintenancePath(directory), request);
41
+ return request;
42
+ }
43
+
44
+ // One host supervisor owns the replacement. Never disable/unload the service from its own job.
45
+ export function createMaintenanceController({ directory, pid = process.pid, isIdle, isConnected, restart, now = Date.now }) {
46
+ let idleSince = null;
47
+ return () => {
48
+ const request = readMaintenance(directory);
49
+ if (!pending(request)) { idleSince = null; return; }
50
+ const finish = (status, message) => writePrivateJson(maintenancePath(directory), { ...request, status, message, finishedAt: now() });
51
+ if (request.workerPid !== pid) {
52
+ if (isConnected()) finish('completed', 'Bridge restarted and reconnected.');
53
+ else if (now() > request.expiresAt) finish('failed', 'The replacement bridge has not reconnected. Check oomi status and the host connection.');
54
+ return;
55
+ }
56
+ if (now() > request.expiresAt) { finish('failed', 'Restart expired while waiting for replies; no further restart will be attempted.'); return; }
57
+ if (request.status !== 'queued') return;
58
+ if (!isIdle()) { idleSince = null; return; }
59
+ idleSince ??= now();
60
+ if (now() - idleSince < 1000) return;
61
+ writePrivateJson(maintenancePath(directory), { ...request, status: 'restarting' });
62
+ try { restart(); } catch (err) { finish('failed', err.message); }
63
+ };
64
+ }
@@ -18,7 +18,7 @@ export function bridgeServiceConfig({ name = '', stateDirectory, logPath, script
18
18
  if (name && !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(name)) throw new Error('Invalid connection name.');
19
19
  const label = `ai.oomi.bridge${name ? '.' + name : ''}`;
20
20
  const directory = path.resolve(stateDirectory);
21
- const environment = { OOMI_SKIP_UPDATE_CHECK: '1', OOMI_CONNECTION: name, PATH: env.PATH || '/usr/local/bin:/usr/bin:/bin',
21
+ const environment = { HOME: home, OOMI_SKIP_UPDATE_CHECK: '1', OOMI_CONNECTION: name, PATH: env.PATH || '/usr/local/bin:/usr/bin:/bin',
22
22
  // Always pin the state root. A login service must not resolve another connection from its environment.
23
23
  OPENCLAW_HOME: path.resolve(env.OPENCLAW_HOME || path.join(home, '.openclaw')) };
24
24
  if (name) environment.OOMI_CONNECTIONS_DIR = path.dirname(directory);
@@ -158,6 +158,13 @@ export function createBridgeService(config, { run = spawnSync } = {}) {
158
158
  else if (!current.running) invoke(['kickstart', config.target]);
159
159
  } else invoke(['enable', '--now', config.target]);
160
160
  }
161
+ function restart() {
162
+ preflight();
163
+ const current = status();
164
+ if (!current.installed || !current.loaded || !current.enabled) throw new Error('Restart requires an enabled, loaded service. Repair it from an independent host terminal.');
165
+ // Submit one supervisor-owned replacement, so killing the caller cannot strand the service.
166
+ invoke(mac ? ['kickstart', '-k', config.target] : ['--no-block', 'restart', config.target]);
167
+ }
161
168
  function install({ noStart = false } = {}) {
162
169
  assertAvailable();
163
170
  writeDefinition();
@@ -169,5 +176,5 @@ export function createBridgeService(config, { run = spawnSync } = {}) {
169
176
  if (fs.existsSync(config.file)) fs.unlinkSync(config.file);
170
177
  if (!mac) invoke(['daemon-reload']);
171
178
  }
172
- return { config, availability, preflight, status, install, start, stop, uninstall };
179
+ return { config, availability, preflight, status, install, start, stop, restart, uninstall };
173
180
  }
package/lib/localAgent.js CHANGED
@@ -165,6 +165,7 @@ export class LocalAgentRuntime {
165
165
  this.onHealth = onHealth;
166
166
  this.queue = Promise.resolve();
167
167
  this.active = new Map();
168
+ this.deliveries = 0;
168
169
  }
169
170
  file(sessionKey) { return path.join(this.directory, 'conversations', createHash('sha256').update(sessionKey).digest('hex') + '.json'); }
170
171
  history(sessionKey) { return readJson(this.file(sessionKey), { messages: [], receipts: {} }); }
@@ -269,6 +270,7 @@ export function startChannelWorker(state, runtime) {
269
270
  return response.json();
270
271
  };
271
272
  const poll = async () => {
273
+ runtime.deliveries++;
272
274
  try {
273
275
  const { messages = [] } = await post('poll', { limit: 1 });
274
276
  for (const message of messages) {
@@ -286,7 +288,7 @@ export function startChannelWorker(state, runtime) {
286
288
  }
287
289
  } catch (err) {
288
290
  runtime.onHealth({ status: 'degraded', lastErrorMessage: err.message });
289
- } finally { if (!stopped) timer = setTimeout(poll, 3000); }
291
+ } finally { runtime.deliveries--; if (!stopped) timer = setTimeout(poll, 3000); }
290
292
  };
291
293
  timer = setTimeout(poll, 0);
292
294
  return () => { stopped = true; clearTimeout(timer); for (const entry of runtime.active.values()) entry.controller.abort(); };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oomi-ai",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Connect OpenClaw, Hermes, and other local 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",
52
+ "check": "node --check bin/oomi-ai.js && node --check lib/bridgeService.js && node --check lib/bridgeMaintenance.js",
53
53
  "test": "node --test test/*.test.mjs"
54
54
  },
55
55
  "dependencies": {
@@ -77,6 +77,7 @@
77
77
  "lib/routines.js",
78
78
  "lib/spaces.js",
79
79
  "lib/objectives.js",
80
- "lib/bridgeService.js"
80
+ "lib/bridgeService.js",
81
+ "lib/bridgeMaintenance.js"
81
82
  ]
82
83
  }
@@ -127,9 +127,22 @@ Owner-originated identity changes are a deliberate exception: one notice per ide
127
127
 
128
128
  The legacy persona-app commands below remain compatibility tools; use Spaces for new agent-composed personal views.
129
129
 
130
+ ## Updating Oomi safely
131
+
132
+ When the user asks you to update Oomi, read `oomi instructions` first; it supersedes copied setup/restart recipes. Keep the same connection on the same computer.
133
+
134
+ 1. Run `oomi update --connection NAME` (omit the selector for default OpenClaw). This only checks versions. If already current, report that and stop: do not reinstall or restart "to be sure."
135
+ 2. If an update exists, run the returned exact-version npm install command **alone**, through the runtime's normal approval flow. Do not append redirections, status checks or restarts. If installation is denied, fails, or cannot be verified, leave the bridge alone and explain the approval/error. Never disable security checks, retry through another executor, or enable blanket unattended approvals.
136
+ 3. After successful installation, check `npm list -g oomi-ai --depth=0` and `oomi update` separately. Only then run `oomi bridge repair --connection NAME`.
137
+ 4. A running 0.8.1+ supervised bridge queues its restart until active replies finish and queued phone replies are saved. If the result is `queued`, finish your reply now; do not wait for your own turn to end, poll in a loop, or claim completion. `oomi status --connection NAME --json` subsequently reports maintenance `completed`/`failed` and the running `packageVersion`. Pairing and conversations stay intact.
138
+
139
+ For packages before 0.8.1 (no `oomi update` command), read the published guide, check `npm list -g oomi-ai --depth=0` and `npm view oomi-ai version` in separate commands. If equal, stop. Otherwise install the exact returned version through normal approvals. The first restart of an older or unsupervised bridge must happen from an independent host terminal/agent session, outside the Oomi conversation it would terminate. New CLI commands refuse to stop their own older host. After that bootstrap, in-chat restarts use the deferred path.
140
+
141
+ Hermes single-query mode has no interactive approval surface. If its package/security check requires a human, tell the user to approve/run the exact install in an approval-capable host session, then continue there. A blocked install is not a reason to restart. Oomi does not override host permissions. `bridge repair` is also available for an actual connection fault when no package update is needed.
142
+
130
143
  ## Standard Connect Flow
131
144
 
132
- Install or update the local package:
145
+ For a new installation (for an existing installation, use the safe update flow below):
133
146
 
134
147
  ```bash
135
148
  npm install -g oomi-ai@latest
@@ -208,7 +221,7 @@ oomi invite --name hermes --app-url https://www.oomi.ai
208
221
 
209
222
  Do not pair again to refresh an expired Auth Link. Re-running `connect` reuses saved pairing and verification; only the first connection or an explicit `oomi verify` sends a model probe. A different command, profile, or working directory requires a new connection name, so one agent cannot silently replace another. Named connections store private state and logs under `~/.oomi/connections/<name>`; default OpenClaw keeps `~/.openclaw`. Since 0.8.0, `connect` installs a separate automatic-start service for each connection: macOS launchd or Linux systemd user. It restarts an exited worker and starts at the next login/user session. Linux servers can start at boot and remain running after logout when the host administrator enables user lingering (`loginctl enable-linger <user>`); `oomi bridge service status --name hermes` reports whether this is enabled. WSL needs systemd and a running WSL instance; native Windows does not yet have an automatic service installer. A sleeping, powered-off, or disconnected computer remains unavailable.
210
223
 
211
- Run `oomi bridge list` to find saved connection names and their startup protection on this computer. To upgrade an existing connection on **each computer**, install `oomi-ai@latest`, then run `oomi bridge repair --name <existing-name>` (omit the name for default OpenClaw). Repair installs/updates its supervisor, preserves pairing and conversations, and waits for the broker connection. It does not send a model probe or switch agents. Do not copy connection credentials between computers.
224
+ Run `oomi bridge list` to find saved connection names and their startup protection on this computer. To update an existing connection on **each computer**, follow Updating Oomi safely above. Repair preserves pairing and conversations. On a running 0.8.1+ supervised bridge it queues a safe restart and reports pending work; on a stopped bridge it installs/starts supervision and waits for the broker connection. It does not send a model probe or switch agents. Do not copy connection credentials between computers.
212
225
 
213
226
  Use `oomi bridge service status --name hermes --json` to inspect startup protection. `oomi bridge stop --name hermes` disables automatic startup and stops only that connection; `oomi bridge start --name hermes` enables it again. `oomi bridge service uninstall --name hermes` removes startup management without deleting saved state. The legacy `oomi openclaw bridge service` command remains supported.
214
227
 
@@ -101,9 +101,22 @@ Owner-originated identity changes are a deliberate exception: one notice per ide
101
101
 
102
102
  The legacy persona-app commands below remain compatibility tools; use Spaces for new agent-composed personal views.
103
103
 
104
+ ## Updating Oomi safely
105
+
106
+ When the user asks you to update Oomi, read `oomi instructions` first; it supersedes copied setup/restart recipes. Keep the same connection on the same computer.
107
+
108
+ 1. Run `oomi update --connection NAME` (omit the selector for default OpenClaw). This only checks versions. If already current, report that and stop: do not reinstall or restart "to be sure."
109
+ 2. If an update exists, run the returned exact-version npm install command **alone**, through the runtime's normal approval flow. Do not append redirections, status checks or restarts. If installation is denied, fails, or cannot be verified, leave the bridge alone and explain the approval/error. Never disable security checks, retry through another executor, or enable blanket unattended approvals.
110
+ 3. After successful installation, check `npm list -g oomi-ai --depth=0` and `oomi update` separately. Only then run `oomi bridge repair --connection NAME`.
111
+ 4. A running 0.8.1+ supervised bridge queues its restart until active replies finish and queued phone replies are saved. If the result is `queued`, finish your reply now; do not wait for your own turn to end, poll in a loop, or claim completion. `oomi status --connection NAME --json` subsequently reports maintenance `completed`/`failed` and the running `packageVersion`. Pairing and conversations stay intact.
112
+
113
+ For packages before 0.8.1 (no `oomi update` command), read the published guide, check `npm list -g oomi-ai --depth=0` and `npm view oomi-ai version` in separate commands. If equal, stop. Otherwise install the exact returned version through normal approvals. The first restart of an older or unsupervised bridge must happen from an independent host terminal/agent session, outside the Oomi conversation it would terminate. New CLI commands refuse to stop their own older host. After that bootstrap, in-chat restarts use the deferred path.
114
+
115
+ Hermes single-query mode has no interactive approval surface. If its package/security check requires a human, tell the user to approve/run the exact install in an approval-capable host session, then continue there. A blocked install is not a reason to restart. Oomi does not override host permissions. `bridge repair` is also available for an actual connection fault when no package update is needed.
116
+
104
117
  ## Standard Connect Flow
105
118
 
106
- Install or update the local package:
119
+ For a new installation (for an existing installation, use the safe update flow below):
107
120
 
108
121
  ```bash
109
122
  npm install -g oomi-ai@latest
@@ -182,7 +195,7 @@ oomi invite --name hermes --app-url https://www.oomi.ai
182
195
 
183
196
  Do not pair again to refresh an expired Auth Link. Re-running `connect` reuses saved pairing and verification; only the first connection or an explicit `oomi verify` sends a model probe. A different command, profile, or working directory requires a new connection name, so one agent cannot silently replace another. Named connections store private state and logs under `~/.oomi/connections/<name>`; default OpenClaw keeps `~/.openclaw`. Since 0.8.0, `connect` installs a separate automatic-start service for each connection: macOS launchd or Linux systemd user. It restarts an exited worker and starts at the next login/user session. Linux servers can start at boot and remain running after logout when the host administrator enables user lingering (`loginctl enable-linger <user>`); `oomi bridge service status --name hermes` reports whether this is enabled. WSL needs systemd and a running WSL instance; native Windows does not yet have an automatic service installer. A sleeping, powered-off, or disconnected computer remains unavailable.
184
197
 
185
- Run `oomi bridge list` to find saved connection names and their startup protection on this computer. To upgrade an existing connection on **each computer**, install `oomi-ai@latest`, then run `oomi bridge repair --name <existing-name>` (omit the name for default OpenClaw). Repair installs/updates its supervisor, preserves pairing and conversations, and waits for the broker connection. It does not send a model probe or switch agents. Do not copy connection credentials between computers.
198
+ Run `oomi bridge list` to find saved connection names and their startup protection on this computer. To update an existing connection on **each computer**, follow Updating Oomi safely above. Repair preserves pairing and conversations. On a running 0.8.1+ supervised bridge it queues a safe restart and reports pending work; on a stopped bridge it installs/starts supervision and waits for the broker connection. It does not send a model probe or switch agents. Do not copy connection credentials between computers.
186
199
 
187
200
  Use `oomi bridge service status --name hermes --json` to inspect startup protection. `oomi bridge stop --name hermes` disables automatic startup and stops only that connection; `oomi bridge start --name hermes` enables it again. `oomi bridge service uninstall --name hermes` removes startup management without deleting saved state. The legacy `oomi openclaw bridge service` command remains supported.
188
201