@oh-my-pi/pi-coding-agent 16.1.10 → 16.1.12
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 +45 -0
- package/dist/cli.js +2480 -2481
- package/dist/types/export/html/index.d.ts +31 -2
- package/dist/types/export/html/web-palette.d.ts +117 -0
- package/dist/types/export/share.d.ts +10 -5
- package/dist/types/hindsight/content.d.ts +7 -0
- package/dist/types/hindsight/transcript.d.ts +1 -1
- package/dist/types/modes/components/hook-editor.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +5 -0
- package/dist/types/modes/types.d.ts +17 -0
- package/dist/types/modes/utils/keybinding-matchers.d.ts +13 -0
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +43 -9
- package/dist/types/session/agent-session.d.ts +4 -0
- package/dist/types/session/session-context.d.ts +2 -0
- package/dist/types/session/session-entries.d.ts +6 -0
- package/dist/types/session/session-manager.d.ts +2 -1
- package/dist/types/tools/acp-bridge.d.ts +29 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +7 -0
- package/dist/types/tools/browser/tab-worker.d.ts +20 -0
- package/dist/types/utils/jj.d.ts +25 -0
- package/dist/types/utils/title-generator.d.ts +0 -2
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +4 -2
- package/src/autoresearch/git.ts +12 -0
- package/src/discovery/opencode.ts +47 -4
- package/src/edit/hashline/filesystem.ts +8 -0
- package/src/edit/modes/patch.ts +18 -2
- package/src/edit/modes/replace.ts +13 -10
- package/src/export/html/index.ts +50 -8
- package/src/export/html/web-palette.ts +142 -0
- package/src/export/share.ts +198 -8
- package/src/hindsight/backend.ts +4 -4
- package/src/hindsight/content.ts +17 -1
- package/src/hindsight/transcript.ts +2 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/main.ts +8 -0
- package/src/modes/components/agent-dashboard.ts +8 -8
- package/src/modes/components/hook-editor.ts +13 -10
- package/src/modes/components/session-selector.ts +3 -0
- package/src/modes/controllers/event-controller.ts +9 -5
- package/src/modes/controllers/extension-ui-controller.ts +6 -2
- package/src/modes/interactive-mode.ts +69 -29
- package/src/modes/theme/dark.json +1 -1
- package/src/modes/types.ts +18 -0
- package/src/modes/utils/keybinding-matchers.ts +36 -1
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/browser.md +3 -2
- package/src/sdk.ts +14 -2
- package/src/secrets/index.ts +1 -1
- package/src/secrets/obfuscator.ts +220 -71
- package/src/session/agent-session.ts +57 -43
- package/src/session/session-context.ts +5 -0
- package/src/session/session-entries.ts +6 -0
- package/src/session/session-manager.ts +3 -1
- package/src/task/worktree.ts +12 -4
- package/src/thinking.ts +1 -1
- package/src/tools/acp-bridge.ts +66 -0
- package/src/tools/browser/cmux/cmux-tab.ts +37 -0
- package/src/tools/browser/tab-worker.ts +160 -37
- package/src/tools/write.ts +2 -25
- package/src/utils/jj.ts +47 -0
- package/src/utils/title-generator.ts +31 -99
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { AssistantMessage, Context, ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import type { SessionContext } from "../session/session-context";
|
|
4
4
|
import { compileSecretRegex } from "./regex";
|
|
5
5
|
|
|
@@ -15,6 +15,9 @@ export interface SecretEntry {
|
|
|
15
15
|
flags?: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue | undefined };
|
|
19
|
+
export type JsonRecord = { [key: string]: JsonValue | undefined };
|
|
20
|
+
|
|
18
21
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
22
|
// Deterministic replacement generation
|
|
20
23
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -85,26 +88,34 @@ export class SecretObfuscator {
|
|
|
85
88
|
|
|
86
89
|
constructor(entries: SecretEntry[]) {
|
|
87
90
|
let index = 0;
|
|
91
|
+
let hasRealSec = false;
|
|
88
92
|
for (const entry of entries) {
|
|
89
93
|
const mode = entry.mode ?? "obfuscate";
|
|
90
94
|
|
|
91
95
|
if (entry.type === "plain") {
|
|
92
96
|
if (mode === "obfuscate") {
|
|
97
|
+
if (entry.content.length < 8) {
|
|
98
|
+
// Tone down short plain secret obfuscation to avoid false matches on small words like "esp"
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
93
101
|
const placeholder = buildPlaceholder(index);
|
|
94
102
|
this.#plainMappings.set(entry.content, index);
|
|
95
103
|
this.#obfuscateMappings.set(index, { secret: entry.content, placeholder });
|
|
96
104
|
this.#deobfuscateMap.set(placeholder, entry.content);
|
|
97
105
|
index++;
|
|
106
|
+
hasRealSec = true;
|
|
98
107
|
} else {
|
|
99
108
|
// replace mode
|
|
100
109
|
const replacement = entry.replacement ?? generateDeterministicReplacement(entry.content);
|
|
101
110
|
this.#replaceMappings.set(entry.content, replacement);
|
|
111
|
+
hasRealSec = true;
|
|
102
112
|
}
|
|
103
113
|
} else {
|
|
104
114
|
// regex type — compiled here, matches discovered during obfuscate()
|
|
105
115
|
try {
|
|
106
116
|
const regex = compileSecretRegex(entry.content, entry.flags);
|
|
107
117
|
this.#regexEntries.push({ regex, mode, replacement: entry.replacement });
|
|
118
|
+
hasRealSec = true;
|
|
108
119
|
} catch {
|
|
109
120
|
// Invalid regex — skip silently (validation happens at load time)
|
|
110
121
|
}
|
|
@@ -112,7 +123,7 @@ export class SecretObfuscator {
|
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
this.#nextIndex = index;
|
|
115
|
-
this.#hasAny =
|
|
126
|
+
this.#hasAny = hasRealSec;
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
hasSecrets(): boolean {
|
|
@@ -154,6 +165,10 @@ export class SecretObfuscator {
|
|
|
154
165
|
const replacement = entry.replacement ?? generateDeterministicReplacement(matchValue);
|
|
155
166
|
result = replaceAll(result, matchValue, replacement);
|
|
156
167
|
} else {
|
|
168
|
+
if (matchValue.length < 8) {
|
|
169
|
+
// Tone down short regex match obfuscation to avoid false matches on small words/fragments
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
157
172
|
// obfuscate mode — get or create stable index
|
|
158
173
|
let index = this.#findObfuscateIndex(matchValue);
|
|
159
174
|
if (index === undefined) {
|
|
@@ -174,30 +189,13 @@ export class SecretObfuscator {
|
|
|
174
189
|
/** Deobfuscate obfuscate-mode placeholders back to original secrets. Replace-mode is NOT reversed. */
|
|
175
190
|
deobfuscate(text: string): string {
|
|
176
191
|
if (!this.#hasAny || !text.includes("#")) return text;
|
|
177
|
-
return text.replace(PLACEHOLDER_RE, match =>
|
|
178
|
-
return this.#deobfuscateMap.get(match) ?? match;
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** Deep-walk an object, deobfuscating all string values. */
|
|
183
|
-
deobfuscateObject<T>(obj: T): T {
|
|
184
|
-
if (!this.#hasAny) return obj;
|
|
185
|
-
return deepWalkStrings(obj, s => this.deobfuscate(s));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** Deep-walk an object, obfuscating all string values. */
|
|
189
|
-
obfuscateObject<T>(obj: T): T {
|
|
190
|
-
if (!this.#hasAny) return obj;
|
|
191
|
-
return deepWalkStrings(obj, s => this.obfuscate(s));
|
|
192
|
+
return text.replace(PLACEHOLDER_RE, match => this.#deobfuscateMap.get(match) ?? match);
|
|
192
193
|
}
|
|
193
|
-
|
|
194
194
|
/** Find the obfuscate index for a known secret value. */
|
|
195
195
|
#findObfuscateIndex(secret: string): number | undefined {
|
|
196
|
-
// Check plain mappings first
|
|
197
196
|
const plainIndex = this.#plainMappings.get(secret);
|
|
198
197
|
if (plainIndex !== undefined) return plainIndex;
|
|
199
198
|
|
|
200
|
-
// Check regex-discovered mappings
|
|
201
199
|
for (const [index, mapping] of this.#obfuscateMappings) {
|
|
202
200
|
if (mapping.secret === secret) return index;
|
|
203
201
|
}
|
|
@@ -205,47 +203,200 @@ export class SecretObfuscator {
|
|
|
205
203
|
}
|
|
206
204
|
}
|
|
207
205
|
|
|
206
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
207
|
+
// Display restore (inbound, persisted/provider → local display)
|
|
208
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Restore secret placeholders for local display. Only message kinds the model
|
|
212
|
+
* itself authored from obfuscated context carry placeholders — assistant
|
|
213
|
+
* content and the LLM-written branch/compaction summaries. User, developer, and
|
|
214
|
+
* tool-result messages are persisted with their literal text, so a literal
|
|
215
|
+
* `#ABCD#` the operator typed must survive untouched; those roles are never
|
|
216
|
+
* walked.
|
|
217
|
+
*/
|
|
208
218
|
export function deobfuscateSessionContext(
|
|
209
219
|
sessionContext: SessionContext,
|
|
210
220
|
obfuscator: SecretObfuscator | undefined,
|
|
211
221
|
): SessionContext {
|
|
212
222
|
if (!obfuscator?.hasSecrets()) return sessionContext;
|
|
213
|
-
const messages = obfuscator
|
|
223
|
+
const messages = deobfuscateAgentMessages(obfuscator, sessionContext.messages);
|
|
214
224
|
return messages === sessionContext.messages ? sessionContext : { ...sessionContext, messages };
|
|
215
225
|
}
|
|
216
226
|
|
|
227
|
+
export function deobfuscateAgentMessages(obfuscator: SecretObfuscator, messages: AgentMessage[]): AgentMessage[] {
|
|
228
|
+
let changed = false;
|
|
229
|
+
const result = messages.map((message): AgentMessage => {
|
|
230
|
+
switch (message.role) {
|
|
231
|
+
case "assistant": {
|
|
232
|
+
const content = deobfuscateAssistantContent(obfuscator, message.content);
|
|
233
|
+
if (content === message.content) return message;
|
|
234
|
+
changed = true;
|
|
235
|
+
return { ...message, content };
|
|
236
|
+
}
|
|
237
|
+
case "branchSummary": {
|
|
238
|
+
const summary = obfuscator.deobfuscate(message.summary);
|
|
239
|
+
if (summary === message.summary) return message;
|
|
240
|
+
changed = true;
|
|
241
|
+
return { ...message, summary };
|
|
242
|
+
}
|
|
243
|
+
case "compactionSummary": {
|
|
244
|
+
const summary = obfuscator.deobfuscate(message.summary);
|
|
245
|
+
const shortSummary =
|
|
246
|
+
message.shortSummary === undefined ? undefined : obfuscator.deobfuscate(message.shortSummary);
|
|
247
|
+
const blocks = message.blocks === undefined ? undefined : deobfuscateTextBlocks(obfuscator, message.blocks);
|
|
248
|
+
if (summary === message.summary && shortSummary === message.shortSummary && blocks === message.blocks) {
|
|
249
|
+
return message;
|
|
250
|
+
}
|
|
251
|
+
changed = true;
|
|
252
|
+
return { ...message, summary, shortSummary, blocks };
|
|
253
|
+
}
|
|
254
|
+
default:
|
|
255
|
+
return message;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
return changed ? result : messages;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Restore placeholders in assistant content: visible text, thinking text, and
|
|
263
|
+
* tool-call arguments/intent/rawBlock. Signatures and redacted-thinking bytes
|
|
264
|
+
* are opaque provider-replay data and pass through byte-identical.
|
|
265
|
+
*/
|
|
266
|
+
export function deobfuscateAssistantContent(
|
|
267
|
+
obfuscator: SecretObfuscator,
|
|
268
|
+
content: AssistantMessage["content"],
|
|
269
|
+
): AssistantMessage["content"] {
|
|
270
|
+
if (!obfuscator.hasSecrets()) return content;
|
|
271
|
+
let changed = false;
|
|
272
|
+
const result = content.map((block): AssistantMessage["content"][number] => {
|
|
273
|
+
if (block.type === "text") {
|
|
274
|
+
const text = obfuscator.deobfuscate(block.text);
|
|
275
|
+
if (text === block.text) return block;
|
|
276
|
+
changed = true;
|
|
277
|
+
return { ...block, text };
|
|
278
|
+
}
|
|
279
|
+
if (block.type === "thinking") {
|
|
280
|
+
const thinking = obfuscator.deobfuscate(block.thinking);
|
|
281
|
+
if (thinking === block.thinking) return block;
|
|
282
|
+
changed = true;
|
|
283
|
+
return { ...block, thinking };
|
|
284
|
+
}
|
|
285
|
+
if (block.type === "toolCall") {
|
|
286
|
+
const args = deobfuscateToolArguments(obfuscator, block.arguments);
|
|
287
|
+
const intent = block.intent === undefined ? undefined : obfuscator.deobfuscate(block.intent);
|
|
288
|
+
const rawBlock = block.rawBlock === undefined ? undefined : obfuscator.deobfuscate(block.rawBlock);
|
|
289
|
+
if (args === block.arguments && intent === block.intent && rawBlock === block.rawBlock) return block;
|
|
290
|
+
changed = true;
|
|
291
|
+
return { ...block, arguments: args, intent, rawBlock };
|
|
292
|
+
}
|
|
293
|
+
return block;
|
|
294
|
+
});
|
|
295
|
+
return changed ? result : content;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Restore placeholders inside a tool call's arguments. Arguments are arbitrary
|
|
300
|
+
* model-authored JSON, so tool-call arguments are the ONLY place a recursive
|
|
301
|
+
* JSON walk runs.
|
|
302
|
+
*/
|
|
303
|
+
export function deobfuscateToolArguments(
|
|
304
|
+
obfuscator: SecretObfuscator,
|
|
305
|
+
args: Record<string, unknown>,
|
|
306
|
+
): Record<string, unknown> {
|
|
307
|
+
if (!obfuscator.hasSecrets()) return args;
|
|
308
|
+
return mapJsonStrings(args as JsonValue, s => obfuscator.deobfuscate(s)) as Record<string, unknown>;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Redact secrets inside a tool call's arguments (same JSON-walk exception as {@link deobfuscateToolArguments}). */
|
|
312
|
+
export function obfuscateToolArguments(
|
|
313
|
+
obfuscator: SecretObfuscator,
|
|
314
|
+
args: Record<string, unknown>,
|
|
315
|
+
): Record<string, unknown> {
|
|
316
|
+
if (!obfuscator.hasSecrets()) return args;
|
|
317
|
+
return mapJsonStrings(args as JsonValue, s => obfuscator.obfuscate(s)) as Record<string, unknown>;
|
|
318
|
+
}
|
|
319
|
+
|
|
217
320
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
218
|
-
//
|
|
321
|
+
// Outbound obfuscation (local → provider)
|
|
219
322
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
220
323
|
|
|
221
|
-
|
|
324
|
+
type UserFacingMessage = Extract<Message, { role: "user" | "developer" | "toolResult" }>;
|
|
325
|
+
|
|
326
|
+
/** Obfuscate `text` blocks of a content array; image and other blocks pass through. */
|
|
327
|
+
function obfuscateTextBlocks(
|
|
328
|
+
obfuscator: SecretObfuscator,
|
|
329
|
+
content: (TextContent | ImageContent)[],
|
|
330
|
+
): (TextContent | ImageContent)[] {
|
|
331
|
+
let changed = false;
|
|
332
|
+
const result = content.map((block): TextContent | ImageContent => {
|
|
333
|
+
if (block.type !== "text") return block;
|
|
334
|
+
const text = obfuscator.obfuscate(block.text);
|
|
335
|
+
if (text === block.text) return block;
|
|
336
|
+
changed = true;
|
|
337
|
+
return { ...block, text };
|
|
338
|
+
});
|
|
339
|
+
return changed ? result : content;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Restore placeholders in `text` blocks of a content array; image and other blocks pass through. */
|
|
343
|
+
function deobfuscateTextBlocks(
|
|
344
|
+
obfuscator: SecretObfuscator,
|
|
345
|
+
content: (TextContent | ImageContent)[],
|
|
346
|
+
): (TextContent | ImageContent)[] {
|
|
347
|
+
let changed = false;
|
|
348
|
+
const result = content.map((block): TextContent | ImageContent => {
|
|
349
|
+
if (block.type !== "text") return block;
|
|
350
|
+
const text = obfuscator.deobfuscate(block.text);
|
|
351
|
+
if (text === block.text) return block;
|
|
352
|
+
changed = true;
|
|
353
|
+
return { ...block, text };
|
|
354
|
+
});
|
|
355
|
+
return changed ? result : content;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Redact secrets from outbound messages. Opt-in by origin: only user messages,
|
|
360
|
+
* tool results, and user-authored developer messages (e.g. `@file` mentions)
|
|
361
|
+
* can carry operator secrets. System prompts, tool schemas, and assistant
|
|
362
|
+
* output are author-controlled or model-generated and pass through untouched.
|
|
363
|
+
* Within a targeted message only `text` blocks are rewritten — inline image
|
|
364
|
+
* bytes are never walked.
|
|
365
|
+
*/
|
|
222
366
|
export function obfuscateMessages(obfuscator: SecretObfuscator, messages: Message[]): Message[] {
|
|
223
|
-
|
|
367
|
+
if (!obfuscator.hasSecrets()) return messages;
|
|
368
|
+
let changed = false;
|
|
369
|
+
const result = messages.map((message): Message => {
|
|
370
|
+
if (
|
|
371
|
+
message.role !== "user" &&
|
|
372
|
+
message.role !== "toolResult" &&
|
|
373
|
+
!(message.role === "developer" && message.attribution === "user")
|
|
374
|
+
) {
|
|
375
|
+
return message;
|
|
376
|
+
}
|
|
377
|
+
const target = message as UserFacingMessage;
|
|
378
|
+
if (typeof target.content === "string") {
|
|
379
|
+
const content = obfuscator.obfuscate(target.content);
|
|
380
|
+
if (content === target.content) return message;
|
|
381
|
+
changed = true;
|
|
382
|
+
return { ...target, content } as Message;
|
|
383
|
+
}
|
|
384
|
+
const content = obfuscateTextBlocks(obfuscator, target.content);
|
|
385
|
+
if (content === target.content) return message;
|
|
386
|
+
changed = true;
|
|
387
|
+
return { ...target, content } as Message;
|
|
388
|
+
});
|
|
389
|
+
return changed ? result : messages;
|
|
224
390
|
}
|
|
225
391
|
|
|
226
|
-
/**
|
|
392
|
+
/**
|
|
393
|
+
* Redact outbound provider context. Only conversation messages are rewritten;
|
|
394
|
+
* the static system prompt and tool schemas pass through unchanged.
|
|
395
|
+
*/
|
|
227
396
|
export function obfuscateProviderContext(obfuscator: SecretObfuscator | undefined, context: Context): Context {
|
|
228
397
|
if (!obfuscator?.hasSecrets()) return context;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
systemPrompt: obfuscator.obfuscateObject(context.systemPrompt),
|
|
232
|
-
messages: obfuscator.obfuscateObject(context.messages),
|
|
233
|
-
tools: obfuscateProviderTools(obfuscator, context.tools),
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** Convert tool schemas to wire JSON Schema before obfuscating provider-visible strings. */
|
|
238
|
-
export function obfuscateProviderTools(
|
|
239
|
-
obfuscator: SecretObfuscator | undefined,
|
|
240
|
-
tools: Tool[] | undefined,
|
|
241
|
-
): Tool[] | undefined {
|
|
242
|
-
if (!tools || !obfuscator?.hasSecrets()) return tools;
|
|
243
|
-
return tools.map(tool => ({
|
|
244
|
-
...tool,
|
|
245
|
-
description: obfuscator.obfuscate(tool.description),
|
|
246
|
-
parameters: obfuscator.obfuscateObject(toolWireSchema(tool)),
|
|
247
|
-
customFormat: tool.customFormat ? obfuscator.obfuscateObject(tool.customFormat) : undefined,
|
|
248
|
-
}));
|
|
398
|
+
const messages = obfuscateMessages(obfuscator, context.messages);
|
|
399
|
+
return messages === context.messages ? context : { ...context, messages };
|
|
249
400
|
}
|
|
250
401
|
|
|
251
402
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -264,35 +415,33 @@ function replaceAll(text: string, search: string, replacement: string): string {
|
|
|
264
415
|
return result;
|
|
265
416
|
}
|
|
266
417
|
|
|
267
|
-
/**
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
418
|
+
/**
|
|
419
|
+
* Map every string in arbitrary JSON. Used ONLY for tool-call arguments, whose
|
|
420
|
+
* shape is model-authored and not known ahead of time. No other caller may walk
|
|
421
|
+
* untyped data: every message/content path is handled by a typed transformer.
|
|
422
|
+
*/
|
|
423
|
+
function mapJsonStrings(value: JsonValue, fn: (s: string) => string): JsonValue {
|
|
424
|
+
if (typeof value === "string") return fn(value);
|
|
425
|
+
if (Array.isArray(value)) {
|
|
273
426
|
let changed = false;
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
277
|
-
return
|
|
427
|
+
const out = value.map(item => {
|
|
428
|
+
const next = mapJsonStrings(item, fn);
|
|
429
|
+
if (next !== item) changed = true;
|
|
430
|
+
return next;
|
|
278
431
|
});
|
|
279
|
-
return
|
|
432
|
+
return changed ? out : value;
|
|
280
433
|
}
|
|
281
|
-
if (
|
|
434
|
+
if (value !== null && typeof value === "object") {
|
|
282
435
|
let changed = false;
|
|
283
|
-
const
|
|
284
|
-
for (const key of Object.keys(
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
436
|
+
const out: JsonRecord = {};
|
|
437
|
+
for (const key of Object.keys(value)) {
|
|
438
|
+
const item = value[key];
|
|
439
|
+
if (item === undefined) continue;
|
|
440
|
+
const next = mapJsonStrings(item, fn);
|
|
441
|
+
if (next !== item) changed = true;
|
|
442
|
+
out[key] = next;
|
|
289
443
|
}
|
|
290
|
-
return
|
|
444
|
+
return changed ? out : value;
|
|
291
445
|
}
|
|
292
|
-
return
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function isPlainRecord(obj: object): obj is Record<string, unknown> {
|
|
296
|
-
const prototype = Object.getPrototypeOf(obj);
|
|
297
|
-
return prototype === Object.prototype || prototype === null;
|
|
446
|
+
return value;
|
|
298
447
|
}
|
|
@@ -210,7 +210,7 @@ import { resolveMemoryBackend } from "../memory-backend";
|
|
|
210
210
|
import { shutdownMnemopiEmbedClient } from "../mnemopi/embed-client";
|
|
211
211
|
import { getMnemopiSessionState, type MnemopiSessionState, setMnemopiSessionState } from "../mnemopi/state";
|
|
212
212
|
import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
|
|
213
|
-
import {
|
|
213
|
+
import { theme } from "../modes/theme/theme";
|
|
214
214
|
import { parseTurnBudget } from "../modes/turn-budget";
|
|
215
215
|
import { containsUltrathink, ULTRATHINK_NOTICE } from "../modes/ultrathink";
|
|
216
216
|
import { computeNonMessageBreakdown, computeNonMessageTokens } from "../modes/utils/context-usage";
|
|
@@ -234,9 +234,10 @@ import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { t
|
|
|
234
234
|
import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" with { type: "text" };
|
|
235
235
|
import unexpectedStopRetryTemplate from "../prompts/system/unexpected-stop-retry.md" with { type: "text" };
|
|
236
236
|
import {
|
|
237
|
+
deobfuscateAssistantContent,
|
|
237
238
|
deobfuscateSessionContext,
|
|
239
|
+
obfuscateMessages,
|
|
238
240
|
obfuscateProviderContext,
|
|
239
|
-
obfuscateProviderTools,
|
|
240
241
|
type SecretObfuscator,
|
|
241
242
|
} from "../secrets/obfuscator";
|
|
242
243
|
import { invalidateHostMetadata } from "../ssh/connection-manager";
|
|
@@ -477,6 +478,8 @@ export interface AgentSessionConfig {
|
|
|
477
478
|
modelRegistry: ModelRegistry;
|
|
478
479
|
/** Tool registry for LSP and settings */
|
|
479
480
|
toolRegistry?: Map<string, AgentTool>;
|
|
481
|
+
/** Tool names whose current registry entry is still the built-in implementation. */
|
|
482
|
+
builtInToolNames?: Iterable<string>;
|
|
480
483
|
/** Current session pre-LLM message transform pipeline */
|
|
481
484
|
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
482
485
|
/** Provider payload hook used by the active session request path */
|
|
@@ -1290,6 +1293,7 @@ export class AgentSession {
|
|
|
1290
1293
|
// Generic tool discovery (covers built-in + MCP + extension when tools.discoveryMode === "all")
|
|
1291
1294
|
#discoverableToolSearchIndex: DiscoverableToolSearchIndex | null = null;
|
|
1292
1295
|
#selectedDiscoveredToolNames = new Set<string>();
|
|
1296
|
+
#builtInToolNames = new Set<string>();
|
|
1293
1297
|
#rpcHostToolNames = new Set<string>();
|
|
1294
1298
|
#defaultSelectedMCPServerNames = new Set<string>();
|
|
1295
1299
|
#defaultSelectedMCPToolNames = new Set<string>();
|
|
@@ -1566,6 +1570,7 @@ export class AgentSession {
|
|
|
1566
1570
|
this.#pruneToolDescriptions = config.pruneToolDescriptions === true;
|
|
1567
1571
|
this.#validateRetryFallbackChains();
|
|
1568
1572
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
1573
|
+
this.#builtInToolNames = new Set(config.builtInToolNames ?? []);
|
|
1569
1574
|
this.#requestedToolNames = config.requestedToolNames;
|
|
1570
1575
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
1571
1576
|
this.#onPayload = config.onPayload;
|
|
@@ -2496,7 +2501,7 @@ export class AgentSession {
|
|
|
2496
2501
|
const obfuscator = this.#obfuscator;
|
|
2497
2502
|
if (obfuscator && event.type === "message_end" && event.message.role === "assistant") {
|
|
2498
2503
|
const message = event.message;
|
|
2499
|
-
const deobfuscatedContent = obfuscator
|
|
2504
|
+
const deobfuscatedContent = deobfuscateAssistantContent(obfuscator, message.content);
|
|
2500
2505
|
if (deobfuscatedContent !== message.content) {
|
|
2501
2506
|
displayEvent = { ...event, message: { ...message, content: deobfuscatedContent } };
|
|
2502
2507
|
}
|
|
@@ -4496,6 +4501,11 @@ export class AgentSession {
|
|
|
4496
4501
|
return this.#toolRegistry.get(name);
|
|
4497
4502
|
}
|
|
4498
4503
|
|
|
4504
|
+
/** True when the current registry entry for `name` came from a built-in factory. */
|
|
4505
|
+
hasBuiltInTool(name: string): boolean {
|
|
4506
|
+
return this.#builtInToolNames.has(name);
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4499
4509
|
/**
|
|
4500
4510
|
* Get all configured tool names (built-in via --tools or default, plus custom tools).
|
|
4501
4511
|
*/
|
|
@@ -5223,28 +5233,17 @@ export class AgentSession {
|
|
|
5223
5233
|
return deobfuscateSessionContext(this.sessionManager.buildSessionContext({ transcript: true }), this.#obfuscator);
|
|
5224
5234
|
}
|
|
5225
5235
|
|
|
5226
|
-
#obfuscateForProvider<T>(value: T): T {
|
|
5227
|
-
if (!this.#obfuscator?.hasSecrets()) return value;
|
|
5228
|
-
return this.#obfuscator.obfuscateObject(value);
|
|
5229
|
-
}
|
|
5230
|
-
|
|
5231
5236
|
#obfuscateTextForProvider(text: string | undefined): string | undefined {
|
|
5232
5237
|
if (!text || !this.#obfuscator?.hasSecrets()) return text;
|
|
5233
5238
|
return this.#obfuscator.obfuscate(text);
|
|
5234
5239
|
}
|
|
5235
5240
|
|
|
5236
5241
|
#obfuscatePreparationForProvider(preparation: CompactionPreparation): CompactionPreparation {
|
|
5237
|
-
if (!this.#obfuscator?.hasSecrets()) return preparation;
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
? this.#obfuscator.obfuscate(preparation.previousSummary)
|
|
5243
|
-
: preparation.previousSummary,
|
|
5244
|
-
previousPreserveData: preparation.previousPreserveData
|
|
5245
|
-
? this.#obfuscator.obfuscateObject(preparation.previousPreserveData)
|
|
5246
|
-
: preparation.previousPreserveData,
|
|
5247
|
-
};
|
|
5242
|
+
if (!this.#obfuscator?.hasSecrets() || !preparation.previousSummary) return preparation;
|
|
5243
|
+
// `previousPreserveData` is opaque provider-replay state (e.g. OpenAI remote-compaction
|
|
5244
|
+
// `encrypted_content`); rewriting it would corrupt replay, so only the plaintext summary
|
|
5245
|
+
// is redacted.
|
|
5246
|
+
return { ...preparation, previousSummary: this.#obfuscator.obfuscate(preparation.previousSummary) };
|
|
5248
5247
|
}
|
|
5249
5248
|
|
|
5250
5249
|
#deobfuscateFromProvider(text: string): string {
|
|
@@ -5261,7 +5260,8 @@ export class AgentSession {
|
|
|
5261
5260
|
}
|
|
5262
5261
|
|
|
5263
5262
|
#convertToLlmForSideRequest(messages: AgentMessage[]): Message[] {
|
|
5264
|
-
|
|
5263
|
+
const converted = convertToLlm(messages);
|
|
5264
|
+
return this.#obfuscator?.hasSecrets() ? obfuscateMessages(this.#obfuscator, converted) : converted;
|
|
5265
5265
|
}
|
|
5266
5266
|
|
|
5267
5267
|
/** Convert session messages using the same pre-LLM pipeline as the active session. */
|
|
@@ -6835,7 +6835,7 @@ export class AgentSession {
|
|
|
6835
6835
|
this.#pendingNextTurnMessages = [];
|
|
6836
6836
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
6837
6837
|
|
|
6838
|
-
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
6838
|
+
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel, this.configuredThinkingLevel());
|
|
6839
6839
|
this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
|
|
6840
6840
|
if (nextDiscoverySessionToolNames) {
|
|
6841
6841
|
await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, { persistMCPSelection: false });
|
|
@@ -7234,16 +7234,20 @@ export class AgentSession {
|
|
|
7234
7234
|
return;
|
|
7235
7235
|
}
|
|
7236
7236
|
|
|
7237
|
+
const wasAuto = this.#autoThinking;
|
|
7237
7238
|
this.#autoThinking = false;
|
|
7238
7239
|
this.#autoResolvedLevel = undefined;
|
|
7239
7240
|
const effectiveLevel = resolveThinkingLevelForModel(this.model, level);
|
|
7240
|
-
|
|
7241
|
+
// Leaving auto must persist even when the resolved effort is unchanged (e.g.
|
|
7242
|
+
// auto resolved to medium, then the user pins medium): otherwise the latest
|
|
7243
|
+
// session entry keeps `configured: "auto"` and resume re-enables auto.
|
|
7244
|
+
const isChanging = wasAuto || effectiveLevel !== this.#thinkingLevel;
|
|
7241
7245
|
|
|
7242
7246
|
this.#thinkingLevel = effectiveLevel;
|
|
7243
7247
|
this.#applyThinkingLevelToAgent(effectiveLevel);
|
|
7244
7248
|
|
|
7245
7249
|
if (isChanging) {
|
|
7246
|
-
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
|
7250
|
+
this.sessionManager.appendThinkingLevelChange(effectiveLevel, effectiveLevel);
|
|
7247
7251
|
if (persist && effectiveLevel !== undefined && effectiveLevel !== ThinkingLevel.Off) {
|
|
7248
7252
|
this.settings.set("defaultThinkingLevel", effectiveLevel);
|
|
7249
7253
|
}
|
|
@@ -7332,7 +7336,7 @@ export class AgentSession {
|
|
|
7332
7336
|
this.#thinkingLevel = effort;
|
|
7333
7337
|
this.#applyThinkingLevelToAgent(effort);
|
|
7334
7338
|
if (shouldPersistResolution) {
|
|
7335
|
-
this.sessionManager.appendThinkingLevelChange(effort);
|
|
7339
|
+
this.sessionManager.appendThinkingLevelChange(effort, AUTO_THINKING);
|
|
7336
7340
|
}
|
|
7337
7341
|
this.#emit({
|
|
7338
7342
|
type: "thinking_level_changed",
|
|
@@ -7852,8 +7856,8 @@ export class AgentSession {
|
|
|
7852
7856
|
compactionAbortController.signal,
|
|
7853
7857
|
{
|
|
7854
7858
|
promptOverride: this.#obfuscateTextForProvider(compactionPrep.hookPrompt),
|
|
7855
|
-
extraContext:
|
|
7856
|
-
remoteInstructions: this.#
|
|
7859
|
+
extraContext: compactionPrep.hookContext,
|
|
7860
|
+
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
7857
7861
|
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
7858
7862
|
},
|
|
7859
7863
|
);
|
|
@@ -8047,11 +8051,10 @@ export class AgentSession {
|
|
|
8047
8051
|
model,
|
|
8048
8052
|
this.#modelRegistry.resolver(model, this.sessionId),
|
|
8049
8053
|
{
|
|
8050
|
-
systemPrompt: this.#
|
|
8051
|
-
tools:
|
|
8052
|
-
this
|
|
8053
|
-
|
|
8054
|
-
),
|
|
8054
|
+
systemPrompt: this.#baseSystemPrompt,
|
|
8055
|
+
tools: this.#pruneToolDescriptions
|
|
8056
|
+
? stripToolDescriptions(this.agent.state.tools)
|
|
8057
|
+
: this.agent.state.tools,
|
|
8055
8058
|
customInstructions: this.#obfuscateTextForProvider(customInstructions),
|
|
8056
8059
|
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
8057
8060
|
initiatorOverride: "agent",
|
|
@@ -9739,8 +9742,8 @@ export class AgentSession {
|
|
|
9739
9742
|
autoCompactionSignal,
|
|
9740
9743
|
{
|
|
9741
9744
|
promptOverride: this.#obfuscateTextForProvider(compactionPrep.hookPrompt),
|
|
9742
|
-
extraContext:
|
|
9743
|
-
remoteInstructions: this.#
|
|
9745
|
+
extraContext: compactionPrep.hookContext,
|
|
9746
|
+
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
9744
9747
|
metadata: this.agent.metadataForProvider(candidate.provider),
|
|
9745
9748
|
initiatorOverride: "agent",
|
|
9746
9749
|
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
@@ -11366,7 +11369,7 @@ export class AgentSession {
|
|
|
11366
11369
|
}
|
|
11367
11370
|
if (event.type === "done") {
|
|
11368
11371
|
assistantMessage = this.#obfuscator?.hasSecrets()
|
|
11369
|
-
? { ...event.message, content: this.#obfuscator
|
|
11372
|
+
? { ...event.message, content: deobfuscateAssistantContent(this.#obfuscator, event.message.content) }
|
|
11370
11373
|
: event.message;
|
|
11371
11374
|
break;
|
|
11372
11375
|
}
|
|
@@ -11608,18 +11611,26 @@ export class AgentSession {
|
|
|
11608
11611
|
.some(entry => entry.type === "service_tier_change");
|
|
11609
11612
|
const defaultThinkingLevel = parseConfiguredThinkingLevel(this.settings.get("defaultThinkingLevel"));
|
|
11610
11613
|
const configuredServiceTier = this.settings.get("serviceTier");
|
|
11611
|
-
//
|
|
11612
|
-
//
|
|
11613
|
-
//
|
|
11614
|
-
//
|
|
11615
|
-
//
|
|
11616
|
-
//
|
|
11614
|
+
// Restore the thinking selector. Each change persists the configured
|
|
11615
|
+
// selector (`auto` or a concrete level), so prefer it: an `auto` session
|
|
11616
|
+
// resumes in auto mode (reclassifying the next turn) instead of freezing at
|
|
11617
|
+
// the last resolved level. Entries written before the `configured` field
|
|
11618
|
+
// existed fall back to the concrete level (legacy pin-on-resume behavior).
|
|
11619
|
+
// With no thinking entry, fall back to the global default so fresh sessions
|
|
11620
|
+
// still classify their first turn.
|
|
11621
|
+
const restoredConfigured = sessionContext.configuredThinkingLevel;
|
|
11617
11622
|
const restoredThinkingLevel: ConfiguredThinkingLevel | undefined =
|
|
11618
11623
|
hasThinkingEntry || (defaultThinkingLevel === AUTO_THINKING && sessionContext.thinkingLevel !== "off")
|
|
11619
|
-
?
|
|
11624
|
+
? restoredConfigured === AUTO_THINKING
|
|
11625
|
+
? AUTO_THINKING
|
|
11626
|
+
: (sessionContext.thinkingLevel as ThinkingLevel | undefined)
|
|
11620
11627
|
: defaultThinkingLevel;
|
|
11621
11628
|
if (restoredThinkingLevel === AUTO_THINKING) {
|
|
11622
11629
|
this.#autoThinking = true;
|
|
11630
|
+
// Resume in auto (pending) like a fresh auto session: the next user
|
|
11631
|
+
// turn reclassifies. We intentionally do not seed the last resolved
|
|
11632
|
+
// effort, so the cold (--continue) and in-app switch paths display
|
|
11633
|
+
// identically as `auto` until then.
|
|
11623
11634
|
this.#autoResolvedLevel = undefined;
|
|
11624
11635
|
this.#thinkingLevel = resolveProvisionalAutoLevel(this.model);
|
|
11625
11636
|
} else {
|
|
@@ -12513,9 +12524,12 @@ export class AgentSession {
|
|
|
12513
12524
|
* @returns Path to exported file
|
|
12514
12525
|
*/
|
|
12515
12526
|
async exportToHtml(outputPath?: string): Promise<string> {
|
|
12516
|
-
|
|
12527
|
+
// Public HTML export ships in the omp brand palette (collab-web
|
|
12528
|
+
// pink/purple), matching my.omp.sh — not the host's terminal theme.
|
|
12529
|
+
// Callers who want a themed export can pass `palette: "theme"` with
|
|
12530
|
+
// `themeName` directly to `exportSessionToHtml`.
|
|
12517
12531
|
const { exportSessionToHtml } = await import("../export/html");
|
|
12518
|
-
return exportSessionToHtml(this.sessionManager, this.state, { outputPath,
|
|
12532
|
+
return exportSessionToHtml(this.sessionManager, this.state, { outputPath, palette: "web" });
|
|
12519
12533
|
}
|
|
12520
12534
|
|
|
12521
12535
|
// =========================================================================
|
|
@@ -7,6 +7,8 @@ import { type CompactionEntry, EPHEMERAL_MODEL_CHANGE_ROLE, type SessionEntry }
|
|
|
7
7
|
export interface SessionContext {
|
|
8
8
|
messages: AgentMessage[];
|
|
9
9
|
thinkingLevel?: string;
|
|
10
|
+
/** Configured thinking selector (`"auto"` or a concrete level) from the latest change. */
|
|
11
|
+
configuredThinkingLevel?: string;
|
|
10
12
|
serviceTier?: ServiceTier;
|
|
11
13
|
/** Model roles: { default: "provider/modelId", small: "provider/modelId", ... } */
|
|
12
14
|
models: Record<string, string>;
|
|
@@ -134,6 +136,7 @@ export function buildSessionContext(
|
|
|
134
136
|
|
|
135
137
|
// Extract settings and find compaction
|
|
136
138
|
let thinkingLevel: string | undefined = "off";
|
|
139
|
+
let configuredThinkingLevel: string | undefined;
|
|
137
140
|
let serviceTier: ServiceTier | undefined;
|
|
138
141
|
const models: Record<string, string> = {};
|
|
139
142
|
let compaction: CompactionEntry | null = null;
|
|
@@ -154,6 +157,7 @@ export function buildSessionContext(
|
|
|
154
157
|
for (const entry of path) {
|
|
155
158
|
if (entry.type === "thinking_level_change") {
|
|
156
159
|
thinkingLevel = entry.thinkingLevel ?? "off";
|
|
160
|
+
configuredThinkingLevel = entry.configured ?? entry.thinkingLevel ?? undefined;
|
|
157
161
|
} else if (entry.type === "model_change") {
|
|
158
162
|
// New format: { model: "provider/id", role?: string }
|
|
159
163
|
if (entry.model) {
|
|
@@ -388,6 +392,7 @@ export function buildSessionContext(
|
|
|
388
392
|
messages,
|
|
389
393
|
cacheMissExplainedAt: options?.transcript ? cacheMissExplainedAt : undefined,
|
|
390
394
|
thinkingLevel,
|
|
395
|
+
configuredThinkingLevel,
|
|
391
396
|
serviceTier,
|
|
392
397
|
models,
|
|
393
398
|
injectedTtsrRules,
|