@herbertgao/pi-extensions 2026.8.7 → 2026.8.8
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 +7 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +63 -15
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +78 -16
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +13 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +17 -6
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +4 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +154 -96
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +3 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +7 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +3 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +137 -96
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +44 -32
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +56 -34
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +50 -4
- package/node_modules/@narumitw/pi-btw/package.json +2 -2
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
- package/node_modules/pi-lens/CHANGELOG.md +90 -0
- package/node_modules/pi-lens/README.md +106 -0
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +97 -6
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +16 -6
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +14 -0
- package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +22 -1
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +8 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +10 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +19 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +35 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +8 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +110 -23
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +117 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +16 -14
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +8 -9
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +22 -0
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +5 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +7 -2
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +27 -2
- package/node_modules/pi-lens/dist/clients/lsp/client.js +144 -5
- package/node_modules/pi-lens/dist/clients/lsp/index.js +151 -6
- package/node_modules/pi-lens/dist/clients/module-report.js +14 -1
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +18 -9
- package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +7 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +88 -19
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +44 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +41 -0
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +47 -2
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
- package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
- package/node_modules/pi-lens/dist/index.js +1224 -393
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +10 -3
- package/node_modules/pi-lens/package.json +18 -5
- package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
- package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
- package/node_modules/pi-web-access/CHANGELOG.md +14 -0
- package/node_modules/pi-web-access/README.md +6 -5
- package/node_modules/pi-web-access/chrome-cookies.ts +86 -15
- package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
- package/node_modules/pi-web-access/extract.ts +12 -2
- package/node_modules/pi-web-access/firecrawl.ts +18 -2
- package/node_modules/pi-web-access/github-extract.ts +65 -36
- package/node_modules/pi-web-access/index.ts +19 -4
- package/node_modules/pi-web-access/openai-search.ts +17 -6
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/pdf-extract.ts +18 -5
- package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
- package/package.json +9 -9
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-plugins/fast-mode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Fast service-tier extension for pi-agent.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fast",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
43
|
+
"@earendil-works/pi-tui": "0.83.0",
|
|
43
44
|
"@types/node": "26.1.2",
|
|
44
45
|
"oxfmt": "0.62.0",
|
|
45
46
|
"oxlint": "1.77.0",
|
|
@@ -51,17 +52,14 @@
|
|
|
51
52
|
"@pi-plugins/tsconfig": "0.0.0"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
54
|
-
"@earendil-works/pi-coding-agent": "*"
|
|
55
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
56
|
+
"@earendil-works/pi-tui": "*"
|
|
55
57
|
},
|
|
56
58
|
"pi": {
|
|
57
59
|
"extensions": [
|
|
58
60
|
"./dist/index.mjs"
|
|
59
61
|
]
|
|
60
62
|
},
|
|
61
|
-
"inlinedDependencies": {
|
|
62
|
-
"@earendil-works/pi-tui": "0.83.0",
|
|
63
|
-
"marked": "18.0.5"
|
|
64
|
-
},
|
|
65
63
|
"scripts": {
|
|
66
64
|
"build": "tsdown",
|
|
67
65
|
"check": "pnpm run lint && pnpm run check-types && pnpm run format:check",
|
|
@@ -16,6 +16,96 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
16
16
|
|
|
17
17
|
### Security
|
|
18
18
|
|
|
19
|
+
## [4.1.1] - 2026-08-21
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Prompt-cache misses now say why, and mixed injections split by source (refs #1071)** — a `cache_usage` record carries `interTurnGapMs` and a `cacheMissCause` verdict of `ttl-expired`, `prefix-broke`, `partial-eviction`, or `unknown`, so the dominant cache cost is readable from `latency.log` instead of reconstructed by hand-joining timestamps. The gap is measured to request time, so a slow response is not mistaken for idle time; `gapBasis` names which endpoint produced the value. A `cache_context` record splits a mixed injection payload by contributing source, with per-source message count, characters, bytes, and an estimated token count. Token figures use a documented four-chars-per-token estimate and are never presented as provider-measured. The TTL threshold defaults to 60s and is overridable via `PI_LENS_PROVIDER_CACHE_TTL_MS`.
|
|
24
|
+
|
|
25
|
+
- **Quick-mode session_start now logs which steps it skipped (closes #1911)** — quick mode silently skipped slow tool probes, language profiling, preinstall, startup scans, and the error-debt baseline, with no record either way. `session_start` now emits one bounded `session_start_skipped_steps` latency record naming the skipped step set, so a reader can tell "quick mode correctly skipped these" from "the probes silently never ran".
|
|
26
|
+
|
|
27
|
+
- **Runner parsers are now held to real binary output (refs #1937)** — a parser written from documentation can be wrong in a way no hand-authored test sees, because the test asserts the same imagined shape. Captured real-bytes fixtures now live in `tests/fixtures/runner-output/`, each with a machine-generated provenance header naming the tool, version, and exact argv; a replay suite feeds those bytes through the runner, fails when the parser finds nothing, and fails when the runner's invocation drifts from the argv the fixture recorded. The sweep found four live instances: taplo spawned a `--output=json` flag taplo rejects, stylelint read stdout while stylelint 16+ reports on stderr, phpstan read the error COUNT as if it were the error array and discarded its file-independent findings entirely, and sqlfluff inserted `--dialect` between `--format` and its value so no unconfigured SQL project was ever linted. A scheduled `parser-smoke` lane now runs the tier-1 tools' real binaries over a planted violation, with a pass floor so a run that installed nothing cannot report green.
|
|
28
|
+
|
|
29
|
+
- **A runner that reads nothing out of a failing tool's output now leaves a record (closes #1948)** — when a dispatch runner's tool exits nonzero, prints output, and the runner's parser extracts zero diagnostics, the degradation ledger gets a bounded `runner-parsed-nothing` row naming the tool, the exit status, the output length, and the first output line. Until now that case was byte-for-byte identical to a clean file in the worklog, which is how five parser bugs (vale, taplo, stylelint, phpstan, sqlfluff) reported clean files for months. A genuinely clean run records nothing, so the ledger does not fill up with one row per save. The gate lives in one shared helper, `parseToolRun`, that fourteen runners now share, and a sweep test keeps a new runner from silently opting out through any of the older spawn-outcome primitives.
|
|
30
|
+
|
|
31
|
+
- **Mechanical lint batch: actionlint, PR-title lint, markdownlint, OSV scan (refs #1844)** — a new `lint.yml` workflow dogfoods actionlint against every workflow file, validates that every PR title carries a conventional prefix and an issue reference **in the title itself** (`scripts/check-pr-title.mjs` — a reference living only in the PR body no longer counts, since it never reaches the merge-commit subject), and lints Markdown docs with `markdownlint-cli2` under a repo-tuned config. A separate `osv-scan.yml` runs an advisory weekly `osv-scanner` sweep plus a scan on lockfile-touching PRs, writing results to the job summary.
|
|
32
|
+
|
|
33
|
+
- **Vale prose lint with a vendored Google style base (refs #1844)** — `.vale.ini` at the repo root scopes Vale to `docs/`, `AGENTS.md`, `CLAUDE.md`, `README.md`, `CONTRIBUTING.md`, `.claude/agents/*.md`, and `.changelog/*.md`, using the Google style package vendored under `.vale/styles/Google/` (no `vale sync` at CI time) plus a `pi-lens/` style with three house rules: no `please` in instructions, an em-dash-density check, and a long-sentence check. A new advisory `vale` job in `lint.yml` runs it on every PR. Because the in-product `vale` runner (`clients/dispatch/runners/vale.ts`) activates on `.vale.ini` presence, dogfood sessions now surface Vale findings on doc edits too.
|
|
34
|
+
|
|
35
|
+
- **Husky-managed local git hooks (closes #1804)** — `npm install` now wires
|
|
36
|
+
a pre-commit hook (changelog fragment validation + `npm run lint`) and a
|
|
37
|
+
pre-push hook (build + a capped, full-path-resolved targeted `vitest`
|
|
38
|
+
selection for changed files, never the full suite; degrades to build-only
|
|
39
|
+
past 25 matched test files or a 2-minute shared test-lock wait). Both are
|
|
40
|
+
skippable with `PI_LENS_SKIP_HOOKS` set to any non-empty value, which
|
|
41
|
+
agents and CI should set; humans leave hooks on. Hook install itself is
|
|
42
|
+
skipped for CI and production/consumer installs, and never fails
|
|
43
|
+
`npm install` on error.
|
|
44
|
+
|
|
45
|
+
- **latency.log now says whether the LSP pool reused a client or spawned one (closes #1934)** — `lsp_client_selected` fired 5601 times in a 20.8h dogfood window carrying only `{serverId, candidateCount}`, so nothing in the log said whether the pool served a warm client or paid for a language-server spawn. This is the most expensive cache in pi-lens: a miss costs a process. The only estimate was a cross-record inference against `lsp_launch_candidate_success`, which counts something else, so a regression that halved pool reuse was invisible. The record now carries an `outcome` field with one of three values on the same record and the same denominator. `warm-reuse` means the pool served an already-connected client. `cold-spawn` means this selection waited on a spawn, whether it started that spawn or joined one already in flight, so a session-start burst where many files wait on the same spawn reads as several cold spawns and deflates the apparent reuse rate for that window. `spawn-failure` means a spawn ran and failed. Reuse rate is `warm-reuse / (warm-reuse + cold-spawn)`. A spawn failure is deliberately distinct from a clean decline: no root, a breaker already open, host trust refused, or a binary absent while installs are disabled. Declines emit no outcome record and keep their existing ones, so `lsp_client_unavailable` and the `outcome` field can both describe the same event and are counted separately. `getWarmClientForFile`, the warm-only lookup the cascade quiet window and read expansion call per file, previously returned `undefined` and emitted nothing; it now emits a bounded `lsp_warm_client_missing` record naming the candidate server and root, rising-edge per candidate set with the exact count in the degradation ledger.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **Search credit ignores pipe truncation (closes #1908)** — `grep -C2 pattern
|
|
50
|
+
file | head -1` no longer credits context lines the pipe cut off. The
|
|
51
|
+
read-guard now detects a truncating pipe tail (`head`, `tail`, `sed q`)
|
|
52
|
+
downstream of a line-numbered grep and falls back to match-line-only
|
|
53
|
+
credit, so a later edit to the uncredited context still requires a read.
|
|
54
|
+
|
|
55
|
+
- **Cascade tier-3 registry now resets at session_start (closes #1910)** — the outstanding-touch registry and its sweep-scoped expired/evicted counters (`clients/lsp/cascade-tier.ts`) used to survive a session replacement, so a new session inherited the prior session's outstanding touches and a stray eviction or expiry landed on the next session's first reconcile gauge. `handleSessionStart` now clears both, primary-only, same as every other per-session latch in that reset block.
|
|
56
|
+
|
|
57
|
+
- **Read-guard eviction telemetry is now always on (closes #1913)** — the
|
|
58
|
+
first time a file's read-guard record cap trims in a session, it now emits
|
|
59
|
+
a `read_cap_trimmed` log line (file, evicted count, credit-vs-genuine
|
|
60
|
+
split, raw read count) regardless of `PI_LENS_READ_GUARD_VERBOSE`, so a
|
|
61
|
+
live eviction regression is visible by default instead of only under
|
|
62
|
+
verbose logging. Later trims on the same file keep updating the running
|
|
63
|
+
totals (queryable via `ReadGuard.getTrimStats`, and via the degradation
|
|
64
|
+
ledger's own tally) without flooding the log on every subsequent trim.
|
|
65
|
+
|
|
66
|
+
- **agent_settled no longer crashes with extension_error when the session is replaced mid-run** —
|
|
67
|
+
stale extension ctx reads are guarded and the ambient abort signal is
|
|
68
|
+
cleared (closes #1924; thanks @Pluto-Yt).
|
|
69
|
+
|
|
70
|
+
- **A session event on a replaced session no longer reports a pi-lens handler error (closes #1925)** — pi invalidates a captured extension ctx on `newSession`/`fork`/`switchSession`/`reload`, and an event already queued when that happens still reaches pi-lens carrying the dead ctx. `tool_result`, `turn_start`, `agent_end`, and `turn_end` each read a ctx property before any guard, so the SDK's `assertActive()` error was caught by `ExtensionRunner.emit` and reported as an extension error against pi-lens. That report named the wrong cause, said nothing about which handler was affected, and counted nothing. All five session-event registrations, including `agent_settled` (#1924), now go through one wrapper in `clients/session-event-guard.ts`: it probes the ctx once before dispatch, still classifies a stale error that arrives mid-handler, and records every skip in the degradation ledger under `extension-ctx-stale` plus a bounded `session_event_stale_ctx_skip` row in `latency.log`, keyed by event name. A new registered-or-fail sweep reds any future `pi.on` or `pi.on?.` registration that is neither wrapped nor given a stated reason.
|
|
71
|
+
|
|
72
|
+
- **Git installs start about 750ms faster (refs #1926)** — pi supplies `typebox` and `@earendil-works/pi-tui` from its own runtime, but pi-lens declared both as runtime dependencies. A `git:` install therefore vendored a private second copy of each, and Node evaluated that whole extra module graph every time the extension loaded. Both are now optional peer dependencies, so nothing vendors them. The `PI_TIMING` module import for the dogfood git install drops from 941ms to 147-196ms, and the extensions block drops from 2165ms to 1449ms. One trade-off, stated plainly: because the bare specifiers no longer resolve on disk, pi loads the entry through jiti's transform path instead of a native import, so the FIRST start after an install, an update, or a jiti cache eviction is slower, around 5.6 to 8.2 seconds on the 4MB bundle. Every start after that is the fast one.
|
|
73
|
+
|
|
74
|
+
- **Updating pi-lens no longer costs a five-second first session (refs #1926)** — pi loads the extension through jiti, which transforms the ~4MB `dist/index.js` bundle and caches the result. Every `git:` install or update produced a new bundle, so the next interactive session paid that transform: 4847ms of `module import`, against a 138ms steady state. The `prepare` chain now runs the transform itself, in `scripts/warm-loader-cache.mjs`, writing the same cache entry pi reads. The first session after an update is warm. The step runs last, is best-effort, and never fails an install; set `PI_LENS_SKIP_WARM_CACHE` to skip it. Each run appends one line to `~/.pi-lens/install.log`.
|
|
75
|
+
|
|
76
|
+
- **A session swap that starves `session_start` or `context` is now counted, not misreported as a pi-lens crash (closes #1929)** — both handlers survived a dead extension ctx already, but each logged it as `session_start crashed: …` or `context event error: …` and recorded nothing, so a replacement that kept starving them was invisible in aggregate. Both now run through the shared stale-ctx wrapper and leave one bounded `extension-ctx-stale` record keyed to the event name. `context` uses a new value-returning wrapper variant that states its stale-path answer explicitly: `undefined`, pi's "this extension contributed nothing", so the host keeps its own message list rather than receiving a half-built injection. Seven of twelve `pi.on` registrations are now wrapped, up from five.
|
|
77
|
+
|
|
78
|
+
- **Diagnostics scans stop logging a phantom ast-grep cache-stats record (closes #1935)** — `#1715` already grows the tree-sitter parse cache to span a project scan's file count, so the diagnostics-scan path does not run at the 50-entry interactive default; that part of `#1935` was resolved before this fix landed. What was still broken: every scan logged a second `cache_stats` record under scope `project_diagnostics_ast_grep_scan`, always all-zero, because ast-grep-napi parses through its own native engine and never touches the WASM tree cache that record claimed to measure. That vacuous record is now removed instead of wired to a cache it structurally cannot use; its one real value — ast-grep's own duration and file count, often a scan's most expensive phase — now rides an `astGrep` sub-field on the surviving `project_diagnostics_scan` record instead of disappearing. Added a memory-safety test pinning `treeCacheTotalBytes` at the existing 500-entry scan ceiling (`TREE_CACHE_SCAN_CAPACITY_CEILING`) using realistic, varying per-file sizes: worst case is 6-18MB of resident source bytes at that ceiling, and `TreeCache` enforces no per-entry byte cap of its own, only the entry count. A regression test pins the scanner to never emit the dead ast-grep scope.
|
|
79
|
+
|
|
80
|
+
- **`cache_context` no longer logs an unusable `unknown` prefix pair on nearly every record (closes #1938)** — `prefixObservation` and `firstMessageChange` were computed from a hash of the pre-injection prefix capped at 64 messages / 2,048 characters. Any transcript past that cap set `prefixHashTruncated`, which forced both fields to `"unknown"`. Every real session outgrows the cap within the first few turns, so the pair reported `unknown` on 97.2% and 7.6% of records respectively — cost without signal. This PR removes the whole family that existed only to support that pair: `prefixObservation`, `prefixObservationUnknown`, `prefixBaseline`, `firstMessageChanged`, `firstMessageChange`, `firstMessageHashTruncated`, `beforeFirstMessageHash`, `afterFirstMessageHash`, `beforePrefixHash`, `afterPrefixHash`, `prefixHashTruncated`, `prefixMessageCountTruncated`, `prefixContentHashTruncated` — 13 fields in all. `cache_prefix_break` already tracks first-message stability with an unbounded hash and never truncates; it remains the source of truth for cache-prefix breaks.
|
|
81
|
+
|
|
82
|
+
- **Review-graph Tier-3 build never grew the tree cache past the 50-entry default (closes #1941)** —
|
|
83
|
+
`builder.ts`'s full-project rebuild parses every non-jsts file through the
|
|
84
|
+
shared `TreeSitterClient`'s parse-tree cache but never called
|
|
85
|
+
`ensureTreeCacheCapacity`, the #1715 fix already wired into the
|
|
86
|
+
diagnostics scanner. A project with more than 50 non-jsts files evicted
|
|
87
|
+
and re-parsed files past the 50th on every Tier-3 build. The build now
|
|
88
|
+
grows the cache to its actual per-parse working set before extraction
|
|
89
|
+
starts — the full file list on a cold build, or the checkpoint's remaining
|
|
90
|
+
files on a resumed build, since resumed files are reused from the
|
|
91
|
+
checkpoint graph and never re-parsed.
|
|
92
|
+
|
|
93
|
+
- **A demoted blocker no longer shouts STOP, and it stops re-serving forever (closes #1944)** — the past-EOF gate moved a blocker whose file had shrunk past the cited lines into the advisory channel, but the advisory embedded the blocker body verbatim, so the agent still read "🔴 STOP — 11 issue(s) must be fixed" with line numbers the file no longer had. Nothing retired the record either, so it re-served on every turn end for the rest of the session (measured live at 80+ minutes). A demoted body now drops the STOP banner and the "must be fixed" imperative, renders a dead coordinate as `L<n> (line no longer exists)`, and — when no re-run could ever confirm it — is delivered once and then retired, with the suppression recorded in the degradation ledger under `demoted-finding-retired`. `lens_diagnostics mode=delta` also stops printing 🔴 on a row whose coordinate it just replaced with the stale marker.
|
|
94
|
+
|
|
95
|
+
- **The oxlint runner now surfaces warning-severity findings instead of discarding them (closes #1947)** — oxlint exits 0 whenever nothing at error severity was found, and warning is oxlint's own default severity. The runner treated any exit 0 as "no findings" and returned early, so a real capture of oxlint on an unused variable — a full JSON report, exit 0 — was thrown away. The runner now parses stdout unconditionally and decides on the parsed diagnostic count instead of the exit code: zero diagnostics is still a clean `succeeded`/`none`, one or more is `failed` with `semantic: "warning"` (or `"blocking"` when a diagnostic is error severity), matching the mapping the runner already used for the nonzero-exit case. A captured real-bytes fixture (`tests/fixtures/runner-output/oxlint/warning-exit-zero.captured.json`, oxlint 1.79.0, exit 0) pins the behavior.
|
|
96
|
+
|
|
97
|
+
- **Merge-train warden's `update-branch` kick 403s under branch protection (#1959)** — the workflow's job permissions granted `pull-requests: write` but not `contents: write`. The PUT `pulls/N/update-branch` endpoint has GitHub create a merge commit on the PR branch, which needs write access to repository contents, not just to the pull request object; the inline comment attributing it to `pull-requests: write` was wrong. The warden also now tells apart the two reasons update-branch can 403: a fork-owned head PR records a distinct benign outcome (`update-branch-forbidden-fork`, logged, not a run failure), while an own-branch PR still fails the run loudly, since that case is a real permissions bug like this one.
|
|
98
|
+
|
|
99
|
+
- **The blind review-graph read no longer drops a valid snapshot on a HEAD-only move (closes #1961)** — `getCachedReviewGraph` verified the persisted snapshot's git stamp against the current HEAD, so a plain `git commit` made every reader see "graph: cold" until something rebuilt in-process. Measured stamp lifetime across four workspaces was 737s to 1562s (median), and two of the four were already missing at probe time. The read path now verifies tree IDENTITY, never revision — the same policy the build path's tier-2 load has followed since #300. A snapshot stamped for a different worktree is still dropped, since nothing downstream of this read content-verifies it. A snapshot stamped at a different HEAD is served, and both `project_report`'s trust notes and `module_report`'s warnings say which commit it was built at. That caveat is derived per call from the snapshot's stamped commit and the worktree's HEAD right now, so it never names a commit that has stopped being HEAD and it clears itself when HEAD returns to the stamped one. The mid-build resume checkpoint carried the same defect and got the same fix: it hashes every processed file on resume, so revision equality proved nothing the hashes did not. Both read verdicts now write a bounded `review-graph.log` record (`snapshot_read_dropped`, `snapshot_read_drifted`), rising-edge per workspace off the `review-graph-snapshot-read` ledger kind — the drop used to be silent, which is why it hid for weeks.
|
|
100
|
+
|
|
101
|
+
- **A settled review-graph build no longer suppresses every later build, and `project_report` only claims a retry when one started (closes #1962)** — `buildOrUpdateGraph`'s dedupe cache deleted its entry on rejection only, so a settled promise for a skipped or completed build answered every later call for the same key for the rest of the process. Only the dispatch pipeline's per-invocation `clearGraphCache()` ever removed it, and the background build `project_report` kicks off never goes through the pipeline: four calls over 37s produced one `build_started` record and three "A retry was started." messages with no retry behind them. The entry now lives only while its build is pending, so genuine concurrency still dedupes and a settled build never latches. `project_report` asks the builder whether a build is already in flight before it claims one started, and reports the current attempt rather than replaying the first one as if it were fresh. A `.pi-lens.json` at the repo root raises `reviewGraph.maxFiles` above the derived 1000-file cap this repo trips, so the tool works on this repo at all.
|
|
102
|
+
|
|
103
|
+
- **A language server that dies mid-session now records why, and the liveness probe stops provoking the deaths (closes #1969)** — an ast-grep child closed with `code=1` and empty stderr 14 times in one day. The visible fallout was 19 `lsp_client_skipped_broken` cooldowns and 32 coverage gaps; the cause was recorded nowhere. Two changes. First, an unprompted process `close` now writes an `lsp-server-unexpected-close` degradation-ledger entry naming the server, the exit code, the signal, and whether stderr carried anything. It is written on `close` rather than `exit`, so "stderr was empty" is a fact about the server and not a race with the pipe, and it is gated on `shutdownRequested`, so evictions and ordinary teardown record nothing. Second, the `pingLiveness` round-trip (#1277) no longer hardcodes `workspace/symbol`. It picks a method the server advertised, best first: `workspace/symbol`, then `textDocument/documentSymbol` or `textDocument/hover` on an already-open document, then nothing. ast-grep's tower_lsp backend logged "got a 'workspace/symbol' request, but it is not implemented" on every probe; it advertises `hoverProvider`, so it keeps a real round-trip. When no probe is available the ledger records `lsp-liveness-probe-unsupported`, because liveness derived from process and connection state alone is the weaker verdict and must not pass as the strong one. Also fixed on the same path: `createLSPClient`'s initialize-timeout catch killed the hung child without claiming the kill, so its own teardown reported as a crash. That fabricated the exact `code=1 signal=none stderr=empty` signature this change exists to make trustworthy, and it had been writing a false `lsp_server_unexpected_exit` latency line on every failed handshake since the #615 follow-up.
|
|
104
|
+
|
|
105
|
+
- **Log writer reopens and retries once on a failed write instead of dropping records silently (refs #1970)** — `ndjson-logger.ts` is the shared write-plumbing behind every NDJSON sink (`latency.log`, `extension.log`, `tree-sitter.log`, `cascade.log`, `word-index.log`, `review-graph.log`, and any other `createNdjsonLogger` caller). This hardens the sink against reachable `appendFile` failures (ENOENT after the parent directory is deleted, EBUSY/EPERM under a syncing OneDrive folder or antivirus lock, EMFILE, ENOSPC) — the specific `ERR_STREAM_DESTROYED` reports that motivated this issue were root-caused to a different process's persistent-stream logger, not this sink, but the reachable-failure classes above land here today with no recovery and no observability. A write that throws now gets one reopen-and-retry (re-verify the parent directory, then retry the write once) before it counts as a loss, matching the `pi-analyze#15` shape. An unrecovered write is counted in a per-sink, in-memory tally (`writeFailures`) rather than thrown or silently discarded, and `degradation-ledger.ts` folds that tally into `getDegradationSummary()` at READ time under a new `log-sink-write-failure` kind — so `pilens_health` names the sink and the dropped-write count. The fold happens by reading ndjson-logger's own tally, never by writing a durable ledger row back through the sink that just failed, so a permanently dead sink cannot recurse into an unbounded chain of self-reporting writes. The tally resets alongside the rest of the ledger at `session_start`.
|
|
106
|
+
|
|
107
|
+
- **Warmup and workspace-scan walkers gate by file extension before consulting the ignore matcher, cutting a 31.7s stall to sub-second on repos with large ignored file piles (closes #1974, reported by @0xkite)** — `isIgnored` recompiles minimatch patterns per ancestor directory on every call; a runtime-output directory like `wal/` holding tens of thousands of `.log` files, gitignored only by a file-level `*.log` pattern (so the directory itself is never pruned), forced every one of those files through that cost. Six walkers checked `isIgnored` before their own cheap extension gate, paying the expensive check for files the gate would have dropped anyway: `collectSourceFilesForWarmup` (`language-profile.ts`, the reported 31.7s case), `JscpdClient.hasSourceFilesRecursive` (`jscpd-client.ts`), `classifyEntry` (`source-filter.ts`), `makeSourceCountVisitor` (`startup-scan.ts`), `getModuleSourceFiles` (`review-graph/workspace-modules.ts`), and `collectWorkspaceDiagnosticFiles` (`lsp/index.ts`). All six now run their extension (or LSP-server) gate first; output is unchanged, since both gates still have to pass to keep a file — only the order changed. A repo-wide sweep of all 14 files that call `isIgnored` found `tree-sitter-client.ts` and `tools/lsp-diagnostics.ts` already extension-first, and cleared the remaining per-event/no-ext-gate sites by construction; see the PR body for the full per-site table.
|
|
108
|
+
|
|
19
109
|
## [4.1.0] - 2026-08-20
|
|
20
110
|
|
|
21
111
|
### Added
|
|
@@ -34,6 +34,112 @@ pi-lens gives AI coding agents fast, language-aware feedback while they write/ed
|
|
|
34
34
|
- MCP server (experimental) so Claude Code or any MCP client can drive the
|
|
35
35
|
same diagnostics/read-substitute tools pi-lens exposes to pi
|
|
36
36
|
|
|
37
|
+
## Architecture
|
|
38
|
+
|
|
39
|
+
Most lifecycle events enter through one wrapper, which drops and counts events
|
|
40
|
+
that arrive on a replaced session. `tool_call` registers raw: it delegates
|
|
41
|
+
straight to a handler that owns its own total guard. Events fan out into the
|
|
42
|
+
edit-time lane and the LSP lane. Both lanes write into the findings stores.
|
|
43
|
+
Nothing reaches the agent from those stores until a freshness gate or an
|
|
44
|
+
explicit age label clears it.
|
|
45
|
+
|
|
46
|
+
```mermaid
|
|
47
|
+
flowchart TD
|
|
48
|
+
subgraph host["pi host"]
|
|
49
|
+
HOST["Host events<br/>tool_call, tool_result, turn_start/end,<br/>session_start/shutdown, agent_end, context"]
|
|
50
|
+
WRAP["Stale-ctx wrapper<br/>skips and counts events on a replaced session<br/>tool_result, turn_start, turn_end, agent_end,<br/>agent_settled, session_start, context"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
subgraph guards["Guards"]
|
|
54
|
+
RG["Read-guard<br/>blocks edits that lack prior reading"]
|
|
55
|
+
GG["Git-guard<br/>holds commit/push while findings stay unresolved"]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
subgraph edit["Edit-time lane"]
|
|
59
|
+
PIPE["Post-write pipeline<br/>secrets, format, autofix, sync, lint, tests"]
|
|
60
|
+
PLAN["Dispatch plan<br/>per file kind, per capability group"]
|
|
61
|
+
RUN["Runners<br/>format, lint, types, security, smells, docs"]
|
|
62
|
+
STRUCT["Structural rules<br/>tree-sitter queries and ast-grep"]
|
|
63
|
+
BUS["files-touched bus<br/>tells extensions which paths moved"]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
subgraph lsp["LSP lane"]
|
|
67
|
+
POOL["Client pool<br/>warm reuse, idle eviction"]
|
|
68
|
+
DIAGS["File and workspace diagnostics"]
|
|
69
|
+
CASC["Impact cascade<br/>tiered wait policy"]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
STORES["Findings stores<br/>widget state, warning caches, project snapshot"]
|
|
73
|
+
|
|
74
|
+
subgraph gate["Freshness gating"]
|
|
75
|
+
FRESH["Path freshness<br/>mtime vs scan time, past-EOF, dependency drift"]
|
|
76
|
+
DISPO["Dispositions<br/>false-positive, suppress, defer, flagged"]
|
|
77
|
+
LABEL["Explicit age label<br/>for findings no path gate can check"]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
subgraph deliver["Delivery surfaces"]
|
|
81
|
+
TURN["Turn-end findings injection"]
|
|
82
|
+
WIDGET["Widget and footer tally"]
|
|
83
|
+
TOOLS["lens_diagnostics tool"]
|
|
84
|
+
NUDGE["Agent nudges"]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
SESSION["Session lifecycle<br/>primary, sequential replacement, concurrent secondary"]
|
|
88
|
+
SINKS["Observability sinks<br/>latency.log, degradation ledger, bounded telemetry,<br/>cache observability, cascade and tree-sitter logs"]
|
|
89
|
+
|
|
90
|
+
HOST --> WRAP
|
|
91
|
+
HOST -->|tool_call, raw| RG
|
|
92
|
+
HOST -->|tool_call, raw| GG
|
|
93
|
+
WRAP -->|session_start| SESSION
|
|
94
|
+
WRAP -->|tool_result| PIPE
|
|
95
|
+
WRAP -->|tool_result, records reads and writes| RG
|
|
96
|
+
SESSION --> POOL
|
|
97
|
+
SESSION --> STORES
|
|
98
|
+
PIPE --> PLAN
|
|
99
|
+
PLAN --> RUN
|
|
100
|
+
PLAN --> STRUCT
|
|
101
|
+
PIPE --> POOL
|
|
102
|
+
PIPE --> BUS
|
|
103
|
+
POOL --> DIAGS
|
|
104
|
+
DIAGS --> CASC
|
|
105
|
+
RUN --> STORES
|
|
106
|
+
STRUCT --> STORES
|
|
107
|
+
DIAGS --> STORES
|
|
108
|
+
CASC --> STORES
|
|
109
|
+
BUS --> NUDGE
|
|
110
|
+
RG -->|read and edit history filter| NUDGE
|
|
111
|
+
STORES --> FRESH
|
|
112
|
+
STORES --> LABEL
|
|
113
|
+
FRESH --> DISPO
|
|
114
|
+
DISPO --> TURN
|
|
115
|
+
DISPO --> WIDGET
|
|
116
|
+
DISPO --> TOOLS
|
|
117
|
+
LABEL --> TURN
|
|
118
|
+
TURN --> GG
|
|
119
|
+
WRAP --> SINKS
|
|
120
|
+
PIPE --> SINKS
|
|
121
|
+
RUN --> SINKS
|
|
122
|
+
STRUCT --> SINKS
|
|
123
|
+
POOL --> SINKS
|
|
124
|
+
CASC --> SINKS
|
|
125
|
+
RG --> SINKS
|
|
126
|
+
GG --> SINKS
|
|
127
|
+
FRESH --> SINKS
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Architecture-level view, updated when a lane changes. Per-tool inventories live
|
|
131
|
+
in [features](docs/features.md) and
|
|
132
|
+
[language coverage](docs/language-coverage.md). Today the edit-time lane carries
|
|
133
|
+
45+ runner modules over 35+ file kinds, and the LSP lane speaks to a dozen-plus
|
|
134
|
+
language servers.
|
|
135
|
+
|
|
136
|
+
The gating box is an abstraction, not a call order. Freshness covers several
|
|
137
|
+
independent mechanisms: path freshness against scan time, past-EOF line checks,
|
|
138
|
+
and forward-import dependency drift. Dispositions are one more filter alongside
|
|
139
|
+
them, not a second stage every finding walks through. Read the box as "a finding
|
|
140
|
+
passes the gates that apply to it", and see `clients/finding-delivery-gate.ts`
|
|
141
|
+
for the per-surface contract.
|
|
142
|
+
|
|
37
143
|
## Install
|
|
38
144
|
|
|
39
145
|
```bash
|
|
@@ -45,10 +45,10 @@ export function tokenizeShellCommand(command) {
|
|
|
45
45
|
word = "";
|
|
46
46
|
atTokenStart = true;
|
|
47
47
|
};
|
|
48
|
-
const flushSegment = () => {
|
|
48
|
+
const flushSegment = (terminator) => {
|
|
49
49
|
flushWord();
|
|
50
50
|
if (tokens.length > 0 || unsupported)
|
|
51
|
-
segments.push({ tokens, unsupported });
|
|
51
|
+
segments.push({ tokens, unsupported, terminator });
|
|
52
52
|
tokens = [];
|
|
53
53
|
unsupported = false;
|
|
54
54
|
atTokenStart = true;
|
|
@@ -122,13 +122,18 @@ export function tokenizeShellCommand(command) {
|
|
|
122
122
|
}
|
|
123
123
|
if (ch === ";" || ch === "\n" || ch === "|" || ch === "&") {
|
|
124
124
|
flushWord();
|
|
125
|
+
let terminator;
|
|
125
126
|
if (ch === "|" && next === "|")
|
|
126
127
|
i++;
|
|
127
128
|
else if (ch === "&" && next === "&")
|
|
128
129
|
i++;
|
|
129
|
-
else if (ch === "|"
|
|
130
|
+
else if (ch === "|") {
|
|
130
131
|
unsupported = true;
|
|
131
|
-
|
|
132
|
+
terminator = "pipe";
|
|
133
|
+
}
|
|
134
|
+
else if (ch === "&")
|
|
135
|
+
unsupported = true;
|
|
136
|
+
flushSegment(terminator);
|
|
132
137
|
continue;
|
|
133
138
|
}
|
|
134
139
|
if (ch === "<" || ch === ">") {
|
|
@@ -514,6 +519,83 @@ function parseGrepLineWithoutFile(line, file) {
|
|
|
514
519
|
return undefined;
|
|
515
520
|
return { file, startLine: lineNumber, endLine: lineNumber };
|
|
516
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* Verbs that DROP printed lines from a piped stream (#1908, review F4): a
|
|
524
|
+
* hit that survives such a tail cannot be trusted to still carry the
|
|
525
|
+
* -A/-B/-C context grep's flags declared, because that context may have
|
|
526
|
+
* been among the dropped lines. `head`/`tail` drop by position (even bare,
|
|
527
|
+
* since both default to a 10-line window); `sed` with a `q` command drops
|
|
528
|
+
* everything after the addressed line; `uniq` drops adjacent duplicate
|
|
529
|
+
* lines, which can include a context line that happens to repeat.
|
|
530
|
+
*
|
|
531
|
+
* The criterion is DROPPING, not "changes grep's output shape" — a review
|
|
532
|
+
* finding (#1913 F4) caught this file previously conflating the two:
|
|
533
|
+
* - `head`/`tail`/`sed q`/`uniq` downstream of a matching grep → some
|
|
534
|
+
* printed lines are genuinely gone, so credit falls back to
|
|
535
|
+
* match-line-only (the same conservative default already used for
|
|
536
|
+
* unparseable commands) rather than trusting the declared flags.
|
|
537
|
+
* - `sort` and other REORDER-ONLY filters keep every line grep printed,
|
|
538
|
+
* just in a different order — a surviving `file:line:` match still
|
|
539
|
+
* carries the full context grep actually printed, so no special case is
|
|
540
|
+
* needed (proven by the "still credits full context through a
|
|
541
|
+
* non-truncating pipe tail" test below).
|
|
542
|
+
* - `wc` and similar filters that REPLACE grep's output entirely (with a
|
|
543
|
+
* count, say) leave no `file:line:`-shaped line at all, so
|
|
544
|
+
* `parseGrepOutputSearchReads` already finds zero matches in the real
|
|
545
|
+
* captured stdout. Also no special case needed, but for the OPPOSITE
|
|
546
|
+
* reason from `sort`: nothing survives to credit, rather than
|
|
547
|
+
* everything surviving intact.
|
|
548
|
+
* - A pass-through filter (`cat`, `grep -v`, ...) between grep and a
|
|
549
|
+
* dropping tail still counts: the walk below follows the whole pipe
|
|
550
|
+
* chain, not just the immediate next segment.
|
|
551
|
+
* - Out of scope, deliberately: `sed -n 'Np'` range-address filtering
|
|
552
|
+
* without `q`, and `sort -u`, also drop lines but aren't in #1908's
|
|
553
|
+
* stated scope — revisit if they recur in the field. Also out of
|
|
554
|
+
* scope: a truncating tail hidden inside a subshell/brace-group
|
|
555
|
+
* (`( ... )`, `{ ...; }`) or reimplemented in `awk`/`perl` — the shared
|
|
556
|
+
* tokenizer this file uses doesn't parse inside those, matching its
|
|
557
|
+
* documented "small conservative shell lexer" scope.
|
|
558
|
+
*/
|
|
559
|
+
const LINE_DROPPING_TAIL_VERBS = new Set(["head", "tail", "uniq"]);
|
|
560
|
+
function isSedQuitCommand(args) {
|
|
561
|
+
for (const arg of args) {
|
|
562
|
+
const token = stripQuotes(arg);
|
|
563
|
+
if (token.startsWith("-"))
|
|
564
|
+
continue;
|
|
565
|
+
// A `q` command anchored at the start of the script or after a `;`/
|
|
566
|
+
// newline separator, optionally preceded by a numeric/`$` address
|
|
567
|
+
// (`q`, `1q`, `$q`, `1,3p;q`). Deliberately not a full sed parser.
|
|
568
|
+
if (/(^|[;\n])\s*(\d+|\$)?\s*q\b/.test(token))
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
function isLineDroppingTailCommand(tokens) {
|
|
574
|
+
const verb = path.basename(stripQuotes(tokens[0] ?? ""));
|
|
575
|
+
if (LINE_DROPPING_TAIL_VERBS.has(verb))
|
|
576
|
+
return true;
|
|
577
|
+
if (verb === "sed")
|
|
578
|
+
return isSedQuitCommand(tokens.slice(1));
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* True when the grep segment starting at `index` feeds — directly or through
|
|
583
|
+
* a chain of pipes — into a line-dropping tail (#1908). Only an unquoted `|`
|
|
584
|
+
* counts: a `;`/`&&`/`&`-separated command downstream never receives grep's
|
|
585
|
+
* stdout, so it must not downgrade credit (#1913 F2 — this was previously
|
|
586
|
+
* unverified by any test that could tell a real pipe from mere adjacency).
|
|
587
|
+
*/
|
|
588
|
+
function grepPipesIntoTruncatingTail(segments, index) {
|
|
589
|
+
if (segments[index]?.terminator !== "pipe")
|
|
590
|
+
return false;
|
|
591
|
+
for (let j = index + 1; j < segments.length; j++) {
|
|
592
|
+
if (isLineDroppingTailCommand(segments[j].tokens))
|
|
593
|
+
return true;
|
|
594
|
+
if (segments[j].terminator !== "pipe")
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
517
599
|
function collectGrepCommandFiles(command, cwd) {
|
|
518
600
|
const files = new Set();
|
|
519
601
|
let hasLineNumberGrep = false;
|
|
@@ -522,7 +604,14 @@ function collectGrepCommandFiles(command, cwd) {
|
|
|
522
604
|
// contributing grep printed, so no line is credited that a hit from the
|
|
523
605
|
// narrowest grep never delivered.
|
|
524
606
|
let context;
|
|
525
|
-
|
|
607
|
+
// #1908: if ANY contributing grep pipes into a truncating tail, fall back
|
|
608
|
+
// to match-line-only credit for the whole command — the parsed hits carry
|
|
609
|
+
// no segment identity, so a per-segment override isn't representable in
|
|
610
|
+
// the aggregate `context` this function returns.
|
|
611
|
+
let truncated = false;
|
|
612
|
+
const segments = tokenizeShellCommand(command);
|
|
613
|
+
for (let i = 0; i < segments.length; i++) {
|
|
614
|
+
const tokens = segments[i].tokens;
|
|
526
615
|
const verb = path.basename(stripQuotes(tokens[0] ?? ""));
|
|
527
616
|
if (verb !== "grep" && verb !== "egrep" && verb !== "fgrep")
|
|
528
617
|
continue;
|
|
@@ -539,11 +628,13 @@ function collectGrepCommandFiles(command, cwd) {
|
|
|
539
628
|
: segmentContext;
|
|
540
629
|
for (const file of extractGrepSearchFiles(args, cwd))
|
|
541
630
|
files.add(file);
|
|
631
|
+
if (grepPipesIntoTruncatingTail(segments, i))
|
|
632
|
+
truncated = true;
|
|
542
633
|
}
|
|
543
634
|
return {
|
|
544
635
|
hasLineNumberGrep,
|
|
545
636
|
files,
|
|
546
|
-
context: context ?? { before: 0, after: 0 },
|
|
637
|
+
context: truncated ? { before: 0, after: 0 } : (context ?? { before: 0, after: 0 }),
|
|
547
638
|
};
|
|
548
639
|
}
|
|
549
640
|
function dedupePushSearchRead(out, seen, loc) {
|
|
@@ -56,7 +56,7 @@ export function sweepInlineBlockerPastEof(runtime, cwd, options) {
|
|
|
56
56
|
total: 0,
|
|
57
57
|
checked: 0,
|
|
58
58
|
demoted: 0,
|
|
59
|
-
|
|
59
|
+
deadLinesByPath: new Map(),
|
|
60
60
|
};
|
|
61
61
|
const resync = options?.resync ?? resyncDocumentOnPastEof;
|
|
62
62
|
let entries;
|
|
@@ -101,15 +101,25 @@ export function sweepInlineBlockerPastEof(runtime, cwd, options) {
|
|
|
101
101
|
// The record cites any of its lines being past EOF as a whole-record
|
|
102
102
|
// demotion — one summary string, not one flag per line.
|
|
103
103
|
const isPastEof = diagnostics.some((d) => d.stale);
|
|
104
|
+
if (isPastEof) {
|
|
105
|
+
// #1944: record WHICH cited lines died, before the transition check
|
|
106
|
+
// below short-circuits. A record demoted on an earlier turn does not
|
|
107
|
+
// transition, yet its dead lines are exactly what the delivery
|
|
108
|
+
// surface must annotate and retire on.
|
|
109
|
+
counts.deadLinesByPath.set(entry.filePath, diagnostics
|
|
110
|
+
.filter((d) => d.stale)
|
|
111
|
+
.map((d) => d.line)
|
|
112
|
+
.filter((line) => typeof line === "number"));
|
|
113
|
+
}
|
|
104
114
|
const transitioned = runtime.setInlineBlockerPastEofStale(entry.filePath, isPastEof);
|
|
105
115
|
if (!transitioned)
|
|
106
116
|
continue;
|
|
107
|
-
|
|
117
|
+
// Only the rising edge is countable here — see `BlockerPastEofCounts`
|
|
118
|
+
// for why the falling edge cannot occur on this store anymore. The
|
|
119
|
+
// call below still un-demotes, so the STORE stays honest even though
|
|
120
|
+
// nothing can observe the transition.
|
|
121
|
+
if (isPastEof)
|
|
108
122
|
counts.demoted += 1;
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
counts.healed += 1;
|
|
112
|
-
}
|
|
113
123
|
}
|
|
114
124
|
catch {
|
|
115
125
|
// Per-entry failure: leave the entry as-is.
|
|
@@ -77,12 +77,26 @@ export const BOUNDED_TELEMETRY_PHASES = [
|
|
|
77
77
|
"lsp_client_skipped_broken",
|
|
78
78
|
/** #1743 review: the same skip, for a temporarily unavailable command. */
|
|
79
79
|
"lsp_client_skipped_unavailable_command",
|
|
80
|
+
/**
|
|
81
|
+
* #1934: `getWarmClientForFile` found no live client for a file that has at
|
|
82
|
+
* least one language server with a resolvable root. Its callers run per
|
|
83
|
+
* file (the cascade quiet window, read expansion, `hasWarmLSP`), so it is
|
|
84
|
+
* rising-edge per candidate (server, root) set, with the ledger holding the
|
|
85
|
+
* exact count.
|
|
86
|
+
*/
|
|
87
|
+
"lsp_warm_client_missing",
|
|
80
88
|
/**
|
|
81
89
|
* #1723: an event-loop block at or above the floor. Not a degradation, so
|
|
82
90
|
* no ledger kind; bounded by call cadence (one `turn_end` runs it once per
|
|
83
91
|
* turn) rather than by an option here.
|
|
84
92
|
*/
|
|
85
93
|
"loop_block",
|
|
94
|
+
/**
|
|
95
|
+
* #1925: a session event skipped because its ctx was invalidated by a
|
|
96
|
+
* session replacement or reload. A replaced session can drain a whole
|
|
97
|
+
* queue of them, so it is rising-edge per event name.
|
|
98
|
+
*/
|
|
99
|
+
"session_event_stale_ctx_skip",
|
|
86
100
|
];
|
|
87
101
|
const REGISTERED_PHASES = new Set(BOUNDED_TELEMETRY_PHASES);
|
|
88
102
|
/** Report whether a phase name is in the registry (used by the sweep). */
|