@bingjiang0611/rotom 0.1.24 → 0.1.26

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 (129) hide show
  1. package/THIRD_PARTY_NOTICES.md +2 -2
  2. package/extensions/observability/index.ts +20 -2
  3. package/extensions/qoder/messages.mjs +1 -1
  4. package/extensions/qoder/provider.mjs +14 -6
  5. package/extensions/qoder/transport.mjs +53 -11
  6. package/extensions/third-party/node_modules/.package-lock.json +6 -6
  7. package/extensions/third-party/node_modules/pi-subagents/README.md +17 -0
  8. package/extensions/third-party/node_modules/pi-subagents/agents/delegate.md +2 -0
  9. package/extensions/third-party/node_modules/pi-subagents/agents/oracle.md +18 -14
  10. package/extensions/third-party/node_modules/pi-subagents/agents/reviewer.md +5 -3
  11. package/extensions/third-party/node_modules/pi-subagents/docs/models.md +1 -1
  12. package/extensions/third-party/node_modules/pi-subagents/docs/tool-reference.md +2 -2
  13. package/extensions/third-party/node_modules/pi-subagents/docs/workflows.md +2 -2
  14. package/extensions/third-party/node_modules/pi-subagents/package.json +1 -1
  15. package/extensions/third-party/node_modules/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  16. package/extensions/third-party/node_modules/pi-subagents/prompts/parallel-research.md +4 -6
  17. package/extensions/third-party/node_modules/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +8 -8
  18. package/extensions/third-party/node_modules/pi-subagents/skills/pi-subagents/references/execution-controls.md +5 -5
  19. package/extensions/third-party/node_modules/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +7 -8
  20. package/extensions/third-party/node_modules/pi-subagents/src/agents/agents.ts +0 -1
  21. package/extensions/third-party/node_modules/pi-subagents/src/extension/index.ts +9 -2
  22. package/extensions/third-party/node_modules/pi-subagents/src/profiles/profiles.ts +1 -2
  23. package/extensions/third-party/node_modules/pi-subagents/src/runs/background/async-execution.ts +81 -52
  24. package/extensions/third-party/node_modules/pi-subagents/src/runs/background/process-terminal.ts +13 -0
  25. package/extensions/third-party/node_modules/pi-subagents/src/runs/background/stale-run-reconciler.ts +7 -3
  26. package/extensions/third-party/node_modules/sol-pi/UPSTREAM.md +8 -3
  27. package/extensions/third-party/node_modules/sol-pi/package.json +1 -1
  28. package/extensions/third-party/node_modules/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/index.ts +6 -2
  29. package/extensions/third-party/node_modules/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/journal.ts +21 -0
  30. package/extensions/third-party/node_modules/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/provider.ts +7 -1
  31. package/extensions/third-party/node_modules/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/receipt.ts +79 -16
  32. package/extensions/third-party/package-lock.json +8 -8
  33. package/extensions/third-party/package.json +2 -2
  34. package/extensions/third-party/vendor/README.md +12 -11
  35. package/extensions/third-party/vendor/pi-subagents-0.52.1-rotom.10.tgz +0 -0
  36. package/extensions/third-party/vendor/sol-pi-0.1.0-rotom.2.tgz +0 -0
  37. package/package.json +10 -10
  38. package/runtime/pi/fork-build.json +24 -24
  39. package/runtime/pi/node_modules/.package-lock.json +29 -29
  40. package/runtime/pi/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  41. package/runtime/pi/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +30 -7
  42. package/runtime/pi/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js.map +1 -1
  43. package/runtime/pi/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  44. package/runtime/pi/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
  45. package/runtime/pi/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +2 -1
  46. package/runtime/pi/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
  47. package/runtime/pi/node_modules/@earendil-works/pi-ai/dist/utils/error-body.d.ts +8 -0
  48. package/runtime/pi/node_modules/@earendil-works/pi-ai/dist/utils/error-body.d.ts.map +1 -1
  49. package/runtime/pi/node_modules/@earendil-works/pi-ai/dist/utils/error-body.js +37 -1
  50. package/runtime/pi/node_modules/@earendil-works/pi-ai/dist/utils/error-body.js.map +1 -1
  51. package/runtime/pi/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts +6 -0
  52. package/runtime/pi/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts.map +1 -1
  53. package/runtime/pi/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +34 -0
  54. package/runtime/pi/node_modules/@earendil-works/pi-ai/dist/utils/retry.js.map +1 -1
  55. package/runtime/pi/node_modules/@earendil-works/pi-ai/package.json +3 -3
  56. package/runtime/pi/node_modules/@earendil-works/pi-codemode/package.json +2 -2
  57. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/anthropic-messages-H6BRNJYC.js +6 -0
  58. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{azure-openai-responses-KZZFKCXW.js → azure-openai-responses-45AJGMB6.js} +1 -1
  59. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
  60. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{chunk-2CKNKV2I.js → chunk-5MTGRVNE.js} +2 -2
  61. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{chunk-ITZNWD4K.js → chunk-7OCND3IM.js} +1 -1
  62. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/chunk-CTEAMRDY.js +2 -0
  63. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{chunk-3CD5ZAQX.js → chunk-ESL4WJJF.js} +11 -11
  64. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{chunk-XQ3UQBJ2.js → chunk-MXCOAUPD.js} +1 -1
  65. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{cloudflare-workers-ai-system-one-UTBL2KOF.js → cloudflare-workers-ai-system-one-IEPXKTM7.js} +1 -1
  66. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{execute-4WILIDZF.js → execute-ULZNGRYF.js} +1 -1
  67. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{google-generative-ai-KJOKBJM3.js → google-generative-ai-KXRSXWRQ.js} +1 -1
  68. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{google-vertex-VCLZ36OB.js → google-vertex-CVLJXJIO.js} +1 -1
  69. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{llama-cpp-classify-FYN623RH.js → llama-cpp-classify-42ZH3VVO.js} +1 -1
  70. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{openai-codex-responses-ZYGHVE5B.js → openai-codex-responses-L4P7LV62.js} +1 -1
  71. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{openai-completions-BGCEXFAB.js → openai-completions-ZIOWDPDJ.js} +1 -1
  72. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{openai-decisions-WVPMQ4JD.js → openai-decisions-L7N23NC7.js} +1 -1
  73. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{openai-responses-KCCCXI3B.js → openai-responses-XT6CQ4R7.js} +1 -1
  74. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{openrouter-images-DUPG5DZO.js → openrouter-images-FQ7HEE4T.js} +1 -1
  75. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{typesafe-system-one-ZSK3RN4B.js → typesafe-system-one-IH2QHGKL.js} +1 -1
  76. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/{virtual-modules-C5BH2VI2.js → virtual-modules-YSF6BJRF.js} +1 -1
  77. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/cli-runtime.js +1 -1
  78. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/index.js +1 -1
  79. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/rpc-entry.js +1 -1
  80. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.d.ts +5 -0
  81. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  82. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +60 -27
  83. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  84. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  85. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  86. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  87. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  88. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/runner.js +1 -0
  89. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  90. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/types.d.ts +8 -0
  91. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  92. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  93. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/status-indicator.d.ts +1 -0
  94. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/status-indicator.d.ts.map +1 -1
  95. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/status-indicator.js +4 -1
  96. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/status-indicator.js.map +1 -1
  97. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  98. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.js +37 -4
  99. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  100. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/docs/extensions.md +1 -0
  101. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/docs/json.md +6 -0
  102. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/docs/rpc-extension-ui.md +2 -0
  103. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/docs/settings.md +3 -1
  104. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/package.json +7 -7
  105. package/runtime/pi/node_modules/@earendil-works/pi-mcp/package.json +2 -2
  106. package/runtime/pi/node_modules/@earendil-works/pi-telemetry/package.json +2 -2
  107. package/runtime/pi/node_modules/@earendil-works/pi-tui/package.json +2 -2
  108. package/runtime/pi/package-lock.json +37 -37
  109. package/runtime/pi/package.json +15 -15
  110. package/runtime/pi/vendor/earendil-works-pi-agent-core-1.3.0-rotom.2.tgz +0 -0
  111. package/runtime/pi/vendor/earendil-works-pi-ai-1.3.0-rotom.2.tgz +0 -0
  112. package/runtime/pi/vendor/earendil-works-pi-codemode-1.3.0-rotom.2.tgz +0 -0
  113. package/runtime/pi/vendor/{earendil-works-pi-coding-agent-1.3.0-rotom.1.tgz → earendil-works-pi-coding-agent-1.3.0-rotom.2.tgz} +0 -0
  114. package/runtime/pi/vendor/earendil-works-pi-mcp-1.3.0-rotom.2.tgz +0 -0
  115. package/runtime/pi/vendor/earendil-works-pi-telemetry-1.3.0-rotom.2.tgz +0 -0
  116. package/runtime/pi/vendor/earendil-works-pi-tui-1.3.0-rotom.2.tgz +0 -0
  117. package/runtime/product-config.mjs +5 -5
  118. package/extensions/third-party/node_modules/pi-subagents/agents/researcher.md +0 -52
  119. package/extensions/third-party/node_modules/pi-subagents/docs/agents.md +0 -383
  120. package/extensions/third-party/vendor/pi-subagents-0.52.1-rotom.7.tgz +0 -0
  121. package/extensions/third-party/vendor/sol-pi-0.1.0-rotom.0.tgz +0 -0
  122. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/anthropic-messages-UIH6DSZT.js +0 -6
  123. package/runtime/pi/node_modules/@earendil-works/pi-coding-agent/dist/bundle/chunks/chunk-TEPMHNKQ.js +0 -2
  124. package/runtime/pi/vendor/earendil-works-pi-agent-core-1.3.0-rotom.1.tgz +0 -0
  125. package/runtime/pi/vendor/earendil-works-pi-ai-1.3.0-rotom.1.tgz +0 -0
  126. package/runtime/pi/vendor/earendil-works-pi-codemode-1.3.0-rotom.1.tgz +0 -0
  127. package/runtime/pi/vendor/earendil-works-pi-mcp-1.3.0-rotom.1.tgz +0 -0
  128. package/runtime/pi/vendor/earendil-works-pi-telemetry-1.3.0-rotom.1.tgz +0 -0
  129. package/runtime/pi/vendor/earendil-works-pi-tui-1.3.0-rotom.1.tgz +0 -0
