dsh-context-compression-improved 0.1.1 → 0.2.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 (148) hide show
  1. package/.gitattributes +1 -0
  2. package/.github/workflows/ci.yml +39 -0
  3. package/CHANGELOG.ja.md +39 -0
  4. package/CHANGELOG.ko.md +39 -0
  5. package/CHANGELOG.md +135 -0
  6. package/CHANGELOG.zh.md +39 -0
  7. package/CONTRIBUTING.md +22 -0
  8. package/README.ja.md +104 -0
  9. package/README.ko.md +103 -0
  10. package/README.md +89 -12
  11. package/README.zh.md +87 -12
  12. package/SECURITY.md +18 -0
  13. package/THIRD_PARTY_NOTICES.md +7 -31
  14. package/docs/installation.ja.md +76 -0
  15. package/docs/installation.ko.md +76 -0
  16. package/docs/installation.md +76 -0
  17. package/docs/installation.zh.md +76 -0
  18. package/docs/repair-log.md +582 -0
  19. package/eslint.config.js +30 -0
  20. package/package.json +84 -81
  21. package/packages/selector/LICENSE +21 -0
  22. package/packages/selector/README.md +26 -0
  23. package/packages/selector/README.zh.md +26 -0
  24. package/packages/selector/THIRD_PARTY_NOTICES.md +38 -0
  25. package/packages/selector/docs/history-tool-call-working-set-spec.md +112 -0
  26. package/packages/selector/docs/native-tool-result-selector-spec.md +34 -0
  27. package/packages/selector/docs/subagent-cache-reuse-spec.md +46 -0
  28. package/packages/selector/lib/style.css +308 -0
  29. package/packages/selector/package.json +115 -0
  30. package/{screenshots.json → packages/selector/screenshots.json} +6 -6
  31. package/packages/selector/src/client/CompressionProfileControls.tsx +229 -0
  32. package/packages/selector/src/client/CompressionProfileSelector.module.css +170 -0
  33. package/packages/selector/src/client/CompressionProfileSelector.tsx +79 -0
  34. package/packages/selector/src/client/CustomPolicyEditor.tsx +216 -0
  35. package/packages/selector/src/client/EstimatorControls.tsx +281 -0
  36. package/packages/selector/src/client/decode.ts +49 -0
  37. package/packages/selector/src/client/index.ts +111 -0
  38. package/packages/selector/src/client/locales.ts +198 -0
  39. package/packages/selector/src/client/preset-options.ts +70 -0
  40. package/packages/selector/src/client/settings-section.tsx +126 -0
  41. package/packages/selector/src/css-modules.d.ts +6 -0
  42. package/packages/selector/src/deepseek-v4-tokenizer.ts +210 -0
  43. package/packages/selector/src/estimator-catalog.ts +104 -0
  44. package/packages/selector/src/index.ts +327 -0
  45. package/packages/selector/src/invariant.ts +113 -0
  46. package/packages/selector/src/preset-overlay.ts +567 -0
  47. package/packages/selector/src/profiles.ts +342 -0
  48. package/packages/selector/src/pruner/content.ts +188 -0
  49. package/packages/selector/src/pruner/session.ts +94 -0
  50. package/packages/selector/src/pruner/state.ts +43 -0
  51. package/packages/selector/src/pruner/tuning.ts +23 -0
  52. package/packages/selector/src/pruner/types.ts +60 -0
  53. package/packages/selector/src/pruner.ts +2144 -0
  54. package/packages/selector/src/runtime/adaptive-cost.ts +194 -0
  55. package/packages/selector/src/runtime/audit.ts +215 -0
  56. package/packages/selector/src/runtime/config.ts +613 -0
  57. package/packages/selector/src/runtime/custom-policy.ts +278 -0
  58. package/packages/selector/src/runtime/deepseek-official-pricing.ts +298 -0
  59. package/packages/selector/src/runtime/deepseek-v4-vision-tokens.ts +254 -0
  60. package/packages/selector/src/runtime/measurement.ts +403 -0
  61. package/packages/selector/src/runtime/reducers.ts +656 -0
  62. package/packages/selector/src/runtime/retrieve.ts +457 -0
  63. package/packages/selector/src/runtime/session-events.ts +17 -0
  64. package/packages/selector/src/runtime/tail-trim.ts +166 -0
  65. package/packages/selector/src/runtime/token-count.ts +72 -0
  66. package/packages/selector/src/runtime/tokenpilot/dedup.ts +81 -0
  67. package/packages/selector/src/runtime/tokenpilot/estimator.ts +183 -0
  68. package/packages/selector/src/runtime/tokenpilot/locator.ts +128 -0
  69. package/packages/selector/src/runtime/tokenpilot/read-state.ts +77 -0
  70. package/packages/selector/src/runtime/types.ts +309 -0
  71. package/packages/selector/src/runtime/value.ts +48 -0
  72. package/packages/selector/tests/auto-compact.client.spec.tsx +226 -0
  73. package/packages/selector/tests/built/client-artifact.spec.ts +51 -0
  74. package/packages/selector/tests/cache-prefix-audit.spec.ts +123 -0
  75. package/packages/selector/tests/code-skeleton.client.spec.ts +88 -0
  76. package/packages/selector/tests/custom-contract.client.spec.ts +202 -0
  77. package/packages/selector/tests/estimator-catalog.spec.ts +70 -0
  78. package/packages/selector/tests/estimator-channel.client.spec.tsx +247 -0
  79. package/packages/selector/tests/estimator-route-registration.host.spec.ts +176 -0
  80. package/packages/selector/tests/host-preset-overlay.host.spec.ts +204 -0
  81. package/packages/selector/tests/preset-options-write.client.spec.ts +181 -0
  82. package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +196 -0
  83. package/packages/selector/tests/preset-overlay.host.spec.ts +243 -0
  84. package/packages/selector/tests/profiles.client.spec.tsx +434 -0
  85. package/packages/selector/tests/public/package-contract.client.spec.ts +33 -0
  86. package/packages/selector/tests/runtime/adaptive-cost.spec.ts +167 -0
  87. package/packages/selector/tests/runtime/audit.spec.ts +129 -0
  88. package/packages/selector/tests/runtime/auto-compact-config.spec.ts +523 -0
  89. package/packages/selector/tests/runtime/code-skeleton.spec.ts +141 -0
  90. package/packages/selector/tests/runtime/deepseek-official-pricing.spec.ts +186 -0
  91. package/packages/selector/tests/runtime/deepseek-v4-tokenizer.spec.ts +122 -0
  92. package/packages/selector/tests/runtime/deepseek-v4-vision-tokens.spec.ts +122 -0
  93. package/packages/selector/tests/runtime/fixtures/profile-baseline.json +273 -0
  94. package/packages/selector/tests/runtime/fixtures/tokenizer-golden.json +106 -0
  95. package/packages/selector/tests/runtime/fixtures/vision-golden.json +459 -0
  96. package/packages/selector/tests/runtime/public/public-runtime.spec.ts +2531 -0
  97. package/packages/selector/tests/runtime/session-events.spec.ts +27 -0
  98. package/packages/selector/tests/runtime/tokenizer-golden.spec.ts +53 -0
  99. package/packages/selector/tests/runtime/tokenpilot/dedup.spec.ts +52 -0
  100. package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +56 -0
  101. package/packages/selector/tests/runtime/tokenpilot/locator.spec.ts +76 -0
  102. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +100 -0
  103. package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +58 -0
  104. package/packages/selector/tests/runtime/value.spec.ts +23 -0
  105. package/packages/selector/tests/standing-generation.host.spec.ts +631 -0
  106. package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +250 -0
  107. package/packages/selector/tests/support/cache-prefix-audit.ts +105 -0
  108. package/packages/selector/tests/support/mock-adapter.ts +37 -0
  109. package/packages/selector/tests/support/ui-primitives.tsx +34 -0
  110. package/packages/selector/tsconfig.json +11 -0
  111. package/packages/selector/tsdown.client.config.ts +102 -0
  112. package/packages/selector/tsdown.config.ts +20 -0
  113. package/pnpm-workspace.yaml +19 -0
  114. package/scripts/capture-profile-baseline.ts +80 -0
  115. package/scripts/generate-tokenizer-fixtures.py +81 -0
  116. package/scripts/generate-vision-fixtures.py +208 -0
  117. package/scripts/packed-components-smoke.ts +713 -0
  118. package/scripts/packed-install-e2e.ts +1072 -0
  119. package/scripts/verify-release.ts +300 -0
  120. package/tests/TEST_INVENTORY.md +42 -0
  121. package/tsconfig.base.json +18 -0
  122. package/tsconfig.json +7 -0
  123. package/tsconfig.scripts.json +13 -0
  124. package/tsconfig.tests.json +15 -0
  125. package/vitest.built.config.ts +9 -0
  126. package/vitest.config.ts +43 -0
  127. /package/{assets → packages/selector/assets}/deepseek-v4/LICENSE.DeepSeek-V4-Pro.txt +0 -0
  128. /package/{assets → packages/selector/assets}/deepseek-v4/manifest.json +0 -0
  129. /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer.json +0 -0
  130. /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer_config.json +0 -0
  131. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/LICENSE.DeepSeek-V4-Flash-Vision-Exp.txt +0 -0
  132. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/manifest.json +0 -0
  133. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer.json +0 -0
  134. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer_config.json +0 -0
  135. /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-profiles.jpg +0 -0
  136. /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-settings.png +0 -0
  137. /package/{cordis.patch.yml → packages/selector/cordis.patch.yml} +0 -0
  138. /package/{dsh.plugin.json → packages/selector/dsh.plugin.json} +0 -0
  139. /package/{lib → packages/selector/lib}/client.d.ts +0 -0
  140. /package/{lib → packages/selector/lib}/client.js +0 -0
  141. /package/{lib → packages/selector/lib}/config.js +0 -0
  142. /package/{lib → packages/selector/lib}/index.d.ts +0 -0
  143. /package/{lib → packages/selector/lib}/index.js +0 -0
  144. /package/{lib → packages/selector/lib}/invariant.d.ts +0 -0
  145. /package/{lib → packages/selector/lib}/invariant.js +0 -0
  146. /package/{lib → packages/selector/lib}/pruner.d.ts +0 -0
  147. /package/{lib → packages/selector/lib}/pruner.js +0 -0
  148. /package/{lib → packages/selector/lib}/tail-trim.js +0 -0
