dsh-context-compression-improved 0.2.1 → 0.3.1
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.
- package/.gitattributes +1 -1
- package/.github/workflows/ci.yml +1 -7
- package/CHANGELOG.ja.md +18 -21
- package/CHANGELOG.ko.md +17 -21
- package/CHANGELOG.md +16 -17
- package/CHANGELOG.zh.md +15 -18
- package/CONTRIBUTING.md +1 -1
- package/README.ja.md +1 -20
- package/README.ko.md +12 -30
- package/README.md +1 -19
- package/README.zh.md +5 -21
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/docs/compat-0.1.5-report.md +77 -0
- package/docs/installation.ja.md +6 -4
- package/docs/installation.ko.md +9 -7
- package/docs/installation.md +3 -1
- package/docs/installation.zh.md +3 -2
- package/docs/repair-log.md +54 -33
- package/eslint.config.js +2 -2
- package/package.json +56 -36
- package/packages/selector/README.md +7 -12
- package/packages/selector/README.zh.md +6 -13
- package/packages/selector/THIRD_PARTY_NOTICES.md +2 -8
- package/packages/selector/cordis.patch.yml +7 -5
- package/packages/selector/dsh.plugin.json +2 -2
- package/packages/selector/lib/client.d.ts +31 -5
- package/packages/selector/lib/client.js +100 -82
- package/packages/selector/lib/index.d.ts +8 -20
- package/packages/selector/lib/index.js +9 -14
- package/packages/selector/lib/invariant.js +4 -4
- package/packages/selector/lib/pruner.d.ts +13 -4
- package/packages/selector/lib/pruner.js +91 -46
- package/packages/selector/lib/tail-trim.js +13 -4
- package/packages/selector/package.json +23 -23
- package/packages/selector/screenshots.json +1 -4
- package/packages/selector/src/client/CompressionProfileControls.tsx +5 -8
- package/packages/selector/src/client/CompressionProfileSelector.tsx +9 -5
- package/packages/selector/src/client/EstimatorControls.tsx +28 -32
- package/packages/selector/src/client/index.ts +101 -63
- package/packages/selector/src/client/locales.ts +0 -2
- package/packages/selector/src/client/preset-options.ts +49 -35
- package/packages/selector/src/client/settings-section.tsx +4 -6
- package/packages/selector/src/deepseek-v4-tokenizer.ts +0 -5
- package/packages/selector/src/index.ts +41 -46
- package/packages/selector/src/invariant.ts +5 -5
- package/packages/selector/src/pruner.ts +67 -36
- package/packages/selector/src/runtime/measurement.ts +6 -2
- package/packages/selector/src/runtime/session-events.ts +14 -1
- package/packages/selector/src/runtime/tail-trim.ts +4 -4
- package/packages/selector/src/runtime/tokenpilot/estimator.ts +34 -3
- package/packages/selector/src/runtime/types.ts +1 -1
- package/packages/selector/tests/auto-compact.client.spec.tsx +0 -13
- package/packages/selector/tests/built/client-artifact.spec.ts +5 -1
- package/packages/selector/tests/code-skeleton.client.spec.ts +2 -1
- package/packages/selector/tests/custom-contract.client.spec.ts +2 -1
- package/packages/selector/tests/estimator-channel.client.spec.tsx +16 -11
- package/packages/selector/tests/estimator-route-registration.host.spec.ts +12 -0
- package/packages/selector/tests/host-preset-overlay.host.spec.ts +6 -4
- package/packages/selector/tests/preset-options-write.client.spec.ts +87 -79
- package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +17 -12
- package/packages/selector/tests/preset-overlay.host.spec.ts +5 -2
- package/packages/selector/tests/profiles.client.spec.tsx +3 -3
- package/packages/selector/tests/public/package-contract.client.spec.ts +27 -3
- package/packages/selector/tests/runtime/public/public-runtime.spec.ts +103 -81
- package/packages/selector/tests/runtime/session-events.spec.ts +1 -1
- package/packages/selector/tests/settings-seat.client.spec.ts +86 -0
- package/packages/selector/tests/standing-generation.host.spec.ts +88 -71
- package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +24 -21
- package/pnpm-workspace.yaml +52 -5
- package/scripts/capture-profile-baseline.mjs +42 -0
- package/scripts/generate-tokenizer-fixtures.py +5 -5
- package/scripts/generate-vision-fixtures.py +2 -2
- package/scripts/{packed-components-smoke.ts → packed-components-smoke.mjs} +116 -119
- package/scripts/{packed-install-e2e.ts → packed-install-e2e.mjs} +188 -160
- package/scripts/{verify-release.ts → verify-release.mjs} +70 -94
- package/tests/TEST_INVENTORY.md +6 -7
- package/scripts/capture-profile-baseline.ts +0 -80
- package/tsconfig.scripts.json +0 -13
- /package/{packages/selector/assets/screenshots → docs/assets}/context-compression-selector-profiles.jpg +0 -0
- /package/{packages/selector/assets/screenshots → docs/assets}/context-compression-selector-settings.png +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
2
|
-
import type { ClientContext } from '@deepseek-ai/
|
|
2
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
3
|
+
import type {} from '@deepseek-ai/dsh-client-ui-session/client'
|
|
3
4
|
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
4
5
|
import type {} from '@deepseek-ai/dsh-client-ui-workspace/client'
|
|
5
6
|
import {
|
|
@@ -10,8 +11,33 @@ import {
|
|
|
10
11
|
import { DEFAULT_CUSTOM_COMPRESSION_POLICY } from '../profiles.ts'
|
|
11
12
|
import { decodeSettings } from './decode.ts'
|
|
12
13
|
import { en, zh } from './locales.ts'
|
|
13
|
-
import {
|
|
14
|
+
import { planPresetOptionsOps, presetOptionsOpsAccepted } from './preset-options.ts'
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Harness 0.1.5 mounts the web core's `slots` service on the client context
|
|
18
|
+
* but no longer ships a public type for it; declare the face this plugin
|
|
19
|
+
* consumes (same shape dsh-thinking-levels and dsh-prime-memory rely on).
|
|
20
|
+
* `inject` factories may be sync (return a disposer) or generator (yield the
|
|
21
|
+
* registration); slot options are validated at runtime per slot kind, which
|
|
22
|
+
* lets one entry supply both `id` (Desktop, list) and `key` (CLI, keyed).
|
|
23
|
+
*/
|
|
24
|
+
interface SlotsService {
|
|
25
|
+
inject(slot: string, register: () => (() => void) | Generator<() => void>): () => void
|
|
26
|
+
register(options: Record<string, unknown>, component: unknown): () => void
|
|
27
|
+
}
|
|
28
|
+
declare module '@deepseek-ai/cordis' {
|
|
29
|
+
interface Context {
|
|
30
|
+
slots: SlotsService
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Declare all consumed services (the official client-plugin pattern, and the
|
|
35
|
+
// shape dsh-thinking-levels proves on this same 0.1.5 host line): cordis holds
|
|
36
|
+
// apply until `locale` / `settingsScope` are provided, so registration can use
|
|
37
|
+
// them directly. Resolving them lazily via ctx.get() instead races the settings
|
|
38
|
+
// client's activation — on a loss the apply early-returned and EVERY settings
|
|
39
|
+
// entry (the standalone section, the plugins-tab card, the item card) silently
|
|
40
|
+
// vanished.
|
|
15
41
|
export const inject = ['slots', 'locale', 'settingsScope']
|
|
16
42
|
const NS = 'context-compression'
|
|
17
43
|
|
|
@@ -40,69 +66,81 @@ function sameCustomPolicy(
|
|
|
40
66
|
}
|
|
41
67
|
|
|
42
68
|
export function apply(ctx: ClientContext): void {
|
|
43
|
-
ctx.
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
ctx.locale.register(NS, { zh, en })
|
|
70
|
+
const injected = (): CompressionSelectorInjected => {
|
|
71
|
+
// Bind per factory call on the caller's fiber (0.1.5: activation must
|
|
72
|
+
// never block on the settings transport; the scope disposer belongs to
|
|
73
|
+
// the calling registration's lifecycle).
|
|
74
|
+
const scope = ctx.settingsScope.bind<ContextCompressionSettings>({ namespace: NS, decode: decodeSettings })
|
|
75
|
+
const writeAndConfirm = async (
|
|
76
|
+
write: () => Promise<void>,
|
|
77
|
+
accepts: (settings: ContextCompressionSettings) => boolean,
|
|
78
|
+
): Promise<void> => {
|
|
79
|
+
const beforeRevision = scope.getSnapshot().revision
|
|
80
|
+
await write()
|
|
81
|
+
const after = scope.getSnapshot()
|
|
82
|
+
if (
|
|
83
|
+
after.status !== 'ready'
|
|
84
|
+
|| after.value === undefined
|
|
85
|
+
|| after.revision === beforeRevision
|
|
86
|
+
|| !accepts(after.value)
|
|
87
|
+
) {
|
|
88
|
+
throw new Error('Context compression settings were not saved.')
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
hooks: { compression: scope },
|
|
93
|
+
select: profile => writeAndConfirm(
|
|
94
|
+
() => scope.set('profile', profile),
|
|
95
|
+
settings => settings.profile === profile,
|
|
96
|
+
),
|
|
97
|
+
saveCustom: custom => writeAndConfirm(
|
|
98
|
+
() => scope.set('custom', custom),
|
|
99
|
+
settings => isCustomCompressionPolicy(settings.custom)
|
|
100
|
+
&& sameCustomPolicy(settings.custom, custom),
|
|
101
|
+
),
|
|
102
|
+
resetCustom: () => writeAndConfirm(
|
|
103
|
+
() => scope.set('custom', structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY)),
|
|
104
|
+
settings => isCustomCompressionPolicy(settings.custom)
|
|
105
|
+
&& sameCustomPolicy(settings.custom, DEFAULT_CUSTOM_COMPRESSION_POLICY),
|
|
106
|
+
),
|
|
107
|
+
saveAutoCompact: thresholdPercent => writeAndConfirm(
|
|
108
|
+
() => scope.set('autoCompact', { thresholdPercent }),
|
|
109
|
+
settings => settings.autoCompact.thresholdPercent === thresholdPercent,
|
|
110
|
+
),
|
|
111
|
+
saveCodeSkeleton: enabled => writeAndConfirm(
|
|
112
|
+
() => scope.set('codeSkeleton', { enabled }),
|
|
113
|
+
settings => settings.codeSkeleton.enabled === enabled,
|
|
114
|
+
),
|
|
115
|
+
savePresetOptions: options => {
|
|
116
|
+
// Path-addressed so one field write cannot delete its siblings: the
|
|
117
|
+
// previous whole-section set erased estimatorMode (and every other
|
|
118
|
+
// override) whenever the user touched a second field.
|
|
119
|
+
const ops = planPresetOptionsOps(scope.getSnapshot().value?.presetOptions, options)
|
|
120
|
+
if (ops.length === 0) return Promise.resolve()
|
|
121
|
+
return writeAndConfirm(
|
|
122
|
+
() => scope.mutate(ops),
|
|
123
|
+
settings => presetOptionsOpsAccepted(settings.presetOptions, ops),
|
|
124
|
+
)
|
|
125
|
+
},
|
|
59
126
|
}
|
|
60
127
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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))
|
|
128
|
+
// 设置 → 上下文压缩 直挂分节(0.1.1 契约;0.1.5 官方分节也注册在此,未声明槽
|
|
129
|
+
// 的注册会在激活期抛错,故 try/catch 守卫 —— 同 dsh-prime-memory 的双槽冗余)。
|
|
130
|
+
// 只挂这一处:再注册 `settings.plugins.tab` / `settings.plugin.item` 会在
|
|
131
|
+
// 设置里同时出现独立分节和插件卡片,重复展示同一张面板。
|
|
132
|
+
try {
|
|
133
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
134
|
+
name: 'settings.section',
|
|
135
|
+
id: 'context-compression',
|
|
136
|
+
order: 17,
|
|
137
|
+
label: () => ctx.locale.bind(NS)('nav'),
|
|
138
|
+
locale: NS,
|
|
139
|
+
inject: injected,
|
|
140
|
+
}, ContextCompressionSettingsSection))
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.warn('[dsh-context-compression-improved] settings.section 注册失败(新宿主已收编):', error)
|
|
143
|
+
}
|
|
106
144
|
}
|
|
107
145
|
|
|
108
146
|
export type {
|
|
@@ -19,7 +19,6 @@ export const zh = {
|
|
|
19
19
|
'estimator.description': '辅助小模型零样本判断旧文件读取是否仍有引用价值,仅建议性加速历史清理;未配置或失败时自动退回纯规则通道,不影响主流程。',
|
|
20
20
|
'estimator.mode': '通道',
|
|
21
21
|
'estimator.mode.off': '关闭(纯规则)',
|
|
22
|
-
'estimator.followHost': '跟随宿主默认模型',
|
|
23
22
|
'estimator.mode.host': '宿主模型(复用已配置供应商)',
|
|
24
23
|
'estimator.mode.direct': '直连 OpenAI 兼容端点',
|
|
25
24
|
'estimator.inactive': '估计器只随「TokenPilot 启发模式」提供:该模式的预设选项(去重指针、摘要定位块、读取状态语义与估计器通道)不会合并进其他 Profile,因此当前 Profile「{profile}」下没有可配置的估计器通道。选择「TokenPilot 启发模式」后,本区块会出现「通道」选择,可复用已配置供应商(宿主模型)或直连 OpenAI 兼容端点。',
|
|
@@ -120,7 +119,6 @@ export const en = {
|
|
|
120
119
|
'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
120
|
'estimator.mode': 'Channel',
|
|
122
121
|
'estimator.mode.off': 'Off (rule-only)',
|
|
123
|
-
'estimator.followHost': 'Follow the host default model',
|
|
124
122
|
'estimator.mode.host': 'Host model (reuse configured providers)',
|
|
125
123
|
'estimator.mode.direct': 'Direct OpenAI-compatible endpoint',
|
|
126
124
|
'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.',
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Path-addressed writes for the `presetOptions` section of the
|
|
3
3
|
* context-compression settings namespace.
|
|
4
4
|
*
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* and the merged result is what gets written.
|
|
5
|
+
* The settings Host applies a `set` op AT its path, so a field write must name
|
|
6
|
+
* only the field it changes. Writing the whole section — `scope.set(
|
|
7
|
+
* 'presetOptions', patch)` — replaces the section root with the patch, which
|
|
8
|
+
* deleted every sibling field: choosing a provider after choosing a channel
|
|
9
|
+
* erased `estimatorMode` and silently turned the estimator back off, and the
|
|
10
|
+
* saved document kept exactly the last field the user touched.
|
|
12
11
|
*
|
|
13
|
-
* A patch member set to `undefined` means "clear this field" (
|
|
14
|
-
* re-inherits the preset default); absent members
|
|
12
|
+
* A patch member set to `undefined` means "clear this field" (the section then
|
|
13
|
+
* re-inherits the preset default) rather than "leave it alone"; absent members
|
|
14
|
+
* are untouched by construction.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import type { SettingsPathOpView } from '@deepseek-ai/dsh-settings/types'
|
|
17
18
|
import type { PresetOptionsSettings } from '../profiles.ts'
|
|
18
19
|
|
|
20
|
+
/** The namespace key holding every tokenpilot-inspired sub-capability override. */
|
|
21
|
+
const PRESET_OPTIONS_KEY = 'presetOptions'
|
|
22
|
+
|
|
19
23
|
/** Every field a patch may address, in the schema's own order. */
|
|
20
24
|
const PRESET_OPTION_KEYS = [
|
|
21
25
|
'dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'estimatorMode',
|
|
@@ -28,43 +32,53 @@ export type PresetOptionsPatch = {
|
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
/**
|
|
31
|
-
*
|
|
35
|
+
* Plan the path ops one patch needs against the section currently stored.
|
|
36
|
+
*
|
|
37
|
+
* Fields already at their requested value produce no op, so a re-selected
|
|
38
|
+
* value neither rewrites the document nor reports a save the Host never
|
|
39
|
+
* committed.
|
|
32
40
|
*
|
|
33
41
|
* @param current - the decoded `presetOptions` section, when one is stored.
|
|
34
42
|
* @param patch - the fields to write or clear.
|
|
35
|
-
* @returns the
|
|
43
|
+
* @returns the ordered ops, empty when the patch changes nothing.
|
|
36
44
|
*/
|
|
37
|
-
export function
|
|
45
|
+
export function planPresetOptionsOps(
|
|
38
46
|
current: PresetOptionsSettings | undefined,
|
|
39
47
|
patch: PresetOptionsPatch,
|
|
40
|
-
):
|
|
41
|
-
const
|
|
42
|
-
const
|
|
48
|
+
): SettingsPathOpView[] {
|
|
49
|
+
const stored = current as Record<string, unknown> | undefined
|
|
50
|
+
const ops: SettingsPathOpView[] = []
|
|
43
51
|
for (const key of PRESET_OPTION_KEYS) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
if (!Object.hasOwn(patch, key)) continue
|
|
53
|
+
const value = patch[key]
|
|
54
|
+
const path = [PRESET_OPTIONS_KEY, key]
|
|
55
|
+
if (value === undefined) {
|
|
56
|
+
if (stored?.[key] !== undefined) ops.push({ op: 'unset', path })
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
if (stored?.[key] === value) continue
|
|
60
|
+
ops.push({ op: 'set', path, value })
|
|
51
61
|
}
|
|
52
|
-
return
|
|
62
|
+
return ops
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
/**
|
|
56
|
-
*
|
|
57
|
-
* the document nor reports a save the Host never committed.
|
|
66
|
+
* Test whether one resolved settings document already carries every planned op.
|
|
58
67
|
*
|
|
59
|
-
* @param
|
|
60
|
-
* @param
|
|
61
|
-
* @returns whether every
|
|
68
|
+
* @param settings - the resolved namespace section after a write.
|
|
69
|
+
* @param ops - the ops that write submitted.
|
|
70
|
+
* @returns whether every named field holds its requested state.
|
|
62
71
|
*/
|
|
63
|
-
export function
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
export function presetOptionsOpsAccepted(
|
|
73
|
+
presetOptions: PresetOptionsSettings | undefined,
|
|
74
|
+
ops: readonly SettingsPathOpView[],
|
|
66
75
|
): boolean {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
if (ops.length === 0) return true
|
|
77
|
+
if (presetOptions === undefined) return false
|
|
78
|
+
const stored = presetOptions as Record<string, unknown>
|
|
79
|
+
return ops.every((op) => {
|
|
80
|
+
const key = op.path[op.path.length - 1]
|
|
81
|
+
if (key === undefined) return false
|
|
82
|
+
return op.op === 'set' ? stored[key] === op.value : stored[key] === undefined
|
|
83
|
+
})
|
|
70
84
|
}
|
|
@@ -25,15 +25,13 @@ export function ContextCompressionSettingsSection(props: CompressionProfileSelec
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export function SettingsCompressionProfileControls({
|
|
28
|
-
useCompression,
|
|
28
|
+
useCompression, select, saveCustom, resetCustom, saveAutoCompact, saveCodeSkeleton,
|
|
29
29
|
savePresetOptions, t,
|
|
30
30
|
}: CompressionProfileSelectorProps) {
|
|
31
31
|
const state = useCompression(snapshot => snapshot)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
})
|
|
36
|
-
const selectorAvailable = currentPreset !== 'minimal'
|
|
32
|
+
// Harness 0.1.5 dropped browser-side agentPreset from session summaries, so
|
|
33
|
+
// the old Minimal-preset gate has no data source; the selector stays enabled.
|
|
34
|
+
const selectorAvailable = true
|
|
37
35
|
const [saving, setSaving] = useState(false)
|
|
38
36
|
const [saveError, setSaveError] = useState<string | null>(null)
|
|
39
37
|
const [draft, setDraft] = useState<CustomCompressionPolicyV3 | null>(null)
|
|
@@ -81,11 +81,6 @@ interface TokenizerArtifact {
|
|
|
81
81
|
* Every bundled artifact is registered with independent asset roots, integrity
|
|
82
82
|
* manifests, and cache entries: one corrupted artifact must never disable the
|
|
83
83
|
* tokenizer serving the other model family.
|
|
84
|
-
*
|
|
85
|
-
* This module deliberately sits at the package's `src/` root rather than in
|
|
86
|
-
* `src/runtime/`: the build flattens the runtime into `lib/*.js`, so only a
|
|
87
|
-
* module one level below the package root resolves `../assets/` identically
|
|
88
|
-
* from source and from the published artifact.
|
|
89
84
|
*/
|
|
90
85
|
const ARTIFACTS: readonly TokenizerArtifact[] = Object.freeze([
|
|
91
86
|
Object.freeze({
|
|
@@ -6,18 +6,28 @@ import {
|
|
|
6
6
|
type SettingsScope,
|
|
7
7
|
type default as SettingsService,
|
|
8
8
|
} from '@deepseek-ai/dsh-settings'
|
|
9
|
+
import { buildEstimatorCatalog, type EstimatorCatalogDeps } from './estimator-catalog.ts'
|
|
9
10
|
import {
|
|
10
11
|
CONTEXT_COMPRESSION_SETTINGS_NAMESPACE,
|
|
11
12
|
ContextCompressionSettingsSchema,
|
|
12
13
|
} from './runtime/config.ts'
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
// The settings namespace literal and the settings schema are owned by the
|
|
16
|
+
// runtime config module. Both were once inlined/replaced here to dodge a
|
|
17
|
+
// cross-package dependency (ab2175a: the namespace literal was duplicated and
|
|
18
|
+
// the schema was downgraded to `z.any()`); the runtime is now part of this
|
|
19
|
+
// package, so the real schema is back and the daily Custom defaults are
|
|
20
|
+
// published again through settings.register(). The namespace value is
|
|
21
|
+
// unchanged ('context-compression').
|
|
22
|
+
const CONTEXT_COMPRESSION_NAMESPACE = CONTEXT_COMPRESSION_SETTINGS_NAMESPACE as never
|
|
14
23
|
import {
|
|
15
24
|
decorateAgentPresets,
|
|
16
25
|
resolveCompressionModulePaths,
|
|
17
26
|
} from './preset-overlay.ts'
|
|
18
27
|
|
|
19
|
-
// 0.1.1/0.1.2
|
|
20
|
-
//
|
|
28
|
+
// 0.1.1/0.1.2 clients ask for /endpoint; 0.1.5 and later ask for /api. Both
|
|
29
|
+
// absolute paths are registered (each is its own (kind, path) table entry) and
|
|
30
|
+
// one handler serves both prefixes. The bundled client only probes /api.
|
|
21
31
|
const ESTIMATOR_CATALOG_ROUTES = [
|
|
22
32
|
'/endpoint/dsh-context-compression-improved/estimator-catalog',
|
|
23
33
|
'/api/dsh-context-compression-improved/estimator-catalog',
|
|
@@ -30,13 +40,13 @@ const ESTIMATOR_CATALOG_ROUTES = [
|
|
|
30
40
|
*/
|
|
31
41
|
const ESTIMATOR_CATALOG_ROUTE_DEPS: readonly ['webServer'] = ['webServer']
|
|
32
42
|
|
|
33
|
-
/**
|
|
43
|
+
/** The minimal face of the DSH `webServer` service this plugin uses. */
|
|
34
44
|
interface WebServerLike {
|
|
35
|
-
register
|
|
45
|
+
register(route: {
|
|
36
46
|
kind: 'exact'
|
|
37
47
|
path: string
|
|
38
|
-
handler: (req: unknown, res: unknown) =>
|
|
39
|
-
}) =>
|
|
48
|
+
handler: (req: unknown, res: unknown) => unknown
|
|
49
|
+
}): () => void
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
/** The estimator-side service the catalog handler enriches its response with. */
|
|
@@ -46,28 +56,22 @@ interface AgentDefaultModelLike {
|
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
function asWebServer(value: unknown): WebServerLike | undefined {
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
if (typeof value !== 'object' || value === null) return undefined
|
|
60
|
+
const candidate = value as { register?: unknown }
|
|
51
61
|
// Hand back the service itself -- never a wrapper re-exporting `register`.
|
|
52
62
|
// The host reads its route tables off `this` (`this.exact` / `this.prefixes`),
|
|
53
63
|
// so a detached call makes `this` the wrapper and throws "Cannot read
|
|
54
64
|
// properties of undefined (reading 'has')" inside the host, after which the
|
|
55
|
-
// route is simply absent and the client sees a bare 404.
|
|
56
|
-
|
|
65
|
+
// route is simply absent and the client sees a bare 404. The 0.1.5 branch
|
|
66
|
+
// never had that defect; take this implementation, not the 0.1.2 history's.
|
|
67
|
+
return typeof candidate.register === 'function' ? (value as WebServerLike) : undefined
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
/**
|
|
60
71
|
* Serve `GET /api/dsh-context-compression-improved/estimator-catalog` — the
|
|
61
72
|
* settings card's host-route dropdowns (live provider/model groups from the DSH
|
|
62
|
-
* `llm` service plus the effective selection).
|
|
63
|
-
* plugin
|
|
64
|
-
*
|
|
65
|
-
* (The isolation reason this placement was originally justified with — "a route
|
|
66
|
-
* registered there can never reach `webServer` across the isolation boundary" —
|
|
67
|
-
* is **unverified**: no `@deepseek-ai` package calls `.isolate(`, so there is no
|
|
68
|
-
* boundary to cross here. Top-level placement is still the right choice, for a
|
|
69
|
-
* reason that needs no framework rule: the route is host-wide, not
|
|
70
|
-
* per-pruner-instance. Don't promote the isolation wording into a rule.)
|
|
73
|
+
* `llm` service plus the effective selection). Best effort: without the
|
|
74
|
+
* `webServer` service the plugin keeps working, only the HTTP API is missing.
|
|
71
75
|
*
|
|
72
76
|
* The route gates on `webServer` **alone**. `llm` and `agentDefaultModel` only
|
|
73
77
|
* enrich the response and are resolved per request, so listing them here would
|
|
@@ -162,9 +166,7 @@ function registerEstimatorCatalogRoute(ctx: Context): void {
|
|
|
162
166
|
log('warn', 'context-compression webServer not active yet — estimator catalog route pending: %s', ESTIMATOR_CATALOG_ROUTES.join(', '))
|
|
163
167
|
}
|
|
164
168
|
|
|
165
|
-
|
|
166
|
-
// same public literal at SettingsProvider.register/get instead.
|
|
167
|
-
const CONTEXT_COMPRESSION_NAMESPACE = CONTEXT_COMPRESSION_SETTINGS_NAMESPACE as never
|
|
169
|
+
|
|
168
170
|
|
|
169
171
|
/** Shared state forwarded through every Cordis proxy of one settings service. */
|
|
170
172
|
interface SharedSettingsRegistration {
|
|
@@ -196,27 +198,15 @@ export interface Config {
|
|
|
196
198
|
/** Add the canonical compression stack to every non-Minimal preset. */
|
|
197
199
|
presetOverlay?: boolean
|
|
198
200
|
/**
|
|
199
|
-
*
|
|
200
|
-
* `inject: [webServer]`.
|
|
201
|
-
*
|
|
202
|
-
* **Measured, and it contradicts the note this field was introduced with.**
|
|
203
|
-
* The original justification — "registering a route authorizes against the
|
|
204
|
-
* calling fiber, and a fiber that has not declared `webServer` cannot reach
|
|
205
|
-
* it, not even through `ctx.inject` or `ctx.get`" — is wrong on both halves:
|
|
206
|
-
* the host's `register` performs no authorization at all (it reads
|
|
207
|
-
* `this.exact` / `this.prefixes` and throws only on a duplicate
|
|
208
|
-
* `(kind, path)`), and `ctx.get(name, strict)` checks only that the providing
|
|
209
|
-
* fiber is active (`state === 2`), never the caller's `inject` list. The one
|
|
210
|
-
* inject-gated path is the `ctx.webServer` **property** access, which this
|
|
211
|
-
* plugin never uses: `registerEstimatorCatalogRoute` uses `ctx.get` plus its
|
|
212
|
-
* own `ctx.inject(['webServer'], …)`.
|
|
201
|
+
* Register the estimator-catalog HTTP route on this row.
|
|
213
202
|
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
203
|
+
* The standalone Bundle patch sets this on its own row (which declares
|
|
204
|
+
* `inject: [webServer]`), so profiles without a host web server never mount a
|
|
205
|
+
* row that could only announce a pending route. The row-level `inject` is
|
|
206
|
+
* belt-and-braces: `dsh-host-webserver.register` performs no authorization
|
|
207
|
+
* check and `ctx.get(name)` only asks whether the providing fiber is active,
|
|
208
|
+
* so the two-channel registration inside this function is what actually
|
|
209
|
+
* covers both arrival orders.
|
|
220
210
|
*/
|
|
221
211
|
estimatorCatalogRoute?: boolean
|
|
222
212
|
}
|
|
@@ -239,6 +229,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
239
229
|
acquireSettingsRegistration(settingsCtx)
|
|
240
230
|
})
|
|
241
231
|
|
|
232
|
+
// The settings card's host-route dropdowns read this route; it lives on the
|
|
233
|
+
// top-level plugin fiber, not inside the isolated `toolResultPruner`
|
|
234
|
+
// service, because the route is host-wide rather than per-pruner-instance.
|
|
242
235
|
if (config.estimatorCatalogRoute === true) registerEstimatorCatalogRoute(ctx)
|
|
243
236
|
|
|
244
237
|
if (config.presetOverlay !== true) return
|
|
@@ -255,7 +248,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
255
248
|
presetsCtx.effect(() => () => installation.dispose(), 'contextCompressionSelector.agentPresets()')
|
|
256
249
|
})
|
|
257
250
|
} catch (error) {
|
|
258
|
-
console.error('context-compression apply
|
|
251
|
+
console.error('context-compression selector apply failed: %o', error)
|
|
259
252
|
throw error
|
|
260
253
|
}
|
|
261
254
|
}
|
|
@@ -268,8 +261,10 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
268
261
|
function resolveAutoCompactThresholdPercent(presetsCtx: Context): number {
|
|
269
262
|
const raw = presetsCtx.get('settings')?.get(CONTEXT_COMPRESSION_NAMESPACE)
|
|
270
263
|
try {
|
|
271
|
-
const
|
|
272
|
-
|
|
264
|
+
const record = structuredClone(raw) as Record<string, unknown> | undefined
|
|
265
|
+
const threshold = record?.autoCompact as { thresholdPercent?: number } | undefined
|
|
266
|
+
const value = typeof threshold?.thresholdPercent === 'number' ? threshold.thresholdPercent : 80
|
|
267
|
+
return Number.isFinite(value) && value >= 50 && value <= 90 ? value : 80
|
|
273
268
|
} catch {
|
|
274
269
|
return 80
|
|
275
270
|
}
|
|
@@ -21,8 +21,8 @@ function resemblesCompanion(prune: PruneEvent, event: SessionEvent): boolean {
|
|
|
21
21
|
if ((event.type !== 'tool/result' && event.type !== 'user/message')
|
|
22
22
|
|| typeof event.surfaceOp !== 'object') return false
|
|
23
23
|
const sources = new Set(event.sourceEventSeqs ?? [])
|
|
24
|
-
return (event.surfaceOp.
|
|
25
|
-
&& event.surfaceOp.
|
|
24
|
+
return (event.surfaceOp.startSeq === prune.data.shadowedRange.start
|
|
25
|
+
&& event.surfaceOp.endSeq === prune.data.shadowedRange.end)
|
|
26
26
|
|| prune.data.shadowedSeqs.some(seq => sources.has(seq))
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -39,11 +39,11 @@ function validateCompanion(
|
|
|
39
39
|
}
|
|
40
40
|
const { shadowedRange, shadowedSeqs } = prune.data
|
|
41
41
|
if (shadowedSeqs.length === 0
|
|
42
|
-
|| shadowedSeqs[0] !== shadowedRange.start
|
|
43
|
-
|| shadowedSeqs.at(-1) !== shadowedRange.end) {
|
|
42
|
+
|| shadowedSeqs[0] !== Number(shadowedRange.start)
|
|
43
|
+
|| shadowedSeqs.at(-1) !== Number(shadowedRange.end)) {
|
|
44
44
|
fail(`compaction/prune at seq ${prune.seq} has a shadowed range inconsistent with shadowedSeqs`)
|
|
45
45
|
}
|
|
46
|
-
if (event.surfaceOp.
|
|
46
|
+
if (event.surfaceOp.startSeq !== shadowedRange.start || event.surfaceOp.endSeq !== shadowedRange.end) {
|
|
47
47
|
fail(`replacement at seq ${event.seq} does not replace compaction/prune range ${shadowedRange.start}-${shadowedRange.end}`)
|
|
48
48
|
}
|
|
49
49
|
const sources = new Set(event.sourceEventSeqs ?? [])
|