@happyvertical/smrt-ingestion 0.0.0-stage → 0.55.6

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.
Files changed (77) hide show
  1. package/AGENTS.md +35 -0
  2. package/CLAUDE.md +1 -0
  3. package/EXTRACTION.md +120 -0
  4. package/LICENSE +7 -0
  5. package/PROPOSALS.md +173 -0
  6. package/README.md +150 -2
  7. package/agents/execution.md +132 -0
  8. package/dist/__smrt-register__.d.ts +2 -0
  9. package/dist/__smrt-register__.d.ts.map +1 -0
  10. package/dist/chunks/execution-C6HJvl9e.js +7667 -0
  11. package/dist/chunks/execution-C6HJvl9e.js.map +1 -0
  12. package/dist/chunks/execution-internal-DWteDHM2.js +7 -0
  13. package/dist/chunks/execution-internal-DWteDHM2.js.map +1 -0
  14. package/dist/chunks/extraction-bseWZuWN.js +293 -0
  15. package/dist/chunks/extraction-bseWZuWN.js.map +1 -0
  16. package/dist/chunks/extraction-providers-C66ADDVe.js +433 -0
  17. package/dist/chunks/extraction-providers-C66ADDVe.js.map +1 -0
  18. package/dist/dto.d.ts +54 -0
  19. package/dist/dto.d.ts.map +1 -0
  20. package/dist/dto.js +0 -0
  21. package/dist/execution-contracts.d.ts +131 -0
  22. package/dist/execution-contracts.d.ts.map +1 -0
  23. package/dist/execution-dto.d.ts +70 -0
  24. package/dist/execution-dto.d.ts.map +1 -0
  25. package/dist/execution-internal.d.ts +7 -0
  26. package/dist/execution-internal.d.ts.map +1 -0
  27. package/dist/execution.d.ts +64 -0
  28. package/dist/execution.d.ts.map +1 -0
  29. package/dist/execution.js +2 -0
  30. package/dist/extraction-providers.d.ts +35 -0
  31. package/dist/extraction-providers.d.ts.map +1 -0
  32. package/dist/extraction-types.d.ts +94 -0
  33. package/dist/extraction-types.d.ts.map +1 -0
  34. package/dist/extraction-worker.d.ts +2 -0
  35. package/dist/extraction-worker.d.ts.map +1 -0
  36. package/dist/extraction-worker.js +81 -0
  37. package/dist/extraction-worker.js.map +1 -0
  38. package/dist/extraction.d.ts +49 -0
  39. package/dist/extraction.d.ts.map +1 -0
  40. package/dist/index.d.ts +4 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +3 -0
  43. package/dist/manifest.json +4091 -0
  44. package/dist/models.d.ts +186 -0
  45. package/dist/models.d.ts.map +1 -0
  46. package/dist/models.js +923 -0
  47. package/dist/models.js.map +1 -0
  48. package/dist/policy.d.ts +46 -0
  49. package/dist/policy.d.ts.map +1 -0
  50. package/dist/proposal-contracts.d.ts +85 -0
  51. package/dist/proposal-contracts.d.ts.map +1 -0
  52. package/dist/proposal-dto.d.ts +120 -0
  53. package/dist/proposal-dto.d.ts.map +1 -0
  54. package/dist/proposal-sdk.d.ts +12 -0
  55. package/dist/proposal-sdk.d.ts.map +1 -0
  56. package/dist/proposal-validation.d.ts +188 -0
  57. package/dist/proposal-validation.d.ts.map +1 -0
  58. package/dist/proposals.d.ts +41 -0
  59. package/dist/proposals.d.ts.map +1 -0
  60. package/dist/proposals.js +865 -0
  61. package/dist/proposals.js.map +1 -0
  62. package/dist/server.d.ts +200 -0
  63. package/dist/server.d.ts.map +1 -0
  64. package/dist/server.js +1588 -0
  65. package/dist/server.js.map +1 -0
  66. package/dist/smrt-knowledge.json +1621 -0
  67. package/dist/sources/common.d.ts +32 -0
  68. package/dist/sources/common.d.ts.map +1 -0
  69. package/dist/sources/email.d.ts +40 -0
  70. package/dist/sources/email.d.ts.map +1 -0
  71. package/dist/sources/http.d.ts +30 -0
  72. package/dist/sources/http.d.ts.map +1 -0
  73. package/dist/sources/index.d.ts +8 -0
  74. package/dist/sources/index.d.ts.map +1 -0
  75. package/dist/sources/watch-folder.d.ts +27 -0
  76. package/dist/sources/watch-folder.d.ts.map +1 -0
  77. package/package.json +86 -3
