@oh-my-pi/pi-coding-agent 17.2.9 → 17.2.11

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 (281) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/{CHANGELOG-8yy8h3bc.md → CHANGELOG-d9xenpn9.md} +83 -0
  3. package/dist/cli.js +10208 -12596
  4. package/dist/types/capability/mcp.d.ts +11 -0
  5. package/dist/types/capability/skill.d.ts +6 -0
  6. package/dist/types/cli/args.d.ts +1 -0
  7. package/dist/types/cli/command-help.d.ts +3 -0
  8. package/dist/types/commands/share.d.ts +25 -0
  9. package/dist/types/commit/agentic/index.d.ts +3 -1
  10. package/dist/types/commit/execute.d.ts +32 -0
  11. package/dist/types/commit/index.d.ts +2 -1
  12. package/dist/types/commit/pipeline.d.ts +7 -1
  13. package/dist/types/config/model-registry.d.ts +4 -0
  14. package/dist/types/config/model-resolver.d.ts +13 -0
  15. package/dist/types/config/settings-schema.d.ts +1 -34
  16. package/dist/types/discovery/agent-plugin-format.d.ts +143 -0
  17. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  18. package/dist/types/discovery/contained-path.d.ts +34 -0
  19. package/dist/types/discovery/index.d.ts +1 -0
  20. package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
  21. package/dist/types/extensibility/custom-tools/types.d.ts +3 -3
  22. package/dist/types/extensibility/extensions/loader.d.ts +9 -2
  23. package/dist/types/extensibility/extensions/types.d.ts +45 -8
  24. package/dist/types/extensibility/hooks/types.d.ts +5 -5
  25. package/dist/types/extensibility/shared-events.d.ts +3 -2
  26. package/dist/types/extensibility/skills.d.ts +5 -0
  27. package/dist/types/index.d.ts +2 -2
  28. package/dist/types/main.d.ts +1 -1
  29. package/dist/types/mcp/transports/header-policy.d.ts +45 -0
  30. package/dist/types/mcp/types.d.ts +15 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  32. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  33. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
  34. package/dist/types/modes/acp/acp-mode.d.ts +1 -1
  35. package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
  36. package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
  37. package/dist/types/modes/components/agent-hub.d.ts +14 -5
  38. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  39. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  40. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  41. package/dist/types/modes/components/model-browser.d.ts +9 -1
  42. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  43. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  44. package/dist/types/modes/interactive-mode.d.ts +8 -1
  45. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  46. package/dist/types/registry/agent-registry.d.ts +47 -0
  47. package/dist/types/registry/persisted-agents.d.ts +3 -1
  48. package/dist/types/secrets/index.d.ts +23 -1
  49. package/dist/types/session/agent-session-events.d.ts +2 -2
  50. package/dist/types/session/agent-session.d.ts +4 -2
  51. package/dist/types/session/messages.d.ts +1 -0
  52. package/dist/types/session/prewalk.d.ts +3 -1
  53. package/dist/types/session/session-entries.d.ts +10 -0
  54. package/dist/types/session/session-loader.d.ts +11 -1
  55. package/dist/types/session/session-manager.d.ts +9 -1
  56. package/dist/types/session/session-tools.d.ts +18 -1
  57. package/dist/types/session/turn-recovery.d.ts +7 -2
  58. package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
  59. package/dist/types/slash-commands/available-commands.d.ts +1 -1
  60. package/dist/types/task/executor.d.ts +6 -0
  61. package/dist/types/task/index.d.ts +4 -3
  62. package/dist/types/task/read-only-policy.d.ts +3 -0
  63. package/dist/types/task/structured-subagent.d.ts +2 -0
  64. package/dist/types/task/types.d.ts +4 -0
  65. package/dist/types/tools/bash.d.ts +3 -3
  66. package/dist/types/tools/browser/launch.d.ts +2 -1
  67. package/dist/types/tools/browser/tab-worker.d.ts +2 -2
  68. package/dist/types/tools/path-utils.d.ts +1 -0
  69. package/dist/types/tools/read.d.ts +1 -5
  70. package/dist/types/tools/run-scope.d.ts +27 -1
  71. package/dist/types/tools/shell-tokenize.d.ts +16 -0
  72. package/dist/types/tools/terminal-output.d.ts +1 -1
  73. package/dist/types/utils/turndown.d.ts +1 -1
  74. package/dist/types/vibe/runtime.d.ts +2 -3
  75. package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
  76. package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
  77. package/examples/custom-tools/README.md +1 -1
  78. package/examples/extensions/README.md +1 -1
  79. package/examples/extensions/api-demo.ts +5 -6
  80. package/examples/extensions/chalk-logger.ts +1 -1
  81. package/examples/extensions/hello.ts +2 -2
  82. package/examples/extensions/reload-runtime.ts +1 -1
  83. package/examples/extensions/tools.ts +2 -2
  84. package/examples/extensions/with-deps/index.ts +1 -1
  85. package/examples/sdk/06-extensions.ts +1 -1
  86. package/package.json +15 -31
  87. package/scripts/bundle-dist.ts +1 -11
  88. package/scripts/legacy-pi-virtual-module.ts +4 -1
  89. package/src/advisor/runtime.ts +54 -0
  90. package/src/capability/mcp.ts +11 -0
  91. package/src/capability/skill.ts +6 -0
  92. package/src/cli/agents-cli.ts +1 -1
  93. package/src/cli/args.ts +24 -2
  94. package/src/cli/auth-broker-cli.ts +1 -1
  95. package/src/cli/auth-gateway-cli.ts +1 -1
  96. package/src/cli/bench-cli.ts +1 -1
  97. package/src/cli/claude-trace-cli.ts +1 -1
  98. package/src/cli/command-help.ts +4 -0
  99. package/src/cli/config-cli.ts +1 -1
  100. package/src/cli/dry-balance-cli.ts +1 -1
  101. package/src/cli/file-processor.ts +1 -1
  102. package/src/cli/flag-tables.ts +4 -0
  103. package/src/cli/gallery-cli.ts +1 -1
  104. package/src/cli/grep-cli.ts +1 -1
  105. package/src/cli/grievances-cli.ts +1 -1
  106. package/src/cli/help-extra.ts +1 -1
  107. package/src/cli/models-cli.ts +1 -1
  108. package/src/cli/plugin-cli.ts +1 -1
  109. package/src/cli/read-cli.ts +1 -1
  110. package/src/cli/setup-cli.ts +1 -1
  111. package/src/cli/shell-cli.ts +1 -1
  112. package/src/cli/ssh-cli.ts +1 -1
  113. package/src/cli/stats-cli.ts +1 -1
  114. package/src/cli/tiny-models-cli.ts +1 -1
  115. package/src/cli/ttsr-cli.ts +1 -1
  116. package/src/cli/update-cli.ts +1 -1
  117. package/src/cli/usage-cli.ts +1 -1
  118. package/src/cli/web-search-cli.ts +1 -1
  119. package/src/cli/worktree-cli.ts +1 -1
  120. package/src/cli-commands.ts +5 -0
  121. package/src/commands/commit.ts +16 -3
  122. package/src/commands/say.ts +1 -1
  123. package/src/commands/share.ts +71 -0
  124. package/src/commands/token.ts +1 -1
  125. package/src/commit/agentic/agent.ts +1 -1
  126. package/src/commit/agentic/index.ts +39 -21
  127. package/src/commit/cli.ts +1 -1
  128. package/src/commit/execute.ts +56 -0
  129. package/src/commit/index.ts +2 -1
  130. package/src/commit/pipeline.ts +20 -7
  131. package/src/config/model-discovery.ts +1 -1
  132. package/src/config/model-registry.ts +21 -1
  133. package/src/config/model-resolver.ts +54 -7
  134. package/src/config/settings-schema.ts +2 -33
  135. package/src/config/settings.ts +46 -0
  136. package/src/discovery/agent-plugin-format.ts +551 -0
  137. package/src/discovery/agent-plugins.ts +341 -0
  138. package/src/discovery/agents.ts +4 -2
  139. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
  140. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
  141. package/src/discovery/claude-plugins.ts +21 -5
  142. package/src/discovery/contained-path.ts +78 -0
  143. package/src/discovery/helpers.ts +23 -9
  144. package/src/discovery/index.ts +1 -0
  145. package/src/discovery/omp-plugins.ts +20 -7
  146. package/src/eval/py/runner.py +38 -1
  147. package/src/exec/non-interactive-env.ts +1 -0
  148. package/src/extensibility/custom-commands/loader.ts +4 -4
  149. package/src/extensibility/custom-commands/types.ts +5 -5
  150. package/src/extensibility/custom-tools/loader.ts +4 -4
  151. package/src/extensibility/custom-tools/types.ts +3 -3
  152. package/src/extensibility/extensions/loader.ts +85 -20
  153. package/src/extensibility/extensions/runner.ts +6 -0
  154. package/src/extensibility/extensions/types.ts +51 -8
  155. package/src/extensibility/hooks/loader.ts +4 -5
  156. package/src/extensibility/hooks/types.ts +5 -5
  157. package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
  158. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  159. package/src/extensibility/shared-events.ts +3 -2
  160. package/src/extensibility/skills.ts +9 -0
  161. package/src/index.ts +2 -2
  162. package/src/internal-urls/memory-protocol.ts +5 -1
  163. package/src/internal-urls/skill-protocol.ts +14 -0
  164. package/src/internal-urls/vault-protocol.ts +2 -2
  165. package/src/launch/client.ts +11 -1
  166. package/src/launch/protocol.ts +7 -2
  167. package/src/launch/terminal-output.ts +1 -1
  168. package/src/main.ts +88 -23
  169. package/src/markit/converters/docx.ts +1 -1
  170. package/src/markit/converters/epub.ts +1 -1
  171. package/src/markit/converters/pptx.ts +1 -1
  172. package/src/markit/converters/xlsx.ts +1 -1
  173. package/src/mcp/config.ts +3 -0
  174. package/src/mcp/manager.ts +12 -9
  175. package/src/mcp/transports/header-policy.ts +95 -0
  176. package/src/mcp/transports/http.ts +35 -52
  177. package/src/mcp/transports/sse.ts +20 -27
  178. package/src/mcp/types.ts +15 -0
  179. package/src/modes/acp/acp-agent.ts +20 -9
  180. package/src/modes/acp/acp-client-bridge.ts +1 -1
  181. package/src/modes/acp/acp-event-mapper.ts +1 -1
  182. package/src/modes/acp/acp-mode.ts +1 -1
  183. package/src/modes/components/agent-dashboard.ts +2 -0
  184. package/src/modes/components/agent-hub-projection.ts +248 -0
  185. package/src/modes/components/agent-hub-renderer.ts +194 -0
  186. package/src/modes/components/agent-hub.ts +674 -207
  187. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  188. package/src/modes/components/assistant-message.ts +1 -1
  189. package/src/modes/components/chat-transcript-builder.ts +3 -0
  190. package/src/modes/components/custom-editor.ts +0 -9
  191. package/src/modes/components/extensions/inspector-panel.ts +11 -6
  192. package/src/modes/components/footer.ts +1 -3
  193. package/src/modes/components/model-browser.ts +11 -3
  194. package/src/modes/components/model-hub.ts +4 -1
  195. package/src/modes/components/status-line/component.ts +1 -0
  196. package/src/modes/components/status-line/segments.ts +10 -7
  197. package/src/modes/components/status-line/types.ts +2 -0
  198. package/src/modes/components/tool-execution.ts +13 -16
  199. package/src/modes/components/tree-selector.ts +62 -3
  200. package/src/modes/controllers/command-controller.ts +4 -1
  201. package/src/modes/controllers/event-controller.ts +44 -20
  202. package/src/modes/controllers/extension-ui-controller.ts +2 -2
  203. package/src/modes/controllers/input-controller.ts +20 -2
  204. package/src/modes/controllers/mcp-command-controller.ts +81 -20
  205. package/src/modes/controllers/selector-controller.ts +41 -49
  206. package/src/modes/controllers/streaming-reveal.ts +1 -1
  207. package/src/modes/interactive-mode.ts +13 -2
  208. package/src/modes/rpc/rpc-mode.ts +2 -2
  209. package/src/modes/runtime-init.ts +1 -1
  210. package/src/modes/theme/theme.ts +2 -2
  211. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  212. package/src/modes/utils/ui-helpers.ts +1 -0
  213. package/src/prompts/tools/computer.md +1 -1
  214. package/src/registry/agent-lifecycle.ts +14 -7
  215. package/src/registry/agent-registry.ts +65 -2
  216. package/src/registry/persisted-agents.ts +341 -16
  217. package/src/sdk.ts +6 -49
  218. package/src/secrets/index.ts +52 -1
  219. package/src/session/agent-session-events.ts +2 -2
  220. package/src/session/agent-session.ts +54 -16
  221. package/src/session/messages.ts +1 -0
  222. package/src/session/prewalk.ts +57 -17
  223. package/src/session/session-context.ts +3 -5
  224. package/src/session/session-entries.ts +10 -0
  225. package/src/session/session-handoff.ts +5 -1
  226. package/src/session/session-listing.ts +1 -1
  227. package/src/session/session-loader.ts +74 -8
  228. package/src/session/session-manager.ts +22 -2
  229. package/src/session/session-paths.ts +57 -9
  230. package/src/session/session-tools.ts +65 -4
  231. package/src/session/todo-tracker.ts +11 -1
  232. package/src/session/turn-recovery.ts +96 -74
  233. package/src/slash-commands/acp-builtins.ts +1 -1
  234. package/src/slash-commands/available-commands.ts +1 -1
  235. package/src/slash-commands/builtin-registry.ts +12 -8
  236. package/src/task/executor.ts +70 -20
  237. package/src/task/index.ts +30 -34
  238. package/src/task/isolation-runner.ts +24 -11
  239. package/src/task/persisted-revive.ts +12 -5
  240. package/src/task/read-only-policy.ts +27 -0
  241. package/src/task/structured-subagent.ts +14 -4
  242. package/src/task/types.ts +4 -0
  243. package/src/tools/auto-generated-guard.ts +1 -1
  244. package/src/tools/bash-interactive.ts +4 -4
  245. package/src/tools/bash-skill-urls.ts +15 -0
  246. package/src/tools/bash.ts +16 -17
  247. package/src/tools/browser/cmux/cmux-tab.ts +66 -18
  248. package/src/tools/browser/launch.ts +41 -12
  249. package/src/tools/browser/readable.ts +9 -9
  250. package/src/tools/browser/tab-supervisor.ts +12 -3
  251. package/src/tools/browser/tab-worker-entry.ts +1 -1
  252. package/src/tools/browser/tab-worker.ts +101 -12
  253. package/src/tools/debug.ts +1 -1
  254. package/src/tools/fetch.ts +1 -1
  255. package/src/tools/jtd-to-json-schema.ts +123 -21
  256. package/src/tools/path-utils.ts +9 -2
  257. package/src/tools/read.ts +21 -9
  258. package/src/tools/run-scope.ts +290 -4
  259. package/src/tools/shell-tokenize.ts +98 -0
  260. package/src/tools/terminal-output.ts +1 -1
  261. package/src/tools/todo.ts +1 -1
  262. package/src/utils/clipboard.ts +10 -2
  263. package/src/utils/external-editor.ts +4 -2
  264. package/src/utils/jj.ts +1 -1
  265. package/src/utils/turndown.ts +1 -2
  266. package/src/vibe/runtime.ts +5 -5
  267. package/src/web/scrapers/arxiv.ts +1 -1
  268. package/src/web/scrapers/go-pkg.ts +1 -1
  269. package/src/web/scrapers/iacr.ts +1 -1
  270. package/src/web/scrapers/readthedocs.ts +2 -1
  271. package/src/web/scrapers/twitter.ts +2 -2
  272. package/src/web/scrapers/types.ts +1 -1
  273. package/src/web/scrapers/wikipedia.ts +1 -1
  274. package/src/web/search/providers/browser-headers.ts +12 -39
  275. package/src/web/search/providers/codex.ts +3 -26
  276. package/src/web/search/providers/ecosia.ts +1 -1
  277. package/src/web/search/providers/exa.ts +1 -1
  278. package/src/web/search/providers/google.ts +1 -1
  279. package/src/web/search/providers/mojeek.ts +1 -1
  280. package/src/web/search/providers/startpage.ts +1 -1
  281. package/src/markit/converters/mammoth.d.ts +0 -24
