privateer-agent 0.12.20 → 0.12.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privateer-agent",
3
- "version": "0.12.20",
3
+ "version": "0.12.21",
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,7 +68,7 @@ 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..c52ea80 100644
71
+ index ce8a9a2..b7b339e 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
74
  @@ -38,6 +38,70 @@ import { createLocalBashOperations } from "./tools/bash.js";
@@ -174,7 +174,7 @@ index ce8a9a2..c52ea80 100644
174
174
  // Emit to extensions first
175
175
  await this._emitExtensionEvent(event);
176
176
  // Notify all listeners
177
- @@ -772,6 +854,15 @@ export class AgentSession {
177
+ @@ -772,6 +854,25 @@ export class AgentSession {
178
178
  finalError: msg.errorMessage,
179
179
  });
180
180
  this._retryAttempt = 0;
@@ -186,11 +186,21 @@ index ce8a9a2..c52ea80 100644
186
186
  + // That re-entry is what produced the endless "retrying 1/3…2/3…3/3" loop
187
187
  + // on a persistently-failing provider/tool. Context-overflow errors never
188
188
  + // reach this branch (_isRetryableError → false), so compaction is untouched.
189
+ + return false;
190
+ + }
191
+ + // Privateer patch: a first-attempt hard 4xx (a WAF 403, a 401, a 404) is also
192
+ + // terminal. Stock Pi still falls through to compaction here because _retryAttempt
193
+ + // is 0, and _checkCompaction will summarise a session that already has usage.
194
+ + // That summary is another LLM call to the SAME blocked endpoint; retryAssistantCall
195
+ + // classifies the raw HTML body as transient (it contains "500"/"502"), so the
196
+ + // agent looks hung — retrying a compaction that can never succeed — until the
197
+ + // summarizer budget burns. Context overflow never matches isHardHttpFailure.
198
+ + if (msg.stopReason === "error" && isHardHttpFailure(msg.errorMessage)) {
189
199
  + return false;
190
200
  }