package/dist/server.js ADDED
@@ -0,0 +1,1588 @@
1
+ import "./models.js";
2
+ import { n as generationSnapshotRead } from "./chunks/execution-internal-DWteDHM2.js";
3
+ import { n as extractAnalysis, r as proposeDocumentSplits, t as createSDKExtractionAdapter } from "./chunks/extraction-bseWZuWN.js";
4
+ import { detectEngine, isEmbeddedDatabase, isTransientDatabaseError, withEmbeddedWriteTransaction } from "@happyvertical/smrt-core";
5
+ import { withTenant } from "@happyvertical/smrt-tenancy";
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import { Asset } from "@happyvertical/smrt-assets";
8
+ import { SmrtJobCollection } from "@happyvertical/smrt-jobs";
9
+ import { join } from "node:path";
10
+ import { constants } from "node:fs";
11
+ import { lstat, mkdir, open, readFile, readdir, rename, writeFile } from "node:fs/promises";
12
+ //#region src/sources/common.ts
13
+ var SourceInputError = class extends Error {
14
+ constructor(category) {
15
+ super(category);
16
+ this.category = category;
17
+ }
18
+ category;
19
+ };
20
+ function record(value) {
21
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new SourceInputError("invalid");
22
+ return value;
23
+ }
24
+ function keys(value, allowed) {
25
+ if (Object.keys(value).some((key) => !allowed.includes(key))) throw new SourceInputError("invalid");
26
+ }
27
+ function text(value) {
28
+ if (typeof value !== "string" || !value || value.length > 1024) throw new SourceInputError("invalid");
29
+ return value;
30
+ }
31
+ function timestamp(value) {
32
+ const date = new Date(text(value));
33
+ if (!Number.isFinite(date.getTime())) throw new SourceInputError("invalid");
34
+ return date;
35
+ }
36
+ async function boundedBytes(stream, limit) {
37
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new SourceInputError("limit");
38
+ if (!stream) return /* @__PURE__ */ new Uint8Array();
39
+ const reader = stream.getReader();
40
+ const chunks = [];
41
+ let size = 0;
42
+ try {
43
+ for (;;) {
44
+ const { value, done } = await reader.read();
45
+ if (done) break;
46
+ size += value.byteLength;
47
+ if (size > limit) {
48
+ await reader.cancel();
49
+ throw new SourceInputError("limit");
50
+ }
51
+ chunks.push(value);
52
+ }
53
+ } finally {
54
+ reader.releaseLock();
55
+ }
56
+ return Buffer.concat(chunks);
57
+ }
58
+ function validateMedia(bytes, type, allowed) {
59
+ if (!allowed.includes(type)) throw new SourceInputError("unsupported_type");
60
+ const b = Buffer.from(bytes);
61
+ const ascii = (start, end) => b.subarray(start, end).toString("ascii");
62
+ const valid = {
63
+ "application/pdf": ascii(0, 5) === "%PDF-",
64
+ "image/tiff": b.subarray(0, 4).equals(Buffer.from([
65
+ 73,
66
+ 73,
67
+ 42,
68
+ 0
69
+ ])) || b.subarray(0, 4).equals(Buffer.from([
70
+ 77,
71
+ 77,
72
+ 0,
73
+ 42
74
+ ])),
75
+ "image/jpeg": b[0] === 255 && b[1] === 216 && b[2] === 255,
76
+ "image/png": b.subarray(0, 8).equals(Buffer.from([
77
+ 137,
78
+ 80,
79
+ 78,
80
+ 71,
81
+ 13,
82
+ 10,
83
+ 26,
84
+ 10
85
+ ])),
86
+ "image/webp": ascii(0, 4) === "RIFF" && ascii(8, 12) === "WEBP",
87
+ "audio/wav": ascii(0, 4) === "RIFF" && ascii(8, 12) === "WAVE",
88
+ "audio/ogg": ascii(0, 4) === "OggS",
89
+ "audio/webm": b.subarray(0, 4).equals(Buffer.from([
90
+ 26,
91
+ 69,
92
+ 223,
93
+ 163
94
+ ])),
95
+ "audio/mpeg": ascii(0, 3) === "ID3" || b[0] === 255 && (b[1] & 224) === 224,
96
+ "audio/mp4": ascii(4, 8) === "ftyp",
97
+ "text/plain": true,
98
+ "text/html": true,
99
+ "application/json": true
100
+ };
101
+ if (!Object.hasOwn(valid, type) || !valid[type]) throw new SourceInputError("unsupported_type");
102
+ if (type.startsWith("text/") || type === "application/json") try {
103
+ const s = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
104
+ if (type === "application/json") JSON.parse(s);
105
+ } catch {
106
+ throw new SourceInputError("invalid");
107
+ }
108
+ }
109
+ async function deliver(binding, deliveryKey, deliveredAt, parts) {
110
+ if (!binding.enabled) return {
111
+ kind: "rejected",
112
+ category: "authentication"
113
+ };
114
+ try {
115
+ text(deliveryKey);
116
+ if (!Number.isFinite(deliveredAt.getTime())) throw new SourceInputError("invalid");
117
+ if (parts.length > binding.limits.maxParts || parts.reduce((n, p) => n + p.bytes.byteLength, 0) > binding.limits.maxBytes) throw new SourceInputError("limit");
118
+ for (const part of parts) validateMedia(part.bytes, part.mediaType, binding.allowedMediaTypes);
119
+ return await binding.service.receive({
120
+ sourceId: binding.sourceId,
121
+ sourceVersion: binding.sourceVersion,
122
+ capturedCeiling: binding.capturedCeiling,
123
+ retention: binding.retention,
124
+ limits: binding.limits,
125
+ deliveryKey,
126
+ deliveredAt,
127
+ parts
128
+ });
129
+ } catch (error) {
130
+ return safeFailure(error);
131
+ }
132
+ }
133
+ function safeFailure(error) {
134
+ return error instanceof SourceInputError ? {
135
+ kind: "rejected",
136
+ category: error.category
137
+ } : {
138
+ kind: "retry",
139
+ category: "unavailable"
140
+ };
141
+ }
142
+ //#endregion
143
+ //#region src/sources/email.ts
144
+ var EmailSourceAdapter = class {
145
+ constructor(options) {
146
+ this.options = options;
147
+ }
148
+ options;
149
+ async receive(delivery) {
150
+ const { binding } = this.options;
151
+ if (!binding.enabled) return {
152
+ kind: "rejected",
153
+ category: "authentication"
154
+ };
155
+ try {
156
+ text(delivery.locator);
157
+ text(delivery.revision);
158
+ text(delivery.checkpoint);
159
+ const snapshot = await this.options.readMessage(delivery);
160
+ if (snapshot.accountId !== this.options.accountId) return {
161
+ kind: "rejected",
162
+ category: "authentication"
163
+ };
164
+ const { attachments } = snapshot;
165
+ const body = {
166
+ accountId: snapshot.accountId,
167
+ messageId: snapshot.messageId,
168
+ threadId: snapshot.threadId,
169
+ inReplyTo: snapshot.inReplyTo,
170
+ subject: snapshot.subject,
171
+ text: snapshot.text,
172
+ html: snapshot.html,
173
+ date: snapshot.date
174
+ };
175
+ if (Object.values(body).some((value) => typeof value !== "string")) throw new SourceInputError("invalid");
176
+ const parts = [{
177
+ partId: "message",
178
+ mediaType: "application/json",
179
+ bytes: Buffer.from(JSON.stringify(body)),
180
+ sourceReference: {
181
+ owner: "message",
182
+ id: delivery.locator,
183
+ version: delivery.revision
184
+ }
185
+ }];
186
+ for (const attachment of attachments) {
187
+ if (!(attachment.bytes instanceof Uint8Array)) throw new SourceInputError("unavailable_bytes");
188
+ parts.push({
189
+ partId: `attachment:${text(attachment.partId)}`,
190
+ parentPartId: "message",
191
+ mediaType: attachment.mediaType,
192
+ bytes: attachment.bytes,
193
+ sourceReference: {
194
+ owner: "message-attachment",
195
+ id: `${delivery.locator}:${attachment.partId}`,
196
+ version: delivery.revision
197
+ }
198
+ });
199
+ }
200
+ parts[0].bytes = Buffer.from(JSON.stringify({
201
+ ...body,
202
+ attachments: attachments.map(({ partId, filename, mediaType }) => ({
203
+ partId,
204
+ filename,
205
+ mediaType
206
+ }))
207
+ }));
208
+ const result = await deliver(binding, JSON.stringify([
209
+ this.options.accountId,
210
+ delivery.locator,
211
+ delivery.revision
212
+ ]), new Date(snapshot.date), parts);
213
+ if (result.kind === "accepted" || result.kind === "duplicate") await this.options.acknowledge(delivery.checkpoint);
214
+ return result;
215
+ } catch (error) {
216
+ if (error instanceof Error && error.name === "EmailIntakeSnapshotError" && "category" in error) {
217
+ const category = error.category;
218
+ if (category === "invalid" || category === "limit" || category === "unavailable_bytes") return {
219
+ kind: "rejected",
220
+ category
221
+ };
222
+ }
223
+ return safeFailure(error);
224
+ }
225
+ }
226
+ async receiveBatch(deliveries) {
227
+ const results = [];
228
+ for (const delivery of deliveries) {
229
+ const result = await this.receive(delivery);
230
+ results.push(result);
231
+ if (result.kind !== "accepted" && result.kind !== "duplicate") break;
232
+ }
233
+ return results;
234
+ }
235
+ };
236
+ //#endregion
237
+ //#region src/sources/http.ts
238
+ function response(result) {
239
+ return Response.json(result, { status: result.kind === "accepted" || result.kind === "duplicate" ? 200 : result.kind === "retry" ? 503 : "category" in result && result.category === "authentication" ? 401 : "category" in result && result.category === "limit" ? 413 : 400 });
240
+ }
241
+ function createSourceDeliveryHandler(options) {
242
+ return async (request) => {
243
+ try {
244
+ const binding = await options.authenticate(request);
245
+ if (!binding?.enabled) return response({
246
+ kind: "rejected",
247
+ category: "authentication"
248
+ });
249
+ if (request.method !== "POST") throw new SourceInputError("invalid");
250
+ const key = text(request.headers.get("idempotency-key"));
251
+ const length = request.headers.get("content-length");
252
+ if (length && (!/^\d+$/.test(length) || Number(length) > binding.maxRequestBytes)) throw new SourceInputError("limit");
253
+ const bytes = await boundedBytes(request.body, binding.maxRequestBytes);
254
+ const type = request.headers.get("content-type") || "";
255
+ let deliveredAt;
256
+ const parts = [];
257
+ if (type.split(";")[0] === "application/json") {
258
+ let payload;
259
+ try {
260
+ payload = record(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
261
+ } catch {
262
+ throw new SourceInputError("invalid");
263
+ }
264
+ keys(payload, [
265
+ "capturedAt",
266
+ "text",
267
+ "structured",
268
+ "reference"
269
+ ]);
270
+ deliveredAt = timestamp(payload.capturedAt);
271
+ if ([
272
+ "text",
273
+ "structured",
274
+ "reference"
275
+ ].filter((field) => payload[field] !== void 0).length !== 1) throw new SourceInputError("invalid");
276
+ if (payload.text !== void 0) {
277
+ if (typeof payload.text !== "string") throw new SourceInputError("invalid");
278
+ parts.push({
279
+ partId: "body",
280
+ mediaType: "text/plain",
281
+ bytes: Buffer.from(payload.text)
282
+ });
283
+ } else if (payload.structured !== void 0) parts.push({
284
+ partId: "body",
285
+ mediaType: "application/json",
286
+ bytes: Buffer.from(JSON.stringify(record(payload.structured)))
287
+ });
288
+ else {
289
+ const ref = record(payload.reference);
290
+ keys(ref, [
291
+ "owner",
292
+ "id",
293
+ "version"
294
+ ]);
295
+ if (ref.owner !== "asset" && ref.owner !== "message" || !binding.snapshotReference) throw new SourceInputError("invalid");
296
+ const reference = {
297
+ owner: ref.owner,
298
+ id: text(ref.id),
299
+ version: text(ref.version)
300
+ };
301
+ const snapshots = await binding.snapshotReference(reference);
302
+ if (!snapshots.length) throw new SourceInputError("unavailable_bytes");
303
+ for (const part of snapshots) parts.push({
304
+ ...part,
305
+ sourceReference: part.sourceReference ?? reference
306
+ });
307
+ }
308
+ } else if (type.startsWith("multipart/form-data;")) {
309
+ let form;
310
+ try {
311
+ form = await new Response(Buffer.from(bytes), { headers: { "content-type": type } }).formData();
312
+ } catch {
313
+ throw new SourceInputError("invalid");
314
+ }
315
+ const fields = {};
316
+ for (const [name, value] of form.entries()) if (typeof value === "string") {
317
+ if (![
318
+ "captureId",
319
+ "capturedAt",
320
+ "captureSource",
321
+ "sha256"
322
+ ].includes(name) || fields[name] !== void 0) throw new SourceInputError("invalid");
323
+ fields[name] = value;
324
+ } else {
325
+ if (name !== "file" || parts.length) throw new SourceInputError("invalid");
326
+ parts.push({
327
+ partId: "file",
328
+ parentPartId: "capture",
329
+ mediaType: value.type,
330
+ bytes: await boundedBytes(value.stream(), binding.limits.maxBytes)
331
+ });
332
+ }
333
+ if (parts.length !== 1 || text(fields.captureId) !== key || ![
334
+ "camera",
335
+ "native_picker",
336
+ "audio",
337
+ "document"
338
+ ].includes(fields.captureSource)) throw new SourceInputError("invalid");
339
+ deliveredAt = timestamp(fields.capturedAt);
340
+ if (fields.sha256) {
341
+ if (!/^[a-f0-9]{64}$/.test(fields.sha256)) throw new SourceInputError("invalid");
342
+ parts[0].expectedHash = fields.sha256;
343
+ }
344
+ parts.unshift({
345
+ partId: "capture",
346
+ mediaType: "application/json",
347
+ bytes: Buffer.from(JSON.stringify({
348
+ captureId: fields.captureId,
349
+ capturedAt: fields.capturedAt,
350
+ captureSource: fields.captureSource,
351
+ ...fields.sha256 === void 0 ? {} : { sha256: fields.sha256 }
352
+ }))
353
+ });
354
+ } else throw new SourceInputError("unsupported_type");
355
+ return response(await deliver(binding, key, deliveredAt, parts));
356
+ } catch (error) {
357
+ return response(error instanceof SyntaxError ? {
358
+ kind: "rejected",
359
+ category: "invalid"
360
+ } : safeFailure(error));
361
+ }
362
+ };
363
+ }
364
+ function createVendorWebhookHandler(options) {
365
+ return async (request) => {
366
+ try {
367
+ if (request.method !== "POST") throw new SourceInputError("invalid");
368
+ const bytes = await boundedBytes(request.body, options.maxRequestBytes);
369
+ const verified = await options.verify(request, bytes);
370
+ if (!verified) return response({
371
+ kind: "rejected",
372
+ category: "authentication"
373
+ });
374
+ return response(await deliver(verified.binding, verified.deliveryKey, verified.deliveredAt, verified.parts));
375
+ } catch (error) {
376
+ return response(safeFailure(error));
377
+ }
378
+ };
379
+ }
380
+ //#endregion
381
+ //#region src/sources/watch-folder.ts
382
+ var WatchFolderSourceAdapter = class {
383
+ constructor(options) {
384
+ this.options = options;
385
+ if (!Number.isSafeInteger(options.stabilityMs) || options.stabilityMs < 1) throw new Error("Positive stability interval required");
386
+ }
387
+ options;
388
+ observations = /* @__PURE__ */ new Map();
389
+ polling = false;
390
+ async stable(path) {
391
+ const stat = await lstat(path);
392
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new SourceInputError("invalid");
393
+ const fingerprint = [
394
+ stat.dev,
395
+ stat.ino,
396
+ stat.size,
397
+ stat.mtimeMs,
398
+ stat.ctimeMs
399
+ ].join(":");
400
+ const now = (this.options.now ?? Date.now)();
401
+ const prior = this.observations.get(path);
402
+ if (!prior || prior.fingerprint !== fingerprint) {
403
+ this.observations.set(path, {
404
+ fingerprint,
405
+ since: now
406
+ });
407
+ return false;
408
+ }
409
+ return now - prior.since >= this.options.stabilityMs;
410
+ }
411
+ async syncDirectory(path) {
412
+ const directory = await open(path, constants.O_RDONLY);
413
+ try {
414
+ await directory.sync();
415
+ } finally {
416
+ await directory.close();
417
+ }
418
+ }
419
+ async importClaim(directory, claim) {
420
+ const path = join(directory, "original");
421
+ try {
422
+ if (!await this.stable(path)) return null;
423
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
424
+ let bytes;
425
+ try {
426
+ const before = await handle.stat();
427
+ if (before.size > this.options.binding.limits.maxBytes) throw new SourceInputError("limit");
428
+ bytes = Buffer.alloc(before.size);
429
+ let offset = 0;
430
+ while (offset < bytes.length) {
431
+ const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, offset);
432
+ if (!bytesRead) throw new SourceInputError("invalid");
433
+ offset += bytesRead;
434
+ }
435
+ const after = await handle.stat();
436
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) throw new SourceInputError("invalid");
437
+ } finally {
438
+ await handle.close();
439
+ }
440
+ const mediaType = /\.pdf$/i.test(claim.filename) ? "application/pdf" : "image/tiff";
441
+ const result = await deliver(this.options.binding, claim.id, new Date(claim.capturedAt), [{
442
+ partId: "original",
443
+ mediaType,
444
+ bytes,
445
+ sourceReference: {
446
+ owner: "watch-folder",
447
+ id: claim.filename,
448
+ version: claim.id
449
+ }
450
+ }]);
451
+ if (result.kind === "accepted" || result.kind === "duplicate") {
452
+ await this.options.checkpoint?.("received", claim.id);
453
+ await rename(directory, join(this.options.directory, ".ingestion-processed", claim.id));
454
+ await this.syncDirectory(join(this.options.directory, ".ingestion-processed"));
455
+ await this.syncDirectory(join(this.options.directory, ".ingestion-claims"));
456
+ this.observations.delete(path);
457
+ } else await writeFile(join(directory, "failure.json"), JSON.stringify(result), { mode: 384 });
458
+ return {
459
+ claimId: claim.id,
460
+ filename: claim.filename,
461
+ result
462
+ };
463
+ } catch (error) {
464
+ const result = safeFailure(error);
465
+ await writeFile(join(directory, "failure.json"), JSON.stringify(result), { mode: 384 }).catch(() => {});
466
+ return {
467
+ claimId: claim.id,
468
+ filename: claim.filename,
469
+ result
470
+ };
471
+ }
472
+ }
473
+ async poll() {
474
+ if (!this.options.binding.enabled || this.polling) return [];
475
+ this.polling = true;
476
+ const results = [];
477
+ try {
478
+ const root = this.options.directory;
479
+ const claims = join(root, ".ingestion-claims");
480
+ const processed = join(root, ".ingestion-processed");
481
+ const rootStat = await lstat(root);
482
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new SourceInputError("invalid");
483
+ await mkdir(claims, {
484
+ recursive: true,
485
+ mode: 448
486
+ });
487
+ await mkdir(processed, {
488
+ recursive: true,
489
+ mode: 448
490
+ });
491
+ for (const privatePath of [claims, processed]) if ((await lstat(privatePath)).isSymbolicLink()) throw new SourceInputError("invalid");
492
+ for (const entry of await readdir(claims, { withFileTypes: true })) {
493
+ if (!entry.isDirectory() || !/^[a-f0-9-]{36}$/.test(entry.name)) continue;
494
+ const directory = join(claims, entry.name);
495
+ try {
496
+ const claim = JSON.parse(await readFile(join(directory, "claim.json"), "utf8"));
497
+ if (claim.id !== entry.name || claim.sourceId !== this.options.binding.sourceId || claim.sourceVersion !== this.options.binding.sourceVersion) throw new SourceInputError("invalid");
498
+ try {
499
+ await lstat(join(directory, "original"));
500
+ } catch {
501
+ continue;
502
+ }
503
+ const result = await this.importClaim(directory, claim);
504
+ if (result) results.push(result);
505
+ } catch (error) {
506
+ results.push({
507
+ claimId: entry.name,
508
+ filename: "",
509
+ result: safeFailure(error)
510
+ });
511
+ }
512
+ }
513
+ for (const entry of await readdir(root, { withFileTypes: true })) {
514
+ if (!/\.(pdf|tiff?)$/i.test(entry.name)) continue;
515
+ const path = join(root, entry.name);
516
+ try {
517
+ if (!await this.stable(path)) continue;
518
+ const stat = await lstat(path);
519
+ const claim = {
520
+ id: randomUUID(),
521
+ filename: entry.name,
522
+ capturedAt: new Date(stat.mtimeMs).toISOString(),
523
+ sourceId: this.options.binding.sourceId,
524
+ sourceVersion: this.options.binding.sourceVersion
525
+ };
526
+ const directory = join(claims, claim.id);
527
+ await mkdir(directory, { mode: 448 });
528
+ const metadata = await open(join(directory, "claim.json"), "wx", 384);
529
+ try {
530
+ await metadata.writeFile(JSON.stringify(claim));
531
+ await metadata.sync();
532
+ } finally {
533
+ await metadata.close();
534
+ }
535
+ await this.syncDirectory(directory);
536
+ await this.syncDirectory(claims);
537
+ await rename(path, join(directory, "original"));
538
+ await this.syncDirectory(directory);
539
+ await this.syncDirectory(root);
540
+ this.observations.delete(path);
541
+ await this.options.checkpoint?.("claimed", claim.id);
542
+ await this.stable(join(directory, "original"));
543
+ } catch (error) {
544
+ results.push({
545
+ filename: entry.name,
546
+ result: safeFailure(error)
547
+ });
548
+ }
549
+ }
550
+ return results;
551
+ } finally {
552
+ this.polling = false;
553
+ }
554
+ }
555
+ };
556
+ //#endregion
557
+ //#region src/server.ts
558
+ var TABLES = [
559
+ "intake_items",
560
+ "intake_evidence",
561
+ "intake_analyses",
562
+ "intake_analysis_attempts",
563
+ "intake_plans",
564
+ "intake_actions",
565
+ "intake_proposals",
566
+ "intake_review_decisions",
567
+ "intake_executions",
568
+ "intake_feedback",
569
+ "intake_dispatches",
570
+ "intake_deletions"
571
+ ];
572
+ var failures = /* @__PURE__ */ new Set([
573
+ "unsupported_type",
574
+ "malformed_output",
575
+ "limit",
576
+ "timeout",
577
+ "unavailable",
578
+ "authentication",
579
+ "cancelled",
580
+ "integrity"
581
+ ]);
582
+ var uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
583
+ function object(value) {
584
+ const parsed = typeof value === "string" ? JSON.parse(value) : value;
585
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Invalid intake payload");
586
+ return parsed;
587
+ }
588
+ function canonical(value) {
589
+ if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
590
+ if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
591
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
592
+ if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`).join(",")}}`;
593
+ throw new Error("Invalid canonical intake value");
594
+ }
595
+ function digest(value) {
596
+ return createHash("sha256").update(canonical(value)).digest("hex");
597
+ }
598
+ function bytesHash(value) {
599
+ return createHash("sha256").update(value).digest("hex");
600
+ }
601
+ function nonempty(value) {
602
+ if (typeof value !== "string" || !value.trim() || value.length > 512) throw new Error("Invalid intake identifier");
603
+ }
604
+ function integer(value, max = 2147483647) {
605
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0 || value > max) throw new Error("Invalid intake limit");
606
+ }
607
+ function persistedDate(value) {
608
+ return value instanceof Date ? value : new Date(String(value));
609
+ }
610
+ function date(value) {
611
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime())) throw new Error("Invalid intake date");
612
+ return value.toISOString();
613
+ }
614
+ var IngestionService = class {
615
+ constructor(options) {
616
+ this.options = options;
617
+ this.db = options.db;
618
+ this.scope = Object.freeze({ ...options.scope });
619
+ if (!uuid.test(this.scope.tenantId)) throw new Error("Required tenant UUID");
620
+ nonempty(this.scope.actorId);
621
+ nonempty(this.scope.confidentialScopeId);
622
+ const configured = this.db;
623
+ const engine = detectEngine(this.db.url || configured.config?.url || "", configured.type || configured.config?.type);
624
+ if (!["sqlite", "postgres"].includes(engine) || !this.db.transaction) throw new Error("Ingestion requires transactional SQLite or PostgreSQL");
625
+ nonempty(options.jobTarget.objectType);
626
+ nonempty(options.jobTarget.method);
627
+ }
628
+ options;
629
+ db;
630
+ scope;
631
+ async execution() {
632
+ if (!this.options.execution) throw new Error("Intake execution is not configured");
633
+ const { IntakeExecutionService } = await import("./execution.js");
634
+ return new IntakeExecutionService(this.options, this.options.execution);
635
+ }
636
+ async previewProposal(input) {
637
+ return (await this.execution()).previewProposal(input);
638
+ }
639
+ async submitDecision(input) {
640
+ return (await this.execution()).submitDecision(input);
641
+ }
642
+ async applyAction(actionId) {
643
+ return (await this.execution()).applyAction(actionId);
644
+ }
645
+ async reconcileAction(actionId) {
646
+ return (await this.execution()).reconcileAction(actionId);
647
+ }
648
+ async getAction(actionId) {
649
+ return (await this.execution()).getAction(actionId);
650
+ }
651
+ async authorizeAutomatic(actionId) {
652
+ return (await this.execution()).authorizeAutomatic(actionId);
653
+ }
654
+ async previewPlan(input) {
655
+ return (await this.execution()).previewPlan(input);
656
+ }
657
+ async applyPlan(planId) {
658
+ return (await this.execution()).applyPlan(planId);
659
+ }
660
+ async proposals() {
661
+ if (!this.options.proposals) throw new Error("Intake proposals are not configured");
662
+ const { IngestionProposalService } = await import("./proposals.js");
663
+ return new IngestionProposalService(this, this.options, this.options.proposals);
664
+ }
665
+ async listHandlers(itemId) {
666
+ return (await this.proposals()).listHandlers(itemId);
667
+ }
668
+ async findCandidates(input) {
669
+ return (await this.proposals()).findCandidates(input);
670
+ }
671
+ async prepareGeneration(itemId, attemptId) {
672
+ return (await this.proposals()).prepareGeneration(itemId, attemptId);
673
+ }
674
+ async generateProposals(lease) {
675
+ return (await this.proposals()).generate(lease);
676
+ }
677
+ async previewGeneratedProposals(input) {
678
+ return (await this.proposals()).preview(input);
679
+ }
680
+ now() {
681
+ return this.options.now?.() ?? /* @__PURE__ */ new Date();
682
+ }
683
+ async point(name) {
684
+ await this.options.checkpoint?.(name);
685
+ }
686
+ async authorize(operation, data, db = this.db) {
687
+ if (!await this.options.authorize({
688
+ scope: this.scope,
689
+ operation,
690
+ capturedCeiling: object(data.capturedCeiling ?? {}),
691
+ db
692
+ })) throw new Error("Intake unavailable");
693
+ }
694
+ async rows(db, table, where = "", params = []) {
695
+ return (await db.query(`SELECT * FROM ${table} WHERE tenant_id = ? AND confidential_scope_id = ?${where ? ` AND ${where}` : ""}`, this.scope.tenantId, this.scope.confidentialScopeId, ...params)).rows;
696
+ }
697
+ async item(id, operation, db = this.db, allowExpired = false) {
698
+ const [item] = await this.rows(db, "intake_items", "id = ?", [id]);
699
+ if (!item) throw new Error("Intake unavailable");
700
+ await this.authorize(operation, object(item.data), db);
701
+ if (!allowExpired && (item.visibility !== "active" || persistedDate(item.expires_at).getTime() <= this.now().getTime())) throw new Error("Intake unavailable");
702
+ return item;
703
+ }
704
+ async tx(work) {
705
+ for (let attempt = 0;; attempt++) try {
706
+ return await withEmbeddedWriteTransaction(this.db, isEmbeddedDatabase(this.db), work);
707
+ } catch (error) {
708
+ if (attempt >= 3 || !isTransientDatabaseError(error)) throw error;
709
+ await new Promise((resolve) => setTimeout(resolve, 10 * 2 ** attempt));
710
+ }
711
+ }
712
+ async lock(db, itemId) {
713
+ if (!(await db.query("UPDATE intake_items SET analysis_revision = analysis_revision WHERE tenant_id = ? AND confidential_scope_id = ? AND id = ? RETURNING id", this.scope.tenantId, this.scope.confidentialScopeId, itemId)).rows.length) throw new Error("Intake unavailable");
714
+ }
715
+ async insert(db, table, values, conflict) {
716
+ const now = this.now().toISOString();
717
+ const row = {
718
+ id: randomUUID(),
719
+ slug: randomUUID(),
720
+ context: "",
721
+ created_at: now,
722
+ updated_at: now,
723
+ tenant_id: this.scope.tenantId,
724
+ confidential_scope_id: this.scope.confidentialScopeId,
725
+ ...values
726
+ };
727
+ const keys = Object.keys(row);
728
+ return (await db.query(`INSERT INTO ${table} (${keys.join(",")}) VALUES (${keys.map(() => "?").join(",")})${conflict ? ` ON CONFLICT (${conflict}) DO NOTHING` : ""} RETURNING *`, ...Object.values(row))).rows;
729
+ }
730
+ /** Diagnostic only: deploy manifest migrations before starting ingestion. */
731
+ async assertReady() {
732
+ for (const table of TABLES) await this.db.query(`SELECT id FROM ${table} WHERE 1 = 0`);
733
+ }
734
+ /** Preserve every original before acknowledgment; retries retain receipt identity. */
735
+ async receive(input) {
736
+ await this.authorize("receive", { capturedCeiling: input.capturedCeiling });
737
+ const now = this.now();
738
+ let parts;
739
+ try {
740
+ for (const value of [
741
+ input.sourceId,
742
+ input.sourceVersion,
743
+ input.deliveryKey,
744
+ input.retention.version
745
+ ]) nonempty(value);
746
+ for (const value of Object.values(input.limits)) integer(value);
747
+ date(input.deliveredAt);
748
+ date(input.retention.expiresAt);
749
+ date(input.retention.replayUntil);
750
+ date(input.retention.acceptAfter);
751
+ if (input.deliveredAt < input.retention.acceptAfter || input.retention.expiresAt <= now || input.retention.replayUntil < input.retention.expiresAt) return {
752
+ kind: "rejected",
753
+ category: "expired"
754
+ };
755
+ if (!input.parts.length || input.parts.length > input.limits.maxParts) return {
756
+ kind: "rejected",
757
+ category: "limit"
758
+ };
759
+ const ids = /* @__PURE__ */ new Set();
760
+ let total = 0;
761
+ parts = input.parts.map((part) => {
762
+ nonempty(part.partId);
763
+ nonempty(part.mediaType);
764
+ if (ids.has(part.partId) || !(part.bytes instanceof Uint8Array)) throw new Error("Invalid part");
765
+ ids.add(part.partId);
766
+ total += part.bytes.byteLength;
767
+ const hash = bytesHash(part.bytes);
768
+ if (part.expectedHash && part.expectedHash !== hash) throw new Error("Invalid hash");
769
+ return {
770
+ ...part,
771
+ bytes: Buffer.from(part.bytes),
772
+ hash
773
+ };
774
+ });
775
+ if (total > input.limits.maxBytes) return {
776
+ kind: "rejected",
777
+ category: "limit"
778
+ };
779
+ for (const part of parts) if (part.parentPartId && (!ids.has(part.parentPartId) || part.parentPartId === part.partId)) throw new Error("Invalid parent");
780
+ for (const part of parts) {
781
+ const seen = /* @__PURE__ */ new Set([part.partId]);
782
+ let parent = part.parentPartId;
783
+ while (parent) {
784
+ if (seen.has(parent)) throw new Error("Cyclic evidence");
785
+ seen.add(parent);
786
+ parent = parts.find((p) => p.partId === parent)?.parentPartId;
787
+ }
788
+ }
789
+ } catch {
790
+ return {
791
+ kind: "rejected",
792
+ category: "invalid"
793
+ };
794
+ }
795
+ const payloadDigest = digest(parts.map((p) => ({
796
+ partId: p.partId,
797
+ parentPartId: p.parentPartId ?? null,
798
+ mediaType: p.mediaType,
799
+ hash: p.hash,
800
+ length: p.bytes.byteLength,
801
+ sourceReference: p.sourceReference ?? null
802
+ })).sort((a, b) => a.partId.localeCompare(b.partId)));
803
+ const scopeDigest = digest({
804
+ scope: this.scope,
805
+ ceiling: input.capturedCeiling,
806
+ sourceVersion: input.sourceVersion
807
+ });
808
+ const reservation = await this.tx(async (db) => {
809
+ await this.authorize("receive", { capturedCeiling: input.capturedCeiling }, db);
810
+ const inserted = await this.insert(db, "intake_items", {
811
+ source_id: input.sourceId,
812
+ source_version: input.sourceVersion,
813
+ delivery_key: input.deliveryKey,
814
+ payload_digest: payloadDigest,
815
+ scope_digest: scopeDigest,
816
+ receipt_state: "reserved",
817
+ processing_state: "queued",
818
+ visibility: "active",
819
+ receipt_version: 1,
820
+ analysis_revision: 0,
821
+ fence: 0,
822
+ lease_token: "",
823
+ lease_until: null,
824
+ expires_at: date(input.retention.expiresAt),
825
+ replay_until: date(input.retention.replayUntil),
826
+ cancelled: false,
827
+ trace_id: input.traceId ?? randomUUID(),
828
+ data: canonical({
829
+ capturedCeiling: input.capturedCeiling,
830
+ limits: input.limits,
831
+ retentionVersion: input.retention.version,
832
+ deliveredAt: date(input.deliveredAt)
833
+ })
834
+ }, "tenant_id,source_id,delivery_key");
835
+ const item2 = (await db.query("SELECT * FROM intake_items WHERE tenant_id = ? AND source_id = ? AND delivery_key = ?", this.scope.tenantId, input.sourceId, input.deliveryKey)).rows[0];
836
+ if (item2.scope_digest !== scopeDigest || item2.payload_digest !== payloadDigest) return { kind: "conflict" };
837
+ if (item2.visibility !== "active" || persistedDate(item2.replay_until) <= now || persistedDate(item2.expires_at) <= now) return { kind: "expired" };
838
+ if (inserted.length) {
839
+ const evidenceIds = new Map(parts.map((p) => [p.partId, randomUUID()]));
840
+ const ordered = [...parts].sort((a, b) => {
841
+ const depth = (p) => p.parentPartId ? 1 + depth(parts.find((x) => x.partId === p.parentPartId)) : 0;
842
+ return depth(a) - depth(b);
843
+ });
844
+ for (const part of ordered) await this.insert(db, "intake_evidence", {
845
+ id: evidenceIds.get(part.partId),
846
+ item_id: item2.id,
847
+ part_id: part.partId,
848
+ parent_evidence_id: part.parentPartId ? evidenceIds.get(part.parentPartId) : null,
849
+ asset_id: randomUUID(),
850
+ state: "intent",
851
+ content_hash: part.hash,
852
+ byte_length: part.bytes.byteLength,
853
+ media_type: part.mediaType,
854
+ source_uri: "",
855
+ data: canonical({ sourceReference: part.sourceReference ?? null })
856
+ });
857
+ }
858
+ return {
859
+ kind: "reserved",
860
+ item: item2,
861
+ created: !!inserted.length
862
+ };
863
+ });
864
+ if (reservation.kind !== "reserved") return {
865
+ kind: "rejected",
866
+ category: reservation.kind
867
+ };
868
+ const item = reservation.item;
869
+ const itemId = String(item.id);
870
+ await this.point("reserved");
871
+ if (item.receipt_state === "ready") {
872
+ try {
873
+ await this.repairDispatches();
874
+ } catch {
875
+ return {
876
+ kind: "retry",
877
+ category: "unavailable"
878
+ };
879
+ }
880
+ return {
881
+ kind: "duplicate",
882
+ itemId,
883
+ receiptVersion: Number(item.receipt_version)
884
+ };
885
+ }
886
+ const token = randomUUID();
887
+ if (!(await this.tx((db) => db.query(`UPDATE intake_items SET receipt_state = 'preserving', fence = fence + 1, lease_token = ?, lease_until = ? WHERE tenant_id = ? AND confidential_scope_id = ? AND id = ? AND visibility = 'active' AND receipt_state <> 'ready' AND (lease_until IS NULL OR lease_until <= ?) RETURNING fence`, token, new Date(now.getTime() + input.limits.leaseMs).toISOString(), this.scope.tenantId, this.scope.confidentialScopeId, itemId, now.toISOString()))).rows.length) return {
888
+ kind: "retry",
889
+ category: "busy"
890
+ };
891
+ try {
892
+ for (const part of parts) await this.preserve(itemId, part, token);
893
+ await this.point("evidence");
894
+ await this.tx(async (db) => {
895
+ await this.lock(db, itemId);
896
+ await this.item(itemId, "process", db);
897
+ const evidence = await this.rows(db, "intake_evidence", "item_id = ?", [itemId]);
898
+ if (evidence.length !== parts.length || evidence.some((e) => e.state !== "durable")) throw new Error("Incomplete evidence");
899
+ if (!(await db.query(`UPDATE intake_items SET receipt_state='ready',lease_token='',lease_until=NULL WHERE tenant_id=? AND confidential_scope_id=? AND id=? AND lease_token=? AND lease_until>? AND visibility='active' RETURNING id`, this.scope.tenantId, this.scope.confidentialScopeId, itemId, token, this.now().toISOString())).rows.length) throw new Error("Lease lost");
900
+ await this.insert(db, "intake_dispatches", {
901
+ item_id: itemId,
902
+ stage: "analyze",
903
+ revision: 1,
904
+ state: "pending",
905
+ job_id: "",
906
+ deliveries: 0,
907
+ data: "{}"
908
+ }, "tenant_id,item_id,stage,revision");
909
+ });
910
+ await this.point("ready");
911
+ await this.repairDispatches();
912
+ await this.point("ack");
913
+ return {
914
+ kind: reservation.created ? "accepted" : "duplicate",
915
+ itemId,
916
+ receiptVersion: 1
917
+ };
918
+ } catch {
919
+ await this.tx((db) => db.query(`UPDATE intake_items SET receipt_state='needs_attention',lease_token='',lease_until=NULL WHERE tenant_id=? AND confidential_scope_id=? AND id=? AND lease_token=? AND receipt_state<>'ready'`, this.scope.tenantId, this.scope.confidentialScopeId, itemId, token));
920
+ return {
921
+ kind: "retry",
922
+ category: "unavailable"
923
+ };
924
+ }
925
+ }
926
+ async preserve(itemId, part, token) {
927
+ const [evidence] = await this.rows(this.db, "intake_evidence", "item_id=? AND part_id=?", [itemId, part.partId]);
928
+ if (!evidence) throw new Error("Missing storage intent");
929
+ await this.item(itemId, "process");
930
+ const asset = await withTenant({ tenantId: this.scope.tenantId }, async () => {
931
+ let asset2 = await this.options.assets.collection.get({ id: String(evidence.asset_id) });
932
+ if (!asset2) asset2 = await this.options.assets.collection.create({
933
+ id: String(evidence.asset_id),
934
+ tenantId: this.scope.tenantId,
935
+ name: String(evidence.id),
936
+ mimeType: part.mediaType,
937
+ typeSlug: "intake",
938
+ statusSlug: "active",
939
+ sourceUri: ""
940
+ });
941
+ if (asset2.tenantId !== this.scope.tenantId) throw new Error("Asset unavailable");
942
+ return asset2;
943
+ });
944
+ const uri = await this.options.assets.store.planFile(asset, {
945
+ mimeType: part.mediaType,
946
+ typeSlug: "intake"
947
+ });
948
+ if (evidence.source_uri && evidence.source_uri !== uri) throw new Error("Storage plan changed");
949
+ await this.tx(async (db) => {
950
+ await this.lock(db, itemId);
951
+ const item = await this.item(itemId, "process", db);
952
+ if (item.lease_token !== token || persistedDate(item.lease_until) <= this.now()) throw new Error("Lease lost");
953
+ await db.query("UPDATE intake_evidence SET source_uri=? WHERE tenant_id=? AND confidential_scope_id=? AND id=?", uri, this.scope.tenantId, this.scope.confidentialScopeId, evidence.id);
954
+ });
955
+ asset.sourceUri = uri;
956
+ await asset.save();
957
+ await this.point("storage_intent");
958
+ await this.options.assets.store.preserveFile(asset, Buffer.from(part.bytes), {
959
+ mimeType: part.mediaType,
960
+ typeSlug: "intake"
961
+ });
962
+ await this.point("storage");
963
+ const actual = await this.options.assets.store.read(asset);
964
+ if (actual.length !== Number(evidence.byte_length) || bytesHash(actual) !== evidence.content_hash) throw new Error("Storage integrity");
965
+ await this.tx(async (db) => {
966
+ await this.lock(db, itemId);
967
+ const item = await this.item(itemId, "process", db);
968
+ if (item.lease_token !== token || persistedDate(item.lease_until) <= this.now()) throw new Error("Lease lost");
969
+ await db.query("UPDATE intake_evidence SET state='durable' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", this.scope.tenantId, this.scope.confidentialScopeId, evidence.id);
970
+ });
971
+ }
972
+ /** Repair lost enqueue/mark writes and deleted/failed jobs. Duplicate jobs are safe. */
973
+ async repairDispatches() {
974
+ let repaired = 0;
975
+ const intents = await this.rows(this.db, "intake_dispatches", "state <> 'completed'");
976
+ const jobs = await SmrtJobCollection.create({ db: this.db });
977
+ for (const intent of intents) {
978
+ const item = await this.item(String(intent.item_id), "process", this.db, true);
979
+ if (item.visibility !== "active" || item.cancelled || persistedDate(item.expires_at) <= this.now()) continue;
980
+ if (intent.job_id && Number(intent.revision) === Math.max(1, Number(item.analysis_revision))) {
981
+ const job2 = await withTenant({ tenantId: this.scope.tenantId }, () => jobs.get({ id: String(intent.job_id) }));
982
+ if (job2 && ["pending", "running"].includes(job2.status)) continue;
983
+ }
984
+ if (!await this.tx(async (db) => {
985
+ await this.lock(db, String(item.id));
986
+ const currentItem = await this.item(String(item.id), "process", db);
987
+ const [currentIntent] = await this.rows(db, "intake_dispatches", "id=? AND item_id=?", [intent.id, item.id]);
988
+ if (!currentIntent || currentIntent.state === "completed") return false;
989
+ if (currentItem.cancelled || Number(currentIntent.revision) !== Math.max(1, Number(currentItem.analysis_revision))) {
990
+ await db.query("UPDATE intake_dispatches SET state='completed' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", this.scope.tenantId, this.scope.confidentialScopeId, currentIntent.id);
991
+ return false;
992
+ }
993
+ const limits = object(object(currentItem.data).limits);
994
+ if (Number(currentIntent.deliveries) >= limits.maxAttempts) {
995
+ await db.query("UPDATE intake_items SET processing_state='needs_attention' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", this.scope.tenantId, this.scope.confidentialScopeId, item.id);
996
+ await db.query("UPDATE intake_dispatches SET state='completed' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", this.scope.tenantId, this.scope.confidentialScopeId, intent.id);
997
+ return false;
998
+ }
999
+ return (await db.query("UPDATE intake_dispatches SET deliveries=deliveries+1 WHERE tenant_id=? AND confidential_scope_id=? AND id=? AND state<>'completed' AND deliveries<? RETURNING id", this.scope.tenantId, this.scope.confidentialScopeId, currentIntent.id, limits.maxAttempts)).rows.length === 1;
1000
+ })) continue;
1001
+ const job = await withTenant({ tenantId: this.scope.tenantId }, () => jobs.enqueueJob({
1002
+ tenantId: this.scope.tenantId,
1003
+ objectType: this.options.jobTarget.objectType,
1004
+ method: this.options.jobTarget.method,
1005
+ args: {
1006
+ itemId: intent.item_id,
1007
+ revision: Number(intent.revision),
1008
+ dispatchId: intent.id
1009
+ },
1010
+ queue: "ingestion",
1011
+ maxAttempts: 1
1012
+ }));
1013
+ await this.point("enqueued");
1014
+ await this.tx((db) => db.query("UPDATE intake_dispatches SET state='delivered',job_id=? WHERE tenant_id=? AND confidential_scope_id=? AND id=? AND state<>'completed'", job.id, this.scope.tenantId, this.scope.confidentialScopeId, intent.id));
1015
+ repaired++;
1016
+ }
1017
+ return repaired;
1018
+ }
1019
+ /** List only authorized active items; omitted items do not contribute counts. */
1020
+ async listItems() {
1021
+ const rows = await this.rows(this.db, "intake_items", "visibility='active' AND expires_at>?", [this.now().toISOString()]);
1022
+ const result = [];
1023
+ for (const row of rows) try {
1024
+ await this.authorize("read", object(row.data));
1025
+ result.push(this.itemDTO(row));
1026
+ } catch {}
1027
+ return result;
1028
+ }
1029
+ itemDTO(row) {
1030
+ return {
1031
+ id: String(row.id),
1032
+ receiptState: String(row.receipt_state),
1033
+ processingState: String(row.processing_state),
1034
+ analysisRevision: Number(row.analysis_revision),
1035
+ cancelled: !!row.cancelled,
1036
+ traceId: String(row.trace_id)
1037
+ };
1038
+ }
1039
+ async getItem(id) {
1040
+ return this.itemDTO(await this.item(id, "read"));
1041
+ }
1042
+ async getEvidence(itemId) {
1043
+ await this.item(itemId, "read");
1044
+ return (await this.rows(this.db, "intake_evidence", "item_id=? AND state='durable'", [itemId])).map((e) => ({
1045
+ id: String(e.id),
1046
+ partId: String(e.part_id),
1047
+ parentEvidenceId: e.parent_evidence_id ? String(e.parent_evidence_id) : null,
1048
+ mediaType: String(e.media_type),
1049
+ contentHash: String(e.content_hash),
1050
+ byteLength: Number(e.byte_length)
1051
+ }));
1052
+ }
1053
+ async readEvidence(itemId, evidenceId) {
1054
+ await this.item(itemId, "read");
1055
+ const [e] = await this.rows(this.db, "intake_evidence", "item_id=? AND id=? AND state='durable'", [itemId, evidenceId]);
1056
+ if (!e) throw new Error("Intake unavailable");
1057
+ const asset = await withTenant({ tenantId: this.scope.tenantId }, () => this.options.assets.collection.get({ id: String(e.asset_id) }));
1058
+ if (!asset || asset.tenantId !== this.scope.tenantId || asset.sourceUri !== e.source_uri) throw new Error("Intake unavailable");
1059
+ const bytes = await this.options.assets.store.read(asset);
1060
+ if (bytes.length !== Number(e.byte_length) || bytesHash(bytes) !== e.content_hash) throw new Error("Intake integrity");
1061
+ await this.item(itemId, "read");
1062
+ return bytes;
1063
+ }
1064
+ /** Append an immutable input revision. Reprocessing never creates actions. */
1065
+ async analyze(itemId, configuration, requestKey) {
1066
+ nonempty(requestKey);
1067
+ const configDigest = digest(configuration);
1068
+ return this.tx(async (db) => {
1069
+ await this.lock(db, itemId);
1070
+ const item = await this.item(itemId, "process", db);
1071
+ if (item.receipt_state !== "ready" || item.cancelled) throw new Error("Intake not processable");
1072
+ const replay = (await this.rows(db, "intake_analyses", "item_id=?", [itemId])).find((a) => object(a.data).requestKey === requestKey);
1073
+ if (replay) {
1074
+ if (object(replay.data).configDigest !== configDigest) throw new Error("Analysis request conflict");
1075
+ return {
1076
+ id: String(replay.id),
1077
+ revision: Number(replay.revision)
1078
+ };
1079
+ }
1080
+ const evidence = await this.rows(db, "intake_evidence", "item_id=? AND state='durable'", [itemId]);
1081
+ await db.query("UPDATE intake_analysis_attempts SET state='superseded' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND state IN ('queued','running')", this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1082
+ await db.query("UPDATE intake_analyses SET state='superseded' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND state IN ('queued','running')", this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1083
+ const revision = Number(item.analysis_revision) + 1;
1084
+ await db.query("UPDATE intake_dispatches SET state='completed' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND revision<? AND state<>'completed'", this.scope.tenantId, this.scope.confidentialScopeId, itemId, revision);
1085
+ const inputs = evidence.map((e) => ({
1086
+ id: e.id,
1087
+ hash: e.content_hash
1088
+ })).sort((a, b) => String(a.id).localeCompare(String(b.id)));
1089
+ const [analysis] = await this.insert(db, "intake_analyses", {
1090
+ item_id: itemId,
1091
+ revision,
1092
+ fence: 0,
1093
+ attempt_number: 0,
1094
+ current_attempt_id: null,
1095
+ state: "queued",
1096
+ input_digest: digest({
1097
+ inputs,
1098
+ configuration
1099
+ }),
1100
+ data: canonical({
1101
+ inputs,
1102
+ configuration,
1103
+ requestKey,
1104
+ configDigest
1105
+ })
1106
+ });
1107
+ await db.query("UPDATE intake_items SET analysis_revision=?,processing_state='queued' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", revision, this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1108
+ await this.insert(db, "intake_dispatches", {
1109
+ item_id: itemId,
1110
+ stage: "analyze",
1111
+ revision,
1112
+ state: "pending",
1113
+ job_id: "",
1114
+ deliveries: 0,
1115
+ data: "{}"
1116
+ }, "tenant_id,item_id,stage,revision");
1117
+ return {
1118
+ id: String(analysis.id),
1119
+ revision
1120
+ };
1121
+ });
1122
+ }
1123
+ /** Allocate a fenced attempt; active leases cannot be stolen before expiry. */
1124
+ async claimAnalysis(itemId, revision, workerId) {
1125
+ integer(revision);
1126
+ nonempty(workerId);
1127
+ return this.tx(async (db) => {
1128
+ await this.lock(db, itemId);
1129
+ const item = await this.item(itemId, "process", db);
1130
+ if (item.cancelled || item.receipt_state !== "ready" || Number(item.analysis_revision) !== revision) return null;
1131
+ const [analysis] = await this.rows(db, "intake_analyses", "item_id=? AND revision=?", [itemId, revision]);
1132
+ if (!analysis || ["completed", "partial"].includes(String(analysis.state))) return null;
1133
+ const limits = object(object(item.data).limits);
1134
+ const prior = (await this.rows(db, "intake_analysis_attempts", "analysis_id=?", [analysis.id])).find((a) => a.id === analysis.current_attempt_id);
1135
+ if (prior?.state === "running" && persistedDate(prior.lease_until) > this.now()) return null;
1136
+ if (Number(analysis.attempt_number) >= limits.maxAttempts) {
1137
+ if (prior?.state === "running") await db.query("UPDATE intake_analysis_attempts SET state='needs_attention',safe_error='limit' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", this.scope.tenantId, this.scope.confidentialScopeId, prior.id);
1138
+ await db.query("UPDATE intake_analyses SET state='needs_attention' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", this.scope.tenantId, this.scope.confidentialScopeId, analysis.id);
1139
+ await db.query("UPDATE intake_items SET processing_state='needs_attention' WHERE tenant_id=? AND confidential_scope_id=? AND id=? AND analysis_revision=?", this.scope.tenantId, this.scope.confidentialScopeId, itemId, revision);
1140
+ await db.query("UPDATE intake_dispatches SET state='completed' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND revision=?", this.scope.tenantId, this.scope.confidentialScopeId, itemId, revision);
1141
+ return null;
1142
+ }
1143
+ if (prior && ["running", "queued"].includes(String(prior.state))) await db.query("UPDATE intake_analysis_attempts SET state='superseded' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", this.scope.tenantId, this.scope.confidentialScopeId, prior.id);
1144
+ const fence = Number(analysis.fence) + 1;
1145
+ const attempt = Number(analysis.attempt_number) + 1;
1146
+ const token = randomUUID();
1147
+ const [row] = await this.insert(db, "intake_analysis_attempts", {
1148
+ item_id: itemId,
1149
+ analysis_id: analysis.id,
1150
+ attempt_number: attempt,
1151
+ fence,
1152
+ state: "running",
1153
+ lease_token: token,
1154
+ lease_until: new Date(this.now().getTime() + limits.leaseMs).toISOString(),
1155
+ output_digest: "",
1156
+ safe_error: "",
1157
+ confidence: null,
1158
+ data: canonical({
1159
+ workerId,
1160
+ startedAt: this.now().toISOString(),
1161
+ limits
1162
+ })
1163
+ });
1164
+ await db.query("UPDATE intake_analyses SET fence=?,attempt_number=?,current_attempt_id=?,state='running' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", fence, attempt, row.id, this.scope.tenantId, this.scope.confidentialScopeId, analysis.id);
1165
+ await db.query("UPDATE intake_items SET processing_state='running' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1166
+ return {
1167
+ itemId,
1168
+ analysisId: String(analysis.id),
1169
+ attemptId: String(row.id),
1170
+ revision,
1171
+ fence,
1172
+ token
1173
+ };
1174
+ });
1175
+ }
1176
+ async completedAnalysisSnapshot(db, itemId, attemptId, current) {
1177
+ await this.lock(db, itemId);
1178
+ const item = await this.item(itemId, "process", db);
1179
+ if (item.cancelled) throw new Error("Analysis unavailable");
1180
+ if (attemptId === void 0) {
1181
+ if (!current) throw new Error("Historical attempt required");
1182
+ const [latest] = await this.rows(db, "intake_analyses", "item_id=? AND revision=?", [itemId, item.analysis_revision]);
1183
+ if (!latest?.current_attempt_id) throw new Error("Analysis unavailable");
1184
+ attemptId = String(latest.current_attempt_id);
1185
+ }
1186
+ const [attempt] = await this.rows(db, "intake_analysis_attempts", "item_id=? AND id=?", [itemId, attemptId]);
1187
+ if (!attempt || !["completed", "partial"].includes(String(attempt.state)) || !attempt.output_digest) throw new Error("Analysis unavailable");
1188
+ const [analysis] = await this.rows(db, "intake_analyses", "item_id=? AND id=?", [itemId, attempt.analysis_id]);
1189
+ if (!analysis || analysis.current_attempt_id !== attemptId || current && Number(item.analysis_revision) !== Number(analysis.revision)) throw new Error("Analysis unavailable");
1190
+ const data = object(analysis.data);
1191
+ const configuration = object(data.configuration);
1192
+ if (!Array.isArray(data.inputs) || !data.inputs.length || digest({
1193
+ inputs: data.inputs,
1194
+ configuration
1195
+ }) !== analysis.input_digest || digest(configuration) !== data.configDigest) throw new Error("Analysis input integrity");
1196
+ const resultData = object(attempt.data);
1197
+ const result = {
1198
+ status: resultData.status,
1199
+ provider: resultData.provider,
1200
+ model: resultData.model,
1201
+ version: resultData.version,
1202
+ output: resultData.output,
1203
+ usage: resultData.usage,
1204
+ ...Object.hasOwn(resultData, "error") ? { error: resultData.error } : {},
1205
+ ...Object.hasOwn(resultData, "confidence") ? { confidence: resultData.confidence } : {}
1206
+ };
1207
+ if (result.status !== attempt.state || digest(result) !== attempt.output_digest) throw new Error("Analysis output integrity");
1208
+ const evidence = [];
1209
+ for (const input of data.inputs) {
1210
+ const frozen = object(input);
1211
+ const [row] = await this.rows(db, "intake_evidence", "item_id=? AND id=? AND state='durable'", [itemId, frozen.id]);
1212
+ if (!row || row.content_hash !== frozen.hash) throw new Error("Analysis input integrity");
1213
+ evidence.push({
1214
+ id: String(row.id),
1215
+ partId: String(row.part_id),
1216
+ parentEvidenceId: row.parent_evidence_id ? String(row.parent_evidence_id) : null,
1217
+ mediaType: String(row.media_type),
1218
+ contentHash: String(row.content_hash),
1219
+ byteLength: Number(row.byte_length)
1220
+ });
1221
+ }
1222
+ if (new Set(evidence.map((entry) => entry.id)).size !== evidence.length) throw new Error("Analysis input integrity");
1223
+ await this.item(itemId, "process", db);
1224
+ return structuredClone({
1225
+ itemId,
1226
+ attemptId,
1227
+ revision: Number(analysis.revision),
1228
+ inputDigest: String(analysis.input_digest),
1229
+ outputDigest: String(attempt.output_digest),
1230
+ evidenceDigest: digest(data.inputs),
1231
+ configuration,
1232
+ evidence,
1233
+ result
1234
+ });
1235
+ }
1236
+ /** Detached successful current revision; always current-scope/process authorized. */
1237
+ async getCompletedAnalysis(itemId, attemptId) {
1238
+ const snapshot = await this.tx((db) => this.completedAnalysisSnapshot(db, itemId, attemptId, true));
1239
+ await this.point("analysis:snapshot-read");
1240
+ const read = async (db) => {
1241
+ const current = await this.completedAnalysisSnapshot(db, itemId, snapshot.attemptId, true);
1242
+ if (current.inputDigest !== snapshot.inputDigest || current.outputDigest !== snapshot.outputDigest) throw new Error("Analysis changed");
1243
+ return current;
1244
+ };
1245
+ if (snapshot.configuration.stage === "interpret") return (await this.proposals())[generationSnapshotRead](itemId, (work) => this.tx(work), read);
1246
+ return this.tx(read);
1247
+ }
1248
+ /** Historical source is readable only through its frozen pin on a live generation lease. */
1249
+ async getGenerationInput(lease) {
1250
+ const generation = await this.getAnalysisInput(lease);
1251
+ const configuration = generation.configuration;
1252
+ const pin = object(object(configuration.proposals).source);
1253
+ if (configuration.stage !== "interpret" || typeof pin.attemptId !== "string") throw new Error("Invalid generation source");
1254
+ const source = await this.tx((db) => this.completedAnalysisSnapshot(db, lease.itemId, String(pin.attemptId), false));
1255
+ for (const key of [
1256
+ "attemptId",
1257
+ "revision",
1258
+ "inputDigest",
1259
+ "outputDigest",
1260
+ "evidenceDigest"
1261
+ ]) if (pin[key] !== source[key]) throw new Error("Generation source changed");
1262
+ if (source.result.provider !== "smrt-ingestion-extraction" || source.configuration.stage === "interpret" || source.revision !== generation.revision - 1 || digest(generation.evidence.map((entry) => ({
1263
+ id: entry.id,
1264
+ hash: entry.contentHash
1265
+ }))) !== source.evidenceDigest) throw new Error("Generation evidence changed");
1266
+ await this.getAnalysisInput(lease);
1267
+ return {
1268
+ generation,
1269
+ source
1270
+ };
1271
+ }
1272
+ /** Reload immutable inputs only through the current live processing lease. */
1273
+ async getAnalysisInput(lease) {
1274
+ return this.tx(async (db) => {
1275
+ await this.lock(db, lease.itemId);
1276
+ const item = await this.item(lease.itemId, "process", db);
1277
+ if (item.cancelled || Number(item.analysis_revision) !== lease.revision) throw new Error("Analysis lease unavailable");
1278
+ const [analysis] = await this.rows(db, "intake_analyses", "id=? AND item_id=?", [lease.analysisId, lease.itemId]);
1279
+ const [attempt] = await this.rows(db, "intake_analysis_attempts", "id=? AND analysis_id=? AND item_id=?", [
1280
+ lease.attemptId,
1281
+ lease.analysisId,
1282
+ lease.itemId
1283
+ ]);
1284
+ if (!analysis || !attempt || Number(analysis.revision) !== lease.revision || analysis.current_attempt_id !== lease.attemptId || Number(analysis.fence) !== lease.fence || Number(attempt.fence) !== lease.fence || attempt.state !== "running" || attempt.lease_token !== lease.token || persistedDate(attempt.lease_until) <= this.now()) throw new Error("Analysis lease unavailable");
1285
+ const data = object(analysis.data);
1286
+ const configuration = object(data.configuration);
1287
+ if (!Array.isArray(data.inputs) || !data.inputs.length || digest({
1288
+ inputs: data.inputs,
1289
+ configuration
1290
+ }) !== analysis.input_digest || digest(configuration) !== data.configDigest) throw new Error("Analysis input integrity");
1291
+ const evidence = [];
1292
+ for (const input of data.inputs) {
1293
+ const frozen = object(input);
1294
+ const [row] = await this.rows(db, "intake_evidence", "item_id=? AND id=? AND state='durable'", [lease.itemId, frozen.id]);
1295
+ if (!row || row.content_hash !== frozen.hash) throw new Error("Analysis input integrity");
1296
+ evidence.push({
1297
+ id: String(row.id),
1298
+ partId: String(row.part_id),
1299
+ parentEvidenceId: row.parent_evidence_id ? String(row.parent_evidence_id) : null,
1300
+ mediaType: String(row.media_type),
1301
+ contentHash: String(row.content_hash),
1302
+ byteLength: Number(row.byte_length)
1303
+ });
1304
+ }
1305
+ if (new Set(evidence.map((entry) => entry.id)).size !== evidence.length) throw new Error("Analysis input integrity");
1306
+ return {
1307
+ revision: lease.revision,
1308
+ attemptId: lease.attemptId,
1309
+ fence: lease.fence,
1310
+ inputDigest: String(analysis.input_digest),
1311
+ maxOutputBytes: Number(object(object(item.data).limits).maxOutputBytes),
1312
+ configuration,
1313
+ evidence
1314
+ };
1315
+ });
1316
+ }
1317
+ /** Publish once, only through the current unexpired fence and captured scope. */
1318
+ async completeAnalysis(lease, output) {
1319
+ if (![
1320
+ "completed",
1321
+ "partial",
1322
+ "failed",
1323
+ "needs_attention"
1324
+ ].includes(output.status) || !output.provider || !output.model || !output.version || output.error && !failures.has(output.error)) throw new Error("Invalid analysis output");
1325
+ if (output.confidence !== void 0 && (!Number.isFinite(output.confidence) || output.confidence < 0 || output.confidence > 1)) throw new Error("Invalid confidence");
1326
+ for (const value of Object.values(output.usage)) if (!Number.isFinite(value) || value < 0) throw new Error("Invalid usage");
1327
+ const serialized = canonical(output);
1328
+ const outputDigest = digest(output);
1329
+ return this.tx(async (db) => {
1330
+ await this.lock(db, lease.itemId);
1331
+ const item = await this.item(lease.itemId, "process", db);
1332
+ if (item.cancelled || Number(item.analysis_revision) !== lease.revision) return false;
1333
+ const limits = object(object(item.data).limits);
1334
+ if (Buffer.byteLength(serialized) > limits.maxOutputBytes) throw new Error("Analysis output budget exceeded");
1335
+ const [analysis] = await this.rows(db, "intake_analyses", "id=? AND item_id=?", [lease.analysisId, lease.itemId]);
1336
+ if (!analysis || analysis.current_attempt_id !== lease.attemptId || Number(analysis.fence) !== lease.fence) return false;
1337
+ const [attempt] = await this.rows(db, "intake_analysis_attempts", "id=? AND analysis_id=?", [lease.attemptId, lease.analysisId]);
1338
+ if (!attempt || attempt.state !== "running" || attempt.lease_token !== lease.token || persistedDate(attempt.lease_until) <= this.now()) return false;
1339
+ const attemptData = {
1340
+ ...object(attempt.data),
1341
+ ...output,
1342
+ completedAt: this.now().toISOString()
1343
+ };
1344
+ await db.query("UPDATE intake_analysis_attempts SET state=?,output_digest=?,safe_error=?,confidence=?,data=? WHERE tenant_id=? AND confidential_scope_id=? AND id=?", output.status, outputDigest, output.error ?? "", output.confidence ?? null, canonical(attemptData), this.scope.tenantId, this.scope.confidentialScopeId, lease.attemptId);
1345
+ await db.query("UPDATE intake_analyses SET state=? WHERE tenant_id=? AND confidential_scope_id=? AND id=?", output.status, this.scope.tenantId, this.scope.confidentialScopeId, lease.analysisId);
1346
+ await db.query("UPDATE intake_items SET processing_state=? WHERE tenant_id=? AND confidential_scope_id=? AND id=?", output.status, this.scope.tenantId, this.scope.confidentialScopeId, lease.itemId);
1347
+ if ([
1348
+ "completed",
1349
+ "partial",
1350
+ "needs_attention"
1351
+ ].includes(output.status)) await db.query("UPDATE intake_dispatches SET state='completed' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND revision=?", this.scope.tenantId, this.scope.confidentialScopeId, lease.itemId, lease.revision);
1352
+ return true;
1353
+ });
1354
+ }
1355
+ /** Terminal failure accounting without a provider payload. The fixed safe error
1356
+ * and lifecycle metadata are not charged to the provider-output byte ceiling. */
1357
+ async failAnalysis(lease, category) {
1358
+ if (!failures.has(category)) throw new Error("Invalid analysis failure");
1359
+ return this.tx(async (db) => {
1360
+ await this.lock(db, lease.itemId);
1361
+ const item = await this.item(lease.itemId, "process", db);
1362
+ if (item.cancelled || Number(item.analysis_revision) !== lease.revision) return false;
1363
+ const [analysis] = await this.rows(db, "intake_analyses", "id=? AND item_id=?", [lease.analysisId, lease.itemId]);
1364
+ const [attempt] = await this.rows(db, "intake_analysis_attempts", "id=? AND analysis_id=? AND item_id=?", [
1365
+ lease.attemptId,
1366
+ lease.analysisId,
1367
+ lease.itemId
1368
+ ]);
1369
+ if (!analysis || !attempt || Number(analysis.revision) !== lease.revision || analysis.current_attempt_id !== lease.attemptId || Number(analysis.fence) !== lease.fence || Number(attempt.fence) !== lease.fence || attempt.state !== "running" || attempt.lease_token !== lease.token || persistedDate(attempt.lease_until) <= this.now()) return false;
1370
+ await db.query("UPDATE intake_analysis_attempts SET state='needs_attention',safe_error=?,data=? WHERE tenant_id=? AND confidential_scope_id=? AND id=?", category, canonical({
1371
+ ...object(attempt.data),
1372
+ completedAt: this.now().toISOString()
1373
+ }), this.scope.tenantId, this.scope.confidentialScopeId, lease.attemptId);
1374
+ await db.query("UPDATE intake_analyses SET state='needs_attention' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", this.scope.tenantId, this.scope.confidentialScopeId, lease.analysisId);
1375
+ await db.query("UPDATE intake_items SET processing_state='needs_attention' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", this.scope.tenantId, this.scope.confidentialScopeId, lease.itemId);
1376
+ await db.query("UPDATE intake_dispatches SET state='completed' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND revision=?", this.scope.tenantId, this.scope.confidentialScopeId, lease.itemId, lease.revision);
1377
+ return true;
1378
+ });
1379
+ }
1380
+ /** Cancel pending processing immediately; original receipt remains ready. */
1381
+ async cancel(itemId) {
1382
+ await this.tx(async (db) => {
1383
+ await this.lock(db, itemId);
1384
+ await this.item(itemId, "process", db);
1385
+ await db.query("UPDATE intake_items SET cancelled=?,processing_state='cancelled' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", true, this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1386
+ await db.query("UPDATE intake_analyses SET state='superseded' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND state IN ('queued','running')", this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1387
+ await db.query("UPDATE intake_analysis_attempts SET state='superseded',safe_error='cancelled' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND state IN ('queued','running')", this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1388
+ await db.query("UPDATE intake_dispatches SET state='completed' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=?", this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1389
+ });
1390
+ }
1391
+ /** Mint an action only on an explicit host intention, never during reanalysis. */
1392
+ async createAction(itemId, actionKey, provenance) {
1393
+ nonempty(actionKey);
1394
+ const payload = canonical(provenance);
1395
+ return this.tx(async (db) => {
1396
+ await this.lock(db, itemId);
1397
+ if ((await this.item(itemId, "process", db)).cancelled) throw new Error("Intake cancelled");
1398
+ const [existing] = await this.rows(db, "intake_actions", "item_id=? AND action_key=?", [itemId, actionKey]);
1399
+ if (existing) {
1400
+ if (canonical(object(existing.data)) !== payload) throw new Error("Action identity conflict");
1401
+ return String(existing.id);
1402
+ }
1403
+ const [action] = await this.insert(db, "intake_actions", {
1404
+ item_id: itemId,
1405
+ action_key: actionKey,
1406
+ state: "proposed",
1407
+ proposal_revision: 0,
1408
+ review_version: 0,
1409
+ execution_attempt: 0,
1410
+ fence: 0,
1411
+ result_digest: "",
1412
+ data: payload
1413
+ });
1414
+ return String(action.id);
1415
+ });
1416
+ }
1417
+ /** Append a proposal pinned to a completed attempt. Policy/preview is host-owned. */
1418
+ async publishProposal(input) {
1419
+ canonical(input.binding);
1420
+ return this.tx(async (db) => {
1421
+ await this.lock(db, input.itemId);
1422
+ const item = await this.item(input.itemId, "process", db);
1423
+ if (item.cancelled) throw new Error("Intake cancelled");
1424
+ const [action] = await this.rows(db, "intake_actions", "item_id=? AND id=?", [input.itemId, input.actionId]);
1425
+ const [attempt] = await this.rows(db, "intake_analysis_attempts", "item_id=? AND id=?", [input.itemId, input.attemptId]);
1426
+ if (!action || !attempt || !["completed", "partial"].includes(String(attempt.state)) || !attempt.output_digest) throw new Error("Invalid proposal evidence");
1427
+ if (Number(action.proposal_revision) !== input.expectedRevision || [
1428
+ "succeeded",
1429
+ "executing",
1430
+ "outcome_unknown"
1431
+ ].includes(String(action.state))) throw new Error("Proposal conflict");
1432
+ const [analysis] = await this.rows(db, "intake_analyses", "id=? AND item_id=?", [attempt.analysis_id, input.itemId]);
1433
+ if (!analysis || Number(analysis.revision) !== Number(item.analysis_revision)) throw new Error("Proposal conflict");
1434
+ const bindingHash = digest({
1435
+ binding: input.binding,
1436
+ attemptId: attempt.id,
1437
+ attemptDigest: attempt.output_digest,
1438
+ inputDigest: analysis.input_digest
1439
+ });
1440
+ const revision = input.expectedRevision + 1;
1441
+ const [proposal] = await this.insert(db, "intake_proposals", {
1442
+ item_id: input.itemId,
1443
+ action_id: input.actionId,
1444
+ analysis_id: attempt.analysis_id,
1445
+ analysis_attempt_id: attempt.id,
1446
+ revision,
1447
+ binding_hash: bindingHash,
1448
+ data: canonical({
1449
+ binding: input.binding,
1450
+ attemptDigest: attempt.output_digest
1451
+ })
1452
+ });
1453
+ await db.query("UPDATE intake_actions SET proposal_revision=?,state='waiting_review',review_version=review_version+1 WHERE tenant_id=? AND confidential_scope_id=? AND id=?", revision, this.scope.tenantId, this.scope.confidentialScopeId, input.actionId);
1454
+ return {
1455
+ id: String(proposal.id),
1456
+ revision,
1457
+ bindingHash
1458
+ };
1459
+ });
1460
+ }
1461
+ /** Persist frozen plan structure; no expansion or apply authority is implied. */
1462
+ async appendPlan(itemId, planKey, revision, steps, definition) {
1463
+ nonempty(planKey);
1464
+ integer(revision);
1465
+ return this.tx(async (db) => {
1466
+ await this.lock(db, itemId);
1467
+ await this.item(itemId, "process", db);
1468
+ if (!steps.length || new Set(steps.map((s) => s.actionId)).size !== steps.length) throw new Error("Invalid plan");
1469
+ for (const step of steps) {
1470
+ const [proposal] = await this.rows(db, "intake_proposals", "item_id=? AND action_id=? AND revision=?", [
1471
+ itemId,
1472
+ step.actionId,
1473
+ step.proposalRevision
1474
+ ]);
1475
+ if (!proposal) throw new Error("Invalid plan binding");
1476
+ }
1477
+ const data = canonical({
1478
+ steps,
1479
+ definition
1480
+ });
1481
+ const hash = digest({
1482
+ steps,
1483
+ definition
1484
+ });
1485
+ const previous = await this.rows(db, "intake_plans", "item_id=? AND plan_key=?", [itemId, planKey]);
1486
+ const replay = previous.find((p) => Number(p.revision) === revision);
1487
+ if (replay) {
1488
+ if (replay.digest !== hash) throw new Error("Plan conflict");
1489
+ return String(replay.id);
1490
+ }
1491
+ if (revision !== Math.max(0, ...previous.map((p) => Number(p.revision))) + 1) throw new Error("Plan revision conflict");
1492
+ const [plan] = await this.insert(db, "intake_plans", {
1493
+ item_id: itemId,
1494
+ plan_key: planKey,
1495
+ revision,
1496
+ digest: hash,
1497
+ data
1498
+ });
1499
+ return String(plan.id);
1500
+ });
1501
+ }
1502
+ /** Revoke immediately and durably schedule deletion before external I/O. */
1503
+ async expire(itemId) {
1504
+ await this.scheduleExpiry(itemId);
1505
+ await this.repairDeletions();
1506
+ }
1507
+ async scheduleExpiry(itemId) {
1508
+ await this.tx(async (db) => {
1509
+ await this.lock(db, itemId);
1510
+ await this.item(itemId, "delete", db, true);
1511
+ await db.query("UPDATE intake_items SET visibility='expired',processing_state='expired',cancelled=?,lease_token='',lease_until=NULL WHERE tenant_id=? AND confidential_scope_id=? AND id=?", true, this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1512
+ await db.query("UPDATE intake_evidence SET state='revoked' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=?", this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1513
+ await db.query("UPDATE intake_dispatches SET state='completed' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=?", this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1514
+ await db.query("UPDATE intake_analyses SET state='superseded' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND state IN ('queued','running')", this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1515
+ await db.query("UPDATE intake_analysis_attempts SET state='superseded' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=? AND state IN ('queued','running')", this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1516
+ const cleanup = (await this.rows(db, "intake_evidence", "item_id=?", [itemId])).filter((e) => e.asset_id && e.source_uri).map((e) => ({
1517
+ assetId: e.asset_id,
1518
+ sourceUri: e.source_uri
1519
+ }));
1520
+ await this.insert(db, "intake_deletions", {
1521
+ item_id: itemId,
1522
+ state: "pending",
1523
+ data: canonical({ cleanup })
1524
+ }, "tenant_id,item_id");
1525
+ });
1526
+ }
1527
+ /** Retry privacy propagation. Minimal receipt/action identities remain replay-safe. */
1528
+ async repairDeletions() {
1529
+ let completed = 0;
1530
+ const pending = await this.rows(this.db, "intake_deletions");
1531
+ for (const intent of pending) {
1532
+ const itemId = String(intent.item_id);
1533
+ await this.item(itemId, "delete", this.db, true);
1534
+ const evidence = await this.rows(this.db, "intake_evidence", "item_id=?", [itemId]);
1535
+ for (const row of evidence) {
1536
+ if (!row.asset_id) continue;
1537
+ const asset = await withTenant({ tenantId: this.scope.tenantId }, () => this.options.assets.collection.get({ id: String(row.asset_id) }));
1538
+ if (asset) {
1539
+ if (asset.tenantId !== this.scope.tenantId) throw new Error("Asset unavailable");
1540
+ await this.options.assets.store.remove(asset);
1541
+ }
1542
+ }
1543
+ const cleanup = object(intent.data).cleanup;
1544
+ if (Array.isArray(cleanup)) for (const entry of cleanup) {
1545
+ const locator = object(entry);
1546
+ const asset = new Asset({ db: this.db });
1547
+ asset.id = String(locator.assetId);
1548
+ asset.tenantId = this.scope.tenantId;
1549
+ asset.sourceUri = String(locator.sourceUri);
1550
+ await this.options.assets.store.removeFile(asset);
1551
+ }
1552
+ if (intent.state === "completed") continue;
1553
+ await this.options.purgeDerived({
1554
+ tenantId: this.scope.tenantId,
1555
+ itemId
1556
+ });
1557
+ await this.point("deleted");
1558
+ await this.tx(async (db) => {
1559
+ await this.lock(db, itemId);
1560
+ const item = await this.item(itemId, "delete", db, true);
1561
+ for (const table of TABLES.filter((t) => t !== "intake_items" && t !== "intake_deletions")) await db.query(`UPDATE ${table} SET data='{}' WHERE tenant_id=? AND confidential_scope_id=? AND item_id=?`, this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1562
+ await db.query("UPDATE intake_evidence SET source_uri='',content_hash='',media_type='',byte_length=0 WHERE tenant_id=? AND confidential_scope_id=? AND item_id=?", this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1563
+ await db.query("UPDATE intake_items SET data=?,trace_id='' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", canonical({ capturedCeiling: object(item.data).capturedCeiling ?? {} }), this.scope.tenantId, this.scope.confidentialScopeId, itemId);
1564
+ await db.query("UPDATE intake_deletions SET state='completed' WHERE tenant_id=? AND confidential_scope_id=? AND id=?", this.scope.tenantId, this.scope.confidentialScopeId, intent.id);
1565
+ });
1566
+ completed++;
1567
+ }
1568
+ return completed;
1569
+ }
1570
+ /** Run host-configured expiry policy and retry interrupted deletion propagation. */
1571
+ async sweepRetention() {
1572
+ const expired = await this.rows(this.db, "intake_items", "visibility='active' AND expires_at<=?", [this.now().toISOString()]);
1573
+ try {
1574
+ for (const item of expired) await this.scheduleExpiry(String(item.id));
1575
+ } catch (error) {
1576
+ try {
1577
+ await this.repairDeletions();
1578
+ } catch {}
1579
+ throw error;
1580
+ }
1581
+ await this.repairDeletions();
1582
+ return expired.length;
1583
+ }
1584
+ };
1585
+ //#endregion
1586
+ export { EmailSourceAdapter, IngestionService, WatchFolderSourceAdapter, createSDKExtractionAdapter, createSourceDeliveryHandler, createVendorWebhookHandler, extractAnalysis, proposeDocumentSplits };
1587
+
1588
+ //# sourceMappingURL=server.js.map