@happyvertical/smrt-ingestion 0.60.5 → 0.60.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.
package/FEEDBACK.md CHANGED
@@ -220,6 +220,13 @@ pending actions; regenerate their proposals and obtain fresh approval to recover
220
220
  A preview replay checks the exact persisted proposal it returns, including its
221
221
  analysis attempt; a newer action revision cannot supply replacement lineage.
222
222
  Plan replay and final sealed plan-step publication use the same gate.
223
+ Reading examples (retrieval and every selection re-check) takes no exclusive
224
+ lock on a source item (#4651): each source is loaded before and after its
225
+ checks, both loads must agree (review binding and analysis revision), and a
226
+ retention narrowing writes the row only when it actually narrows. Parallel
227
+ reads therefore cannot deadlock on PostgreSQL by visiting source items in
228
+ different orders. A database error inside an example check is never treated as
229
+ an excluded example: it propagates and ends the read.
223
230
  For already successful actions, current-authorized durable outcomes remain
224
231
  readable/replayable, but invalid learned lineage suppresses saved proposal
225
232
  display, arguments and plan payload. An empty selection carries no learned source
package/PROPOSALS.md CHANGED
@@ -497,6 +497,13 @@ like `reviewSuggestion`, never enter frozen configuration, digests or
497
497
  `proposalReadConfiguration`. A reviewer can still re-run a finally failed item
498
498
  with "Try again" (REVIEW.md, `retryInterpretation`).
499
499
 
500
+ Independently of `retry`, a PostgreSQL deadlock (`40P01`) or serialization
501
+ failure (`40001`) in the database reads around the provider call (candidate
502
+ discovery, feedback retrieval and the provider-boundary re-checks) re-runs that
503
+ read up to five times with a short jittered pause (#4651). It is never
504
+ published as a failed attempt: one that outlasts those runs is thrown from
505
+ `generate`, leaving the attempt unpublished for the next lease claim.
506
+
500
507
  ### Failed-attempt capture (opt-in, #4285)
501
508
 
502
509
  The value-free reason cannot show what the model actually sent. A host that
@@ -6685,6 +6685,9 @@ function strings(value) {
6685
6685
  function time(value) {
6686
6686
  return new Date(value).getTime();
6687
6687
  }
6688
+ function byItem([a], [b]) {
6689
+ return a < b ? -1 : a > b ? 1 : 0;
6690
+ }
6688
6691
  var IntakeExecutionService = class IntakeExecutionService {
6689
6692
  constructor(options, config) {
6690
6693
  this.options = options;
@@ -6718,14 +6721,14 @@ var IntakeExecutionService = class IntakeExecutionService {
6718
6721
  } catch (error) {
6719
6722
  if (error instanceof RetentionExpired) intents.set(error.itemId, Math.min(intents.get(error.itemId) ?? Infinity, error.deadline));
6720
6723
  if (intents.size) await withEmbeddedWriteTransaction(this.db, isEmbeddedDatabase(this.db), async (db) => {
6721
- for (const [itemId, deadline] of intents) await this.restoreRetention(db, itemId, deadline);
6724
+ for (const [itemId, deadline] of [...intents].sort(byItem)) await this.restoreRetention(db, itemId, deadline);
6722
6725
  });
6723
6726
  throw error;
6724
6727
  }
6725
6728
  }
6726
6729
  async restoreRetention(db, itemId, deadline) {
6727
6730
  const expiry = new Date(deadline).toISOString();
6728
- const item = (await db.query("UPDATE intake_items SET expires_at=CASE WHEN expires_at>? THEN ? ELSE expires_at END WHERE id=? AND tenant_id=? AND confidential_scope_id=? RETURNING id,expires_at,visibility", expiry, expiry, itemId, this.scope.tenantId, this.scope.confidentialScopeId)).rows[0];
6731
+ const item = (await db.query("UPDATE intake_items SET expires_at=? WHERE id=? AND tenant_id=? AND confidential_scope_id=? AND expires_at>? RETURNING id,expires_at,visibility", expiry, itemId, this.scope.tenantId, this.scope.confidentialScopeId, expiry)).rows[0] ?? (await db.query("SELECT id,expires_at,visibility FROM intake_items WHERE id=? AND tenant_id=? AND confidential_scope_id=?", itemId, this.scope.tenantId, this.scope.confidentialScopeId)).rows[0];
6729
6732
  if (!item) throw new IntakeAccessDeniedError();
6730
6733
  if (!this.retained(item)) await this.redactExecution(db, item);
6731
6734
  return item;
@@ -6741,13 +6744,18 @@ var IntakeExecutionService = class IntakeExecutionService {
6741
6744
  "intake_dispositions"
6742
6745
  ]) await db.query(`UPDATE ${table} SET data='{}' WHERE item_id=? AND tenant_id=? AND confidential_scope_id=?`, item.id, this.scope.tenantId, this.scope.confidentialScopeId);
6743
6746
  }
6744
- async narrowRetention(db, item, deadline) {
6745
- const current = await this.item(db, String(item.id), true, true);
6747
+ async narrowRetention(db, item, deadline, exclusive = true) {
6748
+ const current = await this.item(db, String(item.id), exclusive, true);
6746
6749
  const effective = Math.min(time(current.expires_at), deadline);
6747
6750
  if (!Number.isFinite(effective)) throw new Error("Invalid retention deadline");
6748
- if (effective < time(current.expires_at)) await db.query("UPDATE intake_items SET expires_at=? WHERE id=? AND tenant_id=? AND confidential_scope_id=?", new Date(effective).toISOString(), item.id, this.scope.tenantId, this.scope.confidentialScopeId);
6749
- item.expires_at = new Date(effective).toISOString();
6750
- item.visibility = current.visibility;
6751
+ let stored = current;
6752
+ if (effective < time(current.expires_at)) {
6753
+ const expiry = new Date(effective).toISOString();
6754
+ await db.query(`UPDATE intake_items SET expires_at=? WHERE id=? AND tenant_id=? AND confidential_scope_id=?${exclusive ? "" : " AND expires_at>?"}`, expiry, item.id, this.scope.tenantId, this.scope.confidentialScopeId, ...exclusive ? [] : [expiry]);
6755
+ if (!exclusive) stored = await this.item(db, String(item.id), false, true);
6756
+ }
6757
+ item.expires_at = new Date(Math.min(effective, time(stored.expires_at))).toISOString();
6758
+ item.visibility = stored.visibility;
6751
6759
  if (!this.retained(item)) await this.redactExecution(db, item);
6752
6760
  }
6753
6761
  async rows(db, table, where = "", params = []) {
@@ -6856,13 +6864,13 @@ var IntakeExecutionService = class IntakeExecutionService {
6856
6864
  deadline
6857
6865
  };
6858
6866
  }
6859
- async access(db, item, handler, operation) {
6867
+ async access(db, item, handler, operation, exclusive = true) {
6860
6868
  const { current, ceiling, policy, deadline } = await this.authority(db, item, handler, operation);
6861
6869
  const enforce = async (executor) => {
6862
6870
  const intents = this.retentionIntents.getStore();
6863
6871
  const itemId = String(item.id);
6864
6872
  intents?.set(itemId, Math.min(intents.get(itemId) ?? Infinity, deadline));
6865
- await this.narrowRetention(executor, item, deadline);
6873
+ await this.narrowRetention(executor, item, deadline, exclusive);
6866
6874
  };
6867
6875
  if (db === this.db) await this.tx(enforce);
6868
6876
  else await enforce(db);
@@ -6892,8 +6900,8 @@ var IntakeExecutionService = class IntakeExecutionService {
6892
6900
  permissions: intersect(strings(current.permissions), ceiling.permissions)
6893
6901
  };
6894
6902
  }
6895
- async principal(db, item, handler, operation, work) {
6896
- const access = await this.access(db, item, handler, operation);
6903
+ async principal(db, item, handler, operation, work, exclusive = true) {
6904
+ const access = await this.access(db, item, handler, operation, exclusive);
6897
6905
  const registered = ObjectRegistry.getClassByQualifiedName(handler.operation.model);
6898
6906
  const tool = `${registered.collection}.${handler.operation.action}`;
6899
6907
  return executeAsPrincipal({
@@ -6937,23 +6945,23 @@ var IntakeExecutionService = class IntakeExecutionService {
6937
6945
  id: id2
6938
6946
  })), requireReview);
6939
6947
  const result = await work(db, authorize);
6940
- for (const [retainedItemId, deadline] of this.retentionIntents.getStore() ?? []) {
6948
+ for (const [retainedItemId, deadline] of [...this.retentionIntents.getStore() ?? []].sort(byItem)) {
6941
6949
  const item = await this.restoreRetention(db, retainedItemId, deadline);
6942
6950
  if (!this.retained(item)) throw new RetentionExpired(retainedItemId, deadline);
6943
6951
  }
6944
6952
  return result;
6945
6953
  }));
6946
6954
  }
6947
- async discoveryContext(db, itemId, handlerId, handlerVersion, work, requireReview = false, revalidate) {
6955
+ async discoveryContext(db, itemId, handlerId, handlerVersion, work, requireReview = false, revalidate, exclusive = true) {
6948
6956
  const review = async () => {
6949
- const item = await this.item(db, itemId, true);
6950
- await this.access(db, item, this.handler(handlerId, handlerVersion), "review");
6957
+ const item = await this.item(db, itemId, exclusive);
6958
+ await this.access(db, item, this.handler(handlerId, handlerVersion), "review", exclusive);
6951
6959
  };
6952
6960
  const authorize = async (callback) => {
6953
- const item = await this.item(db, itemId, true);
6961
+ const item = await this.item(db, itemId, exclusive);
6954
6962
  const handler = this.handler(handlerId, handlerVersion);
6955
- if ("execution" in handler) return this.principal(db, item, this.operation(handler), "preview", callback);
6956
- const { context } = await this.access(db, item, handler, "preview");
6963
+ if ("execution" in handler) return this.principal(db, item, this.operation(handler), "preview", callback, exclusive);
6964
+ const { context } = await this.access(db, item, handler, "preview", exclusive);
6957
6965
  const resolved = await resolvePlaybook(handler.operation.playbookKey, {
6958
6966
  db,
6959
6967
  tenantId: this.scope.tenantId,
@@ -6969,7 +6977,7 @@ var IntakeExecutionService = class IntakeExecutionService {
6969
6977
  if (operation.kind !== "operation") throw new Error("Playbook unavailable");
6970
6978
  const found = this.config.handlers.find((candidate) => "execution" in candidate && candidate.operation.model === operation.model && candidate.operation.action === operation.action);
6971
6979
  if (!found) throw new Error("Handler unavailable");
6972
- await this.principal(db, item, this.operation(found), "preview", async () => {});
6980
+ await this.principal(db, item, this.operation(found), "preview", async () => {}, exclusive);
6973
6981
  }
6974
6982
  return callback(context);
6975
6983
  };
@@ -7045,15 +7053,15 @@ var IntakeExecutionService = class IntakeExecutionService {
7045
7053
  }
7046
7054
  /** Same-executor current review/evidence/target gate for the owning feedback ledger. */
7047
7055
  /** Join an existing owning transaction; its outer boundary restores retention after rollback. */
7048
- [feedbackDiscoveryContext](db, itemId, handlerId, handlerVersion, work, revalidate) {
7049
- const run = (executor) => this.discoveryContext(executor, itemId, handlerId, handlerVersion, work, false, revalidate);
7056
+ [feedbackDiscoveryContext](db, itemId, handlerId, handlerVersion, work, revalidate, exclusive = true) {
7057
+ const run = (executor) => this.discoveryContext(executor, itemId, handlerId, handlerVersion, work, false, revalidate, exclusive);
7050
7058
  return db ? run(db) : this.tx(run);
7051
7059
  }
7052
- [feedbackActionTransaction](actionId, reviewer, work, executor, requiredProviders, proposalId, targetRevisions = true) {
7060
+ [feedbackActionTransaction](actionId, reviewer, work, executor, requiredProviders, proposalId, targetRevisions = true, exclusive = true) {
7053
7061
  const operation = async (db) => {
7054
7062
  const load = async () => {
7055
- const bound = await this.bound(db, actionId, true, false, proposalId);
7056
- const { context, permissions } = await this.access(db, bound.item, bound.handler, reviewer ? "review" : "read");
7063
+ const bound = await this.bound(db, actionId, exclusive, false, proposalId);
7064
+ const { context, permissions } = await this.access(db, bound.item, bound.handler, reviewer ? "review" : "read", exclusive);
7057
7065
  if (requiredProviders?.some((provider) => !context.policy.providers.includes(provider))) throw new Error("Feedback source provider unavailable");
7058
7066
  await this.verifyEvidence(db, bound);
7059
7067
  for (const target of bound.binding.targetPreconditions) await context.assertTarget(target.model, target.id, bound.action.state === "succeeded" || !targetRevisions ? void 0 : target.revision);
@@ -7063,7 +7071,7 @@ var IntakeExecutionService = class IntakeExecutionService {
7063
7071
  const data2 = object(plan.data);
7064
7072
  await this.discoveryContext(db, String(bound.item.id), String(data2.handlerId), String(data2.handlerVersion), async (parent) => {
7065
7073
  for (const target of object(data2.preview).targetPreconditions) await parent.assertTarget(target.model, target.id, bound.action.state === "succeeded" ? void 0 : target.revision);
7066
- }, reviewer);
7074
+ }, reviewer, void 0, exclusive);
7067
7075
  }
7068
7076
  const [analysis] = await this.rows(db, "intake_analyses", "id=?", [bound.proposal.analysis_id]);
7069
7077
  const [attempt] = await this.rows(db, "intake_analysis_attempts", "id=?", [bound.proposal.analysis_attempt_id]);
@@ -7109,7 +7117,7 @@ var IntakeExecutionService = class IntakeExecutionService {
7109
7117
  const before = await load();
7110
7118
  const result = await work(before);
7111
7119
  const after = await load();
7112
- if (intakeBindingDigest(before.review) !== intakeBindingDigest(after.review)) throw new Error("Feedback binding changed");
7120
+ if (intakeBindingDigest(before.review) !== intakeBindingDigest(after.review) || !exclusive && before.context.analysisRevision !== after.context.analysisRevision) throw new Error("Feedback binding changed");
7113
7121
  if (after.permissions.some((permission) => !before.permissions.includes(permission))) throw new Error("Feedback cannot widen permissions");
7114
7122
  const prior = before.context.policy, next = after.context.policy;
7115
7123
  for (const key of [
@@ -7144,7 +7152,7 @@ var IntakeExecutionService = class IntakeExecutionService {
7144
7152
  if (!Array.isArray(references)) throw new Error("Feedback lineage unavailable");
7145
7153
  if (references.length === 0) return;
7146
7154
  if (!this.options.feedback) throw new Error("Feedback configuration unavailable");
7147
- const { IntakeFeedbackService } = await import("./feedback-hDSMYjiG.js").then((n) => n.r);
7155
+ const { IntakeFeedbackService } = await import("./feedback-CSi5pVUI.js").then((n) => n.r);
7148
7156
  await new IntakeFeedbackService(this.options, this).assertActionLineage(String(bound.action.id), db, String(bound.proposal.id));
7149
7157
  }
7150
7158
  async getAction(actionId) {
@@ -8729,4 +8737,4 @@ async function continueIntakeReview(service, actionId, task) {
8729
8737
  //#endregion
8730
8738
  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 };
8731
8739
 
8732
- //# sourceMappingURL=execution-Clcmi0sj.js.map
8740
+ //# sourceMappingURL=execution-ySzcTXW3.js.map