fix(token-meter): bound projection state via logged shadow prices

The contextBreakdown and contextPressure units carried the full priced
surface, so each session's persisted projection checkpoint grew without
bound. A surface replacement is now priced by the shadow-price event
logged directly before it — compact/summary for compaction, the new
compact/prune from tool-result pruning (priced through the injected
token meter) — and the unit states shrink to a fixed handful of numbers.
Regenerate the persistence/cordis/module/config catalogs.
This commit is contained in:
imccyu
2026-08-06 02:50:38 +08:00
parent 6400e521ba
commit 59df683ef1
21 changed files with 403 additions and 81 deletions

View File

@@ -405,6 +405,8 @@ Source: [`packages/compact/compact-basic/src/types.ts:38`](../packages/compact/c
## `@deepseek-ai/dsh-compact-tool-result-prune`
Requires: `tokenMeter`
```ts config-catalog
/** Character-budget policy for deterministic tool-result pruning. */
export interface ToolResultPruneConfig {

View File

@@ -2358,7 +2358,10 @@ pruneContent(blocks: readonly ContentBlock[]): ContentBlock[] | null
/**
* Prune every over-budget tool result from one stable current-surface snapshot.
* Each replacement preserves the complete event data except for `content`,
* and points at the shadowed node for durable provenance and replay.
* points at the shadowed node for durable provenance and replay, and is
* immediately preceded by a `compact/prune` shadow-price event pricing the
* shadowed node through the injected token meter, so pure consumers can
* subtract it without per-node state.
* @param session - session whose current surface is rewritten.
* @returns landed replacements and aggregate Unicode-code-point savings.
* @throws when the session rejects a replacement; replacements committed
@@ -2369,7 +2372,7 @@ pruneSession(session: Session): PruneResult
Types: [ContentBlock](../core-data-structures/core.md) · [PruneResult](../core-data-structures/compaction.md) · [Session](../core-data-structures/session.md)
Source: [`packages/compact/compact-tool-result-prune/src/index.ts:40`](../../packages/compact/compact-tool-result-prune/src/index.ts)
Source: [`packages/compact/compact-tool-result-prune/src/index.ts:44`](../../packages/compact/compact-tool-result-prune/src/index.ts)
## `ctx.tools` — `ToolRegistry`

View File

@@ -428,9 +428,6 @@ flowchart TD
pkg_compact --> pkg_invariants
pkg_compact --> pkg_llm
pkg_compact --> pkg_session
pkg_compact_tool_result_prune --> pkg_invariants
pkg_compact_tool_result_prune --> pkg_llm
pkg_compact_tool_result_prune --> pkg_session
pkg_web_fetch_local --> pkg_invariants
pkg_web_fetch_local --> pkg_timeout
pkg_web_fetch_local --> pkg_web
@@ -490,6 +487,7 @@ flowchart TD
pkg_llm_retry --> pkg_llm
pkg_llm_retry --> pkg_session
pkg_llm_retry --> pkg_timeout
pkg_token_meter --> pkg_compact
pkg_token_meter --> pkg_invariants
pkg_token_meter --> pkg_llm
pkg_token_meter --> pkg_session
@@ -626,13 +624,11 @@ flowchart TD
pkg_command_compact --> pkg_commands
pkg_command_compact --> pkg_compact
pkg_command_compact --> pkg_invariants
pkg_compact_basic --> pkg_agent
pkg_compact_basic --> pkg_compact
pkg_compact_basic --> pkg_compact_tool_result_prune
pkg_compact_basic --> pkg_invariants
pkg_compact_basic --> pkg_llm
pkg_compact_basic --> pkg_session
pkg_compact_basic --> pkg_token_meter
pkg_compact_tool_result_prune --> pkg_compact
pkg_compact_tool_result_prune --> pkg_invariants
pkg_compact_tool_result_prune --> pkg_llm
pkg_compact_tool_result_prune --> pkg_session
pkg_compact_tool_result_prune --> pkg_token_meter
pkg_session_query --> pkg_brand
pkg_session_query --> pkg_invariants
pkg_session_query --> pkg_llm
@@ -728,6 +724,13 @@ flowchart TD
pkg_tool_skill --> pkg_llm
pkg_tool_skill --> pkg_skill
pkg_tool_skill --> pkg_tools
pkg_compact_basic --> pkg_agent
pkg_compact_basic --> pkg_compact
pkg_compact_basic --> pkg_compact_tool_result_prune
pkg_compact_basic --> pkg_invariants
pkg_compact_basic --> pkg_llm
pkg_compact_basic --> pkg_session
pkg_compact_basic --> pkg_token_meter
pkg_subagent --> pkg_agent
pkg_subagent --> pkg_brand
pkg_subagent --> pkg_invariants
@@ -1138,7 +1141,6 @@ flowchart TD
| [`bash`](../packages/bash/bash) | `bash` | [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`subprocess`](../packages/subprocess/subprocess) |
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`compact`](../packages/compact/compact) | `compact` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | `compact` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`web-fetch-local`](../packages/web/web-fetch-local) | `web` | [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
| [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`invariants`](../packages/support/invariants), [`web`](../packages/web/web) |
| [`web-search-perplexity`](../packages/web/web-search-perplexity) | `web` | [`invariants`](../packages/support/invariants), [`web`](../packages/web/web) |
@@ -1155,7 +1157,7 @@ flowchart TD
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`session-projection`](../packages/session-projection/session-projection) | `session-projection` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
| [`token-meter`](../packages/llm/token-meter) | `llm` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session-projection/session-projection) |
| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session-projection/session-projection) |
| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session-projection/session-projection) |
| [`bash-local`](../packages/bash/bash-local) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`pwsh-local`](../packages/bash/pwsh-local) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
@@ -1188,7 +1190,7 @@ flowchart TD
| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`command-compact`](../packages/compact/command-compact) | `compact` | [`commands`](../packages/ui/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants) |
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
| [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | `compact` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-title`](../packages/session-title/session-title) |
| [`session-title-llm`](../packages/session-title/session-title-llm) | `session-title` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session-title/session-title), [`timeout`](../packages/util/timeout) |
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-approval`](../packages/ui/user-approval) |
@@ -1204,6 +1206,7 @@ flowchart TD
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection`](../packages/session-projection/session-projection), [`session-query`](../packages/session-query/session-query), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) |

