@herbertgao/pi-extensions 2026.8.11 → 2026.8.13

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 (476) hide show
  1. package/README.md +23 -10
  2. package/{node_modules/@herbertgao/pi-mermaid-open/LICENSE → THIRD_PARTY_NOTICES.md} +19 -1
  3. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +15 -0
  4. package/node_modules/@czottmann/pi-automode/README.md +5 -3
  5. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +2 -0
  6. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-002-global-config-in-extension-data-directory.md +60 -0
  7. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +1 -0
  8. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +4 -4
  9. package/node_modules/@czottmann/pi-automode/docs/configuration.md +20 -4
  10. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +1 -1
  11. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/bash.ts +692 -0
  12. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +181 -3
  13. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +4 -1
  14. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +143 -36
  15. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +103 -148
  16. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +32 -6
  17. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +344 -22
  18. package/node_modules/@czottmann/pi-automode/extensions/auto-mode.ts +1 -0
  19. package/node_modules/@czottmann/pi-automode/package.json +4 -1
  20. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -1
  21. package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -1
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +4 -0
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +21 -1
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +1 -1
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +19 -19
  26. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +4 -2
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +2 -1
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +10 -2
  29. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  30. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +10 -0
  31. package/node_modules/@herbertgao/pi-subagents/README.md +21 -15
  32. package/node_modules/@herbertgao/pi-subagents/package.json +3 -3
  33. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +11 -8
  34. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +184 -106
  35. package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +31 -11
  36. package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +9 -1
  37. package/node_modules/@herbertgao/pi-subagents/src/index.ts +156 -31
  38. package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +17 -0
  39. package/node_modules/@herbertgao/pi-subagents/src/model-resolver.ts +14 -0
  40. package/node_modules/@herbertgao/pi-subagents/src/schedule.ts +8 -0
  41. package/node_modules/@herbertgao/pi-subagents/src/settings.ts +32 -1
  42. package/node_modules/@herbertgao/pi-subagents/src/types.ts +17 -2
  43. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +23 -2
  44. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +259 -24
  45. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  46. package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +376 -0
  47. package/node_modules/@luxusai/pi-hindsight/README.md +92 -0
  48. package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +56 -0
  49. package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +169 -0
  50. package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +107 -0
  51. package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +89 -0
  52. package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +189 -0
  53. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
  54. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
  55. package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +54 -0
  56. package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +75 -0
  57. package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +300 -0
  58. package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +158 -0
  59. package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +164 -0
  60. package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +139 -0
  61. package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +74 -0
  62. package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +261 -0
  63. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +151 -0
  64. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +18 -0
  65. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +46 -0
  66. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +383 -0
  67. package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +240 -0
  68. package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +176 -0
  69. package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +178 -0
  70. package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +41 -0
  71. package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +386 -0
  72. package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +39 -0
  73. package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +34 -0
  74. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +140 -0
  75. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +61 -0
  76. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +925 -0
  77. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +247 -0
  78. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +547 -0
  79. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +480 -0
  80. package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +182 -0
  81. package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +86 -0
  82. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +1010 -0
  83. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +175 -0
  84. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +425 -0
  85. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +210 -0
  86. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +817 -0
  87. package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +25 -0
  88. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +319 -0
  89. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +196 -0
  90. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +189 -0
  91. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +65 -0
  92. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +260 -0
  93. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +244 -0
  94. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +59 -0
  95. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +82 -0
  96. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +45 -0
  97. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +331 -0
  98. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +354 -0
  99. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +66 -0
  100. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +132 -0
  101. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +228 -0
  102. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +74 -0
  103. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +16 -0
  104. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +648 -0
  105. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +105 -0
  106. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +68 -0
  107. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +71 -0
  108. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +13 -0
  109. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +162 -0
  110. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +131 -0
  111. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +104 -0
  112. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +37 -0
  113. package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +893 -0
  114. package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +35 -0
  115. package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +177 -0
  116. package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +7 -0
  117. package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +21 -0
  118. package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +112 -0
  119. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +111 -0
  120. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +214 -0
  121. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +70 -0
  122. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +392 -0
  123. package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +45 -0
  124. package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +9 -0
  125. package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +860 -0
  126. package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +105 -0
  127. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +189 -0
  128. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +299 -0
  129. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +201 -0
  130. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +47 -0
  131. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +239 -0
  132. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +50 -0
  133. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +259 -0
  134. package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +77 -0
  135. package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +534 -0
  136. package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +319 -0
  137. package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +325 -0
  138. package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +42 -0
  139. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +244 -0
  140. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +56 -0
  141. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +50 -0
  142. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +167 -0
  143. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +207 -0
  144. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +120 -0
  145. package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +8 -0
  146. package/node_modules/@luxusai/pi-hindsight/package.json +118 -0
  147. package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +75 -0
  148. package/node_modules/@narumitw/pi-btw/dist/index.ts +143 -46
  149. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  150. package/node_modules/@narumitw/pi-btw/package.json +6 -6
  151. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +136 -22
  152. package/node_modules/{@herbertgao → @tifan}/pi-copy-response/README.md +2 -4
  153. package/node_modules/{@herbertgao → @tifan}/pi-copy-response/package.json +5 -15
  154. package/node_modules/{@herbertgao → @tifan}/pi-handoff/README.md +4 -6
  155. package/node_modules/@tifan/pi-handoff/package.json +43 -0
  156. package/node_modules/{@herbertgao → @tifan}/pi-handoff/src/index.ts +1 -1
  157. package/node_modules/{@herbertgao → @tifan}/pi-inline-skills/README.md +2 -4
  158. package/node_modules/{@herbertgao → @tifan}/pi-inline-skills/package.json +5 -15
  159. package/node_modules/{@herbertgao → @tifan}/pi-mermaid-open/README.md +5 -7
  160. package/node_modules/{@herbertgao → @tifan}/pi-mermaid-open/package.json +5 -19
  161. package/node_modules/{@herbertgao → @tifan}/pi-preferred-thinking/README.md +4 -6
  162. package/node_modules/{@herbertgao → @tifan}/pi-preferred-thinking/package.json +6 -16
  163. package/node_modules/{@herbertgao → @tifan}/pi-preferred-thinking/src/index.ts +1 -6
  164. package/node_modules/{@herbertgao → @tifan}/pi-recap/README.md +4 -6
  165. package/node_modules/@tifan/pi-recap/package.json +42 -0
  166. package/node_modules/{@herbertgao → @tifan}/pi-recap/src/models.ts +1 -6
  167. package/node_modules/{@herbertgao → @tifan}/pi-rename/README.md +13 -8
  168. package/node_modules/@tifan/pi-rename/package.json +47 -0
  169. package/node_modules/@tifan/pi-rename/src/herdr-label.ts +4 -0
  170. package/node_modules/{@herbertgao → @tifan}/pi-rename/src/index.ts +4 -16
  171. package/node_modules/{@herbertgao → @tifan}/pi-rename/src/models.ts +1 -6
  172. package/node_modules/{@herbertgao → @tifan}/pi-titlebar-spinner/README.md +2 -4
  173. package/node_modules/{@herbertgao → @tifan}/pi-titlebar-spinner/package.json +5 -15
  174. package/node_modules/pi-lens/CHANGELOG.md +96 -1
  175. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +2 -1
  176. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +14 -5
  177. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +48 -18
  178. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +1 -3
  179. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +16 -4
  180. package/node_modules/pi-lens/dist/clients/bash-file-access.js +3 -1
  181. package/node_modules/pi-lens/dist/clients/biome-client.js +1 -3
  182. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +21 -8
  183. package/node_modules/pi-lens/dist/clients/bounded-cache.js +24 -8
  184. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +4 -3
  185. package/node_modules/pi-lens/dist/clients/cache-manager.js +6 -7
  186. package/node_modules/pi-lens/dist/clients/cache-observability.js +461 -66
  187. package/node_modules/pi-lens/dist/clients/call-graph.js +238 -63
  188. package/node_modules/pi-lens/dist/clients/codebase-model.js +13 -5
  189. package/node_modules/pi-lens/dist/clients/complexity-client.js +1 -3
  190. package/node_modules/pi-lens/dist/clients/dead-code-client.js +19 -8
  191. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +1 -2
  192. package/node_modules/pi-lens/dist/clients/deadline-utils.js +3 -1
  193. package/node_modules/pi-lens/dist/clients/debug-heap.js +8 -1
  194. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +1 -1
  195. package/node_modules/pi-lens/dist/clients/dependency-checker.js +11 -7
  196. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +39 -2
  197. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +5 -1
  198. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +2 -3
  199. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +13 -1
  200. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +3 -1
  201. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +2 -1
  202. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +1 -1
  203. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +9 -4
  204. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +3 -1
  205. package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +6 -2
  206. package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +31 -7
  207. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +5 -2
  208. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +10 -14
  209. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +5 -1
  210. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +1 -1
  211. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +3 -1
  212. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +25 -6
  213. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +1 -1
  214. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +1 -1
  215. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +3 -1
  216. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +13 -8
  217. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +4 -1
  218. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +25 -10
  219. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +1 -1
  220. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +1 -1
  221. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +1 -1
  222. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +28 -4
  223. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +6 -8
  224. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +212 -11
  225. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +1 -1
  226. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +8 -4
  227. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -6
  228. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +23 -14
  229. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +1 -1
  230. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +7 -9
  231. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +5 -2
  232. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +7 -7
  233. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +6 -8
  234. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +13 -8
  235. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +13 -8
  236. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +17 -3
  237. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +7 -1
  238. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +24 -1
  239. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +35 -0
  240. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +1 -3
  241. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +6 -8
  242. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +1 -1
  243. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +2 -1
  244. package/node_modules/pi-lens/dist/clients/dispatch/types.js +6 -1
  245. package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +6 -2
  246. package/node_modules/pi-lens/dist/clients/extension-log.js +5 -3
  247. package/node_modules/pi-lens/dist/clients/feature-hints.js +8 -1
  248. package/node_modules/pi-lens/dist/clients/file-kinds.js +34 -129
  249. package/node_modules/pi-lens/dist/clients/file-utils.js +17 -1
  250. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +57 -2
  251. package/node_modules/pi-lens/dist/clients/formatters.js +144 -27
  252. package/node_modules/pi-lens/dist/clients/freshness.js +38 -0
  253. package/node_modules/pi-lens/dist/clients/git-guard.js +43 -13
  254. package/node_modules/pi-lens/dist/clients/go-client.js +1 -3
  255. package/node_modules/pi-lens/dist/clients/grammar-source.js +1 -1
  256. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +30 -4
  257. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +1 -3
  258. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +4 -2
  259. package/node_modules/pi-lens/dist/clients/installer/index.js +86 -77
  260. package/node_modules/pi-lens/dist/clients/instance-reaper.js +5 -2
  261. package/node_modules/pi-lens/dist/clients/instance-registry.js +4 -2
  262. package/node_modules/pi-lens/dist/clients/jscpd-client.js +6 -1
  263. package/node_modules/pi-lens/dist/clients/knip-client.js +29 -15
  264. package/node_modules/pi-lens/dist/clients/language-profile.js +4 -1
  265. package/node_modules/pi-lens/dist/clients/latency-logger.js +13 -2
  266. package/node_modules/pi-lens/dist/clients/lens-config.js +1 -1
  267. package/node_modules/pi-lens/dist/clients/lens-map.js +8 -3
  268. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +1 -1
  269. package/node_modules/pi-lens/dist/clients/lsp/config.js +4 -6
  270. package/node_modules/pi-lens/dist/clients/lsp/edits.js +119 -32
  271. package/node_modules/pi-lens/dist/clients/lsp/index.js +94 -35
  272. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +2 -1
  273. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +4 -1
  274. package/node_modules/pi-lens/dist/clients/lsp/launch.js +11 -3
  275. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +152 -0
  276. package/node_modules/pi-lens/dist/clients/lsp/server.js +19 -7
  277. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +10 -2
  278. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +9 -4
  279. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +1 -3
  280. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +10 -20
  281. package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +1 -1
  282. package/node_modules/pi-lens/dist/clients/memory-sampler.js +73 -7
  283. package/node_modules/pi-lens/dist/clients/metrics-client.js +1 -3
  284. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +8 -3
  285. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +7 -1
  286. package/node_modules/pi-lens/dist/clients/module-report.js +14 -17
  287. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +3 -5
  288. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +220 -0
  289. package/node_modules/pi-lens/dist/clients/package-manager.js +6 -1
  290. package/node_modules/pi-lens/dist/clients/pipeline.js +57 -9
  291. package/node_modules/pi-lens/dist/clients/project-conventions.js +2 -8
  292. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +10 -7
  293. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +2 -1
  294. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +3 -1
  295. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +13 -9
  296. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +3 -1
  297. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +5 -2
  298. package/node_modules/pi-lens/dist/clients/project-lens-config.js +12 -5
  299. package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +48 -0
  300. package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +15 -1
  301. package/node_modules/pi-lens/dist/clients/project-snapshot.js +472 -66
  302. package/node_modules/pi-lens/dist/clients/read-expansion.js +12 -2
  303. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +5 -4
  304. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +5 -2
  305. package/node_modules/pi-lens/dist/clients/read-guard.js +1 -1
  306. package/node_modules/pi-lens/dist/clients/resource-sampler.js +1 -1
  307. package/node_modules/pi-lens/dist/clients/reverse-deps.js +6 -5
  308. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +42 -17
  309. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +8 -2
  310. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +10 -1
  311. package/node_modules/pi-lens/dist/clients/review-graph/query.js +2 -1
  312. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +4 -2
  313. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +2 -2
  314. package/node_modules/pi-lens/dist/clients/ruff-client.js +6 -5
  315. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +63 -31
  316. package/node_modules/pi-lens/dist/clients/runtime-context.js +12 -4
  317. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +83 -4
  318. package/node_modules/pi-lens/dist/clients/runtime-session.js +14 -0
  319. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +59 -3
  320. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +148 -34
  321. package/node_modules/pi-lens/dist/clients/runtime-turn.js +147 -6
  322. package/node_modules/pi-lens/dist/clients/rust-client.js +1 -3
  323. package/node_modules/pi-lens/dist/clients/safe-spawn.js +150 -11
  324. package/node_modules/pi-lens/dist/clients/security-scan-client.js +1 -3
  325. package/node_modules/pi-lens/dist/clients/session-summary.js +3 -1
  326. package/node_modules/pi-lens/dist/clients/sg-runner.js +1 -3
  327. package/node_modules/pi-lens/dist/clients/source-filter.js +10 -6
  328. package/node_modules/pi-lens/dist/clients/source-walker.js +1 -1
  329. package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +96 -0
  330. package/node_modules/pi-lens/dist/clients/startup-scan.js +4 -1
  331. package/node_modules/pi-lens/dist/clients/tool-definition.js +7 -0
  332. package/node_modules/pi-lens/dist/clients/tool-policy.js +26 -11
  333. package/node_modules/pi-lens/dist/clients/tool-render.js +2 -1
  334. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +10 -4
  335. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +2 -2
  336. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +12 -5
  337. package/node_modules/pi-lens/dist/clients/warm-attach.js +12 -5
  338. package/node_modules/pi-lens/dist/clients/widget-state.js +27 -11
  339. package/node_modules/pi-lens/dist/clients/word-index.js +3 -1
  340. package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -2
  341. package/node_modules/pi-lens/dist/clients/zizmor-config.js +9 -1
  342. package/node_modules/pi-lens/dist/i18n.js +13 -5
  343. package/node_modules/pi-lens/dist/index.js +10797 -8627
  344. package/node_modules/pi-lens/dist/mcp/server.js +3 -1
  345. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +68 -12
  346. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +21 -7
  347. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +164 -35
  348. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +24 -7
  349. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +3 -1
  350. package/node_modules/pi-lens/dist/tools/module-report.js +1 -1
  351. package/node_modules/pi-lens/dist/tools/project-report.js +1 -1
  352. package/node_modules/pi-lens/dist/tools/symbol-search.js +5 -2
  353. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +1 -1
  354. package/node_modules/pi-lens/docs/servercapabilities.md +1 -1
  355. package/node_modules/pi-lens/package.json +160 -156
  356. package/node_modules/pi-lens/rules/typos/_typos.toml +2 -2
  357. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +31 -13
  358. package/node_modules/pi-lens/scripts/download-grammars.js +9 -3
  359. package/node_modules/pi-lens/scripts/rpc-load-check.mjs +30 -9
  360. package/node_modules/pi-mcp-adapter/CHANGELOG.md +47 -0
  361. package/node_modules/pi-mcp-adapter/README.md +42 -13
  362. package/node_modules/pi-mcp-adapter/commands.ts +4 -2
  363. package/node_modules/pi-mcp-adapter/config.ts +45 -5
  364. package/node_modules/pi-mcp-adapter/direct-tools.ts +103 -92
  365. package/node_modules/pi-mcp-adapter/dist/agent-dir.d.ts +10 -0
  366. package/node_modules/pi-mcp-adapter/dist/agent-dir.js +63 -0
  367. package/node_modules/pi-mcp-adapter/dist/agent-dir.js.map +1 -0
  368. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.d.ts +8 -0
  369. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +356 -0
  370. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -0
  371. package/node_modules/pi-mcp-adapter/dist/config.d.ts +123 -0
  372. package/node_modules/pi-mcp-adapter/dist/config.js +1087 -0
  373. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -0
  374. package/node_modules/pi-mcp-adapter/dist/metadata-cache.d.ts +19 -0
  375. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +319 -0
  376. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -0
  377. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.d.ts +2 -0
  378. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +156 -0
  379. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -0
  380. package/node_modules/pi-mcp-adapter/dist/resource-tools.d.ts +1 -0
  381. package/node_modules/pi-mcp-adapter/dist/resource-tools.js +15 -0
  382. package/node_modules/pi-mcp-adapter/dist/resource-tools.js.map +1 -0
  383. package/node_modules/pi-mcp-adapter/dist/types.d.ts +564 -0
  384. package/node_modules/pi-mcp-adapter/dist/types.js +287 -0
  385. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -0
  386. package/node_modules/pi-mcp-adapter/dist/ui-app-bridge-helpers.d.ts +6 -0
  387. package/node_modules/pi-mcp-adapter/dist/ui-app-bridge-helpers.js +37 -0
  388. package/node_modules/pi-mcp-adapter/dist/ui-app-bridge-helpers.js.map +1 -0
  389. package/node_modules/pi-mcp-adapter/dist/ui-stream-types.d.ts +104 -0
  390. package/node_modules/pi-mcp-adapter/dist/ui-stream-types.js +59 -0
  391. package/node_modules/pi-mcp-adapter/dist/ui-stream-types.js.map +1 -0
  392. package/node_modules/pi-mcp-adapter/dist/ui-tool-visibility.d.ts +4 -0
  393. package/node_modules/pi-mcp-adapter/dist/ui-tool-visibility.js +27 -0
  394. package/node_modules/pi-mcp-adapter/dist/ui-tool-visibility.js.map +1 -0
  395. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +32 -0
  396. package/node_modules/pi-mcp-adapter/dist/utils.js +338 -0
  397. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -0
  398. package/node_modules/pi-mcp-adapter/failure-backoff.ts +23 -0
  399. package/node_modules/pi-mcp-adapter/index.ts +204 -82
  400. package/node_modules/pi-mcp-adapter/init.ts +41 -30
  401. package/node_modules/pi-mcp-adapter/lifecycle.ts +16 -8
  402. package/node_modules/pi-mcp-adapter/mcp-code.ts +2 -0
  403. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +66 -16
  404. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +95 -7
  405. package/node_modules/pi-mcp-adapter/mcp-probe.ts +18 -4
  406. package/node_modules/pi-mcp-adapter/mcp-references.ts +368 -0
  407. package/node_modules/pi-mcp-adapter/mcp-status.ts +8 -19
  408. package/node_modules/pi-mcp-adapter/metadata-cache.ts +20 -12
  409. package/node_modules/pi-mcp-adapter/namespace-tools.ts +276 -0
  410. package/node_modules/pi-mcp-adapter/package.json +24 -6
  411. package/node_modules/pi-mcp-adapter/proxy-modes.ts +66 -10
  412. package/node_modules/pi-mcp-adapter/search-ranking.ts +2 -0
  413. package/node_modules/pi-mcp-adapter/server-manager.ts +63 -12
  414. package/node_modules/pi-mcp-adapter/state.ts +1 -3
  415. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +16 -2
  416. package/node_modules/pi-mcp-adapter/types.ts +48 -1
  417. package/node_modules/pi-mcp-adapter/ui-server.ts +3 -19
  418. package/node_modules/pi-mcp-adapter/ui-session.ts +2 -1
  419. package/node_modules/pi-mcp-adapter/utils.ts +21 -19
  420. package/node_modules/pi-web-access/CHANGELOG.md +26 -0
  421. package/node_modules/pi-web-access/README.md +71 -14
  422. package/node_modules/pi-web-access/chrome-cookies.ts +115 -35
  423. package/node_modules/pi-web-access/credential-source.ts +6 -2
  424. package/node_modules/pi-web-access/curator-page.ts +5 -3
  425. package/node_modules/pi-web-access/curator-server.ts +2 -1
  426. package/node_modules/pi-web-access/extract.ts +89 -9
  427. package/node_modules/pi-web-access/fetch-params.ts +13 -0
  428. package/node_modules/pi-web-access/gemini-adc.ts +298 -0
  429. package/node_modules/pi-web-access/gemini-api.ts +59 -15
  430. package/node_modules/pi-web-access/gemini-search.ts +58 -18
  431. package/node_modules/pi-web-access/gemini-url-context.ts +3 -2
  432. package/node_modules/pi-web-access/gemini-web-config.ts +57 -7
  433. package/node_modules/pi-web-access/gemini-web.ts +9 -3
  434. package/node_modules/pi-web-access/github-api.ts +5 -4
  435. package/node_modules/pi-web-access/github-issue-pr.ts +700 -0
  436. package/node_modules/pi-web-access/index.ts +403 -297
  437. package/node_modules/pi-web-access/kimi-search.ts +234 -0
  438. package/node_modules/pi-web-access/openai-search.ts +118 -21
  439. package/node_modules/pi-web-access/package.json +3 -2
  440. package/node_modules/pi-web-access/ssrf-protection.ts +4 -1
  441. package/node_modules/pi-web-access/utils.ts +307 -1
  442. package/node_modules/pi-web-access/youtube-extract.ts +2 -2
  443. package/package.json +46 -37
  444. package/node_modules/@herbertgao/pi-copy-response/CHANGELOG.md +0 -49
  445. package/node_modules/@herbertgao/pi-copy-response/LICENSE +0 -22
  446. package/node_modules/@herbertgao/pi-handoff/CHANGELOG.md +0 -83
  447. package/node_modules/@herbertgao/pi-handoff/LICENSE +0 -22
  448. package/node_modules/@herbertgao/pi-handoff/package.json +0 -60
  449. package/node_modules/@herbertgao/pi-inline-skills/CHANGELOG.md +0 -57
  450. package/node_modules/@herbertgao/pi-inline-skills/LICENSE +0 -22
  451. package/node_modules/@herbertgao/pi-mermaid-open/CHANGELOG.md +0 -39
  452. package/node_modules/@herbertgao/pi-preferred-thinking/CHANGELOG.md +0 -63
  453. package/node_modules/@herbertgao/pi-preferred-thinking/LICENSE +0 -22
  454. package/node_modules/@herbertgao/pi-recap/CHANGELOG.md +0 -101
  455. package/node_modules/@herbertgao/pi-recap/LICENSE +0 -22
  456. package/node_modules/@herbertgao/pi-recap/package.json +0 -55
  457. package/node_modules/@herbertgao/pi-rename/CHANGELOG.md +0 -49
  458. package/node_modules/@herbertgao/pi-rename/LICENSE +0 -22
  459. package/node_modules/@herbertgao/pi-rename/package.json +0 -63
  460. package/node_modules/@herbertgao/pi-titlebar-spinner/CHANGELOG.md +0 -25
  461. package/node_modules/@herbertgao/pi-titlebar-spinner/LICENSE +0 -22
  462. /package/node_modules/{@herbertgao → @tifan}/pi-copy-response/src/index.ts +0 -0
  463. /package/node_modules/{@herbertgao → @tifan}/pi-handoff/src/complete-text.ts +0 -0
  464. /package/node_modules/{@herbertgao → @tifan}/pi-handoff/src/session-query.ts +0 -0
  465. /package/node_modules/{@herbertgao → @tifan}/pi-inline-skills/src/index.ts +0 -0
  466. /package/node_modules/{@herbertgao → @tifan}/pi-mermaid-open/herdr-plugin/herdr-plugin.toml +0 -0
  467. /package/node_modules/{@herbertgao → @tifan}/pi-mermaid-open/herdr-plugin/viewer.mjs +0 -0
  468. /package/node_modules/{@herbertgao → @tifan}/pi-mermaid-open/src/index.ts +0 -0
  469. /package/node_modules/{@herbertgao → @tifan}/pi-recap/src/index.ts +0 -0
  470. /package/node_modules/{@herbertgao → @tifan}/pi-recap/src/model-picker.ts +0 -0
  471. /package/node_modules/{@herbertgao → @tifan}/pi-recap/src/sanitize.ts +0 -0
  472. /package/node_modules/{@herbertgao → @tifan}/pi-recap/src/tui.ts +0 -0
  473. /package/node_modules/{@herbertgao → @tifan}/pi-rename/src/model-picker.ts +0 -0
  474. /package/node_modules/{@herbertgao → @tifan}/pi-rename/src/naming.ts +0 -0
  475. /package/node_modules/{@herbertgao → @tifan}/pi-rename/src/sanitize.ts +0 -0
  476. /package/node_modules/{@herbertgao → @tifan}/pi-titlebar-spinner/src/index.ts +0 -0
