glad-web 1.0.28 → 1.0.30

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.
@@ -1,7 +1,6 @@
1
1
  const { EventEmitter } = require('events');
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
- const os = require('os');
5
4
  const { v4: uuidv4 } = require('uuid');
6
5
  const PTYManager = require('./pty-manager');
7
6
  const TextHistory = require('./text-history');
@@ -10,6 +9,8 @@ const CircularBuffer = require('./buffer');
10
9
  const { getToolByKey } = require('../ai-tools/registry');
11
10
  const ClaudeStructuredSession = require('../claude/structured-session');
12
11
  const CodexStructuredSession = require('../codex/structured-session');
12
+ const ClaudeTranscriptRepository = require('../claude/transcript-repository');
13
+ const CodexImageStore = require('../codex/image-store');
13
14
 
14
15
  function previewText(text, maxChars = 320) {
15
16
  if (!text) return '';
@@ -21,32 +22,15 @@ function previewText(text, maxChars = 320) {
21
22
  return normalized.length > maxChars ? normalized.slice(-maxChars) : normalized;
22
23
  }
23
24
 
24
- const CODEX_IMAGE_MAX_BYTES = 50 * 1024 * 1024;
25
- const CODEX_IMAGE_MAX_PER_SESSION = 5;
26
- const CODEX_IMAGE_CLEANUP_DELAY_MS = 5 * 60 * 1000;
27
- const CODEX_IMAGE_MAX_CHUNKS = 128;
28
-
29
- function detectImageExtension(buffer) {
30
- if (!Buffer.isBuffer(buffer) || buffer.length < 12) return null;
31
- if (buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) return 'png';
32
- if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return 'jpg';
33
- if (buffer.subarray(0, 6).equals(Buffer.from('GIF87a')) || buffer.subarray(0, 6).equals(Buffer.from('GIF89a'))) return 'gif';
34
- if (buffer.subarray(0, 4).equals(Buffer.from('RIFF')) && buffer.subarray(8, 12).equals(Buffer.from('WEBP'))) return 'webp';
35
- return null;
36
- }
37
-
38
- function attachmentError(message, statusCode = 400) {
39
- const error = new Error(message);
40
- error.statusCode = statusCode;
41
- return error;
42
- }
43
-
44
- function isSafeUploadId(value) {
45
- return typeof value === 'string' && /^[a-zA-Z0-9-]{8,100}$/.test(value);
25
+ function imageMediaType(name) {
26
+ const extension = path.extname(String(name || '')).toLowerCase();
27
+ return extension === '.png' ? 'image/png'
28
+ : extension === '.jpg' || extension === '.jpeg' ? 'image/jpeg'
29
+ : extension === '.gif' ? 'image/gif' : 'image/webp';
46
30
  }
47
31
 
48
32
  class SessionManager extends EventEmitter {
49
- constructor({ baseDir, renderHistoryTools, debugHistoryEnabled = false, logger, hasConnectedSessionClient } = {}) {
33
+ constructor({ baseDir, renderHistoryTools, debugHistoryEnabled = false, logger, hasConnectedSessionClient, claudeTranscriptRepository, codexImageStore, claudeForkSession } = {}) {
50
34
  super();
51
35
  this.baseDir = baseDir || process.cwd();
52
36
  this.renderHistoryTools = renderHistoryTools || new Set();
@@ -54,8 +38,25 @@ class SessionManager extends EventEmitter {
54
38
  this.logger = logger || console;
55
39
  this.hasConnectedSessionClient = hasConnectedSessionClient || (() => false);
56
40
  this.sessions = new Map();
57
- this.codexImageRoot = path.join(os.tmpdir(), 'glad', 'codex-images');
58
- this.codexImageUploadRoot = path.join(os.tmpdir(), 'glad', 'codex-image-uploads');
41
+ this.codexImages = codexImageStore || new CodexImageStore({ logger: this.logger });
42
+ Object.defineProperties(this, {
43
+ codexImageRoot: {
44
+ get: () => this.codexImages.root,
45
+ set: value => { this.codexImages.root = value; }
46
+ },
47
+ codexImageUploadRoot: {
48
+ get: () => this.codexImages.uploadRoot,
49
+ set: value => { this.codexImages.uploadRoot = value; }
50
+ }
51
+ });
52
+ this.claudeTranscripts = claudeTranscriptRepository || new ClaudeTranscriptRepository({
53
+ baseDir: this.baseDir,
54
+ logger: this.logger
55
+ });
56
+ this.claudeForkSession = claudeForkSession || (async (sessionId, options) => {
57
+ const sdk = await import('@anthropic-ai/claude-agent-sdk');
58
+ return sdk.forkSession(sessionId, options);
59
+ });
59
60
  }
60
61
 
61
62
  list() {
@@ -183,6 +184,8 @@ class SessionManager extends EventEmitter {
183
184
  logger: this.logger,
184
185
  options: claudeOptions
185
186
  });
187
+ session.imageAttachments = new Map();
188
+ session.imageUploads = new Map();
186
189
 
187
190
  this.sessions.set(id, session);
188
191
  session.on('event', event => this.emit('claude-event', { sessionId: id, event, session }));
@@ -224,148 +227,71 @@ class SessionManager extends EventEmitter {
224
227
  return session.ptyManager.write(data);
225
228
  }
226
229
 
227
- sendClaudeInput(id, text) {
230
+ async sendClaudeInput(id, text, attachmentIds = []) {
228
231
  const session = this.get(id);
229
232
  if (!session || session.kind !== 'claude-structured') return false;
230
- this.markSessionInput(session, text);
231
- return session.sendUserMessage(text);
233
+ const attachments = this.getImageAttachments(id, attachmentIds);
234
+ const prompt = String(text || '');
235
+ if (!prompt.trim() && attachments.length === 0) return false;
236
+ const prepared = await Promise.all(attachments.map(async attachment => ({
237
+ id: attachment.id,
238
+ name: attachment.name,
239
+ size: attachment.size,
240
+ mediaType: imageMediaType(attachment.name),
241
+ data: (await fs.promises.readFile(attachment.path)).toString('base64')
242
+ })));
243
+ this.markSessionInput(session, prompt || '[image attachment]');
244
+ const sent = session.sendUserMessage(prompt, prepared);
245
+ if (sent && attachments.length) this.scheduleImageCleanup(id, attachments.map(item => item.id));
246
+ return sent;
232
247
  }
233
248
 
234
- async storeCodexImageAttachment(id, bytes) {
235
- const session = this.get(id);
236
- if (!session) throw attachmentError('Session not found', 404);
237
- if (session.kind !== 'codex-structured' || session.presentation !== 'structured') {
238
- throw attachmentError('Image attachments are available only in Codex chat mode');
239
- }
240
- if (!Buffer.isBuffer(bytes) || bytes.length === 0) throw attachmentError('Image data is required');
241
- if (bytes.length > CODEX_IMAGE_MAX_BYTES) throw attachmentError('Image must be 50 MB or smaller');
242
- if (session.imageAttachments.size >= CODEX_IMAGE_MAX_PER_SESSION) {
243
- throw attachmentError(`You can attach at most ${CODEX_IMAGE_MAX_PER_SESSION} images at a time`);
244
- }
249
+ storeImageAttachment(id, bytes) {
250
+ return this.codexImages.store(this.get(id), bytes);
251
+ }
245
252
 
246
- const extension = detectImageExtension(bytes);
247
- if (!extension) throw attachmentError('Only PNG, JPEG, GIF, and WebP images are supported');
253
+ appendImageChunk(id, input = {}, bytes) {
254
+ return this.codexImages.appendChunk(this.get(id), input, bytes);
255
+ }
248
256
 
249
- const directory = path.join(this.codexImageRoot, session.id);
250
- await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
251
- const attachment = {
252
- id: uuidv4(),
253
- name: `image.${extension}`,
254
- path: path.join(directory, `${uuidv4()}.${extension}`),
255
- size: bytes.length,
256
- createdAt: Date.now(),
257
- cleanupTimer: null
258
- };
259
- await fs.promises.writeFile(attachment.path, bytes, { mode: 0o600, flag: 'wx' });
260
- session.imageAttachments.set(attachment.id, attachment);
261
- return { id: attachment.id, name: attachment.name, size: attachment.size };
257
+ discardImageUpload(id, uploadId) {
258
+ return this.codexImages.discardUpload(this.get(id), uploadId);
262
259
  }
263
260
 
264
- async appendCodexImageChunk(id, input = {}, bytes) {
265
- const session = this.get(id);
266
- if (!session) throw attachmentError('Session not found', 404);
267
- if (session.kind !== 'codex-structured' || session.presentation !== 'structured') {
268
- throw attachmentError('Image attachments are available only in Codex chat mode');
269
- }
270
- if (!Buffer.isBuffer(bytes) || bytes.length === 0) throw attachmentError('Image chunk is required');
271
- const uploadId = String(input.uploadId || '');
272
- const chunkIndex = Number(input.chunkIndex);
273
- const chunkTotal = Number(input.chunkTotal);
274
- if (!isSafeUploadId(uploadId)) throw attachmentError('Invalid image upload id');
275
- if (!Number.isInteger(chunkIndex) || !Number.isInteger(chunkTotal) || chunkIndex < 0 || chunkTotal < 1 || chunkTotal > CODEX_IMAGE_MAX_CHUNKS || chunkIndex >= chunkTotal) {
276
- throw attachmentError('Invalid image chunk metadata');
277
- }
261
+ discardImageAttachment(id, attachmentId) {
262
+ return this.codexImages.discardAttachment(this.get(id), attachmentId);
263
+ }
278
264
 
279
- let upload = session.imageUploads.get(uploadId);
280
- if (!upload) {
281
- if (chunkIndex !== 0) throw attachmentError('Image upload must start with the first chunk');
282
- const directory = path.join(this.codexImageUploadRoot, session.id);
283
- await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
284
- upload = {
285
- id: uploadId,
286
- path: path.join(directory, `${uploadId}.part`),
287
- chunkTotal,
288
- nextChunkIndex: 0,
289
- bytes: 0
290
- };
291
- session.imageUploads.set(uploadId, upload);
292
- }
293
- if (upload.chunkTotal !== chunkTotal || upload.nextChunkIndex !== chunkIndex) {
294
- throw attachmentError('Image chunks arrived out of order');
295
- }
296
- if (upload.bytes + bytes.length > CODEX_IMAGE_MAX_BYTES) {
297
- await this.discardCodexImageUpload(id, uploadId);
298
- throw attachmentError('Image must be 50 MB or smaller');
299
- }
265
+ getImageAttachments(id, attachmentIds = []) {
266
+ return this.codexImages.resolve(this.get(id), attachmentIds);
267
+ }
300
268
 
301
- if (chunkIndex === 0) await fs.promises.writeFile(upload.path, bytes, { mode: 0o600, flag: 'wx' });
302
- else await fs.promises.appendFile(upload.path, bytes, { mode: 0o600 });
303
- upload.bytes += bytes.length;
304
- upload.nextChunkIndex += 1;
269
+ scheduleImageCleanup(id, attachmentIds) {
270
+ this.codexImages.scheduleCleanup(this.get(id), attachmentIds);
271
+ }
305
272
 
306
- if (upload.nextChunkIndex < upload.chunkTotal) {
307
- return { complete: false, receivedChunks: upload.nextChunkIndex, size: upload.bytes };
308
- }
273
+ storeCodexImageAttachment(id, bytes) {
274
+ return this.storeImageAttachment(id, bytes);
275
+ }
309
276
 
310
- session.imageUploads.delete(uploadId);
311
- try {
312
- const image = await fs.promises.readFile(upload.path);
313
- const attachment = await this.storeCodexImageAttachment(id, image);
314
- return { complete: true, attachment };
315
- } finally {
316
- await fs.promises.rm(upload.path, { force: true });
317
- }
277
+ appendCodexImageChunk(id, input = {}, bytes) {
278
+ return this.appendImageChunk(id, input, bytes);
318
279
  }
319
280
 
320
- async discardCodexImageUpload(id, uploadId) {
321
- const session = this.get(id);
322
- if (!session || !session.imageUploads || !isSafeUploadId(uploadId)) return false;
323
- const upload = session.imageUploads.get(uploadId);
324
- if (!upload) return false;
325
- session.imageUploads.delete(uploadId);
326
- await fs.promises.rm(upload.path, { force: true });
327
- return true;
281
+ discardCodexImageUpload(id, uploadId) {
282
+ return this.discardImageUpload(id, uploadId);
328
283
  }
329
284
 
330
- async discardCodexImageAttachment(id, attachmentId) {
331
- const session = this.get(id);
332
- if (!session || session.kind !== 'codex-structured') return false;
333
- const attachment = session.imageAttachments.get(attachmentId);
334
- if (!attachment) return false;
335
- clearTimeout(attachment.cleanupTimer);
336
- session.imageAttachments.delete(attachmentId);
337
- await fs.promises.rm(attachment.path, { force: true });
338
- return true;
285
+ discardCodexImageAttachment(id, attachmentId) {
286
+ return this.discardImageAttachment(id, attachmentId);
339
287
  }
340
288
 
341
289
  getCodexImageAttachments(id, attachmentIds = []) {
342
- const session = this.get(id);
343
- if (!session || session.kind !== 'codex-structured') throw attachmentError('Codex session not found', 404);
344
- const ids = Array.isArray(attachmentIds) ? attachmentIds : [];
345
- if (ids.length > CODEX_IMAGE_MAX_PER_SESSION) throw attachmentError(`You can attach at most ${CODEX_IMAGE_MAX_PER_SESSION} images at a time`);
346
- const uniqueIds = [...new Set(ids.map(value => String(value)))];
347
- if (uniqueIds.length !== ids.length) throw attachmentError('Duplicate image attachment');
348
- return uniqueIds.map(attachmentId => {
349
- const attachment = session.imageAttachments.get(attachmentId);
350
- if (!attachment) throw attachmentError('Image attachment is no longer available');
351
- return attachment;
352
- });
290
+ return this.getImageAttachments(id, attachmentIds);
353
291
  }
354
292
 
355
293
  scheduleCodexImageCleanup(id, attachmentIds) {
356
- const session = this.get(id);
357
- if (!session || session.kind !== 'codex-structured') return;
358
- for (const attachmentId of attachmentIds) {
359
- const attachment = session.imageAttachments.get(attachmentId);
360
- if (!attachment) continue;
361
- clearTimeout(attachment.cleanupTimer);
362
- attachment.cleanupTimer = setTimeout(() => {
363
- this.discardCodexImageAttachment(id, attachmentId).catch(error => {
364
- this.logger.debugInfo?.(`[codex-image] cleanup failed: ${error.message}`);
365
- });
366
- }, CODEX_IMAGE_CLEANUP_DELAY_MS);
367
- attachment.cleanupTimer.unref?.();
368
- }
294
+ this.scheduleImageCleanup(id, attachmentIds);
369
295
  }
370
296
 
371
297
  async sendCodexInput(id, text, attachmentIds = []) {
@@ -392,6 +318,18 @@ class SessionManager extends EventEmitter {
392
318
  return session.updateSettings(settings || {});
393
319
  }
394
320
 
321
+ showClaudeUsage(id) {
322
+ const session = this.get(id);
323
+ if (!session || session.kind !== 'claude-structured') return Promise.resolve(false);
324
+ return session.showUsage();
325
+ }
326
+
327
+ showClaudeContext(id) {
328
+ const session = this.get(id);
329
+ if (!session || session.kind !== 'claude-structured') return Promise.resolve(false);
330
+ return session.showContext();
331
+ }
332
+
395
333
  abortClaude(id) {
396
334
  const session = this.get(id);
397
335
  if (!session || session.kind !== 'claude-structured') return false;
@@ -405,6 +343,29 @@ class SessionManager extends EventEmitter {
405
343
  return session.selectResumeSession(resumeSessionId, historyMessages);
406
344
  }
407
345
 
346
+ async forkClaude(id, sourceSessionId) {
347
+ const session = this.get(id);
348
+ if (!session || session.kind !== 'claude-structured') return null;
349
+ if (session.status === 'thinking') {
350
+ const error = new Error('Claude must be idle before forking a session');
351
+ error.statusCode = 409;
352
+ throw error;
353
+ }
354
+ const sourceId = String(sourceSessionId || session.claudeSessionId || session.resumeSessionId || '').trim();
355
+ if (!sourceId) {
356
+ const error = new Error('No Claude session is available to fork');
357
+ error.statusCode = 400;
358
+ throw error;
359
+ }
360
+ const result = await this.claudeForkSession(sourceId, { dir: this.getSessionWorkingDirectory(session) });
361
+ const forkedId = result?.sessionId;
362
+ if (!forkedId) throw new Error('Claude SDK did not return a forked session id');
363
+ const historyMessages = this.readClaudeTranscriptMessages(this.getSessionWorkingDirectory(session), forkedId);
364
+ session.selectResumeSession(forkedId, historyMessages);
365
+ session.appendMessage({ kind: 'event', level: 'info', text: `Forked from Claude session ${sourceId}` });
366
+ return { id: session.id, name: session.name, claudeSessionId: forkedId };
367
+ }
368
+
408
369
  listClaudeResumeSessions(id) {
409
370
  const session = this.get(id);
410
371
  if (!session || session.kind !== 'claude-structured') return null;
@@ -716,192 +677,11 @@ class SessionManager extends EventEmitter {
716
677
  }
717
678
 
718
679
  scanClaudeProjectSessions(workingDirectory) {
719
- const projectDir = this.getClaudeProjectDir(workingDirectory);
720
- if (!projectDir || !fs.existsSync(projectDir)) return [];
721
- const files = fs.readdirSync(projectDir)
722
- .filter(file => /^[0-9a-f-]{36}\.jsonl$/i.test(file))
723
- .map(file => {
724
- const fullPath = path.join(projectDir, file);
725
- const stat = fs.statSync(fullPath);
726
- return {
727
- id: file.replace(/\.jsonl$/i, ''),
728
- path: fullPath,
729
- mtimeMs: stat.mtimeMs,
730
- size: stat.size
731
- };
732
- })
733
- .sort((a, b) => b.mtimeMs - a.mtimeMs)
734
- .slice(0, 40);
735
-
736
- return files.map(file => this.describeClaudeSessionFile(file));
737
- }
738
-
739
- getClaudeProjectDir(workingDirectory) {
740
- const cwd = path.resolve(workingDirectory || this.baseDir);
741
- const encoded = cwd.replace(/[^a-zA-Z0-9]/g, '-');
742
- return path.join(os.homedir(), '.claude', 'projects', encoded);
743
- }
744
-
745
- describeClaudeSessionFile(file) {
746
- let cwd = '';
747
- let firstText = '';
748
- let lastText = '';
749
- try {
750
- const content = fs.readFileSync(file.path, 'utf8');
751
- const lines = content.split('\n').filter(Boolean);
752
- for (const line of lines) {
753
- const parsed = this.parseClaudeJsonLine(line);
754
- if (!parsed) continue;
755
- if (!cwd && typeof parsed.cwd === 'string') cwd = parsed.cwd;
756
- const text = this.extractClaudeTranscriptText(parsed);
757
- if (!text) continue;
758
- if (!firstText) firstText = text;
759
- lastText = text;
760
- }
761
- } catch (error) {
762
- this.logger.debugInfo?.(`[claude-resume] Failed to read ${file.path}: ${error.message}`);
763
- }
764
- return {
765
- id: file.id,
766
- cwd,
767
- updatedAt: file.mtimeMs,
768
- size: file.size,
769
- firstText: firstText ? previewText(firstText, 120) : '',
770
- lastText: lastText ? previewText(lastText, 160) : ''
771
- };
680
+ return this.claudeTranscripts.list(workingDirectory);
772
681
  }
773
682
 
774
683
  readClaudeTranscriptMessages(workingDirectory, resumeSessionId) {
775
- const id = String(resumeSessionId || '').trim();
776
- if (!/^[0-9a-f-]{36}$/i.test(id)) return [];
777
- const filePath = path.join(this.getClaudeProjectDir(workingDirectory), `${id}.jsonl`);
778
- if (!fs.existsSync(filePath)) return [];
779
-
780
- const messages = [];
781
- try {
782
- const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
783
- for (const line of lines) {
784
- const record = this.parseClaudeJsonLine(line);
785
- if (!record || record.isSidechain) continue;
786
- messages.push(...this.mapClaudeTranscriptRecord(record));
787
- }
788
- } catch (error) {
789
- this.logger.debugInfo?.(`[claude-resume] Failed to backfill ${filePath}: ${error.message}`);
790
- return [];
791
- }
792
-
793
- const maxMessages = 1000;
794
- const visible = messages.filter(Boolean).slice(-maxMessages);
795
- if (messages.length > maxMessages) {
796
- visible.unshift({
797
- id: uuidv4(),
798
- kind: 'event',
799
- level: 'info',
800
- text: `Showing the latest ${maxMessages} resumed transcript items.`,
801
- createdAt: Date.now()
802
- });
803
- }
804
- return visible;
805
- }
806
-
807
- mapClaudeTranscriptRecord(record) {
808
- const createdAt = Number.isFinite(Date.parse(record.timestamp)) ? Date.parse(record.timestamp) : Date.now();
809
- const message = record.message || {};
810
- const content = message.content;
811
- if (record.type === 'user') {
812
- if (typeof content === 'string') {
813
- const text = content.trim();
814
- return text ? [{ id: uuidv4(), kind: 'user', text, createdAt }] : [];
815
- }
816
- if (Array.isArray(content)) {
817
- return content.flatMap(item => {
818
- if (!item || typeof item !== 'object') return [];
819
- if (item.type === 'tool_result') {
820
- const text = this.textFromClaudeContent(item.content).trim();
821
- return text ? [{
822
- id: uuidv4(),
823
- kind: 'tool-result',
824
- toolUseId: item.tool_use_id,
825
- text,
826
- isError: Boolean(item.is_error),
827
- createdAt
828
- }] : [];
829
- }
830
- if (item.type === 'text' && typeof item.text === 'string' && item.text.trim()) {
831
- return [{ id: uuidv4(), kind: 'user', text: item.text.trim(), createdAt }];
832
- }
833
- return [];
834
- });
835
- }
836
- }
837
-
838
- if (record.type === 'assistant' && Array.isArray(content)) {
839
- const mapped = [];
840
- const text = this.textFromClaudeContent(content).trim();
841
- if (text) mapped.push({ id: uuidv4(), kind: 'assistant', text, createdAt });
842
- for (const item of content) {
843
- if (!item || item.type !== 'tool_use') continue;
844
- mapped.push({
845
- id: uuidv4(),
846
- kind: 'tool',
847
- name: item.name || 'tool',
848
- summary: this.summarizeClaudeToolInput(item.input),
849
- input: item.input,
850
- toolUseId: item.id,
851
- createdAt
852
- });
853
- }
854
- return mapped;
855
- }
856
-
857
- if (record.type === 'summary' && typeof record.summary === 'string' && record.summary.trim()) {
858
- return [{ id: uuidv4(), kind: 'event', level: 'info', text: `Summary: ${record.summary.trim()}`, createdAt }];
859
- }
860
-
861
- return [];
862
- }
863
-
864
- textFromClaudeContent(content) {
865
- if (typeof content === 'string') return content;
866
- if (!Array.isArray(content)) return '';
867
- return content.map(item => {
868
- if (!item || typeof item !== 'object') return '';
869
- if (item.type === 'text' && typeof item.text === 'string') return item.text;
870
- if (item.type === 'tool_result') return this.textFromClaudeContent(item.content);
871
- return '';
872
- }).filter(Boolean).join('\n');
873
- }
874
-
875
- summarizeClaudeToolInput(input) {
876
- if (!input || typeof input !== 'object') return '';
877
- if (typeof input.command === 'string') return input.command;
878
- if (typeof input.file_path === 'string') return input.file_path;
879
- if (typeof input.path === 'string') return input.path;
880
- const serialized = JSON.stringify(input);
881
- return serialized.length > 240 ? serialized.slice(0, 240) + '...' : serialized;
882
- }
883
-
884
- parseClaudeJsonLine(line) {
885
- try {
886
- return JSON.parse(line);
887
- } catch (_) {
888
- return null;
889
- }
890
- }
891
-
892
- extractClaudeTranscriptText(record) {
893
- const message = record && record.message;
894
- const content = message && message.content;
895
- if (typeof content === 'string') return content.trim();
896
- if (Array.isArray(content)) {
897
- return content.map(item => {
898
- if (!item || typeof item !== 'object') return '';
899
- if (item.type === 'text' && typeof item.text === 'string') return item.text;
900
- if (item.type === 'tool_use') return `[tool] ${item.name || 'tool'}`;
901
- return '';
902
- }).filter(Boolean).join('\n').trim();
903
- }
904
- return '';
684
+ return this.claudeTranscripts.readMessages(workingDirectory, resumeSessionId);
905
685
  }
906
686
 
907
687
  logHistoryRequest(id, req) {
@@ -1048,20 +828,11 @@ class SessionManager extends EventEmitter {
1048
828
  }
1049
829
 
1050
830
  clearCodexImageAttachments(session) {
1051
- if (!session || !session.imageAttachments) return;
1052
- for (const attachment of session.imageAttachments.values()) clearTimeout(attachment.cleanupTimer);
1053
- session.imageAttachments.clear();
1054
- fs.promises.rm(path.join(this.codexImageRoot, session.id), { recursive: true, force: true }).catch(error => {
1055
- this.logger.debugInfo?.(`[codex-image] session cleanup failed: ${error.message}`);
1056
- });
831
+ this.codexImages.clearAttachments(session);
1057
832
  }
1058
833
 
1059
834
  clearCodexImageUploads(session) {
1060
- if (!session || !session.imageUploads) return;
1061
- session.imageUploads.clear();
1062
- fs.promises.rm(path.join(this.codexImageUploadRoot, session.id), { recursive: true, force: true }).catch(error => {
1063
- this.logger.debugInfo?.(`[codex-image] upload cleanup failed: ${error.message}`);
1064
- });
835
+ this.codexImages.clearUploads(session);
1065
836
  }
1066
837
 
1067
838
  getSessionDiagnostics(session, extra = {}) {