oomi-ai 0.2.27 → 0.2.29

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
@@ -4,13 +4,14 @@ OpenClaw channel plugin and bridge tooling for Oomi managed chat and voice.
4
4
 
5
5
  ## Current Focus
6
6
 
7
- `0.2.27` keeps the persona automation lane and adds a usable local managed-voice validation path:
7
+ `0.2.29` keeps the persona automation lane, adds a usable local managed-voice validation path, and makes bridge logging quiet by default in production:
8
8
  - WebSpatial-based persona scaffolding for generated Oomi apps
9
9
  - a high-level `oomi personas create-managed` command for agent-driven persona creation
10
10
  - device-authenticated persona runtime registration and job callbacks
11
11
  - automatic bridge-side polling for queued `persona_job` control messages
12
12
  - one shared spoken-metadata normalizer used by both the extension and the bridge
13
13
  - a repo-backed local `tts-pipeline` replay that can validate assistant-final -> backend -> real Qwen TTS before publishing
14
+ - spoken-metadata handling that preserves natural pauses like `...` and keeps the managed voice contract valid on the real chat session path
14
15
 
15
16
  This package is for two audiences:
16
17
  - OpenClaw operators who need to connect a machine to Oomi and keep chat or voice healthy
@@ -140,7 +141,7 @@ That bridge:
140
141
  - preserves or synthesizes `idempotencyKey` for `chat.send`
141
142
  - keeps voice-session faults from poisoning normal provider health where possible
142
143
 
143
- This is the part of the package most likely to matter when debugging voice turn failures.
144
+ This is the part of the package most likely to matter when debugging voice turn failures.
144
145
 
145
146
  For managed cloned-voice replies, the canonical contract is:
146
147
  - visible assistant `content` stays user-facing
@@ -149,6 +150,18 @@ For managed cloned-voice replies, the canonical contract is:
149
150
 
150
151
  The backend cloned-voice path is intentionally strict. If `metadata.spoken` does not reach Oomi, backend TTS fails instead of speaking a flat fallback voice.
151
152
 
153
+ ## Bridge Logging
154
+
155
+ The bridge is intentionally quiet by default in production so normal deploys do not spam logs with frame-level transport noise.
156
+
157
+ To enable verbose bridge tracing temporarily, set:
158
+
159
+ ```bash
160
+ OOMI_BRIDGE_DEBUG=1
161
+ ```
162
+
163
+ With that flag enabled, the bridge will emit low-level session, frame, and spoken-metadata debug logs again.
164
+
152
165
  ## Local TTS Validation
153
166
 
154
167
  If you are developing this package inside the Oomi repo, you can now validate the managed voice path locally before publishing.
package/bin/oomi-ai.js CHANGED
@@ -54,6 +54,17 @@ const DEBUG_PROVIDER_ENV_KEYS = [
54
54
  ];
55
55
  const DEVICE_IDENTITY_PATH = path.join(os.homedir(), '.openclaw', 'identity', 'device.json');
56
56
  const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
57
+ const BRIDGE_DEBUG_ENABLED = process.env.OOMI_BRIDGE_DEBUG === '1';
58
+
59
+ function bridgeDebugLog(...args) {
60
+ if (!BRIDGE_DEBUG_ENABLED) return;
61
+ console.log(...args);
62
+ }
63
+
64
+ function bridgeDebugWarn(...args) {
65
+ if (!BRIDGE_DEBUG_ENABLED) return;
66
+ console.warn(...args);
67
+ }
57
68
 
