dsh-context-compression-improved 0.2.1 → 0.3.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 (80) hide show
  1. package/.gitattributes +1 -1
  2. package/.github/workflows/ci.yml +1 -7
  3. package/CHANGELOG.ja.md +18 -21
  4. package/CHANGELOG.ko.md +17 -21
  5. package/CHANGELOG.md +16 -17
  6. package/CHANGELOG.zh.md +15 -18
  7. package/CONTRIBUTING.md +1 -1
  8. package/README.ja.md +1 -20
  9. package/README.ko.md +12 -30
  10. package/README.md +1 -19
  11. package/README.zh.md +5 -21
  12. package/THIRD_PARTY_NOTICES.md +2 -2
  13. package/docs/compat-0.1.5-report.md +77 -0
  14. package/docs/installation.ja.md +6 -4
  15. package/docs/installation.ko.md +9 -7
  16. package/docs/installation.md +3 -1
  17. package/docs/installation.zh.md +3 -2
  18. package/docs/repair-log.md +54 -33
  19. package/eslint.config.js +2 -2
  20. package/package.json +56 -36
  21. package/packages/selector/README.md +7 -12
  22. package/packages/selector/README.zh.md +6 -13
  23. package/packages/selector/THIRD_PARTY_NOTICES.md +2 -8
  24. package/packages/selector/cordis.patch.yml +7 -5
  25. package/packages/selector/dsh.plugin.json +2 -2
  26. package/packages/selector/lib/client.d.ts +31 -5
  27. package/packages/selector/lib/client.js +100 -82
  28. package/packages/selector/lib/index.d.ts +8 -20
  29. package/packages/selector/lib/index.js +9 -14
  30. package/packages/selector/lib/invariant.js +4 -4
  31. package/packages/selector/lib/pruner.d.ts +13 -4
  32. package/packages/selector/lib/pruner.js +91 -46
  33. package/packages/selector/lib/tail-trim.js +13 -4
  34. package/packages/selector/package.json +23 -23
  35. package/packages/selector/screenshots.json +1 -4
  36. package/packages/selector/src/client/CompressionProfileControls.tsx +5 -8
  37. package/packages/selector/src/client/CompressionProfileSelector.tsx +9 -5
  38. package/packages/selector/src/client/EstimatorControls.tsx +28 -32
  39. package/packages/selector/src/client/index.ts +101 -63
  40. package/packages/selector/src/client/locales.ts +0 -2
  41. package/packages/selector/src/client/preset-options.ts +49 -35
  42. package/packages/selector/src/client/settings-section.tsx +4 -6
  43. package/packages/selector/src/deepseek-v4-tokenizer.ts +0 -5
  44. package/packages/selector/src/index.ts +41 -46
  45. package/packages/selector/src/invariant.ts +5 -5
  46. package/packages/selector/src/pruner.ts +67 -36
  47. package/packages/selector/src/runtime/measurement.ts +6 -2
  48. package/packages/selector/src/runtime/session-events.ts +14 -1
  49. package/packages/selector/src/runtime/tail-trim.ts +4 -4
  50. package/packages/selector/src/runtime/tokenpilot/estimator.ts +34 -3
  51. package/packages/selector/src/runtime/types.ts +1 -1
  52. package/packages/selector/tests/auto-compact.client.spec.tsx +0 -13
  53. package/packages/selector/tests/built/client-artifact.spec.ts +5 -1
  54. package/packages/selector/tests/code-skeleton.client.spec.ts +2 -1
  55. package/packages/selector/tests/custom-contract.client.spec.ts +2 -1
  56. package/packages/selector/tests/estimator-channel.client.spec.tsx +16 -11
  57. package/packages/selector/tests/estimator-route-registration.host.spec.ts +12 -0
  58. package/packages/selector/tests/host-preset-overlay.host.spec.ts +6 -4
  59. package/packages/selector/tests/preset-options-write.client.spec.ts +87 -79
  60. package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +17 -12
  61. package/packages/selector/tests/preset-overlay.host.spec.ts +5 -2
  62. package/packages/selector/tests/profiles.client.spec.tsx +3 -3
  63. package/packages/selector/tests/public/package-contract.client.spec.ts +27 -3
  64. package/packages/selector/tests/runtime/public/public-runtime.spec.ts +103 -81
  65. package/packages/selector/tests/runtime/session-events.spec.ts +1 -1
  66. package/packages/selector/tests/settings-seat.client.spec.ts +86 -0
  67. package/packages/selector/tests/standing-generation.host.spec.ts +88 -71
  68. package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +24 -21
  69. package/pnpm-workspace.yaml +52 -5
  70. package/scripts/capture-profile-baseline.mjs +42 -0
  71. package/scripts/generate-tokenizer-fixtures.py +5 -5
  72. package/scripts/generate-vision-fixtures.py +2 -2
  73. package/scripts/{packed-components-smoke.ts → packed-components-smoke.mjs} +116 -119
  74. package/scripts/{packed-install-e2e.ts → packed-install-e2e.mjs} +188 -160
  75. package/scripts/{verify-release.ts → verify-release.mjs} +70 -94
  76. package/tests/TEST_INVENTORY.md +6 -7
  77. package/scripts/capture-profile-baseline.ts +0 -80
  78. package/tsconfig.scripts.json +0 -13
  79. /package/{packages/selector/assets/screenshots → docs/assets}/context-compression-selector-profiles.jpg +0 -0
  80. /package/{packages/selector/assets/screenshots → docs/assets}/context-compression-selector-settings.png +0 -0
