瀏覽代碼

fix(token-meter): distinguish compact caches from parent scalar version

The parent representation fix now correctly invalidates header-based caches with scalar state version3. The dependent in-history projection has a different compact live-surface schema and must not reuse that version. Advance it to4 and prove that a lower-layer version3 cache is discarded and refolded, alongside the existing version2 regression. No historical Session migration or compatibility mechanism is added.

Addresses ds-review-bot cache-version thread3921300994 across the stack. Validation:62 focused projection, adapter and admission tests passed; context.ts and breakdown-projection.ts each have100% exact-source coverage; paired README record updated.
Tianyi Cui 1 月之前
父節點
當前提交
4574602c07

+ 2 - 2
packages/llm/token-meter/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/llm/token-meter/README.md
-README.md: afbed6783264d4a1af5158f1481dd29e9c301c66
-README.zh.md: 4f09440425b8fcd1a3e9b91f6cd01ff7a6c175c4
+README.md: 102293de091e07f6fe498c65b8d962c38c8b5383
+README.zh.md: 38a3e0e6639ba637011e0de492ce5d579ac5bad1

+ 1 - 1
packages/llm/token-meter/README.md

@@ -96,7 +96,7 @@ Each `measure()` call synchronizes the fold to the current durable tail, then re
 
 ### Projection semantics
 
-`contextBreakdown` retains plain-JSON `{ seq, heuristicTokens, system }` entries in surface order and reuses the measurement plan/commit fold. Its state and surface transitions are O(current retained surface), not O(1) and not O(total historical log); replaced entries and message bodies are not retained. State version 3 invalidates scalar checkpoints and replays the log. `contextPressure` remains the scalar shadow-price consumer: replacements without adjacent claims contribute zero delta. The usage fold retains one last-sample slot because legal logs never report usage for an earlier step after a later step reports usage.
+`contextBreakdown` retains plain-JSON `{ seq, heuristicTokens, system }` entries in surface order and reuses the measurement plan/commit fold. Its state and surface transitions are O(current retained surface), not O(1) and not O(total historical log); replaced entries and message bodies are not retained. State version 4 invalidates scalar checkpoints and replays the log. `contextPressure` remains the scalar shadow-price consumer: replacements without adjacent claims contribute zero delta. The usage fold retains one last-sample slot because legal logs never report usage for an earlier step after a later step reports usage.
 
 </details>
 

+ 1 - 1
packages/llm/token-meter/README.zh.md

@@ -98,7 +98,7 @@ const price = ctx.tokenMeter.estimateMessage(message)
 
 ### 投影语义
 
-`contextBreakdown` 按 surface 顺序保留纯 JSON 的 `{ seq, heuristicTokens, system }` 条目,并复用测量服务的 plan/commit fold。其状态与 surface 转换成本为 O(当前保留 surface),不是 O(1),也不是 O(完整历史日志);被替换条目和消息正文不保留。状态版本 3 使标量检查点失效并重放日志。`contextPressure` 仍是标量影子价消费方:没有相邻 claim 的替换贡献零增量。用量 fold 保留一个最后样本槽,因为合法日志不会在更晚步骤报告用量后再次报告更早步骤的用量。
+`contextBreakdown` 按 surface 顺序保留纯 JSON 的 `{ seq, heuristicTokens, system }` 条目,并复用测量服务的 plan/commit fold。其状态与 surface 转换成本为 O(当前保留 surface),不是 O(1),也不是 O(完整历史日志);被替换条目和消息正文不保留。状态版本 4 使标量检查点失效并重放日志。`contextPressure` 仍是标量影子价消费方:没有相邻 claim 的替换贡献零增量。用量 fold 保留一个最后样本槽,因为合法日志不会在更晚步骤报告用量后再次报告更早步骤的用量。
 
 </details>
 

+ 1 - 1
packages/llm/token-meter/src/breakdown-projection.ts

@@ -47,7 +47,7 @@ type ContextBreakdownState = z.infer<typeof contextBreakdownStateSchema>
  */
 export const contextBreakdownProjectionDefinition = {
   key: 'contextBreakdown',
-  stateVersion: 3,
+  stateVersion: 4,
   stateSchema: contextBreakdownStateSchema,
   init: (): ContextBreakdownState => ({
     nodes: [],

+ 5 - 7
packages/llm/token-meter/tests/context-breakdown-projection.spec.ts

@@ -405,7 +405,7 @@ describe('contextBreakdown session projection', () => {
       ctx.sessionProjections.checkpoint(session),
     )) as ReturnType<typeof ctx.sessionProjections.checkpoint>
     const row = checkpoint['contextBreakdown']!
-    expect(row.ver).toBe(3)
+    expect(row.ver).toBe(4)
     expect(ctx.sessionProjections.viewCheckpoint(checkpoint).contextBreakdown).toEqual(projected(ctx, session))
     const replacement = session.append('user/message', createUserMessage({
       content: [{ type: 'text', text: 'summary' }], source: { kind: 'user' },
@@ -424,7 +424,7 @@ describe('contextBreakdown session projection', () => {
       stale, session.snapshotEvents(), SessionLogOffset(0), session.header, session.inheritedEventCount,
     )
     expect(replayed.snapshot.values.contextBreakdown).toEqual(projected(ctx, session))
-    expect(replayed.checkpoint['contextBreakdown']?.ver).toBe(3)
+    expect(replayed.checkpoint['contextBreakdown']?.ver).toBe(4)
     const invalid = {
       ...checkpoint,
       contextBreakdown: {
@@ -437,19 +437,17 @@ describe('contextBreakdown session projection', () => {
     )).toThrow()
   })
 
-  it('discards version-2 cache values and refolds system messages from the full log', async () => {
+  it('discards lower-layer version-3 scalar caches and refolds the full surface', async () => {
     const { ctx, session } = await harness()
     try {
       appendSystem(session, 'You are terse.')
       session.append('request/header', { header: { config: CONFIG, tools: TOOLS }, reason: 'initial' })
       appendUser(session, 'abcd')
       const current = ctx.sessionProjections.checkpoint(session)
-      // The old fold's fields still validate, but its header-based system price is not reusable.
       const staleValue = { systemTokens: 0, toolsTokens: estimateToolsTokens({ config: CONFIG, tools: TOOLS }), messageTokens: 17 }
-      expect(contextBreakdownProjectionDefinition.stateSchema.parse(staleValue)).toEqual(staleValue)
       const checkpoint = {
         ...current,
-        contextBreakdown: { ver: 2, seq: SessionSeq(session.seq - 1), val: staleValue },
+        contextBreakdown: { ver: 3, seq: SessionSeq(session.seq - 1), val: staleValue },
       }
       expect.soft(ctx.sessionProjections.viewCheckpoint(checkpoint)).not.toHaveProperty('contextBreakdown')
       expect.soft(ctx.sessionProjections.restoreFloor(checkpoint)).toBe(0)
@@ -460,7 +458,7 @@ describe('contextBreakdown session projection', () => {
         systemTokens: 8, toolsTokens: staleValue.toolsTokens, messageTokens: 9,
       })
       expect(restored.checkpoint).toEqual(current)
-      expect(restored.checkpoint['contextBreakdown']?.ver).toBe(3)
+      expect(restored.checkpoint['contextBreakdown']?.ver).toBe(4)
     } finally {
       await ctx.fiber.dispose()
     }