oomi-ai 0.2.28 → 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 +14 -2
- package/bin/oomi-ai.js +36 -25
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ OpenClaw channel plugin and bridge tooling for Oomi managed chat and voice.
|
|
|
4
4
|
|
|
5
5
|
## Current Focus
|
|
6
6
|
|
|
7
|
-
`0.2.
|
|
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
|
|
@@ -141,7 +141,7 @@ That bridge:
|
|
|
141
141
|
- preserves or synthesizes `idempotencyKey` for `chat.send`
|
|
142
142
|
- keeps voice-session faults from poisoning normal provider health where possible
|
|
143
143
|
|
|
144
|
-
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.
|
|
145
145
|
|
|
146
146
|
For managed cloned-voice replies, the canonical contract is:
|
|
147
147
|
- visible assistant `content` stays user-facing
|
|
@@ -150,6 +150,18 @@ For managed cloned-voice replies, the canonical contract is:
|
|
|
150
150
|
|
|
151
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.
|
|
152
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
|
+
|
|
153
165
|
## Local TTS Validation
|
|
154
166
|
|
|
155
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3826
|
+
bridgeDebugLog(`[bridge] client.frame ${sessionId} ${JSON.stringify(summarizeVoiceFrameContract(frame))}`);
|
|
3816
3827
|
} else {
|
|
3817
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/openclaw.plugin.json
CHANGED