@oh-my-pi/pi-coding-agent 13.11.1 → 13.12.3
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 +95 -0
- package/package.json +7 -7
- package/src/capability/context-file.ts +2 -0
- package/src/capability/extension-module.ts +1 -0
- package/src/capability/hook.ts +1 -0
- package/src/capability/index.ts +21 -10
- package/src/capability/instruction.ts +1 -0
- package/src/capability/mcp.ts +1 -0
- package/src/capability/prompt.ts +1 -0
- package/src/capability/rule.ts +5 -0
- package/src/capability/skill.ts +1 -0
- package/src/capability/slash-command.ts +1 -0
- package/src/capability/tool.ts +1 -0
- package/src/capability/types.ts +10 -0
- package/src/cli/commands/init-xdg.ts +27 -0
- package/src/cli/config-cli.ts +8 -3
- package/src/cli/shell-cli.ts +1 -1
- package/src/commands/config.ts +1 -1
- package/src/config/model-registry.ts +63 -10
- package/src/config/model-resolver.ts +84 -21
- package/src/config/settings-schema.ts +977 -769
- package/src/discovery/helpers.ts +8 -2
- package/src/exec/bash-executor.ts +62 -25
- package/src/extensibility/custom-tools/types.ts +2 -3
- package/src/extensibility/extensions/loader.ts +5 -1
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/hooks/types.ts +2 -0
- package/src/extensibility/plugins/loader.ts +23 -5
- package/src/extensibility/plugins/manager.ts +14 -0
- package/src/extensibility/plugins/types.ts +4 -0
- package/src/extensibility/skills.ts +7 -1
- package/src/index.ts +6 -6
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/ipy/kernel.ts +4 -5
- package/src/memories/index.ts +20 -7
- package/src/memories/storage.ts +46 -32
- package/src/modes/components/agent-dashboard.ts +23 -35
- package/src/modes/components/assistant-message.ts +25 -2
- package/src/modes/components/btw-panel.ts +104 -0
- package/src/modes/components/diff.ts +2 -7
- package/src/modes/components/extensions/state-manager.ts +3 -2
- package/src/modes/components/settings-defs.ts +56 -6
- package/src/modes/components/settings-selector.ts +11 -6
- package/src/modes/controllers/btw-controller.ts +193 -0
- package/src/modes/controllers/command-controller.ts +9 -3
- package/src/modes/controllers/event-controller.ts +4 -0
- package/src/modes/controllers/input-controller.ts +10 -1
- package/src/modes/interactive-mode.ts +22 -0
- package/src/modes/prompt-action-autocomplete.ts +17 -3
- package/src/modes/rpc/rpc-client.ts +30 -19
- package/src/modes/theme/theme.ts +28 -36
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +3 -0
- package/src/patch/diff.ts +9 -1
- package/src/patch/index.ts +56 -9
- package/src/prompts/system/btw-user.md +8 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/sdk.ts +23 -26
- package/src/session/agent-session.ts +65 -37
- package/src/session/blob-store.ts +32 -0
- package/src/session/compaction/compaction.ts +37 -6
- package/src/session/history-storage.ts +2 -2
- package/src/session/session-manager.ts +129 -49
- package/src/slash-commands/builtin-registry.ts +11 -0
- package/src/system-prompt.ts +4 -17
- package/src/task/agents.ts +1 -1
- package/src/task/index.ts +9 -8
- package/src/tools/browser.ts +11 -0
- package/src/tools/output-meta.ts +103 -3
- package/src/tools/path-utils.ts +11 -0
- package/src/utils/title-generator.ts +70 -92
- package/src/utils/tools-manager.ts +1 -1
- package/src/web/scrapers/index.ts +7 -7
- package/src/web/scrapers/utils.ts +1 -0
|
@@ -74,6 +74,21 @@ export function parseBlobRef(data: string): string | null {
|
|
|
74
74
|
return data.slice(BLOB_PREFIX.length);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** Identify provider transport image data URLs so persistence can externalize and restore them losslessly. */
|
|
78
|
+
export function isImageDataUrl(data: string): boolean {
|
|
79
|
+
return data.startsWith("data:image/") && data.includes(";base64,");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Externalize a provider image data URL to the blob store, returning a blob reference.
|
|
84
|
+
* The full data URL string is preserved so transport-native history can be reconstructed on resume.
|
|
85
|
+
*/
|
|
86
|
+
export async function externalizeImageDataUrl(blobStore: BlobStore, dataUrl: string): Promise<string> {
|
|
87
|
+
if (isBlobRef(dataUrl)) return dataUrl;
|
|
88
|
+
const { ref } = await blobStore.put(Buffer.from(dataUrl, "utf8"));
|
|
89
|
+
return ref;
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
/**
|
|
78
93
|
* Externalize an image's base64 data to the blob store, returning a blob reference.
|
|
79
94
|
* If the data is already a blob reference, returns it unchanged.
|
|
@@ -85,6 +100,23 @@ export async function externalizeImageData(blobStore: BlobStore, base64Data: str
|
|
|
85
100
|
return ref;
|
|
86
101
|
}
|
|
87
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Resolve an externalized provider image data URL back to its original string.
|
|
105
|
+
* If the data is not a blob reference, returns it unchanged.
|
|
106
|
+
* If the blob is missing, logs a warning and returns the reference as-is.
|
|
107
|
+
*/
|
|
108
|
+
export async function resolveImageDataUrl(blobStore: BlobStore, data: string): Promise<string> {
|
|
109
|
+
const hash = parseBlobRef(data);
|
|
110
|
+
if (!hash) return data;
|
|
111
|
+
|
|
112
|
+
const buffer = await blobStore.get(hash);
|
|
113
|
+
if (!buffer) {
|
|
114
|
+
logger.warn("Blob not found for persisted image data URL", { hash });
|
|
115
|
+
return data;
|
|
116
|
+
}
|
|
117
|
+
return buffer.toString("utf8");
|
|
118
|
+
}
|
|
119
|
+
|
|
88
120
|
/**
|
|
89
121
|
* Resolve a blob reference back to base64 data.
|
|
90
122
|
* If the data is not a blob reference, returns it unchanged.
|
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
* and after compaction the session is reloaded.
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
type AssistantMessage,
|
|
10
|
+
completeSimple,
|
|
11
|
+
Effort,
|
|
12
|
+
type MessageAttribution,
|
|
13
|
+
type Model,
|
|
14
|
+
type Usage,
|
|
15
|
+
} from "@oh-my-pi/pi-ai";
|
|
9
16
|
import {
|
|
10
17
|
CODEX_BASE_URL,
|
|
11
18
|
getCodexAccountId,
|
|
@@ -129,6 +136,7 @@ export interface CompactionSettings {
|
|
|
129
136
|
enabled: boolean;
|
|
130
137
|
strategy?: "context-full" | "handoff" | "off";
|
|
131
138
|
thresholdPercent?: number;
|
|
139
|
+
thresholdTokens?: number;
|
|
132
140
|
reserveTokens: number;
|
|
133
141
|
keepRecentTokens: number;
|
|
134
142
|
autoContinue?: boolean;
|
|
@@ -140,6 +148,7 @@ export const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {
|
|
|
140
148
|
enabled: true,
|
|
141
149
|
strategy: "context-full",
|
|
142
150
|
thresholdPercent: -1,
|
|
151
|
+
thresholdTokens: -1,
|
|
143
152
|
reserveTokens: 16384,
|
|
144
153
|
keepRecentTokens: 20000,
|
|
145
154
|
autoContinue: true,
|
|
@@ -211,6 +220,14 @@ export function shouldCompact(contextTokens: number, contextWindow: number, sett
|
|
|
211
220
|
}
|
|
212
221
|
|
|
213
222
|
function resolveThresholdTokens(contextWindow: number, settings: CompactionSettings): number {
|
|
223
|
+
// Fixed token limit takes priority over percentage
|
|
224
|
+
const thresholdTokens = settings.thresholdTokens;
|
|
225
|
+
if (typeof thresholdTokens === "number" && Number.isFinite(thresholdTokens) && thresholdTokens > 0) {
|
|
226
|
+
// Clamp to [1, contextWindow - 1] so there's always room
|
|
227
|
+
return Math.min(contextWindow - 1, Math.max(1, thresholdTokens));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Percentage-based threshold
|
|
214
231
|
const thresholdPercent = settings.thresholdPercent;
|
|
215
232
|
if (typeof thresholdPercent !== "number" || !Number.isFinite(thresholdPercent) || thresholdPercent <= 0) {
|
|
216
233
|
return contextWindow - effectiveReserveTokens(contextWindow, settings);
|
|
@@ -935,6 +952,7 @@ export interface SummaryOptions {
|
|
|
935
952
|
extraContext?: string[];
|
|
936
953
|
remoteEndpoint?: string;
|
|
937
954
|
remoteInstructions?: string;
|
|
955
|
+
initiatorOverride?: MessageAttribution;
|
|
938
956
|
}
|
|
939
957
|
|
|
940
958
|
export async function generateSummary(
|
|
@@ -990,7 +1008,7 @@ export async function generateSummary(
|
|
|
990
1008
|
const response = await completeSimple(
|
|
991
1009
|
model,
|
|
992
1010
|
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
|
|
993
|
-
{ maxTokens, signal, apiKey, reasoning: Effort.High },
|
|
1011
|
+
{ maxTokens, signal, apiKey, reasoning: Effort.High, initiatorOverride: options?.initiatorOverride },
|
|
994
1012
|
);
|
|
995
1013
|
|
|
996
1014
|
if (response.stopReason === "error") {
|
|
@@ -1039,7 +1057,7 @@ async function generateShortSummary(
|
|
|
1039
1057
|
systemPrompt: SUMMARIZATION_SYSTEM_PROMPT,
|
|
1040
1058
|
messages: [{ role: "user", content: [{ type: "text", text: promptText }], timestamp: Date.now() }],
|
|
1041
1059
|
},
|
|
1042
|
-
{ maxTokens, signal, apiKey, reasoning: Effort.High },
|
|
1060
|
+
{ maxTokens, signal, apiKey, reasoning: Effort.High, initiatorOverride: options?.initiatorOverride },
|
|
1043
1061
|
);
|
|
1044
1062
|
|
|
1045
1063
|
if (response.stopReason === "error") {
|
|
@@ -1218,6 +1236,7 @@ export async function compact(
|
|
|
1218
1236
|
extraContext: options?.extraContext,
|
|
1219
1237
|
remoteEndpoint: settings.remoteEnabled === false ? undefined : settings.remoteEndpoint,
|
|
1220
1238
|
remoteInstructions: options?.remoteInstructions,
|
|
1239
|
+
initiatorOverride: options?.initiatorOverride,
|
|
1221
1240
|
};
|
|
1222
1241
|
|
|
1223
1242
|
let preserveData = withOpenAiRemoteCompactionPreserveData(previousPreserveData, undefined);
|
|
@@ -1266,7 +1285,14 @@ export async function compact(
|
|
|
1266
1285
|
summaryOptions,
|
|
1267
1286
|
)
|
|
1268
1287
|
: Promise.resolve("No prior history."),
|
|
1269
|
-
generateTurnPrefixSummary(
|
|
1288
|
+
generateTurnPrefixSummary(
|
|
1289
|
+
turnPrefixMessages,
|
|
1290
|
+
model,
|
|
1291
|
+
settings.reserveTokens,
|
|
1292
|
+
apiKey,
|
|
1293
|
+
signal,
|
|
1294
|
+
summaryOptions.initiatorOverride,
|
|
1295
|
+
),
|
|
1270
1296
|
]);
|
|
1271
1297
|
// Merge into single summary
|
|
1272
1298
|
summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
|
|
@@ -1297,7 +1323,11 @@ export async function compact(
|
|
|
1297
1323
|
settings.reserveTokens,
|
|
1298
1324
|
apiKey,
|
|
1299
1325
|
signal,
|
|
1300
|
-
{
|
|
1326
|
+
{
|
|
1327
|
+
extraContext: options?.extraContext,
|
|
1328
|
+
remoteEndpoint: summaryOptions.remoteEndpoint,
|
|
1329
|
+
initiatorOverride: summaryOptions.initiatorOverride,
|
|
1330
|
+
},
|
|
1301
1331
|
);
|
|
1302
1332
|
|
|
1303
1333
|
// Compute file lists and append to summary
|
|
@@ -1327,6 +1357,7 @@ async function generateTurnPrefixSummary(
|
|
|
1327
1357
|
reserveTokens: number,
|
|
1328
1358
|
apiKey: string,
|
|
1329
1359
|
signal?: AbortSignal,
|
|
1360
|
+
initiatorOverride?: MessageAttribution,
|
|
1330
1361
|
): Promise<string> {
|
|
1331
1362
|
const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
|
|
1332
1363
|
|
|
@@ -1344,7 +1375,7 @@ async function generateTurnPrefixSummary(
|
|
|
1344
1375
|
const response = await completeSimple(
|
|
1345
1376
|
model,
|
|
1346
1377
|
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
|
|
1347
|
-
{ maxTokens, signal, apiKey, reasoning: Effort.High },
|
|
1378
|
+
{ maxTokens, signal, apiKey, reasoning: Effort.High, initiatorOverride },
|
|
1348
1379
|
);
|
|
1349
1380
|
|
|
1350
1381
|
if (response.stopReason === "error") {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Database, type Statement } from "bun:sqlite";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { getHistoryDbPath, logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
|
|
6
6
|
export interface HistoryEntry {
|
|
7
7
|
id: number;
|
|
@@ -78,7 +78,7 @@ END;
|
|
|
78
78
|
this.#lastPromptCache = last?.prompt ?? null;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
static open(dbPath: string =
|
|
81
|
+
static open(dbPath: string = getHistoryDbPath()): HistoryStorage {
|
|
82
82
|
if (!HistoryStorage.#instance) {
|
|
83
83
|
HistoryStorage.#instance = new HistoryStorage(dbPath);
|
|
84
84
|
}
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
getBlobsDir,
|
|
17
17
|
getAgentDir as getDefaultAgentDir,
|
|
18
18
|
getProjectDir,
|
|
19
|
+
getSessionsDir,
|
|
20
|
+
getTerminalSessionsDir,
|
|
19
21
|
isEnoent,
|
|
20
22
|
logger,
|
|
21
23
|
parseJsonlLenient,
|
|
@@ -23,7 +25,16 @@ import {
|
|
|
23
25
|
toError,
|
|
24
26
|
} from "@oh-my-pi/pi-utils";
|
|
25
27
|
import { ArtifactManager } from "./artifacts";
|
|
26
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
type BlobPutResult,
|
|
30
|
+
BlobStore,
|
|
31
|
+
externalizeImageData,
|
|
32
|
+
externalizeImageDataUrl,
|
|
33
|
+
isBlobRef,
|
|
34
|
+
isImageDataUrl,
|
|
35
|
+
resolveImageData,
|
|
36
|
+
resolveImageDataUrl,
|
|
37
|
+
} from "./blob-store";
|
|
27
38
|
import {
|
|
28
39
|
type BashExecutionMessage,
|
|
29
40
|
type CustomMessage,
|
|
@@ -348,7 +359,7 @@ function migrateHomeSessionDirs(): void {
|
|
|
348
359
|
const homeEncoded = home.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-");
|
|
349
360
|
const oldPrefix = `--${homeEncoded}-`;
|
|
350
361
|
const oldExact = `--${homeEncoded}--`;
|
|
351
|
-
const sessionsRoot =
|
|
362
|
+
const sessionsRoot = getSessionsDir();
|
|
352
363
|
|
|
353
364
|
let entries: string[];
|
|
354
365
|
try {
|
|
@@ -608,8 +619,7 @@ function encodeSessionDirName(cwd: string): string {
|
|
|
608
619
|
*/
|
|
609
620
|
function getDefaultSessionDir(cwd: string, storage: SessionStorage): string {
|
|
610
621
|
migrateHomeSessionDirs();
|
|
611
|
-
const
|
|
612
|
-
const sessionDir = path.join(getDefaultAgentDir(), "sessions", dirName);
|
|
622
|
+
const sessionDir = path.join(getSessionsDir(), encodeSessionDirName(cwd));
|
|
613
623
|
storage.ensureDirSync(sessionDir);
|
|
614
624
|
return sessionDir;
|
|
615
625
|
}
|
|
@@ -618,8 +628,6 @@ function getDefaultSessionDir(cwd: string, storage: SessionStorage): string {
|
|
|
618
628
|
// Terminal breadcrumbs: maps terminal (TTY) -> last session file for --continue
|
|
619
629
|
// =============================================================================
|
|
620
630
|
|
|
621
|
-
const TERMINAL_SESSIONS_DIR = "terminal-sessions";
|
|
622
|
-
|
|
623
631
|
/**
|
|
624
632
|
* Write a breadcrumb linking the current terminal to a session file.
|
|
625
633
|
* The breadcrumb contains the cwd and session path so --continue can
|
|
@@ -629,7 +637,7 @@ function writeTerminalBreadcrumb(cwd: string, sessionFile: string): void {
|
|
|
629
637
|
const terminalId = getTerminalId();
|
|
630
638
|
if (!terminalId) return;
|
|
631
639
|
|
|
632
|
-
const breadcrumbDir =
|
|
640
|
+
const breadcrumbDir = getTerminalSessionsDir();
|
|
633
641
|
const breadcrumbFile = path.join(breadcrumbDir, terminalId);
|
|
634
642
|
const content = `${cwd}\n${sessionFile}\n`;
|
|
635
643
|
// Best-effort — don't break session creation if breadcrumb fails
|
|
@@ -645,7 +653,7 @@ async function readTerminalBreadcrumb(cwd: string): Promise<string | null> {
|
|
|
645
653
|
if (!terminalId) return null;
|
|
646
654
|
|
|
647
655
|
try {
|
|
648
|
-
const breadcrumbFile = path.join(
|
|
656
|
+
const breadcrumbFile = path.join(getTerminalSessionsDir(), terminalId);
|
|
649
657
|
const content = await Bun.file(breadcrumbFile).text();
|
|
650
658
|
const lines = content.trim().split("\n");
|
|
651
659
|
if (lines.length < 2) return null;
|
|
@@ -691,35 +699,54 @@ export async function loadEntriesFromFile(
|
|
|
691
699
|
}
|
|
692
700
|
|
|
693
701
|
/**
|
|
694
|
-
* Resolve blob references in loaded entries,
|
|
695
|
-
*
|
|
702
|
+
* Resolve blob references in loaded entries, restoring both session image blocks and persisted
|
|
703
|
+
* provider image URLs back to the inline data expected by downstream transports. Mutates entries in place.
|
|
696
704
|
*/
|
|
705
|
+
function hasImageUrl(value: unknown): value is { image_url: string } {
|
|
706
|
+
return typeof value === "object" && value !== null && "image_url" in value && typeof value.image_url === "string";
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async function resolvePersistedImageUrlRefs(value: unknown, blobStore: BlobStore): Promise<void> {
|
|
710
|
+
if (Array.isArray(value)) {
|
|
711
|
+
await Promise.all(value.map(item => resolvePersistedImageUrlRefs(item, blobStore)));
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (typeof value !== "object" || value === null) return;
|
|
716
|
+
|
|
717
|
+
if (hasImageUrl(value) && isBlobRef(value.image_url)) {
|
|
718
|
+
value.image_url = await resolveImageDataUrl(blobStore, value.image_url);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
await Promise.all(Object.values(value).map(item => resolvePersistedImageUrlRefs(item, blobStore)));
|
|
722
|
+
}
|
|
723
|
+
|
|
697
724
|
async function resolveBlobRefsInEntries(entries: FileEntry[], blobStore: BlobStore): Promise<void> {
|
|
698
725
|
const promises: Promise<void>[] = [];
|
|
699
726
|
|
|
700
727
|
for (const entry of entries) {
|
|
701
728
|
if (entry.type === "session") continue;
|
|
702
729
|
|
|
703
|
-
// Resolve image blocks in message content arrays
|
|
704
730
|
let contentArray: unknown[] | undefined;
|
|
705
|
-
if (entry.type === "message") {
|
|
706
|
-
|
|
707
|
-
if (Array.isArray(content)) contentArray = content;
|
|
731
|
+
if (entry.type === "message" && "content" in entry.message && Array.isArray(entry.message.content)) {
|
|
732
|
+
contentArray = entry.message.content;
|
|
708
733
|
} else if (entry.type === "custom_message" && Array.isArray(entry.content)) {
|
|
709
734
|
contentArray = entry.content;
|
|
710
735
|
}
|
|
711
736
|
|
|
712
|
-
if (
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
737
|
+
if (contentArray) {
|
|
738
|
+
for (const block of contentArray) {
|
|
739
|
+
if (isImageBlock(block) && isBlobRef(block.data)) {
|
|
740
|
+
promises.push(
|
|
741
|
+
resolveImageData(blobStore, block.data).then(resolved => {
|
|
742
|
+
block.data = resolved;
|
|
743
|
+
}),
|
|
744
|
+
);
|
|
745
|
+
}
|
|
721
746
|
}
|
|
722
747
|
}
|
|
748
|
+
|
|
749
|
+
promises.push(resolvePersistedImageUrlRefs(entry, blobStore));
|
|
723
750
|
}
|
|
724
751
|
|
|
725
752
|
await Promise.all(promises);
|
|
@@ -891,19 +918,34 @@ function isImageBlock(value: unknown): value is { type: "image"; data: string; m
|
|
|
891
918
|
);
|
|
892
919
|
}
|
|
893
920
|
|
|
894
|
-
async function truncateForPersistence
|
|
921
|
+
async function truncateForPersistence(obj: FileEntry, blobStore: BlobStore, key?: string): Promise<FileEntry>;
|
|
922
|
+
async function truncateForPersistence(obj: string, blobStore: BlobStore, key?: string): Promise<string>;
|
|
923
|
+
async function truncateForPersistence(obj: unknown[], blobStore: BlobStore, key?: string): Promise<unknown[]>;
|
|
924
|
+
async function truncateForPersistence(obj: object, blobStore: BlobStore, key?: string): Promise<object>;
|
|
925
|
+
async function truncateForPersistence(
|
|
926
|
+
obj: null | undefined,
|
|
927
|
+
blobStore: BlobStore,
|
|
928
|
+
key?: string,
|
|
929
|
+
): Promise<null | undefined>;
|
|
930
|
+
async function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?: string): Promise<unknown> {
|
|
895
931
|
if (obj === null || obj === undefined) return obj;
|
|
896
932
|
|
|
897
933
|
if (typeof obj === "string") {
|
|
934
|
+
if (key === "image_url" && isImageDataUrl(obj)) {
|
|
935
|
+
return externalizeImageDataUrl(blobStore, obj);
|
|
936
|
+
}
|
|
937
|
+
|
|
898
938
|
if (obj.length > MAX_PERSIST_CHARS) {
|
|
899
939
|
// Cryptographic signatures must be preserved exactly or cleared entirely — never truncated.
|
|
900
940
|
// Truncation would produce an invalid signature that the API rejects.
|
|
901
941
|
if (key === "thinkingSignature" || key === "thoughtSignature" || key === "textSignature") {
|
|
902
|
-
return ""
|
|
942
|
+
return "";
|
|
903
943
|
}
|
|
944
|
+
|
|
904
945
|
const limit = Math.max(0, MAX_PERSIST_CHARS - TRUNCATION_NOTICE.length);
|
|
905
|
-
return `${truncateString(obj, limit)}${TRUNCATION_NOTICE}
|
|
946
|
+
return `${truncateString(obj, limit)}${TRUNCATION_NOTICE}`;
|
|
906
947
|
}
|
|
948
|
+
|
|
907
949
|
return obj;
|
|
908
950
|
}
|
|
909
951
|
|
|
@@ -919,34 +961,54 @@ async function truncateForPersistence<T>(obj: T, blobStore: BlobStore, key?: str
|
|
|
919
961
|
return { ...item, data: blobRef };
|
|
920
962
|
}
|
|
921
963
|
}
|
|
964
|
+
|
|
922
965
|
const newItem = await truncateForPersistence(item, blobStore, key);
|
|
923
966
|
if (newItem !== item) changed = true;
|
|
924
967
|
return newItem;
|
|
925
968
|
}),
|
|
926
969
|
);
|
|
927
|
-
return changed ?
|
|
970
|
+
return changed ? result : obj;
|
|
928
971
|
}
|
|
929
972
|
|
|
930
973
|
if (typeof obj === "object") {
|
|
931
974
|
let changed = false;
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
975
|
+
const entries: Array<readonly [string, unknown]> = await Promise.all(
|
|
976
|
+
Object.entries(obj).flatMap(([childKey, value]) => {
|
|
977
|
+
// Strip transient/redundant properties that shouldn't be persisted.
|
|
978
|
+
// - partialJson: streaming accumulator for tool call JSON parsing
|
|
979
|
+
// - jsonlEvents: raw subprocess streaming events (already saved to artifact files)
|
|
980
|
+
if (childKey === "partialJson" || childKey === "jsonlEvents") {
|
|
981
|
+
changed = true;
|
|
982
|
+
return [];
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
return [
|
|
986
|
+
(async () => {
|
|
987
|
+
const newValue = await truncateForPersistence(value, blobStore, childKey);
|
|
988
|
+
if (newValue !== value) changed = true;
|
|
989
|
+
return [childKey, newValue] as const;
|
|
990
|
+
})(),
|
|
991
|
+
];
|
|
992
|
+
}),
|
|
993
|
+
);
|
|
994
|
+
|
|
995
|
+
if (!changed) return obj;
|
|
996
|
+
|
|
997
|
+
const contentEntry = entries.find(([childKey]) => childKey === "content");
|
|
998
|
+
const lineCountEntry = entries.find(([childKey]) => childKey === "lineCount");
|
|
999
|
+
if (
|
|
1000
|
+
contentEntry &&
|
|
1001
|
+
typeof contentEntry[1] === "string" &&
|
|
1002
|
+
lineCountEntry &&
|
|
1003
|
+
typeof lineCountEntry[1] === "number"
|
|
1004
|
+
) {
|
|
1005
|
+
const content = contentEntry[1];
|
|
1006
|
+
const updatedEntries = entries.map(([childKey, value]) =>
|
|
1007
|
+
childKey === "lineCount" ? ([childKey, content.split("\n").length] as const) : ([childKey, value] as const),
|
|
1008
|
+
);
|
|
1009
|
+
return Object.fromEntries(updatedEntries);
|
|
948
1010
|
}
|
|
949
|
-
return
|
|
1011
|
+
return Object.fromEntries(entries);
|
|
950
1012
|
}
|
|
951
1013
|
|
|
952
1014
|
return obj;
|
|
@@ -1387,6 +1449,7 @@ export class SessionManager {
|
|
|
1387
1449
|
if (resolvedCwd === this.cwd) return;
|
|
1388
1450
|
|
|
1389
1451
|
const newSessionDir = getDefaultSessionDir(resolvedCwd, this.storage);
|
|
1452
|
+
let hadSessionFile = false;
|
|
1390
1453
|
|
|
1391
1454
|
if (this.persist && this.#sessionFile) {
|
|
1392
1455
|
// Close the persist writer before moving files
|
|
@@ -1399,12 +1462,16 @@ export class SessionManager {
|
|
|
1399
1462
|
const newSessionFile = path.join(newSessionDir, path.basename(oldSessionFile));
|
|
1400
1463
|
const oldArtifactDir = oldSessionFile.slice(0, -6); // strip .jsonl
|
|
1401
1464
|
const newArtifactDir = newSessionFile.slice(0, -6);
|
|
1465
|
+
hadSessionFile = this.storage.existsSync(oldSessionFile);
|
|
1402
1466
|
let movedSessionFile = false;
|
|
1403
1467
|
let movedArtifactDir = false;
|
|
1404
1468
|
|
|
1405
1469
|
try {
|
|
1406
|
-
|
|
1407
|
-
|
|
1470
|
+
// Guard: session file may not exist yet (no assistant messages persisted)
|
|
1471
|
+
if (hadSessionFile) {
|
|
1472
|
+
await fs.promises.rename(oldSessionFile, newSessionFile);
|
|
1473
|
+
movedSessionFile = true;
|
|
1474
|
+
}
|
|
1408
1475
|
|
|
1409
1476
|
try {
|
|
1410
1477
|
const stat = await fs.promises.stat(oldArtifactDir);
|
|
@@ -1449,8 +1516,12 @@ export class SessionManager {
|
|
|
1449
1516
|
header.cwd = resolvedCwd;
|
|
1450
1517
|
}
|
|
1451
1518
|
|
|
1452
|
-
// Rewrite the session file at its new location with updated header
|
|
1453
|
-
|
|
1519
|
+
// Rewrite the session file at its new location with updated header.
|
|
1520
|
+
// hadSessionFile: file existed before move → must rewrite to update cwd
|
|
1521
|
+
// hasAssistant: assistant messages in memory but file missing → recreate from memory
|
|
1522
|
+
// Neither true → fresh session, never written → preserve lazy-persist
|
|
1523
|
+
const hasAssistant = this.#fileEntries.some(e => e.type === "message" && e.message.role === "assistant");
|
|
1524
|
+
if (this.persist && this.#sessionFile && (hadSessionFile || hasAssistant)) {
|
|
1454
1525
|
await this.#rewriteFile();
|
|
1455
1526
|
}
|
|
1456
1527
|
|
|
@@ -1633,7 +1704,6 @@ export class SessionManager {
|
|
|
1633
1704
|
|
|
1634
1705
|
/** Flush pending writes to disk. Call before switching sessions or on shutdown. */
|
|
1635
1706
|
async flush(): Promise<void> {
|
|
1636
|
-
if (!this.#persistWriter) return;
|
|
1637
1707
|
await this.#queuePersistTask(async () => {
|
|
1638
1708
|
if (this.#persistWriter) {
|
|
1639
1709
|
await this.#persistWriter.flush();
|
|
@@ -1643,6 +1713,16 @@ export class SessionManager {
|
|
|
1643
1713
|
if (this.#persistError) throw this.#persistError;
|
|
1644
1714
|
}
|
|
1645
1715
|
|
|
1716
|
+
/** Close the persistent writer after flushing all pending data. */
|
|
1717
|
+
async close(): Promise<void> {
|
|
1718
|
+
if (!this.#persistWriter) return;
|
|
1719
|
+
await this.#queuePersistTask(async () => {
|
|
1720
|
+
await this.#closePersistWriterInternal();
|
|
1721
|
+
this.#flushed = true;
|
|
1722
|
+
});
|
|
1723
|
+
if (this.#persistError) throw this.#persistError;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1646
1726
|
getCwd(): string {
|
|
1647
1727
|
return this.cwd;
|
|
1648
1728
|
}
|
|
@@ -465,6 +465,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
465
465
|
runtime.ctx.editor.setText("");
|
|
466
466
|
},
|
|
467
467
|
},
|
|
468
|
+
{
|
|
469
|
+
name: "btw",
|
|
470
|
+
description: "Ask an ephemeral side question using the current session context",
|
|
471
|
+
inlineHint: "<question>",
|
|
472
|
+
allowArgs: true,
|
|
473
|
+
handle: async (command, runtime) => {
|
|
474
|
+
const question = command.text.slice(`/${command.name}`.length).trim();
|
|
475
|
+
runtime.ctx.editor.setText("");
|
|
476
|
+
await runtime.ctx.handleBtwCommand(question);
|
|
477
|
+
},
|
|
478
|
+
},
|
|
468
479
|
{
|
|
469
480
|
name: "background",
|
|
470
481
|
aliases: ["bg"],
|
package/src/system-prompt.ts
CHANGED
|
@@ -447,22 +447,9 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
447
447
|
skills = prepResult.value.skills;
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
const
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
month: "2-digit",
|
|
454
|
-
day: "2-digit",
|
|
455
|
-
});
|
|
456
|
-
const dateTime = now.toLocaleString("en-US", {
|
|
457
|
-
weekday: "long",
|
|
458
|
-
year: "numeric",
|
|
459
|
-
month: "long",
|
|
460
|
-
day: "numeric",
|
|
461
|
-
hour: "2-digit",
|
|
462
|
-
minute: "2-digit",
|
|
463
|
-
second: "2-digit",
|
|
464
|
-
timeZoneName: "short",
|
|
465
|
-
});
|
|
450
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
451
|
+
const dateTime = date;
|
|
452
|
+
const promptCwd = resolvedCwd.replace(/\\/g, "/");
|
|
466
453
|
|
|
467
454
|
// Build tool metadata for system prompt rendering
|
|
468
455
|
// Priority: explicit list > tools map > defaults
|
|
@@ -504,7 +491,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
504
491
|
rules: rules ?? [],
|
|
505
492
|
date,
|
|
506
493
|
dateTime,
|
|
507
|
-
cwd:
|
|
494
|
+
cwd: promptCwd,
|
|
508
495
|
intentTracing: !!intentField,
|
|
509
496
|
intentField: intentField ?? "",
|
|
510
497
|
eagerTasks,
|
package/src/task/agents.ts
CHANGED
|
@@ -53,7 +53,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
|
|
53
53
|
name: "task",
|
|
54
54
|
description: "General-purpose subagent with full capabilities for delegated multi-step tasks",
|
|
55
55
|
spawns: "*",
|
|
56
|
-
model: "
|
|
56
|
+
model: "pi/task",
|
|
57
57
|
thinkingLevel: Effort.Medium,
|
|
58
58
|
},
|
|
59
59
|
template: taskMd,
|
package/src/task/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ import type { Usage } from "@oh-my-pi/pi-ai";
|
|
|
20
20
|
import { $env, Snowflake } from "@oh-my-pi/pi-utils";
|
|
21
21
|
import { $ } from "bun";
|
|
22
22
|
import type { ToolSession } from "..";
|
|
23
|
-
import {
|
|
23
|
+
import { resolveAgentModelPatterns } from "../config/model-resolver";
|
|
24
24
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
25
25
|
import type { Theme } from "../modes/theme/theme";
|
|
26
26
|
import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
|
|
@@ -507,14 +507,15 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
507
507
|
: agent;
|
|
508
508
|
|
|
509
509
|
// Apply per-agent model override from settings (highest priority)
|
|
510
|
-
const agentModelOverrides = this.session.settings.get("task.agentModelOverrides")
|
|
510
|
+
const agentModelOverrides = this.session.settings.get("task.agentModelOverrides");
|
|
511
511
|
const settingsModelOverride = agentModelOverrides[agentName];
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
this.session.getActiveModelString?.()
|
|
517
|
-
this.session.getModelString?.()
|
|
512
|
+
const modelOverride = resolveAgentModelPatterns({
|
|
513
|
+
settingsOverride: settingsModelOverride,
|
|
514
|
+
agentModel: effectiveAgent.model,
|
|
515
|
+
settings: this.session.settings,
|
|
516
|
+
activeModelPattern: this.session.getActiveModelString?.(),
|
|
517
|
+
fallbackModelPattern: this.session.getModelString?.(),
|
|
518
|
+
});
|
|
518
519
|
const thinkingLevelOverride = effectiveAgent.thinkingLevel;
|
|
519
520
|
|
|
520
521
|
// Output schema priority: agent frontmatter > params > inherited from parent session
|
package/src/tools/browser.ts
CHANGED
|
@@ -526,6 +526,17 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
526
526
|
const proxy = process.env.PUPPETEER_PROXY;
|
|
527
527
|
if (proxy) {
|
|
528
528
|
launchArgs.push(`--proxy-server=${proxy}`);
|
|
529
|
+
// Chrome (since v72) bypasses proxies for localhost by default. When PUPPETEER_PROXY_BYPASS_LOOPBACK
|
|
530
|
+
// is true, add <-loopback> so traffic to localhost reaches the proxy (e.g. for mitmdump/auth capture).
|
|
531
|
+
const bypassLoopback = process.env.PUPPETEER_PROXY_BYPASS_LOOPBACK?.toLowerCase();
|
|
532
|
+
if (
|
|
533
|
+
bypassLoopback === "true" ||
|
|
534
|
+
bypassLoopback === "1" ||
|
|
535
|
+
bypassLoopback === "yes" ||
|
|
536
|
+
bypassLoopback === "on"
|
|
537
|
+
) {
|
|
538
|
+
launchArgs.push("--proxy-bypass-list=<-loopback>");
|
|
539
|
+
}
|
|
529
540
|
}
|
|
530
541
|
const ignoreCert = process.env.PUPPETEER_PROXY_IGNORE_CERT_ERRORS?.toLowerCase();
|
|
531
542
|
if (ignoreCert === "true" || ignoreCert === "1" || ignoreCert === "yes" || ignoreCert === "on") {
|