privateer-agent 0.12.34 → 0.12.36

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.
@@ -68,10 +68,10 @@ index f04269f..536e030 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 fd82b71..5e0821c 100644
71
+ index fd82b71..7c10ba9 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,111 @@ import { createLocalBashOperations } from "./tools/bash.js";
74
+ @@ -38,6 +38,122 @@ 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";
@@ -139,6 +139,17 @@ index fd82b71..5e0821c 100644
139
139
  + const status = Number(m[1]);
140
140
  + return status >= 400 && status < 500 && !PV_TRANSIENT_CLIENT_STATUS.has(status);
141
141
  +}
142
+ +// Privateer patch: undici's idle-timeout errors. Mirrors isIdleTimeoutError in
143
+ +// src/engine/errors.ts. The text is exact and stable — undici throws
144
+ +// BodyTimeoutError / HeadersTimeoutError with message "Body/Headers Timeout Error"
145
+ +// and code UND_ERR_BODY_TIMEOUT / UND_ERR_HEADERS_TIMEOUT — and it carries NO HTTP
146
+ +// status. pi's retry regex therefore reads the word "timeout" and silently re-sends
147
+ +// the whole turn across the full budget: three 5-minute stalls before the user sees
148
+ +// anything at all. A stalled transport is a fact; a body substring is a guess.
149
+ +export function isIdleTimeoutError(text) {
150
+ + const s = typeof text === "string" ? text : "";
151
+ + return /(?:^|\b)(?:Body|Headers) Timeout Error\b/i.test(s) || /^UND_ERR_(?:BODY|HEADERS)_TIMEOUT$/.test(s.trim());
152
+ +}
142
153
  +// Privateer patch: throttle handling. Mirrors isThrottleFailure / retryAfterMs /
143
154
  +// retryDelayMs in src/engine/errors.ts — see the incident note there.
