@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.
@@ -10,13 +10,17 @@ exports.commandSourceScan = commandSourceScan;
10
10
  exports.commandUpload = commandUpload;
11
11
  exports.commandSourceApproveRoot = commandSourceApproveRoot;
12
12
  exports.commandSourceList = commandSourceList;
13
+ exports.commandSourceDetect = commandSourceDetect;
13
14
  exports.commandSource = commandSource;
14
15
  const store_1 = require("../store");
16
+ const upload_1 = require("../cloud/upload");
15
17
  const config_1 = require("./config");
16
18
  const db_1 = require("./db");
19
+ const detect_1 = require("./detect");
17
20
  const leases_1 = require("./leases");
18
21
  const scan_1 = require("./scan");
19
- const upload_1 = require("../cloud/upload");
22
+ const source_adapters_1 = require("./source-adapters");
23
+ const state_lock_1 = require("./state-lock");
20
24
  function open() {
21
25
  const installation = (0, store_1.loadOrCreateInstallation)();
22
26
  const db = (0, db_1.openDb)({
@@ -31,7 +35,7 @@ function emit(json, payload, human) {
31
35
  else
32
36
  console.log(human());
33
37
  }
34
- /** Optional override for the per-segment byte cap (advanced/testing); ignored if not a positive integer. */
38
+ /** Optional override for the soft segment packing target (advanced/testing). */
35
39
  function maxSegmentBytesOverride() {
36
40
  const raw = process.env.DREAMS_MAX_SEGMENT_BYTES;
37
41
  if (!raw)
@@ -39,7 +43,15 @@ function maxSegmentBytesOverride() {
39
43
  const value = Number(raw);
40
44
  return Number.isSafeInteger(value) && value > 0 ? value : undefined;
41
45
  }
42
- function commandSourceScan(json) {
46
+ /** Optional override for the hard per-record ceiling (advanced/testing). */
47
+ function maxRecordBytesOverride() {
48
+ const raw = process.env.DREAMS_MAX_RECORD_BYTES;
49
+ if (!raw)
50
+ return undefined;
51
+ const value = Number(raw);
52
+ return Number.isSafeInteger(value) && value > 0 ? value : undefined;
53
+ }
54
+ function commandSourceScanUnlocked(json) {
43
55
  const { db, installationId } = open();
44
56
  try {
45
57
  const owner = (0, leases_1.acquireLease)(db, leases_1.SOURCE_LEASE);
@@ -49,7 +61,13 @@ function commandSourceScan(json) {
49
61
  }
50
62
  let summary;
51
63
  try {
52
- summary = (0, scan_1.reconcile)(db, { installationId, leaseOwner: owner, maxSegmentBytes: maxSegmentBytesOverride() });
64
+ summary = (0, scan_1.reconcile)(db, {
65
+ installationId,
66
+ leaseOwner: owner,
67
+ adapter: (0, source_adapters_1.defaultSourceAdapter)(),
68
+ maxSegmentBytes: maxSegmentBytesOverride(),
69
+ maxRecordBytes: maxRecordBytesOverride(),
70
+ });
53
71
  }
54
72
  finally {
55
73
  (0, leases_1.releaseLease)(db, leases_1.SOURCE_LEASE, owner);
@@ -72,7 +90,7 @@ function commandSourceScan(json) {
72
90
  (0, db_1.closeDb)();
73
91
  }
74
92
  }
75
- async function commandUpload(json) {
93
+ async function commandUploadUnlocked(json) {
76
94
  const { db, installationId } = open();
77
95
  try {
78
96
  const owner = (0, leases_1.acquireLease)(db, leases_1.SOURCE_LEASE);
@@ -82,7 +100,11 @@ async function commandUpload(json) {
82
100
  }
83
101
  let summary;
84
102
  try {
85
- summary = await (0, upload_1.runUpload)(db, { installationId, leaseOwner: owner });
103
+ summary = await (0, upload_1.runUpload)(db, {
104
+ installationId,
105
+ leaseOwner: owner,
106
+ adapter: (0, source_adapters_1.defaultSourceAdapter)(),
107
+ });
86
108
  }
87
109
  finally {
88
110
  (0, leases_1.releaseLease)(db, leases_1.SOURCE_LEASE, owner);
@@ -108,7 +130,7 @@ async function commandUpload(json) {
108
130
  (0, db_1.closeDb)();
109
131
  }
110
132
  }
111
- function commandSourceApproveRoot(json, root) {
133
+ function commandSourceApproveRootUnlocked(json, root) {
112
134
  if (!root) {
113
135
  process.stderr.write("Usage: dreams source approve-root <path>\n");
114
136
  return 1;
@@ -123,12 +145,12 @@ function commandSourceApproveRoot(json, root) {
123
145
  (0, db_1.closeDb)();
124
146
  }
125
147
  }
126
- function commandSourceList(json) {
148
+ function commandSourceListUnlocked(json) {
127
149
  const { db } = open();
128
150
  try {
129
151
  const roots = (0, config_1.getApprovedRoots)(db);
130
152
  const sources = db
131
- .prepare("SELECT id, source_type, source_key, display_name, status FROM sources ORDER BY id")
153
+ .prepare("SELECT id, source_type, source_key, display_name, status, server_source_id FROM sources ORDER BY id")
132
154
  .all();
133
155
  const queue = (0, scan_1.listPendingQueue)(db);
134
156
  emit(json, { status: "ok", db_path: (0, db_1.dbFilePath)(), approved_roots: roots, sources, pending_uploads: queue }, () => [
@@ -145,8 +167,105 @@ function commandSourceList(json) {
145
167
  (0, db_1.closeDb)();
146
168
  }
147
169
  }
170
+ async function commandSourceDetectUnlocked(json) {
171
+ const { db, installationId } = open();
172
+ try {
173
+ const owner = (0, leases_1.acquireLease)(db, leases_1.SOURCE_LEASE);
174
+ if (owner === null) {
175
+ emit(json, { status: "busy", reason: "another source operation holds the local lease" }, () => "Another dreams source operation is in progress; skipping.");
176
+ return 0;
177
+ }
178
+ let result;
179
+ try {
180
+ result = await (0, detect_1.runSourceDetect)(db, {
181
+ installationId,
182
+ leaseOwner: owner,
183
+ adapter: (0, source_adapters_1.defaultSourceAdapter)(),
184
+ });
185
+ }
186
+ finally {
187
+ (0, leases_1.releaseLease)(db, leases_1.SOURCE_LEASE, owner);
188
+ }
189
+ emit(json, result, () => [
190
+ result.status === "ok" ? "Source detection complete" : "Source detection failed",
191
+ ``,
192
+ ` Harness: ${result.harness}${result.harness_present ? "" : " (not found)"}`,
193
+ ` Local source: ${result.local_source_id ?? "(none)"}`,
194
+ ` Server source: ${result.server_source_id ?? "(none)"}`,
195
+ ` Workspace: ${result.workspace_id ?? "(none)"}`,
196
+ ` Source status: ${result.source_status ?? "(none)"}`,
197
+ ` Registration: ${result.registration}`,
198
+ result.message ? ` Message: ${result.message}` : ``,
199
+ ].filter((line) => line !== ``).join("\n"));
200
+ return result.status === "ok" ? 0 : 1;
201
+ }
202
+ finally {
203
+ (0, db_1.closeDb)();
204
+ }
205
+ }
206
+ function stateBusy(json) {
207
+ emit(json, { status: "busy", reason: "another Dreams command holds the local state lock" }, () => "Another Dreams command is using local state; skipping.");
208
+ return 0;
209
+ }
210
+ function commandSourceScan(json) {
211
+ const lock = (0, state_lock_1.acquireLocalStateLock)();
212
+ if (lock === null)
213
+ return stateBusy(json);
214
+ try {
215
+ return commandSourceScanUnlocked(json);
216
+ }
217
+ finally {
218
+ (0, state_lock_1.releaseLocalStateLock)(lock);
219
+ }
220
+ }
221
+ async function commandUpload(json) {
222
+ const lock = (0, state_lock_1.acquireLocalStateLock)();
223
+ if (lock === null)
224
+ return stateBusy(json);
225
+ try {
226
+ return await commandUploadUnlocked(json);
227
+ }
228
+ finally {
229
+ (0, state_lock_1.releaseLocalStateLock)(lock);
230
+ }
231
+ }
232
+ function commandSourceApproveRoot(json, root) {
233
+ const lock = (0, state_lock_1.acquireLocalStateLock)();
234
+ if (lock === null)
235
+ return stateBusy(json);
236
+ try {
237
+ return commandSourceApproveRootUnlocked(json, root);
238
+ }
239
+ finally {
240
+ (0, state_lock_1.releaseLocalStateLock)(lock);
241
+ }
242
+ }
243
+ function commandSourceList(json) {
244
+ const lock = (0, state_lock_1.acquireLocalStateLock)();
245
+ if (lock === null)
246
+ return stateBusy(json);
247
+ try {
248
+ return commandSourceListUnlocked(json);
249
+ }
250
+ finally {
251
+ (0, state_lock_1.releaseLocalStateLock)(lock);
252
+ }
253
+ }
254
+ async function commandSourceDetect(json) {
255
+ const lock = (0, state_lock_1.acquireLocalStateLock)();
256
+ if (lock === null)
257
+ return stateBusy(json);
258
+ try {
259
+ return await commandSourceDetectUnlocked(json);
260
+ }
261
+ finally {
262
+ (0, state_lock_1.releaseLocalStateLock)(lock);
263
+ }
264
+ }
148
265
  function commandSource(subcommand, positional, json) {
149
266
  switch (subcommand) {
267
+ case "detect":
268
+ return commandSourceDetect(json);
150
269
  case "scan":
151
270
  return commandSourceScan(json);
152
271
  case "approve-root":
@@ -154,7 +273,7 @@ function commandSource(subcommand, positional, json) {
154
273
  case "list":
155
274
  return commandSourceList(json);
156
275
  default:
157
- process.stderr.write(`Unknown source subcommand: ${subcommand ?? "(none)"} — expected scan, approve-root, or list\n`);
276
+ process.stderr.write(`Unknown source subcommand: ${subcommand ?? "(none)"} — expected detect, scan, approve-root, or list\n`);
158
277
  return 1;
159
278
  }
160
279
  }
package/dist/local/db.js CHANGED
@@ -188,6 +188,70 @@ const MIGRATIONS = [
188
188
  // Due-row selection: pending rows for a source, ordered by priority then time.
189
189
  `CREATE INDEX idx_upload_queue_due ON upload_queue (source_id, state, priority, next_attempt_at)`,
190
190
  ],
191
+ [
192
+ // Hook-safe sync worker ([LET-10237]): durable trigger coalescing + runtime status.
193
+ `CREATE TABLE sync_triggers (
194
+ source_id TEXT PRIMARY KEY REFERENCES sources(id) ON DELETE CASCADE,
195
+ requested_gen INTEGER NOT NULL DEFAULT 0,
196
+ processed_gen INTEGER NOT NULL DEFAULT 0,
197
+ forced INTEGER NOT NULL DEFAULT 0,
198
+ session_hints TEXT,
199
+ updated_at TEXT NOT NULL
200
+ )`,
201
+ `CREATE TABLE sync_runtime (
202
+ id INTEGER PRIMARY KEY CHECK (id = 1),
203
+ paused INTEGER NOT NULL DEFAULT 0,
204
+ state TEXT NOT NULL DEFAULT 'idle',
205
+ last_attempt_at TEXT,
206
+ last_success_at TEXT,
207
+ last_error_at TEXT,
208
+ last_error_code TEXT,
209
+ last_error_message TEXT,
210
+ next_due_at TEXT,
211
+ updated_at TEXT NOT NULL
212
+ )`,
213
+ `INSERT INTO sync_runtime (id, paused, state, updated_at) VALUES (1, 0, 'idle', datetime('now'))`,
214
+ ],
215
+ [
216
+ // Backfill session frontier ([LET-10239]): per-generation completion for an
217
+ // approved [since_at, until_at] window. Mtime orders unfinished sessions only.
218
+ // Rows are scoped by policy_id so a later window cannot inherit stale completes.
219
+ `CREATE TABLE backfill_frontier (
220
+ policy_id TEXT NOT NULL,
221
+ source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
222
+ session_id TEXT NOT NULL,
223
+ generation INTEGER NOT NULL,
224
+ source_file_id INTEGER REFERENCES source_files(id) ON DELETE SET NULL,
225
+ mtime_ms INTEGER,
226
+ status TEXT NOT NULL DEFAULT 'pending',
227
+ completed_at TEXT,
228
+ updated_at TEXT NOT NULL,
229
+ PRIMARY KEY (policy_id, session_id, generation)
230
+ )`,
231
+ `CREATE INDEX idx_backfill_frontier_unfinished
232
+ ON backfill_frontier (policy_id, status, mtime_ms)`,
233
+ ],
234
+ [
235
+ // Per-source sync schedule ([LET-10314]): cadence/retry isolation across adapters.
236
+ // Global pause/running remain on sync_runtime; due state lives on sync_triggers.
237
+ `ALTER TABLE sync_triggers ADD COLUMN state TEXT NOT NULL DEFAULT 'idle'`,
238
+ `ALTER TABLE sync_triggers ADD COLUMN last_attempt_at TEXT`,
239
+ `ALTER TABLE sync_triggers ADD COLUMN last_success_at TEXT`,
240
+ `ALTER TABLE sync_triggers ADD COLUMN last_error_at TEXT`,
241
+ `ALTER TABLE sync_triggers ADD COLUMN last_error_code TEXT`,
242
+ `ALTER TABLE sync_triggers ADD COLUMN last_error_message TEXT`,
243
+ `ALTER TABLE sync_triggers ADD COLUMN next_due_at TEXT`,
244
+ // Preserve any pre-migration global cadence onto the default Claude trigger row.
245
+ `UPDATE sync_triggers
246
+ SET next_due_at = (SELECT next_due_at FROM sync_runtime WHERE id = 1),
247
+ last_attempt_at = (SELECT last_attempt_at FROM sync_runtime WHERE id = 1),
248
+ last_success_at = (SELECT last_success_at FROM sync_runtime WHERE id = 1),
249
+ last_error_at = (SELECT last_error_at FROM sync_runtime WHERE id = 1),
250
+ last_error_code = (SELECT last_error_code FROM sync_runtime WHERE id = 1),
251
+ last_error_message = (SELECT last_error_message FROM sync_runtime WHERE id = 1),
252
+ state = COALESCE((SELECT state FROM sync_runtime WHERE id = 1), 'idle')
253
+ WHERE source_id = 'src_claude-code'`,
254
+ ],
191
255
  ];
192
256
  exports.SCHEMA_VERSION = MIGRATIONS.length;
193
257
  function tx(db, fn) {
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ /**
3
+ * `dreams source detect` — register a harness as a detected Cloud source
4
+ * without reading transcripts, approving roots, scanning, or uploading.
5
+ *
6
+ * Today CLI defaults to the Claude adapter ([LET-10313]).
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.detectClaudeCodeHarness = detectClaudeCodeHarness;
10
+ exports.localClaudeSourceId = localClaudeSourceId;
11
+ exports.ensureLocalDetectedSource = ensureLocalDetectedSource;
12
+ exports.runSourceDetect = runSourceDetect;
13
+ const index_1 = require("../auth/index");
14
+ const client_1 = require("../cloud/client");
15
+ const version_1 = require("../version");
16
+ const skill_1 = require("../skill");
17
+ const bindings_1 = require("./bindings");
18
+ const claude_1 = require("./claude");
19
+ const db_1 = require("./db");
20
+ const leases_1 = require("./leases");
21
+ /**
22
+ * Deterministic harness-owned filesystem facts only. Never opens transcript
23
+ * files or walks session contents.
24
+ */
25
+ function detectClaudeCodeHarness(home) {
26
+ const result = (0, claude_1.detectClaudeCodeHarness)(home);
27
+ return { present: result.present, harness: claude_1.claudeSourceAdapter.sourceType, evidence: result.evidence };
28
+ }
29
+ function localClaudeSourceId() {
30
+ return claude_1.claudeSourceAdapter.sourceId;
31
+ }
32
+ /** Ensure the local sources row exists without touching queues or files. */
33
+ function ensureLocalDetectedSource(db, owner, adapter) {
34
+ const id = adapter.sourceId;
35
+ return (0, db_1.tx)(db, () => {
36
+ (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
37
+ const existing = db.prepare("SELECT id FROM sources WHERE id = ?").get(id);
38
+ if (!existing) {
39
+ const now = (0, db_1.nowIso)();
40
+ db.prepare(`INSERT INTO sources (id, source_type, source_key, display_name, adapter_version, status, created_at, updated_at)
41
+ VALUES (?, ?, ?, ?, ?, 'detected', ?, ?)`).run(id, adapter.sourceType, adapter.sourceKey, adapter.displayName, adapter.adapterVersion, now, now);
42
+ }
43
+ return id;
44
+ });
45
+ }
46
+ async function runSourceDetect(db, options) {
47
+ const adapter = options.adapter;
48
+ const detection = adapter.detect();
49
+ if (!detection.present) {
50
+ return {
51
+ status: "error",
52
+ harness: adapter.sourceType,
53
+ harness_present: false,
54
+ evidence: [],
55
+ local_source_id: null,
56
+ server_source_id: null,
57
+ workspace_id: null,
58
+ source_status: null,
59
+ registration: "skipped",
60
+ message: adapter.sourceType === claude_1.claudeSourceAdapter.sourceType
61
+ ? "Claude Code was not detected under ~/.claude"
62
+ : `${adapter.displayName} was not detected`,
63
+ };
64
+ }
65
+ const localId = ensureLocalDetectedSource(db, options.leaseOwner, adapter);
66
+ const before = db.prepare("SELECT server_source_id, status FROM sources WHERE id = ?").get(localId);
67
+ let registration;
68
+ try {
69
+ registration = await (0, client_1.registerSource)({
70
+ sourceType: adapter.sourceType,
71
+ sourceKey: adapter.sourceKey,
72
+ displayName: adapter.displayName,
73
+ installationId: options.installationId,
74
+ adapterVersion: adapter.adapterVersion,
75
+ // Required for detection-only create: Cloud defaults omitted status to active.
76
+ status: "detected",
77
+ cliVersion: (0, version_1.packageVersion)(),
78
+ skillVersion: (0, skill_1.installedSkillVersion)(),
79
+ platform: process.platform,
80
+ });
81
+ }
82
+ catch (error) {
83
+ if (error instanceof index_1.NotAuthenticatedError) {
84
+ return {
85
+ status: "error",
86
+ harness: adapter.sourceType,
87
+ harness_present: true,
88
+ evidence: detection.evidence,
89
+ local_source_id: localId,
90
+ server_source_id: before.server_source_id,
91
+ workspace_id: null,
92
+ source_status: before.status,
93
+ registration: "skipped",
94
+ message: "not authenticated: run `dreams auth login`",
95
+ };
96
+ }
97
+ if (error instanceof client_1.CloudAuthError) {
98
+ return {
99
+ status: "error",
100
+ harness: adapter.sourceType,
101
+ harness_present: true,
102
+ evidence: detection.evidence,
103
+ local_source_id: localId,
104
+ server_source_id: before.server_source_id,
105
+ workspace_id: null,
106
+ source_status: before.status,
107
+ registration: "skipped",
108
+ message: `authentication failed: ${error.message}`,
109
+ };
110
+ }
111
+ if (error instanceof client_1.CloudError) {
112
+ return {
113
+ status: "error",
114
+ harness: adapter.sourceType,
115
+ harness_present: true,
116
+ evidence: detection.evidence,
117
+ local_source_id: localId,
118
+ server_source_id: before.server_source_id,
119
+ workspace_id: null,
120
+ source_status: before.status,
121
+ registration: "skipped",
122
+ message: `could not register source: ${error.message}`,
123
+ };
124
+ }
125
+ throw error;
126
+ }
127
+ const persisted = (0, bindings_1.persistSourceBindings)(db, localId, {
128
+ serverSourceId: registration.serverSourceId,
129
+ status: registration.status,
130
+ workspaceId: registration.workspaceId,
131
+ }, options.leaseOwner);
132
+ return {
133
+ status: "ok",
134
+ harness: adapter.sourceType,
135
+ harness_present: true,
136
+ evidence: detection.evidence,
137
+ local_source_id: localId,
138
+ server_source_id: persisted.serverSourceId,
139
+ workspace_id: persisted.workspaceId,
140
+ source_status: persisted.status,
141
+ registration: before.server_source_id ? "refreshed" : "created",
142
+ message: null,
143
+ };
144
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /**
3
+ * Harness-agnostic session discovery shape ([LET-10313]).
4
+ * Parsing and filesystem layout stay inside each source adapter.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -42,7 +42,7 @@ var __importStar = (this && this.__importStar) || (function () {
42
42
  };
43
43
  })();
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.LeaseLostError = exports.SOURCE_LEASE = void 0;
45
+ exports.LeaseLostError = exports.SYNC_WORKER_LEASE = exports.SOURCE_LEASE = void 0;
46
46
  exports.assertLeaseHeld = assertLeaseHeld;
47
47
  exports.renewLease = renewLease;
48
48
  exports.acquireLease = acquireLease;
@@ -57,6 +57,8 @@ const DEFAULT_TTL_MS = 30_000;
57
57
  * against this one lease so they never interleave writes.
58
58
  */
59
59
  exports.SOURCE_LEASE = "source";
60
+ /** Election lease for the detached sync worker so only one worker runs at a time. */
61
+ exports.SYNC_WORKER_LEASE = "sync-worker";
60
62
  /** Thrown when a write is attempted after the lease was lost/reclaimed. */
61
63
  class LeaseLostError extends Error {
62
64
  constructor(name) {