oira666_pi-limits-wait 0.4.11 → 0.4.13
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/README.md +87 -87
- package/package.json +1 -1
- package/src/constants.ts +26 -23
- package/src/fetch-observer.ts +49 -49
- package/src/index.ts +5 -12
- package/src/models.ts +180 -180
- package/src/prompt.ts +41 -26
- package/src/retry-errors.ts +263 -259
- package/src/settings.ts +130 -130
- package/src/state.ts +39 -39
- package/src/stream.ts +354 -352
- package/src/types.ts +41 -41
- package/src/ui.ts +142 -142
package/src/types.ts
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
3
|
-
import type { Api, AssistantMessageEventStream, Context, Model, SimpleStreamOptions } from "@mariozechner/pi-ai";
|
|
4
|
-
|
|
5
|
-
export type RetryReason = "rate-limit" | "overloaded" | "authentication" | "model-frozen" | "network" | "retry";
|
|
6
|
-
|
|
7
|
-
export type RetryableError = {
|
|
8
|
-
reason: RetryReason;
|
|
9
|
-
waitMs: number;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type ConfiguredModel = {
|
|
13
|
-
provider: string;
|
|
14
|
-
modelname: string;
|
|
15
|
-
reasoningEffort?: ThinkingLevel;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type FallbackModel = {
|
|
19
|
-
model: Model<Api>;
|
|
20
|
-
reasoningEffort?: ThinkingLevel;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type RateLimitMemory = {
|
|
24
|
-
reason: RetryReason;
|
|
25
|
-
limitedAt: number;
|
|
26
|
-
deadline: number;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type NonRetryableFailureMemory = {
|
|
30
|
-
failedAt: number;
|
|
31
|
-
deadline: number;
|
|
32
|
-
errorMessage: string;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export type StreamSimpleFn = (
|
|
36
|
-
model: Model<Api>,
|
|
37
|
-
context: Context,
|
|
38
|
-
options?: SimpleStreamOptions,
|
|
39
|
-
) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
|
|
40
|
-
|
|
41
|
-
export type TestExtensionContext = ExtensionContext | undefined;
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
3
|
+
import type { Api, AssistantMessageEventStream, Context, Model, SimpleStreamOptions } from "@mariozechner/pi-ai";
|
|
4
|
+
|
|
5
|
+
export type RetryReason = "rate-limit" | "overloaded" | "authentication" | "model-frozen" | "network" | "retry";
|
|
6
|
+
|
|
7
|
+
export type RetryableError = {
|
|
8
|
+
reason: RetryReason;
|
|
9
|
+
waitMs: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ConfiguredModel = {
|
|
13
|
+
provider: string;
|
|
14
|
+
modelname: string;
|
|
15
|
+
reasoningEffort?: ThinkingLevel;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type FallbackModel = {
|
|
19
|
+
model: Model<Api>;
|
|
20
|
+
reasoningEffort?: ThinkingLevel;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type RateLimitMemory = {
|
|
24
|
+
reason: RetryReason;
|
|
25
|
+
limitedAt: number;
|
|
26
|
+
deadline: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type NonRetryableFailureMemory = {
|
|
30
|
+
failedAt: number;
|
|
31
|
+
deadline: number;
|
|
32
|
+
errorMessage: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type StreamSimpleFn = (
|
|
36
|
+
model: Model<Api>,
|
|
37
|
+
context: Context,
|
|
38
|
+
options?: SimpleStreamOptions,
|
|
39
|
+
) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
|
|
40
|
+
|
|
41
|
+
export type TestExtensionContext = ExtensionContext | undefined;
|
package/src/ui.ts
CHANGED
|
@@ -1,142 +1,142 @@
|
|
|
1
|
-
import { FREEZING_ENV_VAR } from "./constants.js";
|
|
2
|
-
import { state } from "./state.js";
|
|
3
|
-
import type { RetryReason } from "./types.js";
|
|
4
|
-
|
|
5
|
-
export function formatDuration(ms: number): string {
|
|
6
|
-
const totalSecs = Math.max(0, Math.ceil(ms / 1_000));
|
|
7
|
-
const hours = Math.floor(totalSecs / 3_600);
|
|
8
|
-
const mins = Math.floor((totalSecs % 3_600) / 60);
|
|
9
|
-
const secs = (totalSecs % 60).toString().padStart(2, "0");
|
|
10
|
-
return hours > 0 ? `${hours}h ${mins}m ${secs}s` : `${mins}m ${secs}s`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function formatErrorDetail(errorMessage: string, maxLength = 500): string {
|
|
14
|
-
const oneLine = errorMessage.replace(/\s+/g, " ").trim();
|
|
15
|
-
if (!oneLine) return "unknown error";
|
|
16
|
-
return oneLine.length > maxLength ? `${oneLine.slice(0, maxLength - 1)}…` : oneLine;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function reasonLabel(reason: RetryReason): string {
|
|
20
|
-
if (reason === "overloaded") return "Server overloaded";
|
|
21
|
-
if (reason === "authentication") return "Authentication refresh pending";
|
|
22
|
-
if (reason === "model-frozen") return "Model frozen after error";
|
|
23
|
-
if (reason === "network") return "Network/timeout error";
|
|
24
|
-
if (reason === "retry") return "Retrying after error";
|
|
25
|
-
return "Rate limited";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function freezingEnabled(): boolean {
|
|
29
|
-
const raw = process.env[FREEZING_ENV_VAR];
|
|
30
|
-
if (raw === undefined) return true;
|
|
31
|
-
const value = raw.trim().toLowerCase();
|
|
32
|
-
return !(value === "false" || value === "0" || value === "no" || value === "off");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function countdownText(reason: RetryReason, deadline: number, allowSkip: boolean): string {
|
|
36
|
-
const remaining = Math.max(0, deadline - Date.now());
|
|
37
|
-
const totalSecs = Math.ceil(remaining / 1_000);
|
|
38
|
-
const mins = Math.floor(totalSecs / 60);
|
|
39
|
-
const secs = (totalSecs % 60).toString().padStart(2, "0");
|
|
40
|
-
return (
|
|
41
|
-
`⏳ ${reasonLabel(reason)} — next retry in ${mins}m ${secs}s` +
|
|
42
|
-
(allowSkip ? " (Enter to retry now)" : "")
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function showAmbientRetryStatus(reason: RetryReason, waitMs: number): void {
|
|
47
|
-
const ctx = state.sharedCtx;
|
|
48
|
-
if (!ctx) return;
|
|
49
|
-
|
|
50
|
-
state.ambientStatusCleanup?.();
|
|
51
|
-
if (waitMs <= 0) return;
|
|
52
|
-
|
|
53
|
-
const deadline = Date.now() + waitMs;
|
|
54
|
-
const tick = () => {
|
|
55
|
-
if (Date.now() >= deadline) {
|
|
56
|
-
state.ambientStatusCleanup?.();
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
ctx.ui.setWorkingMessage(countdownText(reason, deadline, false));
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
tick();
|
|
63
|
-
const ticker = setInterval(tick, 1_000);
|
|
64
|
-
state.ambientStatusCleanup = () => {
|
|
65
|
-
clearInterval(ticker);
|
|
66
|
-
state.ambientStatusCleanup = undefined;
|
|
67
|
-
ctx.ui.setWorkingMessage();
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function clearAmbientRetryStatus(): void {
|
|
72
|
-
state.ambientStatusCleanup?.();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function clearModelStatus(): void {
|
|
76
|
-
state.modelStatusCleanup?.();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function waitForRetry(
|
|
80
|
-
reason: RetryReason,
|
|
81
|
-
waitMs: number,
|
|
82
|
-
signal?: AbortSignal,
|
|
83
|
-
): Promise<"waited" | "skipped" | "aborted"> {
|
|
84
|
-
if (waitMs <= 0) return Promise.resolve(signal?.aborted ? "aborted" : "waited");
|
|
85
|
-
|
|
86
|
-
return new Promise((resolve) => {
|
|
87
|
-
if (signal?.aborted) { resolve("aborted"); return; }
|
|
88
|
-
|
|
89
|
-
const ctx = state.sharedCtx;
|
|
90
|
-
const deadline = Date.now() + waitMs;
|
|
91
|
-
let done = false;
|
|
92
|
-
let unsubInput: (() => void) | undefined;
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
unsubInput = ctx?.ui.onTerminalInput((data) => {
|
|
96
|
-
if (done) return undefined;
|
|
97
|
-
if (data === "\r" || data === "\n") {
|
|
98
|
-
cleanup();
|
|
99
|
-
resolve("skipped");
|
|
100
|
-
return { consume: true };
|
|
101
|
-
}
|
|
102
|
-
if (data === "\x1b") {
|
|
103
|
-
cleanup();
|
|
104
|
-
resolve("aborted");
|
|
105
|
-
return { consume: true };
|
|
106
|
-
}
|
|
107
|
-
return undefined;
|
|
108
|
-
});
|
|
109
|
-
} catch { /* UI unavailable */ }
|
|
110
|
-
|
|
111
|
-
const onAbort = () => { if (!done) { cleanup(); resolve("aborted"); } };
|
|
112
|
-
signal?.addEventListener("abort", onAbort);
|
|
113
|
-
|
|
114
|
-
const tick = () => {
|
|
115
|
-
ctx?.ui.setWorkingMessage(countdownText(reason, deadline, Boolean(unsubInput)));
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
tick();
|
|
119
|
-
const ticker = setInterval(() => {
|
|
120
|
-
if (Date.now() >= deadline) { cleanup(); resolve("waited"); return; }
|
|
121
|
-
tick();
|
|
122
|
-
}, 1_000);
|
|
123
|
-
const timer = setTimeout(() => { if (!done) { cleanup(); resolve("waited"); } }, Math.max(0, deadline - Date.now()));
|
|
124
|
-
|
|
125
|
-
function cleanup() {
|
|
126
|
-
done = true;
|
|
127
|
-
clearInterval(ticker);
|
|
128
|
-
clearTimeout(timer);
|
|
129
|
-
signal?.removeEventListener("abort", onAbort);
|
|
130
|
-
unsubInput?.();
|
|
131
|
-
ctx?.ui.setWorkingMessage();
|
|
132
|
-
clearAmbientRetryStatus();
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function waitForRateLimit(
|
|
138
|
-
waitMs: number,
|
|
139
|
-
signal?: AbortSignal,
|
|
140
|
-
): Promise<"waited" | "skipped" | "aborted"> {
|
|
141
|
-
return waitForRetry("rate-limit", waitMs, signal);
|
|
142
|
-
}
|
|
1
|
+
import { FREEZING_ENV_VAR } from "./constants.js";
|
|
2
|
+
import { state } from "./state.js";
|
|
3
|
+
import type { RetryReason } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export function formatDuration(ms: number): string {
|
|
6
|
+
const totalSecs = Math.max(0, Math.ceil(ms / 1_000));
|
|
7
|
+
const hours = Math.floor(totalSecs / 3_600);
|
|
8
|
+
const mins = Math.floor((totalSecs % 3_600) / 60);
|
|
9
|
+
const secs = (totalSecs % 60).toString().padStart(2, "0");
|
|
10
|
+
return hours > 0 ? `${hours}h ${mins}m ${secs}s` : `${mins}m ${secs}s`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatErrorDetail(errorMessage: string, maxLength = 500): string {
|
|
14
|
+
const oneLine = errorMessage.replace(/\s+/g, " ").trim();
|
|
15
|
+
if (!oneLine) return "unknown error";
|
|
16
|
+
return oneLine.length > maxLength ? `${oneLine.slice(0, maxLength - 1)}…` : oneLine;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function reasonLabel(reason: RetryReason): string {
|
|
20
|
+
if (reason === "overloaded") return "Server overloaded";
|
|
21
|
+
if (reason === "authentication") return "Authentication refresh pending";
|
|
22
|
+
if (reason === "model-frozen") return "Model frozen after error";
|
|
23
|
+
if (reason === "network") return "Network/timeout error";
|
|
24
|
+
if (reason === "retry") return "Retrying after error";
|
|
25
|
+
return "Rate limited";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function freezingEnabled(): boolean {
|
|
29
|
+
const raw = process.env[FREEZING_ENV_VAR];
|
|
30
|
+
if (raw === undefined) return true;
|
|
31
|
+
const value = raw.trim().toLowerCase();
|
|
32
|
+
return !(value === "false" || value === "0" || value === "no" || value === "off");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function countdownText(reason: RetryReason, deadline: number, allowSkip: boolean): string {
|
|
36
|
+
const remaining = Math.max(0, deadline - Date.now());
|
|
37
|
+
const totalSecs = Math.ceil(remaining / 1_000);
|
|
38
|
+
const mins = Math.floor(totalSecs / 60);
|
|
39
|
+
const secs = (totalSecs % 60).toString().padStart(2, "0");
|
|
40
|
+
return (
|
|
41
|
+
`⏳ ${reasonLabel(reason)} — next retry in ${mins}m ${secs}s` +
|
|
42
|
+
(allowSkip ? " (Enter to retry now)" : "")
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function showAmbientRetryStatus(reason: RetryReason, waitMs: number): void {
|
|
47
|
+
const ctx = state.sharedCtx;
|
|
48
|
+
if (!ctx) return;
|
|
49
|
+
|
|
50
|
+
state.ambientStatusCleanup?.();
|
|
51
|
+
if (waitMs <= 0) return;
|
|
52
|
+
|
|
53
|
+
const deadline = Date.now() + waitMs;
|
|
54
|
+
const tick = () => {
|
|
55
|
+
if (Date.now() >= deadline) {
|
|
56
|
+
state.ambientStatusCleanup?.();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
ctx.ui.setWorkingMessage(countdownText(reason, deadline, false));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
tick();
|
|
63
|
+
const ticker = setInterval(tick, 1_000);
|
|
64
|
+
state.ambientStatusCleanup = () => {
|
|
65
|
+
clearInterval(ticker);
|
|
66
|
+
state.ambientStatusCleanup = undefined;
|
|
67
|
+
ctx.ui.setWorkingMessage();
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function clearAmbientRetryStatus(): void {
|
|
72
|
+
state.ambientStatusCleanup?.();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function clearModelStatus(): void {
|
|
76
|
+
state.modelStatusCleanup?.();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function waitForRetry(
|
|
80
|
+
reason: RetryReason,
|
|
81
|
+
waitMs: number,
|
|
82
|
+
signal?: AbortSignal,
|
|
83
|
+
): Promise<"waited" | "skipped" | "aborted"> {
|
|
84
|
+
if (waitMs <= 0) return Promise.resolve(signal?.aborted ? "aborted" : "waited");
|
|
85
|
+
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
if (signal?.aborted) { resolve("aborted"); return; }
|
|
88
|
+
|
|
89
|
+
const ctx = state.sharedCtx;
|
|
90
|
+
const deadline = Date.now() + waitMs;
|
|
91
|
+
let done = false;
|
|
92
|
+
let unsubInput: (() => void) | undefined;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
unsubInput = ctx?.ui.onTerminalInput((data) => {
|
|
96
|
+
if (done) return undefined;
|
|
97
|
+
if (data === "\r" || data === "\n") {
|
|
98
|
+
cleanup();
|
|
99
|
+
resolve("skipped");
|
|
100
|
+
return { consume: true };
|
|
101
|
+
}
|
|
102
|
+
if (data === "\x1b") {
|
|
103
|
+
cleanup();
|
|
104
|
+
resolve("aborted");
|
|
105
|
+
return { consume: true };
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
});
|
|
109
|
+
} catch { /* UI unavailable */ }
|
|
110
|
+
|
|
111
|
+
const onAbort = () => { if (!done) { cleanup(); resolve("aborted"); } };
|
|
112
|
+
signal?.addEventListener("abort", onAbort);
|
|
113
|
+
|
|
114
|
+
const tick = () => {
|
|
115
|
+
ctx?.ui.setWorkingMessage(countdownText(reason, deadline, Boolean(unsubInput)));
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
tick();
|
|
119
|
+
const ticker = setInterval(() => {
|
|
120
|
+
if (Date.now() >= deadline) { cleanup(); resolve("waited"); return; }
|
|
121
|
+
tick();
|
|
122
|
+
}, 1_000);
|
|
123
|
+
const timer = setTimeout(() => { if (!done) { cleanup(); resolve("waited"); } }, Math.max(0, deadline - Date.now()));
|
|
124
|
+
|
|
125
|
+
function cleanup() {
|
|
126
|
+
done = true;
|
|
127
|
+
clearInterval(ticker);
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
signal?.removeEventListener("abort", onAbort);
|
|
130
|
+
unsubInput?.();
|
|
131
|
+
ctx?.ui.setWorkingMessage();
|
|
132
|
+
clearAmbientRetryStatus();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function waitForRateLimit(
|
|
138
|
+
waitMs: number,
|
|
139
|
+
signal?: AbortSignal,
|
|
140
|
+
): Promise<"waited" | "skipped" | "aborted"> {
|
|
141
|
+
return waitForRetry("rate-limit", waitMs, signal);
|
|
142
|
+
}
|