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
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function sanitizeConnectReason(message) {
|
|
2
|
+
return String(message ?? "")
|
|
3
|
+
.replace(
|
|
4
|
+
/(token|secret|nonce|sig|signature|authorization|password|auth)=([^\s&"']+)/gi,
|
|
5
|
+
"$1=[REDACTED]",
|
|
6
|
+
)
|
|
7
|
+
.replace(/\bbearer\s+\S+/gi, "bearer [REDACTED]")
|
|
8
|
+
.replace(/[A-Za-z0-9_\-+/.=]{40,}/g, "[REDACTED]")
|
|
9
|
+
.slice(0, 300);
|
|
10
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { sanitizeConnectReason } from "./sanitize-connect-reason.ts";
|
|
5
|
+
|
|
6
|
+
describe("sanitizeConnectReason", () => {
|
|
7
|
+
it("redacts token= / authorization= key-value secrets", () => {
|
|
8
|
+
assert.equal(
|
|
9
|
+
sanitizeConnectReason("connect ws://h/?token=abc123def failed"),
|
|
10
|
+
"connect ws://h/?token=[REDACTED] failed",
|
|
11
|
+
);
|
|
12
|
+
assert.equal(sanitizeConnectReason("authorization=Bearerxyz"), "authorization=[REDACTED]");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("redacts a bearer credential", () => {
|
|
16
|
+
assert.equal(sanitizeConnectReason("got bearer shorttok here"), "got bearer [REDACTED] here");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("redacts a long credential-shaped run (jwt / signature / nonce)", () => {
|
|
20
|
+
const jwt = "a".repeat(60);
|
|
21
|
+
assert.equal(sanitizeConnectReason(`sig ${jwt}`), "sig [REDACTED]");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("passes a benign reason through unchanged", () => {
|
|
25
|
+
assert.equal(sanitizeConnectReason("Unexpected server response: 1008"), "Unexpected server response: 1008");
|
|
26
|
+
assert.equal(sanitizeConnectReason("first request must be connect"), "first request must be connect");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("handles null/undefined and caps length at 300", () => {
|
|
30
|
+
assert.equal(sanitizeConnectReason(null), "");
|
|
31
|
+
assert.equal(sanitizeConnectReason(undefined), "");
|
|
32
|
+
assert.equal(sanitizeConnectReason("ok ".repeat(200)).length, 300);
|
|
33
|
+
});
|
|
34
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -64,21 +64,21 @@ export default function register(api) {
|
|
|
64
64
|
try {
|
|
65
65
|
glassesUiDispose();
|
|
66
66
|
} catch (_) {
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
if (typeof deviceInfoDispose === "function") {
|
|
71
71
|
try {
|
|
72
72
|
deviceInfoDispose();
|
|
73
73
|
} catch (_) {
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
if (typeof distillerDispose === "function") {
|
|
78
78
|
try {
|
|
79
79
|
distillerDispose();
|
|
80
80
|
} catch (_) {
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
return service.stop({ logger: ctx && ctx.logger });
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { composeChannelTwoFragment } from "../domain/prompt-channel-fragments.js";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Build a before_prompt_build hook that injects the Channel-2 fragment.
|
|
5
|
-
* @param {{getDisplayStartStates:Function, getDisplayCurrentStates:Function,
|
|
6
|
-
* hasConnectedAppClient:Function}} service
|
|
7
|
-
*/
|
|
8
3
|
export function createChannelTwoHook(service, opts = {}) {
|
|
9
4
|
const emitDebug = typeof opts.emitDebug === "function" ? opts.emitDebug : () => {};
|
|
10
5
|
return function channelTwoBeforePromptBuild(_event, ctx) {
|
|
@@ -27,7 +22,7 @@ export function createChannelTwoHook(service, opts = {}) {
|
|
|
27
22
|
{ sessionKey }, () => ({ chars: fragment.length }));
|
|
28
23
|
return { appendSystemContext: fragment };
|
|
29
24
|
} catch (_err) {
|
|
30
|
-
|
|
25
|
+
|
|
31
26
|
return undefined;
|
|
32
27
|
}
|
|
33
28
|
};
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
|
|
3
|
-
// Marker files written by the container runtimes themselves: Docker creates
|
|
4
|
-
// /.dockerenv, Podman creates /run/.containerenv. Kubernetes-style runtimes
|
|
5
|
-
// leave neither — detection there would need heuristics too fragile to ship.
|
|
6
3
|
const CONTAINER_MARKER_PATHS = ["/.dockerenv", "/run/.containerenv"];
|
|
7
4
|
|
|
8
5
|
export function isLoopbackBindAddress(address) {
|
|
@@ -22,8 +19,7 @@ export function isContainerEnvironment(deps = {}) {
|
|
|
22
19
|
try {
|
|
23
20
|
if (existsSync(markerPath)) return true;
|
|
24
21
|
} catch {
|
|
25
|
-
|
|
26
|
-
// as "not a container" and keep the relay booting.
|
|
22
|
+
|
|
27
23
|
}
|
|
28
24
|
}
|
|
29
25
|
return false;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { assembleBundle, chunkZip } from "../domain/debug-bundle.js";
|
|
2
|
+
import { buildBundlePreview } from "../domain/debug-bundle-preview.js";
|
|
3
|
+
|
|
4
|
+
function computeAvailableSpanMs(dumpResult) {
|
|
5
|
+
const now = dumpResult && typeof dumpResult.nowMs === "number" ? dumpResult.nowMs : 0;
|
|
6
|
+
if (dumpResult && typeof dumpResult.oldestMatchedMs === "number") {
|
|
7
|
+
return Math.max(0, now - dumpResult.oldestMatchedMs);
|
|
8
|
+
}
|
|
9
|
+
const events = dumpResult && dumpResult.events;
|
|
10
|
+
if (!Array.isArray(events) || events.length === 0) return 0;
|
|
11
|
+
let min = Infinity;
|
|
12
|
+
for (const e of events) {
|
|
13
|
+
const ts = e && typeof e.ts === "number" ? e.ts : null;
|
|
14
|
+
if (ts !== null && ts < min) min = ts;
|
|
15
|
+
}
|
|
16
|
+
if (!Number.isFinite(min)) return 0;
|
|
17
|
+
return Math.max(0, now - min);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function handleDebugBundleRequest(deps, clientId, msg) {
|
|
21
|
+
if (!deps.gatesOn()) {
|
|
22
|
+
deps.emit("capture_refused", { requestId: msg.requestId, reason: "gates_off" });
|
|
23
|
+
|
|
24
|
+
deps.send(clientId, {
|
|
25
|
+
type: "debug-bundle-error",
|
|
26
|
+
requestId: msg.requestId,
|
|
27
|
+
reason: "upload_not_allowed",
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
deps.emit("capture_requested", {
|
|
32
|
+
requestId: msg.requestId,
|
|
33
|
+
redactionMode: msg.redactionMode,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const windowMs =
|
|
37
|
+
typeof msg.windowMs === "number" && Number.isFinite(msg.windowMs) && msg.windowMs > 0
|
|
38
|
+
? Math.floor(msg.windowMs)
|
|
39
|
+
: null;
|
|
40
|
+
const dumpResult = deps.dump(
|
|
41
|
+
windowMs ? { categories: deps.preset, sinceAgeMs: windowMs } : { categories: deps.preset },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (!dumpResult || dumpResult.ok === false) {
|
|
45
|
+
deps.emit("capture_failed", { requestId: msg.requestId, reason: "dump_failed" });
|
|
46
|
+
deps.send(clientId, { type: "debug-bundle-error", requestId: msg.requestId, reason: "dump_failed" });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const availableSpanMs = computeAvailableSpanMs(dumpResult);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const bundle = assembleBundle(dumpResult, {
|
|
53
|
+
installId: msg.installId,
|
|
54
|
+
build: deps.build,
|
|
55
|
+
redactionMode: msg.redactionMode || "structural",
|
|
56
|
+
ringCappedWindow: false,
|
|
57
|
+
idSalt: deps.idSalt,
|
|
58
|
+
maxZipBytes: deps.maxZipBytes,
|
|
59
|
+
chunkBytes: deps.chunkBytes,
|
|
60
|
+
note: msg.note,
|
|
61
|
+
});
|
|
62
|
+
deps.emit("bundle_assembled", {
|
|
63
|
+
requestId: msg.requestId,
|
|
64
|
+
categories: bundle.metadata.categories.length,
|
|
65
|
+
totalBytes: bundle.metadata.totalBytes,
|
|
66
|
+
ringCappedWindow: bundle.metadata.window.ringCappedWindow,
|
|
67
|
+
});
|
|
68
|
+
const bundleId = deps.newBundleId();
|
|
69
|
+
deps.cachePut(bundleId, {
|
|
70
|
+
zip: bundle.zip,
|
|
71
|
+
metadataJson: JSON.stringify(bundle.metadata),
|
|
72
|
+
bundleSha256: bundle.bundleSha256,
|
|
73
|
+
cachedMs: deps.now(),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const frameMetadataJson = JSON.stringify({
|
|
77
|
+
...bundle.metadata,
|
|
78
|
+
zipBytes: bundle.zip.length,
|
|
79
|
+
availableSpanMs,
|
|
80
|
+
});
|
|
81
|
+
deps.send(clientId, {
|
|
82
|
+
type: "debug-bundle-meta",
|
|
83
|
+
requestId: msg.requestId,
|
|
84
|
+
bundleId,
|
|
85
|
+
metadataJson: frameMetadataJson,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
deps.send(clientId, {
|
|
89
|
+
type: "debug-bundle-preview",
|
|
90
|
+
requestId: msg.requestId,
|
|
91
|
+
bundleId,
|
|
92
|
+
sampleJson: JSON.stringify(buildBundlePreview(bundle.files, { maxEvents: 15, maxCharsPerEvent: 80 })),
|
|
93
|
+
});
|
|
94
|
+
deps.emit("bundle_cached", {
|
|
95
|
+
requestId: msg.requestId,
|
|
96
|
+
bundleId,
|
|
97
|
+
parts: bundle.chunks.length,
|
|
98
|
+
});
|
|
99
|
+
} catch (err) {
|
|
100
|
+
|
|
101
|
+
deps.emit("upload_failed", { requestId: msg.requestId, reason: "assembly_failed" });
|
|
102
|
+
deps.send(clientId, { type: "debug-bundle-error", requestId: msg.requestId, reason: "assembly_failed" });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function handleDebugBundleSave(deps, clientId, msg) {
|
|
108
|
+
if (!deps.gatesOn()) {
|
|
109
|
+
deps.emit("save_refused", { requestId: msg.requestId, reason: "upload_not_allowed" });
|
|
110
|
+
deps.send(clientId, { type: "debug-bundle-error", requestId: msg.requestId, reason: "upload_not_allowed" });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const entry = deps.cacheGet(msg.bundleId);
|
|
114
|
+
if (!entry) {
|
|
115
|
+
deps.emit("save_expired", { requestId: msg.requestId, bundleId: msg.bundleId });
|
|
116
|
+
deps.send(clientId, { type: "debug-bundle-error", requestId: msg.requestId, reason: "bundle_expired" });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const reporterNote = typeof msg.note === "string" ? msg.note : "";
|
|
121
|
+
let sidecarMetadataJson;
|
|
122
|
+
try {
|
|
123
|
+
sidecarMetadataJson = JSON.stringify(
|
|
124
|
+
{ ...JSON.parse(entry.metadataJson), reporterNote, reporterRedactionMode: "off" },
|
|
125
|
+
null,
|
|
126
|
+
2,
|
|
127
|
+
);
|
|
128
|
+
} catch {
|
|
129
|
+
sidecarMetadataJson = entry.metadataJson;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const { savedPath, fileSize } = deps.saveBundle({ bundleId: msg.bundleId, savedMs: deps.now(), zip: entry.zip, metadataJson: sidecarMetadataJson });
|
|
133
|
+
deps.emit("bundle_written", { requestId: msg.requestId, bundleId: msg.bundleId, fileSize });
|
|
134
|
+
deps.send(clientId, { type: "debug-bundle-saved", requestId: msg.requestId, bundleId: msg.bundleId, savedPath, fileSize });
|
|
135
|
+
} catch {
|
|
136
|
+
deps.emit("save_failed", { requestId: msg.requestId, reason: "save_failed" });
|
|
137
|
+
deps.send(clientId, { type: "debug-bundle-error", requestId: msg.requestId, reason: "save_failed" });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function handleDebugBundleFetch(deps, clientId, msg) {
|
|
143
|
+
if (!deps.gatesOn()) {
|
|
144
|
+
deps.emit("fetch_refused", { requestId: msg.requestId, reason: "upload_not_allowed" });
|
|
145
|
+
deps.send(clientId, { type: "debug-bundle-error", requestId: msg.requestId, reason: "upload_not_allowed" });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const entry = deps.cacheGet(msg.bundleId);
|
|
149
|
+
if (!entry) {
|
|
150
|
+
deps.emit("fetch_expired", { requestId: msg.requestId, bundleId: msg.bundleId });
|
|
151
|
+
deps.send(clientId, { type: "debug-bundle-error", requestId: msg.requestId, reason: "bundle_expired" });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const chunks = chunkZip(entry.zip, deps.chunkBytes);
|
|
155
|
+
for (const chunk of chunks) {
|
|
156
|
+
deps.send(clientId, { type: "debug-bundle", requestId: msg.requestId, bundleId: msg.bundleId, partIndex: chunk.partIndex, partCount: chunk.partCount, partBase64: chunk.partBase64, bundleSha256: entry.bundleSha256 });
|
|
157
|
+
}
|
|
158
|
+
deps.emit("handoff_complete", { requestId: msg.requestId, bundleId: msg.bundleId, parts: chunks.length });
|
|
159
|
+
}
|
|
@@ -4,24 +4,13 @@ import * as path from "node:path";
|
|
|
4
4
|
const DEFAULTS = { emoji: false, pace: false };
|
|
5
5
|
const STORE_FILENAME = "ocuclaw-display-toggles.json";
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Tracks, per session, the display-feature toggle state at session start
|
|
9
|
-
* (frozen on first record) and the latest reported state.
|
|
10
|
-
*
|
|
11
|
-
* The FROZEN start-state is persisted to stateDir so it survives a relay/plugin
|
|
12
|
-
* restart — the Channel-1 prompt snapshot is also persisted, so without this the
|
|
13
|
-
* Channel-2 disable stop-notice would be lost after a restart (a session that
|
|
14
|
-
* started with a feature ON would re-freeze start=OFF from the first post-restart
|
|
15
|
-
* send and never notice the disable). The CURRENT state is NOT persisted: it
|
|
16
|
-
* re-derives from the next send, so persisting it would mean a write per turn.
|
|
17
|
-
*/
|
|
18
7
|
export function createDisplayToggleTracker(opts = {}) {
|
|
19
8
|
const limit = Number.isFinite(opts.limit) ? opts.limit : 200;
|
|
20
9
|
const statePath =
|
|
21
10
|
typeof opts.stateDir === "string" && opts.stateDir.trim()
|
|
22
11
|
? path.join(opts.stateDir.trim(), STORE_FILENAME)
|
|
23
12
|
: null;
|
|
24
|
-
|
|
13
|
+
|
|
25
14
|
const byKey = new Map();
|
|
26
15
|
|
|
27
16
|
function norm(v) {
|
|
@@ -36,13 +25,13 @@ export function createDisplayToggleTracker(opts = {}) {
|
|
|
36
25
|
for (const [k, v] of Object.entries(parsed.entries)) {
|
|
37
26
|
if (v && v.start) {
|
|
38
27
|
const start = norm(v.start);
|
|
39
|
-
|
|
28
|
+
|
|
40
29
|
byKey.set(k, { start, current: { ...start } });
|
|
41
30
|
}
|
|
42
31
|
}
|
|
43
32
|
}
|
|
44
33
|
} catch (_e) {
|
|
45
|
-
|
|
34
|
+
|
|
46
35
|
}
|
|
47
36
|
}
|
|
48
37
|
|
|
@@ -55,7 +44,7 @@ export function createDisplayToggleTracker(opts = {}) {
|
|
|
55
44
|
fs.writeFileSync(tmp, JSON.stringify({ version: 1, entries }), { mode: 0o600 });
|
|
56
45
|
fs.renameSync(tmp, statePath);
|
|
57
46
|
} catch (_e) {
|
|
58
|
-
|
|
47
|
+
|
|
59
48
|
}
|
|
60
49
|
}
|
|
61
50
|
|
|
@@ -76,10 +65,10 @@ export function createDisplayToggleTracker(opts = {}) {
|
|
|
76
65
|
if (!existing) {
|
|
77
66
|
byKey.set(sessionKey, { start: cur, current: cur });
|
|
78
67
|
evictIfNeeded();
|
|
79
|
-
persist();
|
|
68
|
+
persist();
|
|
80
69
|
return;
|
|
81
70
|
}
|
|
82
|
-
existing.current = cur;
|
|
71
|
+
existing.current = cur;
|
|
83
72
|
},
|
|
84
73
|
getStart(sessionKey) {
|
|
85
74
|
const e = byKey.get(sessionKey);
|