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
@@ -1,47 +1,63 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { randomUUID } from "node:crypto";
3
+ import { classifyShellCommand, type ShellCommandClassification } from "../shell-command-policy.js";
2
4
 
3
5
  import {
4
- CONTEXT_GATEWAY_ENFORCE_UNAVAILABLE,
6
+ contextGatewayAccountingLogDrain,
7
+ contextGatewayAccountingLogEnabled,
8
+ writeContextGatewayAccountingLog,
9
+ } from "./accounting-log.js";
10
+ import {
11
+ contextGatewayBudgetForClass,
5
12
  loadContextGatewayConfig,
6
13
  } from "./config.js";
7
- import { ContextGatewayTelemetry } from "./telemetry.js";
14
+ import { ContextGatewayEfficiencyTracker } from "./efficiency.js";
15
+ import { planContextGatewayEnforcement } from "./enforcement.js";
16
+ import { classifyContextGatewayTool, ContextGatewayTelemetry } from "./telemetry.js";
8
17
  import type {
9
18
  ContextGatewayEffectiveMode,
10
19
  ContextGatewayMode,
11
20
  ContextGatewayResolvedConfig,
12
21
  ContextGatewayRuntimeState,
22
+ ContextGatewayToolClass,
13
23
  } from "./types.js";
14
24
 
15
25
  export interface RegisterContextGatewayOptions {
16
26
  loadConfig?: () => ContextGatewayResolvedConfig;
17
27
  telemetry?: ContextGatewayTelemetry;
28
+ efficiency?: ContextGatewayEfficiencyTracker;
18
29
  }
19
30
 
20
31
  export interface RegisteredContextGateway extends ContextGatewayRuntimeState {
32
+ readonly efficiency: ContextGatewayEfficiencyTracker;
21
33
  setRuntimeMode(mode: ContextGatewayMode): { ok: boolean; message: string };
22
34
  }
23
35
 
24
36
  function formatStatus(runtime: RegisteredContextGateway): string {
25
37
  const telemetry = runtime.telemetry.snapshot();
38
+ const efficiency = runtime.efficiency.snapshot();
26
39
  return [
27
40
  `Context Gateway requested=${runtime.requestedMode}, effective=${runtime.effectiveMode}.`,
28
- `Observed results=${telemetry.results}, errors=${telemetry.errors}, upstreamTruncated=${telemetry.upstreamTruncatedResults}, overBudget=${telemetry.overBudgetResults}, unbound results=${telemetry.unboundResults}.`,
29
- `Repeat candidates=${telemetry.repeatCandidateCount}, retrieval calls=${telemetry.retrievalCalls}.`,
41
+ `Observed results=${telemetry.results}, errors=${telemetry.errors}, upstreamTruncated=${telemetry.upstreamTruncatedResults}, overBudget=${telemetry.overBudgetResults}, enforced=${telemetry.enforcedResults}, unbound results=${telemetry.unboundResults}.`,
42
+ `Repeat exact reads=${telemetry.repeatCandidateCount}, same-source different-range reads=${telemetry.sameSourceDifferentRangeCount}, retrieval calls=${telemetry.retrievalCalls}.`,
30
43
  `Native policy results=${telemetry.nativePolicy.results}, refusals=${telemetry.nativePolicy.refusals}, full overrides=${telemetry.nativePolicy.fullOverrides}.`,
31
- `Observed contentBytes=${telemetry.contentBytes}, textBytes=${telemetry.textBytes}, detailsBytes=${telemetry.detailsBytes}, potentialBytesOverBudget=${telemetry.potentialBytesOverBudget}.`,
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}.`,
32
47
  ].join("\n");
33
48
  }
34
49
 
35
50
  function formatDoctor(runtime: RegisteredContextGateway): string {
36
51
  const lines = [
37
52
  `Context Gateway doctor: requested=${runtime.requestedMode}, effective=${runtime.effectiveMode}.`,
38
- "Capture: read=limited; bash=limited+temp-output; repo_*=suite-adapter-not-wired; ast_grep=suite-adapter-not-wired.",
53
+ "Budgets: read=maxExactReadBytes; repo search/AST/structure=maxSearchBytes; other results=maxResultBytes; compact inline view=maxInlineBytes.",
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.",
55
+ "Read: full passthrough even when over budget; exact recovery/archive is not wired, so irreversible read truncation is disabled.",
56
+ "Repo tools: producer-native compact is supported with repoDiscovery.profile=native-compact; Gateway never post-hoc slices repo output.",
39
57
  "External paths: MCP=current Pix adapter unsupported; direct parent browser capture=unsupported.",
40
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.`,
41
60
  ];
