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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -50,7 +50,7 @@ var require_ignore = __commonJS({
50
50
  var REGEX_SPLITALL_CRLF = /\r?\n/g;
51
51
  var REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/;
52
52
  var REGEX_TEST_TRAILING_SLASH = /\/$/;
53
- var SLASH = "/";
53
+ var SLASH2 = "/";
54
54
  var TMP_KEY_IGNORE = "node-ignore";
55
55
  if (typeof Symbol !== "undefined") {
56
56
  TMP_KEY_IGNORE = /* @__PURE__ */ Symbol.for("node-ignore");
@@ -422,11 +422,11 @@ var require_ignore = __commonJS({
422
422
  if (!REGEX_TEST_TRAILING_SLASH.test(path)) {
423
423
  return this.test(path);
424
424
  }
425
- const slices = path.split(SLASH).filter(Boolean);
425
+ const slices = path.split(SLASH2).filter(Boolean);
426
426
  slices.pop();
427
427
  if (slices.length) {
428
428
  const parent = this._t(
429
- slices.join(SLASH) + SLASH,
429
+ slices.join(SLASH2) + SLASH2,
430
430
  this._testCache,
431
431
  true,
432
432
  slices
@@ -442,14 +442,14 @@ var require_ignore = __commonJS({
442
442
  return cache[path];
443
443
  }
444
444
  if (!slices) {
445
- slices = path.split(SLASH).filter(Boolean);
445
+ slices = path.split(SLASH2).filter(Boolean);
446
446
  }
447
447
  slices.pop();
448
448
  if (!slices.length) {
449
449
  return cache[path] = this._rules.test(path, checkUnignored, MODE_IGNORE);
450
450
  }
451
451
  const parent = this._t(
452
- slices.join(SLASH) + SLASH,
452
+ slices.join(SLASH2) + SLASH2,
453
453
  cache,
454
454
  checkUnignored,
455
455
  slices
@@ -500,6 +500,14 @@ var POLICY_CACHE_FILENAME = "policy-cache.json";
500
500
  import { chmodSync as chmodSync2, lstatSync as lstatSync2, readFileSync, rmSync as rmSync3, statSync } from "fs";
501
501
  import { join as join2 } from "path";
502
502
 
503
+ // ../../packages/schema/src/drizzle/deferred-migrations.ts
504
+ var DEFERRED_MIGRATION_TAGS = [
505
+ "0031_audit_capture_by_time_index",
506
+ "0032_audit_capture_by_id_index",
507
+ "0033_audit_capture_location_index",
508
+ "0034_findings_read_indexes"
509
+ ];
510
+
503
511
  // ../../packages/schema/src/drizzle/sqlite-ddl.ts
504
512
  var SQLITE_MIGRATIONS = [
505
513
  {
@@ -617,6 +625,30 @@ var SQLITE_MIGRATIONS = [
617
625
  {
618
626
  tag: "0028_activity_session_probe_indexes",
619
627
  sql: "CREATE INDEX `idx_audit_session_prompt` ON `audit_events` (`root_session_id`) WHERE event_type = 'prompt';--> statement-breakpoint\nCREATE INDEX `idx_audit_session_share` ON `audit_events` (`root_session_id`) WHERE event_type = 'share';--> statement-breakpoint\nCREATE INDEX `idx_audit_ended_at` ON `audit_events` (`ended_at`,`root_session_id`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\nCREATE INDEX `idx_audit_session_ended` ON `audit_events` (`root_session_id`,`ended_at`) WHERE ended_at IS NOT NULL;--> statement-breakpoint\n-- Expression index for the activity list's turns rollup: a live-captured\n-- session's turns are the DISTINCT `run_key` across its `llm_call` leaves, and\n-- carrying the extracted key in the index answers that count from the index\n-- alone instead of parsing every leaf's attribute bag. Written by hand, as\n-- 0013's `idx_audit_code_change_path` was: drizzle-kit cannot emit an\n-- expression containing a comma, so this index is not declared in sqlite.ts.\nCREATE INDEX `idx_audit_session_run_key` ON `audit_events` (`root_session_id`, json_extract(`attributes`, '$.run_key')) WHERE `event_type` = 'llm_call';\n"
628
+ },
629
+ {
630
+ tag: "0029_audit_capture_rollup_index",
631
+ sql: "CREATE INDEX `idx_audit_capture_rollup` ON `audit_events` (`event_type`,`started_at`,`repo`,`id`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
632
+ },
633
+ {
634
+ tag: "0030_audit_content_expiry",
635
+ sql: "ALTER TABLE `audit_events` ADD `content_expired_at` integer;--> statement-breakpoint\nCREATE INDEX `idx_audit_expirable_body` ON `audit_events` (`started_at`) WHERE content IS NOT NULL;"
636
+ },
637
+ {
638
+ tag: "0031_audit_capture_by_time_index",
639
+ sql: "CREATE INDEX `idx_audit_capture_by_time` ON `audit_events` (`started_at`,`id`,`event_type`,`root_session_id`,`source_tool`,`repo`,`file_path`,`tool_name`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
640
+ },
641
+ {
642
+ tag: "0032_audit_capture_by_id_index",
643
+ sql: "CREATE INDEX `idx_audit_capture_by_id` ON `audit_events` (`id`,`started_at`,`event_type`,`root_session_id`,`source_tool`,`repo`,`file_path`,`tool_name`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
644
+ },
645
+ {
646
+ tag: "0033_audit_capture_location_index",
647
+ sql: "CREATE INDEX `idx_audit_capture_location` ON `audit_events` (`repo`,`file_path`,`started_at`,`id`,`event_type`) WHERE event_type IN ('prompt','response','code_change','tool_use');"
648
+ },
649
+ {
650
+ tag: "0034_findings_read_indexes",
651
+ sql: "CREATE INDEX `idx_inspection_definitions_rule` ON `inspection_definitions` (`rule_id`,`severity`,`category`);--> statement-breakpoint\nCREATE INDEX `idx_inspection_findings_def` ON `inspection_findings` (`inspection_definition_id`,`audit_event_id`);--> statement-breakpoint\nCREATE INDEX `idx_inspection_findings_event_cover` ON `inspection_findings` (`audit_event_id`,`inspection_definition_id`,`action_taken`,`finding_key`,`id`);"
620
652
  }
621
653
  ];
622
654
 
@@ -20444,7 +20476,17 @@ var UNPRICEABLE_PROVIDERS = Object.freeze([
20444
20476
  "gateway",
20445
20477
  "unknown",
20446
20478
  "cli",
20447
- "api"
20479
+ "api",
20480
+ // The browser extension's native host records these as `llm_call.provider`
20481
+ // for a web-chat turn — the web tool id, deliberately never the vendor id
20482
+ // (`openai`/`anthropic`) the session root carries. Subscription traffic
20483
+ // burns rate-limit budget, not dollar credits, and listing them here is
20484
+ // what keeps that true structurally: a later maintainer who wants to price
20485
+ // web-chat traffic at API rates has to delete this entry first, and meet
20486
+ // the reason on the way, rather than quietly adding one to
20487
+ // PROVIDER_PLATFORM.
20488
+ "chatgpt",
20489
+ "claude-ai"
20448
20490
  ]);
20449
20491
  function platformForProvider(provider) {
20450
20492
  return PROVIDER_PLATFORM.get(provider.trim().toLowerCase()) ?? null;
@@ -20587,7 +20629,12 @@ var HARNESS = {
20587
20629
  ClaudeDesktop: "claudedesktop",
20588
20630
  ChatGpt: "chatgpt",
20589
20631
  ClaudeAi: "claudeai",
20590
- Api: "api"
20632
+ Api: "api",
20633
+ // Not a coding assistant a person drives — an in-process SDK embedded in an
20634
+ // application, so it has no IDE/CLI/desktop/web surface of its own. Carries
20635
+ // the same id as its SOURCE_TOOL counterpart, unlike every capture-side tool
20636
+ // whose wire spelling differs from its display spelling.
20637
+ AiTcSdk: "ai-tc-sdk"
20591
20638
  };
20592
20639
  var Harness = external_exports.enum(HARNESS).meta({ id: "Harness" });
20593
20640
  var SOURCE_TOOL = {
@@ -20603,9 +20650,15 @@ var SOURCE_TOOL = {
20603
20650
  // whose tool could not be identified both render through the read side's
20604
20651
  // miss path rather than as a harness of their own.
20605
20652
  Cli: "cli",
20606
- Unknown: "unknown"
20653
+ Unknown: "unknown",
20654
+ // The wire id an in-process, request-path SDK stamps on its own structural
20655
+ // rows (`request_decision`) — never a capture of prompt/response/tool text,
20656
+ // since the SDK sits in front of a model call rather than inside a coding
20657
+ // assistant's own hook contract.
20658
+ AiTcSdk: "ai-tc-sdk"
20607
20659
  };
20608
20660
  var SourceTool = external_exports.enum(SOURCE_TOOL).meta({ id: "SourceTool" });
20661
+ var WebSourceTool = SourceTool.extract(["ChatGpt", "ClaudeAi"]);
20609
20662
  var TOOL_TO_HARNESS = {
20610
20663
  [SOURCE_TOOL.ClaudeCode]: HARNESS.ClaudeCode,
20611
20664
  [SOURCE_TOOL.ClaudeDesktop]: HARNESS.ClaudeDesktop,
@@ -20614,7 +20667,12 @@ var TOOL_TO_HARNESS = {
20614
20667
  [SOURCE_TOOL.ChatGpt]: HARNESS.ChatGpt,
20615
20668
  [SOURCE_TOOL.Codex]: HARNESS.Codex,
20616
20669
  [SOURCE_TOOL.Antigravity]: HARNESS.Antigravity,
20617
- [SOURCE_TOOL.ClaudeAi]: HARNESS.ClaudeAi
20670
+ [SOURCE_TOOL.ClaudeAi]: HARNESS.ClaudeAi,
20671
+ // Wire and display id are the same string here, but the row still belongs:
20672
+ // both vocabularies carry the `AiTcSdk` member, and the join is exactly
20673
+ // their intersection — leaving a shared member out would read as an
20674
+ // uninstrumented tool on both surfaces, which this one is not.
20675
+ [SOURCE_TOOL.AiTcSdk]: HARNESS.AiTcSdk
20618
20676
  };
20619
20677
 
20620
20678
  // ../../packages/schema/src/zod/finding.ts
@@ -20648,7 +20706,8 @@ var FindingProvider = Harness.extract([
20648
20706
  "ClaudeAi",
20649
20707
  "Codex",
20650
20708
  "Antigravity",
20651
- "Api"
20709
+ "Api",
20710
+ "AiTcSdk"
20652
20711
  ]).meta({ id: "FindingProvider" });
20653
20712
  var FindingCategory = external_exports.enum([
20654
20713
  "secret",
@@ -20664,6 +20723,15 @@ var FindingCategory = external_exports.enum([
20664
20723
  ]).meta({ id: "FindingCategory" });
20665
20724
  var FindingOrigin = external_exports.enum(["in-flight", "at-rest"]).meta({ id: "FindingOrigin" });
20666
20725
  var FindingStatus = external_exports.enum(["open", "handled", "resolved", "dismissed"]).meta({ id: "FindingStatus" });
20726
+ var SyncFailureReason = external_exports.enum(["deployment_refused", "payload_invalid", "detached_undelivered"]).meta({ id: "SyncFailureReason" });
20727
+ var FindingDeliveryState = external_exports.enum(["sent", "queued", "not_sent", "never_offered", "local_scan"]).meta({ id: "FindingDeliveryState" });
20728
+ var FindingDelivery = external_exports.object({
20729
+ state: FindingDeliveryState,
20730
+ // The delivery time for `sent`; the failure time for `not_sent` when recorded.
20731
+ at: external_exports.iso.datetime().optional(),
20732
+ // Only on `not_sent`, and only when a known reason was recorded.
20733
+ reason: SyncFailureReason.optional()
20734
+ }).meta({ id: "FindingDelivery" });
20667
20735
  var ResolutionMethod = external_exports.enum([
20668
20736
  "enforced-in-flight",
20669
20737
  "fixed-at-source",
@@ -20720,7 +20788,10 @@ var FindingInstance = external_exports.object({
20720
20788
  // The session that event belongs to, when it has one — the seam a
20721
20789
  // per-instance "view session" link needs. Absent for events captured
20722
20790
  // outside a session.
20723
- sessionId: external_exports.string().optional()
20791
+ sessionId: external_exports.string().optional(),
20792
+ // The delivery state of the event above (see FindingDelivery). Optional so
20793
+ // readers that do not project it stay valid.
20794
+ delivery: FindingDelivery.optional()
20724
20795
  }).meta({ id: "FindingInstance" });
20725
20796
  var FindingGroup = external_exports.object({
20726
20797
  id: external_exports.string(),
@@ -20772,7 +20843,10 @@ var FindingFacets = external_exports.object({
20772
20843
  // Host tool (attributes.tool_name). Present only on the instance-level
20773
20844
  // reads, which can filter by it; the type-level read omits the dimension
20774
20845
  // because a group spans tools.
20775
- tool: external_exports.array(FindingFacetItem).optional()
20846
+ tool: external_exports.array(FindingFacetItem).optional(),
20847
+ // Delivery states (FindingDeliveryState). Present only on the
20848
+ // instance-level reads, like `tool`.
20849
+ deployment: external_exports.array(FindingFacetItem).optional()
20776
20850
  }).meta({ id: "FindingFacets" });
20777
20851
  var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
20778
20852
  id: "FindingTypeSummary"
@@ -20883,6 +20957,8 @@ var ListFindingInstancesQuery = external_exports.object({
20883
20957
  // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
20884
20958
  // where the free-text `q` can only match the rendered "via Bash" label.
20885
20959
  tool: external_exports.array(external_exports.string()).optional(),
20960
+ // The delivery state of each finding's event (see FindingDelivery).
20961
+ deployment: external_exports.array(FindingDeliveryState).optional(),
20886
20962
  // Exact repository / file-path matches, for the drill-down out of the
20887
20963
  // locations view. A row whose event carries no repo/file matches neither.
20888
20964
  repo: external_exports.string().optional(),
@@ -20903,6 +20979,10 @@ var ListFindingInstancesResponse = external_exports.object({
20903
20979
  items: external_exports.array(FindingInstanceDetail),
20904
20980
  nextCursor: external_exports.string().nullable()
20905
20981
  }).meta({ id: "ListFindingInstancesResponse" });
20982
+ var ListFindingInstancesPage = external_exports.object({
20983
+ items: external_exports.array(FindingInstanceDetail),
20984
+ nextCursor: external_exports.string().nullable()
20985
+ }).meta({ id: "ListFindingInstancesPage" });
20906
20986
  var FindingLocationSummary = external_exports.object({
20907
20987
  // Opaque, stable, minted from the pair by encodeLocationId. It exists
20908
20988
  // because a location's identity is two values and a URL param carries one:
@@ -20945,6 +21025,8 @@ var ListFindingLocationsQuery = external_exports.object({
20945
21025
  // instances that match, and folds its status from those.
20946
21026
  status: external_exports.array(FindingStatus).optional(),
20947
21027
  tool: external_exports.array(external_exports.string()).optional(),
21028
+ // The delivery state of each finding's event (see FindingDelivery).
21029
+ deployment: external_exports.array(FindingDeliveryState).optional(),
20948
21030
  q: external_exports.string().optional(),
20949
21031
  sessionId: external_exports.string().optional(),
20950
21032
  from: external_exports.iso.datetime().optional(),
@@ -21002,18 +21084,41 @@ var AuditEventType = external_exports.enum([
21002
21084
  // 'tool_call' is the reconciler's structural row for every call, while
21003
21085
  // 'tool_use' exists only where a hook enforced against the arguments.
21004
21086
  "tool_use",
21005
- // One row per model REFUSAL: a switch onto a prohibited model that was
21006
- // denied, or a turn refused because the session was already running on one.
21007
- // A structural row like the ones above rather than a capture — it carries
21008
- // the model that was refused and nothing the user typed, because what is
21009
- // worth recording about a governance decision is the decision, and prompt
21010
- // text is the thing this product exists to keep from travelling.
21087
+ // One row per model REFUSAL, across all four seams a prohibited model can be
21088
+ // stopped at: a switch onto it, a turn already running on it, a subagent
21089
+ // spawn asking for it, or a request-path refusal an embedded request-path
21090
+ // SDK makes in-process before the call leaves the application. Which seam
21091
+ // rides `attributes.refusal_seam`, never this member name. A structural row
21092
+ // like the ones above rather than a capture — it carries the model that was
21093
+ // refused and nothing the user typed, because what is worth recording about
21094
+ // a governance decision is the decision, and prompt text is the thing this
21095
+ // product exists to keep from travelling.
21011
21096
  "model_refusal",
21097
+ // One row per request-path DECISION: a policy check an embedded request-path
21098
+ // SDK performs in-process before a model call leaves the application, or
21099
+ // against that call's non-streamed response. A structural row like
21100
+ // 'model_refusal' rather than a capture — content-free in the same way:
21101
+ // which side, which seam, what action and which field are decided rides
21102
+ // `attributes`, never this member name, and the matched text itself never
21103
+ // travels.
21104
+ //
21105
+ // A prohibited-model refusal on the request path is deliberately NOT this
21106
+ // member: it stays 'model_refusal' with `refusal_seam: 'request'`, so it
21107
+ // shares one bucket with the plugin's switch/turn/spawn refusals rather
21108
+ // than splitting one governance concept across two event types. This
21109
+ // member carries every OTHER request-path decision.
21110
+ "request_decision",
21012
21111
  // One row per config-inventory scan, hung off the session root. It is the
21013
21112
  // fact the posture inspection findings reference (findings require an
21014
21113
  // audit_event_id), and its started_at is the "scanned Nm ago" the read
21015
21114
  // surface renders.
21016
- "config_scan"
21115
+ "config_scan",
21116
+ // One row per reported browser-extension capture status, hung off the web
21117
+ // session root. The durable home of what one tab's network interception
21118
+ // is doing — a write-through of the native host's in-memory tracker, so a
21119
+ // second process (aka extension status) and a restarted host both have
21120
+ // somewhere to read it back from.
21121
+ "capture_status"
21017
21122
  ]).meta({ id: "AuditEventType" });
21018
21123
  var AttributeBag = external_exports.record(external_exports.string(), external_exports.unknown());
21019
21124
  var HostAttributes = external_exports.object({
@@ -21147,6 +21252,10 @@ var CaptureAttributes = external_exports.object({
21147
21252
  // to 'allow' — the enforcement audit trail's link back to the grant that
21148
21253
  // authorized the bypass.
21149
21254
  exception_ids: external_exports.array(external_exports.guid()).optional(),
21255
+ // The persisted spellings of EventMetadata's messageId/conversationId — the
21256
+ // join back to the `llm_call` leaf for the same assistant turn.
21257
+ message_id: external_exports.string().optional(),
21258
+ conversation_id: external_exports.string().optional(),
21150
21259
  // Whole milliseconds this capture's inspection blocked its caller — the
21151
21260
  // plugin's own added latency (see EventMetadata.inspectionMs, whose value
21152
21261
  // this is). Promoted to the `inspection_ms` generated column so the facet is
@@ -21155,7 +21264,33 @@ var CaptureAttributes = external_exports.object({
21155
21264
  // inline json_extract and is not itself an optimization.
21156
21265
  // ABSENT on replayed captures (backfill / worktree scan) and on rows written
21157
21266
  // before the measurement shipped — never present as a placeholder 0.
21158
- inspection_ms: external_exports.number().int().nonnegative().optional()
21267
+ inspection_ms: external_exports.number().int().nonnegative().optional(),
21268
+ // What a `redact` this capture could not carry out became instead (see
21269
+ // EventMetadata.redactDegradedTo, whose value this is). Present only when a
21270
+ // degrade actually happened, so absence is the ordinary case rather than a
21271
+ // reader having to distinguish it from a zero.
21272
+ //
21273
+ // PER CAPTURE, while `inspection_findings.action_taken` is per finding —
21274
+ // so on a multi-finding row this does not say which finding degraded, and
21275
+ // its presence does not mean the fallback decided the capture's action. A
21276
+ // capture denied by another finding's own Block policy carries `block`
21277
+ // here too. The full statement is on EventMetadata.redactDegradedTo; it is
21278
+ // repeated rather than referenced because a store reader opens this file.
21279
+ redact_degraded_to: ActionTaken.optional()
21280
+ }).catchall(external_exports.unknown());
21281
+ var CaptureStatusAttributes = external_exports.object({
21282
+ source_tool: external_exports.string().optional(),
21283
+ patched: external_exports.boolean().optional(),
21284
+ live: external_exports.boolean().optional(),
21285
+ blind: external_exports.boolean().optional(),
21286
+ sends_seen_dom: external_exports.number().int().nonnegative().optional(),
21287
+ exchanges_seen_net: external_exports.number().int().nonnegative().optional(),
21288
+ parse_failures: external_exports.number().int().nonnegative().optional(),
21289
+ unparsed_bodies: external_exports.number().int().nonnegative().optional(),
21290
+ shape_misses: external_exports.array(external_exports.string()).optional(),
21291
+ conversation_endpoints: external_exports.number().int().nonnegative().optional(),
21292
+ closed: external_exports.boolean().optional(),
21293
+ enforcement: external_exports.string().optional()
21159
21294
  }).catchall(external_exports.unknown());
21160
21295
  var ToolCallInspection = external_exports.object({
21161
21296
  ruleId: external_exports.string().min(1),
@@ -21354,7 +21489,17 @@ var AuditEvent = external_exports.object({
21354
21489
  /** `share` to a first-party/internal destination. */
21355
21490
  internal: external_exports.boolean(),
21356
21491
  /** Event needs review (e.g. unverified egress). */
21357
- flagged: external_exports.boolean()
21492
+ flagged: external_exports.boolean(),
21493
+ /**
21494
+ * The body this event's `title` is drawn from was cleared by local body
21495
+ * expiry, so an EMPTY title here means "gone", not "never had one".
21496
+ *
21497
+ * A separate flag rather than a sentinel written into `title`: the title is
21498
+ * rendered text, and a store-layer module that invented display copy for it
21499
+ * would be choosing words the view is supposed to choose. Additive and
21500
+ * defaulted, so an older producer still validates.
21501
+ */
21502
+ bodyExpired: external_exports.boolean().default(false)
21358
21503
  }).meta({ id: "ActivityAuditEvent" });
21359
21504
  var ActivitySessionSummary = external_exports.object({
21360
21505
  id: external_exports.string(),
@@ -22159,6 +22304,11 @@ var RemoteFailureKind = external_exports.enum([
22159
22304
  "rejected",
22160
22305
  "unreachable"
22161
22306
  ]);
22307
+ var ControlPlaneFailure = RemoteFailureKind.extract([
22308
+ "unauthorized",
22309
+ "forbidden",
22310
+ "unreachable"
22311
+ ]);
22162
22312
  var AttachDeviceRequest = external_exports.object({
22163
22313
  // This machine's own continuity id, so re-attaching ROTATES the credential
22164
22314
  // on one machine record instead of producing a second one. Client-minted
@@ -22694,6 +22844,17 @@ var EventMetadata = external_exports.object({
22694
22844
  // to 'allow' — the enforcement audit trail's link back to the grant that
22695
22845
  // authorized the bypass. Absent on captures where no exception applied.
22696
22846
  exceptionIds: external_exports.array(external_exports.guid()).optional(),
22847
+ // The assistant message this capture belongs to, and the conversation it sits
22848
+ // in — set by the browser extension's network capture so a stored `response`
22849
+ // row can be joined to the `llm_call` leaf describing the same turn. Absent
22850
+ // on every other capture path, which has no such id.
22851
+ //
22852
+ // Non-empty for the reason WebExchange.messageId is: it is the join key, and
22853
+ // a blank one matches no `llm_call` leaf. That refusal reaches only the
22854
+ // places an event is PARSED; the local write path types the event and parses
22855
+ // nothing, which is why `toCaptureAttributes` omits a blank one separately.
22856
+ messageId: external_exports.string().min(1).optional(),
22857
+ conversationId: external_exports.string().optional(),
22697
22858
  // How long THIS capture's inspection blocked its caller, in whole
22698
22859
  // milliseconds — the plugin's own added latency, NOT the LLM call it sat in
22699
22860
  // front of. Measured inside `capture()` (@akasecurity/plugin-sdk) across
@@ -22706,7 +22867,37 @@ var EventMetadata = external_exports.object({
22706
22867
  // Absent is also what every pre-measurement client writes, and what a
22707
22868
  // clock failure degrades to — a reader must treat absence as "not measured"
22708
22869
  // and never as a zero, which would read as "inspection is free".
22709
- inspectionMs: external_exports.number().int().nonnegative().optional()
22870
+ inspectionMs: external_exports.number().int().nonnegative().optional(),
22871
+ // What a `redact` this capture COULD NOT CARRY OUT became instead — the
22872
+ // workspace's `redactFallback`, applied because the field could not be
22873
+ // masked in place (a shell command, a URL, or any argument on a host whose
22874
+ // hook contract offers no rewrite channel).
22875
+ //
22876
+ // It exists because the action alone cannot say why. A finding recorded as
22877
+ // `warn` reads identically whether its detection was ASSIGNED Warn or was
22878
+ // assigned Redact on a field that could not take one — and those are
22879
+ // different facts about the same row: the first is a policy the user chose,
22880
+ // the second is a masking the host could not perform. Absent means no
22881
+ // degrade happened, which is every ordinary capture.
22882
+ //
22883
+ // TWO LIMITS a reader of a stored row has to know, because the grain here
22884
+ // is the CAPTURE while `actionTaken` is per FINDING:
22885
+ //
22886
+ // - It does not say WHICH finding degraded. A capture carrying a degraded
22887
+ // `redact` alongside a finding ASSIGNED the same action stores both
22888
+ // identically and one reason for the pair; attributing it to both
22889
+ // describes the assigned one wrongly, and to neither loses the degrade.
22890
+ // - PRESENCE IS NOT CAUSATION. The value is the action the lost redact
22891
+ // became, not the reason the capture ended as it did — a capture denied
22892
+ // by some other finding's own Block policy still carries `block` here,
22893
+ // and clearing the workspace's fallback would not have let it through.
22894
+ // Gate on the value against what a fallback can produce; never read the
22895
+ // field's presence as "this was the fallback's doing".
22896
+ //
22897
+ // Both are pinned as behaviour in @akasecurity/plugin-sdk's runtime suite.
22898
+ // Closing either means moving the reason onto the finding row, which
22899
+ // already carries its own action.
22900
+ redactDegradedTo: ActionTaken.optional()
22710
22901
  }).meta({ id: "EventMetadata" });
22711
22902
  var Event = external_exports.object({
22712
22903
  id: external_exports.guid(),
@@ -22816,7 +23007,32 @@ var RotateKeyInput = external_exports.object({
22816
23007
  confirmation: external_exports.string()
22817
23008
  });
22818
23009
 
23010
+ // ../../packages/schema/src/zod/finding-delivery.ts
23011
+ var KNOWN_REASONS = SyncFailureReason.options;
23012
+ function knownReason(value) {
23013
+ return value !== null && KNOWN_REASONS.includes(value) ? value : void 0;
23014
+ }
23015
+ function deriveFindingDelivery(row) {
23016
+ if (row.kind === "code_change") return { state: "local_scan" };
23017
+ if (row.syncedAt !== null && row.syncedAt > 0) {
23018
+ return { state: "sent", at: epochMillisToIso(row.syncedAt) };
23019
+ }
23020
+ if (row.syncedAt !== null) {
23021
+ const reason = knownReason(row.syncFailure);
23022
+ return {
23023
+ state: "not_sent",
23024
+ ...row.syncFailedAt === null ? {} : { at: epochMillisToIso(row.syncFailedAt) },
23025
+ ...reason === void 0 ? {} : { reason }
23026
+ };
23027
+ }
23028
+ if (row.outboxOwed === 1 || row.syncClaimedAt !== null) return { state: "queued" };
23029
+ return { state: "never_offered" };
23030
+ }
23031
+
22819
23032
  // ../../packages/schema/src/zod/findings-group-build.ts
23033
+ function lookupOwn(map2, key) {
23034
+ return Object.hasOwn(map2, key) ? map2[key] : void 0;
23035
+ }
22820
23036
  function toApiAction(dbVal) {
22821
23037
  const map2 = {
22822
23038
  log: "monitored",
@@ -22825,7 +23041,7 @@ function toApiAction(dbVal) {
22825
23041
  warn: "warned",
22826
23042
  allow: "allowed"
22827
23043
  };
22828
- return map2[dbVal] ?? "allowed";
23044
+ return lookupOwn(map2, dbVal) ?? "allowed";
22829
23045
  }
22830
23046
  function toApiCategory(dbVal) {
22831
23047
  if (dbVal === "code_context") return "source_code";
@@ -22833,13 +23049,18 @@ function toApiCategory(dbVal) {
22833
23049
  return parsed.success ? parsed.data : "custom";
22834
23050
  }
22835
23051
  function toApiProvider(sourceTool) {
22836
- return TOOL_TO_HARNESS[sourceTool] ?? HARNESS.Api;
23052
+ return lookupOwn(TOOL_TO_HARNESS, sourceTool) ?? HARNESS.Api;
22837
23053
  }
22838
- var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
23054
+ var FINDING_STATUS_PRECEDENCE = [
23055
+ "open",
23056
+ "handled",
23057
+ "dismissed",
23058
+ "resolved"
23059
+ ];
22839
23060
  function foldGroupStatus(instanceStatuses) {
22840
23061
  const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
22841
23062
  if (statuses.size === 0) return void 0;
22842
- for (const candidate of STATUS_PRECEDENCE) {
23063
+ for (const candidate of FINDING_STATUS_PRECEDENCE) {
22843
23064
  if (statuses.has(candidate)) return candidate;
22844
23065
  }
22845
23066
  return void 0;
@@ -22946,11 +23167,16 @@ function applyFindingFilters(types, opts) {
22946
23167
  }
22947
23168
  return filtered;
22948
23169
  }
22949
- var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
22950
- var SEVERITY_RANK = SEVERITY_ORDER;
23170
+ function rankByOrder(members2) {
23171
+ return Object.fromEntries(members2.map((member, index) => [member, index]));
23172
+ }
23173
+ var SEVERITY_RANK = rankByOrder(Severity.options);
23174
+ function severityRank(severity) {
23175
+ return lookupOwn(SEVERITY_RANK, severity);
23176
+ }
22951
23177
  function compareFindingGroupOrder(a, b) {
22952
- const rankA = SEVERITY_RANK[a.severity] ?? -1;
22953
- const rankB = SEVERITY_RANK[b.severity] ?? -1;
23178
+ const rankA = severityRank(a.severity) ?? -1;
23179
+ const rankB = severityRank(b.severity) ?? -1;
22954
23180
  const severityDiff = rankA - rankB;
22955
23181
  if (severityDiff !== 0) return severityDiff;
22956
23182
  const recencyDiff = b.latestDetectedAt.localeCompare(a.latestDetectedAt);
@@ -23025,6 +23251,20 @@ function computeFindingFacets(allTypes, opts) {
23025
23251
  }
23026
23252
 
23027
23253
  // ../../packages/schema/src/zod/findings-flat-build.ts
23254
+ function compareCodePoints(a, b) {
23255
+ const aIter = a[Symbol.iterator]();
23256
+ const bIter = b[Symbol.iterator]();
23257
+ for (; ; ) {
23258
+ const aNext = aIter.next();
23259
+ const bNext = bIter.next();
23260
+ if (aNext.done && bNext.done) return 0;
23261
+ if (aNext.done) return -1;
23262
+ if (bNext.done) return 1;
23263
+ const aPoint = aNext.value.codePointAt(0) ?? 0;
23264
+ const bPoint = bNext.value.codePointAt(0) ?? 0;
23265
+ if (aPoint !== bPoint) return aPoint - bPoint;
23266
+ }
23267
+ }
23028
23268
  function rowHaystack(row) {
23029
23269
  return [
23030
23270
  row.ruleId,
@@ -23049,6 +23289,8 @@ function matchesDimension(row, opts, dimension) {
23049
23289
  return !opts.actions?.length || opts.actions.includes(toApiAction(row.actionTaken));
23050
23290
  case "statuses":
23051
23291
  return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
23292
+ case "deliveries":
23293
+ return !opts.deliveries?.length || row.delivery !== void 0 && opts.deliveries.includes(row.delivery.state);
23052
23294
  case "tools":
23053
23295
  return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
23054
23296
  // An EMPTY value is a real filter here, not an absent one. The location
@@ -23075,6 +23317,7 @@ var DIMENSIONS = [
23075
23317
  "providers",
23076
23318
  "actions",
23077
23319
  "statuses",
23320
+ "deliveries",
23078
23321
  "tools",
23079
23322
  "repo",
23080
23323
  "file",
@@ -23088,10 +23331,19 @@ function matchesInstanceFilters(row, opts, except) {
23088
23331
  return true;
23089
23332
  }
23090
23333
  function toItems(counts) {
23091
- return [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
23334
+ return [...counts.entries()].map(([value, count]) => ({ value, count })).sort(
23335
+ (a, b) => b.count - a.count || a.value.localeCompare(b.value) || // localeCompare reports canonically-equivalent strings (an NFC and an
23336
+ // NFD spelling of the same text) as equal, so a count tie between
23337
+ // them would otherwise be ordered by whichever the Map iteration
23338
+ // produced. compareCodePoints breaks that tie deterministically, which
23339
+ // makes this a TOTAL order — not one that agrees with SQL collation,
23340
+ // which it need not: foldFacetTuples runs this same sort over grouped
23341
+ // tuples, so both paths order facets identically by construction.
23342
+ compareCodePoints(a.value, b.value)
23343
+ );
23092
23344
  }
23093
- function bump(counts, value) {
23094
- counts.set(value, (counts.get(value) ?? 0) + 1);
23345
+ function bump(counts, value, by = 1) {
23346
+ counts.set(value, (counts.get(value) ?? 0) + by);
23095
23347
  }
23096
23348
  function createInstanceFacetAccumulator(opts) {
23097
23349
  const severity = /* @__PURE__ */ new Map();
@@ -23100,6 +23352,7 @@ function createInstanceFacetAccumulator(opts) {
23100
23352
  const action = /* @__PURE__ */ new Map();
23101
23353
  const status = /* @__PURE__ */ new Map();
23102
23354
  const tool = /* @__PURE__ */ new Map();
23355
+ const deployment = /* @__PURE__ */ new Map();
23103
23356
  return {
23104
23357
  add(row) {
23105
23358
  if (matchesInstanceFilters(row, opts, "severity")) bump(severity, row.severity);
@@ -23114,6 +23367,9 @@ function createInstanceFacetAccumulator(opts) {
23114
23367
  if (row.toolName !== void 0 && matchesInstanceFilters(row, opts, "tools")) {
23115
23368
  bump(tool, row.toolName);
23116
23369
  }
23370
+ if (row.delivery !== void 0 && matchesInstanceFilters(row, opts, "deliveries")) {
23371
+ bump(deployment, row.delivery.state);
23372
+ }
23117
23373
  },
23118
23374
  facets: () => ({
23119
23375
  severity: toItems(severity),
@@ -23121,7 +23377,8 @@ function createInstanceFacetAccumulator(opts) {
23121
23377
  provider: toItems(provider),
23122
23378
  action: toItems(action),
23123
23379
  status: toItems(status),
23124
- tool: toItems(tool)
23380
+ tool: toItems(tool),
23381
+ deployment: toItems(deployment)
23125
23382
  })
23126
23383
  };
23127
23384
  }
@@ -23135,6 +23392,7 @@ function toInstanceDetail(row) {
23135
23392
  ...row.toolName === void 0 ? {} : { toolName: row.toolName },
23136
23393
  eventId: row.eventId,
23137
23394
  ...row.sessionId === void 0 ? {} : { sessionId: row.sessionId },
23395
+ ...row.delivery === void 0 ? {} : { delivery: row.delivery },
23138
23396
  ...row.user === void 0 ? {} : { user: row.user },
23139
23397
  action: toApiAction(row.actionTaken),
23140
23398
  detectedAt: row.occurredAt,
@@ -23149,12 +23407,6 @@ function toInstanceDetail(row) {
23149
23407
  policy: { id: `category:${category}`, name: category }
23150
23408
  };
23151
23409
  }
23152
- var SEVERITY_ORDER2 = {
23153
- critical: 0,
23154
- high: 1,
23155
- medium: 2,
23156
- low: 3
23157
- };
23158
23410
  function newLocationAccumulator() {
23159
23411
  return {
23160
23412
  instanceCount: 0,
@@ -23169,7 +23421,7 @@ function newLocationAccumulator() {
23169
23421
  }
23170
23422
  function addToLocation(acc, row) {
23171
23423
  acc.instanceCount += 1;
23172
- const rank = SEVERITY_ORDER2[row.severity] ?? Number.MAX_SAFE_INTEGER - 1;
23424
+ const rank = severityRank(row.severity) ?? Number.MAX_SAFE_INTEGER - 1;
23173
23425
  if (rank < acc.maxSeverityRank) {
23174
23426
  acc.maxSeverityRank = rank;
23175
23427
  acc.maxSeverity = row.severity;
@@ -23179,15 +23431,15 @@ function addToLocation(acc, row) {
23179
23431
  acc.ruleIds.add(row.ruleId);
23180
23432
  }
23181
23433
  function compareLocationOrder(a, b) {
23182
- const rankA = SEVERITY_ORDER2[a.maxSeverity] ?? -1;
23183
- const rankB = SEVERITY_ORDER2[b.maxSeverity] ?? -1;
23434
+ const rankA = severityRank(a.maxSeverity) ?? -1;
23435
+ const rankB = severityRank(b.maxSeverity) ?? -1;
23184
23436
  if (rankA !== rankB) return rankA - rankB;
23185
23437
  if (a.latestDetectedAt !== b.latestDetectedAt) {
23186
23438
  return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
23187
23439
  }
23188
- if (a.repo !== b.repo) return a.repo < b.repo ? -1 : 1;
23189
- if (a.file !== b.file) return a.file < b.file ? -1 : 1;
23190
- return 0;
23440
+ const repoDiff = compareCodePoints(a.repo, b.repo);
23441
+ if (repoDiff !== 0) return repoDiff;
23442
+ return compareCodePoints(a.file, b.file);
23191
23443
  }
23192
23444
  function encodeLocationId(repo, file2) {
23193
23445
  return `${encodePart(repo)}/${encodePart(file2)}`;
@@ -23262,6 +23514,11 @@ var Policy = external_exports.object({
23262
23514
  // test `prohibitedModels` passes and `reversibleRuleIds` fails.
23263
23515
  provenance: PolicyProvenance.optional()
23264
23516
  }).meta({ id: "Policy" });
23517
+ var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23518
+ var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23519
+ var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23520
+ id: "RedactFallback"
23521
+ });
23265
23522
  var PolicyBundle = external_exports.object({
23266
23523
  version: external_exports.string(),
23267
23524
  policies: external_exports.array(Policy),
@@ -23309,6 +23566,16 @@ var PolicyBundle = external_exports.object({
23309
23566
  // control plane), so no name resolution stands between the decision and the
23310
23567
  // comparison.
23311
23568
  prohibitedModels: external_exports.array(external_exports.string()).optional(),
23569
+ // What a resolved `redact` becomes on a field the host cannot rewrite, as
23570
+ // the ORGANIZATION would have it. Merged raise-only against the device's own
23571
+ // `WorkspaceSettings.redactFallback` (see strongerRedactFallback below), so
23572
+ // a control plane can tighten a machine and never loosen one — the same
23573
+ // direction `mergeRaiseOnly` enforces for policies.
23574
+ //
23575
+ // Optional so an older backend, and an older on-disk cache, still parses;
23576
+ // absent leaves the device's own setting in force, which is the behaviour
23577
+ // that predates the field and the safe direction to default.
23578
+ redactFallback: RedactFallback.optional(),
23312
23579
  customKeywords: external_exports.array(external_exports.string()),
23313
23580
  fetchedAt: external_exports.iso.datetime()
23314
23581
  }).meta({ id: "PolicyBundle" });
@@ -23343,11 +23610,6 @@ function severityFloorPosture() {
23343
23610
  for (const c of DetectionCategory.options) out[c] = severityFloorPolicy(c);
23344
23611
  return out;
23345
23612
  }
23346
- var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "vault", "block"];
23347
- var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
23348
- var RedactFallback = BuiltinPolicyId.extract(["monitor", "warn", "block"]).meta({
23349
- id: "RedactFallback"
23350
- });
23351
23613
  var BUILTIN_ORDER = KNOWN_BUILTIN_IDS;
23352
23614
  var BUILTIN_POLICY_SPECS = {
23353
23615
  monitor: {
@@ -23643,7 +23905,7 @@ function isVaultConsentValid(consent) {
23643
23905
  }
23644
23906
 
23645
23907
  // ../../packages/schema/src/zod/local.ts
23646
- var WORKSPACE_SETTINGS_SPEC_VERSION = 7;
23908
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 8;
23647
23909
  var MODEL_JUDGE_PAYLOAD_VERSION = 1;
23648
23910
  var RunMode = external_exports.enum(["standalone", "attached"]);
23649
23911
  var ControlPlaneConnection = external_exports.object({
@@ -23666,6 +23928,27 @@ var HistorySyncConsent = external_exports.object({
23666
23928
  payloadVersion: external_exports.number().int().positive(),
23667
23929
  endpoint: external_exports.string()
23668
23930
  });
23931
+ var WebChatCaptureConsent = external_exports.object({
23932
+ acknowledgedAt: external_exports.iso.datetime(),
23933
+ version: external_exports.number().int().positive()
23934
+ });
23935
+ var WebChatResponseCapture = external_exports.enum(["with-findings", "always", "never"]);
23936
+ var WebChatCapture = external_exports.object({
23937
+ responses: WebChatResponseCapture.default("with-findings"),
23938
+ account: external_exports.boolean().default(false),
23939
+ // Absent until granted. Presence alone does not authorize anything — see
23940
+ // isWebChatCaptureConsentValid.
23941
+ consent: WebChatCaptureConsent.optional()
23942
+ });
23943
+ var BODY_RETENTION_DEFAULT_DAYS = 30;
23944
+ var BodyRetention = external_exports.object({
23945
+ enabled: external_exports.boolean().default(false),
23946
+ // Never 0, and the ceiling is a fat-finger guard rather than a policy
23947
+ // limit — `enabled` is the real gate. A low value cannot reach a row the
23948
+ // sync ledger still owes: the sweep's age filter only ever NARROWS a
23949
+ // candidate set that is already bounded by "delivered, or never owed".
23950
+ retainDays: external_exports.number().int().min(1).max(3650).default(BODY_RETENTION_DEFAULT_DAYS)
23951
+ }).meta({ id: "BodyRetention" });
23669
23952
  var WorkspaceSettings = external_exports.object({
23670
23953
  specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
23671
23954
  runMode: RunMode.default("standalone"),
@@ -23714,7 +23997,23 @@ var WorkspaceSettings = external_exports.object({
23714
23997
  // carry prompt/reply/tool-output text in `content`; the key name predates
23715
23998
  // both widenings. Absent until granted, and a grant for a different endpoint
23716
23999
  // or an older payload no longer counts.
23717
- historySyncConsent: HistorySyncConsent.optional()
24000
+ historySyncConsent: HistorySyncConsent.optional(),
24001
+ // What the browser extension may record from a web chat, and the grant that
24002
+ // authorizes it. Absent until the user answers: recording something that was
24003
+ // never recorded before is never an assumed grant on upgrade, so the whole
24004
+ // block is optional rather than defaulted in. What an absent block means is
24005
+ // webChatCaptureOf's answer, in one place.
24006
+ //
24007
+ // Enforcement is NOT gated on this. A machine that has never answered still
24008
+ // blocks, redacts and warns on what a user sends; the grant covers what is
24009
+ // written down.
24010
+ webChatCapture: WebChatCapture.optional(),
24011
+ // Local body expiry (see BodyRetention). Off until switched on; expiring a
24012
+ // body never removes the row or its findings.
24013
+ bodyRetention: BodyRetention.default({
24014
+ enabled: false,
24015
+ retainDays: BODY_RETENTION_DEFAULT_DAYS
24016
+ })
23718
24017
  });
23719
24018
  function defaultWorkspaceSettings() {
23720
24019
  return WorkspaceSettings.parse({});
@@ -23809,12 +24108,18 @@ function toCaptureAttributes(event) {
23809
24108
  ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
23810
24109
  ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
23811
24110
  ...metadata?.inspectionMs !== void 0 ? { inspection_ms: metadata.inspectionMs } : {},
24111
+ ...metadata?.redactDegradedTo !== void 0 ? { redact_degraded_to: metadata.redactDegradedTo } : {},
23812
24112
  // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
23813
24113
  // has ever populated either), but every legacy metadata key still rides
23814
24114
  // the bag rather than being silently dropped — CaptureAttributes'
23815
24115
  // `.catchall(z.unknown())` carries the long tail.
23816
24116
  ...metadata?.model !== void 0 ? { model: metadata.model } : {},
23817
- ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
24117
+ ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {},
24118
+ // A blank id is omitted rather than stored: it is a join key and `''` joins
24119
+ // nothing. This runs on the local write path, which types the event but
24120
+ // never parses it, so EventMetadata's own `.min(1)` does not reach here.
24121
+ ...metadata?.messageId !== void 0 && metadata.messageId !== "" ? { message_id: metadata.messageId } : {},
24122
+ ...metadata?.conversationId !== void 0 ? { conversation_id: metadata.conversationId } : {}
23818
24123
  };
23819
24124
  }
23820
24125
  function captureDefinitionVersion(finding) {
@@ -23842,13 +24147,22 @@ var ManagedSettingKey = external_exports.enum([
23842
24147
  "vaultInlineReveal",
23843
24148
  "modelJudgeConsent",
23844
24149
  "dataSharesInPlace",
23845
- "redactFallback"
24150
+ "redactFallback",
24151
+ // Pins the toggle and the day count together — see BodyRetention on why the
24152
+ // two are one unit. An administrator mandating a window wants the count
24153
+ // enforced with it, not one a user can widen while the toggle stays on.
24154
+ "bodyRetention"
23846
24155
  ]).meta({ id: "ManagedSettingKey" });
23847
24156
  function isManagedSettingKey(value) {
23848
24157
  return ManagedSettingKey.safeParse(value).success;
23849
24158
  }
23850
24159
  var ManagedSettingsValues = external_exports.object({
23851
24160
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
24161
+ // `controlPlane` and `bodyRetention` are the two nested values, and both are
24162
+ // plain, non-strict objects: a key under either that this build does not know
24163
+ // is stripped and nothing reports it. The unknown-value split in
24164
+ // ManagedSettings below classifies top-level names only, so it stops at
24165
+ // these boundaries.
23852
24166
  controlPlane: external_exports.object({
23853
24167
  endpoint: external_exports.string().min(1),
23854
24168
  label: external_exports.string().min(1).optional()
@@ -23859,7 +24173,8 @@ var ManagedSettingsValues = external_exports.object({
23859
24173
  vaultInlineReveal: external_exports.enum(["masked", "full", "off"]).optional(),
23860
24174
  modelJudgeConsent: external_exports.boolean().optional(),
23861
24175
  dataSharesInPlace: external_exports.boolean().optional(),
23862
- redactFallback: RedactFallback.optional()
24176
+ redactFallback: RedactFallback.optional(),
24177
+ bodyRetention: BodyRetention.optional()
23863
24178
  }).meta({ id: "ManagedSettingsValues" });
23864
24179
  var ManagedSettings = external_exports.object({
23865
24180
  specVersion: external_exports.number().int().positive().default(MANAGED_SETTINGS_SPEC_VERSION),
@@ -23867,7 +24182,21 @@ var ManagedSettings = external_exports.object({
23867
24182
  // decision from a bug. Absent renders as a generic "your organization".
23868
24183
  organization: external_exports.string().min(1).optional(),
23869
24184
  // What the administrator pinned.
23870
- values: ManagedSettingsValues.default({}),
24185
+ //
24186
+ // Parsed as a RECORD rather than as the nested schema, and split below for
24187
+ // the same reason `lockedFields` is parsed as names: a plain `z.object`
24188
+ // drops an unrecognised key and succeeds, so a pin this build does not know
24189
+ // vanished and nothing anywhere said so. A pin with no lock is a supported
24190
+ // shape — it is a DEFAULT the user may still change — so that silence hit
24191
+ // exactly the file an administrator is most likely to write while a fleet
24192
+ // is mid-upgrade.
24193
+ //
24194
+ // Splitting here rather than calling `.strict()`: strict would REFUSE the
24195
+ // file, which is the outcome the lock half already rejected — an older
24196
+ // build then runs entirely unmanaged, every pin and lock gone. A bad KNOWN
24197
+ // value still fails, because the nested schema is re-run over the known
24198
+ // subset and its issues are re-raised on this parse.
24199
+ values: external_exports.record(external_exports.string(), external_exports.unknown()).default({}),
23871
24200
  // Which of those the user may not change. A key here with no matching value
23872
24201
  // freezes whatever the user last chose; a value with no lock is a DEFAULT
23873
24202
  // the user may still override. The two are separable on purpose.
@@ -23880,17 +24209,31 @@ var ManagedSettings = external_exports.object({
23880
24209
  // the fleets most likely to carry a version skew. A name outside the enum
23881
24210
  // is still never HONOURED: the lockable set stays explicit above.
23882
24211
  lockedFields: external_exports.array(external_exports.string()).default([])
23883
- }).transform(({ lockedFields, ...rest }) => {
24212
+ }).transform(({ lockedFields, values, ...rest }, ctx) => {
23884
24213
  const known = [];
23885
24214
  const unknown2 = [];
23886
24215
  for (const name of lockedFields) {
23887
24216
  if (isManagedSettingKey(name)) known.push(name);
23888
24217
  else unknown2.push(name);
23889
24218
  }
24219
+ const knownValues = /* @__PURE__ */ Object.create(null);
24220
+ const unknownValues = [];
24221
+ for (const [name, value] of Object.entries(values)) {
24222
+ if (Object.hasOwn(ManagedSettingsValues.shape, name)) knownValues[name] = value;
24223
+ else unknownValues.push(name);
24224
+ }
24225
+ const pinned = ManagedSettingsValues.safeParse(knownValues);
24226
+ if (!pinned.success) {
24227
+ for (const issue2 of pinned.error.issues)
24228
+ ctx.addIssue({ ...issue2, path: ["values", ...issue2.path] });
24229
+ return external_exports.NEVER;
24230
+ }
23890
24231
  return {
23891
24232
  ...rest,
24233
+ values: pinned.data,
23892
24234
  lockedFields: known,
23893
- ...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {}
24235
+ ...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {},
24236
+ ...unknownValues.length > 0 ? { unknownValueFields: unknownValues } : {}
23894
24237
  };
23895
24238
  }).meta({ id: "ManagedSettings" });
23896
24239
  var NO_MANAGED_CONTEXT = { present: false, lockedFields: [] };
@@ -24150,12 +24493,30 @@ var RecommendedActionIdParam = external_exports.object({ id: external_exports.st
24150
24493
  // ../../packages/schema/src/zod/settings-action.ts
24151
24494
  var HistorySyncConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "HistorySyncConsentChoice" });
24152
24495
  var ModelJudgeConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "ModelJudgeConsentChoice" });
24496
+ var WebChatCaptureConsentChoice = external_exports.enum(["granted", "revoked", "unchanged"]).meta({ id: "WebChatCaptureConsentChoice" });
24153
24497
  var SaveSettingsInput = external_exports.object({
24154
24498
  historicalAccess: external_exports.string(),
24155
24499
  modelJudgeConsent: ModelJudgeConsentChoice,
24156
24500
  historySyncConsent: HistorySyncConsentChoice,
24157
24501
  vaultConsent: external_exports.string(),
24158
- vaultInlineReveal: external_exports.string()
24502
+ vaultInlineReveal: external_exports.string(),
24503
+ webChatCaptureConsent: WebChatCaptureConsentChoice,
24504
+ // Widened to `string` like its neighbours rather than typed as
24505
+ // `RedactFallback`, on this module's own layering rule: shape here, VALUE at
24506
+ // the call site, so the domain check receives the type it was written for.
24507
+ //
24508
+ // NOT because a narrower schema would reject differently. `parseActionInput`
24509
+ // is a `safeParse` wrapper and throws for no field schema, so either spelling
24510
+ // reaches a recoverable `{ ok: false }` and there is no rejected promise to
24511
+ // trade against. The real cost runs the other way and is the part worth
24512
+ // knowing: a value this schema admits and the domain enum then rejects lands
24513
+ // on the action's shared refusal, which names NO field, where a shape
24514
+ // rejection reaches `malformedInput` and names the schema key.
24515
+ redactFallback: external_exports.string(),
24516
+ // Shape only, the way the enum fields above are strings only: the RANGE is
24517
+ // `BodyRetention`'s and the action checks it there, so there is one place
24518
+ // that decides what a legal horizon is rather than two that can drift.
24519
+ bodyRetention: external_exports.object({ enabled: external_exports.boolean(), retainDays: external_exports.number() })
24159
24520
  });
24160
24521
  var AttachInput = external_exports.object({
24161
24522
  endpoint: external_exports.string(),
@@ -24327,6 +24688,123 @@ function reviewSeverityRank(reasons) {
24327
24688
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
24328
24689
  }
24329
24690
 
24691
+ // ../../packages/schema/src/zod/web-capture.ts
24692
+ var WebUsageSource = external_exports.enum(["site", "estimated", "none"]);
24693
+ var WebUsage = external_exports.object({
24694
+ inputTokens: external_exports.number().int().nonnegative().optional(),
24695
+ outputTokens: external_exports.number().int().nonnegative().optional(),
24696
+ cacheReadInputTokens: external_exports.number().int().nonnegative().optional(),
24697
+ cacheCreationInputTokens: external_exports.number().int().nonnegative().optional()
24698
+ });
24699
+ var WebToolCall = external_exports.object({
24700
+ toolUseId: external_exports.string().min(1),
24701
+ toolName: external_exports.string().min(1),
24702
+ target: external_exports.string().optional(),
24703
+ isError: external_exports.boolean().optional(),
24704
+ inputSize: external_exports.number().int().nonnegative().optional(),
24705
+ outputSize: external_exports.number().int().nonnegative().optional()
24706
+ });
24707
+ var WebExchange = external_exports.object({
24708
+ messageId: external_exports.string().min(1),
24709
+ startedAt: external_exports.iso.datetime(),
24710
+ model: external_exports.string().optional(),
24711
+ usage: WebUsage.optional(),
24712
+ usageSource: WebUsageSource,
24713
+ stopReason: external_exports.string().optional(),
24714
+ conversationId: external_exports.string().optional(),
24715
+ turnIndex: external_exports.number().int().nonnegative().optional(),
24716
+ toolCalls: external_exports.array(WebToolCall).default([]),
24717
+ // Absent when the adapter recovered no text. Capped by the caller at
24718
+ // RESPONSE_TEXT_MAX_BYTES, so a short capture is never mistaken for a short
24719
+ // reply.
24720
+ responseText: external_exports.string().optional(),
24721
+ // The stored text is short of the reply. It does NOT say which of the two
24722
+ // ceilings on this path cut it: the caller applies its own cap on the raw
24723
+ // bytes it reads off the wire, which can be reached by a stream whose
24724
+ // recovered text stays well under RESPONSE_TEXT_MAX_BYTES, and applies that
24725
+ // one to the text. A reader cannot tell them apart, and nothing downstream
24726
+ // should branch as though it could.
24727
+ truncated: external_exports.boolean().default(false)
24728
+ });
24729
+ var WebEnforcementState = external_exports.enum([
24730
+ "watching",
24731
+ "composer-only",
24732
+ "button-only",
24733
+ "unattached",
24734
+ "unknown"
24735
+ ]);
24736
+ var RESPONSE_TEXT_MAX_BYTES = 2 * 1024 * 1024;
24737
+ var WebCaptureStatus = external_exports.object({
24738
+ patched: external_exports.boolean(),
24739
+ live: external_exports.boolean(),
24740
+ blind: external_exports.boolean(),
24741
+ sendsSeenDom: external_exports.number().int().nonnegative(),
24742
+ exchangesSeenNet: external_exports.number().int().nonnegative(),
24743
+ parseFailures: external_exports.number().int().nonnegative(),
24744
+ unparsedBodies: external_exports.number().int().nonnegative(),
24745
+ // The adapter-declared JSON key paths that were absent from a real payload —
24746
+ // the earliest signal that a site's contract moved.
24747
+ shapeMisses: external_exports.array(external_exports.string()).default([]),
24748
+ // How many `kind: 'conversation'` endpoints the reporting tab's adapter
24749
+ // compiled. Zero means this build declares none for the site, so observing
24750
+ // nothing is the design rather than a fault — the one fact that separates a
24751
+ // site nobody has surveyed yet from one whose contract moved. Defaulted so a
24752
+ // build predating the field is read as declaring nothing rather than refused.
24753
+ conversationEndpoints: external_exports.number().int().nonnegative().default(0),
24754
+ // The document that sent this report is going away. The bridge sets it on
24755
+ // its `pagehide` report and nowhere else.
24756
+ //
24757
+ // A property of the REPORT rather than of capture health, which is why
24758
+ // nothing in `deriveWebCaptureState` reads it and why it stays out of the
24759
+ // bridge's own report signature — a closing tab's last word must not be
24760
+ // suppressed for carrying the same health as the report before it. What
24761
+ // reads it is the per-site fold: a document that said it was unloading stops
24762
+ // voting on the site's state, so the reload the `blind` remediation asks for
24763
+ // can actually clear the verdict it was shown. A document that dies without
24764
+ // sending one is covered by CAPTURE_STATUS_DOCUMENT_QUIET_MS instead.
24765
+ //
24766
+ // Defaulted so a build predating the field reads as a document that never
24767
+ // said it was closing — which keeps it voting, the same as every report that
24768
+ // is not a final one.
24769
+ closed: external_exports.boolean().default(false),
24770
+ // What the DOM enforcement path is doing, which none of the counters above
24771
+ // can say: `sendsSeenDom` rises only once a send has COMPLETED, so a tab
24772
+ // whose watcher never bound reports zero exactly like a tab nobody typed in.
24773
+ // Defaulted to 'unknown' rather than 'watching' so a status from a build
24774
+ // predating the field is not read as reporting a healthy one.
24775
+ enforcement: WebEnforcementState.default("unknown")
24776
+ });
24777
+ function webCaptureStatusObservedTurnPath(status) {
24778
+ if (!status.patched) return true;
24779
+ if (status.conversationEndpoints === 0) return true;
24780
+ return status.blind || status.shapeMisses.length > 0 || status.parseFailures > 0 || status.unparsedBodies > 0 || status.exchangesSeenNet > 0;
24781
+ }
24782
+ function pickReportedCaptureStatus(candidates) {
24783
+ return candidates.find((c) => webCaptureStatusObservedTurnPath(c.status)) ?? candidates[0];
24784
+ }
24785
+ var CAPTURE_STATUS_RECENCY_MS = 30 * 24 * 60 * 60 * 1e3;
24786
+ var CAPTURE_STATUS_RECENCY_DAYS = CAPTURE_STATUS_RECENCY_MS / (24 * 60 * 60 * 1e3);
24787
+ var CAPTURE_STATUS_DOCUMENT_QUIET_MS = 12 * 60 * 60 * 1e3;
24788
+ function fromCaptureStatusAttributes(bag) {
24789
+ const parsedBag = CaptureStatusAttributes.safeParse(bag);
24790
+ if (!parsedBag.success) return null;
24791
+ const b = parsedBag.data;
24792
+ const parsedStatus = WebCaptureStatus.safeParse({
24793
+ patched: b.patched,
24794
+ live: b.live,
24795
+ blind: b.blind,
24796
+ sendsSeenDom: b.sends_seen_dom,
24797
+ exchangesSeenNet: b.exchanges_seen_net,
24798
+ parseFailures: b.parse_failures,
24799
+ unparsedBodies: b.unparsed_bodies,
24800
+ shapeMisses: b.shape_misses,
24801
+ conversationEndpoints: b.conversation_endpoints,
24802
+ closed: b.closed,
24803
+ enforcement: b.enforcement
24804
+ });
24805
+ return parsedStatus.success ? parsedStatus.data : null;
24806
+ }
24807
+
24330
24808
  // ../../packages/persistence/src/paths.ts
24331
24809
  import {
24332
24810
  chmodSync,
@@ -24539,6 +25017,22 @@ function discardStore(file2, backup) {
24539
25017
  }
24540
25018
  }
24541
25019
 
25020
+ // ../../packages/persistence/src/internal/sql-functions.ts
25021
+ var utf8 = new TextDecoder();
25022
+ function akaLower(value) {
25023
+ if (value === null) return null;
25024
+ if (typeof value === "string") return value.toLowerCase();
25025
+ if (typeof value === "number" || typeof value === "bigint") return String(value).toLowerCase();
25026
+ return utf8.decode(value).toLowerCase();
25027
+ }
25028
+ function registerSqlFunctions(db) {
25029
+ db.function(
25030
+ "aka_lower",
25031
+ { deterministic: true, directOnly: true, useBigIntArguments: true },
25032
+ akaLower
25033
+ );
25034
+ }
25035
+
24542
25036
  // ../../packages/persistence/src/internal/sql-text.ts
24543
25037
  function escapeLikePattern(s) {
24544
25038
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -24623,6 +25117,11 @@ function schemaObjectExists(db, kind, name) {
24623
25117
  function indexExists(db, name) {
24624
25118
  return schemaObjectExists(db, "index", name);
24625
25119
  }
25120
+ function indexColumns(db, name) {
25121
+ if (!indexExists(db, name)) return [];
25122
+ const columns = db.prepare(`PRAGMA index_info(${name})`).all();
25123
+ return columns.map((c) => c.name).filter((c) => c !== null);
25124
+ }
24626
25125
  function columnNames(db, table2, opts) {
24627
25126
  const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
24628
25127
  const columns = db.prepare(`PRAGMA ${pragma}(${table2})`).all();
@@ -24684,165 +25183,807 @@ function mapRowsTolerant(rows, map2) {
24684
25183
  return out;
24685
25184
  }
24686
25185
 
24687
- // ../../packages/persistence/src/migrations.ts
24688
- function describeObject(object2) {
24689
- return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
24690
- }
24691
- function splitStatements(sql) {
24692
- return sql.split(/-->\s*statement-breakpoint/).map((s) => s.trim()).filter((s) => s.length > 0);
24693
- }
24694
- function createdIndexName(statement) {
24695
- const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
24696
- return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
25186
+ // ../../packages/persistence/src/internal/outbox-lane.ts
25187
+ var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
25188
+ var OUTBOX_CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
25189
+
25190
+ // ../../packages/persistence/src/sync-failure.ts
25191
+ var SYNC_FAILURE_REASONS = SyncFailureReason.options;
25192
+ function syncFailureRejectCondition(column = "sync_failure") {
25193
+ const members2 = SYNC_FAILURE_REASONS.map((r) => `'${r}'`).join(", ");
25194
+ return `NEW.${column} IS NOT NULL AND NEW.${column} NOT IN (${members2})`;
24697
25195
  }
24698
- var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
24699
- function applyMigrations(db, file2) {
24700
- const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
24701
- db.exec(
24702
- "CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
24703
- );
24704
- const applied = new Set(
24705
- db.prepare("SELECT tag FROM migration_ledger").all().map((r) => r.tag)
24706
- );
24707
- const preLedgerStore = applied.size === 0 && legacyCount > 0;
24708
- const record2 = db.prepare(
24709
- "INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)"
24710
- );
24711
- for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
24712
- if (applied.has(migration.tag)) continue;
24713
- if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
24714
- const evidence = evidenceObjects(migration.sql);
24715
- const present = evidence.filter((o) => evidenceExists(db, o));
24716
- if (present.length > 0 && present.length < evidence.length) {
24717
- const missing = evidence.filter((o) => !present.includes(o));
24718
- const message = `sqlite migration ${migration.tag} has no ledger row, but the store already has ${present.map(describeObject).join(", ")} while missing ${missing.map(describeObject).join(", ")} \u2014 the schema diverged from the migration history; refusing to replay or skip.`;
24719
- akaWarn(message);
24720
- throw new Error(`[aka] ${message}`);
24721
- }
24722
- const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
24723
- const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
24724
- const statements = splitStatements(migration.sql);
24725
- if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
24726
- try {
24727
- withTransaction(
24728
- db,
24729
- () => {
24730
- for (const statement of statements) {
24731
- const indexName = createdIndexName(statement);
24732
- if (indexName === void 0) {
24733
- if (alreadyApplied) continue;
24734
- } else if (indexExists(db, indexName)) {
24735
- continue;
24736
- }
24737
- db.exec(statement);
24738
- }
24739
- if (wantsFkOff && !alreadyApplied) {
24740
- const violations = db.prepare("PRAGMA foreign_key_check").all();
24741
- if (violations.length > 0) {
24742
- throw new Error(
24743
- `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
24744
- );
24745
- }
24746
- }
24747
- record2.run(migration.tag, Date.now());
24748
- },
24749
- "IMMEDIATE"
24750
- );
24751
- } finally {
24752
- if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
24753
- }
25196
+
25197
+ // ../../packages/persistence/src/repositories/history-sync.ts
25198
+ var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
25199
+ var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
25200
+ var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_TYPE_LIST;
25201
+ var COUNTED_EVENT_TYPES = [
25202
+ ...STRUCTURAL_EVENT_TYPES,
25203
+ ...OUTBOX_CAPTURE_EVENT_TYPES
25204
+ ];
25205
+ var COUNTED_TYPE_LIST = COUNTED_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
25206
+ var PARTITION_BUCKETS = `
25207
+ SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NULL THEN 1 ELSE 0 END) AS queued,
25208
+ SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NOT NULL THEN 1 ELSE 0 END) AS inProgress,
25209
+ SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS synced,
25210
+ SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
25211
+ AND sync_failure = 'deployment_refused' THEN 1 ELSE 0 END) AS refused,
25212
+ SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
25213
+ AND sync_failure = 'detached_undelivered' THEN 1 ELSE 0 END) AS detached,
25214
+ -- Spelled as what it INCLUDES rather than what it excludes, so a reason
25215
+ -- added later lands in no bucket and fails the sum assertion, instead
25216
+ -- of silently joining this one.
25217
+ SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0
25218
+ AND (sync_failure IS NULL OR sync_failure = 'payload_invalid')
25219
+ THEN 1 ELSE 0 END) AS failed,
25220
+ COUNT(*) AS total`;
25221
+ var COUNTED_SCOPE = `
25222
+ WHERE event_type IN (${COUNTED_TYPE_LIST})
25223
+ AND (
25224
+ event_type IN (${TYPE_LIST})
25225
+ OR synced_at IS NOT NULL
25226
+ OR outbox_owed = 1
25227
+ )`;
25228
+ var SKIPPED = -1;
25229
+ var ROW_COLUMNS = `id,
25230
+ parent_id AS parentId,
25231
+ root_session_id AS rootSessionId,
25232
+ event_type AS eventType,
25233
+ host_id AS hostId,
25234
+ harness_id AS harnessId,
25235
+ source_project_id AS sourceProjectId,
25236
+ started_at AS startedAt,
25237
+ ended_at AS endedAt,
25238
+ severity,
25239
+ priority,
25240
+ content,
25241
+ content_hash AS contentHash,
25242
+ attributes`;
25243
+ var SqliteHistorySyncRepository = class {
25244
+ constructor(db) {
25245
+ this.db = db;
25246
+ this.ensureRowStmt = db.prepare(`INSERT OR IGNORE INTO history_sync (id) VALUES (1)`);
25247
+ this.sessionsStmt = db.prepare(
25248
+ `SELECT COALESCE(root_session_id, id) AS sessionId, MIN(started_at) AS earliest
25249
+ FROM audit_events
25250
+ WHERE synced_at IS NULL
25251
+ AND event_type IN (${TYPE_LIST})
25252
+ AND started_at < :before
25253
+ GROUP BY sessionId
25254
+ ORDER BY earliest
25255
+ LIMIT :limit`
25256
+ );
25257
+ this.rowsStmt = db.prepare(
25258
+ `SELECT ${ROW_COLUMNS}
25259
+ FROM audit_events
25260
+ WHERE synced_at IS NULL
25261
+ AND event_type IN (${TYPE_LIST})
25262
+ AND started_at < :before
25263
+ AND COALESCE(root_session_id, id) = :sessionId
25264
+ ORDER BY (event_type = 'session') DESC, started_at
25265
+ LIMIT :limit`
25266
+ );
25267
+ this.captureRowsStmt = db.prepare(
25268
+ `SELECT ${ROW_COLUMNS}
25269
+ FROM audit_events
25270
+ WHERE synced_at IS NULL
25271
+ AND sync_claimed_at IS NULL
25272
+ AND outbox_owed = 1
25273
+ AND event_type IN (${CAPTURE_TYPE_LIST})
25274
+ AND started_at < :before
25275
+ ORDER BY started_at
25276
+ LIMIT :limit`
25277
+ );
25278
+ this.markOwedStmt = db.prepare(
25279
+ `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
25280
+ );
25281
+ this.markCaptureBacklogOwedStmt = db.prepare(
25282
+ `UPDATE audit_events SET outbox_owed = 1
25283
+ WHERE synced_at IS NULL
25284
+ AND event_type IN (${CAPTURE_TYPE_LIST})
25285
+ AND started_at < :before`
25286
+ );
25287
+ this.stampStmt = db.prepare(
25288
+ `UPDATE audit_events
25289
+ SET synced_at = :at,
25290
+ sync_claimed_at = NULL,
25291
+ sync_failed_at = :failedAt,
25292
+ sync_failure = :failure
25293
+ WHERE id = :id`
25294
+ );
25295
+ this.claimRowStmt = db.prepare(
25296
+ `UPDATE audit_events SET sync_claimed_at = :at WHERE id = :id AND synced_at IS NULL`
25297
+ );
25298
+ this.releaseRowStmt = db.prepare(
25299
+ `UPDATE audit_events SET sync_claimed_at = NULL WHERE id = :id`
25300
+ );
25301
+ this.releaseStaleClaimsStmt = db.prepare(
25302
+ `UPDATE audit_events SET sync_claimed_at = NULL
25303
+ WHERE sync_claimed_at IS NOT NULL AND sync_claimed_at < :staleBefore`
25304
+ );
25305
+ this.partitionStmt = db.prepare(`SELECT${PARTITION_BUCKETS}
25306
+ FROM audit_events${COUNTED_SCOPE}`);
25307
+ this.partitionByKindStmt = db.prepare(
25308
+ `SELECT event_type AS kind,${PARTITION_BUCKETS}
25309
+ FROM audit_events INDEXED BY idx_audit_events_sync${COUNTED_SCOPE}
25310
+ GROUP BY event_type`
25311
+ );
25312
+ this.countsStmt = db.prepare(
25313
+ `SELECT
25314
+ SUM(CASE WHEN synced_at IS NULL AND started_at < :before THEN 1 ELSE 0 END) AS pending,
25315
+ SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS sent,
25316
+ SUM(CASE WHEN synced_at = ${String(SKIPPED)}
25317
+ AND (sync_failure IS NULL OR sync_failure = 'payload_invalid')
25318
+ THEN 1 ELSE 0 END) AS skipped,
25319
+ SUM(CASE WHEN synced_at = ${String(SKIPPED)}
25320
+ AND sync_failure = 'deployment_refused' THEN 1 ELSE 0 END) AS refused,
25321
+ SUM(CASE WHEN synced_at = ${String(SKIPPED)}
25322
+ AND sync_failure = 'detached_undelivered' THEN 1 ELSE 0 END) AS detached
25323
+ FROM audit_events
25324
+ WHERE event_type IN (${TYPE_LIST})`
25325
+ );
25326
+ this.captureSkipCountStmt = db.prepare(
25327
+ // EVERY sentinel capture, whatever the reason — deliberately NOT split the
25328
+ // way the structural totals are. The split exists because a refusal is
25329
+ // terminal only against the deployment that gave it, and the structural
25330
+ // re-arm frees it on a change of deployment. The capture lane has no such
25331
+ // escape: re-arming a capture would offer one deployment's undelivered
25332
+ // prompts, with their text, to a deployment that never saw them, which is
25333
+ // exactly what disownCapturesStmt exists to prevent. So on this lane both
25334
+ // reasons mean the same thing — this row will not be sent — and splitting
25335
+ // them would put refused captures in a bucket nothing reads and nothing
25336
+ // frees.
25337
+ `SELECT COUNT(*) AS skipped
25338
+ FROM audit_events
25339
+ WHERE synced_at = ${String(SKIPPED)}
25340
+ AND event_type IN (${CAPTURE_TYPE_LIST})`
25341
+ );
25342
+ this.fingerprintStmt = db.prepare(
25343
+ `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
25344
+ FROM history_sync WHERE id = 1`
25345
+ );
25346
+ this.setFingerprintStmt = db.prepare(
25347
+ `UPDATE history_sync
25348
+ SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
25349
+ WHERE id = 1`
25350
+ );
25351
+ this.disownCapturesStmt = db.prepare(
25352
+ `UPDATE audit_events SET outbox_owed = NULL
25353
+ WHERE outbox_owed IS NOT NULL
25354
+ AND event_type IN (${CAPTURE_TYPE_LIST})
25355
+ AND started_at < :attachedAt`
25356
+ );
25357
+ this.rearmStmt = db.prepare(
25358
+ `UPDATE audit_events
25359
+ SET synced_at = NULL, sync_failed_at = NULL, sync_failure = NULL
25360
+ WHERE (synced_at > 0
25361
+ OR sync_failure IN ('deployment_refused', 'detached_undelivered'))
25362
+ AND event_type IN (${TYPE_LIST})`
25363
+ );
25364
+ this.claimStmt = db.prepare(
25365
+ `UPDATE history_sync
25366
+ SET owner_pid = :pid, owner_host = :host, acquired_at = :now, heartbeat_at = :now
25367
+ WHERE id = 1
25368
+ AND (owner_pid IS NULL
25369
+ OR heartbeat_at IS NULL
25370
+ OR heartbeat_at < :staleBefore
25371
+ OR heartbeat_at > :now)`
25372
+ );
25373
+ this.heartbeatStmt = db.prepare(
25374
+ `UPDATE history_sync SET heartbeat_at = :now WHERE id = 1 AND owner_pid = :pid`
25375
+ );
25376
+ this.releaseStmt = db.prepare(
25377
+ `UPDATE history_sync
25378
+ SET owner_pid = NULL, owner_host = NULL, acquired_at = NULL, heartbeat_at = NULL
25379
+ WHERE id = 1 AND owner_pid = :pid`
25380
+ );
25381
+ this.closeWindowStmt = db.prepare(
25382
+ `UPDATE audit_events
25383
+ SET synced_at = ${String(SKIPPED)},
25384
+ sync_failed_at = :at,
25385
+ sync_failure = 'detached_undelivered'
25386
+ WHERE synced_at IS NULL
25387
+ AND event_type IN (${TYPE_LIST})
25388
+ AND started_at >= :attachedAt`
25389
+ );
25390
+ this.releaseBoundaryStmt = db.prepare(
25391
+ `UPDATE history_sync SET backlog_before = NULL WHERE id = 1`
25392
+ );
25393
+ this.freezeBoundaryStmt = db.prepare(
25394
+ `UPDATE history_sync SET backlog_before = :backlogBefore WHERE id = 1`
25395
+ );
25396
+ this.leaseStmt = db.prepare(
25397
+ `SELECT owner_pid AS ownerPid, owner_host AS ownerHost,
25398
+ acquired_at AS acquiredAt, heartbeat_at AS heartbeatAt
25399
+ FROM history_sync WHERE id = 1`
25400
+ );
25401
+ this.inspectionsStmt = db.prepare(
25402
+ `SELECT d.rule_id AS ruleId,
25403
+ d.name AS ruleName,
25404
+ d.version AS ruleVersion,
25405
+ d.category AS category,
25406
+ d.severity AS severity,
25407
+ f.span_start AS spanStart,
25408
+ f.span_end AS spanEnd,
25409
+ f.masked_match AS maskedMatch,
25410
+ f.action_taken AS actionTaken,
25411
+ f.confidence AS confidence
25412
+ FROM inspection_findings f
25413
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
25414
+ WHERE f.audit_event_id = :auditEventId
25415
+ ORDER BY f.span_start, f.id`
25416
+ );
24754
25417
  }
24755
- if (legacyCount < SQLITE_MIGRATIONS.length) {
24756
- db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
25418
+ db;
25419
+ ensureRowStmt;
25420
+ sessionsStmt;
25421
+ rowsStmt;
25422
+ stampStmt;
25423
+ countsStmt;
25424
+ fingerprintStmt;
25425
+ setFingerprintStmt;
25426
+ rearmStmt;
25427
+ claimStmt;
25428
+ heartbeatStmt;
25429
+ releaseStmt;
25430
+ leaseStmt;
25431
+ inspectionsStmt;
25432
+ closeWindowStmt;
25433
+ releaseBoundaryStmt;
25434
+ freezeBoundaryStmt;
25435
+ captureRowsStmt;
25436
+ markOwedStmt;
25437
+ markCaptureBacklogOwedStmt;
25438
+ captureSkipCountStmt;
25439
+ disownCapturesStmt;
25440
+ partitionStmt;
25441
+ partitionByKindStmt;
25442
+ claimRowStmt;
25443
+ releaseRowStmt;
25444
+ releaseStaleClaimsStmt;
25445
+ /**
25446
+ * The masked detections recorded against one tool call.
25447
+ *
25448
+ * These travel with the event because a tool call's target is not
25449
+ * re-inspectable from the event alone — unlike a capture, where the text
25450
+ * itself is re-scannable. What crosses is the masked match and the rule that
25451
+ * produced it, never the value.
25452
+ */
25453
+ inspectionsFor(auditEventId) {
25454
+ return allRows(this.inspectionsStmt, { auditEventId });
24757
25455
  }
24758
- ensureSyncedAtColumn(db, "audit_events");
24759
- ensureScanLedgerTable(db);
24760
- ensureHistorySyncTable(db);
24761
- ensureBlockedDetectionsTable(db);
24762
- ensureRuleProbeCacheTable(db);
24763
- ensureWriteGateTrigger(db);
24764
- ensureTokenUsageColumns(db);
24765
- reconcileSourceProjectIds(db);
24766
- if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
24767
- const drained = runLegacyHistoryBackfill(db);
24768
- if (drained) applyLegacyDropMigration(db, file2);
25456
+ /**
25457
+ * Sessions with structural rows still to send, oldest first.
25458
+ *
25459
+ * BOUNDED BY THE BACKLOG BOUNDARY, which is the whole correctness of this
25460
+ * read. Anything recorded after the machine attached is the live forward
25461
+ * path's to deliver; this drain exists for what was recorded before it, and a
25462
+ * row both paths send is at best a duplicate request and at worst — for a
25463
+ * session root — an overwrite of the inventory ids the live path resolved.
25464
+ */
25465
+ pendingSessions(limit, before) {
25466
+ return allRows(this.sessionsStmt, { limit, before }).map(
25467
+ (r) => r.sessionId
25468
+ );
24769
25469
  }
24770
- }
24771
- function readLegacyTables(db) {
24772
- let holdsRows = false;
24773
- const marks = [];
24774
- for (const table2 of ["events", "findings"]) {
24775
- try {
24776
- const row = db.prepare(`SELECT count(*) AS n, ifnull(max(rowid), -1) AS hi FROM ${table2}`).get();
24777
- if (row === void 0) {
24778
- holdsRows = true;
24779
- marks.push(`${table2}:unreadable`);
24780
- continue;
24781
- }
24782
- if (row.n > 0) holdsRows = true;
24783
- marks.push(`${table2}:${String(row.n)}:${String(row.hi)}`);
24784
- } catch {
24785
- holdsRows = true;
24786
- marks.push(`${table2}:unreadable`);
24787
- }
25470
+ /** One session's undelivered structural rows within the backlog, root first. */
25471
+ pendingRows(sessionId, limit, before) {
25472
+ return allRows(this.rowsStmt, { sessionId, limit, before });
24788
25473
  }
24789
- return { holdsRows, mark: marks.join("|") };
24790
- }
24791
- function applyLegacyDropMigration(db, file2) {
24792
- const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
24793
- if (!migration) return;
24794
- const before = file2 === void 0 ? void 0 : readLegacyTables(db);
24795
- if (file2 !== void 0 && before?.holdsRows === true) {
24796
- try {
24797
- backupBeforeLegacyDrop(db, file2);
24798
- } catch (error61) {
24799
- akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error61)}`);
24800
- return;
24801
- }
25474
+ /**
25475
+ * Captures this machine still owes the deployment, oldest first.
25476
+ *
25477
+ * Selected by the `outbox_owed` marker the attached forward path writes, not
25478
+ * by a time window — see captureRowsStmt for why a window could not express
25479
+ * this. `before` is the grace window that leaves a just-recorded capture to
25480
+ * the live path.
25481
+ */
25482
+ pendingCaptureRows(limit, before) {
25483
+ return allRows(this.captureRowsStmt, { limit, before });
24802
25484
  }
24803
- try {
25485
+ /**
25486
+ * Record that a capture is OWED to the deployment.
25487
+ *
25488
+ * Written by the attached forward path when a live send did not confirm
25489
+ * delivery, and read by the drain as the whole of its eligibility test. It is
25490
+ * a fact rather than an inference: the machine was attached, the send did not
25491
+ * land, so the row is owed — which no time window can state, because the same
25492
+ * window that holds the rows a past attachment left owed also holds every
25493
+ * capture recorded while the machine was DETACHED, and those were never
25494
+ * offered to anyone.
25495
+ *
25496
+ * Idempotent, and never un-set: `markSynced` settling the row is what takes it
25497
+ * out of the drain's read.
25498
+ */
25499
+ markCaptureOwed(id) {
25500
+ this.markOwedStmt.run({ id });
25501
+ }
25502
+ /**
25503
+ * Mark every capture already on disk as owed, as of `before`.
25504
+ *
25505
+ * The consent-time backfill, called once from `aka attach` when a human
25506
+ * grants existing-history consent — never from an ongoing drain pass, and
25507
+ * never inferred from a boundary that could later move. `before` is the
25508
+ * caller's own "now" at the moment consent was granted, so what this marks
25509
+ * is exactly the backlog the consent prompt already counted, not whatever a
25510
+ * later re-attach or key rotation might widen it to.
25511
+ *
25512
+ * Returns how many rows matched, for the caller to log or test against. Not a
25513
+ * count of NEWLY marked rows — a row still unsynced from an earlier call
25514
+ * matches again and is counted again, the same as `UPDATE`'s own `changes`.
25515
+ */
25516
+ markCaptureBacklogOwed(before) {
25517
+ return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
25518
+ }
25519
+ /**
25520
+ * Record delivery. Called only AFTER the far side has accepted the rows.
25521
+ *
25522
+ * CLEARS any failure reason in the same statement. A row that failed against
25523
+ * one deployment and then landed is delivered, and leaving the reason behind
25524
+ * would leave the store holding two contradictory answers about one row —
25525
+ * with the surface free to render either.
25526
+ */
25527
+ markSynced(ids, atMs) {
25528
+ this.stampAll(ids, atMs, null);
25529
+ }
25530
+ /**
25531
+ * Record that THIS MACHINE cannot express the row on the wire.
25532
+ *
25533
+ * Reserved for a local defect — a row that cannot be rebuilt into a valid
25534
+ * payload, or a body the client itself refused to send. It fails identically
25535
+ * against every deployment, so it is terminal everywhere and the re-arm leaves
25536
+ * it alone. A row that merely failed to REACH the deployment stays NULL, so it
25537
+ * is retried; marking those would turn one outage into permanent data loss.
25538
+ */
25539
+ markSkipped(ids, atMs) {
25540
+ this.stampAll(ids, SKIPPED, "payload_invalid", atMs);
25541
+ }
25542
+ /**
25543
+ * Record that THIS DEPLOYMENT refused the row.
25544
+ *
25545
+ * The same sentinel as `markSkipped`, and deliberately so: both stop the row
25546
+ * being re-offered on this lane, and `synced_at` goes on answering whether a
25547
+ * row is outstanding rather than why. What separates them is the reason, and
25548
+ * what the reason buys is the re-arm — a refusal is one deployment's verdict
25549
+ * on one body, so it is terminal only for as long as this machine points at
25550
+ * that deployment, and `rearmFor` clears it when the deployment changes.
25551
+ *
25552
+ * Leaving such a row NULL instead would be worse than the loss it replaces:
25553
+ * these reads carry no cursor, so an unstamped row the deployment refuses is
25554
+ * the head of every subsequent page, and the lane stalls behind it for ever.
25555
+ */
25556
+ markRefused(ids, atMs) {
25557
+ this.stampAll(ids, SKIPPED, "deployment_refused", atMs);
25558
+ }
25559
+ eachInTransaction(ids, run) {
25560
+ if (ids.length === 0) return;
24804
25561
  withTransaction(
24805
- db,
25562
+ this.db,
24806
25563
  () => {
24807
- const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
24808
- if (alreadyDropped) return;
24809
- if (before !== void 0 && readLegacyTables(db).mark !== before.mark) {
24810
- akaWarn(
24811
- "legacy events/findings rows changed after the pre-drop snapshot decision; deferring the drop so the next open can copy them first."
24812
- );
24813
- return;
24814
- }
24815
- for (const statement of splitStatements(migration.sql)) {
24816
- db.exec(statement);
24817
- }
24818
- db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
24819
- migration.tag,
24820
- Date.now()
24821
- );
25564
+ for (const id of ids) run(id);
24822
25565
  },
24823
25566
  "IMMEDIATE"
24824
25567
  );
24825
- } catch (error61) {
24826
- akaWarn(`legacy events/findings drop failed; deferring: ${String(error61)}`);
24827
25568
  }
24828
- }
24829
- function backupBeforeLegacyDrop(db, file2) {
24830
- reapStalePartials(file2);
24831
- const backup = backupPath(file2, "pre-drop");
24832
- snapshotStore(db, backup);
24833
- return backup;
24834
- }
24835
- var TOKEN_USAGE_COLUMNS = [
24836
- {
24837
- name: "input_tokens",
24838
- ddl: "ALTER TABLE audit_events ADD COLUMN input_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.input_tokens')) VIRTUAL"
24839
- },
24840
- {
24841
- name: "output_tokens",
24842
- ddl: "ALTER TABLE audit_events ADD COLUMN output_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.output_tokens')) VIRTUAL"
24843
- },
24844
- {
24845
- name: "cache_creation_input_tokens",
25569
+ stampAll(ids, value, failure, failedAtMs) {
25570
+ if (ids.length === 0) return;
25571
+ const failedAt = failure === null ? null : failedAtMs ?? null;
25572
+ withTransaction(
25573
+ this.db,
25574
+ () => {
25575
+ for (const id of ids) this.stampStmt.run({ at: value, failedAt, failure, id });
25576
+ },
25577
+ "IMMEDIATE"
25578
+ );
25579
+ }
25580
+ /**
25581
+ * Claim rows as in-flight.
25582
+ *
25583
+ * Advisory in exactly the sense the lease is: it records that a send is in
25584
+ * progress so a surface can say so, and a lost claim costs a row showing as
25585
+ * queued while it is actually being sent. It is not exclusion — the far side
25586
+ * settles a duplicate on the row id.
25587
+ */
25588
+ claimRows(ids, atMs) {
25589
+ this.eachInTransaction(ids, (id) => this.claimRowStmt.run({ at: atMs, id }));
25590
+ }
25591
+ /** Give back a claim without settling — the send failed, the row is queued again. */
25592
+ releaseRows(ids) {
25593
+ this.eachInTransaction(ids, (id) => this.releaseRowStmt.run({ id }));
25594
+ }
25595
+ /**
25596
+ * Clear claims older than `staleBefore`, and report how many were cleared.
25597
+ *
25598
+ * A process killed between claiming and settling leaves rows claimed with
25599
+ * nothing left to settle them. Without this they read as "sending" for ever.
25600
+ */
25601
+ releaseStaleClaims(staleBefore) {
25602
+ return Number(this.releaseStaleClaimsStmt.run({ staleBefore }).changes);
25603
+ }
25604
+ /**
25605
+ * Every tracked row in exactly one delivery state.
25606
+ *
25607
+ * Takes no boundary on purpose. The boundary answers "what should the drain
25608
+ * pick up now", which is a different question from "what state is this row
25609
+ * in" — and a machine that has never attached has no boundary to pass, so
25610
+ * requiring one would force a caller to invent one and report the whole store
25611
+ * as queued.
25612
+ */
25613
+ /**
25614
+ * The same partition, one row per kind that a lane carries.
25615
+ *
25616
+ * A kind with nothing to report is ABSENT rather than a row of zeros: the
25617
+ * scope decides which rows exist at all, so a kind that has never been
25618
+ * recorded — or whose captures nobody ever owed — produces no group. A caller
25619
+ * rendering a fixed list of kinds must therefore treat a missing one as "no
25620
+ * rows", never as "zero sent"; the two look identical in a bar and mean
25621
+ * different things.
25622
+ */
25623
+ partitionByKind() {
25624
+ return allRows(
25625
+ this.partitionByKindStmt,
25626
+ {}
25627
+ ).map((row) => ({
25628
+ kind: row.kind,
25629
+ queued: row.queued ?? 0,
25630
+ inProgress: row.inProgress ?? 0,
25631
+ synced: row.synced ?? 0,
25632
+ failed: row.failed ?? 0,
25633
+ refused: row.refused ?? 0,
25634
+ detached: row.detached ?? 0,
25635
+ total: row.total ?? 0
25636
+ }));
25637
+ }
25638
+ partition() {
25639
+ const row = getRow(this.partitionStmt, {});
25640
+ return {
25641
+ queued: row?.queued ?? 0,
25642
+ inProgress: row?.inProgress ?? 0,
25643
+ synced: row?.synced ?? 0,
25644
+ failed: row?.failed ?? 0,
25645
+ refused: row?.refused ?? 0,
25646
+ detached: row?.detached ?? 0,
25647
+ total: row?.total ?? 0
25648
+ };
25649
+ }
25650
+ /** `pending` counts only what is inside the backlog; sent and skipped are totals. */
25651
+ counts(before) {
25652
+ const row = getRow(this.countsStmt, { before });
25653
+ const captures = getRow(this.captureSkipCountStmt);
25654
+ return {
25655
+ pending: row?.pending ?? 0,
25656
+ sent: row?.sent ?? 0,
25657
+ skipped: row?.skipped ?? 0,
25658
+ refused: row?.refused ?? 0,
25659
+ detached: row?.detached ?? 0,
25660
+ capturesSkipped: captures?.skipped ?? 0
25661
+ };
25662
+ }
25663
+ /**
25664
+ * The deployment the current stamps were made against, and where its backlog
25665
+ * ends.
25666
+ *
25667
+ * READ-ONLY. An absent row reads as an absent deployment, which is what a
25668
+ * machine that has never drained is — and every writer below seeds the row
25669
+ * before it needs one, so nothing depends on this creating it. Keeping the
25670
+ * write off the gate path matters because the gate runs on every pass while a
25671
+ * write has to take the database's write lock.
25672
+ */
25673
+ deployment() {
25674
+ const row = getRow(
25675
+ this.fingerprintStmt
25676
+ );
25677
+ return {
25678
+ fingerprint: row?.fingerprint ?? void 0,
25679
+ backlogBefore: row?.backlogBefore ?? void 0
25680
+ };
25681
+ }
25682
+ /**
25683
+ * Point the ledger at a different deployment, discarding what it recorded
25684
+ * about the previous one.
25685
+ *
25686
+ * Delivery is a fact about ONE recipient: rows sent to the deployment a
25687
+ * machine has just left are undelivered as far as the new one is concerned.
25688
+ * All four in one transaction, so a crash between them cannot leave stamps
25689
+ * attributed to the wrong deployment, a boundary that belongs to another, or
25690
+ * a disown with no re-mark to follow it.
25691
+ *
25692
+ * The boundary is written HERE and only here, which is what freezes it: a
25693
+ * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
25694
+ * unchanged, so this never runs and the backlog does not widen back over rows
25695
+ * the live path has since delivered.
25696
+ *
25697
+ * `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
25698
+ * granted existing-history consent for the deployment this call is arming —
25699
+ * a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
25700
+ * instant, `backlogBefore` is the ATTACH instant, and the two can be far
25701
+ * apart. Passed only when that grant is valid, since this method has no way
25702
+ * to check consent itself and must not mark a row owed for a machine that
25703
+ * never agreed to it. Applied AFTER the disown above, in the SAME
25704
+ * transaction: what the disown clears is every marker below `backlogBefore`,
25705
+ * which includes this deployment's OWN pre-attach rows — `aka attach` calls
25706
+ * `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
25707
+ * on the cleared side of that bound — and the re-mark in the same
25708
+ * transaction is what puts those rows back. A crash between the two cannot
25709
+ * strand the ledger disowned with nothing re-marked — the transaction either
25710
+ * lands whole or not at all, and a fingerprint mismatch that has not yet
25711
+ * committed re-enters this method on the very next pass. Omit it (the
25712
+ * structural-only tests do) to exercise the disown in isolation.
25713
+ *
25714
+ * The disown is bounded by `backlogBefore`, which is what keeps it from
25715
+ * touching a marker the NEW deployment's OWN live path has already set: B's
25716
+ * live path can mark a capture owed from the moment `aka attach` writes the
25717
+ * descriptor, before the drain's first pass ever reaches this method, and
25718
+ * such a row sits at or after the bound rather than below it. What keeps the
25719
+ * disown from eating THIS SAME CALL's own re-mark is the order, not the
25720
+ * bound — disown runs first, re-mark second, both inside the one
25721
+ * transaction above.
25722
+ */
25723
+ rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
25724
+ this.ensureRowStmt.run();
25725
+ withTransaction(
25726
+ this.db,
25727
+ () => {
25728
+ const previous = getRow(this.fingerprintStmt)?.fingerprint;
25729
+ this.rearmStmt.run();
25730
+ if (previous !== null && previous !== void 0 && previous !== fingerprint) {
25731
+ this.disownCapturesStmt.run({ attachedAt: backlogBefore });
25732
+ }
25733
+ if (backfillCapturesBefore !== void 0) {
25734
+ this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
25735
+ }
25736
+ this.setFingerprintStmt.run({ fingerprint, backlogBefore });
25737
+ },
25738
+ "IMMEDIATE"
25739
+ );
25740
+ }
25741
+ /**
25742
+ * End the attached period: hand its rows to the live path, and release the
25743
+ * boundary so the next attachment can freeze a new one.
25744
+ *
25745
+ * WITHOUT THIS, a detach and a re-attach to the same deployment leave a window
25746
+ * nothing delivers. The fingerprint is unchanged, so the boundary is never
25747
+ * re-frozen and stays at the FIRST attachment — while nothing forwards at all
25748
+ * during the detached period, because the machine is not attached. Rows
25749
+ * recorded in that window sit after the boundary and before the re-attach, so
25750
+ * neither path takes them, and the pending count reports none outstanding.
25751
+ *
25752
+ * WHAT IT RECORDS, and what it deliberately does not. These rows were the
25753
+ * closing attachment's to deliver and are no longer outstanding — that is what
25754
+ * lets the boundary move. It is NOT a claim that any of them arrived, and the
25755
+ * distinction is not academic: this used to write a delivery TIME, which every
25756
+ * read treats as delivery, so one detach turned a window of undelivered rows
25757
+ * into a window of delivered ones and no surface could tell. It writes the
25758
+ * skip sentinel and a reason of its own instead, so "no longer owed" and
25759
+ * "received" stop being the same fact.
25760
+ *
25761
+ * A change of deployment still frees them (see the re-arm), because the next
25762
+ * deployment has seen none of this machine's history — so the rows reach it
25763
+ * exactly as they did when this wrote a delivery time.
25764
+ *
25765
+ * ONE TRANSACTION, so a crash cannot release the boundary while leaving the
25766
+ * window unstamped — that half-state would re-send the whole attached period
25767
+ * on the next attach, which is the failure the boundary exists to prevent.
25768
+ */
25769
+ closeAttachedWindow(attachedAtMs, atMs) {
25770
+ this.ensureRowStmt.run();
25771
+ withTransaction(
25772
+ this.db,
25773
+ () => {
25774
+ const row = getRow(this.fingerprintStmt);
25775
+ const from = row?.backlogBefore ?? attachedAtMs;
25776
+ this.closeWindowStmt.run({ at: atMs, attachedAt: from });
25777
+ this.releaseBoundaryStmt.run();
25778
+ },
25779
+ "IMMEDIATE"
25780
+ );
25781
+ }
25782
+ /**
25783
+ * Freeze a boundary for the deployment already on file, KEEPING the stamps.
25784
+ *
25785
+ * The re-attach half of the above. Distinct from `rearmFor`, which is for a
25786
+ * different deployment and therefore discards what was delivered to the old
25787
+ * one: here the recipient is the same, so everything already sent to it stays
25788
+ * sent.
25789
+ */
25790
+ freezeBoundary(backlogBefore) {
25791
+ this.ensureRowStmt.run();
25792
+ this.freezeBoundaryStmt.run({ backlogBefore });
25793
+ }
25794
+ /** Take the claim, or report that someone live already holds it. */
25795
+ claim(pid, host, nowMs, staleAfterMs) {
25796
+ this.ensureRowStmt.run();
25797
+ let taken = false;
25798
+ withTransaction(
25799
+ this.db,
25800
+ () => {
25801
+ const result = this.claimStmt.run({
25802
+ pid,
25803
+ host,
25804
+ now: nowMs,
25805
+ staleBefore: nowMs - staleAfterMs
25806
+ });
25807
+ taken = result.changes === 1;
25808
+ },
25809
+ "IMMEDIATE"
25810
+ );
25811
+ return taken;
25812
+ }
25813
+ /** Say the holder is still alive. A no-op once the claim has moved on. */
25814
+ heartbeat(pid, nowMs) {
25815
+ this.heartbeatStmt.run({ now: nowMs, pid });
25816
+ }
25817
+ /** Give the claim up. Scoped to this holder, so a taken-over claim is left be. */
25818
+ release(pid) {
25819
+ this.releaseStmt.run({ pid });
25820
+ }
25821
+ /** Who holds the claim, if anyone. Read-only, for the same reason as above. */
25822
+ lease() {
25823
+ return getRow(this.leaseStmt);
25824
+ }
25825
+ };
25826
+
25827
+ // ../../packages/persistence/src/migrations.ts
25828
+ function describeObject(object2) {
25829
+ return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
25830
+ }
25831
+ function splitStatements(sql) {
25832
+ return sql.split(/-->\s*statement-breakpoint/).map((s) => s.trim()).filter((s) => s.length > 0);
25833
+ }
25834
+ function createdIndexName(statement) {
25835
+ const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
25836
+ return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
25837
+ }
25838
+ var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
25839
+ function applyMigrations(db, file2, options = {}) {
25840
+ const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
25841
+ db.exec(
25842
+ "CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
25843
+ );
25844
+ const applied = new Set(
25845
+ db.prepare("SELECT tag FROM migration_ledger").all().map((r) => r.tag)
25846
+ );
25847
+ const preLedgerStore = applied.size === 0 && legacyCount > 0;
25848
+ const record2 = db.prepare(
25849
+ "INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)"
25850
+ );
25851
+ for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
25852
+ if (applied.has(migration.tag)) continue;
25853
+ if (options.skipTags?.has(migration.tag) === true) continue;
25854
+ if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
25855
+ const evidence = evidenceObjects(migration.sql);
25856
+ const present = evidence.filter((o) => evidenceExists(db, o));
25857
+ if (present.length > 0 && present.length < evidence.length) {
25858
+ const missing = evidence.filter((o) => !present.includes(o));
25859
+ const message = `sqlite migration ${migration.tag} has no ledger row, but the store already has ${present.map(describeObject).join(", ")} while missing ${missing.map(describeObject).join(", ")} \u2014 the schema diverged from the migration history; refusing to replay or skip.`;
25860
+ akaWarn(message);
25861
+ throw new Error(`[aka] ${message}`);
25862
+ }
25863
+ const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
25864
+ const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
25865
+ const statements = splitStatements(migration.sql);
25866
+ if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
25867
+ try {
25868
+ withTransaction(
25869
+ db,
25870
+ () => {
25871
+ for (const statement of statements) {
25872
+ const indexName = createdIndexName(statement);
25873
+ if (indexName === void 0) {
25874
+ if (alreadyApplied) continue;
25875
+ } else if (indexExists(db, indexName)) {
25876
+ continue;
25877
+ }
25878
+ db.exec(statement);
25879
+ }
25880
+ if (wantsFkOff && !alreadyApplied) {
25881
+ const violations = db.prepare("PRAGMA foreign_key_check").all();
25882
+ if (violations.length > 0) {
25883
+ throw new Error(
25884
+ `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
25885
+ );
25886
+ }
25887
+ }
25888
+ record2.run(migration.tag, Date.now());
25889
+ },
25890
+ "IMMEDIATE"
25891
+ );
25892
+ } finally {
25893
+ if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
25894
+ }
25895
+ }
25896
+ if (legacyCount < SQLITE_MIGRATIONS.length) {
25897
+ db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
25898
+ }
25899
+ ensureSyncedAtColumn(db, "audit_events");
25900
+ ensureScanLedgerTable(db);
25901
+ ensureHistorySyncTable(db);
25902
+ ensureBlockedDetectionsTable(db);
25903
+ ensureRuleProbeCacheTable(db);
25904
+ ensureWriteGateTrigger(db);
25905
+ ensureTokenUsageColumns(db);
25906
+ reconcileSourceProjectIds(db);
25907
+ if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
25908
+ const drained = runLegacyHistoryBackfill(db);
25909
+ if (drained) applyLegacyDropMigration(db, file2);
25910
+ }
25911
+ }
25912
+ function readLegacyTables(db) {
25913
+ let holdsRows = false;
25914
+ const marks = [];
25915
+ for (const table2 of ["events", "findings"]) {
25916
+ try {
25917
+ const row = db.prepare(`SELECT count(*) AS n, ifnull(max(rowid), -1) AS hi FROM ${table2}`).get();
25918
+ if (row === void 0) {
25919
+ holdsRows = true;
25920
+ marks.push(`${table2}:unreadable`);
25921
+ continue;
25922
+ }
25923
+ if (row.n > 0) holdsRows = true;
25924
+ marks.push(`${table2}:${String(row.n)}:${String(row.hi)}`);
25925
+ } catch {
25926
+ holdsRows = true;
25927
+ marks.push(`${table2}:unreadable`);
25928
+ }
25929
+ }
25930
+ return { holdsRows, mark: marks.join("|") };
25931
+ }
25932
+ function applyLegacyDropMigration(db, file2) {
25933
+ const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
25934
+ if (!migration) return;
25935
+ const before = file2 === void 0 ? void 0 : readLegacyTables(db);
25936
+ if (file2 !== void 0 && before?.holdsRows === true) {
25937
+ try {
25938
+ backupBeforeLegacyDrop(db, file2);
25939
+ } catch (error61) {
25940
+ akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error61)}`);
25941
+ return;
25942
+ }
25943
+ }
25944
+ try {
25945
+ withTransaction(
25946
+ db,
25947
+ () => {
25948
+ const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
25949
+ if (alreadyDropped) return;
25950
+ if (before !== void 0 && readLegacyTables(db).mark !== before.mark) {
25951
+ akaWarn(
25952
+ "legacy events/findings rows changed after the pre-drop snapshot decision; deferring the drop so the next open can copy them first."
25953
+ );
25954
+ return;
25955
+ }
25956
+ for (const statement of splitStatements(migration.sql)) {
25957
+ db.exec(statement);
25958
+ }
25959
+ db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
25960
+ migration.tag,
25961
+ Date.now()
25962
+ );
25963
+ },
25964
+ "IMMEDIATE"
25965
+ );
25966
+ } catch (error61) {
25967
+ akaWarn(`legacy events/findings drop failed; deferring: ${String(error61)}`);
25968
+ }
25969
+ }
25970
+ function backupBeforeLegacyDrop(db, file2) {
25971
+ reapStalePartials(file2);
25972
+ const backup = backupPath(file2, "pre-drop");
25973
+ snapshotStore(db, backup);
25974
+ return backup;
25975
+ }
25976
+ var TOKEN_USAGE_COLUMNS = [
25977
+ {
25978
+ name: "input_tokens",
25979
+ ddl: "ALTER TABLE audit_events ADD COLUMN input_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.input_tokens')) VIRTUAL"
25980
+ },
25981
+ {
25982
+ name: "output_tokens",
25983
+ ddl: "ALTER TABLE audit_events ADD COLUMN output_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.output_tokens')) VIRTUAL"
25984
+ },
25985
+ {
25986
+ name: "cache_creation_input_tokens",
24846
25987
  ddl: "ALTER TABLE audit_events ADD COLUMN cache_creation_input_tokens integer GENERATED ALWAYS AS (json_extract(attributes, '$.cache_creation_input_tokens')) VIRTUAL"
24847
25988
  },
24848
25989
  {
@@ -25116,10 +26257,62 @@ function ensureSyncedAtColumn(db, table2) {
25116
26257
  if (!columns.includes("outbox_owed")) {
25117
26258
  db.exec(`ALTER TABLE ${table2} ADD COLUMN outbox_owed integer`);
25118
26259
  }
26260
+ if (!columns.includes("sync_failed_at")) {
26261
+ db.exec(`ALTER TABLE ${table2} ADD COLUMN sync_failed_at integer`);
26262
+ }
26263
+ if (!columns.includes("sync_failure")) {
26264
+ withTransaction(
26265
+ db,
26266
+ () => {
26267
+ db.exec(`ALTER TABLE ${table2} ADD COLUMN sync_failure text`);
26268
+ db.exec(
26269
+ `UPDATE ${table2} SET synced_at = NULL
26270
+ WHERE synced_at = -1
26271
+ AND event_type IN (${COUNTED_EVENT_TYPES.map((t) => `'${t}'`).join(", ")})`
26272
+ );
26273
+ },
26274
+ "IMMEDIATE"
26275
+ );
26276
+ }
25119
26277
  db.exec(
25120
- `CREATE INDEX IF NOT EXISTS idx_audit_events_sync
25121
- ON audit_events (event_type, synced_at, sync_claimed_at, started_at)`
26278
+ `CREATE TRIGGER IF NOT EXISTS aka_sync_failure_guard
26279
+ BEFORE UPDATE OF sync_failure ON ${table2}
26280
+ WHEN ${syncFailureRejectCondition()}
26281
+ BEGIN SELECT RAISE(ABORT, 'sync_failure is not one of the recorded reasons'); END`
25122
26282
  );
26283
+ const syncIndexColumns = [
26284
+ "event_type",
26285
+ "synced_at",
26286
+ "sync_claimed_at",
26287
+ "started_at",
26288
+ // Appended LAST on purpose. The delivery-state read now projects it, so it
26289
+ // has to be in the index for the read to stay covered — but putting it
26290
+ // ahead of `started_at` would reorder the prefix the structural drain's
26291
+ // reads match on.
26292
+ "sync_failure"
26293
+ // `outbox_owed` is DELIBERATELY ABSENT, and it was measured both ways.
26294
+ //
26295
+ // The delivery-state read tests it — a capture's state depends on whether a
26296
+ // live forward marked it owed — so carrying it here makes that read covering
26297
+ // rather than a row fetch per row: 16 ms against 40 ms on a real 6 GB store.
26298
+ // But a sixth column changes what the planner charges for this index, and
26299
+ // with no ANALYZE statistics it plans from schema shape alone: measured, it
26300
+ // then stops choosing the per-session index for the token rollup and walks
26301
+ // every `llm_call` in the store through the event-type index instead. That
26302
+ // read grows with the store; this one does not.
26303
+ //
26304
+ // 40 ms on the largest store measured, once per render, is a cost worth
26305
+ // paying to leave every other read's plan where it was.
26306
+ ];
26307
+ const currentSyncIndex = indexColumns(db, "idx_audit_events_sync");
26308
+ const syncIndexMatches = currentSyncIndex.length === syncIndexColumns.length && currentSyncIndex.every((column, i) => column === syncIndexColumns[i]);
26309
+ if (!syncIndexMatches) {
26310
+ db.exec("DROP INDEX IF EXISTS idx_audit_events_sync");
26311
+ db.exec(
26312
+ `CREATE INDEX idx_audit_events_sync
26313
+ ON audit_events (${syncIndexColumns.join(", ")})`
26314
+ );
26315
+ }
25123
26316
  db.exec(
25124
26317
  `CREATE INDEX IF NOT EXISTS idx_audit_outbox_owed
25125
26318
  ON audit_events (event_type, synced_at, sync_claimed_at, started_at) WHERE outbox_owed = 1`
@@ -25341,7 +26534,11 @@ function buildAuditEvent(row) {
25341
26534
  link: linkParsed?.success ? linkParsed.data : null,
25342
26535
  targetId: row.target_id,
25343
26536
  internal: intToBool(row.internal),
25344
- flagged: intToBool(row.flagged)
26537
+ flagged: intToBool(row.flagged),
26538
+ // Only meaningful when the title came out empty — a row whose body was
26539
+ // expired but whose title fell back to `tool_name` still has something to
26540
+ // render, and flagging it would make the view apologise for nothing.
26541
+ bodyExpired: row.content_expired_at !== null && (row.title ?? "") === ""
25345
26542
  };
25346
26543
  }
25347
26544
  var TIMELINE_COLUMNS = `
@@ -25349,6 +26546,7 @@ var TIMELINE_COLUMNS = `
25349
26546
  event_type,
25350
26547
  started_at,
25351
26548
  coalesce(content, json_extract(attributes, '$.tool_name')) AS title,
26549
+ content_expired_at,
25352
26550
  coalesce(json_extract(attributes, '$.detail'), json_extract(attributes, '$.target')) AS detail,
25353
26551
  coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
25354
26552
  json_extract(attributes, '$.severity') AS severity,
@@ -25391,7 +26589,7 @@ var SESSION_ROOT = `event_type = 'session'`;
25391
26589
  var HAS_ACTIVITY = `EXISTS (
25392
26590
  SELECT 1 FROM audit_events c
25393
26591
  WHERE c.root_session_id = audit_events.id
25394
- AND c.event_type NOT IN ('hook', 'config_scan'))`;
26592
+ AND c.event_type NOT IN ('hook', 'config_scan', 'capture_status'))`;
25395
26593
  var SqliteActivityRepository = class {
25396
26594
  constructor(db, now = () => Date.now()) {
25397
26595
  this.db = db;
@@ -25418,10 +26616,10 @@ var SqliteActivityRepository = class {
25418
26616
  SELECT id FROM audit_events WHERE event_type = 'session' AND started_at >= ?
25419
26617
  UNION
25420
26618
  SELECT root_session_id FROM audit_events INDEXED BY idx_audit_started_at
25421
- WHERE started_at >= ?
26619
+ WHERE started_at >= ? AND event_type <> 'capture_status'
25422
26620
  UNION
25423
26621
  SELECT root_session_id FROM audit_events INDEXED BY idx_audit_ended_at
25424
- WHERE ended_at >= ?)`,
26622
+ WHERE ended_at >= ? AND event_type <> 'capture_status')`,
25425
26623
  [liveThreshold, liveThreshold, liveThreshold]
25426
26624
  );
25427
26625
  const toolCallsToday = countScalar(
@@ -25828,7 +27026,10 @@ var SqliteAuditEventsRepository = class {
25828
27026
  attributes = excluded.attributes,
25829
27027
  ended_at = excluded.ended_at
25830
27028
  WHERE COALESCE(json_extract(excluded.attributes, '$.output_tokens'), 0)
25831
- > COALESCE(json_extract(audit_events.attributes, '$.output_tokens'), 0)`
27029
+ > COALESCE(json_extract(audit_events.attributes, '$.output_tokens'), 0)
27030
+ OR (json_extract(excluded.attributes, '$.usage_source') IS NOT NULL
27031
+ AND json_extract(excluded.attributes, '$.output_tokens') IS NULL
27032
+ AND excluded.attributes <> audit_events.attributes)`
25832
27033
  );
25833
27034
  this.upsertSessionRootStmt = db.prepare(
25834
27035
  `INSERT OR IGNORE INTO audit_events
@@ -26014,6 +27215,169 @@ var SqliteAuditEventsRepository = class {
26014
27215
  }
26015
27216
  };
26016
27217
 
27218
+ // ../../packages/persistence/src/repositories/body-retention.ts
27219
+ var DEFAULT_BATCH_SIZE = 500;
27220
+ var DEFAULT_MAX_ROWS = 5e4;
27221
+ var SYNC_LANE_TYPES_SQL = OUTBOX_CAPTURE_TYPE_LIST;
27222
+ var SqliteBodyRetentionRepository = class {
27223
+ constructor(db) {
27224
+ this.db = db;
27225
+ const select = (laneClause) => `
27226
+ SELECT id, LENGTH(CAST(content AS BLOB)) AS bytes
27227
+ FROM audit_events
27228
+ WHERE content IS NOT NULL
27229
+ AND started_at < :cutoff
27230
+ AND event_type IN (${CAPTURE_EVENT_TYPES_SQL})
27231
+ ${laneClause}
27232
+ ORDER BY started_at
27233
+ LIMIT :limit`;
27234
+ this.candidatesStmt = this.db.prepare(select(""));
27235
+ this.candidatesSyncSafeStmt = this.db.prepare(
27236
+ select(`AND (event_type NOT IN (${SYNC_LANE_TYPES_SQL}) OR synced_at IS NOT NULL)`)
27237
+ );
27238
+ this.heldBySyncStmt = this.db.prepare(`
27239
+ SELECT COUNT(*) AS n
27240
+ FROM audit_events
27241
+ WHERE content IS NOT NULL
27242
+ AND started_at < :cutoff
27243
+ AND event_type IN (${SYNC_LANE_TYPES_SQL})
27244
+ AND synced_at IS NULL`);
27245
+ this.expireStmt = this.db.prepare(
27246
+ `UPDATE audit_events SET content = NULL, content_expired_at = :now WHERE id = :id`
27247
+ );
27248
+ }
27249
+ db;
27250
+ candidatesStmt;
27251
+ candidatesSyncSafeStmt;
27252
+ heldBySyncStmt;
27253
+ expireStmt;
27254
+ /** How many bytes a pass with these options would free, changing nothing. */
27255
+ preview(opts) {
27256
+ const limit = opts.maxRows ?? DEFAULT_MAX_ROWS;
27257
+ const stmt = opts.sweepSyncLane ? this.candidatesStmt : this.candidatesSyncSafeStmt;
27258
+ const rows = stmt.all({ cutoff: opts.cutoff, limit });
27259
+ return {
27260
+ rowsExpired: rows.length,
27261
+ bytesFreed: rows.reduce((sum, r) => sum + r.bytes, 0),
27262
+ rowsHeldBySync: this.countHeldBySync(opts)
27263
+ };
27264
+ }
27265
+ /** Clear eligible bodies, in bounded batches. */
27266
+ expire(opts) {
27267
+ const batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
27268
+ const maxRows = opts.maxRows ?? DEFAULT_MAX_ROWS;
27269
+ const stmt = opts.sweepSyncLane ? this.candidatesStmt : this.candidatesSyncSafeStmt;
27270
+ let rowsExpired = 0;
27271
+ let bytesFreed = 0;
27272
+ let done = true;
27273
+ while (rowsExpired < maxRows) {
27274
+ const remaining = Math.min(batchSize, maxRows - rowsExpired);
27275
+ const batch = stmt.all({ cutoff: opts.cutoff, limit: remaining });
27276
+ if (batch.length === 0) break;
27277
+ withTransaction(
27278
+ this.db,
27279
+ () => {
27280
+ for (const row of batch) this.expireStmt.run({ id: row.id, now: opts.now });
27281
+ },
27282
+ "IMMEDIATE"
27283
+ );
27284
+ rowsExpired += batch.length;
27285
+ bytesFreed += batch.reduce((sum, r) => sum + r.bytes, 0);
27286
+ if (batch.length < remaining) break;
27287
+ if (rowsExpired >= maxRows) {
27288
+ done = stmt.all({ cutoff: opts.cutoff, limit: 1 }).length === 0;
27289
+ }
27290
+ }
27291
+ return { rowsExpired, bytesFreed, rowsHeldBySync: this.countHeldBySync(opts), done };
27292
+ }
27293
+ countHeldBySync(opts) {
27294
+ if (opts.sweepSyncLane) return 0;
27295
+ const row = this.heldBySyncStmt.get({ cutoff: opts.cutoff });
27296
+ return row.n;
27297
+ }
27298
+ };
27299
+
27300
+ // ../../packages/persistence/src/repositories/capture-status.ts
27301
+ var STATUS_LOOKBACK_ROWS = 128;
27302
+ var SqliteCaptureStatusRepository = class {
27303
+ constructor(db) {
27304
+ this.db = db;
27305
+ this.recentStmt = db.prepare(
27306
+ `SELECT a.started_at AS startedAt,
27307
+ a.attributes AS attributes,
27308
+ a.root_session_id AS rootSessionId
27309
+ FROM audit_events a
27310
+ WHERE a.event_type = 'capture_status'
27311
+ AND a.source_tool = ?
27312
+ AND a.started_at >= ?
27313
+ ORDER BY a.started_at DESC, a.id DESC
27314
+ LIMIT ?`
27315
+ );
27316
+ }
27317
+ db;
27318
+ recentStmt;
27319
+ /**
27320
+ * Every document that reported for a site, in registry order by site, from
27321
+ * the last `CAPTURE_STATUS_RECENCY_MS`.
27322
+ *
27323
+ * SEVERAL per site, not one: a browser is many documents and each reports
27324
+ * for itself, so one row per site is a choice about which of them a user
27325
+ * sees — and the newest is the wrong one, since a healthy tab writing a
27326
+ * fresh report would hide a drifting tab's verdict, which is the whole
27327
+ * reason these rows exist. The pick WITHIN a document is made here (the
27328
+ * unchanged `pickReportedCaptureStatus`, over that document's own rows);
27329
+ * choosing between documents belongs where the state semantics live, and
27330
+ * that is `reportedCaptureDocumentForSite` in `@akasecurity/detections` —
27331
+ * this package may not import it.
27332
+ *
27333
+ * `now` is a required argument rather than a `Date.now()` read, so a caller
27334
+ * that already holds a render instant passes THAT one and a test can drive
27335
+ * the window without moving the wall clock.
27336
+ *
27337
+ * A site whose reports have all aged out contributes nothing, so it derives
27338
+ * to `unreported`. That is the point: nothing but the browser extension ever
27339
+ * writes these rows, so an uninstalled extension's last verdict would
27340
+ * otherwise stand as a live claim for ever with no later report able to
27341
+ * clear it.
27342
+ */
27343
+ latest(now) {
27344
+ const since = now - CAPTURE_STATUS_RECENCY_MS;
27345
+ const documents = [];
27346
+ for (const tool of WebSourceTool.options) {
27347
+ const rows = /* @__PURE__ */ new Map();
27348
+ const lastWord = /* @__PURE__ */ new Map();
27349
+ for (const row of allRows(this.recentStmt, [
27350
+ tool,
27351
+ since,
27352
+ STATUS_LOOKBACK_ROWS
27353
+ ])) {
27354
+ const status = fromCaptureStatusAttributes(parseJsonObject(row.attributes));
27355
+ if (status === null) continue;
27356
+ const record2 = { tool, observedAt: epochMillisToIso(row.startedAt), status };
27357
+ const group = rows.get(row.rootSessionId);
27358
+ if (group === void 0) {
27359
+ rows.set(row.rootSessionId, [record2]);
27360
+ lastWord.set(row.rootSessionId, { at: record2.observedAt, closed: status.closed });
27361
+ } else {
27362
+ group.push(record2);
27363
+ }
27364
+ }
27365
+ for (const [root, candidates] of rows) {
27366
+ const picked = pickReportedCaptureStatus(candidates);
27367
+ const last = lastWord.get(root);
27368
+ if (picked === void 0 || last === void 0) continue;
27369
+ documents.push({
27370
+ ...picked,
27371
+ ...root === null ? {} : { rootSessionId: root },
27372
+ lastReportAt: last.at,
27373
+ closed: last.closed
27374
+ });
27375
+ }
27376
+ }
27377
+ return documents;
27378
+ }
27379
+ };
27380
+
26017
27381
  // ../../packages/persistence/src/repositories/classified-data.ts
26018
27382
  var SqliteClassifiedDataRepository = class {
26019
27383
  constructor(db) {
@@ -26842,7 +28206,15 @@ function toFlatFindingRow(r) {
26842
28206
  ...r.tool_name === null ? {} : { toolName: r.tool_name },
26843
28207
  eventId: r.event_id,
26844
28208
  ...r.session_id === null ? {} : { sessionId: r.session_id },
26845
- status: deriveInstanceStatus(r)
28209
+ status: deriveInstanceStatus(r),
28210
+ delivery: deriveFindingDelivery({
28211
+ kind: r.kind,
28212
+ syncedAt: r.synced_at,
28213
+ syncClaimedAt: r.sync_claimed_at,
28214
+ syncFailedAt: r.sync_failed_at,
28215
+ syncFailure: r.sync_failure,
28216
+ outboxOwed: r.outbox_owed
28217
+ })
26846
28218
  };
26847
28219
  }
26848
28220
  function encodeGroupCursor(group) {
@@ -26906,7 +28278,10 @@ var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS c
26906
28278
  e.tool_name AS tool_name,
26907
28279
  f.audit_event_id AS event_id, e.root_session_id AS session_id,
26908
28280
  e.event_type AS kind, f.finding_key AS finding_key,
26909
- ${latestResolutionStatusSql("f")} AS latest_status`;
28281
+ ${latestResolutionStatusSql("f")} AS latest_status,
28282
+ e.synced_at AS synced_at, e.sync_claimed_at AS sync_claimed_at,
28283
+ e.sync_failed_at AS sync_failed_at, e.sync_failure AS sync_failure,
28284
+ e.outbox_owed AS outbox_owed`;
26910
28285
  var DAY_MS3 = 864e5;
26911
28286
  var SqliteFindingsRepository = class {
26912
28287
  constructor(db) {
@@ -27151,6 +28526,7 @@ var SqliteFindingsRepository = class {
27151
28526
  providers: query.provider,
27152
28527
  actions: query.action,
27153
28528
  statuses: query.status,
28529
+ deliveries: query.deployment,
27154
28530
  tools: query.tool,
27155
28531
  repo: query.repo,
27156
28532
  file: query.file,
@@ -27218,6 +28594,7 @@ var SqliteFindingsRepository = class {
27218
28594
  providers: query.provider,
27219
28595
  actions: query.action,
27220
28596
  statuses: query.status,
28597
+ deliveries: query.deployment,
27221
28598
  tools: query.tool,
27222
28599
  q: query.q
27223
28600
  };
@@ -27481,7 +28858,9 @@ var SqliteFindingsRepository = class {
27481
28858
  )
27482
28859
  );
27483
28860
  for (const row of grouped) {
27484
- if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
28861
+ if (Object.hasOwn(byAction, row.action_taken)) {
28862
+ byAction[row.action_taken] = row.c;
28863
+ }
27485
28864
  }
27486
28865
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
27487
28866
  const sevRows = allRows(
@@ -27498,7 +28877,9 @@ var SqliteFindingsRepository = class {
27498
28877
  )
27499
28878
  );
27500
28879
  for (const row of sevRows) {
27501
- if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
28880
+ if (Object.hasOwn(bySeverity, row.severity)) {
28881
+ bySeverity[row.severity] = row.c;
28882
+ }
27502
28883
  }
27503
28884
  const categories = ENFORCEABLE_CATEGORIES;
27504
28885
  const enabledRows = allRows(
@@ -27547,525 +28928,6 @@ function isoDay(ms) {
27547
28928
  return new Date(ms).toISOString().slice(0, 10);
27548
28929
  }
27549
28930
 
27550
- // ../../packages/persistence/src/repositories/history-sync.ts
27551
- var STRUCTURAL_EVENT_TYPES = ["session", "llm_call", "tool_call"];
27552
- var TYPE_LIST = STRUCTURAL_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27553
- var OUTBOX_CAPTURE_EVENT_TYPES = ["prompt", "response", "tool_use"];
27554
- var CAPTURE_TYPE_LIST = OUTBOX_CAPTURE_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
27555
- var SKIPPED = -1;
27556
- var ROW_COLUMNS = `id,
27557
- parent_id AS parentId,
27558
- root_session_id AS rootSessionId,
27559
- event_type AS eventType,
27560
- host_id AS hostId,
27561
- harness_id AS harnessId,
27562
- source_project_id AS sourceProjectId,
27563
- started_at AS startedAt,
27564
- ended_at AS endedAt,
27565
- severity,
27566
- priority,
27567
- content,
27568
- content_hash AS contentHash,
27569
- attributes`;
27570
- var SqliteHistorySyncRepository = class {
27571
- constructor(db) {
27572
- this.db = db;
27573
- this.ensureRowStmt = db.prepare(`INSERT OR IGNORE INTO history_sync (id) VALUES (1)`);
27574
- this.sessionsStmt = db.prepare(
27575
- `SELECT COALESCE(root_session_id, id) AS sessionId, MIN(started_at) AS earliest
27576
- FROM audit_events
27577
- WHERE synced_at IS NULL
27578
- AND event_type IN (${TYPE_LIST})
27579
- AND started_at < :before
27580
- GROUP BY sessionId
27581
- ORDER BY earliest
27582
- LIMIT :limit`
27583
- );
27584
- this.rowsStmt = db.prepare(
27585
- `SELECT ${ROW_COLUMNS}
27586
- FROM audit_events
27587
- WHERE synced_at IS NULL
27588
- AND event_type IN (${TYPE_LIST})
27589
- AND started_at < :before
27590
- AND COALESCE(root_session_id, id) = :sessionId
27591
- ORDER BY (event_type = 'session') DESC, started_at
27592
- LIMIT :limit`
27593
- );
27594
- this.captureRowsStmt = db.prepare(
27595
- `SELECT ${ROW_COLUMNS}
27596
- FROM audit_events
27597
- WHERE synced_at IS NULL
27598
- AND sync_claimed_at IS NULL
27599
- AND outbox_owed = 1
27600
- AND event_type IN (${CAPTURE_TYPE_LIST})
27601
- AND started_at < :before
27602
- ORDER BY started_at
27603
- LIMIT :limit`
27604
- );
27605
- this.markOwedStmt = db.prepare(
27606
- `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27607
- );
27608
- this.markCaptureBacklogOwedStmt = db.prepare(
27609
- `UPDATE audit_events SET outbox_owed = 1
27610
- WHERE synced_at IS NULL
27611
- AND event_type IN (${CAPTURE_TYPE_LIST})
27612
- AND started_at < :before`
27613
- );
27614
- this.stampStmt = db.prepare(
27615
- `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27616
- );
27617
- this.claimRowStmt = db.prepare(
27618
- `UPDATE audit_events SET sync_claimed_at = :at WHERE id = :id AND synced_at IS NULL`
27619
- );
27620
- this.releaseRowStmt = db.prepare(
27621
- `UPDATE audit_events SET sync_claimed_at = NULL WHERE id = :id`
27622
- );
27623
- this.releaseStaleClaimsStmt = db.prepare(
27624
- `UPDATE audit_events SET sync_claimed_at = NULL
27625
- WHERE sync_claimed_at IS NOT NULL AND sync_claimed_at < :staleBefore`
27626
- );
27627
- this.partitionStmt = db.prepare(
27628
- `SELECT
27629
- SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NULL THEN 1 ELSE 0 END) AS queued,
27630
- SUM(CASE WHEN synced_at IS NULL AND sync_claimed_at IS NOT NULL THEN 1 ELSE 0 END) AS inProgress,
27631
- SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS synced,
27632
- SUM(CASE WHEN synced_at IS NOT NULL AND synced_at <= 0 THEN 1 ELSE 0 END) AS failed,
27633
- COUNT(*) AS total
27634
- FROM audit_events
27635
- WHERE event_type IN (${TYPE_LIST})`
27636
- );
27637
- this.countsStmt = db.prepare(
27638
- `SELECT
27639
- SUM(CASE WHEN synced_at IS NULL AND started_at < :before THEN 1 ELSE 0 END) AS pending,
27640
- SUM(CASE WHEN synced_at > 0 THEN 1 ELSE 0 END) AS sent,
27641
- SUM(CASE WHEN synced_at = ${String(SKIPPED)} THEN 1 ELSE 0 END) AS skipped
27642
- FROM audit_events
27643
- WHERE event_type IN (${TYPE_LIST})`
27644
- );
27645
- this.captureSkipCountStmt = db.prepare(
27646
- `SELECT COUNT(*) AS skipped
27647
- FROM audit_events
27648
- WHERE synced_at = ${String(SKIPPED)}
27649
- AND event_type IN (${CAPTURE_TYPE_LIST})`
27650
- );
27651
- this.fingerprintStmt = db.prepare(
27652
- `SELECT endpoint_fingerprint AS fingerprint, backlog_before AS backlogBefore
27653
- FROM history_sync WHERE id = 1`
27654
- );
27655
- this.setFingerprintStmt = db.prepare(
27656
- `UPDATE history_sync
27657
- SET endpoint_fingerprint = :fingerprint, backlog_before = :backlogBefore
27658
- WHERE id = 1`
27659
- );
27660
- this.disownCapturesStmt = db.prepare(
27661
- `UPDATE audit_events SET outbox_owed = NULL
27662
- WHERE outbox_owed IS NOT NULL
27663
- AND event_type IN (${CAPTURE_TYPE_LIST})
27664
- AND started_at < :attachedAt`
27665
- );
27666
- this.rearmStmt = db.prepare(
27667
- `UPDATE audit_events SET synced_at = NULL
27668
- WHERE synced_at > 0 AND event_type IN (${TYPE_LIST})`
27669
- );
27670
- this.claimStmt = db.prepare(
27671
- `UPDATE history_sync
27672
- SET owner_pid = :pid, owner_host = :host, acquired_at = :now, heartbeat_at = :now
27673
- WHERE id = 1
27674
- AND (owner_pid IS NULL
27675
- OR heartbeat_at IS NULL
27676
- OR heartbeat_at < :staleBefore
27677
- OR heartbeat_at > :now)`
27678
- );
27679
- this.heartbeatStmt = db.prepare(
27680
- `UPDATE history_sync SET heartbeat_at = :now WHERE id = 1 AND owner_pid = :pid`
27681
- );
27682
- this.releaseStmt = db.prepare(
27683
- `UPDATE history_sync
27684
- SET owner_pid = NULL, owner_host = NULL, acquired_at = NULL, heartbeat_at = NULL
27685
- WHERE id = 1 AND owner_pid = :pid`
27686
- );
27687
- this.closeWindowStmt = db.prepare(
27688
- `UPDATE audit_events SET synced_at = :at
27689
- WHERE synced_at IS NULL
27690
- AND event_type IN (${TYPE_LIST})
27691
- AND started_at >= :attachedAt`
27692
- );
27693
- this.releaseBoundaryStmt = db.prepare(
27694
- `UPDATE history_sync SET backlog_before = NULL WHERE id = 1`
27695
- );
27696
- this.freezeBoundaryStmt = db.prepare(
27697
- `UPDATE history_sync SET backlog_before = :backlogBefore WHERE id = 1`
27698
- );
27699
- this.leaseStmt = db.prepare(
27700
- `SELECT owner_pid AS ownerPid, owner_host AS ownerHost,
27701
- acquired_at AS acquiredAt, heartbeat_at AS heartbeatAt
27702
- FROM history_sync WHERE id = 1`
27703
- );
27704
- this.inspectionsStmt = db.prepare(
27705
- `SELECT d.rule_id AS ruleId,
27706
- d.name AS ruleName,
27707
- d.version AS ruleVersion,
27708
- d.category AS category,
27709
- d.severity AS severity,
27710
- f.span_start AS spanStart,
27711
- f.span_end AS spanEnd,
27712
- f.masked_match AS maskedMatch,
27713
- f.action_taken AS actionTaken,
27714
- f.confidence AS confidence
27715
- FROM inspection_findings f
27716
- JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27717
- WHERE f.audit_event_id = :auditEventId
27718
- ORDER BY f.span_start, f.id`
27719
- );
27720
- }
27721
- db;
27722
- ensureRowStmt;
27723
- sessionsStmt;
27724
- rowsStmt;
27725
- stampStmt;
27726
- countsStmt;
27727
- fingerprintStmt;
27728
- setFingerprintStmt;
27729
- rearmStmt;
27730
- claimStmt;
27731
- heartbeatStmt;
27732
- releaseStmt;
27733
- leaseStmt;
27734
- inspectionsStmt;
27735
- closeWindowStmt;
27736
- releaseBoundaryStmt;
27737
- freezeBoundaryStmt;
27738
- captureRowsStmt;
27739
- markOwedStmt;
27740
- markCaptureBacklogOwedStmt;
27741
- captureSkipCountStmt;
27742
- disownCapturesStmt;
27743
- partitionStmt;
27744
- claimRowStmt;
27745
- releaseRowStmt;
27746
- releaseStaleClaimsStmt;
27747
- /**
27748
- * The masked detections recorded against one tool call.
27749
- *
27750
- * These travel with the event because a tool call's target is not
27751
- * re-inspectable from the event alone — unlike a capture, where the text
27752
- * itself is re-scannable. What crosses is the masked match and the rule that
27753
- * produced it, never the value.
27754
- */
27755
- inspectionsFor(auditEventId) {
27756
- return allRows(this.inspectionsStmt, { auditEventId });
27757
- }
27758
- /**
27759
- * Sessions with structural rows still to send, oldest first.
27760
- *
27761
- * BOUNDED BY THE BACKLOG BOUNDARY, which is the whole correctness of this
27762
- * read. Anything recorded after the machine attached is the live forward
27763
- * path's to deliver; this drain exists for what was recorded before it, and a
27764
- * row both paths send is at best a duplicate request and at worst — for a
27765
- * session root — an overwrite of the inventory ids the live path resolved.
27766
- */
27767
- pendingSessions(limit, before) {
27768
- return allRows(this.sessionsStmt, { limit, before }).map(
27769
- (r) => r.sessionId
27770
- );
27771
- }
27772
- /** One session's undelivered structural rows within the backlog, root first. */
27773
- pendingRows(sessionId, limit, before) {
27774
- return allRows(this.rowsStmt, { sessionId, limit, before });
27775
- }
27776
- /**
27777
- * Captures this machine still owes the deployment, oldest first.
27778
- *
27779
- * Selected by the `outbox_owed` marker the attached forward path writes, not
27780
- * by a time window — see captureRowsStmt for why a window could not express
27781
- * this. `before` is the grace window that leaves a just-recorded capture to
27782
- * the live path.
27783
- */
27784
- pendingCaptureRows(limit, before) {
27785
- return allRows(this.captureRowsStmt, { limit, before });
27786
- }
27787
- /**
27788
- * Record that a capture is OWED to the deployment.
27789
- *
27790
- * Written by the attached forward path when a live send did not confirm
27791
- * delivery, and read by the drain as the whole of its eligibility test. It is
27792
- * a fact rather than an inference: the machine was attached, the send did not
27793
- * land, so the row is owed — which no time window can state, because the same
27794
- * window that holds the rows a past attachment left owed also holds every
27795
- * capture recorded while the machine was DETACHED, and those were never
27796
- * offered to anyone.
27797
- *
27798
- * Idempotent, and never un-set: `markSynced` settling the row is what takes it
27799
- * out of the drain's read.
27800
- */
27801
- markCaptureOwed(id) {
27802
- this.markOwedStmt.run({ id });
27803
- }
27804
- /**
27805
- * Mark every capture already on disk as owed, as of `before`.
27806
- *
27807
- * The consent-time backfill, called once from `aka attach` when a human
27808
- * grants existing-history consent — never from an ongoing drain pass, and
27809
- * never inferred from a boundary that could later move. `before` is the
27810
- * caller's own "now" at the moment consent was granted, so what this marks
27811
- * is exactly the backlog the consent prompt already counted, not whatever a
27812
- * later re-attach or key rotation might widen it to.
27813
- *
27814
- * Returns how many rows matched, for the caller to log or test against. Not a
27815
- * count of NEWLY marked rows — a row still unsynced from an earlier call
27816
- * matches again and is counted again, the same as `UPDATE`'s own `changes`.
27817
- */
27818
- markCaptureBacklogOwed(before) {
27819
- return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
27820
- }
27821
- /** Record delivery. Called only AFTER the far side has accepted the rows. */
27822
- markSynced(ids, atMs) {
27823
- this.stampAll(ids, atMs);
27824
- }
27825
- /**
27826
- * Record that a row will never be sent.
27827
- *
27828
- * Reserved for a local defect — a row that cannot be rebuilt into a valid
27829
- * payload. A row that merely failed to reach the deployment stays NULL, so it
27830
- * is retried; marking those would turn one outage into permanent data loss.
27831
- */
27832
- markSkipped(ids) {
27833
- this.stampAll(ids, SKIPPED);
27834
- }
27835
- eachInTransaction(ids, run) {
27836
- if (ids.length === 0) return;
27837
- withTransaction(
27838
- this.db,
27839
- () => {
27840
- for (const id of ids) run(id);
27841
- },
27842
- "IMMEDIATE"
27843
- );
27844
- }
27845
- stampAll(ids, value) {
27846
- if (ids.length === 0) return;
27847
- withTransaction(
27848
- this.db,
27849
- () => {
27850
- for (const id of ids) this.stampStmt.run({ at: value, id });
27851
- },
27852
- "IMMEDIATE"
27853
- );
27854
- }
27855
- /**
27856
- * Claim rows as in-flight.
27857
- *
27858
- * Advisory in exactly the sense the lease is: it records that a send is in
27859
- * progress so a surface can say so, and a lost claim costs a row showing as
27860
- * queued while it is actually being sent. It is not exclusion — the far side
27861
- * settles a duplicate on the row id.
27862
- */
27863
- claimRows(ids, atMs) {
27864
- this.eachInTransaction(ids, (id) => this.claimRowStmt.run({ at: atMs, id }));
27865
- }
27866
- /** Give back a claim without settling — the send failed, the row is queued again. */
27867
- releaseRows(ids) {
27868
- this.eachInTransaction(ids, (id) => this.releaseRowStmt.run({ id }));
27869
- }
27870
- /**
27871
- * Clear claims older than `staleBefore`, and report how many were cleared.
27872
- *
27873
- * A process killed between claiming and settling leaves rows claimed with
27874
- * nothing left to settle them. Without this they read as "sending" for ever.
27875
- */
27876
- releaseStaleClaims(staleBefore) {
27877
- return Number(this.releaseStaleClaimsStmt.run({ staleBefore }).changes);
27878
- }
27879
- /**
27880
- * Every tracked row in exactly one delivery state.
27881
- *
27882
- * Takes no boundary on purpose. The boundary answers "what should the drain
27883
- * pick up now", which is a different question from "what state is this row
27884
- * in" — and a machine that has never attached has no boundary to pass, so
27885
- * requiring one would force a caller to invent one and report the whole store
27886
- * as queued.
27887
- */
27888
- partition() {
27889
- const row = getRow(this.partitionStmt, {});
27890
- return {
27891
- queued: row?.queued ?? 0,
27892
- inProgress: row?.inProgress ?? 0,
27893
- synced: row?.synced ?? 0,
27894
- failed: row?.failed ?? 0,
27895
- total: row?.total ?? 0
27896
- };
27897
- }
27898
- /** `pending` counts only what is inside the backlog; sent and skipped are totals. */
27899
- counts(before) {
27900
- const row = getRow(
27901
- this.countsStmt,
27902
- { before }
27903
- );
27904
- const captures = getRow(this.captureSkipCountStmt);
27905
- return {
27906
- pending: row?.pending ?? 0,
27907
- sent: row?.sent ?? 0,
27908
- skipped: row?.skipped ?? 0,
27909
- capturesSkipped: captures?.skipped ?? 0
27910
- };
27911
- }
27912
- /**
27913
- * The deployment the current stamps were made against, and where its backlog
27914
- * ends.
27915
- *
27916
- * READ-ONLY. An absent row reads as an absent deployment, which is what a
27917
- * machine that has never drained is — and every writer below seeds the row
27918
- * before it needs one, so nothing depends on this creating it. Keeping the
27919
- * write off the gate path matters because the gate runs on every pass while a
27920
- * write has to take the database's write lock.
27921
- */
27922
- deployment() {
27923
- const row = getRow(
27924
- this.fingerprintStmt
27925
- );
27926
- return {
27927
- fingerprint: row?.fingerprint ?? void 0,
27928
- backlogBefore: row?.backlogBefore ?? void 0
27929
- };
27930
- }
27931
- /**
27932
- * Point the ledger at a different deployment, discarding what it recorded
27933
- * about the previous one.
27934
- *
27935
- * Delivery is a fact about ONE recipient: rows sent to the deployment a
27936
- * machine has just left are undelivered as far as the new one is concerned.
27937
- * All four in one transaction, so a crash between them cannot leave stamps
27938
- * attributed to the wrong deployment, a boundary that belongs to another, or
27939
- * a disown with no re-mark to follow it.
27940
- *
27941
- * The boundary is written HERE and only here, which is what freezes it: a
27942
- * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
27943
- * unchanged, so this never runs and the backlog does not widen back over rows
27944
- * the live path has since delivered.
27945
- *
27946
- * `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
27947
- * granted existing-history consent for the deployment this call is arming —
27948
- * a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
27949
- * instant, `backlogBefore` is the ATTACH instant, and the two can be far
27950
- * apart. Passed only when that grant is valid, since this method has no way
27951
- * to check consent itself and must not mark a row owed for a machine that
27952
- * never agreed to it. Applied AFTER the disown above, in the SAME
27953
- * transaction: what the disown clears is every marker below `backlogBefore`,
27954
- * which includes this deployment's OWN pre-attach rows — `aka attach` calls
27955
- * `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
27956
- * on the cleared side of that bound — and the re-mark in the same
27957
- * transaction is what puts those rows back. A crash between the two cannot
27958
- * strand the ledger disowned with nothing re-marked — the transaction either
27959
- * lands whole or not at all, and a fingerprint mismatch that has not yet
27960
- * committed re-enters this method on the very next pass. Omit it (the
27961
- * structural-only tests do) to exercise the disown in isolation.
27962
- *
27963
- * The disown is bounded by `backlogBefore`, which is what keeps it from
27964
- * touching a marker the NEW deployment's OWN live path has already set: B's
27965
- * live path can mark a capture owed from the moment `aka attach` writes the
27966
- * descriptor, before the drain's first pass ever reaches this method, and
27967
- * such a row sits at or after the bound rather than below it. What keeps the
27968
- * disown from eating THIS SAME CALL's own re-mark is the order, not the
27969
- * bound — disown runs first, re-mark second, both inside the one
27970
- * transaction above.
27971
- */
27972
- rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
27973
- this.ensureRowStmt.run();
27974
- withTransaction(
27975
- this.db,
27976
- () => {
27977
- const previous = getRow(this.fingerprintStmt)?.fingerprint;
27978
- this.rearmStmt.run();
27979
- if (previous !== null && previous !== void 0 && previous !== fingerprint) {
27980
- this.disownCapturesStmt.run({ attachedAt: backlogBefore });
27981
- }
27982
- if (backfillCapturesBefore !== void 0) {
27983
- this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
27984
- }
27985
- this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27986
- },
27987
- "IMMEDIATE"
27988
- );
27989
- }
27990
- /**
27991
- * End the attached period: hand its rows to the live path, and release the
27992
- * boundary so the next attachment can freeze a new one.
27993
- *
27994
- * WITHOUT THIS, a detach and a re-attach to the same deployment leave a window
27995
- * nothing delivers. The fingerprint is unchanged, so the boundary is never
27996
- * re-frozen and stays at the FIRST attachment — while nothing forwards at all
27997
- * during the detached period, because the machine is not attached. Rows
27998
- * recorded in that window sit after the boundary and before the re-attach, so
27999
- * neither path takes them, and the pending count reports none outstanding.
28000
- *
28001
- * Stamping the attached window is not a claim that every one of those rows
28002
- * reached the deployment — the live path drops on failure and says so
28003
- * elsewhere. It records that they were ITS to deliver, which is exactly the
28004
- * status quo: they sit outside the frozen boundary today and are equally never
28005
- * re-sent. Making it explicit is what lets the boundary move.
28006
- *
28007
- * ONE TRANSACTION, so a crash cannot release the boundary while leaving the
28008
- * window unstamped — that half-state would re-send the whole attached period
28009
- * on the next attach, which is the failure the boundary exists to prevent.
28010
- */
28011
- closeAttachedWindow(attachedAtMs, atMs) {
28012
- this.ensureRowStmt.run();
28013
- withTransaction(
28014
- this.db,
28015
- () => {
28016
- const row = getRow(this.fingerprintStmt);
28017
- const from = row?.backlogBefore ?? attachedAtMs;
28018
- this.closeWindowStmt.run({ at: atMs, attachedAt: from });
28019
- this.releaseBoundaryStmt.run();
28020
- },
28021
- "IMMEDIATE"
28022
- );
28023
- }
28024
- /**
28025
- * Freeze a boundary for the deployment already on file, KEEPING the stamps.
28026
- *
28027
- * The re-attach half of the above. Distinct from `rearmFor`, which is for a
28028
- * different deployment and therefore discards what was delivered to the old
28029
- * one: here the recipient is the same, so everything already sent to it stays
28030
- * sent.
28031
- */
28032
- freezeBoundary(backlogBefore) {
28033
- this.ensureRowStmt.run();
28034
- this.freezeBoundaryStmt.run({ backlogBefore });
28035
- }
28036
- /** Take the claim, or report that someone live already holds it. */
28037
- claim(pid, host, nowMs, staleAfterMs) {
28038
- this.ensureRowStmt.run();
28039
- let taken = false;
28040
- withTransaction(
28041
- this.db,
28042
- () => {
28043
- const result = this.claimStmt.run({
28044
- pid,
28045
- host,
28046
- now: nowMs,
28047
- staleBefore: nowMs - staleAfterMs
28048
- });
28049
- taken = result.changes === 1;
28050
- },
28051
- "IMMEDIATE"
28052
- );
28053
- return taken;
28054
- }
28055
- /** Say the holder is still alive. A no-op once the claim has moved on. */
28056
- heartbeat(pid, nowMs) {
28057
- this.heartbeatStmt.run({ now: nowMs, pid });
28058
- }
28059
- /** Give the claim up. Scoped to this holder, so a taken-over claim is left be. */
28060
- release(pid) {
28061
- this.releaseStmt.run({ pid });
28062
- }
28063
- /** Who holds the claim, if anyone. Read-only, for the same reason as above. */
28064
- lease() {
28065
- return getRow(this.leaseStmt);
28066
- }
28067
- };
28068
-
28069
28931
  // ../../packages/persistence/src/repositories/inspection-definitions.ts
28070
28932
  var SqliteInspectionDefinitionsRepository = class {
28071
28933
  constructor(db) {
@@ -28475,7 +29337,10 @@ function managedContextOf(managed) {
28475
29337
  present: true,
28476
29338
  ...managed.organization === void 0 ? {} : { organization: managed.organization },
28477
29339
  lockedFields: managed.lockedFields,
28478
- ...managed.unknownLockedFields === void 0 ? {} : { unknownLockedFields: managed.unknownLockedFields }
29340
+ // The COUNT crosses to the dashboard, not the names — see ManagedContext.
29341
+ // The names remain on `managed` for any caller that wants to list them.
29342
+ ...managed.unknownLockedFields === void 0 ? {} : { unknownLockedCount: managed.unknownLockedFields.length },
29343
+ ...managed.unknownValueFields === void 0 ? {} : { unknownValueCount: managed.unknownValueFields.length }
28479
29344
  };
28480
29345
  }
28481
29346
  function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ new Date()) {
@@ -28498,6 +29363,7 @@ function overlayManagedSettings(settings, managed, now = () => /* @__PURE__ */ n
28498
29363
  if (values.vaultInlineReveal !== void 0) merged.vaultInlineReveal = values.vaultInlineReveal;
28499
29364
  if (values.dataSharesInPlace !== void 0) merged.dataSharesInPlace = values.dataSharesInPlace;
28500
29365
  if (values.redactFallback !== void 0) merged.redactFallback = values.redactFallback;
29366
+ if (values.bodyRetention !== void 0) merged.bodyRetention = values.bodyRetention;
28501
29367
  if (values.vaultConsent !== void 0) {
28502
29368
  merged.vaultConsent = values.vaultConsent ? (
28503
29369
  // Keep an existing valid grant so its acknowledgedAt survives; mint one
@@ -28550,6 +29416,9 @@ function lockableKeysTouched(current, applied) {
28550
29416
  if (changed("vaultInlineReveal")) keys.push("vaultInlineReveal");
28551
29417
  if (changed("dataSharesInPlace")) keys.push("dataSharesInPlace");
28552
29418
  if (changed("redactFallback")) keys.push("redactFallback");
29419
+ const appliedRetention = applied.bodyRetention;
29420
+ const retentionChanged = appliedRetention !== void 0 && (appliedRetention.enabled !== current.bodyRetention.enabled || appliedRetention.retainDays !== current.bodyRetention.retainDays);
29421
+ if (retentionChanged) keys.push("bodyRetention");
28553
29422
  if ("vaultConsent" in applied && isVaultConsentValid(applied.vaultConsent) !== isVaultConsentValid(current.vaultConsent)) {
28554
29423
  keys.push("vaultConsent");
28555
29424
  }
@@ -28570,6 +29439,7 @@ function pinnedKeys(managed) {
28570
29439
  if (values.modelJudgeConsent !== void 0) keys.push("modelJudgeConsent");
28571
29440
  if (values.dataSharesInPlace !== void 0) keys.push("dataSharesInPlace");
28572
29441
  if (values.redactFallback !== void 0) keys.push("redactFallback");
29442
+ if (values.bodyRetention !== void 0) keys.push("bodyRetention");
28573
29443
  return keys;
28574
29444
  }
28575
29445
  function withoutManagedKeys(applied, managed, pinned, touched) {
@@ -28587,6 +29457,7 @@ function withoutManagedKeys(applied, managed, pinned, touched) {
28587
29457
  if (strip("modelJudgeConsent")) delete out.modelJudgeConsent;
28588
29458
  if (strip("dataSharesInPlace")) delete out.dataSharesInPlace;
28589
29459
  if (strip("redactFallback")) delete out.redactFallback;
29460
+ if (strip("bodyRetention")) delete out.bodyRetention;
28590
29461
  return out;
28591
29462
  }
28592
29463
  function applyOnboarding(answers2, base = defaultDataDir(), managedOverride) {
@@ -31095,7 +31966,7 @@ var SqliteSecurityRepository = class {
31095
31966
  ELSE 0
31096
31967
  END) AS open_at_rest
31097
31968
  FROM inspection_findings f
31098
- JOIN audit_events e ON e.id = f.audit_event_id
31969
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31099
31970
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31100
31971
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
31101
31972
  ON latest.finding_key = f.finding_key
@@ -31321,7 +32192,7 @@ var SqliteSecurityRepository = class {
31321
32192
  this.db.prepare(
31322
32193
  `SELECT e.repo AS repo, count(*) AS c
31323
32194
  FROM inspection_findings f
31324
- JOIN audit_events e ON e.id = f.audit_event_id
32195
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31325
32196
  WHERE e.started_at >= :from AND e.started_at < :to
31326
32197
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
31327
32198
  AND e.repo IS NOT NULL
@@ -31445,7 +32316,7 @@ var SqliteSecurityRepository = class {
31445
32316
  d.severity AS severity,
31446
32317
  COUNT(*) AS count
31447
32318
  FROM inspection_findings f
31448
- JOIN audit_events e ON e.id = f.audit_event_id
32319
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31449
32320
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31450
32321
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
31451
32322
  ON latest.finding_key = f.finding_key
@@ -31480,7 +32351,7 @@ var SqliteSecurityRepository = class {
31480
32351
  `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
31481
32352
  d.rule_id AS rule_id, d.category AS category
31482
32353
  FROM inspection_findings f
31483
- JOIN audit_events e ON e.id = f.audit_event_id
32354
+ JOIN audit_events e INDEXED BY idx_audit_capture_rollup ON e.id = f.audit_event_id
31484
32355
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31485
32356
  WHERE e.started_at >= :from AND e.started_at < :to
31486
32357
  AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
@@ -32321,6 +33192,7 @@ function openWithPragmas(file2) {
32321
33192
  db.exec("PRAGMA journal_mode = WAL");
32322
33193
  db.exec("PRAGMA busy_timeout = 2000");
32323
33194
  db.exec("PRAGMA foreign_keys = ON");
33195
+ registerSqlFunctions(db);
32324
33196
  } catch (err) {
32325
33197
  closeQuietly(db);
32326
33198
  throw err;
@@ -32350,7 +33222,7 @@ function backupLegacyStore(db, file2) {
32350
33222
  discardStore(file2, backup);
32351
33223
  return backup;
32352
33224
  }
32353
- function openAndInitialize(file2, base) {
33225
+ function openAndInitialize(file2, base, skipTags) {
32354
33226
  let db = openWithPragmas(file2);
32355
33227
  try {
32356
33228
  if (isForeignSqliteLineage(db)) {
@@ -32360,7 +33232,7 @@ function openAndInitialize(file2, base) {
32360
33232
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
32361
33233
  );
32362
33234
  }
32363
- applyMigrations(db, file2);
33235
+ applyMigrations(db, file2, { skipTags });
32364
33236
  tightenPerms(file2);
32365
33237
  const policies = new SqlitePoliciesRepository(db);
32366
33238
  const installedPacks = new SqliteInstalledPacksRepository(db, base);
@@ -32375,6 +33247,7 @@ function openAndInitialize(file2, base) {
32375
33247
  exceptions: new SqliteExceptionsRepository(db),
32376
33248
  resolutions: new SqliteResolutionsRepository(db),
32377
33249
  ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
33250
+ bodyRetention: new SqliteBodyRetentionRepository(db),
32378
33251
  security: new SqliteSecurityRepository(db),
32379
33252
  detections: new SqliteDetectionsRepository(db),
32380
33253
  shares: new SqliteSharesRepository(db),
@@ -32385,6 +33258,7 @@ function openAndInitialize(file2, base) {
32385
33258
  activity: new SqliteActivityRepository(db),
32386
33259
  sourceProject: new SqliteSourceProjectRepository(db),
32387
33260
  auditEvents: new SqliteAuditEventsRepository(db),
33261
+ captureStatus: new SqliteCaptureStatusRepository(db),
32388
33262
  classifiedData: new SqliteClassifiedDataRepository(db),
32389
33263
  inspectionDefinitions: new SqliteInspectionDefinitionsRepository(db),
32390
33264
  inspectionFindings: new SqliteInspectionFindingsRepository(db),
@@ -32397,7 +33271,8 @@ function openAndInitialize(file2, base) {
32397
33271
  throw err;
32398
33272
  }
32399
33273
  }
32400
- function openLocalDatabase(dir) {
33274
+ var DEFERRED_TAGS = new Set(DEFERRED_MIGRATION_TAGS);
33275
+ function openLocalDatabase(dir, options = {}) {
32401
33276
  ensureDataDirSync(dir);
32402
33277
  const file2 = join7(dir, DB_FILENAME);
32403
33278
  reapStalePartials(file2);
@@ -32409,6 +33284,7 @@ function openLocalDatabase(dir) {
32409
33284
  installedPacks,
32410
33285
  scanLedger,
32411
33286
  historySync,
33287
+ bodyRetention,
32412
33288
  secretVault,
32413
33289
  exceptions,
32414
33290
  resolutions,
@@ -32423,6 +33299,7 @@ function openLocalDatabase(dir) {
32423
33299
  activity,
32424
33300
  sourceProject,
32425
33301
  auditEvents,
33302
+ captureStatus,
32426
33303
  classifiedData,
32427
33304
  inspectionDefinitions,
32428
33305
  inspectionFindings,
@@ -32432,7 +33309,8 @@ function openLocalDatabase(dir) {
32432
33309
  // `dir` is always `<base>/data` — every caller resolves it through
32433
33310
  // `dataDir()` — so its parent is the `~/.aka` base the layout splits into
32434
33311
  // settings/ and data/, and the pack-policy floor needs both halves.
32435
- dirname2(dir)
33312
+ dirname2(dir),
33313
+ options.applyDeferredMigrations === true ? void 0 : DEFERRED_TAGS
32436
33314
  );
32437
33315
  function captureRowId(event) {
32438
33316
  return captureId(
@@ -32625,6 +33503,7 @@ function openLocalDatabase(dir) {
32625
33503
  installedPacks,
32626
33504
  scanLedger,
32627
33505
  historySync,
33506
+ bodyRetention,
32628
33507
  secretVault,
32629
33508
  exceptions,
32630
33509
  resolutions,
@@ -32638,6 +33517,7 @@ function openLocalDatabase(dir) {
32638
33517
  activity,
32639
33518
  sourceProject,
32640
33519
  auditEvents,
33520
+ captureStatus,
32641
33521
  classifiedData,
32642
33522
  inspectionDefinitions,
32643
33523
  inspectionFindings,
@@ -32665,6 +33545,7 @@ function openLocalDatabase(dir) {
32665
33545
 
32666
33546
  // ../../packages/persistence/src/egress-wire.ts
32667
33547
  import { createHash as createHash3 } from "crypto";
33548
+ var SLASH = "/".charCodeAt(0);
32668
33549
 
32669
33550
  // ../../packages/persistence/src/finding-key.ts
32670
33551
  import { createHash as createHash4 } from "crypto";
@@ -32675,18 +33556,26 @@ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
32675
33556
  import { join as join8 } from "path";
32676
33557
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
32677
33558
 
33559
+ // ../../packages/persistence/src/forward-health.ts
33560
+ import { readFileSync as readFileSync7 } from "fs";
33561
+ import { join as join9 } from "path";
33562
+
32678
33563
  // ../../packages/persistence/src/history-backfill.ts
32679
33564
  import { existsSync as existsSync4 } from "fs";
32680
- import { join as join9 } from "path";
33565
+ import { join as join10 } from "path";
32681
33566
 
32682
33567
  // ../../packages/persistence/src/history-preview.ts
32683
33568
  import { existsSync as existsSync5 } from "fs";
32684
- import { join as join10 } from "path";
33569
+ import { join as join11 } from "path";
32685
33570
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
32686
33571
 
33572
+ // ../../packages/persistence/src/history-sync-state.ts
33573
+ import { readFileSync as readFileSync8 } from "fs";
33574
+ import { join as join12 } from "path";
33575
+
32687
33576
  // ../../packages/persistence/src/store-symlinks.ts
32688
33577
  import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32689
- import { dirname as dirname3, join as join11, resolve } from "path";
33578
+ import { dirname as dirname3, join as join13, resolve } from "path";
32690
33579
 
32691
33580
  // ../../packages/persistence/src/vault/crypto.ts
32692
33581
  import {
@@ -32700,19 +33589,19 @@ import {
32700
33589
  // ../../packages/persistence/src/vault/key-provider.ts
32701
33590
  import { execFileSync } from "child_process";
32702
33591
  import { randomBytes as randomBytes2 } from "crypto";
32703
- import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32704
- import { join as join12 } from "path";
33592
+ import { chmodSync as chmodSync3, readFileSync as readFileSync9, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
33593
+ import { join as join14 } from "path";
32705
33594
 
32706
33595
  // ../../packages/persistence/src/vault/vault.ts
32707
33596
  import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
32708
33597
 
32709
33598
  // ../../packages/persistence/src/warn-era-cap.ts
32710
33599
  import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
32711
- import { join as join13 } from "path";
33600
+ import { join as join15 } from "path";
32712
33601
  var MARKER = "warn-era-capped";
32713
33602
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32714
33603
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
32715
- const marker = join13(dataDir2, MARKER);
33604
+ const marker = join15(dataDir2, MARKER);
32716
33605
  if (existsSync7(marker)) return { capped: 0, skipped: "already-run" };
32717
33606
  const capped = db.policies.capCategoryActions();
32718
33607
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
@@ -32722,7 +33611,7 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32722
33611
 
32723
33612
  // ../../packages/plugin-sdk/src/config.ts
32724
33613
  import { existsSync as existsSync8 } from "fs";
32725
- import { join as join14 } from "path";
33614
+ import { join as join16 } from "path";
32726
33615
 
32727
33616
  // ../../packages/plugin-sdk/src/provider-env.ts
32728
33617
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -32776,7 +33665,7 @@ function resolveProvider() {
32776
33665
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
32777
33666
  try {
32778
33667
  ensureLayoutDirSync(base);
32779
- const settingsFile = join14(settingsDir(base), "settings.json");
33668
+ const settingsFile = join16(settingsDir(base), "settings.json");
32780
33669
  if (existsSync8(settingsFile)) tightenFile(settingsFile);
32781
33670
  } catch {
32782
33671
  }
@@ -32800,9 +33689,9 @@ function resolveProviderSafe(resolveProviderFn) {
32800
33689
  }
32801
33690
 
32802
33691
  // ../../packages/plugin-sdk/src/config-inventory.ts
32803
- import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
33692
+ import { readdirSync as readdirSync2, readFileSync as readFileSync11, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32804
33693
  import { homedir as homedir2 } from "os";
32805
- import { basename as basename3, join as join16 } from "path";
33694
+ import { basename as basename3, join as join18 } from "path";
32806
33695
 
32807
33696
  // ../../packages/detections/src/egress/registry.ts
32808
33697
  var EXTRACTOR_VERSION = "1";
@@ -33531,6 +34420,56 @@ var CONFIG_POSTURE_RULES = [
33531
34420
  }
33532
34421
  ];
33533
34422
 
34423
+ // ../../packages/detections/src/posture/web-capture-posture.ts
34424
+ var RULE_VERSION2 = "1";
34425
+ var DRIFT_MIN_PARSE_FAILURES = 2;
34426
+ var WEB_CAPTURE_DRIFT_STATES = /* @__PURE__ */ new Set([
34427
+ "blind",
34428
+ "degraded"
34429
+ ]);
34430
+ var WEB_CAPTURE_DRIFT_RULE = {
34431
+ ruleId: "web-capture-drift",
34432
+ version: RULE_VERSION2,
34433
+ name: "Web chat capture is not reading the site",
34434
+ category: "config",
34435
+ severity: "medium",
34436
+ definition: JSON.stringify({
34437
+ kind: "web-capture-drift",
34438
+ states: [...WEB_CAPTURE_DRIFT_STATES],
34439
+ minParseFailures: DRIFT_MIN_PARSE_FAILURES
34440
+ })
34441
+ };
34442
+ var STATIC_COPY = {
34443
+ active: { headline: "turns are being observed on this site" },
34444
+ unreported: {
34445
+ // Says "recently" rather than "yet": the store read is bounded to
34446
+ // CAPTURE_STATUS_RECENCY_MS, so this state covers a site nothing has ever
34447
+ // reported for AND one whose last report has aged out. The two are the
34448
+ // same fact to a reader — nobody has confirmed anything lately — and the
34449
+ // copy may not claim the stronger of them.
34450
+ headline: `no report in the last ${String(CAPTURE_STATUS_RECENCY_DAYS)} days \u2014 open the site in Chrome with the extension loaded`
34451
+ },
34452
+ standby: {
34453
+ headline: "this build declares no endpoints for the site, so nothing is observed yet"
34454
+ },
34455
+ unpatched: {
34456
+ // Says what the flags say and no more. `patched` is false both for a tap
34457
+ // that installed and hooked neither transport and for one that never ran
34458
+ // at all — a page reports the same status either way, so the copy may not
34459
+ // assert one of them.
34460
+ headline: "the page tap captured neither fetch nor XHR \u2014 it may not have installed; reload the extension at chrome://extensions"
34461
+ },
34462
+ idle: { headline: "watching; no turn has been observed yet" },
34463
+ blind: {
34464
+ headline: "messages were sent in the page that the network capture never saw",
34465
+ remediation: "reload the tab. If it persists after `aka update` and reloading the extension at chrome://extensions, the site's send path has changed and needs a new extension build."
34466
+ },
34467
+ degraded: {
34468
+ headline: "the site's payloads no longer carry the fields the extension reads",
34469
+ remediation: "run `aka update`, then reload the extension at chrome://extensions. If it stays degraded after an update, the site's contract has changed and needs a new extension build."
34470
+ }
34471
+ };
34472
+
33534
34473
  // ../../packages/detections/src/security/redos-probe.ts
33535
34474
  var BUDGET_MS = 100;
33536
34475
  var EXPONENTIAL_UNITS = [
@@ -33560,8 +34499,8 @@ var CPU_CORROBORATION_SHARE = 0.2;
33560
34499
  var CORROBORATION_FLOOR_MS = BUDGET_MS * CPU_CORROBORATION_SHARE;
33561
34500
 
33562
34501
  // ../../packages/plugin-sdk/src/repo.ts
33563
- import { existsSync as existsSync9, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
33564
- import { basename as basename2, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
34502
+ import { existsSync as existsSync9, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
34503
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join17, sep as sep2 } from "path";
33565
34504
 
33566
34505
  // ../../packages/plugin-sdk/src/events.ts
33567
34506
  import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
@@ -33572,8 +34511,8 @@ import { fileURLToPath } from "url";
33572
34511
  import { Worker } from "worker_threads";
33573
34512
 
33574
34513
  // ../../packages/plugin-sdk/src/host-floor.ts
33575
- import { readFileSync as readFileSync11 } from "fs";
33576
- import { join as join18 } from "path";
34514
+ import { readFileSync as readFileSync13 } from "fs";
34515
+ import { join as join20 } from "path";
33577
34516
 
33578
34517
  // ../../packages/plugin-sdk/src/model-governance.ts
33579
34518
  import {
@@ -33581,11 +34520,11 @@ import {
33581
34520
  fstatSync,
33582
34521
  mkdirSync as mkdirSync2,
33583
34522
  openSync as openSync2,
33584
- readFileSync as readFileSync10,
34523
+ readFileSync as readFileSync12,
33585
34524
  readSync,
33586
34525
  writeFileSync as writeFileSync5
33587
34526
  } from "fs";
33588
- import { join as join17 } from "path";
34527
+ import { join as join19 } from "path";
33589
34528
  var TAIL_BYTES = 256 * 1024;
33590
34529
 
33591
34530
  // ../../packages/plugin-sdk/src/host-floor.ts
@@ -33608,15 +34547,15 @@ var HOST_FLOORS = {
33608
34547
 
33609
34548
  // ../../packages/plugin-sdk/src/ignore-layers.ts
33610
34549
  var import_ignore = __toESM(require_ignore(), 1);
33611
- import { readFileSync as readFileSync12 } from "fs";
33612
- import { join as join19 } from "path";
34550
+ import { readFileSync as readFileSync14 } from "fs";
34551
+ import { join as join21 } from "path";
33613
34552
 
33614
34553
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
33615
34554
  import { arch, hostname as hostname4, platform, release as release2 } from "os";
33616
34555
 
33617
34556
  // ../../packages/plugin-sdk/src/nudge.ts
33618
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
33619
- import { join as join20 } from "path";
34557
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync15, writeFileSync as writeFileSync6 } from "fs";
34558
+ import { join as join22 } from "path";
33620
34559
 
33621
34560
  // ../../packages/plugin-sdk/src/paths.ts
33622
34561
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
@@ -33634,7 +34573,7 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
33634
34573
 
33635
34574
  // ../../packages/plugin-sdk/src/project-files.ts
33636
34575
  import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
33637
- import { basename as basename5, join as join21 } from "path";
34576
+ import { basename as basename5, join as join23 } from "path";
33638
34577
 
33639
34578
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
33640
34579
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -33670,7 +34609,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
33670
34609
 
33671
34610
  // ../../packages/plugin-sdk/src/throttle.ts
33672
34611
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
33673
- import { join as join22 } from "path";
34612
+ import { join as join24 } from "path";
33674
34613
 
33675
34614
  // ../../packages/setup-wizard/src/onboard-posture.ts
33676
34615
  function parsePosture(json2) {
@@ -33693,7 +34632,7 @@ function parsePosture(json2) {
33693
34632
 
33694
34633
  // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
33695
34634
  import { writeFileSync as writeFileSync8 } from "fs";
33696
- import { join as join23 } from "path";
34635
+ import { join as join25 } from "path";
33697
34636
 
33698
34637
  // ../../packages/setup-wizard/src/triage/merge.ts
33699
34638
  var RANK = Object.fromEntries(
@@ -33701,9 +34640,9 @@ var RANK = Object.fromEntries(
33701
34640
  );
33702
34641
 
33703
34642
  // ../../packages/setup-wizard/src/triage/plan-file.ts
33704
- import { mkdtempSync, readFileSync as readFileSync14, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
34643
+ import { mkdtempSync, readFileSync as readFileSync16, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
33705
34644
  import { tmpdir } from "os";
33706
- import { basename as basename6, dirname as dirname6, join as join24 } from "path";
34645
+ import { basename as basename6, dirname as dirname6, join as join26 } from "path";
33707
34646
  var SuppressionEntrySchema = external_exports.object({
33708
34647
  ruleId: external_exports.string(),
33709
34648
  category: DetectionCategory,