privateer-agent 0.12.21 → 0.12.23
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/bin/privateer-launch.mjs +2 -2
- package/extensions/privateer-privacy.ts +8 -4
- package/package.json +1 -1
- package/patches/@earendil-works+pi-coding-agent+0.84.1.patch +211 -18
- package/src/config/moat.ts +5 -1
- package/src/engine/errors.ts +130 -0
- package/src/permissions/classify.ts +14 -1
- package/src/providers/account.ts +10 -5
- package/src/providers/defaultModel.ts +42 -9
- package/src/providers/vision.ts +95 -0
- package/src/remote/librarySave.ts +156 -0
- package/src/remote/relayClient.ts +86 -0
- package/src/remote/remoteBridge.ts +73 -0
- package/src/tools/media.ts +299 -0
- package/src/tools/relayFileTools.ts +12 -1
- package/src/tools/saveToLibrary.ts +181 -0
package/bin/privateer-launch.mjs
CHANGED
|
@@ -405,11 +405,11 @@ else {
|
|
|
405
405
|
const signedIn = fs.existsSync(CRED);
|
|
406
406
|
// Mirrors TINFOIL_MODEL_ID in src/providers/defaultModel.ts — keep them in step; that
|
|
407
407
|
// file carries the measurements behind the choice.
|
|
408
|
-
const ACCOUNT_MODEL = "privateer/tinfoil/
|
|
408
|
+
const ACCOUNT_MODEL = "privateer/tinfoil/gemma4-31b";
|
|
409
409
|
const MODEL = process.env.PRIVATEER_MODEL
|
|
410
410
|
? process.env.PRIVATEER_MODEL
|
|
411
411
|
: haveTinfoilKey()
|
|
412
|
-
? "tinfoil/
|
|
412
|
+
? "tinfoil/gemma4-31b"
|
|
413
413
|
: signedIn
|
|
414
414
|
? ACCOUNT_MODEL
|
|
415
415
|
: haveKey("ANTHROPIC_API_KEY")
|
|
@@ -24,19 +24,20 @@
|
|
|
24
24
|
// display/resolution + routing list — posture and attestation are dispatcher-bound and
|
|
25
25
|
// unaffected by the model set.
|
|
26
26
|
import { registerAccountModels } from "../src/providers/account.ts";
|
|
27
|
+
import { visionInput } from "../src/providers/vision.ts";
|
|
27
28
|
import { privacyExtension } from "../src/config/privacyPolicy.ts";
|
|
28
29
|
|
|
29
|
-
// Tinfoil's live chat models (inference.tinfoil.sh/v1/models),
|
|
30
|
-
// launcher's default. Non-chat endpoints (embeddings, tts, whisper, websearch,
|
|
30
|
+
// Tinfoil's live chat models (inference.tinfoil.sh/v1/models), gemma4-31b first — the
|
|
31
|
+
// launcher's default, and the only one here that can see an image. Non-chat endpoints (embeddings, tts, whisper, websearch,
|
|
31
32
|
// doc-upload) are intentionally omitted. Refresh from the live catalog if Tinfoil adds
|
|
32
33
|
// models; this static list just needs to cover what we default to and commonly pick.
|
|
33
34
|
const TINFOIL_MODELS = [
|
|
35
|
+
"gemma4-31b",
|
|
34
36
|
"kimi-k2-6",
|
|
35
37
|
"glm-5-2",
|
|
36
38
|
"deepseek-v4-pro",
|
|
37
39
|
"gpt-oss-120b",
|
|
38
40
|
"gpt-oss-safeguard-120b",
|
|
39
|
-
"gemma4-31b",
|
|
40
41
|
"llama3-3-70b",
|
|
41
42
|
];
|
|
42
43
|
|
|
@@ -45,7 +46,10 @@ function tinfoilModel(id: string) {
|
|
|
45
46
|
id,
|
|
46
47
|
name: id,
|
|
47
48
|
reasoning: false,
|
|
48
|
-
|
|
49
|
+
// Tinfoil ids are bare here (`gemma4-31b`), so scope it before asking — the
|
|
50
|
+
// allowlist is written against full `provider/model` ids. Getting this wrong is
|
|
51
|
+
// not cosmetic: `input` is what Pi checks before it will send an image at all.
|
|
52
|
+
input: visionInput(`tinfoil/${id}`),
|
|
49
53
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
50
54
|
contextWindow: 128000,
|
|
51
55
|
maxTokens: 4096,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "privateer-agent",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.23",
|
|
4
4
|
"description": "Privacy-first terminal coding agent — bring your own model across 20 providers (Anthropic, OpenAI, OpenRouter, Google, local Ollama…). Safe-by-default permissions, MCP, sub-agents, workflows, and verifiable TEE inference. Built on the Pi toolkit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,10 +68,10 @@ index 4600b23..075ecae 100644
|
|
|
68
68
|
export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
|
|
69
69
|
export const ENV_SESSION_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_SESSION_DIR`;
|
|
70
70
|
diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js
|
|
71
|
-
index ce8a9a2..
|
|
71
|
+
index ce8a9a2..679e584 100644
|
|
72
72
|
--- a/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js
|
|
73
73
|
+++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js
|
|
74
|
-
@@ -38,6 +38,
|
|
74
|
+
@@ -38,6 +38,111 @@ import { createLocalBashOperations } from "./tools/bash.js";
|
|
75
75
|
import { createAllToolDefinitions } from "./tools/index.js";
|
|
76
76
|
import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
|
|
77
77
|
import { addUsageToTotals, createUsageTotals } from "./usage-totals.js";
|
|
@@ -138,11 +138,61 @@ index ce8a9a2..b7b339e 100644
|
|
|
138
138
|
+ return false;
|
|
139
139
|
+ const status = Number(m[1]);
|
|
140
140
|
+ return status >= 400 && status < 500 && !PV_TRANSIENT_CLIENT_STATUS.has(status);
|
|
141
|
+
+}
|
|
142
|
+
+// Privateer patch: throttle handling. Mirrors isThrottleFailure / retryAfterMs /
|
|
143
|
+
+// retryDelayMs in src/engine/errors.ts — see the incident note there.
|
|
144
|
+
+export function isThrottleFailure(text) {
|
|
145
|
+
+ return /^\s*429\b/.test(typeof text === "string" ? text : "");
|
|
146
|
+
+}
|
|
147
|
+
+const PV_MAX_RETRY_DELAY_MS = 60_000;
|
|
148
|
+
+const PV_RETRY_AFTER_PATTERNS = [
|
|
149
|
+
+ /retry-after(?:-ms)?["'\s:=]+(\d+(?:\.\d+)?)/i,
|
|
150
|
+
+ /(?:retry|try) again in (\d+(?:\.\d+)?)\s*(m?s|seconds?|minutes?)/i,
|
|
151
|
+
+ /retry after (\d+(?:\.\d+)?)\s*(m?s|seconds?|minutes?)/i,
|
|
152
|
+
+];
|
|
153
|
+
+export function retryAfterMs(text) {
|
|
154
|
+
+ const s = typeof text === "string" ? text : "";
|
|
155
|
+
+ for (const re of PV_RETRY_AFTER_PATTERNS) {
|
|
156
|
+
+ const m = re.exec(s);
|
|
157
|
+
+ if (!m)
|
|
158
|
+
+ continue;
|
|
159
|
+
+ const value = Number.parseFloat(m[1]);
|
|
160
|
+
+ if (!Number.isFinite(value) || value <= 0)
|
|
161
|
+
+ continue;
|
|
162
|
+
+ const unit = (m[2] ?? "").toLowerCase();
|
|
163
|
+
+ const ms = unit === "ms" || /retry-after-ms/i.test(m[0])
|
|
164
|
+
+ ? value
|
|
165
|
+
+ : unit.startsWith("m") && unit !== "ms"
|
|
166
|
+
+ ? value * 60_000
|
|
167
|
+
+ : value * 1000;
|
|
168
|
+
+ return Math.min(Math.max(Math.round(ms), 1_000), PV_MAX_RETRY_DELAY_MS);
|
|
169
|
+
+ }
|
|
170
|
+
+ return null;
|
|
171
|
+
+}
|
|
172
|
+
+// A server-stated delay wins outright — it is the only number here that is a fact.
|
|
173
|
+
+// The jitter is the point of the rest: without it every parallel request that tripped
|
|
174
|
+
+// the same limit wakes at the identical millisecond and trips it again.
|
|
175
|
+
+export function retryDelayMs(errorText, attempt, baseDelayMs) {
|
|
176
|
+
+ const stated = retryAfterMs(errorText);
|
|
177
|
+
+ if (stated != null)
|
|
178
|
+
+ return stated;
|
|
179
|
+
+ const n = Math.max(1, Math.floor(attempt));
|
|
180
|
+
+ const backoff = Math.min(baseDelayMs * 2 ** (n - 1), PV_MAX_RETRY_DELAY_MS);
|
|
181
|
+
+ return Math.round(backoff * (1 - Math.random() * 0.25));
|
|
141
182
|
+}
|
|
142
183
|
/**
|
|
143
184
|
* Parse a skill block from message text.
|
|
144
185
|
* Returns null if the text doesn't contain a skill block.
|
|
145
|
-
@@ -
|
|
186
|
+
@@ -102,6 +207,8 @@ export class AgentSession {
|
|
187
|
+
// Retry state
|
|
188
|
+
_retryAbortController = undefined;
|
|
189
|
+
_retryAttempt = 0;
|
|
190
|
+
+ // Privateer patch: when the provider last exhausted the retry budget on a 429.
|
|
191
|
+
+ _pvThrottledAt = 0;
|
|
192
|
+
// Bash execution state
|
|
193
|
+
_bashAbortControllers = new Set();
|
|
194
|
+
_pendingBashMessages = [];
|
|
195
|
+
@@ -186,6 +293,14 @@ export class AgentSession {
|
|
146
196
|
}
|
|
147
197
|
const isOAuth = this._modelRuntime.isUsingOAuth(model.provider);
|
|
148
198
|
if (isOAuth) {
|
|
@@ -157,7 +207,7 @@ index ce8a9a2..b7b339e 100644
|
|
|
157
207
|
throw new Error(`Authentication failed for "${model.provider}". ` +
|
|
158
208
|
`Credentials may have expired or network is unavailable. ` +
|
|
159
209
|
`Run '/login ${model.provider}' to re-authenticate.`);
|
|
160
|
-
@@ -360,6 +
|
|
210
|
+
@@ -360,6 +475,16 @@ export class AgentSession {
|
|
161
211
|
}
|
|
162
212
|
}
|
|
163
213
|
}
|
|
@@ -174,10 +224,18 @@ index ce8a9a2..b7b339e 100644
|
|
|
174
224
|
// Emit to extensions first
|
|
175
225
|
await this._emitExtensionEvent(event);
|
|
176
226
|
// Notify all listeners
|
|
177
|
-
@@ -772,6 +
|
|
227
|
+
@@ -772,6 +897,33 @@ export class AgentSession {
|
|
178
228
|
finalError: msg.errorMessage,
|
|
179
229
|
});
|
|
180
230
|
this._retryAttempt = 0;
|
|
231
|
+
+ // Privateer patch: remember that the endpoint is throttling us. The
|
|
232
|
+
+ // pre-prompt compaction in prompt() is an LLM call to this same endpoint,
|
|
233
|
+
+ // and firing it now just spends the summarizer budget on another 429 —
|
|
234
|
+
+ // which is what answered the user's next message with "Auto-compaction
|
|
235
|
+
+ // cancelled" instead of a reply. See the guard in prompt().
|
|
236
|
+
+ if (isThrottleFailure(msg.errorMessage)) {
|
|
237
|
+
+ this._pvThrottledAt = Date.now();
|
|
238
|
+
+ }
|
|
181
239
|
+ // Privateer patch: a transient error that survived the full retry budget
|
|
182
240
|
+ // is terminal. Ending the turn here (instead of falling through to
|
|
183
241
|
+ // compaction / queued-message continuation) prevents the agent loop from
|
|
@@ -200,7 +258,7 @@ index ce8a9a2..b7b339e 100644
|
|
|
200
258
|
}
|
|
201
259
|
if (await this._checkCompaction(msg)) {
|
|
202
260
|
return true;
|
|
203
|
-
@@ -852,6 +
|
|
261
|
+
@@ -852,6 +1004,14 @@ export class AgentSession {
|
|
204
262
|
if (!hasConfiguredAuth) {
|
|
205
263
|
const isOAuth = this._modelRuntime.isUsingOAuth(this.model.provider);
|
|
206
264
|
if (isOAuth) {
|
|
@@ -215,7 +273,22 @@ index ce8a9a2..b7b339e 100644
|
|
|
215
273
|
throw new Error(`Authentication failed for "${this.model.provider}". ` +
|
|
216
274
|
`Credentials may have expired or network is unavailable. ` +
|
|
217
275
|
`Run '/login ${this.model.provider}' to re-authenticate.`);
|
|
218
|
-
@@ -
|
|
276
|
+
@@ -861,7 +1021,13 @@ export class AgentSession {
|
|
277
|
+
// Check if we need to compact before sending (catches aborted responses).
|
|
278
|
+
// The user's new prompt is sent below, so do not call agent.continue() here.
|
|
279
|
+
const lastAssistant = this._findLastAssistantMessage();
|
|
280
|
+
- if (lastAssistant) {
|
|
281
|
+
+ // Privateer patch: skip this while the provider is still throttling us.
|
|
282
|
+
+ // Compaction here is another LLM call to the endpoint that just spent our
|
|
283
|
+
+ // whole retry budget on 429s; it cannot succeed, and its failure is reported
|
|
284
|
+
+ // as "Auto-compaction cancelled" — burying the user's actual prompt. One
|
|
285
|
+
+ // rate-limit window of patience, then we try again as normal. The context is
|
|
286
|
+
+ // not lost: the next turn re-checks compaction once the window has passed.
|
|
287
|
+
+ if (lastAssistant && Date.now() - this._pvThrottledAt >= PV_MAX_RETRY_DELAY_MS) {
|
|
288
|
+
await this._checkCompaction(lastAssistant, false);
|
|
289
|
+
}
|
|
290
|
+
// Build messages array (custom message if any, then user message)
|
|
291
|
+
@@ -2084,6 +2250,27 @@ export class AgentSession {
|
|
219
292
|
// Context overflow is handled by compaction, not retry.
|
|
220
293
|
if (isContextOverflow(message, this.model?.contextWindow ?? 0))
|
|
221
294
|
return false;
|
|
@@ -243,6 +316,19 @@ index ce8a9a2..b7b339e 100644
|
|
|
243
316
|
return isRetryableAssistantError(message);
|
|
244
317
|
}
|
|
245
318
|
/**
|
|
319
|
+
@@ -2129,7 +2316,11 @@ export class AgentSession {
|
|
320
|
+
this._retryAttempt--;
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
- const delayMs = settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
|
|
324
|
+
+ // Privateer patch: honour a server-stated `retry-after` and jitter the fallback.
|
|
325
|
+
+ // Stock Pi's fixed 2s/4s/8s ladder spends its whole budget inside a rate-limit
|
|
326
|
+
+ // window it was never long enough to outlast, and fires every parallel retry at
|
|
327
|
+
+ // the same instant. See retryDelayMs above.
|
|
328
|
+
+ const delayMs = retryDelayMs(message.errorMessage, this._retryAttempt, settings.baseDelayMs);
|
|
329
|
+
this._emit({
|
|
330
|
+
type: "auto_retry_start",
|
|
331
|
+
attempt: this._retryAttempt,
|
|
246
332
|
diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/auth-guidance.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/auth-guidance.js
|
|
247
333
|
index 197bccc..bc9ac3f 100644
|
|
248
334
|
--- a/node_modules/@earendil-works/pi-coding-agent/dist/core/auth-guidance.js
|
|
@@ -681,7 +767,7 @@ index 97af6cd..74d5d52 100644
|
|
|
681
767
|
}
|
|
682
768
|
const globalPath = join(this.agentDir, "APPEND_SYSTEM.md");
|
|
683
769
|
diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js
|
|
684
|
-
index cb06c10..
|
|
770
|
+
index cb06c10..41fb90c 100644
|
|
685
771
|
--- a/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js
|
|
686
772
|
+++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js
|
|
687
773
|
@@ -2,7 +2,7 @@ import { randomUUID } from "crypto";
|
|
@@ -822,6 +908,23 @@ index cb06c10..930a0de 100644
|
|
|
822
908
|
const dir = dirname(path);
|
|
823
909
|
let release;
|
|
824
910
|
try {
|
|
911
|
+
@@ -568,7 +672,15 @@ export class SettingsManager {
|
|
912
|
+
getProviderRetrySettings() {
|
|
913
|
+
return {
|
|
914
|
+
timeoutMs: this.settings.retry?.provider?.timeoutMs,
|
|
915
|
+
- maxRetries: this.settings.retry?.provider?.maxRetries,
|
|
916
|
+
+ // Privateer patch: give the provider-level retry a real default. pi-ai's
|
|
917
|
+
+ // retryProviderRequest is the only layer that reads `retry-after` and jitters,
|
|
918
|
+
+ // but it takes `options.maxRetries ?? 0` and stock Pi hands it `undefined` — so
|
|
919
|
+
+ // it ran ZERO retries and every throttle fell through to the session-level loop
|
|
920
|
+
+ // (3 attempts, fixed 2s/4s/8s, no retry-after), which cannot outlast a 60s rate
|
|
921
|
+
+ // limit window. A bare `429` from the account edge exhausted the budget in ~14s.
|
|
922
|
+
+ // 2 here is deliberately small: it is a *per-attempt* budget that nests inside
|
|
923
|
+
+ // the session retry, and each of its waits already honours the server's number.
|
|
924
|
+
+ maxRetries: this.settings.retry?.provider?.maxRetries ?? 2,
|
|
925
|
+
maxRetryDelayMs: this.settings.retry?.provider?.maxRetryDelayMs ?? 60000,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
825
928
|
diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/skills.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/skills.js
|
|
826
929
|
index 4e7e784..7383a2b 100644
|
|
827
930
|
--- a/node_modules/@earendil-works/pi-coding-agent/dist/core/skills.js
|
|
@@ -1307,7 +1410,7 @@ index 1d9f046..c0326e3 100644
|
|
|
1307
1410
|
return lines;
|
|
1308
1411
|
}
|
|
1309
1412
|
diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js
|
|
1310
|
-
index 3f93cc6..
|
|
1413
|
+
index 3f93cc6..50a2a99 100644
|
|
1311
1414
|
--- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js
|
|
1312
1415
|
+++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js
|
|
1313
1416
|
@@ -1,8 +1,35 @@
|
|
@@ -1347,7 +1450,23 @@ index 3f93cc6..bbe2c10 100644
|
|
|
1347
1450
|
export class ToolExecutionComponent extends Container {
|
|
1348
1451
|
contentBox;
|
|
1349
1452
|
contentText;
|
|
1350
|
-
@@ -
|
|
1453
|
+
@@ -43,8 +70,13 @@ export class ToolExecutionComponent extends Container {
|
|
1454
|
+
// Always create all shell variants. contentBox is used for default renderer-based composition.
|
|
1455
|
+
// selfRenderContainer is used when the tool renders its own framing.
|
|
1456
|
+
// contentText is reserved for generic fallback rendering when no tool definition exists.
|
|
1457
|
+
- this.contentBox = new Box(1, 1, (text) => theme.bg("toolPendingBg", text));
|
|
1458
|
+
- this.contentText = new Text("", 1, 1, (text) => theme.bg("toolPendingBg", text));
|
|
1459
|
+
+ // Privateer: paddingY is 0 because we flatten the tool-box washes (dark.json /
|
|
1460
|
+
+ // light.json). With no background to fill, upstream's paddingY=1 reads as a blank
|
|
1461
|
+
+ // line above AND below every call, stacking with the Spacer(1) into three empty
|
|
1462
|
+
+ // rows between consecutive tools. 0 leaves exactly the one Spacer row, which also
|
|
1463
|
+
+ // matches what the self-render path above emits (a single lines.push("")).
|
|
1464
|
+
+ this.contentBox = new Box(1, 0, (text) => theme.bg("toolPendingBg", text));
|
|
1465
|
+
+ this.contentText = new Text("", 1, 0, (text) => theme.bg("toolPendingBg", text));
|
|
1466
|
+
this.selfRenderContainer = new Container();
|
|
1467
|
+
if (this.hasRendererDefinition()) {
|
|
1468
|
+
this.addChild(this.getRenderShell() === "self" ? this.selfRenderContainer : this.contentBox);
|
|
1469
|
+
@@ -106,6 +138,13 @@ export class ToolExecutionComponent extends Container {
|
|
1351
1470
|
createCallFallback() {
|
|
1352
1471
|
return new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0);
|
|
1353
1472
|
}
|
|
@@ -1361,7 +1480,7 @@ index 3f93cc6..bbe2c10 100644
|
|
|
1361
1480
|
createResultFallback() {
|
|
1362
1481
|
const output = this.getTextOutput();
|
|
1363
1482
|
if (!output) {
|
|
1364
|
-
@@ -218,19 +
|
|
1483
|
+
@@ -218,19 +257,22 @@ export class ToolExecutionComponent extends Container {
|
|
1365
1484
|
renderContainer.clear();
|
|
1366
1485
|
const callRenderer = this.getCallRenderer();
|
|
1367
1486
|
if (!callRenderer) {
|
|
@@ -1387,7 +1506,7 @@ index 3f93cc6..bbe2c10 100644
|
|
|
1387
1506
|
hasContent = true;
|
|
1388
1507
|
}
|
|
1389
1508
|
}
|
|
1390
|
-
@@ -302,7 +
|
|
1509
|
+
@@ -302,7 +344,8 @@ export class ToolExecutionComponent extends Container {
|
|
1391
1510
|
return getRenderedTextOutput(this.result, this.showImages);
|
|
1392
1511
|
}
|
|
1393
1512
|
formatToolExecution() {
|
|
@@ -1398,7 +1517,7 @@ index 3f93cc6..bbe2c10 100644
|
|
|
1398
1517
|
if (content) {
|
|
1399
1518
|
text += `\n\n${content}`;
|
|
1400
1519
|
diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
|
|
1401
|
-
index 42e655d..
|
|
1520
|
+
index 42e655d..606c7cc 100644
|
|
1402
1521
|
--- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
|
|
1403
1522
|
+++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
|
|
1404
1523
|
@@ -10,7 +10,7 @@ import * as TuiLayouts from "@earendil-works/pi-tui";
|
|
@@ -1424,7 +1543,62 @@ index 42e655d..d00bec9 100644
|
|
|
1424
1543
|
if (!sessionManager.usesDefaultSessionDir()) {
|
|
1425
1544
|
args.push("--session-dir", quoteIfNeeded(sessionManager.getSessionDir()));
|
|
1426
1545
|
}
|
|
1427
|
-
@@ -
|
|
1546
|
+
@@ -207,6 +212,54 @@ export function createInteractiveTuiReference(getTui) {
|
|
1547
|
+
getPrototypeOf: () => Reflect.getPrototypeOf(getTui()),
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
+// Privateer patch: describe an error we only have the TEXT of. Mirrors
|
|
1551
|
+
+// describeErrorText in src/engine/errors.ts — see the incident note there.
|
|
1552
|
+
+const PV_RETRY_AFTER_PATTERNS = [
|
|
1553
|
+
+ /retry-after(?:-ms)?["'\s:=]+(\d+(?:\.\d+)?)/i,
|
|
1554
|
+
+ /(?:retry|try) again in (\d+(?:\.\d+)?)\s*(m?s|seconds?|minutes?)/i,
|
|
1555
|
+
+ /retry after (\d+(?:\.\d+)?)\s*(m?s|seconds?|minutes?)/i,
|
|
1556
|
+
+];
|
|
1557
|
+
+function pvRetryAfterMs(text) {
|
|
1558
|
+
+ const s = typeof text === "string" ? text : "";
|
|
1559
|
+
+ for (const re of PV_RETRY_AFTER_PATTERNS) {
|
|
1560
|
+
+ const m = re.exec(s);
|
|
1561
|
+
+ if (!m)
|
|
1562
|
+
+ continue;
|
|
1563
|
+
+ const value = Number.parseFloat(m[1]);
|
|
1564
|
+
+ if (!Number.isFinite(value) || value <= 0)
|
|
1565
|
+
+ continue;
|
|
1566
|
+
+ const unit = (m[2] ?? "").toLowerCase();
|
|
1567
|
+
+ const ms = unit === "ms" || /retry-after-ms/i.test(m[0])
|
|
1568
|
+
+ ? value
|
|
1569
|
+
+ : unit.startsWith("m") && unit !== "ms"
|
|
1570
|
+
+ ? value * 60_000
|
|
1571
|
+
+ : value * 1000;
|
|
1572
|
+
+ return Math.min(Math.max(Math.round(ms), 1_000), 60_000);
|
|
1573
|
+
+ }
|
|
1574
|
+
+ return null;
|
|
1575
|
+
+}
|
|
1576
|
+
+function pvDescribeErrorText(text) {
|
|
1577
|
+
+ const s = typeof text === "string" ? text : "";
|
|
1578
|
+
+ const status = Number(/^\s*(\d{3})\b/.exec(s)?.[1] ?? NaN);
|
|
1579
|
+
+ if (!Number.isFinite(status))
|
|
1580
|
+
+ return null;
|
|
1581
|
+
+ if (status === 429) {
|
|
1582
|
+
+ const stated = pvRetryAfterMs(s);
|
|
1583
|
+
+ return {
|
|
1584
|
+
+ message: "Rate limited (429).",
|
|
1585
|
+
+ hint: stated
|
|
1586
|
+
+ ? `The provider asked for ${Math.ceil(stated / 1000)}s. Wait that long and send it again.`
|
|
1587
|
+
+ : "Wait a moment and send it again — or run /model to switch to another provider.",
|
|
1588
|
+
+ };
|
|
1589
|
+
+ }
|
|
1590
|
+
+ if (status === 401 || status === 403)
|
|
1591
|
+
+ return { message: s, hint: "Check your credentials — run /login to re-authenticate." };
|
|
1592
|
+
+ if (status === 404)
|
|
1593
|
+
+ return { message: s, hint: "Check the model id — run /model to switch." };
|
|
1594
|
+
+ if (status >= 500)
|
|
1595
|
+
+ return { message: s, hint: "Usually transient — retry shortly." };
|
|
1596
|
+
+ return null;
|
|
1597
|
+
+}
|
|
1598
|
+
export class InteractiveMode {
|
|
1599
|
+
runtimeHost;
|
|
1600
|
+
renderer;
|
|
1601
|
+
@@ -405,9 +458,15 @@ export class InteractiveMode {
|
|
1428
1602
|
}
|
|
1429
1603
|
getBuiltInCommandConflictDiagnostics(extensionRunner) {
|
|
1430
1604
|
const builtinNames = new Set(BUILTIN_SLASH_COMMANDS.map((command) => command.name));
|
|
@@ -1441,7 +1615,7 @@ index 42e655d..d00bec9 100644
|
|
|
1441
1615
|
.map((command) => ({
|
|
1442
1616
|
type: "warning",
|
|
1443
1617
|
message: command.invocationName === command.name
|
|
1444
|
-
@@ -754,20 +
|
|
1618
|
+
@@ -754,20 +813,18 @@ export class InteractiveMode {
|
|
1445
1619
|
this.showNewVersionNotification(newRelease);
|
|
1446
1620
|
}
|
|
1447
1621
|
});
|
|
@@ -1474,7 +1648,7 @@ index 42e655d..d00bec9 100644
|
|
|
1474
1648
|
// Check tmux keyboard setup asynchronously
|
|
1475
1649
|
this.checkTmuxKeyboardSetup().then((warning) => {
|
|
1476
1650
|
if (warning) {
|
|
1477
|
-
@@ -2310,7 +
|
|
1651
|
+
@@ -2310,7 +2367,17 @@ export class InteractiveMode {
|
|
1478
1652
|
if (text === "/model" || text.startsWith("/model ")) {
|
|
1479
1653
|
const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
|
|
1480
1654
|
this.editor.setText("");
|
|
@@ -1493,7 +1667,7 @@ index 42e655d..d00bec9 100644
|
|
|
1493
1667
|
return;
|
|
1494
1668
|
}
|
|
1495
1669
|
if (text === "/export" || text.startsWith("/export ")) {
|
|
1496
|
-
@@ -2376,12 +
|
|
1670
|
+
@@ -2376,12 +2443,42 @@ export class InteractiveMode {
|
|
1497
1671
|
if (text === "/login" || text.startsWith("/login ")) {
|
|
1498
1672
|
const providerRef = text.startsWith("/login ") ? text.slice(7).trim() : undefined;
|
|
1499
1673
|
this.editor.setText("");
|
|
@@ -1538,7 +1712,7 @@ index 42e655d..d00bec9 100644
|
|
|
1538
1712
|
return;
|
|
1539
1713
|
}
|
|
1540
1714
|
if (text === "/new") {
|
|
1541
|
-
@@ -3024,7 +
|
|
1715
|
+
@@ -3024,7 +3121,7 @@ export class InteractiveMode {
|
|
1542
1716
|
if (this.chatContainer.children.length > 0) {
|
|
1543
1717
|
this.chatContainer.addChild(new Spacer(1));
|
|
1544
1718
|
}
|
|
@@ -1547,6 +1721,25 @@ index 42e655d..d00bec9 100644
|
|
|
1547
1721
|
}
|
|
1548
1722
|
async getUserInput() {
|
|
1549
1723
|
const queuedInput = this.pendingUserInputs.shift();
|
|
1724
|
+
@@ -3363,7 +3460,17 @@ export class InteractiveMode {
|
|
1725
|
+
}
|
|
1726
|
+
showError(errorMessage) {
|
|
1727
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
1728
|
+
- this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), this.outputPad, 0));
|
|
1729
|
+
+ // Privateer patch: by the time an error reaches here it is only a string — the
|
|
1730
|
+
+ // structured fields describeError() reads are long gone, so the user was shown
|
|
1731
|
+
+ // the SDK's own words ("429 status code (no body)"), which say nothing about
|
|
1732
|
+
+ // what to do. The status is still readable at the front of that string; recover
|
|
1733
|
+
+ // it and say what describeError would. Mirrors describeErrorText in
|
|
1734
|
+
+ // src/engine/errors.ts. Anything without a leading status prints unchanged.
|
|
1735
|
+
+ const described = pvDescribeErrorText(errorMessage);
|
|
1736
|
+
+ this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${described?.message ?? errorMessage}`), this.outputPad, 0));
|
|
1737
|
+
+ if (described?.hint) {
|
|
1738
|
+
+ this.chatContainer.addChild(new Text(theme.fg("muted", described.hint), this.outputPad, 0));
|
|
1739
|
+
+ }
|
|
1740
|
+
this.ui.requestRender();
|
|
1741
|
+
}
|
|
1742
|
+
showWarning(warningMessage) {
|
|
1550
1743
|
diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json
|
|
1551
1744
|
index 9db9cbd..b370180 100644
|
|
1552
1745
|
--- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json
|
package/src/config/moat.ts
CHANGED
|
@@ -45,6 +45,7 @@ import type { GateController } from "../ext/permissionGate.ts";
|
|
|
45
45
|
import type { SendFileBridge } from "../tools/sendFile.ts";
|
|
46
46
|
import type { CargoSaveBridge } from "../tools/cargo.ts";
|
|
47
47
|
import type { ChartOpBridge } from "../tools/charts.ts";
|
|
48
|
+
import type { LibrarySaveBridge } from "../tools/saveToLibrary.ts";
|
|
48
49
|
import type { AttachmentStore } from "../util/attachmentStore.ts";
|
|
49
50
|
|
|
50
51
|
/** A Pi extension factory, as DefaultResourceLoader takes them. */
|
|
@@ -73,7 +74,10 @@ export interface MoatOptions {
|
|
|
73
74
|
* its module-level bridge and stands them down inside the daemon, so a live spawn's own
|
|
74
75
|
* pair is what the model gets (see tools/relayFileTools.ts).
|
|
75
76
|
*/
|
|
76
|
-
relayFiles?: {
|
|
77
|
+
relayFiles?: {
|
|
78
|
+
bridge: SendFileBridge & CargoSaveBridge & ChartOpBridge & LibrarySaveBridge;
|
|
79
|
+
attachments: AttachmentStore;
|
|
80
|
+
};
|
|
77
81
|
/**
|
|
78
82
|
* THIS run's inbox-attachment staging area (routines/resultMedia.ts). Passed only by
|
|
79
83
|
* a path whose result reaches the app's Inbox — a scheduled routine, a submitted
|
package/src/engine/errors.ts
CHANGED
|
@@ -348,3 +348,133 @@ export function describeError(err: unknown): DescribedError {
|
|
|
348
348
|
// arrives, so it goes through the compactor first.
|
|
349
349
|
return out({ message: compactProviderError(text) });
|
|
350
350
|
}
|
|
351
|
+
|
|
352
|
+
// ── Throttles: waiting the right amount, and only once ───────────────────────
|
|
353
|
+
//
|
|
354
|
+
// The incident this exists for: the account channel's edge answered a burst of
|
|
355
|
+
// turns with a bare `429` — no body, no JSON, just the status. Two retry layers
|
|
356
|
+
// exist and the wrong one was live. pi-ai's `retryProviderRequest` is the good
|
|
357
|
+
// one (it reads `retry-after`, jitters, caps at 60s) but it takes `maxRetries ??
|
|
358
|
+
// 0` and Pi's settings gave it no default, so it ran ZERO retries. All the
|
|
359
|
+
// patience came from the session-level loop: 3 attempts at 2s/4s/8s, no
|
|
360
|
+
// `retry-after`, no jitter — ~14 seconds against a window that is conventionally
|
|
361
|
+
// 60. Exhaustion was arithmetic, not bad luck. Then the pre-prompt compaction
|
|
362
|
+
// fired the summarizer into the same throttled endpoint and died there too, so
|
|
363
|
+
// the user's next message was answered with "Auto-compaction cancelled".
|
|
364
|
+
//
|
|
365
|
+
// The fix is three-part and mirrored into the Pi patch: give the provider layer a
|
|
366
|
+
// real retry budget, make the session layer's backoff honour a server-stated
|
|
367
|
+
// delay and jitter, and don't summarise into an endpoint that just throttled us.
|
|
368
|
+
|
|
369
|
+
/** A throttle can clear on its own — but only after the server's stated window. */
|
|
370
|
+
export function isThrottleFailure(text: string | null | undefined): boolean {
|
|
371
|
+
return /^\s*429\b/.test(typeof text === "string" ? text : "");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Longest we will sit on a single backoff. Past this the user deserves the turn back. */
|
|
375
|
+
export const MAX_RETRY_DELAY_MS = 60_000;
|
|
376
|
+
|
|
377
|
+
// A bare 429 carries nothing, but a provider that bothers to explain itself puts the
|
|
378
|
+
// number in the text one of a few ways. Read it where it is offered; guess otherwise.
|
|
379
|
+
const RETRY_AFTER_PATTERNS = [
|
|
380
|
+
/retry-after(?:-ms)?["'\s:=]+(\d+(?:\.\d+)?)/i,
|
|
381
|
+
/(?:retry|try) again in (\d+(?:\.\d+)?)\s*(m?s|seconds?|minutes?)/i,
|
|
382
|
+
/retry after (\d+(?:\.\d+)?)\s*(m?s|seconds?|minutes?)/i,
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* The server's requested delay in ms, or null when it did not state one.
|
|
387
|
+
*
|
|
388
|
+
* `retry-after` is seconds by convention and `retry-after-ms` is milliseconds; a
|
|
389
|
+
* prose "try again in 30 seconds" carries its own unit. Values are clamped rather
|
|
390
|
+
* than rejected — a provider asking for an hour gets our ceiling, not a crash.
|
|
391
|
+
*/
|
|
392
|
+
export function retryAfterMs(text: string | null | undefined): number | null {
|
|
393
|
+
const s = typeof text === "string" ? text : "";
|
|
394
|
+
for (const re of RETRY_AFTER_PATTERNS) {
|
|
395
|
+
const m = re.exec(s);
|
|
396
|
+
if (!m) continue;
|
|
397
|
+
const value = Number.parseFloat(m[1]);
|
|
398
|
+
if (!Number.isFinite(value) || value <= 0) continue;
|
|
399
|
+
const unit = (m[2] ?? "").toLowerCase();
|
|
400
|
+
const ms =
|
|
401
|
+
unit === "ms" || /retry-after-ms/i.test(m[0])
|
|
402
|
+
? value
|
|
403
|
+
: unit.startsWith("m") && unit !== "ms"
|
|
404
|
+
? value * 60_000
|
|
405
|
+
: value * 1000;
|
|
406
|
+
return Math.min(Math.max(Math.round(ms), 1_000), MAX_RETRY_DELAY_MS);
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* How long the session-level retry should wait before attempt `attempt` (1-based).
|
|
413
|
+
*
|
|
414
|
+
* A server-stated delay wins outright — it is the only number here that is a fact.
|
|
415
|
+
* Otherwise back off exponentially from `baseDelayMs`, capped, and then jitter DOWN
|
|
416
|
+
* by up to 25%. The jitter is the point of the exercise: without it every parallel
|
|
417
|
+
* request that tripped the same limit wakes at the identical millisecond and trips
|
|
418
|
+
* it again, which is how one throttled turn becomes a throttled session.
|
|
419
|
+
*
|
|
420
|
+
* `rand` is injectable so the schedule can be asserted in a test.
|
|
421
|
+
*/
|
|
422
|
+
export function retryDelayMs(
|
|
423
|
+
errorText: string | null | undefined,
|
|
424
|
+
attempt: number,
|
|
425
|
+
baseDelayMs: number,
|
|
426
|
+
rand: () => number = Math.random,
|
|
427
|
+
): number {
|
|
428
|
+
const stated = retryAfterMs(errorText);
|
|
429
|
+
if (stated != null) return stated;
|
|
430
|
+
const n = Math.max(1, Math.floor(attempt));
|
|
431
|
+
const backoff = Math.min(baseDelayMs * 2 ** (n - 1), MAX_RETRY_DELAY_MS);
|
|
432
|
+
return Math.round(backoff * (1 - rand() * 0.25));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Describe an error we only have the TEXT of, for the one place that has nothing else.
|
|
437
|
+
*
|
|
438
|
+
* `describeError` above reads structured fields off the error object; by the time a
|
|
439
|
+
* message reaches the TUI's `showError` those are gone and all that survives is a
|
|
440
|
+
* string the SDK built status-first ("429 status code (no body)"). That string is
|
|
441
|
+
* what the user was shown six times during the incident. Recover the status from it
|
|
442
|
+
* and say the same thing `describeError` would. Returns null when there is no leading
|
|
443
|
+
* status to read, so ordinary messages print unchanged.
|
|
444
|
+
*/
|
|
445
|
+
export function describeErrorText(text: string | null | undefined): DescribedError | null {
|
|
446
|
+
const s = typeof text === "string" ? text : "";
|
|
447
|
+
const status = Number(/^\s*(\d{3})\b/.exec(s)?.[1] ?? NaN);
|
|
448
|
+
if (!Number.isFinite(status)) return null;
|
|
449
|
+
|
|
450
|
+
if (status === 429) {
|
|
451
|
+
const stated = retryAfterMs(s);
|
|
452
|
+
return {
|
|
453
|
+
message: redactText(`Rate limited (429).`),
|
|
454
|
+
hint: stated
|
|
455
|
+
? `The provider asked for ${Math.ceil(stated / 1000)}s. Wait that long and send it again.`
|
|
456
|
+
: "Wait a moment and send it again — or run /model to switch to another provider.",
|
|
457
|
+
retryable: true,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (status === 401 || status === 403) {
|
|
461
|
+
return {
|
|
462
|
+
message: redactText(compactProviderError(s)),
|
|
463
|
+
hint: "Check your credentials — run /login to re-authenticate.",
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
if (status === 404) {
|
|
467
|
+
return {
|
|
468
|
+
message: redactText(compactProviderError(s)),
|
|
469
|
+
hint: "Check the model id — run /model to switch.",
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
if (status >= 500) {
|
|
473
|
+
return {
|
|
474
|
+
message: redactText(compactProviderError(s)),
|
|
475
|
+
hint: "Usually transient — retry shortly.",
|
|
476
|
+
retryable: true,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
@@ -175,6 +175,7 @@ const MEDIA_TOOLS = new Set([
|
|
|
175
175
|
"generate_image",
|
|
176
176
|
"generate_video",
|
|
177
177
|
"generate_model",
|
|
178
|
+
"generate_sprite",
|
|
178
179
|
"generate_speech",
|
|
179
180
|
"generate_music",
|
|
180
181
|
"generate_sfx",
|
|
@@ -199,6 +200,7 @@ export const BILLED_MEDIA_TOOLS: ReadonlySet<string> = new Set([
|
|
|
199
200
|
"generate_image",
|
|
200
201
|
"generate_video",
|
|
201
202
|
"generate_model",
|
|
203
|
+
"generate_sprite",
|
|
202
204
|
"generate_speech",
|
|
203
205
|
"generate_music",
|
|
204
206
|
"generate_sfx",
|
|
@@ -210,6 +212,11 @@ const MEDIA_TITLES: Record<string, string> = {
|
|
|
210
212
|
// options, so the title says so out loud rather than leaving the human to
|
|
211
213
|
// work it out from a JSON blob of flags.
|
|
212
214
|
generate_model: "Generate a 3D model (billed; $0.14-$2.41 a mesh depending on the model)",
|
|
215
|
+
// The one whose price is a MULTIPLE rather than a rate: it renders a video per
|
|
216
|
+
// facing, so approving it can be approving five video generations at once. The
|
|
217
|
+
// title says the multiplier out loud, because "generate a sprite" reads like
|
|
218
|
+
// one cheap call and it is not.
|
|
219
|
+
generate_sprite: "Generate a sprite animation (billed; 1, 3 or 5 video generations depending on facings)",
|
|
213
220
|
generate_speech: "Generate speech (billed to your Privateer account)",
|
|
214
221
|
generate_music: "Generate music (billed; music prompts have no zero-retention option)",
|
|
215
222
|
// Cheap per call and therefore the one most likely to be called twenty times in a
|
|
@@ -410,7 +417,13 @@ export function classifyToolCall(
|
|
|
410
417
|
const outsideInputs = resolvedInputs.filter((a) => isOutsideScope(scope, a));
|
|
411
418
|
const protectedInputs = resolvedInputs.filter((a) => isProtectedPath(a));
|
|
412
419
|
|
|
413
|
-
|
|
420
|
+
// `dir` is generate_sprite's output: it unpacks a whole bundle (sheet, frames
|
|
421
|
+
// and the .tres) into a DIRECTORY rather than writing one named file. Without
|
|
422
|
+
// it here the gate finds no output path and fails safe — which reads to the
|
|
423
|
+
// user as an unexplained denial on a tool that is in fact just writing where
|
|
424
|
+
// they asked. The directory is the right thing to show and to judge for
|
|
425
|
+
// scope: everything the tool writes lands inside it.
|
|
426
|
+
const outPath = str(obj.path ?? obj.output ?? obj.dir);
|
|
414
427
|
// `probe` reads and writes nothing; so does any composition call with no output
|
|
415
428
|
// (which the tool itself rejects). Gate those only when they touch a sensitive input.
|
|
416
429
|
if (!outPath) {
|