oomi-ai 0.2.42 → 0.2.45

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/bin/oomi-ai.js CHANGED
@@ -14,35 +14,37 @@ import { startPersonaJobPoller } from '../lib/personaJobPoller.js';
14
14
  import { startPersonaRuntimeSupervisor } from '../lib/personaRuntimeSupervisor.js';
15
15
  import { executePersonaJob, extractPersonaJobPayload } from '../lib/personaJobExecutor.js';
16
16
  import { inferSpokenMetadataFromContent, normalizeSpokenMetadata } from '../lib/spokenMetadata.js';
17
- import {
18
- resolveOpenclawBridgeLiveLogPath,
19
- resolveOpenclawBridgeLockPath,
20
- resolveOpenclawBridgeStatePath,
21
- resolveOpenclawBridgeStatusPath,
22
- resolveOpenclawConfigCandidates,
23
- resolveOpenclawHome,
24
- resolveOpenclawIdentityPath,
25
- resolveOpenclawProfilePath,
26
- resolveOpenclawSkillsDir,
27
- resolveOpenclawUpdateStatePath,
28
- resolveOpenclawWorkspaceRoot,
29
- } from '../lib/openclawPaths.js';
17
+ import {
18
+ resolveOpenclawBridgeLiveLogPath,
19
+ resolveOpenclawBridgeLockPath,
20
+ resolveOpenclawBridgeStatePath,
21
+ resolveOpenclawBridgeStatusPath,
22
+ resolveOpenclawConfigCandidates,
23
+ resolveOpenclawHome,
24
+ resolveOpenclawIdentityPath,
25
+ resolveOpenclawLegacyPersonasDir,
26
+ resolveOpenclawProfilePath,
27
+ resolveOpenclawSkillsDir,
28
+ resolveOpenclawUpdateStatePath,
29
+ resolveOpenclawWorkspaceRoot,
30
+ } from '../lib/openclawPaths.js';
30
31
  import {
31
32
  applyOpenclawProfile,
32
33
  buildOomiDevLocalProfile,
33
34
  readOpenclawProfile,
34
35
  writeOpenclawProfile,
35
36
  } from '../lib/openclawProfile.js';
