@walkhi/code-relax 0.1.0-beta.2 → 0.1.0-beta.20

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.
Files changed (120) hide show
  1. package/README.md +55 -71
  2. package/dist/bin/codex-remote.mjs +150 -126
  3. package/dist/bin/manage-relay-admin.mjs +20 -0
  4. package/dist/bin/migrate-relay-db.mjs +107 -0
  5. package/dist/bin/self-relay-admin.mjs +8 -0
  6. package/dist/bin/self-relay-demo.mjs +2 -2
  7. package/dist/bin/self-relay-server.mjs +18 -10
  8. package/dist/shared/app-server-events.cjs +42 -15
  9. package/dist/shared/file-mentions.cjs +35 -0
  10. package/dist/shared/server-requests.cjs +86 -58
  11. package/dist/skills/code-relax/SKILL.md +7 -6
  12. package/dist/src/desktop-launcher.mjs +16 -21
  13. package/dist/src/file-upload-session.mjs +124 -0
  14. package/dist/src/lan-socket-server.mjs +52 -9
  15. package/dist/src/managed-app-server.mjs +44 -13
  16. package/dist/src/message-images.mjs +0 -18
  17. package/dist/src/onboarding.mjs +9 -11
  18. package/dist/src/platform/README.md +1 -1
  19. package/dist/src/platform/windows/background-process.mjs +1 -1
  20. package/dist/src/platform/windows/codex-runtime.mjs +251 -0
  21. package/dist/src/platform/windows/desktop-host.mjs +17 -3
  22. package/dist/src/platform/windows/desktop-info.ps1 +2 -4
  23. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -0
  24. package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -14
  25. package/dist/src/platform/windows/launch-worker.mjs +42 -0
  26. package/dist/src/platform/windows/resident-start.js +20 -0
  27. package/dist/src/platform/windows/resident-startup.ps1 +76 -20
  28. package/dist/src/platform/windows/service-host.mjs +4 -16
  29. package/dist/src/platform/windows/start-hidden-console.ps1 +10 -5
  30. package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
  31. package/dist/src/platform/windows/user-environment.mjs +1 -1
  32. package/dist/src/runtime-discovery.mjs +43 -0
  33. package/dist/src/self-relay/admin-cloud.mjs +101 -0
  34. package/dist/src/self-relay/admin-page.mjs +79 -0
  35. package/dist/src/self-relay/admin-server.mjs +66 -0
  36. package/dist/src/self-relay/admin-state.mjs +82 -19
  37. package/dist/src/self-relay/admin-style.mjs +9 -0
  38. package/dist/src/self-relay/admin-usage-page.mjs +28 -0
  39. package/dist/src/self-relay/connector.mjs +243 -30
  40. package/dist/src/self-relay/demo.mjs +3 -4
  41. package/dist/src/self-relay/email-accounts.mjs +141 -0
  42. package/dist/src/self-relay/lifecycle.mjs +99 -142
  43. package/dist/src/self-relay/mysql-state.mjs +104 -0
  44. package/dist/src/self-relay/p2p-probe.mjs +1 -1
  45. package/dist/src/self-relay/server.mjs +335 -59
  46. package/dist/src/self-relay/usage-diagnostics.mjs +46 -0
  47. package/dist/src/self-relay/usage-meter.mjs +125 -0
  48. package/dist/src/server.mjs +731 -1439
  49. package/dist/src/service-control.mjs +4 -4
  50. package/dist/src/service-doctor.mjs +27 -29
  51. package/dist/src/service-health.mjs +2 -2
  52. package/dist/src/service-install.mjs +1 -4
  53. package/dist/src/service-lifecycle.mjs +19 -19
  54. package/dist/src/shared-app-server.mjs +277 -270
  55. package/dist/src/shared-catalog.mjs +1 -11
  56. package/dist/src/shared-recovery.mjs +11 -9
  57. package/dist/src/shared-thread-stream.mjs +1 -1
  58. package/dist/src/skill-install.mjs +0 -13
  59. package/package.json +17 -18
  60. package/tools/postinstall.mjs +69 -15
  61. package/dist/src/app-server-client.mjs +0 -468
  62. package/dist/src/app-server-tasks.mjs +0 -358
  63. package/dist/src/debug-ui.mjs +0 -131
  64. package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
  65. package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
  66. package/dist/src/platform/windows/desktop-tools.mjs +0 -48
  67. package/dist/src/platform/windows/launch-worker.ps1 +0 -20
  68. package/dist/src/thread-catalog.mjs +0 -47
  69. package/dist/tools/find-desktop-pipe.mjs +0 -10
  70. package/dist/web/activity-view.js +0 -283
  71. package/dist/web/approval-ui.js +0 -188
  72. package/dist/web/capabilities.js +0 -57
  73. package/dist/web/chat-transport.js +0 -202
  74. package/dist/web/chat.css +0 -782
  75. package/dist/web/chat.js +0 -3830
  76. package/dist/web/client-platform.js +0 -11
  77. package/dist/web/community-view.js +0 -20
  78. package/dist/web/community.css +0 -77
  79. package/dist/web/community.html +0 -37
  80. package/dist/web/composer-controller.js +0 -101
  81. package/dist/web/conversation-controller.js +0 -99
  82. package/dist/web/disclosure-state-controller.js +0 -95
  83. package/dist/web/draft-controller.js +0 -103
  84. package/dist/web/harmony-platform.js +0 -52
  85. package/dist/web/highlight-powershell.min.js +0 -39
  86. package/dist/web/highlight.min.js +0 -1262
  87. package/dist/web/history-cache.js +0 -240
  88. package/dist/web/history-controller.js +0 -167
  89. package/dist/web/index.html +0 -325
  90. package/dist/web/lan-session.js +0 -122
  91. package/dist/web/link-action-controller.js +0 -212
  92. package/dist/web/message-send-controller.js +0 -177
  93. package/dist/web/message-view.js +0 -98
  94. package/dist/web/p2p-data-channel.js +0 -50
  95. package/dist/web/p2p-diagnostics.js +0 -28
  96. package/dist/web/p2p-probe.js +0 -123
  97. package/dist/web/page-resume.js +0 -28
  98. package/dist/web/pending-message-store.js +0 -108
  99. package/dist/web/queue-controller.js +0 -82
  100. package/dist/web/resources.json +0 -1
  101. package/dist/web/self-relay-session.js +0 -129
  102. package/dist/web/server-requests.js +0 -67
  103. package/dist/web/station-connection-controller.js +0 -75
  104. package/dist/web/task-list-view.js +0 -296
  105. package/dist/web/thread-attention-controller.js +0 -124
  106. package/dist/web/thread-context-controller.js +0 -30
  107. package/dist/web/thread-list-controller.js +0 -61
  108. package/dist/web/thread-list-sync.js +0 -86
  109. package/dist/web/thread-title-controller.js +0 -57
  110. package/dist/web/timeline-formatters.js +0 -249
  111. package/dist/web/timeline-reducer.js +0 -81
  112. package/dist/web/timeline-renderer.js +0 -161
  113. package/dist/web/timeline-scroll-controller.js +0 -50
  114. package/dist/web/timeline-sync.js +0 -67
  115. package/dist/web/usage-controller.js +0 -258
  116. package/dist/web/vendor/README.md +0 -14
  117. package/dist/web/vendor/highlight-powershell.min.js +0 -39
  118. package/dist/web/vendor/highlight.LICENSE.txt +0 -29
  119. package/dist/web/vendor/highlight.min.js +0 -1262
  120. package/dist/web/vendor/lucide.LICENSE.txt +0 -17