@@ -29,12 +29,18 @@
29
29
  * flags that something (pi-lens or otherwise) broke the cache prefix. This
30
30
  * remains a local observation, never a provider cache-miss claim.
31
31
  *
32
- * #1071 adds miss ATTRIBUTION on top of those three, without a new phase. Each
33
- * `cache_usage` record carries the inter-turn gap and a `cacheMissCause`
34
- * verdict, and each `cache_context` record splits a mixed injection payload by
35
- * contributing source. Both ride records that already fire once per turn, and
36
- * both derive from state this module already keeps in process — nothing reads
37
- * latency.log back.
32
+ * #1071 adds miss ATTRIBUTION on top of those three. Each `cache_usage` record
33
+ * carries the inter-turn gap and a `cacheMissCause` verdict, and each
34
+ * `cache_context` record splits a mixed injection payload by contributing
35
+ * source. #1996 makes every unexplained miss name the absent/conflicting
36
+ * evidence and emits one fixed-key `cache_usage_summary` at session shutdown.
37
+ * Heuristic causes require complete request hashing and full provider/model
38
+ * identity evidence. Identity comparison uses SHA-256 over the full normalized
39
+ * value while logs retain only a bounded display prefix; malformed numeric
40
+ * usage is sanitized before it reaches JSON metadata.
41
+ * The per-turn records still ride events that already fire once per turn, and
42
+ * all attribution derives from bounded in-process state — nothing reads
43
+ * latency.log back or serializes transcript content.
38
44
  *
