@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 SLASH2 = "/";
53
+ var SLASH3 = "/";
54
54
  var TMP_KEY_IGNORE = "node-ignore";
55
55
  if (typeof Symbol !== "undefined") {
56
56
  TMP_KEY_IGNORE = /* @__PURE__ */ Symbol.for("node-ignore");
@@ -422,11 +422,11 @@ var require_ignore = __commonJS({
422
422
  if (!REGEX_TEST_TRAILING_SLASH.test(path)) {
423
423
  return this.test(path);
424
424
  }
425
- const slices = path.split(SLASH2).filter(Boolean);
425
+ const slices = path.split(SLASH3).filter(Boolean);
426
426
  slices.pop();
427
427
  if (slices.length) {
428
428
  const parent = this._t(
429
- slices.join(SLASH2) + SLASH2,
429
+ slices.join(SLASH3) + SLASH3,
430
430
  this._testCache,
431
431
  true,
432
432
  slices
@@ -442,14 +442,14 @@ var require_ignore = __commonJS({
442
442
  return cache[path];
443
443
  }
444
444
  if (!slices) {
445
- slices = path.split(SLASH2).filter(Boolean);
445
+ slices = path.split(SLASH3).filter(Boolean);
446
446
  }
447
447
  slices.pop();
448
448
  if (!slices.length) {
449
449
  return cache[path] = this._rules.test(path, checkUnignored, MODE_IGNORE);
450
450
  }
451
451
  const parent = this._t(
452
- slices.join(SLASH2) + SLASH2,
452
+ slices.join(SLASH3) + SLASH3,
453
453
  cache,
454
454
  checkUnignored,
455
455
  slices
@@ -491,9 +491,6 @@ var require_ignore = __commonJS({
491
491
  }
492
492
  });
493
493
 
494
- // ../../packages/plugin-runtime/src/attached/egress-wire.ts
495
- import { createHash as createHash4 } from "crypto";
496
-
497
494
  // ../../packages/persistence/src/attached-derived.ts
498
495
  import { rmSync } from "fs";
499
496
  import { join } from "path";
@@ -505,6 +502,14 @@ var ATTACHED_FORWARD_DROPS_FILENAME = "attached-forward-drops.json";
505
502
  import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync, rmSync as rmSync3, statSync } from "fs";
506
503
  import { join as join2 } from "path";
507
504
 
505
+ // ../../packages/schema/src/drizzle/deferred-migrations.ts
506
+ var DEFERRED_MIGRATION_TAGS = [
507
+ "0031_audit_capture_by_time_index",
508
+ "0032_audit_capture_by_id_index",
509
+ "0033_audit_capture_location_index",
510
+ "0034_findings_read_indexes"
511
+ ];
512
+
508
513
  // ../../packages/schema/src/drizzle/sqlite-ddl.ts
509
514
  var SQLITE_MIGRATIONS = [
510
515
  {
@@ -622,6 +627,30 @@ var SQLITE_MIGRATIONS = [
622
627
  {
623
628
  tag: "0028_activity_session_probe_indexes",
624
629
  sql: "CREATE INDEX `idx_audit_session_prompt` ON `audit_events` (`root_session_id`) WHERE event_type = 'prompt';--> statement-breakpoint\nCREATE INDEX `idx_audit_session_share` ON `audit_events` (`root_session_id`) WHERE event_type = 'share';--> statement-breakpoint\nCREATE INDEX `idx_audit_ended_at` ON `audit_events` (`ended_at`,`root_session_id`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\nCREATE INDEX `idx_audit_session_ended` ON `audit_events` (`root_session_id`,`ended_at`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\n-- Expression index for the activity list's turns rollup: a live-captured\n-- session's turns are the DISTINCT `run_key` across its `llm_call` leaves, and\n-- carrying the extracted key in the index answers that count from the index\n-- alone instead of parsing every leaf's attribute bag. Written by hand, as\n-- 0013's `idx_audit_code_change_path` was: drizzle-kit cannot emit an\n-- expression containing a comma, so this index is not declared in sqlite.ts.\nCREATE INDEX `idx_audit_session_run_key` ON `audit_events` (`root_session_id`, json_extract(`attributes`, '$.run_key')) WHERE `event_type` = 'llm_call';\n"
630
+ },
631
+ {
632
+ tag: "0029_audit_capture_rollup_index",
633
+ sql: "CREATE INDEX `idx_audit_capture_rollup` ON `audit_events` (`event_type`,`started_at`,`repo`,`id`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
634
+ },
635
+ {
636
+ tag: "0030_audit_content_expiry",
637
+ sql: "ALTER TABLE `audit_events` ADD `content_expired_at` integer;--> statement-breakpoint\nCREATE INDEX `idx_audit_expirable_body` ON `audit_events` (`started_at`) WHERE content IS NOT NULL;"
638
+ },
639
+ {
640
+ tag: "0031_audit_capture_by_time_index",
641
+ sql: "CREATE INDEX `idx_audit_capture_by_time` ON `audit_events` (`started_at`,`id`,`event_type`,`root_session_id`,`source_tool`,`repo`,`file_path`,`tool_name`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
642
+ },
643
+ {
644
+ tag: "0032_audit_capture_by_id_index",
645
+ sql: "CREATE INDEX `idx_audit_capture_by_id` ON `audit_events` (`id`,`started_at`,`event_type`,`root_session_id`,`source_tool`,`repo`,`file_path`,`tool_name`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
646
+ },
647
+ {
648
+ tag: "0033_audit_capture_location_index",
649
+ sql: "CREATE INDEX `idx_audit_capture_location` ON `audit_events` (`repo`,`file_path`,`started_at`,`id`,`event_type`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
650
+ },
651
+ {
652
+ tag: "0034_findings_read_indexes",
653
+ sql: "CREATE INDEX `idx_inspection_definitions_rule` ON `inspection_definitions` (`rule_id`,`severity`,`category`);--> statement-breakpoint\nCREATE INDEX `idx_inspection_findings_def` ON `inspection_findings` (`inspection_definition_id`,`audit_event_id`);--> statement-breakpoint\nCREATE INDEX `idx_inspection_findings_event_cover` ON `inspection_findings` (`audit_event_id`,`inspection_definition_id`,`action_taken`,`finding_key`,`id`);"
625
654
  }
626
655
  ];
627
656
 
@@ -20669,6 +20698,15 @@ var FindingCategory = external_exports.enum([
20669
20698
  ]).meta({ id: "FindingCategory" });
20670
20699
  var FindingOrigin = external_exports.enum(["in-flight", "at-rest"]).meta({ id: "FindingOrigin" });
20671
20700
  var FindingStatus = external_exports.enum(["open", "handled", "resolved", "dismissed"]).meta({ id: "FindingStatus" });
20701
+ var SyncFailureReason = external_exports.enum(["deployment_refused", "payload_invalid", "detached_undelivered"]).meta({ id: "SyncFailureReason" });
20702
+ var FindingDeliveryState = external_exports.enum(["sent", "queued", "not_sent", "never_offered", "local_scan"]).meta({ id: "FindingDeliveryState" });
20703
+ var FindingDelivery = external_exports.object({
20704
+ state: FindingDeliveryState,
20705
+ // The delivery time for `sent`; the failure time for `not_sent` when recorded.
20706
+ at: external_exports.iso.datetime().optional(),
20707
+ // Only on `not_sent`, and only when a known reason was recorded.
20708
+ reason: SyncFailureReason.optional()
20709
+ }).meta({ id: "FindingDelivery" });
20672
20710
  var ResolutionMethod = external_exports.enum([
20673
20711
  "enforced-in-flight",
20674
20712
  "fixed-at-source",
@@ -20725,7 +20763,10 @@ var FindingInstance = external_exports.object({
20725
20763
  // The session that event belongs to, when it has one — the seam a
20726
20764
  // per-instance "view session" link needs. Absent for events captured
20727
20765
  // outside a session.
20728
- sessionId: external_exports.string().optional()
20766
+ sessionId: external_exports.string().optional(),
20767
+ // The delivery state of the event above (see FindingDelivery). Optional so
20768
+ // readers that do not project it stay valid.
20769
+ delivery: FindingDelivery.optional()
20729
20770
  }).meta({ id: "FindingInstance" });
20730
20771
  var FindingGroup = external_exports.object({
20731
20772
  id: external_exports.string(),
@@ -20742,13 +20783,11 @@ var FindingGroup = external_exports.object({
20742
20783
  latestDetectedAt: external_exports.iso.datetime(),
20743
20784
  instances: external_exports.array(FindingInstance),
20744
20785
  // Derived from instances' statuses with open-dominates precedence (see
20745
- // buildFindingGroups). Undefined only when no instance carries a status.
20786
+ // foldGroupStatus). Undefined only when no instance carries a status.
20746
20787
  status: FindingStatus.optional(),
20747
- // The distinct people across the WHOLE group, not just the `instances`
20748
- // preview — from the store's whole-group aggregate when it supplies one,
20749
- // else folded from the rows (see buildFindingGroups). Undefined when no
20750
- // instance carries a user, or when the store supplied whole-group folds
20751
- // without one.
20788
+ // The distinct people across the WHOLE group, not just the instances
20789
+ // carried here. Undefined when no instance carries a user, or when the
20790
+ // store supplied whole-group folds without one.
20752
20791
  users: external_exports.array(FindingUser).optional()
20753
20792
  }).meta({ id: "FindingGroup" });
20754
20793
  var FindingStats = external_exports.object({
@@ -20777,21 +20816,34 @@ var FindingFacets = external_exports.object({
20777
20816
  // counted under no value.
20778
20817
  status: external_exports.array(FindingFacetItem),
20779
20818
  // Host tool (attributes.tool_name). Present only on the instance-level
20780
- // reads, which can filter by it; the grouped read omits the dimension
20819
+ // reads, which can filter by it; the type-level read omits the dimension
20781
20820
  // because a group spans tools.
20782
- tool: external_exports.array(FindingFacetItem).optional()
20821
+ tool: external_exports.array(FindingFacetItem).optional(),
20822
+ // Delivery states (FindingDeliveryState). Present only on the
20823
+ // instance-level reads, like `tool`.
20824
+ deployment: external_exports.array(FindingFacetItem).optional()
20783
20825
  }).meta({ id: "FindingFacets" });
20784
- var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
20785
- var ListGroupedFindingsQuery = external_exports.object({
20826
+ var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
20827
+ id: "FindingTypeSummary"
20828
+ });
20829
+ var DEFAULT_FINDING_TYPES_LIMIT = 50;
20830
+ var MAX_FINDING_TYPES_LIMIT = 100;
20831
+ var ListFindingTypesQuery = external_exports.object({
20786
20832
  // NOTE: severity filters by Severity (critical/high/medium/low), not by
20787
- // FindingAction.
20833
+ // FindingAction. It narrows TYPES: a type's severity is the one its newest
20834
+ // firing version carries, and this list pages types.
20835
+ //
20836
+ // That is NOT a claim the findings of a type share it. A rule can hold several
20837
+ // definition versions at different severities, so a type kept by this filter
20838
+ // can hold findings that individually do not match — see totals.findings on
20839
+ // ListFindingTypesResponse, which counts them all.
20788
20840
  severity: external_exports.array(Severity).optional(),
20789
20841
  subtype: external_exports.array(external_exports.string()).optional(),
20790
20842
  provider: external_exports.array(FindingProvider).optional(),
20791
20843
  action: external_exports.array(FindingAction).optional(),
20792
- // Matches a group's DERIVED status (see FindingGroup.status), not its
20793
- // individual instances' — so a filtered group's Status column always reads
20794
- // one of the requested values.
20844
+ // Matches a type's DERIVED status (see FindingGroup.status), not its
20845
+ // individual findings' — so a filtered row's status always reads one of the
20846
+ // requested values.
20795
20847
  status: external_exports.array(FindingStatus).optional(),
20796
20848
  q: external_exports.string().optional(),
20797
20849
  // Scope to findings whose event carries this session id (the Activity page's
@@ -20801,23 +20853,37 @@ var ListGroupedFindingsQuery = external_exports.object({
20801
20853
  // from a time-scoped page (Activity's range) can carry that scope. Absent
20802
20854
  // means all time — this list has no default window.
20803
20855
  from: external_exports.iso.datetime().optional(),
20804
- // A group or instance id that must appear in the page even when the cursor
20805
- // has already advanced past its sort position. This is what keeps the
20806
- // Findings page's one-shot ?finding= deep link resolving once the list
20807
- // paginates: the target group is appended out of sort order rather than
20808
- // scanning forward for it. Never affects totals, facets or the cursor.
20856
+ // A RULE id that must appear in the page even when the cursor has already
20857
+ // advanced past its sort position. This is what keeps the selected type
20858
+ // visible in the list once it paginates: the target is appended out of sort
20859
+ // order rather than scanned forward for. Never affects totals, facets or the
20860
+ // cursor. Unlike the grouped read this replaces, it names a rule only — an
20861
+ // instance id is resolved by `findingInstance`, which is a primary-key seek
20862
+ // and so is not bounded by what any page happens to hold.
20809
20863
  includeId: external_exports.string().optional(),
20810
- groupBy: external_exports.literal("type").optional(),
20811
- limit: external_exports.coerce.number().int().min(1).max(100).optional(),
20864
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_TYPES_LIMIT).optional(),
20812
20865
  cursor: external_exports.string().optional()
20813
20866
  });
20814
- var ListGroupedFindingsResponse = external_exports.object({
20867
+ var ListFindingTypesResponse = external_exports.object({
20815
20868
  totals: external_exports.object({
20869
+ // Findings belonging to the matching TYPES — not findings that each match
20870
+ // the filters. The filters here select types, so a type that survives
20871
+ // contributes its whole instanceCount.
20872
+ //
20873
+ // `status` is the one exception, narrowed per finding via
20874
+ // countInstancesByStatus. `severity`, `provider` and `action` are not, so
20875
+ // this can exceed what the instance read reports for the same filters: a
20876
+ // rule whose severity moved between versions is kept on its newest and
20877
+ // still counts its older findings. Narrowing the other three needs
20878
+ // per-dimension counts the aggregate does not carry today.
20816
20879
  findings: external_exports.number().int().nonnegative(),
20817
- groups: external_exports.number().int().nonnegative()
20880
+ // Counts TYPES, which is the unit this read pages. The instance read's
20881
+ // own totals count findings; the two deliberately answer different
20882
+ // questions and are never summed.
20883
+ types: external_exports.number().int().nonnegative()
20818
20884
  }),
20819
20885
  facets: FindingFacets,
20820
- items: external_exports.array(FindingGroup),
20886
+ items: external_exports.array(FindingTypeSummary),
20821
20887
  nextCursor: external_exports.string().nullable(),
20822
20888
  // Present only on session-scoped queries (`sessionId` set): per ruleId, how
20823
20889
  // many times that rule fired in the session's persisted transcript. Findings
@@ -20825,7 +20891,7 @@ var ListGroupedFindingsResponse = external_exports.object({
20825
20891
  // every firing, so the two numbers legitimately differ — this map lets a
20826
20892
  // session-scoped view show both.
20827
20893
  sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
20828
- }).meta({ id: "ListGroupedFindingsResponse" });
20894
+ }).meta({ id: "ListFindingTypesResponse" });
20829
20895
  var ApplyFindingActionRequest = external_exports.object({
20830
20896
  // 'quarantined' is system-assigned (see FindingAction) — clients may not set
20831
20897
  // it, so it is excluded from the request contract. The mapping helper
@@ -20855,16 +20921,19 @@ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
20855
20921
  var MAX_FLAT_FINDINGS_LIMIT = 200;
20856
20922
  var ListFindingInstancesQuery = external_exports.object({
20857
20923
  severity: external_exports.array(Severity).optional(),
20858
- // Rule ids, the same vocabulary the grouped list's `subtype` carries.
20924
+ // Rule ids, the same vocabulary the types list's `subtype` carries. Pinning
20925
+ // ONE of them is how the master/detail view scopes its right-hand panel.
20859
20926
  subtype: external_exports.array(external_exports.string()).optional(),
20860
20927
  provider: external_exports.array(FindingProvider).optional(),
20861
20928
  action: external_exports.array(FindingAction).optional(),
20862
20929
  // Matches each instance's OWN derived status (deriveFindingStatus), unlike
20863
- // the grouped query's group-level fold.
20930
+ // the types query's type-level fold.
20864
20931
  status: external_exports.array(FindingStatus).optional(),
20865
20932
  // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
20866
20933
  // where the free-text `q` can only match the rendered "via Bash" label.
20867
20934
  tool: external_exports.array(external_exports.string()).optional(),
20935
+ // The delivery state of each finding's event (see FindingDelivery).
20936
+ deployment: external_exports.array(FindingDeliveryState).optional(),
20868
20937
  // Exact repository / file-path matches, for the drill-down out of the
20869
20938
  // locations view. A row whose event carries no repo/file matches neither.
20870
20939
  repo: external_exports.string().optional(),
@@ -20877,37 +20946,51 @@ var ListFindingInstancesQuery = external_exports.object({
20877
20946
  });
20878
20947
  var ListFindingInstancesResponse = external_exports.object({
20879
20948
  // Instances matching the filters across the whole scope, not just this
20880
- // page — cursor-independent, like the grouped list's totals.
20949
+ // page — cursor-independent, like the types list's totals.
20881
20950
  totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
20882
- // Counts in INSTANCES here, where the grouped response counts groups. Each
20951
+ // Counts in INSTANCES here, where the types response counts types. Each
20883
20952
  // dimension still excludes its own filter.
20884
20953
  facets: FindingFacets,
20885
20954
  items: external_exports.array(FindingInstanceDetail),
20886
20955
  nextCursor: external_exports.string().nullable()
20887
20956
  }).meta({ id: "ListFindingInstancesResponse" });
20888
- var FindingLocationFile = external_exports.object({
20889
- // Empty when the instances carried no file path (a prompt or a tool call
20890
- // with no file attribution).
20891
- file: external_exports.string(),
20892
- instanceCount: external_exports.number().int().nonnegative(),
20893
- maxSeverity: Severity,
20894
- latestDetectedAt: external_exports.iso.datetime(),
20895
- // Folded from the instances' derived statuses with the same
20896
- // open-dominates precedence a group uses.
20897
- status: FindingStatus.optional(),
20898
- // Distinct rules seen at this location, capped — the row shows them as
20899
- // chips, and the count is what conveys scale.
20900
- ruleIds: external_exports.array(external_exports.string())
20901
- }).meta({ id: "FindingLocationFile" });
20902
- var FindingLocationRepo = external_exports.object({
20957
+ var ListFindingInstancesPage = external_exports.object({
20958
+ items: external_exports.array(FindingInstanceDetail),
20959
+ nextCursor: external_exports.string().nullable()
20960
+ }).meta({ id: "ListFindingInstancesPage" });
20961
+ var FindingLocationSummary = external_exports.object({
20962
+ // Opaque, stable, minted from the pair by encodeLocationId. It exists
20963
+ // because a location's identity is two values and a URL param carries one:
20964
+ // `?loc=` names a location the way `?rule=` names a type. Only ever compared
20965
+ // for EQUALITY — the page's selection check, this read's `includeId`, the
20966
+ // client's page dedupe — never decoded, and never a sort key.
20967
+ id: external_exports.string(),
20903
20968
  /** Empty when the instances carried no repo attribute. */
20904
20969
  repo: external_exports.string(),
20970
+ // Empty when the instances carried no file path (a prompt, or a tool call
20971
+ // with no file attribution). Both halves empty is a real location — usually
20972
+ // the largest one in a store — and is selectable like any other.
20973
+ file: external_exports.string(),
20905
20974
  instanceCount: external_exports.number().int().nonnegative(),
20975
+ // The WORST severity present, not the first row's. It is this list's primary
20976
+ // sort key, so it is also what explains why a row is where it is, and it is
20977
+ // how a reader decides what to open without opening everything.
20906
20978
  maxSeverity: Severity,
20907
20979
  latestDetectedAt: external_exports.iso.datetime(),
20980
+ // Folded from the instances' derived statuses with the same open-dominates
20981
+ // precedence a group uses, so it answers "is anything left to do here" and
20982
+ // not much more: a location holding 1 open among 40 resolved reads like one
20983
+ // holding 40 open. That loss is accepted — the panel beside this list
20984
+ // carries each finding's own status, and instanceCount sits next to the
20985
+ // badge.
20908
20986
  status: FindingStatus.optional(),
20909
- files: external_exports.array(FindingLocationFile)
20910
- }).meta({ id: "FindingLocationRepo" });
20987
+ // Every distinct rule seen at this location, UNCAPPED — so the length is a
20988
+ // tally rather than a sample and a row can say how many there are. Bounded
20989
+ // by the ruleset, not by the store. The view bounds what it DISPLAYS.
20990
+ ruleIds: external_exports.array(external_exports.string())
20991
+ }).meta({ id: "FindingLocationSummary" });
20992
+ var DEFAULT_FINDING_LOCATIONS_LIMIT = 50;
20993
+ var MAX_FINDING_LOCATIONS_LIMIT = 100;
20911
20994
  var ListFindingLocationsQuery = external_exports.object({
20912
20995
  severity: external_exports.array(Severity).optional(),
20913
20996
  subtype: external_exports.array(external_exports.string()).optional(),
@@ -20917,21 +21000,47 @@ var ListFindingLocationsQuery = external_exports.object({
20917
21000
  // instances that match, and folds its status from those.
20918
21001
  status: external_exports.array(FindingStatus).optional(),
20919
21002
  tool: external_exports.array(external_exports.string()).optional(),
21003
+ // The delivery state of each finding's event (see FindingDelivery).
21004
+ deployment: external_exports.array(FindingDeliveryState).optional(),
20920
21005
  q: external_exports.string().optional(),
20921
21006
  sessionId: external_exports.string().optional(),
20922
21007
  from: external_exports.iso.datetime().optional(),
20923
- limit: external_exports.coerce.number().int().min(1).max(500).optional()
21008
+ // A LOCATION id (see FindingLocationSummary.id) that must appear in the page
21009
+ // even when the cursor has already advanced past its sort position — the
21010
+ // counterpart of ListFindingTypesQuery.includeId, and needed far more often
21011
+ // here. Selecting a row pushes the URL, which re-renders the server and resets
21012
+ // the client's page cache to page 0; with distinct (repo, file) pairs running
21013
+ // into the thousands, a selection sitting off page 0 is the ordinary case
21014
+ // rather than a deep-link corner. Never affects totals, facets or the cursor.
21015
+ includeId: external_exports.string().optional(),
21016
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_LOCATIONS_LIMIT).optional(),
21017
+ cursor: external_exports.string().optional()
20924
21018
  });
20925
21019
  var ListFindingLocationsResponse = external_exports.object({
20926
21020
  totals: external_exports.object({
21021
+ // Findings matching the filters across the whole scope. Unlike the types
21022
+ // read's same-named field this needs no caveat: the filters here narrow
21023
+ // per finding, so this is the sum of every row's instanceCount.
20927
21024
  findings: external_exports.number().int().nonnegative(),
20928
- repos: external_exports.number().int().nonnegative(),
20929
- files: external_exports.number().int().nonnegative()
21025
+ // Counts LOCATIONS, the unit this read pages — the number the paginator
21026
+ // states. The facets beside it count FINDINGS (see below); a surface
21027
+ // showing both says which is which.
21028
+ locations: external_exports.number().int().nonnegative()
20930
21029
  }),
20931
- /** Sorted by max severity, then most recent. */
20932
- items: external_exports.array(FindingLocationRepo),
20933
- /** Whether `limit` truncated the repo list. */
20934
- hasMore: external_exports.boolean()
21030
+ // Counts in FINDINGS, where the types response counts types, each dimension
21031
+ // still excluding its own filter. Deliberately not locations: counting those
21032
+ // needs a set of location keys per dimension per value — memory tracking the
21033
+ // store times the vocabulary, in a read whose scan promises flat memory —
21034
+ // and the cheap per-location version is not an approximation but WRONG. A
21035
+ // location holding {claudecode, block} and {codex, warn} would survive
21036
+ // provider=claudecode AND action=warn, under which no single finding
21037
+ // matches, so the facet would contradict the instanceCount this whole view
21038
+ // rests on. Findings also keep the toolbar in the same unit as the page
21039
+ // tally and the panel it sits above.
21040
+ facets: FindingFacets,
21041
+ /** Sorted by max severity, then most recent, then (repo, file). */
21042
+ items: external_exports.array(FindingLocationSummary),
21043
+ nextCursor: external_exports.string().nullable()
20935
21044
  }).meta({ id: "ListFindingLocationsResponse" });
20936
21045
 
20937
21046
  // ../../packages/schema/src/zod/meta.ts
@@ -21095,6 +21204,10 @@ var CaptureAttributes = external_exports.object({
21095
21204
  // to 'allow' — the enforcement audit trail's link back to the grant that
21096
21205
  // authorized the bypass.
21097
21206
  exception_ids: external_exports.array(external_exports.guid()).optional(),
21207
+ // The persisted spellings of EventMetadata's messageId/conversationId — the
21208
+ // join back to the `llm_call` leaf for the same assistant turn.
21209
+ message_id: external_exports.string().optional(),
21210
+ conversation_id: external_exports.string().optional(),
21098
21211
  // Whole milliseconds this capture's inspection blocked its caller — the
21099
21212
  // plugin's own added latency (see EventMetadata.inspectionMs, whose value
21100
21213
  // this is). Promoted to the `inspection_ms` generated column so the facet is
@@ -21103,7 +21216,19 @@ var CaptureAttributes = external_exports.object({
21103
21216
  // inline json_extract and is not itself an optimization.
21104
21217
  // ABSENT on replayed captures (backfill / worktree scan) and on rows written
21105
21218
  // before the measurement shipped — never present as a placeholder 0.
21106
- inspection_ms: external_exports.number().int().nonnegative().optional()
21219
+ inspection_ms: external_exports.number().int().nonnegative().optional(),
21220
+ // What a `redact` this capture could not carry out became instead (see
21221
+ // EventMetadata.redactDegradedTo, whose value this is). Present only when a
21222
+ // degrade actually happened, so absence is the ordinary case rather than a
21223
+ // reader having to distinguish it from a zero.
21224
+ //
21225
+ // PER CAPTURE, while `inspection_findings.action_taken` is per finding —
21226
+ // so on a multi-finding row this does not say which finding degraded, and
21227
+ // its presence does not mean the fallback decided the capture's action. A
21228
+ // capture denied by another finding's own Block policy carries `block`
21229
+ // here too. The full statement is on EventMetadata.redactDegradedTo; it is
21230
+ // repeated rather than referenced because a store reader opens this file.
21231
+ redact_degraded_to: ActionTaken.optional()
21107
21232
  }).catchall(external_exports.unknown());
21108
21233
  var ToolCallInspection = external_exports.object({
21109
21234
  ruleId: external_exports.string().min(1),
@@ -21302,7 +21427,17 @@ var AuditEvent = external_exports.object({
21302
21427
  /** `share` to a first-party/internal destination. */
21303
21428
  internal: external_exports.boolean(),
21304
21429
  /** Event needs review (e.g. unverified egress). */
21305
- flagged: external_exports.boolean()
21430
+ flagged: external_exports.boolean(),
21431
+ /**
21432
+ * The body this event's `title` is drawn from was cleared by local body
21433
+ * expiry, so an EMPTY title here means "gone", not "never had one".
21434
+ *
21435
+ * A separate flag rather than a sentinel written into `title`: the title is
21436
+ * rendered text, and a store-layer module that invented display copy for it
21437
+ * would be choosing words the view is supposed to choose. Additive and
21438
+ * defaulted, so an older producer still validates.
21439
+ */
21440
+ bodyExpired: external_exports.boolean().default(false)
21306
21441
  }).meta({ id: "ActivityAuditEvent" });
21307
21442
  var ActivitySessionSummary = external_exports.object({
21308
21443
  id: external_exports.string(),
@@ -22100,6 +22235,14 @@ var ControlPlaneErrorBody = external_exports.object({
22100
22235
  message: external_exports.string().optional()
22101
22236
  }).optional()
22102
22237
  });
22238
+ var RemoteFailureKind = external_exports.enum([
22239
+ "unauthorized",
22240
+ "forbidden",
22241
+ "route-absent",
22242
+ "invalid-request",
22243
+ "rejected",
22244
+ "unreachable"
22245
+ ]);
22103
22246
  var AttachDeviceRequest = external_exports.object({
22104
22247
  // This machine's own continuity id, so re-attaching ROTATES the credential
22105
22248
  // on one machine record instead of producing a second one. Client-minted
@@ -22635,6 +22778,12 @@ var EventMetadata = external_exports.object({
22635
22778
  // to 'allow' — the enforcement audit trail's link back to the grant that
22636
22779
  // authorized the bypass. Absent on captures where no exception applied.
22637
22780
  exceptionIds: external_exports.array(external_exports.guid()).optional(),
22781
+ // The assistant message this capture belongs to, and the conversation it sits
22782
+ // in — set by the browser extension's network capture so a stored `response`
22783
+ // row can be joined to the `llm_call` leaf describing the same turn. Absent
22784
+ // on every other capture path, which has no such id.
22785
+ messageId: external_exports.string().optional(),
22786
+ conversationId: external_exports.string().optional(),
22638
22787
  // How long THIS capture's inspection blocked its caller, in whole
22639
22788
  // milliseconds — the plugin's own added latency, NOT the LLM call it sat in
22640
22789
  // front of. Measured inside `capture()` (@akasecurity/plugin-sdk) across
@@ -22647,7 +22796,37 @@ var EventMetadata = external_exports.object({
22647
22796
  // Absent is also what every pre-measurement client writes, and what a
22648
22797
  // clock failure degrades to — a reader must treat absence as "not measured"
22649
22798
  // and never as a zero, which would read as "inspection is free".
22650
- inspectionMs: external_exports.number().int().nonnegative().optional()
22799
+ inspectionMs: external_exports.number().int().nonnegative().optional(),
22800
+ // What a `redact` this capture COULD NOT CARRY OUT became instead — the
22801
+ // workspace's `redactFallback`, applied because the field could not be
22802
+ // masked in place (a shell command, a URL, or any argument on a host whose
22803
+ // hook contract offers no rewrite channel).
22804
+ //
22805
+ // It exists because the action alone cannot say why. A finding recorded as
22806
+ // `warn` reads identically whether its detection was ASSIGNED Warn or was
22807
+ // assigned Redact on a field that could not take one — and those are
22808
+ // different facts about the same row: the first is a policy the user chose,
22809
+ // the second is a masking the host could not perform. Absent means no
22810
+ // degrade happened, which is every ordinary capture.
22811
+ //
22812
+ // TWO LIMITS a reader of a stored row has to know, because the grain here
22813
+ // is the CAPTURE while `actionTaken` is per FINDING:
22814
+ //
22815
+ // - It does not say WHICH finding degraded. A capture carrying a degraded
22816
+ // `redact` alongside a finding ASSIGNED the same action stores both
22817
+ // identically and one reason for the pair; attributing it to both
22818
+ // describes the assigned one wrongly, and to neither loses the degrade.
22819
+ // - PRESENCE IS NOT CAUSATION. The value is the action the lost redact
22820
+ // became, not the reason the capture ended as it did — a capture denied
22821
+ // by some other finding's own Block policy still carries `block` here,
22822
+ // and clearing the workspace's fallback would not have let it through.
22823
+ // Gate on the value against what a fallback can produce; never read the
22824
+ // field's presence as "this was the fallback's doing".
22825
+ //
22826
+ // Both are pinned as behaviour in @akasecurity/plugin-sdk's runtime suite.
22827
+ // Closing either means moving the reason onto the finding row, which
22828
+ // already carries its own action.
22829
+ redactDegradedTo: ActionTaken.optional()
22651
22830
  }).meta({ id: "EventMetadata" });
22652
22831
  var Event = external_exports.object({
22653
22832
  id: external_exports.guid(),
@@ -22757,7 +22936,32 @@ var RotateKeyInput = external_exports.object({
22757
22936
  confirmation: external_exports.string()
22758
22937
  });
22759
22938
 
22939
+ // ../../packages/schema/src/zod/finding-delivery.ts
22940
+ var KNOWN_REASONS = SyncFailureReason.options;
22941
+ function knownReason(value) {
22942
+ return value !== null && KNOWN_REASONS.includes(value) ? value : void 0;
22943
+ }
22944
+ function deriveFindingDelivery(row) {
22945
+ if (row.kind === "code_change") return { state: "local_scan" };
22946
+ if (row.syncedAt !== null && row.syncedAt > 0) {
22947
+ return { state: "sent", at: epochMillisToIso(row.syncedAt) };
22948
+ }
22949
+ if (row.syncedAt !== null) {
22950
+ const reason = knownReason(row.syncFailure);
22951
+ return {
22952
+ state: "not_sent",
22953
+ ...row.syncFailedAt === null ? {} : { at: epochMillisToIso(row.syncFailedAt) },
22954
+ ...reason === void 0 ? {} : { reason }
22955
+ };
22956
+ }
22957
+ if (row.outboxOwed === 1 || row.syncClaimedAt !== null) return { state: "queued" };
22958
+ return { state: "never_offered" };
22959
+ }
22960
+
22760
22961
  // ../../packages/schema/src/zod/findings-group-build.ts
22962
+ function lookupOwn(map2, key) {
22963
+ return Object.hasOwn(map2, key) ? map2[key] : void 0;
22964
+ }
22761
22965
  function toApiAction(dbVal) {
22762
22966
  const map2 = {
22763
22967
  log: "monitored",
@@ -22766,7 +22970,7 @@ function toApiAction(dbVal) {
22766
22970
  warn: "warned",
22767
22971
  allow: "allowed"
22768
22972
  };
22769
- return map2[dbVal] ?? "allowed";
22973
+ return lookupOwn(map2, dbVal) ?? "allowed";
22770
22974
  }
22771
22975
  function toApiCategory(dbVal) {
22772
22976
  if (dbVal === "code_context") return "source_code";
@@ -22774,13 +22978,18 @@ function toApiCategory(dbVal) {
22774
22978
  return parsed2.success ? parsed2.data : "custom";
22775
22979
  }
22776
22980
  function toApiProvider(sourceTool) {
22777
- return TOOL_TO_HARNESS[sourceTool] ?? HARNESS.Api;
22981
+ return lookupOwn(TOOL_TO_HARNESS, sourceTool) ?? HARNESS.Api;
22778
22982
  }
22779
- var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
22983
+ var FINDING_STATUS_PRECEDENCE = [
22984
+ "open",
22985
+ "handled",
22986
+ "dismissed",
22987
+ "resolved"
22988
+ ];
22780
22989
  function foldGroupStatus(instanceStatuses) {
22781
22990
  const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
22782
22991
  if (statuses.size === 0) return void 0;
22783
- for (const candidate of STATUS_PRECEDENCE) {
22992
+ for (const candidate of FINDING_STATUS_PRECEDENCE) {
22784
22993
  if (statuses.has(candidate)) return candidate;
22785
22994
  }
22786
22995
  return void 0;
@@ -22793,139 +23002,62 @@ function deriveFindingStatus(row) {
22793
23002
  if (row.latestResolutionStatus === "dismissed") return "dismissed";
22794
23003
  return "open";
22795
23004
  }
22796
- function distinctUsers(instances) {
22797
- const seen = /* @__PURE__ */ new Set();
22798
- const users = [];
22799
- for (const i of instances) {
22800
- if (i.user === void 0 || seen.has(i.user.id)) continue;
22801
- seen.add(i.user.id);
22802
- users.push(i.user);
22803
- }
22804
- return users;
22805
- }
22806
23005
  function sortUsers(users) {
22807
23006
  return [...users].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
22808
23007
  }
22809
- function buildFindingGroups(rows, opts = {}) {
22810
- const overrides = opts.overrides;
23008
+ function buildFindingTypes(aggregates, opts = {}) {
22811
23009
  const packNames = opts.packNames;
22812
- const aggregates = opts.aggregates;
22813
- const byRuleId = /* @__PURE__ */ new Map();
22814
- for (const row of rows) {
22815
- const existing = byRuleId.get(row.ruleId);
22816
- if (existing) existing.push(row);
22817
- else byRuleId.set(row.ruleId, [row]);
22818
- }
22819
- const groups = [];
22820
- for (const [ruleId, ruleRows] of byRuleId) {
22821
- const instances = ruleRows.map((r) => {
22822
- const effectiveDbAction = overrides?.get(r.id) ?? r.actionTaken;
22823
- return {
22824
- id: r.id,
22825
- provider: toApiProvider(r.sourceTool),
22826
- repo: r.repo,
22827
- file: r.file,
22828
- ...r.toolName === void 0 ? {} : { toolName: r.toolName },
22829
- ...r.eventId === void 0 ? {} : { eventId: r.eventId },
22830
- ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
22831
- ...r.user === void 0 ? {} : { user: r.user },
22832
- action: toApiAction(effectiveDbAction),
22833
- detectedAt: r.occurredAt,
22834
- confidence: r.confidence,
22835
- status: r.status
22836
- };
22837
- });
22838
- const agg = aggregates?.get(ruleId);
22839
- const users = agg ? sortUsers(agg.users ?? []) : distinctUsers(instances);
22840
- const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
22841
- (max, r) => r.occurredAt > max ? r.occurredAt : max,
22842
- ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
22843
- );
22844
- const seenProviders = /* @__PURE__ */ new Set();
22845
- const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
22846
- if (seenProviders.has(p)) return false;
22847
- seenProviders.add(p);
22848
- return true;
22849
- });
22850
- const actionSet = new Set(
22851
- agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
22852
- );
23010
+ const types = [];
23011
+ for (const [ruleId, agg] of aggregates) {
23012
+ const users = sortUsers(agg.users ?? []);
23013
+ const providers = [...new Set(agg.sourceTools.map(toApiProvider))].sort();
23014
+ const actionSet = new Set(agg.actionsTaken.map(toApiAction));
22853
23015
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
22854
- const severity = ruleRows[0]?.severity ?? "low";
22855
- const detection = {
22856
- id: ruleId,
22857
- name: packNames?.get(ruleId) ?? null
22858
- };
22859
- const apiCategory = toApiCategory(ruleRows[0]?.category ?? "custom");
22860
- const policy = { id: `category:${apiCategory}`, name: apiCategory };
22861
- const match = {
22862
- maskedValue: ruleRows[0]?.maskedMatch ?? "",
22863
- contextPrefix: ""
22864
- // empty (pending privacy review)
22865
- };
22866
- const status = foldGroupStatus(
22867
- agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
22868
- );
22869
- const group = {
23016
+ const apiCategory = toApiCategory(agg.category ?? "custom");
23017
+ const type = {
22870
23018
  id: ruleId,
22871
23019
  category: apiCategory,
22872
23020
  subtype: ruleId,
22873
23021
  // human label comes with pack metadata later
22874
- severity,
22875
- match,
22876
- detection,
22877
- policy,
22878
- instanceCount: agg?.instanceCount ?? instances.length,
23022
+ severity: agg.severity ?? "low",
23023
+ detection: { id: ruleId, name: packNames?.get(ruleId) ?? null },
23024
+ policy: { id: `category:${apiCategory}`, name: apiCategory },
23025
+ instanceCount: agg.instanceCount,
22879
23026
  providers,
22880
23027
  aggregateAction,
22881
- latestDetectedAt,
22882
- instances,
22883
- status,
23028
+ latestDetectedAt: agg.latestDetectedAt,
23029
+ status: foldGroupStatus(agg.statusInputs.map(deriveFindingStatus)),
22884
23030
  ...users.length > 0 ? { users } : {}
22885
23031
  };
22886
- if (agg) {
22887
- actionsCache.set(group, [...actionSet]);
22888
- if (agg.searchText !== void 0) {
22889
- haystackCache.set(group, buildHaystack(group, agg.searchText));
22890
- }
23032
+ actionsCache.set(type, [...actionSet]);
23033
+ if (agg.searchText !== void 0) {
23034
+ haystackCache.set(type, buildHaystack(type, agg.searchText));
22891
23035
  }
22892
- groups.push(group);
23036
+ types.push(type);
22893
23037
  }
22894
- return groups;
23038
+ return types;
22895
23039
  }
22896
23040
  var haystackCache = /* @__PURE__ */ new WeakMap();
22897
- function buildHaystack(g, extra) {
23041
+ function buildHaystack(t, extra) {
22898
23042
  return [
22899
- g.subtype,
22900
- g.category,
22901
- g.match.maskedValue,
22902
- g.policy.name,
22903
- g.id,
22904
- ...g.instances.map((i) => i.repo),
22905
- ...g.instances.map((i) => i.file),
22906
- ...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
22907
- ...g.instances.map((i) => i.id),
22908
- // The people: the whole group's list when the store folded one, plus the
22909
- // preview's own — the two overlap, and a haystack does not mind.
22910
- ...(g.users ?? []).map((u) => u.name),
22911
- ...g.instances.map((i) => i.user?.name ?? ""),
23043
+ t.subtype,
23044
+ t.category,
23045
+ t.policy.name,
23046
+ t.id,
23047
+ ...(t.users ?? []).map((u) => u.name),
22912
23048
  ...extra === void 0 ? [] : [extra]
22913
23049
  ].join(" ").toLowerCase();
22914
23050
  }
22915
- function groupHaystack(g) {
22916
- const cached2 = haystackCache.get(g);
23051
+ function typeHaystack(t) {
23052
+ const cached2 = haystackCache.get(t);
22917
23053
  if (cached2 !== void 0) return cached2;
22918
- const haystack = buildHaystack(g);
22919
- haystackCache.set(g, haystack);
23054
+ const haystack = buildHaystack(t);
23055
+ haystackCache.set(t, haystack);
22920
23056
  return haystack;
22921
23057
  }
22922
23058
  var actionsCache = /* @__PURE__ */ new WeakMap();
22923
- function groupActions(g) {
22924
- const cached2 = actionsCache.get(g);
22925
- if (cached2 !== void 0) return cached2;
22926
- const actions = [...new Set(g.instances.map((i) => i.action))];
22927
- actionsCache.set(g, actions);
22928
- return actions;
23059
+ function typeActions(t) {
23060
+ return actionsCache.get(t) ?? [];
22929
23061
  }
22930
23062
  function countInstancesByStatus(statusInputs, statuses) {
22931
23063
  const statusSet = new Set(statuses);
@@ -22936,8 +23068,8 @@ function countInstancesByStatus(statusInputs, statuses) {
22936
23068
  }
22937
23069
  return sum;
22938
23070
  }
22939
- function applyFindingFilters(groups, opts) {
22940
- let filtered = groups;
23071
+ function applyFindingFilters(types, opts) {
23072
+ let filtered = types;
22941
23073
  if (opts.severity && opts.severity.length > 0) {
22942
23074
  const sevSet = new Set(opts.severity);
22943
23075
  filtered = filtered.filter((g) => sevSet.has(g.severity));
@@ -22948,7 +23080,7 @@ function applyFindingFilters(groups, opts) {
22948
23080
  }
22949
23081
  if (opts.actions && opts.actions.length > 0) {
22950
23082
  const actionSet = new Set(opts.actions);
22951
- filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
23083
+ filtered = filtered.filter((t) => typeActions(t).some((a) => actionSet.has(a)));
22952
23084
  }
22953
23085
  if (opts.subtype && opts.subtype.length > 0) {
22954
23086
  const subtypeSet = new Set(opts.subtype);
@@ -22960,26 +23092,31 @@ function applyFindingFilters(groups, opts) {
22960
23092
  }
22961
23093
  if (opts.q) {
22962
23094
  const q = opts.q.toLowerCase();
22963
- filtered = filtered.filter((g) => groupHaystack(g).includes(q));
23095
+ filtered = filtered.filter((t) => typeHaystack(t).includes(q));
22964
23096
  }
22965
23097
  return filtered;
22966
23098
  }
22967
- var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
22968
- var SEVERITY_RANK = SEVERITY_ORDER;
23099
+ function rankByOrder(members2) {
23100
+ return Object.fromEntries(members2.map((member, index) => [member, index]));
23101
+ }
23102
+ var SEVERITY_RANK = rankByOrder(Severity.options);
23103
+ function severityRank(severity) {
23104
+ return lookupOwn(SEVERITY_RANK, severity);
23105
+ }
22969
23106
  function compareFindingGroupOrder(a, b) {
22970
- const rankA = SEVERITY_RANK[a.severity] ?? -1;
22971
- const rankB = SEVERITY_RANK[b.severity] ?? -1;
23107
+ const rankA = severityRank(a.severity) ?? -1;
23108
+ const rankB = severityRank(b.severity) ?? -1;
22972
23109
  const severityDiff = rankA - rankB;
22973
23110
  if (severityDiff !== 0) return severityDiff;
22974
23111
  const recencyDiff = b.latestDetectedAt.localeCompare(a.latestDetectedAt);
22975
23112
  if (recencyDiff !== 0) return recencyDiff;
22976
23113
  return a.id.localeCompare(b.id);
22977
23114
  }
22978
- function sortFindingGroups(groups) {
22979
- return [...groups].sort(compareFindingGroupOrder);
23115
+ function sortFindingTypes(types) {
23116
+ return [...types].sort(compareFindingGroupOrder);
22980
23117
  }
22981
- function computeFindingFacets(allGroups, opts) {
22982
- const forSeverity = applyFindingFilters(allGroups, {
23118
+ function computeFindingFacets(allTypes, opts) {
23119
+ const forSeverity = applyFindingFilters(allTypes, {
22983
23120
  providers: opts.providers,
22984
23121
  actions: opts.actions,
22985
23122
  statuses: opts.statuses,
@@ -22990,7 +23127,7 @@ function computeFindingFacets(allGroups, opts) {
22990
23127
  for (const g of forSeverity) {
22991
23128
  severityMap.set(g.severity, (severityMap.get(g.severity) ?? 0) + 1);
22992
23129
  }
22993
- const forProvider = applyFindingFilters(allGroups, {
23130
+ const forProvider = applyFindingFilters(allTypes, {
22994
23131
  actions: opts.actions,
22995
23132
  statuses: opts.statuses,
22996
23133
  q: opts.q,
@@ -23001,7 +23138,7 @@ function computeFindingFacets(allGroups, opts) {
23001
23138
  for (const g of forProvider) {
23002
23139
  for (const p of g.providers) providerMap.set(p, (providerMap.get(p) ?? 0) + 1);
23003
23140
  }
23004
- const forAction = applyFindingFilters(allGroups, {
23141
+ const forAction = applyFindingFilters(allTypes, {
23005
23142
  providers: opts.providers,
23006
23143
  statuses: opts.statuses,
23007
23144
  q: opts.q,
@@ -23010,9 +23147,9 @@ function computeFindingFacets(allGroups, opts) {
23010
23147
  });
23011
23148
  const actionMap = /* @__PURE__ */ new Map();
23012
23149
  for (const g of forAction) {
23013
- for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
23150
+ for (const a of typeActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
23014
23151
  }
23015
- const forSubtype = applyFindingFilters(allGroups, {
23152
+ const forSubtype = applyFindingFilters(allTypes, {
23016
23153
  providers: opts.providers,
23017
23154
  actions: opts.actions,
23018
23155
  statuses: opts.statuses,
@@ -23021,7 +23158,7 @@ function computeFindingFacets(allGroups, opts) {
23021
23158
  });
23022
23159
  const subtypeMap = /* @__PURE__ */ new Map();
23023
23160
  for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
23024
- const forStatus = applyFindingFilters(allGroups, {
23161
+ const forStatus = applyFindingFilters(allTypes, {
23025
23162
  providers: opts.providers,
23026
23163
  actions: opts.actions,
23027
23164
  q: opts.q,
@@ -23043,6 +23180,20 @@ function computeFindingFacets(allGroups, opts) {
23043
23180
  }
23044
23181
 
23045
23182
  // ../../packages/schema/src/zod/findings-flat-build.ts
23183
+ function compareCodePoints(a, b) {
23184
+ const aIter = a[Symbol.iterator]();
23185
+ const bIter = b[Symbol.iterator]();
23186
+ for (; ; ) {
23187
+ const aNext = aIter.next();
23188
+ const bNext = bIter.next();
23189
+ if (aNext.done && bNext.done) return 0;
23190
+ if (aNext.done) return -1;
23191
+ if (bNext.done) return 1;
23192
+ const aPoint = aNext.value.codePointAt(0) ?? 0;
23193
+ const bPoint = bNext.value.codePointAt(0) ?? 0;
23194
+ if (aPoint !== bPoint) return aPoint - bPoint;
23195
+ }
23196
+ }
23046
23197
  function rowHaystack(row) {
23047
23198
  return [
23048
23199
  row.ruleId,
@@ -23067,12 +23218,24 @@ function matchesDimension(row, opts, dimension) {
23067
23218
  return !opts.actions?.length || opts.actions.includes(toApiAction(row.actionTaken));
23068
23219
  case "statuses":
23069
23220
  return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
23221
+ case "deliveries":
23222
+ return !opts.deliveries?.length || row.delivery !== void 0 && opts.deliveries.includes(row.delivery.state);
23070
23223
  case "tools":
23071
23224
  return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
23225
+ // An EMPTY value is a real filter here, not an absent one. The location
23226
+ // list buckets a finding whose event recorded no repo — or no file — under
23227
+ // the empty string, and selecting that bucket has to narrow the panel to
23228
+ // exactly it. Only `undefined` means "no filter"; a caller that wants every
23229
+ // row omits the key, which every call site already does.
23230
+ //
23231
+ // Reading '' as unset is what this replaced, and it failed in the one place
23232
+ // it mattered: the no-repo/no-file bucket is often the largest in a real
23233
+ // store, and its panel dropped both predicates and returned the WHOLE scope
23234
+ // — a row reading 3 findings beside a panel listing every finding there is.
23072
23235
  case "repo":
23073
- return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
23236
+ return opts.repo === void 0 || row.repo === opts.repo;
23074
23237
  case "file":
23075
- return opts.file === void 0 || opts.file === "" || row.file === opts.file;
23238
+ return opts.file === void 0 || row.file === opts.file;
23076
23239
  case "q":
23077
23240
  return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
23078
23241
  }
@@ -23083,6 +23246,7 @@ var DIMENSIONS = [
23083
23246
  "providers",
23084
23247
  "actions",
23085
23248
  "statuses",
23249
+ "deliveries",
23086
23250
  "tools",
23087
23251
  "repo",
23088
23252
  "file",
@@ -23096,10 +23260,19 @@ function matchesInstanceFilters(row, opts, except) {
23096
23260
  return true;
23097
23261
  }
23098
23262
  function toItems(counts) {
23099
- return [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
23263
+ return [...counts.entries()].map(([value, count]) => ({ value, count })).sort(
23264
+ (a, b) => b.count - a.count || a.value.localeCompare(b.value) || // localeCompare reports canonically-equivalent strings (an NFC and an
23265
+ // NFD spelling of the same text) as equal, so a count tie between
23266
+ // them would otherwise be ordered by whichever the Map iteration
23267
+ // produced. compareCodePoints breaks that tie deterministically, which
23268
+ // makes this a TOTAL order — not one that agrees with SQL collation,
23269
+ // which it need not: foldFacetTuples runs this same sort over grouped
23270
+ // tuples, so both paths order facets identically by construction.
23271
+ compareCodePoints(a.value, b.value)
23272
+ );
23100
23273
  }
23101
- function bump(counts, value) {
23102
- counts.set(value, (counts.get(value) ?? 0) + 1);
23274
+ function bump(counts, value, by = 1) {
23275
+ counts.set(value, (counts.get(value) ?? 0) + by);
23103
23276
  }
23104
23277
  function createInstanceFacetAccumulator(opts) {
23105
23278
  const severity = /* @__PURE__ */ new Map();
@@ -23108,6 +23281,7 @@ function createInstanceFacetAccumulator(opts) {
23108
23281
  const action = /* @__PURE__ */ new Map();
23109
23282
  const status = /* @__PURE__ */ new Map();
23110
23283
  const tool = /* @__PURE__ */ new Map();
23284
+ const deployment = /* @__PURE__ */ new Map();
23111
23285
  return {
23112
23286
  add(row) {
23113
23287
  if (matchesInstanceFilters(row, opts, "severity")) bump(severity, row.severity);
@@ -23122,6 +23296,9 @@ function createInstanceFacetAccumulator(opts) {
23122
23296
  if (row.toolName !== void 0 && matchesInstanceFilters(row, opts, "tools")) {
23123
23297
  bump(tool, row.toolName);
23124
23298
  }
23299
+ if (row.delivery !== void 0 && matchesInstanceFilters(row, opts, "deliveries")) {
23300
+ bump(deployment, row.delivery.state);
23301
+ }
23125
23302
  },
23126
23303
  facets: () => ({
23127
23304
  severity: toItems(severity),
@@ -23129,7 +23306,8 @@ function createInstanceFacetAccumulator(opts) {
23129
23306
  provider: toItems(provider),
23130
23307
  action: toItems(action),
23131
23308
  status: toItems(status),
23132
- tool: toItems(tool)
23309
+ tool: toItems(tool),
23310
+ deployment: toItems(deployment)
23133
23311
  })
23134
23312
  };
23135
23313
  }
@@ -23143,6 +23321,7 @@ function toInstanceDetail(row) {
23143
23321
  ...row.toolName === void 0 ? {} : { toolName: row.toolName },
23144
23322
  eventId: row.eventId,
23145
23323
  ...row.sessionId === void 0 ? {} : { sessionId: row.sessionId },
23324
+ ...row.delivery === void 0 ? {} : { delivery: row.delivery },
23146
23325
  ...row.user === void 0 ? {} : { user: row.user },
23147
23326
  action: toApiAction(row.actionTaken),
23148
23327
  detectedAt: row.occurredAt,
@@ -23157,12 +23336,6 @@ function toInstanceDetail(row) {
23157
23336
  policy: { id: `category:${category}`, name: category }
23158
23337
  };
23159
23338
  }
23160
- var SEVERITY_ORDER2 = {
23161
- critical: 0,
23162
- high: 1,
23163
- medium: 2,
23164
- low: 3
23165
- };
23166
23339
  function newLocationAccumulator() {
23167
23340
  return {
23168
23341
  instanceCount: 0,
@@ -23177,7 +23350,7 @@ function newLocationAccumulator() {
23177
23350
  }
23178
23351
  function addToLocation(acc, row) {
23179
23352
  acc.instanceCount += 1;
23180
- const rank = SEVERITY_ORDER2[row.severity] ?? Number.MAX_SAFE_INTEGER - 1;
23353
+ const rank = severityRank(row.severity) ?? Number.MAX_SAFE_INTEGER - 1;
23181
23354
  if (rank < acc.maxSeverityRank) {
23182
23355
  acc.maxSeverityRank = rank;
23183
23356
  acc.maxSeverity = row.severity;
@@ -23186,6 +23359,23 @@ function addToLocation(acc, row) {
23186
23359
  acc.statuses.push(row.status);
23187
23360
  acc.ruleIds.add(row.ruleId);
23188
23361
  }
23362
+ function compareLocationOrder(a, b) {
23363
+ const rankA = severityRank(a.maxSeverity) ?? -1;
23364
+ const rankB = severityRank(b.maxSeverity) ?? -1;
23365
+ if (rankA !== rankB) return rankA - rankB;
23366
+ if (a.latestDetectedAt !== b.latestDetectedAt) {
23367
+ return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
23368
+ }
23369
+ const repoDiff = compareCodePoints(a.repo, b.repo);
23370
+ if (repoDiff !== 0) return repoDiff;
23371
+ return compareCodePoints(a.file, b.file);
23372
+ }
23373
+ function encodeLocationId(repo, file2) {
23374
+ return `${encodePart(repo)}/${encodePart(file2)}`;
23375
+ }
23376
+ function encodePart(value) {
23377
+ return encodeURIComponent(value.replace(/[\uD800-\uDFFF]/gu, "\uFFFD"));
23378
+ }
23189
23379
 
23190
23380
  // ../../packages/schema/src/zod/installed-pack.ts
23191
23381
  var InstalledPack = external_exports.object({
@@ -23253,6 +23443,11 @@ var Policy = external_exports.object({
23253
23443
  // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23254
23444
  provenance: PolicyProvenance.optional()
23255
23445
  }).meta({ id: "Policy" });
23446
+ var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23447
+ var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23448
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23449
+ id: "RedactFallback"
23450
+ });
23256
23451
  var PolicyBundle = external_exports.object({
23257
23452
  version: external_exports.string(),
23258
23453
  policies: external_exports.array(Policy),
@@ -23300,6 +23495,16 @@ var PolicyBundle = external_exports.object({
23300
23495
  // control plane), so no name resolution stands between the decision and the
23301
23496
  // comparison.
23302
23497
  prohibitedModels: external_exports.array(external_exports.string()).optional(),
23498
+ // What a resolved `redact` becomes on a field the host cannot rewrite, as
23499
+ // the ORGANIZATION would have it. Merged raise-only against the device's own
23500
+ // `WorkspaceSettings.redactFallback` (see strongerRedactFallback below), so
23501
+ // a control plane can tighten a machine and never loosen one — the same
23502
+ // direction `mergeRaiseOnly` enforces for policies.
23503
+ //
23504
+ // Optional so an older backend, and an older on-disk cache, still parses;
23505
+ // absent leaves the device's own setting in force, which is the behaviour
23506
+ // that predates the field and the safe direction to default.
23507
+ redactFallback: RedactFallback.optional(),
23303
23508
  customKeywords: external_exports.array(external_exports.string()),
23304
23509
  fetchedAt: external_exports.iso.datetime()
23305
23510
  }).meta({ id: "PolicyBundle" });
@@ -23329,11 +23534,6 @@ function severityFloorPolicy(category) {
23329
23534
  const peak = CATEGORY_PEAK_SEVERITY[category];
23330
23535
  return peak === "critical" || peak === "high" ? "warn" : "monitor";
23331
23536
  }
23332
- var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23333
- var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23334
- var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23335
- id: "RedactFallback"
23336
- });
23337
23537
  var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23338
23538
  var BUILTIN_POLICY_SPECS = {
23339
23539
  monitor: {
@@ -23389,6 +23589,11 @@ function isActionAtLeast(action, floor) {
23389
23589
  function strongerAction(a, b) {
23390
23590
  return actionRank(a) >= actionRank(b) ? a : b;
23391
23591
  }
23592
+ function strongerRedactFallback(local, remote) {
23593
+ if (remote === void 0) return local;
23594
+ const localAction = builtinPolicyToAction(local);
23595
+ return localAction === strongerAction(localAction, builtinPolicyToAction(remote)) ? local : remote;
23596
+ }
23392
23597
  function weakestBuiltinAtLeast(floor) {
23393
23598
  return KNOWN_BUILTIN_IDS.find((id) => isActionAtLeast(builtinPolicyToAction(id), floor)) ?? "block";
23394
23599
  }
@@ -23637,7 +23842,7 @@ function isVaultConsentValid(consent) {
23637
23842
  }
23638
23843
 
23639
23844
  // ../../packages/schema/src/zod/local.ts
23640
- var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23845
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 8;
23641
23846
  var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23642
23847
  var RunMode = external_exports.enum(["standalone", "attached"]);
23643
23848
  var ControlPlaneConnection = external_exports.object({
@@ -23657,6 +23862,15 @@ var HistorySyncConsent = external_exports.object({
23657
23862
  payloadVersion: external_exports.number().int().positive(),
23658
23863
  endpoint: external_exports.string()
23659
23864
  });
23865
+ var BODY_RETENTION_DEFAULT_DAYS = 30;
23866
+ var BodyRetention = external_exports.object({
23867
+ enabled: external_exports.boolean().default(false),
23868
+ // Never 0, and the ceiling is a fat-finger guard rather than a policy
23869
+ // limit — `enabled` is the real gate. A low value cannot reach a row the
23870
+ // sync ledger still owes: the sweep's age filter only ever NARROWS a
23871
+ // candidate set that is already bounded by "delivered, or never owed".
23872
+ retainDays: external_exports.number().int().min(1).max(3650).default(BODY_RETENTION_DEFAULT_DAYS)
23873
+ }).meta({ id: "BodyRetention" });
23660
23874
  var WorkspaceSettings = external_exports.object({
23661
23875
  specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
23662
23876
  runMode: RunMode.default("standalone"),
@@ -23700,12 +23914,18 @@ var WorkspaceSettings = external_exports.object({
23700
23914
  // covers the current payload and must be re-granted.
23701
23915
  modelJudgeConsent: ModelJudgeConsent.optional(),
23702
23916
  // Records that the user consented to the DEFERRED send — the outbox — along
23703
- // with the payload shape and the endpoint they agreed to. Since payload v2
23704
- // that covers both the pre-attach backlog and undelivered captures (which
23705
- // carry prompt/reply text in `content`); the key name predates the widening.
23706
- // Absent until granted, and a grant for a different endpoint or an older
23707
- // payload no longer counts.
23708
- historySyncConsent: HistorySyncConsent.optional()
23917
+ // with the payload shape and the endpoint they agreed to. Since payload v3
23918
+ // that covers the pre-attach backlog AND undelivered captures alike, and both
23919
+ // carry prompt/reply/tool-output text in `content`; the key name predates
23920
+ // both widenings. Absent until granted, and a grant for a different endpoint
23921
+ // or an older payload no longer counts.
23922
+ historySyncConsent: HistorySyncConsent.optional(),
23923
+ // Local body expiry (see BodyRetention). Off until switched on; expiring a
23924
+ // body never removes the row or its findings.
23925
+ bodyRetention: BodyRetention.default({
23926
+ enabled: false,
23927
+ retainDays: BODY_RETENTION_DEFAULT_DAYS
23928
+ })
23709
23929
  });
23710
23930
  function defaultWorkspaceSettings() {
23711
23931
  return WorkspaceSettings.parse({});
@@ -23800,12 +24020,15 @@ function toCaptureAttributes(event) {
23800
24020
  ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
23801
24021
  ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
23802
24022
  ...metadata?.inspectionMs !== void 0 ? { inspection_ms: metadata.inspectionMs } : {},
24023
+ ...metadata?.redactDegradedTo !== void 0 ? { redact_degraded_to: metadata.redactDegradedTo } : {},
23803
24024
  // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
23804
24025
  // has ever populated either), but every legacy metadata key still rides
23805
24026
  // the bag rather than being silently dropped — CaptureAttributes'
23806
24027
  // `.catchall(z.unknown())` carries the long tail.
23807
24028
  ...metadata?.model !== void 0 ? { model: metadata.model } : {},
23808
- ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
24029
+ ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {},
24030
+ ...metadata?.messageId !== void 0 ? { message_id: metadata.messageId } : {},
24031
+ ...metadata?.conversationId !== void 0 ? { conversation_id: metadata.conversationId } : {}
23809
24032
  };
23810
24033
  }
23811
24034
  function captureDefinitionVersion(finding) {
@@ -23833,10 +24056,22 @@ var ManagedSettingKey = external_exports.enum([
23833
24056
  "vaultInlineReveal",
23834
24057
  "modelJudgeConsent",
23835
24058
  "dataSharesInPlace",
23836
- "redactFallback"
24059
+ "redactFallback",
24060
+ // Pins the toggle and the day count together — see BodyRetention on why the
24061
+ // two are one unit. An administrator mandating a window wants the count
24062
+ // enforced with it, not one a user can widen while the toggle stays on.
24063
+ "bodyRetention"
23837
24064
  ]).meta({ id: "ManagedSettingKey" });
24065
+ function isManagedSettingKey(value) {
24066
+ return ManagedSettingKey.safeParse(value).success;
24067
+ }
23838
24068
  var ManagedSettingsValues = external_exports.object({
23839
24069
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
24070
+ // `controlPlane` and `bodyRetention` are the two nested values, and both are
24071
+ // plain, non-strict objects: a key under either that this build does not know
24072
+ // is stripped and nothing reports it. The unknown-value split in
24073
+ // ManagedSettings below classifies top-level names only, so it stops at
24074
+ // these boundaries.
23840
24075
  controlPlane: external_exports.object({
23841
24076
  endpoint: external_exports.string().min(1),
23842
24077
  label: external_exports.string().min(1).optional()
@@ -23847,7 +24082,8 @@ var ManagedSettingsValues = external_exports.object({
23847
24082
  vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23848
24083
  modelJudgeConsent: external_exports.boolean().optional(),
23849
24084
  dataSharesInPlace: external_exports.boolean().optional(),
23850
- redactFallback: RedactFallback.optional()
24085
+ redactFallback: RedactFallback.optional(),
24086
+ bodyRetention: BodyRetention.optional()
23851
24087
  }).meta({ id: "ManagedSettingsValues" });
23852
24088
  var ManagedSettings = external_exports.object({
23853
24089
  specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
@@ -23855,11 +24091,59 @@ var ManagedSettings = external_exports.object({
23855
24091
  // decision from a bug. Absent renders as a generic "your organization".
23856
24092
  organization: external_exports.string().min(1).optional(),
23857
24093
  // What the administrator pinned.
23858
- values: ManagedSettingsValues.default({}),
24094
+ //
24095
+ // Parsed as a RECORD rather than as the nested schema, and split below for
24096
+ // the same reason `lockedFields` is parsed as names: a plain `z.object`
24097
+ // drops an unrecognised key and succeeds, so a pin this build does not know
24098
+ // vanished and nothing anywhere said so. A pin with no lock is a supported
24099
+ // shape — it is a DEFAULT the user may still change — so that silence hit
24100
+ // exactly the file an administrator is most likely to write while a fleet
24101
+ // is mid-upgrade.
24102
+ //
24103
+ // Splitting here rather than calling `.strict()`: strict would REFUSE the
24104
+ // file, which is the outcome the lock half already rejected — an older
24105
+ // build then runs entirely unmanaged, every pin and lock gone. A bad KNOWN
24106
+ // value still fails, because the nested schema is re-run over the known
24107
+ // subset and its issues are re-raised on this parse.
24108
+ values: external_exports.record(external_exports.string(), external_exports.unknown()).default({}),
23859
24109
  // Which of those the user may not change. A key here with no matching value
23860
24110
  // freezes whatever the user last chose; a value with no lock is a DEFAULT
23861
24111
  // the user may still override. The two are separable on purpose.
23862
- lockedFields: external_exports.array(ManagedSettingKey).default([])
24112
+ //
24113
+ // Parsed as NAMES rather than as the enum, and split below: a name this
24114
+ // build does not know is dropped from the locked set and reported, never a
24115
+ // reason to refuse the file. The same shape reaches an older build whenever
24116
+ // an administrator locks a key a newer build added, and refusing it there
24117
+ // ran that build entirely unmanaged — every pin and lock gone — on exactly
24118
+ // the fleets most likely to carry a version skew. A name outside the enum
24119
+ // is still never HONOURED: the lockable set stays explicit above.
24120
+ lockedFields: external_exports.array(external_exports.string()).default([])
24121
+ }).transform(({ lockedFields, values, ...rest }, ctx) => {
24122
+ const known = [];
24123
+ const unknown2 = [];
24124
+ for (const name of lockedFields) {
24125
+ if (isManagedSettingKey(name)) known.push(name);
24126
+ else unknown2.push(name);
24127
+ }
24128
+ const knownValues = /* @__PURE__ */ Object.create(null);
24129
+ const unknownValues = [];
24130
+ for (const [name, value] of Object.entries(values)) {
24131
+ if (Object.hasOwn(ManagedSettingsValues.shape, name)) knownValues[name] = value;
24132
+ else unknownValues.push(name);
24133
+ }
24134
+ const pinned = ManagedSettingsValues.safeParse(knownValues);
24135
+ if (!pinned.success) {
24136
+ for (const issue2 of pinned.error.issues)
24137
+ ctx.addIssue({ ...issue2, path: ["values", ...issue2.path] });
24138
+ return external_exports.NEVER;
24139
+ }
24140
+ return {
24141
+ ...rest,
24142
+ values: pinned.data,
24143
+ lockedFields: known,
24144
+ ...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {},
24145
+ ...unknownValues.length > 0 ? { unknownValueFields: unknownValues } : {}
24146
+ };
23863
24147
  }).meta({ id: "ManagedSettings" });
23864
24148
 
23865
24149
  // ../../packages/schema/src/zod/project-files.ts
@@ -23983,7 +24267,11 @@ var FindingsTimeseriesPoint = external_exports.object({
23983
24267
  timestamp: external_exports.iso.date(),
23984
24268
  critical: external_exports.number().int().nonnegative(),
23985
24269
  high: external_exports.number().int().nonnegative(),
23986
- medium: external_exports.number().int().nonnegative()
24270
+ medium: external_exports.number().int().nonnegative(),
24271
+ // Optional and additive, so a producer written against the earlier
24272
+ // three-series contract keeps validating. A consumer plotting it resolves the
24273
+ // absent case itself — the chart point requires a number.
24274
+ low: external_exports.number().int().nonnegative().optional()
23987
24275
  }).meta({ id: "FindingsTimeseriesPoint" });
23988
24276
  var FindingsTimeseriesResponse = external_exports.object({
23989
24277
  range: TimeRange,
@@ -24009,6 +24297,10 @@ var ResolvedFeedItem = external_exports.object({
24009
24297
  findingKey: external_exports.string(),
24010
24298
  ruleId: external_exports.string(),
24011
24299
  severity: Severity,
24300
+ // Repository slug, and the file path RELATIVE to it. The pair is what
24301
+ // identifies the file: a bare path matches the same name in every repo.
24302
+ // Optional and additive; empty when the event carried no repo.
24303
+ repo: external_exports.string().optional(),
24012
24304
  path: external_exports.string(),
24013
24305
  // ISO-8601 datetime (matches FindingInstance.detectedAt / the rest of the
24014
24306
  // findings domain). The reader `.toISOString()`s the DB epoch-ms values.
@@ -24114,7 +24406,23 @@ var SaveSettingsInput = external_exports.object({
24114
24406
  modelJudgeConsent: ModelJudgeConsentChoice,
24115
24407
  historySyncConsent: HistorySyncConsentChoice,
24116
24408
  vaultConsent: external_exports.string(),
24117
- vaultInlineReveal: external_exports.string()
24409
+ vaultInlineReveal: external_exports.string(),
24410
+ // Widened to `string` like its neighbours rather than typed as
24411
+ // `RedactFallback`, on this module's own layering rule: shape here, VALUE at
24412
+ // the call site, so the domain check receives the type it was written for.
24413
+ //
24414
+ // NOT because a narrower schema would reject differently. `parseActionInput`
24415
+ // is a `safeParse` wrapper and throws for no field schema, so either spelling
24416
+ // reaches a recoverable `{ ok: false }` and there is no rejected promise to
24417
+ // trade against. The real cost runs the other way and is the part worth
24418
+ // knowing: a value this schema admits and the domain enum then rejects lands
24419
+ // on the action's shared refusal, which names NO field, where a shape
24420
+ // rejection reaches `malformedInput` and names the schema key.
24421
+ redactFallback: external_exports.string(),
24422
+ // Shape only, the way the enum fields above are strings only: the RANGE is
24423
+ // `BodyRetention`'s and the action checks it there, so there is one place
24424
+ // that decides what a legal horizon is rather than two that can drift.
24425
+ bodyRetention: external_exports.object({ enabled: external_exports.boolean(), retainDays: external_exports.number() })
24118
24426
  });
24119
24427
  var AttachInput = external_exports.object({
24120
24428
  endpoint: external_exports.string(),
@@ -24286,6 +24594,52 @@ function reviewSeverityRank(reasons) {
24286
24594
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
24287
24595
  }
24288
24596
 
24597
+ // ../../packages/schema/src/zod/web-capture.ts
24598
+ var WebUsageSource = external_exports.enum(["site", "estimated", "none"]);
24599
+ var WebUsage = external_exports.object({
24600
+ inputTokens: external_exports.number().int().nonnegative().optional(),
24601
+ outputTokens: external_exports.number().int().nonnegative().optional(),
24602
+ cacheReadInputTokens: external_exports.number().int().nonnegative().optional(),
24603
+ cacheCreationInputTokens: external_exports.number().int().nonnegative().optional()
24604
+ });
24605
+ var WebToolCall = external_exports.object({
24606
+ toolUseId: external_exports.string().min(1),
24607
+ toolName: external_exports.string().min(1),
24608
+ target: external_exports.string().optional(),
24609
+ isError: external_exports.boolean().optional(),
24610
+ inputSize: external_exports.number().int().nonnegative().optional(),
24611
+ outputSize: external_exports.number().int().nonnegative().optional()
24612
+ });
24613
+ var WebExchange = external_exports.object({
24614
+ messageId: external_exports.string().min(1),
24615
+ startedAt: external_exports.iso.datetime(),
24616
+ model: external_exports.string().optional(),
24617
+ usage: WebUsage.optional(),
24618
+ usageSource: WebUsageSource,
24619
+ stopReason: external_exports.string().optional(),
24620
+ conversationId: external_exports.string().optional(),
24621
+ turnIndex: external_exports.number().int().nonnegative().optional(),
24622
+ toolCalls: external_exports.array(WebToolCall).default([]),
24623
+ // Absent when the adapter recovered no text. Capped by the caller at
24624
+ // RESPONSE_TEXT_MAX_BYTES; `truncated` records that the cap was reached, so a
24625
+ // short capture is never mistaken for a short reply.
24626
+ responseText: external_exports.string().optional(),
24627
+ truncated: external_exports.boolean().default(false)
24628
+ });
24629
+ var RESPONSE_TEXT_MAX_BYTES = 2 * 1024 * 1024;
24630
+ var WebCaptureStatus = external_exports.object({
24631
+ patched: external_exports.boolean(),
24632
+ live: external_exports.boolean(),
24633
+ blind: external_exports.boolean(),
24634
+ sendsSeenDom: external_exports.number().int().nonnegative(),
24635
+ exchangesSeenNet: external_exports.number().int().nonnegative(),
24636
+ parseFailures: external_exports.number().int().nonnegative(),
24637
+ unparsedBodies: external_exports.number().int().nonnegative(),
24638
+ // The adapter-declared JSON key paths that were absent from a real payload —
24639
+ // the earliest signal that a site's contract moved.
24640
+ shapeMisses: external_exports.array(external_exports.string()).default([])
24641
+ });
24642
+
24289
24643
  // ../../packages/persistence/src/paths.ts
24290
24644
  import {
24291
24645
  chmodSync,
@@ -24646,6 +25000,22 @@ function discardStore(file2, backup) {
24646
25000
  }
24647
25001
  }
24648
25002
 
25003
+ // ../../packages/persistence/src/internal/sql-functions.ts
25004
+ var utf8 = new TextDecoder();
25005
+ function akaLower(value) {
25006
+ if (value === null) return null;
25007
+ if (typeof value === "string") return value.toLowerCase();
25008
+ if (typeof value === "number" || typeof value === "bigint") return String(value).toLowerCase();
25009
+ return utf8.decode(value).toLowerCase();
25010
+ }
25011
+ function registerSqlFunctions(db) {
25012
+ db.function(
25013
+ "aka_lower",
25014
+ { deterministic: true, directOnly: true, useBigIntArguments: true },
25015
+ akaLower
25016
+ );
25017
+ }
25018
+
24649
25019
  // ../../packages/persistence/src/internal/sql-text.ts
24650
25020
  function escapeLikePattern(s) {
24651
25021
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -24730,6 +25100,11 @@ function schemaObjectExists(db, kind, name) {
24730
25100
  function indexExists(db, name) {
24731
25101
  return schemaObjectExists(db, "index", name);
24732
25102
  }
25103
+ function indexColumns(db, name) {
25104
+ if (!indexExists(db, name)) return [];
25105
+ const columns = db.prepare(`PRAGMA index_info(${name})`).all();
25106
+ return columns.map((c) => c.name).filter((c) => c !== null);
25107
+ }
24733
25108
  function columnNames(db, table, opts) {
24734
25109
  const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
24735
25110
  const columns = db.prepare(`PRAGMA ${pragma}(${table})`).all();
@@ -24791,6 +25166,647 @@ function mapRowsTolerant(rows, map2) {
24791
25166
  return out;
24792
25167
  }
24793
25168
 
25169
+ // ../../packages/persistence/src/internal/outbox-lane.ts
25170
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
25171
+ var OUTBOX_CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
25172
+
25173
+ // ../../packages/persistence/src/sync-failure.ts
25174
+ var SYNC_FAILURE_REASONS = SyncFailureReason.options;
25175
+ function syncFailureRejectCondition(column = "sync_failure") {
25176
+ const members2 = SYNC_FAILURE_REASONS.map((r) => `'${r}'`).join(", ");
25177
+ return `NEW.${column} IS NOT NULL AND NEW.${column} NOT IN (${members2})`;
25178
+ }
25179
+
25180
+ // ../../packages/persistence/src/repositories/history-sync.ts
25181
+ var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
25182
+ var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
25183
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_TYPE_LIST;
25184
+ var COUNTED_EVENT_TYPES = [
25185
+ ...STRUCTURAL_EVENT_TYPES,
25186
+ ...OUTBOX_CAPTURE_EVENT_TYPES
25187
+ ];
25188
+ var COUNTED_TYPE_LIST = COUNTED_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
25189
+ var PARTITION_BUCKETS = `
25190
+ SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NULL THEN 1 ELSE 0 END) AS queued,
25191
+ SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NOT NULL THEN 1 ELSE 0 END) AS inProgress,
25192
+ SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS synced,
25193
+ SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
25194
+ AND sync_failure = 'deployment_refused' THEN 1 ELSE 0 END) AS refused,
25195
+ SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
25196
+ AND sync_failure = 'detached_undelivered' THEN 1 ELSE 0 END) AS detached,
25197
+ -- Spelled as what it INCLUDES rather than what it excludes, so a reason
25198
+ -- added later lands in no bucket and fails the sum assertion, instead
25199
+ -- of silently joining this one.
25200
+ SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
25201
+ AND (sync_failure IS NULL OR sync_failure = 'payload_invalid')
25202
+ THEN 1 ELSE 0 END) AS failed,
25203
+ COUNT(*) AS total`;
25204
+ var COUNTED_SCOPE = `
25205
+ WHERE event_type IN (${COUNTED_TYPE_LIST})
25206
+ AND (
25207
+ event_type IN (${TYPE_LIST})
25208
+ OR synced_at IS NOT NULL
25209
+ OR outbox_owed = 1
25210
+ )`;
25211
+ var SKIPPED = -1;
25212
+ var ROW_COLUMNS = `id,
25213
+ parent_id AS parentId,
25214
+ root_session_id AS rootSessionId,
25215
+ event_type AS eventType,
25216
+ host_id AS hostId,
25217
+ harness_id AS harnessId,
25218
+ source_project_id AS sourceProjectId,
25219
+ started_at AS startedAt,
25220
+ ended_at AS endedAt,
25221
+ severity,
25222
+ priority,
25223
+ content,
25224
+ content_hash AS contentHash,
25225
+ attributes`;
25226
+ var SqliteHistorySyncRepository = class {
25227
+ constructor(db) {
25228
+ this.db = db;
25229
+ this.ensureRowStmt = db.prepare(`INSERT OR IGNORE INTO history_sync (id) VALUES (1)`);
25230
+ this.sessionsStmt = db.prepare(
25231
+ `SELECT COALESCE(root_session_id, id) AS sessionId, MIN(started_at) AS earliest
25232
+ FROM audit_events
25233
+ WHERE synced_at IS NULL
25234
+ AND event_type IN (${TYPE_LIST})
25235
+ AND started_at < :before
25236
+ GROUP BY sessionId
25237
+ ORDER BY earliest
25238
+ LIMIT :limit`
25239
+ );
25240
+ this.rowsStmt = db.prepare(
25241
+ `SELECT ${ROW_COLUMNS}
25242
+ FROM audit_events
25243
+ WHERE synced_at IS NULL
25244
+ AND event_type IN (${TYPE_LIST})
25245
+ AND started_at < :before
25246
+ AND COALESCE(root_session_id, id) = :sessionId
25247
+ ORDER BY (event_type = 'session') DESC, started_at
25248
+ LIMIT :limit`
25249
+ );
25250
+ this.captureRowsStmt = db.prepare(
25251
+ `SELECT ${ROW_COLUMNS}
25252
+ FROM audit_events
25253
+ WHERE synced_at IS NULL
25254
+ AND sync_claimed_at IS NULL
25255
+ AND outbox_owed = 1
25256
+ AND event_type IN (${CAPTURE_TYPE_LIST})
25257
+ AND started_at < :before
25258
+ ORDER BY started_at
25259
+ LIMIT :limit`
25260
+ );
25261
+ this.markOwedStmt = db.prepare(
25262
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
25263
+ );
25264
+ this.markCaptureBacklogOwedStmt = db.prepare(
25265
+ `UPDATE audit_events SET outbox_owed = 1
25266
+ WHERE synced_at IS NULL
25267
+ AND event_type IN (${CAPTURE_TYPE_LIST})
25268
+ AND started_at < :before`
25269
+ );
25270
+ this.stampStmt = db.prepare(
25271
+ `UPDATE audit_events
25272
+ SET synced_at = :at,
25273
+ sync_claimed_at = NULL,
25274
+ sync_failed_at = :failedAt,
25275
+ sync_failure = :failure
25276
+ WHERE id = :id`
25277
+ );
25278
+ this.claimRowStmt = db.prepare(
25279
+ `UPDATE audit_events SET sync_claimed_at = :at WHERE id = :id AND synced_at IS NULL`
25280
+ );
25281
+ this.releaseRowStmt = db.prepare(
25282
+ `UPDATE audit_events SET sync_claimed_at = NULL WHERE id = :id`
25283
+ );
25284
+ this.releaseStaleClaimsStmt = db.prepare(
25285
+ `UPDATE audit_events SET sync_claimed_at = NULL
25286
+ WHERE sync_claimed_at IS NOT NULL AND sync_claimed_at < :staleBefore`
25287
+ );
25288
+ this.partitionStmt = db.prepare(`SELECT${PARTITION_BUCKETS}
25289
+ FROM audit_events${COUNTED_SCOPE}`);
25290
+ this.partitionByKindStmt = db.prepare(
25291
+ `SELECT event_type AS kind,${PARTITION_BUCKETS}
25292
+ FROM audit_events INDEXED BY idx_audit_events_sync${COUNTED_SCOPE}
25293
+ GROUP BY event_type`
25294
+ );
25295
+ this.countsStmt = db.prepare(
25296
+ `SELECT
25297
+ SUM(CASE WHEN synced_at IS NULL AND started_at < :before THEN 1 ELSE 0 END) AS pending,
25298
+ SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS sent,
25299
+ SUM(CASE WHEN synced_at = ${String(SKIPPED)}
25300
+ AND (sync_failure IS NULL OR sync_failure = 'payload_invalid')
25301
+ THEN 1 ELSE 0 END) AS skipped,
25302
+ SUM(CASE WHEN synced_at = ${String(SKIPPED)}
25303
+ AND sync_failure = 'deployment_refused' THEN 1 ELSE 0 END) AS refused,
25304
+ SUM(CASE WHEN synced_at = ${String(SKIPPED)}
25305
+ AND sync_failure = 'detached_undelivered' THEN 1 ELSE 0 END) AS detached
25306
+ FROM audit_events
25307
+ WHERE event_type IN (${TYPE_LIST})`
25308
+ );
25309
+ this.captureSkipCountStmt = db.prepare(
25310
+ // EVERY sentinel capture, whatever the reason — deliberately NOT split the
25311
+ // way the structural totals are. The split exists because a refusal is
25312
+ // terminal only against the deployment that gave it, and the structural
25313
+ // re-arm frees it on a change of deployment. The capture lane has no such
25314
+ // escape: re-arming a capture would offer one deployment's undelivered
25315
+ // prompts, with their text, to a deployment that never saw them, which is
25316
+ // exactly what disownCapturesStmt exists to prevent. So on this lane both
25317
+ // reasons mean the same thing — this row will not be sent — and splitting
25318
+ // them would put refused captures in a bucket nothing reads and nothing
25319
+ // frees.
25320
+ `SELECT COUNT(*) AS skipped
25321
+ FROM audit_events
25322
+ WHERE synced_at = ${String(SKIPPED)}
25323
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
25324
+ );
25325
+ this.fingerprintStmt = db.prepare(
25326
+ `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
25327
+ FROM history_sync WHERE id = 1`
25328
+ );
25329
+ this.setFingerprintStmt = db.prepare(
25330
+ `UPDATE history_sync
25331
+ SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
25332
+ WHERE id = 1`
25333
+ );
25334
+ this.disownCapturesStmt = db.prepare(
25335
+ `UPDATE audit_events SET outbox_owed = NULL
25336
+ WHERE outbox_owed IS NOT NULL
25337
+ AND event_type IN (${CAPTURE_TYPE_LIST})
25338
+ AND started_at < :attachedAt`
25339
+ );
25340
+ this.rearmStmt = db.prepare(
25341
+ `UPDATE audit_events
25342
+ SET synced_at = NULL, sync_failed_at = NULL, sync_failure = NULL
25343
+ WHERE (synced_at > 0
25344
+ OR sync_failure IN ('deployment_refused', 'detached_undelivered'))
25345
+ AND event_type IN (${TYPE_LIST})`
25346
+ );
25347
+ this.claimStmt = db.prepare(
25348
+ `UPDATE history_sync
25349
+ SET owner_pid = :pid, owner_host = :host, acquired_at = :now, heartbeat_at = :now
25350
+ WHERE id = 1
25351
+ AND (owner_pid IS NULL
25352
+ OR heartbeat_at IS NULL
25353
+ OR heartbeat_at < :staleBefore
25354
+ OR heartbeat_at > :now)`
25355
+ );
25356
+ this.heartbeatStmt = db.prepare(
25357
+ `UPDATE history_sync SET heartbeat_at = :now WHERE id = 1 AND owner_pid = :pid`
25358
+ );
25359
+ this.releaseStmt = db.prepare(
25360
+ `UPDATE history_sync
25361
+ SET owner_pid = NULL, owner_host = NULL, acquired_at = NULL, heartbeat_at = NULL
25362
+ WHERE id = 1 AND owner_pid = :pid`
25363
+ );
25364
+ this.closeWindowStmt = db.prepare(
25365
+ `UPDATE audit_events
25366
+ SET synced_at = ${String(SKIPPED)},
25367
+ sync_failed_at = :at,
25368
+ sync_failure = 'detached_undelivered'
25369
+ WHERE synced_at IS NULL
25370
+ AND event_type IN (${TYPE_LIST})
25371
+ AND started_at >= :attachedAt`
25372
+ );
25373
+ this.releaseBoundaryStmt = db.prepare(
25374
+ `UPDATE history_sync SET backlog_before = NULL WHERE id = 1`
25375
+ );
25376
+ this.freezeBoundaryStmt = db.prepare(
25377
+ `UPDATE history_sync SET backlog_before = :backlogBefore WHERE id = 1`
25378
+ );
25379
+ this.leaseStmt = db.prepare(
25380
+ `SELECT owner_pid AS ownerPid, owner_host AS ownerHost,
25381
+ acquired_at AS acquiredAt, heartbeat_at AS heartbeatAt
25382
+ FROM history_sync WHERE id = 1`
25383
+ );
25384
+ this.inspectionsStmt = db.prepare(
25385
+ `SELECT d.rule_id AS ruleId,
25386
+ d.name AS ruleName,
25387
+ d.version AS ruleVersion,
25388
+ d.category AS category,
25389
+ d.severity AS severity,
25390
+ f.span_start AS spanStart,
25391
+ f.span_end AS spanEnd,
25392
+ f.masked_match AS maskedMatch,
25393
+ f.action_taken AS actionTaken,
25394
+ f.confidence AS confidence
25395
+ FROM inspection_findings f
25396
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
25397
+ WHERE f.audit_event_id = :auditEventId
25398
+ ORDER BY f.span_start, f.id`
25399
+ );
25400
+ }
25401
+ db;
25402
+ ensureRowStmt;
25403
+ sessionsStmt;
25404
+ rowsStmt;
25405
+ stampStmt;
25406
+ countsStmt;
25407
+ fingerprintStmt;
25408
+ setFingerprintStmt;
25409
+ rearmStmt;
25410
+ claimStmt;
25411
+ heartbeatStmt;
25412
+ releaseStmt;
25413
+ leaseStmt;
25414
+ inspectionsStmt;
25415
+ closeWindowStmt;
25416
+ releaseBoundaryStmt;
25417
+ freezeBoundaryStmt;
25418
+ captureRowsStmt;
25419
+ markOwedStmt;
25420
+ markCaptureBacklogOwedStmt;
25421
+ captureSkipCountStmt;
25422
+ disownCapturesStmt;
25423
+ partitionStmt;
25424
+ partitionByKindStmt;
25425
+ claimRowStmt;
25426
+ releaseRowStmt;
25427
+ releaseStaleClaimsStmt;
25428
+ /**
25429
+ * The masked detections recorded against one tool call.
25430
+ *
25431
+ * These travel with the event because a tool call's target is not
25432
+ * re-inspectable from the event alone — unlike a capture, where the text
25433
+ * itself is re-scannable. What crosses is the masked match and the rule that
25434
+ * produced it, never the value.
25435
+ */
25436
+ inspectionsFor(auditEventId) {
25437
+ return allRows(this.inspectionsStmt, { auditEventId });
25438
+ }
25439
+ /**
25440
+ * Sessions with structural rows still to send, oldest first.
25441
+ *
25442
+ * BOUNDED BY THE BACKLOG BOUNDARY, which is the whole correctness of this
25443
+ * read. Anything recorded after the machine attached is the live forward
25444
+ * path's to deliver; this drain exists for what was recorded before it, and a
25445
+ * row both paths send is at best a duplicate request and at worst — for a
25446
+ * session root — an overwrite of the inventory ids the live path resolved.
25447
+ */
25448
+ pendingSessions(limit, before) {
25449
+ return allRows(this.sessionsStmt, { limit, before }).map(
25450
+ (r) => r.sessionId
25451
+ );
25452
+ }
25453
+ /** One session's undelivered structural rows within the backlog, root first. */
25454
+ pendingRows(sessionId, limit, before) {
25455
+ return allRows(this.rowsStmt, { sessionId, limit, before });
25456
+ }
25457
+ /**
25458
+ * Captures this machine still owes the deployment, oldest first.
25459
+ *
25460
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
25461
+ * by a time window — see captureRowsStmt for why a window could not express
25462
+ * this. `before` is the grace window that leaves a just-recorded capture to
25463
+ * the live path.
25464
+ */
25465
+ pendingCaptureRows(limit, before) {
25466
+ return allRows(this.captureRowsStmt, { limit, before });
25467
+ }
25468
+ /**
25469
+ * Record that a capture is OWED to the deployment.
25470
+ *
25471
+ * Written by the attached forward path when a live send did not confirm
25472
+ * delivery, and read by the drain as the whole of its eligibility test. It is
25473
+ * a fact rather than an inference: the machine was attached, the send did not
25474
+ * land, so the row is owed — which no time window can state, because the same
25475
+ * window that holds the rows a past attachment left owed also holds every
25476
+ * capture recorded while the machine was DETACHED, and those were never
25477
+ * offered to anyone.
25478
+ *
25479
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
25480
+ * out of the drain's read.
25481
+ */
25482
+ markCaptureOwed(id) {
25483
+ this.markOwedStmt.run({ id });
25484
+ }
25485
+ /**
25486
+ * Mark every capture already on disk as owed, as of `before`.
25487
+ *
25488
+ * The consent-time backfill, called once from `aka attach` when a human
25489
+ * grants existing-history consent — never from an ongoing drain pass, and
25490
+ * never inferred from a boundary that could later move. `before` is the
25491
+ * caller's own "now" at the moment consent was granted, so what this marks
25492
+ * is exactly the backlog the consent prompt already counted, not whatever a
25493
+ * later re-attach or key rotation might widen it to.
25494
+ *
25495
+ * Returns how many rows matched, for the caller to log or test against. Not a
25496
+ * count of NEWLY marked rows — a row still unsynced from an earlier call
25497
+ * matches again and is counted again, the same as `UPDATE`'s own `changes`.
25498
+ */
25499
+ markCaptureBacklogOwed(before) {
25500
+ return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
25501
+ }
25502
+ /**
25503
+ * Record delivery. Called only AFTER the far side has accepted the rows.
25504
+ *
25505
+ * CLEARS any failure reason in the same statement. A row that failed against
25506
+ * one deployment and then landed is delivered, and leaving the reason behind
25507
+ * would leave the store holding two contradictory answers about one row —
25508
+ * with the surface free to render either.
25509
+ */
25510
+ markSynced(ids, atMs) {
25511
+ this.stampAll(ids, atMs, null);
25512
+ }
25513
+ /**
25514
+ * Record that THIS MACHINE cannot express the row on the wire.
25515
+ *
25516
+ * Reserved for a local defect — a row that cannot be rebuilt into a valid
25517
+ * payload, or a body the client itself refused to send. It fails identically
25518
+ * against every deployment, so it is terminal everywhere and the re-arm leaves
25519
+ * it alone. A row that merely failed to REACH the deployment stays NULL, so it
25520
+ * is retried; marking those would turn one outage into permanent data loss.
25521
+ */
25522
+ markSkipped(ids, atMs) {
25523
+ this.stampAll(ids, SKIPPED, "payload_invalid", atMs);
25524
+ }
25525
+ /**
25526
+ * Record that THIS DEPLOYMENT refused the row.
25527
+ *
25528
+ * The same sentinel as `markSkipped`, and deliberately so: both stop the row
25529
+ * being re-offered on this lane, and `synced_at` goes on answering whether a
25530
+ * row is outstanding rather than why. What separates them is the reason, and
25531
+ * what the reason buys is the re-arm — a refusal is one deployment's verdict
25532
+ * on one body, so it is terminal only for as long as this machine points at
25533
+ * that deployment, and `rearmFor` clears it when the deployment changes.
25534
+ *
25535
+ * Leaving such a row NULL instead would be worse than the loss it replaces:
25536
+ * these reads carry no cursor, so an unstamped row the deployment refuses is
25537
+ * the head of every subsequent page, and the lane stalls behind it for ever.
25538
+ */
25539
+ markRefused(ids, atMs) {
25540
+ this.stampAll(ids, SKIPPED, "deployment_refused", atMs);
25541
+ }
25542
+ eachInTransaction(ids, run) {
25543
+ if (ids.length === 0) return;
25544
+ withTransaction(
25545
+ this.db,
25546
+ () => {
25547
+ for (const id of ids) run(id);
25548
+ },
25549
+ "IMMEDIATE"
25550
+ );
25551
+ }
25552
+ stampAll(ids, value, failure, failedAtMs) {
25553
+ if (ids.length === 0) return;
25554
+ const failedAt = failure === null ? null : failedAtMs ?? null;
25555
+ withTransaction(
25556
+ this.db,
25557
+ () => {
25558
+ for (const id of ids) this.stampStmt.run({ at: value, failedAt, failure, id });
25559
+ },
25560
+ "IMMEDIATE"
25561
+ );
25562
+ }
25563
+ /**
25564
+ * Claim rows as in-flight.
25565
+ *
25566
+ * Advisory in exactly the sense the lease is: it records that a send is in
25567
+ * progress so a surface can say so, and a lost claim costs a row showing as
25568
+ * queued while it is actually being sent. It is not exclusion — the far side
25569
+ * settles a duplicate on the row id.
25570
+ */
25571
+ claimRows(ids, atMs) {
25572
+ this.eachInTransaction(ids, (id) => this.claimRowStmt.run({ at: atMs, id }));
25573
+ }
25574
+ /** Give back a claim without settling — the send failed, the row is queued again. */
25575
+ releaseRows(ids) {
25576
+ this.eachInTransaction(ids, (id) => this.releaseRowStmt.run({ id }));
25577
+ }
25578
+ /**
25579
+ * Clear claims older than `staleBefore`, and report how many were cleared.
25580
+ *
25581
+ * A process killed between claiming and settling leaves rows claimed with
25582
+ * nothing left to settle them. Without this they read as "sending" for ever.
25583
+ */
25584
+ releaseStaleClaims(staleBefore) {
25585
+ return Number(this.releaseStaleClaimsStmt.run({ staleBefore }).changes);
25586
+ }
25587
+ /**
25588
+ * Every tracked row in exactly one delivery state.
25589
+ *
25590
+ * Takes no boundary on purpose. The boundary answers "what should the drain
25591
+ * pick up now", which is a different question from "what state is this row
25592
+ * in" — and a machine that has never attached has no boundary to pass, so
25593
+ * requiring one would force a caller to invent one and report the whole store
25594
+ * as queued.
25595
+ */
25596
+ /**
25597
+ * The same partition, one row per kind that a lane carries.
25598
+ *
25599
+ * A kind with nothing to report is ABSENT rather than a row of zeros: the
25600
+ * scope decides which rows exist at all, so a kind that has never been
25601
+ * recorded — or whose captures nobody ever owed — produces no group. A caller
25602
+ * rendering a fixed list of kinds must therefore treat a missing one as "no
25603
+ * rows", never as "zero sent"; the two look identical in a bar and mean
25604
+ * different things.
25605
+ */
25606
+ partitionByKind() {
25607
+ return allRows(
25608
+ this.partitionByKindStmt,
25609
+ {}
25610
+ ).map((row) => ({
25611
+ kind: row.kind,
25612
+ queued: row.queued ?? 0,
25613
+ inProgress: row.inProgress ?? 0,
25614
+ synced: row.synced ?? 0,
25615
+ failed: row.failed ?? 0,
25616
+ refused: row.refused ?? 0,
25617
+ detached: row.detached ?? 0,
25618
+ total: row.total ?? 0
25619
+ }));
25620
+ }
25621
+ partition() {
25622
+ const row = getRow(this.partitionStmt, {});
25623
+ return {
25624
+ queued: row?.queued ?? 0,
25625
+ inProgress: row?.inProgress ?? 0,
25626
+ synced: row?.synced ?? 0,
25627
+ failed: row?.failed ?? 0,
25628
+ refused: row?.refused ?? 0,
25629
+ detached: row?.detached ?? 0,
25630
+ total: row?.total ?? 0
25631
+ };
25632
+ }
25633
+ /** `pending` counts only what is inside the backlog; sent and skipped are totals. */
25634
+ counts(before) {
25635
+ const row = getRow(this.countsStmt, { before });
25636
+ const captures = getRow(this.captureSkipCountStmt);
25637
+ return {
25638
+ pending: row?.pending ?? 0,
25639
+ sent: row?.sent ?? 0,
25640
+ skipped: row?.skipped ?? 0,
25641
+ refused: row?.refused ?? 0,
25642
+ detached: row?.detached ?? 0,
25643
+ capturesSkipped: captures?.skipped ?? 0
25644
+ };
25645
+ }
25646
+ /**
25647
+ * The deployment the current stamps were made against, and where its backlog
25648
+ * ends.
25649
+ *
25650
+ * READ-ONLY. An absent row reads as an absent deployment, which is what a
25651
+ * machine that has never drained is — and every writer below seeds the row
25652
+ * before it needs one, so nothing depends on this creating it. Keeping the
25653
+ * write off the gate path matters because the gate runs on every pass while a
25654
+ * write has to take the database's write lock.
25655
+ */
25656
+ deployment() {
25657
+ const row = getRow(
25658
+ this.fingerprintStmt
25659
+ );
25660
+ return {
25661
+ fingerprint: row?.fingerprint ?? void 0,
25662
+ backlogBefore: row?.backlogBefore ?? void 0
25663
+ };
25664
+ }
25665
+ /**
25666
+ * Point the ledger at a different deployment, discarding what it recorded
25667
+ * about the previous one.
25668
+ *
25669
+ * Delivery is a fact about ONE recipient: rows sent to the deployment a
25670
+ * machine has just left are undelivered as far as the new one is concerned.
25671
+ * All four in one transaction, so a crash between them cannot leave stamps
25672
+ * attributed to the wrong deployment, a boundary that belongs to another, or
25673
+ * a disown with no re-mark to follow it.
25674
+ *
25675
+ * The boundary is written HERE and only here, which is what freezes it: a
25676
+ * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
25677
+ * unchanged, so this never runs and the backlog does not widen back over rows
25678
+ * the live path has since delivered.
25679
+ *
25680
+ * `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
25681
+ * granted existing-history consent for the deployment this call is arming —
25682
+ * a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
25683
+ * instant, `backlogBefore` is the ATTACH instant, and the two can be far
25684
+ * apart. Passed only when that grant is valid, since this method has no way
25685
+ * to check consent itself and must not mark a row owed for a machine that
25686
+ * never agreed to it. Applied AFTER the disown above, in the SAME
25687
+ * transaction: what the disown clears is every marker below `backlogBefore`,
25688
+ * which includes this deployment's OWN pre-attach rows — `aka attach` calls
25689
+ * `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
25690
+ * on the cleared side of that bound — and the re-mark in the same
25691
+ * transaction is what puts those rows back. A crash between the two cannot
25692
+ * strand the ledger disowned with nothing re-marked — the transaction either
25693
+ * lands whole or not at all, and a fingerprint mismatch that has not yet
25694
+ * committed re-enters this method on the very next pass. Omit it (the
25695
+ * structural-only tests do) to exercise the disown in isolation.
25696
+ *
25697
+ * The disown is bounded by `backlogBefore`, which is what keeps it from
25698
+ * touching a marker the NEW deployment's OWN live path has already set: B's
25699
+ * live path can mark a capture owed from the moment `aka attach` writes the
25700
+ * descriptor, before the drain's first pass ever reaches this method, and
25701
+ * such a row sits at or after the bound rather than below it. What keeps the
25702
+ * disown from eating THIS SAME CALL's own re-mark is the order, not the
25703
+ * bound — disown runs first, re-mark second, both inside the one
25704
+ * transaction above.
25705
+ */
25706
+ rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
25707
+ this.ensureRowStmt.run();
25708
+ withTransaction(
25709
+ this.db,
25710
+ () => {
25711
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
25712
+ this.rearmStmt.run();
25713
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
25714
+ this.disownCapturesStmt.run({ attachedAt: backlogBefore });
25715
+ }
25716
+ if (backfillCapturesBefore !== void 0) {
25717
+ this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
25718
+ }
25719
+ this.setFingerprintStmt.run({ fingerprint, backlogBefore });
25720
+ },
25721
+ "IMMEDIATE"
25722
+ );
25723
+ }
25724
+ /**
25725
+ * End the attached period: hand its rows to the live path, and release the
25726
+ * boundary so the next attachment can freeze a new one.
25727
+ *
25728
+ * WITHOUT THIS, a detach and a re-attach to the same deployment leave a window
25729
+ * nothing delivers. The fingerprint is unchanged, so the boundary is never
25730
+ * re-frozen and stays at the FIRST attachment — while nothing forwards at all
25731
+ * during the detached period, because the machine is not attached. Rows
25732
+ * recorded in that window sit after the boundary and before the re-attach, so
25733
+ * neither path takes them, and the pending count reports none outstanding.
25734
+ *
25735
+ * WHAT IT RECORDS, and what it deliberately does not. These rows were the
25736
+ * closing attachment's to deliver and are no longer outstanding — that is what
25737
+ * lets the boundary move. It is NOT a claim that any of them arrived, and the
25738
+ * distinction is not academic: this used to write a delivery TIME, which every
25739
+ * read treats as delivery, so one detach turned a window of undelivered rows
25740
+ * into a window of delivered ones and no surface could tell. It writes the
25741
+ * skip sentinel and a reason of its own instead, so "no longer owed" and
25742
+ * "received" stop being the same fact.
25743
+ *
25744
+ * A change of deployment still frees them (see the re-arm), because the next
25745
+ * deployment has seen none of this machine's history — so the rows reach it
25746
+ * exactly as they did when this wrote a delivery time.
25747
+ *
25748
+ * ONE TRANSACTION, so a crash cannot release the boundary while leaving the
25749
+ * window unstamped — that half-state would re-send the whole attached period
25750
+ * on the next attach, which is the failure the boundary exists to prevent.
25751
+ */
25752
+ closeAttachedWindow(attachedAtMs, atMs) {
25753
+ this.ensureRowStmt.run();
25754
+ withTransaction(
25755
+ this.db,
25756
+ () => {
25757
+ const row = getRow(this.fingerprintStmt);
25758
+ const from = row?.backlogBefore ?? attachedAtMs;
25759
+ this.closeWindowStmt.run({ at: atMs, attachedAt: from });
25760
+ this.releaseBoundaryStmt.run();
25761
+ },
25762
+ "IMMEDIATE"
25763
+ );
25764
+ }
25765
+ /**
25766
+ * Freeze a boundary for the deployment already on file, KEEPING the stamps.
25767
+ *
25768
+ * The re-attach half of the above. Distinct from `rearmFor`, which is for a
25769
+ * different deployment and therefore discards what was delivered to the old
25770
+ * one: here the recipient is the same, so everything already sent to it stays
25771
+ * sent.
25772
+ */
25773
+ freezeBoundary(backlogBefore) {
25774
+ this.ensureRowStmt.run();
25775
+ this.freezeBoundaryStmt.run({ backlogBefore });
25776
+ }
25777
+ /** Take the claim, or report that someone live already holds it. */
25778
+ claim(pid, host, nowMs, staleAfterMs) {
25779
+ this.ensureRowStmt.run();
25780
+ let taken = false;
25781
+ withTransaction(
25782
+ this.db,
25783
+ () => {
25784
+ const result = this.claimStmt.run({
25785
+ pid,
25786
+ host,
25787
+ now: nowMs,
25788
+ staleBefore: nowMs - staleAfterMs
25789
+ });
25790
+ taken = result.changes === 1;
25791
+ },
25792
+ "IMMEDIATE"
25793
+ );
25794
+ return taken;
25795
+ }
25796
+ /** Say the holder is still alive. A no-op once the claim has moved on. */
25797
+ heartbeat(pid, nowMs) {
25798
+ this.heartbeatStmt.run({ now: nowMs, pid });
25799
+ }
25800
+ /** Give the claim up. Scoped to this holder, so a taken-over claim is left be. */
25801
+ release(pid) {
25802
+ this.releaseStmt.run({ pid });
25803
+ }
25804
+ /** Who holds the claim, if anyone. Read-only, for the same reason as above. */
25805
+ lease() {
25806
+ return getRow(this.leaseStmt);
25807
+ }
25808
+ };
25809
+
24794
25810
  // ../../packages/persistence/src/migrations.ts
24795
25811
  function describeObject(object2) {
24796
25812
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
@@ -24803,7 +25819,7 @@ function createdIndexName(statement) {
24803
25819
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
24804
25820
  }
24805
25821
  var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
24806
- function applyMigrations(db, file2) {
25822
+ function applyMigrations(db, file2, options = {}) {
24807
25823
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
24808
25824
  db.exec(
24809
25825
  "CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
@@ -24817,6 +25833,7 @@ function applyMigrations(db, file2) {
24817
25833
  );
24818
25834
  for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
24819
25835
  if (applied.has(migration.tag)) continue;
25836
+ if (options.skipTags?.has(migration.tag) === true) continue;
24820
25837
  if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
24821
25838
  const evidence = evidenceObjects(migration.sql);
24822
25839
  const present = evidence.filter((o) => evidenceExists(db, o));
@@ -25223,10 +26240,62 @@ function ensureSyncedAtColumn(db, table) {
25223
26240
  if (!columns.includes("outbox_owed")) {
25224
26241
  db.exec(`ALTER TABLE ${table} ADD COLUMN outbox_owed integer`);
25225
26242
  }
26243
+ if (!columns.includes("sync_failed_at")) {
26244
+ db.exec(`ALTER TABLE ${table} ADD COLUMN sync_failed_at integer`);
26245
+ }
26246
+ if (!columns.includes("sync_failure")) {
26247
+ withTransaction(
26248
+ db,
26249
+ () => {
26250
+ db.exec(`ALTER TABLE ${table} ADD COLUMN sync_failure text`);
26251
+ db.exec(
26252
+ `UPDATE ${table} SET synced_at = NULL
26253
+ WHERE synced_at = -1
26254
+ AND event_type IN (${COUNTED_EVENT_TYPES.map((t) => `'${t}'`).join(", ")})`
26255
+ );
26256
+ },
26257
+ "IMMEDIATE"
26258
+ );
26259
+ }
25226
26260
  db.exec(
25227
- `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
25228
- ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
26261
+ `CREATE TRIGGER IF NOT EXISTS aka_sync_failure_guard
26262
+ BEFORE UPDATE OF sync_failure ON ${table}
26263
+ WHEN ${syncFailureRejectCondition()}
26264
+ BEGIN SELECT RAISE(ABORT, 'sync_failure is not one of the recorded reasons'); END`
25229
26265
  );
26266
+ const syncIndexColumns = [
26267
+ "event_type",
26268
+ "synced_at",
26269
+ "sync_claimed_at",
26270
+ "started_at",
26271
+ // Appended LAST on purpose. The delivery-state read now projects it, so it
26272
+ // has to be in the index for the read to stay covered — but putting it
26273
+ // ahead of `started_at` would reorder the prefix the structural drain's
26274
+ // reads match on.
26275
+ "sync_failure"
26276
+ // `outbox_owed` is DELIBERATELY ABSENT, and it was measured both ways.
26277
+ //
26278
+ // The delivery-state read tests it — a capture's state depends on whether a
26279
+ // live forward marked it owed — so carrying it here makes that read covering
26280
+ // rather than a row fetch per row: 16 ms against 40 ms on a real 6 GB store.
26281
+ // But a sixth column changes what the planner charges for this index, and
26282
+ // with no ANALYZE statistics it plans from schema shape alone: measured, it
26283
+ // then stops choosing the per-session index for the token rollup and walks
26284
+ // every `llm_call` in the store through the event-type index instead. That
26285
+ // read grows with the store; this one does not.
26286
+ //
26287
+ // 40 ms on the largest store measured, once per render, is a cost worth
26288
+ // paying to leave every other read's plan where it was.
26289
+ ];
26290
+ const currentSyncIndex = indexColumns(db, "idx_audit_events_sync");
26291
+ const syncIndexMatches = currentSyncIndex.length === syncIndexColumns.length && currentSyncIndex.every((column, i) => column === syncIndexColumns[i]);
26292
+ if (!syncIndexMatches) {
26293
+ db.exec("DROP INDEX IF EXISTS idx_audit_events_sync");
26294
+ db.exec(
26295
+ `CREATE INDEX idx_audit_events_sync
26296
+ ON audit_events (${syncIndexColumns.join(", ")})`
26297
+ );
26298
+ }
25230
26299
  db.exec(
25231
26300
  `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25232
26301
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
@@ -25448,7 +26517,11 @@ function buildAuditEvent(row) {
25448
26517
  link: linkParsed?.success ? linkParsed.data : null,
25449
26518
  targetId: row.target_id,
25450
26519
  internal: intToBool(row.internal),
25451
- flagged: intToBool(row.flagged)
26520
+ flagged: intToBool(row.flagged),
26521
+ // Only meaningful when the title came out empty — a row whose body was
26522
+ // expired but whose title fell back to `tool_name` still has something to
26523
+ // render, and flagging it would make the view apologise for nothing.
26524
+ bodyExpired: row.content_expired_at !== null && (row.title ?? "") === ""
25452
26525
  };
25453
26526
  }
25454
26527
  var TIMELINE_COLUMNS = `
@@ -25456,6 +26529,7 @@ var TIMELINE_COLUMNS = `
25456
26529
  event_type,
25457
26530
  started_at,
25458
26531
  coalesce(content, json_extract(attributes, '$.tool_name')) AS title,
26532
+ content_expired_at,
25459
26533
  coalesce(json_extract(attributes, '$.detail'), json_extract(attributes, '$.target')) AS detail,
25460
26534
  coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25461
26535
  json_extract(attributes, '$.severity') AS severity,
@@ -25582,7 +26656,8 @@ var SqliteActivityRepository = class {
25582
26656
  SELECT 1 FROM audit_events d
25583
26657
  WHERE d.root_session_id = audit_events.id
25584
26658
  AND (d.content LIKE ? ESCAPE '\\'
25585
- OR json_extract(d.attributes, '$.detail') LIKE ? ESCAPE '\\')))`
26659
+ OR coalesce(json_extract(d.attributes, '$.detail'),
26660
+ json_extract(d.attributes, '$.target')) LIKE ? ESCAPE '\\')))`
25586
26661
  );
25587
26662
  params.push(pattern, pattern, pattern, pattern, pattern, pattern);
25588
26663
  }
@@ -26120,6 +27195,88 @@ var SqliteAuditEventsRepository = class {
26120
27195
  }
26121
27196
  };
26122
27197
 
27198
+ // ../../packages/persistence/src/repositories/body-retention.ts
27199
+ var DEFAULT_BATCH_SIZE = 500;
27200
+ var DEFAULT_MAX_ROWS = 5e4;
27201
+ var SYNC_LANE_TYPES_SQL = OUTBOX_CAPTURE_TYPE_LIST;
27202
+ var SqliteBodyRetentionRepository = class {
27203
+ constructor(db) {
27204
+ this.db = db;
27205
+ const select = (laneClause) => `
27206
+ SELECT id, LENGTH(CAST(content AS BLOB)) AS bytes
27207
+ FROM audit_events
27208
+ WHERE content IS NOT NULL
27209
+ AND started_at < :cutoff
27210
+ AND event_type IN (${CAPTURE_EVENT_TYPES_SQL})
27211
+ ${laneClause}
27212
+ ORDER BY started_at
27213
+ LIMIT :limit`;
27214
+ this.candidatesStmt = this.db.prepare(select(""));
27215
+ this.candidatesSyncSafeStmt = this.db.prepare(
27216
+ select(`AND (event_type NOT IN (${SYNC_LANE_TYPES_SQL}) OR synced_at IS NOT NULL)`)
27217
+ );
27218
+ this.heldBySyncStmt = this.db.prepare(`
27219
+ SELECT COUNT(*) AS n
27220
+ FROM audit_events
27221
+ WHERE content IS NOT NULL
27222
+ AND started_at < :cutoff
27223
+ AND event_type IN (${SYNC_LANE_TYPES_SQL})
27224
+ AND synced_at IS NULL`);
27225
+ this.expireStmt = this.db.prepare(
27226
+ `UPDATE audit_events SET content = NULL, content_expired_at = :now WHERE id = :id`
27227
+ );
27228
+ }
27229
+ db;
27230
+ candidatesStmt;
27231
+ candidatesSyncSafeStmt;
27232
+ heldBySyncStmt;
27233
+ expireStmt;
27234
+ /** How many bytes a pass with these options would free, changing nothing. */
27235
+ preview(opts) {
27236
+ const limit = opts.maxRows ?? DEFAULT_MAX_ROWS;
27237
+ const stmt = opts.sweepSyncLane ? this.candidatesStmt : this.candidatesSyncSafeStmt;
27238
+ const rows = stmt.all({ cutoff: opts.cutoff, limit });
27239
+ return {
27240
+ rowsExpired: rows.length,
27241
+ bytesFreed: rows.reduce((sum, r) => sum + r.bytes, 0),
27242
+ rowsHeldBySync: this.countHeldBySync(opts)
27243
+ };
27244
+ }
27245
+ /** Clear eligible bodies, in bounded batches. */
27246
+ expire(opts) {
27247
+ const batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
27248
+ const maxRows = opts.maxRows ?? DEFAULT_MAX_ROWS;
27249
+ const stmt = opts.sweepSyncLane ? this.candidatesStmt : this.candidatesSyncSafeStmt;
27250
+ let rowsExpired = 0;
27251
+ let bytesFreed = 0;
27252
+ let done = true;
27253
+ while (rowsExpired < maxRows) {
27254
+ const remaining = Math.min(batchSize, maxRows - rowsExpired);
27255
+ const batch = stmt.all({ cutoff: opts.cutoff, limit: remaining });
27256
+ if (batch.length === 0) break;
27257
+ withTransaction(
27258
+ this.db,
27259
+ () => {
27260
+ for (const row of batch) this.expireStmt.run({ id: row.id, now: opts.now });
27261
+ },
27262
+ "IMMEDIATE"
27263
+ );
27264
+ rowsExpired += batch.length;
27265
+ bytesFreed += batch.reduce((sum, r) => sum + r.bytes, 0);
27266
+ if (batch.length < remaining) break;
27267
+ if (rowsExpired >= maxRows) {
27268
+ done = stmt.all({ cutoff: opts.cutoff, limit: 1 }).length === 0;
27269
+ }
27270
+ }
27271
+ return { rowsExpired, bytesFreed, rowsHeldBySync: this.countHeldBySync(opts), done };
27272
+ }
27273
+ countHeldBySync(opts) {
27274
+ if (opts.sweepSyncLane) return 0;
27275
+ const row = this.heldBySyncStmt.get({ cutoff: opts.cutoff });
27276
+ return row.n;
27277
+ }
27278
+ };
27279
+
26123
27280
  // ../../packages/persistence/src/repositories/classified-data.ts
26124
27281
  var SqliteClassifiedDataRepository = class {
26125
27282
  constructor(db) {
@@ -26920,23 +28077,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26920
28077
  )`;
26921
28078
 
26922
28079
  // ../../packages/persistence/src/repositories/findings.ts
26923
- var PREVIEW_INSTANCES_PER_GROUP = 200;
26924
- var DEFAULT_LOCATIONS_LIMIT = 100;
26925
- var LOCATION_RULE_IDS_CAP = 20;
26926
- function compareLocationOrder(a, b) {
26927
- return compareFindingGroupOrder(
26928
- {
26929
- severity: a.maxSeverity,
26930
- latestDetectedAt: a.latestDetectedAt,
26931
- id: ""
26932
- },
26933
- {
26934
- severity: b.maxSeverity,
26935
- latestDetectedAt: b.latestDetectedAt,
26936
- id: ""
26937
- }
26938
- );
26939
- }
26940
28080
  var CONCAT_SEP = ",";
26941
28081
  var TUPLE_SEP = "|";
26942
28082
  function splitConcat(value) {
@@ -26965,7 +28105,15 @@ function toFlatFindingRow(r) {
26965
28105
  ...r.tool_name === null ? {} : { toolName: r.tool_name },
26966
28106
  eventId: r.event_id,
26967
28107
  ...r.session_id === null ? {} : { sessionId: r.session_id },
26968
- status: deriveInstanceStatus(r)
28108
+ status: deriveInstanceStatus(r),
28109
+ delivery: deriveFindingDelivery({
28110
+ kind: r.kind,
28111
+ syncedAt: r.synced_at,
28112
+ syncClaimedAt: r.sync_claimed_at,
28113
+ syncFailedAt: r.sync_failed_at,
28114
+ syncFailure: r.sync_failure,
28115
+ outboxOwed: r.outbox_owed
28116
+ })
26969
28117
  };
26970
28118
  }
26971
28119
  function encodeGroupCursor(group) {
@@ -26988,13 +28136,51 @@ function decodeGroupCursor(cursor) {
26988
28136
  return null;
26989
28137
  }
26990
28138
  function firstAfter(sorted, cursor) {
26991
- const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
28139
+ const index = sorted.findIndex((t) => compareFindingGroupOrder(t, cursor) > 0);
26992
28140
  return index === -1 ? sorted.length : index;
26993
28141
  }
26994
28142
  function findDeepLinked(sorted, page, id) {
26995
- if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
26996
- return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
28143
+ if (page.some((t) => t.id === id)) return void 0;
28144
+ return sorted.find((t) => t.id === id);
28145
+ }
28146
+ function encodeLocationCursor(location) {
28147
+ const payload = {
28148
+ sev: location.maxSeverity,
28149
+ t: location.latestDetectedAt,
28150
+ r: location.repo,
28151
+ f: location.file
28152
+ };
28153
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
28154
+ }
28155
+ function decodeLocationCursor(cursor) {
28156
+ const parsed2 = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
28157
+ if (parsed2 !== void 0 && typeof parsed2.sev === "string" && typeof parsed2.t === "string" && typeof parsed2.r === "string" && typeof parsed2.f === "string") {
28158
+ return { maxSeverity: parsed2.sev, latestDetectedAt: parsed2.t, repo: parsed2.r, file: parsed2.f };
28159
+ }
28160
+ return null;
28161
+ }
28162
+ function firstLocationAfter(sorted, cursor) {
28163
+ const index = sorted.findIndex((l) => compareLocationOrder(l, cursor) > 0);
28164
+ return index === -1 ? sorted.length : index;
26997
28165
  }
28166
+ function findDeepLinkedLocation(sorted, page, id) {
28167
+ if (page.some((l) => l.id === id)) return void 0;
28168
+ return sorted.find((l) => l.id === id);
28169
+ }
28170
+ var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS category,
28171
+ d.severity AS severity, f.masked_match AS masked_match,
28172
+ f.action_taken AS action_taken, f.confidence AS confidence,
28173
+ e.started_at AS occurred_at,
28174
+ e.source_tool AS source_tool,
28175
+ e.repo AS repo,
28176
+ e.file_path AS file,
28177
+ e.tool_name AS tool_name,
28178
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
28179
+ e.event_type AS kind, f.finding_key AS finding_key,
28180
+ ${latestResolutionStatusSql("f")} AS latest_status,
28181
+ e.synced_at AS synced_at, e.sync_claimed_at AS sync_claimed_at,
28182
+ e.sync_failed_at AS sync_failed_at, e.sync_failure AS sync_failure,
28183
+ e.outbox_owed AS outbox_owed`;
26998
28184
  var DAY_MS3 = 864e5;
26999
28185
  var SqliteFindingsRepository = class {
27000
28186
  constructor(db) {
@@ -27115,30 +28301,26 @@ var SqliteFindingsRepository = class {
27115
28301
  );
27116
28302
  }
27117
28303
  /**
27118
- * Grouped findings for the dashboard — joins inspection_findings⋈audit_events
27119
- * ⋈inspection_definitions (repo/file/toolName from the audit event's
27120
- * attributes bag, rule_id/category/severity from the definition), scoped to
27121
- * the four capture kinds (audit_events also holds structural/reconciler/scan
27122
- * rows this list must never surface), groups by ruleId, computes
27123
- * per-filter-excluded facets, applies the requested filters, and sorts by
27124
- * severity then recency. Filtering
27125
- * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
27126
- * reflect the full filtered set; `items` is the requested
27127
- * page (default 50); no cursor (nextCursor is always null). Under a `status`
27128
- * filter, `totals.findings` counts only instances whose derived status was
27129
- * requested, and each item's instance preview is narrowed the same way.
28304
+ * Finding TYPES for the dashboard — one row per rule, scoped to the four
28305
+ * capture kinds (audit_events also holds structural/reconciler/scan rows this
28306
+ * list must never surface), with per-filter-excluded facets, the requested
28307
+ * filters applied, and sorted by severity then recency. Filtering and faceting
28308
+ * run in JS via the shared @akasecurity/schema helpers. `totals` reflect the
28309
+ * full filtered set; `items` is the requested page (default 50), keyset-paged.
28310
+ * Under a `status` filter, `totals.findings` counts only findings whose
28311
+ * derived status was requested.
28312
+ *
28313
+ * ONE read, which materializes no findings: a single aggregate per rule_id,
28314
+ * folding EVERY finding into the numbers a type row and the filters need
28315
+ * (count, severity, category, providers, actions, statuses, latest, search
28316
+ * text). The findings OF a type come from listFindingInstances scoped to
28317
+ * `subtype`, so neither list bounds the other and no per-type cap exists.
27130
28318
  *
27131
- * Two reads, neither of which materializes a row per finding:
27132
- * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
27133
- * the group and the filters need (count, providers, actions, statuses,
27134
- * latest, search text);
27135
- * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
27136
- * populate `instances` for the table's expanded rows.
27137
28319
  * The aggregates carry raw DB values and are translated by the same
27138
- * @akasecurity/schema mappers the row path uses, so no enum mapping or status
27139
- * rule is ever restated in SQL.
28320
+ * @akasecurity/schema mappers every other path uses, so no enum mapping or
28321
+ * status rule is ever restated in SQL.
27140
28322
  */
27141
- listGroupedFindings(query) {
28323
+ listFindingTypes(query) {
27142
28324
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
27143
28325
  const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
27144
28326
  const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
@@ -27151,12 +28333,7 @@ var SqliteFindingsRepository = class {
27151
28333
  predicate,
27152
28334
  params: sessionParams
27153
28335
  });
27154
- const rows = this.previewRows(aggregates, {
27155
- sessionId: query.sessionId,
27156
- from: query.from
27157
- });
27158
- const groupable = rows.map(toFlatFindingRow);
27159
- const allGroups = buildFindingGroups(groupable, { aggregates });
28336
+ const allTypes = buildFindingTypes(aggregates);
27160
28337
  const filterOpts = {
27161
28338
  severity: query.severity,
27162
28339
  providers: query.provider,
@@ -27165,30 +28342,25 @@ var SqliteFindingsRepository = class {
27165
28342
  subtype: query.subtype,
27166
28343
  q: query.q
27167
28344
  };
27168
- const facets = computeFindingFacets(allGroups, filterOpts);
27169
- const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
28345
+ const facets = computeFindingFacets(allTypes, filterOpts);
28346
+ const sorted = sortFindingTypes(applyFindingFilters(allTypes, filterOpts));
27170
28347
  const statusFilter = query.status ?? [];
27171
28348
  const totals = {
27172
- findings: sorted.reduce((acc, g) => {
27173
- if (statusFilter.length === 0) return acc + g.instanceCount;
27174
- const agg = aggregates.get(g.id);
27175
- return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
28349
+ findings: sorted.reduce((acc, t) => {
28350
+ if (statusFilter.length === 0) return acc + t.instanceCount;
28351
+ const agg = aggregates.get(t.id);
28352
+ return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? t.instanceCount : t.instanceCount);
27176
28353
  }, 0),
27177
- groups: sorted.length
28354
+ types: sorted.length
27178
28355
  };
27179
- const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
28356
+ const limit = query.limit ?? DEFAULT_FINDING_TYPES_LIMIT;
27180
28357
  const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
27181
28358
  const start = cursor === null ? 0 : firstAfter(sorted, cursor);
27182
28359
  const page = sorted.slice(start, start + limit);
27183
28360
  const lastOnPage = page.at(-1);
27184
28361
  const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
27185
28362
  const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
27186
- const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
27187
- const narrow = (g) => statusSet ? {
27188
- ...g,
27189
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
27190
- } : g;
27191
- const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
28363
+ const items = [...page, ...deepLinked ? [deepLinked] : []];
27192
28364
  return Promise.resolve({
27193
28365
  totals,
27194
28366
  facets,
@@ -27199,7 +28371,7 @@ var SqliteFindingsRepository = class {
27199
28371
  }
27200
28372
  /**
27201
28373
  * One row per rule_id, folding EVERY instance of the group into the values
27202
- * buildFindingGroups cannot recover from a preview. Bounded by the number of
28374
+ * buildFindingTypes cannot recover from an aggregate. Bounded by the number of
27203
28375
  * distinct rule_ids (the installed packs' rules), not by the store's size.
27204
28376
  *
27205
28377
  * A single scan, folded in two levels: the inner SELECT groups by
@@ -27253,6 +28425,7 @@ var SqliteFindingsRepository = class {
27253
28425
  providers: query.provider,
27254
28426
  actions: query.action,
27255
28427
  statuses: query.status,
28428
+ deliveries: query.deployment,
27256
28429
  tools: query.tool,
27257
28430
  repo: query.repo,
27258
28431
  file: query.file,
@@ -27293,13 +28466,25 @@ var SqliteFindingsRepository = class {
27293
28466
  });
27294
28467
  }
27295
28468
  /**
27296
- * The same findings folded by location: repository, then file within it.
28469
+ * The same findings folded by WHERE they live — one row per (repo, file) pair.
27297
28470
  *
27298
28471
  * The grouping keys come from the capturing event's attributes, which is what
27299
- * the local store relates a finding to — there is no finding↔asset row to
27300
- * group by instead. A repo or file the event did not record folds into the
27301
- * empty-string bucket, which the view renders but does not link, since no
27302
- * filter can name it.
28472
+ * the local store relates a finding to; there is no finding↔asset row to group
28473
+ * by instead. A repo or file the event did not record folds into the
28474
+ * empty-string bucket, which is a real location like any other: it is listed,
28475
+ * it is selectable, and its `?loc=` token is as good as any other row's.
28476
+ *
28477
+ * ONE flat list rather than repos nesting files. A rollup can only be paged by
28478
+ * repo, which leaves the file list inside it unbounded — the shape the by-type
28479
+ * list was rebuilt to remove — and two-level pagination inside an
28480
+ * expand/collapse table is what pushed that view to master/detail in the first
28481
+ * place.
28482
+ *
28483
+ * Every filter narrows the FINDINGS and the locations fall out of what
28484
+ * survives, so each row's `instanceCount` is exactly what listFindingInstances
28485
+ * reports for the same filters scoped to that pair. The view depends on it:
28486
+ * one toolbar sits over both panels precisely because a location owns none of
28487
+ * its fields.
27303
28488
  */
27304
28489
  listFindingLocations(query) {
27305
28490
  const opts = {
@@ -27308,16 +28493,20 @@ var SqliteFindingsRepository = class {
27308
28493
  providers: query.provider,
27309
28494
  actions: query.action,
27310
28495
  statuses: query.status,
28496
+ deliveries: query.deployment,
27311
28497
  tools: query.tool,
27312
28498
  q: query.q
27313
28499
  };
27314
- const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
28500
+ const limit = query.limit ?? DEFAULT_FINDING_LOCATIONS_LIMIT;
28501
+ const cursor = query.cursor === void 0 ? null : decodeLocationCursor(query.cursor);
27315
28502
  const byRepo = /* @__PURE__ */ new Map();
28503
+ const accumulator = createInstanceFacetAccumulator(opts);
27316
28504
  let total = 0;
27317
28505
  for (const row of this.scanFindingRows({
27318
28506
  sessionId: query.sessionId,
27319
28507
  from: query.from
27320
28508
  })) {
28509
+ accumulator.add(row);
27321
28510
  if (!matchesInstanceFilters(row, opts)) continue;
27322
28511
  total += 1;
27323
28512
  let files = byRepo.get(row.repo);
@@ -27332,103 +28521,35 @@ var SqliteFindingsRepository = class {
27332
28521
  }
27333
28522
  addToLocation(acc, row);
27334
28523
  }
27335
- let fileCount = 0;
27336
- const repos = [...byRepo.entries()].map(([repo, files]) => {
27337
- fileCount += files.size;
27338
- const fileRows = [...files.entries()].map(([file2, acc]) => ({
27339
- file: file2,
27340
- instanceCount: acc.instanceCount,
27341
- maxSeverity: acc.maxSeverity,
27342
- latestDetectedAt: acc.latestDetectedAt,
27343
- ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
27344
- ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
27345
- })).sort(compareLocationOrder);
27346
- const rollup = fileRows.reduce(
27347
- (a, f) => ({
27348
- instanceCount: a.instanceCount + f.instanceCount,
27349
- maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
27350
- latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
27351
- }),
27352
- {
27353
- instanceCount: 0,
27354
- maxSeverity: fileRows[0]?.maxSeverity ?? "low",
27355
- latestDetectedAt: ""
27356
- }
27357
- );
27358
- const statuses = fileRows.map((f) => f.status);
27359
- const folded = foldGroupStatus(statuses);
27360
- return {
27361
- repo,
27362
- instanceCount: rollup.instanceCount,
27363
- maxSeverity: rollup.maxSeverity,
27364
- latestDetectedAt: rollup.latestDetectedAt,
27365
- ...folded === void 0 ? {} : { status: folded },
27366
- files: fileRows
27367
- };
27368
- });
27369
- repos.sort(compareLocationOrder);
28524
+ const sorted = [];
28525
+ for (const [repo, files] of byRepo) {
28526
+ for (const [file2, acc] of files) {
28527
+ const status = foldGroupStatus(acc.statuses);
28528
+ sorted.push({
28529
+ id: encodeLocationId(repo, file2),
28530
+ repo,
28531
+ file: file2,
28532
+ instanceCount: acc.instanceCount,
28533
+ maxSeverity: acc.maxSeverity,
28534
+ latestDetectedAt: acc.latestDetectedAt,
28535
+ ...status === void 0 ? {} : { status },
28536
+ ruleIds: [...acc.ruleIds]
28537
+ });
28538
+ }
28539
+ }
28540
+ sorted.sort(compareLocationOrder);
28541
+ const start = cursor === null ? 0 : firstLocationAfter(sorted, cursor);
28542
+ const page = sorted.slice(start, start + limit);
28543
+ const lastOnPage = page.at(-1);
28544
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeLocationCursor(lastOnPage) : null;
28545
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinkedLocation(sorted, page, query.includeId);
27370
28546
  return Promise.resolve({
27371
- totals: { findings: total, repos: repos.length, files: fileCount },
27372
- items: repos.slice(0, limit),
27373
- hasMore: repos.length > limit
28547
+ totals: { findings: total, locations: sorted.length },
28548
+ facets: accumulator.facets(),
28549
+ items: [...page, ...deepLinked ? [deepLinked] : []],
28550
+ nextCursor
27374
28551
  });
27375
28552
  }
27376
- /**
27377
- * Each group's newest instances, for the table's expanded rows.
27378
- *
27379
- * ONE index-ordered scan with early termination, and the shape is the point.
27380
- * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
27381
- * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
27382
- * through a temp B-tree to keep a bounded preview of each group, and then
27383
- * sorts the survivors again for the page order. Both sorts grow with the
27384
- * store while the answer does not.
27385
- *
27386
- * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
27387
- * (or the session or window index the scope names — see `findingScanSql`),
27388
- * which is already the order the page wants, and keeps rows per rule until
27389
- * each rule has as many as it can show. The aggregate the caller already holds
27390
- * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
27391
- * per rule, summed, is the number of rows this scan has to find, and it stops
27392
- * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
27393
- * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
27394
- * store with many firing rules widens it. The bound that DOES hold
27395
- * unconditionally is the sorted form's floor: this scan visits at most as
27396
- * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
27397
- * sorted, and stops the moment every rule has its cap, where the sorted form
27398
- * sorts the whole scope regardless. The true worst case — the rarest rule's
27399
- * wanted instances sitting at the tail of the scope — is one pass over
27400
- * everything in scope with a block sort of the id tie-break only, never a
27401
- * sort of the scope, which is still that floor.
27402
- *
27403
- * A row whose rule the aggregate did not see is skipped: the two statements
27404
- * run without a shared snapshot, so a capture landing between them can add a
27405
- * rule here that has no counts there, and the counts are what the group is
27406
- * built from.
27407
- */
27408
- previewRows(aggregates, scope) {
27409
- const wanted = /* @__PURE__ */ new Map();
27410
- let remaining = 0;
27411
- for (const [ruleId, agg] of aggregates) {
27412
- const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
27413
- wanted.set(ruleId, n);
27414
- remaining += n;
27415
- }
27416
- const rows = [];
27417
- if (remaining === 0) return rows;
27418
- const { sql, params } = this.findingScanSql(scope);
27419
- const taken = /* @__PURE__ */ new Map();
27420
- for (const r of iterateRows(this.db.prepare(sql), params)) {
27421
- const want = wanted.get(r.rule_id);
27422
- if (want === void 0) continue;
27423
- const have = taken.get(r.rule_id) ?? 0;
27424
- if (have >= want) continue;
27425
- taken.set(r.rule_id, have + 1);
27426
- rows.push(r);
27427
- remaining -= 1;
27428
- if (remaining === 0) break;
27429
- }
27430
- return rows;
27431
- }
27432
28553
  /**
27433
28554
  * Every finding in scope as a FlatFindingRow, newest first, streamed.
27434
28555
  *
@@ -27455,6 +28576,33 @@ var SqliteFindingsRepository = class {
27455
28576
  yield toFlatFindingRow(r);
27456
28577
  }
27457
28578
  }
28579
+ /**
28580
+ * One finding by its own id, or null when no such row exists.
28581
+ *
28582
+ * A primary-key seek on `inspection_findings`, so its cost does not grow with
28583
+ * the store — and, unlike anything derived from a list page, it resolves a
28584
+ * finding of ANY age. That is what the Findings page's one-shot `?finding=`
28585
+ * deep link needs: the id it carries may name a finding thousands of rows
28586
+ * older than anything a first page holds.
28587
+ *
28588
+ * Deliberately UNFILTERED — no capture-kind, session or time predicate. It
28589
+ * RESOLVES an id; whether that row would survive the list's current filters is
28590
+ * a different question, and hiding the target because a filter excludes it is
28591
+ * worse than showing it.
28592
+ *
28593
+ * `groupId` on the result IS the rule id, so this one read answers both "which
28594
+ * type should the list select?" and "what does the drawer show?".
28595
+ */
28596
+ findingInstance(id) {
28597
+ const row = this.db.prepare(
28598
+ `SELECT ${FINDING_ROW_COLUMNS_SQL}
28599
+ FROM inspection_findings f
28600
+ JOIN audit_events e ON e.id = f.audit_event_id
28601
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
28602
+ WHERE f.id = ?`
28603
+ ).get(id);
28604
+ return Promise.resolve(row === void 0 ? null : toInstanceDetail(toFlatFindingRow(row)));
28605
+ }
27458
28606
  /**
27459
28607
  * The one statement both instance-level scans run: every finding in scope,
27460
28608
  * joined to its event and definition, newest first.
@@ -27488,17 +28636,7 @@ var SqliteFindingsRepository = class {
27488
28636
  conditions.push("e.started_at >= ?");
27489
28637
  params.push(isoToEpochMillis(scope.from));
27490
28638
  }
27491
- const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
27492
- d.severity AS severity, f.masked_match AS masked_match,
27493
- f.action_taken AS action_taken, f.confidence AS confidence,
27494
- e.started_at AS occurred_at,
27495
- e.source_tool AS source_tool,
27496
- e.repo AS repo,
27497
- e.file_path AS file,
27498
- e.tool_name AS tool_name,
27499
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
27500
- e.event_type AS kind, f.finding_key AS finding_key,
27501
- ${latestResolutionStatusSql("f")} AS latest_status
28639
+ const sql = `SELECT ${FINDING_ROW_COLUMNS_SQL}
27502
28640
  FROM audit_events e
27503
28641
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27504
28642
  CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
@@ -27512,6 +28650,26 @@ var SqliteFindingsRepository = class {
27512
28650
  group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27513
28651
  const rows = this.db.prepare(
27514
28652
  `SELECT rule_id,
28653
+ -- BARE columns beside max(latest_at), which is deliberate and
28654
+ -- is SQLite's documented behaviour: with a single min()/max()
28655
+ -- in an aggregate query, every bare column takes its value from
28656
+ -- the row that produced the extremum. So these are the severity
28657
+ -- and category of the definition whose finding is NEWEST, which
28658
+ -- is what the row-based build they replaced read off its first
28659
+ -- (newest-first) row.
28660
+ --
28661
+ -- min() is WRONG here and was the defect: inspection_definitions
28662
+ -- holds one row per rule VERSION (see its writer \u2014 a version bump
28663
+ -- mints a new row), so a rule whose severity moved between
28664
+ -- versions has several, and min() picks the ALPHABETICALLY
28665
+ -- smallest \u2014 'low' over 'medium', but 'critical' over 'high'.
28666
+ -- That is arbitrary in direction, and it feeds the badge, the
28667
+ -- filter, the facet counts and the primary sort key.
28668
+ --
28669
+ -- Adding a second min()/max() aggregate here would make these
28670
+ -- bare columns ambiguous again; keep max(latest_at) the only one.
28671
+ severity,
28672
+ category,
27515
28673
  sum(tuple_count) AS instance_count,
27516
28674
  max(latest_at) AS latest_at,
27517
28675
  group_concat(source_tools) AS source_tools,
@@ -27522,6 +28680,14 @@ var SqliteFindingsRepository = class {
27522
28680
  group_concat(tool_names) AS tool_names
27523
28681
  FROM (
27524
28682
  SELECT d.rule_id AS rule_id,
28683
+ -- Severity and category are columns of the DEFINITION, and
28684
+ -- a rule can have SEVERAL definitions (one per version), so
28685
+ -- these are grouped on below and resolved to the newest
28686
+ -- firing version by the outer query's bare-column select.
28687
+ -- They ride the aggregate because the type build has no rows
28688
+ -- to read them off \u2014 see buildFindingTypes.
28689
+ d.severity AS severity,
28690
+ d.category AS category,
27525
28691
  e.event_type || '${TUPLE_SEP}' ||
27526
28692
  (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
27527
28693
  coalesce(latest.status, '') AS status_tuple,
@@ -27536,7 +28702,7 @@ var SqliteFindingsRepository = class {
27536
28702
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
27537
28703
  ON latest.finding_key = f.finding_key
27538
28704
  ${scope.predicate}
27539
- GROUP BY d.rule_id, status_tuple
28705
+ GROUP BY d.rule_id, d.severity, d.category, status_tuple
27540
28706
  )
27541
28707
  GROUP BY rule_id`
27542
28708
  ).all(scope.params);
@@ -27545,6 +28711,8 @@ var SqliteFindingsRepository = class {
27545
28711
  r.rule_id,
27546
28712
  {
27547
28713
  instanceCount: r.instance_count,
28714
+ severity: r.severity,
28715
+ category: r.category,
27548
28716
  sourceTools: splitConcat(r.source_tools),
27549
28717
  actionsTaken: splitConcat(r.actions_taken),
27550
28718
  statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
@@ -27561,7 +28729,7 @@ var SqliteFindingsRepository = class {
27561
28729
  latestDetectedAt: epochMillisToIso(r.latest_at),
27562
28730
  // Free text only — joined and substring-matched, so group_concat's
27563
28731
  // commas need no unpicking (a repo/path containing one still matches).
27564
- // Left undefined (not '') when unfetched, so buildFindingGroups can
28732
+ // Left undefined (not '') when unfetched, so buildFindingTypes can
27565
28733
  // tell "no q this request" from "a group with no repo/file at all"
27566
28734
  // and skip priming a haystack nothing will read.
27567
28735
  ...withSearchText ? {
@@ -27589,7 +28757,9 @@ var SqliteFindingsRepository = class {
27589
28757
  )
27590
28758
  );
27591
28759
  for (const row of grouped) {
27592
- if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
28760
+ if (Object.hasOwn(byAction, row.action_taken)) {
28761
+ byAction[row.action_taken] = row.c;
28762
+ }
27593
28763
  }
27594
28764
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
27595
28765
  const sevRows = allRows(
@@ -27606,7 +28776,9 @@ var SqliteFindingsRepository = class {
27606
28776
  )
27607
28777
  );
27608
28778
  for (const row of sevRows) {
27609
- if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
28779
+ if (Object.hasOwn(bySeverity, row.severity)) {
28780
+ bySeverity[row.severity] = row.c;
28781
+ }
27610
28782
  }
27611
28783
  const categories = ENFORCEABLE_CATEGORIES;
27612
28784
  const enabledRows = allRows(
@@ -27655,469 +28827,6 @@ function isoDay(ms) {
27655
28827
  return new Date(ms).toISOString().slice(0, 10);
27656
28828
  }
27657
28829
 
27658
- // ../../packages/persistence/src/repositories/history-sync.ts
27659
- var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27660
- var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27661
- var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27662
- var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27663
- var SKIPPED = -1;
27664
- var ROW_COLUMNS = `id,
27665
- parent_id AS parentId,
27666
- root_session_id AS rootSessionId,
27667
- event_type AS eventType,
27668
- host_id AS hostId,
27669
- harness_id AS harnessId,
27670
- source_project_id AS sourceProjectId,
27671
- started_at AS startedAt,
27672
- ended_at AS endedAt,
27673
- severity,
27674
- priority,
27675
- content,
27676
- content_hash AS contentHash,
27677
- attributes`;
27678
- var SqliteHistorySyncRepository = class {
27679
- constructor(db) {
27680
- this.db = db;
27681
- this.ensureRowStmt = db.prepare(`INSERT OR IGNORE INTO history_sync (id) VALUES (1)`);
27682
- this.sessionsStmt = db.prepare(
27683
- `SELECT COALESCE(root_session_id, id) AS sessionId, MIN(started_at) AS earliest
27684
- FROM audit_events
27685
- WHERE synced_at IS NULL
27686
- AND event_type IN (${TYPE_LIST})
27687
- AND started_at < :before
27688
- GROUP BY sessionId
27689
- ORDER BY earliest
27690
- LIMIT :limit`
27691
- );
27692
- this.rowsStmt = db.prepare(
27693
- `SELECT ${ROW_COLUMNS}
27694
- FROM audit_events
27695
- WHERE synced_at IS NULL
27696
- AND event_type IN (${TYPE_LIST})
27697
- AND started_at < :before
27698
- AND COALESCE(root_session_id, id) = :sessionId
27699
- ORDER BY (event_type = 'session') DESC, started_at
27700
- LIMIT :limit`
27701
- );
27702
- this.captureRowsStmt = db.prepare(
27703
- `SELECT ${ROW_COLUMNS}
27704
- FROM audit_events
27705
- WHERE synced_at IS NULL
27706
- AND sync_claimed_at IS NULL
27707
- AND outbox_owed = 1
27708
- AND event_type IN (${CAPTURE_TYPE_LIST})
27709
- AND started_at < :before
27710
- ORDER BY started_at
27711
- LIMIT :limit`
27712
- );
27713
- this.markOwedStmt = db.prepare(
27714
- `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27715
- );
27716
- this.stampStmt = db.prepare(
27717
- `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27718
- );
27719
- this.claimRowStmt = db.prepare(
27720
- `UPDATE audit_events SET sync_claimed_at = :at WHERE id = :id AND synced_at IS NULL`
27721
- );
27722
- this.releaseRowStmt = db.prepare(
27723
- `UPDATE audit_events SET sync_claimed_at = NULL WHERE id = :id`
27724
- );
27725
- this.releaseStaleClaimsStmt = db.prepare(
27726
- `UPDATE audit_events SET sync_claimed_at = NULL
27727
- WHERE sync_claimed_at IS NOT NULL AND sync_claimed_at < :staleBefore`
27728
- );
27729
- this.partitionStmt = db.prepare(
27730
- `SELECT
27731
- SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NULL THEN 1 ELSE 0 END) AS queued,
27732
- SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NOT NULL THEN 1 ELSE 0 END) AS inProgress,
27733
- SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS synced,
27734
- SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0 THEN 1 ELSE 0 END) AS failed,
27735
- COUNT(*) AS total
27736
- FROM audit_events
27737
- WHERE event_type IN (${TYPE_LIST})`
27738
- );
27739
- this.countsStmt = db.prepare(
27740
- `SELECT
27741
- SUM(CASE WHEN synced_at IS NULL AND started_at < :before THEN 1 ELSE 0 END) AS pending,
27742
- SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS sent,
27743
- SUM(CASE WHEN synced_at = ${String(SKIPPED)} THEN 1 ELSE 0 END) AS skipped
27744
- FROM audit_events
27745
- WHERE event_type IN (${TYPE_LIST})`
27746
- );
27747
- this.captureSkipCountStmt = db.prepare(
27748
- `SELECT COUNT(*) AS skipped
27749
- FROM audit_events
27750
- WHERE synced_at = ${String(SKIPPED)}
27751
- AND event_type IN (${CAPTURE_TYPE_LIST})`
27752
- );
27753
- this.fingerprintStmt = db.prepare(
27754
- `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27755
- FROM history_sync WHERE id = 1`
27756
- );
27757
- this.setFingerprintStmt = db.prepare(
27758
- `UPDATE history_sync
27759
- SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27760
- WHERE id = 1`
27761
- );
27762
- this.disownCapturesStmt = db.prepare(
27763
- `UPDATE audit_events SET outbox_owed = NULL
27764
- WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
27765
- );
27766
- this.rearmStmt = db.prepare(
27767
- `UPDATE audit_events SET synced_at = NULL
27768
- WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
27769
- );
27770
- this.claimStmt = db.prepare(
27771
- `UPDATE history_sync
27772
- SET owner_pid = :pid, owner_host = :host, acquired_at = :now, heartbeat_at = :now
27773
- WHERE id = 1
27774
- AND (owner_pid IS NULL
27775
- OR heartbeat_at IS NULL
27776
- OR heartbeat_at < :staleBefore
27777
- OR heartbeat_at > :now)`
27778
- );
27779
- this.heartbeatStmt = db.prepare(
27780
- `UPDATE history_sync SET heartbeat_at = :now WHERE id = 1 AND owner_pid = :pid`
27781
- );
27782
- this.releaseStmt = db.prepare(
27783
- `UPDATE history_sync
27784
- SET owner_pid = NULL, owner_host = NULL, acquired_at = NULL, heartbeat_at = NULL
27785
- WHERE id = 1 AND owner_pid = :pid`
27786
- );
27787
- this.closeWindowStmt = db.prepare(
27788
- `UPDATE audit_events SET synced_at = :at
27789
- WHERE synced_at IS NULL
27790
- AND event_type IN (${TYPE_LIST})
27791
- AND started_at >= :attachedAt`
27792
- );
27793
- this.releaseBoundaryStmt = db.prepare(
27794
- `UPDATE history_sync SET backlog_before = NULL WHERE id = 1`
27795
- );
27796
- this.freezeBoundaryStmt = db.prepare(
27797
- `UPDATE history_sync SET backlog_before = :backlogBefore WHERE id = 1`
27798
- );
27799
- this.leaseStmt = db.prepare(
27800
- `SELECT owner_pid AS ownerPid, owner_host AS ownerHost,
27801
- acquired_at AS acquiredAt, heartbeat_at AS heartbeatAt
27802
- FROM history_sync WHERE id = 1`
27803
- );
27804
- this.inspectionsStmt = db.prepare(
27805
- `SELECT d.rule_id AS ruleId,
27806
- d.name AS ruleName,
27807
- d.version AS ruleVersion,
27808
- d.category AS category,
27809
- d.severity AS severity,
27810
- f.span_start AS spanStart,
27811
- f.span_end AS spanEnd,
27812
- f.masked_match AS maskedMatch,
27813
- f.action_taken AS actionTaken,
27814
- f.confidence AS confidence
27815
- FROM inspection_findings f
27816
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27817
- WHERE f.audit_event_id = :auditEventId
27818
- ORDER BY f.span_start, f.id`
27819
- );
27820
- }
27821
- db;
27822
- ensureRowStmt;
27823
- sessionsStmt;
27824
- rowsStmt;
27825
- stampStmt;
27826
- countsStmt;
27827
- fingerprintStmt;
27828
- setFingerprintStmt;
27829
- rearmStmt;
27830
- claimStmt;
27831
- heartbeatStmt;
27832
- releaseStmt;
27833
- leaseStmt;
27834
- inspectionsStmt;
27835
- closeWindowStmt;
27836
- releaseBoundaryStmt;
27837
- freezeBoundaryStmt;
27838
- captureRowsStmt;
27839
- markOwedStmt;
27840
- captureSkipCountStmt;
27841
- disownCapturesStmt;
27842
- partitionStmt;
27843
- claimRowStmt;
27844
- releaseRowStmt;
27845
- releaseStaleClaimsStmt;
27846
- /**
27847
- * The masked detections recorded against one tool call.
27848
- *
27849
- * These travel with the event because a tool call's target is not
27850
- * re-inspectable from the event alone — unlike a capture, where the text
27851
- * itself is re-scannable. What crosses is the masked match and the rule that
27852
- * produced it, never the value.
27853
- */
27854
- inspectionsFor(auditEventId) {
27855
- return allRows(this.inspectionsStmt, { auditEventId });
27856
- }
27857
- /**
27858
- * Sessions with structural rows still to send, oldest first.
27859
- *
27860
- * BOUNDED BY THE BACKLOG BOUNDARY, which is the whole correctness of this
27861
- * read. Anything recorded after the machine attached is the live forward
27862
- * path's to deliver; this drain exists for what was recorded before it, and a
27863
- * row both paths send is at best a duplicate request and at worst — for a
27864
- * session root — an overwrite of the inventory ids the live path resolved.
27865
- */
27866
- pendingSessions(limit, before) {
27867
- return allRows(this.sessionsStmt, { limit, before }).map(
27868
- (r) => r.sessionId
27869
- );
27870
- }
27871
- /** One session's undelivered structural rows within the backlog, root first. */
27872
- pendingRows(sessionId, limit, before) {
27873
- return allRows(this.rowsStmt, { sessionId, limit, before });
27874
- }
27875
- /**
27876
- * Captures this machine still owes the deployment, oldest first.
27877
- *
27878
- * Selected by the `outbox_owed` marker the attached forward path writes, not
27879
- * by a time window — see captureRowsStmt for why a window could not express
27880
- * this. `before` is the grace window that leaves a just-recorded capture to
27881
- * the live path.
27882
- */
27883
- pendingCaptureRows(limit, before) {
27884
- return allRows(this.captureRowsStmt, { limit, before });
27885
- }
27886
- /**
27887
- * Record that a capture is OWED to the deployment.
27888
- *
27889
- * Written by the attached forward path when a live send did not confirm
27890
- * delivery, and read by the drain as the whole of its eligibility test. It is
27891
- * a fact rather than an inference: the machine was attached, the send did not
27892
- * land, so the row is owed — which no time window can state, because the same
27893
- * window that holds the rows a past attachment left owed also holds every
27894
- * capture recorded while the machine was DETACHED, and those were never
27895
- * offered to anyone.
27896
- *
27897
- * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27898
- * out of the drain's read.
27899
- */
27900
- markCaptureOwed(id) {
27901
- this.markOwedStmt.run({ id });
27902
- }
27903
- /** Record delivery. Called only AFTER the far side has accepted the rows. */
27904
- markSynced(ids, atMs) {
27905
- this.stampAll(ids, atMs);
27906
- }
27907
- /**
27908
- * Record that a row will never be sent.
27909
- *
27910
- * Reserved for a local defect — a row that cannot be rebuilt into a valid
27911
- * payload. A row that merely failed to reach the deployment stays NULL, so it
27912
- * is retried; marking those would turn one outage into permanent data loss.
27913
- */
27914
- markSkipped(ids) {
27915
- this.stampAll(ids, SKIPPED);
27916
- }
27917
- eachInTransaction(ids, run) {
27918
- if (ids.length === 0) return;
27919
- withTransaction(
27920
- this.db,
27921
- () => {
27922
- for (const id of ids) run(id);
27923
- },
27924
- "IMMEDIATE"
27925
- );
27926
- }
27927
- stampAll(ids, value) {
27928
- if (ids.length === 0) return;
27929
- withTransaction(
27930
- this.db,
27931
- () => {
27932
- for (const id of ids) this.stampStmt.run({ at: value, id });
27933
- },
27934
- "IMMEDIATE"
27935
- );
27936
- }
27937
- /**
27938
- * Claim rows as in-flight.
27939
- *
27940
- * Advisory in exactly the sense the lease is: it records that a send is in
27941
- * progress so a surface can say so, and a lost claim costs a row showing as
27942
- * queued while it is actually being sent. It is not exclusion — the far side
27943
- * settles a duplicate on the row id.
27944
- */
27945
- claimRows(ids, atMs) {
27946
- this.eachInTransaction(ids, (id) => this.claimRowStmt.run({ at: atMs, id }));
27947
- }
27948
- /** Give back a claim without settling — the send failed, the row is queued again. */
27949
- releaseRows(ids) {
27950
- this.eachInTransaction(ids, (id) => this.releaseRowStmt.run({ id }));
27951
- }
27952
- /**
27953
- * Clear claims older than `staleBefore`, and report how many were cleared.
27954
- *
27955
- * A process killed between claiming and settling leaves rows claimed with
27956
- * nothing left to settle them. Without this they read as "sending" for ever.
27957
- */
27958
- releaseStaleClaims(staleBefore) {
27959
- return Number(this.releaseStaleClaimsStmt.run({ staleBefore }).changes);
27960
- }
27961
- /**
27962
- * Every tracked row in exactly one delivery state.
27963
- *
27964
- * Takes no boundary on purpose. The boundary answers "what should the drain
27965
- * pick up now", which is a different question from "what state is this row
27966
- * in" — and a machine that has never attached has no boundary to pass, so
27967
- * requiring one would force a caller to invent one and report the whole store
27968
- * as queued.
27969
- */
27970
- partition() {
27971
- const row = getRow(this.partitionStmt, {});
27972
- return {
27973
- queued: row?.queued ?? 0,
27974
- inProgress: row?.inProgress ?? 0,
27975
- synced: row?.synced ?? 0,
27976
- failed: row?.failed ?? 0,
27977
- total: row?.total ?? 0
27978
- };
27979
- }
27980
- /** `pending` counts only what is inside the backlog; sent and skipped are totals. */
27981
- counts(before) {
27982
- const row = getRow(
27983
- this.countsStmt,
27984
- { before }
27985
- );
27986
- const captures = getRow(this.captureSkipCountStmt);
27987
- return {
27988
- pending: row?.pending ?? 0,
27989
- sent: row?.sent ?? 0,
27990
- skipped: row?.skipped ?? 0,
27991
- capturesSkipped: captures?.skipped ?? 0
27992
- };
27993
- }
27994
- /**
27995
- * The deployment the current stamps were made against, and where its backlog
27996
- * ends.
27997
- *
27998
- * READ-ONLY. An absent row reads as an absent deployment, which is what a
27999
- * machine that has never drained is — and every writer below seeds the row
28000
- * before it needs one, so nothing depends on this creating it. Keeping the
28001
- * write off the gate path matters because the gate runs on every pass while a
28002
- * write has to take the database's write lock.
28003
- */
28004
- deployment() {
28005
- const row = getRow(
28006
- this.fingerprintStmt
28007
- );
28008
- return {
28009
- fingerprint: row?.fingerprint ?? void 0,
28010
- backlogBefore: row?.backlogBefore ?? void 0
28011
- };
28012
- }
28013
- /**
28014
- * Point the ledger at a different deployment, discarding what it recorded
28015
- * about the previous one.
28016
- *
28017
- * Delivery is a fact about ONE recipient: rows sent to the deployment a
28018
- * machine has just left are undelivered as far as the new one is concerned.
28019
- * All three in one transaction, so a crash between them cannot leave stamps
28020
- * attributed to the wrong deployment, or a boundary that belongs to another.
28021
- *
28022
- * The boundary is written HERE and only here, which is what freezes it: a
28023
- * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
28024
- * unchanged, so this never runs and the backlog does not widen back over rows
28025
- * the live path has since delivered.
28026
- */
28027
- rearmFor(fingerprint, backlogBefore) {
28028
- this.ensureRowStmt.run();
28029
- withTransaction(
28030
- this.db,
28031
- () => {
28032
- const previous = getRow(this.fingerprintStmt)?.fingerprint;
28033
- this.rearmStmt.run();
28034
- if (previous !== null && previous !== void 0 && previous !== fingerprint) {
28035
- this.disownCapturesStmt.run();
28036
- }
28037
- this.setFingerprintStmt.run({ fingerprint, backlogBefore });
28038
- },
28039
- "IMMEDIATE"
28040
- );
28041
- }
28042
- /**
28043
- * End the attached period: hand its rows to the live path, and release the
28044
- * boundary so the next attachment can freeze a new one.
28045
- *
28046
- * WITHOUT THIS, a detach and a re-attach to the same deployment leave a window
28047
- * nothing delivers. The fingerprint is unchanged, so the boundary is never
28048
- * re-frozen and stays at the FIRST attachment — while nothing forwards at all
28049
- * during the detached period, because the machine is not attached. Rows
28050
- * recorded in that window sit after the boundary and before the re-attach, so
28051
- * neither path takes them, and the pending count reports none outstanding.
28052
- *
28053
- * Stamping the attached window is not a claim that every one of those rows
28054
- * reached the deployment — the live path drops on failure and says so
28055
- * elsewhere. It records that they were ITS to deliver, which is exactly the
28056
- * status quo: they sit outside the frozen boundary today and are equally never
28057
- * re-sent. Making it explicit is what lets the boundary move.
28058
- *
28059
- * ONE TRANSACTION, so a crash cannot release the boundary while leaving the
28060
- * window unstamped — that half-state would re-send the whole attached period
28061
- * on the next attach, which is the failure the boundary exists to prevent.
28062
- */
28063
- closeAttachedWindow(attachedAtMs, atMs) {
28064
- this.ensureRowStmt.run();
28065
- withTransaction(
28066
- this.db,
28067
- () => {
28068
- const row = getRow(this.fingerprintStmt);
28069
- const from = row?.backlogBefore ?? attachedAtMs;
28070
- this.closeWindowStmt.run({ at: atMs, attachedAt: from });
28071
- this.releaseBoundaryStmt.run();
28072
- },
28073
- "IMMEDIATE"
28074
- );
28075
- }
28076
- /**
28077
- * Freeze a boundary for the deployment already on file, KEEPING the stamps.
28078
- *
28079
- * The re-attach half of the above. Distinct from `rearmFor`, which is for a
28080
- * different deployment and therefore discards what was delivered to the old
28081
- * one: here the recipient is the same, so everything already sent to it stays
28082
- * sent.
28083
- */
28084
- freezeBoundary(backlogBefore) {
28085
- this.ensureRowStmt.run();
28086
- this.freezeBoundaryStmt.run({ backlogBefore });
28087
- }
28088
- /** Take the claim, or report that someone live already holds it. */
28089
- claim(pid, host, nowMs, staleAfterMs) {
28090
- this.ensureRowStmt.run();
28091
- let taken = false;
28092
- withTransaction(
28093
- this.db,
28094
- () => {
28095
- const result = this.claimStmt.run({
28096
- pid,
28097
- host,
28098
- now: nowMs,
28099
- staleBefore: nowMs - staleAfterMs
28100
- });
28101
- taken = result.changes === 1;
28102
- },
28103
- "IMMEDIATE"
28104
- );
28105
- return taken;
28106
- }
28107
- /** Say the holder is still alive. A no-op once the claim has moved on. */
28108
- heartbeat(pid, nowMs) {
28109
- this.heartbeatStmt.run({ now: nowMs, pid });
28110
- }
28111
- /** Give the claim up. Scoped to this holder, so a taken-over claim is left be. */
28112
- release(pid) {
28113
- this.releaseStmt.run({ pid });
28114
- }
28115
- /** Who holds the claim, if anyone. Read-only, for the same reason as above. */
28116
- lease() {
28117
- return getRow(this.leaseStmt);
28118
- }
28119
- };
28120
-
28121
28830
  // ../../packages/persistence/src/repositories/inspection-definitions.ts
28122
28831
  var SqliteInspectionDefinitionsRepository = class {
28123
28832
  constructor(db) {
@@ -28312,7 +29021,8 @@ function managedSettingsPaths(platform2 = process.platform) {
28312
29021
  }
28313
29022
  return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28314
29023
  }
28315
- function readManagedSettings(paths = managedSettingsPaths()) {
29024
+ var testOnlyManagedPaths = null;
29025
+ function readManagedSettings(paths = testOnlyManagedPaths ?? managedSettingsPaths()) {
28316
29026
  for (const path of paths) {
28317
29027
  let text;
28318
29028
  try {
@@ -28347,6 +29057,7 @@ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ n
28347
29057
  if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28348
29058
  if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28349
29059
  if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
29060
+ if (values.bodyRetention !== void 0) merged.bodyRetention = values.bodyRetention;
28350
29061
  if (values.vaultConsent !== void 0) {
28351
29062
  merged.vaultConsent = values.vaultConsent ? (
28352
29063
  // Keep an existing valid grant so its acknowledgedAt survives; mint one
@@ -30792,7 +31503,7 @@ function toUtcDateString(ms) {
30792
31503
  return new Date(ms).toISOString().slice(0, 10);
30793
31504
  }
30794
31505
  function isTimeseriesSeverity(s) {
30795
- return s === "critical" || s === "high" || s === "medium";
31506
+ return s === "critical" || s === "high" || s === "medium" || s === "low";
30796
31507
  }
30797
31508
  var SqliteSecurityRepository = class {
30798
31509
  constructor(db, now = () => Date.now()) {
@@ -30854,7 +31565,7 @@ var SqliteSecurityRepository = class {
30854
31565
  ELSE 0
30855
31566
  END) AS open_at_rest
30856
31567
  FROM inspection_findings f
30857
- JOIN audit_events e ON e.id = f.audit_event_id
31568
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
30858
31569
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
30859
31570
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
30860
31571
  ON latest.finding_key = f.finding_key
@@ -30921,12 +31632,16 @@ var SqliteSecurityRepository = class {
30921
31632
  const now = this.now();
30922
31633
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
30923
31634
  const rows = this.findingsInRange(windowStart, now);
30924
- const points = Array.from({ length: numBuckets }, (_, i) => ({
30925
- timestamp: toUtcDateString(windowStart + i * bucketMs),
30926
- critical: 0,
30927
- high: 0,
30928
- medium: 0
30929
- }));
31635
+ const points = Array.from(
31636
+ { length: numBuckets },
31637
+ (_, i) => ({
31638
+ timestamp: toUtcDateString(windowStart + i * bucketMs),
31639
+ critical: 0,
31640
+ high: 0,
31641
+ medium: 0,
31642
+ low: 0
31643
+ })
31644
+ );
30930
31645
  for (const r of rows) {
30931
31646
  const idx = Math.floor((r.occurredAt - windowStart) / bucketMs);
30932
31647
  const bucket = points[idx];
@@ -31076,7 +31791,7 @@ var SqliteSecurityRepository = class {
31076
31791
  this.db.prepare(
31077
31792
  `SELECT e.repo AS repo, count(*) AS c
31078
31793
  FROM inspection_findings f
31079
- JOIN audit_events e ON e.id = f.audit_event_id
31794
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31080
31795
  WHERE e.started_at >= :from AND e.started_at < :to
31081
31796
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
31082
31797
  AND e.repo IS NOT NULL
@@ -31144,6 +31859,7 @@ var SqliteSecurityRepository = class {
31144
31859
  `SELECT f.finding_key AS finding_key,
31145
31860
  d.rule_id AS rule_id,
31146
31861
  d.severity AS severity,
31862
+ e.repo AS repo,
31147
31863
  e.file_path AS path,
31148
31864
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
31149
31865
  latest.resolved_at AS latest_resolved_at
@@ -31163,6 +31879,7 @@ var SqliteSecurityRepository = class {
31163
31879
  const items = rows.map((r) => ({
31164
31880
  findingKey: r.finding_key,
31165
31881
  ruleId: r.rule_id,
31882
+ repo: r.repo ?? "",
31166
31883
  severity: r.severity,
31167
31884
  path: r.path ?? "",
31168
31885
  resolvedAt: new Date(r.latest_resolved_at).toISOString(),
@@ -31172,15 +31889,68 @@ var SqliteSecurityRepository = class {
31172
31889
  }));
31173
31890
  return Promise.resolve({ items });
31174
31891
  }
31892
+ /**
31893
+ * Per-rule tallies of the findings that are still OPEN, whole-store.
31894
+ *
31895
+ * Scoped by status rather than by time, because the card this feeds is a to-do
31896
+ * list: a secret committed three weeks ago and never rotated is still the most
31897
+ * important thing to fix, and any window hides it. It carried a "newest N
31898
+ * findings" cap and then a range; the first meant a different span on every
31899
+ * machine, and the second reported "no recommendations" over live exposure.
31900
+ *
31901
+ * `open` mirrors `deriveFindingStatus` — at-rest, minus resolved and dismissed —
31902
+ * so a row's count is exactly what `?status=open&type=<rule>` returns. Note that
31903
+ * is NOT `severitySummary`'s `openAtRest`, which keeps dismissed findings (a
31904
+ * dismissal is a judgement, not a remediation) and drops untracked legacy rows.
31905
+ * The two answer different questions and only this one has to match a link.
31906
+ *
31907
+ * Aggregated in SQL: the result is O(distinct rule × category × severity), so a
31908
+ * whole-store scope costs a grouped scan rather than a row per finding.
31909
+ */
31910
+ recommendationInputs() {
31911
+ const rows = allRows(
31912
+ this.db.prepare(
31913
+ `SELECT d.rule_id AS rule_id,
31914
+ d.category AS category,
31915
+ d.severity AS severity,
31916
+ COUNT(*) AS count
31917
+ FROM inspection_findings f
31918
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31919
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31920
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
31921
+ ON latest.finding_key = f.finding_key
31922
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
31923
+ AND e.event_type = 'code_change'
31924
+ AND (
31925
+ f.finding_key IS NULL
31926
+ OR latest.status IS NULL
31927
+ OR latest.status NOT IN ('resolved', 'dismissed')
31928
+ )
31929
+ GROUP BY d.rule_id, d.category, d.severity`
31930
+ )
31931
+ );
31932
+ return Promise.resolve(
31933
+ rows.map((r) => ({
31934
+ ruleId: r.rule_id,
31935
+ category: r.category,
31936
+ severity: r.severity,
31937
+ count: r.count
31938
+ }))
31939
+ );
31940
+ }
31175
31941
  // Findings whose parent event occurred in [fromMs, toMs), with the parent's
31176
31942
  // epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
31177
31943
  // numeric and the JS aggregations bucket/split on ms directly.
31178
31944
  findingsInRange(fromMs, toMs) {
31179
31945
  const rows = allRows(
31180
31946
  this.db.prepare(
31181
- `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
31947
+ // `rule_id`/`category` cost nothing extra: inspection_definitions is already
31948
+ // joined for `severity`, so they are two more columns off a row this read
31949
+ // already fetches. They feed the recommended-actions rollup.
31950
+ `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
31951
+ d.rule_id AS rule_id, d.category AS category
31182
31952
  FROM inspection_findings f
31183
- JOIN audit_events e ON e.id = f.audit_event_id
31953
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31184
31954
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31185
31955
  WHERE e.started_at >= :from AND e.started_at < :to
31186
31956
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
@@ -31191,7 +31961,9 @@ var SqliteSecurityRepository = class {
31191
31961
  return rows.map((r) => ({
31192
31962
  occurredAt: r.occurred_at,
31193
31963
  severity: r.severity,
31194
- actionTaken: r.action_taken
31964
+ actionTaken: r.action_taken,
31965
+ ruleId: r.rule_id,
31966
+ category: r.category
31195
31967
  }));
31196
31968
  }
31197
31969
  };
@@ -32019,6 +32791,7 @@ function openWithPragmas(file2) {
32019
32791
  db.exec("PRAGMA journal_mode = WAL");
32020
32792
  db.exec("PRAGMA busy_timeout = 2000");
32021
32793
  db.exec("PRAGMA foreign_keys = ON");
32794
+ registerSqlFunctions(db);
32022
32795
  } catch (err) {
32023
32796
  closeQuietly(db);
32024
32797
  throw err;
@@ -32048,7 +32821,7 @@ function backupLegacyStore(db, file2) {
32048
32821
  discardStore(file2, backup);
32049
32822
  return backup;
32050
32823
  }
32051
- function openAndInitialize(file2, base) {
32824
+ function openAndInitialize(file2, base, skipTags) {
32052
32825
  let db = openWithPragmas(file2);
32053
32826
  try {
32054
32827
  if (isForeignSqliteLineage(db)) {
@@ -32058,7 +32831,7 @@ function openAndInitialize(file2, base) {
32058
32831
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
32059
32832
  );
32060
32833
  }
32061
- applyMigrations(db, file2);
32834
+ applyMigrations(db, file2, { skipTags });
32062
32835
  tightenPerms(file2);
32063
32836
  const policies = new SqlitePoliciesRepository(db);
32064
32837
  const installedPacks = new SqliteInstalledPacksRepository(db, base);
@@ -32073,6 +32846,7 @@ function openAndInitialize(file2, base) {
32073
32846
  exceptions: new SqliteExceptionsRepository(db),
32074
32847
  resolutions: new SqliteResolutionsRepository(db),
32075
32848
  ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
32849
+ bodyRetention: new SqliteBodyRetentionRepository(db),
32076
32850
  security: new SqliteSecurityRepository(db),
32077
32851
  detections: new SqliteDetectionsRepository(db),
32078
32852
  shares: new SqliteSharesRepository(db),
@@ -32095,7 +32869,8 @@ function openAndInitialize(file2, base) {
32095
32869
  throw err;
32096
32870
  }
32097
32871
  }
32098
- function openLocalDatabase(dir) {
32872
+ var DEFERRED_TAGS = new Set(DEFERRED_MIGRATION_TAGS);
32873
+ function openLocalDatabase(dir, options = {}) {
32099
32874
  ensureDataDirSync(dir);
32100
32875
  const file2 = join7(dir, DB_FILENAME);
32101
32876
  reapStalePartials(file2);
@@ -32107,6 +32882,7 @@ function openLocalDatabase(dir) {
32107
32882
  installedPacks,
32108
32883
  scanLedger,
32109
32884
  historySync,
32885
+ bodyRetention,
32110
32886
  secretVault,
32111
32887
  exceptions,
32112
32888
  resolutions,
@@ -32130,7 +32906,8 @@ function openLocalDatabase(dir) {
32130
32906
  // `dir` is always `<base>/data` — every caller resolves it through
32131
32907
  // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
32132
32908
  // settings/ and data/, and the pack-policy floor needs both halves.
32133
- dirname2(dir)
32909
+ dirname2(dir),
32910
+ options.applyDeferredMigrations === true ? void 0 : DEFERRED_TAGS
32134
32911
  );
32135
32912
  function captureRowId(event) {
32136
32913
  return captureId(
@@ -32323,6 +33100,7 @@ function openLocalDatabase(dir) {
32323
33100
  installedPacks,
32324
33101
  scanLedger,
32325
33102
  historySync,
33103
+ bodyRetention,
32326
33104
  secretVault,
32327
33105
  exceptions,
32328
33106
  resolutions,
@@ -32361,6 +33139,70 @@ function openLocalDatabase(dir) {
32361
33139
  };
32362
33140
  }
32363
33141
 
33142
+ // ../../packages/persistence/src/egress-wire.ts
33143
+ import { createHash as createHash3 } from "crypto";
33144
+ var PROJECT_KEY_DIGEST_VERSION = "v2";
33145
+ var SCP_FORM = /^(?:[^@/]+@)?([^/:]+):(.+)$/;
33146
+ var DOS_DRIVE = /^[A-Za-z]:[\\/]/;
33147
+ var FILE_URL = /^file:\/\//i;
33148
+ var SCHEME_FORM = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?(\/.*)?$/i;
33149
+ var SLASH = "/".charCodeAt(0);
33150
+ var GIT_SUFFIX = ".git";
33151
+ function trimSlashes(path) {
33152
+ let start = 0;
33153
+ let end = path.length;
33154
+ while (start < end && path.charCodeAt(start) === SLASH) start += 1;
33155
+ while (end > start && path.charCodeAt(end - 1) === SLASH) end -= 1;
33156
+ return path.slice(start, end);
33157
+ }
33158
+ function canonicalGitUrl(url2) {
33159
+ const trimmed = url2.trim();
33160
+ if (DOS_DRIVE.test(trimmed) || FILE_URL.test(trimmed)) return trimmed;
33161
+ const scheme = SCHEME_FORM.exec(trimmed);
33162
+ const scp = scheme === null ? SCP_FORM.exec(trimmed) : null;
33163
+ const host = (scheme?.[1] ?? scp?.[1])?.toLowerCase();
33164
+ if (host === void 0) return trimmed;
33165
+ const path = (scheme === null ? scp?.[2] : scheme[2]) ?? "";
33166
+ const bare = trimSlashes(path);
33167
+ const cleaned = bare.endsWith(GIT_SUFFIX) ? bare.slice(0, -GIT_SUFFIX.length) : bare;
33168
+ return cleaned === "" ? host : `${host}/${cleaned}`;
33169
+ }
33170
+ function hashProjectKey(projectKey) {
33171
+ const canonical = projectKey.startsWith("git:") ? `git:${canonicalGitUrl(projectKey.slice("git:".length))}` : projectKey;
33172
+ return createHash3("sha256").update(`${PROJECT_KEY_DIGEST_VERSION}:${canonical}`, "utf8").digest("hex");
33173
+ }
33174
+ function toIngestHit(hit) {
33175
+ return {
33176
+ host: hit.host,
33177
+ kind: hit.kind,
33178
+ name: hit.name,
33179
+ category: hit.category,
33180
+ trust: hit.trust,
33181
+ network: hit.network,
33182
+ method: hit.method,
33183
+ transport: hit.transport,
33184
+ url: hit.url,
33185
+ template: hit.template,
33186
+ dataClass: hit.dataClass,
33187
+ site: {
33188
+ file: hit.site.file,
33189
+ line: hit.site.line,
33190
+ dynamic: hit.site.dynamic,
33191
+ vendored: hit.site.vendored
33192
+ }
33193
+ };
33194
+ }
33195
+ function toEgressIngestRequest(input2) {
33196
+ const { hits, droppedFiles } = capHits(input2.hits, input2.reconcile.mode);
33197
+ const reconcile = withoutDroppedFiles(input2.reconcile, droppedFiles);
33198
+ return {
33199
+ projectKey: hashProjectKey(input2.projectKey),
33200
+ project: input2.project,
33201
+ reconcile,
33202
+ hits: hits.map(toIngestHit)
33203
+ };
33204
+ }
33205
+
32364
33206
  // ../../packages/persistence/src/exception-policy.ts
32365
33207
  var UserGrantPolicyProvider = class {
32366
33208
  #exceptions;
@@ -32382,13 +33224,13 @@ var UserGrantPolicyProvider = class {
32382
33224
  };
32383
33225
 
32384
33226
  // ../../packages/persistence/src/finding-key.ts
32385
- import { createHash as createHash3 } from "crypto";
33227
+ import { createHash as createHash4 } from "crypto";
32386
33228
  function normalizeFilePath(filePath) {
32387
33229
  return filePath.replaceAll("\\", "/");
32388
33230
  }
32389
33231
  function computeFindingKey(input2) {
32390
33232
  const normalizedPath = normalizeFilePath(input2.filePath);
32391
- return createHash3("sha256").update(`${input2.ruleId}\0${normalizedPath}\0${input2.valueFingerprint}`).digest("hex");
33233
+ return createHash4("sha256").update(`${input2.ruleId}\0${normalizedPath}\0${input2.valueFingerprint}`).digest("hex");
32392
33234
  }
32393
33235
 
32394
33236
  // ../../packages/persistence/src/fingerprint.ts
@@ -32514,14 +33356,50 @@ function fingerprintValue(key, raw) {
32514
33356
  return createHmac("sha256", key.material).update(raw, "utf8").digest("hex");
32515
33357
  }
32516
33358
 
32517
- // ../../packages/persistence/src/history-preview.ts
32518
- import { existsSync as existsSync4 } from "fs";
33359
+ // ../../packages/persistence/src/forward-health.ts
33360
+ import { readFileSync as readFileSync7 } from "fs";
32519
33361
  import { join as join9 } from "path";
33362
+ var FAILURES = /* @__PURE__ */ new Set([
33363
+ "unauthorized",
33364
+ "forbidden",
33365
+ "unreachable"
33366
+ ]);
33367
+ var BREAKER_COOLDOWN_MS = 3e4;
33368
+ function parseForwardHealth(raw, nowMs) {
33369
+ try {
33370
+ const parsed2 = JSON.parse(raw);
33371
+ if (typeof parsed2 !== "object" || parsed2 === null) return null;
33372
+ const record2 = parsed2;
33373
+ const failures = typeof record2.consecutiveFailures === "number" && record2.consecutiveFailures >= 0 ? record2.consecutiveFailures : 0;
33374
+ const openedAtMs = typeof record2.openedAtMs === "number" && Number.isFinite(record2.openedAtMs) && record2.openedAtMs <= nowMs ? record2.openedAtMs : null;
33375
+ const lastFailure = typeof record2.lastFailure === "string" && FAILURES.has(record2.lastFailure) ? record2.lastFailure : null;
33376
+ return { consecutiveFailures: failures, openedAtMs, lastFailure };
33377
+ } catch {
33378
+ return null;
33379
+ }
33380
+ }
33381
+ function isForwardPaused(health, nowMs) {
33382
+ const openedAtMs = health?.openedAtMs ?? null;
33383
+ if (openedAtMs === null) return false;
33384
+ return nowMs - openedAtMs < BREAKER_COOLDOWN_MS;
33385
+ }
33386
+
33387
+ // ../../packages/persistence/src/history-backfill.ts
33388
+ import { existsSync as existsSync4 } from "fs";
33389
+ import { join as join10 } from "path";
33390
+
33391
+ // ../../packages/persistence/src/history-preview.ts
33392
+ import { existsSync as existsSync5 } from "fs";
33393
+ import { join as join11 } from "path";
32520
33394
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
32521
33395
 
33396
+ // ../../packages/persistence/src/history-sync-state.ts
33397
+ import { readFileSync as readFileSync8 } from "fs";
33398
+ import { join as join12 } from "path";
33399
+
32522
33400
  // ../../packages/persistence/src/store-symlinks.ts
32523
- import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32524
- import { dirname as dirname3, join as join10, resolve } from "path";
33401
+ import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
33402
+ import { dirname as dirname3, join as join13, resolve } from "path";
32525
33403
  var STORE_DB = "the store database (including the prompt corpus)";
32526
33404
  var STORE_SETTINGS = "your settings file";
32527
33405
  function storeContents(home) {
@@ -32530,7 +33408,7 @@ function storeContents(home) {
32530
33408
  [settingsDir(home), STORE_SETTINGS],
32531
33409
  [dataDir(home), STORE_DB],
32532
33410
  [keysDir(home), "the vault key"],
32533
- [join10(settingsDir(home), "settings.json"), STORE_SETTINGS],
33411
+ [join13(settingsDir(home), "settings.json"), STORE_SETTINGS],
32534
33412
  [dbPath(home), STORE_DB]
32535
33413
  ]);
32536
33414
  }
@@ -32545,7 +33423,7 @@ function symlinkedStorePaths(home, platform2 = process.platform) {
32545
33423
  holds,
32546
33424
  // existsSync follows the link, so a target that is gone reads as
32547
33425
  // absent here while lstat above still sees the link itself.
32548
- missing: !existsSync5(path),
33426
+ missing: !existsSync6(path),
32549
33427
  mode: targetMode(path, platform2)
32550
33428
  }
32551
33429
  ];
@@ -32681,8 +33559,8 @@ function formatPointer(category, keyVersion, pointerId, tag) {
32681
33559
  // ../../packages/persistence/src/vault/key-provider.ts
32682
33560
  import { execFileSync } from "child_process";
32683
33561
  import { randomBytes as randomBytes2 } from "crypto";
32684
- import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32685
- import { join as join11 } from "path";
33562
+ import { chmodSync as chmodSync3, readFileSync as readFileSync9, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
33563
+ import { join as join14 } from "path";
32686
33564
  var VAULT_OCCUPANT_REASON = {
32687
33565
  symlink: "the path is a symlink; remove it so a keyring can be created",
32688
33566
  gone: "the path was occupied but holds no keyring (removed while it was being created)",
@@ -32781,7 +33659,7 @@ function claimRotationLock(lock, owner) {
32781
33659
  throw asError(err);
32782
33660
  }
32783
33661
  try {
32784
- writeFileSync3(join11(lock, LOCK_OWNER_FILE), `${owner}
33662
+ writeFileSync3(join14(lock, LOCK_OWNER_FILE), `${owner}
32785
33663
  `, { mode: DATA_FILE_MODE });
32786
33664
  return true;
32787
33665
  } catch (err) {
@@ -32790,7 +33668,7 @@ function claimRotationLock(lock, owner) {
32790
33668
  }
32791
33669
  }
32792
33670
  function acquireRotationLock(keysDir2) {
32793
- const lock = join11(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
33671
+ const lock = join14(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
32794
33672
  const owner = randomBytes2(16).toString("hex");
32795
33673
  if (claimRotationLock(lock, owner)) return { lock, owner };
32796
33674
  let held;
@@ -32817,7 +33695,7 @@ function acquireRotationLock(keysDir2) {
32817
33695
  }
32818
33696
  function releaseRotationLock(lease) {
32819
33697
  try {
32820
- if (readFileSync7(join11(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
33698
+ if (readFileSync9(join14(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
32821
33699
  } catch {
32822
33700
  return;
32823
33701
  }
@@ -32838,7 +33716,7 @@ var FileKeyProvider = class {
32838
33716
  this.#keysDir = keysDir2;
32839
33717
  }
32840
33718
  get filePath() {
32841
- return join11(this.#keysDir, VAULT_KEY_FILENAME);
33719
+ return join14(this.#keysDir, VAULT_KEY_FILENAME);
32842
33720
  }
32843
33721
  loadOrCreate() {
32844
33722
  return asAsync(() => {
@@ -32868,7 +33746,7 @@ var FileKeyProvider = class {
32868
33746
  #read() {
32869
33747
  let raw;
32870
33748
  try {
32871
- raw = readFileSync7(this.filePath, "utf8");
33749
+ raw = readFileSync9(this.filePath, "utf8");
32872
33750
  } catch (err) {
32873
33751
  if (err.code === "ENOENT") return null;
32874
33752
  throw err instanceof Error ? err : new Error(String(err));
@@ -33503,55 +34381,19 @@ var SecretVault = class {
33503
34381
  };
33504
34382
 
33505
34383
  // ../../packages/persistence/src/warn-era-cap.ts
33506
- import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
33507
- import { join as join12 } from "path";
34384
+ import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
34385
+ import { join as join15 } from "path";
33508
34386
  var MARKER = "warn-era-capped";
33509
34387
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
33510
34388
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
33511
- const marker = join12(dataDir2, MARKER);
33512
- if (existsSync6(marker)) return { capped: 0, skipped: "already-run" };
34389
+ const marker = join15(dataDir2, MARKER);
34390
+ if (existsSync7(marker)) return { capped: 0, skipped: "already-run" };
33513
34391
  const capped = db.policies.capCategoryActions();
33514
34392
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
33515
34393
  `, { mode: DATA_FILE_MODE });
33516
34394
  return { capped };
33517
34395
  }
33518
34396
 
33519
- // ../../packages/plugin-runtime/src/attached/egress-wire.ts
33520
- function hashProjectKey(projectKey) {
33521
- return createHash4("sha256").update(projectKey, "utf8").digest("hex");
33522
- }
33523
- function toIngestHit(hit) {
33524
- return {
33525
- host: hit.host,
33526
- kind: hit.kind,
33527
- name: hit.name,
33528
- category: hit.category,
33529
- trust: hit.trust,
33530
- network: hit.network,
33531
- method: hit.method,
33532
- transport: hit.transport,
33533
- url: hit.url,
33534
- template: hit.template,
33535
- dataClass: hit.dataClass,
33536
- site: {
33537
- file: hit.site.file,
33538
- line: hit.site.line,
33539
- dynamic: hit.site.dynamic,
33540
- vendored: hit.site.vendored
33541
- }
33542
- };
33543
- }
33544
- function toEgressIngestRequest(input2) {
33545
- const { hits, droppedFiles } = capHits(input2.hits, input2.reconcile.mode);
33546
- const reconcile = withoutDroppedFiles(input2.reconcile, droppedFiles);
33547
- return {
33548
- projectKey: hashProjectKey(input2.projectKey),
33549
- project: input2.project,
33550
- reconcile,
33551
- hits: hits.map(toIngestHit)
33552
- };
33553
- }
33554
-
33555
34397
  // ../../packages/remote/src/http.ts
33556
34398
  import { request as httpRequest } from "http";
33557
34399
  import { request as httpsRequest } from "https";
@@ -33735,10 +34577,10 @@ function parsed(schema, body, route) {
33735
34577
  }
33736
34578
  function withoutTrailingSlashes(endpoint) {
33737
34579
  let end = endpoint.length;
33738
- while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH) end -= 1;
34580
+ while (end > 0 && endpoint.charCodeAt(end - 1) === SLASH2) end -= 1;
33739
34581
  return endpoint.slice(0, end);
33740
34582
  }
33741
- var SLASH = "/".charCodeAt(0);
34583
+ var SLASH2 = "/".charCodeAt(0);
33742
34584
  function createRemoteClient(options) {
33743
34585
  const base = withoutTrailingSlashes(options.endpoint);
33744
34586
  const url2 = (route) => `${base}${route}`;
@@ -33831,6 +34673,7 @@ function createRemoteClient(options) {
33831
34673
  url: url2(ROUTES.shares),
33832
34674
  body: JSON.stringify(validated.data)
33833
34675
  });
34676
+ if (response.status === 404) throw new RemoteRouteAbsent(ROUTES.shares);
33834
34677
  okBody(response);
33835
34678
  },
33836
34679
  async pollCommand() {
@@ -33853,19 +34696,51 @@ function createRemoteClient(options) {
33853
34696
  };
33854
34697
  }
33855
34698
 
33856
- // ../../packages/plugin-runtime/src/attached/failure.ts
34699
+ // ../../packages/remote/src/failure-kind.ts
33857
34700
  function statusOf(err) {
33858
34701
  if (typeof err !== "object" || err === null || !("status" in err)) return null;
33859
34702
  const { status } = err;
33860
34703
  if (typeof status !== "number" || !Number.isInteger(status)) return null;
33861
34704
  return status >= 100 && status <= 599 ? status : null;
33862
34705
  }
33863
- function classifyFailure(err) {
33864
- switch (statusOf(err)) {
34706
+ function nameOf(err) {
34707
+ if (typeof err !== "object" || err === null || !("name" in err)) return null;
34708
+ return typeof err.name === "string" ? err.name : null;
34709
+ }
34710
+ function classifyRemoteFailure(err) {
34711
+ switch (nameOf(err)) {
34712
+ case "RemoteRouteAbsent":
34713
+ return "route-absent";
34714
+ case "RemoteRequestInvalid":
34715
+ return "invalid-request";
34716
+ case "RemoteResponseInvalid":
34717
+ return "rejected";
34718
+ default:
34719
+ break;
34720
+ }
34721
+ const status = statusOf(err);
34722
+ if (status === null) return "unreachable";
34723
+ switch (status) {
33865
34724
  case 401:
33866
34725
  return "unauthorized";
33867
34726
  case 403:
33868
34727
  return "forbidden";
34728
+ case 429:
34729
+ return "unreachable";
34730
+ case 404:
34731
+ return "unreachable";
34732
+ default:
34733
+ return status >= 400 && status <= 499 ? "rejected" : "unreachable";
34734
+ }
34735
+ }
34736
+
34737
+ // ../../packages/plugin-runtime/src/attached/failure.ts
34738
+ function classifyFailure(err) {
34739
+ switch (classifyRemoteFailure(err)) {
34740
+ case "unauthorized":
34741
+ return "unauthorized";
34742
+ case "forbidden":
34743
+ return "forbidden";
33869
34744
  default:
33870
34745
  return "unreachable";
33871
34746
  }
@@ -33887,11 +34762,11 @@ function withTimeout(promise2, ms) {
33887
34762
  }
33888
34763
 
33889
34764
  // ../../packages/plugin-runtime/src/attached/forward-drops.ts
33890
- import { readFileSync as readFileSync8 } from "fs";
33891
- import { join as join13 } from "path";
34765
+ import { readFileSync as readFileSync10 } from "fs";
34766
+ import { join as join16 } from "path";
33892
34767
  var FORWARD_DROPS_FILENAME = ATTACHED_FORWARD_DROPS_FILENAME;
33893
34768
  function forwardDropsPath(dataDir2) {
33894
- return join13(dataDir2, FORWARD_DROPS_FILENAME);
34769
+ return join16(dataDir2, FORWARD_DROPS_FILENAME);
33895
34770
  }
33896
34771
  function recordForwardDrops(dataDir2, count, nowMs) {
33897
34772
  if (count <= 0) return;
@@ -33909,7 +34784,7 @@ function recordForwardDrops(dataDir2, count, nowMs) {
33909
34784
  }
33910
34785
  function readForwardDrops(dataDir2) {
33911
34786
  try {
33912
- const parsed2 = JSON.parse(readFileSync8(forwardDropsPath(dataDir2), "utf8"));
34787
+ const parsed2 = JSON.parse(readFileSync10(forwardDropsPath(dataDir2), "utf8"));
33913
34788
  if (typeof parsed2 !== "object" || parsed2 === null) return null;
33914
34789
  const record2 = parsed2;
33915
34790
  if (typeof record2.droppedForwards !== "number" || !Number.isFinite(record2.droppedForwards)) {
@@ -33927,13 +34802,12 @@ function readForwardDrops(dataDir2) {
33927
34802
 
33928
34803
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
33929
34804
  import { randomUUID as randomUUID15 } from "crypto";
33930
- import { readFileSync as readFileSync14 } from "fs";
33931
34805
  import { readFile, rename, writeFile } from "fs/promises";
33932
- import { join as join22 } from "path";
34806
+ import { join as join26 } from "path";
33933
34807
 
33934
34808
  // ../../packages/plugin-sdk/src/config.ts
33935
- import { existsSync as existsSync7 } from "fs";
33936
- import { join as join14 } from "path";
34809
+ import { existsSync as existsSync8 } from "fs";
34810
+ import { join as join17 } from "path";
33937
34811
 
33938
34812
  // ../../packages/plugin-sdk/src/provider-env.ts
33939
34813
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -33987,8 +34861,8 @@ function resolveProvider() {
33987
34861
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
33988
34862
  try {
33989
34863
  ensureLayoutDirSync(base);
33990
- const settingsFile = join14(settingsDir(base), "settings.json");
33991
- if (existsSync7(settingsFile)) tightenFile(settingsFile);
34864
+ const settingsFile = join17(settingsDir(base), "settings.json");
34865
+ if (existsSync8(settingsFile)) tightenFile(settingsFile);
33992
34866
  } catch {
33993
34867
  }
33994
34868
  migrateLegacyLayout(base);
@@ -34011,9 +34885,9 @@ function resolveProviderSafe(resolveProviderFn) {
34011
34885
  }
34012
34886
 
34013
34887
  // ../../packages/plugin-sdk/src/config-inventory.ts
34014
- import { readdirSync as readdirSync2, readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
34888
+ import { readdirSync as readdirSync2, readFileSync as readFileSync12, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
34015
34889
  import { homedir as homedir2 } from "os";
34016
- import { basename as basename3, join as join16 } from "path";
34890
+ import { basename as basename3, join as join19 } from "path";
34017
34891
 
34018
34892
  // ../../packages/detections/src/egress/registry.ts
34019
34893
  var EXTRACTOR_VERSION = "1";
@@ -37105,8 +37979,8 @@ function uniqueRuleIds(findings) {
37105
37979
  }
37106
37980
 
37107
37981
  // ../../packages/plugin-sdk/src/repo.ts
37108
- import { existsSync as existsSync8, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
37109
- import { basename as basename2, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
37982
+ import { existsSync as existsSync9, readFileSync as readFileSync11, statSync as statSync6 } from "fs";
37983
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join18, sep as sep2 } from "path";
37110
37984
  function resolveRepo(cwd) {
37111
37985
  try {
37112
37986
  const root = findGitRoot(cwd);
@@ -37121,36 +37995,36 @@ function resolveRepo(cwd) {
37121
37995
  function findGitRoot(start) {
37122
37996
  let dir = start;
37123
37997
  for (; ; ) {
37124
- if (existsSync8(join15(dir, ".git"))) return dir;
37998
+ if (existsSync9(join18(dir, ".git"))) return dir;
37125
37999
  const parent = dirname4(dir);
37126
38000
  if (parent === dir) return void 0;
37127
38001
  dir = parent;
37128
38002
  }
37129
38003
  }
37130
38004
  function resolveGitContext(root) {
37131
- const dotGit = join15(root, ".git");
38005
+ const dotGit = join18(root, ".git");
37132
38006
  try {
37133
38007
  if (statSync6(dotGit).isDirectory()) {
37134
- return { configPath: join15(dotGit, "config"), headRoot: root };
38008
+ return { configPath: join18(dotGit, "config"), headRoot: root };
37135
38009
  }
37136
38010
  } catch {
37137
38011
  return void 0;
37138
38012
  }
37139
38013
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
37140
38014
  if (!target) return void 0;
37141
- const gitdir = isAbsolute(target) ? target : join15(root, target);
37142
- if (existsSync8(join15(gitdir, "config"))) {
37143
- return { configPath: join15(gitdir, "config"), headRoot: root };
38015
+ const gitdir = isAbsolute(target) ? target : join18(root, target);
38016
+ if (existsSync9(join18(gitdir, "config"))) {
38017
+ return { configPath: join18(gitdir, "config"), headRoot: root };
37144
38018
  }
37145
- const commonRaw = safeRead(join15(gitdir, "commondir"))?.trim();
38019
+ const commonRaw = safeRead(join18(gitdir, "commondir"))?.trim();
37146
38020
  if (!commonRaw) return void 0;
37147
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join15(gitdir, commonRaw);
38021
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join18(gitdir, commonRaw);
37148
38022
  const headRoot = basename2(commonGitDir) === ".git" ? dirname4(commonGitDir) : root;
37149
- return { configPath: join15(commonGitDir, "config"), headRoot };
38023
+ return { configPath: join18(commonGitDir, "config"), headRoot };
37150
38024
  }
37151
38025
  function safeRead(path) {
37152
38026
  try {
37153
- return readFileSync9(path, "utf8");
38027
+ return readFileSync11(path, "utf8");
37154
38028
  } catch {
37155
38029
  return void 0;
37156
38030
  }
@@ -37216,7 +38090,7 @@ function buildIngestEvent(input2) {
37216
38090
  }
37217
38091
 
37218
38092
  // ../../packages/plugin-sdk/src/isolated-scan.ts
37219
- import { existsSync as existsSync9 } from "fs";
38093
+ import { existsSync as existsSync10 } from "fs";
37220
38094
  import { fileURLToPath } from "url";
37221
38095
  import { Worker } from "worker_threads";
37222
38096
  var ISOLATED_SCAN_BUDGET_MS = 2e3;
@@ -37230,7 +38104,7 @@ function resolveWorkerUrl() {
37230
38104
  for (const name of ["scan-worker.js", "scan-worker.ts"]) {
37231
38105
  const candidate = new URL(name, import.meta.url);
37232
38106
  try {
37233
- if (existsSync9(fileURLToPath(candidate))) {
38107
+ if (existsSync10(fileURLToPath(candidate))) {
37234
38108
  resolvedWorkerUrl = candidate;
37235
38109
  return candidate;
37236
38110
  }
@@ -37693,13 +38567,9 @@ function createGuardedScanner(partition, gateway, opts) {
37693
38567
  };
37694
38568
  }
37695
38569
 
37696
- // ../../packages/plugin-sdk/src/ignore-layers.ts
37697
- var import_ignore = __toESM(require_ignore(), 1);
37698
- import { readFileSync as readFileSync11 } from "fs";
37699
- import { join as join17 } from "path";
37700
-
37701
- // ../../packages/plugin-sdk/src/inventory-resolver.ts
37702
- import { arch, hostname as hostname4, platform, release } from "os";
38570
+ // ../../packages/plugin-sdk/src/host-floor.ts
38571
+ import { readFileSync as readFileSync14 } from "fs";
38572
+ import { join as join21 } from "path";
37703
38573
 
37704
38574
  // ../../packages/plugin-sdk/src/model-governance.ts
37705
38575
  import {
@@ -37707,16 +38577,138 @@ import {
37707
38577
  fstatSync,
37708
38578
  mkdirSync as mkdirSync2,
37709
38579
  openSync as openSync2,
37710
- readFileSync as readFileSync12,
38580
+ readFileSync as readFileSync13,
37711
38581
  readSync,
37712
38582
  writeFileSync as writeFileSync5
37713
38583
  } from "fs";
37714
- import { join as join18 } from "path";
38584
+ import { join as join20 } from "path";
37715
38585
  var TAIL_BYTES = 256 * 1024;
38586
+ function readTail(path) {
38587
+ const fd = openSync2(path, "r");
38588
+ try {
38589
+ const { size } = fstatSync(fd);
38590
+ if (size <= TAIL_BYTES) return { text: readFileSync13(fd, "utf8"), truncated: false };
38591
+ const buffer = Buffer.allocUnsafe(TAIL_BYTES);
38592
+ let filled = 0;
38593
+ while (filled < TAIL_BYTES) {
38594
+ const n = readSync(fd, buffer, filled, TAIL_BYTES - filled, size - TAIL_BYTES + filled);
38595
+ if (n === 0) break;
38596
+ filled += n;
38597
+ }
38598
+ return { text: buffer.subarray(0, filled).toString("utf8"), truncated: true };
38599
+ } finally {
38600
+ closeSync2(fd);
38601
+ }
38602
+ }
38603
+ function modelFromTranscriptTail(transcriptPath, fromRecord) {
38604
+ if (transcriptPath === void 0 || transcriptPath === "") return void 0;
38605
+ try {
38606
+ const { text: slice, truncated } = readTail(transcriptPath);
38607
+ const lines = slice.split("\n");
38608
+ if (truncated) lines.shift();
38609
+ for (let i = lines.length - 1; i >= 0; i--) {
38610
+ const line = lines[i];
38611
+ if (line === void 0 || line === "") continue;
38612
+ let parsed2;
38613
+ try {
38614
+ parsed2 = JSON.parse(line);
38615
+ } catch {
38616
+ continue;
38617
+ }
38618
+ const model = fromRecord(parsed2);
38619
+ if (model !== void 0 && model !== "") return model;
38620
+ }
38621
+ return void 0;
38622
+ } catch {
38623
+ return void 0;
38624
+ }
38625
+ }
38626
+
38627
+ // ../../packages/plugin-sdk/src/host-floor.ts
38628
+ var HOST_FEATURE = {
38629
+ ModelSwitch: "model-switch",
38630
+ VaultPointerDisplay: "vault-pointer-display"
38631
+ };
38632
+ var HOST_FLOORS = {
38633
+ [HOST_FEATURE.ModelSwitch]: {
38634
+ label: "model-switch protection",
38635
+ hookEvents: ["PreModelSwitch", "PostModelSwitch"],
38636
+ since: "2.1.251"
38637
+ },
38638
+ [HOST_FEATURE.VaultPointerDisplay]: {
38639
+ label: "vault pointer display",
38640
+ hookEvents: ["MessageDisplay"],
38641
+ since: "2.1.152"
38642
+ }
38643
+ };
38644
+ var hostVersionFromRecord = (record2) => {
38645
+ if (typeof record2 !== "object" || record2 === null) return void 0;
38646
+ const { version: version2 } = record2;
38647
+ return typeof version2 === "string" && version2 !== "" ? version2 : void 0;
38648
+ };
38649
+ function hostVersionFromTranscript(transcriptPath) {
38650
+ return modelFromTranscriptTail(transcriptPath, hostVersionFromRecord);
38651
+ }
38652
+ function hostFloorGaps(hostVersion) {
38653
+ if (hostVersion === void 0) return [];
38654
+ const gaps = [];
38655
+ for (const [feature, row] of Object.entries(HOST_FLOORS)) {
38656
+ if (compareBinaryVersions(hostVersion, row.since) < 0) {
38657
+ gaps.push({ feature, label: row.label, since: row.since });
38658
+ }
38659
+ }
38660
+ return gaps;
38661
+ }
38662
+ function requiredHostVersion(gaps) {
38663
+ let highest;
38664
+ for (const gap of gaps) {
38665
+ if (highest === void 0 || compareBinaryVersions(gap.since, highest) > 0) highest = gap.since;
38666
+ }
38667
+ return highest;
38668
+ }
38669
+ function hostFloorNotice(hostVersion) {
38670
+ if (hostVersion === void 0) return null;
38671
+ const gaps = hostFloorGaps(hostVersion);
38672
+ const required2 = requiredHostVersion(gaps);
38673
+ if (required2 === void 0) return null;
38674
+ return `This Claude Code (${hostVersion}) is older than AKA needs \u2014 some of AKA's protections are inactive in this session. Update Claude Code to ${required2} or newer to turn them on. Everything else AKA does is working normally. Run \`aka status\` for detail.`;
38675
+ }
38676
+ var HOST_VERSION_MARKER = "host-version.json";
38677
+ function recordHostVersion(dataDir2, version2) {
38678
+ if (version2 === void 0 || !isParseableBinaryVersion(version2)) return;
38679
+ try {
38680
+ const current = readHostVersionCache(dataDir2);
38681
+ if (current !== null && compareBinaryVersions(version2, current.version) <= 0) return;
38682
+ const cache = { version: version2, observedAt: Date.now() };
38683
+ ensureDataDirSync(dataDir2);
38684
+ writeOwnerOnlyFileSync(join21(dataDir2, HOST_VERSION_MARKER), JSON.stringify(cache));
38685
+ } catch {
38686
+ }
38687
+ }
38688
+ function readHostVersionCache(dataDir2) {
38689
+ try {
38690
+ const parsed2 = JSON.parse(readFileSync14(join21(dataDir2, HOST_VERSION_MARKER), "utf8"));
38691
+ if (typeof parsed2 !== "object" || parsed2 === null) return null;
38692
+ const { version: version2, observedAt } = parsed2;
38693
+ if (typeof version2 !== "string" || !isParseableBinaryVersion(version2)) return null;
38694
+ if (typeof observedAt !== "number" || !Number.isFinite(observedAt)) return null;
38695
+ return { version: version2, observedAt };
38696
+ } catch {
38697
+ return null;
38698
+ }
38699
+ }
38700
+
38701
+ // ../../packages/plugin-sdk/src/ignore-layers.ts
38702
+ var import_ignore = __toESM(require_ignore(), 1);
38703
+ import { readFileSync as readFileSync15 } from "fs";
38704
+ import { join as join22 } from "path";
38705
+
38706
+ // ../../packages/plugin-sdk/src/inventory-resolver.ts
38707
+ import { arch, hostname as hostname4, platform, release } from "os";
37716
38708
 
37717
38709
  // ../../packages/plugin-sdk/src/nudge.ts
37718
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
37719
- import { join as join19 } from "path";
38710
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync16, writeFileSync as writeFileSync6 } from "fs";
38711
+ import { join as join23 } from "path";
37720
38712
 
37721
38713
  // ../../packages/plugin-sdk/src/paths.ts
37722
38714
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
@@ -37758,8 +38750,8 @@ function createPolicyResolver(bundle) {
37758
38750
  }
37759
38751
 
37760
38752
  // ../../packages/plugin-sdk/src/project-files.ts
37761
- import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
37762
- import { basename as basename5, join as join20 } from "path";
38753
+ import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
38754
+ import { basename as basename5, join as join24 } from "path";
37763
38755
 
37764
38756
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
37765
38757
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -37790,6 +38782,14 @@ var CodexProviderEnvSchema = external_exports.object(codexProviderEnvShape);
37790
38782
  // ../../packages/plugin-sdk/src/runtime.ts
37791
38783
  import { randomUUID as randomUUID14 } from "crypto";
37792
38784
  var ENFORCEMENT_CEILING_ENABLED = false;
38785
+ function applyEnforcementCeiling(action, policyMode, enabled) {
38786
+ if (!enabled || policyMode !== "warn") return action;
38787
+ return action === "block" || action === "redact" ? "warn" : action;
38788
+ }
38789
+ function resolveEnforcedAction(action, opts) {
38790
+ const degraded = !opts.rewritable && action === "redact" ? builtinPolicyToAction(opts.redactFallback) : action;
38791
+ return applyEnforcementCeiling(degraded, opts.policyMode, opts.ceilingEnabled);
38792
+ }
37793
38793
  function startTiming() {
37794
38794
  try {
37795
38795
  return performance.now();
@@ -37826,7 +38826,7 @@ function createPluginRuntime(gateway, settings, opts) {
37826
38826
  bundlesPacked = true;
37827
38827
  }
37828
38828
  const policyMode = settings.policy;
37829
- const redactFallback = settings.redactFallback;
38829
+ let redactFallback = settings.redactFallback;
37830
38830
  const dataDir2 = opts?.dataDir;
37831
38831
  let rules = [];
37832
38832
  let scanner;
@@ -37870,6 +38870,7 @@ function createPluginRuntime(gateway, settings, opts) {
37870
38870
  rules = [...verified, ...unverified];
37871
38871
  scanner = createGuardedScanner({ verified, unverified }, gateway, opts?.scanIsolation);
37872
38872
  bundleExceptions = bundle.exceptions ?? [];
38873
+ redactFallback = strongerRedactFallback(settings.redactFallback, bundle.redactFallback);
37873
38874
  initialized = true;
37874
38875
  }
37875
38876
  let cachedKey;
@@ -37898,21 +38899,23 @@ function createPluginRuntime(gateway, settings, opts) {
37898
38899
  }
37899
38900
  function actionForFinding(finding, excepted, rewritable = true) {
37900
38901
  if (excepted?.has(finding)) return "allow";
37901
- const action = resolveAction(finding.ruleId, finding.category);
37902
- if (!rewritable && action === "redact") return builtinPolicyToAction(redactFallback);
37903
- if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn" && (action === "block" || action === "redact")) {
37904
- return "warn";
37905
- }
37906
- return action;
38902
+ return resolveEnforcedAction(resolveAction(finding.ruleId, finding.category), {
38903
+ policyMode,
38904
+ redactFallback,
38905
+ rewritable,
38906
+ ceilingEnabled: ENFORCEMENT_CEILING_ENABLED
38907
+ });
37907
38908
  }
37908
38909
  function decide(findings, text, excepted, rewritable = true) {
37909
38910
  if (findings.length === 0) return { action: "log", text, findings: [] };
37910
38911
  const actionFor = (finding) => actionForFinding(finding, excepted, rewritable);
38912
+ const degradedActions = rewritable ? [] : findings.filter((f) => actionForFinding(f, excepted, true) === "redact").map(actionFor);
38913
+ const degraded = degradedActions.length === 0 ? {} : { redactDegradedTo: degradedActions.reduce((a, b) => strongerAction(a, b)) };
37911
38914
  let worst = "log";
37912
38915
  for (const finding of findings) {
37913
38916
  worst = strongerAction(worst, actionFor(finding));
37914
38917
  }
37915
- if (worst === "block") return { action: "block", text: null, findings };
38918
+ if (worst === "block") return { action: "block", text: null, findings, ...degraded };
37916
38919
  if (worst === "redact") {
37917
38920
  const redactFindings = findings.filter((f) => actionFor(f) === "redact");
37918
38921
  const reversibleFindings = redactFindings.filter((f) => resolver.isReversible(f.ruleId));
@@ -37922,9 +38925,13 @@ function createPluginRuntime(gateway, settings, opts) {
37922
38925
  findings,
37923
38926
  enforcedFindings: redactFindings,
37924
38927
  reversibleFindings
38928
+ // No `degraded` here, and it is not an omission: `rewritable` is per
38929
+ // CAPTURE, so on an unrewritable field every redact has already become
38930
+ // the fallback and this branch is unreachable. Spreading it would read
38931
+ // as a case that can happen.
37925
38932
  };
37926
38933
  }
37927
- return { action: worst, text, findings };
38934
+ return { action: worst, text, findings, ...degraded };
37928
38935
  }
37929
38936
  function fingerprintOf(key, finding, cache) {
37930
38937
  let fp = cache.get(finding);
@@ -38053,8 +39060,8 @@ function createPluginRuntime(gateway, settings, opts) {
38053
39060
  };
38054
39061
  }
38055
39062
  }
38056
- async function processText(text, context) {
38057
- return (await evaluate(text, context, {})).decision;
39063
+ async function processText(text, context, opts2 = {}) {
39064
+ return (await evaluate(text, context, {}, opts2.rewritable)).decision;
38058
39065
  }
38059
39066
  async function capture(input2, opts2 = {}) {
38060
39067
  const timingStartedAt = input2.occurredAt === void 0 ? startTiming() : void 0;
@@ -38077,10 +39084,12 @@ function createPluginRuntime(gateway, settings, opts) {
38077
39084
  );
38078
39085
  const storedContent = maskedFindings.length > 0 ? redact(input2.text, maskedFindings) : input2.text;
38079
39086
  const inspectionMs = elapsedMs(timingStartedAt);
38080
- const metadata = exceptionIds.length > 0 || inspectionMs !== void 0 ? {
39087
+ const redactDegradedTo = decision.redactDegradedTo;
39088
+ const metadata = exceptionIds.length > 0 || inspectionMs !== void 0 || redactDegradedTo !== void 0 ? {
38081
39089
  ...input2.metadata,
38082
39090
  ...exceptionIds.length > 0 ? { exceptionIds } : {},
38083
- ...inspectionMs !== void 0 ? { inspectionMs } : {}
39091
+ ...inspectionMs !== void 0 ? { inspectionMs } : {},
39092
+ ...redactDegradedTo !== void 0 ? { redactDegradedTo } : {}
38084
39093
  } : input2.metadata;
38085
39094
  const event = buildIngestEvent({
38086
39095
  kind: input2.kind,
@@ -38152,7 +39161,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
38152
39161
 
38153
39162
  // ../../packages/plugin-sdk/src/throttle.ts
38154
39163
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
38155
- import { join as join21 } from "path";
39164
+ import { join as join25 } from "path";
38156
39165
 
38157
39166
  // ../../packages/plugin-sdk/src/tokenize.ts
38158
39167
  function redactedPlaceholder(category) {
@@ -38481,31 +39490,12 @@ function isServerRejection(err) {
38481
39490
  var FORWARD_BUDGET_MS = 1500;
38482
39491
  var DECISION_PATH_BUDGET_MS = 800;
38483
39492
  var BREAKER_FAILURE_THRESHOLD = 3;
38484
- var BREAKER_COOLDOWN_MS = 3e4;
38485
39493
  var CLOSED = { consecutiveFailures: 0, openedAtMs: null, lastFailure: null };
38486
- var FAILURES = /* @__PURE__ */ new Set([
38487
- "unauthorized",
38488
- "forbidden",
38489
- "unreachable"
38490
- ]);
38491
39494
  var FORWARD_STATE_FILENAME = ATTACHED_FORWARD_STATE_FILENAME;
38492
39495
  var STATE_FILENAME = FORWARD_STATE_FILENAME;
38493
- function parseBreakerState(raw, nowMs) {
38494
- try {
38495
- const parsed2 = JSON.parse(raw);
38496
- if (typeof parsed2 !== "object" || parsed2 === null) return null;
38497
- const record2 = parsed2;
38498
- const failures = typeof record2.consecutiveFailures === "number" && record2.consecutiveFailures >= 0 ? record2.consecutiveFailures : 0;
38499
- const openedAtMs = typeof record2.openedAtMs === "number" && Number.isFinite(record2.openedAtMs) && record2.openedAtMs <= nowMs ? record2.openedAtMs : null;
38500
- const lastFailure = typeof record2.lastFailure === "string" && FAILURES.has(record2.lastFailure) ? record2.lastFailure : null;
38501
- return { consecutiveFailures: failures, openedAtMs, lastFailure };
38502
- } catch {
38503
- return null;
38504
- }
38505
- }
38506
39496
  function createForwardPolicy(deps) {
38507
39497
  const now = deps.now ?? (() => Date.now());
38508
- const file2 = join22(deps.dir, STATE_FILENAME);
39498
+ const file2 = join26(deps.dir, STATE_FILENAME);
38509
39499
  let state = null;
38510
39500
  let loading = null;
38511
39501
  async function readState() {
@@ -38515,7 +39505,7 @@ function createForwardPolicy(deps) {
38515
39505
  } catch {
38516
39506
  return { ...CLOSED };
38517
39507
  }
38518
- return parseBreakerState(raw, now()) ?? { ...CLOSED };
39508
+ return parseForwardHealth(raw, now()) ?? { ...CLOSED };
38519
39509
  }
38520
39510
  async function load() {
38521
39511
  if (state !== null) return state;
@@ -38561,7 +39551,7 @@ function createForwardPolicy(deps) {
38561
39551
  };
38562
39552
  const at = now();
38563
39553
  if (current.openedAtMs !== null) {
38564
- if (at - current.openedAtMs < BREAKER_COOLDOWN_MS) {
39554
+ if (isForwardPaused(current, at)) {
38565
39555
  return { ok: false, reason: "breaker-open" };
38566
39556
  }
38567
39557
  await persist({
@@ -39098,7 +40088,18 @@ var AttachedDataGateway = class {
39098
40088
  // and the spread above would otherwise drop the field silently — which is
39099
40089
  // exactly what it did, leaving the whole control inert on every device
39100
40090
  // while every test around it stayed green.
39101
- prohibitedModels: cached2.prohibitedModels
40091
+ prohibitedModels: cached2.prohibitedModels,
40092
+ // NAMED for the same reason as the line above, and it is the same defect
40093
+ // if it is not: `...local` above spreads the DEVICE's bundle, so a field
40094
+ // only the cache carries is dropped in silence. That is what left
40095
+ // `prohibitedModels` inert on every attached device with every test
40096
+ // around it green.
40097
+ //
40098
+ // Taken from the cache rather than merged here, because merging it needs
40099
+ // the device's own SETTING — which is not a bundle field and is not in
40100
+ // scope at this seam. The runtime does that merge, raise-only, where both
40101
+ // values are in hand (createPluginRuntime's ensureInitialized).
40102
+ redactFallback: cached2.redactFallback
39102
40103
  // ALSO HONOURED FROM THE CACHE, and not a bundle field at all: each
39103
40104
  // merged policy's own `provenance`. `mergeRaiseOnly` spreads the policies
39104
40105
  // it emits, so an 'authored' policy arriving from the control plane
@@ -39226,10 +40227,6 @@ function toolAuditEvent(input2) {
39226
40227
  };
39227
40228
  }
39228
40229
 
39229
- // ../../packages/plugin-runtime/src/attached/history-state.ts
39230
- import { readFileSync as readFileSync15 } from "fs";
39231
- import { join as join23 } from "path";
39232
-
39233
40230
  // ../../packages/plugin-runtime/src/attached/history-sync.ts
39234
40231
  import { createHash as createHash6 } from "crypto";
39235
40232
  import { hostname as hostname5 } from "os";
@@ -39238,6 +40235,10 @@ import { hostname as hostname5 } from "os";
39238
40235
  var CORRELATION_ID = EventMetadata.shape.correlationId;
39239
40236
  var TRACE_ID = EventMetadata.shape.traceId;
39240
40237
  var EXCEPTION_ID = EventMetadata.shape.exceptionIds.unwrap().element;
40238
+ var REDACT_DEGRADED_TO = EventMetadata.shape.redactDegradedTo.unwrap();
40239
+
40240
+ // ../../packages/plugin-runtime/src/attached/history-sync.ts
40241
+ var CAPTURE_BATCH_BYTES = 1024 * 1024;
39241
40242
 
39242
40243
  // ../../packages/plugin-runtime/src/attached/history-sync-trigger.ts
39243
40244
  import { spawn } from "child_process";
@@ -39245,7 +40246,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
39245
40246
  var HISTORY_SYNC_THROTTLE_MS = 5 * 60 * 1e3;
39246
40247
 
39247
40248
  // ../../packages/plugin-runtime/src/attached/plugin-block.ts
39248
- import { readFileSync as readFileSync16 } from "fs";
40249
+ import { readFileSync as readFileSync17 } from "fs";
39249
40250
  function createPluginBlock(build, policyStore) {
39250
40251
  return async () => {
39251
40252
  const cached2 = await policyStore.read();
@@ -39264,7 +40265,7 @@ function createPluginBlock(build, policyStore) {
39264
40265
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
39265
40266
  import { randomUUID as randomUUID16 } from "crypto";
39266
40267
  import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
39267
- import { join as join24 } from "path";
40268
+ import { join as join27 } from "path";
39268
40269
 
39269
40270
  // ../../packages/plugin-runtime/src/attached/atomic-publish.ts
39270
40271
  import { rename as rename2 } from "fs/promises";
@@ -39288,7 +40289,7 @@ async function publishByRename(tmp, file2, move = rename2) {
39288
40289
 
39289
40290
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
39290
40291
  function createPolicyStore(dir = dataDir()) {
39291
- const file2 = join24(dir, "policy-cache.json");
40292
+ const file2 = join27(dir, "policy-cache.json");
39292
40293
  async function read() {
39293
40294
  try {
39294
40295
  const raw = await readFile2(file2, "utf8");
@@ -39519,11 +40520,11 @@ function readStorePosture(dbPath2) {
39519
40520
  // ../../packages/plugin-runtime/src/attached/posture-store.ts
39520
40521
  import { randomUUID as randomUUID17 } from "crypto";
39521
40522
  import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
39522
- import { join as join25 } from "path";
40523
+ import { join as join28 } from "path";
39523
40524
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
39524
40525
  function createPostureStore(dir = settingsDir(), legacyDir) {
39525
- const file2 = join25(dir, "posture-state.json");
39526
- const legacyFile = legacyDir === void 0 ? null : join25(legacyDir, "posture-state.json");
40526
+ const file2 = join28(dir, "posture-state.json");
40527
+ const legacyFile = legacyDir === void 0 ? null : join28(legacyDir, "posture-state.json");
39527
40528
  async function persist(state) {
39528
40529
  await ensureDataDir(dir);
39529
40530
  const tmp = `${file2}.${randomUUID17()}.tmp`;
@@ -39591,8 +40592,8 @@ function createPostureStore(dir = settingsDir(), legacyDir) {
39591
40592
  }
39592
40593
 
39593
40594
  // ../../packages/plugin-runtime/src/attached/sync-state.ts
39594
- import { readFileSync as readFileSync17 } from "fs";
39595
- import { join as join26 } from "path";
40595
+ import { readFileSync as readFileSync18 } from "fs";
40596
+ import { join as join29 } from "path";
39596
40597
 
39597
40598
  // ../../packages/plugin-runtime/src/attached/status.ts
39598
40599
  var REFUSAL_LINES = {
@@ -39613,6 +40614,14 @@ import { spawn as spawn2 } from "child_process";
39613
40614
  import { fileURLToPath as fileURLToPath3 } from "url";
39614
40615
  var SYNC_THROTTLE_MS = 15 * 60 * 1e3;
39615
40616
 
40617
+ // ../../packages/plugin-runtime/src/content-retention-pass.ts
40618
+ var MAX_ROWS_PER_SWEEP = 50 * 1e3;
40619
+
40620
+ // ../../packages/plugin-runtime/src/content-retention-trigger.ts
40621
+ import { spawn as spawn3 } from "child_process";
40622
+ import { fileURLToPath as fileURLToPath4 } from "url";
40623
+ var CONTENT_RETENTION_THROTTLE_MS = 60 * 60 * 1e3;
40624
+
39616
40625
  // ../../packages/plugin-runtime/src/attached/factory.ts
39617
40626
  import { hostname as hostname6 } from "os";
39618
40627
 
@@ -40064,17 +41073,17 @@ var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
40064
41073
 
40065
41074
  // src/protocol/marker.ts
40066
41075
  import { randomBytes as randomBytes4 } from "crypto";
40067
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync18, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "fs";
40068
- import { join as join27 } from "path";
41076
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync19, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "fs";
41077
+ import { join as join30 } from "path";
40069
41078
  var MARKER_FILE = "protocol-marker";
40070
41079
  function mintMarker() {
40071
41080
  return randomBytes4(8).toString("hex");
40072
41081
  }
40073
41082
  function sessionProtocolMarker(dataDir2, sessionId) {
40074
41083
  if (!sessionId) return mintMarker();
40075
- const path = join27(dataDir2, MARKER_FILE);
41084
+ const path = join30(dataDir2, MARKER_FILE);
40076
41085
  try {
40077
- const stored = JSON.parse(readFileSync18(path, "utf8"));
41086
+ const stored = JSON.parse(readFileSync19(path, "utf8"));
40078
41087
  if (stored.sessionId === sessionId && typeof stored.marker === "string" && /^[0-9a-f]{16}$/.test(stored.marker)) {
40079
41088
  return stored.marker;
40080
41089
  }
@@ -40083,7 +41092,7 @@ function sessionProtocolMarker(dataDir2, sessionId) {
40083
41092
  const marker = mintMarker();
40084
41093
  try {
40085
41094
  mkdirSync5(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
40086
- const tmp = join27(dataDir2, `${MARKER_FILE}.tmp`);
41095
+ const tmp = join30(dataDir2, `${MARKER_FILE}.tmp`);
40087
41096
  writeFileSync8(tmp, JSON.stringify({ sessionId, marker }), { mode: DATA_FILE_MODE });
40088
41097
  renameSync5(tmp, path);
40089
41098
  } catch {
@@ -40136,6 +41145,68 @@ function userDisclosure(opts) {
40136
41145
  return sentences.join(" ");
40137
41146
  }
40138
41147
 
41148
+ // src/hooks/host-floor-notice.ts
41149
+ import { mkdirSync as mkdirSync6, readdirSync as readdirSync5, rmSync as rmSync7, statSync as statSync10, writeFileSync as writeFileSync9 } from "fs";
41150
+ import { join as join31 } from "path";
41151
+ var CLAIM_DIR = "host-floor-claims";
41152
+ var CLAIM_TTL_MS = 12 * 60 * 60 * 1e3;
41153
+ var CLAIM_SWEEP_MS = 7 * 24 * 60 * 60 * 1e3;
41154
+ function sweep(dir) {
41155
+ try {
41156
+ for (const name of readdirSync5(dir)) {
41157
+ const path = join31(dir, name);
41158
+ try {
41159
+ if (Date.now() - statSync10(path).mtimeMs > CLAIM_SWEEP_MS) rmSync7(path, { force: true });
41160
+ } catch {
41161
+ }
41162
+ }
41163
+ } catch {
41164
+ }
41165
+ }
41166
+ function tryClaim(dataDir2, sessionId) {
41167
+ if (sessionId === void 0 || sessionId === "") return () => void 0;
41168
+ const dir = join31(dataDir2, CLAIM_DIR);
41169
+ const path = join31(dir, encodeURIComponent(sessionId));
41170
+ try {
41171
+ if (Date.now() - statSync10(path).mtimeMs < CLAIM_TTL_MS) return null;
41172
+ rmSync7(path, { force: true });
41173
+ } catch {
41174
+ }
41175
+ try {
41176
+ mkdirSync6(dir, { recursive: true, mode: DATA_DIR_MODE });
41177
+ writeFileSync9(path, String(Date.now()), { flag: "wx", mode: DATA_FILE_MODE });
41178
+ } catch (err) {
41179
+ if (err.code === "EEXIST") return null;
41180
+ return () => void 0;
41181
+ }
41182
+ sweep(dir);
41183
+ return () => {
41184
+ try {
41185
+ rmSync7(path, { force: true });
41186
+ } catch {
41187
+ }
41188
+ };
41189
+ }
41190
+ function warnIfHostBelowFloor(config2, sessionId, transcriptPath, write = (message) => void process.stderr.write(message)) {
41191
+ try {
41192
+ const release2 = tryClaim(config2.dataDir, sessionId);
41193
+ if (release2 === null) return;
41194
+ let settled = false;
41195
+ try {
41196
+ const version2 = hostVersionFromTranscript(transcriptPath);
41197
+ if (version2 === void 0) return;
41198
+ recordHostVersion(config2.dataDir, version2);
41199
+ const notice = hostFloorNotice(version2);
41200
+ if (notice !== null) write(`[aka] ${notice}
41201
+ `);
41202
+ settled = true;
41203
+ } finally {
41204
+ if (!settled) release2();
41205
+ }
41206
+ } catch {
41207
+ }
41208
+ }
41209
+
40139
41210
  // src/present.ts
40140
41211
  var SHADE = {
40141
41212
  light: "\u2591",
@@ -40389,8 +41460,8 @@ function baseMetadata(input2) {
40389
41460
  }
40390
41461
 
40391
41462
  // src/hooks/store-health.ts
40392
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync19, writeFileSync as writeFileSync9 } from "fs";
40393
- import { dirname as dirname6, join as join28 } from "path";
41463
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync20, writeFileSync as writeFileSync10 } from "fs";
41464
+ import { dirname as dirname6, join as join32 } from "path";
40394
41465
  var STORE_REDIRECT_MARKER = "store-redirect-last-session";
40395
41466
  function markerDirs(dataDir2) {
40396
41467
  return [dataDir2, dirname6(dataDir2)];
@@ -40398,7 +41469,7 @@ function markerDirs(dataDir2) {
40398
41469
  function alreadyClaimed(dirs, marker, sessionId) {
40399
41470
  return dirs.some((dir) => {
40400
41471
  try {
40401
- return readFileSync19(join28(dir, marker), "utf8") === sessionId;
41472
+ return readFileSync20(join32(dir, marker), "utf8") === sessionId;
40402
41473
  } catch {
40403
41474
  return false;
40404
41475
  }
@@ -40407,8 +41478,8 @@ function alreadyClaimed(dirs, marker, sessionId) {
40407
41478
  function recordClaim(dirs, marker, sessionId) {
40408
41479
  for (const dir of dirs) {
40409
41480
  try {
40410
- mkdirSync6(dir, { recursive: true, mode: DATA_DIR_MODE });
40411
- writeFileSync9(join28(dir, marker), sessionId, { mode: DATA_FILE_MODE });
41481
+ mkdirSync7(dir, { recursive: true, mode: DATA_DIR_MODE });
41482
+ writeFileSync10(join32(dir, marker), sessionId, { mode: DATA_FILE_MODE });
40412
41483
  return;
40413
41484
  } catch {
40414
41485
  }
@@ -40467,6 +41538,7 @@ async function main() {
40467
41538
  if (filePath) metadata.filePath = filePath;
40468
41539
  const config2 = loadConfig();
40469
41540
  warnIfStoreRedirected(config2, getString(input2, "session_id"));
41541
+ warnIfHostBelowFloor(config2, getString(input2, "session_id"), getString(input2, "transcript_path"));
40470
41542
  const gateway = resolveDataGateway(config2);
40471
41543
  const runtime = createPluginRuntime(gateway, config2.settings, { dataDir: config2.dataDir });
40472
41544
  const vaultGlue = isVaultConsentValid(config2.settings.vaultConsent) ? createVaultGlue() : null;