@walkhi/code-relax 0.1.0-beta.1 → 0.1.0-beta.3
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 +5 -5
- package/dist/bin/self-relay-server.mjs +5 -2
- package/dist/shared/app-server-events.cjs +45 -19
- package/dist/shared/p2p-data-channel.cjs +50 -0
- package/dist/src/app-server-tasks.mjs +28 -25
- package/dist/src/lan-socket-server.mjs +8 -2
- package/dist/src/platform/windows/IsolatedProcess.cs +7 -3
- package/dist/src/platform/windows/background-process.mjs +7 -2
- package/dist/src/platform/windows/launch-worker.mjs +42 -0
- package/dist/src/platform/windows/start-hidden-console.ps1 +18 -8
- package/dist/src/self-relay/admin-state.mjs +61 -0
- package/dist/src/self-relay/client.mjs +2 -0
- package/dist/src/self-relay/connector.mjs +23 -8
- package/dist/src/self-relay/demo.mjs +2 -2
- package/dist/src/self-relay/lifecycle.mjs +1 -1
- package/dist/src/self-relay/p2p-probe.mjs +123 -83
- package/dist/src/self-relay/server.mjs +137 -11
- package/dist/src/server.mjs +337 -255
- package/dist/src/shared-app-server.mjs +180 -24
- package/dist/src/shared-catalog.mjs +4 -14
- package/dist/src/thread-catalog.mjs +12 -7
- package/dist/web/activity-view.js +283 -0
- package/dist/web/capabilities.js +2 -2
- package/dist/web/chat-transport.js +15 -9
- package/dist/web/chat.css +139 -93
- package/dist/web/chat.js +1451 -2770
- package/dist/web/community-view.js +20 -0
- package/dist/web/community.css +77 -0
- package/dist/web/community.html +37 -0
- package/dist/web/composer-controller.js +101 -0
- package/dist/web/conversation-controller.js +99 -0
- package/dist/web/disclosure-state-controller.js +95 -0
- package/dist/web/draft-controller.js +103 -0
- package/dist/web/harmony-platform.js +3 -2
- package/dist/web/history-cache.js +112 -18
- package/dist/web/history-controller.js +167 -0
- package/dist/web/index.html +141 -38
- package/dist/web/link-action-controller.js +212 -0
- package/dist/web/message-send-controller.js +177 -0
- package/dist/web/message-view.js +98 -0
- package/dist/web/p2p-data-channel.js +50 -0
- package/dist/web/p2p-probe.js +67 -27
- package/dist/web/page-resume.js +28 -0
- package/dist/web/pending-message-store.js +108 -0
- package/dist/web/queue-controller.js +82 -0
- package/dist/web/resources.json +1 -1
- package/dist/web/self-relay-session.js +28 -18
- package/dist/web/station-connection-controller.js +75 -0
- package/dist/web/task-list-view.js +296 -0
- package/dist/web/thread-attention-controller.js +124 -0
- package/dist/web/thread-context-controller.js +30 -0
- package/dist/web/thread-list-controller.js +61 -0
- package/dist/web/thread-list-sync.js +86 -0
- package/dist/web/thread-title-controller.js +57 -0
- package/dist/web/timeline-formatters.js +249 -0
- package/dist/web/timeline-reducer.js +81 -0
- package/dist/web/timeline-renderer.js +161 -0
- package/dist/web/timeline-scroll-controller.js +50 -0
- package/dist/web/usage-controller.js +258 -0
- package/dist/web/vendor/lucide.LICENSE.txt +17 -0
- package/package.json +1 -1
- package/tools/postinstall.mjs +111 -2
- package/dist/src/platform/windows/launch-worker.ps1 +0 -13
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
const limit = 100 * 1024 * 1024;
|
|
4
4
|
let opening, writes = Promise.resolve(), displayVersion = 0;
|
|
5
5
|
const urls = new Map();
|
|
6
|
+
function metric(detail) {
|
|
7
|
+
try { window.codexNative?.reportSyncMetric?.(detail); } catch {}
|
|
8
|
+
}
|
|
6
9
|
function database() {
|
|
7
10
|
if (!opening) opening = new Promise((resolve, reject) => {
|
|
8
11
|
const request = indexedDB.open('codex-remote-history-v1', 1);
|
|
@@ -22,11 +25,76 @@
|
|
|
22
25
|
request.onerror = () => reject(request.error);
|
|
23
26
|
});
|
|
24
27
|
}
|
|
25
|
-
function release() {
|
|
28
|
+
function release() {
|
|
29
|
+
displayVersion++;
|
|
30
|
+
const released = urls.size;
|
|
31
|
+
for (const url of urls.values()) URL.revokeObjectURL(url);
|
|
32
|
+
urls.clear();
|
|
33
|
+
metric(`stage=image-cache;action=release-all;released=${released};version=${displayVersion}`);
|
|
34
|
+
}
|
|
35
|
+
function releaseUnused(value, reason = 'sync') {
|
|
36
|
+
const retained = new Set();
|
|
37
|
+
function collect(child) {
|
|
38
|
+
if (typeof child === 'string') { retained.add(child); return; }
|
|
39
|
+
if (Array.isArray(child)) { for (const item of child) collect(item); return; }
|
|
40
|
+
if (!child || typeof child !== 'object') return;
|
|
41
|
+
for (const item of Object.values(child)) collect(item);
|
|
42
|
+
}
|
|
43
|
+
collect(value);
|
|
44
|
+
let released = 0;
|
|
45
|
+
for (const [key, url] of urls) {
|
|
46
|
+
if (retained.has(url)) continue;
|
|
47
|
+
URL.revokeObjectURL(url); urls.delete(key); released++;
|
|
48
|
+
}
|
|
49
|
+
metric(`stage=image-cache;action=release-unused;reason=${reason};released=${released};retained=${urls.size}`);
|
|
50
|
+
}
|
|
51
|
+
function compactActivity(entry) {
|
|
52
|
+
if (!entry || entry.type !== 'activity') return entry;
|
|
53
|
+
if (entry.titleOnly || ['imageGeneration', 'imageView', 'lazyOperationGroup'].includes(entry.kind)) return entry;
|
|
54
|
+
return { type: 'activity', id: entry.id, kind: entry.kind, title: entry.title,
|
|
55
|
+
...(entry.kind === 'reasoning' && entry.detail ? { detail: entry.detail } : {}),
|
|
56
|
+
...(entry.kind === 'webSearch' ? { query: entry.query || '', ...(entry.action ? { action: entry.action } : {}) } : {}),
|
|
57
|
+
...(entry.status ? { status: entry.status } : {}) };
|
|
58
|
+
}
|
|
59
|
+
function compactTimeline(data) {
|
|
60
|
+
if (!data || !Array.isArray(data.turns)) return data;
|
|
61
|
+
return { ...data, turns: data.turns.map(turn => {
|
|
62
|
+
const entries = Array.isArray(turn.entries) ? turn.entries : [];
|
|
63
|
+
const activities = entries.filter(entry => entry?.type === 'activity' && !entry.titleOnly);
|
|
64
|
+
const details = Array.isArray(turn.toolDetails) ? turn.toolDetails : [];
|
|
65
|
+
const byId = new Map(details.map((entry, index) => [entry?.id || `detail:${index}`, entry]));
|
|
66
|
+
for (const [index, entry] of activities.entries()) {
|
|
67
|
+
const key = entry?.id || `activity:${index}`;
|
|
68
|
+
if (!byId.has(key)) byId.set(key, entry);
|
|
69
|
+
}
|
|
70
|
+
return { ...turn, entries: entries.map((entry, index) => {
|
|
71
|
+
const compact = compactActivity(entry);
|
|
72
|
+
if (compact?.kind !== 'webSearch' || compact.query || compact.action) return compact;
|
|
73
|
+
return compactActivity(byId.get(entry?.id || `activity:${index}`) || entry);
|
|
74
|
+
}),
|
|
75
|
+
...(byId.size ? { toolDetails: [...byId.values()] } : {}) };
|
|
76
|
+
}) };
|
|
77
|
+
}
|
|
78
|
+
function preserveToolDetails(data, previous) {
|
|
79
|
+
if (!data || !Array.isArray(data.turns) || !previous || !Array.isArray(previous.turns)) return data;
|
|
80
|
+
const previousTurns = new Map(compactTimeline(previous).turns.map(turn => [turn.id, turn]));
|
|
81
|
+
return { ...data, turns: data.turns.map(turn => {
|
|
82
|
+
const cached = previousTurns.get(turn.id)?.toolDetails || [];
|
|
83
|
+
if (!cached.length) return turn;
|
|
84
|
+
const current = Array.isArray(turn.toolDetails) ? turn.toolDetails : [];
|
|
85
|
+
const byId = new Map(cached.map((entry, index) => [entry?.id || `cached:${index}`, entry]));
|
|
86
|
+
for (const [index, entry] of current.entries()) {
|
|
87
|
+
const key = entry?.id || `current:${index}`;
|
|
88
|
+
if (!byId.has(key)) byId.set(key, entry);
|
|
89
|
+
}
|
|
90
|
+
return { ...turn, toolDetails: [...byId.values()] };
|
|
91
|
+
}) };
|
|
92
|
+
}
|
|
26
93
|
async function get(scope, id) {
|
|
27
94
|
const version = displayVersion;
|
|
28
95
|
const db = await database(), record = await read(db, 'threads', JSON.stringify([scope, id]));
|
|
29
96
|
if (!record) return null;
|
|
97
|
+
let created = 0, stale = 0;
|
|
30
98
|
async function unpack(value) {
|
|
31
99
|
if (version !== displayVersion) return null;
|
|
32
100
|
if (value && typeof value === 'object' && value.cacheImage) {
|
|
@@ -36,30 +104,48 @@
|
|
|
36
104
|
if (version !== displayVersion) return null;
|
|
37
105
|
if (!image) throw new Error('缓存图片缺失');
|
|
38
106
|
urls.set(key, URL.createObjectURL(image.blob));
|
|
107
|
+
created++;
|
|
39
108
|
}
|
|
40
109
|
return urls.get(key);
|
|
41
110
|
}
|
|
111
|
+
// A persisted blob URL only belongs to the WebView document that created
|
|
112
|
+
// it. Treat older poisoned cache entries as missing so remotePath can
|
|
113
|
+
// reload the image instead of rendering a permanently broken URL.
|
|
114
|
+
if (typeof value === 'string' && value.startsWith('blob:')) { stale++; return ''; }
|
|
42
115
|
if (Array.isArray(value)) { const result = []; for (const item of value) result.push(await unpack(item)); return result; }
|
|
43
116
|
if (value && typeof value === 'object') { const result = {}; for (const [k, v] of Object.entries(value)) result[k] = await unpack(v); return result; }
|
|
44
117
|
return value;
|
|
45
118
|
}
|
|
46
|
-
const data = await unpack(record.data);
|
|
47
|
-
|
|
119
|
+
const data = compactTimeline(await unpack(record.data));
|
|
120
|
+
metric(`stage=image-cache;action=read;created=${created};stale=${stale};active=${urls.size}`);
|
|
121
|
+
return version === displayVersion ? { ...data, cached: true } : null;
|
|
48
122
|
}
|
|
49
123
|
function save(scope, data, hash, id = data.thread.id) {
|
|
50
124
|
// Snapshot at scheduling time; live events may mutate the UI model while hashing.
|
|
51
|
-
|
|
125
|
+
let snapshot = structuredClone(compactTimeline({ ...data, turns: data.turns.slice(-40) }));
|
|
52
126
|
const key = JSON.stringify([scope, id]);
|
|
53
127
|
const work = writes.then(async () => {
|
|
128
|
+
let outcome = { retained: true, evictedThreads: 0, size: 0 };
|
|
54
129
|
const db = await database(), images = new Map(), byUrl = new Map(), referenced = new Set(), previousImages = new Map();
|
|
130
|
+
const objectUrlKeys = new Map([...urls].map(([imageKey, url]) => [url, imageKey]));
|
|
55
131
|
const previous = await read(db, 'threads', key);
|
|
132
|
+
snapshot = preserveToolDetails(snapshot, previous?.data);
|
|
56
133
|
function collect(value) {
|
|
57
134
|
if (!value || typeof value !== 'object') return;
|
|
58
|
-
if (value.remotePath && value.url?.cacheImage
|
|
135
|
+
if (value.remotePath && (value.url?.cacheImage || value.thumbnailUrl?.cacheImage)) {
|
|
136
|
+
previousImages.set(value.remotePath, {
|
|
137
|
+
original: value.url?.cacheImage || '', thumbnail: value.thumbnailUrl?.cacheImage || '',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
59
140
|
for (const child of Object.values(value)) collect(child);
|
|
60
141
|
}
|
|
61
142
|
collect(previous?.data);
|
|
62
143
|
async function pack(value) {
|
|
144
|
+
if (value && typeof value === 'object' && value.cacheImage) { referenced.add(value.cacheImage); return value; }
|
|
145
|
+
if (typeof value === 'string' && objectUrlKeys.has(value)) {
|
|
146
|
+
const imageKey = objectUrlKeys.get(value); referenced.add(imageKey); return { cacheImage: imageKey };
|
|
147
|
+
}
|
|
148
|
+
if (typeof value === 'string' && value.startsWith('blob:')) return '';
|
|
63
149
|
if (typeof value === 'string' && value.startsWith('data:image/')) {
|
|
64
150
|
if (!byUrl.has(value)) byUrl.set(value, (async () => {
|
|
65
151
|
const key = JSON.stringify([scope, await hash(value)]);
|
|
@@ -73,16 +159,19 @@
|
|
|
73
159
|
}
|
|
74
160
|
if (Array.isArray(value)) return Promise.all(value.map(pack));
|
|
75
161
|
if (value && typeof value === 'object') {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
162
|
+
let source = value;
|
|
163
|
+
if (value.remotePath && previousImages.has(value.remotePath)) {
|
|
164
|
+
const cached = previousImages.get(value.remotePath);
|
|
165
|
+
if (!value.url && cached.original) { referenced.add(cached.original); source = { ...source, available: true, url: { cacheImage: cached.original } }; }
|
|
166
|
+
if (!value.thumbnailUrl && cached.thumbnail) { referenced.add(cached.thumbnail); source = { ...source, available: true, thumbnailUrl: { cacheImage: cached.thumbnail } }; }
|
|
79
167
|
}
|
|
80
|
-
return Object.fromEntries(await Promise.all(Object.entries(
|
|
168
|
+
return Object.fromEntries(await Promise.all(Object.entries(source).map(async ([k, v]) => [k, await pack(v)])));
|
|
81
169
|
}
|
|
82
170
|
return value;
|
|
83
171
|
}
|
|
84
172
|
const packed = await pack(snapshot);
|
|
85
173
|
const record = { key, data: packed, at: Date.now(), imageKeys: [...referenced], size: new TextEncoder().encode(JSON.stringify(packed)).length };
|
|
174
|
+
outcome.size = record.size;
|
|
86
175
|
await new Promise((resolve, reject) => {
|
|
87
176
|
const tx = db.transaction(['threads', 'images', 'blobs'], 'readwrite');
|
|
88
177
|
tx.oncomplete = resolve; tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error || new Error('缓存写入中止'));
|
|
@@ -90,30 +179,35 @@
|
|
|
90
179
|
threads.put(record);
|
|
91
180
|
for (const image of images.values()) { metadata.put({ key: image.key, size: image.size }); blobs.put({ key: image.key, blob: image.blob }); }
|
|
92
181
|
const allThreads = threads.getAll(), allImages = metadata.getAll();
|
|
93
|
-
let records, sizes;
|
|
182
|
+
let records, sizes, pruned = false;
|
|
94
183
|
function prune() {
|
|
95
|
-
if (!records || !sizes) return;
|
|
184
|
+
if (!records || !sizes || pruned) return;
|
|
185
|
+
pruned = true;
|
|
96
186
|
records.sort((a, b) => b.at - a.at);
|
|
97
|
-
const keep = new Set(); let bytes = 0;
|
|
187
|
+
const keep = new Set(), keptThreads = new Set(); let bytes = 0;
|
|
98
188
|
for (const [index, item] of records.entries()) {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
bytes
|
|
189
|
+
const imageKeys = item.imageKeys || [];
|
|
190
|
+
const added = item.size + imageKeys.reduce((sum, image) => sum + (keep.has(image) ? 0 : sizes.get(image) || 0), 0);
|
|
191
|
+
if (index >= 20 || bytes + added > limit) { threads.delete(item.key); outcome.evictedThreads++; continue; }
|
|
192
|
+
keptThreads.add(item.key); bytes += added; for (const image of imageKeys) keep.add(image);
|
|
102
193
|
}
|
|
194
|
+
outcome.retained = keptThreads.has(key);
|
|
103
195
|
for (const image of sizes.keys()) if (!keep.has(image)) { metadata.delete(image); blobs.delete(image); }
|
|
104
196
|
}
|
|
105
197
|
allThreads.onsuccess = () => { records = allThreads.result; prune(); };
|
|
106
198
|
allImages.onsuccess = () => { sizes = new Map(allImages.result.map(image => [image.key, image.size])); prune(); };
|
|
107
199
|
});
|
|
200
|
+
return outcome;
|
|
108
201
|
});
|
|
109
202
|
writes = work.catch(() => {});
|
|
110
203
|
return work;
|
|
111
204
|
}
|
|
112
|
-
async function getImage(scope, id, path) {
|
|
205
|
+
async function getImage(scope, id, path, variant = 'original') {
|
|
113
206
|
const db = await database(), record = await read(db, 'threads', JSON.stringify([scope, id]));
|
|
114
207
|
function find(value) {
|
|
115
208
|
if (!value || typeof value !== 'object') return null;
|
|
116
|
-
|
|
209
|
+
const source = variant === 'thumbnail' ? value.thumbnailUrl : value.url;
|
|
210
|
+
if (value.remotePath === path && source?.cacheImage) return source.cacheImage;
|
|
117
211
|
for (const child of Object.values(value)) { const key = find(child); if (key) return key; }
|
|
118
212
|
return null;
|
|
119
213
|
}
|
|
@@ -142,5 +236,5 @@
|
|
|
142
236
|
const record = records.filter(item => JSON.parse(item.key)[0] === scope).sort((a, b) => b.at - a.at)[0];
|
|
143
237
|
return record ? get(scope, JSON.parse(record.key)[1]) : null;
|
|
144
238
|
}
|
|
145
|
-
window.codexHistoryCache = { get, getImage, save, latest, clear, release };
|
|
239
|
+
window.codexHistoryCache = { get, getImage, save, latest, clear, release, releaseUnused };
|
|
146
240
|
})();
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const create = factory(root);
|
|
3
|
+
if (typeof module === 'object' && module.exports) module.exports = create;
|
|
4
|
+
else root.createHistoryController = create;
|
|
5
|
+
})(globalThis, function (root) {
|
|
6
|
+
function createHistoryController(options) {
|
|
7
|
+
const { state, transport, api, isThreadRunning, report, onCacheError } = options;
|
|
8
|
+
let saveTimer, readVersion = 0, writeVersion = 0;
|
|
9
|
+
|
|
10
|
+
function updateCache(patch) {
|
|
11
|
+
state.historyCache = { ...state.historyCache, ...patch, changedAt: Date.now() };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function metric(action, fields) {
|
|
15
|
+
report(`stage=cache;action=${action};${Object.entries(fields).map(([key, value]) => `${key}=${value}`).join(';')}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function read(id, thread, reason = 'select') {
|
|
19
|
+
const version = ++readVersion;
|
|
20
|
+
const started = performance.now();
|
|
21
|
+
if (!transport.has('historyCache')) {
|
|
22
|
+
updateCache({ threadId: id, read: 'unavailable', readMs: 0, turns: 0, error: '' });
|
|
23
|
+
metric('read', { reason, result: 'unavailable', ms: 0, turns: 0 });
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
updateCache({ threadId: id, read: 'loading', readMs: 0, turns: 0, error: '' });
|
|
27
|
+
try {
|
|
28
|
+
const cached = await transport.readCached(id, thread);
|
|
29
|
+
const ms = Math.round(performance.now() - started), turns = cached?.turns?.length || 0;
|
|
30
|
+
if (version === readVersion) updateCache({ threadId: id, read: cached ? 'hit' : 'miss', readMs: ms, turns, error: '' });
|
|
31
|
+
metric('read', { reason, result: cached ? 'hit' : 'miss', ms, turns });
|
|
32
|
+
return cached || null;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
const ms = Math.round(performance.now() - started);
|
|
35
|
+
if (version === readVersion) updateCache({ threadId: id, read: 'error', readMs: ms, turns: 0, error: error.message });
|
|
36
|
+
metric('read', { reason, result: 'error', ms, turns: 0 });
|
|
37
|
+
onCacheError(error);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function readLatest(reason = 'startup') {
|
|
43
|
+
const version = ++readVersion;
|
|
44
|
+
const started = performance.now();
|
|
45
|
+
if (!transport.has('historyCache')) {
|
|
46
|
+
updateCache({ threadId: '', read: 'unavailable', readMs: 0, turns: 0, error: '' });
|
|
47
|
+
metric('read', { reason, result: 'unavailable', ms: 0, turns: 0 });
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
updateCache({ threadId: '', read: 'loading', readMs: 0, turns: 0, error: '' });
|
|
51
|
+
try {
|
|
52
|
+
const cached = await transport.readLastCached();
|
|
53
|
+
const ms = Math.round(performance.now() - started), turns = cached?.turns?.length || 0;
|
|
54
|
+
if (version === readVersion) updateCache({ threadId: cached?.thread?.id || '', read: cached ? 'hit' : 'miss', readMs: ms, turns, error: '' });
|
|
55
|
+
metric('read', { reason, result: cached ? 'hit' : 'miss', ms, turns });
|
|
56
|
+
return cached || null;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const ms = Math.round(performance.now() - started);
|
|
59
|
+
if (version === readVersion) updateCache({ threadId: '', read: 'error', readMs: ms, turns: 0, error: error.message });
|
|
60
|
+
metric('read', { reason, result: 'error', ms, turns: 0 });
|
|
61
|
+
onCacheError(error);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cancelSave() {
|
|
67
|
+
clearTimeout(saveTimer);
|
|
68
|
+
saveTimer = undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function saveNow(timeline, thread, reason = 'sync') {
|
|
72
|
+
if (!transport.has('historyCache') || !timeline || timeline.cached || !thread) return false;
|
|
73
|
+
const version = ++writeVersion;
|
|
74
|
+
const started = performance.now();
|
|
75
|
+
updateCache({ threadId: thread.id, write: 'writing', writeMs: 0, error: '' });
|
|
76
|
+
try {
|
|
77
|
+
const outcome = await transport.saveCached({ ...timeline, thread });
|
|
78
|
+
const ms = Math.round(performance.now() - started);
|
|
79
|
+
if (version === writeVersion) updateCache({ threadId: thread.id, write: 'saved', writeMs: ms,
|
|
80
|
+
retained: outcome?.retained !== false, evicted: Number(outcome?.evictedThreads) || 0, error: '' });
|
|
81
|
+
metric('write', { reason, result: 'saved', ms, retained: outcome?.retained === false ? 0 : 1,
|
|
82
|
+
evicted: Number(outcome?.evictedThreads) || 0 });
|
|
83
|
+
return true;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const ms = Math.round(performance.now() - started);
|
|
86
|
+
if (version === writeVersion) updateCache({ threadId: thread.id, write: 'error', writeMs: ms, error: error.message });
|
|
87
|
+
metric('write', { reason, result: 'error', ms, retained: 0, evicted: 0 });
|
|
88
|
+
onCacheError(error);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function scheduleSave(timeline, thread, historyLoading) {
|
|
94
|
+
if (!transport.has('historyCache') || !timeline || timeline.cached || historyLoading || !thread) return;
|
|
95
|
+
cancelSave();
|
|
96
|
+
const snapshot = { ...timeline, thread };
|
|
97
|
+
saveTimer = setTimeout(() => {
|
|
98
|
+
saveTimer = undefined;
|
|
99
|
+
void saveNow(snapshot, thread, 'settled');
|
|
100
|
+
}, 500);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function release() {
|
|
104
|
+
root.codexHistoryCache?.release();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function releaseUnused(timeline, reason) {
|
|
108
|
+
root.codexHistoryCache?.releaseUnused?.(timeline, reason);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function recover(id, previous, isCurrent, hostId = '') {
|
|
112
|
+
const previousTurns = previous?.thread?.id === id && Array.isArray(previous.turns) ? previous.turns : [];
|
|
113
|
+
const listed = state.threads.find(thread => thread.id === id);
|
|
114
|
+
const previousUpdatedAt = Number(previous?.thread?.updatedAt) || 0;
|
|
115
|
+
const latestCached = previousTurns.at(-1);
|
|
116
|
+
const listConfirmsCache = transport.has('incrementalEvents') && transport.transportMode() !== 'relay'
|
|
117
|
+
&& listed && !isThreadRunning(listed) && Number(listed.updatedAt) > 0
|
|
118
|
+
&& Number(listed.updatedAt) === previousUpdatedAt
|
|
119
|
+
&& latestCached?.id && latestCached.status !== 'inProgress';
|
|
120
|
+
if (listConfirmsCache) {
|
|
121
|
+
return { timeline: { ...previous, thread: { ...previous.thread, ...listed }, cached: false },
|
|
122
|
+
unchanged: true, partial: false, listTrusted: true };
|
|
123
|
+
}
|
|
124
|
+
const query = new URLSearchParams({ turnLimit: '10' });
|
|
125
|
+
if (hostId) query.set('hostId', hostId);
|
|
126
|
+
const previousLatest = previousTurns.at(-1);
|
|
127
|
+
if (previousLatest?.id) {
|
|
128
|
+
if (previousUpdatedAt > 0) query.set('knownUpdatedAt', previousUpdatedAt.toString());
|
|
129
|
+
query.set('knownTurnId', previousLatest.id);
|
|
130
|
+
if (previousLatest.status) query.set('knownTurnStatus', previousLatest.status);
|
|
131
|
+
if (previous.historyView) query.set('knownHistoryView', previous.historyView);
|
|
132
|
+
}
|
|
133
|
+
const data = await api(`/api/threads/${encodeURIComponent(id)}/timeline?${query}`);
|
|
134
|
+
if (!isCurrent()) return null;
|
|
135
|
+
if (data.notModified === true) {
|
|
136
|
+
const { notModified, ...fresh } = data;
|
|
137
|
+
return { timeline: { ...previous, ...fresh, thread: { ...previous.thread, ...data.thread },
|
|
138
|
+
turns: previousTurns, nextCursor: previous.nextCursor, cached: false }, unchanged: true };
|
|
139
|
+
}
|
|
140
|
+
let partial = false;
|
|
141
|
+
if (data.partialFromTurnId) {
|
|
142
|
+
const start = previousTurns.findIndex(turn => turn.id === data.partialFromTurnId);
|
|
143
|
+
if (start >= 0) {
|
|
144
|
+
data.turns = [...previousTurns.slice(0, start), ...data.turns];
|
|
145
|
+
if (Object.prototype.hasOwnProperty.call(previous, 'nextCursor')) data.nextCursor = previous.nextCursor;
|
|
146
|
+
partial = true;
|
|
147
|
+
}
|
|
148
|
+
delete data.partialFromTurnId;
|
|
149
|
+
}
|
|
150
|
+
const freshIds = new Set(data.turns.map(turn => turn.id));
|
|
151
|
+
const firstOverlap = previousTurns.findIndex(turn => freshIds.has(turn.id));
|
|
152
|
+
if (firstOverlap >= 0) {
|
|
153
|
+
data.turns.unshift(...previousTurns.slice(0, firstOverlap).filter(turn => !freshIds.has(turn.id)));
|
|
154
|
+
if (Object.prototype.hasOwnProperty.call(previous, 'nextCursor')) data.nextCursor = previous.nextCursor;
|
|
155
|
+
if (previous.historyGapBeforeTurnId) data.historyGapBeforeTurnId = previous.historyGapBeforeTurnId;
|
|
156
|
+
} else if (previousTurns.length && data.turns.length && data.nextCursor) {
|
|
157
|
+
data.turns = [...previousTurns, ...data.turns];
|
|
158
|
+
data.historyGapBeforeTurnId = data.turns[previousTurns.length]?.id || '';
|
|
159
|
+
}
|
|
160
|
+
return { timeline: data, unchanged: false, partial };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return Object.freeze({ read, readLatest, saveNow, scheduleSave, cancelSave, release, releaseUnused, recover });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return createHistoryController;
|
|
167
|
+
});
|