@happyvertical/smrt-ingestion 0.57.21 → 0.57.23
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/FEEDBACK.md +104 -0
- package/PROPOSALS.md +77 -0
- package/README.md +126 -0
- package/REVIEW.md +272 -4
- package/agents/execution.md +72 -0
- package/dist/chunks/{execution-9CfjxX8I.js → execution-BzVQax0n.js} +421 -23
- package/dist/chunks/execution-BzVQax0n.js.map +1 -0
- package/dist/chunks/{feedback-CuUzWKzP.js → feedback-D3UTF8fm.js} +119 -21
- package/dist/chunks/feedback-D3UTF8fm.js.map +1 -0
- package/dist/chunks/{extraction-CJ-zrbg0.js → proposal-contracts-77Uo1B1j.js} +39 -39
- package/dist/chunks/proposal-contracts-77Uo1B1j.js.map +1 -0
- package/dist/chunks/{reviewer-instruction-Db3jDN9a.js → reviewer-instruction-CEc3AOuU.js} +2 -2
- package/dist/chunks/{reviewer-instruction-Db3jDN9a.js.map → reviewer-instruction-CEc3AOuU.js.map} +1 -1
- package/dist/dto.d.ts +69 -0
- package/dist/dto.d.ts.map +1 -1
- package/dist/execution-contracts.d.ts +4 -1
- package/dist/execution-contracts.d.ts.map +1 -1
- package/dist/execution-dto.d.ts +16 -0
- package/dist/execution-dto.d.ts.map +1 -1
- package/dist/execution.d.ts +58 -3
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +2 -2
- package/dist/feedback-dto.d.ts +12 -4
- package/dist/feedback-dto.d.ts.map +1 -1
- package/dist/feedback-internal.d.ts +9 -0
- package/dist/feedback-internal.d.ts.map +1 -1
- package/dist/feedback.d.ts +3 -0
- package/dist/feedback.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/manifest.json +321 -3
- package/dist/models.d.ts +15 -0
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +73 -2
- package/dist/models.js.map +1 -1
- package/dist/policy.d.ts +3 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/proposal-errors.d.ts +4 -0
- package/dist/proposal-errors.d.ts.map +1 -1
- package/dist/proposal-sdk.d.ts +54 -1
- package/dist/proposal-sdk.d.ts.map +1 -1
- package/dist/proposals.d.ts +2 -1
- package/dist/proposals.d.ts.map +1 -1
- package/dist/proposals.js +215 -10
- package/dist/proposals.js.map +1 -1
- package/dist/provider-failure.d.ts +3 -0
- package/dist/provider-failure.d.ts.map +1 -0
- package/dist/redaction.d.ts +9 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/review-dto.d.ts +14 -4
- package/dist/review-dto.d.ts.map +1 -1
- package/dist/review-host.d.ts.map +1 -1
- package/dist/server.d.ts +82 -10
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +366 -47
- package/dist/server.js.map +1 -1
- package/dist/smrt-knowledge.json +128 -22
- package/dist/svelte/components/ActionReview.svelte +39 -9
- package/dist/svelte/components/ActionReview.svelte.d.ts +8 -1
- package/dist/svelte/components/ActionReview.svelte.d.ts.map +1 -1
- package/dist/svelte/components/EvidenceViewer.svelte +60 -4
- package/dist/svelte/components/EvidenceViewer.svelte.d.ts +12 -1
- package/dist/svelte/components/EvidenceViewer.svelte.d.ts.map +1 -1
- package/dist/svelte/components/InboxView.svelte +160 -18
- package/dist/svelte/components/InboxView.svelte.d.ts +28 -1
- package/dist/svelte/components/InboxView.svelte.d.ts.map +1 -1
- package/dist/svelte/components/IntakeWorkspace.svelte +27 -0
- package/dist/svelte/components/IntakeWorkspace.svelte.d.ts +19 -0
- package/dist/svelte/components/IntakeWorkspace.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ReviewView.svelte +264 -23
- package/dist/svelte/components/ReviewView.svelte.d.ts +18 -1
- package/dist/svelte/components/ReviewView.svelte.d.ts.map +1 -1
- package/dist/svelte/components/TriageView.svelte +669 -0
- package/dist/svelte/components/TriageView.svelte.d.ts +16 -0
- package/dist/svelte/components/TriageView.svelte.d.ts.map +1 -0
- package/dist/svelte/i18n.d.ts +39 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +39 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +1 -0
- package/dist/svelte/view-utils.d.ts +65 -0
- package/dist/svelte/view-utils.d.ts.map +1 -1
- package/dist/svelte/view-utils.js +145 -0
- package/package.json +13 -13
- package/dist/chunks/execution-9CfjxX8I.js.map +0 -1
- package/dist/chunks/extraction-CJ-zrbg0.js.map +0 -1
- package/dist/chunks/feedback-CuUzWKzP.js.map +0 -1
package/dist/server.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as listReadGate, d as GenerationSnapshotStaleError, f as GenerationSourceStaleError, g as generationSnapshotRead, l as UNFINISHED_ACTION_STATES, m as ProposalsNotConfiguredError, p as HandlerArgumentsInvalidError, u as redactedCeiling } from "./chunks/execution-BzVQax0n.js";
|
|
2
2
|
import "./models.js";
|
|
3
|
-
import {
|
|
4
|
-
import { i as
|
|
3
|
+
import { r as extractWithProviders } from "./chunks/extraction-providers-C66ADDVe.js";
|
|
4
|
+
import { a as proposeDocumentSplits, i as extractAnalysis, n as assertProposalLimits, r as createSDKExtractionAdapter, t as proposalReadConfiguration } from "./chunks/proposal-contracts-77Uo1B1j.js";
|
|
5
|
+
import { i as reviewerInstructionView, n as ReviewerInstructionInvalidError, r as ReviewerInstructionStaleError, t as REVIEWER_INSTRUCTION_MAX_LENGTH } from "./chunks/reviewer-instruction-CEc3AOuU.js";
|
|
5
6
|
import { detectEngine, isEmbeddedDatabase, isTransientDatabaseError, withEmbeddedWriteTransaction } from "@happyvertical/smrt-core";
|
|
6
7
|
import { withTenant } from "@happyvertical/smrt-tenancy";
|
|
7
8
|
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
|
|
@@ -81,7 +82,7 @@ async function loadReviewSummary(service, item, requestedCursor) {
|
|
|
81
82
|
} while (cursor);
|
|
82
83
|
const current = await service.getItem(item.id);
|
|
83
84
|
if (!requested) throw new Error("Review page unavailable");
|
|
84
|
-
const state = ["queued", "running"].includes(current.processingState) ? "processing" : total ? succeeded === total ? "completed" : succeeded ? "partially_completed" : deferred ? "deferred" : failed ? "failed" : "waiting" : current.processingState === "needs_attention" ? "failed" : "unresolved";
|
|
85
|
+
const state = current.disposition && !succeeded ? "resolved" : ["queued", "running"].includes(current.processingState) ? "processing" : total ? succeeded === total ? "completed" : succeeded ? "partially_completed" : deferred ? "deferred" : failed ? "failed" : "waiting" : current.processingState === "needs_attention" ? "failed" : "unresolved";
|
|
85
86
|
return {
|
|
86
87
|
reviews: requested,
|
|
87
88
|
state
|
|
@@ -233,6 +234,18 @@ function createIntakeReviewHost(service, options) {
|
|
|
233
234
|
evidence: part
|
|
234
235
|
});
|
|
235
236
|
const assignment = await options.assignment(itemId);
|
|
237
|
+
let catalog;
|
|
238
|
+
try {
|
|
239
|
+
catalog = await service.listHandlers(itemId);
|
|
240
|
+
} catch {
|
|
241
|
+
catalog = void 0;
|
|
242
|
+
}
|
|
243
|
+
let disposition;
|
|
244
|
+
if (item.disposition) try {
|
|
245
|
+
disposition = await service.getItemDisposition(itemId);
|
|
246
|
+
} catch {
|
|
247
|
+
disposition = void 0;
|
|
248
|
+
}
|
|
236
249
|
const instruction = await loadInstructionView(service, itemId, analysis);
|
|
237
250
|
await service.getItem(itemId);
|
|
238
251
|
return {
|
|
@@ -244,6 +257,8 @@ function createIntakeReviewHost(service, options) {
|
|
|
244
257
|
},
|
|
245
258
|
evidence,
|
|
246
259
|
analysis,
|
|
260
|
+
...catalog ? { catalog } : {},
|
|
261
|
+
...disposition ? { disposition } : {},
|
|
247
262
|
reviews,
|
|
248
263
|
...instruction ? { instruction } : {},
|
|
249
264
|
availability: "available",
|
|
@@ -334,6 +349,12 @@ function createIntakeReviewHost(service, options) {
|
|
|
334
349
|
const result = await service.reviseLogicalSplit(input);
|
|
335
350
|
await options.onSplit?.(input.itemId, result.revision);
|
|
336
351
|
},
|
|
352
|
+
async startAction(input) {
|
|
353
|
+
await service.startReviewerAction(input);
|
|
354
|
+
},
|
|
355
|
+
async resolve(input) {
|
|
356
|
+
await service.resolveItem(input);
|
|
357
|
+
},
|
|
337
358
|
async instruct(input) {
|
|
338
359
|
let result;
|
|
339
360
|
try {
|
|
@@ -918,6 +939,7 @@ var TABLES = [
|
|
|
918
939
|
"intake_review_decisions",
|
|
919
940
|
"intake_executions",
|
|
920
941
|
"intake_feedback",
|
|
942
|
+
"intake_dispositions",
|
|
921
943
|
"intake_dispatches",
|
|
922
944
|
"intake_deletions"
|
|
923
945
|
];
|
|
@@ -967,6 +989,7 @@ function isIntakeId(value) {
|
|
|
967
989
|
return typeof value === "string" && uuid.test(value);
|
|
968
990
|
}
|
|
969
991
|
var INBOX_STATES = [
|
|
992
|
+
"resolved",
|
|
970
993
|
"processing",
|
|
971
994
|
"unresolved",
|
|
972
995
|
"waiting",
|
|
@@ -978,7 +1001,8 @@ var INBOX_STATES = [
|
|
|
978
1001
|
function actionCount(where = "") {
|
|
979
1002
|
return `(SELECT COUNT(*) FROM intake_actions a WHERE a.tenant_id = i.tenant_id AND a.confidential_scope_id = i.confidential_scope_id AND a.item_id = i.id${where ? ` AND ${where}` : ""})`;
|
|
980
1003
|
}
|
|
981
|
-
var
|
|
1004
|
+
var dispositionWhere = "d.tenant_id=i.tenant_id AND d.confidential_scope_id=i.confidential_scope_id AND d.item_id=i.id";
|
|
1005
|
+
var INBOX_STATE_SQL = `CASE WHEN EXISTS (SELECT 1 FROM intake_dispositions d WHERE ${dispositionWhere}) AND ${actionCount("a.state = 'succeeded'")} = 0 THEN 'resolved' WHEN i.processing_state IN ('queued','running') THEN 'processing' WHEN ${actionCount()} = 0 THEN CASE WHEN i.processing_state = 'needs_attention' THEN 'failed' ELSE 'unresolved' END WHEN ${actionCount("a.state <> 'succeeded'")} = 0 THEN 'completed' WHEN ${actionCount("a.state = 'succeeded'")} > 0 THEN 'partially_completed' WHEN ${actionCount("a.state = 'deferred'")} > 0 THEN 'deferred' WHEN ${actionCount("a.state IN ('failed','outcome_unknown')")} > 0 THEN 'failed' ELSE 'waiting' END`;
|
|
982
1006
|
var RECEIPT_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
983
1007
|
function receiptTime(row) {
|
|
984
1008
|
const value = row.created_at;
|
|
@@ -1015,6 +1039,89 @@ function decodeItemCursor(cursor) {
|
|
|
1015
1039
|
function persistedDate(value) {
|
|
1016
1040
|
return value instanceof Date ? value : new Date(String(value));
|
|
1017
1041
|
}
|
|
1042
|
+
var inlineMedia = /* @__PURE__ */ new Set(["text/plain", "application/json"]);
|
|
1043
|
+
var REDACTED = "redacted";
|
|
1044
|
+
var REDACTED_DIGESTS = [
|
|
1045
|
+
["intake_analyses", "input_digest"],
|
|
1046
|
+
["intake_analysis_attempts", "output_digest"],
|
|
1047
|
+
["intake_plans", "digest"],
|
|
1048
|
+
["intake_proposals", "binding_hash"],
|
|
1049
|
+
["intake_review_decisions", "binding_hash"],
|
|
1050
|
+
["intake_executions", "binding_hash"]
|
|
1051
|
+
];
|
|
1052
|
+
function mediaKind(mediaType) {
|
|
1053
|
+
return mediaType.split(";")[0].trim().toLowerCase();
|
|
1054
|
+
}
|
|
1055
|
+
function ownerKey(owner) {
|
|
1056
|
+
nonempty(owner.kind);
|
|
1057
|
+
nonempty(owner.id);
|
|
1058
|
+
if (owner.kind.includes(":") || !storableText(owner.kind) || !storableText(owner.id)) throw new Error("Invalid intake owner");
|
|
1059
|
+
return `${owner.kind}:${owner.id}`;
|
|
1060
|
+
}
|
|
1061
|
+
function storableText(value) {
|
|
1062
|
+
if (value.includes("\0")) return false;
|
|
1063
|
+
for (let i = 0; i < value.length; i++) {
|
|
1064
|
+
const code = value.charCodeAt(i);
|
|
1065
|
+
if (code >= 55296 && code <= 56319) {
|
|
1066
|
+
const next = value.charCodeAt(i + 1);
|
|
1067
|
+
if (!(next >= 56320 && next <= 57343)) return false;
|
|
1068
|
+
i++;
|
|
1069
|
+
} else if (code >= 56320 && code <= 57343) return false;
|
|
1070
|
+
}
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
function receivedPart(part) {
|
|
1074
|
+
nonempty(part.partId);
|
|
1075
|
+
if (!("reference" in part)) {
|
|
1076
|
+
nonempty(part.mediaType);
|
|
1077
|
+
if (!(part.bytes instanceof Uint8Array)) throw new Error("Invalid part");
|
|
1078
|
+
const bytes2 = Buffer.from(part.bytes);
|
|
1079
|
+
const hash = bytesHash(bytes2);
|
|
1080
|
+
if (part.expectedHash && part.expectedHash !== hash) throw new Error("Invalid hash");
|
|
1081
|
+
if (part.filename !== void 0) filename(part.filename);
|
|
1082
|
+
return {
|
|
1083
|
+
...part,
|
|
1084
|
+
bytes: bytes2,
|
|
1085
|
+
hash,
|
|
1086
|
+
retain: "copy"
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
const reference = object(part.reference);
|
|
1090
|
+
nonempty(reference.owner);
|
|
1091
|
+
nonempty(reference.id);
|
|
1092
|
+
if (reference.version !== void 0) nonempty(reference.version);
|
|
1093
|
+
for (const value2 of [
|
|
1094
|
+
reference.owner,
|
|
1095
|
+
reference.id,
|
|
1096
|
+
reference.version
|
|
1097
|
+
]) if (typeof value2 === "string" && !storableText(value2)) throw new Error("Invalid reference");
|
|
1098
|
+
const snapshot = object(part.snapshot);
|
|
1099
|
+
let mediaType;
|
|
1100
|
+
let value;
|
|
1101
|
+
if ("text" in snapshot && !("structured" in snapshot)) {
|
|
1102
|
+
if (typeof snapshot.text !== "string" || !storableText(snapshot.text)) throw new Error("Invalid snapshot");
|
|
1103
|
+
mediaType = "text/plain";
|
|
1104
|
+
value = snapshot.text;
|
|
1105
|
+
} else if ("structured" in snapshot && !("text" in snapshot)) {
|
|
1106
|
+
mediaType = "application/json";
|
|
1107
|
+
value = canonical(object(snapshot.structured));
|
|
1108
|
+
} else throw new Error("Invalid snapshot");
|
|
1109
|
+
const bytes = Buffer.from(value, "utf8");
|
|
1110
|
+
return {
|
|
1111
|
+
partId: part.partId,
|
|
1112
|
+
...part.parentPartId === void 0 ? {} : { parentPartId: part.parentPartId },
|
|
1113
|
+
mediaType,
|
|
1114
|
+
bytes,
|
|
1115
|
+
hash: bytesHash(bytes),
|
|
1116
|
+
sourceReference: {
|
|
1117
|
+
owner: reference.owner,
|
|
1118
|
+
id: reference.id,
|
|
1119
|
+
version: reference.version ?? ""
|
|
1120
|
+
},
|
|
1121
|
+
retain: "reference",
|
|
1122
|
+
snapshot: value
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1018
1125
|
function date(value) {
|
|
1019
1126
|
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) throw new Error("Invalid intake date");
|
|
1020
1127
|
return value.toISOString();
|
|
@@ -1078,9 +1185,28 @@ var IngestionService = class {
|
|
|
1078
1185
|
async getAction(actionId) {
|
|
1079
1186
|
return (await this.execution()).getAction(actionId);
|
|
1080
1187
|
}
|
|
1188
|
+
/** Own-submission review surface; see execution for its authority gate. */
|
|
1189
|
+
async listOwnProposals(input = {}) {
|
|
1190
|
+
return (await this.execution()).listOwnProposals(input);
|
|
1191
|
+
}
|
|
1192
|
+
async submitOwnDecision(input) {
|
|
1193
|
+
return (await this.execution()).submitOwnDecision(input);
|
|
1194
|
+
}
|
|
1081
1195
|
async authorizeAutomatic(actionId) {
|
|
1082
1196
|
return (await this.execution()).authorizeAutomatic(actionId);
|
|
1083
1197
|
}
|
|
1198
|
+
/** Reviewer-chosen handler for any item; same preview/approval path, never automatic. */
|
|
1199
|
+
async startReviewerAction(input) {
|
|
1200
|
+
return (await this.execution()).startAction(input);
|
|
1201
|
+
}
|
|
1202
|
+
/** Terminal "handled, no action" disposition; distinct from rejecting a proposal. */
|
|
1203
|
+
async resolveItem(input) {
|
|
1204
|
+
return (await this.execution()).resolveItem(input);
|
|
1205
|
+
}
|
|
1206
|
+
/** Reviewer and reason of a resolved item; requires reviewer authority. */
|
|
1207
|
+
async getItemDisposition(itemId) {
|
|
1208
|
+
return (await this.execution()).getItemDisposition(itemId);
|
|
1209
|
+
}
|
|
1084
1210
|
async previewPlan(input) {
|
|
1085
1211
|
return (await this.execution()).previewPlan(input);
|
|
1086
1212
|
}
|
|
@@ -1088,18 +1214,19 @@ var IngestionService = class {
|
|
|
1088
1214
|
return (await this.execution()).applyPlan(planId);
|
|
1089
1215
|
}
|
|
1090
1216
|
async proposals() {
|
|
1091
|
-
if (!this.options.proposals) throw new
|
|
1217
|
+
if (!this.options.proposals) throw new ProposalsNotConfiguredError();
|
|
1092
1218
|
const { IngestionProposalService } = await import("./proposals.js");
|
|
1093
1219
|
return new IngestionProposalService(this, this.options, this.options.proposals);
|
|
1094
1220
|
}
|
|
1095
1221
|
async proposalReader() {
|
|
1096
1222
|
const config = this.options.proposals ?? this.options.proposalRead;
|
|
1097
|
-
if (!config) throw new
|
|
1223
|
+
if (!config) throw new ProposalsNotConfiguredError();
|
|
1098
1224
|
const { IngestionProposalService } = await import("./proposals.js");
|
|
1099
1225
|
return new IngestionProposalService(this, this.options, config);
|
|
1100
1226
|
}
|
|
1227
|
+
/** Currently offered reviewer catalog; needs no generation credentials. */
|
|
1101
1228
|
async listHandlers(itemId) {
|
|
1102
|
-
return (await this.
|
|
1229
|
+
return (await this.proposalReader()).listHandlers(itemId);
|
|
1103
1230
|
}
|
|
1104
1231
|
async findCandidates(input) {
|
|
1105
1232
|
return (await this.proposals()).findCandidates(input);
|
|
@@ -1114,7 +1241,7 @@ var IngestionService = class {
|
|
|
1114
1241
|
return (await this.proposalReader()).preview(input);
|
|
1115
1242
|
}
|
|
1116
1243
|
async feedback() {
|
|
1117
|
-
const { IntakeFeedbackService } = await import("./chunks/feedback-
|
|
1244
|
+
const { IntakeFeedbackService } = await import("./chunks/feedback-D3UTF8fm.js").then((n) => n.r);
|
|
1118
1245
|
return new IntakeFeedbackService(this.options);
|
|
1119
1246
|
}
|
|
1120
1247
|
async recordFeedback(input) {
|
|
@@ -1167,6 +1294,10 @@ var IngestionService = class {
|
|
|
1167
1294
|
await new Promise((resolve) => setTimeout(resolve, 10 * 2 ** attempt));
|
|
1168
1295
|
}
|
|
1169
1296
|
}
|
|
1297
|
+
/** Refuse under the caller's item lock when a terminal disposition exists. */
|
|
1298
|
+
async assertUnresolved(db, itemId) {
|
|
1299
|
+
if ((await this.rows(db, "intake_dispositions", "item_id=?", [itemId])).length) throw new Error("Item resolved");
|
|
1300
|
+
}
|
|
1170
1301
|
async lock(db, itemId) {
|
|
1171
1302
|
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");
|
|
1172
1303
|
}
|
|
@@ -1194,6 +1325,8 @@ var IngestionService = class {
|
|
|
1194
1325
|
await this.authorize("receive", { capturedCeiling: input.capturedCeiling });
|
|
1195
1326
|
const now = this.now();
|
|
1196
1327
|
let parts;
|
|
1328
|
+
let owner = null;
|
|
1329
|
+
const inline = input.dispatch === "inline";
|
|
1197
1330
|
try {
|
|
1198
1331
|
for (const value of [
|
|
1199
1332
|
input.sourceId,
|
|
@@ -1201,6 +1334,16 @@ var IngestionService = class {
|
|
|
1201
1334
|
input.deliveryKey,
|
|
1202
1335
|
input.retention.version
|
|
1203
1336
|
]) nonempty(value);
|
|
1337
|
+
if (input.dispatch !== void 0 && !["job", "inline"].includes(input.dispatch)) throw new Error("Invalid dispatch mode");
|
|
1338
|
+
const retention = input.retention;
|
|
1339
|
+
if (retention.kind !== void 0 && !["time", "owner"].includes(retention.kind)) throw new Error("Invalid retention mode");
|
|
1340
|
+
if (retention.kind === "owner") {
|
|
1341
|
+
ownerKey(retention.owner);
|
|
1342
|
+
owner = {
|
|
1343
|
+
kind: retention.owner.kind,
|
|
1344
|
+
id: retention.owner.id
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1204
1347
|
for (const value of Object.values(input.limits)) integer(value);
|
|
1205
1348
|
date(input.deliveredAt);
|
|
1206
1349
|
date(input.retention.expiresAt);
|
|
@@ -1217,24 +1360,17 @@ var IngestionService = class {
|
|
|
1217
1360
|
const ids = /* @__PURE__ */ new Set();
|
|
1218
1361
|
let total = 0;
|
|
1219
1362
|
parts = input.parts.map((part) => {
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
const hash = bytesHash(part.bytes);
|
|
1226
|
-
if (part.expectedHash && part.expectedHash !== hash) throw new Error("Invalid hash");
|
|
1227
|
-
if (part.filename !== void 0) filename(part.filename);
|
|
1228
|
-
return {
|
|
1229
|
-
...part,
|
|
1230
|
-
bytes: Buffer.from(part.bytes),
|
|
1231
|
-
hash
|
|
1232
|
-
};
|
|
1363
|
+
const received = receivedPart(part);
|
|
1364
|
+
if (ids.has(received.partId)) throw new Error("Invalid part");
|
|
1365
|
+
ids.add(received.partId);
|
|
1366
|
+
total += received.bytes.byteLength;
|
|
1367
|
+
return received;
|
|
1233
1368
|
});
|
|
1234
1369
|
if (total > input.limits.maxBytes) return {
|
|
1235
1370
|
kind: "rejected",
|
|
1236
1371
|
category: "limit"
|
|
1237
1372
|
};
|
|
1373
|
+
if (inline && parts.some((p) => !inlineMedia.has(mediaKind(p.mediaType)))) throw new Error("Inline receipt requires text or structured parts");
|
|
1238
1374
|
for (const part of parts) if (part.parentPartId && (!ids.has(part.parentPartId) || part.parentPartId === part.partId)) throw new Error("Invalid parent");
|
|
1239
1375
|
for (const part of parts) {
|
|
1240
1376
|
const seen = /* @__PURE__ */ new Set([part.partId]);
|
|
@@ -1257,12 +1393,15 @@ var IngestionService = class {
|
|
|
1257
1393
|
mediaType: p.mediaType,
|
|
1258
1394
|
hash: p.hash,
|
|
1259
1395
|
length: p.bytes.byteLength,
|
|
1260
|
-
sourceReference: p.sourceReference ?? null
|
|
1396
|
+
sourceReference: p.sourceReference ?? null,
|
|
1397
|
+
...p.retain === "reference" ? { retain: "reference" } : {}
|
|
1261
1398
|
})).sort((a, b) => a.partId.localeCompare(b.partId)));
|
|
1262
1399
|
const scopeDigest = digest({
|
|
1263
1400
|
scope: this.scope,
|
|
1264
1401
|
ceiling: input.capturedCeiling,
|
|
1265
|
-
sourceVersion: input.sourceVersion
|
|
1402
|
+
sourceVersion: input.sourceVersion,
|
|
1403
|
+
...inline ? { dispatch: "inline" } : {},
|
|
1404
|
+
...owner ? { owner } : {}
|
|
1266
1405
|
});
|
|
1267
1406
|
const reservation = await this.tx(async (db) => {
|
|
1268
1407
|
await this.authorize("receive", { capturedCeiling: input.capturedCeiling }, db);
|
|
@@ -1284,12 +1423,16 @@ var IngestionService = class {
|
|
|
1284
1423
|
replay_until: date(input.retention.replayUntil),
|
|
1285
1424
|
cancelled: false,
|
|
1286
1425
|
trace_id: input.traceId ?? randomUUID(),
|
|
1426
|
+
submitter_id: this.scope.actorId,
|
|
1427
|
+
owner_key: owner ? ownerKey(owner) : "",
|
|
1287
1428
|
data: canonical({
|
|
1288
1429
|
capturedCeiling: input.capturedCeiling,
|
|
1289
1430
|
limits: input.limits,
|
|
1290
1431
|
retentionVersion: input.retention.version,
|
|
1291
1432
|
deliveredAt: date(input.deliveredAt),
|
|
1292
|
-
dispatchTarget: this.dispatchTarget
|
|
1433
|
+
dispatchTarget: this.dispatchTarget,
|
|
1434
|
+
...inline ? { dispatch: "inline" } : {},
|
|
1435
|
+
...owner ? { retentionOwner: owner } : {}
|
|
1293
1436
|
})
|
|
1294
1437
|
}, "tenant_id,source_id,delivery_key");
|
|
1295
1438
|
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];
|
|
@@ -1306,15 +1449,19 @@ var IngestionService = class {
|
|
|
1306
1449
|
item_id: item2.id,
|
|
1307
1450
|
part_id: part.partId,
|
|
1308
1451
|
parent_evidence_id: part.parentPartId ? evidenceIds.get(part.parentPartId) : null,
|
|
1309
|
-
asset_id: randomUUID(),
|
|
1310
|
-
state: "intent",
|
|
1452
|
+
asset_id: part.retain === "reference" ? null : randomUUID(),
|
|
1453
|
+
state: part.retain === "reference" ? "durable" : "intent",
|
|
1311
1454
|
content_hash: part.hash,
|
|
1312
1455
|
byte_length: part.bytes.byteLength,
|
|
1313
1456
|
media_type: part.mediaType,
|
|
1314
1457
|
source_uri: "",
|
|
1315
1458
|
data: canonical({
|
|
1316
1459
|
sourceReference: part.sourceReference ?? null,
|
|
1317
|
-
...part.filename === void 0 ? {} : { filename: part.filename }
|
|
1460
|
+
...part.filename === void 0 ? {} : { filename: part.filename },
|
|
1461
|
+
...part.retain === "reference" ? {
|
|
1462
|
+
retain: "reference",
|
|
1463
|
+
snapshot: part.snapshot
|
|
1464
|
+
} : {}
|
|
1318
1465
|
})
|
|
1319
1466
|
});
|
|
1320
1467
|
}
|
|
@@ -1332,8 +1479,12 @@ var IngestionService = class {
|
|
|
1332
1479
|
const itemId = String(item.id);
|
|
1333
1480
|
await this.point("reserved");
|
|
1334
1481
|
if (item.receipt_state === "ready") {
|
|
1482
|
+
if (await this.revokedBeforeAck(itemId)) return {
|
|
1483
|
+
kind: "rejected",
|
|
1484
|
+
category: "expired"
|
|
1485
|
+
};
|
|
1335
1486
|
try {
|
|
1336
|
-
await this.repairDispatches();
|
|
1487
|
+
if (!inline) await this.repairDispatches();
|
|
1337
1488
|
} catch {
|
|
1338
1489
|
return {
|
|
1339
1490
|
kind: "retry",
|
|
@@ -1347,12 +1498,15 @@ var IngestionService = class {
|
|
|
1347
1498
|
};
|
|
1348
1499
|
}
|
|
1349
1500
|
const token = randomUUID();
|
|
1350
|
-
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 {
|
|
1501
|
+
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 await this.revokedBeforeAck(itemId) ? {
|
|
1502
|
+
kind: "rejected",
|
|
1503
|
+
category: "expired"
|
|
1504
|
+
} : {
|
|
1351
1505
|
kind: "retry",
|
|
1352
1506
|
category: "busy"
|
|
1353
1507
|
};
|
|
1354
1508
|
try {
|
|
1355
|
-
for (const part of parts) await this.preserve(itemId, part, token);
|
|
1509
|
+
for (const part of parts) if (part.retain === "copy") await this.preserve(itemId, part, token);
|
|
1356
1510
|
await this.point("evidence");
|
|
1357
1511
|
await this.tx(async (db) => {
|
|
1358
1512
|
await this.lock(db, itemId);
|
|
@@ -1364,15 +1518,19 @@ var IngestionService = class {
|
|
|
1364
1518
|
item_id: itemId,
|
|
1365
1519
|
stage: "analyze",
|
|
1366
1520
|
revision: 1,
|
|
1367
|
-
state: "pending",
|
|
1521
|
+
state: inline ? "completed" : "pending",
|
|
1368
1522
|
job_id: "",
|
|
1369
1523
|
deliveries: 0,
|
|
1370
1524
|
data: canonical({ dispatchTarget: this.dispatchTarget })
|
|
1371
1525
|
}, "tenant_id,item_id,stage,revision");
|
|
1372
1526
|
});
|
|
1373
1527
|
await this.point("ready");
|
|
1374
|
-
await this.repairDispatches();
|
|
1528
|
+
if (!inline) await this.repairDispatches();
|
|
1375
1529
|
await this.point("ack");
|
|
1530
|
+
if (await this.revokedBeforeAck(itemId)) return {
|
|
1531
|
+
kind: "rejected",
|
|
1532
|
+
category: "expired"
|
|
1533
|
+
};
|
|
1376
1534
|
return {
|
|
1377
1535
|
kind: reservation.created ? "accepted" : "duplicate",
|
|
1378
1536
|
itemId,
|
|
@@ -1380,12 +1538,20 @@ var IngestionService = class {
|
|
|
1380
1538
|
};
|
|
1381
1539
|
} catch {
|
|
1382
1540
|
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));
|
|
1541
|
+
if (await this.revokedBeforeAck(itemId)) return {
|
|
1542
|
+
kind: "rejected",
|
|
1543
|
+
category: "expired"
|
|
1544
|
+
};
|
|
1383
1545
|
return {
|
|
1384
1546
|
kind: "retry",
|
|
1385
1547
|
category: "unavailable"
|
|
1386
1548
|
};
|
|
1387
1549
|
}
|
|
1388
1550
|
}
|
|
1551
|
+
async revokedBeforeAck(itemId) {
|
|
1552
|
+
const [row] = await this.rows(this.db, "intake_items", "id=?", [itemId]);
|
|
1553
|
+
return !row || row.visibility !== "active";
|
|
1554
|
+
}
|
|
1389
1555
|
async preserve(itemId, part, token) {
|
|
1390
1556
|
const [evidence] = await this.rows(this.db, "intake_evidence", "item_id=? AND part_id=?", [itemId, part.partId]);
|
|
1391
1557
|
if (!evidence) throw new Error("Missing storage intent");
|
|
@@ -1724,7 +1890,7 @@ var IngestionService = class {
|
|
|
1724
1890
|
}
|
|
1725
1891
|
const wanted = limit - items.length;
|
|
1726
1892
|
const batch = Math.min(scanCeiling - examined, Math.max(wanted, Math.min(100, examined)));
|
|
1727
|
-
const rows = (await this.db.query(`SELECT i.*, ${INBOX_STATE_SQL} AS inbox_state, ${actionCount()} AS action_count, ${receipt} FROM intake_items i${pinned} WHERE i.tenant_id = ? AND i.confidential_scope_id = ? AND i.visibility = 'active' AND i.expires_at > ?${where} ORDER BY i.created_at DESC, i.id DESC LIMIT ?`, ...params, batch + 1)).rows;
|
|
1893
|
+
const rows = (await this.db.query(`SELECT i.*, (SELECT first_d.created_at FROM intake_dispositions first_d WHERE first_d.tenant_id=i.tenant_id AND first_d.confidential_scope_id=i.confidential_scope_id AND first_d.item_id=i.id AND CAST(first_d.id AS TEXT)=(SELECT MIN(CAST(d.id AS TEXT)) FROM intake_dispositions d WHERE ${dispositionWhere})) AS disposition_at, ${INBOX_STATE_SQL} AS inbox_state, ${actionCount()} AS action_count, ${receipt} FROM intake_items i${pinned} WHERE i.tenant_id = ? AND i.confidential_scope_id = ? AND i.visibility = 'active' AND i.expires_at > ?${where} ORDER BY i.created_at DESC, i.id DESC LIMIT ?`, ...params, batch + 1)).rows;
|
|
1728
1894
|
const more = rows.length > batch;
|
|
1729
1895
|
const candidates = rows.slice(0, batch);
|
|
1730
1896
|
const passed = /* @__PURE__ */ new Set();
|
|
@@ -1761,7 +1927,7 @@ var IngestionService = class {
|
|
|
1761
1927
|
lastReadable = position;
|
|
1762
1928
|
if (input.state && row.inbox_state !== input.state) continue;
|
|
1763
1929
|
items.push({
|
|
1764
|
-
item: this.itemDTO(row),
|
|
1930
|
+
item: this.itemDTO(row, row.disposition_at),
|
|
1765
1931
|
state: String(row.inbox_state),
|
|
1766
1932
|
position
|
|
1767
1933
|
});
|
|
@@ -1924,18 +2090,25 @@ var IngestionService = class {
|
|
|
1924
2090
|
}
|
|
1925
2091
|
};
|
|
1926
2092
|
}
|
|
1927
|
-
|
|
2093
|
+
/** Only the disposition state and time; reviewer and reason need review authority. */
|
|
2094
|
+
itemDTO(row, dispositionAt) {
|
|
1928
2095
|
return {
|
|
1929
2096
|
id: String(row.id),
|
|
1930
2097
|
receiptState: String(row.receipt_state),
|
|
1931
2098
|
processingState: String(row.processing_state),
|
|
1932
2099
|
analysisRevision: Number(row.analysis_revision),
|
|
1933
2100
|
cancelled: !!row.cancelled,
|
|
1934
|
-
traceId: String(row.trace_id)
|
|
2101
|
+
traceId: String(row.trace_id),
|
|
2102
|
+
...dispositionAt !== void 0 && dispositionAt !== null ? { disposition: {
|
|
2103
|
+
state: "resolved",
|
|
2104
|
+
at: persistedDate(dispositionAt).toISOString()
|
|
2105
|
+
} } : {}
|
|
1935
2106
|
};
|
|
1936
2107
|
}
|
|
1937
2108
|
async getItem(id) {
|
|
1938
|
-
|
|
2109
|
+
const item = await this.item(id, "read");
|
|
2110
|
+
const [disposition] = await this.rows(this.db, "intake_dispositions", "item_id=? ORDER BY id LIMIT 1", [item.id]);
|
|
2111
|
+
return this.itemDTO(item, disposition?.created_at);
|
|
1939
2112
|
}
|
|
1940
2113
|
async getEvidence(itemId) {
|
|
1941
2114
|
await this.item(itemId, "read");
|
|
@@ -1986,13 +2159,23 @@ var IngestionService = class {
|
|
|
1986
2159
|
await this.item(itemId, "read");
|
|
1987
2160
|
const [e] = await this.rows(this.db, "intake_evidence", "item_id=? AND id=? AND state='durable'", [itemId, evidenceId]);
|
|
1988
2161
|
if (!e) throw new Error("Intake unavailable");
|
|
1989
|
-
const
|
|
1990
|
-
if (!asset || asset.tenantId !== this.scope.tenantId || asset.sourceUri !== e.source_uri) throw new Error("Intake unavailable");
|
|
1991
|
-
const bytes = await this.options.assets.store.read(asset);
|
|
2162
|
+
const bytes = await this.evidenceBytes(e);
|
|
1992
2163
|
if (bytes.length !== Number(e.byte_length) || bytesHash(bytes) !== e.content_hash) throw new Error("Intake integrity");
|
|
1993
2164
|
await this.item(itemId, "read");
|
|
1994
2165
|
return bytes;
|
|
1995
2166
|
}
|
|
2167
|
+
async evidenceBytes(e) {
|
|
2168
|
+
const data = object(e.data);
|
|
2169
|
+
if (data.retain === "reference") {
|
|
2170
|
+
if (e.asset_id) throw new Error("Intake integrity");
|
|
2171
|
+
if (typeof data.snapshot !== "string") throw new Error("Intake unavailable");
|
|
2172
|
+
return Buffer.from(data.snapshot, "utf8");
|
|
2173
|
+
}
|
|
2174
|
+
if (!e.asset_id) throw new Error("Intake unavailable");
|
|
2175
|
+
const asset = await withTenant({ tenantId: this.scope.tenantId }, () => this.options.assets.collection.get({ id: String(e.asset_id) }));
|
|
2176
|
+
if (!asset || asset.tenantId !== this.scope.tenantId || asset.sourceUri !== e.source_uri) throw new Error("Intake unavailable");
|
|
2177
|
+
return this.options.assets.store.read(asset);
|
|
2178
|
+
}
|
|
1996
2179
|
/** Append an immutable input revision. Reprocessing never creates actions. */
|
|
1997
2180
|
async analyze(itemId, configuration, requestKey) {
|
|
1998
2181
|
const frozen = structuredClone(configuration);
|
|
@@ -2000,11 +2183,12 @@ var IngestionService = class {
|
|
|
2000
2183
|
if (Object.hasOwn(frozen, "reviewerInstruction") && frozen.stage !== "interpret") throw new Error("Reviewer instructions require authenticated instruction revision");
|
|
2001
2184
|
return this.tx((db) => this.appendAnalysis(db, itemId, frozen, requestKey));
|
|
2002
2185
|
}
|
|
2003
|
-
async appendAnalysis(db, itemId, configuration, requestKey) {
|
|
2186
|
+
async appendAnalysis(db, itemId, configuration, requestKey, pinnedSourceAttemptId) {
|
|
2004
2187
|
nonempty(requestKey);
|
|
2005
2188
|
const configDigest = digest(configuration);
|
|
2006
2189
|
await this.lock(db, itemId);
|
|
2007
2190
|
const item = await this.item(itemId, "process", db);
|
|
2191
|
+
await this.assertUnresolved(db, itemId);
|
|
2008
2192
|
if (!await this.matchesDispatchRoute(db, item)) throw new Error("Intake dispatch route unavailable");
|
|
2009
2193
|
await this.bindDispatchRoute(db, item);
|
|
2010
2194
|
if (item.receipt_state !== "ready" || item.cancelled) throw new Error("Intake not processable");
|
|
@@ -2020,6 +2204,10 @@ var IngestionService = class {
|
|
|
2020
2204
|
if (configuration.stage === "interpret") {
|
|
2021
2205
|
if (Number(object(object(configuration.proposals).source).revision) !== previous.filter((row) => object(object(row.data).configuration).stage !== "interpret").reduce((max, row) => Math.max(max, Number(row.revision)), Number.NEGATIVE_INFINITY)) throw new GenerationSourceStaleError();
|
|
2022
2206
|
}
|
|
2207
|
+
if (pinnedSourceAttemptId !== void 0) {
|
|
2208
|
+
const latest = previous.find((a) => Number(a.revision) === Number(item.analysis_revision));
|
|
2209
|
+
if (!latest || latest.current_attempt_id !== pinnedSourceAttemptId) throw new Error("Generation source changed");
|
|
2210
|
+
}
|
|
2023
2211
|
const evidence = await this.rows(db, "intake_evidence", "item_id=? AND state='durable'", [itemId]);
|
|
2024
2212
|
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);
|
|
2025
2213
|
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);
|
|
@@ -2052,7 +2240,7 @@ var IngestionService = class {
|
|
|
2052
2240
|
item_id: itemId,
|
|
2053
2241
|
stage: "analyze",
|
|
2054
2242
|
revision,
|
|
2055
|
-
state: "pending",
|
|
2243
|
+
state: object(item.data).dispatch === "inline" ? "completed" : "pending",
|
|
2056
2244
|
job_id: "",
|
|
2057
2245
|
deliveries: 0,
|
|
2058
2246
|
data: canonical({ dispatchTarget: this.dispatchTarget })
|
|
@@ -2062,6 +2250,107 @@ var IngestionService = class {
|
|
|
2062
2250
|
revision
|
|
2063
2251
|
};
|
|
2064
2252
|
}
|
|
2253
|
+
/** Run one fenced stage in-process and reload its terminal projection. */
|
|
2254
|
+
async inlineStage(itemId, revision, work) {
|
|
2255
|
+
const lease = await this.claimAnalysis(itemId, revision, "inline");
|
|
2256
|
+
if (lease) await work(lease);
|
|
2257
|
+
return this.tx(async (db) => {
|
|
2258
|
+
await this.lock(db, itemId);
|
|
2259
|
+
await this.item(itemId, "process", db);
|
|
2260
|
+
const [analysis] = await this.rows(db, "intake_analyses", "item_id=? AND revision=?", [itemId, revision]);
|
|
2261
|
+
if (!analysis) throw new Error("Analysis unavailable");
|
|
2262
|
+
const [attempt] = analysis.current_attempt_id ? await this.rows(db, "intake_analysis_attempts", "id=? AND item_id=?", [analysis.current_attempt_id, itemId]) : [];
|
|
2263
|
+
if (!attempt || ![
|
|
2264
|
+
"completed",
|
|
2265
|
+
"partial",
|
|
2266
|
+
"needs_attention",
|
|
2267
|
+
"failed"
|
|
2268
|
+
].includes(String(attempt.state))) throw new Error("Inline interpretation busy");
|
|
2269
|
+
return {
|
|
2270
|
+
analysisId: String(analysis.id),
|
|
2271
|
+
revision,
|
|
2272
|
+
attemptId: String(attempt.id),
|
|
2273
|
+
state: String(attempt.state),
|
|
2274
|
+
error: attempt.safe_error ? String(attempt.safe_error) : null
|
|
2275
|
+
};
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
/** Documented in-process receive-to-proposals path for `dispatch: 'inline'`
|
|
2279
|
+
* receipts. Extraction (UTF-8 text/structured only) and interpretation use the
|
|
2280
|
+
* same immutable revisions, fenced leases, catalog-live checks, provenance and
|
|
2281
|
+
* failure categories as the job path; no queue intent is created or delivered. */
|
|
2282
|
+
async interpretInline(itemId, input) {
|
|
2283
|
+
nonempty(input.requestId);
|
|
2284
|
+
if (!storableText(input.requestId)) throw new Error("Invalid intake identifier");
|
|
2285
|
+
if (typeof input.extraction?.configurationRevision !== "string" || !input.extraction.configurationRevision || !storableText(input.extraction.configurationRevision)) throw new Error("Invalid extraction configuration");
|
|
2286
|
+
if (object((await this.item(itemId, "process")).data).dispatch !== "inline") throw new Error("Inline interpretation requires an inline receipt");
|
|
2287
|
+
const extraction = {
|
|
2288
|
+
configurationRevision: input.extraction.configurationRevision,
|
|
2289
|
+
limits: structuredClone(input.extraction.limits)
|
|
2290
|
+
};
|
|
2291
|
+
const requested = await this.analyze(itemId, {
|
|
2292
|
+
stage: "extract",
|
|
2293
|
+
extraction
|
|
2294
|
+
}, `inline:extract:${input.requestId}`);
|
|
2295
|
+
const extracted = await this.inlineStage(itemId, requested.revision, (lease) => extractAnalysis(this, lease, { extract: (request) => extractWithProviders(request, {}) }, extraction));
|
|
2296
|
+
if (!["completed", "partial"].includes(extracted.state)) return {
|
|
2297
|
+
itemId,
|
|
2298
|
+
extraction: extracted
|
|
2299
|
+
};
|
|
2300
|
+
const interpretKey = `inline:interpret:${input.requestId}`;
|
|
2301
|
+
const interpretation = await this.analysisByRequest(itemId, interpretKey) ?? await this.appendInterpretation(itemId, extracted.attemptId, interpretKey);
|
|
2302
|
+
const generation = await this.inlineStage(itemId, interpretation.revision, (lease) => this.generateProposals(lease));
|
|
2303
|
+
if (!["completed", "partial"].includes(generation.state)) return {
|
|
2304
|
+
itemId,
|
|
2305
|
+
extraction: extracted,
|
|
2306
|
+
generation
|
|
2307
|
+
};
|
|
2308
|
+
return {
|
|
2309
|
+
itemId,
|
|
2310
|
+
extraction: extracted,
|
|
2311
|
+
generation,
|
|
2312
|
+
analysis: await this.getCompletedAnalysis(itemId, generation.attemptId)
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
/** Prepare outside the lock (it reads catalog and analysis), then append
|
|
2316
|
+
* under it with the source pin rechecked. A concurrent identical request
|
|
2317
|
+
* that appended first makes preparation fail; its revision is the replay. */
|
|
2318
|
+
async appendInterpretation(itemId, sourceAttemptId, requestKey) {
|
|
2319
|
+
await this.point("inline:replay-miss");
|
|
2320
|
+
let configuration;
|
|
2321
|
+
try {
|
|
2322
|
+
configuration = structuredClone(await this.prepareGeneration(itemId, sourceAttemptId));
|
|
2323
|
+
} catch (error) {
|
|
2324
|
+
const replay = await this.analysisByRequest(itemId, requestKey);
|
|
2325
|
+
if (replay) return replay;
|
|
2326
|
+
throw error;
|
|
2327
|
+
}
|
|
2328
|
+
await this.point("inline:prepared");
|
|
2329
|
+
return this.tx((db) => this.appendAnalysis(db, itemId, configuration, requestKey, sourceAttemptId));
|
|
2330
|
+
}
|
|
2331
|
+
async analysisByRequest(itemId, requestKey) {
|
|
2332
|
+
const found = (await this.rows(this.db, "intake_analyses", "item_id=?", [itemId])).find((row) => object(row.data).requestKey === requestKey);
|
|
2333
|
+
return found ? {
|
|
2334
|
+
id: String(found.id),
|
|
2335
|
+
revision: Number(found.revision)
|
|
2336
|
+
} : null;
|
|
2337
|
+
}
|
|
2338
|
+
/** Owner-lifecycle hook: expire every active receipt bound to a deleted owner
|
|
2339
|
+
* through the ordinary revoke-then-delete path. Returns the number scheduled. */
|
|
2340
|
+
async expireOwned(owner) {
|
|
2341
|
+
const key = ownerKey(owner);
|
|
2342
|
+
const items = await this.rows(this.db, "intake_items", "owner_key=? AND visibility='active'", [key]);
|
|
2343
|
+
try {
|
|
2344
|
+
for (const item of items) await this.scheduleExpiry(String(item.id));
|
|
2345
|
+
} catch (error) {
|
|
2346
|
+
try {
|
|
2347
|
+
await this.repairDeletions();
|
|
2348
|
+
} catch {}
|
|
2349
|
+
throw error;
|
|
2350
|
+
}
|
|
2351
|
+
await this.repairDeletions();
|
|
2352
|
+
return items.length;
|
|
2353
|
+
}
|
|
2065
2354
|
/** Record an authenticated split correction and queue fresh extraction, never copy model output. */
|
|
2066
2355
|
async reviseLogicalSplit(input) {
|
|
2067
2356
|
const frozen = structuredClone(input);
|
|
@@ -2079,6 +2368,7 @@ var IngestionService = class {
|
|
|
2079
2368
|
const replay = await this.tx(async (db) => {
|
|
2080
2369
|
await this.lock(db, frozen.itemId);
|
|
2081
2370
|
await this.item(frozen.itemId, "process", db);
|
|
2371
|
+
await this.assertUnresolved(db, frozen.itemId);
|
|
2082
2372
|
const found = (await this.rows(db, "intake_analyses", "item_id=?", [frozen.itemId])).find((row) => object(row.data).requestKey === requestKey);
|
|
2083
2373
|
if (!found) return null;
|
|
2084
2374
|
if (digest(object(object(found.data).configuration).humanCorrection) !== digest(correction)) throw new Error("Split request conflict");
|
|
@@ -2351,7 +2641,7 @@ var IngestionService = class {
|
|
|
2351
2641
|
return this.tx(async (db) => {
|
|
2352
2642
|
await this.lock(db, itemId);
|
|
2353
2643
|
const item = await this.item(itemId, "process", db);
|
|
2354
|
-
if (item.cancelled || item.receipt_state !== "ready" || Number(item.analysis_revision) !== revision) return null;
|
|
2644
|
+
if (item.cancelled || item.receipt_state !== "ready" || Number(item.analysis_revision) !== revision || (await this.rows(db, "intake_dispositions", "item_id=?", [itemId])).length) return null;
|
|
2355
2645
|
const [analysis] = await this.rows(db, "intake_analyses", "item_id=? AND revision=?", [itemId, revision]);
|
|
2356
2646
|
if (!analysis || ["completed", "partial"].includes(String(analysis.state))) return null;
|
|
2357
2647
|
const limits = object(object(item.data).limits);
|
|
@@ -2615,10 +2905,12 @@ var IngestionService = class {
|
|
|
2615
2905
|
/** Mint an action only on an explicit host intention, never during reanalysis. */
|
|
2616
2906
|
async createAction(itemId, actionKey, provenance) {
|
|
2617
2907
|
nonempty(actionKey);
|
|
2908
|
+
if (actionKey.startsWith("reviewer:") || provenance.origin === "reviewer-selected") throw new Error("Reserved action provenance");
|
|
2618
2909
|
const payload = canonical(provenance);
|
|
2619
2910
|
return this.tx(async (db) => {
|
|
2620
2911
|
await this.lock(db, itemId);
|
|
2621
2912
|
if ((await this.item(itemId, "process", db)).cancelled) throw new Error("Intake cancelled");
|
|
2913
|
+
await this.assertUnresolved(db, itemId);
|
|
2622
2914
|
const [existing] = await this.rows(db, "intake_actions", "item_id=? AND action_key=?", [itemId, actionKey]);
|
|
2623
2915
|
if (existing) {
|
|
2624
2916
|
if (canonical(object(existing.data)) !== payload) throw new Error("Action identity conflict");
|
|
@@ -2645,7 +2937,9 @@ var IngestionService = class {
|
|
|
2645
2937
|
await this.lock(db, input.itemId);
|
|
2646
2938
|
const item = await this.item(input.itemId, "process", db);
|
|
2647
2939
|
if (item.cancelled) throw new Error("Intake cancelled");
|
|
2940
|
+
await this.assertUnresolved(db, input.itemId);
|
|
2648
2941
|
const [action] = await this.rows(db, "intake_actions", "item_id=? AND id=?", [input.itemId, input.actionId]);
|
|
2942
|
+
if (action && object(action.data).origin === "reviewer-selected") throw new Error("Reviewer-selected action requires review");
|
|
2649
2943
|
const [attempt] = await this.rows(db, "intake_analysis_attempts", "item_id=? AND id=?", [input.itemId, input.attemptId]);
|
|
2650
2944
|
if (!action || !attempt || !["completed", "partial"].includes(String(attempt.state)) || !attempt.output_digest) throw new Error("Invalid proposal evidence");
|
|
2651
2945
|
if (Number(action.proposal_revision) !== input.expectedRevision || [
|
|
@@ -2773,7 +3067,10 @@ var IngestionService = class {
|
|
|
2773
3067
|
asset.sourceUri = String(locator.sourceUri);
|
|
2774
3068
|
await this.options.assets.store.removeFile(asset);
|
|
2775
3069
|
}
|
|
2776
|
-
if (intent.state === "completed")
|
|
3070
|
+
if (intent.state === "completed") {
|
|
3071
|
+
await this.releaseRedactedPrincipal(itemId);
|
|
3072
|
+
continue;
|
|
3073
|
+
}
|
|
2777
3074
|
await this.options.purgeDerived({
|
|
2778
3075
|
tenantId: this.scope.tenantId,
|
|
2779
3076
|
itemId
|
|
@@ -2784,13 +3081,35 @@ var IngestionService = class {
|
|
|
2784
3081
|
const item = await this.item(itemId, "delete", db, true);
|
|
2785
3082
|
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);
|
|
2786
3083
|
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);
|
|
2787
|
-
await db.query(
|
|
3084
|
+
for (const [table, column] of REDACTED_DIGESTS) await db.query(`UPDATE ${table} SET ${column}=? WHERE tenant_id=? AND confidential_scope_id=? AND item_id=?`, REDACTED, this.scope.tenantId, this.scope.confidentialScopeId, itemId);
|
|
3085
|
+
await db.query("UPDATE intake_items SET data=?,trace_id='',submitter_id='',owner_key='',payload_digest=?,scope_digest=? WHERE tenant_id=? AND confidential_scope_id=? AND id=?", canonical({ capturedCeiling: redactedCeiling(object(object(item.data).capturedCeiling ?? {}), await this.hasUnfinishedActions(db, itemId)) }), REDACTED, REDACTED, this.scope.tenantId, this.scope.confidentialScopeId, itemId);
|
|
2788
3086
|
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);
|
|
2789
3087
|
});
|
|
2790
3088
|
completed++;
|
|
2791
3089
|
}
|
|
2792
3090
|
return completed;
|
|
2793
3091
|
}
|
|
3092
|
+
async hasUnfinishedActions(db, itemId) {
|
|
3093
|
+
return (await this.rows(db, "intake_actions", `item_id=? AND state IN (${UNFINISHED_ACTION_STATES.map(() => "?").join(",")})`, [itemId, ...UNFINISHED_ACTION_STATES])).length > 0;
|
|
3094
|
+
}
|
|
3095
|
+
/** Later sweeps drop a principal retained for a reconciliation that has
|
|
3096
|
+
* since reached a final state. */
|
|
3097
|
+
async releaseRedactedPrincipal(itemId) {
|
|
3098
|
+
await this.tx(async (db) => {
|
|
3099
|
+
await this.lock(db, itemId);
|
|
3100
|
+
const [item] = await this.rows(db, "intake_items", "id=?", [itemId]);
|
|
3101
|
+
if (!item || item.visibility === "active") return;
|
|
3102
|
+
if (await this.hasUnfinishedActions(db, itemId)) return;
|
|
3103
|
+
const data = object(item.data);
|
|
3104
|
+
const ceiling = object(data.capturedCeiling ?? {});
|
|
3105
|
+
const released = redactedCeiling(ceiling, false);
|
|
3106
|
+
if (canonical(released) === canonical(ceiling)) return;
|
|
3107
|
+
await db.query("UPDATE intake_items SET data=? WHERE tenant_id=? AND confidential_scope_id=? AND id=?", canonical({
|
|
3108
|
+
...data,
|
|
3109
|
+
capturedCeiling: released
|
|
3110
|
+
}), this.scope.tenantId, this.scope.confidentialScopeId, itemId);
|
|
3111
|
+
});
|
|
3112
|
+
}
|
|
2794
3113
|
/** Run host-configured expiry policy and retry interrupted deletion propagation. */
|
|
2795
3114
|
async sweepRetention() {
|
|
2796
3115
|
const expired = await this.rows(this.db, "intake_items", "visibility='active' AND expires_at<=?", [this.now().toISOString()]);
|
|
@@ -2807,6 +3126,6 @@ var IngestionService = class {
|
|
|
2807
3126
|
}
|
|
2808
3127
|
};
|
|
2809
3128
|
//#endregion
|
|
2810
|
-
export { EmailSourceAdapter, GenerationSnapshotStaleError, GenerationSourceStaleError, HandlerArgumentsInvalidError, ITEM_ID_FILTER_CEILING, ITEM_PAGE_CEILING, ITEM_SCAN_CEILING, IngestionService, IntakeScanLimitError, REVIEWER_INSTRUCTION_MAX_LENGTH, ReviewerInstructionInvalidError, ReviewerInstructionStaleError, WatchFolderSourceAdapter, createIntakeReviewHost, createSDKExtractionAdapter, createSourceDeliveryHandler, createVendorWebhookHandler, extractAnalysis, isIntakeId, proposeDocumentSplits, reviewerInstructionView };
|
|
3129
|
+
export { EmailSourceAdapter, GenerationSnapshotStaleError, GenerationSourceStaleError, HandlerArgumentsInvalidError, ITEM_ID_FILTER_CEILING, ITEM_PAGE_CEILING, ITEM_SCAN_CEILING, IngestionService, IntakeScanLimitError, ProposalsNotConfiguredError, REVIEWER_INSTRUCTION_MAX_LENGTH, ReviewerInstructionInvalidError, ReviewerInstructionStaleError, WatchFolderSourceAdapter, createIntakeReviewHost, createSDKExtractionAdapter, createSourceDeliveryHandler, createVendorWebhookHandler, extractAnalysis, isIntakeId, proposeDocumentSplits, reviewerInstructionView };
|
|
2811
3130
|
|
|
2812
3131
|
//# sourceMappingURL=server.js.map
|