@akasecurity/ai-tc-claude-code 0.9.10 → 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
@@ -492,8 +492,8 @@ var require_ignore = __commonJS({
492
492
  });
493
493
 
494
494
  // ../../packages/plugin-sdk/src/config.ts
495
- import { existsSync as existsSync7 } from "fs";
496
- import { join as join13 } from "path";
495
+ import { existsSync as existsSync8 } from "fs";
496
+ import { join as join16 } from "path";
497
497
 
498
498
  // ../../packages/persistence/src/attached-derived.ts
499
499
  import { rmSync } from "fs";
@@ -504,6 +504,14 @@ var POLICY_CACHE_FILENAME = "policy-cache.json";
504
504
  import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync, rmSync as rmSync3, statSync } from "fs";
505
505
  import { join as join2 } from "path";
506
506
 
507
+ // ../../packages/schema/src/drizzle/deferred-migrations.ts
508
+ var DEFERRED_MIGRATION_TAGS = [
509
+ "0031_audit_capture_by_time_index",
510
+ "0032_audit_capture_by_id_index",
511
+ "0033_audit_capture_location_index",
512
+ "0034_findings_read_indexes"
513
+ ];
514
+
507
515
  // ../../packages/schema/src/drizzle/sqlite-ddl.ts
508
516
  var SQLITE_MIGRATIONS = [
509
517
  {
@@ -621,6 +629,30 @@ var SQLITE_MIGRATIONS = [
621
629
  {
622
630
  tag: "0028_activity_session_probe_indexes",
623
631
  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"
632
+ },
633
+ {
634
+ tag: "0029_audit_capture_rollup_index",
635
+ 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');"
636
+ },
637
+ {
638
+ tag: "0030_audit_content_expiry",
639
+ 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;"
640
+ },
641
+ {
642
+ tag: "0031_audit_capture_by_time_index",
643
+ 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');"
644
+ },
645
+ {
646
+ tag: "0032_audit_capture_by_id_index",
647
+ 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');"
648
+ },
649
+ {
650
+ tag: "0033_audit_capture_location_index",
651
+ 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');"
652
+ },
653
+ {
654
+ tag: "0034_findings_read_indexes",
655
+ 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`);"
624
656
  }
625
657
  ];
626
658
 
@@ -20668,6 +20700,15 @@ var FindingCategory = external_exports.enum([
20668
20700
  ]).meta({ id: "FindingCategory" });
20669
20701
  var FindingOrigin = external_exports.enum(["in-flight", "at-rest"]).meta({ id: "FindingOrigin" });
20670
20702
  var FindingStatus = external_exports.enum(["open", "handled", "resolved", "dismissed"]).meta({ id: "FindingStatus" });
20703
+ var SyncFailureReason = external_exports.enum(["deployment_refused", "payload_invalid", "detached_undelivered"]).meta({ id: "SyncFailureReason" });
20704
+ var FindingDeliveryState = external_exports.enum(["sent", "queued", "not_sent", "never_offered", "local_scan"]).meta({ id: "FindingDeliveryState" });
20705
+ var FindingDelivery = external_exports.object({
20706
+ state: FindingDeliveryState,
20707
+ // The delivery time for `sent`; the failure time for `not_sent` when recorded.
20708
+ at: external_exports.iso.datetime().optional(),
20709
+ // Only on `not_sent`, and only when a known reason was recorded.
20710
+ reason: SyncFailureReason.optional()
20711
+ }).meta({ id: "FindingDelivery" });
20671
20712
  var ResolutionMethod = external_exports.enum([
20672
20713
  "enforced-in-flight",
20673
20714
  "fixed-at-source",
@@ -20724,7 +20765,10 @@ var FindingInstance = external_exports.object({
20724
20765
  // The session that event belongs to, when it has one — the seam a
20725
20766
  // per-instance "view session" link needs. Absent for events captured
20726
20767
  // outside a session.
20727
- sessionId: external_exports.string().optional()
20768
+ sessionId: external_exports.string().optional(),
20769
+ // The delivery state of the event above (see FindingDelivery). Optional so
20770
+ // readers that do not project it stay valid.
20771
+ delivery: FindingDelivery.optional()
20728
20772
  }).meta({ id: "FindingInstance" });
20729
20773
  var FindingGroup = external_exports.object({
20730
20774
  id: external_exports.string(),
@@ -20741,13 +20785,11 @@ var FindingGroup = external_exports.object({
20741
20785
  latestDetectedAt: external_exports.iso.datetime(),
20742
20786
  instances: external_exports.array(FindingInstance),
20743
20787
  // Derived from instances' statuses with open-dominates precedence (see
20744
- // buildFindingGroups). Undefined only when no instance carries a status.
20788
+ // foldGroupStatus). Undefined only when no instance carries a status.
20745
20789
  status: FindingStatus.optional(),
20746
- // The distinct people across the WHOLE group, not just the `instances`
20747
- // preview — from the store's whole-group aggregate when it supplies one,
20748
- // else folded from the rows (see buildFindingGroups). Undefined when no
20749
- // instance carries a user, or when the store supplied whole-group folds
20750
- // without one.
20790
+ // The distinct people across the WHOLE group, not just the instances
20791
+ // carried here. Undefined when no instance carries a user, or when the
20792
+ // store supplied whole-group folds without one.
20751
20793
  users: external_exports.array(FindingUser).optional()
20752
20794
  }).meta({ id: "FindingGroup" });
20753
20795
  var FindingStats = external_exports.object({
@@ -20776,21 +20818,34 @@ var FindingFacets = external_exports.object({
20776
20818
  // counted under no value.
20777
20819
  status: external_exports.array(FindingFacetItem),
20778
20820
  // Host tool (attributes.tool_name). Present only on the instance-level
20779
- // reads, which can filter by it; the grouped read omits the dimension
20821
+ // reads, which can filter by it; the type-level read omits the dimension
20780
20822
  // because a group spans tools.
20781
- tool: external_exports.array(FindingFacetItem).optional()
20823
+ tool: external_exports.array(FindingFacetItem).optional(),
20824
+ // Delivery states (FindingDeliveryState). Present only on the
20825
+ // instance-level reads, like `tool`.
20826
+ deployment: external_exports.array(FindingFacetItem).optional()
20782
20827
  }).meta({ id: "FindingFacets" });
20783
- var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
20784
- var ListGroupedFindingsQuery = external_exports.object({
20828
+ var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
20829
+ id: "FindingTypeSummary"
20830
+ });
20831
+ var DEFAULT_FINDING_TYPES_LIMIT = 50;
20832
+ var MAX_FINDING_TYPES_LIMIT = 100;
20833
+ var ListFindingTypesQuery = external_exports.object({
20785
20834
  // NOTE: severity filters by Severity (critical/high/medium/low), not by
20786
- // FindingAction.
20835
+ // FindingAction. It narrows TYPES: a type's severity is the one its newest
20836
+ // firing version carries, and this list pages types.
20837
+ //
20838
+ // That is NOT a claim the findings of a type share it. A rule can hold several
20839
+ // definition versions at different severities, so a type kept by this filter
20840
+ // can hold findings that individually do not match — see totals.findings on
20841
+ // ListFindingTypesResponse, which counts them all.
20787
20842
  severity: external_exports.array(Severity).optional(),
20788
20843
  subtype: external_exports.array(external_exports.string()).optional(),
20789
20844
  provider: external_exports.array(FindingProvider).optional(),
20790
20845
  action: external_exports.array(FindingAction).optional(),
20791
- // Matches a group's DERIVED status (see FindingGroup.status), not its
20792
- // individual instances' — so a filtered group's Status column always reads
20793
- // one of the requested values.
20846
+ // Matches a type's DERIVED status (see FindingGroup.status), not its
20847
+ // individual findings' — so a filtered row's status always reads one of the
20848
+ // requested values.
20794
20849
  status: external_exports.array(FindingStatus).optional(),
20795
20850
  q: external_exports.string().optional(),
20796
20851
  // Scope to findings whose event carries this session id (the Activity page's
@@ -20800,23 +20855,37 @@ var ListGroupedFindingsQuery = external_exports.object({
20800
20855
  // from a time-scoped page (Activity's range) can carry that scope. Absent
20801
20856
  // means all time — this list has no default window.
20802
20857
  from: external_exports.iso.datetime().optional(),
20803
- // A group or instance id that must appear in the page even when the cursor
20804
- // has already advanced past its sort position. This is what keeps the
20805
- // Findings page's one-shot ?finding= deep link resolving once the list
20806
- // paginates: the target group is appended out of sort order rather than
20807
- // scanning forward for it. Never affects totals, facets or the cursor.
20858
+ // A RULE id that must appear in the page even when the cursor has already
20859
+ // advanced past its sort position. This is what keeps the selected type
20860
+ // visible in the list once it paginates: the target is appended out of sort
20861
+ // order rather than scanned forward for. Never affects totals, facets or the
20862
+ // cursor. Unlike the grouped read this replaces, it names a rule only — an
20863
+ // instance id is resolved by `findingInstance`, which is a primary-key seek
20864
+ // and so is not bounded by what any page happens to hold.
20808
20865
  includeId: external_exports.string().optional(),
20809
- groupBy: external_exports.literal("type").optional(),
20810
- limit: external_exports.coerce.number().int().min(1).max(100).optional(),
20866
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_TYPES_LIMIT).optional(),
20811
20867
  cursor: external_exports.string().optional()
20812
20868
  });
20813
- var ListGroupedFindingsResponse = external_exports.object({
20869
+ var ListFindingTypesResponse = external_exports.object({
20814
20870
  totals: external_exports.object({
20871
+ // Findings belonging to the matching TYPES — not findings that each match
20872
+ // the filters. The filters here select types, so a type that survives
20873
+ // contributes its whole instanceCount.
20874
+ //
20875
+ // `status` is the one exception, narrowed per finding via
20876
+ // countInstancesByStatus. `severity`, `provider` and `action` are not, so
20877
+ // this can exceed what the instance read reports for the same filters: a
20878
+ // rule whose severity moved between versions is kept on its newest and
20879
+ // still counts its older findings. Narrowing the other three needs
20880
+ // per-dimension counts the aggregate does not carry today.
20815
20881
  findings: external_exports.number().int().nonnegative(),
20816
- groups: external_exports.number().int().nonnegative()
20882
+ // Counts TYPES, which is the unit this read pages. The instance read's
20883
+ // own totals count findings; the two deliberately answer different
20884
+ // questions and are never summed.
20885
+ types: external_exports.number().int().nonnegative()
20817
20886
  }),
20818
20887
  facets: FindingFacets,
20819
- items: external_exports.array(FindingGroup),
20888
+ items: external_exports.array(FindingTypeSummary),
20820
20889
  nextCursor: external_exports.string().nullable(),
20821
20890
  // Present only on session-scoped queries (`sessionId` set): per ruleId, how
20822
20891
  // many times that rule fired in the session's persisted transcript. Findings
@@ -20824,7 +20893,7 @@ var ListGroupedFindingsResponse = external_exports.object({
20824
20893
  // every firing, so the two numbers legitimately differ — this map lets a
20825
20894
  // session-scoped view show both.
20826
20895
  sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
20827
- }).meta({ id: "ListGroupedFindingsResponse" });
20896
+ }).meta({ id: "ListFindingTypesResponse" });
20828
20897
  var ApplyFindingActionRequest = external_exports.object({
20829
20898
  // 'quarantined' is system-assigned (see FindingAction) — clients may not set
20830
20899
  // it, so it is excluded from the request contract. The mapping helper
@@ -20854,16 +20923,19 @@ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
20854
20923
  var MAX_FLAT_FINDINGS_LIMIT = 200;
20855
20924
  var ListFindingInstancesQuery = external_exports.object({
20856
20925
  severity: external_exports.array(Severity).optional(),
20857
- // Rule ids, the same vocabulary the grouped list's `subtype` carries.
20926
+ // Rule ids, the same vocabulary the types list's `subtype` carries. Pinning
20927
+ // ONE of them is how the master/detail view scopes its right-hand panel.
20858
20928
  subtype: external_exports.array(external_exports.string()).optional(),
20859
20929
  provider: external_exports.array(FindingProvider).optional(),
20860
20930
  action: external_exports.array(FindingAction).optional(),
20861
20931
  // Matches each instance's OWN derived status (deriveFindingStatus), unlike
20862
- // the grouped query's group-level fold.
20932
+ // the types query's type-level fold.
20863
20933
  status: external_exports.array(FindingStatus).optional(),
20864
20934
  // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
20865
20935
  // where the free-text `q` can only match the rendered "via Bash" label.
20866
20936
  tool: external_exports.array(external_exports.string()).optional(),
20937
+ // The delivery state of each finding's event (see FindingDelivery).
20938
+ deployment: external_exports.array(FindingDeliveryState).optional(),
20867
20939
  // Exact repository / file-path matches, for the drill-down out of the
20868
20940
  // locations view. A row whose event carries no repo/file matches neither.
20869
20941
  repo: external_exports.string().optional(),
@@ -20876,37 +20948,51 @@ var ListFindingInstancesQuery = external_exports.object({
20876
20948
  });
20877
20949
  var ListFindingInstancesResponse = external_exports.object({
20878
20950
  // Instances matching the filters across the whole scope, not just this
20879
- // page — cursor-independent, like the grouped list's totals.
20951
+ // page — cursor-independent, like the types list's totals.
20880
20952
  totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
20881
- // Counts in INSTANCES here, where the grouped response counts groups. Each
20953
+ // Counts in INSTANCES here, where the types response counts types. Each
20882
20954
  // dimension still excludes its own filter.
20883
20955
  facets: FindingFacets,
20884
20956
  items: external_exports.array(FindingInstanceDetail),
20885
20957
  nextCursor: external_exports.string().nullable()
20886
20958
  }).meta({ id: "ListFindingInstancesResponse" });
20887
- var FindingLocationFile = external_exports.object({
20888
- // Empty when the instances carried no file path (a prompt or a tool call
20889
- // with no file attribution).
20890
- file: external_exports.string(),
20891
- instanceCount: external_exports.number().int().nonnegative(),
20892
- maxSeverity: Severity,
20893
- latestDetectedAt: external_exports.iso.datetime(),
20894
- // Folded from the instances' derived statuses with the same
20895
- // open-dominates precedence a group uses.
20896
- status: FindingStatus.optional(),
20897
- // Distinct rules seen at this location, capped — the row shows them as
20898
- // chips, and the count is what conveys scale.
20899
- ruleIds: external_exports.array(external_exports.string())
20900
- }).meta({ id: "FindingLocationFile" });
20901
- var FindingLocationRepo = external_exports.object({
20959
+ var ListFindingInstancesPage = external_exports.object({
20960
+ items: external_exports.array(FindingInstanceDetail),
20961
+ nextCursor: external_exports.string().nullable()
20962
+ }).meta({ id: "ListFindingInstancesPage" });
20963
+ var FindingLocationSummary = external_exports.object({
20964
+ // Opaque, stable, minted from the pair by encodeLocationId. It exists
20965
+ // because a location's identity is two values and a URL param carries one:
20966
+ // `?loc=` names a location the way `?rule=` names a type. Only ever compared
20967
+ // for EQUALITY — the page's selection check, this read's `includeId`, the
20968
+ // client's page dedupe — never decoded, and never a sort key.
20969
+ id: external_exports.string(),
20902
20970
  /** Empty when the instances carried no repo attribute. */
20903
20971
  repo: external_exports.string(),
20972
+ // Empty when the instances carried no file path (a prompt, or a tool call
20973
+ // with no file attribution). Both halves empty is a real location — usually
20974
+ // the largest one in a store — and is selectable like any other.
20975
+ file: external_exports.string(),
20904
20976
  instanceCount: external_exports.number().int().nonnegative(),
20977
+ // The WORST severity present, not the first row's. It is this list's primary
20978
+ // sort key, so it is also what explains why a row is where it is, and it is
20979
+ // how a reader decides what to open without opening everything.
20905
20980
  maxSeverity: Severity,
20906
20981
  latestDetectedAt: external_exports.iso.datetime(),
20982
+ // Folded from the instances' derived statuses with the same open-dominates
20983
+ // precedence a group uses, so it answers "is anything left to do here" and
20984
+ // not much more: a location holding 1 open among 40 resolved reads like one
20985
+ // holding 40 open. That loss is accepted — the panel beside this list
20986
+ // carries each finding's own status, and instanceCount sits next to the
20987
+ // badge.
20907
20988
  status: FindingStatus.optional(),
20908
- files: external_exports.array(FindingLocationFile)
20909
- }).meta({ id: "FindingLocationRepo" });
20989
+ // Every distinct rule seen at this location, UNCAPPED — so the length is a
20990
+ // tally rather than a sample and a row can say how many there are. Bounded
20991
+ // by the ruleset, not by the store. The view bounds what it DISPLAYS.
20992
+ ruleIds: external_exports.array(external_exports.string())
20993
+ }).meta({ id: "FindingLocationSummary" });
20994
+ var DEFAULT_FINDING_LOCATIONS_LIMIT = 50;
20995
+ var MAX_FINDING_LOCATIONS_LIMIT = 100;
20910
20996
  var ListFindingLocationsQuery = external_exports.object({
20911
20997
  severity: external_exports.array(Severity).optional(),
20912
20998
  subtype: external_exports.array(external_exports.string()).optional(),
@@ -20916,21 +21002,47 @@ var ListFindingLocationsQuery = external_exports.object({
20916
21002
  // instances that match, and folds its status from those.
20917
21003
  status: external_exports.array(FindingStatus).optional(),
20918
21004
  tool: external_exports.array(external_exports.string()).optional(),
21005
+ // The delivery state of each finding's event (see FindingDelivery).
21006
+ deployment: external_exports.array(FindingDeliveryState).optional(),
20919
21007
  q: external_exports.string().optional(),
20920
21008
  sessionId: external_exports.string().optional(),
20921
21009
  from: external_exports.iso.datetime().optional(),
20922
- limit: external_exports.coerce.number().int().min(1).max(500).optional()
21010
+ // A LOCATION id (see FindingLocationSummary.id) that must appear in the page
21011
+ // even when the cursor has already advanced past its sort position — the
21012
+ // counterpart of ListFindingTypesQuery.includeId, and needed far more often
21013
+ // here. Selecting a row pushes the URL, which re-renders the server and resets
21014
+ // the client's page cache to page 0; with distinct (repo, file) pairs running
21015
+ // into the thousands, a selection sitting off page 0 is the ordinary case
21016
+ // rather than a deep-link corner. Never affects totals, facets or the cursor.
21017
+ includeId: external_exports.string().optional(),
21018
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_LOCATIONS_LIMIT).optional(),
21019
+ cursor: external_exports.string().optional()
20923
21020
  });
20924
21021
  var ListFindingLocationsResponse = external_exports.object({
20925
21022
  totals: external_exports.object({
21023
+ // Findings matching the filters across the whole scope. Unlike the types
21024
+ // read's same-named field this needs no caveat: the filters here narrow
21025
+ // per finding, so this is the sum of every row's instanceCount.
20926
21026
  findings: external_exports.number().int().nonnegative(),
20927
- repos: external_exports.number().int().nonnegative(),
20928
- files: external_exports.number().int().nonnegative()
21027
+ // Counts LOCATIONS, the unit this read pages — the number the paginator
21028
+ // states. The facets beside it count FINDINGS (see below); a surface
21029
+ // showing both says which is which.
21030
+ locations: external_exports.number().int().nonnegative()
20929
21031
  }),
20930
- /** Sorted by max severity, then most recent. */
20931
- items: external_exports.array(FindingLocationRepo),
20932
- /** Whether `limit` truncated the repo list. */
20933
- hasMore: external_exports.boolean()
21032
+ // Counts in FINDINGS, where the types response counts types, each dimension
21033
+ // still excluding its own filter. Deliberately not locations: counting those
21034
+ // needs a set of location keys per dimension per value — memory tracking the
21035
+ // store times the vocabulary, in a read whose scan promises flat memory —
21036
+ // and the cheap per-location version is not an approximation but WRONG. A
21037
+ // location holding {claudecode, block} and {codex, warn} would survive
21038
+ // provider=claudecode AND action=warn, under which no single finding
21039
+ // matches, so the facet would contradict the instanceCount this whole view
21040
+ // rests on. Findings also keep the toolbar in the same unit as the page
21041
+ // tally and the panel it sits above.
21042
+ facets: FindingFacets,
21043
+ /** Sorted by max severity, then most recent, then (repo, file). */
21044
+ items: external_exports.array(FindingLocationSummary),
21045
+ nextCursor: external_exports.string().nullable()
20934
21046
  }).meta({ id: "ListFindingLocationsResponse" });
20935
21047
 
20936
21048
  // ../../packages/schema/src/zod/meta.ts
@@ -21094,6 +21206,10 @@ var CaptureAttributes = external_exports.object({
21094
21206
  // to 'allow' — the enforcement audit trail's link back to the grant that
21095
21207
  // authorized the bypass.
21096
21208
  exception_ids: external_exports.array(external_exports.guid()).optional(),
21209
+ // The persisted spellings of EventMetadata's messageId/conversationId — the
21210
+ // join back to the `llm_call` leaf for the same assistant turn.
21211
+ message_id: external_exports.string().optional(),
21212
+ conversation_id: external_exports.string().optional(),
21097
21213
  // Whole milliseconds this capture's inspection blocked its caller — the
21098
21214
  // plugin's own added latency (see EventMetadata.inspectionMs, whose value
21099
21215
  // this is). Promoted to the `inspection_ms` generated column so the facet is
@@ -21102,7 +21218,19 @@ var CaptureAttributes = external_exports.object({
21102
21218
  // inline json_extract and is not itself an optimization.
21103
21219
  // ABSENT on replayed captures (backfill / worktree scan) and on rows written
21104
21220
  // before the measurement shipped — never present as a placeholder 0.
21105
- inspection_ms: external_exports.number().int().nonnegative().optional()
21221
+ inspection_ms: external_exports.number().int().nonnegative().optional(),
21222
+ // What a `redact` this capture could not carry out became instead (see
21223
+ // EventMetadata.redactDegradedTo, whose value this is). Present only when a
21224
+ // degrade actually happened, so absence is the ordinary case rather than a
21225
+ // reader having to distinguish it from a zero.
21226
+ //
21227
+ // PER CAPTURE, while `inspection_findings.action_taken` is per finding —
21228
+ // so on a multi-finding row this does not say which finding degraded, and
21229
+ // its presence does not mean the fallback decided the capture's action. A
21230
+ // capture denied by another finding's own Block policy carries `block`
21231
+ // here too. The full statement is on EventMetadata.redactDegradedTo; it is
21232
+ // repeated rather than referenced because a store reader opens this file.
21233
+ redact_degraded_to: ActionTaken.optional()
21106
21234
  }).catchall(external_exports.unknown());
21107
21235
  var ToolCallInspection = external_exports.object({
21108
21236
  ruleId: external_exports.string().min(1),
@@ -21301,7 +21429,17 @@ var AuditEvent = external_exports.object({
21301
21429
  /** `share` to a first-party/internal destination. */
21302
21430
  internal: external_exports.boolean(),
21303
21431
  /** Event needs review (e.g. unverified egress). */
21304
- flagged: external_exports.boolean()
21432
+ flagged: external_exports.boolean(),
21433
+ /**
21434
+ * The body this event's `title` is drawn from was cleared by local body
21435
+ * expiry, so an EMPTY title here means "gone", not "never had one".
21436
+ *
21437
+ * A separate flag rather than a sentinel written into `title`: the title is
21438
+ * rendered text, and a store-layer module that invented display copy for it
21439
+ * would be choosing words the view is supposed to choose. Additive and
21440
+ * defaulted, so an older producer still validates.
21441
+ */
21442
+ bodyExpired: external_exports.boolean().default(false)
21305
21443
  }).meta({ id: "ActivityAuditEvent" });
21306
21444
  var ActivitySessionSummary = external_exports.object({
21307
21445
  id: external_exports.string(),
@@ -22098,6 +22236,14 @@ var ControlPlaneErrorBody = external_exports.object({
22098
22236
  message: external_exports.string().optional()
22099
22237
  }).optional()
22100
22238
  });
22239
+ var RemoteFailureKind = external_exports.enum([
22240
+ "unauthorized",
22241
+ "forbidden",
22242
+ "route-absent",
22243
+ "invalid-request",
22244
+ "rejected",
22245
+ "unreachable"
22246
+ ]);
22101
22247
  var AttachDeviceRequest = external_exports.object({
22102
22248
  // This machine's own continuity id, so re-attaching ROTATES the credential
22103
22249
  // on one machine record instead of producing a second one. Client-minted
@@ -22633,6 +22779,12 @@ var EventMetadata = external_exports.object({
22633
22779
  // to 'allow' — the enforcement audit trail's link back to the grant that
22634
22780
  // authorized the bypass. Absent on captures where no exception applied.
22635
22781
  exceptionIds: external_exports.array(external_exports.guid()).optional(),
22782
+ // The assistant message this capture belongs to, and the conversation it sits
22783
+ // in — set by the browser extension's network capture so a stored `response`
22784
+ // row can be joined to the `llm_call` leaf describing the same turn. Absent
22785
+ // on every other capture path, which has no such id.
22786
+ messageId: external_exports.string().optional(),
22787
+ conversationId: external_exports.string().optional(),
22636
22788
  // How long THIS capture's inspection blocked its caller, in whole
22637
22789
  // milliseconds — the plugin's own added latency, NOT the LLM call it sat in
22638
22790
  // front of. Measured inside `capture()` (@akasecurity/plugin-sdk) across
@@ -22645,7 +22797,37 @@ var EventMetadata = external_exports.object({
22645
22797
  // Absent is also what every pre-measurement client writes, and what a
22646
22798
  // clock failure degrades to — a reader must treat absence as "not measured"
22647
22799
  // and never as a zero, which would read as "inspection is free".
22648
- inspectionMs: external_exports.number().int().nonnegative().optional()
22800
+ inspectionMs: external_exports.number().int().nonnegative().optional(),
22801
+ // What a `redact` this capture COULD NOT CARRY OUT became instead — the
22802
+ // workspace's `redactFallback`, applied because the field could not be
22803
+ // masked in place (a shell command, a URL, or any argument on a host whose
22804
+ // hook contract offers no rewrite channel).
22805
+ //
22806
+ // It exists because the action alone cannot say why. A finding recorded as
22807
+ // `warn` reads identically whether its detection was ASSIGNED Warn or was
22808
+ // assigned Redact on a field that could not take one — and those are
22809
+ // different facts about the same row: the first is a policy the user chose,
22810
+ // the second is a masking the host could not perform. Absent means no
22811
+ // degrade happened, which is every ordinary capture.
22812
+ //
22813
+ // TWO LIMITS a reader of a stored row has to know, because the grain here
22814
+ // is the CAPTURE while `actionTaken` is per FINDING:
22815
+ //
22816
+ // - It does not say WHICH finding degraded. A capture carrying a degraded
22817
+ // `redact` alongside a finding ASSIGNED the same action stores both
22818
+ // identically and one reason for the pair; attributing it to both
22819
+ // describes the assigned one wrongly, and to neither loses the degrade.
22820
+ // - PRESENCE IS NOT CAUSATION. The value is the action the lost redact
22821
+ // became, not the reason the capture ended as it did — a capture denied
22822
+ // by some other finding's own Block policy still carries `block` here,
22823
+ // and clearing the workspace's fallback would not have let it through.
22824
+ // Gate on the value against what a fallback can produce; never read the
22825
+ // field's presence as "this was the fallback's doing".
22826
+ //
22827
+ // Both are pinned as behaviour in @akasecurity/plugin-sdk's runtime suite.
22828
+ // Closing either means moving the reason onto the finding row, which
22829
+ // already carries its own action.
22830
+ redactDegradedTo: ActionTaken.optional()
22649
22831
  }).meta({ id: "EventMetadata" });
22650
22832
  var Event = external_exports.object({
22651
22833
  id: external_exports.guid(),
@@ -22755,7 +22937,32 @@ var RotateKeyInput = external_exports.object({
22755
22937
  confirmation: external_exports.string()
22756
22938
  });
22757
22939
 
22940
+ // ../../packages/schema/src/zod/finding-delivery.ts
22941
+ var KNOWN_REASONS = SyncFailureReason.options;
22942
+ function knownReason(value) {
22943
+ return value !== null && KNOWN_REASONS.includes(value) ? value : void 0;
22944
+ }
22945
+ function deriveFindingDelivery(row) {
22946
+ if (row.kind === "code_change") return { state: "local_scan" };
22947
+ if (row.syncedAt !== null && row.syncedAt > 0) {
22948
+ return { state: "sent", at: epochMillisToIso(row.syncedAt) };
22949
+ }
22950
+ if (row.syncedAt !== null) {
22951
+ const reason = knownReason(row.syncFailure);
22952
+ return {
22953
+ state: "not_sent",
22954
+ ...row.syncFailedAt === null ? {} : { at: epochMillisToIso(row.syncFailedAt) },
22955
+ ...reason === void 0 ? {} : { reason }
22956
+ };
22957
+ }
22958
+ if (row.outboxOwed === 1 || row.syncClaimedAt !== null) return { state: "queued" };
22959
+ return { state: "never_offered" };
22960
+ }
22961
+
22758
22962
  // ../../packages/schema/src/zod/findings-group-build.ts
22963
+ function lookupOwn(map2, key) {
22964
+ return Object.hasOwn(map2, key) ? map2[key] : void 0;
22965
+ }
22759
22966
  function toApiAction(dbVal) {
22760
22967
  const map2 = {
22761
22968
  log: "monitored",
@@ -22764,7 +22971,7 @@ function toApiAction(dbVal) {
22764
22971
  warn: "warned",
22765
22972
  allow: "allowed"
22766
22973
  };
22767
- return map2[dbVal] ?? "allowed";
22974
+ return lookupOwn(map2, dbVal) ?? "allowed";
22768
22975
  }
22769
22976
  function toApiCategory(dbVal) {
22770
22977
  if (dbVal === "code_context") return "source_code";
@@ -22772,13 +22979,18 @@ function toApiCategory(dbVal) {
22772
22979
  return parsed.success ? parsed.data : "custom";
22773
22980
  }
22774
22981
  function toApiProvider(sourceTool) {
22775
- return TOOL_TO_HARNESS[sourceTool] ?? HARNESS.Api;
22982
+ return lookupOwn(TOOL_TO_HARNESS, sourceTool) ?? HARNESS.Api;
22776
22983
  }
22777
- var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
22984
+ var FINDING_STATUS_PRECEDENCE = [
22985
+ "open",
22986
+ "handled",
22987
+ "dismissed",
22988
+ "resolved"
22989
+ ];
22778
22990
  function foldGroupStatus(instanceStatuses) {
22779
22991
  const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
22780
22992
  if (statuses.size === 0) return void 0;
22781
- for (const candidate of STATUS_PRECEDENCE) {
22993
+ for (const candidate of FINDING_STATUS_PRECEDENCE) {
22782
22994
  if (statuses.has(candidate)) return candidate;
22783
22995
  }
22784
22996
  return void 0;
@@ -22791,139 +23003,62 @@ function deriveFindingStatus(row) {
22791
23003
  if (row.latestResolutionStatus === "dismissed") return "dismissed";
22792
23004
  return "open";
22793
23005
  }
22794
- function distinctUsers(instances) {
22795
- const seen = /* @__PURE__ */ new Set();
22796
- const users = [];
22797
- for (const i of instances) {
22798
- if (i.user === void 0 || seen.has(i.user.id)) continue;
22799
- seen.add(i.user.id);
22800
- users.push(i.user);
22801
- }
22802
- return users;
22803
- }
22804
23006
  function sortUsers(users) {
22805
23007
  return [...users].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
22806
23008
  }
22807
- function buildFindingGroups(rows, opts = {}) {
22808
- const overrides = opts.overrides;
23009
+ function buildFindingTypes(aggregates, opts = {}) {
22809
23010
  const packNames = opts.packNames;
22810
- const aggregates = opts.aggregates;
22811
- const byRuleId = /* @__PURE__ */ new Map();
22812
- for (const row of rows) {
22813
- const existing = byRuleId.get(row.ruleId);
22814
- if (existing) existing.push(row);
22815
- else byRuleId.set(row.ruleId, [row]);
22816
- }
22817
- const groups = [];
22818
- for (const [ruleId, ruleRows] of byRuleId) {
22819
- const instances = ruleRows.map((r) => {
22820
- const effectiveDbAction = overrides?.get(r.id) ?? r.actionTaken;
22821
- return {
22822
- id: r.id,
22823
- provider: toApiProvider(r.sourceTool),
22824
- repo: r.repo,
22825
- file: r.file,
22826
- ...r.toolName === void 0 ? {} : { toolName: r.toolName },
22827
- ...r.eventId === void 0 ? {} : { eventId: r.eventId },
22828
- ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
22829
- ...r.user === void 0 ? {} : { user: r.user },
22830
- action: toApiAction(effectiveDbAction),
22831
- detectedAt: r.occurredAt,
22832
- confidence: r.confidence,
22833
- status: r.status
22834
- };
22835
- });
22836
- const agg = aggregates?.get(ruleId);
22837
- const users = agg ? sortUsers(agg.users ?? []) : distinctUsers(instances);
22838
- const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
22839
- (max, r) => r.occurredAt > max ? r.occurredAt : max,
22840
- ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
22841
- );
22842
- const seenProviders = /* @__PURE__ */ new Set();
22843
- const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
22844
- if (seenProviders.has(p)) return false;
22845
- seenProviders.add(p);
22846
- return true;
22847
- });
22848
- const actionSet = new Set(
22849
- agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
22850
- );
23011
+ const types = [];
23012
+ for (const [ruleId, agg] of aggregates) {
23013
+ const users = sortUsers(agg.users ?? []);
23014
+ const providers = [...new Set(agg.sourceTools.map(toApiProvider))].sort();
23015
+ const actionSet = new Set(agg.actionsTaken.map(toApiAction));
22851
23016
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
22852
- const severity = ruleRows[0]?.severity ?? "low";
22853
- const detection = {
22854
- id: ruleId,
22855
- name: packNames?.get(ruleId) ?? null
22856
- };
22857
- const apiCategory = toApiCategory(ruleRows[0]?.category ?? "custom");
22858
- const policy = { id: `category:${apiCategory}`, name: apiCategory };
22859
- const match = {
22860
- maskedValue: ruleRows[0]?.maskedMatch ?? "",
22861
- contextPrefix: ""
22862
- // empty (pending privacy review)
22863
- };
22864
- const status = foldGroupStatus(
22865
- agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
22866
- );
22867
- const group = {
23017
+ const apiCategory = toApiCategory(agg.category ?? "custom");
23018
+ const type = {
22868
23019
  id: ruleId,
22869
23020
  category: apiCategory,
22870
23021
  subtype: ruleId,
22871
23022
  // human label comes with pack metadata later
22872
- severity,
22873
- match,
22874
- detection,
22875
- policy,
22876
- instanceCount: agg?.instanceCount ?? instances.length,
23023
+ severity: agg.severity ?? "low",
23024
+ detection: { id: ruleId, name: packNames?.get(ruleId) ?? null },
23025
+ policy: { id: `category:${apiCategory}`, name: apiCategory },
23026
+ instanceCount: agg.instanceCount,
22877
23027
  providers,
22878
23028
  aggregateAction,
22879
- latestDetectedAt,
22880
- instances,
22881
- status,
23029
+ latestDetectedAt: agg.latestDetectedAt,
23030
+ status: foldGroupStatus(agg.statusInputs.map(deriveFindingStatus)),
22882
23031
  ...users.length > 0 ? { users } : {}
22883
23032
  };
22884
- if (agg) {
22885
- actionsCache.set(group, [...actionSet]);
22886
- if (agg.searchText !== void 0) {
22887
- haystackCache.set(group, buildHaystack(group, agg.searchText));
22888
- }
23033
+ actionsCache.set(type, [...actionSet]);
23034
+ if (agg.searchText !== void 0) {
23035
+ haystackCache.set(type, buildHaystack(type, agg.searchText));
22889
23036
  }
22890
- groups.push(group);
23037
+ types.push(type);
22891
23038
  }
22892
- return groups;
23039
+ return types;
22893
23040
  }
22894
23041
  var haystackCache = /* @__PURE__ */ new WeakMap();
22895
- function buildHaystack(g, extra) {
23042
+ function buildHaystack(t, extra) {
22896
23043
  return [
22897
- g.subtype,
22898
- g.category,
22899
- g.match.maskedValue,
22900
- g.policy.name,
22901
- g.id,
22902
- ...g.instances.map((i) => i.repo),
22903
- ...g.instances.map((i) => i.file),
22904
- ...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
22905
- ...g.instances.map((i) => i.id),
22906
- // The people: the whole group's list when the store folded one, plus the
22907
- // preview's own — the two overlap, and a haystack does not mind.
22908
- ...(g.users ?? []).map((u) => u.name),
22909
- ...g.instances.map((i) => i.user?.name ?? ""),
23044
+ t.subtype,
23045
+ t.category,
23046
+ t.policy.name,
23047
+ t.id,
23048
+ ...(t.users ?? []).map((u) => u.name),
22910
23049
  ...extra === void 0 ? [] : [extra]
22911
23050
  ].join(" ").toLowerCase();
22912
23051
  }
22913
- function groupHaystack(g) {
22914
- const cached2 = haystackCache.get(g);
23052
+ function typeHaystack(t) {
23053
+ const cached2 = haystackCache.get(t);
22915
23054
  if (cached2 !== void 0) return cached2;
22916
- const haystack = buildHaystack(g);
22917
- haystackCache.set(g, haystack);
23055
+ const haystack = buildHaystack(t);
23056
+ haystackCache.set(t, haystack);
22918
23057
  return haystack;
22919
23058
  }
22920
23059
  var actionsCache = /* @__PURE__ */ new WeakMap();
22921
- function groupActions(g) {
22922
- const cached2 = actionsCache.get(g);
22923
- if (cached2 !== void 0) return cached2;
22924
- const actions = [...new Set(g.instances.map((i) => i.action))];
22925
- actionsCache.set(g, actions);
22926
- return actions;
23060
+ function typeActions(t) {
23061
+ return actionsCache.get(t) ?? [];
22927
23062
  }
22928
23063
  function countInstancesByStatus(statusInputs, statuses) {
22929
23064
  const statusSet = new Set(statuses);
@@ -22934,8 +23069,8 @@ function countInstancesByStatus(statusInputs, statuses) {
22934
23069
  }
22935
23070
  return sum;
22936
23071
  }
22937
- function applyFindingFilters(groups, opts) {
22938
- let filtered = groups;
23072
+ function applyFindingFilters(types, opts) {
23073
+ let filtered = types;
22939
23074
  if (opts.severity && opts.severity.length > 0) {
22940
23075
  const sevSet = new Set(opts.severity);
22941
23076
  filtered = filtered.filter((g) => sevSet.has(g.severity));
@@ -22946,7 +23081,7 @@ function applyFindingFilters(groups, opts) {
22946
23081
  }
22947
23082
  if (opts.actions && opts.actions.length > 0) {
22948
23083
  const actionSet = new Set(opts.actions);
22949
- filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
23084
+ filtered = filtered.filter((t) => typeActions(t).some((a) => actionSet.has(a)));
22950
23085
  }
22951
23086
  if (opts.subtype && opts.subtype.length > 0) {
22952
23087
  const subtypeSet = new Set(opts.subtype);
@@ -22958,26 +23093,31 @@ function applyFindingFilters(groups, opts) {
22958
23093
  }
22959
23094
  if (opts.q) {
22960
23095
  const q = opts.q.toLowerCase();
22961
- filtered = filtered.filter((g) => groupHaystack(g).includes(q));
23096
+ filtered = filtered.filter((t) => typeHaystack(t).includes(q));
22962
23097
  }
22963
23098
  return filtered;
22964
23099
  }
22965
- var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
22966
- var SEVERITY_RANK = SEVERITY_ORDER;
23100
+ function rankByOrder(members2) {
23101
+ return Object.fromEntries(members2.map((member, index) => [member, index]));
23102
+ }
23103
+ var SEVERITY_RANK = rankByOrder(Severity.options);
23104
+ function severityRank(severity) {
23105
+ return lookupOwn(SEVERITY_RANK, severity);
23106
+ }
22967
23107
  function compareFindingGroupOrder(a, b) {
22968
- const rankA = SEVERITY_RANK[a.severity] ?? -1;
22969
- const rankB = SEVERITY_RANK[b.severity] ?? -1;
23108
+ const rankA = severityRank(a.severity) ?? -1;
23109
+ const rankB = severityRank(b.severity) ?? -1;
22970
23110
  const severityDiff = rankA - rankB;
22971
23111
  if (severityDiff !== 0) return severityDiff;
22972
23112
  const recencyDiff = b.latestDetectedAt.localeCompare(a.latestDetectedAt);
22973
23113
  if (recencyDiff !== 0) return recencyDiff;
22974
23114
  return a.id.localeCompare(b.id);
22975
23115
  }
22976
- function sortFindingGroups(groups) {
22977
- return [...groups].sort(compareFindingGroupOrder);
23116
+ function sortFindingTypes(types) {
23117
+ return [...types].sort(compareFindingGroupOrder);
22978
23118
  }
22979
- function computeFindingFacets(allGroups, opts) {
22980
- const forSeverity = applyFindingFilters(allGroups, {
23119
+ function computeFindingFacets(allTypes, opts) {
23120
+ const forSeverity = applyFindingFilters(allTypes, {
22981
23121
  providers: opts.providers,
22982
23122
  actions: opts.actions,
22983
23123
  statuses: opts.statuses,
@@ -22988,7 +23128,7 @@ function computeFindingFacets(allGroups, opts) {
22988
23128
  for (const g of forSeverity) {
22989
23129
  severityMap.set(g.severity, (severityMap.get(g.severity) ?? 0) + 1);
22990
23130
  }
22991
- const forProvider = applyFindingFilters(allGroups, {
23131
+ const forProvider = applyFindingFilters(allTypes, {
22992
23132
  actions: opts.actions,
22993
23133
  statuses: opts.statuses,
22994
23134
  q: opts.q,
@@ -22999,7 +23139,7 @@ function computeFindingFacets(allGroups, opts) {
22999
23139
  for (const g of forProvider) {
23000
23140
  for (const p of g.providers) providerMap.set(p, (providerMap.get(p) ?? 0) + 1);
23001
23141
  }
23002
- const forAction = applyFindingFilters(allGroups, {
23142
+ const forAction = applyFindingFilters(allTypes, {
23003
23143
  providers: opts.providers,
23004
23144
  statuses: opts.statuses,
23005
23145
  q: opts.q,
@@ -23008,9 +23148,9 @@ function computeFindingFacets(allGroups, opts) {
23008
23148
  });
23009
23149
  const actionMap = /* @__PURE__ */ new Map();
23010
23150
  for (const g of forAction) {
23011
- for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
23151
+ for (const a of typeActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
23012
23152
  }
23013
- const forSubtype = applyFindingFilters(allGroups, {
23153
+ const forSubtype = applyFindingFilters(allTypes, {
23014
23154
  providers: opts.providers,
23015
23155
  actions: opts.actions,
23016
23156
  statuses: opts.statuses,
@@ -23019,7 +23159,7 @@ function computeFindingFacets(allGroups, opts) {
23019
23159
  });
23020
23160
  const subtypeMap = /* @__PURE__ */ new Map();
23021
23161
  for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
23022
- const forStatus = applyFindingFilters(allGroups, {
23162
+ const forStatus = applyFindingFilters(allTypes, {
23023
23163
  providers: opts.providers,
23024
23164
  actions: opts.actions,
23025
23165
  q: opts.q,
@@ -23041,6 +23181,20 @@ function computeFindingFacets(allGroups, opts) {
23041
23181
  }
23042
23182
 
23043
23183
  // ../../packages/schema/src/zod/findings-flat-build.ts
23184
+ function compareCodePoints(a, b) {
23185
+ const aIter = a[Symbol.iterator]();
23186
+ const bIter = b[Symbol.iterator]();
23187
+ for (; ; ) {
23188
+ const aNext = aIter.next();
23189
+ const bNext = bIter.next();
23190
+ if (aNext.done && bNext.done) return 0;
23191
+ if (aNext.done) return -1;
23192
+ if (bNext.done) return 1;
23193
+ const aPoint = aNext.value.codePointAt(0) ?? 0;
23194
+ const bPoint = bNext.value.codePointAt(0) ?? 0;
23195
+ if (aPoint !== bPoint) return aPoint - bPoint;
23196
+ }
23197
+ }
23044
23198
  function rowHaystack(row) {
23045
23199
  return [
23046
23200
  row.ruleId,
@@ -23065,12 +23219,24 @@ function matchesDimension(row, opts, dimension) {
23065
23219
  return !opts.actions?.length || opts.actions.includes(toApiAction(row.actionTaken));
23066
23220
  case "statuses":
23067
23221
  return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
23222
+ case "deliveries":
23223
+ return !opts.deliveries?.length || row.delivery !== void 0 && opts.deliveries.includes(row.delivery.state);
23068
23224
  case "tools":
23069
23225
  return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
23226
+ // An EMPTY value is a real filter here, not an absent one. The location
23227
+ // list buckets a finding whose event recorded no repo — or no file — under
23228
+ // the empty string, and selecting that bucket has to narrow the panel to
23229
+ // exactly it. Only `undefined` means "no filter"; a caller that wants every
23230
+ // row omits the key, which every call site already does.
23231
+ //
23232
+ // Reading '' as unset is what this replaced, and it failed in the one place
23233
+ // it mattered: the no-repo/no-file bucket is often the largest in a real
23234
+ // store, and its panel dropped both predicates and returned the WHOLE scope
23235
+ // — a row reading 3 findings beside a panel listing every finding there is.
23070
23236
  case "repo":
23071
- return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
23237
+ return opts.repo === void 0 || row.repo === opts.repo;
23072
23238
  case "file":
23073
- return opts.file === void 0 || opts.file === "" || row.file === opts.file;
23239
+ return opts.file === void 0 || row.file === opts.file;
23074
23240
  case "q":
23075
23241
  return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
23076
23242
  }
@@ -23081,6 +23247,7 @@ var DIMENSIONS = [
23081
23247
  "providers",
23082
23248
  "actions",
23083
23249
  "statuses",
23250
+ "deliveries",
23084
23251
  "tools",
23085
23252
  "repo",
23086
23253
  "file",
@@ -23094,10 +23261,19 @@ function matchesInstanceFilters(row, opts, except) {
23094
23261
  return true;
23095
23262
  }
23096
23263
  function toItems(counts) {
23097
- return [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
23264
+ return [...counts.entries()].map(([value, count]) => ({ value, count })).sort(
23265
+ (a, b) => b.count - a.count || a.value.localeCompare(b.value) || // localeCompare reports canonically-equivalent strings (an NFC and an
23266
+ // NFD spelling of the same text) as equal, so a count tie between
23267
+ // them would otherwise be ordered by whichever the Map iteration
23268
+ // produced. compareCodePoints breaks that tie deterministically, which
23269
+ // makes this a TOTAL order — not one that agrees with SQL collation,
23270
+ // which it need not: foldFacetTuples runs this same sort over grouped
23271
+ // tuples, so both paths order facets identically by construction.
23272
+ compareCodePoints(a.value, b.value)
23273
+ );
23098
23274
  }
23099
- function bump(counts, value) {
23100
- counts.set(value, (counts.get(value) ?? 0) + 1);
23275
+ function bump(counts, value, by = 1) {
23276
+ counts.set(value, (counts.get(value) ?? 0) + by);
23101
23277
  }
23102
23278
  function createInstanceFacetAccumulator(opts) {
23103
23279
  const severity = /* @__PURE__ */ new Map();
@@ -23106,6 +23282,7 @@ function createInstanceFacetAccumulator(opts) {
23106
23282
  const action = /* @__PURE__ */ new Map();
23107
23283
  const status = /* @__PURE__ */ new Map();
23108
23284
  const tool = /* @__PURE__ */ new Map();
23285
+ const deployment = /* @__PURE__ */ new Map();
23109
23286
  return {
23110
23287
  add(row) {
23111
23288
  if (matchesInstanceFilters(row, opts, "severity")) bump(severity, row.severity);
@@ -23120,6 +23297,9 @@ function createInstanceFacetAccumulator(opts) {
23120
23297
  if (row.toolName !== void 0 && matchesInstanceFilters(row, opts, "tools")) {
23121
23298
  bump(tool, row.toolName);
23122
23299
  }
23300
+ if (row.delivery !== void 0 && matchesInstanceFilters(row, opts, "deliveries")) {
23301
+ bump(deployment, row.delivery.state);
23302
+ }
23123
23303
  },
23124
23304
  facets: () => ({
23125
23305
  severity: toItems(severity),
@@ -23127,7 +23307,8 @@ function createInstanceFacetAccumulator(opts) {
23127
23307
  provider: toItems(provider),
23128
23308
  action: toItems(action),
23129
23309
  status: toItems(status),
23130
- tool: toItems(tool)
23310
+ tool: toItems(tool),
23311
+ deployment: toItems(deployment)
23131
23312
  })
23132
23313
  };
23133
23314
  }
@@ -23141,6 +23322,7 @@ function toInstanceDetail(row) {
23141
23322
  ...row.toolName === void 0 ? {} : { toolName: row.toolName },
23142
23323
  eventId: row.eventId,
23143
23324
  ...row.sessionId === void 0 ? {} : { sessionId: row.sessionId },
23325
+ ...row.delivery === void 0 ? {} : { delivery: row.delivery },
23144
23326
  ...row.user === void 0 ? {} : { user: row.user },
23145
23327
  action: toApiAction(row.actionTaken),
23146
23328
  detectedAt: row.occurredAt,
@@ -23155,12 +23337,6 @@ function toInstanceDetail(row) {
23155
23337
  policy: { id: `category:${category}`, name: category }
23156
23338
  };
23157
23339
  }
23158
- var SEVERITY_ORDER2 = {
23159
- critical: 0,
23160
- high: 1,
23161
- medium: 2,
23162
- low: 3
23163
- };
23164
23340
  function newLocationAccumulator() {
23165
23341
  return {
23166
23342
  instanceCount: 0,
@@ -23175,7 +23351,7 @@ function newLocationAccumulator() {
23175
23351
  }
23176
23352
  function addToLocation(acc, row) {
23177
23353
  acc.instanceCount += 1;
23178
- const rank = SEVERITY_ORDER2[row.severity] ?? Number.MAX_SAFE_INTEGER - 1;
23354
+ const rank = severityRank(row.severity) ?? Number.MAX_SAFE_INTEGER - 1;
23179
23355
  if (rank < acc.maxSeverityRank) {
23180
23356
  acc.maxSeverityRank = rank;
23181
23357
  acc.maxSeverity = row.severity;
@@ -23184,6 +23360,23 @@ function addToLocation(acc, row) {
23184
23360
  acc.statuses.push(row.status);
23185
23361
  acc.ruleIds.add(row.ruleId);
23186
23362
  }
23363
+ function compareLocationOrder(a, b) {
23364
+ const rankA = severityRank(a.maxSeverity) ?? -1;
23365
+ const rankB = severityRank(b.maxSeverity) ?? -1;
23366
+ if (rankA !== rankB) return rankA - rankB;
23367
+ if (a.latestDetectedAt !== b.latestDetectedAt) {
23368
+ return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
23369
+ }
23370
+ const repoDiff = compareCodePoints(a.repo, b.repo);
23371
+ if (repoDiff !== 0) return repoDiff;
23372
+ return compareCodePoints(a.file, b.file);
23373
+ }
23374
+ function encodeLocationId(repo, file2) {
23375
+ return `${encodePart(repo)}/${encodePart(file2)}`;
23376
+ }
23377
+ function encodePart(value) {
23378
+ return encodeURIComponent(value.replace(/[\uD800-\uDFFF]/gu, "\uFFFD"));
23379
+ }
23187
23380
 
23188
23381
  // ../../packages/schema/src/zod/installed-pack.ts
23189
23382
  var InstalledPack = external_exports.object({
@@ -23251,6 +23444,11 @@ var Policy = external_exports.object({
23251
23444
  // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23252
23445
  provenance: PolicyProvenance.optional()
23253
23446
  }).meta({ id: "Policy" });
23447
+ var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23448
+ var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23449
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23450
+ id: "RedactFallback"
23451
+ });
23254
23452
  var PolicyBundle = external_exports.object({
23255
23453
  version: external_exports.string(),
23256
23454
  policies: external_exports.array(Policy),
@@ -23298,6 +23496,16 @@ var PolicyBundle = external_exports.object({
23298
23496
  // control plane), so no name resolution stands between the decision and the
23299
23497
  // comparison.
23300
23498
  prohibitedModels: external_exports.array(external_exports.string()).optional(),
23499
+ // What a resolved `redact` becomes on a field the host cannot rewrite, as
23500
+ // the ORGANIZATION would have it. Merged raise-only against the device's own
23501
+ // `WorkspaceSettings.redactFallback` (see strongerRedactFallback below), so
23502
+ // a control plane can tighten a machine and never loosen one — the same
23503
+ // direction `mergeRaiseOnly` enforces for policies.
23504
+ //
23505
+ // Optional so an older backend, and an older on-disk cache, still parses;
23506
+ // absent leaves the device's own setting in force, which is the behaviour
23507
+ // that predates the field and the safe direction to default.
23508
+ redactFallback: RedactFallback.optional(),
23301
23509
  customKeywords: external_exports.array(external_exports.string()),
23302
23510
  fetchedAt: external_exports.iso.datetime()
23303
23511
  }).meta({ id: "PolicyBundle" });
@@ -23327,11 +23535,6 @@ function severityFloorPolicy(category) {
23327
23535
  const peak = CATEGORY_PEAK_SEVERITY[category];
23328
23536
  return peak === "critical" || peak === "high" ? "warn" : "monitor";
23329
23537
  }
23330
- var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23331
- var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23332
- var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23333
- id: "RedactFallback"
23334
- });
23335
23538
  var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23336
23539
  var BUILTIN_POLICY_SPECS = {
23337
23540
  monitor: {
@@ -23635,7 +23838,7 @@ function isVaultConsentValid(consent) {
23635
23838
  }
23636
23839
 
23637
23840
  // ../../packages/schema/src/zod/local.ts
23638
- var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23841
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 8;
23639
23842
  var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23640
23843
  var RunMode = external_exports.enum(["standalone", "attached"]);
23641
23844
  var ControlPlaneConnection = external_exports.object({
@@ -23655,6 +23858,15 @@ var HistorySyncConsent = external_exports.object({
23655
23858
  payloadVersion: external_exports.number().int().positive(),
23656
23859
  endpoint: external_exports.string()
23657
23860
  });
23861
+ var BODY_RETENTION_DEFAULT_DAYS = 30;
23862
+ var BodyRetention = external_exports.object({
23863
+ enabled: external_exports.boolean().default(false),
23864
+ // Never 0, and the ceiling is a fat-finger guard rather than a policy
23865
+ // limit — `enabled` is the real gate. A low value cannot reach a row the
23866
+ // sync ledger still owes: the sweep's age filter only ever NARROWS a
23867
+ // candidate set that is already bounded by "delivered, or never owed".
23868
+ retainDays: external_exports.number().int().min(1).max(3650).default(BODY_RETENTION_DEFAULT_DAYS)
23869
+ }).meta({ id: "BodyRetention" });
23658
23870
  var WorkspaceSettings = external_exports.object({
23659
23871
  specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
23660
23872
  runMode: RunMode.default("standalone"),
@@ -23698,12 +23910,18 @@ var WorkspaceSettings = external_exports.object({
23698
23910
  // covers the current payload and must be re-granted.
23699
23911
  modelJudgeConsent: ModelJudgeConsent.optional(),
23700
23912
  // Records that the user consented to the DEFERRED send — the outbox — along
23701
- // with the payload shape and the endpoint they agreed to. Since payload v2
23702
- // that covers both the pre-attach backlog and undelivered captures (which
23703
- // carry prompt/reply text in `content`); the key name predates the widening.
23704
- // Absent until granted, and a grant for a different endpoint or an older
23705
- // payload no longer counts.
23706
- historySyncConsent: HistorySyncConsent.optional()
23913
+ // with the payload shape and the endpoint they agreed to. Since payload v3
23914
+ // that covers the pre-attach backlog AND undelivered captures alike, and both
23915
+ // carry prompt/reply/tool-output text in `content`; the key name predates
23916
+ // both widenings. Absent until granted, and a grant for a different endpoint
23917
+ // or an older payload no longer counts.
23918
+ historySyncConsent: HistorySyncConsent.optional(),
23919
+ // Local body expiry (see BodyRetention). Off until switched on; expiring a
23920
+ // body never removes the row or its findings.
23921
+ bodyRetention: BodyRetention.default({
23922
+ enabled: false,
23923
+ retainDays: BODY_RETENTION_DEFAULT_DAYS
23924
+ })
23707
23925
  });
23708
23926
  function defaultWorkspaceSettings() {
23709
23927
  return WorkspaceSettings.parse({});
@@ -23798,12 +24016,15 @@ function toCaptureAttributes(event) {
23798
24016
  ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
23799
24017
  ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
23800
24018
  ...metadata?.inspectionMs !== void 0 ? { inspection_ms: metadata.inspectionMs } : {},
24019
+ ...metadata?.redactDegradedTo !== void 0 ? { redact_degraded_to: metadata.redactDegradedTo } : {},
23801
24020
  // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
23802
24021
  // has ever populated either), but every legacy metadata key still rides
23803
24022
  // the bag rather than being silently dropped — CaptureAttributes'
23804
24023
  // `.catchall(z.unknown())` carries the long tail.
23805
24024
  ...metadata?.model !== void 0 ? { model: metadata.model } : {},
23806
- ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
24025
+ ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {},
24026
+ ...metadata?.messageId !== void 0 ? { message_id: metadata.messageId } : {},
24027
+ ...metadata?.conversationId !== void 0 ? { conversation_id: metadata.conversationId } : {}
23807
24028
  };
23808
24029
  }
23809
24030
  function captureDefinitionVersion(finding) {
@@ -23831,10 +24052,22 @@ var ManagedSettingKey = external_exports.enum([
23831
24052
  "vaultInlineReveal",
23832
24053
  "modelJudgeConsent",
23833
24054
  "dataSharesInPlace",
23834
- "redactFallback"
24055
+ "redactFallback",
24056
+ // Pins the toggle and the day count together — see BodyRetention on why the
24057
+ // two are one unit. An administrator mandating a window wants the count
24058
+ // enforced with it, not one a user can widen while the toggle stays on.
24059
+ "bodyRetention"
23835
24060
  ]).meta({ id: "ManagedSettingKey" });
24061
+ function isManagedSettingKey(value) {
24062
+ return ManagedSettingKey.safeParse(value).success;
24063
+ }
23836
24064
  var ManagedSettingsValues = external_exports.object({
23837
24065
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
24066
+ // `controlPlane` and `bodyRetention` are the two nested values, and both are
24067
+ // plain, non-strict objects: a key under either that this build does not know
24068
+ // is stripped and nothing reports it. The unknown-value split in
24069
+ // ManagedSettings below classifies top-level names only, so it stops at
24070
+ // these boundaries.
23838
24071
  controlPlane: external_exports.object({
23839
24072
  endpoint: external_exports.string().min(1),
23840
24073
  label: external_exports.string().min(1).optional()
@@ -23845,7 +24078,8 @@ var ManagedSettingsValues = external_exports.object({
23845
24078
  vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23846
24079
  modelJudgeConsent: external_exports.boolean().optional(),
23847
24080
  dataSharesInPlace: external_exports.boolean().optional(),
23848
- redactFallback: RedactFallback.optional()
24081
+ redactFallback: RedactFallback.optional(),
24082
+ bodyRetention: BodyRetention.optional()
23849
24083
  }).meta({ id: "ManagedSettingsValues" });
23850
24084
  var ManagedSettings = external_exports.object({
23851
24085
  specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
@@ -23853,11 +24087,59 @@ var ManagedSettings = external_exports.object({
23853
24087
  // decision from a bug. Absent renders as a generic "your organization".
23854
24088
  organization: external_exports.string().min(1).optional(),
23855
24089
  // What the administrator pinned.
23856
- values: ManagedSettingsValues.default({}),
24090
+ //
24091
+ // Parsed as a RECORD rather than as the nested schema, and split below for
24092
+ // the same reason `lockedFields` is parsed as names: a plain `z.object`
24093
+ // drops an unrecognised key and succeeds, so a pin this build does not know
24094
+ // vanished and nothing anywhere said so. A pin with no lock is a supported
24095
+ // shape — it is a DEFAULT the user may still change — so that silence hit
24096
+ // exactly the file an administrator is most likely to write while a fleet
24097
+ // is mid-upgrade.
24098
+ //
24099
+ // Splitting here rather than calling `.strict()`: strict would REFUSE the
24100
+ // file, which is the outcome the lock half already rejected — an older
24101
+ // build then runs entirely unmanaged, every pin and lock gone. A bad KNOWN
24102
+ // value still fails, because the nested schema is re-run over the known
24103
+ // subset and its issues are re-raised on this parse.
24104
+ values: external_exports.record(external_exports.string(), external_exports.unknown()).default({}),
23857
24105
  // Which of those the user may not change. A key here with no matching value
23858
24106
  // freezes whatever the user last chose; a value with no lock is a DEFAULT
23859
24107
  // the user may still override. The two are separable on purpose.
23860
- lockedFields: external_exports.array(ManagedSettingKey).default([])
24108
+ //
24109
+ // Parsed as NAMES rather than as the enum, and split below: a name this
24110
+ // build does not know is dropped from the locked set and reported, never a
24111
+ // reason to refuse the file. The same shape reaches an older build whenever
24112
+ // an administrator locks a key a newer build added, and refusing it there
24113
+ // ran that build entirely unmanaged — every pin and lock gone — on exactly
24114
+ // the fleets most likely to carry a version skew. A name outside the enum
24115
+ // is still never HONOURED: the lockable set stays explicit above.
24116
+ lockedFields: external_exports.array(external_exports.string()).default([])
24117
+ }).transform(({ lockedFields, values, ...rest }, ctx) => {
24118
+ const known = [];
24119
+ const unknown2 = [];
24120
+ for (const name of lockedFields) {
24121
+ if (isManagedSettingKey(name)) known.push(name);
24122
+ else unknown2.push(name);
24123
+ }
24124
+ const knownValues = /* @__PURE__ */ Object.create(null);
24125
+ const unknownValues = [];
24126
+ for (const [name, value] of Object.entries(values)) {
24127
+ if (Object.hasOwn(ManagedSettingsValues.shape, name)) knownValues[name] = value;
24128
+ else unknownValues.push(name);
24129
+ }
24130
+ const pinned = ManagedSettingsValues.safeParse(knownValues);
24131
+ if (!pinned.success) {
24132
+ for (const issue2 of pinned.error.issues)
24133
+ ctx.addIssue({ ...issue2, path: ["values", ...issue2.path] });
24134
+ return external_exports.NEVER;
24135
+ }
24136
+ return {
24137
+ ...rest,
24138
+ values: pinned.data,
24139
+ lockedFields: known,
24140
+ ...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {},
24141
+ ...unknownValues.length > 0 ? { unknownValueFields: unknownValues } : {}
24142
+ };
23861
24143
  }).meta({ id: "ManagedSettings" });
23862
24144
 
23863
24145
  // ../../packages/schema/src/zod/project-files.ts
@@ -23981,7 +24263,11 @@ var FindingsTimeseriesPoint = external_exports.object({
23981
24263
  timestamp: external_exports.iso.date(),
23982
24264
  critical: external_exports.number().int().nonnegative(),
23983
24265
  high: external_exports.number().int().nonnegative(),
23984
- medium: external_exports.number().int().nonnegative()
24266
+ medium: external_exports.number().int().nonnegative(),
24267
+ // Optional and additive, so a producer written against the earlier
24268
+ // three-series contract keeps validating. A consumer plotting it resolves the
24269
+ // absent case itself — the chart point requires a number.
24270
+ low: external_exports.number().int().nonnegative().optional()
23985
24271
  }).meta({ id: "FindingsTimeseriesPoint" });
23986
24272
  var FindingsTimeseriesResponse = external_exports.object({
23987
24273
  range: TimeRange,
@@ -24007,6 +24293,10 @@ var ResolvedFeedItem = external_exports.object({
24007
24293
  findingKey: external_exports.string(),
24008
24294
  ruleId: external_exports.string(),
24009
24295
  severity: Severity,
24296
+ // Repository slug, and the file path RELATIVE to it. The pair is what
24297
+ // identifies the file: a bare path matches the same name in every repo.
24298
+ // Optional and additive; empty when the event carried no repo.
24299
+ repo: external_exports.string().optional(),
24010
24300
  path: external_exports.string(),
24011
24301
  // ISO-8601 datetime (matches FindingInstance.detectedAt / the rest of the
24012
24302
  // findings domain). The reader `.toISOString()`s the DB epoch-ms values.
@@ -24112,7 +24402,23 @@ var SaveSettingsInput = external_exports.object({
24112
24402
  modelJudgeConsent: ModelJudgeConsentChoice,
24113
24403
  historySyncConsent: HistorySyncConsentChoice,
24114
24404
  vaultConsent: external_exports.string(),
24115
- vaultInlineReveal: external_exports.string()
24405
+ vaultInlineReveal: external_exports.string(),
24406
+ // Widened to `string` like its neighbours rather than typed as
24407
+ // `RedactFallback`, on this module's own layering rule: shape here, VALUE at
24408
+ // the call site, so the domain check receives the type it was written for.
24409
+ //
24410
+ // NOT because a narrower schema would reject differently. `parseActionInput`
24411
+ // is a `safeParse` wrapper and throws for no field schema, so either spelling
24412
+ // reaches a recoverable `{ ok: false }` and there is no rejected promise to
24413
+ // trade against. The real cost runs the other way and is the part worth
24414
+ // knowing: a value this schema admits and the domain enum then rejects lands
24415
+ // on the action's shared refusal, which names NO field, where a shape
24416
+ // rejection reaches `malformedInput` and names the schema key.
24417
+ redactFallback: external_exports.string(),
24418
+ // Shape only, the way the enum fields above are strings only: the RANGE is
24419
+ // `BodyRetention`'s and the action checks it there, so there is one place
24420
+ // that decides what a legal horizon is rather than two that can drift.
24421
+ bodyRetention: external_exports.object({ enabled: external_exports.boolean(), retainDays: external_exports.number() })
24116
24422
  });
24117
24423
  var AttachInput = external_exports.object({
24118
24424
  endpoint: external_exports.string(),
@@ -24284,6 +24590,52 @@ function reviewSeverityRank(reasons) {
24284
24590
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
24285
24591
  }
24286
24592
 
24593
+ // ../../packages/schema/src/zod/web-capture.ts
24594
+ var WebUsageSource = external_exports.enum(["site", "estimated", "none"]);
24595
+ var WebUsage = external_exports.object({
24596
+ inputTokens: external_exports.number().int().nonnegative().optional(),
24597
+ outputTokens: external_exports.number().int().nonnegative().optional(),
24598
+ cacheReadInputTokens: external_exports.number().int().nonnegative().optional(),
24599
+ cacheCreationInputTokens: external_exports.number().int().nonnegative().optional()
24600
+ });
24601
+ var WebToolCall = external_exports.object({
24602
+ toolUseId: external_exports.string().min(1),
24603
+ toolName: external_exports.string().min(1),
24604
+ target: external_exports.string().optional(),
24605
+ isError: external_exports.boolean().optional(),
24606
+ inputSize: external_exports.number().int().nonnegative().optional(),
24607
+ outputSize: external_exports.number().int().nonnegative().optional()
24608
+ });
24609
+ var WebExchange = external_exports.object({
24610
+ messageId: external_exports.string().min(1),
24611
+ startedAt: external_exports.iso.datetime(),
24612
+ model: external_exports.string().optional(),
24613
+ usage: WebUsage.optional(),
24614
+ usageSource: WebUsageSource,
24615
+ stopReason: external_exports.string().optional(),
24616
+ conversationId: external_exports.string().optional(),
24617
+ turnIndex: external_exports.number().int().nonnegative().optional(),
24618
+ toolCalls: external_exports.array(WebToolCall).default([]),
24619
+ // Absent when the adapter recovered no text. Capped by the caller at
24620
+ // RESPONSE_TEXT_MAX_BYTES; `truncated` records that the cap was reached, so a
24621
+ // short capture is never mistaken for a short reply.
24622
+ responseText: external_exports.string().optional(),
24623
+ truncated: external_exports.boolean().default(false)
24624
+ });
24625
+ var RESPONSE_TEXT_MAX_BYTES = 2 * 1024 * 1024;
24626
+ var WebCaptureStatus = external_exports.object({
24627
+ patched: external_exports.boolean(),
24628
+ live: external_exports.boolean(),
24629
+ blind: external_exports.boolean(),
24630
+ sendsSeenDom: external_exports.number().int().nonnegative(),
24631
+ exchangesSeenNet: external_exports.number().int().nonnegative(),
24632
+ parseFailures: external_exports.number().int().nonnegative(),
24633
+ unparsedBodies: external_exports.number().int().nonnegative(),
24634
+ // The adapter-declared JSON key paths that were absent from a real payload —
24635
+ // the earliest signal that a site's contract moved.
24636
+ shapeMisses: external_exports.array(external_exports.string()).default([])
24637
+ });
24638
+
24287
24639
  // ../../packages/persistence/src/paths.ts
24288
24640
  import {
24289
24641
  chmodSync,
@@ -24553,6 +24905,22 @@ function discardStore(file2, backup) {
24553
24905
  }
24554
24906
  }
24555
24907
 
24908
+ // ../../packages/persistence/src/internal/sql-functions.ts
24909
+ var utf8 = new TextDecoder();
24910
+ function akaLower(value) {
24911
+ if (value === null) return null;
24912
+ if (typeof value === "string") return value.toLowerCase();
24913
+ if (typeof value === "number" || typeof value === "bigint") return String(value).toLowerCase();
24914
+ return utf8.decode(value).toLowerCase();
24915
+ }
24916
+ function registerSqlFunctions(db) {
24917
+ db.function(
24918
+ "aka_lower",
24919
+ { deterministic: true, directOnly: true, useBigIntArguments: true },
24920
+ akaLower
24921
+ );
24922
+ }
24923
+
24556
24924
  // ../../packages/persistence/src/internal/sql-text.ts
24557
24925
  function escapeLikePattern(s) {
24558
24926
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -24637,6 +25005,11 @@ function schemaObjectExists(db, kind, name) {
24637
25005
  function indexExists(db, name) {
24638
25006
  return schemaObjectExists(db, "index", name);
24639
25007
  }
25008
+ function indexColumns(db, name) {
25009
+ if (!indexExists(db, name)) return [];
25010
+ const columns = db.prepare(`PRAGMA index_info(${name})`).all();
25011
+ return columns.map((c) => c.name).filter((c) => c !== null);
25012
+ }
24640
25013
  function columnNames(db, table, opts) {
24641
25014
  const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
24642
25015
  const columns = db.prepare(`PRAGMA ${pragma}(${table})`).all();
@@ -24698,6 +25071,647 @@ function mapRowsTolerant(rows, map2) {
24698
25071
  return out;
24699
25072
  }
24700
25073
 
25074
+ // ../../packages/persistence/src/internal/outbox-lane.ts
25075
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
25076
+ var OUTBOX_CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
25077
+
25078
+ // ../../packages/persistence/src/sync-failure.ts
25079
+ var SYNC_FAILURE_REASONS = SyncFailureReason.options;
25080
+ function syncFailureRejectCondition(column = "sync_failure") {
25081
+ const members2 = SYNC_FAILURE_REASONS.map((r) => `'${r}'`).join(", ");
25082
+ return `NEW.${column} IS NOT NULL AND NEW.${column} NOT IN (${members2})`;
25083
+ }
25084
+
25085
+ // ../../packages/persistence/src/repositories/history-sync.ts
25086
+ var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
25087
+ var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
25088
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_TYPE_LIST;
25089
+ var COUNTED_EVENT_TYPES = [
25090
+ ...STRUCTURAL_EVENT_TYPES,
25091
+ ...OUTBOX_CAPTURE_EVENT_TYPES
25092
+ ];
25093
+ var COUNTED_TYPE_LIST = COUNTED_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
25094
+ var PARTITION_BUCKETS = `
25095
+ SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NULL THEN 1 ELSE 0 END) AS queued,
25096
+ SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NOT NULL THEN 1 ELSE 0 END) AS inProgress,
25097
+ SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS synced,
25098
+ SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
25099
+ AND sync_failure = 'deployment_refused' THEN 1 ELSE 0 END) AS refused,
25100
+ SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
25101
+ AND sync_failure = 'detached_undelivered' THEN 1 ELSE 0 END) AS detached,
25102
+ -- Spelled as what it INCLUDES rather than what it excludes, so a reason
25103
+ -- added later lands in no bucket and fails the sum assertion, instead
25104
+ -- of silently joining this one.
25105
+ SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
25106
+ AND (sync_failure IS NULL OR sync_failure = 'payload_invalid')
25107
+ THEN 1 ELSE 0 END) AS failed,
25108
+ COUNT(*) AS total`;
25109
+ var COUNTED_SCOPE = `
25110
+ WHERE event_type IN (${COUNTED_TYPE_LIST})
25111
+ AND (
25112
+ event_type IN (${TYPE_LIST})
25113
+ OR synced_at IS NOT NULL
25114
+ OR outbox_owed = 1
25115
+ )`;
25116
+ var SKIPPED = -1;
25117
+ var ROW_COLUMNS = `id,
25118
+ parent_id AS parentId,
25119
+ root_session_id AS rootSessionId,
25120
+ event_type AS eventType,
25121
+ host_id AS hostId,
25122
+ harness_id AS harnessId,
25123
+ source_project_id AS sourceProjectId,
25124
+ started_at AS startedAt,
25125
+ ended_at AS endedAt,
25126
+ severity,
25127
+ priority,
25128
+ content,
25129
+ content_hash AS contentHash,
25130
+ attributes`;
25131
+ var SqliteHistorySyncRepository = class {
25132
+ constructor(db) {
25133
+ this.db = db;
25134
+ this.ensureRowStmt = db.prepare(`INSERT OR IGNORE INTO history_sync (id) VALUES (1)`);
25135
+ this.sessionsStmt = db.prepare(
25136
+ `SELECT COALESCE(root_session_id, id) AS sessionId, MIN(started_at) AS earliest
25137
+ FROM audit_events
25138
+ WHERE synced_at IS NULL
25139
+ AND event_type IN (${TYPE_LIST})
25140
+ AND started_at < :before
25141
+ GROUP BY sessionId
25142
+ ORDER BY earliest
25143
+ LIMIT :limit`
25144
+ );
25145
+ this.rowsStmt = db.prepare(
25146
+ `SELECT ${ROW_COLUMNS}
25147
+ FROM audit_events
25148
+ WHERE synced_at IS NULL
25149
+ AND event_type IN (${TYPE_LIST})
25150
+ AND started_at < :before
25151
+ AND COALESCE(root_session_id, id) = :sessionId
25152
+ ORDER BY (event_type = 'session') DESC, started_at
25153
+ LIMIT :limit`
25154
+ );
25155
+ this.captureRowsStmt = db.prepare(
25156
+ `SELECT ${ROW_COLUMNS}
25157
+ FROM audit_events
25158
+ WHERE synced_at IS NULL
25159
+ AND sync_claimed_at IS NULL
25160
+ AND outbox_owed = 1
25161
+ AND event_type IN (${CAPTURE_TYPE_LIST})
25162
+ AND started_at < :before
25163
+ ORDER BY started_at
25164
+ LIMIT :limit`
25165
+ );
25166
+ this.markOwedStmt = db.prepare(
25167
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
25168
+ );
25169
+ this.markCaptureBacklogOwedStmt = db.prepare(
25170
+ `UPDATE audit_events SET outbox_owed = 1
25171
+ WHERE synced_at IS NULL
25172
+ AND event_type IN (${CAPTURE_TYPE_LIST})
25173
+ AND started_at < :before`
25174
+ );
25175
+ this.stampStmt = db.prepare(
25176
+ `UPDATE audit_events
25177
+ SET synced_at = :at,
25178
+ sync_claimed_at = NULL,
25179
+ sync_failed_at = :failedAt,
25180
+ sync_failure = :failure
25181
+ WHERE id = :id`
25182
+ );
25183
+ this.claimRowStmt = db.prepare(
25184
+ `UPDATE audit_events SET sync_claimed_at = :at WHERE id = :id AND synced_at IS NULL`
25185
+ );
25186
+ this.releaseRowStmt = db.prepare(
25187
+ `UPDATE audit_events SET sync_claimed_at = NULL WHERE id = :id`
25188
+ );
25189
+ this.releaseStaleClaimsStmt = db.prepare(
25190
+ `UPDATE audit_events SET sync_claimed_at = NULL
25191
+ WHERE sync_claimed_at IS NOT NULL AND sync_claimed_at < :staleBefore`
25192
+ );
25193
+ this.partitionStmt = db.prepare(`SELECT${PARTITION_BUCKETS}
25194
+ FROM audit_events${COUNTED_SCOPE}`);
25195
+ this.partitionByKindStmt = db.prepare(
25196
+ `SELECT event_type AS kind,${PARTITION_BUCKETS}
25197
+ FROM audit_events INDEXED BY idx_audit_events_sync${COUNTED_SCOPE}
25198
+ GROUP BY event_type`
25199
+ );
25200
+ this.countsStmt = db.prepare(
25201
+ `SELECT
25202
+ SUM(CASE WHEN synced_at IS NULL AND started_at < :before THEN 1 ELSE 0 END) AS pending,
25203
+ SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS sent,
25204
+ SUM(CASE WHEN synced_at = ${String(SKIPPED)}
25205
+ AND (sync_failure IS NULL OR sync_failure = 'payload_invalid')
25206
+ THEN 1 ELSE 0 END) AS skipped,
25207
+ SUM(CASE WHEN synced_at = ${String(SKIPPED)}
25208
+ AND sync_failure = 'deployment_refused' THEN 1 ELSE 0 END) AS refused,
25209
+ SUM(CASE WHEN synced_at = ${String(SKIPPED)}
25210
+ AND sync_failure = 'detached_undelivered' THEN 1 ELSE 0 END) AS detached
25211
+ FROM audit_events
25212
+ WHERE event_type IN (${TYPE_LIST})`
25213
+ );
25214
+ this.captureSkipCountStmt = db.prepare(
25215
+ // EVERY sentinel capture, whatever the reason — deliberately NOT split the
25216
+ // way the structural totals are. The split exists because a refusal is
25217
+ // terminal only against the deployment that gave it, and the structural
25218
+ // re-arm frees it on a change of deployment. The capture lane has no such
25219
+ // escape: re-arming a capture would offer one deployment's undelivered
25220
+ // prompts, with their text, to a deployment that never saw them, which is
25221
+ // exactly what disownCapturesStmt exists to prevent. So on this lane both
25222
+ // reasons mean the same thing — this row will not be sent — and splitting
25223
+ // them would put refused captures in a bucket nothing reads and nothing
25224
+ // frees.
25225
+ `SELECT COUNT(*) AS skipped
25226
+ FROM audit_events
25227
+ WHERE synced_at = ${String(SKIPPED)}
25228
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
25229
+ );
25230
+ this.fingerprintStmt = db.prepare(
25231
+ `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
25232
+ FROM history_sync WHERE id = 1`
25233
+ );
25234
+ this.setFingerprintStmt = db.prepare(
25235
+ `UPDATE history_sync
25236
+ SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
25237
+ WHERE id = 1`
25238
+ );
25239
+ this.disownCapturesStmt = db.prepare(
25240
+ `UPDATE audit_events SET outbox_owed = NULL
25241
+ WHERE outbox_owed IS NOT NULL
25242
+ AND event_type IN (${CAPTURE_TYPE_LIST})
25243
+ AND started_at < :attachedAt`
25244
+ );
25245
+ this.rearmStmt = db.prepare(
25246
+ `UPDATE audit_events
25247
+ SET synced_at = NULL, sync_failed_at = NULL, sync_failure = NULL
25248
+ WHERE (synced_at > 0
25249
+ OR sync_failure IN ('deployment_refused', 'detached_undelivered'))
25250
+ AND event_type IN (${TYPE_LIST})`
25251
+ );
25252
+ this.claimStmt = db.prepare(
25253
+ `UPDATE history_sync
25254
+ SET owner_pid = :pid, owner_host = :host, acquired_at = :now, heartbeat_at = :now
25255
+ WHERE id = 1
25256
+ AND (owner_pid IS NULL
25257
+ OR heartbeat_at IS NULL
25258
+ OR heartbeat_at < :staleBefore
25259
+ OR heartbeat_at > :now)`
25260
+ );
25261
+ this.heartbeatStmt = db.prepare(
25262
+ `UPDATE history_sync SET heartbeat_at = :now WHERE id = 1 AND owner_pid = :pid`
25263
+ );
25264
+ this.releaseStmt = db.prepare(
25265
+ `UPDATE history_sync
25266
+ SET owner_pid = NULL, owner_host = NULL, acquired_at = NULL, heartbeat_at = NULL
25267
+ WHERE id = 1 AND owner_pid = :pid`
25268
+ );
25269
+ this.closeWindowStmt = db.prepare(
25270
+ `UPDATE audit_events
25271
+ SET synced_at = ${String(SKIPPED)},
25272
+ sync_failed_at = :at,
25273
+ sync_failure = 'detached_undelivered'
25274
+ WHERE synced_at IS NULL
25275
+ AND event_type IN (${TYPE_LIST})
25276
+ AND started_at >= :attachedAt`
25277
+ );
25278
+ this.releaseBoundaryStmt = db.prepare(
25279
+ `UPDATE history_sync SET backlog_before = NULL WHERE id = 1`
25280
+ );
25281
+ this.freezeBoundaryStmt = db.prepare(
25282
+ `UPDATE history_sync SET backlog_before = :backlogBefore WHERE id = 1`
25283
+ );
25284
+ this.leaseStmt = db.prepare(
25285
+ `SELECT owner_pid AS ownerPid, owner_host AS ownerHost,
25286
+ acquired_at AS acquiredAt, heartbeat_at AS heartbeatAt
25287
+ FROM history_sync WHERE id = 1`
25288
+ );
25289
+ this.inspectionsStmt = db.prepare(
25290
+ `SELECT d.rule_id AS ruleId,
25291
+ d.name AS ruleName,
25292
+ d.version AS ruleVersion,
25293
+ d.category AS category,
25294
+ d.severity AS severity,
25295
+ f.span_start AS spanStart,
25296
+ f.span_end AS spanEnd,
25297
+ f.masked_match AS maskedMatch,
25298
+ f.action_taken AS actionTaken,
25299
+ f.confidence AS confidence
25300
+ FROM inspection_findings f
25301
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
25302
+ WHERE f.audit_event_id = :auditEventId
25303
+ ORDER BY f.span_start, f.id`
25304
+ );
25305
+ }
25306
+ db;
25307
+ ensureRowStmt;
25308
+ sessionsStmt;
25309
+ rowsStmt;
25310
+ stampStmt;
25311
+ countsStmt;
25312
+ fingerprintStmt;
25313
+ setFingerprintStmt;
25314
+ rearmStmt;
25315
+ claimStmt;
25316
+ heartbeatStmt;
25317
+ releaseStmt;
25318
+ leaseStmt;
25319
+ inspectionsStmt;
25320
+ closeWindowStmt;
25321
+ releaseBoundaryStmt;
25322
+ freezeBoundaryStmt;
25323
+ captureRowsStmt;
25324
+ markOwedStmt;
25325
+ markCaptureBacklogOwedStmt;
25326
+ captureSkipCountStmt;
25327
+ disownCapturesStmt;
25328
+ partitionStmt;
25329
+ partitionByKindStmt;
25330
+ claimRowStmt;
25331
+ releaseRowStmt;
25332
+ releaseStaleClaimsStmt;
25333
+ /**
25334
+ * The masked detections recorded against one tool call.
25335
+ *
25336
+ * These travel with the event because a tool call's target is not
25337
+ * re-inspectable from the event alone — unlike a capture, where the text
25338
+ * itself is re-scannable. What crosses is the masked match and the rule that
25339
+ * produced it, never the value.
25340
+ */
25341
+ inspectionsFor(auditEventId) {
25342
+ return allRows(this.inspectionsStmt, { auditEventId });
25343
+ }
25344
+ /**
25345
+ * Sessions with structural rows still to send, oldest first.
25346
+ *
25347
+ * BOUNDED BY THE BACKLOG BOUNDARY, which is the whole correctness of this
25348
+ * read. Anything recorded after the machine attached is the live forward
25349
+ * path's to deliver; this drain exists for what was recorded before it, and a
25350
+ * row both paths send is at best a duplicate request and at worst — for a
25351
+ * session root — an overwrite of the inventory ids the live path resolved.
25352
+ */
25353
+ pendingSessions(limit, before) {
25354
+ return allRows(this.sessionsStmt, { limit, before }).map(
25355
+ (r) => r.sessionId
25356
+ );
25357
+ }
25358
+ /** One session's undelivered structural rows within the backlog, root first. */
25359
+ pendingRows(sessionId, limit, before) {
25360
+ return allRows(this.rowsStmt, { sessionId, limit, before });
25361
+ }
25362
+ /**
25363
+ * Captures this machine still owes the deployment, oldest first.
25364
+ *
25365
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
25366
+ * by a time window — see captureRowsStmt for why a window could not express
25367
+ * this. `before` is the grace window that leaves a just-recorded capture to
25368
+ * the live path.
25369
+ */
25370
+ pendingCaptureRows(limit, before) {
25371
+ return allRows(this.captureRowsStmt, { limit, before });
25372
+ }
25373
+ /**
25374
+ * Record that a capture is OWED to the deployment.
25375
+ *
25376
+ * Written by the attached forward path when a live send did not confirm
25377
+ * delivery, and read by the drain as the whole of its eligibility test. It is
25378
+ * a fact rather than an inference: the machine was attached, the send did not
25379
+ * land, so the row is owed — which no time window can state, because the same
25380
+ * window that holds the rows a past attachment left owed also holds every
25381
+ * capture recorded while the machine was DETACHED, and those were never
25382
+ * offered to anyone.
25383
+ *
25384
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
25385
+ * out of the drain's read.
25386
+ */
25387
+ markCaptureOwed(id) {
25388
+ this.markOwedStmt.run({ id });
25389
+ }
25390
+ /**
25391
+ * Mark every capture already on disk as owed, as of `before`.
25392
+ *
25393
+ * The consent-time backfill, called once from `aka attach` when a human
25394
+ * grants existing-history consent — never from an ongoing drain pass, and
25395
+ * never inferred from a boundary that could later move. `before` is the
25396
+ * caller's own "now" at the moment consent was granted, so what this marks
25397
+ * is exactly the backlog the consent prompt already counted, not whatever a
25398
+ * later re-attach or key rotation might widen it to.
25399
+ *
25400
+ * Returns how many rows matched, for the caller to log or test against. Not a
25401
+ * count of NEWLY marked rows — a row still unsynced from an earlier call
25402
+ * matches again and is counted again, the same as `UPDATE`'s own `changes`.
25403
+ */
25404
+ markCaptureBacklogOwed(before) {
25405
+ return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
25406
+ }
25407
+ /**
25408
+ * Record delivery. Called only AFTER the far side has accepted the rows.
25409
+ *
25410
+ * CLEARS any failure reason in the same statement. A row that failed against
25411
+ * one deployment and then landed is delivered, and leaving the reason behind
25412
+ * would leave the store holding two contradictory answers about one row —
25413
+ * with the surface free to render either.
25414
+ */
25415
+ markSynced(ids, atMs) {
25416
+ this.stampAll(ids, atMs, null);
25417
+ }
25418
+ /**
25419
+ * Record that THIS MACHINE cannot express the row on the wire.
25420
+ *
25421
+ * Reserved for a local defect — a row that cannot be rebuilt into a valid
25422
+ * payload, or a body the client itself refused to send. It fails identically
25423
+ * against every deployment, so it is terminal everywhere and the re-arm leaves
25424
+ * it alone. A row that merely failed to REACH the deployment stays NULL, so it
25425
+ * is retried; marking those would turn one outage into permanent data loss.
25426
+ */
25427
+ markSkipped(ids, atMs) {
25428
+ this.stampAll(ids, SKIPPED, "payload_invalid", atMs);
25429
+ }
25430
+ /**
25431
+ * Record that THIS DEPLOYMENT refused the row.
25432
+ *
25433
+ * The same sentinel as `markSkipped`, and deliberately so: both stop the row
25434
+ * being re-offered on this lane, and `synced_at` goes on answering whether a
25435
+ * row is outstanding rather than why. What separates them is the reason, and
25436
+ * what the reason buys is the re-arm — a refusal is one deployment's verdict
25437
+ * on one body, so it is terminal only for as long as this machine points at
25438
+ * that deployment, and `rearmFor` clears it when the deployment changes.
25439
+ *
25440
+ * Leaving such a row NULL instead would be worse than the loss it replaces:
25441
+ * these reads carry no cursor, so an unstamped row the deployment refuses is
25442
+ * the head of every subsequent page, and the lane stalls behind it for ever.
25443
+ */
25444
+ markRefused(ids, atMs) {
25445
+ this.stampAll(ids, SKIPPED, "deployment_refused", atMs);
25446
+ }
25447
+ eachInTransaction(ids, run) {
25448
+ if (ids.length === 0) return;
25449
+ withTransaction(
25450
+ this.db,
25451
+ () => {
25452
+ for (const id of ids) run(id);
25453
+ },
25454
+ "IMMEDIATE"
25455
+ );
25456
+ }
25457
+ stampAll(ids, value, failure, failedAtMs) {
25458
+ if (ids.length === 0) return;
25459
+ const failedAt = failure === null ? null : failedAtMs ?? null;
25460
+ withTransaction(
25461
+ this.db,
25462
+ () => {
25463
+ for (const id of ids) this.stampStmt.run({ at: value, failedAt, failure, id });
25464
+ },
25465
+ "IMMEDIATE"
25466
+ );
25467
+ }
25468
+ /**
25469
+ * Claim rows as in-flight.
25470
+ *
25471
+ * Advisory in exactly the sense the lease is: it records that a send is in
25472
+ * progress so a surface can say so, and a lost claim costs a row showing as
25473
+ * queued while it is actually being sent. It is not exclusion — the far side
25474
+ * settles a duplicate on the row id.
25475
+ */
25476
+ claimRows(ids, atMs) {
25477
+ this.eachInTransaction(ids, (id) => this.claimRowStmt.run({ at: atMs, id }));
25478
+ }
25479
+ /** Give back a claim without settling — the send failed, the row is queued again. */
25480
+ releaseRows(ids) {
25481
+ this.eachInTransaction(ids, (id) => this.releaseRowStmt.run({ id }));
25482
+ }
25483
+ /**
25484
+ * Clear claims older than `staleBefore`, and report how many were cleared.
25485
+ *
25486
+ * A process killed between claiming and settling leaves rows claimed with
25487
+ * nothing left to settle them. Without this they read as "sending" for ever.
25488
+ */
25489
+ releaseStaleClaims(staleBefore) {
25490
+ return Number(this.releaseStaleClaimsStmt.run({ staleBefore }).changes);
25491
+ }
25492
+ /**
25493
+ * Every tracked row in exactly one delivery state.
25494
+ *
25495
+ * Takes no boundary on purpose. The boundary answers "what should the drain
25496
+ * pick up now", which is a different question from "what state is this row
25497
+ * in" — and a machine that has never attached has no boundary to pass, so
25498
+ * requiring one would force a caller to invent one and report the whole store
25499
+ * as queued.
25500
+ */
25501
+ /**
25502
+ * The same partition, one row per kind that a lane carries.
25503
+ *
25504
+ * A kind with nothing to report is ABSENT rather than a row of zeros: the
25505
+ * scope decides which rows exist at all, so a kind that has never been
25506
+ * recorded — or whose captures nobody ever owed — produces no group. A caller
25507
+ * rendering a fixed list of kinds must therefore treat a missing one as "no
25508
+ * rows", never as "zero sent"; the two look identical in a bar and mean
25509
+ * different things.
25510
+ */
25511
+ partitionByKind() {
25512
+ return allRows(
25513
+ this.partitionByKindStmt,
25514
+ {}
25515
+ ).map((row) => ({
25516
+ kind: row.kind,
25517
+ queued: row.queued ?? 0,
25518
+ inProgress: row.inProgress ?? 0,
25519
+ synced: row.synced ?? 0,
25520
+ failed: row.failed ?? 0,
25521
+ refused: row.refused ?? 0,
25522
+ detached: row.detached ?? 0,
25523
+ total: row.total ?? 0
25524
+ }));
25525
+ }
25526
+ partition() {
25527
+ const row = getRow(this.partitionStmt, {});
25528
+ return {
25529
+ queued: row?.queued ?? 0,
25530
+ inProgress: row?.inProgress ?? 0,
25531
+ synced: row?.synced ?? 0,
25532
+ failed: row?.failed ?? 0,
25533
+ refused: row?.refused ?? 0,
25534
+ detached: row?.detached ?? 0,
25535
+ total: row?.total ?? 0
25536
+ };
25537
+ }
25538
+ /** `pending` counts only what is inside the backlog; sent and skipped are totals. */
25539
+ counts(before) {
25540
+ const row = getRow(this.countsStmt, { before });
25541
+ const captures = getRow(this.captureSkipCountStmt);
25542
+ return {
25543
+ pending: row?.pending ?? 0,
25544
+ sent: row?.sent ?? 0,
25545
+ skipped: row?.skipped ?? 0,
25546
+ refused: row?.refused ?? 0,
25547
+ detached: row?.detached ?? 0,
25548
+ capturesSkipped: captures?.skipped ?? 0
25549
+ };
25550
+ }
25551
+ /**
25552
+ * The deployment the current stamps were made against, and where its backlog
25553
+ * ends.
25554
+ *
25555
+ * READ-ONLY. An absent row reads as an absent deployment, which is what a
25556
+ * machine that has never drained is — and every writer below seeds the row
25557
+ * before it needs one, so nothing depends on this creating it. Keeping the
25558
+ * write off the gate path matters because the gate runs on every pass while a
25559
+ * write has to take the database's write lock.
25560
+ */
25561
+ deployment() {
25562
+ const row = getRow(
25563
+ this.fingerprintStmt
25564
+ );
25565
+ return {
25566
+ fingerprint: row?.fingerprint ?? void 0,
25567
+ backlogBefore: row?.backlogBefore ?? void 0
25568
+ };
25569
+ }
25570
+ /**
25571
+ * Point the ledger at a different deployment, discarding what it recorded
25572
+ * about the previous one.
25573
+ *
25574
+ * Delivery is a fact about ONE recipient: rows sent to the deployment a
25575
+ * machine has just left are undelivered as far as the new one is concerned.
25576
+ * All four in one transaction, so a crash between them cannot leave stamps
25577
+ * attributed to the wrong deployment, a boundary that belongs to another, or
25578
+ * a disown with no re-mark to follow it.
25579
+ *
25580
+ * The boundary is written HERE and only here, which is what freezes it: a
25581
+ * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
25582
+ * unchanged, so this never runs and the backlog does not widen back over rows
25583
+ * the live path has since delivered.
25584
+ *
25585
+ * `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
25586
+ * granted existing-history consent for the deployment this call is arming —
25587
+ * a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
25588
+ * instant, `backlogBefore` is the ATTACH instant, and the two can be far
25589
+ * apart. Passed only when that grant is valid, since this method has no way
25590
+ * to check consent itself and must not mark a row owed for a machine that
25591
+ * never agreed to it. Applied AFTER the disown above, in the SAME
25592
+ * transaction: what the disown clears is every marker below `backlogBefore`,
25593
+ * which includes this deployment's OWN pre-attach rows — `aka attach` calls
25594
+ * `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
25595
+ * on the cleared side of that bound — and the re-mark in the same
25596
+ * transaction is what puts those rows back. A crash between the two cannot
25597
+ * strand the ledger disowned with nothing re-marked — the transaction either
25598
+ * lands whole or not at all, and a fingerprint mismatch that has not yet
25599
+ * committed re-enters this method on the very next pass. Omit it (the
25600
+ * structural-only tests do) to exercise the disown in isolation.
25601
+ *
25602
+ * The disown is bounded by `backlogBefore`, which is what keeps it from
25603
+ * touching a marker the NEW deployment's OWN live path has already set: B's
25604
+ * live path can mark a capture owed from the moment `aka attach` writes the
25605
+ * descriptor, before the drain's first pass ever reaches this method, and
25606
+ * such a row sits at or after the bound rather than below it. What keeps the
25607
+ * disown from eating THIS SAME CALL's own re-mark is the order, not the
25608
+ * bound — disown runs first, re-mark second, both inside the one
25609
+ * transaction above.
25610
+ */
25611
+ rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
25612
+ this.ensureRowStmt.run();
25613
+ withTransaction(
25614
+ this.db,
25615
+ () => {
25616
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
25617
+ this.rearmStmt.run();
25618
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
25619
+ this.disownCapturesStmt.run({ attachedAt: backlogBefore });
25620
+ }
25621
+ if (backfillCapturesBefore !== void 0) {
25622
+ this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
25623
+ }
25624
+ this.setFingerprintStmt.run({ fingerprint, backlogBefore });
25625
+ },
25626
+ "IMMEDIATE"
25627
+ );
25628
+ }
25629
+ /**
25630
+ * End the attached period: hand its rows to the live path, and release the
25631
+ * boundary so the next attachment can freeze a new one.
25632
+ *
25633
+ * WITHOUT THIS, a detach and a re-attach to the same deployment leave a window
25634
+ * nothing delivers. The fingerprint is unchanged, so the boundary is never
25635
+ * re-frozen and stays at the FIRST attachment — while nothing forwards at all
25636
+ * during the detached period, because the machine is not attached. Rows
25637
+ * recorded in that window sit after the boundary and before the re-attach, so
25638
+ * neither path takes them, and the pending count reports none outstanding.
25639
+ *
25640
+ * WHAT IT RECORDS, and what it deliberately does not. These rows were the
25641
+ * closing attachment's to deliver and are no longer outstanding — that is what
25642
+ * lets the boundary move. It is NOT a claim that any of them arrived, and the
25643
+ * distinction is not academic: this used to write a delivery TIME, which every
25644
+ * read treats as delivery, so one detach turned a window of undelivered rows
25645
+ * into a window of delivered ones and no surface could tell. It writes the
25646
+ * skip sentinel and a reason of its own instead, so "no longer owed" and
25647
+ * "received" stop being the same fact.
25648
+ *
25649
+ * A change of deployment still frees them (see the re-arm), because the next
25650
+ * deployment has seen none of this machine's history — so the rows reach it
25651
+ * exactly as they did when this wrote a delivery time.
25652
+ *
25653
+ * ONE TRANSACTION, so a crash cannot release the boundary while leaving the
25654
+ * window unstamped — that half-state would re-send the whole attached period
25655
+ * on the next attach, which is the failure the boundary exists to prevent.
25656
+ */
25657
+ closeAttachedWindow(attachedAtMs, atMs) {
25658
+ this.ensureRowStmt.run();
25659
+ withTransaction(
25660
+ this.db,
25661
+ () => {
25662
+ const row = getRow(this.fingerprintStmt);
25663
+ const from = row?.backlogBefore ?? attachedAtMs;
25664
+ this.closeWindowStmt.run({ at: atMs, attachedAt: from });
25665
+ this.releaseBoundaryStmt.run();
25666
+ },
25667
+ "IMMEDIATE"
25668
+ );
25669
+ }
25670
+ /**
25671
+ * Freeze a boundary for the deployment already on file, KEEPING the stamps.
25672
+ *
25673
+ * The re-attach half of the above. Distinct from `rearmFor`, which is for a
25674
+ * different deployment and therefore discards what was delivered to the old
25675
+ * one: here the recipient is the same, so everything already sent to it stays
25676
+ * sent.
25677
+ */
25678
+ freezeBoundary(backlogBefore) {
25679
+ this.ensureRowStmt.run();
25680
+ this.freezeBoundaryStmt.run({ backlogBefore });
25681
+ }
25682
+ /** Take the claim, or report that someone live already holds it. */
25683
+ claim(pid, host, nowMs, staleAfterMs) {
25684
+ this.ensureRowStmt.run();
25685
+ let taken = false;
25686
+ withTransaction(
25687
+ this.db,
25688
+ () => {
25689
+ const result = this.claimStmt.run({
25690
+ pid,
25691
+ host,
25692
+ now: nowMs,
25693
+ staleBefore: nowMs - staleAfterMs
25694
+ });
25695
+ taken = result.changes === 1;
25696
+ },
25697
+ "IMMEDIATE"
25698
+ );
25699
+ return taken;
25700
+ }
25701
+ /** Say the holder is still alive. A no-op once the claim has moved on. */
25702
+ heartbeat(pid, nowMs) {
25703
+ this.heartbeatStmt.run({ now: nowMs, pid });
25704
+ }
25705
+ /** Give the claim up. Scoped to this holder, so a taken-over claim is left be. */
25706
+ release(pid) {
25707
+ this.releaseStmt.run({ pid });
25708
+ }
25709
+ /** Who holds the claim, if anyone. Read-only, for the same reason as above. */
25710
+ lease() {
25711
+ return getRow(this.leaseStmt);
25712
+ }
25713
+ };
25714
+
24701
25715
  // ../../packages/persistence/src/migrations.ts