42
- if (runtime.requestedMode === "enforce") {
43
- lines.push("BLOCKED: enforce is unavailable in P01; effective mode is off.");
44
- }
45
61
  for (const issue of runtime.config.issues) lines.push(`Config issue: ${issue}`);
46
62
  return lines.join("\n");
47
63
  }
@@ -56,13 +72,48 @@ export function registerContextGateway(
56
72
  ): RegisteredContextGateway {
57
73
  const config = (options.loadConfig ?? (() => loadContextGatewayConfig()))();
58
74
  const telemetry = options.telemetry ?? new ContextGatewayTelemetry();
75
+ const efficiency = options.efficiency ?? new ContextGatewayEfficiencyTracker();
59
76
  let requestedMode = config.mode;
60
- let effectiveMode: ContextGatewayEffectiveMode = requestedMode === "enforce" ? "off" : requestedMode;
61
- const inFlightToolCalls = new Set<string>();
77
+ let effectiveMode: ContextGatewayEffectiveMode = requestedMode;
78
+ type InFlight = { toolClass: ContextGatewayToolClass; shell: ShellCommandClassification };
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
+ };
62
112
 
63
113
  const resetRuntimeTelemetry = (): void => {
64
114
  inFlightToolCalls.clear();
65
115
  telemetry.reset();
116
+ efficiency.clearTransientBindings();
66
117
  };
67
118
 
68
119
  const runtime: RegisteredContextGateway = {
@@ -70,55 +121,182 @@ export function registerContextGateway(
70
121
  get effectiveMode() { return effectiveMode; },
71
122
  config,
72
123
  telemetry,
124
+ efficiency,
73
125
  setRuntimeMode(mode) {
74
- if (mode === "enforce") {
75
- return { ok: false, message: CONTEXT_GATEWAY_ENFORCE_UNAVAILABLE };
76
- }
77
126
  if (mode !== effectiveMode && inFlightToolCalls.size > 0) {
78
127
  return {
79
128
  ok: false,
80
129
  message: `Context Gateway mode change requires a safe boundary; ${inFlightToolCalls.size} tool call(s) are still in flight.`,
81
130
  };
82
131
  }
132
+ const previousMode = effectiveMode;
133
+ if (previousMode !== mode) finalizeAccountingEpoch("mode-change");
83
134
  requestedMode = mode;
84
135
  effectiveMode = mode;
85
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
+ }
86
147
  return { ok: true, message: `Context Gateway runtime mode set to ${mode}; config was not changed.` };
87
148
  },
88
149
  };
89
150
 
90
151
  pi.on("tool_call", async (event) => {
91
- if (typeof event.toolCallId === "string") inFlightToolCalls.add(event.toolCallId);
92
- if (effectiveMode === "observe") telemetry.recordToolCall(event);
152
+ if (typeof event.toolCallId === "string" && typeof event.toolName === "string") {
153
+ const toolClass = classifyContextGatewayTool(event.toolName);
154
+ inFlightToolCalls.set(event.toolCallId, {
155
+ toolClass,
156
+ shell: toolClass === "shell"
157
+ ? classifyShellCommand(event.input)
158
+ : { scope: "unknown", kind: "unknown" },
159
+ });
160
+ }
161
+ if (effectiveMode === "observe" || effectiveMode === "enforce") {
162
+ telemetry.recordToolCall(event);
163
+ const accountingCall = efficiency.recordToolCall(event);
164
+ if (accountingCall) writeAccounting("tool.call", { ...accountingCall });
165
+ }
93
166
  return undefined;
94
167
  });
