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.
- package/README.md +7 -0
- package/bin/apply-patches.mjs +29 -14
- package/bin/privateer-launch.mjs +244 -5
- package/bin/privateer-splash.mjs +2 -4
- package/bin/privateer-subagent.mjs +2 -0
- package/bin/startup-cache.d.mts +1 -0
- package/bin/startup-cache.mjs +19 -0
- package/extensions/privateer-models.ts +36 -9
- package/extensions/privateer-posture.ts +30 -1
- package/package.json +2 -2
- package/patches/@earendil-works+pi-ai+0.84.4.patch +46 -0
- package/patches/@earendil-works+pi-coding-agent+0.84.4.patch +88 -21
- package/patches/pi-background-tasks+2.4.2.patch +110 -0
- package/src/boot.ts +7 -0
- package/src/bridge/engineAdapter.ts +47 -49
- package/src/config/privacyDisabled.ts +45 -0
- package/src/config/privacyPolicy.ts +135 -40
- package/src/engine/contextBudget.ts +74 -0
- package/src/engine/errors.ts +89 -2
- package/src/engine/events.ts +3 -1
- package/src/providers/account.ts +205 -27
- package/src/providers/defaultModel.ts +4 -2
- package/src/remote/relayClient.ts +2 -0
|
@@ -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..
|
|
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,
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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..
|
|
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
|
-
@@ -
|
|
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..
|
|
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,
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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
|
-
@@ -
|
|
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 +
|
|
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
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
return
|
|
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
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
}
|