pi-ui-extend 1.0.42 → 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 (233) hide show
  1. package/README.md +41 -12
  2. package/dist/app/app.d.ts +10 -0
  3. package/dist/app/app.js +132 -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 +3 -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 +51 -34
  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 +14 -0
  17. package/dist/app/commands/reload-context-inventory.js +12 -1
  18. package/dist/app/input/autocomplete-controller.js +61 -34
  19. package/dist/app/input/input-action-controller.d.ts +2 -0
  20. package/dist/app/input/input-action-controller.js +8 -1
  21. package/dist/app/input/input-controller.d.ts +2 -1
  22. package/dist/app/input/input-controller.js +7 -2
  23. package/dist/app/input/prompt-enhancer-controller.js +53 -35
  24. package/dist/app/input/voice-controller.d.ts +41 -45
  25. package/dist/app/input/voice-controller.js +351 -419
  26. package/dist/app/model/model-usage-controller.js +17 -7
  27. package/dist/app/model/model-usage-status.d.ts +4 -1
  28. package/dist/app/model/model-usage-status.js +270 -46
  29. package/dist/app/popup/menu-items-controller.d.ts +13 -3
  30. package/dist/app/popup/menu-items-controller.js +37 -21
  31. package/dist/app/popup/popup-action-controller.d.ts +12 -2
  32. package/dist/app/popup/popup-action-controller.js +77 -24
  33. package/dist/app/popup/popup-menu-controller.d.ts +36 -14
  34. package/dist/app/popup/popup-menu-controller.js +239 -69
  35. package/dist/app/rendering/dcp-stats.d.ts +9 -4
  36. package/dist/app/rendering/dcp-stats.js +40 -425
  37. package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
  38. package/dist/app/rendering/popup-menu-renderer.js +42 -37
  39. package/dist/app/rendering/render-controller.js +23 -2
  40. package/dist/app/rendering/status-line-renderer.d.ts +5 -0
  41. package/dist/app/rendering/status-line-renderer.js +41 -5
  42. package/dist/app/rendering/tab-line-renderer.js +26 -20
  43. package/dist/app/runtime.d.ts +12 -1
  44. package/dist/app/runtime.js +120 -12
  45. package/dist/app/screen/mouse-controller.d.ts +2 -0
  46. package/dist/app/screen/mouse-controller.js +17 -7
  47. package/dist/app/screen/status-controller.d.ts +4 -0
  48. package/dist/app/screen/status-controller.js +5 -0
  49. package/dist/app/session/lazy-session-manager.js +34 -0
  50. package/dist/app/session/session-event-controller.d.ts +1 -0
  51. package/dist/app/session/session-event-controller.js +10 -1
  52. package/dist/app/session/session-history.d.ts +1 -0
  53. package/dist/app/session/session-history.js +12 -1
  54. package/dist/app/session/session-lifecycle-controller.d.ts +6 -1
  55. package/dist/app/session/session-lifecycle-controller.js +10 -1
  56. package/dist/app/session/tabs-controller.d.ts +25 -1
  57. package/dist/app/session/tabs-controller.js +442 -153
  58. package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
  59. package/dist/app/subagents/subagents-widget-controller.js +6 -0
  60. package/dist/app/types.d.ts +11 -1
  61. package/dist/app/workspace/workspace-actions-controller.js +10 -2
  62. package/dist/app/workspace/workspace-undo.d.ts +1 -0
  63. package/dist/app/workspace/workspace-undo.js +1 -0
  64. package/dist/bundled-extensions/question/index.js +9 -1
  65. package/dist/bundled-extensions/question/remote.d.ts +4 -0
  66. package/dist/bundled-extensions/question/remote.js +33 -0
  67. package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
  68. package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
  69. package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
  70. package/dist/bundled-extensions/telegram-connector/config.js +87 -0
  71. package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
  72. package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
  73. package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
  74. package/dist/bundled-extensions/telegram-connector/index.js +195 -0
  75. package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
  76. package/dist/bundled-extensions/terminal-bell/index.js +0 -76
  77. package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
  78. package/dist/bundled-extensions/workspace-undo/index.js +191 -0
  79. package/dist/config.d.ts +21 -2
  80. package/dist/config.js +159 -32
  81. package/dist/default-pix-config.js +20 -10
  82. package/dist/schemas/pi-tools-suite-schema.d.ts +8 -4
  83. package/dist/schemas/pi-tools-suite-schema.js +11 -5
  84. package/dist/schemas/pix-schema.d.ts +12 -2
  85. package/dist/schemas/pix-schema.js +37 -9
  86. package/dist/schemas/tasks-schema.js +1 -1
  87. package/docs/concurrency.md +3 -1
  88. package/docs/desktop-mvp.md +10 -5
  89. package/docs/desktop-task-manager.md +81 -28
  90. package/docs/release.md +30 -6
  91. package/external/pi-tools-suite/README.md +165 -39
  92. package/external/pi-tools-suite/docs/session-recovery.md +7 -0
  93. package/external/pi-tools-suite/docs/subagent-model-pools.md +12 -5
  94. package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
  95. package/external/pi-tools-suite/package.json +6 -1
  96. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
  97. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
  98. package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
  99. package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
  100. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
  101. package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
  102. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
  103. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
  104. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
  105. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
  106. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
  107. package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
  108. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
  109. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
  110. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
  111. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
  112. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
  113. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
  114. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
  115. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
  116. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
  117. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
  118. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
  119. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
  120. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
  121. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
  122. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
  123. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
  124. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
  125. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
  126. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
  127. package/external/pi-tools-suite/src/async-subagents/commands.ts +1 -1
  128. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  129. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +0 -2
  130. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
  131. package/external/pi-tools-suite/src/async-subagents/core/config.ts +40 -17
  132. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
  133. package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
  134. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +7 -4
  135. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +34 -24
  136. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
  137. package/external/pi-tools-suite/src/async-subagents/lib.ts +9 -1
  138. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +0 -1
  139. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +2 -2
  140. package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
  141. package/external/pi-tools-suite/src/config.ts +14 -0
  142. package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
  143. package/external/pi-tools-suite/src/context-gateway/config.ts +66 -1
  144. package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
  145. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +123 -3
  146. package/external/pi-tools-suite/src/context-gateway/index.ts +137 -4
  147. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +5 -5
  148. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +2 -2
  149. package/external/pi-tools-suite/src/context-gateway/types.ts +8 -1
  150. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
  151. package/external/pi-tools-suite/src/dcp/auto-compress.ts +41 -14
  152. package/external/pi-tools-suite/src/dcp/commands.ts +14 -156
  153. package/external/pi-tools-suite/src/dcp/compress-tool.ts +8 -2
  154. package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
  155. package/external/pi-tools-suite/src/dcp/config.ts +58 -4
  156. package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
  157. package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
  158. package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
  159. package/external/pi-tools-suite/src/dcp/index.ts +338 -66
  160. package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
  161. package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
  162. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +92 -2
  163. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
  164. package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
  165. package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
  166. package/external/pi-tools-suite/src/dcp/state.ts +9 -0
  167. package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
  168. package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
  169. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +68 -16
  170. package/external/pi-tools-suite/src/lib/project.ts +36 -1
  171. package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
  172. package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
  173. package/external/pi-tools-suite/src/resource-registry/index.ts +282 -21
  174. package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
  175. package/external/pi-tools-suite/src/todo/index.ts +21 -0
  176. package/external/pi-tools-suite/src/todo/todo.ts +19 -1
  177. package/external/pi-tools-suite/src/tool-descriptions.ts +44 -15
  178. package/package.json +7 -8
  179. package/schemas/pi-tools-suite.json +44 -12
  180. package/schemas/pix.json +109 -15
  181. package/schemas/tasks.json +1 -2
  182. package/skills/simplify/SKILL.md +33 -5
  183. package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -196
  184. package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
  185. package/skills/playwright-cli/SKILL.md +0 -420
  186. package/skills/playwright-cli/references/element-attributes.md +0 -23
  187. package/skills/playwright-cli/references/playwright-tests.md +0 -50
  188. package/skills/playwright-cli/references/request-mocking.md +0 -87
  189. package/skills/playwright-cli/references/running-code.md +0 -241
  190. package/skills/playwright-cli/references/session-management.md +0 -273
  191. package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
  192. package/skills/playwright-cli/references/storage-state.md +0 -290
  193. package/skills/playwright-cli/references/test-generation.md +0 -142
  194. package/skills/playwright-cli/references/tracing.md +0 -154
  195. package/skills/playwright-cli/references/video-recording.md +0 -147
  196. package/skills/spec-wiki/SKILL.md +0 -55
  197. package/skills/spec-wiki/evals/evals.json +0 -132
  198. package/skills/spec-wiki/evals/files/bootstrap/architecture/rfc-007.md +0 -14
  199. package/skills/spec-wiki/evals/files/bootstrap/docs/how-to-pay.md +0 -4
  200. package/skills/spec-wiki/evals/files/bootstrap/docs/overview.md +0 -9
  201. package/skills/spec-wiki/evals/files/bootstrap/requirements/payment-behavior.md +0 -17
  202. package/skills/spec-wiki/evals/files/bootstrap/src/payments.ts +0 -12
  203. package/skills/spec-wiki/evals/files/bootstrap/tests/payments.test.ts +0 -11
  204. package/skills/spec-wiki/evals/files/focused/src/session.ts +0 -5
  205. package/skills/spec-wiki/evals/files/focused/tests/session.test.ts +0 -17
  206. package/skills/spec-wiki/evals/files/maintenance/low-signal/incoming/capture.txt +0 -2
  207. package/skills/spec-wiki/evals/files/maintenance/low-signal/src/capture.ts +0 -3
  208. package/skills/spec-wiki/evals/files/maintenance/move/docs/old-job-contract.md +0 -18
  209. package/skills/spec-wiki/evals/files/maintenance/move/src/job.ts +0 -5
  210. package/skills/spec-wiki/evals/files/maintenance/new-relation/docs/session-refresh.md +0 -24
  211. package/skills/spec-wiki/evals/files/maintenance/new-relation/incoming/refresh-worker.ts +0 -7
  212. package/skills/spec-wiki/evals/files/maintenance/new-relation/src/session.ts +0 -7
  213. package/skills/spec-wiki/evals/files/maintenance/no-impact/docs/auth-contract.md +0 -17
  214. package/skills/spec-wiki/evals/files/maintenance/no-impact/incoming/telemetry-format.ts +0 -3
  215. package/skills/spec-wiki/evals/files/maintenance/no-impact/src/auth.ts +0 -3
  216. package/skills/spec-wiki/evals/files/query/docs/how-to-run-jobs.md +0 -4
  217. package/skills/spec-wiki/evals/files/query/docs/spec-overview.md +0 -9
  218. package/skills/spec-wiki/evals/files/query/features/jobs/cancellation-contract.md +0 -15
  219. package/skills/spec-wiki/evals/files/query/features/jobs/retry-spec.md +0 -6
  220. package/skills/spec-wiki/evals/maintenance_regression.py +0 -311
  221. package/skills/spec-wiki/evals/retrieval-eval.md +0 -37
  222. package/skills/spec-wiki/evals/retrieval-evals.json +0 -37
  223. package/skills/spec-wiki/evals/trigger-evals.json +0 -94
  224. package/skills/spec-wiki/references/bootstrap.md +0 -100
  225. package/skills/spec-wiki/references/focused-spec.md +0 -117
  226. package/skills/spec-wiki/references/index-schema.md +0 -330
  227. package/skills/spec-wiki/references/maintenance.md +0 -134
  228. package/skills/spec-wiki/references/query.md +0 -74
  229. package/skills/spec-wiki/scripts/spec_wiki.py +0 -2076
  230. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
  231. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
  232. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
  233. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