@@ -0,0 +1,281 @@
1
+ /**
2
+ * TokenPilot-inspired estimator channel card components.
3
+ *
4
+ * Extracted from CompressionProfileSelector.tsx to reduce god-module size.
5
+ *
6
+ * @module dsh-context-compression-improved/client/EstimatorControls
7
+ */
8
+
9
+ import { useEffect, useState } from 'react'
10
+ import type { ContextCompressionLocaleKey } from './locales.ts'
11
+ import type { PresetOptionsPatch } from './preset-options.ts'
12
+ import type { PresetOptionsSettings } from '../profiles.ts'
13
+ import css from './CompressionProfileSelector.module.css'
14
+
15
+ interface EstimatorInactiveNoticeProps {
16
+ /** Already-localized label of the profile that currently owns the selector. */
17
+ profile: string
18
+ t: (key: ContextCompressionLocaleKey) => string
19
+ }
20
+
21
+ /**
22
+ * The estimator card is gated on the tokenpilot-inspired profile, because
23
+ * `presetOptions` is merged into that profile alone. A card that merely
24
+ * disappears reads as a missing feature — the first real-machine report was
25
+ * exactly that — so keep the heading and its anchor id in place and spend them
26
+ * on the reason plus the profile that unlocks the card. The gate itself is
27
+ * unchanged: no estimator control exists outside tokenpilot-inspired.
28
+ */
29
+ export function EstimatorInactiveNotice({ profile, t }: EstimatorInactiveNoticeProps) {
30
+ return (
31
+ <section className={css.autoCompact} aria-labelledby="context-compression-estimator-title">
32
+ <h3 id="context-compression-estimator-title" className={css.autoCompactTitle}>{t('estimator.title')}</h3>
33
+ <p className={css.customNote}>{t('estimator.inactive').replace('{profile}', profile)}</p>
34
+ </section>
35
+ )
36
+ }
37
+
38
+ interface EstimatorControlsProps {
39
+ options: PresetOptionsSettings
40
+ disabled: boolean
41
+ save: (options: PresetOptionsPatch) => Promise<void>
42
+ settle: (operation: () => Promise<void>) => void
43
+ t: (key: ContextCompressionLocaleKey) => string
44
+ }
45
+
46
+ interface CatalogModelEntry { readonly id: string, readonly name: string }
47
+ interface CatalogProviderEntry { readonly id: string, readonly name: string, readonly models: readonly CatalogModelEntry[], readonly error?: string }
48
+ interface EstimatorCatalogBody {
49
+ readonly ok?: boolean
50
+ readonly providers?: readonly CatalogProviderEntry[]
51
+ readonly selection?: { readonly provider: string, readonly model: string }
52
+ }
53
+
54
+ // The host channel is `/api`, and only `/api`: the pre-0.1.2 `/endpoint` prefix
55
+ // does not exist on this tier's hosts, so probing it first only bought a
56
+ // guaranteed 404 round-trip ahead of every successful load.
57
+ export const ESTIMATOR_CATALOG_ROUTES = [
58
+ '/api/dsh-context-compression-improved/estimator-catalog',
59
+ ]
60
+
61
+ /**
62
+ * TokenPilot-inspired estimator channel card. Shown only while the
63
+ * tokenpilot-inspired profile is selected, and split by channel:
64
+ *
65
+ * - `host` reuses the providers and credentials already configured in DSH
66
+ * through the Harness `llm` service, so this card names a provider and a
67
+ * model and accepts NO API key — the key field belongs to the direct channel
68
+ * alone.
69
+ * - `direct` talks to a native OpenAI-compatible endpoint, the only channel
70
+ * carrying its own base URL and write-only key.
71
+ *
72
+ * The whole card is advisory: an unconfigured or failing endpoint keeps every
73
+ * consumer on its rule-only fallback.
74
+ */
75
+ export function EstimatorControls({ options, disabled, save, settle, t }: EstimatorControlsProps) {
76
+ const [keyDraft, setKeyDraft] = useState('')
77
+ const [baseUrl, setBaseUrl] = useState(options.estimatorBaseUrl ?? '')
78
+ const [model, setModel] = useState(options.estimatorModel ?? '')
79
+ const [provider, setProvider] = useState(options.estimatorProvider ?? '')
80
+ const mode = options.estimatorMode ?? ''
81
+ // Host-mode dropdown source: the live provider/model-group catalog served by
82
+ // the runtime's estimator-catalog route (dsh-perm-gate receiver pattern).
83
+ // Empty catalog (route missing / llm service absent) falls back to the
84
+ // manual text inputs so nothing breaks on hosts without the webServer.
85
+ const [catalog, setCatalog] = useState<EstimatorCatalogBody | undefined>()
86
+ useEffect(() => {
87
+ if (mode !== 'host') return
88
+ let alive = true
89
+ // The catalog route answers once the host webServer/llm services are up;
90
+ // poll briefly so the dropdowns fill without reopening the panel.
91
+ let attempts = 0
92
+ const load = async (routes: readonly string[]): Promise<EstimatorCatalogBody | undefined> => {
93
+ for (const route of routes) {
94
+ try {
95
+ const response = await fetch(route, { headers: { 'cache-control': 'no-cache' } })
96
+ if (response.ok) return (await response.json()) as EstimatorCatalogBody
97
+ } catch {
98
+ // try the next prefix
99
+ }
100
+ }
101
+ return undefined
102
+ }
103
+ const tick = (): void => {
104
+ attempts += 1
105
+ void load(ESTIMATOR_CATALOG_ROUTES).then((body) => {
106
+ if (!alive) return
107
+ if (body !== undefined && (body.providers?.length ?? 0) > 0) {
108
+ setCatalog(body)
109
+ return
110
+ }
111
+ if (attempts < 10) setTimeout(tick, 3_000)
112
+ })
113
+ }
114
+ tick()
115
+ return () => { alive = false }
116
+ }, [mode])
117
+ const hostProviders = catalog?.providers ?? []
118
+ // Model suggestions follow the provider draft exactly like dsh-perm-gate's
119
+ // receiver card: an empty provider lists every group's models, a chosen
120
+ // provider narrows to its own. The provider error (if any) rides along in
121
+ // the option label so a broken group is visible without blocking typing.
122
+ const providerDraft = provider
123
+ const hostModels = hostProviders
124
+ .filter(entry => providerDraft === '' || entry.id === providerDraft)
125
+ .flatMap(entry => entry.models.map(model => ({ ...model, provider: entry.id })))
126
+ const hostProvider = hostProviders.find(entry => entry.id === providerDraft)
127
+ ?? hostProviders.find(entry => entry.id === (options.estimatorProvider ?? ''))
128
+ const hasKey = (options.estimatorApiKey ?? '') !== ''
129
+ const commit = (patch: PresetOptionsPatch) => {
130
+ settle(() => save(patch))
131
+ }
132
+ // What the host channel would actually call right now: an explicit override
133
+ // wins, otherwise the session default the catalog reports. Naming the
134
+ // resolved route is the whole point of this channel — the user re-enters
135
+ // nothing the Harness already knows.
136
+ const overrideProvider = options.estimatorProvider ?? ''
137
+ const overrideModel = options.estimatorModel ?? ''
138
+ const effectiveProvider = overrideProvider !== '' ? overrideProvider : catalog?.selection?.provider ?? ''
139
+ const effectiveModel = overrideModel !== '' ? overrideModel : catalog?.selection?.model ?? ''
140
+ const effectiveRoute = effectiveProvider !== '' && effectiveModel !== ''
141
+ ? `${effectiveProvider} / ${effectiveModel}`
142
+ : t('estimator.hostUnresolved')
143
+ return (
144
+ <section className={css.autoCompact} aria-labelledby="context-compression-estimator-title">
145
+ <h3 id="context-compression-estimator-title" className={css.autoCompactTitle}>{t('estimator.title')}</h3>
146
+ <p className={css.customNote}>{t('estimator.description')}</p>
147
+ <label className={css.field}>
148
+ <span>{t('estimator.mode')}</span>
149
+ <select
150
+ value={mode}
151
+ disabled={disabled}
152
+ onChange={(event) => { settle(() => save({ estimatorMode: event.currentTarget.value as '' | 'host' | 'direct' })) }}
153
+ >
154
+ <option value="">{t('estimator.mode.off')}</option>
155
+ <option value="host">{t('estimator.mode.host')}</option>
156
+ <option value="direct">{t('estimator.mode.direct')}</option>
157
+ </select>
158
+ </label>
159
+ {mode === '' ? null : mode === 'host' ? (
160
+ <>
161
+ <label className={css.field}>
162
+ <span>{t('estimator.provider')}</span>
163
+ <input
164
+ type="text"
165
+ list="estimator-provider-options"
166
+ value={provider}
167
+ disabled={disabled}
168
+ placeholder={t('estimator.provider.placeholder')}
169
+ onChange={(event) => {
170
+ const next = event.currentTarget.value
171
+ setProvider(next)
172
+ // Picking from the datalist fires change without blur: commit
173
+ // an exact catalog hit immediately; free typing waits for blur
174
+ // so mid-edit keystrokes never disable the panel.
175
+ if (next !== '' && hostProviders.some(entry => entry.id === next)) {
176
+ commit({ estimatorProvider: next })
177
+ }
178
+ }}
179
+ onBlur={() => { if (provider !== (options.estimatorProvider ?? '')) commit({ estimatorProvider: provider }) }}
180
+ />
181
+ </label>
182
+ {/* Datalists ride outside the labels on purpose: option text inside a
183
+ label would leak into its accessible name. */}
184
+ <datalist id="estimator-provider-options">
185
+ {hostProviders.map(entry => (
186
+ <option key={entry.id} value={entry.id}>
187
+ {entry.name === '' ? entry.id : entry.name}{entry.error === undefined ? '' : ` (${entry.error})`}
188
+ </option>
189
+ ))}
190
+ </datalist>
191
+ <label className={css.field}>
192
+ <span>{t('estimator.model')}</span>
193
+ <input
194
+ type="text"
195
+ list="estimator-model-options"
196
+ value={model}
197
+ disabled={disabled}
198
+ placeholder={t('estimator.model.placeholder')}
199
+ onChange={(event) => {
200
+ const next = event.currentTarget.value
201
+ setModel(next)
202
+ if (next !== '' && hostModels.some(entry => entry.id === next)) {
203
+ commit({ estimatorModel: next })
204
+ }
205
+ }}
206
+ onBlur={() => { if (model !== (options.estimatorModel ?? '')) commit({ estimatorModel: model }) }}
207
+ />
208
+ </label>
209
+ <datalist id="estimator-model-options">
210
+ {hostModels.map(entry => (
211
+ <option key={`${entry.provider}\0${entry.id}`} value={entry.id}>{entry.name}</option>
212
+ ))}
213
+ </datalist>
214
+ {hostProvider?.error === undefined ? null : (
215
+ <p className={css.customNote}>{String(hostProvider.error)}</p>
216
+ )}
217
+ <p className={css.customNote}>{t('estimator.hostReuse')}</p>
218
+ <p className={css.customNote}>{t('estimator.hostRoute').replace('{route}', effectiveRoute)}</p>
219
+ </>
220
+ ) : (
221
+ <>
222
+ <label className={css.field}>
223
+ <span>{t('estimator.baseUrl')}</span>
224
+ <input
225
+ type="text"
226
+ value={baseUrl}
227
+ disabled={disabled}
228
+ placeholder="https://127.0.0.1:8000/v1"
229
+ onChange={(event) => { setBaseUrl(event.currentTarget.value) }}
230
+ onBlur={() => { if (baseUrl !== (options.estimatorBaseUrl ?? '')) commit({ estimatorBaseUrl: baseUrl }) }}
231
+ />
232
+ </label>
233
+ <label className={css.field}>
234
+ <span>{t('estimator.model')}</span>
235
+ <input
236
+ type="text"
237
+ value={model}
238
+ disabled={disabled}
239
+ placeholder={t('estimator.model.placeholder')}
240
+ onChange={(event) => { setModel(event.currentTarget.value) }}
241
+ onBlur={() => { if (model !== (options.estimatorModel ?? '')) commit({ estimatorModel: model }) }}
242
+ />
243
+ </label>
244
+ <label className={css.field}>
245
+ <span>{t('estimator.apiKey')}</span>
246
+ <div style={{ display: 'flex', gap: '6px' }}>
247
+ <input
248
+ type="password"
249
+ autoComplete="off"
250
+ spellCheck={false}
251
+ value={keyDraft}
252
+ disabled={disabled}
253
+ placeholder={hasKey ? t('estimator.apiKey.set') : t('estimator.apiKey.placeholder')}
254
+ onChange={(event) => { setKeyDraft(event.currentTarget.value) }}
255
+ onBlur={() => {
256
+ const next = keyDraft.trim()
257
+ if (next === '') return
258
+ settle(() => save({ estimatorApiKey: next }))
259
+ setKeyDraft('')
260
+ }}
261
+ onKeyDown={(event) => {
262
+ if (event.key === 'Enter') event.currentTarget.blur()
263
+ }}
264
+ />
265
+ {hasKey ? (
266
+ <button
267
+ type="button"
268
+ disabled={disabled}
269
+ onClick={() => { settle(() => save({ estimatorApiKey: undefined })) }}
270
+ >
271
+ {t('estimator.apiKey.clear')}
272
+ </button>
273
+ ) : null}
274
+ </div>
275
+ {hasKey ? <span className={css.customNote}>{t('estimator.apiKey.overwrite')}</span> : null}
276
+ </label>
277
+ </>
278
+ )}
279
+ </section>
280
+ )
281
+ }
@@ -0,0 +1,49 @@
1
+ /** Browser-safe settings decoding shared by the client entry and node tests. */
2
+
3
+ import {
4
+ canonicalizeCustomPolicy,
5
+ COMPRESSION_PROFILES,
6
+ decodeAutoCompactSettings,
7
+ decodeCodeSkeletonSettings,
8
+ decodePresetOptionsSettings,
9
+ isCustomCompressionPolicy,
10
+ isPlainRecord,
11
+ type CompressionProfile,
12
+ type ContextCompressionSettings,
13
+ } from '../profiles.ts'
14
+
15
+ /**
16
+ * Decode one stored context-compression settings document with exactly the
17
+ * runtime schema's strictness: a plain object with only `profile`, `custom`,
18
+ * `autoCompact`, and `codeSkeleton` keys, a supported profile, a valid Custom
19
+ * document canonicalized to v3 exactly as the runtime resolver would, a
20
+ * strictly-shaped autoCompact section (absent inherits the 80% default), and
21
+ * a strictly-shaped codeSkeleton gate (absent inherits off). Anything else
22
+ * decodes to `undefined` so the UI reports the document as unreadable instead
23
+ * of silently disagreeing with the runtime.
24
+ */
25
+ export function decodeSettings(value: unknown): ContextCompressionSettings | undefined {
26
+ if (!isPlainRecord(value)) return undefined
27
+ const keys = Object.keys(value)
28
+ if (keys.some(key => key !== 'profile' && key !== 'custom' && key !== 'autoCompact' && key !== 'codeSkeleton' && key !== 'presetOptions')) {
29
+ return undefined
30
+ }
31
+ const profile = (value as { profile?: unknown }).profile
32
+ const custom = (value as { custom?: unknown }).custom
33
+ const autoCompact = decodeAutoCompactSettings((value as { autoCompact?: unknown }).autoCompact)
34
+ const codeSkeleton = decodeCodeSkeletonSettings((value as { codeSkeleton?: unknown }).codeSkeleton)
35
+ const presetOptions = decodePresetOptionsSettings((value as { presetOptions?: unknown }).presetOptions)
36
+ return typeof profile === 'string'
37
+ && (COMPRESSION_PROFILES as readonly string[]).includes(profile)
38
+ && isCustomCompressionPolicy(custom)
39
+ && autoCompact !== undefined
40
+ && codeSkeleton !== undefined
41
+ ? {
42
+ profile: profile as CompressionProfile,
43
+ custom: canonicalizeCustomPolicy(custom),
44
+ autoCompact,
45
+ codeSkeleton,
46
+ ...presetOptions === undefined ? {} : { presetOptions },
47
+ }
48
+ : undefined
49
+ }
@@ -0,0 +1,111 @@
1
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
2
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
3
+ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
4
+ import type {} from '@deepseek-ai/dsh-client-ui-workspace/client'
5
+ import {
6
+ isCustomCompressionPolicy,
7
+ ContextCompressionSettingsSection,
8
+ type CompressionSelectorInjected, type ContextCompressionSettings,
9
+ } from './CompressionProfileSelector.tsx'
10
+ import { DEFAULT_CUSTOM_COMPRESSION_POLICY } from '../profiles.ts'
11
+ import { decodeSettings } from './decode.ts'
12
+ import { en, zh } from './locales.ts'
13
+ import { mergePresetOptionsPatch, presetOptionsEqual } from './preset-options.ts'
14
+
15
+ export const inject = ['slots', 'locale', 'settingsScope']
16
+ const NS = 'context-compression'
17
+
18
+
19
+ function sameCustomPolicy(
20
+ left: ContextCompressionSettings['custom'],
21
+ right: ContextCompressionSettings['custom'],
22
+ ): boolean {
23
+ if (left.version !== 3 || right.version !== 3) return false
24
+ return left.version === right.version
25
+ && left.unit === right.unit
26
+ && left.prefixPolicy === right.prefixPolicy
27
+ && left.fresh.enabled === right.fresh.enabled
28
+ && left.fresh.trigger === right.fresh.trigger
29
+ && left.fresh.target === right.fresh.target
30
+ && left.aggregate.enabled === right.aggregate.enabled
31
+ && left.aggregate.trigger === right.aggregate.trigger
32
+ && left.aggregate.target === right.aggregate.target
33
+ && left.history.enabled === right.history.enabled
34
+ && left.history.trigger === right.history.trigger
35
+ && left.history.keepRecentToolCalls === right.history.keepRecentToolCalls
36
+ && left.history.keepRecentTokens === right.history.keepRecentTokens
37
+ && left.history.minReclaim === right.history.minReclaim
38
+ && left.tailTrim.enabled === right.tailTrim.enabled
39
+ && left.tailTrim.trigger === right.tailTrim.trigger
40
+ }
41
+
42
+ export function apply(ctx: ClientContext): void {
43
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-context-compression: dictionaries')
44
+ const scope = ctx.settingsScope.bind<ContextCompressionSettings>({ namespace: NS, decode: decodeSettings })
45
+ const writeAndConfirm = async (
46
+ write: () => Promise<void>,
47
+ accepts: (settings: ContextCompressionSettings) => boolean,
48
+ ): Promise<void> => {
49
+ const beforeRevision = scope.getSnapshot().revision
50
+ await write()
51
+ const after = scope.getSnapshot()
52
+ if (
53
+ after.status !== 'ready'
54
+ || after.value === undefined
55
+ || after.revision === beforeRevision
56
+ || !accepts(after.value)
57
+ ) {
58
+ throw new Error('Context compression settings were not saved.')
59
+ }
60
+ }
61
+ const injected = (): CompressionSelectorInjected => ({
62
+ hooks: { compression: scope },
63
+ select: profile => writeAndConfirm(
64
+ () => scope.set('profile', profile),
65
+ settings => settings.profile === profile,
66
+ ),
67
+ saveCustom: custom => writeAndConfirm(
68
+ () => scope.set('custom', custom),
69
+ settings => isCustomCompressionPolicy(settings.custom)
70
+ && sameCustomPolicy(settings.custom, custom),
71
+ ),
72
+ resetCustom: () => writeAndConfirm(
73
+ () => scope.set('custom', structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY)),
74
+ settings => isCustomCompressionPolicy(settings.custom)
75
+ && sameCustomPolicy(settings.custom, DEFAULT_CUSTOM_COMPRESSION_POLICY),
76
+ ),
77
+ saveAutoCompact: thresholdPercent => writeAndConfirm(
78
+ () => scope.set('autoCompact', { thresholdPercent }),
79
+ settings => settings.autoCompact.thresholdPercent === thresholdPercent,
80
+ ),
81
+ saveCodeSkeleton: enabled => writeAndConfirm(
82
+ () => scope.set('codeSkeleton', { enabled }),
83
+ settings => settings.codeSkeleton.enabled === enabled,
84
+ ),
85
+ savePresetOptions: options => {
86
+ // The section root is replaced by whatever is written there, so merge the
87
+ // patch over the stored document: writing the bare patch deleted
88
+ // estimatorMode (and every other override) on the next field edit.
89
+ const current = scope.getSnapshot().value?.presetOptions
90
+ const next = mergePresetOptionsPatch(current, options)
91
+ if (presetOptionsEqual(current, next)) return Promise.resolve()
92
+ return writeAndConfirm(
93
+ () => scope.set('presetOptions', next),
94
+ settings => presetOptionsEqual(settings.presetOptions, next),
95
+ )
96
+ },
97
+ })
98
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
99
+ name: 'settings.section',
100
+ id: 'context-compression',
101
+ order: 17,
102
+ label: () => ctx.locale.bind(NS)('nav'),
103
+ locale: NS,
104
+ inject: injected,
105
+ }, ContextCompressionSettingsSection))
106
+ }
107
+
108
+ export type {
109
+ CompressionProfile, CompressionProfileSelectorProps, CompressionSelectorInjected,
110
+ ContextCompressionSettings,
111
+ } from './CompressionProfileSelector.tsx'
@@ -0,0 +1,198 @@
1
+ /** Simplified Chinese copy for the context-compression selector. */
2
+ export const zh = {
3
+ 'nav': '上下文压缩选择器',
4
+ 'settings.title': '上下文压缩选择器',
5
+ 'settings.description': '为当前会话选择压缩 Profile,并配置该 Profile 提供的参数。',
6
+ 'label': '上下文压缩',
7
+ 'status.loading': '加载中',
8
+ 'status.unavailable': '不可用',
9
+ 'status.presetUnavailable': '此会话的 preset 未提供上下文压缩,或能力尚未确认。',
10
+ 'status.minimalUnavailable': '极简模式不会为此会话加载上下文压缩能力,因此选择器在本会话中等效为关闭,仅保留 Harness 原生行为。切换到标准、PTC/Coding、创造模式,或支持该能力的自定义 preset 后即可配置。',
11
+ 'status.saveFailed': '保存失败,请重试',
12
+ 'pricing.disclosure': 'DeepSeek 官方价格目录复核于 2026-08-25。Asia/Shanghai 周一至周五 09:00–12:00、14:00–18:00 为峰时,其余为谷时;跨边界请求按成本区间处理。',
13
+ 'profile.balanced': '平衡模式',
14
+ 'profile.cache-strict': 'Cache Strict(前缀保护)',
15
+ 'profile.savings': '节省模式',
16
+ 'profile.adaptive': 'Adaptive(保守成本)',
17
+ 'profile.tokenpilot-inspired': 'TokenPilot 启发模式',
18
+ 'estimator.title': '估计器(可选)',
19
+ 'estimator.description': '辅助小模型零样本判断旧文件读取是否仍有引用价值,仅建议性加速历史清理;未配置或失败时自动退回纯规则通道,不影响主流程。',
20
+ 'estimator.mode': '通道',
21
+ 'estimator.mode.off': '关闭(纯规则)',
22
+ 'estimator.followHost': '跟随宿主默认模型',
23
+ 'estimator.mode.host': '宿主模型(复用已配置供应商)',
24
+ 'estimator.mode.direct': '直连 OpenAI 兼容端点',
25
+ 'estimator.inactive': '估计器只随「TokenPilot 启发模式」提供:该模式的预设选项(去重指针、摘要定位块、读取状态语义与估计器通道)不会合并进其他 Profile,因此当前 Profile「{profile}」下没有可配置的估计器通道。选择「TokenPilot 启发模式」后,本区块会出现「通道」选择,可复用已配置供应商(宿主模型)或直连 OpenAI 兼容端点。',
26
+ 'estimator.provider': '供应商',
27
+ 'estimator.provider.placeholder': '留空则跟随会话默认模型,可从下拉选择或自定义输入',
28
+ 'estimator.model.placeholder': '留空则跟随会话默认模型,可从下拉选择或自定义输入',
29
+ 'estimator.hostReuse': '宿主通道直接复用你在 DSH 中已配置的供应商与凭据,无需填写 API Key;此通道也不接收 API Key。',
30
+ 'estimator.hostRoute': '当前生效路由:{route}。',
31
+ 'estimator.hostUnresolved': '尚未确定(请在 DSH 设置中选择默认模型,或在此指定供应商与模型)',
32
+ 'estimator.baseUrl': '端点地址(/v1)',
33
+ 'estimator.model': '模型',
34
+ 'estimator.apiKey': 'API Key(仅写入,不回显)',
35
+ 'estimator.apiKey.placeholder': '输入端点密钥并失焦保存',
36
+ 'estimator.apiKey.set': '已设置 · 输入新值覆盖',
37
+ 'estimator.apiKey.clear': '清除',
38
+ 'estimator.apiKey.overwrite': '已设置保密值,输入新值并失焦即可覆盖。',
39
+ 'detail.tokenpilot-inspired': '在平衡模式之上叠加去重指针、恢复豁免、摘要定位块、前缀稳定与读取状态语义;估计器需另行配置端点',
40
+ 'profile.custom': 'Custom/实验模式',
41
+ 'profile.native': '原生对照',
42
+ 'profile.off': '插件关闭',
43
+ 'profile.current': '当前选择',
44
+ 'detail.balanced': '确定性压缩新工具结果;高水位时老化旧结果',
45
+ 'detail.cache-strict': '仅在确认容量压力时老化已发送历史;服务端缓存命中仍是 best-effort',
46
+ 'detail.savings': '使用更小目标并更早清理旧工具结果;不保证每个请求更便宜',
47
+ 'detail.adaptive': 'Fresh/Aggregate 与平衡模式一致;仅当紧邻官方 usage 与当前官方价格证明历史压缩明确更省钱时老化历史,否则保留',
48
+ 'detail.custom': '为新会话选择已实现的压缩阶段和计量阈值',
49
+ 'detail.native': '只使用 DeepSeek Harness 原生头尾裁剪',
50
+ 'detail.off': '关闭确定性选择器;原生 auto-compact 仍由 Harness 配置决定',
51
+ 'autoCompact.title': 'Auto Compact 触发水位',
52
+ 'autoCompact.description': '模型驱动 Auto Compact 在请求占用达到该水位时触发。调整后,标准 Profile 的 History 触发值、最小回收量与近期尾窗随水位联动;修改只影响新会话。',
53
+ 'autoCompact.inputLabel': 'Auto Compact 阈值(%)',
54
+ 'autoCompact.sliderLabel': 'Auto Compact 阈值滑杆',
55
+ 'autoCompact.quick': '快捷值',
56
+ 'autoCompact.riskLow': '低于推荐范围:更早触发会增加摘要调用与前缀重建。',
57
+ 'autoCompact.riskHigh': '高于推荐范围:上下文容量为请求与输出共享,过晚触发会减少单次大输出、推理与工具 schema 的余量。',
58
+ 'autoCompact.invalid': 'Auto Compact 阈值必须是 50–90 之间的整数。',
59
+ 'autoCompact.save': '保存 Auto Compact 阈值',
60
+ 'autoCompact.summaryHint': 'Auto Compact 阈值:{percent}%。可在设置中修改。',
61
+ 'codeSkeleton.title': '代码骨架压缩(备选)',
62
+ 'codeSkeleton.description': '正交开关:独立于上方 Profile。开启后,首次曝光的超大源码类工具结果会先尝试保留导入与声明的骨架(省略函数体并保留错误行),失败时自动回退到原头部裁剪;需要精确 tokenizer,修改只影响新会话。',
63
+ 'codeSkeleton.enabled': '代码骨架压缩',
64
+ 'codeSkeleton.enabled.on': '开',
65
+ 'codeSkeleton.enabled.off': '关(默认)',
66
+ 'custom.title': 'Custom 策略',
67
+ 'custom.settingsHint': '具体参数请前往“设置 > 上下文压缩选择器”中编辑。',
68
+ 'custom.sessionScope': '保存后的修改会在当前压缩运行时随后首次观察某个 Session 时生效;已被该运行时观察的 Session 继续使用其冻结策略。',
69
+ 'custom.measurement': '首选 DeepSeek 精确 tokenizer;不可用时回退到带校准的 tokenizer estimate,绝不使用 chars/4。缓存归因仍未知。',
70
+ 'custom.unit': '规范单位',
71
+ 'custom.unit.tokens': 'Tokens',
72
+ 'custom.unit.contextPercent': '上下文百分比',
73
+ 'custom.enabled': '是否启用',
74
+ 'custom.enabled.on': '开',
75
+ 'custom.enabled.off': '关',
76
+ 'custom.fresh.enabled': '启用 Fresh',
77
+ 'custom.fresh.trigger': 'Fresh 触发值',
78
+ 'custom.fresh.target': 'Fresh 目标值',
79
+ 'custom.aggregate.enabled': '启用 Aggregate',
80
+ 'custom.aggregate.trigger': 'Aggregate 触发值',
81
+ 'custom.aggregate.target': 'Aggregate 目标值',
82
+ 'custom.history.enabled': '启用 History',
83
+ 'custom.history.trigger': 'History 触发值',
84
+ 'custom.history.keepRecentToolCalls': '保护近期工具调用数',
85
+ 'custom.history.keepRecentTokens': '保护近期工具结果尾窗',
86
+ 'custom.history.minReclaim': '最小回收量',
87
+ 'custom.prefixPolicy': '已发送前缀策略',
88
+ 'custom.prefixPolicy.preserve': '仅在容量压力时改写',
89
+ 'custom.prefixPolicy.pressureBreak': '允许常规历史老化',
90
+ 'custom.experimental': 'Experimental:以下功能仅用于 Custom,不会加入标准 Profile。',
91
+ 'custom.tailTrim.enabled': '启用 TailTrim(实验)',
92
+ 'custom.tailTrim.trigger': 'TailTrim 触发值',
93
+ 'custom.tailTrim.warning': 'TailTrim 只在精确 tokenizer 可用时,把一个完整、已结束且非错误的纯工具组替换为可恢复引用;它与 History 共用近期工具调用数、工具结果尾窗和最小回收参数。它会改写已发送前缀,可能降低缓存命中。',
94
+ 'custom.save': '保存 Custom 策略',
95
+ 'custom.reset': '重置 Custom 策略',
96
+ 'custom.invalid': 'Custom 策略参数无效。',
97
+ } satisfies Record<string, string>
98
+
99
+ /** Locale keys that every context-compression selector dictionary must provide. */
100
+ export type ContextCompressionLocaleKey = keyof typeof zh
101
+
102
+ /** English copy matching every simplified Chinese selector key. */
103
+ export const en = {
104
+ 'nav': 'Context compression selector',
105
+ 'settings.title': 'Context compression selector',
106
+ 'settings.description': 'Choose a compression profile for the current session and configure the parameters it provides.',
107
+ 'label': 'Context compression',
108
+ 'status.loading': 'Loading',
109
+ 'status.unavailable': 'Unavailable',
110
+ 'status.presetUnavailable': 'This session’s preset does not provide context compression, or availability is not yet confirmed.',
111
+ 'status.minimalUnavailable': 'Minimal mode does not load context compression for this session. The selector is effectively off and Harness native behavior remains. Switch to Standard, PTC / Coding, Creative, or a capable custom preset to configure it.',
112
+ 'status.saveFailed': 'Save failed. Try again.',
113
+ 'pricing.disclosure': 'DeepSeek official prices checked 2026-08-25. Peak Mon–Fri 09:00–12:00 and 14:00–18:00 Asia/Shanghai; otherwise off-peak. Cross-boundary requests use a cost range.',
114
+ 'profile.balanced': 'Balanced',
115
+ 'profile.cache-strict': 'Cache Strict (prefix protection)',
116
+ 'profile.savings': 'Savings',
117
+ 'profile.adaptive': 'Adaptive (conservative cost)',
118
+ 'profile.tokenpilot-inspired': 'TokenPilot-inspired',
119
+ 'estimator.title': 'Estimator (optional)',
120
+ '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.',
121
+ 'estimator.mode': 'Channel',
122
+ 'estimator.mode.off': 'Off (rule-only)',
123
+ 'estimator.followHost': 'Follow the host default model',
124
+ 'estimator.mode.host': 'Host model (reuse configured providers)',
125
+ 'estimator.mode.direct': 'Direct OpenAI-compatible endpoint',
126
+ '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.',
127
+ 'estimator.provider': 'Provider',
128
+ 'estimator.provider.placeholder': 'Empty follows the session default model; pick from the dropdown or type a custom id',
129
+ 'estimator.model.placeholder': 'Empty follows the session default model; pick from the dropdown or type a custom id',
130
+ 'estimator.hostReuse': 'The host channel reuses the providers and credentials you already configured in DSH, so no API key is needed — and none is accepted here.',
131
+ 'estimator.hostRoute': 'Effective route: {route}.',
132
+ 'estimator.hostUnresolved': 'not determined yet (choose a default model in DSH settings, or name a provider and model here)',
133
+ 'estimator.baseUrl': 'Endpoint base URL (/v1)',
134
+ 'estimator.model': 'Model',
135
+ 'estimator.apiKey': 'API key (write-only, never echoed)',
136
+ 'estimator.apiKey.placeholder': 'Type the endpoint key; blur to save',
137
+ 'estimator.apiKey.set': 'Set · type a new value to overwrite',
138
+ 'estimator.apiKey.clear': 'Clear',
139
+ 'estimator.apiKey.overwrite': 'A secret is stored; type a new value and blur to overwrite it.',
140
+ 'detail.tokenpilot-inspired': 'Layered on Balanced: dedupe pointers, recovery exemption, summary locators, prefix stabilization, and read-state semantics; the estimator needs an endpoint configured separately',
141
+ 'profile.custom': 'Custom / Experimental',
142
+ 'profile.native': 'Native baseline',
143
+ 'profile.off': 'Plugin off',
144
+ 'profile.current': 'Current profile',
145
+ 'detail.balanced': 'Reduce fresh tool results deterministically; age old results at high watermarks',
146
+ 'detail.cache-strict': 'Age sent history only under confirmed capacity pressure; provider cache hits remain best-effort',
147
+ 'detail.savings': 'Use smaller targets and age old tool results earlier; does not guarantee a cheaper request',
148
+ 'detail.adaptive': 'Use Balanced Fresh/Aggregate; age history only when adjacent official usage and current official prices prove a clear saving',
149
+ 'detail.custom': 'Choose implemented stages and measured thresholds for new sessions',
150
+ 'detail.native': 'Use only the Harness native head/tail pruner',
151
+ 'detail.off': 'Disable the deterministic selector; native auto-compact remains separately configured',
152
+ 'autoCompact.title': 'Auto Compact trigger level',
153
+ 'autoCompact.description': 'Model-driven Auto Compact triggers once request usage crosses this level. Standard-profile History triggers, minimum reclaim, and the recent tail follow the level; changes affect new sessions only.',
154
+ 'autoCompact.inputLabel': 'Auto Compact threshold (%)',
155
+ 'autoCompact.sliderLabel': 'Auto Compact threshold slider',
156
+ 'autoCompact.quick': 'Quick values',
157
+ 'autoCompact.riskLow': 'Below the recommended band: triggering earlier increases summarization calls and prefix rebuilds.',
158
+ 'autoCompact.riskHigh': 'Above the recommended band: context capacity is shared by requests and output, so triggering later reduces headroom for single large outputs, reasoning, and tool schemas.',
159
+ 'autoCompact.invalid': 'The Auto Compact threshold must be an integer between 50 and 90.',
160
+ 'autoCompact.save': 'Save Auto Compact threshold',
161
+ 'autoCompact.summaryHint': 'Auto Compact threshold: {percent}%. Change it in Settings.',
162
+ 'codeSkeleton.title': 'Code skeleton compression (opt-in)',
163
+ 'codeSkeleton.description': 'Orthogonal switch, independent of the profiles above. When enabled, an oversized fresh source-code tool result first tries a skeleton that keeps imports and declarations (bodies elided, error lines kept) and falls back to the original head pruning on failure. Requires the exact tokenizer; changes affect new sessions only.',
164
+ 'codeSkeleton.enabled': 'Code skeleton compression',
165
+ 'codeSkeleton.enabled.on': 'On',
166
+ 'codeSkeleton.enabled.off': 'Off (default)',
167
+ 'custom.title': 'Custom policy',
168
+ 'custom.settingsHint': 'Edit detailed parameters in Settings > Context compression selector.',
169
+ 'custom.sessionScope': 'Saved changes apply when the current compression runtime next observes a Session for the first time. A Session already observed by that runtime keeps its frozen policy.',
170
+ 'custom.measurement': 'Exact DeepSeek tokenizer first; tokenizer estimate with calibration fallback. Never chars/4. Cache attribution remains unknown.',
171
+ 'custom.unit': 'Canonical unit',
172
+ 'custom.unit.tokens': 'Tokens',
173
+ 'custom.unit.contextPercent': 'Context percent',
174
+ 'custom.enabled': 'Enabled',
175
+ 'custom.enabled.on': 'On',
176
+ 'custom.enabled.off': 'Off',
177
+ 'custom.fresh.enabled': 'Enable Fresh',
178
+ 'custom.fresh.trigger': 'Fresh trigger',
179
+ 'custom.fresh.target': 'Fresh target',
180
+ 'custom.aggregate.enabled': 'Enable Aggregate',
181
+ 'custom.aggregate.trigger': 'Aggregate trigger',
182
+ 'custom.aggregate.target': 'Aggregate target',
183
+ 'custom.history.enabled': 'Enable History',
184
+ 'custom.history.trigger': 'History trigger',
185
+ 'custom.history.keepRecentToolCalls': 'Protected recent tool calls',
186
+ 'custom.history.keepRecentTokens': 'Protected recent tool-result tail',
187
+ 'custom.history.minReclaim': 'Minimum reclaim',
188
+ 'custom.prefixPolicy': 'Sent-prefix policy',
189
+ 'custom.prefixPolicy.preserve': 'Preserve until capacity pressure',
190
+ 'custom.prefixPolicy.pressureBreak': 'Allow routine history aging',
191
+ 'custom.experimental': 'Experimental: these controls are Custom-only and never added to standard profiles.',
192
+ 'custom.tailTrim.enabled': 'Enable TailTrim (experimental)',
193
+ 'custom.tailTrim.trigger': 'TailTrim trigger',
194
+ 'custom.tailTrim.warning': 'TailTrim requires the exact tokenizer and replaces at most one complete, finished, non-error tool-only group with a recoverable reference. It shares Protected recent tool calls, Protected recent tool-result tail, and Minimum reclaim with History. It rewrites a sent prefix and may reduce cache hits.',
195
+ 'custom.save': 'Save Custom policy',
196
+ 'custom.reset': 'Reset Custom policy',
197
+ 'custom.invalid': 'Custom policy values are invalid.',
198
+ } satisfies Record<ContextCompressionLocaleKey, string>