24702
25716
  function describeObject(object2) {
24703
25717
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
@@ -24710,7 +25724,7 @@ function createdIndexName(statement) {
24710
25724
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
24711
25725
  }
24712
25726
  var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
24713
- function applyMigrations(db, file2) {
25727
+ function applyMigrations(db, file2, options = {}) {
24714
25728
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
24715
25729
  db.exec(
24716
25730
  "CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
@@ -24724,6 +25738,7 @@ function applyMigrations(db, file2) {
24724
25738
  );
24725
25739
  for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
24726
25740
  if (applied.has(migration.tag)) continue;
25741
+ if (options.skipTags?.has(migration.tag) === true) continue;
24727
25742
  if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
24728
25743
  const evidence = evidenceObjects(migration.sql);
24729
25744
  const present = evidence.filter((o) => evidenceExists(db, o));
@@ -25130,10 +26145,62 @@ function ensureSyncedAtColumn(db, table) {
25130
26145
  if (!columns.includes("outbox_owed")) {
25131
26146
  db.exec(`ALTER TABLE ${table} ADD COLUMN outbox_owed integer`);
25132
26147
  }
26148
+ if (!columns.includes("sync_failed_at")) {
26149
+ db.exec(`ALTER TABLE ${table} ADD COLUMN sync_failed_at integer`);
26150
+ }
26151
+ if (!columns.includes("sync_failure")) {
26152
+ withTransaction(
26153
+ db,
26154
+ () => {
26155
+ db.exec(`ALTER TABLE ${table} ADD COLUMN sync_failure text`);
26156
+ db.exec(
26157
+ `UPDATE ${table} SET synced_at = NULL
26158
+ WHERE synced_at = -1
26159
+ AND event_type IN (${COUNTED_EVENT_TYPES.map((t) => `'${t}'`).join(", ")})`
26160
+ );
26161
+ },
26162
+ "IMMEDIATE"
26163
+ );
26164
+ }
25133
26165
  db.exec(
25134
- `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
25135
- ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
26166
+ `CREATE TRIGGER IF NOT EXISTS aka_sync_failure_guard
26167
+ BEFORE UPDATE OF sync_failure ON ${table}
26168
+ WHEN ${syncFailureRejectCondition()}
26169
+ BEGIN SELECT RAISE(ABORT, 'sync_failure is not one of the recorded reasons'); END`
25136
26170
  );
26171
+ const syncIndexColumns = [
26172
+ "event_type",
26173
+ "synced_at",
26174
+ "sync_claimed_at",
26175
+ "started_at",
26176
+ // Appended LAST on purpose. The delivery-state read now projects it, so it
26177
+ // has to be in the index for the read to stay covered — but putting it
26178
+ // ahead of `started_at` would reorder the prefix the structural drain's
26179
+ // reads match on.
26180
+ "sync_failure"
26181
+ // `outbox_owed` is DELIBERATELY ABSENT, and it was measured both ways.
26182
+ //
26183
+ // The delivery-state read tests it — a capture's state depends on whether a
26184
+ // live forward marked it owed — so carrying it here makes that read covering
26185
+ // rather than a row fetch per row: 16 ms against 40 ms on a real 6 GB store.
26186
+ // But a sixth column changes what the planner charges for this index, and
26187
+ // with no ANALYZE statistics it plans from schema shape alone: measured, it
26188
+ // then stops choosing the per-session index for the token rollup and walks
26189
+ // every `llm_call` in the store through the event-type index instead. That
26190
+ // read grows with the store; this one does not.
26191
+ //
26192
+ // 40 ms on the largest store measured, once per render, is a cost worth
26193
+ // paying to leave every other read's plan where it was.
26194
+ ];
26195
+ const currentSyncIndex = indexColumns(db, "idx_audit_events_sync");
26196
+ const syncIndexMatches = currentSyncIndex.length === syncIndexColumns.length && currentSyncIndex.every((column, i) => column === syncIndexColumns[i]);
26197
+ if (!syncIndexMatches) {
26198
+ db.exec("DROP INDEX IF EXISTS idx_audit_events_sync");
26199
+ db.exec(
26200
+ `CREATE INDEX idx_audit_events_sync
26201
+ ON audit_events (${syncIndexColumns.join(", ")})`
26202
+ );
26203
+ }
25137
26204
  db.exec(
25138
26205
  `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25139
26206
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
@@ -25355,7 +26422,11 @@ function buildAuditEvent(row) {
25355
26422
  link: linkParsed?.success ? linkParsed.data : null,
25356
26423
  targetId: row.target_id,
25357
26424
  internal: intToBool(row.internal),
25358
- flagged: intToBool(row.flagged)
26425
+ flagged: intToBool(row.flagged),
26426
+ // Only meaningful when the title came out empty — a row whose body was
26427
+ // expired but whose title fell back to `tool_name` still has something to
26428
+ // render, and flagging it would make the view apologise for nothing.
26429
+ bodyExpired: row.content_expired_at !== null && (row.title ?? "") === ""
25359
26430
  };
25360
26431
  }
25361
26432
  var TIMELINE_COLUMNS = `
@@ -25363,6 +26434,7 @@ var TIMELINE_COLUMNS = `
25363
26434
  event_type,
25364
26435
  started_at,
25365
26436
  coalesce(content, json_extract(attributes, '$.tool_name')) AS title,
26437
+ content_expired_at,
25366
26438
  coalesce(json_extract(attributes, '$.detail'), json_extract(attributes, '$.target')) AS detail,
25367
26439
  coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25368
26440
  json_extract(attributes, '$.severity') AS severity,
@@ -25489,7 +26561,8 @@ var SqliteActivityRepository = class {
25489
26561
  SELECT 1 FROM audit_events d
25490
26562
  WHERE d.root_session_id = audit_events.id
25491
26563
  AND (d.content LIKE ? ESCAPE '\\'
25492
- OR json_extract(d.attributes, '$.detail') LIKE ? ESCAPE '\\')))`
26564
+ OR coalesce(json_extract(d.attributes, '$.detail'),
26565
+ json_extract(d.attributes, '$.target')) LIKE ? ESCAPE '\\')))`
25493
26566
  );
25494
26567
  params.push(pattern, pattern, pattern, pattern, pattern, pattern);
25495
26568
  }
@@ -26027,6 +27100,88 @@ var SqliteAuditEventsRepository = class {
26027
27100
  }
26028
27101
  };
26029
27102
 
27103
+ // ../../packages/persistence/src/repositories/body-retention.ts
27104
+ var DEFAULT_BATCH_SIZE = 500;
27105
+ var DEFAULT_MAX_ROWS = 5e4;
27106
+ var SYNC_LANE_TYPES_SQL = OUTBOX_CAPTURE_TYPE_LIST;
27107
+ var SqliteBodyRetentionRepository = class {
27108
+ constructor(db) {
27109
+ this.db = db;
27110
+ const select = (laneClause) => `
27111
+ SELECT id, LENGTH(CAST(content AS BLOB)) AS bytes
27112
+ FROM audit_events
27113
+ WHERE content IS NOT NULL
27114
+ AND started_at < :cutoff
27115
+ AND event_type IN (${CAPTURE_EVENT_TYPES_SQL})
27116
+ ${laneClause}
27117
+ ORDER BY started_at
27118
+ LIMIT :limit`;
27119
+ this.candidatesStmt = this.db.prepare(select(""));
27120
+ this.candidatesSyncSafeStmt = this.db.prepare(
27121
+ select(`AND (event_type NOT IN (${SYNC_LANE_TYPES_SQL}) OR synced_at IS NOT NULL)`)
27122
+ );
27123
+ this.heldBySyncStmt = this.db.prepare(`
27124
+ SELECT COUNT(*) AS n
27125
+ FROM audit_events
27126
+ WHERE content IS NOT NULL
27127
+ AND started_at < :cutoff
27128
+ AND event_type IN (${SYNC_LANE_TYPES_SQL})
27129
+ AND synced_at IS NULL`);
27130
+ this.expireStmt = this.db.prepare(
27131
+ `UPDATE audit_events SET content = NULL, content_expired_at = :now WHERE id = :id`
27132
+ );
27133
+ }
27134
+ db;
27135
+ candidatesStmt;
27136
+ candidatesSyncSafeStmt;
27137
+ heldBySyncStmt;
27138
+ expireStmt;
27139
+ /** How many bytes a pass with these options would free, changing nothing. */
27140
+ preview(opts) {
27141
+ const limit = opts.maxRows ?? DEFAULT_MAX_ROWS;
27142
+ const stmt = opts.sweepSyncLane ? this.candidatesStmt : this.candidatesSyncSafeStmt;
27143
+ const rows = stmt.all({ cutoff: opts.cutoff, limit });
27144
+ return {
27145
+ rowsExpired: rows.length,
27146
+ bytesFreed: rows.reduce((sum, r) => sum + r.bytes, 0),
27147
+ rowsHeldBySync: this.countHeldBySync(opts)
27148
+ };
27149
+ }
27150
+ /** Clear eligible bodies, in bounded batches. */
27151
+ expire(opts) {
27152
+ const batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
27153
+ const maxRows = opts.maxRows ?? DEFAULT_MAX_ROWS;
27154
+ const stmt = opts.sweepSyncLane ? this.candidatesStmt : this.candidatesSyncSafeStmt;
27155
+ let rowsExpired = 0;
27156
+ let bytesFreed = 0;
27157
+ let done = true;
27158
+ while (rowsExpired < maxRows) {
27159
+ const remaining = Math.min(batchSize, maxRows - rowsExpired);
27160
+ const batch = stmt.all({ cutoff: opts.cutoff, limit: remaining });
27161
+ if (batch.length === 0) break;
27162
+ withTransaction(
27163
+ this.db,
27164
+ () => {
27165
+ for (const row of batch) this.expireStmt.run({ id: row.id, now: opts.now });
27166
+ },
27167
+ "IMMEDIATE"
27168
+ );
27169
+ rowsExpired += batch.length;
27170
+ bytesFreed += batch.reduce((sum, r) => sum + r.bytes, 0);
27171
+ if (batch.length < remaining) break;
27172
+ if (rowsExpired >= maxRows) {
27173
+ done = stmt.all({ cutoff: opts.cutoff, limit: 1 }).length === 0;
27174
+ }
27175
+ }
27176
+ return { rowsExpired, bytesFreed, rowsHeldBySync: this.countHeldBySync(opts), done };
27177
+ }
27178
+ countHeldBySync(opts) {
27179
+ if (opts.sweepSyncLane) return 0;
27180
+ const row = this.heldBySyncStmt.get({ cutoff: opts.cutoff });
27181
+ return row.n;
27182
+ }
27183
+ };
27184
+
26030
27185
  // ../../packages/persistence/src/repositories/classified-data.ts
26031
27186
  var SqliteClassifiedDataRepository = class {
26032
27187
  constructor(db) {
@@ -26827,23 +27982,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26827
27982
  )`;
26828
27983
 
26829
27984
  // ../../packages/persistence/src/repositories/findings.ts
26830
- var PREVIEW_INSTANCES_PER_GROUP = 200;
26831
- var DEFAULT_LOCATIONS_LIMIT = 100;
26832
- var LOCATION_RULE_IDS_CAP = 20;
26833
- function compareLocationOrder(a, b) {
26834
- return compareFindingGroupOrder(
26835
- {
26836
- severity: a.maxSeverity,
26837
- latestDetectedAt: a.latestDetectedAt,
26838
- id: ""
26839
- },
26840
- {
26841
- severity: b.maxSeverity,
26842
- latestDetectedAt: b.latestDetectedAt,
26843
- id: ""
26844
- }
26845
- );
26846
- }
26847
27985
  var CONCAT_SEP = ",";
26848
27986
  var TUPLE_SEP = "|";
26849
27987
  function splitConcat(value) {
@@ -26872,7 +28010,15 @@ function toFlatFindingRow(r) {
26872
28010
  ...r.tool_name === null ? {} : { toolName: r.tool_name },
26873
28011
  eventId: r.event_id,
26874
28012
  ...r.session_id === null ? {} : { sessionId: r.session_id },
26875
- status: deriveInstanceStatus(r)
28013
+ status: deriveInstanceStatus(r),
28014
+ delivery: deriveFindingDelivery({
28015
+ kind: r.kind,
28016
+ syncedAt: r.synced_at,
28017
+ syncClaimedAt: r.sync_claimed_at,
28018
+ syncFailedAt: r.sync_failed_at,
28019
+ syncFailure: r.sync_failure,
28020
+ outboxOwed: r.outbox_owed
28021
+ })
26876
28022
  };
26877
28023
  }
26878
28024
  function encodeGroupCursor(group) {
@@ -26895,13 +28041,51 @@ function decodeGroupCursor(cursor) {
26895
28041
  return null;
26896
28042
  }
26897
28043
  function firstAfter(sorted, cursor) {
26898
- const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
28044
+ const index = sorted.findIndex((t) => compareFindingGroupOrder(t, cursor) > 0);
26899
28045
  return index === -1 ? sorted.length : index;
26900
28046
  }
26901
28047
  function findDeepLinked(sorted, page, id) {
26902
- if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
26903
- return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
28048
+ if (page.some((t) => t.id === id)) return void 0;
28049
+ return sorted.find((t) => t.id === id);
28050
+ }
28051
+ function encodeLocationCursor(location) {
28052
+ const payload = {
28053
+ sev: location.maxSeverity,
28054
+ t: location.latestDetectedAt,
28055
+ r: location.repo,
28056
+ f: location.file
28057
+ };
28058
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
28059
+ }
28060
+ function decodeLocationCursor(cursor) {
28061
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
28062
+ if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.r === "string" && typeof parsed.f === "string") {
28063
+ return { maxSeverity: parsed.sev, latestDetectedAt: parsed.t, repo: parsed.r, file: parsed.f };
28064
+ }
28065
+ return null;
26904
28066
  }
28067
+ function firstLocationAfter(sorted, cursor) {
28068
+ const index = sorted.findIndex((l) => compareLocationOrder(l, cursor) > 0);
28069
+ return index === -1 ? sorted.length : index;
28070
+ }
28071
+ function findDeepLinkedLocation(sorted, page, id) {
28072
+ if (page.some((l) => l.id === id)) return void 0;
28073
+ return sorted.find((l) => l.id === id);
28074
+ }
28075
+ var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS category,
28076
+ d.severity AS severity, f.masked_match AS masked_match,
28077
+ f.action_taken AS action_taken, f.confidence AS confidence,
28078
+ e.started_at AS occurred_at,
28079
+ e.source_tool AS source_tool,
28080
+ e.repo AS repo,
28081
+ e.file_path AS file,
28082
+ e.tool_name AS tool_name,
28083
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
28084
+ e.event_type AS kind, f.finding_key AS finding_key,
28085
+ ${latestResolutionStatusSql("f")} AS latest_status,
28086
+ e.synced_at AS synced_at, e.sync_claimed_at AS sync_claimed_at,
28087
+ e.sync_failed_at AS sync_failed_at, e.sync_failure AS sync_failure,
28088
+ e.outbox_owed AS outbox_owed`;
26905
28089
  var DAY_MS3 = 864e5;
26906
28090
  var SqliteFindingsRepository = class {
26907
28091
  constructor(db) {
@@ -27022,30 +28206,26 @@ var SqliteFindingsRepository = class {
27022
28206
  );
27023
28207
  }
27024
28208
  /**
27025
- * Grouped findings for the dashboard — joins inspection_findings⋈audit_events
27026
- * ⋈inspection_definitions (repo/file/toolName from the audit event's
27027
- * attributes bag, rule_id/category/severity from the definition), scoped to
27028
- * the four capture kinds (audit_events also holds structural/reconciler/scan
27029
- * rows this list must never surface), groups by ruleId, computes
27030
- * per-filter-excluded facets, applies the requested filters, and sorts by
27031
- * severity then recency. Filtering
27032
- * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
27033
- * reflect the full filtered set; `items` is the requested
27034
- * page (default 50); no cursor (nextCursor is always null). Under a `status`
27035
- * filter, `totals.findings` counts only instances whose derived status was
27036
- * requested, and each item's instance preview is narrowed the same way.
28209
+ * Finding TYPES for the dashboard — one row per rule, scoped to the four
28210
+ * capture kinds (audit_events also holds structural/reconciler/scan rows this
28211
+ * list must never surface), with per-filter-excluded facets, the requested
28212
+ * filters applied, and sorted by severity then recency. Filtering and faceting
28213
+ * run in JS via the shared @akasecurity/schema helpers. `totals` reflect the
28214
+ * full filtered set; `items` is the requested page (default 50), keyset-paged.
28215
+ * Under a `status` filter, `totals.findings` counts only findings whose
28216
+ * derived status was requested.
28217
+ *
28218
+ * ONE read, which materializes no findings: a single aggregate per rule_id,
28219
+ * folding EVERY finding into the numbers a type row and the filters need
28220
+ * (count, severity, category, providers, actions, statuses, latest, search
28221
+ * text). The findings OF a type come from listFindingInstances scoped to
28222
+ * `subtype`, so neither list bounds the other and no per-type cap exists.
27037
28223
  *
27038
- * Two reads, neither of which materializes a row per finding:
27039
- * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
27040
- * the group and the filters need (count, providers, actions, statuses,
27041
- * latest, search text);
27042
- * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
27043
- * populate `instances` for the table's expanded rows.
27044
28224
  * The aggregates carry raw DB values and are translated by the same
27045
- * @akasecurity/schema mappers the row path uses, so no enum mapping or status
27046
- * rule is ever restated in SQL.
28225
+ * @akasecurity/schema mappers every other path uses, so no enum mapping or
28226
+ * status rule is ever restated in SQL.
27047
28227
  */
27048
- listGroupedFindings(query) {
28228
+ listFindingTypes(query) {
27049
28229
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
27050
28230
  const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
27051
28231
  const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
@@ -27058,12 +28238,7 @@ var SqliteFindingsRepository = class {
27058
28238
  predicate,
27059
28239
  params: sessionParams
27060
28240
  });
27061
- const rows = this.previewRows(aggregates, {
27062
- sessionId: query.sessionId,
27063
- from: query.from
27064
- });
27065
- const groupable = rows.map(toFlatFindingRow);
27066
- const allGroups = buildFindingGroups(groupable, { aggregates });
28241
+ const allTypes = buildFindingTypes(aggregates);
27067
28242
  const filterOpts = {
27068
28243
  severity: query.severity,
27069
28244
  providers: query.provider,
@@ -27072,30 +28247,25 @@ var SqliteFindingsRepository = class {
27072
28247
  subtype: query.subtype,
27073
28248
  q: query.q
27074
28249
  };
27075
- const facets = computeFindingFacets(allGroups, filterOpts);
27076
- const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
28250
+ const facets = computeFindingFacets(allTypes, filterOpts);
28251
+ const sorted = sortFindingTypes(applyFindingFilters(allTypes, filterOpts));
27077
28252
  const statusFilter = query.status ?? [];
27078
28253
  const totals = {
27079
- findings: sorted.reduce((acc, g) => {
27080
- if (statusFilter.length === 0) return acc + g.instanceCount;
27081
- const agg = aggregates.get(g.id);
27082
- return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
28254
+ findings: sorted.reduce((acc, t) => {
28255
+ if (statusFilter.length === 0) return acc + t.instanceCount;
28256
+ const agg = aggregates.get(t.id);
28257
+ return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? t.instanceCount : t.instanceCount);
27083
28258
  }, 0),
27084
- groups: sorted.length
28259
+ types: sorted.length
27085
28260
  };
27086
- const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
28261
+ const limit = query.limit ?? DEFAULT_FINDING_TYPES_LIMIT;
27087
28262
  const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
27088
28263
  const start = cursor === null ? 0 : firstAfter(sorted, cursor);
27089
28264
  const page = sorted.slice(start, start + limit);
27090
28265
  const lastOnPage = page.at(-1);
27091
28266
  const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
27092
28267
  const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
27093
- const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
27094
- const narrow = (g) => statusSet ? {
27095
- ...g,
27096
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
27097
- } : g;
27098
- const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
28268
+ const items = [...page, ...deepLinked ? [deepLinked] : []];
27099
28269
  return Promise.resolve({
27100
28270
  totals,
27101
28271
  facets,
@@ -27106,7 +28276,7 @@ var SqliteFindingsRepository = class {
27106
28276
  }
27107
28277
  /**
27108
28278
  * One row per rule_id, folding EVERY instance of the group into the values
27109
- * buildFindingGroups cannot recover from a preview. Bounded by the number of
28279
+ * buildFindingTypes cannot recover from an aggregate. Bounded by the number of
27110
28280
  * distinct rule_ids (the installed packs' rules), not by the store's size.
27111
28281
  *
27112
28282
  * A single scan, folded in two levels: the inner SELECT groups by
@@ -27160,6 +28330,7 @@ var SqliteFindingsRepository = class {
27160
28330
  providers: query.provider,
27161
28331
  actions: query.action,
27162
28332
  statuses: query.status,
28333
+ deliveries: query.deployment,
27163
28334
  tools: query.tool,
27164
28335
  repo: query.repo,
27165
28336
  file: query.file,
@@ -27200,13 +28371,25 @@ var SqliteFindingsRepository = class {
27200
28371
  });
27201
28372
  }
27202
28373
  /**
27203
- * The same findings folded by location: repository, then file within it.
28374
+ * The same findings folded by WHERE they live — one row per (repo, file) pair.
27204
28375
  *
27205
28376
  * The grouping keys come from the capturing event's attributes, which is what
27206
- * the local store relates a finding to — there is no finding↔asset row to
27207
- * group by instead. A repo or file the event did not record folds into the
27208
- * empty-string bucket, which the view renders but does not link, since no
27209
- * filter can name it.
28377
+ * the local store relates a finding to; there is no finding↔asset row to group
28378
+ * by instead. A repo or file the event did not record folds into the
28379
+ * empty-string bucket, which is a real location like any other: it is listed,
28380
+ * it is selectable, and its `?loc=` token is as good as any other row's.
28381
+ *
28382
+ * ONE flat list rather than repos nesting files. A rollup can only be paged by
28383
+ * repo, which leaves the file list inside it unbounded — the shape the by-type
28384
+ * list was rebuilt to remove — and two-level pagination inside an
28385
+ * expand/collapse table is what pushed that view to master/detail in the first
28386
+ * place.
28387
+ *
28388
+ * Every filter narrows the FINDINGS and the locations fall out of what
28389
+ * survives, so each row's `instanceCount` is exactly what listFindingInstances
28390
+ * reports for the same filters scoped to that pair. The view depends on it:
28391
+ * one toolbar sits over both panels precisely because a location owns none of
28392
+ * its fields.
27210
28393
  */
27211
28394
  listFindingLocations(query) {
27212
28395
  const opts = {
@@ -27215,16 +28398,20 @@ var SqliteFindingsRepository = class {
27215
28398
  providers: query.provider,
27216
28399
  actions: query.action,
27217
28400
  statuses: query.status,
28401
+ deliveries: query.deployment,
27218
28402
  tools: query.tool,
27219
28403
  q: query.q
27220
28404
  };
27221
- const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
28405
+ const limit = query.limit ?? DEFAULT_FINDING_LOCATIONS_LIMIT;
28406
+ const cursor = query.cursor === void 0 ? null : decodeLocationCursor(query.cursor);
27222
28407
  const byRepo = /* @__PURE__ */ new Map();
28408
+ const accumulator = createInstanceFacetAccumulator(opts);
27223
28409
  let total = 0;
27224
28410
  for (const row of this.scanFindingRows({
27225
28411
  sessionId: query.sessionId,
27226
28412
  from: query.from
27227
28413
  })) {
28414
+ accumulator.add(row);
27228
28415
  if (!matchesInstanceFilters(row, opts)) continue;
27229
28416
  total += 1;
27230
28417
  let files = byRepo.get(row.repo);
@@ -27239,103 +28426,35 @@ var SqliteFindingsRepository = class {
27239
28426
  }
27240
28427
  addToLocation(acc, row);
27241
28428
  }
27242
- let fileCount = 0;
27243
- const repos = [...byRepo.entries()].map(([repo, files]) => {
27244
- fileCount += files.size;
27245
- const fileRows = [...files.entries()].map(([file2, acc]) => ({
27246
- file: file2,
27247
- instanceCount: acc.instanceCount,
27248
- maxSeverity: acc.maxSeverity,
27249
- latestDetectedAt: acc.latestDetectedAt,
27250
- ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
27251
- ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
27252
- })).sort(compareLocationOrder);
27253
- const rollup = fileRows.reduce(
27254
- (a, f) => ({
27255
- instanceCount: a.instanceCount + f.instanceCount,
27256
- maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
27257
- latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
27258
- }),
27259
- {
27260
- instanceCount: 0,
27261
- maxSeverity: fileRows[0]?.maxSeverity ?? "low",
27262
- latestDetectedAt: ""
27263
- }
27264
- );
27265
- const statuses = fileRows.map((f) => f.status);
27266
- const folded = foldGroupStatus(statuses);
27267
- return {
27268
- repo,
27269
- instanceCount: rollup.instanceCount,
27270
- maxSeverity: rollup.maxSeverity,
27271
- latestDetectedAt: rollup.latestDetectedAt,
27272
- ...folded === void 0 ? {} : { status: folded },
27273
- files: fileRows
27274
- };
27275
- });
27276
- repos.sort(compareLocationOrder);
28429
+ const sorted = [];
28430
+ for (const [repo, files] of byRepo) {
28431
+ for (const [file2, acc] of files) {
28432
+ const status = foldGroupStatus(acc.statuses);
28433
+ sorted.push({
28434
+ id: encodeLocationId(repo, file2),
28435
+ repo,
28436
+ file: file2,
28437
+ instanceCount: acc.instanceCount,
28438
+ maxSeverity: acc.maxSeverity,
28439
+ latestDetectedAt: acc.latestDetectedAt,
28440
+ ...status === void 0 ? {} : { status },
28441
+ ruleIds: [...acc.ruleIds]
28442
+ });
28443
+ }
28444
+ }
28445
+ sorted.sort(compareLocationOrder);
28446
+ const start = cursor === null ? 0 : firstLocationAfter(sorted, cursor);
28447
+ const page = sorted.slice(start, start + limit);
28448
+ const lastOnPage = page.at(-1);
28449
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeLocationCursor(lastOnPage) : null;
28450
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinkedLocation(sorted, page, query.includeId);
27277
28451
  return Promise.resolve({
27278
- totals: { findings: total, repos: repos.length, files: fileCount },
27279
- items: repos.slice(0, limit),
27280
- hasMore: repos.length > limit
28452
+ totals: { findings: total, locations: sorted.length },
28453
+ facets: accumulator.facets(),
28454
+ items: [...page, ...deepLinked ? [deepLinked] : []],
28455
+ nextCursor
27281
28456
  });
27282
28457
  }
27283
- /**
27284
- * Each group's newest instances, for the table's expanded rows.
27285
- *
27286
- * ONE index-ordered scan with early termination, and the shape is the point.
27287
- * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
27288
- * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
27289
- * through a temp B-tree to keep a bounded preview of each group, and then
27290
- * sorts the survivors again for the page order. Both sorts grow with the
27291
- * store while the answer does not.
27292
- *
27293
- * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
27294
- * (or the session or window index the scope names — see `findingScanSql`),
27295
- * which is already the order the page wants, and keeps rows per rule until
27296
- * each rule has as many as it can show. The aggregate the caller already holds
27297
- * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
27298
- * per rule, summed, is the number of rows this scan has to find, and it stops
27299
- * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
27300
- * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
27301
- * store with many firing rules widens it. The bound that DOES hold
27302
- * unconditionally is the sorted form's floor: this scan visits at most as
27303
- * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
27304
- * sorted, and stops the moment every rule has its cap, where the sorted form
27305
- * sorts the whole scope regardless. The true worst case — the rarest rule's
27306
- * wanted instances sitting at the tail of the scope — is one pass over
27307
- * everything in scope with a block sort of the id tie-break only, never a
27308
- * sort of the scope, which is still that floor.
27309
- *
27310
- * A row whose rule the aggregate did not see is skipped: the two statements
27311
- * run without a shared snapshot, so a capture landing between them can add a
27312
- * rule here that has no counts there, and the counts are what the group is
27313
- * built from.
27314
- */
27315
- previewRows(aggregates, scope) {
27316
- const wanted = /* @__PURE__ */ new Map();
27317
- let remaining = 0;
27318
- for (const [ruleId, agg] of aggregates) {
27319
- const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
27320
- wanted.set(ruleId, n);
27321
- remaining += n;
27322
- }
27323
- const rows = [];
27324
- if (remaining === 0) return rows;
27325
- const { sql, params } = this.findingScanSql(scope);
27326
- const taken = /* @__PURE__ */ new Map();
27327
- for (const r of iterateRows(this.db.prepare(sql), params)) {
27328
- const want = wanted.get(r.rule_id);
27329
- if (want === void 0) continue;
27330
- const have = taken.get(r.rule_id) ?? 0;
27331
- if (have >= want) continue;
27332
- taken.set(r.rule_id, have + 1);
27333
- rows.push(r);
27334
- remaining -= 1;
27335
- if (remaining === 0) break;
27336
- }
27337
- return rows;
27338
- }
27339
28458
  /**
27340
28459
  * Every finding in scope as a FlatFindingRow, newest first, streamed.
27341
28460
  *
@@ -27362,6 +28481,33 @@ var SqliteFindingsRepository = class {
27362
28481
  yield toFlatFindingRow(r);
27363
28482
  }
27364
28483
  }
28484
+ /**
28485
+ * One finding by its own id, or null when no such row exists.
28486
+ *
28487
+ * A primary-key seek on `inspection_findings`, so its cost does not grow with
28488
+ * the store — and, unlike anything derived from a list page, it resolves a
28489
+ * finding of ANY age. That is what the Findings page's one-shot `?finding=`
28490
+ * deep link needs: the id it carries may name a finding thousands of rows
28491
+ * older than anything a first page holds.
28492
+ *
28493
+ * Deliberately UNFILTERED — no capture-kind, session or time predicate. It
28494
+ * RESOLVES an id; whether that row would survive the list's current filters is
28495
+ * a different question, and hiding the target because a filter excludes it is
28496
+ * worse than showing it.
28497
+ *
28498
+ * `groupId` on the result IS the rule id, so this one read answers both "which
28499
+ * type should the list select?" and "what does the drawer show?".
28500
+ */
28501
+ findingInstance(id) {
28502
+ const row = this.db.prepare(
28503
+ `SELECT ${FINDING_ROW_COLUMNS_SQL}
28504
+ FROM inspection_findings f
28505
+ JOIN audit_events e ON e.id = f.audit_event_id
28506
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
28507
+ WHERE f.id = ?`
28508
+ ).get(id);
28509
+ return Promise.resolve(row === void 0 ? null : toInstanceDetail(toFlatFindingRow(row)));
28510
+ }
27365
28511
  /**
27366
28512
  * The one statement both instance-level scans run: every finding in scope,
27367
28513
  * joined to its event and definition, newest first.
@@ -27395,17 +28541,7 @@ var SqliteFindingsRepository = class {
27395
28541
  conditions.push("e.started_at >= ?");
27396
28542
  params.push(isoToEpochMillis(scope.from));
27397
28543
  }
27398
- const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
27399
- d.severity AS severity, f.masked_match AS masked_match,
27400
- f.action_taken AS action_taken, f.confidence AS confidence,
27401
- e.started_at AS occurred_at,
27402
- e.source_tool AS source_tool,
27403
- e.repo AS repo,
27404
- e.file_path AS file,
27405
- e.tool_name AS tool_name,
27406
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
27407
- e.event_type AS kind, f.finding_key AS finding_key,
27408
- ${latestResolutionStatusSql("f")} AS latest_status
28544
+ const sql = `SELECT ${FINDING_ROW_COLUMNS_SQL}
27409
28545
  FROM audit_events e
27410
28546
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27411
28547
  CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
@@ -27419,6 +28555,26 @@ var SqliteFindingsRepository = class {
27419
28555
  group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27420
28556
  const rows = this.db.prepare(
27421
28557
  `SELECT rule_id,
28558
+ -- BARE columns beside max(latest_at), which is deliberate and
28559
+ -- is SQLite's documented behaviour: with a single min()/max()
28560
+ -- in an aggregate query, every bare column takes its value from
28561
+ -- the row that produced the extremum. So these are the severity
28562
+ -- and category of the definition whose finding is NEWEST, which
28563
+ -- is what the row-based build they replaced read off its first
28564
+ -- (newest-first) row.
28565
+ --
28566
+ -- min() is WRONG here and was the defect: inspection_definitions
28567
+ -- holds one row per rule VERSION (see its writer \u2014 a version bump
28568
+ -- mints a new row), so a rule whose severity moved between
28569
+ -- versions has several, and min() picks the ALPHABETICALLY
28570
+ -- smallest \u2014 'low' over 'medium', but 'critical' over 'high'.
28571
+ -- That is arbitrary in direction, and it feeds the badge, the
28572
+ -- filter, the facet counts and the primary sort key.
28573
+ --
28574
+ -- Adding a second min()/max() aggregate here would make these
28575
+ -- bare columns ambiguous again; keep max(latest_at) the only one.
28576
+ severity,
28577
+ category,
27422
28578
  sum(tuple_count) AS instance_count,
27423
28579
  max(latest_at) AS latest_at,
27424
28580
  group_concat(source_tools) AS source_tools,
@@ -27429,6 +28585,14 @@ var SqliteFindingsRepository = class {
27429
28585
  group_concat(tool_names) AS tool_names
27430
28586
  FROM (
27431
28587
  SELECT d.rule_id AS rule_id,
28588
+ -- Severity and category are columns of the DEFINITION, and
28589
+ -- a rule can have SEVERAL definitions (one per version), so
28590
+ -- these are grouped on below and resolved to the newest
28591
+ -- firing version by the outer query's bare-column select.
28592
+ -- They ride the aggregate because the type build has no rows
28593
+ -- to read them off \u2014 see buildFindingTypes.
28594
+ d.severity AS severity,
28595
+ d.category AS category,
27432
28596
  e.event_type || '${TUPLE_SEP}' ||
27433
28597
  (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
27434
28598
  coalesce(latest.status, '') AS status_tuple,
@@ -27443,7 +28607,7 @@ var SqliteFindingsRepository = class {
27443
28607
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
27444
28608
  ON latest.finding_key = f.finding_key
27445
28609
  ${scope.predicate}
27446
- GROUP BY d.rule_id, status_tuple
28610
+ GROUP BY d.rule_id, d.severity, d.category, status_tuple
27447
28611
  )
27448
28612
  GROUP BY rule_id`
27449
28613
  ).all(scope.params);
@@ -27452,6 +28616,8 @@ var SqliteFindingsRepository = class {
27452
28616
  r.rule_id,
27453
28617
  {
27454
28618
  instanceCount: r.instance_count,
28619
+ severity: r.severity,
28620
+ category: r.category,
27455
28621
  sourceTools: splitConcat(r.source_tools),
27456
28622
  actionsTaken: splitConcat(r.actions_taken),
27457
28623
  statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
@@ -27468,7 +28634,7 @@ var SqliteFindingsRepository = class {
27468
28634
  latestDetectedAt: epochMillisToIso(r.latest_at),
27469
28635
  // Free text only — joined and substring-matched, so group_concat's
27470
28636
  // commas need no unpicking (a repo/path containing one still matches).
27471
- // Left undefined (not '') when unfetched, so buildFindingGroups can
28637
+ // Left undefined (not '') when unfetched, so buildFindingTypes can
27472
28638
  // tell "no q this request" from "a group with no repo/file at all"
27473
28639
  // and skip priming a haystack nothing will read.
27474
28640
  ...withSearchText ? {
@@ -27496,7 +28662,9 @@ var SqliteFindingsRepository = class {
27496
28662
  )
27497
28663
  );
27498
28664
  for (const row of grouped) {
27499
- if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
28665
+ if (Object.hasOwn(byAction, row.action_taken)) {
28666
+ byAction[row.action_taken] = row.c;
28667
+ }
27500
28668
  }
27501
28669
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
27502
28670
  const sevRows = allRows(
@@ -27513,7 +28681,9 @@ var SqliteFindingsRepository = class {
27513
28681
  )
27514
28682
  );
27515
28683
  for (const row of sevRows) {
27516
- if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
28684
+ if (Object.hasOwn(bySeverity, row.severity)) {
28685
+ bySeverity[row.severity] = row.c;
28686
+ }
27517
28687
  }
27518
28688
  const categories = ENFORCEABLE_CATEGORIES;
27519
28689
  const enabledRows = allRows(
@@ -27562,469 +28732,6 @@ function isoDay(ms) {
27562
28732
  return new Date(ms).toISOString().slice(0, 10);
27563
28733
  }
27564
28734
 
27565
- // ../../packages/persistence/src/repositories/history-sync.ts
27566
- var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27567
- var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27568
- var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27569
- var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27570
- var SKIPPED = -1;
27571
- var ROW_COLUMNS = `id,
27572
- parent_id AS parentId,
27573
- root_session_id AS rootSessionId,
27574
- event_type AS eventType,
27575
- host_id AS hostId,
27576
- harness_id AS harnessId,
27577
- source_project_id AS sourceProjectId,
27578
- started_at AS startedAt,
27579
- ended_at AS endedAt,
27580
- severity,
27581
- priority,
27582
- content,
27583
- content_hash AS contentHash,
27584
- attributes`;
27585
- var SqliteHistorySyncRepository = class {
27586
- constructor(db) {
27587
- this.db = db;
27588
- this.ensureRowStmt = db.prepare(`INSERT OR IGNORE INTO history_sync (id) VALUES (1)`);
27589
- this.sessionsStmt = db.prepare(
27590
- `SELECT COALESCE(root_session_id, id) AS sessionId, MIN(started_at) AS earliest
27591
- FROM audit_events
27592
- WHERE synced_at IS NULL
27593
- AND event_type IN (${TYPE_LIST})
27594
- AND started_at < :before
27595
- GROUP BY sessionId
27596
- ORDER BY earliest
27597
- LIMIT :limit`
27598
- );
27599
- this.rowsStmt = db.prepare(
27600
- `SELECT ${ROW_COLUMNS}
27601
- FROM audit_events
27602
- WHERE synced_at IS NULL
27603
- AND event_type IN (${TYPE_LIST})
27604
- AND started_at < :before
27605
- AND COALESCE(root_session_id, id) = :sessionId
27606
- ORDER BY (event_type = 'session') DESC, started_at
27607
- LIMIT :limit`
27608
- );
27609
- this.captureRowsStmt = db.prepare(
27610
- `SELECT ${ROW_COLUMNS}
27611
- FROM audit_events
27612
- WHERE synced_at IS NULL
27613
- AND sync_claimed_at IS NULL
27614
- AND outbox_owed = 1
27615
- AND event_type IN (${CAPTURE_TYPE_LIST})
27616
- AND started_at < :before
27617
- ORDER BY started_at
27618
- LIMIT :limit`
27619
- );
27620
- this.markOwedStmt = db.prepare(
27621
- `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27622
- );
27623
- this.stampStmt = db.prepare(
27624
- `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27625
- );
27626
- this.claimRowStmt = db.prepare(
27627
- `UPDATE audit_events SET sync_claimed_at = :at WHERE id = :id AND synced_at IS NULL`
27628
- );
27629
- this.releaseRowStmt = db.prepare(
27630
- `UPDATE audit_events SET sync_claimed_at = NULL WHERE id = :id`
27631
- );
27632
- this.releaseStaleClaimsStmt = db.prepare(
27633
- `UPDATE audit_events SET sync_claimed_at = NULL
27634
- WHERE sync_claimed_at IS NOT NULL AND sync_claimed_at < :staleBefore`
27635
- );
27636
- this.partitionStmt = db.prepare(
27637
- `SELECT
27638
- SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NULL THEN 1 ELSE 0 END) AS queued,
27639
- SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NOT NULL THEN 1 ELSE 0 END) AS inProgress,
27640
- SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS synced,
27641
- SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0 THEN 1 ELSE 0 END) AS failed,
27642
- COUNT(*) AS total
27643
- FROM audit_events
27644
- WHERE event_type IN (${TYPE_LIST})`
27645
- );
27646
- this.countsStmt = db.prepare(
27647
- `SELECT
27648
- SUM(CASE WHEN synced_at IS NULL AND started_at < :before THEN 1 ELSE 0 END) AS pending,
27649
- SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS sent,
27650
- SUM(CASE WHEN synced_at = ${String(SKIPPED)} THEN 1 ELSE 0 END) AS skipped
27651
- FROM audit_events
27652
- WHERE event_type IN (${TYPE_LIST})`
27653
- );
27654
- this.captureSkipCountStmt = db.prepare(
27655
- `SELECT COUNT(*) AS skipped
27656
- FROM audit_events
27657
- WHERE synced_at = ${String(SKIPPED)}
27658
- AND event_type IN (${CAPTURE_TYPE_LIST})`
27659
- );
27660
- this.fingerprintStmt = db.prepare(
27661
- `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27662
- FROM history_sync WHERE id = 1`
27663
- );
27664
- this.setFingerprintStmt = db.prepare(
27665
- `UPDATE history_sync
27666
- SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27667
- WHERE id = 1`
27668
- );
27669
- this.disownCapturesStmt = db.prepare(
27670
- `UPDATE audit_events SET outbox_owed = NULL
27671
- WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
27672
- );
27673
- this.rearmStmt = db.prepare(
27674
- `UPDATE audit_events SET synced_at = NULL
27675
- WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
27676
- );
27677
- this.claimStmt = db.prepare(
27678
- `UPDATE history_sync
27679
- SET owner_pid = :pid, owner_host = :host, acquired_at = :now, heartbeat_at = :now
27680
- WHERE id = 1
27681
- AND (owner_pid IS NULL
27682
- OR heartbeat_at IS NULL
27683
- OR heartbeat_at < :staleBefore
27684
- OR heartbeat_at > :now)`
27685
- );
27686
- this.heartbeatStmt = db.prepare(
27687
- `UPDATE history_sync SET heartbeat_at = :now WHERE id = 1 AND owner_pid = :pid`
27688
- );
27689
- this.releaseStmt = db.prepare(
27690
- `UPDATE history_sync
27691
- SET owner_pid = NULL, owner_host = NULL, acquired_at = NULL, heartbeat_at = NULL
27692
- WHERE id = 1 AND owner_pid = :pid`
27693
- );
27694
- this.closeWindowStmt = db.prepare(
27695
- `UPDATE audit_events SET synced_at = :at
27696
- WHERE synced_at IS NULL
27697
- AND event_type IN (${TYPE_LIST})
27698
- AND started_at >= :attachedAt`
27699
- );
27700
- this.releaseBoundaryStmt = db.prepare(
27701
- `UPDATE history_sync SET backlog_before = NULL WHERE id = 1`
27702
- );
27703
- this.freezeBoundaryStmt = db.prepare(
27704
- `UPDATE history_sync SET backlog_before = :backlogBefore WHERE id = 1`
27705
- );
27706
- this.leaseStmt = db.prepare(
27707
- `SELECT owner_pid AS ownerPid, owner_host AS ownerHost,
27708
- acquired_at AS acquiredAt, heartbeat_at AS heartbeatAt
27709
- FROM history_sync WHERE id = 1`
27710
- );
27711
- this.inspectionsStmt = db.prepare(
27712
- `SELECT d.rule_id AS ruleId,
27713
- d.name AS ruleName,
27714
- d.version AS ruleVersion,
27715
- d.category AS category,
27716
- d.severity AS severity,
27717
- f.span_start AS spanStart,
27718
- f.span_end AS spanEnd,
27719
- f.masked_match AS maskedMatch,
27720
- f.action_taken AS actionTaken,
27721
- f.confidence AS confidence
27722
- FROM inspection_findings f
27723
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27724
- WHERE f.audit_event_id = :auditEventId
27725
- ORDER BY f.span_start, f.id`
27726
- );
27727
- }
27728
- db;
27729
- ensureRowStmt;
27730
- sessionsStmt;
27731
- rowsStmt;
27732
- stampStmt;
27733
- countsStmt;
27734
- fingerprintStmt;
27735
- setFingerprintStmt;
27736
- rearmStmt;
27737
- claimStmt;
27738
- heartbeatStmt;
27739
- releaseStmt;
27740
- leaseStmt;
27741
- inspectionsStmt;
27742
- closeWindowStmt;
27743
- releaseBoundaryStmt;
27744
- freezeBoundaryStmt;
27745
- captureRowsStmt;
27746
- markOwedStmt;
27747
- captureSkipCountStmt;
27748
- disownCapturesStmt;
27749
- partitionStmt;
27750
- claimRowStmt;
27751
- releaseRowStmt;
27752
- releaseStaleClaimsStmt;
27753
- /**
27754
- * The masked detections recorded against one tool call.
27755
- *
27756
- * These travel with the event because a tool call's target is not
27757
- * re-inspectable from the event alone — unlike a capture, where the text
27758
- * itself is re-scannable. What crosses is the masked match and the rule that
27759
- * produced it, never the value.
27760
- */
27761
- inspectionsFor(auditEventId) {
27762
- return allRows(this.inspectionsStmt, { auditEventId });
27763
- }
27764
- /**
27765
- * Sessions with structural rows still to send, oldest first.
27766
- *
27767
- * BOUNDED BY THE BACKLOG BOUNDARY, which is the whole correctness of this
27768
- * read. Anything recorded after the machine attached is the live forward
27769
- * path's to deliver; this drain exists for what was recorded before it, and a
27770
- * row both paths send is at best a duplicate request and at worst — for a
27771
- * session root — an overwrite of the inventory ids the live path resolved.
27772
- */
27773
- pendingSessions(limit, before) {
27774
- return allRows(this.sessionsStmt, { limit, before }).map(
27775
- (r) => r.sessionId
27776
- );
27777
- }
27778
- /** One session's undelivered structural rows within the backlog, root first. */
27779
- pendingRows(sessionId, limit, before) {
27780
- return allRows(this.rowsStmt, { sessionId, limit, before });
27781
- }
27782
- /**
27783
- * Captures this machine still owes the deployment, oldest first.
27784
- *
27785
- * Selected by the `outbox_owed` marker the attached forward path writes, not
27786
- * by a time window — see captureRowsStmt for why a window could not express
27787
- * this. `before` is the grace window that leaves a just-recorded capture to
27788
- * the live path.
27789
- */
27790
- pendingCaptureRows(limit, before) {
27791
- return allRows(this.captureRowsStmt, { limit, before });
27792
- }
27793
- /**
27794
- * Record that a capture is OWED to the deployment.
27795
- *
27796
- * Written by the attached forward path when a live send did not confirm
27797
- * delivery, and read by the drain as the whole of its eligibility test. It is
27798
- * a fact rather than an inference: the machine was attached, the send did not
27799
- * land, so the row is owed — which no time window can state, because the same
27800
- * window that holds the rows a past attachment left owed also holds every
27801
- * capture recorded while the machine was DETACHED, and those were never
27802
- * offered to anyone.
27803
- *
27804
- * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27805
- * out of the drain's read.
27806
- */
27807
- markCaptureOwed(id) {
27808
- this.markOwedStmt.run({ id });
27809
- }
27810
- /** Record delivery. Called only AFTER the far side has accepted the rows. */
27811
- markSynced(ids, atMs) {
27812
- this.stampAll(ids, atMs);
27813
- }
27814
- /**
27815
- * Record that a row will never be sent.
27816
- *
27817
- * Reserved for a local defect — a row that cannot be rebuilt into a valid
27818
- * payload. A row that merely failed to reach the deployment stays NULL, so it
27819
- * is retried; marking those would turn one outage into permanent data loss.
27820
- */
27821
- markSkipped(ids) {
27822
- this.stampAll(ids, SKIPPED);
27823
- }
27824
- eachInTransaction(ids, run) {
27825
- if (ids.length === 0) return;
27826
- withTransaction(
27827
- this.db,
27828
- () => {
27829
- for (const id of ids) run(id);
27830
- },
27831
- "IMMEDIATE"
27832
- );
27833
- }
27834
- stampAll(ids, value) {
27835
- if (ids.length === 0) return;
27836
- withTransaction(
27837
- this.db,
27838
- () => {
27839
- for (const id of ids) this.stampStmt.run({ at: value, id });
27840
- },
27841
- "IMMEDIATE"
27842
- );
27843
- }
27844
- /**
27845
- * Claim rows as in-flight.
27846
- *
27847
- * Advisory in exactly the sense the lease is: it records that a send is in
27848
- * progress so a surface can say so, and a lost claim costs a row showing as
27849
- * queued while it is actually being sent. It is not exclusion — the far side
27850
- * settles a duplicate on the row id.
27851
- */
27852
- claimRows(ids, atMs) {
27853
- this.eachInTransaction(ids, (id) => this.claimRowStmt.run({ at: atMs, id }));
27854
- }
27855
- /** Give back a claim without settling — the send failed, the row is queued again. */
27856
- releaseRows(ids) {
27857
- this.eachInTransaction(ids, (id) => this.releaseRowStmt.run({ id }));
27858
- }
27859
- /**
27860
- * Clear claims older than `staleBefore`, and report how many were cleared.
27861
- *
27862
- * A process killed between claiming and settling leaves rows claimed with
27863
- * nothing left to settle them. Without this they read as "sending" for ever.
27864
- */
27865
- releaseStaleClaims(staleBefore) {
27866
- return Number(this.releaseStaleClaimsStmt.run({ staleBefore }).changes);
27867
- }
27868
- /**
27869
- * Every tracked row in exactly one delivery state.
27870
- *
27871
- * Takes no boundary on purpose. The boundary answers "what should the drain
27872
- * pick up now", which is a different question from "what state is this row
27873
- * in" — and a machine that has never attached has no boundary to pass, so
27874
- * requiring one would force a caller to invent one and report the whole store
27875
- * as queued.
27876
- */
27877
- partition() {
27878
- const row = getRow(this.partitionStmt, {});
27879
- return {
27880
- queued: row?.queued ?? 0,
27881
- inProgress: row?.inProgress ?? 0,
27882
- synced: row?.synced ?? 0,
27883
- failed: row?.failed ?? 0,
27884
- total: row?.total ?? 0
27885
- };
27886
- }
27887
- /** `pending` counts only what is inside the backlog; sent and skipped are totals. */
27888
- counts(before) {
27889
- const row = getRow(
27890
- this.countsStmt,
27891
- { before }
27892
- );
27893
- const captures = getRow(this.captureSkipCountStmt);
27894
- return {
27895
- pending: row?.pending ?? 0,
27896
- sent: row?.sent ?? 0,
27897
- skipped: row?.skipped ?? 0,
27898
- capturesSkipped: captures?.skipped ?? 0
27899
- };
27900
- }
27901
- /**
27902
- * The deployment the current stamps were made against, and where its backlog
27903
- * ends.
27904
- *
27905
- * READ-ONLY. An absent row reads as an absent deployment, which is what a
27906
- * machine that has never drained is — and every writer below seeds the row
27907
- * before it needs one, so nothing depends on this creating it. Keeping the
27908
- * write off the gate path matters because the gate runs on every pass while a
27909
- * write has to take the database's write lock.
27910
- */
27911
- deployment() {
27912
- const row = getRow(
27913
- this.fingerprintStmt
27914
- );
27915
- return {
27916
- fingerprint: row?.fingerprint ?? void 0,
27917
- backlogBefore: row?.backlogBefore ?? void 0
27918
- };
27919
- }
27920
- /**
27921
- * Point the ledger at a different deployment, discarding what it recorded
27922
- * about the previous one.
27923
- *
27924
- * Delivery is a fact about ONE recipient: rows sent to the deployment a
27925
- * machine has just left are undelivered as far as the new one is concerned.
27926
- * All three in one transaction, so a crash between them cannot leave stamps
27927
- * attributed to the wrong deployment, or a boundary that belongs to another.
27928
- *
27929
- * The boundary is written HERE and only here, which is what freezes it: a
27930
- * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
27931
- * unchanged, so this never runs and the backlog does not widen back over rows
27932
- * the live path has since delivered.
27933
- */
27934
- rearmFor(fingerprint, backlogBefore) {
27935
- this.ensureRowStmt.run();
27936
- withTransaction(
27937
- this.db,
27938
- () => {
27939
- const previous = getRow(this.fingerprintStmt)?.fingerprint;
27940
- this.rearmStmt.run();
27941
- if (previous !== null && previous !== void 0 && previous !== fingerprint) {
27942
- this.disownCapturesStmt.run();
27943
- }
27944
- this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27945
- },
27946
- "IMMEDIATE"
27947
- );
27948
- }
27949
- /**
27950
- * End the attached period: hand its rows to the live path, and release the
27951
- * boundary so the next attachment can freeze a new one.
27952
- *
27953
- * WITHOUT THIS, a detach and a re-attach to the same deployment leave a window
27954
- * nothing delivers. The fingerprint is unchanged, so the boundary is never
27955
- * re-frozen and stays at the FIRST attachment — while nothing forwards at all
27956
- * during the detached period, because the machine is not attached. Rows
27957
- * recorded in that window sit after the boundary and before the re-attach, so
27958
- * neither path takes them, and the pending count reports none outstanding.
27959
- *
27960
- * Stamping the attached window is not a claim that every one of those rows
27961
- * reached the deployment — the live path drops on failure and says so
27962
- * elsewhere. It records that they were ITS to deliver, which is exactly the
27963
- * status quo: they sit outside the frozen boundary today and are equally never
27964
- * re-sent. Making it explicit is what lets the boundary move.
27965
- *
27966
- * ONE TRANSACTION, so a crash cannot release the boundary while leaving the
27967
- * window unstamped — that half-state would re-send the whole attached period
27968
- * on the next attach, which is the failure the boundary exists to prevent.
27969
- */
27970
- closeAttachedWindow(attachedAtMs, atMs) {
27971
- this.ensureRowStmt.run();
27972
- withTransaction(
27973
- this.db,
27974
- () => {
27975
- const row = getRow(this.fingerprintStmt);
27976
- const from = row?.backlogBefore ?? attachedAtMs;
27977
- this.closeWindowStmt.run({ at: atMs, attachedAt: from });
27978
- this.releaseBoundaryStmt.run();
27979
- },
27980
- "IMMEDIATE"
27981
- );
27982
- }
27983
- /**
27984
- * Freeze a boundary for the deployment already on file, KEEPING the stamps.
27985
- *
27986
- * The re-attach half of the above. Distinct from `rearmFor`, which is for a
27987
- * different deployment and therefore discards what was delivered to the old
27988
- * one: here the recipient is the same, so everything already sent to it stays
27989
- * sent.
27990
- */
27991
- freezeBoundary(backlogBefore) {
27992
- this.ensureRowStmt.run();
27993
- this.freezeBoundaryStmt.run({ backlogBefore });
27994
- }
27995
- /** Take the claim, or report that someone live already holds it. */
27996
- claim(pid, host, nowMs, staleAfterMs) {
27997
- this.ensureRowStmt.run();
27998
- let taken = false;
27999
- withTransaction(
28000
- this.db,
28001
- () => {
28002
- const result = this.claimStmt.run({
28003
- pid,
28004
- host,
28005
- now: nowMs,
28006
- staleBefore: nowMs - staleAfterMs
28007
- });
28008
- taken = result.changes === 1;
28009
- },
28010
- "IMMEDIATE"
28011
- );
28012
- return taken;
28013
- }
28014
- /** Say the holder is still alive. A no-op once the claim has moved on. */
28015
- heartbeat(pid, nowMs) {
28016
- this.heartbeatStmt.run({ now: nowMs, pid });
28017
- }
28018
- /** Give the claim up. Scoped to this holder, so a taken-over claim is left be. */
28019
- release(pid) {
28020
- this.releaseStmt.run({ pid });
28021
- }
28022
- /** Who holds the claim, if anyone. Read-only, for the same reason as above. */
28023
- lease() {
28024
- return getRow(this.leaseStmt);
28025
- }
28026
- };
28027
-
28028
28735
  // ../../packages/persistence/src/repositories/inspection-definitions.ts
28029
28736
  var SqliteInspectionDefinitionsRepository = class {
28030
28737
  constructor(db) {
@@ -28215,7 +28922,8 @@ function managedSettingsPaths(platform2 = process.platform) {
28215
28922
  }
28216
28923
  return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28217
28924
  }
28218
- function readManagedSettings(paths = managedSettingsPaths()) {
28925
+ var testOnlyManagedPaths = null;
28926
+ function readManagedSettings(paths = testOnlyManagedPaths ?? managedSettingsPaths()) {
28219
28927
  for (const path of paths) {
28220
28928
  let text;
28221
28929
  try {
@@ -28250,6 +28958,7 @@ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ n
28250
28958
  if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28251
28959
  if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28252
28960
  if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
28961
+ if (values.bodyRetention !== void 0) merged.bodyRetention = values.bodyRetention;
28253
28962
  if (values.vaultConsent !== void 0) {
28254
28963
  merged.vaultConsent = values.vaultConsent ? (
28255
28964
  // Keep an existing valid grant so its acknowledgedAt survives; mint one
@@ -30695,7 +31404,7 @@ function toUtcDateString(ms) {
30695
31404
  return new Date(ms).toISOString().slice(0, 10);
30696
31405
  }
30697
31406
  function isTimeseriesSeverity(s) {
30698
- return s === "critical" || s === "high" || s === "medium";
31407
+ return s === "critical" || s === "high" || s === "medium" || s === "low";
30699
31408
  }
30700
31409
  var SqliteSecurityRepository = class {
30701
31410
  constructor(db, now = () => Date.now()) {
@@ -30757,7 +31466,7 @@ var SqliteSecurityRepository = class {
30757
31466
  ELSE 0
30758
31467
  END) AS open_at_rest
30759
31468
  FROM inspection_findings f
30760
- JOIN audit_events e ON e.id = f.audit_event_id
31469
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
30761
31470
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
30762
31471
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
30763
31472
  ON latest.finding_key = f.finding_key
@@ -30824,12 +31533,16 @@ var SqliteSecurityRepository = class {
30824
31533
  const now = this.now();
30825
31534
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
30826
31535
  const rows = this.findingsInRange(windowStart, now);
30827
- const points = Array.from({ length: numBuckets }, (_, i) => ({
30828
- timestamp: toUtcDateString(windowStart + i * bucketMs),
30829
- critical: 0,
30830
- high: 0,
30831
- medium: 0
30832
- }));
31536
+ const points = Array.from(
31537
+ { length: numBuckets },
31538
+ (_, i) => ({
31539
+ timestamp: toUtcDateString(windowStart + i * bucketMs),
31540
+ critical: 0,
31541
+ high: 0,
31542
+ medium: 0,
31543
+ low: 0
31544
+ })
31545
+ );
30833
31546
  for (const r of rows) {
30834
31547
  const idx = Math.floor((r.occurredAt - windowStart) / bucketMs);
30835
31548
  const bucket = points[idx];
@@ -30979,7 +31692,7 @@ var SqliteSecurityRepository = class {
30979
31692
  this.db.prepare(
30980
31693
  `SELECT e.repo AS repo, count(*) AS c
30981
31694
  FROM inspection_findings f
30982
- JOIN audit_events e ON e.id = f.audit_event_id
31695
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
30983
31696
  WHERE e.started_at >= :from AND e.started_at < :to
30984
31697
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
30985
31698
  AND e.repo IS NOT NULL
@@ -31047,6 +31760,7 @@ var SqliteSecurityRepository = class {
31047
31760
  `SELECT f.finding_key AS finding_key,
31048
31761
  d.rule_id AS rule_id,
31049
31762
  d.severity AS severity,
31763
+ e.repo AS repo,
31050
31764
  e.file_path AS path,
31051
31765
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
31052
31766
  latest.resolved_at AS latest_resolved_at
@@ -31066,6 +31780,7 @@ var SqliteSecurityRepository = class {
31066
31780
  const items = rows.map((r) => ({
31067
31781
  findingKey: r.finding_key,
31068
31782
  ruleId: r.rule_id,
31783
+ repo: r.repo ?? "",
31069
31784
  severity: r.severity,
31070
31785
  path: r.path ?? "",
31071
31786
  resolvedAt: new Date(r.latest_resolved_at).toISOString(),
@@ -31075,15 +31790,68 @@ var SqliteSecurityRepository = class {
31075
31790
  }));
31076
31791
  return Promise.resolve({ items });
31077
31792
  }
31793
+ /**
31794
+ * Per-rule tallies of the findings that are still OPEN, whole-store.
31795
+ *
31796
+ * Scoped by status rather than by time, because the card this feeds is a to-do
31797
+ * list: a secret committed three weeks ago and never rotated is still the most
31798
+ * important thing to fix, and any window hides it. It carried a "newest N
31799
+ * findings" cap and then a range; the first meant a different span on every
31800
+ * machine, and the second reported "no recommendations" over live exposure.
31801
+ *
31802
+ * `open` mirrors `deriveFindingStatus` — at-rest, minus resolved and dismissed —
31803
+ * so a row's count is exactly what `?status=open&type=<rule>` returns. Note that
31804
+ * is NOT `severitySummary`'s `openAtRest`, which keeps dismissed findings (a
31805
+ * dismissal is a judgement, not a remediation) and drops untracked legacy rows.
31806
+ * The two answer different questions and only this one has to match a link.
31807
+ *
31808
+ * Aggregated in SQL: the result is O(distinct rule × category × severity), so a
31809
+ * whole-store scope costs a grouped scan rather than a row per finding.
31810
+ */
31811
+ recommendationInputs() {
31812
+ const rows = allRows(
31813
+ this.db.prepare(
31814
+ `SELECT d.rule_id AS rule_id,
31815
+ d.category AS category,
31816
+ d.severity AS severity,
31817
+ COUNT(*) AS count
31818
+ FROM inspection_findings f
31819
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31820
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31821
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
31822
+ ON latest.finding_key = f.finding_key
31823
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
31824
+ AND e.event_type = 'code_change'
31825
+ AND (
31826
+ f.finding_key IS NULL
31827
+ OR latest.status IS NULL
31828
+ OR latest.status NOT IN ('resolved', 'dismissed')
31829
+ )
31830
+ GROUP BY d.rule_id, d.category, d.severity`
31831
+ )
31832
+ );
31833
+ return Promise.resolve(
31834
+ rows.map((r) => ({
31835
+ ruleId: r.rule_id,
31836
+ category: r.category,
31837
+ severity: r.severity,
31838
+ count: r.count
31839
+ }))
31840
+ );
31841
+ }
31078
31842
  // Findings whose parent event occurred in [fromMs, toMs), with the parent's
31079
31843
  // epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
31080
31844
  // numeric and the JS aggregations bucket/split on ms directly.
31081
31845
  findingsInRange(fromMs, toMs) {
31082
31846
  const rows = allRows(
31083
31847
  this.db.prepare(
31084
- `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
31848
+ // `rule_id`/`category` cost nothing extra: inspection_definitions is already
31849
+ // joined for `severity`, so they are two more columns off a row this read
31850
+ // already fetches. They feed the recommended-actions rollup.
31851
+ `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
31852
+ d.rule_id AS rule_id, d.category AS category
31085
31853
  FROM inspection_findings f
31086
- JOIN audit_events e ON e.id = f.audit_event_id
31854
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31087
31855
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31088
31856
  WHERE e.started_at >= :from AND e.started_at < :to
31089
31857
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
@@ -31094,7 +31862,9 @@ var SqliteSecurityRepository = class {
31094
31862
  return rows.map((r) => ({
31095
31863
  occurredAt: r.occurred_at,
31096
31864
  severity: r.severity,
31097
- actionTaken: r.action_taken
31865
+ actionTaken: r.action_taken,
31866
+ ruleId: r.rule_id,
31867
+ category: r.category
31098
31868
  }));
31099
31869
  }
31100
31870
  };
@@ -31922,6 +32692,7 @@ function openWithPragmas(file2) {
31922
32692
  db.exec("PRAGMA journal_mode = WAL");
31923
32693
  db.exec("PRAGMA busy_timeout = 2000");
31924
32694
  db.exec("PRAGMA foreign_keys = ON");
32695
+ registerSqlFunctions(db);
31925
32696
  } catch (err) {
31926
32697
  closeQuietly(db);
31927
32698
  throw err;
@@ -31951,7 +32722,7 @@ function backupLegacyStore(db, file2) {
31951
32722
  discardStore(file2, backup);
31952
32723
  return backup;
31953
32724
  }
31954
- function openAndInitialize(file2, base) {
32725
+ function openAndInitialize(file2, base, skipTags) {
31955
32726
  let db = openWithPragmas(file2);
31956
32727
  try {
31957
32728
  if (isForeignSqliteLineage(db)) {
@@ -31961,7 +32732,7 @@ function openAndInitialize(file2, base) {
31961
32732
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
31962
32733
  );
31963
32734
  }
31964
- applyMigrations(db, file2);
32735
+ applyMigrations(db, file2, { skipTags });
31965
32736
  tightenPerms(file2);
31966
32737
  const policies = new SqlitePoliciesRepository(db);
31967
32738
  const installedPacks = new SqliteInstalledPacksRepository(db, base);
@@ -31976,6 +32747,7 @@ function openAndInitialize(file2, base) {
31976
32747
  exceptions: new SqliteExceptionsRepository(db),
31977
32748
  resolutions: new SqliteResolutionsRepository(db),
31978
32749
  ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
32750
+ bodyRetention: new SqliteBodyRetentionRepository(db),
31979
32751
  security: new SqliteSecurityRepository(db),
31980
32752
  detections: new SqliteDetectionsRepository(db),
31981
32753
  shares: new SqliteSharesRepository(db),
@@ -31998,7 +32770,8 @@ function openAndInitialize(file2, base) {
31998
32770
  throw err;
31999
32771
  }
32000
32772
  }
32001
- function openLocalDatabase(dir) {
32773
+ var DEFERRED_TAGS = new Set(DEFERRED_MIGRATION_TAGS);
32774
+ function openLocalDatabase(dir, options = {}) {
32002
32775
  ensureDataDirSync(dir);
32003
32776
  const file2 = join7(dir, DB_FILENAME);
32004
32777
  reapStalePartials(file2);
@@ -32010,6 +32783,7 @@ function openLocalDatabase(dir) {
32010
32783
  installedPacks,
32011
32784
  scanLedger,
32012
32785
  historySync,
32786
+ bodyRetention,
32013
32787
  secretVault,
32014
32788
  exceptions,
32015
32789
  resolutions,
@@ -32033,7 +32807,8 @@ function openLocalDatabase(dir) {
32033
32807
  // `dir` is always `<base>/data` — every caller resolves it through
32034
32808
  // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
32035
32809
  // settings/ and data/, and the pack-policy floor needs both halves.
32036
- dirname2(dir)
32810
+ dirname2(dir),
32811
+ options.applyDeferredMigrations === true ? void 0 : DEFERRED_TAGS
32037
32812
  );
32038
32813
  function captureRowId(event) {
32039
32814
  return captureId(
@@ -32226,6 +33001,7 @@ function openLocalDatabase(dir) {
32226
33001
  installedPacks,
32227
33002
  scanLedger,
32228
33003
  historySync,
33004
+ bodyRetention,
32229
33005
  secretVault,
32230
33006
  exceptions,
32231
33007
  resolutions,
@@ -32264,6 +33040,10 @@ function openLocalDatabase(dir) {
32264
33040
  };
32265
33041
  }
32266
33042
 
33043
+ // ../../packages/persistence/src/egress-wire.ts
33044
+ import { createHash as createHash3 } from "crypto";
33045
+ var SLASH = "/".charCodeAt(0);
33046
+
32267
33047
  // ../../packages/persistence/src/exception-policy.ts
32268
33048
  var UserGrantPolicyProvider = class {
32269
33049
  #exceptions;
@@ -32285,7 +33065,7 @@ var UserGrantPolicyProvider = class {
32285
33065
  };
32286
33066
 
32287
33067
  // ../../packages/persistence/src/finding-key.ts
32288
- import { createHash as createHash3 } from "crypto";
33068
+ import { createHash as createHash4 } from "crypto";
32289
33069
 
32290
33070
  // ../../packages/persistence/src/fingerprint.ts
32291
33071
  import { createHmac, randomBytes } from "crypto";
@@ -32410,14 +33190,26 @@ function fingerprintValue(key, raw) {
32410
33190
  return createHmac("sha256", key.material).update(raw, "utf8").digest("hex");
32411
33191
  }
32412
33192
 
32413
- // ../../packages/persistence/src/history-preview.ts
32414
- import { existsSync as existsSync4 } from "fs";
33193
+ // ../../packages/persistence/src/forward-health.ts
33194
+ import { readFileSync as readFileSync7 } from "fs";
32415
33195
  import { join as join9 } from "path";
33196
+
33197
+ // ../../packages/persistence/src/history-backfill.ts
33198
+ import { existsSync as existsSync4 } from "fs";
33199
+ import { join as join10 } from "path";
33200
+
33201
+ // ../../packages/persistence/src/history-preview.ts
33202
+ import { existsSync as existsSync5 } from "fs";
33203
+ import { join as join11 } from "path";
32416
33204
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
32417
33205
 
33206
+ // ../../packages/persistence/src/history-sync-state.ts
33207
+ import { readFileSync as readFileSync8 } from "fs";
33208
+ import { join as join12 } from "path";
33209
+
32418
33210
  // ../../packages/persistence/src/store-symlinks.ts
32419
- import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32420
- import { dirname as dirname3, join as join10, resolve } from "path";
33211
+ import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
33212
+ import { dirname as dirname3, join as join13, resolve } from "path";
32421
33213
 
32422
33214
  // ../../packages/persistence/src/vault/crypto.ts
32423
33215
  import {
@@ -32530,8 +33322,8 @@ function formatPointer(category, keyVersion, pointerId, tag) {
32530
33322
  // ../../packages/persistence/src/vault/key-provider.ts
32531
33323
  import { execFileSync } from "child_process";
32532
33324
  import { randomBytes as randomBytes2 } from "crypto";
32533
- import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32534
- import { join as join11 } from "path";
33325
+ import { chmodSync as chmodSync3, readFileSync as readFileSync9, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
33326
+ import { join as join14 } from "path";
32535
33327
  var VAULT_OCCUPANT_REASON = {
32536
33328
  symlink: "the path is a symlink; remove it so a keyring can be created",
32537
33329
  gone: "the path was occupied but holds no keyring (removed while it was being created)",
@@ -32630,7 +33422,7 @@ function claimRotationLock(lock, owner) {
32630
33422
  throw asError(err);
32631
33423
  }
32632
33424
  try {
32633
- writeFileSync3(join11(lock, LOCK_OWNER_FILE), `${owner}
33425
+ writeFileSync3(join14(lock, LOCK_OWNER_FILE), `${owner}
32634
33426
  `, { mode: DATA_FILE_MODE });
32635
33427
  return true;
32636
33428
  } catch (err) {
@@ -32639,7 +33431,7 @@ function claimRotationLock(lock, owner) {
32639
33431
  }
32640
33432
  }
32641
33433
  function acquireRotationLock(keysDir2) {
32642
- const lock = join11(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
33434
+ const lock = join14(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
32643
33435
  const owner = randomBytes2(16).toString("hex");
32644
33436
  if (claimRotationLock(lock, owner)) return { lock, owner };
32645
33437
  let held;
@@ -32666,7 +33458,7 @@ function acquireRotationLock(keysDir2) {
32666
33458
  }
32667
33459
  function releaseRotationLock(lease) {
32668
33460
  try {
32669
- if (readFileSync7(join11(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
33461
+ if (readFileSync9(join14(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
32670
33462
  } catch {
32671
33463
  return;
32672
33464
  }
@@ -32687,7 +33479,7 @@ var FileKeyProvider = class {
32687
33479
  this.#keysDir = keysDir2;
32688
33480
  }
32689
33481
  get filePath() {
32690
- return join11(this.#keysDir, VAULT_KEY_FILENAME);
33482
+ return join14(this.#keysDir, VAULT_KEY_FILENAME);
32691
33483
  }
32692
33484
  loadOrCreate() {
32693
33485
  return asAsync(() => {
@@ -32717,7 +33509,7 @@ var FileKeyProvider = class {
32717
33509
  #read() {
32718
33510
  let raw;
32719
33511
  try {
32720
- raw = readFileSync7(this.filePath, "utf8");
33512
+ raw = readFileSync9(this.filePath, "utf8");
32721
33513
  } catch (err) {
32722
33514
  if (err.code === "ENOENT") return null;
32723
33515
  throw err instanceof Error ? err : new Error(String(err));
@@ -33352,8 +34144,8 @@ var SecretVault = class {
33352
34144
  };
33353
34145
 
33354
34146
  // ../../packages/persistence/src/warn-era-cap.ts
33355
- import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
33356
- import { join as join12 } from "path";
34147
+ import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
34148
+ import { join as join15 } from "path";
33357
34149
 
33358
34150
  // ../../packages/plugin-sdk/src/provider-env.ts
33359
34151
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -33407,8 +34199,8 @@ function resolveProvider() {
33407
34199
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
33408
34200
  try {
33409
34201
  ensureLayoutDirSync(base);
33410
- const settingsFile = join13(settingsDir(base), "settings.json");
33411
- if (existsSync7(settingsFile)) tightenFile(settingsFile);
34202
+ const settingsFile = join16(settingsDir(base), "settings.json");
34203
+ if (existsSync8(settingsFile)) tightenFile(settingsFile);
33412
34204
  } catch {
33413
34205
  }
33414
34206
  migrateLegacyLayout(base);
@@ -33431,9 +34223,9 @@ function resolveProviderSafe(resolveProviderFn) {
33431
34223
  }
33432
34224
 
33433
34225
  // ../../packages/plugin-sdk/src/config-inventory.ts
33434
- import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
34226
+ import { readdirSync as readdirSync2, readFileSync as readFileSync11, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
33435
34227
  import { homedir as homedir2 } from "os";
33436
- import { basename as basename3, join as join15 } from "path";
34228
+ import { basename as basename3, join as join18 } from "path";
33437
34229
 
33438
34230
  // ../../packages/detections/src/egress/registry.ts
33439
34231
  var EXTRACTOR_VERSION = "1";
@@ -36391,24 +37183,20 @@ function registerBundledPacks() {
36391
37183
  }
36392
37184
 
36393
37185
  // ../../packages/plugin-sdk/src/repo.ts
36394
- import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
36395
- import { basename as basename2, dirname as dirname4, isAbsolute, join as join14, sep as sep2 } from "path";
37186
+ import { existsSync as existsSync9, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
37187
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join17, sep as sep2 } from "path";
36396
37188
 
36397
37189
  // ../../packages/plugin-sdk/src/events.ts
36398
- import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
37190
+ import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
36399
37191
 
36400
37192
  // ../../packages/plugin-sdk/src/isolated-scan.ts
36401
- import { existsSync as existsSync9 } from "fs";
37193
+ import { existsSync as existsSync10 } from "fs";
36402
37194
  import { fileURLToPath } from "url";
36403
37195
  import { Worker } from "worker_threads";
36404
37196
 
36405
- // ../../packages/plugin-sdk/src/ignore-layers.ts
36406
- var import_ignore = __toESM(require_ignore(), 1);
36407
- import { readFileSync as readFileSync10 } from "fs";
36408
- import { join as join16 } from "path";
36409
-
36410
- // ../../packages/plugin-sdk/src/inventory-resolver.ts
36411
- import { arch, hostname as hostname4, platform, release } from "os";
37197
+ // ../../packages/plugin-sdk/src/host-floor.ts
37198
+ import { readFileSync as readFileSync13 } from "fs";
37199
+ import { join as join20 } from "path";
36412
37200
 
36413
37201
  // ../../packages/plugin-sdk/src/model-governance.ts
36414
37202
  import {
@@ -36416,24 +37204,50 @@ import {
36416
37204
  fstatSync,
36417
37205
  mkdirSync as mkdirSync2,
36418
37206
  openSync as openSync2,
36419
- readFileSync as readFileSync11,
37207
+ readFileSync as readFileSync12,
36420
37208
  readSync,
36421
37209
  writeFileSync as writeFileSync5
36422
37210
  } from "fs";
36423
- import { join as join17 } from "path";
37211
+ import { join as join19 } from "path";
36424
37212
  var TAIL_BYTES = 256 * 1024;
36425
37213
 
37214
+ // ../../packages/plugin-sdk/src/host-floor.ts
37215
+ var HOST_FEATURE = {
37216
+ ModelSwitch: "model-switch",
37217
+ VaultPointerDisplay: "vault-pointer-display"
37218
+ };
37219
+ var HOST_FLOORS = {
37220
+ [HOST_FEATURE.ModelSwitch]: {
37221
+ label: "model-switch protection",
37222
+ hookEvents: ["PreModelSwitch", "PostModelSwitch"],
37223
+ since: "2.1.251"
37224
+ },
37225
+ [HOST_FEATURE.VaultPointerDisplay]: {
37226
+ label: "vault pointer display",
37227
+ hookEvents: ["MessageDisplay"],
37228
+ since: "2.1.152"
37229
+ }
37230
+ };
37231
+
37232
+ // ../../packages/plugin-sdk/src/ignore-layers.ts
37233
+ var import_ignore = __toESM(require_ignore(), 1);
37234
+ import { readFileSync as readFileSync14 } from "fs";
37235
+ import { join as join21 } from "path";
37236
+
37237
+ // ../../packages/plugin-sdk/src/inventory-resolver.ts
37238
+ import { arch, hostname as hostname4, platform, release } from "os";
37239
+
36426
37240
  // ../../packages/plugin-sdk/src/nudge.ts
36427
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
36428
- import { join as join18 } from "path";
37241
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync15, writeFileSync as writeFileSync6 } from "fs";
37242
+ import { join as join22 } from "path";
36429
37243
 
36430
37244
  // ../../packages/plugin-sdk/src/paths.ts
36431
37245
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
36432
37246
  import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
36433
37247
 
36434
37248
  // ../../packages/plugin-sdk/src/project-files.ts
36435
- import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
36436
- import { basename as basename5, join as join19 } from "path";
37249
+ import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
37250
+ import { basename as basename5, join as join23 } from "path";
36437
37251
 
36438
37252
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
36439
37253
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -36469,7 +37283,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
36469
37283
 
36470
37284
  // ../../packages/plugin-sdk/src/throttle.ts
36471
37285
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
36472
- import { join as join20 } from "path";
37286
+ import { join as join24 } from "path";
36473
37287
 
36474
37288
  // ../../packages/plugin-sdk/src/tokenize.ts
36475
37289
  function redactedPlaceholder(category) {
@@ -36799,13 +37613,13 @@ function describePointerSafe(token) {
36799
37613
  import {
36800
37614
  mkdirSync as mkdirSync5,
36801
37615
  readdirSync as readdirSync5,
36802
- readFileSync as readFileSync13,
37616
+ readFileSync as readFileSync16,
36803
37617
  renameSync as renameSync5,
36804
37618
  rmSync as rmSync7,
36805
37619
  statSync as statSync9,
36806
37620
  writeFileSync as writeFileSync8
36807
37621
  } from "fs";
36808
- import { dirname as dirname6, join as join21 } from "path";
37622
+ import { dirname as dirname6, join as join25 } from "path";
36809
37623
  var EMPTY_CARRY = Object.freeze({
36810
37624
  tail: "",
36811
37625
  fence: null,
@@ -37002,7 +37816,7 @@ var CARRY_FILE_PREFIX = "display-carry";
37002
37816
  var STALE_CARRY_MS = 15 * 60 * 1e3;
37003
37817
  function carryFilePath(dataDir2, sessionId) {
37004
37818
  const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
37005
- return join21(dataDir2, `${CARRY_FILE_PREFIX}-${safe === "" ? "session" : safe}.json`);
37819
+ return join25(dataDir2, `${CARRY_FILE_PREFIX}-${safe === "" ? "session" : safe}.json`);
37006
37820
  }
37007
37821
  function parseFence(value) {
37008
37822
  if (typeof value !== "object" || value === null) return null;
@@ -37013,7 +37827,7 @@ function parseFence(value) {
37013
37827
  }
37014
37828
  function loadCarry(file2, keys) {
37015
37829
  try {
37016
- const parsed = JSON.parse(readFileSync13(file2, "utf8"));
37830
+ const parsed = JSON.parse(readFileSync16(file2, "utf8"));
37017
37831
  if (typeof parsed !== "object" || parsed === null) return EMPTY_CARRY;
37018
37832
  const record2 = parsed;
37019
37833
  const revealedCount = record2.messageKey === keys.messageKey && typeof record2.revealedCount === "number" && Number.isFinite(record2.revealedCount) ? record2.revealedCount : 0;
@@ -37066,7 +37880,7 @@ function removeStaleCarryFiles(dir, keep) {
37066
37880
  const cutoff = Date.now() - STALE_CARRY_MS;
37067
37881
  for (const name of readdirSync5(dir)) {
37068
37882
  if (!name.startsWith(CARRY_FILE_PREFIX) || !name.endsWith(".json")) continue;
37069
- const path = join21(dir, name);
37883
+ const path = join25(dir, name);
37070
37884
  if (path === keep) continue;
37071
37885
  try {
37072
37886
  if (statSync9(path).mtimeMs < cutoff) rmSync7(path, { force: true });
@@ -37097,7 +37911,7 @@ function finalizeCarry(file2, keys, carry) {
37097
37911
  }
37098
37912
  let owned = true;
37099
37913
  try {
37100
- const parsed = JSON.parse(readFileSync13(file2, "utf8"));
37914
+ const parsed = JSON.parse(readFileSync16(file2, "utf8"));
37101
37915
  if (typeof parsed === "object" && parsed !== null) {
37102
37916
  const record2 = parsed;
37103
37917
  owned = record2.blockKey === keys.blockKey || record2.messageKey === keys.messageKey;