pi-lens 4.1.5 → 4.2.0
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 +415 -0
- package/README.md +13 -8
- package/config/dependency-cruiser-eager-allowlist.json +6 -1
- package/dist/clients/actionable-warnings.js +30 -4
- package/dist/clients/agent-behavior-client.js +2 -0
- package/dist/clients/analysed-root.js +1 -0
- package/dist/clients/ast-grep-tool-logger.js +1 -1
- package/dist/clients/bash-file-access.js +26 -0
- package/dist/clients/biome-client.js +13 -2
- package/dist/clients/blocker-freshness.js +403 -35
- package/dist/clients/bootstrap.js +6 -1
- package/dist/clients/bounded-telemetry.js +2 -0
- package/dist/clients/cache-observability.js +122 -1
- package/dist/clients/cascade-format.js +200 -4
- package/dist/clients/code-quality-warnings.js +10 -2
- package/dist/clients/complexity-client.js +5 -0
- package/dist/clients/config-diagnostic-codes.js +2 -0
- package/dist/clients/config-resolve.js +6 -3
- package/dist/clients/config-schema.js +17 -0
- package/dist/clients/config-warn.js +2 -2
- package/dist/clients/dead-code-client.js +8 -1
- package/dist/clients/degradation-ledger.js +16 -1
- package/dist/clients/dependency-checker.js +5 -1
- package/dist/clients/diagnostic-dispositions.js +12 -1
- package/dist/clients/dispatch/ast-grep-catalog.js +65 -0
- package/dist/clients/dispatch/auxiliary-lsp.js +28 -0
- package/dist/clients/dispatch/dispatcher.js +47 -17
- package/dist/clients/dispatch/facts/function-facts.js +1 -1
- package/dist/clients/dispatch/facts/import-facts.js +5 -1
- package/dist/clients/dispatch/finding-policy.js +222 -0
- package/dist/clients/dispatch/indent-detect.js +389 -13
- package/dist/clients/dispatch/integration.js +175 -25
- package/dist/clients/dispatch/rule-ignores.js +62 -0
- package/dist/clients/dispatch/runners/actionlint.js +2 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
- package/dist/clients/dispatch/runners/biome-check.js +8 -4
- package/dist/clients/dispatch/runners/cpp-check.js +4 -3
- package/dist/clients/dispatch/runners/credo.js +2 -1
- package/dist/clients/dispatch/runners/cue-vet.js +2 -1
- package/dist/clients/dispatch/runners/dart-analyze.js +2 -1
- package/dist/clients/dispatch/runners/detekt.js +2 -1
- package/dist/clients/dispatch/runners/dotnet-build.js +2 -1
- package/dist/clients/dispatch/runners/elixir-check.js +2 -1
- package/dist/clients/dispatch/runners/eslint.js +2 -1
- package/dist/clients/dispatch/runners/fish-indent.js +2 -1
- package/dist/clients/dispatch/runners/gleam-check.js +2 -1
- package/dist/clients/dispatch/runners/go-vet.js +2 -1
- package/dist/clients/dispatch/runners/golangci-lint.js +2 -1
- package/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/dist/clients/dispatch/runners/helm-lint.js +3 -1
- package/dist/clients/dispatch/runners/helm-render.js +2 -1
- package/dist/clients/dispatch/runners/htmlhint.js +2 -1
- package/dist/clients/dispatch/runners/javac.js +2 -1
- package/dist/clients/dispatch/runners/ktlint.js +2 -1
- package/dist/clients/dispatch/runners/lsp.js +19 -1
- package/dist/clients/dispatch/runners/markdownlint.js +3 -2
- package/dist/clients/dispatch/runners/mypy.js +2 -1
- package/dist/clients/dispatch/runners/oxlint.js +2 -1
- package/dist/clients/dispatch/runners/php-lint.js +2 -1
- package/dist/clients/dispatch/runners/phpstan.js +2 -1
- package/dist/clients/dispatch/runners/prisma-validate.js +2 -1
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +12 -12
- package/dist/clients/dispatch/runners/pyright.js +2 -1
- package/dist/clients/dispatch/runners/rubocop.js +2 -1
- package/dist/clients/dispatch/runners/ruff.js +2 -2
- package/dist/clients/dispatch/runners/rust-clippy.js +12 -11
- package/dist/clients/dispatch/runners/shellcheck.js +2 -1
- package/dist/clients/dispatch/runners/shfmt.js +4 -13
- package/dist/clients/dispatch/runners/spellcheck.js +2 -2
- package/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/dist/clients/dispatch/runners/sqlfluff.js +3 -2
- package/dist/clients/dispatch/runners/stylelint.js +3 -2
- package/dist/clients/dispatch/runners/swiftlint.js +2 -1
- package/dist/clients/dispatch/runners/taplo.js +2 -1
- package/dist/clients/dispatch/runners/terragrunt.js +2 -1
- package/dist/clients/dispatch/runners/tflint.js +2 -1
- package/dist/clients/dispatch/runners/tree-sitter.js +2 -21
- package/dist/clients/dispatch/runners/trivy-config.js +2 -1
- package/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
- package/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
- package/dist/clients/dispatch/runners/vale.js +2 -1
- package/dist/clients/dispatch/runners/yamllint.js +4 -3
- package/dist/clients/dispatch/runners/zig-check.js +2 -1
- package/dist/clients/dispatch/types.js +4 -1
- package/dist/clients/disposition-publish.js +1 -0
- package/dist/clients/effective-config.js +12 -2
- package/dist/clients/extension-log.js +2 -0
- package/dist/clients/file-kinds.js +26 -1
- package/dist/clients/file-role.js +23 -6
- package/dist/clients/file-utils.js +156 -10
- package/dist/clients/finding-delivery-gate.js +60 -9
- package/dist/clients/format-service.js +39 -21
- package/dist/clients/formatters.js +113 -200
- package/dist/clients/generation-guard.js +6 -2
- package/dist/clients/git-guard.js +11 -3
- package/dist/clients/gitleaks-client.js +42 -6
- package/dist/clients/govulncheck-client.js +48 -7
- package/dist/clients/indent-retarget.js +78 -8
- package/dist/clients/installer/index.js +524 -161
- package/dist/clients/instance-reaper-state.js +7 -0
- package/dist/clients/instance-reaper.js +20 -10
- package/dist/clients/jscpd-client.js +9 -1
- package/dist/clients/knip-client.js +9 -2
- package/dist/clients/language-profile.js +35 -3
- package/dist/clients/language-registry.js +5 -1
- package/dist/clients/latency-logger.js +6 -1
- package/dist/clients/lens-config.js +4 -0
- package/dist/clients/lens-engine.js +12 -3
- package/dist/clients/lens-events.js +2 -1
- package/dist/clients/lens-flag-registry.js +79 -0
- package/dist/clients/live-bus-emitter.js +2 -1
- package/dist/clients/lsp/client.js +25 -21
- package/dist/clients/lsp/config.js +36 -7
- package/dist/clients/lsp/diagnostic-binding.js +24 -1
- package/dist/clients/lsp/document-drift.js +78 -0
- package/dist/clients/lsp/index.js +288 -45
- package/dist/clients/lsp/launch.js +16 -4
- package/dist/clients/lsp/server.js +79 -15
- package/dist/clients/lsp/workspace-diagnostics-cache.js +39 -5
- package/dist/clients/lsp-mutation.js +5 -3
- package/dist/clients/map-with-concurrency.js +12 -8
- package/dist/clients/mcp/analyze.js +3 -0
- package/dist/clients/mcp/session.js +38 -4
- package/dist/clients/ndjson-logger.js +24 -7
- package/dist/clients/observed-mutation.js +40 -2
- package/dist/clients/opaque-mutation-scan.js +23 -3
- package/dist/clients/opengrep-client.js +111 -14
- package/dist/clients/package-manager.js +2 -1
- package/dist/clients/path-utils.js +123 -3
- package/dist/clients/performance-report.js +5 -1
- package/dist/clients/php-cs-fixer-config.js +8 -3
- package/dist/clients/pipeline.js +77 -7
- package/dist/clients/project-diagnostics/cache.js +37 -5
- package/dist/clients/project-diagnostics/fresh-fetch.js +173 -18
- package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
- package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
- package/dist/clients/project-diagnostics/scanner.js +59 -3
- package/dist/clients/project-lens-config.js +48 -2
- package/dist/clients/read-guard-logger.js +2 -0
- package/dist/clients/read-guard.js +70 -11
- package/dist/clients/resource-sampler.js +147 -9
- package/dist/clients/review-graph-logger.js +2 -0
- package/dist/clients/ruff-client.js +18 -9
- package/dist/clients/runtime-agent-end.js +42 -5
- package/dist/clients/runtime-context.js +6 -2
- package/dist/clients/runtime-coordinator.js +82 -7
- package/dist/clients/runtime-session.js +33 -5
- package/dist/clients/runtime-tool-call.js +43 -12
- package/dist/clients/runtime-tool-result.js +334 -86
- package/dist/clients/runtime-turn.js +195 -18
- package/dist/clients/safe-spawn.js +258 -5
- package/dist/clients/scratch-tree-policy.js +6 -0
- package/dist/clients/security-scan-client.js +2 -2
- package/dist/clients/session-event-guard.js +119 -6
- package/dist/clients/sg-runner.js +25 -12
- package/dist/clients/sgconfig.js +79 -2
- package/dist/clients/situational-tool-telemetry.js +108 -0
- package/dist/clients/test-runner-client.js +258 -78
- package/dist/clients/test-runner-delivery.js +43 -0
- package/dist/clients/tool-agreement.js +423 -0
- package/dist/clients/tool-config.js +249 -0
- package/dist/clients/tool-cwd.js +270 -0
- package/dist/clients/tool-policy.js +163 -7
- package/dist/clients/tool-probe.js +53 -0
- package/dist/clients/tool-set-policy.js +42 -0
- package/dist/clients/tree-sitter-client.js +120 -0
- package/dist/clients/tree-sitter-query-loader.js +127 -189
- package/dist/clients/trivy-client.js +9 -1
- package/dist/clients/turn-context.js +52 -0
- package/dist/clients/widget-state.js +102 -3
- package/dist/clients/word-index.js +5 -2
- package/dist/clients/write-ordering-guard.js +7 -0
- package/dist/index.js +26968 -21741
- package/dist/mcp/analyze-cli.js +3 -2
- package/dist/mcp/server.js +140 -161
- package/dist/tools/activate-tools.js +10 -7
- package/dist/tools/ast-grep-outline.js +2 -13
- package/dist/tools/ast-grep-replace.js +8 -13
- package/dist/tools/ast-grep-search.js +83 -39
- package/dist/tools/effective-config.js +4 -3
- package/dist/tools/lens-diagnostic-mark.js +8 -11
- package/dist/tools/lens-diagnostics.js +516 -133
- package/dist/tools/lsp-diagnostics.js +265 -182
- package/dist/tools/lsp-navigation.js +20 -44
- package/dist/tools/module-report.js +5 -9
- package/dist/tools/project-report.js +9 -17
- package/dist/tools/render-compact.js +280 -1
- package/dist/tools/symbol-search.js +2 -2
- package/docs/agent-guide.md +3 -4
- package/docs/agent-tools.md +46 -22
- package/docs/audit1.md +5 -5
- package/docs/configuration.md +9 -0
- package/docs/dependencies.md +11 -0
- package/docs/dispositions.md +52 -0
- package/docs/environment-variables.md +2 -7
- package/docs/features.md +7 -7
- package/docs/globalconfig.md +14 -2
- package/docs/language-coverage.md +1 -0
- package/docs/mcp.md +13 -1
- package/docs/pi-lens-fixer.md +48 -18
- package/docs/pi-lens-investigator.md +38 -18
- package/docs/pi-lens-monitor.md +88 -0
- package/docs/pi-lens-retro.md +97 -0
- package/docs/pi-lens-reviewer.md +53 -31
- package/docs/pi-lens-subagent.md +12 -4
- package/docs/public-api-stability.md +1 -0
- package/docs/real-harness.md +46 -0
- package/docs/release-qa-baseline.md +27 -1
- package/docs/servercapabilities.md +1 -6
- package/docs/settings.md +44 -6
- package/docs/tools_improvement2.md +4 -4
- package/docs/usage.md +15 -3
- package/package.json +10 -1
- package/rules/tree-sitter-queries/typescript/sql-injection.yml +23 -7
- package/scripts/analyze-pi-lens-logs.mjs +40 -0
- package/skills/pi-lens-ast-grep/SKILL.md +23 -6
- package/skills/pi-lens-lsp-navigation/SKILL.md +26 -9
- package/dist/tools/ast-dump.js +0 -103
|
@@ -2,8 +2,9 @@ import * as nodeCrypto from "node:crypto";
|
|
|
2
2
|
import * as nodeFs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { noteAuthoritativeContentAttachment } from "./agent-nudge.js";
|
|
5
|
-
import { captureFileStats,
|
|
5
|
+
import { captureFileStats, diffFileContent, getOpaqueBaselineStore, recoverOpaqueChangesViaGit, } from "./opaque-mutation-scan.js";
|
|
6
6
|
import { normalizeMapKey } from "./path-utils.js";
|
|
7
|
+
import { detectFileKind } from "./file-kinds.js";
|
|
7
8
|
import { extractReadPathsFromCommand, extractDeletedPathsFromCommand, extractGrepSearchReadsFromOutput, extractWrittenPathsFromCommand, tokenizeShellCommand, } from "./bash-file-access.js";
|
|
8
9
|
import { registerSearchReads, } from "./search-read-registration.js";
|
|
9
10
|
import { createFileTime } from "./file-time.js";
|
|
@@ -21,14 +22,22 @@ import { replayThroughMutationBridge, storedLineHashesFor, } from "./observed-mu
|
|
|
21
22
|
import { getAmbientAbortSignal } from "./safe-spawn.js";
|
|
22
23
|
import { resolveToolCallCorrelationId } from "./tool-event.js";
|
|
23
24
|
import { boundedIndexesForCount, createReadGuardEditBatchSummary, getReadGuardCorrelationId, logReadGuardEvent, } from "./read-guard-logger.js";
|
|
24
|
-
import {
|
|
25
|
+
import { countFileLines } from "./read-guard-tool-lines.js";
|
|
26
|
+
import { notifyExternalFileChange, resyncGitChangedFiles, } from "./lsp/index.js";
|
|
25
27
|
import { runPipeline } from "./pipeline.js";
|
|
26
28
|
import { renderPostAutofixNotice, } from "./post-autofix-notice.js";
|
|
27
29
|
import { syncGitGuardRecord } from "./git-guard.js";
|
|
28
30
|
import { scheduleWordIndexPersist } from "./word-index.js";
|
|
29
31
|
import { RUNTIME_CONFIG } from "./runtime-config.js";
|
|
30
32
|
import { getActiveSessionId } from "./session-lifecycle.js";
|
|
33
|
+
import { requestBootstrapClients } from "./bootstrap.js";
|
|
34
|
+
import { bounded } from "./deadline-utils.js";
|
|
35
|
+
import { HOOK_WALL_BUDGET_MS } from "./hook-budgets.js";
|
|
36
|
+
import { incrementDegradationCount, recordDegradationOnce, } from "./degradation-ledger.js";
|
|
31
37
|
const AUTHORITATIVE_CONTENT_MAX_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
|
|
38
|
+
// Keep same-turn analysis below the observed-directory capture bound so one
|
|
39
|
+
// opaque codemod cannot monopolize the hook or analyzer fleet.
|
|
40
|
+
const OBSERVED_DISPATCH_MAX_PATHS = 32;
|
|
32
41
|
/**
|
|
33
42
|
* Git subcommands that import ANOTHER commit's content into the index. The
|
|
34
43
|
* whole family, with a verdict for each (#2060):
|
|
@@ -93,6 +102,29 @@ export function isFailedGitIntegrationCommand(command, isError) {
|
|
|
93
102
|
return false;
|
|
94
103
|
});
|
|
95
104
|
}
|
|
105
|
+
function ensureToolResultClients(deps) {
|
|
106
|
+
if (deps.biomeClient && deps.ruffClient && deps.metricsClient)
|
|
107
|
+
return true;
|
|
108
|
+
const request = {
|
|
109
|
+
reason: "tool-result-analysis",
|
|
110
|
+
hook: "tool_result_edit",
|
|
111
|
+
timeoutMs: HOOK_WALL_BUDGET_MS.tool_result_edit,
|
|
112
|
+
...(deps.signal === undefined ? {} : { signal: deps.signal }),
|
|
113
|
+
};
|
|
114
|
+
return bounded(requestBootstrapClients(request), {
|
|
115
|
+
ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
|
|
116
|
+
signal: deps.signal,
|
|
117
|
+
hook: "tool_result_edit",
|
|
118
|
+
label: "tool-result-bootstrap-demand",
|
|
119
|
+
}).then((clients) => {
|
|
120
|
+
if (!clients)
|
|
121
|
+
return false;
|
|
122
|
+
deps.biomeClient = clients.biomeClient;
|
|
123
|
+
deps.ruffClient = clients.ruffClient;
|
|
124
|
+
deps.metricsClient = clients.metricsClient;
|
|
125
|
+
return true;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
96
128
|
function parseDiffRanges(diff) {
|
|
97
129
|
const changedLines = [];
|
|
98
130
|
for (const line of diff.split("\n")) {
|
|
@@ -144,13 +176,11 @@ export function clearLastAnalyzedStateCache() {
|
|
|
144
176
|
* with one owner (#2464 review round 3, F1) rather than two open-coded blocks
|
|
145
177
|
* ~80 lines apart whose invariants only line up if a reader checks both.
|
|
146
178
|
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
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.
|
|
179
|
+
* Kept private because both dispatch call sites now use the shared claim before
|
|
180
|
+
* registration; publishing this seam without the claim obligation would make
|
|
181
|
+
* the identity guard's precondition easy to violate again.
|
|
152
182
|
*/
|
|
153
|
-
|
|
183
|
+
function registerInFlightPipeline(filePath, stateHash, pipeline) {
|
|
154
184
|
let filePipelines = inFlightPipelines.get(filePath);
|
|
155
185
|
if (!filePipelines) {
|
|
156
186
|
filePipelines = new Map();
|
|
@@ -172,7 +202,7 @@ export function registerInFlightPipeline(filePath, stateHash, pipeline) {
|
|
|
172
202
|
* deduping and its `participantIds`/`participantTotal` under-count. Delete only
|
|
173
203
|
* when the outer map still points at the very map this registration went into.
|
|
174
204
|
*/
|
|
175
|
-
|
|
205
|
+
function releaseInFlightPipeline(filePath, stateHash, registered) {
|
|
176
206
|
registered.delete(stateHash);
|
|
177
207
|
if (registered.size === 0 && inFlightPipelines.get(filePath) === registered) {
|
|
178
208
|
inFlightPipelines.delete(filePath);
|
|
@@ -208,7 +238,7 @@ export function releaseInFlightPipeline(filePath, stateHash, registered) {
|
|
|
208
238
|
* already analysed this turn — the duplicate-recording inversion #2464 exists
|
|
209
239
|
* to remove.
|
|
210
240
|
*/
|
|
211
|
-
|
|
241
|
+
function claimPipelineDispatch(args) {
|
|
212
242
|
const { filePath, stateHash, turnIndex, participantId, dbg } = args;
|
|
213
243
|
// Deduplicate concurrent calls for the same final file state (pi can fire one
|
|
214
244
|
// tool_result per edit hunk). Do not dedupe by file alone: a distinct later
|
|
@@ -454,9 +484,10 @@ function refreshCachedExports(runtime, filePath) {
|
|
|
454
484
|
* pipeline AND record" block could not express.
|
|
455
485
|
*/
|
|
456
486
|
async function dispatchPipelineAnalysis(args) {
|
|
457
|
-
const { deps, runtime, filePath, dispatchCwd, turnStateCwd, autofixMode, modifiedRanges, writeIndex, initialStateHash, readGuardCorrelationId, requestedEditIndexes, requestedEditTotal, isPartialApplyResult, toolResultStart, nativeAppliedPairs, } = args;
|
|
487
|
+
const { deps, runtime, filePath, dispatchCwd, turnStateCwd, autofixMode, modifiedRanges, writeIndex, initialStateHash, readGuardCorrelationId, requestedEditIndexes, requestedEditTotal, isPartialApplyResult, toolResultStart, nativeAppliedPairs, allowReadGuardWrites, } = args;
|
|
458
488
|
const { event, getFlag, getFlagSource, dbg, biomeClient, ruffClient, metricsClient, resetLSPService, } = deps;
|
|
459
489
|
const pipelinePromise = runPipeline({
|
|
490
|
+
signal: deps.signal,
|
|
460
491
|
filePath,
|
|
461
492
|
cwd: dispatchCwd,
|
|
462
493
|
projectRoot: turnStateCwd,
|
|
@@ -497,9 +528,9 @@ async function dispatchPipelineAnalysis(args) {
|
|
|
497
528
|
scheduleWordIndexPersist(dispatchCwd, index, dbg);
|
|
498
529
|
},
|
|
499
530
|
}, {
|
|
500
|
-
biomeClient,
|
|
501
|
-
ruffClient,
|
|
502
|
-
metricsClient,
|
|
531
|
+
biomeClient: biomeClient,
|
|
532
|
+
ruffClient: ruffClient,
|
|
533
|
+
metricsClient: metricsClient,
|
|
503
534
|
getFormatService,
|
|
504
535
|
fixedThisTurn: runtime.fixedThisTurn,
|
|
505
536
|
});
|
|
@@ -632,11 +663,20 @@ async function dispatchPipelineAnalysis(args) {
|
|
|
632
663
|
// ladder (the #2402 after-write hash was never stamped), and a duplicate
|
|
633
664
|
// tool_result for the same bytes analysed the file a second time (the
|
|
634
665
|
// already-analysed latch was never set).
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
666
|
+
// The latch identifies the bytes this pipeline actually analysed. A pipeline
|
|
667
|
+
// that reports no write analysed its input state, even if another writer
|
|
668
|
+
// changed disk while the await was parked. A pipeline-reported write selects
|
|
669
|
+
// only the identity captured by runPipeline before analysis awaits (#2499).
|
|
670
|
+
const pipelineOwnedWriteHash = result.fileModified
|
|
671
|
+
? result.postWriteStateHash
|
|
672
|
+
: undefined;
|
|
673
|
+
const finalStateHash = pipelineOwnedWriteHash ?? initialStateHash;
|
|
674
|
+
if (!result.fileModified || pipelineOwnedWriteHash !== undefined) {
|
|
675
|
+
lastAnalyzedStateByFile.set(filePath, {
|
|
676
|
+
turnIndex: runtime.turnIndex,
|
|
677
|
+
stateHash: finalStateHash,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
640
680
|
// #2402: pi-lens' own immediate format/autofix may have rewritten the file
|
|
641
681
|
// after the native edit was recorded by the caller. Stamp the post-pipeline
|
|
642
682
|
// state so an identical retry against the formatted bytes is still
|
|
@@ -645,7 +685,7 @@ async function dispatchPipelineAnalysis(args) {
|
|
|
645
685
|
// `initialStateHash` IS the post-write hash at both call sites — the
|
|
646
686
|
// classified chain passes `postWriteStateHash` verbatim, the observed path
|
|
647
687
|
// passes the same pre-settle digest.
|
|
648
|
-
if (
|
|
688
|
+
if (pipelineOwnedWriteHash !== undefined) {
|
|
649
689
|
for (const pair of nativeAppliedPairs) {
|
|
650
690
|
runtime.partialApplyRecords.noteAfterWriteHash(filePath, pair.oldText, pair.newText, finalStateHash);
|
|
651
691
|
}
|
|
@@ -653,7 +693,7 @@ async function dispatchPipelineAnalysis(args) {
|
|
|
653
693
|
// The model's write/edit and pi-lens' own immediate format/autofix are now
|
|
654
694
|
// reflected on disk. Refresh read-guard staleness stamps so a follow-up edit
|
|
655
695
|
// is judged by read-range coverage, not by our own previous write.
|
|
656
|
-
if (!getFlag("no-read-guard")) {
|
|
696
|
+
if (!getFlag("no-read-guard") && allowReadGuardWrites) {
|
|
657
697
|
const changedForReadGuard = new Set([
|
|
658
698
|
path.resolve(filePath),
|
|
659
699
|
...(result.changedFiles ?? []).map((changedFile) => path.resolve(changedFile)),
|
|
@@ -670,6 +710,7 @@ export async function handleToolResult(deps) {
|
|
|
670
710
|
const { event, getFlag, dbg, runtime, cacheManager, agentBehaviorRecord, formatBehaviorWarnings, } = deps;
|
|
671
711
|
const rawFilePath = event.input.path;
|
|
672
712
|
const workspaceRoot = runtime.projectRoot || process.cwd();
|
|
713
|
+
let bashAuthorshipConfirmed = deps._readGuardAuthorship ?? event.toolName !== "bash";
|
|
673
714
|
// #1642: a gitignored worktree edit got re-attributed onto a
|
|
674
715
|
// same-relative-path file in the parent checkout because this handler
|
|
675
716
|
// used to always resolve a relative path against `workspaceRoot`
|
|
@@ -791,10 +832,16 @@ export async function handleToolResult(deps) {
|
|
|
791
832
|
// THIS set, not raw recognized - otherwise a redirect target dropped
|
|
792
833
|
// by the isError filter would be subtracted from recovery AND
|
|
793
834
|
// excluded here, attributed nowhere.
|
|
794
|
-
|
|
835
|
+
let recognizedWritten = event.isError !== true
|
|
795
836
|
? recognized.filter((wp) => !isExternalOrVendorFile(wp, workspaceRoot) &&
|
|
796
837
|
!isPathIgnoredByProject(wp, workspaceRoot, false))
|
|
797
838
|
: [];
|
|
839
|
+
// Fence the mtime-based session-authored fallback before any async
|
|
840
|
+
// observation. Only later content evidence may clear this fence.
|
|
841
|
+
if (!getFlag("no-read-guard")) {
|
|
842
|
+
for (const recognizedPath of recognizedWritten)
|
|
843
|
+
deps.readGuard?.recordUnchanged?.(recognizedPath);
|
|
844
|
+
}
|
|
798
845
|
// #2000 phase 2: when the extractor recognizes NOTHING, the command is
|
|
799
846
|
// opaque-candidate — recover its actual changed set by diffing the pre
|
|
800
847
|
// snapshot taken at tool_call. Partial writes that landed before a
|
|
@@ -802,6 +849,8 @@ export async function handleToolResult(deps) {
|
|
|
802
849
|
// them) — a deliberate divergence from the isError filter above, which
|
|
803
850
|
// exists for restore semantics where attribution would lie.
|
|
804
851
|
let opaquePaths = [];
|
|
852
|
+
let observedChangedKeys;
|
|
853
|
+
let recognizedAuthored = [];
|
|
805
854
|
// Recovery runs for EVERY bash command with a pending baseline - not
|
|
806
855
|
// only recognized-empty ones. A mixed command (`python x.py > out.ts`
|
|
807
856
|
// plus script-internal writes) previously skipped observation entirely
|
|
@@ -829,6 +878,7 @@ export async function handleToolResult(deps) {
|
|
|
829
878
|
if (recovery.verdict === "recovered") {
|
|
830
879
|
opaquePaths = recovery.paths.filter((p) => !isExternalOrVendorFile(p, scanRoot) &&
|
|
831
880
|
!isPathIgnoredByProject(p, scanRoot, false));
|
|
881
|
+
observedChangedKeys = new Set(opaquePaths);
|
|
832
882
|
}
|
|
833
883
|
else if (recovery.verdict === "unknown") {
|
|
834
884
|
// #2060: deliberately WIDER than the old `recognized.length > 0`
|
|
@@ -837,6 +887,12 @@ export async function handleToolResult(deps) {
|
|
|
837
887
|
// nothing at all.
|
|
838
888
|
unknownReason = recovery.unknownReason;
|
|
839
889
|
}
|
|
890
|
+
else {
|
|
891
|
+
// A clean git verdict is complete evidence that no path
|
|
892
|
+
// changed; it is not missing evidence. Keep recognized writes
|
|
893
|
+
// out of authorship because git found no changed bytes.
|
|
894
|
+
observedChangedKeys = new Set();
|
|
895
|
+
}
|
|
840
896
|
// #2060: both counts are bounded (one record per tool_result, no
|
|
841
897
|
// per-path logging) and exist because filtering is invisible in
|
|
842
898
|
// production otherwise - the dropped paths simply never appear.
|
|
@@ -862,9 +918,11 @@ export async function handleToolResult(deps) {
|
|
|
862
918
|
else if (pending.stats) {
|
|
863
919
|
const outcome = await captureFileStats(scanRoot, {
|
|
864
920
|
withHashes: true,
|
|
921
|
+
...deps._opaqueCaptureOptions,
|
|
865
922
|
});
|
|
866
923
|
if (outcome.snapshot && !outcome.unknownReason) {
|
|
867
|
-
opaquePaths =
|
|
924
|
+
opaquePaths = diffFileContent(pending.stats, outcome.snapshot);
|
|
925
|
+
observedChangedKeys = new Set(opaquePaths);
|
|
868
926
|
}
|
|
869
927
|
else {
|
|
870
928
|
unknownReason =
|
|
@@ -880,6 +938,16 @@ export async function handleToolResult(deps) {
|
|
|
880
938
|
const survivingKeys = new Set(recognizedWritten.map((p) => normalizeMapKey(path.resolve(p))));
|
|
881
939
|
opaquePaths = opaquePaths.filter((p) => !survivingKeys.has(p));
|
|
882
940
|
}
|
|
941
|
+
if (observedChangedKeys) {
|
|
942
|
+
recognizedAuthored = recognizedWritten.filter((file) => observedChangedKeys.has(normalizeMapKey(path.resolve(file))));
|
|
943
|
+
if (recognizedAuthored.length === 0)
|
|
944
|
+
recognizedWritten = [];
|
|
945
|
+
}
|
|
946
|
+
else if (unknownReason) {
|
|
947
|
+
recognizedAuthored = [];
|
|
948
|
+
// The bounded opaque_mutation_coverage_unknown record below remains
|
|
949
|
+
// the production proof that authorship evidence was unavailable.
|
|
950
|
+
}
|
|
883
951
|
if (unknownReason) {
|
|
884
952
|
logLatency({
|
|
885
953
|
type: "phase",
|
|
@@ -897,15 +965,24 @@ export async function handleToolResult(deps) {
|
|
|
897
965
|
durationMs: Date.now() - started,
|
|
898
966
|
result: `changed:${opaquePaths.length}`,
|
|
899
967
|
});
|
|
968
|
+
void resyncGitChangedFiles(opaquePaths).catch((err) => {
|
|
969
|
+
dbg(`git-change LSP resync failed: ${err}`);
|
|
970
|
+
});
|
|
900
971
|
}
|
|
901
972
|
}
|
|
902
973
|
// wp iterates opaquePaths VERBATIM (already normalizeMapKey keys), so the
|
|
903
974
|
// set must hold those exact strings - no re-resolution.
|
|
904
975
|
const opaqueSet = new Set(opaquePaths);
|
|
905
976
|
const written = [...recognizedWritten, ...opaquePaths];
|
|
977
|
+
const recognizedAuthoredSet = new Set(recognizedAuthored);
|
|
978
|
+
bashAuthorshipConfirmed =
|
|
979
|
+
recognizedAuthored.length > 0 || opaquePaths.length > 0;
|
|
906
980
|
for (const wp of written) {
|
|
907
|
-
if (!getFlag("no-read-guard")
|
|
981
|
+
if (!getFlag("no-read-guard") &&
|
|
982
|
+
(opaqueSet.has(wp) || recognizedAuthoredSet.has(wp)))
|
|
908
983
|
deps.readGuard?.recordWritten(wp);
|
|
984
|
+
else if (!getFlag("no-read-guard") && recognizedWritten.includes(wp))
|
|
985
|
+
deps.readGuard?.recordUnchanged?.(wp);
|
|
909
986
|
const receipt = runtime
|
|
910
987
|
.recordMutationToolReceipt;
|
|
911
988
|
const autofixMode = receipt
|
|
@@ -936,6 +1013,7 @@ export async function handleToolResult(deps) {
|
|
|
936
1013
|
_autofixMode: autofixMode,
|
|
937
1014
|
_attachmentBudget: syntheticAttachmentBudget,
|
|
938
1015
|
_mutationSourceOverride: isOpaque ? "opaque-script" : undefined,
|
|
1016
|
+
_readGuardAuthorship: opaqueSet.has(wp) || recognizedAuthoredSet.has(wp),
|
|
939
1017
|
});
|
|
940
1018
|
if (syntheticResult) {
|
|
941
1019
|
// #1590: forward verbatim. The synthetic call already charged the
|
|
@@ -946,7 +1024,57 @@ export async function handleToolResult(deps) {
|
|
|
946
1024
|
}
|
|
947
1025
|
}
|
|
948
1026
|
if (event.isError !== true && !getFlag("no-read-guard")) {
|
|
949
|
-
|
|
1027
|
+
const truncation = event.details?.truncation;
|
|
1028
|
+
const parsedSpans = extractReadPathsFromCommand(command, workspaceRoot);
|
|
1029
|
+
let spans = parsedSpans;
|
|
1030
|
+
if (truncation?.truncated === true &&
|
|
1031
|
+
typeof truncation.outputLines === "number") {
|
|
1032
|
+
if (parsedSpans.length === 1) {
|
|
1033
|
+
const span = parsedSpans[0];
|
|
1034
|
+
if (span) {
|
|
1035
|
+
// countFileLines intentionally preserves the split-based guard
|
|
1036
|
+
// convention, where a trailing newline contributes an empty final
|
|
1037
|
+
// element. The host's output line count does not include that
|
|
1038
|
+
// element, so remove it from the span basis before taking the tail.
|
|
1039
|
+
const hasTrailingNewline = nodeFs
|
|
1040
|
+
.readFileSync(span.filePath, "utf8")
|
|
1041
|
+
.endsWith("\n");
|
|
1042
|
+
const spanLineCount = hasTrailingNewline
|
|
1043
|
+
? Math.max(1, span.limit - 1)
|
|
1044
|
+
: span.limit;
|
|
1045
|
+
const shown = Math.min(truncation.outputLines, spanLineCount);
|
|
1046
|
+
spans =
|
|
1047
|
+
shown > 0
|
|
1048
|
+
? [
|
|
1049
|
+
{
|
|
1050
|
+
...span,
|
|
1051
|
+
offset: span.offset + spanLineCount - shown,
|
|
1052
|
+
limit: shown,
|
|
1053
|
+
},
|
|
1054
|
+
]
|
|
1055
|
+
: [];
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
else if (parsedSpans.length > 1) {
|
|
1059
|
+
recordDegradationOnce({
|
|
1060
|
+
kind: "bash-view-clipped",
|
|
1061
|
+
subject: "multi-span",
|
|
1062
|
+
reason: "truncated bash view contained multiple spans",
|
|
1063
|
+
});
|
|
1064
|
+
spans = [];
|
|
1065
|
+
}
|
|
1066
|
+
if (parsedSpans.length === 1 &&
|
|
1067
|
+
spans.length === 1 &&
|
|
1068
|
+
(spans[0]?.offset !== parsedSpans[0]?.offset ||
|
|
1069
|
+
spans[0]?.limit !== parsedSpans[0]?.limit)) {
|
|
1070
|
+
recordDegradationOnce({
|
|
1071
|
+
kind: "bash-view-clipped",
|
|
1072
|
+
subject: parsedSpans[0]?.filePath ?? "unknown",
|
|
1073
|
+
reason: "truncated bash view narrowed a single span",
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
for (const span of spans) {
|
|
950
1078
|
if (isExternalOrVendorFile(span.filePath, workspaceRoot))
|
|
951
1079
|
continue;
|
|
952
1080
|
if (isPathIgnoredByProject(span.filePath, workspaceRoot, false))
|
|
@@ -1026,6 +1154,74 @@ export async function handleToolResult(deps) {
|
|
|
1026
1154
|
});
|
|
1027
1155
|
}
|
|
1028
1156
|
}
|
|
1157
|
+
// Native read results are the authoritative read boundary. The tool_call
|
|
1158
|
+
// input can request past EOF, and the host can cap bytes or lines before the
|
|
1159
|
+
// result reaches the model. Register only the delivered range (#2802).
|
|
1160
|
+
if (deps.readGuard &&
|
|
1161
|
+
event.toolName === "read" &&
|
|
1162
|
+
event.isError !== true &&
|
|
1163
|
+
filePath &&
|
|
1164
|
+
!getFlag("no-read-guard") &&
|
|
1165
|
+
!isExternalOrVendorFile(filePath, workspaceRoot)) {
|
|
1166
|
+
const deliveredFilePath = attribution?.resolvedPath && nodeFs.existsSync(attribution.resolvedPath)
|
|
1167
|
+
? attribution.resolvedPath
|
|
1168
|
+
: filePath;
|
|
1169
|
+
if (nodeFs.existsSync(deliveredFilePath)) {
|
|
1170
|
+
const nativeReadToolCallId = resolveToolCallCorrelationId(event);
|
|
1171
|
+
const input = event.input;
|
|
1172
|
+
const requestedOffset = input.offset ?? 1;
|
|
1173
|
+
const requestedLimit = input.limit;
|
|
1174
|
+
const truncation = event.details?.truncation;
|
|
1175
|
+
const available = Math.max(0, countFileLines(deliveredFilePath) - requestedOffset + 1);
|
|
1176
|
+
const deliveredLimit = Math.min(available, truncation?.outputLines ?? requestedLimit ?? available);
|
|
1177
|
+
if (deliveredLimit > 0) {
|
|
1178
|
+
if (truncation?.truncated === true && deliveredLimit < available) {
|
|
1179
|
+
recordDegradationOnce({
|
|
1180
|
+
kind: "native-read-clipped",
|
|
1181
|
+
subject: deliveredFilePath,
|
|
1182
|
+
reason: "native read result was clipped before delivery",
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
logReadGuardEvent({
|
|
1186
|
+
event: "read_pattern",
|
|
1187
|
+
sessionId: runtime.telemetrySessionId,
|
|
1188
|
+
filePath: deliveredFilePath,
|
|
1189
|
+
requestedOffset,
|
|
1190
|
+
requestedLimit: requestedLimit ?? deliveredLimit,
|
|
1191
|
+
effectiveOffset: requestedOffset,
|
|
1192
|
+
effectiveLimit: deliveredLimit,
|
|
1193
|
+
metadata: {
|
|
1194
|
+
totalLines: countFileLines(deliveredFilePath),
|
|
1195
|
+
isPartial: deliveredLimit < available,
|
|
1196
|
+
fileKind: detectFileKind(deliveredFilePath) ?? "unknown",
|
|
1197
|
+
fractionRead: available > 0
|
|
1198
|
+
? Math.round((deliveredLimit / available) * 100) / 100
|
|
1199
|
+
: 1,
|
|
1200
|
+
expandedByTs: false,
|
|
1201
|
+
},
|
|
1202
|
+
});
|
|
1203
|
+
const deliveredRecord = {
|
|
1204
|
+
filePath: deliveredFilePath,
|
|
1205
|
+
requestedOffset,
|
|
1206
|
+
requestedLimit: requestedLimit ?? deliveredLimit,
|
|
1207
|
+
effectiveOffset: requestedOffset,
|
|
1208
|
+
effectiveLimit: deliveredLimit,
|
|
1209
|
+
expandedByLsp: false,
|
|
1210
|
+
turnIndex: runtime.turnIndex,
|
|
1211
|
+
writeIndex: runtime.peekWriteIndex(),
|
|
1212
|
+
timestamp: Date.now(),
|
|
1213
|
+
};
|
|
1214
|
+
if (nativeReadToolCallId) {
|
|
1215
|
+
deps.readGuard.recordRead(deliveredRecord, {
|
|
1216
|
+
supersedes: { toolCallId: nativeReadToolCallId },
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
else {
|
|
1220
|
+
deps.readGuard.recordRead(deliveredRecord);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1029
1225
|
// #2423: THE inbound gate. Everything below this line is the mutation
|
|
1030
1226
|
// bookkeeping chain — read-guard staleness stamp, turn state, change-log
|
|
1031
1227
|
// receipt, deferred autofix and format. Before the seam this compared
|
|
@@ -1195,14 +1391,36 @@ export async function handleToolResult(deps) {
|
|
|
1195
1391
|
// "edit", so the un-receipted default is "deferred" either way.
|
|
1196
1392
|
const observedAutofixMode = receiptOutcome?.autofixMode ??
|
|
1197
1393
|
(observedKind === "edit" ? "deferred" : "immediate");
|
|
1198
|
-
// Analyse the
|
|
1199
|
-
//
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1394
|
+
// Analyse the files the observation actually RECORDED, not merely the
|
|
1395
|
+
// path the tool named. A directory is never a pipeline target.
|
|
1396
|
+
const observedDispatchPaths = observedChangedPaths
|
|
1397
|
+
.filter((candidate) => {
|
|
1398
|
+
try {
|
|
1399
|
+
return !nodeFs.statSync(candidate).isDirectory();
|
|
1400
|
+
}
|
|
1401
|
+
catch {
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
})
|
|
1405
|
+
.slice(0, OBSERVED_DISPATCH_MAX_PATHS);
|
|
1406
|
+
const observedDispatchDropped = observedChangedPaths.length - observedDispatchPaths.length;
|
|
1407
|
+
if (observedDispatchDropped > 0) {
|
|
1408
|
+
incrementDegradationCount({
|
|
1409
|
+
kind: "observed-mutation-dispatch-cap",
|
|
1410
|
+
subject: event.toolName,
|
|
1411
|
+
reason: `observed mutation dispatch capped at ${OBSERVED_DISPATCH_MAX_PATHS}; ${observedDispatchDropped} path(s) not dispatched`,
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
if (observedDispatchPaths.length > 0) {
|
|
1415
|
+
const observedClients = ensureToolResultClients(deps);
|
|
1416
|
+
if (observedClients !== true &&
|
|
1417
|
+
!(await bounded(Promise.resolve(observedClients), {
|
|
1418
|
+
ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
|
|
1419
|
+
signal: deps.signal ?? undefined,
|
|
1420
|
+
hook: "tool_result_edit",
|
|
1421
|
+
label: "observed-tool-result-analysis",
|
|
1422
|
+
})))
|
|
1423
|
+
return;
|
|
1206
1424
|
const observedReadGuardCorrelationId = getReadGuardCorrelationId(event);
|
|
1207
1425
|
// #2464 review round 3 (F1): the SAME two pre-conditions the
|
|
1208
1426
|
// classified site consults, through the same shared claim. Round 2
|
|
@@ -1212,60 +1430,72 @@ export async function handleToolResult(deps) {
|
|
|
1212
1430
|
// second overwrote the first's registry entry, and the first's
|
|
1213
1431
|
// release then evicted the outer entry a live, unrelated classified
|
|
1214
1432
|
// pipeline had since re-created under it.
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1433
|
+
for (const observedPath of observedDispatchPaths) {
|
|
1434
|
+
const observedDispatchSignal = deps.signal;
|
|
1435
|
+
const observedJoinSignal = deps.signal;
|
|
1436
|
+
const observedStateHashForPath = getFileStateHash(observedPath);
|
|
1437
|
+
const observedClaim = claimPipelineDispatch({
|
|
1438
|
+
filePath: observedPath,
|
|
1439
|
+
stateHash: observedStateHashForPath,
|
|
1440
|
+
turnIndex: runtime.turnIndex,
|
|
1441
|
+
participantId: observedReadGuardCorrelationId,
|
|
1442
|
+
dbg,
|
|
1443
|
+
});
|
|
1444
|
+
if (!observedClaim.proceed) {
|
|
1445
|
+
if (observedClaim.joined) {
|
|
1446
|
+
await bounded(observedClaim.joined, {
|
|
1447
|
+
ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
|
|
1448
|
+
signal: observedJoinSignal,
|
|
1449
|
+
hook: "tool_result_edit",
|
|
1450
|
+
label: "observed-tool-result-join",
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
const observedDispatchOutcome = await bounded(dispatchPipelineAnalysis({
|
|
1456
|
+
deps,
|
|
1457
|
+
runtime,
|
|
1458
|
+
filePath: observedPath,
|
|
1459
|
+
dispatchCwd: resolveLanguageRootForFile(observedPath, workspaceRoot),
|
|
1460
|
+
turnStateCwd: path.resolve(workspaceRoot),
|
|
1461
|
+
autofixMode: observedAutofixMode,
|
|
1462
|
+
modifiedRanges: undefined,
|
|
1463
|
+
writeIndex: runtime.nextWriteIndex(),
|
|
1464
|
+
initialStateHash: observedStateHashForPath,
|
|
1465
|
+
readGuardCorrelationId: observedReadGuardCorrelationId,
|
|
1466
|
+
requestedEditIndexes: getRequestedEditIndexes(event, observedKind),
|
|
1467
|
+
requestedEditTotal: getRequestedEditCount(event, observedKind),
|
|
1468
|
+
isPartialApplyResult: (event.details ?? {})
|
|
1469
|
+
.piLensPartialApply === true,
|
|
1470
|
+
participantIds: [observedReadGuardCorrelationId],
|
|
1471
|
+
participantTotal: 1,
|
|
1472
|
+
toolResultStart,
|
|
1473
|
+
nativeAppliedPairs: observedAppliedPairs,
|
|
1474
|
+
// #2802 authorship rule (master): the observational
|
|
1475
|
+
// settle already recorded this edit through the
|
|
1476
|
+
// bridge, so the dispatch's read-guard write refresh
|
|
1477
|
+
// carries evidence and stays enabled on every
|
|
1478
|
+
// per-path dispatch.
|
|
1479
|
+
allowReadGuardWrites: true,
|
|
1480
|
+
}), {
|
|
1481
|
+
ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
|
|
1482
|
+
signal: observedDispatchSignal,
|
|
1483
|
+
hook: "tool_result_edit",
|
|
1484
|
+
label: "observed-tool-result-analysis",
|
|
1485
|
+
});
|
|
1486
|
+
if (observedDispatchOutcome?.crashed) {
|
|
1487
|
+
// #2464 review round 2, S6: parity with the classified chain — a
|
|
1488
|
+
// pipeline crash surfaces its notice to the agent instead of being
|
|
1489
|
+
// swallowed into a dbg line the model never sees. The recorded
|
|
1490
|
+
// edit stands either way; only the analysis was lost.
|
|
1491
|
+
dbg(`tool_result: pipeline analysis crashed for the observed mutation on ${observedPath}; the recorded edit stands, analysis did not run this turn`);
|
|
1492
|
+
return observedDispatchOutcome.response;
|
|
1225
1493
|
}
|
|
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
1494
|
}
|
|
1265
1495
|
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
1496
|
}
|
|
1267
1497
|
else {
|
|
1268
|
-
dbg(`tool_result: the observational settle recorded ${observedReplayed} mutation(s) for "${event.toolName}",
|
|
1498
|
+
dbg(`tool_result: the observational settle recorded ${observedReplayed} mutation(s) for "${event.toolName}", but no dispatchable changed paths remained`);
|
|
1269
1499
|
}
|
|
1270
1500
|
}
|
|
1271
1501
|
return syntheticWriteContent.length > 0
|
|
@@ -1376,7 +1606,8 @@ export async function handleToolResult(deps) {
|
|
|
1376
1606
|
}
|
|
1377
1607
|
// Refresh the read-guard's FileTime stamp so that the model's own write
|
|
1378
1608
|
// doesn't trigger a spurious "file_modified" block on the next edit.
|
|
1379
|
-
|
|
1609
|
+
if (bashAuthorshipConfirmed)
|
|
1610
|
+
deps.readGuard?.recordWritten(filePath);
|
|
1380
1611
|
// Keep cachedExports in sync after each write/edit so the pre-write STOP
|
|
1381
1612
|
// check doesn't fire on names that were removed from this file this session.
|
|
1382
1613
|
refreshCachedExports(runtime, filePath);
|
|
@@ -1408,7 +1639,7 @@ export async function handleToolResult(deps) {
|
|
|
1408
1639
|
// tool_result is emitted after write/edit has already been applied.
|
|
1409
1640
|
// Asserting pre-write stamps here produces false positives on rapid edits.
|
|
1410
1641
|
sessionFileTime.read(filePath);
|
|
1411
|
-
if (!getFlag("no-read-guard")) {
|
|
1642
|
+
if (!getFlag("no-read-guard") && bashAuthorshipConfirmed) {
|
|
1412
1643
|
const readGuard = runtime.readGuard;
|
|
1413
1644
|
readGuard?.recordWritten?.(filePath);
|
|
1414
1645
|
}
|
|
@@ -1529,7 +1760,16 @@ export async function handleToolResult(deps) {
|
|
|
1529
1760
|
// (defined above `handleToolResult`), shared with the observed-mutation
|
|
1530
1761
|
// early return. This call site is otherwise unchanged — same arguments, same
|
|
1531
1762
|
// crash-then-return / success-then-continue shape as before the split.
|
|
1532
|
-
const
|
|
1763
|
+
const classifiedClients = ensureToolResultClients(deps);
|
|
1764
|
+
if (classifiedClients !== true &&
|
|
1765
|
+
!(await bounded(Promise.resolve(classifiedClients), {
|
|
1766
|
+
ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
|
|
1767
|
+
signal: deps.signal,
|
|
1768
|
+
hook: "tool_result_edit",
|
|
1769
|
+
label: "classified-tool-result-analysis",
|
|
1770
|
+
})))
|
|
1771
|
+
return;
|
|
1772
|
+
const dispatchOutcome = await bounded(dispatchPipelineAnalysis({
|
|
1533
1773
|
deps,
|
|
1534
1774
|
runtime,
|
|
1535
1775
|
filePath,
|
|
@@ -1547,7 +1787,15 @@ export async function handleToolResult(deps) {
|
|
|
1547
1787
|
participantTotal,
|
|
1548
1788
|
toolResultStart,
|
|
1549
1789
|
nativeAppliedPairs,
|
|
1790
|
+
allowReadGuardWrites: bashAuthorshipConfirmed,
|
|
1791
|
+
}), {
|
|
1792
|
+
ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
|
|
1793
|
+
signal: deps.signal || undefined,
|
|
1794
|
+
hook: "tool_result_edit",
|
|
1795
|
+
label: "pipeline-analysis",
|
|
1550
1796
|
});
|
|
1797
|
+
if (!dispatchOutcome)
|
|
1798
|
+
return;
|
|
1551
1799
|
if (dispatchOutcome.crashed) {
|
|
1552
1800
|
return dispatchOutcome.response;
|
|
1553
1801
|
}
|
|
@@ -1681,7 +1929,7 @@ export async function handleToolResult(deps) {
|
|
|
1681
1929
|
// #1561: stamp the verdict with THIS dispatch's write token — the same
|
|
1682
1930
|
// counter `lsp_diagnostics`' reconciliation seam draws from — so a later
|
|
1683
1931
|
// confirmed-clean result can be ordered against it instead of racing it.
|
|
1684
|
-
runtime.recordInlineBlockers(filePath, result.inlineBlockerSummary, writeIndex, result.inlineBlockerSources, result.inlineBlockerLines);
|
|
1932
|
+
runtime.recordInlineBlockers(filePath, result.inlineBlockerSummary, writeIndex, result.inlineBlockerSources, result.inlineBlockerLines, result.inlineBlockerFileContent);
|
|
1685
1933
|
}
|
|
1686
1934
|
else {
|
|
1687
1935
|
runtime.clearInlineBlockers(filePath);
|