@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 SLASH = "/";
53
+ var SLASH2 = "/";
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(SLASH).filter(Boolean);
425
+ const slices = path.split(SLASH2).filter(Boolean);
426
426
  slices.pop();
427
427
  if (slices.length) {
428
428
  const parent = this._t(
429
- slices.join(SLASH) + SLASH,
429
+ slices.join(SLASH2) + SLASH2,
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(SLASH).filter(Boolean);
445
+ slices = path.split(SLASH2).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(SLASH) + SLASH,
452
+ slices.join(SLASH2) + SLASH2,
453
453
  cache,
454
454
  checkUnignored,
455
455
  slices
@@ -500,6 +500,14 @@ var POLICY_CACHE_FILENAME = "policy-cache.json";
500
500
  import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync, rmSync as rmSync3, statSync } from "fs";
501
501
  import { join as join2 } from "path";
502
502
 
503
+ // ../../packages/schema/src/drizzle/deferred-migrations.ts
504
+ var DEFERRED_MIGRATION_TAGS = [
505
+ "0031_audit_capture_by_time_index",
506
+ "0032_audit_capture_by_id_index",
507
+ "0033_audit_capture_location_index",
508
+ "0034_findings_read_indexes"
509
+ ];
510
+
503
511
  // ../../packages/schema/src/drizzle/sqlite-ddl.ts
504
512
  var SQLITE_MIGRATIONS = [
505
513
  {
@@ -617,6 +625,30 @@ var SQLITE_MIGRATIONS = [
617
625
  {
618
626
  tag: "0028_activity_session_probe_indexes",
619
627
  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"
628
+ },
629
+ {
630
+ tag: "0029_audit_capture_rollup_index",
631
+ 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');"
632
+ },
633
+ {
634
+ tag: "0030_audit_content_expiry",
635
+ 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;"
636
+ },
637
+ {
638
+ tag: "0031_audit_capture_by_time_index",
639
+ 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');"
640
+ },
641
+ {
642
+ tag: "0032_audit_capture_by_id_index",
643
+ 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');"
644
+ },
645
+ {
646
+ tag: "0033_audit_capture_location_index",
647
+ 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');"
648
+ },
649
+ {
650
+ tag: "0034_findings_read_indexes",
651
+ 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`);"
620
652
  }
621
653
  ];
622
654
 
@@ -20664,6 +20696,15 @@ var FindingCategory = external_exports.enum([
20664
20696
  ]).meta({ id: "FindingCategory" });
20665
20697
  var FindingOrigin = external_exports.enum(["in-flight", "at-rest"]).meta({ id: "FindingOrigin" });
20666
20698
  var FindingStatus = external_exports.enum(["open", "handled", "resolved", "dismissed"]).meta({ id: "FindingStatus" });
20699
+ var SyncFailureReason = external_exports.enum(["deployment_refused", "payload_invalid", "detached_undelivered"]).meta({ id: "SyncFailureReason" });
20700
+ var FindingDeliveryState = external_exports.enum(["sent", "queued", "not_sent", "never_offered", "local_scan"]).meta({ id: "FindingDeliveryState" });
20701
+ var FindingDelivery = external_exports.object({
20702
+ state: FindingDeliveryState,
20703
+ // The delivery time for `sent`; the failure time for `not_sent` when recorded.
20704
+ at: external_exports.iso.datetime().optional(),
20705
+ // Only on `not_sent`, and only when a known reason was recorded.
20706
+ reason: SyncFailureReason.optional()
20707
+ }).meta({ id: "FindingDelivery" });
20667
20708
  var ResolutionMethod = external_exports.enum([
20668
20709
  "enforced-in-flight",
20669
20710
  "fixed-at-source",
@@ -20720,7 +20761,10 @@ var FindingInstance = external_exports.object({
20720
20761
  // The session that event belongs to, when it has one — the seam a
20721
20762
  // per-instance "view session" link needs. Absent for events captured
20722
20763
  // outside a session.
20723
- sessionId: external_exports.string().optional()
20764
+ sessionId: external_exports.string().optional(),
20765
+ // The delivery state of the event above (see FindingDelivery). Optional so
20766
+ // readers that do not project it stay valid.
20767
+ delivery: FindingDelivery.optional()
20724
20768
  }).meta({ id: "FindingInstance" });
20725
20769
  var FindingGroup = external_exports.object({
20726
20770
  id: external_exports.string(),
@@ -20772,7 +20816,10 @@ var FindingFacets = external_exports.object({
20772
20816
  // Host tool (attributes.tool_name). Present only on the instance-level
20773
20817
  // reads, which can filter by it; the type-level read omits the dimension
20774
20818
  // because a group spans tools.
20775
- tool: external_exports.array(FindingFacetItem).optional()
20819
+ tool: external_exports.array(FindingFacetItem).optional(),
20820
+ // Delivery states (FindingDeliveryState). Present only on the
20821
+ // instance-level reads, like `tool`.
20822
+ deployment: external_exports.array(FindingFacetItem).optional()
20776
20823
  }).meta({ id: "FindingFacets" });
20777
20824
  var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
20778
20825
  id: "FindingTypeSummary"
@@ -20883,6 +20930,8 @@ var ListFindingInstancesQuery = external_exports.object({
20883
20930
  // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
20884
20931
  // where the free-text `q` can only match the rendered "via Bash" label.
20885
20932
  tool: external_exports.array(external_exports.string()).optional(),
20933
+ // The delivery state of each finding's event (see FindingDelivery).
20934
+ deployment: external_exports.array(FindingDeliveryState).optional(),
20886
20935
  // Exact repository / file-path matches, for the drill-down out of the
20887
20936
  // locations view. A row whose event carries no repo/file matches neither.
20888
20937
  repo: external_exports.string().optional(),
@@ -20903,6 +20952,10 @@ var ListFindingInstancesResponse = external_exports.object({
20903
20952
  items: external_exports.array(FindingInstanceDetail),
20904
20953
  nextCursor: external_exports.string().nullable()
20905
20954
  }).meta({ id: "ListFindingInstancesResponse" });
20955
+ var ListFindingInstancesPage = external_exports.object({
20956
+ items: external_exports.array(FindingInstanceDetail),
20957
+ nextCursor: external_exports.string().nullable()
20958
+ }).meta({ id: "ListFindingInstancesPage" });
20906
20959
  var FindingLocationSummary = external_exports.object({
20907
20960
  // Opaque, stable, minted from the pair by encodeLocationId. It exists
20908
20961
  // because a location's identity is two values and a URL param carries one:
@@ -20945,6 +20998,8 @@ var ListFindingLocationsQuery = external_exports.object({
20945
20998
  // instances that match, and folds its status from those.
20946
20999
  status: external_exports.array(FindingStatus).optional(),
20947
21000
  tool: external_exports.array(external_exports.string()).optional(),
21001
+ // The delivery state of each finding's event (see FindingDelivery).
21002
+ deployment: external_exports.array(FindingDeliveryState).optional(),
20948
21003
  q: external_exports.string().optional(),
20949
21004
  sessionId: external_exports.string().optional(),
20950
21005
  from: external_exports.iso.datetime().optional(),
@@ -21147,6 +21202,10 @@ var CaptureAttributes = external_exports.object({
21147
21202
  // to 'allow' — the enforcement audit trail's link back to the grant that
21148
21203
  // authorized the bypass.
21149
21204
  exception_ids: external_exports.array(external_exports.guid()).optional(),
21205
+ // The persisted spellings of EventMetadata's messageId/conversationId — the
21206
+ // join back to the `llm_call` leaf for the same assistant turn.
21207
+ message_id: external_exports.string().optional(),
21208
+ conversation_id: external_exports.string().optional(),
21150
21209
  // Whole milliseconds this capture's inspection blocked its caller — the
21151
21210
  // plugin's own added latency (see EventMetadata.inspectionMs, whose value
21152
21211
  // this is). Promoted to the `inspection_ms` generated column so the facet is
@@ -21155,7 +21214,19 @@ var CaptureAttributes = external_exports.object({
21155
21214
  // inline json_extract and is not itself an optimization.
21156
21215
  // ABSENT on replayed captures (backfill / worktree scan) and on rows written
21157
21216
  // before the measurement shipped — never present as a placeholder 0.
21158
- inspection_ms: external_exports.number().int().nonnegative().optional()
21217
+ inspection_ms: external_exports.number().int().nonnegative().optional(),
21218
+ // What a `redact` this capture could not carry out became instead (see
21219
+ // EventMetadata.redactDegradedTo, whose value this is). Present only when a
21220
+ // degrade actually happened, so absence is the ordinary case rather than a
21221
+ // reader having to distinguish it from a zero.
21222
+ //
21223
+ // PER CAPTURE, while `inspection_findings.action_taken` is per finding —
21224
+ // so on a multi-finding row this does not say which finding degraded, and
21225
+ // its presence does not mean the fallback decided the capture's action. A
21226
+ // capture denied by another finding's own Block policy carries `block`
21227
+ // here too. The full statement is on EventMetadata.redactDegradedTo; it is
21228
+ // repeated rather than referenced because a store reader opens this file.
21229
+ redact_degraded_to: ActionTaken.optional()
21159
21230
  }).catchall(external_exports.unknown());
21160
21231
  var ToolCallInspection = external_exports.object({
21161
21232
  ruleId: external_exports.string().min(1),
@@ -21354,7 +21425,17 @@ var AuditEvent = external_exports.object({
21354
21425
  /** `share` to a first-party/internal destination. */
21355
21426
  internal: external_exports.boolean(),
21356
21427
  /** Event needs review (e.g. unverified egress). */
21357
- flagged: external_exports.boolean()
21428
+ flagged: external_exports.boolean(),
21429
+ /**
21430
+ * The body this event's `title` is drawn from was cleared by local body
21431
+ * expiry, so an EMPTY title here means "gone", not "never had one".
21432
+ *
21433
+ * A separate flag rather than a sentinel written into `title`: the title is
21434
+ * rendered text, and a store-layer module that invented display copy for it
21435
+ * would be choosing words the view is supposed to choose. Additive and
21436
+ * defaulted, so an older producer still validates.
21437
+ */
21438
+ bodyExpired: external_exports.boolean().default(false)
21358
21439
  }).meta({ id: "ActivityAuditEvent" });
21359
21440
  var ActivitySessionSummary = external_exports.object({
21360
21441
  id: external_exports.string(),
@@ -22694,6 +22775,12 @@ var EventMetadata = external_exports.object({
22694
22775
  // to 'allow' — the enforcement audit trail's link back to the grant that
22695
22776
  // authorized the bypass. Absent on captures where no exception applied.
22696
22777
  exceptionIds: external_exports.array(external_exports.guid()).optional(),
22778
+ // The assistant message this capture belongs to, and the conversation it sits
22779
+ // in — set by the browser extension's network capture so a stored `response`
22780
+ // row can be joined to the `llm_call` leaf describing the same turn. Absent
22781
+ // on every other capture path, which has no such id.
22782
+ messageId: external_exports.string().optional(),
22783
+ conversationId: external_exports.string().optional(),
22697
22784
  // How long THIS capture's inspection blocked its caller, in whole
22698
22785
  // milliseconds — the plugin's own added latency, NOT the LLM call it sat in
22699
22786
  // front of. Measured inside `capture()` (@akasecurity/plugin-sdk) across
@@ -22706,7 +22793,37 @@ var EventMetadata = external_exports.object({
22706
22793
  // Absent is also what every pre-measurement client writes, and what a
22707
22794
  // clock failure degrades to — a reader must treat absence as "not measured"
22708
22795
  // and never as a zero, which would read as "inspection is free".
22709
- inspectionMs: external_exports.number().int().nonnegative().optional()
22796
+ inspectionMs: external_exports.number().int().nonnegative().optional(),
22797
+ // What a `redact` this capture COULD NOT CARRY OUT became instead — the
22798
+ // workspace's `redactFallback`, applied because the field could not be
22799
+ // masked in place (a shell command, a URL, or any argument on a host whose
22800
+ // hook contract offers no rewrite channel).
22801
+ //
22802
+ // It exists because the action alone cannot say why. A finding recorded as
22803
+ // `warn` reads identically whether its detection was ASSIGNED Warn or was
22804
+ // assigned Redact on a field that could not take one — and those are
22805
+ // different facts about the same row: the first is a policy the user chose,
22806
+ // the second is a masking the host could not perform. Absent means no
22807
+ // degrade happened, which is every ordinary capture.
22808
+ //
22809
+ // TWO LIMITS a reader of a stored row has to know, because the grain here
22810
+ // is the CAPTURE while `actionTaken` is per FINDING:
22811
+ //
22812
+ // - It does not say WHICH finding degraded. A capture carrying a degraded
22813
+ // `redact` alongside a finding ASSIGNED the same action stores both
22814
+ // identically and one reason for the pair; attributing it to both
22815
+ // describes the assigned one wrongly, and to neither loses the degrade.
22816
+ // - PRESENCE IS NOT CAUSATION. The value is the action the lost redact
22817
+ // became, not the reason the capture ended as it did — a capture denied
22818
+ // by some other finding's own Block policy still carries `block` here,
22819
+ // and clearing the workspace's fallback would not have let it through.
22820
+ // Gate on the value against what a fallback can produce; never read the
22821
+ // field's presence as "this was the fallback's doing".
22822
+ //
22823
+ // Both are pinned as behaviour in @akasecurity/plugin-sdk's runtime suite.
22824
+ // Closing either means moving the reason onto the finding row, which
22825
+ // already carries its own action.
22826
+ redactDegradedTo: ActionTaken.optional()
22710
22827
  }).meta({ id: "EventMetadata" });
22711
22828
  var Event = external_exports.object({
22712
22829
  id: external_exports.guid(),
@@ -22816,7 +22933,32 @@ var RotateKeyInput = external_exports.object({
22816
22933
  confirmation: external_exports.string()
22817
22934
  });
22818
22935
 
22936
+ // ../../packages/schema/src/zod/finding-delivery.ts
22937
+ var KNOWN_REASONS = SyncFailureReason.options;
22938
+ function knownReason(value) {
22939
+ return value !== null && KNOWN_REASONS.includes(value) ? value : void 0;
22940
+ }
22941
+ function deriveFindingDelivery(row) {
22942
+ if (row.kind === "code_change") return { state: "local_scan" };
22943
+ if (row.syncedAt !== null && row.syncedAt > 0) {
22944
+ return { state: "sent", at: epochMillisToIso(row.syncedAt) };
22945
+ }
22946
+ if (row.syncedAt !== null) {
22947
+ const reason = knownReason(row.syncFailure);
22948
+ return {
22949
+ state: "not_sent",
22950
+ ...row.syncFailedAt === null ? {} : { at: epochMillisToIso(row.syncFailedAt) },
22951
+ ...reason === void 0 ? {} : { reason }
22952
+ };
22953
+ }
22954
+ if (row.outboxOwed === 1 || row.syncClaimedAt !== null) return { state: "queued" };
22955
+ return { state: "never_offered" };
22956
+ }
22957
+
22819
22958
  // ../../packages/schema/src/zod/findings-group-build.ts
22959
+ function lookupOwn(map2, key) {
22960
+ return Object.hasOwn(map2, key) ? map2[key] : void 0;
22961
+ }
22820
22962
  function toApiAction(dbVal) {
22821
22963
  const map2 = {
22822
22964
  log: "monitored",
@@ -22825,7 +22967,7 @@ function toApiAction(dbVal) {
22825
22967
  warn: "warned",
22826
22968
  allow: "allowed"
22827
22969
  };
22828
- return map2[dbVal] ?? "allowed";
22970
+ return lookupOwn(map2, dbVal) ?? "allowed";
22829
22971
  }
22830
22972
  function toApiCategory(dbVal) {
22831
22973
  if (dbVal === "code_context") return "source_code";
@@ -22833,13 +22975,18 @@ function toApiCategory(dbVal) {
22833
22975
  return parsed.success ? parsed.data : "custom";
22834
22976
  }
22835
22977
  function toApiProvider(sourceTool) {
22836
- return TOOL_TO_HARNESS[sourceTool] ?? HARNESS.Api;
22978
+ return lookupOwn(TOOL_TO_HARNESS, sourceTool) ?? HARNESS.Api;
22837
22979
  }
22838
- var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
22980
+ var FINDING_STATUS_PRECEDENCE = [
22981
+ "open",
22982
+ "handled",
22983
+ "dismissed",
22984
+ "resolved"
22985
+ ];
22839
22986
  function foldGroupStatus(instanceStatuses) {
22840
22987
  const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
22841
22988
  if (statuses.size === 0) return void 0;
22842
- for (const candidate of STATUS_PRECEDENCE) {
22989
+ for (const candidate of FINDING_STATUS_PRECEDENCE) {
22843
22990
  if (statuses.has(candidate)) return candidate;
22844
22991
  }
22845
22992
  return void 0;
@@ -22946,11 +23093,16 @@ function applyFindingFilters(types, opts) {
22946
23093
  }
22947
23094
  return filtered;
22948
23095
  }
22949
- var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
22950
- var SEVERITY_RANK = SEVERITY_ORDER;
23096
+ function rankByOrder(members2) {
23097
+ return Object.fromEntries(members2.map((member, index) => [member, index]));
23098
+ }
23099
+ var SEVERITY_RANK = rankByOrder(Severity.options);
23100
+ function severityRank(severity) {
23101
+ return lookupOwn(SEVERITY_RANK, severity);
23102
+ }
22951
23103
  function compareFindingGroupOrder(a, b) {
22952
- const rankA = SEVERITY_RANK[a.severity] ?? -1;
22953
- const rankB = SEVERITY_RANK[b.severity] ?? -1;
23104
+ const rankA = severityRank(a.severity) ?? -1;
23105
+ const rankB = severityRank(b.severity) ?? -1;
22954
23106
  const severityDiff = rankA - rankB;
22955
23107
  if (severityDiff !== 0) return severityDiff;
22956
23108
  const recencyDiff = b.latestDetectedAt.localeCompare(a.latestDetectedAt);
@@ -23025,6 +23177,20 @@ function computeFindingFacets(allTypes, opts) {
23025
23177
  }
23026
23178
 
23027
23179
  // ../../packages/schema/src/zod/findings-flat-build.ts
23180
+ function compareCodePoints(a, b) {
23181
+ const aIter = a[Symbol.iterator]();
23182
+ const bIter = b[Symbol.iterator]();
23183
+ for (; ; ) {
23184
+ const aNext = aIter.next();
23185
+ const bNext = bIter.next();
23186
+ if (aNext.done && bNext.done) return 0;
23187
+ if (aNext.done) return -1;
23188
+ if (bNext.done) return 1;
23189
+ const aPoint = aNext.value.codePointAt(0) ?? 0;
23190
+ const bPoint = bNext.value.codePointAt(0) ?? 0;
23191
+ if (aPoint !== bPoint) return aPoint - bPoint;
23192
+ }
23193
+ }
23028
23194
  function rowHaystack(row) {
23029
23195
  return [
23030
23196
  row.ruleId,
@@ -23049,6 +23215,8 @@ function matchesDimension(row, opts, dimension) {
23049
23215
  return !opts.actions?.length || opts.actions.includes(toApiAction(row.actionTaken));
23050
23216
  case "statuses":
23051
23217
  return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
23218
+ case "deliveries":
23219
+ return !opts.deliveries?.length || row.delivery !== void 0 && opts.deliveries.includes(row.delivery.state);
23052
23220
  case "tools":
23053
23221
  return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
23054
23222
  // An EMPTY value is a real filter here, not an absent one. The location
@@ -23075,6 +23243,7 @@ var DIMENSIONS = [
23075
23243
  "providers",
23076
23244
  "actions",
23077
23245
  "statuses",
23246
+ "deliveries",
23078
23247
  "tools",
23079
23248
  "repo",
23080
23249
  "file",
@@ -23088,10 +23257,19 @@ function matchesInstanceFilters(row, opts, except) {
23088
23257
  return true;
23089
23258
  }
23090
23259
  function toItems(counts) {
23091
- return [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
23260
+ return [...counts.entries()].map(([value, count]) => ({ value, count })).sort(
23261
+ (a, b) => b.count - a.count || a.value.localeCompare(b.value) || // localeCompare reports canonically-equivalent strings (an NFC and an
23262
+ // NFD spelling of the same text) as equal, so a count tie between
23263
+ // them would otherwise be ordered by whichever the Map iteration
23264
+ // produced. compareCodePoints breaks that tie deterministically, which
23265
+ // makes this a TOTAL order — not one that agrees with SQL collation,
23266
+ // which it need not: foldFacetTuples runs this same sort over grouped
23267
+ // tuples, so both paths order facets identically by construction.
23268
+ compareCodePoints(a.value, b.value)
23269
+ );
23092
23270
  }
23093
- function bump(counts, value) {
23094
- counts.set(value, (counts.get(value) ?? 0) + 1);
23271
+ function bump(counts, value, by = 1) {
23272
+ counts.set(value, (counts.get(value) ?? 0) + by);
23095
23273
  }
23096
23274
  function createInstanceFacetAccumulator(opts) {
23097
23275
  const severity = /* @__PURE__ */ new Map();
@@ -23100,6 +23278,7 @@ function createInstanceFacetAccumulator(opts) {
23100
23278
  const action = /* @__PURE__ */ new Map();
23101
23279
  const status = /* @__PURE__ */ new Map();
23102
23280
  const tool = /* @__PURE__ */ new Map();
23281
+ const deployment = /* @__PURE__ */ new Map();
23103
23282
  return {
23104
23283
  add(row) {
23105
23284
  if (matchesInstanceFilters(row, opts, "severity")) bump(severity, row.severity);
@@ -23114,6 +23293,9 @@ function createInstanceFacetAccumulator(opts) {
23114
23293
  if (row.toolName !== void 0 && matchesInstanceFilters(row, opts, "tools")) {
23115
23294
  bump(tool, row.toolName);
23116
23295
  }
23296
+ if (row.delivery !== void 0 && matchesInstanceFilters(row, opts, "deliveries")) {
23297
+ bump(deployment, row.delivery.state);
23298
+ }
23117
23299
  },
23118
23300
  facets: () => ({
23119
23301
  severity: toItems(severity),
@@ -23121,7 +23303,8 @@ function createInstanceFacetAccumulator(opts) {
23121
23303
  provider: toItems(provider),
23122
23304
  action: toItems(action),
23123
23305
  status: toItems(status),
23124
- tool: toItems(tool)
23306
+ tool: toItems(tool),
23307
+ deployment: toItems(deployment)
23125
23308
  })
23126
23309
  };
23127
23310
  }
@@ -23135,6 +23318,7 @@ function toInstanceDetail(row) {
23135
23318
  ...row.toolName === void 0 ? {} : { toolName: row.toolName },
23136
23319
  eventId: row.eventId,
23137
23320
  ...row.sessionId === void 0 ? {} : { sessionId: row.sessionId },
23321
+ ...row.delivery === void 0 ? {} : { delivery: row.delivery },
23138
23322
  ...row.user === void 0 ? {} : { user: row.user },
23139
23323
  action: toApiAction(row.actionTaken),
23140
23324
  detectedAt: row.occurredAt,
@@ -23149,12 +23333,6 @@ function toInstanceDetail(row) {
23149
23333
  policy: { id: `category:${category}`, name: category }
23150
23334
  };
23151
23335
  }
23152
- var SEVERITY_ORDER2 = {
23153
- critical: 0,
23154
- high: 1,
23155
- medium: 2,
23156
- low: 3
23157
- };
23158
23336
  function newLocationAccumulator() {
23159
23337
  return {
23160
23338
  instanceCount: 0,
@@ -23169,7 +23347,7 @@ function newLocationAccumulator() {
23169
23347
  }
23170
23348
  function addToLocation(acc, row) {
23171
23349
  acc.instanceCount += 1;
23172
- const rank = SEVERITY_ORDER2[row.severity] ?? Number.MAX_SAFE_INTEGER - 1;
23350
+ const rank = severityRank(row.severity) ?? Number.MAX_SAFE_INTEGER - 1;
23173
23351
  if (rank < acc.maxSeverityRank) {
23174
23352
  acc.maxSeverityRank = rank;
23175
23353
  acc.maxSeverity = row.severity;
@@ -23179,15 +23357,15 @@ function addToLocation(acc, row) {
23179
23357
  acc.ruleIds.add(row.ruleId);
23180
23358
  }
23181
23359
  function compareLocationOrder(a, b) {
23182
- const rankA = SEVERITY_ORDER2[a.maxSeverity] ?? -1;
23183
- const rankB = SEVERITY_ORDER2[b.maxSeverity] ?? -1;
23360
+ const rankA = severityRank(a.maxSeverity) ?? -1;
23361
+ const rankB = severityRank(b.maxSeverity) ?? -1;
23184
23362
  if (rankA !== rankB) return rankA - rankB;
23185
23363
  if (a.latestDetectedAt !== b.latestDetectedAt) {
23186
23364
  return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
23187
23365
  }
23188
- if (a.repo !== b.repo) return a.repo < b.repo ? -1 : 1;
23189
- if (a.file !== b.file) return a.file < b.file ? -1 : 1;
23190
- return 0;
23366
+ const repoDiff = compareCodePoints(a.repo, b.repo);
23367
+ if (repoDiff !== 0) return repoDiff;
23368
+ return compareCodePoints(a.file, b.file);
23191
23369
  }
23192
23370
  function encodeLocationId(repo, file2) {
23193
23371
  return `${encodePart(repo)}/${encodePart(file2)}`;
@@ -23262,6 +23440,11 @@ var Policy = external_exports.object({
23262
23440
  // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23263
23441
  provenance: PolicyProvenance.optional()
23264
23442
  }).meta({ id: "Policy" });
23443
+ var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23444
+ var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23445
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23446
+ id: "RedactFallback"
23447
+ });
23265
23448
  var PolicyBundle = external_exports.object({
23266
23449
  version: external_exports.string(),
23267
23450
  policies: external_exports.array(Policy),
@@ -23309,6 +23492,16 @@ var PolicyBundle = external_exports.object({
23309
23492
  // control plane), so no name resolution stands between the decision and the
23310
23493
  // comparison.
23311
23494
  prohibitedModels: external_exports.array(external_exports.string()).optional(),
23495
+ // What a resolved `redact` becomes on a field the host cannot rewrite, as
23496
+ // the ORGANIZATION would have it. Merged raise-only against the device's own
23497
+ // `WorkspaceSettings.redactFallback` (see strongerRedactFallback below), so
23498
+ // a control plane can tighten a machine and never loosen one — the same
23499
+ // direction `mergeRaiseOnly` enforces for policies.
23500
+ //
23501
+ // Optional so an older backend, and an older on-disk cache, still parses;
23502
+ // absent leaves the device's own setting in force, which is the behaviour
23503
+ // that predates the field and the safe direction to default.
23504
+ redactFallback: RedactFallback.optional(),
23312
23505
  customKeywords: external_exports.array(external_exports.string()),
23313
23506
  fetchedAt: external_exports.iso.datetime()
23314
23507
  }).meta({ id: "PolicyBundle" });
@@ -23343,11 +23536,6 @@ function severityFloorPosture() {
23343
23536
  for (const c of DetectionCategory.options) out[c] = severityFloorPolicy(c);
23344
23537
  return out;
23345
23538
  }
23346
- var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23347
- var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23348
- var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23349
- id: "RedactFallback"
23350
- });
23351
23539
  var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23352
23540
  var BUILTIN_POLICY_SPECS = {
23353
23541
  monitor: {
@@ -23643,7 +23831,7 @@ function isVaultConsentValid(consent) {
23643
23831
  }
23644
23832
 
23645
23833
  // ../../packages/schema/src/zod/local.ts
23646
- var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23834
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 8;
23647
23835
  var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23648
23836
  var RunMode = external_exports.enum(["standalone", "attached"]);
23649
23837
  var ControlPlaneConnection = external_exports.object({
@@ -23666,6 +23854,15 @@ var HistorySyncConsent = external_exports.object({
23666
23854
  payloadVersion: external_exports.number().int().positive(),
23667
23855
  endpoint: external_exports.string()
23668
23856
  });
23857
+ var BODY_RETENTION_DEFAULT_DAYS = 30;
23858
+ var BodyRetention = external_exports.object({
23859
+ enabled: external_exports.boolean().default(false),
23860
+ // Never 0, and the ceiling is a fat-finger guard rather than a policy
23861
+ // limit — `enabled` is the real gate. A low value cannot reach a row the
23862
+ // sync ledger still owes: the sweep's age filter only ever NARROWS a
23863
+ // candidate set that is already bounded by "delivered, or never owed".
23864
+ retainDays: external_exports.number().int().min(1).max(3650).default(BODY_RETENTION_DEFAULT_DAYS)
23865
+ }).meta({ id: "BodyRetention" });
23669
23866
  var WorkspaceSettings = external_exports.object({
23670
23867
  specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
23671
23868
  runMode: RunMode.default("standalone"),
@@ -23714,7 +23911,13 @@ var WorkspaceSettings = external_exports.object({
23714
23911
  // carry prompt/reply/tool-output text in `content`; the key name predates
23715
23912
  // both widenings. Absent until granted, and a grant for a different endpoint
23716
23913
  // or an older payload no longer counts.
23717
- historySyncConsent: HistorySyncConsent.optional()
23914
+ historySyncConsent: HistorySyncConsent.optional(),
23915
+ // Local body expiry (see BodyRetention). Off until switched on; expiring a
23916
+ // body never removes the row or its findings.
23917
+ bodyRetention: BodyRetention.default({
23918
+ enabled: false,
23919
+ retainDays: BODY_RETENTION_DEFAULT_DAYS
23920
+ })
23718
23921
  });
23719
23922
  function defaultWorkspaceSettings() {
23720
23923
  return WorkspaceSettings.parse({});
@@ -23809,12 +24012,15 @@ function toCaptureAttributes(event) {
23809
24012
  ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
23810
24013
  ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
23811
24014
  ...metadata?.inspectionMs !== void 0 ? { inspection_ms: metadata.inspectionMs } : {},
24015
+ ...metadata?.redactDegradedTo !== void 0 ? { redact_degraded_to: metadata.redactDegradedTo } : {},
23812
24016
  // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
23813
24017
  // has ever populated either), but every legacy metadata key still rides
23814
24018
  // the bag rather than being silently dropped — CaptureAttributes'
23815
24019
  // `.catchall(z.unknown())` carries the long tail.
23816
24020
  ...metadata?.model !== void 0 ? { model: metadata.model } : {},
23817
- ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
24021
+ ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {},
24022
+ ...metadata?.messageId !== void 0 ? { message_id: metadata.messageId } : {},
24023
+ ...metadata?.conversationId !== void 0 ? { conversation_id: metadata.conversationId } : {}
23818
24024
  };
23819
24025
  }
23820
24026
  function captureDefinitionVersion(finding) {
@@ -23842,13 +24048,22 @@ var ManagedSettingKey = external_exports.enum([
23842
24048
  "vaultInlineReveal",
23843
24049
  "modelJudgeConsent",
23844
24050
  "dataSharesInPlace",
23845
- "redactFallback"
24051
+ "redactFallback",
24052
+ // Pins the toggle and the day count together — see BodyRetention on why the
24053
+ // two are one unit. An administrator mandating a window wants the count
24054
+ // enforced with it, not one a user can widen while the toggle stays on.
24055
+ "bodyRetention"
23846
24056
  ]).meta({ id: "ManagedSettingKey" });
23847
24057
  function isManagedSettingKey(value) {
23848
24058
  return ManagedSettingKey.safeParse(value).success;
23849
24059
  }
23850
24060
  var ManagedSettingsValues = external_exports.object({
23851
24061
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
24062
+ // `controlPlane` and `bodyRetention` are the two nested values, and both are
24063
+ // plain, non-strict objects: a key under either that this build does not know
24064
+ // is stripped and nothing reports it. The unknown-value split in
24065
+ // ManagedSettings below classifies top-level names only, so it stops at
24066
+ // these boundaries.
23852
24067
  controlPlane: external_exports.object({
23853
24068
  endpoint: external_exports.string().min(1),
23854
24069
  label: external_exports.string().min(1).optional()
@@ -23859,7 +24074,8 @@ var ManagedSettingsValues = external_exports.object({
23859
24074
  vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23860
24075
  modelJudgeConsent: external_exports.boolean().optional(),
23861
24076
  dataSharesInPlace: external_exports.boolean().optional(),
23862
- redactFallback: RedactFallback.optional()
24077
+ redactFallback: RedactFallback.optional(),
24078
+ bodyRetention: BodyRetention.optional()
23863
24079
  }).meta({ id: "ManagedSettingsValues" });
23864
24080
  var ManagedSettings = external_exports.object({
23865
24081
  specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
@@ -23867,7 +24083,21 @@ var ManagedSettings = external_exports.object({
23867
24083
  // decision from a bug. Absent renders as a generic "your organization".
23868
24084
  organization: external_exports.string().min(1).optional(),
23869
24085
  // What the administrator pinned.
23870
- values: ManagedSettingsValues.default({}),
24086
+ //
24087
+ // Parsed as a RECORD rather than as the nested schema, and split below for
24088
+ // the same reason `lockedFields` is parsed as names: a plain `z.object`
24089
+ // drops an unrecognised key and succeeds, so a pin this build does not know
24090
+ // vanished and nothing anywhere said so. A pin with no lock is a supported
24091
+ // shape — it is a DEFAULT the user may still change — so that silence hit
24092
+ // exactly the file an administrator is most likely to write while a fleet
24093
+ // is mid-upgrade.
24094
+ //
24095
+ // Splitting here rather than calling `.strict()`: strict would REFUSE the
24096
+ // file, which is the outcome the lock half already rejected — an older
24097
+ // build then runs entirely unmanaged, every pin and lock gone. A bad KNOWN
24098
+ // value still fails, because the nested schema is re-run over the known
24099
+ // subset and its issues are re-raised on this parse.
24100
+ values: external_exports.record(external_exports.string(), external_exports.unknown()).default({}),
23871
24101
  // Which of those the user may not change. A key here with no matching value
23872
24102
  // freezes whatever the user last chose; a value with no lock is a DEFAULT
23873
24103
  // the user may still override. The two are separable on purpose.
@@ -23880,17 +24110,31 @@ var ManagedSettings = external_exports.object({
23880
24110
  // the fleets most likely to carry a version skew. A name outside the enum
23881
24111
  // is still never HONOURED: the lockable set stays explicit above.
23882
24112
  lockedFields: external_exports.array(external_exports.string()).default([])
23883
- }).transform(({ lockedFields, ...rest }) => {
24113
+ }).transform(({ lockedFields, values, ...rest }, ctx) => {
23884
24114
  const known = [];
23885
24115
  const unknown2 = [];
23886
24116
  for (const name of lockedFields) {
23887
24117
  if (isManagedSettingKey(name)) known.push(name);
23888
24118
  else unknown2.push(name);
23889
24119
  }
24120
+ const knownValues = /* @__PURE__ */ Object.create(null);
24121
+ const unknownValues = [];
24122
+ for (const [name, value] of Object.entries(values)) {
24123
+ if (Object.hasOwn(ManagedSettingsValues.shape, name)) knownValues[name] = value;
24124
+ else unknownValues.push(name);
24125
+ }
24126
+ const pinned = ManagedSettingsValues.safeParse(knownValues);
24127
+ if (!pinned.success) {
24128
+ for (const issue2 of pinned.error.issues)
24129
+ ctx.addIssue({ ...issue2, path: ["values", ...issue2.path] });
24130
+ return external_exports.NEVER;
24131
+ }
23890
24132
  return {
23891
24133
  ...rest,
24134
+ values: pinned.data,
23892
24135
  lockedFields: known,
23893
- ...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {}
24136
+ ...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {},
24137
+ ...unknownValues.length > 0 ? { unknownValueFields: unknownValues } : {}
23894
24138
  };
23895
24139
  }).meta({ id: "ManagedSettings" });
23896
24140
  var NO_MANAGED_CONTEXT = { present: false, lockedFields: [] };
@@ -24155,7 +24399,23 @@ var SaveSettingsInput = external_exports.object({
24155
24399
  modelJudgeConsent: ModelJudgeConsentChoice,
24156
24400
  historySyncConsent: HistorySyncConsentChoice,
24157
24401
  vaultConsent: external_exports.string(),
24158
- vaultInlineReveal: external_exports.string()
24402
+ vaultInlineReveal: external_exports.string(),
24403
+ // Widened to `string` like its neighbours rather than typed as
24404
+ // `RedactFallback`, on this module's own layering rule: shape here, VALUE at
24405
+ // the call site, so the domain check receives the type it was written for.
24406
+ //
24407
+ // NOT because a narrower schema would reject differently. `parseActionInput`
24408
+ // is a `safeParse` wrapper and throws for no field schema, so either spelling
24409
+ // reaches a recoverable `{ ok: false }` and there is no rejected promise to
24410
+ // trade against. The real cost runs the other way and is the part worth
24411
+ // knowing: a value this schema admits and the domain enum then rejects lands
24412
+ // on the action's shared refusal, which names NO field, where a shape
24413
+ // rejection reaches `malformedInput` and names the schema key.
24414
+ redactFallback: external_exports.string(),
24415
+ // Shape only, the way the enum fields above are strings only: the RANGE is
24416
+ // `BodyRetention`'s and the action checks it there, so there is one place
24417
+ // that decides what a legal horizon is rather than two that can drift.
24418
+ bodyRetention: external_exports.object({ enabled: external_exports.boolean(), retainDays: external_exports.number() })
24159
24419
  });
24160
24420
  var AttachInput = external_exports.object({
24161
24421
  endpoint: external_exports.string(),
@@ -24327,6 +24587,52 @@ function reviewSeverityRank(reasons) {
24327
24587
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
24328
24588
  }
24329
24589
 
24590
+ // ../../packages/schema/src/zod/web-capture.ts
24591
+ var WebUsageSource = external_exports.enum(["site", "estimated", "none"]);
24592
+ var WebUsage = external_exports.object({
24593
+ inputTokens: external_exports.number().int().nonnegative().optional(),
24594
+ outputTokens: external_exports.number().int().nonnegative().optional(),
24595
+ cacheReadInputTokens: external_exports.number().int().nonnegative().optional(),
24596
+ cacheCreationInputTokens: external_exports.number().int().nonnegative().optional()
24597
+ });
24598
+ var WebToolCall = external_exports.object({
24599
+ toolUseId: external_exports.string().min(1),
24600
+ toolName: external_exports.string().min(1),
24601
+ target: external_exports.string().optional(),
24602
+ isError: external_exports.boolean().optional(),
24603
+ inputSize: external_exports.number().int().nonnegative().optional(),
24604
+ outputSize: external_exports.number().int().nonnegative().optional()
24605
+ });
24606
+ var WebExchange = external_exports.object({
24607
+ messageId: external_exports.string().min(1),
24608
+ startedAt: external_exports.iso.datetime(),
24609
+ model: external_exports.string().optional(),
24610
+ usage: WebUsage.optional(),
24611
+ usageSource: WebUsageSource,
24612
+ stopReason: external_exports.string().optional(),
24613
+ conversationId: external_exports.string().optional(),
24614
+ turnIndex: external_exports.number().int().nonnegative().optional(),
24615
+ toolCalls: external_exports.array(WebToolCall).default([]),
24616
+ // Absent when the adapter recovered no text. Capped by the caller at
24617
+ // RESPONSE_TEXT_MAX_BYTES; `truncated` records that the cap was reached, so a
24618
+ // short capture is never mistaken for a short reply.
24619
+ responseText: external_exports.string().optional(),
24620
+ truncated: external_exports.boolean().default(false)
24621
+ });
24622
+ var RESPONSE_TEXT_MAX_BYTES = 2 * 1024 * 1024;
24623
+ var WebCaptureStatus = external_exports.object({
24624
+ patched: external_exports.boolean(),
24625
+ live: external_exports.boolean(),
24626
+ blind: external_exports.boolean(),
24627
+ sendsSeenDom: external_exports.number().int().nonnegative(),
24628
+ exchangesSeenNet: external_exports.number().int().nonnegative(),
24629
+ parseFailures: external_exports.number().int().nonnegative(),
24630
+ unparsedBodies: external_exports.number().int().nonnegative(),
24631
+ // The adapter-declared JSON key paths that were absent from a real payload —
24632
+ // the earliest signal that a site's contract moved.
24633
+ shapeMisses: external_exports.array(external_exports.string()).default([])
24634
+ });
24635
+
24330
24636
  // ../../packages/persistence/src/paths.ts
24331
24637
  import {
24332
24638
  chmodSync,
@@ -24539,6 +24845,22 @@ function discardStore(file2, backup) {
24539
24845
  }
24540
24846
  }
24541
24847
 
24848
+ // ../../packages/persistence/src/internal/sql-functions.ts
24849
+ var utf8 = new TextDecoder();
24850
+ function akaLower(value) {
24851
+ if (value === null) return null;
24852
+ if (typeof value === "string") return value.toLowerCase();
24853
+ if (typeof value === "number" || typeof value === "bigint") return String(value).toLowerCase();
24854
+ return utf8.decode(value).toLowerCase();
24855
+ }
24856
+ function registerSqlFunctions(db) {
24857
+ db.function(
24858
+ "aka_lower",
24859
+ { deterministic: true, directOnly: true, useBigIntArguments: true },
24860
+ akaLower
24861
+ );
24862
+ }
24863
+
24542
24864
  // ../../packages/persistence/src/internal/sql-text.ts
24543
24865
  function escapeLikePattern(s) {
24544
24866
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -24623,6 +24945,11 @@ function schemaObjectExists(db, kind, name) {
24623
24945
  function indexExists(db, name) {
24624
24946
  return schemaObjectExists(db, "index", name);
24625
24947
  }
24948
+ function indexColumns(db, name) {
24949
+ if (!indexExists(db, name)) return [];
24950
+ const columns = db.prepare(`PRAGMA index_info(${name})`).all();
24951
+ return columns.map((c) => c.name).filter((c) => c !== null);
24952
+ }
24626
24953
  function columnNames(db, table2, opts) {
24627
24954
  const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
24628
24955
  const columns = db.prepare(`PRAGMA ${pragma}(${table2})`).all();
@@ -24684,183 +25011,825 @@ function mapRowsTolerant(rows, map2) {
24684
25011
  return out;
24685
25012
  }
24686
25013
 
24687
- // ../../packages/persistence/src/migrations.ts
24688
- function describeObject(object2) {
24689
- return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
24690
- }
24691
- function splitStatements(sql) {
24692
- return sql.split(/-->\s*statement-breakpoint/).map((s) => s.trim()).filter((s) => s.length > 0);
24693
- }
24694
- function createdIndexName(statement) {
24695
- const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
24696
- return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
25014
+ // ../../packages/persistence/src/internal/outbox-lane.ts
25015
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
25016
+ var OUTBOX_CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
25017
+
25018
+ // ../../packages/persistence/src/sync-failure.ts
25019
+ var SYNC_FAILURE_REASONS = SyncFailureReason.options;
25020
+ function syncFailureRejectCondition(column = "sync_failure") {
25021
+ const members2 = SYNC_FAILURE_REASONS.map((r) => `'${r}'`).join(", ");
25022
+ return `NEW.${column} IS NOT NULL AND NEW.${column} NOT IN (${members2})`;
24697
25023
  }
24698
- var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
24699
- function applyMigrations(db, file2) {
24700
- const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
24701
- db.exec(
24702
- "CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
24703
- );
24704
- const applied = new Set(
24705
- db.prepare("SELECT tag FROM migration_ledger").all().map((r) => r.tag)
24706
- );
24707
- const preLedgerStore = applied.size === 0 && legacyCount > 0;
24708
- const record2 = db.prepare(
24709
- "INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)"
24710
- );
24711
- for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
24712
- if (applied.has(migration.tag)) continue;
24713
- if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
24714
- const evidence = evidenceObjects(migration.sql);
24715
- const present = evidence.filter((o) => evidenceExists(db, o));
24716
- if (present.length > 0 && present.length < evidence.length) {
24717
- const missing = evidence.filter((o) => !present.includes(o));
24718
- 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.`;
24719
- akaWarn(message);
24720
- throw new Error(`[aka] ${message}`);
24721
- }
24722
- const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
24723
- const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
24724
- const statements = splitStatements(migration.sql);
24725
- if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
24726
- try {
24727
- withTransaction(
24728
- db,
24729
- () => {
24730
- for (const statement of statements) {
24731
- const indexName = createdIndexName(statement);
24732
- if (indexName === void 0) {
24733
- if (alreadyApplied) continue;
24734
- } else if (indexExists(db, indexName)) {
24735
- continue;
24736
- }
24737
- db.exec(statement);
24738
- }
24739
- if (wantsFkOff && !alreadyApplied) {
24740
- const violations = db.prepare("PRAGMA foreign_key_check").all();
24741
- if (violations.length > 0) {
24742
- throw new Error(
24743
- `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
24744
- );
24745
- }
24746
- }
24747
- record2.run(migration.tag, Date.now());
24748
- },
24749
- "IMMEDIATE"
24750
- );
24751
- } finally {
24752
- if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
24753
- }
25024
+
25025
+ // ../../packages/persistence/src/repositories/history-sync.ts
25026
+ var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
25027
+ var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
25028
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_TYPE_LIST;
25029
+ var COUNTED_EVENT_TYPES = [
25030
+ ...STRUCTURAL_EVENT_TYPES,
25031
+ ...OUTBOX_CAPTURE_EVENT_TYPES
25032
+ ];
25033
+ var COUNTED_TYPE_LIST = COUNTED_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
25034
+ var PARTITION_BUCKETS = `
25035
+ SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NULL THEN 1 ELSE 0 END) AS queued,
25036
+ SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NOT NULL THEN 1 ELSE 0 END) AS inProgress,
25037
+ SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS synced,
25038
+ SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
25039
+ AND sync_failure = 'deployment_refused' THEN 1 ELSE 0 END) AS refused,
25040
+ SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
25041
+ AND sync_failure = 'detached_undelivered' THEN 1 ELSE 0 END) AS detached,
25042
+ -- Spelled as what it INCLUDES rather than what it excludes, so a reason
25043
+ -- added later lands in no bucket and fails the sum assertion, instead
25044
+ -- of silently joining this one.
25045
+ SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
25046
+ AND (sync_failure IS NULL OR sync_failure = 'payload_invalid')
25047
+ THEN 1 ELSE 0 END) AS failed,
25048
+ COUNT(*) AS total`;
25049
+ var COUNTED_SCOPE = `
25050
+ WHERE event_type IN (${COUNTED_TYPE_LIST})
25051
+ AND (
25052
+ event_type IN (${TYPE_LIST})
25053
+ OR synced_at IS NOT NULL
25054
+ OR outbox_owed = 1
25055
+ )`;
25056
+ var SKIPPED = -1;
25057
+ var ROW_COLUMNS = `id,
25058
+ parent_id AS parentId,
25059
+ root_session_id AS rootSessionId,
25060
+ event_type AS eventType,
25061
+ host_id AS hostId,
25062
+ harness_id AS harnessId,
25063
+ source_project_id AS sourceProjectId,
25064
+ started_at AS startedAt,
25065
+ ended_at AS endedAt,
25066
+ severity,
25067
+ priority,
25068
+ content,
25069
+ content_hash AS contentHash,
25070
+ attributes`;
25071
+ var SqliteHistorySyncRepository = class {
25072
+ constructor(db) {
25073
+ this.db = db;
25074
+ this.ensureRowStmt = db.prepare(`INSERT OR IGNORE INTO history_sync (id) VALUES (1)`);
25075
+ this.sessionsStmt = db.prepare(
25076
+ `SELECT COALESCE(root_session_id, id) AS sessionId, MIN(started_at) AS earliest
25077
+ FROM audit_events
25078
+ WHERE synced_at IS NULL
25079
+ AND event_type IN (${TYPE_LIST})
25080
+ AND started_at < :before
25081
+ GROUP BY sessionId
25082
+ ORDER BY earliest
25083
+ LIMIT :limit`
25084
+ );
25085
+ this.rowsStmt = db.prepare(
25086
+ `SELECT ${ROW_COLUMNS}
25087
+ FROM audit_events
25088
+ WHERE synced_at IS NULL
25089
+ AND event_type IN (${TYPE_LIST})
25090
+ AND started_at < :before
25091
+ AND COALESCE(root_session_id, id) = :sessionId
25092
+ ORDER BY (event_type = 'session') DESC, started_at
25093
+ LIMIT :limit`
25094
+ );
25095
+ this.captureRowsStmt = db.prepare(
25096
+ `SELECT ${ROW_COLUMNS}
25097
+ FROM audit_events
25098
+ WHERE synced_at IS NULL
25099
+ AND sync_claimed_at IS NULL
25100
+ AND outbox_owed = 1
25101
+ AND event_type IN (${CAPTURE_TYPE_LIST})
25102
+ AND started_at < :before
25103
+ ORDER BY started_at
25104
+ LIMIT :limit`
25105
+ );
25106
+ this.markOwedStmt = db.prepare(
25107
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
25108
+ );
25109
+ this.markCaptureBacklogOwedStmt = db.prepare(
25110
+ `UPDATE audit_events SET outbox_owed = 1
25111
+ WHERE synced_at IS NULL
25112
+ AND event_type IN (${CAPTURE_TYPE_LIST})
25113
+ AND started_at < :before`
25114
+ );
25115
+ this.stampStmt = db.prepare(
25116
+ `UPDATE audit_events
25117
+ SET synced_at = :at,
25118
+ sync_claimed_at = NULL,
25119
+ sync_failed_at = :failedAt,
25120
+ sync_failure = :failure
25121
+ WHERE id = :id`
25122
+ );
25123
+ this.claimRowStmt = db.prepare(
25124
+ `UPDATE audit_events SET sync_claimed_at = :at WHERE id = :id AND synced_at IS NULL`
25125
+ );
25126
+ this.releaseRowStmt = db.prepare(
25127
+ `UPDATE audit_events SET sync_claimed_at = NULL WHERE id = :id`
25128
+ );
25129
+ this.releaseStaleClaimsStmt = db.prepare(
25130
+ `UPDATE audit_events SET sync_claimed_at = NULL
25131
+ WHERE sync_claimed_at IS NOT NULL AND sync_claimed_at < :staleBefore`
25132
+ );
25133
+ this.partitionStmt = db.prepare(`SELECT${PARTITION_BUCKETS}
25134
+ FROM audit_events${COUNTED_SCOPE}`);
25135
+ this.partitionByKindStmt = db.prepare(
25136
+ `SELECT event_type AS kind,${PARTITION_BUCKETS}
25137
+ FROM audit_events INDEXED BY idx_audit_events_sync${COUNTED_SCOPE}
25138
+ GROUP BY event_type`
25139
+ );
25140
+ this.countsStmt = db.prepare(
25141
+ `SELECT
25142
+ SUM(CASE WHEN synced_at IS NULL AND started_at < :before THEN 1 ELSE 0 END) AS pending,
25143
+ SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS sent,
25144
+ SUM(CASE WHEN synced_at = ${String(SKIPPED)}
25145
+ AND (sync_failure IS NULL OR sync_failure = 'payload_invalid')
25146
+ THEN 1 ELSE 0 END) AS skipped,
25147
+ SUM(CASE WHEN synced_at = ${String(SKIPPED)}
25148
+ AND sync_failure = 'deployment_refused' THEN 1 ELSE 0 END) AS refused,
25149
+ SUM(CASE WHEN synced_at = ${String(SKIPPED)}
25150
+ AND sync_failure = 'detached_undelivered' THEN 1 ELSE 0 END) AS detached
25151
+ FROM audit_events
25152
+ WHERE event_type IN (${TYPE_LIST})`
25153
+ );
25154
+ this.captureSkipCountStmt = db.prepare(
25155
+ // EVERY sentinel capture, whatever the reason — deliberately NOT split the
25156
+ // way the structural totals are. The split exists because a refusal is
25157
+ // terminal only against the deployment that gave it, and the structural
25158
+ // re-arm frees it on a change of deployment. The capture lane has no such
25159
+ // escape: re-arming a capture would offer one deployment's undelivered
25160
+ // prompts, with their text, to a deployment that never saw them, which is
25161
+ // exactly what disownCapturesStmt exists to prevent. So on this lane both
25162
+ // reasons mean the same thing — this row will not be sent — and splitting
25163
+ // them would put refused captures in a bucket nothing reads and nothing
25164
+ // frees.
25165
+ `SELECT COUNT(*) AS skipped
25166
+ FROM audit_events
25167
+ WHERE synced_at = ${String(SKIPPED)}
25168
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
25169
+ );
25170
+ this.fingerprintStmt = db.prepare(
25171
+ `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
25172
+ FROM history_sync WHERE id = 1`
25173
+ );
25174
+ this.setFingerprintStmt = db.prepare(
25175
+ `UPDATE history_sync
25176
+ SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
25177
+ WHERE id = 1`
25178
+ );
25179
+ this.disownCapturesStmt = db.prepare(
25180
+ `UPDATE audit_events SET outbox_owed = NULL
25181
+ WHERE outbox_owed IS NOT NULL
25182
+ AND event_type IN (${CAPTURE_TYPE_LIST})
25183
+ AND started_at < :attachedAt`
25184
+ );
25185
+ this.rearmStmt = db.prepare(
25186
+ `UPDATE audit_events
25187
+ SET synced_at = NULL, sync_failed_at = NULL, sync_failure = NULL
25188
+ WHERE (synced_at > 0
25189
+ OR sync_failure IN ('deployment_refused', 'detached_undelivered'))
25190
+ AND event_type IN (${TYPE_LIST})`
25191
+ );
25192
+ this.claimStmt = db.prepare(
25193
+ `UPDATE history_sync
25194
+ SET owner_pid = :pid, owner_host = :host, acquired_at = :now, heartbeat_at = :now
25195
+ WHERE id = 1
25196
+ AND (owner_pid IS NULL
25197
+ OR heartbeat_at IS NULL
25198
+ OR heartbeat_at < :staleBefore
25199
+ OR heartbeat_at > :now)`
25200
+ );
25201
+ this.heartbeatStmt = db.prepare(
25202
+ `UPDATE history_sync SET heartbeat_at = :now WHERE id = 1 AND owner_pid = :pid`
25203
+ );
25204
+ this.releaseStmt = db.prepare(
25205
+ `UPDATE history_sync
25206
+ SET owner_pid = NULL, owner_host = NULL, acquired_at = NULL, heartbeat_at = NULL
25207
+ WHERE id = 1 AND owner_pid = :pid`
25208
+ );
25209
+ this.closeWindowStmt = db.prepare(
25210
+ `UPDATE audit_events
25211
+ SET synced_at = ${String(SKIPPED)},
25212
+ sync_failed_at = :at,
25213
+ sync_failure = 'detached_undelivered'
25214
+ WHERE synced_at IS NULL
25215
+ AND event_type IN (${TYPE_LIST})
25216
+ AND started_at >= :attachedAt`
25217
+ );
25218
+ this.releaseBoundaryStmt = db.prepare(
25219
+ `UPDATE history_sync SET backlog_before = NULL WHERE id = 1`
25220
+ );
25221
+ this.freezeBoundaryStmt = db.prepare(
25222
+ `UPDATE history_sync SET backlog_before = :backlogBefore WHERE id = 1`
25223
+ );
25224
+ this.leaseStmt = db.prepare(
25225
+ `SELECT owner_pid AS ownerPid, owner_host AS ownerHost,
25226
+ acquired_at AS acquiredAt, heartbeat_at AS heartbeatAt
25227
+ FROM history_sync WHERE id = 1`
25228
+ );
25229
+ this.inspectionsStmt = db.prepare(
25230
+ `SELECT d.rule_id AS ruleId,
25231
+ d.name AS ruleName,
25232
+ d.version AS ruleVersion,
25233
+ d.category AS category,
25234
+ d.severity AS severity,
25235
+ f.span_start AS spanStart,
25236
+ f.span_end AS spanEnd,
25237
+ f.masked_match AS maskedMatch,
25238
+ f.action_taken AS actionTaken,
25239
+ f.confidence AS confidence
25240
+ FROM inspection_findings f
25241
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
25242
+ WHERE f.audit_event_id = :auditEventId
25243
+ ORDER BY f.span_start, f.id`
25244
+ );
24754
25245
  }
24755
- if (legacyCount < SQLITE_MIGRATIONS.length) {
24756
- db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
25246
+ db;
25247
+ ensureRowStmt;
25248
+ sessionsStmt;
25249
+ rowsStmt;
25250
+ stampStmt;
25251
+ countsStmt;
25252
+ fingerprintStmt;
25253
+ setFingerprintStmt;
25254
+ rearmStmt;
25255
+ claimStmt;
25256
+ heartbeatStmt;
25257
+ releaseStmt;
25258
+ leaseStmt;
25259
+ inspectionsStmt;
25260
+ closeWindowStmt;
25261
+ releaseBoundaryStmt;
25262
+ freezeBoundaryStmt;
25263
+ captureRowsStmt;
25264
+ markOwedStmt;
25265
+ markCaptureBacklogOwedStmt;
25266
+ captureSkipCountStmt;
25267
+ disownCapturesStmt;
25268
+ partitionStmt;
25269
+ partitionByKindStmt;
25270
+ claimRowStmt;
25271
+ releaseRowStmt;
25272
+ releaseStaleClaimsStmt;
25273
+ /**
25274
+ * The masked detections recorded against one tool call.
25275
+ *
25276
+ * These travel with the event because a tool call's target is not
25277
+ * re-inspectable from the event alone — unlike a capture, where the text
25278
+ * itself is re-scannable. What crosses is the masked match and the rule that
25279
+ * produced it, never the value.
25280
+ */
25281
+ inspectionsFor(auditEventId) {
25282
+ return allRows(this.inspectionsStmt, { auditEventId });
24757
25283
  }
24758
- ensureSyncedAtColumn(db, "audit_events");
24759
- ensureScanLedgerTable(db);
24760
- ensureHistorySyncTable(db);
24761
- ensureBlockedDetectionsTable(db);
24762
- ensureRuleProbeCacheTable(db);
24763
- ensureWriteGateTrigger(db);
24764
- ensureTokenUsageColumns(db);
24765
- reconcileSourceProjectIds(db);
24766
- if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
24767
- const drained = runLegacyHistoryBackfill(db);
24768
- if (drained) applyLegacyDropMigration(db, file2);
25284
+ /**
25285
+ * Sessions with structural rows still to send, oldest first.
25286
+ *
25287
+ * BOUNDED BY THE BACKLOG BOUNDARY, which is the whole correctness of this
25288
+ * read. Anything recorded after the machine attached is the live forward
25289
+ * path's to deliver; this drain exists for what was recorded before it, and a
25290
+ * row both paths send is at best a duplicate request and at worst — for a
25291
+ * session root — an overwrite of the inventory ids the live path resolved.
25292
+ */
25293
+ pendingSessions(limit, before) {
25294
+ return allRows(this.sessionsStmt, { limit, before }).map(
25295
+ (r) => r.sessionId
25296
+ );
24769
25297
  }
24770
- }
24771
- function readLegacyTables(db) {
24772
- let holdsRows = false;
24773
- const marks = [];
24774
- for (const table2 of ["events", "findings"]) {
24775
- try {
24776
- const row = db.prepare(`SELECT count(*) AS n, ifnull(max(rowid), -1) AS hi FROM ${table2}`).get();
24777
- if (row === void 0) {
24778
- holdsRows = true;
24779
- marks.push(`${table2}:unreadable`);
24780
- continue;
24781
- }
24782
- if (row.n > 0) holdsRows = true;
24783
- marks.push(`${table2}:${String(row.n)}:${String(row.hi)}`);
24784
- } catch {
24785
- holdsRows = true;
24786
- marks.push(`${table2}:unreadable`);
24787
- }
25298
+ /** One session's undelivered structural rows within the backlog, root first. */
25299
+ pendingRows(sessionId, limit, before) {
25300
+ return allRows(this.rowsStmt, { sessionId, limit, before });
24788
25301
  }
24789
- return { holdsRows, mark: marks.join("|") };
24790
- }
24791
- function applyLegacyDropMigration(db, file2) {
24792
- const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
24793
- if (!migration) return;
24794
- const before = file2 === void 0 ? void 0 : readLegacyTables(db);
24795
- if (file2 !== void 0 && before?.holdsRows === true) {
24796
- try {
24797
- backupBeforeLegacyDrop(db, file2);
24798
- } catch (error61) {
24799
- akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error61)}`);
24800
- return;
24801
- }
25302
+ /**
25303
+ * Captures this machine still owes the deployment, oldest first.
25304
+ *
25305
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
25306
+ * by a time window — see captureRowsStmt for why a window could not express
25307
+ * this. `before` is the grace window that leaves a just-recorded capture to
25308
+ * the live path.
25309
+ */
25310
+ pendingCaptureRows(limit, before) {
25311
+ return allRows(this.captureRowsStmt, { limit, before });
24802
25312
  }
24803
- try {
25313
+ /**
25314
+ * Record that a capture is OWED to the deployment.
25315
+ *
25316
+ * Written by the attached forward path when a live send did not confirm
25317
+ * delivery, and read by the drain as the whole of its eligibility test. It is
25318
+ * a fact rather than an inference: the machine was attached, the send did not
25319
+ * land, so the row is owed — which no time window can state, because the same
25320
+ * window that holds the rows a past attachment left owed also holds every
25321
+ * capture recorded while the machine was DETACHED, and those were never
25322
+ * offered to anyone.
25323
+ *
25324
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
25325
+ * out of the drain's read.
25326
+ */
25327
+ markCaptureOwed(id) {
25328
+ this.markOwedStmt.run({ id });
25329
+ }
25330
+ /**
25331
+ * Mark every capture already on disk as owed, as of `before`.
25332
+ *
25333
+ * The consent-time backfill, called once from `aka attach` when a human
25334
+ * grants existing-history consent — never from an ongoing drain pass, and
25335
+ * never inferred from a boundary that could later move. `before` is the
25336
+ * caller's own "now" at the moment consent was granted, so what this marks
25337
+ * is exactly the backlog the consent prompt already counted, not whatever a
25338
+ * later re-attach or key rotation might widen it to.
25339
+ *
25340
+ * Returns how many rows matched, for the caller to log or test against. Not a
25341
+ * count of NEWLY marked rows — a row still unsynced from an earlier call
25342
+ * matches again and is counted again, the same as `UPDATE`'s own `changes`.
25343
+ */
25344
+ markCaptureBacklogOwed(before) {
25345
+ return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
25346
+ }
25347
+ /**
25348
+ * Record delivery. Called only AFTER the far side has accepted the rows.
25349
+ *
25350
+ * CLEARS any failure reason in the same statement. A row that failed against
25351
+ * one deployment and then landed is delivered, and leaving the reason behind
25352
+ * would leave the store holding two contradictory answers about one row —
25353
+ * with the surface free to render either.
25354
+ */
25355
+ markSynced(ids, atMs) {
25356
+ this.stampAll(ids, atMs, null);
25357
+ }
25358
+ /**
25359
+ * Record that THIS MACHINE cannot express the row on the wire.
25360
+ *
25361
+ * Reserved for a local defect — a row that cannot be rebuilt into a valid
25362
+ * payload, or a body the client itself refused to send. It fails identically
25363
+ * against every deployment, so it is terminal everywhere and the re-arm leaves
25364
+ * it alone. A row that merely failed to REACH the deployment stays NULL, so it
25365
+ * is retried; marking those would turn one outage into permanent data loss.
25366
+ */
25367
+ markSkipped(ids, atMs) {
25368
+ this.stampAll(ids, SKIPPED, "payload_invalid", atMs);
25369
+ }
25370
+ /**
25371
+ * Record that THIS DEPLOYMENT refused the row.
25372
+ *
25373
+ * The same sentinel as `markSkipped`, and deliberately so: both stop the row
25374
+ * being re-offered on this lane, and `synced_at` goes on answering whether a
25375
+ * row is outstanding rather than why. What separates them is the reason, and
25376
+ * what the reason buys is the re-arm — a refusal is one deployment's verdict
25377
+ * on one body, so it is terminal only for as long as this machine points at
25378
+ * that deployment, and `rearmFor` clears it when the deployment changes.
25379
+ *
25380
+ * Leaving such a row NULL instead would be worse than the loss it replaces:
25381
+ * these reads carry no cursor, so an unstamped row the deployment refuses is
25382
+ * the head of every subsequent page, and the lane stalls behind it for ever.
25383
+ */
25384
+ markRefused(ids, atMs) {
25385
+ this.stampAll(ids, SKIPPED, "deployment_refused", atMs);
25386
+ }
25387
+ eachInTransaction(ids, run) {
25388
+ if (ids.length === 0) return;
24804
25389
  withTransaction(
24805
- db,
25390
+ this.db,
24806
25391
  () => {
24807
- const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
24808
- if (alreadyDropped) return;
24809
- if (before !== void 0 && readLegacyTables(db).mark !== before.mark) {
24810
- akaWarn(
24811
- "legacy events/findings rows changed after the pre-drop snapshot decision; deferring the drop so the next open can copy them first."
24812
- );
24813
- return;
24814
- }
24815
- for (const statement of splitStatements(migration.sql)) {
24816
- db.exec(statement);
24817
- }
24818
- db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
24819
- migration.tag,
24820
- Date.now()
24821
- );
25392
+ for (const id of ids) run(id);
24822
25393
  },
24823
25394
  "IMMEDIATE"
24824
25395
  );
24825
- } catch (error61) {
24826
- akaWarn(`legacy events/findings drop failed; deferring: ${String(error61)}`);
24827
- }
24828
- }
24829
- function backupBeforeLegacyDrop(db, file2) {
24830
- reapStalePartials(file2);
24831
- const backup = backupPath(file2, "pre-drop");
24832
- snapshotStore(db, backup);
24833
- return backup;
24834
- }
24835
- var TOKEN_USAGE_COLUMNS = [
24836
- {
24837
- name: "input_tokens",
24838
- ddl: "ALTER TABLE audit_events ADD COLUMN input_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.input_tokens')) VIRTUAL"
24839
- },
24840
- {
24841
- name: "output_tokens",
24842
- ddl: "ALTER TABLE audit_events ADD COLUMN output_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.output_tokens')) VIRTUAL"
24843
- },
24844
- {
24845
- name: "cache_creation_input_tokens",
24846
- ddl: "ALTER TABLE audit_events ADD COLUMN cache_creation_input_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.cache_creation_input_tokens')) VIRTUAL"
24847
- },
24848
- {
24849
- name: "cache_read_input_tokens",
24850
- ddl: "ALTER TABLE audit_events ADD COLUMN cache_read_input_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.cache_read_input_tokens')) VIRTUAL"
24851
- },
24852
- {
24853
- name: "model",
24854
- ddl: "ALTER TABLE audit_events ADD COLUMN model text GENERATED ALWAYS AS (json_extract(attributes, '$.model')) VIRTUAL"
24855
- },
24856
- {
24857
- name: "provider",
24858
- ddl: "ALTER TABLE audit_events ADD COLUMN provider text GENERATED ALWAYS AS (json_extract(attributes, '$.provider')) VIRTUAL"
24859
25396
  }
24860
- ];
24861
- function ensureTokenUsageColumns(db) {
24862
- if (!schemaObjectExists(db, "table", "audit_events")) return;
24863
- const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
25397
+ stampAll(ids, value, failure, failedAtMs) {
25398
+ if (ids.length === 0) return;
25399
+ const failedAt = failure === null ? null : failedAtMs ?? null;
25400
+ withTransaction(
25401
+ this.db,
25402
+ () => {
25403
+ for (const id of ids) this.stampStmt.run({ at: value, failedAt, failure, id });
25404
+ },
25405
+ "IMMEDIATE"
25406
+ );
25407
+ }
25408
+ /**
25409
+ * Claim rows as in-flight.
25410
+ *
25411
+ * Advisory in exactly the sense the lease is: it records that a send is in
25412
+ * progress so a surface can say so, and a lost claim costs a row showing as
25413
+ * queued while it is actually being sent. It is not exclusion — the far side
25414
+ * settles a duplicate on the row id.
25415
+ */
25416
+ claimRows(ids, atMs) {
25417
+ this.eachInTransaction(ids, (id) => this.claimRowStmt.run({ at: atMs, id }));
25418
+ }
25419
+ /** Give back a claim without settling — the send failed, the row is queued again. */
25420
+ releaseRows(ids) {
25421
+ this.eachInTransaction(ids, (id) => this.releaseRowStmt.run({ id }));
25422
+ }
25423
+ /**
25424
+ * Clear claims older than `staleBefore`, and report how many were cleared.
25425
+ *
25426
+ * A process killed between claiming and settling leaves rows claimed with
25427
+ * nothing left to settle them. Without this they read as "sending" for ever.
25428
+ */
25429
+ releaseStaleClaims(staleBefore) {
25430
+ return Number(this.releaseStaleClaimsStmt.run({ staleBefore }).changes);
25431
+ }
25432
+ /**
25433
+ * Every tracked row in exactly one delivery state.
25434
+ *
25435
+ * Takes no boundary on purpose. The boundary answers "what should the drain
25436
+ * pick up now", which is a different question from "what state is this row
25437
+ * in" — and a machine that has never attached has no boundary to pass, so
25438
+ * requiring one would force a caller to invent one and report the whole store
25439
+ * as queued.
25440
+ */
25441
+ /**
25442
+ * The same partition, one row per kind that a lane carries.
25443
+ *
25444
+ * A kind with nothing to report is ABSENT rather than a row of zeros: the
25445
+ * scope decides which rows exist at all, so a kind that has never been
25446
+ * recorded — or whose captures nobody ever owed — produces no group. A caller
25447
+ * rendering a fixed list of kinds must therefore treat a missing one as "no
25448
+ * rows", never as "zero sent"; the two look identical in a bar and mean
25449
+ * different things.
25450
+ */
25451
+ partitionByKind() {
25452
+ return allRows(
25453
+ this.partitionByKindStmt,
25454
+ {}
25455
+ ).map((row) => ({
25456
+ kind: row.kind,
25457
+ queued: row.queued ?? 0,
25458
+ inProgress: row.inProgress ?? 0,
25459
+ synced: row.synced ?? 0,
25460
+ failed: row.failed ?? 0,
25461
+ refused: row.refused ?? 0,
25462
+ detached: row.detached ?? 0,
25463
+ total: row.total ?? 0
25464
+ }));
25465
+ }
25466
+ partition() {
25467
+ const row = getRow(this.partitionStmt, {});
25468
+ return {
25469
+ queued: row?.queued ?? 0,
25470
+ inProgress: row?.inProgress ?? 0,
25471
+ synced: row?.synced ?? 0,
25472
+ failed: row?.failed ?? 0,
25473
+ refused: row?.refused ?? 0,
25474
+ detached: row?.detached ?? 0,
25475
+ total: row?.total ?? 0
25476
+ };
25477
+ }
25478
+ /** `pending` counts only what is inside the backlog; sent and skipped are totals. */
25479
+ counts(before) {
25480
+ const row = getRow(this.countsStmt, { before });
25481
+ const captures = getRow(this.captureSkipCountStmt);
25482
+ return {
25483
+ pending: row?.pending ?? 0,
25484
+ sent: row?.sent ?? 0,
25485
+ skipped: row?.skipped ?? 0,
25486
+ refused: row?.refused ?? 0,
25487
+ detached: row?.detached ?? 0,
25488
+ capturesSkipped: captures?.skipped ?? 0
25489
+ };
25490
+ }
25491
+ /**
25492
+ * The deployment the current stamps were made against, and where its backlog
25493
+ * ends.
25494
+ *
25495
+ * READ-ONLY. An absent row reads as an absent deployment, which is what a
25496
+ * machine that has never drained is — and every writer below seeds the row
25497
+ * before it needs one, so nothing depends on this creating it. Keeping the
25498
+ * write off the gate path matters because the gate runs on every pass while a
25499
+ * write has to take the database's write lock.
25500
+ */
25501
+ deployment() {
25502
+ const row = getRow(
25503
+ this.fingerprintStmt
25504
+ );
25505
+ return {
25506
+ fingerprint: row?.fingerprint ?? void 0,
25507
+ backlogBefore: row?.backlogBefore ?? void 0
25508
+ };
25509
+ }
25510
+ /**
25511
+ * Point the ledger at a different deployment, discarding what it recorded
25512
+ * about the previous one.
25513
+ *
25514
+ * Delivery is a fact about ONE recipient: rows sent to the deployment a
25515
+ * machine has just left are undelivered as far as the new one is concerned.
25516
+ * All four in one transaction, so a crash between them cannot leave stamps
25517
+ * attributed to the wrong deployment, a boundary that belongs to another, or
25518
+ * a disown with no re-mark to follow it.
25519
+ *
25520
+ * The boundary is written HERE and only here, which is what freezes it: a
25521
+ * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
25522
+ * unchanged, so this never runs and the backlog does not widen back over rows
25523
+ * the live path has since delivered.
25524
+ *
25525
+ * `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
25526
+ * granted existing-history consent for the deployment this call is arming —
25527
+ * a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
25528
+ * instant, `backlogBefore` is the ATTACH instant, and the two can be far
25529
+ * apart. Passed only when that grant is valid, since this method has no way
25530
+ * to check consent itself and must not mark a row owed for a machine that
25531
+ * never agreed to it. Applied AFTER the disown above, in the SAME
25532
+ * transaction: what the disown clears is every marker below `backlogBefore`,
25533
+ * which includes this deployment's OWN pre-attach rows — `aka attach` calls
25534
+ * `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
25535
+ * on the cleared side of that bound — and the re-mark in the same
25536
+ * transaction is what puts those rows back. A crash between the two cannot
25537
+ * strand the ledger disowned with nothing re-marked — the transaction either
25538
+ * lands whole or not at all, and a fingerprint mismatch that has not yet
25539
+ * committed re-enters this method on the very next pass. Omit it (the
25540
+ * structural-only tests do) to exercise the disown in isolation.
25541
+ *
25542
+ * The disown is bounded by `backlogBefore`, which is what keeps it from
25543
+ * touching a marker the NEW deployment's OWN live path has already set: B's
25544
+ * live path can mark a capture owed from the moment `aka attach` writes the
25545
+ * descriptor, before the drain's first pass ever reaches this method, and
25546
+ * such a row sits at or after the bound rather than below it. What keeps the
25547
+ * disown from eating THIS SAME CALL's own re-mark is the order, not the
25548
+ * bound — disown runs first, re-mark second, both inside the one
25549
+ * transaction above.
25550
+ */
25551
+ rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
25552
+ this.ensureRowStmt.run();
25553
+ withTransaction(
25554
+ this.db,
25555
+ () => {
25556
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
25557
+ this.rearmStmt.run();
25558
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
25559
+ this.disownCapturesStmt.run({ attachedAt: backlogBefore });
25560
+ }
25561
+ if (backfillCapturesBefore !== void 0) {
25562
+ this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
25563
+ }
25564
+ this.setFingerprintStmt.run({ fingerprint, backlogBefore });
25565
+ },
25566
+ "IMMEDIATE"
25567
+ );
25568
+ }
25569
+ /**
25570
+ * End the attached period: hand its rows to the live path, and release the
25571
+ * boundary so the next attachment can freeze a new one.
25572
+ *
25573
+ * WITHOUT THIS, a detach and a re-attach to the same deployment leave a window
25574
+ * nothing delivers. The fingerprint is unchanged, so the boundary is never
25575
+ * re-frozen and stays at the FIRST attachment — while nothing forwards at all
25576
+ * during the detached period, because the machine is not attached. Rows
25577
+ * recorded in that window sit after the boundary and before the re-attach, so
25578
+ * neither path takes them, and the pending count reports none outstanding.
25579
+ *
25580
+ * WHAT IT RECORDS, and what it deliberately does not. These rows were the
25581
+ * closing attachment's to deliver and are no longer outstanding — that is what
25582
+ * lets the boundary move. It is NOT a claim that any of them arrived, and the
25583
+ * distinction is not academic: this used to write a delivery TIME, which every
25584
+ * read treats as delivery, so one detach turned a window of undelivered rows
25585
+ * into a window of delivered ones and no surface could tell. It writes the
25586
+ * skip sentinel and a reason of its own instead, so "no longer owed" and
25587
+ * "received" stop being the same fact.
25588
+ *
25589
+ * A change of deployment still frees them (see the re-arm), because the next
25590
+ * deployment has seen none of this machine's history — so the rows reach it
25591
+ * exactly as they did when this wrote a delivery time.
25592
+ *
25593
+ * ONE TRANSACTION, so a crash cannot release the boundary while leaving the
25594
+ * window unstamped — that half-state would re-send the whole attached period
25595
+ * on the next attach, which is the failure the boundary exists to prevent.
25596
+ */
25597
+ closeAttachedWindow(attachedAtMs, atMs) {
25598
+ this.ensureRowStmt.run();
25599
+ withTransaction(
25600
+ this.db,
25601
+ () => {
25602
+ const row = getRow(this.fingerprintStmt);
25603
+ const from = row?.backlogBefore ?? attachedAtMs;
25604
+ this.closeWindowStmt.run({ at: atMs, attachedAt: from });
25605
+ this.releaseBoundaryStmt.run();
25606
+ },
25607
+ "IMMEDIATE"
25608
+ );
25609
+ }
25610
+ /**
25611
+ * Freeze a boundary for the deployment already on file, KEEPING the stamps.
25612
+ *
25613
+ * The re-attach half of the above. Distinct from `rearmFor`, which is for a
25614
+ * different deployment and therefore discards what was delivered to the old
25615
+ * one: here the recipient is the same, so everything already sent to it stays
25616
+ * sent.
25617
+ */
25618
+ freezeBoundary(backlogBefore) {
25619
+ this.ensureRowStmt.run();
25620
+ this.freezeBoundaryStmt.run({ backlogBefore });
25621
+ }
25622
+ /** Take the claim, or report that someone live already holds it. */
25623
+ claim(pid, host, nowMs, staleAfterMs) {
25624
+ this.ensureRowStmt.run();
25625
+ let taken = false;
25626
+ withTransaction(
25627
+ this.db,
25628
+ () => {
25629
+ const result = this.claimStmt.run({
25630
+ pid,
25631
+ host,
25632
+ now: nowMs,
25633
+ staleBefore: nowMs - staleAfterMs
25634
+ });
25635
+ taken = result.changes === 1;
25636
+ },
25637
+ "IMMEDIATE"
25638
+ );
25639
+ return taken;
25640
+ }
25641
+ /** Say the holder is still alive. A no-op once the claim has moved on. */
25642
+ heartbeat(pid, nowMs) {
25643
+ this.heartbeatStmt.run({ now: nowMs, pid });
25644
+ }
25645
+ /** Give the claim up. Scoped to this holder, so a taken-over claim is left be. */
25646
+ release(pid) {
25647
+ this.releaseStmt.run({ pid });
25648
+ }
25649
+ /** Who holds the claim, if anyone. Read-only, for the same reason as above. */
25650
+ lease() {
25651
+ return getRow(this.leaseStmt);
25652
+ }
25653
+ };
25654
+
25655
+ // ../../packages/persistence/src/migrations.ts
25656
+ function describeObject(object2) {
25657
+ return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
25658
+ }
25659
+ function splitStatements(sql) {
25660
+ return sql.split(/-->\s*statement-breakpoint/).map((s) => s.trim()).filter((s) => s.length > 0);
25661
+ }
25662
+ function createdIndexName(statement) {
25663
+ const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
25664
+ return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
25665
+ }
25666
+ var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
25667
+ function applyMigrations(db, file2, options = {}) {
25668
+ const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
25669
+ db.exec(
25670
+ "CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
25671
+ );
25672
+ const applied = new Set(
25673
+ db.prepare("SELECT tag FROM migration_ledger").all().map((r) => r.tag)
25674
+ );
25675
+ const preLedgerStore = applied.size === 0 && legacyCount > 0;
25676
+ const record2 = db.prepare(
25677
+ "INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)"
25678
+ );
25679
+ for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
25680
+ if (applied.has(migration.tag)) continue;
25681
+ if (options.skipTags?.has(migration.tag) === true) continue;
25682
+ if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
25683
+ const evidence = evidenceObjects(migration.sql);
25684
+ const present = evidence.filter((o) => evidenceExists(db, o));
25685
+ if (present.length > 0 && present.length < evidence.length) {
25686
+ const missing = evidence.filter((o) => !present.includes(o));
25687
+ 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.`;
25688
+ akaWarn(message);
25689
+ throw new Error(`[aka] ${message}`);
25690
+ }
25691
+ const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
25692
+ const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
25693
+ const statements = splitStatements(migration.sql);
25694
+ if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
25695
+ try {
25696
+ withTransaction(
25697
+ db,
25698
+ () => {
25699
+ for (const statement of statements) {
25700
+ const indexName = createdIndexName(statement);
25701
+ if (indexName === void 0) {
25702
+ if (alreadyApplied) continue;
25703
+ } else if (indexExists(db, indexName)) {
25704
+ continue;
25705
+ }
25706
+ db.exec(statement);
25707
+ }
25708
+ if (wantsFkOff && !alreadyApplied) {
25709
+ const violations = db.prepare("PRAGMA foreign_key_check").all();
25710
+ if (violations.length > 0) {
25711
+ throw new Error(
25712
+ `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
25713
+ );
25714
+ }
25715
+ }
25716
+ record2.run(migration.tag, Date.now());
25717
+ },
25718
+ "IMMEDIATE"
25719
+ );
25720
+ } finally {
25721
+ if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
25722
+ }
25723
+ }
25724
+ if (legacyCount < SQLITE_MIGRATIONS.length) {
25725
+ db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
25726
+ }
25727
+ ensureSyncedAtColumn(db, "audit_events");
25728
+ ensureScanLedgerTable(db);
25729
+ ensureHistorySyncTable(db);
25730
+ ensureBlockedDetectionsTable(db);
25731
+ ensureRuleProbeCacheTable(db);
25732
+ ensureWriteGateTrigger(db);
25733
+ ensureTokenUsageColumns(db);
25734
+ reconcileSourceProjectIds(db);
25735
+ if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
25736
+ const drained = runLegacyHistoryBackfill(db);
25737
+ if (drained) applyLegacyDropMigration(db, file2);
25738
+ }
25739
+ }
25740
+ function readLegacyTables(db) {
25741
+ let holdsRows = false;
25742
+ const marks = [];
25743
+ for (const table2 of ["events", "findings"]) {
25744
+ try {
25745
+ const row = db.prepare(`SELECT count(*) AS n, ifnull(max(rowid), -1) AS hi FROM ${table2}`).get();
25746
+ if (row === void 0) {
25747
+ holdsRows = true;
25748
+ marks.push(`${table2}:unreadable`);
25749
+ continue;
25750
+ }
25751
+ if (row.n > 0) holdsRows = true;
25752
+ marks.push(`${table2}:${String(row.n)}:${String(row.hi)}`);
25753
+ } catch {
25754
+ holdsRows = true;
25755
+ marks.push(`${table2}:unreadable`);
25756
+ }
25757
+ }
25758
+ return { holdsRows, mark: marks.join("|") };
25759
+ }
25760
+ function applyLegacyDropMigration(db, file2) {
25761
+ const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
25762
+ if (!migration) return;
25763
+ const before = file2 === void 0 ? void 0 : readLegacyTables(db);
25764
+ if (file2 !== void 0 && before?.holdsRows === true) {
25765
+ try {
25766
+ backupBeforeLegacyDrop(db, file2);
25767
+ } catch (error61) {
25768
+ akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error61)}`);
25769
+ return;
25770
+ }
25771
+ }
25772
+ try {
25773
+ withTransaction(
25774
+ db,
25775
+ () => {
25776
+ const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
25777
+ if (alreadyDropped) return;
25778
+ if (before !== void 0 && readLegacyTables(db).mark !== before.mark) {
25779
+ akaWarn(
25780
+ "legacy events/findings rows changed after the pre-drop snapshot decision; deferring the drop so the next open can copy them first."
25781
+ );
25782
+ return;
25783
+ }
25784
+ for (const statement of splitStatements(migration.sql)) {
25785
+ db.exec(statement);
25786
+ }
25787
+ db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
25788
+ migration.tag,
25789
+ Date.now()
25790
+ );
25791
+ },
25792
+ "IMMEDIATE"
25793
+ );
25794
+ } catch (error61) {
25795
+ akaWarn(`legacy events/findings drop failed; deferring: ${String(error61)}`);
25796
+ }
25797
+ }
25798
+ function backupBeforeLegacyDrop(db, file2) {
25799
+ reapStalePartials(file2);
25800
+ const backup = backupPath(file2, "pre-drop");
25801
+ snapshotStore(db, backup);
25802
+ return backup;
25803
+ }
25804
+ var TOKEN_USAGE_COLUMNS = [
25805
+ {
25806
+ name: "input_tokens",
25807
+ ddl: "ALTER TABLE audit_events ADD COLUMN input_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.input_tokens')) VIRTUAL"
25808
+ },
25809
+ {
25810
+ name: "output_tokens",
25811
+ ddl: "ALTER TABLE audit_events ADD COLUMN output_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.output_tokens')) VIRTUAL"
25812
+ },
25813
+ {
25814
+ name: "cache_creation_input_tokens",
25815
+ ddl: "ALTER TABLE audit_events ADD COLUMN cache_creation_input_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.cache_creation_input_tokens')) VIRTUAL"
25816
+ },
25817
+ {
25818
+ name: "cache_read_input_tokens",
25819
+ ddl: "ALTER TABLE audit_events ADD COLUMN cache_read_input_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.cache_read_input_tokens')) VIRTUAL"
25820
+ },
25821
+ {
25822
+ name: "model",
25823
+ ddl: "ALTER TABLE audit_events ADD COLUMN model text GENERATED ALWAYS AS (json_extract(attributes, '$.model')) VIRTUAL"
25824
+ },
25825
+ {
25826
+ name: "provider",
25827
+ ddl: "ALTER TABLE audit_events ADD COLUMN provider text GENERATED ALWAYS AS (json_extract(attributes, '$.provider')) VIRTUAL"
25828
+ }
25829
+ ];
25830
+ function ensureTokenUsageColumns(db) {
25831
+ if (!schemaObjectExists(db, "table", "audit_events")) return;
25832
+ const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
24864
25833
  for (const column of TOKEN_USAGE_COLUMNS) {
24865
25834
  if (!existing.has(column.name)) {
24866
25835
  db.exec(column.ddl);
@@ -25116,10 +26085,62 @@ function ensureSyncedAtColumn(db, table2) {
25116
26085
  if (!columns.includes("outbox_owed")) {
25117
26086
  db.exec(`ALTER TABLE ${table2} ADD COLUMN outbox_owed integer`);
25118
26087
  }
26088
+ if (!columns.includes("sync_failed_at")) {
26089
+ db.exec(`ALTER TABLE ${table2} ADD COLUMN sync_failed_at integer`);
26090
+ }
26091
+ if (!columns.includes("sync_failure")) {
26092
+ withTransaction(
26093
+ db,
26094
+ () => {
26095
+ db.exec(`ALTER TABLE ${table2} ADD COLUMN sync_failure text`);
26096
+ db.exec(
26097
+ `UPDATE ${table2} SET synced_at = NULL
26098
+ WHERE synced_at = -1
26099
+ AND event_type IN (${COUNTED_EVENT_TYPES.map((t) => `'${t}'`).join(", ")})`
26100
+ );
26101
+ },
26102
+ "IMMEDIATE"
26103
+ );
26104
+ }
25119
26105
  db.exec(
25120
- `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
25121
- ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
26106
+ `CREATE TRIGGER IF NOT EXISTS aka_sync_failure_guard
26107
+ BEFORE UPDATE OF sync_failure ON ${table2}
26108
+ WHEN ${syncFailureRejectCondition()}
26109
+ BEGIN SELECT RAISE(ABORT, 'sync_failure is not one of the recorded reasons'); END`
25122
26110
  );
26111
+ const syncIndexColumns = [
26112
+ "event_type",
26113
+ "synced_at",
26114
+ "sync_claimed_at",
26115
+ "started_at",
26116
+ // Appended LAST on purpose. The delivery-state read now projects it, so it
26117
+ // has to be in the index for the read to stay covered — but putting it
26118
+ // ahead of `started_at` would reorder the prefix the structural drain's
26119
+ // reads match on.
26120
+ "sync_failure"
26121
+ // `outbox_owed` is DELIBERATELY ABSENT, and it was measured both ways.
26122
+ //
26123
+ // The delivery-state read tests it — a capture's state depends on whether a
26124
+ // live forward marked it owed — so carrying it here makes that read covering
26125
+ // rather than a row fetch per row: 16 ms against 40 ms on a real 6 GB store.
26126
+ // But a sixth column changes what the planner charges for this index, and
26127
+ // with no ANALYZE statistics it plans from schema shape alone: measured, it
26128
+ // then stops choosing the per-session index for the token rollup and walks
26129
+ // every `llm_call` in the store through the event-type index instead. That
26130
+ // read grows with the store; this one does not.
26131
+ //
26132
+ // 40 ms on the largest store measured, once per render, is a cost worth
26133
+ // paying to leave every other read's plan where it was.
26134
+ ];
26135
+ const currentSyncIndex = indexColumns(db, "idx_audit_events_sync");
26136
+ const syncIndexMatches = currentSyncIndex.length === syncIndexColumns.length && currentSyncIndex.every((column, i) => column === syncIndexColumns[i]);
26137
+ if (!syncIndexMatches) {
26138
+ db.exec("DROP INDEX IF EXISTS idx_audit_events_sync");
26139
+ db.exec(
26140
+ `CREATE INDEX idx_audit_events_sync
26141
+ ON audit_events (${syncIndexColumns.join(", ")})`
26142
+ );
26143
+ }
25123
26144
  db.exec(
25124
26145
  `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25125
26146
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
@@ -25341,7 +26362,11 @@ function buildAuditEvent(row) {
25341
26362
  link: linkParsed?.success ? linkParsed.data : null,
25342
26363
  targetId: row.target_id,
25343
26364
  internal: intToBool(row.internal),
25344
- flagged: intToBool(row.flagged)
26365
+ flagged: intToBool(row.flagged),
26366
+ // Only meaningful when the title came out empty — a row whose body was
26367
+ // expired but whose title fell back to `tool_name` still has something to
26368
+ // render, and flagging it would make the view apologise for nothing.
26369
+ bodyExpired: row.content_expired_at !== null && (row.title ?? "") === ""
25345
26370
  };
25346
26371
  }
25347
26372
  var TIMELINE_COLUMNS = `
@@ -25349,6 +26374,7 @@ var TIMELINE_COLUMNS = `
25349
26374
  event_type,
25350
26375
  started_at,
25351
26376
  coalesce(content, json_extract(attributes, '$.tool_name')) AS title,
26377
+ content_expired_at,
25352
26378
  coalesce(json_extract(attributes, '$.detail'), json_extract(attributes, '$.target')) AS detail,
25353
26379
  coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25354
26380
  json_extract(attributes, '$.severity') AS severity,
@@ -26014,6 +27040,88 @@ var SqliteAuditEventsRepository = class {
26014
27040
  }
26015
27041
  };
26016
27042
 
27043
+ // ../../packages/persistence/src/repositories/body-retention.ts
27044
+ var DEFAULT_BATCH_SIZE = 500;
27045
+ var DEFAULT_MAX_ROWS = 5e4;
27046
+ var SYNC_LANE_TYPES_SQL = OUTBOX_CAPTURE_TYPE_LIST;
27047
+ var SqliteBodyRetentionRepository = class {
27048
+ constructor(db) {
27049
+ this.db = db;
27050
+ const select = (laneClause) => `
27051
+ SELECT id, LENGTH(CAST(content AS BLOB)) AS bytes
27052
+ FROM audit_events
27053
+ WHERE content IS NOT NULL
27054
+ AND started_at < :cutoff
27055
+ AND event_type IN (${CAPTURE_EVENT_TYPES_SQL})
27056
+ ${laneClause}
27057
+ ORDER BY started_at
27058
+ LIMIT :limit`;
27059
+ this.candidatesStmt = this.db.prepare(select(""));
27060
+ this.candidatesSyncSafeStmt = this.db.prepare(
27061
+ select(`AND (event_type NOT IN (${SYNC_LANE_TYPES_SQL}) OR synced_at IS NOT NULL)`)
27062
+ );
27063
+ this.heldBySyncStmt = this.db.prepare(`
27064
+ SELECT COUNT(*) AS n
27065
+ FROM audit_events
27066
+ WHERE content IS NOT NULL
27067
+ AND started_at < :cutoff
27068
+ AND event_type IN (${SYNC_LANE_TYPES_SQL})
27069
+ AND synced_at IS NULL`);
27070
+ this.expireStmt = this.db.prepare(
27071
+ `UPDATE audit_events SET content = NULL, content_expired_at = :now WHERE id = :id`
27072
+ );
27073
+ }
27074
+ db;
27075
+ candidatesStmt;
27076
+ candidatesSyncSafeStmt;
27077
+ heldBySyncStmt;
27078
+ expireStmt;
27079
+ /** How many bytes a pass with these options would free, changing nothing. */
27080
+ preview(opts) {
27081
+ const limit = opts.maxRows ?? DEFAULT_MAX_ROWS;
27082
+ const stmt = opts.sweepSyncLane ? this.candidatesStmt : this.candidatesSyncSafeStmt;
27083
+ const rows = stmt.all({ cutoff: opts.cutoff, limit });
27084
+ return {
27085
+ rowsExpired: rows.length,
27086
+ bytesFreed: rows.reduce((sum, r) => sum + r.bytes, 0),
27087
+ rowsHeldBySync: this.countHeldBySync(opts)
27088
+ };
27089
+ }
27090
+ /** Clear eligible bodies, in bounded batches. */
27091
+ expire(opts) {
27092
+ const batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
27093
+ const maxRows = opts.maxRows ?? DEFAULT_MAX_ROWS;
27094
+ const stmt = opts.sweepSyncLane ? this.candidatesStmt : this.candidatesSyncSafeStmt;
27095
+ let rowsExpired = 0;
27096
+ let bytesFreed = 0;
27097
+ let done = true;
27098
+ while (rowsExpired < maxRows) {
27099
+ const remaining = Math.min(batchSize, maxRows - rowsExpired);
27100
+ const batch = stmt.all({ cutoff: opts.cutoff, limit: remaining });
27101
+ if (batch.length === 0) break;
27102
+ withTransaction(
27103
+ this.db,
27104
+ () => {
27105
+ for (const row of batch) this.expireStmt.run({ id: row.id, now: opts.now });
27106
+ },
27107
+ "IMMEDIATE"
27108
+ );
27109
+ rowsExpired += batch.length;
27110
+ bytesFreed += batch.reduce((sum, r) => sum + r.bytes, 0);
27111
+ if (batch.length < remaining) break;
27112
+ if (rowsExpired >= maxRows) {
27113
+ done = stmt.all({ cutoff: opts.cutoff, limit: 1 }).length === 0;
27114
+ }
27115
+ }
27116
+ return { rowsExpired, bytesFreed, rowsHeldBySync: this.countHeldBySync(opts), done };
27117
+ }
27118
+ countHeldBySync(opts) {
27119
+ if (opts.sweepSyncLane) return 0;
27120
+ const row = this.heldBySyncStmt.get({ cutoff: opts.cutoff });
27121
+ return row.n;
27122
+ }
27123
+ };
27124
+
26017
27125
  // ../../packages/persistence/src/repositories/classified-data.ts
26018
27126
  var SqliteClassifiedDataRepository = class {
26019
27127
  constructor(db) {
@@ -26842,7 +27950,15 @@ function toFlatFindingRow(r) {
26842
27950
  ...r.tool_name === null ? {} : { toolName: r.tool_name },
26843
27951
  eventId: r.event_id,
26844
27952
  ...r.session_id === null ? {} : { sessionId: r.session_id },
26845
- status: deriveInstanceStatus(r)
27953
+ status: deriveInstanceStatus(r),
27954
+ delivery: deriveFindingDelivery({
27955
+ kind: r.kind,
27956
+ syncedAt: r.synced_at,
27957
+ syncClaimedAt: r.sync_claimed_at,
27958
+ syncFailedAt: r.sync_failed_at,
27959
+ syncFailure: r.sync_failure,
27960
+ outboxOwed: r.outbox_owed
27961
+ })
26846
27962
  };
26847
27963
  }
26848
27964
  function encodeGroupCursor(group) {
@@ -26906,7 +28022,10 @@ var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS c
26906
28022
  e.tool_name AS tool_name,
26907
28023
  f.audit_event_id AS event_id, e.root_session_id AS session_id,
26908
28024
  e.event_type AS kind, f.finding_key AS finding_key,
26909
- ${latestResolutionStatusSql("f")} AS latest_status`;
28025
+ ${latestResolutionStatusSql("f")} AS latest_status,
28026
+ e.synced_at AS synced_at, e.sync_claimed_at AS sync_claimed_at,
28027
+ e.sync_failed_at AS sync_failed_at, e.sync_failure AS sync_failure,
28028
+ e.outbox_owed AS outbox_owed`;
26910
28029
  var DAY_MS3 = 864e5;
26911
28030
  var SqliteFindingsRepository = class {
26912
28031
  constructor(db) {
@@ -27151,6 +28270,7 @@ var SqliteFindingsRepository = class {
27151
28270
  providers: query.provider,
27152
28271
  actions: query.action,
27153
28272
  statuses: query.status,
28273
+ deliveries: query.deployment,
27154
28274
  tools: query.tool,
27155
28275
  repo: query.repo,
27156
28276
  file: query.file,
@@ -27218,6 +28338,7 @@ var SqliteFindingsRepository = class {
27218
28338
  providers: query.provider,
27219
28339
  actions: query.action,
27220
28340
  statuses: query.status,
28341
+ deliveries: query.deployment,
27221
28342
  tools: query.tool,
27222
28343
  q: query.q
27223
28344
  };
@@ -27481,7 +28602,9 @@ var SqliteFindingsRepository = class {
27481
28602
  )
27482
28603
  );
27483
28604
  for (const row of grouped) {
27484
- if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
28605
+ if (Object.hasOwn(byAction, row.action_taken)) {
28606
+ byAction[row.action_taken] = row.c;
28607
+ }
27485
28608
  }
27486
28609
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
27487
28610
  const sevRows = allRows(
@@ -27498,7 +28621,9 @@ var SqliteFindingsRepository = class {
27498
28621
  )
27499
28622
  );
27500
28623
  for (const row of sevRows) {
27501
- if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
28624
+ if (Object.hasOwn(bySeverity, row.severity)) {
28625
+ bySeverity[row.severity] = row.c;
28626
+ }
27502
28627
  }
27503
28628
  const categories = ENFORCEABLE_CATEGORIES;
27504
28629
  const enabledRows = allRows(
@@ -27547,525 +28672,6 @@ function isoDay(ms) {
27547
28672
  return new Date(ms).toISOString().slice(0, 10);
27548
28673
  }
27549
28674
 
27550
- // ../../packages/persistence/src/repositories/history-sync.ts
27551
- var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27552
- var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27553
- var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27554
- var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27555
- var SKIPPED = -1;
27556
- var ROW_COLUMNS = `id,
27557
- parent_id AS parentId,
27558
- root_session_id AS rootSessionId,
27559
- event_type AS eventType,
27560
- host_id AS hostId,
27561
- harness_id AS harnessId,
27562
- source_project_id AS sourceProjectId,
27563
- started_at AS startedAt,
27564
- ended_at AS endedAt,
27565
- severity,
27566
- priority,
27567
- content,
27568
- content_hash AS contentHash,
27569
- attributes`;
27570
- var SqliteHistorySyncRepository = class {
27571
- constructor(db) {
27572
- this.db = db;
27573
- this.ensureRowStmt = db.prepare(`INSERT OR IGNORE INTO history_sync (id) VALUES (1)`);
27574
- this.sessionsStmt = db.prepare(
27575
- `SELECT COALESCE(root_session_id, id) AS sessionId, MIN(started_at) AS earliest
27576
- FROM audit_events
27577
- WHERE synced_at IS NULL
27578
- AND event_type IN (${TYPE_LIST})
27579
- AND started_at < :before
27580
- GROUP BY sessionId
27581
- ORDER BY earliest
27582
- LIMIT :limit`
27583
- );
27584
- this.rowsStmt = db.prepare(
27585
- `SELECT ${ROW_COLUMNS}
27586
- FROM audit_events
27587
- WHERE synced_at IS NULL
27588
- AND event_type IN (${TYPE_LIST})
27589
- AND started_at < :before
27590
- AND COALESCE(root_session_id, id) = :sessionId
27591
- ORDER BY (event_type = 'session') DESC, started_at
27592
- LIMIT :limit`
27593
- );
27594
- this.captureRowsStmt = db.prepare(
27595
- `SELECT ${ROW_COLUMNS}
27596
- FROM audit_events
27597
- WHERE synced_at IS NULL
27598
- AND sync_claimed_at IS NULL
27599
- AND outbox_owed = 1
27600
- AND event_type IN (${CAPTURE_TYPE_LIST})
27601
- AND started_at < :before
27602
- ORDER BY started_at
27603
- LIMIT :limit`
27604
- );
27605
- this.markOwedStmt = db.prepare(
27606
- `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27607
- );
27608
- this.markCaptureBacklogOwedStmt = db.prepare(
27609
- `UPDATE audit_events SET outbox_owed = 1
27610
- WHERE synced_at IS NULL
27611
- AND event_type IN (${CAPTURE_TYPE_LIST})
27612
- AND started_at < :before`
27613
- );
27614
- this.stampStmt = db.prepare(
27615
- `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27616
- );
27617
- this.claimRowStmt = db.prepare(
27618
- `UPDATE audit_events SET sync_claimed_at = :at WHERE id = :id AND synced_at IS NULL`
27619
- );
27620
- this.releaseRowStmt = db.prepare(
27621
- `UPDATE audit_events SET sync_claimed_at = NULL WHERE id = :id`
27622
- );
27623
- this.releaseStaleClaimsStmt = db.prepare(
27624
- `UPDATE audit_events SET sync_claimed_at = NULL
27625
- WHERE sync_claimed_at IS NOT NULL AND sync_claimed_at < :staleBefore`
27626
- );
27627
- this.partitionStmt = db.prepare(
27628
- `SELECT
27629
- SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NULL THEN 1 ELSE 0 END) AS queued,
27630
- SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NOT NULL THEN 1 ELSE 0 END) AS inProgress,
27631
- SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS synced,
27632
- SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0 THEN 1 ELSE 0 END) AS failed,
27633
- COUNT(*) AS total
27634
- FROM audit_events
27635
- WHERE event_type IN (${TYPE_LIST})`
27636
- );
27637
- this.countsStmt = db.prepare(
27638
- `SELECT
27639
- SUM(CASE WHEN synced_at IS NULL AND started_at < :before THEN 1 ELSE 0 END) AS pending,
27640
- SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS sent,
27641
- SUM(CASE WHEN synced_at = ${String(SKIPPED)} THEN 1 ELSE 0 END) AS skipped
27642
- FROM audit_events
27643
- WHERE event_type IN (${TYPE_LIST})`
27644
- );
27645
- this.captureSkipCountStmt = db.prepare(
27646
- `SELECT COUNT(*) AS skipped
27647
- FROM audit_events
27648
- WHERE synced_at = ${String(SKIPPED)}
27649
- AND event_type IN (${CAPTURE_TYPE_LIST})`
27650
- );
27651
- this.fingerprintStmt = db.prepare(
27652
- `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27653
- FROM history_sync WHERE id = 1`
27654
- );
27655
- this.setFingerprintStmt = db.prepare(
27656
- `UPDATE history_sync
27657
- SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27658
- WHERE id = 1`
27659
- );
27660
- this.disownCapturesStmt = db.prepare(
27661
- `UPDATE audit_events SET outbox_owed = NULL
27662
- WHERE outbox_owed IS NOT NULL
27663
- AND event_type IN (${CAPTURE_TYPE_LIST})
27664
- AND started_at < :attachedAt`
27665
- );
27666
- this.rearmStmt = db.prepare(
27667
- `UPDATE audit_events SET synced_at = NULL
27668
- WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
27669
- );
27670
- this.claimStmt = db.prepare(
27671
- `UPDATE history_sync
27672
- SET owner_pid = :pid, owner_host = :host, acquired_at = :now, heartbeat_at = :now
27673
- WHERE id = 1
27674
- AND (owner_pid IS NULL
27675
- OR heartbeat_at IS NULL
27676
- OR heartbeat_at < :staleBefore
27677
- OR heartbeat_at > :now)`
27678
- );
27679
- this.heartbeatStmt = db.prepare(
27680
- `UPDATE history_sync SET heartbeat_at = :now WHERE id = 1 AND owner_pid = :pid`
27681
- );
27682
- this.releaseStmt = db.prepare(
27683
- `UPDATE history_sync
27684
- SET owner_pid = NULL, owner_host = NULL, acquired_at = NULL, heartbeat_at = NULL
27685
- WHERE id = 1 AND owner_pid = :pid`
27686
- );
27687
- this.closeWindowStmt = db.prepare(
27688
- `UPDATE audit_events SET synced_at = :at
27689
- WHERE synced_at IS NULL
27690
- AND event_type IN (${TYPE_LIST})
27691
- AND started_at >= :attachedAt`
27692
- );
27693
- this.releaseBoundaryStmt = db.prepare(
27694
- `UPDATE history_sync SET backlog_before = NULL WHERE id = 1`
27695
- );
27696
- this.freezeBoundaryStmt = db.prepare(
27697
- `UPDATE history_sync SET backlog_before = :backlogBefore WHERE id = 1`
27698
- );
27699
- this.leaseStmt = db.prepare(
27700
- `SELECT owner_pid AS ownerPid, owner_host AS ownerHost,
27701
- acquired_at AS acquiredAt, heartbeat_at AS heartbeatAt
27702
- FROM history_sync WHERE id = 1`
27703
- );
27704
- this.inspectionsStmt = db.prepare(
27705
- `SELECT d.rule_id AS ruleId,
27706
- d.name AS ruleName,
27707
- d.version AS ruleVersion,
27708
- d.category AS category,
27709
- d.severity AS severity,
27710
- f.span_start AS spanStart,
27711
- f.span_end AS spanEnd,
27712
- f.masked_match AS maskedMatch,
27713
- f.action_taken AS actionTaken,
27714
- f.confidence AS confidence
27715
- FROM inspection_findings f
27716
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27717
- WHERE f.audit_event_id = :auditEventId
27718
- ORDER BY f.span_start, f.id`
27719
- );
27720
- }
27721
- db;
27722
- ensureRowStmt;
27723
- sessionsStmt;
27724
- rowsStmt;
27725
- stampStmt;
27726
- countsStmt;
27727
- fingerprintStmt;
27728
- setFingerprintStmt;
27729
- rearmStmt;
27730
- claimStmt;
27731
- heartbeatStmt;
27732
- releaseStmt;
27733
- leaseStmt;
27734
- inspectionsStmt;
27735
- closeWindowStmt;
27736
- releaseBoundaryStmt;
27737
- freezeBoundaryStmt;
27738
- captureRowsStmt;
27739
- markOwedStmt;
27740
- markCaptureBacklogOwedStmt;
27741
- captureSkipCountStmt;
27742
- disownCapturesStmt;
27743
- partitionStmt;
27744
- claimRowStmt;
27745
- releaseRowStmt;
27746
- releaseStaleClaimsStmt;
27747
- /**
27748
- * The masked detections recorded against one tool call.
27749
- *
27750
- * These travel with the event because a tool call's target is not
27751
- * re-inspectable from the event alone — unlike a capture, where the text
27752
- * itself is re-scannable. What crosses is the masked match and the rule that
27753
- * produced it, never the value.
27754
- */
27755
- inspectionsFor(auditEventId) {
27756
- return allRows(this.inspectionsStmt, { auditEventId });
27757
- }
27758
- /**
27759
- * Sessions with structural rows still to send, oldest first.
27760
- *
27761
- * BOUNDED BY THE BACKLOG BOUNDARY, which is the whole correctness of this
27762
- * read. Anything recorded after the machine attached is the live forward
27763
- * path's to deliver; this drain exists for what was recorded before it, and a
27764
- * row both paths send is at best a duplicate request and at worst — for a
27765
- * session root — an overwrite of the inventory ids the live path resolved.
27766
- */
27767
- pendingSessions(limit, before) {
27768
- return allRows(this.sessionsStmt, { limit, before }).map(
27769
- (r) => r.sessionId
27770
- );
27771
- }
27772
- /** One session's undelivered structural rows within the backlog, root first. */
27773
- pendingRows(sessionId, limit, before) {
27774
- return allRows(this.rowsStmt, { sessionId, limit, before });
27775
- }
27776
- /**
27777
- * Captures this machine still owes the deployment, oldest first.
27778
- *
27779
- * Selected by the `outbox_owed` marker the attached forward path writes, not
27780
- * by a time window — see captureRowsStmt for why a window could not express
27781
- * this. `before` is the grace window that leaves a just-recorded capture to
27782
- * the live path.
27783
- */
27784
- pendingCaptureRows(limit, before) {
27785
- return allRows(this.captureRowsStmt, { limit, before });
27786
- }
27787
- /**
27788
- * Record that a capture is OWED to the deployment.
27789
- *
27790
- * Written by the attached forward path when a live send did not confirm
27791
- * delivery, and read by the drain as the whole of its eligibility test. It is
27792
- * a fact rather than an inference: the machine was attached, the send did not
27793
- * land, so the row is owed — which no time window can state, because the same
27794
- * window that holds the rows a past attachment left owed also holds every
27795
- * capture recorded while the machine was DETACHED, and those were never
27796
- * offered to anyone.
27797
- *
27798
- * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27799
- * out of the drain's read.
27800
- */
27801
- markCaptureOwed(id) {
27802
- this.markOwedStmt.run({ id });
27803
- }
27804
- /**
27805
- * Mark every capture already on disk as owed, as of `before`.
27806
- *
27807
- * The consent-time backfill, called once from `aka attach` when a human
27808
- * grants existing-history consent — never from an ongoing drain pass, and
27809
- * never inferred from a boundary that could later move. `before` is the
27810
- * caller's own "now" at the moment consent was granted, so what this marks
27811
- * is exactly the backlog the consent prompt already counted, not whatever a
27812
- * later re-attach or key rotation might widen it to.
27813
- *
27814
- * Returns how many rows matched, for the caller to log or test against. Not a
27815
- * count of NEWLY marked rows — a row still unsynced from an earlier call
27816
- * matches again and is counted again, the same as `UPDATE`'s own `changes`.
27817
- */
27818
- markCaptureBacklogOwed(before) {
27819
- return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
27820
- }
27821
- /** Record delivery. Called only AFTER the far side has accepted the rows. */
27822
- markSynced(ids, atMs) {
27823
- this.stampAll(ids, atMs);
27824
- }
27825
- /**
27826
- * Record that a row will never be sent.
27827
- *
27828
- * Reserved for a local defect — a row that cannot be rebuilt into a valid
27829
- * payload. A row that merely failed to reach the deployment stays NULL, so it
27830
- * is retried; marking those would turn one outage into permanent data loss.
27831
- */
27832
- markSkipped(ids) {
27833
- this.stampAll(ids, SKIPPED);
27834
- }
27835
- eachInTransaction(ids, run) {
27836
- if (ids.length === 0) return;
27837
- withTransaction(
27838
- this.db,
27839
- () => {
27840
- for (const id of ids) run(id);
27841
- },
27842
- "IMMEDIATE"
27843
- );
27844
- }
27845
- stampAll(ids, value) {
27846
- if (ids.length === 0) return;
27847
- withTransaction(
27848
- this.db,
27849
- () => {
27850
- for (const id of ids) this.stampStmt.run({ at: value, id });
27851
- },
27852
- "IMMEDIATE"
27853
- );
27854
- }
27855
- /**
27856
- * Claim rows as in-flight.
27857
- *
27858
- * Advisory in exactly the sense the lease is: it records that a send is in
27859
- * progress so a surface can say so, and a lost claim costs a row showing as
27860
- * queued while it is actually being sent. It is not exclusion — the far side
27861
- * settles a duplicate on the row id.
27862
- */
27863
- claimRows(ids, atMs) {
27864
- this.eachInTransaction(ids, (id) => this.claimRowStmt.run({ at: atMs, id }));
27865
- }
27866
- /** Give back a claim without settling — the send failed, the row is queued again. */
27867
- releaseRows(ids) {
27868
- this.eachInTransaction(ids, (id) => this.releaseRowStmt.run({ id }));
27869
- }
27870
- /**
27871
- * Clear claims older than `staleBefore`, and report how many were cleared.
27872
- *
27873
- * A process killed between claiming and settling leaves rows claimed with
27874
- * nothing left to settle them. Without this they read as "sending" for ever.
27875
- */
27876
- releaseStaleClaims(staleBefore) {
27877
- return Number(this.releaseStaleClaimsStmt.run({ staleBefore }).changes);
27878
- }
27879
- /**
27880
- * Every tracked row in exactly one delivery state.
27881
- *
27882
- * Takes no boundary on purpose. The boundary answers "what should the drain
27883
- * pick up now", which is a different question from "what state is this row
27884
- * in" — and a machine that has never attached has no boundary to pass, so
27885
- * requiring one would force a caller to invent one and report the whole store
27886
- * as queued.
27887
- */
27888
- partition() {
27889
- const row = getRow(this.partitionStmt, {});
27890
- return {
27891
- queued: row?.queued ?? 0,
27892
- inProgress: row?.inProgress ?? 0,
27893
- synced: row?.synced ?? 0,
27894
- failed: row?.failed ?? 0,
27895
- total: row?.total ?? 0
27896
- };
27897
- }
27898
- /** `pending` counts only what is inside the backlog; sent and skipped are totals. */
27899
- counts(before) {
27900
- const row = getRow(
27901
- this.countsStmt,
27902
- { before }
27903
- );
27904
- const captures = getRow(this.captureSkipCountStmt);
27905
- return {
27906
- pending: row?.pending ?? 0,
27907
- sent: row?.sent ?? 0,
27908
- skipped: row?.skipped ?? 0,
27909
- capturesSkipped: captures?.skipped ?? 0
27910
- };
27911
- }
27912
- /**
27913
- * The deployment the current stamps were made against, and where its backlog
27914
- * ends.
27915
- *
27916
- * READ-ONLY. An absent row reads as an absent deployment, which is what a
27917
- * machine that has never drained is — and every writer below seeds the row
27918
- * before it needs one, so nothing depends on this creating it. Keeping the
27919
- * write off the gate path matters because the gate runs on every pass while a
27920
- * write has to take the database's write lock.
27921
- */
27922
- deployment() {
27923
- const row = getRow(
27924
- this.fingerprintStmt
27925
- );
27926
- return {
27927
- fingerprint: row?.fingerprint ?? void 0,
27928
- backlogBefore: row?.backlogBefore ?? void 0
27929
- };
27930
- }
27931
- /**
27932
- * Point the ledger at a different deployment, discarding what it recorded
27933
- * about the previous one.
27934
- *
27935
- * Delivery is a fact about ONE recipient: rows sent to the deployment a
27936
- * machine has just left are undelivered as far as the new one is concerned.
27937
- * All four in one transaction, so a crash between them cannot leave stamps
27938
- * attributed to the wrong deployment, a boundary that belongs to another, or
27939
- * a disown with no re-mark to follow it.
27940
- *
27941
- * The boundary is written HERE and only here, which is what freezes it: a
27942
- * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
27943
- * unchanged, so this never runs and the backlog does not widen back over rows
27944
- * the live path has since delivered.
27945
- *
27946
- * `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
27947
- * granted existing-history consent for the deployment this call is arming —
27948
- * a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
27949
- * instant, `backlogBefore` is the ATTACH instant, and the two can be far
27950
- * apart. Passed only when that grant is valid, since this method has no way
27951
- * to check consent itself and must not mark a row owed for a machine that
27952
- * never agreed to it. Applied AFTER the disown above, in the SAME
27953
- * transaction: what the disown clears is every marker below `backlogBefore`,
27954
- * which includes this deployment's OWN pre-attach rows — `aka attach` calls
27955
- * `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
27956
- * on the cleared side of that bound — and the re-mark in the same
27957
- * transaction is what puts those rows back. A crash between the two cannot
27958
- * strand the ledger disowned with nothing re-marked — the transaction either
27959
- * lands whole or not at all, and a fingerprint mismatch that has not yet
27960
- * committed re-enters this method on the very next pass. Omit it (the
27961
- * structural-only tests do) to exercise the disown in isolation.
27962
- *
27963
- * The disown is bounded by `backlogBefore`, which is what keeps it from
27964
- * touching a marker the NEW deployment's OWN live path has already set: B's
27965
- * live path can mark a capture owed from the moment `aka attach` writes the
27966
- * descriptor, before the drain's first pass ever reaches this method, and
27967
- * such a row sits at or after the bound rather than below it. What keeps the
27968
- * disown from eating THIS SAME CALL's own re-mark is the order, not the
27969
- * bound — disown runs first, re-mark second, both inside the one
27970
- * transaction above.
27971
- */
27972
- rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
27973
- this.ensureRowStmt.run();
27974
- withTransaction(
27975
- this.db,
27976
- () => {
27977
- const previous = getRow(this.fingerprintStmt)?.fingerprint;
27978
- this.rearmStmt.run();
27979
- if (previous !== null && previous !== void 0 && previous !== fingerprint) {
27980
- this.disownCapturesStmt.run({ attachedAt: backlogBefore });
27981
- }
27982
- if (backfillCapturesBefore !== void 0) {
27983
- this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
27984
- }
27985
- this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27986
- },
27987
- "IMMEDIATE"
27988
- );
27989
- }
27990
- /**
27991
- * End the attached period: hand its rows to the live path, and release the
27992
- * boundary so the next attachment can freeze a new one.
27993
- *
27994
- * WITHOUT THIS, a detach and a re-attach to the same deployment leave a window
27995
- * nothing delivers. The fingerprint is unchanged, so the boundary is never
27996
- * re-frozen and stays at the FIRST attachment — while nothing forwards at all
27997
- * during the detached period, because the machine is not attached. Rows
27998
- * recorded in that window sit after the boundary and before the re-attach, so
27999
- * neither path takes them, and the pending count reports none outstanding.
28000
- *
28001
- * Stamping the attached window is not a claim that every one of those rows
28002
- * reached the deployment — the live path drops on failure and says so
28003
- * elsewhere. It records that they were ITS to deliver, which is exactly the
28004
- * status quo: they sit outside the frozen boundary today and are equally never
28005
- * re-sent. Making it explicit is what lets the boundary move.
28006
- *
28007
- * ONE TRANSACTION, so a crash cannot release the boundary while leaving the
28008
- * window unstamped — that half-state would re-send the whole attached period
28009
- * on the next attach, which is the failure the boundary exists to prevent.
28010
- */
28011
- closeAttachedWindow(attachedAtMs, atMs) {
28012
- this.ensureRowStmt.run();
28013
- withTransaction(
28014
- this.db,
28015
- () => {
28016
- const row = getRow(this.fingerprintStmt);
28017
- const from = row?.backlogBefore ?? attachedAtMs;
28018
- this.closeWindowStmt.run({ at: atMs, attachedAt: from });
28019
- this.releaseBoundaryStmt.run();
28020
- },
28021
- "IMMEDIATE"
28022
- );
28023
- }
28024
- /**
28025
- * Freeze a boundary for the deployment already on file, KEEPING the stamps.
28026
- *
28027
- * The re-attach half of the above. Distinct from `rearmFor`, which is for a
28028
- * different deployment and therefore discards what was delivered to the old
28029
- * one: here the recipient is the same, so everything already sent to it stays
28030
- * sent.
28031
- */
28032
- freezeBoundary(backlogBefore) {
28033
- this.ensureRowStmt.run();
28034
- this.freezeBoundaryStmt.run({ backlogBefore });
28035
- }
28036
- /** Take the claim, or report that someone live already holds it. */
28037
- claim(pid, host, nowMs, staleAfterMs) {
28038
- this.ensureRowStmt.run();
28039
- let taken = false;
28040
- withTransaction(
28041
- this.db,
28042
- () => {
28043
- const result = this.claimStmt.run({
28044
- pid,
28045
- host,
28046
- now: nowMs,
28047
- staleBefore: nowMs - staleAfterMs
28048
- });
28049
- taken = result.changes === 1;
28050
- },
28051
- "IMMEDIATE"
28052
- );
28053
- return taken;
28054
- }
28055
- /** Say the holder is still alive. A no-op once the claim has moved on. */
28056
- heartbeat(pid, nowMs) {
28057
- this.heartbeatStmt.run({ now: nowMs, pid });
28058
- }
28059
- /** Give the claim up. Scoped to this holder, so a taken-over claim is left be. */
28060
- release(pid) {
28061
- this.releaseStmt.run({ pid });
28062
- }
28063
- /** Who holds the claim, if anyone. Read-only, for the same reason as above. */
28064
- lease() {
28065
- return getRow(this.leaseStmt);
28066
- }
28067
- };
28068
-
28069
28675
  // ../../packages/persistence/src/repositories/inspection-definitions.ts
28070
28676
  var SqliteInspectionDefinitionsRepository = class {
28071
28677
  constructor(db) {
@@ -28475,7 +29081,10 @@ function managedContextOf(managed) {
28475
29081
  present: true,
28476
29082
  ...managed.organization === void 0 ? {} : { organization: managed.organization },
28477
29083
  lockedFields: managed.lockedFields,
28478
- ...managed.unknownLockedFields === void 0 ? {} : { unknownLockedFields: managed.unknownLockedFields }
29084
+ // The COUNT crosses to the dashboard, not the names — see ManagedContext.
29085
+ // The names remain on `managed` for any caller that wants to list them.
29086
+ ...managed.unknownLockedFields === void 0 ? {} : { unknownLockedCount: managed.unknownLockedFields.length },
29087
+ ...managed.unknownValueFields === void 0 ? {} : { unknownValueCount: managed.unknownValueFields.length }
28479
29088
  };
28480
29089
  }
28481
29090
  function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
@@ -28498,6 +29107,7 @@ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ n
28498
29107
  if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28499
29108
  if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28500
29109
  if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
29110
+ if (values.bodyRetention !== void 0) merged.bodyRetention = values.bodyRetention;
28501
29111
  if (values.vaultConsent !== void 0) {
28502
29112
  merged.vaultConsent = values.vaultConsent ? (
28503
29113
  // Keep an existing valid grant so its acknowledgedAt survives; mint one
@@ -28550,6 +29160,9 @@ function lockableKeysTouched(current, applied) {
28550
29160
  if (changed("vaultInlineReveal")) keys.push("vaultInlineReveal");
28551
29161
  if (changed("dataSharesInPlace")) keys.push("dataSharesInPlace");
28552
29162
  if (changed("redactFallback")) keys.push("redactFallback");
29163
+ const appliedRetention = applied.bodyRetention;
29164
+ const retentionChanged = appliedRetention !== void 0 && (appliedRetention.enabled !== current.bodyRetention.enabled || appliedRetention.retainDays !== current.bodyRetention.retainDays);
29165
+ if (retentionChanged) keys.push("bodyRetention");
28553
29166
  if ("vaultConsent" in applied && isVaultConsentValid(applied.vaultConsent) !== isVaultConsentValid(current.vaultConsent)) {
28554
29167
  keys.push("vaultConsent");
28555
29168
  }
@@ -28570,6 +29183,7 @@ function pinnedKeys(managed) {
28570
29183
  if (values.modelJudgeConsent !== void 0) keys.push("modelJudgeConsent");
28571
29184
  if (values.dataSharesInPlace !== void 0) keys.push("dataSharesInPlace");
28572
29185
  if (values.redactFallback !== void 0) keys.push("redactFallback");
29186
+ if (values.bodyRetention !== void 0) keys.push("bodyRetention");
28573
29187
  return keys;
28574
29188
  }
28575
29189
  function withoutManagedKeys(applied, managed, pinned, touched) {
@@ -28587,6 +29201,7 @@ function withoutManagedKeys(applied, managed, pinned, touched) {
28587
29201
  if (strip("modelJudgeConsent")) delete out.modelJudgeConsent;
28588
29202
  if (strip("dataSharesInPlace")) delete out.dataSharesInPlace;
28589
29203
  if (strip("redactFallback")) delete out.redactFallback;
29204
+ if (strip("bodyRetention")) delete out.bodyRetention;
28590
29205
  return out;
28591
29206
  }
28592
29207
  function applyOnboarding(answers2, base = defaultDataDir(), managedOverride) {
@@ -31095,7 +31710,7 @@ var SqliteSecurityRepository = class {
31095
31710
  ELSE 0
31096
31711
  END) AS open_at_rest
31097
31712
  FROM inspection_findings f
31098
- JOIN audit_events e ON e.id = f.audit_event_id
31713
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31099
31714
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31100
31715
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
31101
31716
  ON latest.finding_key = f.finding_key
@@ -31321,7 +31936,7 @@ var SqliteSecurityRepository = class {
31321
31936
  this.db.prepare(
31322
31937
  `SELECT e.repo AS repo, count(*) AS c
31323
31938
  FROM inspection_findings f
31324
- JOIN audit_events e ON e.id = f.audit_event_id
31939
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31325
31940
  WHERE e.started_at >= :from AND e.started_at < :to
31326
31941
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
31327
31942
  AND e.repo IS NOT NULL
@@ -31445,7 +32060,7 @@ var SqliteSecurityRepository = class {
31445
32060
  d.severity AS severity,
31446
32061
  COUNT(*) AS count
31447
32062
  FROM inspection_findings f
31448
- JOIN audit_events e ON e.id = f.audit_event_id
32063
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31449
32064
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31450
32065
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
31451
32066
  ON latest.finding_key = f.finding_key
@@ -31480,7 +32095,7 @@ var SqliteSecurityRepository = class {
31480
32095
  `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
31481
32096
  d.rule_id AS rule_id, d.category AS category
31482
32097
  FROM inspection_findings f
31483
- JOIN audit_events e ON e.id = f.audit_event_id
32098
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31484
32099
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31485
32100
  WHERE e.started_at >= :from AND e.started_at < :to
31486
32101
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
@@ -32321,6 +32936,7 @@ function openWithPragmas(file2) {
32321
32936
  db.exec("PRAGMA journal_mode = WAL");
32322
32937
  db.exec("PRAGMA busy_timeout = 2000");
32323
32938
  db.exec("PRAGMA foreign_keys = ON");
32939
+ registerSqlFunctions(db);
32324
32940
  } catch (err) {
32325
32941
  closeQuietly(db);
32326
32942
  throw err;
@@ -32350,7 +32966,7 @@ function backupLegacyStore(db, file2) {
32350
32966
  discardStore(file2, backup);
32351
32967
  return backup;
32352
32968
  }
32353
- function openAndInitialize(file2, base) {
32969
+ function openAndInitialize(file2, base, skipTags) {
32354
32970
  let db = openWithPragmas(file2);
32355
32971
  try {
32356
32972
  if (isForeignSqliteLineage(db)) {
@@ -32360,7 +32976,7 @@ function openAndInitialize(file2, base) {
32360
32976
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
32361
32977
  );
32362
32978
  }
32363
- applyMigrations(db, file2);
32979
+ applyMigrations(db, file2, { skipTags });
32364
32980
  tightenPerms(file2);
32365
32981
  const policies = new SqlitePoliciesRepository(db);
32366
32982
  const installedPacks = new SqliteInstalledPacksRepository(db, base);
@@ -32375,6 +32991,7 @@ function openAndInitialize(file2, base) {
32375
32991
  exceptions: new SqliteExceptionsRepository(db),
32376
32992
  resolutions: new SqliteResolutionsRepository(db),
32377
32993
  ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
32994
+ bodyRetention: new SqliteBodyRetentionRepository(db),
32378
32995
  security: new SqliteSecurityRepository(db),
32379
32996
  detections: new SqliteDetectionsRepository(db),
32380
32997
  shares: new SqliteSharesRepository(db),
@@ -32397,7 +33014,8 @@ function openAndInitialize(file2, base) {
32397
33014
  throw err;
32398
33015
  }
32399
33016
  }
32400
- function openLocalDatabase(dir) {
33017
+ var DEFERRED_TAGS = new Set(DEFERRED_MIGRATION_TAGS);
33018
+ function openLocalDatabase(dir, options = {}) {
32401
33019
  ensureDataDirSync(dir);
32402
33020
  const file2 = join7(dir, DB_FILENAME);
32403
33021
  reapStalePartials(file2);
@@ -32409,6 +33027,7 @@ function openLocalDatabase(dir) {
32409
33027
  installedPacks,
32410
33028
  scanLedger,
32411
33029
  historySync,
33030
+ bodyRetention,
32412
33031
  secretVault,
32413
33032
  exceptions,
32414
33033
  resolutions,
@@ -32432,7 +33051,8 @@ function openLocalDatabase(dir) {
32432
33051
  // `dir` is always `<base>/data` — every caller resolves it through
32433
33052
  // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
32434
33053
  // settings/ and data/, and the pack-policy floor needs both halves.
32435
- dirname2(dir)
33054
+ dirname2(dir),
33055
+ options.applyDeferredMigrations === true ? void 0 : DEFERRED_TAGS
32436
33056
  );
32437
33057
  function captureRowId(event) {
32438
33058
  return captureId(
@@ -32625,6 +33245,7 @@ function openLocalDatabase(dir) {
32625
33245
  installedPacks,
32626
33246
  scanLedger,
32627
33247
  historySync,
33248
+ bodyRetention,
32628
33249
  secretVault,
32629
33250
  exceptions,
32630
33251
  resolutions,
@@ -32665,6 +33286,7 @@ function openLocalDatabase(dir) {
32665
33286
 
32666
33287
  // ../../packages/persistence/src/egress-wire.ts
32667
33288
  import { createHash as createHash3 } from "crypto";
33289
+ var SLASH = "/".charCodeAt(0);
32668
33290
 
32669
33291
  // ../../packages/persistence/src/finding-key.ts
32670
33292
  import { createHash as createHash4 } from "crypto";
@@ -32675,18 +33297,26 @@ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
32675
33297
  import { join as join8 } from "path";
32676
33298
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
32677
33299
 
33300
+ // ../../packages/persistence/src/forward-health.ts
33301
+ import { readFileSync as readFileSync7 } from "fs";
33302
+ import { join as join9 } from "path";
33303
+
32678
33304
  // ../../packages/persistence/src/history-backfill.ts
32679
33305
  import { existsSync as existsSync4 } from "fs";
32680
- import { join as join9 } from "path";
33306
+ import { join as join10 } from "path";
32681
33307
 
32682
33308
  // ../../packages/persistence/src/history-preview.ts
32683
33309
  import { existsSync as existsSync5 } from "fs";
32684
- import { join as join10 } from "path";
33310
+ import { join as join11 } from "path";
32685
33311
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
32686
33312
 
33313
+ // ../../packages/persistence/src/history-sync-state.ts
33314
+ import { readFileSync as readFileSync8 } from "fs";
33315
+ import { join as join12 } from "path";
33316
+
32687
33317
  // ../../packages/persistence/src/store-symlinks.ts
32688
33318
  import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32689
- import { dirname as dirname3, join as join11, resolve } from "path";
33319
+ import { dirname as dirname3, join as join13, resolve } from "path";
32690
33320
 
32691
33321
  // ../../packages/persistence/src/vault/crypto.ts
32692
33322
  import {
@@ -32700,19 +33330,19 @@ import {
32700
33330
  // ../../packages/persistence/src/vault/key-provider.ts
32701
33331
  import { execFileSync } from "child_process";
32702
33332
  import { randomBytes as randomBytes2 } from "crypto";
32703
- import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32704
- import { join as join12 } from "path";
33333
+ import { chmodSync as chmodSync3, readFileSync as readFileSync9, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
33334
+ import { join as join14 } from "path";
32705
33335
 
32706
33336
  // ../../packages/persistence/src/vault/vault.ts
32707
33337
  import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
32708
33338
 
32709
33339
  // ../../packages/persistence/src/warn-era-cap.ts
32710
33340
  import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
32711
- import { join as join13 } from "path";
33341
+ import { join as join15 } from "path";
32712
33342
  var MARKER = "warn-era-capped";
32713
33343
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32714
33344
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
32715
- const marker = join13(dataDir2, MARKER);
33345
+ const marker = join15(dataDir2, MARKER);
32716
33346
  if (existsSync7(marker)) return { capped: 0, skipped: "already-run" };
32717
33347
  const capped = db.policies.capCategoryActions();
32718
33348
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
@@ -32722,7 +33352,7 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32722
33352
 
32723
33353
  // ../../packages/plugin-sdk/src/config.ts
32724
33354
  import { existsSync as existsSync8 } from "fs";
32725
- import { join as join14 } from "path";
33355
+ import { join as join16 } from "path";
32726
33356
 
32727
33357
  // ../../packages/plugin-sdk/src/provider-env.ts
32728
33358
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -32776,7 +33406,7 @@ function resolveProvider() {
32776
33406
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
32777
33407
  try {
32778
33408
  ensureLayoutDirSync(base);
32779
- const settingsFile = join14(settingsDir(base), "settings.json");
33409
+ const settingsFile = join16(settingsDir(base), "settings.json");
32780
33410
  if (existsSync8(settingsFile)) tightenFile(settingsFile);
32781
33411
  } catch {
32782
33412
  }
@@ -32800,9 +33430,9 @@ function resolveProviderSafe(resolveProviderFn) {
32800
33430
  }
32801
33431
 
32802
33432
  // ../../packages/plugin-sdk/src/config-inventory.ts
32803
- import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
33433
+ import { readdirSync as readdirSync2, readFileSync as readFileSync11, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32804
33434
  import { homedir as homedir2 } from "os";
32805
- import { basename as basename3, join as join16 } from "path";
33435
+ import { basename as basename3, join as join18 } from "path";
32806
33436
 
32807
33437
  // ../../packages/detections/src/egress/registry.ts
32808
33438
  var EXTRACTOR_VERSION = "1";
@@ -33560,8 +34190,8 @@ var CPU_CORROBORATION_SHARE = 0.2;
33560
34190
  var CORROBORATION_FLOOR_MS = BUDGET_MS * CPU_CORROBORATION_SHARE;
33561
34191
 
33562
34192
  // ../../packages/plugin-sdk/src/repo.ts
33563
- import { existsSync as existsSync9, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
33564
- import { basename as basename2, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
34193
+ import { existsSync as existsSync9, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
34194
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join17, sep as sep2 } from "path";
33565
34195
 
33566
34196
  // ../../packages/plugin-sdk/src/events.ts
33567
34197
  import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
@@ -33572,8 +34202,8 @@ import { fileURLToPath } from "url";
33572
34202
  import { Worker } from "worker_threads";
33573
34203
 
33574
34204
  // ../../packages/plugin-sdk/src/host-floor.ts
33575
- import { readFileSync as readFileSync11 } from "fs";
33576
- import { join as join18 } from "path";
34205
+ import { readFileSync as readFileSync13 } from "fs";
34206
+ import { join as join20 } from "path";
33577
34207
 
33578
34208
  // ../../packages/plugin-sdk/src/model-governance.ts
33579
34209
  import {
@@ -33581,11 +34211,11 @@ import {
33581
34211
  fstatSync,
33582
34212
  mkdirSync as mkdirSync2,
33583
34213
  openSync as openSync2,
33584
- readFileSync as readFileSync10,
34214
+ readFileSync as readFileSync12,
33585
34215
  readSync,
33586
34216
  writeFileSync as writeFileSync5
33587
34217
  } from "fs";
33588
- import { join as join17 } from "path";
34218
+ import { join as join19 } from "path";
33589
34219
  var TAIL_BYTES = 256 * 1024;
33590
34220
 
33591
34221
  // ../../packages/plugin-sdk/src/host-floor.ts
@@ -33608,15 +34238,15 @@ var HOST_FLOORS = {
33608
34238
 
33609
34239
  // ../../packages/plugin-sdk/src/ignore-layers.ts
33610
34240
  var import_ignore = __toESM(require_ignore(), 1);
33611
- import { readFileSync as readFileSync12 } from "fs";
33612
- import { join as join19 } from "path";
34241
+ import { readFileSync as readFileSync14 } from "fs";
34242
+ import { join as join21 } from "path";
33613
34243
 
33614
34244
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
33615
34245
  import { arch, hostname as hostname4, platform, release as release2 } from "os";
33616
34246
 
33617
34247
  // ../../packages/plugin-sdk/src/nudge.ts
33618
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
33619
- import { join as join20 } from "path";
34248
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync15, writeFileSync as writeFileSync6 } from "fs";
34249
+ import { join as join22 } from "path";
33620
34250
 
33621
34251
  // ../../packages/plugin-sdk/src/paths.ts
33622
34252
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
@@ -33634,7 +34264,7 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
33634
34264
 
33635
34265
  // ../../packages/plugin-sdk/src/project-files.ts
33636
34266
  import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
33637
- import { basename as basename5, join as join21 } from "path";
34267
+ import { basename as basename5, join as join23 } from "path";
33638
34268
 
33639
34269
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
33640
34270
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -33670,7 +34300,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
33670
34300
 
33671
34301
  // ../../packages/plugin-sdk/src/throttle.ts
33672
34302
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
33673
- import { join as join22 } from "path";
34303
+ import { join as join24 } from "path";
33674
34304
 
33675
34305
  // ../../packages/setup-wizard/src/onboard-posture.ts
33676
34306
  function parsePosture(json2) {
@@ -33693,7 +34323,7 @@ function parsePosture(json2) {
33693
34323
 
33694
34324
  // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
33695
34325
  import { writeFileSync as writeFileSync8 } from "fs";
33696
- import { join as join23 } from "path";
34326
+ import { join as join25 } from "path";
33697
34327
 
33698
34328
  // ../../packages/setup-wizard/src/triage/merge.ts
33699
34329
  var RANK = Object.fromEntries(
@@ -33701,9 +34331,9 @@ var RANK = Object.fromEntries(
33701
34331
  );
33702
34332
 
33703
34333
  // ../../packages/setup-wizard/src/triage/plan-file.ts
33704
- import { mkdtempSync, readFileSync as readFileSync14, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
34334
+ import { mkdtempSync, readFileSync as readFileSync16, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
33705
34335
  import { tmpdir } from "os";
33706
- import { basename as basename6, dirname as dirname6, join as join24 } from "path";
34336
+ import { basename as basename6, dirname as dirname6, join as join26 } from "path";
33707
34337
  var SuppressionEntrySchema = external_exports.object({
33708
34338
  ruleId: external_exports.string(),
33709
34339
  category: DetectionCategory,