View File

@@ -236,7 +236,31 @@ Source: [`packages/ui/commands/src/index.ts:132`](../packages/ui/commands/src/in
'compact/end': { turn: number | null; error?: string }
```
Source: [`packages/compact/compact/src/types.ts:51`](../packages/compact/compact/src/types.ts)
Source: [`packages/compact/compact/src/types.ts:54`](../packages/compact/compact/src/types.ts)
#### `compact/prune` — log-only
```ts persistence-catalog
/**
* Shadow price of one model-free prune replacement — log-only, no
* surfaceOp. The shared shadow-price protocol: a surface `replace` event
* is priced by the metering event immediately before it (`compact/summary`
* for a summarizing compaction, this event for a prune), which states the
* heuristic token price of the exact replaced range so a pure consumer
* can subtract it without retaining per-node prices. The replacement MUST
* be appended synchronously right after this event.
*/
'compact/prune': {
/** The replaced range's first and last surface-node seqs (a surface-position span, like {@link CompactionResult.shadowedRange}). */
shadowedRange: { start: number; end: number }
/** The seqs of all shadowed surface nodes, in surface order. */
shadowedSeqs: number[]
/** Heuristic price of the shadowed content under the token-meter's fixed estimator. */
shadowedTokenCount: number
}
```
Source: [`packages/compact/compact/src/types.ts:64`](../packages/compact/compact/src/types.ts)
#### `compact/start` — log-only
@@ -257,8 +281,11 @@ Source: [`packages/compact/compact/src/types.ts:19`](../packages/compact/compact
/**
* Provenance record of a completed summarization — log-only, no surfaceOp.
* The summary content is in `data.summary`; the actual surface replacement
* is performed by a subsequent `user/message` event that shadows the
* compacted range.
* is performed by the immediately following `user/message` event that
* shadows the compacted range. That adjacency is contractual — the
* shadowed pricing fields are the replacement's shadow price, so a
* consumer may pair a replacement with the metering event directly
* before it (`compact/prune` documents the shared protocol).
*/
'compact/summary': {
summary: ContentBlock[]
@@ -285,7 +312,7 @@ Source: [`packages/compact/compact/src/types.ts:19`](../packages/compact/compact
Types: [ContentBlock](core-data-structures/core.md) · [TokenUsage](core-data-structures/llm-streaming.md)
Source: [`packages/compact/compact/src/types.ts:26`](../packages/compact/compact/src/types.ts)
Source: [`packages/compact/compact/src/types.ts:29`](../packages/compact/compact/src/types.ts)
### `goal/*`

View File

@@ -25,9 +25,11 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-compact": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-token-meter": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"dependencies": {
@@ -36,9 +38,11 @@
"devDependencies": {
"@cordisjs/plugin-include": "workspace:^",
"@cordisjs/plugin-loader": "workspace:^",
"@deepseek-ai/dsh-compact": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-token-meter": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -9,6 +9,10 @@ import z from 'schemastery'
import { freezeMessage } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { Session, SessionEvent, ToolResultMessage } from '@deepseek-ai/dsh-session'
// Type-only: the `compact/*` SessionEventMap merges (the shadow-price event).
import type {} from '@deepseek-ai/dsh-compact'
// Type-only: the `ctx.tokenMeter` Context merge for the declared injection.
import type {} from '@deepseek-ai/dsh-token-meter'
import { codePointLength, DEFAULTS, PRUNE_MARKER, resolveConfig } from './config.ts'
import type {
PrunedEntry,
@@ -38,6 +42,10 @@ interface SnapshotCandidate {
/** Deterministic head/middle/tail pruning for current tool-result surface nodes. */
export class ToolResultPruneService extends Service {
// The token meter prices each shadowed node for its logged shadow-price
// event, so pruning genuinely requires the pricing capability.
static inject = ['tokenMeter']
static Config: z<ToolResultPruneConfig> = z.object({
thresholdChars: z.number().step(1).min(1).default(DEFAULTS.thresholdChars),
headChars: z.number().step(1).min(0).default(DEFAULTS.headChars),
@@ -116,7 +124,10 @@ export class ToolResultPruneService extends Service {
/**
* Prune every over-budget tool result from one stable current-surface snapshot.
* Each replacement preserves the complete event data except for `content`,
* and points at the shadowed node for durable provenance and replay.
* points at the shadowed node for durable provenance and replay, and is
* immediately preceded by a `compact/prune` shadow-price event pricing the
* shadowed node through the injected token meter, so pure consumers can
* subtract it without per-node state.
* @param session - session whose current surface is rewritten.
* @returns landed replacements and aggregate Unicode-code-point savings.
* @throws when the session rejects a replacement; replacements committed
@@ -145,6 +156,14 @@ export class ToolResultPruneService extends Service {
content,
}] as [typeof result],
})
// Shadow-price protocol: the metering event and its replacement are
// appended synchronously adjacent, so pure consumers subtract the
// shadowed node's heuristic price without retaining per-node state.
session.append('compact/prune', {
shadowedRange: { start: seq, end: seq },
shadowedSeqs: [seq],
shadowedTokenCount: this.ctx.tokenMeter.estimateMessage(event.data.message),
})
const replacement = session.append('tool/result', {
...event.data,
message,

View File

@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import Include from '@cordisjs/plugin-include'
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune'
let root: string | undefined
@@ -23,6 +24,7 @@ describe('compact-tool-result-prune real Loader composition', () => {
root = await mkdtemp(join(tmpdir(), 'dsh-compact-tool-result-prune-loader-'))
const configPath = join(root, 'cordis.yml')
await writeFile(configPath, [
"- name: '@deepseek-ai/dsh-token-meter'",
"- name: '@deepseek-ai/dsh-compact-tool-result-prune'",
' config:',
' thresholdChars: 100',
@@ -38,10 +40,9 @@ describe('compact-tool-result-prune real Loader composition', () => {
context.loader.internal = {
version: 'v2',
async import(specifier: string) {
if (specifier !== '@deepseek-ai/dsh-compact-tool-result-prune') {
throw new Error(`unexpected Loader import: ${specifier}`)
}
return ToolResultPruneService
if (specifier === '@deepseek-ai/dsh-token-meter') return TokenMeterService
if (specifier === '@deepseek-ai/dsh-compact-tool-result-prune') return ToolResultPruneService
throw new Error(`unexpected Loader import: ${specifier}`)
},
} as unknown as NonNullable<typeof context.loader.internal>
await context.loader.create({
@@ -60,6 +61,9 @@ describe('compact-tool-result-prune real Loader composition', () => {
it('rejects stale config after plugin schema normalization', async () => {
context = new Context()
// Satisfy the declared injection first: config normalization runs in the
// service constructor, which a pending fiber never reaches.
await context.plugin(TokenMeterService)
await expect(context.plugin(ToolResultPruneService, {
maxChars: 100,
} as never)).rejects.toThrow(/unknown key "maxChars"/)

View File

@@ -9,6 +9,7 @@ import SessionStore, {
import type { SurfaceEvent } from '@deepseek-ai/dsh-session'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
import ToolResultPruneService, {
codePointLength,
DEFAULTS,
@@ -25,9 +26,16 @@ const SMALL: ToolResultPruneConfig = {
}
function service(config: ToolResultPruneConfig = SMALL): ToolResultPruneService {
return new ToolResultPruneService(new Context(), config)
const ctx = new Context()
// Service constructors self-register, so `ctx.tokenMeter` resolves for the
// shadow-price pricing without a full plugin boot.
void new TokenMeterService(ctx)
return new ToolResultPruneService(ctx, config)
}
/** Pricing oracle mirroring the service's estimator for expectations. */
const METER = new TokenMeterService(new Context())
function appendToolStep(
session: Session,
turn: number,
@@ -203,6 +211,18 @@ describe('ToolResultPruneService session transaction', () => {
sourceEventSeqs: [originalSeq],
})
expect(session.surface.nodes).not.toContain(originalSeq)
// Shadow-price protocol: the metering event sits directly before the
// replacement and prices the shadowed node with the shared estimator.
if (original.type !== 'tool/result') throw new Error('original is not a tool/result')
expect(session.events[entry.replacementSeq - 1]).toMatchObject({
type: 'compact/prune',
data: {
shadowedRange: { start: originalSeq, end: originalSeq },
shadowedSeqs: [originalSeq],
shadowedTokenCount: METER.estimateMessage(original.data.message),
},
})
})
it('prunes multiple results, skips short ones, and converges in one pass', () => {
@@ -240,6 +260,7 @@ describe('ToolResultPruneService session transaction', () => {
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantService)
await ctx.plugin(SessionInvariant)
await ctx.plugin(TokenMeterService)
const prune = new ToolResultPruneService(ctx, SMALL)
const session = ctx.sessions.create(SessionId('invariants'))
appendToolStep(session, 1, 'a', [{ type: 'text', text: 'A'.repeat(100) }])

View File

@@ -10,7 +10,9 @@
{ "path": "../../../vendor/cordis" },
{ "path": "../../../vendor/schemastery" },
{ "path": "../../llm/llm" },
{ "path": "../../llm/token-meter" },
{ "path": "../../core/session" },
{ "path": "../compact" },
{ "path": "../../support/invariants" }
]
}

View File

@@ -20,8 +20,11 @@ declare module '@deepseek-ai/dsh-session' {
/**
* Provenance record of a completed summarization — log-only, no surfaceOp.
* The summary content is in `data.summary`; the actual surface replacement
* is performed by a subsequent `user/message` event that shadows the
* compacted range.
* is performed by the immediately following `user/message` event that
* shadows the compacted range. That adjacency is contractual — the
* shadowed pricing fields are the replacement's shadow price, so a
* consumer may pair a replacement with the metering event directly
* before it (`compact/prune` documents the shared protocol).
*/
'compact/summary': {
summary: ContentBlock[]
@@ -49,6 +52,23 @@ declare module '@deepseek-ai/dsh-session' {
* matches `compact/start`; `error` records an unsuccessful attempt.
*/
'compact/end': { turn: number | null; error?: string }
/**
* Shadow price of one model-free prune replacement — log-only, no
* surfaceOp. The shared shadow-price protocol: a surface `replace` event
* is priced by the metering event immediately before it (`compact/summary`
* for a summarizing compaction, this event for a prune), which states the
* heuristic token price of the exact replaced range so a pure consumer
* can subtract it without retaining per-node prices. The replacement MUST
* be appended synchronously right after this event.
*/
'compact/prune': {
/** The replaced range's first and last surface-node seqs (a surface-position span, like {@link CompactionResult.shadowedRange}). */
shadowedRange: { start: number; end: number }
/** The seqs of all shadowed surface nodes, in surface order. */
shadowedSeqs: number[]
/** Heuristic price of the shadowed content under the token-meter's fixed estimator. */
shadowedTokenCount: number
}
}
}

View File

@@ -1058,7 +1058,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
},
{
signature: 'pruneSession(session: Session): PruneResult',
jsDoc: '/**\n * Prune every over-budget tool result from one stable current-surface snapshot.\n * Each replacement preserves the complete event data except for `content`,\n * and points at the shadowed node for durable provenance and replay.\n * @param session - session whose current surface is rewritten.\n * @returns landed replacements and aggregate Unicode-code-point savings.\n * @throws when the session rejects a replacement; replacements committed\n * earlier in the pass remain durable.\n */',
jsDoc: '/**\n * Prune every over-budget tool result from one stable current-surface snapshot.\n * Each replacement preserves the complete event data except for `content`,\n * points at the shadowed node for durable provenance and replay, and is\n * immediately preceded by a `compact/prune` shadow-price event pricing the\n * shadowed node through the injected token meter, so pure consumers can\n * subtract it without per-node state.\n * @param session - session whose current surface is rewritten.\n * @returns landed replacements and aggregate Unicode-code-point savings.\n * @throws when the session rejects a replacement; replacements committed\n * earlier in the pass remain durable.\n */',
},
],
},

View File

@@ -30,6 +30,7 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-compact": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
@@ -41,6 +42,7 @@
"zod": "^4.4.3"
},
"devDependencies": {
"@deepseek-ai/dsh-compact": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",

View File

@@ -6,11 +6,11 @@
*/
import { z } from 'zod'
import { canonicalHeader, isSurfaceEvent } from '@deepseek-ai/dsh-session'
import { canonicalHeader } from '@deepseek-ai/dsh-session'
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
import type { TokenSurfaceNode } from './types.ts'
import { estimateSystemTokens, estimateToolsTokens } from './estimate.ts'
import { foldSurfaceTokens } from './surface-fold.ts'
import { foldSurfaceProjection } from './surface-projection.ts'
import type { ShadowPriceClaim } from './surface-projection.ts'
// Import for the `contextBreakdown` SessionProjectionMap key merge.
import type {} from './projection.ts'
@@ -18,8 +18,8 @@ interface ContextBreakdownState {
systemTokens: number
toolsTokens: number
messageTokens: number
/** Priced surface nodes (plain JSON for the persisted projection cache). */
surface: TokenSurfaceNode[]
/** Shadow price armed by the immediately preceding metering event. */
claim?: ShadowPriceClaim
}
const breakdownSchema = z.object({
@@ -32,31 +32,38 @@ const breakdownSchema = z.object({
* Token-meter's context-composition projection unit.
*
* Envelope figures are last-wins per `request/header`; the message figure
* rides {@link foldSurfaceTokens} — the same fold the measurement service
* replays — so it equals `measure().surfaceTokens` at every event boundary and
* compaction shrinks it the way it shrinks the next request.
* rides {@link foldSurfaceProjection} — the same O(1) fold the occupancy
* projection uses — so it equals `measure().surfaceTokens` at every event
* boundary and compaction shrinks it by its logged shadow price, the way it
* shrinks the next request. The state is a fixed handful of numbers, so the
* persisted checkpoint stays O(1) over the session's life.
*/
export const contextBreakdownProjectionDefinition:
ProjectionDefinition<'contextBreakdown', ContextBreakdownState> = {
key: 'contextBreakdown',
schema: breakdownSchema,
init: () => ({ systemTokens: 0, toolsTokens: 0, messageTokens: 0, surface: [] }),
init: () => ({ systemTokens: 0, toolsTokens: 0, messageTokens: 0 }),
apply: (state, event) => {
const fold = foldSurfaceProjection(state.claim, event)
let systemTokens = state.systemTokens
let toolsTokens = state.toolsTokens
if (event.type === 'request/header') {
const header = canonicalHeader(event.data.header)
const systemTokens = estimateSystemTokens(header)
const toolsTokens = estimateToolsTokens(header)
if (systemTokens === state.systemTokens && toolsTokens === state.toolsTokens) return state
return { ...state, systemTokens, toolsTokens }
systemTokens = estimateSystemTokens(header)
toolsTokens = estimateToolsTokens(header)
}
if (!isSurfaceEvent(event)) return state
const fold = foldSurfaceTokens(state.surface, event)
if (systemTokens === state.systemTokens
&& toolsTokens === state.toolsTokens
&& fold.deltaTokens === 0
&& fold.claim === undefined
&& state.claim === undefined) return state
return {
...state,
systemTokens,
toolsTokens,
messageTokens: state.messageTokens + fold.deltaTokens,
surface: fold.nodes,
...fold.claim === undefined ? {} : { claim: fold.claim },
}
},
view: ({ systemTokens, toolsTokens, messageTokens }) => ({ systemTokens, toolsTokens, messageTokens }),
stateVersion: 1,
stateVersion: 2,
}

View File

@@ -20,9 +20,11 @@ export const inject = ['invariants']
* three projections do expose observation streams, but their schemas fix the
* JSON payloads; the usage folds replace same-step samples, so totals need not
* be monotone when a final sample corrects an earlier chunk, and the
* composition fold shares `surface-fold.ts` with the measurement service,
* which makes its message figure equal `measure().surfaceTokens` by
* construction rather than by a relation worth observing at runtime.
* composition fold prices through the same `estimate.ts` heuristic as the
* measurement service and subtracts producer-logged shadow prices derived
* from that service's own nodes, which makes its message figure equal
* `measure().surfaceTokens` by construction rather than by a relation worth
* observing at runtime.
*/
const install: InvariantInstaller = () => {}

View File

@@ -1,10 +1,11 @@
/**
* The one positional surface fold, shared by the measurement service's replay
* state and the pure `contextBreakdown` projection. Both answer "what does the
* current model-visible conversation cost", so they MUST price and place every
* node identically: a private copy in either owner would let the panel's
* message figure drift away from `measure().surfaceTokens` with both sides
* still passing their own tests.
* The measurement service's positional surface fold: the per-node priced
* surface `measure()` serves and compaction plans against. The projection
* units deliberately do NOT share this fold — their state must stay O(1)
* for the persisted checkpoint, so they ride `surface-projection.ts`'s
* shadow-price protocol instead. The two stay in agreement by construction:
* both price through `estimate.ts`, and every logged shadow price is derived
* from THIS fold's nodes by the replace producer.
*
* @module @deepseek-ai/dsh-token-meter/surface-fold
*/

View File

@@ -0,0 +1,84 @@
/**
* The O(1) surface-token fold shared by the token-meter projection units.
*
* A projection state must stay bounded — the persisted projection cache
* checkpoints every unit's whole state, so carrying the priced surface
* (one node per model-visible message) would grow a checkpoint without
* bound over the session's life. Instead, replacements ride the compact
* seam's shadow-price protocol: the metering event immediately before a
* surface `replace` (`compact/summary` or `compact/prune`) states the
* heuristic price of the exact replaced range, so the fold keeps a running
* total plus at most one pending claim and never retains per-node prices.
* The counts are exact by construction: producers derive them from the same
* fixed estimator this module prices appends with.
*
* @module @deepseek-ai/dsh-token-meter/surface-projection
*/
import { deriveEventMessage, isSurfaceEvent } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
// Type-only: the `compact/*` SessionEventMap merges (shadow-price events).
import type {} from '@deepseek-ai/dsh-compact'
import { estimateMessage } from './estimate.ts'
/**
* One armed shadow price: the heuristic tokens of the surface range the
* IMMEDIATELY following event replaces. Plain JSON — it is part of the
* persisted unit state while armed.
*/
export interface ShadowPriceClaim {
/** Declared inclusive first surface-node seq of the priced range. */
start: number
/** Declared inclusive last surface-node seq of the priced range. */
end: number
/** Heuristic tokens of the priced range under the fixed estimator. */
tokens: number
}
/** One event's effect on a running surface-token total. */
export interface SurfaceTokensFold {
/** Signed change in the surface total; 0 for events off the surface. */
readonly deltaTokens: number
/** Claim to carry into the next event; undefined when none survives. */
readonly claim: ShadowPriceClaim | undefined
}
/**
* Fold one committed event onto a running surface-token total.
*
* A shadow-price event arms a claim; any other event expires it, and a
* surface `replace` must consume a claim naming its exact range — the
* producers append the metering event and the replacement synchronously
* adjacent, so a surviving claim always prices the very next event.
* @param claim - the claim armed by the immediately preceding event, if any.
* @param event - the next committed session event.
* @returns the signed token delta and the claim state after this event.
* @throws when a replacement arrives without a claim for its exact range —
* every in-repo replace producer meters its replacement, so an unpriced
* replacement is a shadow-price contract violation and must fail loud
* rather than let the total drift.
*/
export function foldSurfaceProjection(
claim: ShadowPriceClaim | undefined,
event: SessionEvent,
): SurfaceTokensFold {
if (event.type === 'compact/summary' || event.type === 'compact/prune') {
const { shadowedRange, shadowedTokenCount } = event.data
return {
deltaTokens: 0,
claim: { start: shadowedRange.start, end: shadowedRange.end, tokens: shadowedTokenCount },
}
}
if (!isSurfaceEvent(event)) return { deltaTokens: 0, claim: undefined }
const message = deriveEventMessage(event)
const tokens = message === null ? 0 : estimateMessage(message)
const op = event.surfaceOp
if (op === 'append') return { deltaTokens: tokens, claim: undefined }
if (claim === undefined || claim.start !== op.start || claim.end !== op.end) {
throw new Error(
`token surface: replace at seq ${event.seq} over range ${op.start}-${op.end} has no adjacent shadow price`
+ (claim === undefined ? '' : ` (armed claim covers ${claim.start}-${claim.end})`),
)
}
return { deltaTokens: tokens - claim.tokens, claim: undefined }
}

View File

@@ -4,12 +4,11 @@
import { z } from 'zod'
import type { TokenUsage } from '@deepseek-ai/dsh-llm'
import { isSurfaceEvent } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
import type { ContextPressureProjection, TokenUsageProjection } from './projection.ts'
import type { TokenSurfaceNode } from './types.ts'
import { foldSurfaceTokens } from './surface-fold.ts'
import { foldSurfaceProjection } from './surface-projection.ts'
import type { ShadowPriceClaim } from './surface-projection.ts'
interface UsageSample {
turn: number
@@ -82,19 +81,25 @@ const usageOf = (event: SessionEvent): TokenUsage | undefined =>
/**
* Context-occupancy state: the two independent last-wins records plus the
* priced surface needed to carry the newest sample forward.
* O(1) running surface total needed to carry the newest sample forward.
*/
interface ContextPressureState {
contextWindow?: number
pressureTokens?: number
/** Priced surface, folded identically to the measurement service's. */
surface: TokenSurfaceNode[]
/** Summed heuristic tokens over {@link surface}. */
/** Running heuristic total over the current surface ({@link foldSurfaceProjection}). */
surfaceTokens: number
/** {@link surfaceTokens} at the newest usage sample; absent until one lands. */
sampledSurfaceTokens?: number
/** Shadow price armed by the immediately preceding metering event. */
claim?: ShadowPriceClaim
}
/** Whether two optional shadow-price claims price the same range identically. */
const claimEquals = (left: ShadowPriceClaim | undefined, right: ShadowPriceClaim | undefined): boolean =>
left === right
|| (left !== undefined && right !== undefined
&& left.start === right.start && left.end === right.end && left.tokens === right.tokens)
/**
* Token-meter's session projection unit.
*
@@ -152,26 +157,33 @@ ProjectionDefinition<'tokenUsage', TokenUsageState> = {
* `pressureTokens` is prompt-side only, so it holds still while a turn streams
* and steps forward once the next request reports its usage. Because nothing
* but a request reports usage, it also cannot see a compaction: the fold
* therefore carries the priced surface alongside it and publishes
* therefore carries a running surface total alongside it and publishes
* `projectedTokens` — the sample plus the surface's signed movement since it
* was taken — so occupancy answers for the next request rather than the last
* one. A usage sample is stamped BEFORE the same event joins the surface, so
* an `assistant/message` anchors against the surface its own request saw.
* one. The total rides {@link foldSurfaceProjection}, so the state stays O(1)
* and a replacement shrinks it by its logged shadow price. A usage sample is
* stamped BEFORE the same event joins the surface, so an `assistant/message`
* anchors against the surface its own request saw.
*/
export const contextPressureProjectionDefinition:
ProjectionDefinition<'contextPressure', ContextPressureState> = {
key: 'contextPressure',
schema: pressureSchema,
init: () => ({ surface: [], surfaceTokens: 0 }),
init: () => ({ surfaceTokens: 0 }),
apply: (state, event) => {
const fold = foldSurfaceProjection(state.claim, event)
let next = state
if (event.type === 'request/context') {
const contextWindow = event.data.contextWindow
if (contextWindow === state.contextWindow) return state
if (contextWindow !== undefined) return { ...state, contextWindow }
const { contextWindow: _removed, ...withoutContextWindow } = state
return withoutContextWindow
if (contextWindow !== state.contextWindow) {
if (contextWindow !== undefined) {
next = { ...next, contextWindow }
} else {
const { contextWindow: _removed, ...withoutContextWindow } = next
next = withoutContextWindow
}
}
}
let next = state
const usage = usageOf(event)
if (usage !== undefined) {
const pressureTokens = pressureFrom(usage)
@@ -179,9 +191,12 @@ ProjectionDefinition<'contextPressure', ContextPressureState> = {
next = { ...next, pressureTokens, sampledSurfaceTokens: next.surfaceTokens }
}
}
if (!isSurfaceEvent(event)) return next
const fold = foldSurfaceTokens(next.surface, event)
return { ...next, surface: fold.nodes, surfaceTokens: next.surfaceTokens + fold.deltaTokens }
if (fold.deltaTokens !== 0) {
next = { ...next, surfaceTokens: next.surfaceTokens + fold.deltaTokens }
}
if (claimEquals(state.claim, fold.claim)) return next
const { claim: _expired, ...withoutClaim } = next
return fold.claim === undefined ? withoutClaim : { ...withoutClaim, claim: fold.claim }
},
view: ({ contextWindow, pressureTokens, surfaceTokens, sampledSurfaceTokens }) => ({
...contextWindow === undefined ? {} : { contextWindow },
@@ -190,5 +205,5 @@ ProjectionDefinition<'contextPressure', ContextPressureState> = {
? {}
: { projectedTokens: Math.max(0, pressureTokens + surfaceTokens - sampledSurfaceTokens) },
}),
stateVersion: 3,
stateVersion: 4,
}

View File

@@ -48,6 +48,26 @@ function appendUser(session: Session, text: string): number {
}), { surfaceOp: 'append' }).seq
}
/**
* Meter one upcoming replacement the way compact-basic does: price the
* replaced span from the measurement service's own nodes and log the
* shadow-price event directly before the replace.
*/
function appendSummaryMeter(ctx: Context, session: Session, start: number, end: number): void {
const nodes = ctx.tokenMeter.measure(session).nodes
const startIdx = nodes.findIndex(node => node.seq === start)
const endIdx = nodes.findIndex(node => node.seq === end)
const shadowed = nodes.slice(startIdx, endIdx + 1)
session.append('compact/summary', {
summary: [{ type: 'text', text: 'summary' }],
shadowedRange: { start, end },
shadowedSeqs: shadowed.map(node => node.seq),
shadowedTokenCount: shadowed.reduce((total, node) => total + node.tokens, 0),
provider: 'mock',
model: 'mock',
})
}
describe('contextBreakdown session projection', () => {
it('serves zeros for an empty log', async () => {
const { ctx, session } = await harness()
@@ -100,7 +120,7 @@ describe('contextBreakdown session projection', () => {
expect(projected(ctx, session).messageTokens).toBe(9)
})
it('shrinks the message figure when a replacement compacts the surface', async () => {
it('shrinks the message figure when a metered replacement compacts the surface', async () => {
const { ctx, session } = await harness()
const first = appendUser(session, 'before compaction, a longer message')
const second = appendUser(session, 'and a second entry')
@@ -108,6 +128,7 @@ describe('contextBreakdown session projection', () => {
content: [{ type: 'text', text: 'summary' }],
source: { kind: 'plugin', plugin: 'test' },
})
appendSummaryMeter(ctx, session, first, second)
session.append('user/message', summary, {
surfaceOp: { op: 'replace', start: first, end: second },
sourceEventSeqs: [first, second],
@@ -146,6 +167,9 @@ describe('contextBreakdown session projection', () => {
const grown = agree()
expect(grown).toBeGreaterThan(0)
appendSummaryMeter(ctx, session, question, answer)
// The armed shadow price must not move the published figure by itself.
expect(agree()).toBe(grown)
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'summary' }],
source: { kind: 'plugin', plugin: 'test' },
@@ -156,7 +180,7 @@ describe('contextBreakdown session projection', () => {
expect(agree()).toBeLessThan(grown)
})
it('fails loud on a replace range absent from the folded surface', () => {
it('fails loud on a replacement without an adjacent matching shadow price', () => {
const definition = contextBreakdownProjectionDefinition
const replace = (start: number, end: number): SessionEvent => ({
type: 'user/message',
@@ -173,12 +197,59 @@ describe('contextBreakdown session projection', () => {
data: createUserMessage({ content: [{ type: 'text', text: 'x' }], source: { kind: 'user' } }),
surfaceOp: 'append',
} as unknown as SessionEvent)
const meter = (start: number, end: number, seq: number): SessionEvent => ({
type: 'compact/prune',
seq,
time: 0,
data: { shadowedRange: { start, end }, shadowedSeqs: [start, end], shadowedTokenCount: 5 },
} as unknown as SessionEvent)
let state = definition.init()
state = definition.apply(state, append(1))
state = definition.apply(state, append(3))
expect(() => definition.apply(state, replace(7, 3))).toThrow('invalid current range')
expect(() => definition.apply(state, replace(1, 7))).toThrow('invalid current range')
expect(() => definition.apply(state, replace(3, 1))).toThrow('invalid current range')
// No metering event at all.
expect(() => definition.apply(state, replace(1, 3))).toThrow('no adjacent shadow price')
// A claim for a different range does not price this replacement.
const mismatched = definition.apply(state, meter(1, 1, 8))
expect(() => definition.apply(mismatched, replace(1, 3))).toThrow('no adjacent shadow price')
// A claim expires after one intervening event instead of lingering.
let expired = definition.apply(state, meter(1, 3, 8))
expired = definition.apply(expired, { type: 'todo/write', seq: 9, time: 0, data: { todos: [] } } as unknown as SessionEvent)
expect(() => definition.apply(expired, replace(1, 3))).toThrow('no adjacent shadow price')
// The armed claim prices exactly the next event's matching replacement.
const armed = definition.apply(state, meter(1, 3, 8))
expect(definition.view(definition.apply(armed, replace(1, 3))).messageTokens)
.toBe(definition.view(state).messageTokens - 5 + estimateMessage(
createUserMessage({ content: [{ type: 'text', text: 'x' }], source: { kind: 'user' } }),
))
})
it('keeps the persisted checkpoint O(1) as the surface grows and compacts', async () => {
const { ctx, session } = await harness()
const first = appendUser(session, 'the first of many messages')
for (let index = 0; index < 24; index += 1) appendUser(session, `message number ${index} with some text`)
const last = appendUser(session, 'the last message before compaction')
const stateKeys = (): string[] => {
const row = ctx.sessionProjections.checkpoint(session)['contextBreakdown']
if (row === undefined) throw new Error('contextBreakdown checkpoint row is missing')
return Object.keys(row.val as Record<string, unknown>).sort()
}
// Growth adds no per-node bookkeeping to the durable state.
expect(stateKeys()).toEqual(['messageTokens', 'systemTokens', 'toolsTokens'])
const shadowed = session.surface.nodes.slice(
session.surface.nodes.indexOf(first),
session.surface.nodes.indexOf(last) + 1,
)
appendSummaryMeter(ctx, session, first, last)
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'summary' }],
source: { kind: 'plugin', plugin: 'test' },
}), {
surfaceOp: { op: 'replace', start: first, end: last },
sourceEventSeqs: [...shadowed],
})
expect(stateKeys()).toEqual(['messageTokens', 'systemTokens', 'toolsTokens'])
expect(projected(ctx, session).messageTokens)
.toBe(ctx.tokenMeter.measure(session).surfaceTokens)
})
it('restores from a JSON checkpoint and unregisters with the token-meter fiber', async () => {

View File

@@ -70,6 +70,26 @@ const projected = (ctx: Context, session: Session): TokenUsageProjection => {
return value
}
/**
* Meter one upcoming replacement the way compact-basic does: price the
* replaced span from the measurement service's own nodes and log the
* shadow-price event directly before the replace.
*/
function appendSummaryMeter(ctx: Context, session: Session, start: number, end: number): void {
const nodes = ctx.tokenMeter.measure(session).nodes
const startIdx = nodes.findIndex(node => node.seq === start)
const endIdx = nodes.findIndex(node => node.seq === end)
const shadowed = nodes.slice(startIdx, endIdx + 1)
session.append('compact/summary', {
summary: [{ type: 'text', text: 'summary' }],
shadowedRange: { start, end },
shadowedSeqs: shadowed.map(node => node.seq),
shadowedTokenCount: shadowed.reduce((total, node) => total + node.tokens, 0),
provider: 'mock',
model: 'mock',
})
}
describe('tokenUsage session projection', () => {
it('serves zero buckets for an empty log', async () => {
const { ctx, session } = await harness()
@@ -184,6 +204,7 @@ describe('tokenUsage session projection', () => {
content: [{ type: 'text', text: 'before compaction' }],
source: { kind: 'user' },
}), { surfaceOp: 'append' })
appendSummaryMeter(ctx, session, before.seq, before.seq)
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'compacted' }],
source: { kind: 'plugin', plugin: 'test' },
@@ -351,7 +372,7 @@ describe('contextPressure session projection', () => {
const checkpoint = JSON.parse(JSON.stringify(
ctx.sessionProjections.checkpoint(session),
)) as ReturnType<typeof ctx.sessionProjections.checkpoint>
expect(checkpoint.contextPressure?.ver).toBe(3)
expect(checkpoint.contextPressure?.ver).toBe(4)
await meterFiber.dispose()
expect(ctx.sessionProjections.snapshot(session).values).not.toHaveProperty('contextPressure')
@@ -385,6 +406,7 @@ describe('contextPressure session projection', () => {
// Compaction reports no usage of its own, so `pressureTokens` cannot move;
// the projected figure must shrink anyway — the defect this field fixes.
appendSummaryMeter(ctx, session, question, grown)
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'summary' }],
source: { kind: 'plugin', plugin: 'test' },
@@ -406,6 +428,7 @@ describe('contextPressure session projection', () => {
// shadowing that span subtracts more than the sample holds.
appendAssistant(session, 'ok', { inputTokens: 3, outputTokens: 1 }, 1, 1)
session.append('step/end', { turn: 1, step: 1 })
appendSummaryMeter(ctx, session, question, question)
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: '.' }],
source: { kind: 'plugin', plugin: 'test' },

View File

@@ -23,6 +23,9 @@
{
"path": "../../core/session"
},
{
"path": "../../compact/compact"
},
{
"path": "../../session-projection/session-projection"
},

9
pnpm-lock.yaml generated
View File

@@ -2363,6 +2363,9 @@ importers:
'@cordisjs/plugin-loader':
specifier: workspace:^
version: link:../../../vendor/loader
'@deepseek-ai/dsh-compact':
specifier: workspace:^
version: link:../compact
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
version: link:../../support/invariants
@@ -2372,6 +2375,9 @@ importers:
'@deepseek-ai/dsh-session':
specifier: workspace:^
version: link:../../core/session
'@deepseek-ai/dsh-token-meter':
specifier: workspace:^
version: link:../../llm/token-meter
cordis:
specifier: ^4.0.0-rc.7
version: link:../../../vendor/cordis
@@ -3857,6 +3863,9 @@ importers:
specifier: ^4.4.3
version: 4.4.3
devDependencies:
'@deepseek-ai/dsh-compact':
specifier: workspace:^
version: link:../../compact/compact
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
version: link:../../support/invariants