@happyvertical/smrt-ingestion 0.57.28 → 0.57.30

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 (38) hide show
  1. package/PROPOSALS.md +115 -8
  2. package/README.md +12 -0
  3. package/REVIEW.md +9 -0
  4. package/dist/chunks/{execution-Dv3G0d62.js → execution-BxkUOtLf.js} +119 -3
  5. package/dist/chunks/execution-BxkUOtLf.js.map +1 -0
  6. package/dist/chunks/{feedback-PLOZth0a.js → feedback-D69e1hX5.js} +3 -3
  7. package/dist/chunks/{feedback-PLOZth0a.js.map → feedback-D69e1hX5.js.map} +1 -1
  8. package/dist/chunks/proposal-contracts-CqVVPvqq.js.map +1 -1
  9. package/dist/chunks/{reviewer-instruction-zn11aVA5.js → reviewer-instruction-Ip4W4f0j.js} +2 -2
  10. package/dist/chunks/{reviewer-instruction-zn11aVA5.js.map → reviewer-instruction-Ip4W4f0j.js.map} +1 -1
  11. package/dist/execution-internal.d.ts +4 -0
  12. package/dist/execution-internal.d.ts.map +1 -1
  13. package/dist/execution.d.ts +7 -0
  14. package/dist/execution.d.ts.map +1 -1
  15. package/dist/execution.js +1 -1
  16. package/dist/failure-capture.d.ts +73 -0
  17. package/dist/failure-capture.d.ts.map +1 -0
  18. package/dist/manifest.json +1 -1
  19. package/dist/models.js +1 -1
  20. package/dist/models.js.map +1 -1
  21. package/dist/proposal-contracts.d.ts +8 -0
  22. package/dist/proposal-contracts.d.ts.map +1 -1
  23. package/dist/proposal-dto.d.ts +27 -0
  24. package/dist/proposal-dto.d.ts.map +1 -1
  25. package/dist/proposal-sdk.d.ts.map +1 -1
  26. package/dist/proposals.d.ts +2 -0
  27. package/dist/proposals.d.ts.map +1 -1
  28. package/dist/proposals.js +188 -11
  29. package/dist/proposals.js.map +1 -1
  30. package/dist/provider-failure.d.ts +15 -0
  31. package/dist/provider-failure.d.ts.map +1 -1
  32. package/dist/server.d.ts +15 -0
  33. package/dist/server.d.ts.map +1 -1
  34. package/dist/server.js +39 -6
  35. package/dist/server.js.map +1 -1
  36. package/dist/smrt-knowledge.json +14 -14
  37. package/package.json +17 -17
  38. package/dist/chunks/execution-Dv3G0d62.js.map +0 -1
package/PROPOSALS.md CHANGED
@@ -314,14 +314,121 @@ A generator call that throws ends the attempt as `provider_error` with the
314
314
  `unavailable` warning and `provenance.failure` `{category: 'unavailable',
315
315
  reason}`. The reason is value-free by construction: a provider's own text can
316
316
  quote the prompt or document, so it is never copied. The reason is built only
