@oh-my-pi/pi-coding-agent 17.3.1 → 17.3.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 +16 -0
- package/dist/{CHANGELOG-2swgsx9f.md → CHANGELOG-s5t1xdmy.md} +16 -0
- package/dist/cli.js +3357 -3355
- package/dist/docs-index.generated.txt +1 -1
- package/dist/types/cli/update-cli.d.ts +35 -1
- package/dist/types/session/turn-recovery.d.ts +1 -1
- package/package.json +13 -13
- package/src/cli/update-cli.ts +121 -25
- package/src/modes/controllers/event-controller.ts +57 -11
- package/src/prompts/system/empty-stop-retry.md +1 -1
- package/src/session/agent-session.ts +31 -10
- package/src/session/turn-recovery.ts +59 -55
- package/src/task/executor.ts +2 -1
- package/src/tools/read-format.ts +11 -10
- package/src/tools/run-scope.ts +4 -2
- package/src/utils/external-editor.ts +10 -11
- package/src/web/search/providers/gemini.ts +4 -9
|
@@ -112,6 +112,8 @@ interface UpdateMethodResolutionOptions {
|
|
|
112
112
|
miseBinDirs?: readonly string[];
|
|
113
113
|
miseDataDir?: string;
|
|
114
114
|
npmBinDir?: string;
|
|
115
|
+
/** Bun's configured global package directory, independent of its bin directory. */
|
|
116
|
+
bunGlobalDir?: string;
|
|
115
117
|
/**
|
|
116
118
|
* Whether the resolved omp path is a plain file (the standalone binary)
|
|
117
119
|
* rather than a package-manager symlink. Stops a binary install from being
|
|
@@ -119,8 +121,34 @@ interface UpdateMethodResolutionOptions {
|
|
|
119
121
|
* target directory.
|
|
120
122
|
*/
|
|
121
123
|
ompIsRegularFile?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Absolute path named by the bin entry's first symlink hop. This deliberately
|
|
126
|
+
* preserves a global package symlink instead of resolving into its checkout.
|
|
127
|
+
*/
|
|
128
|
+
ompLinkTarget?: string;
|
|
122
129
|
}
|
|
130
|
+
type UpdateTarget = {
|
|
131
|
+
method: "brew";
|
|
132
|
+
} | {
|
|
133
|
+
method: "mise";
|
|
134
|
+
} | {
|
|
135
|
+
method: "nix";
|
|
136
|
+
} | {
|
|
137
|
+
method: "bun";
|
|
138
|
+
path?: string;
|
|
139
|
+
} | {
|
|
140
|
+
method: "npm";
|
|
141
|
+
path?: string;
|
|
142
|
+
} | {
|
|
143
|
+
method: "binary";
|
|
144
|
+
path: string;
|
|
145
|
+
replacesSymlink: boolean;
|
|
146
|
+
};
|
|
123
147
|
export declare function resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined, options?: UpdateMethodResolutionOptions): UpdateMethod;
|
|
148
|
+
/** Resolve an update target from the concrete PATH entry selected by the shell. */
|
|
149
|
+
export declare function resolveUpdateTargetFromPath(ompPath: string, bunBinDir: string | undefined, options: UpdateMethodResolutionOptions & {
|
|
150
|
+
allowPackageManagers: boolean;
|
|
151
|
+
}): UpdateTarget;
|
|
124
152
|
/**
|
|
125
153
|
* Get the latest release info from the npm registry, following `omp.rename`
|
|
126
154
|
* pointers ({@link resolveReleaseRename}) when the package has moved to a new
|
|
@@ -145,7 +173,13 @@ interface BunInstallCachePruneResult {
|
|
|
145
173
|
* cache stays internally consistent.
|
|
146
174
|
*/
|
|
147
175
|
export declare function pruneBunInstallCache(cacheDir: string, packageNames?: Set<string>): Promise<BunInstallCachePruneResult>;
|
|
148
|
-
|
|
176
|
+
interface BunGlobalInstallLocations {
|
|
177
|
+
globalDir?: string;
|
|
178
|
+
globalBinDir?: string;
|
|
179
|
+
cacheDir?: string;
|
|
180
|
+
}
|
|
181
|
+
/** Resolve Bun's global node_modules root from explicit, default, or cache locations. */
|
|
182
|
+
export declare function resolveBunGlobalNodeModulesDirFromLocations({ globalDir, globalBinDir, cacheDir, }: BunGlobalInstallLocations): string | undefined;
|
|
149
183
|
/**
|
|
150
184
|
* Detect a musl-libc Linux host (Alpine, Void-musl) so self-update replaces a
|
|
151
185
|
* musl binary with the musl release asset instead of the glibc build, which
|
|
@@ -126,7 +126,7 @@ export declare class TurnRecovery {
|
|
|
126
126
|
/** Persists an otherwise skipped terminal empty error turn. */
|
|
127
127
|
persistTerminalEmptyErrorTurn(message: AssistantMessage): Promise<void>;
|
|
128
128
|
/** Handles empty terminal assistant turns and schedules bounded recovery. */
|
|
129
|
-
handleEmptyAssistantStop(message: AssistantMessage): Promise<
|
|
129
|
+
handleEmptyAssistantStop(message: AssistantMessage): Promise<"continue" | "terminal" | undefined>;
|
|
130
130
|
/** Classifies suspicious terminal stops and schedules bounded recovery. */
|
|
131
131
|
handleUnexpectedAssistantStop(message: AssistantMessage): Promise<boolean>;
|
|
132
132
|
/** Removes a persisted failed assistant turn after its persistence slot settles. */
|
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": "17.3.
|
|
4
|
+
"version": "17.3.3",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -50,18 +50,18 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@babel/parser": "^7.29.7",
|
|
53
|
-
"@oh-my-pi/hashline": "17.3.
|
|
54
|
-
"@oh-my-pi/omp-stats": "17.3.
|
|
55
|
-
"@oh-my-pi/omptype": "17.3.
|
|
56
|
-
"@oh-my-pi/pi-agent-core": "17.3.
|
|
57
|
-
"@oh-my-pi/pi-ai": "17.3.
|
|
58
|
-
"@oh-my-pi/pi-catalog": "17.3.
|
|
59
|
-
"@oh-my-pi/pi-mnemopi": "17.3.
|
|
60
|
-
"@oh-my-pi/pi-natives": "17.3.
|
|
61
|
-
"@oh-my-pi/pi-tui": "17.3.
|
|
62
|
-
"@oh-my-pi/pi-utils": "17.3.
|
|
63
|
-
"@oh-my-pi/pi-wire": "17.3.
|
|
64
|
-
"@oh-my-pi/snapcompact": "17.3.
|
|
53
|
+
"@oh-my-pi/hashline": "17.3.3",
|
|
54
|
+
"@oh-my-pi/omp-stats": "17.3.3",
|
|
55
|
+
"@oh-my-pi/omptype": "17.3.3",
|
|
56
|
+
"@oh-my-pi/pi-agent-core": "17.3.3",
|
|
57
|
+
"@oh-my-pi/pi-ai": "17.3.3",
|
|
58
|
+
"@oh-my-pi/pi-catalog": "17.3.3",
|
|
59
|
+
"@oh-my-pi/pi-mnemopi": "17.3.3",
|
|
60
|
+
"@oh-my-pi/pi-natives": "17.3.3",
|
|
61
|
+
"@oh-my-pi/pi-tui": "17.3.3",
|
|
62
|
+
"@oh-my-pi/pi-utils": "17.3.3",
|
|
63
|
+
"@oh-my-pi/pi-wire": "17.3.3",
|
|
64
|
+
"@oh-my-pi/snapcompact": "17.3.3",
|
|
65
65
|
"@opentelemetry/api": "^1.9.1",
|
|
66
66
|
"@opentelemetry/api-logs": "^0.220.0",
|
|
67
67
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -467,6 +467,27 @@ function isPathInDirectory(filePath: string, directoryPath: string): boolean {
|
|
|
467
467
|
return isPathInDirectoryLexical(resolvedFile, dirReal);
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
+
function isPathInManagerRoot(linkTarget: string, nodeModulesDir: string): boolean {
|
|
471
|
+
if (isPathInDirectoryLexical(linkTarget, nodeModulesDir)) return true;
|
|
472
|
+
// Resolve only the manager root. Resolving the link target itself would
|
|
473
|
+
// follow globally linked packages into their checkout and lose ownership.
|
|
474
|
+
const nodeModulesReal = tryRealpath(path.resolve(nodeModulesDir));
|
|
475
|
+
return nodeModulesReal !== undefined && isPathInDirectoryLexical(linkTarget, nodeModulesReal);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function resolveNpmGlobalNodeModulesDir(globalBinDir: string | undefined): string | undefined {
|
|
479
|
+
if (!globalBinDir) return undefined;
|
|
480
|
+
if (process.platform === "win32") return path.join(globalBinDir, "node_modules");
|
|
481
|
+
return path.join(path.dirname(globalBinDir), "lib", "node_modules");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function isManagerOwnedBinEntry(linkTarget: string | undefined, nodeModulesDir: string | undefined): boolean {
|
|
485
|
+
// Non-symlink launchers and unreadable links retain the existing bin-dir
|
|
486
|
+
// classification. A readable link must point through the manager's exact
|
|
487
|
+
// global node_modules tree.
|
|
488
|
+
return linkTarget === undefined || (nodeModulesDir !== undefined && isPathInManagerRoot(linkTarget, nodeModulesDir));
|
|
489
|
+
}
|
|
490
|
+
|
|
470
491
|
type UpdateMethod = "brew" | "mise" | "nix" | "bun" | "npm" | "binary";
|
|
471
492
|
|
|
472
493
|
interface UpdateMethodResolutionOptions {
|
|
@@ -474,6 +495,8 @@ interface UpdateMethodResolutionOptions {
|
|
|
474
495
|
miseBinDirs?: readonly string[];
|
|
475
496
|
miseDataDir?: string;
|
|
476
497
|
npmBinDir?: string;
|
|
498
|
+
/** Bun's configured global package directory, independent of its bin directory. */
|
|
499
|
+
bunGlobalDir?: string;
|
|
477
500
|
/**
|
|
478
501
|
* Whether the resolved omp path is a plain file (the standalone binary)
|
|
479
502
|
* rather than a package-manager symlink. Stops a binary install from being
|
|
@@ -481,6 +504,11 @@ interface UpdateMethodResolutionOptions {
|
|
|
481
504
|
* target directory.
|
|
482
505
|
*/
|
|
483
506
|
ompIsRegularFile?: boolean;
|
|
507
|
+
/**
|
|
508
|
+
* Absolute path named by the bin entry's first symlink hop. This deliberately
|
|
509
|
+
* preserves a global package symlink instead of resolving into its checkout.
|
|
510
|
+
*/
|
|
511
|
+
ompLinkTarget?: string;
|
|
484
512
|
}
|
|
485
513
|
|
|
486
514
|
type UpdateTarget =
|
|
@@ -496,7 +524,15 @@ function resolveUpdateMethod(
|
|
|
496
524
|
bunBinDir: string | undefined,
|
|
497
525
|
options: UpdateMethodResolutionOptions = {},
|
|
498
526
|
): UpdateMethod {
|
|
499
|
-
const {
|
|
527
|
+
const {
|
|
528
|
+
bunGlobalDir,
|
|
529
|
+
homebrewPrefix,
|
|
530
|
+
miseBinDirs = [],
|
|
531
|
+
miseDataDir,
|
|
532
|
+
npmBinDir,
|
|
533
|
+
ompIsRegularFile = false,
|
|
534
|
+
ompLinkTarget,
|
|
535
|
+
} = options;
|
|
500
536
|
const launcherExtension = path.extname(ompPath).toLowerCase();
|
|
501
537
|
const isWindowsScriptLauncher =
|
|
502
538
|
launcherExtension === ".cmd" || launcherExtension === ".ps1" || launcherExtension === ".bat";
|
|
@@ -514,9 +550,28 @@ function resolveUpdateMethod(
|
|
|
514
550
|
// (bun's .exe launcher, npm's .cmd/.ps1), so a regular file is NOT evidence
|
|
515
551
|
// of a standalone install and the override would hijack managed installs.
|
|
516
552
|
const isStandaloneRegularFile = ompIsRegularFile && process.platform !== "win32";
|
|
517
|
-
|
|
518
|
-
|
|
553
|
+
const bunNodeModulesDir = resolveBunGlobalNodeModulesDirFromLocations({
|
|
554
|
+
globalDir: bunGlobalDir,
|
|
555
|
+
globalBinDir: bunBinDir,
|
|
556
|
+
});
|
|
557
|
+
if (
|
|
558
|
+
bunBinDir &&
|
|
559
|
+
isPathInDirectory(ompPath, bunBinDir) &&
|
|
560
|
+
!isStandaloneRegularFile &&
|
|
561
|
+
isManagerOwnedBinEntry(ompLinkTarget, bunNodeModulesDir)
|
|
562
|
+
) {
|
|
563
|
+
return "bun";
|
|
564
|
+
}
|
|
565
|
+
const npmNodeModulesDir = resolveNpmGlobalNodeModulesDir(npmBinDir);
|
|
566
|
+
if (
|
|
567
|
+
npmBinDir &&
|
|
568
|
+
isPathInDirectory(ompPath, npmBinDir) &&
|
|
569
|
+
!isStandaloneRegularFile &&
|
|
570
|
+
isManagerOwnedBinEntry(ompLinkTarget, npmNodeModulesDir)
|
|
571
|
+
) {
|
|
519
572
|
return "npm";
|
|
573
|
+
}
|
|
574
|
+
if (isWindowsScriptLauncher) return "npm";
|
|
520
575
|
return "binary";
|
|
521
576
|
}
|
|
522
577
|
|
|
@@ -527,6 +582,44 @@ export function resolveUpdateMethodForTest(
|
|
|
527
582
|
): UpdateMethod {
|
|
528
583
|
return resolveUpdateMethod(ompPath, bunBinDir, options);
|
|
529
584
|
}
|
|
585
|
+
|
|
586
|
+
/** Resolve an update target from the concrete PATH entry selected by the shell. */
|
|
587
|
+
export function resolveUpdateTargetFromPath(
|
|
588
|
+
ompPath: string,
|
|
589
|
+
bunBinDir: string | undefined,
|
|
590
|
+
options: UpdateMethodResolutionOptions & { allowPackageManagers: boolean },
|
|
591
|
+
): UpdateTarget {
|
|
592
|
+
let ompIsRegularFile = false;
|
|
593
|
+
let ompIsSymlink = false;
|
|
594
|
+
let ompLinkTarget: string | undefined;
|
|
595
|
+
let ompRealpath: string | undefined;
|
|
596
|
+
try {
|
|
597
|
+
const stat = fs.lstatSync(ompPath);
|
|
598
|
+
ompIsRegularFile = stat.isFile() && !stat.isSymbolicLink();
|
|
599
|
+
ompIsSymlink = stat.isSymbolicLink();
|
|
600
|
+
if (ompIsSymlink) {
|
|
601
|
+
const rawTarget = fs.readlinkSync(ompPath);
|
|
602
|
+
const linkDir = path.dirname(ompPath);
|
|
603
|
+
ompLinkTarget = path.resolve(tryRealpath(linkDir) ?? linkDir, rawTarget);
|
|
604
|
+
ompRealpath = tryRealpath(ompPath);
|
|
605
|
+
}
|
|
606
|
+
} catch {}
|
|
607
|
+
|
|
608
|
+
const method = resolveUpdateMethod(ompPath, bunBinDir, {
|
|
609
|
+
...options,
|
|
610
|
+
ompIsRegularFile,
|
|
611
|
+
ompLinkTarget,
|
|
612
|
+
});
|
|
613
|
+
if (method === "binary") {
|
|
614
|
+
// A package-manager-enabled update follows a foreign alias to replace
|
|
615
|
+
// its standalone binary. Binary-only releases intentionally replace the
|
|
616
|
+
// selected manager launcher in place.
|
|
617
|
+
const binaryPath = options.allowPackageManagers && ompIsSymlink ? (ompRealpath ?? ompPath) : ompPath;
|
|
618
|
+
return { method, path: binaryPath, replacesSymlink: ompIsSymlink && binaryPath === ompPath };
|
|
619
|
+
}
|
|
620
|
+
if (method === "bun" || method === "npm") return { method, path: ompPath };
|
|
621
|
+
return { method };
|
|
622
|
+
}
|
|
530
623
|
/**
|
|
531
624
|
* Resolve how the running install should be updated.
|
|
532
625
|
*
|
|
@@ -546,27 +639,14 @@ async function resolveUpdateTarget(options: { allowPackageManagers: boolean }):
|
|
|
546
639
|
const ompPath = resolveOmpPath();
|
|
547
640
|
|
|
548
641
|
if (ompPath) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
// directory containment — distinguishes a binary install from npm/bun.
|
|
553
|
-
let ompIsRegularFile = false;
|
|
554
|
-
let ompIsSymlink = false;
|
|
555
|
-
try {
|
|
556
|
-
const stat = fs.lstatSync(ompPath);
|
|
557
|
-
ompIsRegularFile = stat.isFile() && !stat.isSymbolicLink();
|
|
558
|
-
ompIsSymlink = stat.isSymbolicLink();
|
|
559
|
-
} catch {}
|
|
560
|
-
const method = resolveUpdateMethod(ompPath, bunBinDir, {
|
|
642
|
+
return resolveUpdateTargetFromPath(ompPath, bunBinDir, {
|
|
643
|
+
allowPackageManagers: options.allowPackageManagers,
|
|
644
|
+
bunGlobalDir: options.allowPackageManagers ? process.env.BUN_INSTALL_GLOBAL_DIR : undefined,
|
|
561
645
|
homebrewPrefix,
|
|
562
646
|
miseBinDirs,
|
|
563
647
|
miseDataDir,
|
|
564
648
|
npmBinDir,
|
|
565
|
-
ompIsRegularFile,
|
|
566
649
|
});
|
|
567
|
-
if (method === "binary") return { method, path: ompPath, replacesSymlink: ompIsSymlink };
|
|
568
|
-
if (method === "bun" || method === "npm") return { method, path: ompPath };
|
|
569
|
-
return { method };
|
|
570
650
|
}
|
|
571
651
|
|
|
572
652
|
if (bunBinDir) return { method: "bun" };
|
|
@@ -795,10 +875,19 @@ async function resolveBunInstallCacheDir(): Promise<string | undefined> {
|
|
|
795
875
|
}
|
|
796
876
|
}
|
|
797
877
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
878
|
+
interface BunGlobalInstallLocations {
|
|
879
|
+
globalDir?: string;
|
|
880
|
+
globalBinDir?: string;
|
|
881
|
+
cacheDir?: string;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** Resolve Bun's global node_modules root from explicit, default, or cache locations. */
|
|
885
|
+
export function resolveBunGlobalNodeModulesDirFromLocations({
|
|
886
|
+
globalDir,
|
|
887
|
+
globalBinDir,
|
|
888
|
+
cacheDir,
|
|
889
|
+
}: BunGlobalInstallLocations): string | undefined {
|
|
890
|
+
if (globalDir && globalDir.length > 0) return path.join(globalDir, "node_modules");
|
|
802
891
|
if (globalBinDir && globalBinDir.length > 0) {
|
|
803
892
|
return path.join(path.dirname(globalBinDir), "install", "global", "node_modules");
|
|
804
893
|
}
|
|
@@ -812,9 +901,16 @@ async function resolveBunGlobalNodeModulesDir(cacheDir: string): Promise<string
|
|
|
812
901
|
try {
|
|
813
902
|
const result = await $`bun pm bin -g`.quiet().nothrow();
|
|
814
903
|
const globalBinDir = result.exitCode === 0 ? result.text().trim() : undefined;
|
|
815
|
-
return resolveBunGlobalNodeModulesDirFromLocations(
|
|
904
|
+
return resolveBunGlobalNodeModulesDirFromLocations({
|
|
905
|
+
globalDir: process.env.BUN_INSTALL_GLOBAL_DIR,
|
|
906
|
+
globalBinDir,
|
|
907
|
+
cacheDir,
|
|
908
|
+
});
|
|
816
909
|
} catch {
|
|
817
|
-
return resolveBunGlobalNodeModulesDirFromLocations(
|
|
910
|
+
return resolveBunGlobalNodeModulesDirFromLocations({
|
|
911
|
+
globalDir: process.env.BUN_INSTALL_GLOBAL_DIR,
|
|
912
|
+
cacheDir,
|
|
913
|
+
});
|
|
818
914
|
}
|
|
819
915
|
}
|
|
820
916
|
|
|
@@ -141,6 +141,8 @@ export class EventController {
|
|
|
141
141
|
// restored when the banner clears at the next `agent_start` (see
|
|
142
142
|
// #handleMessageEnd / #handleAgentStart).
|
|
143
143
|
#pinnedErrorComponent: AssistantMessageComponent | undefined = undefined;
|
|
144
|
+
#pinnedErrorMessage: AssistantMessage | undefined = undefined;
|
|
145
|
+
#restorePinnedErrorInline = true;
|
|
144
146
|
#retrySupersededAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
145
147
|
#retrySupersededAssistantQueue: AssistantMessageComponent[] = [];
|
|
146
148
|
// Set when `auto_retry_start` fires and cleared by `auto_retry_end` (both
|
|
@@ -650,6 +652,8 @@ export class EventController {
|
|
|
650
652
|
this.#readToolCallAssistantComponents.clear();
|
|
651
653
|
this.#lastAssistantComponent = undefined;
|
|
652
654
|
this.#pinnedErrorComponent = undefined;
|
|
655
|
+
this.#pinnedErrorMessage = undefined;
|
|
656
|
+
this.#restorePinnedErrorInline = true;
|
|
653
657
|
this.#retryPending = this.ctx.viewSession.isRetrying;
|
|
654
658
|
this.#cancelIdleCompaction();
|
|
655
659
|
this.#cancelIdleRecap();
|
|
@@ -743,10 +747,13 @@ export class EventController {
|
|
|
743
747
|
this.#resetReadGroup();
|
|
744
748
|
this.#resolveDisplaceableTodo();
|
|
745
749
|
this.#lastAssistantComponent = undefined;
|
|
746
|
-
// Restore
|
|
747
|
-
//
|
|
748
|
-
|
|
750
|
+
// Restore terminal errors in transcript history when their banner clears.
|
|
751
|
+
// Recoverable empty-output attempts are discarded by session recovery and
|
|
752
|
+
// must stay hidden rather than resurfacing as a stale inline error.
|
|
753
|
+
if (this.#restorePinnedErrorInline) this.#pinnedErrorComponent?.setErrorPinned(false);
|
|
749
754
|
this.#pinnedErrorComponent = undefined;
|
|
755
|
+
this.#pinnedErrorMessage = undefined;
|
|
756
|
+
this.#restorePinnedErrorInline = true;
|
|
750
757
|
this.ctx.clearPinnedError();
|
|
751
758
|
if (this.ctx.retryLoader) {
|
|
752
759
|
this.ctx.retryLoader.stop();
|
|
@@ -1320,14 +1327,20 @@ export class EventController {
|
|
|
1320
1327
|
}
|
|
1321
1328
|
this.ctx.streamingComponent = undefined;
|
|
1322
1329
|
this.ctx.streamingMessage = undefined;
|
|
1323
|
-
// Pin a turn-ending provider error
|
|
1324
|
-
//
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1330
|
+
// Pin a turn-ending provider error above the editor so it survives
|
|
1331
|
+
// transcript scroll and suppress its duplicate inline row. Empty-output
|
|
1332
|
+
// errors are known intermediate attempts: hide them entirely while
|
|
1333
|
+
// session recovery continues, but retain the component so a terminal
|
|
1334
|
+
// retry-cap event can promote its final error into the one banner.
|
|
1327
1335
|
if (event.message.stopReason === "error" && event.message.errorMessage && !isSilentAbort(event.message)) {
|
|
1336
|
+
const recoverableEmptyOutput =
|
|
1337
|
+
!event.message.errorMessage.startsWith("Retry budget exhausted") &&
|
|
1338
|
+
AIError.is(AIError.classifyMessage(event.message), AIError.Flag.EmptyResponse);
|
|
1328
1339
|
this.#lastAssistantComponent?.setErrorPinned(true);
|
|
1329
1340
|
this.#pinnedErrorComponent = this.#lastAssistantComponent;
|
|
1330
|
-
this
|
|
1341
|
+
this.#pinnedErrorMessage = event.message;
|
|
1342
|
+
this.#restorePinnedErrorInline = !recoverableEmptyOutput;
|
|
1343
|
+
if (!recoverableEmptyOutput) this.ctx.showPinnedError(event.message.errorMessage);
|
|
1331
1344
|
}
|
|
1332
1345
|
this.ctx.statusLine.invalidate();
|
|
1333
1346
|
this.ctx.ui.requestRender();
|
|
@@ -1947,6 +1960,8 @@ export class EventController {
|
|
|
1947
1960
|
// restore its inline Error row; just unpin the fixed-region banner so the
|
|
1948
1961
|
// retry UI is the visible state.
|
|
1949
1962
|
this.#pinnedErrorComponent = undefined;
|
|
1963
|
+
this.#pinnedErrorMessage = undefined;
|
|
1964
|
+
this.#restorePinnedErrorInline = true;
|
|
1950
1965
|
this.ctx.clearPinnedError();
|
|
1951
1966
|
}
|
|
1952
1967
|
const delaySeconds = Math.round(event.delayMs / 1000);
|
|
@@ -1968,20 +1983,51 @@ export class EventController {
|
|
|
1968
1983
|
this.ctx.retryLoader = undefined;
|
|
1969
1984
|
this.ctx.statusContainer.disposeChildren();
|
|
1970
1985
|
}
|
|
1986
|
+
const pinnedError = this.#pinnedErrorMessage?.errorMessage;
|
|
1987
|
+
const terminalFailurePinned =
|
|
1988
|
+
!event.success &&
|
|
1989
|
+
this.#pinnedErrorComponent !== undefined &&
|
|
1990
|
+
pinnedError !== undefined &&
|
|
1991
|
+
pinnedError === event.finalError;
|
|
1992
|
+
let stalePinnedErrorCleared = false;
|
|
1993
|
+
if (!event.success && this.#pinnedErrorComponent && !terminalFailurePinned) {
|
|
1994
|
+
this.#pinnedErrorComponent.setErrorPinned(false);
|
|
1995
|
+
this.#pinnedErrorComponent = undefined;
|
|
1996
|
+
this.#pinnedErrorMessage = undefined;
|
|
1997
|
+
this.#restorePinnedErrorInline = true;
|
|
1998
|
+
this.ctx.clearPinnedError();
|
|
1999
|
+
stalePinnedErrorCleared = true;
|
|
2000
|
+
}
|
|
1971
2001
|
let appliedRetryUpdate = false;
|
|
1972
2002
|
for (const retryError of event.retryErrors ?? []) {
|
|
1973
2003
|
const component = this.#takeRetrySupersededAssistantComponent(retryError.persistenceKey);
|
|
1974
2004
|
if (!component) continue;
|
|
1975
2005
|
component.applyRetryRecovery(retryError.retryRecovery);
|
|
1976
|
-
if (this.#pinnedErrorComponent === component)
|
|
2006
|
+
if (!terminalFailurePinned && this.#pinnedErrorComponent === component) {
|
|
2007
|
+
this.#pinnedErrorComponent = undefined;
|
|
2008
|
+
this.#pinnedErrorMessage = undefined;
|
|
2009
|
+
this.#restorePinnedErrorInline = true;
|
|
2010
|
+
}
|
|
1977
2011
|
appliedRetryUpdate = true;
|
|
1978
2012
|
}
|
|
1979
|
-
if (
|
|
2013
|
+
if (
|
|
2014
|
+
!terminalFailurePinned &&
|
|
2015
|
+
!stalePinnedErrorCleared &&
|
|
2016
|
+
(appliedRetryUpdate || (event.retryErrors?.length ?? 0) > 0)
|
|
2017
|
+
) {
|
|
1980
2018
|
this.ctx.clearPinnedError();
|
|
1981
2019
|
}
|
|
1982
2020
|
this.#clearRetrySupersededAssistantComponents();
|
|
1983
2021
|
if (!event.success) {
|
|
1984
|
-
|
|
2022
|
+
if (terminalFailurePinned) {
|
|
2023
|
+
const terminalError = this.#restorePinnedErrorInline
|
|
2024
|
+
? `Retry failed after ${event.attempt} attempts: ${event.finalError || pinnedError || "Unknown error"}`
|
|
2025
|
+
: (pinnedError ?? event.finalError);
|
|
2026
|
+
if (terminalError) this.ctx.showPinnedError(terminalError);
|
|
2027
|
+
this.#restorePinnedErrorInline = true;
|
|
2028
|
+
} else {
|
|
2029
|
+
this.ctx.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
|
|
2030
|
+
}
|
|
1985
2031
|
}
|
|
1986
2032
|
this.#ensureWorkingLoaderWhileStreaming();
|
|
1987
2033
|
this.ctx.ui.requestRender();
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
type AsideMessage,
|
|
38
38
|
type BeforeToolCallContext,
|
|
39
39
|
type BeforeToolCallResult,
|
|
40
|
+
EventLoopKeepalive,
|
|
40
41
|
resolveTelemetry,
|
|
41
42
|
type StreamFn,
|
|
42
43
|
TERMINAL_TOOL_RESULT_ABORT_REASON,
|
|
@@ -1213,15 +1214,28 @@ export class AgentSession {
|
|
|
1213
1214
|
}
|
|
1214
1215
|
},
|
|
1215
1216
|
scheduleIdleFlush: run => {
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1217
|
+
const keepalive = new EventLoopKeepalive();
|
|
1218
|
+
try {
|
|
1219
|
+
this.#schedulePostPromptTask(
|
|
1220
|
+
async () => {
|
|
1221
|
+
try {
|
|
1222
|
+
await run();
|
|
1223
|
+
} finally {
|
|
1224
|
+
keepalive[Symbol.dispose]();
|
|
1225
|
+
}
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
delayMs: 1,
|
|
1229
|
+
onSkip: () => {
|
|
1230
|
+
keepalive[Symbol.dispose]();
|
|
1231
|
+
this.yieldQueue.cancelIdleFlushScheduling();
|
|
1232
|
+
},
|
|
1233
|
+
},
|
|
1234
|
+
);
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
keepalive[Symbol.dispose]();
|
|
1237
|
+
throw error;
|
|
1238
|
+
}
|
|
1225
1239
|
},
|
|
1226
1240
|
});
|
|
1227
1241
|
this.yieldQueue.register<LaunchCompletionEntry>(LAUNCH_COMPLETION_MESSAGE_TYPE, {
|
|
@@ -2840,11 +2854,18 @@ export class AgentSession {
|
|
|
2840
2854
|
// tool_result and corrupts message history. The handler also
|
|
2841
2855
|
// schedules its own retry, so a real empty stop never needs the
|
|
2842
2856
|
// active-goal threshold pre-empt below.
|
|
2843
|
-
|
|
2857
|
+
const emptyOutputRecovery = await this.#recovery.handleEmptyAssistantStop(msg);
|
|
2858
|
+
if (emptyOutputRecovery === "continue") {
|
|
2844
2859
|
maintenanceRoute("empty-stop-handled");
|
|
2845
2860
|
await emitAgentEndNotification({ willContinue: true });
|
|
2846
2861
|
return;
|
|
2847
2862
|
}
|
|
2863
|
+
if (emptyOutputRecovery === "terminal") {
|
|
2864
|
+
// The cap already closed retry state and made provider-empty errors
|
|
2865
|
+
// non-retryable. Continue through terminal maintenance so session_stop
|
|
2866
|
+
// hooks and queued follow-up handling retain their normal contract.
|
|
2867
|
+
maintenanceRoute("empty-stop-retry-cap");
|
|
2868
|
+
}
|
|
2848
2869
|
|
|
2849
2870
|
// Record quota exhaustion before deciding whether this failed turn may be
|
|
2850
2871
|
// replayed. Visible/side-effecting output then remains terminal while its
|