@piagent/platform 1.5.5 → 1.6.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.
Files changed (227) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.md +9 -8
  3. package/README.vi.md +9 -5
  4. package/adapters/web-frontend/profile.json +2 -4
  5. package/architecture/layers.json +18 -3
  6. package/benchmarks/deep-logic-v1/SPECIALIST_PROTOCOL.vi.md +122 -0
  7. package/benchmarks/deep-logic-v1/grade.mjs +272 -10
  8. package/benchmarks/deep-logic-v1/project/apps/admin/src/billing-summary.js +3 -0
  9. package/benchmarks/deep-logic-v1/project/benchmark-contract/public-smoke.test.js +166 -0
  10. package/benchmarks/deep-logic-v1/project/package.json +1 -1
  11. package/benchmarks/deep-logic-v1/project/packages/billing/src/close-period.js +3 -0
  12. package/benchmarks/deep-logic-v1/project/packages/billing/src/plan-timeline.js +3 -0
  13. package/benchmarks/deep-logic-v1/project/test/smoke.test.js +7 -1
  14. package/benchmarks/deep-logic-v1/prompts/budgeted-context-graph.md +2 -0
  15. package/benchmarks/deep-logic-v1/prompts/fair-dependency-scheduler.md +2 -0
  16. package/benchmarks/deep-logic-v1/prompts/layered-policy-resolution.md +2 -0
  17. package/benchmarks/deep-logic-v1/prompts/resumable-stream-assembly.md +3 -1
  18. package/benchmarks/deep-logic-v1/prompts/revision-event-reconciliation.md +2 -0
  19. package/benchmarks/deep-logic-v1/prompts/temporal-usage-billing-close.md +123 -0
  20. package/benchmarks/deep-logic-v1/prompts/transactional-config-merge.md +2 -0
  21. package/benchmarks/deep-logic-v1/suite.json +51 -3
  22. package/benchmarks/deep-logic-v1/variant.mjs +22 -1
  23. package/benchmarks/production-v1/spend-control.v1.json +68 -0
  24. package/benchmarks/production-v1/suite.json +42 -3
  25. package/catalog/capabilities.json +6 -6
  26. package/docs/command-reference-vietnamese.md +6 -6
  27. package/docs/context-engine.md +7 -1
  28. package/docs/distribution-standard.md +7 -7
  29. package/docs/en/architecture.md +1 -0
  30. package/docs/languages.json +4 -4
  31. package/docs/model-options.md +8 -5
  32. package/docs/operator-manual-vietnamese.md +13 -11
  33. package/docs/project-adapters.md +2 -2
  34. package/docs/publishing-for-teams.md +5 -0
  35. package/docs/quality-benchmark.md +271 -62
  36. package/docs/release-install-policy.md +10 -9
  37. package/docs/runtime-policy-design.md +4 -4
  38. package/docs/runtime-quality-baseline.md +13 -6
  39. package/docs/security-threat-model.md +2 -2
  40. package/docs/task-lifecycle-vietnamese.md +2 -2
  41. package/docs/team-onboarding.md +2 -2
  42. package/docs/usage-observability.md +7 -2
  43. package/docs/vi/architecture.md +1 -0
  44. package/docs-site/architecture.html +1 -1
  45. package/docs-site/benchmark.html +1 -1
  46. package/docs-site/commands.html +1 -1
  47. package/docs-site/context-engine.html +1 -1
  48. package/docs-site/en/architecture.html +1 -1
  49. package/docs-site/en/benchmark.html +1 -1
  50. package/docs-site/en/commands.html +1 -1
  51. package/docs-site/en/context-engine.html +1 -1
  52. package/docs-site/en/index.html +1 -1
  53. package/docs-site/en/mcp-approval.html +1 -1
  54. package/docs-site/en/mcp-auth.html +1 -1
  55. package/docs-site/en/mcp-servers.html +1 -1
  56. package/docs-site/en/mcp.html +1 -1
  57. package/docs-site/en/permissions.html +1 -1
  58. package/docs-site/en/profiles.html +1 -1
  59. package/docs-site/en/quickstart.html +1 -1
  60. package/docs-site/en/runtime.html +1 -1
  61. package/docs-site/en/scope.html +1 -1
  62. package/docs-site/en/security.html +1 -1
  63. package/docs-site/en/team.html +1 -1
  64. package/docs-site/en/whats-new.html +1 -1
  65. package/docs-site/en/workflows.html +1 -1
  66. package/docs-site/index.html +1 -1
  67. package/docs-site/mcp-approval.html +1 -1
  68. package/docs-site/mcp-auth.html +1 -1
  69. package/docs-site/mcp-servers.html +1 -1
  70. package/docs-site/mcp.html +1 -1
  71. package/docs-site/permissions.html +1 -1
  72. package/docs-site/profiles.html +1 -1
  73. package/docs-site/quickstart.html +1 -1
  74. package/docs-site/runtime.html +1 -1
  75. package/docs-site/scope.html +1 -1
  76. package/docs-site/security.html +1 -1
  77. package/docs-site/team.html +1 -1
  78. package/docs-site/whats-new.html +1 -1
  79. package/docs-site/workflows.html +1 -1
  80. package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
  81. package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
  82. package/evals/fixtures/piagent-webui/session-live-state-v1.invalid.json +18 -0
  83. package/evals/fixtures/piagent-webui/session-live-state-v1.valid.json +27 -0
  84. package/evals/fs4-readiness-matrix.v1.json +2 -2
  85. package/evals/fs5-causal-arm.v1.json +2 -2
  86. package/evals/fs5-pilot-protocol.v1.json +2 -2
  87. package/evals/fs5-pilot-protocol.v4.json +6 -5
  88. package/evals/fs5-pilot-protocol.v5.json +7 -6
  89. package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
  90. package/package.json +3 -1
  91. package/packages/piagent-core/benchmark/benchmark-assurance.js +4 -1
  92. package/packages/piagent-core/benchmark/benchmark-bootstrap.js +91 -9
  93. package/packages/piagent-core/benchmark/benchmark-claim-restrictions.js +38 -7
  94. package/packages/piagent-core/benchmark/benchmark-cli.js +7 -7
  95. package/packages/piagent-core/benchmark/benchmark-codex.js +3 -1
  96. package/packages/piagent-core/benchmark/benchmark-comparison.js +271 -2
  97. package/packages/piagent-core/benchmark/benchmark-core.js +732 -661
  98. package/packages/piagent-core/benchmark/benchmark-execution-guard.js +7 -5
  99. package/packages/piagent-core/benchmark/benchmark-forensics.js +439 -4
  100. package/packages/piagent-core/benchmark/benchmark-host-readiness.js +639 -0
  101. package/packages/piagent-core/benchmark/benchmark-matrix.js +22 -4
  102. package/packages/piagent-core/benchmark/benchmark-normalized-cost.js +317 -0
  103. package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
  104. package/packages/piagent-core/benchmark/benchmark-process.js +6 -5
  105. package/packages/piagent-core/benchmark/benchmark-provider-wire.js +126 -0
  106. package/packages/piagent-core/benchmark/benchmark-record-validation.js +183 -0
  107. package/packages/piagent-core/benchmark/benchmark-report.js +116 -12
  108. package/packages/piagent-core/benchmark/benchmark-resume-recovery.js +14 -0
  109. package/packages/piagent-core/benchmark/benchmark-runner-policy.js +26 -0
  110. package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +782 -0
  111. package/packages/piagent-core/benchmark/benchmark-statistics.js +64 -0
  112. package/packages/piagent-core/benchmark/benchmark-stop-policy.js +8 -5
  113. package/packages/piagent-core/benchmark/benchmark-suite-identity.js +31 -0
  114. package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +13 -9
  115. package/packages/piagent-core/benchmark/benchmark-suite.js +128 -2
  116. package/packages/piagent-core/benchmark/benchmark-summary-support.js +376 -0
  117. package/packages/piagent-core/benchmark/benchmark-timing-diagnostics.js +971 -0
  118. package/packages/piagent-core/benchmark/benchmark-usage.js +311 -0
  119. package/packages/piagent-core/benchmark/benchmark-workflow.js +166 -0
  120. package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +1080 -0
  121. package/packages/piagent-core/extensions/acceptance-contract-semantics.js +83 -69
  122. package/packages/piagent-core/extensions/acceptance-error-classes.js +160 -0
  123. package/packages/piagent-core/extensions/acceptance-executable-evidence.js +12 -9
  124. package/packages/piagent-core/extensions/acceptance-receipt.js +34 -11
  125. package/packages/piagent-core/extensions/acceptance-state-machine-evidence.js +518 -0
  126. package/packages/piagent-core/extensions/acceptance-test-binding-evidence.js +60 -0
  127. package/packages/piagent-core/extensions/context-efficiency-metrics.js +302 -0
  128. package/packages/piagent-core/extensions/context-efficiency-report.js +167 -0
  129. package/packages/piagent-core/extensions/context-engine.js +148 -145
  130. package/packages/piagent-core/extensions/context-evidence.js +48 -0
  131. package/packages/piagent-core/extensions/context-import-links.js +296 -0
  132. package/packages/piagent-core/extensions/context-telemetry-integrity.js +65 -0
  133. package/packages/piagent-core/extensions/criterion-context-pack.js +422 -7
  134. package/packages/piagent-core/extensions/criterion-graph.js +78 -8
  135. package/packages/piagent-core/extensions/guard-types.ts +2 -0
  136. package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
  137. package/packages/piagent-core/extensions/piagent-guard.ts +233 -41
  138. package/packages/piagent-core/extensions/task-contract-view.js +17 -3
  139. package/packages/piagent-core/extensions/task-lifecycle.js +7 -2
  140. package/packages/piagent-core/extensions/task-state.js +40 -134
  141. package/packages/piagent-core/extensions/verification-intelligence.js +3 -1
  142. package/packages/piagent-core/extensions/workspace-evidence-roots.js +157 -0
  143. package/packages/piagent-core/package.json +1 -1
  144. package/packages/piagent-core/policies/base-policy.json +2 -0
  145. package/packages/piagent-core/runtime/context/context-delivery.ts +103 -0
  146. package/packages/piagent-core/runtime/context/context-delta-shadow.ts +53 -0
  147. package/packages/piagent-core/runtime/context/context-efficiency-format.ts +47 -0
  148. package/packages/piagent-core/runtime/context/context-evidence-qualification.ts +32 -0
  149. package/packages/piagent-core/runtime/context/prefix-telemetry.ts +47 -0
  150. package/packages/piagent-core/runtime/context/repository-memory-hints.ts +27 -0
  151. package/packages/piagent-core/runtime/hooks/agent-prompt-telemetry.ts +56 -0
  152. package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +162 -102
  153. package/packages/piagent-core/runtime/hooks/completion-hook.ts +5 -6
  154. package/packages/piagent-core/runtime/hooks/input-hook.ts +11 -1
  155. package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
  156. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +7 -2
  157. package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
  158. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +84 -102
  159. package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +4 -2
  160. package/packages/piagent-core/runtime/inspection/activity-recovery.ts +104 -0
  161. package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +1 -1
  162. package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
  163. package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +53 -0
  164. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +34 -6
  165. package/packages/piagent-core/runtime/model/authenticated-catalog.ts +17 -9
  166. package/packages/piagent-core/runtime/model/openai-codex-reasoning.ts +83 -0
  167. package/packages/piagent-core/runtime/model/provider-wire-fingerprint.ts +238 -0
  168. package/packages/piagent-core/runtime/orchestration/role-policy.ts +1 -1
  169. package/packages/piagent-core/runtime/product/activity-inspector.ts +7 -2
  170. package/packages/piagent-core/runtime/product/efficiency-metrics.ts +4 -2
  171. package/packages/piagent-core/runtime/quality/exact-output-contract.ts +2 -1
  172. package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
  173. package/packages/piagent-core/runtime/quality/performance-assurance.ts +6 -2
  174. package/packages/piagent-core/runtime/recovery/edit-recovery-context.ts +113 -0
  175. package/packages/piagent-core/runtime/recovery/edit-recovery-delivery.ts +42 -0
  176. package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +55 -2
  177. package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
  178. package/packages/piagent-core/runtime/registration/context-commands.ts +66 -5
  179. package/packages/piagent-core/runtime/registration/policy-tools.ts +46 -11
  180. package/packages/piagent-core/runtime/registration/task-completion-tools.ts +49 -16
  181. package/packages/piagent-core/runtime/registration/task-start-guidance.ts +163 -0
  182. package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
  183. package/packages/piagent-core/runtime/registration/task-start-tool.ts +67 -62
  184. package/packages/piagent-core/runtime/session/runtime-state.ts +154 -2
  185. package/packages/piagent-core/runtime/session/system-prompt.ts +4 -4
  186. package/packages/piagent-core/runtime/tools/apply-patch-tool.ts +500 -0
  187. package/packages/piagent-core/runtime/trajectory/trajectory-runtime.ts +2 -1
  188. package/packages/piagent-core/runtime/workflows/task-intake.ts +43 -2
  189. package/packages/piagent-core/security/sensitive-data.js +13 -218
  190. package/packages/piagent-core/security/sensitive-project-file.js +42 -0
  191. package/packages/piagent-core/security/sensitive-source-expression.js +330 -0
  192. package/packages/piagent-core/security/sensitive-source-formats.js +394 -0
  193. package/packages/piagent-core/security/sensitive-text.js +234 -0
  194. package/packages/piagent-core/subagents/piagent-worker.md +1 -1
  195. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-CxeZqTte.js} +1 -1
  196. package/packages/piagent-webui/dist/client/assets/index-DynIMe8I.js +176 -0
  197. package/packages/piagent-webui/dist/client/index.html +1 -1
  198. package/packages/piagent-webui/gateway/gateway-events.ts +109 -5
  199. package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
  200. package/packages/piagent-webui/gateway/gateway-session-stream.ts +93 -7
  201. package/packages/piagent-webui/gateway/session-command-controller.ts +18 -3
  202. package/packages/piagent-webui/gateway/session-effective-options.ts +47 -0
  203. package/packages/piagent-webui/gateway/session-live-state.ts +21 -0
  204. package/packages/piagent-webui/gateway/session-operation-start.ts +5 -1
  205. package/packages/piagent-webui/gateway/session-operation-watchdog.ts +158 -0
  206. package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
  207. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +139 -124
  208. package/packages/piagent-webui/server/loopback-server.ts +8 -0
  209. package/packages/piagent-webui/server/transcript-projection.ts +22 -4
  210. package/packages/piagent-webui/shared/text-visibility.ts +10 -0
  211. package/schemas/piagent-webui/catalog-v1.json +9 -0
  212. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +53 -3
  213. package/schemas/piagent-webui/session-live-state-v1.schema.json +76 -0
  214. package/schemas/task-contract.schema.json +12 -1
  215. package/scripts/benchmark-runner-core.mjs +355 -359
  216. package/scripts/benchmark-runner-finalization.mjs +309 -0
  217. package/scripts/benchmark-runner-host-readiness.mjs +94 -0
  218. package/scripts/benchmark-runner-support.mjs +200 -0
  219. package/scripts/benchmark-session.mjs +95 -41
  220. package/scripts/configure-model-scope.sh +4 -3
  221. package/scripts/helper-evaluation.mjs +1 -1
  222. package/scripts/install-global.sh +2 -2
  223. package/scripts/pi-usage-history.mjs +37 -10
  224. package/scripts/setup.sh +2 -2
  225. package/scripts/verify-local.sh +5 -1
  226. package/templates/global/settings.json +2 -1
  227. package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.js +0 -174
