dsh-context-compression-improved 0.4.0-beta.1 → 0.5.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.
Files changed (71) hide show
  1. package/CHANGELOG.ja.md +68 -36
  2. package/CHANGELOG.ko.md +67 -35
  3. package/CHANGELOG.md +195 -134
  4. package/CHANGELOG.zh.md +64 -36
  5. package/README.ja.md +1 -1
  6. package/README.ko.md +1 -1
  7. package/README.md +1 -1
  8. package/README.zh.md +1 -1
  9. package/docs/installation.ja.md +2 -2
  10. package/docs/installation.ko.md +2 -2
  11. package/docs/installation.md +103 -78
  12. package/docs/installation.zh.md +100 -77
  13. package/docs/repair-log.md +54 -0
  14. package/package.json +1 -1
  15. package/packages/selector/lib/{config.js → advisor-state.js} +329 -5
  16. package/packages/selector/lib/client.d.ts +7 -0
  17. package/packages/selector/lib/client.js +33 -3
  18. package/packages/selector/lib/index.d.ts +7 -0
  19. package/packages/selector/lib/index.js +112 -3
  20. package/packages/selector/lib/pruner.d.ts +128 -1
  21. package/packages/selector/lib/pruner.js +2802 -1374
  22. package/packages/selector/src/client/ReviewOverlay.tsx +1 -1
  23. package/packages/selector/src/client/index.ts +1 -1
  24. package/packages/selector/src/client/preset-options.ts +2 -0
  25. package/packages/selector/src/index.ts +129 -49
  26. package/packages/selector/src/profiles.ts +48 -0
  27. package/packages/selector/src/pruner/content.ts +18 -5
  28. package/packages/selector/src/pruner/state.ts +3 -0
  29. package/packages/selector/src/pruner/types.ts +23 -5
  30. package/packages/selector/src/pruner.ts +297 -162
  31. package/packages/selector/src/runtime/adaptive-cost.ts +23 -12
  32. package/packages/selector/src/runtime/audit.ts +40 -2
  33. package/packages/selector/src/runtime/config.ts +88 -1
  34. package/packages/selector/src/runtime/measurement.ts +31 -2
  35. package/packages/selector/src/runtime/reducers.ts +1115 -97
  36. package/packages/selector/src/runtime/tokenpilot/advisor-prompt.ts +188 -0
  37. package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +133 -0
  38. package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -0
  39. package/packages/selector/src/runtime/tokenpilot/dedup.ts +1 -1
  40. package/packages/selector/src/runtime/tokenpilot/estimator.ts +8 -118
  41. package/packages/selector/src/runtime/tokenpilot/locator.ts +1 -1
  42. package/packages/selector/src/runtime/tokenpilot/proposal.ts +76 -32
  43. package/packages/selector/src/runtime/tokenpilot/read-state.ts +23 -2
  44. package/packages/selector/src/runtime/tokenpilot/review-registry.ts +117 -0
  45. package/packages/selector/src/runtime/tokenpilot/sidechannel.ts +303 -0
  46. package/packages/selector/src/runtime/toolclass.ts +103 -0
  47. package/packages/selector/src/runtime/types.ts +37 -0
  48. package/packages/selector/tests/advisor-report.host.spec.ts +223 -0
  49. package/packages/selector/tests/public/package-contract.client.spec.ts +2 -1
  50. package/packages/selector/tests/review-routes-registry.host.spec.ts +142 -0
  51. package/packages/selector/tests/runtime/adaptive-cost.spec.ts +7 -7
  52. package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -0
  53. package/packages/selector/tests/runtime/advisor.spec.ts +226 -0
  54. package/packages/selector/tests/runtime/audit.spec.ts +88 -1
  55. package/packages/selector/tests/runtime/char-basis.spec.ts +30 -0
  56. package/packages/selector/tests/runtime/code-skeleton.spec.ts +14 -3
  57. package/packages/selector/tests/runtime/frequency-longstrings.spec.ts +74 -0
  58. package/packages/selector/tests/runtime/html-reducer.spec.ts +212 -0
  59. package/packages/selector/tests/runtime/line-mapping.spec.ts +153 -0
  60. package/packages/selector/tests/runtime/prose-reducers.spec.ts +133 -0
  61. package/packages/selector/tests/runtime/public/public-runtime.spec.ts +198 -27
  62. package/packages/selector/tests/runtime/read-input-cap.spec.ts +33 -0
  63. package/packages/selector/tests/runtime/search-reducer.spec.ts +110 -0
  64. package/packages/selector/tests/runtime/sidechannel.spec.ts +241 -0
  65. package/packages/selector/tests/runtime/toc-and-bundled.spec.ts +159 -0
  66. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +12 -0
  67. package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +194 -0
  68. package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +70 -1
  69. package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +24 -0
  70. package/packages/selector/tests/runtime/toolclass.spec.ts +156 -0
  71. package/scripts/toolclass-corpus-replay.mjs +281 -0
@@ -394,7 +394,13 @@ function parsePresetOptionsSettings(value) {
394
394
  "reviewMode",
395
395
  "reviewTimeoutTurns",
396
396
  "cacheHitDiscountAlpha",
397
- "reviewHighImpactTokens"
397
+ "reviewHighImpactTokens",
398
+ "advisorMode",
399
+ "advisorTimeoutMs",
400
+ "advisorRefreshTurns",
401
+ "advisorScoreThreshold",
402
+ "advisorSampleLimit",
403
+ "advisorMinTokens"
398
404
  ]);
399
405
  const unknown = Object.keys(value).find((key) => !allowed.has(key));
400
406
  if (unknown !== void 0) throw new TypeError(`Context-compression presetOptions: unknown key "${unknown}"`);
@@ -410,6 +416,18 @@ function parsePresetOptionsSettings(value) {
410
416
  }