317
- from a fixed phrase and, for an SDK `AIError` only, the HTTP status (from a
318
- structured field, or from the exact `<status> ...` / `TypeSafe request failed
319
- (<status>): ...` shapes the SDK produces), the SDK error code (from the closed
320
- list of codes the SDK raises) and, for a schema rejection, quoted tokens that
321
- are JSON Schema keywords. Any other error contributes at most a structured
322
- numeric status. For example: `provider rejected the response schema (HTTP 400,
323
- code API_ERROR); unsupported response-schema keywords: uniqueItems`, or just
324
- `provider call failed`. It is cut at 500 characters.
317
+ from a fixed phrase and:
318
+
319
+ - for an SDK `AIError` only, the HTTP status (from a structured field, or from
320
+ the exact `<status> ...` / `TypeSafe request failed (<status>): ...` shapes
321
+ the SDK produces), the SDK error code (from the closed list of codes the SDK
322
+ raises), for a schema rejection the quoted tokens that are JSON Schema
323
+ keywords, and the OpenAI SDK's exact fixed transport messages
324
+ (`Connection error.`, `Request timed out.`, `Request was aborted.`,
325
+ `(no status code or body)`), which its `mapError` keeps without a cause;
326
+ - for the SDK adapter's own refusal of a reply it did receive, a fixed problem:
327
+ finish reason other than `stop` (named from the closed list `stop`, `length`,
328
+ `tool_calls`, `content_filter`, otherwise `other`), truncation, tool calls,
329
+ an unexpected response model, no text content, an empty reply, a reply over
330
+ the output limit, or a reply that is not valid JSON (#4285);
331
+ - for any other error (#4285), the code-defined constructor name (an
332
+ identifier ending in `Error` or `Exception`; never the settable `name`),
333
+ an `AbortError`/`TimeoutError` name, a network `code` from a closed list
334
+ (`ECONNREFUSED`, `ECONNRESET`, `ETIMEDOUT`, `ENOTFOUND`, `EAI_AGAIN`,
335
+ `UND_ERR_*`, TLS certificate codes, ...) read from the error, up to three
336
+ `cause` levels and an `AggregateError`'s entries, and a structured numeric
337
+ status. A timeout or abort is named as one, except that a structured HTTP
338
+ status always wins over an abort name or network timeout code on a non-SDK
339
+ error, whether on the error itself or in its cause chain. An SDK error's
340
+ abort is read only from its code or exact transport message.
341
+
342
+ For example: `provider rejected the response schema (HTTP 400, code API_ERROR);
343
+ unsupported response-schema keywords: uniqueItems`,
344
+ `provider response incomplete (finish reason length)`,
345
+ `provider call failed (class TypeError, network ECONNREFUSED)`,
346
+ `provider call aborted or timed out (class DOMException, name TimeoutError)`, or
347
+ `provider call failed (class Error)`. It is cut at 500 characters.
348
+
349
+ **Why some failures were a bare `provider call failed` (#4285).** The SDK
350
+ adapter's own response checks threw plain `Error`s (finish reason not `stop`,
351
+ e.g. `length` when the reply reached `maxTokens`; truncation; tool calls; an
352
+ unexpected response model; non-text or oversized content) and `JSON.parse`
353
+ threw a `SyntaxError` for an empty or non-JSON reply. The reason deliberately
354
+ reads nothing from a non-SDK error, so all of these collapsed into the fixed
355
+ phrase. Transport failures through the OpenAI SDK already carried
356
+ `code API_ERROR`. The adapter now raises `ProposalResponseError` with a fixed
357
+ problem, and other non-SDK errors contribute the transport facts above. The
358
+ category is unchanged (`unavailable`).
359
+
360
+ ### Failed-attempt capture (opt-in, #4285)
361
+
362
+ The value-free reason cannot show what the model actually sent. A host that
363
+ needs that for diagnosis can opt in to keeping it:
364
+
365
+ ```ts
366
+ proposals: {
367
+ // ...
368
+ failureCapture: {
369
+ enabled: true, // required; absent or false keeps capture off
370
+ maxBytes: 16384, // default 16384, at most 65536 (UTF-8 bytes, after redaction)
371
+ redact: ({ text, source }) => hostRedaction(text), // required to retain text
372
+ },
373
+ },
374
+ ```
375
+
376
+ `ProposalConfiguration.failureCapture` is **off by default**. When enabled with
377
+ a host `redact` hook, a failed interpretation attempt (`provider_error`) keeps
378
+ one of:
379
+
380
+ - `raw_response`: the reply text the SDK adapter refused (for example invalid
381
+ JSON or a `length` finish), or
382
+ - `generated_output`: the generator's parsed output, as JSON, that failed
383
+ validation, the declared-completion check or the output limit.
384
+
385
+ Errors without model output (transport, timeouts, authority, decision
386
+ failures) capture nothing. Successful attempts never store a capture.
387
+
388
+ Without a `redact` hook, only `status: 'redaction_failed'` metadata is stored,
389
+ with `redacted: false` and no text; the candidate text is not materialized.
390
+ There is no built-in sanitizer. Hosts own the hook's semantic redaction policy;
391
+ the service cannot verify that the returned text removes every document secret.
392
+
393
+ The text, cut to the larger of the generation `maxOutputBytes` and
394
+ `maxBytes`, goes through the host `redact` hook, and the result is cut to
395
+ `maxBytes` without splitting a character (`truncated: true` when either cut
396
+ applied). NUL and unpaired surrogates are stored as U+FFFD so PostgreSQL JSON accepts the record. When
397
+ the hook throws, returns a non-string or exceeds its asynchronous wait budget,
398
+ only `status: 'redaction_failed'` is stored. The budget is the generation
399
+ `timeoutMs`, cut to half of the analysis lease still left when the hook starts
400
+ (no time left: the hook is not called). The hook runs after the authority
401
+ recheck, reserving the rest of the lease for the second recheck and publication.
402
+ This timer cannot preempt synchronous work or cancel the hook. Hosts must use
403
+ bounded, nonblocking redactors. Missing or failed redaction does not replace the
404
+ attempt's real failure or value-free reason; hook errors are discarded.
405
+
406
+ **Where it lives.** In the attempt row's `data` JSON, as `failureCapture`
407
+ beside the result fields, never inside `result`: it needs no migration, is not
408
+ part of `output_digest` or any configuration digest, never appears in
409
+ `getCompletedAnalysis` or any other snapshot, and is not public read
410
+ configuration (`proposalReadConfiguration` omits it, so read-only hosts still
411
+ match). Proposal digests, replay and frozen generation configuration are
412
+ unchanged by enabling it.
413
+
414
+ **Reading it back.** Only
415
+ `IngestionService.getFailedAttemptCapture(itemId, attemptId?)` returns it
416
+ (`FailedAttemptCapture` from `/dto`, or `null` when nothing was stored). The
417
+ attempt defaults to the item's current analysis attempt. It requires the
418
+ execution catalog and the current **reviewer** authority for that item (the
419
+ same gate as `getItemDisposition`) inside the caller's tenant and confidential
420
+ scope; every denial, expiry, cancellation or foreign item fails closed before
421
+ the row is read. It is never part of a list DTO, inbox row, review page,
422
+ log line, checkpoint name, telemetry or error message.
423
+ Publication and reads also reject a persisted `captured` record with
424
+ `redacted: false`; capture text must be marked as processed by a configured
425
+ host hook. Persisted caps must remain within 1–65536 bytes, and captured text
426
+ must fit its declared UTF-8 byte cap; malformed records fail closed.
427
+
428
+ **Retention and deletion.** It follows the item. There is no separate delete
429
+ API: `expire`, `sweepRetention` and the `repairDeletions` retry of an
430
+ interrupted propagation all empty attempt `data`, which removes the capture
431
+ with the rest of the payload; a revoked item is unreadable before that.
325
432
 
326
433
  `createSDKProposalDecisionClient(client)` adapts an existing SDK client’s optional
327
434
  capability without changing its errors or protocol. Optional `decision` uses public core `executeDecision` and the existing SDK
package/README.md CHANGED
@@ -262,6 +262,18 @@ and `maxSuggestions` are each capped at 20; `timeoutMs` is capped at 2147483647.
262
262
  Invalid limits are rejected when constructing `IngestionService`, before receipt
263
263
  or interpretation. See [REVIEW.md](REVIEW.md) for the published server review host.
264
264
 
265
+ `proposals.failureCapture` (`{ enabled, maxBytes?, redact? }`) is off by
266
+ default. With `enabled: true` and a host `redact` hook, a failed interpretation
267
+ attempt keeps the model output (or the parsed output that failed validation)
268
+ for diagnosis: cut to
269
+ `maxBytes` (default 16384, at most 65536 UTF-8 bytes) after the host
270
+ `redact` hook, stored beside the attempt result (not digested), readable only
271
+ through the reviewer-authorized `getFailedAttemptCapture(itemId, attemptId?)`
272
+ and removed with the item. A missing or failed hook stores only
273
+ `redaction_failed` metadata, never text. Hosts own the semantic redaction policy;
274
+ no built-in sanitizer is supplied. Invalid settings are rejected at construction.
275
+ See [PROPOSALS.md](PROPOSALS.md#failed-attempt-capture-opt-in-4285).
276
+
265
277
  ## Trusted internal sources
266
278
 
267
279
  A host with a trusted internal source, such as chat messages recorded in
package/REVIEW.md CHANGED
@@ -135,6 +135,15 @@ Changing it or itemId destroys the old view and fences late callback results.
135
135
  Denied refresh clears prior originals, candidates, arguments and results.
136
136
  Missing confidence is unavailable; page/time links exist only for observed
137
137
  locations. Provider errors and unresolved/partial results remain review states.
138
+ A host that enabled `proposals.failureCapture` may fetch a failed attempt's
139
+ redacted model output with `getFailedAttemptCapture(itemId, attemptId?)`.
140
+ Retaining text requires a configured host redaction hook; a missing or failed
141
+ hook yields only `redaction_failed` metadata. The host owns that redaction policy.
142
+ The getter requires current reviewer authority for that item and fails closed
143
+ otherwise;
144
+ `load`, inbox rows and review pages never include it, so a host shows it only
145
+ behind its own explicit diagnostic action (see
146
+ [PROPOSALS.md](PROPOSALS.md#failed-attempt-capture-opt-in-4285)).
138
147
 
139
148
  ## Submitter-scoped decisions
140
149
 
@@ -47,8 +47,92 @@ var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["modu
47
47
  //#region src/execution-internal.ts
48
48
  var discoveryTransaction = /* @__PURE__ */ Symbol("ingestion.discoveryTransaction");
49
49
  var generationSnapshotRead = /* @__PURE__ */ Symbol("ingestion.generationSnapshotRead");
50
+ var completeWithFailureCapture = /* @__PURE__ */ Symbol("ingestion.completeWithFailureCapture");
51
+ var analysisLeaseRemaining = /* @__PURE__ */ Symbol("ingestion.analysisLeaseRemaining");
50
52
  var listReadGate = /* @__PURE__ */ Symbol("ingestion.listReadGate");
51
53
  //#endregion
54
+ //#region src/failure-capture.ts
55
+ var FAILURE_CAPTURE_DEFAULT_MAX_BYTES = 16384;
56
+ var FAILURE_CAPTURE_MAX_BYTES = 65536;
57
+ function assertFailureCapture(config) {
58
+ if (config === void 0) return;
59
+ if (!config || typeof config !== "object" || typeof config.enabled !== "boolean" || config.maxBytes !== void 0 && (!Number.isSafeInteger(config.maxBytes) || config.maxBytes < 1 || config.maxBytes > 65536) || config.redact !== void 0 && typeof config.redact !== "function") throw new Error("Invalid failure capture configuration");
60
+ }
61
+ function truncateUtf8(text, maxBytes) {
62
+ const bytes = Buffer.from(text, "utf8");
63
+ if (bytes.length <= maxBytes) return {
64
+ text,
65
+ truncated: false
66
+ };
67
+ let end = maxBytes;
68
+ while (end > 0 && (bytes[end] & 192) === 128) end--;
69
+ return {
70
+ text: bytes.subarray(0, end).toString("utf8"),
71
+ truncated: true
72
+ };
73
+ }
74
+ function storable(text) {
75
+ return text.replaceAll(String.fromCharCode(0), "�").replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "�");
76
+ }
77
+ async function prepareFailureCapture(config, candidate, bounds) {
78
+ if (config?.enabled !== true) return void 0;
79
+ const maxBytes = config.maxBytes ?? 16384;
80
+ const hook = config.redact;
81
+ const base = {
82
+ version: 1,
83
+ source: candidate.source,
84
+ redacted: !!hook,
85
+ maxBytes,
86
+ capturedAt: bounds.now.toISOString()
87
+ };
88
+ if (!hook || bounds.timeoutMs < 1) return {
89
+ ...base,
90
+ status: "redaction_failed",
91
+ truncated: false
92
+ };
93
+ let raw;
94
+ try {
95
+ raw = candidate.text();
96
+ } catch {
97
+ return;
98
+ }
99
+ if (typeof raw !== "string" || !raw.length) return void 0;
100
+ const input = truncateUtf8(raw, Math.max(bounds.inputBytes, maxBytes));
101
+ let text = input.text;
102
+ let timer;
103
+ try {
104
+ const redacted = await Promise.race([Promise.resolve().then(() => hook.call(config, {
105
+ text: input.text,
106
+ source: candidate.source
107
+ })), new Promise((_resolve, reject) => {
108
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("timeout")), bounds.timeoutMs);
109
+ })]);
110
+ if (typeof redacted !== "string") throw new Error("invalid");
111
+ text = redacted;
112
+ } catch {
113
+ return {
114
+ ...base,
115
+ status: "redaction_failed",
116
+ truncated: false
117
+ };
118
+ } finally {
119
+ clearTimeout(timer);
120
+ }
121
+ const stored = truncateUtf8(storable(text), maxBytes);
122
+ return {
123
+ ...base,
124
+ status: "captured",
125
+ text: stored.text,
126
+ truncated: input.truncated || stored.truncated
127
+ };
128
+ }
129
+ function storedFailureCapture(value) {
130
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
131
+ const record = value;
132
+ if (record.version !== 1 || !["raw_response", "generated_output"].includes(String(record.source)) || !["captured", "redaction_failed"].includes(String(record.status)) || typeof record.truncated !== "boolean" || typeof record.redacted !== "boolean" || typeof record.maxBytes !== "number" || !Number.isSafeInteger(record.maxBytes) || record.maxBytes < 1 || record.maxBytes > 65536 || typeof record.capturedAt !== "string" || (record.status === "captured" ? typeof record.text !== "string" || record.redacted !== true || Buffer.byteLength(record.text, "utf8") > record.maxBytes : record.text !== void 0)) return void 0;
133
+ return record;
134
+ }
135
+ //#endregion
52
136
  //#region src/proposal-errors.ts
53
137
  var HandlerArgumentsInvalidError = class extends Error {
54
138
  issues;
@@ -7057,7 +7141,7 @@ var IntakeExecutionService = class IntakeExecutionService {
7057
7141
  if (!Array.isArray(references)) throw new Error("Feedback lineage unavailable");
7058
7142
  if (references.length === 0) return;
7059
7143
  if (!this.options.feedback) throw new Error("Feedback configuration unavailable");
7060
- const { IntakeFeedbackService } = await import("./feedback-PLOZth0a.js").then((n) => n.r);
7144
+ const { IntakeFeedbackService } = await import("./feedback-D69e1hX5.js").then((n) => n.r);
7061
7145
  await new IntakeFeedbackService(this.options, this).assertActionLineage(String(bound.action.id), db, String(bound.proposal.id));
7062
7146
  }
7063
7147
  async getAction(actionId) {
@@ -8222,6 +8306,38 @@ var IntakeExecutionService = class IntakeExecutionService {
8222
8306
  return row ? this.dispositionDTO(row) : void 0;
8223
8307
  });
8224
8308
  }
8309
+ /**
8310
+ * A failed attempt's opt-in diagnostic capture (#4285) under the current
8311
+ * reviewer authority for this item, inside the caller's tenant and
8312
+ * confidential scope; every denial fails closed before the row is read.
8313
+ */
8314
+ async getFailedAttemptCapture(itemId, attemptId) {
8315
+ return this.tx(async (db) => {
8316
+ const item = await this.item(db, itemId);
8317
+ await this.reviewerAccess(db, item);
8318
+ let id2 = attemptId;
8319
+ if (id2 === void 0) {
8320
+ const [analysis] = await this.rows(db, "intake_analyses", "item_id=? AND revision=?", [item.id, item.analysis_revision]);
8321
+ if (!analysis?.current_attempt_id) throw new Error("Analysis unavailable");
8322
+ id2 = String(analysis.current_attempt_id);
8323
+ }
8324
+ const [attempt] = await this.rows(db, "intake_analysis_attempts", "id=? AND item_id=?", [id2, item.id]);
8325
+ if (!attempt) throw new Error("Analysis unavailable");
8326
+ const capture = storedFailureCapture(object(attempt.data).failureCapture);
8327
+ if (!capture || !attempt.safe_error) return null;
8328
+ return {
8329
+ itemId: String(item.id),
8330
+ attemptId: String(attempt.id),
8331
+ source: capture.source,
8332
+ status: capture.status,
8333
+ ...capture.status === "captured" ? { text: capture.text } : {},
8334
+ truncated: capture.truncated,
8335
+ redacted: capture.redacted,
8336
+ maxBytes: capture.maxBytes,
8337
+ capturedAt: capture.capturedAt
8338
+ };
8339
+ });
8340
+ }
8225
8341
  /** Close an item as handled without an action. Terminal; replay returns the same disposition. */
8226
8342
  async resolveItem(input) {
8227
8343
  const frozen = JSON.parse(canonical(JSON.parse(JSON.stringify(input))));
@@ -8600,6 +8716,6 @@ async function continueIntakeReview(service, actionId, task) {
8600
8716
  return service.applyAction(actionId);
8601
8717
  }
8602
8718
  //#endregion
8603
- export { __toESM as C, __exportAll as S, HandlerArgumentsInvalidError as _, resolveIntakePolicy as a, generationSnapshotRead as b, HANDLER_ARGUMENT_ISSUE_LIMIT as c, schemaOwnedPath as d, require_ajv as f, GenerationSourceStaleError as g, GenerationSnapshotStaleError as h, intakeBindingDigest as i, HANDLER_ARGUMENT_PATH_LIMIT as l, redactedCeiling as m, IntakeExecutionService as n, feedbackActionTransaction as o, UNFINISHED_ACTION_STATES as p, continueIntakeReview as r, feedbackDiscoveryContext as s, IntakeAccessDeniedError as t, describeArgumentRefusal as u, ProposalsNotConfiguredError as v, listReadGate as x, discoveryTransaction as y };
8719
+ export { __toESM as A, storedFailureCapture as C, generationSnapshotRead as D, discoveryTransaction as E, listReadGate as O, prepareFailureCapture as S, completeWithFailureCapture as T, HandlerArgumentsInvalidError as _, resolveIntakePolicy as a, FAILURE_CAPTURE_MAX_BYTES as b, HANDLER_ARGUMENT_ISSUE_LIMIT as c, schemaOwnedPath as d, require_ajv as f, GenerationSourceStaleError as g, GenerationSnapshotStaleError as h, intakeBindingDigest as i, __exportAll as k, HANDLER_ARGUMENT_PATH_LIMIT as l, redactedCeiling as m, IntakeExecutionService as n, feedbackActionTransaction as o, UNFINISHED_ACTION_STATES as p, continueIntakeReview as r, feedbackDiscoveryContext as s, IntakeAccessDeniedError as t, describeArgumentRefusal as u, ProposalsNotConfiguredError as v, analysisLeaseRemaining as w, assertFailureCapture as x, FAILURE_CAPTURE_DEFAULT_MAX_BYTES as y };
8604
8720
 
8605
- //# sourceMappingURL=execution-Dv3G0d62.js.map
8721
+ //# sourceMappingURL=execution-BxkUOtLf.js.map