@oh-my-pi/pi-coding-agent 14.7.7 → 14.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [14.7.8] - 2026-05-08
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed indefinite startup hang on large repos introduced in 14.7.6 ([#975](https://github.com/can1357/oh-my-pi/issues/975)) on two fronts: (1) `createAgentSession` was awaiting `buildAgentsMdSearch` and `buildWorkspaceTree` directly in its blocking `Promise.all`, bypassing the existing 5s preparation deadline that previously protected startup — both scans are now raced against a 5s deadline and fall back to the system-prompt fallback path on timeout; (2) `buildWorkspaceTree` now derives its listing from `git ls-files --cached --others --exclude-standard` when the workspace is a git worktree, which is O(index size) and avoids the per-call full-tree gitignore-aware native scan that the previous implementation triggered. Repos without git, or where the call fails / times out, transparently fall back to the previous native-glob path.
|
|
10
|
+
|
|
5
11
|
## [14.7.6] - 2026-05-07
|
|
6
12
|
### Changed
|
|
7
13
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "14.7.
|
|
4
|
+
"version": "14.7.8",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
48
48
|
"@mozilla/readability": "^0.6.0",
|
|
49
|
-
"@oh-my-pi/omp-stats": "14.7.
|
|
50
|
-
"@oh-my-pi/pi-agent-core": "14.7.
|
|
51
|
-
"@oh-my-pi/pi-ai": "14.7.
|
|
52
|
-
"@oh-my-pi/pi-natives": "14.7.
|
|
53
|
-
"@oh-my-pi/pi-tui": "14.7.
|
|
54
|
-
"@oh-my-pi/pi-utils": "14.7.
|
|
49
|
+
"@oh-my-pi/omp-stats": "14.7.8",
|
|
50
|
+
"@oh-my-pi/pi-agent-core": "14.7.8",
|
|
51
|
+
"@oh-my-pi/pi-ai": "14.7.8",
|
|
52
|
+
"@oh-my-pi/pi-natives": "14.7.8",
|
|
53
|
+
"@oh-my-pi/pi-tui": "14.7.8",
|
|
54
|
+
"@oh-my-pi/pi-utils": "14.7.8",
|
|
55
55
|
"@puppeteer/browsers": "^2.13.0",
|
|
56
56
|
"@sinclair/typebox": "^0.34.49",
|
|
57
57
|
"@types/turndown": "5.0.6",
|
|
@@ -25,7 +25,9 @@ Use `irc` only when you need a quick answer from a peer; do not use it for long-
|
|
|
25
25
|
{{SECTION_SEPARATOR "Closure"}}
|
|
26
26
|
No TODO tracking, no progress updates. Execute, call `yield`, done.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Every turn **MUST** end with a tool call. A turn whose final block is plain text (or thinking only) is treated as a stop and you will be reminded to yield. While work remains, always continue with another tool call — investigate, edit, run, verify. Save narrative for the final `yield` payload.
|
|
29
|
+
|
|
30
|
+
When finished, you **MUST** call `yield` exactly once. This is like writing to a ticket: provide what is required and close it.
|
|
29
31
|
|
|
30
32
|
This is your only way to return a result. You **MUST NOT** put JSON in plain text, and you **MUST NOT** substitute a text summary for the structured `result.data` parameter.
|
|
31
33
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<system-reminder>
|
|
2
|
-
|
|
2
|
+
Your last turn ended without a tool call, so the session went idle. This is reminder {{retryCount}} of {{maxRetries}}.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Every turn **MUST** end with a tool call. Pick exactly one of:
|
|
5
|
+
1. **Resume the work** — if the assignment is not finished, call the next tool you would have called (edit, write, bash, search, etc.). Do **NOT** yield. Do **NOT** treat this reminder as a forced stop.
|
|
6
|
+
2. **Yield with success** — only if the assignment is genuinely complete: call `yield` with the structured payload in `result.data`.
|
|
7
|
+
3. **Yield with error** — only if you hit a real, concrete blocker you can name (missing file, unavailable API, contradictory spec). Describe what you tried and the exact blocker. Do **NOT** fabricate a "forced immediate-yield" or "system reminder required termination" reason — this reminder is not a blocker.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
Default to option 1 unless the work is actually done or actually blocked.
|
|
9
10
|
|
|
10
|
-
You **MUST NOT**
|
|
11
|
+
You **MUST NOT** end this turn with text only.
|
|
11
12
|
</system-reminder>
|
package/src/sdk.ts
CHANGED
|
@@ -898,10 +898,28 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
898
898
|
return { ttsrManager, rulebookRules, alwaysApplyRules };
|
|
899
899
|
});
|
|
900
900
|
|
|
901
|
+
// Resolve contextFiles up-front (it's needed before tool creation). The agentsMd / workspace tree
|
|
902
|
+
// scans are slowest on large repos and we MUST NOT block startup on them — race them against a
|
|
903
|
+
// short deadline. On timeout we forward `undefined` to ToolSession; buildSystemPromptInternal will
|
|
904
|
+
// re-race them through its own withDeadline path, and subagents will scan independently (still
|
|
905
|
+
// cheaper than an unbounded parent hang). Background work continues so caches still warm.
|
|
906
|
+
const STARTUP_SCAN_DEADLINE_MS = 5000;
|
|
907
|
+
const raceWithDeadline = <T>(name: string, work: Promise<T>): Promise<T | undefined> =>
|
|
908
|
+
Promise.race([
|
|
909
|
+
work,
|
|
910
|
+
Bun.sleep(STARTUP_SCAN_DEADLINE_MS).then(() => {
|
|
911
|
+
logger.warn("Startup scan exceeded deadline; deferring to system prompt fallback", {
|
|
912
|
+
name,
|
|
913
|
+
timeoutMs: STARTUP_SCAN_DEADLINE_MS,
|
|
914
|
+
cwd,
|
|
915
|
+
});
|
|
916
|
+
return undefined;
|
|
917
|
+
}),
|
|
918
|
+
]);
|
|
901
919
|
const [contextFiles, resolvedAgentsMdSearch, resolvedWorkspaceTree] = await Promise.all([
|
|
902
920
|
contextFilesPromise,
|
|
903
|
-
agentsMdSearchPromise,
|
|
904
|
-
workspaceTreePromise,
|
|
921
|
+
raceWithDeadline("buildAgentsMdSearch", agentsMdSearchPromise),
|
|
922
|
+
raceWithDeadline("buildWorkspaceTree", workspaceTreePromise),
|
|
905
923
|
]);
|
|
906
924
|
|
|
907
925
|
let agent: Agent;
|
package/src/task/executor.ts
CHANGED
|
@@ -1119,9 +1119,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1119
1119
|
maxRetries: MAX_YIELD_RETRIES,
|
|
1120
1120
|
});
|
|
1121
1121
|
|
|
1122
|
+
const isFinalRetry = retryCount >= MAX_YIELD_RETRIES;
|
|
1122
1123
|
await session.prompt(reminder, {
|
|
1123
1124
|
attribution: "agent",
|
|
1124
|
-
...(reminderToolChoice ? { toolChoice: reminderToolChoice } : {}),
|
|
1125
|
+
...(isFinalRetry && reminderToolChoice ? { toolChoice: reminderToolChoice } : {}),
|
|
1125
1126
|
});
|
|
1126
1127
|
await session.waitForIdle();
|
|
1127
1128
|
} catch (err) {
|
package/src/workspace-tree.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { glob } from "@oh-my-pi/pi-natives";
|
|
4
|
-
import { formatAge, formatBytes } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { $which, formatAge, formatBytes, logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
|
|
6
6
|
export interface DirectoryTree {
|
|
7
7
|
rootPath: string;
|
|
@@ -35,6 +35,13 @@ export interface DirectoryTreeOptions {
|
|
|
35
35
|
cache?: boolean;
|
|
36
36
|
/** Rendered label for the root line. */
|
|
37
37
|
rootLabel?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Pre-built map of `parentRelativePath` → child name set used in place of
|
|
40
|
+
* native directory listing. When provided, the tree builder consults this
|
|
41
|
+
* map for child enumeration instead of `glob` / `readdir`. Stat calls per
|
|
42
|
+
* displayed node are still performed for mtime/size/dir-ness.
|
|
43
|
+
*/
|
|
44
|
+
childIndex?: ReadonlyMap<string, ReadonlySet<string>>;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
const WORKSPACE_TREE_MAX_DEPTH = 3;
|
|
@@ -81,6 +88,7 @@ interface ResolvedDirectoryTreeOptions {
|
|
|
81
88
|
gitignore: boolean;
|
|
82
89
|
cache: boolean;
|
|
83
90
|
rootLabel: string;
|
|
91
|
+
childIndex: ReadonlyMap<string, ReadonlySet<string>> | null;
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
interface RenderLine {
|
|
@@ -122,6 +130,7 @@ function resolveDirectoryTreeOptions(options: DirectoryTreeOptions): ResolvedDir
|
|
|
122
130
|
gitignore: options.gitignore ?? false,
|
|
123
131
|
cache: options.cache ?? true,
|
|
124
132
|
rootLabel: options.rootLabel ?? ".",
|
|
133
|
+
childIndex: options.childIndex ?? null,
|
|
125
134
|
};
|
|
126
135
|
}
|
|
127
136
|
|
|
@@ -157,6 +166,10 @@ async function listDirectChildNames(
|
|
|
157
166
|
parent: DirectoryTreeNode,
|
|
158
167
|
options: ResolvedDirectoryTreeOptions,
|
|
159
168
|
): Promise<string[]> {
|
|
169
|
+
if (options.childIndex) {
|
|
170
|
+
const names = options.childIndex.get(parent.relativePath);
|
|
171
|
+
return names ? Array.from(names) : [];
|
|
172
|
+
}
|
|
160
173
|
if (!options.gitignore) {
|
|
161
174
|
const directoryPath = parent.relativePath ? path.join(rootPath, parent.relativePath) : rootPath;
|
|
162
175
|
return await fs.readdir(directoryPath);
|
|
@@ -377,19 +390,96 @@ export async function buildDirectoryTree(rootPath: string, options: DirectoryTre
|
|
|
377
390
|
};
|
|
378
391
|
}
|
|
379
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Build a `parentRelativePath` → child name index from a flat list of POSIX
|
|
395
|
+
* paths. Intermediate directory components are inferred from path segments;
|
|
396
|
+
* the index covers every ancestor directory implied by the input.
|
|
397
|
+
*/
|
|
398
|
+
function buildChildIndexFromPaths(paths: readonly string[]): Map<string, Set<string>> {
|
|
399
|
+
const index = new Map<string, Set<string>>();
|
|
400
|
+
const ensure = (parent: string): Set<string> => {
|
|
401
|
+
let bucket = index.get(parent);
|
|
402
|
+
if (!bucket) {
|
|
403
|
+
bucket = new Set<string>();
|
|
404
|
+
index.set(parent, bucket);
|
|
405
|
+
}
|
|
406
|
+
return bucket;
|
|
407
|
+
};
|
|
408
|
+
for (const raw of paths) {
|
|
409
|
+
if (!raw) continue;
|
|
410
|
+
const normalized = raw.replace(/\\/g, "/");
|
|
411
|
+
const parts = normalized.split("/").filter(segment => segment.length > 0);
|
|
412
|
+
if (parts.length === 0) continue;
|
|
413
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
414
|
+
const parent = parts.slice(0, i).join("/");
|
|
415
|
+
const segment = parts[i];
|
|
416
|
+
if (segment !== undefined) ensure(parent).add(segment);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return index;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const GIT_LS_FILES_TIMEOUT_MS = 3000;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* List tracked + untracked-not-ignored files at `rootPath` via `git ls-files`.
|
|
426
|
+
* Returns `null` when git is unavailable, the directory is not inside a
|
|
427
|
+
* worktree, or the call fails / times out — caller falls back to native
|
|
428
|
+
* directory listing.
|
|
429
|
+
*/
|
|
430
|
+
async function tryListGitFiles(rootPath: string): Promise<string[] | null> {
|
|
431
|
+
const gitPath = $which("git");
|
|
432
|
+
if (!gitPath) return null;
|
|
433
|
+
const signal = AbortSignal.timeout(GIT_LS_FILES_TIMEOUT_MS);
|
|
434
|
+
try {
|
|
435
|
+
const child = Bun.spawn([gitPath, "ls-files", "--cached", "--others", "--exclude-standard", "-z"], {
|
|
436
|
+
cwd: rootPath,
|
|
437
|
+
stdout: "pipe",
|
|
438
|
+
stderr: "pipe",
|
|
439
|
+
stdin: "ignore",
|
|
440
|
+
signal,
|
|
441
|
+
});
|
|
442
|
+
const [stdout, exitCode] = await Promise.all([
|
|
443
|
+
new Response(child.stdout as ReadableStream<Uint8Array>).text(),
|
|
444
|
+
child.exited,
|
|
445
|
+
]);
|
|
446
|
+
if (exitCode !== 0) return null;
|
|
447
|
+
if (!stdout) return [];
|
|
448
|
+
// `-z` separates entries with NUL; trailing NUL after final entry.
|
|
449
|
+
return stdout.split("\0").filter(entry => entry.length > 0);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
logger.debug("git ls-files failed; falling back to native directory listing", {
|
|
452
|
+
rootPath,
|
|
453
|
+
error: error instanceof Error ? error.message : String(error),
|
|
454
|
+
});
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
380
459
|
export async function buildWorkspaceTree(cwd: string): Promise<WorkspaceTree> {
|
|
381
460
|
const rootPath = path.resolve(cwd);
|
|
461
|
+
const baseOptions = {
|
|
462
|
+
maxDepth: WORKSPACE_TREE_MAX_DEPTH,
|
|
463
|
+
directoryEntryLimit: WORKSPACE_TREE_DIR_LIMIT,
|
|
464
|
+
lineCap: WORKSPACE_TREE_LINE_CAP,
|
|
465
|
+
excludedDirectoryNames: WORKSPACE_TREE_EXCLUDED_DIRS,
|
|
466
|
+
hidden: false,
|
|
467
|
+
cache: true,
|
|
468
|
+
rootLabel: ".",
|
|
469
|
+
} satisfies DirectoryTreeOptions;
|
|
470
|
+
|
|
382
471
|
try {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
472
|
+
const gitFiles = await tryListGitFiles(rootPath);
|
|
473
|
+
if (gitFiles !== null) {
|
|
474
|
+
// Git already applied gitignore + tracking semantics: bypass native
|
|
475
|
+
// recursive scan and feed the index directly to the tree builder.
|
|
476
|
+
return await buildDirectoryTree(rootPath, {
|
|
477
|
+
...baseOptions,
|
|
478
|
+
gitignore: false,
|
|
479
|
+
childIndex: buildChildIndexFromPaths(gitFiles),
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
return await buildDirectoryTree(rootPath, { ...baseOptions, gitignore: true });
|
|
393
483
|
} catch {
|
|
394
484
|
return emptyWorkspaceTree(rootPath);
|
|
395
485
|
}
|