411
417
  const estimatorMode = value.estimatorMode;
412
418
  if (estimatorMode !== void 0 && estimatorMode !== "" && estimatorMode !== "host" && estimatorMode !== "direct") throw new TypeError("Context-compression presetOptions.estimatorMode must be \"\", \"host\", or \"direct\"");
419
+ const advisorMode = value.advisorMode;
420
+ if (advisorMode !== void 0 && advisorMode !== "" && advisorMode !== "host" && advisorMode !== "direct") throw new TypeError("Context-compression presetOptions.advisorMode must be \"\", \"host\", or \"direct\"");
421
+ const advisorTimeoutMs = value.advisorTimeoutMs;
422
+ if (advisorTimeoutMs !== void 0 && (typeof advisorTimeoutMs !== "number" || !Number.isSafeInteger(advisorTimeoutMs) || advisorTimeoutMs < 100 || advisorTimeoutMs > 6e4)) throw new TypeError("Context-compression presetOptions.advisorTimeoutMs must be an integer between 100 and 60000");
423
+ const advisorRefreshTurns = value.advisorRefreshTurns;
424
+ if (advisorRefreshTurns !== void 0 && (typeof advisorRefreshTurns !== "number" || !Number.isSafeInteger(advisorRefreshTurns) || advisorRefreshTurns < 1)) throw new TypeError("Context-compression presetOptions.advisorRefreshTurns must be an integer of at least 1");
425
+ const advisorScoreThreshold = value.advisorScoreThreshold;
426
+ if (advisorScoreThreshold !== void 0 && (typeof advisorScoreThreshold !== "number" || !Number.isFinite(advisorScoreThreshold) || advisorScoreThreshold <= 0 || advisorScoreThreshold >= 1)) throw new TypeError("Context-compression presetOptions.advisorScoreThreshold must be a number strictly between 0 and 1");
427
+ const advisorSampleLimit = value.advisorSampleLimit;
428
+ if (advisorSampleLimit !== void 0 && (typeof advisorSampleLimit !== "number" || !Number.isSafeInteger(advisorSampleLimit) || advisorSampleLimit < 1 || advisorSampleLimit > 64)) throw new TypeError("Context-compression presetOptions.advisorSampleLimit must be an integer between 1 and 64");
429
+ const advisorMinTokens = value.advisorMinTokens;
430
+ if (advisorMinTokens !== void 0 && (typeof advisorMinTokens !== "number" || !Number.isSafeInteger(advisorMinTokens) || advisorMinTokens < 1)) throw new TypeError("Context-compression presetOptions.advisorMinTokens must be a positive integer");
413
431
  const estimatorTimeoutMs = value.estimatorTimeoutMs;
414
432
  if (estimatorTimeoutMs !== void 0 && (typeof estimatorTimeoutMs !== "number" || !Number.isSafeInteger(estimatorTimeoutMs) || estimatorTimeoutMs < 100 || estimatorTimeoutMs > 6e4)) throw new TypeError("Context-compression presetOptions.estimatorTimeoutMs must be an integer between 100 and 60000");
415
433
  const reviewTimeoutTurns = value.reviewTimeoutTurns;
@@ -442,6 +460,12 @@ function parsePresetOptionsSettings(value) {
442
460
  if (reviewTimeoutTurns !== void 0) result.reviewTimeoutTurns = reviewTimeoutTurns;
443
461
  if (cacheHitDiscountAlpha !== void 0) result.cacheHitDiscountAlpha = cacheHitDiscountAlpha;
444
462
  if (reviewHighImpactTokens !== void 0) result.reviewHighImpactTokens = reviewHighImpactTokens;
463
+ if (advisorMode !== void 0) result.advisorMode = advisorMode;
464
+ if (advisorTimeoutMs !== void 0) result.advisorTimeoutMs = advisorTimeoutMs;
465
+ if (advisorRefreshTurns !== void 0) result.advisorRefreshTurns = advisorRefreshTurns;
466
+ if (advisorScoreThreshold !== void 0) result.advisorScoreThreshold = advisorScoreThreshold;
467
+ if (advisorSampleLimit !== void 0) result.advisorSampleLimit = advisorSampleLimit;
468
+ if (advisorMinTokens !== void 0) result.advisorMinTokens = advisorMinTokens;
445
469
  return result;
446
470
  }
447
471
  /** Settings schema used by the user-facing profile selector. */
@@ -518,6 +542,7 @@ const CONFIG_KEYS = /* @__PURE__ */ new Set([
518
542
  "historyKeepRecentToolCalls",
519
543
  "historyKeepRecentTokens",
520
544
  "historyMinReclaimTokens",
545
+ "readInputCapChars",
521
546
  "autoCompactThresholdPercent",
522
547
  "presetOptions"
523
548
  ]);
@@ -542,6 +567,15 @@ function codePointLength(text) {
542
567
  for (const _point of text) length++;
543
568
  return length;
544
569
  }
