@akasecurity/ai-tc-claude-code 0.9.11 → 0.9.12

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.
@@ -50,7 +50,7 @@ var require_ignore = __commonJS({
50
50
  var REGEX_SPLITALL_CRLF = /\r?\n/g;
51
51
  var REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/;
52
52
  var REGEX_TEST_TRAILING_SLASH = /\/$/;
53
- var SLASH2 = "/";
53
+ var SLASH3 = "/";
54
54
  var TMP_KEY_IGNORE = "node-ignore";
55
55
  if (typeof Symbol !== "undefined") {
56
56
  TMP_KEY_IGNORE = /* @__PURE__ */ Symbol.for("node-ignore");
@@ -422,11 +422,11 @@ var require_ignore = __commonJS({
422
422
  if (!REGEX_TEST_TRAILING_SLASH.test(path)) {
423
423
  return this.test(path);
424
424
  }
425
- const slices = path.split(SLASH2).filter(Boolean);
425
+ const slices = path.split(SLASH3).filter(Boolean);
426
426
  slices.pop();
427
427
  if (slices.length) {
428
428
  const parent = this._t(
429
- slices.join(SLASH2) + SLASH2,
429
+ slices.join(SLASH3) + SLASH3,
430
430
  this._testCache,
431
431
  true,
432
432
  slices
@@ -442,14 +442,14 @@ var require_ignore = __commonJS({
442
442
  return cache[path];
443
443
  }
444
444
  if (!slices) {
445
- slices = path.split(SLASH2).filter(Boolean);
445
+ slices = path.split(SLASH3).filter(Boolean);
446
446
  }
447
447
  slices.pop();
448
448
  if (!slices.length) {
449
449
  return cache[path] = this._rules.test(path, checkUnignored, MODE_IGNORE);
450
450
  }
451
451
  const parent = this._t(
452
- slices.join(SLASH2) + SLASH2,
452
+ slices.join(SLASH3) + SLASH3,
453
453
  cache,
454
454
  checkUnignored,
455
455
  slices
@@ -502,6 +502,14 @@ var ATTACHED_FORWARD_DROPS_FILENAME = "attached-forward-drops.json";
502
502
  import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync, rmSync as rmSync3, statSync } from "fs";
503
503
  import { join as join2 } from "path";
504
504
 
505
+ // ../../packages/schema/src/drizzle/deferred-migrations.ts
506
+ var DEFERRED_MIGRATION_TAGS = [
507
+ "0031_audit_capture_by_time_index",
508
+ "0032_audit_capture_by_id_index",
509
+ "0033_audit_capture_location_index",
510
+ "0034_findings_read_indexes"
511
+ ];
512
+
505
513
  // ../../packages/schema/src/drizzle/sqlite-ddl.ts
506
514
  var SQLITE_MIGRATIONS = [
507
515
  {
@@ -619,6 +627,30 @@ var SQLITE_MIGRATIONS = [
619
627
  {
620
628
  tag: "0028_activity_session_probe_indexes",
621
629
  sql: "CREATE INDEX `idx_audit_session_prompt` ON `audit_events` (`root_session_id`) WHERE event_type = 'prompt';--> statement-breakpoint\nCREATE INDEX `idx_audit_session_share` ON `audit_events` (`root_session_id`) WHERE event_type = 'share';--> statement-breakpoint\nCREATE INDEX `idx_audit_ended_at` ON `audit_events` (`ended_at`,`root_session_id`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\nCREATE INDEX `idx_audit_session_ended` ON `audit_events` (`root_session_id`,`ended_at`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\n-- Expression index for the activity list's turns rollup: a live-captured\n-- session's turns are the DISTINCT `run_key` across its `llm_call` leaves, and\n-- carrying the extracted key in the index answers that count from the index\n-- alone instead of parsing every leaf's attribute bag. Written by hand, as\n-- 0013's `idx_audit_code_change_path` was: drizzle-kit cannot emit an\n-- expression containing a comma, so this index is not declared in sqlite.ts.\nCREATE INDEX `idx_audit_session_run_key` ON `audit_events` (`root_session_id`, json_extract(`attributes`, '$.run_key')) WHERE `event_type` = 'llm_call';\n"
630
+ },
631
+ {
632
+ tag: "0029_audit_capture_rollup_index",
633
+ sql: "CREATE INDEX `idx_audit_capture_rollup` ON `audit_events` (`event_type`,`started_at`,`repo`,`id`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
634
+ },
635
+ {
636
+ tag: "0030_audit_content_expiry",
637
+ sql: "ALTER TABLE `audit_events` ADD `content_expired_at` integer;--> statement-breakpoint\nCREATE INDEX `idx_audit_expirable_body` ON `audit_events` (`started_at`) WHERE content IS NOT NULL;"
638
+ },
639
+ {
640
+ tag: "0031_audit_capture_by_time_index",
641
+ sql: "CREATE INDEX `idx_audit_capture_by_time` ON `audit_events` (`started_at`,`id`,`event_type`,`root_session_id`,`source_tool`,`repo`,`file_path`,`tool_name`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
642
+ },
643
+ {
644
+ tag: "0032_audit_capture_by_id_index",
645
+ sql: "CREATE INDEX `idx_audit_capture_by_id` ON `audit_events` (`id`,`started_at`,`event_type`,`root_session_id`,`source_tool`,`repo`,`file_path`,`tool_name`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
646
+ },
647
+ {
648
+ tag: "0033_audit_capture_location_index",
649
+ sql: "CREATE INDEX `idx_audit_capture_location` ON `audit_events` (`repo`,`file_path`,`started_at`,`id`,`event_type`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
650
+ },
651
+ {
652
+ tag: "0034_findings_read_indexes",
653
+ sql: "CREATE INDEX `idx_inspection_definitions_rule` ON `inspection_definitions` (`rule_id`,`severity`,`category`);--> statement-breakpoint\nCREATE INDEX `idx_inspection_findings_def` ON `inspection_findings` (`inspection_definition_id`,`audit_event_id`);--> statement-breakpoint\nCREATE INDEX `idx_inspection_findings_event_cover` ON `inspection_findings` (`audit_event_id`,`inspection_definition_id`,`action_taken`,`finding_key`,`id`);"
622
654
  }
623
655
  ];
624
656
 
@@ -20680,6 +20712,15 @@ var FindingCategory = external_exports.enum([
20680
20712
  ]).meta({ id: "FindingCategory" });
20681
20713
  var FindingOrigin = external_exports.enum(["in-flight", "at-rest"]).meta({ id: "FindingOrigin" });
20682
20714
  var FindingStatus = external_exports.enum(["open", "handled", "resolved", "dismissed"]).meta({ id: "FindingStatus" });
20715
+ var SyncFailureReason = external_exports.enum(["deployment_refused", "payload_invalid", "detached_undelivered"]).meta({ id: "SyncFailureReason" });
20716
+ var FindingDeliveryState = external_exports.enum(["sent", "queued", "not_sent", "never_offered", "local_scan"]).meta({ id: "FindingDeliveryState" });
20717
+ var FindingDelivery = external_exports.object({
20718
+ state: FindingDeliveryState,
20719
+ // The delivery time for `sent`; the failure time for `not_sent` when recorded.
20720
+ at: external_exports.iso.datetime().optional(),
20721
+ // Only on `not_sent`, and only when a known reason was recorded.
20722
+ reason: SyncFailureReason.optional()
20723
+ }).meta({ id: "FindingDelivery" });
20683
20724
  var ResolutionMethod = external_exports.enum([
20684
20725
  "enforced-in-flight",
20685
20726
  "fixed-at-source",
@@ -20736,7 +20777,10 @@ var FindingInstance = external_exports.object({
20736
20777
  // The session that event belongs to, when it has one — the seam a
20737
20778
  // per-instance "view session" link needs. Absent for events captured
20738
20779
  // outside a session.
20739
- sessionId: external_exports.string().optional()
20780
+ sessionId: external_exports.string().optional(),
20781
+ // The delivery state of the event above (see FindingDelivery). Optional so
20782
+ // readers that do not project it stay valid.
20783
+ delivery: FindingDelivery.optional()
20740
20784
  }).meta({ id: "FindingInstance" });
20741
20785
  var FindingGroup = external_exports.object({
20742
20786
  id: external_exports.string(),
@@ -20788,7 +20832,10 @@ var FindingFacets = external_exports.object({
20788
20832
  // Host tool (attributes.tool_name). Present only on the instance-level
20789
20833
  // reads, which can filter by it; the type-level read omits the dimension
20790
20834
  // because a group spans tools.
20791
- tool: external_exports.array(FindingFacetItem).optional()
20835
+ tool: external_exports.array(FindingFacetItem).optional(),
20836
+ // Delivery states (FindingDeliveryState). Present only on the
20837
+ // instance-level reads, like `tool`.
20838
+ deployment: external_exports.array(FindingFacetItem).optional()
20792
20839
  }).meta({ id: "FindingFacets" });
20793
20840
  var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
20794
20841
  id: "FindingTypeSummary"
@@ -20899,6 +20946,8 @@ var ListFindingInstancesQuery = external_exports.object({
20899
20946
  // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
20900
20947
  // where the free-text `q` can only match the rendered "via Bash" label.
20901
20948
  tool: external_exports.array(external_exports.string()).optional(),
20949
+ // The delivery state of each finding's event (see FindingDelivery).
20950
+ deployment: external_exports.array(FindingDeliveryState).optional(),
20902
20951
  // Exact repository / file-path matches, for the drill-down out of the
20903
20952
  // locations view. A row whose event carries no repo/file matches neither.
20904
20953
  repo: external_exports.string().optional(),
@@ -20919,6 +20968,10 @@ var ListFindingInstancesResponse = external_exports.object({
20919
20968
  items: external_exports.array(FindingInstanceDetail),
20920
20969
  nextCursor: external_exports.string().nullable()
20921
20970
  }).meta({ id: "ListFindingInstancesResponse" });
20971
+ var ListFindingInstancesPage = external_exports.object({
20972
+ items: external_exports.array(FindingInstanceDetail),
20973
+ nextCursor: external_exports.string().nullable()
20974
+ }).meta({ id: "ListFindingInstancesPage" });
20922
20975
  var FindingLocationSummary = external_exports.object({
20923
20976
  // Opaque, stable, minted from the pair by encodeLocationId. It exists
20924
20977
  // because a location's identity is two values and a URL param carries one:
@@ -20961,6 +21014,8 @@ var ListFindingLocationsQuery = external_exports.object({
20961
21014
  // instances that match, and folds its status from those.
20962
21015
  status: external_exports.array(FindingStatus).optional(),
20963
21016
  tool: external_exports.array(external_exports.string()).optional(),
21017
+ // The delivery state of each finding's event (see FindingDelivery).
21018
+ deployment: external_exports.array(FindingDeliveryState).optional(),
20964
21019
  q: external_exports.string().optional(),
20965
21020
  sessionId: external_exports.string().optional(),
20966
21021
  from: external_exports.iso.datetime().optional(),
@@ -21163,6 +21218,10 @@ var CaptureAttributes = external_exports.object({
21163
21218
  // to 'allow' — the enforcement audit trail's link back to the grant that
21164
21219
  // authorized the bypass.
21165
21220
  exception_ids: external_exports.array(external_exports.guid()).optional(),
21221
+ // The persisted spellings of EventMetadata's messageId/conversationId — the
21222
+ // join back to the `llm_call` leaf for the same assistant turn.
21223
+ message_id: external_exports.string().optional(),
21224
+ conversation_id: external_exports.string().optional(),
21166
21225
  // Whole milliseconds this capture's inspection blocked its caller — the
21167
21226
  // plugin's own added latency (see EventMetadata.inspectionMs, whose value
21168
21227
  // this is). Promoted to the `inspection_ms` generated column so the facet is
@@ -21171,7 +21230,19 @@ var CaptureAttributes = external_exports.object({
21171
21230
  // inline json_extract and is not itself an optimization.
21172
21231
  // ABSENT on replayed captures (backfill / worktree scan) and on rows written
21173
21232
  // before the measurement shipped — never present as a placeholder 0.
21174
- inspection_ms: external_exports.number().int().nonnegative().optional()
21233
+ inspection_ms: external_exports.number().int().nonnegative().optional(),
21234
+ // What a `redact` this capture could not carry out became instead (see
21235
+ // EventMetadata.redactDegradedTo, whose value this is). Present only when a
21236
+ // degrade actually happened, so absence is the ordinary case rather than a
21237
+ // reader having to distinguish it from a zero.
21238
+ //
21239
+ // PER CAPTURE, while `inspection_findings.action_taken` is per finding —
21240
+ // so on a multi-finding row this does not say which finding degraded, and
21241
+ // its presence does not mean the fallback decided the capture's action. A
21242
+ // capture denied by another finding's own Block policy carries `block`
21243
+ // here too. The full statement is on EventMetadata.redactDegradedTo; it is
21244
+ // repeated rather than referenced because a store reader opens this file.
21245
+ redact_degraded_to: ActionTaken.optional()
21175
21246
  }).catchall(external_exports.unknown());
21176
21247
  var ToolCallInspection = external_exports.object({
21177
21248
  ruleId: external_exports.string().min(1),
@@ -21370,7 +21441,17 @@ var AuditEvent = external_exports.object({
21370
21441
  /** `share` to a first-party/internal destination. */
21371
21442
  internal: external_exports.boolean(),
21372
21443
  /** Event needs review (e.g. unverified egress). */
21373
- flagged: external_exports.boolean()
21444
+ flagged: external_exports.boolean(),
21445
+ /**
21446
+ * The body this event's `title` is drawn from was cleared by local body
21447
+ * expiry, so an EMPTY title here means "gone", not "never had one".
21448
+ *
21449
+ * A separate flag rather than a sentinel written into `title`: the title is
21450
+ * rendered text, and a store-layer module that invented display copy for it
21451
+ * would be choosing words the view is supposed to choose. Additive and
21452
+ * defaulted, so an older producer still validates.
21453
+ */
21454
+ bodyExpired: external_exports.boolean().default(false)
21374
21455
  }).meta({ id: "ActivityAuditEvent" });
21375
21456
  var ActivitySessionSummary = external_exports.object({
21376
21457
  id: external_exports.string(),
@@ -22711,6 +22792,12 @@ var EventMetadata = external_exports.object({
22711
22792
  // to 'allow' — the enforcement audit trail's link back to the grant that
22712
22793
  // authorized the bypass. Absent on captures where no exception applied.
22713
22794
  exceptionIds: external_exports.array(external_exports.guid()).optional(),
22795
+ // The assistant message this capture belongs to, and the conversation it sits
22796
+ // in — set by the browser extension's network capture so a stored `response`
22797
+ // row can be joined to the `llm_call` leaf describing the same turn. Absent
22798
+ // on every other capture path, which has no such id.
22799
+ messageId: external_exports.string().optional(),
22800
+ conversationId: external_exports.string().optional(),
22714
22801
  // How long THIS capture's inspection blocked its caller, in whole
22715
22802
  // milliseconds — the plugin's own added latency, NOT the LLM call it sat in
22716
22803
  // front of. Measured inside `capture()` (@akasecurity/plugin-sdk) across
@@ -22723,7 +22810,37 @@ var EventMetadata = external_exports.object({
22723
22810
  // Absent is also what every pre-measurement client writes, and what a
22724
22811
  // clock failure degrades to — a reader must treat absence as "not measured"
22725
22812
  // and never as a zero, which would read as "inspection is free".
22726
- inspectionMs: external_exports.number().int().nonnegative().optional()
22813
+ inspectionMs: external_exports.number().int().nonnegative().optional(),
22814
+ // What a `redact` this capture COULD NOT CARRY OUT became instead — the
22815
+ // workspace's `redactFallback`, applied because the field could not be
22816
+ // masked in place (a shell command, a URL, or any argument on a host whose
22817
+ // hook contract offers no rewrite channel).
22818
+ //
22819
+ // It exists because the action alone cannot say why. A finding recorded as
22820
+ // `warn` reads identically whether its detection was ASSIGNED Warn or was
22821
+ // assigned Redact on a field that could not take one — and those are
22822
+ // different facts about the same row: the first is a policy the user chose,
22823
+ // the second is a masking the host could not perform. Absent means no
22824
+ // degrade happened, which is every ordinary capture.
22825
+ //
22826
+ // TWO LIMITS a reader of a stored row has to know, because the grain here
22827
+ // is the CAPTURE while `actionTaken` is per FINDING:
22828
+ //
22829
+ // - It does not say WHICH finding degraded. A capture carrying a degraded
22830
+ // `redact` alongside a finding ASSIGNED the same action stores both
22831
+ // identically and one reason for the pair; attributing it to both
22832
+ // describes the assigned one wrongly, and to neither loses the degrade.
22833
+ // - PRESENCE IS NOT CAUSATION. The value is the action the lost redact
22834
+ // became, not the reason the capture ended as it did — a capture denied
22835
+ // by some other finding's own Block policy still carries `block` here,
22836
+ // and clearing the workspace's fallback would not have let it through.
22837
+ // Gate on the value against what a fallback can produce; never read the
22838
+ // field's presence as "this was the fallback's doing".
22839
+ //
22840
+ // Both are pinned as behaviour in @akasecurity/plugin-sdk's runtime suite.
22841
+ // Closing either means moving the reason onto the finding row, which
22842
+ // already carries its own action.
22843
+ redactDegradedTo: ActionTaken.optional()
22727
22844
  }).meta({ id: "EventMetadata" });
22728
22845
  var Event = external_exports.object({
22729
22846
  id: external_exports.guid(),
@@ -22833,7 +22950,32 @@ var RotateKeyInput = external_exports.object({
22833
22950
  confirmation: external_exports.string()
22834
22951
  });
22835
22952
 
22953
+ // ../../packages/schema/src/zod/finding-delivery.ts
22954
+ var KNOWN_REASONS = SyncFailureReason.options;
22955
+ function knownReason(value) {
22956
+ return value !== null && KNOWN_REASONS.includes(value) ? value : void 0;
22957
+ }
22958
+ function deriveFindingDelivery(row) {
22959
+ if (row.kind === "code_change") return { state: "local_scan" };
22960
+ if (row.syncedAt !== null && row.syncedAt > 0) {
22961
+ return { state: "sent", at: epochMillisToIso(row.syncedAt) };
22962
+ }
22963
+ if (row.syncedAt !== null) {
22964
+ const reason = knownReason(row.syncFailure);
22965
+ return {
22966
+ state: "not_sent",
22967
+ ...row.syncFailedAt === null ? {} : { at: epochMillisToIso(row.syncFailedAt) },
22968
+ ...reason === void 0 ? {} : { reason }
22969
+ };
22970
+ }
22971
+ if (row.outboxOwed === 1 || row.syncClaimedAt !== null) return { state: "queued" };
22972
+ return { state: "never_offered" };
22973
+ }
22974
+
22836
22975
  // ../../packages/schema/src/zod/findings-group-build.ts
22976
+ function lookupOwn(map2, key) {
22977
+ return Object.hasOwn(map2, key) ? map2[key] : void 0;
22978
+ }
22837
22979
  function toApiAction(dbVal) {
22838
22980
  const map2 = {
22839
22981
  log: "monitored",
@@ -22842,7 +22984,7 @@ function toApiAction(dbVal) {
22842
22984
  warn: "warned",
22843
22985
  allow: "allowed"
22844
22986
  };
22845
- return map2[dbVal] ?? "allowed";
22987
+ return lookupOwn(map2, dbVal) ?? "allowed";
22846
22988
  }
22847
22989
  function toApiCategory(dbVal) {
22848
22990
  if (dbVal === "code_context") return "source_code";
@@ -22850,13 +22992,18 @@ function toApiCategory(dbVal) {
22850
22992
  return parsed2.success ? parsed2.data : "custom";
22851
22993
  }
22852
22994
  function toApiProvider(sourceTool) {
22853
- return TOOL_TO_HARNESS[sourceTool] ?? HARNESS.Api;
22995
+ return lookupOwn(TOOL_TO_HARNESS, sourceTool) ?? HARNESS.Api;
22854
22996
  }
22855
- var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
22997
+ var FINDING_STATUS_PRECEDENCE = [
22998
+ "open",
22999
+ "handled",
23000
+ "dismissed",
23001
+ "resolved"
23002
+ ];
22856
23003
  function foldGroupStatus(instanceStatuses) {
22857
23004
  const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
22858
23005
  if (statuses.size === 0) return void 0;
22859
- for (const candidate of STATUS_PRECEDENCE) {
23006
+ for (const candidate of FINDING_STATUS_PRECEDENCE) {
22860
23007
  if (statuses.has(candidate)) return candidate;
22861
23008
  }
22862
23009
  return void 0;
@@ -22963,11 +23110,16 @@ function applyFindingFilters(types, opts) {
22963
23110
  }
22964
23111
  return filtered;
22965
23112
  }
22966
- var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
22967
- var SEVERITY_RANK = SEVERITY_ORDER;
23113
+ function rankByOrder(members2) {
23114
+ return Object.fromEntries(members2.map((member, index) => [member, index]));
23115
+ }
23116
+ var SEVERITY_RANK = rankByOrder(Severity.options);
23117
+ function severityRank(severity) {
23118
+ return lookupOwn(SEVERITY_RANK, severity);
23119
+ }
22968
23120
  function compareFindingGroupOrder(a, b) {
22969
- const rankA = SEVERITY_RANK[a.severity] ?? -1;
22970
- const rankB = SEVERITY_RANK[b.severity] ?? -1;
23121
+ const rankA = severityRank(a.severity) ?? -1;
23122
+ const rankB = severityRank(b.severity) ?? -1;
22971
23123
  const severityDiff = rankA - rankB;
22972
23124
  if (severityDiff !== 0) return severityDiff;
22973
23125
  const recencyDiff = b.latestDetectedAt.localeCompare(a.latestDetectedAt);
@@ -23042,6 +23194,20 @@ function computeFindingFacets(allTypes, opts) {
23042
23194
  }
23043
23195
 
23044
23196
  // ../../packages/schema/src/zod/findings-flat-build.ts
23197
+ function compareCodePoints(a, b) {
23198
+ const aIter = a[Symbol.iterator]();
23199
+ const bIter = b[Symbol.iterator]();
23200
+ for (; ; ) {
23201
+ const aNext = aIter.next();
23202
+ const bNext = bIter.next();
23203
+ if (aNext.done && bNext.done) return 0;
23204
+ if (aNext.done) return -1;
23205
+ if (bNext.done) return 1;
23206
+ const aPoint = aNext.value.codePointAt(0) ?? 0;
23207
+ const bPoint = bNext.value.codePointAt(0) ?? 0;
23208
+ if (aPoint !== bPoint) return aPoint - bPoint;
23209
+ }
23210
+ }
23045
23211
  function rowHaystack(row) {
23046
23212
  return [
23047
23213
  row.ruleId,
@@ -23066,6 +23232,8 @@ function matchesDimension(row, opts, dimension) {
23066
23232
  return !opts.actions?.length || opts.actions.includes(toApiAction(row.actionTaken));
23067
23233
  case "statuses":
23068
23234
  return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
23235
+ case "deliveries":
23236
+ return !opts.deliveries?.length || row.delivery !== void 0 && opts.deliveries.includes(row.delivery.state);
23069
23237
  case "tools":
23070
23238
  return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
23071
23239
  // An EMPTY value is a real filter here, not an absent one. The location
@@ -23092,6 +23260,7 @@ var DIMENSIONS = [
23092
23260
  "providers",
23093
23261
  "actions",
23094
23262
  "statuses",
23263
+ "deliveries",
23095
23264
  "tools",
23096
23265
  "repo",
23097
23266
  "file",
@@ -23105,10 +23274,19 @@ function matchesInstanceFilters(row, opts, except) {
23105
23274
  return true;
23106
23275
  }
23107
23276
  function toItems(counts) {
23108
- return [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
23277
+ return [...counts.entries()].map(([value, count]) => ({ value, count })).sort(
23278
+ (a, b) => b.count - a.count || a.value.localeCompare(b.value) || // localeCompare reports canonically-equivalent strings (an NFC and an
23279
+ // NFD spelling of the same text) as equal, so a count tie between
23280
+ // them would otherwise be ordered by whichever the Map iteration
23281
+ // produced. compareCodePoints breaks that tie deterministically, which
23282
+ // makes this a TOTAL order — not one that agrees with SQL collation,
23283
+ // which it need not: foldFacetTuples runs this same sort over grouped
23284
+ // tuples, so both paths order facets identically by construction.
23285
+ compareCodePoints(a.value, b.value)
23286
+ );
23109
23287
  }
23110
- function bump(counts, value) {
23111
- counts.set(value, (counts.get(value) ?? 0) + 1);
23288
+ function bump(counts, value, by = 1) {
23289
+ counts.set(value, (counts.get(value) ?? 0) + by);
23112
23290
  }
23113
23291
  function createInstanceFacetAccumulator(opts) {
23114
23292
  const severity = /* @__PURE__ */ new Map();
@@ -23117,6 +23295,7 @@ function createInstanceFacetAccumulator(opts) {
23117
23295
  const action = /* @__PURE__ */ new Map();
23118
23296
  const status = /* @__PURE__ */ new Map();
23119
23297
  const tool = /* @__PURE__ */ new Map();
23298
+ const deployment = /* @__PURE__ */ new Map();
23120
23299
  return {
23121
23300
  add(row) {
23122
23301
  if (matchesInstanceFilters(row, opts, "severity")) bump(severity, row.severity);
@@ -23131,6 +23310,9 @@ function createInstanceFacetAccumulator(opts) {
23131
23310
  if (row.toolName !== void 0 && matchesInstanceFilters(row, opts, "tools")) {
23132
23311
  bump(tool, row.toolName);
23133
23312
  }
23313
+ if (row.delivery !== void 0 && matchesInstanceFilters(row, opts, "deliveries")) {
23314
+ bump(deployment, row.delivery.state);
23315
+ }
23134
23316
  },
23135
23317
  facets: () => ({
23136
23318
  severity: toItems(severity),
@@ -23138,7 +23320,8 @@ function createInstanceFacetAccumulator(opts) {
23138
23320
  provider: toItems(provider),
23139
23321
  action: toItems(action),
23140
23322
  status: toItems(status),
23141
- tool: toItems(tool)
23323
+ tool: toItems(tool),
23324
+ deployment: toItems(deployment)
23142
23325
  })
23143
23326
  };
23144
23327
  }
@@ -23152,6 +23335,7 @@ function toInstanceDetail(row) {
23152
23335
  ...row.toolName === void 0 ? {} : { toolName: row.toolName },
23153
23336
  eventId: row.eventId,
23154
23337
  ...row.sessionId === void 0 ? {} : { sessionId: row.sessionId },
23338
+ ...row.delivery === void 0 ? {} : { delivery: row.delivery },
23155
23339
  ...row.user === void 0 ? {} : { user: row.user },
23156
23340
  action: toApiAction(row.actionTaken),
23157
23341
  detectedAt: row.occurredAt,
@@ -23166,12 +23350,6 @@ function toInstanceDetail(row) {
23166
23350
  policy: { id: `category:${category}`, name: category }
23167
23351
  };
23168
23352
  }
23169
- var SEVERITY_ORDER2 = {
23170
- critical: 0,
23171
- high: 1,
23172
- medium: 2,
23173
- low: 3
23174
- };
23175
23353
  function newLocationAccumulator() {
23176
23354
  return {
23177
23355
  instanceCount: 0,
@@ -23186,7 +23364,7 @@ function newLocationAccumulator() {
23186
23364
  }
23187
23365
  function addToLocation(acc, row) {
23188
23366
  acc.instanceCount += 1;
23189
- const rank = SEVERITY_ORDER2[row.severity] ?? Number.MAX_SAFE_INTEGER - 1;
23367
+ const rank = severityRank(row.severity) ?? Number.MAX_SAFE_INTEGER - 1;
23190
23368
  if (rank < acc.maxSeverityRank) {
23191
23369
  acc.maxSeverityRank = rank;
23192
23370
  acc.maxSeverity = row.severity;
@@ -23196,15 +23374,15 @@ function addToLocation(acc, row) {
23196
23374
  acc.ruleIds.add(row.ruleId);
23197
23375
  }
23198
23376
  function compareLocationOrder(a, b) {
23199
- const rankA = SEVERITY_ORDER2[a.maxSeverity] ?? -1;
23200
- const rankB = SEVERITY_ORDER2[b.maxSeverity] ?? -1;
23377
+ const rankA = severityRank(a.maxSeverity) ?? -1;
23378
+ const rankB = severityRank(b.maxSeverity) ?? -1;
23201
23379
  if (rankA !== rankB) return rankA - rankB;
23202
23380
  if (a.latestDetectedAt !== b.latestDetectedAt) {
23203
23381
  return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
23204
23382
  }
23205
- if (a.repo !== b.repo) return a.repo < b.repo ? -1 : 1;
23206
- if (a.file !== b.file) return a.file < b.file ? -1 : 1;
23207
- return 0;
23383
+ const repoDiff = compareCodePoints(a.repo, b.repo);
23384
+ if (repoDiff !== 0) return repoDiff;
23385
+ return compareCodePoints(a.file, b.file);
23208
23386
  }
23209
23387
  function encodeLocationId(repo, file2) {
23210
23388
  return `${encodePart(repo)}/${encodePart(file2)}`;
@@ -23279,6 +23457,11 @@ var Policy = external_exports.object({
23279
23457
  // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23280
23458
  provenance: PolicyProvenance.optional()
23281
23459
  }).meta({ id: "Policy" });
23460
+ var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23461
+ var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23462
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23463
+ id: "RedactFallback"
23464
+ });
23282
23465
  var PolicyBundle = external_exports.object({
23283
23466
  version: external_exports.string(),
23284
23467
  policies: external_exports.array(Policy),
@@ -23326,6 +23509,16 @@ var PolicyBundle = external_exports.object({
23326
23509
  // control plane), so no name resolution stands between the decision and the
23327
23510
  // comparison.
23328
23511
  prohibitedModels: external_exports.array(external_exports.string()).optional(),
23512
+ // What a resolved `redact` becomes on a field the host cannot rewrite, as
23513
+ // the ORGANIZATION would have it. Merged raise-only against the device's own
23514
+ // `WorkspaceSettings.redactFallback` (see strongerRedactFallback below), so
23515
+ // a control plane can tighten a machine and never loosen one — the same
23516
+ // direction `mergeRaiseOnly` enforces for policies.
23517
+ //
23518
+ // Optional so an older backend, and an older on-disk cache, still parses;
23519
+ // absent leaves the device's own setting in force, which is the behaviour
23520
+ // that predates the field and the safe direction to default.
23521
+ redactFallback: RedactFallback.optional(),
23329
23522
  customKeywords: external_exports.array(external_exports.string()),
23330
23523
  fetchedAt: external_exports.iso.datetime()
23331
23524
  }).meta({ id: "PolicyBundle" });
@@ -23355,11 +23548,6 @@ function severityFloorPolicy(category) {
23355
23548
  const peak = CATEGORY_PEAK_SEVERITY[category];
23356
23549
  return peak === "critical" || peak === "high" ? "warn" : "monitor";
23357
23550
  }
23358
- var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23359
- var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23360
- var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23361
- id: "RedactFallback"
23362
- });
23363
23551
  var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23364
23552
  var BUILTIN_POLICY_SPECS = {
23365
23553
  monitor: {
@@ -23652,7 +23840,7 @@ var VaultConsent = external_exports.object({
23652
23840
  });
23653
23841
 
23654
23842
  // ../../packages/schema/src/zod/local.ts
23655
- var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23843
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 8;
23656
23844
  var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23657
23845
  var RunMode = external_exports.enum(["standalone", "attached"]);
23658
23846
  var ControlPlaneConnection = external_exports.object({
@@ -23672,6 +23860,15 @@ var HistorySyncConsent = external_exports.object({
23672
23860
  payloadVersion: external_exports.number().int().positive(),
23673
23861
  endpoint: external_exports.string()
23674
23862
  });
23863
+ var BODY_RETENTION_DEFAULT_DAYS = 30;
23864
+ var BodyRetention = external_exports.object({
23865
+ enabled: external_exports.boolean().default(false),
23866
+ // Never 0, and the ceiling is a fat-finger guard rather than a policy
23867
+ // limit — `enabled` is the real gate. A low value cannot reach a row the
23868
+ // sync ledger still owes: the sweep's age filter only ever NARROWS a
23869
+ // candidate set that is already bounded by "delivered, or never owed".
23870
+ retainDays: external_exports.number().int().min(1).max(3650).default(BODY_RETENTION_DEFAULT_DAYS)
23871
+ }).meta({ id: "BodyRetention" });
23675
23872
  var WorkspaceSettings = external_exports.object({
23676
23873
  specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
23677
23874
  runMode: RunMode.default("standalone"),
@@ -23720,7 +23917,13 @@ var WorkspaceSettings = external_exports.object({
23720
23917
  // carry prompt/reply/tool-output text in `content`; the key name predates
23721
23918
  // both widenings. Absent until granted, and a grant for a different endpoint
23722
23919
  // or an older payload no longer counts.
23723
- historySyncConsent: HistorySyncConsent.optional()
23920
+ historySyncConsent: HistorySyncConsent.optional(),
23921
+ // Local body expiry (see BodyRetention). Off until switched on; expiring a
23922
+ // body never removes the row or its findings.
23923
+ bodyRetention: BodyRetention.default({
23924
+ enabled: false,
23925
+ retainDays: BODY_RETENTION_DEFAULT_DAYS
23926
+ })
23724
23927
  });
23725
23928
  function defaultWorkspaceSettings() {
23726
23929
  return WorkspaceSettings.parse({});
@@ -23815,12 +24018,15 @@ function toCaptureAttributes(event) {
23815
24018
  ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
23816
24019
  ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
23817
24020
  ...metadata?.inspectionMs !== void 0 ? { inspection_ms: metadata.inspectionMs } : {},
24021
+ ...metadata?.redactDegradedTo !== void 0 ? { redact_degraded_to: metadata.redactDegradedTo } : {},
23818
24022
  // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
23819
24023
  // has ever populated either), but every legacy metadata key still rides
23820
24024
  // the bag rather than being silently dropped — CaptureAttributes'
23821
24025
  // `.catchall(z.unknown())` carries the long tail.
23822
24026
  ...metadata?.model !== void 0 ? { model: metadata.model } : {},
23823
- ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
24027
+ ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {},
24028
+ ...metadata?.messageId !== void 0 ? { message_id: metadata.messageId } : {},
24029
+ ...metadata?.conversationId !== void 0 ? { conversation_id: metadata.conversationId } : {}
23824
24030
  };
23825
24031
  }
23826
24032
  function captureDefinitionVersion(finding) {
@@ -23848,13 +24054,22 @@ var ManagedSettingKey = external_exports.enum([
23848
24054
  "vaultInlineReveal",
23849
24055
  "modelJudgeConsent",
23850
24056
  "dataSharesInPlace",
23851
- "redactFallback"
24057
+ "redactFallback",
24058
+ // Pins the toggle and the day count together — see BodyRetention on why the
24059
+ // two are one unit. An administrator mandating a window wants the count
24060
+ // enforced with it, not one a user can widen while the toggle stays on.
24061
+ "bodyRetention"
23852
24062
  ]).meta({ id: "ManagedSettingKey" });
23853
24063
  function isManagedSettingKey(value) {
23854
24064
  return ManagedSettingKey.safeParse(value).success;
23855
24065
  }
23856
24066
  var ManagedSettingsValues = external_exports.object({
23857
24067
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
24068
+ // `controlPlane` and `bodyRetention` are the two nested values, and both are
24069
+ // plain, non-strict objects: a key under either that this build does not know
24070
+ // is stripped and nothing reports it. The unknown-value split in
24071
+ // ManagedSettings below classifies top-level names only, so it stops at
24072
+ // these boundaries.
23858
24073
  controlPlane: external_exports.object({
23859
24074
  endpoint: external_exports.string().min(1),
23860
24075
  label: external_exports.string().min(1).optional()
@@ -23865,7 +24080,8 @@ var ManagedSettingsValues = external_exports.object({
23865
24080
  vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23866
24081
  modelJudgeConsent: external_exports.boolean().optional(),
23867
24082
  dataSharesInPlace: external_exports.boolean().optional(),
23868
- redactFallback: RedactFallback.optional()
24083
+ redactFallback: RedactFallback.optional(),
24084
+ bodyRetention: BodyRetention.optional()
23869
24085
  }).meta({ id: "ManagedSettingsValues" });
23870
24086
  var ManagedSettings = external_exports.object({
23871
24087
  specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
@@ -23873,7 +24089,21 @@ var ManagedSettings = external_exports.object({
23873
24089
  // decision from a bug. Absent renders as a generic "your organization".
23874
24090
  organization: external_exports.string().min(1).optional(),
23875
24091
  // What the administrator pinned.
23876
- values: ManagedSettingsValues.default({}),
24092
+ //
24093
+ // Parsed as a RECORD rather than as the nested schema, and split below for
24094
+ // the same reason `lockedFields` is parsed as names: a plain `z.object`
24095
+ // drops an unrecognised key and succeeds, so a pin this build does not know
24096
+ // vanished and nothing anywhere said so. A pin with no lock is a supported
24097
+ // shape — it is a DEFAULT the user may still change — so that silence hit
24098
+ // exactly the file an administrator is most likely to write while a fleet
24099
+ // is mid-upgrade.
24100
+ //
24101
+ // Splitting here rather than calling `.strict()`: strict would REFUSE the
24102
+ // file, which is the outcome the lock half already rejected — an older
24103
+ // build then runs entirely unmanaged, every pin and lock gone. A bad KNOWN
24104
+ // value still fails, because the nested schema is re-run over the known
24105
+ // subset and its issues are re-raised on this parse.
24106
+ values: external_exports.record(external_exports.string(), external_exports.unknown()).default({}),
23877
24107
  // Which of those the user may not change. A key here with no matching value
23878
24108
  // freezes whatever the user last chose; a value with no lock is a DEFAULT
23879
24109
  // the user may still override. The two are separable on purpose.
@@ -23886,17 +24116,31 @@ var ManagedSettings = external_exports.object({
23886
24116
  // the fleets most likely to carry a version skew. A name outside the enum
23887
24117
  // is still never HONOURED: the lockable set stays explicit above.
23888
24118
  lockedFields: external_exports.array(external_exports.string()).default([])
23889
- }).transform(({ lockedFields, ...rest }) => {
24119
+ }).transform(({ lockedFields, values, ...rest }, ctx) => {
23890
24120
  const known = [];
23891
24121
  const unknown2 = [];
23892
24122
  for (const name of lockedFields) {
23893
24123
  if (isManagedSettingKey(name)) known.push(name);
23894
24124
  else unknown2.push(name);
23895
24125
  }
24126
+ const knownValues = /* @__PURE__ */ Object.create(null);
24127
+ const unknownValues = [];
24128
+ for (const [name, value] of Object.entries(values)) {
24129
+ if (Object.hasOwn(ManagedSettingsValues.shape, name)) knownValues[name] = value;
24130
+ else unknownValues.push(name);
24131
+ }
24132
+ const pinned = ManagedSettingsValues.safeParse(knownValues);
24133
+ if (!pinned.success) {
24134
+ for (const issue2 of pinned.error.issues)
24135
+ ctx.addIssue({ ...issue2, path: ["values", ...issue2.path] });
24136
+ return external_exports.NEVER;
24137
+ }
23896
24138
  return {
23897
24139
  ...rest,
24140
+ values: pinned.data,
23898
24141
  lockedFields: known,
23899
- ...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {}
24142
+ ...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {},
24143
+ ...unknownValues.length > 0 ? { unknownValueFields: unknownValues } : {}
23900
24144
  };
23901
24145
  }).meta({ id: "ManagedSettings" });
23902
24146
 
@@ -24160,7 +24404,23 @@ var SaveSettingsInput = external_exports.object({
24160
24404
  modelJudgeConsent: ModelJudgeConsentChoice,
24161
24405
  historySyncConsent: HistorySyncConsentChoice,
24162
24406
  vaultConsent: external_exports.string(),
24163
- vaultInlineReveal: external_exports.string()
24407
+ vaultInlineReveal: external_exports.string(),
24408
+ // Widened to `string` like its neighbours rather than typed as
24409
+ // `RedactFallback`, on this module's own layering rule: shape here, VALUE at
24410
+ // the call site, so the domain check receives the type it was written for.
24411
+ //
24412
+ // NOT because a narrower schema would reject differently. `parseActionInput`
24413
+ // is a `safeParse` wrapper and throws for no field schema, so either spelling
24414
+ // reaches a recoverable `{ ok: false }` and there is no rejected promise to
24415
+ // trade against. The real cost runs the other way and is the part worth
24416
+ // knowing: a value this schema admits and the domain enum then rejects lands
24417
+ // on the action's shared refusal, which names NO field, where a shape
24418
+ // rejection reaches `malformedInput` and names the schema key.
24419
+ redactFallback: external_exports.string(),
24420
+ // Shape only, the way the enum fields above are strings only: the RANGE is
24421
+ // `BodyRetention`'s and the action checks it there, so there is one place
24422
+ // that decides what a legal horizon is rather than two that can drift.
24423
+ bodyRetention: external_exports.object({ enabled: external_exports.boolean(), retainDays: external_exports.number() })
24164
24424
  });
24165
24425
  var AttachInput = external_exports.object({
24166
24426
  endpoint: external_exports.string(),
@@ -24332,6 +24592,52 @@ function reviewSeverityRank(reasons) {
24332
24592
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
24333
24593
  }
24334
24594
 
24595
+ // ../../packages/schema/src/zod/web-capture.ts
24596
+ var WebUsageSource = external_exports.enum(["site", "estimated", "none"]);
24597
+ var WebUsage = external_exports.object({
24598
+ inputTokens: external_exports.number().int().nonnegative().optional(),
24599
+ outputTokens: external_exports.number().int().nonnegative().optional(),
24600
+ cacheReadInputTokens: external_exports.number().int().nonnegative().optional(),
24601
+ cacheCreationInputTokens: external_exports.number().int().nonnegative().optional()
24602
+ });
24603
+ var WebToolCall = external_exports.object({
24604
+ toolUseId: external_exports.string().min(1),
24605
+ toolName: external_exports.string().min(1),
24606
+ target: external_exports.string().optional(),
24607
+ isError: external_exports.boolean().optional(),
24608
+ inputSize: external_exports.number().int().nonnegative().optional(),
24609
+ outputSize: external_exports.number().int().nonnegative().optional()
24610
+ });
24611
+ var WebExchange = external_exports.object({
24612
+ messageId: external_exports.string().min(1),
24613
+ startedAt: external_exports.iso.datetime(),
24614
+ model: external_exports.string().optional(),
24615
+ usage: WebUsage.optional(),
24616
+ usageSource: WebUsageSource,
24617
+ stopReason: external_exports.string().optional(),
24618
+ conversationId: external_exports.string().optional(),
24619
+ turnIndex: external_exports.number().int().nonnegative().optional(),
24620
+ toolCalls: external_exports.array(WebToolCall).default([]),
24621
+ // Absent when the adapter recovered no text. Capped by the caller at
24622
+ // RESPONSE_TEXT_MAX_BYTES; `truncated` records that the cap was reached, so a
24623
+ // short capture is never mistaken for a short reply.
24624
+ responseText: external_exports.string().optional(),
24625
+ truncated: external_exports.boolean().default(false)
24626
+ });
24627
+ var RESPONSE_TEXT_MAX_BYTES = 2 * 1024 * 1024;
24628
+ var WebCaptureStatus = external_exports.object({
24629
+ patched: external_exports.boolean(),
24630
+ live: external_exports.boolean(),
24631
+ blind: external_exports.boolean(),
24632
+ sendsSeenDom: external_exports.number().int().nonnegative(),
24633
+ exchangesSeenNet: external_exports.number().int().nonnegative(),
24634
+ parseFailures: external_exports.number().int().nonnegative(),
24635
+ unparsedBodies: external_exports.number().int().nonnegative(),
24636
+ // The adapter-declared JSON key paths that were absent from a real payload —
24637
+ // the earliest signal that a site's contract moved.
24638
+ shapeMisses: external_exports.array(external_exports.string()).default([])
24639
+ });
24640
+
24335
24641
  // ../../packages/persistence/src/paths.ts
24336
24642
  import {
24337
24643
  chmodSync,
@@ -24662,6 +24968,22 @@ function discardStore(file2, backup) {
24662
24968
  }
24663
24969
  }
24664
24970
 
24971
+ // ../../packages/persistence/src/internal/sql-functions.ts
24972
+ var utf8 = new TextDecoder();
24973
+ function akaLower(value) {
24974
+ if (value === null) return null;
24975
+ if (typeof value === "string") return value.toLowerCase();
24976
+ if (typeof value === "number" || typeof value === "bigint") return String(value).toLowerCase();
24977
+ return utf8.decode(value).toLowerCase();
24978
+ }
24979
+ function registerSqlFunctions(db) {
24980
+ db.function(
24981
+ "aka_lower",
24982
+ { deterministic: true, directOnly: true, useBigIntArguments: true },
24983
+ akaLower
24984
+ );
24985
+ }
24986
+
24665
24987
  // ../../packages/persistence/src/internal/sql-text.ts
24666
24988
  function escapeLikePattern(s) {
24667
24989
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -24746,6 +25068,11 @@ function schemaObjectExists(db, kind, name) {
24746
25068
  function indexExists(db, name) {
24747
25069
  return schemaObjectExists(db, "index", name);
24748
25070
  }
25071
+ function indexColumns(db, name) {
25072
+ if (!indexExists(db, name)) return [];
25073
+ const columns = db.prepare(`PRAGMA index_info(${name})`).all();
25074
+ return columns.map((c) => c.name).filter((c) => c !== null);
25075
+ }
24749
25076
  function columnNames(db, table2, opts) {
24750
25077
  const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
24751
25078
  const columns = db.prepare(`PRAGMA ${pragma}(${table2})`).all();
@@ -24807,178 +25134,820 @@ function mapRowsTolerant(rows, map2) {
24807
25134
  return out;
24808
25135
  }
24809
25136
 
24810
- // ../../packages/persistence/src/migrations.ts
24811
- function describeObject(object2) {
24812
- return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
24813
- }
24814
- function splitStatements(sql) {
24815
- return sql.split(/-->\s*statement-breakpoint/).map((s) => s.trim()).filter((s) => s.length > 0);
24816
- }
24817
- function createdIndexName(statement) {
24818
- const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
24819
- return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
25137
+ // ../../packages/persistence/src/internal/outbox-lane.ts
25138
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
25139
+ var OUTBOX_CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
25140
+
25141
+ // ../../packages/persistence/src/sync-failure.ts
25142
+ var SYNC_FAILURE_REASONS = SyncFailureReason.options;
25143
+ function syncFailureRejectCondition(column = "sync_failure") {
25144
+ const members2 = SYNC_FAILURE_REASONS.map((r) => `'${r}'`).join(", ");
25145
+ return `NEW.${column} IS NOT NULL AND NEW.${column} NOT IN (${members2})`;
24820
25146
  }
24821
- var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
24822
- function applyMigrations(db, file2) {
24823
- const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
24824
- db.exec(
24825
- "CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
24826
- );
24827
- const applied = new Set(
24828
- db.prepare("SELECT tag FROM migration_ledger").all().map((r) => r.tag)
24829
- );
24830
- const preLedgerStore = applied.size === 0 && legacyCount > 0;
24831
- const record2 = db.prepare(
24832
- "INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)"
24833
- );
24834
- for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
24835
- if (applied.has(migration.tag)) continue;
24836
- if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
24837
- const evidence = evidenceObjects(migration.sql);
24838
- const present = evidence.filter((o) => evidenceExists(db, o));
24839
- if (present.length > 0 && present.length < evidence.length) {
24840
- const missing = evidence.filter((o) => !present.includes(o));
24841
- const message = `sqlite migration ${migration.tag} has no ledger row, but the store already has ${present.map(describeObject).join(", ")} while missing ${missing.map(describeObject).join(", ")} \u2014 the schema diverged from the migration history; refusing to replay or skip.`;
24842
- akaWarn(message);
24843
- throw new Error(`[aka] ${message}`);
24844
- }
24845
- const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
24846
- const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
24847
- const statements = splitStatements(migration.sql);
24848
- if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
24849
- try {
24850
- withTransaction(
24851
- db,
24852
- () => {
24853
- for (const statement of statements) {
24854
- const indexName = createdIndexName(statement);
24855
- if (indexName === void 0) {
24856
- if (alreadyApplied) continue;
24857
- } else if (indexExists(db, indexName)) {
24858
- continue;
24859
- }
24860
- db.exec(statement);
24861
- }
24862
- if (wantsFkOff && !alreadyApplied) {
24863
- const violations = db.prepare("PRAGMA foreign_key_check").all();
24864
- if (violations.length > 0) {
24865
- throw new Error(
24866
- `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
24867
- );
24868
- }
24869
- }
24870
- record2.run(migration.tag, Date.now());
24871
- },
24872
- "IMMEDIATE"
24873
- );
24874
- } finally {
24875
- if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
24876
- }
25147
+
25148
+ // ../../packages/persistence/src/repositories/history-sync.ts
25149
+ var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
25150
+ var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
25151
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_TYPE_LIST;
25152
+ var COUNTED_EVENT_TYPES = [
25153
+ ...STRUCTURAL_EVENT_TYPES,
25154
+ ...OUTBOX_CAPTURE_EVENT_TYPES
25155
+ ];
25156
+ var COUNTED_TYPE_LIST = COUNTED_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
25157
+ var PARTITION_BUCKETS = `
25158
+ SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NULL THEN 1 ELSE 0 END) AS queued,
25159
+ SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NOT NULL THEN 1 ELSE 0 END) AS inProgress,
25160
+ SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS synced,
25161
+ SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
25162
+ AND sync_failure = 'deployment_refused' THEN 1 ELSE 0 END) AS refused,
25163
+ SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
25164
+ AND sync_failure = 'detached_undelivered' THEN 1 ELSE 0 END) AS detached,
25165
+ -- Spelled as what it INCLUDES rather than what it excludes, so a reason
25166
+ -- added later lands in no bucket and fails the sum assertion, instead
25167
+ -- of silently joining this one.
25168
+ SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
25169
+ AND (sync_failure IS NULL OR sync_failure = 'payload_invalid')
25170
+ THEN 1 ELSE 0 END) AS failed,
25171
+ COUNT(*) AS total`;
25172
+ var COUNTED_SCOPE = `
25173
+ WHERE event_type IN (${COUNTED_TYPE_LIST})
25174
+ AND (
25175
+ event_type IN (${TYPE_LIST})
25176
+ OR synced_at IS NOT NULL
25177
+ OR outbox_owed = 1
25178
+ )`;
25179
+ var SKIPPED = -1;
25180
+ var ROW_COLUMNS = `id,
25181
+ parent_id AS parentId,
25182
+ root_session_id AS rootSessionId,
25183
+ event_type AS eventType,
25184
+ host_id AS hostId,
25185
+ harness_id AS harnessId,
25186
+ source_project_id AS sourceProjectId,
25187
+ started_at AS startedAt,
25188
+ ended_at AS endedAt,
25189
+ severity,
25190
+ priority,
25191
+ content,
25192
+ content_hash AS contentHash,
25193
+ attributes`;
25194
+ var SqliteHistorySyncRepository = class {
25195
+ constructor(db) {
25196
+ this.db = db;
25197
+ this.ensureRowStmt = db.prepare(`INSERT OR IGNORE INTO history_sync (id) VALUES (1)`);
25198
+ this.sessionsStmt = db.prepare(
25199
+ `SELECT COALESCE(root_session_id, id) AS sessionId, MIN(started_at) AS earliest
25200
+ FROM audit_events
25201
+ WHERE synced_at IS NULL
25202
+ AND event_type IN (${TYPE_LIST})
25203
+ AND started_at < :before
25204
+ GROUP BY sessionId
25205
+ ORDER BY earliest
25206
+ LIMIT :limit`
25207
+ );
25208
+ this.rowsStmt = db.prepare(
25209
+ `SELECT ${ROW_COLUMNS}
25210
+ FROM audit_events
25211
+ WHERE synced_at IS NULL
25212
+ AND event_type IN (${TYPE_LIST})
25213
+ AND started_at < :before
25214
+ AND COALESCE(root_session_id, id) = :sessionId
25215
+ ORDER BY (event_type = 'session') DESC, started_at
25216
+ LIMIT :limit`
25217
+ );
25218
+ this.captureRowsStmt = db.prepare(
25219
+ `SELECT ${ROW_COLUMNS}
25220
+ FROM audit_events
25221
+ WHERE synced_at IS NULL
25222
+ AND sync_claimed_at IS NULL
25223
+ AND outbox_owed = 1
25224
+ AND event_type IN (${CAPTURE_TYPE_LIST})
25225
+ AND started_at < :before
25226
+ ORDER BY started_at
25227
+ LIMIT :limit`
25228
+ );
25229
+ this.markOwedStmt = db.prepare(
25230
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
25231
+ );
25232
+ this.markCaptureBacklogOwedStmt = db.prepare(
25233
+ `UPDATE audit_events SET outbox_owed = 1
25234
+ WHERE synced_at IS NULL
25235
+ AND event_type IN (${CAPTURE_TYPE_LIST})
25236
+ AND started_at < :before`
25237
+ );
25238
+ this.stampStmt = db.prepare(
25239
+ `UPDATE audit_events
25240
+ SET synced_at = :at,
25241
+ sync_claimed_at = NULL,
25242
+ sync_failed_at = :failedAt,
25243
+ sync_failure = :failure
25244
+ WHERE id = :id`
25245
+ );
25246
+ this.claimRowStmt = db.prepare(
25247
+ `UPDATE audit_events SET sync_claimed_at = :at WHERE id = :id AND synced_at IS NULL`
25248
+ );
25249
+ this.releaseRowStmt = db.prepare(
25250
+ `UPDATE audit_events SET sync_claimed_at = NULL WHERE id = :id`
25251
+ );
25252
+ this.releaseStaleClaimsStmt = db.prepare(
25253
+ `UPDATE audit_events SET sync_claimed_at = NULL
25254
+ WHERE sync_claimed_at IS NOT NULL AND sync_claimed_at < :staleBefore`
25255
+ );
25256
+ this.partitionStmt = db.prepare(`SELECT${PARTITION_BUCKETS}
25257
+ FROM audit_events${COUNTED_SCOPE}`);
25258
+ this.partitionByKindStmt = db.prepare(
25259
+ `SELECT event_type AS kind,${PARTITION_BUCKETS}
25260
+ FROM audit_events INDEXED BY idx_audit_events_sync${COUNTED_SCOPE}
25261
+ GROUP BY event_type`
25262
+ );
25263
+ this.countsStmt = db.prepare(
25264
+ `SELECT
25265
+ SUM(CASE WHEN synced_at IS NULL AND started_at < :before THEN 1 ELSE 0 END) AS pending,
25266
+ SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS sent,
25267
+ SUM(CASE WHEN synced_at = ${String(SKIPPED)}
25268
+ AND (sync_failure IS NULL OR sync_failure = 'payload_invalid')
25269
+ THEN 1 ELSE 0 END) AS skipped,
25270
+ SUM(CASE WHEN synced_at = ${String(SKIPPED)}
25271
+ AND sync_failure = 'deployment_refused' THEN 1 ELSE 0 END) AS refused,
25272
+ SUM(CASE WHEN synced_at = ${String(SKIPPED)}
25273
+ AND sync_failure = 'detached_undelivered' THEN 1 ELSE 0 END) AS detached
25274
+ FROM audit_events
25275
+ WHERE event_type IN (${TYPE_LIST})`
25276
+ );
25277
+ this.captureSkipCountStmt = db.prepare(
25278
+ // EVERY sentinel capture, whatever the reason — deliberately NOT split the
25279
+ // way the structural totals are. The split exists because a refusal is
25280
+ // terminal only against the deployment that gave it, and the structural
25281
+ // re-arm frees it on a change of deployment. The capture lane has no such
25282
+ // escape: re-arming a capture would offer one deployment's undelivered
25283
+ // prompts, with their text, to a deployment that never saw them, which is
25284
+ // exactly what disownCapturesStmt exists to prevent. So on this lane both
25285
+ // reasons mean the same thing — this row will not be sent — and splitting
25286
+ // them would put refused captures in a bucket nothing reads and nothing
25287
+ // frees.
25288
+ `SELECT COUNT(*) AS skipped
25289
+ FROM audit_events
25290
+ WHERE synced_at = ${String(SKIPPED)}
25291
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
25292
+ );
25293
+ this.fingerprintStmt = db.prepare(
25294
+ `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
25295
+ FROM history_sync WHERE id = 1`
25296
+ );
25297
+ this.setFingerprintStmt = db.prepare(
25298
+ `UPDATE history_sync
25299
+ SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
25300
+ WHERE id = 1`
25301
+ );
25302
+ this.disownCapturesStmt = db.prepare(
25303
+ `UPDATE audit_events SET outbox_owed = NULL
25304
+ WHERE outbox_owed IS NOT NULL
25305
+ AND event_type IN (${CAPTURE_TYPE_LIST})
25306
+ AND started_at < :attachedAt`
25307
+ );
25308
+ this.rearmStmt = db.prepare(
25309
+ `UPDATE audit_events
25310
+ SET synced_at = NULL, sync_failed_at = NULL, sync_failure = NULL
25311
+ WHERE (synced_at > 0
25312
+ OR sync_failure IN ('deployment_refused', 'detached_undelivered'))
25313
+ AND event_type IN (${TYPE_LIST})`
25314
+ );
25315
+ this.claimStmt = db.prepare(
25316
+ `UPDATE history_sync
25317
+ SET owner_pid = :pid, owner_host = :host, acquired_at = :now, heartbeat_at = :now
25318
+ WHERE id = 1
25319
+ AND (owner_pid IS NULL
25320
+ OR heartbeat_at IS NULL
25321
+ OR heartbeat_at < :staleBefore
25322
+ OR heartbeat_at > :now)`
25323
+ );
25324
+ this.heartbeatStmt = db.prepare(
25325
+ `UPDATE history_sync SET heartbeat_at = :now WHERE id = 1 AND owner_pid = :pid`
25326
+ );
25327
+ this.releaseStmt = db.prepare(
25328
+ `UPDATE history_sync
25329
+ SET owner_pid = NULL, owner_host = NULL, acquired_at = NULL, heartbeat_at = NULL
25330
+ WHERE id = 1 AND owner_pid = :pid`
25331
+ );
25332
+ this.closeWindowStmt = db.prepare(
25333
+ `UPDATE audit_events
25334
+ SET synced_at = ${String(SKIPPED)},
25335
+ sync_failed_at = :at,
25336
+ sync_failure = 'detached_undelivered'
25337
+ WHERE synced_at IS NULL
25338
+ AND event_type IN (${TYPE_LIST})
25339
+ AND started_at >= :attachedAt`
25340
+ );
25341
+ this.releaseBoundaryStmt = db.prepare(
25342
+ `UPDATE history_sync SET backlog_before = NULL WHERE id = 1`
25343
+ );
25344
+ this.freezeBoundaryStmt = db.prepare(
25345
+ `UPDATE history_sync SET backlog_before = :backlogBefore WHERE id = 1`
25346
+ );
25347
+ this.leaseStmt = db.prepare(
25348
+ `SELECT owner_pid AS ownerPid, owner_host AS ownerHost,
25349
+ acquired_at AS acquiredAt, heartbeat_at AS heartbeatAt
25350
+ FROM history_sync WHERE id = 1`
25351
+ );
25352
+ this.inspectionsStmt = db.prepare(
25353
+ `SELECT d.rule_id AS ruleId,
25354
+ d.name AS ruleName,
25355
+ d.version AS ruleVersion,
25356
+ d.category AS category,
25357
+ d.severity AS severity,
25358
+ f.span_start AS spanStart,
25359
+ f.span_end AS spanEnd,
25360
+ f.masked_match AS maskedMatch,
25361
+ f.action_taken AS actionTaken,
25362
+ f.confidence AS confidence
25363
+ FROM inspection_findings f
25364
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
25365
+ WHERE f.audit_event_id = :auditEventId
25366
+ ORDER BY f.span_start, f.id`
25367
+ );
24877
25368
  }
24878
- if (legacyCount < SQLITE_MIGRATIONS.length) {
24879
- db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
25369
+ db;
25370
+ ensureRowStmt;
25371
+ sessionsStmt;
25372
+ rowsStmt;
25373
+ stampStmt;
25374
+ countsStmt;
25375
+ fingerprintStmt;
25376
+ setFingerprintStmt;
25377
+ rearmStmt;
25378
+ claimStmt;
25379
+ heartbeatStmt;
25380
+ releaseStmt;
25381
+ leaseStmt;
25382
+ inspectionsStmt;
25383
+ closeWindowStmt;
25384
+ releaseBoundaryStmt;
25385
+ freezeBoundaryStmt;
25386
+ captureRowsStmt;
25387
+ markOwedStmt;
25388
+ markCaptureBacklogOwedStmt;
25389
+ captureSkipCountStmt;
25390
+ disownCapturesStmt;
25391
+ partitionStmt;
25392
+ partitionByKindStmt;
25393
+ claimRowStmt;
25394
+ releaseRowStmt;
25395
+ releaseStaleClaimsStmt;
25396
+ /**
25397
+ * The masked detections recorded against one tool call.
25398
+ *
25399
+ * These travel with the event because a tool call's target is not
25400
+ * re-inspectable from the event alone — unlike a capture, where the text
25401
+ * itself is re-scannable. What crosses is the masked match and the rule that
25402
+ * produced it, never the value.
25403
+ */
25404
+ inspectionsFor(auditEventId) {
25405
+ return allRows(this.inspectionsStmt, { auditEventId });
24880
25406
  }
24881
- ensureSyncedAtColumn(db, "audit_events");
24882
- ensureScanLedgerTable(db);
24883
- ensureHistorySyncTable(db);
24884
- ensureBlockedDetectionsTable(db);
24885
- ensureRuleProbeCacheTable(db);
24886
- ensureWriteGateTrigger(db);
24887
- ensureTokenUsageColumns(db);
24888
- reconcileSourceProjectIds(db);
24889
- if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
24890
- const drained = runLegacyHistoryBackfill(db);
24891
- if (drained) applyLegacyDropMigration(db, file2);
25407
+ /**
25408
+ * Sessions with structural rows still to send, oldest first.
25409
+ *
25410
+ * BOUNDED BY THE BACKLOG BOUNDARY, which is the whole correctness of this
25411
+ * read. Anything recorded after the machine attached is the live forward
25412
+ * path's to deliver; this drain exists for what was recorded before it, and a
25413
+ * row both paths send is at best a duplicate request and at worst — for a
25414
+ * session root — an overwrite of the inventory ids the live path resolved.
25415
+ */
25416
+ pendingSessions(limit, before) {
25417
+ return allRows(this.sessionsStmt, { limit, before }).map(
25418
+ (r) => r.sessionId
25419
+ );
24892
25420
  }
24893
- }
24894
- function readLegacyTables(db) {
24895
- let holdsRows = false;
24896
- const marks = [];
24897
- for (const table2 of ["events", "findings"]) {
24898
- try {
24899
- const row = db.prepare(`SELECT count(*) AS n, ifnull(max(rowid), -1) AS hi FROM ${table2}`).get();
24900
- if (row === void 0) {
24901
- holdsRows = true;
24902
- marks.push(`${table2}:unreadable`);
24903
- continue;
24904
- }
24905
- if (row.n > 0) holdsRows = true;
24906
- marks.push(`${table2}:${String(row.n)}:${String(row.hi)}`);
24907
- } catch {
24908
- holdsRows = true;
24909
- marks.push(`${table2}:unreadable`);
24910
- }
25421
+ /** One session's undelivered structural rows within the backlog, root first. */
25422
+ pendingRows(sessionId, limit, before) {
25423
+ return allRows(this.rowsStmt, { sessionId, limit, before });
24911
25424
  }
24912
- return { holdsRows, mark: marks.join("|") };
24913
- }
24914
- function applyLegacyDropMigration(db, file2) {
24915
- const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
24916
- if (!migration) return;
24917
- const before = file2 === void 0 ? void 0 : readLegacyTables(db);
24918
- if (file2 !== void 0 && before?.holdsRows === true) {
24919
- try {
24920
- backupBeforeLegacyDrop(db, file2);
24921
- } catch (error61) {
24922
- akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error61)}`);
24923
- return;
24924
- }
25425
+ /**
25426
+ * Captures this machine still owes the deployment, oldest first.
25427
+ *
25428
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
25429
+ * by a time window — see captureRowsStmt for why a window could not express
25430
+ * this. `before` is the grace window that leaves a just-recorded capture to
25431
+ * the live path.
25432
+ */
25433
+ pendingCaptureRows(limit, before) {
25434
+ return allRows(this.captureRowsStmt, { limit, before });
24925
25435
  }
24926
- try {
25436
+ /**
25437
+ * Record that a capture is OWED to the deployment.
25438
+ *
25439
+ * Written by the attached forward path when a live send did not confirm
25440
+ * delivery, and read by the drain as the whole of its eligibility test. It is
25441
+ * a fact rather than an inference: the machine was attached, the send did not
25442
+ * land, so the row is owed — which no time window can state, because the same
25443
+ * window that holds the rows a past attachment left owed also holds every
25444
+ * capture recorded while the machine was DETACHED, and those were never
25445
+ * offered to anyone.
25446
+ *
25447
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
25448
+ * out of the drain's read.
25449
+ */
25450
+ markCaptureOwed(id) {
25451
+ this.markOwedStmt.run({ id });
25452
+ }
25453
+ /**
25454
+ * Mark every capture already on disk as owed, as of `before`.
25455
+ *
25456
+ * The consent-time backfill, called once from `aka attach` when a human
25457
+ * grants existing-history consent — never from an ongoing drain pass, and
25458
+ * never inferred from a boundary that could later move. `before` is the
25459
+ * caller's own "now" at the moment consent was granted, so what this marks
25460
+ * is exactly the backlog the consent prompt already counted, not whatever a
25461
+ * later re-attach or key rotation might widen it to.
25462
+ *
25463
+ * Returns how many rows matched, for the caller to log or test against. Not a
25464
+ * count of NEWLY marked rows — a row still unsynced from an earlier call
25465
+ * matches again and is counted again, the same as `UPDATE`'s own `changes`.
25466
+ */
25467
+ markCaptureBacklogOwed(before) {
25468
+ return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
25469
+ }
25470
+ /**
25471
+ * Record delivery. Called only AFTER the far side has accepted the rows.
25472
+ *
25473
+ * CLEARS any failure reason in the same statement. A row that failed against
25474
+ * one deployment and then landed is delivered, and leaving the reason behind
25475
+ * would leave the store holding two contradictory answers about one row —
25476
+ * with the surface free to render either.
25477
+ */
25478
+ markSynced(ids, atMs) {
25479
+ this.stampAll(ids, atMs, null);
25480
+ }
25481
+ /**
25482
+ * Record that THIS MACHINE cannot express the row on the wire.
25483
+ *
25484
+ * Reserved for a local defect — a row that cannot be rebuilt into a valid
25485
+ * payload, or a body the client itself refused to send. It fails identically
25486
+ * against every deployment, so it is terminal everywhere and the re-arm leaves
25487
+ * it alone. A row that merely failed to REACH the deployment stays NULL, so it
25488
+ * is retried; marking those would turn one outage into permanent data loss.
25489
+ */
25490
+ markSkipped(ids, atMs) {
25491
+ this.stampAll(ids, SKIPPED, "payload_invalid", atMs);
25492
+ }
25493
+ /**
25494
+ * Record that THIS DEPLOYMENT refused the row.
25495
+ *
25496
+ * The same sentinel as `markSkipped`, and deliberately so: both stop the row
25497
+ * being re-offered on this lane, and `synced_at` goes on answering whether a
25498
+ * row is outstanding rather than why. What separates them is the reason, and
25499
+ * what the reason buys is the re-arm — a refusal is one deployment's verdict
25500
+ * on one body, so it is terminal only for as long as this machine points at
25501
+ * that deployment, and `rearmFor` clears it when the deployment changes.
25502
+ *
25503
+ * Leaving such a row NULL instead would be worse than the loss it replaces:
25504
+ * these reads carry no cursor, so an unstamped row the deployment refuses is
25505
+ * the head of every subsequent page, and the lane stalls behind it for ever.
25506
+ */
25507
+ markRefused(ids, atMs) {
25508
+ this.stampAll(ids, SKIPPED, "deployment_refused", atMs);
25509
+ }
25510
+ eachInTransaction(ids, run) {
25511
+ if (ids.length === 0) return;
24927
25512
  withTransaction(
24928
- db,
25513
+ this.db,
24929
25514
  () => {
24930
- const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
24931
- if (alreadyDropped) return;
24932
- if (before !== void 0 && readLegacyTables(db).mark !== before.mark) {
24933
- akaWarn(
24934
- "legacy events/findings rows changed after the pre-drop snapshot decision; deferring the drop so the next open can copy them first."
24935
- );
24936
- return;
24937
- }
24938
- for (const statement of splitStatements(migration.sql)) {
24939
- db.exec(statement);
24940
- }
24941
- db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
24942
- migration.tag,
24943
- Date.now()
24944
- );
25515
+ for (const id of ids) run(id);
24945
25516
  },
24946
25517
  "IMMEDIATE"
24947
25518
  );
24948
- } catch (error61) {
24949
- akaWarn(`legacy events/findings drop failed; deferring: ${String(error61)}`);
24950
- }
24951
- }
24952
- function backupBeforeLegacyDrop(db, file2) {
24953
- reapStalePartials(file2);
24954
- const backup = backupPath(file2, "pre-drop");
24955
- snapshotStore(db, backup);
24956
- return backup;
24957
- }
24958
- var TOKEN_USAGE_COLUMNS = [
24959
- {
24960
- name: "input_tokens",
24961
- ddl: "ALTER TABLE audit_events ADD COLUMN input_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.input_tokens')) VIRTUAL"
24962
- },
24963
- {
24964
- name: "output_tokens",
24965
- ddl: "ALTER TABLE audit_events ADD COLUMN output_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.output_tokens')) VIRTUAL"
24966
- },
24967
- {
24968
- name: "cache_creation_input_tokens",
24969
- ddl: "ALTER TABLE audit_events ADD COLUMN cache_creation_input_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.cache_creation_input_tokens')) VIRTUAL"
24970
- },
24971
- {
24972
- name: "cache_read_input_tokens",
24973
- ddl: "ALTER TABLE audit_events ADD COLUMN cache_read_input_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.cache_read_input_tokens')) VIRTUAL"
24974
- },
24975
- {
24976
- name: "model",
24977
- ddl: "ALTER TABLE audit_events ADD COLUMN model text GENERATED ALWAYS AS (json_extract(attributes, '$.model')) VIRTUAL"
24978
- },
24979
- {
24980
- name: "provider",
24981
- ddl: "ALTER TABLE audit_events ADD COLUMN provider text GENERATED ALWAYS AS (json_extract(attributes, '$.provider')) VIRTUAL"
25519
+ }
25520
+ stampAll(ids, value, failure, failedAtMs) {
25521
+ if (ids.length === 0) return;
25522
+ const failedAt = failure === null ? null : failedAtMs ?? null;
25523
+ withTransaction(
25524
+ this.db,
25525
+ () => {
25526
+ for (const id of ids) this.stampStmt.run({ at: value, failedAt, failure, id });
25527
+ },
25528
+ "IMMEDIATE"
25529
+ );
25530
+ }
25531
+ /**
25532
+ * Claim rows as in-flight.
25533
+ *
25534
+ * Advisory in exactly the sense the lease is: it records that a send is in
25535
+ * progress so a surface can say so, and a lost claim costs a row showing as
25536
+ * queued while it is actually being sent. It is not exclusion — the far side
25537
+ * settles a duplicate on the row id.
25538
+ */
25539
+ claimRows(ids, atMs) {
25540
+ this.eachInTransaction(ids, (id) => this.claimRowStmt.run({ at: atMs, id }));
25541
+ }
25542
+ /** Give back a claim without settling — the send failed, the row is queued again. */
25543
+ releaseRows(ids) {
25544
+ this.eachInTransaction(ids, (id) => this.releaseRowStmt.run({ id }));
25545
+ }
25546
+ /**
25547
+ * Clear claims older than `staleBefore`, and report how many were cleared.
25548
+ *
25549
+ * A process killed between claiming and settling leaves rows claimed with
25550
+ * nothing left to settle them. Without this they read as "sending" for ever.
25551
+ */
25552
+ releaseStaleClaims(staleBefore) {
25553
+ return Number(this.releaseStaleClaimsStmt.run({ staleBefore }).changes);
25554
+ }
25555
+ /**
25556
+ * Every tracked row in exactly one delivery state.
25557
+ *
25558
+ * Takes no boundary on purpose. The boundary answers "what should the drain
25559
+ * pick up now", which is a different question from "what state is this row
25560
+ * in" — and a machine that has never attached has no boundary to pass, so
25561
+ * requiring one would force a caller to invent one and report the whole store
25562
+ * as queued.
25563
+ */
25564
+ /**
25565
+ * The same partition, one row per kind that a lane carries.
25566
+ *
25567
+ * A kind with nothing to report is ABSENT rather than a row of zeros: the
25568
+ * scope decides which rows exist at all, so a kind that has never been
25569
+ * recorded — or whose captures nobody ever owed — produces no group. A caller
25570
+ * rendering a fixed list of kinds must therefore treat a missing one as "no
25571
+ * rows", never as "zero sent"; the two look identical in a bar and mean
25572
+ * different things.
25573
+ */
25574
+ partitionByKind() {
25575
+ return allRows(
25576
+ this.partitionByKindStmt,
25577
+ {}
25578
+ ).map((row) => ({
25579
+ kind: row.kind,
25580
+ queued: row.queued ?? 0,
25581
+ inProgress: row.inProgress ?? 0,
25582
+ synced: row.synced ?? 0,
25583
+ failed: row.failed ?? 0,
25584
+ refused: row.refused ?? 0,
25585
+ detached: row.detached ?? 0,
25586
+ total: row.total ?? 0
25587
+ }));
25588
+ }
25589
+ partition() {
25590
+ const row = getRow(this.partitionStmt, {});
25591
+ return {
25592
+ queued: row?.queued ?? 0,
25593
+ inProgress: row?.inProgress ?? 0,
25594
+ synced: row?.synced ?? 0,
25595
+ failed: row?.failed ?? 0,
25596
+ refused: row?.refused ?? 0,
25597
+ detached: row?.detached ?? 0,
25598
+ total: row?.total ?? 0
25599
+ };
25600
+ }
25601
+ /** `pending` counts only what is inside the backlog; sent and skipped are totals. */
25602
+ counts(before) {
25603
+ const row = getRow(this.countsStmt, { before });
25604
+ const captures = getRow(this.captureSkipCountStmt);
25605
+ return {
25606
+ pending: row?.pending ?? 0,
25607
+ sent: row?.sent ?? 0,
25608
+ skipped: row?.skipped ?? 0,
25609
+ refused: row?.refused ?? 0,
25610
+ detached: row?.detached ?? 0,
25611
+ capturesSkipped: captures?.skipped ?? 0
25612
+ };
25613
+ }
25614
+ /**
25615
+ * The deployment the current stamps were made against, and where its backlog
25616
+ * ends.
25617
+ *
25618
+ * READ-ONLY. An absent row reads as an absent deployment, which is what a
25619
+ * machine that has never drained is — and every writer below seeds the row
25620
+ * before it needs one, so nothing depends on this creating it. Keeping the
25621
+ * write off the gate path matters because the gate runs on every pass while a
25622
+ * write has to take the database's write lock.
25623
+ */
25624
+ deployment() {
25625
+ const row = getRow(
25626
+ this.fingerprintStmt
25627
+ );
25628
+ return {
25629
+ fingerprint: row?.fingerprint ?? void 0,
25630
+ backlogBefore: row?.backlogBefore ?? void 0
25631
+ };
25632
+ }
25633
+ /**
25634
+ * Point the ledger at a different deployment, discarding what it recorded
25635
+ * about the previous one.
25636
+ *
25637
+ * Delivery is a fact about ONE recipient: rows sent to the deployment a
25638
+ * machine has just left are undelivered as far as the new one is concerned.
25639
+ * All four in one transaction, so a crash between them cannot leave stamps
25640
+ * attributed to the wrong deployment, a boundary that belongs to another, or
25641
+ * a disown with no re-mark to follow it.
25642
+ *
25643
+ * The boundary is written HERE and only here, which is what freezes it: a
25644
+ * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
25645
+ * unchanged, so this never runs and the backlog does not widen back over rows
25646
+ * the live path has since delivered.
25647
+ *
25648
+ * `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
25649
+ * granted existing-history consent for the deployment this call is arming —
25650
+ * a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
25651
+ * instant, `backlogBefore` is the ATTACH instant, and the two can be far
25652
+ * apart. Passed only when that grant is valid, since this method has no way
25653
+ * to check consent itself and must not mark a row owed for a machine that
25654
+ * never agreed to it. Applied AFTER the disown above, in the SAME
25655
+ * transaction: what the disown clears is every marker below `backlogBefore`,
25656
+ * which includes this deployment's OWN pre-attach rows — `aka attach` calls
25657
+ * `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
25658
+ * on the cleared side of that bound — and the re-mark in the same
25659
+ * transaction is what puts those rows back. A crash between the two cannot
25660
+ * strand the ledger disowned with nothing re-marked — the transaction either
25661
+ * lands whole or not at all, and a fingerprint mismatch that has not yet
25662
+ * committed re-enters this method on the very next pass. Omit it (the
25663
+ * structural-only tests do) to exercise the disown in isolation.
25664
+ *
25665
+ * The disown is bounded by `backlogBefore`, which is what keeps it from
25666
+ * touching a marker the NEW deployment's OWN live path has already set: B's
25667
+ * live path can mark a capture owed from the moment `aka attach` writes the
25668
+ * descriptor, before the drain's first pass ever reaches this method, and
25669
+ * such a row sits at or after the bound rather than below it. What keeps the
25670
+ * disown from eating THIS SAME CALL's own re-mark is the order, not the
25671
+ * bound — disown runs first, re-mark second, both inside the one
25672
+ * transaction above.
25673
+ */
25674
+ rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
25675
+ this.ensureRowStmt.run();
25676
+ withTransaction(
25677
+ this.db,
25678
+ () => {
25679
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
25680
+ this.rearmStmt.run();
25681
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
25682
+ this.disownCapturesStmt.run({ attachedAt: backlogBefore });
25683
+ }
25684
+ if (backfillCapturesBefore !== void 0) {
25685
+ this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
25686
+ }
25687
+ this.setFingerprintStmt.run({ fingerprint, backlogBefore });
25688
+ },
25689
+ "IMMEDIATE"
25690
+ );
25691
+ }
25692
+ /**
25693
+ * End the attached period: hand its rows to the live path, and release the
25694
+ * boundary so the next attachment can freeze a new one.
25695
+ *
25696
+ * WITHOUT THIS, a detach and a re-attach to the same deployment leave a window
25697
+ * nothing delivers. The fingerprint is unchanged, so the boundary is never
25698
+ * re-frozen and stays at the FIRST attachment — while nothing forwards at all
25699
+ * during the detached period, because the machine is not attached. Rows
25700
+ * recorded in that window sit after the boundary and before the re-attach, so
25701
+ * neither path takes them, and the pending count reports none outstanding.
25702
+ *
25703
+ * WHAT IT RECORDS, and what it deliberately does not. These rows were the
25704
+ * closing attachment's to deliver and are no longer outstanding — that is what
25705
+ * lets the boundary move. It is NOT a claim that any of them arrived, and the
25706
+ * distinction is not academic: this used to write a delivery TIME, which every
25707
+ * read treats as delivery, so one detach turned a window of undelivered rows
25708
+ * into a window of delivered ones and no surface could tell. It writes the
25709
+ * skip sentinel and a reason of its own instead, so "no longer owed" and
25710
+ * "received" stop being the same fact.
25711
+ *
25712
+ * A change of deployment still frees them (see the re-arm), because the next
25713
+ * deployment has seen none of this machine's history — so the rows reach it
25714
+ * exactly as they did when this wrote a delivery time.
25715
+ *
25716
+ * ONE TRANSACTION, so a crash cannot release the boundary while leaving the
25717
+ * window unstamped — that half-state would re-send the whole attached period
25718
+ * on the next attach, which is the failure the boundary exists to prevent.
25719
+ */
25720
+ closeAttachedWindow(attachedAtMs, atMs) {
25721
+ this.ensureRowStmt.run();
25722
+ withTransaction(
25723
+ this.db,
25724
+ () => {
25725
+ const row = getRow(this.fingerprintStmt);
25726
+ const from = row?.backlogBefore ?? attachedAtMs;
25727
+ this.closeWindowStmt.run({ at: atMs, attachedAt: from });
25728
+ this.releaseBoundaryStmt.run();
25729
+ },
25730
+ "IMMEDIATE"
25731
+ );
25732
+ }
25733
+ /**
25734
+ * Freeze a boundary for the deployment already on file, KEEPING the stamps.
25735
+ *
25736
+ * The re-attach half of the above. Distinct from `rearmFor`, which is for a
25737
+ * different deployment and therefore discards what was delivered to the old
25738
+ * one: here the recipient is the same, so everything already sent to it stays
25739
+ * sent.
25740
+ */
25741
+ freezeBoundary(backlogBefore) {
25742
+ this.ensureRowStmt.run();
25743
+ this.freezeBoundaryStmt.run({ backlogBefore });
25744
+ }
25745
+ /** Take the claim, or report that someone live already holds it. */
25746
+ claim(pid, host, nowMs, staleAfterMs) {
25747
+ this.ensureRowStmt.run();
25748
+ let taken = false;
25749
+ withTransaction(
25750
+ this.db,
25751
+ () => {
25752
+ const result = this.claimStmt.run({
25753
+ pid,
25754
+ host,
25755
+ now: nowMs,
25756
+ staleBefore: nowMs - staleAfterMs
25757
+ });
25758
+ taken = result.changes === 1;
25759
+ },
25760
+ "IMMEDIATE"
25761
+ );
25762
+ return taken;
25763
+ }
25764
+ /** Say the holder is still alive. A no-op once the claim has moved on. */
25765
+ heartbeat(pid, nowMs) {
25766
+ this.heartbeatStmt.run({ now: nowMs, pid });
25767
+ }
25768
+ /** Give the claim up. Scoped to this holder, so a taken-over claim is left be. */
25769
+ release(pid) {
25770
+ this.releaseStmt.run({ pid });
25771
+ }
25772
+ /** Who holds the claim, if anyone. Read-only, for the same reason as above. */
25773
+ lease() {
25774
+ return getRow(this.leaseStmt);
25775
+ }
25776
+ };
25777
+
25778
+ // ../../packages/persistence/src/migrations.ts
25779
+ function describeObject(object2) {
25780
+ return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
25781
+ }
25782
+ function splitStatements(sql) {
25783
+ return sql.split(/-->\s*statement-breakpoint/).map((s) => s.trim()).filter((s) => s.length > 0);
25784
+ }
25785
+ function createdIndexName(statement) {
25786
+ const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
25787
+ return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
25788
+ }
25789
+ var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
25790
+ function applyMigrations(db, file2, options = {}) {
25791
+ const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
25792
+ db.exec(
25793
+ "CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
25794
+ );
25795
+ const applied = new Set(
25796
+ db.prepare("SELECT tag FROM migration_ledger").all().map((r) => r.tag)
25797
+ );
25798
+ const preLedgerStore = applied.size === 0 && legacyCount > 0;
25799
+ const record2 = db.prepare(
25800
+ "INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)"
25801
+ );
25802
+ for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
25803
+ if (applied.has(migration.tag)) continue;
25804
+ if (options.skipTags?.has(migration.tag) === true) continue;
25805
+ if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
25806
+ const evidence = evidenceObjects(migration.sql);
25807
+ const present = evidence.filter((o) => evidenceExists(db, o));
25808
+ if (present.length > 0 && present.length < evidence.length) {
25809
+ const missing = evidence.filter((o) => !present.includes(o));
25810
+ const message = `sqlite migration ${migration.tag} has no ledger row, but the store already has ${present.map(describeObject).join(", ")} while missing ${missing.map(describeObject).join(", ")} \u2014 the schema diverged from the migration history; refusing to replay or skip.`;
25811
+ akaWarn(message);
25812
+ throw new Error(`[aka] ${message}`);
25813
+ }
25814
+ const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
25815
+ const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
25816
+ const statements = splitStatements(migration.sql);
25817
+ if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
25818
+ try {
25819
+ withTransaction(
25820
+ db,
25821
+ () => {
25822
+ for (const statement of statements) {
25823
+ const indexName = createdIndexName(statement);
25824
+ if (indexName === void 0) {
25825
+ if (alreadyApplied) continue;
25826
+ } else if (indexExists(db, indexName)) {
25827
+ continue;
25828
+ }
25829
+ db.exec(statement);
25830
+ }
25831
+ if (wantsFkOff && !alreadyApplied) {
25832
+ const violations = db.prepare("PRAGMA foreign_key_check").all();
25833
+ if (violations.length > 0) {
25834
+ throw new Error(
25835
+ `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
25836
+ );
25837
+ }
25838
+ }
25839
+ record2.run(migration.tag, Date.now());
25840
+ },
25841
+ "IMMEDIATE"
25842
+ );
25843
+ } finally {
25844
+ if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
25845
+ }
25846
+ }
25847
+ if (legacyCount < SQLITE_MIGRATIONS.length) {
25848
+ db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
25849
+ }
25850
+ ensureSyncedAtColumn(db, "audit_events");
25851
+ ensureScanLedgerTable(db);
25852
+ ensureHistorySyncTable(db);
25853
+ ensureBlockedDetectionsTable(db);
25854
+ ensureRuleProbeCacheTable(db);
25855
+ ensureWriteGateTrigger(db);
25856
+ ensureTokenUsageColumns(db);
25857
+ reconcileSourceProjectIds(db);
25858
+ if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
25859
+ const drained = runLegacyHistoryBackfill(db);
25860
+ if (drained) applyLegacyDropMigration(db, file2);
25861
+ }
25862
+ }
25863
+ function readLegacyTables(db) {
25864
+ let holdsRows = false;
25865
+ const marks = [];
25866
+ for (const table2 of ["events", "findings"]) {
25867
+ try {
25868
+ const row = db.prepare(`SELECT count(*) AS n, ifnull(max(rowid), -1) AS hi FROM ${table2}`).get();
25869
+ if (row === void 0) {
25870
+ holdsRows = true;
25871
+ marks.push(`${table2}:unreadable`);
25872
+ continue;
25873
+ }
25874
+ if (row.n > 0) holdsRows = true;
25875
+ marks.push(`${table2}:${String(row.n)}:${String(row.hi)}`);
25876
+ } catch {
25877
+ holdsRows = true;
25878
+ marks.push(`${table2}:unreadable`);
25879
+ }
25880
+ }
25881
+ return { holdsRows, mark: marks.join("|") };
25882
+ }
25883
+ function applyLegacyDropMigration(db, file2) {
25884
+ const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
25885
+ if (!migration) return;
25886
+ const before = file2 === void 0 ? void 0 : readLegacyTables(db);
25887
+ if (file2 !== void 0 && before?.holdsRows === true) {
25888
+ try {
25889
+ backupBeforeLegacyDrop(db, file2);
25890
+ } catch (error61) {
25891
+ akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error61)}`);
25892
+ return;
25893
+ }
25894
+ }
25895
+ try {
25896
+ withTransaction(
25897
+ db,
25898
+ () => {
25899
+ const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
25900
+ if (alreadyDropped) return;
25901
+ if (before !== void 0 && readLegacyTables(db).mark !== before.mark) {
25902
+ akaWarn(
25903
+ "legacy events/findings rows changed after the pre-drop snapshot decision; deferring the drop so the next open can copy them first."
25904
+ );
25905
+ return;
25906
+ }
25907
+ for (const statement of splitStatements(migration.sql)) {
25908
+ db.exec(statement);
25909
+ }
25910
+ db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
25911
+ migration.tag,
25912
+ Date.now()
25913
+ );
25914
+ },
25915
+ "IMMEDIATE"
25916
+ );
25917
+ } catch (error61) {
25918
+ akaWarn(`legacy events/findings drop failed; deferring: ${String(error61)}`);
25919
+ }
25920
+ }
25921
+ function backupBeforeLegacyDrop(db, file2) {
25922
+ reapStalePartials(file2);
25923
+ const backup = backupPath(file2, "pre-drop");
25924
+ snapshotStore(db, backup);
25925
+ return backup;
25926
+ }
25927
+ var TOKEN_USAGE_COLUMNS = [
25928
+ {
25929
+ name: "input_tokens",
25930
+ ddl: "ALTER TABLE audit_events ADD COLUMN input_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.input_tokens')) VIRTUAL"
25931
+ },
25932
+ {
25933
+ name: "output_tokens",
25934
+ ddl: "ALTER TABLE audit_events ADD COLUMN output_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.output_tokens')) VIRTUAL"
25935
+ },
25936
+ {
25937
+ name: "cache_creation_input_tokens",
25938
+ ddl: "ALTER TABLE audit_events ADD COLUMN cache_creation_input_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.cache_creation_input_tokens')) VIRTUAL"
25939
+ },
25940
+ {
25941
+ name: "cache_read_input_tokens",
25942
+ ddl: "ALTER TABLE audit_events ADD COLUMN cache_read_input_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.cache_read_input_tokens')) VIRTUAL"
25943
+ },
25944
+ {
25945
+ name: "model",
25946
+ ddl: "ALTER TABLE audit_events ADD COLUMN model text GENERATED ALWAYS AS (json_extract(attributes, '$.model')) VIRTUAL"
25947
+ },
25948
+ {
25949
+ name: "provider",
25950
+ ddl: "ALTER TABLE audit_events ADD COLUMN provider text GENERATED ALWAYS AS (json_extract(attributes, '$.provider')) VIRTUAL"
24982
25951
  }
24983
25952
  ];
24984
25953
  function ensureTokenUsageColumns(db) {
@@ -25239,10 +26208,62 @@ function ensureSyncedAtColumn(db, table2) {
25239
26208
  if (!columns.includes("outbox_owed")) {
25240
26209
  db.exec(`ALTER TABLE ${table2} ADD COLUMN outbox_owed integer`);
25241
26210
  }
26211
+ if (!columns.includes("sync_failed_at")) {
26212
+ db.exec(`ALTER TABLE ${table2} ADD COLUMN sync_failed_at integer`);
26213
+ }
26214
+ if (!columns.includes("sync_failure")) {
26215
+ withTransaction(
26216
+ db,
26217
+ () => {
26218
+ db.exec(`ALTER TABLE ${table2} ADD COLUMN sync_failure text`);
26219
+ db.exec(
26220
+ `UPDATE ${table2} SET synced_at = NULL
26221
+ WHERE synced_at = -1
26222
+ AND event_type IN (${COUNTED_EVENT_TYPES.map((t) => `'${t}'`).join(", ")})`
26223
+ );
26224
+ },
26225
+ "IMMEDIATE"
26226
+ );
26227
+ }
25242
26228
  db.exec(
25243
- `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
25244
- ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
26229
+ `CREATE TRIGGER IF NOT EXISTS aka_sync_failure_guard
26230
+ BEFORE UPDATE OF sync_failure ON ${table2}
26231
+ WHEN ${syncFailureRejectCondition()}
26232
+ BEGIN SELECT RAISE(ABORT, 'sync_failure is not one of the recorded reasons'); END`
25245
26233
  );
26234
+ const syncIndexColumns = [
26235
+ "event_type",
26236
+ "synced_at",
26237
+ "sync_claimed_at",
26238
+ "started_at",
26239
+ // Appended LAST on purpose. The delivery-state read now projects it, so it
26240
+ // has to be in the index for the read to stay covered — but putting it
26241
+ // ahead of `started_at` would reorder the prefix the structural drain's
26242
+ // reads match on.
26243
+ "sync_failure"
26244
+ // `outbox_owed` is DELIBERATELY ABSENT, and it was measured both ways.
26245
+ //
26246
+ // The delivery-state read tests it — a capture's state depends on whether a
26247
+ // live forward marked it owed — so carrying it here makes that read covering
26248
+ // rather than a row fetch per row: 16 ms against 40 ms on a real 6 GB store.
26249
+ // But a sixth column changes what the planner charges for this index, and
26250
+ // with no ANALYZE statistics it plans from schema shape alone: measured, it
26251
+ // then stops choosing the per-session index for the token rollup and walks
26252
+ // every `llm_call` in the store through the event-type index instead. That
26253
+ // read grows with the store; this one does not.
26254
+ //
26255
+ // 40 ms on the largest store measured, once per render, is a cost worth
26256
+ // paying to leave every other read's plan where it was.
26257
+ ];
26258
+ const currentSyncIndex = indexColumns(db, "idx_audit_events_sync");
26259
+ const syncIndexMatches = currentSyncIndex.length === syncIndexColumns.length && currentSyncIndex.every((column, i) => column === syncIndexColumns[i]);
26260
+ if (!syncIndexMatches) {
26261
+ db.exec("DROP INDEX IF EXISTS idx_audit_events_sync");
26262
+ db.exec(
26263
+ `CREATE INDEX idx_audit_events_sync
26264
+ ON audit_events (${syncIndexColumns.join(", ")})`
26265
+ );
26266
+ }
25246
26267
  db.exec(
25247
26268
  `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25248
26269
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
@@ -25464,7 +26485,11 @@ function buildAuditEvent(row) {
25464
26485
  link: linkParsed?.success ? linkParsed.data : null,
25465
26486
  targetId: row.target_id,
25466
26487
  internal: intToBool(row.internal),
25467
- flagged: intToBool(row.flagged)
26488
+ flagged: intToBool(row.flagged),
26489
+ // Only meaningful when the title came out empty — a row whose body was
26490
+ // expired but whose title fell back to `tool_name` still has something to
26491
+ // render, and flagging it would make the view apologise for nothing.
26492
+ bodyExpired: row.content_expired_at !== null && (row.title ?? "") === ""
25468
26493
  };
25469
26494
  }
25470
26495
  var TIMELINE_COLUMNS = `
@@ -25472,6 +26497,7 @@ var TIMELINE_COLUMNS = `
25472
26497
  event_type,
25473
26498
  started_at,
25474
26499
  coalesce(content, json_extract(attributes, '$.tool_name')) AS title,
26500
+ content_expired_at,
25475
26501
  coalesce(json_extract(attributes, '$.detail'), json_extract(attributes, '$.target')) AS detail,
25476
26502
  coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25477
26503
  json_extract(attributes, '$.severity') AS severity,
@@ -26137,6 +27163,88 @@ var SqliteAuditEventsRepository = class {
26137
27163
  }
26138
27164
  };
26139
27165
 
27166
+ // ../../packages/persistence/src/repositories/body-retention.ts
27167
+ var DEFAULT_BATCH_SIZE = 500;
27168
+ var DEFAULT_MAX_ROWS = 5e4;
27169
+ var SYNC_LANE_TYPES_SQL = OUTBOX_CAPTURE_TYPE_LIST;
27170
+ var SqliteBodyRetentionRepository = class {
27171
+ constructor(db) {
27172
+ this.db = db;
27173
+ const select = (laneClause) => `
27174
+ SELECT id, LENGTH(CAST(content AS BLOB)) AS bytes
27175
+ FROM audit_events
27176
+ WHERE content IS NOT NULL
27177
+ AND started_at < :cutoff
27178
+ AND event_type IN (${CAPTURE_EVENT_TYPES_SQL})
27179
+ ${laneClause}
27180
+ ORDER BY started_at
27181
+ LIMIT :limit`;
27182
+ this.candidatesStmt = this.db.prepare(select(""));
27183
+ this.candidatesSyncSafeStmt = this.db.prepare(
27184
+ select(`AND (event_type NOT IN (${SYNC_LANE_TYPES_SQL}) OR synced_at IS NOT NULL)`)
27185
+ );
27186
+ this.heldBySyncStmt = this.db.prepare(`
27187
+ SELECT COUNT(*) AS n
27188
+ FROM audit_events
27189
+ WHERE content IS NOT NULL
27190
+ AND started_at < :cutoff
27191
+ AND event_type IN (${SYNC_LANE_TYPES_SQL})
27192
+ AND synced_at IS NULL`);
27193
+ this.expireStmt = this.db.prepare(
27194
+ `UPDATE audit_events SET content = NULL, content_expired_at = :now WHERE id = :id`
27195
+ );
27196
+ }
27197
+ db;
27198
+ candidatesStmt;
27199
+ candidatesSyncSafeStmt;
27200
+ heldBySyncStmt;
27201
+ expireStmt;
27202
+ /** How many bytes a pass with these options would free, changing nothing. */
27203
+ preview(opts) {
27204
+ const limit = opts.maxRows ?? DEFAULT_MAX_ROWS;
27205
+ const stmt = opts.sweepSyncLane ? this.candidatesStmt : this.candidatesSyncSafeStmt;
27206
+ const rows = stmt.all({ cutoff: opts.cutoff, limit });
27207
+ return {
27208
+ rowsExpired: rows.length,
27209
+ bytesFreed: rows.reduce((sum, r) => sum + r.bytes, 0),
27210
+ rowsHeldBySync: this.countHeldBySync(opts)
27211
+ };
27212
+ }
27213
+ /** Clear eligible bodies, in bounded batches. */
27214
+ expire(opts) {
27215
+ const batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
27216
+ const maxRows = opts.maxRows ?? DEFAULT_MAX_ROWS;
27217
+ const stmt = opts.sweepSyncLane ? this.candidatesStmt : this.candidatesSyncSafeStmt;
27218
+ let rowsExpired = 0;
27219
+ let bytesFreed = 0;
27220
+ let done = true;
27221
+ while (rowsExpired < maxRows) {
27222
+ const remaining = Math.min(batchSize, maxRows - rowsExpired);
27223
+ const batch = stmt.all({ cutoff: opts.cutoff, limit: remaining });
27224
+ if (batch.length === 0) break;
27225
+ withTransaction(
27226
+ this.db,
27227
+ () => {
27228
+ for (const row of batch) this.expireStmt.run({ id: row.id, now: opts.now });
27229
+ },
27230
+ "IMMEDIATE"
27231
+ );
27232
+ rowsExpired += batch.length;
27233
+ bytesFreed += batch.reduce((sum, r) => sum + r.bytes, 0);
27234
+ if (batch.length < remaining) break;
27235
+ if (rowsExpired >= maxRows) {
27236
+ done = stmt.all({ cutoff: opts.cutoff, limit: 1 }).length === 0;
27237
+ }
27238
+ }
27239
+ return { rowsExpired, bytesFreed, rowsHeldBySync: this.countHeldBySync(opts), done };
27240
+ }
27241
+ countHeldBySync(opts) {
27242
+ if (opts.sweepSyncLane) return 0;
27243
+ const row = this.heldBySyncStmt.get({ cutoff: opts.cutoff });
27244
+ return row.n;
27245
+ }
27246
+ };
27247
+
26140
27248
  // ../../packages/persistence/src/repositories/classified-data.ts
26141
27249
  var SqliteClassifiedDataRepository = class {
26142
27250
  constructor(db) {
@@ -26965,7 +28073,15 @@ function toFlatFindingRow(r) {
26965
28073
  ...r.tool_name === null ? {} : { toolName: r.tool_name },
26966
28074
  eventId: r.event_id,
26967
28075
  ...r.session_id === null ? {} : { sessionId: r.session_id },
26968
- status: deriveInstanceStatus(r)
28076
+ status: deriveInstanceStatus(r),
28077
+ delivery: deriveFindingDelivery({
28078
+ kind: r.kind,
28079
+ syncedAt: r.synced_at,
28080
+ syncClaimedAt: r.sync_claimed_at,
28081
+ syncFailedAt: r.sync_failed_at,
28082
+ syncFailure: r.sync_failure,
28083
+ outboxOwed: r.outbox_owed
28084
+ })
26969
28085
  };
26970
28086
  }
26971
28087
  function encodeGroupCursor(group) {
@@ -27029,7 +28145,10 @@ var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS c
27029
28145
  e.tool_name AS tool_name,
27030
28146
  f.audit_event_id AS event_id, e.root_session_id AS session_id,
27031
28147
  e.event_type AS kind, f.finding_key AS finding_key,
27032
- ${latestResolutionStatusSql("f")} AS latest_status`;
28148
+ ${latestResolutionStatusSql("f")} AS latest_status,
28149
+ e.synced_at AS synced_at, e.sync_claimed_at AS sync_claimed_at,
28150
+ e.sync_failed_at AS sync_failed_at, e.sync_failure AS sync_failure,
28151
+ e.outbox_owed AS outbox_owed`;
27033
28152
  var DAY_MS3 = 864e5;
27034
28153
  var SqliteFindingsRepository = class {
27035
28154
  constructor(db) {
@@ -27274,6 +28393,7 @@ var SqliteFindingsRepository = class {
27274
28393
  providers: query.provider,
27275
28394
  actions: query.action,
27276
28395
  statuses: query.status,
28396
+ deliveries: query.deployment,
27277
28397
  tools: query.tool,
27278
28398
  repo: query.repo,
27279
28399
  file: query.file,
@@ -27341,6 +28461,7 @@ var SqliteFindingsRepository = class {
27341
28461
  providers: query.provider,
27342
28462
  actions: query.action,
27343
28463
  statuses: query.status,
28464
+ deliveries: query.deployment,
27344
28465
  tools: query.tool,
27345
28466
  q: query.q
27346
28467
  };
@@ -27604,7 +28725,9 @@ var SqliteFindingsRepository = class {
27604
28725
  )
27605
28726
  );
27606
28727
  for (const row of grouped) {
27607
- if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
28728
+ if (Object.hasOwn(byAction, row.action_taken)) {
28729
+ byAction[row.action_taken] = row.c;
28730
+ }
27608
28731
  }
27609
28732
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
27610
28733
  const sevRows = allRows(
@@ -27621,7 +28744,9 @@ var SqliteFindingsRepository = class {
27621
28744
  )
27622
28745
  );
27623
28746
  for (const row of sevRows) {
27624
- if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
28747
+ if (Object.hasOwn(bySeverity, row.severity)) {
28748
+ bySeverity[row.severity] = row.c;
28749
+ }
27625
28750
  }
27626
28751
  const categories = ENFORCEABLE_CATEGORIES;
27627
28752
  const enabledRows = allRows(
@@ -27670,525 +28795,6 @@ function isoDay(ms) {
27670
28795
  return new Date(ms).toISOString().slice(0, 10);
27671
28796
  }
27672
28797
 
27673
- // ../../packages/persistence/src/repositories/history-sync.ts
27674
- var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27675
- var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27676
- var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27677
- var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27678
- var SKIPPED = -1;
27679
- var ROW_COLUMNS = `id,
27680
- parent_id AS parentId,
27681
- root_session_id AS rootSessionId,
27682
- event_type AS eventType,
27683
- host_id AS hostId,
27684
- harness_id AS harnessId,
27685
- source_project_id AS sourceProjectId,
27686
- started_at AS startedAt,
27687
- ended_at AS endedAt,
27688
- severity,
27689
- priority,
27690
- content,
27691
- content_hash AS contentHash,
27692
- attributes`;
27693
- var SqliteHistorySyncRepository = class {
27694
- constructor(db) {
27695
- this.db = db;
27696
- this.ensureRowStmt = db.prepare(`INSERT OR IGNORE INTO history_sync (id) VALUES (1)`);
27697
- this.sessionsStmt = db.prepare(
27698
- `SELECT COALESCE(root_session_id, id) AS sessionId, MIN(started_at) AS earliest
27699
- FROM audit_events
27700
- WHERE synced_at IS NULL
27701
- AND event_type IN (${TYPE_LIST})
27702
- AND started_at < :before
27703
- GROUP BY sessionId
27704
- ORDER BY earliest
27705
- LIMIT :limit`
27706
- );
27707
- this.rowsStmt = db.prepare(
27708
- `SELECT ${ROW_COLUMNS}
27709
- FROM audit_events
27710
- WHERE synced_at IS NULL
27711
- AND event_type IN (${TYPE_LIST})
27712
- AND started_at < :before
27713
- AND COALESCE(root_session_id, id) = :sessionId
27714
- ORDER BY (event_type = 'session') DESC, started_at
27715
- LIMIT :limit`
27716
- );
27717
- this.captureRowsStmt = db.prepare(
27718
- `SELECT ${ROW_COLUMNS}
27719
- FROM audit_events
27720
- WHERE synced_at IS NULL
27721
- AND sync_claimed_at IS NULL
27722
- AND outbox_owed = 1
27723
- AND event_type IN (${CAPTURE_TYPE_LIST})
27724
- AND started_at < :before
27725
- ORDER BY started_at
27726
- LIMIT :limit`
27727
- );
27728
- this.markOwedStmt = db.prepare(
27729
- `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27730
- );
27731
- this.markCaptureBacklogOwedStmt = db.prepare(
27732
- `UPDATE audit_events SET outbox_owed = 1
27733
- WHERE synced_at IS NULL
27734
- AND event_type IN (${CAPTURE_TYPE_LIST})
27735
- AND started_at < :before`
27736
- );
27737
- this.stampStmt = db.prepare(
27738
- `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27739
- );
27740
- this.claimRowStmt = db.prepare(
27741
- `UPDATE audit_events SET sync_claimed_at = :at WHERE id = :id AND synced_at IS NULL`
27742
- );
27743
- this.releaseRowStmt = db.prepare(
27744
- `UPDATE audit_events SET sync_claimed_at = NULL WHERE id = :id`
27745
- );
27746
- this.releaseStaleClaimsStmt = db.prepare(
27747
- `UPDATE audit_events SET sync_claimed_at = NULL
27748
- WHERE sync_claimed_at IS NOT NULL AND sync_claimed_at < :staleBefore`
27749
- );
27750
- this.partitionStmt = db.prepare(
27751
- `SELECT
27752
- SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NULL THEN 1 ELSE 0 END) AS queued,
27753
- SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NOT NULL THEN 1 ELSE 0 END) AS inProgress,
27754
- SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS synced,
27755
- SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0 THEN 1 ELSE 0 END) AS failed,
27756
- COUNT(*) AS total
27757
- FROM audit_events
27758
- WHERE event_type IN (${TYPE_LIST})`
27759
- );
27760
- this.countsStmt = db.prepare(
27761
- `SELECT
27762
- SUM(CASE WHEN synced_at IS NULL AND started_at < :before THEN 1 ELSE 0 END) AS pending,
27763
- SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS sent,
27764
- SUM(CASE WHEN synced_at = ${String(SKIPPED)} THEN 1 ELSE 0 END) AS skipped
27765
- FROM audit_events
27766
- WHERE event_type IN (${TYPE_LIST})`
27767
- );
27768
- this.captureSkipCountStmt = db.prepare(
27769
- `SELECT COUNT(*) AS skipped
27770
- FROM audit_events
27771
- WHERE synced_at = ${String(SKIPPED)}
27772
- AND event_type IN (${CAPTURE_TYPE_LIST})`
27773
- );
27774
- this.fingerprintStmt = db.prepare(
27775
- `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27776
- FROM history_sync WHERE id = 1`
27777
- );
27778
- this.setFingerprintStmt = db.prepare(
27779
- `UPDATE history_sync
27780
- SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27781
- WHERE id = 1`
27782
- );
27783
- this.disownCapturesStmt = db.prepare(
27784
- `UPDATE audit_events SET outbox_owed = NULL
27785
- WHERE outbox_owed IS NOT NULL
27786
- AND event_type IN (${CAPTURE_TYPE_LIST})
27787
- AND started_at < :attachedAt`
27788
- );
27789
- this.rearmStmt = db.prepare(
27790
- `UPDATE audit_events SET synced_at = NULL
27791
- WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
27792
- );
27793
- this.claimStmt = db.prepare(
27794
- `UPDATE history_sync
27795
- SET owner_pid = :pid, owner_host = :host, acquired_at = :now, heartbeat_at = :now
27796
- WHERE id = 1
27797
- AND (owner_pid IS NULL
27798
- OR heartbeat_at IS NULL
27799
- OR heartbeat_at < :staleBefore
27800
- OR heartbeat_at > :now)`
27801
- );
27802
- this.heartbeatStmt = db.prepare(
27803
- `UPDATE history_sync SET heartbeat_at = :now WHERE id = 1 AND owner_pid = :pid`
27804
- );
27805
- this.releaseStmt = db.prepare(
27806
- `UPDATE history_sync
27807
- SET owner_pid = NULL, owner_host = NULL, acquired_at = NULL, heartbeat_at = NULL
27808
- WHERE id = 1 AND owner_pid = :pid`
27809
- );
27810
- this.closeWindowStmt = db.prepare(
27811
- `UPDATE audit_events SET synced_at = :at
27812
- WHERE synced_at IS NULL
27813
- AND event_type IN (${TYPE_LIST})
27814
- AND started_at >= :attachedAt`
27815
- );
27816
- this.releaseBoundaryStmt = db.prepare(
27817
- `UPDATE history_sync SET backlog_before = NULL WHERE id = 1`
27818
- );
27819
- this.freezeBoundaryStmt = db.prepare(
27820
- `UPDATE history_sync SET backlog_before = :backlogBefore WHERE id = 1`
27821
- );
27822
- this.leaseStmt = db.prepare(
27823
- `SELECT owner_pid AS ownerPid, owner_host AS ownerHost,
27824
- acquired_at AS acquiredAt, heartbeat_at AS heartbeatAt
27825
- FROM history_sync WHERE id = 1`
27826
- );
27827
- this.inspectionsStmt = db.prepare(
27828
- `SELECT d.rule_id AS ruleId,
27829
- d.name AS ruleName,
27830
- d.version AS ruleVersion,
27831
- d.category AS category,
27832
- d.severity AS severity,
27833
- f.span_start AS spanStart,
27834
- f.span_end AS spanEnd,
27835
- f.masked_match AS maskedMatch,
27836
- f.action_taken AS actionTaken,
27837
- f.confidence AS confidence
27838
- FROM inspection_findings f
27839
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27840
- WHERE f.audit_event_id = :auditEventId
27841
- ORDER BY f.span_start, f.id`
27842
- );
27843
- }
27844
- db;
27845
- ensureRowStmt;
27846
- sessionsStmt;
27847
- rowsStmt;
27848
- stampStmt;
27849
- countsStmt;
27850
- fingerprintStmt;
27851
- setFingerprintStmt;
27852
- rearmStmt;
27853
- claimStmt;
27854
- heartbeatStmt;
27855
- releaseStmt;
27856
- leaseStmt;
27857
- inspectionsStmt;
27858
- closeWindowStmt;
27859
- releaseBoundaryStmt;
27860
- freezeBoundaryStmt;
27861
- captureRowsStmt;
27862
- markOwedStmt;
27863
- markCaptureBacklogOwedStmt;
27864
- captureSkipCountStmt;
27865
- disownCapturesStmt;
27866
- partitionStmt;
27867
- claimRowStmt;
27868
- releaseRowStmt;
27869
- releaseStaleClaimsStmt;
27870
- /**
27871
- * The masked detections recorded against one tool call.
27872
- *
27873
- * These travel with the event because a tool call's target is not
27874
- * re-inspectable from the event alone — unlike a capture, where the text
27875
- * itself is re-scannable. What crosses is the masked match and the rule that
27876
- * produced it, never the value.
27877
- */
27878
- inspectionsFor(auditEventId) {
27879
- return allRows(this.inspectionsStmt, { auditEventId });
27880
- }
27881
- /**
27882
- * Sessions with structural rows still to send, oldest first.
27883
- *
27884
- * BOUNDED BY THE BACKLOG BOUNDARY, which is the whole correctness of this
27885
- * read. Anything recorded after the machine attached is the live forward
27886
- * path's to deliver; this drain exists for what was recorded before it, and a
27887
- * row both paths send is at best a duplicate request and at worst — for a
27888
- * session root — an overwrite of the inventory ids the live path resolved.
27889
- */
27890
- pendingSessions(limit, before) {
27891
- return allRows(this.sessionsStmt, { limit, before }).map(
27892
- (r) => r.sessionId
27893
- );
27894
- }
27895
- /** One session's undelivered structural rows within the backlog, root first. */
27896
- pendingRows(sessionId, limit, before) {
27897
- return allRows(this.rowsStmt, { sessionId, limit, before });
27898
- }
27899
- /**
27900
- * Captures this machine still owes the deployment, oldest first.
27901
- *
27902
- * Selected by the `outbox_owed` marker the attached forward path writes, not
27903
- * by a time window — see captureRowsStmt for why a window could not express
27904
- * this. `before` is the grace window that leaves a just-recorded capture to
27905
- * the live path.
27906
- */
27907
- pendingCaptureRows(limit, before) {
27908
- return allRows(this.captureRowsStmt, { limit, before });
27909
- }
27910
- /**
27911
- * Record that a capture is OWED to the deployment.
27912
- *
27913
- * Written by the attached forward path when a live send did not confirm
27914
- * delivery, and read by the drain as the whole of its eligibility test. It is
27915
- * a fact rather than an inference: the machine was attached, the send did not
27916
- * land, so the row is owed — which no time window can state, because the same
27917
- * window that holds the rows a past attachment left owed also holds every
27918
- * capture recorded while the machine was DETACHED, and those were never
27919
- * offered to anyone.
27920
- *
27921
- * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27922
- * out of the drain's read.
27923
- */
27924
- markCaptureOwed(id) {
27925
- this.markOwedStmt.run({ id });
27926
- }
27927
- /**
27928
- * Mark every capture already on disk as owed, as of `before`.
27929
- *
27930
- * The consent-time backfill, called once from `aka attach` when a human
27931
- * grants existing-history consent — never from an ongoing drain pass, and
27932
- * never inferred from a boundary that could later move. `before` is the
27933
- * caller's own "now" at the moment consent was granted, so what this marks
27934
- * is exactly the backlog the consent prompt already counted, not whatever a
27935
- * later re-attach or key rotation might widen it to.
27936
- *
27937
- * Returns how many rows matched, for the caller to log or test against. Not a
27938
- * count of NEWLY marked rows — a row still unsynced from an earlier call
27939
- * matches again and is counted again, the same as `UPDATE`'s own `changes`.
27940
- */
27941
- markCaptureBacklogOwed(before) {
27942
- return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
27943
- }
27944
- /** Record delivery. Called only AFTER the far side has accepted the rows. */
27945
- markSynced(ids, atMs) {
27946
- this.stampAll(ids, atMs);
27947
- }
27948
- /**
27949
- * Record that a row will never be sent.
27950
- *
27951
- * Reserved for a local defect — a row that cannot be rebuilt into a valid
27952
- * payload. A row that merely failed to reach the deployment stays NULL, so it
27953
- * is retried; marking those would turn one outage into permanent data loss.
27954
- */
27955
- markSkipped(ids) {
27956
- this.stampAll(ids, SKIPPED);
27957
- }
27958
- eachInTransaction(ids, run) {
27959
- if (ids.length === 0) return;
27960
- withTransaction(
27961
- this.db,
27962
- () => {
27963
- for (const id of ids) run(id);
27964
- },
27965
- "IMMEDIATE"
27966
- );
27967
- }
27968
- stampAll(ids, value) {
27969
- if (ids.length === 0) return;
27970
- withTransaction(
27971
- this.db,
27972
- () => {
27973
- for (const id of ids) this.stampStmt.run({ at: value, id });
27974
- },
27975
- "IMMEDIATE"
27976
- );
27977
- }
27978
- /**
27979
- * Claim rows as in-flight.
27980
- *
27981
- * Advisory in exactly the sense the lease is: it records that a send is in
27982
- * progress so a surface can say so, and a lost claim costs a row showing as
27983
- * queued while it is actually being sent. It is not exclusion — the far side
27984
- * settles a duplicate on the row id.
27985
- */
27986
- claimRows(ids, atMs) {
27987
- this.eachInTransaction(ids, (id) => this.claimRowStmt.run({ at: atMs, id }));
27988
- }
27989
- /** Give back a claim without settling — the send failed, the row is queued again. */
27990
- releaseRows(ids) {
27991
- this.eachInTransaction(ids, (id) => this.releaseRowStmt.run({ id }));
27992
- }
27993
- /**
27994
- * Clear claims older than `staleBefore`, and report how many were cleared.
27995
- *
27996
- * A process killed between claiming and settling leaves rows claimed with
27997
- * nothing left to settle them. Without this they read as "sending" for ever.
27998
- */
27999
- releaseStaleClaims(staleBefore) {
28000
- return Number(this.releaseStaleClaimsStmt.run({ staleBefore }).changes);
28001
- }
28002
- /**
28003
- * Every tracked row in exactly one delivery state.
28004
- *
28005
- * Takes no boundary on purpose. The boundary answers "what should the drain
28006
- * pick up now", which is a different question from "what state is this row
28007
- * in" — and a machine that has never attached has no boundary to pass, so
28008
- * requiring one would force a caller to invent one and report the whole store
28009
- * as queued.
28010
- */
28011
- partition() {
28012
- const row = getRow(this.partitionStmt, {});
28013
- return {
28014
- queued: row?.queued ?? 0,
28015
- inProgress: row?.inProgress ?? 0,
28016
- synced: row?.synced ?? 0,
28017
- failed: row?.failed ?? 0,
28018
- total: row?.total ?? 0
28019
- };
28020
- }
28021
- /** `pending` counts only what is inside the backlog; sent and skipped are totals. */
28022
- counts(before) {
28023
- const row = getRow(
28024
- this.countsStmt,
28025
- { before }
28026
- );
28027
- const captures = getRow(this.captureSkipCountStmt);
28028
- return {
28029
- pending: row?.pending ?? 0,
28030
- sent: row?.sent ?? 0,
28031
- skipped: row?.skipped ?? 0,
28032
- capturesSkipped: captures?.skipped ?? 0
28033
- };
28034
- }
28035
- /**
28036
- * The deployment the current stamps were made against, and where its backlog
28037
- * ends.
28038
- *
28039
- * READ-ONLY. An absent row reads as an absent deployment, which is what a
28040
- * machine that has never drained is — and every writer below seeds the row
28041
- * before it needs one, so nothing depends on this creating it. Keeping the
28042
- * write off the gate path matters because the gate runs on every pass while a
28043
- * write has to take the database's write lock.
28044
- */
28045
- deployment() {
28046
- const row = getRow(
28047
- this.fingerprintStmt
28048
- );
28049
- return {
28050
- fingerprint: row?.fingerprint ?? void 0,
28051
- backlogBefore: row?.backlogBefore ?? void 0
28052
- };
28053
- }
28054
- /**
28055
- * Point the ledger at a different deployment, discarding what it recorded
28056
- * about the previous one.
28057
- *
28058
- * Delivery is a fact about ONE recipient: rows sent to the deployment a
28059
- * machine has just left are undelivered as far as the new one is concerned.
28060
- * All four in one transaction, so a crash between them cannot leave stamps
28061
- * attributed to the wrong deployment, a boundary that belongs to another, or
28062
- * a disown with no re-mark to follow it.
28063
- *
28064
- * The boundary is written HERE and only here, which is what freezes it: a
28065
- * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
28066
- * unchanged, so this never runs and the backlog does not widen back over rows
28067
- * the live path has since delivered.
28068
- *
28069
- * `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
28070
- * granted existing-history consent for the deployment this call is arming —
28071
- * a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
28072
- * instant, `backlogBefore` is the ATTACH instant, and the two can be far
28073
- * apart. Passed only when that grant is valid, since this method has no way
28074
- * to check consent itself and must not mark a row owed for a machine that
28075
- * never agreed to it. Applied AFTER the disown above, in the SAME
28076
- * transaction: what the disown clears is every marker below `backlogBefore`,
28077
- * which includes this deployment's OWN pre-attach rows — `aka attach` calls
28078
- * `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
28079
- * on the cleared side of that bound — and the re-mark in the same
28080
- * transaction is what puts those rows back. A crash between the two cannot
28081
- * strand the ledger disowned with nothing re-marked — the transaction either
28082
- * lands whole or not at all, and a fingerprint mismatch that has not yet
28083
- * committed re-enters this method on the very next pass. Omit it (the
28084
- * structural-only tests do) to exercise the disown in isolation.
28085
- *
28086
- * The disown is bounded by `backlogBefore`, which is what keeps it from
28087
- * touching a marker the NEW deployment's OWN live path has already set: B's
28088
- * live path can mark a capture owed from the moment `aka attach` writes the
28089
- * descriptor, before the drain's first pass ever reaches this method, and
28090
- * such a row sits at or after the bound rather than below it. What keeps the
28091
- * disown from eating THIS SAME CALL's own re-mark is the order, not the
28092
- * bound — disown runs first, re-mark second, both inside the one
28093
- * transaction above.
28094
- */
28095
- rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
28096
- this.ensureRowStmt.run();
28097
- withTransaction(
28098
- this.db,
28099
- () => {
28100
- const previous = getRow(this.fingerprintStmt)?.fingerprint;
28101
- this.rearmStmt.run();
28102
- if (previous !== null && previous !== void 0 && previous !== fingerprint) {
28103
- this.disownCapturesStmt.run({ attachedAt: backlogBefore });
28104
- }
28105
- if (backfillCapturesBefore !== void 0) {
28106
- this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
28107
- }
28108
- this.setFingerprintStmt.run({ fingerprint, backlogBefore });
28109
- },
28110
- "IMMEDIATE"
28111
- );
28112
- }
28113
- /**
28114
- * End the attached period: hand its rows to the live path, and release the
28115
- * boundary so the next attachment can freeze a new one.
28116
- *
28117
- * WITHOUT THIS, a detach and a re-attach to the same deployment leave a window
28118
- * nothing delivers. The fingerprint is unchanged, so the boundary is never
28119
- * re-frozen and stays at the FIRST attachment — while nothing forwards at all
28120
- * during the detached period, because the machine is not attached. Rows
28121
- * recorded in that window sit after the boundary and before the re-attach, so
28122
- * neither path takes them, and the pending count reports none outstanding.
28123
- *
28124
- * Stamping the attached window is not a claim that every one of those rows
28125
- * reached the deployment — the live path drops on failure and says so
28126
- * elsewhere. It records that they were ITS to deliver, which is exactly the
28127
- * status quo: they sit outside the frozen boundary today and are equally never
28128
- * re-sent. Making it explicit is what lets the boundary move.
28129
- *
28130
- * ONE TRANSACTION, so a crash cannot release the boundary while leaving the
28131
- * window unstamped — that half-state would re-send the whole attached period
28132
- * on the next attach, which is the failure the boundary exists to prevent.
28133
- */
28134
- closeAttachedWindow(attachedAtMs, atMs) {
28135
- this.ensureRowStmt.run();
28136
- withTransaction(
28137
- this.db,
28138
- () => {
28139
- const row = getRow(this.fingerprintStmt);
28140
- const from = row?.backlogBefore ?? attachedAtMs;
28141
- this.closeWindowStmt.run({ at: atMs, attachedAt: from });
28142
- this.releaseBoundaryStmt.run();
28143
- },
28144
- "IMMEDIATE"
28145
- );
28146
- }
28147
- /**
28148
- * Freeze a boundary for the deployment already on file, KEEPING the stamps.
28149
- *
28150
- * The re-attach half of the above. Distinct from `rearmFor`, which is for a
28151
- * different deployment and therefore discards what was delivered to the old
28152
- * one: here the recipient is the same, so everything already sent to it stays
28153
- * sent.
28154
- */
28155
- freezeBoundary(backlogBefore) {
28156
- this.ensureRowStmt.run();
28157
- this.freezeBoundaryStmt.run({ backlogBefore });
28158
- }
28159
- /** Take the claim, or report that someone live already holds it. */
28160
- claim(pid, host, nowMs, staleAfterMs) {
28161
- this.ensureRowStmt.run();
28162
- let taken = false;
28163
- withTransaction(
28164
- this.db,
28165
- () => {
28166
- const result = this.claimStmt.run({
28167
- pid,
28168
- host,
28169
- now: nowMs,
28170
- staleBefore: nowMs - staleAfterMs
28171
- });
28172
- taken = result.changes === 1;
28173
- },
28174
- "IMMEDIATE"
28175
- );
28176
- return taken;
28177
- }
28178
- /** Say the holder is still alive. A no-op once the claim has moved on. */
28179
- heartbeat(pid, nowMs) {
28180
- this.heartbeatStmt.run({ now: nowMs, pid });
28181
- }
28182
- /** Give the claim up. Scoped to this holder, so a taken-over claim is left be. */
28183
- release(pid) {
28184
- this.releaseStmt.run({ pid });
28185
- }
28186
- /** Who holds the claim, if anyone. Read-only, for the same reason as above. */
28187
- lease() {
28188
- return getRow(this.leaseStmt);
28189
- }
28190
- };
28191
-
28192
28798
  // ../../packages/persistence/src/repositories/inspection-definitions.ts
28193
28799
  var SqliteInspectionDefinitionsRepository = class {
28194
28800
  constructor(db) {
@@ -28416,6 +29022,7 @@ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ n
28416
29022
  if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28417
29023
  if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28418
29024
  if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
29025
+ if (values.bodyRetention !== void 0) merged.bodyRetention = values.bodyRetention;
28419
29026
  if (values.vaultConsent !== void 0) {
28420
29027
  merged.vaultConsent = values.vaultConsent ? (
28421
29028
  // Keep an existing valid grant so its acknowledgedAt survives; mint one
@@ -30923,7 +31530,7 @@ var SqliteSecurityRepository = class {
30923
31530
  ELSE 0
30924
31531
  END) AS open_at_rest
30925
31532
  FROM inspection_findings f
30926
- JOIN audit_events e ON e.id = f.audit_event_id
31533
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
30927
31534
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
30928
31535
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
30929
31536
  ON latest.finding_key = f.finding_key
@@ -31149,7 +31756,7 @@ var SqliteSecurityRepository = class {
31149
31756
  this.db.prepare(
31150
31757
  `SELECT e.repo AS repo, count(*) AS c
31151
31758
  FROM inspection_findings f
31152
- JOIN audit_events e ON e.id = f.audit_event_id
31759
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31153
31760
  WHERE e.started_at >= :from AND e.started_at < :to
31154
31761
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
31155
31762
  AND e.repo IS NOT NULL
@@ -31273,7 +31880,7 @@ var SqliteSecurityRepository = class {
31273
31880
  d.severity AS severity,
31274
31881
  COUNT(*) AS count
31275
31882
  FROM inspection_findings f
31276
- JOIN audit_events e ON e.id = f.audit_event_id
31883
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31277
31884
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31278
31885
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
31279
31886
  ON latest.finding_key = f.finding_key
@@ -31308,7 +31915,7 @@ var SqliteSecurityRepository = class {
31308
31915
  `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
31309
31916
  d.rule_id AS rule_id, d.category AS category
31310
31917
  FROM inspection_findings f
31311
- JOIN audit_events e ON e.id = f.audit_event_id
31918
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31312
31919
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31313
31920
  WHERE e.started_at >= :from AND e.started_at < :to
31314
31921
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
@@ -32149,6 +32756,7 @@ function openWithPragmas(file2) {
32149
32756
  db.exec("PRAGMA journal_mode = WAL");
32150
32757
  db.exec("PRAGMA busy_timeout = 2000");
32151
32758
  db.exec("PRAGMA foreign_keys = ON");
32759
+ registerSqlFunctions(db);
32152
32760
  } catch (err) {
32153
32761
  closeQuietly(db);
32154
32762
  throw err;
@@ -32178,7 +32786,7 @@ function backupLegacyStore(db, file2) {
32178
32786
  discardStore(file2, backup);
32179
32787
  return backup;
32180
32788
  }
32181
- function openAndInitialize(file2, base) {
32789
+ function openAndInitialize(file2, base, skipTags) {
32182
32790
  let db = openWithPragmas(file2);
32183
32791
  try {
32184
32792
  if (isForeignSqliteLineage(db)) {
@@ -32188,7 +32796,7 @@ function openAndInitialize(file2, base) {
32188
32796
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
32189
32797
  );
32190
32798
  }
32191
- applyMigrations(db, file2);
32799
+ applyMigrations(db, file2, { skipTags });
32192
32800
  tightenPerms(file2);
32193
32801
  const policies = new SqlitePoliciesRepository(db);
32194
32802
  const installedPacks = new SqliteInstalledPacksRepository(db, base);
@@ -32203,6 +32811,7 @@ function openAndInitialize(file2, base) {
32203
32811
  exceptions: new SqliteExceptionsRepository(db),
32204
32812
  resolutions: new SqliteResolutionsRepository(db),
32205
32813
  ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
32814
+ bodyRetention: new SqliteBodyRetentionRepository(db),
32206
32815
  security: new SqliteSecurityRepository(db),
32207
32816
  detections: new SqliteDetectionsRepository(db),
32208
32817
  shares: new SqliteSharesRepository(db),
@@ -32225,7 +32834,8 @@ function openAndInitialize(file2, base) {
32225
32834
  throw err;
32226
32835
  }
32227
32836
  }
32228
- function openLocalDatabase(dir) {
32837
+ var DEFERRED_TAGS = new Set(DEFERRED_MIGRATION_TAGS);
32838
+ function openLocalDatabase(dir, options = {}) {
32229
32839
  ensureDataDirSync(dir);
32230
32840
  const file2 = join7(dir, DB_FILENAME);
32231
32841
  reapStalePartials(file2);
@@ -32237,6 +32847,7 @@ function openLocalDatabase(dir) {
32237
32847
  installedPacks,
32238
32848
  scanLedger,
32239
32849
  historySync,
32850
+ bodyRetention,
32240
32851
  secretVault,
32241
32852
  exceptions,
32242
32853
  resolutions,
@@ -32260,7 +32871,8 @@ function openLocalDatabase(dir) {
32260
32871
  // `dir` is always `<base>/data` — every caller resolves it through
32261
32872
  // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
32262
32873
  // settings/ and data/, and the pack-policy floor needs both halves.
32263
- dirname2(dir)
32874
+ dirname2(dir),
32875
+ options.applyDeferredMigrations === true ? void 0 : DEFERRED_TAGS
32264
32876
  );
32265
32877
  function captureRowId(event) {
32266
32878
  return captureId(
@@ -32453,6 +33065,7 @@ function openLocalDatabase(dir) {
32453
33065
  installedPacks,
32454
33066
  scanLedger,
32455
33067
  historySync,
33068
+ bodyRetention,
32456
33069
  secretVault,
32457
33070
  exceptions,
32458
33071
  resolutions,
@@ -32493,8 +33106,35 @@ function openLocalDatabase(dir) {
32493
33106
 
32494
33107
  // ../../packages/persistence/src/egress-wire.ts
32495
33108
  import { createHash as createHash3 } from "crypto";
33109
+ var PROJECT_KEY_DIGEST_VERSION = "v2";
33110
+ var SCP_FORM = /^(?:[^@/]+@)?([^/:]+):(.+)$/;
33111
+ var DOS_DRIVE = /^[A-Za-z]:[\\/]/;
33112
+ var FILE_URL = /^file:\/\//i;
33113
+ var SCHEME_FORM = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?(\/.*)?$/i;
33114
+ var SLASH = "/".charCodeAt(0);
33115
+ var GIT_SUFFIX = ".git";
33116
+ function trimSlashes(path) {
33117
+ let start = 0;
33118
+ let end = path.length;
33119
+ while (start < end && path.charCodeAt(start) === SLASH) start += 1;
33120
+ while (end > start && path.charCodeAt(end - 1) === SLASH) end -= 1;
33121
+ return path.slice(start, end);
33122
+ }
33123
+ function canonicalGitUrl(url2) {
33124
+ const trimmed = url2.trim();
33125
+ if (DOS_DRIVE.test(trimmed) || FILE_URL.test(trimmed)) return trimmed;
33126
+ const scheme = SCHEME_FORM.exec(trimmed);
33127
+ const scp = scheme === null ? SCP_FORM.exec(trimmed) : null;
33128
+ const host = (scheme?.[1] ?? scp?.[1])?.toLowerCase();
33129
+ if (host === void 0) return trimmed;
33130
+ const path = (scheme === null ? scp?.[2] : scheme[2]) ?? "";
33131
+ const bare = trimSlashes(path);
33132
+ const cleaned = bare.endsWith(GIT_SUFFIX) ? bare.slice(0, -GIT_SUFFIX.length) : bare;
33133
+ return cleaned === "" ? host : `${host}/${cleaned}`;
33134
+ }
32496
33135
  function hashProjectKey(projectKey) {
32497
- return createHash3("sha256").update(projectKey, "utf8").digest("hex");
33136
+ const canonical = projectKey.startsWith("git:") ? `git:${canonicalGitUrl(projectKey.slice("git:".length))}` : projectKey;
33137
+ return createHash3("sha256").update(`${PROJECT_KEY_DIGEST_VERSION}:${canonical}`, "utf8").digest("hex");
32498
33138
  }
32499
33139
  function toIngestHit(hit) {
32500
33140
  return {
@@ -32570,18 +33210,50 @@ function readFingerprintKey(dataDir2) {
32570
33210
  return parseKeyFile(raw);
32571
33211
  }
32572
33212
 
33213
+ // ../../packages/persistence/src/forward-health.ts
33214
+ import { readFileSync as readFileSync7 } from "fs";
33215
+ import { join as join9 } from "path";
33216
+ var FAILURES = /* @__PURE__ */ new Set([
33217
+ "unauthorized",
33218
+ "forbidden",
33219
+ "unreachable"
33220
+ ]);
33221
+ var BREAKER_COOLDOWN_MS = 3e4;
33222
+ function parseForwardHealth(raw, nowMs) {
33223
+ try {
33224
+ const parsed2 = JSON.parse(raw);
33225
+ if (typeof parsed2 !== "object" || parsed2 === null) return null;
33226
+ const record2 = parsed2;
33227
+ const failures = typeof record2.consecutiveFailures === "number" && record2.consecutiveFailures >= 0 ? record2.consecutiveFailures : 0;
33228
+ const openedAtMs = typeof record2.openedAtMs === "number" && Number.isFinite(record2.openedAtMs) && record2.openedAtMs <= nowMs ? record2.openedAtMs : null;
33229
+ const lastFailure = typeof record2.lastFailure === "string" && FAILURES.has(record2.lastFailure) ? record2.lastFailure : null;
33230
+ return { consecutiveFailures: failures, openedAtMs, lastFailure };
33231
+ } catch {
33232
+ return null;
33233
+ }
33234
+ }
33235
+ function isForwardPaused(health, nowMs) {
33236
+ const openedAtMs = health?.openedAtMs ?? null;
33237
+ if (openedAtMs === null) return false;
33238
+ return nowMs - openedAtMs < BREAKER_COOLDOWN_MS;
33239
+ }
33240
+
32573
33241
  // ../../packages/persistence/src/history-backfill.ts
32574
33242
  import { existsSync as existsSync4 } from "fs";
32575
- import { join as join9 } from "path";
33243
+ import { join as join10 } from "path";
32576
33244
 
32577
33245
  // ../../packages/persistence/src/history-preview.ts
32578
33246
  import { existsSync as existsSync5 } from "fs";
32579
- import { join as join10 } from "path";
33247
+ import { join as join11 } from "path";
32580
33248
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
32581
33249
 
33250
+ // ../../packages/persistence/src/history-sync-state.ts
33251
+ import { readFileSync as readFileSync8 } from "fs";
33252
+ import { join as join12 } from "path";
33253
+
32582
33254
  // ../../packages/persistence/src/store-symlinks.ts
32583
33255
  import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32584
- import { dirname as dirname3, join as join11, resolve } from "path";
33256
+ import { dirname as dirname3, join as join13, resolve } from "path";
32585
33257
 
32586
33258
  // ../../packages/persistence/src/vault/crypto.ts
32587
33259
  import {
@@ -32595,19 +33267,19 @@ import {
32595
33267
  // ../../packages/persistence/src/vault/key-provider.ts
32596
33268
  import { execFileSync } from "child_process";
32597
33269
  import { randomBytes as randomBytes2 } from "crypto";
32598
- import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32599
- import { join as join12 } from "path";
33270
+ import { chmodSync as chmodSync3, readFileSync as readFileSync9, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
33271
+ import { join as join14 } from "path";
32600
33272
 
32601
33273
  // ../../packages/persistence/src/vault/vault.ts
32602
33274
  import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
32603
33275
 
32604
33276
  // ../../packages/persistence/src/warn-era-cap.ts
32605
33277
  import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
32606
- import { join as join13 } from "path";
33278
+ import { join as join15 } from "path";
32607
33279
  var MARKER = "warn-era-capped";
32608
33280
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32609
33281
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
32610
- const marker = join13(dataDir2, MARKER);
33282
+ const marker = join15(dataDir2, MARKER);
32611
33283
  if (existsSync7(marker)) return { capped: 0, skipped: "already-run" };
32612
33284
  const capped = db.policies.capCategoryActions();
32613
33285
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
@@ -32798,10 +33470,10 @@ function parsed(schema, body, route) {
32798
33470
  }
32799
33471
  function withoutTrailingSlashes(endpoint) {
32800
33472
  let end = endpoint.length;
32801
- while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
33473
+ while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH2) end -= 1;
32802
33474
  return endpoint.slice(0, end);
32803
33475
  }
32804
- var SLASH = "/".charCodeAt(0);
33476
+ var SLASH2 = "/".charCodeAt(0);
32805
33477
  function createRemoteClient(options) {
32806
33478
  const base = withoutTrailingSlashes(options.endpoint);
32807
33479
  const url2 = (route) => `${base}${route}`;
@@ -32983,11 +33655,11 @@ function withTimeout(promise2, ms) {
32983
33655
  }
32984
33656
 
32985
33657
  // ../../packages/plugin-runtime/src/attached/forward-drops.ts
32986
- import { readFileSync as readFileSync8 } from "fs";
32987
- import { join as join14 } from "path";
33658
+ import { readFileSync as readFileSync10 } from "fs";
33659
+ import { join as join16 } from "path";
32988
33660
  var FORWARD_DROPS_FILENAME = ATTACHED_FORWARD_DROPS_FILENAME;
32989
33661
  function forwardDropsPath(dataDir2) {
32990
- return join14(dataDir2, FORWARD_DROPS_FILENAME);
33662
+ return join16(dataDir2, FORWARD_DROPS_FILENAME);
32991
33663
  }
32992
33664
  function recordForwardDrops(dataDir2, count, nowMs) {
32993
33665
  if (count <= 0) return;
@@ -33005,7 +33677,7 @@ function recordForwardDrops(dataDir2, count, nowMs) {
33005
33677
  }
33006
33678
  function readForwardDrops(dataDir2) {
33007
33679
  try {
33008
- const parsed2 = JSON.parse(readFileSync8(forwardDropsPath(dataDir2), "utf8"));
33680
+ const parsed2 = JSON.parse(readFileSync10(forwardDropsPath(dataDir2), "utf8"));
33009
33681
  if (typeof parsed2 !== "object" || parsed2 === null) return null;
33010
33682
  const record2 = parsed2;
33011
33683
  if (typeof record2.droppedForwards !== "number" || !Number.isFinite(record2.droppedForwards)) {
@@ -33023,13 +33695,12 @@ function readForwardDrops(dataDir2) {
33023
33695
 
33024
33696
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
33025
33697
  import { randomUUID as randomUUID15 } from "crypto";
33026
- import { readFileSync as readFileSync15 } from "fs";
33027
33698
  import { readFile, rename, writeFile } from "fs/promises";
33028
- import { join as join24 } from "path";
33699
+ import { join as join26 } from "path";
33029
33700
 
33030
33701
  // ../../packages/plugin-sdk/src/config.ts
33031
33702
  import { existsSync as existsSync8 } from "fs";
33032
- import { join as join15 } from "path";
33703
+ import { join as join17 } from "path";
33033
33704
 
33034
33705
  // ../../packages/plugin-sdk/src/provider-env.ts
33035
33706
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -33083,7 +33754,7 @@ function resolveProvider() {
33083
33754
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
33084
33755
  try {
33085
33756
  ensureLayoutDirSync(base);
33086
- const settingsFile = join15(settingsDir(base), "settings.json");
33757
+ const settingsFile = join17(settingsDir(base), "settings.json");
33087
33758
  if (existsSync8(settingsFile)) tightenFile(settingsFile);
33088
33759
  } catch {
33089
33760
  }
@@ -33107,9 +33778,9 @@ function resolveProviderSafe(resolveProviderFn) {
33107
33778
  }
33108
33779
 
33109
33780
  // ../../packages/plugin-sdk/src/config-inventory.ts
33110
- import { readdirSync as readdirSync2, readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
33781
+ import { readdirSync as readdirSync2, readFileSync as readFileSync12, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
33111
33782
  import { homedir as homedir2 } from "os";
33112
- import { basename as basename3, join as join17 } from "path";
33783
+ import { basename as basename3, join as join19 } from "path";
33113
33784
 
33114
33785
  // ../../packages/detections/src/egress/registry.ts
33115
33786
  var EXTRACTOR_VERSION = "1";
@@ -35892,8 +36563,8 @@ function bundledDetections() {
35892
36563
  }
35893
36564
 
35894
36565
  // ../../packages/plugin-sdk/src/repo.ts
35895
- import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
35896
- import { basename as basename2, dirname as dirname4, isAbsolute, join as join16, sep as sep2 } from "path";
36566
+ import { existsSync as existsSync9, readFileSync as readFileSync11, statSync as statSync6 } from "fs";
36567
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join18, sep as sep2 } from "path";
35897
36568
 
35898
36569
  // ../../packages/plugin-sdk/src/events.ts
35899
36570
  import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
@@ -35904,8 +36575,8 @@ import { fileURLToPath } from "url";
35904
36575
  import { Worker } from "worker_threads";
35905
36576
 
35906
36577
  // ../../packages/plugin-sdk/src/host-floor.ts
35907
- import { readFileSync as readFileSync12 } from "fs";
35908
- import { join as join19 } from "path";
36578
+ import { readFileSync as readFileSync14 } from "fs";
36579
+ import { join as join21 } from "path";
35909
36580
 
35910
36581
  // ../../packages/plugin-sdk/src/model-governance.ts
35911
36582
  import {
@@ -35913,11 +36584,11 @@ import {
35913
36584
  fstatSync,
35914
36585
  mkdirSync as mkdirSync2,
35915
36586
  openSync as openSync2,
35916
- readFileSync as readFileSync11,
36587
+ readFileSync as readFileSync13,
35917
36588
  readSync,
35918
36589
  writeFileSync as writeFileSync5
35919
36590
  } from "fs";
35920
- import { join as join18 } from "path";
36591
+ import { join as join20 } from "path";
35921
36592
  var TAIL_BYTES = 256 * 1024;
35922
36593
 
35923
36594
  // ../../packages/plugin-sdk/src/host-floor.ts
@@ -35940,15 +36611,15 @@ var HOST_FLOORS = {
35940
36611
 
35941
36612
  // ../../packages/plugin-sdk/src/ignore-layers.ts
35942
36613
  var import_ignore = __toESM(require_ignore(), 1);
35943
- import { readFileSync as readFileSync13 } from "fs";
35944
- import { join as join20 } from "path";
36614
+ import { readFileSync as readFileSync15 } from "fs";
36615
+ import { join as join22 } from "path";
35945
36616
 
35946
36617
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
35947
36618
  import { arch, hostname as hostname4, platform, release } from "os";
35948
36619
 
35949
36620
  // ../../packages/plugin-sdk/src/nudge.ts
35950
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
35951
- import { join as join21 } from "path";
36621
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync16, writeFileSync as writeFileSync6 } from "fs";
36622
+ import { join as join23 } from "path";
35952
36623
 
35953
36624
  // ../../packages/plugin-sdk/src/paths.ts
35954
36625
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
@@ -35956,7 +36627,7 @@ import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
35956
36627
 
35957
36628
  // ../../packages/plugin-sdk/src/project-files.ts
35958
36629
  import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
35959
- import { basename as basename5, join as join22 } from "path";
36630
+ import { basename as basename5, join as join24 } from "path";
35960
36631
 
35961
36632
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
35962
36633
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -35992,7 +36663,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
35992
36663
 
35993
36664
  // ../../packages/plugin-sdk/src/throttle.ts
35994
36665
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
35995
- import { join as join23 } from "path";
36666
+ import { join as join25 } from "path";
35996
36667
 
35997
36668
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
35998
36669
  function isInvalidRequest(err) {
@@ -36008,31 +36679,12 @@ function isServerRejection(err) {
36008
36679
  var FORWARD_BUDGET_MS = 1500;
36009
36680
  var DECISION_PATH_BUDGET_MS = 800;
36010
36681
  var BREAKER_FAILURE_THRESHOLD = 3;
36011
- var BREAKER_COOLDOWN_MS = 3e4;
36012
36682
  var CLOSED = { consecutiveFailures: 0, openedAtMs: null, lastFailure: null };
36013
- var FAILURES = /* @__PURE__ */ new Set([
36014
- "unauthorized",
36015
- "forbidden",
36016
- "unreachable"
36017
- ]);
36018
36683
  var FORWARD_STATE_FILENAME = ATTACHED_FORWARD_STATE_FILENAME;
36019
36684
  var STATE_FILENAME = FORWARD_STATE_FILENAME;
36020
- function parseBreakerState(raw, nowMs) {
36021
- try {
36022
- const parsed2 = JSON.parse(raw);
36023
- if (typeof parsed2 !== "object" || parsed2 === null) return null;
36024
- const record2 = parsed2;
36025
- const failures = typeof record2.consecutiveFailures === "number" && record2.consecutiveFailures >= 0 ? record2.consecutiveFailures : 0;
36026
- const openedAtMs = typeof record2.openedAtMs === "number" && Number.isFinite(record2.openedAtMs) && record2.openedAtMs <= nowMs ? record2.openedAtMs : null;
36027
- const lastFailure = typeof record2.lastFailure === "string" && FAILURES.has(record2.lastFailure) ? record2.lastFailure : null;
36028
- return { consecutiveFailures: failures, openedAtMs, lastFailure };
36029
- } catch {
36030
- return null;
36031
- }
36032
- }
36033
36685
  function createForwardPolicy(deps) {
36034
36686
  const now = deps.now ?? (() => Date.now());
36035
- const file2 = join24(deps.dir, STATE_FILENAME);
36687
+ const file2 = join26(deps.dir, STATE_FILENAME);
36036
36688
  let state = null;
36037
36689
  let loading = null;
36038
36690
  async function readState() {
@@ -36042,7 +36694,7 @@ function createForwardPolicy(deps) {
36042
36694
  } catch {
36043
36695
  return { ...CLOSED };
36044
36696
  }
36045
- return parseBreakerState(raw, now()) ?? { ...CLOSED };
36697
+ return parseForwardHealth(raw, now()) ?? { ...CLOSED };
36046
36698
  }
36047
36699
  async function load() {
36048
36700
  if (state !== null) return state;
@@ -36088,7 +36740,7 @@ function createForwardPolicy(deps) {
36088
36740
  };
36089
36741
  const at = now();
36090
36742
  if (current.openedAtMs !== null) {
36091
- if (at - current.openedAtMs < BREAKER_COOLDOWN_MS) {
36743
+ if (isForwardPaused(current, at)) {
36092
36744
  return { ok: false, reason: "breaker-open" };
36093
36745
  }
36094
36746
  await persist({
@@ -36625,7 +37277,18 @@ var AttachedDataGateway = class {
36625
37277
  // and the spread above would otherwise drop the field silently — which is
36626
37278
  // exactly what it did, leaving the whole control inert on every device
36627
37279
  // while every test around it stayed green.
36628
- prohibitedModels: cached2.prohibitedModels
37280
+ prohibitedModels: cached2.prohibitedModels,
37281
+ // NAMED for the same reason as the line above, and it is the same defect
37282
+ // if it is not: `...local` above spreads the DEVICE's bundle, so a field
37283
+ // only the cache carries is dropped in silence. That is what left
37284
+ // `prohibitedModels` inert on every attached device with every test
37285
+ // around it green.
37286
+ //
37287
+ // Taken from the cache rather than merged here, because merging it needs
37288
+ // the device's own SETTING — which is not a bundle field and is not in
37289
+ // scope at this seam. The runtime does that merge, raise-only, where both
37290
+ // values are in hand (createPluginRuntime's ensureInitialized).
37291
+ redactFallback: cached2.redactFallback
36629
37292
  // ALSO HONOURED FROM THE CACHE, and not a bundle field at all: each
36630
37293
  // merged policy's own `provenance`. `mergeRaiseOnly` spreads the policies
36631
37294
  // it emits, so an 'authored' policy arriving from the control plane
@@ -36753,10 +37416,6 @@ function toolAuditEvent(input2) {
36753
37416
  };
36754
37417
  }
36755
37418
 
36756
- // ../../packages/plugin-runtime/src/attached/history-state.ts
36757
- import { readFileSync as readFileSync16 } from "fs";
36758
- import { join as join25 } from "path";
36759
-
36760
37419
  // ../../packages/plugin-runtime/src/attached/history-sync.ts
36761
37420
  import { createHash as createHash6 } from "crypto";
36762
37421
  import { hostname as hostname5 } from "os";
@@ -36765,6 +37424,10 @@ import { hostname as hostname5 } from "os";
36765
37424
  var CORRELATION_ID = EventMetadata.shape.correlationId;
36766
37425
  var TRACE_ID = EventMetadata.shape.traceId;
36767
37426
  var EXCEPTION_ID = EventMetadata.shape.exceptionIds.unwrap().element;
37427
+ var REDACT_DEGRADED_TO = EventMetadata.shape.redactDegradedTo.unwrap();
37428
+
37429
+ // ../../packages/plugin-runtime/src/attached/history-sync.ts
37430
+ var CAPTURE_BATCH_BYTES = 1024 * 1024;
36768
37431
 
36769
37432
  // ../../packages/plugin-runtime/src/attached/history-sync-trigger.ts
36770
37433
  import { spawn } from "child_process";
@@ -36791,7 +37454,7 @@ function createPluginBlock(build, policyStore) {
36791
37454
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
36792
37455
  import { randomUUID as randomUUID16 } from "crypto";
36793
37456
  import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
36794
- import { join as join26 } from "path";
37457
+ import { join as join27 } from "path";
36795
37458
 
36796
37459
  // ../../packages/plugin-runtime/src/attached/atomic-publish.ts
36797
37460
  import { rename as rename2 } from "fs/promises";
@@ -36815,7 +37478,7 @@ async function publishByRename(tmp, file2, move = rename2) {
36815
37478
 
36816
37479
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
36817
37480
  function createPolicyStore(dir = dataDir()) {
36818
- const file2 = join26(dir, "policy-cache.json");
37481
+ const file2 = join27(dir, "policy-cache.json");
36819
37482
  async function read() {
36820
37483
  try {
36821
37484
  const raw = await readFile2(file2, "utf8");
@@ -37046,11 +37709,11 @@ function readStorePosture(dbPath2) {
37046
37709
  // ../../packages/plugin-runtime/src/attached/posture-store.ts
37047
37710
  import { randomUUID as randomUUID17 } from "crypto";
37048
37711
  import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
37049
- import { join as join27 } from "path";
37712
+ import { join as join28 } from "path";
37050
37713
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
37051
37714
  function createPostureStore(dir = settingsDir(), legacyDir) {
37052
- const file2 = join27(dir, "posture-state.json");
37053
- const legacyFile = legacyDir === void 0 ? null : join27(legacyDir, "posture-state.json");
37715
+ const file2 = join28(dir, "posture-state.json");
37716
+ const legacyFile = legacyDir === void 0 ? null : join28(legacyDir, "posture-state.json");
37054
37717
  async function persist(state) {
37055
37718
  await ensureDataDir(dir);
37056
37719
  const tmp = `${file2}.${randomUUID17()}.tmp`;
@@ -37119,7 +37782,7 @@ function createPostureStore(dir = settingsDir(), legacyDir) {
37119
37782
 
37120
37783
  // ../../packages/plugin-runtime/src/attached/sync-state.ts
37121
37784
  import { readFileSync as readFileSync18 } from "fs";
37122
- import { join as join28 } from "path";
37785
+ import { join as join29 } from "path";
37123
37786
 
37124
37787
  // ../../packages/plugin-runtime/src/attached/status.ts
37125
37788
  var REFUSAL_LINES = {
@@ -37140,6 +37803,14 @@ import { spawn as spawn2 } from "child_process";
37140
37803
  import { fileURLToPath as fileURLToPath3 } from "url";
37141
37804
  var SYNC_THROTTLE_MS = 15 * 60 * 1e3;
37142
37805
 
37806
+ // ../../packages/plugin-runtime/src/content-retention-pass.ts
37807
+ var MAX_ROWS_PER_SWEEP = 50 * 1e3;
37808
+
37809
+ // ../../packages/plugin-runtime/src/content-retention-trigger.ts
37810
+ import { spawn as spawn3 } from "child_process";
37811
+ import { fileURLToPath as fileURLToPath4 } from "url";
37812
+ var CONTENT_RETENTION_THROTTLE_MS = 60 * 60 * 1e3;
37813
+
37143
37814
  // ../../packages/plugin-runtime/src/attached/factory.ts
37144
37815
  import { hostname as hostname6 } from "os";
37145
37816
 
@@ -37615,7 +38286,7 @@ async function readStdin() {
37615
38286
 
37616
38287
  // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
37617
38288
  import { writeFileSync as writeFileSync8 } from "fs";
37618
- import { join as join29 } from "path";
38289
+ import { join as join30 } from "path";
37619
38290
 
37620
38291
  // ../../packages/setup-wizard/src/triage/merge.ts
37621
38292
  var RANK = Object.fromEntries(
@@ -37625,7 +38296,7 @@ var RANK = Object.fromEntries(
37625
38296
  // ../../packages/setup-wizard/src/triage/plan-file.ts
37626
38297
  import { mkdtempSync, readFileSync as readFileSync19, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
37627
38298
  import { tmpdir } from "os";
37628
- import { basename as basename6, dirname as dirname6, join as join30 } from "path";
38299
+ import { basename as basename6, dirname as dirname6, join as join31 } from "path";
37629
38300
  var SuppressionEntrySchema = external_exports.object({
37630
38301
  ruleId: external_exports.string(),
37631
38302
  category: DetectionCategory,
@@ -37668,8 +38339,8 @@ var PersistedPlanSchema = external_exports.object({
37668
38339
 
37669
38340
  // src/command-registry.ts
37670
38341
  import { readdirSync as readdirSync5 } from "fs";
37671
- import { fileURLToPath as fileURLToPath4 } from "url";
37672
- var COMMANDS_DIR = fileURLToPath4(new URL("../commands", import.meta.url));
38342
+ import { fileURLToPath as fileURLToPath5 } from "url";
38343
+ var COMMANDS_DIR = fileURLToPath5(new URL("../commands", import.meta.url));
37673
38344
 
37674
38345
  // src/present.ts
37675
38346
  var SHADE = {