fix(session): simplify reference byte limits
This commit is contained in:
@@ -18,12 +18,11 @@ The context source is `{ kind: 'plugin', plugin: 'session-reference' }`. Its met
|
||||
|
||||
| Key | Default | Contract |
|
||||
|---|---:|---|
|
||||
| `maxReferences` | `3` | Maximum distinct source sessions in one prepared message. |
|
||||
| `maxReferences` | `3` | Maximum distinct source sessions in one prepared message; must be at most `3`. |
|
||||
| `candidateLimit` | `50` | Default metadata candidate count returned to a host. |
|
||||
| `maxReferenceBytes` | `65536` | Maximum serialized JSON bytes for one reference object. |
|
||||
| `maxTotalBytes` | `196608` | Maximum complete prompt bytes, including fixed warning and tags. |
|
||||
|
||||
Retention keeps compact checkpoints and the newest message before dropping older non-checkpoint units. Oversized retained text uses `dsh-retention` head/tail truncation with an exact UTF-8 omission notice. The total budget is applied to the complete rendered prompt, including escaped JSON and fixed warning text; a snapshot whose fixed data cannot fit fails with `SESSION_REFERENCE_BUDGET_EXCEEDED`.
|
||||
Retention applies `maxReferenceBytes` independently to each source, keeps compact checkpoints and the newest message before dropping older non-checkpoint units, and uses `dsh-retention` head/tail truncation with an exact UTF-8 omission notice. If one source's fixed serialized fields cannot fit, preparation fails with `SESSION_REFERENCE_BUDGET_EXCEEDED` instead of returning a partial context.
|
||||
|
||||
## Model Experience
|
||||
|
||||
@@ -35,7 +34,7 @@ The model sees the current message's readable `@label` plus one same-level user-
|
||||
|
||||
#### Token effect
|
||||
|
||||
Each referenced message adds the fixed warning plus the retained serialized snapshots, bounded by `maxReferenceBytes` and `maxTotalBytes`. The exact snapshot remains in target history until target compaction shadows or summarizes it; source-session changes add no further tokens.
|
||||
Each referenced message adds the fixed warning plus up to three serialized snapshots, each independently bounded by `maxReferenceBytes`. The exact snapshot remains in target history until target compaction shadows or summarizes it; source-session changes add no further tokens.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
/** Configuration and stable diagnostics for session references. */
|
||||
|
||||
/** Default maximum references accepted by one message. */
|
||||
export const DEFAULT_MAX_REFERENCES = 3
|
||||
/** Hard maximum references accepted by one message. */
|
||||
export const MAX_REFERENCES = 3
|
||||
/** Default number of discovery candidates returned to a host. */
|
||||
export const DEFAULT_CANDIDATE_LIMIT = 50
|
||||
/** Default UTF-8 budget for one rendered reference JSON object. */
|
||||
export const DEFAULT_MAX_REFERENCE_BYTES = 65_536
|
||||
/** Default UTF-8 budget for the complete injected reference prompt. */
|
||||
export const DEFAULT_MAX_TOTAL_BYTES = 196_608
|
||||
|
||||
/** Session-reference service configuration. */
|
||||
export interface Config {
|
||||
/** Maximum distinct source sessions referenced by one message. */
|
||||
/** Maximum distinct source sessions referenced by one message, from one to three. */
|
||||
maxReferences?: number
|
||||
/** Default host candidate-list limit. */
|
||||
candidateLimit?: number
|
||||
/** Maximum rendered UTF-8 bytes for one source snapshot. */
|
||||
maxReferenceBytes?: number
|
||||
/** Maximum rendered UTF-8 bytes for the complete injected prompt. */
|
||||
maxTotalBytes?: number
|
||||
}
|
||||
|
||||
/** Stable failure codes exposed to host adapters. */
|
||||
|
||||
@@ -13,9 +13,8 @@ import type { JsonValue, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionSurfaceSnapshot } from '@deepseek-ai/dsh-session-query'
|
||||
import {
|
||||
DEFAULT_CANDIDATE_LIMIT,
|
||||
DEFAULT_MAX_REFERENCES,
|
||||
DEFAULT_MAX_REFERENCE_BYTES,
|
||||
DEFAULT_MAX_TOTAL_BYTES,
|
||||
MAX_REFERENCES,
|
||||
SessionReferenceError,
|
||||
type Config,
|
||||
} from './config.ts'
|
||||
@@ -27,9 +26,8 @@ export type * from './types.ts'
|
||||
export type { Config, SessionReferenceErrorCode } from './config.ts'
|
||||
export {
|
||||
DEFAULT_CANDIDATE_LIMIT,
|
||||
DEFAULT_MAX_REFERENCES,
|
||||
DEFAULT_MAX_REFERENCE_BYTES,
|
||||
DEFAULT_MAX_TOTAL_BYTES,
|
||||
MAX_REFERENCES,
|
||||
SessionReferenceError,
|
||||
} from './config.ts'
|
||||
export {
|
||||
@@ -71,10 +69,9 @@ interface RenderedSource {
|
||||
export class SessionReferenceService extends Service {
|
||||
static inject = ['sessionQuery']
|
||||
static Config: z<Config> = z.object({
|
||||
maxReferences: z.number().step(1).min(1).default(DEFAULT_MAX_REFERENCES),
|
||||
maxReferences: z.number().step(1).min(1).max(MAX_REFERENCES).default(MAX_REFERENCES),
|
||||
candidateLimit: z.number().step(1).min(1).default(DEFAULT_CANDIDATE_LIMIT),
|
||||
maxReferenceBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REFERENCE_BYTES),
|
||||
maxTotalBytes: z.number().step(1).min(1).default(DEFAULT_MAX_TOTAL_BYTES),
|
||||
})
|
||||
|
||||
private readonly config: Required<Config>
|
||||
@@ -82,10 +79,9 @@ export class SessionReferenceService extends Service {
|
||||
constructor(ctx: Context, config: Config = {}) {
|
||||
super(ctx, 'sessionReferences')
|
||||
this.config = {
|
||||
maxReferences: config.maxReferences ?? DEFAULT_MAX_REFERENCES,
|
||||
maxReferences: config.maxReferences ?? MAX_REFERENCES,
|
||||
candidateLimit: config.candidateLimit ?? DEFAULT_CANDIDATE_LIMIT,
|
||||
maxReferenceBytes: config.maxReferenceBytes ?? DEFAULT_MAX_REFERENCE_BYTES,
|
||||
maxTotalBytes: config.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES,
|
||||
}
|
||||
for (const [name, value] of Object.entries(this.config)) {
|
||||
if (!Number.isSafeInteger(value) || value <= 0) {
|
||||
@@ -95,6 +91,12 @@ export class SessionReferenceService extends Service {
|
||||
)
|
||||
}
|
||||
}
|
||||
if (this.config.maxReferences > MAX_REFERENCES) {
|
||||
throw new SessionReferenceError(
|
||||
`session-reference: maxReferences must not exceed ${MAX_REFERENCES}`,
|
||||
'SESSION_REFERENCE_INVALID_CONFIG',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,7 +175,7 @@ export class SessionReferenceService extends Service {
|
||||
}
|
||||
assertNotCancelled(signal)
|
||||
|
||||
const rendered = this.fitTotalBudget(prepared)
|
||||
const rendered = this.renderSources(prepared)
|
||||
const prompt = renderPrompt(rendered.map(source => source.data))
|
||||
const meta = {
|
||||
kind: 'session-reference',
|
||||
@@ -194,32 +196,19 @@ export class SessionReferenceService extends Service {
|
||||
return { content: acceptedContent, contexts: [context] }
|
||||
}
|
||||
|
||||
private fitTotalBudget(sources: readonly PreparedSource[]): RenderedSource[] {
|
||||
let low = 1
|
||||
let high = this.config.maxReferenceBytes
|
||||
let best: RenderedSource[] | undefined
|
||||
while (low <= high) {
|
||||
const cap = Math.floor((low + high) / 2)
|
||||
const candidate = sources.map(source => retainReferencedSession(source.snapshot, source.input.label, cap))
|
||||
if (candidate.some(source => source === undefined)) {
|
||||
low = cap + 1
|
||||
continue
|
||||
}
|
||||
const rendered = candidate as RenderedSource[]
|
||||
if (Buffer.byteLength(renderPrompt(rendered.map(source => source.data)), 'utf8') <= this.config.maxTotalBytes) {
|
||||
best = rendered
|
||||
low = cap + 1
|
||||
} else {
|
||||
high = cap - 1
|
||||
private renderSources(sources: readonly PreparedSource[]): RenderedSource[] {
|
||||
const rendered: RenderedSource[] = []
|
||||
for (const source of sources) {
|
||||
const retained = retainReferencedSession(source.snapshot, source.input.label, this.config.maxReferenceBytes)
|
||||
if (retained === undefined) {
|
||||
throw new SessionReferenceError(
|
||||
'referenced session snapshot cannot fit the configured byte budget',
|
||||
'SESSION_REFERENCE_BUDGET_EXCEEDED',
|
||||
)
|
||||
}
|
||||
rendered.push(retained)
|
||||
}
|
||||
if (best === undefined) {
|
||||
throw new SessionReferenceError(
|
||||
'referenced session snapshot cannot fit the configured byte budgets',
|
||||
'SESSION_REFERENCE_BUDGET_EXCEEDED',
|
||||
)
|
||||
}
|
||||
return best
|
||||
return rendered
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -358,8 +358,8 @@ describe('session reference discovery and preparation', () => {
|
||||
.rejects.toThrow(expectCode('SESSION_REFERENCE_CANCELLED'))
|
||||
})
|
||||
|
||||
it('retains compact checkpoints and latest messages within exact UTF-8 budgets', async () => {
|
||||
const ctx = await harness({ maxReferenceBytes: 360, maxTotalBytes: 650 })
|
||||
it('retains compact checkpoints and latest messages within an exact per-reference UTF-8 budget', async () => {
|
||||
const ctx = await harness({ maxReferenceBytes: 360 })
|
||||
const target = ctx.sessions.create(SessionId('target'))
|
||||
const source = ctx.sessions.create(SessionId('source'))
|
||||
appendConversation(source)
|
||||
@@ -377,7 +377,6 @@ describe('session reference discovery and preparation', () => {
|
||||
const prepared = await ctx.sessionReferences.prepare(fakeAgent(target), [{ type: 'text', text: 'go' }], [{ sessionId: source.id }])
|
||||
const context = prepared.contexts[0]
|
||||
if (context?.content[0]?.type !== 'text') throw new Error('expected text context')
|
||||
expect(Buffer.byteLength(context.content[0].text, 'utf8')).toBeLessThanOrEqual(650)
|
||||
const data = promptData(context.content[0].text) as unknown[]
|
||||
expect(Buffer.byteLength(stringifyTagSafeJson(data[0]), 'utf8')).toBeLessThanOrEqual(360)
|
||||
expect(context.content[0].text).toContain('checkpoint')
|
||||
@@ -386,8 +385,41 @@ describe('session reference discovery and preparation', () => {
|
||||
expect(context.meta).toMatchObject({ references: [{ truncated: true, compacted: true }] })
|
||||
})
|
||||
|
||||
it('applies the full byte limit independently to each of three references', async () => {
|
||||
const maxReferenceBytes = 360
|
||||
const ctx = await harness({ maxReferenceBytes })
|
||||
const target = ctx.sessions.create(SessionId('target'))
|
||||
const sources = ['one', 'two', 'three'].map((id) => {
|
||||
const source = ctx.sessions.create(SessionId(id))
|
||||
source.append(
|
||||
'user/message',
|
||||
{ content: [{ type: 'text', text: `${id}-${'界'.repeat(400)}` }], source: COMPACT_CHECKPOINT_SOURCE },
|
||||
{ surfaceOp: 'append' },
|
||||
)
|
||||
source.append(
|
||||
'user/message',
|
||||
{ content: [{ type: 'text', text: `${id}-tail` }], source: { kind: 'user' } },
|
||||
{ surfaceOp: 'append' },
|
||||
)
|
||||
return source
|
||||
})
|
||||
|
||||
const prepared = await ctx.sessionReferences.prepare(
|
||||
fakeAgent(target),
|
||||
[{ type: 'text', text: 'go' }],
|
||||
sources.map(source => ({ sessionId: source.id })),
|
||||
)
|
||||
const context = prepared.contexts[0]
|
||||
if (context?.content[0]?.type !== 'text') throw new Error('expected text context')
|
||||
const data = promptData(context.content[0].text) as unknown[]
|
||||
const sizes = data.map(source => Buffer.byteLength(stringifyTagSafeJson(source), 'utf8'))
|
||||
expect(sizes).toHaveLength(3)
|
||||
expect(sizes.every(size => size <= maxReferenceBytes)).toBe(true)
|
||||
expect(sizes.reduce((sum, size) => sum + size, 0)).toBeGreaterThan(maxReferenceBytes * 2)
|
||||
})
|
||||
|
||||
it('fails without producing a partial context when fixed prompt data cannot fit', async () => {
|
||||
const ctx = await harness({ maxReferenceBytes: 16, maxTotalBytes: 32 })
|
||||
const ctx = await harness({ maxReferenceBytes: 16 })
|
||||
const target = ctx.sessions.create(SessionId('target'))
|
||||
const source = ctx.sessions.create(SessionId('source'))
|
||||
await expect(ctx.sessionReferences.prepare(fakeAgent(target), [{ type: 'text', text: 'go' }], [{ sessionId: source.id }]))
|
||||
@@ -454,6 +486,12 @@ describe('session reference discovery and preparation', () => {
|
||||
expect(() => new SessionReferenceService(ctx, { maxReferences: 0 }))
|
||||
.toThrow(expectCode('SESSION_REFERENCE_INVALID_CONFIG'))
|
||||
|
||||
const oversizedCtx = new Context()
|
||||
await oversizedCtx.plugin(SessionStore)
|
||||
await oversizedCtx.plugin(SessionQueryService)
|
||||
expect(() => new SessionReferenceService(oversizedCtx, { maxReferences: 4 }))
|
||||
.toThrow(expectCode('SESSION_REFERENCE_INVALID_CONFIG'))
|
||||
|
||||
const defaultCtx = new Context()
|
||||
await defaultCtx.plugin(SessionStore)
|
||||
await defaultCtx.plugin(SessionQueryService)
|
||||
|
||||
Reference in New Issue
Block a user