@@ -1,26 +1,281 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { ADVISOR_TRANSCRIPT_FILENAME, isAdvisorTranscriptName } from "../advisor/transcript-recorder";
4
- import { SessionManager } from "../session/session-manager";
4
+ import { resolveExplicitModelRole } from "../config/model-resolver";
5
+ import { EPHEMERAL_MODEL_CHANGE_ROLE } from "../session/session-entries";
6
+ import { visitEntriesFromFileStream } from "../session/session-loader";
7
+ import { loadBundledAgents } from "../task/agents";
8
+ import { isReadOnlyAgent } from "../task/read-only-policy";
5
9
  import { persistedVibeChildIds } from "../vibe/runtime";
6
- import { type AgentRegistry, MAIN_AGENT_ID } from "./agent-registry";
10
+ import {
11
+ type AgentHistorySummary,
12
+ type AgentMetricsSummary,
13
+ type AgentRegistry,
14
+ getAgentTombstonePath,
15
+ MAIN_AGENT_ID,
16
+ } from "./agent-registry";
17
+
18
+ /** Maximum prefix entries inspected for task metadata. */
19
+ const MAX_METADATA_LINES = 64;
20
+
21
+ interface PersistedAgentMetadata {
22
+ activity?: string;
23
+ createdAt?: number;
24
+ lastActivity?: number;
25
+ history?: AgentHistorySummary;
26
+ }
27
+
28
+ interface PersistedTranscript {
29
+ id: string;
30
+ sessionFile: string;
31
+ createdAt?: number;
32
+ lastActivity?: number;
33
+ }
34
+
35
+ function recordOf(value: unknown): Record<string, unknown> | undefined {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value)
37
+ ? (value as Record<string, unknown>)
38
+ : undefined;
39
+ }
40
+
41
+ function timestampOf(value: unknown): number | undefined {
42
+ if (typeof value !== "string") return undefined;
43
+ const timestamp = Date.parse(value);
44
+ return Number.isFinite(timestamp) ? timestamp : undefined;
45
+ }
46
+
47
+ function summarizePersistedTask(task: string): string | undefined {
48
+ const withoutPreamble = task.replace(/^Complete the assignment below,\s*thoroughly:\s*/i, "");
49
+ const lines = withoutPreamble.split(/\r?\n/);
50
+ const targetIndex = lines.findIndex(line => line.trim().toLowerCase() === "# target");
51
+ const targetLines: string[] = [];
52
+ if (targetIndex >= 0) {
53
+ for (const line of lines.slice(targetIndex + 1)) {
54
+ if (line.trimStart().startsWith("# ")) break;
55
+ targetLines.push(line);
56
+ }
57
+ }
58
+ const summary = (targetLines.length > 0 ? targetLines : lines).join(" ").replace(/\s+/g, " ").trim();
59
+ return summary ? summary.slice(0, 1_000) : undefined;
60
+ }
61
+
62
+ function finiteNumber(value: unknown): number {
63
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
64
+ }
65
+
66
+ function inferBundledAgent(systemPrompt: string): { agent?: string; modelRole?: string; readOnly?: boolean } {
67
+ const matches = loadBundledAgents().filter(agent => {
68
+ const rolePrompt = agent.systemPrompt.trim();
69
+ return rolePrompt.length > 0 && systemPrompt.includes(rolePrompt);
70
+ });
71
+ // `task` and `sonic` intentionally share a prompt body. Ambiguous historical
72
+ // prompts stay unlabelled rather than inventing provenance.
73
+ if (matches.length !== 1) return {};
74
+ const [agent] = matches;
75
+ return {
76
+ agent: agent.name,
77
+ modelRole: resolveExplicitModelRole(agent.model),
78
+ readOnly: isReadOnlyAgent(agent),
79
+ };
80
+ }
81
+
82
+ function usageTokens(usage: Record<string, unknown>): number {
83
+ const computed = finiteNumber(usage.input) + finiteNumber(usage.output) + finiteNumber(usage.cacheWrite);
84
+ return computed > 0 ? computed : finiteNumber(usage.totalTokens);
85
+ }
86
+
87
+ interface AssistantMetrics {
88
+ tokens: number;
89
+ tools: number;
90
+ cost: number;
91
+ contextTokens?: number;
92
+ resolvedModel?: string;
93
+ }
94
+
95
+ function assistantMetrics(message: Record<string, unknown>): AssistantMetrics {
96
+ const usage = recordOf(message.usage) ?? {};
97
+ const cost = recordOf(usage.cost);
98
+ const content = Array.isArray(message.content) ? message.content : [];
99
+ const provider = typeof message.provider === "string" ? message.provider : undefined;
100
+ const model = typeof message.model === "string" ? message.model : undefined;
101
+ return {
102
+ tokens: usageTokens(usage),
103
+ tools: content.filter(part => recordOf(part)?.type === "toolCall").length,
104
+ cost: finiteNumber(cost?.total),
105
+ contextTokens: finiteNumber(usage.totalTokens) || undefined,
106
+ resolvedModel: provider && model ? `${provider}/${model}` : undefined,
107
+ };
108
+ }
109
+
110
+ async function readPersistedAgentHistory(
111
+ transcript: PersistedTranscript,
112
+ shouldContinue: () => boolean,
113
+ ): Promise<AgentHistorySummary> {
114
+ const parents = new Map<string, string | undefined>();
115
+ const assistantById = new Map<string, AssistantMetrics>();
116
+ const modelChangeById = new Map<string, { model: string; role?: string; resolvedModelIsFallback: boolean }>();
117
+ let leafId: string | undefined;
118
+ let leafTimestamp: number | undefined;
119
+ try {
120
+ await visitEntriesFromFileStream(
121
+ transcript.sessionFile,
122
+ entry => {
123
+ const record = recordOf(entry);
124
+ if (!record) return;
125
+ const id = typeof record.id === "string" ? record.id : undefined;
126
+ if (!id) return;
127
+ const parentId = typeof record.parentId === "string" ? record.parentId : undefined;
128
+ parents.set(id, parentId);
129
+ leafId = id;
130
+ const parsedTimestamp = timestampOf(record.timestamp);
131
+ if (parsedTimestamp !== undefined) leafTimestamp = parsedTimestamp;
132
+ if (record.type === "model_change" && typeof record.model === "string") {
133
+ modelChangeById.set(id, {
134
+ model: record.model,
135
+ role: typeof record.role === "string" ? record.role : undefined,
136
+ resolvedModelIsFallback: record.resolvedModelIsFallback === true,
137
+ });
138
+ return;
139
+ }
140
+ if (record.type !== "message") return;
141
+ const message = recordOf(record.message);
142
+ if (message?.role === "assistant") assistantById.set(id, assistantMetrics(message));
143
+ },
144
+ { shouldContinue },
145
+ );
146
+ } catch {
147
+ return {};
148
+ }
149
+
150
+ const metrics: AgentMetricsSummary = {
151
+ tokens: 0,
152
+ requests: 0,
153
+ tools: 0,
154
+ cost: 0,
155
+ durationMs: Math.max(
156
+ 0,
157
+ (leafTimestamp ?? transcript.lastActivity ?? transcript.createdAt ?? 0) -
158
+ (transcript.createdAt ?? leafTimestamp ?? 0),
159
+ ),
160
+ durationKind: "span",
161
+ };
162
+ let resolvedModel: string | undefined;
163
+ let resolvedModelIsFallback: boolean | undefined;
164
+ let modelRole: string | undefined;
165
+ let contextTokens: number | undefined;
166
+ let modelChangeFound = false;
167
+ const visited = new Set<string>();
168
+ for (let id = leafId; id && !visited.has(id); id = parents.get(id)) {
169
+ visited.add(id);
170
+ const modelChange = modelChangeById.get(id);
171
+ if (modelChange && !modelChangeFound) {
172
+ modelChangeFound = true;
173
+ resolvedModel = modelChange.model;
174
+ resolvedModelIsFallback = modelChange.resolvedModelIsFallback;
175
+ if (modelChange.role && modelChange.role !== EPHEMERAL_MODEL_CHANGE_ROLE) {
176
+ modelRole = modelChange.role;
177
+ }
178
+ }
179
+ const assistant = assistantById.get(id);
180
+ if (!assistant) continue;
181
+ if (!modelChangeFound && resolvedModel === undefined && assistant.resolvedModel) {
182
+ resolvedModel = assistant.resolvedModel;
183
+ }
184
+ metrics.requests++;
185
+ metrics.tokens += assistant.tokens;
186
+ metrics.tools += assistant.tools;
187
+ metrics.cost += assistant.cost;
188
+ contextTokens ??= assistant.contextTokens;
189
+ }
190
+ if (contextTokens !== undefined) metrics.contextTokens = contextTokens;
191
+ return {
192
+ ...(metrics.requests > 0 ? { metrics } : {}),
193
+ ...(resolvedModel ? { resolvedModel, resolvedModelIsFallback } : {}),
194
+ ...(modelRole ? { modelRole } : {}),
195
+ };
196
+ }
7
197
 
