@makerbi/remodex 1.5.3 → 1.5.8
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/bin/remodex-jsonl-diagnose.js +0 -0
- package/bin/remodex.js +42 -10
- package/package.json +1 -1
- package/src/account-status.js +7 -1
- package/src/apply-patch-changes.js +185 -0
- package/src/bootstrap-codex-cli.js +1 -1
- package/src/bridge-status.js +122 -0
- package/src/bridge.js +823 -157
- package/src/codex-transport.js +38 -11
- package/src/desktop-handler.js +14 -1
- package/src/desktop-ipc-action-follower.js +173 -1
- package/src/index.js +4 -2
- package/src/macos-launch-agent.js +87 -2
- package/src/project-handler.js +162 -1
- package/src/push-notification-service-client.js +85 -37
- package/src/push-notification-tracker.js +15 -0
- package/src/rollout-live-mirror.js +331 -20
- package/src/rollout-watch.js +5 -1
- package/src/secure-device-state.js +26 -1
- package/src/secure-transport.js +40 -12
- package/src/session-jsonl-history.js +860 -14
- package/src/voice-handler.js +129 -62
- package/src/workspace-handler.js +327 -14
- package/src/private-defaults.json +0 -4
package/src/project-handler.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// Purpose: Serves safe Mac-local project folder discovery and creation requests from the iOS app.
|
|
3
3
|
// Layer: Bridge handler
|
|
4
4
|
// Exports: handleProjectRequest plus testable project filesystem helpers
|
|
5
|
-
// Depends on: fs, os, path
|
|
5
|
+
// Depends on: fs, os, path, ./codex-home
|
|
6
6
|
|
|
7
7
|
const fs = require("fs");
|
|
8
8
|
const os = require("os");
|
|
9
9
|
const path = require("path");
|
|
10
|
+
const { resolveCodexHome } = require("./codex-home");
|
|
10
11
|
|
|
11
12
|
const DEFAULT_DIRECTORY_LIMIT = 200;
|
|
12
13
|
const DEFAULT_DIRECTORY_SEARCH_LIMIT = 80;
|
|
@@ -14,6 +15,14 @@ const DEFAULT_DIRECTORY_SEARCH_MAX_DEPTH = 8;
|
|
|
14
15
|
const DEFAULT_DIRECTORY_SEARCH_MAX_VISITED = 5000;
|
|
15
16
|
const DEFAULT_HIDDEN_DIRECTORY_NAMES = new Set(["Library"]);
|
|
16
17
|
|
|
18
|
+
// Rootless chat slug rules mirror Codex Desktop's `~/Documents/Codex/<DATE>/<slug>`
|
|
19
|
+
// convention so iOS-created Quick Chats land in the same bucket Desktop classifies
|
|
20
|
+
// as projectless.
|
|
21
|
+
const ROOTLESS_CHAT_SLUG_MAX_TOKENS = 6;
|
|
22
|
+
const ROOTLESS_CHAT_SLUG_MAX_LENGTH = 60;
|
|
23
|
+
const ROOTLESS_CHAT_SLUG_FALLBACK = "new-chat";
|
|
24
|
+
const ROOTLESS_CHAT_DEDUP_LIMIT = 50;
|
|
25
|
+
|
|
17
26
|
// ─── ENTRY POINT ─────────────────────────────────────────────
|
|
18
27
|
|
|
19
28
|
function handleProjectRequest(rawMessage, sendResponse) {
|
|
@@ -58,6 +67,8 @@ async function handleProjectMethod(method, params, options = {}) {
|
|
|
58
67
|
switch (method) {
|
|
59
68
|
case "project/quickLocations":
|
|
60
69
|
return projectQuickLocations(options);
|
|
70
|
+
case "project/projectlessRoots":
|
|
71
|
+
return projectProjectlessRoots(options);
|
|
61
72
|
case "project/listDirectory":
|
|
62
73
|
return projectListDirectory(params, options);
|
|
63
74
|
case "project/searchDirectories":
|
|
@@ -66,6 +77,8 @@ async function handleProjectMethod(method, params, options = {}) {
|
|
|
66
77
|
return projectValidatePath(params, options);
|
|
67
78
|
case "project/createDirectory":
|
|
68
79
|
return projectCreateDirectory(params, options);
|
|
80
|
+
case "project/createRootlessChatRoot":
|
|
81
|
+
return projectCreateRootlessChatRoot(params, options);
|
|
69
82
|
default:
|
|
70
83
|
throw projectError("unknown_method", `Unknown project method: ${method}`);
|
|
71
84
|
}
|
|
@@ -99,6 +112,24 @@ async function projectQuickLocations(options = {}) {
|
|
|
99
112
|
return { locations };
|
|
100
113
|
}
|
|
101
114
|
|
|
115
|
+
async function projectProjectlessRoots(options = {}) {
|
|
116
|
+
const homeDir = resolveHomeDir(options);
|
|
117
|
+
const codexHome = path.resolve(readString(options.codexHome) || resolveCodexHome());
|
|
118
|
+
const documentedThreadsRoot = path.join(codexHome, "threads");
|
|
119
|
+
const desktopDocumentsRoot = path.join(homeDir, "Documents", "Codex");
|
|
120
|
+
const roots = uniqueExistingOrCandidatePaths([
|
|
121
|
+
documentedThreadsRoot,
|
|
122
|
+
desktopDocumentsRoot,
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
codexHome,
|
|
127
|
+
roots,
|
|
128
|
+
documentedThreadsRoot,
|
|
129
|
+
desktopDocumentsRoot,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
102
133
|
async function projectListDirectory(params, options = {}) {
|
|
103
134
|
const requestedPath = readString(params.path) || resolveHomeDir(options);
|
|
104
135
|
const directory = await requireUsableDirectory(requestedPath, options);
|
|
@@ -184,6 +215,116 @@ async function projectCreateDirectory(params, options = {}) {
|
|
|
184
215
|
};
|
|
185
216
|
}
|
|
186
217
|
|
|
218
|
+
// Mirrors the Codex Desktop "Chats" convention by materializing a dated rootless
|
|
219
|
+
// chat folder under `~/Documents/Codex/<DATE>/<slug>` (or its Windows equivalent
|
|
220
|
+
// `%USERPROFILE%\Documents\Codex\<DATE>\<slug>`) before the iOS client issues
|
|
221
|
+
// `thread/start`. Without an explicit cwd the app-server falls back to its own
|
|
222
|
+
// process working directory (often the user's home), which would otherwise show
|
|
223
|
+
// up in the sidebar as a project named after the user account.
|
|
224
|
+
async function projectCreateRootlessChatRoot(params = {}, options = {}) {
|
|
225
|
+
const homeDir = resolveHomeDir(options);
|
|
226
|
+
const desktopDocumentsRoot = path.join(homeDir, "Documents", "Codex");
|
|
227
|
+
const dateFolder = readString(params.dateFolder) || formatRootlessChatDate(new Date());
|
|
228
|
+
if (!isISODateFolderName(dateFolder)) {
|
|
229
|
+
throw projectError("invalid_date_folder", "The chat date folder must be in YYYY-MM-DD format.");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const slugBase = rootlessChatSlugFromPromptHint(params.promptHint);
|
|
233
|
+
const dateRootPath = path.join(desktopDocumentsRoot, dateFolder);
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
await fs.promises.mkdir(dateRootPath, { recursive: true });
|
|
237
|
+
} catch (error) {
|
|
238
|
+
throw projectError("create_failed", error?.message || "Unable to prepare the Codex chats folder.");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const targetPath = await reserveUniqueRootlessChatPath(dateRootPath, slugBase);
|
|
242
|
+
try {
|
|
243
|
+
await fs.promises.mkdir(targetPath, { recursive: false });
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (error?.code !== "EEXIST") {
|
|
246
|
+
throw projectError("create_failed", error?.message || "Unable to create the rootless chat folder.");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const resolvedPath = await safeRealpath(targetPath);
|
|
251
|
+
return {
|
|
252
|
+
path: resolvedPath,
|
|
253
|
+
parentPath: dateRootPath,
|
|
254
|
+
name: path.basename(resolvedPath),
|
|
255
|
+
slug: slugBase,
|
|
256
|
+
dateFolder,
|
|
257
|
+
root: desktopDocumentsRoot,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Codex Desktop generates kebab-case slugs from the first words of the prompt
|
|
262
|
+
// (e.g. "mi-dici-la-pwd-corrente" from "mi dici la pwd corrente?"). We mirror
|
|
263
|
+
// the same shape so that side-by-side users with the desktop app see a
|
|
264
|
+
// consistent chat folder naming scheme.
|
|
265
|
+
function rootlessChatSlugFromPromptHint(rawPromptHint) {
|
|
266
|
+
const hint = typeof rawPromptHint === "string" ? rawPromptHint.normalize("NFKD") : "";
|
|
267
|
+
const sanitized = hint
|
|
268
|
+
.toLowerCase()
|
|
269
|
+
.replace(/[^a-z0-9]+/gu, " ")
|
|
270
|
+
.trim();
|
|
271
|
+
if (!sanitized) {
|
|
272
|
+
return ROOTLESS_CHAT_SLUG_FALLBACK;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const tokens = sanitized
|
|
276
|
+
.split(/\s+/)
|
|
277
|
+
.filter(Boolean)
|
|
278
|
+
.slice(0, ROOTLESS_CHAT_SLUG_MAX_TOKENS);
|
|
279
|
+
if (!tokens.length) {
|
|
280
|
+
return ROOTLESS_CHAT_SLUG_FALLBACK;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let slug = tokens.join("-");
|
|
284
|
+
if (slug.length > ROOTLESS_CHAT_SLUG_MAX_LENGTH) {
|
|
285
|
+
slug = slug.slice(0, ROOTLESS_CHAT_SLUG_MAX_LENGTH).replace(/-+$/g, "");
|
|
286
|
+
}
|
|
287
|
+
return slug || ROOTLESS_CHAT_SLUG_FALLBACK;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Appends "-2", "-3", … so two rapid-fire chats with the same first words
|
|
291
|
+
// keep distinct folders instead of fighting over the same path.
|
|
292
|
+
async function reserveUniqueRootlessChatPath(parentDirectory, slugBase) {
|
|
293
|
+
for (let attempt = 0; attempt < ROOTLESS_CHAT_DEDUP_LIMIT; attempt += 1) {
|
|
294
|
+
const candidateSlug = attempt === 0 ? slugBase : `${slugBase}-${attempt + 1}`;
|
|
295
|
+
const candidatePath = path.join(parentDirectory, candidateSlug);
|
|
296
|
+
try {
|
|
297
|
+
await fs.promises.access(candidatePath, fs.constants.F_OK);
|
|
298
|
+
} catch {
|
|
299
|
+
return candidatePath;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return path.join(parentDirectory, `${slugBase}-${Date.now()}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function formatRootlessChatDate(date) {
|
|
307
|
+
const year = date.getFullYear().toString().padStart(4, "0");
|
|
308
|
+
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
|
309
|
+
const day = date.getDate().toString().padStart(2, "0");
|
|
310
|
+
return `${year}-${month}-${day}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function isISODateFolderName(value) {
|
|
314
|
+
if (typeof value !== "string" || value.length !== 10) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
return /^\d{4}-\d{2}-\d{2}$/u.test(value);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function safeRealpath(candidatePath) {
|
|
321
|
+
try {
|
|
322
|
+
return await fs.promises.realpath(candidatePath);
|
|
323
|
+
} catch {
|
|
324
|
+
return path.resolve(candidatePath);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
187
328
|
// ─── Filesystem Helpers ──────────────────────────────────────
|
|
188
329
|
|
|
189
330
|
async function readDirectoryEntries(directoryPath, options = {}) {
|
|
@@ -462,6 +603,23 @@ function resolveHomeDir(options = {}) {
|
|
|
462
603
|
return options.homeDir || os.homedir();
|
|
463
604
|
}
|
|
464
605
|
|
|
606
|
+
function uniqueExistingOrCandidatePaths(paths) {
|
|
607
|
+
const seen = new Set();
|
|
608
|
+
const result = [];
|
|
609
|
+
|
|
610
|
+
for (const candidatePath of paths) {
|
|
611
|
+
const normalizedPath = path.resolve(candidatePath);
|
|
612
|
+
const realPath = realpathSyncIfAvailable(normalizedPath) || normalizedPath;
|
|
613
|
+
if (seen.has(realPath)) {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
seen.add(realPath);
|
|
617
|
+
result.push(realPath);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return result;
|
|
621
|
+
}
|
|
622
|
+
|
|
465
623
|
function realpathSyncIfAvailable(candidatePath) {
|
|
466
624
|
try {
|
|
467
625
|
return fs.realpathSync(candidatePath);
|
|
@@ -485,9 +643,12 @@ module.exports = {
|
|
|
485
643
|
handleProjectRequest,
|
|
486
644
|
handleProjectMethod,
|
|
487
645
|
projectQuickLocations,
|
|
646
|
+
projectProjectlessRoots,
|
|
488
647
|
projectListDirectory,
|
|
489
648
|
projectSearchDirectories,
|
|
490
649
|
projectValidatePath,
|
|
491
650
|
projectCreateDirectory,
|
|
651
|
+
projectCreateRootlessChatRoot,
|
|
492
652
|
validateDirectory,
|
|
653
|
+
rootlessChatSlugFromPromptHint,
|
|
493
654
|
};
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// Depends on: global fetch
|
|
6
6
|
|
|
7
7
|
const DEFAULT_PUSH_SERVICE_TIMEOUT_MS = 10_000;
|
|
8
|
+
const DEFAULT_PUSH_SERVICE_RETRY_LIMIT = 2;
|
|
9
|
+
const DEFAULT_PUSH_SERVICE_RETRY_BASE_DELAY_MS = 500;
|
|
8
10
|
|
|
9
11
|
function createPushNotificationServiceClient({
|
|
10
12
|
baseUrl = "",
|
|
@@ -13,8 +15,16 @@ function createPushNotificationServiceClient({
|
|
|
13
15
|
fetchImpl = globalThis.fetch,
|
|
14
16
|
logPrefix = "[remodex]",
|
|
15
17
|
requestTimeoutMs = DEFAULT_PUSH_SERVICE_TIMEOUT_MS,
|
|
18
|
+
retryLimit = DEFAULT_PUSH_SERVICE_RETRY_LIMIT,
|
|
19
|
+
retryBaseDelayMs = DEFAULT_PUSH_SERVICE_RETRY_BASE_DELAY_MS,
|
|
20
|
+
sleepImpl = sleep,
|
|
16
21
|
} = {}) {
|
|
17
22
|
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
23
|
+
const safeRetryLimit = normalizeNonNegativeInteger(retryLimit, DEFAULT_PUSH_SERVICE_RETRY_LIMIT);
|
|
24
|
+
const safeRetryBaseDelayMs = normalizeNonNegativeInteger(
|
|
25
|
+
retryBaseDelayMs,
|
|
26
|
+
DEFAULT_PUSH_SERVICE_RETRY_BASE_DELAY_MS
|
|
27
|
+
);
|
|
18
28
|
|
|
19
29
|
async function registerDevice({
|
|
20
30
|
deviceToken,
|
|
@@ -55,48 +65,70 @@ function createPushNotificationServiceClient({
|
|
|
55
65
|
return { ok: false, skipped: true };
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
let response;
|
|
68
|
-
try {
|
|
69
|
-
response = await fetchImpl(`${normalizedBaseUrl}${pathname}`, {
|
|
70
|
-
method: "POST",
|
|
71
|
-
headers: {
|
|
72
|
-
"content-type": "application/json",
|
|
73
|
-
},
|
|
74
|
-
body: JSON.stringify(payload),
|
|
75
|
-
signal: controller?.signal,
|
|
76
|
-
});
|
|
77
|
-
} catch (error) {
|
|
78
|
-
if (isAbortError(error)) {
|
|
79
|
-
const timeoutError = new Error(`Push service request timed out after ${requestTimeoutMs}ms`);
|
|
80
|
-
timeoutError.code = "push_request_timeout";
|
|
81
|
-
throw timeoutError;
|
|
68
|
+
const bodyJSON = JSON.stringify(payload);
|
|
69
|
+
let lastError = null;
|
|
70
|
+
for (let attempt = 0; attempt <= safeRetryLimit; attempt += 1) {
|
|
71
|
+
if (attempt > 0) {
|
|
72
|
+
const delayMs = safeRetryBaseDelayMs * Math.pow(2, attempt - 1);
|
|
73
|
+
if (delayMs > 0) {
|
|
74
|
+
await sleepImpl(delayMs);
|
|
75
|
+
}
|
|
82
76
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
|
|
78
|
+
const controller = typeof AbortController === "function" && requestTimeoutMs > 0
|
|
79
|
+
? new AbortController()
|
|
80
|
+
: null;
|
|
81
|
+
const timeoutID = controller
|
|
82
|
+
? setTimeout(() => {
|
|
83
|
+
controller.abort(createTimeoutAbortError(requestTimeoutMs));
|
|
84
|
+
}, requestTimeoutMs)
|
|
85
|
+
: null;
|
|
86
|
+
|
|
87
|
+
let response;
|
|
88
|
+
try {
|
|
89
|
+
response = await fetchImpl(`${normalizedBaseUrl}${pathname}`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: {
|
|
92
|
+
"content-type": "application/json",
|
|
93
|
+
},
|
|
94
|
+
body: bodyJSON,
|
|
95
|
+
signal: controller?.signal,
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
lastError = error;
|
|
99
|
+
if (isAbortError(error)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (isRetryableNetworkError(error)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
} finally {
|
|
107
|
+
if (timeoutID) {
|
|
108
|
+
clearTimeout(timeoutID);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const responseText = await response.text();
|
|
113
|
+
const parsed = safeParseJSON(responseText);
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const message = parsed?.error || parsed?.message || responseText || `HTTP ${response.status}`;
|
|
116
|
+
const error = new Error(message);
|
|
117
|
+
error.status = response.status;
|
|
118
|
+
if (response.status >= 500 && attempt < safeRetryLimit) {
|
|
119
|
+
lastError = error;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
87
123
|
}
|
|
88
|
-
}
|
|
89
124
|
|
|
90
|
-
|
|
91
|
-
const parsed = safeParseJSON(responseText);
|
|
92
|
-
if (!response.ok) {
|
|
93
|
-
const message = parsed?.error || parsed?.message || responseText || `HTTP ${response.status}`;
|
|
94
|
-
const error = new Error(message);
|
|
95
|
-
error.status = response.status;
|
|
96
|
-
throw error;
|
|
125
|
+
return parsed ?? { ok: true };
|
|
97
126
|
}
|
|
98
127
|
|
|
99
|
-
|
|
128
|
+
if (lastError) {
|
|
129
|
+
throw lastError;
|
|
130
|
+
}
|
|
131
|
+
return { ok: false };
|
|
100
132
|
}
|
|
101
133
|
|
|
102
134
|
return {
|
|
@@ -127,6 +159,7 @@ function normalizeBaseUrl(value) {
|
|
|
127
159
|
function createTimeoutAbortError(timeoutMs) {
|
|
128
160
|
const error = new Error(`Push service request timed out after ${timeoutMs}ms`);
|
|
129
161
|
error.name = "AbortError";
|
|
162
|
+
error.code = "push_request_timeout";
|
|
130
163
|
return error;
|
|
131
164
|
}
|
|
132
165
|
|
|
@@ -134,6 +167,21 @@ function isAbortError(error) {
|
|
|
134
167
|
return error?.name === "AbortError" || error?.code === "ABORT_ERR";
|
|
135
168
|
}
|
|
136
169
|
|
|
170
|
+
function isRetryableNetworkError(error) {
|
|
171
|
+
const code = error?.code ?? "";
|
|
172
|
+
return code === "ECONNRESET" || code === "ECONNREFUSED" || code === "ETIMEDOUT"
|
|
173
|
+
|| code === "ENETUNREACH" || code === "EHOSTUNREACH" || code === "ENOTFOUND"
|
|
174
|
+
|| error?.message?.includes("fetch failed");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normalizeNonNegativeInteger(value, fallback) {
|
|
178
|
+
return Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function sleep(delayMs) {
|
|
182
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
183
|
+
}
|
|
184
|
+
|
|
137
185
|
function safeParseJSON(value) {
|
|
138
186
|
if (!value || typeof value !== "string") {
|
|
139
187
|
return null;
|
|
@@ -9,6 +9,9 @@ const {
|
|
|
9
9
|
} = require("./push-notification-completion-dedupe");
|
|
10
10
|
|
|
11
11
|
const DEFAULT_PREVIEW_MAX_CHARS = 160;
|
|
12
|
+
const MAX_THREAD_TITLE_ENTRIES = 200;
|
|
13
|
+
const MAX_TURN_STATE_ENTRIES = 500;
|
|
14
|
+
const MAX_THREAD_ID_BY_TURN_ENTRIES = 500;
|
|
12
15
|
|
|
13
16
|
function createPushNotificationTracker({
|
|
14
17
|
sessionId,
|
|
@@ -73,6 +76,10 @@ function createPushNotificationTracker({
|
|
|
73
76
|
// Remembers thread/turn linkage before the terminal event arrives on a different payload shape.
|
|
74
77
|
function rememberMessageContext({ threadId, turnId, params, eventObject }) {
|
|
75
78
|
if (threadId && turnId) {
|
|
79
|
+
if (!threadIdByTurnId.has(turnId) && threadIdByTurnId.size >= MAX_THREAD_ID_BY_TURN_ENTRIES) {
|
|
80
|
+
const oldest = threadIdByTurnId.keys().next().value;
|
|
81
|
+
threadIdByTurnId.delete(oldest);
|
|
82
|
+
}
|
|
76
83
|
threadIdByTurnId.set(turnId, threadId);
|
|
77
84
|
ensureTurnState(threadId, turnId);
|
|
78
85
|
}
|
|
@@ -83,6 +90,10 @@ function createPushNotificationTracker({
|
|
|
83
90
|
|
|
84
91
|
const nextTitle = extractThreadTitle(params, eventObject);
|
|
85
92
|
if (nextTitle) {
|
|
93
|
+
if (!threadTitleById.has(threadId) && threadTitleById.size >= MAX_THREAD_TITLE_ENTRIES) {
|
|
94
|
+
const oldest = threadTitleById.keys().next().value;
|
|
95
|
+
threadTitleById.delete(oldest);
|
|
96
|
+
}
|
|
86
97
|
threadTitleById.set(threadId, nextTitle);
|
|
87
98
|
}
|
|
88
99
|
}
|
|
@@ -225,6 +236,10 @@ function createPushNotificationTracker({
|
|
|
225
236
|
function ensureTurnState(threadId, turnId) {
|
|
226
237
|
const key = turnStateKey(threadId, turnId);
|
|
227
238
|
if (!turnStateByKey.has(key)) {
|
|
239
|
+
if (turnStateByKey.size >= MAX_TURN_STATE_ENTRIES) {
|
|
240
|
+
const oldest = turnStateByKey.keys().next().value;
|
|
241
|
+
turnStateByKey.delete(oldest);
|
|
242
|
+
}
|
|
228
243
|
turnStateByKey.set(key, {
|
|
229
244
|
latestAssistantPreview: "",
|
|
230
245
|
latestFailurePreview: "",
|