@herbertgao/pi-extensions 2026.9.6 → 2026.9.7
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 +3 -8
- package/THIRD_PARTY_NOTICES.md +0 -4
- package/node_modules/@narumitw/pi-btw/README.md +2 -0
- package/node_modules/@narumitw/pi-btw/dist/index.ts +284 -365
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +52 -53
- package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +494 -547
- package/node_modules/@narumitw/pi-btw/src/btw.ts +814 -856
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +673 -674
- package/node_modules/@narumitw/pi-btw/src/keybindings.ts +228 -270
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +309 -318
- package/node_modules/@narumitw/pi-btw/src/menu.ts +416 -454
- package/node_modules/@narumitw/pi-btw/src/settings.ts +203 -219
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +173 -201
- package/node_modules/@narumitw/pi-btw/src/text.ts +21 -23
- package/node_modules/@narumitw/pi-btw/src/transcript-markdown.ts +65 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +611 -662
- package/node_modules/pi-lens/CHANGELOG.md +239 -0
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +30 -4
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +2 -0
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +26 -0
- package/node_modules/pi-lens/dist/clients/biome-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +403 -35
- package/node_modules/pi-lens/dist/clients/bootstrap.js +6 -1
- package/node_modules/pi-lens/dist/clients/cascade-format.js +200 -4
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +10 -2
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +16 -1
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +12 -1
- package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +65 -0
- package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +28 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +9 -2
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +222 -0
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +389 -13
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +175 -25
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +62 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +6 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +7 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -12
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +2 -21
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/types.js +4 -1
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +1 -0
- package/node_modules/pi-lens/dist/clients/effective-config.js +1 -1
- package/node_modules/pi-lens/dist/clients/file-kinds.js +26 -1
- package/node_modules/pi-lens/dist/clients/file-role.js +21 -21
- package/node_modules/pi-lens/dist/clients/file-utils.js +156 -10
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +56 -9
- package/node_modules/pi-lens/dist/clients/format-service.js +39 -21
- package/node_modules/pi-lens/dist/clients/formatters.js +60 -13
- package/node_modules/pi-lens/dist/clients/generation-guard.js +2 -2
- package/node_modules/pi-lens/dist/clients/git-guard.js +11 -3
- package/node_modules/pi-lens/dist/clients/indent-retarget.js +78 -8
- package/node_modules/pi-lens/dist/clients/installer/index.js +333 -114
- package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +7 -0
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +20 -10
- package/node_modules/pi-lens/dist/clients/language-profile.js +23 -1
- package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
- package/node_modules/pi-lens/dist/clients/latency-logger.js +4 -1
- package/node_modules/pi-lens/dist/clients/lens-engine.js +12 -3
- package/node_modules/pi-lens/dist/clients/lens-events.js +2 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +79 -0
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/client.js +25 -21
- package/node_modules/pi-lens/dist/clients/lsp/config.js +2 -2
- package/node_modules/pi-lens/dist/clients/lsp/index.js +7 -0
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +16 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +19 -2
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +5 -3
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +12 -8
- package/node_modules/pi-lens/dist/clients/mcp/session.js +36 -4
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +24 -7
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +40 -2
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +23 -3
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +110 -15
- package/node_modules/pi-lens/dist/clients/path-utils.js +123 -3
- package/node_modules/pi-lens/dist/clients/performance-report.js +5 -1
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
- package/node_modules/pi-lens/dist/clients/pipeline.js +74 -5
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +37 -5
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +145 -18
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +59 -3
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +45 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +70 -11
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +147 -9
- package/node_modules/pi-lens/dist/clients/ruff-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +33 -5
- package/node_modules/pi-lens/dist/clients/runtime-context.js +6 -2
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +77 -7
- package/node_modules/pi-lens/dist/clients/runtime-session.js +21 -2
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +43 -12
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +330 -85
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +147 -17
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +258 -5
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +6 -0
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +50 -5
- package/node_modules/pi-lens/dist/clients/sg-runner.js +23 -11
- package/node_modules/pi-lens/dist/clients/sgconfig.js +79 -2
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +6 -41
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +43 -0
- package/node_modules/pi-lens/dist/clients/tool-agreement.js +423 -0
- package/node_modules/pi-lens/dist/clients/tool-config.js +50 -0
- package/node_modules/pi-lens/dist/clients/tool-cwd.js +47 -76
- package/node_modules/pi-lens/dist/clients/tool-policy.js +163 -7
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +120 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +127 -189
- package/node_modules/pi-lens/dist/clients/widget-state.js +98 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +5 -2
- package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +7 -0
- package/node_modules/pi-lens/dist/index.js +27416 -24153
- package/node_modules/pi-lens/dist/mcp/server.js +5 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +7 -1
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +168 -53
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +244 -72
- package/node_modules/pi-lens/docs/audit1.md +5 -5
- package/node_modules/pi-lens/docs/dependencies.md +11 -0
- package/node_modules/pi-lens/docs/dispositions.md +52 -0
- package/node_modules/pi-lens/docs/environment-variables.md +2 -2
- package/node_modules/pi-lens/docs/features.md +4 -4
- package/node_modules/pi-lens/docs/globalconfig.md +3 -1
- package/node_modules/pi-lens/docs/language-coverage.md +1 -0
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +53 -94
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +43 -38
- package/node_modules/pi-lens/docs/pi-lens-retro.md +97 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +53 -66
- package/node_modules/pi-lens/docs/release-qa-baseline.md +22 -0
- package/node_modules/pi-lens/docs/settings.md +7 -4
- package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
- package/node_modules/pi-lens/docs/usage.md +2 -0
- package/node_modules/pi-lens/package.json +1 -1
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +24 -12
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +40 -0
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +15 -0
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +7 -0
- package/package.json +5 -11
- package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +0 -376
- package/node_modules/@luxusai/pi-hindsight/README.md +0 -92
- package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +0 -56
- package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +0 -169
- package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +0 -107
- package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +0 -89
- package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +0 -189
- package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
- package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
- package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +0 -54
- package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +0 -75
- package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +0 -300
- package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +0 -158
- package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +0 -164
- package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +0 -139
- package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +0 -74
- package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +0 -261
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +0 -151
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +0 -18
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +0 -46
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +0 -383
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +0 -240
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +0 -176
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +0 -178
- package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +0 -41
- package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +0 -386
- package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +0 -39
- package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +0 -34
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +0 -140
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +0 -61
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +0 -925
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +0 -247
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +0 -547
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +0 -480
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +0 -182
- package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +0 -86
- package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +0 -1010
- package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +0 -175
- package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +0 -425
- package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +0 -210
- package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +0 -817
- package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +0 -25
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +0 -319
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +0 -196
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +0 -189
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +0 -65
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +0 -260
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +0 -244
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +0 -59
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +0 -82
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +0 -45
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +0 -331
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +0 -354
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +0 -66
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +0 -132
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +0 -228
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +0 -74
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +0 -16
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +0 -648
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +0 -105
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +0 -68
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +0 -71
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +0 -13
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +0 -162
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +0 -131
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +0 -104
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +0 -37
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +0 -893
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +0 -35
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +0 -177
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +0 -7
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +0 -21
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +0 -112
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +0 -111
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +0 -214
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +0 -70
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +0 -392
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +0 -45
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +0 -9
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +0 -860
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +0 -105
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +0 -189
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +0 -299
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +0 -201
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +0 -47
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +0 -239
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +0 -50
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +0 -259
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +0 -77
- package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +0 -534
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +0 -319
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +0 -325
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +0 -42
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +0 -244
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +0 -56
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +0 -50
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +0 -167
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +0 -207
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +0 -120
- package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +0 -8
- package/node_modules/@luxusai/pi-hindsight/package.json +0 -118
- package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +0 -75
|
@@ -39,6 +39,9 @@ import { sweepInlineBlockerPastEof } from "./blocker-past-eof.js";
|
|
|
39
39
|
import { getLSPService } from "./lsp/index.js";
|
|
40
40
|
import { drainPendingAuxCapEvictedCount, drainPendingAuxiliaryCoverage, isPendingAuxiliaryPastRearmTtl, rearmPendingAuxiliaryCoverage, MAX_LATE_AUX_REARMS, pendingAuxiliaryCoverageSize, } from "./lsp/pending-aux-coverage.js";
|
|
41
41
|
import { convertLspDiagnostics } from "./dispatch/utils/lsp-diagnostics.js";
|
|
42
|
+
import { retagAuxiliaryDiagnostics } from "./dispatch/auxiliary-lsp.js";
|
|
43
|
+
import { applyFindingPolicy, loadProjectRulePolicyMap, renderedRuleIdentities, } from "./dispatch/finding-policy.js";
|
|
44
|
+
import { detectFileRole } from "./file-role.js";
|
|
42
45
|
import { drainPendingRunnerFindings, dropStaleRunnerFindings, pendingRunnerFindingsSize, } from "./dispatch/pending-runner-findings.js";
|
|
43
46
|
// #1631 review V2: moved to its own leaf module so a low-level store
|
|
44
47
|
// (widget-state.ts) can use the marker without importing this orchestrator —
|
|
@@ -472,7 +475,15 @@ function capTurnEndMessage(content) {
|
|
|
472
475
|
return out;
|
|
473
476
|
}
|
|
474
477
|
export async function handleTurnEnd(deps) {
|
|
475
|
-
const { ctxCwd, getFlag, dbg, runtime, cacheManager, knipClient, deadCodeClients, depChecker, testRunnerClient, sessionId, owner, resetLSPService, resetFormatService, } = deps;
|
|
478
|
+
const { ctxCwd, getFlag, dbg, runtime, cacheManager, knipClient, deadCodeClients, depChecker, testRunnerClient, sessionId, host = "pi", owner, resetLSPService, resetFormatService, } = deps;
|
|
479
|
+
const turnIndexAtDispatch = runtime.turnIndex;
|
|
480
|
+
const clearOwnedTurnState = () => {
|
|
481
|
+
if (runtime.turnIndex !== turnIndexAtDispatch) {
|
|
482
|
+
dbg(`turn_end: retaining newer turn state (dispatch=${turnIndexAtDispatch}, current=${runtime.turnIndex})`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
cacheManager.clearTurnState(cwd, currentOwner);
|
|
486
|
+
};
|
|
476
487
|
// #449 slice 1: piggyback the instance-registry heartbeat on this existing
|
|
477
488
|
// per-turn touchpoint rather than adding a new timer/interval. Cheap (reads
|
|
478
489
|
// process.memoryUsage().rss, one read-modify-write of instances.json) and
|
|
@@ -527,7 +538,7 @@ export async function handleTurnEnd(deps) {
|
|
|
527
538
|
if (access === "available" &&
|
|
528
539
|
(turnState.files || turnState.owner || turnState.sessionId)) {
|
|
529
540
|
dbg("turn_end: evicting stale turn-state owner");
|
|
530
|
-
|
|
541
|
+
clearOwnedTurnState();
|
|
531
542
|
turnState = cacheManager.readTurnState(cwd);
|
|
532
543
|
}
|
|
533
544
|
const files = Object.keys(turnState.files);
|
|
@@ -640,7 +651,7 @@ export async function handleTurnEnd(deps) {
|
|
|
640
651
|
dbg(`turn_end: ${files.length} file(s) modified, cycles: ${turnState.turnCycles}/${turnState.maxCycles}, access: ${access}, owner: ${gateOwnerLabel}`);
|
|
641
652
|
if (cacheManager.isMaxCyclesExceeded(cwd)) {
|
|
642
653
|
dbg("turn_end: max cycles exceeded, clearing state and forcing through");
|
|
643
|
-
|
|
654
|
+
clearOwnedTurnState();
|
|
644
655
|
runtime.fixedThisTurn.clear();
|
|
645
656
|
resetFormatService();
|
|
646
657
|
return;
|
|
@@ -691,6 +702,9 @@ export async function handleTurnEnd(deps) {
|
|
|
691
702
|
// map — see blocker-freshness.ts's `WidgetSweepBlockerEntry` doc for why this
|
|
692
703
|
// is injected here rather than imported by blocker-freshness.ts itself.
|
|
693
704
|
const blockerFreshness = await sweepInlineBlockerFreshness(runtime, cwd, {
|
|
705
|
+
// #2982: the hook's own signal, so the self axis's filesystem work is
|
|
706
|
+
// bounded by the same abort everything else in this handler honours.
|
|
707
|
+
...(deps.signal === undefined ? {} : { signal: deps.signal }),
|
|
694
708
|
additionalEntries: getWidgetBlockingFilesForSweep().map((row) => ({
|
|
695
709
|
filePath: row.filePath,
|
|
696
710
|
recordedAtMs: row.recordedAtMs,
|
|
@@ -709,6 +723,9 @@ export async function handleTurnEnd(deps) {
|
|
|
709
723
|
revalidated: blockerFreshness.revalidated,
|
|
710
724
|
alreadyStale: blockerFreshness.alreadyStale,
|
|
711
725
|
truncatedImports: blockerFreshness.truncatedImports,
|
|
726
|
+
selfHealed: blockerFreshness.selfHealed,
|
|
727
|
+
selfUnverifiable: blockerFreshness.selfUnverifiable,
|
|
728
|
+
hashBudgetExhausted: blockerFreshness.hashBudgetExhausted,
|
|
712
729
|
},
|
|
713
730
|
});
|
|
714
731
|
// Re-surface inline blockers from this turn that the agent didn't fix.
|
|
@@ -1960,6 +1977,7 @@ export async function handleTurnEnd(deps) {
|
|
|
1960
1977
|
heldDeferred.push({
|
|
1961
1978
|
testFile,
|
|
1962
1979
|
runner: carried.runner,
|
|
1980
|
+
sourceFile: carried.sourceFile ?? testFile,
|
|
1963
1981
|
attempts,
|
|
1964
1982
|
sessionId: turnSessionId,
|
|
1965
1983
|
});
|
|
@@ -1970,6 +1988,7 @@ export async function handleTurnEnd(deps) {
|
|
|
1970
1988
|
redispatchedDeferred++;
|
|
1971
1989
|
targets.push({
|
|
1972
1990
|
testFile,
|
|
1991
|
+
sourceFile: carried.sourceFile ?? testFile,
|
|
1973
1992
|
runner: carried.runner,
|
|
1974
1993
|
config,
|
|
1975
1994
|
strategy: "deferred",
|
|
@@ -2063,7 +2082,7 @@ export async function handleTurnEnd(deps) {
|
|
|
2063
2082
|
overCapTargets++;
|
|
2064
2083
|
continue;
|
|
2065
2084
|
}
|
|
2066
|
-
targets.push(target);
|
|
2085
|
+
targets.push({ ...target, sourceFile: abs });
|
|
2067
2086
|
dbg(`turn_end: ${display} → test ${target.runner} ${path.relative(cwd, target.testFile)} (${target.strategy}${isNeighbor ? ", cascade-neighbor" : ""})`);
|
|
2068
2087
|
}
|
|
2069
2088
|
else if (!target) {
|
|
@@ -2091,6 +2110,9 @@ export async function handleTurnEnd(deps) {
|
|
|
2091
2110
|
dbg(`turn_end: test target count capped at ${TEST_RUNNER_MAX_TARGETS}, ${overCapTargets} skipped`);
|
|
2092
2111
|
}
|
|
2093
2112
|
if (targets.length > 0) {
|
|
2113
|
+
for (const target of targets) {
|
|
2114
|
+
target.fileSeqAtRun = runtime.getFileSeq(target.sourceFile);
|
|
2115
|
+
}
|
|
2094
2116
|
dbg(`turn_end: firing ${targets.length} test target(s) async (non-blocking, max ${TEST_RUNNER_BATCH_CONCURRENCY} concurrent)`);
|
|
2095
2117
|
const firedAtTurn = runtime.turnIndex;
|
|
2096
2118
|
const firedSessionId = turnSessionId;
|
|
@@ -2137,8 +2159,32 @@ export async function handleTurnEnd(deps) {
|
|
|
2137
2159
|
}),
|
|
2138
2160
|
})
|
|
2139
2161
|
.then(({ results, deferred, stopReason }) => {
|
|
2162
|
+
const settledResults = results;
|
|
2163
|
+
const verdicts = settledResults.flatMap((result) => {
|
|
2164
|
+
if (result.status === "rejected")
|
|
2165
|
+
return [];
|
|
2166
|
+
const target = targets.find((candidate) => candidate.testFile === result.value.file);
|
|
2167
|
+
return target
|
|
2168
|
+
? [
|
|
2169
|
+
{
|
|
2170
|
+
file: result.value.file,
|
|
2171
|
+
sourceFile: target.sourceFile,
|
|
2172
|
+
fileSeq: target.fileSeqAtRun === undefined
|
|
2173
|
+
? {
|
|
2174
|
+
state: "unknown",
|
|
2175
|
+
reason: "sequence-unavailable",
|
|
2176
|
+
}
|
|
2177
|
+
: {
|
|
2178
|
+
state: "known",
|
|
2179
|
+
value: target.fileSeqAtRun,
|
|
2180
|
+
},
|
|
2181
|
+
},
|
|
2182
|
+
]
|
|
2183
|
+
: [];
|
|
2184
|
+
});
|
|
2140
2185
|
const deferredTargets = deferred.map((t) => ({
|
|
2141
2186
|
testFile: t.testFile,
|
|
2187
|
+
sourceFile: t.sourceFile,
|
|
2142
2188
|
runner: t.runner,
|
|
2143
2189
|
// One more cut batch for this target. Read back by the
|
|
2144
2190
|
// selection loop above, which retires it at
|
|
@@ -2342,6 +2388,7 @@ export async function handleTurnEnd(deps) {
|
|
|
2342
2388
|
content,
|
|
2343
2389
|
stale,
|
|
2344
2390
|
results: resultValues,
|
|
2391
|
+
verdicts,
|
|
2345
2392
|
testRunGeneration,
|
|
2346
2393
|
launchedFrom,
|
|
2347
2394
|
publishedAgainst,
|
|
@@ -2439,6 +2486,7 @@ export async function handleTurnEnd(deps) {
|
|
|
2439
2486
|
content: deferralNote,
|
|
2440
2487
|
stale,
|
|
2441
2488
|
results: resultValues,
|
|
2489
|
+
verdicts,
|
|
2442
2490
|
testRunGeneration,
|
|
2443
2491
|
launchedFrom,
|
|
2444
2492
|
publishedAgainst,
|
|
@@ -2718,7 +2766,7 @@ export async function handleTurnEnd(deps) {
|
|
|
2718
2766
|
// #2521: `cwd` is what resolves the store location the advisory
|
|
2719
2767
|
// names -- it is the SAME cwd `publishActionableWarningsReport`
|
|
2720
2768
|
// just wrote through, so the two cannot disagree.
|
|
2721
|
-
const advisory = formatActionableWarningsAdvisory(publishResult.report, cwd);
|
|
2769
|
+
const advisory = formatActionableWarningsAdvisory(publishResult.report, cwd, host);
|
|
2722
2770
|
// @delivery-surface: runtime-turn:actionable-warnings-advisory
|
|
2723
2771
|
if (advisory)
|
|
2724
2772
|
advisoryParts.push(advisory);
|
|
@@ -2774,7 +2822,7 @@ export async function handleTurnEnd(deps) {
|
|
|
2774
2822
|
});
|
|
2775
2823
|
writeCodeQualityWarningsReport(cacheManager, cwd, qualityReport);
|
|
2776
2824
|
appendCodeQualityWarningsHistory(cwd, qualityReport);
|
|
2777
|
-
const advisory = formatCodeQualityWarningsAdvisory(qualityReport, cwd);
|
|
2825
|
+
const advisory = formatCodeQualityWarningsAdvisory(qualityReport, cwd, host);
|
|
2778
2826
|
// @delivery-surface: runtime-turn:code-quality-warnings-advisory
|
|
2779
2827
|
if (advisory)
|
|
2780
2828
|
advisoryParts.push(advisory);
|
|
@@ -2900,6 +2948,13 @@ export async function handleTurnEnd(deps) {
|
|
|
2900
2948
|
let lateAuxCeilingExhausted = 0;
|
|
2901
2949
|
let lateAuxAnswered = 0;
|
|
2902
2950
|
let lateAuxNotifyStallDemoted = 0;
|
|
2951
|
+
// #3102: dropped by the shared finding-policy stack (inline `pi-lens-ignore`
|
|
2952
|
+
// / stored disposition / `.pi-lens.json` rule policy) and, separately, by the
|
|
2953
|
+
// auxiliary profile's OWN native suppression inside `retagAuxiliaryDiagnostics`.
|
|
2954
|
+
// Both are reported in the one bounded per-turn record below — a drop is
|
|
2955
|
+
// never silent (shape 10).
|
|
2956
|
+
let lateAuxDispositionSuppressed = 0;
|
|
2957
|
+
let lateAuxAuxSuppressed = 0;
|
|
2903
2958
|
const lateAuxCoverageGapPairs = [];
|
|
2904
2959
|
let lateAuxCoverageGapDetailCount = 0;
|
|
2905
2960
|
let lateAuxCoverageGapDropCount = 0;
|
|
@@ -2947,6 +3002,32 @@ export async function handleTurnEnd(deps) {
|
|
|
2947
3002
|
continue;
|
|
2948
3003
|
}
|
|
2949
3004
|
const displayLateAuxPath = toRunnerDisplayPath(cwd, lateAuxPath);
|
|
3005
|
+
// #3102: the file's CURRENT bytes, for the two content-bound halves
|
|
3006
|
+
// of the policy stack (inline `pi-lens-ignore` and the STRICT
|
|
3007
|
+
// `false-positive` anchor) and for the auxiliary profile's own
|
|
3008
|
+
// native suppression. Read at most ONCE per file per drain, lazily:
|
|
3009
|
+
// a turn that drains nothing, finds no live client, re-arms, or
|
|
3010
|
+
// confirms clean pays no I/O at all, and a file with several pending
|
|
3011
|
+
// servers pays one read for all of them. Measured cost on the one
|
|
3012
|
+
// file that does publish: 0.10-0.20 ms typical, 4.0 ms worst case
|
|
3013
|
+
// (a 180 KB file, 5 findings, a populated disposition store) against
|
|
3014
|
+
// the 3000 ms `HOOK_WALL_BUDGET_MS.turn_end`. A read failure yields
|
|
3015
|
+
// `undefined`: the content-free half still applies and nothing is
|
|
3016
|
+
// hidden on an I/O error (shape 48).
|
|
3017
|
+
let lateAuxContentRead = false;
|
|
3018
|
+
let lateAuxContentValue;
|
|
3019
|
+
const readLateAuxContent = () => {
|
|
3020
|
+
if (!lateAuxContentRead) {
|
|
3021
|
+
lateAuxContentRead = true;
|
|
3022
|
+
try {
|
|
3023
|
+
lateAuxContentValue = fs.readFileSync(lateAuxPath, "utf-8");
|
|
3024
|
+
}
|
|
3025
|
+
catch {
|
|
3026
|
+
lateAuxContentValue = undefined;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
return lateAuxContentValue;
|
|
3030
|
+
};
|
|
2950
3031
|
for (const pair of pairs) {
|
|
2951
3032
|
const cachedEntry = cached.get(pair.serverId);
|
|
2952
3033
|
if (cachedEntry === undefined) {
|
|
@@ -3034,14 +3115,29 @@ export async function handleTurnEnd(deps) {
|
|
|
3034
3115
|
lateAuxCleanConfirmed += 1;
|
|
3035
3116
|
continue;
|
|
3036
3117
|
}
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3118
|
+
// `convertLspDiagnostics` drops entries with no start line, which
|
|
3119
|
+
// would break the 1:1 index alignment `retagAuxiliaryDiagnostics`
|
|
3120
|
+
// needs. Partition first so `converted[i]` IS `anchored[i]` —
|
|
3121
|
+
// the same pre-partition `applyLspFindingPolicy` does.
|
|
3122
|
+
const anchored = rawDiags.filter((d) => d.range?.start?.line !== undefined);
|
|
3123
|
+
if (anchored.length === 0) {
|
|
3041
3124
|
lateAuxMissing += rawDiags.length;
|
|
3042
3125
|
lateAuxAnswered += 1;
|
|
3043
3126
|
continue;
|
|
3044
3127
|
}
|
|
3128
|
+
const lateAuxContent = readLateAuxContent();
|
|
3129
|
+
const converted = convertLspDiagnostics(anchored, lateAuxPath);
|
|
3130
|
+
// #3046/#3047: the auxiliary's REAL tool id (and its own native
|
|
3131
|
+
// inline suppression — opengrep's `# nosemgrep`, ast-grep's
|
|
3132
|
+
// test-file gate), from the ONE shared derivation every other
|
|
3133
|
+
// surface anchors a mark against. These diagnostics come straight
|
|
3134
|
+
// off the aux client's cache, so nothing upstream applied it.
|
|
3135
|
+
const retained = retagAuxiliaryDiagnostics(converted, anchored, lateAuxContent ?? "", { cwd, fileRole: detectFileRole(lateAuxPath, lateAuxContent) });
|
|
3136
|
+
lateAuxAuxSuppressed += converted.length - retained.length;
|
|
3137
|
+
if (retained.length === 0) {
|
|
3138
|
+
lateAuxAnswered += 1;
|
|
3139
|
+
continue;
|
|
3140
|
+
}
|
|
3045
3141
|
// Freshness kernel (#1634 gated surface): stat the cited file
|
|
3046
3142
|
// against the mark timestamp. Missing → drop (no remediation for
|
|
3047
3143
|
// a deleted file); mtime drifted past the mark → drop too, NOT
|
|
@@ -3053,14 +3149,14 @@ export async function handleTurnEnd(deps) {
|
|
|
3053
3149
|
// never silent (shape 10).
|
|
3054
3150
|
const gate = gateFindingsByPathFreshness({
|
|
3055
3151
|
store: "late-auxiliary-findings",
|
|
3056
|
-
findings:
|
|
3152
|
+
findings: retained,
|
|
3057
3153
|
cwd,
|
|
3058
3154
|
scannedAt: pair.markedAtMs,
|
|
3059
3155
|
citedPath: () => lateAuxPath,
|
|
3060
3156
|
});
|
|
3061
3157
|
lateAuxStale += gate.stale.length;
|
|
3062
3158
|
lateAuxMissing +=
|
|
3063
|
-
|
|
3159
|
+
retained.length - gate.live.length - gate.stale.length;
|
|
3064
3160
|
if (gate.live.length === 0) {
|
|
3065
3161
|
if (gate.stale.length > 0) {
|
|
3066
3162
|
// Stale findings mean the scan predates the last edit. Re-arm
|
|
@@ -3092,11 +3188,43 @@ export async function handleTurnEnd(deps) {
|
|
|
3092
3188
|
// merged one). Late findings reach the agent as the gated advisory
|
|
3093
3189
|
// below; the turn-end hash-guarded fast path stays cold for a file
|
|
3094
3190
|
// whose touch was partial, which is exactly what #1470 requires.
|
|
3095
|
-
|
|
3096
|
-
|
|
3191
|
+
// #3102: the survivors are what the agent READS, so they take the
|
|
3192
|
+
// same `clients/dispatch/finding-policy.ts` stack — inline
|
|
3193
|
+
// `pi-lens-ignore` → stored dispositions → `.pi-lens.json` rule
|
|
3194
|
+
// policy — the per-edit dispatcher, `mode=full` and the
|
|
3195
|
+
// `source=lsp` probe lane apply. Without it a finding the agent
|
|
3196
|
+
// marked `false-positive` re-reported on every turn that drained
|
|
3197
|
+
// a late pair. Runs AFTER the freshness gate, like the #1625
|
|
3198
|
+
// govulncheck/secrets filters: the anchor is derived from the
|
|
3199
|
+
// post-gate identity, never the raw pre-gate set.
|
|
3200
|
+
const { kept: lateAuxKept } = applyFindingPolicy(gate.live, {
|
|
3201
|
+
cwd,
|
|
3202
|
+
filePath: lateAuxPath,
|
|
3203
|
+
content: lateAuxContent ?? "",
|
|
3204
|
+
// mtime-cached, so a drain of several files costs one stat.
|
|
3205
|
+
policyMap: loadProjectRulePolicyMap(cwd),
|
|
3206
|
+
identities: renderedRuleIdentities,
|
|
3207
|
+
});
|
|
3208
|
+
const lateAuxSuppressedHere = gate.live.length - lateAuxKept.length;
|
|
3209
|
+
lateAuxDispositionSuppressed += lateAuxSuppressedHere;
|
|
3210
|
+
if (lateAuxKept.length === 0) {
|
|
3211
|
+
// Every late finding was suppressed. This is a PUSH surface:
|
|
3212
|
+
// silence after a mark is the mark working, not a clean
|
|
3213
|
+
// verdict, so the count rides the bounded per-turn record below
|
|
3214
|
+
// instead of re-announcing the suppression every single turn.
|
|
3215
|
+
lateAuxAnswered += 1;
|
|
3216
|
+
continue;
|
|
3217
|
+
}
|
|
3218
|
+
const lines = lateAuxKept.map((f) => ` ${displayLateAuxPath}:${f.line}:${f.column} [${f.rule}] ${f.message}`);
|
|
3219
|
+
lateAuxDelivered += lateAuxKept.length;
|
|
3097
3220
|
lateAuxAnswered += 1;
|
|
3221
|
+
// #1616 suppressed-bucket rule: a delivery that still has
|
|
3222
|
+
// something to say states what it dropped, once per delivery.
|
|
3223
|
+
const lateAuxSuppressedNote = lateAuxSuppressedHere > 0
|
|
3224
|
+
? `; suppressed by disposition: ${lateAuxSuppressedHere} finding(s)`
|
|
3225
|
+
: "";
|
|
3098
3226
|
// @delivery-surface: runtime-turn:late-auxiliary-findings
|
|
3099
|
-
advisoryParts.push(`🕐 Late auxiliary diagnostics (${pair.serverId} answered after its grace window):\n${lines.join("\n")}`);
|
|
3227
|
+
advisoryParts.push(`🕐 Late auxiliary diagnostics (${pair.serverId} answered after its grace window${lateAuxSuppressedNote}):\n${lines.join("\n")}`);
|
|
3100
3228
|
}
|
|
3101
3229
|
}
|
|
3102
3230
|
}
|
|
@@ -3150,6 +3278,8 @@ export async function handleTurnEnd(deps) {
|
|
|
3150
3278
|
expired: lateAuxExpired,
|
|
3151
3279
|
ceilingExhausted: lateAuxCeilingExhausted,
|
|
3152
3280
|
answered: lateAuxAnswered,
|
|
3281
|
+
dispositionSuppressed: lateAuxDispositionSuppressed,
|
|
3282
|
+
auxSuppressed: lateAuxAuxSuppressed,
|
|
3153
3283
|
notifyStallDemoted: lateAuxNotifyStallDemoted,
|
|
3154
3284
|
coverageGapReRaised: lateAuxCoverageGapPairs.length,
|
|
3155
3285
|
coverageGapReRaisedDetailed: lateAuxCoverageGapDetailCount,
|
|
@@ -3202,7 +3332,7 @@ export async function handleTurnEnd(deps) {
|
|
|
3202
3332
|
});
|
|
3203
3333
|
}
|
|
3204
3334
|
}
|
|
3205
|
-
|
|
3335
|
+
clearOwnedTurnState();
|
|
3206
3336
|
runtime.fixedThisTurn.clear();
|
|
3207
3337
|
resetFormatService();
|
|
3208
3338
|
return;
|
|
@@ -3289,7 +3419,7 @@ export async function handleTurnEnd(deps) {
|
|
|
3289
3419
|
});
|
|
3290
3420
|
}
|
|
3291
3421
|
if (blockerParts.length === 0) {
|
|
3292
|
-
|
|
3422
|
+
clearOwnedTurnState();
|
|
3293
3423
|
// `staleSecretParts` counts here too (#1622 review M2): clearing the
|
|
3294
3424
|
// findings record while a stale secret is still unverified would drop the
|
|
3295
3425
|
// only surviving trace of it.
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
import { spawn, spawnSync, } from "node:child_process";
|
|
17
17
|
import * as fs from "node:fs";
|
|
18
18
|
import * as path from "node:path";
|
|
19
|
-
import { BoundedFifoMap } from "./bounded-cache.js";
|
|
19
|
+
import { BoundedFifoMap, BoundedSet } from "./bounded-cache.js";
|
|
20
20
|
import { logLatency } from "./latency-logger.js";
|
|
21
|
-
import { recordDegradation } from "./degradation-ledger.js";
|
|
21
|
+
import { incrementDegradationCount, recordDegradation, recordDegradationOnce, } from "./degradation-ledger.js";
|
|
22
22
|
import { logExtension } from "./extension-log.js";
|
|
23
23
|
import { isFullyQualifiedWin32 } from "./path-utils.js";
|
|
24
24
|
import { startSpawnUsageSampler } from "./resource-sampler.js";
|
|
@@ -212,6 +212,228 @@ const lifetimeState = processWithLifetimeState[lifetimeStateKey] ??
|
|
|
212
212
|
pids: new Set(),
|
|
213
213
|
installed: false,
|
|
214
214
|
});
|
|
215
|
+
/**
|
|
216
|
+
* THE ownership predicate for every kill-by-raw-pid in this repo (#2042).
|
|
217
|
+
*
|
|
218
|
+
* A pid is signalable only when it is a LIVE DIRECT CHILD of this process.
|
|
219
|
+
* `tests/clients/lsp/launch.test.ts` mocked `node:child_process` with a fake
|
|
220
|
+
* child carrying the literal pid `2468`; `safeSpawnAsync` registered it in
|
|
221
|
+
* `lifetimeState.pids`, the fake never emitted `close` so nothing removed it,
|
|
222
|
+
* and at fork teardown `installLifetimeCleanup()` fired
|
|
223
|
+
* `process.kill(-2468, "SIGKILL")`. On ~10 % of GitHub runners pid 2468 was
|
|
224
|
+
* one of the job's own long-lived processes — that is the whole of #2042:
|
|
225
|
+
* exit 137, no failing assertion, no kernel record, five weeks of "infra
|
|
226
|
+
* kill" reruns. A `pid <= 0` guard would not have stopped it; 2468 is a
|
|
227
|
+
* perfectly plausible pid. The defect is OWNERSHIP, not sign.
|
|
228
|
+
*
|
|
229
|
+
* Three guards fold into this one (net-count rule): `lsp/client.ts`'s
|
|
230
|
+
* `pid <= 0`, `lsp/launch.ts`'s `handle.pid <= 0` plus its already-exited
|
|
231
|
+
* recycled-pid check, and `killTree`'s `child.pid > 0` below. It closes
|
|
232
|
+
* fabricated, stale and recycled pids together.
|
|
233
|
+
*
|
|
234
|
+
* NOT folded: `instance-reaper.ts#killPidTree`. Its pids come from OTHER
|
|
235
|
+
* pi-lens instances' registries and from an OS scan of managed binaries with
|
|
236
|
+
* confirmed-dead parents — deliberately NOT our children. Its `!isFinite ||
|
|
237
|
+
* pid <= 0` guard is well-formedness on a different axis, and applying this
|
|
238
|
+
* predicate there would turn the orphan reaper into a no-op.
|
|
239
|
+
*
|
|
240
|
+
* Platform behaviour, stated rather than implied:
|
|
241
|
+
* - Linux (the authoritative lane, and the one that lost five weeks):
|
|
242
|
+
* `/proc/<pid>/status` is the kernel's own answer. Missing entry = the pid
|
|
243
|
+
* does not exist, so there is nothing of ours to kill; `PPid` other than
|
|
244
|
+
* ours = someone else's process, which is recorded and never signalled.
|
|
245
|
+
* - Windows and macOS have no `/proc` (and neither does a Linux host with
|
|
246
|
+
* `/proc` unmounted — probed once, not assumed from `process.platform`),
|
|
247
|
+
* so ownership is unverifiable from a raw pid and the predicate keeps
|
|
248
|
+
* today's best-effort behaviour. The `handle` arm applies THERE and only
|
|
249
|
+
* there: it is the only ownership evidence Windows has, and it is what
|
|
250
|
+
* `killWindowsTree` already used. It must not gate the POSIX group kill,
|
|
251
|
+
* which legitimately runs after its leader has died (#3091 F1).
|
|
252
|
+
*/
|
|
253
|
+
/**
|
|
254
|
+
* Whether `/proc/<pid>/status` can actually be read here, probed ONCE against
|
|
255
|
+
* this process's own entry (#3091 F4). `process.platform === "linux"` is not
|
|
256
|
+
* the same question: a container or a hardened host can run Linux with no
|
|
257
|
+
* `/proc` mounted, and there the per-pid read fails exactly the way a dead pid
|
|
258
|
+
* does. Without this probe that silently refused every registration, so
|
|
259
|
+
* `lifetimeState.pids` stayed empty and the host-exit tree kill stopped working
|
|
260
|
+
* with no record anywhere.
|
|
261
|
+
*/
|
|
262
|
+
const PROC_PPID_READABLE = (() => {
|
|
263
|
+
if (process.platform !== "linux")
|
|
264
|
+
return false;
|
|
265
|
+
try {
|
|
266
|
+
return /^PPid:\s*\d+$/m.test(fs.readFileSync("/proc/self/status", "utf8"));
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
})();
|
|
272
|
+
/**
|
|
273
|
+
* Pids this process has PROVEN, from the kernel, to be its own live children.
|
|
274
|
+
* FIFO-bounded (`BoundedFifoMap`, not a wholesale clear): a process that spawns
|
|
275
|
+
* more than the cap drops its OLDEST verdicts, where a wholesale clear would
|
|
276
|
+
* drop the one it is about to need.
|
|
277
|
+
*/
|
|
278
|
+
export const VERIFIED_OWN_PID_CAP = 512;
|
|
279
|
+
const verifiedOwnPids = new BoundedFifoMap(VERIFIED_OWN_PID_CAP);
|
|
280
|
+
/**
|
|
281
|
+
* Pids whose OWNERSHIP IS HELD for the lifetime of a long-lived resource —
|
|
282
|
+
* today, the LSP children `clients/lsp/launch.ts` spawns with `nodeSpawn`
|
|
283
|
+
* (#3091 F1-r2b).
|
|
284
|
+
*
|
|
285
|
+
* These deliberately do NOT live in `verifiedOwnPids`. Age is the wrong
|
|
286
|
+
* retirement axis for a resource-scoped verdict: an LSP leader is verified once
|
|
287
|
+
* at spawn and then sits untouched for the whole session, so in a FIFO that
|
|
288
|
+
* every `safeSpawnAsync` writes to it is the OLDEST entry and therefore the
|
|
289
|
+
* FIRST evicted — measured, 520 later verdicts were enough — and its group kill
|
|
290
|
+
* at host exit is refused precisely because the server was long-lived enough to
|
|
291
|
+
* matter. Retirement here is by RESOURCE STATE instead: `releaseOwnChildPid`
|
|
292
|
+
* when the shutdown ladder is done with the pid, plus the confirmed-dead sweep
|
|
293
|
+
* below for the paths that never reach a ladder (a server that crashes, or a
|
|
294
|
+
* host killed mid-session).
|
|
295
|
+
*
|
|
296
|
+
* The bound that remains, stated rather than implied: at most
|
|
297
|
+
* `HELD_OWN_PID_SWEEP_AT` entries — the confirmed-dead sweep below runs before
|
|
298
|
+
* every insert, and `BoundedSet`'s own FIFO overflow is the last resort for the
|
|
299
|
+
* case where every held group really is still alive. A crash-looping server cannot grow it without bound
|
|
300
|
+
* because each sweep drops every pid whose process GROUP no longer exists,
|
|
301
|
+
* which is exactly the condition under which the entry can never be needed
|
|
302
|
+
* again.
|
|
303
|
+
*/
|
|
304
|
+
const HELD_OWN_PID_SWEEP_AT = 256;
|
|
305
|
+
const heldOwnPids = new BoundedSet(HELD_OWN_PID_SWEEP_AT);
|
|
306
|
+
/** A process group that no longer exists can never need another signal. */
|
|
307
|
+
function groupIsGone(pid) {
|
|
308
|
+
try {
|
|
309
|
+
process.kill(-pid, 0);
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
return errorCode(error) === "ESRCH";
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function sweepHeldOwnPids() {
|
|
317
|
+
if (heldOwnPids.size < HELD_OWN_PID_SWEEP_AT)
|
|
318
|
+
return;
|
|
319
|
+
// Deleting the current entry mid-iteration is well-defined for a Set, so
|
|
320
|
+
// no snapshot copy is needed.
|
|
321
|
+
for (const pid of heldOwnPids) {
|
|
322
|
+
if (groupIsGone(pid))
|
|
323
|
+
heldOwnPids.delete(pid);
|
|
324
|
+
}
|
|
325
|
+
// No hand-rolled "drop the oldest" block (#2442 / #3091 round 4): the
|
|
326
|
+
// last-resort eviction, for the case where every held group is still alive
|
|
327
|
+
// at the ceiling, is `BoundedSet.add`'s own FIFO overflow.
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Verify a pid at ADMISSION and hold the verdict for the resource's lifetime.
|
|
331
|
+
*
|
|
332
|
+
* This is AGENTS.md shape 50's own rule, which #3091 round 1 broke at the site
|
|
333
|
+
* it was fixing: `isOwnLiveChild`'s three call sites were the `safeSpawnAsync`
|
|
334
|
+
* registration, the Windows tree kill, and the SHUTDOWN group kill — so for an
|
|
335
|
+
* LSP child, spawned with `nodeSpawn`, the first offer of verification was also
|
|
336
|
+
* the last, and on the `processExiting` path the leader could already be dead
|
|
337
|
+
* by then (`/proc` gone, nothing on record, signal refused, grandchildren
|
|
338
|
+
* orphaned).
|
|
339
|
+
*/
|
|
340
|
+
export function holdOwnChildPid(pid, site) {
|
|
341
|
+
const owned = isOwnLiveChild(pid, site);
|
|
342
|
+
if (!owned || !PROC_PPID_READABLE || typeof pid !== "number")
|
|
343
|
+
return owned;
|
|
344
|
+
// Exactly one home: the verification above filed the verdict in the
|
|
345
|
+
// age-bounded FIFO, and leaving a copy there would make BOTH the held set
|
|
346
|
+
// and its release mutation-inert — the FIFO copy would answer for them.
|
|
347
|
+
verifiedOwnPids.delete(pid);
|
|
348
|
+
sweepHeldOwnPids();
|
|
349
|
+
heldOwnPids.add(pid);
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Retire a held pid once nothing will signal it again — the end of the
|
|
354
|
+
* shutdown ladder. Idempotent.
|
|
355
|
+
*/
|
|
356
|
+
export function releaseOwnChildPid(pid) {
|
|
357
|
+
if (typeof pid === "number")
|
|
358
|
+
heldOwnPids.delete(pid);
|
|
359
|
+
}
|
|
360
|
+
/** Once per session: a Linux host whose `/proc` cannot answer the question. */
|
|
361
|
+
function recordProcUnavailable(site) {
|
|
362
|
+
if (process.platform !== "linux")
|
|
363
|
+
return;
|
|
364
|
+
recordDegradationOnce({
|
|
365
|
+
kind: "kill-ownership-unverifiable",
|
|
366
|
+
subject: site,
|
|
367
|
+
reason: "/proc/self/status is unreadable on this Linux host; kill-by-pid ownership falls back to best-effort",
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
export function isOwnLiveChild(pid, site, handle) {
|
|
371
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
372
|
+
return false;
|
|
373
|
+
if (!PROC_PPID_READABLE) {
|
|
374
|
+
// No `/proc` to consult. A handle that has already reported exit is then
|
|
375
|
+
// the ONLY ownership evidence available, and it is exactly what
|
|
376
|
+
// `killWindowsTree` used before this predicate existed: a dead pid may
|
|
377
|
+
// have been recycled, and `taskkill /F /T` on a recycled pid destroys an
|
|
378
|
+
// unrelated tree. It overrides the memo below deliberately.
|
|
379
|
+
if (handle && (handle.exitCode != null || handle.signalCode != null))
|
|
380
|
+
return false;
|
|
381
|
+
recordProcUnavailable(site);
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
let status;
|
|
385
|
+
try {
|
|
386
|
+
status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// The pid does not exist. Not a degradation — a child that exited before
|
|
390
|
+
// the teardown signal is the ordinary case — but not automatically a
|
|
391
|
+
// refusal either: a POSIX process GROUP outlives its leader, and #2026's
|
|
392
|
+
// whole point is that the escalation must still reach a SIGTERM-hardy
|
|
393
|
+
// grandchild after the direct child has died. Ownership verified while
|
|
394
|
+
// the leader was alive is what answers that; a pid this process never
|
|
395
|
+
// owned is still refused.
|
|
396
|
+
return heldOwnPids.has(pid) || verifiedOwnPids.has(pid);
|
|
397
|
+
}
|
|
398
|
+
const parent = /^PPid:\s*(\d+)$/m.exec(status);
|
|
399
|
+
if (!parent)
|
|
400
|
+
return true;
|
|
401
|
+
if (Number(parent[1]) === process.pid) {
|
|
402
|
+
// Memoized only on a KERNEL-verified verdict, and only here: a pid we
|
|
403
|
+
// never proved was ours can never enter the memo, and a pid that is
|
|
404
|
+
// alive under a different parent is refused above regardless of what the
|
|
405
|
+
// memo holds — so a recycled pid cannot be signalled on the strength of
|
|
406
|
+
// its previous owner.
|
|
407
|
+
//
|
|
408
|
+
// Never for a pid already HELD, though: the shutdown ladder verifies a
|
|
409
|
+
// still-live leader on its way to the group signal, and filing that
|
|
410
|
+
// verdict in the FIFO too would put a held pid back in both stores —
|
|
411
|
+
// which outlives `releaseOwnChildPid` (the FIFO copy would keep
|
|
412
|
+
// answering for a pid this process has explicitly stopped claiming) and
|
|
413
|
+
// makes the held store's own tests unable to fail.
|
|
414
|
+
if (!heldOwnPids.has(pid))
|
|
415
|
+
verifiedOwnPids.set(pid, true);
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
// Alive AND someone else's: the dangerous case, and the only one worth a
|
|
419
|
+
// record. Subject is the SITE (a fixed, tiny set), never the pid, so the
|
|
420
|
+
// ledger stays bounded however often it fires.
|
|
421
|
+
incrementDegradationCount({
|
|
422
|
+
kind: "kill-foreign-pid-refused",
|
|
423
|
+
subject: site,
|
|
424
|
+
reason: `pid ${pid} has parent ${parent[1]}, not this process — signal refused`,
|
|
425
|
+
});
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Kill one registered pid's tree at host exit. Deliberately NOT re-checked
|
|
430
|
+
* against `isOwnLiveChild` here: `lifetimeState.pids` has exactly one writer
|
|
431
|
+
* (the registration below), that writer is now gated, and a second gate on
|
|
432
|
+
* the same feeder would make BOTH mutation-inert — remove either one and the
|
|
433
|
+
* other still blocks the kill, so neither would have a test that reds. The
|
|
434
|
+
* gate lives where the pid enters, which is also where ownership is a fact
|
|
435
|
+
* rather than a stale reading (#2042).
|
|
436
|
+
*/
|
|
215
437
|
function killPidTreeSync(pid) {
|
|
216
438
|
if (process.platform === "win32") {
|
|
217
439
|
try {
|
|
@@ -836,6 +1058,17 @@ export function resetUtf8ConsoleCodePageStateForTests() {
|
|
|
836
1058
|
// caller's overall budget unboundedly.
|
|
837
1059
|
const EXIT_PIPE_IDLE_GRACE_MS = 100;
|
|
838
1060
|
const EXIT_PIPE_IDLE_MAX_WAIT_MS = 2000;
|
|
1061
|
+
/**
|
|
1062
|
+
* #2968: how long past its own deadline this spawn's CPU/RSS sampler may keep
|
|
1063
|
+
* polling. Everything this function does to end a child after the deadline
|
|
1064
|
+
* fits inside it: `killTree`'s SIGTERM plus its 1000ms SIGKILL escalation (or
|
|
1065
|
+
* Windows' `taskkill /F /T`), then at most EXIT_PIPE_IDLE_MAX_WAIT_MS of
|
|
1066
|
+
* pipe-idle wait. A child still alive past deadline + this grace has outlived
|
|
1067
|
+
* every teardown available here, so polling it further only buys the Windows
|
|
1068
|
+
* process pile-up the report measured — and `stop()` still returns whatever
|
|
1069
|
+
* the sampler gathered before the cap.
|
|
1070
|
+
*/
|
|
1071
|
+
const SPAWN_SAMPLER_TEARDOWN_GRACE_MS = 5_000;
|
|
839
1072
|
// ============================================================================
|
|
840
1073
|
// ASYNC VERSION (Recommended - Non-blocking)
|
|
841
1074
|
// ============================================================================
|
|
@@ -1225,7 +1458,14 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1225
1458
|
child.stdin?.on("error", () => { });
|
|
1226
1459
|
child.stdin?.end(options.input);
|
|
1227
1460
|
}
|
|
1228
|
-
|
|
1461
|
+
// #2042: ownership is a SPAWN-TIME fact — a real child is alive with
|
|
1462
|
+
// `PPid == ours` the instant `spawn` returns, while a test double's
|
|
1463
|
+
// invented pid never is. Resolve it once, here, and reuse the verdict:
|
|
1464
|
+
// the group escalation below must still fire after the direct child
|
|
1465
|
+
// dies (#2027: a SIGTERM-hardy grandchild keeps the group alive), so it
|
|
1466
|
+
// cannot re-read `/proc` at kill time.
|
|
1467
|
+
const ownsChildPid = isOwnLiveChild(child.pid, "safe-spawn-register");
|
|
1468
|
+
if (ownsChildPid && (posixProcessGroup || options?.lifetimeCoupled)) {
|
|
1229
1469
|
// #2026: POSIX registers unconditionally - detached children no
|
|
1230
1470
|
// longer receive terminal signals directly, so the lifetime
|
|
1231
1471
|
// cleanup's signal forwarding IS their die-with-host path.
|
|
@@ -1240,9 +1480,22 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1240
1480
|
// best-effort/never-throws by design, but this call site wraps it anyway
|
|
1241
1481
|
// (belt and suspenders: the sampling seam must never be the reason a real
|
|
1242
1482
|
// spawn fails) with a no-op fallback sampler.
|
|
1483
|
+
//
|
|
1484
|
+
// #2968: the sampler's polling lifetime is bounded by THIS spawn's own
|
|
1485
|
+
// deadline plus the teardown grace, not by the child's willingness to
|
|
1486
|
+
// exit. `finalize`/the `error` handler still stop it on every settle
|
|
1487
|
+
// path, but none of those paths exist for a child that never settles —
|
|
1488
|
+
// a hung `.cmd` shim whose tree survives `taskkill /F /T` kept polling
|
|
1489
|
+
// for 5-6 hours in the report.
|
|
1243
1490
|
let usageSampler;
|
|
1244
1491
|
try {
|
|
1245
|
-
usageSampler = startSpawnUsageSampler(
|
|
1492
|
+
usageSampler = startSpawnUsageSampler(
|
|
1493
|
+
// The poll CADENCE is the sampler's own policy (it owns the backoff
|
|
1494
|
+
// too); the DEADLINE is this function's. Passing `undefined` rather
|
|
1495
|
+
// than importing the cadence constant keeps this module's view of
|
|
1496
|
+
// the sampler at one export, which is also what every existing
|
|
1497
|
+
// `vi.mock` of it provides.
|
|
1498
|
+
child.pid, undefined, timeout + SPAWN_SAMPLER_TEARDOWN_GRACE_MS);
|
|
1246
1499
|
}
|
|
1247
1500
|
catch {
|
|
1248
1501
|
usageSampler = { stop: () => null };
|
|
@@ -1272,7 +1525,7 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
1272
1525
|
child.kill("SIGKILL");
|
|
1273
1526
|
}
|
|
1274
1527
|
}
|
|
1275
|
-
else if (posixProcessGroup &&
|
|
1528
|
+
else if (posixProcessGroup && ownsChildPid) {
|
|
1276
1529
|
// #2026: signal the whole process group. Grandchildren spawned
|
|
1277
1530
|
// by the tool share its group, so one signal reaches the whole
|
|
1278
1531
|
// tree; a negative-pid ESRCH means it already exited.
|
|
@@ -91,6 +91,12 @@ export function getScratchTreeFnmatchPatterns() {
|
|
|
91
91
|
export const SECRETS_LANE_SCRATCH_DIR_NAMES = [
|
|
92
92
|
// pi-ecosystem / coding-agent data + cache directories.
|
|
93
93
|
".pi",
|
|
94
|
+
// The same agent's rebranded config dir (#3112) — pi's config-dir name is
|
|
95
|
+
// `pkg.piConfig?.configDir || ".pi"`, so `.omp/agent/sessions` is the very
|
|
96
|
+
// history `.pi/agent/sessions` holds. Without it the secrets lane READS a
|
|
97
|
+
// project-local session history and can report a key a user pasted into a
|
|
98
|
+
// transcript as a blocking first-party leak.
|
|
99
|
+
".omp",
|
|
94
100
|
".pi-lens",
|
|
95
101
|
".claude",
|
|
96
102
|
".codex",
|