@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
@@ -5,9 +5,11 @@ import { Worker } from "node:worker_threads";
5
5
  import { gunzipSync, gzipSync } from "node:zlib";
6
6
  import { writeFileAtomic } from "./atomic-write.js";
7
7
  import { getProjectDataDir } from "./file-utils.js";
8
+ import { incrementDegradationCount } from "./degradation-ledger.js";
8
9
  import { readJsonCache } from "./json-cache-read.js";
9
10
  import { logLatency } from "./latency-logger.js";
10
11
  import { normalizeMapKey } from "./path-utils.js";
12
+ import { fingerprintProjectSnapshotJson } from "./project-snapshot-fingerprint.js";
11
13
  import { detectProjectConventions, } from "./project-conventions.js";
12
14
  import { deserializeWordIndex, serializeWordIndex, } from "./word-index.js";
13
15
  // v2: added `wordIndex` (identifier inverted index + BM25, #162). Bumping the
@@ -97,6 +99,15 @@ function parseSnapshotMeta(value) {
97
99
  timestamp: typeof meta.timestamp === "string" ? meta.timestamp : "",
98
100
  version: meta.version,
99
101
  seq: meta.seq,
102
+ fingerprint: typeof meta.fingerprint === "string" &&
103
+ /^[a-f0-9]{64}$/.test(meta.fingerprint)
104
+ ? meta.fingerprint
105
+ : undefined,
106
+ gzBytes: typeof meta.gzBytes === "number" &&
107
+ Number.isInteger(meta.gzBytes) &&
108
+ meta.gzBytes > 0
109
+ ? meta.gzBytes
110
+ : undefined,
100
111
  sequenceIndex: parseSequenceIndex(meta.sequenceIndex),
101
112
  };
102
113
  }
@@ -149,7 +160,9 @@ const PROJECT_SNAPSHOT_MAX_WARM_ROOTS = 8;
149
160
  const PROJECT_SNAPSHOT_IDLE_EVICT_MS_DEFAULT = 20 * 60_000;
150
161
  function projectSnapshotIdleEvictMs() {
151
162
  const value = Number.parseInt(process.env.PI_LENS_PROJECT_SNAPSHOT_IDLE_EVICT_MS ?? "", 10);
152
- return Number.isSafeInteger(value) && value > 0 ? value : PROJECT_SNAPSHOT_IDLE_EVICT_MS_DEFAULT;
163
+ return Number.isSafeInteger(value) && value > 0
164
+ ? value
165
+ : PROJECT_SNAPSHOT_IDLE_EVICT_MS_DEFAULT;
153
166
  }
