dsh-context-compression-improved 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +1 -1
- package/.github/workflows/ci.yml +1 -7
- package/CHANGELOG.ja.md +18 -21
- package/CHANGELOG.ko.md +17 -21
- package/CHANGELOG.md +16 -17
- package/CHANGELOG.zh.md +15 -18
- package/CONTRIBUTING.md +1 -1
- package/README.ja.md +1 -20
- package/README.ko.md +12 -30
- package/README.md +1 -19
- package/README.zh.md +5 -21
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/docs/compat-0.1.5-report.md +77 -0
- package/docs/installation.ja.md +6 -4
- package/docs/installation.ko.md +9 -7
- package/docs/installation.md +3 -1
- package/docs/installation.zh.md +3 -2
- package/docs/repair-log.md +54 -33
- package/eslint.config.js +2 -2
- package/package.json +56 -36
- package/packages/selector/README.md +7 -12
- package/packages/selector/README.zh.md +6 -13
- package/packages/selector/THIRD_PARTY_NOTICES.md +2 -8
- package/packages/selector/cordis.patch.yml +7 -5
- package/packages/selector/dsh.plugin.json +2 -2
- package/packages/selector/lib/client.d.ts +31 -5
- package/packages/selector/lib/client.js +100 -82
- package/packages/selector/lib/index.d.ts +8 -20
- package/packages/selector/lib/index.js +9 -14
- package/packages/selector/lib/invariant.js +4 -4
- package/packages/selector/lib/pruner.d.ts +13 -4
- package/packages/selector/lib/pruner.js +91 -46
- package/packages/selector/lib/tail-trim.js +13 -4
- package/packages/selector/package.json +23 -23
- package/packages/selector/screenshots.json +1 -4
- package/packages/selector/src/client/CompressionProfileControls.tsx +5 -8
- package/packages/selector/src/client/CompressionProfileSelector.tsx +9 -5
- package/packages/selector/src/client/EstimatorControls.tsx +28 -32
- package/packages/selector/src/client/index.ts +101 -63
- package/packages/selector/src/client/locales.ts +0 -2
- package/packages/selector/src/client/preset-options.ts +49 -35
- package/packages/selector/src/client/settings-section.tsx +4 -6
- package/packages/selector/src/deepseek-v4-tokenizer.ts +0 -5
- package/packages/selector/src/index.ts +41 -46
- package/packages/selector/src/invariant.ts +5 -5
- package/packages/selector/src/pruner.ts +67 -36
- package/packages/selector/src/runtime/measurement.ts +6 -2
- package/packages/selector/src/runtime/session-events.ts +14 -1
- package/packages/selector/src/runtime/tail-trim.ts +4 -4
- package/packages/selector/src/runtime/tokenpilot/estimator.ts +34 -3
- package/packages/selector/src/runtime/types.ts +1 -1
- package/packages/selector/tests/auto-compact.client.spec.tsx +0 -13
- package/packages/selector/tests/built/client-artifact.spec.ts +5 -1
- package/packages/selector/tests/code-skeleton.client.spec.ts +2 -1
- package/packages/selector/tests/custom-contract.client.spec.ts +2 -1
- package/packages/selector/tests/estimator-channel.client.spec.tsx +16 -11
- package/packages/selector/tests/estimator-route-registration.host.spec.ts +12 -0
- package/packages/selector/tests/host-preset-overlay.host.spec.ts +6 -4
- package/packages/selector/tests/preset-options-write.client.spec.ts +87 -79
- package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +17 -12
- package/packages/selector/tests/preset-overlay.host.spec.ts +5 -2
- package/packages/selector/tests/profiles.client.spec.tsx +3 -3
- package/packages/selector/tests/public/package-contract.client.spec.ts +27 -3
- package/packages/selector/tests/runtime/public/public-runtime.spec.ts +103 -81
- package/packages/selector/tests/runtime/session-events.spec.ts +1 -1
- package/packages/selector/tests/settings-seat.client.spec.ts +86 -0
- package/packages/selector/tests/standing-generation.host.spec.ts +88 -71
- package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +24 -21
- package/pnpm-workspace.yaml +52 -5
- package/scripts/capture-profile-baseline.mjs +42 -0
- package/scripts/generate-tokenizer-fixtures.py +5 -5
- package/scripts/generate-vision-fixtures.py +2 -2
- package/scripts/{packed-components-smoke.ts → packed-components-smoke.mjs} +116 -119
- package/scripts/{packed-install-e2e.ts → packed-install-e2e.mjs} +188 -160
- package/scripts/{verify-release.ts → verify-release.mjs} +70 -94
- package/tests/TEST_INVENTORY.md +6 -7
- package/scripts/capture-profile-baseline.ts +0 -80
- package/tsconfig.scripts.json +0 -13
- /package/{packages/selector/assets/screenshots → docs/assets}/context-compression-selector-profiles.jpg +0 -0
- /package/{packages/selector/assets/screenshots → docs/assets}/context-compression-selector-settings.png +0 -0
|
@@ -13,6 +13,7 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
13
13
|
import z from '@deepseek-ai/schemastery'
|
|
14
14
|
import { createUserMessage, freezeMessage } from '@deepseek-ai/dsh-llm'
|
|
15
15
|
import type { ContentBlock, UserMessage } from '@deepseek-ai/dsh-llm'
|
|
16
|
+
import { SessionSeq } from '@deepseek-ai/dsh-session'
|
|
16
17
|
import type { Session, SessionEvent, ToolResultMessage } from '@deepseek-ai/dsh-session'
|
|
17
18
|
import type {} from '@deepseek-ai/dsh-agent'
|
|
18
19
|
import type {} from '@deepseek-ai/dsh-compaction'
|
|
@@ -23,7 +24,7 @@ import type {
|
|
|
23
24
|
TokenCount,
|
|
24
25
|
} from './runtime/measurement.ts'
|
|
25
26
|
import { measureForCompaction } from './runtime/measurement.ts'
|
|
26
|
-
import { sessionEvents } from './runtime/session-events.ts'
|
|
27
|
+
import { eventBySeq, sessionEvents } from './runtime/session-events.ts'
|
|
27
28
|
import { countExactCanonicalTextFields } from './runtime/token-count.ts'
|
|
28
29
|
import type {} from '@deepseek-ai/dsh-tools'
|
|
29
30
|
import {
|
|
@@ -45,7 +46,6 @@ import {
|
|
|
45
46
|
recoveryMarker,
|
|
46
47
|
summarize,
|
|
47
48
|
emptyResult,
|
|
48
|
-
measureContent,
|
|
49
49
|
pressureCost,
|
|
50
50
|
nativePruneContent,
|
|
51
51
|
} from './pruner/content.ts'
|
|
@@ -165,15 +165,12 @@ export type {
|
|
|
165
165
|
PruneResult,
|
|
166
166
|
PruneSessionOptions,
|
|
167
167
|
PruneStage,
|
|
168
|
-
ResolvedConfig,
|
|
169
168
|
ToolResultPruneConfig,
|
|
170
169
|
} from './runtime/types.ts'
|
|
171
170
|
|
|
172
171
|
// Re-export the canonical profile list from the type module as a runtime value.
|
|
173
172
|
export { COMPRESSION_PROFILES } from './runtime/types.ts'
|
|
174
173
|
|
|
175
|
-
|
|
176
|
-
|
|
177
174
|
declare module '@deepseek-ai/cordis' {
|
|
178
175
|
interface Context {
|
|
179
176
|
toolResultPruner: ToolResultPruner
|
|
@@ -204,6 +201,9 @@ export class ToolResultPruner extends Service {
|
|
|
204
201
|
/** Consolidated per-session mutable state. */
|
|
205
202
|
readonly state: PrunerState
|
|
206
203
|
|
|
204
|
+
/** 0.1.5-specific: per-session sets of already-audited native summary seqs. */
|
|
205
|
+
private readonly auditedNativeSummaries = new WeakMap<Session, Set<number>>()
|
|
206
|
+
|
|
207
207
|
constructor(ctx: Context, config: ToolResultPruneConfig = {}) {
|
|
208
208
|
super(ctx, 'toolResultPruner')
|
|
209
209
|
ctx.inject(['tools', 'systemPrompt'], recoveryCtx => {
|
|
@@ -225,19 +225,9 @@ export class ToolResultPruner extends Service {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
ctx.on('session/event', (session, event) => {
|
|
228
|
+
this.scanForSeededNativeSummary(session)
|
|
228
229
|
if (event.type === 'compaction/summary') {
|
|
229
|
-
|
|
230
|
-
schemaVersion: 1,
|
|
231
|
-
kind: 'native-auto-compact',
|
|
232
|
-
sessionId: String(session.id),
|
|
233
|
-
manifestEventType: 'compaction/summary',
|
|
234
|
-
manifestSeq: event.seq,
|
|
235
|
-
reducer: 'llm-summary',
|
|
236
|
-
provider: event.data.provider,
|
|
237
|
-
model: event.data.model,
|
|
238
|
-
tokensBefore: event.data.shadowedTokenCount,
|
|
239
|
-
tokensAfter: null,
|
|
240
|
-
})
|
|
230
|
+
this.emitNativeSummaryAudit(session, event.seq, event.data)
|
|
241
231
|
return
|
|
242
232
|
}
|
|
243
233
|
if (event.type === 'compaction/end') {
|
|
@@ -271,7 +261,12 @@ export class ToolResultPruner extends Service {
|
|
|
271
261
|
ctx.logger.warn('context-compression fresh pass failed open: %o', error)
|
|
272
262
|
}
|
|
273
263
|
}
|
|
274
|
-
|
|
264
|
+
const outcome = await next()
|
|
265
|
+
// BasicCompactionEngine's own pre-step may have just landed a Native
|
|
266
|
+
// auto-compact summary (possibly through a seed-reopen that never
|
|
267
|
+
// reaches the event firehose): scan once more after the step opens.
|
|
268
|
+
this.scanForSeededNativeSummary(agent.session)
|
|
269
|
+
return outcome
|
|
275
270
|
} finally {
|
|
276
271
|
if (this.state.activeRequestBoundaries.get(agent.session) === boundary) {
|
|
277
272
|
this.state.activeRequestBoundaries.delete(agent.session)
|
|
@@ -299,10 +294,6 @@ export class ToolResultPruner extends Service {
|
|
|
299
294
|
* @param blocks - tool-result content to measure.
|
|
300
295
|
* @returns total Unicode code points across text blocks.
|
|
301
296
|
*/
|
|
302
|
-
measureContent(blocks: readonly ContentBlock[]): number {
|
|
303
|
-
return measureContent(blocks)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
297
|
/**
|
|
307
298
|
* Apply the configured native head/middle/tail transform.
|
|
308
299
|
* @param blocks - original tool-result content.
|
|
@@ -326,6 +317,7 @@ export class ToolResultPruner extends Service {
|
|
|
326
317
|
* @returns landed replacements and aggregate Unicode-code-point savings.
|
|
327
318
|
*/
|
|
328
319
|
pruneSession(session: Session, options: PruneSessionOptions = {}): PruneResult {
|
|
320
|
+
this.scanForSeededNativeSummary(session)
|
|
329
321
|
const stage = options.stage ?? 'pressure'
|
|
330
322
|
// External callers (compaction-basic) do not carry routed capacity; the
|
|
331
323
|
// runtime resolves it itself so the frozen Auto Compact linkage and the
|
|
@@ -491,7 +483,7 @@ export class ToolResultPruner extends Service {
|
|
|
491
483
|
// carrying this compaction's checkpoint provenance. Newest match wins.
|
|
492
484
|
let checkpointSeq: number | undefined
|
|
493
485
|
for (const seq of [...session.surface.nodes].reverse()) {
|
|
494
|
-
const event = events
|
|
486
|
+
const event = eventBySeq(events, seq)
|
|
495
487
|
if (event === undefined || event.type !== 'user/message') continue
|
|
496
488
|
const source = (event.data as { source?: { compactionId?: unknown } }).source
|
|
497
489
|
if (source === undefined || source === null) continue
|
|
@@ -518,8 +510,8 @@ export class ToolResultPruner extends Service {
|
|
|
518
510
|
source: { kind: 'plugin', plugin: 'dsh-context-compression-improved-runtime' },
|
|
519
511
|
})
|
|
520
512
|
session.append('user/message', replacement, {
|
|
521
|
-
surfaceOp: { op: 'replace',
|
|
522
|
-
sourceEventSeqs: [checkpointSeq],
|
|
513
|
+
surfaceOp: { op: 'replace', startSeq: SessionSeq(checkpointSeq), endSeq: SessionSeq(checkpointSeq) },
|
|
514
|
+
sourceEventSeqs: [SessionSeq(checkpointSeq)],
|
|
523
515
|
})
|
|
524
516
|
emitCompressionAudit(this.ctx.logger, {
|
|
525
517
|
schemaVersion: 1,
|
|
@@ -672,8 +664,6 @@ export class ToolResultPruner extends Service {
|
|
|
672
664
|
}
|
|
673
665
|
}
|
|
674
666
|
|
|
675
|
-
|
|
676
|
-
|
|
677
667
|
private contextWindowForRequest(
|
|
678
668
|
session: Session,
|
|
679
669
|
): number | undefined {
|
|
@@ -1115,7 +1105,7 @@ export class ToolResultPruner extends Service {
|
|
|
1115
1105
|
const measured = new Map(view.measuredNodes.map(node => [node.seq, node.count]))
|
|
1116
1106
|
const projectionPrices = new Map(view.nodes.map(node => [node.seq, node.tokens]))
|
|
1117
1107
|
for (const seq of [...session.surface.nodes]) {
|
|
1118
|
-
const event = events
|
|
1108
|
+
const event = eventBySeq(events, seq)
|
|
1119
1109
|
if (event?.type !== 'tool/result') continue
|
|
1120
1110
|
const shadowedHeuristicTokenCount = projectionPrices.get(seq)
|
|
1121
1111
|
if (shadowedHeuristicTokenCount === undefined) {
|
|
@@ -1636,7 +1626,7 @@ export class ToolResultPruner extends Service {
|
|
|
1636
1626
|
else if (event.type === 'step/end') completedSteps.add(`${String(event.data.turn)}:${String(event.data.step)}`)
|
|
1637
1627
|
}
|
|
1638
1628
|
const firstCompletedSurfaceTurn = session.surface.nodes
|
|
1639
|
-
.map(seq => events
|
|
1629
|
+
.map(seq => eventBySeq(events, seq))
|
|
1640
1630
|
.filter((event): event is SessionEvent<'assistant/message'> | SessionEvent<'tool/result'> =>
|
|
1641
1631
|
(event?.type === 'assistant/message' || event?.type === 'tool/result')
|
|
1642
1632
|
&& completedTurns.has(event.data.turn))
|
|
@@ -1648,7 +1638,7 @@ export class ToolResultPruner extends Service {
|
|
|
1648
1638
|
for (let index = 0; index < nodes.length; index++) {
|
|
1649
1639
|
const assistantSeq = nodes[index]
|
|
1650
1640
|
if (assistantSeq === undefined) continue
|
|
1651
|
-
const assistant = events
|
|
1641
|
+
const assistant = eventBySeq(events, assistantSeq)
|
|
1652
1642
|
if (assistant?.type !== 'assistant/message'
|
|
1653
1643
|
|| assistant.data.interrupted === true
|
|
1654
1644
|
|| assistant.data.message.content.length === 0
|
|
@@ -1707,7 +1697,8 @@ export class ToolResultPruner extends Service {
|
|
|
1707
1697
|
|| stubCount.tokens <= 0
|
|
1708
1698
|
|| tokensBefore - stubCount.tokens < policy.historyMinReclaimTokens) continue
|
|
1709
1699
|
const heuristicTokens = shadowedSeqs.reduce((sum, seq) => sum + (heuristic.get(seq) ?? 0), 0)
|
|
1710
|
-
const range = { start: assistantSeq, end: resultSeqs.at(-1) ?? assistantSeq }
|
|
1700
|
+
const range = { start: SessionSeq(assistantSeq), end: SessionSeq(resultSeqs.at(-1) ?? assistantSeq) }
|
|
1701
|
+
const surfaceRange = { op: 'replace' as const, startSeq: range.start, endSeq: range.end }
|
|
1711
1702
|
if (!this.reserveTailTrimBoundaryAttempt(session)) {
|
|
1712
1703
|
this.auditComponent(session, policy, 'tail-trim', 'pressure', 'skipped',
|
|
1713
1704
|
'already-attempted-at-request-boundary', {
|
|
@@ -1725,7 +1716,7 @@ export class ToolResultPruner extends Service {
|
|
|
1725
1716
|
let replacement: SessionEvent<'user/message'>
|
|
1726
1717
|
try {
|
|
1727
1718
|
replacement = session.append('user/message', tailTrimMessage(stub), {
|
|
1728
|
-
surfaceOp:
|
|
1719
|
+
surfaceOp: surfaceRange,
|
|
1729
1720
|
sourceEventSeqs: [manifest.seq, ...shadowedSeqs],
|
|
1730
1721
|
})
|
|
1731
1722
|
} catch (error) {
|
|
@@ -1857,8 +1848,8 @@ export class ToolResultPruner extends Service {
|
|
|
1857
1848
|
content: [{ ...result, content: plan.content }] as [typeof result],
|
|
1858
1849
|
})
|
|
1859
1850
|
const manifest = session.append('compaction/prune', {
|
|
1860
|
-
shadowedRange: { start: candidate.seq, end: candidate.seq },
|
|
1861
|
-
shadowedSeqs: [candidate.seq],
|
|
1851
|
+
shadowedRange: { start: SessionSeq(candidate.seq), end: SessionSeq(candidate.seq) },
|
|
1852
|
+
shadowedSeqs: [SessionSeq(candidate.seq)],
|
|
1862
1853
|
shadowedTokenCount: candidate.shadowedHeuristicTokenCount,
|
|
1863
1854
|
})
|
|
1864
1855
|
let replacement: SessionEvent<'tool/result'>
|
|
@@ -1867,8 +1858,8 @@ export class ToolResultPruner extends Service {
|
|
|
1867
1858
|
...candidate.event.data,
|
|
1868
1859
|
message,
|
|
1869
1860
|
}, {
|
|
1870
|
-
surfaceOp: { op: 'replace',
|
|
1871
|
-
sourceEventSeqs: [candidate.seq],
|
|
1861
|
+
surfaceOp: { op: 'replace', startSeq: SessionSeq(candidate.seq), endSeq: SessionSeq(candidate.seq) },
|
|
1862
|
+
sourceEventSeqs: [SessionSeq(candidate.seq)],
|
|
1872
1863
|
})
|
|
1873
1864
|
} catch (error) {
|
|
1874
1865
|
this.auditPublicationFailure(
|
|
@@ -2064,6 +2055,46 @@ export class ToolResultPruner extends Service {
|
|
|
2064
2055
|
})
|
|
2065
2056
|
}
|
|
2066
2057
|
|
|
2058
|
+
/** Emit the native-auto-compact audit for one summary manifest, once. */
|
|
2059
|
+
private emitNativeSummaryAudit(
|
|
2060
|
+
session: Session,
|
|
2061
|
+
manifestSeq: number,
|
|
2062
|
+
data: { provider?: unknown, model?: unknown, shadowedTokenCount?: unknown },
|
|
2063
|
+
): void {
|
|
2064
|
+
let audited = this.auditedNativeSummaries.get(session)
|
|
2065
|
+
if (audited === undefined) {
|
|
2066
|
+
audited = new Set()
|
|
2067
|
+
this.auditedNativeSummaries.set(session, audited)
|
|
2068
|
+
}
|
|
2069
|
+
if (audited.has(manifestSeq)) return
|
|
2070
|
+
audited.add(manifestSeq)
|
|
2071
|
+
emitCompressionAudit(this.ctx.logger, {
|
|
2072
|
+
schemaVersion: 1,
|
|
2073
|
+
kind: 'native-auto-compact',
|
|
2074
|
+
sessionId: String(session.id),
|
|
2075
|
+
manifestEventType: 'compaction/summary',
|
|
2076
|
+
manifestSeq,
|
|
2077
|
+
reducer: 'llm-summary',
|
|
2078
|
+
provider: data.provider === undefined ? 'unknown' : String(data.provider),
|
|
2079
|
+
model: data.model === undefined ? 'unknown' : String(data.model),
|
|
2080
|
+
tokensBefore: typeof data.shadowedTokenCount === 'number' ? data.shadowedTokenCount : null,
|
|
2081
|
+
tokensAfter: null,
|
|
2082
|
+
})
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
/**
|
|
2086
|
+
* 0.1.5 commits Native auto-compact by reopening the Session with a seed
|
|
2087
|
+
* log; seed events never reach the `session/event` firehose, so scan the
|
|
2088
|
+
* snapshot for summary manifests the live listener could not observe.
|
|
2089
|
+
*/
|
|
2090
|
+
private scanForSeededNativeSummary(session: Session): void {
|
|
2091
|
+
for (const event of sessionEvents(session)) {
|
|
2092
|
+
if (event.type === 'compaction/summary') {
|
|
2093
|
+
this.emitNativeSummaryAudit(session, event.seq, event.data)
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2067
2098
|
private auditFailure(
|
|
2068
2099
|
session: Session,
|
|
2069
2100
|
stage: PruneStage,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { Context } from '@deepseek-ai/cordis'
|
|
4
4
|
import type { ContentBlock, TokenUsage } from '@deepseek-ai/dsh-llm'
|
|
5
5
|
import { deriveEventMessage } from '@deepseek-ai/dsh-session'
|
|
6
|
+
import { sessionEvents } from './session-events.ts'
|
|
6
7
|
import type { Session } from '@deepseek-ai/dsh-session'
|
|
7
8
|
import type {} from '@deepseek-ai/dsh-token-meter'
|
|
8
9
|
import type { TokenMeasurement } from '@deepseek-ai/dsh-token-meter'
|
|
@@ -15,7 +16,6 @@ import {
|
|
|
15
16
|
estimateDeepSeekVisionImageTokens,
|
|
16
17
|
} from './deepseek-v4-vision-tokens.ts'
|
|
17
18
|
import { unavailableTokenCount } from './token-count.ts'
|
|
18
|
-
import { sessionEvents } from './session-events.ts'
|
|
19
19
|
import type {
|
|
20
20
|
CanonicalTextTokenCounter,
|
|
21
21
|
ExactTokenizerTokenCount,
|
|
@@ -113,8 +113,12 @@ export function measureForCompaction(ctx: Context, session: Session): Compaction
|
|
|
113
113
|
const target = header?.config
|
|
114
114
|
const counter = bindCounter(target?.provider, target?.model)
|
|
115
115
|
const events = sessionEvents(session)
|
|
116
|
+
// 0.1.5 surface nodes are positional and may replace earlier ranges, so
|
|
117
|
+
// their seq values are not guaranteed to equal the snapshot array index:
|
|
118
|
+
// resolve events by seq, never by position.
|
|
119
|
+
const eventsBySeq = new Map(events.map(event => [Number(event.seq), event]))
|
|
116
120
|
const measuredNodes = measurement.nodes.map((node): MeasuredTokenSurfaceNode => {
|
|
117
|
-
const event =
|
|
121
|
+
const event = eventsBySeq.get(Number(node.seq))
|
|
118
122
|
if (event === undefined) {
|
|
119
123
|
return { seq: node.seq, count: unavailableTokenCount(`surface node ${String(node.seq)} is missing`) }
|
|
120
124
|
}
|
|
@@ -13,5 +13,18 @@ export function sessionEvents(session: Session): readonly SessionEvent[] {
|
|
|
13
13
|
const snapshot = (session as unknown as {
|
|
14
14
|
snapshotEvents?: () => readonly SessionEvent[]
|
|
15
15
|
}).snapshotEvents
|
|
16
|
-
|
|
16
|
+
if (typeof snapshot === 'function') return snapshot.call(session)
|
|
17
|
+
return (session as unknown as { events?: readonly SessionEvent[] }).events ?? []
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve one surface-node seq to its event. 0.1.5 surface nodes are
|
|
22
|
+
* positional and may replace earlier ranges, so a node's seq is not
|
|
23
|
+
* guaranteed to equal the snapshot array index: always resolve by seq.
|
|
24
|
+
*/
|
|
25
|
+
export function eventBySeq(
|
|
26
|
+
events: readonly SessionEvent[],
|
|
27
|
+
seq: number | bigint | { valueOf(): number },
|
|
28
|
+
): SessionEvent | undefined {
|
|
29
|
+
return events[Number(seq)]
|
|
17
30
|
}
|
|
@@ -69,8 +69,8 @@ export function validatePublishedTailTrim(
|
|
|
69
69
|
if (manifest?.type !== 'compaction/prune'
|
|
70
70
|
|| manifest.data.shadowedSeqs.length < 2
|
|
71
71
|
|| manifest.data.shadowedSeqs.length > MAX_ROOTS
|
|
72
|
-
|| manifest.data.shadowedSeqs[0] !== manifest.data.shadowedRange.start
|
|
73
|
-
|| manifest.data.shadowedSeqs.at(-1) !== manifest.data.shadowedRange.end
|
|
72
|
+
|| manifest.data.shadowedSeqs[0] !== Number(manifest.data.shadowedRange.start)
|
|
73
|
+
|| manifest.data.shadowedSeqs.at(-1) !== Number(manifest.data.shadowedRange.end)
|
|
74
74
|
|| new Set(manifest.data.shadowedSeqs).size !== manifest.data.shadowedSeqs.length) return null
|
|
75
75
|
const replacement = events[manifestSeq + 1]
|
|
76
76
|
if (replacement?.type !== 'user/message'
|
|
@@ -79,8 +79,8 @@ export function validatePublishedTailTrim(
|
|
|
79
79
|
|| replacement.data.source.plugin !== 'dsh-context-compression-improved-runtime'
|
|
80
80
|
|| replacement.surfaceOp === undefined
|
|
81
81
|
|| replacement.surfaceOp === 'append'
|
|
82
|
-
|| replacement.surfaceOp.
|
|
83
|
-
|| replacement.surfaceOp.
|
|
82
|
+
|| replacement.surfaceOp.startSeq !== manifest.data.shadowedRange.start
|
|
83
|
+
|| replacement.surfaceOp.endSeq !== manifest.data.shadowedRange.end
|
|
84
84
|
|| !sameNumbers(
|
|
85
85
|
replacement.sourceEventSeqs,
|
|
86
86
|
[manifest.seq, ...manifest.data.shadowedSeqs],
|
|
@@ -118,6 +118,36 @@ export class Estimator {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the host LLM route. Explicit estimator provider/model win; with
|
|
123
|
+
* them empty, reuse the route the harness already has configured via the
|
|
124
|
+
* optional `agentDefaultModel` service's current selection (same seam as
|
|
125
|
+
* dsh-prime-memory's resolveModelRoute) — the user must not re-enter a
|
|
126
|
+
* provider/model the host already knows.
|
|
127
|
+
*/
|
|
128
|
+
private resolveHostRoute(): { provider: string, model: string } | undefined {
|
|
129
|
+
const provider = this.options.estimatorProvider ?? ''
|
|
130
|
+
const model = this.options.estimatorModel ?? ''
|
|
131
|
+
if (provider.length > 0 && model.length > 0) return { provider, model }
|
|
132
|
+
try {
|
|
133
|
+
const defaults = this.ctx.get('agentDefaultModel' as never) as
|
|
134
|
+
| { currentSelection?: () => { provider?: string, model?: string } | undefined }
|
|
135
|
+
| undefined
|
|
136
|
+
const selected = defaults?.currentSelection?.()
|
|
137
|
+
const selectedProvider = selected?.provider ?? ''
|
|
138
|
+
const selectedModel = selected?.model ?? ''
|
|
139
|
+
if (selectedProvider.length > 0 && selectedModel.length > 0) {
|
|
140
|
+
return {
|
|
141
|
+
provider: provider.length > 0 ? provider : selectedProvider,
|
|
142
|
+
model: model.length > 0 ? model : selectedModel,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// optional service; absence must not throw
|
|
147
|
+
}
|
|
148
|
+
return undefined
|
|
149
|
+
}
|
|
150
|
+
|
|
121
151
|
private async askHost(system: string, user: string, signal: AbortSignal): Promise<string | undefined> {
|
|
122
152
|
let llm: HostLlmLike | undefined
|
|
123
153
|
try {
|
|
@@ -126,9 +156,10 @@ export class Estimator {
|
|
|
126
156
|
return undefined
|
|
127
157
|
}
|
|
128
158
|
if (llm?.stream === undefined) return undefined
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
159
|
+
const route = this.resolveHostRoute()
|
|
160
|
+
if (route === undefined) return undefined
|
|
161
|
+
const provider = route.provider
|
|
162
|
+
const model = route.model
|
|
132
163
|
let text = ''
|
|
133
164
|
const stream = llm.stream({
|
|
134
165
|
provider,
|
|
@@ -115,8 +115,6 @@ function mountAutoCompact(options: MountOptions = {}) {
|
|
|
115
115
|
saveCustom: vi.fn(() => Promise.resolve()),
|
|
116
116
|
resetCustom: vi.fn(() => Promise.resolve()),
|
|
117
117
|
saveAutoCompact,
|
|
118
|
-
saveCodeSkeleton: vi.fn(() => Promise.resolve()),
|
|
119
|
-
savePresetOptions: vi.fn(() => Promise.resolve()),
|
|
120
118
|
t,
|
|
121
119
|
} as unknown as CompressionProfileSelectorProps)} />)
|
|
122
120
|
return { state, saveAutoCompact }
|
|
@@ -139,17 +137,6 @@ describe('Auto Compact threshold controls', () => {
|
|
|
139
137
|
expect(screen.getByText(/Auto Compact threshold: 80%/)).not.toBeNull()
|
|
140
138
|
})
|
|
141
139
|
|
|
142
|
-
// Regression guard: the settings surface must keep BOTH section-level gates.
|
|
143
|
-
// A refactor once dropped CodeSkeletonControls from the settings section while
|
|
144
|
-
// leaving the injected save path intact, so the code-skeleton save contract
|
|
145
|
-
// stayed green with the control missing from the UI.
|
|
146
|
-
it('keeps the code-skeleton gate alongside the Auto Compact editor in the settings section', () => {
|
|
147
|
-
mountAutoCompact({ settingsSection: true })
|
|
148
|
-
expect(screen.getByRole('heading', { name: en['codeSkeleton.title'] })).not.toBeNull()
|
|
149
|
-
expect(screen.getByLabelText(en['codeSkeleton.enabled'])).not.toBeNull()
|
|
150
|
-
expect(screen.getByRole('heading', { name: en['autoCompact.title'] })).not.toBeNull()
|
|
151
|
-
})
|
|
152
|
-
|
|
153
140
|
it('saves a non-quick value like 73 and reads the same value back after remount', async () => {
|
|
154
141
|
const saving = vi.fn((thresholdPercent: number): Promise<void> => {
|
|
155
142
|
mounted.state.update(draft => {
|
|
@@ -34,7 +34,11 @@ describe('built Harness client artifact', () => {
|
|
|
34
34
|
}) as { apply?: unknown, inject?: unknown }
|
|
35
35
|
|
|
36
36
|
expect(exported.apply).toBeTypeOf('function')
|
|
37
|
-
|
|
37
|
+
// 0.1.5 declares only the always-present `slots` service: `locale` and
|
|
38
|
+
// `settingsScope` are resolved lazily inside apply, because a declarative
|
|
39
|
+
// inject of an absent or late service suspends apply forever and the whole
|
|
40
|
+
// UI disappears without a trace.
|
|
41
|
+
expect(exported.inject).toEqual(['slots'])
|
|
38
42
|
const style = document.querySelector<HTMLStyleElement>(
|
|
39
43
|
'style[data-plugin-css="dsh-context-compression-improved/CompressionProfileSelector.module.css"]',
|
|
40
44
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import type { ClientContext
|
|
2
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
3
|
+
import type { SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
3
4
|
import { DEFAULT_CUSTOM_COMPRESSION_POLICY } from '../src/profiles.ts'
|
|
4
5
|
import { apply } from '../src/client/index.ts'
|
|
5
6
|
import type {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import type { ClientContext
|
|
2
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
3
|
+
import type { SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
3
4
|
import {
|
|
4
5
|
DEFAULT_CUSTOM_COMPRESSION_POLICY,
|
|
5
6
|
isCustomCompressionPolicy,
|
|
@@ -15,6 +15,16 @@ afterEach(() => {
|
|
|
15
15
|
vi.unstubAllGlobals()
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
+
interface SettingsScopeSnapshot<T> {
|
|
19
|
+
status: 'loading' | 'ready' | 'unavailable'
|
|
20
|
+
value: T | undefined
|
|
21
|
+
base: T | undefined
|
|
22
|
+
user: T | undefined
|
|
23
|
+
revision: number | undefined
|
|
24
|
+
writable: boolean
|
|
25
|
+
mode: 'host' | 'memory'
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
interface SnapshotStore<T> {
|
|
19
29
|
readonly getSnapshot: () => T
|
|
20
30
|
readonly subscribe: (listener: () => void) => () => void
|
|
@@ -82,10 +92,10 @@ function mountEstimator(
|
|
|
82
92
|
presetOptions: ContextCompressionSettings['presetOptions'],
|
|
83
93
|
options: { readonly catalog?: unknown, readonly profile?: ContextCompressionSettings['profile'] } = {},
|
|
84
94
|
) {
|
|
85
|
-
const state = createSnapshotStore({
|
|
86
|
-
status: 'ready'
|
|
95
|
+
const state = createSnapshotStore<SettingsScopeSnapshot<ContextCompressionSettings>>({
|
|
96
|
+
status: 'ready',
|
|
87
97
|
value: {
|
|
88
|
-
profile: options.profile ??
|
|
98
|
+
profile: options.profile ?? 'tokenpilot-inspired',
|
|
89
99
|
custom: structuredClone(DEFAULT_CUSTOM),
|
|
90
100
|
autoCompact: { thresholdPercent: 80 },
|
|
91
101
|
codeSkeleton: { enabled: false },
|
|
@@ -95,12 +105,7 @@ function mountEstimator(
|
|
|
95
105
|
user: undefined,
|
|
96
106
|
revision: 0,
|
|
97
107
|
writable: true,
|
|
98
|
-
mode: 'host'
|
|
99
|
-
})
|
|
100
|
-
const sessions = createSnapshotStore({
|
|
101
|
-
ids: ['s1'],
|
|
102
|
-
byId: { s1: { id: 's1', agentPreset: 'standard' } },
|
|
103
|
-
current: 's1',
|
|
108
|
+
mode: 'host',
|
|
104
109
|
})
|
|
105
110
|
const savePresetOptions = vi.fn(() => Promise.resolve())
|
|
106
111
|
if (options.catalog !== undefined) {
|
|
@@ -111,7 +116,6 @@ function mountEstimator(
|
|
|
111
116
|
}
|
|
112
117
|
render(<ContextCompressionSettingsSection {...({
|
|
113
118
|
useCompression: bindSnapshotSelector(state),
|
|
114
|
-
useSessions: bindSnapshotSelector(sessions),
|
|
115
119
|
select: vi.fn(() => Promise.resolve()),
|
|
116
120
|
saveCustom: vi.fn(() => Promise.resolve()),
|
|
117
121
|
resetCustom: vi.fn(() => Promise.resolve()),
|
|
@@ -188,7 +192,8 @@ describe('estimator channel card', () => {
|
|
|
188
192
|
const { savePresetOptions } = mountEstimator({ estimatorMode: 'host' })
|
|
189
193
|
|
|
190
194
|
const provider = screen.getByLabelText<HTMLInputElement>(PROVIDER_LABEL)
|
|
191
|
-
|
|
195
|
+
// The model field stays present and editable without a served catalog.
|
|
196
|
+
expect(screen.getByLabelText<HTMLInputElement>(MODEL_LABEL)).toBeTruthy()
|
|
192
197
|
expect(document.querySelectorAll('#estimator-provider-options option').length).toBe(0)
|
|
193
198
|
expect(document.querySelectorAll('#estimator-model-options option').length).toBe(0)
|
|
194
199
|
fireEvent.change(provider, { target: { value: 'manual-group' } })
|
|
@@ -173,4 +173,16 @@ describe('estimator catalog route registration', () => {
|
|
|
173
173
|
expect(response.status).toBe(200)
|
|
174
174
|
expect(JSON.parse(String(response.body))).toMatchObject({ ok: true, providers: [] })
|
|
175
175
|
})
|
|
176
|
+
|
|
177
|
+
it('registers no route when the row does not opt in', async () => {
|
|
178
|
+
const routes: RegisteredRoute[] = []
|
|
179
|
+
const runtime = new Context()
|
|
180
|
+
ctx = runtime
|
|
181
|
+
await mountWebServer(runtime, routes)
|
|
182
|
+
|
|
183
|
+
apply(runtime, { presetOverlay: false })
|
|
184
|
+
await settle()
|
|
185
|
+
|
|
186
|
+
expect(routes).toHaveLength(0)
|
|
187
|
+
})
|
|
176
188
|
})
|
|
@@ -5,13 +5,15 @@ import { Context, Service } from '@deepseek-ai/cordis'
|
|
|
5
5
|
import type { AgentPreset } from '@deepseek-ai/dsh-agent-presets'
|
|
6
6
|
import {
|
|
7
7
|
SettingsProvider,
|
|
8
|
-
settingsNamespace,
|
|
9
8
|
type SettingsNamespace,
|
|
10
9
|
} from '@deepseek-ai/dsh-settings'
|
|
11
10
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
12
11
|
import { apply } from '../src/index.ts'
|
|
13
12
|
import type { OverlayableAgentPresets } from '../src/preset-overlay.ts'
|
|
14
13
|
|
|
14
|
+
// 0.1.5 removed the settingsNamespace() wrapper; namespaces are validated at runtime.
|
|
15
|
+
const nsBrand = (value: string): SettingsNamespace => value as unknown as SettingsNamespace
|
|
16
|
+
|
|
15
17
|
let root: string | undefined
|
|
16
18
|
let ctx: Context | undefined
|
|
17
19
|
|
|
@@ -97,7 +99,7 @@ describe('context compression selector Host preset integration', () => {
|
|
|
97
99
|
const builtIn = ctx.plugin({ apply })
|
|
98
100
|
await builtIn.await()
|
|
99
101
|
|
|
100
|
-
const namespace =
|
|
102
|
+
const namespace = nsBrand('context-compression')
|
|
101
103
|
const service = ctx.agentPresets as unknown as OverlayableAgentPresets
|
|
102
104
|
expect((await service.mount({})).path).toBe(preset.path)
|
|
103
105
|
expect(ctx.settings.describe().filter(row => row.ns === namespace)).toHaveLength(1)
|
|
@@ -133,7 +135,7 @@ describe('context compression selector Host preset integration', () => {
|
|
|
133
135
|
await ctx.plugin(FakeAgentPresets, preset).await()
|
|
134
136
|
// A settings-owning row must register the namespace before the update.
|
|
135
137
|
await ctx.plugin({ apply }).await()
|
|
136
|
-
const namespace =
|
|
138
|
+
const namespace = nsBrand('context-compression')
|
|
137
139
|
await ctx.settings.update(namespace, { autoCompact: { thresholdPercent: 73 } })
|
|
138
140
|
|
|
139
141
|
const bundle = ctx.plugin({ apply: (child) => {
|
|
@@ -179,7 +181,7 @@ describe('context compression selector Host preset integration', () => {
|
|
|
179
181
|
await ctx.plugin(FakeAgentPresets, preset).await()
|
|
180
182
|
// A settings-owning row must register the namespace before the update.
|
|
181
183
|
await ctx.plugin({ apply }).await()
|
|
182
|
-
const namespace =
|
|
184
|
+
const namespace = nsBrand('context-compression')
|
|
183
185
|
await ctx.settings.update(namespace, { autoCompact: { thresholdPercent: 70 } })
|
|
184
186
|
|
|
185
187
|
const bundle = ctx.plugin({ apply: (child) => {
|