39
45
  * All paths are defensive: `usage` (and its fields) may be absent on older
40
46
  * hosts or non-assistant messages, so every access is guarded and the handlers
@@ -122,13 +128,49 @@ const PARTIAL_EVICTION_MIN_FRESH_TOKENS = 512;
122
128
  const MAX_ATTRIBUTION_CHARS = 262_144;
123
129
  /** Byte companion to {@link MAX_ATTRIBUTION_CHARS}, at four bytes per char. */
124
130
  const MAX_ATTRIBUTION_BYTES = 1_048_576;
131
+ function emptyCacheMissCauses() {
132
+ return {
133
+ "ttl-expired": 0,
134
+ "prefix-broke": 0,
135
+ "partial-eviction": 0,
136
+ "model-provider-changed": 0,
137
+ unknown: 0,
138
+ };
139
+ }
140
+ function emptyUnknownEvidenceReasons() {
141
+ return {
142
+ "no-prior-sample": 0,
143
+ "cache-read-unavailable": 0,
144
+ "malformed-provider-usage": 0,
145
+ "request-correlation-unavailable": 0,
146
+ "request-evidence-incomplete": 0,
147
+ "sequence-hash-truncated": 0,
148
+ "model-provider-unavailable": 0,
149
+ "provider-reported-zero-stable-prefix": 0,
150
+ "provider-reported-low-read-stable-prefix": 0,
151
+ "no-local-explanation": 0,
152
+ };
153
+ }
154
+ function newCacheUsageSummary() {
155
+ return {
156
+ usageRecords: 0,
157
+ cacheHits: 0,
158
+ missObservations: 0,
159
+ cacheMissCauses: emptyCacheMissCauses(),
160
+ unknownEvidenceReasons: emptyUnknownEvidenceReasons(),
161
+ };
162
+ }
125
163
  const attributionBySession = new Map();
