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
@@ -31,6 +31,7 @@ import {
31
31
  import {
32
32
  applyPruning,
33
33
  getNudgeType,
34
+ detectCompressionBlockConsolidationCandidate,
34
35
  detectCompressionCandidate,
35
36
  detectEmergencyCompressionCandidate,
36
37
  detectMessageCompressionCandidates,
@@ -60,6 +61,11 @@ import {
60
61
  import { DCP_STATS_MESSAGE_TYPE, registerCommands } from "./commands.js"
61
62
  import { normalizeDcpContextUsage } from "./ui.js"
62
63
  import { safeGetContextUsage } from "../context-usage.js"
64
+ import { FreshToolResultTracker } from "./fresh-tool-results.js"
65
+ import { RoutinePressureTracker } from "./routine-pressure.js"
66
+ import { canonicalMessageHash } from "./conversation-index.js"
67
+ import { randomUUID } from "node:crypto"
68
+ import { dcpRequestSnapshot } from "./diagnostics.js"
63
69
  import {
64
70
  collectProviderToolResultEvidence,
65
71
  providerPayloadIncludesToolResult,
@@ -68,23 +74,18 @@ import {
68
74
  ProviderEvidenceTracker,
69
75
  } from "./provider-tool-results.js"
70
76
  import { rehydrateToolRecordsFromMessages } from "./recovery.js"
71
- import { inferDcpBlockedReason, planDcpBudget } from "./progress-controller.js"
77
+ import { inferDcpBlockedReason, planDcpBudget, type DcpBlockedReason } from "./progress-controller.js"
72
78
  import { createBudgetedAutoCompressionBlock } from "./auto-compress-budget.js"
73
- import { outstandingCompressionTokens, resetCompressionProgress, routineRecoveryTokens, trackCompressionProgress } from "./compression-progress.js"
79
+ import { compressionPlanningTokens, outstandingCompressionTokens, resetCompressionProgress, routineRecoveryTokens, trackCompressionProgress } from "./compression-progress.js"
74
80
  import { captureDcpTransactionGuard, cloneDcpTransactionState, runDcpStateTransaction, invalidateDcpStateOwner } from "./state-transaction.js"
75
81
 
82
+ const PIX_DCP_RUNTIME_STATS_SYMBOL = Symbol.for("pix.dcp.runtime-stats")
83
+
76
84
  // ---------------------------------------------------------------------------
77
85
  // Helpers
78
86
  // ---------------------------------------------------------------------------
79
87
 
80
- function annotateMessagesWithBranchEntryIds(messages: any[], ctx: ExtensionContext): void {
81
- let branch: any[] = []
82
- try {
83
- branch = ctx.sessionManager.getBranch()
84
- } catch {
85
- return
86
- }
87
-
88
+ function annotateMessagesWithBranchEntryIds(messages: any[], branch: any[]): void {
88
89
  const entries = branch.filter((entry) => entry?.type === "message" && entry.message)
89
90
  let searchFrom = 0
90
91
  for (const msg of messages) {
@@ -97,6 +98,7 @@ function annotateMessagesWithBranchEntryIds(messages: any[], ctx: ExtensionConte
97
98
  Number.isFinite(msg?.timestamp) &&
98
99
  entryMsg.timestamp !== msg.timestamp
99
100
  ) continue
101
+ if (canonicalMessageHash(stripStaleDcpMetadataFromMessage(entryMsg)) !== canonicalMessageHash(msg)) continue
100
102
  msg._dcpEntryId = entry.id
101
103
  searchFrom = i + 1
102
104
  break
@@ -125,6 +127,20 @@ function isDcpControlPlaneMessage(message: any): boolean {
125
127
  return message?.role === "custom" && DCP_CONTROL_PLANE_CUSTOM_TYPES.has(message.customType)
126
128
  }
127
129
 
130
+ function withoutAutoSummaryModels(config: ReturnType<typeof loadConfig>): ReturnType<typeof loadConfig> {
131
+ return {
132
+ ...config,
133
+ compress: {
134
+ ...config.compress,
135
+ autoCompress: {
136
+ ...config.compress.autoCompress,
137
+ summarizerModel: [],
138
+ summarizerFallbackModels: [],
139
+ },
140
+ },
141
+ }
142
+ }
143
+
128
144
  // ---------------------------------------------------------------------------
129
145
  // Module export
130
146
  // ---------------------------------------------------------------------------
@@ -132,6 +148,10 @@ function isDcpControlPlaneMessage(message: any): boolean {
132
148
  export default async function dcpModule(pi: ExtensionAPI, dependencies: { config?: ReturnType<typeof loadConfig>; state?: ReturnType<typeof createState> } = {}): Promise<void> {
133
149
  // ── 1. Load config ────────────────────────────────────────────────────────
134
150
  const config = dependencies.config ?? loadConfig()
151
+ const runtimeGlobals = globalThis as typeof globalThis & Record<symbol, unknown>
152
+ // Extension reloads may disable DCP. Clear any getter left by the previous
153
+ // module instance before deciding whether this instance is active.
154
+ runtimeGlobals[PIX_DCP_RUNTIME_STATS_SYMBOL] = undefined
135
155
  const configForContext = (ctx: unknown) => resolveModelConfig(config, modelKeysFromContext(ctx))
136
156
  const hasEnabledModelOverride = Object.values(config.modelOverrides).some(
137
157
  (override) => override.enabled === true,
@@ -141,15 +161,33 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
141
161
 
142
162
  // ── 2. Create state ───────────────────────────────────────────────────────
143
163
  const state = dependencies.state ?? createState()
164
+ // Pix ACP runs one pi process per session. Expose only this tiny live metric
165
+ // to the patched RPC session-stats surface; DCP state remains extension-owned.
166
+ runtimeGlobals[PIX_DCP_RUNTIME_STATS_SYMBOL] = () => ({ tokensSaved: state.tokensSaved })
144
167
  let journalMirror: DcpJournalMirror | undefined
145
168
  let journalSupported = false
146
169
  let journalBlockedReason: string | undefined
147
170
  let journalPersistent = false
148
171
  const providerEvidenceTracker = new ProviderEvidenceTracker()
172
+ const freshToolResults = new FreshToolResultTracker()
173
+ const routinePressureTracker = new RoutinePressureTracker()
149
174
  let providerEvidenceCommitQueue = Promise.resolve()
150
175
  let latestProviderOpportunityAvailable = false
151
176
  let latestProviderOpportunityKind: "routine" | "emergency" | undefined
152
177
  let latestProviderReminder: string | undefined
178
+ let autoSummarizerDegraded = false
179
+ let lastProviderReadyProjectionEpoch: number | undefined
180
+ let diagnosticEpoch = randomUUID()
181
+ let diagnosticSnapshot: ReturnType<typeof dcpRequestSnapshot> | undefined
182
+ const diagnostic = (event: string, data: Record<string, unknown> = {}) => {
183
+ try { pi.appendEntry("dcp-diagnostic", { version: 1, event, epoch: diagnosticEpoch, ...data }) }
184
+ catch { /* Diagnostics must never affect a transaction or provider send. */ }
185
+ }
186
+ const resetDiagnostics = (reason: string) => {
187
+ diagnosticEpoch = randomUUID()
188
+ diagnosticSnapshot = undefined
189
+ diagnostic("epoch", { reason })
190
+ }
153
191
  const warnedProgress = new Set<string>()
154
192
  const isJournalSessionSupported = () => journalSupported && journalBlockedReason === undefined
155
193
  const persistJournalState = async (
@@ -174,15 +212,18 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
174
212
  entry?.type === "message" || entry?.type === "custom_message" || entry?.type === "compaction" || entry?.type === "branch_summary",
175
213
  )
176
214
  const loadJournalState = async (ctx: ExtensionContext, allowInitialize: boolean): Promise<void> => {
215
+ const epoch = state.sessionEpoch
177
216
  journalMirror = undefined
178
217
  journalSupported = false
179
- journalBlockedReason = undefined
218
+ journalBlockedReason = "DCP journal load in progress"
180
219
  journalPersistent = false
181
- const branch = await readDcpJournalBranch(ctx)
182
220
  const manager = ctx.sessionManager as any
183
221
  const persistentSession = (typeof manager.isPersisted === "function" && manager.isPersisted())
184
222
  || (typeof manager.getSessionFile === "function" && Boolean(manager.getSessionFile()))
185
223
  try {
224
+ const branch = await readDcpJournalBranch(ctx)
225
+ if (state.sessionEpoch !== epoch) return
226
+ journalBlockedReason = undefined
186
227
  const replay = replayDcpJournal(branch, state)
187
228
  if (replay.initialized && replay.lastOperationId) {
188
229
  journalSupported = true
@@ -203,6 +244,7 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
203
244
  // compatibility checkpoint from existing history.
204
245
  journalSupported = false
205
246
  } catch (error) {
247
+ if (state.sessionEpoch !== epoch) return
206
248
  journalBlockedReason = error instanceof Error ? error.message : String(error)
207
249
  journalSupported = false
208
250
  journalMirror = undefined
@@ -242,12 +284,20 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
242
284
  }
243
285
  const invalidateOwner = () => {
244
286
  invalidateDcpStateOwner(state)
287
+ routinePressureTracker.reset()
288
+ // Successful exposure belongs to the old provider/branch too, not only
289
+ // the pending attempt. The new owner must establish its own evidence.
290
+ state.providerSeenToolIds.clear()
291
+ freshToolResults.reset()
245
292
  providerEvidenceTracker.reset()
246
293
  latestProviderOpportunityAvailable = false
247
294
  latestProviderOpportunityKind = undefined
248
295
  latestProviderReminder = undefined
296
+ autoSummarizerDegraded = false
297
+ lastProviderReadyProjectionEpoch = undefined
249
298
  resetCompressionProgress(state)
250
299
  warnedProgress.clear()
300
+ resetDiagnostics("owner-changed")
251
301
  }
252
302
  pi.on("model_select", invalidateOwner)
253
303
  pi.on("session_tree", async (_event, ctx) => {
@@ -304,13 +354,20 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
304
354
  // ── 5. session_start: restore state from session entries ──────────────────
305
355
  pi.on("session_start", async (event, ctx) => {
306
356
  resetState(state)
357
+ routinePressureTracker.reset()
358
+ freshToolResults.reset()
359
+ lastProviderReadyProjectionEpoch = undefined
307
360
  providerEvidenceTracker.reset()
308
361
  latestProviderOpportunityAvailable = false
309
362
  latestProviderOpportunityKind = undefined
310
363
  latestProviderReminder = undefined
364
+ autoSummarizerDegraded = false
311
365
  warnedProgress.clear()
312
366
  if (config.manualMode.enabled) state.manualMode = true
367
+ const startingEpoch = state.sessionEpoch
313
368
  await loadJournalState(ctx, event.reason === "new" || event.reason === "startup")
369
+ if (state.sessionEpoch !== startingEpoch) return
370
+ resetDiagnostics(event.reason)
314
371
  writeDcpDebugLog(configForContext(ctx), "session_start.journal", {
315
372
  reason: event.reason,
316
373
  supported: journalSupported,
@@ -330,6 +387,8 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
330
387
  // Journal operations are committed at the mutation boundary; shutdown does
331
388
  // not write a full runtime snapshot.
332
389
  pi.on("session_shutdown", async () => {
390
+ routinePressureTracker.reset()
391
+ freshToolResults.reset()
333
392
  journalMirror = undefined
334
393
  journalSupported = false
335
394
  journalBlockedReason = undefined
@@ -355,6 +414,9 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
355
414
 
356
415
  // ── 8. tool_call: record input args for dedup / purge fingerprinting ───────
357
416
  pi.on("tool_call", async (event, _ctx) => {
417
+ if (configForContext(_ctx).enabled && isJournalSessionSupported()) {
418
+ freshToolResults.toolCall(event.toolCallId, event.toolName, state.toolCalls.has(event.toolCallId))
419
+ }
358
420
  if (!state.toolCalls.has(event.toolCallId)) {
359
421
  state.toolCalls.set(event.toolCallId, {
360
422
  toolCallId: event.toolCallId,
@@ -375,6 +437,7 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
375
437
 
376
438
  // ── 9. tool_result: finalise tool record with result info ─────────────────
377
439
  pi.on("tool_result", async (event, _ctx) => {
440
+ freshToolResults.toolResult(event.toolCallId, event.toolName)
378
441
  const record = state.toolCalls.get(event.toolCallId)
379
442
 
380
443
  const outputText = event.content
@@ -414,7 +477,29 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
414
477
  const contextMessages = event.messages
415
478
  .filter((message: any) => !isUserVisibleOnlyMessage(message) && !isDcpControlPlaneMessage(message))
416
479
  .map((message: any) => stripStaleDcpMetadataFromMessage(message))
417
- const finishContext = (reason: string, messages: any[], details: Record<string, unknown> = {}) => {
480
+ const rawContextTokens = contextMessages.reduce((sum, message) => sum + estimateMessageTokens(message), 0)
481
+ const pressureOwner = JSON.stringify([contextEpoch, ...modelKeysFromContext(ctx), ctx.model?.contextWindow, ctx.model?.maxTokens])
482
+ const finishContext = (
483
+ reason: string,
484
+ messages: any[],
485
+ details: Record<string, unknown> = {},
486
+ providerReady = true,
487
+ ) => {
488
+ if (state.sessionEpoch !== contextEpoch) throw new DcpJournalError("DCP projection belongs to a stale owner")
489
+ lastProviderReadyProjectionEpoch = providerReady ? state.sessionEpoch : undefined
490
+ const observedUsage = safeGetContextUsage(ctx)
491
+ diagnosticSnapshot = dcpRequestSnapshot(effectiveConfig, state, ctx.model,
492
+ observedUsage, contextMessages, messages, reason)
493
+ const routine = routinePressureTracker.estimate(pressureOwner,
494
+ normalizeDcpContextUsage(observedUsage)?.tokens,
495
+ contextMessages, rawContextTokens, diagnosticSnapshot.projectedTokens, state.compressionBlocks)
496
+ diagnosticSnapshot.routineProjectedTokens = routine.projectedTokens
497
+ diagnosticSnapshot.routineUsageAdjustmentTokens = routine.adjustmentTokens
498
+ if (diagnosticSnapshot.pressure === "routine" && routine.projectedTokens <= (diagnosticSnapshot.routineTokens ?? 0)) {
499
+ diagnosticSnapshot.pressure = "below routine threshold"
500
+ }
501
+ if (providerReady) routinePressureTracker.prepare(pressureOwner, rawContextTokens,
502
+ diagnosticSnapshot.projectedTokens, state.compressionBlocks)
418
503
  writeDcpDebugLog(effectiveConfig, "context.result", {
419
504
  reason,
420
505
  inputMessages: event.messages.length,
@@ -434,6 +519,7 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
434
519
  }, ctx)
435
520
  latestProviderOpportunityAvailable = false
436
521
  if (!effectiveConfig.enabled) {
522
+ routinePressureTracker.reset()
437
523
  writeDcpDebugLog(effectiveConfig, "context.disabled", {
438
524
  inputMessages: event.messages.length,
439
525
  filteredMessages: contextMessages.length,
@@ -448,7 +534,12 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
448
534
  }
449
535
  latestProviderOpportunityKind = undefined
450
536
  latestProviderReminder = undefined
451
- annotateMessagesWithBranchEntryIds(contextMessages, ctx)
537
+ // Failed full-history reads must not reassign IDs or expose uncompressed
538
+ // source. Async work from an old session/model cannot publish a projection.
539
+ lastProviderReadyProjectionEpoch = undefined
540
+ const fullBranch = await readDcpJournalBranch(ctx)
541
+ if (state.sessionEpoch !== contextEpoch) throw new DcpJournalError("DCP context owner changed during history read")
542
+ annotateMessagesWithBranchEntryIds(contextMessages, fullBranch)
452
543
  const rehydration = rehydrateToolRecordsFromMessages(contextMessages, state)
453
544
  if (rehydration.recordsUpdated > 0) {
454
545
  writeDcpDebugLog(effectiveConfig, "context.rehydrated_tool_records", { ...rehydration }, ctx)
@@ -458,6 +549,7 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
458
549
  // projection must be committed before the request can use them.
459
550
  await persistJournalState(ctx, state)
460
551
  let candidate = null as ReturnType<typeof detectCompressionCandidate>
552
+ let blockConsolidationCandidate = null as ReturnType<typeof detectCompressionBlockConsolidationCandidate>
461
553
  let emergencyCompressionCandidate = null as ReturnType<typeof detectEmergencyCompressionCandidate>
462
554
  let messageCandidates = [] as ReturnType<typeof detectMessageCompressionCandidates>
463
555
  let emergencySelection = null as ReturnType<typeof analyzeEmergencyCurrentTurn> | null
@@ -466,8 +558,14 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
466
558
  // Manual mode skips routine autonomous nudges and automatic summary
467
559
  // creation. The bounded emergency safety path remains separate, matching
468
560
  // the manual-mode prompt.
469
- const nativeUsage = normalizeDcpContextUsage(safeGetContextUsage(ctx))
470
561
  const ctxModel = (ctx as any).model
562
+ const observedUsage = normalizeDcpContextUsage(safeGetContextUsage(ctx))
563
+ // Model capacity is authoritative even while SDK usage still describes
564
+ // the preceding model. Keep its token count only as a conservative floor.
565
+ const modelWindow = Number.isFinite(ctxModel?.contextWindow) && ctxModel.contextWindow > 0 ? ctxModel.contextWindow : undefined
566
+ const nativeUsage = observedUsage && modelWindow
567
+ ? { ...observedUsage, contextWindow: modelWindow, percent: observedUsage.tokens === null ? null : observedUsage.tokens / modelWindow * 100 }
568
+ : observedUsage
471
569
  const fallbackContextWindow = nativeUsage?.contextWindow ?? (
472
570
  typeof ctxModel?.contextWindow === "number" && Number.isFinite(ctxModel.contextWindow) && ctxModel.contextWindow > 0
473
571
  ? ctxModel.contextWindow
@@ -522,23 +620,34 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
522
620
  estimatorMarginTokens,
523
621
  })
524
622
  thresholds.maxContextPercent = budget.softHeadroomTokens / usage.contextWindow
525
- const contextPercent = budget.projectedBeforeTokens / usage.contextWindow
623
+ const budgetContextPercent = budget.projectedBeforeTokens / usage.contextWindow
526
624
  const nativePressure = emergencyPressureState(
527
- contextPercent,
625
+ budgetContextPercent,
528
626
  thresholds.maxContextPercent,
529
627
  emergencySettings.hardContextPercent,
530
628
  )
531
629
  const hardEmergencyReached = nativePressure.hardEmergencyReached || budget.hardPressure
532
630
  const contextLimitReached = nativePressure.contextLimitReached || budget.pressured
533
631
  const emergencyPressureReached = nativePressure.emergencyPressureReached || budget.pressured
632
+ const routinePressure = routinePressureTracker.estimate(pressureOwner, nativeUsage?.tokens,
633
+ contextMessages, rawContextTokens, repoProjectedTokens, state.compressionBlocks)
634
+ // Correct only routine policy. Capacity, strong pressure and emergency
635
+ // recovery retain the unadjusted conservative provider-native floor.
636
+ const policyTokens = emergencyPressureReached ? budget.projectedBeforeTokens : routinePressure.projectedTokens
637
+ const contextPercent = policyTokens / usage.contextWindow
534
638
  const routineNudgesAllowed = contextPercent > thresholds.minContextPercent
535
639
  if (!budget.capacityExceeded && state.progressRecovery) state.progressRecovery = undefined
536
640
  if (!emergencyPressureReached && !routineNudgesAllowed) {
537
- const clearedAnchors = clearDcpNudgeAnchors(state)
641
+ // Falling usage is not an intentional history rewrite. Replay a
642
+ // frozen reminder even after restart or a lower native sample; only
643
+ // discard anchors whose exact carrier is no longer present.
644
+ const anchorsBefore = state.nudgeAnchors.length
645
+ const replay = applyAnchoredNudges(prunedMessages, state, (anchor) => baseNudgeText(anchor.type))
646
+ const clearedAnchors = anchorsBefore - state.nudgeAnchors.length
538
647
  const resetEmergencyPasses = state.consecutiveIgnoredStrongNudges > 0 || state.consecutiveIgnoredNudges > 0 || !!state.compressionProgress
539
648
  resetCompressionProgress(state)
540
649
  warnedProgress.clear()
541
- if (clearedAnchors > 0) await persistJournalState(ctx, state)
650
+ if (replay.stateChanged) await persistJournalState(ctx, state)
542
651
  return finishContext("below-threshold", prunedMessages, {
543
652
  contextPercent,
544
653
  thresholds,
@@ -578,10 +687,10 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
578
687
 
579
688
  const requestedRecoveryTokens = emergencyPressureReached
580
689
  ? budget.requiredSavingsTokens
581
- : routineRecoveryTokens(budget.projectedBeforeTokens, usage.contextWindow, thresholds.minContextPercent)
690
+ : routineRecoveryTokens(policyTokens, usage.contextWindow, thresholds.minContextPercent)
582
691
  const progressInput = {
583
692
  projectedTokens: repoProjectedTokens,
584
- observedTokens: budget.projectedBeforeTokens,
693
+ observedTokens: policyTokens,
585
694
  contextWindow: usage.contextWindow,
586
695
  requiredTokens: requestedRecoveryTokens,
587
696
  kind: emergencyPressureReached ? "emergency" as const : "routine" as const,
@@ -592,9 +701,11 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
592
701
  resetCompressionProgress(state)
593
702
  warnedProgress.clear()
594
703
  }
595
- const planningRecoveryTokens = remainingRecoveryTokens !== undefined && remainingRecoveryTokens > 0
596
- ? state.compressionProgress?.kind === progressInput.kind ? remainingRecoveryTokens : Math.max(remainingRecoveryTokens, requestedRecoveryTokens)
597
- : requestedRecoveryTokens
704
+ const planningRecoveryTokens = compressionPlanningTokens(
705
+ state,
706
+ progressInput,
707
+ remainingRecoveryTokens,
708
+ )
598
709
  const routineEscalated = !state.manualMode && routineNudgesAllowed &&
599
710
  state.consecutiveIgnoredNudges > Math.max(0, Math.floor(effectiveConfig.compress.autoCompress.patience))
600
711
  const progressPressureReached = emergencyPressureReached || routineEscalated
@@ -630,7 +741,7 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
630
741
  effectiveConfig,
631
742
  contextPercent,
632
743
  progressPressureReached
633
- ? { requiredSavingsTokens: planningRecoveryTokens }
744
+ ? { requiredSavingsTokens: planningRecoveryTokens, allowCompressionBlocks: false }
634
745
  : undefined,
635
746
  )
636
747
  messageCandidates = detectMessageCompressionCandidates(
@@ -639,12 +750,24 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
639
750
  effectiveConfig,
640
751
  contextPercent,
641
752
  )
753
+ if (progressPressureReached) {
754
+ blockConsolidationCandidate = detectCompressionBlockConsolidationCandidate(
755
+ prunedMessages,
756
+ state,
757
+ effectiveConfig,
758
+ {
759
+ requiredSavingsTokens: planningRecoveryTokens,
760
+ estimatorMarginTokens,
761
+ },
762
+ )
763
+ }
642
764
  writeDcpDebugLog(effectiveConfig, "context.candidates", {
643
765
  contextPercent,
644
766
  thresholds,
645
767
  budget,
646
768
  nudgeType,
647
769
  candidate,
770
+ blockConsolidationCandidate,
648
771
  messageCandidates,
649
772
  state: summarizeDcpState(state, effectiveConfig),
650
773
  }, ctx)
@@ -676,7 +799,7 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
676
799
  effectiveConfig,
677
800
  contextPercent,
678
801
  planningThreshold,
679
- { requiredSavingsTokens: planningRecoveryTokens },
802
+ { requiredSavingsTokens: planningRecoveryTokens, allowCompressionBlocks: false },
680
803
  )
681
804
  if (emergencyCompressionCandidate) {
682
805
  writeDcpDebugLog(effectiveConfig, "context.emergency_compression_candidate", {
@@ -732,6 +855,11 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
732
855
  `DCP cannot safely meet its compression goal: ${blockedReason}. Context has not been recovered.`)
733
856
  }
734
857
  if (blockedReason && budget.capacityExceeded) {
858
+ warnProgress(
859
+ ctx,
860
+ blockedReason,
861
+ `DCP cannot fit the next provider request safely (${budget.projectedBeforeTokens} projected input tokens > ${budget.inputCapacityTokens} input capacity): ${blockedReason}. The current agent operation is being stopped instead of sending an oversized request.`,
862
+ )
735
863
  state.progressRecovery = {
736
864
  blockedReason,
737
865
  projectedBeforeTokens: budget.projectedBeforeTokens,
@@ -755,17 +883,17 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
755
883
  blocked_reason: blockedReason,
756
884
  handoff: abortSupported ? "abort-current-agent-operation" : "abort-unavailable",
757
885
  budget,
758
- })
886
+ }, false)
759
887
  }
760
888
  if (!manualEmergencyOnly && !emergencyPressureReached && !hasCompressionSuggestion) {
761
- const clearedAnchors = clearDcpNudgeAnchors(state)
762
- if (clearedAnchors > 0) await persistJournalState(ctx, state)
763
- if (nudgeType || clearedAnchors > 0) {
889
+ // Candidate disappearance is not permission to erase a published
890
+ // reminder. Finalization replays it or drops an absent exact carrier.
891
+ if (nudgeType) {
764
892
  writeDcpDebugLog(effectiveConfig, "context.no_compression_candidate", {
765
893
  contextPercent,
766
894
  thresholds,
767
895
  nudgeType,
768
- clearedAnchors,
896
+ clearedAnchors: 0,
769
897
  state: summarizeDcpState(state, effectiveConfig),
770
898
  }, ctx)
771
899
  }
@@ -781,9 +909,13 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
781
909
  if (routineEscalated && !effectiveConfig.compress.autoCompress.enabled) {
782
910
  warnProgress(ctx, "auto-disabled", "DCP compression has not made enough progress. Automatic compression is disabled; use compress or enable compress.autoCompress explicitly.")
783
911
  }
912
+ let autoCompressionFailure: { blockedReason?: DcpBlockedReason; error: string } | undefined
913
+ let partialAutoProjectionTokens: number | undefined
784
914
  if (!manualEmergencyOnly) {
785
- const autoCandidate = candidate ?? emergencyCompressionCandidate
786
- const cacheSafeReminderAvailable = hasCacheSafeNudgeCarrier(prunedMessages)
915
+ const autoCandidate = blockConsolidationCandidate ?? candidate ?? emergencyCompressionCandidate
916
+ const cacheSafeReminderAvailable = hasCacheSafeNudgeCarrier(
917
+ prunedMessages, freshToolResults.eligibleIds, state.nudgeAnchors,
918
+ )
787
919
  const autoDecision = decideAutoCompress(
788
920
  state,
789
921
  effectiveConfig,
@@ -809,22 +941,44 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
809
941
  }, ctx)
810
942
  }
811
943
  if (autoDecision.shouldFire && autoCandidate) {
944
+ const modelSummaryConfigured =
945
+ effectiveConfig.compress.autoCompress.summarizerModel.length > 0 ||
946
+ effectiveConfig.compress.autoCompress.summarizerFallbackModels.length > 0
812
947
  try {
813
948
  const autoOperationEpoch = state.sessionEpoch
814
- const largestSafeCandidate = candidate
815
- ? detectCompressionCandidate(prunedMessages, state, effectiveConfig, contextPercent)
816
- : detectEmergencyCompressionCandidate(prunedMessages, state, effectiveConfig, contextPercent, planningThreshold)
949
+ const rawFallbackCandidate = candidate ?? emergencyCompressionCandidate
950
+ const largestSafeCandidate = blockConsolidationCandidate
951
+ ? rawFallbackCandidate
952
+ : candidate
953
+ ? detectCompressionCandidate(
954
+ prunedMessages,
955
+ state,
956
+ effectiveConfig,
957
+ contextPercent,
958
+ progressPressureReached ? { allowCompressionBlocks: false } : undefined,
959
+ )
960
+ : detectEmergencyCompressionCandidate(
961
+ prunedMessages,
962
+ state,
963
+ effectiveConfig,
964
+ contextPercent,
965
+ planningThreshold,
966
+ { allowCompressionBlocks: false },
967
+ )
817
968
  let preparedProjection: any[] | undefined
969
+ const autoConfig = autoSummarizerDegraded
970
+ ? withoutAutoSummaryModels(effectiveConfig)
971
+ : effectiveConfig
818
972
  const autoResult = await createBudgetedAutoCompressionBlock({
819
973
  candidate: autoCandidate,
820
974
  topic: "Auto-compressed slice",
821
975
  state,
822
- config: effectiveConfig,
976
+ config: autoConfig,
823
977
  messages: prunedMessages,
824
978
  modelRegistry: (ctx as any).modelRegistry,
825
979
  signal: (ctx as any).signal,
826
980
  cwd: (ctx as any).cwd,
827
- requiredGainTokens: state.compressionProgress?.remainingTokens ?? planningRecoveryTokens,
981
+ requiredGainTokens: planningRecoveryTokens,
828
982
  allowPartialGain: true,
829
983
  persistState: (preparedState, publication) => persistJournalState(ctx, preparedState, publication),
830
984
  prepareProjection: (preparedState) => {
@@ -839,6 +993,21 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
839
993
  // failed or mutate/nudge the replacement session.
840
994
  return { messages: contextMessages }
841
995
  }
996
+ if (
997
+ modelSummaryConfigured &&
998
+ !autoSummarizerDegraded &&
999
+ autoResult.summaryMode === "programmatic_fallback"
1000
+ ) {
1001
+ // A successful deterministic commit can still prove that every
1002
+ // configured summary model failed for this live session. Avoid paying
1003
+ // the same model timeout on each subsequent autonomous attempt.
1004
+ autoSummarizerDegraded = true
1005
+ writeDcpDebugLog(effectiveConfig, "compress.auto_summarizer_degraded", {
1006
+ reason: "model-summary-fallback",
1007
+ summarizerAttempts: autoResult.summarizerAttempts,
1008
+ nextMode: "deterministic-extractive",
1009
+ }, ctx)
1010
+ }
842
1011
  // Re-apply pruning so the new block takes effect on this
843
1012
  // same context pass instead of the next one.
844
1013
  prunedMessages = preparedProjection ?? applyPruning(contextMessages, state, effectiveConfig)
@@ -859,46 +1028,71 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
859
1028
  clearedAnchors,
860
1029
  state: summarizeDcpState(state, effectiveConfig),
861
1030
  }, ctx)
862
- return finishContext("compress.auto", prunedMessages, {
1031
+ const projectedAfterAuto = prunedMessages.reduce(
1032
+ (sum, message) => sum + estimateMessageTokens(message),
1033
+ 0,
1034
+ )
1035
+ const stillAboveInputCapacity =
1036
+ budget.capacityExceeded && projectedAfterAuto > budget.inputCapacityTokens
1037
+ if (!stillAboveInputCapacity) return finishContext("compress.auto", prunedMessages, {
863
1038
  candidate: autoResult.effectiveCandidate,
864
1039
  messageCandidates,
865
1040
  contextPercent,
866
1041
  thresholds,
867
1042
  clearedAnchors,
868
1043
  })
1044
+
1045
+ // A positive partial commit is useful progress, but it is not permission
1046
+ // to send a request that still exceeds provider input capacity. Continue
1047
+ // through the model-independent recovery floor in this same context pass;
1048
+ // if that cannot make the request fit, the fail-closed handoff below wins.
1049
+ partialAutoProjectionTokens = projectedAfterAuto
1050
+ candidate = null
1051
+ emergencyCompressionCandidate = null
1052
+ if (emergencySettings.enabled) {
1053
+ emergencySelection = analyzeEmergencyCurrentTurn(prunedMessages, state, effectiveConfig)
1054
+ messageCandidates = emergencyCurrentTurnMessageCandidates(emergencySelection, effectiveConfig)
1055
+ }
1056
+ writeDcpDebugLog(effectiveConfig, "compress.auto_partial_capacity", {
1057
+ projectedAfterAuto,
1058
+ inputCapacityTokens: budget.inputCapacityTokens,
1059
+ remainingCapacityGap: projectedAfterAuto - budget.inputCapacityTokens,
1060
+ blockId: `b${autoResult.blockId}`,
1061
+ state: summarizeDcpState(state, effectiveConfig),
1062
+ }, ctx)
869
1063
  } catch (error) {
870
1064
  if (state.sessionEpoch !== contextEpoch || ctx.signal?.aborted) return { messages: contextMessages }
1065
+ diagnostic("auto-rejected", { reason: error instanceof AutoCompressionBlockedError ? error.blockedReason : "preparation-failed" })
871
1066
  const autoBlockedReason = error instanceof AutoCompressionBlockedError
872
1067
  ? error.blockedReason
873
1068
  : undefined
1069
+ const autoError = error instanceof Error ? error.message : String(error)
1070
+ autoCompressionFailure = { blockedReason: autoBlockedReason, error: autoError }
1071
+ const summarizerFailure = error instanceof AutoCompressionBlockedError && (
1072
+ error.blockedReason === "summarizer-unavailable" || error.summarizerFallbackUsed
1073
+ )
1074
+ if (modelSummaryConfigured && !autoSummarizerDegraded && summarizerFailure) {
1075
+ autoSummarizerDegraded = true
1076
+ writeDcpDebugLog(effectiveConfig, "compress.auto_summarizer_degraded", {
1077
+ reason: error.summarizerFallbackUsed ? "model-summary-fallback" : autoBlockedReason ?? "auto-failed",
1078
+ summarizerAttempts: error.summarizerAttempts,
1079
+ nextMode: "deterministic-extractive",
1080
+ }, ctx)
1081
+ }
874
1082
  writeDcpDebugLog(effectiveConfig, "compress.auto_failed", {
875
1083
  trigger: autoDecision.reason,
876
1084
  phase: autoBlockedReason ? "blocked" : "degraded",
877
1085
  blocked_reason: autoBlockedReason,
878
- error: error instanceof Error ? error.message : String(error),
1086
+ error: autoError,
1087
+ summarizerAttempts: error instanceof AutoCompressionBlockedError ? error.summarizerAttempts : undefined,
879
1088
  candidate: autoCandidate,
880
1089
  state: summarizeDcpState(state, effectiveConfig),
881
1090
  }, ctx)
882
- if (routineEscalated) warnProgress(ctx, autoBlockedReason ?? "auto-failed",
883
- `DCP automatic compression did not recover the context: ${autoBlockedReason ?? "preparation failed"}. The existing context was preserved.`)
884
- if (autoBlockedReason && budget.capacityExceeded) {
885
- state.progressRecovery = {
886
- blockedReason: autoBlockedReason,
887
- projectedBeforeTokens: budget.projectedBeforeTokens,
888
- inputCapacityTokens: budget.inputCapacityTokens,
889
- requiredSavingsTokens: budget.requiredSavingsTokens,
890
- contextWindow: budget.contextWindow,
891
- createdAt: Date.now(),
892
- }
893
- clearDcpNudgeAnchors(state)
894
- await persistJournalState(ctx, state)
895
- const abortSupported = typeof (ctx as any).abort === "function"
896
- if (abortSupported) (ctx as any).abort()
897
- return finishContext("progress.blocked_handoff", prunedMessages, {
898
- blocked_reason: autoBlockedReason,
899
- handoff: abortSupported ? "abort-current-agent-operation" : "abort-unavailable",
900
- budget,
901
- })
1091
+ warnProgress(ctx, autoBlockedReason ?? "auto-failed",
1092
+ `DCP automatic compression failed (${autoBlockedReason ?? "preparation failed"}). ${modelSummaryConfigured && autoSummarizerDegraded ? "Future attempts in this session will skip the summary model and use deterministic extraction. " : ""}The current context is preserved; emergency pruning will be attempted before any capacity abort.`)
1093
+ if (budget.capacityExceeded && emergencySettings.enabled) {
1094
+ emergencySelection = analyzeEmergencyCurrentTurn(prunedMessages, state, effectiveConfig)
1095
+ messageCandidates = emergencyCurrentTurnMessageCandidates(emergencySelection, effectiveConfig)
902
1096
  }
903
1097
  // Recoverable failures fall through to normal nudge emission.
904
1098
  }
@@ -914,7 +1108,7 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
914
1108
  if (
915
1109
  emergencySettings.enabled &&
916
1110
  emergencyPressureReached &&
917
- candidate === null &&
1111
+ (candidate === null || autoCompressionFailure !== undefined) &&
918
1112
  emergencySelection &&
919
1113
  emergencySelection.eligible.length > 0 &&
920
1114
  (hardEmergencyReached || emergencyPatienceExceeded)
@@ -974,6 +1168,55 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
974
1168
  }
975
1169
  }
976
1170
 
1171
+ if (budget.capacityExceeded && (autoCompressionFailure || partialAutoProjectionTokens !== undefined)) {
1172
+ const recoveredTokens = emergencyPruneResult?.estimatedTokensRecovered ?? 0
1173
+ const projectedAfterRecovery = prunedMessages.reduce(
1174
+ (sum, message) => sum + estimateMessageTokens(message),
1175
+ 0,
1176
+ )
1177
+ if (projectedAfterRecovery > budget.inputCapacityTokens) {
1178
+ const finalBlockedReason = autoCompressionFailure?.blockedReason ?? "budget-exhausted"
1179
+ const capacityGap = projectedAfterRecovery - budget.inputCapacityTokens
1180
+ state.progressRecovery = {
1181
+ blockedReason: finalBlockedReason,
1182
+ projectedBeforeTokens: projectedAfterRecovery,
1183
+ inputCapacityTokens: budget.inputCapacityTokens,
1184
+ requiredSavingsTokens: capacityGap,
1185
+ contextWindow: budget.contextWindow,
1186
+ createdAt: Date.now(),
1187
+ }
1188
+ clearDcpNudgeAnchors(state)
1189
+ await persistJournalState(ctx, state)
1190
+ warnProgress(
1191
+ ctx,
1192
+ finalBlockedReason,
1193
+ autoCompressionFailure
1194
+ ? `DCP recovery could not make the next request fit safely after auto-compression failed (${autoCompressionFailure.error}). ${projectedAfterRecovery} projected input tokens still exceed ${budget.inputCapacityTokens} input capacity by ${capacityGap}; the current agent operation is being stopped.`
1195
+ : `DCP partial auto-compression made progress but the next request still does not fit safely. ${projectedAfterRecovery} projected input tokens exceed ${budget.inputCapacityTokens} input capacity by ${capacityGap}; the current agent operation is being stopped instead of sending an oversized request.`,
1196
+ )
1197
+ const abortSupported = typeof (ctx as any).abort === "function"
1198
+ if (abortSupported) (ctx as any).abort()
1199
+ writeDcpDebugLog(effectiveConfig, "context.progress_handoff", {
1200
+ phase: "blocked",
1201
+ blocked_reason: finalBlockedReason,
1202
+ autoCompressionFailure,
1203
+ recoveredTokens,
1204
+ projectedAfterRecovery,
1205
+ handoff: abortSupported ? "abort-current-agent-operation" : "abort-unavailable",
1206
+ budget,
1207
+ state: summarizeDcpState(state, effectiveConfig),
1208
+ }, ctx)
1209
+ return finishContext("progress.blocked_handoff", prunedMessages, {
1210
+ blocked_reason: finalBlockedReason,
1211
+ autoCompressionFailure,
1212
+ recoveredTokens,
1213
+ projectedAfterRecovery,
1214
+ handoff: abortSupported ? "abort-current-agent-operation" : "abort-unavailable",
1215
+ budget,
1216
+ }, false)
1217
+ }
1218
+ }
1219
+
977
1220
  if (nudgeType && !manualEmergencyOnly && (hasCompressionSuggestion || emergencyPressureReached)) {
978
1221
  latestProviderOpportunityAvailable = latestProviderOpportunityKind !== undefined
979
1222
  const nudgeText = appendConcreteNudgeGuidance(
@@ -987,7 +1230,7 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
987
1230
  prunedMessages,
988
1231
  state,
989
1232
  nudgeType,
990
- { contextPercent, renderedReminder: nudgeText },
1233
+ { contextPercent, renderedReminder: nudgeText, freshToolResultIds: freshToolResults.eligibleIds },
991
1234
  )
992
1235
  if (anchorResult.anchor) {
993
1236
  if (anchorResult.updated) {
@@ -995,7 +1238,7 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
995
1238
  anchorResult.created ? "emitted" : "upgraded",
996
1239
  nudgeType,
997
1240
  anchorResult.anchor,
998
- usage,
1241
+ { ...usage, tokens: policyTokens, percent: contextPercent * 100 },
999
1242
  toolCallsSinceLastUser,
1000
1243
  )
1001
1244
  await persistJournalState(ctx, state)
@@ -1011,15 +1254,15 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
1011
1254
  "reapplied",
1012
1255
  anchorResult.anchor.type,
1013
1256
  anchorResult.anchor,
1014
- usage,
1257
+ { ...usage, tokens: policyTokens, percent: contextPercent * 100 },
1015
1258
  toolCallsSinceLastUser,
1016
1259
  )
1017
1260
  }
1018
1261
  } else {
1019
1262
  // Do not rewrite an already-sent user message and do not create an
1020
1263
  // ephemeral synthetic tail that disappears from the next provider
1021
- // continuation. The reminder is deferred until a fresh user tail; hard
1022
- // pressure is handled by the bounded emergency path above.
1264
+ // continuation. Wait for a fresh user or proven-local tool result;
1265
+ // hard pressure is handled by the bounded emergency path above.
1023
1266
  writeDcpDebugLog(effectiveConfig, "nudge.deferred_for_cache", {
1024
1267
  type: nudgeType,
1025
1268
  contextPercent,
@@ -1069,12 +1312,36 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
1069
1312
 
1070
1313
  // ── 10b. provider lifecycle evidence ─────────────────────────────────────
1071
1314
  pi.on("before_provider_request", async (event, ctx) => {
1315
+ // Consume grants even on failed, retried, omitted-result or disabled sends.
1316
+ // A completed-provider-response check here would be too late for caching.
1317
+ freshToolResults.reset()
1072
1318
  const effectiveConfig = configForContext(ctx)
1073
1319
  if (!effectiveConfig.enabled) {
1074
1320
  providerEvidenceTracker.reset()
1075
1321
  return undefined
1076
1322
  }
1323
+ if (journalBlockedReason) throw new DcpJournalError(`DCP journal is blocked: ${journalBlockedReason}`)
1324
+ if (isJournalSessionSupported() && lastProviderReadyProjectionEpoch !== state.sessionEpoch) {
1325
+ const message =
1326
+ `DCP blocked a provider request for session epoch ${state.sessionEpoch} because no provider-ready context projection ` +
1327
+ "has completed in this epoch. This can happen after a session lifecycle reset between context construction and provider send; " +
1328
+ "the raw session history will not be sent. Retry the turn so context can be rebuilt safely."
1329
+ warnProgress(ctx, "stale-context-projection", message)
1330
+ const abortSupported = typeof (ctx as any).abort === "function"
1331
+ writeDcpDebugLog(effectiveConfig, "provider_payload.blocked_stale_projection", {
1332
+ sessionEpoch: state.sessionEpoch,
1333
+ lastProviderReadyProjectionEpoch,
1334
+ handoff: abortSupported ? "abort-current-agent-operation" : "throw-before-provider-request",
1335
+ state: summarizeDcpState(state, effectiveConfig),
1336
+ }, ctx)
1337
+ if (abortSupported) {
1338
+ (ctx as any).abort()
1339
+ return undefined
1340
+ }
1341
+ throw new Error(message)
1342
+ }
1077
1343
 
1344
+ freshToolResults.beforeProviderRequest()
1078
1345
  const providerEvidence = collectProviderToolResultEvidence(event.payload)
1079
1346
  const pendingToolIds = new Set<string>()
1080
1347
  for (const meta of state.messageMetaSnapshot.values()) {
@@ -1099,6 +1366,8 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
1099
1366
  opportunityAvailable: reminderDelivered,
1100
1367
  opportunityKind: reminderDelivered ? latestProviderOpportunityKind : undefined,
1101
1368
  })
1369
+ routinePressureTracker.beforeRequest()
1370
+ diagnostic("request", { id: `${diagnosticEpoch}:${pending.lastAttemptId}`, reminder: reminderDelivered, snapshot: diagnosticSnapshot })
1102
1371
 
1103
1372
  writeDcpDebugLog(effectiveConfig, "provider_payload.message_ids", {
1104
1373
  injected: false,
@@ -1152,6 +1421,7 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
1152
1421
  model: typeof message.model === "string" ? message.model : undefined,
1153
1422
  stopReason: typeof message.stopReason === "string" ? message.stopReason : undefined,
1154
1423
  })
1424
+ routinePressureTracker.complete(message, completion.status === "promote")
1155
1425
  latestProviderOpportunityAvailable = false
1156
1426
 
1157
1427
  if (completion.status !== "promote") {
@@ -1213,6 +1483,8 @@ export default async function dcpModule(pi: ExtensionAPI, dependencies: { config
1213
1483
  state.providerSeenToolIds = workingState.providerSeenToolIds
1214
1484
  state.consecutiveIgnoredStrongNudges = workingState.consecutiveIgnoredStrongNudges
1215
1485
  state.consecutiveIgnoredNudges = workingState.consecutiveIgnoredNudges
1486
+ diagnostic("completion", { id: `${diagnosticEpoch}:${completion.lastAttemptId}`,
1487
+ reminder: ignoredCompressionOpportunity, ignored: state.consecutiveIgnoredNudges })
1216
1488
  writeDcpDebugLog(effectiveConfig, "provider_payload.tool_results_seen", {
1217
1489
  attempts: completion.attempts,
1218
1490
  newlySeenToolResults: newlySeen.length,