@letta-ai/dreams 0.0.1

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.
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ /**
3
+ * Short-lived local process leases.
4
+ *
5
+ * Concurrent CLI invocations (a manual `dreams source scan` racing a future
6
+ * hook-triggered worker) must not both mutate the queue at once. A lease is a
7
+ * single row keyed by name; acquisition is atomic (one transaction), and a
8
+ * stale lease past its expiry is reclaimable so a crashed holder never wedges
9
+ * the CLI forever.
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.LeaseLostError = exports.SOURCE_LEASE = void 0;
46
+ exports.assertLeaseHeld = assertLeaseHeld;
47
+ exports.renewLease = renewLease;
48
+ exports.acquireLease = acquireLease;
49
+ exports.releaseLease = releaseLease;
50
+ exports.withLease = withLease;
51
+ const crypto = __importStar(require("node:crypto"));
52
+ const db_1 = require("./db");
53
+ const DEFAULT_TTL_MS = 30_000;
54
+ /**
55
+ * The single per-source mutation lease. Scanning, privacy cancellation, upload
56
+ * state transitions, acknowledged-cursor collapse, and blob GC all serialize
57
+ * against this one lease so they never interleave writes.
58
+ */
59
+ exports.SOURCE_LEASE = "source";
60
+ /** Thrown when a write is attempted after the lease was lost/reclaimed. */
61
+ class LeaseLostError extends Error {
62
+ constructor(name) {
63
+ super(`lease "${name}" was lost or reclaimed by another process`);
64
+ this.name = "LeaseLostError";
65
+ }
66
+ }
67
+ exports.LeaseLostError = LeaseLostError;
68
+ /**
69
+ * Fence token check: throws unless we still hold `name`. Call this INSIDE every
70
+ * write transaction so a process that stalled past its lease cannot commit
71
+ * state after another process reclaimed the lease.
72
+ */
73
+ function assertLeaseHeld(db, name, owner) {
74
+ const row = db.prepare("SELECT owner, expires_at FROM leases WHERE name = ?").get(name);
75
+ if (!row || row.owner !== owner || row.expires_at <= Date.now())
76
+ throw new LeaseLostError(name);
77
+ }
78
+ /** Extend our lease; returns false (without extending) if we no longer hold it. */
79
+ function renewLease(db, name, owner, ttlMs = DEFAULT_TTL_MS) {
80
+ const now = Date.now();
81
+ return (0, db_1.tx)(db, () => {
82
+ const row = db.prepare("SELECT owner, expires_at FROM leases WHERE name = ?").get(name);
83
+ if (!row || row.owner !== owner || row.expires_at <= now)
84
+ return false;
85
+ db.prepare("UPDATE leases SET expires_at = ? WHERE name = ? AND owner = ?").run(now + ttlMs, name, owner);
86
+ return true;
87
+ });
88
+ }
89
+ /** Atomically take `name` for `ttlMs`. Returns an owner token, or null if held. */
90
+ function acquireLease(db, name, ttlMs = DEFAULT_TTL_MS) {
91
+ const owner = crypto.randomUUID();
92
+ const now = Date.now();
93
+ return (0, db_1.tx)(db, () => {
94
+ const existing = db.prepare("SELECT owner, expires_at FROM leases WHERE name = ?").get(name);
95
+ if (existing && existing.expires_at > now)
96
+ return null;
97
+ db.prepare("INSERT INTO leases (name, owner, acquired_at, expires_at) VALUES (?, ?, ?, ?) " +
98
+ "ON CONFLICT(name) DO UPDATE SET owner = excluded.owner, acquired_at = excluded.acquired_at, expires_at = excluded.expires_at").run(name, owner, now, now + ttlMs);
99
+ return owner;
100
+ });
101
+ }
102
+ /** Release only if we still hold it (a reclaimed lease must not be released by the old owner). */
103
+ function releaseLease(db, name, owner) {
104
+ db.prepare("DELETE FROM leases WHERE name = ? AND owner = ?").run(name, owner);
105
+ }
106
+ /** Run `fn` under a lease; returns null without running if the lease is held elsewhere. */
107
+ function withLease(db, name, fn, ttlMs = DEFAULT_TTL_MS) {
108
+ const owner = acquireLease(db, name, ttlMs);
109
+ if (owner === null)
110
+ return { ran: false };
111
+ try {
112
+ return { ran: true, result: fn() };
113
+ }
114
+ finally {
115
+ releaseLease(db, name, owner);
116
+ }
117
+ }
@@ -0,0 +1,475 @@
1
+ "use strict";
2
+ /**
3
+ * Reconcile discovered Claude Code sessions into a durable local upload queue.
4
+ * Queue rows, exclusions, and the covered cursor form one contiguous prefix;
5
+ * accepted bytes remain distinct from pending bytes canceled by later policy.
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.policyFingerprint = policyFingerprint;
42
+ exports.chunkCompleteLines = chunkCompleteLines;
43
+ exports.addExcludedRange = addExcludedRange;
44
+ exports.isEligible = isEligible;
45
+ exports.reconcile = reconcile;
46
+ exports.listPendingQueue = listPendingQueue;
47
+ const path = __importStar(require("node:path"));
48
+ const blobs_1 = require("./blobs");
49
+ const claude_1 = require("./claude");
50
+ const config_1 = require("./config");
51
+ const db_1 = require("./db");
52
+ const dreamignore_1 = require("./dreamignore");
53
+ const leases_1 = require("./leases");
54
+ const ids_1 = require("./ids");
55
+ /** Raised when privacy policy tightens mid-scan; the affected session is skipped, not committed. */
56
+ class PolicyChangedError extends Error {
57
+ constructor() {
58
+ super("privacy policy changed during scan");
59
+ this.name = "PolicyChangedError";
60
+ }
61
+ }
62
+ /**
63
+ * Deterministic fingerprint of the policy that governs a session's root: the full
64
+ * approved-root set plus that root's `.dreamignore`. Stored on each queued row so
65
+ * the future uploader ([07]) can refuse to send rows enqueued under stale policy.
66
+ */
67
+ function policyFingerprint(roots, approvedRoot, ignoreText) {
68
+ return (0, ids_1.sha256Hex)(Buffer.from(JSON.stringify({ roots: [...roots].sort(), approvedRoot, ignore: ignoreText }), "utf8"));
69
+ }
70
+ 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) {
73
+ const segments = [];
74
+ let segmentStart = 0;
75
+ let segmentEnd = 0;
76
+ let lineStart = 0;
77
+ for (let i = 0; i < tail.length; i++) {
78
+ if (tail[i] !== 0x0a)
79
+ continue;
80
+ const lineEnd = i + 1;
81
+ const lineLength = lineEnd - lineStart;
82
+ if (lineLength > maxSegmentBytes)
83
+ throw new RangeError(`JSONL record is ${lineLength} bytes; maximum is ${maxSegmentBytes}`);
84
+ if (segmentEnd > segmentStart && lineEnd - segmentStart > maxSegmentBytes) {
85
+ segments.push({ relStart: segmentStart, relEnd: segmentEnd });
86
+ segmentStart = lineStart;
87
+ }
88
+ segmentEnd = lineEnd;
89
+ lineStart = lineEnd;
90
+ }
91
+ if (segmentEnd > segmentStart)
92
+ segments.push({ relStart: segmentStart, relEnd: segmentEnd });
93
+ return segments;
94
+ }
95
+ function getOrCreateSource(db, owner) {
96
+ const id = `src_${claude_1.CLAUDE_SOURCE_TYPE}`;
97
+ return (0, db_1.tx)(db, () => {
98
+ (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
99
+ const existing = db.prepare("SELECT id FROM sources WHERE id = ?").get(id);
100
+ if (!existing) {
101
+ const now = (0, db_1.nowIso)();
102
+ 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);
104
+ }
105
+ return id;
106
+ });
107
+ }
108
+ function currentFile(db, sourceId, sessionId) {
109
+ return db
110
+ .prepare("SELECT id, generation, size, mtime_ms, inode, first_chunk_fp, prefix_fp FROM source_files WHERE source_id = ? AND session_id = ? AND is_current = 1")
111
+ .get(sourceId, sessionId);
112
+ }
113
+ function createFileGeneration(db, sourceId, session, generation) {
114
+ const now = (0, db_1.nowIso)();
115
+ const info = db
116
+ .prepare(`INSERT INTO source_files
117
+ (source_id, session_id, generation, path, cwd, inode, birthtime_ms, size, mtime_ms, first_chunk_fp, prefix_fp, is_current, created_at, updated_at)
118
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 1, ?, ?)`)
119
+ .run(sourceId, session.session_id, generation, session.real_path, session.cwd, session.inode, session.birthtime_ms, session.size, session.mtime_ms, session.first_chunk_fp, now, now);
120
+ const fileId = Number(info.lastInsertRowid);
121
+ db.prepare("INSERT INTO source_cursors (source_file_id, queued_through_offset, acked_offset, updated_at) VALUES (?, 0, 0, ?)").run(fileId, now);
122
+ return fileId;
123
+ }
124
+ function readCursor(db, fileId) {
125
+ return db.prepare("SELECT queued_through_offset, acked_offset FROM source_cursors WHERE source_file_id = ?").get(fileId);
126
+ }
127
+ function isReplacement(stored, cursor, session, onProgress) {
128
+ if (session.size < cursor.queued_through_offset)
129
+ return true;
130
+ if (stored.inode && session.inode && stored.inode !== session.inode)
131
+ return true;
132
+ const statChanged = session.size !== stored.size || session.mtime_ms !== stored.mtime_ms;
133
+ 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)
135
+ return true;
136
+ }
137
+ if (stored.first_chunk_fp && session.first_chunk_fp && stored.first_chunk_fp !== session.first_chunk_fp)
138
+ return true;
139
+ return false;
140
+ }
141
+ function updateFileMeta(db, fileId, session, verified) {
142
+ db.prepare(`UPDATE source_files
143
+ SET path = ?, cwd = ?, size = ?, mtime_ms = ?, inode = ?, first_chunk_fp = ?, prefix_fp = ?, updated_at = ?
144
+ WHERE id = ?`).run(session.real_path, session.cwd, verified.size, verified.mtime_ms, verified.inode, session.first_chunk_fp, verified.prefix_fp, (0, db_1.nowIso)(), fileId);
145
+ }
146
+ function addExcludedRange(db, fileId, start, end, reason) {
147
+ if (end <= start)
148
+ return;
149
+ const overlaps = db
150
+ .prepare(`SELECT id, start_offset, end_offset, reason FROM excluded_ranges
151
+ WHERE source_file_id = ? AND end_offset >= ? AND start_offset <= ? ORDER BY start_offset`)
152
+ .all(fileId, start, end);
153
+ let mergedStart = start;
154
+ let mergedEnd = end;
155
+ let mergedReason = reason;
156
+ for (const row of overlaps) {
157
+ mergedStart = Math.min(mergedStart, row.start_offset);
158
+ mergedEnd = Math.max(mergedEnd, row.end_offset);
159
+ if (row.reason !== reason)
160
+ mergedReason = "privacy";
161
+ db.prepare("DELETE FROM excluded_ranges WHERE id = ?").run(row.id);
162
+ }
163
+ db.prepare("INSERT INTO excluded_ranges (source_file_id, start_offset, end_offset, reason, created_at) VALUES (?, ?, ?, ?, ?)").run(fileId, mergedStart, mergedEnd, mergedReason, (0, db_1.nowIso)());
164
+ }
165
+ function isEligible(roots, cwd, rulesByRoot) {
166
+ const approvedRoot = cwd ? (0, config_1.isUnderApprovedRoot)(roots, cwd) : null;
167
+ if (approvedRoot === null || cwd === null)
168
+ return { eligible: false, approvedRoot: null, ignored: false };
169
+ let rules = rulesByRoot.get(approvedRoot);
170
+ if (!rules) {
171
+ rules = (0, dreamignore_1.loadDreamignore)(approvedRoot);
172
+ rulesByRoot.set(approvedRoot, rules);
173
+ }
174
+ const ignored = (0, dreamignore_1.isIgnored)(rules, path.relative(approvedRoot, cwd), true);
175
+ return { eligible: !ignored, approvedRoot, ignored };
176
+ }
177
+ function deleteBlobIfUnreferenced(db, contentSha, owner) {
178
+ // Hold the SQLite writer lock while checking and deleting so a reclaimed
179
+ // process cannot commit a new reference between the query and unlink.
180
+ (0, db_1.tx)(db, () => {
181
+ (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
182
+ const row = db
183
+ .prepare("SELECT count(*) c FROM upload_queue WHERE content_sha256 = ? AND state IN ('pending','quarantined')")
184
+ .get(contentSha);
185
+ if (row.c === 0)
186
+ (0, blobs_1.deleteBlob)(contentSha);
187
+ });
188
+ }
189
+ function cleanupUnreferencedBlobs(db, hashes, owner) {
190
+ for (const contentSha of new Set(hashes)) {
191
+ try {
192
+ deleteBlobIfUnreferenced(db, contentSha, owner);
193
+ }
194
+ catch (error) {
195
+ if (error instanceof leases_1.LeaseLostError)
196
+ return; // The next holder will sweep it.
197
+ throw error;
198
+ }
199
+ }
200
+ }
201
+ function cancelPendingRows(db, rows, owner) {
202
+ if (rows.length === 0)
203
+ return 0;
204
+ const canceledHashes = [];
205
+ const count = (0, db_1.tx)(db, () => {
206
+ (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
207
+ let changed = 0;
208
+ for (const row of rows) {
209
+ const info = db
210
+ .prepare("UPDATE upload_queue SET state = 'canceled', updated_at = ? WHERE id = ? AND state = 'pending'")
211
+ .run((0, db_1.nowIso)(), row.id);
212
+ if (info.changes === 0)
213
+ continue;
214
+ addExcludedRange(db, row.source_file_id, row.start_offset, row.end_offset, "privacy");
215
+ canceledHashes.push(row.content_sha256);
216
+ changed++;
217
+ }
218
+ return changed;
219
+ });
220
+ // Filesystem deletion is intentionally after commit. A crash before this point
221
+ // leaves an orphan that the next lease holder removes; it never loses live data.
222
+ cleanupUnreferencedBlobs(db, canceledHashes, owner);
223
+ return count;
224
+ }
225
+ function pendingRows(db, sourceId, sessionId) {
226
+ const suffix = sessionId === undefined ? "" : " AND q.session_id = ?";
227
+ const params = sessionId === undefined ? [sourceId] : [sourceId, sessionId];
228
+ return db
229
+ .prepare(`SELECT q.id, q.source_file_id, q.session_id, q.start_offset, q.end_offset, q.content_sha256, sf.cwd
230
+ FROM upload_queue q JOIN source_files sf ON sf.id = q.source_file_id
231
+ WHERE q.source_id = ? AND q.state = 'pending'${suffix}`)
232
+ .all(...params);
233
+ }
234
+ function reconcilePendingPrivacy(db, sourceId, roots, rulesByRoot, owner) {
235
+ const disallowed = pendingRows(db, sourceId).filter((row) => !isEligible(roots, row.cwd, rulesByRoot).eligible);
236
+ return cancelPendingRows(db, disallowed, owner);
237
+ }
238
+ function reconcileOrphanBlobs(db, owner) {
239
+ return (0, db_1.tx)(db, () => {
240
+ (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
241
+ const rows = db
242
+ .prepare("SELECT DISTINCT content_sha256 FROM upload_queue WHERE state IN ('pending','quarantined')")
243
+ .all();
244
+ // The write lock fences lease reclamation until the filesystem sweep ends.
245
+ return (0, blobs_1.pruneOrphanBlobs)(new Set(rows.map((row) => row.content_sha256)));
246
+ });
247
+ }
248
+ function excludedRangesAfter(db, fileId, offset) {
249
+ return db
250
+ .prepare("SELECT start_offset, end_offset FROM excluded_ranges WHERE source_file_id = ? AND end_offset > ? ORDER BY start_offset, end_offset")
251
+ .all(fileId, offset);
252
+ }
253
+ function coalesceRange(ranges, start, end) {
254
+ const last = ranges[ranges.length - 1];
255
+ if (last && last.end_offset >= start)
256
+ last.end_offset = Math.max(last.end_offset, end);
257
+ else
258
+ ranges.push({ start_offset: start, end_offset: end });
259
+ }
260
+ function isFilesystemError(error) {
261
+ return error instanceof Error && typeof error.code === "string";
262
+ }
263
+ /** Record a per-file failure without advancing its cursor, so one poisoned session cannot abort the scan. */
264
+ function recordSessionFailure(db, fileId, code, message, owner) {
265
+ (0, db_1.tx)(db, () => {
266
+ (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
267
+ 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
+ });
269
+ }
270
+ function reconcile(db, options) {
271
+ const maxSegmentBytes = options.maxSegmentBytes ?? DEFAULT_MAX_SEGMENT_BYTES;
272
+ if (!Number.isSafeInteger(maxSegmentBytes) || maxSegmentBytes <= 0)
273
+ throw new Error("maxSegmentBytes must be a positive integer");
274
+ const priority = options.priority ?? "live";
275
+ const owner = options.leaseOwner;
276
+ const sourceId = getOrCreateSource(db, owner);
277
+ const roots = (0, config_1.getApprovedRoots)(db);
278
+ const rulesByRoot = new Map();
279
+ reconcileOrphanBlobs(db, owner);
280
+ const objectsCanceled = reconcilePendingPrivacy(db, sourceId, roots, rulesByRoot, owner);
281
+ const sessions = (0, claude_1.discoverSessions)(options.projectsDir);
282
+ const summary = {
283
+ source_id: sourceId,
284
+ sessions_discovered: sessions.length,
285
+ sessions_queued: 0,
286
+ sessions_excluded: 0,
287
+ sessions_skipped_unapproved: 0,
288
+ sessions_failed: 0,
289
+ generations_reset: 0,
290
+ objects_canceled: objectsCanceled,
291
+ segments_enqueued: 0,
292
+ bytes_enqueued: 0,
293
+ queued: [],
294
+ };
295
+ for (const session of sessions) {
296
+ if (!(0, leases_1.renewLease)(db, leases_1.SOURCE_LEASE, owner))
297
+ throw new leases_1.LeaseLostError(leases_1.SOURCE_LEASE);
298
+ let lastRenewedAt = Date.now();
299
+ const keepLeaseAlive = () => {
300
+ if (Date.now() - lastRenewedAt < 5_000)
301
+ return;
302
+ if (!(0, leases_1.renewLease)(db, leases_1.SOURCE_LEASE, owner))
303
+ throw new leases_1.LeaseLostError(leases_1.SOURCE_LEASE);
304
+ lastRenewedAt = Date.now();
305
+ };
306
+ let fileId;
307
+ let generation;
308
+ const stored = currentFile(db, sourceId, session.session_id);
309
+ if (!stored) {
310
+ ({ fileId, generation } = (0, db_1.tx)(db, () => {
311
+ (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
312
+ return { fileId: createFileGeneration(db, sourceId, session, 1), generation: 1 };
313
+ }));
314
+ }
315
+ else {
316
+ const cursor = readCursor(db, stored.id);
317
+ if (isReplacement(stored, cursor, session, keepLeaseAlive)) {
318
+ ({ fileId, generation } = (0, db_1.tx)(db, () => {
319
+ (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
320
+ db.prepare("UPDATE source_files SET is_current = 0, updated_at = ? WHERE id = ?").run((0, db_1.nowIso)(), stored.id);
321
+ return {
322
+ fileId: createFileGeneration(db, sourceId, session, stored.generation + 1),
323
+ generation: stored.generation + 1,
324
+ };
325
+ }));
326
+ summary.generations_reset++;
327
+ }
328
+ else {
329
+ fileId = stored.id;
330
+ generation = stored.generation;
331
+ }
332
+ }
333
+ const policy = isEligible(roots, session.cwd, rulesByRoot);
334
+ // Pre-existing pending rows that are no longer eligible were already canceled
335
+ // by reconcilePendingPrivacy using each row's OWN persisted cwd. Do NOT cancel
336
+ // by session id here: a still-eligible older generation must not be dropped
337
+ // because a newer generation's cwd is disallowed.
338
+ if (policy.approvedRoot === null) {
339
+ summary.sessions_skipped_unapproved++;
340
+ continue;
341
+ }
342
+ const prepared = [];
343
+ try {
344
+ const cursor = readCursor(db, fileId);
345
+ const scanStart = cursor.queued_through_offset;
346
+ const explicitExclusions = excludedRangesAfter(db, fileId, scanStart);
347
+ let exclusionIndex = 0;
348
+ const excludedLines = [];
349
+ let segmentLines = [];
350
+ let segmentLength = 0;
351
+ let segmentStart = 0;
352
+ let segmentEnd = 0;
353
+ const flushSegment = () => {
354
+ if (segmentLength === 0)
355
+ return;
356
+ const bytes = Buffer.concat(segmentLines, segmentLength);
357
+ const contentSha = (0, blobs_1.writeBlob)(bytes);
358
+ prepared.push({
359
+ upload_id: (0, ids_1.segmentId)({
360
+ installation_id: options.installationId,
361
+ source_key: claude_1.CLAUDE_SOURCE_TYPE,
362
+ file_identity: session.session_id,
363
+ generation,
364
+ start_offset: segmentStart,
365
+ end_offset: segmentEnd,
366
+ }),
367
+ source_file_id: fileId,
368
+ session_id: session.session_id,
369
+ generation,
370
+ start_offset: segmentStart,
371
+ end_offset: segmentEnd,
372
+ byte_length: segmentLength,
373
+ content_sha256: contentSha,
374
+ });
375
+ segmentLines = [];
376
+ segmentLength = 0;
377
+ };
378
+ const verified = (0, claude_1.scanCompleteLinesVerified)(session.real_path, scanStart, session.inode, maxSegmentBytes, (line) => {
379
+ while (exclusionIndex < explicitExclusions.length && explicitExclusions[exclusionIndex].end_offset <= line.start_offset) {
380
+ exclusionIndex++;
381
+ }
382
+ const explicit = exclusionIndex < explicitExclusions.length &&
383
+ explicitExclusions[exclusionIndex].start_offset < line.end_offset &&
384
+ explicitExclusions[exclusionIndex].end_offset > line.start_offset;
385
+ if (policy.ignored || explicit) {
386
+ flushSegment();
387
+ coalesceRange(excludedLines, line.start_offset, line.end_offset);
388
+ return;
389
+ }
390
+ if (segmentLength > 0 && segmentLength + line.bytes.length > maxSegmentBytes)
391
+ flushSegment();
392
+ if (segmentLength === 0)
393
+ segmentStart = line.start_offset;
394
+ segmentLines.push(line.bytes);
395
+ segmentLength += line.bytes.length;
396
+ segmentEnd = line.end_offset;
397
+ }, keepLeaseAlive);
398
+ flushSegment();
399
+ const newOffset = verified.complete_through_offset;
400
+ if (newOffset === scanStart) {
401
+ (0, db_1.tx)(db, () => {
402
+ (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
403
+ updateFileMeta(db, fileId, session, verified);
404
+ });
405
+ continue;
406
+ }
407
+ const enqueued = [];
408
+ (0, db_1.tx)(db, () => {
409
+ (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
410
+ // Re-read policy at the durable commit boundary. Approved roots are
411
+ // transactional (kv); .dreamignore is reloaded from disk. If a tightening
412
+ // raced this scan, abort rather than commit now-disallowed pending rows;
413
+ // the fingerprint stored on each row lets the uploader ([07]) re-gate.
414
+ let policyFp = null;
415
+ if (prepared.length > 0) {
416
+ const freshRoots = (0, config_1.getApprovedRoots)(db);
417
+ const fresh = isEligible(freshRoots, session.cwd, new Map());
418
+ if (!fresh.eligible || fresh.approvedRoot === null)
419
+ throw new PolicyChangedError();
420
+ policyFp = policyFingerprint(freshRoots, fresh.approvedRoot, (0, dreamignore_1.loadDreamignoreText)(fresh.approvedRoot));
421
+ }
422
+ for (const range of excludedLines)
423
+ addExcludedRange(db, fileId, range.start_offset, range.end_offset, policy.ignored ? "dreamignore" : "privacy");
424
+ for (const entry of prepared) {
425
+ const now = (0, db_1.nowIso)();
426
+ const info = db
427
+ .prepare(`INSERT OR IGNORE INTO upload_queue
428
+ (upload_id, source_id, source_file_id, session_id, generation, start_offset, end_offset, byte_length, content_sha256, priority, state, policy_fingerprint, created_at, updated_at)
429
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?)`)
430
+ .run(entry.upload_id, sourceId, fileId, session.session_id, generation, entry.start_offset, entry.end_offset, entry.byte_length, entry.content_sha256, priority, policyFp, now, now);
431
+ if (info.changes > 0) {
432
+ const { source_file_id: _sourceFileId, ...queued } = entry;
433
+ enqueued.push(queued);
434
+ }
435
+ }
436
+ db.prepare("UPDATE source_cursors SET queued_through_offset = ?, updated_at = ? WHERE source_file_id = ?").run(newOffset, (0, db_1.nowIso)(), fileId);
437
+ updateFileMeta(db, fileId, session, verified);
438
+ });
439
+ cleanupUnreferencedBlobs(db, prepared.map((entry) => entry.content_sha256), owner);
440
+ if (excludedLines.length > 0)
441
+ summary.sessions_excluded++;
442
+ if (enqueued.length > 0) {
443
+ summary.sessions_queued++;
444
+ summary.segments_enqueued += enqueued.length;
445
+ for (const segment of enqueued) {
446
+ summary.bytes_enqueued += segment.byte_length;
447
+ summary.queued.push(segment);
448
+ }
449
+ }
450
+ }
451
+ catch (error) {
452
+ // Blobs published before an aborted commit become orphans; remove the ones
453
+ // no committed row references (safe even on partial failure).
454
+ cleanupUnreferencedBlobs(db, prepared.map((entry) => entry.content_sha256), owner);
455
+ if (error instanceof leases_1.LeaseLostError)
456
+ throw error;
457
+ if (error instanceof claude_1.IdentityChangedError || error instanceof PolicyChangedError)
458
+ continue;
459
+ if (error instanceof claude_1.OversizedLineError || isFilesystemError(error)) {
460
+ const code = error instanceof claude_1.OversizedLineError ? "oversized_record" : error.code ?? "fs_error";
461
+ recordSessionFailure(db, fileId, code, error.message, owner);
462
+ summary.sessions_failed++;
463
+ continue;
464
+ }
465
+ throw error;
466
+ }
467
+ }
468
+ return summary;
469
+ }
470
+ function listPendingQueue(db) {
471
+ return db
472
+ .prepare(`SELECT upload_id, source_id, session_id, generation, start_offset, end_offset, byte_length, content_sha256, priority, state
473
+ FROM upload_queue WHERE state = 'pending' ORDER BY session_id, generation, start_offset`)
474
+ .all();
475
+ }