144
155
  +export function isThrottleFailure(text) {
@@ -183,7 +194,7 @@ index fd82b71..5e0821c 100644
183
194
  /**
184
195
  * Parse a skill block from message text.
185
196
  * Returns null if the text doesn't contain a skill block.
186
- @@ -102,6 +207,8 @@ export class AgentSession {
197
+ @@ -102,6 +218,8 @@ export class AgentSession {
187
198
  // Retry state
188
199
  _retryAbortController = undefined;
189
200
  _retryAttempt = 0;
@@ -192,7 +203,7 @@ index fd82b71..5e0821c 100644
192
203
  // Bash execution state
193
204
  _bashAbortControllers = new Set();
194
205
  _pendingBashMessages = [];
195
- @@ -186,6 +293,14 @@ export class AgentSession {
206
+ @@ -186,6 +304,14 @@ export class AgentSession {
196
207
  }
197
208
  const isOAuth = this._modelRuntime.isUsingOAuth(model.provider);
198
209
  if (isOAuth) {
@@ -207,7 +218,7 @@ index fd82b71..5e0821c 100644
207
218
  throw new Error(`Authentication failed for "${model.provider}". ` +
208
219
  `Credentials may have expired or network is unavailable. ` +
209
220
  `Run '/login ${model.provider}' to re-authenticate.`);
210
- @@ -380,6 +495,16 @@ export class AgentSession {
221
+ @@ -380,6 +506,16 @@ export class AgentSession {
211
222
  }
212
223
  }
213
224
  }
@@ -224,7 +235,7 @@ index fd82b71..5e0821c 100644
224
235
  // Emit to extensions first
225
236
  await this._emitExtensionEvent(event);
226
237
  // Notify all listeners
227
- @@ -801,6 +926,33 @@ export class AgentSession {
238
+ @@ -801,6 +937,33 @@ export class AgentSession {
228
239
  finalError: msg.errorMessage,
229
240
  });
230
241
  this._retryAttempt = 0;
@@ -258,7 +269,7 @@ index fd82b71..5e0821c 100644
258
269
  }
259
270
  if (await this._checkCompaction(msg)) {
260
271
  return true;
261
- @@ -882,6 +1034,14 @@ export class AgentSession {
272
+ @@ -882,6 +1045,14 @@ export class AgentSession {
262
273
  if (!hasConfiguredAuth) {
263
274
  const isOAuth = this._modelRuntime.isUsingOAuth(this.model.provider);
264
275
  if (isOAuth) {
@@ -273,7 +284,7 @@ index fd82b71..5e0821c 100644
273
284
  throw new Error(`Authentication failed for "${this.model.provider}". ` +
274
285
  `Credentials may have expired or network is unavailable. ` +
275
286
  `Run '/login ${this.model.provider}' to re-authenticate.`);
276
- @@ -891,7 +1051,13 @@ export class AgentSession {
287
+ @@ -891,7 +1062,13 @@ export class AgentSession {
277
288
  // Check if we need to compact before sending (catches aborted responses).
278
289
  // The user's new prompt is sent below, so do not call agent.continue() here.
279
290
  const lastAssistant = this._findLastAssistantMessage();
@@ -288,7 +299,7 @@ index fd82b71..5e0821c 100644
288
299
  await this._checkCompaction(lastAssistant, false);
289
300
  }
290
301
  // Build messages array (custom message if any, then user message)
291
- @@ -2242,6 +2408,27 @@ export class AgentSession {
302
+ @@ -2242,6 +2419,34 @@ export class AgentSession {
292
303
  // Context overflow is handled by compaction, not retry.
293
304
  if (isContextOverflow(message, this.model?.contextWindow ?? 0))
294
305
  return false;
@@ -312,11 +323,18 @@ index fd82b71..5e0821c 100644
312
323
  + // rate limit is affected. Mirrors isAccountCapCode in src/engine/errors.ts.
313
324
  + if (this.model?.provider === "privateer" &&
314
325
  + /"code"\s*:\s*"[A-Z0-9_]*(?:CAP|QUOTA|LIMIT_REACHED|INSUFFICIENT|TOP_?UP)[A-Z0-9_]*"|limit of [^.]*reached|upgrade or top ?up|usage limit reached/i.test(message.errorMessage ?? ""))
326
+ + return false;
327
+ + // Privateer patch: a stalled stream is a transport fact, not a body substring.
328
+ + // undici's "Body/Headers Timeout Error" contains "timeout", so pi spent the
329
+ + // whole retry budget re-sending a turn the transport had already given up on —
330
+ + // a 5-minute stall became three, and only then did the user see a message.
331
+ + // Mirrors isIdleTimeoutError in src/engine/errors.ts.
332
+ + if (isIdleTimeoutError(message.errorMessage))
315
333
  + return false;
316
334
  return isRetryableAssistantError(message);
317
335
  }
318
336
  /**
319
- @@ -2287,7 +2474,11 @@ export class AgentSession {
337
+ @@ -2287,7 +2492,11 @@ export class AgentSession {
320
338
  this._retryAttempt--;
321
339
  return false;
322
340
  }
@@ -375,7 +393,7 @@ index 197bccc..bc9ac3f 100644
375
393
  //# sourceMappingURL=auth-guidance.js.map
376
394
 
377
395
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js
378
- index bb022d4..382ff2b 100644
396
+ index bb022d4..207f3ba 100644
379
397
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js
380
398
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js
381
399
  @@ -18,7 +18,7 @@ import { createJiti } from "jiti/static";
@@ -387,7 +405,23 @@ index bb022d4..382ff2b 100644
387
405
  // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
388
406
  // avoiding a circular dependency. Extensions can import from @earendil-works/pi-coding-agent.
389
407
  import * as _bundledPiCodingAgent from "../../index.js";
390
- @@ -621,9 +621,23 @@ export async function discoverAndLoadExtensions(configuredPaths, cwd, agentDir =
408
+ @@ -419,11 +419,14 @@ async function loadExtensionModule(extensionPath, cacheToken) {
409
+ // Compiled binaries and the bundled Node distribution use embedded modules.
410
+ // Source TypeScript reuses host modules and root tsconfig paths. Unbundled
411
+ // Node builds use dist aliases.
412
+ + // Privateer: reuse the host's Pi/Typebox namespaces in Node builds too.
413
+ + // Otherwise each extension can reload that graph through Jiti. Aliases
414
+ + // remain a fallback for non-virtual imports; extension reload is unchanged.
415
+ ...(isBunBinary || isNodeSeaBinary || isBundledNode
416
+ ? { virtualModules: VIRTUAL_MODULES, tryNative: false }
417
+ : isTypeScriptSourceRuntime
418
+ ? { virtualModules: VIRTUAL_MODULES, tsconfigPaths: true }
419
+ - : { alias: getAliases() }),
420
+ + : { virtualModules: VIRTUAL_MODULES, alias: getAliases() }),
421
+ });
422
+ const module = await jiti.import(extensionPath, { default: true });
423
+ const factory = module;
424
+ @@ -621,9 +624,23 @@ export async function discoverAndLoadExtensions(configuredPaths, cwd, agentDir =
391
425
  }
392
426
  }
393
427
  };
@@ -1641,7 +1675,7 @@ index c33fc39..060a38f 100644
1641
1675
  if (content) {
1642
1676
  text += `\n\n${content}`;
1643
1677
  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
1644
- index a27754f..61a1fff 100644
1678
+ index a27754f..3403ca8 100644
1645
1679
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
1646
1680
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
1647
1681
  @@ -10,7 +10,7 @@ import * as TuiLayouts from "@earendil-works/pi-tui";
@@ -1667,7 +1701,7 @@ index a27754f..61a1fff 100644
1667
1701
  if (!sessionManager.usesDefaultSessionDir()) {
1668
1702
  args.push("--session-dir", quoteIfNeeded(sessionManager.getSessionDir()));
1669
1703
  }
1670
- @@ -232,6 +237,54 @@ export function createInteractiveTuiReference(getTui) {
1704
+ @@ -232,6 +237,64 @@ export function createInteractiveTuiReference(getTui) {
1671
1705
  getPrototypeOf: () => Reflect.getPrototypeOf(getTui()),
1672
1706
  });
1673
1707
  }
@@ -1700,8 +1734,18 @@ index a27754f..61a1fff 100644
1700
1734
  +function pvDescribeErrorText(text) {
1701
1735
  + const s = typeof text === "string" ? text : "";
1702
1736
  + const status = Number(/^\s*(\d{3})\b/.exec(s)?.[1] ?? NaN);
1703
- + if (!Number.isFinite(status))
1737
+ + if (!Number.isFinite(status)) {
1738
+ + // Privateer patch: no HTTP status, but undici's idle-timeout message is exact
1739
+ + // and is the one error a stalled turn produces — printing it raw leaves the
1740
+ + // user with two words and no next step. Mirrors isIdleTimeoutError /
1741
+ + // describeErrorText in src/engine/errors.ts.
1742
+ + if (/(?:^|\b)(?:Body|Headers) Timeout Error\b/i.test(s))
1743
+ + return {
1744
+ + message: "The provider stopped responding — the connection went idle.",
1745
+ + hint: "No data arrived for the whole idle-timeout window, so the turn was cut off. Send it again, or run /model to switch providers (a slow model can be given longer in /settings → HTTP idle timeout).",
1746
+ + };
1704
1747
  + return null;
1748
+ + }
1705
1749
  + if (status === 429) {
1706
1750
  + const stated = pvRetryAfterMs(s);
1707
1751
  + return {
@@ -1722,7 +1766,7 @@ index a27754f..61a1fff 100644
1722
1766
  export class InteractiveMode {
1723
1767
  runtimeHost;
1724
1768
  renderer;
1725
- @@ -439,9 +492,15 @@ export class InteractiveMode {
1769
+ @@ -439,9 +502,15 @@ export class InteractiveMode {
1726
1770
  }
1727
1771
  getBuiltInCommandConflictDiagnostics(extensionRunner) {
1728
1772
  const builtinNames = new Set(BUILTIN_SLASH_COMMANDS.map((command) => command.name));
@@ -1739,7 +1783,7 @@ index a27754f..61a1fff 100644
1739
1783
  .map((command) => ({
1740
1784
  type: "warning",
1741
1785
  message: command.invocationName === command.name
1742
- @@ -815,20 +874,18 @@ export class InteractiveMode {
1786
+ @@ -815,20 +884,18 @@ export class InteractiveMode {
1743
1787
  this.showNewVersionNotification(newRelease);
1744
1788
  }
1745
1789
  });
@@ -1772,7 +1816,7 @@ index a27754f..61a1fff 100644
1772
1816
  // Check tmux keyboard setup asynchronously
1773
1817
  this.checkTmuxKeyboardSetup().then((warning) => {
1774
1818
  if (warning) {
1775
- @@ -2393,7 +2450,17 @@ export class InteractiveMode {
1819
+ @@ -2393,7 +2460,17 @@ export class InteractiveMode {
1776
1820
  if (text === "/model" || text.startsWith("/model ")) {
1777
1821
  const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
1778
1822
  this.editor.setText("");
@@ -1791,7 +1835,7 @@ index a27754f..61a1fff 100644
1791
1835
  return;
1792
1836
  }
1793
1837
  if (text === "/thinking" || text.startsWith("/thinking ")) {
1794
- @@ -2465,12 +2532,42 @@ export class InteractiveMode {
1838
+ @@ -2465,12 +2542,42 @@ export class InteractiveMode {
1795
1839
  if (text === "/login" || text.startsWith("/login ")) {
1796
1840
  const providerRef = text.startsWith("/login ") ? text.slice(7).trim() : undefined;
1797
1841
  this.editor.setText("");
@@ -1836,7 +1880,30 @@ index a27754f..61a1fff 100644
1836
1880
  return;
1837
1881
  }
1838
1882
  if (text === "/new") {
1839
- @@ -3164,7 +3261,7 @@ export class InteractiveMode {
1883
+ @@ -2788,9 +2895,19 @@ export class InteractiveMode {
1884
+ if (entries[0]?.type !== "compaction") {
1885
+ throw new Error("Completed compaction is missing from the session context");
1886
+ }
1887
+ - this.chatContainer.clear();
1888
+ - // The latest compaction is prepended for model context; append it below at its chronological position.
1889
+ - this.renderSessionEntries(entries.slice(1));
1890
+ + // Privateer patch: a compaction must never erase what the user can see.
1891
+ + // Stock Pi cleared the whole chat container here and re-rendered only
1892
+ + // buildContextEntries().slice(1) — the MODEL's new context, a summary plus
1893
+ + // the firstKeptEntryId tail. That is the right set of messages to send to
1894
+ + // the model and the wrong set to leave on screen: every prompt, answer and
1895
+ + // tool result above the keep boundary vanished from the transcript the
1896
+ + // instant the summary appeared, so the user watched the thing they typed get
1897
+ + // swallowed and replaced by a summary — worst of all when the compaction was
1898
+ + // triggered by that very prompt. The model context is allowed to shrink; the
1899
+ + // transcript is the record of what actually happened and does not. The kept
1900
+ + // tail is already on screen (it was rendered as it happened), so re-rendering
1901
+ + // it would duplicate it — keep every child and append the summary at its
1902
+ + // chronological position, which is the boundary ("now").
1903
+ this.addMessageToChat(createCompactionSummaryMessage(event.result.summary, event.result.tokensBefore, new Date().toISOString()));
1904
+ if (event.result.usage) {
1905
+ this.addCompactionCostNotice({
1906
+ @@ -3164,7 +3281,7 @@ export class InteractiveMode {
1840
1907
  if (this.chatContainer.children.length > 0) {
1841
1908
  this.chatContainer.addChild(new Spacer(1));
1842
1909
  }
@@ -1845,7 +1912,7 @@ index a27754f..61a1fff 100644
1845
1912
  }
1846
1913
  async getUserInput() {
1847
1914
  const queuedInput = this.pendingUserInputs.shift();
1848
- @@ -3504,7 +3601,17 @@ export class InteractiveMode {
1915
+ @@ -3504,7 +3621,17 @@ export class InteractiveMode {
1849
1916
  }
1850
1917
  showError(errorMessage) {
1851
1918
  this.chatContainer.addChild(new Spacer(1));
@@ -0,0 +1,110 @@
1
+ diff --git a/node_modules/pi-background-tasks/src/core/registry.ts b/node_modules/pi-background-tasks/src/core/registry.ts
2
+ index cfbf133..c632390 100644
3
+ --- a/node_modules/pi-background-tasks/src/core/registry.ts
4
+ +++ b/node_modules/pi-background-tasks/src/core/registry.ts
5
+ @@ -1,11 +1,11 @@
6
+ import { spawn as nodeSpawn, type SpawnOptions } from 'node:child_process';
7
+ -import { randomBytes } from 'node:crypto';
8
+ +import { createHash, randomBytes } from 'node:crypto';
9
+ import { createWriteStream, existsSync } from 'node:fs';
10
+ import { mkdir, realpath, writeFile } from 'node:fs/promises';
11
+ -import { join } from 'node:path';
12
+ +import { join, resolve } from 'node:path';
13
+ import type { Api, Model } from '@earendil-works/pi-ai';
14
+ import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
15
+ -import { formatSize } from '@earendil-works/pi-coding-agent';
16
+ +import { formatSize, getAgentDir } from '@earendil-works/pi-coding-agent';
17
+ import {
18
+ boundedRead,
19
+ deriveTaskNameFromCommand,
20
+ @@ -781,9 +781,29 @@ export class BackgroundTaskRegistry {
21
+ if (this.runtimeDir) return this.runtimeDir;
22
+ const sessionId = sanitizePathSegment(ctx.sessionId ?? `session-${String(process.pid)}`);
23
+ const runId = `${sessionId}-${String(process.pid)}`;
24
+ - const runtimeDirAbs = join(ctx.cwd, '.pi', 'tasks', runId);
25
+ - const runtimeDirDisplay = join('.pi', 'tasks', runId);
26
+ - await mkdir(runtimeDirAbs, { recursive: true });
27
+ + let runtimeDirAbs = join(ctx.cwd, '.pi', 'tasks', runId);
28
+ + let runtimeDirDisplay = join('.pi', 'tasks', runId);
29
+ + try {
30
+ + await mkdir(runtimeDirAbs, { recursive: true });
31
+ + } catch (error) {
32
+ + // Privateer: Git Bash can open in Program Files. Keep the command cwd,
33
+ + // but store task artifacts in the user's agent dir when cwd is protected.
34
+ + const code = (error as NodeJS.ErrnoException).code;
35
+ + if (code !== 'EACCES' && code !== 'EPERM' && code !== 'EROFS') throw error;
36
+ + const projectDir = runtimeDirAbs;
37
+ + const projectId = createHash('sha256').update(resolve(ctx.cwd)).digest('hex').slice(0, 16);
38
+ + runtimeDirAbs = resolve(getAgentDir(), 'tasks', projectId, runId);
39
+ + // Never describe an external artifact as project-relative.
40
+ + runtimeDirDisplay = runtimeDirAbs;
41
+ + try {
42
+ + await mkdir(runtimeDirAbs, { recursive: true, mode: 0o700 });
43
+ + } catch (fallbackError) {
44
+ + throw new Error(
45
+ + `Cannot create background task storage at ${projectDir} (${code}) or ${runtimeDirAbs}: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`,
46
+ + { cause: fallbackError },
47
+ + );
48
+ + }
49
+ + }
50
+ this.runtimeDir = { abs: runtimeDirAbs, display: runtimeDirDisplay };
51
+ return this.runtimeDir;
52
+ }
53
+ diff --git a/node_modules/pi-background-tasks/src/extension.ts b/node_modules/pi-background-tasks/src/extension.ts
54
+ index 3afbd8e..d003a57 100644
55
+ --- a/node_modules/pi-background-tasks/src/extension.ts
56
+ +++ b/node_modules/pi-background-tasks/src/extension.ts
57
+ @@ -473,14 +473,15 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
58
+ pi.on('session_start', async (_event, ctx) => {
59
+ registry.setShuttingDown(false);
60
+ currentCtx = ctx;
61
+ - await registry.ensureRuntimeDir(ctx);
62
+ + // Privateer: no project writes just to open a session. Each task-start path
63
+ + // creates its runtime directory on demand, with a per-user fallback.
64
+ updateUi(ctx);
65
+ if (statusInterval) clearInterval(statusInterval);
66
+ statusInterval = setInterval(() => {
67
+ updateUi();
68
+ }, STATUS_INTERVAL_MS);
69
+ - // One-shot, non-blocking: never awaited on the session-start path or the status tick.
70
+ - void scheduleUpdateCheck(ctx);
71
+ + // Privateer: update check disabled. Privateer manages updates at the distribution
72
+ + // level via `privateer update`; individual tool packs shouldn't check npm or prompt /bg-update.
73
+ });
74
+
75
+ pi.on('session_shutdown', async (_event, ctx) => {
76
+ @@ -565,22 +566,11 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
77
+ });
78
+
79
+ pi.registerCommand('bg-update', {
80
+ - description: 'Show how to update pi-background-tasks to the latest published version',
81
+ + description: 'Show update guidance for background tasks in Privateer',
82
+ handler: (_args, ctx) => {
83
+ - const current = PACKAGE_VERSION ?? 'unknown';
84
+ - const latest = latestKnownVersion;
85
+ - const pinnedNpm = latest ? `${PACKAGE_NAME}@${latest}` : `${PACKAGE_NAME}@<version>`;
86
+ const lines = [
87
+ - latest
88
+ - ? `pi-background-tasks ${current} is installed; ${latest} is the latest published version.`
89
+ - : `pi-background-tasks ${current} is installed.`,
90
+ - 'Update from npm:',
91
+ - ` pi install npm:${PACKAGE_NAME}@latest`,
92
+ - ` pi install npm:${pinnedNpm}`,
93
+ - 'Git releases are independent of npm versions; use main only when you want current repository state:',
94
+ - ` pi install ${GIT_INSTALL_TARGET}@main`,
95
+ - `For a pinned git release, first verify the tag exists, then use ${GIT_INSTALL_TARGET}@<existing-tag>.`,
96
+ - 'This command only prints update instructions; it does not install or self-update.',
97
+ + 'pi-background-tasks is built into Privateer.',
98
+ + 'To update Privateer and bundled tools, run: privateer update',
99
+ ];
100
+ ctx.ui.notify(lines.join('\n'), 'info');
101
+ return Promise.resolve();
102
+ @@ -682,7 +672,7 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
103
+ pi.registerTool<typeof BgRunParams, BgRunDetails>({
104
+ name: 'bg_run',
105
+ label: 'Background Run',
106
+ - description: `Start a named long-running shell command in the background and return immediately with a task ID and output path. By default, completed, failed, or killed terminal state is delivered automatically as <background-task-notification> and starts a follow-up agent turn; do not sleep or poll merely to wait. Output is written to .pi/tasks and model-visible logs are bounded to ${formatSize(MAX_LOG_BYTES)}.`,
107
+ + description: `Start a named long-running shell command in the background and return immediately with a task ID and output path. By default, completed, failed, or killed terminal state is delivered automatically as <background-task-notification> and starts a follow-up agent turn; do not sleep or poll merely to wait. Output is written to .pi/tasks (or per-user agent storage when the project denies writes); use the returned output path. Model-visible logs are bounded to ${formatSize(MAX_LOG_BYTES)}.`,
108
+ promptSnippet:
109
+ 'Start a named long-running shell command; default terminal notification wakes a follow-up turn, so yield instead of polling',
110
+ promptGuidelines: [
package/src/boot.ts CHANGED
@@ -38,6 +38,13 @@ process.env[PI_AGENT_DIR_ENV] ??= agentDir();
38
38
  // (2) Install the attestation dispatcher (idempotent).
39
39
  installAttestationDispatcher();
40
40
 
41
+ // (3) Suppress upstream update checks: Pi's CLI update banner and pi-background-tasks'
42
+ // update check / footer notice ("bg ⬆ v... /bg-update"). Privateer manages updates
43
+ // at the distribution level via `privateer update` and its own banner; individual
44
+ // bundled tool packs must not query npm or display `/bg-update`.
45
+ process.env.PI_SKIP_VERSION_CHECK ??= "1";
46
+ process.env.PI_BG_DISABLE_UPDATE_CHECK ??= "1";
47
+
41
48
  // Marker other modules can assert on to catch an accidental "imported Pi before
42
49
  // boot" regression during development.
43
50
  export const BOOTED = true;
@@ -10,9 +10,9 @@
10
10
  // - `finish` carries usage + finishReason,
11
11
  // - compaction / auto-retry / agent-end-error mapped through.
12
12
  //
13
- // Field names for the compaction/retry/error events are best-effort against Pi
14
- // 0.80 and marked TODO(verify) — Phase 1's test (`tests/engine.test.ts`, ported
15
- // from tree-cli) asserts the mapping against a real event stream and pins them.
13
+ // Pi's turn_end is ONE model response plus tools, not the user's whole run.
14
+ // agent_end can still be followed by retries/compaction. Only agent_settled
15
+ // (Pi 0.84) is terminal; keeping this distinction preserves the app's busy pill.
16
16
 
17
17
  import {
18
18
  addUsage,
@@ -55,7 +55,7 @@ function textOf(result: unknown): string {
55
55
  const parts = (result as any)?.content;
56
56
  if (Array.isArray(parts)) return parts.map((p: any) => p.text ?? `[${p.type}]`).join("");
57
57
  try {
58
- return JSON.stringify(result);
58
+ return JSON.stringify(result) ?? "";
59
59
  } catch {
60
60
  return String(result);
61
61
  }
@@ -70,9 +70,16 @@ function num(v: unknown, fallback = 0): number {
70
70
  // each Pi event; feed each through in `session.subscribe`.
71
71
  export function createEngineEventAdapter() {
72
72
  let sessionTotal: UsageTotals = emptyUsage();
73
+ let turnTotal: UsageTotals = emptyUsage();
74
+ let lastMessage: any;
75
+ let terminalSent = false;
73
76
 
74
77
  function toEngineEvents(ev: PiSessionEvent): EngineEvent[] {
75
78
  switch (ev.type) {
79
+ case "agent_start":
80
+ terminalSent = false;
81
+ return [];
82
+
76
83
  case "message_update": {
77
84
  const a = ev.assistantMessageEvent as any;
78
85
  if (a?.type === "text_delta") return [{ type: "text", text: a.delta }];
@@ -90,6 +97,16 @@ export function createEngineEventAdapter() {
90
97
  },
91
98
  ];
92
99
 
100
+ // Pi partial results are snapshots, NOT deltas. Transports bound these;
101
+ // cloud redaction must see the full text BEFORE any tail truncation.
102
+ case "tool_execution_update":
103
+ return [{
104
+ type: "tool-progress",
105
+ id: ev.toolCallId as string,
106
+ name: ev.toolName as string,
107
+ output: textOf(ev.partialResult),
108
+ }];
109
+
93
110
  case "tool_execution_end":
94
111
  return ev.isError
95
112
  ? [
@@ -113,43 +130,35 @@ export function createEngineEventAdapter() {
113
130
  const msg = ev.message as any;
114
131
  const turn = normUsage(msg?.usage);
115
132
  sessionTotal = addUsage(sessionTotal, turn);
116
- const finishReason =
117
- (ev.finishReason as string) ?? (msg?.stopReason as string) ?? "stop";
118
- const out: EngineEvent[] = [{ type: "usage", usage: sessionTotal, turn }];
119
- // A FAILED turn ends here, not by throwing — and this is the only place the
120
- // app can learn it failed.
121
- //
122
- // Pi reports a dead model call as an assistant message with
123
- // `stopReason: "error"` and an `errorMessage`; `prompt()` then resolves
124
- // NORMALLY, so the desktop's runTurn catch never fires. Our own pi patch
125
- // widened that path deliberately (a hard 4xx, and a 429 that outlived the
126
- // retry budget, both end the turn instead of re-entering the agent loop),
127
- // which is right for the CLI — its TUI reads the assistant message and
128
- // prints describeErrorText. The app reads EngineEvents, and this adapter
129
- // used to drop `errorMessage` on the floor: the turn arrived as a bare
130
- // `finish`, which RemoteDriveContext closes as a green ✓ "done". A signed-in
131
- // user whose first turn 401'd or hit their cap saw a tick and no words.
132
- //
133
- // So say it. Same wording the terminal gets, ahead of the finish so the feed
134
- // reads in order, and redacted either way because an unrecognised body goes
135
- // out verbatim.
133
+ turnTotal = addUsage(turnTotal, turn);
134
+ lastMessage = msg;
135
+ return [{ type: "usage", usage: sessionTotal, turn: turnTotal }, { type: "step-finish" }];
136
+ }
137
+
138
+ case "agent_settled": {
139
+ if (terminalSent) return [];
140
+ terminalSent = true;
141
+ const msg = lastMessage;
142
+ lastMessage = undefined;
143
+ turnTotal = emptyUsage();
144
+ // Failed model calls often resolve prompt() normally. Delay the error
145
+ // until retries/overflow recovery are exhausted, but never hide it.
136
146
  if (msg?.stopReason === "error") {
137
147
  const raw = typeof msg.errorMessage === "string" ? msg.errorMessage : "";
138
148
  const described = describeErrorText(raw);
139
- out.push({
149
+ return [{
140
150
  type: "error",
141
151
  error: described?.message ?? redactText(raw || "The model call failed."),
142
152
  ...(described?.hint ? { hint: described.hint } : {}),
143
153
  ...(described?.retryable != null ? { retryable: described.retryable } : {}),
144
- });
154
+ }];
145
155
  }
146
- out.push({ type: "finish", usage: sessionTotal, finishReason });
147
- return out;
156
+ if (msg?.stopReason === "aborted") return [{ type: "aborted" }];
157
+ return [{ type: "finish", usage: sessionTotal, finishReason: msg?.stopReason ?? "stop" }];
148
158
  }
149
159
 
150
- // Compaction: Pi collapses history to free context. TODO(verify) field
151
- // names for before/after token counts against a real compaction event.
152
160
  case "compaction_start":
161
+ return [{ type: "step-finish" }];
153
162
  case "compaction_end":
154
163
  return [
155
164
  {
@@ -173,28 +182,17 @@ export function createEngineEventAdapter() {
173
182
  },
174
183
  ];
175
184
 
176
- // Terminal error surfaced at the end of an agent run.
177
- //
178
- // ⚠️ Pi 0.84's `agent_end` is `{ messages, willRetry }` — there is NO `error`
179
- // field, so this branch has been returning [] for every run since the 0.84
180
- // refactor. It is kept (harmless, and other Pi versions have carried one)
181
- // but it is NOT the error path: a failed model call arrives on `turn_end`
182
- // above, which is where the real mapping lives. Do not "restore" error
183
- // reporting here and delete it there.
184
- case "agent_end": {
185
- const err = (ev as any).error;
186
- if (!err) return [];
187
- return [
188
- {
189
- type: "error",
190
- error: typeof err === "string" ? err : (err.message ?? String(err)),
191
- retryable: Boolean((ev as any).retryable),
192
- },
193
- ];
194
- }
185
+ // agent_end is only a low-level loop boundary. willRetry:false still
186
+ // permits overflow compaction and queued continuation; never finish here.
187
+ case "agent_end":
188
+ return [];
195
189
 
196
190
  case "abort":
197
191
  case "aborted":
192
+ if (terminalSent) return [];
193
+ terminalSent = true;
194
+ lastMessage = undefined;
195
+ turnTotal = emptyUsage();
198
196
  return [{ type: "aborted" }];
199
197
 
200
198
  default:
@@ -0,0 +1,45 @@
1
+ // Privacy toggle: completely disable pi-privacy across agent sessions.
2
+ //
3
+ // When active, all pi-privacy hooks are completely bypassed:
4
+ // - Outbound PII scanning and prompts are disabled (IP addresses, emails, and
5
+ // other non-sensitive data that trigger detection can be sent as-is with NO interference).
6
+ // - Unattended mode does NOT auto-redact PII.
7
+ // - Tool exfiltration guards and warnings are disabled.
8
+ // - Tool result credential scanning / redaction is disabled.
9
+ // - Model downgrade guards are disabled.
10
+ //
11
+ // THE ENV VAR IS THE STORE:
12
+ // Exactly like `src/permissions/noQuarter.ts`, state is stored in `process.env.PRIVATEER_PRIVACY_OFF`
13
+ // and `process.env.PI_PRIVACY_OFF`:
14
+ // 1. ACROSS PROCESSES: subagent children inherit the env var and run without privacy gates.
15
+ // 2. ACROSS EXTENSIONS: different jiti instances and late-loaded extensions all share process.env.
16
+ // 3. LAUNCH CONFIG: users can launch with `PRIVATEER_PRIVACY_OFF=1` or `PI_PRIVACY_OFF=1`
17
+ // or `--privacy-off` / `--no-privacy` CLI flags.
18
+
19
+ const ENV = "PRIVATEER_PRIVACY_OFF";
20
+ const ALT_ENV = "PI_PRIVACY_OFF";
21
+
22
+ /** True while pi-privacy is completely disabled for this session. Read live, never cached. */
23
+ export function privacyDisabled(): boolean {
24
+ return process.env[ENV] === "1" || process.env[ALT_ENV] === "1";
25
+ }
26
+
27
+ /**
28
+ * Set the disabled state. Modifies process.env so every module copy and child process agrees.
29
+ * Returns the new state.
30
+ */
31
+ export function setPrivacyDisabled(disabled: boolean): boolean {
32
+ if (disabled) {
33
+ process.env[ENV] = "1";
34
+ process.env[ALT_ENV] = "1";
35
+ } else {
36
+ delete process.env[ENV];
37
+ delete process.env[ALT_ENV];
38
+ }
39
+ return disabled;
40
+ }
41
+
42
+ /** Flip the disabled state. Returns the new state. */
43
+ export function togglePrivacyDisabled(): boolean {
44
+ return setPrivacyDisabled(!privacyDisabled());
45
+ }