pi-ui-extend 1.0.41 → 1.0.45

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 (236) hide show
  1. package/README.md +50 -13
  2. package/dist/app/app.d.ts +13 -0
  3. package/dist/app/app.js +158 -8
  4. package/dist/app/cli/install.js +2 -2
  5. package/dist/app/commands/command-controller.d.ts +4 -0
  6. package/dist/app/commands/command-controller.js +11 -0
  7. package/dist/app/commands/command-git-actions.d.ts +25 -0
  8. package/dist/app/commands/command-git-actions.js +381 -0
  9. package/dist/app/commands/command-host.d.ts +4 -0
  10. package/dist/app/commands/command-host.js +5 -2
  11. package/dist/app/commands/command-model-actions.d.ts +1 -0
  12. package/dist/app/commands/command-model-actions.js +57 -35
  13. package/dist/app/commands/command-navigation-actions.d.ts +1 -0
  14. package/dist/app/commands/command-navigation-actions.js +16 -3
  15. package/dist/app/commands/command-registry.d.ts +2 -0
  16. package/dist/app/commands/command-registry.js +15 -1
  17. package/dist/app/commands/command-session-actions.js +9 -3
  18. package/dist/app/commands/reload-context-inventory.d.ts +11 -0
  19. package/dist/app/commands/reload-context-inventory.js +70 -0
  20. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  21. package/dist/app/extensions/extension-actions-controller.js +6 -0
  22. package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
  23. package/dist/app/extensions/subagent-catalog-state.js +23 -0
  24. package/dist/app/input/autocomplete-controller.js +61 -34
  25. package/dist/app/input/input-action-controller.d.ts +2 -0
  26. package/dist/app/input/input-action-controller.js +8 -1
  27. package/dist/app/input/input-controller.d.ts +2 -1
  28. package/dist/app/input/input-controller.js +7 -2
  29. package/dist/app/input/prompt-enhancer-controller.js +53 -35
  30. package/dist/app/input/voice-controller.d.ts +41 -45
  31. package/dist/app/input/voice-controller.js +351 -419
  32. package/dist/app/model/model-usage-controller.js +17 -7
  33. package/dist/app/model/model-usage-status.d.ts +4 -1
  34. package/dist/app/model/model-usage-status.js +270 -46
  35. package/dist/app/popup/menu-items-controller.d.ts +13 -3
  36. package/dist/app/popup/menu-items-controller.js +37 -21
  37. package/dist/app/popup/popup-action-controller.d.ts +12 -2
  38. package/dist/app/popup/popup-action-controller.js +77 -24
  39. package/dist/app/popup/popup-menu-controller.d.ts +36 -14
  40. package/dist/app/popup/popup-menu-controller.js +239 -69
  41. package/dist/app/rendering/dcp-stats.d.ts +9 -4
  42. package/dist/app/rendering/dcp-stats.js +40 -425
  43. package/dist/app/rendering/editor-panels.js +10 -4
  44. package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
  45. package/dist/app/rendering/popup-menu-renderer.js +42 -37
  46. package/dist/app/rendering/render-controller.js +23 -2
  47. package/dist/app/rendering/status-line-renderer.d.ts +5 -0
  48. package/dist/app/rendering/status-line-renderer.js +41 -5
  49. package/dist/app/rendering/tab-line-renderer.js +26 -20
  50. package/dist/app/runtime.d.ts +12 -1
  51. package/dist/app/runtime.js +120 -12
  52. package/dist/app/screen/mouse-controller.d.ts +2 -0
  53. package/dist/app/screen/mouse-controller.js +17 -7
  54. package/dist/app/screen/status-controller.d.ts +4 -0
  55. package/dist/app/screen/status-controller.js +5 -0
  56. package/dist/app/session/lazy-session-manager.js +34 -0
  57. package/dist/app/session/session-event-controller.d.ts +1 -0
  58. package/dist/app/session/session-event-controller.js +10 -1
  59. package/dist/app/session/session-history.d.ts +1 -0
  60. package/dist/app/session/session-history.js +12 -1
  61. package/dist/app/session/session-lifecycle-controller.d.ts +7 -1
  62. package/dist/app/session/session-lifecycle-controller.js +16 -1
  63. package/dist/app/session/tabs-controller.d.ts +26 -1
  64. package/dist/app/session/tabs-controller.js +454 -147
  65. package/dist/app/subagents/subagents-files.js +60 -1
  66. package/dist/app/subagents/subagents-model.d.ts +1 -0
  67. package/dist/app/subagents/subagents-model.js +18 -2
  68. package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
  69. package/dist/app/subagents/subagents-widget-controller.js +6 -0
  70. package/dist/app/types.d.ts +16 -1
  71. package/dist/app/workspace/workspace-actions-controller.js +10 -2
  72. package/dist/app/workspace/workspace-undo.d.ts +1 -0
  73. package/dist/app/workspace/workspace-undo.js +1 -0
  74. package/dist/bundled-extensions/question/index.js +9 -1
  75. package/dist/bundled-extensions/question/remote.d.ts +4 -0
  76. package/dist/bundled-extensions/question/remote.js +33 -0
  77. package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
  78. package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
  79. package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
  80. package/dist/bundled-extensions/telegram-connector/config.js +87 -0
  81. package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
  82. package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
  83. package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
  84. package/dist/bundled-extensions/telegram-connector/index.js +195 -0
  85. package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
  86. package/dist/bundled-extensions/terminal-bell/index.js +0 -76
  87. package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
  88. package/dist/bundled-extensions/workspace-undo/index.js +191 -0
  89. package/dist/config.d.ts +21 -2
  90. package/dist/config.js +159 -32
  91. package/dist/default-pix-config.js +25 -5
  92. package/dist/schemas/index.d.ts +1 -0
  93. package/dist/schemas/index.js +1 -0
  94. package/dist/schemas/pi-tools-suite-schema.d.ts +88 -62
  95. package/dist/schemas/pi-tools-suite-schema.js +54 -83
  96. package/dist/schemas/pix-schema.d.ts +19 -2
  97. package/dist/schemas/pix-schema.js +47 -5
  98. package/dist/schemas/tasks-schema.d.ts +18 -0
  99. package/dist/schemas/tasks-schema.js +41 -0
  100. package/docs/concurrency.md +9 -1
  101. package/docs/desktop-mvp.md +22 -6
  102. package/docs/desktop-task-manager.md +148 -84
  103. package/docs/release.md +30 -6
  104. package/external/pi-tools-suite/README.md +197 -84
  105. package/external/pi-tools-suite/docs/evals.md +1 -1
  106. package/external/pi-tools-suite/docs/session-recovery.md +47 -14
  107. package/external/pi-tools-suite/docs/subagent-model-pools.md +48 -30
  108. package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
  109. package/external/pi-tools-suite/package.json +6 -1
  110. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
  111. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
  112. package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
  113. package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
  114. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
  115. package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
  116. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
  117. package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
  118. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
  119. package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
  120. package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
  121. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
  122. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
  123. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
  124. package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
  125. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
  126. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
  127. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
  128. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
  129. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
  130. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
  131. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
  132. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
  133. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
  134. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
  135. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
  136. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
  137. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
  138. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
  139. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
  140. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
  141. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
  142. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
  143. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
  144. package/external/pi-tools-suite/src/async-subagents/commands.ts +15 -71
  145. package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
  146. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
  147. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  148. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -5
  149. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
  150. package/external/pi-tools-suite/src/async-subagents/core/config.ts +160 -327
  151. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
  152. package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
  153. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +21 -5
  154. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +40 -26
  155. package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
  156. package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
  157. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
  158. package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
  159. package/external/pi-tools-suite/src/async-subagents/lib.ts +16 -9
  160. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -5
  161. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +5 -4
  162. package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
  163. package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
  164. package/external/pi-tools-suite/src/config.ts +55 -1
  165. package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
  166. package/external/pi-tools-suite/src/context-gateway/config.ts +77 -3
  167. package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
  168. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +222 -0
  169. package/external/pi-tools-suite/src/context-gateway/index.ts +206 -19
  170. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +7 -7
  171. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
  172. package/external/pi-tools-suite/src/context-gateway/types.ts +16 -3
  173. package/external/pi-tools-suite/src/context-inventory.ts +99 -0
  174. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
  175. package/external/pi-tools-suite/src/dcp/auto-compress.ts +128 -49
  176. package/external/pi-tools-suite/src/dcp/commands.ts +32 -156
  177. package/external/pi-tools-suite/src/dcp/compress-tool.ts +177 -46
  178. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
  179. package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
  180. package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
  181. package/external/pi-tools-suite/src/dcp/config.ts +114 -17
  182. package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
  183. package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
  184. package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
  185. package/external/pi-tools-suite/src/dcp/index.ts +338 -66
  186. package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
  187. package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
  188. package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
  189. package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
  190. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +93 -3
  191. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
  192. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
  193. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
  194. package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
  195. package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
  196. package/external/pi-tools-suite/src/dcp/state.ts +9 -0
  197. package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
  198. package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
  199. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +78 -22
  200. package/external/pi-tools-suite/src/index.ts +7 -1
  201. package/external/pi-tools-suite/src/lib/project.ts +36 -1
  202. package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
  203. package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
  204. package/external/pi-tools-suite/src/resource-registry/index.ts +2551 -0
  205. package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
  206. package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
  207. package/external/pi-tools-suite/src/todo/index.ts +21 -0
  208. package/external/pi-tools-suite/src/todo/todo.ts +19 -1
  209. package/external/pi-tools-suite/src/tool-descriptions.ts +47 -19
  210. package/package.json +10 -9
  211. package/schemas/pi-tools-suite.json +466 -287
  212. package/schemas/pix.json +129 -11
  213. package/schemas/tasks.json +131 -0
  214. package/skills/simplify/SKILL.md +33 -5
  215. package/docs/desktop-markdown-media.md +0 -77
  216. package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -177
  217. package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
  218. package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
  219. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
  220. package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
  221. package/skills/playwright-cli/SKILL.md +0 -420
  222. package/skills/playwright-cli/references/element-attributes.md +0 -23
  223. package/skills/playwright-cli/references/playwright-tests.md +0 -50
  224. package/skills/playwright-cli/references/request-mocking.md +0 -87
  225. package/skills/playwright-cli/references/running-code.md +0 -241
  226. package/skills/playwright-cli/references/session-management.md +0 -273
  227. package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
  228. package/skills/playwright-cli/references/storage-state.md +0 -290
  229. package/skills/playwright-cli/references/test-generation.md +0 -142
  230. package/skills/playwright-cli/references/tracing.md +0 -154
  231. package/skills/playwright-cli/references/video-recording.md +0 -147
  232. package/skills/spec-lite/SKILL.md +0 -140
  233. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
  234. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
  235. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
  236. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
