@herbertgao/pi-extensions 2026.9.6 → 2026.9.7

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 (240) hide show
  1. package/README.md +3 -8
  2. package/THIRD_PARTY_NOTICES.md +0 -4
  3. package/node_modules/@narumitw/pi-btw/README.md +2 -0
  4. package/node_modules/@narumitw/pi-btw/dist/index.ts +284 -365
  5. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  6. package/node_modules/@narumitw/pi-btw/package.json +52 -53
  7. package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +494 -547
  8. package/node_modules/@narumitw/pi-btw/src/btw.ts +814 -856
  9. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +673 -674
  10. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +228 -270
  11. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +309 -318
  12. package/node_modules/@narumitw/pi-btw/src/menu.ts +416 -454
  13. package/node_modules/@narumitw/pi-btw/src/settings.ts +203 -219
  14. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +173 -201
  15. package/node_modules/@narumitw/pi-btw/src/text.ts +21 -23
  16. package/node_modules/@narumitw/pi-btw/src/transcript-markdown.ts +65 -0
  17. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +611 -662
  18. package/node_modules/pi-lens/CHANGELOG.md +239 -0
  19. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -0
  20. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +30 -4
  21. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +2 -0
  22. package/node_modules/pi-lens/dist/clients/bash-file-access.js +26 -0
  23. package/node_modules/pi-lens/dist/clients/biome-client.js +1 -1
  24. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +403 -35
  25. package/node_modules/pi-lens/dist/clients/bootstrap.js +6 -1
  26. package/node_modules/pi-lens/dist/clients/cascade-format.js +200 -4
  27. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +10 -2
  28. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +16 -1
  29. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +12 -1
  30. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +65 -0
  31. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +28 -0
  32. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +9 -2
  33. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +222 -0
  34. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +389 -13
  35. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +175 -25
  36. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +62 -0
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +6 -1
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +7 -7
  40. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -12
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +2 -21
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +1 -1
  43. package/node_modules/pi-lens/dist/clients/dispatch/types.js +4 -1
  44. package/node_modules/pi-lens/dist/clients/disposition-publish.js +1 -0
  45. package/node_modules/pi-lens/dist/clients/effective-config.js +1 -1
  46. package/node_modules/pi-lens/dist/clients/file-kinds.js +26 -1
  47. package/node_modules/pi-lens/dist/clients/file-role.js +21 -21
  48. package/node_modules/pi-lens/dist/clients/file-utils.js +156 -10
  49. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +56 -9
  50. package/node_modules/pi-lens/dist/clients/format-service.js +39 -21
  51. package/node_modules/pi-lens/dist/clients/formatters.js +60 -13
  52. package/node_modules/pi-lens/dist/clients/generation-guard.js +2 -2
  53. package/node_modules/pi-lens/dist/clients/git-guard.js +11 -3
  54. package/node_modules/pi-lens/dist/clients/indent-retarget.js +78 -8
  55. package/node_modules/pi-lens/dist/clients/installer/index.js +333 -114
  56. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +7 -0
  57. package/node_modules/pi-lens/dist/clients/instance-reaper.js +20 -10
  58. package/node_modules/pi-lens/dist/clients/language-profile.js +23 -1
  59. package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
  60. package/node_modules/pi-lens/dist/clients/latency-logger.js +4 -1
  61. package/node_modules/pi-lens/dist/clients/lens-engine.js +12 -3
  62. package/node_modules/pi-lens/dist/clients/lens-events.js +2 -1
  63. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +79 -0
  64. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +2 -1
  65. package/node_modules/pi-lens/dist/clients/lsp/client.js +25 -21
  66. package/node_modules/pi-lens/dist/clients/lsp/config.js +2 -2
  67. package/node_modules/pi-lens/dist/clients/lsp/index.js +7 -0
  68. package/node_modules/pi-lens/dist/clients/lsp/launch.js +16 -4
  69. package/node_modules/pi-lens/dist/clients/lsp/server.js +19 -2
  70. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +2 -1
  71. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +5 -3
  72. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +12 -8
  73. package/node_modules/pi-lens/dist/clients/mcp/session.js +36 -4
  74. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +24 -7
  75. package/node_modules/pi-lens/dist/clients/observed-mutation.js +40 -2
  76. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +23 -3
  77. package/node_modules/pi-lens/dist/clients/opengrep-client.js +110 -15
  78. package/node_modules/pi-lens/dist/clients/path-utils.js +123 -3
  79. package/node_modules/pi-lens/dist/clients/performance-report.js +5 -1
  80. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
  81. package/node_modules/pi-lens/dist/clients/pipeline.js +74 -5
  82. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +37 -5
  83. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +145 -18
  84. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
  85. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +59 -3
  86. package/node_modules/pi-lens/dist/clients/project-lens-config.js +45 -2
  87. package/node_modules/pi-lens/dist/clients/read-guard.js +70 -11
  88. package/node_modules/pi-lens/dist/clients/resource-sampler.js +147 -9
  89. package/node_modules/pi-lens/dist/clients/ruff-client.js +1 -1
  90. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +33 -5
  91. package/node_modules/pi-lens/dist/clients/runtime-context.js +6 -2
  92. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +77 -7
  93. package/node_modules/pi-lens/dist/clients/runtime-session.js +21 -2
  94. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +43 -12
  95. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +330 -85
  96. package/node_modules/pi-lens/dist/clients/runtime-turn.js +147 -17
  97. package/node_modules/pi-lens/dist/clients/safe-spawn.js +258 -5
  98. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +6 -0
  99. package/node_modules/pi-lens/dist/clients/session-event-guard.js +50 -5
  100. package/node_modules/pi-lens/dist/clients/sg-runner.js +23 -11
  101. package/node_modules/pi-lens/dist/clients/sgconfig.js +79 -2
  102. package/node_modules/pi-lens/dist/clients/test-runner-client.js +6 -41
  103. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +43 -0
  104. package/node_modules/pi-lens/dist/clients/tool-agreement.js +423 -0
  105. package/node_modules/pi-lens/dist/clients/tool-config.js +50 -0
  106. package/node_modules/pi-lens/dist/clients/tool-cwd.js +47 -76
  107. package/node_modules/pi-lens/dist/clients/tool-policy.js +163 -7
  108. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +120 -0
  109. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +127 -189
  110. package/node_modules/pi-lens/dist/clients/widget-state.js +98 -0
  111. package/node_modules/pi-lens/dist/clients/word-index.js +5 -2
  112. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +7 -0
  113. package/node_modules/pi-lens/dist/index.js +27416 -24153
  114. package/node_modules/pi-lens/dist/mcp/server.js +5 -2
  115. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +7 -1
  116. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +168 -53
  117. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +244 -72
  118. package/node_modules/pi-lens/docs/audit1.md +5 -5
  119. package/node_modules/pi-lens/docs/dependencies.md +11 -0
  120. package/node_modules/pi-lens/docs/dispositions.md +52 -0
  121. package/node_modules/pi-lens/docs/environment-variables.md +2 -2
  122. package/node_modules/pi-lens/docs/features.md +4 -4
  123. package/node_modules/pi-lens/docs/globalconfig.md +3 -1
  124. package/node_modules/pi-lens/docs/language-coverage.md +1 -0
  125. package/node_modules/pi-lens/docs/pi-lens-fixer.md +53 -94
  126. package/node_modules/pi-lens/docs/pi-lens-investigator.md +43 -38
  127. package/node_modules/pi-lens/docs/pi-lens-retro.md +97 -0
  128. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +53 -66
  129. package/node_modules/pi-lens/docs/release-qa-baseline.md +22 -0
  130. package/node_modules/pi-lens/docs/settings.md +7 -4
  131. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  132. package/node_modules/pi-lens/docs/usage.md +2 -0
  133. package/node_modules/pi-lens/package.json +1 -1
  134. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +24 -12
  135. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +40 -0
  136. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +15 -0
  137. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +7 -0
  138. package/package.json +5 -11
  139. package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +0 -376
  140. package/node_modules/@luxusai/pi-hindsight/README.md +0 -92
  141. package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +0 -56
  142. package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +0 -169
  143. package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +0 -107
  144. package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +0 -89
  145. package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +0 -189
  146. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
  147. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
  148. package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +0 -54
  149. package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +0 -75
  150. package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +0 -300
  151. package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +0 -158
  152. package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +0 -164
  153. package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +0 -139
  154. package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +0 -74
  155. package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +0 -261
  156. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +0 -151
  157. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +0 -18
  158. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +0 -46
  159. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +0 -383
  160. package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +0 -240
  161. package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +0 -176
  162. package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +0 -178
  163. package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +0 -41
  164. package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +0 -386
  165. package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +0 -39
  166. package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +0 -34
  167. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +0 -140
  168. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +0 -61
  169. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +0 -925
  170. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +0 -247
  171. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +0 -547
  172. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +0 -480
  173. package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +0 -182
  174. package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +0 -86
  175. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +0 -1010
  176. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +0 -175
  177. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +0 -425
  178. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +0 -210
  179. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +0 -817
  180. package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +0 -25
  181. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +0 -319
  182. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +0 -196
  183. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +0 -189
  184. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +0 -65
  185. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +0 -260
  186. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +0 -244
  187. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +0 -59
  188. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +0 -82
  189. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +0 -45
  190. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +0 -331
  191. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +0 -354
  192. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +0 -66
  193. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +0 -132
  194. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +0 -228
  195. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +0 -74
  196. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +0 -16
  197. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +0 -648
  198. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +0 -105
  199. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +0 -68
  200. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +0 -71
  201. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +0 -13
  202. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +0 -162
  203. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +0 -131
  204. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +0 -104
  205. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +0 -37
  206. package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +0 -893
  207. package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +0 -35
  208. package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +0 -177
  209. package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +0 -7
  210. package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +0 -21
  211. package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +0 -112
  212. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +0 -111
  213. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +0 -214
  214. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +0 -70
  215. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +0 -392
  216. package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +0 -45
  217. package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +0 -9
  218. package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +0 -860
  219. package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +0 -105
  220. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +0 -189
  221. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +0 -299
  222. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +0 -201
  223. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +0 -47
  224. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +0 -239
  225. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +0 -50
  226. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +0 -259
  227. package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +0 -77
  228. package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +0 -534
  229. package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +0 -319
  230. package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +0 -325
  231. package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +0 -42
  232. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +0 -244
  233. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +0 -56
  234. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +0 -50
  235. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +0 -167
  236. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +0 -207
  237. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +0 -120
  238. package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +0 -8
  239. package/node_modules/@luxusai/pi-hindsight/package.json +0 -118
  240. package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +0 -75
