@herbertgao/pi-extensions 2026.10.1 → 2026.10.2
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.
- package/README.md +13 -9
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/README.md +68 -72
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +7 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
- package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -32
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +17 -16
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +9 -10
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -57
- package/node_modules/@herbertgao/pi-agency/LICENSE +21 -0
- package/node_modules/@herbertgao/pi-agency/README.md +40 -0
- package/node_modules/@herbertgao/pi-agency/package.json +53 -0
- package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
- package/node_modules/@narumitw/pi-caffeinate/package.json +3 -3
- package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
- package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
- package/node_modules/@tifan/pi-recap/package.json +1 -1
- package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
- package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
- package/node_modules/pi-antigravity/README.md +23 -2
- package/node_modules/pi-antigravity/package.json +5 -3
- package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
- package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
- package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
- package/node_modules/pi-antigravity/src/image/image.ts +86 -47
- package/node_modules/pi-antigravity/src/image/native.ts +69 -0
- package/node_modules/pi-antigravity/src/index.ts +52 -137
- package/node_modules/pi-antigravity/src/search/search.ts +130 -31
- package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
- package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
- package/node_modules/pi-antigravity/src/types/types.ts +1 -1
- package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
- package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
- package/node_modules/pi-lens/CHANGELOG.md +1064 -0
- package/node_modules/pi-lens/README.md +115 -25
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
- package/node_modules/pi-lens/dist/chunk-22AVIGXS.js +560 -0
- package/node_modules/pi-lens/dist/chunk-27BBHSZC.js +106 -0
- package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
- package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
- package/node_modules/pi-lens/dist/chunk-34U7PLGE.js +23 -0
- package/node_modules/pi-lens/dist/chunk-3I5QFT3N.js +181 -0
- package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
- package/node_modules/pi-lens/dist/chunk-47D2BO4S.js +87 -0
- package/node_modules/pi-lens/dist/chunk-4RERDEXR.js +348 -0
- package/node_modules/pi-lens/dist/chunk-4W2PLC4J.js +27 -0
- package/node_modules/pi-lens/dist/chunk-52H5B5TC.js +202 -0
- package/node_modules/pi-lens/dist/chunk-54B42YRX.js +27 -0
- package/node_modules/pi-lens/dist/chunk-5RXBNSW2.js +266 -0
- package/node_modules/pi-lens/dist/chunk-5THXD6X5.js +201 -0
- package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
- package/node_modules/pi-lens/dist/chunk-5ZEJHV35.js +341 -0
- package/node_modules/pi-lens/dist/chunk-6P62QVEA.js +118 -0
- package/node_modules/pi-lens/dist/chunk-6QIUX4EP.js +459 -0
- package/node_modules/pi-lens/dist/chunk-6VCD4VKZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-7F72PXY2.js +326 -0
- package/node_modules/pi-lens/dist/chunk-7FBF7NPR.js +998 -0
- package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
- package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
- package/node_modules/pi-lens/dist/chunk-AHDW3GPE.js +50 -0
- package/node_modules/pi-lens/dist/chunk-AJ3QXTRQ.js +756 -0
- package/node_modules/pi-lens/dist/chunk-AJVLWNFN.js +1610 -0
- package/node_modules/pi-lens/dist/chunk-AJXODZY6.js +112 -0
- package/node_modules/pi-lens/dist/chunk-ARFCXG7S.js +84 -0
- package/node_modules/pi-lens/dist/chunk-AS44CFQ4.js +708 -0
- package/node_modules/pi-lens/dist/chunk-AWE5TMPO.js +38 -0
- package/node_modules/pi-lens/dist/chunk-B3I2UUT4.js +4692 -0
- package/node_modules/pi-lens/dist/chunk-BRJKOBZP.js +331 -0
- package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
- package/node_modules/pi-lens/dist/chunk-C2AUYXQH.js +149 -0
- package/node_modules/pi-lens/dist/chunk-CGGAIP7J.js +29 -0
- package/node_modules/pi-lens/dist/chunk-D6QS3U6M.js +30 -0
- package/node_modules/pi-lens/dist/chunk-DFVMIKMQ.js +349 -0
- package/node_modules/pi-lens/dist/chunk-DIULIWKU.js +169 -0
- package/node_modules/pi-lens/dist/chunk-DQ2NZ7C4.js +2317 -0
- package/node_modules/pi-lens/dist/chunk-E2WFBRHM.js +213 -0
- package/node_modules/pi-lens/dist/chunk-E4MAEDPM.js +50 -0
- package/node_modules/pi-lens/dist/chunk-EOKRQ3SA.js +30 -0
- package/node_modules/pi-lens/dist/chunk-F3AU2TMM.js +2626 -0
- package/node_modules/pi-lens/dist/chunk-F3P7VQ7H.js +170 -0
- package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
- package/node_modules/pi-lens/dist/chunk-FJNKANOS.js +32 -0
- package/node_modules/pi-lens/dist/chunk-GUOGVTCQ.js +146 -0
- package/node_modules/pi-lens/dist/chunk-GVXA6CCE.js +18 -0
- package/node_modules/pi-lens/dist/chunk-HHYAELLJ.js +2334 -0
- package/node_modules/pi-lens/dist/chunk-HMFXILPV.js +83 -0
- package/node_modules/pi-lens/dist/chunk-HVUZXSWK.js +421 -0
- package/node_modules/pi-lens/dist/chunk-HWBZIGMJ.js +62 -0
- package/node_modules/pi-lens/dist/chunk-I3R7GKDR.js +17 -0
- package/node_modules/pi-lens/dist/chunk-I7SPGOWI.js +55 -0
- package/node_modules/pi-lens/dist/chunk-IIXOAGHR.js +38 -0
- package/node_modules/pi-lens/dist/chunk-IVP5KAB7.js +2250 -0
- package/node_modules/pi-lens/dist/chunk-IXEBJSUE.js +22 -0
- package/node_modules/pi-lens/dist/chunk-JOLITGCC.js +27 -0
- package/node_modules/pi-lens/dist/chunk-K6VYLB2U.js +145 -0
- package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LNAKEO63.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LSMJ6KBE.js +4139 -0
- package/node_modules/pi-lens/dist/chunk-LW6XKDZQ.js +5565 -0
- package/node_modules/pi-lens/dist/chunk-N3YQJI6O.js +899 -0
- package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
- package/node_modules/pi-lens/dist/chunk-NKOUFPQF.js +659 -0
- package/node_modules/pi-lens/dist/chunk-NL5GLMFI.js +211 -0
- package/node_modules/pi-lens/dist/chunk-NMABWBZN.js +327 -0
- package/node_modules/pi-lens/dist/chunk-NTHH2J74.js +27 -0
- package/node_modules/pi-lens/dist/chunk-NTUJEGDZ.js +59 -0
- package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
- package/node_modules/pi-lens/dist/chunk-NXPS2L6U.js +466 -0
- package/node_modules/pi-lens/dist/chunk-O5WILGMI.js +42 -0
- package/node_modules/pi-lens/dist/chunk-O6TQT6RI.js +48 -0
- package/node_modules/pi-lens/dist/chunk-OAXNDZJ7.js +323 -0
- package/node_modules/pi-lens/dist/chunk-ODEEOXPH.js +44 -0
- package/node_modules/pi-lens/dist/chunk-OQGFCZJ4.js +4533 -0
- package/node_modules/pi-lens/dist/chunk-ORTMY4Q4.js +282 -0
- package/node_modules/pi-lens/dist/chunk-OS32RNR2.js +442 -0
- package/node_modules/pi-lens/dist/chunk-PAK6IDME.js +189 -0
- package/node_modules/pi-lens/dist/chunk-PWFHBTSF.js +406 -0
- package/node_modules/pi-lens/dist/chunk-Q5U6FMDI.js +2352 -0
- package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
- package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
- package/node_modules/pi-lens/dist/chunk-RIISMHKN.js +36 -0
- package/node_modules/pi-lens/dist/chunk-RPD4ZSB7.js +25 -0
- package/node_modules/pi-lens/dist/chunk-RPD5K5NC.js +79 -0
- package/node_modules/pi-lens/dist/chunk-RUOG5D2C.js +262 -0
- package/node_modules/pi-lens/dist/chunk-T4ERT73Y.js +599 -0
- package/node_modules/pi-lens/dist/chunk-T6D4Q4AQ.js +54 -0
- package/node_modules/pi-lens/dist/chunk-TGPXLY2J.js +391 -0
- package/node_modules/pi-lens/dist/chunk-TLGFBM3F.js +12116 -0
- package/node_modules/pi-lens/dist/chunk-TO6WL3GM.js +88 -0
- package/node_modules/pi-lens/dist/chunk-TT22N74J.js +517 -0
- package/node_modules/pi-lens/dist/chunk-TVN5N6GY.js +21216 -0
- package/node_modules/pi-lens/dist/chunk-U3VGR4VU.js +308 -0
- package/node_modules/pi-lens/dist/chunk-UK3CMEAL.js +1250 -0
- package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
- package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
- package/node_modules/pi-lens/dist/chunk-UY752QSN.js +45 -0
- package/node_modules/pi-lens/dist/chunk-V5PXFSZK.js +188 -0
- package/node_modules/pi-lens/dist/chunk-V7R425XG.js +398 -0
- package/node_modules/pi-lens/dist/chunk-VCSRSYLF.js +81 -0
- package/node_modules/pi-lens/dist/chunk-VD4JQCQC.js +162 -0
- package/node_modules/pi-lens/dist/chunk-VDBPVIKD.js +62 -0
- package/node_modules/pi-lens/dist/chunk-VHQLPIRJ.js +114 -0
- package/node_modules/pi-lens/dist/chunk-VN2AXLEX.js +5414 -0
- package/node_modules/pi-lens/dist/chunk-VNMT7C64.js +478 -0
- package/node_modules/pi-lens/dist/chunk-WONERJTP.js +309 -0
- package/node_modules/pi-lens/dist/chunk-XECLWQZN.js +85 -0
- package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
- package/node_modules/pi-lens/dist/chunk-XK4CIILI.js +144 -0
- package/node_modules/pi-lens/dist/chunk-Y4T27COV.js +562 -0
- package/node_modules/pi-lens/dist/chunk-YGVYKNSN.js +1860 -0
- package/node_modules/pi-lens/dist/chunk-YLF3PPT5.js +308 -0
- package/node_modules/pi-lens/dist/chunk-YSNFMJKN.js +14 -0
- package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
- package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
- package/node_modules/pi-lens/dist/chunk-ZAWW3AF6.js +97 -0
- package/node_modules/pi-lens/dist/chunk-ZMOL6RQC.js +167 -0
- package/node_modules/pi-lens/dist/index.js +16456 -8335
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
- package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
- package/node_modules/pi-lens/dist/mcp/server.js +22589 -1573
- package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
- package/node_modules/pi-lens/dist/probes/installer.js +135 -0
- package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
- package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
- package/node_modules/pi-lens/docs/agent-guide.md +13 -4
- package/node_modules/pi-lens/docs/agent-tools.md +42 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +3 -2
- package/node_modules/pi-lens/docs/configuration.md +20 -1
- package/node_modules/pi-lens/docs/environment-variables.md +52 -2
- package/node_modules/pi-lens/docs/features.md +8 -4
- package/node_modules/pi-lens/docs/globalconfig.md +1 -0
- package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +36 -4
- package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
- package/node_modules/pi-lens/docs/mcp.md +21 -5
- package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +156 -88
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
- package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +694 -0
- package/node_modules/pi-lens/docs/pi-lens-monitor.md +73 -7
- package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +188 -48
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
- package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
- package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
- package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +53 -49
- package/node_modules/pi-lens/docs/settings.md +1 -0
- package/node_modules/pi-lens/docs/usage.md +5 -1
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
- package/node_modules/pi-lens/package.json +18 -6
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
- package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
- package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
- package/package.json +15 -10
- package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.ja.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +0 -212
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
- package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
- package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
- package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
- package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
- package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
- package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
- package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
- package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
- package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
- package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
- package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
- package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
- package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
- package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
- package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
- package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
- package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
- package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
- package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
- package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
- package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
- package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
- package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
- package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
- package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
- package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
- package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
- package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
- package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
- package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
- package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
- package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
- package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
- package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
- package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
- package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
- package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
- package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
- package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
- package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
- package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
- package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
- package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
- package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
- package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
- package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
- package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
- package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
- package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
- package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
- package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
- package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
- package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
- package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
- package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
- package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
- package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
- package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
- package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
- package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
- package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
- package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
- package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
- package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
- package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
- package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
- package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
- package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
- package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
- package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
- package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
- package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
- package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
- package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
- package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
- package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
- package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
- package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
- package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
- package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
- package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
- package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
- package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
- package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
- package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
- package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
- package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
- package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
- package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
- package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
- package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
- package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
- package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
- package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
- package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
- package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
- package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
- package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
- package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
- package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
- package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
- package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
- package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
- package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
- package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
- package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
- package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
- package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
- package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
- package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
- package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
- package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
- package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
- package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
- package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
- package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
- package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
- package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
- package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
- package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
- package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
- package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
- package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
- package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
- package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
- package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
- package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
- package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
- package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
- package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
- package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
- package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
- package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
- package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
- package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
- package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
- package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
- package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
- package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
- package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
- package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
- package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
- package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
- package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
- package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
- package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
- package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
- package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
- package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
- package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
- package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
- package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
- package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
- package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
- package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
- package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
- package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
- package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
- package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
- package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
- package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
- package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
- package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
- package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
- package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
- package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
- package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
- package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
- package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
- package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
- package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
- package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
- package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
- package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
- package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
- package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
- package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
- package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
- package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
- package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
- package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
- package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
- package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
- package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
- package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
- package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
- package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
- package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
- package/node_modules/pi-lens/dist/i18n.js +0 -69
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
- package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
- package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
- package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
- package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
- package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
- package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
- package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
- package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
- package/node_modules/pi-lens/dist/tools/shared.js +0 -29
- package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
|
@@ -1,2485 +0,0 @@
|
|
|
1
|
-
import * as nodeFs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { resetBoundedTelemetry } from "./bounded-telemetry.js";
|
|
4
|
-
import { rotateMessageEndAttribution } from "./message-end-attribution.js";
|
|
5
|
-
import { createDeadline, yieldIfOverBudget } from "./cooperative-budget.js";
|
|
6
|
-
import { deadCodeIssueCount } from "./dead-code-client.js";
|
|
7
|
-
import { logDeadCodeScan } from "./dead-code-logger.js";
|
|
8
|
-
import { incrementDegradationCount, recordDegradationOnce, resetDegradationLedger, } from "./degradation-ledger.js";
|
|
9
|
-
import { getDiagnosticTracker } from "./diagnostic-tracker.js";
|
|
10
|
-
import { resetPsScriptAnalyzerAvailability } from "./dispatch/runners/psscriptanalyzer.js";
|
|
11
|
-
import { resetInstallRetryLatches } from "./dispatch/runners/utils/availability-policy.js";
|
|
12
|
-
import { resetLazyInstallAttempts } from "./dispatch/runners/utils/lazy-installer.js";
|
|
13
|
-
import { resetDispatchAvailabilityState } from "./dispatch/runners/utils/runner-helpers.js";
|
|
14
|
-
import { resetObservedRunnerLatency } from "./dispatch/collect-later-tier.js";
|
|
15
|
-
import { resetPendingRunnerFindings } from "./dispatch/pending-runner-findings.js";
|
|
16
|
-
import { clearAllSessions as clearFileTimeSessions } from "./file-time.js";
|
|
17
|
-
import { drainProjectDataDirMigrations, getGlobalPiLensDir, getKnipIgnorePatterns, getProjectDataDir, resetProjectDataDirSessionState, } from "./file-utils.js";
|
|
18
|
-
import { GitleaksClient } from "./gitleaks-client.js";
|
|
19
|
-
import { resetGoAvailability } from "./go-client.js";
|
|
20
|
-
import { GovulncheckClient, } from "./govulncheck-client.js";
|
|
21
|
-
import { sweepAtomicWriteStages } from "./instance-reaper.js";
|
|
22
|
-
import { canRunStartupHeavyScans } from "./language-policy.js";
|
|
23
|
-
import { detectProjectLanguageProfile, getDefaultStartupTools, } from "./language-profile.js";
|
|
24
|
-
import { logLatency } from "./latency-logger.js";
|
|
25
|
-
import { runLogCleanup } from "./log-cleanup.js";
|
|
26
|
-
import { resetCascadeTierSessionState } from "./lsp/cascade-tier.js";
|
|
27
|
-
import { initLSPConfig, loadLSPConfig } from "./lsp/config.js";
|
|
28
|
-
import { resetWorkspaceDiagnosticsCacheSession } from "./lsp/workspace-diagnostics-session.js";
|
|
29
|
-
import { resetDirectLspCommandAvailability, resetLSPCaseSensitivityState, } from "./lsp/server.js";
|
|
30
|
-
import { loadLspService } from "./lsp-lazy.js";
|
|
31
|
-
import { resetManagedToolRefreshSession } from "./installer/managed-tool-refresh-session.js";
|
|
32
|
-
import { resetResolvedPathCache } from "./installer/index.js";
|
|
33
|
-
import { _resetPackageManagerCache } from "./package-manager.js";
|
|
34
|
-
import { clearFormatterCache } from "./formatters.js";
|
|
35
|
-
import { isAtOrAboveHomeDir } from "./path-utils.js";
|
|
36
|
-
import { isPrintMode } from "./print-mode.js";
|
|
37
|
-
import { readLatestProjectSequence, readLatestProjectSequenceAsync, } from "./project-changes.js";
|
|
38
|
-
import { getProjectSnapshotPath, getProjectSnapshotLegacyPath, hydrateRuntimeFromProjectSnapshot, hydrateRuntimeFromProjectSnapshotIfIdle, isProjectSnapshotFresh, isProjectSnapshotMetaStale, loadProjectSnapshot, loadProjectSnapshotExportsAndRules, PROJECT_SNAPSHOT_VERSION, readProjectSnapshotMeta, saveRuntimeProjectSnapshot, } from "./project-snapshot.js";
|
|
39
|
-
import { scanProjectRules } from "./rules-scanner.js";
|
|
40
|
-
import { resetRustAvailability } from "./rust-client.js";
|
|
41
|
-
import { resetSafeSpawnWindowsCommandCache } from "./safe-spawn.js";
|
|
42
|
-
import { resetAnalyzerBootstrapSessionState, } from "./bootstrap.js";
|
|
43
|
-
import { getSlowFsVerdict, isSlowFs, slowFsDegradationNotice, } from "./slow-fs.js";
|
|
44
|
-
import { countRecentSmells, formatSmellsSessionStartLine, resetSmellsSessionState, } from "./smells-rollup.js";
|
|
45
|
-
import { resetSituationalToolTelemetry } from "./situational-tool-telemetry.js";
|
|
46
|
-
import { findNearestProjectRoot, getStartupScanMaxEntries, isStartupScanVerdictFresh, resolveStartupScanContext, } from "./startup-scan.js";
|
|
47
|
-
import { getSubagentIdentity, isSubagentSession, subagentLightModeNotice, } from "./subagent-mode.js";
|
|
48
|
-
import { TrivyClient } from "./trivy-client.js";
|
|
49
|
-
import { isWarmAttached } from "./warm-attach.js";
|
|
50
|
-
import { setSessionLanguages } from "./widget-state.js";
|
|
51
|
-
import { logWordIndex } from "./word-index-logger.js";
|
|
52
|
-
import { resetOpaqueMutationState } from "./opaque-mutation-scan.js";
|
|
53
|
-
import { primePersistedMutationAttribution, resetMutationAttribution, } from "./mutation-attribution.js";
|
|
54
|
-
import { resetObservedMutationNet } from "./observed-mutation.js";
|
|
55
|
-
import { resetPendingAuxiliaryCoverage } from "./lsp/pending-aux-coverage.js";
|
|
56
|
-
import { resetWorkspaceTopology } from "./workspace-topology.js";
|
|
57
|
-
import { resetZizmorTokenAvailability } from "./zizmor-config.js";
|
|
58
|
-
import { resetSpawnTimeoutCooldowns } from "./spawn-timeout-cooldown.js";
|
|
59
|
-
import { resetTestRunnerDelivery } from "./test-runner-delivery.js";
|
|
60
|
-
import { resetLspMutationNoBridgeDbgLatch } from "./lsp-mutation.js";
|
|
61
|
-
/**
|
|
62
|
-
* `deps` with resolved analyzer clients merged in, or `null` when the
|
|
63
|
-
* bootstrap could not be served and the caller must proceed without them.
|
|
64
|
-
*
|
|
65
|
-
* No cast and no "is one representative field present?" probe: the seam
|
|
66
|
-
* answers with all seventeen clients or with `null`, so the two outcomes are
|
|
67
|
-
* the two branches. A skipped consumer is already counted in the ledger under
|
|
68
|
-
* `analyzer-bootstrap-unavailable`, so it is never mistaken for a clean one.
|
|
69
|
-
*/
|
|
70
|
-
async function demandBootstrapDeps(deps, reason) {
|
|
71
|
-
const clients = await deps.bootstrap.request(reason);
|
|
72
|
-
if (!clients)
|
|
73
|
-
return null;
|
|
74
|
-
return { ...deps, ...clients };
|
|
75
|
-
}
|
|
76
|
-
const HOST_STALL_THRESHOLD_MS = 30_000;
|
|
77
|
-
function logHostReadyDelay(deps, cwd) {
|
|
78
|
-
if (!deps.emitHostReadyDelay ||
|
|
79
|
-
deps.sessionStartMonotonicAt === undefined ||
|
|
80
|
-
deps.extensionLoadedAt === undefined) {
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const durationMs = Math.max(0, deps.sessionStartMonotonicAt - deps.extensionLoadedAt);
|
|
84
|
-
logLatency({
|
|
85
|
-
type: "phase",
|
|
86
|
-
filePath: cwd,
|
|
87
|
-
phase: "host_ready_delay",
|
|
88
|
-
durationMs,
|
|
89
|
-
metadata: {
|
|
90
|
-
hostStallSuspected: durationMs > HOST_STALL_THRESHOLD_MS,
|
|
91
|
-
sessionStart: "first-process-session",
|
|
92
|
-
reason: deps.sessionReason,
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
function resolveSnapshotRoot(cwd) {
|
|
97
|
-
const resolvedCwd = path.resolve(cwd);
|
|
98
|
-
const nearest = findNearestProjectRoot(resolvedCwd);
|
|
99
|
-
// Reject a root at — or above — $HOME (the #250/#253 escape); fall back to
|
|
100
|
-
// the cwd so the snapshot stays scoped to the actual workspace.
|
|
101
|
-
if (!nearest || isAtOrAboveHomeDir(nearest)) {
|
|
102
|
-
return resolvedCwd;
|
|
103
|
-
}
|
|
104
|
-
return nearest;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* #947: meta-first staleness gate. Both interactive paths used to sync-parse
|
|
108
|
-
* the whole `project-snapshot.json` body (110-130ms at 40MB, ~0.5s at the
|
|
109
|
-
* observed 112MB) BEFORE checking freshness — wasted work in the 71% of
|
|
110
|
-
* sessions where the snapshot turns out stale. Read the tiny meta sidecar
|
|
111
|
-
* (`project-snapshot.meta.json`, written on every save) first; when it says
|
|
112
|
-
* stale (seq or version mismatch — the exact fields `isProjectSnapshotFresh`
|
|
113
|
-
* checks), skip the body parse entirely. Callers already tolerate a missing
|
|
114
|
-
* snapshot (fail-open contract). A missing meta file (legacy install, or a
|
|
115
|
-
* snapshot written before the sidecar existed) falls through to parsing the
|
|
116
|
-
* body exactly as before.
|
|
117
|
-
*/
|
|
118
|
-
/**
|
|
119
|
-
* #1019: build the bounded-replay base from the snapshot's tiny meta sidecar.
|
|
120
|
-
* The sequence index is mirrored into the meta (not just the body) precisely so
|
|
121
|
-
* this read is cheap — reading it does NOT parse the 40-112MB body, preserving
|
|
122
|
-
* the #947 skip-stale optimization. Returns `undefined` (→ full replay) for a
|
|
123
|
-
* legacy meta with no embedded index, a version-mismatched meta (a foreign
|
|
124
|
-
* snapshot generation), or a missing meta. A seq-stale meta is NOT rejected
|
|
125
|
-
* here — folding the newer entries onto it is the whole point; the base's
|
|
126
|
-
* trustworthiness against a truncated/ahead log is guarded inside
|
|
127
|
-
* `readLatestProjectSequence` itself.
|
|
128
|
-
*/
|
|
129
|
-
function snapshotSequenceBase(root) {
|
|
130
|
-
const meta = readProjectSnapshotMeta(root);
|
|
131
|
-
if (!meta || meta.version !== PROJECT_SNAPSHOT_VERSION)
|
|
132
|
-
return undefined;
|
|
133
|
-
const index = meta.sequenceIndex;
|
|
134
|
-
if (!index)
|
|
135
|
-
return undefined;
|
|
136
|
-
return {
|
|
137
|
-
projectSeq: index.projectSeq,
|
|
138
|
-
fileSeqByPath: index.fileSeqByPath,
|
|
139
|
-
sinceSeq: meta.seq,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* #1785: one bounded ledger record per timed-out sequence read, so a real
|
|
144
|
-
* occurrence is diagnosable from the ledger alone rather than only from a
|
|
145
|
-
* `dbg` line that most hosts never surface. Carries the project root and the
|
|
146
|
-
* on-disk snapshot's age/size (when one exists) — the identity AC1 asks for:
|
|
147
|
-
* which project, how stale/large the snapshot that got skipped was.
|
|
148
|
-
*/
|
|
149
|
-
function recordSnapshotSequenceTimeout(args) {
|
|
150
|
-
let reason = "sequence read timed out; no snapshot on disk";
|
|
151
|
-
try {
|
|
152
|
-
const stat = nodeFs.statSync(args.snapshotPath);
|
|
153
|
-
reason = `sequence read timed out; snapshot age=${Math.round(Date.now() - stat.mtimeMs)}ms size=${stat.size}b`;
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
// No snapshot on disk yet — the reason above already covers it.
|
|
157
|
-
}
|
|
158
|
-
incrementDegradationCount({
|
|
159
|
-
kind: "snapshot-sequence-read-timeout",
|
|
160
|
-
subject: args.snapshotRoot,
|
|
161
|
-
reason,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* #1785: the synchronous freshness check made a conservative call with the
|
|
166
|
-
* `UNKNOWN_PROJECT_SEQ` sentinel (never hydrate when the real sequence is
|
|
167
|
-
* unknown). Once the deferred read resolves — a deterministic completion
|
|
168
|
-
* signal, not a sleep — this re-checks freshness against the now-known
|
|
169
|
-
* sequence and hydrates late when the snapshot really was current, instead of
|
|
170
|
-
* leaving the runtime without `cachedExports`/`projectRulesScan` for the rest
|
|
171
|
-
* of the session.
|
|
172
|
-
*
|
|
173
|
-
* #1785 review round F5 (design (a), replacing rounds 1-3's synchronous
|
|
174
|
-
* capture entirely): quick mode's own cold-start warmup ALREADY loads the
|
|
175
|
-
* on-disk snapshot for its own purposes — `cachedSnapshot` at the top of the
|
|
176
|
-
* warmup body, used to reuse a still-fresh `startupScan` verdict — strictly
|
|
177
|
-
* BEFORE the warmup's own possible save (`saveRuntimeProjectSnapshot`, only
|
|
178
|
-
* reached when that verdict was NOT fresh). Reusing THAT already-loaded
|
|
179
|
-
* value costs nothing new: the read was always going to happen regardless of
|
|
180
|
-
* this fix, and it is guaranteed to predate any overwrite the SAME warmup
|
|
181
|
-
* invocation might go on to make — a read-before-write invariant of the
|
|
182
|
-
* warmup's own code, not something this function has to defend against with
|
|
183
|
-
* a race window of its own. `getWarmupOwnSnapshotRead` is a closure over a
|
|
184
|
-
* per-`handleSessionStart`-call LOCAL variable (never module-scope — #1785
|
|
185
|
-
* F6 is exactly the class of bug a module-scope holder here would
|
|
186
|
-
* reintroduce) that the warmup publishes into once its own read completes.
|
|
187
|
-
*
|
|
188
|
-
* When the getter still returns `undefined` — the deferred read resolved
|
|
189
|
-
* before the warmup got that far (or no warmup was ever armed by this call:
|
|
190
|
-
* a later quick-mode call in the same process, print mode, full mode) —
|
|
191
|
-
* there is nothing published yet, so this falls back to a live re-load via
|
|
192
|
-
* `loadProjectSnapshotExportsAndRules`. That fallback is exactly as safe as
|
|
193
|
-
* the primary path: if the warmup hasn't reached its own read yet, it
|
|
194
|
-
* certainly hasn't saved yet either (same invariant), so disk still holds
|
|
195
|
-
* whatever was there before any warmup activity.
|
|
196
|
-
*
|
|
197
|
-
* Both the published value and the fallback re-load are the narrow,
|
|
198
|
-
* postings-free `ProjectSnapshotExportsAndRules` shape — never the full
|
|
199
|
-
* `loadProjectSnapshot`. This path only ever hydrates
|
|
200
|
-
* `cachedExports`/`projectRulesScan`; it can NOT hydrate `wordIndex` even
|
|
201
|
-
* when the underlying snapshot would otherwise have one, because the narrow
|
|
202
|
-
* type never carries it. That is intentional, not a regression: F2's hazard
|
|
203
|
-
* was this exact path nulling a `wordIndex` that quick mode's warmup had, in
|
|
204
|
-
* the meantime, built for real — the warmup remains the sole source of a
|
|
205
|
-
* late-arriving `wordIndex` for the interactive path.
|
|
206
|
-
*/
|
|
207
|
-
function retroactivelyHydrateAfterDeferredSequence(args) {
|
|
208
|
-
return (latestSeq) => {
|
|
209
|
-
const published = args.getWarmupOwnSnapshotRead();
|
|
210
|
-
const snapshot = published !== undefined
|
|
211
|
-
? published
|
|
212
|
-
: loadProjectSnapshotExportsAndRules(args.snapshotRoot);
|
|
213
|
-
if (!snapshot ||
|
|
214
|
-
snapshot.version !== PROJECT_SNAPSHOT_VERSION ||
|
|
215
|
-
snapshot.seq !== latestSeq.projectSeq) {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
const hydrated = hydrateRuntimeFromProjectSnapshotIfIdle(args.runtime, snapshot);
|
|
219
|
-
if (hydrated) {
|
|
220
|
-
args.dbg(`session_start: deferred sequence read confirmed snapshot freshness — hydrating cachedExports/projectRulesScan late (seq=${snapshot.seq})`);
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
args.dbg(`session_start: deferred sequence read confirmed snapshot freshness (seq=${snapshot.seq}), but the runtime already has live state (warmup or another task got there first) — skipping late hydration to avoid clobbering it`);
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
function loadSnapshotBodyUnlessStale(args) {
|
|
228
|
-
const meta = readProjectSnapshotMeta(args.root);
|
|
229
|
-
if (meta && isProjectSnapshotMetaStale(meta, args.currentProjectSeq)) {
|
|
230
|
-
args.dbg(`project_snapshot: meta gate stale (metaSeq=${meta.seq} metaVersion=${meta.version} current=${args.currentProjectSeq}) — skipping body parse`);
|
|
231
|
-
return { snapshot: null, skippedStale: true };
|
|
232
|
-
}
|
|
233
|
-
return {
|
|
234
|
-
snapshot: loadProjectSnapshot(args.root),
|
|
235
|
-
skippedStale: false,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
function describeSnapshotMiss(snapshot, currentProjectSeq, args) {
|
|
239
|
-
if (args.skippedStale)
|
|
240
|
-
return "stale-meta-gate";
|
|
241
|
-
if (!snapshot)
|
|
242
|
-
return args.bodyPresent ? "invalid-body" : "missing";
|
|
243
|
-
if (snapshot.seq !== currentProjectSeq) {
|
|
244
|
-
return `stale(seq=${snapshot.seq}, current=${currentProjectSeq})`;
|
|
245
|
-
}
|
|
246
|
-
// Defensive-only arm: parseSnapshot rejects incompatible versions, and a
|
|
247
|
-
// same-sequence parsed snapshot is fresh. Keep this classification explicit
|
|
248
|
-
// so a future loader change cannot collapse it into a generic miss.
|
|
249
|
-
return "incompatible";
|
|
250
|
-
}
|
|
251
|
-
function logProjectSnapshotProbe(args) {
|
|
252
|
-
args.dbg(`project_snapshot: probe root=${args.root} path=${getProjectSnapshotPath(args.root)} currentSeq=${args.currentProjectSeq}`);
|
|
253
|
-
if (isProjectSnapshotFresh(args.snapshot, args.currentProjectSeq)) {
|
|
254
|
-
args.dbg(`project_snapshot: loaded seq=${args.snapshot.seq} exports=${args.snapshot.cachedExports.length} files=${Object.keys(args.snapshot.files ?? {}).length} reverseDeps=${Object.keys(args.snapshot.reverseDeps ?? {}).length} startupScan=${Boolean(args.snapshot.startupScan)} languageProfile=${Boolean(args.snapshot.languageProfile)}`);
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
args.dbg(`project_snapshot: miss reason=${args.missReason}`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
function resolveStartupMode(projectConfig, globalConfig) {
|
|
261
|
-
const envMode = (process.env.PI_LENS_STARTUP_MODE ?? "").trim().toLowerCase();
|
|
262
|
-
if (envMode === "full" || envMode === "minimal" || envMode === "quick") {
|
|
263
|
-
return envMode;
|
|
264
|
-
}
|
|
265
|
-
const configMode = projectConfig?.startup?.mode ?? globalConfig?.startup?.mode;
|
|
266
|
-
if (configMode)
|
|
267
|
-
return configMode;
|
|
268
|
-
if (isPrintMode()) {
|
|
269
|
-
return "quick";
|
|
270
|
-
}
|
|
271
|
-
return "full";
|
|
272
|
-
}
|
|
273
|
-
// --- Session-start helpers ---
|
|
274
|
-
// #1162: bound the session_start sequence read to this budget. The default
|
|
275
|
-
// mirrors the issue's ~250ms figure; overridable for tests (mirrors the
|
|
276
|
-
// `PI_LENS_WARMUP_DELAY_MS` precedent above) so a regression can use a tiny
|
|
277
|
-
// budget instead of racing a real 250ms wall-clock wait.
|
|
278
|
-
function sequenceReadBudgetMs() {
|
|
279
|
-
const raw = Number(process.env.PI_LENS_SEQUENCE_READ_BUDGET_MS ?? 250);
|
|
280
|
-
return Number.isFinite(raw) && raw > 0 ? raw : 250;
|
|
281
|
-
}
|
|
282
|
-
// #1162 review follow-up (P3): the sentinel used ONLY for the snapshot
|
|
283
|
-
// freshness check (`isProjectSnapshotFresh`/`isProjectSnapshotMetaStale`,
|
|
284
|
-
// both `=== currentProjectSeq` equality) when a sequence read timed out.
|
|
285
|
-
// Every real `projectSeq`/`snapshot.seq` is >= 0 (both derive from
|
|
286
|
-
// `Math.max(0, ...)`/a monotonic fold starting at 0), so this value can
|
|
287
|
-
// never collide with a legitimate seq — including a project's real
|
|
288
|
-
// first-ever snapshot persisted at `seq === 0`, which the cold-seed's OWN
|
|
289
|
-
// `projectSeq: 0` would otherwise alias. Deliberately kept separate from
|
|
290
|
-
// `runtime.projectSeq` (which a timed-out read still seeds to plain `0`):
|
|
291
|
-
// the reseed-advancement guard on the deferred continuation below needs
|
|
292
|
-
// `runtime.projectSeq` to read exactly 0 immediately after a cold seed so
|
|
293
|
-
// `> 0` reliably means "a bump happened in the stall window", which this
|
|
294
|
-
// sentinel is never fed into.
|
|
295
|
-
const UNKNOWN_PROJECT_SEQ = -1;
|
|
296
|
-
/**
|
|
297
|
-
* Bound the sequence read that gates snapshot freshness (#1162). Normally
|
|
298
|
-
* ~2ms, but the underlying read is a blocking `fs.readFileSync` that can
|
|
299
|
-
* balloon under host I/O pressure (2125ms observed) with NO escape hatch —
|
|
300
|
-
* and a `setTimeout`/`Promise.race` timeout can never preempt a *synchronous*
|
|
301
|
-
* read (the thread only returns to the event loop once the OS call
|
|
302
|
-
* returns). The fix is to read asynchronously (`readLatestProjectSequenceAsync`,
|
|
303
|
-
* which uses `fs.promises.readFile` and genuinely yields) so a timeout CAN
|
|
304
|
-
* win the race.
|
|
305
|
-
*
|
|
306
|
-
* On the healthy path (the overwhelming common case) the async read settles
|
|
307
|
-
* first and this adds ~zero overhead beyond one microtask hop. On a stalled
|
|
308
|
-
* read, the timeout wins: the caller proceeds immediately with a cold
|
|
309
|
-
* sequence (`{ projectSeq: 0, fileSeqByPath: empty }` — byte-identical to
|
|
310
|
-
* what a full replay of a missing/empty log already produces, so this is
|
|
311
|
-
* exactly the existing cold-start case, not a new code path) while the real
|
|
312
|
-
* read keeps running in the background. When it resolves, it re-seeds the
|
|
313
|
-
* runtime so the warm-start optimization is recovered for the NEXT
|
|
314
|
-
* session_start/turn instead of lost for the rest of the process — unless
|
|
315
|
-
* the session has since moved on (`runtime.isCurrentSession`) or this is a
|
|
316
|
-
* one-shot `pi --print` process (`isPrintMode()`), which has no future
|
|
317
|
-
* session in-process to benefit and where letting background work outlive
|
|
318
|
-
* settle is exactly the referenced-handle retention class #1154/#1153 just
|
|
319
|
-
* closed. The timeout timer itself is `.unref()`'d and always cleared on
|
|
320
|
-
* both race outcomes, so it never holds the process open either.
|
|
321
|
-
*
|
|
322
|
-
* Never silent (shape 10): the caller's `session_start_sequence_read`
|
|
323
|
-
* latency line always carries `timedOut`, and a successful deferred reseed
|
|
324
|
-
* logs its own `session_start_sequence_read_deferred_reseed` phase.
|
|
325
|
-
*/
|
|
326
|
-
async function readSequenceWithBudget(args) {
|
|
327
|
-
const { snapshotRoot, base, cwd, runtime, sessionGeneration, dbg, onDeferredSequenceResolved, } = args;
|
|
328
|
-
const readPromise = readLatestProjectSequenceAsync(snapshotRoot, base);
|
|
329
|
-
let timeoutHandle;
|
|
330
|
-
const timeoutPromise = new Promise((resolve) => {
|
|
331
|
-
timeoutHandle = setTimeout(() => resolve({ timedOut: true }), sequenceReadBudgetMs());
|
|
332
|
-
timeoutHandle.unref();
|
|
333
|
-
});
|
|
334
|
-
const raced = await Promise.race([
|
|
335
|
-
readPromise.then((latestSeq) => ({ timedOut: false, latestSeq })),
|
|
336
|
-
timeoutPromise,
|
|
337
|
-
]);
|
|
338
|
-
if (timeoutHandle)
|
|
339
|
-
clearTimeout(timeoutHandle);
|
|
340
|
-
if (!raced.timedOut) {
|
|
341
|
-
return { latestSeq: raced.latestSeq, timedOut: false };
|
|
342
|
-
}
|
|
343
|
-
dbg(`session_start: sequence read exceeded ${sequenceReadBudgetMs()}ms budget — falling back to cold-start sequence`);
|
|
344
|
-
if (!isPrintMode()) {
|
|
345
|
-
readPromise
|
|
346
|
-
.then((latestSeq) => {
|
|
347
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
348
|
-
return;
|
|
349
|
-
// #1162 review follow-up (P3): `isCurrentSession` alone catches a
|
|
350
|
-
// CROSS-session move-on but not a SAME-session advancement in the
|
|
351
|
-
// stall window. Interleaving: timeout -> cold seed sets
|
|
352
|
-
// projectSeq=0 -> handler returns -> agent edits file X ->
|
|
353
|
-
// `bumpFileSeq(X)` advances `_projectSeq`/`_fileSeq[X]` -> this
|
|
354
|
-
// deferred read completes. Reseeding here would blindly `.clear()`
|
|
355
|
-
// and repopulate `_fileSeq`/`_fileLastProjectSeq` from the STALE
|
|
356
|
-
// pre-edit snapshot, erasing the in-window bump for file X (a
|
|
357
|
-
// transient inconsistency that self-heals into a full sweep per
|
|
358
|
-
// `seedProjectSequence`'s empty-changed-map invariant — safe, but
|
|
359
|
-
// worth not doing). The cold seed always sets `projectSeq` to
|
|
360
|
-
// exactly 0, so `runtime.projectSeq > 0` here can ONLY be true if a
|
|
361
|
-
// `bumpFileSeq` ran since — i.e. an in-window edit — never a false
|
|
362
|
-
// positive from a legitimately-empty log (which leaves it at 0, and
|
|
363
|
-
// the reseed below is then still wanted: it recovers the real,
|
|
364
|
-
// possibly non-empty, result that arrived too late for the budget).
|
|
365
|
-
if (runtime.projectSeq > 0) {
|
|
366
|
-
dbg("session_start: deferred sequence read completed, but the session already advanced (in-window edit) — skipping reseed to avoid clobbering it");
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
const reseedStartedAt = Date.now();
|
|
370
|
-
runtime.seedProjectSequence?.(latestSeq.projectSeq, latestSeq.fileSeqByPath);
|
|
371
|
-
logLatency({
|
|
372
|
-
type: "phase",
|
|
373
|
-
phase: "session_start_sequence_read_deferred_reseed",
|
|
374
|
-
filePath: cwd,
|
|
375
|
-
startedAt: new Date(reseedStartedAt).toISOString(),
|
|
376
|
-
durationMs: Date.now() - reseedStartedAt,
|
|
377
|
-
metadata: { entries: latestSeq.fileSeqByPath.size, deferred: true },
|
|
378
|
-
});
|
|
379
|
-
dbg(`session_start: deferred sequence read completed — reseeded projectSeq=${latestSeq.projectSeq} fileSeqEntries=${latestSeq.fileSeqByPath.size}`);
|
|
380
|
-
onDeferredSequenceResolved?.(latestSeq);
|
|
381
|
-
})
|
|
382
|
-
.catch((err) => {
|
|
383
|
-
dbg(`session_start: deferred sequence read failed: ${err}`);
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
return {
|
|
387
|
-
latestSeq: { projectSeq: 0, fileSeqByPath: new Map() },
|
|
388
|
-
timedOut: true,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
async function igniteWarmFiles(cwd, warmFiles, runtime, sessionGeneration, dbg) {
|
|
392
|
-
try {
|
|
393
|
-
dbg(`session_start lsp-warm: ${warmFiles.length} warm file(s) configured`);
|
|
394
|
-
await initLSPConfig(cwd);
|
|
395
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
396
|
-
return;
|
|
397
|
-
const lspService = (await loadLspService()).getLSPService();
|
|
398
|
-
const total = warmFiles.length;
|
|
399
|
-
let loaded = 0;
|
|
400
|
-
let errors = 0;
|
|
401
|
-
for (const relPath of warmFiles) {
|
|
402
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
403
|
-
return;
|
|
404
|
-
const filePath = path.isAbsolute(relPath)
|
|
405
|
-
? relPath
|
|
406
|
-
: path.resolve(cwd, relPath);
|
|
407
|
-
if (!nodeFs.existsSync(filePath)) {
|
|
408
|
-
dbg(`session_start lsp-warm: not found: ${relPath}`);
|
|
409
|
-
errors++;
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
try {
|
|
413
|
-
const content = nodeFs.readFileSync(filePath, "utf-8");
|
|
414
|
-
await lspService.touchFile(filePath, content, {
|
|
415
|
-
diagnostics: "none",
|
|
416
|
-
source: "startup-warm",
|
|
417
|
-
clientScope: "primary",
|
|
418
|
-
maxClientWaitMs: 2000,
|
|
419
|
-
});
|
|
420
|
-
loaded++;
|
|
421
|
-
}
|
|
422
|
-
catch (err) {
|
|
423
|
-
dbg(`session_start lsp-warm: error ${relPath}: ${err}`);
|
|
424
|
-
errors++;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
dbg(`session_start lsp-warm: ${loaded}/${total} opened (${errors} err)`);
|
|
428
|
-
}
|
|
429
|
-
catch (err) {
|
|
430
|
-
dbg(`session_start lsp-warm: config/init error: ${err}`);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* Fallback warm when a project has no explicit `warmFiles`: pre-spawn the LSP
|
|
435
|
-
* for the project's *dominant* language (highest source-file count) by opening
|
|
436
|
-
* one representative file. This eliminates the cold-spawn stall the first edit
|
|
437
|
-
* would otherwise pay (`lsp_client_wait_timeout`, observed up to 5s on
|
|
438
|
-
* TypeScript/Deno). Only a single server is warmed — launching every detected
|
|
439
|
-
* language's server at once (rust-analyzer + gopls + tsserver …) would spike CPU
|
|
440
|
-
* and the event loop at startup, working against the very latency we protect.
|
|
441
|
-
* Projects that want more pre-warming can list explicit `warmFiles` (#203).
|
|
442
|
-
*/
|
|
443
|
-
async function igniteDominantLanguageWarm(cwd, runtime, sessionGeneration, dbg) {
|
|
444
|
-
try {
|
|
445
|
-
await initLSPConfig(cwd);
|
|
446
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
447
|
-
return;
|
|
448
|
-
const lspService = (await loadLspService()).getLSPService();
|
|
449
|
-
const { collectSourceFilesAsync } = await import("./source-filter.js");
|
|
450
|
-
const { CODE_KINDS, detectFileKind } = await import("./file-kinds.js");
|
|
451
|
-
// Async, event-loop-yielding walk (deferred off the interactive path).
|
|
452
|
-
// inspectGeneratedHeaders:false keeps the walk to directory reads only — no
|
|
453
|
-
// per-file content opens — so we never hold a file handle (cheaper, and it
|
|
454
|
-
// can't collide with concurrent fs teardown). Picking a representative file
|
|
455
|
-
// doesn't need generated-banner filtering.
|
|
456
|
-
const files = await collectSourceFilesAsync(cwd, {
|
|
457
|
-
inspectGeneratedHeaders: false,
|
|
458
|
-
});
|
|
459
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
460
|
-
return;
|
|
461
|
-
// Rank languages by source-file count. Computed here from the scan rather
|
|
462
|
-
// than reused from languageProfile.counts, which is left empty on the
|
|
463
|
-
// no-warm-caches startup path (detectProjectLanguageProfile is called with
|
|
464
|
-
// an empty file list there).
|
|
465
|
-
const counts = new Map();
|
|
466
|
-
for (const f of files) {
|
|
467
|
-
const kind = detectFileKind(f);
|
|
468
|
-
if (kind)
|
|
469
|
-
counts.set(kind, (counts.get(kind) ?? 0) + 1);
|
|
470
|
-
}
|
|
471
|
-
if (counts.size === 0) {
|
|
472
|
-
dbg("session_start lsp-warm: no detected languages to auto-warm");
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
476
|
-
// #894 review: data/doc kinds (json/yaml/markdown/…) have builtin LSP
|
|
477
|
-
// servers too, and broadened enumeration makes them countable — so a TS
|
|
478
|
-
// repo with more .json/.yml than .ts files would otherwise warm the
|
|
479
|
-
// json/yaml server while tsserver still pays the ~5s cold-spawn stall
|
|
480
|
-
// this warm exists to prevent (#203). Rank CODE_KINDS first; non-code
|
|
481
|
-
// kinds stay as a fallback so a pure-config/docs repo still warms its
|
|
482
|
-
// dominant server.
|
|
483
|
-
const warmOrder = [
|
|
484
|
-
...ranked.filter(([kind]) => CODE_KINDS.has(kind)),
|
|
485
|
-
...ranked.filter(([kind]) => !CODE_KINDS.has(kind)),
|
|
486
|
-
];
|
|
487
|
-
// Walk languages by descending count; warm the first that has both an LSP
|
|
488
|
-
// server and a representative on-disk file.
|
|
489
|
-
for (const [kind] of warmOrder) {
|
|
490
|
-
const sample = files.find((f) => detectFileKind(f) === kind && lspService.supportsLSP(f));
|
|
491
|
-
if (!sample)
|
|
492
|
-
continue;
|
|
493
|
-
const content = await nodeFs.promises.readFile(sample, "utf-8");
|
|
494
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
495
|
-
return;
|
|
496
|
-
await lspService.touchFile(sample, content, {
|
|
497
|
-
diagnostics: "none",
|
|
498
|
-
source: "startup-warm-dominant",
|
|
499
|
-
clientScope: "primary",
|
|
500
|
-
maxClientWaitMs: 2000,
|
|
501
|
-
});
|
|
502
|
-
dbg(`session_start lsp-warm: dominant=${kind} via ${path.basename(sample)}`);
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
dbg("session_start lsp-warm: no dominant-language LSP file found to warm");
|
|
506
|
-
}
|
|
507
|
-
catch (err) {
|
|
508
|
-
dbg(`session_start lsp-warm: dominant warm error: ${err}`);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
function firePreinstallDefaults(ensureTool, dbg, startupDefaults) {
|
|
512
|
-
for (const tool of startupDefaults) {
|
|
513
|
-
const startedAt = Date.now();
|
|
514
|
-
dbg(`session_start preinstall ${tool}: start`);
|
|
515
|
-
ensureTool(tool)
|
|
516
|
-
.then((toolPath) => {
|
|
517
|
-
if (toolPath) {
|
|
518
|
-
dbg(`session_start: ${tool} ready at ${toolPath}`);
|
|
519
|
-
dbg(`session_start preinstall ${tool}: success (${Date.now() - startedAt}ms)`);
|
|
520
|
-
}
|
|
521
|
-
else {
|
|
522
|
-
dbg(`session_start: ${tool} installation unavailable`);
|
|
523
|
-
dbg(`session_start preinstall ${tool}: unavailable (${Date.now() - startedAt}ms)`);
|
|
524
|
-
}
|
|
525
|
-
})
|
|
526
|
-
.catch((err) => {
|
|
527
|
-
dbg(`session_start: ${tool} pre-install error: ${err}`);
|
|
528
|
-
dbg(`session_start preinstall ${tool}: error (${Date.now() - startedAt}ms)`);
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
|
-
* Default delay before the managed-tool version refresh runs (#1730). Long
|
|
534
|
-
* enough that `session_start`, the startup scans, and the tool preinstalls have
|
|
535
|
-
* all settled — the refresh may spawn one `npm update`, and that must never
|
|
536
|
-
* compete with the work the user is waiting on.
|
|
537
|
-
*/
|
|
538
|
-
const MANAGED_TOOL_REFRESH_DELAY_MS = 30_000;
|
|
539
|
-
/**
|
|
540
|
-
* Schedule the periodic managed-tool refresh (#1730) on an unref'd background
|
|
541
|
-
* timer.
|
|
542
|
-
*
|
|
543
|
-
* `session_start`'s own cost here is a single `setTimeout` registration: the
|
|
544
|
-
* refresh module is not even imported until the timer fires. The timer is
|
|
545
|
-
* unref'd so a one-shot process exits without waiting for it, matching the
|
|
546
|
-
* warmup timer above. Whether a refresh actually runs is decided inside
|
|
547
|
-
* `runManagedToolRefresh` against a PERSISTED per-tool stamp, so a launch loop
|
|
548
|
-
* cannot escalate the weekly cadence.
|
|
549
|
-
*/
|
|
550
|
-
function scheduleManagedToolRefresh(dbg) {
|
|
551
|
-
const delayMs = Number(process.env.PI_LENS_TOOL_REFRESH_DELAY_MS ?? MANAGED_TOOL_REFRESH_DELAY_MS);
|
|
552
|
-
const timer = setTimeout(() => {
|
|
553
|
-
void (async () => {
|
|
554
|
-
try {
|
|
555
|
-
const refresh = await import("./installer/managed-tool-refresh.js");
|
|
556
|
-
const outcome = await refresh.runManagedToolRefresh();
|
|
557
|
-
if (outcome.skipped) {
|
|
558
|
-
dbg(`session_start tool-refresh: skipped (${outcome.skipped})`);
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
for (const result of outcome.refreshed) {
|
|
562
|
-
dbg(`session_start tool-refresh: ${result.toolId} ${result.ok ? "ok" : "failed"}` +
|
|
563
|
-
`${result.changed ? ` ${result.previousVersion ?? "unknown"} → ${result.currentVersion}` : " (unchanged)"}`);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
catch (err) {
|
|
567
|
-
dbg(`session_start tool-refresh: error ${err}`);
|
|
568
|
-
}
|
|
569
|
-
})();
|
|
570
|
-
}, Number.isFinite(delayMs) ? delayMs : MANAGED_TOOL_REFRESH_DELAY_MS);
|
|
571
|
-
timer.unref?.();
|
|
572
|
-
}
|
|
573
|
-
async function probePrettierInstall(ensureTool, dbg, analysisRoot) {
|
|
574
|
-
const pkgPath = path.join(analysisRoot, "package.json");
|
|
575
|
-
try {
|
|
576
|
-
const raw = await nodeFs.promises.readFile(pkgPath, "utf-8");
|
|
577
|
-
const pkg = JSON.parse(raw);
|
|
578
|
-
const usesPrettier = !!pkg.devDependencies?.prettier ||
|
|
579
|
-
!!pkg.dependencies?.prettier ||
|
|
580
|
-
pkg.prettier !== undefined;
|
|
581
|
-
if (usesPrettier) {
|
|
582
|
-
dbg("session_start: project uses prettier, ensuring install...");
|
|
583
|
-
ensureTool("prettier")
|
|
584
|
-
.then((p) => {
|
|
585
|
-
if (p)
|
|
586
|
-
dbg(`session_start: prettier ready at ${p}`);
|
|
587
|
-
else
|
|
588
|
-
dbg("session_start: prettier install failed silently");
|
|
589
|
-
})
|
|
590
|
-
.catch((err) => dbg(`session_start: prettier install error: ${err}`));
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
catch {
|
|
594
|
-
// no package.json at cwd root
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
/** Scan one file via the per-file API, pushing any items. Tolerates an
|
|
598
|
-
* unreadable file and a scanner without `scanFile` (no-op). */
|
|
599
|
-
function scanOneTodoFile(scanner, filePath, items) {
|
|
600
|
-
if (typeof scanner.scanFile !== "function")
|
|
601
|
-
return;
|
|
602
|
-
try {
|
|
603
|
-
// scanFile returns TodoItem[] directly (not a { items } result).
|
|
604
|
-
const result = scanner.scanFile(filePath);
|
|
605
|
-
if (Array.isArray(result))
|
|
606
|
-
items.push(...result);
|
|
607
|
-
}
|
|
608
|
-
catch {
|
|
609
|
-
// Per-file error: skip and continue (matches scanDirectory's tolerance).
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
/** Collect the TODO baseline without blocking: enumerate source files and scan
|
|
613
|
-
* them per-file, yielding to the event loop every 30 files. Falls back to the
|
|
614
|
-
* blocking `scanDirectory` if the chunked path can't run (import error or a
|
|
615
|
-
* scanner without `scanFile`). */
|
|
616
|
-
async function collectTodoBaselineItems(scanner, analysisRoot, stillCurrent) {
|
|
617
|
-
const items = [];
|
|
618
|
-
try {
|
|
619
|
-
const { getSourceFilesAsync } = await import("./scan-utils.js");
|
|
620
|
-
// Enumerate with the chunked-yield walker so the file collection itself
|
|
621
|
-
// (the previously-synchronous ~1.5s burst on a 2k-file tree) no longer
|
|
622
|
-
// blocks the event loop before the per-file scan loop below even starts.
|
|
623
|
-
const files = await getSourceFilesAsync(analysisRoot, true);
|
|
624
|
-
if (!stillCurrent())
|
|
625
|
-
return items;
|
|
626
|
-
const deadline = createDeadline(8);
|
|
627
|
-
for (const file of files) {
|
|
628
|
-
if (!stillCurrent())
|
|
629
|
-
return items;
|
|
630
|
-
scanOneTodoFile(scanner, file, items);
|
|
631
|
-
// scanFile cost scales with the file contents, so a count cadence cannot
|
|
632
|
-
// bound the event-loop block across differently-sized corpora.
|
|
633
|
-
await yieldIfOverBudget(deadline);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
catch {
|
|
637
|
-
const todoResult = scanner.scanDirectory(analysisRoot);
|
|
638
|
-
items.push(...todoResult.items);
|
|
639
|
-
}
|
|
640
|
-
return items;
|
|
641
|
-
}
|
|
642
|
-
async function buildOrRefreshWordIndex(args) {
|
|
643
|
-
const { runtime, sessionGeneration, analysisRoot, snapshotRoot, dbg } = args;
|
|
644
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
645
|
-
return;
|
|
646
|
-
const startMs = Date.now();
|
|
647
|
-
let rebuildPreflightFiles;
|
|
648
|
-
const latestSeq = readLatestProjectSequence(snapshotRoot, snapshotSequenceBase(snapshotRoot));
|
|
649
|
-
const effectiveSeq = runtime.projectSeq ?? latestSeq.projectSeq;
|
|
650
|
-
const snapshotLoadStartMs = Date.now();
|
|
651
|
-
const snapshot = loadProjectSnapshot(snapshotRoot);
|
|
652
|
-
const snapshotLoadMs = Date.now() - snapshotLoadStartMs;
|
|
653
|
-
if (snapshot?.wordIndex) {
|
|
654
|
-
const { deserializeWordIndex, refreshWordIndexIncrementally, countWordIndexPostingEntries, estimateWordIndexResidentBytes, } = await import("./word-index.js");
|
|
655
|
-
const deserializeStartMs = Date.now();
|
|
656
|
-
const index = deserializeWordIndex(snapshot.wordIndex);
|
|
657
|
-
const deserializeMs = Date.now() - deserializeStartMs;
|
|
658
|
-
if (index) {
|
|
659
|
-
try {
|
|
660
|
-
const result = await refreshWordIndexIncrementally(index, analysisRoot, () => runtime.isCurrentSession(sessionGeneration));
|
|
661
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
662
|
-
return;
|
|
663
|
-
if (result.mode === "full-required") {
|
|
664
|
-
rebuildPreflightFiles = result.preflightFiles;
|
|
665
|
-
dbg(`session_start word-index: incremental preflight selected full rebuild (${result.reason})`);
|
|
666
|
-
logWordIndex({
|
|
667
|
-
phase: "incremental_fallback",
|
|
668
|
-
cwd: snapshotRoot,
|
|
669
|
-
trigger: "session_start",
|
|
670
|
-
reason: result.reason,
|
|
671
|
-
phaseDurationsMs: {
|
|
672
|
-
snapshotLoadMs,
|
|
673
|
-
deserializeMs,
|
|
674
|
-
...result.timings,
|
|
675
|
-
},
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
else {
|
|
679
|
-
runtime.wordIndex = index;
|
|
680
|
-
let snapshotSaveSyncMs = 0;
|
|
681
|
-
// A stale project seq must be advanced even when mtimes prove every
|
|
682
|
-
// indexed document reusable. Fresh snapshots with no changes avoid
|
|
683
|
-
// an unnecessary rewrite of the large shared snapshot.
|
|
684
|
-
if (!isProjectSnapshotFresh(snapshot, effectiveSeq) ||
|
|
685
|
-
result.refreshed > 0 ||
|
|
686
|
-
result.dropped > 0 ||
|
|
687
|
-
snapshot.wordIndex.truncated !== index.truncated) {
|
|
688
|
-
const serializeSaveEnqueueStartMs = Date.now();
|
|
689
|
-
saveRuntimeProjectSnapshot({ cwd: snapshotRoot, runtime, dbg });
|
|
690
|
-
snapshotSaveSyncMs = Date.now() - serializeSaveEnqueueStartMs;
|
|
691
|
-
}
|
|
692
|
-
const phases = `snapshot-load=${snapshotLoadMs}ms, deserialize=${deserializeMs}ms, ` +
|
|
693
|
-
`source-walk=${result.timings.sourceWalkMs}ms, stat-walk=${result.timings.statWalkMs}ms, ` +
|
|
694
|
-
`refresh-reads=${result.timings.refreshReadsMs}ms, ` +
|
|
695
|
-
`snapshot-save-sync=${snapshotSaveSyncMs}ms`;
|
|
696
|
-
dbg(`session_start word-index: incremental (seq=${effectiveSeq}, refreshed=${result.refreshed}, dropped=${result.dropped}, skipped=${result.skipped}, reused=${result.reused}, ${Date.now() - startMs}ms, phases: ${phases})`);
|
|
697
|
-
// M2, #958: the incremental-vs-full decision + honest coverage
|
|
698
|
-
// (indexedFileCount/truncated/skipped), independent of host `dbg`.
|
|
699
|
-
logWordIndex({
|
|
700
|
-
phase: "incremental_refresh",
|
|
701
|
-
cwd: snapshotRoot,
|
|
702
|
-
trigger: "session_start",
|
|
703
|
-
durationMs: Date.now() - startMs,
|
|
704
|
-
indexedFileCount: index.docCount,
|
|
705
|
-
tokens: index.postings.size,
|
|
706
|
-
postingEntries: countWordIndexPostingEntries(index),
|
|
707
|
-
residentBytes: estimateWordIndexResidentBytes(index),
|
|
708
|
-
truncated: index.truncated,
|
|
709
|
-
phaseDurationsMs: {
|
|
710
|
-
snapshotLoadMs,
|
|
711
|
-
deserializeMs,
|
|
712
|
-
...result.timings,
|
|
713
|
-
snapshotSaveSyncMs,
|
|
714
|
-
},
|
|
715
|
-
refreshed: result.refreshed,
|
|
716
|
-
dropped: result.dropped,
|
|
717
|
-
skipped: result.skipped,
|
|
718
|
-
reused: result.reused,
|
|
719
|
-
});
|
|
720
|
-
return result;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
catch (err) {
|
|
724
|
-
// Supersession is an expected transition, not a failure: a newer
|
|
725
|
-
// session_start bumped the generation mid-refresh. Return undefined the
|
|
726
|
-
// way master's synchronous path did instead of reporting a fallback and
|
|
727
|
-
// re-walking for a rebuild whose result this generation may not publish.
|
|
728
|
-
if (!runtime.isCurrentSession(sessionGeneration)) {
|
|
729
|
-
dbg("session_start word-index: incremental refresh superseded");
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
dbg(`session_start word-index: incremental refresh failed; falling back to full rebuild (${err})`);
|
|
733
|
-
logWordIndex({
|
|
734
|
-
phase: "incremental_fallback",
|
|
735
|
-
cwd: snapshotRoot,
|
|
736
|
-
trigger: "session_start",
|
|
737
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
738
|
-
phaseDurationsMs: { snapshotLoadMs, deserializeMs },
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
// Shared file-walk-and-read helper (#348): the ONE collectWordIndexDocs
|
|
744
|
-
// implementation backs this task, the quick-mode warmup call below, AND the
|
|
745
|
-
// stateless cold-query background trigger in word-index.ts — a bound/skip
|
|
746
|
-
// -rule change lands once, not in three copies.
|
|
747
|
-
const { buildWordIndexAsync, collectWordIndexDocs, countWordIndexPostingEntries, estimateWordIndexResidentBytes, } = await import("./word-index.js");
|
|
748
|
-
const docs = await collectWordIndexDocs(analysisRoot, () => runtime.isCurrentSession(sessionGeneration), rebuildPreflightFiles);
|
|
749
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
750
|
-
return;
|
|
751
|
-
// #1197 review finding 2: `buildWordIndexAsync` THROWS on supersession, and
|
|
752
|
-
// the quick-mode warmup caller (below) awaits this function OUTSIDE any
|
|
753
|
-
// per-step try/catch — an escaping throw there skips the rest of warmup,
|
|
754
|
-
// including the #947 LSP pre-warm, and resets `__piLensWarmupScheduled`.
|
|
755
|
-
// Supersession is an expected transition (master's synchronous path just
|
|
756
|
-
// returned), so absorb it here and let the caller continue.
|
|
757
|
-
let rebuiltIndex;
|
|
758
|
-
try {
|
|
759
|
-
rebuiltIndex = await buildWordIndexAsync(docs, () => runtime.isCurrentSession(sessionGeneration));
|
|
760
|
-
}
|
|
761
|
-
catch (err) {
|
|
762
|
-
if (!runtime.isCurrentSession(sessionGeneration)) {
|
|
763
|
-
dbg("session_start word-index: rebuild superseded");
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
throw err;
|
|
767
|
-
}
|
|
768
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
769
|
-
return;
|
|
770
|
-
runtime.wordIndex = rebuiltIndex;
|
|
771
|
-
saveRuntimeProjectSnapshot({ cwd: snapshotRoot, runtime, dbg });
|
|
772
|
-
dbg(`session_start word-index: rebuilt (absent/stale, seq=${effectiveSeq}) ` +
|
|
773
|
-
`${runtime.wordIndex.docCount} files, ${runtime.wordIndex.postings.size} tokens (${Date.now() - startMs}ms)`);
|
|
774
|
-
// M2, #958: full-rebuild decision + honest coverage. `docs.skipped` (L1)
|
|
775
|
-
// counts files the walk saw but could not index (unreadable / over the byte
|
|
776
|
-
// cap) so a partial index is never reported as complete (#533).
|
|
777
|
-
logWordIndex({
|
|
778
|
-
phase: "full_rebuild",
|
|
779
|
-
cwd: snapshotRoot,
|
|
780
|
-
trigger: "session_start",
|
|
781
|
-
durationMs: Date.now() - startMs,
|
|
782
|
-
indexedFileCount: runtime.wordIndex.docCount,
|
|
783
|
-
tokens: runtime.wordIndex.postings.size,
|
|
784
|
-
postingEntries: countWordIndexPostingEntries(runtime.wordIndex),
|
|
785
|
-
residentBytes: estimateWordIndexResidentBytes(runtime.wordIndex),
|
|
786
|
-
truncated: runtime.wordIndex.truncated,
|
|
787
|
-
skipped: docs.skipped,
|
|
788
|
-
});
|
|
789
|
-
return {
|
|
790
|
-
mode: "full",
|
|
791
|
-
refreshed: runtime.wordIndex.docCount,
|
|
792
|
-
dropped: 0,
|
|
793
|
-
skipped: docs.skipped,
|
|
794
|
-
reused: 0,
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
// Fire off heavy scans as background tasks — don't block session start.
|
|
798
|
-
// Each consumer already handles the "not ready yet" case gracefully
|
|
799
|
-
// (cachedExports.size > 0, cache miss paths).
|
|
800
|
-
function scheduleStartupScans(deps, runtime, sessionGeneration, analysisRoot, snapshotRoot, languageProfile, dbg) {
|
|
801
|
-
// #2467: the analyzers these scans drive are loaded HERE, on the deferred
|
|
802
|
-
// background path, instead of before the handler ran. This function was
|
|
803
|
-
// already fire-and-forget ("don't block session start"), so resolving them
|
|
804
|
-
// across one await adds no wait to the interactive path and changes no
|
|
805
|
-
// ordering its caller depends on — the caller never awaited it.
|
|
806
|
-
void (async () => {
|
|
807
|
-
const resolved = await demandBootstrapDeps(deps, "session-start-scans");
|
|
808
|
-
// Fail open: no analyzers means no scans, said out loud. The demand is
|
|
809
|
-
// already counted in the ledger, so this is not a silent skip.
|
|
810
|
-
if (!resolved) {
|
|
811
|
-
dbg("session_start scans: analyzer bootstrap unavailable — skipped");
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
scheduleStartupScansWithClients(resolved, runtime, sessionGeneration, analysisRoot, snapshotRoot, languageProfile, dbg);
|
|
815
|
-
})();
|
|
816
|
-
}
|
|
817
|
-
/** The scan bodies, once the analyzers they need are resolved. */
|
|
818
|
-
function scheduleStartupScansWithClients(deps, runtime, sessionGeneration, analysisRoot, snapshotRoot, languageProfile, dbg) {
|
|
819
|
-
const { todoScanner, cacheManager, knipClient, jscpdClient, deadCodeClients, govulncheckClient, gitleaksClient, trivyClient, opengrepClient, astGrepClient, depChecker, } = deps;
|
|
820
|
-
const analyzerEnabled = (flag) => !deps.getFlag(flag);
|
|
821
|
-
if (!analyzerEnabled("no-complexity")) {
|
|
822
|
-
dbg("session_start complexity: skipped (disabled by config)");
|
|
823
|
-
recordDegradationOnce({
|
|
824
|
-
kind: "startup-analyzer-disabled",
|
|
825
|
-
subject: "complexity",
|
|
826
|
-
reason: "skipped (disabled by config)",
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
// Some background scans are CPU-heavy and arrive on the event loop
|
|
830
|
-
// just as the user is most likely typing (right after /new). Defer
|
|
831
|
-
// those by a few seconds so the perceptible 50-100ms sync bursts they
|
|
832
|
-
// contain land during LLM streaming idle time instead. All other
|
|
833
|
-
// tasks (those not listed here) run on the next `setImmediate` tick
|
|
834
|
-
// as before. The delays are deliberately staggered (200ms apart) so
|
|
835
|
-
// two heavy tasks don't both run on the same macrotask.
|
|
836
|
-
const taskDeferMsByName = {
|
|
837
|
-
"call-graph": 5000,
|
|
838
|
-
"codebase-model": 5200,
|
|
839
|
-
"ast-grep exports": 5400,
|
|
840
|
-
"project index": 5400,
|
|
841
|
-
};
|
|
842
|
-
const runTask = (name, task) => {
|
|
843
|
-
const queuedAt = Date.now();
|
|
844
|
-
dbg(`session_start task ${name}: scheduled`);
|
|
845
|
-
runtime.markStartupScanInFlight(name, sessionGeneration);
|
|
846
|
-
const completion = new Promise((resolve) => {
|
|
847
|
-
const fire = () => {
|
|
848
|
-
const startedAt = Date.now();
|
|
849
|
-
dbg(`session_start task ${name}: start queuedMs=${startedAt - queuedAt}`);
|
|
850
|
-
void task()
|
|
851
|
-
.then(() => {
|
|
852
|
-
dbg(`session_start task ${name}: success runMs=${Date.now() - startedAt} queuedMs=${startedAt - queuedAt}`);
|
|
853
|
-
})
|
|
854
|
-
.catch((err) => {
|
|
855
|
-
dbg(`session_start: ${name} background scan failed: ${err}`);
|
|
856
|
-
dbg(`session_start task ${name}: failed runMs=${Date.now() - startedAt} queuedMs=${startedAt - queuedAt}`);
|
|
857
|
-
})
|
|
858
|
-
.finally(() => {
|
|
859
|
-
runtime.clearStartupScanInFlight(name, sessionGeneration);
|
|
860
|
-
dbg(`session_start task ${name}: end`);
|
|
861
|
-
resolve();
|
|
862
|
-
});
|
|
863
|
-
};
|
|
864
|
-
const delay = taskDeferMsByName[name] ?? 0;
|
|
865
|
-
if (delay > 0) {
|
|
866
|
-
// #1154: unref to match the repo-wide convention that every
|
|
867
|
-
// background timer is `.unref()`'d. Full-mode deferred scans only
|
|
868
|
-
// run in long-lived/interactive sessions today (never a one-shot),
|
|
869
|
-
// so this is defensive — but a referenced 5s timer would keep a
|
|
870
|
-
// process open were a full-mode one-shot ever introduced.
|
|
871
|
-
setTimeout(fire, delay).unref?.();
|
|
872
|
-
}
|
|
873
|
-
else {
|
|
874
|
-
setImmediate(fire);
|
|
875
|
-
}
|
|
876
|
-
});
|
|
877
|
-
return completion;
|
|
878
|
-
};
|
|
879
|
-
const canRunJsTsHeavyScans = canRunStartupHeavyScans(languageProfile, "jsts");
|
|
880
|
-
const scanNames = ["todo", "dead-code"];
|
|
881
|
-
if (canRunJsTsHeavyScans) {
|
|
882
|
-
scanNames.push("knip", "jscpd", "ast-grep exports", "project index");
|
|
883
|
-
}
|
|
884
|
-
dbg(`session_start: launching background scans (${scanNames.join(", ")})`);
|
|
885
|
-
void runTask("todo", async () => {
|
|
886
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
887
|
-
return;
|
|
888
|
-
// The original implementation called todoScanner.scanDirectory(), which
|
|
889
|
-
// walks the project synchronously and freezes the TUI for ~3s on a 2k-file
|
|
890
|
-
// project. collectTodoBaselineItems re-implements the walk in async chunks
|
|
891
|
-
// (per-file scan, yielding every 30 files), falling back to scanDirectory.
|
|
892
|
-
const items = await collectTodoBaselineItems(todoScanner, analysisRoot, () => runtime.isCurrentSession(sessionGeneration));
|
|
893
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
894
|
-
return;
|
|
895
|
-
dbg(`session_start TODO scan: ${items.length} items (baseline stored)`);
|
|
896
|
-
cacheManager.writeCache("todo-baseline", { items }, analysisRoot);
|
|
897
|
-
});
|
|
898
|
-
if (!canRunJsTsHeavyScans) {
|
|
899
|
-
dbg("session_start: skipping JS/TS startup scans (requires JS/TS language + project config)");
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
// #462: knip/jscpd/madge/dead-code/govulncheck/gitleaks/trivy/opengrep each spawn an
|
|
903
|
-
// external CLI that walks the whole project tree on its own — a walk we
|
|
904
|
-
// don't control or get to route to an async collector. On a measured-slow
|
|
905
|
-
// filesystem that walk reproduces the exact multi-second freeze this
|
|
906
|
-
// feature exists to prevent, so skip exactly those seven with a visible
|
|
907
|
-
// reason instead of leaving the agent to read an empty/stale cache as
|
|
908
|
-
// "clean". The other scans in this function (todo above; call-graph/
|
|
909
|
-
// codebase-model/ast-grep-exports/word-index below) walk via
|
|
910
|
-
// `collectSourceFilesAsync` or build from cached review-graph data, so
|
|
911
|
-
// they stay on.
|
|
912
|
-
//
|
|
913
|
-
// #449 slice 0: the same gate also fires inside a nicobailon/pi-subagents
|
|
914
|
-
// child `pi` process (`PI_SUBAGENT_CHILD=1`) — a fan-out of N subagents in
|
|
915
|
-
// the same cwd otherwise pays N full heavyweight-scan fleets for
|
|
916
|
-
// short-lived task agents that rarely consult them. In-process scans stay
|
|
917
|
-
// on for the same reason as slow-FS: the subagent may still use symbol
|
|
918
|
-
// search / word-index.
|
|
919
|
-
const isSubagent = isSubagentSession();
|
|
920
|
-
const skipHeavyweightScans = isSlowFs(analysisRoot) || isSubagent;
|
|
921
|
-
const runHeavyweightTask = (name, task) => {
|
|
922
|
-
const flag = `no-${name}`;
|
|
923
|
-
if (name !== "opengrep" && name !== "trivy" && !analyzerEnabled(flag)) {
|
|
924
|
-
dbg(`session_start ${name}: skipped (disabled by config)`);
|
|
925
|
-
recordDegradationOnce({
|
|
926
|
-
kind: "startup-analyzer-disabled",
|
|
927
|
-
subject: name,
|
|
928
|
-
reason: "skipped (disabled by config)",
|
|
929
|
-
});
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
if (skipHeavyweightScans)
|
|
933
|
-
return;
|
|
934
|
-
void runTask(name, task);
|
|
935
|
-
};
|
|
936
|
-
if (skipHeavyweightScans) {
|
|
937
|
-
dbg(`session_start: skipping knip/jscpd/madge/dead-code/govulncheck/gitleaks/trivy/opengrep (${isSubagent ? "subagent" : "slow-fs"})`);
|
|
938
|
-
deps.notify(`⏭️ Skipped background code-quality scans (knip/jscpd/madge/dead-code/govulncheck/gitleaks/trivy/opengrep): ${isSubagent ? subagentLightModeNotice() : slowFsDegradationNotice()}`, "info");
|
|
939
|
-
}
|
|
940
|
-
// Knip — dead code / unused exports
|
|
941
|
-
runHeavyweightTask("knip", async () => {
|
|
942
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
943
|
-
return;
|
|
944
|
-
const cached = cacheManager.readCache("knip", analysisRoot);
|
|
945
|
-
if (cached) {
|
|
946
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
947
|
-
return;
|
|
948
|
-
dbg(`session_start Knip: cache hit (${Math.round((Date.now() - new Date(cached.meta.timestamp).getTime()) / 1000)}s ago)`);
|
|
949
|
-
}
|
|
950
|
-
else {
|
|
951
|
-
// KnipClient skips before probing/installing when analysisRoot is not a real
|
|
952
|
-
// JS/Knip project. This avoids running knip from Unity/C#/generic repos.
|
|
953
|
-
const startMs = Date.now();
|
|
954
|
-
const knipResult = await knipClient.analyze(analysisRoot, getKnipIgnorePatterns(), { projectSeq: runtime.projectSeq });
|
|
955
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
956
|
-
return;
|
|
957
|
-
if (knipResult.success) {
|
|
958
|
-
cacheManager.writeCache("knip", knipResult, analysisRoot, {
|
|
959
|
-
scanDurationMs: Date.now() - startMs,
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
dbg(`session_start Knip scan done (${Date.now() - startMs}ms)`);
|
|
963
|
-
}
|
|
964
|
-
});
|
|
965
|
-
// jscpd — duplicate code detection
|
|
966
|
-
runHeavyweightTask("jscpd", async () => {
|
|
967
|
-
if (await jscpdClient.ensureAvailable()) {
|
|
968
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
969
|
-
return;
|
|
970
|
-
// Detect TS projects by tsconfig.json at the analysis root. When
|
|
971
|
-
// set, JscpdClient.scan adds **/*.js and **/*.jsx to its ignore
|
|
972
|
-
// pattern so compiled artifacts under dist/ aren't flagged as
|
|
973
|
-
// duplicates of their TypeScript sources (closes #126's latent
|
|
974
|
-
// dist/-as-duplicate bug). Cache scanner key varies by this flag
|
|
975
|
-
// so a stale cache built with the wrong setting invalidates on
|
|
976
|
-
// first read.
|
|
977
|
-
const isTsProject = nodeFs.existsSync(path.join(analysisRoot, "tsconfig.json"));
|
|
978
|
-
const scannerKey = isTsProject ? "jscpd-ts" : "jscpd";
|
|
979
|
-
const cached = cacheManager.readCache(scannerKey, analysisRoot);
|
|
980
|
-
if (cached) {
|
|
981
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
982
|
-
return;
|
|
983
|
-
dbg(`session_start jscpd: cache hit (${scannerKey})`);
|
|
984
|
-
}
|
|
985
|
-
else {
|
|
986
|
-
const startMs = Date.now();
|
|
987
|
-
const jscpdResult = await jscpdClient.scan(analysisRoot, undefined, undefined, isTsProject);
|
|
988
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
989
|
-
return;
|
|
990
|
-
if (jscpdResult.success) {
|
|
991
|
-
cacheManager.writeCache(scannerKey, jscpdResult, analysisRoot, {
|
|
992
|
-
scanDurationMs: Date.now() - startMs,
|
|
993
|
-
});
|
|
994
|
-
}
|
|
995
|
-
dbg(`session_start jscpd scan done (${Date.now() - startMs}ms, isTsProject=${isTsProject})`);
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
else {
|
|
999
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1000
|
-
return;
|
|
1001
|
-
dbg("session_start jscpd: not available");
|
|
1002
|
-
}
|
|
1003
|
-
});
|
|
1004
|
-
// dead-code — cross-file dead-code for non-JS/TS languages (#127). Each
|
|
1005
|
-
// client self-gates via detect() (a cheap fs marker probe), so only a
|
|
1006
|
-
// matching-language project incurs the whole-tree scan cost. Knip remains
|
|
1007
|
-
// the JS/TS path (above); these run alongside it for polyglot repos.
|
|
1008
|
-
runHeavyweightTask("dead-code", async () => {
|
|
1009
|
-
const applicable = deadCodeClients.filter((c) => c.detect(analysisRoot));
|
|
1010
|
-
if (applicable.length === 0)
|
|
1011
|
-
return;
|
|
1012
|
-
await Promise.all(applicable.map(async (client) => {
|
|
1013
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1014
|
-
return undefined;
|
|
1015
|
-
const cacheKey = `dead-code-${client.id}`;
|
|
1016
|
-
const cached = cacheManager.readCache(cacheKey, analysisRoot);
|
|
1017
|
-
if (cached) {
|
|
1018
|
-
dbg(`session_start dead-code(${client.id}): cache hit`);
|
|
1019
|
-
return undefined;
|
|
1020
|
-
}
|
|
1021
|
-
const startMs = Date.now();
|
|
1022
|
-
const result = await client.analyze(analysisRoot);
|
|
1023
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1024
|
-
return undefined;
|
|
1025
|
-
if (result.success) {
|
|
1026
|
-
cacheManager.writeCache(cacheKey, result, analysisRoot, {
|
|
1027
|
-
scanDurationMs: Date.now() - startMs,
|
|
1028
|
-
});
|
|
1029
|
-
}
|
|
1030
|
-
logDeadCodeScan({
|
|
1031
|
-
language: client.language,
|
|
1032
|
-
success: result.success,
|
|
1033
|
-
cached: false,
|
|
1034
|
-
unusedExports: result.unusedExports.length,
|
|
1035
|
-
unusedFiles: result.unusedFiles.length,
|
|
1036
|
-
unusedDeps: result.unusedDeps.length,
|
|
1037
|
-
unlistedDeps: result.unlistedDeps.length,
|
|
1038
|
-
durationMs: result.durationMs ?? Date.now() - startMs,
|
|
1039
|
-
...(!result.success && { reason: result.summary }),
|
|
1040
|
-
});
|
|
1041
|
-
dbg(`session_start dead-code(${client.id}) done (${Date.now() - startMs}ms, ${deadCodeIssueCount(result)} issues)`);
|
|
1042
|
-
}));
|
|
1043
|
-
});
|
|
1044
|
-
// govulncheck — Go module CVE detection (#132)
|
|
1045
|
-
// Skipped silently when the project isn't a Go module or when
|
|
1046
|
-
// `govulncheck` isn't installed (no auto-install in this slice).
|
|
1047
|
-
runHeavyweightTask("govulncheck", async () => {
|
|
1048
|
-
if (!GovulncheckClient.hasGoModule(analysisRoot)) {
|
|
1049
|
-
dbg("session_start govulncheck: no go.mod — skipped");
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
if (!(await govulncheckClient.ensureAvailable())) {
|
|
1053
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1054
|
-
return;
|
|
1055
|
-
dbg("session_start govulncheck: not installed (go install golang.org/x/vuln/cmd/govulncheck@latest)");
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1059
|
-
return;
|
|
1060
|
-
const cached = cacheManager.readCache("govulncheck", analysisRoot);
|
|
1061
|
-
if (cached) {
|
|
1062
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1063
|
-
return;
|
|
1064
|
-
dbg(`session_start govulncheck: cache hit (${cached.data.findings.length} findings)`);
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
const startMs = Date.now();
|
|
1068
|
-
const result = await govulncheckClient.analyze(analysisRoot);
|
|
1069
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1070
|
-
return;
|
|
1071
|
-
if (result.success) {
|
|
1072
|
-
cacheManager.writeCache("govulncheck", result, analysisRoot, {
|
|
1073
|
-
scanDurationMs: Date.now() - startMs,
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1076
|
-
dbg(`session_start govulncheck: ${result.findings.length} reachable findings (${Date.now() - startMs}ms)`);
|
|
1077
|
-
});
|
|
1078
|
-
// gitleaks — committed-secrets detection (#130)
|
|
1079
|
-
// Config-gated: opts in via .gitleaks.toml / .gitleaksignore / git
|
|
1080
|
-
// hook / gitleaks dep. Cross-language by design.
|
|
1081
|
-
runHeavyweightTask("gitleaks", async () => {
|
|
1082
|
-
if (!GitleaksClient.hasGitleaksSignal(analysisRoot)) {
|
|
1083
|
-
dbg("session_start gitleaks: no opt-in signal — skipped");
|
|
1084
|
-
return;
|
|
1085
|
-
}
|
|
1086
|
-
if (!(await gitleaksClient.ensureAvailable())) {
|
|
1087
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1088
|
-
return;
|
|
1089
|
-
dbg("session_start gitleaks: not available (install failed?)");
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1093
|
-
return;
|
|
1094
|
-
const cached = cacheManager.readCache("gitleaks", analysisRoot);
|
|
1095
|
-
if (cached) {
|
|
1096
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1097
|
-
return;
|
|
1098
|
-
dbg(`session_start gitleaks: cache hit (${cached.data.findings.length} findings)`);
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
const startMs = Date.now();
|
|
1102
|
-
const result = await gitleaksClient.scan(analysisRoot);
|
|
1103
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1104
|
-
return;
|
|
1105
|
-
if (result.success) {
|
|
1106
|
-
cacheManager.writeCache("gitleaks", result, analysisRoot, {
|
|
1107
|
-
scanDurationMs: Date.now() - startMs,
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
dbg(`session_start gitleaks: ${result.findings.length} findings (${Date.now() - startMs}ms)`);
|
|
1111
|
-
});
|
|
1112
|
-
// opengrep — full-workspace security/quality findings via a single CLI scan
|
|
1113
|
-
// (#584). Structurally always-on (mirrors the LSP auxiliary's own
|
|
1114
|
-
// enablement — see `OpengrepClient.resolveConfig`): the local rule file or
|
|
1115
|
-
// `auto` registry ruleset always runs, `resolveOpengrepConfig` only picks
|
|
1116
|
-
// which. Replaces the per-file LSP sweep as the source of opengrep
|
|
1117
|
-
// findings for `lens_diagnostics mode=full` (`runWorkspaceDiagnostics` now
|
|
1118
|
-
// excludes the opengrep server from its per-file "all"-scope touch —
|
|
1119
|
-
// clients/lsp/index.ts `WORKSPACE_SWEEP_EXCLUDED_SERVER_IDS`); the per-edit
|
|
1120
|
-
// real-time LSP path is untouched.
|
|
1121
|
-
runHeavyweightTask("opengrep", async () => {
|
|
1122
|
-
if (!(await opengrepClient.ensureAvailable())) {
|
|
1123
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1124
|
-
return;
|
|
1125
|
-
dbg("session_start opengrep: not available (install failed?)");
|
|
1126
|
-
return;
|
|
1127
|
-
}
|
|
1128
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1129
|
-
return;
|
|
1130
|
-
const cached = cacheManager.readCache("opengrep", analysisRoot);
|
|
1131
|
-
if (cached) {
|
|
1132
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1133
|
-
return;
|
|
1134
|
-
dbg(`session_start opengrep: cache hit (${cached.data.findings.length} findings)`);
|
|
1135
|
-
return;
|
|
1136
|
-
}
|
|
1137
|
-
const startMs = Date.now();
|
|
1138
|
-
const result = await opengrepClient.scan(analysisRoot);
|
|
1139
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1140
|
-
return;
|
|
1141
|
-
if (result.success) {
|
|
1142
|
-
cacheManager.writeCache("opengrep", result, analysisRoot, {
|
|
1143
|
-
scanDurationMs: Date.now() - startMs,
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1146
|
-
dbg(`session_start opengrep: ${result.findings.length} findings (${Date.now() - startMs}ms)`);
|
|
1147
|
-
});
|
|
1148
|
-
// madge — whole-project circular-dependency detection. Session-start + cached
|
|
1149
|
-
// (uniform with knip/jscpd/gitleaks) so lens_diagnostics mode=full reads it
|
|
1150
|
-
// from the `madge` cache via the extractor registry — never a fresh scan.
|
|
1151
|
-
runHeavyweightTask("madge", async () => {
|
|
1152
|
-
if (!(await depChecker.ensureAvailable())) {
|
|
1153
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1154
|
-
return;
|
|
1155
|
-
dbg("session_start madge: not available");
|
|
1156
|
-
return;
|
|
1157
|
-
}
|
|
1158
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1159
|
-
return;
|
|
1160
|
-
const cached = cacheManager.readCache("madge", analysisRoot);
|
|
1161
|
-
if (cached) {
|
|
1162
|
-
dbg(`session_start madge: cache hit (${cached.data.circular.length} cycles)`);
|
|
1163
|
-
return;
|
|
1164
|
-
}
|
|
1165
|
-
const startMs = Date.now();
|
|
1166
|
-
const result = await depChecker.scanProject(analysisRoot);
|
|
1167
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1168
|
-
return;
|
|
1169
|
-
cacheManager.writeCache("madge", result, analysisRoot, {
|
|
1170
|
-
scanDurationMs: Date.now() - startMs,
|
|
1171
|
-
});
|
|
1172
|
-
dbg(`session_start madge: ${result.circular.length} circular dependency chain(s) (${Date.now() - startMs}ms)`);
|
|
1173
|
-
});
|
|
1174
|
-
// trivy — dependency CVE detection (#131, Phase 1)
|
|
1175
|
-
// Explicit opt-in: `trivy.enabled: true` in .pi-lens.json AND a dependency
|
|
1176
|
-
// manifest present. The first run downloads Trivy's vuln DB (~30-200 MB);
|
|
1177
|
-
// harmless here since this whole task runs in the background session_start
|
|
1178
|
-
// wrapper.
|
|
1179
|
-
runHeavyweightTask("trivy", async () => {
|
|
1180
|
-
if (!TrivyClient.shouldScan(analysisRoot)) {
|
|
1181
|
-
dbg("session_start trivy: not enabled / no dependency manifest — skipped");
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
if (!(await trivyClient.ensureAvailable())) {
|
|
1185
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1186
|
-
return;
|
|
1187
|
-
dbg("session_start trivy: not available (install failed?)");
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1191
|
-
return;
|
|
1192
|
-
const cached = cacheManager.readCache("trivy", analysisRoot);
|
|
1193
|
-
if (cached) {
|
|
1194
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1195
|
-
return;
|
|
1196
|
-
dbg(`session_start trivy: cache hit (${cached.data.findings.length} findings)`);
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
const startMs = Date.now();
|
|
1200
|
-
const result = await trivyClient.scan(analysisRoot);
|
|
1201
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1202
|
-
return;
|
|
1203
|
-
if (result.success) {
|
|
1204
|
-
cacheManager.writeCache("trivy", result, analysisRoot, {
|
|
1205
|
-
scanDurationMs: Date.now() - startMs,
|
|
1206
|
-
});
|
|
1207
|
-
}
|
|
1208
|
-
dbg(`session_start trivy: ${result.findings.length} CVE findings (${Date.now() - startMs}ms)`);
|
|
1209
|
-
});
|
|
1210
|
-
// call-graph — build function-level call graph from review graph data
|
|
1211
|
-
let callGraphIdentity;
|
|
1212
|
-
const callGraphTask = runTask("call-graph", async () => {
|
|
1213
|
-
const { FactStore } = await import("./dispatch/fact-store.js");
|
|
1214
|
-
const { buildOrUpdateGraph, extractSymbolsAndRefsFromGraph, getReviewGraphCacheIdentity, } = await import("./review-graph/builder.js");
|
|
1215
|
-
const { buildCallGraph, saveCallGraph, loadCallGraph } = await import("./call-graph.js");
|
|
1216
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1217
|
-
return;
|
|
1218
|
-
const startMs = Date.now();
|
|
1219
|
-
// Build (or hydrate) the canonical review graph first. The call graph is a
|
|
1220
|
-
// derived projection of that graph, so its freshness is the review graph's
|
|
1221
|
-
// version/signature—not a second source walk and mtime policy.
|
|
1222
|
-
// Subject labels this store's capacity-eviction telemetry distinctly
|
|
1223
|
-
// from the other five production FactStore instances (#2243 review
|
|
1224
|
-
// round 3, F1) — this session-start walk runs BEFORE any dispatch and
|
|
1225
|
-
// can visit every file in the project, so a shared subject would let
|
|
1226
|
-
// it consume the dispatch store's once-per-session ledger slot first.
|
|
1227
|
-
const sessionFacts = new FactStore("runtime-session-call-graph");
|
|
1228
|
-
const graph = await buildOrUpdateGraph(analysisRoot, [], sessionFacts);
|
|
1229
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1230
|
-
return;
|
|
1231
|
-
const identity = getReviewGraphCacheIdentity(analysisRoot, graph);
|
|
1232
|
-
if (!identity) {
|
|
1233
|
-
dbg("session_start call-graph: canonical review-graph identity unavailable");
|
|
1234
|
-
return;
|
|
1235
|
-
}
|
|
1236
|
-
callGraphIdentity = {
|
|
1237
|
-
reviewGraphVersion: identity.version,
|
|
1238
|
-
reviewGraphSignature: identity.signature,
|
|
1239
|
-
};
|
|
1240
|
-
const cached = loadCallGraph(snapshotRoot, {
|
|
1241
|
-
reviewGraphVersion: identity.version,
|
|
1242
|
-
reviewGraphSignature: identity.signature,
|
|
1243
|
-
});
|
|
1244
|
-
if (cached) {
|
|
1245
|
-
runtime.callGraph = cached.graph;
|
|
1246
|
-
dbg(`session_start call-graph: loaded from cache (${cached.graph.edges.length} edges, ${cached.graph.callers.size} callee entries, ${Date.now() - startMs}ms)`);
|
|
1247
|
-
return;
|
|
1248
|
-
}
|
|
1249
|
-
// Build from the canonical review graph (reuses already-parsed data, no
|
|
1250
|
-
// duplicate parser or source walk).
|
|
1251
|
-
const { allSymbols, allRefs, coverage } = extractSymbolsAndRefsFromGraph(graph);
|
|
1252
|
-
const callGraph = buildCallGraph(allSymbols, allRefs, coverage);
|
|
1253
|
-
runtime.callGraph = callGraph;
|
|
1254
|
-
saveCallGraph(snapshotRoot, callGraph, {
|
|
1255
|
-
reviewGraphVersion: identity.version,
|
|
1256
|
-
reviewGraphSignature: identity.signature,
|
|
1257
|
-
});
|
|
1258
|
-
dbg(`session_start call-graph: built ${callGraph.edges.length} edges, ${callGraph.callers.size} callee entries (${Date.now() - startMs}ms)`);
|
|
1259
|
-
});
|
|
1260
|
-
// codebase-model — build mental model from call graph (internal-only until validated)
|
|
1261
|
-
// Keep this deferred, but await the call-graph task's completion rather than
|
|
1262
|
-
// racing its independently deferred timer. Otherwise a slow graph build leaves
|
|
1263
|
-
// `runtime.callGraph` unset at this task's start and loses the model for the
|
|
1264
|
-
// entire session (#1070).
|
|
1265
|
-
void runTask("codebase-model", async () => {
|
|
1266
|
-
await callGraphTask;
|
|
1267
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1268
|
-
return;
|
|
1269
|
-
if (!runtime.callGraph)
|
|
1270
|
-
return;
|
|
1271
|
-
const { buildCodebaseModel, saveCodebaseModel, DEFAULT_CODEBASE_MODEL_TOKEN_BUDGET, } = await import("./codebase-model.js");
|
|
1272
|
-
if (!callGraphIdentity)
|
|
1273
|
-
return;
|
|
1274
|
-
const model = buildCodebaseModel(runtime.callGraph, analysisRoot, DEFAULT_CODEBASE_MODEL_TOKEN_BUDGET, callGraphIdentity);
|
|
1275
|
-
saveCodebaseModel(snapshotRoot, model);
|
|
1276
|
-
const top3 = model.entries
|
|
1277
|
-
.slice(0, 3)
|
|
1278
|
-
.map((e) => e.name)
|
|
1279
|
-
.join(", ");
|
|
1280
|
-
dbg(`session_start codebase-model: ${model.entries.length} entries, ` +
|
|
1281
|
-
`${model.totalTokens} tokens, top symbols: ${top3 || "(none)"}`);
|
|
1282
|
-
});
|
|
1283
|
-
// ast-grep — export scan for duplicate detection
|
|
1284
|
-
void runTask("ast-grep-exports", async () => {
|
|
1285
|
-
if (await astGrepClient.ensureAvailable()) {
|
|
1286
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1287
|
-
return;
|
|
1288
|
-
const exports = await astGrepClient.scanExports(analysisRoot, "typescript");
|
|
1289
|
-
if (!runtime.isCurrentSession(sessionGeneration))
|
|
1290
|
-
return;
|
|
1291
|
-
dbg(`session_start exports scan: ${exports.size} functions found`);
|
|
1292
|
-
for (const [name, file] of exports) {
|
|
1293
|
-
runtime.cachedExports.set(name, file);
|
|
1294
|
-
}
|
|
1295
|
-
saveRuntimeProjectSnapshot({ cwd: snapshotRoot, runtime, dbg });
|
|
1296
|
-
}
|
|
1297
|
-
});
|
|
1298
|
-
// word-index — identifier inverted index + BM25 for ranked symbol search
|
|
1299
|
-
// (#162). Load -> rebuild-if-stale -> persist lifecycle (#348), shared with
|
|
1300
|
-
// the quick-mode cold-start warmup pass below.
|
|
1301
|
-
void runTask("word-index", async () => {
|
|
1302
|
-
await buildOrRefreshWordIndex({
|
|
1303
|
-
runtime,
|
|
1304
|
-
sessionGeneration,
|
|
1305
|
-
analysisRoot,
|
|
1306
|
-
snapshotRoot,
|
|
1307
|
-
dbg,
|
|
1308
|
-
});
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
function scheduleDeferredToolProbes(deps, languageProfile, startupDefaults, startupScansWillRun, dbg) {
|
|
1312
|
-
// #2467: same deferral as `scheduleStartupScans` — the probes were already
|
|
1313
|
-
// fire-and-forget, so the analyzers they probe load here rather than before
|
|
1314
|
-
// the handler.
|
|
1315
|
-
void (async () => {
|
|
1316
|
-
const resolved = await demandBootstrapDeps(deps, "session-start-tool-probes");
|
|
1317
|
-
if (!resolved) {
|
|
1318
|
-
dbg("session_start tools: analyzer bootstrap unavailable — no probes");
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1321
|
-
scheduleDeferredToolProbesWithClients(resolved, languageProfile, startupDefaults, startupScansWillRun, dbg);
|
|
1322
|
-
})();
|
|
1323
|
-
}
|
|
1324
|
-
/** The probe bodies, once the analyzers they probe are resolved. */
|
|
1325
|
-
function scheduleDeferredToolProbesWithClients(deps, languageProfile, startupDefaults, startupScansWillRun, dbg) {
|
|
1326
|
-
const { biomeClient, ruffClient, depChecker } = deps;
|
|
1327
|
-
const defaultTools = new Set(startupDefaults);
|
|
1328
|
-
const probes = [];
|
|
1329
|
-
// Do not probe tools already covered by startup preinstall or startup scans.
|
|
1330
|
-
// This keeps session_start logs from showing duplicate "ensure X: start" lines
|
|
1331
|
-
// while preserving lazy checks for tools that are actually relevant.
|
|
1332
|
-
if (languageProfile.present.jsts && !defaultTools.has("biome")) {
|
|
1333
|
-
probes.push(["biome", () => biomeClient.ensureAvailable()]);
|
|
1334
|
-
}
|
|
1335
|
-
if (languageProfile.present.python && !defaultTools.has("ruff")) {
|
|
1336
|
-
probes.push(["ruff", () => ruffClient.ensureAvailable()]);
|
|
1337
|
-
}
|
|
1338
|
-
if (startupScansWillRun) {
|
|
1339
|
-
probes.push(["madge", () => depChecker.ensureAvailable()]);
|
|
1340
|
-
}
|
|
1341
|
-
if (probes.length === 0) {
|
|
1342
|
-
dbg("session_start tools: no deferred availability probes needed");
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
void (async () => {
|
|
1346
|
-
const warmStart = Date.now();
|
|
1347
|
-
const results = await Promise.all(probes.map(async ([name, run]) => {
|
|
1348
|
-
try {
|
|
1349
|
-
return [name, await run()];
|
|
1350
|
-
}
|
|
1351
|
-
catch (err) {
|
|
1352
|
-
dbg(`session_start: ${name} availability check failed: ${err}`);
|
|
1353
|
-
return [name, false];
|
|
1354
|
-
}
|
|
1355
|
-
}));
|
|
1356
|
-
const summary = results
|
|
1357
|
-
.map(([name, ready]) => `${name}=${ready}`)
|
|
1358
|
-
.join(" ");
|
|
1359
|
-
dbg(`session_start tools (deferred probes complete, ${Date.now() - warmStart}ms): ${summary}`);
|
|
1360
|
-
})();
|
|
1361
|
-
}
|
|
1362
|
-
/**
|
|
1363
|
-
* Session-start orientation prepended as a context message (gated by the
|
|
1364
|
-
* context-injection toggle). Deliberately lean: it names the high-value tools
|
|
1365
|
-
* and the distinction between cached reporting and active verification
|
|
1366
|
-
* — per-tool argument detail lives in each tool's own registered description, so
|
|
1367
|
-
* re-documenting it here would just pay the tokens twice every session.
|
|
1368
|
-
*/
|
|
1369
|
-
export const SESSION_START_GUIDANCE = [
|
|
1370
|
-
"📌 pi-lens active — automated checks run on every edit/write; blocking errors (including pre-existing) show inline and must be fixed.\n" +
|
|
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 (aggregate hosts: lens(action=diagnostics)).\n" +
|
|
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
|
-
"• 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
|
-
];
|
|
1376
|
-
export async function handleSessionStart(deps) {
|
|
1377
|
-
resetDegradationLedger();
|
|
1378
|
-
// #2467: re-arm the analyzer bootstrap's shutdown gate. The gate is a
|
|
1379
|
-
// per-SESSION claim ("this session is over") held in process-lived storage,
|
|
1380
|
-
// so without this a replacement session in the same process would find
|
|
1381
|
-
// every analyzer refused for the rest of the process — AGENTS.md defect
|
|
1382
|
-
// shape 17. The resident clients themselves are deliberately kept.
|
|
1383
|
-
resetAnalyzerBootstrapSessionState();
|
|
1384
|
-
resetProjectDataDirSessionState();
|
|
1385
|
-
resetTestRunnerDelivery();
|
|
1386
|
-
// #2450 fix round 3, catalog shape 17: the "bridge unavailable" dbg latch
|
|
1387
|
-
// (`clients/lsp-mutation.ts`) is a process-lifetime once-per-session flag,
|
|
1388
|
-
// same shape as the ledger's own once-per-subject bookkeeping above, but
|
|
1389
|
-
// kept as its own reset (not folded into `resetDegradationLedger`) to
|
|
1390
|
-
// avoid that module reaching back into this one — see the latch's own
|
|
1391
|
-
// comment for why.
|
|
1392
|
-
resetLspMutationNoBridgeDbgLatch();
|
|
1393
|
-
// #1743: the bounded-telemetry per-turn counters. The rising-edge state is
|
|
1394
|
-
// NOT here — it is the ledger's own tally, reset on the line above. These
|
|
1395
|
-
// counters are keyed by turn index, and a new session restarts turn
|
|
1396
|
-
// numbering at 0, so without this a stale count could survive a session
|
|
1397
|
-
// boundary that happened to land on the same index.
|
|
1398
|
-
resetBoundedTelemetry();
|
|
1399
|
-
// #1956 R3: stale message_end events drain after replacement. Rotate the
|
|
1400
|
-
// live anchor into one bounded previous slot so the queued event keeps the
|
|
1401
|
-
// replaced session's attribution until a newer live message_end is seen.
|
|
1402
|
-
rotateMessageEndAttribution();
|
|
1403
|
-
// #2526: the once-per-session `config_resolved` claim is NOT re-armed here.
|
|
1404
|
-
// index.ts resolves this session's config in `ensureLSPConfigInitialized`
|
|
1405
|
-
// BEFORE it calls this handler, so a re-arm on this line would fire between
|
|
1406
|
-
// the session's own two resolutions and the deferred `loadLSPConfig` below
|
|
1407
|
-
// would write a second row for one session (review round 2, F1). The re-arm
|
|
1408
|
-
// lives in index.ts's `session_start` closure, ahead of that ensure and
|
|
1409
|
-
// behind the #473 gate — see `resetOncePerSessionPhases`'s doc comment.
|
|
1410
|
-
const handlerEnteredAt = Date.now();
|
|
1411
|
-
const sessionStartMs = deps.sessionStartFiredAt ?? handlerEnteredAt;
|
|
1412
|
-
const cwdForTelemetry = deps.ctxCwd ?? process.cwd();
|
|
1413
|
-
if (deps.sessionStartFiredAt !== undefined) {
|
|
1414
|
-
logLatency({
|
|
1415
|
-
type: "phase",
|
|
1416
|
-
filePath: cwdForTelemetry,
|
|
1417
|
-
phase: "session_start_prehandler",
|
|
1418
|
-
startedAt: new Date(deps.sessionStartFiredAt).toISOString(),
|
|
1419
|
-
durationMs: (deps.handlerEnteredAt ?? handlerEnteredAt) - deps.sessionStartFiredAt,
|
|
1420
|
-
metadata: { reason: deps.sessionReason },
|
|
1421
|
-
});
|
|
1422
|
-
}
|
|
1423
|
-
// Cold-start input-latency mitigation. The first `session_start` of
|
|
1424
|
-
// the process — i.e. the one that fires immediately after the user
|
|
1425
|
-
// launches `pi` — must return as fast as possible so the TUI input
|
|
1426
|
-
// box becomes responsive. The full startup mode runs several
|
|
1427
|
-
// expensive synchronous walks (resolveStartupScanContext,
|
|
1428
|
-
// detectProjectLanguageProfile, scanProjectRules, scheduleStartupScans)
|
|
1429
|
-
// that together can block the event loop for 3-6s on a 2k-file
|
|
1430
|
-
// project, during which keystrokes are dropped or batched.
|
|
1431
|
-
//
|
|
1432
|
-
// Strategy:
|
|
1433
|
-
// - Force the very first invocation to "quick" mode, which exits
|
|
1434
|
-
// after a minimal runtime reset and snapshot hydration.
|
|
1435
|
-
// - 2 seconds later, schedule a background warmup that walks the
|
|
1436
|
-
// project asynchronously (yielding every 100 entries) and
|
|
1437
|
-
// populates the in-process memo caches
|
|
1438
|
-
// (startupScanContextCache + languageProfileCache).
|
|
1439
|
-
// - The user's first /new (or any subsequent session_start) sees
|
|
1440
|
-
// a cache hit on both walks and finishes the full path in <50ms.
|
|
1441
|
-
//
|
|
1442
|
-
// Opt-out: PI_LENS_COLD_START_QUICK=0 disables this behaviour.
|
|
1443
|
-
// Override: PI_LENS_STARTUP_MODE explicitly set wins (we honour it).
|
|
1444
|
-
// deps.startupModeOverride lets a host (e.g. the MCP server, which has
|
|
1445
|
-
// no TUI keystroke latency to protect) skip the quick-mode heuristic
|
|
1446
|
-
// entirely — but only when PI_LENS_STARTUP_MODE is unset in the env
|
|
1447
|
-
// (an explicit env var still takes highest precedence).
|
|
1448
|
-
// Tunable: PI_LENS_WARMUP_DELAY_MS adjusts the warmup delay.
|
|
1449
|
-
let startupMode = resolveStartupMode(deps.projectConfig, deps.globalConfig);
|
|
1450
|
-
// SAFETY: these two flags are process-lifetime state pi-lens stashes on
|
|
1451
|
-
// `globalThis` so a second extension instance in the same process sees the
|
|
1452
|
-
// first one's warmup. There is no ambient declaration for them, and adding
|
|
1453
|
-
// one would let any module write them. Both are declared OPTIONAL here, so
|
|
1454
|
-
// the reads below are `undefined`-safe on a process where nothing set them.
|
|
1455
|
-
const processGlobals = globalThis;
|
|
1456
|
-
const isFirstSessionOfProcess = !processGlobals.__piLensFirstSessionDone;
|
|
1457
|
-
if (isFirstSessionOfProcess &&
|
|
1458
|
-
process.env.PI_LENS_COLD_START_QUICK !== "0" &&
|
|
1459
|
-
!process.env.PI_LENS_STARTUP_MODE &&
|
|
1460
|
-
!deps.projectConfig?.startup?.mode &&
|
|
1461
|
-
!deps.globalConfig?.startup?.mode) {
|
|
1462
|
-
// Apply host-provided override (e.g. MCP server forces "full") before
|
|
1463
|
-
// falling back to the TUI quick-mode heuristic.
|
|
1464
|
-
startupMode = deps.startupModeOverride ?? "quick";
|
|
1465
|
-
}
|
|
1466
|
-
processGlobals.__piLensFirstSessionDone = true;
|
|
1467
|
-
// #1785 F5 (round 4, design (a)): the warmup timer body below ALREADY
|
|
1468
|
-
// loads the on-disk snapshot for its own purposes (`cachedSnapshot`,
|
|
1469
|
-
// reused for the `startupScan` verdict cache) strictly BEFORE any save it
|
|
1470
|
-
// might go on to make. This LOCAL variable — per-`handleSessionStart`-call,
|
|
1471
|
-
// deliberately NOT module-scope (see #1785 F6) — lets the warmup publish
|
|
1472
|
-
// that already-loaded read (narrowed to exports+rules, never the full
|
|
1473
|
-
// object with `wordIndex`) for `retroactivelyHydrateAfterDeferredSequence`
|
|
1474
|
-
// to reuse at zero additional cost, instead of this call paying for its
|
|
1475
|
-
// own separate synchronous capture (rounds 1-3's approach, reverted here:
|
|
1476
|
-
// see that function's doc comment for the full history). `undefined`
|
|
1477
|
-
// means the warmup hasn't reached its own read yet (or was never armed by
|
|
1478
|
-
// this call at all) — the quick-mode block's retroactive-hydration
|
|
1479
|
-
// callback then falls back to a live re-load instead.
|
|
1480
|
-
let warmupOwnSnapshotRead;
|
|
1481
|
-
// #1154: quick mode is entered on BOTH `pi -p`/`--print` (a one-shot that
|
|
1482
|
-
// exits right after this turn) and an interactive process's first
|
|
1483
|
-
// session_start (forced quick to protect keystroke latency, then warms
|
|
1484
|
-
// caches for the NEXT /new). The warmup only benefits a *future* session in
|
|
1485
|
-
// the same process — a one-shot print invocation has none. Worse, in a
|
|
1486
|
-
// one-shot the warmup is a referenced-handle keep-alive: the +2s timer and
|
|
1487
|
-
// the LSP-prewarm children / language-profile source walk it launches
|
|
1488
|
-
// outlive settle with no session_shutdown teardown, holding the process
|
|
1489
|
-
// open (the #1097/#1110/#1122-hyp-A class). So skip warmup entirely in
|
|
1490
|
-
// print mode; interactive first-sessions (not print) still warm.
|
|
1491
|
-
if (startupMode === "quick" &&
|
|
1492
|
-
!isPrintMode() &&
|
|
1493
|
-
process.env.PI_LENS_COLD_START_QUICK !== "0" &&
|
|
1494
|
-
!processGlobals.__piLensWarmupScheduled) {
|
|
1495
|
-
processGlobals.__piLensWarmupScheduled = true;
|
|
1496
|
-
const warmupDelayMs = Number(process.env.PI_LENS_WARMUP_DELAY_MS ?? 2000);
|
|
1497
|
-
const warmupCwd = deps.ctxCwd ?? process.cwd();
|
|
1498
|
-
const warmupDbg = deps.dbg;
|
|
1499
|
-
// #1154: `.unref()` the warmup timer so — even for a warmup that IS
|
|
1500
|
-
// scheduled (an interactive first-session) — the pending timer alone can
|
|
1501
|
-
// never hold the loop open. Interactive processes stay alive for other
|
|
1502
|
-
// reasons, so the warmup still fires; a one-shot never reaches here (the
|
|
1503
|
-
// isPrintMode gate above), and repo convention is that every background
|
|
1504
|
-
// timer is unref'd (this file previously had none).
|
|
1505
|
-
const warmupTimer = setTimeout(() => {
|
|
1506
|
-
const warmupStartedAt = Date.now();
|
|
1507
|
-
void (async () => {
|
|
1508
|
-
try {
|
|
1509
|
-
warmupDbg("warmup: starting background warmup");
|
|
1510
|
-
const scanContextStartedAt = Date.now();
|
|
1511
|
-
// Dynamic imports keep the warmup pipeline off the hot
|
|
1512
|
-
// startup path — these modules don't load until the timer
|
|
1513
|
-
// fires, well after the TUI is interactive.
|
|
1514
|
-
const startupScanModule = await import("./startup-scan.js");
|
|
1515
|
-
const languageProfileModule = await import("./language-profile.js");
|
|
1516
|
-
const warmupSnapshotRoot = resolveSnapshotRoot(warmupCwd);
|
|
1517
|
-
// #699: this background timer is the ONLY place a quick-mode
|
|
1518
|
-
// session (i.e. every `-p`/`--print` invocation, which forces
|
|
1519
|
-
// quick mode and returns before line ~1253 without ever touching
|
|
1520
|
-
// scan-context) computes scan-context — and that process exits
|
|
1521
|
-
// right after, discarding the result. Before walking, check for a
|
|
1522
|
-
// still-fresh persisted verdict (mirrors the interactive path's
|
|
1523
|
-
// snapshot reuse); after walking, persist unconditionally
|
|
1524
|
-
// (canWarmCaches true OR false) so the NEXT one-shot process can
|
|
1525
|
-
// reuse it instead of re-walking a possibly huge tree from
|
|
1526
|
-
// scratch on every single startup.
|
|
1527
|
-
const cachedSnapshot = loadProjectSnapshot(warmupSnapshotRoot);
|
|
1528
|
-
// #1785 F5 (round 4): publish this ALREADY-loaded read — narrowed
|
|
1529
|
-
// to exports+rules, never a reference to `cachedSnapshot` itself
|
|
1530
|
-
// (which carries `wordIndex`/`files`/`symbols`/`reverseDeps`) — for
|
|
1531
|
-
// `retroactivelyHydrateAfterDeferredSequence` to reuse. This read
|
|
1532
|
-
// strictly precedes the warmup's own possible save a few lines
|
|
1533
|
-
// below, so it is guaranteed to reflect disk as it stood before
|
|
1534
|
-
// THIS warmup could have touched it. Zero marginal cost: this read
|
|
1535
|
-
// already happens unconditionally for the `startupScan` verdict
|
|
1536
|
-
// cache below, with or without this fix.
|
|
1537
|
-
warmupOwnSnapshotRead = cachedSnapshot
|
|
1538
|
-
? {
|
|
1539
|
-
version: cachedSnapshot.version,
|
|
1540
|
-
seq: cachedSnapshot.seq,
|
|
1541
|
-
cachedExports: cachedSnapshot.cachedExports,
|
|
1542
|
-
projectRulesScan: cachedSnapshot.projectRulesScan,
|
|
1543
|
-
}
|
|
1544
|
-
: null;
|
|
1545
|
-
const cachedVerdict = cachedSnapshot?.startupScan;
|
|
1546
|
-
let scan;
|
|
1547
|
-
if (cachedVerdict &&
|
|
1548
|
-
startupScanModule.isStartupScanVerdictFresh(cachedVerdict)) {
|
|
1549
|
-
scan = { ...cachedVerdict, cwd: path.resolve(warmupCwd) };
|
|
1550
|
-
warmupDbg(`warmup: scan-context reused from cache (canWarm=${scan.canWarmCaches}${scan.reason ? `, reason=${scan.reason}` : ""})`);
|
|
1551
|
-
}
|
|
1552
|
-
else {
|
|
1553
|
-
scan =
|
|
1554
|
-
await startupScanModule.resolveStartupScanContextAsync(warmupCwd);
|
|
1555
|
-
logLatency({
|
|
1556
|
-
type: "phase",
|
|
1557
|
-
phase: "session_start_scan_context_compute",
|
|
1558
|
-
filePath: warmupCwd,
|
|
1559
|
-
startedAt: new Date(scanContextStartedAt).toISOString(),
|
|
1560
|
-
durationMs: Date.now() - scanContextStartedAt,
|
|
1561
|
-
metadata: { mode: "quick-background" },
|
|
1562
|
-
});
|
|
1563
|
-
warmupDbg(`warmup: scan-context done in ${Date.now() - warmupStartedAt}ms (canWarm=${scan.canWarmCaches})`);
|
|
1564
|
-
// Best-effort: a save failure must never affect warmup itself —
|
|
1565
|
-
// saveRuntimeProjectSnapshot already swallows its own errors.
|
|
1566
|
-
saveRuntimeProjectSnapshot({
|
|
1567
|
-
cwd: warmupSnapshotRoot,
|
|
1568
|
-
runtime: deps.runtime,
|
|
1569
|
-
startupScan: scan,
|
|
1570
|
-
dbg: warmupDbg,
|
|
1571
|
-
});
|
|
1572
|
-
}
|
|
1573
|
-
logLatency({
|
|
1574
|
-
type: "phase",
|
|
1575
|
-
phase: "warmup_scan_context",
|
|
1576
|
-
filePath: warmupCwd,
|
|
1577
|
-
startedAt: new Date(scanContextStartedAt).toISOString(),
|
|
1578
|
-
durationMs: Date.now() - scanContextStartedAt,
|
|
1579
|
-
});
|
|
1580
|
-
// Respect the startup-scan guard (#250): canWarmCaches is false for
|
|
1581
|
-
// home-dir / no-project-root / too-many-source-files. Proceeding into
|
|
1582
|
-
// the language-profile source walk in those cases lets it root at an
|
|
1583
|
-
// ancestor (e.g. a marker in $HOME when pi runs in ~/tmp) and traverse
|
|
1584
|
-
// the entire home tree — multi-hour scans. Nothing to warm anyway when
|
|
1585
|
-
// the guard says caches can't be warmed.
|
|
1586
|
-
if (!scan.canWarmCaches) {
|
|
1587
|
-
warmupDbg(`warmup: skipping language-profile (canWarm=false, reason=${scan.reason ?? "unknown"})`);
|
|
1588
|
-
return;
|
|
1589
|
-
}
|
|
1590
|
-
const languageRoot = scan.projectRoot ?? warmupCwd;
|
|
1591
|
-
const languageProfileStartedAt = Date.now();
|
|
1592
|
-
await languageProfileModule.detectProjectLanguageProfileAsync(languageRoot);
|
|
1593
|
-
warmupDbg(`warmup: language-profile done in ${Date.now() - languageProfileStartedAt}ms`);
|
|
1594
|
-
logLatency({
|
|
1595
|
-
type: "phase",
|
|
1596
|
-
phase: "warmup_language_profile",
|
|
1597
|
-
filePath: warmupCwd,
|
|
1598
|
-
startedAt: new Date(languageProfileStartedAt).toISOString(),
|
|
1599
|
-
durationMs: Date.now() - languageProfileStartedAt,
|
|
1600
|
-
});
|
|
1601
|
-
// #348: fold the word-index build/refresh into this existing
|
|
1602
|
-
// cold-start warmup pass so quick-mode (and any session whose very
|
|
1603
|
-
// first session_start is forced quick) still ends up with a
|
|
1604
|
-
// queryable index — full-mode sessions get it via the "word-index"
|
|
1605
|
-
// runTask above; this is the quick-mode equivalent, once per
|
|
1606
|
-
// process, off the hot path.
|
|
1607
|
-
const wordIndexStartedAt = Date.now();
|
|
1608
|
-
const wordIndexResult = await buildOrRefreshWordIndex({
|
|
1609
|
-
runtime: deps.runtime,
|
|
1610
|
-
sessionGeneration: deps.runtime.sessionGeneration,
|
|
1611
|
-
analysisRoot: languageRoot,
|
|
1612
|
-
snapshotRoot: warmupSnapshotRoot,
|
|
1613
|
-
dbg: warmupDbg,
|
|
1614
|
-
});
|
|
1615
|
-
warmupDbg(`warmup: word-index done in ${Date.now() - wordIndexStartedAt}ms`);
|
|
1616
|
-
logLatency({
|
|
1617
|
-
type: "phase",
|
|
1618
|
-
phase: "warmup_word_index",
|
|
1619
|
-
filePath: warmupCwd,
|
|
1620
|
-
startedAt: new Date(wordIndexStartedAt).toISOString(),
|
|
1621
|
-
durationMs: Date.now() - wordIndexStartedAt,
|
|
1622
|
-
metadata: wordIndexResult ? { ...wordIndexResult } : undefined,
|
|
1623
|
-
});
|
|
1624
|
-
// #947: fold the dominant-language LSP pre-warm into this
|
|
1625
|
-
// warmup pass. The first-session-of-process heuristic forces
|
|
1626
|
-
// quick mode, and the pre-warm below is gated on
|
|
1627
|
-
// allowBootstrapTasks (full mode only) — so it NEVER ran in
|
|
1628
|
-
// practice and every session's first edit paid a cold LSP
|
|
1629
|
-
// spawn (~750ms wait + ~2.5s spawn, measured). Fire it here
|
|
1630
|
-
// instead: off the interactive path, once per process (the
|
|
1631
|
-
// __piLensWarmupScheduled guard above), generation-guarded
|
|
1632
|
-
// inside igniteDominantLanguageWarm, and honoring the same
|
|
1633
|
-
// skips as the full-mode path — subagent light mode (#449),
|
|
1634
|
-
// warm-attach (#822), the no-lsp flag, and the
|
|
1635
|
-
// canWarmCaches guard (the early return above). Concurrent
|
|
1636
|
-
// secondaries never reach handleSessionStart at all (the
|
|
1637
|
-
// #473 guard in index.ts), so they never schedule this
|
|
1638
|
-
// warmup in the first place.
|
|
1639
|
-
const lspPrewarmStartedAt = Date.now();
|
|
1640
|
-
if (deps.getFlag("no-lsp")) {
|
|
1641
|
-
warmupDbg("warmup: skipping LSP pre-warm (no-lsp)");
|
|
1642
|
-
}
|
|
1643
|
-
else if (isSubagentSession()) {
|
|
1644
|
-
warmupDbg("warmup: skipping LSP pre-warm (subagent session)");
|
|
1645
|
-
}
|
|
1646
|
-
else if (isWarmAttached()) {
|
|
1647
|
-
warmupDbg("warmup: skipping LSP pre-warm (attached to incumbent)");
|
|
1648
|
-
}
|
|
1649
|
-
else {
|
|
1650
|
-
// #957 review: honor explicit warmFiles (#203) like the full
|
|
1651
|
-
// path does — configured projects warm exactly what they
|
|
1652
|
-
// asked for; dominant-language warm is the fallback.
|
|
1653
|
-
const lspConfig = await loadLSPConfig(warmupCwd).catch(() => ({
|
|
1654
|
-
warmFiles: [],
|
|
1655
|
-
}));
|
|
1656
|
-
const warmFiles = lspConfig.warmFiles ?? [];
|
|
1657
|
-
if (warmFiles.length > 0) {
|
|
1658
|
-
await igniteWarmFiles(warmupCwd, warmFiles, deps.runtime, deps.runtime.sessionGeneration, warmupDbg);
|
|
1659
|
-
}
|
|
1660
|
-
else {
|
|
1661
|
-
await igniteDominantLanguageWarm(languageRoot, deps.runtime, deps.runtime.sessionGeneration, warmupDbg);
|
|
1662
|
-
}
|
|
1663
|
-
logLatency({
|
|
1664
|
-
type: "phase",
|
|
1665
|
-
phase: "warmup_lsp_prewarm",
|
|
1666
|
-
filePath: warmupCwd,
|
|
1667
|
-
startedAt: new Date(lspPrewarmStartedAt).toISOString(),
|
|
1668
|
-
durationMs: Date.now() - lspPrewarmStartedAt,
|
|
1669
|
-
});
|
|
1670
|
-
warmupDbg(`warmup: lsp-prewarm done in ${Date.now() - lspPrewarmStartedAt}ms`);
|
|
1671
|
-
}
|
|
1672
|
-
warmupDbg(`warmup: total ${Date.now() - warmupStartedAt}ms`);
|
|
1673
|
-
}
|
|
1674
|
-
catch (err) {
|
|
1675
|
-
warmupDbg(`warmup: error ${err}`);
|
|
1676
|
-
// Allow a future session to retry the warmup.
|
|
1677
|
-
processGlobals.__piLensWarmupScheduled = false;
|
|
1678
|
-
}
|
|
1679
|
-
finally {
|
|
1680
|
-
logLatency({
|
|
1681
|
-
type: "phase",
|
|
1682
|
-
phase: "warmup_total",
|
|
1683
|
-
filePath: warmupCwd,
|
|
1684
|
-
startedAt: new Date(warmupStartedAt).toISOString(),
|
|
1685
|
-
durationMs: Date.now() - warmupStartedAt,
|
|
1686
|
-
});
|
|
1687
|
-
}
|
|
1688
|
-
})();
|
|
1689
|
-
}, warmupDelayMs);
|
|
1690
|
-
warmupTimer.unref?.();
|
|
1691
|
-
}
|
|
1692
|
-
const allowBootstrapTasks = startupMode === "full";
|
|
1693
|
-
const quickMode = startupMode === "quick";
|
|
1694
|
-
const { ctxCwd, getFlag, notify, dbg, log, runtime, cacheManager, ensureTool, cleanStaleTsBuildInfo, resetDispatchBaselines, resetLSPService, } = deps;
|
|
1695
|
-
// Lightweight phase timer — resets after each call so each log line shows
|
|
1696
|
-
// the cost of that phase alone, not cumulative time from session start.
|
|
1697
|
-
let _phaseT = Date.now();
|
|
1698
|
-
const phase = (name) => {
|
|
1699
|
-
dbg(`session_start phase ${name}: ${Date.now() - _phaseT}ms`);
|
|
1700
|
-
_phaseT = Date.now();
|
|
1701
|
-
};
|
|
1702
|
-
// #2467: `peek`, never a load. A metrics client that was never constructed
|
|
1703
|
-
// holds no per-session state to re-arm, so the reset is vacuous — and
|
|
1704
|
-
// loading the analyzer graph in order to reset nothing is the interactive-
|
|
1705
|
-
// path cost this issue removes.
|
|
1706
|
-
deps.bootstrap.peek()?.metricsClient.reset();
|
|
1707
|
-
getDiagnosticTracker().reset();
|
|
1708
|
-
clearFileTimeSessions();
|
|
1709
|
-
runtime.complexityBaselines.clear();
|
|
1710
|
-
resetDispatchBaselines(ctxCwd);
|
|
1711
|
-
// #806: clear the shared per-directory marker index and registered
|
|
1712
|
-
// topology-derived caches only at session start. Mid-session edits are NOT
|
|
1713
|
-
// detected; #805's tsconfig matcher cache follows the same registry reset.
|
|
1714
|
-
resetWorkspaceTopology();
|
|
1715
|
-
// #2000: opaque-recovery baselines are keyed cwd:generation (unreachable
|
|
1716
|
-
// after reset) and the git-worktree memo must re-probe after a session
|
|
1717
|
-
// that may have seen a non-git dir become one.
|
|
1718
|
-
resetOpaqueMutationState();
|
|
1719
|
-
// #2430: the observational net's pending baselines are keyed by tool-call
|
|
1720
|
-
// id and its content ledger describes the previous session's files; both
|
|
1721
|
-
// mis-answer forever without this. The learned attribution map is cleared
|
|
1722
|
-
// and then RE-PRIMED from this project's persisted file, so a tool an
|
|
1723
|
-
// earlier session observed is classified by name with no snapshot at all.
|
|
1724
|
-
resetObservedMutationNet();
|
|
1725
|
-
resetMutationAttribution();
|
|
1726
|
-
primePersistedMutationAttribution(ctxCwd);
|
|
1727
|
-
// #2026: pending auxiliary baselines are unreachable after generation bump.
|
|
1728
|
-
resetPendingAuxiliaryCoverage();
|
|
1729
|
-
// #817/#1199: Windows command resolution is cached per (command, canonical
|
|
1730
|
-
// effective child PATH/PATHEXT/cwd/per-drive provenance); drop it each
|
|
1731
|
-
// session so environment changes (e.g.
|
|
1732
|
-
// a tool installed mid-session or a differently-scoped managed bin) are
|
|
1733
|
-
// picked up fresh.
|
|
1734
|
-
resetSafeSpawnWindowsCommandCache();
|
|
1735
|
-
// #1266: install-failure suppression in dispatch runner availability
|
|
1736
|
-
// checkers (`noteInstallFailure`) is meant to last only until "the next
|
|
1737
|
-
// session", per #1222 — but nothing called the reset helper, so a
|
|
1738
|
-
// transient install failure (npm registry blip, locked file) suppressed
|
|
1739
|
-
// the tool for the rest of the process lifetime. Clear it here, same
|
|
1740
|
-
// boundary as the other per-session caches on this line.
|
|
1741
|
-
resetDispatchAvailabilityState();
|
|
1742
|
-
// Runner collect-later observations and their late findings are session
|
|
1743
|
-
// scoped. A new session must re-probe rather than inherit a process latch.
|
|
1744
|
-
resetObservedRunnerLatency();
|
|
1745
|
-
resetPendingRunnerFindings();
|
|
1746
|
-
// #1995: a command that TIMED OUT (not merely failed a probe) cools down
|
|
1747
|
-
// for the rest of the session so a hot loop of edits cannot hand the same
|
|
1748
|
-
// wedged .cmd shim a second budget. A new session may retry: the executable
|
|
1749
|
-
// or its environment may have changed.
|
|
1750
|
-
resetSpawnTimeoutCooldowns();
|
|
1751
|
-
// #1895: formatter PATH verdicts are session-scoped, but they live in
|
|
1752
|
-
// formatters.ts and are not covered by the dispatch generation. Re-arm them
|
|
1753
|
-
// here so a formatter installed or removed between sessions is observed by
|
|
1754
|
-
// the next session. `clearFormatterCache` drops the per-cwd selection cache
|
|
1755
|
-
// as well as the which latches, which is what a real re-probe needs: the
|
|
1756
|
-
// selection cache answers a same-cwd lookup before any probe runs, so
|
|
1757
|
-
// clearing the latches alone would leave the stale verdict standing in the
|
|
1758
|
-
// one directory the user is working in.
|
|
1759
|
-
clearFormatterCache();
|
|
1760
|
-
// #1535: same #1266 pattern, one caller earlier — the `gh auth token` latch
|
|
1761
|
-
// zizmor's spawn reads is process-lived storage whose durability contract is
|
|
1762
|
-
// per SESSION, not per process. Without this, a user who runs `gh auth
|
|
1763
|
-
// login` and starts a fresh session still reads the previous session's
|
|
1764
|
-
// stale "no token" verdict until the cooldown (if any) happens to expire.
|
|
1765
|
-
resetZizmorTokenAvailability();
|
|
1766
|
-
// #2455: same #1496/#1535 process-lifetime-latch shape, one caller later —
|
|
1767
|
-
// the `goClient`/`rustClient` singletons each hold their own
|
|
1768
|
-
// `createAvailabilityLatch()` outside the dispatch generation counter above
|
|
1769
|
-
// (they predate `createCwdCachedProbe`). A "missing" verdict never expires
|
|
1770
|
-
// on its own, so without these a go/cargo install between sessions stayed
|
|
1771
|
-
// unobserved for the rest of the process's life. Found by hand while
|
|
1772
|
-
// auditing #2455's detector widening, NOT surfaced by it: the sweep skips
|
|
1773
|
-
// any file exporting no reset at all, and neither client module exported
|
|
1774
|
-
// one, so no container predicate could have yielded them (#2455 fix round 4,
|
|
1775
|
-
// F5 — an earlier draft of this comment had the causality backwards).
|
|
1776
|
-
resetGoAvailability();
|
|
1777
|
-
resetRustAvailability();
|
|
1778
|
-
// psscriptanalyzer's three latches (interpreter, module, -File exec) are
|
|
1779
|
-
// module-local, so the generation counter above does not reach them
|
|
1780
|
-
// (#1490, #1540).
|
|
1781
|
-
resetPsScriptAnalyzerAvailability();
|
|
1782
|
-
// #1497: the install-class retry ceiling is terminal for a SESSION, but the
|
|
1783
|
-
// latches holding it live on process-lived client instances (bootstrap builds
|
|
1784
|
-
// them once). Same #1266/#1490/#1535 shape — without this line "terminal for
|
|
1785
|
-
// the session" is terminal for the process, and a repaired network never
|
|
1786
|
-
// re-earns its `go install`.
|
|
1787
|
-
resetInstallRetryLatches();
|
|
1788
|
-
// #1537: the lazy-install seam's hold (`gem install rubocop`, `rustup
|
|
1789
|
-
// component add`) is durable for a SESSION. Its map is module-local, so the
|
|
1790
|
-
// generation counter above does not reach it — same #1490/#1497 shape. It
|
|
1791
|
-
// belongs on THIS line and not on the turn_end path: the first cut cleared it
|
|
1792
|
-
// from `clearFormatterRuntimeState()`, which runs every turn, so a failing
|
|
1793
|
-
// install re-spawned every turn instead of once per session.
|
|
1794
|
-
resetLazyInstallAttempts();
|
|
1795
|
-
// #1897: direct-LSP negative availability and bare installer paths are
|
|
1796
|
-
// session facts. A command or PATH entry can appear between sessions.
|
|
1797
|
-
resetDirectLspCommandAvailability();
|
|
1798
|
-
// #2052: a root may not exist when the case-sensitivity probe first sees it.
|
|
1799
|
-
// Re-probe it at the next session boundary after the workspace is created.
|
|
1800
|
-
resetLSPCaseSensitivityState();
|
|
1801
|
-
resetResolvedPathCache();
|
|
1802
|
-
// #1653: pnpm/yarn/bun/npm's availability latches (package-manager.ts) are
|
|
1803
|
-
// module-local, same #1490/#1535 shape as the two lines above — the
|
|
1804
|
-
// generation counter above does not reach them. Without this line, a
|
|
1805
|
-
// pnpm/yarn/bun install done mid-day stayed invisible: a genuine "missing"
|
|
1806
|
-
// verdict from one session latched into the next until a process restart.
|
|
1807
|
-
_resetPackageManagerCache();
|
|
1808
|
-
// #1730: the managed-tool refresh budget is a per-SESSION allowance (at most
|
|
1809
|
-
// one `npm update` per session). Left process-lived, one long-running pi
|
|
1810
|
-
// would refresh a single tool at launch and never look at the other 21
|
|
1811
|
-
// again — the same latch shape as the two lines above. The weekly cadence
|
|
1812
|
-
// itself is NOT reset here: it lives in the persisted per-tool stamp, so
|
|
1813
|
-
// re-arming the budget only restores the session's right to ask.
|
|
1814
|
-
resetManagedToolRefreshSession();
|
|
1815
|
-
// #1123 item 3: a fresh session can re-report smells that a prior session
|
|
1816
|
-
// already surfaced once (see `checkSmellsAndNoteOnce`'s once-per-session gate).
|
|
1817
|
-
resetSmellsSessionState();
|
|
1818
|
-
// #2800 item 8: the situational-tool dead-weight observation (which tools
|
|
1819
|
-
// this session never activated or called) is session-scoped, so its sets
|
|
1820
|
-
// and once-latch re-arm here beside the other registered resets. Both hosts
|
|
1821
|
-
// open the session's row through startSituationalToolTelemetrySession()
|
|
1822
|
-
// BEFORE this handler runs, and a repeated MCP session_start refresh
|
|
1823
|
-
// legitimately re-runs this handler — so the reset only acts when no
|
|
1824
|
-
// telemetry session is open; clearing a live session here would wipe the
|
|
1825
|
-
// calls recorded before the refresh.
|
|
1826
|
-
resetSituationalToolTelemetry();
|
|
1827
|
-
// #1782: re-arm the workspace-diagnostics cache session clock. Entries
|
|
1828
|
-
// written before this instant assert findings from a session that is over,
|
|
1829
|
-
// so they must revalidate before they can be served as current again.
|
|
1830
|
-
resetWorkspaceDiagnosticsCacheSession(sessionStartMs);
|
|
1831
|
-
// Some embedders inject a capability-shaped Knip client rather than the
|
|
1832
|
-
// concrete KnipClient. Session reset is an optional lifecycle capability;
|
|
1833
|
-
// its absence must not make session_start fail.
|
|
1834
|
-
// #2467: `peek` for the same reason as `metricsClient.reset()` above —
|
|
1835
|
-
// an unloaded knip client has no session state to clear.
|
|
1836
|
-
deps.bootstrap.peek()?.knipClient.resetSessionState?.();
|
|
1837
|
-
// #1910: the tier-3 cascade outstanding-touch registry and its
|
|
1838
|
-
// sweep-scoped expired/evicted counters (clients/lsp/cascade-tier.ts) are
|
|
1839
|
-
// a per-SESSION claim about touches THIS session fired. #1899 bounded the
|
|
1840
|
-
// registry between sweeps but, by its own review, left the session
|
|
1841
|
-
// boundary unwired — a session replacement inherited the prior session's
|
|
1842
|
-
// outstanding touches and misattributed a stray expiry/eviction to the
|
|
1843
|
-
// next session's first reconcile gauge.
|
|
1844
|
-
resetCascadeTierSessionState();
|
|
1845
|
-
runtime.resetForSession(sessionStartMs);
|
|
1846
|
-
logLatency({
|
|
1847
|
-
type: "phase",
|
|
1848
|
-
phase: "session_start_runtime_reset",
|
|
1849
|
-
filePath: ctxCwd ?? process.cwd(),
|
|
1850
|
-
startedAt: new Date(handlerEnteredAt).toISOString(),
|
|
1851
|
-
durationMs: Date.now() - handlerEnteredAt,
|
|
1852
|
-
metadata: { mode: startupMode, reason: deps.sessionReason },
|
|
1853
|
-
});
|
|
1854
|
-
// #1019: log cleanup is deferred OFF the interactive critical path. It does
|
|
1855
|
-
// synchronous fs sweeps (~7ms) whose only consumer is an async notification —
|
|
1856
|
-
// nothing on the hot path reads its result — so running it inline just taxed
|
|
1857
|
-
// every session start. It still runs every session (correctness unchanged),
|
|
1858
|
-
// now on the next tick, and notifies when done. Errors are swallowed to a
|
|
1859
|
-
// dbg line: a best-effort log sweep must never surface as a session failure.
|
|
1860
|
-
const cleanupCwd = ctxCwd ?? process.cwd();
|
|
1861
|
-
setImmediate(() => {
|
|
1862
|
-
const logCleanupStartedAt = Date.now();
|
|
1863
|
-
try {
|
|
1864
|
-
const logCleanup = runLogCleanup(dbg);
|
|
1865
|
-
logLatency({
|
|
1866
|
-
type: "phase",
|
|
1867
|
-
phase: "session_start_log_cleanup",
|
|
1868
|
-
filePath: cleanupCwd,
|
|
1869
|
-
startedAt: new Date(logCleanupStartedAt).toISOString(),
|
|
1870
|
-
durationMs: Date.now() - logCleanupStartedAt,
|
|
1871
|
-
metadata: {
|
|
1872
|
-
mode: startupMode,
|
|
1873
|
-
reason: deps.sessionReason,
|
|
1874
|
-
deferred: true,
|
|
1875
|
-
},
|
|
1876
|
-
});
|
|
1877
|
-
if (logCleanup.cleaned > 0 || logCleanup.rotated > 0) {
|
|
1878
|
-
notify(`🧹 ${logCleanup.report}`, "info");
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
catch (err) {
|
|
1882
|
-
dbg(`session_start: deferred log cleanup failed: ${err}`);
|
|
1883
|
-
}
|
|
1884
|
-
});
|
|
1885
|
-
dbg(`session_start startup mode: ${startupMode}`);
|
|
1886
|
-
if (!getFlag("no-lsp")) {
|
|
1887
|
-
resetLSPService({ fast: true, reason: "session_start" });
|
|
1888
|
-
dbg("session_start: LSP service reset");
|
|
1889
|
-
dbg("session_start: phase0 workspace diagnostics observation enabled (capability probe only)");
|
|
1890
|
-
}
|
|
1891
|
-
const hasWorkspaceCwd = typeof ctxCwd === "string" && ctxCwd.length > 0;
|
|
1892
|
-
const cwd = ctxCwd ?? process.cwd();
|
|
1893
|
-
// #2526 review round 3, S1: this handler no longer predicts whether config
|
|
1894
|
-
// will resolve. `loadLSPConfig` itself publishes a `config_resolution_pending`
|
|
1895
|
-
// mark, from inside the function, at the instant a resolution is actually
|
|
1896
|
-
// attempted — see that function's doc comment (`clients/lsp/config.ts`) for
|
|
1897
|
-
// why a prediction mirrored against this handler's own gates drifted from
|
|
1898
|
-
// what index.ts and this handler's callers actually schedule.
|
|
1899
|
-
// #1228: generic atomic-write stages are shared by several project stores,
|
|
1900
|
-
// so the review-graph-specific sweep cannot own this namespace. Sweep the
|
|
1901
|
-
// project data roots and machine-global registry root once per session start;
|
|
1902
|
-
// this is fire-and-forget and bounded so it never delays startup.
|
|
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
|
-
}
|
|
1922
|
-
// #1609 review F1: sweepOwnStagingFiles does not recurse, so the installer's
|
|
1923
|
-
// bin/ and tools/ subdirectories (clients/installer/index.ts's
|
|
1924
|
-
// GITHUB_BIN_DIR / TOOLS_DIR, now atomic-write.js writers too) need their
|
|
1925
|
-
// own entries — otherwise an orphaned staging file from a kill mid-install
|
|
1926
|
-
// (this PR's own motivating scenario) never gets reaped, and unique
|
|
1927
|
-
// pid-thread-seq staging names mean repeated kills ACCUMULATE full-size
|
|
1928
|
-
// orphan binaries instead of being cleaned up.
|
|
1929
|
-
const globalDir = getGlobalPiLensDir();
|
|
1930
|
-
void sweepAtomicWriteStages([
|
|
1931
|
-
projectDataDir,
|
|
1932
|
-
path.join(projectDataDir, "cache"),
|
|
1933
|
-
path.join(projectDataDir, "sessions"),
|
|
1934
|
-
globalDir,
|
|
1935
|
-
path.join(globalDir, "bin"),
|
|
1936
|
-
path.join(globalDir, "tools"),
|
|
1937
|
-
]).catch(() => {
|
|
1938
|
-
// best-effort lifecycle cleanup — never fail session_start
|
|
1939
|
-
});
|
|
1940
|
-
if (quickMode) {
|
|
1941
|
-
runtime.projectRoot = cwd;
|
|
1942
|
-
const sequenceReadStartedAt = Date.now();
|
|
1943
|
-
const snapshotRoot = resolveSnapshotRoot(cwd);
|
|
1944
|
-
// #1785 F5 (round 4): the getter closes over `warmupOwnSnapshotRead`
|
|
1945
|
-
// directly (a `let` in this same `handleSessionStart` invocation's
|
|
1946
|
-
// scope), so it reads whatever value is current AT DEFERRED-RESOLVE
|
|
1947
|
-
// TIME — not whatever it was at this wiring instant. See
|
|
1948
|
-
// `retroactivelyHydrateAfterDeferredSequence`'s doc comment for the
|
|
1949
|
-
// full design.
|
|
1950
|
-
const { latestSeq, timedOut } = await readSequenceWithBudget({
|
|
1951
|
-
snapshotRoot,
|
|
1952
|
-
base: snapshotSequenceBase(snapshotRoot),
|
|
1953
|
-
cwd,
|
|
1954
|
-
runtime,
|
|
1955
|
-
sessionGeneration: runtime.sessionGeneration,
|
|
1956
|
-
dbg,
|
|
1957
|
-
onDeferredSequenceResolved: retroactivelyHydrateAfterDeferredSequence({
|
|
1958
|
-
getWarmupOwnSnapshotRead: () => warmupOwnSnapshotRead,
|
|
1959
|
-
snapshotRoot,
|
|
1960
|
-
runtime,
|
|
1961
|
-
dbg,
|
|
1962
|
-
}),
|
|
1963
|
-
});
|
|
1964
|
-
logLatency({
|
|
1965
|
-
type: "phase",
|
|
1966
|
-
phase: "session_start_sequence_read",
|
|
1967
|
-
filePath: cwd,
|
|
1968
|
-
startedAt: new Date(sequenceReadStartedAt).toISOString(),
|
|
1969
|
-
durationMs: Date.now() - sequenceReadStartedAt,
|
|
1970
|
-
metadata: { entries: latestSeq.fileSeqByPath.size, timedOut },
|
|
1971
|
-
});
|
|
1972
|
-
if (timedOut) {
|
|
1973
|
-
recordSnapshotSequenceTimeout({
|
|
1974
|
-
snapshotRoot,
|
|
1975
|
-
snapshotPath: getProjectSnapshotPath(snapshotRoot),
|
|
1976
|
-
});
|
|
1977
|
-
}
|
|
1978
|
-
runtime.seedProjectSequence?.(latestSeq.projectSeq, latestSeq.fileSeqByPath);
|
|
1979
|
-
const effectiveSeq = runtime.projectSeq ?? latestSeq.projectSeq;
|
|
1980
|
-
dbg(`session_start sequence: projectSeq=${effectiveSeq} fileSeqEntries=${latestSeq.fileSeqByPath.size}`);
|
|
1981
|
-
// #1162 review follow-up (P3): a timed-out read's cold sentinel is
|
|
1982
|
-
// `projectSeq: 0`, which is indistinguishable from a project's real
|
|
1983
|
-
// first-ever snapshot (persisted with `seq === 0` before any change was
|
|
1984
|
-
// logged). Feeding `effectiveSeq` (0) straight into the freshness check
|
|
1985
|
-
// would let that legit seq-0 snapshot match and hydrate as FRESH even
|
|
1986
|
-
// though the on-disk log has since moved past it. Use a value the
|
|
1987
|
-
// freshness check can never match (`snapshot.seq` is always >= 0) ONLY
|
|
1988
|
-
// for this gate when the read timed out — `runtime.projectSeq` itself
|
|
1989
|
-
// stays 0 (untouched), which is what Fix 1's advancement guard above
|
|
1990
|
-
// relies on to detect an in-window bump.
|
|
1991
|
-
const freshnessSeq = timedOut ? UNKNOWN_PROJECT_SEQ : effectiveSeq;
|
|
1992
|
-
const snapshotLoadStartedAt = Date.now();
|
|
1993
|
-
const snapshotPath = getProjectSnapshotPath(snapshotRoot);
|
|
1994
|
-
let snapshotBytes = 0;
|
|
1995
|
-
try {
|
|
1996
|
-
snapshotBytes = nodeFs.statSync(snapshotPath).size;
|
|
1997
|
-
}
|
|
1998
|
-
catch {
|
|
1999
|
-
// Missing snapshots are the normal cold-start case.
|
|
2000
|
-
}
|
|
2001
|
-
const snapshotBodyPresent = nodeFs.existsSync(snapshotPath) ||
|
|
2002
|
-
nodeFs.existsSync(getProjectSnapshotLegacyPath(snapshotRoot));
|
|
2003
|
-
const snapshotGate = loadSnapshotBodyUnlessStale({
|
|
2004
|
-
root: snapshotRoot,
|
|
2005
|
-
currentProjectSeq: freshnessSeq,
|
|
2006
|
-
dbg,
|
|
2007
|
-
});
|
|
2008
|
-
const snapshot = snapshotGate.snapshot;
|
|
2009
|
-
const snapshotFresh = isProjectSnapshotFresh(snapshot, freshnessSeq);
|
|
2010
|
-
const snapshotMissReason = describeSnapshotMiss(snapshot, freshnessSeq, {
|
|
2011
|
-
skippedStale: snapshotGate.skippedStale,
|
|
2012
|
-
bodyPresent: snapshotBodyPresent,
|
|
2013
|
-
});
|
|
2014
|
-
logLatency({
|
|
2015
|
-
type: "phase",
|
|
2016
|
-
phase: "session_start_snapshot_load",
|
|
2017
|
-
filePath: cwd,
|
|
2018
|
-
startedAt: new Date(snapshotLoadStartedAt).toISOString(),
|
|
2019
|
-
durationMs: Date.now() - snapshotLoadStartedAt,
|
|
2020
|
-
metadata: {
|
|
2021
|
-
bytes: snapshotBytes,
|
|
2022
|
-
fresh: snapshotFresh,
|
|
2023
|
-
seq: snapshot?.seq ?? null,
|
|
2024
|
-
reason: snapshotFresh ? undefined : snapshotMissReason,
|
|
2025
|
-
...(snapshotGate.skippedStale ? { skippedStale: true } : {}),
|
|
2026
|
-
...(timedOut ? { sequenceUnknown: true } : {}),
|
|
2027
|
-
},
|
|
2028
|
-
});
|
|
2029
|
-
logProjectSnapshotProbe({
|
|
2030
|
-
dbg,
|
|
2031
|
-
root: snapshotRoot,
|
|
2032
|
-
currentProjectSeq: freshnessSeq,
|
|
2033
|
-
snapshot,
|
|
2034
|
-
missReason: snapshotMissReason,
|
|
2035
|
-
});
|
|
2036
|
-
if (snapshotFresh) {
|
|
2037
|
-
hydrateRuntimeFromProjectSnapshot(runtime, snapshot);
|
|
2038
|
-
}
|
|
2039
|
-
const quickTools = [];
|
|
2040
|
-
if (!getFlag("no-lsp")) {
|
|
2041
|
-
quickTools.push("LSP Service");
|
|
2042
|
-
}
|
|
2043
|
-
log(`Active tools: ${quickTools.join(", ")}`);
|
|
2044
|
-
dbg(`session_start tools: ${quickTools.join(", ") || "deferred (quick mode)"}`);
|
|
2045
|
-
dbg("session_start: quick mode active - skipping slow tool probes, language profiling, preinstall, scans, and error debt baseline");
|
|
2046
|
-
// #1911: the debug line above says WHICH steps were skipped, but nothing
|
|
2047
|
-
// bounded or structured said so — quick mode's absence of work and an
|
|
2048
|
-
// absent LOGGER read identically in latency.log. This record is that
|
|
2049
|
-
// line's structured twin: one bounded gauge per quick-mode session_start,
|
|
2050
|
-
// naming exactly the step set skipped, so a reader can tell "quick mode
|
|
2051
|
-
// correctly skipped these" from "the probes silently never ran".
|
|
2052
|
-
logLatency({
|
|
2053
|
-
type: "phase",
|
|
2054
|
-
phase: "session_start_skipped_steps",
|
|
2055
|
-
filePath: cwd,
|
|
2056
|
-
durationMs: 0,
|
|
2057
|
-
metadata: {
|
|
2058
|
-
mode: startupMode,
|
|
2059
|
-
reason: deps.sessionReason,
|
|
2060
|
-
// #1911 review F3: this `if (quickMode)` branch is an early return —
|
|
2061
|
-
// the whole quick path — so there is no enumerable list of "steps
|
|
2062
|
-
// run vs. skipped" this array could be derived from mechanically. It
|
|
2063
|
-
// is DESCRIPTIVE documentation of what the early return skips,
|
|
2064
|
-
// matching the `dbg()` line above word for word; keep both in sync by
|
|
2065
|
-
// hand if either changes. It also does NOT narrow under
|
|
2066
|
-
// `getFlag("no-lsp")` — quick mode skips the same five steps either
|
|
2067
|
-
// way; the LSP flag instead affects `quickTools` above, a separate
|
|
2068
|
-
// record.
|
|
2069
|
-
steps: [
|
|
2070
|
-
"slow_tool_probes",
|
|
2071
|
-
"language_profiling",
|
|
2072
|
-
"tool_preinstall",
|
|
2073
|
-
"startup_scans",
|
|
2074
|
-
"error_debt_baseline",
|
|
2075
|
-
],
|
|
2076
|
-
},
|
|
2077
|
-
});
|
|
2078
|
-
const totalDurationMs = Date.now() - sessionStartMs;
|
|
2079
|
-
dbg(`session_start total: ${totalDurationMs}ms (interactive path)`);
|
|
2080
|
-
logLatency({
|
|
2081
|
-
type: "phase",
|
|
2082
|
-
phase: "session_start_total",
|
|
2083
|
-
filePath: cwd,
|
|
2084
|
-
startedAt: new Date(sessionStartMs).toISOString(),
|
|
2085
|
-
durationMs: totalDurationMs,
|
|
2086
|
-
metadata: {
|
|
2087
|
-
mode: startupMode,
|
|
2088
|
-
reason: deps.sessionReason,
|
|
2089
|
-
// #2129: the root-identity input decideSessionStart consulted,
|
|
2090
|
-
// alongside `mode` — every start reaching this line already
|
|
2091
|
-
// classified `primary`/`sequential-replacement` (a `secondary-root`
|
|
2092
|
-
// start returns before `handleSessionStart` is ever called).
|
|
2093
|
-
classification: deps.sessionStartClassification,
|
|
2094
|
-
// `undefined` is omitted by JSON.stringify. Keep unknown explicit so
|
|
2095
|
-
// strict log readers can distinguish it from legacy omission.
|
|
2096
|
-
sameRoot: deps.sessionStartSameRoot ?? "unknown",
|
|
2097
|
-
},
|
|
2098
|
-
});
|
|
2099
|
-
logHostReadyDelay(deps, cwd);
|
|
2100
|
-
emitSmellsSessionStartLine(dbg, sessionStartMs);
|
|
2101
|
-
return;
|
|
2102
|
-
}
|
|
2103
|
-
const tools = [];
|
|
2104
|
-
if (!getFlag("no-lsp"))
|
|
2105
|
-
tools.push("LSP Service");
|
|
2106
|
-
if (allowBootstrapTasks && !getFlag("no-lsp")) {
|
|
2107
|
-
const cleaned = cleanStaleTsBuildInfo(ctxCwd ?? process.cwd());
|
|
2108
|
-
if (cleaned.length > 0) {
|
|
2109
|
-
notify(`🧹 Deleted stale TypeScript build cache (${cleaned.map((f) => path.basename(f)).join(", ")}) — phantom errors suppressed.`, "warning");
|
|
2110
|
-
dbg(`session_start: cleaned stale tsbuildinfo: ${cleaned.join(", ")}`);
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
// Captured here (not at first use, below) because #1162's bounded read
|
|
2114
|
-
// needs it to gate the deferred reseed against a stale/moved-on session —
|
|
2115
|
-
// it's stable for the rest of this call (no further `resetForSession`
|
|
2116
|
-
// between here and the later uses of the same generation).
|
|
2117
|
-
const sessionGeneration = runtime.sessionGeneration;
|
|
2118
|
-
const sequenceReadStartedAt = Date.now();
|
|
2119
|
-
const snapshotRoot = resolveSnapshotRoot(cwd);
|
|
2120
|
-
const { latestSeq, timedOut } = await readSequenceWithBudget({
|
|
2121
|
-
snapshotRoot,
|
|
2122
|
-
base: snapshotSequenceBase(snapshotRoot),
|
|
2123
|
-
cwd,
|
|
2124
|
-
runtime,
|
|
2125
|
-
sessionGeneration,
|
|
2126
|
-
dbg,
|
|
2127
|
-
});
|
|
2128
|
-
logLatency({
|
|
2129
|
-
type: "phase",
|
|
2130
|
-
phase: "session_start_sequence_read",
|
|
2131
|
-
filePath: cwd,
|
|
2132
|
-
startedAt: new Date(sequenceReadStartedAt).toISOString(),
|
|
2133
|
-
durationMs: Date.now() - sequenceReadStartedAt,
|
|
2134
|
-
metadata: { entries: latestSeq.fileSeqByPath.size, timedOut },
|
|
2135
|
-
});
|
|
2136
|
-
if (timedOut) {
|
|
2137
|
-
// #1785: instrumentation only here, deliberately no
|
|
2138
|
-
// `onDeferredSequenceResolved` — unlike quick mode's cold path (which
|
|
2139
|
-
// does nothing when the snapshot isn't trusted), full mode's cold path
|
|
2140
|
-
// actively kicks off a real rescan below to rebuild this same state.
|
|
2141
|
-
// Retroactively hydrating from the on-disk snapshot once the deferred
|
|
2142
|
-
// read lands could overwrite that fresher, actively-computed state with
|
|
2143
|
-
// the stale disk copy — a regression, not a fix. See the PR body for the
|
|
2144
|
-
// full reasoning.
|
|
2145
|
-
recordSnapshotSequenceTimeout({
|
|
2146
|
-
snapshotRoot,
|
|
2147
|
-
snapshotPath: getProjectSnapshotPath(snapshotRoot),
|
|
2148
|
-
});
|
|
2149
|
-
}
|
|
2150
|
-
runtime.seedProjectSequence?.(latestSeq.projectSeq, latestSeq.fileSeqByPath);
|
|
2151
|
-
const effectiveSeq = runtime.projectSeq ?? latestSeq.projectSeq;
|
|
2152
|
-
dbg(`session_start sequence: projectSeq=${effectiveSeq} fileSeqEntries=${latestSeq.fileSeqByPath.size}`);
|
|
2153
|
-
// #1162 review follow-up (P3) — see the matching comment in the quick-mode
|
|
2154
|
-
// path above: a timed-out read's cold sentinel (`projectSeq: 0`) must
|
|
2155
|
-
// never satisfy the freshness check, or a project's real first-ever
|
|
2156
|
-
// snapshot (persisted at `seq === 0`) would hydrate as fresh despite the
|
|
2157
|
-
// on-disk log having moved past it. `runtime.projectSeq` itself is left
|
|
2158
|
-
// at 0, which Fix 1's advancement guard above depends on.
|
|
2159
|
-
const freshnessSeq = timedOut ? UNKNOWN_PROJECT_SEQ : effectiveSeq;
|
|
2160
|
-
const snapshotLoadStartedAt = Date.now();
|
|
2161
|
-
const snapshotPath = getProjectSnapshotPath(snapshotRoot);
|
|
2162
|
-
let snapshotBytes = 0;
|
|
2163
|
-
try {
|
|
2164
|
-
snapshotBytes = nodeFs.statSync(snapshotPath).size;
|
|
2165
|
-
}
|
|
2166
|
-
catch {
|
|
2167
|
-
// Missing snapshots are the normal cold-start case.
|
|
2168
|
-
}
|
|
2169
|
-
const snapshotBodyPresent = nodeFs.existsSync(snapshotPath) ||
|
|
2170
|
-
nodeFs.existsSync(getProjectSnapshotLegacyPath(snapshotRoot));
|
|
2171
|
-
const snapshotGate = loadSnapshotBodyUnlessStale({
|
|
2172
|
-
root: snapshotRoot,
|
|
2173
|
-
currentProjectSeq: freshnessSeq,
|
|
2174
|
-
dbg,
|
|
2175
|
-
});
|
|
2176
|
-
const snapshot = snapshotGate.snapshot;
|
|
2177
|
-
const snapshotFresh = isProjectSnapshotFresh(snapshot, freshnessSeq);
|
|
2178
|
-
const snapshotMissReason = describeSnapshotMiss(snapshot, freshnessSeq, {
|
|
2179
|
-
skippedStale: snapshotGate.skippedStale,
|
|
2180
|
-
bodyPresent: snapshotBodyPresent,
|
|
2181
|
-
});
|
|
2182
|
-
logLatency({
|
|
2183
|
-
type: "phase",
|
|
2184
|
-
phase: "session_start_snapshot_load",
|
|
2185
|
-
filePath: cwd,
|
|
2186
|
-
startedAt: new Date(snapshotLoadStartedAt).toISOString(),
|
|
2187
|
-
durationMs: Date.now() - snapshotLoadStartedAt,
|
|
2188
|
-
metadata: {
|
|
2189
|
-
bytes: snapshotBytes,
|
|
2190
|
-
fresh: snapshotFresh,
|
|
2191
|
-
seq: snapshot?.seq ?? null,
|
|
2192
|
-
reason: snapshotFresh ? undefined : snapshotMissReason,
|
|
2193
|
-
...(snapshotGate.skippedStale ? { skippedStale: true } : {}),
|
|
2194
|
-
...(timedOut ? { sequenceUnknown: true } : {}),
|
|
2195
|
-
},
|
|
2196
|
-
});
|
|
2197
|
-
logProjectSnapshotProbe({
|
|
2198
|
-
dbg,
|
|
2199
|
-
root: snapshotRoot,
|
|
2200
|
-
currentProjectSeq: freshnessSeq,
|
|
2201
|
-
snapshot,
|
|
2202
|
-
missReason: snapshotMissReason,
|
|
2203
|
-
});
|
|
2204
|
-
const freshSnapshot = snapshotFresh ? snapshot : null;
|
|
2205
|
-
if (freshSnapshot) {
|
|
2206
|
-
hydrateRuntimeFromProjectSnapshot(runtime, freshSnapshot);
|
|
2207
|
-
}
|
|
2208
|
-
// #699: a persisted `too-many-source-files` verdict is only reused while
|
|
2209
|
-
// still within its TTL (isStartupScanVerdictFresh) — the seq-based
|
|
2210
|
-
// freshness check above (isProjectSnapshotFresh) never fires for that
|
|
2211
|
-
// reason on its own, since pi-lens never wrote anything while
|
|
2212
|
-
// canWarmCaches was false, so the snapshot's seq never advances.
|
|
2213
|
-
const cachedStartupScan = freshSnapshot?.startupScan &&
|
|
2214
|
-
isStartupScanVerdictFresh(freshSnapshot.startupScan)
|
|
2215
|
-
? freshSnapshot.startupScan
|
|
2216
|
-
: undefined;
|
|
2217
|
-
const startupScanSource = cachedStartupScan ? "snapshot" : "computed";
|
|
2218
|
-
let startupScan;
|
|
2219
|
-
if (cachedStartupScan) {
|
|
2220
|
-
startupScan = { ...cachedStartupScan, cwd: path.resolve(cwd) };
|
|
2221
|
-
}
|
|
2222
|
-
else {
|
|
2223
|
-
const scanContextStartedAt = Date.now();
|
|
2224
|
-
startupScan = resolveStartupScanContext(cwd);
|
|
2225
|
-
logLatency({
|
|
2226
|
-
type: "phase",
|
|
2227
|
-
phase: "session_start_scan_context_compute",
|
|
2228
|
-
filePath: cwd,
|
|
2229
|
-
startedAt: new Date(scanContextStartedAt).toISOString(),
|
|
2230
|
-
durationMs: Date.now() - scanContextStartedAt,
|
|
2231
|
-
metadata: { mode: startupMode },
|
|
2232
|
-
});
|
|
2233
|
-
}
|
|
2234
|
-
phase("scan-context");
|
|
2235
|
-
dbg(`session_start scan-context source=${startupScanSource}`);
|
|
2236
|
-
const scanRoot = startupScan.projectRoot ?? cwd;
|
|
2237
|
-
// Both "tree is too big" verdicts still found a real project root — the
|
|
2238
|
-
// walk just refused to warm caches over it — so signals stay anchored at
|
|
2239
|
-
// that root rather than falling back to cwd (#758 added too-many-entries
|
|
2240
|
-
// as the sibling of too-many-source-files).
|
|
2241
|
-
const useScanRootForSignals = startupScan.canWarmCaches ||
|
|
2242
|
-
startupScan.reason === "too-many-source-files" ||
|
|
2243
|
-
startupScan.reason === "too-many-entries";
|
|
2244
|
-
const analysisRoot = useScanRootForSignals ? scanRoot : cwd;
|
|
2245
|
-
runtime.projectRoot = cwd;
|
|
2246
|
-
const languageProfileSource = freshSnapshot?.languageProfile
|
|
2247
|
-
? "snapshot"
|
|
2248
|
-
: "computed";
|
|
2249
|
-
const languageProfile = freshSnapshot?.languageProfile
|
|
2250
|
-
? freshSnapshot.languageProfile
|
|
2251
|
-
: detectProjectLanguageProfile(analysisRoot, startupScan.canWarmCaches ? undefined : []);
|
|
2252
|
-
phase("language-profile");
|
|
2253
|
-
dbg(`session_start language-profile source=${languageProfileSource}`);
|
|
2254
|
-
dbg(`session_start cwd: ${cwd}`);
|
|
2255
|
-
dbg(`session_start scan root: ${scanRoot} (warmCaches=${startupScan.canWarmCaches}${startupScan.reason ? `, reason=${startupScan.reason}` : ""})`);
|
|
2256
|
-
dbg(`session_start analysis root: ${analysisRoot}`);
|
|
2257
|
-
dbg(`session_start workspace root: ${runtime.projectRoot}`);
|
|
2258
|
-
dbg(`session_start language profile: ${languageProfile.detectedKinds.join(", ") || "none"}`);
|
|
2259
|
-
dbg(`session_start language counts: ${JSON.stringify(languageProfile.counts)} configured=${JSON.stringify(languageProfile.configured)}`);
|
|
2260
|
-
dbg(`session_start workspace cwd available: ${hasWorkspaceCwd}`);
|
|
2261
|
-
if (useScanRootForSignals && analysisRoot !== cwd) {
|
|
2262
|
-
dbg(`session_start: monorepo analysis root override -> ${analysisRoot}`);
|
|
2263
|
-
}
|
|
2264
|
-
// Slow-FS probe (#462): classify the workspace filesystem by measurement
|
|
2265
|
-
// (median fs.statSync cost) before any tree walk runs. WSL 9p mounts cost
|
|
2266
|
-
// ~1.3ms/stat vs ~17µs native — a 75x slowdown that turns a 5,000-file sync
|
|
2267
|
-
// walk into a multi-second TUI freeze. Logged to the latency log so
|
|
2268
|
-
// dogfooding can see the verdict; a visible notice fires when engaged so a
|
|
2269
|
-
// degraded scan is never mistaken for a silently-empty one.
|
|
2270
|
-
const slowFsVerdict = getSlowFsVerdict(analysisRoot);
|
|
2271
|
-
logLatency({
|
|
2272
|
-
type: "phase",
|
|
2273
|
-
phase: "slow_fs_probe",
|
|
2274
|
-
filePath: analysisRoot,
|
|
2275
|
-
durationMs: 0,
|
|
2276
|
-
metadata: {
|
|
2277
|
-
slow: slowFsVerdict.slow,
|
|
2278
|
-
medianStatMicros: slowFsVerdict.medianStatMicros,
|
|
2279
|
-
samples: slowFsVerdict.samples,
|
|
2280
|
-
},
|
|
2281
|
-
});
|
|
2282
|
-
dbg(`session_start slow-fs probe: slow=${slowFsVerdict.slow} medianStatMicros=${slowFsVerdict.medianStatMicros.toFixed(1)} samples=${slowFsVerdict.samples}`);
|
|
2283
|
-
if (slowFsVerdict.slow) {
|
|
2284
|
-
notify(`🐢 Slow filesystem detected (median ${slowFsVerdict.medianStatMicros.toFixed(0)}µs/stat) — reduced-scan mode engaged (set PI_LENS_ALLOW_SLOW_FS_SCAN=1 to override).`, "warning");
|
|
2285
|
-
}
|
|
2286
|
-
// Subagent light mode (#449 slice 0): detected once per session, alongside
|
|
2287
|
-
// the slow-FS probe above. Logged to the latency log so dogfooding can see
|
|
2288
|
-
// how often subagent fan-outs engage it and what identity they carry.
|
|
2289
|
-
const subagentSession = isSubagentSession();
|
|
2290
|
-
if (isWarmAttached()) {
|
|
2291
|
-
dbg("session_start lsp-warm: skipping pre-warm (attached to incumbent)");
|
|
2292
|
-
}
|
|
2293
|
-
else if (subagentSession) {
|
|
2294
|
-
const identity = getSubagentIdentity();
|
|
2295
|
-
logLatency({
|
|
2296
|
-
type: "phase",
|
|
2297
|
-
phase: "subagent_light_mode",
|
|
2298
|
-
filePath: analysisRoot,
|
|
2299
|
-
durationMs: 0,
|
|
2300
|
-
metadata: {
|
|
2301
|
-
runId: identity?.runId,
|
|
2302
|
-
agentName: identity?.agentName,
|
|
2303
|
-
marker: identity?.marker,
|
|
2304
|
-
},
|
|
2305
|
-
});
|
|
2306
|
-
dbg(`session_start subagent light mode: engaged (marker=${identity?.marker ?? "unknown"} runId=${identity?.runId ?? "unknown"} agentName=${identity?.agentName ?? "unknown"})`);
|
|
2307
|
-
}
|
|
2308
|
-
const lensLspEnabled = !getFlag("no-lsp");
|
|
2309
|
-
const startupDefaults = getDefaultStartupTools(languageProfile).filter((tool) => {
|
|
2310
|
-
if ((tool === "typescript-language-server" || tool === "pyright") &&
|
|
2311
|
-
!lensLspEnabled) {
|
|
2312
|
-
return false;
|
|
2313
|
-
}
|
|
2314
|
-
return true;
|
|
2315
|
-
});
|
|
2316
|
-
if (!allowBootstrapTasks) {
|
|
2317
|
-
dbg("session_start: skipping tool preinstall (startup mode)");
|
|
2318
|
-
}
|
|
2319
|
-
else if (startupDefaults.length > 0) {
|
|
2320
|
-
dbg(`session_start: pre-install defaults -> ${startupDefaults.join(", ")}`);
|
|
2321
|
-
firePreinstallDefaults(ensureTool, dbg, startupDefaults);
|
|
2322
|
-
}
|
|
2323
|
-
else {
|
|
2324
|
-
dbg("session_start: no language defaults selected for pre-install");
|
|
2325
|
-
}
|
|
2326
|
-
const startupScansEnabled = deps.projectConfig?.startup?.scans?.enabled ??
|
|
2327
|
-
deps.globalConfig?.startup?.scans?.enabled ??
|
|
2328
|
-
true;
|
|
2329
|
-
const startupScansWillRun = allowBootstrapTasks && startupScan.canWarmCaches && startupScansEnabled;
|
|
2330
|
-
const jstsHeavyScansWillRun = startupScansWillRun && canRunStartupHeavyScans(languageProfile, "jsts");
|
|
2331
|
-
if (allowBootstrapTasks) {
|
|
2332
|
-
scheduleDeferredToolProbes(deps, languageProfile, startupDefaults, jstsHeavyScansWillRun, dbg);
|
|
2333
|
-
scheduleManagedToolRefresh(dbg);
|
|
2334
|
-
}
|
|
2335
|
-
if (allowBootstrapTasks) {
|
|
2336
|
-
// Fire-and-forget like other tool probes
|
|
2337
|
-
void probePrettierInstall(ensureTool, dbg, analysisRoot);
|
|
2338
|
-
}
|
|
2339
|
-
else {
|
|
2340
|
-
dbg("session_start: skipping prettier preinstall probe (startup mode)");
|
|
2341
|
-
}
|
|
2342
|
-
// #2467: the first point on the FULL-mode path that genuinely needs the
|
|
2343
|
-
// analyzer clients, so it is where the load is paid. Quick mode — the
|
|
2344
|
-
// process's first session, the one the user is waiting on — returns above
|
|
2345
|
-
// and never reaches here. Fail open: an unavailable bootstrap drops these
|
|
2346
|
-
// three entries from the "Active tools" line rather than failing the start.
|
|
2347
|
-
const summaryClients = await deps.bootstrap.request("session-start-tools");
|
|
2348
|
-
const detectedRunner = summaryClients?.testRunnerClient.detectRunner(analysisRoot);
|
|
2349
|
-
phase("test-runner-detect");
|
|
2350
|
-
if (detectedRunner)
|
|
2351
|
-
tools.push(`Test runner (${detectedRunner.runner})`);
|
|
2352
|
-
if (await summaryClients?.goClient.isGoAvailableAsync())
|
|
2353
|
-
tools.push("Go (go vet)");
|
|
2354
|
-
if (await summaryClients?.rustClient.isAvailableAsync())
|
|
2355
|
-
tools.push("Rust (cargo)");
|
|
2356
|
-
log(`Active tools: ${tools.join(", ")}`);
|
|
2357
|
-
dbg(`session_start tools: ${tools.join(", ")}`);
|
|
2358
|
-
const agentStartupGuidance = SESSION_START_GUIDANCE;
|
|
2359
|
-
runtime.projectRulesScan = scanProjectRules(analysisRoot);
|
|
2360
|
-
saveRuntimeProjectSnapshot({
|
|
2361
|
-
cwd: snapshotRoot,
|
|
2362
|
-
runtime,
|
|
2363
|
-
startupScan,
|
|
2364
|
-
languageProfile,
|
|
2365
|
-
dbg,
|
|
2366
|
-
});
|
|
2367
|
-
phase("project-rules");
|
|
2368
|
-
if (runtime.projectRulesScan.hasCustomRules) {
|
|
2369
|
-
const ruleCount = runtime.projectRulesScan.rules.length;
|
|
2370
|
-
const sources = [
|
|
2371
|
-
...new Set(runtime.projectRulesScan.rules.map((r) => r.source)),
|
|
2372
|
-
];
|
|
2373
|
-
dbg(`session_start: found ${ruleCount} project rule(s) from ${sources.join(", ")}`);
|
|
2374
|
-
}
|
|
2375
|
-
else {
|
|
2376
|
-
dbg("session_start: no project rules found");
|
|
2377
|
-
}
|
|
2378
|
-
cacheManager.writeCache("session-start-guidance", { content: agentStartupGuidance.join("\n") }, analysisRoot);
|
|
2379
|
-
// `sessionGeneration` was captured earlier (#1162's bounded sequence read
|
|
2380
|
-
// needs it before this point) and is stable across this whole call.
|
|
2381
|
-
if (!allowBootstrapTasks) {
|
|
2382
|
-
dbg("session_start: skipping startup background scans (startup mode)");
|
|
2383
|
-
}
|
|
2384
|
-
else if (!startupScansEnabled) {
|
|
2385
|
-
dbg("session_start: skipping startup background scans (disabled by config)");
|
|
2386
|
-
recordDegradationOnce({
|
|
2387
|
-
kind: "startup-analyzer-disabled",
|
|
2388
|
-
subject: "startup-scans",
|
|
2389
|
-
reason: "skipped (disabled by config)",
|
|
2390
|
-
});
|
|
2391
|
-
}
|
|
2392
|
-
else if (!startupScan.canWarmCaches) {
|
|
2393
|
-
dbg(`session_start: skipping heavy scans (${startupScan.reason ?? "unknown"})`);
|
|
2394
|
-
dbg(`session_start: skipping TODO scan (${startupScan.reason ?? "unknown"})`);
|
|
2395
|
-
// #775: mirror the slow-fs notify above — a size-skipped warm pipeline is
|
|
2396
|
-
// otherwise silent (debug-log only), so a large project can look like
|
|
2397
|
-
// pi-lens just isn't scanning anything. Fires ONCE per session (this
|
|
2398
|
-
// `else` branch runs once per handleSessionStart call — the
|
|
2399
|
-
// dominant-language LSP pre-warm skip further down reuses this same
|
|
2400
|
-
// verdict rather than notifying a second time).
|
|
2401
|
-
if (startupScan.reason === "too-many-source-files" ||
|
|
2402
|
-
startupScan.reason === "too-many-entries") {
|
|
2403
|
-
const overrideHint = startupScan.reason === "too-many-entries"
|
|
2404
|
-
? ` (set PI_LENS_STARTUP_SCAN_MAX_ENTRIES=<n> to override the ${getStartupScanMaxEntries()}-entry cap)`
|
|
2405
|
-
: "";
|
|
2406
|
-
notify(`📦 Project-size limits disabled background warm scans (heavy scans, TODO scan, LSP pre-warm)${overrideHint}.`, "warning");
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
else {
|
|
2410
|
-
scheduleStartupScans(deps, runtime, sessionGeneration, analysisRoot, snapshotRoot, languageProfile, dbg);
|
|
2411
|
-
}
|
|
2412
|
-
// LSP warm files — deferred to the next event-loop turn so the config walk
|
|
2413
|
-
// (several ENOENT readFile calls up the directory tree) never runs on the
|
|
2414
|
-
// interactive path. setImmediate guarantees handleSessionStart has already
|
|
2415
|
-
// resolved before loadLSPConfig is even called.
|
|
2416
|
-
//
|
|
2417
|
-
// #449 slice 0: skip both the explicit warmFiles warm and the
|
|
2418
|
-
// dominant-language auto-warm inside a subagent session — a fan-out of N
|
|
2419
|
-
// subagents otherwise pays N full LSP pre-warms in the same cwd. Per-edit
|
|
2420
|
-
// LSP dispatch is untouched (see `pipeline.ts`), so a subagent that
|
|
2421
|
-
// actually edits code still gets diagnostics; it just spawns the server
|
|
2422
|
-
// lazily on first edit instead of eagerly at session start.
|
|
2423
|
-
if (subagentSession) {
|
|
2424
|
-
dbg("session_start lsp-warm: skipping pre-warm (subagent session)");
|
|
2425
|
-
}
|
|
2426
|
-
else if (!getFlag("no-lsp") && allowBootstrapTasks) {
|
|
2427
|
-
setImmediate(() => {
|
|
2428
|
-
void loadLSPConfig(cwd).then((lspConfig) => {
|
|
2429
|
-
const warmFiles = lspConfig.warmFiles ?? [];
|
|
2430
|
-
dbg(`session_start lsp-config: loaded (${warmFiles.length} warm file(s) configured)`);
|
|
2431
|
-
if (warmFiles.length > 0) {
|
|
2432
|
-
igniteWarmFiles(cwd, warmFiles, runtime, sessionGeneration, dbg).catch((err) => dbg(`session_start lsp-warm: unhandled error: ${err}`));
|
|
2433
|
-
}
|
|
2434
|
-
else if (startupScan.canWarmCaches) {
|
|
2435
|
-
// No explicit warmFiles — pre-spawn just the dominant language's
|
|
2436
|
-
// LSP so the first edit doesn't pay the cold-spawn stall (#203).
|
|
2437
|
-
// Only do the auto-discovery warm on guarded real project roots; on
|
|
2438
|
-
// home/no-project/too-large roots this source walk can become the same
|
|
2439
|
-
// delayed background tree scan that the startup-scan guard prevents.
|
|
2440
|
-
igniteDominantLanguageWarm(analysisRoot, runtime, sessionGeneration, dbg).catch((err) => dbg(`session_start lsp-warm: unhandled dominant error: ${err}`));
|
|
2441
|
-
}
|
|
2442
|
-
else {
|
|
2443
|
-
dbg(`session_start lsp-warm: skipping dominant-language auto-warm (${startupScan.reason ?? "unknown"})`);
|
|
2444
|
-
}
|
|
2445
|
-
});
|
|
2446
|
-
});
|
|
2447
|
-
phase("lsp-config");
|
|
2448
|
-
}
|
|
2449
|
-
setSessionLanguages(languageProfile.detectedKinds);
|
|
2450
|
-
const totalDurationMs = Date.now() - sessionStartMs;
|
|
2451
|
-
dbg(`session_start total: ${totalDurationMs}ms (interactive path; background tasks may continue)`);
|
|
2452
|
-
logLatency({
|
|
2453
|
-
type: "phase",
|
|
2454
|
-
phase: "session_start_total",
|
|
2455
|
-
filePath: cwd,
|
|
2456
|
-
startedAt: new Date(sessionStartMs).toISOString(),
|
|
2457
|
-
durationMs: totalDurationMs,
|
|
2458
|
-
metadata: {
|
|
2459
|
-
mode: startupMode,
|
|
2460
|
-
reason: deps.sessionReason,
|
|
2461
|
-
// #2129: see the quick-mode session_start_total record above.
|
|
2462
|
-
classification: deps.sessionStartClassification,
|
|
2463
|
-
// Keep the full path's durable shape identical to quick mode.
|
|
2464
|
-
sameRoot: deps.sessionStartSameRoot ?? "unknown",
|
|
2465
|
-
},
|
|
2466
|
-
});
|
|
2467
|
-
logHostReadyDelay(deps, cwd);
|
|
2468
|
-
emitSmellsSessionStartLine(dbg, sessionStartMs);
|
|
2469
|
-
}
|
|
2470
|
-
/**
|
|
2471
|
-
* #1123 item 3: one bounded `session_start` line surfacing smells the manual
|
|
2472
|
-
* `npm run logs:smells` analyzer would otherwise only catch on demand — see
|
|
2473
|
-
* `clients/smells-rollup.ts` for the tail-scan cost bound and threshold
|
|
2474
|
-
* gating. Never throws: a rollup miss must not break session_start.
|
|
2475
|
-
*/
|
|
2476
|
-
function emitSmellsSessionStartLine(dbg, sessionStartMs) {
|
|
2477
|
-
try {
|
|
2478
|
-
const line = formatSmellsSessionStartLine(countRecentSmells(undefined, sessionStartMs));
|
|
2479
|
-
if (line)
|
|
2480
|
-
dbg(line);
|
|
2481
|
-
}
|
|
2482
|
-
catch {
|
|
2483
|
-
// best-effort — smells rollup must never break session_start
|
|
2484
|
-
}
|
|
2485
|
-
}
|