95
168
 
96
169
  pi.on("tool_result", async (event) => {
97
170
  try {
98
- if (effectiveMode === "observe") telemetry.recordToolResult(event, config.budgets.maxResultBytes);
171
+ if (effectiveMode === "off") return undefined;
172
+ const binding = typeof event.toolCallId === "string"
173
+ ? inFlightToolCalls.get(event.toolCallId)
174
+ : undefined;
175
+ const classBudget = binding
176
+ ? contextGatewayBudgetForClass(binding.toolClass, config.budgets)
177
+ : config.budgets.maxResultBytes;
178
+ const enforcement = effectiveMode === "enforce" && binding
179
+ ? planContextGatewayEnforcement({
180
+ event,
181
+ toolCallId: typeof event.toolCallId === "string" ? event.toolCallId : undefined,
182
+ toolClass: binding.toolClass,
183
+ shell: binding.shell,
184
+ budgetBytes: classBudget,
185
+ maxInlineBytes: Math.min(
186
+ config.budgets.maxInlineBytes,
187
+ classBudget,
188
+ ),
189
+ })
190
+ : undefined;
191
+ const observation = telemetry.recordToolResult(event, config.budgets, {
192
+ maxInlineBytes: config.budgets.maxInlineBytes,
193
+ ...(enforcement ? {
194
+ delivery: {
195
+ representation: enforcement.representation,
196
+ contentBytes: enforcement.contentBytes,
197
+ textBytes: enforcement.textBytes,
198
+ },
199
+ } : {}),
200
+ });
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") {
221
+ const originalDetails = event.details && typeof event.details === "object" && !Array.isArray(event.details)
222
+ ? event.details as Record<string, unknown>
223
+ : {};
224
+ return {
225
+ content: enforcement.content as any,
226
+ details: {
227
+ ...originalDetails,
228
+ contextGateway: {
229
+ version: 1,
230
+ representation: enforcement.representation,
231
+ sourceContentBytes: enforcement.sourceContentBytes,
232
+ deliveredContentBytes: enforcement.contentBytes,
233
+ },
234
+ },
235
+ };
236
+ }
99
237
  } finally {
100
238
  if (typeof event.toolCallId === "string") inFlightToolCalls.delete(event.toolCallId);
101
239
  }
102
240
  return undefined;
103
241
  });
104
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
+
105
275
  pi.on("session_start", async () => {
276
+ finalizeAccountingEpoch("session-start-reset");
106
277
  resetRuntimeTelemetry();
278
+ beginAccountingEpoch("session-start");
107
279
  return undefined;
108
280
  });
109
281
 
110
282
  pi.on("session_tree", async () => {
283
+ finalizeAccountingEpoch("session-tree");
111
284
  resetRuntimeTelemetry();
285
+ beginAccountingEpoch("session-tree");
112
286
  return undefined;
113
287
  });
114
288
 
115
289
  pi.on("session_shutdown", async () => {
290
+ finalizeAccountingEpoch("session-shutdown");
116
291
  resetRuntimeTelemetry();
292
+ efficiency.reset();
293
+ accountingEpochActive = false;
294
+ await contextGatewayAccountingLogDrain();
117
295
  return undefined;
118
296
  });
119
297
 