8
198
  /**
9
- * Child ids owned by the Vibe roster persisted in this session file. Vibe
10
- * workers are revived through the Vibe registry's own journal, so the generic
11
- * persisted-subagent scan must not register them as plain `sub` refs.
199
+ * Read only the small session prefix needed by the Hub. A subagent's first
200
+ * `session_init` is written before its conversation, so this never walks a
201
+ * multi-megabyte historical transcript just to populate one roster row.
12
202
  */
13
- async function readPersistedVibeChildIds(sessionFile: string): Promise<Set<string>> {
14
- let sessionManager: SessionManager;
203
+ async function readPersistedAgentMetadata(sessionFile: string): Promise<PersistedAgentMetadata> {
204
+ const stat = fs.promises.stat(sessionFile).catch(() => undefined);
205
+ const artifactBase = sessionFile.slice(0, -".jsonl".length);
206
+ const outputPath = `${artifactBase}.md`;
207
+ const patchPath = `${artifactBase}.patch`;
208
+ const artifactFiles = Promise.all([Bun.file(outputPath).exists(), Bun.file(patchPath).exists()]);
209
+ let createdAt: number | undefined;
210
+ let activity: string | undefined;
211
+ let history: AgentHistorySummary = {};
15
212
  try {
16
- sessionManager = await SessionManager.open(sessionFile, undefined, undefined, { suppressBreadcrumb: true });
213
+ await visitEntriesFromFileStream(
214
+ sessionFile,
215
+ entry => {
216
+ const record = recordOf(entry);
217
+ if (!record) return;
218
+ if (record.type === "session") {
219
+ createdAt ??= timestampOf(record.timestamp);
220
+ return;
221
+ }
222
+ if (record.type === "model_change") {
223
+ if (typeof record.model === "string") history.resolvedModel = record.model;
224
+ if (typeof record.role === "string" && record.role !== EPHEMERAL_MODEL_CHANGE_ROLE) {
225
+ history.modelRole = record.role;
226
+ }
227
+ if (typeof record.resolvedModelIsFallback === "boolean") {
228
+ history.resolvedModelIsFallback = record.resolvedModelIsFallback;
229
+ }
230
+ return;
231
+ }
232
+ if (record.type !== "session_init") return;
233
+ createdAt ??= timestampOf(record.timestamp);
234
+ if (typeof record.task === "string") activity = summarizePersistedTask(record.task);
235
+ const inferred = typeof record.systemPrompt === "string" ? inferBundledAgent(record.systemPrompt) : {};
236
+ history = {
237
+ ...history,
238
+ ...inferred,
239
+ agent: typeof record.agent === "string" ? record.agent : inferred.agent,
240
+ modelRole:
241
+ typeof record.modelRole === "string" ? record.modelRole : (history.modelRole ?? inferred.modelRole),
242
+ resolvedModel: typeof record.resolvedModel === "string" ? record.resolvedModel : history.resolvedModel,
243
+ readOnly: typeof record.readOnly === "boolean" ? record.readOnly : inferred.readOnly,
244
+ };
245
+ return false;
246
+ },
247
+ { maxRecords: MAX_METADATA_LINES },
248
+ );
17
249
  } catch {
18
- return new Set();
250
+ // A readable transcript is still useful even when its optional metadata
251
+ // prefix is malformed.
19
252
  }
253
+ const [file, [hasOutput, hasPatch]] = await Promise.all([stat, artifactFiles]);
254
+ return {
255
+ activity,
256
+ createdAt: createdAt ?? file?.birthtimeMs,
257
+ lastActivity: file?.mtimeMs,
258
+ history: {
259
+ ...history,
260
+ ...(hasOutput ? { outputPath } : {}),
261
+ ...(hasPatch ? { patchPath } : {}),
262
+ },
263
+ };
264
+ }
265
+
266
+ async function readPersistedVibeChildIds(sessionFile: string, shouldContinue: () => boolean): Promise<Set<string>> {
267
+ const ids = new Set<string>();
20
268
  try {
21
- return persistedVibeChildIds(sessionManager.getEntries());
22
- } finally {
23
- await sessionManager.close();
269
+ await visitEntriesFromFileStream(
270
+ sessionFile,
271
+ entry => {
272
+ for (const id of persistedVibeChildIds([entry])) ids.add(id);
273
+ },
274
+ { shouldContinue },
275
+ );
276
+ return ids;
277
+ } catch {
278
+ return new Set();
24
279
  }
25
280
  }
26
281
 
@@ -28,11 +283,30 @@ async function readPersistedVibeChildIds(sessionFile: string): Promise<Set<strin
28
283
  export async function registerPersistedSubagents(
29
284
  registry: AgentRegistry,
30
285
  sessionFile: string | null | undefined,
286
+ options: { shouldContinue?: () => boolean } = {},
31
287
  ): Promise<void> {
32
288
  if (!sessionFile?.endsWith(".jsonl")) return;
33
- const vibeOwnedIds = await readPersistedVibeChildIds(sessionFile);
289
+ const shouldContinue = options.shouldContinue ?? (() => true);
290
+ if (!shouldContinue()) return;
291
+ const vibeOwnedIds = await readPersistedVibeChildIds(sessionFile, shouldContinue);
292
+ if (!shouldContinue()) return;
34
293
  const root = sessionFile.slice(0, -6);
35
- await registerPersistedSubagentsFromDir(registry, root, undefined, vibeOwnedIds);
294
+ const transcripts: PersistedTranscript[] = [];
295
+ await registerPersistedSubagentsFromDir(registry, root, undefined, vibeOwnedIds, transcripts, shouldContinue);
296
+ if (!shouldContinue()) return;
297
+ let nextTranscript = 0;
298
+ const workers = Array.from({ length: Math.min(4, transcripts.length) }, async () => {
299
+ for (;;) {
300
+ if (!shouldContinue()) return;
301
+ const index = nextTranscript++;
302
+ const transcript = transcripts[index];
303
+ if (!transcript) return;
304
+ const history = await readPersistedAgentHistory(transcript, shouldContinue);
305
+ if (!shouldContinue()) return;
306
+ registry.setHistory(transcript.id, history, transcript.sessionFile);
307
+ }
308
+ });
309
+ await Promise.all(workers);
36
310
  }
37
311
 
38
312
  async function registerPersistedSubagentsFromDir(
@@ -40,14 +314,25 @@ async function registerPersistedSubagentsFromDir(
40
314
  dir: string,
41
315
  parentId: string | undefined,
42
316
  vibeOwnedIds: ReadonlySet<string>,
317
+ transcripts: PersistedTranscript[],
318
+ shouldContinue: () => boolean,
43
319
  ): Promise<void> {
320
+ if (!shouldContinue()) return;
44
321
  let entries: fs.Dirent[];
45
322
  try {
46
323
  entries = await fs.promises.readdir(dir, { withFileTypes: true });
47
324
  } catch {
48
325
  return;
49
326
  }
327
+ if (!shouldContinue()) return;
328
+ let entriesSinceYield = 0;
50
329
  for (const entry of entries) {
330
+ if (!shouldContinue()) return;
331
+ if (++entriesSinceYield >= 16) {
332
+ entriesSinceYield = 0;
333
+ await Bun.sleep(0);
334
+ }
335
+ if (!shouldContinue()) return;
51
336
  if (!entry.isFile() || !entry.name.endsWith(".jsonl") || entry.name.includes(".bak")) continue;
52
337
  const sessionFile = path.join(dir, entry.name);
53
338
  // The advisor transcript is observability-only: register it as a non-peer
@@ -66,6 +351,8 @@ async function registerPersistedSubagentsFromDir(
66
351
  // user task literally named `<owner>/advisor`): leave it, skip the advisor.
67
352
  if (existing && existing.kind !== "advisor") continue;
68
353
  if (existing?.sessionFile !== sessionFile) {
354
+ const metadata = await readPersistedAgentMetadata(sessionFile);
355
+ if (!shouldContinue()) return;
69
356
  // The id is reused across `/new`; refresh it to the current session's file.
70
357
  if (existing) registry.unregister(advisorId);
71
358
  registry.register({
@@ -75,14 +362,34 @@ async function registerPersistedSubagentsFromDir(
75
362
  parentId: owner,
76
363
  session: null,
77
364
  sessionFile,
365
+ activity: metadata.activity,
366
+ createdAt: metadata.createdAt,
367
+ lastActivity: metadata.lastActivity,
368
+ history: { ...metadata.history, readOnly: true },
78
369
  status: "parked",
79
370
  });
371
+ transcripts.push({
372
+ id: advisorId,
373
+ sessionFile,
374
+ createdAt: metadata.createdAt,
375
+ lastActivity: metadata.lastActivity,
376
+ });
80
377
  }
81
378
  continue;
82
379
  }
83
380
  const id = entry.name.slice(0, -6);
84
381
  if (vibeOwnedIds.has(id) && registry.get(id)?.sessionFile !== sessionFile) continue;
382
+ let tombstoned = false;
383
+ try {
384
+ await fs.promises.access(getAgentTombstonePath(sessionFile));
385
+ tombstoned = true;
386
+ } catch (error) {
387
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") continue;
388
+ }
389
+ if (!shouldContinue()) return;
85
390
  if (!registry.get(id)) {
391
+ const metadata = await readPersistedAgentMetadata(sessionFile);
392
+ if (!shouldContinue()) return;
86
393
  registry.register({
87
394
  id,
88
395
  displayName: id,
@@ -90,9 +397,27 @@ async function registerPersistedSubagentsFromDir(
90
397
  parentId: parentId ?? MAIN_AGENT_ID,
91
398
  session: null,
92
399
  sessionFile,
93
- status: "parked",
400
+ activity: metadata.activity,
401
+ createdAt: metadata.createdAt,
402
+ lastActivity: metadata.lastActivity,
403
+ history: metadata.history,
404
+ status: tombstoned ? "aborted" : "parked",
405
+ });
406
+ const ref = registry.get(id);
407
+ transcripts.push({
408
+ id,
409
+ sessionFile,
410
+ createdAt: ref?.createdAt,
411
+ lastActivity: ref?.lastActivity,
94
412
  });
95
413
  }
96
- await registerPersistedSubagentsFromDir(registry, path.join(dir, id), id, vibeOwnedIds);
414
+ await registerPersistedSubagentsFromDir(
415
+ registry,
416
+ path.join(dir, id),
417
+ id,
418
+ vibeOwnedIds,
419
+ transcripts,
420
+ shouldContinue,
421
+ );
97
422
  }
98
423
  }
package/src/sdk.ts CHANGED
@@ -123,18 +123,12 @@ import lateDiagnosticTemplate from "./prompts/tools/lsp-late-diagnostic.md" with
123
123
  import { AgentLifecycleManager } from "./registry/agent-lifecycle";
124
124
  import { type AgentRef, AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
125
125
  import {
126
- builtinCredentialSecretEntries,
127
- collectEnvSecrets,
126
+ buildSecretObfuscator,
128
127
  deobfuscateSessionContext,
129
128
  deobfuscateToolArguments,
130
- getExistingSecretPlaceholderKey,
131
- getSecretPlaceholderKey,
132
- getSecretPlaceholderKeySync,
133
- loadSecrets,
134
129
  obfuscateMessages,
135
130
  obfuscateProviderContext,
136
- SecretObfuscator,
137
- secretEntriesNeedPlaceholderKey,
131
+ type SecretObfuscator,
138
132
  } from "./secrets";
139
133
  import { AgentSession, type InitialRetryFallbackState, type PlanYolo, type Prewalk } from "./session/agent-session";
140
134
  import { discoverAuthStorage as discoverAuthStorageFromConfig } from "./session/auth-broker-config";
@@ -1039,7 +1033,7 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
1039
1033
  success: event.success,
1040
1034
  attempt: event.attempt,
1041
1035
  finalError: event.finalError,
1042
- recoveredErrors: event.recoveredErrors,
1036
+ retryErrors: event.retryErrors,
1043
1037
  },
1044
1038
  ctx,
1045
1039
  ),
@@ -1377,46 +1371,9 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
1377
1371
 
1378
1372
  // Load and create secret obfuscator early so resumed session state and prompt warnings
1379
1373
  // reflect actual loaded secrets, not just the setting toggle.
1380
- let obfuscator: SecretObfuscator | undefined;
1381
- if (settings.get("secrets.enabled")) {
1382
- const fileEntries = await logger.time("loadSecrets", loadSecrets, cwd, agentDir);
1383
- const envEntries = collectEnvSecrets();
1384
- // Built-in credential-pattern entries come last so user-configured entries
1385
- // (plain literals, custom regexes) take precedence in the scan order.
1386
- const allEntries = [...envEntries, ...fileEntries, ...builtinCredentialSecretEntries()];
1387
- // Only CONFIGURED entries force startup key creation: a configured
1388
- // obfuscate-mode secret — or a default (no custom `replacement`)
1389
- // replace-mode regex whose key-derived idempotent fallback marker needs a
1390
- // stable key across restarts (see `secretEntryNeedsPlaceholderKey`) —
1391
- // mints placeholders as soon as the obfuscator is built. The built-in
1392
- // credential-pattern entry matches dynamically, so it resolves the
1393
- // persisted key lazily on first match instead of creating the key file
1394
- // for every secrets.enabled session; a session whose content never
1395
- // contains a credential-shaped token must not require the key, otherwise a
1396
- // headless run on an unwritable default config root pays for a feature it
1397
- // does not use.
1398
- const needsPlaceholderKey = secretEntriesNeedPlaceholderKey([...envEntries, ...fileEntries]);
1399
- const explicitAgentDir = options.agentDir;
1400
- const placeholderKey = needsPlaceholderKey
1401
- ? await getSecretPlaceholderKey(explicitAgentDir)
1402
- : await getExistingSecretPlaceholderKey(explicitAgentDir);
1403
- if (allEntries.length > 0) {
1404
- obfuscator = new SecretObfuscator(
1405
- allEntries,
1406
- placeholderKey ?? (() => getSecretPlaceholderKeySync(explicitAgentDir)),
1407
- );
1408
- }
1409
- if (obfuscator?.hasSecrets() !== true && placeholderKey !== undefined) {
1410
- // No configured entry produced an active secret (e.g. only ignored short
1411
- // plain entries, or no entries at all), but a persisted key exists. Build a
1412
- // redaction-only obfuscator so a tool read of the key file does not ship the
1413
- // reusable HMAC key to the provider.
1414
- obfuscator = new SecretObfuscator(
1415
- [{ type: "plain", mode: "replace", content: placeholderKey }],
1416
- placeholderKey,
1417
- );
1418
- }
1419
- }
1374
+ const obfuscator: SecretObfuscator | undefined = settings.get("secrets.enabled")
1375
+ ? await buildSecretObfuscator(cwd, agentDir, options.agentDir)
1376
+ : undefined;
1420
1377
  const secretsEnabled = obfuscator?.hasSecrets() === true;
1421
1378
 
1422
1379
  // An abnormal process exit after a non-terminal message tail is durable
@@ -4,7 +4,13 @@ import * as path from "node:path";
4
4
  import { SENSITIVE_TOKEN_RE } from "@oh-my-pi/pi-ai/providers/transform-messages";
5
5
  import { getSecretPlaceholderKeyPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
6
6
  import { YAML } from "bun";
7
- import { regexHasUnresolvableShortMatchFallback, type SecretEntry, sanitizeSecretFriendlyName } from "./obfuscator";
7
+ import {
8
+ regexHasUnresolvableShortMatchFallback,
9
+ type SecretEntry,
10
+ SecretObfuscator,
11
+ sanitizeSecretFriendlyName,
12
+ secretEntriesNeedPlaceholderKey,
13
+ } from "./obfuscator";
8
14
  import { compileSecretRegex } from "./regex";
9
15
 
10
16
  const PLACEHOLDER_KEY_RE = /^[A-Za-z0-9_-]{43}$/;
@@ -223,6 +229,51 @@ export function builtinCredentialSecretEntries(): SecretEntry[] {
223
229
  ];
224
230
  }
225
231
 
232
+ /**
233
+ * Build the session secret obfuscator from every configured source: secrets.yml
234
+ * (project + global), secret-shaped environment variables, and the built-in
235
+ * credential patterns. Callers gate on `secrets.enabled`.
236
+ *
237
+ * Only CONFIGURED entries force startup key creation: a configured
238
+ * obfuscate-mode secret — or a default (no custom `replacement`) replace-mode
239
+ * regex whose key-derived idempotent fallback marker needs a stable key across
240
+ * restarts (see `secretEntryNeedsPlaceholderKey`) — mints placeholders as soon
241
+ * as the obfuscator is built. The built-in credential-pattern entry matches
242
+ * dynamically, so it resolves the persisted key lazily on first match instead
243
+ * of creating the key file for every secrets-enabled session.
244
+ *
245
+ * When no configured entry produced an active secret but a persisted key
246
+ * exists, returns a redaction-only obfuscator so a tool read of the key file
247
+ * does not ship the reusable HMAC key to the provider. Returns undefined when
248
+ * there is nothing to protect.
249
+ *
250
+ * `keyDir` is the explicit agent dir override for the placeholder-key file
251
+ * (default XDG/agent location when omitted).
252
+ */
253
+ export async function buildSecretObfuscator(
254
+ cwd: string,
255
+ agentDir: string,
256
+ keyDir?: string,
257
+ ): Promise<SecretObfuscator | undefined> {
258
+ const fileEntries = await logger.time("loadSecrets", loadSecrets, cwd, agentDir);
259
+ const envEntries = collectEnvSecrets();
260
+ // Built-in credential-pattern entries come last so user-configured entries
261
+ // (plain literals, custom regexes) take precedence in the scan order.
262
+ const allEntries = [...envEntries, ...fileEntries, ...builtinCredentialSecretEntries()];
263
+ const needsPlaceholderKey = secretEntriesNeedPlaceholderKey([...envEntries, ...fileEntries]);
264
+ const placeholderKey = needsPlaceholderKey
265
+ ? await getSecretPlaceholderKey(keyDir)
266
+ : await getExistingSecretPlaceholderKey(keyDir);
267
+ let obfuscator: SecretObfuscator | undefined;
268
+ if (allEntries.length > 0) {
269
+ obfuscator = new SecretObfuscator(allEntries, placeholderKey ?? (() => getSecretPlaceholderKeySync(keyDir)));
270
+ }
271
+ if (obfuscator?.hasSecrets() !== true && placeholderKey !== undefined) {
272
+ obfuscator = new SecretObfuscator([{ type: "plain", mode: "replace", content: placeholderKey }], placeholderKey);
273
+ }
274
+ return obfuscator;
275
+ }
276
+
226
277
  async function loadSecretsFile(filePath: string): Promise<SecretEntry[]> {
227
278
  try {
228
279
  const text = await Bun.file(filePath).text();
@@ -2,7 +2,7 @@ import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
3
3
  import type { Effort } from "@oh-my-pi/pi-ai";
4
4
  import type { Rule } from "../capability/rule";
5
- import type { RecoveredRetryError } from "../extensibility/shared-events";
5
+ import type { RetryErrorUpdate } from "../extensibility/shared-events";
6
6
  import type { Goal, GoalModeState } from "../goals/state";
7
7
  import type { ConfiguredThinkingLevel } from "../thinking";
8
8
  import type { TodoItem } from "../tools/todo";
@@ -43,7 +43,7 @@ export type AgentSessionEvent =
43
43
  success: boolean;
44
44
  attempt: number;
45
45
  finalError?: string;
46
- recoveredErrors?: RecoveredRetryError[];
46
+ retryErrors?: RetryErrorUpdate[];
47
47
  }
48
48
  | { type: "retry_fallback_applied"; from: string; to: string; role: string }
49
49
  | { type: "retry_fallback_succeeded"; model: string; role: string }