@oh-my-pi/pi-coding-agent 8.12.2 → 8.12.4
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/package.json +7 -10
- package/src/config/settings-manager.ts +36 -0
- package/src/config.ts +0 -5
- package/src/exec/bash-executor.ts +4 -4
- package/src/exec/exec.ts +9 -12
- package/src/extensibility/plugins/doctor.ts +0 -2
- package/src/ipy/kernel.ts +11 -13
- package/src/migrations.ts +1 -46
- package/src/modes/components/settings-defs.ts +23 -0
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/session/compaction/compaction.ts +37 -3
- package/src/ssh/ssh-executor.ts +3 -5
- package/src/system-prompt.ts +14 -9
- package/src/tools/ask.ts +38 -6
- package/src/tools/fetch.ts +9 -61
- package/src/tools/find.ts +19 -14
- package/src/tools/grep.ts +110 -562
- package/src/tools/read.ts +31 -25
- package/src/utils/image-convert.ts +7 -11
- package/src/utils/image-resize.ts +15 -25
- package/src/utils/tools-manager.ts +3 -43
- package/src/web/scrapers/utils.ts +11 -6
- package/src/web/scrapers/youtube.ts +21 -49
- package/src/utils/utils.ts +0 -1
- package/src/vendor/photon/LICENSE.md +0 -201
- package/src/vendor/photon/README.md +0 -158
- package/src/vendor/photon/index.d.ts +0 -3013
- package/src/vendor/photon/index.js +0 -4521
- package/src/vendor/photon/photon_rs_bg.wasm +0 -0
- package/src/vendor/photon/photon_rs_bg.wasm.d.ts +0 -193
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "8.12.
|
|
3
|
+
"version": "8.12.4",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -58,10 +58,6 @@
|
|
|
58
58
|
"types": "./src/internal-urls/*.ts",
|
|
59
59
|
"import": "./src/internal-urls/*.ts"
|
|
60
60
|
},
|
|
61
|
-
"./vendor/photon/*": {
|
|
62
|
-
"types": "./src/vendor/photon/*",
|
|
63
|
-
"import": "./src/vendor/photon/*"
|
|
64
|
-
},
|
|
65
61
|
"./*": {
|
|
66
62
|
"types": "./src/*.ts",
|
|
67
63
|
"import": "./src/*.ts"
|
|
@@ -83,11 +79,12 @@
|
|
|
83
79
|
"test": "bun test"
|
|
84
80
|
},
|
|
85
81
|
"dependencies": {
|
|
86
|
-
"@oh-my-pi/omp-stats": "8.12.
|
|
87
|
-
"@oh-my-pi/pi-agent-core": "8.12.
|
|
88
|
-
"@oh-my-pi/pi-ai": "8.12.
|
|
89
|
-
"@oh-my-pi/pi-
|
|
90
|
-
"@oh-my-pi/pi-
|
|
82
|
+
"@oh-my-pi/omp-stats": "8.12.4",
|
|
83
|
+
"@oh-my-pi/pi-agent-core": "8.12.4",
|
|
84
|
+
"@oh-my-pi/pi-ai": "8.12.4",
|
|
85
|
+
"@oh-my-pi/pi-natives": "8.12.4",
|
|
86
|
+
"@oh-my-pi/pi-tui": "8.12.4",
|
|
87
|
+
"@oh-my-pi/pi-utils": "8.12.4",
|
|
91
88
|
"@openai/agents": "^0.4.4",
|
|
92
89
|
"@sinclair/typebox": "^0.34.48",
|
|
93
90
|
"ajv": "^8.17.1",
|
|
@@ -72,6 +72,13 @@ export interface NotificationSettings {
|
|
|
72
72
|
onComplete?: NotificationMethod; // default: "auto"
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
export interface AskSettings {
|
|
76
|
+
/** Timeout in seconds for ask tool selections (0 or null to disable, default: 30) */
|
|
77
|
+
timeout?: number | null;
|
|
78
|
+
/** Notification method when ask tool is waiting for input (default: "auto") */
|
|
79
|
+
notification?: NotificationMethod;
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
export interface ExaSettings {
|
|
76
83
|
enabled?: boolean; // default: true (master toggle for all Exa tools)
|
|
77
84
|
enableSearch?: boolean; // default: true (search, deep, code, crawl)
|
|
@@ -242,6 +249,7 @@ export interface Settings {
|
|
|
242
249
|
showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME
|
|
243
250
|
normativeRewrite?: boolean; // default: false (rewrite tool call arguments to normalized format in session history)
|
|
244
251
|
readLineNumbers?: boolean; // default: false (prepend line numbers to read tool output by default)
|
|
252
|
+
ask?: AskSettings;
|
|
245
253
|
}
|
|
246
254
|
|
|
247
255
|
export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
|
|
@@ -307,6 +315,7 @@ const DEFAULT_SETTINGS: Settings = {
|
|
|
307
315
|
terminal: { showImages: true },
|
|
308
316
|
images: { autoResize: true },
|
|
309
317
|
notifications: { onComplete: "auto" },
|
|
318
|
+
ask: { timeout: 30, notification: "auto" },
|
|
310
319
|
exa: {
|
|
311
320
|
enabled: true,
|
|
312
321
|
enableSearch: true,
|
|
@@ -1117,6 +1126,33 @@ export class SettingsManager {
|
|
|
1117
1126
|
await this.save();
|
|
1118
1127
|
}
|
|
1119
1128
|
|
|
1129
|
+
/** Get ask tool timeout in milliseconds (0 or null = disabled) */
|
|
1130
|
+
getAskTimeout(): number | null {
|
|
1131
|
+
const timeout = this.settings.ask?.timeout;
|
|
1132
|
+
if (timeout === null || timeout === 0) return null;
|
|
1133
|
+
return (timeout ?? 30) * 1000;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
async setAskTimeout(seconds: number | null): Promise<void> {
|
|
1137
|
+
if (!this.globalSettings.ask) {
|
|
1138
|
+
this.globalSettings.ask = {};
|
|
1139
|
+
}
|
|
1140
|
+
this.globalSettings.ask.timeout = seconds;
|
|
1141
|
+
await this.save();
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
getAskNotification(): NotificationMethod {
|
|
1145
|
+
return this.settings.ask?.notification ?? "auto";
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async setAskNotification(method: NotificationMethod): Promise<void> {
|
|
1149
|
+
if (!this.globalSettings.ask) {
|
|
1150
|
+
this.globalSettings.ask = {};
|
|
1151
|
+
}
|
|
1152
|
+
this.globalSettings.ask.notification = method;
|
|
1153
|
+
await this.save();
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1120
1156
|
getImageAutoResize(): boolean {
|
|
1121
1157
|
return this.settings.images?.autoResize ?? true;
|
|
1122
1158
|
}
|
package/src/config.ts
CHANGED
|
@@ -93,11 +93,6 @@ export function getToolsDir(): string {
|
|
|
93
93
|
return path.join(getAgentDir(), "tools");
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/** Get path to managed binaries directory (fd, rg) */
|
|
97
|
-
export function getBinDir(): string {
|
|
98
|
-
return path.join(getAgentDir(), "bin");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
96
|
/** Get path to slash commands directory */
|
|
102
97
|
export function getCommandsDir(): string {
|
|
103
98
|
return path.join(getAgentDir(), "commands");
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides unified bash execution for AgentSession.executeBash() and direct calls.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { Exception, ptree } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { OutputSink } from "../session/streaming-output";
|
|
8
8
|
import { getShellConfig } from "../utils/shell";
|
|
9
9
|
import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
|
|
@@ -50,11 +50,12 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
50
50
|
artifactId: options?.artifactId,
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
using child = ptree.spawn([shell, ...args, finalCommand], {
|
|
54
54
|
cwd: options?.cwd,
|
|
55
55
|
env: finalEnv,
|
|
56
56
|
signal: options?.signal,
|
|
57
57
|
timeout: options?.timeout,
|
|
58
|
+
detached: true,
|
|
58
59
|
});
|
|
59
60
|
|
|
60
61
|
// Pump streams - errors during abort/timeout are expected
|
|
@@ -65,9 +66,8 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
65
66
|
|
|
66
67
|
// Wait for process exit
|
|
67
68
|
try {
|
|
68
|
-
await child.exited;
|
|
69
69
|
return {
|
|
70
|
-
exitCode: child.
|
|
70
|
+
exitCode: await child.exited,
|
|
71
71
|
cancelled: false,
|
|
72
72
|
...(await sink.dump()),
|
|
73
73
|
};
|
package/src/exec/exec.ts
CHANGED
|
@@ -35,22 +35,19 @@ export async function execCommand(
|
|
|
35
35
|
cwd: string,
|
|
36
36
|
options?: ExecOptions,
|
|
37
37
|
): Promise<ExecResult> {
|
|
38
|
-
const
|
|
38
|
+
const result = await ptree.exec([command, ...args], {
|
|
39
39
|
cwd,
|
|
40
40
|
signal: options?.signal,
|
|
41
41
|
timeout: options?.timeout,
|
|
42
|
+
allowNonZero: true,
|
|
43
|
+
allowAbort: true,
|
|
44
|
+
stderr: "full",
|
|
42
45
|
});
|
|
43
|
-
|
|
44
|
-
const [stdoutText, stderrText] = await Promise.all([proc.stdout.text(), proc.stderr.text()]);
|
|
45
|
-
try {
|
|
46
|
-
await proc.exited;
|
|
47
|
-
} catch {
|
|
48
|
-
// ChildProcess rejects on non-zero exit; we handle it below
|
|
49
|
-
}
|
|
46
|
+
|
|
50
47
|
return {
|
|
51
|
-
stdout:
|
|
52
|
-
stderr:
|
|
53
|
-
code:
|
|
54
|
-
killed:
|
|
48
|
+
stdout: result.stdout,
|
|
49
|
+
stderr: result.stderr,
|
|
50
|
+
code: result.exitCode ?? 0,
|
|
51
|
+
killed: Boolean(result.exitError?.aborted),
|
|
55
52
|
};
|
|
56
53
|
}
|
|
@@ -6,8 +6,6 @@ export async function runDoctorChecks(): Promise<DoctorCheck[]> {
|
|
|
6
6
|
|
|
7
7
|
// Check external tools
|
|
8
8
|
const tools = [
|
|
9
|
-
{ name: "fd", description: "File finder" },
|
|
10
|
-
{ name: "rg", description: "Ripgrep" },
|
|
11
9
|
{ name: "sd", description: "Find-replace" },
|
|
12
10
|
{ name: "sg", description: "AST-grep" },
|
|
13
11
|
{ name: "git", description: "Version control" },
|
package/src/ipy/kernel.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createServer } from "node:net";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
|
-
import {
|
|
3
|
+
import { logger, ptree } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { $ } from "bun";
|
|
5
5
|
import { nanoid } from "nanoid";
|
|
6
|
-
import { getShellConfig
|
|
6
|
+
import { getShellConfig } from "../utils/shell";
|
|
7
7
|
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
|
|
8
8
|
import { time } from "../utils/timings";
|
|
9
9
|
import { htmlToBasicMarkdown } from "../web/scrapers/types";
|
|
@@ -452,7 +452,7 @@ export function serializeWebSocketMessage(msg: JupyterMessage): ArrayBuffer {
|
|
|
452
452
|
export class PythonKernel {
|
|
453
453
|
readonly id: string;
|
|
454
454
|
readonly kernelId: string;
|
|
455
|
-
readonly gatewayProcess:
|
|
455
|
+
readonly gatewayProcess: ptree.ChildProcess | null;
|
|
456
456
|
readonly gatewayUrl: string;
|
|
457
457
|
readonly sessionId: string;
|
|
458
458
|
readonly username: string;
|
|
@@ -469,7 +469,7 @@ export class PythonKernel {
|
|
|
469
469
|
private constructor(
|
|
470
470
|
id: string,
|
|
471
471
|
kernelId: string,
|
|
472
|
-
gatewayProcess:
|
|
472
|
+
gatewayProcess: ptree.ChildProcess | null,
|
|
473
473
|
gatewayUrl: string,
|
|
474
474
|
sessionId: string,
|
|
475
475
|
username: string,
|
|
@@ -633,14 +633,14 @@ export class PythonKernel {
|
|
|
633
633
|
kernelEnv.PYTHONPATH = pythonPathParts;
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
-
let gatewayProcess:
|
|
636
|
+
let gatewayProcess: ptree.ChildProcess | null = null;
|
|
637
637
|
let gatewayUrl: string | null = null;
|
|
638
638
|
let lastError: string | null = null;
|
|
639
639
|
|
|
640
640
|
for (let attempt = 0; attempt < GATEWAY_STARTUP_ATTEMPTS; attempt += 1) {
|
|
641
641
|
const gatewayPort = await allocatePort();
|
|
642
642
|
const candidateUrl = `http://127.0.0.1:${gatewayPort}`;
|
|
643
|
-
const candidateProcess =
|
|
643
|
+
const candidateProcess = ptree.spawn(
|
|
644
644
|
[
|
|
645
645
|
runtime.pythonPath,
|
|
646
646
|
"-m",
|
|
@@ -653,10 +653,8 @@ export class PythonKernel {
|
|
|
653
653
|
],
|
|
654
654
|
{
|
|
655
655
|
cwd: options.cwd,
|
|
656
|
-
stdin: "ignore",
|
|
657
|
-
stdout: "pipe",
|
|
658
|
-
stderr: "pipe",
|
|
659
656
|
env: kernelEnv,
|
|
657
|
+
detached: true,
|
|
660
658
|
},
|
|
661
659
|
);
|
|
662
660
|
|
|
@@ -687,7 +685,7 @@ export class PythonKernel {
|
|
|
687
685
|
|
|
688
686
|
if (gatewayProcess && gatewayUrl) break;
|
|
689
687
|
|
|
690
|
-
|
|
688
|
+
candidateProcess.kill();
|
|
691
689
|
lastError = exited ? "Kernel gateway process exited during startup" : "Kernel gateway failed to start";
|
|
692
690
|
}
|
|
693
691
|
|
|
@@ -702,7 +700,7 @@ export class PythonKernel {
|
|
|
702
700
|
});
|
|
703
701
|
|
|
704
702
|
if (!createResponse.ok) {
|
|
705
|
-
|
|
703
|
+
gatewayProcess.kill();
|
|
706
704
|
throw new Error(`Failed to create kernel: ${await createResponse.text()}`);
|
|
707
705
|
}
|
|
708
706
|
|
|
@@ -1118,7 +1116,7 @@ export class PythonKernel {
|
|
|
1118
1116
|
await releaseSharedGateway();
|
|
1119
1117
|
} else if (this.gatewayProcess) {
|
|
1120
1118
|
try {
|
|
1121
|
-
|
|
1119
|
+
this.gatewayProcess.kill();
|
|
1122
1120
|
} catch (err: unknown) {
|
|
1123
1121
|
logger.warn("Failed to terminate gateway process", {
|
|
1124
1122
|
error: err instanceof Error ? err.message : String(err),
|
package/src/migrations.ts
CHANGED
|
@@ -5,7 +5,7 @@ import * as fs from "node:fs";
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import chalk from "chalk";
|
|
8
|
-
import { getAgentDbPath, getAgentDir
|
|
8
|
+
import { getAgentDbPath, getAgentDir } from "./config";
|
|
9
9
|
import { AgentStorage } from "./session/agent-storage";
|
|
10
10
|
import type { AuthCredential } from "./session/auth-storage";
|
|
11
11
|
|
|
@@ -144,50 +144,6 @@ export async function migrateSessionsFromAgentRoot(): Promise<void> {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
/**
|
|
148
|
-
* Move fd/rg binaries from tools/ to bin/ if they exist.
|
|
149
|
-
*/
|
|
150
|
-
async function migrateToolsToBin(): Promise<void> {
|
|
151
|
-
const agentDir = getAgentDir();
|
|
152
|
-
const toolsDir = path.join(agentDir, "tools");
|
|
153
|
-
const binDir = getBinDir();
|
|
154
|
-
|
|
155
|
-
if (!fs.existsSync(toolsDir)) return;
|
|
156
|
-
|
|
157
|
-
const binaries = ["fd", "rg", "fd.exe", "rg.exe"];
|
|
158
|
-
let movedAny = false;
|
|
159
|
-
|
|
160
|
-
for (const bin of binaries) {
|
|
161
|
-
const oldPath = path.join(toolsDir, bin);
|
|
162
|
-
const newPath = path.join(binDir, bin);
|
|
163
|
-
if (!fs.existsSync(oldPath)) continue;
|
|
164
|
-
|
|
165
|
-
if (!fs.existsSync(binDir)) {
|
|
166
|
-
await fs.promises.mkdir(binDir, { recursive: true });
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (!fs.existsSync(newPath)) {
|
|
170
|
-
try {
|
|
171
|
-
await fs.promises.rename(oldPath, newPath);
|
|
172
|
-
movedAny = true;
|
|
173
|
-
} catch (error) {
|
|
174
|
-
logger.warn("Failed to migrate binary", { from: oldPath, to: newPath, error: String(error) });
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
// Target exists, just delete the old one
|
|
178
|
-
try {
|
|
179
|
-
await fs.promises.rm(oldPath, { force: true });
|
|
180
|
-
} catch {
|
|
181
|
-
// Ignore
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (movedAny) {
|
|
187
|
-
console.log(chalk.green(`Migrated managed binaries tools/ → bin/`));
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
147
|
/**
|
|
192
148
|
* Run all migrations. Called once on startup.
|
|
193
149
|
*
|
|
@@ -201,7 +157,6 @@ export async function runMigrations(_cwd: string): Promise<{
|
|
|
201
157
|
// Then: run data migrations
|
|
202
158
|
const migratedAuthProviders = await migrateAuthToAgentDb();
|
|
203
159
|
await migrateSessionsFromAgentRoot();
|
|
204
|
-
await migrateToolsToBin();
|
|
205
160
|
|
|
206
161
|
return { migratedAuthProviders, deprecationWarnings: [] };
|
|
207
162
|
}
|
|
@@ -189,6 +189,29 @@ export const SETTINGS_DEFS: SettingDef[] = [
|
|
|
189
189
|
get: sm => sm.getNotificationOnComplete(),
|
|
190
190
|
set: (sm, v) => sm.setNotificationOnComplete(v as NotificationMethod),
|
|
191
191
|
},
|
|
192
|
+
{
|
|
193
|
+
id: "askTimeout",
|
|
194
|
+
tab: "behavior",
|
|
195
|
+
type: "enum",
|
|
196
|
+
label: "Ask tool timeout",
|
|
197
|
+
description: "Auto-select recommended option after timeout (disabled in plan mode)",
|
|
198
|
+
values: ["off", "15", "30", "60", "120"],
|
|
199
|
+
get: sm => {
|
|
200
|
+
const timeout = sm.getAskTimeout();
|
|
201
|
+
return timeout === null ? "off" : String(timeout / 1000);
|
|
202
|
+
},
|
|
203
|
+
set: (sm, v) => sm.setAskTimeout(v === "off" ? null : Number.parseInt(v, 10)),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "askNotification",
|
|
207
|
+
tab: "behavior",
|
|
208
|
+
type: "enum",
|
|
209
|
+
label: "Ask notification",
|
|
210
|
+
description: "Notify when ask tool is waiting for input",
|
|
211
|
+
values: ["auto", "bell", "osc99", "osc9", "off"],
|
|
212
|
+
get: sm => sm.getAskNotification(),
|
|
213
|
+
set: (sm, v) => sm.setAskNotification(v as NotificationMethod),
|
|
214
|
+
},
|
|
192
215
|
{
|
|
193
216
|
id: "startupQuiet",
|
|
194
217
|
tab: "behavior",
|
|
@@ -112,7 +112,7 @@ export class RpcClient {
|
|
|
112
112
|
args.push(...this.options.args);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
this.process = ptree.
|
|
115
|
+
this.process = ptree.spawn(["bun", cliPath, ...args], {
|
|
116
116
|
cwd: this.options.cwd,
|
|
117
117
|
env: { ...process.env, ...this.options.env },
|
|
118
118
|
stdin: "pipe",
|
|
@@ -140,7 +140,7 @@ export class RpcClient {
|
|
|
140
140
|
await Bun.sleep(100);
|
|
141
141
|
|
|
142
142
|
try {
|
|
143
|
-
const exitCode = await Promise.race([this.process.exited, Bun.sleep(
|
|
143
|
+
const exitCode = await Promise.race([this.process.exited, Bun.sleep(500).then(() => null)]);
|
|
144
144
|
if (exitCode !== null) {
|
|
145
145
|
throw new Error(
|
|
146
146
|
`Agent process exited immediately with code ${exitCode}. Stderr: ${this.process.peekStderr()}`,
|
|
@@ -154,11 +154,11 @@ export class RpcClient {
|
|
|
154
154
|
/**
|
|
155
155
|
* Stop the RPC agent process.
|
|
156
156
|
*/
|
|
157
|
-
|
|
157
|
+
stop() {
|
|
158
158
|
if (!this.process) return;
|
|
159
159
|
|
|
160
160
|
this.lineReader?.cancel();
|
|
161
|
-
|
|
161
|
+
this.process.kill();
|
|
162
162
|
|
|
163
163
|
this.process = null;
|
|
164
164
|
this.lineReader = null;
|
|
@@ -133,6 +133,14 @@ export function calculateContextTokens(usage: Usage): number {
|
|
|
133
133
|
return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
export function calculatePromptTokens(usage: Usage): number {
|
|
137
|
+
const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
138
|
+
if (promptTokens > 0) {
|
|
139
|
+
return promptTokens;
|
|
140
|
+
}
|
|
141
|
+
return calculateContextTokens(usage);
|
|
142
|
+
}
|
|
143
|
+
|
|
136
144
|
/**
|
|
137
145
|
* Get usage from an assistant message if available.
|
|
138
146
|
* Skips aborted and error messages as they don't have valid usage data.
|
|
@@ -237,6 +245,17 @@ export function estimateTokens(message: AgentMessage): number {
|
|
|
237
245
|
return 0;
|
|
238
246
|
}
|
|
239
247
|
|
|
248
|
+
function estimateEntriesTokens(entries: SessionEntry[], startIndex: number, endIndex: number): number {
|
|
249
|
+
let total = 0;
|
|
250
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
251
|
+
const msg = getMessageFromEntry(entries[i]);
|
|
252
|
+
if (msg) {
|
|
253
|
+
total += estimateTokens(msg);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return total;
|
|
257
|
+
}
|
|
258
|
+
|
|
240
259
|
/**
|
|
241
260
|
* Find valid cut points: indices of user, assistant, custom, or bashExecution messages.
|
|
242
261
|
* Never cut at tool results (they must follow their tool call).
|
|
@@ -616,8 +635,17 @@ export function prepareCompaction(
|
|
|
616
635
|
|
|
617
636
|
const lastUsage = getLastAssistantUsage(pathEntries);
|
|
618
637
|
const tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;
|
|
638
|
+
let keepRecentTokens = settings.keepRecentTokens;
|
|
639
|
+
if (lastUsage) {
|
|
640
|
+
const estimatedTokens = estimateEntriesTokens(pathEntries, boundaryStart, boundaryEnd);
|
|
641
|
+
const promptTokens = calculatePromptTokens(lastUsage);
|
|
642
|
+
const ratio = estimatedTokens > 0 ? promptTokens / estimatedTokens : 0;
|
|
643
|
+
if (Number.isFinite(ratio) && ratio > 1) {
|
|
644
|
+
keepRecentTokens = Math.max(1, Math.floor(keepRecentTokens / ratio));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
619
647
|
|
|
620
|
-
const cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd,
|
|
648
|
+
const cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, keepRecentTokens);
|
|
621
649
|
|
|
622
650
|
// Get UUID of first kept entry
|
|
623
651
|
const firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];
|
|
@@ -742,8 +770,8 @@ export async function compact(
|
|
|
742
770
|
]);
|
|
743
771
|
// Merge into single summary
|
|
744
772
|
summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
|
|
745
|
-
} else {
|
|
746
|
-
//
|
|
773
|
+
} else if (messagesToSummarize.length > 0) {
|
|
774
|
+
// Generate history summary from messages to summarize
|
|
747
775
|
summary = await generateSummary(
|
|
748
776
|
messagesToSummarize,
|
|
749
777
|
model,
|
|
@@ -754,6 +782,12 @@ export async function compact(
|
|
|
754
782
|
previousSummary,
|
|
755
783
|
summaryOptions,
|
|
756
784
|
);
|
|
785
|
+
} else if (previousSummary) {
|
|
786
|
+
// No new messages to summarize, preserve previous summary
|
|
787
|
+
summary = previousSummary;
|
|
788
|
+
} else {
|
|
789
|
+
// No messages and no previous summary
|
|
790
|
+
summary = "No prior history.";
|
|
757
791
|
}
|
|
758
792
|
|
|
759
793
|
const shortSummary = await generateShortSummary(
|
package/src/ssh/ssh-executor.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger, ptree } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { OutputSink } from "../session/streaming-output";
|
|
3
3
|
import { buildRemoteCommand, ensureConnection, ensureHostInfo, type SSHConnectionTarget } from "./connection-manager";
|
|
4
4
|
import { hasSshfs, mountRemote } from "./sshfs-mount";
|
|
@@ -76,7 +76,7 @@ export async function executeSSH(
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
using child = ptree.spawn(["ssh", ...(await buildRemoteCommand(host, resolvedCommand))], {
|
|
80
80
|
signal: options?.signal,
|
|
81
81
|
timeout: options?.timeout,
|
|
82
82
|
});
|
|
@@ -92,10 +92,8 @@ export async function executeSSH(
|
|
|
92
92
|
);
|
|
93
93
|
|
|
94
94
|
try {
|
|
95
|
-
await child.exited;
|
|
96
|
-
const exitCode = child.exitCode ?? 0;
|
|
97
95
|
return {
|
|
98
|
-
exitCode,
|
|
96
|
+
exitCode: await child.exited,
|
|
99
97
|
cancelled: false,
|
|
100
98
|
...(await sink.dump()),
|
|
101
99
|
};
|
package/src/system-prompt.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import * as fs from "node:fs/promises";
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
-
import {
|
|
7
|
+
import { find as wasmFind } from "@oh-my-pi/pi-natives";
|
|
8
|
+
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
9
|
import { $ } from "bun";
|
|
9
10
|
import chalk from "chalk";
|
|
10
11
|
import { contextFileCapability } from "./capability/context-file";
|
|
@@ -201,17 +202,21 @@ type ProjectTreeScan = {
|
|
|
201
202
|
const GLOB_TIMEOUT_MS = 5000;
|
|
202
203
|
|
|
203
204
|
/**
|
|
204
|
-
* Scan project tree using
|
|
205
|
-
* Returns null if
|
|
205
|
+
* Scan project tree using ripgrep-wasm find with exclusion filters.
|
|
206
|
+
* Returns null if scan fails.
|
|
206
207
|
*/
|
|
207
208
|
async function scanProjectTreeWithGlob(root: string): Promise<ProjectTreeScan | null> {
|
|
208
209
|
let entries: string[];
|
|
210
|
+
const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
|
|
209
211
|
try {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
const result = await untilAborted(timeoutSignal, () =>
|
|
213
|
+
wasmFind({
|
|
214
|
+
pattern: "**/*",
|
|
215
|
+
path: root,
|
|
216
|
+
fileType: "file",
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
entries = result.matches.map(match => match.path).filter(entry => entry.length > 0);
|
|
215
220
|
} catch {
|
|
216
221
|
return null;
|
|
217
222
|
}
|
|
@@ -222,7 +227,7 @@ async function scanProjectTreeWithGlob(root: string): Promise<ProjectTreeScan |
|
|
|
222
227
|
dirContents.set(root, new Map());
|
|
223
228
|
|
|
224
229
|
for (const entry of entries) {
|
|
225
|
-
const filePath = entry
|
|
230
|
+
const filePath = entry;
|
|
226
231
|
if (!filePath) continue;
|
|
227
232
|
const absolutePath = path.join(root, filePath);
|
|
228
233
|
// Check static ignores on path components
|