@letta-ai/dreams 0.0.1 → 0.0.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.
@@ -46,7 +46,7 @@ exports.reconcile = reconcile;
46
46
  exports.listPendingQueue = listPendingQueue;
47
47
  const path = __importStar(require("node:path"));
48
48
  const blobs_1 = require("./blobs");
49
- const claude_1 = require("./claude");
49
+ const transcript_bytes_1 = require("./transcript-bytes");
50
50
  const config_1 = require("./config");
51
51
  const db_1 = require("./db");
52
52
  const dreamignore_1 = require("./dreamignore");
@@ -59,6 +59,13 @@ class PolicyChangedError extends Error {
59
59
  this.name = "PolicyChangedError";
60
60
  }
61
61
  }
62
+ /** Raised when shouldCancel trips mid-session so pause can stop a large scan. */
63
+ class ScanCancelledError extends Error {
64
+ constructor() {
65
+ super("scan cancelled");
66
+ this.name = "ScanCancelledError";
67
+ }
68
+ }
62
69
  /**
63
70
  * Deterministic fingerprint of the policy that governs a session's root: the full
64
71
  * approved-root set plus that root's `.dreamignore`. Stored on each queued row so
@@ -67,9 +74,31 @@ class PolicyChangedError extends Error {
67
74
  function policyFingerprint(roots, approvedRoot, ignoreText) {
68
75
  return (0, ids_1.sha256Hex)(Buffer.from(JSON.stringify({ roots: [...roots].sort(), approvedRoot, ignore: ignoreText }), "utf8"));
69
76
  }
77
+ /**
78
+ * Soft packing target (`targetSegmentBytes` in design notes): pack ordinary
79
+ * complete JSONL lines until the next line would exceed this. Shared by every
80
+ * SourceAdapter — not Codex-specific.
81
+ */
70
82
  const DEFAULT_MAX_SEGMENT_BYTES = 1024 * 1024;
