@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.
@@ -0,0 +1,608 @@
1
+ "use strict";
2
+ /**
3
+ * Historical backfill policy + session frontier ([LET-10239] / [LET-10240]).
4
+ *
5
+ * `dreams backfill start --since <RFC3339>` freezes `until_at = now`, persists
6
+ * the approved window, and runs one bounded local scan/queue pass. The sync
7
+ * worker continues an existing active policy via `runBackfillContinuePass`
8
+ * (never mints a new policy).
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.BackfillArgumentError = exports.BackfillPolicyConflictError = exports.DEFAULT_BACKFILL_SESSION_BUDGET = exports.DEFAULT_SOURCE_ID = void 0;
45
+ exports.parseRfc3339 = parseRfc3339;
46
+ exports.sessionBudget = sessionBudget;
47
+ exports.readBackfillPolicy = readBackfillPolicy;
48
+ exports.ensureBackfillPolicy = ensureBackfillPolicy;
49
+ exports.runBackfillStartPass = runBackfillStartPass;
50
+ exports.runBackfillContinuePass = runBackfillContinuePass;
51
+ exports.commandBackfillStart = commandBackfillStart;
52
+ const crypto = __importStar(require("node:crypto"));
53
+ const store_1 = require("../store");
54
+ const config_1 = require("./config");
55
+ const db_1 = require("./db");
56
+ const leases_1 = require("./leases");
57
+ const scan_1 = require("./scan");
58
+ const source_adapters_1 = require("./source-adapters");
59
+ const state_lock_1 = require("./state-lock");
60
+ /** CLI default — Claude via deterministic registry ([LET-10313]). */
61
+ exports.DEFAULT_SOURCE_ID = (0, source_adapters_1.defaultSourceAdapter)().sourceId;
62
+ exports.DEFAULT_BACKFILL_SESSION_BUDGET = 16;
63
+ class BackfillPolicyConflictError extends Error {
64
+ code = "backfill_policy_conflict";
65
+ constructor(message) {
66
+ super(message);
67
+ this.name = "BackfillPolicyConflictError";
68
+ }
69
+ }
70
+ exports.BackfillPolicyConflictError = BackfillPolicyConflictError;
71
+ class BackfillArgumentError extends Error {
72
+ code = "invalid_argument";
73
+ constructor(message) {
74
+ super(message);
75
+ this.name = "BackfillArgumentError";
76
+ }
77
+ }
78
+ exports.BackfillArgumentError = BackfillArgumentError;
79
+ function ensureLocalSource(db, adapter) {
80
+ const now = (0, db_1.nowIso)();
81
+ const id = adapter.sourceId;
82
+ db.prepare(`INSERT INTO sources (id, source_type, source_key, display_name, adapter_version, status, created_at, updated_at)
83
+ VALUES (?, ?, ?, ?, ?, 'detected', ?, ?)
84
+ ON CONFLICT(id) DO NOTHING`).run(id, adapter.sourceType, adapter.sourceKey, adapter.displayName, adapter.adapterVersion, now, now);
85
+ return id;
86
+ }
87
+ /** Strict RFC3339: timezone required, and calendar components must be real dates. */
88
+ function parseRfc3339(value, label) {
89
+ const match = value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:Z|([+-])(\d{2}):(\d{2}))$/);
90
+ if (!match) {
91
+ throw new BackfillArgumentError(`${label} must be RFC3339 with a timezone (e.g. 2026-01-01T00:00:00.000Z)`);
92
+ }
93
+ const year = Number(match[1]);
94
+ const month = Number(match[2]);
95
+ const day = Number(match[3]);
96
+ const hour = Number(match[4]);
97
+ const minute = Number(match[5]);
98
+ const second = Number(match[6]);
99
+ if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59) {
100
+ throw new BackfillArgumentError(`${label} has out-of-range date/time components`);
101
+ }
102
+ const utc = Date.UTC(year, month - 1, day, hour, minute, second);
103
+ const probe = new Date(utc);
104
+ if (probe.getUTCFullYear() !== year ||
105
+ probe.getUTCMonth() !== month - 1 ||
106
+ probe.getUTCDate() !== day ||
107
+ probe.getUTCHours() !== hour ||
108
+ probe.getUTCMinutes() !== minute ||
109
+ probe.getUTCSeconds() !== second) {
110
+ throw new BackfillArgumentError(`${label} is not a real calendar date/time`);
111
+ }
112
+ const ms = Date.parse(value);
113
+ if (!Number.isFinite(ms)) {
114
+ throw new BackfillArgumentError(`${label} must be a valid RFC3339 timestamp`);
115
+ }
116
+ return new Date(ms).toISOString();
117
+ }
118
+ function sessionBudget() {
119
+ const raw = process.env.DREAMS_BACKFILL_SESSION_BUDGET;
120
+ if (raw) {
121
+ const n = Number(raw);
122
+ if (Number.isSafeInteger(n) && n > 0)
123
+ return n;
124
+ }
125
+ return exports.DEFAULT_BACKFILL_SESSION_BUDGET;
126
+ }
127
+ function readBackfillPolicy(db, sourceId = exports.DEFAULT_SOURCE_ID) {
128
+ const row = db.prepare("SELECT backfill_policy FROM sources WHERE id = ?").get(sourceId);
129
+ if (!row?.backfill_policy)
130
+ return null;
131
+ try {
132
+ const parsed = JSON.parse(row.backfill_policy);
133
+ if (parsed.version !== 1 ||
134
+ typeof parsed.policy_id !== "string" ||
135
+ typeof parsed.since_at !== "string" ||
136
+ typeof parsed.until_at !== "string" ||
137
+ (parsed.status !== "active" && parsed.status !== "completed") ||
138
+ typeof parsed.started_at !== "string") {
139
+ return null;
140
+ }
141
+ return {
142
+ version: 1,
143
+ policy_id: parsed.policy_id,
144
+ since_at: parsed.since_at,
145
+ until_at: parsed.until_at,
146
+ status: parsed.status,
147
+ started_at: parsed.started_at,
148
+ completed_at: typeof parsed.completed_at === "string" ? parsed.completed_at : null,
149
+ };
150
+ }
151
+ catch {
152
+ return null;
153
+ }
154
+ }
155
+ function writePolicy(db, sourceId, policy) {
156
+ db.prepare("UPDATE sources SET backfill_policy = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(policy), (0, db_1.nowIso)(), sourceId);
157
+ }
158
+ /**
159
+ * Initialize or resume a backfill policy.
160
+ * Same active since_at is idempotent (keeps frozen until_at + policy_id).
161
+ * A different active since_at fails closed without mutation.
162
+ * Starting after a completed policy mints a new policy_id (fresh frontier).
163
+ */
164
+ function ensureBackfillPolicy(db, sinceRaw, adapter) {
165
+ const sinceAt = parseRfc3339(sinceRaw, "--since");
166
+ const sourceId = adapter.sourceId;
167
+ return (0, db_1.tx)(db, () => {
168
+ ensureLocalSource(db, adapter);
169
+ const existing = readBackfillPolicy(db, sourceId);
170
+ if (existing?.status === "active") {
171
+ if (existing.since_at !== sinceAt) {
172
+ throw new BackfillPolicyConflictError(`active backfill policy uses since_at=${existing.since_at}; refuse to replace with ${sinceAt}`);
173
+ }
174
+ return existing;
175
+ }
176
+ const now = (0, db_1.nowIso)();
177
+ if (Date.parse(sinceAt) > Date.parse(now)) {
178
+ throw new BackfillArgumentError("--since must not be after the frozen until_at (now)");
179
+ }
180
+ const policy = {
181
+ version: 1,
182
+ policy_id: crypto.randomUUID(),
183
+ since_at: sinceAt,
184
+ until_at: now,
185
+ status: "active",
186
+ started_at: now,
187
+ completed_at: null,
188
+ };
189
+ writePolicy(db, sourceId, policy);
190
+ return policy;
191
+ });
192
+ }
193
+ function withinWindow(policy, mtimeMs) {
194
+ if (mtimeMs === null)
195
+ return false;
196
+ const since = Date.parse(policy.since_at);
197
+ const until = Date.parse(policy.until_at);
198
+ return mtimeMs >= since && mtimeMs <= until;
199
+ }
200
+ function upsertFrontierPending(db, policyId, sourceId, session, generation, sourceFileId) {
201
+ const now = (0, db_1.nowIso)();
202
+ db.prepare(`INSERT INTO backfill_frontier
203
+ (policy_id, source_id, session_id, generation, source_file_id, mtime_ms, status, completed_at, updated_at)
204
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL, ?)
205
+ ON CONFLICT(policy_id, session_id, generation) DO UPDATE SET
206
+ mtime_ms = excluded.mtime_ms,
207
+ source_file_id = COALESCE(excluded.source_file_id, backfill_frontier.source_file_id),
208
+ updated_at = excluded.updated_at
209
+ WHERE backfill_frontier.status = 'pending'`).run(policyId, sourceId, session.session_id, generation, sourceFileId, session.mtime_ms, now);
210
+ }
211
+ function markFrontierComplete(db, policyId, sessionId, generation) {
212
+ const now = (0, db_1.nowIso)();
213
+ db.prepare(`UPDATE backfill_frontier
214
+ SET status = 'complete', completed_at = ?, updated_at = ?
215
+ WHERE policy_id = ? AND session_id = ? AND generation = ? AND status = 'pending'`).run(now, now, policyId, sessionId, generation);
216
+ }
217
+ function markFrontierExcluded(db, policyId, sessionId, generation) {
218
+ const now = (0, db_1.nowIso)();
219
+ db.prepare(`UPDATE backfill_frontier
220
+ SET status = 'excluded', completed_at = COALESCE(completed_at, ?), updated_at = ?
221
+ WHERE policy_id = ? AND session_id = ? AND generation = ? AND status = 'pending'`).run(now, now, policyId, sessionId, generation);
222
+ }
223
+ function supersedeOlderGenerations(db, policyId, sessionId, currentGeneration) {
224
+ const now = (0, db_1.nowIso)();
225
+ db.prepare(`UPDATE backfill_frontier
226
+ SET status = 'complete', completed_at = COALESCE(completed_at, ?), updated_at = ?
227
+ WHERE policy_id = ? AND session_id = ? AND generation < ? AND status = 'pending'`).run(now, now, policyId, sessionId, currentGeneration);
228
+ }
229
+ function currentGeneration(db, sourceId, sessionId) {
230
+ return (db
231
+ .prepare("SELECT id AS source_file_id, generation FROM source_files WHERE source_id = ? AND session_id = ? AND is_current = 1")
232
+ .get(sourceId, sessionId) ?? null);
233
+ }
234
+ function unfinishedFrontier(db, policyId) {
235
+ return db
236
+ .prepare(`SELECT session_id, generation, mtime_ms
237
+ FROM backfill_frontier
238
+ WHERE policy_id = ? AND status = 'pending'
239
+ ORDER BY CASE WHEN mtime_ms IS NULL THEN 1 ELSE 0 END, mtime_ms DESC, session_id ASC`)
240
+ .all(policyId);
241
+ }
242
+ function frontierCounts(db, policyId) {
243
+ const rows = db
244
+ .prepare(`SELECT status, count(*) AS c FROM backfill_frontier WHERE policy_id = ? GROUP BY status`)
245
+ .all(policyId);
246
+ const counts = { pending: 0, complete: 0, excluded: 0 };
247
+ for (const row of rows) {
248
+ if (row.status === "pending")
249
+ counts.pending = row.c;
250
+ if (row.status === "complete")
251
+ counts.complete = row.c;
252
+ if (row.status === "excluded")
253
+ counts.excluded = row.c;
254
+ }
255
+ return counts;
256
+ }
257
+ function maybeCompletePolicy(db, sourceId, policy) {
258
+ const counts = frontierCounts(db, policy.policy_id);
259
+ if (counts.pending > 0)
260
+ return policy;
261
+ const completed = { ...policy, status: "completed", completed_at: (0, db_1.nowIso)() };
262
+ writePolicy(db, sourceId, completed);
263
+ return completed;
264
+ }
265
+ function emptyScanSummary(sourceId = exports.DEFAULT_SOURCE_ID) {
266
+ return {
267
+ source_id: sourceId,
268
+ sessions_discovered: 0,
269
+ sessions_queued: 0,
270
+ sessions_excluded: 0,
271
+ sessions_skipped_unapproved: 0,
272
+ sessions_failed: 0,
273
+ generations_reset: 0,
274
+ objects_canceled: 0,
275
+ segments_enqueued: 0,
276
+ bytes_enqueued: 0,
277
+ queued: [],
278
+ processed_sessions: [],
279
+ };
280
+ }
281
+ function countActionableFrontier(db, policyId, discoveredById) {
282
+ const seen = new Set();
283
+ let count = 0;
284
+ for (const row of unfinishedFrontier(db, policyId)) {
285
+ if (seen.has(row.session_id))
286
+ continue;
287
+ seen.add(row.session_id);
288
+ if (discoveredById.has(row.session_id))
289
+ count++;
290
+ }
291
+ return count;
292
+ }
293
+ /**
294
+ * One bounded backfill slice against an already-resolved policy: refresh
295
+ * frontier, scan/queue up to the session budget (newest unfinished sessions
296
+ * first by mtime), mark completions. Bytes within a session stay forward-only.
297
+ */
298
+ function runBackfillSlice(db, policy, options) {
299
+ const adapter = options.adapter;
300
+ const sourceId = adapter.sourceId;
301
+ if (options.shouldCancel?.()) {
302
+ return {
303
+ status: "ok",
304
+ skip_reason: null,
305
+ policy,
306
+ sessions_in_window: 0,
307
+ sessions_selected: 0,
308
+ frontier_pending: frontierCounts(db, policy.policy_id).pending,
309
+ frontier_complete: frontierCounts(db, policy.policy_id).complete,
310
+ actionable_remaining: 0,
311
+ made_progress: false,
312
+ canceled: true,
313
+ scan: emptyScanSummary(sourceId),
314
+ };
315
+ }
316
+ const beforeCounts = frontierCounts(db, policy.policy_id);
317
+ const terminalBefore = beforeCounts.complete + beforeCounts.excluded;
318
+ const discovered = adapter.discoverSessions();
319
+ const roots = (0, config_1.getApprovedRoots)(db);
320
+ const discoveredById = new Map(discovered.map((session) => [session.session_id, session]));
321
+ const inWindowApproved = discovered.filter((session) => withinWindow(policy, session.mtime_ms) &&
322
+ session.cwd !== null &&
323
+ (0, config_1.isUnderApprovedRoot)(roots, session.cwd) !== null);
324
+ // Seed frontier for approved sessions currently in the frozen window.
325
+ // Persisted pending rows remain selectable even if mtime later moves outside.
326
+ (0, db_1.tx)(db, () => {
327
+ for (const session of inWindowApproved) {
328
+ const current = currentGeneration(db, sourceId, session.session_id);
329
+ const generation = current?.generation ?? 1;
330
+ if (current)
331
+ supersedeOlderGenerations(db, policy.policy_id, session.session_id, current.generation);
332
+ upsertFrontierPending(db, policy.policy_id, sourceId, session, generation, current?.source_file_id ?? null);
333
+ }
334
+ // Terminally exclude only when we can prove the root is unapproved.
335
+ // Discovery absence is not proof (transient incomplete/missing files stay pending).
336
+ // Null cwd is deny-by-default → excluded.
337
+ for (const row of unfinishedFrontier(db, policy.policy_id)) {
338
+ const session = discoveredById.get(row.session_id);
339
+ if (session) {
340
+ if (session.cwd === null || (0, config_1.isUnderApprovedRoot)(roots, session.cwd) === null) {
341
+ markFrontierExcluded(db, policy.policy_id, row.session_id, row.generation);
342
+ }
343
+ continue;
344
+ }
345
+ const persisted = db
346
+ .prepare(`SELECT sf.cwd
347
+ FROM backfill_frontier bf
348
+ JOIN source_files sf ON sf.id = bf.source_file_id
349
+ WHERE bf.policy_id = ? AND bf.session_id = ? AND bf.generation = ?`)
350
+ .get(policy.policy_id, row.session_id, row.generation);
351
+ if (persisted &&
352
+ (persisted.cwd === null || (0, config_1.isUnderApprovedRoot)(roots, persisted.cwd) === null)) {
353
+ markFrontierExcluded(db, policy.policy_id, row.session_id, row.generation);
354
+ }
355
+ }
356
+ // Supersede stale generations before selection so one session cannot consume the budget twice.
357
+ for (const row of unfinishedFrontier(db, policy.policy_id)) {
358
+ const current = currentGeneration(db, sourceId, row.session_id);
359
+ if (current)
360
+ supersedeOlderGenerations(db, policy.policy_id, row.session_id, current.generation);
361
+ }
362
+ });
363
+ if (options.shouldCancel?.()) {
364
+ const counts = frontierCounts(db, policy.policy_id);
365
+ return {
366
+ status: "ok",
367
+ skip_reason: null,
368
+ policy,
369
+ sessions_in_window: inWindowApproved.length,
370
+ sessions_selected: 0,
371
+ frontier_pending: counts.pending,
372
+ frontier_complete: counts.complete,
373
+ actionable_remaining: countActionableFrontier(db, policy.policy_id, discoveredById),
374
+ made_progress: counts.complete + counts.excluded > terminalBefore,
375
+ canceled: true,
376
+ scan: emptyScanSummary(sourceId),
377
+ };
378
+ }
379
+ options.onProgress?.();
380
+ // Select only currently discoverable pending sessions so a prefix of
381
+ // transiently missing rows cannot starve later actionable work. Missing
382
+ // rows stay pending for a future discovery refresh.
383
+ const unfinished = unfinishedFrontier(db, policy.policy_id);
384
+ const budget = sessionBudget();
385
+ const selected = [];
386
+ const seen = new Set();
387
+ for (const row of unfinished) {
388
+ if (seen.has(row.session_id))
389
+ continue;
390
+ seen.add(row.session_id);
391
+ if (!discoveredById.has(row.session_id))
392
+ continue;
393
+ selected.push(row.session_id);
394
+ if (selected.length >= budget)
395
+ break;
396
+ }
397
+ const scan = (0, scan_1.reconcile)(db, {
398
+ installationId: options.installationId,
399
+ leaseOwner: options.leaseOwner,
400
+ adapter,
401
+ mode: "backfill",
402
+ hintSessionIds: selected,
403
+ priority: "backfill",
404
+ shouldCancel: options.shouldCancel,
405
+ onProgress: options.onProgress,
406
+ });
407
+ if (options.shouldCancel?.()) {
408
+ const counts = frontierCounts(db, policy.policy_id);
409
+ return {
410
+ status: "ok",
411
+ skip_reason: null,
412
+ policy,
413
+ sessions_in_window: inWindowApproved.length,
414
+ sessions_selected: selected.length,
415
+ frontier_pending: counts.pending,
416
+ frontier_complete: counts.complete,
417
+ actionable_remaining: countActionableFrontier(db, policy.policy_id, discoveredById),
418
+ made_progress: scan.segments_enqueued > 0 || counts.complete + counts.excluded > terminalBefore,
419
+ canceled: true,
420
+ scan,
421
+ };
422
+ }
423
+ const processed = new Map(scan.processed_sessions.map((row) => [`${row.session_id}:${row.generation}`, row]));
424
+ (0, db_1.tx)(db, () => {
425
+ for (const sessionId of selected) {
426
+ const current = currentGeneration(db, sourceId, sessionId);
427
+ if (!current)
428
+ continue;
429
+ supersedeOlderGenerations(db, policy.policy_id, sessionId, current.generation);
430
+ const session = discovered.find((item) => item.session_id === sessionId);
431
+ if (session) {
432
+ upsertFrontierPending(db, policy.policy_id, sourceId, session, current.generation, current.source_file_id);
433
+ }
434
+ if (processed.has(`${sessionId}:${current.generation}`)) {
435
+ markFrontierComplete(db, policy.policy_id, sessionId, current.generation);
436
+ }
437
+ }
438
+ });
439
+ // Re-seed any still-in-window approved sessions that gained a new generation.
440
+ (0, db_1.tx)(db, () => {
441
+ for (const session of inWindowApproved) {
442
+ const current = currentGeneration(db, sourceId, session.session_id);
443
+ if (!current) {
444
+ upsertFrontierPending(db, policy.policy_id, sourceId, session, 1, null);
445
+ continue;
446
+ }
447
+ supersedeOlderGenerations(db, policy.policy_id, session.session_id, current.generation);
448
+ const frontier = db
449
+ .prepare("SELECT status FROM backfill_frontier WHERE policy_id = ? AND session_id = ? AND generation = ?")
450
+ .get(policy.policy_id, session.session_id, current.generation);
451
+ if (!frontier) {
452
+ upsertFrontierPending(db, policy.policy_id, sourceId, session, current.generation, current.source_file_id);
453
+ }
454
+ }
455
+ });
456
+ const finalPolicy = (0, db_1.tx)(db, () => maybeCompletePolicy(db, sourceId, readBackfillPolicy(db, sourceId) ?? policy));
457
+ const counts = frontierCounts(db, finalPolicy.policy_id);
458
+ const actionable = countActionableFrontier(db, finalPolicy.policy_id, discoveredById);
459
+ const madeProgress = scan.segments_enqueued > 0 || counts.complete + counts.excluded > terminalBefore;
460
+ return {
461
+ status: finalPolicy.status === "completed" ? "completed" : "ok",
462
+ skip_reason: null,
463
+ policy: finalPolicy,
464
+ sessions_in_window: inWindowApproved.length,
465
+ sessions_selected: selected.length,
466
+ frontier_pending: counts.pending,
467
+ frontier_complete: counts.complete,
468
+ actionable_remaining: actionable,
469
+ made_progress: madeProgress,
470
+ canceled: false,
471
+ scan,
472
+ };
473
+ }
474
+ /**
475
+ * CLI entry: mint/resume policy from `--since`, then run one local queue slice.
476
+ */
477
+ function runBackfillStartPass(db, options) {
478
+ const adapter = options.adapter;
479
+ const policy = ensureBackfillPolicy(db, options.since, adapter);
480
+ const slice = runBackfillSlice(db, policy, options);
481
+ return {
482
+ status: slice.status === "completed" ? "completed" : "ok",
483
+ policy: slice.policy ?? policy,
484
+ sessions_in_window: slice.sessions_in_window,
485
+ sessions_selected: slice.sessions_selected,
486
+ frontier_pending: slice.frontier_pending,
487
+ frontier_complete: slice.frontier_complete,
488
+ scan: slice.scan,
489
+ };
490
+ }
491
+ /**
492
+ * Sync-worker continuation: continue an existing active policy only.
493
+ * Never calls `ensureBackfillPolicy` / never mints a new window.
494
+ */
495
+ function runBackfillContinuePass(db, options) {
496
+ const adapter = options.adapter;
497
+ const policy = readBackfillPolicy(db, adapter.sourceId);
498
+ if (!policy || policy.status !== "active") {
499
+ return {
500
+ status: "skipped",
501
+ skip_reason: "no_active_policy",
502
+ policy: policy?.status === "completed" ? policy : null,
503
+ sessions_in_window: 0,
504
+ sessions_selected: 0,
505
+ frontier_pending: policy ? frontierCounts(db, policy.policy_id).pending : 0,
506
+ frontier_complete: policy ? frontierCounts(db, policy.policy_id).complete : 0,
507
+ actionable_remaining: 0,
508
+ made_progress: false,
509
+ canceled: false,
510
+ scan: emptyScanSummary(adapter.sourceId),
511
+ };
512
+ }
513
+ return runBackfillSlice(db, policy, { ...options, adapter });
514
+ }
515
+ function commandBackfillStart(options) {
516
+ if (!options.since) {
517
+ process.stderr.write("dreams backfill start requires --since <RFC3339>\n");
518
+ return 1;
519
+ }
520
+ const lock = (0, state_lock_1.acquireLocalStateLock)();
521
+ if (lock === null) {
522
+ if (options.json) {
523
+ console.log(JSON.stringify({ status: "busy", code: "local_state_busy", schema_version: 1 }));
524
+ }
525
+ else {
526
+ process.stderr.write("Another Dreams command is using local state. Try again after it finishes.\n");
527
+ }
528
+ return 1;
529
+ }
530
+ try {
531
+ const installation = (0, store_1.loadOrCreateInstallation)();
532
+ const db = (0, db_1.openDb)({
533
+ installationId: installation.installation_id,
534
+ installationCreatedAt: installation.created_at,
535
+ });
536
+ try {
537
+ const owner = (0, leases_1.acquireLease)(db, leases_1.SOURCE_LEASE);
538
+ if (owner === null) {
539
+ if (options.json)
540
+ console.log(JSON.stringify({ status: "busy", code: "busy", schema_version: 1 }));
541
+ else
542
+ process.stderr.write("Another dreams source operation holds the local lease.\n");
543
+ return 1;
544
+ }
545
+ let result;
546
+ try {
547
+ result = runBackfillStartPass(db, {
548
+ installationId: installation.installation_id,
549
+ leaseOwner: owner,
550
+ since: options.since,
551
+ adapter: (0, source_adapters_1.defaultSourceAdapter)(),
552
+ });
553
+ }
554
+ finally {
555
+ (0, leases_1.releaseLease)(db, leases_1.SOURCE_LEASE, owner);
556
+ }
557
+ const payload = {
558
+ schema_version: 1,
559
+ status: result.status,
560
+ policy: result.policy,
561
+ sessions_in_window: result.sessions_in_window,
562
+ sessions_selected: result.sessions_selected,
563
+ frontier_pending: result.frontier_pending,
564
+ frontier_complete: result.frontier_complete,
565
+ scan: result.scan,
566
+ };
567
+ if (options.json)
568
+ console.log(JSON.stringify(payload, null, 2));
569
+ else {
570
+ console.log([
571
+ `Backfill ${result.status} for ${result.policy.since_at} → ${result.policy.until_at}`,
572
+ "",
573
+ ` Sessions in window: ${result.sessions_in_window}`,
574
+ ` Sessions scanned: ${result.sessions_selected}`,
575
+ ` Frontier pending: ${result.frontier_pending}`,
576
+ ` Frontier complete: ${result.frontier_complete}`,
577
+ ` Segments enqueued: ${result.scan.segments_enqueued}`,
578
+ "",
579
+ "Local queue only — wake sync to upload: dreams sync --force",
580
+ ].join("\n"));
581
+ }
582
+ return 0;
583
+ }
584
+ finally {
585
+ (0, db_1.closeDb)();
586
+ }
587
+ }
588
+ catch (error) {
589
+ if (error instanceof BackfillArgumentError || error instanceof BackfillPolicyConflictError) {
590
+ if (options.json) {
591
+ console.log(JSON.stringify({
592
+ status: "error",
593
+ schema_version: 1,
594
+ code: error.code,
595
+ message: error.message,
596
+ }));
597
+ }
598
+ else {
599
+ process.stderr.write(`${error.message}\n`);
600
+ }
601
+ return 1;
602
+ }
603
+ throw error;
604
+ }
605
+ finally {
606
+ (0, state_lock_1.releaseLocalStateLock)(lock);
607
+ }
608
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /**
3
+ * Shared persistence for Cloud workspace/source bindings.
4
+ *
5
+ * Cloud is authoritative for lifecycle on register/re-register (LET-10231):
6
+ * create may set detected|active; re-register preserves lifecycle and returns
7
+ * the current status. Persist that remote status locally.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.mergeRemoteSourceStatus = mergeRemoteSourceStatus;
11
+ exports.persistSourceBindings = persistSourceBindings;
12
+ const db_1 = require("./db");
13
+ const leases_1 = require("./leases");
14
+ /** Prefer Cloud's returned lifecycle; kept as a named helper for call sites/tests. */
15
+ function mergeRemoteSourceStatus(_localStatus, remoteStatus) {
16
+ return remoteStatus;
17
+ }
18
+ function persistSourceBindings(db, localSourceId, registration, owner) {
19
+ const base = process.env.LETTA_BASE_URL || "https://api.letta.com";
20
+ return (0, db_1.tx)(db, () => {
21
+ (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
22
+ const existing = db.prepare("SELECT status FROM sources WHERE id = ?").get(localSourceId);
23
+ const status = mergeRemoteSourceStatus(existing?.status, registration.status);
24
+ db.prepare(`INSERT INTO workspace_bindings (id, api_base_url, workspace_id, organization_id, user_id, created_at, updated_at)
25
+ VALUES (1, ?, ?, NULL, NULL, ?, ?)
26
+ ON CONFLICT(id) DO UPDATE SET api_base_url = excluded.api_base_url, workspace_id = excluded.workspace_id, updated_at = excluded.updated_at`).run(base, registration.workspaceId, (0, db_1.nowIso)(), (0, db_1.nowIso)());
27
+ db.prepare("UPDATE sources SET server_source_id = ?, status = ?, updated_at = ? WHERE id = ?").run(registration.serverSourceId, status, (0, db_1.nowIso)(), localSourceId);
28
+ return { status, serverSourceId: registration.serverSourceId, workspaceId: registration.workspaceId };
29
+ });
30
+ }