@@ -48,7 +48,7 @@ subagent({
48
48
 
49
49
  ```typescript
50
50
  subagent({
51
- workflowScript: `return runs.run("oracle-check", { agent: "oracle", task: "Review my current direction and challenge assumptions.", context: "fork" })`
51
+ workflowScript: `return runs.run("continue-thread", { agent: "delegate", task: "Continue the investigation from the current conversation state.", context: "fork" })`
52
52
  })
53
53
  ```
54
54
 
@@ -86,7 +86,7 @@ Completed workflow children from this parent session stay addressable as retaine
86
86
 
87
87
  ### Async/background
88
88
 
89
- Prefer async mode for every subagent launch. Set `async: true` no matter the task unless the parent must block until completion. This applies to scouts, researchers, workers, reviewers, validators, oracle checks, one-off delegates, final review gates, backlog gates, and scripted workflows. Keep the write path single-threaded even when the run is async.
89
+ Prefer async mode for every subagent launch. Set `async: true` no matter the task unless the parent must block until completion. This applies to scouts, workers, reviewers, validators, oracle checks, one-off delegates, final review gates, backlog gates, and scripted workflows. Keep the write path single-threaded even when the run is async.
90
90
 
91
91
  Use `async:false` only when the parent must block until completion. Async mode still shows progress. Do not use `async:false` because a task is short, because it is the last gate, because no other work is ready, because the user asked to finish the overall job, or because blocking is convenient.
92
92
 
@@ -358,7 +358,7 @@ worktree, first confirm dependencies were linked, installed, or provisioned by
358
358
 
359
359
  ### Oracle consultation loop
360
360
 
361
- For plan, design, or architecture advice that asks to ask, consult, discuss with, or come to agreement with `oracle`, start with one forked oracle run. Read its result. If it challenges the direction or leaves a material tradeoff, resume that same completed child once with a focused follow-up, then synthesize the parent decision. `resume` returns a new run id, but continues the same oracle session and inherited context. Do not force a second round for an explicit one-shot request, a trivial question, or a fully settled first answer.
361
+ For plan, design, or architecture advice that asks to ask, consult, discuss with, or come to agreement with `oracle`, start with one fresh oracle run whose task is a decision brief: goal and acceptance criteria, decisions already made and why, hard constraints, the proposed move, the files that matter, and the specific question. Read its result. If it challenges the direction or leaves a material tradeoff, resume that same completed child once with a focused follow-up, then synthesize the parent decision. `resume` returns a new run id, but continues the same oracle session. Do not force a second round for an explicit one-shot request, a trivial question, or a fully settled first answer.
362
362
 
363
363
  ```typescript
364
364
  const first = await runs.run("oracle-consult", { agent: "oracle", task: "Review this plan and identify the strongest unresolved tradeoff." });
@@ -368,14 +368,14 @@ const final = await runs.run("oracle-consult-follow-up", { resume: first.runId,
368
368
  The parent remains the final decision-maker. Oracle advice does not approve a direction or start implementation.
369
369
 
370
370
  The intended oracle loop is:
371
- 1. the main agent forks to `oracle`
371
+ 1. the main agent writes a decision brief and launches `oracle` with fresh context
372
372
  2. `oracle` reviews direction, drift, assumptions, and risks
373
373
  3. `oracle` can coordinate back through `contact_supervisor` when the bridge injects it
374
374
  4. the main agent decides what direction to approve
375
375
  5. only then should `worker` implement
376
376
 
377
377
  ```typescript
378
- // Advisory review in a branched thread. Oracle defaults to forked context.
378
+ // Advisory review from a parent-written decision brief. Oracle defaults to fresh context.
379
379
  subagent({
380
380
  workflowScript: `return runs.run("oracle-check", { agent: "oracle", task: "Review my current direction, challenge assumptions, and propose the best next move." })`
381
381
  })
@@ -9,7 +9,7 @@ Parent extensions may register a session-scoped, out-of-band ceiling through `pi
9
9
  ## When to Use
10
10
 
11
11
  - **Complex work orchestration**: use Fable mode as the default parent-agent loop for complex work. Complex means the task has multiple moving parts, unclear acceptance, cross-cutting code, meaningful user-visible impact, expensive or irreversible validation, broad review surface, or the user asks for orchestration. Lightweight one-off delegation can stay lightweight.
12
- - **Advisory review**: use fresh-context `reviewer` agents for adversarial code review, or fork to `oracle` when inherited decisions and drift matter
12
+ - **Advisory review**: use fresh-context `reviewer` agents for adversarial code review, or give `oracle` a written decision brief when prior decisions and drift matter
13
13
  - **Implementation handoff**: have `oracle` advise, then `worker` implement only after an approved direction
14
14
  - **Recon and planning**: use `scout`, then write a plan when needed
15
15
  - **Parallel exploration**: run multiple non-conflicting tasks concurrently
@@ -41,7 +41,7 @@ you are guiding a human through an interactive flow.
41
41
  Packaged prompt shortcuts are also available for repeatable workflows. Treat them as reusable orchestration recipes, not just human slash commands. When the user asks for one of these shapes, or when the workflow clearly fits, apply the same pattern directly with `subagent(...)` and other tools:
42
42
  - `/parallel-review` — fresh-context reviewers with distinct review angles, then synthesis
43
43
  - `/review-loop` — parent-orchestrated worker, fresh-reviewer, and fix-worker cycles until clean or capped
44
- - `/parallel-research` — combine `researcher` and `scout` for external evidence plus local code context
44
+ - `/parallel-research` — combine parallel `scout` lanes for local code context with external evidence the parent gathers
45
45
  - `/gather-context-and-clarify` — scout/research first, then ask the user clarifying questions with `interview`
46
46
  - `/parallel-cleanup` — two fresh-context reviewers (deslop + verbosity passes) for an adversarial cleanup review of the current diff
47
47
 
@@ -87,13 +87,13 @@ As a conservative orchestration policy, do not pass `turnBudget` or a hard `tool
87
87
 
88
88
  ### Parallel research technique
89
89
 
90
- Use this when the question needs both external evidence and local implications. Combine `researcher` for official docs, specs, ecosystem behavior, recent changes, benchmarks, and primary sources with `scout` for repository files, patterns, constraints, tests, and likely integration points. Give each child a distinct angle: external evidence, local code context, and practical tradeoffs. Ask for source links or file ranges, confidence level, gaps, and decision implications. Do not ask these children to edit unless implementation was explicitly requested.
90
+ Use this when the question needs both external evidence and local implications. The parent gathers official docs, specs, ecosystem behavior, recent changes, benchmarks, and primary sources itself (there is no builtin web-research agent) and uses `scout` for repository files, patterns, constraints, tests, and likely integration points. Give each child a distinct angle: external evidence, local code context, and practical tradeoffs. Ask for source links or file ranges, confidence level, gaps, and decision implications. Do not ask these children to edit unless implementation was explicitly requested.
91
91
 
92
92
 
93
93
 
94
94
  ### Gather-context-and-clarify technique
95
95
 
96
- Use this at the start of non-trivial work. Launch `scout` for local context and `researcher` only when external docs, recent sources, ecosystem context, or primary evidence would materially improve understanding. Ask children for concise findings plus remaining clarification questions. Then synthesize what is known and use `interview` to ask the unresolved questions needed for shared understanding before planning or implementing.
96
+ Use this at the start of non-trivial work. Launch `scout` for local context, and gather external docs, recent sources, ecosystem context, or primary evidence in the parent only when they would materially improve understanding. Ask children for concise findings plus remaining clarification questions. Then synthesize what is known and use `interview` to ask the unresolved questions needed for shared understanding before planning or implementing.
97
97
 
98
98
  ### Parallel cleanup technique
99
99
 
@@ -157,10 +157,9 @@ and user/project agents override builtins with the same name.
157
157
  |-------|---------|-------|------------------------|
158
158
  | `scout` | Fast codebase recon | inherits default | Writes `context.md` handoff material |
159
159
  | `worker` | Implementation and approved oracle handoffs | inherits default | Single-writer implementation with decision escalation |
160
- | `reviewer` | Review specialist | inherits default | Default recipes are review-only; tools include edit/write when a fix pass is explicit |
161
- | `researcher` | Web research brief generator | inherits default | Writes `research.md` |
160
+ | `reviewer` | Review specialist | inherits default | Review-only; `bash` limited by prompt to read-only inspection and existing checks |
162
161
  | `delegate` | Lightweight generic delegate | inherits default | No fixed output; generic delegated work |
163
- | `oracle` | Decision-consistency advisory review | inherits default | Advisory review, intercom coordination |
162
+ | `oracle` | Decision-consistency advisory review | inherits default | Read-only; fresh context from a parent-written decision brief |
164
163
  | `advisor` | Claude Code-compatible alias for `oracle` | inherits default | Same advisory role as `oracle` |
165
164
 
166
165
  Builtin `worker` and `delegate` use strict tool allowlists and do not inherit ambient parent extension tools. To give a child an extension tool, name it in `tools` and load its provider via `extensions`, a path-like `tools` entry, or `subagentOnlyExtensions`. Custom agents without an `extensions` field follow `subagents.defaultExtensions` when set.
@@ -206,7 +205,7 @@ A strong subagent prompt usually includes:
206
205
 
207
206
  Give each role useful discovery anchors. Name source roots, filenames, symbols, types, methods, and paths for scouts. Give workers context files, plans, task paths, and named source seams before asking them to search. Give reviewers changed files, contracts, and any exhaustive-verification target. Tell oracle whether current source behavior, product/policy documents, plans, or inherited decisions are the evidence that matters.
208
207
 
209
- Avoid carrying over old prompt habits that over-specify every step. Use `must`, `always`, and `never` for real invariants; for judgment calls, give decision rules. For example, tell a reviewer to inspect the staged diff directly and report only evidence-backed findings, rather than prescribing every file or command. Tell a researcher the retrieval budget: start with broad targeted searches, fetch only the strongest sources, search again only when a required fact is missing, then stop.
208
+ Avoid carrying over old prompt habits that over-specify every step. Use `must`, `always`, and `never` for real invariants; for judgment calls, give decision rules. For example, tell a reviewer to inspect the staged diff directly and report only evidence-backed findings, rather than prescribing every file or command.
210
209
 
211
210
  For implementation handoffs, name the approved scope and success criteria more clearly than the process. Good prompts say what to change, what not to change, where the evidence lives, how to validate, and when to escalate. They should not ask the child to create another subagent plan or continue the parent conversation.
212
211
 
@@ -39,7 +39,6 @@ export const BUILTIN_AGENT_NAMES = [
39
39
  "advisor",
40
40
  "delegate",
41
41
  "oracle",
42
- "researcher",
43
42
  "reviewer",
44
43
  "scout",
45
44
  "worker",
@@ -47,6 +47,7 @@ import { resolveWaitToolConfig } from "../runs/background/subagent-wait.ts";
47
47
  import { registerWaitTool } from "../runs/background/wait-tool.ts";
48
48
  import { createWaitSubscriptionManager } from "../runs/background/wait-subscriptions.ts";
49
49
  import { drainOutstandingWork } from "../runs/background/auto-drain.ts";
50
+ import { asyncRunnerDrain } from "../runs/background/async-execution.ts";
50
51
  import registerSubagentNotify, { parseSubagentNotifyContent, type SubagentNotifyDetails } from "../runs/background/notify.ts";
51
52
  import { formatSteeringNotice, handleSubagentSteeringNotice, SUBAGENT_STEERING_MESSAGE_TYPE, type SubagentSteeringMessageDetails } from "./steering-notices.ts";
52
53
  import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/pi-args.ts";
@@ -950,7 +951,13 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
950
951
  });
951
952
 
952
953
  pi.on("session_shutdown", async () => {
953
- runtimeEntry.cleanup();
954
- await herdrStatusBridge.flush();
954
+ try {
955
+ // Quit, reload and replacement retire this runtime's authority. Persist
956
+ // child close evidence before disposing the original observers.
957
+ await asyncRunnerDrain(pi).drain();
958
+ } finally {
959
+ runtimeEntry.cleanup();
960
+ await herdrStatusBridge.flush();
961
+ }
955
962
  });
956
963
  }
