@botcord/daemon 0.2.63 → 0.2.64

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,4 +1,4 @@
1
- import { appendFileSync, mkdirSync, renameSync, statSync } from "node:fs";
1
+ import { appendFileSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import path from "node:path";
4
4
 
@@ -14,6 +14,12 @@ export const TRANSCRIPT_TEXT_LIMIT = 32 * 1024;
14
14
  /** Soft cap on a single transcript file before rotation. */
15
15
  export const TRANSCRIPT_FILE_LIMIT = 8 * 1024 * 1024;
16
16
 
17
+ /** Default retention window for transcript JSONL files. */
18
+ export const TRANSCRIPT_RETENTION_MS = 3 * 24 * 60 * 60 * 1000;
19
+
20
+ /** Minimum interval between background transcript retention sweeps. */
21
+ export const TRANSCRIPT_CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1000;
22
+
17
23
  /** Default root directory for per-agent transcript trees. */
18
24
  export function defaultTranscriptRoot(): string {
19
25
  return path.join(homedir(), ".botcord", "agents");
@@ -26,6 +32,7 @@ export function defaultTranscriptRoot(): string {
26
32
  export type TranscriptRecordKind =
27
33
  | "inbound"
28
34
  | "dispatched"
35
+ | "block"
29
36
  | "compose_failed"
30
37
  | "outbound"
31
38
  | "turn_error"
@@ -65,6 +72,15 @@ export interface DispatchedTranscriptRecord extends TranscriptRecordBase {
65
72
  truncated?: { composedText?: true };
66
73
  }
67
74
 
75
+ export interface BlockTranscriptRecord extends TranscriptRecordBase {
76
+ kind: "block";
77
+ runtime: string;
78
+ seq: number;
79
+ blockType: string;
80
+ summary: TranscriptBlockSummary;
81
+ raw?: unknown;
82
+ }
83
+
68
84
  export interface ComposeFailedTranscriptRecord extends TranscriptRecordBase {
69
85
  kind: "compose_failed";
70
86
  error: string;
@@ -122,6 +138,7 @@ export interface DroppedTranscriptRecord extends TranscriptRecordBase {
122
138
  export type TranscriptRecord =
123
139
  | InboundTranscriptRecord
124
140
  | DispatchedTranscriptRecord
141
+ | BlockTranscriptRecord
125
142
  | ComposeFailedTranscriptRecord
126
143
  | OutboundTranscriptRecord
127
144
  | TurnErrorTranscriptRecord
@@ -162,10 +179,14 @@ export interface CreateTranscriptWriterOptions {
162
179
  /** Defaults to `~/.botcord/agents`. */
163
180
  rootDir?: string;
164
181
  log: GatewayLogger;
165
- /** Defaults to `false` see design §6 (default-off). */
182
+ /** Defaults to `true`; pass `false` to disable persistence. */
166
183
  enabled?: boolean;
167
184
  /** Override file rotation threshold (bytes). Defaults to TRANSCRIPT_FILE_LIMIT. */
168
185
  maxFileBytes?: number;
186
+ /** Delete transcript JSONL files older than this. Defaults to 3 days. */
187
+ retentionMs?: number;
188
+ /** Minimum interval between retention sweeps. Defaults to 6 hours. */
189
+ cleanupIntervalMs?: number;
169
190
  }
170
191
 
171
192
  interface FileMeta {
@@ -188,17 +209,29 @@ class FsTranscriptWriter implements TranscriptWriter {
188
209
  readonly rootDir: string;
189
210
  private readonly log: GatewayLogger;
190
211
  private readonly maxFileBytes: number;
212
+ private readonly retentionMs: number;
213
+ private readonly cleanupIntervalMs: number;
191
214
  private readonly fileMeta = new Map<string, FileMeta>();
192
215
  private firstWriteAnnounced = false;
193
-
194
- constructor(rootDir: string, log: GatewayLogger, maxFileBytes: number) {
216
+ private lastCleanupAt = 0;
217
+
218
+ constructor(
219
+ rootDir: string,
220
+ log: GatewayLogger,
221
+ maxFileBytes: number,
222
+ retentionMs: number,
223
+ cleanupIntervalMs: number,
224
+ ) {
195
225
  this.rootDir = rootDir;
196
226
  this.log = log;
197
227
  this.maxFileBytes = maxFileBytes;
228
+ this.retentionMs = retentionMs;
229
+ this.cleanupIntervalMs = cleanupIntervalMs;
198
230
  }
199
231
 
200
232
  write(rec: TranscriptRecord): void {
201
233
  try {
234
+ this.cleanupOldFiles();
202
235
  const file = transcriptFilePath(this.rootDir, rec.agentId, rec.roomId, rec.topicId);
203
236
  const dir = path.dirname(file);
204
237
  try {
@@ -264,16 +297,37 @@ class FsTranscriptWriter implements TranscriptWriter {
264
297
  });
265
298
  }
266
299
  }
300
+
301
+ private cleanupOldFiles(): void {
302
+ const now = Date.now();
303
+ if (now - this.lastCleanupAt < this.cleanupIntervalMs) return;
304
+ this.lastCleanupAt = now;
305
+ const cutoff = now - this.retentionMs;
306
+ const removed = cleanupTranscriptFiles(this.rootDir, cutoff);
307
+ if (removed > 0) {
308
+ this.log.info("transcript cleanup removed old files", {
309
+ dir: this.rootDir,
310
+ removed,
311
+ retentionMs: this.retentionMs,
312
+ });
313
+ }
314
+ }
267
315
  }
268
316
 
269
317
  export function createTranscriptWriter(
270
318
  opts: CreateTranscriptWriterOptions,
271
319
  ): TranscriptWriter {
272
320
  const rootDir = opts.rootDir ?? defaultTranscriptRoot();
273
- const enabled = opts.enabled ?? false;
321
+ const enabled = opts.enabled ?? true;
274
322
  if (!enabled) return new NoopTranscriptWriter(rootDir);
275
323
  const maxBytes = opts.maxFileBytes ?? TRANSCRIPT_FILE_LIMIT;
276
- return new FsTranscriptWriter(rootDir, opts.log, maxBytes);
324
+ return new FsTranscriptWriter(
325
+ rootDir,
326
+ opts.log,
327
+ maxBytes,
328
+ opts.retentionMs ?? TRANSCRIPT_RETENTION_MS,
329
+ opts.cleanupIntervalMs ?? TRANSCRIPT_CLEANUP_INTERVAL_MS,
330
+ );
277
331
  }
278
332
 
279
333
  /**
@@ -284,11 +338,47 @@ export function createTranscriptWriter(
284
338
  */
285
339
  export function resolveTranscriptEnabled(
286
340
  envVal: string | undefined,
287
- configEnabled: boolean,
341
+ configEnabled: boolean | undefined,
288
342
  ): boolean {
289
343
  if (envVal === "1") return true;
290
344
  if (envVal === "0") return false;
291
- return configEnabled;
345
+ return configEnabled ?? true;
346
+ }
347
+
348
+ export function cleanupTranscriptFiles(rootDir: string, cutoffMs: number): number {
349
+ let removed = 0;
350
+ const visit = (dir: string, depth: number): void => {
351
+ if (depth < 0) return;
352
+ let entries: string[];
353
+ try {
354
+ entries = readdirSync(dir);
355
+ } catch {
356
+ return;
357
+ }
358
+ for (const entry of entries) {
359
+ const file = path.join(dir, entry);
360
+ try {
361
+ const st = statSync(file);
362
+ if (st.isDirectory()) {
363
+ visit(file, depth - 1);
364
+ continue;
365
+ }
366
+ if (
367
+ st.isFile() &&
368
+ entry.endsWith(".jsonl") &&
369
+ file.includes(`${path.sep}transcripts${path.sep}`) &&
370
+ st.mtimeMs < cutoffMs
371
+ ) {
372
+ rmSync(file, { force: true });
373
+ removed += 1;
374
+ }
375
+ } catch {
376
+ // ignore disappearing files and permission errors
377
+ }
378
+ }
379
+ };
380
+ visit(rootDir, 6);
381
+ return removed;
292
382
  }
293
383
 
294
384
  function formatStamp(d: Date): string {
package/src/index.ts CHANGED
@@ -888,13 +888,14 @@ function cmdTranscriptStatus(): void {
888
888
  const e = err as Error & { code?: string };
889
889
  if (e.code !== CONFIG_MISSING) throw err;
890
890
  }
891
- const configEnabled = cfg?.transcript?.enabled === true;
891
+ const configEnabled = cfg?.transcript?.enabled;
892
892
  const env = process.env.BOTCORD_TRANSCRIPT;
893
893
  const effective = resolveTranscriptEnabled(env, configEnabled);
894
894
  let source: string;
895
895
  if (env === "1" || env === "0") source = `env BOTCORD_TRANSCRIPT=${env}`;
896
- else if (configEnabled) source = "config (transcript.enabled=true)";
897
- else source = "default-off";
896
+ else if (configEnabled === true) source = "config (transcript.enabled=true)";
897
+ else if (configEnabled === false) source = "config (transcript.enabled=false)";
898
+ else source = "default-on";
898
899
  console.log(`enabled: ${effective}`);
899
900
  console.log(`source: ${source}`);
900
901
  console.log(`root: ${defaultTranscriptRoot()}`);
@@ -942,11 +943,11 @@ function cmdTranscriptTail(args: ParsedArgs): Promise<void> | void {
942
943
  }
943
944
  const enabled = resolveTranscriptEnabled(
944
945
  process.env.BOTCORD_TRANSCRIPT,
945
- cfg?.transcript?.enabled === true,
946
+ cfg?.transcript?.enabled,
946
947
  );
947
948
  if (!enabled) {
948
949
  console.error(
949
- "hint: transcripts are disabled (default-off). Run `botcord-daemon transcript enable` and restart the daemon, then send a new message.",
950
+ "hint: transcripts are disabled. Run `botcord-daemon transcript enable` and restart the daemon, then send a new message.",
950
951
  );
951
952
  }
952
953
  process.exit(1);