58
69
  function parsePositiveInteger(value, fallback) {
59
70
  const num = Number(value);
@@ -2960,7 +2971,7 @@ async function startOpenclawBridge(flags) {
2960
2971
  : null;
2961
2972
 
2962
2973
  if (personaJobPollEnabled && brokerHttp && deviceToken) {
2963
- console.log('[persona-jobs] polling filtered control queue for persona_job messages.');
2974
+ bridgeDebugLog('[persona-jobs] polling filtered control queue for persona_job messages.');
2964
2975
  } else if (personaJobPollEnabled) {
2965
2976
  console.warn('[persona-jobs] disabled because broker HTTP URL or device token is unavailable.');
2966
2977
  }
@@ -3195,9 +3206,9 @@ async function startOpenclawBridge(flags) {
3195
3206
  classifyBridgeFailure({ reason: 'connection closed without classified error' });
3196
3207
  const delayMs = computeReconnectDelayMs(reconnectState.attempt, failure.baseDelayMs);
3197
3208
 
3198
- console.warn(
3199
- `[bridge] reconnect scheduled in ${delayMs}ms (attempt ${reconnectState.attempt}, class=${failure.failureClass}, code=${failure.errorCode})`
3200
- );
3209
+ bridgeDebugWarn(
3210
+ `[bridge] reconnect scheduled in ${delayMs}ms (attempt ${reconnectState.attempt}, class=${failure.failureClass}, code=${failure.errorCode})`
3211
+ );
3201
3212
 
3202
3213
  updateBridgeStatus({
3203
3214
  status: 'reconnecting',
@@ -3323,7 +3334,7 @@ async function startOpenclawBridge(flags) {
3323
3334
  }
3324
3335
  const result = forwardFrameToSession(sessionBridge, prepared.frameText);
3325
3336
  if (result === 'queued') {
3326
- console.log(`[bridge] client.frame queued after challenge ${sessionId}`);
3337
+ bridgeDebugLog(`[bridge] client.frame queued after challenge ${sessionId}`);
3327
3338
  if (requestMeta) {
3328
3339
  startPendingRequestTimeout(brokerSocket, sessionId, sessionBridge, requestMeta);
3329
3340
  }
@@ -3337,7 +3348,7 @@ async function startOpenclawBridge(flags) {
3337
3348
  });
3338
3349
  }
3339
3350
  } else if (result === 'dropped') {
3340
- console.log(`[bridge] client.frame dropped after challenge ${sessionId}`);
3351
+ bridgeDebugLog(`[bridge] client.frame dropped after challenge ${sessionId}`);
3341
3352
  incrementBridgeMetric('bridge_drop_count');
3342
3353
  if (requestMeta) {
3343
3354
  const pending = sessionBridge.pendingRequests instanceof Map
@@ -3369,7 +3380,7 @@ async function startOpenclawBridge(flags) {
3369
3380
  });
3370
3381
  }
3371
3382
  } else {
3372
- console.log(`[bridge] client.frame sent after challenge ${sessionId}`);
3383
+ bridgeDebugLog(`[bridge] client.frame sent after challenge ${sessionId}`);
3373
3384
  if (requestMeta) {
3374
3385
  startPendingRequestTimeout(brokerSocket, sessionId, sessionBridge, requestMeta);
3375
3386
  }
@@ -3435,7 +3446,7 @@ async function startOpenclawBridge(flags) {
3435
3446
  clearTimeout(connectTimeout);
3436
3447
  connectTimeout = null;
3437
3448
  }
3438
- console.log(`[bridge] gateway.open ${sessionId}`);
3449
+ bridgeDebugLog(`[bridge] gateway.open ${sessionId}`);
3439
3450
  flushSessionQueue(sessionBridge);
3440
3451
  });
3441
3452
 
@@ -3445,17 +3456,17 @@ async function startOpenclawBridge(flags) {
3445
3456
  if (spokenNormalized.changed) {
3446
3457
  frame = spokenNormalized.frameText;
3447
3458
  if (spokenNormalized.scope === 'voice') {
3448
- console.log(`[bridge] voice.spoken_metadata.${spokenNormalized.reason} ${sessionId} ${JSON.stringify({
3459
+ bridgeDebugLog(`[bridge] voice.spoken_metadata.${spokenNormalized.reason} ${sessionId} ${JSON.stringify({
3449
3460
  before: spokenNormalized.summary,
3450
3461
  after: summarizeVoiceFrameContract(frame),
3451
3462
  })}`);
3452
3463
  }
3453
3464
  } else if (spokenNormalized.scope === 'voice' && spokenNormalized.summary.event === 'chat' && spokenNormalized.summary.state === 'final') {
3454
- console.log(`[bridge] voice.chat.final ${sessionId} ${JSON.stringify(spokenNormalized.summary)}`);
3465
+ bridgeDebugLog(`[bridge] voice.chat.final ${sessionId} ${JSON.stringify(spokenNormalized.summary)}`);
3455
3466
  }
3456
3467
  const gatewayPayload = parseJsonPayload(frame);
3457
3468
  if (gatewayPayload?.event === 'connect.challenge') {
3458
- console.log(`[bridge] gateway.connect.challenge ${sessionId}`);
3469
+ bridgeDebugLog(`[bridge] gateway.connect.challenge ${sessionId}`);
3459
3470
  const nonce =
3460
3471
  gatewayPayload.payload && typeof gatewayPayload.payload.nonce === 'string'
3461
3472
  ? gatewayPayload.payload.nonce.trim()
@@ -3601,9 +3612,9 @@ async function startOpenclawBridge(flags) {
3601
3612
  clearChallengeTimer(sessionBridge);
3602
3613
  const reasonText = reason ? reason.toString() : '';
3603
3614
  const closeMeta = classifyGatewayClose(code, reasonText);
3604
- console.log(
3605
- `[bridge] gateway.close ${sessionId} code=${String(code)}${reasonText ? ` reason=${reasonText}` : ''}`
3606
- );
3615
+ bridgeDebugLog(
3616
+ `[bridge] gateway.close ${sessionId} code=${String(code)}${reasonText ? ` reason=${reasonText}` : ''}`
3617
+ );
3607
3618
  if (sessionBridge.pendingRequests instanceof Map) {
3608
3619
  for (const requestMeta of sessionBridge.pendingRequests.values()) {
3609
3620
  if (!requestMeta || typeof requestMeta !== 'object') continue;
@@ -3678,7 +3689,7 @@ async function startOpenclawBridge(flags) {
3678
3689
  };
3679
3690
 
3680
3691
  brokerSocket.on('open', () => {
3681
- console.log('[bridge] Connected to managed broker.');
3692
+ bridgeDebugLog('[bridge] Connected to managed broker.');
3682
3693
  reconnectState.attempt = 0;
3683
3694
  reconnectState.lastFailure = null;
3684
3695
  if (actionCableMode) {
@@ -3795,14 +3806,14 @@ async function startOpenclawBridge(flags) {
3795
3806
  }
3796
3807
 
3797
3808
  if (payload.type === 'device.ready') {
3798
- console.log(`[bridge] Broker ready for device ${payload.deviceId || deviceId}.`);
3809
+ bridgeDebugLog(`[bridge] Broker ready for device ${payload.deviceId || deviceId}.`);
3799
3810
  return;
3800
3811
  }
3801
3812
 
3802
3813
  if (payload.type === 'client.open') {
3803
3814
  const sessionId = String(payload.sessionId || '').trim();
3804
3815
  if (!sessionId) return;
3805
- console.log(`[bridge] client.open ${sessionId}`);
3816
+ bridgeDebugLog(`[bridge] client.open ${sessionId}`);
3806
3817
  getOrCreateGatewaySession(sessionId);
3807
3818
  return;
3808
3819
  }
@@ -3812,9 +3823,9 @@ async function startOpenclawBridge(flags) {
3812
3823
  const frame = typeof payload.frame === 'string' ? payload.frame : '';
3813
3824
  if (!sessionId || !frame) return;
3814
3825
  if (classifyBridgeSessionScope(sessionId) === 'voice') {
3815
- console.log(`[bridge] client.frame ${sessionId} ${JSON.stringify(summarizeVoiceFrameContract(frame))}`);
3826
+ bridgeDebugLog(`[bridge] client.frame ${sessionId} ${JSON.stringify(summarizeVoiceFrameContract(frame))}`);
3816
3827
  } else {
3817
- console.log(`[bridge] client.frame ${sessionId}`);
3828
+ bridgeDebugLog(`[bridge] client.frame ${sessionId}`);
3818
3829
  }
3819
3830
  const sessionBridge = getOrCreateGatewaySession(sessionId);
3820
3831
  if (!sessionBridge) return;
@@ -3840,7 +3851,7 @@ async function startOpenclawBridge(flags) {
3840
3851
  });
3841
3852
  if (prepared.waitForChallenge) {
3842
3853
  queueConnectUntilChallenge(sessionId, sessionBridge, frame);
3843
- console.log(`[bridge] client.frame waiting for challenge ${sessionId}`);
3854
+ bridgeDebugLog(`[bridge] client.frame waiting for challenge ${sessionId}`);
3844
3855
  if (requestMeta) {
3845
3856
  sendGatewayAck(brokerSocket, {
3846
3857
  sessionId,
@@ -3886,7 +3897,7 @@ async function startOpenclawBridge(flags) {
3886
3897
  requiresConnectAccepted: Boolean(requestMeta && requestMeta.method !== 'connect'),
3887
3898
  });
3888
3899
  if (result === 'waiting_for_connect') {
3889
- console.log(`[bridge] client.frame waiting for connect ${sessionId}`);
3900
+ bridgeDebugLog(`[bridge] client.frame waiting for connect ${sessionId}`);
3890
3901
  if (requestMeta) {
3891
3902
  sendGatewayAck(brokerSocket, {
3892
3903
  sessionId,
@@ -3899,7 +3910,7 @@ async function startOpenclawBridge(flags) {
3899
3910
  return;
3900
3911
  }
3901
3912
  if (result === 'queued') {
3902
- console.log(`[bridge] client.frame queued ${sessionId}`);
3913
+ bridgeDebugLog(`[bridge] client.frame queued ${sessionId}`);
3903
3914
  if (requestMeta) {
3904
3915
  startPendingRequestTimeout(brokerSocket, sessionId, sessionBridge, requestMeta);
3905
3916
  }
@@ -3913,7 +3924,7 @@ async function startOpenclawBridge(flags) {
3913
3924
  });
3914
3925
  }
3915
3926
  } else if (result === 'dropped') {
3916
- console.log(`[bridge] client.frame dropped (socket not open) ${sessionId}`);
3927
+ bridgeDebugLog(`[bridge] client.frame dropped (socket not open) ${sessionId}`);
3917
3928
  incrementBridgeMetric('bridge_drop_count');
3918
3929
  if (requestMeta) {
3919
3930
  const pending = sessionBridge.pendingRequests instanceof Map
@@ -3959,7 +3970,7 @@ async function startOpenclawBridge(flags) {
3959
3970
 
3960
3971
  if (payload.type === 'client.close') {
3961
3972
  const sessionId = String(payload.sessionId || '').trim();
3962
- console.log(`[bridge] client.close ${sessionId}`);
3973
+ bridgeDebugLog(`[bridge] client.close ${sessionId}`);
3963
3974
  const sessionBridge = activeGatewaySockets.get(sessionId);
3964
3975
  if (sessionBridge && sessionBridge.socket) {
3965
3976
  clearChallengeTimer(sessionBridge);
@@ -3982,7 +3993,7 @@ async function startOpenclawBridge(flags) {
3982
3993
  actionCableHeartbeat = null;
3983
3994
  }
3984
3995
  const reasonText = reason ? reason.toString() : '';
3985
- console.log(`[bridge] Broker disconnected (${code}) ${reasonText}`);
3996
+ bridgeDebugLog(`[bridge] Broker disconnected (${code}) ${reasonText}`);
3986
3997
  incrementBridgeMetric('bridge_disconnect_count');
3987
3998
  for (const [sessionId, sessionBridge] of activeGatewaySockets.entries()) {
3988
3999
  clearChallengeTimer(sessionBridge);
@@ -2,6 +2,8 @@ function trimString(value, fallback = '') {
2
2
  return typeof value === 'string' && value.trim() ? value.trim() : fallback;
3
3
  }
4
4
 
5
+ const ELLIPSIS_PLACEHOLDER = '__OOMI_ELLIPSIS__';
6
+
5
7
  function stripAvatarCommandTags(text) {
6
8
  return text.replace(/\[(anim|animation|face|expression|emotion|gesture|look|gaze):[^\]]+\]/gi, ' ');
7
9
  }
@@ -70,15 +72,19 @@ function normalizeSpeechText(text) {
70
72
  .replace(/\*\*(.*?)\*\*/g, '$1')
71
73
  .replace(/__(.*?)__/g, '$1')
72
74
  .replace(/`([^`]+)`/g, '$1')
73
- .replace(/[\u2013\u2014]/g, ', ')
74
- .replace(/\u2026/g, '...')
75
- .replace(/\s+/g, ' ')
76
- .replace(/\s+([,.;!?])/g, '$1')
77
- .replace(/([,.;!?])(?=[^\s])/g, '$1 ')
78
- .replace(/,\s*,+/g, ', ')
79
- .replace(/\s+/g, ' ')
80
- .trim();
81
- }
75
+ .replace(/[\u2013\u2014]/g, ', ')
76
+ .replace(/\u2026/g, ELLIPSIS_PLACEHOLDER)
77
+ .replace(/\.{3,}/g, ELLIPSIS_PLACEHOLDER)
78
+ .replace(/\s+/g, ' ')
79
+ .replace(/\s+([,.;!?])/g, '$1')
80
+ .replace(/([,;!?])(?=[^\s])/g, '$1 ')
81
+ .replace(/(\.)(?=[^\s.])/g, '$1 ')
82
+ .replace(/,\s*,+/g, ', ')
83
+ .replace(new RegExp(`${ELLIPSIS_PLACEHOLDER}(?=[^\\s,.;!?])`, 'g'), `${ELLIPSIS_PLACEHOLDER} `)
84
+ .replace(new RegExp(ELLIPSIS_PLACEHOLDER, 'g'), '...')
85
+ .replace(/\s+/g, ' ')
86
+ .trim();
87
+ }
82
88
 
83
89
  function splitSpeechSegments(text) {
84
90
  const normalized = normalizeSpeechText(text);
@@ -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.27",
5
+ "version": "0.2.29",
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.27",
3
+ "version": "0.2.29",
4
4
  "description": "Oomi OpenClaw channel plugin and bridge tooling",
5
5
  "bin": {
6
6
  "oomi": "bin/oomi-ai.js"