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,6 +8,7 @@ import { parseTaggedSpans } from "../domain/tagged-span-parser.js";
|
|
|
8
8
|
import { EMOJI_TAG_FAMILY_CONFIG } from "../domain/neural-emoji-reactor-tag-config.js";
|
|
9
9
|
import { PACE_TAG_FAMILY_CONFIG } from "../domain/neural-pace-modulator-tag-config.js";
|
|
10
10
|
import { createSessionContextService } from "./session-context-service.js";
|
|
11
|
+
import { DISTILLER_SESSION_PREFIX } from "./session-title-distiller-helpers.js";
|
|
11
12
|
|
|
12
13
|
function normalizeLogger(logger) {
|
|
13
14
|
if (!logger || typeof logger !== "object") {
|
|
@@ -25,13 +26,26 @@ function normalizeLogger(logger) {
|
|
|
25
26
|
const DEFAULT_MODEL_PROVIDER = "anthropic";
|
|
26
27
|
const DEFAULT_MODEL_ID = "claude-opus-4-6";
|
|
27
28
|
const POOL_OUTCOME_FRESHNESS_MS = 10 * 60 * 1000;
|
|
29
|
+
const TITLE_DISTILLER_RUN_ID_PREFIX = "ocuclaw-title-";
|
|
30
|
+
const TITLE_DISTILLER_SESSION_MARKER = ":title-distiller:";
|
|
28
31
|
|
|
29
|
-
// Dropped from 150 ms to 33 ms after Session 3 memoization (StreamingBuffer.displayedText
|
|
30
|
-
// + MessageScreenPageSliceCache) reduced per-frame client-side cost. Session 1's earlier
|
|
31
|
-
// 33 ms run produced ~15 s of long-tasks per ~10 s of streaming and a 10.8 s probe gap;
|
|
32
|
-
// the Session-3 re-validation must confirm those signals stay near the 150 ms baseline.
|
|
33
32
|
export const STREAMING_REBROADCAST_THROTTLE_MS = 33;
|
|
34
33
|
|
|
34
|
+
function normalizeStreamingToken(raw) {
|
|
35
|
+
return typeof raw === "string" && raw.trim() ? raw.trim() : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isTitleDistillerStreamingEvent(data) {
|
|
39
|
+
const runId = normalizeStreamingToken(data && data.runId);
|
|
40
|
+
if (runId && runId.startsWith(TITLE_DISTILLER_RUN_ID_PREFIX)) return true;
|
|
41
|
+
const sessionKey = normalizeStreamingToken(data && data.sessionKey);
|
|
42
|
+
return Boolean(
|
|
43
|
+
sessionKey &&
|
|
44
|
+
(sessionKey.startsWith(DISTILLER_SESSION_PREFIX) ||
|
|
45
|
+
sessionKey.includes(TITLE_DISTILLER_SESSION_MARKER)),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
35
49
|
function fullMessageText(content) {
|
|
36
50
|
if (typeof content === "string") return content;
|
|
37
51
|
if (Array.isArray(content)) {
|
|
@@ -307,6 +321,232 @@ function normalizeSkillsCatalogRows(rows) {
|
|
|
307
321
|
return out;
|
|
308
322
|
}
|
|
309
323
|
|
|
324
|
+
function isMethodNotFoundError(err, message) {
|
|
325
|
+
if (err && typeof err === "object") {
|
|
326
|
+
const code = err.code ?? (err.error && err.error.code);
|
|
327
|
+
if (code === -32601) return true;
|
|
328
|
+
}
|
|
329
|
+
const text = typeof message === "string" ? message.toLowerCase() : "";
|
|
330
|
+
return (
|
|
331
|
+
text.includes("method not found") ||
|
|
332
|
+
text.includes("unknown method") ||
|
|
333
|
+
text.includes("no such method") ||
|
|
334
|
+
text.includes("not supported")
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function normalizeAgentsCatalogRows(rows) {
|
|
339
|
+
if (!Array.isArray(rows)) return [];
|
|
340
|
+
const out = [];
|
|
341
|
+
const seen = new Set();
|
|
342
|
+
for (const row of rows) {
|
|
343
|
+
if (!row || typeof row !== "object") continue;
|
|
344
|
+
const id = typeof row.id === "string" ? row.id.trim() : "";
|
|
345
|
+
if (!id) continue;
|
|
346
|
+
if (seen.has(id)) continue;
|
|
347
|
+
seen.add(id);
|
|
348
|
+
const identity =
|
|
349
|
+
row.identity && typeof row.identity === "object" ? row.identity : {};
|
|
350
|
+
const identityName =
|
|
351
|
+
typeof identity.name === "string" && identity.name.trim()
|
|
352
|
+
? identity.name.trim()
|
|
353
|
+
: "";
|
|
354
|
+
const baseName =
|
|
355
|
+
typeof row.name === "string" && row.name.trim() ? row.name.trim() : "";
|
|
356
|
+
const name = identityName || baseName || id;
|
|
357
|
+
const emoji =
|
|
358
|
+
typeof identity.emoji === "string" && identity.emoji.trim()
|
|
359
|
+
? identity.emoji.trim()
|
|
360
|
+
: null;
|
|
361
|
+
const model = row.model && typeof row.model === "object" ? row.model : {};
|
|
362
|
+
const primaryModel =
|
|
363
|
+
typeof model.primary === "string" && model.primary.trim()
|
|
364
|
+
? model.primary.trim()
|
|
365
|
+
: null;
|
|
366
|
+
out.push({ id, name, emoji, primaryModel });
|
|
367
|
+
}
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const WORKSPACE_IDENTITY_FILENAME = "IDENTITY.md";
|
|
372
|
+
|
|
373
|
+
const UPSTREAM_DEFAULT_IDENTITY_NAME = "Assistant";
|
|
374
|
+
|
|
375
|
+
const IDENTITY_PLACEHOLDER_VALUES = new Set([
|
|
376
|
+
"pick something you like",
|
|
377
|
+
"ai? robot? familiar? ghost in the machine? something weirder?",
|
|
378
|
+
"how do you come across? sharp? warm? chaotic? calm?",
|
|
379
|
+
"your signature - pick one that feels right",
|
|
380
|
+
"workspace-relative path, http(s) url, or data uri",
|
|
381
|
+
]);
|
|
382
|
+
|
|
383
|
+
const IDENTITY_LABELS = new Set([
|
|
384
|
+
"name",
|
|
385
|
+
"emoji",
|
|
386
|
+
"creature",
|
|
387
|
+
"vibe",
|
|
388
|
+
"theme",
|
|
389
|
+
"avatar",
|
|
390
|
+
]);
|
|
391
|
+
|
|
392
|
+
function stripIdentityMarkup(value) {
|
|
393
|
+
let s = String(value == null ? "" : value).trim();
|
|
394
|
+
s = s.replace(/^[*_`\s]+|[*_`\s]+$/g, "").trim();
|
|
395
|
+
if (s.startsWith("(") && s.endsWith(")")) s = s.slice(1, -1).trim();
|
|
396
|
+
return s;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function normalizeIdentityLabel(raw) {
|
|
400
|
+
return raw.replace(/[*_`]/g, "").trim().toLowerCase();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function isIdentityPlaceholder(value) {
|
|
404
|
+
const normalized = String(value == null ? "" : value)
|
|
405
|
+
.replace(/[*_`()]/g, " ")
|
|
406
|
+
.replace(/[–—]/g, "-")
|
|
407
|
+
.replace(/\s+/g, " ")
|
|
408
|
+
.trim()
|
|
409
|
+
.toLowerCase();
|
|
410
|
+
return IDENTITY_PLACEHOLDER_VALUES.has(normalized);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function looksLikeIdentityLabelLine(line) {
|
|
414
|
+
const cleaned = String(line).trim().replace(/^[-*]\s*/, "");
|
|
415
|
+
const colon = cleaned.indexOf(":");
|
|
416
|
+
if (colon === -1) return false;
|
|
417
|
+
return IDENTITY_LABELS.has(normalizeIdentityLabel(cleaned.slice(0, colon)));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const MAX_IDENTITY_NAME = 50;
|
|
421
|
+
const MAX_IDENTITY_EMOJI = 16;
|
|
422
|
+
|
|
423
|
+
function hasMeaningfulIdentityChars(value) {
|
|
424
|
+
return /[A-Za-z]/.test(value) || /[^\x00-\x7F]/.test(value);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function normalizeFallbackEmoji(value) {
|
|
428
|
+
const trimmed = stripIdentityMarkup(value);
|
|
429
|
+
if (!trimmed || trimmed.length > MAX_IDENTITY_EMOJI) return null;
|
|
430
|
+
if (trimmed.includes("/") || trimmed.includes("://")) return null;
|
|
431
|
+
let hasNonAscii = false;
|
|
432
|
+
for (let i = 0; i < trimmed.length; i += 1) {
|
|
433
|
+
if (trimmed.charCodeAt(i) > 127) {
|
|
434
|
+
hasNonAscii = true;
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return hasNonAscii ? trimmed : null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function parseWorkspaceIdentityFallback(content) {
|
|
442
|
+
const result = {};
|
|
443
|
+
if (typeof content !== "string" || !content) return result;
|
|
444
|
+
const lines = content.split(/\r?\n/);
|
|
445
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
446
|
+
const cleaned = lines[i].trim().replace(/^[-*]\s*/, "");
|
|
447
|
+
const colon = cleaned.indexOf(":");
|
|
448
|
+
if (colon === -1) continue;
|
|
449
|
+
const label = normalizeIdentityLabel(cleaned.slice(0, colon));
|
|
450
|
+
if (label !== "name" && label !== "emoji") continue;
|
|
451
|
+
let value = stripIdentityMarkup(cleaned.slice(colon + 1));
|
|
452
|
+
if (!value) {
|
|
453
|
+
|
|
454
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
455
|
+
const peek = lines[j].trim();
|
|
456
|
+
if (!peek) continue;
|
|
457
|
+
if (peek.startsWith("#")) break;
|
|
458
|
+
if (/^(-{3,}|\*{3,}|_{3,})$/.test(peek)) break;
|
|
459
|
+
if (looksLikeIdentityLabelLine(peek)) break;
|
|
460
|
+
value = stripIdentityMarkup(peek);
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (!value || isIdentityPlaceholder(value)) continue;
|
|
465
|
+
if (label === "name") {
|
|
466
|
+
if (!hasMeaningfulIdentityChars(value)) continue;
|
|
467
|
+
if (!result.name) {
|
|
468
|
+
result.name =
|
|
469
|
+
value.length > MAX_IDENTITY_NAME ? value.slice(0, MAX_IDENTITY_NAME) : value;
|
|
470
|
+
}
|
|
471
|
+
} else if (label === "emoji") {
|
|
472
|
+
const normalized = normalizeFallbackEmoji(value);
|
|
473
|
+
if (normalized && !result.emoji) result.emoji = normalized;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function applyIdentityFallback(identity, fallback) {
|
|
480
|
+
const name = identity && identity.name ? identity.name : null;
|
|
481
|
+
const emoji = identity && identity.emoji ? identity.emoji : null;
|
|
482
|
+
if (!fallback || (!fallback.name && !fallback.emoji)) return { name, emoji };
|
|
483
|
+
|
|
484
|
+
const needName = !name || (name === UPSTREAM_DEFAULT_IDENTITY_NAME && !emoji);
|
|
485
|
+
const needEmoji = !emoji;
|
|
486
|
+
return {
|
|
487
|
+
name: needName && fallback.name ? fallback.name : name,
|
|
488
|
+
emoji: needEmoji && fallback.emoji ? fallback.emoji : emoji,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function rawRowNameUnresolved(row) {
|
|
493
|
+
const identity =
|
|
494
|
+
row.identity && typeof row.identity === "object" ? row.identity : null;
|
|
495
|
+
const hasIdentityName =
|
|
496
|
+
identity && typeof identity.name === "string" && identity.name.trim();
|
|
497
|
+
const hasBaseName = typeof row.name === "string" && row.name.trim();
|
|
498
|
+
return !hasIdentityName && !hasBaseName;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function rawRowEmojiUnresolved(row) {
|
|
502
|
+
const identity =
|
|
503
|
+
row.identity && typeof row.identity === "object" ? row.identity : null;
|
|
504
|
+
return !(
|
|
505
|
+
identity &&
|
|
506
|
+
typeof identity.emoji === "string" &&
|
|
507
|
+
identity.emoji.trim()
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function rawRowNeedsIdentityFallback(row) {
|
|
512
|
+
if (!row || typeof row !== "object") return false;
|
|
513
|
+
const id = typeof row.id === "string" ? row.id.trim() : "";
|
|
514
|
+
if (!id) return false;
|
|
515
|
+
return rawRowNameUnresolved(row) || rawRowEmojiUnresolved(row);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function lookupIdentityFallback(fallbackByAgentId, id) {
|
|
519
|
+
if (!fallbackByAgentId) return null;
|
|
520
|
+
if (typeof fallbackByAgentId.get === "function") {
|
|
521
|
+
return fallbackByAgentId.get(id) || null;
|
|
522
|
+
}
|
|
523
|
+
return fallbackByAgentId[id] || null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function overlayRawAgentRowsWithFallback(rows, fallbackByAgentId) {
|
|
527
|
+
if (!Array.isArray(rows)) return [];
|
|
528
|
+
return rows.map((row) => {
|
|
529
|
+
if (!row || typeof row !== "object") return row;
|
|
530
|
+
const id = typeof row.id === "string" ? row.id.trim() : "";
|
|
531
|
+
if (!id) return row;
|
|
532
|
+
const fallback = lookupIdentityFallback(fallbackByAgentId, id);
|
|
533
|
+
if (!fallback || (!fallback.name && !fallback.emoji)) return row;
|
|
534
|
+
const fillName = rawRowNameUnresolved(row) && fallback.name;
|
|
535
|
+
const fillEmoji = rawRowEmojiUnresolved(row) && fallback.emoji;
|
|
536
|
+
if (!fillName && !fillEmoji) return row;
|
|
537
|
+
const identity =
|
|
538
|
+
row.identity && typeof row.identity === "object" ? row.identity : {};
|
|
539
|
+
return {
|
|
540
|
+
...row,
|
|
541
|
+
identity: {
|
|
542
|
+
...identity,
|
|
543
|
+
...(fillName ? { name: fallback.name } : {}),
|
|
544
|
+
...(fillEmoji ? { emoji: fallback.emoji } : {}),
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
310
550
|
export function createUpstreamRuntime(opts = {}) {
|
|
311
551
|
const logger = normalizeLogger(opts.logger);
|
|
312
552
|
const gatewayBridge = opts.gatewayBridge;
|
|
@@ -328,6 +568,10 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
328
568
|
typeof opts.broadcastProviderUsageSnapshot === "function"
|
|
329
569
|
? opts.broadcastProviderUsageSnapshot
|
|
330
570
|
: () => {};
|
|
571
|
+
const broadcastAgentsCatalog =
|
|
572
|
+
typeof opts.broadcastAgentsCatalog === "function"
|
|
573
|
+
? opts.broadcastAgentsCatalog
|
|
574
|
+
: () => {};
|
|
331
575
|
const getCurrentSessionModelConfigSnapshot =
|
|
332
576
|
typeof opts.getCurrentSessionModelConfigSnapshot === "function"
|
|
333
577
|
? opts.getCurrentSessionModelConfigSnapshot
|
|
@@ -343,11 +587,7 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
343
587
|
|
|
344
588
|
const gatewayUrl = typeof opts.gatewayUrl === "string" ? opts.gatewayUrl : null;
|
|
345
589
|
const gatewayToken = typeof opts.gatewayToken === "string" ? opts.gatewayToken : null;
|
|
346
|
-
|
|
347
|
-
// gateway can be >1 MB PNGs; the original 256 KB cap silently dropped them.
|
|
348
|
-
// 4 MB is generous for a one-shot identity broadcast over loopback/tailnet
|
|
349
|
-
// and is still tight enough to keep status frames from ballooning if a
|
|
350
|
-
// pathologically large image is configured.
|
|
590
|
+
|
|
351
591
|
const MAX_AVATAR_DATA_URI_BYTES = 4 * 1024 * 1024;
|
|
352
592
|
|
|
353
593
|
function gatewayHttpOriginFromWsUrl(wsUrl) {
|
|
@@ -374,18 +614,15 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
374
614
|
const fetchAgentAvatar =
|
|
375
615
|
typeof opts.fetchAgentAvatar === "function" ? opts.fetchAgentAvatar : defaultFetchAgentAvatar;
|
|
376
616
|
|
|
377
|
-
/** @type {Map<string, {dataUri: string, hash: string}>} */
|
|
378
617
|
const avatarCache = new Map();
|
|
379
|
-
|
|
618
|
+
|
|
380
619
|
const inFlightAvatarFetches = new Map();
|
|
381
|
-
|
|
620
|
+
|
|
382
621
|
const avatarHashIndex = new Map();
|
|
383
622
|
|
|
384
623
|
async function resolveAgentAvatar(agentId, avatarSource) {
|
|
385
624
|
if (!agentId || typeof avatarSource !== "string" || !avatarSource) return null;
|
|
386
625
|
|
|
387
|
-
// For data: URIs, decode the base64 payload to compute a hash, but skip the
|
|
388
|
-
// gateway fetch entirely. Same dedupe semantics as remote sources.
|
|
389
626
|
if (avatarSource.startsWith("data:")) {
|
|
390
627
|
const cacheKey = `${agentId}|${avatarSource}`;
|
|
391
628
|
const cached = avatarCache.get(cacheKey);
|
|
@@ -495,36 +732,56 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
495
732
|
: 60000;
|
|
496
733
|
|
|
497
734
|
let openclawConnected = false;
|
|
498
|
-
|
|
735
|
+
|
|
499
736
|
let agentIdentity = { name: null, emoji: null, avatarDataUri: null, avatarHash: null };
|
|
500
|
-
|
|
737
|
+
|
|
501
738
|
let cachedModelsCatalog = null;
|
|
502
739
|
let cachedModelsCatalogFetchedAt = 0;
|
|
503
740
|
let cachedModelsCatalogStale = true;
|
|
504
|
-
|
|
741
|
+
|
|
505
742
|
let inFlightModelsCatalogFetch = null;
|
|
506
|
-
|
|
743
|
+
|
|
507
744
|
let cachedSkillsCatalog = null;
|
|
508
745
|
let cachedSkillsCatalogFetchedAt = 0;
|
|
509
746
|
let cachedSkillsCatalogStale = true;
|
|
510
|
-
|
|
747
|
+
|
|
511
748
|
let inFlightSkillsCatalogFetch = null;
|
|
749
|
+
|
|
750
|
+
let cachedAgentsCatalog = null;
|
|
751
|
+
let cachedAgentsCatalogFetchedAt = 0;
|
|
752
|
+
let cachedAgentsCatalogStale = true;
|
|
753
|
+
|
|
754
|
+
let cachedAgentsEnvelope = { defaultId: null, mainKey: null, scope: null };
|
|
755
|
+
|
|
756
|
+
let agentsListUnsupported = false;
|
|
757
|
+
|
|
758
|
+
let inFlightAgentsCatalogFetch = null;
|
|
512
759
|
let cachedProviderUsageSummary = null;
|
|
513
760
|
let cachedProviderUsageFetchedAt = 0;
|
|
514
761
|
let cachedProviderUsageObservedAt = 0;
|
|
515
762
|
let cachedProviderUsageStale = true;
|
|
516
|
-
|
|
763
|
+
|
|
517
764
|
let inFlightProviderUsageFetch = null;
|
|
518
765
|
const providerOutcomeState = new Map();
|
|
519
766
|
const cachedAuthProfileCounts = new Map();
|
|
520
767
|
const upstreamRunPipeline = new Map();
|
|
521
768
|
let streamingThrottleTimer = null;
|
|
522
769
|
let pendingStreaming = null;
|
|
523
|
-
|
|
770
|
+
|
|
524
771
|
let activeTyping = null;
|
|
525
772
|
let bootstrapRefreshTimer = null;
|
|
526
773
|
let bootstrapRefreshNonce = 0;
|
|
527
774
|
|
|
775
|
+
const workspaceIdentityFallbackCache = new Map();
|
|
776
|
+
|
|
777
|
+
const inFlightWorkspaceIdentityFetches = new Map();
|
|
778
|
+
|
|
779
|
+
let lastGatewayIdentity = null;
|
|
780
|
+
|
|
781
|
+
let connectionGeneration = 0;
|
|
782
|
+
|
|
783
|
+
let workspaceIdentityFilesUnsupported = false;
|
|
784
|
+
|
|
528
785
|
function getAgentName() {
|
|
529
786
|
return agentIdentity.name;
|
|
530
787
|
}
|
|
@@ -605,6 +862,20 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
605
862
|
}),
|
|
606
863
|
);
|
|
607
864
|
});
|
|
865
|
+
refreshAgentsCatalog(true).then((snapshot) => {
|
|
866
|
+
emitDebug(
|
|
867
|
+
"relay.session",
|
|
868
|
+
"agents_catalog_prefetched",
|
|
869
|
+
"info",
|
|
870
|
+
{ sessionKey: sessionService.ensureSessionKey() },
|
|
871
|
+
() => ({
|
|
872
|
+
count: Array.isArray(snapshot.agents) ? snapshot.agents.length : 0,
|
|
873
|
+
stale: !!snapshot.stale,
|
|
874
|
+
unsupported: !!snapshot.unsupported,
|
|
875
|
+
trigger,
|
|
876
|
+
}),
|
|
877
|
+
);
|
|
878
|
+
});
|
|
608
879
|
sessionService.getCurrentSessionModelConfig().then((config) => {
|
|
609
880
|
emitDebug(
|
|
610
881
|
"relay.session",
|
|
@@ -627,12 +898,21 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
627
898
|
clearTyping("upstream_disconnected");
|
|
628
899
|
inFlightModelsCatalogFetch = null;
|
|
629
900
|
inFlightSkillsCatalogFetch = null;
|
|
901
|
+
inFlightAgentsCatalogFetch = null;
|
|
630
902
|
inFlightProviderUsageFetch = null;
|
|
903
|
+
|
|
904
|
+
connectionGeneration += 1;
|
|
905
|
+
lastGatewayIdentity = null;
|
|
906
|
+
workspaceIdentityFilesUnsupported = false;
|
|
907
|
+
workspaceIdentityFallbackCache.clear();
|
|
908
|
+
inFlightWorkspaceIdentityFetches.clear();
|
|
631
909
|
cachedSkillsCatalogStale = true;
|
|
910
|
+
cachedAgentsCatalogStale = true;
|
|
911
|
+
|
|
912
|
+
agentsListUnsupported = false;
|
|
632
913
|
cachedProviderUsageStale = true;
|
|
633
914
|
resetActivityStatusAdapter();
|
|
634
|
-
|
|
635
|
-
// OcuClaw mark while disconnected (spec 2026-04-27).
|
|
915
|
+
|
|
636
916
|
if (
|
|
637
917
|
agentIdentity.emoji != null ||
|
|
638
918
|
agentIdentity.avatarDataUri != null ||
|
|
@@ -663,16 +943,67 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
663
943
|
broadcastStatus();
|
|
664
944
|
}
|
|
665
945
|
|
|
946
|
+
async function ensureWorkspaceIdentityFallback(agentId) {
|
|
947
|
+
const id = typeof agentId === "string" ? agentId.trim() : "";
|
|
948
|
+
if (!id) return null;
|
|
949
|
+
if (workspaceIdentityFilesUnsupported) return null;
|
|
950
|
+
if (workspaceIdentityFallbackCache.has(id)) {
|
|
951
|
+
return workspaceIdentityFallbackCache.get(id);
|
|
952
|
+
}
|
|
953
|
+
if (inFlightWorkspaceIdentityFetches.has(id)) {
|
|
954
|
+
return inFlightWorkspaceIdentityFetches.get(id);
|
|
955
|
+
}
|
|
956
|
+
const gen = connectionGeneration;
|
|
957
|
+
const promise = gatewayBridge
|
|
958
|
+
.request("agents.files.get", {
|
|
959
|
+
agentId: id,
|
|
960
|
+
name: WORKSPACE_IDENTITY_FILENAME,
|
|
961
|
+
})
|
|
962
|
+
.then((result) => {
|
|
963
|
+
const file = result && result.file;
|
|
964
|
+
const content =
|
|
965
|
+
file && typeof file.content === "string" ? file.content : "";
|
|
966
|
+
const parsed = parseWorkspaceIdentityFallback(content);
|
|
967
|
+
const value = parsed.name || parsed.emoji ? parsed : null;
|
|
968
|
+
|
|
969
|
+
if (gen === connectionGeneration) {
|
|
970
|
+
workspaceIdentityFallbackCache.set(id, value);
|
|
971
|
+
}
|
|
972
|
+
return value;
|
|
973
|
+
})
|
|
974
|
+
.catch((err) => {
|
|
975
|
+
const message = err && err.message ? err.message : String(err);
|
|
976
|
+
|
|
977
|
+
if (isMethodNotFoundError(err, message)) {
|
|
978
|
+
workspaceIdentityFilesUnsupported = true;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (gen === connectionGeneration) {
|
|
982
|
+
workspaceIdentityFallbackCache.set(id, null);
|
|
983
|
+
}
|
|
984
|
+
emitDebug(
|
|
985
|
+
"relay.session",
|
|
986
|
+
"workspace_identity_fallback_failed",
|
|
987
|
+
"debug",
|
|
988
|
+
{ sessionKey: sessionService.ensureSessionKey() },
|
|
989
|
+
() => ({ agentId: id, message }),
|
|
990
|
+
);
|
|
991
|
+
return null;
|
|
992
|
+
})
|
|
993
|
+
.finally(() => {
|
|
994
|
+
inFlightWorkspaceIdentityFetches.delete(id);
|
|
995
|
+
});
|
|
996
|
+
inFlightWorkspaceIdentityFetches.set(id, promise);
|
|
997
|
+
return promise;
|
|
998
|
+
}
|
|
999
|
+
|
|
666
1000
|
function applyAgentIdentity(identity, source) {
|
|
1001
|
+
lastGatewayIdentity = identity || null;
|
|
667
1002
|
const agentId =
|
|
668
1003
|
identity && typeof identity.agentId === "string" && identity.agentId ? identity.agentId : null;
|
|
669
1004
|
const avatarSource =
|
|
670
1005
|
identity && typeof identity.avatar === "string" && identity.avatar ? identity.avatar : null;
|
|
671
|
-
|
|
672
|
-
// For data: URIs the hash is cheap and synchronous — compute it now so the
|
|
673
|
-
// first status broadcast carries both fields. For remote avatars the hash
|
|
674
|
-
// arrives via the resolveAgentAvatar(...).then(...) settle below, after the
|
|
675
|
-
// initial broadcast has gone out with hash:null.
|
|
1006
|
+
|
|
676
1007
|
const inlineAvatarDataUri =
|
|
677
1008
|
avatarSource && avatarSource.startsWith("data:") ? avatarSource : null;
|
|
678
1009
|
let inlineAvatarHash = null;
|
|
@@ -698,6 +1029,17 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
698
1029
|
avatarDataUri: inlineAvatarDataUri,
|
|
699
1030
|
avatarHash: inlineAvatarHash,
|
|
700
1031
|
};
|
|
1032
|
+
const fallback = agentId
|
|
1033
|
+
? workspaceIdentityFallbackCache.get(agentId)
|
|
1034
|
+
: null;
|
|
1035
|
+
if (fallback) {
|
|
1036
|
+
const merged = applyIdentityFallback(
|
|
1037
|
+
{ name: next.name, emoji: next.emoji },
|
|
1038
|
+
fallback,
|
|
1039
|
+
);
|
|
1040
|
+
next.name = merged.name;
|
|
1041
|
+
next.emoji = merged.emoji;
|
|
1042
|
+
}
|
|
701
1043
|
agentIdentity = next;
|
|
702
1044
|
conversationState.setAgentName(next.name || "Agent");
|
|
703
1045
|
emitDebug(
|
|
@@ -711,15 +1053,32 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
711
1053
|
hasEmoji: !!next.emoji,
|
|
712
1054
|
hasAvatarSource: !!avatarSource,
|
|
713
1055
|
avatarInline: !!inlineAvatarDataUri,
|
|
1056
|
+
fallbackApplied: !!fallback,
|
|
714
1057
|
}),
|
|
715
1058
|
);
|
|
716
1059
|
broadcastStatus();
|
|
717
1060
|
|
|
1061
|
+
if (
|
|
1062
|
+
agentId &&
|
|
1063
|
+
!workspaceIdentityFallbackCache.has(agentId) &&
|
|
1064
|
+
(!next.name || !next.emoji)
|
|
1065
|
+
) {
|
|
1066
|
+
ensureWorkspaceIdentityFallback(agentId)
|
|
1067
|
+
.then((value) => {
|
|
1068
|
+
if (!value) return;
|
|
1069
|
+
|
|
1070
|
+
if (!openclawConnected) return;
|
|
1071
|
+
if (lastGatewayIdentity !== identity) return;
|
|
1072
|
+
applyAgentIdentity(identity, `${source}_workspace_fallback`);
|
|
1073
|
+
})
|
|
1074
|
+
.catch(() => {});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
718
1077
|
if (!agentId || !avatarSource) return;
|
|
719
1078
|
const generationName = next.name;
|
|
720
1079
|
const generationEmoji = next.emoji;
|
|
721
1080
|
resolveAgentAvatar(agentId, avatarSource).then((resolved) => {
|
|
722
|
-
|
|
1081
|
+
|
|
723
1082
|
if (
|
|
724
1083
|
agentIdentity.name !== generationName ||
|
|
725
1084
|
agentIdentity.emoji !== generationEmoji
|
|
@@ -808,8 +1167,7 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
808
1167
|
() => ({ runId }),
|
|
809
1168
|
);
|
|
810
1169
|
}
|
|
811
|
-
|
|
812
|
-
// the surviving (non-superseded) buffer reaches the parser.
|
|
1170
|
+
|
|
813
1171
|
const parsedSpans = parseTaggedSpans(queuedStreaming.rawText, [
|
|
814
1172
|
EMOJI_TAG_FAMILY_CONFIG,
|
|
815
1173
|
PACE_TAG_FAMILY_CONFIG,
|
|
@@ -916,6 +1274,54 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
916
1274
|
return skillsCatalogSnapshot(cachedSkillsCatalogFetchedAt);
|
|
917
1275
|
}
|
|
918
1276
|
|
|
1277
|
+
function agentsCatalogSnapshot(nowMs) {
|
|
1278
|
+
const currentNow = Number.isFinite(nowMs) ? nowMs : now();
|
|
1279
|
+
const hasCache = Array.isArray(cachedAgentsCatalog);
|
|
1280
|
+
return {
|
|
1281
|
+
agents: hasCache ? cachedAgentsCatalog : [],
|
|
1282
|
+
defaultId: cachedAgentsEnvelope.defaultId,
|
|
1283
|
+
mainKey: cachedAgentsEnvelope.mainKey,
|
|
1284
|
+
scope: cachedAgentsEnvelope.scope,
|
|
1285
|
+
fetchedAtMs: hasCache ? cachedAgentsCatalogFetchedAt : currentNow,
|
|
1286
|
+
stale: !hasCache || cachedAgentsCatalogStale,
|
|
1287
|
+
unsupported: agentsListUnsupported,
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function cacheAgentsCatalog(agents, envelope, fetchedAtMs, stale) {
|
|
1292
|
+
cachedAgentsCatalog = Array.isArray(agents) ? agents : [];
|
|
1293
|
+
cachedAgentsEnvelope = {
|
|
1294
|
+
defaultId:
|
|
1295
|
+
envelope && typeof envelope.defaultId === "string" && envelope.defaultId
|
|
1296
|
+
? envelope.defaultId
|
|
1297
|
+
: null,
|
|
1298
|
+
mainKey:
|
|
1299
|
+
envelope && typeof envelope.mainKey === "string" && envelope.mainKey
|
|
1300
|
+
? envelope.mainKey
|
|
1301
|
+
: null,
|
|
1302
|
+
scope:
|
|
1303
|
+
envelope && typeof envelope.scope === "string" && envelope.scope
|
|
1304
|
+
? envelope.scope
|
|
1305
|
+
: null,
|
|
1306
|
+
};
|
|
1307
|
+
cachedAgentsCatalogFetchedAt = Number.isFinite(fetchedAtMs)
|
|
1308
|
+
? Math.floor(fetchedAtMs)
|
|
1309
|
+
: now();
|
|
1310
|
+
cachedAgentsCatalogStale = !!stale;
|
|
1311
|
+
const snapshot = agentsCatalogSnapshot(cachedAgentsCatalogFetchedAt);
|
|
1312
|
+
|
|
1313
|
+
broadcastAgentsCatalog(snapshot);
|
|
1314
|
+
return snapshot;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function getAgentDisplayName(agentId) {
|
|
1318
|
+
if (typeof agentId !== "string" || !agentId.trim()) return null;
|
|
1319
|
+
const id = agentId.trim();
|
|
1320
|
+
if (!Array.isArray(cachedAgentsCatalog)) return null;
|
|
1321
|
+
const match = cachedAgentsCatalog.find((entry) => entry && entry.id === id);
|
|
1322
|
+
return match ? match.name : null;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
919
1325
|
function providerUsageCacheState(nowMs) {
|
|
920
1326
|
const currentNow = Number.isFinite(nowMs) ? nowMs : now();
|
|
921
1327
|
const hasCache =
|
|
@@ -1042,10 +1448,7 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1042
1448
|
cachedAuthProfileCounts.set(provider, count);
|
|
1043
1449
|
}
|
|
1044
1450
|
} else {
|
|
1045
|
-
|
|
1046
|
-
// Retain-on-failure preserves the caption count across transient blips;
|
|
1047
|
-
// the next successful fetch overwrites. See spec
|
|
1048
|
-
// 2026-05-07-rate-limit-pool-status-design.md (authStatus retain-on-failure).
|
|
1451
|
+
|
|
1049
1452
|
emitDebug(
|
|
1050
1453
|
"relay.session",
|
|
1051
1454
|
"models_auth_status_refresh_failed",
|
|
@@ -1185,6 +1588,76 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1185
1588
|
});
|
|
1186
1589
|
}
|
|
1187
1590
|
|
|
1591
|
+
async function refreshAgentsCatalog(force) {
|
|
1592
|
+
const snapshot = agentsCatalogSnapshot();
|
|
1593
|
+
if (agentsListUnsupported) {
|
|
1594
|
+
return snapshot;
|
|
1595
|
+
}
|
|
1596
|
+
if (!force && !snapshot.stale) {
|
|
1597
|
+
return snapshot;
|
|
1598
|
+
}
|
|
1599
|
+
if (inFlightAgentsCatalogFetch) {
|
|
1600
|
+
return inFlightAgentsCatalogFetch;
|
|
1601
|
+
}
|
|
1602
|
+
if (!openclawConnected) {
|
|
1603
|
+
return snapshot;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
inFlightAgentsCatalogFetch = gatewayBridge
|
|
1607
|
+
.request("agents.list", {})
|
|
1608
|
+
.then(async (result) => {
|
|
1609
|
+
const rawRows =
|
|
1610
|
+
result && Array.isArray(result.agents) ? result.agents : [];
|
|
1611
|
+
|
|
1612
|
+
const needIds = rawRows
|
|
1613
|
+
.filter(rawRowNeedsIdentityFallback)
|
|
1614
|
+
.map((r) => r.id.trim());
|
|
1615
|
+
if (needIds.length) {
|
|
1616
|
+
await Promise.all(
|
|
1617
|
+
needIds.map((id) => ensureWorkspaceIdentityFallback(id)),
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
const overlaid = overlayRawAgentRowsWithFallback(
|
|
1621
|
+
rawRows,
|
|
1622
|
+
workspaceIdentityFallbackCache,
|
|
1623
|
+
);
|
|
1624
|
+
const agents = normalizeAgentsCatalogRows(overlaid);
|
|
1625
|
+
const envelope = {
|
|
1626
|
+
defaultId: result && result.defaultId,
|
|
1627
|
+
mainKey: result && result.mainKey,
|
|
1628
|
+
scope: result && result.scope,
|
|
1629
|
+
};
|
|
1630
|
+
return cacheAgentsCatalog(agents, envelope, Date.now(), false);
|
|
1631
|
+
})
|
|
1632
|
+
.catch((err) => {
|
|
1633
|
+
const message = err && err.message ? err.message : String(err);
|
|
1634
|
+
|
|
1635
|
+
if (isMethodNotFoundError(err, message)) {
|
|
1636
|
+
agentsListUnsupported = true;
|
|
1637
|
+
}
|
|
1638
|
+
emitDebug(
|
|
1639
|
+
"relay.session",
|
|
1640
|
+
"agents_catalog_refresh_failed",
|
|
1641
|
+
"warn",
|
|
1642
|
+
{ sessionKey: sessionService.ensureSessionKey() },
|
|
1643
|
+
() => ({
|
|
1644
|
+
message,
|
|
1645
|
+
unsupported: agentsListUnsupported,
|
|
1646
|
+
hadCache: Array.isArray(cachedAgentsCatalog),
|
|
1647
|
+
}),
|
|
1648
|
+
);
|
|
1649
|
+
if (Array.isArray(cachedAgentsCatalog)) {
|
|
1650
|
+
cachedAgentsCatalogStale = true;
|
|
1651
|
+
return agentsCatalogSnapshot();
|
|
1652
|
+
}
|
|
1653
|
+
return cacheAgentsCatalog([], cachedAgentsEnvelope, now(), true);
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
return inFlightAgentsCatalogFetch.finally(() => {
|
|
1657
|
+
inFlightAgentsCatalogFetch = null;
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1188
1661
|
function trackAcceptedRun(entry) {
|
|
1189
1662
|
if (!entry || !entry.runId) return;
|
|
1190
1663
|
upstreamRunPipeline.set(entry.runId, {
|
|
@@ -1218,6 +1691,16 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1218
1691
|
return snapshot;
|
|
1219
1692
|
}
|
|
1220
1693
|
|
|
1694
|
+
async function getAgentsCatalogSnapshot() {
|
|
1695
|
+
const snapshot = agentsCatalogSnapshot();
|
|
1696
|
+
if (snapshot.stale && !agentsListUnsupported && openclawConnected) {
|
|
1697
|
+
return refreshAgentsCatalog(true);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
broadcastAgentsCatalog(snapshot);
|
|
1701
|
+
return snapshot;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1221
1704
|
async function getProviderUsageSnapshot() {
|
|
1222
1705
|
const snapshot = projectProviderUsageSnapshot();
|
|
1223
1706
|
if (snapshot.stale && openclawConnected) {
|
|
@@ -1227,15 +1710,7 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1227
1710
|
}
|
|
1228
1711
|
|
|
1229
1712
|
async function handleCurrentSessionModelConfigChanged() {
|
|
1230
|
-
|
|
1231
|
-
// becomes known once the model config resolves. On connect that resolution
|
|
1232
|
-
// happens AFTER the initial connect-time context refresh has already run
|
|
1233
|
-
// with a null model key, so a cached/persisted window for a cold (pre-turn)
|
|
1234
|
-
// session would otherwise never be applied until the first turn warms the
|
|
1235
|
-
// gateway's window. Re-refresh now that the model key is known so the
|
|
1236
|
-
// cached window surfaces immediately; fire-and-forget so it doesn't block
|
|
1237
|
-
// the provider-usage rebroadcast. Also keeps context correct across model
|
|
1238
|
-
// switches.
|
|
1713
|
+
|
|
1239
1714
|
sessionContextService.refreshActiveSessionContext().catch(() => {});
|
|
1240
1715
|
|
|
1241
1716
|
const snapshot = projectProviderUsageSnapshot();
|
|
@@ -1303,15 +1778,8 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1303
1778
|
};
|
|
1304
1779
|
}
|
|
1305
1780
|
|
|
1306
|
-
// Session-context service: tracks contextTokens/contextWindow and broadcasts
|
|
1307
|
-
// snapshots to connected WebUI clients. session.switch.applied trigger is
|
|
1308
|
-
// deferred (see Task 4 plan notes — the switchSession path lives in
|
|
1309
|
-
// downstream-handler.ts and is out of scope for this task).
|
|
1310
1781
|
let cachedRunActiveSessionKey = null;
|
|
1311
|
-
|
|
1312
|
-
// model's window only after the first turn). The service caches the observed
|
|
1313
|
-
// window per model and reuses it for cold/new-session reads, keyed by the
|
|
1314
|
-
// active session's resolved provider/model.
|
|
1782
|
+
|
|
1315
1783
|
const sessionContextService = createSessionContextService({
|
|
1316
1784
|
gatewayBridge,
|
|
1317
1785
|
stateDir: opts.stateDir,
|
|
@@ -1407,8 +1875,6 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1407
1875
|
});
|
|
1408
1876
|
}
|
|
1409
1877
|
|
|
1410
|
-
// Unique sentinel characters used as span boundary markers through the markdown pass.
|
|
1411
|
-
// These are ASCII control characters that never appear in assistant text.
|
|
1412
1878
|
const SPAN_START_MARK = "\x01";
|
|
1413
1879
|
const SPAN_END_MARK = "\x02";
|
|
1414
1880
|
|
|
@@ -1427,8 +1893,7 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1427
1893
|
for (const name of familyNames) empty[name] = [];
|
|
1428
1894
|
return { text: `${prefix}${text}`, spansByFamily: empty };
|
|
1429
1895
|
}
|
|
1430
|
-
|
|
1431
|
-
// Each span contributes two boundary events. Sort by offset, start-before-end on tie.
|
|
1896
|
+
|
|
1432
1897
|
const events = [];
|
|
1433
1898
|
for (const family of familyNames) {
|
|
1434
1899
|
const spans = spansByFamily[family];
|
|
@@ -1439,7 +1904,6 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1439
1904
|
}
|
|
1440
1905
|
events.sort((a, b) => a.offset - b.offset || (a.isEnd ? 1 : -1));
|
|
1441
1906
|
|
|
1442
|
-
// Build marked text, inserting one boundary marker per event.
|
|
1443
1907
|
let markedText = "";
|
|
1444
1908
|
let cursor = 0;
|
|
1445
1909
|
for (const ev of events) {
|
|
@@ -1449,13 +1913,10 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1449
1913
|
}
|
|
1450
1914
|
markedText += cleanText.slice(cursor);
|
|
1451
1915
|
|
|
1452
|
-
// Run the full markdown pass on the marked text.
|
|
1453
1916
|
const { text: rawPost } = conversationState._markdownToPlainText(markedText, {
|
|
1454
1917
|
stripReplyTags: true,
|
|
1455
1918
|
});
|
|
1456
1919
|
|
|
1457
|
-
// Strip markers from the post-markdown text and record their post-strip positions.
|
|
1458
|
-
// Each marker, in document order, corresponds to the i-th event from `events`.
|
|
1459
1920
|
const eventPostPositions = [];
|
|
1460
1921
|
let stripped = "";
|
|
1461
1922
|
for (let j = 0; j < rawPost.length; j++) {
|
|
@@ -1489,32 +1950,10 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1489
1950
|
const runId = data.runId || null;
|
|
1490
1951
|
if (runId) {
|
|
1491
1952
|
stopTypingForRun(runId, "assistant_message_committed");
|
|
1492
|
-
|
|
1493
|
-
// synthesized activity-idle. Pre-fix: activity-idle was broadcast first
|
|
1494
|
-
// (below), and the final flushed streaming chunk arrived at the client
|
|
1495
|
-
// AFTER stream_pipeline_turn_idle had already fired. The client's
|
|
1496
|
-
// VoiceIngressCoordinator then treated the trailing chunk as the start
|
|
1497
|
-
// of a new ingress turn (beginIngressStreamPipelineTurnIfNeeded returns
|
|
1498
|
-
// true once streamTurnIdleLogged is set), resetting
|
|
1499
|
-
// streaming_bleed_in_cursor_compose state and producing a visible
|
|
1500
|
-
// ~50-char streaming-text rewind on hardware (trace
|
|
1501
|
-
// 20260518-074144-Z ws 128→130). Flushing first ensures the final
|
|
1502
|
-
// chunk is processed during the still-active turn; the synthesized
|
|
1503
|
-
// activity-idle then runs after the buffer is up to date.
|
|
1953
|
+
|
|
1504
1954
|
clearStreamingThrottleTimer();
|
|
1505
1955
|
flushPendingStreamingText();
|
|
1506
|
-
|
|
1507
|
-
// turn end at the gateway boundary). Upstream eventually emits its own
|
|
1508
|
-
// terminal activity message via gatewayBridge.on("activity",...) but
|
|
1509
|
-
// with ~1.5s median lag past run_complete, which leaves the
|
|
1510
|
-
// thinking-indicator spinner ticking visibly past the point the agent
|
|
1511
|
-
// has fully delivered its response. App-side ActivityGuardState dedupes
|
|
1512
|
-
// the later upstream terminal message by runId + activityId so the real
|
|
1513
|
-
// upstream activity remains authoritative for fields like detail/intent
|
|
1514
|
-
// — this synth only owns the state=idle transition timing. activityId
|
|
1515
|
-
// is namespaced so it cannot collide with a real upstream activityId.
|
|
1516
|
-
// origin/phase=lifecycle/end satisfies isTerminalActivityBoundary so
|
|
1517
|
-
// downstream consumers treat it as a real terminal boundary.
|
|
1956
|
+
|
|
1518
1957
|
broadcastActivity({
|
|
1519
1958
|
state: "idle",
|
|
1520
1959
|
runId,
|
|
@@ -1567,8 +2006,6 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1567
2006
|
}),
|
|
1568
2007
|
);
|
|
1569
2008
|
|
|
1570
|
-
// (Moved above: flushPendingStreamingText now runs before broadcastActivity
|
|
1571
|
-
// so the client never sees a chunk arrive after stream_pipeline_turn_idle.)
|
|
1572
2009
|
const sanitizedContent =
|
|
1573
2010
|
data.role === "assistant"
|
|
1574
2011
|
? sanitizeAssistantContentBlocks(data.content)
|
|
@@ -1584,8 +2021,7 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1584
2021
|
);
|
|
1585
2022
|
}
|
|
1586
2023
|
broadcastPages();
|
|
1587
|
-
|
|
1588
|
-
// message. Runs async so it doesn't block the message handler.
|
|
2024
|
+
|
|
1589
2025
|
sessionContextService.refreshActiveSessionContext().catch(() => {});
|
|
1590
2026
|
|
|
1591
2027
|
const voiceRuntime = getVoiceRuntime();
|
|
@@ -1625,19 +2061,26 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1625
2061
|
ackToRunStartMs: runPipeline && runPipeline.ackAt ? (now - runPipeline.ackAt) : null,
|
|
1626
2062
|
}),
|
|
1627
2063
|
);
|
|
1628
|
-
|
|
1629
|
-
// spinner / suppress stale token counts during an active run.
|
|
2064
|
+
|
|
1630
2065
|
cachedRunActiveSessionKey = data.sessionKey || sessionService.ensureSessionKey();
|
|
1631
2066
|
sessionContextService.broadcastRunActive(true);
|
|
1632
2067
|
}
|
|
1633
2068
|
if (runId && isTerminalActivityBoundary(data.state, phase, origin)) {
|
|
1634
2069
|
stopTypingForRun(runId, "terminal_activity_boundary");
|
|
1635
|
-
|
|
2070
|
+
|
|
1636
2071
|
if (cachedRunActiveSessionKey) {
|
|
1637
2072
|
cachedRunActiveSessionKey = null;
|
|
1638
2073
|
sessionContextService.broadcastRunActive(false);
|
|
1639
2074
|
sessionContextService.refreshActiveSessionContext().catch(() => {});
|
|
1640
2075
|
}
|
|
2076
|
+
|
|
2077
|
+
if (!activeProviderContext().provider) {
|
|
2078
|
+
sessionService.getCurrentSessionModelConfig().catch((err) => {
|
|
2079
|
+
logger.warn(
|
|
2080
|
+
`[relay] Provider re-resolve after run-end failed: ${err && err.message ? err.message : err}`,
|
|
2081
|
+
);
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
1641
2084
|
}
|
|
1642
2085
|
let activity = data;
|
|
1643
2086
|
let shouldRefreshProviderUsageInBackground = false;
|
|
@@ -1669,7 +2112,7 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1669
2112
|
}
|
|
1670
2113
|
if (runId && data.state === "idle" && data.isError === true && phase === "error") {
|
|
1671
2114
|
upstreamRunPipeline.delete(runId);
|
|
1672
|
-
|
|
2115
|
+
|
|
1673
2116
|
if (cachedRunActiveSessionKey) {
|
|
1674
2117
|
cachedRunActiveSessionKey = null;
|
|
1675
2118
|
sessionContextService.broadcastRunActive(false);
|
|
@@ -1679,8 +2122,50 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1679
2122
|
});
|
|
1680
2123
|
|
|
1681
2124
|
gatewayBridge.on("streaming", (data) => {
|
|
1682
|
-
if (
|
|
1683
|
-
|
|
2125
|
+
if (isTitleDistillerStreamingEvent(data)) {
|
|
2126
|
+
const runId = data && data.runId ? data.runId : null;
|
|
2127
|
+
const sessionKey = data && data.sessionKey ? data.sessionKey : sessionService.ensureSessionKey();
|
|
2128
|
+
emitDebug(
|
|
2129
|
+
"openclaw.run",
|
|
2130
|
+
"streaming_ignored",
|
|
2131
|
+
"debug",
|
|
2132
|
+
{ sessionKey, runId },
|
|
2133
|
+
() => ({
|
|
2134
|
+
reason: "title_distiller",
|
|
2135
|
+
textChars: typeof data.text === "string" ? data.text.length : 0,
|
|
2136
|
+
}),
|
|
2137
|
+
);
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
const runId = data.runId || null;
|
|
2141
|
+
const runPipeline = runId ? upstreamRunPipeline.get(runId) : null;
|
|
2142
|
+
const explicitSessionKey =
|
|
2143
|
+
data && typeof data.sessionKey === "string" && data.sessionKey.trim()
|
|
2144
|
+
? data.sessionKey.trim()
|
|
2145
|
+
: null;
|
|
2146
|
+
const pipelineSessionKey =
|
|
2147
|
+
runPipeline &&
|
|
2148
|
+
typeof runPipeline.sessionKey === "string" &&
|
|
2149
|
+
runPipeline.sessionKey.trim()
|
|
2150
|
+
? runPipeline.sessionKey.trim()
|
|
2151
|
+
: null;
|
|
2152
|
+
const sessionKey =
|
|
2153
|
+
explicitSessionKey ||
|
|
2154
|
+
pipelineSessionKey;
|
|
2155
|
+
if (!sessionKey) {
|
|
2156
|
+
emitDebug(
|
|
2157
|
+
"openclaw.run",
|
|
2158
|
+
"streaming_ignored",
|
|
2159
|
+
"debug",
|
|
2160
|
+
{ sessionKey: null, runId },
|
|
2161
|
+
() => ({
|
|
2162
|
+
reason: "unknown_sessionless_run",
|
|
2163
|
+
textChars: typeof data.text === "string" ? data.text.length : 0,
|
|
2164
|
+
}),
|
|
2165
|
+
);
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
if (!sessionService.isCurrentSession(sessionKey)) return;
|
|
1684
2169
|
const { provider: outcomeProvider } = activeProviderContext();
|
|
1685
2170
|
if (outcomeProvider) {
|
|
1686
2171
|
providerOutcomeState.set(outcomeProvider, {
|
|
@@ -1688,7 +2173,6 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1688
2173
|
lastOutcomeAtMs: now(),
|
|
1689
2174
|
});
|
|
1690
2175
|
}
|
|
1691
|
-
const runId = data.runId || null;
|
|
1692
2176
|
const nowMs = now();
|
|
1693
2177
|
const gatewayReceivedAtMs = Number.isFinite(data.gatewayReceivedAtMs)
|
|
1694
2178
|
? Math.floor(data.gatewayReceivedAtMs)
|
|
@@ -1702,7 +2186,6 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1702
2186
|
const firstGatewayChunk =
|
|
1703
2187
|
typeof data.firstGatewayChunk === "boolean" ? data.firstGatewayChunk : null;
|
|
1704
2188
|
if (runId) {
|
|
1705
|
-
const runPipeline = upstreamRunPipeline.get(runId);
|
|
1706
2189
|
if (runPipeline && !runPipeline.firstStreamingAt) {
|
|
1707
2190
|
runPipeline.firstStreamingAt = nowMs;
|
|
1708
2191
|
runPipeline.firstGatewayReceivedAt = gatewayReceivedAtMs;
|
|
@@ -1730,9 +2213,7 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1730
2213
|
}
|
|
1731
2214
|
}
|
|
1732
2215
|
const prefix = `${agentIdentity.name || "Agent"}: `;
|
|
1733
|
-
|
|
1734
|
-
// parseTaggedSpans + markdown pass to flushPendingStreamingText. Chunks
|
|
1735
|
-
// superseded inside the throttle window never pay parser cost.
|
|
2216
|
+
|
|
1736
2217
|
pendingStreaming = {
|
|
1737
2218
|
rawText: data.text,
|
|
1738
2219
|
prefix,
|
|
@@ -1753,8 +2234,7 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1753
2234
|
runId,
|
|
1754
2235
|
},
|
|
1755
2236
|
() => ({
|
|
1756
|
-
|
|
1757
|
-
// computed here (parse-on-flush moves it to flushPendingStreamingText).
|
|
2237
|
+
|
|
1758
2238
|
textChars: pendingStreaming ? pendingStreaming.rawText.length : 0,
|
|
1759
2239
|
rawAssistantChars,
|
|
1760
2240
|
assistantDeltaChars,
|
|
@@ -1789,8 +2269,7 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1789
2269
|
refreshUpstreamBootstrap("connected_event").catch((err) => {
|
|
1790
2270
|
logger.warn(`[relay] Upstream connected bootstrap failed: ${err.message}`);
|
|
1791
2271
|
});
|
|
1792
|
-
|
|
1793
|
-
// baseline token count before the first agent_end refresh fires.
|
|
2272
|
+
|
|
1794
2273
|
sessionContextService.refreshActiveSessionContext().catch(() => {});
|
|
1795
2274
|
});
|
|
1796
2275
|
|
|
@@ -1890,6 +2369,21 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1890
2369
|
);
|
|
1891
2370
|
});
|
|
1892
2371
|
|
|
2372
|
+
gatewayBridge.on("connectFailed", (info) => {
|
|
2373
|
+
|
|
2374
|
+
emitDebug(
|
|
2375
|
+
"relay.transport",
|
|
2376
|
+
"connect_failed",
|
|
2377
|
+
"warn",
|
|
2378
|
+
{ sessionKey: sessionService.ensureSessionKey() },
|
|
2379
|
+
() => ({
|
|
2380
|
+
reason: info && info.reason ? info.reason : null,
|
|
2381
|
+
minProtocol: info && info.minProtocol != null ? info.minProtocol : null,
|
|
2382
|
+
maxProtocol: info && info.maxProtocol != null ? info.maxProtocol : null,
|
|
2383
|
+
}),
|
|
2384
|
+
);
|
|
2385
|
+
});
|
|
2386
|
+
|
|
1893
2387
|
return {
|
|
1894
2388
|
clearTyping,
|
|
1895
2389
|
compactActiveSession: (sessionKey) =>
|
|
@@ -1900,6 +2394,8 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1900
2394
|
getAgentAvatarHash,
|
|
1901
2395
|
getAgentAvatarDataUriByHash,
|
|
1902
2396
|
getModelsCatalogSnapshot,
|
|
2397
|
+
getAgentsCatalogSnapshot,
|
|
2398
|
+
getAgentDisplayName,
|
|
1903
2399
|
getProviderUsageSnapshot,
|
|
1904
2400
|
getSkillsCatalogSnapshot,
|
|
1905
2401
|
handleCurrentSessionModelConfigChanged,
|
|
@@ -1917,6 +2413,7 @@ export function createUpstreamRuntime(opts = {}) {
|
|
|
1917
2413
|
activeTyping = null;
|
|
1918
2414
|
inFlightModelsCatalogFetch = null;
|
|
1919
2415
|
inFlightSkillsCatalogFetch = null;
|
|
2416
|
+
inFlightAgentsCatalogFetch = null;
|
|
1920
2417
|
upstreamRunPipeline.clear();
|
|
1921
2418
|
},
|
|
1922
2419
|
trackAcceptedRun,
|