@oh-my-pi/pi-coding-agent 17.2.8 → 17.2.10

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 (246) hide show
  1. package/CHANGELOG.md +94 -1
  2. package/dist/{CHANGELOG-0b0w17se.md → CHANGELOG-rhwzpexa.md} +94 -1
  3. package/dist/cli.js +13216 -15614
  4. package/dist/types/async/job-manager.d.ts +12 -0
  5. package/dist/types/cli/args.d.ts +1 -0
  6. package/dist/types/config/model-resolver.d.ts +13 -0
  7. package/dist/types/discovery/agents.d.ts +11 -0
  8. package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
  9. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  10. package/dist/types/extensibility/extensions/loader.d.ts +2 -2
  11. package/dist/types/extensibility/extensions/types.d.ts +34 -8
  12. package/dist/types/extensibility/hooks/types.d.ts +5 -5
  13. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
  14. package/dist/types/hindsight/state.d.ts +0 -1
  15. package/dist/types/index.d.ts +2 -2
  16. package/dist/types/lsp/utils.d.ts +8 -0
  17. package/dist/types/main.d.ts +1 -1
  18. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  19. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  20. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
  21. package/dist/types/modes/acp/acp-mode.d.ts +1 -1
  22. package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
  23. package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
  24. package/dist/types/modes/components/agent-hub.d.ts +12 -5
  25. package/dist/types/modes/components/model-browser.d.ts +9 -1
  26. package/dist/types/modes/interactive-mode.d.ts +8 -1
  27. package/dist/types/modes/session-observer-registry.d.ts +2 -0
  28. package/dist/types/registry/agent-lifecycle.d.ts +1 -1
  29. package/dist/types/registry/agent-registry.d.ts +47 -0
  30. package/dist/types/registry/persisted-agents.d.ts +3 -1
  31. package/dist/types/session/agent-session-types.d.ts +7 -0
  32. package/dist/types/session/agent-session.d.ts +8 -4
  33. package/dist/types/session/session-entries.d.ts +10 -0
  34. package/dist/types/session/session-loader.d.ts +11 -1
  35. package/dist/types/session/session-manager.d.ts +9 -1
  36. package/dist/types/session/session-tools.d.ts +10 -1
  37. package/dist/types/session/turn-recovery.d.ts +6 -2
  38. package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
  39. package/dist/types/slash-commands/available-commands.d.ts +1 -1
  40. package/dist/types/task/executor.d.ts +10 -0
  41. package/dist/types/task/index.d.ts +2 -3
  42. package/dist/types/task/read-only-policy.d.ts +3 -0
  43. package/dist/types/task/structured-subagent.d.ts +2 -0
  44. package/dist/types/task/types.d.ts +4 -0
  45. package/dist/types/tools/bash.d.ts +3 -3
  46. package/dist/types/tools/browser/launch.d.ts +3 -0
  47. package/dist/types/tools/browser/tab-worker.d.ts +2 -2
  48. package/dist/types/tools/path-utils.d.ts +8 -0
  49. package/dist/types/tools/read.d.ts +3 -5
  50. package/dist/types/tools/run-scope.d.ts +27 -1
  51. package/dist/types/tools/shell-tokenize.d.ts +21 -3
  52. package/dist/types/tools/terminal-output.d.ts +1 -1
  53. package/dist/types/utils/changelog.d.ts +3 -2
  54. package/dist/types/utils/late-cleanup.d.ts +2 -0
  55. package/dist/types/utils/turndown.d.ts +1 -1
  56. package/dist/types/vibe/runtime.d.ts +4 -3
  57. package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
  58. package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
  59. package/examples/custom-tools/README.md +1 -1
  60. package/examples/extensions/README.md +1 -1
  61. package/examples/extensions/api-demo.ts +5 -6
  62. package/examples/extensions/chalk-logger.ts +1 -1
  63. package/examples/extensions/hello.ts +2 -2
  64. package/examples/extensions/reload-runtime.ts +1 -1
  65. package/examples/extensions/tools.ts +2 -2
  66. package/examples/extensions/with-deps/index.ts +1 -1
  67. package/examples/sdk/06-extensions.ts +1 -1
  68. package/package.json +15 -31
  69. package/scripts/bundle-dist.ts +1 -11
  70. package/scripts/omp +11 -2
  71. package/src/advisor/runtime.ts +54 -0
  72. package/src/async/job-manager.ts +26 -0
  73. package/src/cli/agents-cli.ts +1 -1
  74. package/src/cli/args.ts +24 -2
  75. package/src/cli/auth-broker-cli.ts +1 -1
  76. package/src/cli/auth-gateway-cli.ts +1 -1
  77. package/src/cli/bench-cli.ts +1 -1
  78. package/src/cli/claude-trace-cli.ts +1 -1
  79. package/src/cli/config-cli.ts +1 -1
  80. package/src/cli/dry-balance-cli.ts +1 -1
  81. package/src/cli/file-processor.ts +1 -1
  82. package/src/cli/flag-tables.ts +4 -0
  83. package/src/cli/grep-cli.ts +1 -1
  84. package/src/cli/grievances-cli.ts +1 -1
  85. package/src/cli/help-extra.ts +1 -1
  86. package/src/cli/models-cli.ts +1 -1
  87. package/src/cli/plugin-cli.ts +1 -1
  88. package/src/cli/read-cli.ts +1 -1
  89. package/src/cli/setup-cli.ts +1 -1
  90. package/src/cli/shell-cli.ts +1 -1
  91. package/src/cli/ssh-cli.ts +1 -1
  92. package/src/cli/stats-cli.ts +4 -4
  93. package/src/cli/tiny-models-cli.ts +1 -1
  94. package/src/cli/ttsr-cli.ts +1 -1
  95. package/src/cli/update-cli.ts +3 -57
  96. package/src/cli/usage-cli.ts +1 -1
  97. package/src/cli/web-search-cli.ts +1 -1
  98. package/src/cli/worktree-cli.ts +1 -1
  99. package/src/commands/say.ts +1 -1
  100. package/src/commands/token.ts +1 -1
  101. package/src/commit/agentic/agent.ts +1 -1
  102. package/src/commit/cli.ts +1 -1
  103. package/src/config/config-file.ts +2 -2
  104. package/src/config/keybindings.ts +3 -3
  105. package/src/config/model-discovery.ts +36 -2
  106. package/src/config/model-registry.ts +4 -1
  107. package/src/config/model-resolver.ts +54 -7
  108. package/src/discovery/agents.ts +73 -3
  109. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
  110. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
  111. package/src/discovery/claude.ts +8 -5
  112. package/src/discovery/cursor.ts +8 -5
  113. package/src/discovery/gemini.ts +11 -8
  114. package/src/discovery/vscode.ts +1 -0
  115. package/src/discovery/windsurf.ts +4 -2
  116. package/src/edit/hashline/filesystem.ts +7 -7
  117. package/src/eval/agent-bridge.ts +1 -3
  118. package/src/eval/jl/prelude.jl +4 -4
  119. package/src/eval/js/shared/prelude.txt +2 -2
  120. package/src/eval/py/prelude.py +0 -3
  121. package/src/eval/py/runner.py +38 -1
  122. package/src/eval/rb/prelude.rb +1 -2
  123. package/src/extensibility/custom-commands/loader.ts +4 -4
  124. package/src/extensibility/custom-commands/types.ts +5 -5
  125. package/src/extensibility/custom-tools/loader.ts +4 -4
  126. package/src/extensibility/custom-tools/types.ts +1 -1
  127. package/src/extensibility/extensions/loader.ts +7 -6
  128. package/src/extensibility/extensions/types.ts +39 -8
  129. package/src/extensibility/hooks/loader.ts +4 -5
  130. package/src/extensibility/hooks/types.ts +5 -5
  131. package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
  132. package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
  133. package/src/hindsight/state.ts +2 -22
  134. package/src/index.ts +2 -2
  135. package/src/internal-urls/memory-protocol.ts +33 -0
  136. package/src/launch/terminal-output.ts +1 -1
  137. package/src/lsp/client.ts +2 -2
  138. package/src/lsp/utils.ts +29 -0
  139. package/src/main.ts +85 -21
  140. package/src/markit/converters/docx.ts +1 -1
  141. package/src/markit/converters/epub.ts +1 -1
  142. package/src/markit/converters/pptx.ts +1 -1
  143. package/src/markit/converters/xlsx.ts +1 -1
  144. package/src/modes/acp/acp-agent.ts +7 -6
  145. package/src/modes/acp/acp-client-bridge.ts +1 -1
  146. package/src/modes/acp/acp-event-mapper.ts +1 -1
  147. package/src/modes/acp/acp-mode.ts +1 -1
  148. package/src/modes/components/agent-hub-projection.ts +248 -0
  149. package/src/modes/components/agent-hub-renderer.ts +194 -0
  150. package/src/modes/components/agent-hub.ts +670 -192
  151. package/src/modes/components/agent-transcript-viewer.ts +1 -3
  152. package/src/modes/components/assistant-message.ts +1 -1
  153. package/src/modes/components/bordered-loader.ts +1 -1
  154. package/src/modes/components/custom-editor.ts +2 -2
  155. package/src/modes/components/extensions/inspector-panel.ts +11 -6
  156. package/src/modes/components/footer.ts +1 -3
  157. package/src/modes/components/hook-input.ts +1 -1
  158. package/src/modes/components/hook-selector.ts +2 -2
  159. package/src/modes/components/model-browser.ts +11 -3
  160. package/src/modes/components/model-hub.ts +4 -1
  161. package/src/modes/components/session-selector.ts +2 -2
  162. package/src/modes/components/status-line/segments.ts +4 -3
  163. package/src/modes/controllers/event-controller.ts +188 -7
  164. package/src/modes/controllers/extension-ui-controller.ts +4 -3
  165. package/src/modes/controllers/mcp-command-controller.ts +81 -20
  166. package/src/modes/controllers/selector-controller.ts +40 -49
  167. package/src/modes/controllers/streaming-reveal.ts +1 -1
  168. package/src/modes/interactive-mode.ts +13 -2
  169. package/src/modes/print-mode.ts +29 -12
  170. package/src/modes/rpc/rpc-mode.ts +2 -2
  171. package/src/modes/runtime-init.ts +1 -1
  172. package/src/modes/session-observer-registry.ts +5 -0
  173. package/src/modes/theme/theme.ts +2 -2
  174. package/src/prompts/tools/computer.md +1 -1
  175. package/src/registry/agent-lifecycle.ts +35 -10
  176. package/src/registry/agent-registry.ts +65 -2
  177. package/src/registry/persisted-agents.ts +341 -16
  178. package/src/sdk.ts +11 -5
  179. package/src/session/agent-session-types.ts +8 -0
  180. package/src/session/agent-session.ts +177 -188
  181. package/src/session/session-entries.ts +10 -0
  182. package/src/session/session-listing.ts +1 -1
  183. package/src/session/session-loader.ts +74 -8
  184. package/src/session/session-manager.ts +22 -2
  185. package/src/session/session-paths.ts +105 -110
  186. package/src/session/session-tools.ts +28 -1
  187. package/src/session/settings-stream-fn.ts +7 -3
  188. package/src/session/turn-recovery.ts +236 -18
  189. package/src/session/unexpected-stop-classifier.ts +12 -3
  190. package/src/slash-commands/acp-builtins.ts +1 -1
  191. package/src/slash-commands/available-commands.ts +1 -1
  192. package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
  193. package/src/task/agents.ts +1 -1
  194. package/src/task/executor.ts +173 -47
  195. package/src/task/index.ts +9 -28
  196. package/src/task/isolation-runner.ts +43 -12
  197. package/src/task/persisted-revive.ts +12 -5
  198. package/src/task/read-only-policy.ts +27 -0
  199. package/src/task/structured-subagent.ts +31 -6
  200. package/src/task/types.ts +4 -0
  201. package/src/tiny/worker.ts +1 -1
  202. package/src/tools/auto-generated-guard.ts +1 -1
  203. package/src/tools/bash-interactive.ts +4 -4
  204. package/src/tools/bash-interceptor.ts +8 -4
  205. package/src/tools/bash.ts +6 -6
  206. package/src/tools/browser/cmux/cmux-tab.ts +66 -18
  207. package/src/tools/browser/launch.ts +76 -19
  208. package/src/tools/browser/readable.ts +9 -9
  209. package/src/tools/browser/tab-supervisor.ts +12 -3
  210. package/src/tools/browser/tab-worker-entry.ts +1 -1
  211. package/src/tools/browser/tab-worker.ts +101 -12
  212. package/src/tools/fetch.ts +1 -1
  213. package/src/tools/glob.ts +4 -2
  214. package/src/tools/path-utils.ts +22 -5
  215. package/src/tools/read.ts +22 -5
  216. package/src/tools/run-scope.ts +290 -4
  217. package/src/tools/shell-tokenize.ts +38 -9
  218. package/src/tools/terminal-output.ts +1 -1
  219. package/src/tools/todo.ts +1 -1
  220. package/src/utils/changelog.ts +5 -4
  221. package/src/utils/clipboard.ts +10 -2
  222. package/src/utils/external-editor.ts +4 -2
  223. package/src/utils/jj.ts +1 -1
  224. package/src/utils/late-cleanup.ts +17 -0
  225. package/src/utils/turndown.ts +1 -2
  226. package/src/vibe/runtime.ts +202 -58
  227. package/src/web/scrapers/arxiv.ts +1 -1
  228. package/src/web/scrapers/go-pkg.ts +1 -1
  229. package/src/web/scrapers/hackage.ts +1 -12
  230. package/src/web/scrapers/iacr.ts +1 -1
  231. package/src/web/scrapers/readthedocs.ts +2 -1
  232. package/src/web/scrapers/twitter.ts +2 -2
  233. package/src/web/scrapers/types.ts +1 -1
  234. package/src/web/scrapers/wikipedia.ts +1 -1
  235. package/src/web/search/providers/browser-headers.ts +12 -39
  236. package/src/web/search/providers/codex.ts +3 -26
  237. package/src/web/search/providers/duckduckgo.ts +1 -1
  238. package/src/web/search/providers/ecosia.ts +2 -2
  239. package/src/web/search/providers/exa.ts +1 -1
  240. package/src/web/search/providers/firecrawl.ts +1 -1
  241. package/src/web/search/providers/google.ts +1 -1
  242. package/src/web/search/providers/mojeek.ts +2 -2
  243. package/src/web/search/providers/perplexity.ts +1 -1
  244. package/src/web/search/providers/public.ts +1 -1
  245. package/src/web/search/providers/startpage.ts +2 -2
  246. 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
