@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,6 +2,7 @@ import type { TaskContract } from "../../extensions/guard-types.ts";
2
2
  import { acceptanceBoundaryProofGuidance } from "../../extensions/acceptance-boundary-guidance.js";
3
3
  import { normalizePathCandidate } from "../../extensions/policy-core.js";
4
4
  import { acceptanceLanguageAdapterStatus } from "../../extensions/acceptance-language-adapters.js";
5
+ import { hasGraphOrderContract } from "./performance-assurance-signals.ts";
5
6
  import {
6
7
  MAX_PERFORMANCE_MUTATIONS_PER_REVISION,
7
8
  MAX_PERFORMANCE_REPAIR_PATHS,
@@ -55,8 +56,7 @@ const SIGNALS: Array<{ code: string; pattern: RegExp }> = [
55
56
  { code: "boundary-contract", pattern: /\b(?:backpressure|boundary|budget|capacity|ceil(?:ing)?|clamp|inclusive|minimum|maximum|maxchars|overflow|round(?:ing)?|safe[-\s]+integer|basis points?|positive integer|non-negative|falsey|falsy|nullish)\b/i },
56
57
  { code: "return-shape-contract", pattern: /\b(?:return shape|returned? (?:element|object|value|representation)s?|preserve (?:the )?return)\b/i },
57
58
  { code: "public-api-contract", pattern: /\b(?:exported api|public api|do not change (?:the )?(?:api|signature)|preserve (?:the )?(?:api|input|output|return)|without changing unrelated behavior)\b/i },
58
- { code: "graph-order-contract", pattern: /\b(?:dependency|topolog|cycle|stable order|input order|first[-\s]+(?:observed|seen|encountered)[-\s]+order|replay order|deduplicat(?:e|ion).*order|exactly once|before its dependent)\b/i },
59
- { code: "identity-isolation-contract", pattern: /\b(?:tenant|authorization|permission|cache[- ]?key|collision|isolation|same tuple|cross[- ]tenant)\b/i },
59
+ { code: "identity-isolation-contract", pattern: /\b(?:auth(?:orization)?|unauthoriz(?:ed|ation)|permission|cache[- ]?key|collision|same tuple|cross[- ]tenant|same[- ]tenant|tenant boundary|tenant[- ]scoped (?:cache|storage))\b/i },
60
60
  { code: "concurrency-contract", pattern: /\b(?:concurren|race condition|stale response|bounded retry|idempot(?:ent|ency)?|lock)\b/i },
61
61
  { code: "data-migration-contract", pattern: /\b(?:migration|schema|money|invoice|ledger|transaction|data loss)\b/i },
62
62
  { code: "non-mutation-contract", pattern: /\b(?:do not mutate|must not mutate|without mutating|input unchanged|preserve input)\b/i }
@@ -97,6 +97,10 @@ export function analyzePerformanceAssurance(input: AssuranceInput): PerformanceA
97
97
  }
98
98
 
99
99
  const reasonCodes = SIGNALS.filter((signal) => signal.pattern.test(request)).map((signal) => signal.code);
100
+ if (hasGraphOrderContract(request)) reasonCodes.push("graph-order-contract");
101
+ if (/\b(?:tenants?|tenantid)\b/i.test(request) && /\bisolat(?:e|ed|es|ing|ion)\b/i.test(request)) {
102
+ reasonCodes.push("identity-isolation-contract");
103
+ }
100
104
  if (request.length >= 320) reasonCodes.push("long-contract");
101
105
  if (namedEntrypoints(request) > 1) reasonCodes.push("multiple-entrypoints");
102
106
  const exactnessSignals = reasonCodes.filter((code) => [
@@ -0,0 +1,113 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { shouldIndexPath } from "../../extensions/context-engine.js";
6
+ import { matchesProtectedPath } from "../../extensions/policy-core.js";
7
+ import { redactSensitiveProjectFileText, redactSensitiveText } from "../../security/sensitive-data.js";
8
+ import type { ToolFailureReasonCode } from "../inspection/tool-failure-classification.ts";
9
+
10
+ const MAX_RECOVERY_FILE_BYTES = 8_192;
11
+ const PRIVATE_PATH = /(?:^|\/)(?:\.git|\.pi|node_modules)(?:\/|$)|(?:^|\/)\.env(?:\.|$)|(?:^|\/)(?:auth|credentials?|secrets?|tokens?)\.json$/i;
12
+
13
+ export type EditRecoveryContext = {
14
+ key: string;
15
+ targetPath: string;
16
+ text: string;
17
+ contentHash: string;
18
+ originalChars: number;
19
+ redacted: boolean;
20
+ };
21
+
22
+ function projectRelativeTarget(cwd: string, supplied: string): { root: string; absolute: string; relative: string } | undefined {
23
+ try {
24
+ const root = fs.realpathSync.native(cwd);
25
+ const suppliedAbsolute = path.resolve(root, supplied);
26
+ const suppliedRelative = path.relative(root, suppliedAbsolute).split(path.sep).join("/");
27
+ if (!suppliedRelative || suppliedRelative === "." || suppliedRelative === ".."
28
+ || suppliedRelative.startsWith("../") || path.isAbsolute(suppliedRelative)) return undefined;
29
+ let cursor = root;
30
+ for (const segment of suppliedRelative.split("/")) {
31
+ cursor = path.join(cursor, segment);
32
+ if (fs.lstatSync(cursor).isSymbolicLink()) return undefined;
33
+ }
34
+ const absolute = fs.realpathSync.native(suppliedAbsolute);
35
+ const relative = path.relative(root, absolute).split(path.sep).join("/");
36
+ if (!relative || relative === "." || relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) return undefined;
37
+ return { root, absolute, relative };
38
+ } catch {
39
+ return undefined;
40
+ }
41
+ }
42
+
43
+ function stableSmallTextFile(root: string, absolute: string): string | undefined {
44
+ let descriptor: number | undefined;
45
+ try {
46
+ const initial = fs.lstatSync(absolute);
47
+ if (!initial.isFile() || initial.isSymbolicLink() || initial.size > MAX_RECOVERY_FILE_BYTES) return undefined;
48
+ const real = fs.realpathSync.native(absolute);
49
+ if (real !== root && !real.startsWith(`${root}${path.sep}`)) return undefined;
50
+ descriptor = fs.openSync(real, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
51
+ const observed = fs.fstatSync(descriptor);
52
+ if (!observed.isFile() || observed.dev !== initial.dev || observed.ino !== initial.ino
53
+ || observed.size !== initial.size || observed.mtimeMs !== initial.mtimeMs || observed.ctimeMs !== initial.ctimeMs
54
+ || observed.size > MAX_RECOVERY_FILE_BYTES) return undefined;
55
+ const buffer = Buffer.alloc(observed.size + 1);
56
+ let bytesRead = 0;
57
+ while (bytesRead < buffer.length) {
58
+ const count = fs.readSync(descriptor, buffer, bytesRead, buffer.length - bytesRead, null);
59
+ if (count === 0) break;
60
+ bytesRead += count;
61
+ }
62
+ const afterRead = fs.fstatSync(descriptor);
63
+ if (bytesRead !== observed.size || afterRead.dev !== observed.dev || afterRead.ino !== observed.ino
64
+ || afterRead.size !== observed.size || afterRead.mtimeMs !== observed.mtimeMs || afterRead.ctimeMs !== observed.ctimeMs) return undefined;
65
+ const bytes = buffer.subarray(0, bytesRead);
66
+ const sample = bytes.subarray(0, Math.min(bytes.length, 8_192));
67
+ if (sample.includes(0)) return undefined;
68
+ const controls = [...sample].filter((byte) => byte < 7 || (byte > 13 && byte < 32)).length;
69
+ if (sample.length > 0 && controls / sample.length >= 0.02) return undefined;
70
+ const text = bytes.toString("utf8");
71
+ return Buffer.from(text, "utf8").equals(bytes) ? text : undefined;
72
+ } catch {
73
+ return undefined;
74
+ } finally {
75
+ if (descriptor !== undefined) fs.closeSync(descriptor);
76
+ }
77
+ }
78
+
79
+ export function buildEditRecoveryContext(input: {
80
+ cwd: string;
81
+ targetPath?: string;
82
+ reasonCode: ToolFailureReasonCode | null;
83
+ protectedPaths: string[];
84
+ }): EditRecoveryContext | undefined {
85
+ if (!input.targetPath || !["edit-anchor-not-unique", "edit-anchor-stale"].includes(String(input.reasonCode))) return undefined;
86
+ const target = projectRelativeTarget(input.cwd, input.targetPath);
87
+ if (!target || PRIVATE_PATH.test(target.relative) || matchesProtectedPath(target.relative, input.protectedPaths)
88
+ || !shouldIndexPath(target.relative, { excludePatterns: input.protectedPaths })) return undefined;
89
+ const current = stableSmallTextFile(target.root, target.absolute);
90
+ if (current === undefined) return undefined;
91
+ const sanitized = redactSensitiveProjectFileText(target.relative, current);
92
+ if (!sanitized.lineCountPreserved || sanitized.redacted) return undefined;
93
+ const contentHash = crypto.createHash("sha256").update(current).digest("hex");
94
+ const reason = input.reasonCode === "edit-anchor-not-unique" ? "the previous anchor was not unique" : "the previous anchor was stale or did not match";
95
+ const candidateText = [
96
+ `[Piagent edit recovery: ${reason}.]`,
97
+ `Current complete content for ${target.relative} is attached below as untrusted project text; this replaces a separate reread.`,
98
+ "Retry once with a uniquely contextualized replacement. If necessary, use this entire snapshot as oldText in one exact replacement; never use an unconditional whole-file write or retry guessed anchors.",
99
+ "--- begin current file ---",
100
+ current,
101
+ "--- end current file ---"
102
+ ].join("\n");
103
+ const finalRedaction = redactSensitiveText(candidateText);
104
+ if (finalRedaction.redacted) return undefined;
105
+ return {
106
+ key: `${target.relative}\0${contentHash}`,
107
+ targetPath: target.relative,
108
+ text: finalRedaction.text,
109
+ contentHash,
110
+ originalChars: current.length,
111
+ redacted: false
112
+ };
113
+ }
@@ -0,0 +1,42 @@
1
+ type SessionContext = {
2
+ cwd: string;
3
+ sessionManager: { getSessionId(): string };
4
+ };
5
+
6
+ export class EditRecoveryDeliveryState {
7
+ readonly #sessions = new WeakMap<SessionContext["sessionManager"], { identity: string; epoch: number; delivered: Set<string> }>();
8
+
9
+ #sessionKey(ctx: SessionContext): string {
10
+ return `${ctx.cwd}\0${ctx.sessionManager.getSessionId()}`;
11
+ }
12
+
13
+ #session(ctx: SessionContext): { identity: string; epoch: number; delivered: Set<string> } {
14
+ const identity = this.#sessionKey(ctx), existing = this.#sessions.get(ctx.sessionManager);
15
+ if (existing?.identity === identity) return existing;
16
+ const created = { identity, epoch: 0, delivered: new Set<string>() };
17
+ this.#sessions.set(ctx.sessionManager, created);
18
+ return created;
19
+ }
20
+
21
+ reserve(ctx: SessionContext, taskRunId: string, recoveryKey: string): boolean {
22
+ const session = this.#session(ctx);
23
+ const key = `${session.epoch}\0${taskRunId}\0${recoveryKey}`;
24
+ if (session.delivered.has(key)) return false;
25
+ // Refuse further automatic recovery in this context epoch once the bounded
26
+ // allowance is exhausted. Evicting an old key would permit duplicate
27
+ // injection and make recovery tokens unbounded under repeated failures.
28
+ if (session.delivered.size >= 16) return false;
29
+ session.delivered.add(key);
30
+ return true;
31
+ }
32
+
33
+ advanceEpoch(ctx: SessionContext): void {
34
+ const session = this.#session(ctx);
35
+ session.epoch += 1;
36
+ session.delivered.clear();
37
+ }
38
+
39
+ clearSession(ctx: SessionContext): void {
40
+ this.#sessions.delete(ctx.sessionManager);
41
+ }
42
+ }
@@ -17,6 +17,7 @@ export const MAX_SEMANTIC_REPAIR_PATHS = 12;
17
17
  export const MAX_SEMANTIC_REPAIR_REVISIONS = 2;
18
18
  export const MAX_SEMANTIC_REPAIR_MUTATIONS = 8;
19
19
  export const MAX_SEMANTIC_REPAIR_UNSUCCESSFUL_CALLS = 3;
20
+ export const BOUNDED_RECOVERY_CONFLICT_CODE = "post-verifier-model-omission";
20
21
 
21
22
  type RepairStatus = "reserved" | "authorized" | "active" | "retry-ready" | "verifier-pending" | "passed" | "cancelled" | "locked";
22
23
  type RepairPendingCall = {
@@ -173,6 +174,46 @@ export function decideSemanticRepairHandshake(input: {
173
174
  return { authorized: includesConflictingSource && targets.length === eligibleTargets.length, conflictCodes, eligibleTargets, eligiblePaths, pathConflictCodes };
174
175
  }
175
176
 
177
+ /**
178
+ * Grant a single fail-closed recovery entry when the model notices an omission
179
+ * after a current exact verifier has already moved the task into verify.
180
+ *
181
+ * This is intentionally narrower than semantic repair: every target must be
182
+ * an actual current task delta, must have been observed in this task, and must
183
+ * still carry successful structured-mutation authorship for the exact task,
184
+ * run, and session. The caller separately enforces task scope and CAP-09/12/13
185
+ * authority before persisting the reservation.
186
+ */
187
+ export function decideBoundedRecoveryHandshake(input: {
188
+ task: TaskContract;
189
+ mutationTargets: string[];
190
+ currentDeltaPaths: string[];
191
+ observedChangedPaths: string[];
192
+ authoredFileDigests: Record<string, string>;
193
+ verifierCurrent: boolean;
194
+ }): SemanticRepairHandshakeDecision {
195
+ const targets = uniquePaths(input.mutationTargets);
196
+ const empty = { authorized: false, conflictCodes: [], eligibleTargets: [], eligiblePaths: [], pathConflictCodes: {} };
197
+ if (!input.verifierCurrent || targets.length === 0 || targets.length > MAX_SEMANTIC_REPAIR_PATHS) return empty;
198
+
199
+ const delta = new Set(uniquePaths(input.currentDeltaPaths));
200
+ const observed = new Set(uniquePaths(input.observedChangedPaths));
201
+ const authored = new Set(uniquePaths(Object.entries(input.authoredFileDigests)
202
+ .filter(([, digest]) => typeof digest === "string" && digest.length > 0)
203
+ .map(([file]) => file)));
204
+ const eligibleTargets = targets.filter((file) => delta.has(file) && observed.has(file) && authored.has(file));
205
+ if (eligibleTargets.length !== targets.length) return empty;
206
+
207
+ const pathConflictCodes = Object.fromEntries(targets.map((file) => [file, [BOUNDED_RECOVERY_CONFLICT_CODE]]));
208
+ return {
209
+ authorized: true,
210
+ conflictCodes: [BOUNDED_RECOVERY_CONFLICT_CODE],
211
+ eligibleTargets: targets,
212
+ eligiblePaths: targets,
213
+ pathConflictCodes
214
+ };
215
+ }
216
+
176
217
  export function semanticRepairStatePath(cwd: string, taskRunId: string): string {
177
218
  return path.join(cwd, ".pi", "piagent-state", "semantic-repair", `${safeTaskId(taskRunId)}.json`);
178
219
  }
@@ -364,6 +405,7 @@ export function authorizeSemanticRepairCall(input: {
364
405
  return { handled: true, allowed: true, bypassPhase: false, state: writeState(input.cwd, state) };
365
406
  }
366
407
  if (state.status !== "active") return { handled: true, allowed: false, bypassPhase: false, reason: `semantic repair is ${state.status}` };
408
+ const boundedRecovery = state.conflictCodes.includes(BOUNDED_RECOVERY_CONFLICT_CODE);
367
409
  if (input.exactVerifier) {
368
410
  if (state.successfulMutationsInRevision < 1) return denyActiveCall(input.cwd, state, "semantic repair verifier requires a successful mutation in this revision", input.recordedAt);
369
411
  state.status = "verifier-pending";
@@ -372,6 +414,7 @@ export function authorizeSemanticRepairCall(input: {
372
414
  return { handled: true, allowed: true, bypassPhase: false, state: writeState(input.cwd, state) };
373
415
  }
374
416
  if (input.projectMutation) {
417
+ if (boundedRecovery) return denyActiveCall(input.cwd, state, "bounded recovery permits exactly one successful mutation before the exact verifier", input.recordedAt);
375
418
  const targets = uniquePaths(input.targetPaths);
376
419
  if (input.targetExtractionComplete === false) return denyActiveCall(input.cwd, state, "semantic repair mutation targets are not statically complete", input.recordedAt);
377
420
  if (targets.length === 0 || targets.some((file) => !state.eligiblePaths.includes(file))) return denyActiveCall(input.cwd, state, "semantic repair mutation is outside the exact persisted grant", input.recordedAt);
@@ -402,6 +445,7 @@ export function pendingSemanticRepairCall(cwd: string, taskRunId: string, toolCa
402
445
  export function completeSemanticRepairCall(input: {
403
446
  cwd: string; taskRunId: string; toolCallId: string; success: boolean; exitCode?: number;
404
447
  currentDigest: string; changedPaths: string[]; retryableFailure?: boolean; correctiveFailure?: boolean; recordedAt?: string;
448
+ authoredChangedPaths?: string[];
405
449
  }): { result: "unmatched" | "opened" | "recorded" | "retry" | "correction" | "passed" | "cancelled" | "locked"; state?: SemanticRepairState } {
406
450
  const view = readSemanticRepairState(input.cwd, input.taskRunId), state = view.state;
407
451
  if (!view.enforcementSafe || !state?.pending || state.pending.toolCallId !== input.toolCallId) return { result: "unmatched", state };
@@ -410,6 +454,7 @@ export function completeSemanticRepairCall(input: {
410
454
  return { result: "locked", state: writeState(input.cwd, state) };
411
455
  }
412
456
  const pending = state.pending, changed = uniquePaths(input.changedPaths), treeChanged = input.currentDigest !== pending.preDigest;
457
+ const boundedRecovery = state.conflictCodes.includes(BOUNDED_RECOVERY_CONFLICT_CODE);
413
458
  const now = input.recordedAt ?? new Date().toISOString();
414
459
  if (!pending.authorized) { state.status = "locked"; state.pending = null; state.updatedAt = now; return { result: "locked", state: writeState(input.cwd, state) }; }
415
460
  if (pending.kind === "verifier") {
@@ -418,12 +463,20 @@ export function completeSemanticRepairCall(input: {
418
463
  state.failedCalls += 1;
419
464
  if (treeChanged || unsuccessful(state) >= MAX_SEMANTIC_REPAIR_UNSUCCESSFUL_CALLS) state.status = "locked";
420
465
  else if (input.retryableFailure && !state.transientRetryUsed) { state.transientRetryUsed = true; state.status = "retry-ready"; }
421
- else if (input.correctiveFailure && state.revision < MAX_SEMANTIC_REPAIR_REVISIONS) { state.revision += 1; state.successfulMutationsInRevision = 0; state.status = "active"; }
466
+ else if (!boundedRecovery && input.correctiveFailure && state.revision < MAX_SEMANTIC_REPAIR_REVISIONS) { state.revision += 1; state.successfulMutationsInRevision = 0; state.status = "active"; }
422
467
  else state.status = "locked";
423
468
  const result = state.status === "retry-ready" ? "retry" : state.status === "active" ? "correction" : "locked";
424
469
  return { result, state: writeState(input.cwd, state) };
425
470
  }
426
- const unexpected = changed.length === 0 || changed.some((file) => !pending.targetPaths.includes(file) || !state.eligiblePaths.includes(file));
471
+ const authoredChanged = uniquePaths(input.authoredChangedPaths ?? []);
472
+ const boundedTargetMismatch = boundedRecovery
473
+ && (changed.length !== pending.targetPaths.length || changed.some((file, index) => pending.targetPaths[index] !== file));
474
+ const boundedAuthorshipMissing = boundedRecovery
475
+ && (authoredChanged.length !== changed.length || changed.some((file, index) => authoredChanged[index] !== file));
476
+ const unexpected = changed.length === 0
477
+ || changed.some((file) => !pending.targetPaths.includes(file) || !state.eligiblePaths.includes(file))
478
+ || boundedTargetMismatch
479
+ || boundedAuthorshipMissing;
427
480
  if (treeChanged && (!input.success || unexpected)) { state.status = "locked"; state.pending = null; state.updatedAt = now; return { result: "locked", state: writeState(input.cwd, state) }; }
428
481
  if (input.success && treeChanged) {
429
482
  if (pending.opensRepair) writeSemanticRepairOrigin(input.cwd, state, input.currentDigest, now);
@@ -3,8 +3,10 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import type { TaskContract } from "../../extensions/guard-types.ts";
4
4
  import type { TrajectorySyncResult } from "../trajectory/trajectory-runtime.ts";
5
5
  import {
6
+ BOUNDED_RECOVERY_CONFLICT_CODE,
6
7
  authorizeSemanticRepairCall,
7
8
  completeSemanticRepairCall,
9
+ decideBoundedRecoveryHandshake,
8
10
  decideSemanticRepairHandshake,
9
11
  pendingSemanticRepairCall,
10
12
  readSemanticRepairState,
@@ -31,6 +33,7 @@ export type SemanticRepairCompletionMetadata = {
31
33
  exitCode: number;
32
34
  currentWorkingTreeDigest: string;
33
35
  changedPaths: string[];
36
+ authoredChangedPaths?: string[];
34
37
  retryableFailure: boolean; correctiveFailure: boolean;
35
38
  };
36
39
 
@@ -100,6 +103,52 @@ export class SemanticRepairRuntime {
100
103
  return true;
101
104
  }
102
105
 
106
+ prepareBoundedRecovery(input: {
107
+ ctx: ExtensionContext; task: TaskContract; event: ToolEvent; currentDigest: string;
108
+ currentDeltaPaths: string[]; observedChangedPaths: string[];
109
+ authoredFileDigests: Record<string, string>; targetPaths: string[]; verifierCurrent: boolean;
110
+ }): boolean {
111
+ // This path is a one-shot CAP-12 bridge, not CAP-13's multi-revision
112
+ // semantic-repair protocol. Any durable state, including a cancelled or
113
+ // interrupted reservation, consumes the task-run grant and fails closed.
114
+ if (readSemanticRepairState(input.ctx.cwd, input.task.taskRunId).status !== "missing"
115
+ || semanticRepairStateRequired(input.ctx.cwd, input.task.taskRunId)) return false;
116
+ const decision = decideBoundedRecoveryHandshake({
117
+ task: input.task,
118
+ mutationTargets: input.targetPaths,
119
+ currentDeltaPaths: input.currentDeltaPaths,
120
+ observedChangedPaths: input.observedChangedPaths,
121
+ authoredFileDigests: input.authoredFileDigests,
122
+ verifierCurrent: input.verifierCurrent
123
+ });
124
+ if (!decision.authorized) return false;
125
+ const toolCallId = String(input.event.toolCallId);
126
+ const reserved = reserveSemanticRepairCall({
127
+ cwd: input.ctx.cwd,
128
+ task: input.task,
129
+ sessionId: input.ctx.sessionManager.getSessionId(),
130
+ toolCallId,
131
+ toolName: input.event.toolName,
132
+ currentDigest: input.currentDigest,
133
+ decision,
134
+ targetPaths: input.targetPaths,
135
+ recordedAt: this.dependencies.now()
136
+ });
137
+ if (!reserved.reserved) return false;
138
+ this.reservationTokens.set(
139
+ this.reservationKey(input.ctx.cwd, input.task.taskRunId, toolCallId),
140
+ reserved.reservationToken!
141
+ );
142
+ this.dependencies.trace(input.ctx, input.task, {
143
+ event: "bounded_recovery_repair_reserved",
144
+ workingTreeDigest: input.currentDigest,
145
+ reasonCode: BOUNDED_RECOVERY_CONFLICT_CODE,
146
+ eligiblePaths: decision.eligiblePaths,
147
+ targetCount: input.targetPaths.length
148
+ });
149
+ return true;
150
+ }
151
+
103
152
  reservedCallMatches(input: {
104
153
  cwd: string; taskRunId: string; sessionId: string; toolCallId: string;
105
154
  toolName: string; currentDigest: string; targetPaths: string[];
@@ -131,7 +180,8 @@ export class SemanticRepairRuntime {
131
180
  if (view.state.successfulMutations > 0 && !semanticRepairOriginMatches(cwd, view.state)) return "semantic repair durable origin is missing or invalid";
132
181
  if (view.state.status === "passed") return;
133
182
  if (view.state.successfulMutations === 0 && view.state.pending === null && !semanticRepairStateRequired(cwd, taskRunId)) return;
134
- return `semantic repair exact final verifier is not satisfied (${view.state.status})`;
183
+ const label = view.state.conflictCodes.includes(BOUNDED_RECOVERY_CONFLICT_CODE) ? "bounded recovery" : "semantic repair";
184
+ return `${label} exact final verifier is not satisfied (${view.state.status})`;
135
185
  }
136
186
 
137
187
  complete(
@@ -148,13 +198,16 @@ export class SemanticRepairRuntime {
148
198
  exitCode: metadata.exitCode,
149
199
  currentDigest: metadata.currentWorkingTreeDigest,
150
200
  changedPaths: metadata.changedPaths,
201
+ authoredChangedPaths: metadata.authoredChangedPaths,
151
202
  retryableFailure: metadata.retryableFailure,
152
203
  correctiveFailure: metadata.correctiveFailure,
153
204
  recordedAt: this.dependencies.now()
154
205
  });
155
206
  if (completion.result === "unmatched") return;
156
207
  this.dependencies.trace(ctx, task, {
157
- event: completion.result === "opened" ? "semantic_contradiction_repair_opened" : `semantic_repair_${completion.result}`,
208
+ event: completion.state?.conflictCodes.includes(BOUNDED_RECOVERY_CONFLICT_CODE)
209
+ ? `bounded_recovery_repair_${completion.result}`
210
+ : completion.result === "opened" ? "semantic_contradiction_repair_opened" : `semantic_repair_${completion.result}`,
158
211
  toolName: event.toolName,
159
212
  revision: completion.state?.revision,
160
213
  successfulMutations: completion.state?.successfulMutations,
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { stageContextDelivery } from "../context/context-delivery.ts";
2
3
 
3
4
  type ExtensionContext = any;
4
5
 
@@ -10,7 +11,7 @@ export function registerContextCommands(pi: ExtensionAPI, deps: Record<string, a
10
11
  contextIndexV2Status, crypto, emitRuntimeMessage, ensureContextIndexV2, estimateContextTokens,
11
12
  formatContextPreflight,
12
13
  formatCount, formatPercent, fs, helpersMode, loadProfileFromContext, memoryHandbookPath,
13
- policy, projectFilePath, resolveMemorySettings, resolveOrchestrationPolicy, searchContextIndex,
14
+ policy, projectFilePath, resolveMemorySettings, resolveOrchestrationPolicy, retrievalKey, runtimeState, searchContextIndex,
14
15
  searchContextIndexV2, selectRuntimeAction, semanticCompactionInstructions, telemetry
15
16
  } = deps;
16
17
  function emitContextIndexSearch(ctx: ExtensionContext, query: string): void {
@@ -123,6 +124,7 @@ export function registerContextCommands(pi: ExtensionAPI, deps: Record<string, a
123
124
  });
124
125
  telemetry(ctx, {
125
126
  event: "context_pack",
127
+ turnId: runtimeState.currentTurn(ctx)?.turnId,
126
128
  queryHash: pack.queryHash,
127
129
  confidence: pack.confidence,
128
130
  candidates: pack.candidates,
@@ -131,12 +133,65 @@ export function registerContextCommands(pi: ExtensionAPI, deps: Record<string, a
131
133
  selectedPaths: pack.selected.map((item) => item.path),
132
134
  source: "command"
133
135
  });
136
+ const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId());
137
+ const deliveryId = task?.trace?.outcome === "pending" && pack.selected.length > 0 ? crypto.randomUUID() : undefined;
138
+ if (deliveryId) {
139
+ stageContextDelivery(ctx, {
140
+ deliveryId,
141
+ taskRunId: task.taskRunId,
142
+ turnId: runtimeState.currentTurn(ctx)?.turnId,
143
+ entries: pack.selected.map((item) => ({
144
+ path: item.path,
145
+ reason: "Runtime confirmed delivery of a /context pack message."
146
+ })),
147
+ pack: {
148
+ retrievalKey: retrievalKey(ctx, query),
149
+ queryHash: pack.queryHash,
150
+ confidence: pack.confidence,
151
+ estimatedTokens: pack.estimatedTokens,
152
+ paths: pack.selected.map((item) => item.path)
153
+ },
154
+ injection: {
155
+ source: "context-command",
156
+ queryHash: pack.queryHash,
157
+ confidence: pack.confidence,
158
+ estimatedTokens: pack.estimatedTokens,
159
+ selectedItems: pack.selected.map((item) => ({
160
+ path: item.path,
161
+ estimatedTokens: item.estimatedTokens,
162
+ sources: item.sources,
163
+ links: item.links,
164
+ fileContentHash: item.fileContentHash,
165
+ ...(item.sanitizedContentDigest ? { sanitizedContentDigest: item.sanitizedContentDigest } : {}),
166
+ payloadHash: item.payloadHash,
167
+ representation: item.representation,
168
+ ranges: item.ranges,
169
+ generation: item.generation,
170
+ sensitiveContentRedacted: item.sensitiveContentRedacted
171
+ }))
172
+ }
173
+ }, { state: runtimeState, telemetry });
174
+ }
134
175
  emitRuntimeMessage(ctx, "piagent-context-pack-v2", pack.text, {
135
176
  queryHash: pack.queryHash,
136
177
  confidence: pack.confidence,
137
178
  estimatedTokens: pack.estimatedTokens,
138
179
  paths: pack.selected.map((item) => item.path),
139
- finderRecommended: pack.finderRecommended
180
+ selectedItems: pack.selected.map((item) => ({
181
+ path: item.path,
182
+ estimatedTokens: item.estimatedTokens,
183
+ sources: item.sources,
184
+ links: item.links,
185
+ fileContentHash: item.fileContentHash,
186
+ ...(item.sanitizedContentDigest ? { sanitizedContentDigest: item.sanitizedContentDigest } : {}),
187
+ payloadHash: item.payloadHash,
188
+ representation: item.representation,
189
+ ranges: item.ranges,
190
+ generation: item.generation,
191
+ sensitiveContentRedacted: item.sensitiveContentRedacted
192
+ })),
193
+ finderRecommended: pack.finderRecommended,
194
+ contextDelivery: deliveryId ? { schemaVersion: 1, deliveryId } : undefined
140
195
  });
141
196
  }
142
197
 
@@ -156,13 +211,19 @@ export function registerContextCommands(pi: ExtensionAPI, deps: Record<string, a
156
211
  const report = buildContextEfficiencyReport(ctx.cwd);
157
212
  const task = activeSessionTask(ctx);
158
213
  const taskEfficiency = task ? buildTaskEfficiencyMetrics(ctx.cwd, task) : null;
214
+ const wasteScore = report.metrics.contextWasteScore === null
215
+ ? `unavailable (diagnostic estimate ${report.metrics.contextWasteScoreEstimate}/100; evidence ${formatPercent(report.metrics.contextWasteScoreEvidenceCoverage)}, ${report.coverage.wasteScore.status})`
216
+ : `${report.metrics.contextWasteScore}/100 (lower is better; evidence ${formatPercent(report.metrics.contextWasteScoreEvidenceCoverage)}, ${report.coverage.wasteScore.status})`;
159
217
  emitRuntimeMessage(ctx, "piagent-context-efficiency", [
160
- `contextWasteScore: ${report.metrics.contextWasteScore}/100 (lower is better)`,
218
+ `contextWasteScore: ${wasteScore}`,
161
219
  `activeTools: ${report.metrics.averageActiveTools}`,
162
- `toolSchemaShare: ${formatPercent(report.metrics.toolSchemaShare)}`,
163
- `duplicateReads: ${report.metrics.duplicateReads}/${report.metrics.readCalls}`,
220
+ `toolSchemaPrefixShare: ${formatPercent(report.metrics.toolSchemaPrefixShare)}`,
221
+ `toolSchemaToSystemRatio: ${report.metrics.toolSchemaToSystemRatio}`,
222
+ `duplicateReads: ${report.metrics.duplicateReads}/${report.metrics.comparableReadCalls} comparable (${report.metrics.readCalls} observed; coverage ${formatPercent(report.metrics.readEvidenceCoverage)})`,
223
+ `fallbackRereads: ${report.metrics.contextFallbackRereads}/${report.metrics.contextSelections}`,
164
224
  `duplicateOutput: ${formatPercent(report.metrics.duplicateOutputRate)}`,
165
225
  `lowConfidencePacks: ${report.metrics.lowConfidencePacks}/${report.sample.contextPacks}`,
226
+ `editRecovery: ${report.metrics.editRecoveryContextCount}/${report.metrics.editRecoveryFailures} failures returned context; ${report.metrics.editRecoverySuppressedFailures} suppressed; ${report.metrics.editRecoveryEstimatedTokens} estimated tokens; classification coverage ${formatPercent(report.metrics.editRecoveryFailureEvidenceCoverage)}`,
166
227
  `taskEfficiency: ${taskEfficiency ? `${taskEfficiency.solver.route}; verify=${taskEfficiency.verification.attempts}; outcome=${taskEfficiency.outcome.task}` : "no active task"}`,
167
228
  ...report.recommendations.map((recommendation) => `- ${recommendation}`)
168
229
  ].join("\n"), { ...report, taskEfficiency });
@@ -1,4 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { stageContextDelivery } from "../context/context-delivery.ts";
3
+ import { formatContextEfficiencyReport } from "../context/context-efficiency-format.ts";
2
4
 
3
5
  type PiagentToolGroup = any;
4
6
  type TechStackManifest = any;
@@ -6,7 +8,7 @@ type TechStackManifest = any;
6
8
 
7
9
  export function registerPolicyTools(pi: ExtensionAPI, deps: Record<string, any>): void {
8
10
  const {
9
- PIAGENT_TOOL_NAMES, READ_ONLY_TOOL_NAMES, StringEnum, Type, activateToolGroups,
11
+ PIAGENT_TOOL_NAMES, READ_ONLY_TOOL_NAMES, StringEnum, Type, activateToolGroups, activeSessionTask,
10
12
  buildContextEfficiencyReport, buildContextIndexStatus, buildContextIndexV2, buildContextPack, buildContextPreflight,
11
13
  buildTestImpact, buildUsageSnapshot, candidateFileBudget, contextBudgetConfig, contextIndexExcludePatterns,
12
14
  contextIndexV2Status, crypto, dynamicToolsEnabled, effectiveProtectedPaths, ensureContextIndexV2,
@@ -142,7 +144,48 @@ export function registerPolicyTools(pi: ExtensionAPI, deps: Record<string, any>)
142
144
  limit: 18,
143
145
  excludePatterns
144
146
  });
145
- result = { ...pack, text: undefined, status };
147
+ telemetry(ctx, { event: "context_pack", turnId: runtimeState.currentTurn(ctx)?.turnId, source: "context-tool", queryHash: pack.queryHash, confidence: pack.confidence, candidates: pack.candidates, selected: pack.selected.length, estimatedTokens: pack.estimatedTokens, selectedPaths: pack.selected.map((item) => item.path) });
148
+ const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId());
149
+ const deliveryId = task?.trace?.outcome === "pending" && pack.selected.length > 0 ? crypto.randomUUID() : undefined;
150
+ if (deliveryId) {
151
+ const packRetrievalKey = retrievalKey(ctx, params.query);
152
+ stageContextDelivery(ctx, {
153
+ deliveryId,
154
+ taskRunId: task.taskRunId,
155
+ turnId: runtimeState.currentTurn(ctx)?.turnId,
156
+ entries: pack.selected.map((item) => ({
157
+ path: item.path,
158
+ reason: "Runtime confirmed delivery of an explicit Context Engine tool pack."
159
+ })),
160
+ pack: {
161
+ retrievalKey: packRetrievalKey,
162
+ queryHash: pack.queryHash,
163
+ confidence: pack.confidence,
164
+ estimatedTokens: pack.estimatedTokens,
165
+ paths: pack.selected.map((item) => item.path)
166
+ },
167
+ injection: {
168
+ source: "context-tool",
169
+ queryHash: pack.queryHash,
170
+ confidence: pack.confidence,
171
+ estimatedTokens: pack.estimatedTokens,
172
+ selectedItems: pack.selected.map((item) => ({
173
+ path: item.path,
174
+ estimatedTokens: item.estimatedTokens,
175
+ sources: item.sources,
176
+ links: item.links,
177
+ fileContentHash: item.fileContentHash,
178
+ ...(item.sanitizedContentDigest ? { sanitizedContentDigest: item.sanitizedContentDigest } : {}),
179
+ payloadHash: item.payloadHash,
180
+ representation: item.representation,
181
+ ranges: item.ranges,
182
+ generation: item.generation,
183
+ sensitiveContentRedacted: item.sensitiveContentRedacted
184
+ }))
185
+ }
186
+ }, { state: runtimeState, telemetry });
187
+ }
188
+ result = { ...pack, text: undefined, status, contextDelivery: deliveryId ? { schemaVersion: 1, deliveryId } : undefined };
146
189
  text = pack.text;
147
190
  } else if (params.action === "impact") {
148
191
  await ensureContextIndexV2(ctx.cwd, { excludePatterns, rebuildMissing: false });
@@ -156,15 +199,7 @@ export function registerPolicyTools(pi: ExtensionAPI, deps: Record<string, any>)
156
199
  } else {
157
200
  result = buildContextEfficiencyReport(ctx.cwd);
158
201
  const report = result as ReturnType<typeof buildContextEfficiencyReport>;
159
- text = [
160
- `contextWasteScore: ${report.metrics.contextWasteScore}/100 (lower is better)`,
161
- `activeTools: ${report.metrics.averageActiveTools}`,
162
- `toolSchemaShare: ${formatPercent(report.metrics.toolSchemaShare)}`,
163
- `duplicateReads: ${report.metrics.duplicateReads}/${report.metrics.readCalls}`,
164
- `duplicateOutput: ${formatPercent(report.metrics.duplicateOutputRate)}`,
165
- `lowConfidencePacks: ${report.metrics.lowConfidencePacks}/${report.sample.contextPacks}`,
166
- ...report.recommendations.map((recommendation) => `- ${recommendation}`)
167
- ].join("\n");
202
+ text = formatContextEfficiencyReport(report, formatPercent);
168
203
  }
169
204
  telemetry(ctx, {
170
205
  event: "context_engine_action",