@@ -1,240 +0,0 @@
1
- /* Device-local display cache. Never supplies execution state or credentials. */
2
- (() => {
3
- const limit = 100 * 1024 * 1024;
4
- let opening, writes = Promise.resolve(), displayVersion = 0;
5
- const urls = new Map();
6
- function metric(detail) {
7
- try { window.codexNative?.reportSyncMetric?.(detail); } catch {}
8
- }
9
- function database() {
10
- if (!opening) opening = new Promise((resolve, reject) => {
11
- const request = indexedDB.open('codex-remote-history-v1', 1);
12
- request.onupgradeneeded = () => {
13
- for (const name of ['threads', 'images', 'blobs']) request.result.createObjectStore(name, { keyPath: 'key' });
14
- };
15
- request.onsuccess = () => resolve(request.result);
16
- request.onerror = () => reject(request.error);
17
- request.onblocked = () => reject(new Error('缓存数据库正在被旧页面占用'));
18
- });
19
- return opening;
20
- }
21
- function read(db, store, key) {
22
- return new Promise((resolve, reject) => {
23
- const request = db.transaction(store).objectStore(store).get(key);
24
- request.onsuccess = () => resolve(request.result);
25
- request.onerror = () => reject(request.error);
26
- });
27
- }
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
- }
93
- async function get(scope, id) {
94
- const version = displayVersion;
95
- const db = await database(), record = await read(db, 'threads', JSON.stringify([scope, id]));
96
- if (!record) return null;
97
- let created = 0, stale = 0;
98
- async function unpack(value) {
99
- if (version !== displayVersion) return null;
100
- if (value && typeof value === 'object' && value.cacheImage) {
101
- const key = value.cacheImage;
102
- if (!urls.has(key)) {
103
- const image = await read(db, 'blobs', key);
104
- if (version !== displayVersion) return null;
105
- if (!image) throw new Error('缓存图片缺失');
106
- urls.set(key, URL.createObjectURL(image.blob));
107
- created++;
108
- }
109
- return urls.get(key);
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 ''; }
115
- if (Array.isArray(value)) { const result = []; for (const item of value) result.push(await unpack(item)); return result; }
116
- if (value && typeof value === 'object') { const result = {}; for (const [k, v] of Object.entries(value)) result[k] = await unpack(v); return result; }
117
- return value;
118
- }
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;
122
- }
123
- function save(scope, data, hash, id = data.thread.id) {
124
- // Snapshot at scheduling time; live events may mutate the UI model while hashing.
125
- let snapshot = structuredClone(compactTimeline({ ...data, turns: data.turns.slice(-40) }));
126
- const key = JSON.stringify([scope, id]);
127
- const work = writes.then(async () => {
128
- let outcome = { retained: true, evictedThreads: 0, size: 0 };
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]));
131
- const previous = await read(db, 'threads', key);
132
- snapshot = preserveToolDetails(snapshot, previous?.data);
133
- function collect(value) {
134
- if (!value || typeof value !== 'object') return;
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
- }
140
- for (const child of Object.values(value)) collect(child);
141
- }
142
- collect(previous?.data);
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 '';
149
- if (typeof value === 'string' && value.startsWith('data:image/')) {
150
- if (!byUrl.has(value)) byUrl.set(value, (async () => {
151
- const key = JSON.stringify([scope, await hash(value)]);
152
- referenced.add(key);
153
- const comma = value.indexOf(','), mime = value.slice(5, value.indexOf(';'));
154
- const bytes = Uint8Array.from(atob(value.slice(comma + 1)), c => c.charCodeAt(0));
155
- images.set(key, { key, blob: new Blob([bytes], { type: mime }), size: bytes.length });
156
- return { cacheImage: key };
157
- })());
158
- return byUrl.get(value);
159
- }
160
- if (Array.isArray(value)) return Promise.all(value.map(pack));
161
- if (value && typeof value === 'object') {
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 } }; }
167
- }
168
- return Object.fromEntries(await Promise.all(Object.entries(source).map(async ([k, v]) => [k, await pack(v)])));
169
- }
170
- return value;
171
- }
172
- const packed = await pack(snapshot);
173
- const record = { key, data: packed, at: Date.now(), imageKeys: [...referenced], size: new TextEncoder().encode(JSON.stringify(packed)).length };
174
- outcome.size = record.size;
175
- await new Promise((resolve, reject) => {
176
- const tx = db.transaction(['threads', 'images', 'blobs'], 'readwrite');
177
- tx.oncomplete = resolve; tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error || new Error('缓存写入中止'));
178
- const threads = tx.objectStore('threads'), metadata = tx.objectStore('images'), blobs = tx.objectStore('blobs');
179
- threads.put(record);
180
- for (const image of images.values()) { metadata.put({ key: image.key, size: image.size }); blobs.put({ key: image.key, blob: image.blob }); }
181
- const allThreads = threads.getAll(), allImages = metadata.getAll();
182
- let records, sizes, pruned = false;
183
- function prune() {
184
- if (!records || !sizes || pruned) return;
185
- pruned = true;
186
- records.sort((a, b) => b.at - a.at);
187
- const keep = new Set(), keptThreads = new Set(); let bytes = 0;
188
- for (const [index, item] of records.entries()) {
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);
193
- }
194
- outcome.retained = keptThreads.has(key);
195
- for (const image of sizes.keys()) if (!keep.has(image)) { metadata.delete(image); blobs.delete(image); }
196
- }
197
- allThreads.onsuccess = () => { records = allThreads.result; prune(); };
198
- allImages.onsuccess = () => { sizes = new Map(allImages.result.map(image => [image.key, image.size])); prune(); };
199
- });
200
- return outcome;
201
- });
202
- writes = work.catch(() => {});
203
- return work;
204
- }
205
- async function getImage(scope, id, path, variant = 'original') {
206
- const db = await database(), record = await read(db, 'threads', JSON.stringify([scope, id]));
207
- function find(value) {
208
- if (!value || typeof value !== 'object') return null;
209
- const source = variant === 'thumbnail' ? value.thumbnailUrl : value.url;
210
- if (value.remotePath === path && source?.cacheImage) return source.cacheImage;
211
- for (const child of Object.values(value)) { const key = find(child); if (key) return key; }
212
- return null;
213
- }
214
- const key = find(record?.data); if (!key) return null;
215
- const image = await read(db, 'blobs', key); if (!image) return null;
216
- const bytes = new Uint8Array(await image.blob.arrayBuffer()); let binary = '';
217
- for (let i = 0; i < bytes.length; i += 16384) binary += String.fromCharCode(...bytes.subarray(i, i + 16384));
218
- return `data:${image.blob.type};base64,${btoa(binary)}`;
219
- }
220
- async function clear() {
221
- await writes;
222
- const db = await database();
223
- await new Promise((resolve, reject) => {
224
- const tx = db.transaction(['threads', 'images', 'blobs'], 'readwrite');
225
- for (const store of ['threads', 'images', 'blobs']) tx.objectStore(store).clear();
226
- tx.oncomplete = resolve; tx.onabort = tx.onerror = () => reject(tx.error);
227
- });
228
- release();
229
- }
230
- async function latest(scope) {
231
- const db = await database();
232
- const records = await new Promise((resolve, reject) => {
233
- const request = db.transaction('threads').objectStore('threads').getAll();
234
- request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error);
235
- });
236
- const record = records.filter(item => JSON.parse(item.key)[0] === scope).sort((a, b) => b.at - a.at)[0];
237
- return record ? get(scope, JSON.parse(record.key)[1]) : null;
238
- }
239
- window.codexHistoryCache = { get, getImage, save, latest, clear, release, releaseUnused };
240
- })();
@@ -1,167 +0,0 @@
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
- });