@@ -734,6 +734,9 @@ window.__ModuleLoader__.load({
734
734
  * exactly that — so keep the heading and its anchor id in place and spend them
735
735
  * on the reason plus the profile that unlocks the card. The gate itself is
736
736
  * unchanged: no estimator control exists outside tokenpilot-inspired.
737
+ *
738
+ * Exported from the estimator card's own module so the gated card and its
739
+ * explanation cannot drift apart.
737
740
  */
738
741
  function EstimatorInactiveNotice({ profile, t }) {
739
742
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
@@ -749,17 +752,17 @@ window.__ModuleLoader__.load({
749
752
  })]
750
753
  });
751
754
  }
752
- const ESTIMATOR_CATALOG_ROUTES = ["/api/dsh-context-compression-improved/estimator-catalog"];
755
+ const ESTIMATOR_CATALOG_ROUTE = "/api/dsh-context-compression-improved/estimator-catalog";
753
756
  /**
754
757
  * TokenPilot-inspired estimator channel card. Shown only while the
755
758
  * tokenpilot-inspired profile is selected, and split by channel:
756
759
  *
757
- * - `host` reuses the providers and credentials already configured in DSH
758
- * through the Harness `llm` service, so this card names a provider and a
760
+ * - `host` reuses the providers and credentials the user already configured in
761
+ * DSH through the Harness `llm` service, so this card names a provider and a
759
762
  * model and accepts NO API key — the key field belongs to the direct channel
760
763
  * alone.
761
- * - `direct` talks to a native OpenAI-compatible endpoint, the only channel
762
- * carrying its own base URL and write-only key.
764
+ * - `direct` talks to a native OpenAI-compatible endpoint, which is the only
765
+ * channel that carries its own base URL and write-only key.
763
766
  *
764
767
  * The whole card is advisory: an unconfigured or failing endpoint keeps every
765
768
  * consumer on its rule-only fallback.
@@ -775,15 +778,17 @@ window.__ModuleLoader__.load({
775
778
  if (mode !== "host") return;
776
779
  let alive = true;
777
780
  let attempts = 0;
778
- const load = async (routes) => {
779
- for (const route of routes) try {
780
- const response = await fetch(route, { headers: { "cache-control": "no-cache" } });
781
- if (response.ok) return await response.json();
782
- } catch {}
781
+ const load = async () => {
782
+ try {
783
+ const response = await fetch(ESTIMATOR_CATALOG_ROUTE, { headers: { "cache-control": "no-cache" } });
784
+ return response.ok ? await response.json() : void 0;
785
+ } catch {
786
+ return;
787
+ }
783
788
  };
784
789
  const tick = () => {
785
790
  attempts += 1;
786
- load(ESTIMATOR_CATALOG_ROUTES).then((body) => {
791
+ load().then((body) => {
787
792
  if (!alive) return;
788
793
  if (body !== void 0 && (body.providers?.length ?? 0) > 0) {
789
794
  setCatalog(body);
@@ -935,7 +940,6 @@ window.__ModuleLoader__.load({
935
940
  type: "text",
936
941
  value: model,
937
942
  disabled,
938
- placeholder: t("estimator.model.placeholder"),
939
943
  onChange: (event) => {
940
944
  setModel(event.currentTarget.value);
941
945
  },
@@ -1004,12 +1008,8 @@ window.__ModuleLoader__.load({
1004
1008
  function ContextCompressionSettingsSection(props) {
1005
1009
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsCompressionProfileControls, { ...props });
1006
1010
  }
1007
- function SettingsCompressionProfileControls({ useCompression, useSessions, select, saveCustom, resetCustom, saveAutoCompact, saveCodeSkeleton, savePresetOptions, t }) {
1011
+ function SettingsCompressionProfileControls({ useCompression, select, saveCustom, resetCustom, saveAutoCompact, saveCodeSkeleton, savePresetOptions, t }) {
1008
1012
  const state = useCompression((snapshot) => snapshot);
1009
- const selectorAvailable = useSessions((sessions) => {
1010
- const current = sessions.current;
1011
- return current === void 0 ? void 0 : sessions.byId[current]?.agentPreset;
1012
- }) !== "minimal";
1013
1013
  const [saving, setSaving] = (0, react.useState)(false);
1014
1014
  const [saveError, setSaveError] = (0, react.useState)(null);
1015
1015
  const [draft, setDraft] = (0, react.useState)(null);
@@ -1021,7 +1021,7 @@ window.__ModuleLoader__.load({
1021
1021
  if (state.status === "unavailable") return null;
1022
1022
  const busy = state.status === "loading" || saving;
1023
1023
  const selectProfile = (profile) => {
1024
- if (!selectorAvailable || !state.writable || profile === current) return;
1024
+ if (!state.writable || profile === current) return;
1025
1025
  setSaveError(null);
1026
1026
  setSaving(true);
1027
1027
  select(profile).then(() => {
@@ -1052,7 +1052,7 @@ window.__ModuleLoader__.load({
1052
1052
  className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.settingsDescription,
1053
1053
  children: t("settings.description")
1054
1054
  }),
1055
- selectorAvailable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1055
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1056
1056
  className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.profileGrid,
1057
1057
  "aria-label": t("label"),
1058
1058
  children: COMPRESSION_PROFILES.map((profile) => {
@@ -1080,21 +1080,17 @@ window.__ModuleLoader__.load({
1080
1080
  })]
1081
1081
  }, profile);
1082
1082
  })
1083
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1084
- className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.unavailable,
1085
- role: "status",
1086
- children: t("status.minimalUnavailable")
1087
1083
  }),
1088
1084
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutoCompactThresholdControls, {
1089
1085
  value: state.value?.autoCompact?.thresholdPercent ?? AUTO_COMPACT_THRESHOLD_LIMITS.default,
1090
- disabled: busy || !state.writable || !selectorAvailable,
1086
+ disabled: busy || !state.writable || false,
1091
1087
  save: saveAutoCompact,
1092
1088
  settle,
1093
1089
  t
1094
1090
  }),
1095
1091
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodeSkeletonControls, {
1096
1092
  value: state.value?.codeSkeleton?.enabled ?? false,
1097
- disabled: busy || !state.writable || !selectorAvailable,
1093
+ disabled: busy || !state.writable || false,
1098
1094
  save: saveCodeSkeleton,
1099
1095
  settle,
1100
1096
  t
@@ -1104,7 +1100,7 @@ window.__ModuleLoader__.load({
1104
1100
  t
1105
1101
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EstimatorControls, {
1106
1102
  options: state.value?.presetOptions ?? {},
1107
- disabled: busy || !state.writable || !selectorAvailable,
1103
+ disabled: busy || !state.writable || false,
1108
1104
  save: savePresetOptions,
1109
1105
  settle,
1110
1106
  t
@@ -1113,7 +1109,7 @@ window.__ModuleLoader__.load({
1113
1109
  className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.pricing,
1114
1110
  children: t("pricing.disclosure")
1115
1111
  }),
1116
- current !== "custom" || draft === null || !selectorAvailable ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomPolicyEditor, {
1112
+ current !== "custom" || draft === null || false ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomPolicyEditor, {
1117
1113
  value: draft,
1118
1114
  disabled: busy || !state.writable,
1119
1115
  setValue: setDraft,
@@ -1182,7 +1178,6 @@ window.__ModuleLoader__.load({
1182
1178
  "estimator.description": "辅助小模型零样本判断旧文件读取是否仍有引用价值,仅建议性加速历史清理;未配置或失败时自动退回纯规则通道,不影响主流程。",
1183
1179
  "estimator.mode": "通道",
1184
1180
  "estimator.mode.off": "关闭(纯规则)",
1185
- "estimator.followHost": "跟随宿主默认模型",
1186
1181
  "estimator.mode.host": "宿主模型(复用已配置供应商)",
1187
1182
  "estimator.mode.direct": "直连 OpenAI 兼容端点",
1188
1183
  "estimator.inactive": "估计器只随「TokenPilot 启发模式」提供:该模式的预设选项(去重指针、摘要定位块、读取状态语义与估计器通道)不会合并进其他 Profile,因此当前 Profile「{profile}」下没有可配置的估计器通道。选择「TokenPilot 启发模式」后,本区块会出现「通道」选择,可复用已配置供应商(宿主模型)或直连 OpenAI 兼容端点。",
@@ -1279,7 +1274,6 @@ window.__ModuleLoader__.load({
1279
1274
  "estimator.description": "A small auxiliary model zero-shots whether old file reads are still likely referenced, advisory-only for history aging; unconfigured or failing endpoints fall back to rule-only behavior.",
1280
1275
  "estimator.mode": "Channel",
1281
1276
  "estimator.mode.off": "Off (rule-only)",
1282
- "estimator.followHost": "Follow the host default model",
1283
1277
  "estimator.mode.host": "Host model (reuse configured providers)",
1284
1278
  "estimator.mode.direct": "Direct OpenAI-compatible endpoint",
1285
1279
  "estimator.inactive": "The estimator ships only with the TokenPilot-inspired profile: that profile’s preset options (dedupe pointers, summary locators, read-state semantics, and the estimator channel) are never merged into another profile, so the current profile “{profile}” has no estimator channel to configure. Select TokenPilot-inspired and this section gains a Channel choice — reuse configured providers (host model) or a direct OpenAI-compatible endpoint.",
@@ -1357,6 +1351,8 @@ window.__ModuleLoader__.load({
1357
1351
  };
1358
1352
  //#endregion
1359
1353
  //#region src/client/preset-options.ts
1354
+ /** The namespace key holding every tokenpilot-inspired sub-capability override. */
1355
+ const PRESET_OPTIONS_KEY = "presetOptions";
1360
1356
  /** Every field a patch may address, in the schema's own order. */