120
298
  pi.registerCommand("context-gateway", {
121
- description: "Show Context Gateway status/doctor output or change the runtime-only off/observe mode.",
299
+ description: "Show Context Gateway status/doctor output or change runtime-only off/observe/enforce mode.",
122
300
  handler: async (rawArgs, ctx) => {
123
301
  const args = rawArgs.trim().split(/\s+/).filter(Boolean);
124
302
  const action = (args[0] ?? "status").toLowerCase();
@@ -127,7 +305,7 @@ export function registerContextGateway(
127
305
  return;
128
306
  }
129
307
  if (action === "doctor") {
130
- notify(ctx, formatDoctor(runtime), runtime.requestedMode === "enforce" ? "warning" : "info");
308
+ notify(ctx, formatDoctor(runtime), runtime.config.issues.length > 0 ? "warning" : "info");
131
309
  return;
132
310
  }
133
311
  if (action === "mode") {
@@ -152,7 +330,16 @@ export default function contextGatewayExtension(pi: ExtensionAPI): void {
152
330
  }
153
331
 
154
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";
155
341
  export { loadContextGatewayConfig } from "./config.js";
342
+ export { ContextGatewayEfficiencyTracker, estimateContextGatewayTokens } from "./efficiency.js";
156
343
  export { normalizeRedundantTruncationMetadata } from "./metadata-normalization.js";
157
344
  export { STORELESS_CAPABILITIES, storelessCapability } from "./storeless-capabilities.js";
158
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" | "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
 
@@ -27,9 +27,9 @@ export const STORELESS_CAPABILITIES: readonly StorelessCapabilityEntry[] = Objec
27
27
  {
28
28
  surface: "test-build",
29
29
  status: "limited",
30
- strategy: "pure-parser-candidate",
30
+ strategy: "pure-parser-compact",
31
31
  lifetime: "current-result",
32
- reason: "Bun/TAP/bounded TypeScript parsing is observe-only; production delivery remains passthrough without a recovery/lifetime contract.",
32
+ reason: "Enforce compacts only recognised complete simple Bun/TAP/bounded TypeScript output; partial, compound, truncated, or unknown results remain passthrough.",
33
33
  },
34
34
  {
35
35
  surface: "mutation-lsp",
@@ -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",
@@ -1,8 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { classifyShellCommand } from "../shell-command-policy.js";
2
3
 
3
4
  import { accountContextGatewayParts } from "./accounting.js";
5
+ import { contextGatewayBudgetForClass } from "./config.js";
4
6
  import { parseTestBuildOutput, planProspectiveTestOutputDelivery } from "./test-output-parser.js";
5
7
  import type {
8
+ ContextGatewayBudgets,
6
9
  ContextGatewayClassTelemetry,
7
10
  ContextGatewayNativePolicyReason,
8
11
  ContextGatewayNativePolicyTelemetry,
@@ -40,6 +43,7 @@ const NATIVE_POLICY_REASONS = new Set<ContextGatewayNativePolicyReason>([
40
43
  type CallBinding = {
41
44
  toolClass: ContextGatewayToolClass;
42
45
  inputFingerprint?: string;
46
+ readSourceFingerprint?: string;
43
47
  shellCommandScope?: "simple" | "compound" | "unknown";
44
48
  };
45
49
 
@@ -60,12 +64,15 @@ function emptyCounters(): ContextGatewayClassTelemetry {
60
64
  results: 0,
61
65
  errors: 0,
62
66
  contentBytes: 0,
67
+ deliveredContentBytes: 0,
63
68
  textBytes: 0,
64
69
  imageBytes: 0,
65
70
  detailsBytes: 0,
66
71
  upstreamTruncatedResults: 0,
67
72
  overBudgetResults: 0,
68
73
  potentialBytesOverBudget: 0,
74
+ enforcedResults: 0,
75
+ actualBytesSaved: 0,
69
76
  };
70
77
  }
71
78
 
@@ -92,13 +99,46 @@ function inputFingerprint(toolName: string, input: unknown): string {
92
99
  return createHash("sha256").update(`${toolName}\u0000${encoded}`).digest("hex");
93
100
  }
94
101
 
95
- function shellCommandScope(input: unknown): "simple" | "compound" | "unknown" {
96
- if (!input || typeof input !== "object" || Array.isArray(input)) return "unknown";
97
- const command = (input as Record<string, unknown>).command;
98
- if (typeof command !== "string" || command.trim().length === 0) return "unknown";
99
- // Conservative only: false positives merely keep passthrough. This does not
100
- // parse, rewrite, approve, or execute the shell command.
101
- return /[\r\n;|`]|&&|\$\(/.test(command) ? "compound" : "simple";
102
+ function normalizedReadPath(value: unknown): string | undefined {
103
+ if (typeof value !== "string") return undefined;
104
+ const raw = value.trim().replace(/\\/g, "/").replace(/\/{2,}/g, "/");
105
+ if (!raw) return undefined;
106
+ const prefix = raw.startsWith("/") ? "/" : /^[A-Za-z]:\//.test(raw) ? raw.slice(0, 3).toLowerCase() : "";
107
+ const body = prefix === "/" ? raw.slice(1) : prefix ? raw.slice(3) : raw;
108
+ const stack: string[] = [];
109
+ for (const part of body.split("/")) {
110
+ if (!part || part === ".") continue;
111
+ if (part === "..") {
112
+ if (stack.length > 0 && stack.at(-1) !== "..") stack.pop();
113
+ else if (!prefix) stack.push(part);
114
+ continue;
115
+ }
116
+ stack.push(part);
117
+ }
118
+ return `${prefix}${stack.join("/")}` || prefix || ".";
119
+ }
120
+
121
+ function normalizedReadRangeValue(value: unknown): number | undefined {
122
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
123
+ if (typeof value === "string" && /^\d+$/.test(value.trim())) {
124
+ const parsed = Number(value);
125
+ if (Number.isSafeInteger(parsed)) return parsed;
126
+ }
127
+ return undefined;
128
+ }
129
+
130
+ export function contextGatewayReadIdentity(input: unknown): { exact?: string; source?: string } {
131
+ if (!input || typeof input !== "object" || Array.isArray(input)) return {};
132
+ const record = input as Record<string, unknown>;
133
+ const path = normalizedReadPath(record.path ?? record.file ?? record.filePath);
134
+ if (!path) return {};
135
+ const source = inputFingerprint("read-source", { path });
136
+ const exact = inputFingerprint("read-exact", {
137
+ path,
138
+ offset: normalizedReadRangeValue(record.offset ?? record.start ?? record.line),
139
+ limit: normalizedReadRangeValue(record.limit ?? record.lines ?? record.count),
140
+ });
141
+ return { source, exact };
102
142
  }
103
143
 
104
144
  function byteLengthJson(value: unknown): number {
@@ -209,19 +249,31 @@ export function classifyContextGatewayTool(toolName: string): ContextGatewayTool
209
249
 
210
250
  export function observeToolResult(
211
251
  event: ToolResultLike,
212
- maxResultBytes: number,
213
- options: { toolClass?: ContextGatewayToolClass; shellCommandScope?: "simple" | "compound" | "unknown" } = {},
252
+ budget: number | ContextGatewayBudgets,
253
+ options: {
254
+ toolClass?: ContextGatewayToolClass;
255
+ shellCommandScope?: "simple" | "compound" | "unknown";
256
+ maxInlineBytes?: number;
257
+ delivery?: { representation: "passthrough" | "test-build-compact" | "web-recoverable-compact"; contentBytes: number; textBytes: number };
258
+ } = {},
214
259
  ): ContextGatewayObservation {
215
260
  const toolName = typeof event.toolName === "string" ? event.toolName : "unknown";
216
261
  const sizes = contentSizes(event.content);
217
262
  const detailsBytes = byteLengthJson(event.details);
218
- const potentialBytesOverBudget = Math.max(0, sizes.contentBytes - maxResultBytes);
219
263
  const toolClass = options.toolClass ?? classifyContextGatewayTool(toolName);
264
+ const budgetBytes = typeof budget === "number" ? budget : contextGatewayBudgetForClass(toolClass, budget);
265
+ const potentialBytesOverBudget = Math.max(0, sizes.contentBytes - budgetBytes);
220
266
  const completeness = isTruncated(event.details) ? "upstream-truncated" : "unknown";
267
+ const delivery = options.delivery ?? {
268
+ representation: "passthrough" as const,
269
+ contentBytes: sizes.contentBytes,
270
+ textBytes: sizes.textBytes,
271
+ };
221
272
  const observation: ContextGatewayObservation = {
222
273
  version: 1,
223
274
  toolClass,
224
275
  outcome: event.isError === true ? "error" : "success",
276
+ budgetBytes,
225
277
  source: {
226
278
  kind: "tool-result-boundary",
227
279
  completeness,
@@ -231,13 +283,10 @@ export function observeToolResult(
231
283
  detailsBytes,
232
284
  },
233
285
  view: { kind: "text-content", textBytes: sizes.textBytes },
234
- delivery: {
235
- representation: "passthrough",
236
- contentBytes: sizes.contentBytes,
237
- textBytes: sizes.textBytes,
238
- },
239
- overBudget: sizes.contentBytes > maxResultBytes,
286
+ delivery,
287
+ overBudget: sizes.contentBytes > budgetBytes,
240
288
  potentialBytesOverBudget,
289
+ actualBytesSaved: Math.max(0, sizes.contentBytes - delivery.contentBytes),
241
290
  };
242
291
  if (toolClass === "shell") {
243
292
  const parsed = parseTestBuildOutput({
@@ -246,7 +295,11 @@ export function observeToolResult(
246
295
  upstreamTruncated: completeness === "upstream-truncated",
247
296
  });
248
297
  const commandScope = options.shellCommandScope ?? "unknown";
249
- const prospective = planProspectiveTestOutputDelivery(parsed, maxResultBytes, { commandScope });
298
+ const prospective = planProspectiveTestOutputDelivery(
299
+ parsed,
300
+ options.maxInlineBytes ?? budgetBytes,
301
+ { commandScope },
302
+ );
250
303
  observation.testOutput = {
251
304
  parserVersion: 1,
252
305
  classification: parsed.classification,
@@ -268,8 +321,10 @@ export class ContextGatewayTelemetry {
268
321
  private byClass: Partial<Record<ContextGatewayToolClass, ContextGatewayClassTelemetry>> = {};
269
322
  private bindings = new Map<string, CallBinding>();
270
323
  private seenInputFingerprints = new Set<string>();
324
+ private seenReadSourceFingerprints = new Set<string>();
271
325
  private unboundResults = 0;
272
326
  private repeatCandidateCount = 0;
327
+ private sameSourceDifferentRangeCount = 0;
273
328
  private retrievalCalls = 0;
274
329
  private nativePolicy: ContextGatewayNativePolicyTelemetry = {
275
330
  results: 0,
@@ -286,27 +341,39 @@ export class ContextGatewayTelemetry {
286
341
  if (normalizedToolName === "artifact_read" || normalizedToolName === "artifact_search") {
287
342
  this.retrievalCalls += 1;
288
343
  }
289
- const fingerprint = normalizedToolName === "read"
290
- ? inputFingerprint(event.toolName, event.input)
291
- : undefined;
344
+ const readFingerprint = normalizedToolName === "read" ? contextGatewayReadIdentity(event.input) : {};
345
+ const fingerprint = readFingerprint.exact;
292
346
  if (fingerprint) {
293
347
  if (this.seenInputFingerprints.has(fingerprint)) this.repeatCandidateCount += 1;
294
- else this.seenInputFingerprints.add(fingerprint);
348
+ else {
349
+ if (readFingerprint.source && this.seenReadSourceFingerprints.has(readFingerprint.source)) {
350
+ this.sameSourceDifferentRangeCount += 1;
351
+ }
352
+ this.seenInputFingerprints.add(fingerprint);
353
+ }
295
354
  }
355
+ if (readFingerprint.source) this.seenReadSourceFingerprints.add(readFingerprint.source);
356
+ const toolClass = classifyContextGatewayTool(event.toolName);
296
357
  this.bindings.set(event.toolCallId, {
297
- toolClass: classifyContextGatewayTool(event.toolName),
358
+ toolClass,
298
359
  inputFingerprint: fingerprint,
299
- ...(classifyContextGatewayTool(event.toolName) === "shell"
300
- ? { shellCommandScope: shellCommandScope(event.input) }
360
+ readSourceFingerprint: readFingerprint.source,
361
+ ...(toolClass === "shell"
362
+ ? { shellCommandScope: classifyShellCommand(event.input).scope }
301
363
  : {}),
302
364
  });
303
365
  }
304
366
 
305
- recordToolResult(event: ToolResultLike, maxResultBytes: number): ContextGatewayObservation {
367
+ recordToolResult(
368
+ event: ToolResultLike,
369
+ budget: number | ContextGatewayBudgets,
370
+ options: { delivery?: { representation: "passthrough" | "test-build-compact" | "web-recoverable-compact"; contentBytes: number; textBytes: number }; maxInlineBytes?: number } = {},
371
+ ): ContextGatewayObservation {
306
372
  const binding = typeof event.toolCallId === "string" ? this.bindings.get(event.toolCallId) : undefined;
307
- const observation = observeToolResult(event, maxResultBytes, {
373
+ const observation = observeToolResult(event, budget, {
308
374
  ...(binding ? { toolClass: binding.toolClass } : {}),
309
375
  ...(binding?.shellCommandScope ? { shellCommandScope: binding.shellCommandScope } : {}),
376
+ ...options,
310
377
  });
311
378
  if (typeof event.toolCallId !== "string" || !binding) {
312
379
  this.unboundResults += 1;
@@ -357,8 +424,8 @@ export class ContextGatewayTelemetry {
357
424
  }
358
425
 
359
426
  /** Backward-compatible convenience for observation-only callers without a tool_call event. */
360
- record(event: ToolResultLike, maxResultBytes: number): ContextGatewayObservation {
361
- const observation = observeToolResult(event, maxResultBytes);
427
+ record(event: ToolResultLike, budget: number | ContextGatewayBudgets): ContextGatewayObservation {
428
+ const observation = observeToolResult(event, budget);
362
429
  this.aggregateObservation(event, observation);
363
430
  return observation;
364
431
  }
@@ -368,8 +435,10 @@ export class ContextGatewayTelemetry {
368
435
  this.byClass = {};
369
436
  this.bindings.clear();
370
437
  this.seenInputFingerprints.clear();
438
+ this.seenReadSourceFingerprints.clear();
371
439
  this.unboundResults = 0;
372
440
  this.repeatCandidateCount = 0;
441
+ this.sameSourceDifferentRangeCount = 0;
373
442
  this.retrievalCalls = 0;
374
443
  this.nativePolicy = { results: 0, refusals: 0, fullOverrides: 0, byReason: {} };
375
444
  this.testOutput = emptyTestOutputTelemetry();
@@ -383,6 +452,7 @@ export class ContextGatewayTelemetry {
383
452
  pendingCalls: this.bindings.size,
384
453
  unboundResults: this.unboundResults,
385
454
  repeatCandidateCount: this.repeatCandidateCount,
455
+ sameSourceDifferentRangeCount: this.sameSourceDifferentRangeCount,
386
456
  retrievalCalls: this.retrievalCalls,
387
457
  nativePolicy: {
388
458
  ...this.nativePolicy,
@@ -404,12 +474,15 @@ export class ContextGatewayTelemetry {
404
474
  target.results += 1;
405
475
  if (observation.outcome === "error") target.errors += 1;
406
476
  target.contentBytes += observation.source.contentBytes;
477
+ target.deliveredContentBytes += observation.delivery.contentBytes;
407
478
  target.textBytes += observation.source.textBytes;
408
479
  target.imageBytes += observation.source.imageBytes;
409
480
  target.detailsBytes += observation.source.detailsBytes;
410
481
  if (observation.source.completeness === "upstream-truncated") target.upstreamTruncatedResults += 1;
411
482
  if (observation.overBudget) target.overBudgetResults += 1;
412
483
  target.potentialBytesOverBudget += observation.potentialBytesOverBudget;
484
+ if (observation.delivery.representation !== "passthrough") target.enforcedResults += 1;
485
+ target.actualBytesSaved += observation.actualBytesSaved;
413
486
  }
414
487
  }
415
488