36
- import {
37
- buildLocalPersonaRuntime,
38
- defaultPersonaWorkspaceRoot,
39
- installPersonaWorkspace,
40
- isPersonaWorkspaceProcessRunning,
41
- resolvePersonaDevCommand,
42
- startPersonaWorkspace,
43
- stopPersonaWorkspace,
44
- waitForPersonaRuntime,
45
- } from '../lib/personaRuntimeProcess.js';
37
+ import {
38
+ buildLocalPersonaRuntime,
39
+ defaultPersonaWorkspaceRoot,
40
+ installPersonaWorkspace,
41
+ isPersonaWorkspaceProcessRunning,
42
+ resolvePersonaHealthPath,
43
+ resolvePersonaDevCommand,
44
+ startPersonaWorkspace,
45
+ stopPersonaWorkspace,
46
+ waitForPersonaRuntime,
47
+ } from '../lib/personaRuntimeProcess.js';
46
48
  import {
47
49
  destroyManagedPersonaRuntime,
48
50
  getManagedPersonaRuntimeStatus,
@@ -50,13 +52,17 @@ import {
50
52
  slugifyPersonaName,
51
53
  stopManagedPersonaRuntime,
52
54
  } from '../lib/personaRuntimeManager.js';
55
+ import {
56
+ readPersonaRuntimeState,
57
+ resolvePersonaWorkspacePath,
58
+ } from '../lib/personaRuntimeRegistry.js';
53
59
  import { startLocalGatewayAgentServer } from '../lib/openclawDevGateway.js';
54
60
  import { ensureSessionBridge, flushSessionQueue, flushWaitingForConnect, forwardFrameToSession } from './sessionBridgeState.js';
55
61
 
56
62
  const MARKER_START = '<oomi-agent-instructions>';
57
63
  const MARKER_END = '</oomi-agent-instructions>';
58
64
 
59
- const PACKAGE_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
65
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
60
66
  const UPDATE_STATE_FILE = resolveOpenclawUpdateStatePath();
61
67
  const DEFAULT_UPDATE_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
62
68
  const DEFAULT_UPDATE_CHECK_TIMEOUT_MS = 1200;
@@ -231,10 +237,12 @@ Commands:
231
237
  Replay an assistant chat.final frame through spoken-metadata normalization.
232
238
  openclaw debug tts-pipeline
233
239
  Replay an assistant chat.final through local backend voice handling.
234
- openclaw debug local-gateway-agent
235
- Run a tiny local OpenClaw gateway/agent for Docker dev testing.
236
- openclaw debug persona-runtime
237
- Scaffold, launch, and stop a managed persona runtime locally.
240
+ openclaw debug local-gateway-agent
241
+ Run a tiny local OpenClaw gateway/agent for Docker dev testing.
242
+ openclaw debug persona-runtime
243
+ Scaffold, launch, and stop a managed persona runtime locally.
244
+ openclaw refresh
245
+ Restart the bridge and running managed persona runtimes after an oomi-ai update.
238
246
 
239
247
  openclaw pair
240
248
  Pair this OpenClaw host with Oomi and start bridge (single command).
@@ -320,18 +328,20 @@ Common flags:
320
328
  --out PATH Output directory for scaffolded persona app
321
329
  --template-version V Scaffold template version (default: v1)
322
330
  --force Overwrite files in an existing output directory
323
- --force-install Reinstall persona workspace dependencies before launch
324
- --no-sync Skip backend sync (for create)
325
- --no-create Do not create a managed persona record if one does not already exist
331
+ --force-install Reinstall persona workspace dependencies before launch
332
+ --include-stopped Relaunch managed persona runtimes even if not currently marked running
333
+ --no-sync Skip backend sync (for create)
334
+ --no-create Do not create a managed persona record if one does not already exist
326
335
  --local-port N Local runtime port for persona runtime callbacks
327
336
  --endpoint URL Runtime endpoint for persona runtime callbacks
328
337
  --entry-url URL Viewer URL to register for a launched persona runtime
329
- --health-path PATH Runtime health path (default: /oomi.health.json)
338
+ --health-path PATH Runtime health path override (default: workspace-specific)
330
339
  --healthcheck-url URL Runtime healthcheck URL override
331
- --transport TEXT Runtime transport label (default: local, relay when --entry-url is used)
340
+ --transport TEXT Runtime transport label (default: local, relay when --entry-url is used)
332
341
  --workspace-root PATH Persona workspace root (default: OPENCLAW_WORKSPACE/personas)
333
- --restart Restart an existing managed persona runtime before launch
334
- --started-at ISO Start timestamp override
342
+ --restart Restart an existing managed persona runtime before launch
343
+ --skip-version-check Skip checking npm for the latest oomi-ai version during refresh
344
+ --started-at ISO Start timestamp override
335
345
  --observed-at ISO Heartbeat timestamp override
336
346
  --completed-at ISO Completion timestamp override
337
347
  --code TEXT Error code for fail callbacks
@@ -829,14 +839,91 @@ function resolvePersonaEntryUrl(flags = {}) {
829
839
  return String(flags['entry-url'] || '').trim();
830
840
  }
831
841
 
832
- function resolvePersonaLaunchTransport(flags = {}) {
833
- const explicitTransport = String(flags.transport || '').trim();
834
- if (explicitTransport) {
835
- return explicitTransport;
836
- }
837
-
838
- return resolvePersonaEntryUrl(flags) ? 'relay' : 'local';
839
- }
842
+ function resolvePersonaLaunchTransport(flags = {}) {
843
+ const explicitTransport = String(flags.transport || '').trim();
844
+ if (explicitTransport) {
845
+ return explicitTransport;
846
+ }
847
+
848
+ return resolvePersonaEntryUrl(flags) ? 'relay' : 'local';
849
+ }
850
+
851
+ function listPersonaWorkspaceRoots(workspaceRoot = defaultPersonaWorkspaceRoot()) {
852
+ const roots = [
853
+ String(workspaceRoot || '').trim(),
854
+ String(resolveOpenclawLegacyPersonasDir() || '').trim(),
855
+ ].filter(Boolean);
856
+
857
+ const deduped = [];
858
+ const seen = new Set();
859
+ for (const root of roots) {
860
+ const resolved = path.resolve(root);
861
+ if (seen.has(resolved) || !fs.existsSync(resolved)) {
862
+ continue;
863
+ }
864
+ seen.add(resolved);
865
+ deduped.push(resolved);
866
+ }
867
+
868
+ return deduped;
869
+ }
870
+
871
+ function collectManagedPersonaRefreshTargets({
872
+ workspaceRoot = defaultPersonaWorkspaceRoot(),
873
+ includeStopped = false,
874
+ } = {}) {
875
+ const targets = [];
876
+ const seenWorkspacePaths = new Set();
877
+ const seenSlugs = new Set();
878
+
879
+ for (const root of listPersonaWorkspaceRoots(workspaceRoot)) {
880
+ const entries = fs.readdirSync(root, { withFileTypes: true });
881
+ for (const entry of entries) {
882
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) {
883
+ continue;
884
+ }
885
+
886
+ const workspacePath = path.join(root, entry.name);
887
+ let dedupeKey = workspacePath;
888
+ try {
889
+ dedupeKey = fs.realpathSync(workspacePath);
890
+ } catch {
891
+ dedupeKey = path.resolve(workspacePath);
892
+ }
893
+ if (seenWorkspacePaths.has(dedupeKey)) {
894
+ continue;
895
+ }
896
+ seenWorkspacePaths.add(dedupeKey);
897
+
898
+ const state = readPersonaRuntimeState(workspacePath);
899
+ if (!state || Object.keys(state).length === 0) {
900
+ continue;
901
+ }
902
+
903
+ const slug = String(state.slug || entry.name || '').trim();
904
+ if (!slug || seenSlugs.has(slug)) {
905
+ continue;
906
+ }
907
+
908
+ const pid = normalizePid(state.pid);
909
+ const processRunning = pid ? isPersonaWorkspaceProcessRunning(pid) : false;
910
+ const runtimeStatus = String(state.status || '').trim().toLowerCase();
911
+ if (!includeStopped && !processRunning && runtimeStatus !== 'running') {
912
+ continue;
913
+ }
914
+
915
+ seenSlugs.add(slug);
916
+ targets.push({
917
+ slug,
918
+ workspacePath,
919
+ state,
920
+ processRunning,
921
+ });
922
+ }
923
+ }
924
+
925
+ return targets.sort((a, b) => a.slug.localeCompare(b.slug));
926
+ }
840
927
 
