oomi-ai 0.2.21 → 0.2.24

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
@@ -1673,6 +1673,31 @@ function extractTextFromGatewayMessage(message) {
1673
1673
  .join(' ');
1674
1674
  }
1675
1675
 
1676
+ function summarizeVoiceFrameContract(frameText) {
1677
+ const frame = parseJsonPayload(frameText);
1678
+ if (!frame || typeof frame !== 'object') {
1679
+ return { parseable: false };
1680
+ }
1681
+ const payload = frame.payload && typeof frame.payload === 'object' ? frame.payload : {};
1682
+ const message = payload.message && typeof payload.message === 'object' ? payload.message : {};
1683
+ const metadata = message.metadata && typeof message.metadata === 'object' ? message.metadata : {};
1684
+ const spokenRaw = Object.prototype.hasOwnProperty.call(metadata, 'spoken') ? metadata.spoken : undefined;
1685
+ const spokenNormalized = normalizeSpokenMetadata(spokenRaw);
1686
+ const text = extractTextFromGatewayMessage(message);
1687
+ return {
1688
+ parseable: true,
1689
+ event: typeof frame.event === 'string' ? frame.event : '',
1690
+ state: typeof payload.state === 'string' ? payload.state : '',
1691
+ role: typeof message.role === 'string' ? message.role : '',
1692
+ contentLength: text.length,
1693
+ hasMetadata: Object.keys(metadata).length > 0,
1694
+ hasSpokenKey: Object.prototype.hasOwnProperty.call(metadata, 'spoken'),
1695
+ spokenRawType: spokenRaw === undefined ? 'missing' : Array.isArray(spokenRaw) ? 'array' : typeof spokenRaw,
1696
+ spokenNormalized: Boolean(spokenNormalized),
1697
+ spokenSegmentCount: Array.isArray(spokenNormalized?.segments) ? spokenNormalized.segments.length : 0,
1698
+ };
1699
+ }
1700
+
1676
1701
  function ensureVoiceAssistantSpokenMetadata(frameText) {
1677
1702
  const frame = parseJsonPayload(frameText);
1678
1703
  if (!frame || typeof frame !== 'object') {
@@ -1702,10 +1727,10 @@ function ensureVoiceAssistantSpokenMetadata(frameText) {
1702
1727
  ? message.metadata
1703
1728
  : {};
1704
1729
  const metadata = { ...originalMetadata };
1705
- const explicitSpokenPresent = Object.prototype.hasOwnProperty.call(originalMetadata, 'spoken');
1730
+ const normalizedExplicitSpoken = normalizeSpokenMetadata(originalMetadata.spoken);
1706
1731
  const spoken =
1707
- normalizeSpokenMetadata(originalMetadata.spoken) ||
1708
- (!explicitSpokenPresent ? inferSpokenMetadataFromContent(extractTextFromGatewayMessage(message)) : null);
1732
+ normalizedExplicitSpoken ||
1733
+ inferSpokenMetadataFromContent(extractTextFromGatewayMessage(message));
1709
1734
  if (!spoken) {
1710
1735
  return { frameText, changed: false, reason: '' };
1711
1736
  }
@@ -1725,7 +1750,7 @@ function ensureVoiceAssistantSpokenMetadata(frameText) {
1725
1750
  return {
1726
1751
  frameText: nextFrame,
1727
1752
  changed: nextFrame !== frameText,
1728
- reason: explicitSpokenPresent ? 'normalized' : (messageRole ? 'synthesized' : 'synthesized_missing_role'),
1753
+ reason: normalizedExplicitSpoken ? 'normalized' : (messageRole ? 'synthesized' : 'synthesized_missing_role'),
1729
1754
  };
1730
1755
  }
1731
1756
 
@@ -1906,11 +1931,11 @@ async function runBridgePreflight({ brokerWs, gatewayUrl, gatewayConfigPath }) {
1906
1931
  await assertTcpReachable(parsedGatewayUrl.toString());
1907
1932
  }
1908
1933
 
1909
- function buildBridgeDetachArgs(rawFlags = {}) {
1910
- const orderedKeys = [
1911
- 'broker-http',
1912
- 'broker-ws',
1913
- 'pair-code',
1934
+ function buildBridgeDetachArgs(rawFlags = {}) {
1935
+ const orderedKeys = [
1936
+ 'broker-http',
1937
+ 'broker-ws',
1938
+ 'pair-code',
1914
1939
  'app-url',
1915
1940
  'device-id',
1916
1941
  'device-token',
@@ -1928,9 +1953,13 @@ function buildBridgeDetachArgs(rawFlags = {}) {
1928
1953
  if (!text) continue;
1929
1954
  args.push(`--${key}`, text);
1930
1955
  }
1931
-
1932
- return args;
1933
- }
1956
+
1957
+ return args;
1958
+ }
1959
+
1960
+ function isServiceManagedBridgeStart(flags = {}) {
1961
+ return isTruthyFlag(flags['service-managed']);
1962
+ }
1934
1963
 
1935
1964
  function startBridgeDetachedProcess(rawFlags = {}) {
1936
1965
  const existing = findRunningBridgeProcess();
@@ -2101,17 +2130,17 @@ function runLaunchctl(args, { allowFailure = false } = {}) {
2101
2130
  return { status, stdout, stderr };
2102
2131
  }
2103
2132
 
2104
- function buildBridgeLaunchAgentPlist() {
2105
- const scriptPath = (() => {
2106
- try {
2107
- return fs.realpathSync(process.argv[1]);
2108
- } catch {
2109
- return process.argv[1];
2110
- }
2111
- })();
2112
- const programArgs = [process.execPath, scriptPath, 'openclaw', 'bridge', 'start'];
2113
- const bridgeLogPath = resolveBridgeLiveLogPath();
2114
- const argsXml = programArgs.map((arg) => `<string>${xmlEscape(arg)}</string>`).join('\n ');
2133
+ function buildBridgeLaunchAgentPlist() {
2134
+ const scriptPath = (() => {
2135
+ try {
2136
+ return fs.realpathSync(process.argv[1]);
2137
+ } catch {
2138
+ return process.argv[1];
2139
+ }
2140
+ })();
2141
+ const programArgs = [process.execPath, scriptPath, 'openclaw', 'bridge', 'start', '--service-managed'];
2142
+ const bridgeLogPath = resolveBridgeLiveLogPath();
2143
+ const argsXml = programArgs.map((arg) => `<string>${xmlEscape(arg)}</string>`).join('\n ');
2115
2144
 
2116
2145
  return `<?xml version="1.0" encoding="UTF-8"?>
2117
2146
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -2167,17 +2196,18 @@ function readBridgeLaunchdStatus() {
2167
2196
  };
2168
2197
  }
2169
2198
 
2170
- function startBridgeLaunchdService() {
2171
- assertMacOSLaunchdAvailable();
2172
- const plistPath = resolveBridgeLaunchAgentPlistPath();
2173
- if (!fs.existsSync(plistPath)) {
2174
- throw new Error('Bridge service is not installed. Run: oomi openclaw bridge service install');
2175
- }
2176
- const domain = launchctlDomain();
2177
- const target = launchctlServiceTarget();
2178
- runLaunchctl(['bootout', domain, plistPath], { allowFailure: true });
2179
- runLaunchctl(['bootstrap', domain, plistPath]);
2180
- runLaunchctl(['enable', target], { allowFailure: true });
2199
+ function startBridgeLaunchdService() {
2200
+ assertMacOSLaunchdAvailable();
2201
+ const plistPath = resolveBridgeLaunchAgentPlistPath();
2202
+ if (!fs.existsSync(plistPath)) {
2203
+ throw new Error('Bridge service is not installed. Run: oomi openclaw bridge service install');
2204
+ }
2205
+ writeFile(plistPath, buildBridgeLaunchAgentPlist());
2206
+ const domain = launchctlDomain();
2207
+ const target = launchctlServiceTarget();
2208
+ runLaunchctl(['bootout', domain, plistPath], { allowFailure: true });
2209
+ runLaunchctl(['bootstrap', domain, plistPath]);
2210
+ runLaunchctl(['enable', target], { allowFailure: true });
2181
2211
  runLaunchctl(['kickstart', '-k', target], { allowFailure: true });
2182
2212
  }
2183
2213
 
@@ -2958,10 +2988,16 @@ async function startOpenclawBridge(flags) {
2958
2988
  gatewaySocket.on('message', runBridgeCallbackSafely((gatewayRaw) => {
2959
2989
  let frame = typeof gatewayRaw === 'string' ? gatewayRaw : gatewayRaw.toString();
2960
2990
  if (classifyBridgeSessionScope(sessionId) === 'voice') {
2991
+ const beforeSummary = summarizeVoiceFrameContract(frame);
2961
2992
  const spokenNormalized = ensureVoiceAssistantSpokenMetadata(frame);
2962
2993
  if (spokenNormalized.changed) {
2963
2994
  frame = spokenNormalized.frameText;
2964
- console.log(`[bridge] voice.spoken_metadata.${spokenNormalized.reason} ${sessionId}`);
2995
+ console.log(`[bridge] voice.spoken_metadata.${spokenNormalized.reason} ${sessionId} ${JSON.stringify({
2996
+ before: beforeSummary,
2997
+ after: summarizeVoiceFrameContract(frame),
2998
+ })}`);
2999
+ } else if (beforeSummary.event === 'chat' && beforeSummary.state === 'final') {
3000
+ console.log(`[bridge] voice.chat.final ${sessionId} ${JSON.stringify(beforeSummary)}`);
2965
3001
  }
2966
3002
  }
2967
3003
  const gatewayPayload = parseJsonPayload(frame);
@@ -3318,13 +3354,17 @@ async function startOpenclawBridge(flags) {
3318
3354
  return;
3319
3355
  }
3320
3356
 
3321
- if (payload.type === 'client.frame') {
3322
- const sessionId = String(payload.sessionId || '').trim();
3323
- const frame = typeof payload.frame === 'string' ? payload.frame : '';
3324
- if (!sessionId || !frame) return;
3325
- console.log(`[bridge] client.frame ${sessionId}`);
3326
- const sessionBridge = getOrCreateGatewaySession(sessionId);
3327
- if (!sessionBridge) return;
3357
+ if (payload.type === 'client.frame') {
3358
+ const sessionId = String(payload.sessionId || '').trim();
3359
+ const frame = typeof payload.frame === 'string' ? payload.frame : '';
3360
+ if (!sessionId || !frame) return;
3361
+ if (classifyBridgeSessionScope(sessionId) === 'voice') {
3362
+ console.log(`[bridge] client.frame ${sessionId} ${JSON.stringify(summarizeVoiceFrameContract(frame))}`);
3363
+ } else {
3364
+ console.log(`[bridge] client.frame ${sessionId}`);
3365
+ }
3366
+ const sessionBridge = getOrCreateGatewaySession(sessionId);
3367
+ if (!sessionBridge) return;
3328
3368
  const requestMeta = extractGatewayRequestMeta(frame);
3329
3369
  if (requestMeta) {
3330
3370
  if (!(sessionBridge.pendingRequests instanceof Map)) {
@@ -3941,12 +3981,17 @@ async function handleBridgeServiceCommand(actionRaw = '', flags = {}) {
3941
3981
  );
3942
3982
  }
3943
3983
 
3944
- async function startBridgeLifecycle(flags = {}) {
3945
- if (Boolean(flags.detach)) {
3946
- const detachedFlags = { ...flags };
3947
- delete detachedFlags.detach;
3948
- const result = startBridgeDetachedProcess(detachedFlags);
3949
- if (result.alreadyRunning) {
3984
+ async function startBridgeLifecycle(flags = {}) {
3985
+ const serviceManaged = isServiceManagedBridgeStart(flags);
3986
+ if (serviceManaged && Boolean(flags.detach)) {
3987
+ throw new Error('Detached bridge mode cannot be combined with --service-managed.');
3988
+ }
3989
+
3990
+ if (Boolean(flags.detach)) {
3991
+ const detachedFlags = { ...flags };
3992
+ delete detachedFlags.detach;
3993
+ const result = startBridgeDetachedProcess(detachedFlags);
3994
+ if (result.alreadyRunning) {
3950
3995
  incrementBridgeMetric('duplicate_start_attempt_count');
3951
3996
  console.log(`Bridge already running (pid: ${result.pid}).`);
3952
3997
  return;
@@ -3955,19 +4000,30 @@ async function startBridgeLifecycle(flags = {}) {
3955
4000
  console.log(`Bridge started in background (pid: ${result.pid}).`);
3956
4001
  return;
3957
4002
  }
3958
-
3959
- const running = findRunningBridgeProcess();
3960
- if (running) {
3961
- incrementBridgeMetric('duplicate_start_attempt_count');
3962
- console.log(
3963
- `Bridge already running (pid ${running.pid})${running.deviceId ? ` for device ${running.deviceId}` : ''}.`
3964
- );
3965
- return;
3966
- }
3967
-
3968
- incrementBridgeMetric('bridge_start_count');
3969
- await startOpenclawBridge(flags);
3970
- }
4003
+
4004
+ const running = findRunningBridgeProcess();
4005
+ if (running) {
4006
+ if (!serviceManaged) {
4007
+ incrementBridgeMetric('duplicate_start_attempt_count');
4008
+ console.log(
4009
+ `Bridge already running (pid ${running.pid})${running.deviceId ? ` for device ${running.deviceId}` : ''}.`
4010
+ );
4011
+ return;
4012
+ }
4013
+
4014
+ incrementBridgeMetric('bridge_restart_count');
4015
+ console.log(
4016
+ `Service-managed bridge start detected existing bridge (pid ${running.pid})${running.deviceId ? ` for device ${running.deviceId}` : ''}; reclaiming ownership.`
4017
+ );
4018
+ const result = await stopBridgeProcesses();
4019
+ if (Array.isArray(result.stillAlive) && result.stillAlive.length > 0) {
4020
+ throw new Error(`Failed to stop bridge processes: ${result.stillAlive.join(', ')}`);
4021
+ }
4022
+ }
4023
+
4024
+ incrementBridgeMetric('bridge_start_count');
4025
+ await startOpenclawBridge(flags);
4026
+ }
3971
4027
 
3972
4028
  async function handleBridgeLifecycleCommand(flags = {}, actionRaw = '') {
3973
4029
  const action = String(actionRaw || 'start').trim().toLowerCase();
@@ -4202,16 +4258,18 @@ if (__isDirectExecution) {
4202
4258
  export {
4203
4259
  prepareGatewayFrameForLocalGateway,
4204
4260
  ensureVoiceAssistantSpokenMetadata,
4261
+ buildBridgeLaunchAgentPlist,
4205
4262
  classifyBridgeFailure,
4206
4263
  classifyBridgeSessionScope,
4207
4264
  createBridgeProcessFaultHandler,
4208
- computeReconnectDelayMs,
4209
- resolveBridgeStatusForBrokerOpen,
4210
- resolveBridgeStatusForRuntimeFault,
4211
- runBridgeCallbackSafely,
4212
- extractGatewayRequestMeta,
4213
- extractGatewayResponseMeta,
4214
- isGatewayRunStartedFrame,
4215
- isBridgeWorkerCommand,
4216
- parsePositiveInteger,
4217
- };
4265
+ computeReconnectDelayMs,
4266
+ resolveBridgeStatusForBrokerOpen,
4267
+ resolveBridgeStatusForRuntimeFault,
4268
+ runBridgeCallbackSafely,
4269
+ extractGatewayRequestMeta,
4270
+ extractGatewayResponseMeta,
4271
+ isServiceManagedBridgeStart,
4272
+ isGatewayRunStartedFrame,
4273
+ isBridgeWorkerCommand,
4274
+ parsePositiveInteger,
4275
+ };
@@ -186,10 +186,9 @@ function normalizeOutgoingMetadata(payloadMetadata, { accountId, correlationId,
186
186
  ? { ...payloadMetadata }
187
187
  : {};
188
188
 
189
- const explicitSpokenPresent = Object.prototype.hasOwnProperty.call(metadata, 'spoken');
190
- const spoken =
191
- normalizeSpokenMetadata(metadata.spoken) ||
192
- (!explicitSpokenPresent ? inferSpokenMetadataFromContent(content) : null);
189
+ const spoken =
190
+ normalizeSpokenMetadata(metadata.spoken) ||
191
+ inferSpokenMetadataFromContent(content);
193
192
  if (spoken) {
194
193
  metadata.spoken = spoken;
195
194
  } else {
@@ -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.21",
5
+ "version": "0.2.24",
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.21",
3
+ "version": "0.2.24",
4
4
  "description": "Oomi OpenClaw channel plugin and bridge tooling",
5
5
  "bin": {
6
6
  "oomi": "bin/oomi-ai.js"