@@ -261,7 +261,7 @@ function qualityTierToRoleTier(quality: QualityTier, cost: CostTier): Recommende
261
261
 
262
262
  function agentsForRoleTier(roleTier: RecommendedRoleTier): BuiltinAgentName[] {
263
263
  if (roleTier === "cheap") return ["scout", "delegate"];
264
- if (roleTier === "medium") return ["researcher", "reviewer"];
264
+ if (roleTier === "medium") return ["reviewer"];
265
265
  return ["worker", "reviewer", "oracle"];
266
266
  }
267
267
 
@@ -407,7 +407,6 @@ function buildProfileFile(kind: ProfileKind, models: { cheap: string; medium: st
407
407
  agentOverrides: {
408
408
  scout: { model: models.cheap },
409
409
  delegate: { model: models.cheap },
410
- researcher: { model: models.medium },
411
410
  worker: { model: models.strong },
412
411
  reviewer: { model: models.strong },
413
412
  oracle: { model: models.strong },
@@ -67,7 +67,9 @@ import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
67
67
  import { usageBudgetState } from "../shared/usage-budget.ts";
68
68
  import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
69
69
  import type { SessionLeaseRequest } from "../shared/session-lease.ts";
70
- import { finalizeProcessTerminal, readProcessTerminal } from "./process-terminal.ts";
70
+ import { finalizeProcessTerminal, readProcessTerminal, registerRunnerCloseObserver } from "./process-terminal.ts";
71
+ import { createRunnerDrain } from "./runner-drain.ts";
72
+
71
73
  import type { ActiveAsyncCapacityHandle } from "./active-async-capacity.ts";
72
74
  import { assertExecutionStoreScope, scopedSessionRoot, assertScopedSessionPath } from "../../shared/execution-store.ts";
73
75
  import { OWNED_EXECUTION_SCOPE, type OwnedExecutionRequest, type OwnedWorkflowParent } from "./owned-execution.ts";
@@ -76,6 +78,17 @@ import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling,
76
78
  import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
77
79
  import { resolvePermissionRules, type PermissionConfig } from "../shared/permissions.ts";
78
80
 
81
+ // Each runtime owns live ChildProcess handles, never historical PIDs.
82
+ const runtimeRunnerDrains = new WeakMap<object, ReturnType<typeof createRunnerDrain>>();
83
+ export function asyncRunnerDrain(owner: object): ReturnType<typeof createRunnerDrain> {
84
+ let drain = runtimeRunnerDrains.get(owner);
85
+ if (!drain) {
86
+ drain = createRunnerDrain();
87
+ runtimeRunnerDrains.set(owner, drain);
88
+ }
89
+ return drain;
90
+ }
91
+
79
92
  const require = createRequire(import.meta.url);
80
93
  const piPackageRoot = resolvePiPackageRoot();
81
94
 
@@ -487,7 +500,8 @@ interface SpawnRunnerResult {
487
500
  startupDidNotProceed?: boolean;
488
501
  }
489
502
 
490
- function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal?: (proof: unknown) => void): SpawnRunnerResult {
503
+ function spawnRunner(cfg: object, suffix: string, cwd: string, drain: ReturnType<typeof createRunnerDrain>, onProcessTerminal?: (proof: unknown) => void): SpawnRunnerResult {
504
+ drain.assertOpen();
491
505
  if (!jitiCliPath) {
492
506
  return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
493
507
  }
@@ -546,62 +560,75 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
546
560
  proc.on("error", (error) => {
547
561
  console.error(`[pi-subagents] async spawn failed: ${error.message}`);
548
562
  });
563
+ const releaseCloseObserver = typeof launchAsyncDir === "string" && typeof launchRunId === "string"
564
+ ? registerRunnerCloseObserver(launchAsyncDir, launchRunId, runnerProcessInstanceId) : undefined;
565
+ const observedClose = drain.track(() => {
566
+ // Awaiting a Promise alone cannot keep Node alive after RPC stdin closes.
567
+ proc.ref();
568
+ if (proc.exitCode === null && proc.signalCode === null) proc.kill("SIGTERM");
569
+ });
549
570
  proc.once("close", (exitCode, signal) => {
550
- const launch = launchConfig as { asyncDir?: unknown; id?: unknown; nestedRoute?: NestedRouteInfo; nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> } };
551
- const asyncDir = launch.asyncDir;
552
- const runId = launch.id;
553
- if (typeof asyncDir !== "string" || typeof runId !== "string") return;
554
- finalizeProcessTerminal(asyncDir, runId, {
555
- processInstanceId: runnerProcessInstanceId,
556
- closeObservedAt: Date.now(),
557
- exitCode,
558
- signal,
559
- });
560
- const persisted = readProcessTerminal(asyncDir, { runId, runnerProcessInstanceId });
561
- if (!persisted) return;
562
- if (launch.nestedRoute && launch.nestedSelf) {
563
- try {
564
- let status: import("../../shared/types.ts").AsyncStatus;
571
+ try {
572
+ const launch = launchConfig as { asyncDir?: unknown; id?: unknown; nestedRoute?: NestedRouteInfo; nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> } };
573
+ const asyncDir = launch.asyncDir;
574
+ const runId = launch.id;
575
+ if (typeof asyncDir !== "string" || typeof runId !== "string") return;
576
+ finalizeProcessTerminal(asyncDir, runId, {
577
+ processInstanceId: runnerProcessInstanceId,
578
+ closeObservedAt: Date.now(),
579
+ exitCode,
580
+ signal,
581
+ });
582
+ const persisted = readProcessTerminal(asyncDir, { runId, runnerProcessInstanceId });
583
+ if (!persisted) return;
584
+ if (launch.nestedRoute && launch.nestedSelf) {
565
585
  try {
566
- status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as import("../../shared/types.ts").AsyncStatus;
567
- status.processTerminal = persisted;
568
- } catch {
569
- status = {
570
- runId,
571
- mode: "single",
572
- state: persisted.state === "observed" ? "complete" : "failed",
573
- startedAt: persisted.observedAt ?? Date.now(),
574
- lastUpdate: Date.now(),
575
- processTerminal: persisted,
576
- };
577
- }
578
- writeNestedEvent(launch.nestedRoute, {
579
- type: "subagent.nested.completed",
580
- ts: Date.now(),
581
- parentRunId: launch.nestedSelf.parentRunId,
582
- parentStepIndex: launch.nestedSelf.parentStepIndex,
583
- child: nestedSummaryFromAsyncStatus(status, asyncDir, {
584
- id: runId,
586
+ let status: import("../../shared/types.ts").AsyncStatus;
587
+ try {
588
+ status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as import("../../shared/types.ts").AsyncStatus;
589
+ status.processTerminal = persisted;
590
+ } catch {
591
+ status = {
592
+ runId,
593
+ mode: "single",
594
+ state: persisted.state === "observed" ? "complete" : "failed",
595
+ startedAt: persisted.observedAt ?? Date.now(),
596
+ lastUpdate: Date.now(),
597
+ processTerminal: persisted,
598
+ };
599
+ }
600
+ writeNestedEvent(launch.nestedRoute, {
601
+ type: "subagent.nested.completed",
602
+ ts: Date.now(),
585
603
  parentRunId: launch.nestedSelf.parentRunId,
586
604
  parentStepIndex: launch.nestedSelf.parentStepIndex,
587
- depth: launch.nestedSelf.depth,
588
- path: launch.nestedSelf.path,
589
- mode: status.mode,
590
- ts: Date.now(),
591
- }),
592
- });
605
+ child: nestedSummaryFromAsyncStatus(status, asyncDir, {
606
+ id: runId,
607
+ parentRunId: launch.nestedSelf.parentRunId,
608
+ parentStepIndex: launch.nestedSelf.parentStepIndex,
609
+ depth: launch.nestedSelf.depth,
610
+ path: launch.nestedSelf.path,
611
+ mode: status.mode,
612
+ ts: Date.now(),
613
+ }),
614
+ });
615
+ } catch (error) {
616
+ console.error("Failed to emit final nested process-terminal status:", error);
617
+ }
618
+ }
619
+ try {
620
+ onProcessTerminal?.(persisted);
593
621
  } catch (error) {
594
- console.error("Failed to emit final nested process-terminal status:", error);
622
+ // The close proof is already durable. Reload/disposal can retire the
623
+ // originating Pi API before this late advisory wake; never redirect it
624
+ // to a new session or claim a receipt. Recovery must read the record.
625
+ if (!(error instanceof Error)
626
+ || !/extension ctx is stale|stale after session replacement or reload/i.test(error.message)) throw error;
595
627
  }
596
- }
597
- try {
598
- onProcessTerminal?.(persisted);
599
- } catch (error) {
600
- // The close proof is already durable. Reload/disposal can retire the
601
- // originating Pi API before this late advisory wake; never redirect it
602
- // to a new session or claim a receipt. Recovery must read the record.
603
- if (!(error instanceof Error)
604
- || !/extension ctx is stale|stale after session replacement or reload/i.test(error.message)) throw error;
628
+ } finally {
629
+ // Finalization (including honest unknown) precedes runtime teardown.
630
+ releaseCloseObserver?.();
631
+ observedClose();
605
632
  }
606
633
  });
607
634
  if (typeof proc.pid !== "number") {
@@ -1202,6 +1229,7 @@ export function executeAsyncChain(
1202
1229
  },
1203
1230
  id,
1204
1231
  runnerCwd,
1232
+ asyncRunnerDrain(ctx.pi),
1205
1233
  (proof) => ctx.pi.events.emit(SUBAGENT_PROCESS_TERMINAL_EVENT, proof),
1206
1234
  );
1207
1235
  } catch (error) {
@@ -1684,6 +1712,7 @@ export function executeAsyncSingle(
1684
1712
  },
1685
1713
  id,
1686
1714
  runnerCwd,
1715
+ asyncRunnerDrain(ctx.pi),
1687
1716
  (proof) => ctx.pi.events.emit(SUBAGENT_PROCESS_TERMINAL_EVENT, proof),
1688
1717
  );
1689
1718
  } catch (error) {
@@ -13,6 +13,19 @@ import { canonicalSessionId, inspectSessionLease } from "../shared/session-lease
13
13
  import { releaseActiveRunIndex } from "./active-run-index.ts";
14
14
  import { validOwnedExecution, projectOwnedClosure, type OwnedExecutionV2 } from "./owned-execution.ts";
15
15
 
16
+ // Process-local liveness of the original observer, not evidence of exit. A new
17
+ // parent cannot reconstruct this registration from disk or a numeric PID.
18
+ const runnerCloseObservers = new Set<string>();
19
+ const observerKey = (asyncDir: string, runId: string, instanceId: string) => JSON.stringify([path.resolve(asyncDir), runId, instanceId]);
20
+ export function registerRunnerCloseObserver(asyncDir: string, runId: string, instanceId: string): () => void {
21
+ const key = observerKey(asyncDir, runId, instanceId);
22
+ runnerCloseObservers.add(key);
23
+ return () => { runnerCloseObservers.delete(key); };
24
+ }
25
+ export function hasRunnerCloseObserver(asyncDir: string, runId: string, instanceId: string): boolean {
26
+ return runnerCloseObservers.has(observerKey(asyncDir, runId, instanceId));
27
+ }
28
+
16
29
  export interface ProcessTerminalCandidate {
17
30
  version: 1;
18
31
  runId: string;
@@ -7,6 +7,7 @@ import { readStatus } from "../../shared/utils.ts";
7
7
  import { DIRS, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode } from "../../shared/types.ts";
8
8
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
9
9
  import { normalizeParallelGroups } from "./parallel-groups.ts";
10
+ import { hasRunnerCloseObserver } from "./process-terminal.ts";
10
11
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent, type NestedRoute } from "../shared/nested-events.ts";
11
12
 
12
13
  export type PidLiveness = "alive" | "dead" | "unknown";
@@ -141,7 +142,7 @@ function childState(overallState: ResultRepairData["state"], child: ResultChildO
141
142
  return overallState;
142
143
  }
143
144
 
144
- function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now: number): AsyncStatus | undefined {
145
+ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now: number, asyncDir: string): AsyncStatus | undefined {
145
146
  const repair = readResultRepairData(resultPath);
146
147
  if (!repair) return undefined;
147
148
  const steps = (status.steps ?? []).map((step, index) => {
@@ -168,7 +169,10 @@ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now:
168
169
  const terminalStatus: AsyncStatus = {
169
170
  ...status,
170
171
  state: repair.state,
171
- ...(status.lifecycleArtifactVersion === 3 && (!status.processTerminal || status.processTerminal.state === "pending") ? {
172
+ ...(status.lifecycleArtifactVersion === 3 && (!status.processTerminal || status.processTerminal.state === "pending")
173
+ // A result can precede the launching parent's native close callback.
174
+ // Retain pending for that actual observer; this never proves exit.
175
+ && !(status.processTerminal?.state === "pending" && hasRunnerCloseObserver(asyncDir, status.runId, status.processTerminal.runnerProcessInstanceId)) ? {
172
176
  processTerminal: { version: 1 as const, state: "unknown" as const, runId: status.runId, runnerProcessInstanceId: "observer-unavailable", reason: "observer-unavailable" as const },
173
177
  } : {}),
174
178
  ...(repair.state === "stopped" ? { stopped: true } : {}),
@@ -379,7 +383,7 @@ export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOp
379
383
  : regularFileExists(resultPath) ? resultPath : undefined;
380
384
  if (existingResultPath) {
381
385
  const terminalStatus = effectiveStatus.state === "running" || effectiveStatus.state === "queued"
382
- ? terminalStatusFromResult(effectiveStatus, existingResultPath, now)
386
+ ? terminalStatusFromResult(effectiveStatus, existingResultPath, now, asyncDir)
383
387
  : undefined;
384
388
  if (terminalStatus) {
385
389
  writeAtomicJson(path.join(asyncDir, "status.json"), terminalStatus);
@@ -3,13 +3,18 @@
3
3
  - Upstream: SoL-Pi, <https://github.com/NVlabs/SoL-Pi>. License: MIT, Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES; the original notice stays in `LICENSE`.
4
4
  - Upstream base: `2b791687` ("Update Pi to 0.85.1 and verify 0.84.2 compatibility (#63)").
5
5
  - Source: the maintainer's SoL-Pi branch at `b463fad0ffab6e42488f5c80687adab67174a0ee`, imported from its tracked files. On top of the upstream base it carries five commits: recalled pages keep source references (`81b80b0`), the Pi 0.85 compaction-reason test (`83c5426`), the 0.85.1 baseline merge (`807ad60`), selected upstream reliability backports (`32840b2`), and Pi error-result handling plus runtime compatibility repairs (`b463fad`).
6
- - Maintained version: `0.1.0-rotom.0`, a private component of [rotom](https://github.com/bingjiang0611/rotom). It is not an upstream or npm release.
6
+ - Maintained version: `0.1.0-rotom.1`, a private component of [rotom](https://github.com/bingjiang0611/rotom). It is not an upstream or npm release.
7
7
 
8
8
  ## Changes in this import
9
9
 
10
- - `package.json` / `package-lock.json`: version `0.1.0-rotom.0`, rotom description, `UPSTREAM.md` added to and the hero image removed from the packed files.
10
+ - `package.json` / `package-lock.json`: version `0.1.0-rotom.1`, rotom description, `UPSTREAM.md` added to and the hero image removed from the packed files.
11
11
  - Not imported: `.github/`, `AGENTS.md`, `CLAUDE.md`, `.gitattributes` and `assets/` (the README hero image link is therefore broken in this copy).
12
- - `src/` and `tests/` are byte-identical to the source commit.
12
+ - `src/` and `tests/` were byte-identical to the source commit in `0.1.0-rotom.0`.
13
+
14
+ ## rotom changes
15
+
16
+ - `0.1.0-rotom.1`: the Evidence-Preserving Reducer records each returned reducer response (applied or rejected) as a Pi `usage` entry of kind `sol_pi_reducer`, attributed to the reducer provider/model with the provider-reported usage unchanged. Pi's footer, session stats, usage breakdown and Goal budgets then include the reducer spend. It uses `pi.appendUsage`, which the rotom Pi fork exposes from `1.3.0-rotom.2`; on Pi releases without it (upstream Pi) only the SoL-Pi journal entry is written, as before. Calls that throw before a response (timeout, unavailable model, missing auth) record no usage. Files: `evidence-preserving-reducer/{provider,journal,index}.ts`, `tests/helpers.ts`, `tests/evidence-preserving-reducer.test.ts`.
17
+ - `0.1.0-rotom.2`: an `unverifiable-quote` fallback journal entry now carries a `quoteDiagnostic` for the first rejected evidence item: `cause` (`invalid-kind`, `not-a-string`, `empty`, `too-long` or `not-in-source`), its index and the evidence count, the quote length and SHA-256, and for `not-in-source` the longest leading part found in the log with its line, plus whether the quote would match after removing ANSI escapes or collapsing whitespace. It never stores quote or log text. Acceptance is unchanged: a receipt is still rejected unless every quote appears byte for byte. Files: `evidence-preserving-reducer/{receipt,index}.ts`, `tests/evidence-preserving-reducer.test.ts`.
13
18
 
14
19
  ## How rotom uses it
15
20
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sol-pi",
3
- "version": "0.1.0-rotom.0",
3
+ "version": "0.1.0-rotom.2",
4
4
  "description": "rotom-maintained build of SoL-Pi: context and tool-efficiency extensions for Pi",
5
5
  "type": "module",
6
6
  "private": true,
@@ -39,7 +39,7 @@ import {
39
39
  type ReducerConfigOptions,
40
40
  sha256,
41
41
  } from "./config.ts";
42
- import { createJournal, type Journal } from "./journal.ts";
42
+ import { createJournal, createUsageRecorder, type Journal, type UsageRecorder } from "./journal.ts";
43
43
  import { callReducer, type ProviderResult } from "./provider.ts";
44
44
  import { receiptText, validateReceipt } from "./receipt.ts";
45
45
 
@@ -57,6 +57,7 @@ function errorName(error: unknown): string | undefined {
57
57
 
58
58
  export async function reduceToolResult(
59
59
  journal: Journal,
60
+ recordUsage: UsageRecorder,
60
61
  config: ReducerConfig,
61
62
  event: ToolResultEvent,
62
63
  context: ExtensionContext,
@@ -103,6 +104,7 @@ export async function reduceToolResult(
103
104
  return undefined;
104
105
  }
105
106
 
107
+ recordUsage(provider);
106
108
  journal("provider_response", {
107
109
  toolCallId: event.toolCallId,
108
110
  sourceSha256: archive.hash,
@@ -129,6 +131,7 @@ export async function reduceToolResult(
129
131
  toolCallId: event.toolCallId,
130
132
  sourceSha256: archive.hash,
131
133
  reason: checked.reason,
134
+ quoteDiagnostic: checked.quoteDiagnostic,
132
135
  usage: provider.usage,
133
136
  });
134
137
  return undefined;
@@ -179,6 +182,7 @@ export async function reduceToolResult(
179
182
  export function createEvidencePreservingReducerExtension(options: EvidencePreservingReducerOptions = {}): ExtensionFactory {
180
183
  return (pi: ExtensionAPI) => {
181
184
  const states = new Map<string, { config: ReducerConfig; journal: Journal }>();
185
+ const recordUsage = createUsageRecorder(pi);
182
186
  pi.on("tool_result", (event, context) => {
183
187
  let root: string;
184
188
  try {
@@ -192,7 +196,7 @@ export function createEvidencePreservingReducerExtension(options: EvidencePreser
192
196
  state = { config, journal: createJournal(pi, config) };
193
197
  states.set(root, state);
194
198
  }
195
- return reduceToolResult(state.journal, state.config, event, context);
199
+ return reduceToolResult(state.journal, recordUsage, state.config, event, context);
196
200
  });
197
201
  };
198
202
  }
@@ -2,8 +2,13 @@
2
2
  * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
3
  * SPDX-License-Identifier: MIT
4
4
  */
5
+ import type { Usage } from "@earendil-works/pi-ai";
5
6
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
7
  import { REDUCER_EVENT_SCHEMA, REDUCER_EVENT_TYPE, type ReducerConfig } from "./config.ts";
8
+ import type { ProviderResult } from "./provider.ts";
9
+
10
+ /** Pi usage-entry kind for reducer model calls. */
11
+ export const REDUCER_USAGE_KIND = "sol_pi_reducer";
7
12
 
8
13
  /**
9
14
  * Append one non-context session entry per decision the reducer made.
@@ -23,3 +28,19 @@ export function createJournal(pi: ExtensionAPI, config: ReducerConfig): Journal
23
28
  });
24
29
  };
25
30
  }
31
+
32
+ type AppendUsage = (kind: string, provider: string, model: string, usage: Usage, note?: string) => void;
33
+ export type UsageRecorder = (provider: ProviderResult) => void;
34
+
35
+ /**
36
+ * Record each reducer response in Pi's usage entries so footer, session stats
37
+ * and budgets include the reducer spend. Pi releases without
38
+ * `pi.appendUsage` (upstream Pi) keep only the journal record.
39
+ */
40
+ export function createUsageRecorder(pi: ExtensionAPI): UsageRecorder {
41
+ const appendUsage = (pi as ExtensionAPI & { appendUsage?: AppendUsage }).appendUsage;
42
+ if (typeof appendUsage !== "function") return () => {};
43
+ return (provider) => {
44
+ appendUsage.call(pi, REDUCER_USAGE_KIND, provider.provider, provider.responseModel, provider.responseUsage);
45
+ };
46
+ }
@@ -3,7 +3,7 @@
3
3
  * SPDX-License-Identifier: MIT
4
4
  */
5
5
 
6
- import type { Api, AssistantMessage, Context, Model, ProviderStreamOptions } from "@earendil-works/pi-ai";
6
+ import type { Api, AssistantMessage, Context, Model, ProviderStreamOptions, Usage } from "@earendil-works/pi-ai";
7
7
  import { complete as completeCompat } from "@earendil-works/pi-ai/compat";
8
8
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
9
9
  import type { ArchiveObject } from "./archive.ts";
@@ -44,6 +44,10 @@ export interface ProviderResult {
44
44
  readonly ok: boolean;
45
45
  readonly outputText: string;
46
46
  readonly provider: string;
47
+ /** Concrete model that served the request, for usage attribution. */
48
+ readonly responseModel: string;
49
+ /** Provider-reported usage including cost, recorded unchanged in Pi's usage entries. */
50
+ readonly responseUsage: Usage;
47
51
  readonly stopReason: AssistantMessage["stopReason"];
48
52
  readonly usage: NormalizedUsage;
49
53
  }
@@ -154,6 +158,8 @@ export async function callReducer(
154
158
  ok: response.stopReason === "stop" || response.stopReason === "length",
155
159
  outputText: responseOutputText(response),
156
160
  provider: response.provider,
161
+ responseModel: response.responseModel ?? response.model,
162
+ responseUsage: response.usage,
157
163
  stopReason: response.stopReason,
158
164
  usage: normalizedUsage(response),
159
165
  };