@agenr/agenr-plugin 2.0.1 → 2.1.0

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.
@@ -1,14 +1,27 @@
1
1
  import {
2
+ DEFAULT_CROSS_ENCODER_ALPHA,
3
+ DEFAULT_CROSS_ENCODER_TOP_K,
4
+ DEFAULT_MMR_LAMBDA,
5
+ DEFAULT_SEEDED_RERANK_WEIGHT,
6
+ DEFAULT_STRONG_SEED_SCORE_GAP,
7
+ DEFAULT_STRONG_SEED_TOP_N,
8
+ applyCrossEncoderRerank,
2
9
  buildLexicalPlan,
3
- combinedRelevance,
4
10
  computeLexicalScore,
5
11
  cosineSimilarity,
6
12
  describeClaimKeyNormalizationFailure,
13
+ maximalMarginalRelevance,
7
14
  normalizeClaimKey,
8
15
  recall,
9
16
  resolveClaimSlotPolicy,
17
+ rrfFuse,
18
+ rrfFuseVectorLexical,
19
+ seededRerank,
20
+ selectStrongSeeds,
21
+ sharesEpisodeLineage,
22
+ sharesProcedureLineage,
10
23
  tokenize
11
- } from "./chunk-MEHOGUZE.js";
24
+ } from "./chunk-6T5RXGIR.js";
12
25
 
13
26
  // src/adapters/db/client.ts
14
27
  import fs from "fs/promises";
