ocuclaw 1.3.0 → 1.3.2
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 +3 -1
- package/dist/config/runtime-config-session-title-model.test.js +22 -0
- package/dist/config/runtime-config.js +24 -15
- package/dist/domain/debug-store.js +18 -0
- 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/plugin-version-service.js +23 -0
- package/dist/runtime/register-session-title-distiller.js +100 -0
- package/dist/runtime/relay-core.js +307 -68
- package/dist/runtime/relay-service.js +120 -13
- package/dist/runtime/relay-worker-entry.js +26 -0
- package/dist/runtime/relay-worker-protocol.js +0 -4
- package/dist/runtime/relay-worker-supervisor.js +43 -79
- package/dist/runtime/relay-worker-transport.js +41 -0
- package/dist/runtime/session-service.js +159 -15
- 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 +9 -3
- package/dist/tools/glasses-ui-paint-floor.js +10 -3
- package/dist/tools/glasses-ui-recipes.js +13 -178
- package/dist/tools/glasses-ui-surfaces.js +8 -1
- package/dist/tools/glasses-ui-tool-description.test.js +16 -0
- package/dist/tools/glasses-ui-tool.js +98 -60
- 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 +6 -4
- package/skills/glasses-ui/SKILL.md +163 -0
- package/dist/runtime/downstream-server.js +0 -2057
- package/dist/runtime/plugin-update-service.js +0 -216
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import * as childProcess from "node:child_process";
|
|
4
3
|
import { EventEmitter } from "node:events";
|
|
5
|
-
import {
|
|
4
|
+
import { createPluginVersionService } from "./plugin-version-service.js";
|
|
6
5
|
import * as conversationStateModule from "../domain/conversation-state.js";
|
|
7
6
|
import { createDebugStore } from "../domain/debug-store.js";
|
|
8
7
|
import { summarizeGlassesUiContent } from "../domain/glasses-ui-content-summary.js";
|
|
@@ -10,6 +9,8 @@ import { composeReadabilitySystemPrompt } from "../domain/readability-system-pro
|
|
|
10
9
|
import { composeNeuralEmojiReactorSystemPrompt } from "../domain/neural-emoji-reactor-system-prompt.js";
|
|
11
10
|
import { composeNeuralPaceModulatorSystemPrompt } from "../domain/neural-pace-modulator-system-prompt.js";
|
|
12
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";
|
|
13
14
|
import { createActivityStatusAdapter } from "../domain/activity-status-adapter.js";
|
|
14
15
|
import { createEvenAiEndpoint } from "../even-ai/even-ai-endpoint.js";
|
|
15
16
|
import { createEvenAiRouter } from "../even-ai/even-ai-router.js";
|
|
@@ -22,7 +23,10 @@ import { createOcuClawSettingsStore } from "./ocuclaw-settings-store.js";
|
|
|
22
23
|
import { createRelayHealthMonitor } from "./relay-health-monitor.js";
|
|
23
24
|
import { createRelayOperationRegistry } from "./relay-operation-registry.js";
|
|
24
25
|
import { createRelayWorkerSupervisor } from "./relay-worker-supervisor.js";
|
|
25
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
createSessionService,
|
|
28
|
+
NEW_SESSION_GREETING_PROMPT,
|
|
29
|
+
} from "./session-service.js";
|
|
26
30
|
import { createUpstreamRuntime } from "./upstream-runtime.js";
|
|
27
31
|
|
|
28
32
|
const SONIOX_TEMP_KEY_URL = "https://api.soniox.com/v1/auth/temporary-api-key";
|
|
@@ -357,7 +361,9 @@ function createRelay(opts) {
|
|
|
357
361
|
/** Relay-local deterministic simulate-stream run sequence counter. */
|
|
358
362
|
let simulateStreamRunSeq = 0;
|
|
359
363
|
/** Active timers for relay-local deterministic simulate-stream runs. */
|
|
360
|
-
|
|
364
|
+
// timer -> sessionKey, so new-chat//reset/new-session can cancel ONLY the
|
|
365
|
+
// affected session's pending injections (re-land of a8a29032, session-scoped).
|
|
366
|
+
const simulateStreamTimers = new Map();
|
|
361
367
|
|
|
362
368
|
// --- Structured debug state ---
|
|
363
369
|
|
|
@@ -373,6 +379,31 @@ function createRelay(opts) {
|
|
|
373
379
|
// log lines share an identical ts (downstream reconcilers dedupe on it).
|
|
374
380
|
const debugNow =
|
|
375
381
|
typeof opts.debugNow === "function" ? opts.debugNow : () => Date.now();
|
|
382
|
+
|
|
383
|
+
// --- Durable debug-store arm (survives relay/gateway restarts) ---
|
|
384
|
+
// The capture arm (enabled categories + TTLs) lives only in the in-memory
|
|
385
|
+
// debug-store, which a restart rebuilds empty. We persist it to debug-arm.json
|
|
386
|
+
// (via persistDebugArm) and rehydrate it here at construction — read-once,
|
|
387
|
+
// mirroring liveUiTraceFlagPath below — so a restart no longer silently drops
|
|
388
|
+
// capture. This path covers process RESTART only: a pure WebUI *reload* does NOT
|
|
389
|
+
// restart the relay and already preserves + re-advertises the arm via
|
|
390
|
+
// relay-worker-transport.ts:327-328 (cache.debugConfig re-broadcast to app
|
|
391
|
+
// clients) — do not add reconnect machinery here.
|
|
392
|
+
const debugArmStatePath =
|
|
393
|
+
typeof opts.stateDir === "string" && opts.stateDir
|
|
394
|
+
? path.join(opts.stateDir, "debug-arm.json")
|
|
395
|
+
: null;
|
|
396
|
+
let initialDebugArm = [];
|
|
397
|
+
if (debugArmStatePath) {
|
|
398
|
+
try {
|
|
399
|
+
const parsed = JSON.parse(fs.readFileSync(debugArmStatePath, "utf8"));
|
|
400
|
+
if (parsed && Array.isArray(parsed.enabled)) {
|
|
401
|
+
initialDebugArm = parsed.enabled;
|
|
402
|
+
}
|
|
403
|
+
} catch {
|
|
404
|
+
initialDebugArm = [];
|
|
405
|
+
}
|
|
406
|
+
}
|
|
376
407
|
const debugStore = createDebugStore({
|
|
377
408
|
categories: debugCategories,
|
|
378
409
|
capacity: opts.debugCapacity,
|
|
@@ -383,6 +414,7 @@ function createRelay(opts) {
|
|
|
383
414
|
dumpMaxLimit: opts.debugDumpMaxLimit,
|
|
384
415
|
now: debugNow,
|
|
385
416
|
noisyPolicies: opts.debugNoisyPolicies,
|
|
417
|
+
initialEnabled: initialDebugArm,
|
|
386
418
|
});
|
|
387
419
|
|
|
388
420
|
// --- Live-interface trace-log flag (durable across restarts) ---
|
|
@@ -522,7 +554,7 @@ function createRelay(opts) {
|
|
|
522
554
|
);
|
|
523
555
|
}
|
|
524
556
|
|
|
525
|
-
function scheduleSimulateStreamTimer(delayMs, callback) {
|
|
557
|
+
function scheduleSimulateStreamTimer(delayMs, callback, sessionKey) {
|
|
526
558
|
const timer = setTimeout(() => {
|
|
527
559
|
simulateStreamTimers.delete(timer);
|
|
528
560
|
try {
|
|
@@ -531,12 +563,29 @@ function createRelay(opts) {
|
|
|
531
563
|
logger.error(`[relay] simulate-stream timer failed: ${err.message}`);
|
|
532
564
|
}
|
|
533
565
|
}, delayMs);
|
|
534
|
-
simulateStreamTimers.
|
|
566
|
+
simulateStreamTimers.set(timer, typeof sessionKey === "string" ? sessionKey : null);
|
|
535
567
|
return timer;
|
|
536
568
|
}
|
|
537
569
|
|
|
570
|
+
function clearSimulateStreamTimersForSession(sessionKey) {
|
|
571
|
+
let cleared = 0;
|
|
572
|
+
for (const [timer, timerSessionKey] of simulateStreamTimers) {
|
|
573
|
+
if (timerSessionKey === sessionKey) {
|
|
574
|
+
clearTimeout(timer);
|
|
575
|
+
simulateStreamTimers.delete(timer);
|
|
576
|
+
cleared += 1;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (cleared > 0) {
|
|
580
|
+
logger.info(
|
|
581
|
+
`[relay] cancelled ${cleared} pending simulate-stream timer(s) for session ${sessionKey}`,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
return cleared;
|
|
585
|
+
}
|
|
586
|
+
|
|
538
587
|
function clearSimulateStreamTimers() {
|
|
539
|
-
for (const timer of simulateStreamTimers) {
|
|
588
|
+
for (const timer of simulateStreamTimers.keys()) {
|
|
540
589
|
clearTimeout(timer);
|
|
541
590
|
}
|
|
542
591
|
simulateStreamTimers.clear();
|
|
@@ -895,37 +944,67 @@ function createRelay(opts) {
|
|
|
895
944
|
systemPrompt: opts.ocuClawSystemPrompt,
|
|
896
945
|
},
|
|
897
946
|
});
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
947
|
+
const stablePromptSnapshots = createStablePromptSnapshotStore({
|
|
948
|
+
stateDir: opts.stateDir,
|
|
949
|
+
emitDebug,
|
|
950
|
+
});
|
|
951
|
+
// Hourly TTL sweep for stale stable-prompt snapshots; started in start(),
|
|
952
|
+
// cleared in stop(). Declared here so both can see it.
|
|
953
|
+
let stablePromptSweepTimer = null;
|
|
954
|
+
|
|
955
|
+
// Channel 1: the per-session-immutable extraSystemPrompt. Computed once from
|
|
956
|
+
// the feature set AT SESSION START and then served byte-identical for the
|
|
957
|
+
// session's lifetime (see stable-prompt-snapshot). Mid-session toggles are
|
|
958
|
+
// bridged by the Channel-2 hook composer, NOT here. The glasses-UI pointer is
|
|
959
|
+
// always present (its disconnected gate moved to Channel 2); the emoji/pace
|
|
960
|
+
// blocks are included only when active at session start.
|
|
961
|
+
function computeStableChannelOne(startSignals) {
|
|
901
962
|
const baseReadability = composeReadabilitySystemPrompt(
|
|
902
963
|
ocuClawSettingsStore.getSnapshot().systemPrompt,
|
|
903
964
|
);
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
const
|
|
909
|
-
const paceState = validState(signals.neuralPaceModulatorState);
|
|
910
|
-
const reactor = composeNeuralEmojiReactorSystemPrompt({ state: reactorState });
|
|
911
|
-
const pace = composeNeuralPaceModulatorSystemPrompt({ state: paceState });
|
|
912
|
-
// Only include the glasses-UI nudge when a downstream app client is
|
|
913
|
-
// connected. Keeps the prompt clean when the agent has nowhere to render
|
|
914
|
-
// the tool's output, and keeps existing prompt-assembly tests stable
|
|
915
|
-
// (they exercise the prompt without spinning up an app client).
|
|
916
|
-
const hasAppClient =
|
|
917
|
-
server &&
|
|
918
|
-
typeof server.getConnectedAppCount === "function" &&
|
|
919
|
-
server.getConnectedAppCount() > 0;
|
|
920
|
-
const glassesUiNudge = hasAppClient ? composeGlassesUiNudgeSystemPrompt() : "";
|
|
965
|
+
const display = composeGlassesDisplaySystemPrompt({
|
|
966
|
+
emoji: startSignals.emoji,
|
|
967
|
+
pace: startSignals.pace,
|
|
968
|
+
});
|
|
969
|
+
const glassesPointer = composeGlassesUiNudgeSystemPrompt();
|
|
921
970
|
const parts = [];
|
|
922
971
|
if (baseReadability) parts.push(baseReadability);
|
|
923
|
-
if (
|
|
924
|
-
if (
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
972
|
+
if (display) parts.push(display);
|
|
973
|
+
if (glassesPointer) parts.push(glassesPointer);
|
|
974
|
+
return parts.join("\n\n");
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function stableSendOptions(resolvedSessionKey, sessionId, perTurnSignals) {
|
|
978
|
+
const signals = perTurnSignals || {};
|
|
979
|
+
// "enabled at start" = the client's reported state on the FIRST send of the
|
|
980
|
+
// session. The 3-state client signal maps to a boolean: only "active" counts.
|
|
981
|
+
const startEmoji = signals.neuralEmojiReactorState === "active";
|
|
982
|
+
const startPace = signals.neuralPaceModulatorState === "active";
|
|
983
|
+
const extraSystemPrompt = stablePromptSnapshots.getOrCreate(
|
|
984
|
+
resolvedSessionKey,
|
|
985
|
+
sessionId,
|
|
986
|
+
() => computeStableChannelOne({ emoji: startEmoji, pace: startPace }),
|
|
987
|
+
);
|
|
988
|
+
// Churn guard: if recomputing TODAY would differ from the served snapshot,
|
|
989
|
+
// something is mutating Channel 1 mid-session (e.g. a toggle flipped the
|
|
990
|
+
// start-state signals) — the case that used to reset the CLI session.
|
|
991
|
+
// Surface it loudly; the served prompt stays the frozen snapshot.
|
|
992
|
+
if (
|
|
993
|
+
stablePromptSnapshots.wouldChurn(
|
|
994
|
+
resolvedSessionKey,
|
|
995
|
+
sessionId,
|
|
996
|
+
computeStableChannelOne({ emoji: startEmoji, pace: startPace }),
|
|
997
|
+
)
|
|
998
|
+
) {
|
|
999
|
+
emitDebug(
|
|
1000
|
+
"relay.session",
|
|
1001
|
+
"stable_prompt_churn_detected",
|
|
1002
|
+
"warn",
|
|
1003
|
+
{ sessionKey: resolvedSessionKey },
|
|
1004
|
+
() => ({ sessionId: sessionId || null }),
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
return { extraSystemPrompt };
|
|
929
1008
|
}
|
|
930
1009
|
|
|
931
1010
|
function buildOcuClawSendDiagnostic(params = {}) {
|
|
@@ -1437,6 +1516,10 @@ function createRelay(opts) {
|
|
|
1437
1516
|
resolvedSessionKey,
|
|
1438
1517
|
clientDisplaySignals.neuralSessionNamesEnabled !== false,
|
|
1439
1518
|
);
|
|
1519
|
+
sessionService.recordDisplayToggleStates(resolvedSessionKey, {
|
|
1520
|
+
emoji: clientDisplaySignals.neuralEmojiReactorState === "active",
|
|
1521
|
+
pace: clientDisplaySignals.neuralPaceModulatorState === "active",
|
|
1522
|
+
});
|
|
1440
1523
|
}
|
|
1441
1524
|
const hasAttachment = !!attachment;
|
|
1442
1525
|
const sendStartedAt = Date.now();
|
|
@@ -1466,7 +1549,16 @@ function createRelay(opts) {
|
|
|
1466
1549
|
resolvedSessionKey,
|
|
1467
1550
|
attachment,
|
|
1468
1551
|
{
|
|
1469
|
-
...
|
|
1552
|
+
...stableSendOptions(
|
|
1553
|
+
resolvedSessionKey,
|
|
1554
|
+
// No synchronous OpenClaw sessionId is available at send time
|
|
1555
|
+
// (resolveSessionCanonicalKey is async). Use the sessionKey as the
|
|
1556
|
+
// snapshot's id; the sessionId-mismatch guard is therefore a no-op,
|
|
1557
|
+
// and new-session safety rests on logical-session-end eviction
|
|
1558
|
+
// (onNewSession / onNewChat / onDeleteSessions evict the snapshot).
|
|
1559
|
+
resolvedSessionKey,
|
|
1560
|
+
clientDisplaySignals,
|
|
1561
|
+
),
|
|
1470
1562
|
diagnostic: buildOcuClawSendDiagnostic({
|
|
1471
1563
|
...params,
|
|
1472
1564
|
sessionKey: resolvedSessionKey,
|
|
@@ -1621,6 +1713,49 @@ function createRelay(opts) {
|
|
|
1621
1713
|
return { ok: true, enabled, persisted, persistedPath: liveUiTraceFlagPath };
|
|
1622
1714
|
}
|
|
1623
1715
|
|
|
1716
|
+
// Persist the current debug-store arm to debug-arm.json. Mirrors the
|
|
1717
|
+
// applyTraceLogSet writeFileSync above (plain, non-atomic): a partial/corrupt
|
|
1718
|
+
// write degrades to an empty arm on next boot — acceptable, the nothing-armed
|
|
1719
|
+
// warning catches it. getSnapshot().enabled is already pruned of expired
|
|
1720
|
+
// categories, so the persisted JSON never holds an expired entry. Never throws
|
|
1721
|
+
// into the caller.
|
|
1722
|
+
function persistDebugArm() {
|
|
1723
|
+
if (!debugArmStatePath) return false;
|
|
1724
|
+
try {
|
|
1725
|
+
const enabled = debugStore.getSnapshot().enabled;
|
|
1726
|
+
fs.writeFileSync(debugArmStatePath, JSON.stringify({ enabled }) + "\n");
|
|
1727
|
+
return true;
|
|
1728
|
+
} catch (err) {
|
|
1729
|
+
logger.warn(`[relay] debug arm persist failed: ${err && err.message ? err.message : err}`);
|
|
1730
|
+
return false;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
function applyDebugSet(clientId, request) {
|
|
1735
|
+
const result = debugStore.setCategories(request);
|
|
1736
|
+
if (!result.ok) {
|
|
1737
|
+
throw new Error(result.error || "debug-set failed");
|
|
1738
|
+
}
|
|
1739
|
+
// Persist after every successful set — enable AND disable-to-empty — so the
|
|
1740
|
+
// on-disk arm always tracks live state and a deliberately-cleared arm is not
|
|
1741
|
+
// resurrected on the next restart.
|
|
1742
|
+
persistDebugArm();
|
|
1743
|
+
emitDebug(
|
|
1744
|
+
"relay.protocol",
|
|
1745
|
+
"debug_set",
|
|
1746
|
+
"info",
|
|
1747
|
+
{ sessionKey: sessionService.ensureSessionKey() },
|
|
1748
|
+
() => ({
|
|
1749
|
+
clientId,
|
|
1750
|
+
enable: result.applied.enable,
|
|
1751
|
+
disable: result.applied.disable,
|
|
1752
|
+
ttlMs: result.ttlMs,
|
|
1753
|
+
enabledCount: result.enabled.length,
|
|
1754
|
+
}),
|
|
1755
|
+
);
|
|
1756
|
+
return result;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1624
1759
|
const handler = createDownstreamHandler({
|
|
1625
1760
|
logger,
|
|
1626
1761
|
externalDebugToolsEnabled,
|
|
@@ -1697,6 +1832,14 @@ function createRelay(opts) {
|
|
|
1697
1832
|
return upstreamRuntime.compactActiveSession(sessionKey);
|
|
1698
1833
|
},
|
|
1699
1834
|
onDeleteSessions(sessionKeys, kind, switchBeforeDelete) {
|
|
1835
|
+
if (Array.isArray(sessionKeys)) {
|
|
1836
|
+
for (const key of sessionKeys) {
|
|
1837
|
+
if (typeof key === "string" && key.trim()) {
|
|
1838
|
+
stablePromptSnapshots.evict(key);
|
|
1839
|
+
sessionService.clearDisplayToggleStates(key);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1700
1843
|
const action = switchBeforeDelete
|
|
1701
1844
|
? sessionService.switchAndDeleteSessions(kind, sessionKeys)
|
|
1702
1845
|
: sessionService.deleteSessions(kind, sessionKeys);
|
|
@@ -1833,7 +1976,7 @@ function createRelay(opts) {
|
|
|
1833
1976
|
totalChars: text.length,
|
|
1834
1977
|
}),
|
|
1835
1978
|
);
|
|
1836
|
-
});
|
|
1979
|
+
}, sessionKey);
|
|
1837
1980
|
}
|
|
1838
1981
|
|
|
1839
1982
|
const completeDelayMs = startDelayMs + (chunkCount * chunkIntervalMs) + thinkingTailMs;
|
|
@@ -1864,7 +2007,7 @@ function createRelay(opts) {
|
|
|
1864
2007
|
completeDelayMs,
|
|
1865
2008
|
}),
|
|
1866
2009
|
);
|
|
1867
|
-
});
|
|
2010
|
+
}, sessionKey);
|
|
1868
2011
|
|
|
1869
2012
|
return Promise.resolve({
|
|
1870
2013
|
status: "accepted",
|
|
@@ -1891,13 +2034,30 @@ function createRelay(opts) {
|
|
|
1891
2034
|
}
|
|
1892
2035
|
sessionService.invalidateSessionsCache();
|
|
1893
2036
|
resetActivityStatusAdapter();
|
|
2037
|
+
// Cancel THIS session's pending simulate-stream timers BEFORE clearing —
|
|
2038
|
+
// a deferred addMessage firing after the clear repopulates the fresh chat
|
|
2039
|
+
// (the 2026-05-15 canary-pollution mechanism).
|
|
2040
|
+
clearSimulateStreamTimersForSession(sessionService.ensureSessionKey());
|
|
1894
2041
|
conversationState.clear();
|
|
2042
|
+
// Logical session end: this key is REUSED for a fresh conversation, so the
|
|
2043
|
+
// Channel-1 snapshot must be dropped or the next send serves a stale prompt,
|
|
2044
|
+
// and ALL other per-session state keyed to it (title + upstream name,
|
|
2045
|
+
// toggles, distiller budget, first-user marker) must be cleared too.
|
|
2046
|
+
const newChatSessionKey = sessionService.ensureSessionKey();
|
|
2047
|
+
stablePromptSnapshots.evict(newChatSessionKey);
|
|
2048
|
+
sessionService.clearLogicalSessionState(newChatSessionKey);
|
|
1895
2049
|
conversationState.setAgentName(
|
|
1896
2050
|
(upstreamRuntime ? upstreamRuntime.getAgentName() : null) || "Agent",
|
|
1897
2051
|
);
|
|
1898
2052
|
const pages = conversationState.getPages();
|
|
1899
2053
|
cachePages(pages);
|
|
1900
2054
|
if (upstreamRuntime && upstreamRuntime.isConnected()) {
|
|
2055
|
+
// NOTE: onNewChat targets the hard-coded "main" key (legacy) without
|
|
2056
|
+
// changing currentSessionKey, so it must NOT elicit a welcome turn here:
|
|
2057
|
+
// the turn's events would carry "main" and be dropped by isCurrentSession()
|
|
2058
|
+
// whenever the active session is an ocuclaw:* key. The welcome restore for
|
|
2059
|
+
// the real glasses paths lives in newSession() (New) and onSlashCommand
|
|
2060
|
+
// "/reset" (Reset). Unifying onNewChat onto newSession() is Phase-2 work.
|
|
1901
2061
|
gatewayBridge.sendMessage("/new", "main").catch((err) => {
|
|
1902
2062
|
logger.error(`[relay] Failed to send /new: ${err.message}`);
|
|
1903
2063
|
});
|
|
@@ -1923,7 +2083,20 @@ function createRelay(opts) {
|
|
|
1923
2083
|
},
|
|
1924
2084
|
|
|
1925
2085
|
async onNewSession() {
|
|
2086
|
+
// Cancel pending simulate-stream timers scheduled under the outgoing key
|
|
2087
|
+
// BEFORE the new key is minted — a deferred addMessage firing after the
|
|
2088
|
+
// switch would repopulate the fresh session's shared conversation view.
|
|
2089
|
+
clearSimulateStreamTimersForSession(sessionService.ensureSessionKey());
|
|
1926
2090
|
const result = await sessionService.newSession();
|
|
2091
|
+
// newSession() mints a FRESH key; defensively clear only the NEW key (it
|
|
2092
|
+
// has no snapshot yet). Do NOT touch the previous key: that session stays
|
|
2093
|
+
// resumable via onSwitchSession, and dropping its frozen snapshot would
|
|
2094
|
+
// recompute — and churn — Channel 1 if the user switches back to it. The
|
|
2095
|
+
// previous session's snapshot is released by delete or the TTL sweep.
|
|
2096
|
+
if (result && typeof result.sessionKey === "string" && result.sessionKey.trim()) {
|
|
2097
|
+
stablePromptSnapshots.evict(result.sessionKey);
|
|
2098
|
+
sessionService.clearDisplayToggleStates(result.sessionKey);
|
|
2099
|
+
}
|
|
1927
2100
|
clearCurrentSessionModelConfigSnapshot("new_session");
|
|
1928
2101
|
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
1929
2102
|
upstreamRuntime.clearTyping("new_session");
|
|
@@ -2033,6 +2206,7 @@ function createRelay(opts) {
|
|
|
2033
2206
|
if (command === "/reset") {
|
|
2034
2207
|
sessionService.invalidateSessionsCache();
|
|
2035
2208
|
resetActivityStatusAdapter();
|
|
2209
|
+
clearSimulateStreamTimersForSession(sessionService.ensureSessionKey());
|
|
2036
2210
|
conversationState.clear();
|
|
2037
2211
|
if (upstreamRuntime && typeof upstreamRuntime.clearTyping === "function") {
|
|
2038
2212
|
upstreamRuntime.clearTyping("slash_reset");
|
|
@@ -2042,8 +2216,31 @@ function createRelay(opts) {
|
|
|
2042
2216
|
);
|
|
2043
2217
|
broadcastPages();
|
|
2044
2218
|
}
|
|
2219
|
+
// A user-typed /new or /reset is a logical session reset on the CURRENT
|
|
2220
|
+
// key (distinct from an automatic CLI session reset, which keeps the same
|
|
2221
|
+
// logical session and must survive). Drop the frozen Channel-1 snapshot so
|
|
2222
|
+
// the next real message recomputes it for the fresh conversation; otherwise
|
|
2223
|
+
// the old conversation's prompt + display start-state bleed into the new one.
|
|
2224
|
+
if (command === "/new" || command === "/reset") {
|
|
2225
|
+
const resetKey = sessionService.ensureSessionKey();
|
|
2226
|
+
stablePromptSnapshots.evict(resetKey);
|
|
2227
|
+
// Clear ALL per-session state keyed to the reused key (title + upstream
|
|
2228
|
+
// name, toggles, distiller budget, first-user marker) so nothing from the
|
|
2229
|
+
// old conversation bleeds into the fresh one.
|
|
2230
|
+
sessionService.clearLogicalSessionState(resetKey);
|
|
2231
|
+
}
|
|
2045
2232
|
if (upstreamRuntime && upstreamRuntime.isConnected()) {
|
|
2046
|
-
|
|
2233
|
+
// Bare /reset no longer elicits an agent turn on OpenClaw 2026.6.x
|
|
2234
|
+
// (fast-reset). Append the greeting prompt so Reset gets the same
|
|
2235
|
+
// welcome as New. Other slash commands forward verbatim.
|
|
2236
|
+
const outboundCommand =
|
|
2237
|
+
command === "/reset"
|
|
2238
|
+
? `/reset ${NEW_SESSION_GREETING_PROMPT}`
|
|
2239
|
+
: command;
|
|
2240
|
+
return gatewayBridge.sendMessage(
|
|
2241
|
+
outboundCommand,
|
|
2242
|
+
sessionService.ensureSessionKey(),
|
|
2243
|
+
);
|
|
2047
2244
|
}
|
|
2048
2245
|
return Promise.resolve();
|
|
2049
2246
|
},
|
|
@@ -2102,26 +2299,7 @@ function createRelay(opts) {
|
|
|
2102
2299
|
},
|
|
2103
2300
|
|
|
2104
2301
|
onDebugSet(clientId, request) {
|
|
2105
|
-
|
|
2106
|
-
if (!result.ok) {
|
|
2107
|
-
throw new Error(result.error || "debug-set failed");
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
emitDebug(
|
|
2111
|
-
"relay.protocol",
|
|
2112
|
-
"debug_set",
|
|
2113
|
-
"info",
|
|
2114
|
-
{ sessionKey: sessionService.ensureSessionKey() },
|
|
2115
|
-
() => ({
|
|
2116
|
-
clientId,
|
|
2117
|
-
enable: result.applied.enable,
|
|
2118
|
-
disable: result.applied.disable,
|
|
2119
|
-
ttlMs: result.ttlMs,
|
|
2120
|
-
enabledCount: result.enabled.length,
|
|
2121
|
-
}),
|
|
2122
|
-
);
|
|
2123
|
-
|
|
2124
|
-
return result;
|
|
2302
|
+
return applyDebugSet(clientId, request);
|
|
2125
2303
|
},
|
|
2126
2304
|
|
|
2127
2305
|
onTraceLogSet(clientId, request) {
|
|
@@ -2496,18 +2674,12 @@ function createRelay(opts) {
|
|
|
2496
2674
|
|
|
2497
2675
|
// --- Worker supervisor ---
|
|
2498
2676
|
|
|
2499
|
-
const
|
|
2500
|
-
spawn: childProcess.spawn,
|
|
2501
|
-
logger,
|
|
2502
|
-
nowMs: () => Date.now(),
|
|
2503
|
-
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
2504
|
-
clearTimeout: (handle) => clearTimeout(handle),
|
|
2505
|
-
});
|
|
2677
|
+
const pluginVersionService = createPluginVersionService();
|
|
2506
2678
|
|
|
2507
2679
|
server = createRelayWorkerSupervisor({
|
|
2508
2680
|
pluginId: "ocuclaw",
|
|
2509
|
-
getPluginVersion: () =>
|
|
2510
|
-
getRequiresClientVersion: () =>
|
|
2681
|
+
getPluginVersion: () => pluginVersionService.getPluginVersion(),
|
|
2682
|
+
getRequiresClientVersion: () => pluginVersionService.getRequiresClientVersion(),
|
|
2511
2683
|
logger,
|
|
2512
2684
|
handler,
|
|
2513
2685
|
operationRegistry: relayOperationRegistry,
|
|
@@ -2515,8 +2687,6 @@ function createRelay(opts) {
|
|
|
2515
2687
|
port: opts.port,
|
|
2516
2688
|
token: opts.token,
|
|
2517
2689
|
externalDebugToolsEnabled,
|
|
2518
|
-
runPluginUpdate: () => pluginUpdateService.runPluginUpdate(),
|
|
2519
|
-
runGatewayRestart: () => pluginUpdateService.runGatewayRestart(),
|
|
2520
2690
|
evenAiRequestTimeoutMs: opts.evenAiRequestTimeoutMs,
|
|
2521
2691
|
evenAiMaxBodyBytes: opts.evenAiMaxBodyBytes,
|
|
2522
2692
|
evenAiMaxResponseBytes: opts.evenAiMaxResponseBytes,
|
|
@@ -2953,6 +3123,16 @@ function createRelay(opts) {
|
|
|
2953
3123
|
* The downstream server is already listening from construction.
|
|
2954
3124
|
*/
|
|
2955
3125
|
start() {
|
|
3126
|
+
// Bounded cleanup of stale stable-prompt snapshots (14-day TTL).
|
|
3127
|
+
if (!stablePromptSweepTimer) {
|
|
3128
|
+
stablePromptSweepTimer = setInterval(
|
|
3129
|
+
() => stablePromptSnapshots.sweep(),
|
|
3130
|
+
60 * 60 * 1000, // hourly
|
|
3131
|
+
);
|
|
3132
|
+
if (typeof stablePromptSweepTimer.unref === "function") {
|
|
3133
|
+
stablePromptSweepTimer.unref();
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
2956
3136
|
const startGateway = () => Promise.resolve(gatewayBridge.start()).then(() => {
|
|
2957
3137
|
prefetchSonioxModels("relay_start").catch((err) => {
|
|
2958
3138
|
logger.warn(`[relay] Soniox models prefetch failed: ${err.message}`);
|
|
@@ -2974,6 +3154,10 @@ function createRelay(opts) {
|
|
|
2974
3154
|
*/
|
|
2975
3155
|
stop() {
|
|
2976
3156
|
clearSimulateStreamTimers();
|
|
3157
|
+
if (stablePromptSweepTimer) {
|
|
3158
|
+
clearInterval(stablePromptSweepTimer);
|
|
3159
|
+
stablePromptSweepTimer = null;
|
|
3160
|
+
}
|
|
2977
3161
|
if (evenAiEndpoint) {
|
|
2978
3162
|
evenAiEndpoint.close();
|
|
2979
3163
|
}
|
|
@@ -3019,6 +3203,9 @@ function createRelay(opts) {
|
|
|
3019
3203
|
__onTraceLogSetForTest(clientId, request) {
|
|
3020
3204
|
return applyTraceLogSet(clientId, request);
|
|
3021
3205
|
},
|
|
3206
|
+
__onDebugSetForTest(clientId, request) {
|
|
3207
|
+
return applyDebugSet(clientId, request);
|
|
3208
|
+
},
|
|
3022
3209
|
|
|
3023
3210
|
get operationRegistryForTest() {
|
|
3024
3211
|
return relayOperationRegistry;
|
|
@@ -3050,6 +3237,49 @@ function createRelay(opts) {
|
|
|
3050
3237
|
return sessionService.isSessionUserLocked(sessionKey);
|
|
3051
3238
|
},
|
|
3052
3239
|
|
|
3240
|
+
getDisplayStartStates(sessionKey) {
|
|
3241
|
+
return sessionService.getDisplayStartStates(sessionKey);
|
|
3242
|
+
},
|
|
3243
|
+
|
|
3244
|
+
getDisplayCurrentStates(sessionKey) {
|
|
3245
|
+
return sessionService.getDisplayCurrentStates(sessionKey);
|
|
3246
|
+
},
|
|
3247
|
+
|
|
3248
|
+
// Accessors used by the session-title distiller sidecar.
|
|
3249
|
+
getSessionTitleRecord(sessionKey) {
|
|
3250
|
+
return sessionService.getSessionTitleRecord(sessionKey);
|
|
3251
|
+
},
|
|
3252
|
+
isEvenAiSessionKey(sessionKey) {
|
|
3253
|
+
return sessionService.isEvenAiSessionKey(sessionKey);
|
|
3254
|
+
},
|
|
3255
|
+
getRawMessages() {
|
|
3256
|
+
return conversationState.getRawMessages();
|
|
3257
|
+
},
|
|
3258
|
+
getDistillerBudget() {
|
|
3259
|
+
return sessionService.getDistillerBudget();
|
|
3260
|
+
},
|
|
3261
|
+
// Canonical-key cleanup for the distiller's throwaway upstream session.
|
|
3262
|
+
// The native subagent deleteSession passes the bare key straight to
|
|
3263
|
+
// sessions.delete, which the 2026.6.x gateway indexes under the canonical
|
|
3264
|
+
// agent:<id>: form — the bare-key delete silently no-ops and the
|
|
3265
|
+
// excerpt-bearing transcript survives. deleteSessions() resolves the
|
|
3266
|
+
// canonical key via sessions.resolve first.
|
|
3267
|
+
deleteDistillerSession(sessionKey) {
|
|
3268
|
+
return sessionService.deleteSessions("ocuclaw", [sessionKey]);
|
|
3269
|
+
},
|
|
3270
|
+
getStateDir() {
|
|
3271
|
+
return opts.stateDir;
|
|
3272
|
+
},
|
|
3273
|
+
emitDebug(...args) {
|
|
3274
|
+
return emitDebug(...args);
|
|
3275
|
+
},
|
|
3276
|
+
gatewayRequest(method, params, requestOpts) {
|
|
3277
|
+
return gatewayBridge.request(method, params, requestOpts);
|
|
3278
|
+
},
|
|
3279
|
+
onGatewayEvent(eventName, listener) {
|
|
3280
|
+
return gatewayBridge.on(eventName, listener);
|
|
3281
|
+
},
|
|
3282
|
+
|
|
3053
3283
|
peekSessionKey() {
|
|
3054
3284
|
return sessionService.peekSessionKey();
|
|
3055
3285
|
},
|
|
@@ -3084,6 +3314,15 @@ function createRelay(opts) {
|
|
|
3084
3314
|
return dispatchOcuClawUserSend(params || {});
|
|
3085
3315
|
},
|
|
3086
3316
|
|
|
3317
|
+
/**
|
|
3318
|
+
* Test-only: run the logical-reset state clear (the same call the /new,
|
|
3319
|
+
* /reset, and new-chat paths make) so integration tests can verify all
|
|
3320
|
+
* per-session state is dropped for a reused session key.
|
|
3321
|
+
*/
|
|
3322
|
+
_clearLogicalSessionState(sessionKey) {
|
|
3323
|
+
sessionService.clearLogicalSessionState(sessionKey);
|
|
3324
|
+
},
|
|
3325
|
+
|
|
3087
3326
|
sendGlassesUiRender(params) {
|
|
3088
3327
|
sendGlassesUiRender(params);
|
|
3089
3328
|
},
|