@oh-my-pi/pi-coding-agent 17.1.6 → 17.1.8

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 (130) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/{CHANGELOG-x9zt79k8.md → CHANGELOG-vt8ene9g.md} +58 -0
  3. package/dist/cli.js +4309 -7287
  4. package/dist/template-dys3vk5b.js +1671 -0
  5. package/dist/template-f8wx9vfn.css +1355 -0
  6. package/dist/template-qat058wr.html +55 -0
  7. package/dist/tool-views.generated-jdfmzwmn.js +35 -0
  8. package/dist/types/advisor/advise-tool.d.ts +0 -7
  9. package/dist/types/advisor/runtime.d.ts +0 -9
  10. package/dist/types/async/job-manager.d.ts +11 -0
  11. package/dist/types/cleanse/agent.d.ts +19 -0
  12. package/dist/types/cleanse/balance.d.ts +7 -0
  13. package/dist/types/cleanse/checkers.d.ts +13 -0
  14. package/dist/types/cleanse/index.d.ts +16 -0
  15. package/dist/types/cleanse/loop.d.ts +16 -0
  16. package/dist/types/cleanse/parsers.d.ts +13 -0
  17. package/dist/types/cleanse/progress.d.ts +14 -0
  18. package/dist/types/cleanse/types.d.ts +64 -0
  19. package/dist/types/commands/cleanse.d.ts +23 -0
  20. package/dist/types/config/settings-schema.d.ts +16 -1
  21. package/dist/types/cursor.d.ts +2 -1
  22. package/dist/types/export/html/index.d.ts +2 -0
  23. package/dist/types/extensibility/extensions/runner.d.ts +4 -0
  24. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
  25. package/dist/types/extensibility/shared-events.d.ts +18 -1
  26. package/dist/types/modes/components/custom-editor.d.ts +5 -0
  27. package/dist/types/modes/interactive-mode.d.ts +4 -3
  28. package/dist/types/session/agent-session-types.d.ts +5 -5
  29. package/dist/types/session/agent-session.d.ts +26 -1
  30. package/dist/types/session/async-job-delivery.d.ts +8 -0
  31. package/dist/types/session/model-controls.d.ts +4 -4
  32. package/dist/types/session/session-tools.d.ts +44 -6
  33. package/dist/types/session/streaming-output.d.ts +7 -2
  34. package/dist/types/session/turn-recovery.d.ts +1 -1
  35. package/dist/types/task/isolation-ownership.d.ts +34 -0
  36. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
  37. package/dist/types/tools/browser/tab-worker.d.ts +1 -4
  38. package/dist/types/tools/index.d.ts +8 -3
  39. package/dist/types/tools/output-meta.d.ts +5 -0
  40. package/dist/types/tools/read.d.ts +9 -1
  41. package/dist/types/tools/xdev.d.ts +53 -67
  42. package/dist/types/tui/output-block.d.ts +5 -5
  43. package/dist/types/utils/cpuprofile.d.ts +51 -0
  44. package/dist/types/utils/inspect-image-mode.d.ts +29 -0
  45. package/dist/types/utils/profile-tree.d.ts +47 -0
  46. package/dist/types/utils/sample-profile.d.ts +67 -0
  47. package/package.json +16 -12
  48. package/scripts/bundle-dist.ts +3 -1
  49. package/src/advisor/advise-tool.ts +0 -11
  50. package/src/advisor/runtime.ts +0 -12
  51. package/src/async/job-manager.ts +30 -7
  52. package/src/cleanse/agent.ts +226 -0
  53. package/src/cleanse/balance.ts +79 -0
  54. package/src/cleanse/checkers.ts +996 -0
  55. package/src/cleanse/index.ts +190 -0
  56. package/src/cleanse/loop.ts +51 -0
  57. package/src/cleanse/parsers.ts +726 -0
  58. package/src/cleanse/progress.ts +50 -0
  59. package/src/cleanse/prompts/assignment.md +47 -0
  60. package/src/cleanse/types.ts +72 -0
  61. package/src/cli/update-cli.ts +3 -0
  62. package/src/cli/usage-cli.ts +29 -4
  63. package/src/cli/worktree-cli.ts +28 -11
  64. package/src/cli-commands.ts +1 -0
  65. package/src/cli.ts +2 -1
  66. package/src/commands/cleanse.ts +45 -0
  67. package/src/config/settings-schema.ts +15 -1
  68. package/src/config/settings.ts +35 -0
  69. package/src/cursor.ts +4 -3
  70. package/src/discovery/builtin-rules/index.ts +2 -0
  71. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  72. package/src/discovery/claude-plugins.ts +144 -34
  73. package/src/export/html/index.ts +17 -10
  74. package/src/extensibility/extensions/runner.ts +26 -0
  75. package/src/extensibility/extensions/wrapper.ts +74 -42
  76. package/src/extensibility/hooks/tool-wrapper.ts +11 -4
  77. package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
  78. package/src/extensibility/shared-events.ts +18 -1
  79. package/src/launch/broker.ts +14 -4
  80. package/src/modes/acp/acp-agent.ts +12 -9
  81. package/src/modes/acp/acp-event-mapper.ts +38 -4
  82. package/src/modes/components/custom-editor.ts +39 -16
  83. package/src/modes/components/settings-selector.ts +9 -1
  84. package/src/modes/components/tips.txt +2 -1
  85. package/src/modes/components/tool-execution.ts +8 -7
  86. package/src/modes/controllers/extension-ui-controller.ts +4 -4
  87. package/src/modes/controllers/selector-controller.ts +7 -2
  88. package/src/modes/interactive-mode.ts +36 -47
  89. package/src/modes/prompt-action-autocomplete.ts +5 -3
  90. package/src/prompts/goals/guided-goal-interview.md +41 -6
  91. package/src/prompts/system/vibe-mode-active.md +4 -1
  92. package/src/prompts/tools/browser.md +1 -0
  93. package/src/prompts/tools/goal.md +1 -1
  94. package/src/sdk.ts +47 -50
  95. package/src/session/agent-session-types.ts +5 -5
  96. package/src/session/agent-session.ts +151 -11
  97. package/src/session/async-job-delivery.ts +8 -0
  98. package/src/session/model-controls.ts +9 -9
  99. package/src/session/session-advisors.ts +2 -7
  100. package/src/session/session-history-format.ts +31 -6
  101. package/src/session/session-listing.ts +66 -4
  102. package/src/session/session-tools.ts +158 -52
  103. package/src/session/streaming-output.ts +18 -6
  104. package/src/session/turn-recovery.ts +4 -4
  105. package/src/slash-commands/builtin-registry.ts +74 -2
  106. package/src/task/isolation-ownership.ts +106 -0
  107. package/src/task/worktree.ts +8 -0
  108. package/src/tools/ask.ts +3 -3
  109. package/src/tools/ast-edit.ts +9 -2
  110. package/src/tools/bash.ts +16 -9
  111. package/src/tools/browser/cmux/cmux-tab.ts +9 -14
  112. package/src/tools/browser/tab-worker.ts +12 -35
  113. package/src/tools/browser.ts +2 -2
  114. package/src/tools/gh-renderer.ts +3 -3
  115. package/src/tools/index.ts +46 -17
  116. package/src/tools/output-meta.ts +20 -0
  117. package/src/tools/read.ts +87 -13
  118. package/src/tools/write.ts +51 -7
  119. package/src/tools/xdev.ts +198 -210
  120. package/src/tui/code-cell.ts +4 -4
  121. package/src/tui/output-block.ts +25 -8
  122. package/src/utils/cpuprofile.ts +235 -0
  123. package/src/utils/inspect-image-mode.ts +39 -0
  124. package/src/utils/profile-tree.ts +111 -0
  125. package/src/utils/sample-profile.ts +437 -0
  126. package/src/utils/shell-snapshot-fn-env.sh +5 -2
  127. package/src/web/search/render.ts +2 -2
  128. package/dist/types/goals/guided-setup.d.ts +0 -30
  129. package/src/goals/guided-setup.ts +0 -171
  130. package/src/prompts/goals/guided-goal-system.md +0 -33
