@letta-ai/dreams 0.0.1 → 0.0.4

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.
@@ -54,15 +54,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
54
54
  exports.runUpload = runUpload;
55
55
  const path = __importStar(require("node:path"));
56
56
  const index_1 = require("./../auth/index");
57
- const claude_1 = require("./../local/claude");
57
+ const bindings_1 = require("./../local/bindings");
58
58
  const config_1 = require("./../local/config");
59
59
  const db_1 = require("./../local/db");
60
60
  const dreamignore_1 = require("./../local/dreamignore");
61
61
  const leases_1 = require("./../local/leases");
62
62
  const scan_1 = require("./../local/scan");
63
63
  const blobs_1 = require("./../local/blobs");
64
+ const skill_1 = require("./../skill");
64
65
  const version_1 = require("./../version");
65
66
  const client_1 = require("./client");
67
+ /** Upload schemas stay in the upload layer — not on SourceAdapter ([LET-10314]). */
68
+ const CLAUDE_SOURCE_SCHEMA = "claude-code-jsonl-v1";
69
+ const CODEX_SOURCE_SCHEMA = "codex-jsonl-v1";
66
70
  /**
67
71
  * Final local policy gate: ONE fresh roots/.dreamignore snapshot drives both the
68
72
  * eligibility decision and the fingerprint, so a tightening cannot be recorded
@@ -77,11 +81,28 @@ function evaluatePolicy(db, cwd) {
77
81
  const eligible = !(0, dreamignore_1.isIgnored)((0, dreamignore_1.parseDreamignore)(ignoreText), path.relative(approvedRoot, cwd), true);
78
82
  return { eligible, approvedRoot, fingerprint: (0, scan_1.policyFingerprint)(roots, approvedRoot, ignoreText) };
79
83
  }
80
- const SOURCE_SCHEMA = "claude-code-jsonl-v1";
81
- const UPLOAD_CAPABLE = new Set(["active", "backfilling", "discovered", "awaiting_approval"]);
84
+ /** Must match Cloud upload-capable statuses (LET-10231). */
85
+ const UPLOAD_CAPABLE = new Set(["active", "backfilling", "detected"]);
82
86
  const MAX_BACKOFF_MS = 5 * 60_000;
