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.
- package/.gitattributes +1 -0
- package/.github/workflows/ci.yml +39 -0
- package/CHANGELOG.ja.md +39 -0
- package/CHANGELOG.ko.md +39 -0
- package/CHANGELOG.md +135 -0
- package/CHANGELOG.zh.md +39 -0
- package/CONTRIBUTING.md +22 -0
- package/README.ja.md +104 -0
- package/README.ko.md +103 -0
- package/README.md +89 -12
- package/README.zh.md +87 -12
- package/SECURITY.md +18 -0
- package/THIRD_PARTY_NOTICES.md +7 -31
- package/docs/installation.ja.md +76 -0
- package/docs/installation.ko.md +76 -0
- package/docs/installation.md +76 -0
- package/docs/installation.zh.md +76 -0
- package/docs/repair-log.md +582 -0
- package/eslint.config.js +30 -0
- package/package.json +84 -81
- package/packages/selector/LICENSE +21 -0
- package/packages/selector/README.md +26 -0
- package/packages/selector/README.zh.md +26 -0
- package/packages/selector/THIRD_PARTY_NOTICES.md +38 -0
- package/packages/selector/docs/history-tool-call-working-set-spec.md +112 -0
- package/packages/selector/docs/native-tool-result-selector-spec.md +34 -0
- package/packages/selector/docs/subagent-cache-reuse-spec.md +46 -0
- package/packages/selector/lib/style.css +308 -0
- package/packages/selector/package.json +115 -0
- package/{screenshots.json → packages/selector/screenshots.json} +6 -6
- package/packages/selector/src/client/CompressionProfileControls.tsx +229 -0
- package/packages/selector/src/client/CompressionProfileSelector.module.css +170 -0
- package/packages/selector/src/client/CompressionProfileSelector.tsx +79 -0
- package/packages/selector/src/client/CustomPolicyEditor.tsx +216 -0
- package/packages/selector/src/client/EstimatorControls.tsx +281 -0
- package/packages/selector/src/client/decode.ts +49 -0
- package/packages/selector/src/client/index.ts +111 -0
- package/packages/selector/src/client/locales.ts +198 -0
- package/packages/selector/src/client/preset-options.ts +70 -0
- package/packages/selector/src/client/settings-section.tsx +126 -0
- package/packages/selector/src/css-modules.d.ts +6 -0
- package/packages/selector/src/deepseek-v4-tokenizer.ts +210 -0
- package/packages/selector/src/estimator-catalog.ts +104 -0
- package/packages/selector/src/index.ts +327 -0
- package/packages/selector/src/invariant.ts +113 -0
- package/packages/selector/src/preset-overlay.ts +567 -0
- package/packages/selector/src/profiles.ts +342 -0
- package/packages/selector/src/pruner/content.ts +188 -0
- package/packages/selector/src/pruner/session.ts +94 -0
- package/packages/selector/src/pruner/state.ts +43 -0
- package/packages/selector/src/pruner/tuning.ts +23 -0
- package/packages/selector/src/pruner/types.ts +60 -0
- package/packages/selector/src/pruner.ts +2144 -0
- package/packages/selector/src/runtime/adaptive-cost.ts +194 -0
- package/packages/selector/src/runtime/audit.ts +215 -0
- package/packages/selector/src/runtime/config.ts +613 -0
- package/packages/selector/src/runtime/custom-policy.ts +278 -0
- package/packages/selector/src/runtime/deepseek-official-pricing.ts +298 -0
- package/packages/selector/src/runtime/deepseek-v4-vision-tokens.ts +254 -0
- package/packages/selector/src/runtime/measurement.ts +403 -0
- package/packages/selector/src/runtime/reducers.ts +656 -0
- package/packages/selector/src/runtime/retrieve.ts +457 -0
- package/packages/selector/src/runtime/session-events.ts +17 -0
- package/packages/selector/src/runtime/tail-trim.ts +166 -0
- package/packages/selector/src/runtime/token-count.ts +72 -0
- package/packages/selector/src/runtime/tokenpilot/dedup.ts +81 -0
- package/packages/selector/src/runtime/tokenpilot/estimator.ts +183 -0
- package/packages/selector/src/runtime/tokenpilot/locator.ts +128 -0
- package/packages/selector/src/runtime/tokenpilot/read-state.ts +77 -0
- package/packages/selector/src/runtime/types.ts +309 -0
- package/packages/selector/src/runtime/value.ts +48 -0
- package/packages/selector/tests/auto-compact.client.spec.tsx +226 -0
- package/packages/selector/tests/built/client-artifact.spec.ts +51 -0
- package/packages/selector/tests/cache-prefix-audit.spec.ts +123 -0
- package/packages/selector/tests/code-skeleton.client.spec.ts +88 -0
- package/packages/selector/tests/custom-contract.client.spec.ts +202 -0
- package/packages/selector/tests/estimator-catalog.spec.ts +70 -0
- package/packages/selector/tests/estimator-channel.client.spec.tsx +247 -0
- package/packages/selector/tests/estimator-route-registration.host.spec.ts +176 -0
- package/packages/selector/tests/host-preset-overlay.host.spec.ts +204 -0
- package/packages/selector/tests/preset-options-write.client.spec.ts +181 -0
- package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +196 -0
- package/packages/selector/tests/preset-overlay.host.spec.ts +243 -0
- package/packages/selector/tests/profiles.client.spec.tsx +434 -0
- package/packages/selector/tests/public/package-contract.client.spec.ts +33 -0
- package/packages/selector/tests/runtime/adaptive-cost.spec.ts +167 -0
- package/packages/selector/tests/runtime/audit.spec.ts +129 -0
- package/packages/selector/tests/runtime/auto-compact-config.spec.ts +523 -0
- package/packages/selector/tests/runtime/code-skeleton.spec.ts +141 -0
- package/packages/selector/tests/runtime/deepseek-official-pricing.spec.ts +186 -0
- package/packages/selector/tests/runtime/deepseek-v4-tokenizer.spec.ts +122 -0
- package/packages/selector/tests/runtime/deepseek-v4-vision-tokens.spec.ts +122 -0
- package/packages/selector/tests/runtime/fixtures/profile-baseline.json +273 -0
- package/packages/selector/tests/runtime/fixtures/tokenizer-golden.json +106 -0
- package/packages/selector/tests/runtime/fixtures/vision-golden.json +459 -0
- package/packages/selector/tests/runtime/public/public-runtime.spec.ts +2531 -0
- package/packages/selector/tests/runtime/session-events.spec.ts +27 -0
- package/packages/selector/tests/runtime/tokenizer-golden.spec.ts +53 -0
- package/packages/selector/tests/runtime/tokenpilot/dedup.spec.ts +52 -0
- package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +56 -0
- package/packages/selector/tests/runtime/tokenpilot/locator.spec.ts +76 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +100 -0
- package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +58 -0
- package/packages/selector/tests/runtime/value.spec.ts +23 -0
- package/packages/selector/tests/standing-generation.host.spec.ts +631 -0
- package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +250 -0
- package/packages/selector/tests/support/cache-prefix-audit.ts +105 -0
- package/packages/selector/tests/support/mock-adapter.ts +37 -0
- package/packages/selector/tests/support/ui-primitives.tsx +34 -0
- package/packages/selector/tsconfig.json +11 -0
- package/packages/selector/tsdown.client.config.ts +102 -0
- package/packages/selector/tsdown.config.ts +20 -0
- package/pnpm-workspace.yaml +19 -0
- package/scripts/capture-profile-baseline.ts +80 -0
- package/scripts/generate-tokenizer-fixtures.py +81 -0
- package/scripts/generate-vision-fixtures.py +208 -0
- package/scripts/packed-components-smoke.ts +713 -0
- package/scripts/packed-install-e2e.ts +1072 -0
- package/scripts/verify-release.ts +300 -0
- package/tests/TEST_INVENTORY.md +42 -0
- package/tsconfig.base.json +18 -0
- package/tsconfig.json +7 -0
- package/tsconfig.scripts.json +13 -0
- package/tsconfig.tests.json +15 -0
- package/vitest.built.config.ts +9 -0
- package/vitest.config.ts +43 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/LICENSE.DeepSeek-V4-Pro.txt +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/manifest.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer_config.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/LICENSE.DeepSeek-V4-Flash-Vision-Exp.txt +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/manifest.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer.json +0 -0
- /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer_config.json +0 -0
- /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-profiles.jpg +0 -0
- /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-settings.png +0 -0
- /package/{cordis.patch.yml → packages/selector/cordis.patch.yml} +0 -0
- /package/{dsh.plugin.json → packages/selector/dsh.plugin.json} +0 -0
- /package/{lib → packages/selector/lib}/client.d.ts +0 -0
- /package/{lib → packages/selector/lib}/client.js +0 -0
- /package/{lib → packages/selector/lib}/config.js +0 -0
- /package/{lib → packages/selector/lib}/index.d.ts +0 -0
- /package/{lib → packages/selector/lib}/index.js +0 -0
- /package/{lib → packages/selector/lib}/invariant.d.ts +0 -0
- /package/{lib → packages/selector/lib}/invariant.js +0 -0
- /package/{lib → packages/selector/lib}/pruner.d.ts +0 -0
- /package/{lib → packages/selector/lib}/pruner.js +0 -0
- /package/{lib → packages/selector/lib}/tail-trim.js +0 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/** Host owner of the context-compression preference consumed by the browser selector. */
|
|
2
|
+
|
|
3
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
4
|
+
import z from '@deepseek-ai/schemastery'
|
|
5
|
+
import {
|
|
6
|
+
type SettingsScope,
|
|
7
|
+
type default as SettingsService,
|
|
8
|
+
} from '@deepseek-ai/dsh-settings'
|
|
9
|
+
import {
|
|
10
|
+
CONTEXT_COMPRESSION_SETTINGS_NAMESPACE,
|
|
11
|
+
ContextCompressionSettingsSchema,
|
|
12
|
+
} from './runtime/config.ts'
|
|
13
|
+
import { buildEstimatorCatalog, type EstimatorCatalogDeps } from './estimator-catalog.ts'
|
|
14
|
+
import {
|
|
15
|
+
decorateAgentPresets,
|
|
16
|
+
resolveCompressionModulePaths,
|
|
17
|
+
} from './preset-overlay.ts'
|
|
18
|
+
|
|
19
|
+
// 0.1.1/0.1.2 客户端 API 前缀是 /endpoint,0.1.5 起改为 /api —— 两条绝对路径都
|
|
20
|
+
// 注册(各自的 (kind, path) 表项),一份处理器服务两个前缀。
|
|
21
|
+
const ESTIMATOR_CATALOG_ROUTES = [
|
|
22
|
+
'/endpoint/dsh-context-compression-improved/estimator-catalog',
|
|
23
|
+
'/api/dsh-context-compression-improved/estimator-catalog',
|
|
24
|
+
] as const
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The one service the catalog route actually needs. `llm` and
|
|
28
|
+
* `agentDefaultModel` are payload enrichment the handler resolves per request,
|
|
29
|
+
* never reasons to withhold the route.
|
|
30
|
+
*/
|
|
31
|
+
const ESTIMATOR_CATALOG_ROUTE_DEPS: readonly ['webServer'] = ['webServer']
|
|
32
|
+
|
|
33
|
+
/** Runtime detection of the host web server (same pattern as dsh-perm-gate). */
|
|
34
|
+
interface WebServerLike {
|
|
35
|
+
register: (spec: {
|
|
36
|
+
kind: 'exact'
|
|
37
|
+
path: string
|
|
38
|
+
handler: (req: unknown, res: unknown) => void
|
|
39
|
+
}) => unknown
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The estimator-side service the catalog handler enriches its response with. */
|
|
43
|
+
interface AgentDefaultModelLike {
|
|
44
|
+
/** Current host model-group selection, when the service exposes one. */
|
|
45
|
+
currentSelection?: () => { provider?: unknown, model?: unknown } | undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function asWebServer(value: unknown): WebServerLike | undefined {
|
|
49
|
+
const register = (value as { register?: unknown } | undefined)?.register
|
|
50
|
+
if (typeof register !== 'function') return undefined
|
|
51
|
+
// Hand back the service itself -- never a wrapper re-exporting `register`.
|
|
52
|
+
// The host reads its route tables off `this` (`this.exact` / `this.prefixes`),
|
|
53
|
+
// so a detached call makes `this` the wrapper and throws "Cannot read
|
|
54
|
+
// properties of undefined (reading 'has')" inside the host, after which the
|
|
55
|
+
// route is simply absent and the client sees a bare 404.
|
|
56
|
+
return value as WebServerLike
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Serve `GET /api/dsh-context-compression-improved/estimator-catalog` — the
|
|
61
|
+
* settings card's host-route dropdowns (live provider/model groups from the DSH
|
|
62
|
+
* `llm` service plus the effective selection). This lives on the top-level
|
|
63
|
+
* plugin context, NOT inside the isolated toolResultPruner service.
|
|
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.)
|
|
71
|
+
*
|
|
72
|
+
* The route gates on `webServer` **alone**. `llm` and `agentDefaultModel` only
|
|
73
|
+
* enrich the response and are resolved per request, so listing them here would
|
|
74
|
+
* let an estimator-side service the handler never needs keep the route
|
|
75
|
+
* unregistered. That failure is silent by construction — an unsatisfied
|
|
76
|
+
* `ctx.inject` callback never runs, so the plugin simply has no HTTP API and
|
|
77
|
+
* every request falls through to the host 404.
|
|
78
|
+
*
|
|
79
|
+
* Two channels cover the two arrival orders: a direct lookup catches a
|
|
80
|
+
* `webServer` that is already active when the plugin loads, and `ctx.inject`
|
|
81
|
+
* catches one that activates later. Both funnel into a single guarded
|
|
82
|
+
* registration, because a late-arriving service must not re-register a
|
|
83
|
+
* `(kind, path)` the host treats as a composition-contract violation.
|
|
84
|
+
*/
|
|
85
|
+
function registerEstimatorCatalogRoute(ctx: Context): void {
|
|
86
|
+
const readService = (name: string): unknown => {
|
|
87
|
+
try {
|
|
88
|
+
return (ctx as unknown as { get: (service: string) => unknown }).get(name)
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// `console`, not `ctx.logger`: measured on the 0.1.2 host, the cordis logger
|
|
94
|
+
// surfaces no plugin output in the `dsh web` terminal at all — a full boot
|
|
95
|
+
// produced zero plugin log lines while the process itself stayed chatty — so
|
|
96
|
+
// a lifecycle diagnostic published there is unobservable. `dsh-perm-gate`
|
|
97
|
+
// uses `console.warn` for the same message class on the same host.
|
|
98
|
+
const log = (level: 'info' | 'warn', message: string, ...args: unknown[]): void => {
|
|
99
|
+
console[level](message, ...args)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let registered = false
|
|
103
|
+
const register = (webServer: WebServerLike, channel: 'direct' | 'inject'): void => {
|
|
104
|
+
if (registered) return
|
|
105
|
+
const handler = (_req: unknown, res: unknown): void => {
|
|
106
|
+
const resTyped = res as {
|
|
107
|
+
writeHead: (code: number, headers?: Record<string, string>) => void
|
|
108
|
+
end: (body?: string) => void
|
|
109
|
+
}
|
|
110
|
+
if (typeof resTyped?.writeHead !== 'function' || typeof resTyped?.end !== 'function') return
|
|
111
|
+
const llm = readService('llm')
|
|
112
|
+
const defaults = readService('agentDefaultModel') as AgentDefaultModelLike | undefined
|
|
113
|
+
const deps: EstimatorCatalogDeps = {
|
|
114
|
+
...(llm === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: { llm: llm as NonNullable<EstimatorCatalogDeps['llm']> }),
|
|
117
|
+
...(typeof defaults?.currentSelection === 'function'
|
|
118
|
+
? { currentSelection: () => defaults.currentSelection?.() }
|
|
119
|
+
: {}),
|
|
120
|
+
}
|
|
121
|
+
buildEstimatorCatalog(deps).then(
|
|
122
|
+
catalog => {
|
|
123
|
+
resTyped.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
|
|
124
|
+
resTyped.end(JSON.stringify({ ok: true, ...catalog }))
|
|
125
|
+
},
|
|
126
|
+
(error: unknown) => {
|
|
127
|
+
resTyped.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })
|
|
128
|
+
resTyped.end(JSON.stringify({ ok: false, error: String((error as Error)?.message ?? error) }))
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const disposers = ESTIMATOR_CATALOG_ROUTES
|
|
134
|
+
.map(path => webServer.register({ kind: 'exact', path, handler }))
|
|
135
|
+
.filter((off): off is () => void => typeof off === 'function')
|
|
136
|
+
registered = true
|
|
137
|
+
ctx.effect(
|
|
138
|
+
() => () => { for (const off of disposers) off() },
|
|
139
|
+
'contextCompressionSelector.estimator-catalog route',
|
|
140
|
+
)
|
|
141
|
+
log('info', 'context-compression estimator catalog route registered (%s): %s', channel, ESTIMATOR_CATALOG_ROUTES.join(', '))
|
|
142
|
+
} catch (error) {
|
|
143
|
+
log('warn', 'context-compression estimator catalog route registration failed (%s): %o', channel, error)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const active = asWebServer(readService('webServer'))
|
|
148
|
+
if (active !== undefined) {
|
|
149
|
+
register(active, 'direct')
|
|
150
|
+
if (registered) return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
ctx.inject([...ESTIMATOR_CATALOG_ROUTE_DEPS], (injected) => {
|
|
154
|
+
const webServer = asWebServer((injected as { webServer?: unknown }).webServer)
|
|
155
|
+
if (webServer === undefined) {
|
|
156
|
+
log('warn', 'context-compression webServer exposes no register() — estimator catalog route not registered')
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
register(webServer, 'inject')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
log('warn', 'context-compression webServer not active yet — estimator catalog route pending: %s', ESTIMATOR_CATALOG_ROUTES.join(', '))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Harness 0.1.1 exposed a namespace-branding helper; 0.1.2 validates the
|
|
166
|
+
// same public literal at SettingsProvider.register/get instead.
|
|
167
|
+
const CONTEXT_COMPRESSION_NAMESPACE = CONTEXT_COMPRESSION_SETTINGS_NAMESPACE as never
|
|
168
|
+
|
|
169
|
+
/** Shared state forwarded through every Cordis proxy of one settings service. */
|
|
170
|
+
interface SharedSettingsRegistration {
|
|
171
|
+
/** Plugin fibers currently leasing the namespace. */
|
|
172
|
+
readonly owners: Set<SettingsOwner>
|
|
173
|
+
/** Owner whose fiber currently carries settings.register's native effect. */
|
|
174
|
+
registrationOwner: SettingsOwner
|
|
175
|
+
/** Current owner scope; replaced without changing the stored document. */
|
|
176
|
+
scope: SettingsScope<unknown>
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** One selector Host row able to own the registration effect. */
|
|
180
|
+
interface SettingsOwner {
|
|
181
|
+
/** Traceable service proxy binding register() to this row's fiber. */
|
|
182
|
+
readonly settings: SettingsService
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Symbol properties reach the shared service target through Cordis proxies. */
|
|
186
|
+
const SHARED_SETTINGS = Symbol.for(
|
|
187
|
+
'dsh-context-compression-improved/settings-registration',
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
type SettingsCarrier = SettingsService & {
|
|
191
|
+
[SHARED_SETTINGS]?: SharedSettingsRegistration
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Standalone Bundle behavior; the settings/UI owner remains safe when false. */
|
|
195
|
+
export interface Config {
|
|
196
|
+
/** Add the canonical compression stack to every non-Minimal preset. */
|
|
197
|
+
presetOverlay?: boolean
|
|
198
|
+
/**
|
|
199
|
+
* Own the estimator catalog HTTP route. Set on the Loader row that declares
|
|
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'], …)`.
|
|
213
|
+
*
|
|
214
|
+
* So the row-level `inject` is **not load-bearing**; it is kept as
|
|
215
|
+
* belt-and-braces so the route row stays inactive until `webServer` exists,
|
|
216
|
+
* and the flag keeps the route off standalone Bundle rows on profiles that
|
|
217
|
+
* have no web server. The internal two-channel registration is what actually
|
|
218
|
+
* covers both arrival orders. Do not cite this comment as a rule to the
|
|
219
|
+
* 0.1.5 replay — cite the host source.
|
|
220
|
+
*/
|
|
221
|
+
estimatorCatalogRoute?: boolean
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Loader validation for the standalone Bundle opt-in. */
|
|
225
|
+
export const Config: z<Config> = z.object({
|
|
226
|
+
presetOverlay: z.boolean().default(false),
|
|
227
|
+
estimatorCatalogRoute: z.boolean().default(false),
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
/** Register the persisted default read by the currently mounted root pruner. */
|
|
231
|
+
export function apply(ctx: Context, config: Config = {}): void {
|
|
232
|
+
// Measured on the 0.1.2 host: a plugin-load failure surfaces only through the
|
|
233
|
+
// cordis logger, which prints nothing in the `dsh web` terminal — so a throw
|
|
234
|
+
// here is completely invisible and looks exactly like a plugin that loaded
|
|
235
|
+
// and quietly did nothing. Report it to a sink the host shows, then re-throw
|
|
236
|
+
// unchanged: behaviour is untouched, only observability is restored.
|
|
237
|
+
try {
|
|
238
|
+
ctx.inject(['settings'], (settingsCtx) => {
|
|
239
|
+
acquireSettingsRegistration(settingsCtx)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
if (config.estimatorCatalogRoute === true) registerEstimatorCatalogRoute(ctx)
|
|
243
|
+
|
|
244
|
+
if (config.presetOverlay !== true) return
|
|
245
|
+
|
|
246
|
+
ctx.inject(['agentPresets'], (presetsCtx) => {
|
|
247
|
+
const installation = decorateAgentPresets(
|
|
248
|
+
presetsCtx.agentPresets,
|
|
249
|
+
{
|
|
250
|
+
modules: resolveCompressionModulePaths(),
|
|
251
|
+
excludedPresetIds: ['minimal'],
|
|
252
|
+
autoCompactThresholdPercent: () => resolveAutoCompactThresholdPercent(presetsCtx),
|
|
253
|
+
},
|
|
254
|
+
)
|
|
255
|
+
presetsCtx.effect(() => () => installation.dispose(), 'contextCompressionSelector.agentPresets()')
|
|
256
|
+
})
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error('context-compression apply() failed:', error)
|
|
259
|
+
throw error
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Read the current Auto Compact threshold ratio at composition time. Settings
|
|
265
|
+
* values are revalidated here, and any unreadable value falls back to the 80%
|
|
266
|
+
* default rather than blocking preset composition.
|
|
267
|
+
*/
|
|
268
|
+
function resolveAutoCompactThresholdPercent(presetsCtx: Context): number {
|
|
269
|
+
const raw = presetsCtx.get('settings')?.get(CONTEXT_COMPRESSION_NAMESPACE)
|
|
270
|
+
try {
|
|
271
|
+
const parsed = ContextCompressionSettingsSchema(structuredClone(raw) as never)
|
|
272
|
+
return parsed.autoCompact.thresholdPercent
|
|
273
|
+
} catch {
|
|
274
|
+
return 80
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Lease one native settings registration across duplicate Host rows.
|
|
280
|
+
*
|
|
281
|
+
* The lease effect is intentionally registered before settings.register().
|
|
282
|
+
* Cordis disposes effects in reverse order, so the native registration first
|
|
283
|
+
* releases the namespace; this disposer can then transfer it to another live
|
|
284
|
+
* owner without a duplicate-registration window.
|
|
285
|
+
*/
|
|
286
|
+
function acquireSettingsRegistration(ctx: Context): void {
|
|
287
|
+
const settings = ctx.settings as SettingsCarrier
|
|
288
|
+
const owner: SettingsOwner = { settings }
|
|
289
|
+
let shared = settings[SHARED_SETTINGS]
|
|
290
|
+
if (shared === undefined) {
|
|
291
|
+
shared = {
|
|
292
|
+
owners: new Set(),
|
|
293
|
+
registrationOwner: owner,
|
|
294
|
+
scope: undefined as unknown as SettingsScope<unknown>,
|
|
295
|
+
}
|
|
296
|
+
Object.defineProperty(settings, SHARED_SETTINGS, {
|
|
297
|
+
configurable: true,
|
|
298
|
+
enumerable: false,
|
|
299
|
+
writable: false,
|
|
300
|
+
value: shared,
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
shared.owners.add(owner)
|
|
304
|
+
const state = shared
|
|
305
|
+
|
|
306
|
+
ctx.effect(() => () => {
|
|
307
|
+
state.owners.delete(owner)
|
|
308
|
+
if (state.registrationOwner === owner && state.owners.size > 0) {
|
|
309
|
+
const next = state.owners.values().next().value as SettingsOwner
|
|
310
|
+
state.registrationOwner = next
|
|
311
|
+
state.scope = next.settings.register(
|
|
312
|
+
CONTEXT_COMPRESSION_NAMESPACE,
|
|
313
|
+
ContextCompressionSettingsSchema,
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
if (state.owners.size === 0 && settings[SHARED_SETTINGS] === state) {
|
|
317
|
+
Reflect.deleteProperty(settings, SHARED_SETTINGS)
|
|
318
|
+
}
|
|
319
|
+
}, 'contextCompressionSelector.settingsLease()')
|
|
320
|
+
|
|
321
|
+
if (state.owners.size === 1) {
|
|
322
|
+
state.scope = settings.register(
|
|
323
|
+
CONTEXT_COMPRESSION_NAMESPACE,
|
|
324
|
+
ContextCompressionSettingsSchema,
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** Package-owned invariants for standard-prune compression publications. */
|
|
2
|
+
|
|
3
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
4
|
+
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
|
5
|
+
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
|
6
|
+
import type {} from '@deepseek-ai/dsh-compaction'
|
|
7
|
+
import { validatePublishedTailTrim } from './runtime/tail-trim.ts'
|
|
8
|
+
import { sessionEvents } from './runtime/session-events.ts'
|
|
9
|
+
|
|
10
|
+
const PACKAGE_NAME = 'dsh-context-compression-improved-runtime'
|
|
11
|
+
|
|
12
|
+
/** Cordis companion plugin name. */
|
|
13
|
+
export const name = 'context-compression-selector-runtime-invariant'
|
|
14
|
+
/** Services required before the companion can register. */
|
|
15
|
+
export const inject = ['invariants']
|
|
16
|
+
|
|
17
|
+
type PruneEvent = SessionEvent<'compaction/prune'>
|
|
18
|
+
|
|
19
|
+
/** Whether the next event claims the unresolved prune's replacement range. */
|
|
20
|
+
function resemblesCompanion(prune: PruneEvent, event: SessionEvent): boolean {
|
|
21
|
+
if ((event.type !== 'tool/result' && event.type !== 'user/message')
|
|
22
|
+
|| typeof event.surfaceOp !== 'object') return false
|
|
23
|
+
const sources = new Set(event.sourceEventSeqs ?? [])
|
|
24
|
+
return (event.surfaceOp.start === prune.data.shadowedRange.start
|
|
25
|
+
&& event.surfaceOp.end === prune.data.shadowedRange.end)
|
|
26
|
+
|| prune.data.shadowedSeqs.some(seq => sources.has(seq))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Validate one standard prune's immediately adjacent surface replacement. */
|
|
30
|
+
function validateCompanion(
|
|
31
|
+
session: Session,
|
|
32
|
+
prune: PruneEvent,
|
|
33
|
+
event: SessionEvent,
|
|
34
|
+
fail: InvariantFailure,
|
|
35
|
+
): void {
|
|
36
|
+
if ((event.type !== 'tool/result' && event.type !== 'user/message')
|
|
37
|
+
|| typeof event.surfaceOp !== 'object') {
|
|
38
|
+
fail(`compaction/prune at seq ${prune.seq} must be immediately followed by a replacement surface event`)
|
|
39
|
+
}
|
|
40
|
+
const { shadowedRange, shadowedSeqs } = prune.data
|
|
41
|
+
if (shadowedSeqs.length === 0
|
|
42
|
+
|| shadowedSeqs[0] !== shadowedRange.start
|
|
43
|
+
|| shadowedSeqs.at(-1) !== shadowedRange.end) {
|
|
44
|
+
fail(`compaction/prune at seq ${prune.seq} has a shadowed range inconsistent with shadowedSeqs`)
|
|
45
|
+
}
|
|
46
|
+
if (event.surfaceOp.start !== shadowedRange.start || event.surfaceOp.end !== shadowedRange.end) {
|
|
47
|
+
fail(`replacement at seq ${event.seq} does not replace compaction/prune range ${shadowedRange.start}-${shadowedRange.end}`)
|
|
48
|
+
}
|
|
49
|
+
const sources = new Set(event.sourceEventSeqs ?? [])
|
|
50
|
+
const missing = shadowedSeqs.filter(seq => !sources.has(seq))
|
|
51
|
+
if (missing.length > 0) {
|
|
52
|
+
fail(`replacement at seq ${event.seq} omits shadowed source seqs ${missing.join(', ')}`)
|
|
53
|
+
}
|
|
54
|
+
if (event.type === 'user/message' && validatePublishedTailTrim(session, prune.seq) === null) {
|
|
55
|
+
fail(`TailTrim publication at seq ${prune.seq} is invalid`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Validate a replayed log and return its only legal unresolved tail. */
|
|
60
|
+
function seedPending(session: Session, fail: InvariantFailure): PruneEvent | undefined {
|
|
61
|
+
let pending: PruneEvent | undefined
|
|
62
|
+
for (const event of sessionEvents(session)) {
|
|
63
|
+
if (pending !== undefined) {
|
|
64
|
+
// A prune whose synchronous companion never committed is an inert,
|
|
65
|
+
// aborted publication. It must not brick a restored Session. A surface
|
|
66
|
+
// replacement that claims the prune is still validated strictly.
|
|
67
|
+
if (resemblesCompanion(pending, event)) validateCompanion(session, pending, event, fail)
|
|
68
|
+
pending = undefined
|
|
69
|
+
}
|
|
70
|
+
if (event.type === 'compaction/prune') pending = event
|
|
71
|
+
}
|
|
72
|
+
return pending
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Install adjacency checks with pre-commit staging. */
|
|
76
|
+
const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
|
|
77
|
+
const pending = new WeakMap<Session, PruneEvent | undefined>()
|
|
78
|
+
const staged = new WeakMap<SessionEvent, { session: Session; next: PruneEvent | undefined }>()
|
|
79
|
+
const seed = (session: Session): void => { pending.set(session, seedPending(session, fail)) }
|
|
80
|
+
const current = (session: Session): PruneEvent | undefined => {
|
|
81
|
+
if (!pending.has(session)) seed(session)
|
|
82
|
+
return pending.get(session)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const session of ctx.sessions.list()) seed(session)
|
|
86
|
+
ctx.on('session/created', seed, { global: true })
|
|
87
|
+
|
|
88
|
+
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
|
89
|
+
if (eventName !== 'session/event') return
|
|
90
|
+
const [session, event] = args as [Session, SessionEvent]
|
|
91
|
+
const open = current(session)
|
|
92
|
+
if (open !== undefined && resemblesCompanion(open, event)) {
|
|
93
|
+
validateCompanion(session, open, event, fail)
|
|
94
|
+
}
|
|
95
|
+
staged.set(event, {
|
|
96
|
+
session,
|
|
97
|
+
next: event.type === 'compaction/prune' ? event : undefined,
|
|
98
|
+
})
|
|
99
|
+
}, { global: true })
|
|
100
|
+
|
|
101
|
+
ctx.on('session/event', (session, event) => {
|
|
102
|
+
const candidate = staged.get(event)
|
|
103
|
+
if (candidate === undefined || candidate.session !== session) {
|
|
104
|
+
return fail('session/event reached publication without matching pre-commit validation')
|
|
105
|
+
}
|
|
106
|
+
staged.delete(event)
|
|
107
|
+
pending.set(session, candidate.next)
|
|
108
|
+
}, { global: true })
|
|
109
|
+
}, { inject: ['sessions'] })
|
|
110
|
+
|
|
111
|
+
/** Register this package's invariant companion. */
|
|
112
|
+
export const apply = (ctx: Context): Promise<() => void> =>
|
|
113
|
+
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|