ocuclaw 1.2.4 → 1.3.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 +21 -6
- package/dist/config/runtime-config.js +84 -3
- package/dist/domain/activity-status-adapter.js +138 -605
- package/dist/domain/activity-status-arbiter.js +109 -0
- package/dist/domain/activity-status-labels.js +906 -0
- package/dist/domain/code-span-regions.js +103 -0
- package/dist/domain/conversation-state.js +14 -1
- package/dist/domain/debug-store.js +56 -182
- package/dist/domain/glasses-ui-content-summary.js +62 -0
- package/dist/domain/glasses-ui-system-prompt.js +28 -0
- package/dist/domain/message-emoji-allowlist.js +16 -0
- package/dist/domain/message-emoji-filter.js +33 -55
- package/dist/domain/neural-emoji-reactor-system-prompt.js +43 -0
- package/dist/domain/neural-emoji-reactor-tag-config.js +56 -0
- package/dist/domain/neural-pace-modulator-system-prompt.js +32 -0
- package/dist/domain/neural-pace-modulator-tag-config.js +51 -0
- package/dist/domain/tagged-span-parser.js +121 -0
- package/dist/domain/tagged-span-strip.js +38 -0
- package/dist/even-ai/even-ai-endpoint.js +91 -0
- package/dist/even-ai/even-ai-run-waiter.js +14 -0
- package/dist/even-ai/even-ai-settings-store.js +14 -0
- package/dist/gateway/gateway-bridge.js +14 -2
- package/dist/gateway/gateway-timing-ledger.js +457 -0
- package/dist/gateway/openclaw-client.js +462 -38
- package/dist/index.js +28 -1
- package/dist/runtime/downstream-handler.js +754 -83
- package/dist/runtime/ocuclaw-settings-store.js +74 -31
- package/dist/runtime/plugin-version-service.js +23 -0
- package/dist/runtime/protocol-adapter.js +9 -0
- package/dist/runtime/provider-usage-select.js +168 -0
- package/dist/runtime/relay-client-nudge-controller.js +553 -0
- package/dist/runtime/relay-core.js +1293 -225
- package/dist/runtime/relay-health-monitor.js +172 -0
- package/dist/runtime/relay-operation-registry.js +263 -0
- package/dist/runtime/relay-service.js +201 -1
- package/dist/runtime/relay-worker-approval-replay-cache.js +68 -0
- package/dist/runtime/relay-worker-entry.js +32 -0
- package/dist/runtime/relay-worker-health.js +272 -0
- package/dist/runtime/relay-worker-protocol.js +281 -0
- package/dist/runtime/relay-worker-queue.js +202 -0
- package/dist/runtime/relay-worker-supervisor.js +1004 -0
- package/dist/runtime/relay-worker-transport.js +1051 -0
- package/dist/runtime/session-context-service.js +189 -0
- package/dist/runtime/session-service.js +638 -27
- package/dist/runtime/upstream-runtime.js +1167 -60
- package/dist/tools/device-info-tool.js +242 -0
- package/dist/tools/glasses-ui-cron.js +427 -0
- package/dist/tools/glasses-ui-descriptors.js +261 -0
- package/dist/tools/glasses-ui-limits.js +21 -0
- package/dist/tools/glasses-ui-paint-floor.js +99 -0
- package/dist/tools/glasses-ui-recipes.js +581 -0
- package/dist/tools/glasses-ui-surfaces.js +278 -0
- package/dist/tools/glasses-ui-template.js +182 -0
- package/dist/tools/glasses-ui-tool.js +1111 -0
- package/dist/tools/session-title-tool.js +209 -0
- package/dist/version.js +2 -0
- package/openclaw.plugin.json +163 -15
- package/package.json +14 -5
- package/skills/glasses-ui/SKILL.md +156 -0
- package/dist/runtime/downstream-server.js +0 -1891
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { PerformanceObserver, monitorEventLoopDelay } from "node:perf_hooks";
|
|
2
|
+
|
|
3
|
+
const LOW_LAG_SAMPLE_THRESHOLD_MS = 50;
|
|
4
|
+
const LOW_LAG_HEARTBEAT_MS = 60_000;
|
|
5
|
+
const SPIKE_BUCKETS_MS = [250, 1_000, 5_000, 10_000];
|
|
6
|
+
const DEFAULT_SEND_BUFFER_HIGH_WATER_BYTES = 262_144;
|
|
7
|
+
|
|
8
|
+
export function classifyFrameForRelayHealth(messageType) {
|
|
9
|
+
if (
|
|
10
|
+
messageType === "ping" ||
|
|
11
|
+
messageType === "pong" ||
|
|
12
|
+
messageType === "protocolHelloAck" ||
|
|
13
|
+
messageType === "ocuclaw.sync.resume.ack" ||
|
|
14
|
+
messageType === "ocuclaw.worker.health"
|
|
15
|
+
) {
|
|
16
|
+
return "transport-control";
|
|
17
|
+
}
|
|
18
|
+
if (
|
|
19
|
+
messageType === "ocuclaw.operation.received" ||
|
|
20
|
+
messageType === "ocuclaw.worker.operation.received" ||
|
|
21
|
+
messageType === "ocuclaw.relay.busy"
|
|
22
|
+
) {
|
|
23
|
+
return "operation-control";
|
|
24
|
+
}
|
|
25
|
+
if (
|
|
26
|
+
messageType === "ocuclaw.message.send.ack" ||
|
|
27
|
+
messageType === "ocuclaw.approval.resolve.ack"
|
|
28
|
+
) {
|
|
29
|
+
return "transactional";
|
|
30
|
+
}
|
|
31
|
+
if (
|
|
32
|
+
messageType === "ocuclaw.session.switch.applied" ||
|
|
33
|
+
messageType === "ocuclaw.session.config.set.ack" ||
|
|
34
|
+
messageType === "ocuclaw.evenai.settings.set.ack" ||
|
|
35
|
+
messageType === "ocuclaw.settings.set.ack"
|
|
36
|
+
) {
|
|
37
|
+
return "latest-mutation";
|
|
38
|
+
}
|
|
39
|
+
if (
|
|
40
|
+
messageType === "ocuclaw.runtime.status" ||
|
|
41
|
+
messageType === "ocuclaw.session.list.result" ||
|
|
42
|
+
messageType === "ocuclaw.provider.usage.snapshot" ||
|
|
43
|
+
messageType === "ocuclaw.model.catalog.snapshot" ||
|
|
44
|
+
messageType === "ocuclaw.skills.catalog.snapshot"
|
|
45
|
+
) {
|
|
46
|
+
return "coalescable-read";
|
|
47
|
+
}
|
|
48
|
+
return "best-effort";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createRelayHealthMonitor(options) {
|
|
52
|
+
const now = options.now || Date.now;
|
|
53
|
+
const setIntervalFn = options.setIntervalFn || setInterval;
|
|
54
|
+
const clearIntervalFn = options.clearIntervalFn || clearInterval;
|
|
55
|
+
const sampleIntervalMs = options.sampleIntervalMs || 1_000;
|
|
56
|
+
const sendBufferHighWaterBytes =
|
|
57
|
+
options.sendBufferHighWaterBytes || DEFAULT_SEND_BUFFER_HIGH_WATER_BYTES;
|
|
58
|
+
let intervalId = null;
|
|
59
|
+
let delayMonitor = null;
|
|
60
|
+
let gcObserver = null;
|
|
61
|
+
let lastLowLagEmitAtMs = null;
|
|
62
|
+
const emittedSpikeBuckets = new Set();
|
|
63
|
+
|
|
64
|
+
function defaultSampleEventLoopDelay() {
|
|
65
|
+
if (!delayMonitor) {
|
|
66
|
+
return { p50Ms: 0, p95Ms: 0, maxMs: 0, sampleCount: 0 };
|
|
67
|
+
}
|
|
68
|
+
const sample = {
|
|
69
|
+
p50Ms: delayMonitor.percentile(50) / 1_000_000,
|
|
70
|
+
p95Ms: delayMonitor.percentile(95) / 1_000_000,
|
|
71
|
+
maxMs: delayMonitor.max / 1_000_000,
|
|
72
|
+
sampleCount: Number(delayMonitor.count || 0),
|
|
73
|
+
};
|
|
74
|
+
delayMonitor.reset();
|
|
75
|
+
return sample;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function emitLagSample() {
|
|
79
|
+
const sample = options.sampleEventLoopDelay
|
|
80
|
+
? options.sampleEventLoopDelay()
|
|
81
|
+
: defaultSampleEventLoopDelay();
|
|
82
|
+
const nowMs = now();
|
|
83
|
+
const shouldEmitLowLagHeartbeat =
|
|
84
|
+
lastLowLagEmitAtMs === null || nowMs - lastLowLagEmitAtMs >= LOW_LAG_HEARTBEAT_MS;
|
|
85
|
+
const shouldEmitSample =
|
|
86
|
+
sample.maxMs >= LOW_LAG_SAMPLE_THRESHOLD_MS || shouldEmitLowLagHeartbeat;
|
|
87
|
+
if (shouldEmitSample) {
|
|
88
|
+
if (sample.maxMs < LOW_LAG_SAMPLE_THRESHOLD_MS) {
|
|
89
|
+
lastLowLagEmitAtMs = nowMs;
|
|
90
|
+
}
|
|
91
|
+
options.emitDebug("event_loop_lag_sample", "debug", {
|
|
92
|
+
p50Ms: Math.round(sample.p50Ms),
|
|
93
|
+
p95Ms: Math.round(sample.p95Ms),
|
|
94
|
+
maxMs: Math.round(sample.maxMs),
|
|
95
|
+
sampleCount: sample.sampleCount,
|
|
96
|
+
sampleIntervalMs,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
for (const bucketMs of SPIKE_BUCKETS_MS) {
|
|
100
|
+
if (sample.maxMs >= bucketMs && !emittedSpikeBuckets.has(bucketMs)) {
|
|
101
|
+
emittedSpikeBuckets.add(bucketMs);
|
|
102
|
+
options.emitDebug("event_loop_lag_spike", "warn", {
|
|
103
|
+
bucketMs,
|
|
104
|
+
maxMs: Math.round(sample.maxMs),
|
|
105
|
+
p95Ms: Math.round(sample.p95Ms),
|
|
106
|
+
sampleCount: sample.sampleCount,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (sample.maxMs < SPIKE_BUCKETS_MS[0]) {
|
|
111
|
+
emittedSpikeBuckets.clear();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function start() {
|
|
116
|
+
if (intervalId !== null) return;
|
|
117
|
+
lastLowLagEmitAtMs = now();
|
|
118
|
+
if (!options.sampleEventLoopDelay) {
|
|
119
|
+
delayMonitor = monitorEventLoopDelay({ resolution: 50 });
|
|
120
|
+
delayMonitor.enable();
|
|
121
|
+
}
|
|
122
|
+
if (options.observeGc !== false) {
|
|
123
|
+
gcObserver = new PerformanceObserver((list) => {
|
|
124
|
+
for (const entry of list.getEntries()) {
|
|
125
|
+
options.emitDebug("gc_pause", "warn", {
|
|
126
|
+
durationMs: Math.round(entry.duration),
|
|
127
|
+
kind: Number(entry.kind || 0),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
gcObserver.observe({ entryTypes: ["gc"] });
|
|
132
|
+
}
|
|
133
|
+
intervalId = setIntervalFn(emitLagSample, sampleIntervalMs);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function stop() {
|
|
137
|
+
if (intervalId !== null) {
|
|
138
|
+
clearIntervalFn(intervalId);
|
|
139
|
+
intervalId = null;
|
|
140
|
+
}
|
|
141
|
+
if (delayMonitor) {
|
|
142
|
+
delayMonitor.disable();
|
|
143
|
+
delayMonitor = null;
|
|
144
|
+
}
|
|
145
|
+
if (gcObserver) {
|
|
146
|
+
gcObserver.disconnect();
|
|
147
|
+
gcObserver = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function observeSendBuffer(params) {
|
|
152
|
+
if (
|
|
153
|
+
params.bufferedAmountBytes !== null &&
|
|
154
|
+
Number.isFinite(params.bufferedAmountBytes) &&
|
|
155
|
+
params.bufferedAmountBytes >= sendBufferHighWaterBytes
|
|
156
|
+
) {
|
|
157
|
+
options.emitDebug("ws_send_buffer_high_water", "warn", {
|
|
158
|
+
clientId: params.clientId,
|
|
159
|
+
messageType: params.messageType,
|
|
160
|
+
frameClass: classifyFrameForRelayHealth(params.messageType),
|
|
161
|
+
bufferedAmountBytes: params.bufferedAmountBytes,
|
|
162
|
+
thresholdBytes: sendBufferHighWaterBytes,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function emitQueueDepth(snapshot) {
|
|
168
|
+
options.emitDebug("relay_queue_depth", "debug", snapshot);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { start, stop, observeSendBuffer, emitQueueDepth };
|
|
172
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
const SLOW_BUCKETS_MS = [5_000, 10_000, 30_000, 60_000];
|
|
2
|
+
|
|
3
|
+
export function createRelayOperationRegistry(options) {
|
|
4
|
+
const now = options.now || Date.now;
|
|
5
|
+
const retentionMs = options.retentionMs || 90_000;
|
|
6
|
+
const entriesByRequestId = new Map();
|
|
7
|
+
const requestIdByRunId = new Map();
|
|
8
|
+
|
|
9
|
+
function prune(nowMs = now()) {
|
|
10
|
+
for (const [requestId, entry] of entriesByRequestId) {
|
|
11
|
+
if (nowMs - entry.receivedAtMs > retentionMs) {
|
|
12
|
+
entriesByRequestId.delete(requestId);
|
|
13
|
+
for (const [runId, mappedRequestId] of requestIdByRunId) {
|
|
14
|
+
if (mappedRequestId === requestId) {
|
|
15
|
+
requestIdByRunId.delete(runId);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function receiptFrame(entry) {
|
|
23
|
+
return JSON.stringify({
|
|
24
|
+
type: "ocuclaw.operation.received",
|
|
25
|
+
requestId: entry.requestId,
|
|
26
|
+
operation: entry.operation,
|
|
27
|
+
status: "upstream_pending",
|
|
28
|
+
phase: "relay_received",
|
|
29
|
+
receivedAtMs: entry.receivedAtMs,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function beginMessageSend(params) {
|
|
34
|
+
prune();
|
|
35
|
+
const existing = entriesByRequestId.get(params.requestId);
|
|
36
|
+
if (existing) {
|
|
37
|
+
options.emitDebug(
|
|
38
|
+
"operation_received",
|
|
39
|
+
"info",
|
|
40
|
+
{
|
|
41
|
+
requestId: existing.requestId,
|
|
42
|
+
operation: existing.operation,
|
|
43
|
+
class: existing.class,
|
|
44
|
+
clientId: existing.clientId,
|
|
45
|
+
sessionKey: existing.sessionKey,
|
|
46
|
+
duplicate: true,
|
|
47
|
+
retainedFinal: existing.finalFrame !== null,
|
|
48
|
+
},
|
|
49
|
+
{ sessionKey: existing.sessionKey },
|
|
50
|
+
);
|
|
51
|
+
return {
|
|
52
|
+
duplicate: true,
|
|
53
|
+
receipt: receiptFrame(existing),
|
|
54
|
+
finalFrame: existing.finalFrame,
|
|
55
|
+
complete: (finalFrame, result = {}) =>
|
|
56
|
+
complete(existing.requestId, finalFrame, result),
|
|
57
|
+
fail: (finalFrame, result = {}) =>
|
|
58
|
+
fail(existing.requestId, finalFrame, result),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const receivedAtMs = now();
|
|
63
|
+
const entry = {
|
|
64
|
+
requestId: params.requestId,
|
|
65
|
+
operation: "message.send",
|
|
66
|
+
class: "transactional",
|
|
67
|
+
clientId: params.clientId,
|
|
68
|
+
sessionKey: params.sessionKey || null,
|
|
69
|
+
receivedAtMs,
|
|
70
|
+
startedAtMs: null,
|
|
71
|
+
upstreamAckAtMs: null,
|
|
72
|
+
lifecycleStartAtMs: null,
|
|
73
|
+
firstStreamAtMs: null,
|
|
74
|
+
completedAtMs: null,
|
|
75
|
+
finalFrame: null,
|
|
76
|
+
slowBuckets: new Set(),
|
|
77
|
+
};
|
|
78
|
+
entriesByRequestId.set(entry.requestId, entry);
|
|
79
|
+
options.emitDebug(
|
|
80
|
+
"operation_received",
|
|
81
|
+
"info",
|
|
82
|
+
{
|
|
83
|
+
requestId: entry.requestId,
|
|
84
|
+
operation: entry.operation,
|
|
85
|
+
class: entry.class,
|
|
86
|
+
clientId: entry.clientId,
|
|
87
|
+
sessionKey: entry.sessionKey,
|
|
88
|
+
duplicate: false,
|
|
89
|
+
},
|
|
90
|
+
{ sessionKey: entry.sessionKey },
|
|
91
|
+
);
|
|
92
|
+
return {
|
|
93
|
+
duplicate: false,
|
|
94
|
+
receipt: receiptFrame(entry),
|
|
95
|
+
finalFrame: null,
|
|
96
|
+
complete: (finalFrame, result = {}) => complete(entry.requestId, finalFrame, result),
|
|
97
|
+
fail: (finalFrame, result = {}) => fail(entry.requestId, finalFrame, result),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function markStarted(requestId) {
|
|
102
|
+
const entry = entriesByRequestId.get(requestId);
|
|
103
|
+
if (!entry || entry.startedAtMs !== null) return;
|
|
104
|
+
entry.startedAtMs = now();
|
|
105
|
+
options.emitDebug(
|
|
106
|
+
"operation_started",
|
|
107
|
+
"debug",
|
|
108
|
+
{
|
|
109
|
+
requestId,
|
|
110
|
+
operation: entry.operation,
|
|
111
|
+
class: entry.class,
|
|
112
|
+
elapsedMs: entry.startedAtMs - entry.receivedAtMs,
|
|
113
|
+
},
|
|
114
|
+
{ sessionKey: entry.sessionKey },
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function markUpstreamAck(requestId, params = {}) {
|
|
119
|
+
const entry = entriesByRequestId.get(requestId);
|
|
120
|
+
if (!entry) return;
|
|
121
|
+
entry.upstreamAckAtMs = now();
|
|
122
|
+
if (params.runId) requestIdByRunId.set(params.runId, requestId);
|
|
123
|
+
options.emitDebug(
|
|
124
|
+
"operation_phase",
|
|
125
|
+
"debug",
|
|
126
|
+
{
|
|
127
|
+
requestId,
|
|
128
|
+
operation: entry.operation,
|
|
129
|
+
class: entry.class,
|
|
130
|
+
phase: "upstream_ack",
|
|
131
|
+
status: params.status || null,
|
|
132
|
+
elapsedMs: entry.upstreamAckAtMs - entry.receivedAtMs,
|
|
133
|
+
upstreamAckMs: entry.upstreamAckAtMs - entry.receivedAtMs,
|
|
134
|
+
},
|
|
135
|
+
{ sessionKey: entry.sessionKey, runId: params.runId || null },
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function markRunPhase(runId, phase) {
|
|
140
|
+
const requestId = requestIdByRunId.get(runId);
|
|
141
|
+
if (!requestId) return;
|
|
142
|
+
const entry = entriesByRequestId.get(requestId);
|
|
143
|
+
if (!entry) return;
|
|
144
|
+
const atMs = now();
|
|
145
|
+
if (phase === "lifecycle_start" && entry.lifecycleStartAtMs === null) {
|
|
146
|
+
entry.lifecycleStartAtMs = atMs;
|
|
147
|
+
}
|
|
148
|
+
if (phase === "first_stream" && entry.firstStreamAtMs === null) {
|
|
149
|
+
entry.firstStreamAtMs = atMs;
|
|
150
|
+
}
|
|
151
|
+
if (phase === "complete") {
|
|
152
|
+
entry.completedAtMs = atMs;
|
|
153
|
+
}
|
|
154
|
+
options.emitDebug(
|
|
155
|
+
"operation_phase",
|
|
156
|
+
"debug",
|
|
157
|
+
{
|
|
158
|
+
requestId,
|
|
159
|
+
operation: entry.operation,
|
|
160
|
+
class: entry.class,
|
|
161
|
+
phase,
|
|
162
|
+
elapsedMs: atMs - entry.receivedAtMs,
|
|
163
|
+
ackToPhaseMs: entry.upstreamAckAtMs ? atMs - entry.upstreamAckAtMs : null,
|
|
164
|
+
lifecycleStartMs: entry.lifecycleStartAtMs
|
|
165
|
+
? entry.lifecycleStartAtMs - entry.receivedAtMs
|
|
166
|
+
: null,
|
|
167
|
+
firstStreamMs: entry.firstStreamAtMs
|
|
168
|
+
? entry.firstStreamAtMs - entry.receivedAtMs
|
|
169
|
+
: null,
|
|
170
|
+
},
|
|
171
|
+
{ sessionKey: entry.sessionKey, runId },
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function complete(requestId, finalFrame, result = {}) {
|
|
176
|
+
const entry = entriesByRequestId.get(requestId);
|
|
177
|
+
if (!entry) return;
|
|
178
|
+
const completedAtMs = now();
|
|
179
|
+
entry.completedAtMs = completedAtMs;
|
|
180
|
+
entry.finalFrame = finalFrame;
|
|
181
|
+
options.emitDebug(
|
|
182
|
+
"operation_completed",
|
|
183
|
+
"info",
|
|
184
|
+
{
|
|
185
|
+
requestId,
|
|
186
|
+
operation: entry.operation,
|
|
187
|
+
class: entry.class,
|
|
188
|
+
elapsedMs: completedAtMs - entry.receivedAtMs,
|
|
189
|
+
resultSource: "typed_final_frame",
|
|
190
|
+
...result,
|
|
191
|
+
},
|
|
192
|
+
{ sessionKey: entry.sessionKey },
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function fail(requestId, finalFrame, result = {}) {
|
|
197
|
+
const entry = entriesByRequestId.get(requestId);
|
|
198
|
+
if (!entry) return;
|
|
199
|
+
const failedAtMs = now();
|
|
200
|
+
entry.completedAtMs = failedAtMs;
|
|
201
|
+
entry.finalFrame = finalFrame;
|
|
202
|
+
options.emitDebug(
|
|
203
|
+
"operation_failed",
|
|
204
|
+
"warn",
|
|
205
|
+
{
|
|
206
|
+
requestId,
|
|
207
|
+
operation: entry.operation,
|
|
208
|
+
class: entry.class,
|
|
209
|
+
elapsedMs: failedAtMs - entry.receivedAtMs,
|
|
210
|
+
...result,
|
|
211
|
+
},
|
|
212
|
+
{ sessionKey: entry.sessionKey },
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function queueDepthSnapshot() {
|
|
217
|
+
prune();
|
|
218
|
+
let transactional = 0;
|
|
219
|
+
for (const entry of entriesByRequestId.values()) {
|
|
220
|
+
if (!entry.completedAtMs && entry.class === "transactional") transactional += 1;
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
transactional,
|
|
224
|
+
latestMutation: 0,
|
|
225
|
+
coalescableRead: 0,
|
|
226
|
+
bestEffort: 0,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function reconcileRequestIds(requestIds) {
|
|
231
|
+
prune();
|
|
232
|
+
const ids = Array.isArray(requestIds) ? requestIds : [];
|
|
233
|
+
return ids
|
|
234
|
+
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
|
235
|
+
.filter(Boolean)
|
|
236
|
+
.map((requestId) => {
|
|
237
|
+
const entry = entriesByRequestId.get(requestId);
|
|
238
|
+
if (!entry) {
|
|
239
|
+
return {
|
|
240
|
+
requestId,
|
|
241
|
+
known: false,
|
|
242
|
+
receiptFrame: null,
|
|
243
|
+
finalFrame: null,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
requestId,
|
|
248
|
+
known: true,
|
|
249
|
+
receiptFrame: receiptFrame(entry),
|
|
250
|
+
finalFrame: entry.finalFrame || null,
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
beginMessageSend,
|
|
257
|
+
markStarted,
|
|
258
|
+
markUpstreamAck,
|
|
259
|
+
markRunPhase,
|
|
260
|
+
queueDepthSnapshot,
|
|
261
|
+
reconcileRequestIds,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
@@ -33,10 +33,37 @@ function resolveOpenclawClient(openclawClientOverride, runtimeConfig, logger, st
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Shared cross-context relay singleton. OpenClaw can load the plugin in
|
|
37
|
+
// multiple isolated registration contexts (gateway startup, agent runs,
|
|
38
|
+
// tool discovery) and each call to createOcuClawRelayService produces a
|
|
39
|
+
// fresh service instance. Only one of those instances actually starts the
|
|
40
|
+
// WebSocket-bearing relay; the others need to reach the same relay so that
|
|
41
|
+
// tools registered in those contexts (e.g. render_glasses_ui) can call
|
|
42
|
+
// sendGlassesUiRender / onGlassesUiResult against the real running relay.
|
|
43
|
+
const SHARED_RELAY_SYMBOL = Symbol.for("ocuclaw.shared.relay");
|
|
44
|
+
|
|
45
|
+
function getSharedRelay() {
|
|
46
|
+
return globalThis[SHARED_RELAY_SYMBOL] || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function setSharedRelay(relay) {
|
|
50
|
+
globalThis[SHARED_RELAY_SYMBOL] = relay;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function clearSharedRelay(relay) {
|
|
54
|
+
if (globalThis[SHARED_RELAY_SYMBOL] === relay) {
|
|
55
|
+
globalThis[SHARED_RELAY_SYMBOL] = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
36
59
|
export function createOcuClawRelayService(opts = {}) {
|
|
37
60
|
const baseLogger = normalizeLogger(opts.logger);
|
|
38
61
|
let relay = null;
|
|
39
62
|
let runtimeConfig = null;
|
|
63
|
+
const pendingGlassesUiResultHandlers = [];
|
|
64
|
+
const pendingGlassesUiNavEventHandlers = [];
|
|
65
|
+
const pendingDeviceInfoResponseHandlers = [];
|
|
66
|
+
const pendingAppClientDisconnectHandlers = [];
|
|
40
67
|
|
|
41
68
|
function getRuntimeConfig() {
|
|
42
69
|
if (!runtimeConfig) {
|
|
@@ -94,9 +121,38 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
94
121
|
openclawClient,
|
|
95
122
|
logger,
|
|
96
123
|
consoleLogPath: opts.consoleLogPath,
|
|
124
|
+
activityStatusAdapter: {
|
|
125
|
+
freshnessWindowMs: config.freshnessWindowMs,
|
|
126
|
+
now: () => Date.now(),
|
|
127
|
+
},
|
|
97
128
|
});
|
|
98
129
|
|
|
99
130
|
relay = nextRelay;
|
|
131
|
+
setSharedRelay(nextRelay);
|
|
132
|
+
if (typeof nextRelay.onGlassesUiResult === "function" && pendingGlassesUiResultHandlers.length > 0) {
|
|
133
|
+
for (const handler of pendingGlassesUiResultHandlers) {
|
|
134
|
+
nextRelay.onGlassesUiResult(handler);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (typeof nextRelay.onGlassesUiNavEvent === "function" && pendingGlassesUiNavEventHandlers.length > 0) {
|
|
138
|
+
for (const handler of pendingGlassesUiNavEventHandlers.splice(0)) {
|
|
139
|
+
nextRelay.onGlassesUiNavEvent(handler);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (typeof nextRelay.onDeviceInfoResponse === "function" && pendingDeviceInfoResponseHandlers.length > 0) {
|
|
143
|
+
// splice(0) atomically drains the buffer so the unsubscribe returned to
|
|
144
|
+
// pre-start() callers becomes a safe no-op (it indexOf-s an empty list).
|
|
145
|
+
// After flush, handler lifetime is owned by the live relay's own
|
|
146
|
+
// unsubscribe; the pending buffer is no longer the source of truth.
|
|
147
|
+
for (const handler of pendingDeviceInfoResponseHandlers.splice(0)) {
|
|
148
|
+
nextRelay.onDeviceInfoResponse(handler);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (typeof nextRelay.onAppClientDisconnect === "function" && pendingAppClientDisconnectHandlers.length > 0) {
|
|
152
|
+
for (const handler of pendingAppClientDisconnectHandlers.splice(0)) {
|
|
153
|
+
nextRelay.onAppClientDisconnect(handler);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
100
156
|
try {
|
|
101
157
|
await Promise.resolve(nextRelay.start());
|
|
102
158
|
logger.info(
|
|
@@ -104,6 +160,7 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
104
160
|
);
|
|
105
161
|
return nextRelay;
|
|
106
162
|
} catch (err) {
|
|
163
|
+
clearSharedRelay(nextRelay);
|
|
107
164
|
relay = null;
|
|
108
165
|
throw err;
|
|
109
166
|
}
|
|
@@ -117,14 +174,108 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
117
174
|
const logger = normalizeLogger(stopOpts.logger || baseLogger);
|
|
118
175
|
const activeRelay = relay;
|
|
119
176
|
relay = null;
|
|
177
|
+
clearSharedRelay(activeRelay);
|
|
120
178
|
await Promise.resolve(activeRelay.stop());
|
|
121
179
|
logger.info("[ocuclaw] relay service stopped");
|
|
122
180
|
}
|
|
123
181
|
|
|
182
|
+
function resolveLiveRelay() {
|
|
183
|
+
// Prefer the local-instance relay (started in this context); fall back to
|
|
184
|
+
// the cross-context shared relay (started in a sibling plugin-load context
|
|
185
|
+
// such as the gateway main process). This matters because OpenClaw can
|
|
186
|
+
// invoke a plugin's register() in multiple isolated contexts, but only
|
|
187
|
+
// one of them holds the running WebSocket relay.
|
|
188
|
+
return relay || getSharedRelay();
|
|
189
|
+
}
|
|
124
190
|
return {
|
|
125
191
|
getRuntimeConfig,
|
|
126
192
|
getRelay() {
|
|
127
|
-
return
|
|
193
|
+
return resolveLiveRelay();
|
|
194
|
+
},
|
|
195
|
+
sendGlassesUiRender(params) {
|
|
196
|
+
const liveRelay = resolveLiveRelay();
|
|
197
|
+
if (!liveRelay || typeof liveRelay.sendGlassesUiRender !== "function") {
|
|
198
|
+
throw new Error("ocuclaw relay not started");
|
|
199
|
+
}
|
|
200
|
+
liveRelay.sendGlassesUiRender(params);
|
|
201
|
+
},
|
|
202
|
+
sendGlassesUiSurfaceUpdate(params) {
|
|
203
|
+
const liveRelay = resolveLiveRelay();
|
|
204
|
+
if (!liveRelay || typeof liveRelay.sendGlassesUiSurfaceUpdate !== "function") {
|
|
205
|
+
throw new Error("ocuclaw relay not started");
|
|
206
|
+
}
|
|
207
|
+
liveRelay.sendGlassesUiSurfaceUpdate(params);
|
|
208
|
+
},
|
|
209
|
+
// Permanent glasses.lifecycle debug category passthrough (nav reconcile +
|
|
210
|
+
// cron pause/resume/tick). No-op until the relay is live; events are only
|
|
211
|
+
// recorded when the category is enabled via debug-set.
|
|
212
|
+
emitGlassesUiLifecycle(event, severity, data) {
|
|
213
|
+
const liveRelay = resolveLiveRelay();
|
|
214
|
+
if (liveRelay && typeof liveRelay.emitGlassesUiLifecycle === "function") {
|
|
215
|
+
liveRelay.emitGlassesUiLifecycle(event, severity, data);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
onGlassesUiResult(handler) {
|
|
219
|
+
if (typeof handler !== "function") {
|
|
220
|
+
return () => {};
|
|
221
|
+
}
|
|
222
|
+
const liveRelay = resolveLiveRelay();
|
|
223
|
+
if (liveRelay && typeof liveRelay.onGlassesUiResult === "function") {
|
|
224
|
+
return liveRelay.onGlassesUiResult(handler);
|
|
225
|
+
}
|
|
226
|
+
// Relay not started yet — buffer until start() can register the handler.
|
|
227
|
+
pendingGlassesUiResultHandlers.push(handler);
|
|
228
|
+
return () => {
|
|
229
|
+
const idx = pendingGlassesUiResultHandlers.indexOf(handler);
|
|
230
|
+
if (idx !== -1) pendingGlassesUiResultHandlers.splice(idx, 1);
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
onGlassesUiNavEvent(handler) {
|
|
234
|
+
if (typeof handler !== "function") return () => {};
|
|
235
|
+
const liveRelay = resolveLiveRelay();
|
|
236
|
+
if (liveRelay && typeof liveRelay.onGlassesUiNavEvent === "function") {
|
|
237
|
+
return liveRelay.onGlassesUiNavEvent(handler);
|
|
238
|
+
}
|
|
239
|
+
pendingGlassesUiNavEventHandlers.push(handler);
|
|
240
|
+
return () => {
|
|
241
|
+
const idx = pendingGlassesUiNavEventHandlers.indexOf(handler);
|
|
242
|
+
if (idx !== -1) pendingGlassesUiNavEventHandlers.splice(idx, 1);
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
onAppClientDisconnect(handler) {
|
|
246
|
+
if (typeof handler !== "function") return () => {};
|
|
247
|
+
const liveRelay = resolveLiveRelay();
|
|
248
|
+
if (liveRelay && typeof liveRelay.onAppClientDisconnect === "function") {
|
|
249
|
+
return liveRelay.onAppClientDisconnect(handler);
|
|
250
|
+
}
|
|
251
|
+
// Relay not started yet — buffer until start() can register the handler.
|
|
252
|
+
pendingAppClientDisconnectHandlers.push(handler);
|
|
253
|
+
return () => {
|
|
254
|
+
const idx = pendingAppClientDisconnectHandlers.indexOf(handler);
|
|
255
|
+
if (idx !== -1) pendingAppClientDisconnectHandlers.splice(idx, 1);
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
sendDeviceInfoRequest(params) {
|
|
259
|
+
const liveRelay = resolveLiveRelay();
|
|
260
|
+
if (!liveRelay || typeof liveRelay.sendDeviceInfoRequest !== "function") {
|
|
261
|
+
throw new Error("ocuclaw relay not started");
|
|
262
|
+
}
|
|
263
|
+
liveRelay.sendDeviceInfoRequest(params);
|
|
264
|
+
},
|
|
265
|
+
onDeviceInfoResponse(handler) {
|
|
266
|
+
if (typeof handler !== "function") {
|
|
267
|
+
return () => {};
|
|
268
|
+
}
|
|
269
|
+
const liveRelay = resolveLiveRelay();
|
|
270
|
+
if (liveRelay && typeof liveRelay.onDeviceInfoResponse === "function") {
|
|
271
|
+
return liveRelay.onDeviceInfoResponse(handler);
|
|
272
|
+
}
|
|
273
|
+
// Relay not started yet — buffer until start() can register the handler.
|
|
274
|
+
pendingDeviceInfoResponseHandlers.push(handler);
|
|
275
|
+
return () => {
|
|
276
|
+
const idx = pendingDeviceInfoResponseHandlers.indexOf(handler);
|
|
277
|
+
if (idx !== -1) pendingDeviceInfoResponseHandlers.splice(idx, 1);
|
|
278
|
+
};
|
|
128
279
|
},
|
|
129
280
|
getEvenAiSettingsSnapshot() {
|
|
130
281
|
if (relay && typeof relay.getEvenAiSettingsSnapshot === "function") {
|
|
@@ -140,6 +291,55 @@ export function createOcuClawRelayService(opts = {}) {
|
|
|
140
291
|
trackedThrowawayKeys: [],
|
|
141
292
|
};
|
|
142
293
|
},
|
|
294
|
+
getSessionTitle(sessionKey) {
|
|
295
|
+
if (relay && typeof relay.getSessionTitle === "function") {
|
|
296
|
+
return relay.getSessionTitle(sessionKey);
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
},
|
|
300
|
+
hasRecordedUserMessage(sessionKey) {
|
|
301
|
+
if (relay && typeof relay.hasRecordedUserMessage === "function") {
|
|
302
|
+
return relay.hasRecordedUserMessage(sessionKey);
|
|
303
|
+
}
|
|
304
|
+
// Fail-closed when the relay isn't running: block titling.
|
|
305
|
+
return false;
|
|
306
|
+
},
|
|
307
|
+
isNeuralSessionNamesEnabled(sessionKey) {
|
|
308
|
+
if (relay && typeof relay.isNeuralSessionNamesEnabled === "function") {
|
|
309
|
+
return relay.isNeuralSessionNamesEnabled(sessionKey);
|
|
310
|
+
}
|
|
311
|
+
return true;
|
|
312
|
+
},
|
|
313
|
+
isSessionUserLocked(sessionKey) {
|
|
314
|
+
if (relay && typeof relay.isSessionUserLocked === "function") {
|
|
315
|
+
return relay.isSessionUserLocked(sessionKey);
|
|
316
|
+
}
|
|
317
|
+
return false;
|
|
318
|
+
},
|
|
319
|
+
peekSessionKey() {
|
|
320
|
+
if (relay && typeof relay.peekSessionKey === "function") {
|
|
321
|
+
return relay.peekSessionKey();
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
},
|
|
325
|
+
recordNeuralSessionNamesEnabled(sessionKey, enabled) {
|
|
326
|
+
if (relay && typeof relay.recordNeuralSessionNamesEnabled === "function") {
|
|
327
|
+
relay.recordNeuralSessionNamesEnabled(sessionKey, enabled);
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
setSessionTitle(sessionKey, title, opts) {
|
|
331
|
+
if (relay && typeof relay.setSessionTitle === "function") {
|
|
332
|
+
return relay.setSessionTitle(sessionKey, title, opts);
|
|
333
|
+
}
|
|
334
|
+
return { ok: false, code: "relay_not_running" };
|
|
335
|
+
},
|
|
336
|
+
hasConnectedAppClient() {
|
|
337
|
+
const liveRelay = resolveLiveRelay();
|
|
338
|
+
if (liveRelay && typeof liveRelay.hasConnectedAppClient === "function") {
|
|
339
|
+
return liveRelay.hasConnectedAppClient();
|
|
340
|
+
}
|
|
341
|
+
return false;
|
|
342
|
+
},
|
|
143
343
|
start,
|
|
144
344
|
stop,
|
|
145
345
|
};
|