126
164
  function newAttributionState() {
127
165
  return {
128
166
  prefixBrokeSinceLastUsage: false,
167
+ prefixStableSinceLastUsage: false,
168
+ sequenceHashTruncatedSinceLastUsage: false,
169
+ requestEvidenceIncompleteSinceLastUsage: false,
129
170
  injectedCharsSinceLastUsage: 0,
130
171
  newTranscriptCharsSinceLastUsage: 0,
131
172
  attributionCharsCapped: false,
173
+ summary: newCacheUsageSummary(),
132
174
  };
133
175
  }
134
176
  /**
@@ -207,10 +249,50 @@ function resolveInterTurnGap(state, nowMs) {
207
249
  function estimateTokens(chars) {
208
250
  return Math.ceil(chars / CHARS_PER_ESTIMATED_TOKEN);
209
251
  }
252
+ function readableTokenCount(value) {
253
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
254
+ ? value
255
+ : undefined;
256
+ }
257
+ function readNumericEvidence(record, key) {
258
+ let raw;
259
+ try {
260
+ raw = record[key];
261
+ }
262
+ catch {
263
+ return { status: "malformed" };
264
+ }
265
+ if (raw === undefined)
266
+ return { status: "absent" };
267
+ const value = readableTokenCount(raw);
268
+ return value === undefined
269
+ ? { status: "malformed" }
270
+ : { status: "available", value };
271
+ }
272
+ const MAX_LOGGED_IDENTITY_CHARS = 200;
273
+ /**
274
+ * Keep the human-readable identity bounded while comparing a SHA-256 digest of
275
+ * the FULL normalized value. Comparing the display prefix would make two long
276
+ * provider/model ids that differ after the cap collide (#1996 review).
277
+ */
278
+ function identityEvidence(value) {
279
+ if (typeof value !== "string")
280
+ return { status: "unavailable" };
281
+ const normalized = value.trim();
282
+ if (!normalized)
283
+ return { status: "unavailable" };
284
+ const truncated = normalized.length > MAX_LOGGED_IDENTITY_CHARS;
285
+ return {
286
+ display: normalized.slice(0, MAX_LOGGED_IDENTITY_CHARS),
287
+ hash: createHash("sha256").update(normalized).digest("hex"),
288
+ status: truncated ? "truncated" : "complete",
289
+ };
290
+ }
210
291
  /**
211
- * Classify a turn's prompt-cache shortfall. Returns `null` when there is
212
- * nothing to explain: no prior usage record in this session (the first turn
213
- * legitimately reads nothing), an unreadable `cacheRead`, or a healthy read.
292
+ * Classify a turn's prompt-cache shortfall. Returns `null` when no comparable
293
+ * miss exists: no prior readable usage record or a healthy read. The caller
294
+ * separately records why an unreadable/first zero sample could not be
295
+ * classified, so those evidence gaps remain visible in the session summary.
214
296
  *
215
297
  * Rules, in the order they are applied, each stated with the evidence behind
216
298
  * it (2026-08-21 investigation, 63 sessions, 2,288 turns, 94.2% overall hit
@@ -221,25 +303,33 @@ function estimateTokens(chars) {
221
303
  * the cause, so it outranks the timing heuristics below. The window was
222
304
  * byte-stable throughout, so this verdict should stay rare; if it does
223
305
  * not, #1016's fix has regressed.
224
- * 2. `ttl-expired` — the gap since the previous usage record reached the
225
- * provider TTL threshold. The 34 zero-read turns had a median gap of 166s
226
- * against 9s elsewhere, and carried 35.1% of all fresh input.
227
- * 3. `partial-eviction` — a non-zero but low read, where fresh input runs far
306
+ * 2. `model-provider-changed` — both adjacent identities were present and
307
+ * differ. This is direct local evidence and outranks timing heuristics.
308
+ * 3. Evidence gates heuristic causes require a REQUEST-time observation,
309
+ * a complete bounded sequence hash, adjacent full provider/model identity,
310
+ * and well-formed provider usage numbers.
311
+ * Missing evidence returns a specific unknown reason; it never lets a
312
+ * plausible but unproved heuristic win.
313
+ * 4. `ttl-expired` — a REQUEST-time gap reached the configured provider TTL.
314
+ * A message-end fallback includes generation time and cannot prove this.
315
+ * 5. `partial-eviction` — a non-zero but low read, where fresh input runs far
228
316
  * above the new content we measured. pi-lens injections average ~2.7
229
317
  * estimated tokens per turn, so they cannot account for a multiple of the
230
318
  * measured content; the remainder is provider-side eviction, typically at
231
319
  * a context limit. Suppressed when the character accumulators were capped,
232
320
  * because a floor estimate would manufacture the verdict.
233
- * 4. `unknown` — a real shortfall with no local explanation. Reported, never
234
- * guessed at.
321
+ * 6. `unknown` — a real shortfall with no local explanation. A bounded
322
+ * `cacheMissUnknownReason` names the missing or conflicting evidence;
323
+ * provider behavior is never guessed.
235
324
  */