@@ -1,10 +1,17 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { classifyShellCommand, type ShellCommandClassification } from "../shell-command-policy.js";
3
4
 
5
+ import {
6
+ contextGatewayAccountingLogDrain,
7
+ contextGatewayAccountingLogEnabled,
8
+ writeContextGatewayAccountingLog,
9
+ } from "./accounting-log.js";
4
10
  import {
5
11
  contextGatewayBudgetForClass,
6
12
  loadContextGatewayConfig,
7
13
  } from "./config.js";
14
+ import { ContextGatewayEfficiencyTracker } from "./efficiency.js";
8
15
  import { planContextGatewayEnforcement } from "./enforcement.js";
9
16
  import { classifyContextGatewayTool, ContextGatewayTelemetry } from "./telemetry.js";
10
17
  import type {
@@ -18,20 +25,25 @@ import type {
18
25
  export interface RegisterContextGatewayOptions {
19
26
  loadConfig?: () => ContextGatewayResolvedConfig;
20
27
  telemetry?: ContextGatewayTelemetry;
28
+ efficiency?: ContextGatewayEfficiencyTracker;
21
29
  }
22
30
 
23
31
  export interface RegisteredContextGateway extends ContextGatewayRuntimeState {
32
+ readonly efficiency: ContextGatewayEfficiencyTracker;
24
33
  setRuntimeMode(mode: ContextGatewayMode): { ok: boolean; message: string };
25
34
  }
26
35
 
27
36
  function formatStatus(runtime: RegisteredContextGateway): string {
28
37
  const telemetry = runtime.telemetry.snapshot();
38
+ const efficiency = runtime.efficiency.snapshot();
29
39
  return [
30
40
  `Context Gateway requested=${runtime.requestedMode}, effective=${runtime.effectiveMode}.`,
31
41
  `Observed results=${telemetry.results}, errors=${telemetry.errors}, upstreamTruncated=${telemetry.upstreamTruncatedResults}, overBudget=${telemetry.overBudgetResults}, enforced=${telemetry.enforcedResults}, unbound results=${telemetry.unboundResults}.`,
32
42
  `Repeat exact reads=${telemetry.repeatCandidateCount}, same-source different-range reads=${telemetry.sameSourceDifferentRangeCount}, retrieval calls=${telemetry.retrievalCalls}.`,
33
43
  `Native policy results=${telemetry.nativePolicy.results}, refusals=${telemetry.nativePolicy.refusals}, full overrides=${telemetry.nativePolicy.fullOverrides}.`,
34
44
  `Source contentBytes=${telemetry.contentBytes}, deliveredContentBytes=${telemetry.deliveredContentBytes}, actualBytesSaved=${telemetry.actualBytesSaved}, potentialBytesOverBudget=${telemetry.potentialBytesOverBudget}, detailsBytes=${telemetry.detailsBytes}.`,
45
+ `Efficiency gross≈${efficiency.grossEstimatedTokensSaved} tokens/${efficiency.grossBytesSaved} bytes, retrieval tax≈${efficiency.retrievalEstimatedTokens} tokens/${efficiency.retrievalBytes} bytes, conservative net≈${efficiency.conservativeNetEstimatedTokens} tokens/${efficiency.conservativeNetBytes} bytes.`,
46
+ `Provider attempts=${efficiency.providerAttempts}, completions=${efficiency.providerCompletions}, input=${efficiency.providerInputTokens}, cacheRead=${efficiency.providerCacheReadTokens}, cacheWrite=${efficiency.providerCacheWriteTokens}, output=${efficiency.providerOutputTokens}, total=${efficiency.providerTotalTokens}.`,
35
47
  ].join("\n");
36
48
  }
37
49
 
@@ -39,11 +51,12 @@ function formatDoctor(runtime: RegisteredContextGateway): string {
39
51
  const lines = [
40
52
  `Context Gateway doctor: requested=${runtime.requestedMode}, effective=${runtime.effectiveMode}.`,
41
53
  "Budgets: read=maxExactReadBytes; repo search/AST/structure=maxSearchBytes; other results=maxResultBytes; compact inline view=maxInlineBytes.",
42
- "Enforce: recognised complete simple test/build output=bounded parser compact; partial/unknown/compound/upstream-truncated/non-text=passthrough.",
54
+ "Enforce: recognised complete simple test/build output=bounded parser compact; over-budget web_search/web_fetch with structured raw details=bounded recoverable compact; unsupported/unsafe results remain passthrough.",
43
55
  "Read: full passthrough even when over budget; exact recovery/archive is not wired, so irreversible read truncation is disabled.",
44
56
  "Repo tools: producer-native compact is supported with repoDiscovery.profile=native-compact; Gateway never post-hoc slices repo output.",
45
57
  "External paths: MCP=current Pix adapter unsupported; direct parent browser capture=unsupported.",
46
58
  "Storage: disabled; no durable archive, index, quota reservation, or retrieval tools are active.",
59
+ `Accounting log: ${contextGatewayAccountingLogEnabled(runtime.config) ? "enabled" : "disabled"}; scalar-only JSONL with bounded rotation, no result bodies, paths, or tool arguments.`,
47
60
  ];
48
61
  for (const issue of runtime.config.issues) lines.push(`Config issue: ${issue}`);
49
62
  return lines.join("\n");
@@ -59,14 +72,48 @@ export function registerContextGateway(
59
72
  ): RegisteredContextGateway {
60
73
  const config = (options.loadConfig ?? (() => loadContextGatewayConfig()))();
61
74
  const telemetry = options.telemetry ?? new ContextGatewayTelemetry();
75
+ const efficiency = options.efficiency ?? new ContextGatewayEfficiencyTracker();
62
76
  let requestedMode = config.mode;
63
77
  let effectiveMode: ContextGatewayEffectiveMode = requestedMode;
64
78
  type InFlight = { toolClass: ContextGatewayToolClass; shell: ShellCommandClassification };
65
79
  const inFlightToolCalls = new Map<string, InFlight>();
80
+ const runtimeId = randomUUID();
81
+ let accountingEpoch = 0;
82
+ let accountingEpochStartedAt = Date.now();
83
+ let accountingEpochActive = false;
84
+
85
+ const writeAccounting = (event: string, details: Record<string, unknown> = {}): void => {
86
+ writeContextGatewayAccountingLog(config, event, {
87
+ runtimeId,
88
+ sessionEpoch: accountingEpoch,
89
+ mode: effectiveMode,
90
+ ...details,
91
+ });
92
+ };
93
+
94
+ const accountingSummary = (): Record<string, unknown> => ({
95
+ durationMs: Math.max(0, Date.now() - accountingEpochStartedAt),
96
+ totals: efficiency.snapshot(),
97
+ });
98
+
99
+ const finalizeAccountingEpoch = (reason: string): void => {
100
+ if (!accountingEpochActive || effectiveMode === "off") return;
101
+ writeAccounting("session.summary", { reason, ...accountingSummary() });
102
+ accountingEpochActive = false;
103
+ };
104
+
105
+ const beginAccountingEpoch = (reason: string): void => {
106
+ efficiency.reset();
107
+ accountingEpoch += 1;
108
+ accountingEpochStartedAt = Date.now();
109
+ accountingEpochActive = effectiveMode !== "off";
110
+ if (accountingEpochActive) writeAccounting("session.start", { reason });
111
+ };
66
112
 
67
113
  const resetRuntimeTelemetry = (): void => {
68
114
  inFlightToolCalls.clear();
69
115
  telemetry.reset();
116
+ efficiency.clearTransientBindings();
70
117
  };
71
118
 
72
119
  const runtime: RegisteredContextGateway = {
@@ -74,6 +121,7 @@ export function registerContextGateway(
74
121
  get effectiveMode() { return effectiveMode; },
75
122
  config,
76
123
  telemetry,
124
+ efficiency,
77
125
  setRuntimeMode(mode) {
78
126
  if (mode !== effectiveMode && inFlightToolCalls.size > 0) {
79
127
  return {
@@ -81,9 +129,21 @@ export function registerContextGateway(
81
129
  message: `Context Gateway mode change requires a safe boundary; ${inFlightToolCalls.size} tool call(s) are still in flight.`,
82
130
  };
83
131
  }
132
+ const previousMode = effectiveMode;
133
+ if (previousMode !== mode) finalizeAccountingEpoch("mode-change");
84
134
  requestedMode = mode;
85
135
  effectiveMode = mode;
86
136
  telemetry.clearTransientBindings();
137
+ efficiency.clearTransientBindings();
138
+ if (previousMode !== mode) {
139
+ writeContextGatewayAccountingLog(config, "mode.change", {
140
+ runtimeId,
141
+ sessionEpoch: accountingEpoch,
142
+ from: previousMode,
143
+ to: mode,
144
+ });
145
+ beginAccountingEpoch("mode-change");
146
+ }
87
147
  return { ok: true, message: `Context Gateway runtime mode set to ${mode}; config was not changed.` };
88
148
  },
89
149
  };
@@ -98,7 +158,11 @@ export function registerContextGateway(
98
158
  : { scope: "unknown", kind: "unknown" },
99
159
  });
100
160
  }
101
- if (effectiveMode === "observe" || effectiveMode === "enforce") telemetry.recordToolCall(event);
161
+ if (effectiveMode === "observe" || effectiveMode === "enforce") {
162
+ telemetry.recordToolCall(event);
163
+ const accountingCall = efficiency.recordToolCall(event);
164
+ if (accountingCall) writeAccounting("tool.call", { ...accountingCall });
165
+ }
102
166
  return undefined;
103
167
  });
104
168
 
@@ -114,6 +178,7 @@ export function registerContextGateway(
114
178
  const enforcement = effectiveMode === "enforce" && binding
115
179
  ? planContextGatewayEnforcement({
116
180
  event,
181
+ toolCallId: typeof event.toolCallId === "string" ? event.toolCallId : undefined,
117
182
  toolClass: binding.toolClass,
118
183
  shell: binding.shell,
119
184
  budgetBytes: classBudget,
@@ -123,7 +188,7 @@ export function registerContextGateway(
123
188
  ),
124
189
  })
125
190
  : undefined;
126
- telemetry.recordToolResult(event, config.budgets, {
191
+ const observation = telemetry.recordToolResult(event, config.budgets, {
127
192
  maxInlineBytes: config.budgets.maxInlineBytes,
128
193
  ...(enforcement ? {
129
194
  delivery: {
@@ -133,7 +198,26 @@ export function registerContextGateway(
133
198
  },
134
199
  } : {}),
135
200
  });
136
- if (enforcement?.representation === "test-build-compact") {
201
+ const accountingResult = efficiency.recordToolResult(
202
+ event,
203
+ observation,
204
+ enforcement?.content ?? event.content,
205
+ );
206
+ if (accountingResult) {
207
+ const efficiencySnapshot = efficiency.snapshot();
208
+ writeAccounting("tool.result", {
209
+ ...accountingResult,
210
+ cumulative: {
211
+ grossBytesSaved: efficiencySnapshot.grossBytesSaved,
212
+ grossEstimatedTokensSaved: efficiencySnapshot.grossEstimatedTokensSaved,
213
+ retrievalBytes: efficiencySnapshot.retrievalBytes,
214
+ retrievalEstimatedTokens: efficiencySnapshot.retrievalEstimatedTokens,
215
+ conservativeNetBytes: efficiencySnapshot.conservativeNetBytes,
216
+ conservativeNetEstimatedTokens: efficiencySnapshot.conservativeNetEstimatedTokens,
217
+ },
218
+ });
219
+ }
220
+ if (enforcement && enforcement.representation !== "passthrough") {
137
221
  const originalDetails = event.details && typeof event.details === "object" && !Array.isArray(event.details)
138
222
  ? event.details as Record<string, unknown>
139
223
  : {};
@@ -156,18 +240,58 @@ export function registerContextGateway(
156
240
  return undefined;
157
241
  });
158
242
 
243
+ pi.on("before_provider_request", async (_event, ctx) => {
244
+ if (effectiveMode === "off") return undefined;
245
+ const attempt = efficiency.recordProviderAttempt();
246
+ const model = (ctx as any)?.model;
247
+ writeAccounting("provider.request", {
248
+ attempt,
249
+ provider: typeof model?.provider === "string" ? model.provider : undefined,
250
+ model: typeof model?.id === "string" ? model.id : undefined,
251
+ });
252
+ return undefined;
253
+ });
254
+
255
+ pi.on("message_end", async (event) => {
256
+ if (effectiveMode === "off" || event.message?.role !== "assistant") return;
257
+ const completion = efficiency.recordProviderCompletion(event.message as any);
258
+ const snapshot = efficiency.snapshot();
259
+ writeAccounting("provider.completion", {
260
+ ...completion,
261
+ cumulative: {
262
+ providerInputTokens: snapshot.providerInputTokens,
263
+ providerOutputTokens: snapshot.providerOutputTokens,
264
+ providerCacheReadTokens: snapshot.providerCacheReadTokens,
265
+ providerCacheWriteTokens: snapshot.providerCacheWriteTokens,
266
+ providerTotalTokens: snapshot.providerTotalTokens,
267
+ providerCost: snapshot.providerCost,
268
+ grossEstimatedTokensSaved: snapshot.grossEstimatedTokensSaved,
269
+ retrievalEstimatedTokens: snapshot.retrievalEstimatedTokens,
270
+ conservativeNetEstimatedTokens: snapshot.conservativeNetEstimatedTokens,
271
+ },
272
+ });
273
+ });
274
+
159
275
  pi.on("session_start", async () => {
276
+ finalizeAccountingEpoch("session-start-reset");
160
277
  resetRuntimeTelemetry();
278
+ beginAccountingEpoch("session-start");
161
279
  return undefined;
162
280
  });
163
281
 
164
282
  pi.on("session_tree", async () => {
283
+ finalizeAccountingEpoch("session-tree");
165
284
  resetRuntimeTelemetry();
285
+ beginAccountingEpoch("session-tree");
166
286
  return undefined;
167
287
  });
168
288
 
169
289
  pi.on("session_shutdown", async () => {
290
+ finalizeAccountingEpoch("session-shutdown");
170
291
  resetRuntimeTelemetry();
292
+ efficiency.reset();
293
+ accountingEpochActive = false;
294
+ await contextGatewayAccountingLogDrain();
171
295
  return undefined;
172
296
  });
173
297
 
@@ -206,7 +330,16 @@ export default function contextGatewayExtension(pi: ExtensionAPI): void {
206
330
  }
207
331
 
208
332
  export { accountContextGatewayParts } from "./accounting.js";
333
+ export {
334
+ contextGatewayAccountingLogDrain,
335
+ contextGatewayAccountingLogEnabled,
336
+ contextGatewayAccountingLogMaxBackups,
337
+ contextGatewayAccountingLogMaxBytes,
338
+ contextGatewayAccountingLogPath,
339
+ writeContextGatewayAccountingLog,
340
+ } from "./accounting-log.js";
209
341
  export { loadContextGatewayConfig } from "./config.js";
342
+ export { ContextGatewayEfficiencyTracker, estimateContextGatewayTokens } from "./efficiency.js";
210
343
  export { normalizeRedundantTruncationMetadata } from "./metadata-normalization.js";
211
344
  export { STORELESS_CAPABILITIES, storelessCapability } from "./storeless-capabilities.js";
212
345
  export { parseTestBuildOutput, planProspectiveTestOutputDelivery } from "./test-output-parser.js";
@@ -1,5 +1,5 @@
1
1
  export type StorelessCapabilityStatus = "supported" | "limited" | "unsupported";
2
- export type StorelessCapabilityStrategy = "native-passthrough" | "metadata-only" | "pure-parser-candidate" | "pure-parser-compact" | "none";
2
+ export type StorelessCapabilityStrategy = "native-passthrough" | "metadata-only" | "pure-parser-candidate" | "pure-parser-compact" | "raw-session-recoverable-compact" | "none";
3
3
 
4
4
  export interface StorelessCapabilityEntry {
5
5
  surface:
@@ -13,7 +13,7 @@ export interface StorelessCapabilityEntry {
13
13
  | "mcp-direct";
14
14
  status: StorelessCapabilityStatus;
15
15
  strategy: StorelessCapabilityStrategy;
16
- lifetime: "current-result" | "producer-managed" | "visual-message" | "none";
16
+ lifetime: "current-result" | "producer-managed" | "raw-session" | "visual-message" | "none";
17
17
  reason: string;
18
18
  }
19
19
 
@@ -41,9 +41,9 @@ export const STORELESS_CAPABILITIES: readonly StorelessCapabilityEntry[] = Objec
41
41
  {
42
42
  surface: "web-document",
43
43
  status: "limited",
44
- strategy: "native-passthrough",
45
- lifetime: "current-result",
46
- reason: "Existing web tools own fetch, URL/provider metadata and truncation. Storeless Gateway does not refetch, archive, or claim stable pagination of omitted content.",
44
+ strategy: "raw-session-recoverable-compact",
45
+ lifetime: "raw-session",
46
+ reason: "In enforce mode, over-budget web_search/web_fetch results are compacted only when the producer supplied full structured details. Those raw details remain in the session toolResult and are recoverable by toolCallId through session-recovery; unsupported web shapes remain passthrough.",
47
47
  },
48
48
  {
49
49
  surface: "structured-json",
@@ -254,7 +254,7 @@ export function observeToolResult(
254
254
  toolClass?: ContextGatewayToolClass;
255
255
  shellCommandScope?: "simple" | "compound" | "unknown";
256
256
  maxInlineBytes?: number;
257
- delivery?: { representation: "passthrough" | "test-build-compact"; contentBytes: number; textBytes: number };
257
+ delivery?: { representation: "passthrough" | "test-build-compact" | "web-recoverable-compact"; contentBytes: number; textBytes: number };
258
258
  } = {},
259
259
  ): ContextGatewayObservation {
260
260
  const toolName = typeof event.toolName === "string" ? event.toolName : "unknown";
@@ -367,7 +367,7 @@ export class ContextGatewayTelemetry {
367
367
  recordToolResult(
368
368
  event: ToolResultLike,
369
369
  budget: number | ContextGatewayBudgets,
370
- options: { delivery?: { representation: "passthrough" | "test-build-compact"; contentBytes: number; textBytes: number }; maxInlineBytes?: number } = {},
370
+ options: { delivery?: { representation: "passthrough" | "test-build-compact" | "web-recoverable-compact"; contentBytes: number; textBytes: number }; maxInlineBytes?: number } = {},
371
371
  ): ContextGatewayObservation {
372
372
  const binding = typeof event.toolCallId === "string" ? this.bindings.get(event.toolCallId) : undefined;
373
373
  const observation = observeToolResult(event, budget, {
@@ -10,10 +10,17 @@ export interface ContextGatewayBudgets {
10
10
  maxSearchMatches: number;
11
11
  }
12
12
 
13
+ export interface ContextGatewayAccountingLogConfig {
14
+ enabled: boolean;
15
+ maxBytes: number;
16
+ maxBackups: number;
17
+ }
18
+
13
19
  /** Parsed layered config. `enforce` is selective and fail-open for unsupported result classes. */
14
20
  export interface ContextGatewayResolvedConfig {
15
21
  mode: ContextGatewayMode;
16
22
  budgets: ContextGatewayBudgets;
23
+ accountingLog: ContextGatewayAccountingLogConfig;
17
24
  issues: string[];
18
25
  }
19
26
 
@@ -48,7 +55,7 @@ export interface ContextGatewayObservedView {
48
55
  }
49
56
 
50
57
  export interface ContextGatewayObservedDelivery {
51
- representation: "passthrough" | "test-build-compact";
58
+ representation: "passthrough" | "test-build-compact" | "web-recoverable-compact";
52
59
  contentBytes: number;
53
60
  textBytes: number;
54
61
  }
@@ -5,13 +5,32 @@ import {
5
5
  type AutoCompressionResult,
6
6
  type CreateAutoCompressionBlockOptions,
7
7
  } from "./auto-compress.js";
8
- import { canonicalMessageHash } from "./conversation-index.js";
8
+ import { buildExactRangeMembership, canonicalMessageHash } from "./conversation-index.js";
9
9
  import { stableMessageKeys } from "./pruner-message-ids.js";
10
10
  import type { CompressionCandidate } from "./pruner-types.js";
11
11
  import type { DcpState } from "./state.js";
12
12
 
13
13
  const rejectedSources = new WeakMap<DcpState, { key: string; error: AutoCompressionBlockedError }>();
14
14
  const FINALIZATION_GRACE_MS = 1_000;
15
+ const SUMMARY_DEADLINE_FRACTION = 0.75;
16
+
17
+ function exactCandidateFingerprint(
18
+ options: CreateAutoCompressionBlockOptions,
19
+ candidate: CompressionCandidate,
20
+ ): { startId: string; endId: string; sourceMembers: Array<{ stableId: string; hash: string }> } | undefined {
21
+ const membership = buildExactRangeMembership(
22
+ options.state.conversationIndexSnapshot,
23
+ candidate.startId,
24
+ candidate.endId,
25
+ options.state,
26
+ );
27
+ if (!membership) return undefined;
28
+ return {
29
+ startId: candidate.startId,
30
+ endId: candidate.endId,
31
+ sourceMembers: membership.sourceMembers,
32
+ };
33
+ }
15
34
 
16
35
  /**
17
36
  * A source-size estimate is not a net-savings estimate. Try the economical
@@ -25,14 +44,28 @@ export async function createBudgetedAutoCompressionBlock(
25
44
  ): Promise<AutoCompressionResult> {
26
45
  options.signal?.throwIfAborted();
27
46
  const epoch = options.state.sessionEpoch;
47
+ const primaryFingerprint = exactCandidateFingerprint(options, options.candidate);
48
+ const largestFingerprint = largestSafeCandidate
49
+ ? exactCandidateFingerprint(options, largestSafeCandidate)
50
+ : undefined;
51
+ // Rejection memoization belongs to the exact source that was rejected, not
52
+ // to the whole provider tail. A long-running agent can append unrelated
53
+ // messages while the oldest safe candidate stays byte-identical; hashing the
54
+ // whole tail made that same impossible candidate run again after every tool
55
+ // result. Fall back to the full-context identity only when exact membership
56
+ // is unavailable, so memoization never guesses source equivalence.
57
+ const sourceIdentity = primaryFingerprint && (!largestSafeCandidate || largestFingerprint)
58
+ ? { primary: primaryFingerprint, largest: largestFingerprint }
59
+ : {
60
+ source: options.messages.map(canonicalMessageHash),
61
+ ids: stableMessageKeys(options.messages),
62
+ };
28
63
  const key = createHash("sha256").update(JSON.stringify({
29
64
  epoch,
30
- source: options.messages.map(canonicalMessageHash),
31
- ids: stableMessageKeys(options.messages),
65
+ sourceIdentity,
32
66
  config: options.config,
33
67
  budget: options.requiredGainTokens ?? 0,
34
68
  allowPartialGain: options.allowPartialGain === true,
35
- largest: largestSafeCandidate,
36
69
  })).digest("hex");
37
70
  const rejected = rejectedSources.get(options.state);
38
71
  if (rejected?.key === key) throw rejected.error;
@@ -41,6 +74,7 @@ export async function createBudgetedAutoCompressionBlock(
41
74
  const onAbort = () => controller.abort(options.signal?.reason);
42
75
  options.signal?.addEventListener("abort", onAbort, { once: true });
43
76
  const timeout = Math.max(1, options.config.compress.autoCompress.timeoutMs);
77
+ const summaryTimeoutMs = Math.max(1, Math.floor(timeout * SUMMARY_DEADLINE_FRACTION));
44
78
  // generateModelSummary owns the configured summarizer deadline and can
45
79
  // deliberately recover from it with the deterministic extractive fallback.
46
80
  // Keep a small bounded tail for that fallback and durable publication;
@@ -84,6 +118,7 @@ export async function createBudgetedAutoCompressionBlock(
84
118
  ...options,
85
119
  candidate: candidates[attempt]!,
86
120
  signal: controller.signal,
121
+ summaryTimeoutMs,
87
122
  });
88
123
  rejectedSources.delete(options.state);
89
124
  return result;
@@ -16,7 +16,7 @@ import { createHash } from "node:crypto"
16
16
  import type { Model, Api, ProviderHeaders } from "@earendil-works/pi-ai"
17
17
  import { completeWithModelRegistry, type ModelCompletionRegistry } from "../model-completion.js"
18
18
  import type { DcpState } from "./state.js"
19
- import type { DcpConfig } from "./config.js"
19
+ import { summarizerModelRefs, type DcpConfig } from "./config.js"
20
20
  import type { CompressionCandidate } from "./pruner-types.js"
21
21
  import { estimateMessageTokens, estimateTokens, stripStaleDcpMetadataLines } from "./pruner-metadata.js"
22
22
  import {
@@ -37,11 +37,19 @@ import { settleCompressionProgress } from "./compression-progress.js"
37
37
 
38
38
  export class AutoCompressionBlockedError extends Error {
39
39
  readonly blockedReason: DcpBlockedReason
40
-
41
- constructor(blockedReason: DcpBlockedReason, message: string) {
40
+ readonly summarizerFallbackUsed: boolean
41
+ readonly summarizerAttempts?: ModelSummaryAttempt[]
42
+
43
+ constructor(
44
+ blockedReason: DcpBlockedReason,
45
+ message: string,
46
+ details: { summarizerFallbackUsed?: boolean; summarizerAttempts?: ModelSummaryAttempt[] } = {},
47
+ ) {
42
48
  super(message)
43
49
  this.name = "AutoCompressionBlockedError"
44
50
  this.blockedReason = blockedReason
51
+ this.summarizerFallbackUsed = details.summarizerFallbackUsed === true
52
+ this.summarizerAttempts = details.summarizerAttempts
45
53
  }
46
54
  }
47
55
 
@@ -828,12 +836,12 @@ export async function prepareCompressionSummary(options: {
828
836
  fallbackCandidate,
829
837
  sourceManifest,
830
838
  )
831
- if (!modelResult?.text && estimateTokens(text) > 8192) {
832
- throw new AutoCompressionBlockedError(
833
- "budget-exhausted",
834
- "Extractive continuity minimum exceeds its 8192-token budget; no checkpoints were silently dropped",
835
- )
836
- }
839
+ // Do not impose a second, fixed output ceiling on the deterministic safety
840
+ // floor. A large source can legitimately require more than 8192 tokens to
841
+ // preserve every explicit continuity checkpoint. The auto-compression path
842
+ // already measures the actual replacement against the exact source and
843
+ // rejects non-positive/full-projection gain below, so compression economics
844
+ // — not an unrelated constant — decide whether this fallback is usable.
837
845
  return {
838
846
  text,
839
847
  representation: modelResult?.text
@@ -881,6 +889,8 @@ export interface CreateAutoCompressionBlockOptions {
881
889
  requiredGainTokens?: number
882
890
  /** Allow a positive safe commit that reduces, but does not fully settle, the current recovery debt. */
883
891
  allowPartialGain?: boolean
892
+ /** Optional model-summary sub-deadline. The outer budgeted operation may reserve time for deterministic fallback/publication. */
893
+ summaryTimeoutMs?: number
884
894
  /** Optional durable publication hook. Live state is not changed unless it succeeds. */
885
895
  persistState?: (preparedState: DcpState, publication?: DcpJournalPublicationOptions) => Promise<void>
886
896
  /** Optional pure projection preparation; included in the same durable generation. */
@@ -919,8 +929,9 @@ function createAutoCompressionWorkingState(state: DcpState): DcpState {
919
929
 
920
930
  /**
921
931
  * Create the auto-compression block. Selects the summary source based on
922
- * `config.compress.autoCompress.summarizerModel`: empty → programmatic digest;
923
- * non-empty → model summary with programmatic fallback. Then delegates block
932
+ * `config.compress.autoCompress.summarizerModel` plus explicit
933
+ * `summarizerFallbackModels`: empty → programmatic digest; non-empty → model
934
+ * summary with ordered model fallbacks, then programmatic fallback. Delegates block
924
935
  * creation to the shared `createRangeCompressionBlock` path so protected
925
936
  * content (user messages, tool outputs, prompt info) is handled identically to
926
937
  * a model-initiated compress.
@@ -991,8 +1002,8 @@ export async function createAutoCompressionBlock(
991
1002
  topic,
992
1003
  messages: messagesInRange,
993
1004
  candidate: effectiveCandidate,
994
- modelRefs: settings.summarizerModel,
995
- timeoutMs: settings.timeoutMs,
1005
+ modelRefs: summarizerModelRefs(settings),
1006
+ timeoutMs: options.summaryTimeoutMs ?? settings.timeoutMs,
996
1007
  modelRegistry,
997
1008
  signal,
998
1009
  })
@@ -1065,11 +1076,16 @@ export async function createAutoCompressionBlock(
1065
1076
  const sourceExactEstimate = messagesInRange.reduce((sum, message) => sum + estimateMessageTokens(message), 0)
1066
1077
  const replacementExactEstimate = estimateCompressionBlockReplacementTokens(created.block)
1067
1078
  const projectedGain = sourceExactEstimate - replacementExactEstimate
1079
+ const blockedSummaryDetails = {
1080
+ summarizerFallbackUsed: preparedSummary.representation === "extractive-fallback",
1081
+ summarizerAttempts: preparedSummary.summarizerAttempts,
1082
+ }
1068
1083
  if (projectedGain <= 0) {
1069
1084
  throw new AutoCompressionBlockedError(
1070
1085
  "non-positive-gain",
1071
1086
  `Auto-compress rejected non-positive full-projection gain for ${effectiveCandidate.startId}..${effectiveCandidate.endId}: ` +
1072
1087
  `source ${sourceExactEstimate} tokens, replacement ${replacementExactEstimate} tokens`,
1088
+ blockedSummaryDetails,
1073
1089
  )
1074
1090
  }
1075
1091
  const requiredGainTokens = Math.max(0, Math.floor(options.requiredGainTokens ?? 0))
@@ -1078,6 +1094,7 @@ export async function createAutoCompressionBlock(
1078
1094
  "budget-exhausted",
1079
1095
  `Auto-compress projected gain for ${effectiveCandidate.startId}..${effectiveCandidate.endId} is below required budget recovery: ` +
1080
1096
  `${projectedGain} < ${requiredGainTokens} tokens`,
1097
+ blockedSummaryDetails,
1081
1098
  )
1082
1099
  }
1083
1100
 
@@ -1089,12 +1106,22 @@ export async function createAutoCompressionBlock(
1089
1106
  fullProjectedAfterTokens = finalProjection.reduce((sum, message) => sum + estimateMessageTokens(message), 0)
1090
1107
  fullProjectionGain = fullBefore - fullProjectedAfterTokens
1091
1108
  if (fullProjectionGain <= 0 || (fullProjectionGain < requiredGainTokens && !options.allowPartialGain)) {
1092
- throw new AutoCompressionBlockedError("budget-exhausted", `Final provider projection saves ${fullProjectionGain}, below required ${requiredGainTokens}; no state published`)
1109
+ throw new AutoCompressionBlockedError(
1110
+ "budget-exhausted",
1111
+ `Final provider projection saves ${fullProjectionGain}, below required ${requiredGainTokens}; no state published`,
1112
+ blockedSummaryDetails,
1113
+ )
1093
1114
  }
1094
1115
  }
1095
1116
  const pressureRelieved = workingState.compressionProgress
1096
1117
  ? settleCompressionProgress(workingState, fullProjectionGain, fullProjectedAfterTokens)
1097
1118
  : true
1119
+ created.block.commitMetrics = {
1120
+ operationId: `auto:${created.block.id}`,
1121
+ kind: effectiveCandidate.includedBlockIds.length >= 2 && sourceCoverage.itemCount === effectiveCandidate.includedBlockIds.length ? "consolidation" : "auto",
1122
+ beforeTokens: fullProjectedAfterTokens + fullProjectionGain,
1123
+ afterTokens: fullProjectedAfterTokens, netGainTokens: fullProjectionGain,
1124
+ }
1098
1125
  assertCurrent()
1099
1126
  let published = false
1100
1127
  if (options.persistState) await options.persistState(workingState, {