@@ -2283,9 +2283,10 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
2283
2283
  }
2284
2284
  }
2285
2285
  const usageReservePolicy = settings.get("retry.usageReservePolicy");
2286
+ const modelFallbackEnabled = settings.get("retry.modelFallback");
2286
2287
  if (
2287
- (hasUsageFallbackCandidate || usageReservePolicy === "fail-closed") &&
2288
- settings.get("retry.modelFallback") &&
2288
+ ((modelFallbackEnabled && (hasUsageFallbackCandidate || usageFallbackTriggered)) ||
2289
+ usageReservePolicy === "fail-closed") &&
2289
2290
  settings.get("retry.usageAwareFallback")
2290
2291
  ) {
2291
2292
  let usageHealth: ModelUsageHealth | undefined;
@@ -2308,8 +2309,10 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
2308
2309
  `Usage depleted for ${primary.model.provider}/${primary.model.id}; reserve policy is fail-closed.`,
2309
2310
  );
2310
2311
  }
2311
- usageFallbackTriggered = true;
2312
- continue;
2312
+ if (modelFallbackEnabled) {
2313
+ usageFallbackTriggered = true;
2314
+ continue;
2315
+ }
2313
2316
  }
2314
2317
  if (usageHealth?.state === "reserve") {
2315
2318
  if (usageReservePolicy === "fail-closed") {
@@ -2317,7 +2320,10 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
2317
2320
  `Usage reserve reached for ${primary.model.provider}/${primary.model.id}; reserve policy is fail-closed.`,
2318
2321
  );
2319
2322
  }
2320
- if (usageReservePolicy === "auto" || (!options.hasUI && !options.deferUsageReserveConfirmation)) {
2323
+ if (
2324
+ modelFallbackEnabled &&
2325
+ (usageReservePolicy === "auto" || (!options.hasUI && !options.deferUsageReserveConfirmation))
2326
+ ) {
2321
2327
  usageFallbackTriggered = true;
2322
2328
  continue;
2323
2329
  }
@@ -92,6 +92,14 @@ export interface UsageFallbackConfirmation {
92
92
  remainingPercent: number | undefined;
93
93
  }
94
94
 
95
+ /**
96
+ * Confirms whether a reserve-triggered model fallback may proceed.
97
+ *
98
+ * Interactive callers use the confirmation details to present the pending
99
+ * route change; aborting `signal` cancels that pending confirmation.
100
+ */
101
+ export type UsageFallbackConfirmer = (confirmation: UsageFallbackConfirmation, signal: AbortSignal) => Promise<boolean>;
102
+
95
103
  /** Identifies a retry fallback chain already entered during startup model resolution. */
96
104
  export interface InitialRetryFallbackState {
97
105
  /** Role whose configured primary was unavailable. */