191
201
  if (await this._checkCompaction(msg)) {
192
202
  return true;
193
- @@ -852,6 +943,14 @@ export class AgentSession {
203
+ @@ -852,6 +953,14 @@ export class AgentSession {
194
204
  if (!hasConfiguredAuth) {
195
205
  const isOAuth = this._modelRuntime.isUsingOAuth(this.model.provider);
196
206
  if (isOAuth) {
@@ -205,7 +215,7 @@ index ce8a9a2..c52ea80 100644
205
215
  throw new Error(`Authentication failed for "${this.model.provider}". ` +
206
216
  `Credentials may have expired or network is unavailable. ` +
207
217
  `Run '/login ${this.model.provider}' to re-authenticate.`);
208
- @@ -2084,6 +2183,27 @@ export class AgentSession {
218
+ @@ -2084,6 +2193,27 @@ export class AgentSession {
209
219
  // Context overflow is handled by compaction, not retry.
210
220
  if (isContextOverflow(message, this.model?.contextWindow ?? 0))
211
221
  return false;
@@ -1052,17 +1062,19 @@ index 6f7ed6a..b0ed0ae 100644
1052
1062
  getResourcePattern(item) {
1053
1063
  const scope = item.metadata.scope;
1054
1064
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js
1055
- index 1d9f046..2a1fba8 100644
1065
+ index 1d9f046..c0326e3 100644
1056
1066
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js
1057
1067
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js
1058
- @@ -1,5 +1,5 @@
1068
+ @@ -1,7 +1,6 @@
1059
1069
  import { isAbsolute, relative, resolve, sep } from "node:path";
1060
1070
  -import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
1061
1071
  +import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
1062
1072
  import { areExperimentalFeaturesEnabled } from "../../../core/experimental.js";
1063
- import { addUsageToTotals, createUsageTotals } from "../../../core/usage-totals.js";
1073
+ -import { addUsageToTotals, createUsageTotals } from "../../../core/usage-totals.js";
1064
1074
  import { theme } from "../theme/theme.js";
1065
- @@ -14,6 +14,43 @@ function sanitizeStatusText(text) {
1075
+ /**
1076
+ * Sanitize text for display in a single-line status.
1077
+ @@ -14,6 +13,43 @@ function sanitizeStatusText(text) {
1066
1078
  .replace(/ +/g, " ")
1067
1079
  .trim();
1068
1080
  }
@@ -1106,7 +1118,182 @@ index 1d9f046..2a1fba8 100644
1106
1118
  /**
1107
1119
  * Format token counts for compact footer display.
1108
1120
  */
1109
- @@ -212,9 +249,9 @@ export class FooterComponent {
1121
+ @@ -41,8 +77,11 @@ export function formatCwdForFooter(cwd, home) {
1122
+ return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
1123
+ }
1124
+ /**
1125
+ - * Footer component that shows pwd, token stats, and context usage.
1126
+ - * Computes token/context stats from session, gets git branch and extension statuses from provider.
1127
+ + * Footer component that shows pwd (with git branch) and the active model.
1128
+ + *
1129
+ + * Privateer trimmed this: upstream also drew a second row of cumulative token
1130
+ + * counters, cache-hit rate, cost and a context gauge. Gets git branch and
1131
+ + * extension statuses from provider.
1132
+ */
1133
+ export class FooterComponent {
1134
+ autoCompactEnabled = true;
1135
+ @@ -74,29 +113,9 @@ export class FooterComponent {
1136
+ }
1137
+ render(width) {
1138
+ const state = this.session.state;
1139
+ - // Calculate cumulative usage from ALL session entries (not just post-compaction messages)
1140
+ - const usageTotals = createUsageTotals();
1141
+ - let latestCacheHitRate;
1142
+ - for (const entry of this.session.sessionManager.getEntries()) {
1143
+ - if (entry.type === "message" && entry.message.role === "assistant") {
1144
+ - addUsageToTotals(usageTotals, entry.message.usage);
1145
+ - const latestPromptTokens = entry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;
1146
+ - latestCacheHitRate =
1147
+ - latestPromptTokens > 0 ? (entry.message.usage.cacheRead / latestPromptTokens) * 100 : undefined;
1148
+ - }
1149
+ - else if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.usage) {
1150
+ - addUsageToTotals(usageTotals, entry.message.usage);
1151
+ - }
1152
+ - else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
1153
+ - addUsageToTotals(usageTotals, entry.usage);
1154
+ - }
1155
+ - }
1156
+ - // Calculate context usage from session (handles compaction correctly).
1157
+ - // After compaction, tokens are unknown until the next LLM response.
1158
+ - const contextUsage = this.session.getContextUsage();
1159
+ - const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
1160
+ - const contextPercentValue = contextUsage?.percent ?? 0;
1161
+ - const contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
1162
+ + // Privateer: no token counters, cache-hit rate, cost or context gauge in the
1163
+ + // footer — so the accumulation that fed them is gone too. Upstream walked EVERY
1164
+ + // session entry on EVERY render to build numbers we no longer draw.
1165
+ // Replace home directory with ~
1166
+ let pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);
1167
+ // Add git branch if available
1168
+ @@ -109,56 +128,24 @@ export class FooterComponent {
1169
+ if (sessionName) {
1170
+ pwd = `${pwd} • ${sessionName}`;
1171
+ }
1172
+ - // Build stats line
1173
+ - const statsParts = [];
1174
+ - if (usageTotals.input)
1175
+ - statsParts.push(`↑${formatTokens(usageTotals.input)}`);
1176
+ - if (usageTotals.output)
1177
+ - statsParts.push(`↓${formatTokens(usageTotals.output)}`);
1178
+ - if (usageTotals.cacheRead)
1179
+ - statsParts.push(`R${formatTokens(usageTotals.cacheRead)}`);
1180
+ - if (usageTotals.cacheWrite)
1181
+ - statsParts.push(`W${formatTokens(usageTotals.cacheWrite)}`);
1182
+ - if ((usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) && latestCacheHitRate !== undefined) {
1183
+ - statsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);
1184
+ - }
1185
+ - // Kimi Coding is subscription-backed despite using API-key authentication.
1186
+ - const usingSubscription = state.model
1187
+ - ? state.model.provider === "kimi-coding" || this.session.modelRuntime.isUsingSubscription(state.model.provider)
1188
+ - : false;
1189
+ - if (usageTotals.cost || usingSubscription) {
1190
+ - const costStr = `$${usageTotals.cost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`;
1191
+ - statsParts.push(costStr);
1192
+ - }
1193
+ - // Colorize context percentage based on usage
1194
+ - let contextPercentStr;
1195
+ - const autoIndicator = this.autoCompactEnabled ? " (auto)" : "";
1196
+ - const contextPercentDisplay = contextPercent === "?"
1197
+ - ? `?/${formatTokens(contextWindow)}${autoIndicator}`
1198
+ - : `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;
1199
+ - if (contextPercentValue > 90) {
1200
+ - contextPercentStr = theme.fg("error", contextPercentDisplay);
1201
+ - }
1202
+ - else if (contextPercentValue > 70) {
1203
+ - contextPercentStr = theme.fg("warning", contextPercentDisplay);
1204
+ - }
1205
+ - else {
1206
+ - contextPercentStr = contextPercentDisplay;
1207
+ - }
1208
+ - statsParts.push(contextPercentStr);
1209
+ - if (areExperimentalFeaturesEnabled()) {
1210
+ - statsParts.push(`${theme.fg("dim", "•")} ${theme.bold(theme.fg("warning", "xp"))}`);
1211
+ - }
1212
+ - let statsLeft = statsParts.join(" ");
1213
+ + // Privateer: ONE footer row — the working directory on the left, the model on
1214
+ + // the right. Upstream spent a second row on cumulative token counters, cache-hit
1215
+ + // rate and cost; those are session trivia that never change what you do next, and
1216
+ + // they made the busiest part of the screen the least useful. The `xp` flag stays:
1217
+ + // it warns that experimental features are live, which is not trivia.
1218
+ + const experimentalFlag = areExperimentalFeaturesEnabled()
1219
+ + ? ` ${theme.fg("dim", "•")} ${theme.bold(theme.fg("warning", "xp"))}`
1220
+ + : "";
1221
+ + let left = pwd + experimentalFlag;
1222
+ // Add model name on the right side, plus thinking level if model supports it
1223
+ const modelName = state.model?.id || "no-model";
1224
+ - let statsLeftWidth = visibleWidth(statsLeft);
1225
+ - // If statsLeft is too wide, truncate it
1226
+ - if (statsLeftWidth > width) {
1227
+ - statsLeft = truncateToWidth(statsLeft, width, "...");
1228
+ - statsLeftWidth = visibleWidth(statsLeft);
1229
+ + let leftWidth = visibleWidth(left);
1230
+ + // If the left side is too wide, truncate it
1231
+ + if (leftWidth > width) {
1232
+ + left = truncateToWidth(left, width, "...");
1233
+ + leftWidth = visibleWidth(left);
1234
+ }
1235
+ - // Calculate available space for padding (minimum 2 spaces between stats and model)
1236
+ + // Calculate available space for padding (minimum 2 spaces between cwd and model)
1237
+ const minPadding = 2;
1238
+ // Add thinking level indicator if model supports reasoning
1239
+ let rightSideWithoutProvider = modelName;
1240
+ @@ -171,50 +158,48 @@ export class FooterComponent {
1241
+ let rightSide = rightSideWithoutProvider;
1242
+ if (this.footerData.getAvailableProviderCount() > 1 && state.model) {
1243
+ rightSide = `(${state.model.provider}) ${rightSideWithoutProvider}`;
1244
+ - if (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {
1245
+ + if (leftWidth + minPadding + visibleWidth(rightSide) > width) {
1246
+ // Too wide, fall back
1247
+ rightSide = rightSideWithoutProvider;
1248
+ }
1249
+ }
1250
+ const rightSideWidth = visibleWidth(rightSide);
1251
+ - const totalNeeded = statsLeftWidth + minPadding + rightSideWidth;
1252
+ - let statsLine;
1253
+ + const totalNeeded = leftWidth + minPadding + rightSideWidth;
1254
+ + let footerLine;
1255
+ if (totalNeeded <= width) {
1256
+ // Both fit - add padding to right-align model
1257
+ - const padding = " ".repeat(width - statsLeftWidth - rightSideWidth);
1258
+ - statsLine = statsLeft + padding + rightSide;
1259
+ + const padding = " ".repeat(width - leftWidth - rightSideWidth);
1260
+ + footerLine = left + padding + rightSide;
1261
+ }
1262
+ else {
1263
+ // Need to truncate right side
1264
+ - const availableForRight = width - statsLeftWidth - minPadding;
1265
+ + const availableForRight = width - leftWidth - minPadding;
1266
+ if (availableForRight > 0) {
1267
+ const truncatedRight = truncateToWidth(rightSide, availableForRight, "");
1268
+ const truncatedRightWidth = visibleWidth(truncatedRight);
1269
+ - const padding = " ".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));
1270
+ - statsLine = statsLeft + padding + truncatedRight;
1271
+ + const padding = " ".repeat(Math.max(0, width - leftWidth - truncatedRightWidth));
1272
+ + footerLine = left + padding + truncatedRight;
1273
+ }
1274
+ else {
1275
+ // Not enough space for right side at all
1276
+ - statsLine = statsLeft;
1277
+ + footerLine = left;
1278
+ }
1279
+ }
1280
+ - // Apply dim to each part separately. statsLeft may contain color codes (for context %)
1281
+ - // that end with a reset, which would clear an outer dim wrapper. So we dim the parts
1282
+ - // before and after the colored section independently.
1283
+ - const dimStatsLeft = theme.fg("dim", statsLeft);
1284
+ - const remainder = statsLine.slice(statsLeft.length); // padding + rightSide
1285
+ - const dimRemainder = theme.fg("dim", remainder);
1286
+ - const pwdLine = truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "..."));
1287
+ - const lines = [pwdLine, dimStatsLeft + dimRemainder];
1288
+ + // Apply dim to each part separately. `left` may carry the bold/warning-coloured
1289
+ + // xp flag, whose reset would clear an outer dim wrapper. So dim the parts before
1290
+ + // and after the coloured section independently.
1291
+ + const dimLeft = theme.fg("dim", left);
1292
+ + const remainder = footerLine.slice(left.length); // padding + rightSide
1293
+ + const lines = [dimLeft + theme.fg("dim", remainder)];
1294
+ // Add extension statuses on a single line, sorted by key alphabetically
1295
+ const extensionStatuses = this.footerData.getExtensionStatuses();
1296
+ if (extensionStatuses.size > 0) {
1110
1297
  const sortedStatuses = Array.from(extensionStatuses.entries())
1111
1298
  .sort(([a], [b]) => a.localeCompare(b))
1112
1299
  .map(([, text]) => sanitizeStatusText(text));
@@ -1119,6 +1306,97 @@ index 1d9f046..2a1fba8 100644
1119
1306
  }
1120
1307
  return lines;
1121
1308
  }
1309
+ 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..bbe2c10 100644
1311
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js
1312
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js
1313
+ @@ -1,8 +1,35 @@
1314
+ -import { Box, Container, getCapabilities, Image, Spacer, Text } from "@earendil-works/pi-tui";
1315
+ +import { Box, Container, getCapabilities, Image, Spacer, visibleWidth, Text } from "@earendil-works/pi-tui";
1316
+ import { createAllToolDefinitions } from "../../../core/tools/index.js";
1317
+ import { getTextOutput as getRenderedTextOutput } from "../../../core/tools/render-utils.js";
1318
+ import { convertToPng } from "../../../utils/image-convert.js";
1319
+ import { theme } from "../theme/theme.js";
1320
+ +/**
1321
+ + * Privateer: mark a failed tool call inline on its title line.
1322
+ + *
1323
+ + * Upstream's ONLY failure signal is the full-width red `toolErrorBg` wash behind
1324
+ + * the whole block. We flatten those washes (see dark.json / light.json) because
1325
+ + * large blocks of colour read as noise — but flattening the error one alone would
1326
+ + * make a failure indistinguishable from a success, which is a real regression.
1327
+ + *
1328
+ + * So we put the signal back where it costs nothing: a red glyph prefixed to the
1329
+ + * first line of whatever the tool's own renderCall produced. Wrapping the CALL
1330
+ + * component (not the Box) means Box still owns padding and width, and the marker
1331
+ + * works for every tool without touching seven per-tool renderers. Subsequent
1332
+ + * lines get a blank hanging indent so the title stays visually aligned.
1333
+ + */
1334
+ +function markCallErrored(component, marker) {
1335
+ + const markerWidth = visibleWidth(marker);
1336
+ + const indent = " ".repeat(markerWidth);
1337
+ + return {
1338
+ + render(width) {
1339
+ + const lines = component.render(Math.max(1, width - markerWidth));
1340
+ + return lines.map((line, i) => (i === 0 ? marker + line : indent + line));
1341
+ + },
1342
+ + invalidate() {
1343
+ + component.invalidate?.();
1344
+ + },
1345
+ + };
1346
+ +}
1347
+ export class ToolExecutionComponent extends Container {
1348
+ contentBox;
1349
+ contentText;
1350
+ @@ -106,6 +133,13 @@ export class ToolExecutionComponent extends Container {
1351
+ createCallFallback() {
1352
+ return new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0);
1353
+ }
1354
+ + /** Privateer: red-flag a failed call, pass a pending/succeeded one straight through. */
1355
+ + decorateCall(component) {
1356
+ + if (!this.result?.isError || this.isPartial) {
1357
+ + return component;
1358
+ + }
1359
+ + return markCallErrored(component, theme.fg("error", "\u2717 "));
1360
+ + }
1361
+ createResultFallback() {
1362
+ const output = this.getTextOutput();
1363
+ if (!output) {
1364
+ @@ -218,19 +252,22 @@ export class ToolExecutionComponent extends Container {
1365
+ renderContainer.clear();
1366
+ const callRenderer = this.getCallRenderer();
1367
+ if (!callRenderer) {
1368
+ - renderContainer.addChild(this.createCallFallback());
1369
+ + renderContainer.addChild(this.decorateCall(this.createCallFallback()));
1370
+ hasContent = true;
1371
+ }
1372
+ else {
1373
+ try {
1374
+ const component = callRenderer(this.args, theme, this.getRenderContext(this.callRendererComponent));
1375
+ + // Store the RAW component: it comes back as `lastComponent` on the
1376
+ + // next render and the tool's renderer calls its own methods on it,
1377
+ + // so handing it a wrapper would break every stateful renderer.
1378
+ this.callRendererComponent = component;
1379
+ - renderContainer.addChild(component);
1380
+ + renderContainer.addChild(this.decorateCall(component));
1381
+ hasContent = true;
1382
+ }
1383
+ catch {
1384
+ this.callRendererComponent = undefined;
1385
+ - renderContainer.addChild(this.createCallFallback());
1386
+ + renderContainer.addChild(this.decorateCall(this.createCallFallback()));
1387
+ hasContent = true;
1388
+ }
1389
+ }
1390
+ @@ -302,7 +339,8 @@ export class ToolExecutionComponent extends Container {
1391
+ return getRenderedTextOutput(this.result, this.showImages);
1392
+ }
1393
+ formatToolExecution() {
1394
+ - let text = theme.fg("toolTitle", theme.bold(this.toolName));
1395
+ + const errorMark = this.result?.isError && !this.isPartial ? theme.fg("error", "\u2717 ") : "";
1396
+ + let text = errorMark + theme.fg("toolTitle", theme.bold(this.toolName));
1397
+ const content = JSON.stringify(this.args, null, 2);
1398
+ if (content) {
1399
+ text += `\n\n${content}`;
1122
1400
  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
1123
1401
  index 42e655d..d00bec9 100644
1124
1402
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
@@ -1269,6 +1547,40 @@ index 42e655d..d00bec9 100644
1269
1547
  }
1270
1548
  async getUserInput() {
1271
1549
  const queuedInput = this.pendingUserInputs.shift();
1550
+ 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
+ index 9db9cbd..b370180 100644
1552
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json
1553
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json
1554
+ @@ -39,9 +39,9 @@
1555
+ "customMessageBg": "customMsgBg",
1556
+ "customMessageText": "text",
1557
+ "customMessageLabel": "#9575cd",
1558
+ - "toolPendingBg": "toolPendingBg",
1559
+ - "toolSuccessBg": "toolSuccessBg",
1560
+ - "toolErrorBg": "toolErrorBg",
1561
+ + "toolPendingBg": "",
1562
+ + "toolSuccessBg": "",
1563
+ + "toolErrorBg": "",
1564
+ "toolTitle": "text",
1565
+ "toolOutput": "gray",
1566
+
1567
+ diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json
1568
+ index 74ef3d1..709a21d 100644
1569
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json
1570
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json
1571
+ @@ -38,9 +38,9 @@
1572
+ "customMessageBg": "customMsgBg",
1573
+ "customMessageText": "text",
1574
+ "customMessageLabel": "#7e57c2",
1575
+ - "toolPendingBg": "toolPendingBg",
1576
+ - "toolSuccessBg": "toolSuccessBg",
1577
+ - "toolErrorBg": "toolErrorBg",
1578
+ + "toolPendingBg": "",
1579
+ + "toolSuccessBg": "",
1580
+ + "toolErrorBg": "",
1581
+ "toolTitle": "text",
1582
+ "toolOutput": "mediumGray",
1583
+
1272
1584
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js b/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js
1273
1585
  index 4230b18..f47e80f 100644
1274
1586
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js
@@ -134,6 +134,13 @@ export function isAccountCapCode(code: string | null | undefined): boolean {
134
134
  // patches/@earendil-works+pi-coding-agent+*.patch, which mirrors these two helpers):
135
135
  // squeeze the page down to the line a person can act on, and let the STATUS decide
136
136
  // retryability rather than a substring of the body.
137
+ //
138
+ // A first-attempt hard 4xx is also terminal for the post-run loop. Stock Pi still
139
+ // falls through to compaction when _retryAttempt is 0, and a session that already
140
+ // has usage will summarise — another LLM call to the same blocked endpoint. The
141
+ // summarizer retries on the raw HTML (it contains "500"/"502"), so the agent looks
142
+ // hung until that budget burns. The patch returns false from _handlePostAgentRun
143
+ // before _checkCompaction when isHardHttpFailure matches.
137
144
 
138
145
  /** Hard cap on an error message we display, persist, or classify. */
139
146
  export const MAX_ERROR_CHARS = 2_000;