@@ -1476,14 +1489,14 @@ function normalizeProcedureSources(value, label, filePath, options = {}) {
1476
1489
  function normalizeProcedureSource(record, label, filePath) {
1477
1490
  rejectUnexpectedProcedureFields(record, SOURCE_KEYS, label, filePath);
1478
1491
  const kind = readProcedureSourceKind(record.kind, `${label}.kind`, filePath, PROCEDURE_SOURCE_KINDS);
1479
- const path3 = readOptionalProcedureString(record.path, `${label}.path`, filePath);
1492
+ const path4 = readOptionalProcedureString(record.path, `${label}.path`, filePath);
1480
1493
  const locator = readOptionalProcedureString(record.locator, `${label}.locator`, filePath);
1481
1494
  const sourceLabel = readOptionalProcedureString(record.label, `${label}.label`, filePath);
1482
1495
  switch (kind) {
1483
1496
  case "skill":
1484
1497
  case "doc":
1485
1498
  case "repo_file":
1486
- if (!path3) {
1499
+ if (!path4) {
1487
1500
  throw new Error(`Invalid procedure ${filePath}: ${label}.${kind} sources require a path.`);
1488
1501
  }
1489
1502
  break;
@@ -1501,7 +1514,7 @@ function normalizeProcedureSource(record, label, filePath) {
1501
1514
  }
1502
1515
  return {
1503
1516
  kind,
1504
- ...path3 ? { path: path3 } : {},
1517
+ ...path4 ? { path: path4 } : {},
1505
1518
  ...locator ? { locator } : {},
1506
1519
  ...sourceLabel ? { label: sourceLabel } : {}
1507
1520
  };
@@ -3518,8 +3531,8 @@ import { fileURLToPath } from "url";
3518
3531
  function isRecord2(value) {
3519
3532
  return typeof value === "object" && value !== null && !Array.isArray(value);
3520
3533
  }
3521
- function pushIssue(issues, path3, message) {
3522
- issues.push({ path: path3, message });
3534
+ function pushIssue(issues, path4, message) {
3535
+ issues.push({ path: path4, message });
3523
3536
  }
3524
3537
  function pushUnexpectedFields(value, allowedKeys, basePath, issues) {
3525
3538
  for (const key of Object.keys(value)) {
@@ -3529,68 +3542,68 @@ function pushUnexpectedFields(value, allowedKeys, basePath, issues) {
3529
3542
  pushIssue(issues, joinPath(basePath, key), "Unexpected field.");
3530
3543
  }
3531
3544
  }
3532
- function parseRequiredTrimmedString(value, path3, issues, message = "Expected a non-empty string.") {
3545
+ function parseRequiredTrimmedString(value, path4, issues, message = "Expected a non-empty string.") {
3533
3546
  if (typeof value !== "string") {
3534
- pushIssue(issues, path3, message);
3547
+ pushIssue(issues, path4, message);
3535
3548
  return void 0;
3536
3549
  }
3537
3550
  const normalized = value.trim();
3538
3551
  if (normalized.length === 0) {
3539
- pushIssue(issues, path3, message);
3552
+ pushIssue(issues, path4, message);
3540
3553
  return void 0;
3541
3554
  }
3542
3555
  return normalized;
3543
3556
  }
3544
- function parseOptionalTrimmedString(value, path3, issues, typeMessage = "Expected a string.", emptyMessage = "Expected a non-empty string.") {
3557
+ function parseOptionalTrimmedString(value, path4, issues, typeMessage = "Expected a string.", emptyMessage = "Expected a non-empty string.") {
3545
3558
  if (value === void 0) {
3546
3559
  return void 0;
3547
3560
  }
3548
3561
  if (typeof value !== "string") {
3549
- pushIssue(issues, path3, typeMessage);
3562
+ pushIssue(issues, path4, typeMessage);
3550
3563
  return void 0;
3551
3564
  }
3552
3565
  const normalized = value.trim();
3553
3566
  if (normalized.length === 0) {
3554
- pushIssue(issues, path3, emptyMessage);
3567
+ pushIssue(issues, path4, emptyMessage);
3555
3568
  return void 0;
3556
3569
  }
3557
3570
  return normalized;
3558
3571
  }
3559
- function parseOptionalBoolean(value, path3, issues, message = "Expected a boolean.") {
3572
+ function parseOptionalBoolean(value, path4, issues, message = "Expected a boolean.") {
3560
3573
  if (value === void 0) {
3561
3574
  return void 0;
3562
3575
  }
3563
3576
  if (typeof value !== "boolean") {
3564
- pushIssue(issues, path3, message);
3577
+ pushIssue(issues, path4, message);
3565
3578
  return void 0;
3566
3579
  }
3567
3580
  return value;
3568
3581
  }
3569
- function parseOptionalIntegerInRange(value, path3, issues, bounds) {
3582
+ function parseOptionalIntegerInRange(value, path4, issues, bounds) {
3570
3583
  if (value === void 0) {
3571
3584
  return void 0;
3572
3585
  }
3573
3586
  if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
3574
- pushIssue(issues, path3, integerRangeMessage(bounds));
3587
+ pushIssue(issues, path4, integerRangeMessage(bounds));
3575
3588
  return void 0;
3576
3589
  }
3577
3590
  if (bounds.min !== void 0 && value < bounds.min) {
3578
- pushIssue(issues, path3, integerRangeMessage(bounds));
3591
+ pushIssue(issues, path4, integerRangeMessage(bounds));
3579
3592
  return void 0;
3580
3593
  }
3581
3594
  if (bounds.max !== void 0 && value > bounds.max) {
3582
- pushIssue(issues, path3, integerRangeMessage(bounds));
3595
+ pushIssue(issues, path4, integerRangeMessage(bounds));
3583
3596
  return void 0;
3584
3597
  }
3585
3598
  return value;
3586
3599
  }
3587
- function parseOptionalTimestampString(value, path3, issues, message = "Expected a valid timestamp string.") {
3588
- const timestamp = parseOptionalTrimmedString(value, path3, issues);
3600
+ function parseOptionalTimestampString(value, path4, issues, message = "Expected a valid timestamp string.") {
3601
+ const timestamp = parseOptionalTrimmedString(value, path4, issues);
3589
3602
  if (timestamp === void 0) {
3590
3603
  return void 0;
3591
3604
  }
3592
3605
  if (Number.isNaN(Date.parse(timestamp))) {
3593
- pushIssue(issues, path3, message);
3606
+ pushIssue(issues, path4, message);
3594
3607
  return void 0;
3595
3608
  }
3596
3609
  return timestamp;
@@ -3716,7 +3729,8 @@ function toAgenrConfigInput(config, options = {}) {
3716
3729
  ...config.extractionContext ? { extractionContext: config.extractionContext } : {},
3717
3730
  ...hasModelConfig(config.extractionModel) ? { extractionModel: config.extractionModel } : {},
3718
3731
  ...hasModelConfig(config.dedupModel) ? { dedupModel: config.dedupModel } : {},
3719
- ...hasModelConfig(config.episodeModel) ? { episodeModel: config.episodeModel } : {}
3732
+ ...hasModelConfig(config.episodeModel) ? { episodeModel: config.episodeModel } : {},
3733
+ ...hasModelConfig(config.crossEncoderModel) ? { crossEncoderModel: config.crossEncoderModel } : {}
3720
3734
  };
3721
3735
  const claimExtraction = toClaimExtractionInput(config.claimExtraction);
3722
3736
  if (claimExtraction) {
@@ -3766,6 +3780,7 @@ function normalizeAgenrConfig(value, options) {
3766
3780
  const extractionModel = parseModelConfig(value.extractionModel, "extractionModel", issues);
3767
3781
  const dedupModel = parseModelConfig(value.dedupModel, "dedupModel", issues);
3768
3782
  const episodeModel = parseModelConfig(value.episodeModel, "episodeModel", issues);
3783
+ const crossEncoderModel = parseModelConfig(value.crossEncoderModel, "crossEncoderModel", issues);
3769
3784
  const claimExtraction = parseClaimExtractionConfig(value.claimExtraction, "claimExtraction", issues);
3770
3785
  const surgeon = parseSurgeonConfig(value.surgeon, "surgeon", issues);
3771
3786
  const dbPath = parseOptionalTrimmedString(value.dbPath, "dbPath", issues);
@@ -3792,6 +3807,7 @@ function normalizeAgenrConfig(value, options) {
3792
3807
  ...extractionModel ? { extractionModel } : {},
3793
3808
  ...dedupModel ? { dedupModel } : {},
3794
3809
  ...episodeModel ? { episodeModel } : {},
3810
+ ...crossEncoderModel ? { crossEncoderModel } : {},
3795
3811
  ...claimExtraction.input ? { claimExtraction: claimExtraction.input } : {},
3796
3812
  ...surgeon.input ? { surgeon: surgeon.input } : {},
3797
3813
  ...dbPath ? { dbPath } : {},
@@ -3810,6 +3826,7 @@ function normalizeAgenrConfig(value, options) {
3810
3826
  ...extractionModel ? { extractionModel } : {},
3811
3827
  ...dedupModel ? { dedupModel } : {},
3812
3828
  ...episodeModel ? { episodeModel } : {},
3829
+ ...crossEncoderModel ? { crossEncoderModel } : {},
3813
3830
  claimExtraction: claimExtraction.resolved,
3814
3831
  surgeon: surgeon.resolved,
3815
3832
  dbPath: dbPath ?? options.defaultDbPath,
@@ -3828,6 +3845,7 @@ function pushTopLevelIssues(value, issues) {
3828
3845
  "extractionModel",
3829
3846
  "dedupModel",
3830
3847
  "episodeModel",
3848
+ "crossEncoderModel",
3831
3849
  "claimExtraction",
3832
3850
  "surgeon",
3833
3851
  "dbPath",
@@ -3843,41 +3861,41 @@ function pushTopLevelIssues(value, issues) {
3843
3861
  pushIssue(issues, "embeddingApiKey", "Removed field. Move this value to credentials.openaiApiKey, then delete embeddingApiKey.");
3844
3862
  }
3845
3863
  }
3846
- function parseAuth(value, path3, issues) {
3847
- const normalized = parseOptionalTrimmedString(value, path3, issues);
3864
+ function parseAuth(value, path4, issues) {
3865
+ const normalized = parseOptionalTrimmedString(value, path4, issues);
3848
3866
  if (!normalized) {
3849
3867
  return void 0;
3850
3868
  }
3851
3869
  if (!isAgenrAuthMethod(normalized)) {
3852
- pushIssue(issues, path3, "Expected a supported auth method.");
3870
+ pushIssue(issues, path4, "Expected a supported auth method.");
3853
3871
  return void 0;
3854
3872
  }
3855
3873
  return normalized;
3856
3874
  }
3857
- function parseProvider(value, path3, issues) {
3858
- const normalized = parseOptionalTrimmedString(value, path3, issues);
3875
+ function parseProvider(value, path4, issues) {
3876
+ const normalized = parseOptionalTrimmedString(value, path4, issues);
3859
3877
  if (!normalized) {
3860
3878
  return void 0;
3861
3879
  }
3862
3880
  if (!isAgenrProvider(normalized)) {
3863
- pushIssue(issues, path3, "Expected a supported provider.");
3881
+ pushIssue(issues, path4, "Expected a supported provider.");
3864
3882
  return void 0;
3865
3883
  }
3866
3884
  return normalized;
3867
3885
  }
3868
- function parseCredentials(value, path3, issues) {
3886
+ function parseCredentials(value, path4, issues) {
3869
3887
  if (value === void 0) {
3870
3888
  return void 0;
3871
3889
  }
3872
3890
  if (!isRecord2(value)) {
3873
- pushIssue(issues, path3, "Expected an object.");
3891
+ pushIssue(issues, path4, "Expected an object.");
3874
3892
  return void 0;
3875
3893
  }
3876
3894
  const startIndex = issues.length;
3877
- pushUnexpectedFields(value, /* @__PURE__ */ new Set(["openaiApiKey", "anthropicApiKey", "anthropicOauthToken"]), path3, issues);
3878
- const openaiApiKey = parseOptionalTrimmedString(value.openaiApiKey, `${path3}.openaiApiKey`, issues);
3879
- const anthropicApiKey = parseOptionalTrimmedString(value.anthropicApiKey, `${path3}.anthropicApiKey`, issues);
3880
- const anthropicOauthToken = parseOptionalTrimmedString(value.anthropicOauthToken, `${path3}.anthropicOauthToken`, issues);
3895
+ pushUnexpectedFields(value, /* @__PURE__ */ new Set(["openaiApiKey", "anthropicApiKey", "anthropicOauthToken"]), path4, issues);
3896
+ const openaiApiKey = parseOptionalTrimmedString(value.openaiApiKey, `${path4}.openaiApiKey`, issues);
3897
+ const anthropicApiKey = parseOptionalTrimmedString(value.anthropicApiKey, `${path4}.anthropicApiKey`, issues);
3898
+ const anthropicOauthToken = parseOptionalTrimmedString(value.anthropicOauthToken, `${path4}.anthropicOauthToken`, issues);
3881
3899
  if (issues.length > startIndex) {
3882
3900
  return void 0;
3883
3901
  }
@@ -3888,20 +3906,20 @@ function parseCredentials(value, path3, issues) {
3888
3906
  };
3889
3907
  return hasStoredCredentials(credentials) ? credentials : void 0;
3890
3908
  }
3891
- function parseModelConfig(value, path3, issues) {
3909
+ function parseModelConfig(value, path4, issues) {
3892
3910
  if (value === void 0) {
3893
3911
  return void 0;
3894
3912
  }
3895
3913
  if (!isRecord2(value)) {
3896
- pushIssue(issues, path3, "Expected an object.");
3914
+ pushIssue(issues, path4, "Expected an object.");
3897
3915
  return void 0;
3898
3916
  }
3899
3917
  const startIndex = issues.length;
3900
- pushUnexpectedFields(value, /* @__PURE__ */ new Set(["provider", "model"]), path3, issues);
3901
- const provider = parseProvider(value.provider, `${path3}.provider`, issues);
3902
- const model = parseOptionalTrimmedString(value.model, `${path3}.model`, issues);
3918
+ pushUnexpectedFields(value, /* @__PURE__ */ new Set(["provider", "model"]), path4, issues);
3919
+ const provider = parseProvider(value.provider, `${path4}.provider`, issues);
3920
+ const model = parseOptionalTrimmedString(value.model, `${path4}.model`, issues);
3903
3921
  if (!provider && !model) {
3904
- pushIssue(issues, path3, "Expected at least one of provider or model.");
3922
+ pushIssue(issues, path4, "Expected at least one of provider or model.");
3905
3923
  }
3906
3924
  if (issues.length > startIndex) {
3907
3925
  return void 0;
@@ -3911,7 +3929,7 @@ function parseModelConfig(value, path3, issues) {
3911
3929
  ...model ? { model } : {}
3912
3930
  };
3913
3931
  }
3914
- function parseClaimExtractionConfig(value, path3, issues) {
3932
+ function parseClaimExtractionConfig(value, path4, issues) {
3915
3933
  const defaults = createDefaultClaimExtractionConfig();
3916
3934
  if (value === void 0) {
3917
3935
  return {
@@ -3919,20 +3937,20 @@ function parseClaimExtractionConfig(value, path3, issues) {
3919
3937
  };
3920
3938
  }
3921
3939
  if (!isRecord2(value)) {
3922
- pushIssue(issues, path3, "Expected an object.");
3940
+ pushIssue(issues, path4, "Expected an object.");
3923
3941
  return {
3924
3942
  resolved: defaults
3925
3943
  };
3926
3944
  }
3927
3945
  const startIndex = issues.length;
3928
- pushUnexpectedFields(value, /* @__PURE__ */ new Set(["enabled", "confidenceThreshold", "eligibleTypes", "concurrency", "model"]), path3, issues);
3929
- const enabled = parseOptionalBoolean(value.enabled, `${path3}.enabled`, issues);
3930
- const confidenceThreshold = parseOptionalUnitInterval(value.confidenceThreshold, `${path3}.confidenceThreshold`, issues);
3931
- const eligibleTypes = parseEligibleTypes(value.eligibleTypes, `${path3}.eligibleTypes`, issues);
3932
- const concurrency = parseOptionalIntegerInRange(value.concurrency, `${path3}.concurrency`, issues, {
3946
+ pushUnexpectedFields(value, /* @__PURE__ */ new Set(["enabled", "confidenceThreshold", "eligibleTypes", "concurrency", "model"]), path4, issues);
3947
+ const enabled = parseOptionalBoolean(value.enabled, `${path4}.enabled`, issues);
3948
+ const confidenceThreshold = parseOptionalUnitInterval(value.confidenceThreshold, `${path4}.confidenceThreshold`, issues);
3949
+ const eligibleTypes = parseEligibleTypes(value.eligibleTypes, `${path4}.eligibleTypes`, issues);
3950
+ const concurrency = parseOptionalIntegerInRange(value.concurrency, `${path4}.concurrency`, issues, {
3933
3951
  min: 1
3934
3952
  });
3935
- const model = parseModelConfig(value.model, `${path3}.model`, issues);
3953
+ const model = parseModelConfig(value.model, `${path4}.model`, issues);
3936
3954
  if (issues.length > startIndex) {
3937
3955
  return {
3938
3956
  resolved: defaults
@@ -3956,7 +3974,7 @@ function parseClaimExtractionConfig(value, path3, issues) {
3956
3974
  }
3957
3975
  };
3958
3976
  }
3959
- function parseSurgeonConfig(value, path3, issues) {
3977
+ function parseSurgeonConfig(value, path4, issues) {
3960
3978
  const defaults = createDefaultSurgeonConfig();
3961
3979
  if (value === void 0) {
3962
3980
  return {
@@ -3964,19 +3982,19 @@ function parseSurgeonConfig(value, path3, issues) {
3964
3982
  };
3965
3983
  }
3966
3984
  if (!isRecord2(value)) {
3967
- pushIssue(issues, path3, "Expected an object.");
3985
+ pushIssue(issues, path4, "Expected an object.");
3968
3986
  return {
3969
3987
  resolved: defaults
3970
3988
  };
3971
3989
  }
3972
3990
  const startIndex = issues.length;
3973
- pushUnexpectedFields(value, /* @__PURE__ */ new Set(["model", "costCap", "dailyCostCap", "contextLimit", "customInstructions", "passes"]), path3, issues);
3974
- const model = parseModelConfig(value.model, `${path3}.model`, issues);
3975
- const costCap = parseOptionalPositiveNumber(value.costCap, `${path3}.costCap`, issues);
3976
- const dailyCostCap = parseOptionalNonNegativeNumber(value.dailyCostCap, `${path3}.dailyCostCap`, issues);
3977
- const contextLimit = parseOptionalIntegerInRange(value.contextLimit, `${path3}.contextLimit`, issues, { min: 0 });
3978
- const customInstructions = parseOptionalTrimmedString(value.customInstructions, `${path3}.customInstructions`, issues);
3979
- const retirement = parseRetirementPassConfig(value.passes, `${path3}.passes`, issues);
3991
+ pushUnexpectedFields(value, /* @__PURE__ */ new Set(["model", "costCap", "dailyCostCap", "contextLimit", "customInstructions", "passes"]), path4, issues);
3992
+ const model = parseModelConfig(value.model, `${path4}.model`, issues);
3993
+ const costCap = parseOptionalPositiveNumber(value.costCap, `${path4}.costCap`, issues);
3994
+ const dailyCostCap = parseOptionalNonNegativeNumber(value.dailyCostCap, `${path4}.dailyCostCap`, issues);
3995
+ const contextLimit = parseOptionalIntegerInRange(value.contextLimit, `${path4}.contextLimit`, issues, { min: 0 });
3996
+ const customInstructions = parseOptionalTrimmedString(value.customInstructions, `${path4}.customInstructions`, issues);
3997
+ const retirement = parseRetirementPassConfig(value.passes, `${path4}.passes`, issues);
3980
3998
  if (issues.length > startIndex) {
3981
3999
  return {
3982
4000
  resolved: defaults
@@ -4008,7 +4026,7 @@ function parseSurgeonConfig(value, path3, issues) {
4008
4026
  }
4009
4027
  };
4010
4028
  }
4011
- function parseRetirementPassConfig(value, path3, issues) {
4029
+ function parseRetirementPassConfig(value, path4, issues) {
4012
4030
  const defaults = createDefaultRetirementPassConfig();
4013
4031
  if (value === void 0) {
4014
4032
  return {
@@ -4016,36 +4034,36 @@ function parseRetirementPassConfig(value, path3, issues) {
4016
4034
  };
4017
4035
  }
4018
4036
  if (!isRecord2(value)) {
4019
- pushIssue(issues, path3, "Expected an object.");
4037
+ pushIssue(issues, path4, "Expected an object.");
4020
4038
  return {
4021
4039
  resolved: defaults
4022
4040
  };
4023
4041
  }
4024
4042
  const startIndex = issues.length;
4025
- pushUnexpectedFields(value, /* @__PURE__ */ new Set(["retirement"]), path3, issues);
4043
+ pushUnexpectedFields(value, /* @__PURE__ */ new Set(["retirement"]), path4, issues);
4026
4044
  const retirement = value.retirement;
4027
4045
  if (retirement === void 0) {
4028
4046
  if (issues.length === startIndex) {
4029
- pushIssue(issues, path3, "Expected a retirement config when passes is provided.");
4047
+ pushIssue(issues, path4, "Expected a retirement config when passes is provided.");
4030
4048
  }
4031
4049
  return {
4032
4050
  resolved: defaults
4033
4051
  };
4034
4052
  }
4035
4053
  if (!isRecord2(retirement)) {
4036
- pushIssue(issues, `${path3}.retirement`, "Expected an object.");
4054
+ pushIssue(issues, `${path4}.retirement`, "Expected an object.");
4037
4055
  return {
4038
4056
  resolved: defaults
4039
4057
  };
4040
4058
  }
4041
- pushUnexpectedFields(retirement, /* @__PURE__ */ new Set(["protectRecalledDays", "protectMinImportance", "skipRecentlyEvaluatedDays"]), `${path3}.retirement`, issues);
4042
- const protectRecalledDays = parseOptionalIntegerInRange(retirement.protectRecalledDays, `${path3}.retirement.protectRecalledDays`, issues, {
4059
+ pushUnexpectedFields(retirement, /* @__PURE__ */ new Set(["protectRecalledDays", "protectMinImportance", "skipRecentlyEvaluatedDays"]), `${path4}.retirement`, issues);
4060
+ const protectRecalledDays = parseOptionalIntegerInRange(retirement.protectRecalledDays, `${path4}.retirement.protectRecalledDays`, issues, {
4043
4061
  min: 0
4044
4062
  });
4045
- const protectMinImportance = parseOptionalIntegerInRange(retirement.protectMinImportance, `${path3}.retirement.protectMinImportance`, issues, {
4063
+ const protectMinImportance = parseOptionalIntegerInRange(retirement.protectMinImportance, `${path4}.retirement.protectMinImportance`, issues, {
4046
4064
  min: 0
4047
4065
  });
4048
- const skipRecentlyEvaluatedDays = parseOptionalIntegerInRange(retirement.skipRecentlyEvaluatedDays, `${path3}.retirement.skipRecentlyEvaluatedDays`, issues, {
4066
+ const skipRecentlyEvaluatedDays = parseOptionalIntegerInRange(retirement.skipRecentlyEvaluatedDays, `${path4}.retirement.skipRecentlyEvaluatedDays`, issues, {
4049
4067
  min: 0
4050
4068
  });
4051
4069
  if (issues.length > startIndex) {
@@ -4067,54 +4085,54 @@ function parseRetirementPassConfig(value, path3, issues) {
4067
4085
  }
4068
4086
  };
4069
4087
  }
4070
- function parseOptionalUnitInterval(value, path3, issues) {
4088
+ function parseOptionalUnitInterval(value, path4, issues) {
4071
4089
  if (value === void 0) {
4072
4090
  return void 0;
4073
4091
  }
4074
4092
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
4075
- pushIssue(issues, path3, "Expected a number from 0 to 1.");
4093
+ pushIssue(issues, path4, "Expected a number from 0 to 1.");
4076
4094
  return void 0;
4077
4095
  }
4078
4096
  return value;
4079
4097
  }
4080
- function parseOptionalPositiveNumber(value, path3, issues) {
4098
+ function parseOptionalPositiveNumber(value, path4, issues) {
4081
4099
  if (value === void 0) {
4082
4100
  return void 0;
4083
4101
  }
4084
4102
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
4085
- pushIssue(issues, path3, "Expected a positive number.");
4103
+ pushIssue(issues, path4, "Expected a positive number.");
4086
4104
  return void 0;
4087
4105
  }
4088
4106
  return value;
4089
4107
  }
4090
- function parseOptionalNonNegativeNumber(value, path3, issues) {
4108
+ function parseOptionalNonNegativeNumber(value, path4, issues) {
4091
4109
  if (value === void 0) {
4092
4110
  return void 0;
4093
4111
  }
4094
4112
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
4095
- pushIssue(issues, path3, "Expected a non-negative number.");
4113
+ pushIssue(issues, path4, "Expected a non-negative number.");
4096
4114
  return void 0;
4097
4115
  }
4098
4116
  return value;
4099
4117
  }
4100
- function parseEligibleTypes(value, path3, issues) {
4118
+ function parseEligibleTypes(value, path4, issues) {
4101
4119
  if (value === void 0) {
4102
4120
  return void 0;
4103
4121
  }
4104
4122
  if (!Array.isArray(value)) {
4105
- pushIssue(issues, path3, "Expected an array of entry types.");
4123
+ pushIssue(issues, path4, "Expected an array of entry types.");
4106
4124
  return void 0;
4107
4125
  }
4108
4126
  const normalized = [];
4109
4127
  const seen = /* @__PURE__ */ new Set();
4110
4128
  for (const [index, item] of value.entries()) {
4111
4129
  if (typeof item !== "string") {
4112
- pushIssue(issues, `${path3}.${index}`, "Expected a supported entry type.");
4130
+ pushIssue(issues, `${path4}.${index}`, "Expected a supported entry type.");
4113
4131
  continue;
4114
4132
  }
4115
4133
  const trimmed = item.trim();
4116
4134
  if (!isEntryType(trimmed)) {
4117
- pushIssue(issues, `${path3}.${index}`, "Expected a supported entry type.");
4135
+ pushIssue(issues, `${path4}.${index}`, "Expected a supported entry type.");
4118
4136
  continue;
4119
4137
  }
4120
4138
  if (!seen.has(trimmed)) {
@@ -4123,7 +4141,7 @@ function parseEligibleTypes(value, path3, issues) {
4123
4141
  }
4124
4142
  }
4125
4143
  if (normalized.length === 0) {
4126
- pushIssue(issues, path3, "Expected at least one supported entry type.");
4144
+ pushIssue(issues, path4, "Expected at least one supported entry type.");
4127
4145
  return void 0;
4128
4146
  }
4129
4147
  return normalized;
@@ -4526,6 +4544,604 @@ async function sleep(durationMs) {
4526
4544
  });
4527
4545
  }
4528
4546
 
4547
+ // src/adapters/llm.ts
4548
+ import { createHash as createHash2 } from "crypto";
4549
+ import fs3 from "fs";
4550
+ import os2 from "os";
4551
+ import path3 from "path";
4552
+ import { createRequire } from "module";
4553
+ import { completeSimple, getEnvApiKey, getModel } from "@mariozechner/pi-ai";
4554
+ var DEFAULT_REASONING = "medium";
4555
+ var require2 = createRequire(import.meta.url);
4556
+ var getModelWithStrings = getModel;
4557
+ function probeLlmCredentials(params) {
4558
+ const candidate = resolveCredentialCandidate(params);
4559
+ if (!candidate) {
4560
+ return {
4561
+ available: false,
4562
+ guidance: credentialSetupGuidance(params.auth)
4563
+ };
4564
+ }
4565
+ return {
4566
+ available: true,
4567
+ source: candidate.source,
4568
+ guidance: "Credentials available.",
4569
+ credentials: {
4570
+ apiKey: candidate.token,
4571
+ source: candidate.source,
4572
+ auth: params.auth
4573
+ }
4574
+ };
4575
+ }
4576
+ function resolveAuthCredentials(params) {
4577
+ const probe = probeLlmCredentials(params);
4578
+ if (!probe.available || !probe.credentials) {
4579
+ throw new Error(probe.guidance);
4580
+ }
4581
+ return probe.credentials;
4582
+ }
4583
+ function createLlmClient(provider, modelId, options = {}) {
4584
+ const model = getModelWithStrings(provider, modelId);
4585
+ const metadata = {
4586
+ model,
4587
+ contextWindowTokens: model.contextWindow,
4588
+ maxOutputTokens: model.maxTokens,
4589
+ supportsReasoning: model.reasoning,
4590
+ usage: createEmptyUsageStats()
4591
+ };
4592
+ const resolvedApiKey = normalizeOptionalString6(options.apiKey);
4593
+ const requestCompletion = async (systemPrompt, userMessage) => {
4594
+ const response = await completeSimple(
4595
+ model,
4596
+ {
4597
+ systemPrompt,
4598
+ messages: [
4599
+ {
4600
+ role: "user",
4601
+ content: userMessage,
4602
+ timestamp: Date.now()
4603
+ }
4604
+ ]
4605
+ },
4606
+ {
4607
+ apiKey: resolvedApiKey,
4608
+ reasoning: metadata.supportsReasoning ? options.reasoning ?? DEFAULT_REASONING : void 0
4609
+ }
4610
+ );
4611
+ accumulateUsage(metadata.usage, response.usage);
4612
+ if (response.stopReason === "error") {
4613
+ throw new Error(response.errorMessage ?? `LLM completion failed for ${provider}/${modelId}.`);
4614
+ }
4615
+ return response;
4616
+ };
4617
+ const complete = async (systemPrompt, userMessage) => {
4618
+ const response = await requestCompletion(systemPrompt, userMessage);
4619
+ return extractText(response);
4620
+ };
4621
+ return {
4622
+ metadata,
4623
+ complete,
4624
+ completeJson: async (systemPrompt, userMessage) => {
4625
+ const response = await requestCompletion(systemPrompt, userMessage);
4626
+ const text = extractText(response);
4627
+ return JSON.parse(stripCodeFence(text));
4628
+ }
4629
+ };
4630
+ }
4631
+ function resolveModel(config, stage) {
4632
+ const override = resolveStageOverride(config, stage);
4633
+ return {
4634
+ provider: normalizeOptionalString6(override?.provider) ?? resolveStageDefaultProvider(config, stage),
4635
+ modelId: normalizeOptionalString6(override?.model) ?? resolveStageDefaultModel(config, stage)
4636
+ };
4637
+ }
4638
+ function resolveStageOverride(config, stage) {
4639
+ switch (stage) {
4640
+ case "extraction":
4641
+ return config?.extractionModel;
4642
+ case "dedup":
4643
+ return config?.dedupModel;
4644
+ case "episode":
4645
+ return config?.episodeModel;
4646
+ case "claim":
4647
+ return config?.claimExtraction?.model ?? config?.extractionModel;
4648
+ case "cross_encoder":
4649
+ return config?.crossEncoderModel;
4650
+ }
4651
+ }
4652
+ function resolveStageDefaultProvider(config, stage) {
4653
+ if (stage === "cross_encoder") {
4654
+ const topLevel = normalizeOptionalString6(config?.provider);
4655
+ if (!topLevel || topLevel === "openai-codex") {
4656
+ return "openai";
4657
+ }
4658
+ return topLevel;
4659
+ }
4660
+ return normalizeOptionalString6(config?.provider) ?? "openai";
4661
+ }
4662
+ function resolveStageDefaultModel(config, stage) {
4663
+ if (stage === "cross_encoder") {
4664
+ return defaultModelForStage(stage);
4665
+ }
4666
+ return normalizeOptionalString6(config?.model) ?? defaultModelForStage(stage);
4667
+ }
4668
+ function resolveLlmCredentials(config, provider, env = process.env) {
4669
+ const normalizedProvider = normalizeOptionalString6(provider);
4670
+ if (!normalizedProvider) {
4671
+ throw new Error("Provider is required to resolve LLM credentials.");
4672
+ }
4673
+ const auth = normalizeAuthMethod(config?.auth);
4674
+ if (auth && authMethodToProvider(auth) === normalizedProvider) {
4675
+ return resolveAuthCredentials({
4676
+ auth,
4677
+ storedCredentials: config?.credentials,
4678
+ env
4679
+ });
4680
+ }
4681
+ const fallback = resolveProviderCredentialCandidate(config, normalizedProvider, env);
4682
+ if (fallback) {
4683
+ return {
4684
+ apiKey: fallback.token,
4685
+ source: fallback.source
4686
+ };
4687
+ }
4688
+ if (normalizedProvider === "openai-codex") {
4689
+ throw new Error("No OpenAI subscription credential found. Run `codex auth` or configure `auth` as `openai-api-key`.");
4690
+ }
4691
+ const exampleEnv = normalizedProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
4692
+ throw new Error(`No credential found for provider "${normalizedProvider}". Set the appropriate auth method in config or provide ${exampleEnv}.`);
4693
+ }
4694
+ function resolveLlmApiKey(config, provider, env = process.env) {
4695
+ return resolveLlmCredentials(config, provider, env).apiKey;
4696
+ }
4697
+ function stripCodeFence(text) {
4698
+ const trimmed = text.trim();
4699
+ const match = /^```(?:json)?\s*([\s\S]+?)\s*```$/i.exec(trimmed);
4700
+ return match?.[1]?.trim() ?? trimmed;
4701
+ }
4702
+ function defaultModelForStage(stage) {
4703
+ switch (stage) {
4704
+ case "extraction":
4705
+ case "episode":
4706
+ case "claim":
4707
+ return "gpt-5.4-mini";
4708
+ case "dedup":
4709
+ case "cross_encoder":
4710
+ return "gpt-5.4-nano";
4711
+ }
4712
+ }
4713
+ function normalizeOptionalString6(value) {
4714
+ const trimmed = value?.trim();
4715
+ return trimmed && trimmed.length > 0 ? trimmed : void 0;
4716
+ }
4717
+ function normalizeAuthMethod(value) {
4718
+ const normalized = normalizeOptionalString6(value);
4719
+ return normalized && isAgenrAuthMethod(normalized) ? normalized : void 0;
4720
+ }
4721
+ function safeReadJson(filePath) {
4722
+ try {
4723
+ const raw = fs3.readFileSync(filePath, "utf8");
4724
+ return JSON.parse(raw);
4725
+ } catch {
4726
+ return null;
4727
+ }
4728
+ }
4729
+ function resolveHomeDir(env) {
4730
+ const home = normalizeOptionalString6(env.HOME);
4731
+ return home ? resolveUserPath(home) : os2.homedir();
4732
+ }
4733
+ function resolveCodexHome(env) {
4734
+ const configured = normalizeOptionalString6(env.CODEX_HOME) ?? "~/.codex";
4735
+ const resolved = resolveUserPath(configured);
4736
+ try {
4737
+ return fs3.realpathSync.native(resolved);
4738
+ } catch {
4739
+ return resolved;
4740
+ }
4741
+ }
4742
+ function resolveUserPath(value) {
4743
+ const trimmed = value.trim();
4744
+ if (trimmed === "~") {
4745
+ return os2.homedir();
4746
+ }
4747
+ if (trimmed.startsWith("~/")) {
4748
+ return path3.join(os2.homedir(), trimmed.slice(2));
4749
+ }
4750
+ if (trimmed.startsWith("~\\")) {
4751
+ return path3.join(os2.homedir(), trimmed.slice(2));
4752
+ }
4753
+ return path3.resolve(trimmed);
4754
+ }
4755
+ function parseCodexFromFile(env) {
4756
+ const authPath = path3.join(resolveCodexHome(env), "auth.json");
4757
+ const parsed = safeReadJson(authPath);
4758
+ if (!parsed || typeof parsed !== "object") {
4759
+ return null;
4760
+ }
4761
+ const record = parsed;
4762
+ const tokens = record.tokens;
4763
+ const accessToken = tokens?.access_token;
4764
+ if (typeof accessToken !== "string" || !accessToken.trim()) {
4765
+ return null;
4766
+ }
4767
+ return {
4768
+ token: accessToken.trim(),
4769
+ source: `file:${authPath}`
4770
+ };
4771
+ }
4772
+ function resolveCodexKeychainAccount(env) {
4773
+ const hash = createHash2("sha256").update(resolveCodexHome(env)).digest("hex");
4774
+ return `cli|${hash.slice(0, 16)}`;
4775
+ }
4776
+ function parseCodexFromKeychain(env) {
4777
+ if (process.platform !== "darwin") {
4778
+ return null;
4779
+ }
4780
+ try {
4781
+ const account = resolveCodexKeychainAccount(env);
4782
+ const { execSync } = require2("node:child_process");
4783
+ const raw = execSync(`security find-generic-password -s "Codex Auth" -a "${account}" -w`, {
4784
+ encoding: "utf8",
4785
+ stdio: ["pipe", "pipe", "pipe"],
4786
+ timeout: 5e3
4787
+ }).trim();
4788
+ const parsed = JSON.parse(raw);
4789
+ const tokens = parsed.tokens;
4790
+ const accessToken = tokens?.access_token;
4791
+ if (typeof accessToken !== "string" || !accessToken.trim()) {
4792
+ return null;
4793
+ }
4794
+ return {
4795
+ token: accessToken.trim(),
4796
+ source: "keychain:Codex Auth"
4797
+ };
4798
+ } catch {
4799
+ return null;
4800
+ }
4801
+ }
4802
+ function parseClaudeCredentialRecord(parsed, source) {
4803
+ if (!parsed || typeof parsed !== "object") {
4804
+ return null;
4805
+ }
4806
+ const record = parsed;
4807
+ const claudeOauth = record.claudeAiOauth;
4808
+ const accessToken = claudeOauth?.accessToken;
4809
+ if (typeof accessToken !== "string" || !accessToken.trim()) {
4810
+ return null;
4811
+ }
4812
+ return {
4813
+ token: accessToken.trim(),
4814
+ source
4815
+ };
4816
+ }
4817
+ function parseClaudeFromFiles(env) {
4818
+ const homeDir = resolveHomeDir(env);
4819
+ const candidates = [path3.join(homeDir, ".claude", ".credentials.json"), path3.join(homeDir, ".claude", "credentials.json")];
4820
+ for (const candidate of candidates) {
4821
+ const parsed = safeReadJson(candidate);
4822
+ const resolved = parseClaudeCredentialRecord(parsed, `file:${candidate}`);
4823
+ if (resolved) {
4824
+ return resolved;
4825
+ }
4826
+ }
4827
+ return null;
4828
+ }
4829
+ function parseClaudeFromKeychain() {
4830
+ if (process.platform !== "darwin") {
4831
+ return null;
4832
+ }
4833
+ try {
4834
+ const { execSync } = require2("node:child_process");
4835
+ const raw = execSync('security find-generic-password -s "Claude Code-credentials" -w', {
4836
+ encoding: "utf8",
4837
+ stdio: ["pipe", "pipe", "pipe"],
4838
+ timeout: 5e3
4839
+ }).trim();
4840
+ return parseClaudeCredentialRecord(JSON.parse(raw), "keychain:Claude Code-credentials");
4841
+ } catch {
4842
+ return null;
4843
+ }
4844
+ }
4845
+ function candidateFromToken(token, source) {
4846
+ const normalized = normalizeOptionalString6(token);
4847
+ if (!normalized) {
4848
+ return null;
4849
+ }
4850
+ return {
4851
+ token: normalized,
4852
+ source
4853
+ };
4854
+ }
4855
+ function resolveOpenAIApiKeyCandidate(storedCredentials, env) {
4856
+ return candidateFromToken(env.OPENAI_API_KEY, "env:OPENAI_API_KEY") ?? candidateFromToken(storedCredentials?.openaiApiKey, "config:credentials.openaiApiKey");
4857
+ }
4858
+ function resolveAnthropicApiKeyCandidate(storedCredentials, env) {
4859
+ return candidateFromToken(env.ANTHROPIC_API_KEY, "env:ANTHROPIC_API_KEY") ?? candidateFromToken(storedCredentials?.anthropicApiKey, "config:credentials.anthropicApiKey");
4860
+ }
4861
+ function resolveAnthropicTokenCandidate(storedCredentials, env) {
4862
+ return candidateFromToken(env.ANTHROPIC_OAUTH_TOKEN, "env:ANTHROPIC_OAUTH_TOKEN") ?? candidateFromToken(storedCredentials?.anthropicOauthToken, "config:credentials.anthropicOauthToken");
4863
+ }
4864
+ function resolveAnthropicOauthCandidate(env) {
4865
+ return parseClaudeFromFiles(env) ?? parseClaudeFromKeychain();
4866
+ }
4867
+ function resolveOpenAiSubscriptionCandidate(env) {
4868
+ return parseCodexFromFile(env) ?? parseCodexFromKeychain(env);
4869
+ }
4870
+ function credentialSetupGuidance(auth) {
4871
+ switch (auth) {
4872
+ case "anthropic-oauth":
4873
+ return "Claude Code credentials not found. Install Claude Code CLI and sign in with `claude`.";
4874
+ case "anthropic-token":
4875
+ return "No Anthropic long-lived token found. Set ANTHROPIC_OAUTH_TOKEN or save credentials.anthropicOauthToken.";
4876
+ case "anthropic-api-key":
4877
+ return "No Anthropic API key found. Set ANTHROPIC_API_KEY or save credentials.anthropicApiKey.";
4878
+ case "openai-subscription":
4879
+ return "Codex CLI credentials not found or expired. Run `codex auth`.";
4880
+ case "openai-api-key":
4881
+ return "No OpenAI API key found. Set OPENAI_API_KEY or save credentials.openaiApiKey.";
4882
+ }
4883
+ }
4884
+ function resolveCredentialCandidate(params) {
4885
+ const env = params.env ?? process.env;
4886
+ switch (params.auth) {
4887
+ case "anthropic-oauth":
4888
+ return resolveAnthropicOauthCandidate(env);
4889
+ case "anthropic-token":
4890
+ return resolveAnthropicTokenCandidate(params.storedCredentials, env);
4891
+ case "anthropic-api-key":
4892
+ return resolveAnthropicApiKeyCandidate(params.storedCredentials, env);
4893
+ case "openai-subscription":
4894
+ return resolveOpenAiSubscriptionCandidate(env);
4895
+ case "openai-api-key":
4896
+ return resolveOpenAIApiKeyCandidate(params.storedCredentials, env);
4897
+ }
4898
+ }
4899
+ function resolveProviderCredentialCandidate(config, provider, env) {
4900
+ if (provider === "openai") {
4901
+ return resolveOpenAIApiKeyCandidate(config?.credentials, env);
4902
+ }
4903
+ if (provider === "anthropic") {
4904
+ const auth = normalizeAuthMethod(config?.auth);
4905
+ if (auth && authMethodToProvider(auth) === "anthropic") {
4906
+ return resolveCredentialCandidate({
4907
+ auth,
4908
+ storedCredentials: config?.credentials,
4909
+ env
4910
+ });
4911
+ }
4912
+ return resolveAnthropicApiKeyCandidate(config?.credentials, env);
4913
+ }
4914
+ const envApiKey = getEnvApiKey(provider) ?? getEnvApiKey(provider);
4915
+ return candidateFromToken(envApiKey, `env:${provider}`);
4916
+ }
4917
+ function createEmptyUsageStats() {
4918
+ return {
4919
+ calls: 0,
4920
+ inputTokens: 0,
4921
+ outputTokens: 0,
4922
+ cacheReadTokens: 0,
4923
+ cacheWriteTokens: 0,
4924
+ totalTokens: 0,
4925
+ totalCost: 0
4926
+ };
4927
+ }
4928
+ function accumulateUsage(target, usage) {
4929
+ target.calls += 1;
4930
+ target.inputTokens += usage.input;
4931
+ target.outputTokens += usage.output;
4932
+ target.cacheReadTokens += usage.cacheRead;
4933
+ target.cacheWriteTokens += usage.cacheWrite;
4934
+ target.totalTokens += usage.totalTokens;
4935
+ target.totalCost += usage.cost.total;
4936
+ }
4937
+ function extractText(response) {
4938
+ const blocks = [];
4939
+ for (const contentBlock of response.content) {
4940
+ if (contentBlock.type === "text") {
4941
+ blocks.push(contentBlock.text);
4942
+ }
4943
+ }
4944
+ return blocks.join("");
4945
+ }
4946
+
4947
+ // src/adapters/cross-encoder/openai-cross-encoder.ts
4948
+ var OPENAI_CHAT_COMPLETIONS_URL = "https://api.openai.com/v1/chat/completions";
4949
+ var DEFAULT_MODEL = "gpt-5.4-nano";
4950
+ var DEFAULT_MAX_CONCURRENCY = 4;
4951
+ var DEFAULT_MAX_RETRIES = 2;
4952
+ var REQUEST_TIMEOUT_MS = 2e4;
4953
+ var SYSTEM_PROMPT = "You are an expert tasked with determining whether the passage is relevant to the query";
4954
+ function createOpenAICrossEncoder(options) {
4955
+ const apiKey = options.apiKey.trim();
4956
+ if (apiKey.length === 0) {
4957
+ throw new Error("OpenAI cross-encoder adapter requires a non-empty API key.");
4958
+ }
4959
+ const model = normalizeOptional(options.model) ?? DEFAULT_MODEL;
4960
+ const baseUrl = normalizeOptional(options.baseUrl) ?? OPENAI_CHAT_COMPLETIONS_URL;
4961
+ const maxConcurrency = clampPositiveInteger(options.maxConcurrency, DEFAULT_MAX_CONCURRENCY);
4962
+ const maxRetries = clampPositiveInteger(options.maxRetries, DEFAULT_MAX_RETRIES, { allowZero: true });
4963
+ const requestTimeoutMs = clampPositiveInteger(options.requestTimeoutMs, REQUEST_TIMEOUT_MS);
4964
+ const fetchImpl = options.fetchImpl ?? fetch;
4965
+ return {
4966
+ async rank(query, passages) {
4967
+ if (passages.length === 0) {
4968
+ return [];
4969
+ }
4970
+ const trimmedQuery = query.trim();
4971
+ if (trimmedQuery.length === 0) {
4972
+ return [];
4973
+ }
4974
+ const results = new Array(passages.length).fill(null);
4975
+ let nextIndex = 0;
4976
+ const workerCount = Math.max(1, Math.min(maxConcurrency, passages.length));
4977
+ await Promise.all(
4978
+ Array.from({ length: workerCount }, async () => {
4979
+ while (true) {
4980
+ const index = nextIndex;
4981
+ nextIndex += 1;
4982
+ if (index >= passages.length) {
4983
+ return;
4984
+ }
4985
+ const passage = passages[index];
4986
+ const score = await rankSinglePassage({
4987
+ apiKey,
4988
+ baseUrl,
4989
+ model,
4990
+ query: trimmedQuery,
4991
+ passage,
4992
+ fetchImpl,
4993
+ maxRetries,
4994
+ requestTimeoutMs
4995
+ });
4996
+ if (score !== null) {
4997
+ results[index] = { id: passage.id, score };
4998
+ }
4999
+ }
5000
+ })
5001
+ );
5002
+ return results.filter((result) => result !== null);
5003
+ }
5004
+ };
5005
+ }
5006
+ function resolveCrossEncoderApiKey(config) {
5007
+ const candidates = [config?.credentials?.openaiApiKey, process.env.OPENAI_API_KEY];
5008
+ for (const candidate of candidates) {
5009
+ const normalized = candidate?.trim();
5010
+ if (normalized && normalized.length > 0) {
5011
+ return normalized;
5012
+ }
5013
+ }
5014
+ throw new Error("Cross-encoder API key is required. Set config.credentials.openaiApiKey or OPENAI_API_KEY.");
5015
+ }
5016
+ async function rankSinglePassage(params) {
5017
+ const body = JSON.stringify({
5018
+ model: params.model,
5019
+ temperature: 0,
5020
+ // gpt-5.4 chat completions reject `max_tokens=1` and may return an
5021
+ // empty completion when constrained below 4 completion tokens.
5022
+ max_completion_tokens: 4,
5023
+ logprobs: true,
5024
+ top_logprobs: 2,
5025
+ messages: [
5026
+ {
5027
+ role: "system",
5028
+ content: SYSTEM_PROMPT
5029
+ },
5030
+ {
5031
+ role: "user",
5032
+ content: buildUserPrompt(params.query, params.passage.text)
5033
+ }
5034
+ ]
5035
+ });
5036
+ let attempt = 0;
5037
+ while (true) {
5038
+ const outcome = await performSingleRequest({
5039
+ ...params,
5040
+ body
5041
+ });
5042
+ if (outcome.kind === "score") {
5043
+ return outcome.value;
5044
+ }
5045
+ if (outcome.kind === "fatal" || attempt >= params.maxRetries) {
5046
+ return null;
5047
+ }
5048
+ attempt += 1;
5049
+ await sleep2(backoffMs2(attempt));
5050
+ }
5051
+ }
5052
+ async function performSingleRequest(params) {
5053
+ const controller = new AbortController();
5054
+ const timeout = setTimeout(() => controller.abort(), params.requestTimeoutMs);
5055
+ try {
5056
+ const response = await params.fetchImpl(params.baseUrl, {
5057
+ method: "POST",
5058
+ headers: {
5059
+ Authorization: `Bearer ${params.apiKey}`,
5060
+ "Content-Type": "application/json"
5061
+ },
5062
+ body: params.body,
5063
+ signal: controller.signal
5064
+ });
5065
+ const rawBody = await response.text();
5066
+ if (!response.ok) {
5067
+ return isRetryableStatus2(response.status) ? { kind: "retryable" } : { kind: "fatal" };
5068
+ }
5069
+ const score = parseRelevanceScore(rawBody);
5070
+ return score === null ? { kind: "fatal" } : { kind: "score", value: score };
5071
+ } catch {
5072
+ return { kind: "retryable" };
5073
+ } finally {
5074
+ clearTimeout(timeout);
5075
+ }
5076
+ }
5077
+ function parseRelevanceScore(rawBody) {
5078
+ let parsed;
5079
+ try {
5080
+ parsed = JSON.parse(rawBody);
5081
+ } catch {
5082
+ return null;
5083
+ }
5084
+ const firstChoice = parsed.choices?.[0];
5085
+ const topLogprobs = firstChoice?.logprobs?.content?.[0]?.top_logprobs;
5086
+ if (!Array.isArray(topLogprobs) || topLogprobs.length === 0) {
5087
+ return null;
5088
+ }
5089
+ const top = topLogprobs[0];
5090
+ if (!top || typeof top.logprob !== "number" || !Number.isFinite(top.logprob) || typeof top.token !== "string") {
5091
+ return null;
5092
+ }
5093
+ const normalizedProb = Math.exp(top.logprob);
5094
+ const tokenFirstWord = top.token.trim().split(/\s+/)[0]?.toLowerCase();
5095
+ const score = tokenFirstWord === "true" ? normalizedProb : 1 - normalizedProb;
5096
+ if (!Number.isFinite(score)) {
5097
+ return null;
5098
+ }
5099
+ if (score <= 0) {
5100
+ return 0;
5101
+ }
5102
+ if (score >= 1) {
5103
+ return 1;
5104
+ }
5105
+ return score;
5106
+ }
5107
+ function buildUserPrompt(query, passage) {
5108
+ return `Respond with "True" if PASSAGE is relevant to QUERY and "False" otherwise.
5109
+ <PASSAGE>
5110
+ ${passage}
5111
+ </PASSAGE>
5112
+ <QUERY>
5113
+ ${query}
5114
+ </QUERY>`;
5115
+ }
5116
+ function isRetryableStatus2(status) {
5117
+ return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500 && status < 600;
5118
+ }
5119
+ function clampPositiveInteger(value, fallback, opts) {
5120
+ if (typeof value !== "number" || !Number.isFinite(value)) {
5121
+ return fallback;
5122
+ }
5123
+ const floored = Math.floor(value);
5124
+ if (floored < 0) {
5125
+ return fallback;
5126
+ }
5127
+ if (floored === 0) {
5128
+ return opts?.allowZero === true ? 0 : fallback;
5129
+ }
5130
+ return floored;
5131
+ }
5132
+ function normalizeOptional(value) {
5133
+ const trimmed = value?.trim();
5134
+ return trimmed && trimmed.length > 0 ? trimmed : void 0;
5135
+ }
5136
+ function backoffMs2(attempt) {
5137
+ return Math.min(500 * 2 ** (attempt - 1), 5e3);
5138
+ }
5139
+ async function sleep2(durationMs) {
5140
+ await new Promise((resolve) => {
5141
+ setTimeout(resolve, durationMs);
5142
+ });
5143
+ }
5144
+
4529
5145
  // src/adapters/db/recall-adapter.ts
4530
5146
  var RECALL_CANDIDATE_SELECT_COLUMNS = `
4531
5147
  e.id,
@@ -4544,8 +5160,8 @@ var RECALL_CANDIDATE_SELECT_COLUMNS = `
4544
5160
  e.created_at
4545
5161
  `;
4546
5162
  var FTS_TIERS2 = ["exact", "all_tokens", "any_tokens"];
4547
- var PREDECESSOR_EXPANSION_LIMIT_PER_SEED = 8;
4548
- var PREDECESSOR_EXPANSION_MAX_RESULTS = 40;
5163
+ var NEIGHBORHOOD_DEFAULT_BUDGET_CAP = 40;
5164
+ var NEIGHBORHOOD_PER_SEED_BUDGET = 8;
4549
5165
  function createRecallAdapter(executor, embeddingPort) {
4550
5166
  return new LibsqlRecallAdapter(executor, embeddingPort);
4551
5167
  }
@@ -4646,23 +5262,34 @@ var LibsqlRecallAdapter = class {
4646
5262
  return Array.from(matches.values()).sort((left, right) => compareFtsCandidates(left, right)).slice(0, params.limit);
4647
5263
  }
4648
5264
  /**
4649
- * Finds historical predecessors scoped to a seed set of active candidate IDs.
5265
+ * Expand a typed entry neighborhood around a seed set of candidate IDs.
4650
5266
  *
4651
- * Direct supersession links are preferred. Same-claim-key siblings are used as
4652
- * the structural lineage path, with retired same-subject entries preserved as
4653
- * a weaker fallback when explicit slot identity is unavailable.
5267
+ * Honors the requested `families` exactly. `supersession_chain` adds rows
5268
+ * that either supersede or are superseded by a seed. `claim_key_sibling`
5269
+ * adds rows sharing a claim key with any seed. `topic_family` adds rows
5270
+ * that share an exact subject with a seed and is the weakest fallback.
5271
+ * `includeRetired` is applied as a hard gate so the default ranking
5272
+ * profile never pulls retired rows into its candidate pool.
4654
5273
  */
4655
- async fetchPredecessors(params) {
4656
- const normalizedIds = normalizeStrings(params.activeEntryIds);
5274
+ async expandNeighborhood(request) {
5275
+ const normalizedIds = normalizeStrings(request.seedIds);
4657
5276
  if (normalizedIds.length === 0) {
4658
5277
  return [];
4659
5278
  }
5279
+ const families = dedupeFamilies(request.families);
5280
+ if (families.length === 0) {
5281
+ return [];
5282
+ }
5283
+ const includeRetired = request.includeRetired === true;
5284
+ const budget = normalizeNeighborhoodBudget(request.budget, normalizedIds.length);
4660
5285
  const placeholders = normalizedIds.map(() => "?").join(", ");
4661
- const expansionLimit = normalizePredecessorExpansionLimit(normalizedIds.length);
5286
+ const retiredGate = includeRetired ? "" : "AND e.retired = 0";
5287
+ const priorityExpression = buildNeighborhoodPriorityExpression(families, includeRetired);
5288
+ const membershipExpression = buildNeighborhoodMembershipExpression(families);
4662
5289
  const result = await this.executor.execute({
4663
5290
  sql: `
4664
5291
  WITH seed AS (
4665
- SELECT id, subject, claim_key
5292
+ SELECT id, subject, claim_key, superseded_by
4666
5293
  FROM entries
4667
5294
  WHERE id IN (${placeholders})
4668
5295
  ),
@@ -4676,47 +5303,26 @@ var LibsqlRecallAdapter = class {
4676
5303
  FROM seed
4677
5304
  WHERE claim_key IS NOT NULL
4678
5305
  ),
4679
- lineage AS (
5306
+ seed_supersessions AS (
5307
+ SELECT DISTINCT superseded_by AS target_id
5308
+ FROM seed
5309
+ WHERE superseded_by IS NOT NULL
5310
+ ),
5311
+ neighborhood AS (
4680
5312
  SELECT
4681
5313
  ${RECALL_CANDIDATE_SELECT_COLUMNS},
4682
- CASE
4683
- WHEN e.superseded_by IN (SELECT id FROM seed) THEN 0
4684
- WHEN e.claim_key IS NOT NULL
4685
- AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys)
4686
- AND e.claim_key_status = 'trusted'
4687
- AND (e.retired = 1 OR e.superseded_by IS NOT NULL) THEN 1
4688
- WHEN e.claim_key IS NOT NULL
4689
- AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys)
4690
- AND e.claim_key_status = 'trusted' THEN 2
4691
- WHEN e.claim_key IS NOT NULL
4692
- AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys)
4693
- AND (e.retired = 1 OR e.superseded_by IS NOT NULL) THEN 3
4694
- WHEN e.claim_key IS NOT NULL
4695
- AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys) THEN 4
4696
- WHEN e.retired = 1
4697
- AND e.subject IN (SELECT subject FROM seed_subjects) THEN 5
4698
- ELSE 6
4699
- END AS lineage_priority
5314
+ ${priorityExpression} AS family_priority
4700
5315
  FROM entries AS e
4701
5316
  WHERE e.id NOT IN (SELECT id FROM seed)
4702
- AND (
4703
- e.superseded_by IN (SELECT id FROM seed)
4704
- OR (
4705
- e.claim_key IS NOT NULL
4706
- AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys)
4707
- )
4708
- OR (
4709
- e.retired = 1
4710
- AND e.subject IN (SELECT subject FROM seed_subjects)
4711
- )
4712
- )
5317
+ ${retiredGate}
5318
+ AND (${membershipExpression})
4713
5319
  )
4714
5320
  SELECT *
4715
- FROM lineage
4716
- ORDER BY lineage_priority ASC, created_at ASC, id ASC
5321
+ FROM neighborhood
5322
+ ORDER BY family_priority ASC, created_at ASC, id ASC
4717
5323
  LIMIT ?
4718
5324
  `,
4719
- args: [...normalizedIds, expansionLimit]
5325
+ args: [...normalizedIds, budget]
4720
5326
  });
4721
5327
  return result.rows.map((row) => mapRecallCandidateRow(row));
4722
5328
  }
@@ -4844,14 +5450,101 @@ function readOptionalClaimKeyStatus(row) {
4844
5450
  }
4845
5451
  return parsed;
4846
5452
  }
4847
- function normalizePredecessorExpansionLimit(seedCount) {
4848
- return Math.min(PREDECESSOR_EXPANSION_MAX_RESULTS, seedCount * PREDECESSOR_EXPANSION_LIMIT_PER_SEED);
5453
+ function normalizeNeighborhoodBudget(requestedBudget, seedCount) {
5454
+ const perSeedCap = seedCount * NEIGHBORHOOD_PER_SEED_BUDGET;
5455
+ const safeRequested = Number.isFinite(requestedBudget) && requestedBudget > 0 ? Math.floor(requestedBudget) : perSeedCap;
5456
+ return Math.max(1, Math.min(NEIGHBORHOOD_DEFAULT_BUDGET_CAP, perSeedCap, safeRequested));
5457
+ }
5458
+ function dedupeFamilies(families) {
5459
+ return Array.from(new Set(families));
5460
+ }
5461
+ function buildNeighborhoodPriorityExpression(families, includeRetired) {
5462
+ const branches = [];
5463
+ if (families.includes("supersession_chain")) {
5464
+ branches.push(`WHEN e.superseded_by IN (SELECT id FROM seed) THEN 0`);
5465
+ branches.push(`WHEN e.id IN (SELECT target_id FROM seed_supersessions) THEN 1`);
5466
+ }
5467
+ if (families.includes("claim_key_sibling")) {
5468
+ const retiredOrReplacedGuard = includeRetired ? "(e.retired = 1 OR e.superseded_by IS NOT NULL)" : "e.superseded_by IS NOT NULL";
5469
+ branches.push(
5470
+ `WHEN e.claim_key IS NOT NULL
5471
+ AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys)
5472
+ AND e.claim_key_status = 'trusted'
5473
+ AND ${retiredOrReplacedGuard} THEN 2`
5474
+ );
5475
+ branches.push(
5476
+ `WHEN e.claim_key IS NOT NULL
5477
+ AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys)
5478
+ AND e.claim_key_status = 'trusted' THEN 3`
5479
+ );
5480
+ branches.push(
5481
+ `WHEN e.claim_key IS NOT NULL
5482
+ AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys)
5483
+ AND ${retiredOrReplacedGuard} THEN 4`
5484
+ );
5485
+ branches.push(
5486
+ `WHEN e.claim_key IS NOT NULL
5487
+ AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys) THEN 5`
5488
+ );
5489
+ }
5490
+ if (families.includes("topic_family")) {
5491
+ if (includeRetired) {
5492
+ branches.push(`WHEN e.retired = 1 AND e.subject IN (SELECT subject FROM seed_subjects) THEN 6`);
5493
+ } else {
5494
+ branches.push(`WHEN e.subject IN (SELECT subject FROM seed_subjects) THEN 6`);
5495
+ }
5496
+ }
5497
+ return `CASE ${branches.join("\n ")} ELSE 9 END`;
5498
+ }
5499
+ function buildNeighborhoodMembershipExpression(families) {
5500
+ const clauses = [];
5501
+ if (families.includes("supersession_chain")) {
5502
+ clauses.push(`e.superseded_by IN (SELECT id FROM seed)`);
5503
+ clauses.push(`e.id IN (SELECT target_id FROM seed_supersessions)`);
5504
+ }
5505
+ if (families.includes("claim_key_sibling")) {
5506
+ clauses.push(`(e.claim_key IS NOT NULL AND e.claim_key IN (SELECT claim_key FROM seed_claim_keys))`);
5507
+ }
5508
+ if (families.includes("topic_family")) {
5509
+ clauses.push(`e.subject IN (SELECT subject FROM seed_subjects)`);
5510
+ }
5511
+ return clauses.length === 0 ? "0" : clauses.join("\n OR ");
4849
5512
  }
4850
5513
  function wrapVectorError(error) {
4851
5514
  const message = error instanceof Error ? error.message : String(error);
4852
5515
  return new Error(`Vector search is unavailable: ${message}`);
4853
5516
  }
4854
5517
 
5518
+ // src/app/evals/recall/attach-cross-encoder.ts
5519
+ function attachCrossEncoderPort(ports, crossEncoder) {
5520
+ if (!crossEncoder) {
5521
+ return ports;
5522
+ }
5523
+ return {
5524
+ async embed(text) {
5525
+ return ports.embed(text);
5526
+ },
5527
+ async vectorSearch(params) {
5528
+ return ports.vectorSearch(params);
5529
+ },
5530
+ async ftsSearch(params) {
5531
+ return ports.ftsSearch(params);
5532
+ },
5533
+ ...ports.expandNeighborhood ? {
5534
+ async expandNeighborhood(request) {
5535
+ return ports.expandNeighborhood(request);
5536
+ }
5537
+ } : {},
5538
+ crossEncoder,
5539
+ async hydrateEntries(ids) {
5540
+ return ports.hydrateEntries(ids);
5541
+ },
5542
+ async recordRecallEvents(params) {
5543
+ return ports.recordRecallEvents(params);
5544
+ }
5545
+ };
5546
+ }
5547
+
4855
5548
  // src/app/recall/claim-centric.ts
4856
5549
  function projectClaimCentricRecallEntries(entries, options = {}) {
4857
5550
  const families = /* @__PURE__ */ new Map();
@@ -4878,7 +5571,7 @@ function flattenClaimCentricRecallFamilies(families) {
4878
5571
  }
4879
5572
  function projectClaimCentricRecallEntry(recall2, options = {}) {
4880
5573
  const entry = recall2.entry;
4881
- const claimKey = normalizeOptionalString6(entry.claim_key);
5574
+ const claimKey = normalizeOptionalString7(entry.claim_key);
4882
5575
  const familyKey = claimKey ?? `entry:${entry.id}`;
4883
5576
  const slotPolicy = resolveClaimSlotPolicy(claimKey, options.slotPolicyConfig).policy;
4884
5577
  const asOfResolution = buildAsOfResolution(recall2, options.asOf);
@@ -4904,38 +5597,38 @@ function resolveMemoryState(recall2, asOfResolution) {
4904
5597
  if (asOfResolution.relation === "active") {
4905
5598
  return "current";
4906
5599
  }
4907
- return normalizeOptionalString6(entry.superseded_by) ? "superseded" : "historical";
5600
+ return normalizeOptionalString7(entry.superseded_by) ? "superseded" : "historical";
4908
5601
  }
4909
5602
  if (asOfResolution.relation === "observed_after" || asOfResolution.relation === "created_after") {
4910
5603
  return "historical";
4911
5604
  }
4912
- if (normalizeOptionalString6(entry.superseded_by)) {
5605
+ if (normalizeOptionalString7(entry.superseded_by)) {
4913
5606
  return "superseded";
4914
5607
  }
4915
- if (entry.retired || normalizeOptionalString6(entry.valid_to)) {
5608
+ if (entry.retired || normalizeOptionalString7(entry.valid_to)) {
4916
5609
  return "historical";
4917
5610
  }
4918
5611
  return "current";
4919
5612
  }
4920
- if (normalizeOptionalString6(entry.superseded_by)) {
5613
+ if (normalizeOptionalString7(entry.superseded_by)) {
4921
5614
  return "superseded";
4922
5615
  }
4923
- if (entry.retired || normalizeOptionalString6(entry.valid_to)) {
5616
+ if (entry.retired || normalizeOptionalString7(entry.valid_to)) {
4924
5617
  return "historical";
4925
5618
  }
4926
5619
  return "current";
4927
5620
  }
4928
5621
  function resolveClaimStatus(recall2) {
4929
5622
  const entry = recall2.entry;
4930
- if (!normalizeOptionalString6(entry.claim_key)) {
5623
+ if (!normalizeOptionalString7(entry.claim_key)) {
4931
5624
  return "no_key";
4932
5625
  }
4933
5626
  return entry.claim_key_status ?? "legacy";
4934
5627
  }
4935
5628
  function buildFreshness(recall2, memoryState, asOfResolution) {
4936
5629
  const entry = recall2.entry;
4937
- const validFrom = normalizeOptionalString6(entry.valid_from);
4938
- const validTo = normalizeOptionalString6(entry.valid_to);
5630
+ const validFrom = normalizeOptionalString7(entry.valid_from);
5631
+ const validTo = normalizeOptionalString7(entry.valid_to);
4939
5632
  const createdAt = entry.created_at;
4940
5633
  const labelParts = [`created ${createdAt}`];
4941
5634
  if (validFrom || validTo) {
@@ -4963,12 +5656,12 @@ function buildFreshness(recall2, memoryState, asOfResolution) {
4963
5656
  function buildProvenance(recall2) {
4964
5657
  const entry = recall2.entry;
4965
5658
  return {
4966
- ...normalizeOptionalString6(entry.superseded_by) ? { supersededById: entry.superseded_by } : {},
4967
- ...normalizeOptionalString6(entry.supersession_kind) ? { supersessionKind: entry.supersession_kind } : {},
4968
- ...normalizeOptionalString6(entry.supersession_reason) ? { supersessionReason: entry.supersession_reason } : {},
4969
- ...normalizeOptionalString6(entry.claim_support_source_kind) ? { supportSourceKind: entry.claim_support_source_kind } : {},
4970
- ...normalizeOptionalString6(entry.claim_support_locator) ? { supportLocator: entry.claim_support_locator } : {},
4971
- ...normalizeOptionalString6(entry.claim_support_observed_at) ? { supportObservedAt: entry.claim_support_observed_at } : {},
5659
+ ...normalizeOptionalString7(entry.superseded_by) ? { supersededById: entry.superseded_by } : {},
5660
+ ...normalizeOptionalString7(entry.supersession_kind) ? { supersessionKind: entry.supersession_kind } : {},
5661
+ ...normalizeOptionalString7(entry.supersession_reason) ? { supersessionReason: entry.supersession_reason } : {},
5662
+ ...normalizeOptionalString7(entry.claim_support_source_kind) ? { supportSourceKind: entry.claim_support_source_kind } : {},
5663
+ ...normalizeOptionalString7(entry.claim_support_locator) ? { supportLocator: entry.claim_support_locator } : {},
5664
+ ...normalizeOptionalString7(entry.claim_support_observed_at) ? { supportObservedAt: entry.claim_support_observed_at } : {},
4972
5665
  ...entry.claim_support_mode ? { supportMode: entry.claim_support_mode } : {}
4973
5666
  };
4974
5667
  }
@@ -5004,12 +5697,12 @@ function buildWhySurfaced(recall2, asOfResolution) {
5004
5697
  function formatScore(value) {
5005
5698
  return value.toFixed(2);
5006
5699
  }
5007
- function normalizeOptionalString6(value) {
5700
+ function normalizeOptionalString7(value) {
5008
5701
  const normalized = value?.trim();
5009
5702
  return normalized && normalized.length > 0 ? normalized : void 0;
5010
5703
  }
5011
5704
  function buildAsOfResolution(recall2, asOf) {
5012
- const normalizedAsOf = normalizeOptionalString6(asOf);
5705
+ const normalizedAsOf = normalizeOptionalString7(asOf);
5013
5706
  if (!normalizedAsOf) {
5014
5707
  return void 0;
5015
5708
  }
@@ -5073,12 +5766,296 @@ function formatAsOfRelation(relation) {
5073
5766
  }
5074
5767
  }
5075
5768
  function parseTimestamp(value) {
5076
- const normalized = normalizeOptionalString6(value);
5769
+ const normalized = normalizeOptionalString7(value);
5077
5770
  if (!normalized) {
5078
5771
  return void 0;
5079
5772
  }
5080
- const parsed = new Date(normalized);
5081
- return Number.isFinite(parsed.getTime()) ? parsed : void 0;
5773
+ const parsed = new Date(normalized);
5774
+ return Number.isFinite(parsed.getTime()) ? parsed : void 0;
5775
+ }
5776
+
5777
+ // src/app/procedures/recall/service.ts
5778
+ var DEFAULT_LIMIT = 5;
5779
+ var DEFAULT_CANONICAL_THRESHOLD = 0.55;
5780
+ var DEFAULT_CANONICAL_MARGIN = 0.08;
5781
+ var LEXICAL_CANDIDATE_MULTIPLIER = 3;
5782
+ var VECTOR_CANDIDATE_MULTIPLIER = 4;
5783
+ var VECTOR_ONLY_CANONICAL_FLOOR = 0.6;
5784
+ var LEXICAL_ONLY_NOTICE = "Semantic procedure search unavailable - using lexical-only procedure ranking.";
5785
+ var LEXICAL_ONLY_FALLBACK_NOTICE = "Semantic procedure search failed during procedure recall - using lexical-only procedure ranking.";
5786
+ async function runProcedureRecall(input, deps) {
5787
+ const text = input.text.trim();
5788
+ const limit = normalizeLimit(input.limit);
5789
+ if (text.length === 0 || limit === 0) {
5790
+ return {
5791
+ candidates: [],
5792
+ notices: []
5793
+ };
5794
+ }
5795
+ const lexicalLimit = limit * LEXICAL_CANDIDATE_MULTIPLIER;
5796
+ const vectorLimit = limit * VECTOR_CANDIDATE_MULTIPLIER;
5797
+ const notices = [];
5798
+ const lexicalMatches = await deps.db.procedureFtsSearch({
5799
+ text,
5800
+ limit: lexicalLimit
5801
+ });
5802
+ const queryEmbedding = await maybeEmbedQuery(text, deps.embedQuery, notices);
5803
+ const vectorMatches = queryEmbedding.length > 0 ? await deps.db.procedureVectorSearch({
5804
+ embedding: queryEmbedding,
5805
+ limit: vectorLimit
5806
+ }).catch(() => {
5807
+ notices.push(LEXICAL_ONLY_FALLBACK_NOTICE);
5808
+ return [];
5809
+ }) : [];
5810
+ const diversified = applyProcedureMmrDiversification(rankProcedureCandidates(text, lexicalMatches, vectorMatches), queryEmbedding, input.mmr);
5811
+ const reranked = await applyProcedureCrossEncoderRerank(diversified, text, input.crossEncoder);
5812
+ const ranked = reranked.slice(0, limit);
5813
+ const canonicalProcedure = selectCanonicalProcedure(ranked, input.threshold);
5814
+ return {
5815
+ ...canonicalProcedure ? { canonicalProcedure } : {},
5816
+ candidates: ranked,
5817
+ notices: dedupePreservingOrder(notices)
5818
+ };
5819
+ }
5820
+ async function applyProcedureCrossEncoderRerank(candidates, query, options) {
5821
+ if (!options || !options.enabled || candidates.length === 0) {
5822
+ return candidates;
5823
+ }
5824
+ const rerank = await applyCrossEncoderRerank({
5825
+ query,
5826
+ candidates: candidates.map((candidate) => ({
5827
+ id: candidate.procedure.id,
5828
+ text: buildProcedureCrossEncoderText(candidate.procedure),
5829
+ score: candidate.score,
5830
+ candidate
5831
+ })),
5832
+ port: options.port,
5833
+ topK: options.topK ?? DEFAULT_CROSS_ENCODER_TOP_K,
5834
+ alpha: options.alpha ?? DEFAULT_CROSS_ENCODER_ALPHA
5835
+ });
5836
+ return rerank.candidates.map((entry) => {
5837
+ const base = entry.candidate;
5838
+ const nextScore = entry.score;
5839
+ if (typeof entry.crossEncoderScore !== "number" && nextScore === base.score) {
5840
+ return base;
5841
+ }
5842
+ return {
5843
+ ...base,
5844
+ score: nextScore,
5845
+ scores: {
5846
+ ...base.scores,
5847
+ relevance: nextScore,
5848
+ rrf: nextScore,
5849
+ ...typeof entry.crossEncoderScore === "number" ? { crossEncoder: entry.crossEncoderScore } : {}
5850
+ }
5851
+ };
5852
+ });
5853
+ }
5854
+ function buildProcedureCrossEncoderText(procedure) {
5855
+ const title = procedure.title?.trim() ?? "";
5856
+ const recallText = procedure.recall_text?.trim() ?? "";
5857
+ if (title.length === 0) {
5858
+ return recallText;
5859
+ }
5860
+ if (recallText.length === 0) {
5861
+ return title;
5862
+ }
5863
+ return `${title}
5864
+
5865
+ ${recallText}`;
5866
+ }
5867
+ async function maybeEmbedQuery(text, embedQuery, notices) {
5868
+ if (!embedQuery) {
5869
+ notices.push(LEXICAL_ONLY_NOTICE);
5870
+ return [];
5871
+ }
5872
+ try {
5873
+ const embedding = await embedQuery(text);
5874
+ if (embedding.length === 0) {
5875
+ notices.push(LEXICAL_ONLY_NOTICE);
5876
+ return [];
5877
+ }
5878
+ return embedding;
5879
+ } catch {
5880
+ notices.push(LEXICAL_ONLY_FALLBACK_NOTICE);
5881
+ return [];
5882
+ }
5883
+ }
5884
+ function rankProcedureCandidates(query, lexicalMatches, vectorMatches) {
5885
+ const merged = /* @__PURE__ */ new Map();
5886
+ for (const match of lexicalMatches) {
5887
+ const lexical = computeProcedureLexicalScore(query, match.procedure);
5888
+ merged.set(match.procedure.id, {
5889
+ procedure: match.procedure,
5890
+ lexical,
5891
+ vector: 0
5892
+ });
5893
+ }
5894
+ for (const match of vectorMatches) {
5895
+ const existing = merged.get(match.procedure.id);
5896
+ merged.set(match.procedure.id, {
5897
+ procedure: match.procedure,
5898
+ lexical: existing?.lexical ?? computeProcedureLexicalScore(query, match.procedure),
5899
+ vector: Math.max(existing?.vector ?? 0, match.vectorSim)
5900
+ });
5901
+ }
5902
+ const lexicalRanks = rankByDescending(merged, (signals) => signals.lexical);
5903
+ const vectorRanks = rankByDescending(merged, (signals) => signals.vector);
5904
+ const relevanceByProcedureId = rrfFuseVectorLexical(vectorRanks, lexicalRanks);
5905
+ const ranked = Array.from(merged.values()).map((candidate) => {
5906
+ const relevance = relevanceByProcedureId.get(candidate.procedure.id) ?? 0;
5907
+ return {
5908
+ procedure: candidate.procedure,
5909
+ score: relevance,
5910
+ scores: {
5911
+ relevance,
5912
+ rrf: relevance,
5913
+ lexical: candidate.lexical,
5914
+ vector: candidate.vector
5915
+ }
5916
+ };
5917
+ }).filter((candidate) => candidate.score > 0);
5918
+ return applySeededProcedureRerank(ranked).sort(compareProcedureCandidates);
5919
+ }
5920
+ function applyProcedureMmrDiversification(candidates, queryEmbedding, options) {
5921
+ if (!options || !options.enabled || candidates.length < 2 || queryEmbedding.length === 0) {
5922
+ return candidates;
5923
+ }
5924
+ const reorder = maximalMarginalRelevance({
5925
+ queryVector: queryEmbedding,
5926
+ candidates: candidates.map((candidate) => ({
5927
+ id: candidate.procedure.id,
5928
+ relevance: candidate.score,
5929
+ ...candidate.procedure.embedding ? { embedding: candidate.procedure.embedding } : {}
5930
+ })),
5931
+ lambda: resolveProcedureMmrLambda(options.lambda),
5932
+ ...typeof options.minPoolSize === "number" ? { minPoolSize: options.minPoolSize } : {}
5933
+ });
5934
+ if (!reorder.applied) {
5935
+ return candidates;
5936
+ }
5937
+ const candidatesById = new Map(candidates.map((candidate) => [candidate.procedure.id, candidate]));
5938
+ return reorder.orderedIds.flatMap((id) => {
5939
+ const candidate = candidatesById.get(id);
5940
+ return candidate ? [candidate] : [];
5941
+ });
5942
+ }
5943
+ function resolveProcedureMmrLambda(value) {
5944
+ if (typeof value !== "number" || !Number.isFinite(value)) {
5945
+ return DEFAULT_MMR_LAMBDA;
5946
+ }
5947
+ return Math.max(0, Math.min(1, value));
5948
+ }
5949
+ function applySeededProcedureRerank(candidates) {
5950
+ if (candidates.length === 0) {
5951
+ return candidates;
5952
+ }
5953
+ const seeds = selectStrongSeeds(
5954
+ candidates.map((candidate) => ({ id: candidate.procedure.id, score: candidate.score, procedure: candidate.procedure })),
5955
+ {
5956
+ topN: DEFAULT_STRONG_SEED_TOP_N,
5957
+ scoreGapFloor: DEFAULT_STRONG_SEED_SCORE_GAP
5958
+ }
5959
+ );
5960
+ if (seeds.length === 0) {
5961
+ return candidates;
5962
+ }
5963
+ const payloads = candidates.map((candidate) => ({
5964
+ id: candidate.procedure.id,
5965
+ score: candidate.score,
5966
+ procedure: candidate.procedure
5967
+ }));
5968
+ const reranked = seededRerank(payloads, seeds, (candidate, seed) => sharesProcedureLineage(candidate.procedure, seed.procedure), {
5969
+ weight: DEFAULT_SEEDED_RERANK_WEIGHT
5970
+ });
5971
+ const scoreById = new Map(reranked.candidates.map((candidate) => [candidate.id, candidate.score]));
5972
+ return candidates.map((candidate) => {
5973
+ const nextScore = scoreById.get(candidate.procedure.id);
5974
+ if (nextScore === void 0 || nextScore === candidate.score) {
5975
+ return candidate;
5976
+ }
5977
+ return {
5978
+ ...candidate,
5979
+ score: nextScore,
5980
+ scores: {
5981
+ ...candidate.scores,
5982
+ relevance: nextScore,
5983
+ rrf: nextScore
5984
+ }
5985
+ };
5986
+ });
5987
+ }
5988
+ function computeProcedureLexicalScore(query, procedure) {
5989
+ return computeLexicalScore(query, procedure.title, procedure.recall_text);
5990
+ }
5991
+ function rankByDescending(merged, signalOf) {
5992
+ return Array.from(merged.values()).filter((signals) => signalOf(signals) > 0).sort((left, right) => {
5993
+ const delta = signalOf(right) - signalOf(left);
5994
+ if (delta !== 0) {
5995
+ return delta;
5996
+ }
5997
+ return left.procedure.procedure_key.localeCompare(right.procedure.procedure_key);
5998
+ }).map((signals) => signals.procedure.id);
5999
+ }
6000
+ function selectCanonicalProcedure(ranked, threshold) {
6001
+ const leader = ranked[0];
6002
+ if (!leader) {
6003
+ return void 0;
6004
+ }
6005
+ const minimumScore = normalizeThreshold(threshold);
6006
+ if (leader.score < minimumScore) {
6007
+ return void 0;
6008
+ }
6009
+ if (leader.scores.lexical === 0 && leader.scores.vector < VECTOR_ONLY_CANONICAL_FLOOR) {
6010
+ return void 0;
6011
+ }
6012
+ const runnerUp = ranked[1];
6013
+ if (runnerUp && signalStrength(leader) - signalStrength(runnerUp) < DEFAULT_CANONICAL_MARGIN) {
6014
+ return void 0;
6015
+ }
6016
+ return leader.procedure;
6017
+ }
6018
+ function signalStrength(candidate) {
6019
+ return Math.max(candidate.scores.vector, candidate.scores.lexical);
6020
+ }
6021
+ function compareProcedureCandidates(left, right) {
6022
+ if (left.score !== right.score) {
6023
+ return right.score - left.score;
6024
+ }
6025
+ if (left.scores.lexical !== right.scores.lexical) {
6026
+ return right.scores.lexical - left.scores.lexical;
6027
+ }
6028
+ if (left.scores.vector !== right.scores.vector) {
6029
+ return right.scores.vector - left.scores.vector;
6030
+ }
6031
+ if (left.procedure.updated_at !== right.procedure.updated_at) {
6032
+ return right.procedure.updated_at.localeCompare(left.procedure.updated_at);
6033
+ }
6034
+ return left.procedure.procedure_key.localeCompare(right.procedure.procedure_key);
6035
+ }
6036
+ function normalizeLimit(value) {
6037
+ if (value === void 0 || !Number.isFinite(value)) {
6038
+ return DEFAULT_LIMIT;
6039
+ }
6040
+ return Math.max(0, Math.trunc(value));
6041
+ }
6042
+ function normalizeThreshold(value) {
6043
+ if (value === void 0 || !Number.isFinite(value)) {
6044
+ return DEFAULT_CANONICAL_THRESHOLD;
6045
+ }
6046
+ return Math.min(1, Math.max(0, value));
6047
+ }
6048
+ function dedupePreservingOrder(values) {
6049
+ const seen = /* @__PURE__ */ new Set();
6050
+ const deduped = [];
6051
+ for (const value of values) {
6052
+ if (seen.has(value)) {
6053
+ continue;
6054
+ }
6055
+ seen.add(value);
6056
+ deduped.push(value);
6057
+ }
6058
+ return deduped;
5082
6059
  }
5083
6060
 
5084
6061
  // src/core/episode/temporal-window.ts
@@ -5561,12 +6538,12 @@ function compareAscending(left, right) {
5561
6538
  }
5562
6539
 
5563
6540
  // src/core/episode/search.ts
5564
- var DEFAULT_LIMIT = 10;
6541
+ var DEFAULT_LIMIT2 = 10;
5565
6542
  var MIN_CANDIDATE_LIMIT = 25;
5566
6543
  var MAX_CANDIDATE_LIMIT = 100;
5567
6544
  var CANDIDATE_MULTIPLIER = 5;
5568
6545
  async function searchEpisodes(query, database, now = /* @__PURE__ */ new Date()) {
5569
- const limit = normalizeLimit(query.limit);
6546
+ const limit = normalizeLimit2(query.limit);
5570
6547
  if (limit === 0) {
5571
6548
  return [];
5572
6549
  }
@@ -5589,14 +6566,139 @@ async function searchEpisodes(query, database, now = /* @__PURE__ */ new Date())
5589
6566
  return matches.map((match) => buildSemanticResult(match.episode, match.vectorSim, now)).sort(compareSemanticEpisodeResults).slice(0, limit);
5590
6567
  }
5591
6568
  const candidates = await database.listEpisodesByTimeWindow(query.timeWindow, computeCandidateLimit(limit));
5592
- return candidates.map((episode) => buildHybridResult(episode, normalizedEmbedding, bounds, now)).sort(compareSemanticEpisodeResults).slice(0, limit);
6569
+ const hybridResults = candidates.map((episode) => buildHybridResult(episode, normalizedEmbedding, bounds, now));
6570
+ const fused = fuseHybridResultsWithRrf(hybridResults);
6571
+ const diversified = applyEpisodeMmrDiversification(fused, normalizedEmbedding, query.mmr);
6572
+ const reranked = await applyEpisodeCrossEncoderRerank(diversified, query.text, query.crossEncoder);
6573
+ return reranked.slice(0, limit);
6574
+ }
6575
+ async function applyEpisodeCrossEncoderRerank(results, query, options) {
6576
+ if (!options || !options.enabled || results.length === 0) {
6577
+ return results;
6578
+ }
6579
+ const rerank = await applyCrossEncoderRerank({
6580
+ query,
6581
+ candidates: results.map((result) => ({
6582
+ id: result.episode.id,
6583
+ text: buildEpisodeCrossEncoderText(result.episode),
6584
+ score: result.score,
6585
+ candidate: result
6586
+ })),
6587
+ port: options.port,
6588
+ topK: options.topK ?? DEFAULT_CROSS_ENCODER_TOP_K,
6589
+ alpha: options.alpha ?? DEFAULT_CROSS_ENCODER_ALPHA
6590
+ });
6591
+ return rerank.candidates.map((entry) => {
6592
+ const base = entry.candidate;
6593
+ const nextScore = Number(entry.score.toFixed(6));
6594
+ if (typeof entry.crossEncoderScore !== "number" && nextScore === base.score) {
6595
+ return base;
6596
+ }
6597
+ return {
6598
+ ...base,
6599
+ score: nextScore,
6600
+ scores: {
6601
+ ...base.scores,
6602
+ ...typeof entry.crossEncoderScore === "number" ? { crossEncoder: Number(entry.crossEncoderScore.toFixed(6)) } : {}
6603
+ }
6604
+ };
6605
+ });
5593
6606
  }
5594
- function normalizeLimit(value) {
6607
+ function buildEpisodeCrossEncoderText(episode) {
6608
+ const summary = episode.summary?.trim() ?? "";
6609
+ if (summary.length > 0) {
6610
+ return summary;
6611
+ }
6612
+ const source = episode.source ?? "";
6613
+ const ref = episode.sourceRef ?? episode.sourceId ?? "";
6614
+ return `${source} ${ref}`.trim();
6615
+ }
6616
+ function applyEpisodeMmrDiversification(results, queryEmbedding, options) {
6617
+ if (!options || !options.enabled || results.length < 2 || queryEmbedding.length === 0) {
6618
+ return results;
6619
+ }
6620
+ const reorder = maximalMarginalRelevance({
6621
+ queryVector: queryEmbedding,
6622
+ candidates: results.map((result) => ({
6623
+ id: result.episode.id,
6624
+ relevance: result.score,
6625
+ ...result.episode.embedding ? { embedding: result.episode.embedding } : {}
6626
+ })),
6627
+ lambda: resolveEpisodeMmrLambda(options.lambda),
6628
+ ...typeof options.minPoolSize === "number" ? { minPoolSize: options.minPoolSize } : {}
6629
+ });
6630
+ if (!reorder.applied) {
6631
+ return results;
6632
+ }
6633
+ const resultsById = new Map(results.map((result) => [result.episode.id, result]));
6634
+ return reorder.orderedIds.flatMap((id) => {
6635
+ const result = resultsById.get(id);
6636
+ return result ? [result] : [];
6637
+ });
6638
+ }
6639
+ function resolveEpisodeMmrLambda(value) {
6640
+ if (typeof value !== "number" || !Number.isFinite(value)) {
6641
+ return DEFAULT_MMR_LAMBDA;
6642
+ }
6643
+ return Math.max(0, Math.min(1, value));
6644
+ }
6645
+ function fuseHybridResultsWithRrf(hybridResults) {
6646
+ if (hybridResults.length === 0) {
6647
+ return [];
6648
+ }
6649
+ const temporalRanks = [...hybridResults].sort((left, right) => compareDescending2(left.scores.temporal, right.scores.temporal) || compareAscending2(left.episode.id, right.episode.id)).map((result) => result.episode.id);
6650
+ const semanticRanks = [...hybridResults].filter((result) => result.scores.semantic > 0).sort((left, right) => compareDescending2(left.scores.semantic, right.scores.semantic) || compareAscending2(left.episode.id, right.episode.id)).map((result) => result.episode.id);
6651
+ const fusedScores = rrfFuse([temporalRanks, semanticRanks]);
6652
+ const fused = hybridResults.map((result) => ({
6653
+ ...result,
6654
+ score: Number((fusedScores.get(result.episode.id) ?? 0).toFixed(6))
6655
+ }));
6656
+ const reranked = applySeededEpisodeRerank(fused);
6657
+ return reranked.sort(compareFusedEpisodeResults);
6658
+ }
6659
+ function applySeededEpisodeRerank(results) {
6660
+ if (results.length === 0) {
6661
+ return results;
6662
+ }
6663
+ const seeds = selectStrongSeeds(
6664
+ results.map((result) => ({ id: result.episode.id, score: result.score, episode: result.episode })),
6665
+ {
6666
+ topN: DEFAULT_STRONG_SEED_TOP_N,
6667
+ scoreGapFloor: DEFAULT_STRONG_SEED_SCORE_GAP
6668
+ }
6669
+ );
6670
+ if (seeds.length === 0) {
6671
+ return results;
6672
+ }
6673
+ const payloads = results.map((result) => ({
6674
+ id: result.episode.id,
6675
+ score: result.score,
6676
+ episode: result.episode
6677
+ }));
6678
+ const reranked = seededRerank(payloads, seeds, (candidate, seed) => sharesEpisodeLineage(candidate.episode, seed.episode), {
6679
+ weight: DEFAULT_SEEDED_RERANK_WEIGHT
6680
+ });
6681
+ const scoreById = new Map(reranked.candidates.map((candidate) => [candidate.id, candidate.score]));
6682
+ return results.map((result) => {
6683
+ const nextScore = scoreById.get(result.episode.id);
6684
+ if (nextScore === void 0 || nextScore === result.score) {
6685
+ return result;
6686
+ }
6687
+ return {
6688
+ ...result,
6689
+ score: Number(nextScore.toFixed(6))
6690
+ };
6691
+ });
6692
+ }
6693
+ function compareFusedEpisodeResults(left, right) {
6694
+ return compareDescending2(left.score, right.score) || compareDescending2(left.scores.semantic, right.scores.semantic) || compareDescending2(left.scores.temporal, right.scores.temporal) || compareDescending2(left.scores.activity, right.scores.activity) || compareDescending2(left.scores.recency, right.scores.recency) || compareAscending2(left.episode.startedAt, right.episode.startedAt) || compareAscending2(left.episode.id, right.episode.id);
6695
+ }
6696
+ function normalizeLimit2(value) {
5595
6697
  if (value === void 0) {
5596
- return DEFAULT_LIMIT;
6698
+ return DEFAULT_LIMIT2;
5597
6699
  }
5598
6700
  if (!Number.isFinite(value)) {
5599
- return DEFAULT_LIMIT;
6701
+ return DEFAULT_LIMIT2;
5600
6702
  }
5601
6703
  return Math.max(0, Math.trunc(value));
5602
6704
  }
@@ -5653,156 +6755,6 @@ function compareAscending2(left, right) {
5653
6755
  return left.localeCompare(right);
5654
6756
  }
5655
6757
 
5656
- // src/app/procedures/recall/service.ts
5657
- var DEFAULT_LIMIT2 = 5;
5658
- var DEFAULT_CANONICAL_THRESHOLD = 0.55;
5659
- var DEFAULT_CANONICAL_MARGIN = 0.08;
5660
- var LEXICAL_CANDIDATE_MULTIPLIER = 3;
5661
- var VECTOR_CANDIDATE_MULTIPLIER = 4;
5662
- var VECTOR_ONLY_CANONICAL_FLOOR = 0.6;
5663
- var LEXICAL_ONLY_NOTICE = "Semantic procedure search unavailable - using lexical-only procedure ranking.";
5664
- var LEXICAL_ONLY_FALLBACK_NOTICE = "Semantic procedure search failed during procedure recall - using lexical-only procedure ranking.";
5665
- async function runProcedureRecall(input, deps) {
5666
- const text = input.text.trim();
5667
- const limit = normalizeLimit2(input.limit);
5668
- if (text.length === 0 || limit === 0) {
5669
- return {
5670
- candidates: [],
5671
- notices: []
5672
- };
5673
- }
5674
- const lexicalLimit = limit * LEXICAL_CANDIDATE_MULTIPLIER;
5675
- const vectorLimit = limit * VECTOR_CANDIDATE_MULTIPLIER;
5676
- const notices = [];
5677
- const lexicalMatches = await deps.db.procedureFtsSearch({
5678
- text,
5679
- limit: lexicalLimit
5680
- });
5681
- const queryEmbedding = await maybeEmbedQuery(text, deps.embedQuery, notices);
5682
- const vectorMatches = queryEmbedding.length > 0 ? await deps.db.procedureVectorSearch({
5683
- embedding: queryEmbedding,
5684
- limit: vectorLimit
5685
- }).catch(() => {
5686
- notices.push(LEXICAL_ONLY_FALLBACK_NOTICE);
5687
- return [];
5688
- }) : [];
5689
- const ranked = rankProcedureCandidates(text, lexicalMatches, vectorMatches).slice(0, limit);
5690
- const canonicalProcedure = selectCanonicalProcedure(ranked, input.threshold);
5691
- return {
5692
- ...canonicalProcedure ? { canonicalProcedure } : {},
5693
- candidates: ranked,
5694
- notices: dedupePreservingOrder(notices)
5695
- };
5696
- }
5697
- async function maybeEmbedQuery(text, embedQuery, notices) {
5698
- if (!embedQuery) {
5699
- notices.push(LEXICAL_ONLY_NOTICE);
5700
- return [];
5701
- }
5702
- try {
5703
- const embedding = await embedQuery(text);
5704
- if (embedding.length === 0) {
5705
- notices.push(LEXICAL_ONLY_NOTICE);
5706
- return [];
5707
- }
5708
- return embedding;
5709
- } catch {
5710
- notices.push(LEXICAL_ONLY_FALLBACK_NOTICE);
5711
- return [];
5712
- }
5713
- }
5714
- function rankProcedureCandidates(query, lexicalMatches, vectorMatches) {
5715
- const merged = /* @__PURE__ */ new Map();
5716
- for (const match of lexicalMatches) {
5717
- const lexical = computeProcedureLexicalScore(query, match.procedure);
5718
- merged.set(match.procedure.id, {
5719
- procedure: match.procedure,
5720
- lexical,
5721
- vector: 0
5722
- });
5723
- }
5724
- for (const match of vectorMatches) {
5725
- const existing = merged.get(match.procedure.id);
5726
- merged.set(match.procedure.id, {
5727
- procedure: match.procedure,
5728
- lexical: existing?.lexical ?? computeProcedureLexicalScore(query, match.procedure),
5729
- vector: Math.max(existing?.vector ?? 0, match.vectorSim)
5730
- });
5731
- }
5732
- return Array.from(merged.values()).map((candidate) => {
5733
- const relevance = combinedRelevance(candidate.vector, candidate.lexical);
5734
- return {
5735
- procedure: candidate.procedure,
5736
- score: relevance,
5737
- scores: {
5738
- relevance,
5739
- lexical: candidate.lexical,
5740
- vector: candidate.vector
5741
- }
5742
- };
5743
- }).filter((candidate) => candidate.score > 0).sort(compareProcedureCandidates);
5744
- }
5745
- function computeProcedureLexicalScore(query, procedure) {
5746
- return computeLexicalScore(query, procedure.title, procedure.recall_text);
5747
- }
5748
- function selectCanonicalProcedure(ranked, threshold) {
5749
- const leader = ranked[0];
5750
- if (!leader) {
5751
- return void 0;
5752
- }
5753
- const minimumScore = normalizeThreshold(threshold);
5754
- if (leader.score < minimumScore) {
5755
- return void 0;
5756
- }
5757
- if (leader.scores.lexical === 0 && leader.scores.vector < VECTOR_ONLY_CANONICAL_FLOOR) {
5758
- return void 0;
5759
- }
5760
- const runnerUp = ranked[1];
5761
- if (runnerUp && leader.score - runnerUp.score < DEFAULT_CANONICAL_MARGIN) {
5762
- return void 0;
5763
- }
5764
- return leader.procedure;
5765
- }
5766
- function compareProcedureCandidates(left, right) {
5767
- if (left.score !== right.score) {
5768
- return right.score - left.score;
5769
- }
5770
- if (left.scores.lexical !== right.scores.lexical) {
5771
- return right.scores.lexical - left.scores.lexical;
5772
- }
5773
- if (left.scores.vector !== right.scores.vector) {
5774
- return right.scores.vector - left.scores.vector;
5775
- }
5776
- if (left.procedure.updated_at !== right.procedure.updated_at) {
5777
- return right.procedure.updated_at.localeCompare(left.procedure.updated_at);
5778
- }
5779
- return left.procedure.procedure_key.localeCompare(right.procedure.procedure_key);
5780
- }
5781
- function normalizeLimit2(value) {
5782
- if (value === void 0 || !Number.isFinite(value)) {
5783
- return DEFAULT_LIMIT2;
5784
- }
5785
- return Math.max(0, Math.trunc(value));
5786
- }
5787
- function normalizeThreshold(value) {
5788
- if (value === void 0 || !Number.isFinite(value)) {
5789
- return DEFAULT_CANONICAL_THRESHOLD;
5790
- }
5791
- return Math.min(1, Math.max(0, value));
5792
- }
5793
- function dedupePreservingOrder(values) {
5794
- const seen = /* @__PURE__ */ new Set();
5795
- const deduped = [];
5796
- for (const value of values) {
5797
- if (seen.has(value)) {
5798
- continue;
5799
- }
5800
- seen.add(value);
5801
- deduped.push(value);
5802
- }
5803
- return deduped;
5804
- }
5805
-
5806
6758
  // src/app/recall/transitions.ts
5807
6759
  function buildClaimTransitionExplanations(params) {
5808
6760
  if (params.families.length === 0 || params.detectedIntent === "temporal_narrative") {
@@ -5939,14 +6891,32 @@ var PROCEDURAL_REGEX_PATTERNS = [
5939
6891
  /\b(?:checklist|playbook|runbook|procedure|process|instructions?|workflow|method)\b.*\b(?:for|to)\b/u,
5940
6892
  /\bwhat(?:'s| is) the (?:best|recommended|right) way to\b/u
5941
6893
  ];
6894
+ var ENTITY_ATTRIBUTE_MAX_WORDS = 5;
6895
+ var ENTITY_ATTRIBUTE_CONTEXTUAL_PREFIX_RE = /^(?:on|in|at|for|about|during|after|before)\b/u;
6896
+ var ENTITY_ATTRIBUTE_CONTEXTUAL_TIME_RE = /\b(?:today|tomorrow|yesterday|tonight|currently|right now|this week|next week|last week|this month|next month|last month|this year|next year|last year)\b/u;
6897
+ var ENTITY_ATTRIBUTE_CONTEXTUAL_ACTIVITY_RE = /\b(?:on call|available|working|assigned|scheduled|responsible)\b/u;
6898
+ var ENTITY_ATTRIBUTE_GENERIC_ENTITY_RE = /^(?:it|this|that|these|those|they|them|he|she|someone|anyone|anything|everything)\b/u;
6899
+ var ENTITY_ATTRIBUTE_KIND_TOKENS = {
6900
+ identity: ["identity", "profile", "bio", "biography", "summary"],
6901
+ location: ["location", "live", "lives", "reside", "resides", "located", "home", "city"],
6902
+ email: ["email", "e-mail", "mail"],
6903
+ phone: ["phone", "number", "mobile", "cell", "telephone"],
6904
+ address: ["address", "street", "mailing"]
6905
+ };
5942
6906
  async function runUnifiedRecall(input, deps) {
5943
6907
  const now = deps.now ?? /* @__PURE__ */ new Date();
5944
6908
  const requested = normalizeMode(input.mode);
5945
6909
  const parsedTimeWindow = parseTemporalWindow(input.text, now);
5946
6910
  const hasEntryFilters = hasEntryScopedFilters(input);
5947
6911
  const topicAnchor = hasTopicAnchor(input.text, hasEntryFilters);
6912
+ const entityAttributeQuery = detectEntityAttributeQuery(input.text);
5948
6913
  const historicalStatePattern = detectHistoricalStatePattern(input.text);
5949
6914
  const proceduralPattern = detectProceduralPattern(input.text);
6915
+ if (entityAttributeQuery) {
6916
+ deps.debugLog?.(
6917
+ `[agenr] unified recall matched entity-attribute kind=${JSON.stringify(entityAttributeQuery.attributeKind)} entity=${JSON.stringify(entityAttributeQuery.entityText)} query=${JSON.stringify(input.text)}`
6918
+ );
6919
+ }
5950
6920
  if (historicalStatePattern) {
5951
6921
  deps.debugLog?.(`[agenr] unified recall matched historical-state pattern=${JSON.stringify(historicalStatePattern)} query=${JSON.stringify(input.text)}`);
5952
6922
  }
@@ -5967,7 +6937,9 @@ async function runUnifiedRecall(input, deps) {
5967
6937
  detectedIntent: routing.detectedIntent,
5968
6938
  parsedTimeWindow,
5969
6939
  topicAnchor,
5970
- embedQuery: deps.embedQuery
6940
+ embedQuery: deps.embedQuery,
6941
+ rankingPolicy: deps.recallOptions?.rankingPolicy,
6942
+ crossEncoder: deps.recall.crossEncoder
5971
6943
  }) : {
5972
6944
  notices: []
5973
6945
  };
@@ -5979,11 +6951,15 @@ async function runUnifiedRecall(input, deps) {
5979
6951
  if (routing.queried.includes("episodes") && hasEntryScopedFilters(input)) {
5980
6952
  notices.push(ENTRY_FILTER_NOTICE);
5981
6953
  }
6954
+ const procedureMmr = resolveProcedureMmrOptions(deps.recallOptions?.rankingPolicy);
6955
+ const procedureCrossEncoder = resolveProcedureCrossEncoderOptions(deps.recallOptions?.rankingPolicy, deps.recall.crossEncoder);
5982
6956
  const procedureResults = routing.queried.includes("procedures") ? await runProcedureRecall(
5983
6957
  {
5984
6958
  text: input.text,
5985
6959
  ...input.limit !== void 0 ? { limit: input.limit } : {},
5986
- ...input.threshold !== void 0 ? { threshold: input.threshold } : {}
6960
+ ...input.threshold !== void 0 ? { threshold: input.threshold } : {},
6961
+ ...procedureMmr ? { mmr: procedureMmr } : {},
6962
+ ...procedureCrossEncoder ? { crossEncoder: procedureCrossEncoder } : {}
5987
6963
  },
5988
6964
  {
5989
6965
  db: deps.procedures,
@@ -6044,13 +7020,14 @@ function routeRecall(params) {
6044
7020
  const lower = params.text.trim().toLowerCase();
6045
7021
  const factual = /^(when did|when was|what decision|what preference|what(?:'s| is) the default|which version|what threshold)\b/.test(lower);
6046
7022
  const narrative = /\b(what happened|what were we doing|what was going on|summarize|catch me up)\b/.test(lower);
7023
+ const entityAttributeQuery = detectEntityAttributeQuery(params.text);
6047
7024
  const historicalState = detectHistoricalStatePattern(params.text) !== void 0;
6048
7025
  const procedural = detectProceduralPattern(params.text) !== void 0;
6049
7026
  const topicAnchor = hasTopicAnchor(params.text, params.hasEntryFilters);
6050
7027
  if (params.requested === "entries") {
6051
7028
  return {
6052
7029
  requested: params.requested,
6053
- detectedIntent: historicalState ? "historical_state" : factual ? "factual" : params.parsedTimeWindow ? "mixed" : "factual",
7030
+ detectedIntent: entityAttributeQuery ? "entity_attribute" : historicalState ? "historical_state" : factual ? "factual" : params.parsedTimeWindow ? "mixed" : "factual",
6054
7031
  queried: ["entries"],
6055
7032
  reason: "Explicit mode=entries override."
6056
7033
  };
@@ -6071,6 +7048,14 @@ function routeRecall(params) {
6071
7048
  reason: "Explicit mode=procedures override."
6072
7049
  };
6073
7050
  }
7051
+ if (entityAttributeQuery) {
7052
+ return {
7053
+ requested: params.requested,
7054
+ detectedIntent: "entity_attribute",
7055
+ queried: ["entries"],
7056
+ reason: "The query asks for a specific entity attribute, so precision-first entry recall was used."
7057
+ };
7058
+ }
6074
7059
  if (historicalState) {
6075
7060
  return {
6076
7061
  requested: params.requested,
@@ -6173,16 +7158,65 @@ async function buildEpisodeQueryPlan(params) {
6173
7158
  notices
6174
7159
  };
6175
7160
  }
7161
+ const mmr = resolveEpisodeMmrOptions(params.detectedIntent, params.rankingPolicy);
7162
+ const crossEncoder = resolveEpisodeCrossEncoderOptions(params.rankingPolicy, params.crossEncoder);
6176
7163
  return {
6177
7164
  query: {
6178
7165
  text: params.text,
6179
7166
  ...params.limit !== void 0 ? { limit: params.limit } : {},
6180
7167
  ...params.parsedTimeWindow ? { timeWindow: params.parsedTimeWindow.window } : {},
6181
- ...embedding ? { embedding } : {}
7168
+ ...embedding ? { embedding } : {},
7169
+ ...mmr ? { mmr } : {},
7170
+ ...crossEncoder ? { crossEncoder } : {}
6182
7171
  },
6183
7172
  notices
6184
7173
  };
6185
7174
  }
7175
+ function resolveEpisodeMmrOptions(detectedIntent, rankingPolicy) {
7176
+ if (rankingPolicy?.mmr === "disabled") {
7177
+ return void 0;
7178
+ }
7179
+ if (detectedIntent !== "factual" && detectedIntent !== "mixed") {
7180
+ return void 0;
7181
+ }
7182
+ return {
7183
+ enabled: true,
7184
+ ...typeof rankingPolicy?.mmrLambda === "number" ? { lambda: rankingPolicy.mmrLambda } : {},
7185
+ ...typeof rankingPolicy?.mmrMinPoolSize === "number" ? { minPoolSize: rankingPolicy.mmrMinPoolSize } : {}
7186
+ };
7187
+ }
7188
+ function resolveProcedureMmrOptions(rankingPolicy) {
7189
+ if (rankingPolicy?.mmr === "disabled") {
7190
+ return void 0;
7191
+ }
7192
+ return {
7193
+ enabled: true,
7194
+ ...typeof rankingPolicy?.mmrLambda === "number" ? { lambda: rankingPolicy.mmrLambda } : {},
7195
+ ...typeof rankingPolicy?.mmrMinPoolSize === "number" ? { minPoolSize: rankingPolicy.mmrMinPoolSize } : {}
7196
+ };
7197
+ }
7198
+ function resolveEpisodeCrossEncoderOptions(rankingPolicy, crossEncoder) {
7199
+ if (!crossEncoder || rankingPolicy?.crossEncoder === "disabled") {
7200
+ return void 0;
7201
+ }
7202
+ return {
7203
+ enabled: true,
7204
+ port: crossEncoder,
7205
+ ...typeof rankingPolicy?.crossEncoderTopK === "number" ? { topK: rankingPolicy.crossEncoderTopK } : {},
7206
+ ...typeof rankingPolicy?.crossEncoderAlpha === "number" ? { alpha: rankingPolicy.crossEncoderAlpha } : {}
7207
+ };
7208
+ }
7209
+ function resolveProcedureCrossEncoderOptions(rankingPolicy, crossEncoder) {
7210
+ if (!crossEncoder || rankingPolicy?.crossEncoder === "disabled") {
7211
+ return void 0;
7212
+ }
7213
+ return {
7214
+ enabled: true,
7215
+ port: crossEncoder,
7216
+ ...typeof rankingPolicy?.crossEncoderTopK === "number" ? { topK: rankingPolicy.crossEncoderTopK } : {},
7217
+ ...typeof rankingPolicy?.crossEncoderAlpha === "number" ? { alpha: rankingPolicy.crossEncoderAlpha } : {}
7218
+ };
7219
+ }
6186
7220
  async function maybeRunEntryRecall(params) {
6187
7221
  if (!params.routing.queried.includes("entries")) {
6188
7222
  return {
@@ -6219,6 +7253,7 @@ function composeRecallTrace(upstream, onSummary) {
6219
7253
  };
6220
7254
  }
6221
7255
  function buildEntryRecallInput(input, parsedTimeWindow, routing) {
7256
+ const entityAttributeQuery = routing.detectedIntent === "entity_attribute" ? detectEntityAttributeQuery(input.text) : void 0;
6222
7257
  const request = {
6223
7258
  text: input.text,
6224
7259
  ...input.limit !== void 0 ? { limit: input.limit } : {},
@@ -6227,7 +7262,9 @@ function buildEntryRecallInput(input, parsedTimeWindow, routing) {
6227
7262
  ...input.tags && input.tags.length > 0 ? { tags: input.tags } : {},
6228
7263
  ...input.sessionKey ? { sessionKey: input.sessionKey } : {},
6229
7264
  ...input.asOf ? { asOf: input.asOf } : {},
6230
- ...routing.detectedIntent === "historical_state" ? { rankingProfile: "historical_state" } : {}
7265
+ ...routing.detectedIntent === "historical_state" ? { rankingProfile: "historical_state" } : {},
7266
+ ...routing.detectedIntent === "entity_attribute" ? { rankingProfile: "entity_attribute" } : {},
7267
+ ...entityAttributeQuery ? { queryShape: entityAttributeQuery } : {}
6231
7268
  };
6232
7269
  if (!parsedTimeWindow || input.asOf) {
6233
7270
  return request;
@@ -6262,6 +7299,90 @@ function detectProceduralPattern(text) {
6262
7299
  const regexPattern = PROCEDURAL_REGEX_PATTERNS.find((pattern) => pattern.test(lower));
6263
7300
  return regexPattern?.source;
6264
7301
  }
7302
+ function detectEntityAttributeQuery(text) {
7303
+ const normalizedText = normalizeEntityAttributeWhitespace(text);
7304
+ const whereDoesLive = /^where\s+does\s+(.+?)\s+live[?!.,]*$/iu.exec(normalizedText);
7305
+ if (whereDoesLive) {
7306
+ return buildEntityAttributeQueryShape(whereDoesLive[1], "location");
7307
+ }
7308
+ const whereIs = /^where\s+is\s+(.+?)[?!.,]*$/iu.exec(normalizedText);
7309
+ if (whereIs) {
7310
+ return buildEntityAttributeQueryShape(whereIs[1], "location");
7311
+ }
7312
+ const possessiveAttribute = /^(?:what\s+is|what's)\s+(.+?)'s\s+(.+?)[?!.,]*$/iu.exec(normalizedText);
7313
+ if (possessiveAttribute) {
7314
+ const attributeKind = resolveEntityAttributeKind(possessiveAttribute[2]);
7315
+ if (attributeKind) {
7316
+ return buildEntityAttributeQueryShape(possessiveAttribute[1], attributeKind);
7317
+ }
7318
+ }
7319
+ const whoIs = /^(?:who\s+is|who's)\s+(.+?)(?:\s+again)?[?!.,]*$/iu.exec(normalizedText);
7320
+ if (whoIs) {
7321
+ return buildEntityAttributeQueryShape(whoIs[1], "identity");
7322
+ }
7323
+ const whatIs = /^what\s+is\s+(.+?)(?:\s+again)?[?!.,]*$/iu.exec(normalizedText);
7324
+ if (whatIs) {
7325
+ return buildEntityAttributeQueryShape(whatIs[1], "identity");
7326
+ }
7327
+ return void 0;
7328
+ }
7329
+ function buildEntityAttributeQueryShape(rawEntityText, attributeKind) {
7330
+ const entityText = normalizeEntityAttributeEntity(rawEntityText);
7331
+ if (!entityText) {
7332
+ return void 0;
7333
+ }
7334
+ const entityTokens = tokenize(entityText);
7335
+ if (entityTokens.length === 0) {
7336
+ return void 0;
7337
+ }
7338
+ return {
7339
+ kind: "entity_attribute",
7340
+ entityText,
7341
+ normalizedEntity: normalizeEntityAttributeText(entityText),
7342
+ entityTokens,
7343
+ attributeKind,
7344
+ attributeTokens: [...ENTITY_ATTRIBUTE_KIND_TOKENS[attributeKind]]
7345
+ };
7346
+ }
7347
+ function resolveEntityAttributeKind(rawAttributeText) {
7348
+ const tokens = tokenize(rawAttributeText ?? "");
7349
+ if (tokens.some((token) => token === "email" || token === "e-mail" || token === "mail")) {
7350
+ return "email";
7351
+ }
7352
+ if (tokens.some((token) => token === "phone" || token === "number" || token === "mobile" || token === "cell" || token === "telephone")) {
7353
+ return "phone";
7354
+ }
7355
+ if (tokens.some((token) => token === "address" || token === "street" || token === "mailing")) {
7356
+ return "address";
7357
+ }
7358
+ if (tokens.some(
7359
+ (token) => token === "location" || token === "live" || token === "lives" || token === "reside" || token === "resides" || token === "located" || token === "home" || token === "city"
7360
+ )) {
7361
+ return "location";
7362
+ }
7363
+ if (tokens.some((token) => ENTITY_ATTRIBUTE_KIND_TOKENS.identity.includes(token))) {
7364
+ return "identity";
7365
+ }
7366
+ return void 0;
7367
+ }
7368
+ function normalizeEntityAttributeEntity(rawEntityText) {
7369
+ const cleaned = rawEntityText ? normalizeEntityAttributeWhitespace(rawEntityText).replace(/^[("'`]+/u, "").replace(/[)"'`?!.,]+$/u, "").replace(/^(?:the|a|an)\s+/iu, "").trim() : "";
7370
+ if (cleaned.length === 0) {
7371
+ return void 0;
7372
+ }
7373
+ const normalized = normalizeEntityAttributeText(cleaned);
7374
+ const wordCount = cleaned.split(/\s+/u).filter((token) => token.length > 0).length;
7375
+ if (wordCount === 0 || wordCount > ENTITY_ATTRIBUTE_MAX_WORDS || ENTITY_ATTRIBUTE_GENERIC_ENTITY_RE.test(normalized) || ENTITY_ATTRIBUTE_CONTEXTUAL_PREFIX_RE.test(normalized) || ENTITY_ATTRIBUTE_CONTEXTUAL_TIME_RE.test(normalized) || ENTITY_ATTRIBUTE_CONTEXTUAL_ACTIVITY_RE.test(normalized)) {
7376
+ return void 0;
7377
+ }
7378
+ return cleaned;
7379
+ }
7380
+ function normalizeEntityAttributeWhitespace(text) {
7381
+ return text.replace(/\s+/gu, " ").trim();
7382
+ }
7383
+ function normalizeEntityAttributeText(text) {
7384
+ return normalizeEntityAttributeWhitespace(text).normalize("NFKC").toLocaleLowerCase();
7385
+ }
6265
7386
  function normalizeMode(value) {
6266
7387
  return value === "entries" || value === "episodes" || value === "procedures" ? value : "auto";
6267
7388
  }
@@ -6362,7 +7483,6 @@ export {
6362
7483
  DEFAULT_SURGEON_RETIREMENT_PROTECT_MIN_IMPORTANCE,
6363
7484
  DEFAULT_SURGEON_SKIP_RECENTLY_EVALUATED_DAYS,
6364
7485
  DEFAULT_CLAIM_EXTRACTION_CONCURRENCY,
6365
- isAgenrAuthMethod,
6366
7486
  authMethodToProvider,
6367
7487
  getAuthMethodDefinition,
6368
7488
  toAgenrConfigInput,
@@ -6378,7 +7498,16 @@ export {
6378
7498
  createEmbeddingClient,
6379
7499
  resolveEmbeddingApiKey,
6380
7500
  resolveEmbeddingModel,
7501
+ probeLlmCredentials,
7502
+ createLlmClient,
7503
+ resolveModel,
7504
+ resolveLlmCredentials,
7505
+ resolveLlmApiKey,
7506
+ createOpenAICrossEncoder,
7507
+ resolveCrossEncoderApiKey,
6381
7508
  createRecallAdapter,
7509
+ attachCrossEncoderPort,
6382
7510
  projectClaimCentricRecallEntry,
7511
+ runProcedureRecall,
6383
7512
  runUnifiedRecall
6384
7513
  };