pi-lens 3.8.73 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2614 -12
- package/README.md +50 -21
- package/dist/clients/actionable-warnings.js +73 -18
- package/dist/clients/agent-behavior-client.js +21 -4
- package/dist/clients/ast-grep-client.js +68 -16
- package/dist/clients/ast-grep-types.js +0 -1
- package/dist/clients/ast-grep-yaml-synth.js +37 -10
- package/dist/clients/atomic-write-staging.js +63 -0
- package/dist/clients/atomic-write.js +116 -16
- package/dist/clients/bash-file-access.js +193 -23
- package/dist/clients/biome-client.js +29 -25
- package/dist/clients/bootstrap.js +11 -2
- package/dist/clients/bounded-cache.js +34 -0
- package/dist/clients/bounded-pid-file-lock.js +192 -0
- package/dist/clients/bus-events-logger.js +5 -1
- package/dist/clients/bus-publish.js +10 -3
- package/dist/clients/cache/rule-cache.js +89 -6
- package/dist/clients/cache-manager.js +97 -9
- package/dist/clients/cache-observability.js +498 -0
- package/dist/clients/call-graph.js +460 -108
- package/dist/clients/cascade-logger.js +5 -1
- package/dist/clients/child-unref.js +85 -0
- package/dist/clients/codebase-model.js +52 -23
- package/dist/clients/collateral-test-role.js +40 -0
- package/dist/clients/complexity-client.js +6 -8
- package/dist/clients/console-guard-install.js +14 -0
- package/dist/clients/cooperative-budget.js +42 -0
- package/dist/clients/dead-code-client.js +2 -1
- package/dist/clients/debug-handles.js +206 -0
- package/dist/clients/debug-heap.js +167 -0
- package/dist/clients/degradation-ledger.js +153 -0
- package/dist/clients/dependency-checker.js +466 -63
- package/dist/clients/diagnostic-dispositions.js +83 -13
- package/dist/clients/diagnostics-publish.js +10 -3
- package/dist/clients/dispatch/auxiliary-lsp.js +34 -0
- package/dist/clients/dispatch/dispatcher.js +71 -13
- package/dist/clients/dispatch/fact-store.js +11 -0
- package/dist/clients/dispatch/facts/function-facts.js +37 -7
- package/dist/clients/dispatch/facts/import-facts.js +108 -119
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +12 -12
- package/dist/clients/dispatch/facts/try-catch-facts.js +13 -0
- package/dist/clients/dispatch/indent-detect.js +35 -0
- package/dist/clients/dispatch/inline-suppressions.js +17 -8
- package/dist/clients/dispatch/integration.js +1094 -611
- package/dist/clients/dispatch/lazy.js +14 -0
- package/dist/clients/dispatch/plan.js +11 -1
- package/dist/clients/dispatch/rule-id-normalize.js +50 -0
- package/dist/clients/dispatch/rule-policy.js +135 -0
- package/dist/clients/dispatch/rules/async-noise.js +1 -0
- package/dist/clients/dispatch/rules/unsafe-boundary.js +7 -0
- package/dist/clients/dispatch/runners/ast-grep-napi.js +47 -6
- package/dist/clients/dispatch/runners/biome-check.js +5 -9
- package/dist/clients/dispatch/runners/detekt.js +3 -2
- package/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/dist/clients/dispatch/runners/helm-lint.js +154 -0
- package/dist/clients/dispatch/runners/htmlhint.js +3 -2
- package/dist/clients/dispatch/runners/index.js +5 -1
- package/dist/clients/dispatch/runners/lsp.js +14 -6
- package/dist/clients/dispatch/runners/markdownlint.js +4 -9
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -1
- package/dist/clients/dispatch/runners/pyright.js +9 -5
- package/dist/clients/dispatch/runners/ruff.js +4 -8
- package/dist/clients/dispatch/runners/shellcheck.js +2 -3
- package/dist/clients/dispatch/runners/shfmt.js +2 -3
- package/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/dist/clients/dispatch/runners/taplo.js +2 -1
- package/dist/clients/dispatch/runners/terragrunt.js +163 -0
- package/dist/clients/dispatch/runners/tflint.js +37 -5
- package/dist/clients/dispatch/runners/tree-sitter.js +155 -166
- package/dist/clients/dispatch/runners/trivy-config.js +13 -7
- package/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +284 -34
- package/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
- package/dist/clients/disposition-publish.js +10 -3
- package/dist/clients/durable-store.js +97 -0
- package/dist/clients/event-loop-monitor.js +94 -3
- package/dist/clients/extension-log.js +139 -0
- package/dist/clients/extension-mode.js +73 -0
- package/dist/clients/feature-hints.js +53 -7
- package/dist/clients/file-kinds.js +143 -31
- package/dist/clients/file-role.js +18 -3
- package/dist/clients/file-utils.js +56 -34
- package/dist/clients/fix-worklog.js +14 -2
- package/dist/clients/format-events-publish.js +16 -3
- package/dist/clients/format-service.js +24 -8
- package/dist/clients/formatters-lazy.js +8 -0
- package/dist/clients/formatters.js +358 -61
- package/dist/clients/generated-artifacts.js +169 -15
- package/dist/clients/git-guard.js +681 -11
- package/dist/clients/git-tracked-ignore.js +4 -0
- package/dist/clients/go-client.js +2 -1
- package/dist/clients/govulncheck-client.js +8 -0
- package/dist/clients/grammar-source.js +12 -4
- package/dist/clients/gzip-stage-write.js +102 -0
- package/dist/clients/host-ports.js +31 -0
- package/dist/clients/installer/index.js +733 -213
- package/dist/clients/instance-reaper.js +142 -172
- package/dist/clients/jscpd-client.js +31 -54
- package/dist/clients/knip-client.js +78 -36
- package/dist/clients/language-policy.js +14 -2
- package/dist/clients/language-profile.js +72 -77
- package/dist/clients/latency-logger.js +28 -16
- package/dist/clients/lens-config.js +140 -166
- package/dist/clients/lens-engine.js +124 -21
- package/dist/clients/lens-events.js +27 -4
- package/dist/clients/lens-flag-registry.js +293 -0
- package/dist/clients/lens-map.js +96 -26
- package/dist/clients/live-bus-emitter.js +30 -0
- package/dist/clients/log-cleanup.js +8 -18
- package/dist/clients/lsp/cascade-tier.js +23 -3
- package/dist/clients/lsp/client.js +566 -57
- package/dist/clients/lsp/config.js +74 -10
- package/dist/clients/lsp/diagnostic-binding.js +86 -0
- package/dist/clients/lsp/edits.js +1012 -121
- package/dist/clients/lsp/index.js +1840 -803
- package/dist/clients/lsp/language.js +4 -1
- package/dist/clients/lsp/launch.js +40 -32
- package/dist/clients/lsp/path-utils.js +1 -1
- package/dist/clients/lsp/ruby-drive-dirs.js +83 -0
- package/dist/clients/lsp/server.js +279 -60
- package/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
- package/dist/clients/lsp-document-symbols.js +112 -0
- package/dist/clients/lsp-lazy.js +8 -0
- package/dist/clients/lsp-mutation.js +248 -0
- package/dist/clients/mcp/analyze.js +128 -36
- package/dist/clients/mcp/ipc.js +157 -24
- package/dist/clients/mcp/review.js +36 -3
- package/dist/clients/mcp/session.js +216 -8
- package/dist/clients/memory-sampler.js +115 -0
- package/dist/clients/metrics-client.js +2 -1
- package/dist/clients/metrics-history.js +2 -1
- package/dist/clients/module-report-lsp.js +5 -0
- package/dist/clients/module-report.js +267 -82
- package/dist/clients/ndjson-logger.js +334 -103
- package/dist/clients/opengrep-client.js +1 -1
- package/dist/clients/partial-edit-apply.js +120 -5
- package/dist/clients/path-keyed-map.js +76 -0
- package/dist/clients/path-utils.js +141 -5
- package/dist/clients/performance-report.js +327 -0
- package/dist/clients/pipeline.js +54 -13
- package/dist/clients/print-mode.js +21 -0
- package/dist/clients/project-changes.js +117 -4
- package/dist/clients/project-diagnostics/cache.js +3 -2
- package/dist/clients/project-diagnostics/extractors.js +26 -115
- package/dist/clients/project-diagnostics/fresh-fetch.js +149 -19
- package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +117 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +37 -5
- package/dist/clients/project-diagnostics/scanner.js +268 -128
- package/dist/clients/project-lens-config.js +138 -34
- package/dist/clients/project-report.js +64 -25
- package/dist/clients/project-scale.js +8 -10
- package/dist/clients/project-snapshot-persist-worker.js +6 -0
- package/dist/clients/project-snapshot.js +767 -8
- package/dist/clients/project-trust.js +181 -0
- package/dist/clients/quiet-window.js +27 -5
- package/dist/clients/read-bridge.js +147 -0
- package/dist/clients/read-expansion.js +22 -12
- package/dist/clients/read-guard-logger.js +159 -1
- package/dist/clients/read-guard-tool-lines.js +150 -29
- package/dist/clients/read-guard.js +262 -3
- package/dist/clients/redact/secrets.js +449 -0
- package/dist/clients/resource-sampler.js +144 -39
- package/dist/clients/reverse-deps.js +48 -0
- package/dist/clients/review-graph/builder.js +2570 -326
- package/dist/clients/review-graph/git-identity.js +2 -1
- package/dist/clients/review-graph/import-resolvers.js +21 -10
- package/dist/clients/review-graph/persist-worker.js +10 -0
- package/dist/clients/review-graph/query.js +5 -0
- package/dist/clients/review-graph/shared-extraction-ir.js +90 -0
- package/dist/clients/review-graph/symbol-id.js +46 -0
- package/dist/clients/review-graph/tsconfig-paths.js +57 -6
- package/dist/clients/review-graph/workspace-modules.js +51 -3
- package/dist/clients/review-graph-logger.js +53 -0
- package/dist/clients/ruff-client.js +27 -44
- package/dist/clients/runtime-agent-end.js +111 -75
- package/dist/clients/runtime-context.js +36 -12
- package/dist/clients/runtime-coordinator.js +76 -8
- package/dist/clients/runtime-session.js +799 -137
- package/dist/clients/runtime-tool-call.js +117 -62
- package/dist/clients/runtime-tool-result.js +218 -23
- package/dist/clients/runtime-turn.js +284 -40
- package/dist/clients/rust-client.js +2 -1
- package/dist/clients/safe-spawn.js +873 -71
- package/dist/clients/security-scan-client.js +2 -1
- package/dist/clients/session-start-observability.js +15 -0
- package/dist/clients/session-state-store.js +5 -3
- package/dist/clients/sessionstart-logger.js +18 -0
- package/dist/clients/sg-runner.js +284 -159
- package/dist/clients/slow-fs.js +2 -1
- package/dist/clients/smells-rollup.js +224 -0
- package/dist/clients/source-filter.js +231 -43
- package/dist/clients/source-walker.js +66 -10
- package/dist/clients/startup-scan.js +15 -16
- package/dist/clients/startup-timing.js +9 -0
- package/dist/clients/test-runner-client.js +38 -8
- package/dist/clients/todo-scanner.js +30 -3
- package/dist/clients/tool-event.js +35 -0
- package/dist/clients/tool-policy.js +388 -39
- package/dist/clients/tool-render.js +155 -0
- package/dist/clients/tree-sitter-cache.js +153 -140
- package/dist/clients/tree-sitter-client.js +1722 -80
- package/dist/clients/tree-sitter-logger.js +64 -1
- package/dist/clients/tree-sitter-query-loader.js +117 -12
- package/dist/clients/tree-sitter-shared.js +48 -19
- package/dist/clients/tree-sitter-symbol-extractor.js +115 -11
- package/dist/clients/user-notify.js +51 -0
- package/dist/clients/vanished-instance-marker.js +57 -0
- package/dist/clients/warm-attach.js +33 -13
- package/dist/clients/widget-state.js +342 -49
- package/dist/clients/word-index-logger.js +44 -0
- package/dist/clients/word-index.js +612 -68
- package/dist/clients/workspace-topology.js +100 -7
- package/dist/index.js +65923 -47161
- package/dist/mcp/analyze-cli.js +185 -29
- package/dist/mcp/cli.js +110 -0
- package/dist/mcp/server.js +272 -50
- package/dist/tools/ast-grep-search.js +93 -27
- package/dist/tools/lens-diagnostics.js +247 -41
- package/dist/tools/lsp-diagnostics.js +153 -40
- package/dist/tools/lsp-navigation.js +51 -31
- package/dist/tools/module-report.js +9 -0
- package/dist/tools/symbol-search.js +21 -2
- package/docs/agent-guide.md +247 -0
- package/docs/agent-tools.md +21 -10
- package/docs/analysisall.md +192 -0
- package/docs/api-ports-inventory.md +165 -0
- package/docs/ast-grep_rules_catalog.md +5 -2
- package/docs/astplayground.md +3 -4
- package/docs/audit1.md +5 -5
- package/docs/durable-store-audit-1202.md +32 -0
- package/docs/environment-variables.md +197 -11
- package/docs/fable.md +27 -0
- package/docs/features.md +35 -5
- package/docs/globalconfig.md +101 -5
- package/docs/language-coverage.md +6 -5
- package/docs/lsp-capability-matrix.md +2 -2
- package/docs/mcp.md +54 -20
- package/docs/servercapabilities.md +7 -4
- package/docs/settings.md +192 -0
- package/docs/tools.md +18 -1
- package/docs/tree-sitter_rules_catalog.md +11 -31
- package/docs/usage.md +31 -0
- package/package.json +139 -138
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-js-test.yml +1 -0
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-test.yml +2 -0
- package/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
- package/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
- package/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-test.yml +4 -0
- package/rules/ast-grep-rules/rules/hardcoded-url-js.yml +22 -4
- package/rules/ast-grep-rules/rules/hardcoded-url.yml +22 -4
- package/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
- package/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
- package/rules/ast-grep-rules/rules/no-case-declarations-js.yml +4 -3
- package/rules/ast-grep-rules/rules/no-case-declarations.yml +4 -3
- package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +10 -0
- package/rules/ast-grep-rules/rules/no-console-except-error.yml +10 -0
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +42 -6
- package/rules/ast-grep-rules/rules/no-javascript-url.yml +42 -6
- package/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
- package/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
- package/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
- package/rules/ast-grep-rules/rules/strict-equality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-equality.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality.yml +14 -1
- package/rules/tree-sitter-queries/cpp/no-memset-sensitive-data.yml +9 -3
- package/rules/tree-sitter-queries/cpp/no-scoped-lock-without-args.yml +6 -6
- package/rules/tree-sitter-queries/css/calc-spacing.yml +8 -6
- package/rules/tree-sitter-queries/go/go-mutex-copy.yml +21 -18
- package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +3 -3
- package/rules/tree-sitter-queries/java/infinite-loop-java.yml +3 -3
- package/rules/tree-sitter-queries/java/junit-call-super.yml +6 -4
- package/rules/tree-sitter-queries/java/no-double-checked-locking.yml +10 -8
- package/rules/tree-sitter-queries/java/no-field-shadowing.yml +1 -1
- package/rules/tree-sitter-queries/java/prepared-statement-valid-indices.yml +2 -3
- package/rules/tree-sitter-queries/java/resources-closed.yml +2 -2
- package/rules/tree-sitter-queries/java/springboot-default-package.yml +6 -3
- package/rules/tree-sitter-queries/java/switch-fall-through.yml +6 -4
- package/rules/tree-sitter-queries/java/switch-non-case-labels.yml +4 -3
- package/rules/tree-sitter-queries/java/tests-include-assertions.yml +6 -5
- package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +5 -8
- package/rules/tree-sitter-queries/javascript/switch-non-case-labels.yml +3 -2
- package/rules/tree-sitter-queries/kotlin/prepared-statement-indices.yml +9 -5
- package/rules/tree-sitter-queries/php/this-in-static-context.yml +5 -5
- package/rules/tree-sitter-queries/python/exit-signature-check.yml +2 -9
- package/rules/tree-sitter-queries/python/in-operator-unsupported.yml +3 -4
- package/rules/tree-sitter-queries/python/no-super-torchscript.yml +4 -8
- package/rules/tree-sitter-queries/python/notimplemented-boolean-context.yml +1 -5
- package/rules/tree-sitter-queries/python/python-empty-except.yml +1 -1
- package/rules/tree-sitter-queries/python/return-in-init.yml +0 -2
- package/rules/tree-sitter-queries/python/yield-return-outside-function.yml +0 -3
- package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +1 -12
- package/rules/tree-sitter-queries/ruby/ruby-eval.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +4 -4
- package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +4 -11
- package/rules/tree-sitter-queries/ruby/ruby-open-struct.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-string-eval.yml +2 -3
- package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +4 -4
- package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +4 -3
- package/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
- package/rules/tree-sitter-queries/typescript/console-statement.yml +11 -0
- package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +4 -9
- package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +13 -9
- package/rules/tree-sitter-queries/typescript/infinite-loop.yml +2 -2
- package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +10 -1
- package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +5 -8
- package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +7 -10
- package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +4 -2
- package/rules/typos/_typos.toml +17 -0
- package/scripts/analyze-pi-lens-logs.mjs +55 -2
- package/skills/pi-lens-write-tree-sitter-rule/SKILL.md +5 -0
- package/dist/clients/ast-grep-parser.js +0 -86
- package/dist/clients/config-enabled-shape.js +0 -39
- package/dist/clients/lsp/interactive-install.js +0 -367
- package/dist/clients/lsp/lsp-index.js +0 -10
- package/dist/clients/lsp/server-strategies.js +0 -5
- package/dist/clients/project-metadata.js +0 -690
- package/dist/clients/source-groups.js +0 -96
- package/dist/clients/types.js +0 -11
- package/docs/env_variables.md +0 -10
- package/rules/tree-sitter-queries/abap-disabled/delete-where.yml +0 -46
- package/rules/tree-sitter-queries/cpp/no-confused-move-forward.yml +0 -59
- package/rules/tree-sitter-queries/cpp/noexcept-functions.yml +0 -58
- package/rules/tree-sitter-queries/java/mockito-initialized.yml +0 -66
- package/rules/tree-sitter-queries/java/no-wait-notify-on-thread.yml +0 -58
- package/rules/tree-sitter-queries/java/short-circuit-logic.yml +0 -57
- package/rules/tree-sitter-queries/java/spring-session-attributes-setcomplete.yml +0 -75
- package/rules/tree-sitter-queries/plsql-disabled/delete-update-where.yml +0 -56
- package/rules/tree-sitter-queries/plsql-disabled/end-loop-semicolon.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/fetch-bulk-collect-limit.yml +0 -47
- package/rules/tree-sitter-queries/plsql-disabled/forallsave-exceptions.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/nchar-nvarchar2-bytes.yml +0 -54
- package/rules/tree-sitter-queries/plsql-disabled/not-null-initialization.yml +0 -59
- package/rules/tree-sitter-queries/plsql-disabled/raise-application-error-codes.yml +0 -50
- package/rules/tree-sitter-queries/typescript-disabled/ts-path-traversal.yml +0 -71
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-cache observability (#1018).
|
|
3
|
+
*
|
|
4
|
+
* Two provider-independent signals, both sinking into ~/.pi-lens/latency.log
|
|
5
|
+
* via {@link logLatency} (matching the neighboring `type: "phase"` records):
|
|
6
|
+
*
|
|
7
|
+
* 1. Response-side cache usage (`cache_usage`) — on each assistant
|
|
8
|
+
* `message_end`, record the provider-reported token/cost breakdown so a
|
|
9
|
+
* session's actual cacheRead/cacheWrite behavior is queryable after the
|
|
10
|
+
* fact. This is what the provider DID, not what we hoped it would do.
|
|
11
|
+
*
|
|
12
|
+
* 2. Request-side context observations (`cache_context`) — one bounded record
|
|
13
|
+
* for every `context` call, describing pi-lens's own before/after message
|
|
14
|
+
* transformation, placement, injection sources, and privacy-preserving
|
|
15
|
+
* hashes. This is what pi-lens observed locally, not the final provider
|
|
16
|
+
* request after other context handlers have run and not a provider cache
|
|
17
|
+
* result.
|
|
18
|
+
*
|
|
19
|
+
* 3. Request-side prefix stability (`cache_prefix_break`) — a content hash of
|
|
20
|
+
* `messages[0]` observed on every `context` call. After #1016 the first
|
|
21
|
+
* message must stay byte-stable across a whole session; a logged CHANGE
|
|
22
|
+
* flags that something (pi-lens or otherwise) broke the cache prefix. This
|
|
23
|
+
* remains a local observation, never a provider cache-miss claim.
|
|
24
|
+
*
|
|
25
|
+
* All paths are defensive: `usage` (and its fields) may be absent on older
|
|
26
|
+
* hosts or non-assistant messages, so every access is guarded and the handlers
|
|
27
|
+
* never throw — on error they `dbg(...)` and no-op, like the other index.ts
|
|
28
|
+
* event handlers.
|
|
29
|
+
*/
|
|
30
|
+
import { createHash } from "node:crypto";
|
|
31
|
+
import { logLatency } from "./latency-logger.js";
|
|
32
|
+
/** RuntimeCoordinator's counter is process-global when sessions share a host. */
|
|
33
|
+
const PROCESS_TURN_SCOPE = "process-global-runtime";
|
|
34
|
+
const SECONDARY_TURN_SCOPE = "unavailable-concurrent-secondary";
|
|
35
|
+
/**
|
|
36
|
+
* Keep context telemetry bounded even when a tool result or user message is
|
|
37
|
+
* unexpectedly large. This is a hash input only; none of the sampled text is
|
|
38
|
+
* written to the log.
|
|
39
|
+
*/
|
|
40
|
+
const MAX_HASHED_MESSAGES = 64;
|
|
41
|
+
const MAX_HASHED_CONTENT_CHARS = 2048;
|
|
42
|
+
const MAX_REPORTED_MESSAGES = 10_000;
|
|
43
|
+
const MAX_INJECTED_CHARS = 16_384;
|
|
44
|
+
const MAX_INJECTED_BYTES = 65_536;
|
|
45
|
+
let contextObservationCounter = 0;
|
|
46
|
+
function boundedHashScalar(value, state) {
|
|
47
|
+
if (value === null)
|
|
48
|
+
return "null";
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
return "undefined";
|
|
51
|
+
if (typeof value === "string") {
|
|
52
|
+
const truncated = value.length > MAX_HASHED_CONTENT_CHARS;
|
|
53
|
+
if (truncated)
|
|
54
|
+
state.contentTruncated = true;
|
|
55
|
+
const suffix = truncated ? ":content-truncated" : "";
|
|
56
|
+
return `string:${value.length}:${value.slice(0, MAX_HASHED_CONTENT_CHARS)}${suffix}`;
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "number" ||
|
|
59
|
+
typeof value === "boolean" ||
|
|
60
|
+
typeof value === "bigint") {
|
|
61
|
+
return `${typeof value}:${String(value)}`;
|
|
62
|
+
}
|
|
63
|
+
return typeof value;
|
|
64
|
+
}
|
|
65
|
+
function boundedHashArray(value, depth, seen, state) {
|
|
66
|
+
if (value.length > 12)
|
|
67
|
+
state.contentTruncated = true;
|
|
68
|
+
const items = value
|
|
69
|
+
.slice(0, 12)
|
|
70
|
+
.map((item) => boundedHashValue(item, depth + 1, seen, state));
|
|
71
|
+
return `array:${value.length}:[${items.join(",")}]`;
|
|
72
|
+
}
|
|
73
|
+
function boundedHashObject(value, depth, seen, state) {
|
|
74
|
+
const keys = Object.keys(value).sort((left, right) => left.localeCompare(right));
|
|
75
|
+
if (keys.length > 24)
|
|
76
|
+
state.contentTruncated = true;
|
|
77
|
+
const fields = keys
|
|
78
|
+
.slice(0, 24)
|
|
79
|
+
.map((key) => `${key}:${boundedHashValue(value[key], depth + 1, seen, state)}`);
|
|
80
|
+
return `object:${keys.length}:{${fields.join(",")}}`;
|
|
81
|
+
}
|
|
82
|
+
function boundedHashValue(value, depth = 0, seen = new Set(), state) {
|
|
83
|
+
const hashState = state ?? { contentTruncated: false };
|
|
84
|
+
if (value === null || typeof value !== "object") {
|
|
85
|
+
return boundedHashScalar(value, hashState);
|
|
86
|
+
}
|
|
87
|
+
if (depth >= 3) {
|
|
88
|
+
hashState.contentTruncated = true;
|
|
89
|
+
return Object.prototype.toString.call(value);
|
|
90
|
+
}
|
|
91
|
+
if (seen.has(value))
|
|
92
|
+
return "[cycle]";
|
|
93
|
+
seen.add(value);
|
|
94
|
+
try {
|
|
95
|
+
if (Array.isArray(value)) {
|
|
96
|
+
return boundedHashArray(value, depth, seen, hashState);
|
|
97
|
+
}
|
|
98
|
+
return boundedHashObject(value, depth, seen, hashState);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return "[unreadable]";
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
seen.delete(value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function hashMessageSequence(messages) {
|
|
108
|
+
const hash = createHash("sha256");
|
|
109
|
+
const state = { contentTruncated: false };
|
|
110
|
+
hash.update(`message-count:${messages.length};`);
|
|
111
|
+
const sampled = Math.min(messages.length, MAX_HASHED_MESSAGES);
|
|
112
|
+
for (let i = 0; i < sampled; i++) {
|
|
113
|
+
const message = messages[i];
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
hash.update(`${i}|role:${boundedHashValue(message?.role, 0, seen, state)}|content:${boundedHashValue(message?.content, 0, seen, state)};`);
|
|
116
|
+
}
|
|
117
|
+
const truncated = messages.length > sampled;
|
|
118
|
+
if (truncated)
|
|
119
|
+
hash.update(`truncated-after:${sampled};`);
|
|
120
|
+
return {
|
|
121
|
+
hash: hash.digest("hex"),
|
|
122
|
+
truncated,
|
|
123
|
+
contentTruncated: state.contentTruncated,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function messageTextSize(content) {
|
|
127
|
+
if (typeof content === "string") {
|
|
128
|
+
const chars = Math.min(content.length, MAX_INJECTED_CHARS);
|
|
129
|
+
return {
|
|
130
|
+
chars,
|
|
131
|
+
bytes: Math.min(Buffer.byteLength(content.slice(0, chars), "utf8"), MAX_INJECTED_BYTES),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const bounded = boundedHashValue(content);
|
|
135
|
+
return {
|
|
136
|
+
chars: Math.min(bounded.length, MAX_INJECTED_CHARS),
|
|
137
|
+
bytes: Math.min(Buffer.byteLength(bounded.slice(0, MAX_INJECTED_CHARS), "utf8"), MAX_INJECTED_BYTES),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function measureInjectedMessages(messages) {
|
|
141
|
+
let chars = 0;
|
|
142
|
+
let bytes = 0;
|
|
143
|
+
const measuredCount = Math.min(messages.length, MAX_REPORTED_MESSAGES);
|
|
144
|
+
for (let i = 0; i < measuredCount; i++) {
|
|
145
|
+
const message = messages[i];
|
|
146
|
+
const size = messageTextSize(message?.content);
|
|
147
|
+
chars = Math.min(MAX_INJECTED_CHARS, chars + size.chars);
|
|
148
|
+
bytes = Math.min(MAX_INJECTED_BYTES, bytes + size.bytes);
|
|
149
|
+
if (chars === MAX_INJECTED_CHARS || bytes === MAX_INJECTED_BYTES)
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
chars,
|
|
154
|
+
bytes,
|
|
155
|
+
capped: chars >= MAX_INJECTED_CHARS || bytes >= MAX_INJECTED_BYTES,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function sessionKey(sessionId) {
|
|
159
|
+
return sessionId?.trim() ? sessionId.trim() : NO_SESSION_KEY;
|
|
160
|
+
}
|
|
161
|
+
function prefixLengthForPlacement(placement, messageCount) {
|
|
162
|
+
if (placement === "insert-before-final") {
|
|
163
|
+
return Math.max(0, messageCount - 1);
|
|
164
|
+
}
|
|
165
|
+
if (placement === "prepend")
|
|
166
|
+
return 0;
|
|
167
|
+
return messageCount;
|
|
168
|
+
}
|
|
169
|
+
function resolvePrefixObservation(truncated, observation) {
|
|
170
|
+
if (truncated)
|
|
171
|
+
return "unknown";
|
|
172
|
+
return observation ?? "empty";
|
|
173
|
+
}
|
|
174
|
+
function prefixBaselineForObservation(observation) {
|
|
175
|
+
if (observation === "baseline")
|
|
176
|
+
return true;
|
|
177
|
+
if (observation === "empty" || observation === "unknown")
|
|
178
|
+
return null;
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
function firstMessageChangeFor(truncated, before, after) {
|
|
182
|
+
if (truncated)
|
|
183
|
+
return "unknown";
|
|
184
|
+
return before !== after ? "changed" : "unchanged";
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Log one bounded request-side observation for every `context` call. This is
|
|
188
|
+
* deliberately a separate phase from `cache_prefix_break`: it describes the
|
|
189
|
+
* messages pi-lens saw and returned from its own context handler, not the final
|
|
190
|
+
* provider request after other context handlers and not whether a provider
|
|
191
|
+
* reused or missed a prompt cache. `observationId` is local to this process; the
|
|
192
|
+
* host currently exposes no request id on ContextEvent/MessageEndEvent. A
|
|
193
|
+
* RuntimeCoordinator turn is process-global, so it is explicitly scoped as
|
|
194
|
+
* such and is omitted for concurrent secondary sessions.
|
|
195
|
+
*/
|
|
196
|
+
export function observeCacheContext(args) {
|
|
197
|
+
try {
|
|
198
|
+
const existingMessages = args.existingMessages ?? [];
|
|
199
|
+
const resultMessages = args.resultMessages ?? existingMessages;
|
|
200
|
+
const injectedMessages = args.injectedMessages ?? [];
|
|
201
|
+
const placement = args.placement ?? "none";
|
|
202
|
+
const beforeSequence = hashMessageSequence(existingMessages);
|
|
203
|
+
const afterSequence = hashMessageSequence(resultMessages);
|
|
204
|
+
const beforePrefixLength = prefixLengthForPlacement(placement, existingMessages.length);
|
|
205
|
+
const afterPrefixLength = Math.min(beforePrefixLength, resultMessages.length);
|
|
206
|
+
const beforePrefix = hashMessageSequence(existingMessages.slice(0, beforePrefixLength));
|
|
207
|
+
const afterPrefix = hashMessageSequence(resultMessages.slice(0, afterPrefixLength));
|
|
208
|
+
const beforeFirstSequence = existingMessages.length
|
|
209
|
+
? hashMessageSequence([existingMessages[0]])
|
|
210
|
+
: undefined;
|
|
211
|
+
const afterFirstSequence = resultMessages.length
|
|
212
|
+
? hashMessageSequence([resultMessages[0]])
|
|
213
|
+
: undefined;
|
|
214
|
+
const beforeFirst = beforeFirstSequence?.hash ?? null;
|
|
215
|
+
const afterFirst = afterFirstSequence?.hash ?? null;
|
|
216
|
+
const firstMessageHashTruncated = beforeFirstSequence?.truncated === true ||
|
|
217
|
+
beforeFirstSequence?.contentTruncated === true ||
|
|
218
|
+
afterFirstSequence?.truncated === true ||
|
|
219
|
+
afterFirstSequence?.contentTruncated === true;
|
|
220
|
+
const prefixHashTruncated = beforePrefix.truncated ||
|
|
221
|
+
afterPrefix.truncated ||
|
|
222
|
+
beforePrefix.contentTruncated ||
|
|
223
|
+
afterPrefix.contentTruncated;
|
|
224
|
+
const prefixObservation = resolvePrefixObservation(prefixHashTruncated, args.prefixObservation);
|
|
225
|
+
const sizes = measureInjectedMessages(injectedMessages);
|
|
226
|
+
const messageCountCapped = existingMessages.length > MAX_REPORTED_MESSAGES ||
|
|
227
|
+
resultMessages.length > MAX_REPORTED_MESSAGES;
|
|
228
|
+
logLatency({
|
|
229
|
+
type: "phase",
|
|
230
|
+
filePath: "<pi-lens>",
|
|
231
|
+
phase: "cache_context",
|
|
232
|
+
durationMs: 0,
|
|
233
|
+
metadata: {
|
|
234
|
+
version: 1,
|
|
235
|
+
observedStage: "pi-lens-context-handler",
|
|
236
|
+
observationId: `ctx-${(++contextObservationCounter).toString(36)}`,
|
|
237
|
+
sessionId: sessionKey(args.sessionId),
|
|
238
|
+
sessionRole: args.sessionRole,
|
|
239
|
+
...(args.sessionRole === "concurrent-secondary"
|
|
240
|
+
? { turnScope: SECONDARY_TURN_SCOPE }
|
|
241
|
+
: { turnIndex: args.turnIndex, turnScope: PROCESS_TURN_SCOPE }),
|
|
242
|
+
injectionEnabled: args.injectionEnabled,
|
|
243
|
+
injectionSources: Array.from(args.injectionSources ?? []),
|
|
244
|
+
injectedMessageCount: Math.min(injectedMessages.length, MAX_REPORTED_MESSAGES),
|
|
245
|
+
injectedMessageCountCapped: injectedMessages.length > MAX_REPORTED_MESSAGES,
|
|
246
|
+
injectedChars: sizes.chars,
|
|
247
|
+
injectedBytes: sizes.bytes,
|
|
248
|
+
injectedCountsCapped: sizes.capped,
|
|
249
|
+
existingMessageCount: Math.min(existingMessages.length, MAX_REPORTED_MESSAGES),
|
|
250
|
+
resultMessageCount: Math.min(resultMessages.length, MAX_REPORTED_MESSAGES),
|
|
251
|
+
messageCountCapped,
|
|
252
|
+
placement,
|
|
253
|
+
prefixObservation,
|
|
254
|
+
prefixObservationUnknown: prefixObservation === "unknown",
|
|
255
|
+
prefixBaseline: prefixBaselineForObservation(prefixObservation),
|
|
256
|
+
firstMessageChanged: firstMessageHashTruncated
|
|
257
|
+
? null
|
|
258
|
+
: beforeFirst !== afterFirst,
|
|
259
|
+
firstMessageChange: firstMessageChangeFor(firstMessageHashTruncated, beforeFirst, afterFirst),
|
|
260
|
+
firstMessageHashTruncated,
|
|
261
|
+
beforeFirstMessageHash: beforeFirst,
|
|
262
|
+
afterFirstMessageHash: afterFirst,
|
|
263
|
+
beforeSequenceHash: beforeSequence.hash,
|
|
264
|
+
afterSequenceHash: afterSequence.hash,
|
|
265
|
+
beforePrefixHash: beforePrefix.hash,
|
|
266
|
+
afterPrefixHash: afterPrefix.hash,
|
|
267
|
+
sequenceHashTruncated: beforeSequence.truncated ||
|
|
268
|
+
afterSequence.truncated ||
|
|
269
|
+
beforeSequence.contentTruncated ||
|
|
270
|
+
afterSequence.contentTruncated,
|
|
271
|
+
sequenceMessageCountTruncated: beforeSequence.truncated || afterSequence.truncated,
|
|
272
|
+
sequenceContentHashTruncated: beforeSequence.contentTruncated || afterSequence.contentTruncated,
|
|
273
|
+
prefixHashTruncated,
|
|
274
|
+
prefixMessageCountTruncated: beforePrefix.truncated || afterPrefix.truncated,
|
|
275
|
+
prefixContentHashTruncated: beforePrefix.contentTruncated || afterPrefix.contentTruncated,
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
args.dbg?.(`cache-context: failed to log context observation: ${err}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Part 1 — log one `cache_usage` record for an assistant `message_end` that
|
|
285
|
+
* carries a `usage`. Provider/model come straight off the message itself
|
|
286
|
+
* (`AssistantMessage.provider` / `.model` in pi-ai) — no dependency on the
|
|
287
|
+
* runtime telemetry identity. Skips silently (no record) when the message is
|
|
288
|
+
* not an assistant message or has no usage; never logs a zeros-only record for
|
|
289
|
+
* a message that simply lacks usage.
|
|
290
|
+
*/
|
|
291
|
+
export function logCacheUsage(message, dbg, context) {
|
|
292
|
+
try {
|
|
293
|
+
if (!message || typeof message !== "object")
|
|
294
|
+
return;
|
|
295
|
+
const msg = message;
|
|
296
|
+
// Only assistant messages carry LLM usage; tool-result / user messages
|
|
297
|
+
// (and unknown custom AgentMessage variants) are skipped.
|
|
298
|
+
if (msg.role !== "assistant")
|
|
299
|
+
return;
|
|
300
|
+
const usage = msg.usage;
|
|
301
|
+
if (!usage || typeof usage !== "object")
|
|
302
|
+
return;
|
|
303
|
+
const u = usage;
|
|
304
|
+
logLatency({
|
|
305
|
+
type: "phase",
|
|
306
|
+
filePath: "<pi-lens>",
|
|
307
|
+
phase: "cache_usage",
|
|
308
|
+
durationMs: 0,
|
|
309
|
+
metadata: {
|
|
310
|
+
provider: typeof msg.provider === "string" ? msg.provider : undefined,
|
|
311
|
+
model: typeof msg.model === "string" ? msg.model : undefined,
|
|
312
|
+
cacheRead: u.cacheRead,
|
|
313
|
+
cacheWrite: u.cacheWrite,
|
|
314
|
+
input: u.input,
|
|
315
|
+
output: u.output,
|
|
316
|
+
// `Usage.cost` is a breakdown object; the total is the headline number.
|
|
317
|
+
cost: u.cost?.total,
|
|
318
|
+
...(context
|
|
319
|
+
? {
|
|
320
|
+
// MessageEndEvent has no request/context id in the host API. These
|
|
321
|
+
// fields permit session correlation without inventing an exact
|
|
322
|
+
// provider-request linkage. The process-global turn is omitted
|
|
323
|
+
// for a concurrent secondary session.
|
|
324
|
+
sessionId: sessionKey(context.sessionId),
|
|
325
|
+
...(context.sessionRole === "concurrent-secondary"
|
|
326
|
+
? { turnScope: SECONDARY_TURN_SCOPE }
|
|
327
|
+
: {
|
|
328
|
+
turnIndex: context.turnIndex,
|
|
329
|
+
turnScope: PROCESS_TURN_SCOPE,
|
|
330
|
+
}),
|
|
331
|
+
contextCorrelation: context.sessionId
|
|
332
|
+
? "session-only-no-request-id"
|
|
333
|
+
: "no-stable-session-id",
|
|
334
|
+
}
|
|
335
|
+
: {}),
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
dbg?.(`cache-usage: failed to log message_end usage: ${err}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Content hash of the first transcript message. Stable for identical content
|
|
345
|
+
* (role + content are serialized in a fixed order), so a changing hash means
|
|
346
|
+
* `messages[0]` actually changed byte-for-byte.
|
|
347
|
+
*/
|
|
348
|
+
function hashFirstMessage(first) {
|
|
349
|
+
// Keep the original full local hash semantics for the existing
|
|
350
|
+
// `cache_prefix_break` signal. The new `cache_context` hashes above are the
|
|
351
|
+
// bounded hashes; this signal must not silently miss a suffix-only change.
|
|
352
|
+
const serialized = JSON.stringify({
|
|
353
|
+
role: first.role,
|
|
354
|
+
content: first.content,
|
|
355
|
+
});
|
|
356
|
+
return createHash("sha256").update(serialized).digest("hex");
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Per-session baseline hash of `messages[0]`, keyed by the STABLE pi session id
|
|
360
|
+
* (`ctx.sessionManager.getSessionId()`). A single module-scoped var was wrong:
|
|
361
|
+
* in ONE process, multiple logical conversations touch this module, and they
|
|
362
|
+
* must not share a baseline —
|
|
363
|
+
* - new / fork / a concurrent in-process subagent (#473) is a DIFFERENT id →
|
|
364
|
+
* its OWN independent baseline, never compared against another id's (else a
|
|
365
|
+
* benign session boundary logs a spurious `cache_prefix_break`, and a
|
|
366
|
+
* concurrent subagent + its parent stomp each other's baseline, each
|
|
367
|
+
* emitting a false positive — fatal for a signal whose value is trust);
|
|
368
|
+
* - an IN-PROCESS reload / resume reuses the SAME id → the baseline is still in
|
|
369
|
+
* this map, so a genuine break is caught (no blinding reset). NOTE: a full
|
|
370
|
+
* process restart (quit → `pi --session <id>`) starts a NEW process with an
|
|
371
|
+
* empty map, so the first post-restart `context` re-anchors a fresh baseline
|
|
372
|
+
* rather than comparing across the restart. That's acceptable here — this is a
|
|
373
|
+
* pure observability signal (at worst a missed `cache_prefix_break` log on the
|
|
374
|
+
* first post-restart turn, never a user-facing action) — unlike genuine
|
|
375
|
+
* session state (read guard #1041, widget #190) which IS rehydrated from the
|
|
376
|
+
* sidecar.
|
|
377
|
+
*
|
|
378
|
+
* Bounded as an insertion-ordered LRU (evict oldest-inserted past the cap) so a
|
|
379
|
+
* long-lived process cycling through many sessions can't grow this unbounded.
|
|
380
|
+
*/
|
|
381
|
+
const MAX_TRACKED_SESSIONS = 32;
|
|
382
|
+
const prefixHashBySession = new Map();
|
|
383
|
+
/**
|
|
384
|
+
* Bucket key used when no session id is available (undefined/empty). Degrades
|
|
385
|
+
* gracefully to the old single-var semantics — one shared baseline — rather than
|
|
386
|
+
* throwing or dropping the signal.
|
|
387
|
+
*/
|
|
388
|
+
const NO_SESSION_KEY = "<no-session>";
|
|
389
|
+
/**
|
|
390
|
+
* Store `hash` for `key`, refreshing its LRU recency (re-insert moves it to the
|
|
391
|
+
* newest position since `Map` preserves insertion order) and evicting the
|
|
392
|
+
* oldest-inserted entries once the cap is exceeded.
|
|
393
|
+
*/
|
|
394
|
+
function recordSessionHash(key, hash) {
|
|
395
|
+
prefixHashBySession.delete(key);
|
|
396
|
+
prefixHashBySession.set(key, hash);
|
|
397
|
+
while (prefixHashBySession.size > MAX_TRACKED_SESSIONS) {
|
|
398
|
+
const oldest = prefixHashBySession.keys().next().value;
|
|
399
|
+
if (oldest === undefined)
|
|
400
|
+
break;
|
|
401
|
+
prefixHashBySession.delete(oldest);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Part 2 — observe `messages[0]` stability turn-over-turn, keyed by session id.
|
|
406
|
+
* Logs a baseline the first time it sees a non-empty transcript FOR A GIVEN
|
|
407
|
+
* session id, then logs a `cache_prefix_break` whenever that same session's
|
|
408
|
+
* first-message hash changes. Pure observation: it never inspects or mutates
|
|
409
|
+
* anything but its own per-session hash map, and never throws.
|
|
410
|
+
*
|
|
411
|
+
* `sessionId` should be pi's STABLE id (`ctx.sessionManager.getSessionId()`),
|
|
412
|
+
* which uniquely identifies the CURRENTLY-firing session: a concurrent
|
|
413
|
+
* in-process subagent runs its own `AgentSession`/`sessionManager`, so its
|
|
414
|
+
* `context` calls carry a DIFFERENT id than the parent's and get their own
|
|
415
|
+
* baseline. (`runtime.telemetrySessionId` cannot be used here: per the #473
|
|
416
|
+
* guard a concurrent secondary skips `updateRuntimeIdentityFromEvent`, so that
|
|
417
|
+
* process-global singleton stays pinned to the PARENT — it would collapse
|
|
418
|
+
* parent and subagent onto one baseline.) Residual limitation: if the host ever
|
|
419
|
+
* does NOT supply a stable id, all such sessions collapse onto `NO_SESSION_KEY`
|
|
420
|
+
* and behave like the old single-var (a concurrent subagent could then still
|
|
421
|
+
* cross-contaminate) — accepted as graceful degradation, not silently perfect.
|
|
422
|
+
*
|
|
423
|
+
* Does nothing on an empty transcript (nothing to anchor a prefix to yet).
|
|
424
|
+
*/
|
|
425
|
+
export function observeCachePrefix(messages, turnIndex, sessionId, sessionRole, dbg) {
|
|
426
|
+
try {
|
|
427
|
+
if (!messages || messages.length === 0)
|
|
428
|
+
return "empty";
|
|
429
|
+
const first = messages[0];
|
|
430
|
+
if (!first || typeof first !== "object")
|
|
431
|
+
return "empty";
|
|
432
|
+
const key = sessionKey(sessionId);
|
|
433
|
+
const currentHash = hashFirstMessage(first);
|
|
434
|
+
const previousHash = prefixHashBySession.get(key);
|
|
435
|
+
if (previousHash === undefined) {
|
|
436
|
+
// Baseline: record this session's starting prefix so a later break has a
|
|
437
|
+
// reference point in the log. `previousHash: null` marks the baseline.
|
|
438
|
+
recordSessionHash(key, currentHash);
|
|
439
|
+
logLatency({
|
|
440
|
+
type: "phase",
|
|
441
|
+
filePath: "<pi-lens>",
|
|
442
|
+
phase: "cache_prefix_break",
|
|
443
|
+
durationMs: 0,
|
|
444
|
+
metadata: {
|
|
445
|
+
...(sessionRole === "concurrent-secondary"
|
|
446
|
+
? { turnScope: SECONDARY_TURN_SCOPE }
|
|
447
|
+
: { turnIndex, turnScope: PROCESS_TURN_SCOPE }),
|
|
448
|
+
previousHash: null,
|
|
449
|
+
currentHash,
|
|
450
|
+
baseline: true,
|
|
451
|
+
sessionId: key,
|
|
452
|
+
sessionRole,
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
return "baseline";
|
|
456
|
+
}
|
|
457
|
+
if (currentHash !== previousHash) {
|
|
458
|
+
logLatency({
|
|
459
|
+
type: "phase",
|
|
460
|
+
filePath: "<pi-lens>",
|
|
461
|
+
phase: "cache_prefix_break",
|
|
462
|
+
durationMs: 0,
|
|
463
|
+
metadata: {
|
|
464
|
+
...(sessionRole === "concurrent-secondary"
|
|
465
|
+
? { turnScope: SECONDARY_TURN_SCOPE }
|
|
466
|
+
: { turnIndex, turnScope: PROCESS_TURN_SCOPE }),
|
|
467
|
+
previousHash,
|
|
468
|
+
currentHash,
|
|
469
|
+
sessionId: key,
|
|
470
|
+
sessionRole,
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
// Refresh recency (and update the stored hash after a break) so an active
|
|
475
|
+
// session stays warm in the LRU and isn't evicted while still in use.
|
|
476
|
+
recordSessionHash(key, currentHash);
|
|
477
|
+
return currentHash === previousHash ? "unchanged" : "changed";
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
dbg?.(`cache-prefix: failed to observe messages[0]: ${err}`);
|
|
481
|
+
return "empty";
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Drop a single session's prefix baseline. Called from the `session_shutdown`
|
|
486
|
+
* handler (primary path only — the concurrent-secondary guard there returns
|
|
487
|
+
* first, and the LRU cap backstops any secondary entry left behind) so an ended
|
|
488
|
+
* conversation's entry is reclaimed promptly rather than only when the LRU
|
|
489
|
+
* evicts it. Idempotent and never throws.
|
|
490
|
+
*/
|
|
491
|
+
export function clearCachePrefixSession(sessionId) {
|
|
492
|
+
const key = sessionKey(sessionId);
|
|
493
|
+
prefixHashBySession.delete(key);
|
|
494
|
+
}
|
|
495
|
+
/** Clear all per-session prefix hashes. For tests / session boundaries. */
|
|
496
|
+
export function resetCachePrefixObservation() {
|
|
497
|
+
prefixHashBySession.clear();
|
|
498
|
+
}
|