841
928
  async function findExistingManagedPersona(client, slug) {
842
929
  try {
@@ -1114,31 +1201,81 @@ async function runManagedPersonaJobExecution({
1114
1201
  }
1115
1202
  }
1116
1203
 
1117
- function resolvePersonaRuntimeInput(flags = {}, defaults = {}) {
1118
- const localPort = parseOptionalPositiveInteger(flags['local-port'] || flags.localPort || defaults.localPort);
1119
- const endpoint = String(flags.endpoint || defaults.endpoint || '').trim();
1120
- const healthPath = String(flags['health-path'] || defaults.healthPath || '/oomi.health.json').trim() || '/oomi.health.json';
1121
- const healthcheckUrl = String(flags['healthcheck-url'] || defaults.healthcheckUrl || '').trim();
1122
- const transport = String(flags.transport || defaults.transport || 'local').trim() || 'local';
1123
-
1124
- if (endpoint) {
1125
- return {
1126
- endpoint,
1127
- healthcheckUrl: healthcheckUrl || `${endpoint.replace(/\/$/, '')}${healthPath}`,
1204
+ function resolvePersonaRuntimeWorkspacePath({
1205
+ slug = '',
1206
+ workspacePath = '',
1207
+ workspaceRoot = '',
1208
+ } = {}) {
1209
+ const explicitWorkspacePath = String(workspacePath || '').trim();
1210
+ if (explicitWorkspacePath) {
1211
+ return path.resolve(explicitWorkspacePath);
1212
+ }
1213
+
1214
+ const safeSlug = String(slug || '').trim();
1215
+ if (!safeSlug) {
1216
+ return '';
1217
+ }
1218
+
1219
+ const safeWorkspaceRoot = String(workspaceRoot || defaultPersonaWorkspaceRoot()).trim();
1220
+ if (!safeWorkspaceRoot) {
1221
+ return '';
1222
+ }
1223
+
1224
+ return resolvePersonaWorkspacePath({
1225
+ workspaceRoot: safeWorkspaceRoot,
1226
+ slug: safeSlug,
1227
+ });
1228
+ }
1229
+
1230
+ function resolvePersonaRuntimeInput(flags = {}, defaults = {}, options = {}) {
1231
+ const localPort = parseOptionalPositiveInteger(flags['local-port'] || flags.localPort || defaults.localPort);
1232
+ const endpoint = String(flags.endpoint || defaults.endpoint || '').trim();
1233
+ const explicitHealthPath = String(flags['health-path'] || defaults.healthPath || '').trim();
1234
+ const healthcheckUrl = String(flags['healthcheck-url'] || defaults.healthcheckUrl || '').trim();
1235
+ const transport = String(flags.transport || defaults.transport || 'local').trim() || 'local';
1236
+ const workspacePath = resolvePersonaRuntimeWorkspacePath({
1237
+ slug: options.slug || defaults.slug || flags.slug,
1238
+ workspacePath:
1239
+ options.workspacePath ||
1240
+ defaults.workspacePath ||
1241
+ flags['workspace-path'] ||
1242
+ flags.workspacePath,
1243
+ workspaceRoot:
1244
+ options.workspaceRoot ||
1245
+ defaults.workspaceRoot ||
1246
+ flags['workspace-root'] ||
1247
+ flags.workspaceRoot,
1248
+ });
1249
+ const healthPath = explicitHealthPath ||
1250
+ resolvePersonaHealthPath({
1251
+ workspacePath,
1252
+ fallback: '/oomi.health.json',
1253
+ });
1254
+
1255
+ if (endpoint) {
1256
+ return {
1257
+ endpoint,
1258
+ healthcheckUrl: healthcheckUrl || `${endpoint.replace(/\/$/, '')}${healthPath}`,
1128
1259
  localPort,
1129
1260
  transport,
1130
1261
  };
1131
1262
  }
1132
1263
 
1133
- if (!localPort) {
1134
- throw new Error('Runtime endpoint or local port is required.');
1135
- }
1136
-
1137
- return buildLocalPersonaRuntime({
1138
- localPort,
1139
- healthPath,
1140
- });
1141
- }
1264
+ if (!localPort) {
1265
+ throw new Error('Runtime endpoint or local port is required.');
1266
+ }
1267
+
1268
+ const runtime = buildLocalPersonaRuntime({
1269
+ localPort,
1270
+ healthPath,
1271
+ });
1272
+
1273
+ return {
1274
+ ...runtime,
1275
+ endpoint: runtime.reachableEndpoint || runtime.endpoint,
1276
+ localEndpoint: runtime.endpoint,
1277
+ };
1278
+ }
1142
1279
 
1143
1280
  function parseIsoTimestamp(rawValue, label) {
1144
1281
  const value = String(rawValue || '').trim();
@@ -1175,12 +1312,19 @@ function printStructuredResult(result, asJson = false) {
1175
1312
  }
1176
1313
  }
1177
1314
 
1178
- async function handlePersonaRuntimeRegisterCommand(slug, flags = {}) {
1179
- const client = createCliPersonaApiClient(flags);
1180
- const runtime = resolvePersonaRuntimeInput(flags);
1181
- const payload = await client.registerRuntime({
1182
- slug,
1183
- endpoint: runtime.endpoint,
1315
+ async function handlePersonaRuntimeRegisterCommand(slug, flags = {}) {
1316
+ const client = createCliPersonaApiClient(flags);
1317
+ const runtime = resolvePersonaRuntimeInput(
1318
+ flags,
1319
+ {},
1320
+ {
1321
+ slug,
1322
+ workspaceRoot: resolvePersonaWorkspaceRoot(flags),
1323
+ },
1324
+ );
1325
+ const payload = await client.registerRuntime({
1326
+ slug,
1327
+ endpoint: runtime.endpoint,
1184
1328
  healthcheckUrl: runtime.healthcheckUrl,
1185
1329
  localPort: runtime.localPort,
1186
1330
  transport: runtime.transport,
@@ -1189,12 +1333,19 @@ async function handlePersonaRuntimeRegisterCommand(slug, flags = {}) {
1189
1333
  printStructuredResult(payload, isTruthyFlag(flags.json));
1190
1334
  }
1191
1335
 
1192
- async function handlePersonaHeartbeatCommand(slug, flags = {}) {
1193
- const client = createCliPersonaApiClient(flags);
1194
- const runtime = resolvePersonaRuntimeInput(flags);
1195
- const payload = await client.heartbeatRuntime({
1196
- slug,
1197
- endpoint: runtime.endpoint,
1336
+ async function handlePersonaHeartbeatCommand(slug, flags = {}) {
1337
+ const client = createCliPersonaApiClient(flags);
1338
+ const runtime = resolvePersonaRuntimeInput(
1339
+ flags,
1340
+ {},
1341
+ {
1342
+ slug,
1343
+ workspaceRoot: resolvePersonaWorkspaceRoot(flags),
1344
+ },
1345
+ );
1346
+ const payload = await client.heartbeatRuntime({
1347
+ slug,
1348
+ endpoint: runtime.endpoint,
1198
1349
  healthcheckUrl: runtime.healthcheckUrl,
1199
1350
  localPort: runtime.localPort,
1200
1351
  transport: runtime.transport,
@@ -1231,16 +1382,22 @@ async function handlePersonaJobStartCommand(jobId, flags = {}) {
1231
1382
  printStructuredResult(payload, isTruthyFlag(flags.json));
1232
1383
  }
1233
1384
 
1234
- async function handlePersonaJobSucceedCommand(jobId, flags = {}) {
1235
- const client = createCliPersonaApiClient(flags);
1236
- const runtime = resolvePersonaRuntimeInput(flags);
1237
- const workspacePath = String(flags['workspace-path'] || flags.workspacePath || '').trim();
1238
- if (!workspacePath) {
1239
- throw new Error('Workspace path is required. Use --workspace-path.');
1240
- }
1241
-
1242
- const payload = await client.succeedJob({
1243
- jobId,
1385
+ async function handlePersonaJobSucceedCommand(jobId, flags = {}) {
1386
+ const client = createCliPersonaApiClient(flags);
1387
+ const workspacePath = String(flags['workspace-path'] || flags.workspacePath || '').trim();
1388
+ if (!workspacePath) {
1389
+ throw new Error('Workspace path is required. Use --workspace-path.');
1390
+ }
1391
+ const runtime = resolvePersonaRuntimeInput(
1392
+ flags,
1393
+ {},
1394
+ {
1395
+ workspacePath,
1396
+ },
1397
+ );
1398
+
1399
+ const payload = await client.succeedJob({
1400
+ jobId,
1244
1401
  workspacePath,
1245
1402
  localPort: runtime.localPort,
1246
1403
  transport: runtime.transport,
@@ -1408,6 +1565,162 @@ async function handlePersonaDeleteCommand(slug, flags = {}) {
1408
1565
  });
1409
1566
  printStructuredResult(result, isTruthyFlag(flags.json));
1410
1567
  }
1568
+
1569
+ async function restartManagedPersonaRefreshTargets(flags = {}) {
1570
+ const workspaceRoot = resolvePersonaWorkspaceRoot(flags);
1571
+ const targets = collectManagedPersonaRefreshTargets({
1572
+ workspaceRoot,
1573
+ includeStopped: isTruthyFlag(flags['include-stopped']),
1574
+ });
1575
+ const results = [];
1576
+
1577
+ let client = null;
1578
+ let registrationError = '';
1579
+ try {
1580
+ client = createCliPersonaApiClient(flags);
1581
+ } catch (error) {
1582
+ registrationError = error instanceof Error ? error.message : String(error);
1583
+ }
1584
+
1585
+ for (const target of targets) {
1586
+ const state = target.state && typeof target.state === 'object' ? target.state : {};
1587
+ const launchResult = await launchManagedPersonaRuntime({
1588
+ slug: target.slug,
1589
+ name: String(state.name || target.slug).trim() || target.slug,
1590
+ description: String(state.description || state.name || target.slug).trim() || target.slug,
1591
+ workspaceRoot,
1592
+ templateVersion: String(state.templateVersion || 'v1').trim() || 'v1',
1593
+ forceInstall: isTruthyFlag(flags['force-install']),
1594
+ restart: true,
1595
+ logFilePath: String(state.logFilePath || '').trim(),
1596
+ entryUrl: '',
1597
+ transport: 'local',
1598
+ });
1599
+
1600
+ let registration = null;
1601
+ if (client) {
1602
+ try {
1603
+ registration = await client.registerRuntime({
1604
+ slug: launchResult.slug,
1605
+ endpoint: launchResult.runtime.endpoint,
1606
+ healthcheckUrl: launchResult.runtime.healthcheckUrl,
1607
+ localPort: launchResult.runtime.localPort,
1608
+ transport: launchResult.runtime.transport,
1609
+ startedAt: new Date().toISOString(),
1610
+ });
1611
+ } catch (error) {
1612
+ registration = {
1613
+ ok: false,
1614
+ error: error instanceof Error ? error.message : String(error),
1615
+ };
1616
+ }
1617
+ }
1618
+
1619
+ results.push({
1620
+ slug: launchResult.slug,
1621
+ workspacePath: launchResult.workspacePath,
1622
+ localPort: launchResult.runtime.localPort,
1623
+ endpoint: launchResult.runtime.endpoint,
1624
+ healthcheckUrl: launchResult.runtime.healthcheckUrl,
1625
+ registration,
1626
+ });
1627
+ }
1628
+
1629
+ return {
1630
+ workspaceRoot,
1631
+ targets,
1632
+ results,
1633
+ registrationError,
1634
+ };
1635
+ }
1636
+
1637
+ async function refreshBridgeForUpdate(flags = {}) {
1638
+ if (process.platform === 'darwin') {
1639
+ const launchdStatus = readBridgeLaunchdStatus();
1640
+ if (launchdStatus.installed) {
1641
+ await stopBridgeLaunchdService();
1642
+ startBridgeLaunchdService();
1643
+ incrementBridgeMetric('bridge_restart_count');
1644
+ return {
1645
+ restarted: true,
1646
+ mode: 'service',
1647
+ target: launchdStatus.target,
1648
+ };
1649
+ }
1650
+ }
1651
+
1652
+ const running = findRunningBridgeProcess();
1653
+ if (!running) {
1654
+ return {
1655
+ restarted: false,
1656
+ mode: 'process',
1657
+ };
1658
+ }
1659
+
1660
+ const stopResult = await stopBridgeProcesses();
1661
+ if (Array.isArray(stopResult.stillAlive) && stopResult.stillAlive.length > 0) {
1662
+ throw new Error(`Failed to stop bridge processes: ${stopResult.stillAlive.join(', ')}`);
1663
+ }
1664
+
1665
+ const detachedResult = startBridgeDetachedProcess(flags);
1666
+ incrementBridgeMetric('bridge_restart_count');
1667
+ return {
1668
+ restarted: true,
1669
+ mode: 'process',
1670
+ pid: detachedResult.pid,
1671
+ alreadyRunning: Boolean(detachedResult.alreadyRunning),
1672
+ };
1673
+ }
1674
+
1675
+ async function handleOpenclawRefreshCommand(flags = {}) {
1676
+ const currentVersion = currentPackageVersion();
1677
+ let latestVersion = '';
1678
+ if (!isTruthyFlag(flags['skip-version-check'])) {
1679
+ latestVersion = await fetchLatestPublishedVersion('oomi-ai');
1680
+ if (latestVersion && compareVersions(currentVersion, latestVersion) < 0) {
1681
+ throw new Error(
1682
+ `Installed oomi-ai ${currentVersion} is behind npm ${latestVersion}. Update first, then rerun: oomi openclaw refresh`
1683
+ );
1684
+ }
1685
+ }
1686
+
1687
+ const personaRefresh = await restartManagedPersonaRefreshTargets(flags);
1688
+ const bridgeRefresh = await refreshBridgeForUpdate(flags);
1689
+ const payload = {
1690
+ ok: true,
1691
+ currentVersion,
1692
+ latestVersion: latestVersion || currentVersion,
1693
+ personas: {
1694
+ discovered: personaRefresh.targets.length,
1695
+ restarted: personaRefresh.results.length,
1696
+ registrationError: personaRefresh.registrationError || null,
1697
+ results: personaRefresh.results,
1698
+ },
1699
+ bridge: bridgeRefresh,
1700
+ };
1701
+
1702
+ if (isTruthyFlag(flags.json)) {
1703
+ console.log(JSON.stringify(payload, null, 2));
1704
+ return;
1705
+ }
1706
+
1707
+ console.log(`Oomi refresh complete (${payload.currentVersion})`);
1708
+ console.log(`Personas restarted: ${payload.personas.restarted}/${payload.personas.discovered}`);
1709
+ if (payload.personas.registrationError) {
1710
+ console.log(`Backend registration skipped: ${payload.personas.registrationError}`);
1711
+ }
1712
+ payload.personas.results.forEach((result) => {
1713
+ console.log(`- ${result.slug}: ${result.endpoint}`);
1714
+ if (result.registration && result.registration.ok === false) {
1715
+ console.log(` registration: ${result.registration.error}`);
1716
+ }
1717
+ });
1718
+ if (payload.bridge.restarted) {
1719
+ console.log(`Bridge restarted (${payload.bridge.mode}).`);
1720
+ } else {
1721
+ console.log('Bridge not running; no restart performed.');
1722
+ }
1723
+ }
1411
1724
 
1412
1725
  function resolveOpenclawConfigPath() {
1413
1726
  const candidates = resolveOpenclawConfigCandidates();
@@ -5339,15 +5652,20 @@ async function main() {
5339
5652
  return;
5340
5653
  }
5341
5654
 
5342
- if (command === 'openclaw' && subcommand === 'debug') {
5343
- await handleOpenclawDebugCommand(args.positionals[0], args.flags);
5344
- return;
5345
- }
5346
-
5347
- if (command === 'personas' && subcommand === 'sync') {
5348
- await syncPersonas({ backendUrl: args.flags['backend-url'], root: args.flags.root });
5349
- return;
5350
- }
5655
+ if (command === 'openclaw' && subcommand === 'debug') {
5656
+ await handleOpenclawDebugCommand(args.positionals[0], args.flags);
5657
+ return;
5658
+ }
5659
+
5660
+ if (command === 'openclaw' && subcommand === 'refresh') {
5661
+ await handleOpenclawRefreshCommand(args.flags);
5662
+ return;
5663
+ }
5664
+
5665
+ if (command === 'personas' && subcommand === 'sync') {
5666
+ await syncPersonas({ backendUrl: args.flags['backend-url'], root: args.flags.root });
5667
+ return;
5668
+ }
5351
5669
 
5352
5670
  if (command === 'personas' && subcommand === 'create') {
5353
5671
  const id = args.positionals[0];
@@ -5504,8 +5822,10 @@ export {
5504
5822
  runBridgeCallbackSafely,
5505
5823
  extractGatewayRequestMeta,
5506
5824
  extractGatewayResponseMeta,
5507
- isServiceManagedBridgeStart,
5508
- isGatewayRunStartedFrame,
5509
- isBridgeWorkerCommand,
5510
- parsePositiveInteger,
5511
- };
5825
+ isServiceManagedBridgeStart,
5826
+ isGatewayRunStartedFrame,
5827
+ isBridgeWorkerCommand,
5828
+ parsePositiveInteger,
5829
+ collectManagedPersonaRefreshTargets,
5830
+ resolvePersonaRuntimeInput,
5831
+ };
@@ -8,6 +8,7 @@ import {
8
8
  defaultPersonaWorkspaceRoot,
9
9
  installPersonaWorkspace,
10
10
  isPersonaWorkspaceProcessRunning,
11
+ resolvePersonaHealthPath,
11
12
  resolvePersonaDevCommand,
12
13
  syncLegacyWebSpatialScaffoldFiles,
13
14
  syncVendoredWebSpatialPackages,
@@ -143,20 +144,12 @@ export function slugifyPersonaName(name) {
143
144
  return normalized;
144
145
  }
145
146
 
146
- function resolveHealthPath(workspacePath) {
147
- const runtimeConfigPath = path.join(workspacePath, 'oomi.runtime.json');
148
- if (!fs.existsSync(runtimeConfigPath)) {
149
- return '/oomi.health.json';
150
- }
151
-
152
- try {
153
- const parsed = JSON.parse(fs.readFileSync(runtimeConfigPath, 'utf8'));
154
- const healthPath = trimString(parsed?.healthPath);
155
- return healthPath || '/oomi.health.json';
156
- } catch {
157
- return '/oomi.health.json';
158
- }
159
- }
147
+ function resolveHealthPath(workspacePath) {
148
+ return resolvePersonaHealthPath({
149
+ workspacePath,
150
+ fallback: '/oomi.health.json',
151
+ });
152
+ }
160
153
 
161
154
  function workspaceNeedsScaffold(workspacePath) {
162
155
  return !fs.existsSync(path.join(workspacePath, 'package.json'));
@@ -270,12 +263,12 @@ export async function launchManagedPersonaRuntime({
270
263
  workspacePath,
271
264
  templateVersion,
272
265
  });
273
- const installed = await ensureWorkspaceInstall({
274
- workspacePath,
275
- forceInstall,
276
- });
277
-
278
- const healthPath = scaffoldInfo.healthPath || resolveHealthPath(workspacePath);
266
+ const installed = await ensureWorkspaceInstall({
267
+ workspacePath,
268
+ forceInstall,
269
+ });
270
+
271
+ const healthPath = resolveHealthPath(workspacePath);
279
272
  const preferredPort = previousState.localPort || scaffoldInfo.defaultPort;
280
273
  const expectedDevCommand = resolvePersonaDevCommand({
281
274
  workspacePath,
@@ -30,12 +30,25 @@ const WEBSPATIAL_VENDOR_ROOT = path.join(
30
30
  );
31
31
  const VENDORED_WEBSPATIAL_CORE_SPEC = 'file:./vendor/webspatial/core-sdk';
32
32
  const VENDORED_WEBSPATIAL_REACT_SPEC = 'file:./vendor/webspatial/react-sdk';
33
+ const WEBSPATIAL_RUNTIME_BASE_PATH = '/webspatial/avp';
33
34
  const WEBSPATIAL_TEMPLATE_DEV_DEPENDENCIES = [
34
35
  '@webspatial/builder',
35
36
  '@webspatial/platform-visionos',
36
37
  '@webspatial/vite-plugin',
37
38
  ];
38
39
  const LEGACY_WEBSPATIAL_TEMPLATE_FILE_RULES = [
40
+ {
41
+ relativePath: 'oomi.runtime.json',
42
+ shouldReplace: (content) =>
43
+ content.includes('"renderMode": "webspatial"') &&
44
+ content.includes('"healthPath": "/oomi.health.json"'),
45
+ },
46
+ {
47
+ relativePath: 'package.json',
48
+ shouldReplace: (content) =>
49
+ content.includes('vite --host 127.0.0.1 --port 4789') ||
50
+ content.includes('vite preview --host 127.0.0.1 --port 4789'),
51
+ },
39
52
  {
40
53
  relativePath: path.join('src', 'spatial.ts'),
41
54
  shouldReplace: (content) =>
@@ -295,6 +308,21 @@ function trimString(value) {
295
308
  return typeof value === 'string' ? value.trim() : '';
296
309
  }
297
310
 
311
+ function normalizePersonaHealthPath(healthPath, runtimeConfig = {}) {
312
+ const safeHealthPath = trimString(healthPath) || '/oomi.health.json';
313
+ const renderMode = trimString(runtimeConfig?.renderMode).toLowerCase();
314
+ if (renderMode !== 'webspatial') {
315
+ return safeHealthPath;
316
+ }
317
+
318
+ if (safeHealthPath.startsWith(`${WEBSPATIAL_RUNTIME_BASE_PATH}/`)) {
319
+ return safeHealthPath;
320
+ }
321
+
322
+ const normalizedSuffix = safeHealthPath.startsWith('/') ? safeHealthPath : `/${safeHealthPath}`;
323
+ return `${WEBSPATIAL_RUNTIME_BASE_PATH}${normalizedSuffix}`;
324
+ }
325
+
298
326
  function readPersonaConfigLiteral(source, key) {
299
327
  if (!source) {
300
328
  return '';
@@ -664,6 +692,17 @@ export function resolvePersonaDevEnvironment({
664
692
  return {};
665
693
  }
666
694
 
695
+ export function resolvePersonaHealthPath({
696
+ workspacePath,
697
+ fallback = '/oomi.health.json',
698
+ } = {}) {
699
+ const runtimeConfig = readPersonaRuntimeConfig(workspacePath);
700
+ return normalizePersonaHealthPath(
701
+ trimString(runtimeConfig?.healthPath) || fallback,
702
+ runtimeConfig,
703
+ );
704
+ }
705
+
667
706
  export function startPersonaWorkspace({
668
707
  workspacePath,
669
708
  logFilePath,
@@ -855,6 +894,7 @@ export function buildLocalPersonaRuntime({
855
894
  const reachableHost = resolvePersonaReachableHost({ bindHost });
856
895
  const endpoint = `http://127.0.0.1:${port}`;
857
896
  const reachableEndpoint = `http://${formatPersonaRuntimeHostForUrl(reachableHost)}:${port}`;
897
+ const normalizedHealthPath = healthPath || '/oomi.health.json';
858
898
  return {
859
899
  transport: 'local',
860
900
  endpoint,
@@ -862,7 +902,7 @@ export function buildLocalPersonaRuntime({
862
902
  bindHost,
863
903
  reachableHost,
864
904
  localPort: port,
865
- healthcheckUrl: `${endpoint}${healthPath}`,
905
+ healthcheckUrl: `${endpoint}${normalizedHealthPath}`,
866
906
  };
867
907
  }
868
908
 
@@ -5,7 +5,12 @@ import { resolveOpenclawLegacyPersonasDir } from './openclawPaths.js';
5
5
  import { createPersonaApiClient } from './personaApiClient.js';
6
6
  import { launchManagedPersonaRuntime } from './personaRuntimeManager.js';
7
7
  import { readPersonaRuntimeState, updatePersonaRuntimeState } from './personaRuntimeRegistry.js';
8
- import { buildLocalPersonaRuntime, isPersonaWorkspaceProcessRunning, resolvePersonaDevCommand } from './personaRuntimeProcess.js';
8
+ import {
9
+ buildLocalPersonaRuntime,
10
+ isPersonaWorkspaceProcessRunning,
11
+ resolvePersonaDevCommand,
12
+ resolvePersonaHealthPath,
13
+ } from './personaRuntimeProcess.js';
9
14
 
10
15
  function trimString(value) {
11
16
  return typeof value === 'string' ? value.trim() : '';
@@ -17,20 +22,6 @@ function wait(ms) {
17
22
  });
18
23
  }
19
24
 
20
- function resolveHealthPath(healthcheckUrl) {
21
- const safeUrl = trimString(healthcheckUrl);
22
- if (!safeUrl) {
23
- return '/oomi.health.json';
24
- }
25
-
26
- try {
27
- const parsed = new URL(safeUrl);
28
- return `${parsed.pathname || '/oomi.health.json'}${parsed.search || ''}`;
29
- } catch {
30
- return '/oomi.health.json';
31
- }
32
- }
33
-
34
25
  function listWorkspacePaths(workspaceRoot) {
35
26
  const roots = [trimString(workspaceRoot), trimString(resolveOpenclawLegacyPersonasDir())]
36
27
  .filter(Boolean)
@@ -100,7 +91,10 @@ async function reconcileWorkspace({
100
91
  const localRuntime = runtime.localPort
101
92
  ? buildLocalPersonaRuntime({
102
93
  localPort: runtime.localPort,
103
- healthPath: resolveHealthPath(runtime.healthcheckUrl),
94
+ healthPath: resolvePersonaHealthPath({
95
+ workspacePath,
96
+ fallback: '/oomi.health.json',
97
+ }),
104
98
  })
105
99
  : null;
106
100
 
@@ -166,29 +160,86 @@ async function reconcileWorkspace({
166
160
 
167
161
  const healthy = await healthcheckOk(effectiveRuntime.healthcheckUrl);
168
162
  if (!healthy) {
169
- return;
163
+ if (!autoRestart) {
164
+ return;
165
+ }
166
+
167
+ try {
168
+ const recovered = await launchManagedPersonaRuntime({
169
+ slug,
170
+ name: trimString(state.name) || slug,
171
+ description: trimString(state.description) || trimString(state.name) || slug,
172
+ workspaceRoot,
173
+ templateVersion: trimString(state.templateVersion) || 'v1',
174
+ forceInstall: false,
175
+ restart: true,
176
+ logFilePath: trimString(state.logFilePath),
177
+ entryUrl: '',
178
+ transport: 'local',
179
+ });
180
+
181
+ effectiveRuntime = {
182
+ slug,
183
+ endpoint: recovered.runtime.endpoint,
184
+ localEndpoint: recovered.runtime.localEndpoint || recovered.localRuntime.endpoint,
185
+ reachableEndpoint: recovered.runtime.reachableEndpoint || recovered.localRuntime.reachableEndpoint,
186
+ healthcheckUrl: recovered.runtime.healthcheckUrl,
187
+ transport: recovered.runtime.transport,
188
+ localPort: recovered.runtime.localPort,
189
+ };
190
+
191
+ await client.registerRuntime({
192
+ slug,
193
+ endpoint: effectiveRuntime.endpoint,
194
+ healthcheckUrl: effectiveRuntime.healthcheckUrl,
195
+ localPort: effectiveRuntime.localPort,
196
+ transport: effectiveRuntime.transport,
197
+ startedAt: new Date().toISOString(),
198
+ });
199
+ } catch (error) {
200
+ logger.warn?.(
201
+ `[persona-runtime] unhealthy runtime restart failed for ${slug}: ${
202
+ error instanceof Error ? error.message : String(error)
203
+ }`
204
+ );
205
+ return;
206
+ }
207
+ }
208
+
209
+ if (!healthy) {
210
+ const recoveredHealthy = await healthcheckOk(effectiveRuntime.healthcheckUrl);
211
+ if (!recoveredHealthy) {
212
+ return;
213
+ }
170
214
  }
171
215
 
172
216
  if (localRuntime) {
173
- const desiredEndpoint = localRuntime.reachableEndpoint || runtime.endpoint;
217
+ const refreshedLocalRuntime = buildLocalPersonaRuntime({
218
+ localPort: effectiveRuntime.localPort,
219
+ healthPath: resolvePersonaHealthPath({
220
+ workspacePath,
221
+ fallback: '/oomi.health.json',
222
+ }),
223
+ });
224
+ const desiredEndpoint = refreshedLocalRuntime.reachableEndpoint || runtime.endpoint;
174
225
  const endpointChanged = desiredEndpoint && desiredEndpoint !== effectiveRuntime.endpoint;
175
- const localEndpointChanged = localRuntime.endpoint !== effectiveRuntime.localEndpoint;
176
- const reachableEndpointChanged = localRuntime.reachableEndpoint !== effectiveRuntime.reachableEndpoint;
226
+ const localEndpointChanged = refreshedLocalRuntime.endpoint !== effectiveRuntime.localEndpoint;
227
+ const reachableEndpointChanged = refreshedLocalRuntime.reachableEndpoint !== effectiveRuntime.reachableEndpoint;
177
228
 
178
229
  if (endpointChanged || localEndpointChanged || reachableEndpointChanged) {
179
230
  effectiveRuntime = {
180
231
  ...effectiveRuntime,
181
232
  endpoint: desiredEndpoint,
182
- localEndpoint: localRuntime.endpoint,
183
- reachableEndpoint: localRuntime.reachableEndpoint,
233
+ localEndpoint: refreshedLocalRuntime.endpoint,
234
+ reachableEndpoint: refreshedLocalRuntime.reachableEndpoint,
184
235
  };
185
236
  updatePersonaRuntimeState(workspacePath, {
186
237
  endpoint: desiredEndpoint,
187
238
  entryUrl: desiredEndpoint,
188
- localEndpoint: localRuntime.endpoint,
189
- reachableEndpoint: localRuntime.reachableEndpoint,
190
- bindHost: localRuntime.bindHost,
191
- reachableHost: localRuntime.reachableHost,
239
+ localEndpoint: refreshedLocalRuntime.endpoint,
240
+ reachableEndpoint: refreshedLocalRuntime.reachableEndpoint,
241
+ bindHost: refreshedLocalRuntime.bindHost,
242
+ reachableHost: refreshedLocalRuntime.reachableHost,
192
243
  });
193
244
 
194
245
  try {
@@ -2,7 +2,7 @@
2
2
  "id": "oomi-ai",
3
3
  "name": "Oomi Channel Plugin",
4
4
  "description": "Managed Oomi channel integration for OpenClaw.",
5
- "version": "0.2.42",
5
+ "version": "0.2.45",
6
6
  "author": "Oomi",
7
7
  "license": "MIT",
8
8
  "openclawVersion": ">=0.5.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oomi-ai",
3
- "version": "0.2.42",
3
+ "version": "0.2.45",
4
4
  "description": "Oomi OpenClaw channel plugin and bridge tooling",
5
5
  "bin": {
6
6
  "oomi": "bin/oomi-ai.js"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "templateVersion": "__OOMI_TEMPLATE_VERSION__",
3
3
  "appKind": "oomi-persona-app",
4
- "healthPath": "/oomi.health.json",
4
+ "healthPath": "/webspatial/avp/oomi.health.json",
5
5
  "defaultPort": 4789,
6
6
  "supportsRuntimeRegistration": true,
7
7
  "renderMode": "webspatial",
@@ -5,10 +5,10 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "install:clean": "npm install",
8
- "dev": "vite --host 127.0.0.1 --port 4789",
9
- "dev:avp": "cross-env XR_ENV=avp vite --host 127.0.0.1 --port 4789",
8
+ "dev": "vite --host 0.0.0.0 --port 4789 --strictPort",
9
+ "dev:avp": "cross-env XR_ENV=avp vite --host 0.0.0.0 --port 4789 --strictPort",
10
10
  "build": "vite build && cross-env XR_ENV=avp vite build",
11
- "preview": "vite preview --host 127.0.0.1 --port 4789",
11
+ "preview": "vite preview --host 0.0.0.0 --port 4789 --strictPort",
12
12
  "lint": "eslint ."
13
13
  },
14
14
  "dependencies": {
@@ -4,7 +4,7 @@
4
4
  "displayName": "Oomi Persona App",
5
5
  "appKind": "oomi-persona-app",
6
6
  "defaultPort": 4789,
7
- "healthPath": "/oomi.health.json",
7
+ "healthPath": "/webspatial/avp/oomi.health.json",
8
8
  "startCommand": "npm run dev:avp",
9
9
  "editableZones": [
10
10
  "src/persona",