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,631 @@
1
+ /** Real-AgentPresets standing-generation behavior for the overlay threshold. */
2
+
3
+ import { createHash } from 'node:crypto'
4
+ import { mkdir, mkdtemp, readdir, readFile, rm, stat, utimes, writeFile } from 'node:fs/promises'
5
+ import { tmpdir } from 'node:os'
6
+ import { dirname, join } from 'node:path'
7
+ import { Context } from '@deepseek-ai/cordis'
8
+ import Group from '@deepseek-ai/cordis-plugin-group'
9
+ import Include from '@deepseek-ai/cordis-plugin-include'
10
+ import Loader from '@deepseek-ai/cordis-plugin-loader'
11
+ import AgentRegistry from '@deepseek-ai/dsh-agent'
12
+ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
13
+ import AgentPresets from '@deepseek-ai/dsh-agent-presets'
14
+ import CommandRuntime from '@deepseek-ai/dsh-commands'
15
+ import LlmRuntime from '@deepseek-ai/dsh-llm'
16
+ import SessionStore from '@deepseek-ai/dsh-session'
17
+ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
18
+ import TokenMeter from '@deepseek-ai/dsh-token-meter'
19
+ import ToolRuntime from '@deepseek-ai/dsh-tools'
20
+ import {
21
+ SettingsProvider,
22
+ settingsNamespace,
23
+ type SettingsNamespace,
24
+ } from '@deepseek-ai/dsh-settings'
25
+ import { pathToFileURL } from 'node:url'
26
+ import { afterEach, describe, expect, it } from 'vitest'
27
+ import { apply } from '../src/index.ts'
28
+ import {
29
+ decorateAgentPresets,
30
+ resolveCompressionModulePaths,
31
+ standingStampMs,
32
+ standingStampMsAtWindow,
33
+ } from '../src/preset-overlay.ts'
34
+ import type {
35
+ CompressionModulePaths,
36
+ OverlayableAgentPresets,
37
+ PresetOverlayMetadataIo,
38
+ } from '../src/preset-overlay.ts'
39
+
40
+ let root: string | undefined
41
+ let ctx: Context | undefined
42
+
43
+ afterEach(async () => {
44
+ await ctx?.fiber.dispose()
45
+ ctx = undefined
46
+ if (root !== undefined) await rm(root, { recursive: true, force: true })
47
+ root = undefined
48
+ })
49
+
50
+ class MemorySettings extends SettingsProvider {
51
+ readonly writable = true
52
+ private readonly stored: Record<string, unknown> = {}
53
+
54
+ protected load(): Promise<Record<string, unknown>> {
55
+ return Promise.resolve(structuredClone(this.stored))
56
+ }
57
+
58
+ protected persist(ns: SettingsNamespace, section: Record<string, unknown>): Promise<void> {
59
+ this.stored[ns] = structuredClone(section)
60
+ return Promise.resolve()
61
+ }
62
+ }
63
+
64
+ async function presetsRoot(): Promise<string> {
65
+ root = await mkdtemp(join(tmpdir(), 'dsh-selector-standing-'))
66
+ const marker = join(root, 'marker-aaaaaaaaaa.mjs')
67
+ await writeFile(marker, 'export function apply() {}\n')
68
+ const path = join(root, 'standard', 'agent.cordis.yml')
69
+ await mkdir(dirname(path), { recursive: true })
70
+ await writeFile(path, `- id: source-marker\n name: ${JSON.stringify(marker)}\n`)
71
+ return root
72
+ }
73
+
74
+ /**
75
+ * Read the overlay compositions generated from THIS test's source preset.
76
+ * Concurrent spec files write their own stores; the embedded marker path
77
+ * distinguishes ours without depending on directory discovery timing.
78
+ */
79
+ async function overlayFiles(sourceMarker: string): Promise<{ path: string, rendered: string }[]> {
80
+ const parent = await readdir(tmpdir(), { withFileTypes: true })
81
+ const files: { path: string, rendered: string }[] = []
82
+ for (const entry of parent) {
83
+ if (!entry.isDirectory() || !entry.name.startsWith('dsh-context-compression-presets-')) continue
84
+ const directory = join(tmpdir(), entry.name)
85
+ let children
86
+ try {
87
+ children = await readdir(directory)
88
+ } catch {
89
+ continue // a concurrent spec disposed its store mid-scan
90
+ }
91
+ for (const child of children) {
92
+ if (!child.startsWith('standard-') || !child.endsWith('.agent.cordis.yml')) continue
93
+ const path = join(directory, child)
94
+ const rendered = await readFile(path, 'utf8')
95
+ if (rendered.includes(sourceMarker)) files.push({ path, rendered })
96
+ }
97
+ }
98
+ return files
99
+ }
100
+
101
+ let sourceRoot: string | undefined
102
+
103
+ /** Rewrite the source preset to an equal-length marker module. */
104
+ async function rewriteSourceMarker(name: string): Promise<void> {
105
+ if (sourceRoot === undefined) throw new Error('source root not prepared')
106
+ const marker = join(sourceRoot, name)
107
+ await writeFile(marker, 'export function apply() {}\n')
108
+ await writeFile(join(sourceRoot, 'standard', 'agent.cordis.yml'), `- id: source-marker\n name: ${JSON.stringify(marker)}\n`)
109
+ }
110
+
111
+ /** Create an importable marker module with an exact-length directory name. */
112
+ async function sameLengthModule(label: string): Promise<string> {
113
+ if (sourceRoot === undefined) throw new Error('source root not prepared')
114
+ const directory = join(sourceRoot, label)
115
+ await mkdir(directory, { recursive: true })
116
+ const file = join(directory, 'index.mjs')
117
+ await writeFile(file, 'export function apply() {}\n')
118
+ return file
119
+ }
120
+
121
+ /** Reproduce the production content identity without replacing it in the store. */
122
+ function overlayIdentity(
123
+ source: string,
124
+ modules: CompressionModulePaths,
125
+ thresholdPercent: number,
126
+ ): string {
127
+ return createHash('sha256')
128
+ .update('standard').update('\0')
129
+ .update(source).update('\0')
130
+ .update(JSON.stringify({ modules, autoCompactThresholdPercent: thresholdPercent }))
131
+ .digest('hex')
132
+ .slice(0, 24)
133
+ }
134
+
135
+ /** Find equal-length valid presets whose production identities share window 0. */
136
+ function coarseCollisionFixture(
137
+ marker: string,
138
+ modules: CompressionModulePaths,
139
+ ): Readonly<{ first: string, second: string, firstIdentity: string, secondIdentity: string }> {
140
+ const seen = new Map<string, Readonly<{ source: string, identity: string }>>()
141
+ for (let index = 0; index < 300_000; index += 1) {
142
+ const label = index.toString(16).padStart(8, '0')
143
+ const source = [
144
+ '- id: source-marker',
145
+ ` name: ${JSON.stringify(marker)}`,
146
+ `- id: collision-${label}`,
147
+ ' name: cordis:group',
148
+ ' group: true',
149
+ ' config: []',
150
+ '',
151
+ ].join('\n')
152
+ const identity = overlayIdentity(source, modules, 80)
153
+ const prefix = identity.slice(0, 8)
154
+ const prior = seen.get(prefix)
155
+ if (prior !== undefined && prior.identity !== identity) {
156
+ return {
157
+ first: prior.source,
158
+ second: source,
159
+ firstIdentity: prior.identity,
160
+ secondIdentity: identity,
161
+ }
162
+ }
163
+ seen.set(prefix, { source, identity })
164
+ }
165
+ throw new Error('failed to find a deterministic 32-bit standing-stamp collision')
166
+ }
167
+
168
+ /**
169
+ * Mount the full real stack, run `arrange`, capture the standing key and
170
+ * generated file, run `act`, and assert the generation moved.
171
+ */
172
+ async function assertStandingSwitch(
173
+ arrange: () => Promise<() => Promise<void>>,
174
+ ): Promise<void> {
175
+ const rootPath = await presetsRoot()
176
+ sourceRoot = rootPath
177
+ ctx = new Context()
178
+ ctx.baseUrl = `${pathToFileURL(rootPath).href}/`
179
+ await ctx.plugin(Loader)
180
+ ctx.loader.builtins.include = Include
181
+ ctx.loader.builtins.group = Group
182
+ await ctx.plugin(LlmRuntime)
183
+ await ctx.plugin(SessionStore)
184
+ await ctx.plugin(SystemPrompt, { persona: '' })
185
+ await ctx.plugin(ToolRuntime)
186
+ await ctx.plugin(AgentRegistry)
187
+ await ctx.plugin(AgentLoop, { agents: [] })
188
+ await ctx.plugin(CommandRuntime)
189
+ await ctx.plugin(TokenMeter)
190
+ await ctx.plugin(MemorySettings).await()
191
+ await ctx.plugin({ apply }).await()
192
+ await ctx.plugin(AgentPresets, {
193
+ default: 'standard',
194
+ roots: [{ path: rootPath, trust: 'system' }],
195
+ includeUserRoot: false,
196
+ }).await()
197
+ const act = await arrange()
198
+ const bundle = ctx.plugin({ apply: (child) => {
199
+ apply(child, { presetOverlay: true })
200
+ } })
201
+ await bundle.await()
202
+ const presets = ctx.agentPresets as unknown as OverlayableAgentPresets
203
+ const first = await presets.standingKeyFor()
204
+ const firstFiles = await overlayFiles(rootPath)
205
+ expect(firstFiles).toHaveLength(1)
206
+
207
+ await act()
208
+ const second = await presets.standingKeyFor()
209
+ const secondFiles = await overlayFiles(rootPath)
210
+ expect(secondFiles).toHaveLength(2)
211
+ expect(second).not.toBe(first)
212
+ const stamps = await Promise.all(secondFiles.map(async file => (await stat(file.path)).mtimeMs))
213
+ expect(new Set(stamps).size).toBe(2)
214
+ }
215
+
216
+ describe('deterministic standing stamps', () => {
217
+ // The reviewer's reproduced 1-second-granularity collision: under the old
218
+ // millisecond-prefix mapping these two identities stamped 280ms apart, and
219
+ // floor(stamp/1000) collapsed both onto second 586129064.
220
+ const COLLISION_A = '887803c2b6c57357206d271e'
221
+ const COLLISION_B = '887803c3ceb119ac3cfb7d32'
222
+
223
+ it('never shares a one-second bucket between distinct identities', () => {
224
+ const stampA = standingStampMs(COLLISION_A)
225
+ const stampB = standingStampMs(COLLISION_B)
226
+ expect(Math.floor(stampA / 1000)).not.toBe(Math.floor(stampB / 1000))
227
+ })
228
+
229
+ it('stays inside the nanosecond range every filesystem can store', () => {
230
+ const maxIdentity = 'ffffffffffffffffffffffff'
231
+ const stamp = standingStampMs(maxIdentity)
232
+ expect(Number.isFinite(stamp)).toBe(true)
233
+ expect(stamp).toBeLessThan(Date.UTC(2260, 0, 1))
234
+ expect(stamp).toBeGreaterThan(Date.UTC(2026, 0, 1))
235
+ })
236
+
237
+ it('is deterministic per identity and separable across hash windows', () => {
238
+ expect(standingStampMs(COLLISION_A)).toBe(standingStampMs(COLLISION_A))
239
+ // Equal 8-hex prefixes share the second bucket at window 0 but the
240
+ // escalated window reads later hash digits into distinct seconds.
241
+ const sharedPrefix = '887803c2'
242
+ const first = `${sharedPrefix}aaaaaaaaaaaaaaaa`
243
+ const second = `${sharedPrefix}bbbbbbbbbbbbbbbb`
244
+ expect(Math.floor(standingStampMsAtWindow(first, 0) / 1000))
245
+ .toBe(Math.floor(standingStampMsAtWindow(second, 0) / 1000))
246
+ expect(standingStampMsAtWindow(first, 0)).not.toBe(standingStampMsAtWindow(second, 0))
247
+ expect(Math.floor(standingStampMsAtWindow(first, 1) / 1000))
248
+ .not.toBe(Math.floor(standingStampMsAtWindow(second, 1) / 1000))
249
+ })
250
+ })
251
+
252
+ describe('real AgentPresets standing generations with the overlay threshold', () => {
253
+ it('separates colliding equal-size generations on a whole-second metadata surface before publish', async () => {
254
+ const rootPath = await presetsRoot()
255
+ sourceRoot = rootPath
256
+ const sourcePath = join(rootPath, 'standard', 'agent.cordis.yml')
257
+ const marker = join(rootPath, 'marker-aaaaaaaaaa.mjs')
258
+ const modules = resolveCompressionModulePaths()
259
+ const collision = coarseCollisionFixture(marker, modules)
260
+ expect(collision.first).toHaveLength(collision.second.length)
261
+ expect(collision.firstIdentity.slice(0, 8)).toBe(collision.secondIdentity.slice(0, 8))
262
+ await writeFile(sourcePath, collision.first)
263
+
264
+ ctx = new Context()
265
+ ctx.baseUrl = `${pathToFileURL(rootPath).href}/`
266
+ await ctx.plugin(Loader)
267
+ ctx.loader.builtins.include = Include
268
+ ctx.loader.builtins.group = Group
269
+ await ctx.plugin(LlmRuntime)
270
+ await ctx.plugin(SessionStore)
271
+ await ctx.plugin(SystemPrompt, { persona: '' })
272
+ await ctx.plugin(ToolRuntime)
273
+ await ctx.plugin(AgentRegistry)
274
+ await ctx.plugin(AgentLoop, { agents: [] })
275
+ await ctx.plugin(CommandRuntime)
276
+ await ctx.plugin(TokenMeter)
277
+ await ctx.plugin(AgentPresets, {
278
+ default: 'standard',
279
+ roots: [{ path: rootPath, trust: 'system' }],
280
+ includeUserRoot: false,
281
+ }).await()
282
+
283
+ const coarseMetadataIo: PresetOverlayMetadataIo = {
284
+ async setTimes(path, stamp) {
285
+ const wholeSecond = new Date(Math.floor(stamp.getTime() / 1000) * 1000)
286
+ await utimes(path, wholeSecond, wholeSecond)
287
+ },
288
+ async read(path) {
289
+ const observed = await stat(path)
290
+ return { mtimeMs: observed.mtimeMs, size: observed.size }
291
+ },
292
+ }
293
+ const presets = ctx.agentPresets as unknown as OverlayableAgentPresets
294
+ const installation = decorateAgentPresets(presets, {
295
+ modules,
296
+ autoCompactThresholdPercent: () => 80,
297
+ metadataIo: coarseMetadataIo,
298
+ })
299
+
300
+ const firstKey = await presets.standingKeyFor()
301
+ await writeFile(sourcePath, collision.second)
302
+ const changedSourceStamp = new Date(Date.now() + 2_000)
303
+ await utimes(sourcePath, changedSourceStamp, changedSourceStamp)
304
+ const secondKey = await presets.standingKeyFor()
305
+ // Scope keys are structurally `{ agentPreset: id }`; a new object identity
306
+ // proves real AgentPresets observed a different composition stamp and
307
+ // created a new standing generation.
308
+ expect(secondKey).not.toBe(firstKey)
309
+
310
+ const files = await overlayFiles(rootPath)
311
+ expect(files).toHaveLength(2)
312
+ const byIdentity = new Map(files.map(file => [
313
+ /standard-([0-9a-f]+)\.agent\.cordis\.yml$/u.exec(file.path)?.[1],
314
+ file.path,
315
+ ]))
316
+ const firstFile = byIdentity.get(collision.firstIdentity)
317
+ const secondFile = byIdentity.get(collision.secondIdentity)
318
+ expect(firstFile).toBeDefined()
319
+ expect(secondFile).toBeDefined()
320
+ const firstStat = await stat(firstFile as string)
321
+ const secondStat = await stat(secondFile as string)
322
+ expect(firstStat.size).toBe(secondStat.size)
323
+ expect(firstStat.mtimeMs % 1000).toBe(0)
324
+ expect(secondStat.mtimeMs % 1000).toBe(0)
325
+ expect(firstStat.mtimeMs).toBe(Math.floor(standingStampMsAtWindow(collision.firstIdentity, 0) / 1000) * 1000)
326
+ expect(secondStat.mtimeMs).toBe(Math.floor(standingStampMsAtWindow(collision.secondIdentity, 1) / 1000) * 1000)
327
+
328
+ await installation.dispose()
329
+ })
330
+
331
+ it('publishes distinct second buckets for equal-length colliding sources on the real filesystem', async () => {
332
+ const rootPath = await presetsRoot()
333
+ sourceRoot = rootPath
334
+ ctx = new Context()
335
+ ctx.baseUrl = `${pathToFileURL(rootPath).href}/`
336
+ await ctx.plugin(Loader)
337
+ ctx.loader.builtins.include = Include
338
+ ctx.loader.builtins.group = Group
339
+ await ctx.plugin(LlmRuntime)
340
+ await ctx.plugin(SessionStore)
341
+ await ctx.plugin(SystemPrompt, { persona: '' })
342
+ await ctx.plugin(ToolRuntime)
343
+ await ctx.plugin(AgentRegistry)
344
+ await ctx.plugin(AgentLoop, { agents: [] })
345
+ await ctx.plugin(CommandRuntime)
346
+ await ctx.plugin(TokenMeter)
347
+ await ctx.plugin(MemorySettings).await()
348
+ await ctx.plugin({ apply }).await()
349
+ await ctx.plugin(AgentPresets, {
350
+ default: 'standard',
351
+ roots: [{ path: rootPath, trust: 'system' }],
352
+ includeUserRoot: false,
353
+ }).await()
354
+ const namespace = settingsNamespace('context-compression')
355
+ await ctx.settings.update(namespace, { autoCompact: { thresholdPercent: 80 } })
356
+ const bundle = ctx.plugin({ apply: (child) => {
357
+ apply(child, { presetOverlay: true })
358
+ } })
359
+ await bundle.await()
360
+
361
+ const presets = ctx.agentPresets as unknown as OverlayableAgentPresets
362
+ await presets.standingKeyFor()
363
+ // Two equal-length sources (55 bytes each, matching the collision
364
+ // fixture's shape): distinct identities whose stamps must never collapse
365
+ // onto the same whole second even if the filesystem truncates mtimes.
366
+ await rewriteSourceMarker('marker-00000aa9.mjs')
367
+ await presets.standingKeyFor()
368
+ await rewriteSourceMarker('marker-00000wil.mjs')
369
+ const third = await presets.standingKeyFor()
370
+ expect(third).toBeDefined()
371
+
372
+ const files = await overlayFiles(rootPath)
373
+ expect(files.length).toBeGreaterThanOrEqual(3)
374
+ const buckets = await Promise.all(files.map(async file => Math.floor((await stat(file.path)).mtimeMs / 1000)))
375
+ expect(new Set(buckets).size).toBe(buckets.length)
376
+ // No staging leftovers survive a publish.
377
+ const storeDirs = await Promise.all(
378
+ (await readdir(tmpdir(), { withFileTypes: true }))
379
+ .filter(entry => entry.isDirectory() && entry.name.startsWith('dsh-context-compression-presets-'))
380
+ .map(async entry => readdir(join(tmpdir(), entry.name))),
381
+ )
382
+ expect(storeDirs.flat().some(name => name.endsWith('.tmp'))).toBe(false)
383
+
384
+ await bundle.dispose()
385
+ })
386
+
387
+ it('keeps one fully-identical generation under concurrent composition of the same identity', async () => {
388
+ const rootPath = await presetsRoot()
389
+ ctx = new Context()
390
+ ctx.baseUrl = `${pathToFileURL(rootPath).href}/`
391
+ await ctx.plugin(Loader)
392
+ ctx.loader.builtins.include = Include
393
+ ctx.loader.builtins.group = Group
394
+ await ctx.plugin(LlmRuntime)
395
+ await ctx.plugin(SessionStore)
396
+ await ctx.plugin(SystemPrompt, { persona: '' })
397
+ await ctx.plugin(ToolRuntime)
398
+ await ctx.plugin(AgentRegistry)
399
+ await ctx.plugin(AgentLoop, { agents: [] })
400
+ await ctx.plugin(CommandRuntime)
401
+ await ctx.plugin(TokenMeter)
402
+ await ctx.plugin(MemorySettings).await()
403
+ await ctx.plugin({ apply }).await()
404
+ await ctx.plugin(AgentPresets, {
405
+ default: 'standard',
406
+ roots: [{ path: rootPath, trust: 'system' }],
407
+ includeUserRoot: false,
408
+ }).await()
409
+ const namespace = settingsNamespace('context-compression')
410
+ await ctx.settings.update(namespace, { autoCompact: { thresholdPercent: 73 } })
411
+ const bundle = ctx.plugin({ apply: (child) => {
412
+ apply(child, { presetOverlay: true })
413
+ } })
414
+ await bundle.await()
415
+
416
+ const presets = ctx.agentPresets as unknown as OverlayableAgentPresets
417
+ // Every concurrent composer must hand out the SAME path, and the file's
418
+ // persisted stamp must be the deterministic identity stamp — never a
419
+ // wall-clock mtime from a metadata window.
420
+ const concurrent = await Promise.all([
421
+ presets.standingKeyFor(),
422
+ presets.standingKeyFor(),
423
+ presets.standingKeyFor(),
424
+ presets.standingKeyFor(),
425
+ presets.standingKeyFor(),
426
+ presets.standingKeyFor(),
427
+ ])
428
+ expect(new Set(concurrent.map(key => JSON.stringify(key)))).toHaveLength(1)
429
+ const files = await overlayFiles(rootPath)
430
+ expect(files).toHaveLength(1)
431
+ const details = await stat(files[0]!.path)
432
+ const now = Date.now()
433
+ expect(Math.abs(details.mtimeMs - now)).toBeGreaterThan(60 * 60 * 1000)
434
+ // The persisted whole-second bucket matches the identity-derived stamp's
435
+ // bucket regardless of filesystem mtime granularity, and the persisted
436
+ // sub-second component is the identity-derived one (or its zero-truncated
437
+ // form on a coarse filesystem).
438
+ const rendered = files[0]!.rendered
439
+ const identity = rendered.length > 0
440
+ ? /standard-([0-9a-f]+)\.agent\.cordis\.yml$/u.exec(files[0]!.path)?.[1]
441
+ : undefined
442
+ expect(identity).toBeDefined()
443
+ expect(Math.floor(details.mtimeMs / 1000))
444
+ .toBe(Math.floor(standingStampMs(identity as string) / 1000))
445
+ const observedSubSecond = details.mtimeMs % 1000
446
+ const expectedSubSecond = standingStampMs(identity as string) % 1000
447
+ expect(observedSubSecond === 0 || Math.abs(observedSubSecond - expectedSubSecond) < 1).toBe(true)
448
+
449
+ await bundle.dispose()
450
+ })
451
+
452
+ it('switches the standing generation for an equal-length threshold change', async () => {
453
+ await assertStandingSwitch(async () => {
454
+ const namespace = settingsNamespace('context-compression')
455
+ await ctx!.settings.update(namespace, { autoCompact: { thresholdPercent: 70 } })
456
+ return async () => ctx!.settings.update(namespace, { autoCompact: { thresholdPercent: 80 } })
457
+ })
458
+ })
459
+
460
+ it('switches the standing generation for an equal-length source change at a fixed threshold', async () => {
461
+ await assertStandingSwitch(async () => {
462
+ const namespace = settingsNamespace('context-compression')
463
+ await ctx!.settings.update(namespace, { autoCompact: { thresholdPercent: 80 } })
464
+ return async () => {
465
+ await rewriteSourceMarker('marker-bbbbbbbbbb.mjs')
466
+ }
467
+ })
468
+ })
469
+
470
+ it('switches the standing generation for an equal-length module identity change', async () => {
471
+ const rootPath = await presetsRoot()
472
+ sourceRoot = rootPath
473
+ ctx = new Context()
474
+ ctx.baseUrl = `${pathToFileURL(rootPath).href}/`
475
+ await ctx.plugin(Loader)
476
+ ctx.loader.builtins.include = Include
477
+ ctx.loader.builtins.group = Group
478
+ await ctx.plugin(LlmRuntime)
479
+ await ctx.plugin(SessionStore)
480
+ await ctx.plugin(SystemPrompt, { persona: '' })
481
+ await ctx.plugin(ToolRuntime)
482
+ await ctx.plugin(AgentRegistry)
483
+ await ctx.plugin(AgentLoop, { agents: [] })
484
+ await ctx.plugin(CommandRuntime)
485
+ await ctx.plugin(TokenMeter)
486
+ await ctx.plugin(MemorySettings).await()
487
+ await ctx.plugin({ apply }).await()
488
+ await ctx.plugin(AgentPresets, {
489
+ default: 'standard',
490
+ roots: [{ path: rootPath, trust: 'system' }],
491
+ includeUserRoot: false,
492
+ }).await()
493
+ const namespace = settingsNamespace('context-compression')
494
+ await ctx.settings.update(namespace, { autoCompact: { thresholdPercent: 80 } })
495
+
496
+ const presets = ctx.agentPresets as unknown as OverlayableAgentPresets
497
+ // Two module sets whose entry paths differ in content but not length.
498
+ const moduleA = await sameLengthModule('module-aaaaaaaaaa')
499
+ const moduleB = await sameLengthModule('module-bbbbbbbbbb')
500
+ const paths = (m: string) => ({
501
+ compactionBasic: m,
502
+ commandCompact: m,
503
+ toolResultPruner: m,
504
+ })
505
+ let installation = decorateAgentPresets(presets, {
506
+ modules: paths(moduleA),
507
+ excludedPresetIds: ['minimal'],
508
+ autoCompactThresholdPercent: () => 80,
509
+ })
510
+ const first = await presets.standingKeyFor()
511
+ const firstFiles = await overlayFiles(rootPath)
512
+ expect(firstFiles).toHaveLength(1)
513
+ const firstPath = firstFiles[0]!.path
514
+ const firstStamp = (await stat(firstPath)).mtimeMs
515
+ expect(firstFiles[0]!.rendered).toContain(moduleA)
516
+
517
+ // Disposing the old decoration also removes its generated store, so the
518
+ // swap is proven by the new composition's path, content, and stamp.
519
+ await installation.dispose()
520
+ installation = decorateAgentPresets(presets, {
521
+ modules: paths(moduleB),
522
+ excludedPresetIds: ['minimal'],
523
+ autoCompactThresholdPercent: () => 80,
524
+ })
525
+ const second = await presets.standingKeyFor()
526
+ const secondFiles = await overlayFiles(rootPath)
527
+ expect(secondFiles).toHaveLength(1)
528
+ expect(second).not.toBe(first)
529
+ expect(secondFiles[0]!.path).not.toBe(firstPath)
530
+ expect(secondFiles[0]!.rendered).toContain(moduleB)
531
+ expect((await stat(secondFiles[0]!.path)).mtimeMs).not.toBe(firstStamp)
532
+ await installation.dispose()
533
+ })
534
+
535
+ it('keeps one generation under concurrent and repeated composition of the same identity', async () => {
536
+ const rootPath = await presetsRoot()
537
+ ctx = new Context()
538
+ ctx.baseUrl = `${pathToFileURL(rootPath).href}/`
539
+ await ctx.plugin(Loader)
540
+ ctx.loader.builtins.include = Include
541
+ ctx.loader.builtins.group = Group
542
+ await ctx.plugin(LlmRuntime)
543
+ await ctx.plugin(SessionStore)
544
+ await ctx.plugin(SystemPrompt, { persona: '' })
545
+ await ctx.plugin(ToolRuntime)
546
+ await ctx.plugin(AgentRegistry)
547
+ await ctx.plugin(AgentLoop, { agents: [] })
548
+ await ctx.plugin(CommandRuntime)
549
+ await ctx.plugin(TokenMeter)
550
+ await ctx.plugin(MemorySettings).await()
551
+ await ctx.plugin({ apply }).await()
552
+ await ctx.plugin(AgentPresets, {
553
+ default: 'standard',
554
+ roots: [{ path: rootPath, trust: 'system' }],
555
+ includeUserRoot: false,
556
+ }).await()
557
+ const namespace = settingsNamespace('context-compression')
558
+ await ctx.settings.update(namespace, { autoCompact: { thresholdPercent: 73 } })
559
+ const bundle = ctx.plugin({ apply: (child) => {
560
+ apply(child, { presetOverlay: true })
561
+ } })
562
+ await bundle.await()
563
+
564
+ const presets = ctx.agentPresets as unknown as OverlayableAgentPresets
565
+ const concurrent = await Promise.all([
566
+ presets.standingKeyFor(),
567
+ presets.standingKeyFor(),
568
+ presets.standingKeyFor(),
569
+ ])
570
+ expect(new Set(concurrent.map(key => JSON.stringify(key)))).toHaveLength(1)
571
+ const files = await overlayFiles(rootPath)
572
+ expect(files).toHaveLength(1)
573
+ const stampFirst = (await stat(files[0]!.path)).mtimeMs
574
+ const repeated = await presets.standingKeyFor()
575
+ expect(JSON.stringify(repeated)).toBe(JSON.stringify(concurrent[0]))
576
+ expect(await overlayFiles(rootPath)).toHaveLength(1)
577
+ expect((await stat(files[0]!.path)).mtimeMs).toBe(stampFirst)
578
+ })
579
+
580
+ it('switches the standing generation for an equal-length threshold change (legacy body)', async () => {
581
+ const rootPath = await presetsRoot()
582
+ ctx = new Context()
583
+ ctx.baseUrl = `${pathToFileURL(rootPath).href}/`
584
+ await ctx.plugin(Loader)
585
+ ctx.loader.builtins.include = Include
586
+ ctx.loader.builtins.group = Group
587
+ await ctx.plugin(LlmRuntime)
588
+ await ctx.plugin(SessionStore)
589
+ await ctx.plugin(SystemPrompt, { persona: '' })
590
+ await ctx.plugin(ToolRuntime)
591
+ await ctx.plugin(AgentRegistry)
592
+ await ctx.plugin(AgentLoop, { agents: [] })
593
+ await ctx.plugin(CommandRuntime)
594
+ await ctx.plugin(TokenMeter)
595
+ await ctx.plugin(MemorySettings).await()
596
+ await ctx.plugin({ apply }).await()
597
+ await ctx.plugin(AgentPresets, {
598
+ default: 'standard',
599
+ roots: [{ path: rootPath, trust: 'system' }],
600
+ includeUserRoot: false,
601
+ }).await()
602
+ const namespace = settingsNamespace('context-compression')
603
+ await ctx.settings.update(namespace, { autoCompact: { thresholdPercent: 70 } })
604
+ const bundle = ctx.plugin({ apply: (child) => {
605
+ apply(child, { presetOverlay: true })
606
+ } })
607
+ await bundle.await()
608
+
609
+ const presets = ctx.agentPresets as unknown as OverlayableAgentPresets
610
+ const first = await presets.standingKeyFor()
611
+ const firstFiles = await overlayFiles(rootPath)
612
+ expect(firstFiles).toHaveLength(1)
613
+ expect(firstFiles[0]?.rendered).toContain('thresholdRatio: 0.7')
614
+ expect(firstFiles[0]?.rendered).toContain('autoCompactThresholdPercent: 70')
615
+
616
+ // Equal-length change: identical rendered sizes, and without the
617
+ // deterministic threshold mtime a same-millisecond switch could have
618
+ // reused the superseded generation.
619
+ await ctx.settings.update(namespace, { autoCompact: { thresholdPercent: 80 } })
620
+ const second = await presets.standingKeyFor()
621
+ const secondFiles = await overlayFiles(rootPath)
622
+ expect(secondFiles).toHaveLength(2)
623
+ expect(secondFiles.some(file => file.rendered.includes('thresholdRatio: 0.8')
624
+ && file.rendered.includes('autoCompactThresholdPercent: 80'))).toBe(true)
625
+ expect(second).not.toBe(first)
626
+ const stamps = await Promise.all(secondFiles.map(async file => (await stat(file.path)).mtimeMs))
627
+ expect(new Set(stamps).size).toBe(2)
628
+
629
+ await bundle.dispose()
630
+ })
631
+ })