@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.
- package/README.md +74 -20
- package/dist/auth/commands.js +46 -4
- package/dist/auth/credentials.js +28 -8
- package/dist/cli.js +198 -8
- package/dist/cloud/client.js +89 -5
- package/dist/cloud/upload.js +150 -70
- package/dist/development-reset.js +359 -0
- package/dist/launcher.js +311 -0
- package/dist/local/backfill.js +608 -0
- package/dist/local/bindings.js +30 -0
- package/dist/local/claude.js +61 -163
- package/dist/local/codex.js +313 -0
- package/dist/local/commands.js +129 -10
- package/dist/local/db.js +64 -0
- package/dist/local/detect.js +144 -0
- package/dist/local/discovered.js +6 -0
- package/dist/local/leases.js +3 -1
- package/dist/local/scan.js +99 -17
- package/dist/local/source-adapter.js +8 -0
- package/dist/local/source-adapters.js +58 -0
- package/dist/local/state-lock.js +88 -0
- package/dist/local/sync-control.js +381 -0
- package/dist/local/transcript-bytes.js +204 -0
- package/dist/managed-install.js +74 -86
- package/dist/onboard.js +26 -72
- package/dist/skill.js +281 -16
- package/dist/snapshots.js +210 -0
- package/dist/sync.js +790 -0
- package/package.json +1 -1
package/dist/cloud/client.js
CHANGED
|
@@ -42,8 +42,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
42
42
|
};
|
|
43
43
|
})();
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.CloudError = exports.CloudAuthError = void 0;
|
|
45
|
+
exports.CLOUD_UPLOAD_MAX_CONTENT_CHARS = exports.CloudError = exports.CloudAuthError = void 0;
|
|
46
|
+
exports.setCloudUploadMaxContentCharsForTests = setCloudUploadMaxContentCharsForTests;
|
|
47
|
+
exports.encodeUploadContent = encodeUploadContent;
|
|
46
48
|
exports.registerSource = registerSource;
|
|
49
|
+
exports.createSnapshot = createSnapshot;
|
|
47
50
|
exports.uploadObject = uploadObject;
|
|
48
51
|
const zlib = __importStar(require("node:zlib"));
|
|
49
52
|
const index_1 = require("../auth/index");
|
|
@@ -71,6 +74,33 @@ exports.CloudError = CloudError;
|
|
|
71
74
|
// Comfortably below the source lease TTL (30s) so a stalled request cannot
|
|
72
75
|
// outlive the lease and let another process reclaim it mid-flight.
|
|
73
76
|
const REQUEST_TIMEOUT_MS = 20_000;
|
|
77
|
+
/**
|
|
78
|
+
* Cloud `createUploadBodySchema.content` max: base64 of gzip'd bytes (not the
|
|
79
|
+
* uncompressed transcript). Enforced locally so oversized payloads quarantine
|
|
80
|
+
* without a wasted round trip ([Amelia] two-limit model).
|
|
81
|
+
*/
|
|
82
|
+
exports.CLOUD_UPLOAD_MAX_CONTENT_CHARS = 8 * 1024 * 1024;
|
|
83
|
+
let cloudUploadMaxContentCharsForTests = null;
|
|
84
|
+
/** Test-only override for the encoded upload size gate. Pass null to clear. */
|
|
85
|
+
function setCloudUploadMaxContentCharsForTests(value) {
|
|
86
|
+
cloudUploadMaxContentCharsForTests = value;
|
|
87
|
+
}
|
|
88
|
+
function cloudUploadMaxContentChars() {
|
|
89
|
+
return cloudUploadMaxContentCharsForTests ?? exports.CLOUD_UPLOAD_MAX_CONTENT_CHARS;
|
|
90
|
+
}
|
|
91
|
+
/** Gzip + base64 encode transcript bytes and enforce the Cloud content char cap. */
|
|
92
|
+
function encodeUploadContent(bytes, maxContentChars = cloudUploadMaxContentChars()) {
|
|
93
|
+
const content = zlib.gzipSync(bytes).toString("base64");
|
|
94
|
+
if (content.length > maxContentChars) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
code: "payload_too_large",
|
|
98
|
+
message: `gzip+base64 upload content is ${content.length} chars; Cloud maximum is ${maxContentChars}`,
|
|
99
|
+
encodedLength: content.length,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return { ok: true, content, encodedLength: content.length };
|
|
103
|
+
}
|
|
74
104
|
function errorDetail(error) {
|
|
75
105
|
const cause = error.cause;
|
|
76
106
|
return cause?.code || cause?.message || (error instanceof Error ? error.message : String(error));
|
|
@@ -144,7 +174,7 @@ function classifyAuthOrRetry(status, json, path) {
|
|
|
144
174
|
* id, so it is also how the CLI resolves the workspace.
|
|
145
175
|
*/
|
|
146
176
|
async function registerSource(input) {
|
|
147
|
-
const
|
|
177
|
+
const body = {
|
|
148
178
|
source_type: input.sourceType,
|
|
149
179
|
source_key: input.sourceKey,
|
|
150
180
|
display_name: input.displayName,
|
|
@@ -152,7 +182,16 @@ async function registerSource(input) {
|
|
|
152
182
|
adapter_version: input.adapterVersion,
|
|
153
183
|
privacy_config_hash: input.privacyConfigHash ?? null,
|
|
154
184
|
backfill_policy: input.backfillPolicy ?? null,
|
|
155
|
-
}
|
|
185
|
+
};
|
|
186
|
+
if (input.status != null)
|
|
187
|
+
body.status = input.status;
|
|
188
|
+
if (input.cliVersion != null)
|
|
189
|
+
body.cli_version = input.cliVersion;
|
|
190
|
+
if (input.skillVersion != null)
|
|
191
|
+
body.skill_version = input.skillVersion;
|
|
192
|
+
if (input.platform != null)
|
|
193
|
+
body.platform = input.platform;
|
|
194
|
+
const r = await request("POST", "/v1/dreams/sources", body);
|
|
156
195
|
if (r.status !== 200 && r.status !== 201)
|
|
157
196
|
classifyAuthOrRetry(r.status, r.json, "sources");
|
|
158
197
|
const id = r.json?.id;
|
|
@@ -164,14 +203,59 @@ async function registerSource(input) {
|
|
|
164
203
|
workspaceId: typeof r.json?.workspace_id === "string" ? r.json.workspace_id : null,
|
|
165
204
|
};
|
|
166
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Narrow transport for skill-authored snapshots ([11] / LET-9833).
|
|
208
|
+
* Validation beyond JSON-object + size belongs on the server.
|
|
209
|
+
*
|
|
210
|
+
* Cloud contract: POST /v1/dreams/workspaces/{workspaceId}/snapshots
|
|
211
|
+
* with body `{ type, data, client_request_id }`.
|
|
212
|
+
*/
|
|
213
|
+
async function createSnapshot(input) {
|
|
214
|
+
const path = `/v1/dreams/workspaces/${encodeURIComponent(input.workspaceId)}/snapshots`;
|
|
215
|
+
const r = await request("POST", path, {
|
|
216
|
+
type: input.type,
|
|
217
|
+
client_request_id: input.clientRequestId,
|
|
218
|
+
data: input.data,
|
|
219
|
+
});
|
|
220
|
+
if (r.status !== 200 && r.status !== 201)
|
|
221
|
+
classifyAuthOrRetry(r.status, r.json, "snapshots");
|
|
222
|
+
const id = r.json?.id;
|
|
223
|
+
if (typeof id !== "string")
|
|
224
|
+
throw new CloudError("snapshot response missing id", { retryable: true });
|
|
225
|
+
return {
|
|
226
|
+
id,
|
|
227
|
+
type: typeof r.json?.type === "string" ? r.json.type : input.type,
|
|
228
|
+
workspaceId: input.workspaceId,
|
|
229
|
+
clientRequestId: typeof r.json?.client_request_id === "string" ? r.json.client_request_id : input.clientRequestId,
|
|
230
|
+
status: typeof r.json?.status === "string" ? r.json.status : "stored",
|
|
231
|
+
createdAt: typeof r.json?.created_at === "string" ? r.json.created_at : null,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
167
234
|
async function uploadObject(input) {
|
|
168
|
-
|
|
235
|
+
// Prefer a preflight-encoded body; otherwise encode here (defense in depth).
|
|
236
|
+
const gated = input.encodedContent !== undefined
|
|
237
|
+
? input.encodedContent.length > cloudUploadMaxContentChars()
|
|
238
|
+
? {
|
|
239
|
+
ok: false,
|
|
240
|
+
code: "payload_too_large",
|
|
241
|
+
message: `gzip+base64 upload content is ${input.encodedContent.length} chars; Cloud maximum is ${cloudUploadMaxContentChars()}`,
|
|
242
|
+
encodedLength: input.encodedContent.length,
|
|
243
|
+
}
|
|
244
|
+
: {
|
|
245
|
+
ok: true,
|
|
246
|
+
content: input.encodedContent,
|
|
247
|
+
encodedLength: input.encodedContent.length,
|
|
248
|
+
}
|
|
249
|
+
: encodeUploadContent(input.bytes);
|
|
250
|
+
if (!gated.ok) {
|
|
251
|
+
return { kind: "quarantine", code: gated.code, message: gated.message };
|
|
252
|
+
}
|
|
169
253
|
const r = await request("POST", "/v1/dreams/uploads", {
|
|
170
254
|
source_id: input.sourceId,
|
|
171
255
|
client_upload_id: input.clientUploadId,
|
|
172
256
|
content_sha256: input.contentSha256,
|
|
173
257
|
content_encoding: "gzip+base64",
|
|
174
|
-
content,
|
|
258
|
+
content: gated.content,
|
|
175
259
|
source_schema: input.sourceSchema,
|
|
176
260
|
source_metadata: input.sourceMetadata,
|
|
177
261
|
priority: input.priority,
|
package/dist/cloud/upload.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
81
|
-
const UPLOAD_CAPABLE = new Set(["active", "backfilling", "
|
|
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
|
|
84
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
let
|
|
217
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
|
290
|
+
return false;
|
|
243
291
|
}
|
|
244
|
-
|
|
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
|
-
|
|
307
|
+
summary.rows_processed++;
|
|
261
308
|
let bytes;
|
|
262
309
|
try {
|
|
263
|
-
bytes = (0, blobs_1.readBlob)(row.content_sha256);
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
}
|