dsh-context-compression-improved 0.2.1 → 0.3.0

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.
Files changed (80) hide show
  1. package/.gitattributes +1 -1
  2. package/.github/workflows/ci.yml +1 -7
  3. package/CHANGELOG.ja.md +18 -21
  4. package/CHANGELOG.ko.md +17 -21
  5. package/CHANGELOG.md +16 -17
  6. package/CHANGELOG.zh.md +15 -18
  7. package/CONTRIBUTING.md +1 -1
  8. package/README.ja.md +1 -20
  9. package/README.ko.md +12 -30
  10. package/README.md +1 -19
  11. package/README.zh.md +5 -21
  12. package/THIRD_PARTY_NOTICES.md +2 -2
  13. package/docs/compat-0.1.5-report.md +77 -0
  14. package/docs/installation.ja.md +6 -4
  15. package/docs/installation.ko.md +9 -7
  16. package/docs/installation.md +3 -1
  17. package/docs/installation.zh.md +3 -2
  18. package/docs/repair-log.md +54 -33
  19. package/eslint.config.js +2 -2
  20. package/package.json +56 -36
  21. package/packages/selector/README.md +7 -12
  22. package/packages/selector/README.zh.md +6 -13
  23. package/packages/selector/THIRD_PARTY_NOTICES.md +2 -8
  24. package/packages/selector/cordis.patch.yml +7 -5
  25. package/packages/selector/dsh.plugin.json +2 -2
  26. package/packages/selector/lib/client.d.ts +31 -5
  27. package/packages/selector/lib/client.js +100 -82
  28. package/packages/selector/lib/index.d.ts +8 -20
  29. package/packages/selector/lib/index.js +9 -14
  30. package/packages/selector/lib/invariant.js +4 -4
  31. package/packages/selector/lib/pruner.d.ts +13 -4
  32. package/packages/selector/lib/pruner.js +91 -46
  33. package/packages/selector/lib/tail-trim.js +13 -4
  34. package/packages/selector/package.json +23 -23
  35. package/packages/selector/screenshots.json +1 -4
  36. package/packages/selector/src/client/CompressionProfileControls.tsx +5 -8
  37. package/packages/selector/src/client/CompressionProfileSelector.tsx +9 -5
  38. package/packages/selector/src/client/EstimatorControls.tsx +28 -32
  39. package/packages/selector/src/client/index.ts +101 -63
  40. package/packages/selector/src/client/locales.ts +0 -2
  41. package/packages/selector/src/client/preset-options.ts +49 -35
  42. package/packages/selector/src/client/settings-section.tsx +4 -6
  43. package/packages/selector/src/deepseek-v4-tokenizer.ts +0 -5
  44. package/packages/selector/src/index.ts +41 -46
  45. package/packages/selector/src/invariant.ts +5 -5
  46. package/packages/selector/src/pruner.ts +67 -36
  47. package/packages/selector/src/runtime/measurement.ts +6 -2
  48. package/packages/selector/src/runtime/session-events.ts +14 -1
  49. package/packages/selector/src/runtime/tail-trim.ts +4 -4
  50. package/packages/selector/src/runtime/tokenpilot/estimator.ts +34 -3
  51. package/packages/selector/src/runtime/types.ts +1 -1
  52. package/packages/selector/tests/auto-compact.client.spec.tsx +0 -13
  53. package/packages/selector/tests/built/client-artifact.spec.ts +5 -1
  54. package/packages/selector/tests/code-skeleton.client.spec.ts +2 -1
  55. package/packages/selector/tests/custom-contract.client.spec.ts +2 -1
  56. package/packages/selector/tests/estimator-channel.client.spec.tsx +16 -11
  57. package/packages/selector/tests/estimator-route-registration.host.spec.ts +12 -0
  58. package/packages/selector/tests/host-preset-overlay.host.spec.ts +6 -4
  59. package/packages/selector/tests/preset-options-write.client.spec.ts +87 -79
  60. package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +17 -12
  61. package/packages/selector/tests/preset-overlay.host.spec.ts +5 -2
  62. package/packages/selector/tests/profiles.client.spec.tsx +3 -3
  63. package/packages/selector/tests/public/package-contract.client.spec.ts +27 -3
  64. package/packages/selector/tests/runtime/public/public-runtime.spec.ts +103 -81
  65. package/packages/selector/tests/runtime/session-events.spec.ts +1 -1
  66. package/packages/selector/tests/settings-seat.client.spec.ts +86 -0
  67. package/packages/selector/tests/standing-generation.host.spec.ts +88 -71
  68. package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +24 -21
  69. package/pnpm-workspace.yaml +52 -5
  70. package/scripts/capture-profile-baseline.mjs +42 -0
  71. package/scripts/generate-tokenizer-fixtures.py +5 -5
  72. package/scripts/generate-vision-fixtures.py +2 -2
  73. package/scripts/{packed-components-smoke.ts → packed-components-smoke.mjs} +116 -119
  74. package/scripts/{packed-install-e2e.ts → packed-install-e2e.mjs} +188 -160
  75. package/scripts/{verify-release.ts → verify-release.mjs} +70 -94
  76. package/tests/TEST_INVENTORY.md +6 -7
  77. package/scripts/capture-profile-baseline.ts +0 -80
  78. package/tsconfig.scripts.json +0 -13
  79. /package/{packages/selector/assets/screenshots → docs/assets}/context-compression-selector-profiles.jpg +0 -0
  80. /package/{packages/selector/assets/screenshots → docs/assets}/context-compression-selector-settings.png +0 -0
