@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.
- package/CHANGELOG.md +58 -0
- package/dist/{CHANGELOG-x9zt79k8.md → CHANGELOG-vt8ene9g.md} +58 -0
- package/dist/cli.js +4309 -7287
- package/dist/template-dys3vk5b.js +1671 -0
- package/dist/template-f8wx9vfn.css +1355 -0
- package/dist/template-qat058wr.html +55 -0
- package/dist/tool-views.generated-jdfmzwmn.js +35 -0
- package/dist/types/advisor/advise-tool.d.ts +0 -7
- package/dist/types/advisor/runtime.d.ts +0 -9
- package/dist/types/async/job-manager.d.ts +11 -0
- package/dist/types/cleanse/agent.d.ts +19 -0
- package/dist/types/cleanse/balance.d.ts +7 -0
- package/dist/types/cleanse/checkers.d.ts +13 -0
- package/dist/types/cleanse/index.d.ts +16 -0
- package/dist/types/cleanse/loop.d.ts +16 -0
- package/dist/types/cleanse/parsers.d.ts +13 -0
- package/dist/types/cleanse/progress.d.ts +14 -0
- package/dist/types/cleanse/types.d.ts +64 -0
- package/dist/types/commands/cleanse.d.ts +23 -0
- package/dist/types/config/settings-schema.d.ts +16 -1
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/export/html/index.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -3
- package/dist/types/session/agent-session-types.d.ts +5 -5
- package/dist/types/session/agent-session.d.ts +26 -1
- package/dist/types/session/async-job-delivery.d.ts +8 -0
- package/dist/types/session/model-controls.d.ts +4 -4
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/turn-recovery.d.ts +1 -1
- package/dist/types/task/isolation-ownership.d.ts +34 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
- package/dist/types/tools/browser/tab-worker.d.ts +1 -4
- package/dist/types/tools/index.d.ts +8 -3
- package/dist/types/tools/output-meta.d.ts +5 -0
- package/dist/types/tools/read.d.ts +9 -1
- package/dist/types/tools/xdev.d.ts +53 -67
- package/dist/types/tui/output-block.d.ts +5 -5
- package/dist/types/utils/cpuprofile.d.ts +51 -0
- package/dist/types/utils/inspect-image-mode.d.ts +29 -0
- package/dist/types/utils/profile-tree.d.ts +47 -0
- package/dist/types/utils/sample-profile.d.ts +67 -0
- package/package.json +16 -12
- package/scripts/bundle-dist.ts +3 -1
- package/src/advisor/advise-tool.ts +0 -11
- package/src/advisor/runtime.ts +0 -12
- package/src/async/job-manager.ts +30 -7
- package/src/cleanse/agent.ts +226 -0
- package/src/cleanse/balance.ts +79 -0
- package/src/cleanse/checkers.ts +996 -0
- package/src/cleanse/index.ts +190 -0
- package/src/cleanse/loop.ts +51 -0
- package/src/cleanse/parsers.ts +726 -0
- package/src/cleanse/progress.ts +50 -0
- package/src/cleanse/prompts/assignment.md +47 -0
- package/src/cleanse/types.ts +72 -0
- package/src/cli/update-cli.ts +3 -0
- package/src/cli/usage-cli.ts +29 -4
- package/src/cli/worktree-cli.ts +28 -11
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +2 -1
- package/src/commands/cleanse.ts +45 -0
- package/src/config/settings-schema.ts +15 -1
- package/src/config/settings.ts +35 -0
- package/src/cursor.ts +4 -3
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/discovery/claude-plugins.ts +144 -34
- package/src/export/html/index.ts +17 -10
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
- package/src/extensibility/shared-events.ts +18 -1
- package/src/launch/broker.ts +14 -4
- package/src/modes/acp/acp-agent.ts +12 -9
- package/src/modes/acp/acp-event-mapper.ts +38 -4
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/settings-selector.ts +9 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/extension-ui-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/interactive-mode.ts +36 -47
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- package/src/prompts/goals/guided-goal-interview.md +41 -6
- package/src/prompts/system/vibe-mode-active.md +4 -1
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/goal.md +1 -1
- package/src/sdk.ts +47 -50
- package/src/session/agent-session-types.ts +5 -5
- package/src/session/agent-session.ts +151 -11
- package/src/session/async-job-delivery.ts +8 -0
- package/src/session/model-controls.ts +9 -9
- package/src/session/session-advisors.ts +2 -7
- package/src/session/session-history-format.ts +31 -6
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +158 -52
- package/src/session/streaming-output.ts +18 -6
- package/src/session/turn-recovery.ts +4 -4
- package/src/slash-commands/builtin-registry.ts +74 -2
- package/src/task/isolation-ownership.ts +106 -0
- package/src/task/worktree.ts +8 -0
- package/src/tools/ask.ts +3 -3
- package/src/tools/ast-edit.ts +9 -2
- package/src/tools/bash.ts +16 -9
- package/src/tools/browser/cmux/cmux-tab.ts +9 -14
- package/src/tools/browser/tab-worker.ts +12 -35
- package/src/tools/browser.ts +2 -2
- package/src/tools/gh-renderer.ts +3 -3
- package/src/tools/index.ts +46 -17
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +87 -13
- package/src/tools/write.ts +51 -7
- package/src/tools/xdev.ts +198 -210
- package/src/tui/code-cell.ts +4 -4
- package/src/tui/output-block.ts +25 -8
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/sample-profile.ts +437 -0
- package/src/utils/shell-snapshot-fn-env.sh +5 -2
- package/src/web/search/render.ts +2 -2
- package/dist/types/goals/guided-setup.d.ts +0 -30
- package/src/goals/guided-setup.ts +0 -171
- 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
|
|
26
|
-
__omp_qbuf=${__omp_qbuf
|
|
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
|
}
|
package/src/web/search/render.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
}
|