@@ -266,15 +266,19 @@ export class ReadGuard {
266
266
  // pi Write tool authored, independent of filesystem mtime granularity
267
267
  // or clock skew (NFS, FAT32, etc.).
268
268
  writtenThisSession = new Set();
269
+ unchangedThisSession = new Set();
269
270
  // Existence-independent index for hasKnownPath/forgetPath (#1668 review
270
271
  // F1). `this.key()` (normalizeFilePath) branches on whether `filePath`
271
- // currently exists on disk: an existing file resolves to realpathSync
272
- // canonical casing, a missing one to a lowercased tail. recordRead/
273
- // recordWritten always key while the file is still on disk (real
274
- // casing); hasKnownPath/forgetPath are queried AFTER an external delete
275
- // already landed, when the path no longer exists — recomputing
272
+ // currently exists on disk, on EVERY platform since #3098 — an existing
273
+ // file resolves to its on-disk casing (win32: `realpathSync.native`;
274
+ // POSIX: the on-disk casing of the trailing segments), a missing one to a
275
+ // lowercased tail on win32 and to the caller's own spelling on POSIX.
276
+ // recordRead/recordWritten always key while the file is still on disk
277
+ // (real casing); hasKnownPath/forgetPath are queried AFTER an external
278
+ // delete already landed, when the path no longer exists — recomputing
276
279
  // `this.key()` at that point returns a DIFFERENT string for any
277
- // mixed-case basename (`MyModule.ts` → `mymodule.ts`), so a lookup
280
+ // mixed-case basename (win32 `MyModule.ts` → `mymodule.ts`; POSIX
281
+ // `Components/x.ts` held against an on-disk `components/`), so a lookup
278
282
  // against `reads`/`writtenThisSession` silently misses. This index maps
279
283
  // a purely syntactic key (`normalizeEphemeralMapKey` — slash-fold +
280
284
  // lowercase, no filesystem access, so it never depends on current disk
@@ -282,6 +286,17 @@ export class ReadGuard {
282
286
  // time, so a post-delete lookup finds the same entry regardless of what
283
287
  // happened to the file since. Pruned inside `evictFile` so it never
284
288
  // outlives the record it points at.
289
+ //
290
+ // ONLY hasKnownPath/forgetPath route through this index, because only they
291
+ // need the real key BACK. `noteCreatedFile` (keys while the file is still
292
+ // absent) and `recordWritten` (keys after it exists) straddle the same
293
+ // state change, and used to do it through `this.key()` directly — a
294
+ // spelling whose key moved orphaned the `pendingCreations` entry and the
295
+ // creation read was never injected (#3163, pre-existing on win32 and
296
+ // reachable on POSIX since #3098). They now key `pendingCreations` by the
297
+ // same existence-independent `normalizeEphemeralMapKey` spelling this index
298
+ // is keyed by; no index is needed there, since the real key is derived by
299
+ // `recordWritten` itself at the moment it is used.
285
300
  knownPathIndex = new Map();
286
301
  /** Running per-file record-cap trim totals for this session (#1913 F1). */
287
302
  trimAccumulators = new Map();
@@ -424,8 +439,15 @@ export class ReadGuard {
424
439
  * Record that a file was read.
425
440
  * Call this from the tool_call handler after any LSP expansion.
426
441
  */
427
- recordRead(record) {
442
+ recordRead(record, opts) {
428
443
  const filePath = this.key(record.filePath);
444
+ if (opts?.supersedes) {
445
+ const records = this.reads.get(filePath);
446
+ const provisionalSource = `native-read:${opts.supersedes.toolCallId}:provisional`;
447
+ const provisionalIndex = records?.findIndex((candidate) => candidate.source === provisionalSource);
448
+ if (provisionalIndex !== undefined && provisionalIndex >= 0)
449
+ records.splice(provisionalIndex, 1);
450
+ }
429
451
  // #1668 review F1: index by the existence-independent syntactic key
430
452
  // while the file is (presumably) still on disk, so a later
431
453
  // hasKnownPath/forgetPath lookup after an external delete can still
@@ -865,9 +887,26 @@ export class ReadGuard {
865
887
  * Must be called from the tool_call handler before the write lands so
866
888
  * isNewFile() still returns true. recordWritten will inject a synthetic
867
889
  * read so immediate follow-up edits are not blocked by zero_read.
890
+ *
891
+ * Keyed by the existence-INDEPENDENT syntactic spelling
892
+ * (`normalizeEphemeralMapKey`, the same key `knownPathIndex` uses), NOT by
893
+ * `this.key()` (#3163). This entry is written while the file is still
894
+ * ABSENT and read back by `recordWritten` once it EXISTS — the one state
895
+ * change `normalizeFilePath` itself branches on — so a `this.key()` on each
896
+ * side answered with two different strings for any spelling whose key moves
897
+ * when the file appears (win32: a mixed-case basename, lower-cased in the
898
+ * absent branch by `resolveNonExisting`; POSIX since #3098: a parent whose
899
+ * canonical casing is adopted once `realpathSync.native` succeeds). The
900
+ * entry was then orphaned — nothing prunes `pendingCreations` — and the
901
+ * creation read never injected. The syntactic key never touches the
902
+ * filesystem, so it cannot move; the REAL key the injected read needs is
903
+ * derived by `recordWritten` itself and is not stored here.
868
904
  */
869
905
  noteCreatedFile(filePath, turnIndex, writeIndex) {
870
- this.pendingCreations.set(this.key(filePath), { turnIndex, writeIndex });
906
+ this.pendingCreations.set(normalizeEphemeralMapKey(filePath), {
907
+ turnIndex,
908
+ writeIndex,
909
+ });
871
910
  }
872
911
  /**
873
912
  * Refresh the FileTime stamp after the model's own write lands on disk.
@@ -876,6 +915,7 @@ export class ReadGuard {
876
915
  */
877
916
  recordWritten(rawFilePath) {
878
917
  const filePath = this.key(rawFilePath);
918
+ this.unchangedThisSession.delete(filePath);
879
919
  // #1668 review F1: index by the existence-independent syntactic key
880
920
  // (see `knownPathIndex`) so a later hasKnownPath/forgetPath lookup
881
921
  // after an external delete can still find this entry's real key.
@@ -886,12 +926,25 @@ export class ReadGuard {
886
926
  this.consumedReadFiles.add(filePath);
887
927
  this.touchFile(filePath);
888
928
  this.enforceFileCap();
889
- const creation = this.pendingCreations.get(filePath);
929
+ // #3163: `noteCreatedFile` announced this creation while the file was
930
+ // still absent, so the announcement is keyed by the syntactic spelling —
931
+ // look it up the same way, never through the existence-dependent
932
+ // `filePath` above. `injectCreationRead` still receives the real key.
933
+ const creationKey = normalizeEphemeralMapKey(rawFilePath);
934
+ const creation = this.pendingCreations.get(creationKey);
890
935
  if (creation) {
891
- this.pendingCreations.delete(filePath);
936
+ this.pendingCreations.delete(creationKey);
892
937
  this.injectCreationRead(filePath, creation.turnIndex, creation.writeIndex);
893
938
  }
894
939
  }
940
+ /** Record that a recognized mutation had complete evidence but changed no bytes. */
941
+ recordUnchanged(rawFilePath) {
942
+ const filePath = this.key(rawFilePath);
943
+ // A no-op command is scoped to this command. It must not erase a
944
+ // confirmed Write from earlier in the session.
945
+ if (!this.writtenThisSession.has(filePath))
946
+ this.unchangedThisSession.add(filePath);
947
+ }
895
948
  /**
896
949
  * Add a one-time exemption for a file.
897
950
  * Called via /lens-allow-edit command.
@@ -1074,6 +1127,8 @@ export class ReadGuard {
1074
1127
  });
1075
1128
  }
1076
1129
  wasWrittenThisSession(filePath) {
1130
+ if (this.unchangedThisSession.has(filePath))
1131
+ return false;
1077
1132
  // Authoritative path: we observed a write of this file via recordWritten.
1078
1133
  // Survives mtime granularity (FAT32 ~2s), clock skew (NFS), and external
1079
1134
  // tools that touch mtime backward.
@@ -1338,6 +1393,8 @@ export class ReadGuard {
1338
1393
  const reads = this.reads.get(filePath) ?? [];
1339
1394
  // First pass: check symbol coverage and any single read that covers the edit.
1340
1395
  for (const read of reads) {
1396
+ if (read.provisional === true)
1397
+ continue;
1341
1398
  const readStart = Math.max(1, read.effectiveOffset - this.config.contextLines);
1342
1399
  const readEnd = read.effectiveOffset +
1343
1400
  read.effectiveLimit -
@@ -1356,7 +1413,9 @@ export class ReadGuard {
1356
1413
  }
1357
1414
  // Second pass: merge all read intervals and check if their union covers
1358
1415
  // [editStart, editEnd]. Handles multi-chunk reads (e.g. 1-100 + 101-200).
1359
- const intervals = reads.map((read) => [
1416
+ const intervals = reads
1417
+ .filter((read) => read.provisional !== true)
1418
+ .map((read) => [
1360
1419
  Math.max(1, read.effectiveOffset - this.config.contextLines),
1361
1420
  read.effectiveOffset +
1362
1421
  read.effectiveLimit -
@@ -44,10 +44,20 @@
44
44
  * clients/instance-reaper.ts (`decideOrphanReaping` vs `sweepOrphans`).
45
45
  */
46
46
  import pidusage from "pidusage";
47
- import { recordDegradationOnce } from "./degradation-ledger.js";
47
+ import { incrementDegradationCount, recordDegradationOnce, } from "./degradation-ledger.js";
48
48
  import { terminateScannerChild } from "./instance-reaper.js";
49
49
  import { queryProcessTable } from "./process-snapshot.js";
50
50
  export const RESOURCE_SAMPLE_QUERY_TIMEOUT_MS = 2_000;
51
+ /**
52
+ * ONE ledger subject for every spawn sampler in the session (#2968). The
53
+ * per-spawn identity (pid, command) is NOT the subject: a subject is a
54
+ * `tallies` key, and one key per spawn would make the ledger grow with the
55
+ * session's spawn count — catalog shape 9, the same leak this fix exists to
56
+ * close. The command that was sampled is already on the `spawn_resource_usage`
57
+ * latency row; what the ledger answers is "did sampling hit its bounds, and
58
+ * how often", which is a per-session tally.
59
+ */
60
+ const SPAWN_SAMPLER_LEDGER_SUBJECT = "spawn-usage-sampler";
51
61
  function recordQueryFailure(subject, status, exitCode) {
52
62
  const exitReason = status === "exit-error" ? ` (exit code ${exitCode ?? "unknown"})` : "";
53
63
  recordDegradationOnce({
@@ -434,13 +444,89 @@ export async function sampleProcessTreeCpuPercent(pid, windowMs = 1200, floorPer
434
444
  cpuPercent: observed,
435
445
  };
436
446
  }
437
- export function startSpawnUsageSampler(pid, intervalMs = 750) {
447
+ /**
448
+ * Base poll cadence — inside #620's suggested 500ms-1s band, cheap enough not
449
+ * to become measurable overhead for the (usually sub-few-second) analyzer
450
+ * children this brackets.
451
+ */
452
+ const SPAWN_SAMPLE_INTERVAL_MS = 750;
453
+ /**
454
+ * #2968: how many ticks keep the full `intervalMs` cadence before the backoff
455
+ * starts. 8 × 750ms = the first 6 seconds, which covers the short-lived
456
+ * analyzer children the short interval exists for; a child still running past
457
+ * that is not short-lived and does not need sub-second resolution.
458
+ */
459
+ const SPAWN_SAMPLE_FULL_RATE_TICKS = 8;
460
+ /**
461
+ * #2968: backoff ceiling, as a multiple of `intervalMs` (16 × 750ms = 12s).
462
+ * Past the full-rate window the delay doubles each tick up to this, so a
463
+ * long-lived child costs a bounded ~5 polls/minute instead of 80.
464
+ */
465
+ const SPAWN_SAMPLE_MAX_INTERVAL_MULTIPLIER = 16;
466
+ /**
467
+ * Brackets one transient spawn with a short-interval poll. Usage:
468
+ *
469
+ * const sampler = startSpawnUsageSampler(child.pid, interval, capMs);
470
+ * child.on("close", () => {
471
+ * const usage = sampler.stop(); // null if never got a single sample
472
+ * });
473
+ *
474
+ * Best-effort: a poll tick that throws (pid already gone, sampling error) is
475
+ * silently skipped — it never stops the polling or the spawn early, and
476
+ * `stop()` is always safe to call even if zero samples ever landed.
477
+ *
478
+ * Windows note: `clients/safe-spawn.ts` spawns with `shell: true` on Windows,
479
+ * so `pid` here is `cmd.exe`'s pid, not the real tool's — sampling it alone
480
+ * would report near-zero usage for the whole invocation. Each Windows tick
481
+ * resolves `pid`'s live descendant tree (`findDescendantPidsWindows`) and
482
+ * sums usage across `pid` + every descendant, so a `node`/`npx`-wrapped tool
483
+ * (or one that re-execs itself) is actually captured. POSIX spawns are
484
+ * unwrapped (`shell: false`), so `pid` there is already the real tool.
485
+ *
486
+ * ## Backpressure: three bounds, three axes (#2968, external report)
487
+ *
488
+ * A Windows tick is not free — it is TWO `powershell.exe` CIM queries (the
489
+ * descendant walk plus the usage read), and a query that blows
490
+ * `RESOURCE_SAMPLE_QUERY_TIMEOUT_MS` adds a `taskkill.exe`. With a `setInterval`
491
+ * that neither waited for its own tick nor ever expired, four children that
492
+ * hung for 5-6 hours left 234 live `powershell.exe`/`taskkill.exe` processes
493
+ * (~10GB) parented by the host. Each bound below closes one axis; catalog
494
+ * shape 9 is exactly the shape where closing only one of them is not a fix:
495
+ *
496
+ * 1. CONCURRENCY — a tick never starts while the previous one is unsettled
497
+ * (`inFlight`). The old `setInterval` fired regardless, so a query slower
498
+ * than the interval stacked one more pair of children every tick.
499
+ * 2. RATE — past `SPAWN_SAMPLE_FULL_RATE_TICKS` the delay doubles per tick up
500
+ * to `SPAWN_SAMPLE_MAX_INTERVAL_MULTIPLIER × intervalMs`. The short
501
+ * interval exists to catch short-lived children; a 180s runner (the
502
+ * longest spawn timeout in the tree) costs ~26 polls instead of 240.
503
+ * 3. LIFETIME — polling stops for good at `lifetimeCapMs`, measured from
504
+ * start. `stop()` still returns everything gathered up to that point; a
505
+ * capped sampler loses resolution, never the reading it already had. The
506
+ * cap is read at TICK granularity, so the last poll can land up to one
507
+ * backed-off interval (≤ 12s by default) before polling ends — the tick
508
+ * that discovers the cap does no sampling and arms nothing.
509
+ *
510
+ * Ticks skipped for (1) and the cap in (3) are both recorded, bounded, on the
511
+ * degradation ledger — a sampler that quietly stops sampling is the #1863 /
512
+ * #2132 shape one level up.
513
+ *
514
+ * `lifetimeCapMs` has no default ON PURPOSE: an unbounded sampler is the whole
515
+ * defect, so the bound is the caller's to state rather than something a future
516
+ * call site can forget. The cadence keeps its default, which is policy this
517
+ * module owns.
518
+ */
519
+ export function startSpawnUsageSampler(pid, intervalMs = SPAWN_SAMPLE_INTERVAL_MS, lifetimeCapMs) {
438
520
  if (!Number.isFinite(pid) || pid <= 0) {
439
521
  return { stop: () => null };
440
522
  }
441
523
  const targetPid = pid;
442
524
  const accumulator = new UsageAccumulator();
525
+ const startedAt = Date.now();
443
526
  let stopped = false;
527
+ let inFlight = false;
528
+ let tickCount = 0;
529
+ let timer;
444
530
  const tick = async () => {
445
531
  if (stopped)
446
532
  return;
@@ -470,20 +556,72 @@ export function startSpawnUsageSampler(pid, intervalMs = 750) {
470
556
  // Best-effort: a failed poll tick just misses one sample.
471
557
  }
472
558
  };
559
+ const releaseInFlight = () => {
560
+ inFlight = false;
561
+ };
562
+ /**
563
+ * Delay before the NEXT tick, given how many have already run. Full rate
564
+ * through the short-lived window, then doubling to the ceiling (#2968).
565
+ */
566
+ const nextDelayMs = () => {
567
+ if (tickCount <= SPAWN_SAMPLE_FULL_RATE_TICKS)
568
+ return intervalMs;
569
+ const grown = intervalMs * 2 ** (tickCount - SPAWN_SAMPLE_FULL_RATE_TICKS);
570
+ return Math.min(grown, intervalMs * SPAWN_SAMPLE_MAX_INTERVAL_MULTIPLIER);
571
+ };
572
+ const arm = (delayMs) => {
573
+ timer = setTimeout(runTick, delayMs);
574
+ // Never let this timer keep the process alive on its own.
575
+ timer.unref?.();
576
+ };
577
+ function runTick() {
578
+ timer = undefined;
579
+ if (stopped)
580
+ return;
581
+ if (Date.now() - startedAt >= lifetimeCapMs) {
582
+ // LIFETIME bound: deliberately NOT re-armed. The accumulator is kept,
583
+ // so `stop()` still answers with everything gathered before the cap.
584
+ incrementDegradationCount({
585
+ kind: "resource-sampler-lifetime-capped",
586
+ subject: SPAWN_SAMPLER_LEDGER_SUBJECT,
587
+ reason: `spawn sampler stopped polling at its ${lifetimeCapMs}ms lifetime cap; the child was still running`,
588
+ });
589
+ return;
590
+ }
591
+ tickCount++;
592
+ if (inFlight) {
593
+ // CONCURRENCY bound: the previous tick's process-table queries have
594
+ // not settled, so starting another would stack a second set of
595
+ // children on top of them (#2968's Windows pile-up).
596
+ incrementDegradationCount({
597
+ kind: "resource-sampler-tick-overlapped",
598
+ subject: SPAWN_SAMPLER_LEDGER_SUBJECT,
599
+ reason: `spawn sampler skipped a poll tick: the previous sample was still in flight after ${intervalMs}ms`,
600
+ });
601
+ }
602
+ else {
603
+ inFlight = true;
604
+ // `tick` never rejects (it is fully guarded), but settle BOTH ways
605
+ // anyway: a rejection that escaped would otherwise latch `inFlight`
606
+ // true and silently end all sampling.
607
+ void tick().then(releaseInFlight, releaseInFlight);
608
+ }
609
+ arm(nextDelayMs());
610
+ }
473
611
  // Fire one tick immediately (short-lived children can exit before the
474
612
  // first interval elapses) plus a recurring poll.
475
- void tick();
476
- const timer = setInterval(() => {
477
- void tick();
478
- }, intervalMs);
479
- // Never let this timer keep the process alive on its own.
480
- timer.unref?.();
613
+ tickCount = 1;
614
+ inFlight = true;
615
+ void tick().then(releaseInFlight, releaseInFlight);
616
+ arm(nextDelayMs());
481
617
  return {
482
618
  stop() {
483
619
  if (stopped)
484
620
  return accumulator.summarize();
485
621
  stopped = true;
486
- clearInterval(timer);
622
+ if (timer !== undefined)
623
+ clearTimeout(timer);
624
+ timer = undefined;
487
625
  return accumulator.summarize();
488
626
  },
489
627
  };
@@ -79,7 +79,7 @@ export class RuffClient {
79
79
  // nested `pyproject.toml` whenever the caller passed a workspace root.
80
80
  const ruffCwd = resolveToolCwd("runner", "ruff", absolutePath, {
81
81
  ...(cwd !== undefined && { cwd }),
82
- });
82
+ }).cwd;
83
83
  // Shared config-args seam (#1247): the lint runner consumes the same
84
84
  // builder, so `check --fix` can never drift to ruff's default rule
85
85
  // set when the project lacks its own config and the package-owned
@@ -6,6 +6,8 @@ import { publishAutofixStart, publishFormatStart, } from "./format-events-publis
6
6
  import { logLatency } from "./latency-logger.js";
7
7
  import { isPathIgnoredByProject } from "./file-utils.js";
8
8
  import { admitBounded, emitBounded } from "./bounded-telemetry.js";
9
+ import { bounded } from "./deadline-utils.js";
10
+ import { HOOK_WALL_BUDGET_MS } from "./hook-budgets.js";
9
11
  import { newLspMutationCorrelationId, } from "./lsp-mutation.js";
10
12
  import { getGlobalActionableWarningMaxFixes, } from "./lens-config.js";
11
13
  import { resyncLspFile, runAutofix, runFormatPhase } from "./pipeline.js";
@@ -32,7 +34,7 @@ function recordProjectChange(args) {
32
34
  onAppendError: (err) => args.dbg(`project change log append failed for ${args.filePath}: ${err}`),
33
35
  });
34
36
  }
35
- export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, dbg, runtime, cacheManager, getFormatService, biomeClient, ruffClient, getAutofixClients, currentSessionId, staleAfterMs = DEFERRED_FORMAT_STALE_AFTER_MS, }) {
37
+ export async function handleAgentEnd({ signal, ctxCwd, getFlag, getFlagSource, notify, dbg, runtime, cacheManager, getFormatService, biomeClient, ruffClient, getAutofixClients, currentSessionId, staleAfterMs = DEFERRED_FORMAT_STALE_AFTER_MS, }) {
36
38
  // #791: ownership-filtered drain — records queued by a DIFFERENT known
37
39
  // session (e.g. a concurrent in-process secondary/subagent) stay queued
38
40
  // for their owner's own agent_end, unless they've been stale long enough
@@ -204,7 +206,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
204
206
  const deferredAutofixChanged = new Set();
205
207
  // Mutation ordering is intentional: lint --write may disturb wrapping, so
206
208
  // autofix reaches the final edited state first and formatting stabilizes it.
207
- const ambientSignal = getAmbientAbortSignal();
209
+ const ambientSignal = signal ?? getAmbientAbortSignal();
208
210
  const executedAutofixScopes = new Set();
209
211
  const autofixRecords = records.filter((candidate) => candidate.kinds.has("autofix"));
210
212
  if (autofixRecords.length > 0 &&
@@ -367,7 +369,13 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
367
369
  const filePath = path.resolve(record.filePath);
368
370
  started.add(index);
369
371
  if (!nodeFs.existsSync(filePath)) {
370
- work[index] = { record, filePath, fileStart, missing: true };
372
+ work[index] = {
373
+ record,
374
+ filePath,
375
+ fileStart,
376
+ result: undefined,
377
+ missing: true,
378
+ };
371
379
  continue;
372
380
  }
373
381
  try {
@@ -375,7 +383,12 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
375
383
  record,
376
384
  filePath,
377
385
  fileStart,
378
- result: await runFormatPhase(filePath, getFormatService, dbg),
386
+ result: await bounded(runFormatPhase(filePath, getFormatService, dbg, ambientSignal, 30_000, "agent_settled"), {
387
+ ms: HOOK_WALL_BUDGET_MS.agent_settled,
388
+ signal: ambientSignal,
389
+ hook: "agent_settled",
390
+ label: "deferred-format",
391
+ }),
379
392
  };
380
393
  }
381
394
  catch (err) {
@@ -383,6 +396,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
383
396
  record,
384
397
  filePath,
385
398
  fileStart,
399
+ result: undefined,
386
400
  error: err instanceof Error ? err.message : String(err),
387
401
  };
388
402
  }
@@ -423,8 +437,22 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
423
437
  continue;
424
438
  }
425
439
  const result = entry.result;
426
- if (!result)
440
+ if (!result) {
441
+ // The abort branch above already requeues work that never started;
442
+ // preserve its established ownership for an in-flight caller abort.
443
+ if (ambientSignal?.aborted)
444
+ continue;
445
+ const reason = entry.error ?? "deferred formatter exceeded agent_settled budget";
446
+ summary.failed.push({ filePath, errors: [reason] });
447
+ requeue([
448
+ {
449
+ ...record,
450
+ kinds: new Set(["format"]),
451
+ toolNames: new Set(record.toolNames),
452
+ },
453
+ ], "format-failed");
427
454
  continue;
455
+ }
428
456
  summary.formatted++;
429
457
  if (result.formatFailures.length > 0) {
430
458
  summary.failed.push({ filePath, errors: result.formatFailures });
@@ -1,5 +1,6 @@
1
1
  import { provenanceStamp, validateAdvisoryProvenance, } from "./advisory-provenance.js";
2
2
  import { logLatency } from "./latency-logger.js";
3
+ import { translateGuidanceToolNames, } from "./tool-config.js";
3
4
  // Exported so the Stop-hook bin strips exactly what these bridges prepend.
4
5
  export const AUTOMATION_FRAMING = "[pi-lens automated check — not a user request] ";
5
6
  // #1432 review (S3b): a multi-file bash write can carry hundreds of changed
@@ -184,7 +185,7 @@ export function acknowledgeTestFindings(cacheManager, cwd) {
184
185
  return;
185
186
  retireTestFindings(cacheManager, cwd);
186
187
  }
187
- export function consumeSessionStartGuidance(cacheManager, cwd) {
188
+ export function consumeSessionStartGuidance(cacheManager, cwd, host = "pi") {
188
189
  const guidance = cacheManager.readCache("session-start-guidance", cwd);
189
190
  if (!guidance?.data?.content)
190
191
  return;
@@ -200,7 +201,10 @@ export function consumeSessionStartGuidance(cacheManager, cwd) {
200
201
  messages: [
201
202
  {
202
203
  role: "user",
203
- content: `[pi-lens automated context — not a user request]\n\n${guidance.data.content}`,
204
+ // #2535: the stored guidance names pi tools; MCP agents receive
205
+ // the registry-translated rendering. The stored record keeps
206
+ // its shape — resolution happens here, at delivery.
207
+ content: `[pi-lens automated context — not a user request]\n\n${translateGuidanceToolNames(guidance.data.content, host)}`,
204
208
  },
205
209
  ],
206
210
  };
@@ -12,6 +12,9 @@ import { RUNTIME_CONFIG } from "./runtime-config.js";
12
12
  import { TurnSummaryCollector } from "./turn-summary.js";
13
13
  import { deriveProviderFromModelId } from "./model-provider.js";
14
14
  import { beginTurnContext, setTurnContextSession } from "./turn-context.js";
15
+ import { recordDegradationOnce } from "./degradation-ledger.js";
16
+ /** Keep deferred cascade admission bounded without dropping late findings. */
17
+ export const MAX_PENDING_CASCADE_RUNS = 32;
15
18
  /**
16
19
  * Bounded receipt ring capacity (defect shape 9: bound the axis that grows —
17
20
  * per-mutation receipts are unbounded across a long session). Oldest receipts
@@ -286,8 +289,8 @@ export class RuntimeCoordinator {
286
289
  }
287
290
  this._mutationReceipts.push({
288
291
  seq: projectSeq,
289
- // Reuse the bump's normalized key (~200us realpath on Windows) —
290
- // never re-derive it here.
292
+ // Reuse the bump's normalized key (a realpath syscall: ~200us on
293
+ // Windows, ~1.8us on POSIX since #3098) — never re-derive it here.
291
294
  filePath: key,
292
295
  source: args.source,
293
296
  turnIndex: this._turnIndex,
@@ -455,8 +458,9 @@ export class RuntimeCoordinator {
455
458
  }
456
459
  }
457
460
  bumpFileSeq(filePath) {
458
- // normalizeMapKey costs ~200us/call on Windows (realpath); every caller
459
- // that also needs the key must reuse this one instead of paying it twice.
461
+ // normalizeMapKey costs a realpath syscall per call — ~200us on Windows,
462
+ // ~1.8us on POSIX since #3098; every caller that also needs the key must
463
+ // reuse this one instead of paying it twice.
460
464
  const key = normalizeMapKey(path.resolve(filePath));
461
465
  this._projectSeq += 1;
462
466
  const fileSeq = (this._fileSeq.get(key) ?? 0) + 1;
@@ -542,7 +546,24 @@ export class RuntimeCoordinator {
542
546
  this._cascadeRuns.push(run);
543
547
  }
544
548
  appendCascadePromise(p) {
545
- this._pendingCascadeRuns.push(p);
549
+ if (this._pendingCascadeRuns.length < MAX_PENDING_CASCADE_RUNS) {
550
+ this._pendingCascadeRuns.push(p);
551
+ return;
552
+ }
553
+ // Preserve delivery for overflow rather than growing the per-edit array.
554
+ // The settled run enters the same accumulator off-hook and is therefore
555
+ // visible to the next turn-end drain.
556
+ recordDegradationOnce({
557
+ kind: "cascade-pending-cap",
558
+ subject: "runtime-coordinator",
559
+ reason: `deferred cascade admission capped at ${MAX_PENDING_CASCADE_RUNS}`,
560
+ });
561
+ void p
562
+ .then((run) => this.appendCascadeRun(run))
563
+ .catch(() => {
564
+ // Pipeline promises are normally non-rejecting; preserve the existing
565
+ // failure sink if a caller violates that contract.
566
+ });
546
567
  }
547
568
  /**
548
569
  * The active turn_end settle clock, or undefined outside that wait.
@@ -644,16 +665,24 @@ export class RuntimeCoordinator {
644
665
  // that actually settles it delivers it.
645
666
  return this._cascadeRuns.length > 0;
646
667
  }
647
- recordInlineBlockers(filePath, summary, writeIndex, sources, lines) {
668
+ recordInlineBlockers(filePath, summary, writeIndex, sources, lines, contentBaseline) {
669
+ const recordedAtMs = Date.now();
648
670
  this._pendingInlineBlockers.set(path.resolve(filePath), {
649
671
  filePath,
650
672
  summary,
651
673
  writeIndex,
652
674
  sources,
653
675
  lines,
654
- recordedAtMs: Date.now(),
676
+ recordedAtMs,
655
677
  stale: false,
678
+ ...(contentBaseline
679
+ ? {
680
+ recordedSize: contentBaseline.size,
681
+ recordedHash: contentBaseline.sha256,
682
+ }
683
+ : {}),
656
684
  });
685
+ return recordedAtMs;
657
686
  }
658
687
  clearInlineBlockers(filePath) {
659
688
  this._pendingInlineBlockers.delete(path.resolve(filePath));
@@ -708,6 +737,47 @@ export class RuntimeCoordinator {
708
737
  });
709
738
  return true;
710
739
  }
740
+ /**
741
+ * #2982: re-derive the self-drift demotion for one inline-blocker record.
742
+ *
743
+ * Modelled on {@link setInlineBlockerPastEofStale}, not on
744
+ * {@link markInlineBlockerStale}: it RE-ARMS rather than latching, so a
745
+ * file whose bytes drift and then come back (a checkout, a revert, an
746
+ * editor writing the original content) un-demotes on the next sweep
747
+ * instead of staying demoted for the session. That re-arm is what lets
748
+ * this axis skip the #1950 delivery cap entirely — a record that can heal
749
+ * needs no bounded-noise retirement, and applying the cap here would walk
750
+ * an ast-grep or tree-sitter security finding out of turn-end rendering
751
+ * permanently (#2982 review).
752
+ *
753
+ * Composes with the sibling gates the same way they compose with each
754
+ * other: it never touches a record another gate demoted, and never heals a
755
+ * demotion it did not make. Returns true only on an actual transition, so
756
+ * the caller counts exactly one edge.
757
+ */
758
+ setInlineBlockerSelfDriftStale(filePath, isSelfDrift) {
759
+ const key = path.resolve(filePath);
760
+ const existing = this._pendingInlineBlockers.get(key);
761
+ if (!existing)
762
+ return false;
763
+ if (existing.stale && existing.staleReason !== "self-drift")
764
+ return false;
765
+ const currentlySelfDrift = !!existing.stale && existing.staleReason === "self-drift";
766
+ if (currentlySelfDrift === isSelfDrift)
767
+ return false;
768
+ // Omit the key rather than assigning `undefined` to it: under
769
+ // `exactOptionalPropertyTypes` those are different, and writing the
770
+ // `undefined` is a strictness spike the ratchet counts. Healing has to
771
+ // drop `staleReason` off the rebuilt record, so destructure it away
772
+ // instead of letting `...existing` carry the old value through.
773
+ const { staleReason: _cleared, ...rest } = existing;
774
+ this._pendingInlineBlockers.set(key, {
775
+ ...rest,
776
+ stale: isSelfDrift,
777
+ ...(isSelfDrift ? { staleReason: "self-drift" } : {}),
778
+ });
779
+ return true;
780
+ }
711
781
  /**
712
782
  * #1944: retire a past-EOF demotion after its ONE degraded delivery.
713
783
  *
@@ -14,7 +14,7 @@ import { resetDispatchAvailabilityState } from "./dispatch/runners/utils/runner-
14
14
  import { resetObservedRunnerLatency } from "./dispatch/collect-later-tier.js";
15
15
  import { resetPendingRunnerFindings } from "./dispatch/pending-runner-findings.js";
16
16
  import { clearAllSessions as clearFileTimeSessions } from "./file-time.js";
17
- import { getGlobalPiLensDir, getKnipIgnorePatterns, getProjectDataDir, } from "./file-utils.js";
17
+ import { drainProjectDataDirMigrations, getGlobalPiLensDir, getKnipIgnorePatterns, getProjectDataDir, resetProjectDataDirSessionState, } from "./file-utils.js";
18
18
  import { GitleaksClient } from "./gitleaks-client.js";
19
19
  import { resetGoAvailability } from "./go-client.js";
20
20
  import { GovulncheckClient, } from "./govulncheck-client.js";
@@ -1369,7 +1369,7 @@ function scheduleDeferredToolProbesWithClients(deps, languageProfile, startupDef
1369
1369
  export const SESSION_START_GUIDANCE = [
1370
1370
  "📌 pi-lens active — automated checks run on every edit/write; blocking errors (including pre-existing) show inline and must be fixed.\n" +
1371
1371
  "Key tools (see each tool's own description for args):\n" +
1372
- "• lens_diagnostics — source=session reads cache; empty cache ≠ clean; use source=lsp scope=paths for changed files with absent or stale findings.\n" +
1372
+ "• lens_diagnostics — source=session reads cache; empty cache ≠ clean; use source=lsp scope=paths for changed files with absent or stale findings (aggregate hosts: lens(action=diagnostics)).\n" +
1373
1373
  "• symbol_search → module_report → read_symbol/read_enclosing — ranked identifier search, then navigable outline/callback handles + exact body reads; cheaper than reading a whole file before editing.\n" +
1374
1374
  "• Situational (activate via pi_lens_activate_tools): lsp_navigation, ast_grep_search, ast_grep_replace. Use ast_grep_search with dump=true to inspect AST nodes.",
1375
1375
  ];
@@ -1381,6 +1381,7 @@ export async function handleSessionStart(deps) {
1381
1381
  // every analyzer refused for the rest of the process — AGENTS.md defect
1382
1382
  // shape 17. The resident clients themselves are deliberately kept.
1383
1383
  resetAnalyzerBootstrapSessionState();
1384
+ resetProjectDataDirSessionState();
1384
1385
  resetTestRunnerDelivery();
1385
1386
  // #2450 fix round 3, catalog shape 17: the "bridge unavailable" dbg latch
1386
1387
  // (`clients/lsp-mutation.ts`) is a process-lifetime once-per-session flag,
@@ -1900,6 +1901,24 @@ export async function handleSessionStart(deps) {
1900
1901
  // project data roots and machine-global registry root once per session start;
1901
1902
  // this is fire-and-forget and bounded so it never delays startup.
1902
1903
  const projectDataDir = getProjectDataDir(cwd);
1904
+ // #2874: `getProjectDataDir` queues one migration per old-slug directory
1905
+ // it renames (or finds coexisting with its hashed successor). Drain here
1906
+ // so each migration emits one bounded record per session at most.
1907
+ for (const migration of drainProjectDataDirMigrations()) {
1908
+ const targetName = path.basename(migration.to);
1909
+ const hash = targetName.match(/([0-9a-f]{8})$/)?.[1] ?? "unknown";
1910
+ recordDegradationOnce({
1911
+ kind: "data_dir_migrated",
1912
+ subject: hash,
1913
+ reason: migration.outcome === "renamed"
1914
+ ? "using hashed directory after renaming legacy directory"
1915
+ : migration.outcome === "coexisting"
1916
+ ? "using hashed directory because legacy and hashed directories both exist"
1917
+ : migration.outcome === "rename-failed"
1918
+ ? "using legacy directory after hashed-directory rename failure"
1919
+ : "using resolved directory after realpath fallback",
1920
+ });
1921
+ }
1903
1922
  // #1609 review F1: sweepOwnStagingFiles does not recurse, so the installer's
1904
1923
  // bin/ and tools/ subdirectories (clients/installer/index.ts's
1905
1924
  // GITHUB_BIN_DIR / TOOLS_DIR, now atomic-write.js writers too) need their