dsh-context-compression-improved 0.1.1 → 0.2.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 -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 +85 -82
- 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,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
|
+
})
|