@@ -1,7 +1,7 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest'
2
2
  import { Context } from '@deepseek-ai/cordis'
3
3
  import {
4
- CallId,
4
+ ToolCallId as CallId,
5
5
  createMessage,
6
6
  createUserMessage,
7
7
  createToolResultMessage,
@@ -18,25 +18,26 @@ import BasicCompactionEngine from '@deepseek-ai/dsh-compaction-basic'
18
18
  import SessionStore, {
19
19
  Session,
20
20
  SessionId,
21
+ SessionSeq,
21
22
  canonicalHeader,
22
23
  } from '@deepseek-ai/dsh-session'
23
24
  import {
24
25
  agentEvents,
25
- Inbox,
26
26
  type Agent,
27
27
  } from '@deepseek-ai/dsh-agent'
28
28
  import {
29
29
  SettingsProvider,
30
- settingsNamespace,
31
30
  type SettingsNamespace,
32
31
  } from '@deepseek-ai/dsh-settings'
33
32
  import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
33
+ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
34
34
  import TokenMeter from '@deepseek-ai/dsh-token-meter'
35
35
  import ToolRuntime from '@deepseek-ai/dsh-tools'
36
36
  import { CompactionId } from '@deepseek-ai/dsh-compaction'
37
37
  import InvariantRegistry from '@deepseek-ai/dsh-invariants'
38
38
  import * as SelectorHost from '../../../src/index.ts'
39
39
  import * as RuntimeInvariant from '../../../src/invariant.ts'
40
+ import { sessionEvents } from '../../../src/runtime/session-events.ts'
40
41
  import ToolResultPruner, {
41
42
  CONTEXT_COMPRESSION_SETTINGS_NAMESPACE,
42
43
  DEFAULT_CUSTOM_COMPRESSION_POLICY,
@@ -130,10 +131,11 @@ class NativeSummaryAdapter extends LlmAdapter {
130
131
  async function runtimeContext(): Promise<Context> {
131
132
  const ctx = new Context()
132
133
  activeContexts.push(ctx)
133
- await ctx.plugin(SessionStore)
134
- await ctx.plugin(SystemPrompt)
135
- await ctx.plugin(ToolRuntime)
136
- await ctx.plugin(TokenMeter)
134
+ await ctx.plugin(SessionStore).await()
135
+ await ctx.plugin(SystemPrompt).await()
136
+ await ctx.plugin(ToolRuntime).await()
137
+ await ctx.plugin(SessionProjectionRegistry).await()
138
+ await ctx.plugin(TokenMeter).await()
137
139
  return ctx
138
140
  }
139
141
 
@@ -180,6 +182,7 @@ function appendToolTurn(
180
182
  }
181
183
  session.append('step/start', { turn, step: 1 })
182
184
  const assistant = session.append('assistant/message', {
185
+ stream: [],
183
186
  turn,
184
187
  step: 1,
185
188
  message: createMessage({
@@ -229,6 +232,7 @@ function appendToolBatchTurn(
229
232
  }
230
233
  session.append('step/start', { turn, step: 1 })
231
234
  const assistant = session.append('assistant/message', {
235
+ stream: [],
232
236
  turn,
233
237
  step: 1,
234
238
  message: createMessage({
@@ -267,12 +271,18 @@ function appendToolBatchTurn(
267
271
  return { assistantSeq: assistant.seq, resultSeqs }
268
272
  }
269
273
 
274
+ // 0.1.5 removed the settingsNamespace() wrapper; namespaces are branded
275
+ // strings validated at runtime by SettingsProvider.parse.
276
+ const nsBrand = (value: string): SettingsNamespace => value as unknown as SettingsNamespace
277
+
270
278
  function stubAgent(ctx: Context, session: Session): Agent {
271
279
  return {
272
280
  id: session.id,
273
281
  options: {},
274
282
  session,
275
- inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
283
+ // 0.1.5 moved the durable Inbox behind the loop driver; the pruner only
284
+ // reads id/session, so an inert face is enough for the stub.
285
+ inbox: undefined as unknown as Agent['inbox'],
276
286
  status: 'idle',
277
287
  ctx,
278
288
  send: () => {},
@@ -350,16 +360,16 @@ describe('standalone runtime on published Harness APIs', () => {
350
360
  const result = pruner.pruneSession(session, { stage: 'pressure' })
351
361
 
352
362
  expect(result.pruned).toHaveLength(1)
353
- const manifest = session.events.at(-2)
354
- const replacement = session.events.at(-1)
363
+ const manifest = sessionEvents(session).at(-2)
364
+ const replacement = sessionEvents(session).at(-1)
355
365
  expect(manifest?.type).toBe('compaction/prune')
356
- expect(session.events.some(event => event.type === ('compaction/group-trim' as string))).toBe(false)
366
+ expect(sessionEvents(session).some(event => event.type === ('compaction/group-trim' as string))).toBe(false)
357
367
  expect(replacement?.type).toBe('tool/result')
358
368
  if (replacement?.type !== 'tool/result') throw new Error('Native prune did not append a tool result replacement')
359
369
  expect(replacement.surfaceOp).toEqual({
360
370
  op: 'replace',
361
- start: source.resultSeq,
362
- end: source.resultSeq,
371
+ startSeq: source.resultSeq,
372
+ endSeq: source.resultSeq,
363
373
  })
364
374
  expect(replacement.sourceEventSeqs).toContain(source.resultSeq)
365
375
  expect(rewrites(audit.records())).toContainEqual(expect.objectContaining({
@@ -521,7 +531,7 @@ describe('standalone runtime on published Harness APIs', () => {
521
531
  await ctx.plugin(TestSettings).await()
522
532
  await ctx.plugin(SelectorHost).await()
523
533
  const audit = captureAudit(ctx)
524
- const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
534
+ const namespace = nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
525
535
 
526
536
  // A high threshold delays the micro-compact last-chance gate past the old
527
537
  // fixed 0.7 ratio: pressure between 0.7*C and D must NOT age history.
@@ -557,7 +567,7 @@ describe('standalone runtime on published Harness APIs', () => {
557
567
  await ctx.plugin(TestSettings).await()
558
568
  await ctx.plugin(SelectorHost).await()
559
569
  const audit = captureAudit(ctx)
560
- const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
570
+ const namespace = nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
561
571
 
562
572
  // A low threshold pulls the micro-compact deadline below the old fixed
563
573
  // 0.7 ratio: pressure between D and 0.7*C must age history now.
@@ -591,7 +601,7 @@ describe('standalone runtime on published Harness APIs', () => {
591
601
  await ctx.plugin(TestSettings).await()
592
602
  await ctx.plugin(SelectorHost).await()
593
603
  const audit = captureAudit(ctx)
594
- const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
604
+ const namespace = nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
595
605
  // The preset overlay captured 70% into this generation's deployment
596
606
  // config; the user then moved the global setting to 90 before the first
597
607
  // prune. Auto Compact and micro compact must both stay on 70%.
@@ -627,7 +637,7 @@ describe('standalone runtime on published Harness APIs', () => {
627
637
  await ctx.plugin(TestSettings).await()
628
638
  await ctx.plugin(SelectorHost).await()
629
639
  const audit = captureAudit(ctx)
630
- const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
640
+ const namespace = nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
631
641
  // A hand-edited store can surface a document the schema rejects (unknown
632
642
  // top-level key). The runtime must not fall back to a lossy-capable
633
643
  // profile: the session freezes effectively off and keeps every original
@@ -677,7 +687,7 @@ describe('standalone runtime on published Harness APIs', () => {
677
687
  await ctx.plugin(TestSettings).await()
678
688
  await ctx.plugin(SelectorHost).await()
679
689
  const audit = captureAudit(ctx)
680
- const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
690
+ const namespace = nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
681
691
  // Schemastery `.default(...)` would silently replace these present-but-null
682
692
  // sections with the balanced/default-v3 policy; the runtime must reject the
683
693
  // document and freeze the session losslessly instead.
@@ -714,7 +724,7 @@ describe('standalone runtime on published Harness APIs', () => {
714
724
  await ctx.plugin(TestSettings).await()
715
725
  await ctx.plugin(SelectorHost).await()
716
726
  const audit = captureAudit(ctx)
717
- const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
727
+ const namespace = nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
718
728
  const malformed = {
719
729
  profile: 'balanced' as const,
720
730
  custom: structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY),
@@ -814,7 +824,7 @@ describe('standalone runtime on published Harness APIs', () => {
814
824
  await ctx.plugin(TestSettings).await()
815
825
  await ctx.plugin(SelectorHost).await()
816
826
  const audit = captureAudit(ctx)
817
- const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
827
+ const namespace = nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
818
828
  await ctx.settings.update(namespace, { autoCompact: { thresholdPercent: 73 } })
819
829
  await ctx.plugin(ToolResultPruner, { profile: 'balanced' }).await()
820
830
  const session = Session.create(SessionId('public-autocompact-coordination'))
@@ -911,7 +921,7 @@ describe('standalone runtime on published Harness APIs', () => {
911
921
  await ctx.plugin(TestSettings).await()
912
922
  await ctx.plugin(SelectorHost).await()
913
923
  const audit = captureAudit(ctx)
914
- const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
924
+ const namespace = nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
915
925
  await ctx.plugin(ToolResultPruner, { profile: 'balanced' }).await()
916
926
 
917
927
  const first = Session.create(SessionId('public-autocompact-freeze-first'))
@@ -975,6 +985,7 @@ describe('standalone runtime on published Harness APIs', () => {
975
985
  }), { surfaceOp: 'append' })
976
986
  session.append('step/start', { turn: 1, step: 1 })
977
987
  session.append('assistant/message', {
988
+ stream: [],
978
989
  turn: 1,
979
990
  step: 1,
980
991
  message: createMessage({
@@ -1002,7 +1013,7 @@ describe('standalone runtime on published Harness APIs', () => {
1002
1013
 
1003
1014
  const before = measureForCompaction(ctx, session)
1004
1015
  const toolResultTokens = before.measuredNodes
1005
- .filter(node => session.events[node.seq]?.type === 'tool/result')
1016
+ .filter(node => sessionEvents(session)[node.seq]?.type === 'tool/result')
1006
1017
  .reduce((sum, node) => sum + (node.count.kind === 'exact-tokenizer' ? node.count.tokens : 0), 0)
1007
1018
  // Deadline D = 7000 is reached by the complete request, not by the tool
1008
1019
  // results, which stay far below every trigger in force.
@@ -1175,6 +1186,7 @@ describe('standalone runtime on published Harness APIs', () => {
1175
1186
  session.append('turn/start', { turn })
1176
1187
  session.append('step/start', { turn, step: 1 })
1177
1188
  session.append('assistant/message', {
1189
+ stream: [],
1178
1190
  turn,
1179
1191
  step: 1,
1180
1192
  message: createMessage({
@@ -1224,6 +1236,7 @@ describe('standalone runtime on published Harness APIs', () => {
1224
1236
  })
1225
1237
  onlyUnsafe.append('step/start', { turn: 1, step: 1 })
1226
1238
  onlyUnsafe.append('assistant/message', {
1239
+ stream: [],
1227
1240
  turn: 1,
1228
1241
  step: 1,
1229
1242
  message: createMessage({
@@ -1382,9 +1395,10 @@ describe('standalone runtime on published Harness APIs', () => {
1382
1395
  it('audits recovery-tool-unavailable when a committed batch cannot land without the recovery tool', async () => {
1383
1396
  const ctx = new Context()
1384
1397
  activeContexts.push(ctx)
1385
- await ctx.plugin(SessionStore)
1386
- await ctx.plugin(SystemPrompt)
1387
- await ctx.plugin(TokenMeter)
1398
+ await ctx.plugin(SessionStore).await()
1399
+ await ctx.plugin(SystemPrompt).await()
1400
+ await ctx.plugin(SessionProjectionRegistry).await()
1401
+ await ctx.plugin(TokenMeter).await()
1388
1402
  const audit = captureAudit(ctx)
1389
1403
  await ctx.plugin(ToolResultPruner, {
1390
1404
  profile: 'balanced',
@@ -1435,7 +1449,7 @@ describe('standalone runtime on published Harness APIs', () => {
1435
1449
  }
1436
1450
  custom.prefixPolicy = 'pressure-break'
1437
1451
  custom.tailTrim = { enabled: false, trigger: 700_000 }
1438
- await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
1452
+ await ctx.settings.update(nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
1439
1453
  profile: 'custom',
1440
1454
  custom,
1441
1455
  })
@@ -1520,7 +1534,7 @@ describe('standalone runtime on published Harness APIs', () => {
1520
1534
  activeContexts.push(ctx)
1521
1535
  await mountAgentLoopTestDependencies(ctx)
1522
1536
  await ctx.plugin(AgentLoop, { agents: [] })
1523
- await ctx.plugin(TokenMeter)
1537
+ await ctx.plugin(TokenMeter).await()
1524
1538
  const audit = captureAudit(ctx)
1525
1539
  ctx.llm.registerAdapter(
1526
1540
  ['deepseek'],
@@ -1538,7 +1552,7 @@ describe('standalone runtime on published Harness APIs', () => {
1538
1552
  historyMinReclaimTokens: 1,
1539
1553
  }).await()
1540
1554
 
1541
- const agent = ctx.agentLoop.create(SessionId('public-history-capacity-request-boundary'), {
1555
+ const agent = await ctx.agentLoop.create(SessionId('public-history-capacity-request-boundary'), {
1542
1556
  provider: 'deepseek',
1543
1557
  model: MODEL,
1544
1558
  })
@@ -1604,10 +1618,11 @@ describe('standalone runtime on published Harness APIs', () => {
1604
1618
  activeContexts.push(ctx)
1605
1619
  await ctx.plugin(TestSettings).await()
1606
1620
  await ctx.plugin(SelectorHost).await()
1607
- await ctx.plugin(SessionStore)
1608
- await ctx.plugin(SystemPrompt)
1609
- await ctx.plugin(ToolRuntime)
1610
- await ctx.plugin(TokenMeter)
1621
+ await ctx.plugin(SessionStore).await()
1622
+ await ctx.plugin(SystemPrompt).await()
1623
+ await ctx.plugin(ToolRuntime).await()
1624
+ await ctx.plugin(SessionProjectionRegistry).await()
1625
+ await ctx.plugin(TokenMeter).await()
1611
1626
  const audit = captureAudit(ctx)
1612
1627
 
1613
1628
  const firstCustom = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
@@ -1615,7 +1630,7 @@ describe('standalone runtime on published Harness APIs', () => {
1615
1630
  firstCustom.aggregate.enabled = false
1616
1631
  firstCustom.history.enabled = false
1617
1632
  if (firstCustom.version === 3) firstCustom.tailTrim.enabled = false
1618
- const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
1633
+ const namespace = nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
1619
1634
  await ctx.settings.update(namespace, { profile: 'custom', custom: firstCustom })
1620
1635
  await ctx.plugin(ToolResultPruner, {
1621
1636
  profile: 'off',
@@ -1665,10 +1680,11 @@ describe('standalone runtime on published Harness APIs', () => {
1665
1680
  activeContexts.push(ctx)
1666
1681
  await ctx.plugin(TestSettings).await()
1667
1682
  await ctx.plugin(SelectorHost).await()
1668
- await ctx.plugin(SessionStore)
1669
- await ctx.plugin(SystemPrompt)
1670
- await ctx.plugin(ToolRuntime)
1671
- await ctx.plugin(TokenMeter)
1683
+ await ctx.plugin(SessionStore).await()
1684
+ await ctx.plugin(SystemPrompt).await()
1685
+ await ctx.plugin(ToolRuntime).await()
1686
+ await ctx.plugin(SessionProjectionRegistry).await()
1687
+ await ctx.plugin(TokenMeter).await()
1672
1688
  const audit = captureAudit(ctx)
1673
1689
 
1674
1690
  const policy = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
@@ -1681,7 +1697,7 @@ describe('standalone runtime on published Harness APIs', () => {
1681
1697
  policy.history.keepRecentTokens = 0
1682
1698
  policy.history.minReclaim = 1
1683
1699
  policy.tailTrim = { enabled: true, trigger: 8 }
1684
- await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
1700
+ await ctx.settings.update(nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
1685
1701
  profile: 'custom',
1686
1702
  custom: policy,
1687
1703
  })
@@ -1697,7 +1713,7 @@ describe('standalone runtime on published Harness APIs', () => {
1697
1713
 
1698
1714
  pruner.pruneSession(session, { stage: 'pressure' })
1699
1715
 
1700
- const manifest = session.events.findLast(event => event.type === 'compaction/prune')
1716
+ const manifest = sessionEvents(session).findLast(event => event.type === 'compaction/prune')
1701
1717
  expect(manifest?.type).toBe('compaction/prune')
1702
1718
  if (manifest?.type !== 'compaction/prune') throw new Error('TailTrim did not publish a standard prune')
1703
1719
  const publication = validatePublishedTailTrim(session, manifest.seq)
@@ -1732,7 +1748,7 @@ describe('standalone runtime on published Harness APIs', () => {
1732
1748
  expect(recoveredText).toContain('kind: tailtrim-group')
1733
1749
  expect(recoveredText).toContain('old candidate result')
1734
1750
 
1735
- const persistedEvents = JSON.parse(JSON.stringify(session.events))
1751
+ const persistedEvents = JSON.parse(JSON.stringify(sessionEvents(session)))
1736
1752
  const replay = Session.create(session.id, persistedEvents)
1737
1753
  const replayPublication = validatePublishedTailTrim(replay, manifest.seq)
1738
1754
  expect(replayPublication?.ref).toBe(publication?.ref)
@@ -1760,10 +1776,11 @@ describe('standalone runtime on published Harness APIs', () => {
1760
1776
  activeContexts.push(ctx)
1761
1777
  await ctx.plugin(TestSettings).await()
1762
1778
  await ctx.plugin(SelectorHost).await()
1763
- await ctx.plugin(SessionStore)
1764
- await ctx.plugin(SystemPrompt)
1765
- await ctx.plugin(ToolRuntime)
1766
- await ctx.plugin(TokenMeter)
1779
+ await ctx.plugin(SessionStore).await()
1780
+ await ctx.plugin(SystemPrompt).await()
1781
+ await ctx.plugin(ToolRuntime).await()
1782
+ await ctx.plugin(SessionProjectionRegistry).await()
1783
+ await ctx.plugin(TokenMeter).await()
1767
1784
  const audit = captureAudit(ctx)
1768
1785
 
1769
1786
  const policy = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
@@ -1776,7 +1793,7 @@ describe('standalone runtime on published Harness APIs', () => {
1776
1793
  policy.history.keepRecentTokens = 0
1777
1794
  policy.history.minReclaim = options.minReclaim
1778
1795
  policy.tailTrim = { enabled: true, trigger: options.trigger }
1779
- await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
1796
+ await ctx.settings.update(nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
1780
1797
  profile: 'custom',
1781
1798
  custom: policy,
1782
1799
  })
@@ -1842,12 +1859,13 @@ describe('standalone runtime on published Harness APIs', () => {
1842
1859
  activeContexts.push(ctx)
1843
1860
  await ctx.plugin(TestSettings).await()
1844
1861
  await ctx.plugin(SelectorHost).await()
1845
- await ctx.plugin(SessionStore)
1846
- await ctx.plugin(InvariantRegistry)
1847
- await ctx.plugin(RuntimeInvariant)
1848
- await ctx.plugin(SystemPrompt)
1849
- await ctx.plugin(ToolRuntime)
1850
- await ctx.plugin(TokenMeter)
1862
+ await ctx.plugin(SessionStore).await()
1863
+ await ctx.plugin(InvariantRegistry).await()
1864
+ await ctx.plugin(RuntimeInvariant).await()
1865
+ await ctx.plugin(SystemPrompt).await()
1866
+ await ctx.plugin(ToolRuntime).await()
1867
+ await ctx.plugin(SessionProjectionRegistry).await()
1868
+ await ctx.plugin(TokenMeter).await()
1851
1869
  const audit = captureAudit(ctx)
1852
1870
 
1853
1871
  const policy = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
@@ -1859,7 +1877,7 @@ describe('standalone runtime on published Harness APIs', () => {
1859
1877
  policy.history.keepRecentTokens = 0
1860
1878
  policy.history.minReclaim = 1
1861
1879
  policy.tailTrim = { enabled: true, trigger: 8 }
1862
- await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
1880
+ await ctx.settings.update(nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
1863
1881
  profile: 'custom',
1864
1882
  custom: policy,
1865
1883
  })
@@ -1884,27 +1902,27 @@ describe('standalone runtime on published Harness APIs', () => {
1884
1902
 
1885
1903
  expect(() => ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })).not.toThrow()
1886
1904
  append.mockRestore()
1887
- expect(session.events.at(-1)?.type).toBe('compaction/prune')
1905
+ expect(sessionEvents(session).at(-1)?.type).toBe('compaction/prune')
1888
1906
  expect(session.surface.nodes).toEqual(beforeSurface)
1889
1907
  expect(rewrites(audit.records())).toHaveLength(0)
1890
1908
  expect(audit.records()).toContainEqual(expect.objectContaining({
1891
1909
  kind: 'failure',
1892
1910
  operation: 'publication',
1893
1911
  component: 'tail-trim',
1894
- manifestSeq: session.events.length - 1,
1912
+ manifestSeq: sessionEvents(session).length - 1,
1895
1913
  }))
1896
1914
 
1897
1915
  expect(() => session.append('turn/end', {
1898
1916
  turn: 3,
1899
1917
  reason: { kind: 'completed' },
1900
1918
  })).not.toThrow()
1901
- const persisted = JSON.parse(JSON.stringify(session.events))
1919
+ const persisted = JSON.parse(JSON.stringify(sessionEvents(session)))
1902
1920
 
1903
1921
  const resumedCtx = new Context()
1904
1922
  activeContexts.push(resumedCtx)
1905
- await resumedCtx.plugin(SessionStore)
1906
- await resumedCtx.plugin(InvariantRegistry)
1907
- await resumedCtx.plugin(RuntimeInvariant)
1923
+ await resumedCtx.plugin(SessionStore).await()
1924
+ await resumedCtx.plugin(InvariantRegistry).await()
1925
+ await resumedCtx.plugin(RuntimeInvariant).await()
1908
1926
  const resumed = resumedCtx.sessions.create(session.id, { seed: persisted })
1909
1927
  expect(resumed.surface.nodes).toEqual(beforeSurface)
1910
1928
  expect(() => resumed.append('turn/start', { turn: 4 })).not.toThrow()
@@ -1913,23 +1931,23 @@ describe('standalone runtime on published Harness APIs', () => {
1913
1931
  it('still rejects a malformed adjacent replacement instead of treating it as an orphan', async () => {
1914
1932
  const ctx = new Context()
1915
1933
  activeContexts.push(ctx)
1916
- await ctx.plugin(SessionStore)
1917
- await ctx.plugin(InvariantRegistry)
1918
- await ctx.plugin(RuntimeInvariant)
1934
+ await ctx.plugin(SessionStore).await()
1935
+ await ctx.plugin(InvariantRegistry).await()
1936
+ await ctx.plugin(RuntimeInvariant).await()
1919
1937
  const session = ctx.sessions.create(SessionId('public-malformed-companion'))
1920
1938
  const source = appendToolTurn(session, 1, 'source remains intact', false)
1921
1939
  session.append('compaction/prune', {
1922
- shadowedRange: { start: source.resultSeq, end: source.resultSeq },
1923
- shadowedSeqs: [source.resultSeq],
1940
+ shadowedRange: { start: SessionSeq(source.resultSeq), end: SessionSeq(source.resultSeq) },
1941
+ shadowedSeqs: [SessionSeq(source.resultSeq)],
1924
1942
  shadowedTokenCount: 1,
1925
1943
  })
1926
- const result = session.events[source.resultSeq]
1944
+ const result = sessionEvents(session)[source.resultSeq]
1927
1945
  if (result?.type !== 'tool/result') throw new Error('missing source result')
1928
1946
  const before = session.seq
1929
1947
 
1930
1948
  expect(() => session.append('tool/result', result.data, {
1931
- surfaceOp: { op: 'replace', start: source.assistantSeq, end: source.assistantSeq },
1932
- sourceEventSeqs: [source.resultSeq],
1949
+ surfaceOp: { op: 'replace', startSeq: SessionSeq(source.assistantSeq), endSeq: SessionSeq(source.assistantSeq) },
1950
+ sourceEventSeqs: [SessionSeq(source.resultSeq)],
1933
1951
  })).toThrow(/sourceEventSeqs|does not replace compaction\/prune range/)
1934
1952
  expect(session.seq).toBe(before)
1935
1953
  })
@@ -1942,7 +1960,7 @@ describe('standalone runtime on published Harness APIs', () => {
1942
1960
  const summary = session.append('compaction/summary', {
1943
1961
  compactionId: CompactionId('public-native-auto'),
1944
1962
  summary: [{ type: 'text', text: 'summary' }],
1945
- shadowedRange: { start: 0, end: 0 },
1963
+ shadowedRange: { start: SessionSeq(0), end: SessionSeq(0) },
1946
1964
  shadowedSeqs: [],
1947
1965
  shadowedTokenCount: 321,
1948
1966
  provider: 'deepseek',
@@ -1968,7 +1986,7 @@ describe('standalone runtime on published Harness APIs', () => {
1968
1986
  activeContexts.push(ctx)
1969
1987
  await mountAgentLoopTestDependencies(ctx)
1970
1988
  await ctx.plugin(AgentLoop, { agents: [] })
1971
- await ctx.plugin(TokenMeter)
1989
+ await ctx.plugin(TokenMeter).await()
1972
1990
  const audit = captureAudit(ctx)
1973
1991
  ctx.llm.registerAdapter(
1974
1992
  ['deepseek'],
@@ -1986,7 +2004,7 @@ describe('standalone runtime on published Harness APIs', () => {
1986
2004
  maxTokens: 100,
1987
2005
  compactionRetries: 0,
1988
2006
  })
1989
- const agent = ctx.agentLoop.create(SessionId('public-native-auto-real'), {
2007
+ const agent = await ctx.agentLoop.create(SessionId('public-native-auto-real'), {
1990
2008
  provider: 'deepseek',
1991
2009
  model: MODEL,
1992
2010
  })
@@ -2003,7 +2021,7 @@ describe('standalone runtime on published Harness APIs', () => {
2003
2021
  await agent.whenIdle()
2004
2022
 
2005
2023
  expect(rewrites(audit.records())).toHaveLength(0)
2006
- const summary = agent.session.events.findLast(event => event.type === 'compaction/summary')
2024
+ const summary = sessionEvents(agent.session).findLast(event => event.type === 'compaction/summary')
2007
2025
  expect(summary?.type).toBe('compaction/summary')
2008
2026
  expect(audit.records()).toContainEqual(expect.objectContaining({
2009
2027
  kind: 'native-auto-compact',
@@ -2021,11 +2039,12 @@ describe('standalone runtime on published Harness APIs', () => {
2021
2039
  activeContexts.push(ctx)
2022
2040
  await ctx.plugin(TestSettings).await()
2023
2041
  await ctx.plugin(SelectorHost).await()
2024
- await ctx.plugin(LlmRuntime)
2025
- await ctx.plugin(SessionStore)
2026
- await ctx.plugin(SystemPrompt)
2027
- await ctx.plugin(ToolRuntime)
2028
- await ctx.plugin(TokenMeter)
2042
+ await ctx.plugin(LlmRuntime).await()
2043
+ await ctx.plugin(SessionStore).await()
2044
+ await ctx.plugin(SystemPrompt).await()
2045
+ await ctx.plugin(ToolRuntime).await()
2046
+ await ctx.plugin(SessionProjectionRegistry).await()
2047
+ await ctx.plugin(TokenMeter).await()
2029
2048
  const audit = captureAudit(ctx)
2030
2049
 
2031
2050
  const policy = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
@@ -2045,7 +2064,7 @@ describe('standalone runtime on published Harness APIs', () => {
2045
2064
  }
2046
2065
  policy.prefixPolicy = 'pressure-break'
2047
2066
  policy.tailTrim = { enabled: true, trigger: 8 }
2048
- await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
2067
+ await ctx.settings.update(nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
2049
2068
  profile: 'custom',
2050
2069
  custom: policy,
2051
2070
  })
@@ -2100,7 +2119,7 @@ describe('standalone runtime on published Harness APIs', () => {
2100
2119
  () => Promise.resolve({ kind: 'enter' as const, messages: [] }),
2101
2120
  )
2102
2121
  expect(decision).toEqual({ kind: 'enter', messages: [] })
2103
- const summary = session.events.findLast(event => event.type === 'compaction/summary')
2122
+ const summary = sessionEvents(session).findLast(event => event.type === 'compaction/summary')
2104
2123
  expect(summary?.type).toBe('compaction/summary')
2105
2124
 
2106
2125
  const records = audit.records()
@@ -2123,7 +2142,7 @@ describe('standalone runtime on published Harness APIs', () => {
2123
2142
  expect(firstIndex('history')).toBeLessThan(firstIndex('tail-trim'))
2124
2143
  expect(records.findIndex(record => record.kind === 'native-auto-compact'))
2125
2144
  .toBeGreaterThan(firstIndex('tail-trim'))
2126
- expect(session.events.some(event => event.type === ('compaction/group-trim' as string))).toBe(false)
2145
+ expect(sessionEvents(session).some(event => event.type === ('compaction/group-trim' as string))).toBe(false)
2127
2146
  })
2128
2147
 
2129
2148
  it('compresses text tool results exactly in a vision session that also carries a user image', async () => {
@@ -2155,6 +2174,7 @@ describe('standalone runtime on published Harness APIs', () => {
2155
2174
  }), { surfaceOp: 'append' })
2156
2175
  session.append('step/start', { turn: 1, step: 1 })
2157
2176
  session.append('assistant/message', {
2177
+ stream: [],
2158
2178
  turn: 1,
2159
2179
  step: 1,
2160
2180
  message: createMessage({
@@ -2222,6 +2242,7 @@ describe('standalone runtime on published Harness APIs', () => {
2222
2242
  })
2223
2243
  session.append('step/start', { turn: 1, step: 1 })
2224
2244
  session.append('assistant/message', {
2245
+ stream: [],
2225
2246
  turn: 1,
2226
2247
  step: 1,
2227
2248
  message: createMessage({
@@ -2253,7 +2274,7 @@ describe('standalone runtime on published Harness APIs', () => {
2253
2274
  expect(fresh.pruned).toHaveLength(0)
2254
2275
  expect(pressure.pruned).toHaveLength(0)
2255
2276
  // The original image-bearing result stays on the surface untouched.
2256
- const original = session.events[imageResult.seq]
2277
+ const original = sessionEvents(session)[imageResult.seq]
2257
2278
  expect(original?.type).toBe('tool/result')
2258
2279
  expect(audit.records()).toContainEqual(expect.objectContaining({
2259
2280
  kind: 'component-evaluation',
@@ -2460,7 +2481,7 @@ describe('standalone runtime on published Harness APIs', () => {
2460
2481
  await ctx.plugin(TestSettings).await()
2461
2482
  await ctx.plugin(SelectorHost).await()
2462
2483
  const audit = captureAudit(ctx)
2463
- await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
2484
+ await ctx.settings.update(nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
2464
2485
  profile: 'custom',
2465
2486
  custom: {
2466
2487
  version: 3,
@@ -2481,6 +2502,7 @@ describe('standalone runtime on published Harness APIs', () => {
2481
2502
  })
2482
2503
  session.append('step/start', { turn: 1, step: 1 })
2483
2504
  session.append('assistant/message', {
2505
+ stream: [],
2484
2506
  turn: 1,
2485
2507
  step: 1,
2486
2508
  message: createMessage({
@@ -2511,7 +2533,7 @@ describe('standalone runtime on published Harness APIs', () => {
2511
2533
  expect(result.pruned).toHaveLength(0)
2512
2534
  expect(rewrites(audit.records()).some(record =>
2513
2535
  record.sessionId === String(session.id) && record.component === 'tail-trim')).toBe(false)
2514
- expect(session.events.some(event =>
2536
+ expect(sessionEvents(session).some(event =>
2515
2537
  event.type === 'tool/result'
2516
2538
  && event.data.message.content.some(block => block.type === 'tool-result'
2517
2539
  && block.content.some(inner => inner.type === 'image')))).toBe(true)
@@ -8,7 +8,7 @@ const EVENTS = Object.freeze([
8
8
 
9
9
  describe('sessionEvents', () => {
10
10
  it('retains the rc.2 events accessor fallback', () => {
11
- const session = { events: EVENTS } as Session
11
+ const session = { events: EVENTS } as unknown as Session
12
12
  expect(sessionEvents(session)).toBe(EVENTS)
13
13
  })
14
14
 
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Settings-seat contract pin for dsh-context-compression-improved's client.
3
+ *
4
+ * This spec locks WHERE the compression settings panel mounts: exactly ONE
5
+ * seat — the standalone `settings.section` entry (设置 → 上下文压缩) — and
6
+ * explicitly NOT the Plugins-section surfaces. History this pins against: the
7
+ * 0.1.5 line first lost every entry (a lazy `ctx.get` of locale/settingsScope
8
+ * raced the settings client and apply bailed), then showed the panel twice
9
+ * (item card + tab on top of the standalone section). Both were fixed by the
10
+ * declarative-inject + single-section shape below. When a future DSH line
11
+ * moves the seat again, migrate src/client/index.ts AND this file together.
12
+ */
13
+ import { describe, expect, it, vi } from 'vitest'
14
+ import type { Context as ClientContext } from '@deepseek-ai/cordis'
15
+ import { apply, inject } from '../src/client/index.ts'
16
+ import { ContextCompressionSettingsSection } from '../src/client/CompressionProfileSelector.tsx'
17
+
18
+ vi.mock('@deepseek-ai/dsh-client-ui-primitives', () => ({
19
+ IconChevronDownOutline14: () => null,
20
+ Menu: () => null,
21
+ }))
22
+
23
+ interface CapturedRegistration {
24
+ readonly slot: string
25
+ readonly options: Record<string, unknown>
26
+ readonly component: unknown
27
+ }
28
+
29
+ /** Drive apply against a stub host and capture every slot registration. */
30
+ function collectRegistrations(): { declared: string[]; registrations: CapturedRegistration[] } {
31
+ const declared: string[] = []
32
+ const registrations: CapturedRegistration[] = []
33
+ const ctx = {
34
+ locale: { register: vi.fn(() => () => {}), bind: () => (key: string) => key },
35
+ settingsScope: { bind: vi.fn(() => ({}) as never) },
36
+ slots: {
37
+ inject: (slot: string, factory: () => (() => void) | Generator<() => void>) => {
38
+ declared.push(slot)
39
+ const result = factory()
40
+ const steps: Iterable<() => void> = typeof (result as IteratorObject)?.[Symbol.iterator] === 'function'
41
+ ? (result as Generator<() => void>)
42
+ : [result as () => void]
43
+ for (const step of steps) { void step }
44
+ return () => {}
45
+ },
46
+ register: (options: Record<string, unknown>, component: unknown) => {
47
+ registrations.push({ slot: String(options['name']), options, component })
48
+ return () => {}
49
+ },
50
+ },
51
+ }
52
+ apply(ctx as unknown as ClientContext)
53
+ return { declared, registrations }
54
+ }
55
+
56
+ describe('settings-seat contract (standalone settings.section only)', () => {
57
+ it('declares the services apply consumes (cordis waits; no lazy-get race)', () => {
58
+ expect(inject).toEqual(['slots', 'locale', 'settingsScope'])
59
+ })
60
+
61
+ it('injects exactly the settings.section seat and nothing else', () => {
62
+ const { declared, registrations } = collectRegistrations()
63
+ expect(declared).toEqual(['settings.section'])
64
+ expect(registrations).toHaveLength(1)
65
+ expect(registrations[0]!.slot).toBe('settings.section')
66
+ })
67
+
68
+ it('pins the section identity (id/order/label/locale) and the panel component', () => {
69
+ const { registrations } = collectRegistrations()
70
+ const { options, component } = registrations[0]!
71
+ expect(options['id']).toBe('context-compression')
72
+ expect(options['order']).toBe(17)
73
+ expect(options['locale']).toBe('context-compression')
74
+ expect((options['label'] as () => string)()).toBe('nav')
75
+ expect(typeof options['inject']).toBe('function')
76
+ const face = (options['inject'] as () => Record<string, unknown>)()
77
+ expect(Object.keys(face).sort()).toEqual(['hooks', 'saveAutoCompact', 'saveCodeSkeleton', 'saveCustom', 'savePresetOptions', 'select', 'resetCustom'].sort())
78
+ expect(component).toBe(ContextCompressionSettingsSection)
79
+ })
80
+
81
+ it('never adds a Plugins-section card or tab alongside the standalone section', () => {
82
+ const { declared } = collectRegistrations()
83
+ expect(declared).not.toContain('settings.plugins.tab')
84
+ expect(declared).not.toContain('settings.plugin.item')
85
+ })
86
+ })