71
- /** Pure helper retained for unit callers; oversized lines are a hard failure. */
72
- function chunkCompleteLines(tail, maxSegmentBytes) {
83
+ /**
84
+ * Hard per-record safety limit for a single complete JSONL line. Lines above
85
+ * the soft target but at/under this cap flush the preceding segment and queue
86
+ * alone; only larger lines isolate the session. The final transport gate is
87
+ * Cloud's gzip+base64 content char cap (enforced at upload time).
88
+ */
89
+ const DEFAULT_MAX_RECORD_BYTES = 8 * 1024 * 1024;
90
+ /**
91
+ * Pure helper retained for unit callers.
92
+ * Lines above `maxSegmentBytes` but at/under `maxRecordBytes` become standalone
93
+ * segments; only lines above `maxRecordBytes` throw.
94
+ */
95
+ function chunkCompleteLines(tail, maxSegmentBytes, maxRecordBytes = maxSegmentBytes) {
96
+ if (!Number.isSafeInteger(maxSegmentBytes) || maxSegmentBytes <= 0) {
97
+ throw new Error("maxSegmentBytes must be a positive integer");
98
+ }
99
+ if (!Number.isSafeInteger(maxRecordBytes) || maxRecordBytes < maxSegmentBytes) {
100
+ throw new Error("maxRecordBytes must be an integer >= maxSegmentBytes");
101
+ }
73
102
  const segments = [];
74
103
  let segmentStart = 0;
75
104
  let segmentEnd = 0;
@@ -79,8 +108,18 @@ function chunkCompleteLines(tail, maxSegmentBytes) {
79
108
  continue;
80
109
  const lineEnd = i + 1;
81
110
  const lineLength = lineEnd - lineStart;
82
- if (lineLength > maxSegmentBytes)
83
- throw new RangeError(`JSONL record is ${lineLength} bytes; maximum is ${maxSegmentBytes}`);
111
+ if (lineLength > maxRecordBytes) {
112
+ throw new RangeError(`JSONL record is ${lineLength} bytes; maximum is ${maxRecordBytes}`);
113
+ }
114
+ if (lineLength > maxSegmentBytes) {
115
+ if (segmentEnd > segmentStart)
116
+ segments.push({ relStart: segmentStart, relEnd: segmentEnd });
117
+ segments.push({ relStart: lineStart, relEnd: lineEnd });
118
+ segmentStart = lineEnd;
119
+ segmentEnd = lineEnd;
120
+ lineStart = lineEnd;
121
+ continue;
122
+ }
84
123
  if (segmentEnd > segmentStart && lineEnd - segmentStart > maxSegmentBytes) {
85
124
  segments.push({ relStart: segmentStart, relEnd: segmentEnd });
86
125
  segmentStart = lineStart;
@@ -92,15 +131,15 @@ function chunkCompleteLines(tail, maxSegmentBytes) {
92
131
  segments.push({ relStart: segmentStart, relEnd: segmentEnd });
93
132
  return segments;
94
133
  }
95
- function getOrCreateSource(db, owner) {
96
- const id = `src_${claude_1.CLAUDE_SOURCE_TYPE}`;
134
+ function getOrCreateSource(db, owner, adapter) {
135
+ const id = adapter.sourceId;
97
136
  return (0, db_1.tx)(db, () => {
98
137
  (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
99
138
  const existing = db.prepare("SELECT id FROM sources WHERE id = ?").get(id);
100
139
  if (!existing) {
101
140
  const now = (0, db_1.nowIso)();
102
141
  db.prepare(`INSERT INTO sources (id, source_type, source_key, display_name, adapter_version, status, created_at, updated_at)
103
- VALUES (?, ?, ?, ?, ?, 'active', ?, ?)`).run(id, claude_1.CLAUDE_SOURCE_TYPE, claude_1.CLAUDE_SOURCE_TYPE, "Claude Code", claude_1.CLAUDE_ADAPTER_VERSION, now, now);
142
+ VALUES (?, ?, ?, ?, ?, 'detected', ?, ?)`).run(id, adapter.sourceType, adapter.sourceKey, adapter.displayName, adapter.adapterVersion, now, now);
104
143
  }
105
144
  return id;
106
145
  });
@@ -131,7 +170,7 @@ function isReplacement(stored, cursor, session, onProgress) {
131
170
  return true;
132
171
  const statChanged = session.size !== stored.size || session.mtime_ms !== stored.mtime_ms;
133
172
  if (statChanged && cursor.queued_through_offset > 0 && stored.prefix_fp) {
134
- if ((0, claude_1.prefixFingerprint)(session.real_path, cursor.queued_through_offset, onProgress) !== stored.prefix_fp)
173
+ if ((0, transcript_bytes_1.prefixFingerprint)(session.real_path, cursor.queued_through_offset, onProgress) !== stored.prefix_fp)
135
174
  return true;
136
175
  }
137
176
  if (stored.first_chunk_fp && session.first_chunk_fp && stored.first_chunk_fp !== session.first_chunk_fp)
@@ -267,21 +306,43 @@ function recordSessionFailure(db, fileId, code, message, owner) {
267
306
  db.prepare("UPDATE source_files SET last_error_code = ?, last_error_message = ?, last_error_at = ?, updated_at = ? WHERE id = ?").run(code, message.slice(0, 500), (0, db_1.nowIso)(), (0, db_1.nowIso)(), fileId);
268
307
  });
269
308
  }
309
+ function clearSessionFailure(db, fileId, owner) {
310
+ (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
311
+ db.prepare("UPDATE source_files SET last_error_code = NULL, last_error_message = NULL, last_error_at = NULL, updated_at = ? WHERE id = ?").run((0, db_1.nowIso)(), fileId);
312
+ }
270
313
  function reconcile(db, options) {
271
314
  const maxSegmentBytes = options.maxSegmentBytes ?? DEFAULT_MAX_SEGMENT_BYTES;
272
315
  if (!Number.isSafeInteger(maxSegmentBytes) || maxSegmentBytes <= 0)
273
316
  throw new Error("maxSegmentBytes must be a positive integer");
317
+ const maxRecordBytes = options.maxRecordBytes ?? DEFAULT_MAX_RECORD_BYTES;
318
+ if (!Number.isSafeInteger(maxRecordBytes) || maxRecordBytes < maxSegmentBytes) {
319
+ throw new Error("maxRecordBytes must be an integer >= maxSegmentBytes");
320
+ }
274
321
  const priority = options.priority ?? "live";
275
322
  const owner = options.leaseOwner;
276
- const sourceId = getOrCreateSource(db, owner);
323
+ const adapter = options.adapter;
324
+ const sourceId = getOrCreateSource(db, owner, adapter);
277
325
  const roots = (0, config_1.getApprovedRoots)(db);
278
326
  const rulesByRoot = new Map();
279
327
  reconcileOrphanBlobs(db, owner);
280
328
  const objectsCanceled = reconcilePendingPrivacy(db, sourceId, roots, rulesByRoot, owner);
281
- const sessions = (0, claude_1.discoverSessions)(options.projectsDir);
329
+ const discovered = adapter.discoverSessions();
330
+ const mode = options.mode ?? "all";
331
+ let sessions = discovered;
332
+ if (mode === "live") {
333
+ const tracked = new Set(db
334
+ .prepare("SELECT session_id FROM source_files WHERE source_id = ? AND is_current = 1")
335
+ .all(sourceId).map((row) => row.session_id));
336
+ const hints = new Set(options.hintSessionIds ?? []);
337
+ sessions = discovered.filter((session) => tracked.has(session.session_id) || hints.has(session.session_id));
338
+ }
339
+ else if (mode === "backfill") {
340
+ const hints = new Set(options.hintSessionIds ?? []);
341
+ sessions = discovered.filter((session) => hints.has(session.session_id));
342
+ }
282
343
  const summary = {
283
344
  source_id: sourceId,
284
- sessions_discovered: sessions.length,
345
+ sessions_discovered: discovered.length,
285
346
  sessions_queued: 0,
286
347
  sessions_excluded: 0,
287
348
  sessions_skipped_unapproved: 0,
@@ -291,17 +352,26 @@ function reconcile(db, options) {
291
352
  segments_enqueued: 0,
292
353
  bytes_enqueued: 0,
293
354
  queued: [],
355
+ processed_sessions: [],
294
356
  };
295
357
  for (const session of sessions) {
358
+ if (options.shouldCancel?.())
359
+ break;
360
+ options.onProgress?.();
296
361
  if (!(0, leases_1.renewLease)(db, leases_1.SOURCE_LEASE, owner))
297
362
  throw new leases_1.LeaseLostError(leases_1.SOURCE_LEASE);
298
363
  let lastRenewedAt = Date.now();
299
364
  const keepLeaseAlive = () => {
365
+ // Cancel checks are cheap and must run during large single-session scans.
366
+ if (options.shouldCancel?.())
367
+ throw new ScanCancelledError();
300
368
  if (Date.now() - lastRenewedAt < 5_000)
301
369
  return;
302
370
  if (!(0, leases_1.renewLease)(db, leases_1.SOURCE_LEASE, owner))
303
371
  throw new leases_1.LeaseLostError(leases_1.SOURCE_LEASE);
304
372
  lastRenewedAt = Date.now();
373
+ // Heartbeat only on the throttled renewal path (not once per chunk).
374
+ options.onProgress?.();
305
375
  };
306
376
  let fileId;
307
377
  let generation;
@@ -358,7 +428,7 @@ function reconcile(db, options) {
358
428
  prepared.push({
359
429
  upload_id: (0, ids_1.segmentId)({
360
430
  installation_id: options.installationId,
361
- source_key: claude_1.CLAUDE_SOURCE_TYPE,
431
+ source_key: adapter.sourceKey,
362
432
  file_identity: session.session_id,
363
433
  generation,
364
434
  start_offset: segmentStart,
@@ -375,7 +445,7 @@ function reconcile(db, options) {
375
445
  segmentLines = [];
376
446
  segmentLength = 0;
377
447
  };
378
- const verified = (0, claude_1.scanCompleteLinesVerified)(session.real_path, scanStart, session.inode, maxSegmentBytes, (line) => {
448
+ const verified = (0, transcript_bytes_1.scanCompleteLinesVerified)(session.real_path, scanStart, session.inode, maxRecordBytes, (line) => {
379
449
  while (exclusionIndex < explicitExclusions.length && explicitExclusions[exclusionIndex].end_offset <= line.start_offset) {
380
450
  exclusionIndex++;
381
451
  }
@@ -387,6 +457,10 @@ function reconcile(db, options) {
387
457
  coalesceRange(excludedLines, line.start_offset, line.end_offset);
388
458
  return;
389
459
  }
460
+ // Soft packing target: flush before adding a line that would overflow.
461
+ // A single complete record above the target (but under the hard cap)
462
+ // becomes its own segment so real Codex multimodal lines (~2.7 MiB)
463
+ // still upload.
390
464
  if (segmentLength > 0 && segmentLength + line.bytes.length > maxSegmentBytes)
391
465
  flushSegment();
392
466
  if (segmentLength === 0)
@@ -394,6 +468,8 @@ function reconcile(db, options) {
394
468
  segmentLines.push(line.bytes);
395
469
  segmentLength += line.bytes.length;
396
470
  segmentEnd = line.end_offset;
471
+ if (line.bytes.length > maxSegmentBytes)
472
+ flushSegment();
397
473
  }, keepLeaseAlive);
398
474
  flushSegment();
399
475
  const newOffset = verified.complete_through_offset;
@@ -401,7 +477,9 @@ function reconcile(db, options) {
401
477
  (0, db_1.tx)(db, () => {
402
478
  (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
403
479
  updateFileMeta(db, fileId, session, verified);
480
+ clearSessionFailure(db, fileId, owner);
404
481
  });
482
+ summary.processed_sessions.push({ session_id: session.session_id, generation });
405
483
  continue;
406
484
  }
407
485
  const enqueued = [];
@@ -435,6 +513,7 @@ function reconcile(db, options) {
435
513
  }
436
514
  db.prepare("UPDATE source_cursors SET queued_through_offset = ?, updated_at = ? WHERE source_file_id = ?").run(newOffset, (0, db_1.nowIso)(), fileId);
437
515
  updateFileMeta(db, fileId, session, verified);
516
+ clearSessionFailure(db, fileId, owner);
438
517
  });
439
518
  cleanupUnreferencedBlobs(db, prepared.map((entry) => entry.content_sha256), owner);
440
519
  if (excludedLines.length > 0)
@@ -447,6 +526,7 @@ function reconcile(db, options) {
447
526
  summary.queued.push(segment);
448
527
  }
449
528
  }
529
+ summary.processed_sessions.push({ session_id: session.session_id, generation });
450
530
  }
451
531
  catch (error) {
452
532
  // Blobs published before an aborted commit become orphans; remove the ones
@@ -454,10 +534,12 @@ function reconcile(db, options) {
454
534
  cleanupUnreferencedBlobs(db, prepared.map((entry) => entry.content_sha256), owner);
455
535
  if (error instanceof leases_1.LeaseLostError)
456
536
  throw error;
457
- if (error instanceof claude_1.IdentityChangedError || error instanceof PolicyChangedError)
537
+ if (error instanceof ScanCancelledError)
538
+ break;
539
+ if (error instanceof transcript_bytes_1.IdentityChangedError || error instanceof PolicyChangedError)
458
540
  continue;
459
- if (error instanceof claude_1.OversizedLineError || isFilesystemError(error)) {
460
- const code = error instanceof claude_1.OversizedLineError ? "oversized_record" : error.code ?? "fs_error";
541
+ if (error instanceof transcript_bytes_1.OversizedLineError || isFilesystemError(error)) {
542
+ const code = error instanceof transcript_bytes_1.OversizedLineError ? "oversized_record" : error.code ?? "fs_error";
461
543
  recordSessionFailure(db, fileId, code, error.message, owner);
462
544
  summary.sessions_failed++;
463
545
  continue;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ /**
3
+ * Multi-source adapter types ([LET-10313]).
4
+ *
5
+ * Types only — registration lives in `source-adapters.ts` so default selection
6
+ * is deterministic and not import-order dependent.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ /**
3
+ * Deterministic source-adapter registry ([LET-10313]).
4
+ *
5
+ * Imports and registers Claude as the explicit default. Callers that need the
6
+ * default (CLI/worker boundaries, tests) import this module — not `claude.ts`
7
+ * for side effects.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.registerSourceAdapter = registerSourceAdapter;
11
+ exports.getSourceAdapterById = getSourceAdapterById;
12
+ exports.getSourceAdapterByType = getSourceAdapterByType;
13
+ exports.defaultSourceAdapter = defaultSourceAdapter;
14
+ exports.listSourceAdapters = listSourceAdapters;
15
+ const claude_1 = require("./claude");
16
+ const codex_1 = require("./codex");
17
+ const bySourceId = new Map();
18
+ const bySourceType = new Map();
19
+ let defaultSourceId = null;
20
+ function registerSourceAdapter(adapter, options = {}) {
21
+ // Validate every conflict before mutating registry state.
22
+ if (bySourceId.has(adapter.sourceId)) {
23
+ throw new Error(`duplicate source id registration: ${adapter.sourceId}`);
24
+ }
25
+ if (bySourceType.has(adapter.sourceType)) {
26
+ throw new Error(`duplicate source type registration: ${adapter.sourceType}`);
27
+ }
28
+ if (options.default && defaultSourceId !== null && defaultSourceId !== adapter.sourceId) {
29
+ throw new Error(`default source adapter already set: ${defaultSourceId}`);
30
+ }
31
+ bySourceId.set(adapter.sourceId, adapter);
32
+ bySourceType.set(adapter.sourceType, adapter);
33
+ if (options.default)
34
+ defaultSourceId = adapter.sourceId;
35
+ }
36
+ function getSourceAdapterById(sourceId) {
37
+ const adapter = bySourceId.get(sourceId);
38
+ if (!adapter)
39
+ throw new Error(`unknown source id: ${sourceId}`);
40
+ return adapter;
41
+ }
42
+ function getSourceAdapterByType(sourceType) {
43
+ const adapter = bySourceType.get(sourceType);
44
+ if (!adapter)
45
+ throw new Error(`unknown source type: ${sourceType}`);
46
+ return adapter;
47
+ }
48
+ /** CLI / worker default — Claude, registered explicitly below. */
49
+ function defaultSourceAdapter() {
50
+ if (!defaultSourceId)
51
+ throw new Error("no default source adapter registered");
52
+ return getSourceAdapterById(defaultSourceId);
53
+ }
54
+ function listSourceAdapters() {
55
+ return [...bySourceId.values()];
56
+ }
57
+ registerSourceAdapter(claude_1.claudeSourceAdapter, { default: true });
58
+ registerSourceAdapter(codex_1.codexSourceAdapter);
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.localStateLockFilePath = localStateLockFilePath;
37
+ exports.acquireLocalStateLock = acquireLocalStateLock;
38
+ exports.waitForLocalStateLock = waitForLocalStateLock;
39
+ exports.releaseLocalStateLock = releaseLocalStateLock;
40
+ const node_sqlite_1 = require("node:sqlite");
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
43
+ const store_1 = require("../store");
44
+ function localStateLockFilePath() {
45
+ return path.join((0, store_1.dreamsDir)(), "state-lock.db");
46
+ }
47
+ function acquireLocalStateLock(options = {}) {
48
+ const file = localStateLockFilePath();
49
+ const directory = path.dirname(file);
50
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
51
+ const db = new node_sqlite_1.DatabaseSync(file);
52
+ try {
53
+ db.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE");
54
+ if (options.allowAbandonedReset !== true &&
55
+ fs.readdirSync(directory).some((name) => name.startsWith(".reset-staging-") || name.startsWith(".reset-delete-"))) {
56
+ db.exec("ROLLBACK");
57
+ db.close();
58
+ return null;
59
+ }
60
+ return { db };
61
+ }
62
+ catch (error) {
63
+ db.close();
64
+ const message = error instanceof Error ? error.message : String(error);
65
+ if (/busy|locked/i.test(message))
66
+ return null;
67
+ throw error;
68
+ }
69
+ }
70
+ async function waitForLocalStateLock(timeoutMs = 5_000) {
71
+ const deadline = Date.now() + timeoutMs;
72
+ for (;;) {
73
+ const lock = acquireLocalStateLock();
74
+ if (lock !== null)
75
+ return lock;
76
+ if (Date.now() >= deadline)
77
+ return null;
78
+ await new Promise((resolve) => setTimeout(resolve, 25));
79
+ }
80
+ }
81
+ function releaseLocalStateLock(lock) {
82
+ try {
83
+ lock.db.exec("ROLLBACK");
84
+ }
85
+ finally {
86
+ lock.db.close();
87
+ }
88
+ }