236
325
  function classifyCacheMiss(args) {
237
- const { cacheRead, priorCacheRead } = args;
238
- if (typeof cacheRead !== "number" || !Number.isFinite(cacheRead))
326
+ const cacheRead = readableTokenCount(args.cacheRead);
327
+ if (cacheRead === undefined)
239
328
  return null;
240
329
  // No baseline turn in this session: a first turn reads zero because nothing
241
330
  // is cached yet, not because anything went wrong.
242
- if (typeof priorCacheRead !== "number" || !Number.isFinite(priorCacheRead)) {
331
+ const priorCacheRead = readableTokenCount(args.priorCacheRead);
332
+ if (priorCacheRead === undefined) {
243
333
  return null;
244
334
  }
245
335
  let kind;
@@ -253,8 +343,63 @@ function classifyCacheMiss(args) {
253
343
  else {
254
344
  return null;
255
345
  }
346
+ // A missing stable session id makes every cross-event baseline ambiguous. Do
347
+ // not let the shared fallback bucket turn another session's evidence into a
348
+ // cause claim (#1996 concurrent primary/secondary invariant).
349
+ if (!args.stableSessionIdentity) {
350
+ return {
351
+ kind,
352
+ cause: "unknown",
353
+ unknownReason: "request-correlation-unavailable",
354
+ };
355
+ }
256
356
  if (args.prefixBroke)
257
357
  return { kind, cause: "prefix-broke" };
358
+ if (args.modelProviderChanged) {
359
+ return { kind, cause: "model-provider-changed" };
360
+ }
361
+ // TTL and transcript-growth claims need the request-side context observation.
362
+ // A response-time fallback includes generation and can only explain why the
363
+ // verdict stayed unknown; it cannot establish cache expiry.
364
+ if (args.gapBasis !== "request-time") {
365
+ return {
366
+ kind,
367
+ cause: "unknown",
368
+ unknownReason: "request-correlation-unavailable",
369
+ };
370
+ }
371
+ // TTL and eviction are heuristics. They are only evidence-backed if the
372
+ // bounded request observation is complete and both adjacent usage records
373
+ // identify the same provider/model. Otherwise an unseen prompt or identity
374
+ // change remains a competing explanation, so classification must fail closed.
375
+ if (args.requestEvidenceIncomplete) {
376
+ return {
377
+ kind,
378
+ cause: "unknown",
379
+ unknownReason: "request-evidence-incomplete",
380
+ };
381
+ }
382
+ if (args.sequenceHashTruncated) {
383
+ return {
384
+ kind,
385
+ cause: "unknown",
386
+ unknownReason: "sequence-hash-truncated",
387
+ };
388
+ }
389
+ if (args.providerUsageMalformed) {
390
+ return {
391
+ kind,
392
+ cause: "unknown",
393
+ unknownReason: "malformed-provider-usage",
394
+ };
395
+ }
396
+ if (!args.modelProviderEvidenceAvailable) {
397
+ return {
398
+ kind,
399
+ cause: "unknown",
400
+ unknownReason: "model-provider-unavailable",
401
+ };
402
+ }
258
403
  if (args.interTurnGapMs !== null && args.interTurnGapMs >= args.ttlMs) {
259
404
  return { kind, cause: "ttl-expired" };
260
405
  }
@@ -264,7 +409,58 @@ function classifyCacheMiss(args) {
264
409
  Math.max(PARTIAL_EVICTION_MIN_FRESH_TOKENS, args.estimatedNewTokens * PARTIAL_EVICTION_INPUT_FACTOR)) {
265
410
  return { kind, cause: "partial-eviction" };
266
411
  }
267
- return { kind, cause: "unknown" };
412
+ if (args.prefixStable) {
413
+ return {
414
+ kind,
415
+ cause: "unknown",
416
+ unknownReason: kind === "zero-read"
417
+ ? "provider-reported-zero-stable-prefix"
418
+ : "provider-reported-low-read-stable-prefix",
419
+ };
420
+ }
421
+ return {
422
+ kind,
423
+ cause: "unknown",
424
+ unknownReason: "no-local-explanation",
425
+ };
426
+ }
427
+ function recordCacheUsageSummary(args) {
428
+ const summary = args.state.summary;
429
+ summary.usageRecords += 1;
430
+ let unknownReasonRecorded = false;
431
+ if (args.unknownReason === "malformed-provider-usage") {
432
+ summary.unknownEvidenceReasons["malformed-provider-usage"] += 1;
433
+ unknownReasonRecorded = true;
434
+ }
435
+ if (args.cacheRead === undefined) {
436
+ if (!unknownReasonRecorded) {
437
+ summary.unknownEvidenceReasons["cache-read-unavailable"] += 1;
438
+ }
439
+ return;
440
+ }
441
+ if (args.priorCacheRead === undefined) {
442
+ if (args.cacheRead === 0) {
443
+ summary.missObservations += 1;
444
+ if (!unknownReasonRecorded) {
445
+ summary.unknownEvidenceReasons["no-prior-sample"] += 1;
446
+ }
447
+ }
448
+ else {
449
+ summary.cacheHits += 1;
450
+ }
451
+ return;
452
+ }
453
+ if (!args.verdict) {
454
+ summary.cacheHits += 1;
455
+ return;
456
+ }
457
+ summary.missObservations += 1;
458
+ summary.cacheMissCauses[args.verdict.cause] += 1;
459
+ if (args.verdict.cause === "unknown" &&
460
+ args.unknownReason &&
461
+ !unknownReasonRecorded) {
462
+ summary.unknownEvidenceReasons[args.unknownReason] += 1;
463
+ }
268
464
  }
269
465
  function boundedHashScalar(value, state) {
270
466
  if (value === null)
@@ -303,16 +499,18 @@ function boundedHashObject(value, depth, seen, state) {
303
499
  return `object:${keys.length}:{${fields.join(",")}}`;
304
500
  }
305
501
  function boundedHashValue(value, depth = 0, seen = new Set(), state) {
306
- const hashState = state ?? { contentTruncated: false };
502
+ const hashState = state ?? { contentTruncated: false, incomplete: false };
307
503
  if (value === null || typeof value !== "object") {
308
504
  return boundedHashScalar(value, hashState);
309
505
  }
310
506
  if (depth >= 3) {
311
507
  hashState.contentTruncated = true;
312
- return Object.prototype.toString.call(value);
508
+ return "[depth-limit]";
313
509
  }
314
- if (seen.has(value))
510
+ if (seen.has(value)) {
511
+ hashState.incomplete = true;
315
512
  return "[cycle]";
513
+ }
316
514
  seen.add(value);
317
515
  try {
318
516
  if (Array.isArray(value)) {
@@ -321,6 +519,7 @@ function boundedHashValue(value, depth = 0, seen = new Set(), state) {
321
519
  return boundedHashObject(value, depth, seen, hashState);
322
520
  }
323
521
  catch {
522
+ hashState.incomplete = true;
324
523
  return "[unreadable]";
325
524
  }
326
525
  finally {
@@ -329,21 +528,53 @@ function boundedHashValue(value, depth = 0, seen = new Set(), state) {
329
528
  }
330
529
  function hashMessageSequence(messages) {
331
530
  const hash = createHash("sha256");
332
- const state = { contentTruncated: false };
333
- hash.update(`message-count:${messages.length};`);
334
- const sampled = Math.min(messages.length, MAX_HASHED_MESSAGES);
531
+ const state = {
532
+ contentTruncated: false,
533
+ incomplete: false,
534
+ };
535
+ let messageCount;
536
+ try {
537
+ messageCount = messages.length;
538
+ if (!Number.isSafeInteger(messageCount) || messageCount < 0) {
539
+ throw new Error("invalid message count");
540
+ }
541
+ }
542
+ catch {
543
+ state.incomplete = true;
544
+ hash.update("message-count:[unreadable];");
545
+ return {
546
+ hash: hash.digest("hex"),
547
+ truncated: false,
548
+ contentTruncated: false,
549
+ incomplete: true,
550
+ };
551
+ }
552
+ hash.update(`message-count:${messageCount};`);
553
+ const sampled = Math.min(messageCount, MAX_HASHED_MESSAGES);
335
554
  for (let i = 0; i < sampled; i++) {
336
- const message = messages[i];
337
555
  const seen = new Set();
338
- hash.update(`${i}|role:${boundedHashValue(message?.role, 0, seen, state)}|content:${boundedHashValue(message?.content, 0, seen, state)};`);
556
+ let role;
557
+ let content;
558
+ try {
559
+ const message = messages[i];
560
+ role = message?.role;
561
+ content = message?.content;
562
+ }
563
+ catch {
564
+ state.incomplete = true;
565
+ role = "[unreadable]";
566
+ content = "[unreadable]";
567
+ }
568
+ hash.update(`${i}|role:${boundedHashValue(role, 0, seen, state)}|content:${boundedHashValue(content, 0, seen, state)};`);
339
569
  }
340
- const truncated = messages.length > sampled;
570
+ const truncated = messageCount > sampled;
341
571
  if (truncated)
342
572
  hash.update(`truncated-after:${sampled};`);
343
573
  return {
344
574
  hash: hash.digest("hex"),
345
575
  truncated,
346
576
  contentTruncated: state.contentTruncated,
577
+ incomplete: state.incomplete,
347
578
  };
348
579
  }
349
580
  /** Bounds for reporting an injected payload's size in the record. */
@@ -364,7 +595,10 @@ function messageTextSize(content, limits) {
364
595
  bytes: Math.min(Buffer.byteLength(content.slice(0, chars), "utf8"), limits.maxBytes),
365
596
  };
366
597
  }
367
- const bounded = boundedHashValue(content);
598
+ const bounded = boundedHashValue(content, 0, new Set(), {
599
+ contentTruncated: false,
600
+ incomplete: false,
601
+ });
368
602
  return {
369
603
  chars: Math.min(bounded.length, limits.maxChars),
370
604
  bytes: Math.min(Buffer.byteLength(bounded.slice(0, limits.maxChars), "utf8"), limits.maxBytes),
@@ -404,6 +638,18 @@ function measureTranscriptGrowth(messages) {
404
638
  function sessionKey(sessionId) {
405
639
  return sessionId?.trim() ? sessionId.trim() : NO_SESSION_KEY;
406
640
  }
641
+ /**
642
+ * A stable host session id is the only safe cross-event identity. When it is
643
+ * absent, keep primary and concurrent-secondary observations in separate
644
+ * buckets and let classification fail closed instead of allowing a secondary
645
+ * to overwrite the primary's baseline.
646
+ */
647
+ function attributionKey(sessionId, sessionRole) {
648
+ const key = sessionKey(sessionId);
649
+ if (key !== NO_SESSION_KEY)
650
+ return key;
651
+ return `${sessionRole ?? "unknown"}:${key}`;
652
+ }
407
653
  /**
408
654
  * Log one bounded request-side observation for every `context` call. This is
409
655
  * deliberately a separate phase from `cache_prefix_break`: it describes the
@@ -443,7 +689,15 @@ export function observeCacheContext(args) {
443
689
  });
444
690
  const messageCountCapped = existingMessages.length > MAX_REPORTED_MESSAGES ||
445
691
  resultMessages.length > MAX_REPORTED_MESSAGES;
446
- recordContextAttribution(sessionKey(args.sessionId), existingMessages, sizes);
692
+ recordContextAttribution(attributionKey(args.sessionId, args.sessionRole), existingMessages, sizes);
693
+ const state = attributionFor(attributionKey(args.sessionId, args.sessionRole));
694
+ state.sequenceHashTruncatedSinceLastUsage ||=
695
+ beforeSequence.truncated ||
696
+ beforeSequence.contentTruncated ||
697
+ afterSequence.truncated ||
698
+ afterSequence.contentTruncated;
699
+ state.requestEvidenceIncompleteSinceLastUsage ||=
700
+ beforeSequence.incomplete || afterSequence.incomplete;
447
701
  logLatency({
448
702
  type: "phase",
449
703
  filePath: "<pi-lens>",
@@ -481,10 +735,22 @@ export function observeCacheContext(args) {
481
735
  afterSequence.contentTruncated,
482
736
  sequenceMessageCountTruncated: beforeSequence.truncated || afterSequence.truncated,
483
737
  sequenceContentHashTruncated: beforeSequence.contentTruncated || afterSequence.contentTruncated,
738
+ sequenceHashIncomplete: beforeSequence.incomplete || afterSequence.incomplete,
484
739
  },
485
740
  });
486
741
  }
487
742
  catch (err) {
743
+ // The context event still occurred at request time. Preserve that clock but
744
+ // fail the evidence closed so a later usage row cannot infer TTL/eviction
745
+ // from an observation whose hashing/measurement never completed.
746
+ try {
747
+ const state = attributionFor(attributionKey(args.sessionId, args.sessionRole));
748
+ state.lastContextAtMs = Date.now();
749
+ state.requestEvidenceIncompleteSinceLastUsage = true;
750
+ }
751
+ catch {
752
+ // The outer contract is no-throw; there is no safe attribution key left.
753
+ }
488
754
  args.dbg?.(`cache-context: failed to log context observation: ${err}`);
489
755
  }
490
756
  }
@@ -508,28 +774,109 @@ export function logCacheUsage(message, dbg, context) {
508
774
  const usage = msg.usage;
509
775
  if (!usage || typeof usage !== "object")
510
776
  return;
511
- const u = usage;
777
+ // SAFETY: `usage` is verified non-null object above; widening to an
778
+ // indexable record is only so the numeric-evidence readers can probe
779
+ // each field defensively. Every read re-validates its own type.
780
+ const usageRecord = usage;
512
781
  // #1071: attribute the shortfall in-process, from state this module already
513
782
  // keeps, so nothing re-reads latency.log.
514
- const attributionKey = sessionKey(context?.sessionId);
515
- const state = attributionFor(attributionKey);
783
+ const stateKey = attributionKey(context?.sessionId, context?.sessionRole);
784
+ const state = attributionFor(stateKey);
516
785
  const nowMs = Date.now();
517
786
  const gap = resolveInterTurnGap(state, nowMs);
518
787
  const interTurnGapMs = gap.gapMs;
519
788
  const ttlMs = getProviderCacheTtlMs();
789
+ const inputEvidence = readNumericEvidence(usageRecord, "input");
790
+ const outputEvidence = readNumericEvidence(usageRecord, "output");
791
+ const cacheReadEvidence = readNumericEvidence(usageRecord, "cacheRead");
792
+ const cacheWriteEvidence = readNumericEvidence(usageRecord, "cacheWrite");
793
+ let costEvidence;
794
+ try {
795
+ const cost = usageRecord.cost;
796
+ costEvidence =
797
+ cost === undefined
798
+ ? { status: "absent" }
799
+ : cost && typeof cost === "object"
800
+ ? readNumericEvidence(cost, "total")
801
+ : { status: "malformed" };
802
+ }
803
+ catch {
804
+ costEvidence = { status: "malformed" };
805
+ }
806
+ const usageEvidenceFields = [
807
+ ["input", inputEvidence],
808
+ ["output", outputEvidence],
809
+ ["cacheRead", cacheReadEvidence],
810
+ ["cacheWrite", cacheWriteEvidence],
811
+ ["cost.total", costEvidence],
812
+ ];
813
+ const malformedProviderUsageFields = usageEvidenceFields
814
+ .filter(([, evidence]) => evidence.status === "malformed")
815
+ .map(([field]) => field);
816
+ const providerUsageMalformed = malformedProviderUsageFields.length > 0;
817
+ const cacheRead = cacheReadEvidence.value;
818
+ const priorCacheReadValue = readableTokenCount(state.lastCacheRead);
819
+ // SAFETY: `msg` passed the AssistantMessageLike role check above; the
820
+ // record view backs only the guarded identity-field probes below, each
821
+ // re-validated by `identityEvidence` at its own boundary.
822
+ const messageRecord = msg;
823
+ const safeMessageField = (key) => {
824
+ try {
825
+ return identityEvidence(messageRecord[key]);
826
+ }
827
+ catch {
828
+ return { status: "unavailable" };
829
+ }
830
+ };
831
+ const currentProvider = safeMessageField("provider");
832
+ const responseModel = safeMessageField("responseModel");
833
+ const currentModel = responseModel.hash !== undefined
834
+ ? responseModel
835
+ : safeMessageField("model");
836
+ const modelProviderEvidenceAvailable = Boolean(currentProvider.hash &&
837
+ currentModel.hash &&
838
+ state.lastProviderIdentityHash &&
839
+ state.lastModelIdentityHash);
840
+ const modelProviderChanged = Boolean(modelProviderEvidenceAvailable &&
841
+ (currentProvider.hash !== state.lastProviderIdentityHash ||
842
+ currentModel.hash !== state.lastModelIdentityHash));
520
843
  const estimatedNewTokens = estimateTokens(state.injectedCharsSinceLastUsage +
521
844
  state.newTranscriptCharsSinceLastUsage);
522
845
  const verdict = classifyCacheMiss({
523
- cacheRead: u.cacheRead,
524
- input: typeof u.input === "number" ? u.input : 0,
525
- priorCacheRead: state.lastCacheRead,
846
+ cacheRead,
847
+ // Missing/malformed input cannot prove fresh-input pressure. The explicit
848
+ // malformed gate below also keeps every heuristic fail-closed.
849
+ input: inputEvidence.value ?? 0,
850
+ priorCacheRead: priorCacheReadValue,
526
851
  interTurnGapMs,
852
+ gapBasis: gap.basis,
527
853
  ttlMs,
528
854
  prefixBroke: state.prefixBrokeSinceLastUsage,
855
+ prefixStable: state.prefixStableSinceLastUsage,
856
+ sequenceHashTruncated: state.sequenceHashTruncatedSinceLastUsage,
857
+ requestEvidenceIncomplete: state.requestEvidenceIncompleteSinceLastUsage,
858
+ stableSessionIdentity: sessionKey(context?.sessionId) !== NO_SESSION_KEY,
859
+ modelProviderChanged,
860
+ modelProviderEvidenceAvailable,
861
+ providerUsageMalformed,
529
862
  estimatedNewTokens,
530
863
  attributionCharsCapped: state.attributionCharsCapped,
531
864
  });
532
- const priorCacheRead = typeof state.lastCacheRead === "number" ? state.lastCacheRead : null;
865
+ const unknownReason = providerUsageMalformed
866
+ ? "malformed-provider-usage"
867
+ : cacheRead === undefined
868
+ ? "cache-read-unavailable"
869
+ : priorCacheReadValue === undefined && cacheRead === 0
870
+ ? "no-prior-sample"
871
+ : verdict?.unknownReason;
872
+ recordCacheUsageSummary({
873
+ state,
874
+ cacheRead,
875
+ priorCacheRead: priorCacheReadValue,
876
+ verdict,
877
+ unknownReason,
878
+ });
879
+ const priorCacheRead = priorCacheReadValue ?? null;
533
880
  const injectedCharsSinceLastTurn = state.injectedCharsSinceLastUsage;
534
881
  const newTranscriptCharsSinceLastTurn = state.newTranscriptCharsSinceLastUsage;
535
882
  const attributionCharsCapped = state.attributionCharsCapped;
@@ -540,11 +887,13 @@ export function logCacheUsage(message, dbg, context) {
540
887
  // Clear the stored one rather than keeping it: the next turn's gap would be
541
888
  // one turn long while its baseline was two turns old, and a verdict built
542
889
  // on that mismatch is worse than no verdict (#1071 review round 1, F4).
543
- state.lastCacheRead =
544
- typeof u.cacheRead === "number" && Number.isFinite(u.cacheRead)
545
- ? u.cacheRead
546
- : undefined;
890
+ state.lastCacheRead = cacheRead;
891
+ state.lastProviderIdentityHash = currentProvider.hash;
892
+ state.lastModelIdentityHash = currentModel.hash;
547
893
  state.prefixBrokeSinceLastUsage = false;
894
+ state.prefixStableSinceLastUsage = false;
895
+ state.sequenceHashTruncatedSinceLastUsage = false;
896
+ state.requestEvidenceIncompleteSinceLastUsage = false;
548
897
  state.injectedCharsSinceLastUsage = 0;
549
898
  state.newTranscriptCharsSinceLastUsage = 0;
550
899
  state.attributionCharsCapped = false;
@@ -558,14 +907,21 @@ export function logCacheUsage(message, dbg, context) {
558
907
  phase: "cache_usage",
559
908
  durationMs: 0,
560
909
  metadata: {
561
- provider: typeof msg.provider === "string" ? msg.provider : undefined,
562
- model: typeof msg.model === "string" ? msg.model : undefined,
563
- cacheRead: u.cacheRead,
564
- cacheWrite: u.cacheWrite,
565
- input: u.input,
566
- output: u.output,
910
+ provider: currentProvider.display,
911
+ model: currentModel.display,
912
+ providerIdentityHash: currentProvider.hash,
913
+ modelIdentityHash: currentModel.hash,
914
+ providerIdentityStatus: currentProvider.status,
915
+ modelIdentityStatus: currentModel.status,
916
+ providerIdentityTruncated: currentProvider.status === "truncated",
917
+ modelIdentityTruncated: currentModel.status === "truncated",
918
+ cacheRead: cacheReadEvidence.value ?? null,
919
+ cacheWrite: cacheWriteEvidence.value ?? null,
920
+ input: inputEvidence.value ?? null,
921
+ output: outputEvidence.value ?? null,
567
922
  // `Usage.cost` is a breakdown object; the total is the headline number.
568
- cost: u.cost?.total,
923
+ cost: costEvidence.value ?? null,
924
+ providerUsageMalformedFields: malformedProviderUsageFields,
569
925
  // Idle milliseconds before this turn's request, or null for the first
570
926
  // record in the session. This is the field the 2026-08-21
571
927
  // investigation had to reconstruct by hand-joining timestamps.
@@ -575,6 +931,8 @@ export function logCacheUsage(message, dbg, context) {
575
931
  // the explicit no-local-explanation verdict.
576
932
  cacheMissCause: verdict?.cause ?? null,
577
933
  cacheMissKind: verdict?.kind ?? null,
934
+ cacheMissUnknownReason: unknownReason ?? null,
935
+ modelProviderChanged,
578
936
  cacheTtlThresholdMs: ttlMs,
579
937
  priorCacheRead,
580
938
  injectedCharsSinceLastTurn,
@@ -646,9 +1004,10 @@ function hashFirstMessage(first) {
646
1004
  const MAX_TRACKED_SESSIONS = 32;
647
1005
  const prefixHashBySession = new Map();
648
1006
  /**
649
- * Bucket key used when no session id is available (undefined/empty). Degrades
650
- * gracefully to the old single-var semantics — one shared baseline — rather than
651
- * throwing or dropping the signal.
1007
+ * Reported key used when no session id is available (undefined/empty). Internal
1008
+ * attribution additionally separates primary and concurrent-secondary roles;
1009
+ * neither role may overwrite the other's baseline when stable identity is
1010
+ * absent.
652
1011
  */
653
1012
  const NO_SESSION_KEY = "<no-session>";
654
1013
  /**
@@ -680,10 +1039,10 @@ function recordSessionHash(key, hash) {
680
1039
  * baseline. (`runtime.telemetrySessionId` cannot be used here: per the #473
681
1040
  * guard a concurrent secondary skips `updateRuntimeIdentityFromEvent`, so that
682
1041
  * process-global singleton stays pinned to the PARENT — it would collapse
683
- * parent and subagent onto one baseline.) Residual limitation: if the host ever
684
- * does NOT supply a stable id, all such sessions collapse onto `NO_SESSION_KEY`
685
- * and behave like the old single-var (a concurrent subagent could then still
686
- * cross-contaminate) accepted as graceful degradation, not silently perfect.
1042
+ * parent and subagent onto one baseline.) If the host does NOT supply a stable
1043
+ * id, primary and secondary roles still use separate fallback buckets, but
1044
+ * multiple same-role sessions cannot be correlated. Miss attribution therefore
1045
+ * fails closed with `request-correlation-unavailable`.
687
1046
  *
688
1047
  * Does nothing on an empty transcript (nothing to anchor a prefix to yet).
689
1048
  */
@@ -694,13 +1053,16 @@ export function observeCachePrefix(messages, turnIndex, sessionId, sessionRole,
694
1053
  const first = messages[0];
695
1054
  if (!first || typeof first !== "object")
696
1055
  return "empty";
697
- const key = sessionKey(sessionId);
1056
+ const key = attributionKey(sessionId, sessionRole);
1057
+ const reportedSessionId = sessionKey(sessionId);
698
1058
  const currentHash = hashFirstMessage(first);
699
1059
  const previousHash = prefixHashBySession.get(key);
1060
+ const state = attributionFor(key);
700
1061
  if (previousHash === undefined) {
701
1062
  // Baseline: record this session's starting prefix so a later break has a
702
1063
  // reference point in the log. `previousHash: null` marks the baseline.
703
1064
  recordSessionHash(key, currentHash);
1065
+ state.prefixStableSinceLastUsage = true;
704
1066
  logLatency({
705
1067
  type: "phase",
706
1068
  filePath: "<pi-lens>",
@@ -713,7 +1075,7 @@ export function observeCachePrefix(messages, turnIndex, sessionId, sessionRole,
713
1075
  previousHash: null,
714
1076
  currentHash,
715
1077
  baseline: true,
716
- sessionId: key,
1078
+ sessionId: reportedSessionId,
717
1079
  sessionRole,
718
1080
  },
719
1081
  });
@@ -722,7 +1084,8 @@ export function observeCachePrefix(messages, turnIndex, sessionId, sessionRole,
722
1084
  if (currentHash !== previousHash) {
723
1085
  // #1071: arm the direct-cause flag the next `cache_usage` verdict reads.
724
1086
  // A locally observed prefix change outranks the timing heuristics.
725
- attributionFor(key).prefixBrokeSinceLastUsage = true;
1087
+ state.prefixBrokeSinceLastUsage = true;
1088
+ state.prefixStableSinceLastUsage = false;
726
1089
  logLatency({
727
1090
  type: "phase",
728
1091
  filePath: "<pi-lens>",
@@ -734,11 +1097,14 @@ export function observeCachePrefix(messages, turnIndex, sessionId, sessionRole,
734
1097
  : { turnIndex, turnScope: PROCESS_TURN_SCOPE }),
735
1098
  previousHash,
736
1099
  currentHash,
737
- sessionId: key,
1100
+ sessionId: reportedSessionId,
738
1101
  sessionRole,
739
1102
  },
740
1103
  });
741
1104
  }
1105
+ if (currentHash === previousHash) {
1106
+ state.prefixStableSinceLastUsage = true;
1107
+ }
742
1108
  // Refresh recency (and update the stored hash after a break) so an active
743
1109
  // session stays warm in the LRU and isn't evicted while still in use.
744
1110
  recordSessionHash(key, currentHash);
@@ -750,20 +1116,49 @@ export function observeCachePrefix(messages, turnIndex, sessionId, sessionRole,
750
1116
  }
751
1117
  }
752
1118
  /**
753
- * Drop a single session's prefix baseline. Called from the `session_shutdown`
754
- * handler (primary path only the concurrent-secondary guard there returns
755
- * first, and the LRU cap backstops any secondary entry left behind) so an ended
756
- * conversation's entry is reclaimed promptly rather than only when the LRU
757
- * evicts it. Idempotent and never throws.
1119
+ * Drop a single session's prefix baseline and attribution state. Called from
1120
+ * both role-specific `session_shutdown` paths so an ended conversation is
1121
+ * reclaimed promptly rather than only when the LRU evicts it. Idempotent and
1122
+ * never throws.
758
1123
  */
759
- export function clearCachePrefixSession(sessionId) {
760
- const key = sessionKey(sessionId);
1124
+ export function clearCachePrefixSession(sessionId, sessionRole) {
1125
+ const key = attributionKey(sessionId, sessionRole);
761
1126
  prefixHashBySession.delete(key);
762
1127
  // The miss-attribution state has the same lifetime and the same reason to be
763
1128
  // reclaimed at session end; leaving it would let a reused session id inherit
764
1129
  // a stale gap baseline and a stale prefix-break flag.
765
1130
  attributionBySession.delete(key);
766
1131
  }
1132
+ /**
1133
+ * Emit one bounded session summary and retire its attribution state. The
1134
+ * summary contains fixed-key counters only; it never serializes transcript or
1135
+ * context content. A session with no usage records emits nothing.
1136
+ */
1137
+ export function emitCacheUsageSummaryAtSessionEnd(sessionId, sessionRole) {
1138
+ const key = attributionKey(sessionId, sessionRole);
1139
+ const state = attributionBySession.get(key);
1140
+ if (!state || state.summary.usageRecords === 0)
1141
+ return;
1142
+ logLatency({
1143
+ type: "phase",
1144
+ filePath: "<pi-lens>",
1145
+ phase: "cache_usage_summary",
1146
+ durationMs: 0,
1147
+ metadata: {
1148
+ version: 1,
1149
+ sessionId: sessionKey(sessionId),
1150
+ sessionRole,
1151
+ usageRecords: state.summary.usageRecords,
1152
+ cacheHits: state.summary.cacheHits,
1153
+ missObservations: state.summary.missObservations,
1154
+ cacheMissCauses: { ...state.summary.cacheMissCauses },
1155
+ unknownEvidenceReasons: {
1156
+ ...state.summary.unknownEvidenceReasons,
1157
+ },
1158
+ },
1159
+ });
1160
+ attributionBySession.delete(key);
1161
+ }
767
1162
  /**
768
1163
  * Clear all per-session prefix hashes and miss-attribution state. For tests and
769
1164
  * session boundaries. Both maps are cleared together so a test can never start