@@ -2,10 +2,11 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-c
2
2
  import type { AutocompleteItem } from "@earendil-works/pi-tui"
3
3
  import type { DcpState } from "./state.js"
4
4
  import { modelKeysFromContext, resolveModelConfig, type DcpConfig } from "./config.js"
5
- import type { DcpNudgeType } from "./pruner-types.js"
6
5
  import { isToolRecordProtected, markToolPruned } from "./pruner.js"
7
6
  import { ignoreStaleExtensionContextError, safeGetContextUsage } from "../context-usage.js"
8
7
  import { captureDcpTransactionGuard, cloneDcpTransactionState, runDcpStateTransaction } from "./state-transaction.js"
8
+ import { readDcpJournalBranch } from "./journal.js"
9
+ import { formatDcpStatistics } from "./statistics.js"
9
10
 
10
11
  // ---------------------------------------------------------------------------
11
12
  // Constants
@@ -34,42 +35,6 @@ function fmt(n: number): string {
34
35
  return n.toLocaleString()
35
36
  }
36
37
 
37
- const NUDGE_TYPES: DcpNudgeType[] = ["turn", "iteration", "context-soft", "context-strong"]
38
-
39
- function pct(numerator: number, denominator: number): string {
40
- if (denominator <= 0) return "n/a"
41
- return `${((numerator / denominator) * 100).toFixed(1)}%`
42
- }
43
-
44
- function nudgeLabel(type: DcpNudgeType): string {
45
- switch (type) {
46
- case "context-strong": return "context-strong"
47
- case "context-soft": return "context-soft"
48
- case "iteration": return "iteration"
49
- case "turn": return "turn"
50
- }
51
- }
52
-
53
- function isNudgeType(value: unknown): value is DcpNudgeType {
54
- return typeof value === "string" && (NUDGE_TYPES as string[]).includes(value)
55
- }
56
-
57
- function customEntryData(entry: unknown, customType: string): Record<string, unknown> | undefined {
58
- const record = entry as { type?: unknown; customType?: unknown; data?: unknown }
59
- if (record?.type !== "custom" || record.customType !== customType) return undefined
60
- if (!record.data || typeof record.data !== "object" || Array.isArray(record.data)) return undefined
61
- return record.data as Record<string, unknown>
62
- }
63
-
64
- function branchEntries(ctx: ExtensionCommandContext): any[] {
65
- try {
66
- const branch = ctx.sessionManager?.getBranch?.()
67
- return Array.isArray(branch) ? branch : []
68
- } catch {
69
- return []
70
- }
71
- }
72
-
73
38
  async function staleSafe(action: () => void | Promise<void>): Promise<void> {
74
39
  try {
75
40
  await action()
@@ -78,85 +43,6 @@ async function staleSafe(action: () => void | Promise<void>): Promise<void> {
78
43
  }
79
44
  }
80
45
 
81
- interface DcpNudgeStats {
82
- emitted: number
83
- reapplied: number
84
- upgraded: number
85
- clearedEvents: number
86
- clearedAnchors: number
87
- byType: Record<DcpNudgeType, number>
88
- activeByType: Record<DcpNudgeType, number>
89
- last?: {
90
- type: DcpNudgeType
91
- event: "emitted" | "reapplied" | "upgraded"
92
- createdAt?: number
93
- contextPercent?: number | null
94
- }
95
- }
96
-
97
- function collectNudgeStats(ctx: ExtensionCommandContext, state: DcpState): DcpNudgeStats {
98
- const stats: DcpNudgeStats = {
99
- emitted: 0,
100
- reapplied: 0,
101
- upgraded: 0,
102
- clearedEvents: 0,
103
- clearedAnchors: 0,
104
- byType: { "turn": 0, "iteration": 0, "context-soft": 0, "context-strong": 0 },
105
- activeByType: { "turn": 0, "iteration": 0, "context-soft": 0, "context-strong": 0 },
106
- }
107
-
108
- for (const anchor of state.nudgeAnchors) {
109
- if (isNudgeType(anchor.type)) stats.activeByType[anchor.type]++
110
- }
111
-
112
- for (const entry of branchEntries(ctx)) {
113
- const data = customEntryData(entry, "dcp-nudge")
114
- if (!data) continue
115
- const event = data.event
116
- if ((event === "emitted" || event === "reapplied" || event === "upgraded") && isNudgeType(data.type)) {
117
- if (event === "emitted") stats.emitted++
118
- else if (event === "reapplied") stats.reapplied++
119
- else stats.upgraded++
120
- stats.byType[data.type]++
121
- const createdAt = typeof data.createdAt === "number" ? data.createdAt : undefined
122
- const rawContextPercent = data.contextPercent
123
- let contextPercent: number | null | undefined
124
- if (typeof rawContextPercent === "number") contextPercent = rawContextPercent
125
- else if (rawContextPercent === null) contextPercent = null
126
- if (!stats.last || (createdAt ?? 0) >= (stats.last.createdAt ?? 0)) {
127
- stats.last = { type: data.type, event, createdAt, contextPercent }
128
- }
129
- } else if (event === "cleared") {
130
- stats.clearedEvents++
131
- stats.clearedAnchors += typeof data.clearedAnchors === "number" ? Math.max(0, data.clearedAnchors) : 0
132
- }
133
- }
134
-
135
- if (!stats.last && state.lastNudge && isNudgeType(state.lastNudge.type)) {
136
- stats.last = {
137
- type: state.lastNudge.type,
138
- event: "emitted",
139
- createdAt: state.lastNudge.createdAt,
140
- contextPercent: typeof state.lastNudge.contextPercent === "number"
141
- ? state.lastNudge.contextPercent * 100
142
- : undefined,
143
- }
144
- }
145
-
146
- return stats
147
- }
148
-
149
- function formatDate(ts: number | undefined): string {
150
- if (typeof ts !== "number" || !Number.isFinite(ts) || ts <= 0) return "unknown time"
151
- return new Date(ts).toLocaleString()
152
- }
153
-
154
- function formatContextPercent(value: number | null | undefined): string {
155
- if (value === null) return "unknown context"
156
- if (typeof value !== "number" || !Number.isFinite(value)) return "unknown context"
157
- return `${value.toFixed(1)}% context`
158
- }
159
-
160
46
  function sendChatSystemMessage(pi: ExtensionAPI, customType: string, content: string, details?: Record<string, unknown>): void {
161
47
  pi.sendMessage({
162
48
  customType,
@@ -179,6 +65,7 @@ const HELP_TEXT = `DCP — Dynamic Context Pruning
179
65
  Commands:
180
66
  /dcp context — Show context window usage breakdown
181
67
  /dcp stats — Show pruning statistics for this session
68
+ /dcp doctor — Show config compatibility diagnostics
182
69
  /dcp sweep [N] — Prune last N tool outputs (default: all since last user msg)
183
70
  /dcp manual — Show manual mode status
184
71
  /dcp manual on — Enable manual mode (disable autonomous compression nudges)
@@ -189,6 +76,18 @@ function handleHelp(ctx: ExtensionCommandContext): void {
189
76
  ctx.ui.notify(HELP_TEXT, "info")
190
77
  }
191
78
 
79
+ function handleDoctor(ctx: ExtensionCommandContext, config: DcpConfig): void {
80
+ const issues = config.issues ?? []
81
+ const lines = ["DCP Doctor", ""]
82
+ if (issues.length === 0) {
83
+ lines.push("Config: no removed or ignored DCP settings detected.")
84
+ } else {
85
+ lines.push(`Config: ${issues.length} removed/ignored setting${issues.length === 1 ? "" : "s"}:`)
86
+ for (const issue of issues) lines.push(` - ${issue}`)
87
+ }
88
+ ctx.ui.notify(lines.join("\n"), issues.length > 0 ? "warning" : "info")
89
+ }
90
+
192
91
  // ---------------------------------------------------------------------------
193
92
  // Context usage
194
93
  // ---------------------------------------------------------------------------
@@ -225,44 +124,16 @@ function handleContext(ctx: ExtensionCommandContext, state: DcpState): void {
225
124
  // Stats
226
125
  // ---------------------------------------------------------------------------
227
126
 
228
- function handleStats(pi: ExtensionAPI, ctx: ExtensionCommandContext, state: DcpState): void {
229
- const activeBlocks = state.compressionBlocks.filter((b) => b.active).length
230
- const totalBlocks = state.compressionBlocks.length
231
- const nudgeStats = collectNudgeStats(ctx, state)
232
- const totalNudgeEvents = nudgeStats.emitted + nudgeStats.reapplied + nudgeStats.upgraded
233
- const activeAnchors = state.nudgeAnchors.length
234
- const lines: string[] = []
235
- lines.push("DCP Session Statistics:")
236
- lines.push(` Tokens saved (estimated): ${fmt(state.tokensSaved)}`)
237
- lines.push(` Total pruning operations: ${fmt(state.totalPruneCount)}`)
238
- lines.push(` Compression blocks active: ${activeBlocks} / ${totalBlocks} total`)
239
- lines.push(` Manual mode: ${state.manualMode ? "on" : "off"}`)
240
- lines.push("")
241
- lines.push("Nudge telemetry:")
242
- lines.push(` Sent: ${fmt(nudgeStats.emitted)} emitted, ${fmt(nudgeStats.reapplied)} reapplied, ${fmt(nudgeStats.upgraded)} upgraded`)
243
- lines.push(
244
- ` By type: ${NUDGE_TYPES.map((type) => `${nudgeLabel(type)}=${fmt(nudgeStats.byType[type])}`).join(", ")}`,
245
- )
246
- lines.push(
247
- ` Active anchors: ${fmt(activeAnchors)}${activeAnchors > 0
248
- ? ` (${NUDGE_TYPES.map((type) => `${nudgeLabel(type)}=${fmt(nudgeStats.activeByType[type])}`).join(", ")})`
249
- : ""}`,
250
- )
251
- lines.push(` Cleared after compress: ${fmt(nudgeStats.clearedEvents)} time${nudgeStats.clearedEvents === 1 ? "" : "s"} (${fmt(nudgeStats.clearedAnchors)} anchor${nudgeStats.clearedAnchors === 1 ? "" : "s"})`)
252
- lines.push(` Compliance proxy: ${fmt(nudgeStats.clearedEvents)} compress-after-nudge / ${fmt(totalNudgeEvents)} nudge event${totalNudgeEvents === 1 ? "" : "s"} (${pct(nudgeStats.clearedEvents, totalNudgeEvents)})`)
253
- if (nudgeStats.last) {
254
- lines.push(
255
- ` Last nudge: ${nudgeLabel(nudgeStats.last.type)} ${nudgeStats.last.event} at ${formatDate(nudgeStats.last.createdAt)} (${formatContextPercent(nudgeStats.last.contextPercent)})`,
256
- )
257
- } else {
258
- lines.push(" Last nudge: none recorded")
259
- }
260
-
261
- sendChatSystemMessage(pi, DCP_STATS_MESSAGE_TYPE, lines.join("\n"), {
262
- generatedAt: new Date().toISOString(),
263
- activeAnchors,
264
- nudgeEvents: totalNudgeEvents,
265
- })
127
+ async function handleStats(pi: ExtensionAPI, ctx: ExtensionCommandContext, state: DcpState): Promise<void> {
128
+ const epoch = state.sessionEpoch
129
+ let branch: unknown[] = []
130
+ let historyStatus: "full" | "unavailable" = "full"
131
+ try { branch = await readDcpJournalBranch(ctx) }
132
+ catch { historyStatus = "unavailable" }
133
+ if (state.sessionEpoch !== epoch) return
134
+ sendChatSystemMessage(pi, DCP_STATS_MESSAGE_TYPE,
135
+ formatDcpStatistics({ branch, historyStatus, model: ctx.model, usage: safeGetContextUsage(ctx) }),
136
+ { generatedAt: new Date().toISOString() })
266
137
  }
267
138
 
268
139
  // ---------------------------------------------------------------------------
@@ -277,7 +148,7 @@ async function handleSweep(
277
148
  ): Promise<void> {
278
149
  await ctx.waitForIdle()
279
150
 
280
- const branch = branchEntries(ctx)
151
+ const branch = await readDcpJournalBranch(ctx) as any[]
281
152
 
282
153
  // Build the full set of protected tool names.
283
154
  const protectedTools = new Set<string>([
@@ -434,6 +305,7 @@ export function registerCommands(
434
305
  const subcommands: AutocompleteItem[] = [
435
306
  { value: "context", label: "context", description: "Show context window usage breakdown" },
436
307
  { value: "stats", label: "stats", description: "Show pruning statistics" },
308
+ { value: "doctor", label: "doctor", description: "Show config compatibility diagnostics" },
437
309
  { value: "sweep", label: "sweep", description: "Prune tool outputs" },
438
310
  { value: "manual", label: "manual", description: "Toggle manual mode" },
439
311
  { value: "compress", label: "compress", description: "Trigger LLM compression" },
@@ -466,7 +338,11 @@ export function registerCommands(
466
338
  break
467
339
 
468
340
  case "stats":
469
- handleStats(pi, ctx, state)
341
+ await handleStats(pi, ctx, state)
342
+ break
343
+
344
+ case "doctor":
345
+ handleDoctor(ctx, config)
470
346
  break
471
347
 
472
348
  case "sweep": {
@@ -5,7 +5,7 @@
5
5
  import { Type } from "typebox"
6
6
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"
7
7
  import type { CompressionMember, DcpState } from "./state.js"
8
- import { modelKeysFromContext, resolveModelConfig, type DcpConfig } from "./config.js"
8
+ import { modelKeysFromContext, resolveModelConfig, summarizerModelRefs, type DcpConfig } from "./config.js"
9
9
  import { captureDcpTransactionGuard, cloneDcpTransactionState, runDcpStateTransaction } from "./state-transaction.js"
10
10
  import { createHash } from "node:crypto"
11
11
  import { clearDcpNudgeAnchors } from "./pruner.js"
@@ -30,8 +30,55 @@ import {
30
30
  detectToolGroupSpans,
31
31
  findConversationIndexEntry,
32
32
  } from "./conversation-index.js"
33
- import { previewManualCompressionProjection } from "./compression-preview.js"
33
+ import {
34
+ previewManualCompressionProjection,
35
+ verifiedManualCompressionMessages,
36
+ } from "./compression-preview.js"
34
37
  import { settleCompressionProgress } from "./compression-progress.js"
38
+ import { estimateMessageTokens } from "./pruner-metadata.js"
39
+ import {
40
+ buildSummarySourceManifest,
41
+ hashSummarySourceManifest,
42
+ prepareCompressionSummary,
43
+ type ModelSummaryAttempt,
44
+ } from "./auto-compress.js"
45
+
46
+ export const COMPRESS_TOOL_PARAMETERS = Type.Object({
47
+ topic: Type.String({
48
+ description:
49
+ "Short label (3-5 words) for display - e.g., 'Auth System Exploration'",
50
+ }),
51
+ ranges: Type.Optional(Type.Array(
52
+ Type.Object({
53
+ startId: Type.String({
54
+ description:
55
+ "First ID (mNNN/bN); never start inside a tool group—include its calling assistant.",
56
+ }),
57
+ endId: Type.String({
58
+ description:
59
+ "Last ID (mNNN/bN); include all results of the final tool group, including parallel calls.",
60
+ }),
61
+ summary: Type.Optional(Type.String({
62
+ description: "Optional; omit to use the configured DCP summarizer.",
63
+ })),
64
+ }),
65
+ { description: "One or more ranges to compress" },
66
+ )),
67
+ messages: Type.Optional(Type.Array(
68
+ Type.Object({
69
+ messageId: Type.String({
70
+ description: "Raw message ID to compress individually (e.g. m001)",
71
+ }),
72
+ topic: Type.Optional(Type.String({
73
+ description: "Short label for this one-message summary; defaults to top-level topic",
74
+ })),
75
+ summary: Type.Optional(Type.String({
76
+ description: "Optional; omit to use the configured DCP summarizer.",
77
+ })),
78
+ }),
79
+ { description: "Individual raw messages to compress surgically" },
80
+ )),
81
+ })
35
82
 
36
83
  type MessageSkipKind =
37
84
  | "duplicate"
@@ -51,7 +98,7 @@ interface MessageSkipIssue {
51
98
  interface ResolvedRangePlan {
52
99
  startId: string
53
100
  endId: string
54
- summary: string
101
+ summary?: string
55
102
  startTimestamp: number
56
103
  endTimestamp: number
57
104
  startMessageId?: string
@@ -63,7 +110,7 @@ interface ResolvedRangePlan {
63
110
  interface ResolvedMessagePlan {
64
111
  messageId: string
65
112
  topic: string
66
- summary: string
113
+ summary?: string
67
114
  timestamp: number
68
115
  stableId?: string
69
116
  sourceMembers: CompressionMember[]
@@ -72,6 +119,33 @@ interface ResolvedMessagePlan {
72
119
 
73
120
  type ResolvedRangeBoundaries = Omit<ResolvedRangePlan, "sourceMembers" | "mutationMembers">
74
121
 
122
+ type ManualSummaryMode = "explicit" | "model" | "extractive" | "extractive-fallback"
123
+
124
+ function verifiedSelectionMessages(
125
+ state: DcpState,
126
+ sourceMembers: CompressionMember[],
127
+ ): any[] {
128
+ const projection = verifiedManualCompressionMessages(state)
129
+ const byStableId = new Map(
130
+ state.conversationIndexSnapshot.map((entry) => [entry.stableId, entry] as const),
131
+ )
132
+ return sourceMembers.map((member) => {
133
+ const entry = byStableId.get(member.stableId)
134
+ if (!entry || entry.contentHash !== member.hash || !Number.isSafeInteger(entry.index)) {
135
+ throw new Error(
136
+ `Manual compression summary source ${member.stableId} no longer matches the verified provider projection; refresh context and retry.`,
137
+ )
138
+ }
139
+ const message = projection[entry.index]
140
+ if (!message) {
141
+ throw new Error(
142
+ `Manual compression summary source ${member.stableId} is missing from the verified provider projection; refresh context and retry.`,
143
+ )
144
+ }
145
+ return message
146
+ })
147
+ }
148
+
75
149
  function validateNonOverlappingRanges(plans: ResolvedRangeBoundaries[], state: DcpState): void {
76
150
  const sorted = [...plans].sort((a, b) =>
77
151
  compareCompressionBoundaries(
@@ -246,43 +320,7 @@ export function registerCompressTool(
246
320
  label: COMPRESS_TOOL_DESCRIPTION.label,
247
321
  description: COMPRESS_TOOL_DESCRIPTION.description,
248
322
  promptSnippet: COMPRESS_TOOL_DESCRIPTION.promptSnippet ?? "Compress ranges of conversation into summaries to manage context",
249
- parameters: Type.Object({
250
- topic: Type.String({
251
- description:
252
- "Short label (3-5 words) for display - e.g., 'Auth System Exploration'",
253
- }),
254
- ranges: Type.Optional(Type.Array(
255
- Type.Object({
256
- startId: Type.String({
257
- description:
258
- "First ID (mNNN/bN); never start inside a tool group—include its calling assistant.",
259
- }),
260
- endId: Type.String({
261
- description:
262
- "Last ID (mNNN/bN); include all results of the final tool group, including parallel calls.",
263
- }),
264
- summary: Type.String({
265
- description:
266
- "Continuation-focused technical summary; avoid raw JSON/code/diffs unless a short literal is required",
267
- }),
268
- }),
269
- { description: "One or more ranges to compress" },
270
- )),
271
- messages: Type.Optional(Type.Array(
272
- Type.Object({
273
- messageId: Type.String({
274
- description: "Raw message ID to compress individually (e.g. m001)",
275
- }),
276
- topic: Type.Optional(Type.String({
277
- description: "Short label for this one-message summary; defaults to top-level topic",
278
- })),
279
- summary: Type.String({
280
- description: "Continuation-focused technical summary replacing this raw message; avoid raw JSON/code/diffs unless required",
281
- }),
282
- }),
283
- { description: "Individual raw messages to compress surgically" },
284
- )),
285
- }),
323
+ parameters: COMPRESS_TOOL_PARAMETERS,
286
324
 
287
325
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
288
326
  _signal?.throwIfAborted()
@@ -462,6 +500,93 @@ export function registerCompressTool(
462
500
  }
463
501
  assertDisjointSelections(rangePlans, preflightMessages)
464
502
 
503
+ const summaryPreparations: Array<{
504
+ selection: string
505
+ mode: ManualSummaryMode
506
+ summarizerModelRef?: string
507
+ attempts?: Array<{ ref: string; outcome: ModelSummaryAttempt["outcome"] }>
508
+ }> = []
509
+ const resolveSummary = async (input: {
510
+ startId: string
511
+ endId: string
512
+ topic: string
513
+ sourceMembers: CompressionMember[]
514
+ explicitSummary?: string
515
+ }): Promise<string> => {
516
+ const selection = input.startId === input.endId
517
+ ? input.startId
518
+ : `${input.startId}..${input.endId}`
519
+ if (typeof input.explicitSummary === "string") {
520
+ summaryPreparations.push({ selection, mode: "explicit" })
521
+ return input.explicitSummary
522
+ }
523
+ const sourceMessages = verifiedSelectionMessages(state, input.sourceMembers)
524
+ const sourceManifest = buildSummarySourceManifest(sourceMessages)
525
+ const sourceHash = hashSummarySourceManifest(sourceManifest)
526
+ const prepared = await prepareCompressionSummary({
527
+ topic: input.topic,
528
+ messages: sourceMessages,
529
+ candidate: {
530
+ startId: input.startId,
531
+ endId: input.endId,
532
+ messageCount: sourceMessages.length,
533
+ estimatedTokens: sourceMessages.reduce((sum, message) => sum + estimateMessageTokens(message), 0),
534
+ includedBlockIds: [],
535
+ reason: "manual delegated summary",
536
+ },
537
+ modelRefs: summarizerModelRefs(effectiveConfig.compress.autoCompress),
538
+ timeoutMs: effectiveConfig.compress.autoCompress.timeoutMs,
539
+ modelRegistry: (ctx as any).modelRegistry,
540
+ signal: _signal,
541
+ sourceManifest,
542
+ })
543
+ assertCurrent()
544
+ const refreshedMessages = verifiedSelectionMessages(state, input.sourceMembers)
545
+ const refreshedHash = hashSummarySourceManifest(buildSummarySourceManifest(refreshedMessages))
546
+ if (refreshedHash !== sourceHash || prepared.sourceHash !== sourceHash) {
547
+ throw new Error(
548
+ `stale_plan: manual compression summary source changed while preparing ${selection}`,
549
+ )
550
+ }
551
+ const attempts = prepared.summarizerAttempts?.map(({ ref, outcome }) => ({ ref, outcome }))
552
+ summaryPreparations.push({
553
+ selection,
554
+ mode: prepared.representation,
555
+ summarizerModelRef: prepared.summarizerModelRef,
556
+ attempts,
557
+ })
558
+ log("compress.summary_prepared", {
559
+ toolCallId: _toolCallId,
560
+ selection,
561
+ mode: prepared.representation,
562
+ summarizerModelRef: prepared.summarizerModelRef,
563
+ summarizerAttempts: prepared.summarizerAttempts,
564
+ sourceHash: prepared.sourceHash,
565
+ sourceCoverage: prepared.sourceCoverage,
566
+ })
567
+ return prepared.text
568
+ }
569
+
570
+ for (const range of rangePlans) {
571
+ range.summary = await resolveSummary({
572
+ startId: range.startId,
573
+ endId: range.endId,
574
+ topic: params.topic,
575
+ sourceMembers: range.sourceMembers,
576
+ explicitSummary: range.summary,
577
+ })
578
+ }
579
+ for (const plan of preflightMessages) {
580
+ plan.summary = await resolveSummary({
581
+ startId: plan.messageId,
582
+ endId: plan.messageId,
583
+ topic: plan.topic,
584
+ sourceMembers: plan.sourceMembers,
585
+ explicitSummary: plan.summary,
586
+ })
587
+ }
588
+ const preflightMessageById = new Map(preflightMessages.map((plan) => [plan.messageId, plan] as const))
589
+
465
590
  for (const range of rangePlans) {
466
591
  try {
467
592
  const anchor = resolveAnchorBoundary(range.endTimestamp, workingState, range.endMessageId)
@@ -478,7 +603,7 @@ export function registerCompressTool(
478
603
 
479
604
  const created = createRangeCompressionBlock({
480
605
  topic: params.topic,
481
- summary: range.summary,
606
+ summary: range.summary!,
482
607
  startTimestamp: range.startTimestamp,
483
608
  endTimestamp: range.endTimestamp,
484
609
  startMessageId: range.startMessageId,
@@ -521,7 +646,6 @@ export function registerCompressTool(
521
646
  const seenMessageIds = new Set<string>()
522
647
 
523
648
  for (const entry of messages) {
524
- const { summary } = entry
525
649
  const messageId = typeof entry.messageId === "string" ? entry.messageId.trim() : ""
526
650
  if (seenMessageIds.has(messageId)) {
527
651
  skippedMessageIssues.push({ kind: "duplicate", messageId })
@@ -604,7 +728,7 @@ export function registerCompressTool(
604
728
 
605
729
  const created = createRangeCompressionBlock({
606
730
  topic: entry.topic ?? params.topic,
607
- summary,
731
+ summary: preflightMessageById.get(messageId)?.summary!,
608
732
  startTimestamp: meta.timestamp,
609
733
  endTimestamp: meta.timestamp,
610
734
  startMessageId: meta.stableId,
@@ -620,8 +744,8 @@ export function registerCompressTool(
620
744
  validatePlaceholders: false,
621
745
  expandPlaceholders: false,
622
746
  preparedProtectedFragments,
623
- sourceMembers: preflightMessages.find((plan) => plan.messageId === messageId)?.sourceMembers,
624
- mutationMembers: preflightMessages.find((plan) => plan.messageId === messageId)?.mutationMembers,
747
+ sourceMembers: preflightMessageById.get(messageId)?.sourceMembers,
748
+ mutationMembers: preflightMessageById.get(messageId)?.mutationMembers,
625
749
  })
626
750
  const block = created.block
627
751
  newBlockIds.push(block.id)
@@ -671,6 +795,12 @@ export function registerCompressTool(
671
795
  for (const block of workingState.compressionBlocks) {
672
796
  if (newBlockIds.includes(block.id)) block.operationRequestHash = requestHash
673
797
  }
798
+ const firstBlock = workingState.compressionBlocks.find((block) => block.id === newBlockIds[0])
799
+ if (firstBlock) firstBlock.commitMetrics = {
800
+ operationId: `manual:${_toolCallId}`, kind: "manual",
801
+ beforeTokens: projection.projectedBeforeTokens,
802
+ afterTokens: projection.projectedAfterTokens, netGainTokens: projection.netGain,
803
+ }
674
804
  assertCurrent()
675
805
  let published = false
676
806
  await persistState(ctx, workingState, {
@@ -735,6 +865,7 @@ export function registerCompressTool(
735
865
  }
736
866
  const resultDetails = {
737
867
  ...visualDetails,
868
+ summaryPreparations,
738
869
  projectedBeforeTokens: projection?.projectedBeforeTokens,
739
870
  projectedAfterTokens: projection?.projectedAfterTokens,
740
871
  netGain: projection?.netGain,
@@ -8,6 +8,7 @@ import type {
8
8
  import type { DcpConfig } from "./config.js"
9
9
  import { estimateTokens } from "./pruner-metadata.js"
10
10
  import { isToolRecordProtected } from "./pruner-tools.js"
11
+ import { toolRecordContinuity } from "./protected-continuity.js"
11
12
  import { compareConversationStableIds, buildExactRangeMembership } from "./conversation-index.js"
12
13
  import { createHash } from "node:crypto"
13
14
  import { open, realpath } from "node:fs/promises"
@@ -570,10 +571,10 @@ function collectCurrentProtectedFragments(
570
571
  if (!toolCallId) continue
571
572
  const record = state.toolCalls.get(toolCallId)
572
573
  if (!record || record.toolName === "compress" || !isToolRecordProtected(record, config)) continue
573
- const output = (record.outputText ?? meta.text ?? "").trim()
574
- if (!output) continue
575
- const exact = `### Tool: ${record.toolName}\n${output}`
576
- fragments.push(compressionProtectedFragment("tool", `tool:${toolCallId}`, exact))
574
+ const continuity = toolRecordContinuity(record, config)
575
+ const text = continuity.text?.trim()
576
+ if (!text) continue
577
+ fragments.push(compressionProtectedFragment("tool", `tool:${toolCallId}`, text))
577
578
  }
578
579
  return mergeProtectedFragments(fragments)
579
580
  }
@@ -657,51 +658,6 @@ function appendProtectedFragmentLedger(summary: string, fragments: CompressionPr
657
658
  missing.map((fragment) => `\n\n${fragment.text}`).join("")
658
659
  }
659
660
 
660
- function appendProtectedToolOutputs(
661
- summary: string,
662
- startTimestamp: number,
663
- endTimestamp: number,
664
- state: DcpState,
665
- config: DcpConfig,
666
- ids: { startMessageId?: string; endMessageId?: string } = {},
667
- ): string {
668
- const protectedOutputs: string[] = []
669
- const seenToolCallIds = new Set<string>()
670
- const boundaries = rangeBoundaries(startTimestamp, endTimestamp, ids)
671
-
672
- for (const meta of state.messageMetaSnapshot.values()) {
673
- if (meta.blockId !== undefined) continue
674
- if (!Number.isFinite(meta.timestamp)) continue
675
- if (!isCompressionBoundaryWithinRange(
676
- { timestamp: meta.timestamp, stableId: meta.stableId },
677
- boundaries.start,
678
- boundaries.end,
679
- state,
680
- )) continue
681
- if (meta.role !== "toolResult" && meta.role !== "bashExecution") continue
682
-
683
- const toolCallId = meta.toolCallId
684
- if (!toolCallId || seenToolCallIds.has(toolCallId)) continue
685
- const record = state.toolCalls.get(toolCallId)
686
- if (!record) continue
687
-
688
- // The compress tool's own JSON accounting output is control-plane data and
689
- // rarely useful in future summaries; avoid recursively copying it.
690
- if (record.toolName === "compress") continue
691
- if (!isToolRecordProtected(record, config)) continue
692
-
693
- const output = (record.outputText ?? meta.text ?? "").trim()
694
- if (!output) continue
695
-
696
- seenToolCallIds.add(toolCallId)
697
- protectedOutputs.push(`\n### Tool: ${record.toolName}\n${output}`)
698
- }
699
-
700
- if (protectedOutputs.length === 0) return summary
701
- const heading = "\n\nThe following protected tool outputs were included in this conversation section:"
702
- return summary + heading + protectedOutputs.join("")
703
- }
704
-
705
661
  export function estimateVisibleRangeTokens(
706
662
  startTimestamp: number,
707
663
  endTimestamp: number,
@@ -945,20 +901,12 @@ export function createRangeCompressionBlock(
945
901
  ids,
946
902
  )
947
903
 
948
- const expandedSummary = appendProtectedToolOutputs(
949
- promptPreservedSummary,
950
- startTimestamp,
951
- endTimestamp,
952
- state,
953
- config,
954
- ids,
955
- )
956
904
  const protectedFragments = mergeProtectedFragments(
957
905
  collectInheritedProtectedFragments(coveredBlocks),
958
906
  collectCurrentProtectedFragments(startTimestamp, endTimestamp, state, config, mode, ids),
959
907
  preparedProtectedFragments,
960
908
  )
961
- const ledgerPreservedSummary = appendProtectedFragmentLedger(expandedSummary, protectedFragments)
909
+ const ledgerPreservedSummary = appendProtectedFragmentLedger(promptPreservedSummary, protectedFragments)
962
910
 
963
911
  const block: CompressionBlock = {
964
912
  id: state.nextBlockId++,
@@ -58,6 +58,15 @@ function verifiedSnapshot(state: DcpState): any[] {
58
58
  return snapshot
59
59
  }
60
60
 
61
+ /**
62
+ * Return a detached, verified copy of the exact provider projection used by
63
+ * manual compression. Generated summaries must use this source rather than
64
+ * reconstructing history from timestamps or persisted session entries.
65
+ */
66
+ export function verifiedManualCompressionMessages(state: DcpState): any[] {
67
+ return cloneProjectionMessages(verifiedSnapshot(state))
68
+ }
69
+
61
70
  function stripProjectionMetadata(message: any): any {
62
71
  const stripped = stripStaleDcpMetadataFromMessage(message)
63
72
  return copyDcpProvenance(message, stripped)