@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
@@ -11,6 +11,7 @@ import { buildHandoffProjection, writeHandoffProjection } from "../recovery/hand
11
11
  import { buildContextPreflight, buildUsageSnapshot } from "../session/usage.ts";
12
12
  import { activeTaskToolGroups, toolGroupsForPrompt } from "../tools/tool-groups.ts";
13
13
  import type { PiagentToolGroup } from "../tools/tool-groups.ts";
14
+ import type { RuntimeSessionState } from "../session/runtime-state.ts";
14
15
  import {
15
16
  buildFreshCommand,
16
17
  chooseFreshWorkflow,
@@ -26,6 +27,7 @@ import {
26
27
  } from "../workflows/task-intake.ts";
27
28
 
28
29
  type InputHookDependencies = {
30
+ state: RuntimeSessionState;
29
31
  boilerplateCollapseChars: number;
30
32
  activeTask: (ctx: ExtensionContext) => TaskContract | undefined;
31
33
  authorityPolicy: (ctx: ExtensionContext, task: TaskContract) => AuthorityResumeDecision;
@@ -44,7 +46,14 @@ export function registerInputHook(pi: ExtensionAPI, dependencies: InputHookDepen
44
46
  }
45
47
 
46
48
  const taskSignal = classifyContextTask(text);
47
- const activeTask = dependencies.activeTask(ctx);
49
+ const sessionTask = dependencies.activeTask(ctx);
50
+ if (sessionTask && sessionTask.trace.outcome !== "pending") dependencies.state.clearTaskBoundary(ctx, sessionTask.taskRunId);
51
+ else if (!sessionTask) {
52
+ const cachedTask = dependencies.state.taskIdentity(ctx);
53
+ if (cachedTask) dependencies.state.clearTaskBoundary(ctx, cachedTask.taskRunId);
54
+ }
55
+ const activeTask = sessionTask?.trace.outcome === "pending" ? sessionTask : undefined;
56
+ const turn = dependencies.state.beginTurn(ctx, taskSignal.promptHash);
48
57
  const authorityPolicy = activeTask?.trace.outcome === "pending" ? dependencies.authorityPolicy(ctx, activeTask) : undefined;
49
58
  let authorityHandoffReady = false;
50
59
  if (activeTask && authorityPolicy?.disposition === "new-attempt-required") {
@@ -79,6 +88,7 @@ export function registerInputHook(pi: ExtensionAPI, dependencies: InputHookDepen
79
88
  : promptGroups);
80
89
  dependencies.telemetry(ctx, {
81
90
  event: "user_input",
91
+ turnId: turn.turnId,
82
92
  source: event.source,
83
93
  promptHash: taskSignal.promptHash,
84
94
  promptChars: taskSignal.promptChars,
@@ -4,6 +4,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
4
4
  import type { TaskContract } from "../../extensions/guard-types.js";
5
5
  import { RuntimeSessionState } from "../session/runtime-state.ts";
6
6
  import type { ObservedTaskContext } from "../session/runtime-state.ts";
7
+ import { confirmContextDelivery, contextDeliveryIdFromDetails } from "../context/context-delivery.ts";
7
8
 
8
9
  type TelemetryWriter = (ctx: ExtensionContext, payload: Record<string, unknown>) => void;
9
10
 
@@ -39,6 +40,20 @@ export function registerSessionHooks(pi: ExtensionAPI, dependencies: SessionHook
39
40
  flushObservedTaskContext
40
41
  } = dependencies;
41
42
 
43
+ pi.on("message_start", async (event, ctx) => {
44
+ const message = event.message as unknown as { role?: unknown; details?: unknown };
45
+ if (message.role !== "custom") return;
46
+ const deliveryId = contextDeliveryIdFromDetails(message.details);
47
+ if (!deliveryId) return;
48
+ confirmContextDelivery(pi, ctx, deliveryId, {
49
+ state,
50
+ maxManifestFiles,
51
+ activeTask: (deliveryContext) => activeTask(deliveryContext.cwd, deliveryContext.sessionManager.getSessionId()),
52
+ flushObservedTaskContext,
53
+ telemetry
54
+ });
55
+ });
56
+
42
57
  pi.on("session_info_changed", async (event, ctx) => {
43
58
  const sessionId = ctx.sessionManager.getSessionId();
44
59
  const task = activeTask(ctx.cwd, sessionId);
@@ -58,6 +73,7 @@ export function registerSessionHooks(pi: ExtensionAPI, dependencies: SessionHook
58
73
 
59
74
  pi.on("turn_end", async (event, ctx) => {
60
75
  const message = event.message as unknown as { usage?: unknown; stopReason?: unknown; role?: unknown };
76
+ const task = activeTask(ctx.cwd, ctx.sessionManager.getSessionId());
61
77
  telemetry(ctx, {
62
78
  event: "turn_end",
63
79
  turnIndex: event.turnIndex,
@@ -65,9 +81,13 @@ export function registerSessionHooks(pi: ExtensionAPI, dependencies: SessionHook
65
81
  role: message.role,
66
82
  stopReason: message.stopReason,
67
83
  usage: message.usage,
68
- contextUsage: ctx.getContextUsage()
84
+ contextUsage: ctx.getContextUsage(),
85
+ taskId: task?.taskId,
86
+ taskRunId: task?.taskRunId,
87
+ taskOutcome: task?.trace.outcome
69
88
  });
70
89
  dependencies.onTurnEnd?.(ctx);
90
+ if (task && task.trace.outcome !== "pending") state.clearTaskBoundary(ctx, task.taskRunId);
71
91
  });
72
92
 
73
93
  pi.on("agent_settled", async (_event, ctx) => {
@@ -96,7 +116,7 @@ export function registerSessionHooks(pi: ExtensionAPI, dependencies: SessionHook
96
116
  flushObservedTaskContext(
97
117
  pi,
98
118
  ctx,
99
- state.observedContext(ctx),
119
+ state.qualifiedTaskContext(ctx),
100
120
  maxManifestFiles,
101
121
  "context_observed_before_shutdown"
102
122
  );
@@ -139,8 +139,9 @@ export function registerSessionStartHook(pi: ExtensionAPI, dependencies: Session
139
139
  dependencies.bindTask(ctx.cwd, sessionId, sessionName, resumedTask);
140
140
  }
141
141
  if (resumedTask?.workingTreeDigestMigration && resumedTask.workingTreeDigestMigration.status !== "refreshed") dependencies.state.clearDigestMigrationState(ctx, resumedTask.taskRunId, resumedTask.taskId);
142
- dependencies.state.cacheTaskIdentity(ctx, resumedTask);
143
- const resumeState = resumedTask ? dependencies.inspectResume(ctx.cwd, resumedTask, sessionId) : undefined;
142
+ if (resumedTask?.trace.outcome === "pending") dependencies.state.cacheTaskIdentity(ctx, resumedTask);
143
+ else dependencies.state.clearSession(ctx);
144
+ const resumeState = resumedTask?.trace.outcome === "pending" ? dependencies.inspectResume(ctx.cwd, resumedTask, sessionId) : undefined;
144
145
  if (resumeState) dependencies.state.rememberResumeState(resumeState);
145
146
  if (resumedTask && resumeState?.enforcementSafe) observeTrajectorySync(ctx, dependencies.syncTrajectory?.(ctx, resumedTask), dependencies.telemetry);
146
147
  let taskRecovery: Record<string, unknown> | undefined;
@@ -217,6 +218,10 @@ export function registerSessionStartHook(pi: ExtensionAPI, dependencies: Session
217
218
  }
218
219
  dependencies.telemetry(ctx, {
219
220
  event: "session_start",
221
+ // Benchmark and efficiency projections must be able to distinguish a
222
+ // genuine zero-recovery session from telemetry produced before this
223
+ // receipt/result protocol existed.
224
+ editRecoveryContextTelemetryVersion: 1,
220
225
  activeTools: pi.getActiveTools().length,
221
226
  index: engineStatus,
222
227
  taskMigration: migration,
@@ -0,0 +1,95 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import { matchesProtectedPath } from "../../extensions/policy-core.js";
5
+
6
+ function normalizeRelative(cwd: string, candidate: unknown): string | undefined {
7
+ if (typeof candidate !== "string" || candidate.trim().length === 0) return undefined;
8
+ let raw = candidate.trim();
9
+ try {
10
+ raw = decodeURIComponent(raw);
11
+ } catch {
12
+ return undefined;
13
+ }
14
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(raw)) {
15
+ if (!raw.toLowerCase().startsWith("file://")) return undefined;
16
+ try {
17
+ raw = fileURLToPath(raw);
18
+ } catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
23
+ return path.relative(cwd, absolute).split(path.sep).join("/");
24
+ }
25
+
26
+ function filterTextBlocks(
27
+ content: unknown,
28
+ blocked: (line: string) => boolean,
29
+ emptyText: string,
30
+ noticeText: (count: number) => string
31
+ ): { changed: boolean; content?: unknown; redactedLines: number } {
32
+ if (!Array.isArray(content)) return { changed: false, redactedLines: 0 };
33
+ let changed = false;
34
+ let redactedLines = 0;
35
+ const filtered = content.map((block) => {
36
+ if (!block || typeof block !== "object") return block;
37
+ const text = (block as { type?: unknown; text?: unknown }).text;
38
+ if ((block as { type?: unknown }).type !== "text" || typeof text !== "string") return block;
39
+ const kept: string[] = [];
40
+ let blockRedactedLines = 0;
41
+ for (const line of text.split(/\r?\n/)) {
42
+ if (blocked(line)) {
43
+ changed = true;
44
+ redactedLines += 1;
45
+ blockRedactedLines += 1;
46
+ } else {
47
+ kept.push(line);
48
+ }
49
+ }
50
+ if (blockRedactedLines === 0) return block;
51
+ const notice = noticeText(blockRedactedLines);
52
+ const nextText = kept.join("\n").trim().length > 0
53
+ ? `${kept.join("\n")}\n${notice}`
54
+ : `${emptyText}\n${notice}`;
55
+ return { ...block, text: nextText };
56
+ });
57
+ return { changed, content: filtered, redactedLines };
58
+ }
59
+
60
+ export function filterGrepProtectedContent(content: unknown, protectedPatterns: string[]) {
61
+ return filterTextBlocks(
62
+ content,
63
+ (line) => {
64
+ const linePath = line.match(/^(.+?)(?::\d+:|-\d+-)/)?.[1];
65
+ return Boolean(linePath && matchesProtectedPath(linePath, protectedPatterns));
66
+ },
67
+ "No matches found in non-protected paths.",
68
+ (count) => `[Piagent Pi guard redacted ${count} protected grep line${count === 1 ? "" : "s"}.]`
69
+ );
70
+ }
71
+
72
+ export function filterProtectedPathListContent(
73
+ cwd: string,
74
+ content: unknown,
75
+ protectedPatterns: string[],
76
+ basePath: string,
77
+ toolName: string
78
+ ) {
79
+ return filterTextBlocks(
80
+ content,
81
+ (line) => {
82
+ const trimmed = line.trim();
83
+ if (!trimmed || trimmed.startsWith("[") || trimmed.startsWith("No ") || trimmed === "(empty directory)") return false;
84
+ const entry = trimmed.replace(/[\\/]+$/, "");
85
+ const candidates = new Set<string>();
86
+ const direct = normalizeRelative(cwd, entry);
87
+ const underBase = normalizeRelative(cwd, path.posix.join(basePath || ".", entry));
88
+ if (direct !== undefined) candidates.add(direct);
89
+ if (underBase !== undefined) candidates.add(underBase);
90
+ return [...candidates].some((candidate) => matchesProtectedPath(candidate, protectedPatterns));
91
+ },
92
+ "No entries found in non-protected paths.",
93
+ (count) => `[Piagent Pi guard redacted ${count} protected ${toolName} line${count === 1 ? "" : "s"}.]`
94
+ );
95
+ }
@@ -1,9 +1,6 @@
1
- import path from "node:path";
2
- import { fileURLToPath } from "node:url";
3
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
2
 
5
- import { toolResultFingerprint } from "../../extensions/context-engine.js";
6
- import type { TaskContract } from "../../extensions/guard-types.ts";
3
+ import { estimateContextTokens, toolResultFingerprint } from "../../extensions/context-engine.js";
7
4
  import { matchesProtectedPath } from "../../extensions/policy-core.js";
8
5
  import { changedSnapshotFiles, taskDeltaFilesFromSnapshot } from "../../extensions/task-contract-view.js";
9
6
  import { classifyVerificationFailure } from "../../extensions/verification-intelligence.js";
@@ -11,25 +8,29 @@ import { redactForStorage, redactSensitiveText } from "../../extensions/redactio
11
8
  import { appendObservedBashResult, hashEvidenceCommand, observedBashResultFromToolResultEvent } from "../../extensions/runtime-evidence.js";
12
9
  import { workingTreeSnapshot, workingTreeSnapshotHasUnavailableEvidence } from "../../extensions/task-state.js";
13
10
  import { workingTreeObservation } from "../../extensions/working-tree-digest.js";
11
+ import { recordObservedContextEvidence } from "../context/context-evidence-qualification.ts";
12
+ import { confirmContextDeliveryFromToolResult, type ContextDeliveryConfirmationDependencies } from "../context/context-delivery.ts";
14
13
  import { recordMutationResult } from "../inspection/mutation-provenance-recorder.ts";
14
+ import { classifyToolFailure } from "../inspection/tool-failure-classification.ts";
15
15
  import { boundedGitDiffReview } from "../quality/performance-assurance.ts";
16
16
  import { currentFileContentDigests } from "../quality/model-mutation-proof.ts";
17
17
  import { boundedPerformanceReviewResultText } from "../quality/performance-review-evidence.ts";
18
+ import { buildEditRecoveryContext, type EditRecoveryContext } from "../recovery/edit-recovery-context.ts";
19
+ import { EditRecoveryDeliveryState } from "../recovery/edit-recovery-delivery.ts";
18
20
  import { attachToolResultCompactionDetails, compactToolResultDetails, compactToolResultTextContent, type ToolResultCaptureSummary } from "../session/tool-result-compaction.ts";
19
- import { RuntimeSessionState, type ObservedTaskContext } from "../session/runtime-state.ts";
21
+ import type { ObservedTaskContext } from "../session/runtime-state.ts";
20
22
  import { observeTrajectorySync } from "../trajectory/trajectory-observability.ts";
21
23
  import type { TrajectorySyncResult } from "../trajectory/trajectory-runtime.ts";
24
+ import { filterGrepProtectedContent, filterProtectedPathListContent } from "./tool-result-content-guards.ts";
22
25
  import { patchLineStats } from "./tool-result-metadata.ts";
23
26
 
27
+ export { filterGrepProtectedContent, filterProtectedPathListContent };
24
28
  type ToolResultEvent = { toolCallId?: string; toolName: string; input?: unknown; content?: unknown; details?: unknown; isError?: boolean; usage?: unknown };
25
29
  type ObservedBashResult = NonNullable<ReturnType<typeof observedBashResultFromToolResultEvent>>;
26
30
  type ObservedVerificationResult = ObservedBashResult & { outputText?: string };
27
31
  type WorkingTreeObservation = ReturnType<typeof workingTreeObservation>;
28
32
 
29
- type ToolResultHookDependencies = {
30
- state: RuntimeSessionState;
31
- activeTask: (ctx: ExtensionContext) => TaskContract | undefined;
32
- maxManifestFiles: number;
33
+ type ToolResultHookDependencies = ContextDeliveryConfirmationDependencies & {
33
34
  readProtectedPaths: (ctx: ExtensionContext) => string[];
34
35
  recordObservedBash: (observed: ObservedBashResult) => void;
35
36
  observedBashLedgerPath: (cwd: string) => string;
@@ -69,33 +70,13 @@ type ToolResultHookDependencies = {
69
70
  success: boolean;
70
71
  exitCode: number;
71
72
  currentWorkingTreeDigest: string;
72
- changedPaths: string[];
73
+ changedPaths: string[]; authoredChangedPaths: string[];
73
74
  retryableFailure: boolean; correctiveFailure: boolean;
74
75
  }
75
76
  ) => TrajectorySyncResult | undefined;
76
77
  syncTrajectory?: (ctx: ExtensionContext, contextObserved: boolean) => TrajectorySyncResult | undefined;
77
78
  };
78
79
 
79
- function normalizeRelative(cwd: string, candidate: unknown): string | undefined {
80
- if (typeof candidate !== "string" || candidate.trim().length === 0) return undefined;
81
- let raw = candidate.trim();
82
- try {
83
- raw = decodeURIComponent(raw);
84
- } catch {
85
- return undefined;
86
- }
87
- if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(raw)) {
88
- if (!raw.toLowerCase().startsWith("file://")) return undefined;
89
- try {
90
- raw = fileURLToPath(raw);
91
- } catch {
92
- return undefined;
93
- }
94
- }
95
- const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
96
- return path.relative(cwd, absolute).split(path.sep).join("/");
97
- }
98
-
99
80
  function isPlainRecord(value: unknown): value is Record<string, unknown> {
100
81
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
101
82
  const prototype = Object.getPrototypeOf(value);
@@ -154,79 +135,19 @@ function redactToolResultTextContent(content: unknown): { content: unknown; reda
154
135
  return { content: safeContent, redacted };
155
136
  }
156
137
 
157
- function filterTextBlocks(
158
- content: unknown,
159
- blocked: (line: string) => boolean,
160
- emptyText: string,
161
- noticeText: (count: number) => string
162
- ): { changed: boolean; content?: unknown; redactedLines: number } {
163
- if (!Array.isArray(content)) return { changed: false, redactedLines: 0 };
164
- let changed = false;
165
- let redactedLines = 0;
166
- const filtered = content.map((block) => {
167
- if (!block || typeof block !== "object") return block;
168
- const text = (block as { type?: unknown; text?: unknown }).text;
169
- if ((block as { type?: unknown }).type !== "text" || typeof text !== "string") return block;
170
- const kept: string[] = [];
171
- let blockRedactedLines = 0;
172
- for (const line of text.split(/\r?\n/)) {
173
- if (blocked(line)) {
174
- changed = true;
175
- redactedLines += 1;
176
- blockRedactedLines += 1;
177
- } else {
178
- kept.push(line);
179
- }
180
- }
181
- if (blockRedactedLines === 0) return block;
182
- const notice = noticeText(blockRedactedLines);
183
- const nextText = kept.join("\n").trim().length > 0
184
- ? `${kept.join("\n")}\n${notice}`
185
- : `${emptyText}\n${notice}`;
186
- return { ...block, text: nextText };
187
- });
188
- return { changed, content: filtered, redactedLines };
189
- }
190
-
191
- export function filterGrepProtectedContent(content: unknown, protectedPatterns: string[]) {
192
- return filterTextBlocks(
193
- content,
194
- (line) => {
195
- const linePath = line.match(/^(.+?)(?::\d+:|-\d+-)/)?.[1];
196
- return Boolean(linePath && matchesProtectedPath(linePath, protectedPatterns));
197
- },
198
- "No matches found in non-protected paths.",
199
- (count) => `[Piagent Pi guard redacted ${count} protected grep line${count === 1 ? "" : "s"}.]`
200
- );
201
- }
202
-
203
- export function filterProtectedPathListContent(
204
- cwd: string,
205
- content: unknown,
206
- protectedPatterns: string[],
207
- basePath: string,
208
- toolName: string
209
- ) {
210
- return filterTextBlocks(
211
- content,
212
- (line) => {
213
- const trimmed = line.trim();
214
- if (!trimmed || trimmed.startsWith("[") || trimmed.startsWith("No ") || trimmed === "(empty directory)") return false;
215
- const entry = trimmed.replace(/[\\/]+$/, "");
216
- const candidates = new Set<string>();
217
- const direct = normalizeRelative(cwd, entry);
218
- const underBase = normalizeRelative(cwd, path.posix.join(basePath || ".", entry));
219
- if (direct !== undefined) candidates.add(direct);
220
- if (underBase !== undefined) candidates.add(underBase);
221
- return [...candidates].some((candidate) => matchesProtectedPath(candidate, protectedPatterns));
222
- },
223
- "No entries found in non-protected paths.",
224
- (count) => `[Piagent Pi guard redacted ${count} protected ${toolName} line${count === 1 ? "" : "s"}.]`
225
- );
138
+ function appendToolResultText(content: unknown, text: string): unknown[] {
139
+ const block = { type: "text", text };
140
+ if (Array.isArray(content)) return [...content, block];
141
+ if (typeof content === "string" && content) return [{ type: "text", text: content }, block];
142
+ return [block];
226
143
  }
227
144
 
228
145
  export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResultHookDependencies): void {
146
+ const editRecoveryDelivery = new EditRecoveryDeliveryState();
147
+ pi.on("session_compact", async (_event, ctx) => editRecoveryDelivery.advanceEpoch(ctx));
148
+ pi.on("session_shutdown", async (_event, ctx) => editRecoveryDelivery.clearSession(ctx));
229
149
  pi.on("tool_result", async (event, ctx) => {
150
+ confirmContextDeliveryFromToolResult(pi, ctx, event, dependencies);
230
151
  const taskIdentity = dependencies.state.taskIdentity(ctx);
231
152
  const resultToolCallId = event.toolCallId ?? toolResultFingerprint(event.toolName, event.input, []).key;
232
153
  const readProtectedPaths = dependencies.readProtectedPaths(ctx);
@@ -245,8 +166,8 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
245
166
  }
246
167
 
247
168
  const observedContextEntry = dependencies.observedTaskContext(ctx.cwd, event, readProtectedPaths);
248
- if (observedContextEntry) dependencies.state.rememberObservedContext(ctx, observedContextEntry);
249
- const pendingContext = dependencies.state.observedContext(ctx);
169
+ recordObservedContextEvidence(ctx, observedContextEntry, taskIdentity, dependencies);
170
+ const pendingContext = dependencies.state.qualifiedTaskContext(ctx);
250
171
  const shellSnapshotBefore = dependencies.isShellTool(event.toolName)
251
172
  ? dependencies.state.consumeShellMutationSnapshot(ctx, event.toolName, event.input)
252
173
  : undefined;
@@ -303,6 +224,7 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
303
224
  }
304
225
  }
305
226
  const outputText = boundedToolResultText(event.content);
227
+ const failureReasonCode = classifyToolFailure(event.toolName, event.isError === true, event.content, event.input);
306
228
  const performanceReviewOutputText = boundedPerformanceReviewResultText(event.content);
307
229
  const observedExitCode = observed?.exitCode
308
230
  ?? numericExitCode(isPlainRecord(event.details) ? event.details.exitCode ?? event.details.status : undefined)
@@ -326,6 +248,7 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
326
248
  exitCode: observedExitCode,
327
249
  currentWorkingTreeDigest: currentTreeDigest,
328
250
  changedPaths: [...new Set([...directMutationResult.changedPaths, ...shellChangedPaths])].sort(),
251
+ authoredChangedPaths: Object.keys(directMutationResult.recordedDigests).sort(),
329
252
  retryableFailure: failure?.retryable === true, correctiveFailure: failure?.sourceMutationPermission === "eligible-in-scope" && failure?.confidence === "high"
330
253
  })
331
254
  : undefined;
@@ -389,6 +312,8 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
389
312
  let resultContent: unknown = event.content;
390
313
  let resultDetails: unknown = event.details;
391
314
  let resultChanged = false;
315
+ let editRecovery: EditRecoveryContext | undefined;
316
+ const resultTarget = dependencies.extractLikelyPath(ctx.cwd, isPlainRecord(event.input) ? event.input : {});
392
317
  if (event.toolName === "grep") {
393
318
  const filtered = filterGrepProtectedContent(resultContent, readProtectedPaths);
394
319
  if (filtered.changed) {
@@ -412,6 +337,44 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
412
337
  }
413
338
  }
414
339
 
340
+ const recovery = buildEditRecoveryContext({
341
+ cwd: ctx.cwd,
342
+ targetPath: resultTarget,
343
+ reasonCode: failureReasonCode,
344
+ protectedPaths: readProtectedPaths
345
+ });
346
+ if (recovery) {
347
+ if (editRecoveryDelivery.reserve(ctx, taskIdentity?.taskRunId ?? "", recovery.key)) {
348
+ editRecovery = recovery;
349
+ const injectedChars = recovery.text.length;
350
+ const injectedEstimatedTokens = estimateContextTokens(recovery.text);
351
+ const recoveryDetails = {
352
+ schemaVersion: 1,
353
+ targetPath: dependencies.redactText(recovery.targetPath),
354
+ contentHash: recovery.contentHash,
355
+ originalChars: recovery.originalChars,
356
+ injectedChars,
357
+ injectedEstimatedTokens,
358
+ sensitiveContentRedacted: recovery.redacted
359
+ };
360
+ resultDetails = isPlainRecord(resultDetails)
361
+ ? { ...resultDetails, piagentEditRecovery: recoveryDetails }
362
+ : { ...(resultDetails === undefined ? {} : { value: resultDetails }), piagentEditRecovery: recoveryDetails };
363
+ resultChanged = true;
364
+ dependencies.telemetry(ctx, {
365
+ event: "edit_recovery_context",
366
+ toolCallId: resultToolCallId,
367
+ taskRunId: taskIdentity?.taskRunId,
368
+ targetPath: dependencies.redactText(recovery.targetPath),
369
+ contentHash: recovery.contentHash,
370
+ originalChars: recovery.originalChars,
371
+ injectedChars,
372
+ injectedEstimatedTokens,
373
+ sensitiveContentRedacted: recovery.redacted
374
+ });
375
+ }
376
+ }
377
+
415
378
  const safeContent = redactToolResultTextContent(resultContent);
416
379
  const safeDetails = redactForStorage(resultDetails);
417
380
  const sensitiveValuesRedacted = safeContent.redacted + countChangedStringLeaves(resultDetails, safeDetails);
@@ -463,11 +426,22 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
463
426
  if (compactionCaptures.length > 0) {
464
427
  resultDetails = attachToolResultCompactionDetails(resultDetails, compactionCaptures);
465
428
  }
429
+ // Compact the original error independently, then append the already bounded
430
+ // and sanitized recovery snapshot. Otherwise a small but line-dense file can
431
+ // be compacted into a preview and cease to be a complete one-shot recovery.
432
+ if (editRecovery) {
433
+ resultContent = appendToolResultText(resultContent, editRecovery.text);
434
+ resultChanged = true;
435
+ }
466
436
 
467
437
  const resultFingerprintId = event.toolCallId ?? fingerprint.key;
468
438
  const record = dependencies.activity ?? dependencies.telemetry;
469
- const resultTarget = dependencies.extractLikelyPath(ctx.cwd, isPlainRecord(event.input) ? event.input : {});
439
+ const changedPaths = [...new Set([...directMutationResult.changedPaths, ...shellChangedPaths])]
440
+ .filter((filePath) => !matchesProtectedPath(filePath, readProtectedPaths))
441
+ .map((filePath) => dependencies.redactText(filePath))
442
+ .sort();
470
443
  const lineStats = patchLineStats(event.details);
444
+ const editRecoveryFailure = ["edit-anchor-not-unique", "edit-anchor-stale"].includes(String(failureReasonCode));
471
445
  record(ctx, {
472
446
  activityId: `result:${resultFingerprintId}`,
473
447
  event: "tool_result",
@@ -475,6 +449,7 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
475
449
  toolCallId: resultFingerprintId,
476
450
  toolName: event.toolName,
477
451
  targetPath: resultTarget ? dependencies.redactText(resultTarget) : undefined,
452
+ changedPaths: changedPaths.length > 0 ? changedPaths : undefined,
478
453
  inputHash: fingerprint.inputHash,
479
454
  outputHash: fingerprint.outputHash,
480
455
  outputChars: fingerprint.outputChars,
@@ -484,6 +459,13 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
484
459
  compactedCaptures: compactionCaptures.length,
485
460
  sensitiveValuesRedacted,
486
461
  isError: event.isError,
462
+ reasonCode: failureReasonCode,
463
+ // An explicit false is evidence too: it proves that every classified
464
+ // edit-anchor failure passed through the recovery policy even when the
465
+ // target was too large, private, protected, or otherwise ineligible.
466
+ editRecoveryContext: editRecoveryFailure ? Boolean(editRecovery) : undefined,
467
+ editRecoveryInjectedChars: editRecovery?.text.length,
468
+ editRecoveryEstimatedTokens: editRecovery ? estimateContextTokens(editRecovery.text) : undefined,
487
469
  exitCode: observed?.exitCode ?? (dependencies.isShellTool(event.toolName) ? event.isError ? 1 : 0 : undefined),
488
470
  exitCodeExact: observed?.exitCode !== undefined,
489
471
  ...lineStats,
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
 
3
3
  import { redactSensitiveText } from "../../extensions/redaction-core.js";
4
4
  import type { ActivityInspectorEvent } from "../product/activity-inspector.ts";
5
+ import { handledToolFailure } from "./tool-failure-classification.ts";
5
6
  import type { RuntimeEventDraft, RuntimeEventRevision } from "./runtime-event-store.ts";
6
7
 
7
8
  export type RuntimeEventIdentity = {
@@ -52,7 +53,8 @@ export function adaptActivityTelemetryEvent(input: ActivityEventAdapterInput): R
52
53
  const operationRef = hasOperation ? opaque("operation", input.identity.agentOperationId) : null;
53
54
  const toolCallRef = hasOperation ? opaque("tool", rawToolCall) : null;
54
55
  const activityType = hasOperation ? "tool" : ["bash", "shell", "exec"].includes(toolName) ? "command" : "other";
55
- const failed = eventName === "tool_result" && (event.isError === true || typeof event.exitCode === "number" && event.exitCode !== 0);
56
+ const resultFailure = eventName === "tool_result" && (event.isError === true || typeof event.exitCode === "number" && event.exitCode !== 0);
57
+ const failed = resultFailure && !handledToolFailure(event.reasonCode);
56
58
  const blocked = eventName === "tool_decision";
57
59
  // `tool_call` is emitted before policy authorization. Calling it "started"
58
60
  // made the Dashboard claim a command was running while it was still waiting
@@ -64,7 +66,7 @@ export function adaptActivityTelemetryEvent(input: ActivityEventAdapterInput): R
64
66
  const preview = safeText(previewValue, 4000);
65
67
  const storedRedactions = typeof event.sensitiveValuesRedacted === "number" && Number.isInteger(event.sensitiveValuesRedacted)
66
68
  ? Math.max(0, Math.min(10_000, event.sensitiveValuesRedacted)) : 0;
67
- const reasonCode = blocked ? "policy-blocked" : failed ? "tool-result-failed" : null;
69
+ const reasonCode = blocked ? "policy-blocked" : resultFailure ? safeText(event.reasonCode ?? "tool-result-failed", 80).text || "tool-result-failed" : null;
68
70
  return {
69
71
  ...base(input.identity, input.revision, observedAt), agentOperationId: operationRef, toolCallId: toolCallRef,
70
72
  kind, evidence: "derived",
@@ -0,0 +1,104 @@
1
+ import type { ActivityInspectorEvent } from "../product/activity-inspector.ts";
2
+
3
+ const RECOVERY_WINDOW_MS = 15 * 60 * 1_000;
4
+ const SHELL_TOOLS = new Set(["bash", "shell", "exec", "command"]);
5
+ const MUTATION_TOOLS = /(?:^|[._-])(?:apply[_-]?patch|edit|write|create[_-]?file|replace)(?:$|[._-])/i;
6
+
7
+ export type ActivityRecovery = {
8
+ recoveredAt: string;
9
+ recoveryToolCallId: string;
10
+ recoveryToolName: string;
11
+ exitCode: number | null;
12
+ exitCodeExact: boolean;
13
+ };
14
+
15
+ function resultFailed(event: ActivityInspectorEvent | undefined): boolean {
16
+ return Boolean(event && (event.isError === true || typeof event.exitCode === "number" && event.exitCode !== 0));
17
+ }
18
+
19
+ function time(value: unknown): number {
20
+ const parsed = typeof value === "string" ? Date.parse(value) : Number.NaN;
21
+ return Number.isFinite(parsed) ? parsed : Number.NaN;
22
+ }
23
+
24
+ function normalized(value: unknown): string {
25
+ return typeof value === "string" ? value.replaceAll("\\", "/").replace(/\s+/g, " ").trim() : "";
26
+ }
27
+
28
+ function family(toolName: unknown): "read" | "mutation" | "shell" | "other" {
29
+ const value = String(toolName ?? "").toLowerCase();
30
+ if (SHELL_TOOLS.has(value)) return "shell";
31
+ if (MUTATION_TOOLS.test(value)) return "mutation";
32
+ if (/(?:^|[._-])(?:read|document[_-]?read)(?:$|[._-])/.test(value)) return "read";
33
+ return "other";
34
+ }
35
+
36
+ function likelyCorrectedReadTarget(failed: ActivityInspectorEvent, candidate: ActivityInspectorEvent): boolean {
37
+ if (family(failed.toolName) !== "read" || family(candidate.toolName) !== "read") return false;
38
+ const failedPath = normalized(failed.targetPath).toLowerCase();
39
+ const candidatePath = normalized(candidate.targetPath).toLowerCase();
40
+ const failedSlash = failedPath.lastIndexOf("/"), candidateSlash = candidatePath.lastIndexOf("/");
41
+ if (!failedPath || !candidatePath || failedPath.slice(0, failedSlash) !== candidatePath.slice(0, candidateSlash)) return false;
42
+ const failedName = failedPath.slice(failedSlash + 1), candidateName = candidatePath.slice(candidateSlash + 1);
43
+ const failedExtension = failedName.includes(".") ? failedName.slice(failedName.lastIndexOf(".")) : "";
44
+ const candidateExtension = candidateName.includes(".") ? candidateName.slice(candidateName.lastIndexOf(".")) : "";
45
+ if (failedExtension !== candidateExtension) return false;
46
+ const tokens = (value: string) => new Set(value.slice(0, value.length - failedExtension.length).split(/[^a-z0-9]+/).filter(Boolean));
47
+ const failedTokens = tokens(failedName), candidateTokens = tokens(candidateName);
48
+ const overlap = [...failedTokens].filter((token) => candidateTokens.has(token)).length;
49
+ return overlap >= 2 && overlap === Math.min(failedTokens.size, candidateTokens.size);
50
+ }
51
+
52
+ function sameLogicalTarget(failed: ActivityInspectorEvent, candidate: ActivityInspectorEvent): boolean {
53
+ const failedPath = normalized(failed.targetPath), candidatePath = normalized(candidate.targetPath);
54
+ if (failedPath && candidatePath && failedPath === candidatePath) {
55
+ const failedFamily = family(failed.toolName), candidateFamily = family(candidate.toolName);
56
+ return failedFamily === candidateFamily || failedFamily === "read" && candidateFamily === "mutation";
57
+ }
58
+ const failedCommand = normalized(failed.command), candidateCommand = normalized(candidate.command);
59
+ return Boolean(failedCommand && candidateCommand && failedCommand === candidateCommand
60
+ && family(failed.toolName) === "shell" && family(candidate.toolName) === "shell");
61
+ }
62
+
63
+ export function recoveredToolCalls(events: readonly ActivityInspectorEvent[]): ReadonlyMap<string, ActivityRecovery> {
64
+ const results = new Map<string, { event: ActivityInspectorEvent; order: number }>();
65
+ for (let order = 0; order < events.length; order += 1) {
66
+ const event = events[order];
67
+ if (event?.event === "tool_result" && event.toolCallId) results.set(event.toolCallId, { event, order });
68
+ }
69
+ const segments = new Map<number, number>();
70
+ let segment = 0;
71
+ for (let order = 0; order < events.length; order += 1) {
72
+ if (events[order]?.event === "user_input") segment += 1;
73
+ segments.set(order, segment);
74
+ }
75
+ const calls = events.map((event, order) => ({ event, order }))
76
+ .filter((entry) => entry.event?.event === "tool_call" && Boolean(entry.event.toolCallId));
77
+ const recovered = new Map<string, ActivityRecovery>();
78
+ for (let failedCallIndex = 0; failedCallIndex < calls.length; failedCallIndex += 1) {
79
+ const failedCall = calls[failedCallIndex];
80
+ const failedResult = results.get(failedCall.event.toolCallId!);
81
+ if (!failedResult || !resultFailed(failedResult.event)) continue;
82
+ const failedAt = time(failedResult.event.recordedAt ?? failedCall.event.recordedAt);
83
+ const match = calls.find((candidate, candidateIndex) => {
84
+ if (candidate.order <= failedResult.order || segments.get(candidate.order) !== segments.get(failedCall.order)) return false;
85
+ const candidateResult = results.get(candidate.event.toolCallId!);
86
+ if (!candidateResult || resultFailed(candidateResult.event)) return false;
87
+ const recoveredAt = time(candidateResult.event.recordedAt ?? candidate.event.recordedAt);
88
+ return Number.isFinite(failedAt) && Number.isFinite(recoveredAt) && recoveredAt >= failedAt
89
+ && recoveredAt - failedAt <= RECOVERY_WINDOW_MS
90
+ && (sameLogicalTarget(failedCall.event, candidate.event)
91
+ || candidateIndex === failedCallIndex + 1 && likelyCorrectedReadTarget(failedCall.event, candidate.event));
92
+ });
93
+ if (!match) continue;
94
+ const recoveryResult = results.get(match.event.toolCallId!)!.event;
95
+ recovered.set(failedCall.event.toolCallId!, {
96
+ recoveredAt: String(recoveryResult.recordedAt ?? match.event.recordedAt),
97
+ recoveryToolCallId: match.event.toolCallId!,
98
+ recoveryToolName: String(match.event.toolName ?? "unknown"),
99
+ exitCode: typeof recoveryResult.exitCode === "number" ? recoveryResult.exitCode : null,
100
+ exitCodeExact: recoveryResult.exitCodeExact === true
101
+ });
102
+ }
103
+ return recovered;
104
+ }
@@ -96,7 +96,7 @@ export function projectTaskCompactionHistory(input: {
96
96
  let malformed = inspected.corruptions;
97
97
  const matching = inspected.records.flatMap((record: any, ordinal: number) => {
98
98
  if (record?.taskId !== input.task.taskId || record?.taskRunId !== input.task.taskRunId || record?.sessionId !== input.task.sessionId) return [];
99
- if (record.schemaVersion !== 1 || !timestamp(record.recordedAt)) { malformed += 1; return []; }
99
+ if (![1, 2].includes(record.schemaVersion) || !timestamp(record.recordedAt)) { malformed += 1; return []; }
100
100
  if (record.event === "session_compact") {
101
101
  if (typeof record.willRetry !== "boolean" || typeof record.fromExtension !== "boolean") { malformed += 1; return []; }
102
102
  return [{ ordinal, recordedAt: record.recordedAt, kind: "context-compaction", title: "Context đã được compact",