pi-lens 4.1.2 → 4.1.4
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/CHANGELOG.md +1308 -0
- package/README.md +3 -0
- package/config/biome/core.jsonc +11 -2
- package/config/dependency-cruiser-eager-allowlist.json +91 -0
- package/dist/clients/actionable-warnings-logger.js +18 -3
- package/dist/clients/actionable-warnings.js +927 -80
- package/dist/clients/agent-behavior-client.js +13 -4
- package/dist/clients/ast-grep-client.js +136 -9
- package/dist/clients/ast-grep-rule-manager.js +60 -5
- package/dist/clients/ast-grep-tool-logger.js +2 -2
- package/dist/clients/availability-probe-flight.js +12 -0
- package/dist/clients/bash-file-access.js +1 -3
- package/dist/clients/biome-client.js +16 -2
- package/dist/clients/blocker-freshness.js +34 -0
- package/dist/clients/bootstrap.js +509 -73
- package/dist/clients/bounded-cache.js +152 -12
- package/dist/clients/bounded-telemetry.js +115 -23
- package/dist/clients/build-identity.js +95 -0
- package/dist/clients/bundled-resource-health.js +113 -0
- package/dist/clients/bus-events-logger.js +2 -2
- package/dist/clients/bus-publish.js +3 -0
- package/dist/clients/cache/rule-cache.js +33 -5
- package/dist/clients/cache-manager.js +105 -6
- package/dist/clients/cache-observability.js +46 -19
- package/dist/clients/cargo-manifest.js +422 -0
- package/dist/clients/cascade-logger.js +17 -3
- package/dist/clients/code-quality-warnings.js +13 -3
- package/dist/clients/complexity-client.js +16 -5
- package/dist/clients/config-core/deny.js +221 -0
- package/dist/clients/config-core/index.js +47 -0
- package/dist/clients/config-core/merge.js +357 -0
- package/dist/clients/config-core/normalize.js +340 -0
- package/dist/clients/config-core/process-spec.js +248 -0
- package/dist/clients/config-core/provenance.js +171 -0
- package/dist/clients/config-core/records.js +218 -0
- package/dist/clients/config-core/resolve.js +125 -0
- package/dist/clients/config-core/safe-object.js +78 -0
- package/dist/clients/config-core/schema.js +167 -0
- package/dist/clients/config-diagnostic-codes.js +281 -0
- package/dist/clients/config-locations.js +160 -0
- package/dist/clients/config-resolve.js +789 -0
- package/dist/clients/config-schema.js +197 -0
- package/dist/clients/config-warn.js +407 -0
- package/dist/clients/dead-code-client.js +33 -25
- package/dist/clients/dead-code-logger.js +2 -2
- package/dist/clients/deadline-utils.js +178 -0
- package/dist/clients/debug-handles.js +2 -2
- package/dist/clients/debug-heap.js +3 -3
- package/dist/clients/deferred-lsp-work.js +106 -0
- package/dist/clients/degradation-ledger.js +166 -8
- package/dist/clients/demoted-finding-render.js +14 -0
- package/dist/clients/dependency-checker.js +28 -22
- package/dist/clients/deps/minimatch.js +1 -1
- package/dist/clients/diagnostic-line-freshness.js +12 -9
- package/dist/clients/diagnostic-logger.js +14 -4
- package/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/dist/clients/dispatch/dispatcher.js +304 -19
- package/dist/clients/dispatch/fact-store.js +328 -9
- package/dist/clients/dispatch/integration.js +214 -107
- package/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/dist/clients/dispatch/plan.js +21 -5
- package/dist/clients/dispatch/runners/actionlint.js +0 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +550 -77
- package/dist/clients/dispatch/runners/biome-check.js +0 -1
- package/dist/clients/dispatch/runners/cpp-check.js +0 -1
- package/dist/clients/dispatch/runners/credo.js +0 -1
- package/dist/clients/dispatch/runners/cue-vet.js +0 -1
- package/dist/clients/dispatch/runners/dart-analyze.js +0 -1
- package/dist/clients/dispatch/runners/detekt.js +0 -1
- package/dist/clients/dispatch/runners/dotnet-build.js +0 -1
- package/dist/clients/dispatch/runners/elixir-check.js +0 -1
- package/dist/clients/dispatch/runners/eslint.js +5 -2
- package/dist/clients/dispatch/runners/fact-rules.js +0 -1
- package/dist/clients/dispatch/runners/fish-indent.js +0 -1
- package/dist/clients/dispatch/runners/gleam-check.js +0 -1
- package/dist/clients/dispatch/runners/go-vet.js +1 -3
- package/dist/clients/dispatch/runners/golangci-lint.js +0 -1
- package/dist/clients/dispatch/runners/hadolint.js +0 -1
- package/dist/clients/dispatch/runners/helm-lint.js +16 -4
- package/dist/clients/dispatch/runners/helm-render.js +54 -10
- package/dist/clients/dispatch/runners/htmlhint.js +0 -1
- package/dist/clients/dispatch/runners/index.js +1 -3
- package/dist/clients/dispatch/runners/javac.js +0 -1
- package/dist/clients/dispatch/runners/ktlint.js +0 -1
- package/dist/clients/dispatch/runners/lsp.js +37 -2
- package/dist/clients/dispatch/runners/markdownlint.js +4 -2
- package/dist/clients/dispatch/runners/mypy.js +0 -1
- package/dist/clients/dispatch/runners/oxlint.js +24 -22
- package/dist/clients/dispatch/runners/php-lint.js +0 -1
- package/dist/clients/dispatch/runners/phpstan.js +0 -1
- package/dist/clients/dispatch/runners/prisma-validate.js +0 -1
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -1
- package/dist/clients/dispatch/runners/pyright.js +7 -4
- package/dist/clients/dispatch/runners/rubocop.js +0 -1
- package/dist/clients/dispatch/runners/ruff.js +0 -1
- package/dist/clients/dispatch/runners/rust-clippy.js +9 -6
- package/dist/clients/dispatch/runners/shellcheck.js +0 -1
- package/dist/clients/dispatch/runners/shfmt.js +0 -1
- package/dist/clients/dispatch/runners/spellcheck.js +0 -1
- package/dist/clients/dispatch/runners/spotbugs.js +0 -1
- package/dist/clients/dispatch/runners/sqlfluff.js +0 -1
- package/dist/clients/dispatch/runners/stylelint.js +0 -1
- package/dist/clients/dispatch/runners/swiftlint.js +0 -1
- package/dist/clients/dispatch/runners/taplo.js +0 -1
- package/dist/clients/dispatch/runners/terragrunt.js +0 -1
- package/dist/clients/dispatch/runners/tflint.js +0 -1
- package/dist/clients/dispatch/runners/tree-sitter.js +0 -1
- package/dist/clients/dispatch/runners/trivy-config.js +0 -1
- package/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
- package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +270 -105
- package/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
- package/dist/clients/dispatch/runners/utils/toolchain-availability.js +89 -26
- package/dist/clients/dispatch/runners/vale.js +0 -1
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
- package/dist/clients/dispatch/runners/yamllint.js +0 -1
- package/dist/clients/dispatch/runners/zig-check.js +0 -1
- package/dist/clients/disposition-logger.js +3 -3
- package/dist/clients/effective-config.js +403 -0
- package/dist/clients/error-class.js +23 -0
- package/dist/clients/event-loop-hold.js +274 -0
- package/dist/clients/event-loop-monitor.js +66 -1
- package/dist/clients/extension-log.js +2 -2
- package/dist/clients/feature-hints.js +2 -1
- package/dist/clients/file-time.js +6 -1
- package/dist/clients/file-utils.js +266 -13
- package/dist/clients/finding-delivery-gate.js +55 -13
- package/dist/clients/format-service.js +6 -1
- package/dist/clients/formatters.js +303 -104
- package/dist/clients/freshness-cadence.js +17 -0
- package/dist/clients/generated-artifacts.js +114 -19
- package/dist/clients/generation-guard.js +4 -9
- package/dist/clients/git-guard.js +198 -48
- package/dist/clients/git-tracked-ignore.js +32 -1
- package/dist/clients/go-client.js +39 -0
- package/dist/clients/govulncheck-client.js +5 -1
- package/dist/clients/gradle-ktfmt-style.js +252 -0
- package/dist/clients/gzip-stage-write.js +7 -0
- package/dist/clients/hashline-anchor.js +424 -0
- package/dist/clients/hook-budgets.js +76 -0
- package/dist/clients/host-edit-normalize.js +5 -2
- package/dist/clients/host-ports.js +1 -1
- package/dist/clients/installer/index.js +200 -84
- package/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/dist/clients/instance-reaper.js +108 -243
- package/dist/clients/instance-registry-lock.js +145 -0
- package/dist/clients/instance-registry.js +435 -122
- package/dist/clients/jscpd-client.js +6 -1
- package/dist/clients/language-profile.js +4 -2
- package/dist/clients/language-registry.js +595 -0
- package/dist/clients/latency-logger.js +179 -3
- package/dist/clients/ledger-bounds.js +13 -0
- package/dist/clients/lens-config.js +141 -30
- package/dist/clients/lens-engine.js +30 -37
- package/dist/clients/lens-flag-registry.js +67 -6
- package/dist/clients/lens-map.js +3 -1
- package/dist/clients/log-cleanup.js +2 -2
- package/dist/clients/lsp/aggregation.js +3 -1
- package/dist/clients/lsp/cascade-tier.js +4 -0
- package/dist/clients/lsp/client.js +672 -49
- package/dist/clients/lsp/config.js +504 -156
- package/dist/clients/lsp/diagnostic-binding.js +10 -5
- package/dist/clients/lsp/edits.js +15 -2
- package/dist/clients/lsp/index.js +1179 -247
- package/dist/clients/lsp/jvm-runtime.js +5 -0
- package/dist/clients/lsp/language.js +18 -169
- package/dist/clients/lsp/launch.js +38 -9
- package/dist/clients/lsp/pending-aux-coverage.js +113 -12
- package/dist/clients/lsp/server.js +426 -160
- package/dist/clients/lsp/session-roots.js +192 -0
- package/dist/clients/lsp/spawn-history.js +2 -7
- package/dist/clients/lsp/tsserver-sync.js +9 -2
- package/dist/clients/lsp/workspace-diagnostics-cache.js +17 -7
- package/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/dist/clients/lsp-mutation.js +179 -23
- package/dist/clients/map-with-concurrency.js +36 -0
- package/dist/clients/mcp/analyze.js +45 -10
- package/dist/clients/mcp/session.js +19 -17
- package/dist/clients/memory-sampler.js +28 -1
- package/dist/clients/message-end-attribution.js +30 -0
- package/dist/clients/metrics-history.js +21 -4
- package/dist/clients/middle-man-analysis.js +2 -4
- package/dist/clients/module-report.js +20 -45
- package/dist/clients/mutating-tool.js +651 -0
- package/dist/clients/mutation-attribution.js +368 -0
- package/dist/clients/mutation-bridge.js +240 -0
- package/dist/clients/ndjson-logger.js +247 -20
- package/dist/clients/observed-mutation-sources.js +101 -0
- package/dist/clients/observed-mutation.js +1215 -0
- package/dist/clients/opaque-mutation-scan.js +300 -40
- package/dist/clients/opengrep-config.js +4 -0
- package/dist/clients/package-manager.js +201 -44
- package/dist/clients/partial-edit-apply.js +359 -80
- package/dist/clients/path-attribution-telemetry.js +23 -7
- package/dist/clients/path-keyed-map.js +35 -3
- package/dist/clients/path-utils.js +460 -13
- package/dist/clients/persist-debounce.js +8 -1
- package/dist/clients/php-cs-fixer-config.js +114 -0
- package/dist/clients/pipeline.js +77 -24
- package/dist/clients/probe-home-state.js +228 -0
- package/dist/clients/process-bridge.js +66 -0
- package/dist/clients/process-singletons.js +210 -0
- package/dist/clients/process-snapshot.js +68 -0
- package/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
- package/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
- package/dist/clients/project-diagnostics/scanner.js +61 -24
- package/dist/clients/project-lens-config.js +393 -122
- package/dist/clients/project-snapshot.js +85 -35
- package/dist/clients/python-environment.js +217 -0
- package/dist/clients/python-provenance.js +639 -0
- package/dist/clients/quiet-window.js +6 -1
- package/dist/clients/read-bridge.js +13 -21
- package/dist/clients/read-expansion.js +9 -46
- package/dist/clients/read-guard-logger.js +39 -6
- package/dist/clients/read-guard-tool-lines.js +364 -212
- package/dist/clients/read-guard.js +95 -7
- package/dist/clients/recent-touches.js +17 -5
- package/dist/clients/resource-sampler.js +195 -96
- package/dist/clients/review-graph/builder.js +752 -132
- package/dist/clients/review-graph/service.js +55 -3
- package/dist/clients/review-graph/shared-extraction-ir.js +7 -15
- package/dist/clients/review-graph/tsconfig-paths.js +5 -1
- package/dist/clients/review-graph/workspace-modules.js +10 -74
- package/dist/clients/review-graph-logger.js +30 -4
- package/dist/clients/runtime-agent-end.js +18 -2
- package/dist/clients/runtime-context.js +49 -16
- package/dist/clients/runtime-coordinator.js +93 -6
- package/dist/clients/runtime-session.js +169 -34
- package/dist/clients/runtime-tool-call.js +183 -35
- package/dist/clients/runtime-tool-result.js +894 -241
- package/dist/clients/runtime-turn.js +1264 -74
- package/dist/clients/rust-client.js +31 -0
- package/dist/clients/safe-spawn.js +191 -28
- package/dist/clients/sanitize.js +7 -1
- package/dist/clients/scratch-tree-policy.js +1 -3
- package/dist/clients/security-scan-client.js +51 -17
- package/dist/clients/session-lifecycle.js +238 -42
- package/dist/clients/session-start-observability.js +79 -0
- package/dist/clients/sessionstart-logger.js +12 -3
- package/dist/clients/sg-runner.js +27 -15
- package/dist/clients/sgconfig.js +6 -1
- package/dist/clients/shared-checkout-guard.js +338 -0
- package/dist/clients/skills-resolver.js +105 -0
- package/dist/clients/smells-rollup.js +2 -2
- package/dist/clients/spawn-output-cap.js +37 -0
- package/dist/clients/spawn-timeout-cooldown.js +15 -1
- package/dist/clients/startup-scan.js +6 -7
- package/dist/clients/startup-timing.js +69 -4
- package/dist/clients/string-utils.js +33 -0
- package/dist/clients/subagent-mode.js +17 -4
- package/dist/clients/test-runner-client.js +586 -118
- package/dist/clients/test-runner-delivery.js +239 -0
- package/dist/clients/tool-definition.js +41 -1
- package/dist/clients/tool-policy.js +92 -22
- package/dist/clients/tree-sitter-cache.js +40 -40
- package/dist/clients/tree-sitter-client.js +97 -53
- package/dist/clients/tree-sitter-logger.js +19 -3
- package/dist/clients/tree-sitter-query-loader.js +81 -1
- package/dist/clients/tree-sitter-shared.js +51 -46
- package/dist/clients/tree-sitter-symbol-extractor.js +28 -0
- package/dist/clients/typos-config.js +5 -0
- package/dist/clients/user-notify.js +6 -2
- package/dist/clients/warm-attach.js +8 -10
- package/dist/clients/widget-state.js +191 -26
- package/dist/clients/word-index-logger.js +15 -3
- package/dist/clients/word-index-store.js +463 -0
- package/dist/clients/word-index.js +815 -125
- package/dist/clients/workspace-topology.js +25 -1
- package/dist/clients/zizmor-config.js +29 -6
- package/dist/index.js +68577 -53950
- package/dist/mcp/analyze-cli.js +3 -1
- package/dist/mcp/build-staleness.js +8 -7
- package/dist/mcp/cli.js +3 -1
- package/dist/mcp/server.js +133 -6
- package/dist/scripts/lib/process-scan.mjs +583 -0
- package/dist/scripts/lib/skills-predicate.mjs +129 -0
- package/dist/tools/effective-config.js +89 -0
- package/dist/tools/lens-diagnostics.js +99 -24
- package/dist/tools/lsp-diagnostics.js +81 -85
- package/dist/tools/lsp-navigation.js +40 -17
- package/dist/tools/shared.js +0 -1
- package/docs/agent-guide.md +68 -3
- package/docs/ast-grep_rules_catalog.md +41 -10
- package/docs/astplayground.md +26 -17
- package/docs/configuration.md +222 -0
- package/docs/dependencies.md +3 -3
- package/docs/features.md +44 -5
- package/docs/language-coverage.md +2 -2
- package/docs/pi-lens-fixer.md +25 -0
- package/docs/pi-lens-investigator.md +25 -0
- package/docs/pi-lens-reviewer.md +27 -0
- package/docs/pi-lens-subagent.md +38 -0
- package/docs/pi-lens-warden.md +55 -0
- package/docs/public-api-stability.md +359 -0
- package/docs/release-qa-baseline.md +182 -0
- package/docs/subagent-compat.md +110 -30
- package/docs/tree-sitter_rules_catalog.md +2 -2
- package/package.json +17 -21
- package/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
- package/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
- package/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
- package/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
- package/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
- package/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
- package/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
- package/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
- package/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
- package/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
- package/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
- package/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
- package/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
- package/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
- package/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
- package/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
- package/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
- package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
- package/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
- package/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
- package/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
- package/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
- package/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
- package/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
- package/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
- package/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
- package/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
- package/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
- package/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
- package/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
- package/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
- package/rules/ast-grep-rules/rules/no-important.yml +21 -0
- package/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
- package/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
- package/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
- package/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
- package/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
- package/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
- package/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
- package/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
- package/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
- package/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
- package/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
- package/scripts/analyze-pi-lens-logs.mjs +192 -1
- package/scripts/install-selftest.mjs +58 -2
- package/scripts/lib/skills-predicate.mjs +129 -0
- package/scripts/rpc-load-check.mjs +3 -1
|
@@ -4,16 +4,21 @@ import * as path from "node:path";
|
|
|
4
4
|
import { noteAuthoritativeContentAttachment } from "./agent-nudge.js";
|
|
5
5
|
import { captureFileStats, diffFileStats, getOpaqueBaselineStore, recoverOpaqueChangesViaGit, } from "./opaque-mutation-scan.js";
|
|
6
6
|
import { normalizeMapKey } from "./path-utils.js";
|
|
7
|
-
import { extractReadPathsFromCommand, extractDeletedPathsFromCommand, extractGrepSearchReadsFromOutput, extractWrittenPathsFromCommand, } from "./bash-file-access.js";
|
|
7
|
+
import { extractReadPathsFromCommand, extractDeletedPathsFromCommand, extractGrepSearchReadsFromOutput, extractWrittenPathsFromCommand, tokenizeShellCommand, } from "./bash-file-access.js";
|
|
8
8
|
import { registerSearchReads, } from "./search-read-registration.js";
|
|
9
9
|
import { createFileTime } from "./file-time.js";
|
|
10
10
|
import { publishFormatQueued } from "./format-events-publish.js";
|
|
11
|
-
import { isPathIgnoredByProject } from "./file-utils.js";
|
|
11
|
+
import { invalidateProjectIgnoreMatcherForPath, isPathIgnoredByProject, } from "./file-utils.js";
|
|
12
|
+
import { invalidateFormatterCacheForPath } from "./formatters.js";
|
|
12
13
|
import { getFormatService } from "./format-service.js";
|
|
13
14
|
import { isExternalOrVendorFile, normalizeEphemeralMapKey, pathsEqual, } from "./path-utils.js";
|
|
14
15
|
import { PathKeyedMap } from "./path-keyed-map.js";
|
|
15
16
|
import { resolveLanguageRootForFile } from "./language-profile.js";
|
|
16
17
|
import { logLatency } from "./latency-logger.js";
|
|
18
|
+
import { classifyMutatingTool, PI_LENS_SYNTHETIC_MUTATION_FIELD, } from "./mutating-tool.js";
|
|
19
|
+
import { hasPendingObservation, noteMutationHandled, settleObservedMutation, } from "./observed-mutation.js";
|
|
20
|
+
import { replayThroughMutationBridge, storedLineHashesFor, } from "./observed-mutation-sources.js";
|
|
21
|
+
import { getAmbientAbortSignal } from "./safe-spawn.js";
|
|
17
22
|
import { resolveToolCallCorrelationId } from "./tool-event.js";
|
|
18
23
|
import { boundedIndexesForCount, createReadGuardEditBatchSummary, getReadGuardCorrelationId, logReadGuardEvent, } from "./read-guard-logger.js";
|
|
19
24
|
import { notifyExternalFileChange } from "./lsp/index.js";
|
|
@@ -22,7 +27,72 @@ import { renderPostAutofixNotice, } from "./post-autofix-notice.js";
|
|
|
22
27
|
import { syncGitGuardRecord } from "./git-guard.js";
|
|
23
28
|
import { scheduleWordIndexPersist } from "./word-index.js";
|
|
24
29
|
import { RUNTIME_CONFIG } from "./runtime-config.js";
|
|
30
|
+
import { getActiveSessionId } from "./session-lifecycle.js";
|
|
25
31
|
const AUTHORITATIVE_CONTENT_MAX_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
|
|
32
|
+
/**
|
|
33
|
+
* Git subcommands that import ANOTHER commit's content into the index. The
|
|
34
|
+
* whole family, with a verdict for each (#2060):
|
|
35
|
+
* - merge, rebase, cherry-pick, pull, revert, am: IN. Each stages the other
|
|
36
|
+
* side's clean files beside the unmerged ones. `pull` is fetch+merge and
|
|
37
|
+
* `am --3way` reaches the same unmerged state, both probed on git 2.55.
|
|
38
|
+
* - stash pop / stash apply: OUT. A conflicted pop leaves `M ` entries too,
|
|
39
|
+
* but that content is the agent's OWN stashed work. Excluding it would
|
|
40
|
+
* destroy exactly what opaque recovery exists to capture.
|
|
41
|
+
* - checkout -m: OUT. Its "incoming" side is the agent's local modifications
|
|
42
|
+
* carried across the switch, so the same reasoning applies.
|
|
43
|
+
* - apply -3 / --3way: OUT. The patch is normally one the agent wrote, and
|
|
44
|
+
* `apply` is far more often used without conflicts, so the narrower default
|
|
45
|
+
* is to keep capturing.
|
|
46
|
+
* Membership only ARMS the filter; it still needs a real unmerged entry to do
|
|
47
|
+
* anything, so a non-integration use of a listed subcommand is inert.
|
|
48
|
+
*/
|
|
49
|
+
const GIT_INTEGRATION_SUBCOMMANDS = new Set([
|
|
50
|
+
"merge",
|
|
51
|
+
"rebase",
|
|
52
|
+
"cherry-pick",
|
|
53
|
+
"pull",
|
|
54
|
+
"revert",
|
|
55
|
+
"am",
|
|
56
|
+
]);
|
|
57
|
+
const GIT_GLOBAL_OPTIONS_WITH_VALUE = new Set([
|
|
58
|
+
"-C",
|
|
59
|
+
"-c",
|
|
60
|
+
"--config-env",
|
|
61
|
+
"--git-dir",
|
|
62
|
+
"--namespace",
|
|
63
|
+
"--work-tree",
|
|
64
|
+
]);
|
|
65
|
+
/**
|
|
66
|
+
* Failed integration commands are the one opaque-recovery case where Git's
|
|
67
|
+
* index contains changes made by the other branch rather than the agent.
|
|
68
|
+
* Keep this narrow: ordinary scripts and successful Git operations retain the
|
|
69
|
+
* normal recovery contract.
|
|
70
|
+
*/
|
|
71
|
+
export function isFailedGitIntegrationCommand(command, isError) {
|
|
72
|
+
if (isError !== true)
|
|
73
|
+
return false;
|
|
74
|
+
return tokenizeShellCommand(command).some(({ tokens, unsupported }) => {
|
|
75
|
+
if (unsupported)
|
|
76
|
+
return false;
|
|
77
|
+
const executable = path.win32
|
|
78
|
+
.basename(tokens[0] ?? "")
|
|
79
|
+
.toLowerCase()
|
|
80
|
+
.replace(/\.(?:cmd|exe)$/, "");
|
|
81
|
+
if (executable !== "git")
|
|
82
|
+
return false;
|
|
83
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
84
|
+
const token = tokens[index] ?? "";
|
|
85
|
+
if (GIT_GLOBAL_OPTIONS_WITH_VALUE.has(token)) {
|
|
86
|
+
index += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (token.startsWith("-"))
|
|
90
|
+
continue;
|
|
91
|
+
return GIT_INTEGRATION_SUBCOMMANDS.has(token);
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
26
96
|
function parseDiffRanges(diff) {
|
|
27
97
|
const changedLines = [];
|
|
28
98
|
for (const line of diff.split("\n")) {
|
|
@@ -66,6 +136,102 @@ const lastAnalyzedStateByFile = new PathKeyedMap(normalizeEphemeralMapKey);
|
|
|
66
136
|
export function clearLastAnalyzedStateCache() {
|
|
67
137
|
lastAnalyzedStateByFile.clear();
|
|
68
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Register one in-flight pipeline and hand back the inner map it landed in.
|
|
141
|
+
*
|
|
142
|
+
* Paired with {@link releaseInFlightPipeline}; split out of
|
|
143
|
+
* `dispatchPipelineAnalysis` so registration and deregistration are one seam
|
|
144
|
+
* with one owner (#2464 review round 3, F1) rather than two open-coded blocks
|
|
145
|
+
* ~80 lines apart whose invariants only line up if a reader checks both.
|
|
146
|
+
*
|
|
147
|
+
* Exported for the isolation red in
|
|
148
|
+
* `tests/clients/observed-mutation-integration.test.ts`: with
|
|
149
|
+
* `claimPipelineDispatch` below now consulted by BOTH dispatch call sites, the
|
|
150
|
+
* identity guard in the release has no reachable production trigger left, so
|
|
151
|
+
* the only honest way to prove it is to drive the seam directly.
|
|
152
|
+
*/
|
|
153
|
+
export function registerInFlightPipeline(filePath, stateHash, pipeline) {
|
|
154
|
+
let filePipelines = inFlightPipelines.get(filePath);
|
|
155
|
+
if (!filePipelines) {
|
|
156
|
+
filePipelines = new Map();
|
|
157
|
+
inFlightPipelines.set(filePath, filePipelines);
|
|
158
|
+
}
|
|
159
|
+
filePipelines.set(stateHash, pipeline);
|
|
160
|
+
return filePipelines;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Release one in-flight pipeline, pruning the per-file inner map once it is
|
|
164
|
+
* empty so a file touched once this session doesn't leave a permanent empty
|
|
165
|
+
* entry in the outer map.
|
|
166
|
+
*
|
|
167
|
+
* The IDENTITY check on the outer delete is load-bearing (#2464 review round 3,
|
|
168
|
+
* F1). `registered` is a reference captured BEFORE the pipeline await. Deleting
|
|
169
|
+
* by path alone assumes the outer entry still IS that map, and when it is not —
|
|
170
|
+
* anything that replaced it while this pipeline ran — the delete evicts a LIVE,
|
|
171
|
+
* unrelated pipeline's registration: its concurrent duplicates then stop
|
|
172
|
+
* deduping and its `participantIds`/`participantTotal` under-count. Delete only
|
|
173
|
+
* when the outer map still points at the very map this registration went into.
|
|
174
|
+
*/
|
|
175
|
+
export function releaseInFlightPipeline(filePath, stateHash, registered) {
|
|
176
|
+
registered.delete(stateHash);
|
|
177
|
+
if (registered.size === 0 && inFlightPipelines.get(filePath) === registered) {
|
|
178
|
+
inFlightPipelines.delete(filePath);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The two pre-conditions EVERY pipeline dispatch shares (#2464 review round 3,
|
|
183
|
+
* F1): the in-flight dedup for a concurrent call on the same post-write state,
|
|
184
|
+
* and the already-analysed latch for a sequential duplicate in the same turn.
|
|
185
|
+
*
|
|
186
|
+
* Both call sites of {@link dispatchPipelineAnalysis} consult this, so the
|
|
187
|
+
* PRE-conditions are as by-construction as the post-conditions that helper
|
|
188
|
+
* already owns. The observed-mutation path shipped without either check in
|
|
189
|
+
* round 2: two concurrent observed `tool_result`s for one file+hash both
|
|
190
|
+
* registered, the second overwrote the first's entry in the per-file map, and
|
|
191
|
+
* the first's release then evicted the whole outer entry — taking a live,
|
|
192
|
+
* unrelated classified pipeline's registration with it.
|
|
193
|
+
*
|
|
194
|
+
* ## Why this is SYNCHRONOUS and hands the join promise back
|
|
195
|
+
*
|
|
196
|
+
* Check-then-act here has to be atomic against the microtask queue.
|
|
197
|
+
* `dispatchPipelineAnalysis` registers synchronously before its own first
|
|
198
|
+
* await, so a caller that claims and then dispatches with no `await` in between
|
|
199
|
+
* cannot be interleaved. An `async` pre-check would reintroduce exactly that
|
|
200
|
+
* interleave — both racers see an empty registry, both resume, both dispatch —
|
|
201
|
+
* so the join's `await` is deliberately left to the caller.
|
|
202
|
+
*
|
|
203
|
+
* ## Why this is NOT folded into `dispatchPipelineAnalysis`
|
|
204
|
+
*
|
|
205
|
+
* On the classified chain the latch check sits ABOVE `addModifiedRange` and
|
|
206
|
+
* `recordProjectChange`. Moving it down into the helper would write
|
|
207
|
+
* `turn-state.json` ranges and an attributed change-log receipt for a state
|
|
208
|
+
* already analysed this turn — the duplicate-recording inversion #2464 exists
|
|
209
|
+
* to remove.
|
|
210
|
+
*/
|
|
211
|
+
export function claimPipelineDispatch(args) {
|
|
212
|
+
const { filePath, stateHash, turnIndex, participantId, dbg } = args;
|
|
213
|
+
// Deduplicate concurrent calls for the same final file state (pi can fire one
|
|
214
|
+
// tool_result per edit hunk). Do not dedupe by file alone: a distinct later
|
|
215
|
+
// same-turn edit to this file must still be analyzed.
|
|
216
|
+
const inFlight = inFlightPipelines.get(filePath)?.get(stateHash);
|
|
217
|
+
if (inFlight) {
|
|
218
|
+
dbg(`tool_result: skipping duplicate concurrent state for ${filePath}`);
|
|
219
|
+
if (inFlight.participantIds.length < 100) {
|
|
220
|
+
inFlight.participantIds.push(participantId);
|
|
221
|
+
}
|
|
222
|
+
inFlight.participantTotal += 1;
|
|
223
|
+
return { proceed: false, joined: inFlight.promise };
|
|
224
|
+
}
|
|
225
|
+
// Deduplicate sequential duplicate events for the same post-write state in
|
|
226
|
+
// the same turn while allowing later same-file edits whose content changed.
|
|
227
|
+
const lastAnalyzed = lastAnalyzedStateByFile.get(filePath);
|
|
228
|
+
if (lastAnalyzed?.turnIndex === turnIndex &&
|
|
229
|
+
lastAnalyzed.stateHash === stateHash) {
|
|
230
|
+
dbg(`tool_result: skipping already-analyzed file state this turn for ${filePath}`);
|
|
231
|
+
return { proceed: false, joined: undefined };
|
|
232
|
+
}
|
|
233
|
+
return { proceed: true };
|
|
234
|
+
}
|
|
69
235
|
const debouncedPipelines = new PathKeyedMap(normalizeEphemeralMapKey);
|
|
70
236
|
const DEFAULT_DEBOUNCE_MS = 0;
|
|
71
237
|
const MAX_DEBOUNCE_MS = 1000;
|
|
@@ -167,21 +333,32 @@ function getFileStateHash(filePath) {
|
|
|
167
333
|
return `unreadable:${code}`;
|
|
168
334
|
}
|
|
169
335
|
}
|
|
170
|
-
function getRequestedEditCount(event) {
|
|
171
|
-
if (
|
|
336
|
+
function getRequestedEditCount(event, kind) {
|
|
337
|
+
if (kind === "write")
|
|
172
338
|
return 1;
|
|
173
339
|
const edits = event.input?.edits;
|
|
174
340
|
return Array.isArray(edits) && edits.length > 0 ? edits.length : 1;
|
|
175
341
|
}
|
|
176
|
-
function getRequestedEditIndexes(event) {
|
|
177
|
-
return boundedIndexesForCount(getRequestedEditCount(event));
|
|
342
|
+
function getRequestedEditIndexes(event, kind) {
|
|
343
|
+
return boundedIndexesForCount(getRequestedEditCount(event, kind));
|
|
178
344
|
}
|
|
179
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Change-log attribution for one classified mutation (#2423).
|
|
347
|
+
*
|
|
348
|
+
* A third-party tool gets its OWN source (`agent-tool:<name>`) rather than
|
|
349
|
+
* being folded into `agent-edit`: a report that attributes a change to "the
|
|
350
|
+
* model's edit tool" must not silently absorb an extension's rewrite.
|
|
351
|
+
*/
|
|
352
|
+
function sourceForMutation(classification, details) {
|
|
180
353
|
if (details
|
|
181
354
|
?.piLensPartialApply) {
|
|
182
355
|
return "partial-apply";
|
|
183
356
|
}
|
|
184
|
-
|
|
357
|
+
if (classification.provenance === "builtin" ||
|
|
358
|
+
classification.provenance === "bash-derived") {
|
|
359
|
+
return classification.kind === "write" ? "agent-write" : "agent-edit";
|
|
360
|
+
}
|
|
361
|
+
return `agent-tool:${classification.toolName}`;
|
|
185
362
|
}
|
|
186
363
|
function singleRange(ranges) {
|
|
187
364
|
return ranges?.length === 1 ? ranges[0] : undefined;
|
|
@@ -198,8 +375,299 @@ function recordProjectChange(args) {
|
|
|
198
375
|
onAppendError: (err) => args.dbg(`project change log append failed for ${args.filePath}: ${err}`),
|
|
199
376
|
});
|
|
200
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* #2402 applied-edit records for a native edit tool, so an identical retry is
|
|
380
|
+
* recognized as already-applied instead of escalating through the
|
|
381
|
+
* oldText-not-found ladder. `input` carries the EXECUTED args (post-autopatch),
|
|
382
|
+
* the same text the preflight resolves against.
|
|
383
|
+
*
|
|
384
|
+
* Extracted (#2449 review round 4, S4) because the observational-settle skip
|
|
385
|
+
* path needs the IDENTICAL records: the mutation bridge does not write them, so
|
|
386
|
+
* an early return that skipped this block left every retry of an observed
|
|
387
|
+
* tool's edit looking unapplied. One body, two call sites — a second copy on the
|
|
388
|
+
* skip path is the drift this repo keeps catching.
|
|
389
|
+
*/
|
|
390
|
+
function recordNativeAppliedPairs(args) {
|
|
391
|
+
const pairs = [];
|
|
392
|
+
if (args.kind !== "edit")
|
|
393
|
+
return pairs;
|
|
394
|
+
const appliedInput = args.input;
|
|
395
|
+
const record = (oldText, newText) => {
|
|
396
|
+
if (typeof oldText === "string" && oldText.length > 0) {
|
|
397
|
+
args.runtime.partialApplyRecords.record(args.filePath, oldText, newText, args.stateHash);
|
|
398
|
+
pairs.push({ oldText, newText });
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
if (Array.isArray(appliedInput.edits)) {
|
|
402
|
+
for (const edit of appliedInput.edits)
|
|
403
|
+
record(edit.oldText, edit.newText);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
record(appliedInput.oldText, appliedInput.newText);
|
|
407
|
+
}
|
|
408
|
+
return pairs;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Keep `cachedExports` in sync after each write/edit so the pre-write STOP check
|
|
412
|
+
* does not fire on names that were removed from this file this session.
|
|
413
|
+
*
|
|
414
|
+
* Extracted for the same reason as {@link recordNativeAppliedPairs} (#2449
|
|
415
|
+
* review round 4, S4): the mutation bridge does not touch this cache, so the
|
|
416
|
+
* observational-settle skip path left it holding names an observed tool had
|
|
417
|
+
* just deleted.
|
|
418
|
+
*/
|
|
419
|
+
function refreshCachedExports(runtime, filePath) {
|
|
420
|
+
if (runtime.cachedExports.size === 0 || !nodeFs.existsSync(filePath))
|
|
421
|
+
return;
|
|
422
|
+
const exportRe = new RegExp("export\\s+(?:async\\s+)?(?:function|class|const|let|type|interface)\\s+(\\w+)", "g");
|
|
423
|
+
for (const [name, file] of runtime.cachedExports) {
|
|
424
|
+
if (path.resolve(file) === path.resolve(filePath)) {
|
|
425
|
+
runtime.cachedExports.delete(name);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
const freshContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
430
|
+
for (const match of freshContent.matchAll(exportRe)) {
|
|
431
|
+
const name = match[1];
|
|
432
|
+
if (!runtime.cachedExports.has(name)) {
|
|
433
|
+
runtime.cachedExports.set(name, filePath);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
// Non-fatal — stale entry is worse than a missing one
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* #2464: the pipeline's own lint/diagnostics dispatch, factored out of the
|
|
443
|
+
* block below that ALSO writes `turn-state.json` modified ranges and the
|
|
444
|
+
* attributed change-log receipt (`cacheManager.addModifiedRange` /
|
|
445
|
+
* `recordProjectChange`). The classified chain in `handleToolResult` still
|
|
446
|
+
* does both, in the same order as before this split — see the call site
|
|
447
|
+
* right after `recordProjectChange` below, which is byte-identical to what
|
|
448
|
+
* used to be inlined there.
|
|
449
|
+
*
|
|
450
|
+
* The observed-mutation early return (#2430/#2449 round 4, S4) is the SECOND
|
|
451
|
+
* caller: the mutation bridge already recorded the turn-state range and the
|
|
452
|
+
* change-log receipt for that edit, so it calls this helper for analysis
|
|
453
|
+
* WITHOUT asking for the recording a second time — which a shared "run the
|
|
454
|
+
* pipeline AND record" block could not express.
|
|
455
|
+
*/
|
|
456
|
+
async function dispatchPipelineAnalysis(args) {
|
|
457
|
+
const { deps, runtime, filePath, dispatchCwd, turnStateCwd, autofixMode, modifiedRanges, writeIndex, initialStateHash, readGuardCorrelationId, requestedEditIndexes, requestedEditTotal, isPartialApplyResult, toolResultStart, nativeAppliedPairs, } = args;
|
|
458
|
+
const { event, getFlag, getFlagSource, dbg, biomeClient, ruffClient, metricsClient, resetLSPService, } = deps;
|
|
459
|
+
const pipelinePromise = runPipeline({
|
|
460
|
+
filePath,
|
|
461
|
+
cwd: dispatchCwd,
|
|
462
|
+
projectRoot: turnStateCwd,
|
|
463
|
+
toolName: event.toolName,
|
|
464
|
+
autofixMode,
|
|
465
|
+
modifiedRanges,
|
|
466
|
+
telemetry: {
|
|
467
|
+
model: runtime.telemetryModel,
|
|
468
|
+
sessionId: runtime.telemetrySessionId,
|
|
469
|
+
turnIndex: runtime.turnIndex,
|
|
470
|
+
writeIndex,
|
|
471
|
+
modelId: runtime.telemetryModelId,
|
|
472
|
+
provider: runtime.telemetryProviderId,
|
|
473
|
+
},
|
|
474
|
+
getFlag,
|
|
475
|
+
getFlagSource,
|
|
476
|
+
dbg,
|
|
477
|
+
// #451: hand the deferred cascade live sequence accessors so the
|
|
478
|
+
// review-graph builder can skip its per-build O(project) sweep when
|
|
479
|
+
// only pi-observed edits happened. projectSeq is a function because the
|
|
480
|
+
// cascade runs after this returns (#450) — read current, not captured.
|
|
481
|
+
seqState: {
|
|
482
|
+
projectSeq: () => runtime.projectSeq,
|
|
483
|
+
getFilesChangedSince: (seq) => runtime.getFilesChangedSince(seq),
|
|
484
|
+
},
|
|
485
|
+
// The settle clock is live because the deferred cascade may reach its
|
|
486
|
+
// budget derivation before or after turn_end starts waiting.
|
|
487
|
+
turnEndCascadeSettleStart: () => runtime.getTurnEndCascadeSettleStart(),
|
|
488
|
+
// #348 phase 2: live reference so the deferred cascade can update the
|
|
489
|
+
// warm word index in place at the same seam as the graph rebuild.
|
|
490
|
+
// `runtime.wordIndex` is read fresh (not captured) via this closure-free
|
|
491
|
+
// property access being re-evaluated at object-literal construction
|
|
492
|
+
// time here — that's fine because runPipeline reads `ctx.wordIndex`
|
|
493
|
+
// synchronously into computeCascadeForFile's options before returning
|
|
494
|
+
// (the deferred part is the cascade's OWN execution, not this handoff).
|
|
495
|
+
wordIndex: runtime.wordIndex,
|
|
496
|
+
onWordIndexUpdated: (index) => {
|
|
497
|
+
scheduleWordIndexPersist(dispatchCwd, index, dbg);
|
|
498
|
+
},
|
|
499
|
+
}, {
|
|
500
|
+
biomeClient,
|
|
501
|
+
ruffClient,
|
|
502
|
+
metricsClient,
|
|
503
|
+
getFormatService,
|
|
504
|
+
fixedThisTurn: runtime.fixedThisTurn,
|
|
505
|
+
});
|
|
506
|
+
const pipelineTelemetry = {
|
|
507
|
+
promise: pipelinePromise,
|
|
508
|
+
participantIds: [...new Set(args.participantIds)].slice(0, 100),
|
|
509
|
+
participantTotal: args.participantTotal,
|
|
510
|
+
};
|
|
511
|
+
// Synchronous, and the FIRST thing after `runPipeline` handed back its
|
|
512
|
+
// promise: `claimPipelineDispatch` at each call site is only atomic because
|
|
513
|
+
// nothing awaits between the claim and this registration.
|
|
514
|
+
const registeredPipelines = registerInFlightPipeline(filePath, initialStateHash, pipelineTelemetry);
|
|
515
|
+
let result;
|
|
516
|
+
try {
|
|
517
|
+
result = await pipelinePromise;
|
|
518
|
+
}
|
|
519
|
+
catch (pipelineErr) {
|
|
520
|
+
if (getFlag("lens-guard")) {
|
|
521
|
+
runtime.markGitGuardCacheUnknown("pipeline_crash");
|
|
522
|
+
}
|
|
523
|
+
dbg(`runPipeline crashed: ${pipelineErr}`);
|
|
524
|
+
logReadGuardEvent({
|
|
525
|
+
event: "edit_post_edit_pipeline_failed",
|
|
526
|
+
correlationId: readGuardCorrelationId,
|
|
527
|
+
filePath,
|
|
528
|
+
metadata: {
|
|
529
|
+
tool: event.toolName,
|
|
530
|
+
commitStatus: "committed",
|
|
531
|
+
reasonCode: "pipeline_failed",
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
logReadGuardEvent({
|
|
535
|
+
event: "edit_batch_summary",
|
|
536
|
+
correlationId: readGuardCorrelationId,
|
|
537
|
+
filePath,
|
|
538
|
+
metadata: {
|
|
539
|
+
tool: event.toolName,
|
|
540
|
+
editBatchSummary: createReadGuardEditBatchSummary({
|
|
541
|
+
requestedIndexes: requestedEditIndexes,
|
|
542
|
+
requestedTotal: requestedEditTotal,
|
|
543
|
+
resolvedIndexes: requestedEditIndexes,
|
|
544
|
+
resolvedTotal: requestedEditTotal,
|
|
545
|
+
appliedIndexes: requestedEditIndexes,
|
|
546
|
+
appliedTotal: requestedEditTotal,
|
|
547
|
+
participantIds: pipelineTelemetry.participantIds,
|
|
548
|
+
participantTotal: pipelineTelemetry.participantTotal,
|
|
549
|
+
commitStatus: "committed",
|
|
550
|
+
postEditStatus: "failed",
|
|
551
|
+
terminalStatus: "failed",
|
|
552
|
+
durationMs: Date.now() - toolResultStart,
|
|
553
|
+
}),
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
dbg(`runPipeline crash stack: ${pipelineErr.stack}`);
|
|
557
|
+
// The LSP fleet is process-wide, but a pipeline crash belongs to one
|
|
558
|
+
// evaluation. A registered primary owns the fleet; a known secondary
|
|
559
|
+
// must not tear it down. Keep the historical reset when no registration
|
|
560
|
+
// exists because synthetic callers and early startup have no role evidence.
|
|
561
|
+
const activePrimarySessionId = getActiveSessionId();
|
|
562
|
+
const crashBelongsToPrimary = activePrimarySessionId === undefined ||
|
|
563
|
+
activePrimarySessionId === runtime.telemetrySessionId;
|
|
564
|
+
if (!getFlag("no-lsp") && crashBelongsToPrimary) {
|
|
565
|
+
resetLSPService({ fast: true, reason: "pipeline_crash" });
|
|
566
|
+
}
|
|
567
|
+
logLatency({
|
|
568
|
+
type: "tool_result",
|
|
569
|
+
toolName: event.toolName,
|
|
570
|
+
filePath,
|
|
571
|
+
durationMs: Date.now() - toolResultStart,
|
|
572
|
+
result: "pipeline_crash",
|
|
573
|
+
});
|
|
574
|
+
const notice = runtime.formatPipelineCrashNotice(filePath, pipelineErr);
|
|
575
|
+
return {
|
|
576
|
+
crashed: true,
|
|
577
|
+
response: {
|
|
578
|
+
content: notice
|
|
579
|
+
? [...event.content, { type: "text", text: notice }]
|
|
580
|
+
: event.content,
|
|
581
|
+
isError: true,
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
releaseInFlightPipeline(filePath, initialStateHash, registeredPipelines);
|
|
587
|
+
}
|
|
588
|
+
if (!isPartialApplyResult) {
|
|
589
|
+
const postEditStatus = result.isError ? "failed" : "succeeded";
|
|
590
|
+
if (result.isError) {
|
|
591
|
+
logReadGuardEvent({
|
|
592
|
+
event: "edit_post_edit_pipeline_failed",
|
|
593
|
+
correlationId: readGuardCorrelationId,
|
|
594
|
+
filePath,
|
|
595
|
+
metadata: {
|
|
596
|
+
tool: event.toolName,
|
|
597
|
+
commitStatus: "committed",
|
|
598
|
+
reasonCode: "pipeline_failed",
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
logReadGuardEvent({
|
|
603
|
+
event: "edit_batch_summary",
|
|
604
|
+
correlationId: readGuardCorrelationId,
|
|
605
|
+
filePath,
|
|
606
|
+
metadata: {
|
|
607
|
+
tool: event.toolName,
|
|
608
|
+
editBatchSummary: createReadGuardEditBatchSummary({
|
|
609
|
+
requestedIndexes: requestedEditIndexes,
|
|
610
|
+
requestedTotal: requestedEditTotal,
|
|
611
|
+
resolvedIndexes: requestedEditIndexes,
|
|
612
|
+
resolvedTotal: requestedEditTotal,
|
|
613
|
+
appliedIndexes: requestedEditIndexes,
|
|
614
|
+
appliedTotal: requestedEditTotal,
|
|
615
|
+
participantIds: pipelineTelemetry.participantIds,
|
|
616
|
+
participantTotal: pipelineTelemetry.participantTotal,
|
|
617
|
+
commitStatus: "committed",
|
|
618
|
+
postEditStatus,
|
|
619
|
+
terminalStatus: postEditStatus === "failed" ? "failed" : "success",
|
|
620
|
+
durationMs: Date.now() - toolResultStart,
|
|
621
|
+
}),
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
// ── Post-pipeline post-conditions (#2464 review round 2, S1) ───────────────
|
|
626
|
+
// These live INSIDE the helper rather than at its call sites so EVERY
|
|
627
|
+
// provenance gets them by construction. The observed path used to discard
|
|
628
|
+
// `result` entirely, so under `--immediate-format` the pipeline rewrote the
|
|
629
|
+
// file and none of the three below ran: the next edit was then blocked with
|
|
630
|
+
// a spurious `file_modified` (the read-guard staleness stamp was never
|
|
631
|
+
// re-taken), an identical retry escalated through the oldText-not-found
|
|
632
|
+
// ladder (the #2402 after-write hash was never stamped), and a duplicate
|
|
633
|
+
// tool_result for the same bytes analysed the file a second time (the
|
|
634
|
+
// already-analysed latch was never set).
|
|
635
|
+
const finalStateHash = getFileStateHash(filePath);
|
|
636
|
+
lastAnalyzedStateByFile.set(filePath, {
|
|
637
|
+
turnIndex: runtime.turnIndex,
|
|
638
|
+
stateHash: finalStateHash,
|
|
639
|
+
});
|
|
640
|
+
// #2402: pi-lens' own immediate format/autofix may have rewritten the file
|
|
641
|
+
// after the native edit was recorded by the caller. Stamp the post-pipeline
|
|
642
|
+
// state so an identical retry against the formatted bytes is still
|
|
643
|
+
// recognized as already-applied. Only the pairs recorded this invocation are
|
|
644
|
+
// stamped, and only when the pipeline actually changed the bytes.
|
|
645
|
+
// `initialStateHash` IS the post-write hash at both call sites — the
|
|
646
|
+
// classified chain passes `postWriteStateHash` verbatim, the observed path
|
|
647
|
+
// passes the same pre-settle digest.
|
|
648
|
+
if (finalStateHash !== initialStateHash) {
|
|
649
|
+
for (const pair of nativeAppliedPairs) {
|
|
650
|
+
runtime.partialApplyRecords.noteAfterWriteHash(filePath, pair.oldText, pair.newText, finalStateHash);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
// The model's write/edit and pi-lens' own immediate format/autofix are now
|
|
654
|
+
// reflected on disk. Refresh read-guard staleness stamps so a follow-up edit
|
|
655
|
+
// is judged by read-range coverage, not by our own previous write.
|
|
656
|
+
if (!getFlag("no-read-guard")) {
|
|
657
|
+
const changedForReadGuard = new Set([
|
|
658
|
+
path.resolve(filePath),
|
|
659
|
+
...(result.changedFiles ?? []).map((changedFile) => path.resolve(changedFile)),
|
|
660
|
+
]);
|
|
661
|
+
for (const changedFile of changedForReadGuard) {
|
|
662
|
+
if (nodeFs.existsSync(changedFile)) {
|
|
663
|
+
deps.readGuard?.recordWritten(changedFile);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return { crashed: false, result };
|
|
668
|
+
}
|
|
201
669
|
export async function handleToolResult(deps) {
|
|
202
|
-
const { event, getFlag,
|
|
670
|
+
const { event, getFlag, dbg, runtime, cacheManager, agentBehaviorRecord, formatBehaviorWarnings, } = deps;
|
|
203
671
|
const rawFilePath = event.input.path;
|
|
204
672
|
const workspaceRoot = runtime.projectRoot || process.cwd();
|
|
205
673
|
// #1642: a gitignored worktree edit got re-attributed onto a
|
|
@@ -269,6 +737,10 @@ export async function handleToolResult(deps) {
|
|
|
269
737
|
? rawFilePath
|
|
270
738
|
: path.resolve(resolutionBasis, rawFilePath)
|
|
271
739
|
: rawFilePath;
|
|
740
|
+
if (filePath) {
|
|
741
|
+
invalidateProjectIgnoreMatcherForPath(filePath);
|
|
742
|
+
invalidateFormatterCacheForPath(filePath);
|
|
743
|
+
}
|
|
272
744
|
// Purely diagnostic: tool_call's call-time verdict (computed on ITS OWN
|
|
273
745
|
// resolved path, which may since have been superseded) disagreed with
|
|
274
746
|
// what actually executed. This does NOT gate anything by itself — the
|
|
@@ -351,16 +823,41 @@ export async function handleToolResult(deps) {
|
|
|
351
823
|
}
|
|
352
824
|
else if (pending.strategy === "git") {
|
|
353
825
|
// Git-first: no universe cap - works on any repo size.
|
|
354
|
-
const recovery = await recoverOpaqueChangesViaGit(scanRoot, pending.startedAt
|
|
826
|
+
const recovery = await recoverOpaqueChangesViaGit(scanRoot, pending.startedAt, {
|
|
827
|
+
excludeIndexOnlyWhenUnmerged: isFailedGitIntegrationCommand(command, event.isError),
|
|
828
|
+
});
|
|
355
829
|
if (recovery.verdict === "recovered") {
|
|
356
830
|
opaquePaths = recovery.paths.filter((p) => !isExternalOrVendorFile(p, scanRoot) &&
|
|
357
831
|
!isPathIgnoredByProject(p, scanRoot, false));
|
|
358
832
|
}
|
|
359
|
-
else if (recovery.verdict === "unknown"
|
|
360
|
-
//
|
|
361
|
-
//
|
|
833
|
+
else if (recovery.verdict === "unknown") {
|
|
834
|
+
// #2060: deliberately WIDER than the old `recognized.length > 0`
|
|
835
|
+
// guard. A fully opaque command whose probe failed is the shape
|
|
836
|
+
// whose coverage is least knowable, and it used to record
|
|
837
|
+
// nothing at all.
|
|
362
838
|
unknownReason = recovery.unknownReason;
|
|
363
839
|
}
|
|
840
|
+
// #2060: both counts are bounded (one record per tool_result, no
|
|
841
|
+
// per-path logging) and exist because filtering is invisible in
|
|
842
|
+
// production otherwise - the dropped paths simply never appear.
|
|
843
|
+
if ((recovery.excludedIncomingCount ?? 0) > 0) {
|
|
844
|
+
logLatency({
|
|
845
|
+
type: "phase",
|
|
846
|
+
phase: "opaque_mutation_incoming_excluded",
|
|
847
|
+
filePath: command.slice(0, 80),
|
|
848
|
+
durationMs: Date.now() - started,
|
|
849
|
+
result: `excluded:${recovery.excludedIncomingCount}`,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
if ((recovery.unknownStatusCount ?? 0) > 0) {
|
|
853
|
+
logLatency({
|
|
854
|
+
type: "phase",
|
|
855
|
+
phase: "opaque_mutation_status_pair_unknown",
|
|
856
|
+
filePath: command.slice(0, 80),
|
|
857
|
+
durationMs: Date.now() - started,
|
|
858
|
+
result: `kept:${recovery.unknownStatusCount}`,
|
|
859
|
+
});
|
|
860
|
+
}
|
|
364
861
|
}
|
|
365
862
|
else if (pending.stats) {
|
|
366
863
|
const outcome = await captureFileStats(scanRoot, {
|
|
@@ -427,6 +924,10 @@ export async function handleToolResult(deps) {
|
|
|
427
924
|
toolName: "write",
|
|
428
925
|
input: { path: wp },
|
|
429
926
|
isError: false,
|
|
927
|
+
// #2423: pi-lens SYNTHESIZED this write from a bash command, so the
|
|
928
|
+
// seam reports `provenance: "bash-derived"` and a consumer can tell
|
|
929
|
+
// it apart from the host's own write tool.
|
|
930
|
+
[PI_LENS_SYNTHETIC_MUTATION_FIELD]: "bash",
|
|
430
931
|
};
|
|
431
932
|
const syntheticResult = await handleToolResult({
|
|
432
933
|
...deps,
|
|
@@ -525,8 +1026,254 @@ export async function handleToolResult(deps) {
|
|
|
525
1026
|
});
|
|
526
1027
|
}
|
|
527
1028
|
}
|
|
528
|
-
|
|
529
|
-
|
|
1029
|
+
// #2423: THE inbound gate. Everything below this line is the mutation
|
|
1030
|
+
// bookkeeping chain — read-guard staleness stamp, turn state, change-log
|
|
1031
|
+
// receipt, deferred autofix and format. Before the seam this compared
|
|
1032
|
+
// `event.toolName` to two literals, so a host or extension edit tool under
|
|
1033
|
+
// any other name was dropped here with the path already resolved.
|
|
1034
|
+
// `recognizeOnly` (#2423 review round 1, finding F5): the tool_call side
|
|
1035
|
+
// already ran the adapters against the PRE-edit file and logged what they
|
|
1036
|
+
// resolved. Re-running them here logged a second `touched_lines_detected`
|
|
1037
|
+
// for every edit and an `edit_preflight_blocked` for edits that were never
|
|
1038
|
+
// blocked, and any anchor it re-resolved would be resolved against content
|
|
1039
|
+
// the edit has already rewritten. The seam carries the tool_call ranges
|
|
1040
|
+
// forward by `toolCallId` instead.
|
|
1041
|
+
const mutation = classifyMutatingTool(event, {
|
|
1042
|
+
filePath: filePath || undefined,
|
|
1043
|
+
sessionId: deps.sessionId,
|
|
1044
|
+
recognizeOnly: true,
|
|
1045
|
+
});
|
|
1046
|
+
// #2430: settle the observational baseline BEFORE the classification gate
|
|
1047
|
+
// returns. On the FIRST call of an unknown tool nothing below this line will
|
|
1048
|
+
// run — that is the bug — so the disk diff is the only thing that can put
|
|
1049
|
+
// the file in `turn-state.json`, and it replays through the mutation bridge
|
|
1050
|
+
// to get the identical bookkeeping a `write` gets.
|
|
1051
|
+
//
|
|
1052
|
+
// The PROBE is synchronous and is the first thing asked, so the
|
|
1053
|
+
// overwhelmingly common call — no baseline armed — pays one map lookup and
|
|
1054
|
+
// nothing else (#2449 round 2, F1).
|
|
1055
|
+
//
|
|
1056
|
+
// The SETTLE is async (#2449 round 3, T4: no synchronous filesystem work on
|
|
1057
|
+
// this path), so it yields. `handleToolResult` has no other `await` before
|
|
1058
|
+
// it registers with `debouncedPipelines`/`inFlightPipelines`, and
|
|
1059
|
+
// `tests/clients/runtime-tool-result-debounce.test.ts` asserts that a
|
|
1060
|
+
// racing second tool_result for the same path cannot miss the first's
|
|
1061
|
+
// entry — so everything the chain below derives from the file's POST-RESULT
|
|
1062
|
+
// bytes is read HERE, before the yield. Otherwise the racing call rewrites
|
|
1063
|
+
// the file while this one is awaiting and this one registers under the
|
|
1064
|
+
// OTHER call's state hash, collapsing two distinct pipelines into one.
|
|
1065
|
+
const observationPending = !getFlag("no-read-guard") &&
|
|
1066
|
+
hasPendingObservation(resolveToolCallCorrelationId(event));
|
|
1067
|
+
// #2464 review round 2 (S2): NOT gated on `mutation !== undefined` any more.
|
|
1068
|
+
// The observed path now dispatches on call ONE of an unknown tool too, and
|
|
1069
|
+
// that call is by definition unclassified — gating the pre-yield digest on a
|
|
1070
|
+
// classification that does not exist yet would have left exactly the racing
|
|
1071
|
+
// re-read this hoist exists to prevent.
|
|
1072
|
+
const preSettleStateHash = observationPending && filePath ? getFileStateHash(filePath) : undefined;
|
|
1073
|
+
let observedReplayed = 0;
|
|
1074
|
+
let observedChangedPaths = [];
|
|
1075
|
+
if (observationPending) {
|
|
1076
|
+
const observed = await settleObservedMutation({
|
|
1077
|
+
toolCallId: resolveToolCallCorrelationId(event),
|
|
1078
|
+
toolName: event.toolName,
|
|
1079
|
+
sessionGeneration: runtime.sessionGeneration,
|
|
1080
|
+
turnIndex: runtime.turnIndex,
|
|
1081
|
+
signal: getAmbientAbortSignal(),
|
|
1082
|
+
record: replayThroughMutationBridge,
|
|
1083
|
+
getStoredLineHashes: (candidate) => storedLineHashesFor(deps.readGuard, candidate),
|
|
1084
|
+
isRecordable: (candidate) => !isExternalOrVendorFile(candidate, workspaceRoot) &&
|
|
1085
|
+
!isPathIgnoredByProject(candidate, workspaceRoot, false),
|
|
1086
|
+
dbg,
|
|
1087
|
+
});
|
|
1088
|
+
observedReplayed = observed.replayed;
|
|
1089
|
+
observedChangedPaths = observed.changedPaths;
|
|
1090
|
+
if (observed.replayed > 0) {
|
|
1091
|
+
dbg(`tool_result: observed ${observed.replayed} mutation(s) from tool "${event.toolName}"`);
|
|
1092
|
+
}
|
|
1093
|
+
else if (observed.reason) {
|
|
1094
|
+
dbg(`tool_result: observation for "${event.toolName}" did not settle: ${observed.reason}`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
// #2464 review round 2 (S5): ONE clock, started above BOTH exits, so
|
|
1098
|
+
// `edit_batch_summary.durationMs` and the `tool_result` latency row measure
|
|
1099
|
+
// the same span whichever provenance a tool takes. The observed path used to
|
|
1100
|
+
// start its clock at the dispatch call itself, so its durations were
|
|
1101
|
+
// pipeline-only and silently incomparable with the classified path's.
|
|
1102
|
+
// Deliberately below the settle: neither path should be charged for the
|
|
1103
|
+
// observational disk diff, which is arm-time work, not tool_result work.
|
|
1104
|
+
const toolResultStart = Date.now();
|
|
1105
|
+
// #2449 review round 3 (S2), narrowed in round 4 (S4). A tool learned from
|
|
1106
|
+
// ONE observation is classified by NAME from here on AND is still armed —
|
|
1107
|
+
// that second property is what makes a second observation, and therefore
|
|
1108
|
+
// persistence, reachable at all (round 2, F2). Both halves then recorded the
|
|
1109
|
+
// same physical edit: the settle above replayed it through the mutation
|
|
1110
|
+
// bridge with measured ranges, and the chain below recorded it AGAIN as a
|
|
1111
|
+
// whole-file change, so a three-edit session produced four change-log
|
|
1112
|
+
// receipts.
|
|
1113
|
+
//
|
|
1114
|
+
// The settle wins, deliberately. It ran the disk diff, so it knows which
|
|
1115
|
+
// LINES moved; the chain, reached through `recognizeOnly`, has no ranges for
|
|
1116
|
+
// an unknown tool and over-approximates to the file. Skipping the arm
|
|
1117
|
+
// instead would be the other way to fix this, and it is the wrong one — it
|
|
1118
|
+
// makes PERSIST_AFTER_OBSERVATIONS unreachable again.
|
|
1119
|
+
//
|
|
1120
|
+
// ## What this return skips, stated in full (round 4, S4; #2464)
|
|
1121
|
+
//
|
|
1122
|
+
// The first cut called this "skipping turn tracking" and returned. It skipped
|
|
1123
|
+
// considerably more than turn tracking, and THREE of those steps have no
|
|
1124
|
+
// counterpart in `recordMutationThroughSeam`, so nothing else ran them. Those
|
|
1125
|
+
// three run here, before the return:
|
|
1126
|
+
//
|
|
1127
|
+
// - the #2402 applied-edit records — without them an identical retry of
|
|
1128
|
+
// the observed tool's edit escalates through the oldText-not-found ladder
|
|
1129
|
+
// instead of being recognized as already applied;
|
|
1130
|
+
// - `recordMutationToolReceipt`, the write→edit sticky turn transition,
|
|
1131
|
+
// which is pure ordering state the bridge never touches;
|
|
1132
|
+
// - the `cachedExports` refresh — without it the pre-write STOP check keeps
|
|
1133
|
+
// firing on names this very edit removed.
|
|
1134
|
+
//
|
|
1135
|
+
// What stays skipped is skipped because the BRIDGE already did it: the
|
|
1136
|
+
// read-guard staleness stamp, the `turn-state.json` modified ranges, the
|
|
1137
|
+
// attributed change-log receipt, and the deferred autofix/format pair —
|
|
1138
|
+
// `recordMutationThroughSeam` steps 1-4. #2464 pulled the pipeline's own
|
|
1139
|
+
// lint/diagnostics dispatch (`dispatchPipelineAnalysis`, defined above) out
|
|
1140
|
+
// of the block below that also writes those same turn-state ranges and the
|
|
1141
|
+
// change-log receipt, so it can be called HERE too — analysis without asking
|
|
1142
|
+
// for the recording a second time.
|
|
1143
|
+
//
|
|
1144
|
+
// ## Why this sits ABOVE the classification gates (#2464 review round 2, S2)
|
|
1145
|
+
//
|
|
1146
|
+
// It used to sit BELOW `mutation === undefined`, which meant CALL ONE of every
|
|
1147
|
+
// unknown tool — the call the observational net exists for — was recorded by
|
|
1148
|
+
// the bridge and then returned unanalysed: `runPipeline` was never reached, so
|
|
1149
|
+
// the file was fixed and formatted by the `agent_settled` drain but never
|
|
1150
|
+
// linted, and AC2 ("an observed mutation gets its pipeline analysis in the
|
|
1151
|
+
// same turn") was met on the second call only. `observedReplayed > 0` is the
|
|
1152
|
+
// whole condition now: the bridge recorded this edit, whoever the tool is.
|
|
1153
|
+
if (observedReplayed > 0) {
|
|
1154
|
+
// ## The `hostToolResultFailed` asymmetry, stated rather than hidden
|
|
1155
|
+
//
|
|
1156
|
+
// The classified chain returns below on `event.isError === true` with a
|
|
1157
|
+
// `commitStatus: "failed"` receipt; this block does not consult it, so an
|
|
1158
|
+
// unknown tool that reports failure and STILL moved bytes is recorded,
|
|
1159
|
+
// analysed, and receipted as `committed`. That is deliberate: the
|
|
1160
|
+
// observation is disk evidence, not a claim — the bytes changed whatever
|
|
1161
|
+
// the tool says about itself — and the classified early return is safe
|
|
1162
|
+
// only because a failed host edit did not write. Reading a third-party
|
|
1163
|
+
// tool's self-reported status as authority over what is on disk is the
|
|
1164
|
+
// exact gap the observational net exists to close.
|
|
1165
|
+
//
|
|
1166
|
+
// The gates below (`mutation === undefined` / `!filePath` /
|
|
1167
|
+
// `isExternalOrVendorFile`) are deliberately NOT consulted for the SKIP:
|
|
1168
|
+
// once the bridge has recorded the edit, re-running the classified chain
|
|
1169
|
+
// double-records it regardless of how the tool classifies. They are
|
|
1170
|
+
// consulted for what this block can still usefully DO.
|
|
1171
|
+
if (filePath && !isExternalOrVendorFile(filePath, workspaceRoot)) {
|
|
1172
|
+
// On call ONE the tool is not classified at all, so there is no
|
|
1173
|
+
// `mutation.kind` to read. The observation itself is the authority and
|
|
1174
|
+
// it only ever records "edit" (`settleObservedMutation` hard-codes
|
|
1175
|
+
// `kind: "edit"`, and #2430 tier 4 only ever classifies a learned tool
|
|
1176
|
+
// as "edit"), so the two agree wherever both exist.
|
|
1177
|
+
const observedKind = mutation?.kind ?? "edit";
|
|
1178
|
+
const observedStateHash = preSettleStateHash ?? getFileStateHash(filePath);
|
|
1179
|
+
const observedAppliedPairs = recordNativeAppliedPairs({
|
|
1180
|
+
runtime,
|
|
1181
|
+
input: event.input,
|
|
1182
|
+
kind: observedKind,
|
|
1183
|
+
filePath,
|
|
1184
|
+
stateHash: observedStateHash,
|
|
1185
|
+
});
|
|
1186
|
+
const receiptOutcome = runtime.recordMutationToolReceipt?.call(runtime, filePath, observedKind);
|
|
1187
|
+
refreshCachedExports(runtime, filePath);
|
|
1188
|
+
// Same fallback formula the classified chain uses below, minus its
|
|
1189
|
+
// `_bypassDebounce` branch (#2464 review round 2, S3): that branch was
|
|
1190
|
+
// unreachable here. Both callers that set `_bypassDebounce` arrive with
|
|
1191
|
+
// no pending baseline — `scheduleDebounced`'s re-entry because THIS
|
|
1192
|
+
// call already consumed it at the settle above, and the bash-derived
|
|
1193
|
+
// synthetic write because `runtime-tool-call.ts` excludes `bash` from
|
|
1194
|
+
// arming outright (`toolName !== "bash"`). `observedKind` is always
|
|
1195
|
+
// "edit", so the un-receipted default is "deferred" either way.
|
|
1196
|
+
const observedAutofixMode = receiptOutcome?.autofixMode ??
|
|
1197
|
+
(observedKind === "edit" ? "deferred" : "immediate");
|
|
1198
|
+
// Analyse the file the observation actually RECORDED, not merely the
|
|
1199
|
+
// one the tool named: a directory-target tool names a path that is not
|
|
1200
|
+
// a file at all, and `runPipeline` on it is meaningless. `changedPaths`
|
|
1201
|
+
// is already filtered by the `isRecordable` predicate handed to the
|
|
1202
|
+
// settle above, so membership here also implies not-vendored and
|
|
1203
|
+
// not-gitignored — the same two gates the classified chain applies
|
|
1204
|
+
// before its own dispatch.
|
|
1205
|
+
if (observedChangedPaths.some((candidate) => pathsEqual(candidate, filePath))) {
|
|
1206
|
+
const observedReadGuardCorrelationId = getReadGuardCorrelationId(event);
|
|
1207
|
+
// #2464 review round 3 (F1): the SAME two pre-conditions the
|
|
1208
|
+
// classified site consults, through the same shared claim. Round 2
|
|
1209
|
+
// dispatched here with neither, so two concurrent observed
|
|
1210
|
+
// `tool_result`s for one file+hash both registered and both ran
|
|
1211
|
+
// `runPipeline` — two autofix/format writers racing on one file — the
|
|
1212
|
+
// second overwrote the first's registry entry, and the first's
|
|
1213
|
+
// release then evicted the outer entry a live, unrelated classified
|
|
1214
|
+
// pipeline had since re-created under it.
|
|
1215
|
+
const observedClaim = claimPipelineDispatch({
|
|
1216
|
+
filePath,
|
|
1217
|
+
stateHash: observedStateHash,
|
|
1218
|
+
turnIndex: runtime.turnIndex,
|
|
1219
|
+
participantId: observedReadGuardCorrelationId,
|
|
1220
|
+
dbg,
|
|
1221
|
+
});
|
|
1222
|
+
if (!observedClaim.proceed) {
|
|
1223
|
+
if (observedClaim.joined) {
|
|
1224
|
+
await observedClaim.joined;
|
|
1225
|
+
}
|
|
1226
|
+
// Same terminal value as the block's own exit below: the bridge
|
|
1227
|
+
// already recorded this edit, and the analysis it would have asked
|
|
1228
|
+
// for is either running or already done.
|
|
1229
|
+
return syntheticWriteContent.length > 0
|
|
1230
|
+
? { content: [...event.content, ...syntheticWriteContent] }
|
|
1231
|
+
: undefined;
|
|
1232
|
+
}
|
|
1233
|
+
const observedDispatchOutcome = await dispatchPipelineAnalysis({
|
|
1234
|
+
deps,
|
|
1235
|
+
runtime,
|
|
1236
|
+
filePath,
|
|
1237
|
+
dispatchCwd: resolveLanguageRootForFile(filePath, workspaceRoot),
|
|
1238
|
+
turnStateCwd: path.resolve(workspaceRoot),
|
|
1239
|
+
autofixMode: observedAutofixMode,
|
|
1240
|
+
// #2423: no adapter/diff ranges exist for a "learned" (unnamed)
|
|
1241
|
+
// tool — the classified chain below hits the same gap for this
|
|
1242
|
+
// provenance and also leaves `modifiedRanges` undefined, so the
|
|
1243
|
+
// dispatch runs unscoped (whole-file) exactly as it would there.
|
|
1244
|
+
modifiedRanges: undefined,
|
|
1245
|
+
writeIndex: runtime.nextWriteIndex(),
|
|
1246
|
+
initialStateHash: observedStateHash,
|
|
1247
|
+
readGuardCorrelationId: observedReadGuardCorrelationId,
|
|
1248
|
+
requestedEditIndexes: getRequestedEditIndexes(event, observedKind),
|
|
1249
|
+
requestedEditTotal: getRequestedEditCount(event, observedKind),
|
|
1250
|
+
isPartialApplyResult: (event.details ?? {})
|
|
1251
|
+
.piLensPartialApply === true,
|
|
1252
|
+
participantIds: [observedReadGuardCorrelationId],
|
|
1253
|
+
participantTotal: 1,
|
|
1254
|
+
toolResultStart,
|
|
1255
|
+
nativeAppliedPairs: observedAppliedPairs,
|
|
1256
|
+
});
|
|
1257
|
+
if (observedDispatchOutcome.crashed) {
|
|
1258
|
+
// #2464 review round 2, S6: parity with the classified chain — a
|
|
1259
|
+
// pipeline crash surfaces its notice to the agent instead of being
|
|
1260
|
+
// swallowed into a dbg line the model never sees. The recorded
|
|
1261
|
+
// edit stands either way; only the analysis was lost.
|
|
1262
|
+
dbg(`tool_result: pipeline analysis crashed for the observed mutation on ${filePath}; the recorded edit stands, analysis did not run this turn`);
|
|
1263
|
+
return observedDispatchOutcome.response;
|
|
1264
|
+
}
|
|
1265
|
+
dbg(`tool_result: the observational settle already recorded ${observedReplayed} mutation(s) for "${event.toolName}"; kept the applied-edit records, mutation receipt, cachedExports refresh, and ran the pipeline dispatch (#2464); skipped the bridge-covered staleness stamp / turn-state ranges / change-log receipt / deferred autofix+format`);
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
dbg(`tool_result: the observational settle recorded ${observedReplayed} mutation(s) for "${event.toolName}", none of them ${filePath} — nothing to analyse under the named path`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return syntheticWriteContent.length > 0
|
|
1272
|
+
? { content: [...event.content, ...syntheticWriteContent] }
|
|
1273
|
+
: undefined;
|
|
1274
|
+
}
|
|
1275
|
+
if (mutation === undefined) {
|
|
1276
|
+
dbg(`tool_result: skipped turn tracking - toolName="${event.toolName}" is not a classified mutation`);
|
|
530
1277
|
return syntheticWriteContent.length > 0
|
|
531
1278
|
? { content: [...event.content, ...syntheticWriteContent] }
|
|
532
1279
|
: undefined;
|
|
@@ -539,11 +1286,15 @@ export async function handleToolResult(deps) {
|
|
|
539
1286
|
dbg(`tool_result: skipped pipeline - file outside project root or in node_modules: ${filePath}`);
|
|
540
1287
|
return;
|
|
541
1288
|
}
|
|
1289
|
+
// #2430: a classified mutation is accounted for by the chain below, so the
|
|
1290
|
+
// `agent_settled` sweep must re-baseline this file rather than report the
|
|
1291
|
+
// same bytes as unexplained drift.
|
|
1292
|
+
noteMutationHandled(filePath);
|
|
542
1293
|
const readGuardCorrelationId = getReadGuardCorrelationId(event);
|
|
543
1294
|
const resultDetails = (event.details ?? {});
|
|
544
1295
|
const isPartialApplyResult = resultDetails.piLensPartialApply === true;
|
|
545
|
-
const requestedEditIndexes = getRequestedEditIndexes(event);
|
|
546
|
-
const requestedEditTotal = getRequestedEditCount(event);
|
|
1296
|
+
const requestedEditIndexes = getRequestedEditIndexes(event, mutation.kind);
|
|
1297
|
+
const requestedEditTotal = getRequestedEditCount(event, mutation.kind);
|
|
547
1298
|
const participantIds = [
|
|
548
1299
|
...(deps._telemetryParticipantIds ?? []),
|
|
549
1300
|
readGuardCorrelationId,
|
|
@@ -576,16 +1327,37 @@ export async function handleToolResult(deps) {
|
|
|
576
1327
|
});
|
|
577
1328
|
return { content: event.content, isError: true };
|
|
578
1329
|
}
|
|
1330
|
+
// One post-result raw-byte hash, reused for the applied-edit records below and
|
|
1331
|
+
// for the pipeline dedup key (`initialStateHash`) further down (finding 3):
|
|
1332
|
+
// nothing between here and that point rewrites the file, so a second read
|
|
1333
|
+
// would only re-derive the same digest.
|
|
1334
|
+
//
|
|
1335
|
+
// `preSettleStateHash` is that same digest taken BEFORE the observational
|
|
1336
|
+
// settle's yield, on the only path that has one (#2449 round 3, T4). Reusing
|
|
1337
|
+
// it is not an optimization: re-reading here would read whatever a racing
|
|
1338
|
+
// tool_result wrote while this call was awaiting.
|
|
1339
|
+
const postWriteStateHash = preSettleStateHash ?? getFileStateHash(filePath);
|
|
1340
|
+
// #2402: a fully-applied native edit is also an applied record, so an
|
|
1341
|
+
// identical retry is recognized as already-applied instead of escalating
|
|
1342
|
+
// through the oldText-not-found ladder. `event.input` carries the EXECUTED
|
|
1343
|
+
// args (post-autopatch), the same text the preflight resolves against.
|
|
1344
|
+
const nativeAppliedPairs = recordNativeAppliedPairs({
|
|
1345
|
+
runtime,
|
|
1346
|
+
input: event.input,
|
|
1347
|
+
kind: mutation.kind,
|
|
1348
|
+
filePath,
|
|
1349
|
+
stateHash: postWriteStateHash,
|
|
1350
|
+
});
|
|
579
1351
|
// Must happen before debounce admission: latestDeps intentionally retains only
|
|
580
1352
|
// the latest event, but write -> edit is a sticky turn transition.
|
|
581
1353
|
const receipt = runtime
|
|
582
1354
|
.recordMutationToolReceipt;
|
|
583
1355
|
const autofixMode = deps._bypassDebounce
|
|
584
1356
|
? (deps._autofixMode ??
|
|
585
|
-
(
|
|
1357
|
+
(mutation.kind === "edit" ? "deferred" : "immediate"))
|
|
586
1358
|
: receipt
|
|
587
|
-
? receipt.call(runtime, filePath,
|
|
588
|
-
:
|
|
1359
|
+
? receipt.call(runtime, filePath, mutation.kind).autofixMode
|
|
1360
|
+
: mutation.kind === "edit"
|
|
589
1361
|
? "deferred"
|
|
590
1362
|
: "immediate";
|
|
591
1363
|
// Coalesce sequential edits to the same file into one pipeline run against
|
|
@@ -607,47 +1379,29 @@ export async function handleToolResult(deps) {
|
|
|
607
1379
|
deps.readGuard?.recordWritten(filePath);
|
|
608
1380
|
// Keep cachedExports in sync after each write/edit so the pre-write STOP
|
|
609
1381
|
// check doesn't fire on names that were removed from this file this session.
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
// tool_result per edit hunk). Do not dedupe by file alone: a distinct later
|
|
633
|
-
// same-turn edit to this file must still be analyzed.
|
|
634
|
-
const inFlight = inFlightPipelines.get(filePath)?.get(initialStateHash);
|
|
635
|
-
if (inFlight) {
|
|
636
|
-
dbg(`tool_result: skipping duplicate concurrent state for ${filePath}`);
|
|
637
|
-
const duplicateId = readGuardCorrelationId;
|
|
638
|
-
if (inFlight.participantIds.length < 100) {
|
|
639
|
-
inFlight.participantIds.push(duplicateId);
|
|
1382
|
+
refreshCachedExports(runtime, filePath);
|
|
1383
|
+
// Reuse the hash taken right after the write landed (finding 3): the file is
|
|
1384
|
+
// unchanged between that read and here, so this is byte-identical.
|
|
1385
|
+
const initialStateHash = postWriteStateHash;
|
|
1386
|
+
// #2464 review round 3 (F1): the in-flight dedup and the already-analysed
|
|
1387
|
+
// latch moved into `claimPipelineDispatch`, shared verbatim with the observed
|
|
1388
|
+
// call site, which shipped round 2 with NEITHER check. The position is
|
|
1389
|
+
// unchanged — still ABOVE `addModifiedRange`/`recordProjectChange`, so a
|
|
1390
|
+
// duplicate state still writes no turn-state range and no change-log
|
|
1391
|
+
// receipt. Nothing may await between this claim and the dispatch below: the
|
|
1392
|
+
// claim is only atomic because `dispatchPipelineAnalysis` registers before
|
|
1393
|
+
// its own first await.
|
|
1394
|
+
const classifiedClaim = claimPipelineDispatch({
|
|
1395
|
+
filePath,
|
|
1396
|
+
stateHash: initialStateHash,
|
|
1397
|
+
turnIndex: runtime.turnIndex,
|
|
1398
|
+
participantId: readGuardCorrelationId,
|
|
1399
|
+
dbg,
|
|
1400
|
+
});
|
|
1401
|
+
if (!classifiedClaim.proceed) {
|
|
1402
|
+
if (classifiedClaim.joined) {
|
|
1403
|
+
await classifiedClaim.joined;
|
|
640
1404
|
}
|
|
641
|
-
inFlight.participantTotal += 1;
|
|
642
|
-
await inFlight.promise;
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
// Deduplicate sequential duplicate events for the same post-write state in the
|
|
646
|
-
// same turn while allowing later same-file edits whose content changed.
|
|
647
|
-
const lastAnalyzed = lastAnalyzedStateByFile.get(filePath);
|
|
648
|
-
if (lastAnalyzed?.turnIndex === runtime.turnIndex &&
|
|
649
|
-
lastAnalyzed.stateHash === initialStateHash) {
|
|
650
|
-
dbg(`tool_result: skipping already-analyzed file state this turn for ${filePath}`);
|
|
651
1405
|
return;
|
|
652
1406
|
}
|
|
653
1407
|
const sessionFileTime = createFileTime("default");
|
|
@@ -658,7 +1412,9 @@ export async function handleToolResult(deps) {
|
|
|
658
1412
|
const readGuard = runtime.readGuard;
|
|
659
1413
|
readGuard?.recordWritten?.(filePath);
|
|
660
1414
|
}
|
|
661
|
-
|
|
1415
|
+
// `toolResultStart` is hoisted above both provenance exits (#2464 review
|
|
1416
|
+
// round 2, S5) so the two paths' durations are comparable — see its
|
|
1417
|
+
// declaration right after the observational settle.
|
|
662
1418
|
dbg(`tool_result: tracking turn state for ${event.toolName} on ${filePath}`);
|
|
663
1419
|
if (isPathIgnoredByProject(filePath, workspaceRoot, false)) {
|
|
664
1420
|
dbg(`tool_result: skipping gitignored file ${filePath}`);
|
|
@@ -678,6 +1434,14 @@ export async function handleToolResult(deps) {
|
|
|
678
1434
|
// see the `telemetry:` block handed to `runPipeline` below.
|
|
679
1435
|
const writeIndex = runtime.nextWriteIndex();
|
|
680
1436
|
let modifiedRanges;
|
|
1437
|
+
// #2423: ranges a shape adapter resolved from the tool's own input. Only a
|
|
1438
|
+
// classified non-native edit shape sets these — a plain host `edit` carries
|
|
1439
|
+
// its ranges in `details.diff` and is handled by the branch below.
|
|
1440
|
+
const adapterRanges = mutation.editRanges && mutation.editRanges.length > 0
|
|
1441
|
+
? mutation.editRanges.map(([start, end]) => ({ start, end }))
|
|
1442
|
+
: mutation.touchedLines
|
|
1443
|
+
? [{ start: mutation.touchedLines[0], end: mutation.touchedLines[1] }]
|
|
1444
|
+
: undefined;
|
|
681
1445
|
try {
|
|
682
1446
|
// #1334 S6: the host DECLARES this payload (`EditToolDetails`, a
|
|
683
1447
|
// type-only export), so use it instead of re-declaring `{ diff?: string }`
|
|
@@ -688,7 +1452,7 @@ export async function handleToolResult(deps) {
|
|
|
688
1452
|
// has always relied on.
|
|
689
1453
|
const details = event.details;
|
|
690
1454
|
dbg(`tool_result: details.diff=${details?.diff ? "present" : "missing"}, details keys: ${Object.keys(event.details || {}).join(", ")}`);
|
|
691
|
-
if (
|
|
1455
|
+
if (mutation.kind === "edit" && details?.diff) {
|
|
692
1456
|
const diff = details.diff;
|
|
693
1457
|
dbg(`tool_result: diff content (first 500 chars): ${diff.substring(0, 500)}`);
|
|
694
1458
|
const ranges = parseDiffRanges(diff);
|
|
@@ -701,7 +1465,37 @@ export async function handleToolResult(deps) {
|
|
|
701
1465
|
}
|
|
702
1466
|
dbg(`tool_result: turn state after add: ${JSON.stringify(cacheManager.readTurnState(turnStateCwd))}`);
|
|
703
1467
|
}
|
|
704
|
-
else if (
|
|
1468
|
+
else if (mutation.kind === "edit" &&
|
|
1469
|
+
adapterRanges &&
|
|
1470
|
+
nodeFs.existsSync(filePath)) {
|
|
1471
|
+
// #2423: a third-party edit tool ships no host `details.diff`, so the
|
|
1472
|
+
// diff branch above never fires and the file used to leave `files: {}`
|
|
1473
|
+
// empty in turn-state.json. The shape adapter already resolved which
|
|
1474
|
+
// lines the call touched — use those.
|
|
1475
|
+
const content = nodeFs.readFileSync(filePath, "utf-8");
|
|
1476
|
+
const importsChanged = /^import\s/m.test(content);
|
|
1477
|
+
modifiedRanges = adapterRanges;
|
|
1478
|
+
for (const range of adapterRanges) {
|
|
1479
|
+
cacheManager.addModifiedRange(filePath, range, importsChanged, turnStateCwd, runtime.telemetrySessionId);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
else if (mutation.kind === "edit" &&
|
|
1483
|
+
mutation.provenance === "declared" &&
|
|
1484
|
+
nodeFs.existsSync(filePath)) {
|
|
1485
|
+
// #2423 review round 1 (F1): a third-party edit whose anchors did not
|
|
1486
|
+
// resolve — a stale anchor, or the read-guard preflight never ran to
|
|
1487
|
+
// resolve them — still CHANGED the file. Recording the whole file is a
|
|
1488
|
+
// deliberate superset: turn state exists to say "this file changed,
|
|
1489
|
+
// format and attribute it", and an empty `files` map is the exact
|
|
1490
|
+
// symptom the issue reports. A host `edit` keeps its old behavior,
|
|
1491
|
+
// because its ranges come from `details.diff` above.
|
|
1492
|
+
const content = nodeFs.readFileSync(filePath, "utf-8");
|
|
1493
|
+
const lineCount = content.split("\n").length;
|
|
1494
|
+
const importsChanged = /^import\s/m.test(content);
|
|
1495
|
+
modifiedRanges = [{ start: 1, end: lineCount }];
|
|
1496
|
+
cacheManager.addModifiedRange(filePath, { start: 1, end: lineCount }, importsChanged, turnStateCwd, runtime.telemetrySessionId);
|
|
1497
|
+
}
|
|
1498
|
+
else if (mutation.kind === "write" && nodeFs.existsSync(filePath)) {
|
|
705
1499
|
const content = nodeFs.readFileSync(filePath, "utf-8");
|
|
706
1500
|
const lineCount = content.split("\n").length;
|
|
707
1501
|
const hasImports = /^import\s/m.test(content);
|
|
@@ -718,7 +1512,7 @@ export async function handleToolResult(deps) {
|
|
|
718
1512
|
cwd: turnStateCwd,
|
|
719
1513
|
filePath,
|
|
720
1514
|
source: deps._mutationSourceOverride ??
|
|
721
|
-
|
|
1515
|
+
sourceForMutation(mutation, event.details),
|
|
722
1516
|
changedRange: singleRange(modifiedRanges),
|
|
723
1517
|
dbg,
|
|
724
1518
|
});
|
|
@@ -731,187 +1525,39 @@ export async function handleToolResult(deps) {
|
|
|
731
1525
|
durationMs: turnStateMs,
|
|
732
1526
|
});
|
|
733
1527
|
dbg(`tool_result fired for: ${filePath} (turn_state: ${turnStateMs}ms)`);
|
|
734
|
-
|
|
735
|
-
|
|
1528
|
+
// #2464: the pipeline dispatch itself now lives in `dispatchPipelineAnalysis`
|
|
1529
|
+
// (defined above `handleToolResult`), shared with the observed-mutation
|
|
1530
|
+
// early return. This call site is otherwise unchanged — same arguments, same
|
|
1531
|
+
// crash-then-return / success-then-continue shape as before the split.
|
|
1532
|
+
const dispatchOutcome = await dispatchPipelineAnalysis({
|
|
1533
|
+
deps,
|
|
1534
|
+
runtime,
|
|
736
1535
|
filePath,
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
toolName: event.toolName,
|
|
1536
|
+
dispatchCwd,
|
|
1537
|
+
turnStateCwd,
|
|
740
1538
|
autofixMode,
|
|
741
1539
|
modifiedRanges,
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
},
|
|
750
|
-
getFlag,
|
|
751
|
-
getFlagSource,
|
|
752
|
-
dbg,
|
|
753
|
-
// #451: hand the deferred cascade live sequence accessors so the
|
|
754
|
-
// review-graph builder can skip its per-build O(project) sweep when
|
|
755
|
-
// only pi-observed edits happened. projectSeq is a function because the
|
|
756
|
-
// cascade runs after this returns (#450) — read current, not captured.
|
|
757
|
-
seqState: {
|
|
758
|
-
projectSeq: () => runtime.projectSeq,
|
|
759
|
-
getFilesChangedSince: (seq) => runtime.getFilesChangedSince(seq),
|
|
760
|
-
},
|
|
761
|
-
// The settle clock is live because the deferred cascade may reach its
|
|
762
|
-
// budget derivation before or after turn_end starts waiting.
|
|
763
|
-
turnEndCascadeSettleStart: () => runtime.getTurnEndCascadeSettleStart(),
|
|
764
|
-
// #348 phase 2: live reference so the deferred cascade can update the
|
|
765
|
-
// warm word index in place at the same seam as the graph rebuild.
|
|
766
|
-
// `runtime.wordIndex` is read fresh (not captured) via this closure-free
|
|
767
|
-
// property access being re-evaluated at object-literal construction
|
|
768
|
-
// time here — that's fine because runPipeline reads `ctx.wordIndex`
|
|
769
|
-
// synchronously into computeCascadeForFile's options before returning
|
|
770
|
-
// (the deferred part is the cascade's OWN execution, not this handoff).
|
|
771
|
-
wordIndex: runtime.wordIndex,
|
|
772
|
-
onWordIndexUpdated: (index) => {
|
|
773
|
-
scheduleWordIndexPersist(dispatchCwd, index, dbg);
|
|
774
|
-
},
|
|
775
|
-
}, {
|
|
776
|
-
biomeClient,
|
|
777
|
-
ruffClient,
|
|
778
|
-
metricsClient,
|
|
779
|
-
getFormatService,
|
|
780
|
-
fixedThisTurn: runtime.fixedThisTurn,
|
|
781
|
-
});
|
|
782
|
-
const pipelineTelemetry = {
|
|
783
|
-
promise: pipelinePromise,
|
|
784
|
-
participantIds: [...new Set(participantIds)].slice(0, 100),
|
|
1540
|
+
writeIndex,
|
|
1541
|
+
initialStateHash,
|
|
1542
|
+
readGuardCorrelationId,
|
|
1543
|
+
requestedEditIndexes,
|
|
1544
|
+
requestedEditTotal,
|
|
1545
|
+
isPartialApplyResult,
|
|
1546
|
+
participantIds,
|
|
785
1547
|
participantTotal,
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if (!filePipelines) {
|
|
789
|
-
filePipelines = new Map();
|
|
790
|
-
inFlightPipelines.set(filePath, filePipelines);
|
|
791
|
-
}
|
|
792
|
-
filePipelines.set(initialStateHash, pipelineTelemetry);
|
|
793
|
-
try {
|
|
794
|
-
result = await pipelinePromise;
|
|
795
|
-
}
|
|
796
|
-
catch (pipelineErr) {
|
|
797
|
-
if (getFlag("lens-guard")) {
|
|
798
|
-
runtime.markGitGuardCacheUnknown("pipeline_crash");
|
|
799
|
-
}
|
|
800
|
-
dbg(`runPipeline crashed: ${pipelineErr}`);
|
|
801
|
-
logReadGuardEvent({
|
|
802
|
-
event: "edit_post_edit_pipeline_failed",
|
|
803
|
-
correlationId: readGuardCorrelationId,
|
|
804
|
-
filePath,
|
|
805
|
-
metadata: {
|
|
806
|
-
tool: event.toolName,
|
|
807
|
-
commitStatus: "committed",
|
|
808
|
-
reasonCode: "pipeline_failed",
|
|
809
|
-
},
|
|
810
|
-
});
|
|
811
|
-
logReadGuardEvent({
|
|
812
|
-
event: "edit_batch_summary",
|
|
813
|
-
correlationId: readGuardCorrelationId,
|
|
814
|
-
filePath,
|
|
815
|
-
metadata: {
|
|
816
|
-
tool: event.toolName,
|
|
817
|
-
editBatchSummary: createReadGuardEditBatchSummary({
|
|
818
|
-
requestedIndexes: requestedEditIndexes,
|
|
819
|
-
requestedTotal: requestedEditTotal,
|
|
820
|
-
resolvedIndexes: requestedEditIndexes,
|
|
821
|
-
resolvedTotal: requestedEditTotal,
|
|
822
|
-
appliedIndexes: requestedEditIndexes,
|
|
823
|
-
appliedTotal: requestedEditTotal,
|
|
824
|
-
participantIds: pipelineTelemetry.participantIds,
|
|
825
|
-
participantTotal: pipelineTelemetry.participantTotal,
|
|
826
|
-
commitStatus: "committed",
|
|
827
|
-
postEditStatus: "failed",
|
|
828
|
-
terminalStatus: "failed",
|
|
829
|
-
durationMs: Date.now() - toolResultStart,
|
|
830
|
-
}),
|
|
831
|
-
},
|
|
832
|
-
});
|
|
833
|
-
dbg(`runPipeline crash stack: ${pipelineErr.stack}`);
|
|
834
|
-
if (!getFlag("no-lsp")) {
|
|
835
|
-
resetLSPService({ fast: true, reason: "pipeline_crash" });
|
|
836
|
-
}
|
|
837
|
-
logLatency({
|
|
838
|
-
type: "tool_result",
|
|
839
|
-
toolName: event.toolName,
|
|
840
|
-
filePath,
|
|
841
|
-
durationMs: Date.now() - toolResultStart,
|
|
842
|
-
result: "pipeline_crash",
|
|
843
|
-
});
|
|
844
|
-
const notice = runtime.formatPipelineCrashNotice(filePath, pipelineErr);
|
|
845
|
-
return {
|
|
846
|
-
content: notice
|
|
847
|
-
? [...event.content, { type: "text", text: notice }]
|
|
848
|
-
: event.content,
|
|
849
|
-
isError: true,
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
finally {
|
|
853
|
-
// Prune the per-file inner map once it's empty so a file touched once
|
|
854
|
-
// this session doesn't leave a permanent empty entry in the outer map.
|
|
855
|
-
filePipelines.delete(initialStateHash);
|
|
856
|
-
if (filePipelines.size === 0) {
|
|
857
|
-
inFlightPipelines.delete(filePath);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
if (!isPartialApplyResult) {
|
|
861
|
-
const postEditStatus = result.isError ? "failed" : "succeeded";
|
|
862
|
-
if (result.isError) {
|
|
863
|
-
logReadGuardEvent({
|
|
864
|
-
event: "edit_post_edit_pipeline_failed",
|
|
865
|
-
correlationId: readGuardCorrelationId,
|
|
866
|
-
filePath,
|
|
867
|
-
metadata: {
|
|
868
|
-
tool: event.toolName,
|
|
869
|
-
commitStatus: "committed",
|
|
870
|
-
reasonCode: "pipeline_failed",
|
|
871
|
-
},
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
logReadGuardEvent({
|
|
875
|
-
event: "edit_batch_summary",
|
|
876
|
-
correlationId: readGuardCorrelationId,
|
|
877
|
-
filePath,
|
|
878
|
-
metadata: {
|
|
879
|
-
tool: event.toolName,
|
|
880
|
-
editBatchSummary: createReadGuardEditBatchSummary({
|
|
881
|
-
requestedIndexes: requestedEditIndexes,
|
|
882
|
-
requestedTotal: requestedEditTotal,
|
|
883
|
-
resolvedIndexes: requestedEditIndexes,
|
|
884
|
-
resolvedTotal: requestedEditTotal,
|
|
885
|
-
appliedIndexes: requestedEditIndexes,
|
|
886
|
-
appliedTotal: requestedEditTotal,
|
|
887
|
-
participantIds: pipelineTelemetry.participantIds,
|
|
888
|
-
participantTotal: pipelineTelemetry.participantTotal,
|
|
889
|
-
commitStatus: "committed",
|
|
890
|
-
postEditStatus,
|
|
891
|
-
terminalStatus: postEditStatus === "failed" ? "failed" : "success",
|
|
892
|
-
durationMs: Date.now() - toolResultStart,
|
|
893
|
-
}),
|
|
894
|
-
},
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
|
-
lastAnalyzedStateByFile.set(filePath, {
|
|
898
|
-
turnIndex: runtime.turnIndex,
|
|
899
|
-
stateHash: getFileStateHash(filePath),
|
|
1548
|
+
toolResultStart,
|
|
1549
|
+
nativeAppliedPairs,
|
|
900
1550
|
});
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
// is judged by read-range coverage, not by our own previous write.
|
|
904
|
-
if (!getFlag("no-read-guard")) {
|
|
905
|
-
const changedForReadGuard = new Set([
|
|
906
|
-
path.resolve(filePath),
|
|
907
|
-
...(result.changedFiles ?? []).map((changedFile) => path.resolve(changedFile)),
|
|
908
|
-
]);
|
|
909
|
-
for (const changedFile of changedForReadGuard) {
|
|
910
|
-
if (nodeFs.existsSync(changedFile)) {
|
|
911
|
-
deps.readGuard?.recordWritten(changedFile);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
1551
|
+
if (dispatchOutcome.crashed) {
|
|
1552
|
+
return dispatchOutcome.response;
|
|
914
1553
|
}
|
|
1554
|
+
// #2464 review round 2 (S1): the three post-pipeline post-conditions that
|
|
1555
|
+
// used to be written out here — the already-analysed latch, the #2402
|
|
1556
|
+
// after-write hash stamp, and the read-guard staleness re-stamp over
|
|
1557
|
+
// `result.changedFiles` — now live INSIDE `dispatchPipelineAnalysis`, so the
|
|
1558
|
+
// observed path gets them by construction rather than by a second author
|
|
1559
|
+
// remembering to copy them.
|
|
1560
|
+
const result = dispatchOutcome.result;
|
|
915
1561
|
let autofixNewlyQueued = false;
|
|
916
1562
|
if (!result.isError &&
|
|
917
1563
|
autofixMode === "deferred" &&
|
|
@@ -942,7 +1588,12 @@ export async function handleToolResult(deps) {
|
|
|
942
1588
|
publishFormatQueued({
|
|
943
1589
|
filePath,
|
|
944
1590
|
cwd: dispatchCwd,
|
|
945
|
-
|
|
1591
|
+
// The v1 `pilens:format:queued` payload declares
|
|
1592
|
+
// `tool: "write" | "edit"`, so a third-party tool publishes under
|
|
1593
|
+
// its KIND rather than its name. Widening the published field is a
|
|
1594
|
+
// public-API decision tracked in #2421; until then the kind is the
|
|
1595
|
+
// honest projection.
|
|
1596
|
+
tool: mutation.kind,
|
|
946
1597
|
dbg,
|
|
947
1598
|
kinds: autofixMode === "deferred" ? ["autofix", "format"] : ["format"],
|
|
948
1599
|
});
|
|
@@ -952,13 +1603,15 @@ export async function handleToolResult(deps) {
|
|
|
952
1603
|
publishFormatQueued({
|
|
953
1604
|
filePath,
|
|
954
1605
|
cwd: dispatchCwd,
|
|
955
|
-
|
|
1606
|
+
// Same #2421 projection as the queued-with-format publish above.
|
|
1607
|
+
tool: mutation.kind,
|
|
956
1608
|
kinds: ["autofix"],
|
|
957
1609
|
dbg,
|
|
958
1610
|
});
|
|
959
1611
|
}
|
|
960
1612
|
for (const changedFile of result.changedFiles ?? []) {
|
|
961
1613
|
const resolvedChanged = path.resolve(changedFile);
|
|
1614
|
+
invalidateFormatterCacheForPath(resolvedChanged);
|
|
962
1615
|
if (!nodeFs.existsSync(resolvedChanged))
|
|
963
1616
|
continue;
|
|
964
1617
|
recordProjectChange({
|