@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
@@ -60,7 +60,7 @@ export function projectTaskHandoffHistory(input: { cwd: string; task: Task; iden
60
60
  const matching = inspected.records.flatMap((record: any, ordinal: number) => {
61
61
  if (record?.event !== "handoff_projection_written" || record.taskId !== input.task.taskId
62
62
  || record.taskRunId !== input.task.taskRunId || record.sessionId !== input.task.sessionId) return [];
63
- if (record.schemaVersion !== 1 || !timestamp(record.recordedAt) || typeof record.completionApproved !== "boolean") { malformed += 1; return []; }
63
+ if (![1, 2].includes(record.schemaVersion) || !timestamp(record.recordedAt) || typeof record.completionApproved !== "boolean") { malformed += 1; return []; }
64
64
  const action = display(record.recoveryAction, 80) || "unknown", phase = display(record.phase, 64) || null;
65
65
  return [{ ordinal, recordedAt: record.recordedAt, phase, completionApproved: record.completionApproved, projectedAction: action }];
66
66
  });
@@ -0,0 +1,53 @@
1
+ export type ToolFailureReasonCode =
2
+ | "target-not-found"
3
+ | "search-target-missing"
4
+ | "edit-anchor-not-unique"
5
+ | "edit-anchor-stale"
6
+ | "tool-result-failed";
7
+
8
+ function boundedText(content: unknown): string {
9
+ const text = typeof content === "string" ? content : Array.isArray(content)
10
+ ? content.filter((item) => item && typeof item === "object" && (item as { type?: unknown }).type === "text")
11
+ .map((item) => String((item as { text?: unknown }).text ?? "")).join("\n")
12
+ : "";
13
+ return text.slice(0, 8_192);
14
+ }
15
+
16
+ function commandFromInput(input: unknown): string {
17
+ if (!input || typeof input !== "object") return "";
18
+ const direct = (input as { command?: unknown }).command;
19
+ if (typeof direct === "string") return direct;
20
+ const args = (input as { args?: unknown }).args;
21
+ return args && typeof args === "object" && typeof (args as { command?: unknown }).command === "string"
22
+ ? (args as { command: string }).command : "";
23
+ }
24
+
25
+ export function classifyToolFailure(toolName: string, isError: boolean, content: unknown, input?: unknown): ToolFailureReasonCode | null {
26
+ if (!isError) return null;
27
+ const text = boundedText(content);
28
+ // Only the registered, policy-governed `edit` tool may receive an automatic
29
+ // current-file recovery snapshot. A similarly named third-party `replace`
30
+ // tool is not necessarily covered by Piagent's mutation guard.
31
+ if (/^edit$/i.test(toolName)) {
32
+ if (/\b(?:oldtext|old text)\b[^\n]{0,240}\b(?:unique|occurrences?)\b|\bfound\s+\d+\s+occurrences?\b/i.test(text)) {
33
+ return "edit-anchor-not-unique";
34
+ }
35
+ if (/could not find (?:the )?exact text|\b(?:oldtext|old text)\b[^\n]{0,240}\b(?:match|mismatch|stale|changed|find)\b|\b(?:generation|content) drift\b/i.test(text)) {
36
+ return "edit-anchor-stale";
37
+ }
38
+ }
39
+ const readLike = /(?:^|[._-])(?:read|document[_-]?read)(?:$|[._-])/i.test(toolName);
40
+ if (readLike && /\bENOENT\b|no such file or directory|file (?:does not exist|not found)|cannot find the (?:file|path)/i.test(text)) {
41
+ return "target-not-found";
42
+ }
43
+ const shellLike = /^(?:bash|shell|exec|command)$/i.test(toolName);
44
+ const searchCommand = /^\s*(?:rg|grep|git\s+grep)\b/.test(commandFromInput(input));
45
+ const missingTarget = /no such file or directory|cannot find the (?:file|path)|\bos error 2\b/i.test(text);
46
+ const usefulMatches = text.split(/\r?\n/).some((line) => !/^\s*(?:rg|grep):/i.test(line) && /^.+?:\d+:/.test(line));
47
+ if (shellLike && searchCommand && missingTarget && usefulMatches) return "search-target-missing";
48
+ return "tool-result-failed";
49
+ }
50
+
51
+ export function handledToolFailure(reasonCode: unknown): boolean {
52
+ return reasonCode === "search-target-missing";
53
+ }
@@ -8,6 +8,8 @@ import { workingTreeEvidenceDigest, workingTreeSnapshotUsesCurrentAlgorithm } fr
8
8
  import { inspectTaskContinuationBudget } from "../recovery/continuation-budget.ts";
9
9
  import { readHandoffProjection } from "../recovery/handoff-projection.ts";
10
10
  import type { ActivityInspectorEvent, CurrentActivity } from "../product/activity-inspector.ts";
11
+ import { recoveredToolCalls } from "./activity-recovery.ts";
12
+ import { classifyToolFailure, handledToolFailure } from "./tool-failure-classification.ts";
11
13
  import { projectCriteriaFileVerifier, type CriteriaLinkProjection } from "./criteria-links.ts";
12
14
  import { collectSourceChangeViews, type SourceChangeDocument, type WebUiIdentity } from "./source-change-projection.ts";
13
15
 
@@ -82,6 +84,27 @@ function scopedEvents(events: ActivityInspectorEvent[], sessionId: string, task?
82
84
  return [...deduped.values()].sort((left, right) => String(left.recordedAt).localeCompare(String(right.recordedAt))).slice(-2_000);
83
85
  }
84
86
 
87
+ function enrichedFailureReasons(events: ActivityInspectorEvent[], sessionEntries: unknown[]): ActivityInspectorEvent[] {
88
+ const inputs = new Map<string, unknown>(), results = new Map<string, { content: unknown; isError: boolean; toolName: string }>();
89
+ for (const entry of sessionEntries) {
90
+ if (!entry || typeof entry !== "object" || (entry as any).type !== "message") continue;
91
+ const message = (entry as any).message;
92
+ if (message?.role === "assistant" && Array.isArray(message.content)) {
93
+ for (const block of message.content) if (block?.type === "toolCall" && typeof block.id === "string") inputs.set(block.id, block.arguments);
94
+ }
95
+ if (message?.role === "toolResult" && typeof message.toolCallId === "string") {
96
+ results.set(message.toolCallId, { content: message.content, isError: message.isError === true, toolName: String(message.toolName ?? "unknown") });
97
+ }
98
+ }
99
+ return events.map((event) => {
100
+ if (event.event !== "tool_result" || !event.toolCallId || !event.isError) return event;
101
+ const raw = results.get(event.toolCallId);
102
+ if (!raw) return event;
103
+ const reasonCode = classifyToolFailure(raw.toolName, raw.isError, raw.content, inputs.get(event.toolCallId));
104
+ return reasonCode ? { ...event, reasonCode } : event;
105
+ });
106
+ }
107
+
85
108
  function sourceSummary(document: SourceChangeDocument | null, view: "task" | "working-tree" | "staged") {
86
109
  if (!document || document.availability.state === "unavailable") return {
87
110
  view, base: view === "task" ? "task-baseline" : "HEAD", state: "unavailable", revision: null,
@@ -109,21 +132,26 @@ function sourceSummary(document: SourceChangeDocument | null, view: "task" | "wo
109
132
  function activityProjection(events: ActivityInspectorEvent[], current: CurrentActivity[]) {
110
133
  const results = new Map(events.filter((event) => event.event === "tool_result").map((event) => [event.toolCallId, event]));
111
134
  const decisions = new Map(events.filter((event) => event.event === "tool_decision").map((event) => [event.toolCallId, event]));
135
+ const recoveries = recoveredToolCalls(events);
112
136
  const calls = events.filter((event) => event.event === "tool_call").slice(-200);
113
137
  const projected = calls.map((call) => {
114
138
  const result = results.get(call.toolCallId), decision = decisions.get(call.toolCallId);
115
139
  const blocked = decision?.decision === "blocked";
116
- const failed = !blocked && Boolean(result && (result.isError === true || (typeof result.exitCode === "number" && result.exitCode !== 0)));
117
- const state = blocked ? "blocked" : !result ? "running" : failed ? "failed" : "passed";
140
+ const handled = Boolean(result && handledToolFailure(result.reasonCode));
141
+ const failed = !blocked && !handled && Boolean(result && (result.isError === true || (typeof result.exitCode === "number" && result.exitCode !== 0)));
142
+ const recovery = call.toolCallId ? recoveries.get(call.toolCallId) : undefined;
143
+ const state = blocked ? "blocked" : !result ? "running" : failed && !recovery ? "failed" : "passed";
118
144
  const toolName = display(call.toolName ?? "unknown", 80).replace(/[^A-Za-z0-9._:@~-]/g, "-") || "unknown";
119
145
  const rawId = String(call.toolCallId ?? call.activityId ?? `${call.recordedAt}:${toolName}`);
120
146
  const command = display(call.command ?? call.targetPath ?? toolName, 65_536);
121
147
  return {
122
148
  activityRef: token("activity", [rawId, call.recordedAt]), kind: ["bash", "shell", "exec"].includes(toolName) ? "command" : "tool", state,
123
- label: display(state === "running" ? `${toolName} running` : `${toolName} ${state}`, 500), preview: command,
149
+ label: display(recovery ? `${toolName} recovered` : handled ? `${toolName} warning` : state === "running" ? `${toolName} running` : `${toolName} ${state}`, 500), preview: command,
124
150
  toolCallId: token("tool", rawId), toolName, commandDigest: call.command ? `sha256:${hash(call.command)}` : null,
125
- logRef: null, exitCode: typeof result?.exitCode === "number" ? result.exitCode : null, exitCodeExact: result?.exitCodeExact === true,
126
- startedAt: timestamp(call.recordedAt), finishedAt: result || blocked ? timestamp(result?.recordedAt ?? decision?.recordedAt, timestamp(call.recordedAt)) : null
151
+ logRef: null, exitCode: recovery?.exitCode ?? (typeof result?.exitCode === "number" ? result.exitCode : null),
152
+ exitCodeExact: recovery?.exitCodeExact ?? (result?.exitCodeExact === true),
153
+ startedAt: timestamp(call.recordedAt), finishedAt: recovery ? timestamp(recovery.recoveredAt, timestamp(call.recordedAt))
154
+ : result || blocked ? timestamp(result?.recordedAt ?? decision?.recordedAt, timestamp(call.recordedAt)) : null
127
155
  };
128
156
  });
129
157
  const callIds = new Set(calls.map((call) => String(call.toolCallId ?? "")));
@@ -274,7 +302,7 @@ export async function buildWebUiInspectionProjection(input: {
274
302
  const taskRevision = input.task ? webUiTaskRevision(input.task) : null;
275
303
  const views = await collectSourceChangeViews({ cwd: input.cwd, identity, generatedAt, taskRevision,
276
304
  isProtectedPath: (_root, repoPath) => Boolean(matchesProtectedPath(repoPath, effectiveProtectedPaths)) });
277
- const events = scopedEvents(input.events ?? [], input.sessionId, input.task);
305
+ const events = enrichedFailureReasons(scopedEvents(input.events ?? [], input.sessionId, input.task), input.sessionEntries ?? []);
278
306
  const linked = projectCriteriaFileVerifier({ cwd: input.cwd, task: input.task, sourceViews: views, currentSnapshot,
279
307
  protectedPaths: input.protectedPaths, events, at: new Date(generatedAt) });
280
308
  const sourceChanges = { task: sourceSummary(linked.sourceViews.task, "task"), workingTree: sourceSummary(linked.sourceViews.workingTree, "working-tree"), staged: sourceSummary(linked.sourceViews.staged, "staged") };
@@ -50,14 +50,22 @@ function positiveInteger(value: unknown): number | null {
50
50
  return Number.isInteger(value) && Number(value) > 0 && Number(value) <= 10_000_000 ? Number(value) : null;
51
51
  }
52
52
 
53
- function thinkingLevels(value: unknown): string[] | null {
54
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
55
- const levels = Object.entries(value)
56
- .filter(([, mapped]) => mapped !== null && mapped !== false)
57
- .map(([level]) => text(level))
58
- .filter(Boolean)
59
- .sort();
60
- return levels.length > 0 ? [...new Set(levels)].slice(0, 16) : [];
53
+ const PI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
54
+
55
+ function thinkingLevels(value: ModelLike): string[] | null {
56
+ if (typeof value.reasoning !== "boolean") return null;
57
+ if (!value.reasoning) return ["off"];
58
+ const mapping = value.thinkingLevelMap && typeof value.thinkingLevelMap === "object" && !Array.isArray(value.thinkingLevelMap)
59
+ ? value.thinkingLevelMap as Record<string, unknown>
60
+ : {};
61
+ // Match Pi's getSupportedThinkingLevels semantics. Reasoning models support
62
+ // the standard host levels through `high` unless a model explicitly marks a
63
+ // level null. `xhigh` and `max` are opt-in capabilities.
64
+ return PI_THINKING_LEVELS.filter((level) => {
65
+ const mapped = mapping[level];
66
+ if (mapped === null) return false;
67
+ return level === "xhigh" || level === "max" ? mapped !== undefined : true;
68
+ });
61
69
  }
62
70
 
63
71
  function boundedModel(value: ModelLike): AuthenticatedModelCatalogEntry | undefined {
@@ -71,7 +79,7 @@ function boundedModel(value: ModelLike): AuthenticatedModelCatalogEntry | undefi
71
79
  contextWindow: positiveInteger(value.contextWindow),
72
80
  reasoning: typeof value.reasoning === "boolean" ? value.reasoning : null,
73
81
  imageInput: input ? input.includes("image") : null,
74
- supportedThinkingLevels: thinkingLevels(value.thinkingLevelMap)
82
+ supportedThinkingLevels: thinkingLevels(value)
75
83
  };
76
84
  }
77
85
 
@@ -0,0 +1,83 @@
1
+ type ProviderPayload = Record<string, unknown>;
2
+
3
+ const GPT56_CODEX_MODEL = /^gpt-5\.6-(?:luna|sol|terra)$/;
4
+ const PROVIDER_EFFORT_BY_HOST_LEVEL: Readonly<Record<string, string>> = Object.freeze({
5
+ off: "none",
6
+ minimal: "low",
7
+ low: "low",
8
+ medium: "medium",
9
+ high: "high",
10
+ xhigh: "xhigh",
11
+ max: "max"
12
+ });
13
+
14
+ export type OpenAiCodexReasoningNormalization = {
15
+ payload: unknown;
16
+ applicable: boolean;
17
+ changed: boolean;
18
+ hostThinkingLevel: string;
19
+ expectedProviderEffort: string | null;
20
+ providerEffort: string | null;
21
+ reasonCode: "not-applicable" | "observed" | "off-normalized-to-none" | "minimal-normalized-to-low" | "provider-effort-normalized";
22
+ };
23
+
24
+ function record(value: unknown): ProviderPayload | null {
25
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value as ProviderPayload : null;
26
+ }
27
+
28
+ function effortFromPayload(payload: ProviderPayload): string | null {
29
+ const reasoning = record(payload.reasoning);
30
+ return typeof reasoning?.effort === "string" ? reasoning.effort : null;
31
+ }
32
+
33
+ /**
34
+ * Compatibility bridge for the pinned Pi host's OpenAI Codex serializer.
35
+ *
36
+ * Pi names the provider's `none` effort `off`. Pi 0.84.1 currently drops
37
+ * `off` before its request builder, which omits the reasoning object and lets
38
+ * GPT-5.6 use its server default. Enforce the same-model/effort invariant for
39
+ * every host level, while keeping the correction deliberately narrow to the
40
+ * three authenticated GPT-5.6 Codex models whose capability map we audit.
41
+ */
42
+ export function normalizeOpenAiCodexReasoningPayload(input: {
43
+ payload: unknown;
44
+ provider: unknown;
45
+ modelId: unknown;
46
+ hostThinkingLevel: unknown;
47
+ }): OpenAiCodexReasoningNormalization {
48
+ const provider = String(input.provider ?? "");
49
+ const modelId = String(input.modelId ?? "");
50
+ const hostThinkingLevel = String(input.hostThinkingLevel ?? "");
51
+ const payload = record(input.payload);
52
+ const expectedProviderEffort = PROVIDER_EFFORT_BY_HOST_LEVEL[hostThinkingLevel] ?? null;
53
+ const applicable = provider === "openai-codex" && GPT56_CODEX_MODEL.test(modelId)
54
+ && payload !== null && payload.model === modelId && expectedProviderEffort !== null;
55
+ if (!applicable || !payload) {
56
+ return { payload: input.payload, applicable: false, changed: false, hostThinkingLevel,
57
+ expectedProviderEffort, providerEffort: payload ? effortFromPayload(payload) : null, reasonCode: "not-applicable" };
58
+ }
59
+
60
+ const currentEffort = effortFromPayload(payload);
61
+ if (currentEffort === expectedProviderEffort) {
62
+ return { payload: input.payload, applicable: true, changed: false, hostThinkingLevel,
63
+ expectedProviderEffort, providerEffort: currentEffort, reasonCode: "observed" };
64
+ }
65
+
66
+ const currentReasoning = record(payload.reasoning) ?? {};
67
+ const normalizedReasoning = {
68
+ ...currentReasoning,
69
+ effort: expectedProviderEffort,
70
+ ...(typeof currentReasoning.summary === "string" ? {} : { summary: "auto" })
71
+ };
72
+ return {
73
+ payload: { ...payload, reasoning: normalizedReasoning },
74
+ applicable: true,
75
+ changed: true,
76
+ hostThinkingLevel,
77
+ expectedProviderEffort,
78
+ providerEffort: expectedProviderEffort,
79
+ reasonCode: hostThinkingLevel === "off" ? "off-normalized-to-none"
80
+ : hostThinkingLevel === "minimal" ? "minimal-normalized-to-low"
81
+ : "provider-effort-normalized"
82
+ };
83
+ }
@@ -0,0 +1,238 @@
1
+ import crypto from "node:crypto";
2
+
3
+ type JsonRecord = Record<string, unknown>;
4
+ type BoundReason = "instructions-unavailable" | "instructions-too-large" | "tools-invalid" | "tools-too-large"
5
+ | "input-invalid" | "input-too-large" | "deferred-tools-invalid" | "deferred-tools-too-large"
6
+ | "surface-too-deep" | "surface-too-large" | "surface-not-json" | "surface-cyclic";
7
+
8
+ const MAX_INSTRUCTIONS_CHARS = 1_000_000;
9
+ const MAX_TOOLS = 512;
10
+ const MAX_INPUT_ITEMS = 4_096;
11
+ const MAX_SURFACE_DEPTH = 32;
12
+ const MAX_SURFACE_NODES = 50_000;
13
+ const MAX_SURFACE_STRING_CHARS = 2_000_000;
14
+ const MAX_CONTAINER_ITEMS = 4_096;
15
+ const PUBLIC_ID = /^[A-Za-z0-9][A-Za-z0-9._:@~-]{0,159}$/;
16
+ const CONTROL_VALUE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
17
+
18
+ export type ProviderWireFingerprint = {
19
+ applicable: boolean;
20
+ state: "known" | "unavailable";
21
+ reasonCode: BoundReason | "not-applicable" | null;
22
+ modelId: string | null;
23
+ instructionsHash: string | null;
24
+ baseInstructionsHash: string | null;
25
+ instructionNormalization: "host-relocation-v1" | "none" | null;
26
+ instructionChars: number | null;
27
+ orderedToolSurfaceHash: string | null;
28
+ toolCount: number | null;
29
+ deferredToolSurfaceHash: string | null;
30
+ deferredToolCount: number | null;
31
+ deferredToolBatchCount: number | null;
32
+ reasoningEffort: string | null;
33
+ textVerbosity: string | null;
34
+ toolChoiceKind: string | null;
35
+ toolChoiceHash: string | null;
36
+ requestPrefixFingerprint: string | null;
37
+ };
38
+
39
+ class SurfaceBoundError extends Error {
40
+ readonly reasonCode: BoundReason;
41
+ constructor(reasonCode: BoundReason) { super(reasonCode); this.reasonCode = reasonCode; }
42
+ }
43
+
44
+ function record(value: unknown): JsonRecord | null {
45
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value as JsonRecord : null;
46
+ }
47
+
48
+ function hash(value: string): string {
49
+ return crypto.createHash("sha256").update(value).digest("hex");
50
+ }
51
+
52
+ function control(value: unknown): string | null {
53
+ return typeof value === "string" && CONTROL_VALUE.test(value) ? value : null;
54
+ }
55
+
56
+ function canonicalJson(value: unknown): string {
57
+ const budget = { nodes: 0, stringChars: 0, active: new WeakSet<object>() };
58
+ const visit = (item: unknown, depth: number, inArray: boolean): unknown => {
59
+ budget.nodes += 1;
60
+ if (budget.nodes > MAX_SURFACE_NODES) throw new SurfaceBoundError("surface-too-large");
61
+ if (depth > MAX_SURFACE_DEPTH) throw new SurfaceBoundError("surface-too-deep");
62
+ if (item === null || typeof item === "boolean") return item;
63
+ if (typeof item === "string") {
64
+ budget.stringChars += item.length;
65
+ if (budget.stringChars > MAX_SURFACE_STRING_CHARS) throw new SurfaceBoundError("surface-too-large");
66
+ return item;
67
+ }
68
+ if (typeof item === "number") return Number.isFinite(item) ? item : null;
69
+ if (["undefined", "function", "symbol"].includes(typeof item)) return inArray ? null : undefined;
70
+ if (typeof item === "bigint" || typeof item !== "object") throw new SurfaceBoundError("surface-not-json");
71
+ if (budget.active.has(item)) throw new SurfaceBoundError("surface-cyclic");
72
+ budget.active.add(item);
73
+ try {
74
+ if (Array.isArray(item)) {
75
+ if (item.length > MAX_CONTAINER_ITEMS) throw new SurfaceBoundError("surface-too-large");
76
+ return item.map((entry) => visit(entry, depth + 1, true));
77
+ }
78
+ const entries = Object.entries(item as JsonRecord)
79
+ .filter(([, entry]) => !["undefined", "function", "symbol"].includes(typeof entry));
80
+ if (entries.length > MAX_CONTAINER_ITEMS) throw new SurfaceBoundError("surface-too-large");
81
+ entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
82
+ const output: JsonRecord = {};
83
+ for (const [key, entry] of entries) {
84
+ budget.stringChars += key.length;
85
+ if (budget.stringChars > MAX_SURFACE_STRING_CHARS) throw new SurfaceBoundError("surface-too-large");
86
+ output[key] = visit(entry, depth + 1, false);
87
+ }
88
+ return output;
89
+ } finally {
90
+ budget.active.delete(item);
91
+ }
92
+ };
93
+ return JSON.stringify(visit(value, 0, false));
94
+ }
95
+
96
+ function unavailable(modelId: string, reasonCode: BoundReason): ProviderWireFingerprint {
97
+ return { applicable: true, state: "unavailable", reasonCode, modelId, instructionsHash: null,
98
+ baseInstructionsHash: null, instructionNormalization: null, instructionChars: null,
99
+ orderedToolSurfaceHash: null, toolCount: null, deferredToolSurfaceHash: null, deferredToolCount: null,
100
+ deferredToolBatchCount: null, reasoningEffort: null, textVerbosity: null, toolChoiceKind: null,
101
+ toolChoiceHash: null, requestPrefixFingerprint: null };
102
+ }
103
+
104
+ function xmlEscape(value: string): string {
105
+ return value
106
+ .replace(/&/g, "&amp;")
107
+ .replace(/</g, "&lt;")
108
+ .replace(/>/g, "&gt;")
109
+ .replace(/"/g, "&quot;")
110
+ .replace(/'/g, "&apos;");
111
+ }
112
+
113
+ function boundedHostPaths(value: unknown): string[] {
114
+ if (typeof value !== "string" || value.length === 0 || value.length > 32_768 || value.includes("\0")) return [];
115
+ if (/^[\\/]+$/.test(value) || /^[A-Za-z]:[\\/]?$/.test(value)) return [];
116
+ const native = value.replace(/[\\/]+$/, "");
117
+ const slash = native.replace(/\\/g, "/");
118
+ return [...new Set([native, slash].filter(Boolean))];
119
+ }
120
+
121
+ function normalizeSkillLocations(instructions: string, platformRoot: unknown): string {
122
+ const roots = boundedHostPaths(platformRoot);
123
+ if (roots.length === 0) return instructions;
124
+ const startTag = "<available_skills>";
125
+ const endTag = "</available_skills>";
126
+ let cursor = 0;
127
+ let output = "";
128
+ while (cursor < instructions.length) {
129
+ const start = instructions.indexOf(startTag, cursor);
130
+ if (start < 0) return output + instructions.slice(cursor);
131
+ const endStart = instructions.indexOf(endTag, start + startTag.length);
132
+ if (endStart < 0) return output + instructions.slice(cursor);
133
+ const end = endStart + endTag.length;
134
+ let section = instructions.slice(start, end);
135
+ for (const root of roots) {
136
+ const locationPrefix = `<location>${xmlEscape(root)}`;
137
+ section = section
138
+ .replaceAll(`${locationPrefix}/`, "<location>[PIAGENT_PLATFORM_ROOT]/")
139
+ .replaceAll(`${locationPrefix}\\`, "<location>[PIAGENT_PLATFORM_ROOT]/")
140
+ .replaceAll(`${locationPrefix}</location>`, "<location>[PIAGENT_PLATFORM_ROOT]</location>");
141
+ }
142
+ output += instructions.slice(cursor, start) + section;
143
+ cursor = end;
144
+ }
145
+ return output;
146
+ }
147
+
148
+ function normalizeHostOwnedRelocation(instructions: string, workingDirectory: unknown, platformRoot: unknown): {
149
+ value: string;
150
+ mode: "host-relocation-v1" | "none";
151
+ } {
152
+ const cwdVariants = boundedHostPaths(workingDirectory);
153
+ let value = normalizeSkillLocations(instructions, platformRoot);
154
+ for (const cwd of cwdVariants) {
155
+ const cwdLine = `Current working directory: ${cwd}`;
156
+ if (value.endsWith(cwdLine)) {
157
+ value = `${value.slice(0, -cwdLine.length)}Current working directory: [PIAGENT_PROJECT_ROOT]`;
158
+ }
159
+ value = value
160
+ .replaceAll(`<project_instructions path="${cwd}">`, '<project_instructions path="[PIAGENT_PROJECT_ROOT]">')
161
+ .replaceAll(`<project_instructions path="${cwd}/`, '<project_instructions path="[PIAGENT_PROJECT_ROOT]/')
162
+ .replaceAll(`<project_instructions path="${cwd}\\`, '<project_instructions path="[PIAGENT_PROJECT_ROOT]/');
163
+ }
164
+ return value === instructions
165
+ ? { value, mode: "none" }
166
+ : { value, mode: "host-relocation-v1" };
167
+ }
168
+
169
+ export function buildOpenAiCodexWireFingerprint(input: {
170
+ payload: unknown;
171
+ provider: unknown;
172
+ modelId: unknown;
173
+ workingDirectory?: unknown;
174
+ platformRoot?: unknown;
175
+ }): ProviderWireFingerprint {
176
+ const payload = record(input.payload);
177
+ const provider = typeof input.provider === "string" ? input.provider : "";
178
+ const modelId = typeof input.modelId === "string" && PUBLIC_ID.test(input.modelId) ? input.modelId : "";
179
+ if (provider !== "openai-codex" || !payload || !modelId || payload.model !== modelId) {
180
+ return { ...unavailable("", "surface-not-json"), applicable: false, state: "unavailable",
181
+ reasonCode: "not-applicable", modelId: null };
182
+ }
183
+ if (typeof payload.instructions !== "string") return unavailable(modelId, "instructions-unavailable");
184
+ if (payload.instructions.length > MAX_INSTRUCTIONS_CHARS) return unavailable(modelId, "instructions-too-large");
185
+ const tools = payload.tools === undefined ? [] : payload.tools;
186
+ if (!Array.isArray(tools)) return unavailable(modelId, "tools-invalid");
187
+ if (tools.length > MAX_TOOLS) return unavailable(modelId, "tools-too-large");
188
+ const requestInput = payload.input === undefined ? [] : payload.input;
189
+ if (!Array.isArray(requestInput)) return unavailable(modelId, "input-invalid");
190
+ if (requestInput.length > MAX_INPUT_ITEMS) return unavailable(modelId, "input-too-large");
191
+
192
+ const deferredBatches: Array<{ inputIndex: number; tools: unknown[] }> = [];
193
+ let deferredToolCount = 0;
194
+ for (let index = 0; index < requestInput.length; index += 1) {
195
+ const item = record(requestInput[index]);
196
+ if (item?.type !== "tool_search_output") continue;
197
+ if (!Array.isArray(item.tools)) return unavailable(modelId, "deferred-tools-invalid");
198
+ deferredToolCount += item.tools.length;
199
+ if (deferredToolCount > MAX_TOOLS) return unavailable(modelId, "deferred-tools-too-large");
200
+ deferredBatches.push({ inputIndex: index, tools: item.tools });
201
+ }
202
+
203
+ try {
204
+ const instructionsHash = hash(payload.instructions);
205
+ const normalizedInstructions = normalizeHostOwnedRelocation(payload.instructions, input.workingDirectory, input.platformRoot);
206
+ const baseInstructionsHash = hash(normalizedInstructions.value);
207
+ // Object keys are canonicalized because their order is semantic noise;
208
+ // arrays are preserved so changing provider-visible tool order is detected.
209
+ const orderedToolSurfaceHash = hash(canonicalJson(tools));
210
+ const deferredToolSurfaceHash = hash(canonicalJson(deferredBatches));
211
+ const toolChoice = payload.tool_choice ?? null;
212
+ const toolChoiceHash = hash(canonicalJson(toolChoice));
213
+ const reasoningEffort = control(record(payload.reasoning)?.effort);
214
+ const textVerbosity = control(record(payload.text)?.verbosity);
215
+ const toolChoiceKind = typeof toolChoice === "string" ? control(toolChoice) : record(toolChoice) ? "structured" : "none";
216
+ const requestPrefixFingerprint = hash(JSON.stringify({
217
+ schemaVersion: 1,
218
+ modelId,
219
+ instructionsHash,
220
+ baseInstructionsHash,
221
+ orderedToolSurfaceHash,
222
+ toolCount: tools.length,
223
+ deferredToolSurfaceHash,
224
+ deferredToolCount,
225
+ deferredToolBatchCount: deferredBatches.length,
226
+ reasoningEffort,
227
+ textVerbosity,
228
+ toolChoiceHash
229
+ }));
230
+ return { applicable: true, state: "known", reasonCode: null, modelId, instructionsHash,
231
+ baseInstructionsHash, instructionNormalization: normalizedInstructions.mode,
232
+ instructionChars: payload.instructions.length, orderedToolSurfaceHash, toolCount: tools.length,
233
+ deferredToolSurfaceHash, deferredToolCount, deferredToolBatchCount: deferredBatches.length,
234
+ reasoningEffort, textVerbosity, toolChoiceKind, toolChoiceHash, requestPrefixFingerprint };
235
+ } catch (error) {
236
+ return unavailable(modelId, error instanceof SurfaceBoundError ? error.reasonCode : "surface-not-json");
237
+ }
238
+ }
@@ -87,7 +87,7 @@ export function defaultRolePolicy(role: HelperRole, scope: string[] = ["**"]): R
87
87
  const readOnly = role !== "worker";
88
88
  const policy: RolePolicy = {
89
89
  schemaVersion: 1, policyVersion: ROLE_POLICY_VERSION, role, authority: readOnly ? "read-only" : "single-writer",
90
- enabledByDefault: readOnly, readScope: [...scope], writeScope: [], allowedTools: readOnly ? [...READ_TOOLS] : [...READ_TOOLS, "bash", "edit", "write", "contact_supervisor"],
90
+ enabledByDefault: readOnly, readScope: [...scope], writeScope: [], allowedTools: readOnly ? [...READ_TOOLS] : [...READ_TOOLS, "bash", "edit", "write", "apply_patch", "contact_supervisor"],
91
91
  modelSelectionSource: "runtime-catalog", effortSelectionSource: "runtime-snapshot", contextBudget: role === "oracle" ? 12000 : role === "reviewer" ? 9000 : 6000,
92
92
  ceilings: { timeSeconds: role === "oracle" ? 900 : 600, calls: role === "retriever" ? 20 : 40, retries: 0 },
93
93
  outputSchema: `${role}-result-v1`, stoppingRule: "Stop when the bounded objective is answered or evidence is insufficient; return uncertainty.",
@@ -3,6 +3,8 @@ import {
3
3
  buildWebUiInspectionProjection,
4
4
  type WebUiInspectionProjection
5
5
  } from "../inspection/webui-snapshot.ts";
6
+ import { recoveredToolCalls } from "../inspection/activity-recovery.ts";
7
+ import { handledToolFailure } from "../inspection/tool-failure-classification.ts";
6
8
 
7
9
  export const ACTIVITY_INSPECTOR_VERSION = "activity-inspector-v2" as const;
8
10
 
@@ -18,6 +20,7 @@ export type ActivityInspectorEvent = {
18
20
  command?: string;
19
21
  decision?: string;
20
22
  reason?: string;
23
+ reasonCode?: string;
21
24
  warningKind?: string;
22
25
  isError?: boolean;
23
26
  exitCode?: number;
@@ -46,13 +49,15 @@ function isTestPath(file: string): boolean { return /(^|\/)(?:test|tests|spec|__
46
49
  function failedToolResult(event: ActivityInspectorEvent): boolean { return event.isError === true || (typeof event.exitCode === "number" && event.exitCode !== 0); }
47
50
 
48
51
  function commandProjection(events: ActivityInspectorEvent[]) {
52
+ const recoveries = recoveredToolCalls(events);
49
53
  const calls = events.filter((event) => event.event === "tool_call" && ["bash", "shell", "exec"].includes(String(event.toolName)));
50
54
  const results = new Map(events.filter((event) => event.event === "tool_result").map((event) => [event.toolCallId, event]));
51
55
  const decisions = new Map(events.filter((event) => event.event === "tool_decision").map((event) => [event.toolCallId, event]));
52
56
  const entries = calls.map((call) => {
53
57
  const decision = decisions.get(call.toolCallId), result = results.get(call.toolCallId);
54
58
  const blocked = decision?.decision === "blocked";
55
- const failed = !blocked && Boolean(result && failedToolResult(result));
59
+ const recovered = Boolean(call.toolCallId && recoveries.has(call.toolCallId));
60
+ const failed = !blocked && !recovered && !handledToolFailure(result?.reasonCode) && Boolean(result && failedToolResult(result));
56
61
  return { toolCallId: call.toolCallId ?? "unknown", command: call.command ?? "[redacted or unavailable]",
57
62
  status: blocked ? "blocked" : !result ? "running" : failed ? "failed" : "passed",
58
63
  exitCode: result?.exitCode ?? null, exitCodeExact: result?.exitCodeExact === true,
@@ -121,7 +126,7 @@ export async function buildActivityInspector(input: {
121
126
  phase: snapshot.session.operation.hostPhase.state === "known" && snapshot.session.operation.hostPhase.value !== "idle" ? snapshot.session.operation.hostPhase.value : null,
122
127
  outcome: snapshot.task?.outcome ?? "idle", running: snapshot.activity.running.length, current: input.current ?? [] },
123
128
  criteria: snapshot.task?.criteria ?? [], sourceChanges: snapshot.sourceChanges, files,
124
- tools: { calls: calls.length, results: results.length, failed: results.filter(failedToolResult).length,
129
+ tools: { calls: calls.length, results: results.length, failed: results.filter((event) => failedToolResult(event) && !handledToolFailure(event.reasonCode)).length,
125
130
  blocked: events.filter((event) => event.event === "tool_decision" && event.decision === "blocked").length, byName,
126
131
  perToolTokens: null, perToolTokensReason: "Pi reports model usage by response/turn; built-in tool calls do not carry attributable model-token totals." },
127
132
  commands, safety,
@@ -1,5 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
 
3
+ import { durableContextEvidenceEntries } from "../../extensions/context-evidence.js";
3
4
  import type { TaskContract } from "../../extensions/guard-types.ts";
4
5
  import { readSolverShadowEvents } from "../solver/solver-shadow.ts";
5
6
  import { readTrajectoryStore } from "../trajectory/trajectory-store.ts";
@@ -52,8 +53,9 @@ export function buildTaskEfficiencyMetrics(
52
53
  const decision = solverEvent?.decision;
53
54
  const helperUsage = task.acceptanceReceipt?.helperUsage;
54
55
  const provenance = task.acceptanceReceipt?.provenance;
56
+ const contextEvidence = durableContextEvidenceEntries(task);
55
57
  const relevantEvent = trajectory.events.find((event) => event.cause === "context-observed");
56
- const relevantFileMs = relevantEvent && task.contextManifest.length > 0 ? duration(task.createdAt, relevantEvent.observedAt) : null;
58
+ const relevantFileMs = relevantEvent && contextEvidence.length > 0 ? duration(task.createdAt, relevantEvent.observedAt) : null;
57
59
  const recommendedTools = decision?.toolGroups ?? [];
58
60
  const activeTools = [...new Set(input.activeToolGroups ?? [])].sort();
59
61
  const exactUsage = input.exactUsage && (input.exactUsage.tokens !== null || input.exactUsage.cost !== null)
@@ -83,7 +85,7 @@ export function buildTaskEfficiencyMetrics(
83
85
  },
84
86
  context: {
85
87
  requiredFiles: task.requiredContext.length,
86
- manifestedFiles: task.contextManifest.length,
88
+ manifestedFiles: contextEvidence.length,
87
89
  memoryCitations: task.memoryCitations.length,
88
90
  recommendedBudget: decision?.context.budgetBand ?? "unknown",
89
91
  timeToRelevantFileMs: relevantFileMs,
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
+ import { durableContextEvidenceEntries } from "../../extensions/context-evidence.js";
4
5
  import type { TaskContract } from "../../extensions/guard-types.js";
5
6
 
6
7
  const MAX_EVIDENCE_BYTES = 256 * 1024;
@@ -71,7 +72,7 @@ export function evaluateExactFinalOutputContract(
71
72
 
72
73
  const evidencePaths: string[] = [];
73
74
  const observedValues = new Set<string>();
74
- for (const entry of task.contextManifest ?? []) {
75
+ for (const entry of durableContextEvidenceEntries(task)) {
75
76
  const observed = safeObservedFile(cwd, entry.path);
76
77
  if (!observed) continue;
77
78
  const values = valuesForKey(directive.key, observed.text);
@@ -0,0 +1,23 @@
1
+ const GRAPH_ORDER_PATTERNS = [
2
+ /\b(?:(?:dependenc(?:y|ies)|prerequisites?)[-\s]+(?:dag|graph|order(?:ing)?|relation(?:ship)?s?|direction|edges?|chains?)|(?:topological(?:ly)?\s+(?:sort(?:ed|ing)?|order(?:ed|ing)?)|dag(?:\s+(?:order(?:ing)?|edges?|schedule))?)|first[-\s]+(?:observed|seen|encountered)[-\s]+order|replay order|deduplicat(?:e|ion)[^.!?\n]{0,48}\border|before (?:its|their|the) dependents?)\b/i,
3
+ /\b(?:preserve|maintain|follow|respect|use)\b[^.!?\n]{0,24}\b(?:stable (?:input )?order|input order|stable order)\b[^.!?\n]{0,48}\b(?:schedul(?:e|ed|ing)|jobs?|tasks?|nodes?|steps?|dependents?|dependenc(?:y|ies)|prerequisites?|ranks?|ranked|priorit(?:y|ies)|ties?|tie[-\s]?break)\b|\b(?:schedul(?:e|ed|ing)|emit(?:s|ted|ting)?|process(?:es|ed|ing)?|ranks?|ranked|priorit(?:y|ies)|ties?|tie[-\s]?break)\b[^.!?\n]{0,48}\b(?:jobs?|tasks?|nodes?|steps?|dependents?)?[^.!?\n]{0,32}\b(?:stable (?:input )?order|input order|stable order)\b/i,
4
+ /\b(?:dependenc(?:y|ies)|prerequisites?|dag|graph|scheduler|jobs?|tasks?|nodes?)\b[^.!?\n]{0,64}\b(?:acyclic|cyclic|cycle detection|(?:detect|reject|prevent|avoid|report|forbid|form|contain|create)(?:s|ed|ing)?\s+cycles?)\b|\b(?:acyclic|cyclic|cycle detection|(?:detect|reject|prevent|avoid|report|forbid|form|contain|create)(?:s|ed|ing)?\s+cycles?|cycles?\s+(?:in|among|through))\b[^.!?\n]{0,64}\b(?:dependenc(?:y|ies)|prerequisites?|dag|graph|scheduler|jobs?|tasks?|nodes?)\b/i,
5
+ /\b(?:run|execute|start|schedule|process|emit|appear)\b[^.!?\n]{0,48}\bafter\b[^.!?\n]{0,48}\b(?:(?:all|each|every|its|their|the)\s+)?(?:dependenc(?:y|ies)|prerequisites?)\s+(?:(?:must|should|shall|has|have|had)\s+)?(?:complete[sd]?|finish(?:ed|es)?|resolve[sd]?|ran|executed)\b/i,
6
+ /\b(?:run|execute|start|schedule|process|emit|begin|appear)\b[^.!?\n]{0,48}\b(?:only\s+)?(?:once|when)\b[^.!?\n]{0,48}\b(?:all|each|every|its|their|the)?\s*(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,32}\b(?:complete[sd]?|finish(?:ed|es)?|resolve[sd]?|ran|executed)\b/i,
7
+ /\b(?:run|execute|start|schedule|process|emit|begin|appear)\b[^.!?\n]{0,48}\buntil\b[^.!?\n]{0,48}\b(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,32}\b(?:complete[sd]?|finish(?:ed|es)?|resolve[sd]?|ran|executed)\b/i,
8
+ /\b(?:jobs?|tasks?|nodes?|steps?|dependents?)\b[^.!?\n]{0,48}\bwait(?:s|ed|ing)?\s+for\b[^.!?\n]{0,32}\b(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,32}\b(?:complete|finish|resolve)\b/i,
9
+ /\bwait\s+for\b[^.!?\n]{0,32}\b(?:(?:all|each|every|its|their|the)\s+)?(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,32}\bbefore\b[^.!?\n]{0,32}\b(?:run|running|execute|executing|start|starting|schedule|scheduling|process|processing|emit|emitting)\b[^.!?\n]{0,32}\b(?:jobs?|tasks?|nodes?|steps?|dependents?)\b/i,
10
+ /\b(?:(?:each|every|all|the)\s+)?(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,48}\b(?:complete|finish|resolve)(?:s|d|ed)?\b[^.!?\n]{0,24}\b(?:before|prior\s+to)\b[^.!?\n]{0,24}\b(?:(?:run|running|execute|executing|start|starting|schedule|scheduling|process|processing|emit|emitting)\s+)?(?:the\s+)?(?:(?:dependent\s+)?(?:job|task|node|step)s?|dependents?)\b/i,
11
+ /\b(?:jobs?|tasks?|nodes?|steps?|dependents?)\b[^.!?\n]{0,32}\b(?:eligible|ready)\b[^.!?\n]{0,24}\b(?:when|once)\b[^.!?\n]{0,32}\b(?:each|every|all|the|its|their)?\s*(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,24}\b(?:is|are|be|become(?:s)?|remain(?:s)?)?\s*(?:done|complete[sd]?|finish(?:ed|es)?|resolve[sd]?)\b/i,
12
+ /\b(?:run|execute|process|resolve|complete|schedule)\s+(?:each|every|all|the)\s+(?:dependenc(?:y|ies)|prerequisites?)\s+first\b/i,
13
+ /\b(?:(?:each|every|all|the)\s+)?(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,48}\b(?:precede(?:s)?|come(?:s)?\s+before|run(?:s)?\s+before|execute(?:s)?\s+before|finish(?:es)?\s+before|complete(?:s|d)?\s+before)\b[^.!?\n]{0,32}\b(?:the\s+)?(?:(?:dependent\s+)?(?:job|task|node|step)s?|dependents?)\b/i,
14
+ /\bdepends?\s+on\b[^.!?\n]{0,64}\b(?:run|runs|ran|execute|executes|executed|start|starts|started|schedule|schedules|scheduled|process|processes|processed|emit|emits|emitted)\b[^.!?\n]{0,24}\b(?:after|afterward|afterwards|later)\b/i,
15
+ /\bdepends?\s+on\b[^.!?\n]{0,64}\b(?:run|execute|start|schedule|process|emit)(?:s|d|ed|ing)?\b[^.!?\n]{0,24}\b[^.!?\n]{0,24}\bfirst\b/i,
16
+ /\b(?:run|execute|start|schedule|process|emit)(?:s|d|ed|ing)?\b[^.!?\n]{0,32}\b(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,24}\bfollowed\s+by\b[^.!?\n]{0,24}\b(?:(?:the\s+)?dependent|(?:the\s+)?(?:job|task|node|step)s?)\b/i,
17
+ /\b(?:run|execute|start|schedule|process|emit)(?:s|d|ed|ing)?\b[^.!?\n]{0,48}\bbefore\b[^.!?\n]{0,48}\bdepends?\s+on\b/i,
18
+ /\b(?:do\s+not|don't|never)\s+(?:run|execute|start|schedule|process|emit|begin)\b[^.!?\n]{0,48}\b(?:jobs?|tasks?|nodes?|steps?|dependents?)\b[^.!?\n]{0,48}\bbefore\b[^.!?\n]{0,32}\b(?:dependenc(?:y|ies)|prerequisites?)\b/i
19
+ ];
20
+
21
+ export function hasGraphOrderContract(request: string): boolean {
22
+ return GRAPH_ORDER_PATTERNS.some((pattern) => pattern.test(request));
23
+ }