1361
1357
  const PRESET_OPTION_KEYS = [
1362
1358
  "dedupeToolResults",
@@ -1371,38 +1367,55 @@ window.__ModuleLoader__.load({
1371
1367
  "estimatorTimeoutMs"
1372
1368
  ];
1373
1369
  /**
1374
- * Apply one patch over the stored section.
1370
+ * Plan the path ops one patch needs against the section currently stored.
1371
+ *
1372
+ * Fields already at their requested value produce no op, so a re-selected
1373
+ * value neither rewrites the document nor reports a save the Host never
1374
+ * committed.
1375
1375
  *
1376
1376
  * @param current - the decoded `presetOptions` section, when one is stored.
1377
1377
  * @param patch - the fields to write or clear.
1378
- * @returns the complete section to store.
1378
+ * @returns the ordered ops, empty when the patch changes nothing.
1379
1379
  */
1380
- function mergePresetOptionsPatch(current, patch) {
1381
- const source = current;
1382
- const merged = {};
1380
+ function planPresetOptionsOps(current, patch) {
1381
+ const stored = current;
1382
+ const ops = [];
1383
1383
  for (const key of PRESET_OPTION_KEYS) {
1384
- const stored = source?.[key];
1385
- if (stored !== void 0) merged[key] = stored;
1386
- }
1387
- for (const key of Object.keys(patch)) {
1384
+ if (!Object.hasOwn(patch, key)) continue;
1388
1385
  const value = patch[key];
1389
- if (value === void 0) delete merged[key];
1390
- else merged[key] = value;
1386
+ const path = [PRESET_OPTIONS_KEY, key];
1387
+ if (value === void 0) {
1388
+ if (stored?.[key] !== void 0) ops.push({
1389
+ op: "unset",
1390
+ path
1391
+ });
1392
+ continue;
1393
+ }
1394
+ if (stored?.[key] === value) continue;
1395
+ ops.push({
1396
+ op: "set",
1397
+ path,
1398
+ value
1399
+ });
1391
1400
  }
1392
- return merged;
1401
+ return ops;
1393
1402
  }
1394
1403
  /**
1395
- * Compare two sections field by field, so an unchanged patch neither rewrites
1396
- * the document nor reports a save the Host never committed.
1404
+ * Test whether one resolved settings document already carries every planned op.
1397
1405
  *
1398
- * @param left - one section (or none).
1399
- * @param right - the other section.
1400
- * @returns whether every known field holds the same value.
1406
+ * @param settings - the resolved namespace section after a write.
1407
+ * @param ops - the ops that write submitted.
1408
+ * @returns whether every named field holds its requested state.
1401
1409
  */
1402
- function presetOptionsEqual(left, right) {
1403
- const a = left;
1404
- const b = right;
1405
- return PRESET_OPTION_KEYS.every((key) => a?.[key] === b?.[key]);
1410
+ function presetOptionsOpsAccepted(presetOptions, ops) {
1411
+ if (ops.length === 0) return true;
1412
+ if (presetOptions === void 0) return false;
1413
+ const stored = presetOptions;
1414
+ return ops.every((op) => {
1415
+ const key = op.path[op.path.length - 1];
1416
+ if (key === void 0) return false;
1417
+ return op.op === "set" ? stored[key] === op.value : stored[key] === void 0;
1418
+ });
1406
1419
  }
1407
1420
  //#endregion
1408
1421
  //#region src/client/index.ts
@@ -1417,42 +1430,47 @@ window.__ModuleLoader__.load({
1417
1430
  return left.version === right.version && left.unit === right.unit && left.prefixPolicy === right.prefixPolicy && left.fresh.enabled === right.fresh.enabled && left.fresh.trigger === right.fresh.trigger && left.fresh.target === right.fresh.target && left.aggregate.enabled === right.aggregate.enabled && left.aggregate.trigger === right.aggregate.trigger && left.aggregate.target === right.aggregate.target && left.history.enabled === right.history.enabled && left.history.trigger === right.history.trigger && left.history.keepRecentToolCalls === right.history.keepRecentToolCalls && left.history.keepRecentTokens === right.history.keepRecentTokens && left.history.minReclaim === right.history.minReclaim && left.tailTrim.enabled === right.tailTrim.enabled && left.tailTrim.trigger === right.tailTrim.trigger;
1418
1431
  }
1419
1432
  function apply(ctx) {
1420
- ctx.effect(() => ctx.locale.register(NS, {
1433
+ ctx.locale.register(NS, {
1421
1434
  zh,
1422
1435
  en
1423
- }), "ui-context-compression: dictionaries");
1424
- const scope = ctx.settingsScope.bind({
1425
- namespace: NS,
1426
- decode: decodeSettings
1427
1436
  });
1428
- const writeAndConfirm = async (write, accepts) => {
1429
- const beforeRevision = scope.getSnapshot().revision;
1430
- await write();
1431
- const after = scope.getSnapshot();
1432
- if (after.status !== "ready" || after.value === void 0 || after.revision === beforeRevision || !accepts(after.value)) throw new Error("Context compression settings were not saved.");
1437
+ const injected = () => {
1438
+ const scope = ctx.settingsScope.bind({
1439
+ namespace: NS,
1440
+ decode: decodeSettings
1441
+ });
1442
+ const writeAndConfirm = async (write, accepts) => {
1443
+ const beforeRevision = scope.getSnapshot().revision;
1444
+ await write();
1445
+ const after = scope.getSnapshot();
1446
+ if (after.status !== "ready" || after.value === void 0 || after.revision === beforeRevision || !accepts(after.value)) throw new Error("Context compression settings were not saved.");
1447
+ };
1448
+ return {
1449
+ hooks: { compression: scope },
1450
+ select: (profile) => writeAndConfirm(() => scope.set("profile", profile), (settings) => settings.profile === profile),
1451
+ saveCustom: (custom) => writeAndConfirm(() => scope.set("custom", custom), (settings) => isCustomCompressionPolicy(settings.custom) && sameCustomPolicy(settings.custom, custom)),
1452
+ resetCustom: () => writeAndConfirm(() => scope.set("custom", structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY)), (settings) => isCustomCompressionPolicy(settings.custom) && sameCustomPolicy(settings.custom, DEFAULT_CUSTOM_COMPRESSION_POLICY)),
1453
+ saveAutoCompact: (thresholdPercent) => writeAndConfirm(() => scope.set("autoCompact", { thresholdPercent }), (settings) => settings.autoCompact.thresholdPercent === thresholdPercent),
1454
+ saveCodeSkeleton: (enabled) => writeAndConfirm(() => scope.set("codeSkeleton", { enabled }), (settings) => settings.codeSkeleton.enabled === enabled),
1455
+ savePresetOptions: (options) => {
1456
+ const ops = planPresetOptionsOps(scope.getSnapshot().value?.presetOptions, options);
1457
+ if (ops.length === 0) return Promise.resolve();
1458
+ return writeAndConfirm(() => scope.mutate(ops), (settings) => presetOptionsOpsAccepted(settings.presetOptions, ops));
1459
+ }
1460
+ };
1433
1461
  };
1434
- const injected = () => ({
1435
- hooks: { compression: scope },
1436
- select: (profile) => writeAndConfirm(() => scope.set("profile", profile), (settings) => settings.profile === profile),
1437
- saveCustom: (custom) => writeAndConfirm(() => scope.set("custom", custom), (settings) => isCustomCompressionPolicy(settings.custom) && sameCustomPolicy(settings.custom, custom)),
1438
- resetCustom: () => writeAndConfirm(() => scope.set("custom", structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY)), (settings) => isCustomCompressionPolicy(settings.custom) && sameCustomPolicy(settings.custom, DEFAULT_CUSTOM_COMPRESSION_POLICY)),
1439
- saveAutoCompact: (thresholdPercent) => writeAndConfirm(() => scope.set("autoCompact", { thresholdPercent }), (settings) => settings.autoCompact.thresholdPercent === thresholdPercent),
1440
- saveCodeSkeleton: (enabled) => writeAndConfirm(() => scope.set("codeSkeleton", { enabled }), (settings) => settings.codeSkeleton.enabled === enabled),
1441
- savePresetOptions: (options) => {
1442
- const current = scope.getSnapshot().value?.presetOptions;
1443
- const next = mergePresetOptionsPatch(current, options);
1444
- if (presetOptionsEqual(current, next)) return Promise.resolve();
1445
- return writeAndConfirm(() => scope.set("presetOptions", next), (settings) => presetOptionsEqual(settings.presetOptions, next));
1446
- }
1447
- });
1448
- ctx.slots.inject("settings.section", () => ctx.slots.register({
1449
- name: "settings.section",
1450
- id: "context-compression",
1451
- order: 17,
1452
- label: () => ctx.locale.bind(NS)("nav"),
1453
- locale: NS,
1454
- inject: injected
1455
- }, ContextCompressionSettingsSection));
1462
+ try {
1463
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
1464
+ name: "settings.section",
1465
+ id: "context-compression",
1466
+ order: 17,
1467
+ label: () => ctx.locale.bind(NS)("nav"),
1468
+ locale: NS,
1469
+ inject: injected
1470
+ }, ContextCompressionSettingsSection));
1471
+ } catch (error) {
1472
+ console.warn("[dsh-context-compression-improved] settings.section 注册失败(新宿主已收编):", error);
1473
+ }
1456
1474
  }
1457
1475
  //#endregion
1458
1476
  exports.apply = apply;
@@ -6,27 +6,15 @@ interface Config {
6
6
  /** Add the canonical compression stack to every non-Minimal preset. */
7
7
  presetOverlay?: boolean;
8
8
  /**
9
- * Own the estimator catalog HTTP route. Set on the Loader row that declares
10
- * `inject: [webServer]`.
9
+ * Register the estimator-catalog HTTP route on this row.
11
10
  *
12
- * **Measured, and it contradicts the note this field was introduced with.**
13
- * The original justification "registering a route authorizes against the
14
- * calling fiber, and a fiber that has not declared `webServer` cannot reach
15
- * it, not even through `ctx.inject` or `ctx.get`" — is wrong on both halves:
16
- * the host's `register` performs no authorization at all (it reads
17
- * `this.exact` / `this.prefixes` and throws only on a duplicate
18
- * `(kind, path)`), and `ctx.get(name, strict)` checks only that the providing
19
- * fiber is active (`state === 2`), never the caller's `inject` list. The one
20
- * inject-gated path is the `ctx.webServer` **property** access, which this
21
- * plugin never uses: `registerEstimatorCatalogRoute` uses `ctx.get` plus its
22
- * own `ctx.inject(['webServer'], …)`.
23
- *
24
- * So the row-level `inject` is **not load-bearing**; it is kept as
25
- * belt-and-braces so the route row stays inactive until `webServer` exists,
26
- * and the flag keeps the route off standalone Bundle rows on profiles that
27
- * have no web server. The internal two-channel registration is what actually
28
- * covers both arrival orders. Do not cite this comment as a rule to the
29
- * 0.1.5 replay — cite the host source.
11
+ * The standalone Bundle patch sets this on its own row (which declares
12
+ * `inject: [webServer]`), so profiles without a host web server never mount a
13
+ * row that could only announce a pending route. The row-level `inject` is
14
+ * belt-and-braces: `dsh-host-webserver.register` performs no authorization
15
+ * check and `ctx.get(name)` only asks whether the providing fiber is active,
16
+ * so the two-channel registration inside this function is what actually
17
+ * covers both arrival orders.
30
18
  */
31
19
  estimatorCatalogRoute?: boolean;
32
20
  }
@@ -455,6 +455,7 @@ function restoreMethod(presets, snapshot) {
455
455
  }
456
456
  //#endregion
457
457
  //#region src/index.ts
458
+ const CONTEXT_COMPRESSION_NAMESPACE = CONTEXT_COMPRESSION_SETTINGS_NAMESPACE;
458
459
  const ESTIMATOR_CATALOG_ROUTES = ["/endpoint/dsh-context-compression-improved/estimator-catalog", "/api/dsh-context-compression-improved/estimator-catalog"];
459
460
  /**
460
461
  * The one service the catalog route actually needs. `llm` and
@@ -463,21 +464,14 @@ const ESTIMATOR_CATALOG_ROUTES = ["/endpoint/dsh-context-compression-improved/es
463
464
  */
464
465
  const ESTIMATOR_CATALOG_ROUTE_DEPS = ["webServer"];
465
466
  function asWebServer(value) {
466
- if (typeof value?.register !== "function") return void 0;
467
- return value;
467
+ if (typeof value !== "object" || value === null) return void 0;
468
+ return typeof value.register === "function" ? value : void 0;
468
469
  }
469
470
  /**
470
471
  * Serve `GET /api/dsh-context-compression-improved/estimator-catalog` — the
471
472
  * settings card's host-route dropdowns (live provider/model groups from the DSH
472
- * `llm` service plus the effective selection). This lives on the top-level
473
- * plugin context, NOT inside the isolated toolResultPruner service.
474
- *
475
- * (The isolation reason this placement was originally justified with — "a route
476
- * registered there can never reach `webServer` across the isolation boundary" —
477
- * is **unverified**: no `@deepseek-ai` package calls `.isolate(`, so there is no
478
- * boundary to cross here. Top-level placement is still the right choice, for a
479
- * reason that needs no framework rule: the route is host-wide, not
480
- * per-pruner-instance. Don't promote the isolation wording into a rule.)
473
+ * `llm` service plus the effective selection). Best effort: without the
474
+ * `webServer` service the plugin keeps working, only the HTTP API is missing.
481
475
  *
482
476
  * The route gates on `webServer` **alone**. `llm` and `agentDefaultModel` only
483
477
  * enrich the response and are resolved per request, so listing them here would
@@ -561,7 +555,6 @@ function registerEstimatorCatalogRoute(ctx) {
561
555
  });
562
556
  log("warn", "context-compression webServer not active yet — estimator catalog route pending: %s", ESTIMATOR_CATALOG_ROUTES.join(", "));
563
557
  }
564
- const CONTEXT_COMPRESSION_NAMESPACE = CONTEXT_COMPRESSION_SETTINGS_NAMESPACE;
565
558
  /** Symbol properties reach the shared service target through Cordis proxies. */
566
559
  const SHARED_SETTINGS = Symbol.for("dsh-context-compression-improved/settings-registration");
567
560
  /** Loader validation for the standalone Bundle opt-in. */
@@ -586,7 +579,7 @@ function apply(ctx, config = {}) {
586
579
  presetsCtx.effect(() => () => installation.dispose(), "contextCompressionSelector.agentPresets()");
587
580
  });
588
581
  } catch (error) {
589
- console.error("context-compression apply() failed:", error);
582
+ console.error("context-compression selector apply failed: %o", error);
590
583
  throw error;
591
584
  }
592
585
  }