570
+ /** Express one token-named policy gate on the character basis. */
571
+ function charsForTokens(tokens) {
572
+ return tokens * 4;
573
+ }
574
+ /** Derive the telemetry-only token figure from a character measurement. */
575
+ function charsToTokens(chars) {
576
+ if (!Number.isFinite(chars) || chars <= 0) return 0;
577
+ return Math.max(1, Math.round(chars / 4));
578
+ }
545
579
  /**
546
580
  * Test whether a settings value names a supported compression profile.
547
581
  * @param value - untrusted settings value.
@@ -575,6 +609,7 @@ function resolveConfig(config = {}) {
575
609
  ...config.historyKeepRecentToolCalls === void 0 ? {} : { historyKeepRecentToolCalls: config.historyKeepRecentToolCalls },
576
610
  ...config.historyKeepRecentTokens === void 0 ? {} : { historyKeepRecentTokens: config.historyKeepRecentTokens },
577
611
  ...config.historyMinReclaimTokens === void 0 ? {} : { historyMinReclaimTokens: config.historyMinReclaimTokens },
612
+ ...config.readInputCapChars === void 0 ? {} : { readInputCapChars: config.readInputCapChars },
578
613
  ...config.autoCompactThresholdPercent === void 0 ? {} : { autoCompactThresholdPercent: config.autoCompactThresholdPercent },
579
614
  ...config.presetOptions === void 0 ? {} : { presetOptions: config.presetOptions }
580
615
  };
@@ -589,7 +624,8 @@ function resolveConfig(config = {}) {
589
624
  "aggregateTriggerTokens",
590
625
  "aggregateTargetTokens",
591
626
  "historyTriggerTokens",
592
- "historyMinReclaimTokens"
627
+ "historyMinReclaimTokens",
628
+ "readInputCapChars"
593
629
  ]) {
594
630
  const value = resolved[key];
595
631
  if (value !== void 0) assertPositiveInteger(key, value);
@@ -648,7 +684,15 @@ const PRESET_OPTION_DEFAULTS = deepFreeze({
648
684
  reviewMode: false,
649
685
  reviewTimeoutTurns: 6,
650
686
  cacheHitDiscountAlpha: .1,
651
- reviewHighImpactTokens: 4e3
687
+ reviewHighImpactTokens: 4e3,
688
+ advisor: {
689
+ mode: "",
690
+ timeoutMs: 8e3,
691
+ refreshTurns: 8,
692
+ scoreThreshold: .35,
693
+ sampleLimit: 16,
694
+ minTokens: 250
695
+ }
652
696
  });
653
697
  /**
654
698
  * Merge persisted presetOptions overrides over the tokenpilot-inspired
@@ -668,7 +712,15 @@ function mergePresetOptions(overrides) {
668
712
  reviewMode: overrides.reviewMode ?? PRESET_OPTION_DEFAULTS.reviewMode,
669
713
  reviewTimeoutTurns: overrides.reviewTimeoutTurns ?? PRESET_OPTION_DEFAULTS.reviewTimeoutTurns,
670
714
  cacheHitDiscountAlpha: overrides.cacheHitDiscountAlpha ?? PRESET_OPTION_DEFAULTS.cacheHitDiscountAlpha,
671
- reviewHighImpactTokens: overrides.reviewHighImpactTokens ?? PRESET_OPTION_DEFAULTS.reviewHighImpactTokens
715
+ reviewHighImpactTokens: overrides.reviewHighImpactTokens ?? PRESET_OPTION_DEFAULTS.reviewHighImpactTokens,
716
+ advisor: {
717
+ mode: overrides.advisorMode ?? PRESET_OPTION_DEFAULTS.advisor.mode,
718
+ timeoutMs: overrides.advisorTimeoutMs ?? PRESET_OPTION_DEFAULTS.advisor.timeoutMs,
719
+ refreshTurns: overrides.advisorRefreshTurns ?? PRESET_OPTION_DEFAULTS.advisor.refreshTurns,
720
+ scoreThreshold: overrides.advisorScoreThreshold ?? PRESET_OPTION_DEFAULTS.advisor.scoreThreshold,
721
+ sampleLimit: overrides.advisorSampleLimit ?? PRESET_OPTION_DEFAULTS.advisor.sampleLimit,
722
+ minTokens: overrides.advisorMinTokens ?? PRESET_OPTION_DEFAULTS.advisor.minTokens
723
+ }
672
724
  });
673
725
  }
674
726
  /**
@@ -840,6 +892,7 @@ function resolvePolicy(config, profile, custom = DEFAULT_CUSTOM_COMPRESSION_POLI
840
892
  historyKeepRecentToolCalls: config.historyKeepRecentToolCalls ?? preset.historyKeepRecentToolCalls,
841
893
  historyKeepRecentTokens: config.historyKeepRecentTokens ?? linkage?.historyKeepRecentTokens ?? preset.historyKeepRecentTokens,
842
894
  historyMinReclaimTokens: config.historyMinReclaimTokens ?? linkage?.historyMinReclaimTokens ?? preset.historyMinReclaimTokens,
895
+ ...config.readInputCapChars === void 0 ? {} : { readInputCapChars: config.readInputCapChars },
843
896
  ...linkage === void 0 ? {} : {
844
897
  autoCompactTokens: linkage.autoCompactTokens,
845
898
  microDeadlineTokens: linkage.microDeadlineTokens
@@ -849,6 +902,7 @@ function resolvePolicy(config, profile, custom = DEFAULT_CUSTOM_COMPRESSION_POLI
849
902
  if (policy.nativeTargetTokens >= policy.nativeTriggerTokens && profile === "native") throw new Error("context compression policy: native target must be below trigger");
850
903
  if (policy.freshTargetTokens >= policy.freshTriggerTokens && policy.freshEnabled) throw new Error("context compression policy: fresh target must be below trigger");
851
904
  if (policy.aggregateTargetTokens >= policy.aggregateTriggerTokens && policy.freshEnabled) throw new Error("context compression policy: aggregate target must be below trigger");
905
+ if (policy.readInputCapChars !== void 0 && policy.readInputCapChars <= policy.freshTriggerTokens * 4) throw new Error("context compression policy: read input cap would silence the fresh path");
852
906
  return deepFreeze(policy);
853
907
  }
854
908
  function assertTargetBelowTrigger(label, target, trigger) {
@@ -862,4 +916,274 @@ function assertNonNegativeInteger(name, value) {
862
916
  if (!Number.isSafeInteger(value) || value < 0) throw new Error(`ToolResultPruneConfig: ${name} (${String(value)}) must be a non-negative safe integer`);
863
917
  }
864
918
  //#endregion
865
- export { COMPRESSION_PROFILES as _, PRUNE_MARKER as a, isValidAutoCompactThresholdPercent as c, resolvePolicy as d, CustomCompressionPolicySchema as f, deepFreeze as g, assertNever as h, DEFAULTS as i, parseContextCompressionSettings as l, resolveCustomPolicy as m, CONTEXT_COMPRESSION_SETTINGS_NAMESPACE as n, codePointLength as o, DEFAULT_CUSTOM_COMPRESSION_POLICY as p, ContextCompressionSettingsSchema as r, isCompressionProfile as s, AUTO_COMPACT_THRESHOLD_LIMITS as t, resolveConfig as u };
919
+ //#region src/runtime/tokenpilot/review-queue.ts
920
+ /** In-memory store: the fail-open fallback when no durable seam is available. */
921
+ var MemoryReviewStore = class {
922
+ sessions = /* @__PURE__ */ new Map();
923
+ load(sessionId) {
924
+ return this.sessions.get(sessionId);
925
+ }
926
+ save(sessionId, record) {
927
+ this.sessions.set(sessionId, record);
928
+ }
929
+ ids() {
930
+ return [...this.sessions.keys()];
931
+ }
932
+ };
933
+ var ReviewQueue = class {
934
+ store;
935
+ options;
936
+ constructor(store, options) {
937
+ this.store = store;
938
+ this.options = options;
939
+ }
940
+ /**
941
+ * Fail-open store access: a throwing seam must never break the compression
942
+ * pipeline. Reads degrade to "no stored record"; writes degrade to losing
943
+ * durability for that call (the store itself is expected to warn).
944
+ */
945
+ safeLoad(sessionId) {
946
+ try {
947
+ return this.store.load(sessionId);
948
+ } catch {
949
+ return;
950
+ }
951
+ }
952
+ safeSave(sessionId, record) {
953
+ try {
954
+ this.store.save(sessionId, record);
955
+ } catch {}
956
+ }
957
+ sessionRecord(sessionId) {
958
+ return this.safeLoad(sessionId) ?? {
959
+ version: 1,
960
+ proposals: []
961
+ };
962
+ }
963
+ /**
964
+ * Queue one classified proposal. A repeated classification of the same
965
+ * content re-does nothing but refresh the patience clock, so re-enqueue
966
+ * cannot duplicate a live proposal.
967
+ * @returns `false` when an identical live proposal already exists.
968
+ */
969
+ enqueue(sessionId, skeleton, turnIndex) {
970
+ const record = this.sessionRecord(sessionId);
971
+ const existing = record.proposals.find((entry) => entry.id === skeleton.id);
972
+ if (existing !== void 0 && existing.status !== "expired") {
973
+ existing.lastTurnIndex = turnIndex;
974
+ this.safeSave(sessionId, record);
975
+ return false;
976
+ }
977
+ const proposal = {
978
+ id: skeleton.id,
979
+ sessionId,
980
+ kind: skeleton.kind,
981
+ items: skeleton.items.map((item) => ({ ...item })),
982
+ benefit: { ...skeleton.benefit },
983
+ status: "pending",
984
+ enqueuedTurn: turnIndex,
985
+ lastTurnIndex: turnIndex
986
+ };
987
+ this.safeSave(sessionId, {
988
+ version: 1,
989
+ proposals: [...record.proposals.filter((entry) => entry.id !== skeleton.id), proposal]
990
+ });
991
+ return true;
992
+ }
993
+ /** Live pending proposals of one session, oldest enqueue first. */
994
+ listPending(sessionId) {
995
+ return this.sessionRecord(sessionId).proposals.filter((entry) => entry.status === "pending").sort((left, right) => left.enqueuedTurn - right.enqueuedTurn);
996
+ }
997
+ /** Approved proposals waiting for the next turn-boundary batch. */
998
+ listApproved(sessionId) {
999
+ return this.sessionRecord(sessionId).proposals.filter((entry) => entry.status === "approved").sort((left, right) => left.enqueuedTurn - right.enqueuedTurn);
1000
+ }
1001
+ /**
1002
+ * Transition one pending proposal. Idempotent: deciding an unknown id or a
1003
+ * non-pending proposal changes nothing and reports the miss.
1004
+ */
1005
+ decide(sessionId, id, decision) {
1006
+ const record = this.sessionRecord(sessionId);
1007
+ const proposal = record.proposals.find((entry) => entry.id === id);
1008
+ if (proposal === void 0) return {
1009
+ ok: false,
1010
+ reason: "unknown-proposal"
1011
+ };
1012
+ if (proposal.status !== "pending") return {
1013
+ ok: false,
1014
+ reason: "not-pending"
1015
+ };
1016
+ proposal.status = decision;
1017
+ this.safeSave(sessionId, record);
1018
+ return { ok: true };
1019
+ }
1020
+ /**
1021
+ * Expire every pending proposal whose patience has run out at this turn
1022
+ * boundary. Expired proposals are removed from the store (the summary view
1023
+ * aggregates them from the audit log instead).
1024
+ * @returns the expired proposals, for the caller's audit emission.
1025
+ */
1026
+ expireTurn(sessionId, turnIndex) {
1027
+ const record = this.sessionRecord(sessionId);
1028
+ const keep = [];
1029
+ const expired = [];
1030
+ for (const proposal of record.proposals) {
1031
+ if (proposal.status === "pending" && turnIndex - proposal.lastTurnIndex > this.options.timeoutTurns) {
1032
+ expired.push({
1033
+ ...proposal,
1034
+ status: "expired"
1035
+ });
1036
+ continue;
1037
+ }
1038
+ keep.push(proposal);
1039
+ }
1040
+ if (expired.length > 0) this.safeSave(sessionId, {
1041
+ version: 1,
1042
+ proposals: keep
1043
+ });
1044
+ return expired;
1045
+ }
1046
+ /**
1047
+ * Settle an approved proposal with its execution receipt and retire it from
1048
+ * the live store. The caller is responsible for auditing the receipt; the
1049
+ * queue only records which proposal left and why.
1050
+ */
1051
+ recordReceipt(sessionId, id, receipt) {
1052
+ const record = this.sessionRecord(sessionId);
1053
+ const proposal = record.proposals.find((entry) => entry.id === id);
1054
+ if (proposal === void 0 || proposal.status !== "approved") return void 0;
1055
+ this.safeSave(sessionId, {
1056
+ version: 1,
1057
+ proposals: record.proposals.filter((entry) => entry.id !== id)
1058
+ });
1059
+ return {
1060
+ ...proposal,
1061
+ receipt
1062
+ };
1063
+ }
1064
+ };
1065
+ //#endregion
1066
+ //#region src/runtime/tokenpilot/review-registry.ts
1067
+ /**
1068
+ * Scope-independent handle on the live review pipeline.
1069
+ *
1070
+ * The R4 HTTP routes are registered on the plugin's TOP-LEVEL fiber
1071
+ * (`cordis.patch.yml` → `context-compression-improved-estimator-catalog`),
1072
+ * but every `ToolResultPruner` is mounted inside an agent preset's isolated
1073
+ * group — `canonicalCompressionRows()` declares
1074
+ * `isolate: { compaction: true, toolResultPruner: true }` — so
1075
+ * `ctx.get('toolResultPruner')` at the top level is always `undefined` and the
1076
+ * queue route could only ever answer 503 "review pipeline unavailable".
1077
+ *
1078
+ * Ownership, not transport, was in the wrong place: the queue records are
1079
+ * already keyed by session id, so the store belongs to the plugin rather than
1080
+ * to one pruner instance. Every instance shares one store and publishes itself
1081
+ * here, which lets a top-level reader reach whichever instance currently holds
1082
+ * a session's proposals.
1083
+ *
1084
+ * The durable seam already behaves this way — `REVIEW_STORAGE_DOMAIN` /
1085
+ * `REVIEW_STORAGE_TABLE` are constants, so every instance opens the same table.
1086
+ * Only the in-memory fallback was per-instance, and that is what this module
1087
+ * makes shared.
1088
+ *
1089
+ * @module dsh-context-compression-improved/review-registry
1090
+ */
1091
+ /**
1092
+ * The one in-memory fallback every pruner instance starts from. Session ids are
1093
+ * globally unique and `ReviewSessionRecord` is keyed by them, so a single store
1094
+ * is semantically identical to one store per instance — except that a reader
1095
+ * reaching any instance now observes every session.
1096
+ */
1097
+ const sharedStore = new MemoryReviewStore();
1098
+ const live = /* @__PURE__ */ new Set();
1099
+ /** The process-wide review queue store shared by every pruner instance. */
1100
+ function sharedReviewStore() {
1101
+ return sharedStore;
1102
+ }
1103
+ /**
1104
+ * Publish one pruner instance for scope-independent readers.
1105
+ * @param pruner - the instance to publish.
1106
+ * @returns the disposer removing it, for `ctx.effect`.
1107
+ */
1108
+ function registerReviewPruner(pruner) {
1109
+ live.add(pruner);
1110
+ return () => {
1111
+ live.delete(pruner);
1112
+ };
1113
+ }
1114
+ /**
1115
+ * Resolve a live pruner instance for the top-level routes.
1116
+ *
1117
+ * Any instance can serve an aggregate read because the store is shared, and a
1118
+ * session-scoped read is answered from that same store. Instances that have
1119
+ * upgraded to the durable seam read the same table, so the answer does not
1120
+ * depend on which instance this happens to return.
1121
+ *
1122
+ * @returns a live instance, or `undefined` when no preset has been composed yet.
1123
+ */
1124
+ function resolveReviewPruner() {
1125
+ return live.values().next().value;
1126
+ }
1127
+ const advisorStates = /* @__PURE__ */ new WeakMap();
1128
+ /**
1129
+ * The per-session advisor state, created on first touch.
1130
+ * @param session - the session to key the state on (by object identity).
1131
+ */
1132
+ function getAdvisorState(session) {
1133
+ let state = advisorStates.get(session);
1134
+ if (state === void 0) {
1135
+ state = {
1136
+ todoVersion: void 0,
1137
+ summary: void 0,
1138
+ lastSummaryTurn: -1,
1139
+ watermarkSeq: 0,
1140
+ scores: /* @__PURE__ */ new Map(),
1141
+ recertified: /* @__PURE__ */ new Map(),
1142
+ failures: void 0,
1143
+ inFlight: false,
1144
+ lastDecay: void 0
1145
+ };
1146
+ advisorStates.set(session, state);
1147
+ }
1148
+ return state;
1149
+ }
1150
+ /**
1151
+ * Insert or refresh one score with LRU semantics: a re-touched seq moves to
1152
+ * the newest position, and the oldest entry is evicted once the map exceeds
1153
+ * {@link ADVISOR_SCORES_LIMIT}.
1154
+ */
1155
+ function recordScore(state, seq, entry) {
1156
+ state.scores.delete(seq);
1157
+ state.scores.set(seq, entry);
1158
+ if (state.scores.size > 64) {
1159
+ const oldest = state.scores.keys().next();
1160
+ if (oldest.done !== true) state.scores.delete(oldest.value);
1161
+ }
1162
+ }
1163
+ /**
1164
+ * Mark one seq as LLM-recertified low relevance (a suggestion for later
1165
+ * history-aggressiveness decisions, consumed by nothing in this round).
1166
+ * Bounded at {@link ADVISOR_RECERTIFIED_LIMIT} with the same LRU eviction.
1167
+ */
1168
+ function recordRecertified(state, seq, turn) {
1169
+ state.recertified.delete(seq);
1170
+ state.recertified.set(seq, turn);
1171
+ if (state.recertified.size > 64) {
1172
+ const oldest = state.recertified.keys().next();
1173
+ if (oldest.done !== true) state.recertified.delete(oldest.value);
1174
+ }
1175
+ }
1176
+ /**
1177
+ * Drop every cached artifact that depends on the task semantics: a changed
1178
+ * todo version invalidates the summary and makes all eligible candidates
1179
+ * rescore-worthy (the watermark alone would otherwise hide them).
1180
+ */
1181
+ function invalidateOnTaskChange(state, todoVersion) {
1182
+ if (state.todoVersion === todoVersion) return false;
1183
+ state.todoVersion = todoVersion;
1184
+ state.summary = void 0;
1185
+ state.lastSummaryTurn = -1;
1186
+ return true;
1187
+ }
1188
+ //#endregion
1189
+ export { DEFAULT_CUSTOM_COMPRESSION_POLICY as C, COMPRESSION_PROFILES as D, deepFreeze as E, CustomCompressionPolicySchema as S, assertNever as T, isCompressionProfile as _, registerReviewPruner as a, resolveConfig as b, ReviewQueue as c, ContextCompressionSettingsSchema as d, DEFAULTS as f, codePointLength as g, charsToTokens as h, recordScore as i, AUTO_COMPACT_THRESHOLD_LIMITS as l, charsForTokens as m, invalidateOnTaskChange as n, resolveReviewPruner as o, PRUNE_MARKER as p, recordRecertified as r, sharedReviewStore as s, getAdvisorState as t, CONTEXT_COMPRESSION_SETTINGS_NAMESPACE as u, isValidAutoCompactThresholdPercent as v, resolveCustomPolicy as w, resolvePolicy as x, parseContextCompressionSettings as y };
@@ -89,6 +89,13 @@ interface PresetOptionsSettings {
89
89
  readonly reviewTimeoutTurns?: number;
90
90
  readonly cacheHitDiscountAlpha?: number;
91
91
  readonly reviewHighImpactTokens?: number;
92
+ /** Advisory advisor channel; `''` (the default) keeps the advisor off. */
93
+ readonly advisorMode?: '' | 'host' | 'direct';
94
+ readonly advisorTimeoutMs?: number;
95
+ readonly advisorRefreshTurns?: number;
96
+ readonly advisorScoreThreshold?: number;
97
+ readonly advisorSampleLimit?: number;
98
+ readonly advisorMinTokens?: number;
92
99
  }