83
- function localSourceId() {
84
- return `src_${claude_1.CLAUDE_SOURCE_TYPE}`;
87
+ function sourceRegistrationInput(installationId, adapter) {
88
+ // Omit status so Cloud preserves lifecycle on re-register (create defaults to active).
89
+ return {
90
+ sourceType: adapter.sourceType,
91
+ sourceKey: adapter.sourceKey,
92
+ displayName: adapter.displayName,
93
+ installationId,
94
+ adapterVersion: adapter.adapterVersion,
95
+ cliVersion: (0, version_1.packageVersion)(),
96
+ skillVersion: (0, skill_1.installedSkillVersion)(),
97
+ platform: process.platform,
98
+ };
99
+ }
100
+ function sourceSchemaFor(adapter) {
101
+ if (adapter.sourceType === "claude-code")
102
+ return CLAUDE_SOURCE_SCHEMA;
103
+ if (adapter.sourceType === "codex")
104
+ return CODEX_SOURCE_SCHEMA;
105
+ throw new Error(`no upload schema registered for source type: ${adapter.sourceType}`);
85
106
  }
86
107
  function dueRows(db, localSource) {
87
108
  return db
@@ -91,7 +112,14 @@ function dueRows(db, localSource) {
91
112
  WHERE q.source_id = ? AND q.state = 'pending'
92
113
  AND (q.next_attempt_at IS NULL OR q.next_attempt_at <= ?)
93
114
  ORDER BY CASE q.priority WHEN 'live' THEN 0 WHEN 'backfill' THEN 1 ELSE 2 END,
94
- q.session_id, q.generation, q.start_offset`)
115
+ CASE
116
+ WHEN q.priority = 'backfill' AND sf.mtime_ms IS NULL THEN 1
117
+ ELSE 0
118
+ END,
119
+ CASE WHEN q.priority = 'backfill' THEN sf.mtime_ms END DESC,
120
+ q.session_id ASC,
121
+ q.generation ASC,
122
+ q.start_offset ASC`)
95
123
  .all(localSource, (0, db_1.nowIso)());
96
124
  }
97
125
  function backoffMs(attempts, retryAfterMs) {
@@ -186,7 +214,8 @@ function refreshPolicyFingerprint(db, row, fingerprint, owner) {
186
214
  }
187
215
  async function runUpload(db, options) {
188
216
  const owner = options.leaseOwner;
189
- const local = localSourceId();
217
+ const adapter = options.adapter;
218
+ const local = adapter.sourceId;
190
219
  const summary = {
191
220
  local_source_id: null,
192
221
  server_source_id: null,
@@ -199,68 +228,86 @@ async function runUpload(db, options) {
199
228
  retry_scheduled: 0,
200
229
  canceled_by_policy: 0,
201
230
  bytes_uploaded: 0,
231
+ rows_processed: 0,
232
+ budget_exhausted: false,
202
233
  stopped_reason: null,
234
+ stopped_code: null,
203
235
  partial: false,
204
236
  };
237
+ const maxRows = options.maxRows;
205
238
  const sourceExists = db.prepare("SELECT id FROM sources WHERE id = ?").get(local);
206
239
  if (!sourceExists)
207
240
  return summary; // nothing scanned yet
208
241
  summary.local_source_id = local;
209
- let due = dueRows(db, local);
242
+ const due = dueRows(db, local);
210
243
  summary.due = due.length;
211
244
  if (due.length === 0)
212
245
  return summary; // no eligible pending rows => no auth, no network
213
- // Register the source fresh (server-idempotent). Per [02] this also provisions
214
- // and returns the workspace, so there is no separate workspace call. Registering
215
- // per invocation, bound to the current base URL, avoids reusing a prior env's IDs.
216
- let registration;
217
- try {
246
+ // Single budgeted loop: local size/blob preflight first; lazy-register on the
247
+ // first sendable row so oversized-only passes make zero Cloud requests.
248
+ let registration = null;
249
+ let bindings = null;
250
+ let reRegistered = false;
251
+ let acceptedAny = false;
252
+ async function ensureRegistered() {
253
+ if (registration && bindings)
254
+ return true;
255
+ options.onProgress?.();
218
256
  if (!(0, leases_1.renewLease)(db, leases_1.SOURCE_LEASE, owner))
219
257
  throw new leases_1.LeaseLostError(leases_1.SOURCE_LEASE);
220
- registration = await (0, client_1.registerSource)({
221
- sourceType: claude_1.CLAUDE_SOURCE_TYPE,
222
- sourceKey: claude_1.CLAUDE_SOURCE_TYPE,
223
- displayName: "Claude Code",
224
- installationId: options.installationId,
225
- adapterVersion: claude_1.CLAUDE_ADAPTER_VERSION,
226
- });
227
- }
228
- catch (error) {
229
- if (error instanceof index_1.NotAuthenticatedError) {
230
- summary.stopped_reason = "not authenticated: run `dreams auth login`";
231
- summary.partial = true;
232
- return summary;
258
+ try {
259
+ registration = await (0, client_1.registerSource)(sourceRegistrationInput(options.installationId, adapter));
233
260
  }
234
- if (error instanceof client_1.CloudAuthError) {
235
- summary.stopped_reason = `authentication failed: ${error.message}`;
236
- summary.partial = true;
237
- return summary;
261
+ catch (error) {
262
+ if (error instanceof index_1.NotAuthenticatedError) {
263
+ summary.stopped_reason = "not authenticated: run `dreams auth login`";
264
+ summary.stopped_code = "needs_auth";
265
+ summary.partial = true;
266
+ return false;
267
+ }
268
+ if (error instanceof client_1.CloudAuthError) {
269
+ summary.stopped_reason = `authentication failed: ${error.message}`;
270
+ summary.stopped_code = "needs_auth";
271
+ summary.partial = true;
272
+ return false;
273
+ }
274
+ if (error instanceof client_1.CloudError) {
275
+ summary.stopped_reason = `could not resolve source: ${error.message}`;
276
+ summary.stopped_code = error.retryable ? "network" : "cloud_rejected";
277
+ summary.partial = true;
278
+ return false;
279
+ }
280
+ throw error;
238
281
  }
239
- if (error instanceof client_1.CloudError) {
240
- summary.stopped_reason = `could not resolve source: ${error.message}`;
282
+ bindings = (0, bindings_1.persistSourceBindings)(db, local, registration, owner);
283
+ summary.server_source_id = bindings.serverSourceId;
284
+ summary.source_status = bindings.status;
285
+ summary.workspace_id = bindings.workspaceId;
286
+ if (!UPLOAD_CAPABLE.has(bindings.status)) {
287
+ summary.stopped_reason = `source is ${bindings.status} (not upload-capable)`;
288
+ summary.stopped_code = "cloud_rejected";
241
289
  summary.partial = true;
242
- return summary;
290
+ return false;
243
291
  }
244
- throw error;
292
+ return true;
245
293
  }
246
- summary.server_source_id = registration.serverSourceId;
247
- summary.source_status = registration.status;
248
- summary.workspace_id = registration.workspaceId;
249
- persistBindings(db, registration, owner);
250
- if (!UPLOAD_CAPABLE.has(registration.status)) {
251
- summary.stopped_reason = `source is ${registration.status} (not upload-capable)`;
252
- summary.partial = true;
253
- return summary;
254
- }
255
- let reRegistered = false;
256
294
  for (let index = 0; index < due.length; index++) {
295
+ if (options.shouldCancel?.()) {
296
+ summary.partial = true;
297
+ break;
298
+ }
299
+ if (maxRows !== undefined && summary.rows_processed >= maxRows) {
300
+ summary.budget_exhausted = true;
301
+ break;
302
+ }
257
303
  const row = due[index];
304
+ options.onProgress?.();
258
305
  if (!(0, leases_1.renewLease)(db, leases_1.SOURCE_LEASE, owner))
259
306
  throw new leases_1.LeaseLostError(leases_1.SOURCE_LEASE);
260
- // Prepare the payload first.
307
+ summary.rows_processed++;
261
308
  let bytes;
262
309
  try {
263
- bytes = (0, blobs_1.readBlob)(row.content_sha256); // verifies the hash
310
+ bytes = (0, blobs_1.readBlob)(row.content_sha256);
264
311
  }
265
312
  catch (error) {
266
313
  const code = error.code === "ENOENT" ? "blob_missing" : "blob_corrupt";
@@ -269,6 +316,13 @@ async function runUpload(db, options) {
269
316
  summary.partial = true;
270
317
  continue;
271
318
  }
319
+ const encoded = (0, client_1.encodeUploadContent)(bytes);
320
+ if (!encoded.ok) {
321
+ quarantineRow(db, row, encoded.code, encoded.message, owner);
322
+ summary.quarantined++;
323
+ summary.partial = true;
324
+ continue;
325
+ }
272
326
  // Final local policy gate (one snapshot for eligibility AND fingerprint),
273
327
  // immediately before the send — the policy linearization point.
274
328
  const policy = evaluatePolicy(db, row.cwd);
@@ -279,7 +333,14 @@ async function runUpload(db, options) {
279
333
  }
280
334
  if (policy.fingerprint)
281
335
  refreshPolicyFingerprint(db, row, policy.fingerprint, owner);
336
+ if (options.shouldCancel?.()) {
337
+ summary.partial = true;
338
+ break;
339
+ }
340
+ if (!(await ensureRegistered()))
341
+ return summary;
282
342
  // Renew immediately before the network attempt so the request cannot outlive the lease.
343
+ options.onProgress?.();
283
344
  if (!(0, leases_1.renewLease)(db, leases_1.SOURCE_LEASE, owner))
284
345
  throw new leases_1.LeaseLostError(leases_1.SOURCE_LEASE);
285
346
  let outcome;
@@ -289,21 +350,24 @@ async function runUpload(db, options) {
289
350
  clientUploadId: row.upload_id,
290
351
  contentSha256: row.content_sha256,
291
352
  bytes,
292
- sourceSchema: SOURCE_SCHEMA,
353
+ encodedContent: encoded.content,
354
+ sourceSchema: sourceSchemaFor(adapter),
293
355
  sourceMetadata: { session_id: row.session_id, generation: row.generation, start_offset: row.start_offset, end_offset: row.end_offset },
294
356
  priority: row.priority,
295
357
  cliVersion: (0, version_1.packageVersion)(),
296
- adapterVersion: claude_1.CLAUDE_ADAPTER_VERSION,
358
+ adapterVersion: adapter.adapterVersion,
297
359
  });
298
360
  }
299
361
  catch (error) {
300
362
  if (error instanceof index_1.NotAuthenticatedError) {
301
363
  summary.stopped_reason = "not authenticated: run `dreams auth login`";
364
+ summary.stopped_code = "needs_auth";
302
365
  summary.partial = true;
303
366
  return summary;
304
367
  }
305
368
  if (error instanceof client_1.CloudAuthError) {
306
369
  summary.stopped_reason = `authentication failed: ${error.message}`;
370
+ summary.stopped_code = "needs_auth";
307
371
  summary.partial = true;
308
372
  return summary;
309
373
  }
@@ -319,22 +383,27 @@ async function runUpload(db, options) {
319
383
  markAccepted(db, row, outcome.uploadId, outcome.acceptedAt, owner);
320
384
  summary.accepted++;
321
385
  summary.bytes_uploaded += bytes.length;
386
+ acceptedAny = true;
322
387
  break;
323
388
  case "duplicate":
324
389
  markAccepted(db, row, outcome.uploadId, outcome.acceptedAt, owner);
325
390
  summary.duplicate++;
391
+ acceptedAny = true;
326
392
  break;
327
393
  case "unauthenticated":
328
394
  summary.stopped_reason = `authentication failed: ${outcome.message}`;
395
+ summary.stopped_code = "needs_auth";
329
396
  summary.partial = true;
330
397
  return summary;
331
398
  case "source_unusable":
332
399
  summary.stopped_reason = `source not upload-capable: ${outcome.message}`;
400
+ summary.stopped_code = "cloud_rejected";
333
401
  summary.partial = true;
334
402
  return summary;
335
403
  case "not_found":
336
404
  if (reRegistered) {
337
405
  summary.stopped_reason = `source not found after re-registration: ${outcome.message}`;
406
+ summary.stopped_code = "cloud_rejected";
338
407
  summary.partial = true;
339
408
  return summary;
340
409
  }
@@ -344,20 +413,15 @@ async function runUpload(db, options) {
344
413
  // cannot exceed the lease TTL before persistBindings commits (P2).
345
414
  if (!(0, leases_1.renewLease)(db, leases_1.SOURCE_LEASE, owner))
346
415
  throw new leases_1.LeaseLostError(leases_1.SOURCE_LEASE);
347
- const again = await (0, client_1.registerSource)({
348
- sourceType: claude_1.CLAUDE_SOURCE_TYPE,
349
- sourceKey: claude_1.CLAUDE_SOURCE_TYPE,
350
- displayName: "Claude Code",
351
- installationId: options.installationId,
352
- adapterVersion: claude_1.CLAUDE_ADAPTER_VERSION,
353
- });
354
- summary.server_source_id = again.serverSourceId;
355
- summary.source_status = again.status;
356
- summary.workspace_id = again.workspaceId;
357
- persistBindings(db, again, owner);
358
- registration.serverSourceId = again.serverSourceId;
416
+ const again = await (0, client_1.registerSource)(sourceRegistrationInput(options.installationId, adapter));
417
+ bindings = (0, bindings_1.persistSourceBindings)(db, local, again, owner);
418
+ summary.server_source_id = bindings.serverSourceId;
419
+ summary.source_status = bindings.status;
420
+ summary.workspace_id = bindings.workspaceId;
421
+ registration.serverSourceId = bindings.serverSourceId;
359
422
  }
360
423
  index--; // retry the same row once with the refreshed source id
424
+ summary.rows_processed--; // retry does not consume an extra budget slot
361
425
  break;
362
426
  case "quarantine":
363
427
  quarantineRow(db, row, outcome.code, outcome.message, owner);
@@ -370,15 +434,31 @@ async function runUpload(db, options) {
370
434
  break;
371
435
  }
372
436
  }
437
+ if (maxRows !== undefined &&
438
+ summary.rows_processed >= maxRows &&
439
+ due.length > summary.rows_processed) {
440
+ summary.budget_exhausted = true;
441
+ }
442
+ // Cloud promotes detected→active only after durable acceptance. Upload acks do
443
+ // not echo source status, so refresh via re-register when we may have flipped.
444
+ if (acceptedAny && summary.source_status === "detected" && registration) {
445
+ try {
446
+ // Heartbeat before the second consecutive network call so a long upload
447
+ // pass cannot let SYNC_WORKER_LEASE expire across register+refresh.
448
+ options.onProgress?.();
449
+ if (!(0, leases_1.renewLease)(db, leases_1.SOURCE_LEASE, owner))
450
+ throw new leases_1.LeaseLostError(leases_1.SOURCE_LEASE);
451
+ const refreshed = await (0, client_1.registerSource)(sourceRegistrationInput(options.installationId, adapter));
452
+ options.onProgress?.();
453
+ bindings = (0, bindings_1.persistSourceBindings)(db, local, refreshed, owner);
454
+ summary.server_source_id = bindings.serverSourceId;
455
+ summary.source_status = bindings.status;
456
+ summary.workspace_id = bindings.workspaceId;
457
+ }
458
+ catch {
459
+ // Best-effort refresh; accepted uploads already committed. Next detect/upload
460
+ // will reconcile authoritative Cloud status.
461
+ }
462
+ }
373
463
  return summary;
374
464
  }
375
- function persistBindings(db, registration, owner) {
376
- const base = process.env.LETTA_BASE_URL || "https://api.letta.com";
377
- (0, db_1.tx)(db, () => {
378
- (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
379
- db.prepare(`INSERT INTO workspace_bindings (id, api_base_url, workspace_id, organization_id, user_id, created_at, updated_at)
380
- VALUES (1, ?, ?, NULL, NULL, ?, ?)
381
- 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)());
382
- db.prepare("UPDATE sources SET server_source_id = ?, status = ?, updated_at = ? WHERE id = ?").run(registration.serverSourceId, registration.status, (0, db_1.nowIso)(), localSourceId());
383
- });
384
- }