@@ -2,7 +2,6 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { execFileSync } from "node:child_process";
5
-
6
5
  import {
7
6
  contextIndexExcludeDigest,
8
7
  contextIndexExcludePolicyVersion,
@@ -11,9 +10,13 @@ import {
11
10
  import { matchesAnyPath, matchesProtectedPath } from "./policy-core.js";
12
11
  import { appendJsonlBounded, readJsonlTail } from "./state-retention.js";
13
12
  import { ensurePrivateStateDirectory, resolveLocalStatePath } from "./local-state-path.js";
14
-
13
+ import { contextMetricPartition } from "./context-efficiency-metrics.js";
14
+ import { writeContextEfficiencyReport } from "./context-efficiency-report.js";
15
+ import { inspectContextTelemetryIntegrity } from "./context-telemetry-integrity.js";
16
+ import { directExplicitImportLinks, extractJavaScriptModuleImports } from "./context-import-links.js";
17
+ import { redactSensitiveProjectFileText } from "../security/sensitive-data.js";
15
18
  const INDEX_SCHEMA_VERSION = 2;
16
- const TELEMETRY_SCHEMA_VERSION = 1;
19
+ const TELEMETRY_SCHEMA_VERSION = 2;
17
20
  const RRF_K = 60;
18
21
  const DEFAULT_MAX_FILE_BYTES = 512 * 1024;
19
22
  const DEFAULT_MAX_FILES = 8_000;
@@ -75,6 +78,10 @@ function clampInteger(value, fallback, min, max) {
75
78
  return Math.max(min, Math.min(max, Math.trunc(number)));
76
79
  }
77
80
 
81
+ function ratio(numerator, denominator) {
82
+ return denominator > 0 ? numerator / denominator : 0;
83
+ }
84
+
78
85
  function requireExplicitExcludePatterns(options, operation) {
79
86
  if (!Array.isArray(options?.excludePatterns)) {
80
87
  throw new TypeError(`${operation} requires an explicit excludePatterns array`);
@@ -258,7 +265,7 @@ function isTextBuffer(buffer) {
258
265
  return sample.length === 0 || suspicious / sample.length < 0.02;
259
266
  }
260
267
 
261
- function shouldIndexPath(relativePath, options) {
268
+ export function shouldIndexPath(relativePath, options = {}) {
262
269
  const rel = normalizeRelative(relativePath);
263
270
  if (!rel || path.isAbsolute(rel) || rel.startsWith("../")) return false;
264
271
  const segments = rel.split("/");
@@ -417,12 +424,12 @@ function extractSymbols(text, language) {
417
424
  return symbols;
418
425
  }
419
426
 
420
- function extractImports(text, language) {
427
+ function extractImports(text, language, filePath = "") {
421
428
  const lines = text.split(/\r?\n/);
422
429
  const imports = [];
423
430
  const patterns = [];
424
431
  if (["typescript", "javascript", "svelte", "vue"].includes(language)) {
425
- patterns.push(/\bfrom\s+["']([^"']+)["']/, /\brequire\(\s*["']([^"']+)["']\s*\)/, /\bimport\(\s*["']([^"']+)["']\s*\)/);
432
+ return extractJavaScriptModuleImports(text, { embedded: ["svelte", "vue"].includes(language), jsx: /\.[jt]sx$/i.test(filePath) });
426
433
  } else if (language === "python") {
427
434
  patterns.push(/^\s*from\s+([A-Za-z_][\w.]*)\s+import\b/, /^\s*import\s+([A-Za-z_][\w.]*)/);
428
435
  } else if (["java", "kotlin", "scala"].includes(language)) {
@@ -627,7 +634,7 @@ export async function buildContextIndexV2(cwd, options = {}) {
627
634
  for (const item of changed) {
628
635
  const language = languageForPath(item.relativePath);
629
636
  const symbols = extractSymbols(item.text, language);
630
- const imports = extractImports(item.text, language);
637
+ const imports = extractImports(item.text, language, item.relativePath);
631
638
  insertFile.run(item.relativePath, item.contentHash, item.stat.size, item.stat.mtimeMs, language, indexedAt);
632
639
  insertFts.run(item.relativePath, item.text, symbols.map((symbol) => symbol.name).join(" "));
633
640
  for (const symbol of symbols) {
@@ -859,10 +866,13 @@ function retrievalFeedback(cwd) {
859
866
  } catch {
860
867
  return {
861
868
  rows: [],
869
+ observedSelected: 0,
862
870
  selected: 0,
863
871
  used: 0,
864
872
  unused: 0,
865
- utilizationRate: 0
873
+ utilizationRate: 0,
874
+ fallbackRereads: 0,
875
+ fallbackRereadRate: 0
866
876
  };
867
877
  }
868
878
  const cached = retrievalFeedbackCache.get(cwd);
@@ -871,33 +881,61 @@ function retrievalFeedback(cwd) {
871
881
  const events = readContextTelemetry(cwd, { limit: 12_000, maxBytes: 12 * 1024 * 1024 });
872
882
  const selectedCounts = new Map();
873
883
  const usedCounts = new Map();
874
- const latestPackBySession = new Map();
875
- let selected = 0;
876
- let used = 0;
884
+ const fallbackReadCounts = new Map();
885
+ const latestPackByPartition = new Map();
886
+ const taskByTurn = new Map(events.filter((event) => event.event === "turn_task_bound" && typeof event.sessionId === "string" && typeof event.turnId === "string" && typeof event.taskRunId === "string")
887
+ .map((event) => [`${event.sessionId}\0${event.turnId}`, event.taskRunId]));
888
+ let observedSelected = 0, selected = 0, used = 0, fallbackRereads = 0;
889
+
890
+ const eventPaths = (event) => [...new Set([
891
+ ...(Array.isArray(event.selectedPaths) ? event.selectedPaths : []),
892
+ ...(Array.isArray(event.selectedItems) ? event.selectedItems.map((item) => item?.path) : [])
893
+ ].map(normalizeRelative).filter((filePath) => shouldIndexPath(filePath, {})))];
894
+ const successfulMutationPaths = (event) => {
895
+ if (event.event !== "tool_result" || event.isError === true) return [];
896
+ const explicit = [event.changedPaths, event.mutationPaths, event.targetPaths]
897
+ .find((value) => Array.isArray(value));
898
+ if (explicit) return [...new Set(explicit.map(normalizeRelative).filter(Boolean))];
899
+ // These tools have direct file-mutation semantics. Shell and opaque MCP
900
+ // results are intentionally excluded unless they carry explicit changed
901
+ // paths; a zero exit code alone is not mutation evidence.
902
+ const toolName = String(event.toolName ?? "").toLowerCase();
903
+ if (!["write", "edit", "apply_patch", "patch"].includes(toolName)) return [];
904
+ const targetPath = normalizeRelative(event.targetPath);
905
+ return targetPath ? [targetPath] : [];
906
+ };
877
907
 
878
908
  for (const event of events) {
879
- const sessionId = typeof event.sessionId === "string" ? event.sessionId : "";
880
- if (event.event === "context_pack" && sessionId) {
881
- const selectedPaths = [...new Set(
882
- (Array.isArray(event.selectedPaths) ? event.selectedPaths : [])
883
- .map(normalizeRelative)
884
- .filter((filePath) => shouldIndexPath(filePath, {}))
885
- )];
886
- const pack = { selectedPaths: new Set(selectedPaths), usedPaths: new Set() };
887
- latestPackBySession.set(sessionId, pack);
909
+ const partition = contextMetricPartition(event, taskByTurn);
910
+ if (event.event === "context_pack_injected") {
911
+ const selectedPaths = eventPaths(event);
912
+ observedSelected += selectedPaths.length;
913
+ if (!partition) continue;
914
+ const pack = { selectedPaths: new Set(selectedPaths), usedPaths: new Set(), fallbackReadPaths: new Set() };
915
+ latestPackByPartition.set(partition, pack);
888
916
  for (const filePath of selectedPaths) {
889
917
  selectedCounts.set(filePath, (selectedCounts.get(filePath) ?? 0) + 1);
890
918
  selected += 1;
891
919
  }
892
920
  continue;
893
921
  }
894
- if (event.event !== "tool_call" || !sessionId) continue;
922
+ if (!partition) continue;
923
+ const pack = latestPackByPartition.get(partition);
924
+ if (!pack) continue;
895
925
  const targetPath = normalizeRelative(event.targetPath);
896
- const pack = latestPackBySession.get(sessionId);
897
- if (!targetPath || !pack?.selectedPaths.has(targetPath) || pack.usedPaths.has(targetPath)) continue;
898
- pack.usedPaths.add(targetPath);
899
- usedCounts.set(targetPath, (usedCounts.get(targetPath) ?? 0) + 1);
900
- used += 1;
926
+ if (event.event === "tool_call" && ["read", "grep", "find", "ls"].includes(event.toolName)
927
+ && targetPath && pack.selectedPaths.has(targetPath) && !pack.fallbackReadPaths.has(targetPath)) {
928
+ pack.fallbackReadPaths.add(targetPath);
929
+ fallbackReadCounts.set(targetPath, (fallbackReadCounts.get(targetPath) ?? 0) + 1);
930
+ fallbackRereads += 1;
931
+ continue;
932
+ }
933
+ for (const mutationPath of successfulMutationPaths(event)) {
934
+ if (!pack.selectedPaths.has(mutationPath) || pack.usedPaths.has(mutationPath)) continue;
935
+ pack.usedPaths.add(mutationPath);
936
+ usedCounts.set(mutationPath, (usedCounts.get(mutationPath) ?? 0) + 1);
937
+ used += 1;
938
+ }
901
939
  }
902
940
 
903
941
  const rows = [...usedCounts.entries()]
@@ -909,16 +947,20 @@ function retrievalFeedback(cwd) {
909
947
  path: filePath,
910
948
  selectedCount,
911
949
  usedCount,
950
+ fallbackRereadCount: fallbackReadCounts.get(filePath) ?? 0,
912
951
  feedbackScore: utilization * evidence
913
952
  };
914
953
  })
915
954
  .sort((left, right) => right.feedbackScore - left.feedbackScore || right.usedCount - left.usedCount || left.path.localeCompare(right.path));
916
955
  const value = {
917
956
  rows,
957
+ observedSelected,
918
958
  selected,
919
959
  used,
920
960
  unused: Math.max(0, selected - used),
921
- utilizationRate: ratio(used, selected)
961
+ utilizationRate: ratio(used, selected),
962
+ fallbackRereads,
963
+ fallbackRereadRate: ratio(fallbackRereads, selected)
922
964
  };
923
965
  retrievalFeedbackCache.set(cwd, { signature, value });
924
966
  return value;
@@ -941,15 +983,15 @@ export async function searchContextIndexV2(cwd, query, options = {}) {
941
983
  const allFiles = db.prepare("SELECT path, mtime_ms FROM files").all().filter((file) => pathAllowed(file.path));
942
984
  const explicitRows = [];
943
985
  for (const file of allFiles) {
986
+ const normalizedFile = file.path.toLowerCase();
944
987
  const basename = path.posix.basename(file.path).toLowerCase();
945
988
  const matched = explicitPaths.some((candidate) => {
946
989
  const normalized = candidate.toLowerCase();
947
- return file.path.toLowerCase() === normalized || file.path.toLowerCase().endsWith(`/${normalized}`) || basename === path.posix.basename(normalized);
990
+ return normalizedFile === normalized || (!normalized.includes("/") && basename === normalized);
948
991
  });
949
992
  if (matched) explicitRows.push({ path: file.path, exact: true });
950
993
  }
951
994
  addRankedList(scores, explicitRows, "explicit", 3);
952
-
953
995
  let lexicalRows = [];
954
996
  if (terms.length > 0) {
955
997
  lexicalRows = db.prepare(`
@@ -958,7 +1000,6 @@ export async function searchContextIndexV2(cwd, query, options = {}) {
958
1000
  `).all(ftsQuery(terms), Math.max(limit * 4, 30)).filter((row) => pathAllowed(row.path));
959
1001
  addRankedList(scores, lexicalRows, "lexical", 1.5);
960
1002
  }
961
-
962
1003
  const symbolRows = [];
963
1004
  const symbolStatement = db.prepare(`
964
1005
  SELECT file_path, name, kind, line, end_line, signature
@@ -973,13 +1014,11 @@ export async function searchContextIndexV2(cwd, query, options = {}) {
973
1014
 
974
1015
  const changedRows = gitChangedFiles(cwd).map((filePath) => ({ path: filePath })).filter((row) => scores.has(row.path));
975
1016
  addRankedList(scores, changedRows, "git-change", 0.8);
976
-
977
1017
  const testRows = allFiles
978
1018
  .filter((file) => /(^|\/)(?:test|tests|__tests__)(\/|$)|(?:\.test|\.spec|_test)\./i.test(file.path))
979
1019
  .filter((file) => terms.some((term) => file.path.toLowerCase().includes(term)))
980
1020
  .map((file) => ({ path: file.path }));
981
1021
  addRankedList(scores, testRows, "test", 1.1);
982
-
983
1022
  // Feedback is deliberately positive-only and weak. A file is boosted only
984
1023
  // after a prior context pack selected it and the same session actually used
985
1024
  // it. New files and unused historical candidates are never penalized.
@@ -987,12 +1026,13 @@ export async function searchContextIndexV2(cwd, query, options = {}) {
987
1026
  addRankedList(scores, feedbackRows, "feedback", 0.45);
988
1027
 
989
1028
  const graphSeeds = new Map([...scores.entries()].map(([filePath, value]) => [filePath, value.score]));
990
- const importRows = db.prepare("SELECT file_path, target_path FROM imports WHERE target_path IS NOT NULL")
1029
+ const importRows = db.prepare("SELECT file_path, specifier, target_path FROM imports WHERE target_path IS NOT NULL")
991
1030
  .all()
992
1031
  .filter((row) => pathAllowed(row.file_path) && pathAllowed(row.target_path));
1032
+ const directLinks = directExplicitImportLinks(importRows, explicitRows);
1033
+ addRankedList(scores, directLinks.rankingRows, "import", 1.8);
993
1034
  const graphRows = personalizedPageRank(importRows, graphSeeds).slice(0, Math.max(limit * 3, 30));
994
1035
  addRankedList(scores, graphRows, "graph", 1);
995
-
996
1036
  const ranked = [...scores.values()]
997
1037
  .filter((candidate) => (
998
1038
  shouldIndexPath(candidate.path, { excludePatterns })
@@ -1012,6 +1052,7 @@ export async function searchContextIndexV2(cwd, query, options = {}) {
1012
1052
  path: candidate.path,
1013
1053
  score: Number(candidate.score.toFixed(6)),
1014
1054
  sources: [...candidate.sources],
1055
+ links: directLinks.byPath.get(candidate.path) ?? [],
1015
1056
  symbols
1016
1057
  };
1017
1058
  });
@@ -1105,31 +1146,79 @@ export async function buildContextPack(cwd, query, options = {}) {
1105
1146
  }
1106
1147
 
1107
1148
  for (const result of rankedResults) {
1108
- let text;
1149
+ let text, fileContentHash, sanitizedContentDigest, sensitiveContentRedacted = false;
1150
+ let safeResult = result;
1109
1151
  try {
1110
1152
  const file = projectFileInfo(cwd, result.path, projectRoot, directoryCache);
1111
1153
  if (!file) continue;
1112
- text = fs.readFileSync(file.absolute, "utf8");
1154
+ const bytes = fs.readFileSync(file.absolute);
1155
+ const sanitized = redactSensitiveProjectFileText(result.path, bytes.toString("utf8"));
1156
+ if (!sanitized.lineCountPreserved) continue;
1157
+ text = sanitized.text;
1158
+ sensitiveContentRedacted = sanitized.redacted;
1159
+ safeResult = {
1160
+ ...result,
1161
+ symbols: result.symbols.map((symbol) => ({
1162
+ ...symbol,
1163
+ signature: redactSensitiveProjectFileText(result.path, String(symbol.signature ?? "")).text
1164
+ }))
1165
+ };
1166
+ sanitizedContentDigest = sanitized.redacted
1167
+ ? `context-sanitized-file-v1:${sha256(sanitized.text)}`
1168
+ : undefined;
1169
+ fileContentHash = sanitizedContentDigest ?? `context-file-v1:${sha256(bytes)}`;
1113
1170
  } catch {
1114
1171
  continue;
1115
1172
  }
1116
- const snippets = lineRangesForFile(text, result, search.terms, includeCode);
1173
+ const snippets = lineRangesForFile(text, safeResult, search.terms, includeCode);
1117
1174
  const body = snippets.length > 0
1118
1175
  ? snippets.map((snippet) => `lines ${snippet.start}-${snippet.end}\n${snippet.text}`).join("\n\n")
1119
- : result.symbols.slice(0, 8).map((symbol) => `line ${symbol.line}: ${symbol.signature}`).join("\n");
1176
+ : safeResult.symbols.slice(0, 8).map((symbol) => `line ${symbol.line}: ${symbol.signature}`).join("\n");
1120
1177
  const itemText = [`### ${result.path}`, `why: ${result.sources.join(", ")}`, body].filter(Boolean).join("\n");
1121
1178
  const tokens = estimateContextTokens(itemText);
1122
- const item = { ...result, text: itemText, estimatedTokens: tokens, truncated: false };
1179
+ const itemRanges = snippets.length > 0
1180
+ ? snippets.map(({ start, end }) => ({ start, end }))
1181
+ : safeResult.symbols.slice(0, 8).map((symbol) => ({ start: symbol.line, end: symbol.endLine ?? symbol.line }));
1182
+ const representation = snippets.length > 0 ? "snippet" : body ? "symbol-map" : "navigation";
1183
+ const item = {
1184
+ ...safeResult,
1185
+ text: itemText,
1186
+ estimatedTokens: tokens,
1187
+ truncated: false,
1188
+ fileContentHash,
1189
+ ...(sanitizedContentDigest ? { sanitizedContentDigest } : {}),
1190
+ payloadHash: `context-payload-v1:${sha256(itemText)}`,
1191
+ representation,
1192
+ ranges: itemRanges,
1193
+ generation: 1,
1194
+ sensitiveContentRedacted
1195
+ };
1123
1196
  if (estimateContextTokens(renderPack([...selected, item])) > budgetTokens) {
1124
1197
  if (selected.length === 0) {
1125
1198
  const remaining = budgetTokens - estimateContextTokens(renderPack([]));
1126
1199
  let chars = Math.max(0, remaining * 4 - 16);
1127
- let truncated = { ...result, text: itemText.slice(0, chars), estimatedTokens: remaining, truncated: true };
1200
+ let truncated = {
1201
+ ...safeResult,
1202
+ text: itemText.slice(0, chars),
1203
+ estimatedTokens: remaining,
1204
+ truncated: true,
1205
+ fileContentHash,
1206
+ ...(sanitizedContentDigest ? { sanitizedContentDigest } : {}),
1207
+ payloadHash: "",
1208
+ representation: `${representation}-truncated`,
1209
+ ranges: [],
1210
+ generation: 1,
1211
+ sensitiveContentRedacted
1212
+ };
1128
1213
  while (chars > 0 && estimateContextTokens(renderPack([truncated])) > budgetTokens) {
1129
1214
  chars = Math.max(0, chars - 16);
1130
1215
  truncated = { ...truncated, text: itemText.slice(0, chars) };
1131
1216
  }
1132
- if (chars > 0) selected.push(truncated);
1217
+ if (chars > 0) selected.push({
1218
+ ...truncated,
1219
+ estimatedTokens: estimateContextTokens(truncated.text),
1220
+ payloadHash: `context-payload-v1:${sha256(truncated.text)}`
1221
+ });
1133
1222
  }
1134
1223
  continue;
1135
1224
  }
@@ -1203,10 +1292,10 @@ export async function buildTestImpact(cwd, changedFiles = [], options = {}) {
1203
1292
  export function appendContextTelemetry(cwd, event) {
1204
1293
  const paths = contextEnginePaths(cwd);
1205
1294
  const record = {
1206
- schemaVersion: TELEMETRY_SCHEMA_VERSION,
1207
1295
  source: "piagent",
1208
- recordedAt: nowIso(),
1209
- ...event
1296
+ ...event,
1297
+ schemaVersion: TELEMETRY_SCHEMA_VERSION, telemetrySource: "piagent",
1298
+ recordedAt: typeof event?.recordedAt === "string" ? event.recordedAt : nowIso()
1210
1299
  };
1211
1300
  appendJsonlBounded(paths.telemetry, record, { maxBytes: MAX_TELEMETRY_FILE_BYTES, mode: 0o600, projectRoot: cwd });
1212
1301
  return record;
@@ -1219,113 +1308,27 @@ export function readContextTelemetry(cwd, options = {}) {
1219
1308
  return readJsonlTail(target, { limit, maxBytes, projectRoot: cwd });
1220
1309
  }
1221
1310
 
1222
- function ratio(numerator, denominator) {
1223
- return denominator > 0 ? numerator / denominator : 0;
1311
+ /** Fail-closed telemetry inspection for benchmark evidence. */
1312
+ export function inspectContextTelemetry(cwd, options = {}) {
1313
+ return inspectContextTelemetryIntegrity(cwd, contextEnginePaths(cwd).telemetry, options);
1224
1314
  }
1225
-
1226
1315
  export function buildContextEfficiencyReport(cwd, options = {}) {
1227
1316
  const events = readContextTelemetry(cwd, options);
1228
- const prompts = events.filter((event) => event.event === "agent_prompt");
1229
- const toolCalls = events.filter((event) => event.event === "tool_call");
1230
- const toolResults = events.filter((event) => event.event === "tool_result");
1231
- const packs = events.filter((event) => event.event === "context_pack");
1232
- const compactions = events.filter((event) => event.event === "session_compact");
1233
- const seenReads = new Set();
1234
- let duplicateReads = 0;
1235
- let readCalls = 0;
1236
- for (const event of toolCalls) {
1237
- if (!["read", "grep", "find", "ls"].includes(event.toolName)) continue;
1238
- readCalls += 1;
1239
- const key = `${event.toolName}:${event.targetHash ?? event.inputHash ?? ""}`;
1240
- if (seenReads.has(key)) duplicateReads += 1;
1241
- else seenReads.add(key);
1242
- }
1243
- const outputChars = toolResults.reduce((sum, event) => sum + Number(event.outputChars ?? 0), 0);
1244
- const duplicateOutputChars = toolResults.reduce((sum, event) => sum + (event.repeated ? Number(event.outputChars ?? 0) : 0), 0);
1245
- const averageActiveTools = prompts.length > 0
1246
- ? prompts.reduce((sum, event) => sum + Number(event.activeTools ?? 0), 0) / prompts.length
1247
- : 0;
1248
- const averageSystemPromptTokens = prompts.length > 0
1249
- ? prompts.reduce((sum, event) => sum + Number(event.systemPromptTokens ?? 0), 0) / prompts.length
1250
- : 0;
1251
- const averageToolSchemaTokens = prompts.length > 0
1252
- ? prompts.reduce((sum, event) => sum + Number(event.toolSchemaTokens ?? 0), 0) / prompts.length
1253
- : 0;
1254
- const lowConfidencePacks = packs.filter((event) => ["none", "low"].includes(event.confidence)).length;
1255
- const feedback = retrievalFeedback(cwd);
1256
- const duplicateReadRate = ratio(duplicateReads, readCalls);
1257
- const duplicateOutputRate = ratio(duplicateOutputChars, outputChars);
1258
- const schemaShare = ratio(averageToolSchemaTokens, averageSystemPromptTokens);
1259
- const lowConfidenceRate = ratio(lowConfidencePacks, packs.length);
1260
- const activeToolPenalty = Math.max(0, Math.min(1, (averageActiveTools - 12) / 24));
1261
- const wasteScore = Math.round(100 * (
1262
- duplicateReadRate * 0.3
1263
- + duplicateOutputRate * 0.25
1264
- + Math.min(1, schemaShare * 3) * 0.2
1265
- + lowConfidenceRate * 0.15
1266
- + activeToolPenalty * 0.1
1267
- ));
1268
- const recommendations = [];
1269
- if (duplicateReadRate > 0.2) recommendations.push("Repeated reads are high; reuse the current working set before searching again.");
1270
- if (duplicateOutputRate > 0.15) recommendations.push("Repeated tool output is high; prefer delta results and narrower verification.");
1271
- if (schemaShare > 0.15 || averageActiveTools > 20) recommendations.push("Tool surface is large; activate only the workflow groups needed for the next turn.");
1272
- if (lowConfidenceRate > 0.4) recommendations.push("Retrieval confidence is low; rebuild the index or run one bounded finder pass.");
1273
- if (feedback.selected >= 4 && feedback.utilizationRate < 0.45) recommendations.push("Context-pack utilization is low; reduce pack breadth or improve task-specific ranking signals.");
1274
- if (recommendations.length === 0) recommendations.push("No dominant context waste signal was detected in the sampled events.");
1275
- const report = {
1276
- schemaVersion: 1,
1277
- source: "piagent",
1278
- generatedAt: nowIso(),
1279
- sample: {
1280
- events: events.length,
1281
- prompts: prompts.length,
1282
- toolCalls: toolCalls.length,
1283
- toolResults: toolResults.length,
1284
- contextPacks: packs.length,
1285
- compactions: compactions.length
1286
- },
1287
- metrics: {
1288
- averageActiveTools: Number(averageActiveTools.toFixed(2)),
1289
- averageSystemPromptTokens: Math.round(averageSystemPromptTokens),
1290
- averageToolSchemaTokens: Math.round(averageToolSchemaTokens),
1291
- toolSchemaShare: Number(schemaShare.toFixed(4)),
1292
- readCalls,
1293
- duplicateReads,
1294
- duplicateReadRate: Number(duplicateReadRate.toFixed(4)),
1295
- outputChars,
1296
- duplicateOutputChars,
1297
- duplicateOutputRate: Number(duplicateOutputRate.toFixed(4)),
1298
- lowConfidencePacks,
1299
- lowConfidenceRate: Number(lowConfidenceRate.toFixed(4)),
1300
- contextSelections: feedback.selected,
1301
- contextSelectionsUsed: feedback.used,
1302
- contextSelectionsUnused: feedback.unused,
1303
- contextUtilizationRate: Number(feedback.utilizationRate.toFixed(4)),
1304
- contextWasteScore: wasteScore
1305
- },
1306
- methodology: {
1307
- scoreRange: "0-100; lower is better",
1308
- weights: {
1309
- duplicateReads: 0.3,
1310
- duplicateOutput: 0.25,
1311
- toolSchemaShare: 0.2,
1312
- lowConfidenceRetrieval: 0.15,
1313
- activeToolExcess: 0.1
1314
- },
1315
- retrievalFeedback: "Positive-only reranking: a path receives a weak boost only when a prior pack selected it and the same session later used it. Unseen and unused paths are not penalized.",
1316
- note: "This is an operational signal, not a quality verdict. Compare it with task acceptance and verification results."
1317
- },
1318
- recommendations
1319
- };
1320
- const paths = contextEnginePaths(cwd);
1321
- ensurePrivateStateDirectory(cwd, paths.root, "Context report directory");
1322
- const reportPath = resolveLocalStatePath(cwd, paths.report, { label: "Context efficiency report" });
1323
- fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, { mode: 0o600 });
1324
- return report;
1317
+ return writeContextEfficiencyReport(cwd, events, {
1318
+ contextEnginePaths: () => contextEnginePaths(cwd),
1319
+ nowIso,
1320
+ retrievalFeedback: () => retrievalFeedback(cwd)
1321
+ });
1322
+ }
1323
+ function canonicalJson(value) {
1324
+ return JSON.stringify(value, (_key, nested) => {
1325
+ if (!nested || typeof nested !== "object" || Array.isArray(nested)) return nested;
1326
+ return Object.fromEntries(Object.keys(nested).sort().map((key) => [key, nested[key]]));
1327
+ });
1325
1328
  }
1326
1329
 
1327
1330
  export function toolResultFingerprint(toolName, input, content) {
1328
- const inputHash = sha256(JSON.stringify(input ?? {}));
1331
+ const inputHash = sha256(canonicalJson(input ?? {}));
1329
1332
  const text = Array.isArray(content)
1330
1333
  ? content.filter((block) => block && typeof block === "object" && block.type === "text").map((block) => block.text ?? "").join("\n")
1331
1334
  : String(content ?? "");
@@ -0,0 +1,48 @@
1
+ const PLANNED_CONTEXT_REASON = /^criterion-[a-z0-9_-]+(?:\s|$)/i;
2
+ const RUNTIME_OBSERVED_READ_REASONS = new Set([
3
+ "Runtime observed successful source read.",
4
+ "Runtime observed successful document read."
5
+ ]);
6
+ const RUNTIME_CONFIRMED_DELIVERY_REASONS = new Set([
7
+ "Runtime confirmed delivery of a bounded Context Engine navigation pack.",
8
+ "Runtime confirmed delivery of criterion-selected context.",
9
+ "Runtime confirmed delivery of a /context pack message.",
10
+ "Runtime confirmed delivery of an explicit Context Engine tool pack."
11
+ ]);
12
+
13
+ export function isPlannedContextEntry(entry) {
14
+ return Boolean(
15
+ entry
16
+ && typeof entry.path === "string"
17
+ && typeof entry.reason === "string"
18
+ && PLANNED_CONTEXT_REASON.test(entry.reason.trim())
19
+ );
20
+ }
21
+
22
+ export function isRuntimeOwnedContextEvidenceEntry(entry) {
23
+ if (!entry || typeof entry !== "object") return false;
24
+ const reason = typeof entry.reason === "string" ? entry.reason.trim() : "";
25
+ return RUNTIME_OBSERVED_READ_REASONS.has(reason) || RUNTIME_CONFIRMED_DELIVERY_REASONS.has(reason);
26
+ }
27
+
28
+ export function isDurableContextEvidenceEntry(entry) {
29
+ return Boolean(
30
+ entry
31
+ && typeof entry.path === "string"
32
+ && entry.path.trim().length > 0
33
+ && isRuntimeOwnedContextEvidenceEntry(entry)
34
+ );
35
+ }
36
+
37
+ export function durableContextEvidenceEntries(taskOrEntries) {
38
+ const entries = Array.isArray(taskOrEntries)
39
+ ? taskOrEntries
40
+ : Array.isArray(taskOrEntries?.contextManifest)
41
+ ? taskOrEntries.contextManifest
42
+ : [];
43
+ return entries.filter(isDurableContextEvidenceEntry);
44
+ }
45
+
46
+ export function hasDurableContextEvidence(taskOrEntries) {
47
+ return durableContextEvidenceEntries(taskOrEntries).length > 0;
48
+ }