@herbertgao/pi-extensions 2026.8.5 → 2026.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +48 -6
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +10 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +5 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +24 -17
- package/node_modules/@narumitw/pi-btw/package.json +5 -5
- package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +33 -13
- package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
- package/node_modules/pi-lens/CHANGELOG.md +95 -0
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
- package/node_modules/pi-lens/dist/clients/biome-client.js +121 -13
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
- package/node_modules/pi-lens/dist/clients/bus-publish.js +11 -3
- package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +16 -4
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +135 -30
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +19 -7
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +6 -4
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +114 -10
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +101 -16
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +230 -50
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +296 -3
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +39 -8
- package/node_modules/pi-lens/dist/clients/git-guard.js +18 -21
- package/node_modules/pi-lens/dist/clients/go-client.js +21 -39
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +116 -7
- package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
- package/node_modules/pi-lens/dist/clients/installer/index.js +5 -1
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +4 -9
- package/node_modules/pi-lens/dist/clients/knip-client.js +51 -14
- package/node_modules/pi-lens/dist/clients/latency-logger.js +50 -1
- package/node_modules/pi-lens/dist/clients/lens-events.js +56 -25
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +45 -2
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +58 -13
- package/node_modules/pi-lens/dist/clients/lsp/client.js +169 -8
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +29 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +264 -66
- package/node_modules/pi-lens/dist/clients/lsp/server.js +247 -60
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +4 -0
- package/node_modules/pi-lens/dist/clients/mcp/session.js +30 -17
- package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +29 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
- package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +153 -7
- package/node_modules/pi-lens/dist/clients/runtime-context.js +104 -11
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +170 -22
- package/node_modules/pi-lens/dist/clients/runtime-session.js +28 -5
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +124 -10
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +388 -35
- package/node_modules/pi-lens/dist/clients/rust-client.js +21 -37
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +88 -5
- package/node_modules/pi-lens/dist/clients/sg-runner.js +141 -23
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +18 -11
- package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +431 -24
- package/node_modules/pi-lens/dist/clients/tool-policy.js +2 -0
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +17 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +305 -33
- package/node_modules/pi-lens/dist/index.js +4827 -1633
- package/node_modules/pi-lens/dist/mcp/server.js +8 -2
- package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +3 -2
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +62 -7
- package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
- package/node_modules/pi-lens/docs/agent-guide.md +38 -13
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +11 -3
- package/node_modules/pi-lens/docs/features.md +14 -1
- package/node_modules/pi-lens/docs/globalconfig.md +11 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +1 -1
- package/node_modules/pi-lens/docs/settings.md +6 -0
- package/node_modules/pi-lens/docs/usage.md +23 -5
- package/node_modules/pi-lens/docs/word-index.md +35 -0
- package/node_modules/pi-lens/package.json +1 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +79 -0
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
- package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +13 -0
- package/node_modules/pi-mcp-adapter/README.md +4 -1
- package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
- package/node_modules/pi-mcp-adapter/cli.js +25 -4
- package/node_modules/pi-mcp-adapter/config.ts +4 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +18 -15
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +18 -8
- package/node_modules/pi-mcp-adapter/package.json +2 -1
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
- package/node_modules/pi-mcp-adapter/server-manager.ts +5 -0
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +38 -18
- package/node_modules/pi-mcp-adapter/types.ts +93 -10
- package/node_modules/pi-web-access/CHANGELOG.md +14 -0
- package/node_modules/pi-web-access/README.md +18 -12
- package/node_modules/pi-web-access/auth-fetch.ts +148 -0
- package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
- package/node_modules/pi-web-access/curator-page.ts +5 -3
- package/node_modules/pi-web-access/curator-server.ts +2 -1
- package/node_modules/pi-web-access/extract.ts +106 -34
- package/node_modules/pi-web-access/fetch-params.ts +17 -3
- package/node_modules/pi-web-access/firecrawl.ts +172 -12
- package/node_modules/pi-web-access/gemini-search.ts +18 -4
- package/node_modules/pi-web-access/index.ts +120 -48
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/summary-review.ts +11 -5
- package/node_modules/pi-web-access/youtube-extract.ts +2 -2
- package/package.json +9 -9
|
@@ -1,22 +1,72 @@
|
|
|
1
|
+
import { provenanceStamp, validateAdvisoryProvenance, } from "./advisory-provenance.js";
|
|
2
|
+
import { logLatency } from "./latency-logger.js";
|
|
1
3
|
// Exported so the Stop-hook bin strips exactly what these bridges prepend.
|
|
2
4
|
export const AUTOMATION_FRAMING = "[pi-lens automated check — not a user request] ";
|
|
3
|
-
|
|
5
|
+
// #1432 review (S3b): a multi-file bash write can carry hundreds of changed
|
|
6
|
+
// paths, one `reasons` entry apiece — logging all of them shrinks the
|
|
7
|
+
// smells-rollup tail window (a fixed-size ring) to a handful of these
|
|
8
|
+
// records. Cap what this decision row carries; the full list is never
|
|
9
|
+
// needed for triage, only "how many and roughly what kind".
|
|
10
|
+
const MAX_LOGGED_REASONS = 8;
|
|
11
|
+
function boundedReasons(reasons) {
|
|
12
|
+
if (reasons.length <= MAX_LOGGED_REASONS)
|
|
13
|
+
return reasons;
|
|
14
|
+
return [
|
|
15
|
+
...reasons.slice(0, MAX_LOGGED_REASONS),
|
|
16
|
+
`+${reasons.length - MAX_LOGGED_REASONS} more`,
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
function logProvenanceDecision(validation, provenance, advisoryKind, cwd) {
|
|
20
|
+
logLatency({
|
|
21
|
+
type: "phase",
|
|
22
|
+
phase: "advisory_provenance_decision",
|
|
23
|
+
filePath: cwd,
|
|
24
|
+
durationMs: 0,
|
|
25
|
+
metadata: {
|
|
26
|
+
decision: validation.status === "current" ? "current" : "historical",
|
|
27
|
+
reasons: boundedReasons(validation.reasons),
|
|
28
|
+
changedPathCount: validation.changedPathCount,
|
|
29
|
+
provenanceStamp: provenanceStamp(provenance),
|
|
30
|
+
advisoryKind,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function historicalPrefix(provenance) {
|
|
35
|
+
return `Historical finding; workspace changed since capture; re-run to confirm. (${provenanceStamp(provenance)})`;
|
|
36
|
+
}
|
|
37
|
+
function historicalTestContent(content, provenance) {
|
|
38
|
+
return content.startsWith("[from a prior turn")
|
|
39
|
+
? content
|
|
40
|
+
: `${historicalPrefix(provenance)}\n\n${content}`;
|
|
41
|
+
}
|
|
42
|
+
function turnEndMessage(content, current, provenance) {
|
|
4
43
|
return {
|
|
5
44
|
role: "user",
|
|
6
|
-
content:
|
|
45
|
+
content: current
|
|
46
|
+
? `${AUTOMATION_FRAMING}Address 🔴 blockers before continuing; ℹ️ advisories are informational only.\n\n${content}`
|
|
47
|
+
: `${AUTOMATION_FRAMING}${historicalPrefix(provenance)}\n\n${content}`,
|
|
7
48
|
};
|
|
8
49
|
}
|
|
9
50
|
/** Read a turn-end finding without changing its durable delivery state. */
|
|
10
|
-
export function peekTurnEndFindings(cacheManager, cwd) {
|
|
51
|
+
export function peekTurnEndFindings(cacheManager, cwd, runtime, logDelivery = false) {
|
|
11
52
|
const findings = cacheManager.readCache("turn-end-findings", cwd);
|
|
12
53
|
if (!findings?.data?.content || findings.data.consumed === true)
|
|
13
54
|
return;
|
|
14
|
-
|
|
55
|
+
const validation = validateAdvisoryProvenance(findings.data, cwd, runtime);
|
|
56
|
+
if (logDelivery)
|
|
57
|
+
logProvenanceDecision(validation, findings.data.provenance, "turn-end", cwd);
|
|
58
|
+
if (validation.allFilesDeleted)
|
|
59
|
+
return;
|
|
60
|
+
return {
|
|
61
|
+
messages: [turnEndMessage(findings.data.content, validation.status === "current", findings.data.provenance)],
|
|
62
|
+
};
|
|
15
63
|
}
|
|
16
|
-
export function consumeTurnEndFindings(cacheManager, cwd) {
|
|
64
|
+
export function consumeTurnEndFindings(cacheManager, cwd, runtime) {
|
|
17
65
|
const findings = cacheManager.readCache("turn-end-findings", cwd);
|
|
18
66
|
if (!findings?.data?.content || findings.data.consumed === true)
|
|
19
67
|
return;
|
|
68
|
+
const validation = validateAdvisoryProvenance(findings.data, cwd, runtime);
|
|
69
|
+
logProvenanceDecision(validation, findings.data.provenance, "turn-end", cwd);
|
|
20
70
|
// A blocker record is also the opt-in commit gate's durable state. Mark the
|
|
21
71
|
// context message consumed without deleting the record; clean/advisory-only
|
|
22
72
|
// records retain the historical consume-and-clear behavior.
|
|
@@ -27,29 +77,72 @@ export function consumeTurnEndFindings(cacheManager, cwd) {
|
|
|
27
77
|
else {
|
|
28
78
|
cacheManager.clearCache("turn-end-findings", cwd);
|
|
29
79
|
}
|
|
30
|
-
|
|
80
|
+
if (validation.allFilesDeleted)
|
|
81
|
+
return;
|
|
82
|
+
return {
|
|
83
|
+
messages: [turnEndMessage(findings.data.content, validation.status === "current", findings.data.provenance)],
|
|
84
|
+
};
|
|
31
85
|
}
|
|
32
86
|
/** Read test findings without consuming them; used by acknowledged IPC delivery. */
|
|
33
|
-
export function peekTestFindings(cacheManager, cwd) {
|
|
87
|
+
export function peekTestFindings(cacheManager, cwd, runtime, logDelivery = false) {
|
|
34
88
|
const findings = cacheManager.readCache("test-runner-findings", cwd);
|
|
35
89
|
if (!findings?.data?.content)
|
|
36
90
|
return;
|
|
91
|
+
const validation = validateAdvisoryProvenance(findings.data, cwd, runtime);
|
|
92
|
+
if (logDelivery)
|
|
93
|
+
logProvenanceDecision(validation, findings.data.provenance, "test-findings", cwd);
|
|
94
|
+
if (validation.allFilesDeleted)
|
|
95
|
+
return;
|
|
96
|
+
const current = validation.status === "current";
|
|
37
97
|
return {
|
|
38
98
|
messages: [
|
|
39
99
|
{
|
|
40
100
|
role: "user",
|
|
41
|
-
content:
|
|
101
|
+
content: current
|
|
102
|
+
? `${AUTOMATION_FRAMING}Test failures detected last turn — fix before continuing:\n\n${findings.data.content}`
|
|
103
|
+
: `${AUTOMATION_FRAMING}${historicalTestContent(findings.data.content, findings.data.provenance)}`,
|
|
42
104
|
},
|
|
43
105
|
],
|
|
44
106
|
};
|
|
45
107
|
}
|
|
46
|
-
export function consumeTestFindings(cacheManager, cwd) {
|
|
47
|
-
const
|
|
108
|
+
export function consumeTestFindings(cacheManager, cwd, runtime) {
|
|
109
|
+
const record = cacheManager.readCache("test-runner-findings", cwd);
|
|
110
|
+
if (!record?.data?.content)
|
|
111
|
+
return;
|
|
112
|
+
const findings = peekTestFindings(cacheManager, cwd, runtime, true);
|
|
48
113
|
if (!findings)
|
|
49
114
|
return;
|
|
50
|
-
|
|
115
|
+
// Retire the content but PRESERVE the generation high-water mark: nulling
|
|
116
|
+
// the whole slot would let a still-in-flight OLDER batch see `undefined`,
|
|
117
|
+
// pass the strictly-greater suppression check, and resurrect a consumed
|
|
118
|
+
// one-shot advisory with stale results. An empty-content record peeks as
|
|
119
|
+
// undelivered while keeping late-generation ordering intact.
|
|
120
|
+
const priorGeneration = cacheManager.readCache("test-runner-findings", cwd)?.data?.testRunGeneration;
|
|
121
|
+
cacheManager.writeCache("test-runner-findings", { content: "", testRunGeneration: priorGeneration }, cwd);
|
|
51
122
|
return findings;
|
|
52
123
|
}
|
|
124
|
+
/** Complete an acknowledged MCP delivery without re-validating or re-rendering it. */
|
|
125
|
+
export function acknowledgeTurnEndFindings(cacheManager, cwd) {
|
|
126
|
+
const findings = cacheManager.readCache("turn-end-findings", cwd);
|
|
127
|
+
if (!findings?.data?.content || findings.data.consumed === true)
|
|
128
|
+
return;
|
|
129
|
+
if (findings.data.hasBlockers === true && typeof findings.data.sessionId === "string") {
|
|
130
|
+
cacheManager.writeCache("turn-end-findings", { ...findings.data, consumed: true }, cwd);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
cacheManager.clearCache("turn-end-findings", cwd);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export function acknowledgeTestFindings(cacheManager, cwd) {
|
|
137
|
+
const findings = cacheManager.readCache("test-runner-findings", cwd);
|
|
138
|
+
if (!findings?.data?.content)
|
|
139
|
+
return;
|
|
140
|
+
// Same high-water-mark preservation as consumeTestFindings.
|
|
141
|
+
cacheManager.writeCache("test-runner-findings", {
|
|
142
|
+
content: "",
|
|
143
|
+
testRunGeneration: findings.data.testRunGeneration,
|
|
144
|
+
}, cwd);
|
|
145
|
+
}
|
|
53
146
|
export function consumeSessionStartGuidance(cacheManager, cwd) {
|
|
54
147
|
const guidance = cacheManager.readCache("session-start-guidance", cwd);
|
|
55
148
|
if (!guidance?.data?.content)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { logCascade } from "./cascade-logger.js";
|
|
4
5
|
import { normalizeMapKey } from "./path-utils.js";
|
|
5
6
|
import { PathKeyedMap } from "./path-keyed-map.js";
|
|
6
7
|
import { ReadGuard } from "./read-guard.js";
|
|
7
8
|
import { RUNTIME_CONFIG } from "./runtime-config.js";
|
|
8
9
|
import { TurnSummaryCollector } from "./turn-summary.js";
|
|
10
|
+
import { deriveProviderFromModelId } from "./model-provider.js";
|
|
9
11
|
export class RuntimeCoordinator {
|
|
10
12
|
_projectRoot = normalizeMapKey(process.cwd());
|
|
11
13
|
_sessionGeneration = 0;
|
|
@@ -25,7 +27,9 @@ export class RuntimeCoordinator {
|
|
|
25
27
|
coldSnapshotTouches: 0,
|
|
26
28
|
};
|
|
27
29
|
_complexityBaselines = new Map();
|
|
28
|
-
_fixedThisTurn = new
|
|
30
|
+
_fixedThisTurn = new PathKeyedMap(normalizeMapKey);
|
|
31
|
+
_writtenThisTurn = new PathKeyedMap(normalizeMapKey);
|
|
32
|
+
_autofixDemotedThisTurn = new PathKeyedMap(normalizeMapKey);
|
|
29
33
|
_reportedThisTurn = new Set();
|
|
30
34
|
_projectRulesScan = {
|
|
31
35
|
rules: [],
|
|
@@ -35,6 +39,19 @@ export class RuntimeCoordinator {
|
|
|
35
39
|
_lifecycleReason;
|
|
36
40
|
_hasStableSessionId = false;
|
|
37
41
|
_telemetryModel = "unknown";
|
|
42
|
+
// Raw model/provider identity, separate from the combined `provider/model`
|
|
43
|
+
// display string above — worklog/disposition attribution (#1448) wants the
|
|
44
|
+
// two fields apart, blank when the host never supplied them. `_telemetryProvider`
|
|
45
|
+
// is the explicit host value when given, else derived from the model id
|
|
46
|
+
// (deriveProviderFromModelId, blank on ambiguity — never guessed).
|
|
47
|
+
_telemetryModelId = "";
|
|
48
|
+
_telemetryProvider = "";
|
|
49
|
+
// True once a host has supplied an explicit provider this session. An
|
|
50
|
+
// explicit provider is never downgraded by a derivation from a later
|
|
51
|
+
// model-only call; a DERIVED provider, by contrast, is re-derived on
|
|
52
|
+
// every model-only call so a mid-session model switch (e.g. gpt-5-mini →
|
|
53
|
+
// claude-sonnet-4-5) doesn't leave a stale provider from the old model.
|
|
54
|
+
_telemetryProviderIsExplicit = false;
|
|
38
55
|
_turnIndex = 0;
|
|
39
56
|
_writeIndex = 0;
|
|
40
57
|
_projectSeq = 0;
|
|
@@ -51,7 +68,7 @@ export class RuntimeCoordinator {
|
|
|
51
68
|
callGraph = null;
|
|
52
69
|
wordIndex = null;
|
|
53
70
|
_readGuard = null;
|
|
54
|
-
|
|
71
|
+
_pendingDeferredMutations = new PathKeyedMap(normalizeMapKey);
|
|
55
72
|
_lspReadWarmState = new Map();
|
|
56
73
|
_pendingInlineBlockers = new PathKeyedMap(normalizeMapKey);
|
|
57
74
|
_actionableWarningsThisTurn = new Map();
|
|
@@ -78,10 +95,15 @@ export class RuntimeCoordinator {
|
|
|
78
95
|
coldSnapshotTouches: 0,
|
|
79
96
|
};
|
|
80
97
|
this._fixedThisTurn.clear();
|
|
98
|
+
this._writtenThisTurn.clear();
|
|
99
|
+
this._autofixDemotedThisTurn.clear();
|
|
81
100
|
this._reportedThisTurn.clear();
|
|
82
101
|
this._telemetrySessionId = `lens-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
83
102
|
this._hasStableSessionId = false;
|
|
84
103
|
this._telemetryModel = "unknown";
|
|
104
|
+
this._telemetryModelId = "";
|
|
105
|
+
this._telemetryProvider = "";
|
|
106
|
+
this._telemetryProviderIsExplicit = false;
|
|
85
107
|
this._turnIndex = 0;
|
|
86
108
|
this._writeIndex = 0;
|
|
87
109
|
this._projectSeq = 0;
|
|
@@ -92,7 +114,7 @@ export class RuntimeCoordinator {
|
|
|
92
114
|
this._gitGuardSummary = "";
|
|
93
115
|
this._gitGuardCacheUnknownReason = undefined;
|
|
94
116
|
this._readGuard = null;
|
|
95
|
-
this.
|
|
117
|
+
this._pendingDeferredMutations.clear();
|
|
96
118
|
this._lspReadWarmState.clear();
|
|
97
119
|
this._pendingInlineBlockers.clear();
|
|
98
120
|
this._actionableWarningsThisTurn.clear();
|
|
@@ -145,7 +167,30 @@ export class RuntimeCoordinator {
|
|
|
145
167
|
return this._gitGuardCacheUnknownReason;
|
|
146
168
|
}
|
|
147
169
|
beginTurn() {
|
|
148
|
-
|
|
170
|
+
// #1443: runs sitting here at turn_start were appended AFTER the last
|
|
171
|
+
// turn_end drained them (consumeCascadeRuns) — the quiet-window reconcile's
|
|
172
|
+
// late re-injection (`onResolvedFound`, clients/lsp/cascade-tier.ts) lands
|
|
173
|
+
// in exactly that window. Wiping them dead-ended that delivery path: the
|
|
174
|
+
// finding was computed, formatted, appended, and then deleted before any
|
|
175
|
+
// turn_end could render it. Carry them into THIS turn instead, exactly
|
|
176
|
+
// once: `carriedTurns` is stamped on the way through and a run that the
|
|
177
|
+
// next turn_end still did not consume is dropped here with a log line
|
|
178
|
+
// rather than queued forever (a stale finding must not outlive the state
|
|
179
|
+
// it describes, and an unbounded queue would replay it every turn).
|
|
180
|
+
this._cascadeRuns = this._cascadeRuns.flatMap((run) => {
|
|
181
|
+
const carriedTurns = (run.carriedTurns ?? 0) + 1;
|
|
182
|
+
if (carriedTurns > 1) {
|
|
183
|
+
logCascade({
|
|
184
|
+
phase: "cascade_carry_over_drop",
|
|
185
|
+
filePath: run.filePath,
|
|
186
|
+
neighborCount: run.neighborCount,
|
|
187
|
+
diagnosticCount: run.diagnosticCount,
|
|
188
|
+
metadata: { carriedTurns, turnIndex: this._turnIndex },
|
|
189
|
+
});
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
return [{ ...run, carriedTurns }];
|
|
193
|
+
});
|
|
149
194
|
// _pendingCascadeRuns is deliberately NOT cleared here: a cascade compute
|
|
150
195
|
// still in flight past last turn_end's settle cap (fresh graph builds have
|
|
151
196
|
// measured up to ~19s) must surface on the NEXT turn_end, not be dropped —
|
|
@@ -166,6 +211,25 @@ export class RuntimeCoordinator {
|
|
|
166
211
|
this._turnIndex += 1;
|
|
167
212
|
this._writeIndex = 0;
|
|
168
213
|
this._reportedThisTurn.clear();
|
|
214
|
+
this._writtenThisTurn.clear();
|
|
215
|
+
this._autofixDemotedThisTurn.clear();
|
|
216
|
+
}
|
|
217
|
+
/** Atomically records write/edit ordering before debounce can coalesce it. */
|
|
218
|
+
recordMutationToolReceipt(filePath, toolName) {
|
|
219
|
+
if (toolName === "write") {
|
|
220
|
+
this._writtenThisTurn.set(filePath, true);
|
|
221
|
+
}
|
|
222
|
+
else if (this._writtenThisTurn.has(filePath)) {
|
|
223
|
+
this._autofixDemotedThisTurn.set(filePath, true);
|
|
224
|
+
// A later edit establishes a new final state that must be eligible for
|
|
225
|
+
// the deferred pass even if the preceding write was fixed immediately.
|
|
226
|
+
this._fixedThisTurn.delete(filePath);
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
autofixMode: toolName === "edit" || this._autofixDemotedThisTurn.has(filePath)
|
|
230
|
+
? "deferred"
|
|
231
|
+
: "immediate",
|
|
232
|
+
};
|
|
169
233
|
}
|
|
170
234
|
get reportedThisTurn() {
|
|
171
235
|
return this._reportedThisTurn;
|
|
@@ -192,6 +256,23 @@ export class RuntimeCoordinator {
|
|
|
192
256
|
else if (provider) {
|
|
193
257
|
this._telemetryModel = provider;
|
|
194
258
|
}
|
|
259
|
+
if (model)
|
|
260
|
+
this._telemetryModelId = model;
|
|
261
|
+
if (provider) {
|
|
262
|
+
this._telemetryProvider = provider;
|
|
263
|
+
this._telemetryProviderIsExplicit = true;
|
|
264
|
+
}
|
|
265
|
+
else if (model && !this._telemetryProviderIsExplicit) {
|
|
266
|
+
// No explicit provider has ever been reported this session, so the
|
|
267
|
+
// provider is (still) a derivation — re-derive it from the CURRENT
|
|
268
|
+
// model id every time. Without this, a stale derived provider from
|
|
269
|
+
// an earlier model would survive a mid-session model switch (e.g.
|
|
270
|
+
// gpt-5-mini → claude-sonnet-4-5 with no explicit provider on
|
|
271
|
+
// either call) because the old "has any provider ever been set"
|
|
272
|
+
// guard treated the derived value as sticky. An explicit provider,
|
|
273
|
+
// once set, is never touched here regardless of later model calls.
|
|
274
|
+
this._telemetryProvider = deriveProviderFromModelId(model);
|
|
275
|
+
}
|
|
195
276
|
}
|
|
196
277
|
get telemetrySessionId() {
|
|
197
278
|
return this._telemetrySessionId;
|
|
@@ -220,6 +301,17 @@ export class RuntimeCoordinator {
|
|
|
220
301
|
get telemetryModel() {
|
|
221
302
|
return this._telemetryModel;
|
|
222
303
|
}
|
|
304
|
+
/** Raw model id (never the combined `provider/model` display string), blank
|
|
305
|
+
* when the host hasn't reported one this session. Worklog/disposition
|
|
306
|
+
* attribution (#1448) reads this, not {@link telemetryModel}. */
|
|
307
|
+
get telemetryModelId() {
|
|
308
|
+
return this._telemetryModelId;
|
|
309
|
+
}
|
|
310
|
+
/** Explicit host-reported provider, or a conservative derivation from the
|
|
311
|
+
* model id (see clients/model-provider.ts), blank when neither is known. */
|
|
312
|
+
get telemetryProviderId() {
|
|
313
|
+
return this._telemetryProvider;
|
|
314
|
+
}
|
|
223
315
|
get turnIndex() {
|
|
224
316
|
return this._turnIndex;
|
|
225
317
|
}
|
|
@@ -378,6 +470,26 @@ export class RuntimeCoordinator {
|
|
|
378
470
|
this._cascadeRuns = [];
|
|
379
471
|
return runs;
|
|
380
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* R1 (#1443 follow-up): non-destructive peek used by turn_end's read-only
|
|
475
|
+
* fast path. A carried cascade run (or one still in flight) represents a
|
|
476
|
+
* DELIVERY OPPORTUNITY, not turn activity — an agent that answers a question
|
|
477
|
+
* without editing anything must still get yesterday's late finding. Before
|
|
478
|
+
* this, the files-empty early return skipped `settleCascadeRuns` /
|
|
479
|
+
* `consumeCascadeRuns` entirely on a read-only turn, so `beginTurn`'s next
|
|
480
|
+
* carry pass saw the run as having survived a turn_start with no offsetting
|
|
481
|
+
* drain and dropped it — burning the one-turn carry allowance on a turn that
|
|
482
|
+
* never had a chance to deliver.
|
|
483
|
+
*/
|
|
484
|
+
hasCascadeRuns() {
|
|
485
|
+
// Carried, ALREADY-BUILT runs only. Pending (still-settling) computes are
|
|
486
|
+
// deliberately excluded: a read-only turn that fell through for a pending
|
|
487
|
+
// run would block on the full settle cap — every turn, forever, when the
|
|
488
|
+
// compute never resolves (re-review finding F1). A pending run loses
|
|
489
|
+
// nothing by waiting: settleCascadeRuns re-parks it and the next turn
|
|
490
|
+
// that actually settles it delivers it.
|
|
491
|
+
return this._cascadeRuns.length > 0;
|
|
492
|
+
}
|
|
381
493
|
recordInlineBlockers(filePath, summary) {
|
|
382
494
|
this._pendingInlineBlockers.set(path.resolve(filePath), {
|
|
383
495
|
filePath,
|
|
@@ -462,7 +574,19 @@ export class RuntimeCoordinator {
|
|
|
462
574
|
return this._complexityBaselines;
|
|
463
575
|
}
|
|
464
576
|
get fixedThisTurn() {
|
|
465
|
-
|
|
577
|
+
// Self-referencing local so chained add() returns the same facade
|
|
578
|
+
// instead of re-entering this getter and allocating a new one per call
|
|
579
|
+
// (sonar S7725).
|
|
580
|
+
const facade = {
|
|
581
|
+
add: (filePath) => {
|
|
582
|
+
this._fixedThisTurn.set(filePath, true);
|
|
583
|
+
return facade;
|
|
584
|
+
},
|
|
585
|
+
has: (filePath) => this._fixedThisTurn.has(filePath),
|
|
586
|
+
delete: (filePath) => this._fixedThisTurn.delete(filePath),
|
|
587
|
+
clear: () => this._fixedThisTurn.clear(),
|
|
588
|
+
};
|
|
589
|
+
return facade;
|
|
466
590
|
}
|
|
467
591
|
get projectRulesScan() {
|
|
468
592
|
return this._projectRulesScan;
|
|
@@ -475,39 +599,47 @@ export class RuntimeCoordinator {
|
|
|
475
599
|
return this._readGuard;
|
|
476
600
|
}
|
|
477
601
|
/**
|
|
478
|
-
* Queue `filePath`
|
|
479
|
-
* when this call created a
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
* the same already-queued file before `agent_end` don't spam the bus.
|
|
602
|
+
* Queue one mutation kind for `filePath` at `agent_end`. Returns `true`
|
|
603
|
+
* when this call created a pending entry or added a new kind, and `false`
|
|
604
|
+
* for a same-kind re-touch. Callers publish each kind's first transition
|
|
605
|
+
* without spamming repeated edits before `agent_end`.
|
|
483
606
|
*/
|
|
484
|
-
|
|
607
|
+
deferMutation(filePath, cwd, toolName, turnStateCwd, kind, ownerSessionId) {
|
|
485
608
|
const key = path.resolve(filePath);
|
|
486
609
|
const now = Date.now();
|
|
487
|
-
const existing = this.
|
|
610
|
+
const existing = this._pendingDeferredMutations.get(key);
|
|
488
611
|
if (existing) {
|
|
612
|
+
const addedKind = !existing.kinds.has(kind);
|
|
489
613
|
existing.lastTouchedAt = now;
|
|
490
614
|
existing.cwd = cwd;
|
|
491
615
|
existing.turnStateCwd = turnStateCwd;
|
|
492
616
|
existing.toolNames.add(toolName);
|
|
617
|
+
existing.kinds.add(kind);
|
|
493
618
|
existing.queuedTurnIndex = this._turnIndex;
|
|
494
619
|
existing.ownerSessionId = ownerSessionId;
|
|
495
|
-
return
|
|
620
|
+
return addedKind;
|
|
496
621
|
}
|
|
497
|
-
this.
|
|
622
|
+
this._pendingDeferredMutations.set(key, {
|
|
498
623
|
filePath: key,
|
|
499
624
|
cwd,
|
|
500
625
|
turnStateCwd,
|
|
501
626
|
firstTouchedAt: now,
|
|
502
627
|
lastTouchedAt: now,
|
|
503
628
|
toolNames: new Set([toolName]),
|
|
629
|
+
kinds: new Set([kind]),
|
|
504
630
|
queuedTurnIndex: this._turnIndex,
|
|
505
631
|
ownerSessionId,
|
|
506
632
|
});
|
|
507
633
|
return true;
|
|
508
634
|
}
|
|
635
|
+
deferFormat(filePath, cwd, toolName, turnStateCwd, ownerSessionId) {
|
|
636
|
+
return this.deferMutation(filePath, cwd, toolName, turnStateCwd, "format", ownerSessionId);
|
|
637
|
+
}
|
|
509
638
|
get pendingDeferredFormatCount() {
|
|
510
|
-
return this.
|
|
639
|
+
return this._pendingDeferredMutations.size;
|
|
640
|
+
}
|
|
641
|
+
get pendingDeferredMutationCount() {
|
|
642
|
+
return this._pendingDeferredMutations.size;
|
|
511
643
|
}
|
|
512
644
|
/**
|
|
513
645
|
* Legacy unconditional drain — still exposed for any caller that
|
|
@@ -515,8 +647,8 @@ export class RuntimeCoordinator {
|
|
|
515
647
|
* flush call sites should prefer {@link claimDeferredFormatFiles}.
|
|
516
648
|
*/
|
|
517
649
|
consumeDeferredFormatFiles() {
|
|
518
|
-
const records = [...this.
|
|
519
|
-
this.
|
|
650
|
+
const records = [...this._pendingDeferredMutations.values()];
|
|
651
|
+
this._pendingDeferredMutations.clear();
|
|
520
652
|
return records;
|
|
521
653
|
}
|
|
522
654
|
/**
|
|
@@ -540,19 +672,19 @@ export class RuntimeCoordinator {
|
|
|
540
672
|
const claimed = [];
|
|
541
673
|
const staleClaimed = [];
|
|
542
674
|
const deferredToOwner = [];
|
|
543
|
-
for (const [key, record] of this.
|
|
675
|
+
for (const [key, record] of this._pendingDeferredMutations) {
|
|
544
676
|
const sameSession = record.ownerSessionId === undefined ||
|
|
545
677
|
currentSessionId === undefined ||
|
|
546
678
|
record.ownerSessionId === currentSessionId;
|
|
547
679
|
if (sameSession) {
|
|
548
680
|
claimed.push(record);
|
|
549
|
-
this.
|
|
681
|
+
this._pendingDeferredMutations.delete(key);
|
|
550
682
|
continue;
|
|
551
683
|
}
|
|
552
684
|
const age = now - record.lastTouchedAt;
|
|
553
685
|
if (age > staleAfterMs) {
|
|
554
686
|
staleClaimed.push(record);
|
|
555
|
-
this.
|
|
687
|
+
this._pendingDeferredMutations.delete(key);
|
|
556
688
|
continue;
|
|
557
689
|
}
|
|
558
690
|
deferredToOwner.push(record);
|
|
@@ -563,11 +695,27 @@ export class RuntimeCoordinator {
|
|
|
563
695
|
requeueDeferredFormatFiles(records) {
|
|
564
696
|
for (const record of records) {
|
|
565
697
|
const key = path.resolve(record.filePath);
|
|
566
|
-
|
|
567
|
-
|
|
698
|
+
const existing = this._pendingDeferredMutations.get(key);
|
|
699
|
+
if (existing) {
|
|
700
|
+
for (const kind of record.kinds)
|
|
701
|
+
existing.kinds.add(kind);
|
|
702
|
+
for (const toolName of record.toolNames)
|
|
703
|
+
existing.toolNames.add(toolName);
|
|
704
|
+
continue;
|
|
568
705
|
}
|
|
706
|
+
this._pendingDeferredMutations.set(key, {
|
|
707
|
+
...record,
|
|
708
|
+
kinds: new Set(record.kinds),
|
|
709
|
+
toolNames: new Set(record.toolNames),
|
|
710
|
+
});
|
|
569
711
|
}
|
|
570
712
|
}
|
|
713
|
+
claimDeferredMutations(currentSessionId, now, staleAfterMs) {
|
|
714
|
+
return this.claimDeferredFormatFiles(currentSessionId, now, staleAfterMs);
|
|
715
|
+
}
|
|
716
|
+
requeueDeferredMutations(records) {
|
|
717
|
+
this.requeueDeferredFormatFiles(records);
|
|
718
|
+
}
|
|
571
719
|
shouldWarmLspOnRead(filePath, maxAgeMs = 120_000) {
|
|
572
720
|
const state = this._lspReadWarmState.get(path.resolve(filePath));
|
|
573
721
|
if (!state)
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import * as nodeFs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { createDeadline, yieldIfOverBudget } from "./cooperative-budget.js";
|
|
3
4
|
import { deadCodeIssueCount } from "./dead-code-client.js";
|
|
4
5
|
import { logDeadCodeScan } from "./dead-code-logger.js";
|
|
5
|
-
import { getDiagnosticTracker } from "./diagnostic-tracker.js";
|
|
6
6
|
import { resetDegradationLedger } from "./degradation-ledger.js";
|
|
7
|
+
import { getDiagnosticTracker } from "./diagnostic-tracker.js";
|
|
7
8
|
import { resetDispatchAvailabilityState } from "./dispatch/runners/utils/runner-helpers.js";
|
|
8
9
|
import { clearAllSessions as clearFileTimeSessions } from "./file-time.js";
|
|
9
|
-
import { createDeadline, yieldIfOverBudget } from "./cooperative-budget.js";
|
|
10
10
|
import { getGlobalPiLensDir, getKnipIgnorePatterns, getProjectDataDir, } from "./file-utils.js";
|
|
11
11
|
import { GitleaksClient } from "./gitleaks-client.js";
|
|
12
12
|
import { GovulncheckClient, } from "./govulncheck-client.js";
|
|
13
|
+
import { sweepAtomicWriteStages } from "./instance-reaper.js";
|
|
13
14
|
import { canRunStartupHeavyScans } from "./language-policy.js";
|
|
14
15
|
import { detectProjectLanguageProfile, getDefaultStartupTools, } from "./language-profile.js";
|
|
15
16
|
import { logLatency } from "./latency-logger.js";
|
|
@@ -27,7 +28,6 @@ import { getSlowFsVerdict, isSlowFs, slowFsDegradationNotice, } from "./slow-fs.
|
|
|
27
28
|
import { countRecentSmells, formatSmellsSessionStartLine, resetSmellsSessionState, } from "./smells-rollup.js";
|
|
28
29
|
import { findNearestProjectRoot, getStartupScanMaxEntries, isStartupScanVerdictFresh, resolveStartupScanContext, } from "./startup-scan.js";
|
|
29
30
|
import { getSubagentIdentity, isSubagentSession, subagentLightModeNotice, } from "./subagent-mode.js";
|
|
30
|
-
import { sweepAtomicWriteStages } from "./instance-reaper.js";
|
|
31
31
|
import { TrivyClient } from "./trivy-client.js";
|
|
32
32
|
import { isWarmAttached } from "./warm-attach.js";
|
|
33
33
|
import { setSessionLanguages } from "./widget-state.js";
|
|
@@ -452,10 +452,14 @@ async function buildOrRefreshWordIndex(args) {
|
|
|
452
452
|
let rebuildPreflightFiles;
|
|
453
453
|
const latestSeq = readLatestProjectSequence(snapshotRoot, snapshotSequenceBase(snapshotRoot));
|
|
454
454
|
const effectiveSeq = runtime.projectSeq ?? latestSeq.projectSeq;
|
|
455
|
+
const snapshotLoadStartMs = Date.now();
|
|
455
456
|
const snapshot = loadProjectSnapshot(snapshotRoot);
|
|
457
|
+
const snapshotLoadMs = Date.now() - snapshotLoadStartMs;
|
|
456
458
|
if (snapshot?.wordIndex) {
|
|
457
459
|
const { deserializeWordIndex, refreshWordIndexIncrementally } = await import("./word-index.js");
|
|
460
|
+
const deserializeStartMs = Date.now();
|
|
458
461
|
const index = deserializeWordIndex(snapshot.wordIndex);
|
|
462
|
+
const deserializeMs = Date.now() - deserializeStartMs;
|
|
459
463
|
if (index) {
|
|
460
464
|
try {
|
|
461
465
|
const result = await refreshWordIndexIncrementally(index, analysisRoot, () => runtime.isCurrentSession(sessionGeneration));
|
|
@@ -469,10 +473,16 @@ async function buildOrRefreshWordIndex(args) {
|
|
|
469
473
|
cwd: snapshotRoot,
|
|
470
474
|
trigger: "session_start",
|
|
471
475
|
reason: result.reason,
|
|
476
|
+
phaseDurationsMs: {
|
|
477
|
+
snapshotLoadMs,
|
|
478
|
+
deserializeMs,
|
|
479
|
+
...result.timings,
|
|
480
|
+
},
|
|
472
481
|
});
|
|
473
482
|
}
|
|
474
483
|
else {
|
|
475
484
|
runtime.wordIndex = index;
|
|
485
|
+
let snapshotSaveSyncMs = 0;
|
|
476
486
|
// A stale project seq must be advanced even when mtimes prove every
|
|
477
487
|
// indexed document reusable. Fresh snapshots with no changes avoid
|
|
478
488
|
// an unnecessary rewrite of the large shared snapshot.
|
|
@@ -480,9 +490,15 @@ async function buildOrRefreshWordIndex(args) {
|
|
|
480
490
|
result.refreshed > 0 ||
|
|
481
491
|
result.dropped > 0 ||
|
|
482
492
|
snapshot.wordIndex.truncated !== index.truncated) {
|
|
493
|
+
const serializeSaveEnqueueStartMs = Date.now();
|
|
483
494
|
saveRuntimeProjectSnapshot({ cwd: snapshotRoot, runtime, dbg });
|
|
495
|
+
snapshotSaveSyncMs = Date.now() - serializeSaveEnqueueStartMs;
|
|
484
496
|
}
|
|
485
|
-
|
|
497
|
+
const phases = `snapshot-load=${snapshotLoadMs}ms, deserialize=${deserializeMs}ms, ` +
|
|
498
|
+
`source-walk=${result.timings.sourceWalkMs}ms, stat-walk=${result.timings.statWalkMs}ms, ` +
|
|
499
|
+
`refresh-reads=${result.timings.refreshReadsMs}ms, ` +
|
|
500
|
+
`snapshot-save-sync=${snapshotSaveSyncMs}ms`;
|
|
501
|
+
dbg(`session_start word-index: incremental (seq=${effectiveSeq}, refreshed=${result.refreshed}, dropped=${result.dropped}, skipped=${result.skipped}, reused=${result.reused}, ${Date.now() - startMs}ms, phases: ${phases})`);
|
|
486
502
|
// M2, #958: the incremental-vs-full decision + honest coverage
|
|
487
503
|
// (indexedFileCount/truncated/skipped), independent of host `dbg`.
|
|
488
504
|
logWordIndex({
|
|
@@ -493,6 +509,12 @@ async function buildOrRefreshWordIndex(args) {
|
|
|
493
509
|
indexedFileCount: index.docCount,
|
|
494
510
|
tokens: index.postings.size,
|
|
495
511
|
truncated: index.truncated,
|
|
512
|
+
phaseDurationsMs: {
|
|
513
|
+
snapshotLoadMs,
|
|
514
|
+
deserializeMs,
|
|
515
|
+
...result.timings,
|
|
516
|
+
snapshotSaveSyncMs,
|
|
517
|
+
},
|
|
496
518
|
refreshed: result.refreshed,
|
|
497
519
|
dropped: result.dropped,
|
|
498
520
|
skipped: result.skipped,
|
|
@@ -516,6 +538,7 @@ async function buildOrRefreshWordIndex(args) {
|
|
|
516
538
|
cwd: snapshotRoot,
|
|
517
539
|
trigger: "session_start",
|
|
518
540
|
reason: err instanceof Error ? err.message : String(err),
|
|
541
|
+
phaseDurationsMs: { snapshotLoadMs, deserializeMs },
|
|
519
542
|
});
|
|
520
543
|
}
|
|
521
544
|
}
|
|
@@ -1004,7 +1027,7 @@ function scheduleStartupScans(deps, runtime, sessionGeneration, analysisRoot, sn
|
|
|
1004
1027
|
return;
|
|
1005
1028
|
if (!runtime.callGraph)
|
|
1006
1029
|
return;
|
|
1007
|
-
const { buildCodebaseModel, saveCodebaseModel, DEFAULT_CODEBASE_MODEL_TOKEN_BUDGET } = await import("./codebase-model.js");
|
|
1030
|
+
const { buildCodebaseModel, saveCodebaseModel, DEFAULT_CODEBASE_MODEL_TOKEN_BUDGET, } = await import("./codebase-model.js");
|
|
1008
1031
|
if (!callGraphIdentity)
|
|
1009
1032
|
return;
|
|
1010
1033
|
const model = buildCodebaseModel(runtime.callGraph, analysisRoot, DEFAULT_CODEBASE_MODEL_TOKEN_BUDGET, callGraphIdentity);
|