@@ -0,0 +1,437 @@
1
+ /**
2
+ * Parser and renderer for macOS `/usr/bin/sample` call-tree reports
3
+ * (conventionally saved as `*.sample.txt`).
4
+ *
5
+ * The raw report is a 10k+ line ASCII call tree with mangled symbols —
6
+ * expensive for an agent to digest. `renderSampleProfile` converts it into a
7
+ * compact bottleneck summary:
8
+ *
9
+ * - per-thread hot paths, pruned to frames with meaningful on-CPU samples,
10
+ * with pass-through chains collapsed and direct recursion flattened
11
+ * - blocked/idle threads reduced to a one-line classification
12
+ * - a process-wide "top functions by self samples" table
13
+ * - Rust v0 and legacy symbols demangled (best-effort, path extraction)
14
+ *
15
+ * Consumed by the read tool: `*.sample.txt` reads show the summary, `:raw`
16
+ * returns the original bytes.
17
+ */
18
+
19
+ import { formatPct, mergeInto, type ProfileNode, type RenderTreeContext, renderProfileNode } from "./profile-tree";
20
+
21
+ /** Matches paths the read tool should treat as macOS sample reports. */
22
+ export function isSampleProfilePath(filePath: string): boolean {
23
+ return /\.sample\.txt$/i.test(filePath);
24
+ }
25
+
26
+ /** Leaf symbols that mean "thread is parked in the kernel", not burning CPU. */
27
+ const WAIT_SYMBOLS: Record<string, true> = {
28
+ __accept: true,
29
+ __psynch_cvwait: true,
30
+ __psynch_mutexwait: true,
31
+ __psynch_rw_rdlock: true,
32
+ __psynch_rw_wrlock: true,
33
+ __recvfrom: true,
34
+ __select: true,
35
+ __semwait_signal: true,
36
+ __sigwait: true,
37
+ __ulock_wait: true,
38
+ __ulock_wait2: true,
39
+ __wait4: true,
40
+ __workq_kernreturn: true,
41
+ kevent: true,
42
+ kevent64: true,
43
+ kevent_qos: true,
44
+ mach_msg2_trap: true,
45
+ mach_msg_trap: true,
46
+ poll: true,
47
+ semaphore_timedwait_trap: true,
48
+ semaphore_wait_trap: true,
49
+ start_wqthread: true,
50
+ swtch_pri: true,
51
+ thread_suspend: true,
52
+ usleep: true,
53
+ };
54
+
55
+ /** One frame in the sampled call tree. Counts are sample hits (subtree total). */
56
+ export interface SampleFrame {
57
+ count: number;
58
+ symbol: string;
59
+ module?: string;
60
+ children: SampleFrame[];
61
+ }
62
+
63
+ /** One sampled thread: `Thread_<id>` root plus its call tree. */
64
+ export interface SampleThread {
65
+ id: string;
66
+ name?: string;
67
+ total: number;
68
+ roots: SampleFrame[];
69
+ }
70
+
71
+ /** Metadata from the report preamble (everything before `Call graph:`). */
72
+ export interface SampleProfileHeader {
73
+ process: string;
74
+ pid: number;
75
+ intervalMs: number;
76
+ path?: string;
77
+ codeType?: string;
78
+ osVersion?: string;
79
+ footprint?: string;
80
+ footprintPeak?: string;
81
+ }
82
+
83
+ /** Parsed macOS sample report. */
84
+ export interface SampleProfile {
85
+ header: SampleProfileHeader;
86
+ threads: SampleThread[];
87
+ }
88
+
89
+ const ANALYSIS_RE = /^Analysis of sampling (.+?) \(pid (\d+)\) every (\d+) milliseconds?/;
90
+ const THREAD_RE = /^Thread_([^\s:]+):?\s*(.*)$/;
91
+
92
+ /**
93
+ * Parse a macOS `sample` report. Returns null when the text does not look
94
+ * like sample output (missing analysis preamble or `Call graph:` section).
95
+ */
96
+ export function parseSampleProfile(text: string): SampleProfile | null {
97
+ const lines = text.split("\n");
98
+ const analysis = ANALYSIS_RE.exec(lines[0] ?? "");
99
+ if (!analysis) return null;
100
+ const callGraphIx = lines.indexOf("Call graph:");
101
+ if (callGraphIx === -1) return null;
102
+
103
+ const header: SampleProfileHeader = {
104
+ process: analysis[1],
105
+ pid: Number(analysis[2]),
106
+ intervalMs: Number(analysis[3]),
107
+ };
108
+ for (const line of lines.slice(1, callGraphIx)) {
109
+ const kv = /^([A-Za-z/ ()]+?):\s+(.*)$/.exec(line);
110
+ if (!kv) continue;
111
+ const value = kv[2].trim();
112
+ switch (kv[1]) {
113
+ case "Path":
114
+ header.path = value;
115
+ break;
116
+ case "Code Type":
117
+ header.codeType = value;
118
+ break;
119
+ case "OS Version":
120
+ header.osVersion = value;
121
+ break;
122
+ case "Physical footprint":
123
+ header.footprint = value;
124
+ break;
125
+ case "Physical footprint (peak)":
126
+ header.footprintPeak = value;
127
+ break;
128
+ }
129
+ }
130
+
131
+ const threads: SampleThread[] = [];
132
+ let thread: SampleThread | undefined;
133
+ // Stack of (depth, frame) for the current thread; depth 1 = thread root frame.
134
+ const stack: Array<{ depth: number; frame: SampleFrame }> = [];
135
+
136
+ for (let ix = callGraphIx + 1; ix < lines.length; ix++) {
137
+ const line = lines[ix];
138
+ if (/^(Total number in stack|Sort by top of stack|Binary Images:)/.test(line)) break;
139
+ if (!line.startsWith(" ")) continue;
140
+ const body = line.slice(4);
141
+ // Decorators are one char + one space per tree level (`+ ! : | `), so the
142
+ // sample count starts at an even offset; scan pairs until the first digit.
143
+ let p = 0;
144
+ while (p < body.length && (body[p] < "0" || body[p] > "9")) p += 2;
145
+ if (p >= body.length) continue;
146
+ const depth = p / 2;
147
+ const rest = /^(\d+)\s+(.*)$/.exec(body.slice(p));
148
+ if (!rest) continue;
149
+ const count = Number(rest[1]);
150
+ const frameText = rest[2];
151
+
152
+ if (depth === 0) {
153
+ const tm = THREAD_RE.exec(frameText);
154
+ if (!tm) continue;
155
+ const name = tm[2].trim().replace(/\s+/g, " ");
156
+ thread = { id: tm[1], name: name || undefined, total: count, roots: [] };
157
+ threads.push(thread);
158
+ stack.length = 0;
159
+ continue;
160
+ }
161
+ if (!thread) continue;
162
+ const frame: SampleFrame = { count, ...parseFrameText(frameText), children: [] };
163
+ while (stack.length > 0 && stack[stack.length - 1].depth >= depth) stack.pop();
164
+ const siblings = stack.length > 0 ? stack[stack.length - 1].frame.children : thread.roots;
165
+ siblings.push(frame);
166
+ stack.push({ depth, frame });
167
+ }
168
+
169
+ if (threads.length === 0) return null;
170
+ return { header, threads };
171
+ }
172
+
173
+ /** Split `symbol (in module) + offsets [addrs]` into symbol + module. */
174
+ function parseFrameText(text: string): { symbol: string; module?: string } {
175
+ let s = text;
176
+ const addrIx = s.lastIndexOf(" [");
177
+ if (addrIx !== -1 && s.endsWith("]")) s = s.slice(0, addrIx);
178
+ s = s.replace(/ \+ [0-9][0-9,.]*$/, "");
179
+ const modIx = s.lastIndexOf(" (in ");
180
+ if (modIx !== -1 && s.endsWith(")")) {
181
+ return { symbol: s.slice(0, modIx).trim(), module: s.slice(modIx + 6, -1) };
182
+ }
183
+ return { symbol: s.trim() };
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Demangling
188
+ // ---------------------------------------------------------------------------
189
+
190
+ const LEGACY_ESCAPES: ReadonlyArray<[string, string]> = [
191
+ ["$LT$", "<"],
192
+ ["$GT$", ">"],
193
+ ["$RF$", "&"],
194
+ ["$BP$", "*"],
195
+ ["$C$", ","],
196
+ ["$u20$", " "],
197
+ ["$u27$", "'"],
198
+ ["$u7b$", "{"],
199
+ ["$u7d$", "}"],
200
+ ["..", "::"],
201
+ ];
202
+
203
+ const IDENT_RE = /^[A-Za-z0-9_.$]+$/;
204
+ const LEGACY_HASH_RE = /^h[0-9a-f]{16}$/;
205
+
206
+ /**
207
+ * Best-effort demangle of Rust symbols (v0 `_R…` and legacy `_ZN…E`).
208
+ * For v0 this is a path extractor, not a full demangler: identifiers are
209
+ * pulled out in order and joined with `::`, so generic arguments appear as
210
+ * extra path segments. Non-Rust symbols pass through unchanged.
211
+ */
212
+ export function demangleSymbol(raw: string): string {
213
+ if (raw.startsWith("_R")) return demangleV0(raw) ?? raw;
214
+ const legacy = /^_?_ZN(.*)$/.exec(raw);
215
+ if (legacy) return demangleLegacy(legacy[1]) ?? raw;
216
+ return raw;
217
+ }
218
+
219
+ function demangleV0(raw: string): string | null {
220
+ const s = raw.slice(2);
221
+ const parts: string[] = [];
222
+ let i = 0;
223
+ while (i < s.length) {
224
+ const ch = s[i];
225
+ if (ch >= "1" && ch <= "9") {
226
+ let j = i;
227
+ while (j < s.length && s[j] >= "0" && s[j] <= "9") j++;
228
+ const len = Number(s.slice(i, j));
229
+ // v0 inserts a `_` separator when the identifier starts with a digit or `_`.
230
+ let k = j;
231
+ if (s[k] === "_") k++;
232
+ const ident = s.slice(k, k + len);
233
+ if (ident.length === len && IDENT_RE.test(ident)) {
234
+ parts.push(ident);
235
+ i = k + len;
236
+ } else {
237
+ i = j;
238
+ }
239
+ } else if (ch === "s" || ch === "B") {
240
+ // Disambiguator (`s<base62>_`) or backref (`B<base62>_`): skip.
241
+ const m = /^[sB][0-9a-zA-Z]*_/.exec(s.slice(i));
242
+ i += m ? m[0].length : 1;
243
+ } else {
244
+ i++;
245
+ }
246
+ }
247
+ return parts.length > 0 ? parts.join("::") : null;
248
+ }
249
+
250
+ function demangleLegacy(s: string): string | null {
251
+ const parts: string[] = [];
252
+ let i = 0;
253
+ while (i < s.length && s[i] >= "0" && s[i] <= "9") {
254
+ let j = i;
255
+ while (j < s.length && s[j] >= "0" && s[j] <= "9") j++;
256
+ const len = Number(s.slice(i, j));
257
+ const ident = s.slice(j, j + len);
258
+ if (ident.length !== len) break;
259
+ if (!LEGACY_HASH_RE.test(ident)) parts.push(unescapeLegacy(ident));
260
+ i = j + len;
261
+ }
262
+ return parts.length > 0 ? parts.join("::") : null;
263
+ }
264
+
265
+ function unescapeLegacy(ident: string): string {
266
+ let out = ident;
267
+ for (const [from, to] of LEGACY_ESCAPES) out = out.replaceAll(from, to);
268
+ return out;
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Rendering
273
+ // ---------------------------------------------------------------------------
274
+
275
+ /** Fraction of a thread's on-CPU samples a subtree needs to stay visible. */
276
+ const PRUNE_FRACTION = 0.02;
277
+ /** Threads with fewer on-CPU samples than this share of their total are "idle". */
278
+ const IDLE_FRACTION = 0.002;
279
+ const IDLE_MIN_SAMPLES = 10;
280
+ const TOP_FUNCTIONS = 20;
281
+
282
+ function selfOf(frame: SampleFrame): number {
283
+ let children = 0;
284
+ for (const child of frame.children) children += child.count;
285
+ return Math.max(0, frame.count - children);
286
+ }
287
+
288
+ /**
289
+ * Build a display node for a frame subtree: demangled label, on-CPU value
290
+ * (self samples of non-wait frames), same-symbol siblings merged (`sample`
291
+ * splits them by call-site offset, which only fragments hot totals).
292
+ */
293
+ function buildProfileNode(frame: SampleFrame, demangleCache: Map<string, string>, mainModule: string): ProfileNode {
294
+ let symbol = demangleCache.get(frame.symbol);
295
+ if (symbol === undefined) {
296
+ symbol = demangleSymbol(frame.symbol);
297
+ demangleCache.set(frame.symbol, symbol);
298
+ }
299
+ const children: ProfileNode[] = [];
300
+ for (const rawChild of frame.children) {
301
+ const child = buildProfileNode(rawChild, demangleCache, mainModule);
302
+ const existing = children.find(c => c.key === child.key);
303
+ if (existing) mergeInto(existing, child);
304
+ else children.push(child);
305
+ }
306
+ let cpu = WAIT_SYMBOLS[frame.symbol] ? 0 : selfOf(frame);
307
+ for (const child of children) cpu += child.value;
308
+ const label = frame.module && frame.module !== mainModule ? `${symbol} (${frame.module})` : symbol;
309
+ return { key: symbol, label, value: cpu, recursion: 0, children };
310
+ }
311
+
312
+ /** Aggregate on-CPU self samples per demangled symbol across all threads. */
313
+ function aggregateSelf(
314
+ profile: SampleProfile,
315
+ demangleCache: Map<string, string>,
316
+ ): Map<string, { cpu: number; module?: string }> {
317
+ const totals = new Map<string, { cpu: number; module?: string }>();
318
+ const visit = (frame: SampleFrame): void => {
319
+ if (!WAIT_SYMBOLS[frame.symbol]) {
320
+ const self = selfOf(frame);
321
+ if (self > 0) {
322
+ let symbol = demangleCache.get(frame.symbol);
323
+ if (symbol === undefined) {
324
+ symbol = demangleSymbol(frame.symbol);
325
+ demangleCache.set(frame.symbol, symbol);
326
+ }
327
+ const entry = totals.get(symbol);
328
+ if (entry) entry.cpu += self;
329
+ else totals.set(symbol, { cpu: self, module: frame.module });
330
+ }
331
+ }
332
+ for (const child of frame.children) visit(child);
333
+ };
334
+ for (const thread of profile.threads) for (const root of thread.roots) visit(root);
335
+ return totals;
336
+ }
337
+
338
+ /** Dominant wait leaf of an idle thread ("what is it blocked on"). */
339
+ function dominantWait(thread: SampleThread): string | undefined {
340
+ let best: { symbol: string; count: number } | undefined;
341
+ const visit = (frame: SampleFrame): void => {
342
+ if (frame.children.length === 0 && WAIT_SYMBOLS[frame.symbol]) {
343
+ if (!best || frame.count > best.count) best = { symbol: frame.symbol, count: frame.count };
344
+ }
345
+ for (const child of frame.children) visit(child);
346
+ };
347
+ for (const root of thread.roots) visit(root);
348
+ return best?.symbol;
349
+ }
350
+
351
+ /**
352
+ * Render a macOS sample report as an agent-friendly bottleneck summary.
353
+ * Returns null when `text` is not sample output (caller falls back to the
354
+ * plain-text path).
355
+ */
356
+ export function renderSampleProfile(text: string): string | null {
357
+ const profile = parseSampleProfile(text);
358
+ if (!profile) return null;
359
+ const { header, threads } = profile;
360
+ const demangleCache = new Map<string, string>();
361
+
362
+ const annotated = threads.map(thread => {
363
+ const roots = thread.roots.map(root => buildProfileNode(root, demangleCache, header.process));
364
+ let cpu = 0;
365
+ for (const root of roots) cpu += root.value;
366
+ return { thread, roots, cpu };
367
+ });
368
+ const processCpu = annotated.reduce((sum, t) => sum + t.cpu, 0);
369
+ const maxSamples = threads.reduce((max, t) => Math.max(max, t.total), 0);
370
+
371
+ const out: string[] = [];
372
+ out.push(`macOS sample profile: ${header.process} (pid ${header.pid}), sampled every ${header.intervalMs} ms`);
373
+ const meta: string[] = [];
374
+ if (header.path) meta.push(header.path);
375
+ if (header.codeType) meta.push(header.codeType);
376
+ if (header.osVersion) meta.push(`macOS ${header.osVersion.replace(/^macOS\s+/, "")}`);
377
+ if (meta.length > 0) out.push(meta.join(" | "));
378
+ const durationSec = (maxSamples * header.intervalMs) / 1000;
379
+ let statLine = `Duration: ~${durationSec.toFixed(1)} s (${maxSamples} samples/thread)`;
380
+ if (header.footprint) {
381
+ statLine += ` | Footprint: ${header.footprint}${header.footprintPeak ? ` (peak ${header.footprintPeak})` : ""}`;
382
+ }
383
+ out.push(statLine);
384
+ out.push("");
385
+ out.push(
386
+ `Process total: ${processCpu} on-CPU samples across ${threads.length} threads. Counts and percentages below are on-CPU samples (blocked time excluded).`,
387
+ );
388
+
389
+ const active: typeof annotated = [];
390
+ const idle: typeof annotated = [];
391
+ for (const entry of annotated) {
392
+ const threshold = Math.max(IDLE_MIN_SAMPLES, entry.thread.total * IDLE_FRACTION);
393
+ (entry.cpu >= threshold ? active : idle).push(entry);
394
+ }
395
+ active.sort((a, b) => b.cpu - a.cpu);
396
+
397
+ for (const { thread, roots, cpu } of active) {
398
+ out.push("");
399
+ const title = thread.name ? `${thread.name} (Thread_${thread.id})` : `Thread_${thread.id}`;
400
+ out.push(`## ${title} — ${thread.total} samples, ${cpu} on-CPU (${formatPct(cpu, thread.total)})`);
401
+ const ctx: RenderTreeContext = {
402
+ out,
403
+ total: cpu,
404
+ minValue: Math.max(3, Math.round(cpu * PRUNE_FRACTION)),
405
+ formatValue: String,
406
+ valueWidth: 6,
407
+ };
408
+ const kept = roots.filter(root => root.value >= ctx.minValue).sort((a, b) => b.value - a.value);
409
+ for (const root of kept) renderProfileNode(root, 0, ctx);
410
+ if (kept.length === 0) out.push(` (no call path above ${ctx.minValue} on-CPU samples)`);
411
+ }
412
+
413
+ if (idle.length > 0) {
414
+ out.push("");
415
+ out.push(`## Idle / negligible threads (${idle.length})`);
416
+ for (const { thread, cpu } of idle) {
417
+ const title = thread.name ? `${thread.name} (Thread_${thread.id})` : `Thread_${thread.id}`;
418
+ const wait = dominantWait(thread);
419
+ const state = wait ? `blocked in ${wait} (${cpu} on-CPU)` : `${cpu}/${thread.total} samples on-CPU`;
420
+ out.push(`- ${title}: ${state}`);
421
+ }
422
+ }
423
+
424
+ const totals = [...aggregateSelf(profile, demangleCache).entries()].sort((a, b) => b[1].cpu - a[1].cpu);
425
+ if (totals.length > 0) {
426
+ out.push("");
427
+ out.push("## Top functions by self samples (process-wide, blocked time excluded)");
428
+ for (const [symbol, { cpu, module }] of totals.slice(0, TOP_FUNCTIONS)) {
429
+ const suffix = module && module !== header.process ? ` (${module})` : "";
430
+ out.push(`${String(cpu).padStart(6)} ${formatPct(cpu, processCpu).padStart(6)} ${symbol}${suffix}`);
431
+ }
432
+ }
433
+
434
+ out.push("");
435
+ out.push("[Summarized view of a macOS `sample` call-tree report. Use ':raw' to read the original file.]");
436
+ return out.join("\n");
437
+ }
@@ -1,3 +1,5 @@
1
+ #!/bin/sh
2
+
1
3
  # Helpers inlined into `generateSnapshotScript` (shell-snapshot.ts).
2
4
  #
3
5
  # Activation idioms like `mise activate` install a shell function whose body
@@ -22,8 +24,8 @@ __omp_sq_quote() {
22
24
  __omp_qout=
23
25
  __omp_sq=\'
24
26
  while case "$__omp_qbuf" in *$__omp_sq*) true ;; *) false ;; esac; do
25
- __omp_qout=$__omp_qout${__omp_qbuf%%$__omp_sq*}"'\\''"
26
- __omp_qbuf=${__omp_qbuf#*$__omp_sq}
27
+ __omp_qout=$__omp_qout${__omp_qbuf%%"$__omp_sq"*}"'\\''"
28
+ __omp_qbuf=${__omp_qbuf#*"$__omp_sq"}
27
29
  done
28
30
  __omp_qout=$__omp_qout$__omp_qbuf
29
31
  printf "'%s'" "$__omp_qout"
@@ -45,6 +47,7 @@ __omp_emit_export_for() {
45
47
  *TOKEN*|*SECRET*|*PASSWORD*|*PASSWD*|*API_KEY*|*PRIVATE_KEY*|*ACCESS_KEY*|*CREDENTIAL*|*SESSION_KEY*) return ;;
46
48
  esac
47
49
  eval "[ \"\${$1+x}\" = x ]" 2>/dev/null || return
50
+ __omp_xv=
48
51
  eval "__omp_xv=\"\${$1}\"" 2>/dev/null || return
49
52
  printf 'export %s=%s\n' "$1" "$(__omp_sq_quote "$__omp_xv")"
50
53
  }
@@ -20,7 +20,7 @@ import {
20
20
  truncateToWidth,
21
21
  } from "../../tools/render-utils";
22
22
  import { renderStatusLine, renderTreeList, urlHyperlink } from "../../tui";
23
- import { CachedOutputBlock, markFramedBlockComponent } from "../../tui/output-block";
23
+ import { CachedOutputBlock, markFramedBlockComponent, outputBlockContentWidth } from "../../tui/output-block";
24
24
  import { getSearchProviderLabel } from "./provider";
25
25
  import type { SearchResponse } from "./types";
26
26
 
@@ -159,7 +159,7 @@ export function renderSearchResult(
159
159
  const { expanded } = options;
160
160
 
161
161
  // Answer lines: full markdown when expanded, capped markdown preview when collapsed.
162
- const answerWidth = Math.max(20, width - 3);
162
+ const answerWidth = outputBlockContentWidth(width);
163
163
  const renderedAnswer = answerMarkdown ? answerMarkdown.render(answerWidth) : [];
164
164
  let answerLines: readonly string[];
165
165
  if (renderedAnswer.length === 0) {
@@ -1,30 +0,0 @@
1
- import type { AgentSession } from "../session/agent-session.js";
2
- export interface GuidedGoalMessage {
3
- role: "user" | "assistant";
4
- content: string;
5
- }
6
- export type GuidedGoalTurnResult = {
7
- kind: "question";
8
- question: string;
9
- objective?: string;
10
- } | {
11
- kind: "ready";
12
- objective: string;
13
- };
14
- export interface GuidedGoalTurnOptions {
15
- messages: readonly GuidedGoalMessage[];
16
- signal?: AbortSignal;
17
- /**
18
- * Stable Codex transport session id reused across every turn of one
19
- * interview. `handleGuidedGoalCommand` runs up to six turns; minting a fresh
20
- * id per turn opens a new websocket-only Codex socket each time (kept in
21
- * `providerSessionState` until session dispose), which can trip
22
- * `websocket_connection_limit_reached` and drop back to the SSE path this
23
- * fix avoids. Callers pass one id for the whole interview; omitted for
24
- * one-shot callers, which mint a unique id per call.
25
- */
26
- sideSessionId?: string;
27
- }
28
- /** Mint a guided-goal Codex side-session id keyed off the main session id. */
29
- export declare function newGuidedGoalSessionId(session: AgentSession): string;
30
- export declare function runGuidedGoalTurn(session: AgentSession, options: GuidedGoalTurnOptions): Promise<GuidedGoalTurnResult>;
@@ -1,171 +0,0 @@
1
- import { instrumentedCompleteSimple, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
2
- import type { Tool } from "@oh-my-pi/pi-ai";
3
- import { prompt, Snowflake } from "@oh-my-pi/pi-utils";
4
- import { extractTextContent, extractToolCall, parseJsonPayload } from "../commit/utils";
5
- import guidedGoalInterviewPrompt from "../prompts/goals/guided-goal-interview.md" with { type: "text" };
6
- import guidedGoalSystemPrompt from "../prompts/goals/guided-goal-system.md" with { type: "text" };
7
- import type { AgentSession } from "../session/agent-session";
8
- import { concreteThinkingLevel, shouldDisableReasoning, toReasoningEffort } from "../thinking";
9
-
10
- const RESPOND_TOOL_NAME = "respond";
11
-
12
- const RESPOND_TOOL: Tool = {
13
- name: RESPOND_TOOL_NAME,
14
- description: "Return the next guided-goal interview step.",
15
- parameters: {
16
- type: "object",
17
- properties: {
18
- kind: { type: "string", enum: ["question", "ready"] },
19
- question: { type: "string" },
20
- objective: { type: "string" },
21
- },
22
- required: ["kind"],
23
- additionalProperties: false,
24
- },
25
- strict: false,
26
- };
27
-
28
- export interface GuidedGoalMessage {
29
- role: "user" | "assistant";
30
- content: string;
31
- }
32
-
33
- export type GuidedGoalTurnResult =
34
- | { kind: "question"; question: string; objective?: string }
35
- | { kind: "ready"; objective: string };
36
-
37
- export interface GuidedGoalTurnOptions {
38
- messages: readonly GuidedGoalMessage[];
39
- signal?: AbortSignal;
40
- /**
41
- * Stable Codex transport session id reused across every turn of one
42
- * interview. `handleGuidedGoalCommand` runs up to six turns; minting a fresh
43
- * id per turn opens a new websocket-only Codex socket each time (kept in
44
- * `providerSessionState` until session dispose), which can trip
45
- * `websocket_connection_limit_reached` and drop back to the SSE path this
46
- * fix avoids. Callers pass one id for the whole interview; omitted for
47
- * one-shot callers, which mint a unique id per call.
48
- */
49
- sideSessionId?: string;
50
- }
51
-
52
- /** Mint a guided-goal Codex side-session id keyed off the main session id. */
53
- export function newGuidedGoalSessionId(session: AgentSession): string {
54
- return `${session.sessionId}:guided-goal:${Snowflake.next()}`;
55
- }
56
-
57
- function parseGuidedGoalPayload(value: unknown): GuidedGoalTurnResult {
58
- if (!value || typeof value !== "object" || Array.isArray(value)) {
59
- throw new Error("guided goal returned an invalid response");
60
- }
61
- const payload = value as Record<string, unknown>;
62
- if (payload.kind === "question" && typeof payload.question === "string" && payload.question.trim()) {
63
- const question = payload.question.trim();
64
- if (typeof payload.objective === "string" && payload.objective.trim()) {
65
- return { kind: "question", question, objective: payload.objective.trim() };
66
- }
67
- return { kind: "question", question };
68
- }
69
- if (payload.kind === "ready" && typeof payload.objective === "string" && payload.objective.trim()) {
70
- return { kind: "ready", objective: payload.objective.trim() };
71
- }
72
- throw new Error("guided goal returned an invalid response");
73
- }
74
-
75
- function parseToolArguments(value: unknown): unknown {
76
- return typeof value === "string" ? parseJsonPayload(value) : value;
77
- }
78
-
79
- export async function runGuidedGoalTurn(
80
- session: AgentSession,
81
- options: GuidedGoalTurnOptions,
82
- ): Promise<GuidedGoalTurnResult> {
83
- const plan = session.resolveRoleModelWithThinking("plan");
84
- const slow = plan.model ? plan : session.resolveRoleModelWithThinking("slow");
85
- const resolved = slow.model
86
- ? slow
87
- : {
88
- model: session.model,
89
- thinkingLevel: session.thinkingLevel,
90
- explicitThinkingLevel: false,
91
- warning: undefined,
92
- };
93
- if (!resolved.model) {
94
- throw new Error("No plan, slow, or current session model is available for /guided-goal.");
95
- }
96
-
97
- const apiKey = await session.modelRegistry.getApiKey(resolved.model, session.sessionId);
98
- if (!apiKey) {
99
- throw new Error(`No API key for ${resolved.model.provider}/${resolved.model.id}`);
100
- }
101
-
102
- const userPrompt = prompt.render(guidedGoalInterviewPrompt, {
103
- messages: options.messages.map(message => ({ label: message.role.toUpperCase(), content: message.content })),
104
- });
105
- // Secret obfuscation: route the user-authored transcript through the session obfuscator the
106
- // same way normal turns do, so an API key / secret typed into the rough goal or an answer is
107
- // never sent verbatim to the plan/slow provider. Deobfuscated again below before display/use.
108
- const obfuscator = session.obfuscator;
109
- const promptText = obfuscator?.hasSecrets() ? obfuscator.obfuscate(userPrompt) : userPrompt;
110
- const thinkingLevel = concreteThinkingLevel(resolved.thinkingLevel);
111
- const response = await instrumentedCompleteSimple(
112
- resolved.model,
113
- {
114
- systemPrompt: [prompt.render(guidedGoalSystemPrompt)],
115
- messages: [{ role: "user", content: [{ type: "text", text: promptText }], timestamp: Date.now() }],
116
- tools: [RESPOND_TOOL],
117
- },
118
- {
119
- apiKey: session.modelRegistry.resolver(resolved.model, session.sessionId),
120
- signal: options.signal,
121
- reasoning: toReasoningEffort(thinkingLevel),
122
- disableReasoning: shouldDisableReasoning(thinkingLevel),
123
- toolChoice: { type: "tool", name: RESPOND_TOOL_NAME },
124
- // Route through the session's provider transport so websocket-only Codex
125
- // models (gpt-5.6-luna/sol/terra) get a websocket session instead of
126
- // falling back to SSE — the Codex SSE /responses endpoint does not serve
127
- // those ids and rejects the turn with "Model not found" (#5304, same class
128
- // as the /btw regression in #5213). The side session id is minted once per
129
- // interview and reused across turns so a multi-question interview shares one
130
- // Codex socket instead of opening a fresh one each turn; it stays distinct
131
- // from the main session id so the oneshot's append-only turn state never
132
- // pollutes the main conversation.
133
- sessionId: options.sideSessionId ?? newGuidedGoalSessionId(session),
134
- promptCacheKey: session.sessionId,
135
- preferWebsockets: session.preferWebsockets,
136
- providerSessionState: session.providerSessionState,
137
- },
138
- { telemetry: resolveTelemetry(session.agent.telemetry, session.sessionId), oneshotKind: "guided_goal_setup" },
139
- );
140
-
141
- if (response.stopReason === "error") {
142
- throw new Error(response.errorMessage ?? "guided goal request failed");
143
- }
144
- if (response.stopReason === "aborted") {
145
- throw new Error("guided goal request aborted");
146
- }
147
-
148
- const call = extractToolCall(response, RESPOND_TOOL_NAME);
149
- let result: GuidedGoalTurnResult;
150
- if (call) {
151
- result = parseGuidedGoalPayload(parseToolArguments(call.arguments));
152
- } else {
153
- const text = extractTextContent(response);
154
- if (!text) {
155
- throw new Error("guided goal returned an invalid response");
156
- }
157
- result = parseGuidedGoalPayload(parseJsonPayload(text));
158
- }
159
-
160
- // Reverse the obfuscation: restore any secret placeholders the model echoed back before the
161
- // question/objective is shown or the goal is started.
162
- if (!obfuscator?.hasSecrets()) return result;
163
- if (result.kind === "question") {
164
- return {
165
- kind: "question",
166
- question: obfuscator.deobfuscate(result.question),
167
- objective: result.objective !== undefined ? obfuscator.deobfuscate(result.objective) : undefined,
168
- };
169
- }
170
- return { kind: "ready", objective: obfuscator.deobfuscate(result.objective) };
171
- }