ocuclaw 1.3.1 → 1.3.3
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/dist/config/runtime-config-session-title-model.test.js +22 -0
- package/dist/config/runtime-config.js +7 -1
- package/dist/domain/glasses-display-system-prompt.js +52 -0
- package/dist/domain/glasses-display-system-prompt.test.js +44 -0
- package/dist/domain/glasses-ui-system-prompt.js +6 -22
- package/dist/domain/glasses-ui-system-prompt.test.js +13 -0
- package/dist/domain/prompt-channel-fragments.js +32 -0
- package/dist/domain/prompt-channel-fragments.test.js +70 -0
- package/dist/gateway/gateway-timing-ledger.js +15 -3
- package/dist/gateway/openclaw-client.js +80 -3
- package/dist/index.js +22 -0
- package/dist/runtime/channel-two-hook.js +36 -0
- package/dist/runtime/container-env.js +41 -0
- package/dist/runtime/display-toggle-states.js +98 -0
- package/dist/runtime/glasses-backpressure-latch.js +115 -0
- package/dist/runtime/register-session-title-distiller.js +100 -0
- package/dist/runtime/relay-core.js +284 -33
- package/dist/runtime/relay-service.js +152 -13
- package/dist/runtime/relay-worker-entry.js +26 -0
- package/dist/runtime/relay-worker-supervisor.js +51 -2
- package/dist/runtime/relay-worker-transport.js +51 -1
- package/dist/runtime/session-service.js +136 -12
- package/dist/runtime/session-title-distiller-budget.js +36 -0
- package/dist/runtime/session-title-distiller-helpers.js +130 -0
- package/dist/runtime/session-title-distiller.js +354 -0
- package/dist/runtime/session-title-record.js +21 -0
- package/dist/runtime/stable-prompt-snapshot.js +119 -0
- package/dist/tools/glasses-ui-cron.js +59 -3
- package/dist/tools/glasses-ui-paint-floor.js +33 -4
- package/dist/tools/glasses-ui-surfaces.js +369 -35
- package/dist/tools/glasses-ui-tool-description.test.js +16 -0
- package/dist/tools/glasses-ui-tool.js +662 -80
- package/dist/tools/glasses-ui-voicemail.js +299 -0
- package/dist/tools/glasses-ui-wake.js +262 -0
- package/dist/tools/session-title-tool.js +14 -76
- package/dist/tools/session-title-tool.test.js +53 -0
- package/dist/version.js +2 -2
- package/openclaw.plugin.json +9 -0
- package/package.json +4 -3
- package/skills/glasses-ui/SKILL.md +26 -3
|
@@ -9,6 +9,8 @@ import { composeReadabilitySystemPrompt } from "../domain/readability-system-pro
|
|
|
9
9
|
import { composeNeuralEmojiReactorSystemPrompt } from "../domain/neural-emoji-reactor-system-prompt.js";
|
|
10
10
|
import { composeNeuralPaceModulatorSystemPrompt } from "../domain/neural-pace-modulator-system-prompt.js";
|
|
11
11
|
import { composeGlassesUiNudgeSystemPrompt } from "../domain/glasses-ui-system-prompt.js";
|
|
12
|
+
import { composeGlassesDisplaySystemPrompt } from "../domain/glasses-display-system-prompt.js";
|
|
13
|
+
import { createStablePromptSnapshotStore } from "./stable-prompt-snapshot.js";
|
|
12
14
|
import { createActivityStatusAdapter } from "../domain/activity-status-adapter.js";
|
|
13
15
|
import { createEvenAiEndpoint } from "../even-ai/even-ai-endpoint.js";
|
|
14
16
|
import { createEvenAiRouter } from "../even-ai/even-ai-router.js";
|
|
@@ -16,9 +18,11 @@ import { createEvenAiRunWaiter } from "../even-ai/even-ai-run-waiter.js";
|
|
|
16
18
|
import { createEvenAiSettingsStore } from "../even-ai/even-ai-settings-store.js";
|
|
17
19
|
import { createPluginOpenclawClient } from "../gateway/openclaw-client.js";
|
|
18
20
|
import { createPluginRpcGatewayBridge } from "../gateway/gateway-bridge.js";
|
|
21
|
+
import { createAgentTurnTracker } from "../tools/glasses-ui-wake.js";
|
|
19
22
|
import { createDownstreamHandler } from "./downstream-handler.js";
|
|
20
23
|
import { createOcuClawSettingsStore } from "./ocuclaw-settings-store.js";
|
|
21
24
|
import { createRelayHealthMonitor } from "./relay-health-monitor.js";
|
|
25
|
+
import { createGlassesBackpressureLatch } from "./glasses-backpressure-latch.js";
|
|
22
26
|
import { createRelayOperationRegistry } from "./relay-operation-registry.js";
|
|
23
27
|
import { createRelayWorkerSupervisor } from "./relay-worker-supervisor.js";
|
|
24
28
|
import {
|
|
@@ -27,6 +31,9 @@ import {
|
|
|
27
31
|
} from "./session-service.js";
|
|
28
32
|
import { createUpstreamRuntime } from "./upstream-runtime.js";
|
|
29
33
|
|
|
34
|
+
const GLASSES_UI_MARKERS = new Set(["listening", "parked", "inflight"]);
|
|
35
|
+
export function sanitizeGlassesMarker(v) { return GLASSES_UI_MARKERS.has(v) ? v : undefined; }
|
|
36
|
+
|
|
30
37
|
const SONIOX_TEMP_KEY_URL = "https://api.soniox.com/v1/auth/temporary-api-key";
|
|
31
38
|
const SONIOX_MODELS_URL = "https://api.soniox.com/v1/models";
|
|
32
39
|
const DEFAULT_SONIOX_TEMP_KEY_EXPIRES_IN_SECONDS = 3600;
|
|
@@ -340,6 +347,12 @@ function createRelay(opts) {
|
|
|
340
347
|
const activityStatusAdapter = createActivityStatusAdapter(
|
|
341
348
|
opts.activityStatusAdapter,
|
|
342
349
|
);
|
|
350
|
+
// Per-session "agent turn in flight or imminent" signal (roadmap 6f):
|
|
351
|
+
// marked busy on every dispatched send (voice/user/wake), refreshed by the
|
|
352
|
+
// gateway activity stream, idled on end-phase activity, decay-bounded
|
|
353
|
+
// (fail open). The glasses-ui wake controller consults it so a wake never
|
|
354
|
+
// races a genuine turn (voice absorbs wake, §2.6c).
|
|
355
|
+
const agentTurnTracker = createAgentTurnTracker();
|
|
343
356
|
const sharedHttpServer = opts.httpServer || null;
|
|
344
357
|
|
|
345
358
|
// --- Cached state ---
|
|
@@ -359,7 +372,9 @@ function createRelay(opts) {
|
|
|
359
372
|
/** Relay-local deterministic simulate-stream run sequence counter. */
|
|
360
373
|
let simulateStreamRunSeq = 0;
|
|
361
374
|
/** Active timers for relay-local deterministic simulate-stream runs. */
|
|
362
|
-
|
|
375
|
+
// timer -> sessionKey, so new-chat//reset/new-session can cancel ONLY the
|
|
376
|
+
// affected session's pending injections (re-land of a8a29032, session-scoped).
|
|
377
|
+
const simulateStreamTimers = new Map();
|
|
363
378
|
|
|
364
379
|
// --- Structured debug state ---
|
|
365
380
|
|
|
@@ -550,7 +565,7 @@ function createRelay(opts) {
|
|
|
550
565
|
);
|
|
551
566
|
}
|
|
552
567
|
|
|
553
|
-
function scheduleSimulateStreamTimer(delayMs, callback) {
|
|
568
|
+
function scheduleSimulateStreamTimer(delayMs, callback, sessionKey) {
|
|
554
569
|
const timer = setTimeout(() => {
|
|
555
570
|
simulateStreamTimers.delete(timer);
|
|
556
571
|
try {
|
|
@@ -559,12 +574,29 @@ function createRelay(opts) {
|
|
|
559
574
|
logger.error(`[relay] simulate-stream timer failed: ${err.message}`);
|
|
560
575
|
}
|
|
561
576
|
}, delayMs);
|
|
562
|
-
simulateStreamTimers.
|
|
577
|
+
simulateStreamTimers.set(timer, typeof sessionKey === "string" ? sessionKey : null);
|
|
563
578
|
return timer;
|
|
564
579
|
}
|
|
565
580
|
|
|
581
|
+
function clearSimulateStreamTimersForSession(sessionKey) {
|
|
582
|
+
let cleared = 0;
|
|
583
|
+
for (const [timer, timerSessionKey] of simulateStreamTimers) {
|
|
584
|
+
if (timerSessionKey === sessionKey) {
|
|
585
|
+
clearTimeout(timer);
|
|
586
|
+
simulateStreamTimers.delete(timer);
|
|
587
|
+
cleared += 1;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (cleared > 0) {
|
|
591
|
+
logger.info(
|
|
592
|
+
`[relay] cancelled ${cleared} pending simulate-stream timer(s) for session ${sessionKey}`,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
return cleared;
|
|
596
|
+
}
|
|
597
|
+
|
|
566
598
|
function clearSimulateStreamTimers() {
|
|
567
|
-
for (const timer of simulateStreamTimers) {
|
|
599
|
+
for (const timer of simulateStreamTimers.keys()) {
|
|
568
600
|
clearTimeout(timer);
|
|
569
601
|
}
|
|
570
602
|
simulateStreamTimers.clear();
|
|
@@ -923,37 +955,67 @@ function createRelay(opts) {
|
|
|
923
955
|
systemPrompt: opts.ocuClawSystemPrompt,
|
|
924
956
|
},
|
|
925
957
|
});
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
958
|
+
const stablePromptSnapshots = createStablePromptSnapshotStore({
|
|
959
|
+
stateDir: opts.stateDir,
|
|
960
|
+
emitDebug,
|
|
961
|
+
});
|
|
962
|
+
// Hourly TTL sweep for stale stable-prompt snapshots; started in start(),
|
|
963
|
+
// cleared in stop(). Declared here so both can see it.
|
|
964
|
+
let stablePromptSweepTimer = null;
|
|
965
|
+
|
|
966
|
+
// Channel 1: the per-session-immutable extraSystemPrompt. Computed once from
|
|
967
|
+
// the feature set AT SESSION START and then served byte-identical for the
|
|
968
|
+
// session's lifetime (see stable-prompt-snapshot). Mid-session toggles are
|
|
969
|
+
// bridged by the Channel-2 hook composer, NOT here. The glasses-UI pointer is
|
|
970
|
+
// always present (its disconnected gate moved to Channel 2); the emoji/pace
|
|
971
|
+
// blocks are included only when active at session start.
|
|
972
|
+
function computeStableChannelOne(startSignals) {
|
|
929
973
|
const baseReadability = composeReadabilitySystemPrompt(
|
|
930
974
|
ocuClawSettingsStore.getSnapshot().systemPrompt,
|
|
931
975
|
);
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
const
|
|
937
|
-
const paceState = validState(signals.neuralPaceModulatorState);
|
|
938
|
-
const reactor = composeNeuralEmojiReactorSystemPrompt({ state: reactorState });
|
|
939
|
-
const pace = composeNeuralPaceModulatorSystemPrompt({ state: paceState });
|
|
940
|
-
// Only include the glasses-UI nudge when a downstream app client is
|
|
941
|
-
// connected. Keeps the prompt clean when the agent has nowhere to render
|
|
942
|
-
// the tool's output, and keeps existing prompt-assembly tests stable
|
|
943
|
-
// (they exercise the prompt without spinning up an app client).
|
|
944
|
-
const hasAppClient =
|
|
945
|
-
server &&
|
|
946
|
-
typeof server.getConnectedAppCount === "function" &&
|
|
947
|
-
server.getConnectedAppCount() > 0;
|
|
948
|
-
const glassesUiNudge = hasAppClient ? composeGlassesUiNudgeSystemPrompt() : "";
|
|
976
|
+
const display = composeGlassesDisplaySystemPrompt({
|
|
977
|
+
emoji: startSignals.emoji,
|
|
978
|
+
pace: startSignals.pace,
|
|
979
|
+
});
|
|
980
|
+
const glassesPointer = composeGlassesUiNudgeSystemPrompt();
|
|
949
981
|
const parts = [];
|
|
950
982
|
if (baseReadability) parts.push(baseReadability);
|
|
951
|
-
if (
|
|
952
|
-
if (
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
983
|
+
if (display) parts.push(display);
|
|
984
|
+
if (glassesPointer) parts.push(glassesPointer);
|
|
985
|
+
return parts.join("\n\n");
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function stableSendOptions(resolvedSessionKey, sessionId, perTurnSignals) {
|
|
989
|
+
const signals = perTurnSignals || {};
|
|
990
|
+
// "enabled at start" = the client's reported state on the FIRST send of the
|
|
991
|
+
// session. The 3-state client signal maps to a boolean: only "active" counts.
|
|
992
|
+
const startEmoji = signals.neuralEmojiReactorState === "active";
|
|
993
|
+
const startPace = signals.neuralPaceModulatorState === "active";
|
|
994
|
+
const extraSystemPrompt = stablePromptSnapshots.getOrCreate(
|
|
995
|
+
resolvedSessionKey,
|
|
996
|
+
sessionId,
|
|
997
|
+
() => computeStableChannelOne({ emoji: startEmoji, pace: startPace }),
|
|
998
|
+
);
|
|
999
|
+
// Churn guard: if recomputing TODAY would differ from the served snapshot,
|
|
1000
|
+
// something is mutating Channel 1 mid-session (e.g. a toggle flipped the
|
|
1001
|
+
// start-state signals) — the case that used to reset the CLI session.
|
|
1002
|
+
// Surface it loudly; the served prompt stays the frozen snapshot.
|
|
1003
|
+
if (
|
|
1004
|
+
stablePromptSnapshots.wouldChurn(
|
|
1005
|
+
resolvedSessionKey,
|
|
1006
|
+
sessionId,
|
|
1007
|
+
computeStableChannelOne({ emoji: startEmoji, pace: startPace }),
|
|
1008
|
+
)
|
|
1009
|
+
) {
|
|
1010
|
+
emitDebug(
|
|
1011
|
+
"relay.session",
|
|
1012
|
+
"stable_prompt_churn_detected",
|
|
1013
|
+
"warn",
|
|
1014
|
+
{ sessionKey: resolvedSessionKey },
|
|
1015
|
+
() => ({ sessionId: sessionId || null }),
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
return { extraSystemPrompt };
|
|
957
1019
|
}
|
|
958
1020
|
|
|
959
1021
|
function buildOcuClawSendDiagnostic(params = {}) {
|
|
@@ -1211,6 +1273,10 @@ function createRelay(opts) {
|
|
|
1211
1273
|
const runId = activity && activity.runId ? activity.runId : null;
|
|
1212
1274
|
const origin = activity && activity.origin ? activity.origin : null;
|
|
1213
1275
|
const phase = activity && activity.phase ? activity.phase : null;
|
|
1276
|
+
agentTurnTracker.onActivity(
|
|
1277
|
+
(activity && activity.sessionKey) || sessionService.ensureSessionKey(),
|
|
1278
|
+
phase,
|
|
1279
|
+
);
|
|
1214
1280
|
|
|
1215
1281
|
emitDebug(
|
|
1216
1282
|
"app.timeline",
|
|
@@ -1297,6 +1363,7 @@ function createRelay(opts) {
|
|
|
1297
1363
|
surfaceId: params && typeof params.surfaceId === "string" ? params.surfaceId : "",
|
|
1298
1364
|
depth: Number.isFinite(params && params.depth) ? Math.floor(params.depth) : 1,
|
|
1299
1365
|
spec: params && params.spec ? params.spec : null,
|
|
1366
|
+
marker: sanitizeGlassesMarker(params && params.marker),
|
|
1300
1367
|
};
|
|
1301
1368
|
server.broadcast(JSON.stringify(payload));
|
|
1302
1369
|
emitDebug(
|
|
@@ -1331,6 +1398,7 @@ function createRelay(opts) {
|
|
|
1331
1398
|
})
|
|
1332
1399
|
.filter((i) => i !== null);
|
|
1333
1400
|
}
|
|
1401
|
+
const m = sanitizeGlassesMarker(patch.marker); if (m) cleanPatch.marker = m;
|
|
1334
1402
|
const payload = {
|
|
1335
1403
|
type: "glasses_ui_surface_update",
|
|
1336
1404
|
sessionKey: params && typeof params.sessionKey === "string" ? params.sessionKey : null,
|
|
@@ -1465,6 +1533,10 @@ function createRelay(opts) {
|
|
|
1465
1533
|
resolvedSessionKey,
|
|
1466
1534
|
clientDisplaySignals.neuralSessionNamesEnabled !== false,
|
|
1467
1535
|
);
|
|
1536
|
+
sessionService.recordDisplayToggleStates(resolvedSessionKey, {
|
|
1537
|
+
emoji: clientDisplaySignals.neuralEmojiReactorState === "active",
|
|
1538
|
+
pace: clientDisplaySignals.neuralPaceModulatorState === "active",
|
|
1539
|
+
});
|
|
1468
1540
|
}
|
|
1469
1541
|
const hasAttachment = !!attachment;
|
|
1470
1542
|
const sendStartedAt = Date.now();
|
|
@@ -1487,6 +1559,9 @@ function createRelay(opts) {
|
|
|
1487
1559
|
);
|
|
1488
1560
|
|
|
1489
1561
|
return maybeSeedOcuClawSessionConfig(resolvedSessionKey).then(() => {
|
|
1562
|
+
// A genuine user/voice send is starting an agent turn — mark the
|
|
1563
|
+
// session busy so a racing glasses wake is absorbed (§2.6c).
|
|
1564
|
+
agentTurnTracker.markBusy(resolvedSessionKey);
|
|
1490
1565
|
// Dispatch upstream first so local transcript work cannot delay first
|
|
1491
1566
|
// model tokens on large histories.
|
|
1492
1567
|
const upstreamPromise = gatewayBridge.sendMessage(
|
|
@@ -1494,7 +1569,16 @@ function createRelay(opts) {
|
|
|
1494
1569
|
resolvedSessionKey,
|
|
1495
1570
|
attachment,
|
|
1496
1571
|
{
|
|
1497
|
-
...
|
|
1572
|
+
...stableSendOptions(
|
|
1573
|
+
resolvedSessionKey,
|
|
1574
|
+
// No synchronous OpenClaw sessionId is available at send time
|
|
1575
|
+
// (resolveSessionCanonicalKey is async). Use the sessionKey as the
|
|
1576
|
+
// snapshot's id; the sessionId-mismatch guard is therefore a no-op,
|
|
1577
|
+
// and new-session safety rests on logical-session-end eviction
|
|
1578
|
+
// (onNewSession / onNewChat / onDeleteSessions evict the snapshot).
|
|
1579
|
+
resolvedSessionKey,
|
|
1580
|
+
clientDisplaySignals,
|
|
1581
|
+
),
|
|
1498
1582
|
diagnostic: buildOcuClawSendDiagnostic({
|
|
1499
1583
|
...params,
|
|
1500
1584
|
sessionKey: resolvedSessionKey,
|
|
@@ -1768,6 +1852,14 @@ function createRelay(opts) {
|
|
|
1768
1852
|
return upstreamRuntime.compactActiveSession(sessionKey);
|
|
1769
1853
|
},
|
|
1770
1854
|
onDeleteSessions(sessionKeys, kind, switchBeforeDelete) {
|
|
1855
|
+
if (Array.isArray(sessionKeys)) {
|
|
1856
|
+
for (const key of sessionKeys) {
|
|
1857
|
+
if (typeof key === "string" && key.trim()) {
|
|
1858
|
+
stablePromptSnapshots.evict(key);
|
|
1859
|
+
sessionService.clearDisplayToggleStates(key);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1771
1863
|
const action = switchBeforeDelete
|
|
1772
1864
|
? sessionService.switchAndDeleteSessions(kind, sessionKeys)
|
|
1773
1865
|
: sessionService.deleteSessions(kind, sessionKeys);
|
|
@@ -1904,7 +1996,7 @@ function createRelay(opts) {
|
|
|
1904
1996
|
totalChars: text.length,
|
|
1905
1997
|
}),
|
|
1906
1998
|
);
|
|
1907
|
-
});
|
|
1999
|
+
}, sessionKey);
|
|
1908
2000
|
}
|
|
1909
2001
|
|
|
1910
2002
|
const completeDelayMs = startDelayMs + (chunkCount * chunkIntervalMs) + thinkingTailMs;
|
|
@@ -1935,7 +2027,7 @@ function createRelay(opts) {
|
|
|
1935
2027
|
completeDelayMs,
|
|
1936
2028
|
}),
|
|
1937
2029
|
);
|
|
1938
|
-
});
|
|
2030
|
+
}, sessionKey);
|
|
1939
2031
|
|
|
1940
2032
|
return Promise.resolve({
|
|
1941
2033
|
status: "accepted",
|
|
@@ -1962,7 +2054,18 @@ function createRelay(opts) {
|
|
|
1962
2054
|
}
|
|
1963
2055
|
sessionService.invalidateSessionsCache();
|
|
1964
2056
|
resetActivityStatusAdapter();
|
|
2057
|
+
// Cancel THIS session's pending simulate-stream timers BEFORE clearing —
|
|
2058
|
+
// a deferred addMessage firing after the clear repopulates the fresh chat
|
|
2059
|
+
// (the 2026-05-15 canary-pollution mechanism).
|
|
2060
|
+
clearSimulateStreamTimersForSession(sessionService.ensureSessionKey());
|
|
1965
2061
|
conversationState.clear();
|
|
2062
|
+
// Logical session end: this key is REUSED for a fresh conversation, so the
|
|
2063
|
+
// Channel-1 snapshot must be dropped or the next send serves a stale prompt,
|
|
2064
|
+
// and ALL other per-session state keyed to it (title + upstream name,
|
|
2065
|
+
// toggles, distiller budget, first-user marker) must be cleared too.
|
|
2066
|
+
const newChatSessionKey = sessionService.ensureSessionKey();
|
|
2067
|
+
stablePromptSnapshots.evict(newChatSessionKey);
|
|
2068
|
+
sessionService.clearLogicalSessionState(newChatSessionKey);
|
|
1966
2069
|
conversationState.setAgentName(
|
|
1967
2070
|
(upstreamRuntime ? upstreamRuntime.getAgentName() : null) || "Agent",
|
|
1968
2071
|
);
|
|
@@ -2000,7 +2103,20 @@ function createRelay(opts) {
|
|
|
2000
2103
|
},
|
|
2001
2104
|
|
|
2002
2105
|
async onNewSession() {
|
|
2106
|
+
// Cancel pending simulate-stream timers scheduled under the outgoing key
|
|
2107
|
+
// BEFORE the new key is minted — a deferred addMessage firing after the
|
|
2108
|
+
// switch would repopulate the fresh session's shared conversation view.
|
|
2109
|
+
clearSimulateStreamTimersForSession(sessionService.ensureSessionKey());
|
|
2003
2110
|
const result = await sessionService.newSession();
|
|
2111
|
+
// newSession() mints a FRESH key; defensively clear only the NEW key (it
|
|
2112
|
+
// has no snapshot yet). Do NOT touch the previous key: that session stays
|
|
2113
|
+
// resumable via onSwitchSession, and dropping its frozen snapshot would
|
|
2114
|
+
// recompute — and churn — Channel 1 if the user switches back to it. The
|
|
2115
|
+
// previous session's snapshot is released by delete or the TTL sweep.
|
|
2116
|
+
if (result && typeof result.sessionKey === "string" && result.sessionKey.trim()) {
|
|
2117
|
+
stablePromptSnapshots.evict(result.sessionKey);
|
|
2118
|
+
sessionService.clearDisplayToggleStates(result.sessionKey);
|
|
2119
|
+
}
|
|
2004
2120
|
clearCurrentSessionModelConfigSnapshot("new_session");
|
|
2005
2121
|
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
2006
2122
|
upstreamRuntime.clearTyping("new_session");
|
|
@@ -2110,6 +2226,7 @@ function createRelay(opts) {
|
|
|
2110
2226
|
if (command === "/reset") {
|
|
2111
2227
|
sessionService.invalidateSessionsCache();
|
|
2112
2228
|
resetActivityStatusAdapter();
|
|
2229
|
+
clearSimulateStreamTimersForSession(sessionService.ensureSessionKey());
|
|
2113
2230
|
conversationState.clear();
|
|
2114
2231
|
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
2115
2232
|
upstreamRuntime.clearTyping("slash_reset");
|
|
@@ -2119,6 +2236,19 @@ function createRelay(opts) {
|
|
|
2119
2236
|
);
|
|
2120
2237
|
broadcastPages();
|
|
2121
2238
|
}
|
|
2239
|
+
// A user-typed /new or /reset is a logical session reset on the CURRENT
|
|
2240
|
+
// key (distinct from an automatic CLI session reset, which keeps the same
|
|
2241
|
+
// logical session and must survive). Drop the frozen Channel-1 snapshot so
|
|
2242
|
+
// the next real message recomputes it for the fresh conversation; otherwise
|
|
2243
|
+
// the old conversation's prompt + display start-state bleed into the new one.
|
|
2244
|
+
if (command === "/new" || command === "/reset") {
|
|
2245
|
+
const resetKey = sessionService.ensureSessionKey();
|
|
2246
|
+
stablePromptSnapshots.evict(resetKey);
|
|
2247
|
+
// Clear ALL per-session state keyed to the reused key (title + upstream
|
|
2248
|
+
// name, toggles, distiller budget, first-user marker) so nothing from the
|
|
2249
|
+
// old conversation bleeds into the fresh one.
|
|
2250
|
+
sessionService.clearLogicalSessionState(resetKey);
|
|
2251
|
+
}
|
|
2122
2252
|
if (upstreamRuntime && upstreamRuntime.isConnected()) {
|
|
2123
2253
|
// Bare /reset no longer elicits an agent turn on OpenClaw 2026.6.x
|
|
2124
2254
|
// (fast-reset). Append the greeting prompt so Reset gets the same
|
|
@@ -2566,6 +2696,14 @@ function createRelay(opts) {
|
|
|
2566
2696
|
|
|
2567
2697
|
const pluginVersionService = createPluginVersionService();
|
|
2568
2698
|
|
|
2699
|
+
// Roadmap 4a: latch the worker's per-heartbeat send-buffer pressure counts
|
|
2700
|
+
// into the boolean the glasses-ui paint-floor shed queries
|
|
2701
|
+
// (isGlassesSendBufferOverHighWater on the relay API / relay-service facade).
|
|
2702
|
+
const glassesBackpressureLatch = createGlassesBackpressureLatch({
|
|
2703
|
+
emitDebug: (event, severity, data) =>
|
|
2704
|
+
emitDebug("relay.health", event, severity, null, () => data || {}),
|
|
2705
|
+
});
|
|
2706
|
+
|
|
2569
2707
|
server = createRelayWorkerSupervisor({
|
|
2570
2708
|
pluginId: "ocuclaw",
|
|
2571
2709
|
getPluginVersion: () => pluginVersionService.getPluginVersion(),
|
|
@@ -2576,6 +2714,7 @@ function createRelay(opts) {
|
|
|
2576
2714
|
host: opts.host,
|
|
2577
2715
|
port: opts.port,
|
|
2578
2716
|
token: opts.token,
|
|
2717
|
+
onWorkerBackpressure: (message) => glassesBackpressureLatch.report(message),
|
|
2579
2718
|
externalDebugToolsEnabled,
|
|
2580
2719
|
evenAiRequestTimeoutMs: opts.evenAiRequestTimeoutMs,
|
|
2581
2720
|
evenAiMaxBodyBytes: opts.evenAiMaxBodyBytes,
|
|
@@ -3013,6 +3152,16 @@ function createRelay(opts) {
|
|
|
3013
3152
|
* The downstream server is already listening from construction.
|
|
3014
3153
|
*/
|
|
3015
3154
|
start() {
|
|
3155
|
+
// Bounded cleanup of stale stable-prompt snapshots (14-day TTL).
|
|
3156
|
+
if (!stablePromptSweepTimer) {
|
|
3157
|
+
stablePromptSweepTimer = setInterval(
|
|
3158
|
+
() => stablePromptSnapshots.sweep(),
|
|
3159
|
+
60 * 60 * 1000, // hourly
|
|
3160
|
+
);
|
|
3161
|
+
if (typeof stablePromptSweepTimer.unref === "function") {
|
|
3162
|
+
stablePromptSweepTimer.unref();
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3016
3165
|
const startGateway = () => Promise.resolve(gatewayBridge.start()).then(() => {
|
|
3017
3166
|
prefetchSonioxModels("relay_start").catch((err) => {
|
|
3018
3167
|
logger.warn(`[relay] Soniox models prefetch failed: ${err.message}`);
|
|
@@ -3034,6 +3183,10 @@ function createRelay(opts) {
|
|
|
3034
3183
|
*/
|
|
3035
3184
|
stop() {
|
|
3036
3185
|
clearSimulateStreamTimers();
|
|
3186
|
+
if (stablePromptSweepTimer) {
|
|
3187
|
+
clearInterval(stablePromptSweepTimer);
|
|
3188
|
+
stablePromptSweepTimer = null;
|
|
3189
|
+
}
|
|
3037
3190
|
if (evenAiEndpoint) {
|
|
3038
3191
|
evenAiEndpoint.close();
|
|
3039
3192
|
}
|
|
@@ -3113,6 +3266,49 @@ function createRelay(opts) {
|
|
|
3113
3266
|
return sessionService.isSessionUserLocked(sessionKey);
|
|
3114
3267
|
},
|
|
3115
3268
|
|
|
3269
|
+
getDisplayStartStates(sessionKey) {
|
|
3270
|
+
return sessionService.getDisplayStartStates(sessionKey);
|
|
3271
|
+
},
|
|
3272
|
+
|
|
3273
|
+
getDisplayCurrentStates(sessionKey) {
|
|
3274
|
+
return sessionService.getDisplayCurrentStates(sessionKey);
|
|
3275
|
+
},
|
|
3276
|
+
|
|
3277
|
+
// Accessors used by the session-title distiller sidecar.
|
|
3278
|
+
getSessionTitleRecord(sessionKey) {
|
|
3279
|
+
return sessionService.getSessionTitleRecord(sessionKey);
|
|
3280
|
+
},
|
|
3281
|
+
isEvenAiSessionKey(sessionKey) {
|
|
3282
|
+
return sessionService.isEvenAiSessionKey(sessionKey);
|
|
3283
|
+
},
|
|
3284
|
+
getRawMessages() {
|
|
3285
|
+
return conversationState.getRawMessages();
|
|
3286
|
+
},
|
|
3287
|
+
getDistillerBudget() {
|
|
3288
|
+
return sessionService.getDistillerBudget();
|
|
3289
|
+
},
|
|
3290
|
+
// Canonical-key cleanup for the distiller's throwaway upstream session.
|
|
3291
|
+
// The native subagent deleteSession passes the bare key straight to
|
|
3292
|
+
// sessions.delete, which the 2026.6.x gateway indexes under the canonical
|
|
3293
|
+
// agent:<id>: form — the bare-key delete silently no-ops and the
|
|
3294
|
+
// excerpt-bearing transcript survives. deleteSessions() resolves the
|
|
3295
|
+
// canonical key via sessions.resolve first.
|
|
3296
|
+
deleteDistillerSession(sessionKey) {
|
|
3297
|
+
return sessionService.deleteSessions("ocuclaw", [sessionKey]);
|
|
3298
|
+
},
|
|
3299
|
+
getStateDir() {
|
|
3300
|
+
return opts.stateDir;
|
|
3301
|
+
},
|
|
3302
|
+
emitDebug(...args) {
|
|
3303
|
+
return emitDebug(...args);
|
|
3304
|
+
},
|
|
3305
|
+
gatewayRequest(method, params, requestOpts) {
|
|
3306
|
+
return gatewayBridge.request(method, params, requestOpts);
|
|
3307
|
+
},
|
|
3308
|
+
onGatewayEvent(eventName, listener) {
|
|
3309
|
+
return gatewayBridge.on(eventName, listener);
|
|
3310
|
+
},
|
|
3311
|
+
|
|
3116
3312
|
peekSessionKey() {
|
|
3117
3313
|
return sessionService.peekSessionKey();
|
|
3118
3314
|
},
|
|
@@ -3147,6 +3343,15 @@ function createRelay(opts) {
|
|
|
3147
3343
|
return dispatchOcuClawUserSend(params || {});
|
|
3148
3344
|
},
|
|
3149
3345
|
|
|
3346
|
+
/**
|
|
3347
|
+
* Test-only: run the logical-reset state clear (the same call the /new,
|
|
3348
|
+
* /reset, and new-chat paths make) so integration tests can verify all
|
|
3349
|
+
* per-session state is dropped for a reused session key.
|
|
3350
|
+
*/
|
|
3351
|
+
_clearLogicalSessionState(sessionKey) {
|
|
3352
|
+
sessionService.clearLogicalSessionState(sessionKey);
|
|
3353
|
+
},
|
|
3354
|
+
|
|
3150
3355
|
sendGlassesUiRender(params) {
|
|
3151
3356
|
sendGlassesUiRender(params);
|
|
3152
3357
|
},
|
|
@@ -3155,6 +3360,48 @@ function createRelay(opts) {
|
|
|
3155
3360
|
sendGlassesUiSurfaceUpdate(params);
|
|
3156
3361
|
},
|
|
3157
3362
|
|
|
3363
|
+
/**
|
|
3364
|
+
* Tap-to-wake lane (roadmap 6f): ONE agent turn for a parked glasses
|
|
3365
|
+
* gesture, dispatched through the same gateway client the voice send
|
|
3366
|
+
* uses. The MESSAGE is built (and sanitized) by the glasses-ui wake
|
|
3367
|
+
* controller — refs-only with non-wearer provenance framing; this method
|
|
3368
|
+
* is a dumb transport and deliberately does NOT touch the local
|
|
3369
|
+
* conversation state (a wake is not a wearer utterance — no synthetic
|
|
3370
|
+
* user message without provenance, §2.6).
|
|
3371
|
+
*/
|
|
3372
|
+
dispatchGlassesWake(params) {
|
|
3373
|
+
const sessionKey =
|
|
3374
|
+
params && typeof params.sessionKey === "string" && params.sessionKey
|
|
3375
|
+
? params.sessionKey
|
|
3376
|
+
: sessionService.ensureSessionKey();
|
|
3377
|
+
const message = params && typeof params.message === "string" ? params.message : "";
|
|
3378
|
+
if (!message) {
|
|
3379
|
+
return Promise.reject(new Error("dispatchGlassesWake requires a message"));
|
|
3380
|
+
}
|
|
3381
|
+
const idempotencyKey =
|
|
3382
|
+
params && typeof params.idempotencyKey === "string" && params.idempotencyKey
|
|
3383
|
+
? params.idempotencyKey
|
|
3384
|
+
: null;
|
|
3385
|
+
agentTurnTracker.markBusy(sessionKey);
|
|
3386
|
+
emitDebug(
|
|
3387
|
+
"relay.protocol",
|
|
3388
|
+
"glasses_wake_dispatch",
|
|
3389
|
+
"info",
|
|
3390
|
+
{ sessionKey },
|
|
3391
|
+
() => ({
|
|
3392
|
+
idempotencyKey,
|
|
3393
|
+
messageChars: message.length,
|
|
3394
|
+
}),
|
|
3395
|
+
);
|
|
3396
|
+
const requestParams = { message, sessionKey };
|
|
3397
|
+
if (idempotencyKey) requestParams.idempotencyKey = idempotencyKey;
|
|
3398
|
+
return gatewayBridge.request("agent", requestParams, { expectFinal: false });
|
|
3399
|
+
},
|
|
3400
|
+
|
|
3401
|
+
isAgentTurnBusy(sessionKey) {
|
|
3402
|
+
return agentTurnTracker.isBusy(sessionKey);
|
|
3403
|
+
},
|
|
3404
|
+
|
|
3158
3405
|
onGlassesUiResult(handler) {
|
|
3159
3406
|
return onGlassesUiResult(handler);
|
|
3160
3407
|
},
|
|
@@ -3175,6 +3422,10 @@ function createRelay(opts) {
|
|
|
3175
3422
|
return server ? server.getConnectedAppCount() > 0 : false;
|
|
3176
3423
|
},
|
|
3177
3424
|
|
|
3425
|
+
isGlassesSendBufferOverHighWater() {
|
|
3426
|
+
return glassesBackpressureLatch.isOverHighWater();
|
|
3427
|
+
},
|
|
3428
|
+
|
|
3178
3429
|
onAppClientDisconnect(handler) {
|
|
3179
3430
|
return onAppClientDisconnect(handler);
|
|
3180
3431
|
},
|