@@ -598,7 +591,9 @@ function apply(ctx, config = {}) {
598
591
  function resolveAutoCompactThresholdPercent(presetsCtx) {
599
592
  const raw = presetsCtx.get("settings")?.get(CONTEXT_COMPRESSION_NAMESPACE);
600
593
  try {
601
- return ContextCompressionSettingsSchema(structuredClone(raw)).autoCompact.thresholdPercent;
594
+ const threshold = structuredClone(raw)?.autoCompact;
595
+ const value = typeof threshold?.thresholdPercent === "number" ? threshold.thresholdPercent : 80;
596
+ return Number.isFinite(value) && value >= 50 && value <= 90 ? value : 80;
602
597
  } catch {
603
598
  return 80;
604
599
  }
@@ -1,4 +1,4 @@
1
- import { a as validatePublishedTailTrim, o as sessionEvents } from "./tail-trim.js";
1
+ import { a as validatePublishedTailTrim, s as sessionEvents } from "./tail-trim.js";
2
2
  //#region src/invariant.ts
3
3
  const PACKAGE_NAME = "dsh-context-compression-improved-runtime";
4
4
  /** Cordis companion plugin name. */
@@ -9,14 +9,14 @@ const inject = ["invariants"];
9
9
  function resemblesCompanion(prune, event) {
10
10
  if (event.type !== "tool/result" && event.type !== "user/message" || typeof event.surfaceOp !== "object") return false;
11
11
  const sources = new Set(event.sourceEventSeqs ?? []);
12
- return event.surfaceOp.start === prune.data.shadowedRange.start && event.surfaceOp.end === prune.data.shadowedRange.end || prune.data.shadowedSeqs.some((seq) => sources.has(seq));
12
+ return event.surfaceOp.startSeq === prune.data.shadowedRange.start && event.surfaceOp.endSeq === prune.data.shadowedRange.end || prune.data.shadowedSeqs.some((seq) => sources.has(seq));
13
13
  }
14
14
  /** Validate one standard prune's immediately adjacent surface replacement. */
15
15
  function validateCompanion(session, prune, event, fail) {
16
16
  if (event.type !== "tool/result" && event.type !== "user/message" || typeof event.surfaceOp !== "object") fail(`compaction/prune at seq ${prune.seq} must be immediately followed by a replacement surface event`);
17
17
  const { shadowedRange, shadowedSeqs } = prune.data;
18
- if (shadowedSeqs.length === 0 || shadowedSeqs[0] !== shadowedRange.start || shadowedSeqs.at(-1) !== shadowedRange.end) fail(`compaction/prune at seq ${prune.seq} has a shadowed range inconsistent with shadowedSeqs`);
19
- if (event.surfaceOp.start !== shadowedRange.start || event.surfaceOp.end !== shadowedRange.end) fail(`replacement at seq ${event.seq} does not replace compaction/prune range ${shadowedRange.start}-${shadowedRange.end}`);
18
+ if (shadowedSeqs.length === 0 || shadowedSeqs[0] !== Number(shadowedRange.start) || shadowedSeqs.at(-1) !== Number(shadowedRange.end)) fail(`compaction/prune at seq ${prune.seq} has a shadowed range inconsistent with shadowedSeqs`);
19
+ if (event.surfaceOp.startSeq !== shadowedRange.start || event.surfaceOp.endSeq !== shadowedRange.end) fail(`replacement at seq ${event.seq} does not replace compaction/prune range ${shadowedRange.start}-${shadowedRange.end}`);
20
20
  const sources = new Set(event.sourceEventSeqs ?? []);
21
21
  const missing = shadowedSeqs.filter((seq) => !sources.has(seq));
22
22
  if (missing.length > 0) fail(`replacement at seq ${event.seq} omits shadowed source seqs ${missing.join(", ")}`);
@@ -1,6 +1,6 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { Context, Service } from "@deepseek-ai/cordis";
3
- import { CallId, ContentBlock } from "@deepseek-ai/dsh-llm";
3
+ import { ContentBlock, ToolCallId } from "@deepseek-ai/dsh-llm";
4
4
  import { Session } from "@deepseek-ai/dsh-session";
5
5
  import { TokenMeasurement } from "@deepseek-ai/dsh-token-meter";
6
6
  //#region src/runtime/tokenpilot/dedup.d.ts
@@ -279,7 +279,7 @@ interface PrunedEntry {
279
279
  /** Newly appended compressed tool-result event. */
280
280
  readonly replacementSeq: number;
281
281
  /** Tool call shared by the original and replacement. */
282
- readonly callId: CallId;
282
+ readonly callId: ToolCallId;
283
283
  /** Reducer or aging strategy that produced the replacement. */
284
284
  readonly reducer: string;
285
285
  /** Pass stage that landed the replacement. */
@@ -582,13 +582,14 @@ declare class ToolResultPruner extends Service {
582
582
  static Config: z<ToolResultPruneConfig>;
583
583
  /** Consolidated per-session mutable state. */
584
584
  readonly state: PrunerState;
585
+ /** 0.1.5-specific: per-session sets of already-audited native summary seqs. */
586
+ private readonly auditedNativeSummaries;
585
587
  constructor(ctx: Context, config?: ToolResultPruneConfig);
586
588
  /**
587
589
  * Measure text content in Unicode code points; non-text blocks cost zero.
588
590
  * @param blocks - tool-result content to measure.
589
591
  * @returns total Unicode code points across text blocks.
590
592
  */
591
- measureContent(blocks: readonly ContentBlock[]): number;
592
593
  /**
593
594
  * Apply the configured native head/middle/tail transform.
594
595
  * @param blocks - original tool-result content.
@@ -675,10 +676,18 @@ declare class ToolResultPruner extends Service {
675
676
  private hasRecoveryTool;
676
677
  private auditHistoryEvaluation;
677
678
  private auditComponent;
679
+ /** Emit the native-auto-compact audit for one summary manifest, once. */
680
+ private emitNativeSummaryAudit;
681
+ /**
682
+ * 0.1.5 commits Native auto-compact by reopening the Session with a seed
683
+ * log; seed events never reach the `session/event` firehose, so scan the
684
+ * snapshot for summary manifests the live listener could not observe.
685
+ */
686
+ private scanForSeededNativeSummary;
678
687
  private auditFailure;
679
688
  private auditPublicationFailure;
680
689
  private warnExactUnavailable;
681
690
  private warnOnce;
682
691
  }
683
692
  //#endregion
684
- export { AUTO_COMPACT_THRESHOLD_LIMITS, type AutoCompactSettings, COMPRESSION_PROFILES, CONTEXT_COMPRESSION_SETTINGS_NAMESPACE, type CodeSkeletonSettings, type CompactionTokenView, type CompressionPolicy, type CompressionProfile, type ContextCompressionSettings, ContextCompressionSettingsSchema, type CustomCompressionBudget, type CustomCompressionPolicy, CustomCompressionPolicySchema, type CustomCompressionPolicyV1, type CustomCompressionPolicyV2, type CustomCompressionPolicyV3, type CustomCompressionUnit, type CustomHistoryPolicy, type CustomPolicyResolutionOptions, type CustomPrefixPolicy, type CustomTailTrimPolicy, DEFAULTS, DEFAULT_CUSTOM_COMPRESSION_POLICY, type HistoryMode, type MeasuredTokenSurfaceNode, PRUNE_MARKER, type PruneResult, type PruneSessionOptions, type PruneStage, type PrunedEntry, type ResolvedConfig, type ToolResultPruneConfig, ToolResultPruner, ToolResultPruner as default, codePointLength, historicalPlaceholder, isCompressionProfile, isValidAutoCompactThresholdPercent, measureForCompaction, normalizeTerminalText, parseContextCompressionSettings, reduceFreshToolResult, resolveConfig, resolveCustomPolicy, resolvePolicy, verifyReduction };
693
+ export { AUTO_COMPACT_THRESHOLD_LIMITS, type AutoCompactSettings, COMPRESSION_PROFILES, CONTEXT_COMPRESSION_SETTINGS_NAMESPACE, type CodeSkeletonSettings, type CompactionTokenView, type CompressionPolicy, type CompressionProfile, type ContextCompressionSettings, ContextCompressionSettingsSchema, type CustomCompressionBudget, type CustomCompressionPolicy, CustomCompressionPolicySchema, type CustomCompressionPolicyV1, type CustomCompressionPolicyV2, type CustomCompressionPolicyV3, type CustomCompressionUnit, type CustomHistoryPolicy, type CustomPolicyResolutionOptions, type CustomPrefixPolicy, type CustomTailTrimPolicy, DEFAULTS, DEFAULT_CUSTOM_COMPRESSION_POLICY, type HistoryMode, type MeasuredTokenSurfaceNode, PRUNE_MARKER, type PruneResult, type PruneSessionOptions, type PruneStage, type PrunedEntry, type ToolResultPruneConfig, ToolResultPruner, ToolResultPruner as default, codePointLength, historicalPlaceholder, isCompressionProfile, isValidAutoCompactThresholdPercent, measureForCompaction, normalizeTerminalText, parseContextCompressionSettings, reduceFreshToolResult, resolveConfig, resolveCustomPolicy, resolvePolicy, verifyReduction };