93
100
  /** Durable settings section owned by this package. */
94
101
  interface ContextCompressionSettings {
@@ -82,7 +82,13 @@ window.__ModuleLoader__.load({
82
82
  "reviewMode",
83
83
  "reviewTimeoutTurns",
84
84
  "cacheHitDiscountAlpha",
85
- "reviewHighImpactTokens"
85
+ "reviewHighImpactTokens",
86
+ "advisorMode",
87
+ "advisorTimeoutMs",
88
+ "advisorRefreshTurns",
89
+ "advisorScoreThreshold",
90
+ "advisorSampleLimit",
91
+ "advisorMinTokens"
86
92
  ]);
87
93
  if (Object.keys(value).some((key) => !allowed.has(key))) return void 0;
88
94
  for (const key of [
@@ -97,6 +103,8 @@ window.__ModuleLoader__.load({
97
103
  }
98
104
  const estimatorMode = value.estimatorMode;
99
105
  if (estimatorMode !== void 0 && estimatorMode !== "" && estimatorMode !== "host" && estimatorMode !== "direct") return;
106
+ const advisorMode = value.advisorMode;
107
+ if (advisorMode !== void 0 && advisorMode !== "" && advisorMode !== "host" && advisorMode !== "direct") return;
100
108
  for (const key of [
101
109
  "estimatorProvider",
102
110
  "estimatorModel",
@@ -114,6 +122,16 @@ window.__ModuleLoader__.load({
114
122
  if (cacheHitDiscountAlpha !== void 0 && (typeof cacheHitDiscountAlpha !== "number" || !Number.isFinite(cacheHitDiscountAlpha) || cacheHitDiscountAlpha <= 0 || cacheHitDiscountAlpha >= 1)) return;
115
123
  const reviewHighImpactTokens = value.reviewHighImpactTokens;
116
124
  if (reviewHighImpactTokens !== void 0 && (typeof reviewHighImpactTokens !== "number" || !Number.isSafeInteger(reviewHighImpactTokens) || reviewHighImpactTokens < 0)) return;
125
+ const advisorTimeoutMs = value.advisorTimeoutMs;
126
+ if (advisorTimeoutMs !== void 0 && (typeof advisorTimeoutMs !== "number" || !Number.isSafeInteger(advisorTimeoutMs) || advisorTimeoutMs < 100 || advisorTimeoutMs > 6e4)) return;
127
+ const advisorRefreshTurns = value.advisorRefreshTurns;
128
+ if (advisorRefreshTurns !== void 0 && (typeof advisorRefreshTurns !== "number" || !Number.isSafeInteger(advisorRefreshTurns) || advisorRefreshTurns < 1)) return;
129
+ const advisorScoreThreshold = value.advisorScoreThreshold;
130
+ if (advisorScoreThreshold !== void 0 && (typeof advisorScoreThreshold !== "number" || !Number.isFinite(advisorScoreThreshold) || advisorScoreThreshold <= 0 || advisorScoreThreshold >= 1)) return;
131
+ const advisorSampleLimit = value.advisorSampleLimit;
132
+ if (advisorSampleLimit !== void 0 && (typeof advisorSampleLimit !== "number" || !Number.isSafeInteger(advisorSampleLimit) || advisorSampleLimit < 1 || advisorSampleLimit > 64)) return;
133
+ const advisorMinTokens = value.advisorMinTokens;
134
+ if (advisorMinTokens !== void 0 && (typeof advisorMinTokens !== "number" || !Number.isSafeInteger(advisorMinTokens) || advisorMinTokens < 1)) return;
117
135
  const decoded = {};
118
136
  if (value.dedupeToolResults !== void 0) decoded.dedupeToolResults = value.dedupeToolResults;
119
137
  if (value.summaryLocator !== void 0) decoded.summaryLocator = value.summaryLocator;
@@ -129,6 +147,12 @@ window.__ModuleLoader__.load({
129
147
  if (reviewTimeoutTurns !== void 0) decoded.reviewTimeoutTurns = reviewTimeoutTurns;
130
148
  if (cacheHitDiscountAlpha !== void 0) decoded.cacheHitDiscountAlpha = cacheHitDiscountAlpha;
131
149
  if (reviewHighImpactTokens !== void 0) decoded.reviewHighImpactTokens = reviewHighImpactTokens;
150
+ if (advisorMode !== void 0) decoded.advisorMode = advisorMode;
151
+ if (advisorTimeoutMs !== void 0) decoded.advisorTimeoutMs = advisorTimeoutMs;
152
+ if (advisorRefreshTurns !== void 0) decoded.advisorRefreshTurns = advisorRefreshTurns;
153
+ if (advisorScoreThreshold !== void 0) decoded.advisorScoreThreshold = advisorScoreThreshold;
154
+ if (advisorSampleLimit !== void 0) decoded.advisorSampleLimit = advisorSampleLimit;
155
+ if (advisorMinTokens !== void 0) decoded.advisorMinTokens = advisorMinTokens;
132
156
  return decoded;
133
157
  }
134
158
  /**
@@ -1544,7 +1568,13 @@ window.__ModuleLoader__.load({
1544
1568
  "reviewMode",
1545
1569
  "reviewTimeoutTurns",
1546
1570
  "cacheHitDiscountAlpha",
1547
- "reviewHighImpactTokens"
1571
+ "reviewHighImpactTokens",
1572
+ "advisorMode",
1573
+ "advisorTimeoutMs",
1574
+ "advisorRefreshTurns",
1575
+ "advisorScoreThreshold",
1576
+ "advisorSampleLimit",
1577
+ "advisorMinTokens"
1548
1578
  ];
1549
1579
  /**
1550
1580
  * Plan the path ops one patch needs against the section currently stored.
@@ -1768,7 +1798,7 @@ window.__ModuleLoader__.load({
1768
1798
  }
1769
1799
  };
1770
1800
  pull();
1771
- let unsubscribe = () => {};
1801
+ let unsubscribe;
1772
1802
  try {
1773
1803
  unsubscribe = scope.subscribe(pull);
1774
1804
  } catch {
@@ -24,6 +24,13 @@ interface Config {
24
24
  * transport and simply never appears.
25
25
  */
26
26
  reviewQueueRoute?: boolean;
27
+ /**
28
+ * Register the advisory advisor's read-only HTTP report route (decay
29
+ * figure, task summary, score distribution). Same Bundle opt-in semantics
30
+ * as `reviewQueueRoute`; the advisor itself stays off until the user turns
31
+ * it on through the `presetOptions.advisor*` settings keys.
32
+ */
33
+ advisorReportRoute?: boolean;
27
34
  }
28
35
  /** Loader validation for the standalone Bundle opt-in. */
29
36
  declare const Config: z<Config>;
@@ -1,4 +1,4 @@
1
- import { n as CONTEXT_COMPRESSION_SETTINGS_NAMESPACE, r as ContextCompressionSettingsSchema } from "./config.js";
1
+ import { d as ContextCompressionSettingsSchema, o as resolveReviewPruner, t as getAdvisorState, u as CONTEXT_COMPRESSION_SETTINGS_NAMESPACE } from "./advisor-state.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import "@deepseek-ai/dsh-settings";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -459,9 +459,20 @@ const CONTEXT_COMPRESSION_NAMESPACE = CONTEXT_COMPRESSION_SETTINGS_NAMESPACE;
459
459
  const ESTIMATOR_CATALOG_ROUTES = ["/endpoint/dsh-context-compression-improved/estimator-catalog", "/api/dsh-context-compression-improved/estimator-catalog"];
460
460
  const REVIEW_QUEUE_ROUTES = ["/endpoint/dsh-context-compression-improved/review-queue", "/api/dsh-context-compression-improved/review-queue"];
461
461
  const REVIEW_DECIDE_ROUTES = ["/endpoint/dsh-context-compression-improved/review-decide", "/api/dsh-context-compression-improved/review-decide"];
462
+ const ADVISOR_REPORT_ROUTES = ["/endpoint/dsh-context-compression-improved/advisor-report", "/api/dsh-context-compression-improved/advisor-report"];
463
+ /**
464
+ * Resolve the review pipeline for the top-level routes.
465
+ *
466
+ * A top-level `toolResultPruner` service wins when a deployment actually mounts
467
+ * one, but in production every pruner lives inside an agent preset's isolated
468
+ * group, so the registry is the path that resolves. Without the fallback the
469
+ * queue route answered 503 "review pipeline unavailable" on every request while
470
+ * the review pipeline itself was running normally.
471
+ */
462
472
  function reviewPrunerOf(readService) {
463
473
  const candidate = readService("toolResultPruner");
464
- return typeof candidate?.listReviewProposals === "function" && typeof candidate?.decideReviewProposal === "function" ? candidate : void 0;
474
+ if (typeof candidate?.listReviewProposals === "function" && typeof candidate?.decideReviewProposal === "function") return candidate;
475
+ return resolveReviewPruner();
465
476
  }
466
477
  function sessionFor(readService, sessionId) {
467
478
  const agents = readService("agents");
@@ -684,6 +695,102 @@ function registerReviewQueueRoutes(ctx) {
684
695
  log("warn", "context-compression webServer not active yet — review routes pending: %s", REVIEW_QUEUE_ROUTES.join(", "));
685
696
  }
686
697
  /**
698
+ * Serve the advisory advisor's read-only report route (same registration
699
+ * skeleton as the review routes):
700
+ *
701
+ * `GET .../advisor-report?sessionId=…` → the session's prefix-decay figure,
702
+ * the todolist-bound task summary, and the score distribution. Content-free
703
+ * by construction: task semantics are LLM-derived summaries, never message
704
+ * text, and no score reason or candidate preview is ever returned.
705
+ * Unknown session → 404; no agents service → 503. A session whose advisor
706
+ * never ran reports nulls and empty arrays, not an error.
707
+ */
708
+ function registerAdvisorReportRoute(ctx) {
709
+ const readService = (name) => {
710
+ try {
711
+ return ctx.get(name);
712
+ } catch {
713
+ return;
714
+ }
715
+ };
716
+ const log = (level, message, ...args) => {
717
+ console[level](message, ...args);
718
+ };
719
+ const getHandler = (req, res) => {
720
+ if (typeof readService("agents")?.get !== "function") {
721
+ reviewJson(res, 503, {
722
+ ok: false,
723
+ error: "advisor report unavailable"
724
+ });
725
+ return;
726
+ }
727
+ let sessionId = "";
728
+ try {
729
+ sessionId = new URL(String(req.url ?? ""), "http://localhost").searchParams.get("sessionId") ?? "";
730
+ } catch {}
731
+ if (sessionId === "") {
732
+ reviewJson(res, 400, {
733
+ ok: false,
734
+ error: "sessionId is required"
735
+ });
736
+ return;
737
+ }
738
+ const session = sessionFor(readService, sessionId);
739
+ if (session === void 0) {
740
+ reviewJson(res, 404, {
741
+ ok: false,
742
+ error: "unknown session"
743
+ });
744
+ return;
745
+ }
746
+ const state = getAdvisorState(session);
747
+ reviewJson(res, 200, {
748
+ ok: true,
749
+ sessionId,
750
+ advisor: {
751
+ summary: state.summary ?? null,
752
+ decay: state.lastDecay?.decay ?? null,
753
+ weightedChars: state.lastDecay?.weightedChars ?? null,
754
+ decayTurn: state.lastDecay?.turn ?? null,
755
+ scores: [...state.scores].map(([seq, entry]) => ({
756
+ seq,
757
+ score: entry.score,
758
+ turn: entry.turn
759
+ })),
760
+ lowRelevanceSeqs: [...state.recertified.keys()]
761
+ }
762
+ });
763
+ };
764
+ const register = (webServer) => {
765
+ const disposers = [...ADVISOR_REPORT_ROUTES].map((path) => ({
766
+ path,
767
+ handler: getHandler
768
+ })).map((entry) => webServer.register({
769
+ kind: "exact",
770
+ path: entry.path,
771
+ handler: entry.handler
772
+ })).filter((off) => typeof off === "function");
773
+ ctx.effect(() => () => {
774
+ for (const off of disposers) off();
775
+ }, "contextCompressionSelector.advisor report route");
776
+ log("info", "context-compression advisor report route registered: %s", ADVISOR_REPORT_ROUTES.join(", "));
777
+ };
778
+ const active = asWebServer(readService("webServer"));
779
+ if (active !== void 0) {
780
+ register(active);
781
+ return;
782
+ }
783
+ ctx.inject(["webServer"], (injected) => {
784
+ const webServer = asWebServer(injected.webServer);
785
+ if (webServer === void 0) {
786
+ log("warn", "context-compression webServer exposes no register() — advisor report route not registered");
787
+ return;
788
+ }
789
+ register(webServer);
790
+ });
791
+ log("warn", "context-compression webServer not active yet — advisor report route pending: %s", ADVISOR_REPORT_ROUTES.join(", "));
792
+ }
793
+ /**
687
794
  * The one service the catalog route actually needs. `llm` and
688
795
  * `agentDefaultModel` are payload enrichment the handler resolves per request,
689
796
  * never reasons to withhold the route.
@@ -787,7 +894,8 @@ const SHARED_SETTINGS = Symbol.for("dsh-context-compression-improved/settings-re
787
894
  const Config = z.object({
788
895
  presetOverlay: z.boolean().default(false),
789
896
  estimatorCatalogRoute: z.boolean().default(false),
790
- reviewQueueRoute: z.boolean().default(false)
897
+ reviewQueueRoute: z.boolean().default(false),
898
+ advisorReportRoute: z.boolean().default(false)
791
899
  });
792
900
  /** Register the persisted default read by the currently mounted root pruner. */
793
901
  function apply(ctx, config = {}) {
@@ -797,6 +905,7 @@ function apply(ctx, config = {}) {
797
905
  });
798
906
  if (config.estimatorCatalogRoute === true) registerEstimatorCatalogRoute(ctx);
799
907
  if (config.reviewQueueRoute === true) registerReviewQueueRoutes(ctx);
908
+ if (config.advisorReportRoute === true) registerAdvisorReportRoute(ctx);
800
909
  if (config.presetOverlay !== true) return;
801
910
  ctx.inject(["agentPresets"], (presetsCtx) => {
802
911
  const installation = decorateAgentPresets(presetsCtx.agentPresets, {