154
167
  function clearAuthoritativeSnapshotTimer(entry) {
155
168
  if (entry.idleTimer)
@@ -167,7 +180,8 @@ function scheduleAuthoritativeSnapshotEviction(key, entry) {
167
180
  const generation = entry.lastUsedAt;
168
181
  entry.idleTimer = setTimeout(() => {
169
182
  entry.idleTimer = undefined;
170
- if (authoritativeSnapshots.get(key) !== entry || entry.lastUsedAt !== generation)
183
+ if (authoritativeSnapshots.get(key) !== entry ||
184
+ entry.lastUsedAt !== generation)
171
185
  return;
172
186
  deleteAuthoritativeSnapshot(key);
173
187
  }, projectSnapshotIdleEvictMs());
@@ -590,7 +604,11 @@ export function loadProjectSnapshot(cwd) {
590
604
  export function loadProjectSnapshotWithoutWordIndex(cwd) {
591
605
  return loadProjectSnapshotInternal(cwd, false);
592
606
  }
593
- const _snapshotGenerations = new Map();
607
+ const _snapshotGenerationStates = new Map();
608
+ const _successfulSnapshotPersists = new Map();
609
+ const _failedSnapshotPersists = new Map();
610
+ const _activeSnapshotPersists = new Map();
611
+ const _queuedSnapshotPersists = new Map();
594
612
  const _snapshotWorkerRequests = new Map();
595
613
  let _snapshotPersistWorker;
596
614
  let _snapshotWorkerRequestId = 0;
@@ -598,6 +616,8 @@ let _snapshotWorkerDisabled = false;
598
616
  let _snapshotGenerationGateEnabledForTests = true;
599
617
  let _snapshotPromotionSeamForTests;
600
618
  let _lastSnapshotPersistErrorForTests;
619
+ let _snapshotExiting = false;
620
+ let _snapshotWorkerBodyWritesForTests = 0;
601
621
  function snapshotWorkerEnabled() {
602
622
  // The synchronous fallback writer is a legitimate degraded mode (hosts that
603
623
  // can't spawn a worker); tests also force it so a save→load is fully
@@ -605,31 +625,233 @@ function snapshotWorkerEnabled() {
605
625
  const raw = process.env.PI_LENS_SNAPSHOT_PERSIST_SYNC;
606
626
  return !(raw === "1" || raw === "true");
607
627
  }
608
- function recordSnapshotPersistFailure(cwd, error) {
628
+ function pendingSnapshotIsCurrent(pending) {
629
+ return (!_snapshotGenerationGateEnabledForTests ||
630
+ _snapshotGenerationStates.get(pending.key)?.generation ===
631
+ pending.generation);
632
+ }
633
+ function rememberSuccessfulSnapshotPersist(key, record) {
634
+ _successfulSnapshotPersists.delete(key);
635
+ _successfulSnapshotPersists.set(key, record);
636
+ while (_successfulSnapshotPersists.size > PROJECT_SNAPSHOT_MAX_WARM_ROOTS) {
637
+ const oldest = _successfulSnapshotPersists.keys().next().value;
638
+ if (oldest === undefined)
639
+ break;
640
+ _successfulSnapshotPersists.delete(oldest);
641
+ }
642
+ }
643
+ function assessSnapshotBodyIntegrity(meta, body) {
644
+ if (meta.gzBytes === undefined) {
645
+ // Legacy meta (pre-#2008): nothing to compare against, so the evidence is
646
+ // untrusted until the next successful persist populates gzBytes.
647
+ return { outcome: "legacy-meta", actualBytes: body.size };
648
+ }
649
+ if (!body.gz || body.size !== meta.gzBytes) {
650
+ return {
651
+ outcome: "size-mismatch",
652
+ expectedGzBytes: meta.gzBytes,
653
+ actualBytes: body.size,
654
+ };
655
+ }
656
+ return {
657
+ outcome: "ok",
658
+ expectedGzBytes: meta.gzBytes,
659
+ actualBytes: body.size,
660
+ };
661
+ }
662
+ /**
663
+ * Bounded observability for #2008: the degradation ledger keeps one entry per
664
+ * corrupted subject with the exact detection count for the session; every
665
+ * detection also emits one `project_snapshot_body_integrity` latency row
666
+ * naming expected vs actual bytes, so an operator can confirm both the
667
+ * detection and the forced republish from logs alone.
668
+ */
669
+ function noteSnapshotBodyIntegrityWithheld(args) {
670
+ incrementDegradationCount({
671
+ kind: "snapshot-integrity",
672
+ subject: args.gzPath,
673
+ reason: args.verdict,
674
+ });
675
+ logLatency({
676
+ type: "phase",
677
+ phase: "project_snapshot_body_integrity",
678
+ filePath: args.gzPath,
679
+ durationMs: 0,
680
+ metadata: {
681
+ outcome: "dedupe_withheld",
682
+ reason: args.verdict,
683
+ ...(args.seq !== undefined ? { seq: args.seq } : {}),
684
+ ...(args.expectedGzBytes === undefined
685
+ ? {}
686
+ : { expectedGzBytes: args.expectedGzBytes }),
687
+ actualBytes: args.actualBytes,
688
+ },
689
+ });
690
+ }
691
+ /** Log one row naming a skip that was refused because integrity regressed. */
692
+ function noteSnapshotSkipRefused(args) {
693
+ logLatency({
694
+ type: "phase",
695
+ phase: "project_snapshot_body_integrity",
696
+ filePath: args.gzPath,
697
+ durationMs: 0,
698
+ metadata: {
699
+ outcome: "skip_refused_rewrite",
700
+ reason: args.verdict,
701
+ seq: args.seq,
702
+ },
703
+ });
704
+ }
705
+ /**
706
+ * PURE READ of the durable meta+body evidence for a snapshot key (#2008
707
+ * refactor): it must never mutate {@link _successfulSnapshotPersists}. This
708
+ * runs up to three times per persist — admission in saveProjectSnapshot, the
709
+ * dispatch-time refresh, and the skip-honor re-read — including on results
710
+ * whose fingerprints are only partially used, so the seeding write lives in
711
+ * {@link seedSnapshotPersistBaselineFromDurable} and is called only by the
712
+ * seam that owns the persist lifecycle (dispatchSnapshotPersist).
713
+ */
714
+ function snapshotPersistBaselinesFor(cwd, key) {
715
+ const local = _successfulSnapshotPersists.get(key);
716
+ const meta = readProjectSnapshotMeta(cwd);
717
+ const body = resolveSnapshotBodyPath(cwd);
718
+ // No body means no dedupe evidence. This keeps same-seq deletion repair live.
719
+ if (!meta?.fingerprint || !body) {
720
+ if (!body)
721
+ _successfulSnapshotPersists.delete(key);
722
+ return { fingerprints: [] };
723
+ }
724
+ // #2008: a meta whose recorded gz size no longer matches the on-disk body
725
+ // describes evidence we cannot trust — a truncated/torn gzip under an
726
+ // intact meta used to win same-fingerprint dedupe forever, keeping the
727
+ // corrupt body canonical until seq advanced. Withhold the fingerprints so
728
+ // the pending save republishes (and rewrites) the body.
729
+ const integrity = assessSnapshotBodyIntegrity(meta, body);
730
+ if (integrity.outcome !== "ok") {
731
+ noteSnapshotBodyIntegrityWithheld({
732
+ gzPath: getProjectSnapshotPath(cwd),
733
+ verdict: integrity.outcome,
734
+ seq: meta.seq,
735
+ expectedGzBytes: integrity.expectedGzBytes,
736
+ actualBytes: integrity.actualBytes,
737
+ });
738
+ return { fingerprints: [] };
739
+ }
740
+ const durable = {
741
+ seq: meta.seq,
742
+ fingerprint: meta.fingerprint,
743
+ generatedAt: meta.timestamp,
744
+ generation: _snapshotGenerationStates.get(key)?.generation ?? 0,
745
+ gzBytes: meta.gzBytes,
746
+ };
747
+ const fingerprints = [durable.fingerprint];
748
+ // A sibling process may have advanced durable state from local A to B. An
749
+ // unchanged replay of A is stale work and must not overwrite B. A third
750
+ // semantic state C matches neither fingerprint and still publishes.
751
+ if (local && local.fingerprint !== durable.fingerprint) {
752
+ fingerprints.push(local.fingerprint);
753
+ }
754
+ return { durable, fingerprints, integrity: "ok" };
755
+ }
756
+ /**
757
+ * Seed the in-process baseline for `key` from freshly-read durable state.
758
+ * Deliberately separate from {@link snapshotPersistBaselinesFor} so the read
759
+ * stays pure; called only at the dispatch seam where the persist lifecycle
760
+ * begins.
761
+ */
762
+ function seedSnapshotPersistBaselineFromDurable(key, durable) {
763
+ if (!durable || _successfulSnapshotPersists.has(key))
764
+ return;
765
+ rememberSuccessfulSnapshotPersist(key, durable);
766
+ }
767
+ function writeProjectSnapshotMeta(metaPath, snapshot, bodyRecord) {
768
+ writeFileAtomic(metaPath, JSON.stringify({
769
+ timestamp: bodyRecord?.generatedAt ?? snapshot.generatedAt,
770
+ version: snapshot.version,
771
+ seq: snapshot.seq,
772
+ ...(bodyRecord
773
+ ? {
774
+ fingerprint: bodyRecord.fingerprint,
775
+ ...(bodyRecord.gzBytes === undefined
776
+ ? {}
777
+ : { gzBytes: bodyRecord.gzBytes }),
778
+ }
779
+ : {}),
780
+ ...(snapshot.sequenceIndex
781
+ ? { sequenceIndex: snapshot.sequenceIndex }
782
+ : {}),
783
+ }), { bestEffort: false });
784
+ }
785
+ function recordSnapshotPersistFailure(pending, error, bodyPersisted = false) {
609
786
  // Honesty (#533): a failed async body write must be surfaced, never left to
610
787
  // masquerade as a saved snapshot. The meta gate is already self-healing (an
611
788
  // old body under a newer-seq meta is rejected on the body's own embedded
612
789
  // seq), and dropping the authoritative entry means the next load reflects
613
790
  // what is ACTUALLY on disk rather than the object we failed to persist.
614
791
  _lastSnapshotPersistErrorForTests = error;
615
- deleteAuthoritativeSnapshot(normalizeMapKey(cwd));
792
+ _failedSnapshotPersists.set(pending.key, {
793
+ seq: pending.snapshot.seq,
794
+ generation: pending.generation,
795
+ });
796
+ while (_failedSnapshotPersists.size > PROJECT_SNAPSHOT_MAX_WARM_ROOTS) {
797
+ const oldest = _failedSnapshotPersists.keys().next().value;
798
+ if (oldest === undefined)
799
+ break;
800
+ _failedSnapshotPersists.delete(oldest);
801
+ }
802
+ deleteAuthoritativeSnapshot(pending.key);
616
803
  logLatency({
617
804
  type: "phase",
618
805
  phase: "project_snapshot_persist_failed",
619
- filePath: getProjectSnapshotPath(cwd),
806
+ filePath: pending.gzPath,
620
807
  durationMs: 0,
621
- metadata: { error },
808
+ metadata: {
809
+ error,
810
+ seq: pending.snapshot.seq,
811
+ outcome: bodyPersisted ? "metadata_repair_required" : "failed",
812
+ ...(bodyPersisted ? { bodyPersisted: true } : {}),
813
+ },
622
814
  });
623
815
  // #1333: the logLatency call above already carries this failure to
624
816
  // latency.log — the console.error was a duplicate RAW write into pi's frame.
625
817
  }
626
- function logSnapshotPersistSuccess(pending, stats) {
818
+ function logSnapshotPersistSuccess(pending, fingerprint, stats) {
819
+ rememberSuccessfulSnapshotPersist(pending.key, {
820
+ seq: pending.snapshot.seq,
821
+ fingerprint,
822
+ generatedAt: pending.snapshot.generatedAt,
823
+ generation: pending.generation,
824
+ rawBytes: stats.rawBytes,
825
+ gzBytes: stats.gzBytes,
826
+ });
827
+ _failedSnapshotPersists.delete(pending.key);
627
828
  logLatency({
628
829
  type: "phase",
629
830
  phase: "project_snapshot_persist",
630
831
  filePath: pending.gzPath,
631
832
  durationMs: stats.serializeMs + stats.writeMs,
632
- metadata: { seq: pending.snapshot.seq, ...stats },
833
+ metadata: { seq: pending.snapshot.seq, outcome: "executed", ...stats },
834
+ });
835
+ }
836
+ function logSnapshotPersistDecision(args) {
837
+ logLatency({
838
+ type: "phase",
839
+ phase: "project_snapshot_persist_decision",
840
+ filePath: getProjectSnapshotPath(args.cwd),
841
+ durationMs: 0,
842
+ metadata: {
843
+ seq: args.seq,
844
+ decision: args.decision,
845
+ ...(args.fingerprint
846
+ ? { fingerprint: args.fingerprint.slice(0, 12) }
847
+ : {}),
848
+ ...(args.avoidedRawBytes === undefined
849
+ ? {}
850
+ : { avoidedRawBytes: args.avoidedRawBytes }),
851
+ ...(args.avoidedGzipBytes === undefined
852
+ ? {}
853
+ : { avoidedGzipBytes: args.avoidedGzipBytes }),
854
+ },
633
855
  });
634
856
  }
635
857
  /**
@@ -692,7 +914,33 @@ function reconcileAuthoritativeAfterWrite(pending, rawBytes) {
692
914
  // is one extra disk re-parse on the next load.
693
915
  }
694
916
  }
917
+ function finalizeProjectSnapshotMeta(pending, record) {
918
+ try {
919
+ writeProjectSnapshotMeta(getProjectSnapshotMetaPath(pending.cwd), pending.snapshot, record);
920
+ return true;
921
+ }
922
+ catch (err) {
923
+ recordSnapshotPersistFailure(pending, err instanceof Error ? err.message : String(err), true);
924
+ return false;
925
+ }
926
+ }
927
+ function completeSnapshotPersist(pending) {
928
+ if (_activeSnapshotPersists.get(pending.key) !== pending)
929
+ return;
930
+ _activeSnapshotPersists.delete(pending.key);
931
+ if (_snapshotExiting)
932
+ return;
933
+ const queued = _queuedSnapshotPersists.get(pending.key);
934
+ if (!queued)
935
+ return;
936
+ _queuedSnapshotPersists.delete(pending.key);
937
+ dispatchSnapshotPersist(queued);
938
+ }
695
939
  function writeSnapshotBodyOnMainThread(pending, reason) {
940
+ if (!pendingSnapshotIsCurrent(pending)) {
941
+ completeSnapshotPersist(pending);
942
+ return;
943
+ }
696
944
  if (reason) {
697
945
  // We took the synchronous main-thread gzip path (the +656MB-risk path,
698
946
  // #950) instead of the worker. Surface it rather than burying it in an
@@ -717,13 +965,52 @@ function writeSnapshotBodyOnMainThread(pending, reason) {
717
965
  const json = JSON.stringify(pending.snapshot);
718
966
  const serializeMs = performance.now() - serializeStarted;
719
967
  const rawBytes = Buffer.byteLength(json);
968
+ const fingerprint = fingerprintProjectSnapshotJson(json, pending.snapshot.generatedAt);
969
+ if (pending.dedupeFingerprints.includes(fingerprint)) {
970
+ // Re-read the tiny durable sidecar before honoring a fingerprint
971
+ // captured at dispatch time (#2008): a body torn between dispatch and
972
+ // execution must fall through to the full write, not skip.
973
+ const current = snapshotPersistBaselinesFor(pending.cwd, pending.key);
974
+ if (current.integrity === "ok") {
975
+ const prior = current.durable ?? pending.durablePersist;
976
+ if (authoritativeSnapshots.get(pending.key)?.snapshot === pending.snapshot) {
977
+ deleteAuthoritativeSnapshot(pending.key);
978
+ }
979
+ logSnapshotPersistDecision({
980
+ cwd: pending.cwd,
981
+ seq: pending.snapshot.seq,
982
+ fingerprint,
983
+ decision: "skipped_unchanged",
984
+ avoidedRawBytes: rawBytes,
985
+ avoidedGzipBytes: prior?.gzBytes,
986
+ });
987
+ return;
988
+ }
989
+ if (current.integrity !== undefined) {
990
+ noteSnapshotSkipRefused({
991
+ gzPath: pending.gzPath,
992
+ verdict: current.integrity,
993
+ seq: pending.snapshot.seq,
994
+ });
995
+ }
996
+ // integrity === undefined: the meta/body evidence vanished entirely
997
+ // since dispatch — falling through to the full write repairs it, the
998
+ // same contract as deletion-before-dispatch.
999
+ }
720
1000
  const writeStarted = performance.now();
721
1001
  const gzip = gzipSync(json);
722
1002
  fs.mkdirSync(path.dirname(pending.gzPath), { recursive: true });
723
1003
  writeFileAtomic(pending.gzPath, gzip, { bestEffort: false });
724
1004
  fs.rmSync(pending.legacyPath, { force: true });
1005
+ const metadataFinalized = finalizeProjectSnapshotMeta(pending, {
1006
+ fingerprint,
1007
+ generatedAt: pending.snapshot.generatedAt,
1008
+ gzBytes: gzip.byteLength,
1009
+ });
1010
+ if (!metadataFinalized)
1011
+ return;
725
1012
  reconcileAuthoritativeAfterWrite(pending, rawBytes);
726
- logSnapshotPersistSuccess(pending, {
1013
+ logSnapshotPersistSuccess(pending, fingerprint, {
727
1014
  rawBytes,
728
1015
  gzBytes: gzip.byteLength,
729
1016
  serializeMs,
@@ -734,7 +1021,10 @@ function writeSnapshotBodyOnMainThread(pending, reason) {
734
1021
  _lastSnapshotPersistErrorForTests = reason;
735
1022
  }
736
1023
  catch (err) {
737
- recordSnapshotPersistFailure(pending.cwd, err instanceof Error ? err.message : String(err));
1024
+ recordSnapshotPersistFailure(pending, err instanceof Error ? err.message : String(err));
1025
+ }
1026
+ finally {
1027
+ completeSnapshotPersist(pending);
738
1028
  }
739
1029
  }
740
1030
  /**
@@ -753,7 +1043,7 @@ function dispatchMainThreadWriteThroughSeam(pending, reason) {
753
1043
  function handleSnapshotWorkerResult(result) {
754
1044
  const pending = _snapshotWorkerRequests.get(result.id);
755
1045
  if (!pending) {
756
- fs.rm(result.stagePath, { force: true }, () => { });
1046
+ fs.rmSync(result.stagePath, { force: true });
757
1047
  return;
758
1048
  }
759
1049
  _snapshotWorkerRequests.delete(result.id);
@@ -761,37 +1051,128 @@ function handleSnapshotWorkerResult(result) {
761
1051
  result.rawBytes === undefined ||
762
1052
  result.gzBytes === undefined ||
763
1053
  result.serializeMs === undefined ||
764
- result.writeMs === undefined) {
765
- fs.rm(result.stagePath, { force: true }, () => { });
1054
+ result.writeMs === undefined ||
1055
+ result.semanticFingerprint === undefined) {
1056
+ fs.rmSync(result.stagePath, { force: true });
766
1057
  dispatchMainThreadWriteThroughSeam(pending, result.error ?? "invalid worker result");
767
1058
  return;
768
1059
  }
769
1060
  // Generation gate: a newer save already superseded this one — discard the
770
1061
  // stale stage file rather than promote it over the fresher body.
1062
+ if (!result.skippedUnchanged)
1063
+ _snapshotWorkerBodyWritesForTests++;
771
1064
  if (_snapshotGenerationGateEnabledForTests &&
772
- _snapshotGenerations.get(pending.key) !== result.generation) {
1065
+ _snapshotGenerationStates.get(pending.key)?.generation !== result.generation) {
773
1066
  // The stale stage is part of the promotion transaction: remove it before
774
1067
  // returning so a superseded save cannot leave an orphan behind.
775
- fs.rm(result.stagePath, { force: true }, () => { });
1068
+ fs.rmSync(result.stagePath, { force: true });
1069
+ completeSnapshotPersist(pending);
1070
+ return;
1071
+ }
1072
+ if (result.skippedUnchanged) {
1073
+ // Re-read the tiny durable sidecar at the decision seam. A sibling process
1074
+ // may have advanced B after this request captured A. A stale replay of A
1075
+ // must preserve B's body metadata, not restore the captured A sidecar.
1076
+ const baselines = snapshotPersistBaselinesFor(pending.cwd, pending.key);
1077
+ if (baselines.integrity !== "ok") {
1078
+ // The worker skipped WITHOUT staging a body, trusting the durable
1079
+ // fingerprint captured at dispatch. The re-read says that evidence no
1080
+ // longer passes the #2008 integrity gate — refuse the skip and rewrite
1081
+ // synchronously so a torn/missing body cannot outlive this save. The
1082
+ // sync writer recomputes the same baselines, finds them untrusted, and
1083
+ // performs the full gzip+stage+promote (its finally completes this
1084
+ // pending, so do not complete it twice here).
1085
+ if (baselines.integrity !== undefined) {
1086
+ noteSnapshotSkipRefused({
1087
+ gzPath: pending.gzPath,
1088
+ verdict: baselines.integrity,
1089
+ seq: pending.snapshot.seq,
1090
+ });
1091
+ }
1092
+ dispatchMainThreadWriteThroughSeam(pending, "snapshot body integrity regressed");
1093
+ return;
1094
+ }
1095
+ const prior = baselines.durable ?? pending.durablePersist;
1096
+ if (authoritativeSnapshots.get(pending.key)?.snapshot === pending.snapshot) {
1097
+ deleteAuthoritativeSnapshot(pending.key);
1098
+ }
1099
+ logSnapshotPersistDecision({
1100
+ cwd: pending.cwd,
1101
+ seq: pending.snapshot.seq,
1102
+ fingerprint: result.semanticFingerprint,
1103
+ decision: "skipped_unchanged",
1104
+ avoidedRawBytes: result.rawBytes,
1105
+ avoidedGzipBytes: prior?.gzBytes,
1106
+ });
1107
+ completeSnapshotPersist(pending);
776
1108
  return;
777
1109
  }
778
1110
  try {
779
1111
  fs.renameSync(result.stagePath, pending.gzPath);
780
1112
  fs.rmSync(pending.legacyPath, { force: true });
1113
+ const metadataFinalized = finalizeProjectSnapshotMeta(pending, {
1114
+ fingerprint: result.semanticFingerprint,
1115
+ generatedAt: pending.snapshot.generatedAt,
1116
+ gzBytes: result.gzBytes,
1117
+ });
1118
+ if (!metadataFinalized) {
1119
+ completeSnapshotPersist(pending);
1120
+ return;
1121
+ }
781
1122
  reconcileAuthoritativeAfterWrite(pending, result.rawBytes);
782
- logSnapshotPersistSuccess(pending, {
1123
+ logSnapshotPersistSuccess(pending, result.semanticFingerprint, {
783
1124
  rawBytes: result.rawBytes,
784
1125
  gzBytes: result.gzBytes,
785
1126
  serializeMs: result.serializeMs,
786
1127
  writeMs: result.writeMs,
787
1128
  offloaded: true,
788
1129
  });
1130
+ completeSnapshotPersist(pending);
789
1131
  }
790
1132
  catch (err) {
791
1133
  fs.rm(result.stagePath, { force: true }, () => { });
792
1134
  dispatchMainThreadWriteThroughSeam(pending, err instanceof Error ? err.message : String(err));
793
1135
  }
794
1136
  }
1137
+ function dispatchSnapshotPersist(pending) {
1138
+ // A queued request may have waited behind the publication that created its
1139
+ // best dedupe evidence. Refresh only the tiny sidecar/body stat here; never
1140
+ // retain the stale admission-time baseline through dispatch.
1141
+ const baselines = snapshotPersistBaselinesFor(pending.cwd, pending.key);
1142
+ pending.durablePersist = baselines.durable;
1143
+ pending.dedupeFingerprints = baselines.fingerprints;
1144
+ seedSnapshotPersistBaselineFromDurable(pending.key, baselines.durable);
1145
+ _activeSnapshotPersists.set(pending.key, pending);
1146
+ if (!snapshotWorkerEnabled()) {
1147
+ dispatchMainThreadWriteThroughSeam(pending, undefined);
1148
+ return;
1149
+ }
1150
+ const worker = getSnapshotPersistWorker();
1151
+ if (!worker) {
1152
+ dispatchMainThreadWriteThroughSeam(pending, "persist worker unavailable");
1153
+ return;
1154
+ }
1155
+ const id = ++_snapshotWorkerRequestId;
1156
+ _snapshotWorkerRequests.set(id, pending);
1157
+ const request = {
1158
+ id,
1159
+ generation: pending.generation,
1160
+ stagePath: pending.stagePath,
1161
+ data: pending.snapshot,
1162
+ priorFingerprints: pending.dedupeFingerprints,
1163
+ testDelayMs: process.env.NODE_ENV === "test"
1164
+ ? Number(process.env.PI_LENS_TEST_SNAPSHOT_PERSIST_WORKER_DELAY_MS) ||
1165
+ undefined
1166
+ : undefined,
1167
+ };
1168
+ try {
1169
+ worker.postMessage(request);
1170
+ }
1171
+ catch (err) {
1172
+ _snapshotWorkerRequests.delete(id);
1173
+ dispatchMainThreadWriteThroughSeam(pending, err instanceof Error ? err.message : String(err));
1174
+ }
1175
+ }
795
1176
  function handleSnapshotWorkerDeath(reason) {
796
1177
  _snapshotPersistWorker = undefined;
797
1178
  _snapshotWorkerDisabled = true;
@@ -909,12 +1290,27 @@ function ensureSnapshotPersistExitHook() {
909
1290
  return;
910
1291
  _snapshotExitHookInstalled = true;
911
1292
  process.once("exit", () => {
912
- const requests = [..._snapshotWorkerRequests.values()];
1293
+ _snapshotExiting = true;
1294
+ const latestByKey = new Map();
1295
+ for (const pending of _snapshotWorkerRequests.values()) {
1296
+ latestByKey.set(pending.key, pending);
1297
+ }
1298
+ for (const pending of _queuedSnapshotPersists.values()) {
1299
+ const prior = latestByKey.get(pending.key);
1300
+ // Queued work is later admission order. Equal-seq requests deliberately
1301
+ // share a gate generation, so a tie must still select the queued payload.
1302
+ if (!prior || prior.generation <= pending.generation) {
1303
+ latestByKey.set(pending.key, pending);
1304
+ }
1305
+ }
913
1306
  _snapshotWorkerRequests.clear();
914
- for (const pending of requests) {
1307
+ _queuedSnapshotPersists.clear();
1308
+ _activeSnapshotPersists.clear();
1309
+ for (const pending of latestByKey.values()) {
915
1310
  // Only the newest generation per key still matters; older ones are
916
1311
  // superseded and their stage files are swept on next launch.
917
- if (_snapshotGenerations.get(pending.key) !== pending.generation)
1312
+ if (_snapshotGenerationStates.get(pending.key)?.generation !==
1313
+ pending.generation)
918
1314
  continue;
919
1315
  writeSnapshotBodyOnMainThread(pending, "exit_hook");
920
1316
  }
@@ -928,8 +1324,18 @@ export function saveProjectSnapshot(cwd, snapshot) {
928
1324
  const cacheDir = path.dirname(gzPath);
929
1325
  const key = normalizeMapKey(cwd);
930
1326
  fs.mkdirSync(cacheDir, { recursive: true });
931
- // #958: meta is written FIRST, body SECOND — the reverse of the original
932
- // order. A crash/failure between the two writes can now only produce
1327
+ const baselines = snapshotPersistBaselinesFor(cwd, key);
1328
+ const priorPersist = baselines.durable;
1329
+ logSnapshotPersistDecision({
1330
+ cwd,
1331
+ seq: snapshot.seq,
1332
+ decision: "requested",
1333
+ });
1334
+ if (_failedSnapshotPersists.delete(key)) {
1335
+ logSnapshotPersistDecision({ cwd, seq: snapshot.seq, decision: "retry" });
1336
+ }
1337
+ // #958: for a new seq, meta is written FIRST and body SECOND. A crash or
1338
+ // failure between the two writes can now only produce
933
1339
  // "meta already claims the new seq, body hasn't caught up yet" (the meta
934
1340
  // races ahead). The meta-first gate (isProjectSnapshotMetaStale) reads
935
1341
  // that as *fresh* and falls through to parsing the body, whose own
@@ -941,24 +1347,14 @@ export function saveProjectSnapshot(cwd, snapshot) {
941
1347
  // away a genuinely fresh snapshot. That direction is not recoverable
942
1348
  // until the next save, so it's the one this reorder eliminates.
943
1349
  //
944
- // The meta write is still SYNCHRONOUS (it is tiny) and uses
945
- // `bestEffort: false`: if it fails, the body persist below is skipped
946
- // entirely — the save is simply lost this round (fail-open, caught by
947
- // `saveRuntimeProjectSnapshot`'s own try/catch) rather than leaving a
948
- // stale meta in place while the body writer stampedes ahead.
949
- writeFileAtomic(metaPath, JSON.stringify({
950
- timestamp: snapshot.generatedAt,
951
- version: snapshot.version,
952
- seq: snapshot.seq,
953
- // #1019: mirror the derived sequence index (kept consistent with `seq`
954
- // because both are stamped from the same runtime moment) so the
955
- // interactive path can hydrate it from the tiny sidecar. Omitted when
956
- // absent so a non-runtime side-write (word-index/reverse-deps) that
957
- // carried no index doesn't stamp an empty one.
958
- ...(snapshot.sequenceIndex
959
- ? { sequenceIndex: snapshot.sequenceIndex }
960
- : {}),
961
- }), { bestEffort: false });
1350
+ // The new-seq meta write stays synchronous and throwing. Same-seq work does
1351
+ // not need to advance the freshness gate, so it leaves the current sidecar
1352
+ // untouched until the worker has a semantic verdict. This prevents a stale
1353
+ // local request from replacing a
1354
+ // newer sibling process's same-seq fingerprint before it can coalesce.
1355
+ if (priorPersist?.seq !== snapshot.seq) {
1356
+ writeProjectSnapshotMeta(metaPath, snapshot);
1357
+ }
962
1358
  // Record the authoritative in-process write BEFORE handing the body off, so
963
1359
  // a merge-read between now and the worker's promotion sees our own object
964
1360
  // (the on-disk body still holds the previous generation until promotion). The
@@ -983,8 +1379,11 @@ export function saveProjectSnapshot(cwd, snapshot) {
983
1379
  // A stale disk-parse-cache entry for this path must not out-vote the fresh
984
1380
  // authoritative write once the latter is dropped (oversized bodies).
985
1381
  snapshotParseCache.delete(gzPath);
986
- const generation = (_snapshotGenerations.get(key) ?? 0) + 1;
987
- _snapshotGenerations.set(key, generation);
1382
+ const generationState = _snapshotGenerationStates.get(key);
1383
+ const generation = generationState?.seq === snapshot.seq
1384
+ ? generationState.generation
1385
+ : (generationState?.generation ?? 0) + 1;
1386
+ _snapshotGenerationStates.set(key, { generation, seq: snapshot.seq });
988
1387
  const stagePath = `${gzPath}.stage-${process.pid}-${generation}`;
989
1388
  const pending = {
990
1389
  key,
@@ -994,36 +1393,29 @@ export function saveProjectSnapshot(cwd, snapshot) {
994
1393
  stagePath,
995
1394
  snapshot,
996
1395
  generation,
1396
+ durablePersist: priorPersist,
1397
+ dedupeFingerprints: baselines.fingerprints,
997
1398
  };
998
1399
  sweepStaleSnapshotStageFiles(cacheDir);
999
1400
  ensureSnapshotPersistExitHook();
1000
- if (!snapshotWorkerEnabled()) {
1001
- dispatchMainThreadWriteThroughSeam(pending, undefined);
1002
- return;
1003
- }
1004
- const worker = getSnapshotPersistWorker();
1005
- if (!worker) {
1006
- dispatchMainThreadWriteThroughSeam(pending, "persist worker unavailable");
1401
+ if (_activeSnapshotPersists.has(key)) {
1402
+ _queuedSnapshotPersists.set(key, pending);
1403
+ logSnapshotPersistDecision({
1404
+ cwd,
1405
+ seq: snapshot.seq,
1406
+ decision: "coalesced",
1407
+ });
1007
1408
  return;
1008
1409
  }
1009
- const id = ++_snapshotWorkerRequestId;
1010
- _snapshotWorkerRequests.set(id, pending);
1011
- const request = {
1012
- id,
1013
- generation,
1014
- stagePath,
1015
- data: snapshot,
1016
- testDelayMs: process.env.NODE_ENV === "test"
1017
- ? Number(process.env.PI_LENS_TEST_SNAPSHOT_PERSIST_WORKER_DELAY_MS) ||
1018
- undefined
1019
- : undefined,
1020
- };
1021
- worker.postMessage(request);
1410
+ dispatchSnapshotPersist(pending);
1022
1411
  }
1023
1412
  // --- Test hooks for the worker persist path ---------------------------------
1024
1413
  /** Test-only: wait until worker requests have either landed or degraded. */
1025
1414
  export async function waitForProjectSnapshotPersistsForTests() {
1026
- for (let attempts = 0; attempts < 200 && _snapshotWorkerRequests.size > 0; attempts++) {
1415
+ for (let attempts = 0; attempts < 200 &&
1416
+ (_snapshotWorkerRequests.size > 0 ||
1417
+ _activeSnapshotPersists.size > 0 ||
1418
+ _queuedSnapshotPersists.size > 0); attempts++) {
1027
1419
  await new Promise((resolve) => setTimeout(resolve, 10));
1028
1420
  }
1029
1421
  }
@@ -1032,8 +1424,6 @@ export function flushProjectSnapshotPersistsForTests() {
1032
1424
  const requests = [..._snapshotWorkerRequests.values()];
1033
1425
  _snapshotWorkerRequests.clear();
1034
1426
  for (const pending of requests) {
1035
- if (_snapshotGenerations.get(pending.key) !== pending.generation)
1036
- continue;
1037
1427
  dispatchMainThreadWriteThroughSeam(pending, undefined);
1038
1428
  }
1039
1429
  }
@@ -1050,9 +1440,25 @@ export function resetProjectSnapshotPersistWorkerForTests() {
1050
1440
  _snapshotPromotionSeamForTests = undefined;
1051
1441
  _snapshotPersistWorker = undefined;
1052
1442
  _snapshotWorkerRequests.clear();
1053
- _snapshotGenerations.clear();
1443
+ _snapshotGenerationStates.clear();
1444
+ _successfulSnapshotPersists.clear();
1445
+ _failedSnapshotPersists.clear();
1446
+ _activeSnapshotPersists.clear();
1447
+ _queuedSnapshotPersists.clear();
1448
+ _snapshotExiting = false;
1449
+ _snapshotWorkerBodyWritesForTests = 0;
1054
1450
  _lastSnapshotPersistErrorForTests = undefined;
1055
1451
  }
1452
+ /** Test-only: expose bounded queue state without retaining snapshot bodies. */
1453
+ export function getProjectSnapshotPersistStateForTests(cwd) {
1454
+ const key = normalizeMapKey(cwd);
1455
+ return {
1456
+ active: _activeSnapshotPersists.has(key),
1457
+ queued: _queuedSnapshotPersists.has(key),
1458
+ successfulFingerprint: _successfulSnapshotPersists.get(key)?.fingerprint,
1459
+ workerBodyWrites: _snapshotWorkerBodyWritesForTests,
1460
+ };
1461
+ }
1056
1462
  /** Test-only mutation switch for proving the supersession invariant. */
1057
1463
  export function setProjectSnapshotGenerationGateForTests(enabled) {
1058
1464
  _snapshotGenerationGateEnabledForTests = enabled;