ocuclaw 1.3.2 → 1.3.4
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 +29 -1
- package/dist/config/runtime-config-session-title-model.test.js +0 -3
- package/dist/config/runtime-config.js +22 -33
- package/dist/domain/activity-status-adapter.js +0 -7
- package/dist/domain/activity-status-arbiter.js +3 -27
- package/dist/domain/activity-status-labels.js +8 -38
- package/dist/domain/code-span-regions.js +4 -24
- package/dist/domain/constant-time-equal.js +9 -0
- package/dist/domain/constant-time-equal.test.js +28 -0
- package/dist/domain/conversation-state.js +27 -138
- package/dist/domain/debug-bundle-cache.js +52 -0
- package/dist/domain/debug-bundle-format.js +60 -0
- package/dist/domain/debug-bundle-preview.js +123 -0
- package/dist/domain/debug-bundle-redaction.js +182 -0
- package/dist/domain/debug-bundle-save.js +11 -0
- package/dist/domain/debug-bundle-zip.js +15 -0
- package/dist/domain/debug-bundle.js +97 -0
- package/dist/domain/debug-store.js +6 -17
- package/dist/domain/debug-upload-preset.js +27 -0
- package/dist/domain/glasses-display-system-prompt.js +0 -5
- package/dist/domain/glasses-display-system-prompt.test.js +1 -1
- package/dist/domain/glasses-ui-content-summary.js +0 -6
- package/dist/domain/glasses-ui-system-prompt.test.js +1 -2
- package/dist/domain/message-emoji-allowlist.js +0 -7
- package/dist/domain/message-emoji-filter.js +3 -9
- package/dist/domain/neural-emoji-reactor-tag-config.js +3 -3
- package/dist/domain/prompt-channel-fragments.js +1 -10
- package/dist/domain/tagged-span-parser.js +3 -26
- package/dist/domain/tagged-span-strip.js +0 -7
- package/dist/even-ai/even-ai-endpoint.js +77 -24
- package/dist/even-ai/even-ai-run-waiter.js +0 -1
- package/dist/even-ai/even-ai-settings-store.js +11 -0
- package/dist/gateway/gateway-bridge.js +8 -9
- package/dist/gateway/gateway-timing-ledger.js +8 -6
- package/dist/gateway/openclaw-client.js +97 -297
- package/dist/gateway/sanitize-connect-reason.js +10 -0
- package/dist/gateway/sanitize-connect-reason.test.js +34 -0
- package/dist/index.js +3 -3
- package/dist/runtime/channel-two-hook.js +1 -6
- package/dist/runtime/container-env.js +1 -5
- package/dist/runtime/debug-bundle-handler.js +159 -0
- package/dist/runtime/display-toggle-states.js +6 -17
- package/dist/runtime/downstream-handler.js +682 -508
- package/dist/runtime/glasses-backpressure-latch.js +93 -0
- package/dist/runtime/ocuclaw-settings-store.js +10 -1
- package/dist/runtime/openclaw-host-version.js +5 -0
- package/dist/runtime/plugin-version-service.js +13 -6
- package/dist/runtime/provider-usage-select.js +0 -6
- package/dist/runtime/register-session-title-distiller.js +14 -16
- package/dist/runtime/relay-core.js +657 -271
- package/dist/runtime/relay-service.js +40 -36
- package/dist/runtime/relay-worker-approval-replay-cache.js +1 -1
- package/dist/runtime/relay-worker-entry.js +1 -2
- package/dist/runtime/relay-worker-health.js +2 -10
- package/dist/runtime/relay-worker-protocol.js +6 -1
- package/dist/runtime/relay-worker-supervisor.js +109 -39
- package/dist/runtime/relay-worker-transport.js +157 -15
- package/dist/runtime/session-context-service.js +5 -45
- package/dist/runtime/session-service.js +157 -175
- package/dist/runtime/session-title-distiller-budget.js +1 -5
- package/dist/runtime/session-title-distiller-helpers.js +14 -24
- package/dist/runtime/session-title-distiller.js +109 -122
- package/dist/runtime/session-title-record.js +0 -6
- package/dist/runtime/stable-prompt-snapshot.js +3 -14
- package/dist/runtime/upstream-runtime.js +600 -103
- package/dist/tools/device-info-tool.js +4 -21
- package/dist/tools/glasses-ui-cron.js +58 -63
- package/dist/tools/glasses-ui-descriptors.js +4 -33
- package/dist/tools/glasses-ui-limits.js +0 -13
- package/dist/tools/glasses-ui-paint-floor.js +22 -34
- package/dist/tools/glasses-ui-recipes.js +92 -101
- package/dist/tools/glasses-ui-surfaces.js +295 -100
- package/dist/tools/glasses-ui-template.js +7 -22
- package/dist/tools/glasses-ui-tool-description.test.js +2 -2
- package/dist/tools/glasses-ui-tool.js +475 -331
- package/dist/tools/glasses-ui-voicemail.js +242 -0
- package/dist/tools/glasses-ui-wake.js +195 -0
- package/dist/tools/session-title-tool.js +2 -7
- package/dist/tools/session-title-tool.test.js +1 -1
- package/dist/version.js +3 -2
- package/openclaw.plugin.json +60 -13
- package/package.json +3 -2
- package/skills/glasses-ui/SKILL.md +19 -3
- package/dist/runtime/protocol-adapter.js +0 -387
|
@@ -8,27 +8,12 @@ import { createDistillerBudget } from "./session-title-distiller-budget.js";
|
|
|
8
8
|
const SESSION_FIRST_USER_CACHE_FILE = "session-first-user-cache.json";
|
|
9
9
|
const SESSION_TITLE_CACHE_FILE = "session-title-cache.json";
|
|
10
10
|
const SESSION_PIN_CACHE_FILE = "ocuclaw-session-pins.json";
|
|
11
|
+
const SESSION_AGENT_CACHE_FILE = "ocuclaw-session-agents.json";
|
|
11
12
|
const PIN_CAP_PER_KIND = 20;
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
* Greeting-eliciting content appended to /new and /reset so OpenClaw runs an
|
|
15
|
-
* agent turn (the new-session "welcome"). OpenClaw 2026.6.x ("make bare reset
|
|
16
|
-
* commands fast", gateway commit 2c6a3f6b04) made a BARE /new or /reset return
|
|
17
|
-
* a synchronous ack and run NO agent turn — so a bare reset no longer produces
|
|
18
|
-
* a welcome and leaves the glasses stuck on the "starting new session"
|
|
19
|
-
* placeholder. Sending "/new <prompt>" / "/reset <prompt>" routes through the
|
|
20
|
-
* gateway's normal agent path (content = the prompt) and elicits the welcome.
|
|
21
|
-
* The wording matches the prompt OpenClaw used to inject itself, so the
|
|
22
|
-
* existing synthetic-session-starter filters (conversation-state display hide +
|
|
23
|
-
* isSyntheticSessionStarter title/preview filter) keep it hidden from the
|
|
24
|
-
* transcript and session titles. NOTE: newSession() does NOT call
|
|
25
|
-
* conversationState.addMessage("user", ...), so this content never renders as a
|
|
26
|
-
* user bubble on the glasses.
|
|
27
|
-
*/
|
|
28
14
|
export const NEW_SESSION_GREETING_PROMPT =
|
|
29
15
|
"A new session was started via /new or /reset. Execute your Session Startup sequence now - read the required files before responding to the user. If BOOTSTRAP.md exists in the provided Project Context, read it and follow its instructions first. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. If the runtime model differs from default_model in the system prompt, mention the default model. Do not mention internal steps, files, tools, or reasoning.";
|
|
30
16
|
|
|
31
|
-
|
|
32
17
|
function normalizeLogger(logger) {
|
|
33
18
|
if (!logger || typeof logger !== "object") {
|
|
34
19
|
return console;
|
|
@@ -66,6 +51,18 @@ function resolveSessionPinCachePath(stateDir) {
|
|
|
66
51
|
return path.join(resolvedStateDir, SESSION_PIN_CACHE_FILE);
|
|
67
52
|
}
|
|
68
53
|
|
|
54
|
+
function resolveSessionAgentCachePath(stateDir) {
|
|
55
|
+
const resolvedStateDir = normalizeStateDir(stateDir);
|
|
56
|
+
if (!resolvedStateDir) return null;
|
|
57
|
+
return path.join(resolvedStateDir, SESSION_AGENT_CACHE_FILE);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function deriveAgentIdFromFullKey(fullKey) {
|
|
61
|
+
if (typeof fullKey !== "string") return "";
|
|
62
|
+
const match = /^agent:([a-z0-9][a-z0-9_-]*):/i.exec(fullKey.trim());
|
|
63
|
+
return match ? match[1] : "";
|
|
64
|
+
}
|
|
65
|
+
|
|
69
66
|
function sanitizeAssistantContentBlocks(content) {
|
|
70
67
|
if (typeof content === "string") {
|
|
71
68
|
return stripAllTaggedSpans(content);
|
|
@@ -85,6 +82,14 @@ export function createSessionService(opts = {}) {
|
|
|
85
82
|
const emitDebug = typeof opts.emitDebug === "function" ? opts.emitDebug : () => {};
|
|
86
83
|
const getAgentName =
|
|
87
84
|
typeof opts.getAgentName === "function" ? opts.getAgentName : () => null;
|
|
85
|
+
const getAgentDisplayName =
|
|
86
|
+
typeof opts.getAgentDisplayName === "function"
|
|
87
|
+
? opts.getAgentDisplayName
|
|
88
|
+
: () => null;
|
|
89
|
+
const getDefaultAgentId =
|
|
90
|
+
typeof opts.getDefaultAgentId === "function"
|
|
91
|
+
? opts.getDefaultAgentId
|
|
92
|
+
: () => "";
|
|
88
93
|
const isUpstreamConnected =
|
|
89
94
|
typeof opts.isUpstreamConnected === "function"
|
|
90
95
|
? opts.isUpstreamConnected
|
|
@@ -108,9 +113,8 @@ export function createSessionService(opts = {}) {
|
|
|
108
113
|
? opts.isPinnedFirstUserMessageKey
|
|
109
114
|
: null;
|
|
110
115
|
|
|
111
|
-
/** Current session key. Generated on first use. */
|
|
112
116
|
let currentSessionKey = null;
|
|
113
|
-
|
|
117
|
+
|
|
114
118
|
let pendingSessionListKey = null;
|
|
115
119
|
let lastGeneratedSessionTimestamp = 0;
|
|
116
120
|
const DEFAULT_SESSION_KEY_PREFIX =
|
|
@@ -127,68 +131,54 @@ export function createSessionService(opts = {}) {
|
|
|
127
131
|
(prefix) => String(prefix || "").toLowerCase(),
|
|
128
132
|
);
|
|
129
133
|
|
|
130
|
-
/** Maximum number of sessions to fetch. */
|
|
131
|
-
// Default raised to 100 so the WebUI Sessions panel can show a full
|
|
132
|
-
// scrollable history without being chopped to 10 most-recent.
|
|
133
134
|
const sessionLimit = opts.sessionLimit || 100;
|
|
134
|
-
|
|
135
|
+
|
|
135
136
|
const persistFirstUserMessages = opts.persistFirstUserMessages !== false;
|
|
136
|
-
|
|
137
|
-
* Strict mode by default: only use first real downstream sends for session preview labels.
|
|
138
|
-
* Set to false to re-enable legacy chat.history/fallback extraction.
|
|
139
|
-
*/
|
|
137
|
+
|
|
140
138
|
const strictFirstUserMessage = opts.strictFirstUserMessage !== false;
|
|
141
|
-
|
|
139
|
+
|
|
142
140
|
const firstUserMessageCachePath = resolveSessionFirstUserMessageCachePath(
|
|
143
141
|
opts.stateDir,
|
|
144
142
|
);
|
|
145
143
|
|
|
146
|
-
/** TTL for cached sessions.list payloads. */
|
|
147
144
|
const sessionCacheTtlMs =
|
|
148
145
|
Number.isFinite(opts.sessionCacheTtlMs) && opts.sessionCacheTtlMs > 0
|
|
149
146
|
? Math.floor(opts.sessionCacheTtlMs)
|
|
150
147
|
: 5000;
|
|
151
|
-
|
|
148
|
+
|
|
152
149
|
let cachedSessions = null;
|
|
153
|
-
|
|
150
|
+
|
|
154
151
|
let cachedSessionsFetchedAt = 0;
|
|
155
|
-
|
|
152
|
+
|
|
156
153
|
let inFlightSessionsFetch = null;
|
|
157
154
|
|
|
158
|
-
/** Last-known session model config per session key. */
|
|
159
155
|
const sessionModelConfigCache = new Map();
|
|
160
|
-
|
|
156
|
+
|
|
161
157
|
const pendingInitialConfigSessionKeys = new Set();
|
|
162
|
-
|
|
158
|
+
|
|
163
159
|
const firstUserMessageCache = new Map();
|
|
164
160
|
const firstUserMessageCacheLimit = Math.max(64, sessionLimit * 8);
|
|
165
|
-
|
|
161
|
+
|
|
166
162
|
const firstSentUserMessageBySession = loadFirstSentUserMessageCache();
|
|
167
163
|
|
|
168
|
-
/** Path for session title cache file. */
|
|
169
164
|
const sessionTitleCachePath = resolveSessionTitleCachePath(opts.stateDir);
|
|
170
|
-
|
|
165
|
+
|
|
171
166
|
const sessionTitleByKey = loadSessionTitleCache();
|
|
172
|
-
|
|
167
|
+
|
|
173
168
|
const neuralSessionNamesEnabledByKey = new Map();
|
|
174
|
-
|
|
175
|
-
* The frozen start-state persists to stateDir so a relay restart can't lose it
|
|
176
|
-
* (the Channel-1 snapshot persists too — see stable-prompt-snapshot). */
|
|
169
|
+
|
|
177
170
|
const displayToggleTracker = createDisplayToggleTracker({ stateDir: opts.stateDir });
|
|
178
|
-
|
|
179
|
-
* here (alongside the title record + toggle tracker) so a logical session
|
|
180
|
-
* reset clears all per-session distiller state in one place. */
|
|
171
|
+
|
|
181
172
|
const distillerBudget = createDistillerBudget({});
|
|
182
173
|
|
|
183
|
-
/** Path for session pin metadata cache file. */
|
|
184
174
|
const sessionPinCachePath = resolveSessionPinCachePath(opts.stateDir);
|
|
185
|
-
|
|
175
|
+
|
|
186
176
|
const sessionPinByKey = loadSessionPinCache();
|
|
187
177
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
178
|
+
const sessionAgentCachePath = resolveSessionAgentCachePath(opts.stateDir);
|
|
179
|
+
|
|
180
|
+
const sessionAgentByKey = loadSessionAgentCache();
|
|
181
|
+
|
|
192
182
|
function generateSessionKey(rawPrefix = DEFAULT_SESSION_KEY_PREFIX) {
|
|
193
183
|
const effectivePrefix =
|
|
194
184
|
typeof rawPrefix === "string" && rawPrefix.trim()
|
|
@@ -203,10 +193,6 @@ export function createSessionService(opts = {}) {
|
|
|
203
193
|
return `${effectivePrefix}${nextTimestamp}`;
|
|
204
194
|
}
|
|
205
195
|
|
|
206
|
-
/**
|
|
207
|
-
* Get or create the current session key.
|
|
208
|
-
* @returns {string}
|
|
209
|
-
*/
|
|
210
196
|
function ensureSessionKey() {
|
|
211
197
|
if (!currentSessionKey) {
|
|
212
198
|
currentSessionKey = generateSessionKey();
|
|
@@ -267,8 +253,6 @@ export function createSessionService(opts = {}) {
|
|
|
267
253
|
return "off";
|
|
268
254
|
}
|
|
269
255
|
|
|
270
|
-
// Preserves all four gateway values — "ask" is the alias of "on" and must
|
|
271
|
-
// not be collapsed, or snapshot read-back would lie about /elevated ask.
|
|
272
256
|
function normalizeElevatedLevel(raw) {
|
|
273
257
|
if (typeof raw !== "string") return "off";
|
|
274
258
|
const normalized = raw.trim().toLowerCase();
|
|
@@ -312,6 +296,8 @@ export function createSessionService(opts = {}) {
|
|
|
312
296
|
verboseLevel: normalizeVerboseLevel(row && row.verboseLevel),
|
|
313
297
|
fastMode: !!(row && row.fastMode === true),
|
|
314
298
|
elevatedLevel: normalizeElevatedLevel(row && row.elevatedLevel),
|
|
299
|
+
|
|
300
|
+
agentId: sessionAgentOverrideId(sessionKey),
|
|
315
301
|
};
|
|
316
302
|
}
|
|
317
303
|
|
|
@@ -320,7 +306,7 @@ export function createSessionService(opts = {}) {
|
|
|
320
306
|
search,
|
|
321
307
|
includeGlobal: false,
|
|
322
308
|
includeUnknown: false,
|
|
323
|
-
limit:
|
|
309
|
+
limit: sessionLimit,
|
|
324
310
|
});
|
|
325
311
|
}
|
|
326
312
|
|
|
@@ -339,7 +325,7 @@ export function createSessionService(opts = {}) {
|
|
|
339
325
|
return resolved.key.trim();
|
|
340
326
|
}
|
|
341
327
|
} catch {
|
|
342
|
-
|
|
328
|
+
|
|
343
329
|
}
|
|
344
330
|
return sessionKey;
|
|
345
331
|
}
|
|
@@ -433,6 +419,8 @@ export function createSessionService(opts = {}) {
|
|
|
433
419
|
patch && Object.prototype.hasOwnProperty.call(patch, "elevatedLevel")
|
|
434
420
|
? normalizeElevatedLevel(patch.elevatedLevel)
|
|
435
421
|
: base.elevatedLevel,
|
|
422
|
+
|
|
423
|
+
agentId: sessionAgentOverrideId(sessionKey),
|
|
436
424
|
};
|
|
437
425
|
sessionModelConfigCache.set(sessionKey, config);
|
|
438
426
|
return config;
|
|
@@ -548,11 +536,6 @@ export function createSessionService(opts = {}) {
|
|
|
548
536
|
return setSessionModelConfig(ensureSessionKey(), patch);
|
|
549
537
|
}
|
|
550
538
|
|
|
551
|
-
/**
|
|
552
|
-
* Fetch the list of OcuClaw sessions from OpenClaw.
|
|
553
|
-
* Filters to OcuClaw session key prefix, sorted by updatedAt descending.
|
|
554
|
-
* @returns {Promise<Array<{key: string, updatedAt: number, preview: string, firstUserMessage: string, title: string|null, pinned: boolean, pinnedAtMs: number|null}>>}
|
|
555
|
-
*/
|
|
556
539
|
async function getSessions() {
|
|
557
540
|
if (cachedSessions && Date.now() - cachedSessionsFetchedAt < sessionCacheTtlMs) {
|
|
558
541
|
return cachedSessions;
|
|
@@ -588,6 +571,7 @@ export function createSessionService(opts = {}) {
|
|
|
588
571
|
row.messages,
|
|
589
572
|
);
|
|
590
573
|
const pinMeta = getSessionPin(key);
|
|
574
|
+
const agentFields = resolveSessionAgentFields(key, row.key);
|
|
591
575
|
return {
|
|
592
576
|
key,
|
|
593
577
|
updatedAt,
|
|
@@ -600,6 +584,8 @@ export function createSessionService(opts = {}) {
|
|
|
600
584
|
title: resolveRowTitle(key, row),
|
|
601
585
|
pinned: pinMeta.pinned,
|
|
602
586
|
pinnedAtMs: pinMeta.pinnedAtMs,
|
|
587
|
+
agentId: agentFields.agentId,
|
|
588
|
+
agentName: agentFields.agentName,
|
|
603
589
|
};
|
|
604
590
|
}),
|
|
605
591
|
);
|
|
@@ -623,6 +609,10 @@ export function createSessionService(opts = {}) {
|
|
|
623
609
|
[],
|
|
624
610
|
);
|
|
625
611
|
const pinMeta = getSessionPin(pendingSessionListKey);
|
|
612
|
+
const agentFields = resolveSessionAgentFields(
|
|
613
|
+
pendingSessionListKey,
|
|
614
|
+
pendingSessionListKey,
|
|
615
|
+
);
|
|
626
616
|
sessions.unshift({
|
|
627
617
|
key: pendingSessionListKey,
|
|
628
618
|
updatedAt,
|
|
@@ -631,6 +621,8 @@ export function createSessionService(opts = {}) {
|
|
|
631
621
|
title: resolveRowTitle(pendingSessionListKey, null),
|
|
632
622
|
pinned: pinMeta.pinned,
|
|
633
623
|
pinnedAtMs: pinMeta.pinnedAtMs,
|
|
624
|
+
agentId: agentFields.agentId,
|
|
625
|
+
agentName: agentFields.agentName,
|
|
634
626
|
});
|
|
635
627
|
}
|
|
636
628
|
}
|
|
@@ -696,6 +688,7 @@ export function createSessionService(opts = {}) {
|
|
|
696
688
|
fallbackMessages,
|
|
697
689
|
);
|
|
698
690
|
const pinMeta = getSessionPin(key);
|
|
691
|
+
const agentFields = resolveSessionAgentFields(key, row.key || sessionKey);
|
|
699
692
|
sessions.push({
|
|
700
693
|
key,
|
|
701
694
|
updatedAt,
|
|
@@ -708,18 +701,22 @@ export function createSessionService(opts = {}) {
|
|
|
708
701
|
title: resolveRowTitle(key, row),
|
|
709
702
|
pinned: pinMeta.pinned,
|
|
710
703
|
pinnedAtMs: pinMeta.pinnedAtMs,
|
|
704
|
+
agentId: agentFields.agentId,
|
|
705
|
+
agentName: agentFields.agentName,
|
|
711
706
|
});
|
|
712
707
|
}
|
|
713
708
|
|
|
714
709
|
return sessions;
|
|
715
710
|
}
|
|
716
711
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
712
|
+
function resolveSessionAgentFields(shortKey, fullKey) {
|
|
713
|
+
const agentId = getSessionAgentId(shortKey, fullKey);
|
|
714
|
+
if (!agentId) {
|
|
715
|
+
return { agentId: null, agentName: null };
|
|
716
|
+
}
|
|
717
|
+
return { agentId, agentName: getAgentDisplayName(agentId) || agentId };
|
|
718
|
+
}
|
|
719
|
+
|
|
723
720
|
function extractShortKey(fullKey) {
|
|
724
721
|
if (typeof fullKey !== "string") return "";
|
|
725
722
|
const fullKeyLower = fullKey.toLowerCase();
|
|
@@ -733,12 +730,6 @@ export function createSessionService(opts = {}) {
|
|
|
733
730
|
return prefixIndex >= 0 ? fullKey.slice(prefixIndex) : fullKey;
|
|
734
731
|
}
|
|
735
732
|
|
|
736
|
-
/**
|
|
737
|
-
* Check whether a full/canonical session key belongs to a supported
|
|
738
|
-
* OcuClaw session namespace.
|
|
739
|
-
* @param {string} key
|
|
740
|
-
* @returns {boolean}
|
|
741
|
-
*/
|
|
742
733
|
function hasSupportedSessionKeyPrefix(key) {
|
|
743
734
|
if (typeof key !== "string" || key.length === 0) return false;
|
|
744
735
|
const keyLower = key.toLowerCase();
|
|
@@ -759,12 +750,6 @@ export function createSessionService(opts = {}) {
|
|
|
759
750
|
return left.toLowerCase() === right.toLowerCase();
|
|
760
751
|
}
|
|
761
752
|
|
|
762
|
-
/**
|
|
763
|
-
* Best-effort timestamp extraction from session key suffix.
|
|
764
|
-
* "ocuclaw:1739500000000" -> 1739500000000
|
|
765
|
-
* @param {string} sessionKey
|
|
766
|
-
* @returns {number}
|
|
767
|
-
*/
|
|
768
753
|
function extractSessionTimestampFromKey(sessionKey) {
|
|
769
754
|
if (typeof sessionKey !== "string") return 0;
|
|
770
755
|
const idx = sessionKey.lastIndexOf(":");
|
|
@@ -773,11 +758,6 @@ export function createSessionService(opts = {}) {
|
|
|
773
758
|
return Number.isFinite(maybeTs) && maybeTs > 0 ? maybeTs : 0;
|
|
774
759
|
}
|
|
775
760
|
|
|
776
|
-
/**
|
|
777
|
-
* Extract a preview string from a session's messages array.
|
|
778
|
-
* @param {Array} messages - Last N messages from sessions.list
|
|
779
|
-
* @returns {string}
|
|
780
|
-
*/
|
|
781
761
|
function extractPreview(messages) {
|
|
782
762
|
if (!Array.isArray(messages) || messages.length === 0) return "";
|
|
783
763
|
for (const msg of messages) {
|
|
@@ -791,9 +771,7 @@ export function createSessionService(opts = {}) {
|
|
|
791
771
|
function resolveRowTitle(sessionKey, row) {
|
|
792
772
|
const cached = getSessionTitle(sessionKey);
|
|
793
773
|
if (cached !== null) return cached;
|
|
794
|
-
|
|
795
|
-
// (≤5.27-era docs, session-management-compaction.md §169) used
|
|
796
|
-
// `displayName` — accept both.
|
|
774
|
+
|
|
797
775
|
const rawLabel =
|
|
798
776
|
row && typeof row.label === "string"
|
|
799
777
|
? row.label
|
|
@@ -953,13 +931,85 @@ export function createSessionService(opts = {}) {
|
|
|
953
931
|
return v ?? { pinned: false, pinnedAtMs: null };
|
|
954
932
|
}
|
|
955
933
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
934
|
+
function loadSessionAgentCache() {
|
|
935
|
+
if (!sessionAgentCachePath) return new Map();
|
|
936
|
+
try {
|
|
937
|
+
if (!fs.existsSync(sessionAgentCachePath)) return new Map();
|
|
938
|
+
const raw = fs.readFileSync(sessionAgentCachePath, "utf8");
|
|
939
|
+
const parsed = JSON.parse(raw);
|
|
940
|
+
const out = new Map();
|
|
941
|
+
for (const [key, value] of Object.entries(parsed ?? {})) {
|
|
942
|
+
if (typeof value === "string" && value.trim()) {
|
|
943
|
+
out.set(key, value.trim());
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return out;
|
|
947
|
+
} catch {
|
|
948
|
+
return new Map();
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function persistSessionAgentCache() {
|
|
953
|
+
if (!sessionAgentCachePath) return;
|
|
954
|
+
try {
|
|
955
|
+
fs.mkdirSync(path.dirname(sessionAgentCachePath), { recursive: true });
|
|
956
|
+
const obj = {};
|
|
957
|
+
for (const [key, value] of sessionAgentByKey.entries()) {
|
|
958
|
+
obj[key] = value;
|
|
959
|
+
}
|
|
960
|
+
fs.writeFileSync(sessionAgentCachePath, JSON.stringify(obj), "utf8");
|
|
961
|
+
} catch (err) {
|
|
962
|
+
logger.error(
|
|
963
|
+
`[relay] Failed to persist session agent cache: ${err.message}`,
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function explicitSessionAgentId(sessionKey, fullKey) {
|
|
969
|
+
const override = sessionAgentByKey.get(sessionKey);
|
|
970
|
+
if (typeof override === "string" && override.trim()) {
|
|
971
|
+
return override.trim();
|
|
972
|
+
}
|
|
973
|
+
return deriveAgentIdFromFullKey(fullKey || sessionKey) || "";
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function sessionAgentOverrideId(sessionKey) {
|
|
977
|
+
const override = sessionAgentByKey.get(sessionKey);
|
|
978
|
+
return typeof override === "string" && override.trim()
|
|
979
|
+
? override.trim()
|
|
980
|
+
: "";
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function getSessionAgentId(sessionKey, fullKey) {
|
|
984
|
+
const explicit = explicitSessionAgentId(sessionKey, fullKey);
|
|
985
|
+
if (explicit) {
|
|
986
|
+
return explicit;
|
|
987
|
+
}
|
|
988
|
+
const fallback = getDefaultAgentId();
|
|
989
|
+
return typeof fallback === "string" && fallback.trim()
|
|
990
|
+
? fallback.trim()
|
|
991
|
+
: "";
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function hasExplicitSessionAgent(sessionKey, fullKey) {
|
|
995
|
+
return explicitSessionAgentId(sessionKey, fullKey) !== "";
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function setSessionAgentId(sessionKey, agentId) {
|
|
999
|
+
if (typeof sessionKey !== "string" || !sessionKey.trim()) {
|
|
1000
|
+
return { ok: false, reason: "invalid" };
|
|
1001
|
+
}
|
|
1002
|
+
const normalized = typeof agentId === "string" ? agentId.trim() : "";
|
|
1003
|
+
if (normalized) {
|
|
1004
|
+
sessionAgentByKey.set(sessionKey, normalized);
|
|
1005
|
+
} else {
|
|
1006
|
+
sessionAgentByKey.delete(sessionKey);
|
|
1007
|
+
}
|
|
1008
|
+
persistSessionAgentCache();
|
|
1009
|
+
invalidateSessionsCache();
|
|
1010
|
+
return { ok: true };
|
|
1011
|
+
}
|
|
1012
|
+
|
|
963
1013
|
function setSessionPinned(kind, sessionKey, pinned) {
|
|
964
1014
|
if (!sessionKey || (kind !== "ocuclaw" && kind !== "evenai")) {
|
|
965
1015
|
return { ok: false, reason: "invalid" };
|
|
@@ -979,13 +1029,6 @@ export function createSessionService(opts = {}) {
|
|
|
979
1029
|
return { ok: true };
|
|
980
1030
|
}
|
|
981
1031
|
|
|
982
|
-
/**
|
|
983
|
-
* Delete one or more sessions, sequentially.
|
|
984
|
-
* Partial failures do not abort the batch.
|
|
985
|
-
* @param {"ocuclaw"|"evenai"} kind
|
|
986
|
-
* @param {string[]} sessionKeys
|
|
987
|
-
* @returns {Promise<{ deleted: string[], failed: Array<{ key: string, reason: string }> }>}
|
|
988
|
-
*/
|
|
989
1032
|
async function deleteSessions(kind, sessionKeys) {
|
|
990
1033
|
const deleted = [];
|
|
991
1034
|
const failed = [];
|
|
@@ -1007,14 +1050,6 @@ export function createSessionService(opts = {}) {
|
|
|
1007
1050
|
return { deleted, failed };
|
|
1008
1051
|
}
|
|
1009
1052
|
|
|
1010
|
-
/**
|
|
1011
|
-
* Deep transcript search across all OcuClaw sessions returned by getSessions().
|
|
1012
|
-
* Returns at most `maxSnippets` snippets matching the query (case-insensitive),
|
|
1013
|
-
* each with `before`/`match`/`after` slices so the UI can highlight the hit.
|
|
1014
|
-
* @param {string} kind "ocuclaw" | "evenai"
|
|
1015
|
-
* @param {string} query
|
|
1016
|
-
* @returns {Promise<{snippets: Array<{sessionKey: string, role: string, updatedAtMs: number, before: string, match: string, after: string}>, truncated: boolean}>}
|
|
1017
|
-
*/
|
|
1018
1053
|
async function searchTranscripts(kind, query) {
|
|
1019
1054
|
const needle = (typeof query === "string" ? query.trim() : "").toLowerCase();
|
|
1020
1055
|
if (!needle) return { snippets: [], truncated: false };
|
|
@@ -1083,9 +1118,7 @@ export function createSessionService(opts = {}) {
|
|
|
1083
1118
|
}
|
|
1084
1119
|
|
|
1085
1120
|
async function deleteSingleSession(kind, key) {
|
|
1086
|
-
|
|
1087
|
-
// Resolve to a canonical key first so prefixes like "agent:main:" are applied
|
|
1088
|
-
// when the gateway expects them.
|
|
1121
|
+
|
|
1089
1122
|
const canonicalKey = await resolveSessionCanonicalKey(key);
|
|
1090
1123
|
await gatewayBridge.request("sessions.delete", {
|
|
1091
1124
|
key: canonicalKey,
|
|
@@ -1094,12 +1127,6 @@ export function createSessionService(opts = {}) {
|
|
|
1094
1127
|
});
|
|
1095
1128
|
}
|
|
1096
1129
|
|
|
1097
|
-
/**
|
|
1098
|
-
* Switch first if the active session is in the batch; then delete.
|
|
1099
|
-
* Used when the UI sets `switchBeforeDelete=true`.
|
|
1100
|
-
* @param {"ocuclaw"|"evenai"} kind
|
|
1101
|
-
* @param {string[]} sessionKeys
|
|
1102
|
-
*/
|
|
1103
1130
|
async function switchAndDeleteSessions(kind, sessionKeys) {
|
|
1104
1131
|
if (
|
|
1105
1132
|
kind === "ocuclaw" &&
|
|
@@ -1111,11 +1138,6 @@ export function createSessionService(opts = {}) {
|
|
|
1111
1138
|
return deleteSessions(kind, sessionKeys);
|
|
1112
1139
|
}
|
|
1113
1140
|
|
|
1114
|
-
/**
|
|
1115
|
-
* Re-fetch + broadcast the sessions snapshot for a given kind.
|
|
1116
|
-
* Used by handlers after pin/delete writes.
|
|
1117
|
-
* @param {"ocuclaw"|"evenai"} kind
|
|
1118
|
-
*/
|
|
1119
1141
|
async function broadcastSessionsForKind(kind) {
|
|
1120
1142
|
invalidateSessionsCache();
|
|
1121
1143
|
if (kind === "ocuclaw" && typeof opts.broadcastSessions === "function") {
|
|
@@ -1185,17 +1207,6 @@ export function createSessionService(opts = {}) {
|
|
|
1185
1207
|
}
|
|
1186
1208
|
}
|
|
1187
1209
|
|
|
1188
|
-
// Coalesced async persistence for the first-user-message cache. Mirrors the
|
|
1189
|
-
// ocuclaw-settings-store writeInFlight/pendingWrite shape (commit 6285bc24)
|
|
1190
|
-
// so the SYNC fs.mkdirSync + fs.writeFileSync no longer block the hot send
|
|
1191
|
-
// path on every first user message per session.
|
|
1192
|
-
//
|
|
1193
|
-
// The write serializes the WHOLE in-memory map, so it is a full-snapshot
|
|
1194
|
-
// replace (not a disk read-modify-write). Concurrent fire-and-forget writes
|
|
1195
|
-
// could therefore lose updates (an older snapshot landing after a newer one).
|
|
1196
|
-
// We coalesce: at most one write in flight; any schedule while a write is in
|
|
1197
|
-
// flight marks the cache dirty, and the in-flight write re-runs once it
|
|
1198
|
-
// settles — so the final on-disk file always reflects the latest map.
|
|
1199
1210
|
let firstUserCacheWriteInFlight = false;
|
|
1200
1211
|
let firstUserCacheDirty = false;
|
|
1201
1212
|
let firstUserCacheFlushPromise = null;
|
|
@@ -1235,7 +1246,7 @@ export function createSessionService(opts = {}) {
|
|
|
1235
1246
|
return;
|
|
1236
1247
|
}
|
|
1237
1248
|
if (!firstUserCacheDirty) {
|
|
1238
|
-
|
|
1249
|
+
|
|
1239
1250
|
if (firstUserCacheFlushResolve) {
|
|
1240
1251
|
const resolve = firstUserCacheFlushResolve;
|
|
1241
1252
|
firstUserCacheFlushResolve = null;
|
|
@@ -1248,8 +1259,7 @@ export function createSessionService(opts = {}) {
|
|
|
1248
1259
|
firstUserCacheWriteInFlight = true;
|
|
1249
1260
|
writeFirstSentUserMessageCacheToDisk().finally(() => {
|
|
1250
1261
|
firstUserCacheWriteInFlight = false;
|
|
1251
|
-
|
|
1252
|
-
// settles the flush gate once the map and disk agree.
|
|
1262
|
+
|
|
1253
1263
|
runFirstSentUserMessageCacheWrite();
|
|
1254
1264
|
});
|
|
1255
1265
|
}
|
|
@@ -1260,9 +1270,6 @@ export function createSessionService(opts = {}) {
|
|
|
1260
1270
|
runFirstSentUserMessageCacheWrite();
|
|
1261
1271
|
}
|
|
1262
1272
|
|
|
1263
|
-
// Awaitable flush: resolves once no write is in flight AND no dirty mark is
|
|
1264
|
-
// pending (the on-disk file reflects the latest in-memory map). Used by tests
|
|
1265
|
-
// after rapid sends and available for graceful shutdown.
|
|
1266
1273
|
function flushFirstSentUserMessageCache() {
|
|
1267
1274
|
if (!persistFirstUserMessages || !firstUserMessageCachePath) {
|
|
1268
1275
|
return Promise.resolve();
|
|
@@ -1316,7 +1323,7 @@ export function createSessionService(opts = {}) {
|
|
|
1316
1323
|
return { ok: false, code: "invalid_title" };
|
|
1317
1324
|
}
|
|
1318
1325
|
const trimmed = title.trim();
|
|
1319
|
-
|
|
1326
|
+
|
|
1320
1327
|
const origin =
|
|
1321
1328
|
opts && typeof opts.origin === "string" && opts.origin
|
|
1322
1329
|
? opts.origin
|
|
@@ -1347,7 +1354,7 @@ export function createSessionService(opts = {}) {
|
|
|
1347
1354
|
{ sessionKey },
|
|
1348
1355
|
() => ({ sessionKey, title: trimmed, replaced, userSet: !!nextUserSet, origin }),
|
|
1349
1356
|
);
|
|
1350
|
-
|
|
1357
|
+
|
|
1351
1358
|
if (!isUpstreamConnected()) {
|
|
1352
1359
|
emitDebug(
|
|
1353
1360
|
"relay.session",
|
|
@@ -1360,8 +1367,7 @@ export function createSessionService(opts = {}) {
|
|
|
1360
1367
|
if (isUpstreamConnected()) {
|
|
1361
1368
|
resolveSessionCanonicalKey(sessionKey)
|
|
1362
1369
|
.then((canonicalKey) =>
|
|
1363
|
-
|
|
1364
|
-
// (5.27-era `displayName` is rejected as an unexpected property).
|
|
1370
|
+
|
|
1365
1371
|
gatewayBridge.request("sessions.patch", {
|
|
1366
1372
|
key: canonicalKey,
|
|
1367
1373
|
label: trimmed,
|
|
@@ -1428,12 +1434,7 @@ export function createSessionService(opts = {}) {
|
|
|
1428
1434
|
distillerBudget.clear(sessionKey);
|
|
1429
1435
|
}
|
|
1430
1436
|
}
|
|
1431
|
-
|
|
1432
|
-
// name. setSessionTitle mirrors the title to the upstream session displayName,
|
|
1433
|
-
// and session-list rendering falls back to that displayName when the local
|
|
1434
|
-
// record is gone — so a local-only delete would let the old title reappear on
|
|
1435
|
-
// the next sessions refresh. deleteSessions removes it for genuine deletes;
|
|
1436
|
-
// this is for a reused-key logical reset (/new, /reset).
|
|
1437
|
+
|
|
1437
1438
|
function clearSessionTitle(sessionKey) {
|
|
1438
1439
|
if (typeof sessionKey !== "string" || !sessionKey.trim()) return;
|
|
1439
1440
|
const hadTitle = sessionTitleByKey.delete(sessionKey);
|
|
@@ -1457,21 +1458,12 @@ export function createSessionService(opts = {}) {
|
|
|
1457
1458
|
}
|
|
1458
1459
|
}
|
|
1459
1460
|
|
|
1460
|
-
// Clear ALL per-session state keyed to a conversation that must not bleed into
|
|
1461
|
-
// a fresh conversation reusing the same session key (/new, /reset). Centralized
|
|
1462
|
-
// so reset paths can't miss a piece (title + upstream name, toggle states,
|
|
1463
|
-
// distiller budget, the first-user-message marker the distiller gate reads, and
|
|
1464
|
-
// the per-session feature toggle).
|
|
1465
1461
|
function clearLogicalSessionState(sessionKey) {
|
|
1466
1462
|
if (typeof sessionKey !== "string" || !sessionKey.trim()) return;
|
|
1467
1463
|
clearSessionTitle(sessionKey);
|
|
1468
1464
|
displayToggleTracker.clear(sessionKey);
|
|
1469
1465
|
distillerBudget.clear(sessionKey);
|
|
1470
|
-
|
|
1471
|
-
// the deletion — recordFirstSentUserMessage writes the marker to disk, so a
|
|
1472
|
-
// local-only delete would let a relay/plugin restart reload a stale
|
|
1473
|
-
// "user already spoke" marker for the reused key (the distiller gate reads
|
|
1474
|
-
// this, and the session-list preview reads the derived cache).
|
|
1466
|
+
|
|
1475
1467
|
const hadMarker = firstSentUserMessageBySession.delete(sessionKey);
|
|
1476
1468
|
firstUserMessageCache.delete(sessionKey);
|
|
1477
1469
|
if (hadMarker) persistFirstSentUserMessageCache();
|
|
@@ -1613,11 +1605,6 @@ export function createSessionService(opts = {}) {
|
|
|
1613
1605
|
}
|
|
1614
1606
|
}
|
|
1615
1607
|
|
|
1616
|
-
/**
|
|
1617
|
-
* Switch to a different session: load its history and return pages.
|
|
1618
|
-
* @param {string} sessionKey
|
|
1619
|
-
* @returns {Promise<Array>} Pages array
|
|
1620
|
-
*/
|
|
1621
1608
|
async function switchToSession(sessionKey, opts = {}) {
|
|
1622
1609
|
const markPendingSessionList =
|
|
1623
1610
|
opts.markPendingSessionList === true &&
|
|
@@ -1673,10 +1660,6 @@ export function createSessionService(opts = {}) {
|
|
|
1673
1660
|
return pages;
|
|
1674
1661
|
}
|
|
1675
1662
|
|
|
1676
|
-
/**
|
|
1677
|
-
* Create a new session with a fresh key.
|
|
1678
|
-
* @returns {Promise<{sessionKey: string, pages: Array}>}
|
|
1679
|
-
*/
|
|
1680
1663
|
async function newSession(opts = {}) {
|
|
1681
1664
|
const sendResetCommand = opts.sendResetCommand !== false;
|
|
1682
1665
|
const sessionKey = generateSessionKey();
|
|
@@ -1723,10 +1706,6 @@ export function createSessionService(opts = {}) {
|
|
|
1723
1706
|
return extractShortKey(trimmed).toLowerCase();
|
|
1724
1707
|
}
|
|
1725
1708
|
|
|
1726
|
-
/**
|
|
1727
|
-
* Check if an event's session key matches the current glasses session.
|
|
1728
|
-
* Events without a sessionKey default to "main".
|
|
1729
|
-
*/
|
|
1730
1709
|
function isCurrentSession(eventSessionKey) {
|
|
1731
1710
|
const eventKey = normalizeSessionKeyForCompare(eventSessionKey || "main");
|
|
1732
1711
|
const currentKey = normalizeSessionKeyForCompare(ensureSessionKey());
|
|
@@ -1780,6 +1759,9 @@ export function createSessionService(opts = {}) {
|
|
|
1780
1759
|
isCurrentSession,
|
|
1781
1760
|
setSessionPinned,
|
|
1782
1761
|
getSessionPin,
|
|
1762
|
+
getSessionAgentId,
|
|
1763
|
+
setSessionAgentId,
|
|
1764
|
+
hasExplicitSessionAgent,
|
|
1783
1765
|
deleteSessions,
|
|
1784
1766
|
switchAndDeleteSessions,
|
|
1785
1767
|
broadcastSessionsForKind,
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-session distiller budget. SKIPs are free; only consecutive errors and a
|
|
3
|
-
* total untitled-turn ceiling bound attempts; an applied title ends attempts.
|
|
4
|
-
*/
|
|
5
1
|
export function createDistillerBudget(opts = {}) {
|
|
6
2
|
const maxErr = Number.isFinite(opts.maxConsecutiveErrors) ? opts.maxConsecutiveErrors : 3;
|
|
7
3
|
const ceiling = Number.isFinite(opts.untitledTurnCeiling) ? opts.untitledTurnCeiling : 25;
|
|
8
|
-
|
|
4
|
+
|
|
9
5
|
const byKey = new Map();
|
|
10
6
|
|
|
11
7
|
function get(k) {
|