privateer-agent 0.14.4 → 0.14.5
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.14.
|
|
3
|
+
"version": "0.14.5",
|
|
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",
|
|
@@ -73,11 +73,17 @@ export function createEngineEventAdapter() {
|
|
|
73
73
|
let turnTotal: UsageTotals = emptyUsage();
|
|
74
74
|
let lastMessage: any;
|
|
75
75
|
let terminalSent = false;
|
|
76
|
+
// Between agent_start and agent_settled. Compaction maps to `step-finish` to keep
|
|
77
|
+
// the app's busy pill lit through an auto-compaction INSIDE a run — but a manual
|
|
78
|
+
// `/compact` runs with no run around it, and a `step-finish` there opened a turn
|
|
79
|
+
// in the app that no `finish` would ever close: the pill spun until Stop.
|
|
80
|
+
let inRun = false;
|
|
76
81
|
|
|
77
82
|
function toEngineEvents(ev: PiSessionEvent): EngineEvent[] {
|
|
78
83
|
switch (ev.type) {
|
|
79
84
|
case "agent_start":
|
|
80
85
|
terminalSent = false;
|
|
86
|
+
inRun = true;
|
|
81
87
|
return [];
|
|
82
88
|
|
|
83
89
|
case "message_update": {
|
|
@@ -136,6 +142,7 @@ export function createEngineEventAdapter() {
|
|
|
136
142
|
}
|
|
137
143
|
|
|
138
144
|
case "agent_settled": {
|
|
145
|
+
inRun = false;
|
|
139
146
|
if (terminalSent) return [];
|
|
140
147
|
terminalSent = true;
|
|
141
148
|
const msg = lastMessage;
|
|
@@ -158,7 +165,7 @@ export function createEngineEventAdapter() {
|
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
case "compaction_start":
|
|
161
|
-
return [{ type: "step-finish" }];
|
|
168
|
+
return inRun ? [{ type: "step-finish" }] : [];
|
|
162
169
|
case "compaction_end":
|
|
163
170
|
return [
|
|
164
171
|
{
|
package/src/cli/chat.ts
CHANGED
|
@@ -349,6 +349,7 @@ async function main() {
|
|
|
349
349
|
return;
|
|
350
350
|
}
|
|
351
351
|
turnActive = true;
|
|
352
|
+
let failure: string | undefined;
|
|
352
353
|
if (remote && echo) console.log(`\n${DIM}⟿ [app] ${text.slice(0, 80)}${RESET}`);
|
|
353
354
|
try {
|
|
354
355
|
// Re-arm the account channel BEFORE the prompt, because nothing downstream can.
|
|
@@ -375,10 +376,13 @@ async function main() {
|
|
|
375
376
|
}
|
|
376
377
|
await session.prompt(mentions.text, mentions.images.length ? { images: mentions.images } : undefined);
|
|
377
378
|
} catch (e) {
|
|
378
|
-
|
|
379
|
+
failure = (e as Error).message || "The turn failed.";
|
|
380
|
+
console.log(`\n${RED}${failure}${RESET}`);
|
|
379
381
|
} finally {
|
|
380
382
|
turnActive = false;
|
|
381
|
-
|
|
383
|
+
// The failure goes to the app too: a throw here (no model, no key, auth
|
|
384
|
+
// precheck) used to print on this console only while the app's pill spun on.
|
|
385
|
+
if (remote) bridge.settleTurn(failure);
|
|
382
386
|
}
|
|
383
387
|
}
|
|
384
388
|
|
package/src/config/whatsNew.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "The banner's What's New list, newest first: the two to four things a returning user should notice, not a changelog. `reviewedFor` is the release this list was last looked at for. A release can't ship until it equals package.json's version (scripts/check-whats-new.mjs, run by `npm test` and by release.yml before anything builds), so every release, patches included, is a deliberate look at this list. If nothing new is worth a line, bumping `reviewedFor` alone is the answer. `since` is the release an entry arrived in. `cmd` is shown in the accent colour.",
|
|
3
|
-
"reviewedFor": "0.14.
|
|
3
|
+
"reviewedFor": "0.14.5",
|
|
4
4
|
"items": [
|
|
5
5
|
{ "text": "Save to Cargo with no app attached — it lands on your next app open", "since": "0.14.4" },
|
|
6
6
|
{ "text": "Several privateer -p runs at once no longer sign each other out", "since": "0.14.3" },
|
|
@@ -140,6 +140,7 @@ export async function createLiveTaskSession(spec: TaskSpec, deps: LiveTaskDeps):
|
|
|
140
140
|
const runTurn = async (text: string): Promise<void> => {
|
|
141
141
|
if (turnActive || stopped) return;
|
|
142
142
|
turnActive = true;
|
|
143
|
+
let failure: string | undefined;
|
|
143
144
|
lastAnswer = ""; // keep the CLOSING answer, not the whole session
|
|
144
145
|
turnErrored = false;
|
|
145
146
|
try {
|
|
@@ -161,10 +162,11 @@ export async function createLiveTaskSession(spec: TaskSpec, deps: LiveTaskDeps):
|
|
|
161
162
|
await session.prompt(text + note);
|
|
162
163
|
} catch (e) {
|
|
163
164
|
turnErrored = true;
|
|
164
|
-
|
|
165
|
+
failure = (e as Error).message || "The turn failed.";
|
|
166
|
+
deps.log(`live task ${termId} turn error: ${failure}`);
|
|
165
167
|
} finally {
|
|
166
168
|
turnActive = false;
|
|
167
|
-
bridge.settleTurn();
|
|
169
|
+
bridge.settleTurn(failure);
|
|
168
170
|
}
|
|
169
171
|
};
|
|
170
172
|
|
|
@@ -15,7 +15,8 @@ import { noQuarterActive, setNoQuarter } from "../permissions/noQuarter.ts";
|
|
|
15
15
|
// Pure env-var accessors (see config/privacyDisabled.ts) — no Pi, no node builtins,
|
|
16
16
|
// so this stays safe to import from the bridge, which boot-ordered code reaches.
|
|
17
17
|
import { onPrivacyDisabledChange, privacyDisabled, setPrivacyDisabled } from "../config/privacyDisabled.ts";
|
|
18
|
-
import type
|
|
18
|
+
import { emptyUsage, type EngineEvent } from "../engine/events.ts";
|
|
19
|
+
import { redactText } from "../util/redact.ts";
|
|
19
20
|
import type { PermissionRequest } from "../permissions/gate.ts";
|
|
20
21
|
import type { AskOutcome } from "../permissions/modeGate.ts";
|
|
21
22
|
import type { RelayCallbacks } from "./relayClient.ts";
|
|
@@ -196,6 +197,14 @@ export class RemoteBridge {
|
|
|
196
197
|
private turnDriven = false;
|
|
197
198
|
private turnPrompt = "";
|
|
198
199
|
private turnText = "";
|
|
200
|
+
// Did this turn already put finish/aborted/error on the wire? The app lights its
|
|
201
|
+
// working pill the moment it SENDS a prompt and only ever puts it out on one of
|
|
202
|
+
// those three, but Pi's prompt() has paths that never run the agent at all — an
|
|
203
|
+
// extension command or input hook that handles the text, a throw on the
|
|
204
|
+
// no-model / no-key / auth precheck, "compaction in progress". Each returned (or
|
|
205
|
+
// threw into a catch that only printed locally) with no terminal event, and the
|
|
206
|
+
// driver watched a spinner nothing would ever clear. settleTurn closes the gap.
|
|
207
|
+
private turnTerminalSent = false;
|
|
199
208
|
|
|
200
209
|
// Unsubscribes the privacy watcher below. Held so a bridge that outlives its window
|
|
201
210
|
// (the desktop builds one per window, in one process) stops writing to a transport
|
|
@@ -232,6 +241,7 @@ export class RemoteBridge {
|
|
|
232
241
|
this.turnDriven = true;
|
|
233
242
|
this.turnPrompt = text;
|
|
234
243
|
this.turnText = "";
|
|
244
|
+
this.turnTerminalSent = false;
|
|
235
245
|
const attachments = this.pendingAttachments;
|
|
236
246
|
this.pendingAttachments = [];
|
|
237
247
|
this.cfg.onPrompt(text, attachments);
|
|
@@ -460,20 +470,33 @@ export class RemoteBridge {
|
|
|
460
470
|
// ── turn lifecycle + event forwarding ───────────────────────────────────────
|
|
461
471
|
|
|
462
472
|
// Mark the end of a turn so the next (possibly local) turn isn't treated as
|
|
463
|
-
// remote. Call after each driven turn completes
|
|
473
|
+
// remote. Call after each driven turn completes, passing the rejection message
|
|
474
|
+
// if prompt() threw.
|
|
475
|
+
//
|
|
476
|
+
// A DRIVEN turn always leaves here with exactly one terminal event on the wire
|
|
477
|
+
// (see turnTerminalSent): if the session never sent one, this sends it — `error`
|
|
478
|
+
// carrying the throw, or a plain `finish` for a prompt Pi handled without
|
|
479
|
+
// running the agent. Desktop's turnSupervisor emits its own terminal before it
|
|
480
|
+
// settles, so it is seen here and never doubled.
|
|
464
481
|
//
|
|
465
482
|
// Also the one moment we can tell whether the turn had an audience. If the app
|
|
466
483
|
// drove it and is now gone, everything the turn streamed up was dropped by the
|
|
467
484
|
// relay — so hand the answer to the owner for durable delivery rather than letting
|
|
468
485
|
// a completed piece of work evaporate because someone closed their phone.
|
|
469
|
-
settleTurn(): void {
|
|
486
|
+
settleTurn(error?: string): void {
|
|
470
487
|
const driven = this.turnDriven;
|
|
471
488
|
const prompt = this.turnPrompt;
|
|
472
489
|
const content = this.turnText.trim();
|
|
490
|
+
if (driven && !this.turnTerminalSent) {
|
|
491
|
+
this.forwardEvent(error
|
|
492
|
+
? { type: "error", error: redactText(error) }
|
|
493
|
+
: { type: "finish", usage: emptyUsage(), finishReason: "stop" });
|
|
494
|
+
}
|
|
473
495
|
this.remote = false;
|
|
474
496
|
this.turnDriven = false;
|
|
475
497
|
this.turnPrompt = "";
|
|
476
498
|
this.turnText = "";
|
|
499
|
+
this.turnTerminalSent = false;
|
|
477
500
|
if (!driven || !content) return;
|
|
478
501
|
// Unknown (a relay that can't report presence) counts as watched: better to skip
|
|
479
502
|
// delivery than to duplicate something the app already showed in its feed.
|
|
@@ -488,6 +511,7 @@ export class RemoteBridge {
|
|
|
488
511
|
// Keep the driven turn's reply as it streams, in case settleTurn finds nobody
|
|
489
512
|
// was there to read it. Text only: the outbox item is the answer, not a
|
|
490
513
|
// transcript of every tool call.
|
|
514
|
+
if (ev.type === "finish" || ev.type === "aborted" || ev.type === "error") this.turnTerminalSent = true;
|
|
491
515
|
if (this.turnDriven && ev.type === "text" && this.turnText.length < MAX_TURN_CAPTURE) {
|
|
492
516
|
this.turnText += String(ev.text ?? "");
|
|
493
517
|
}
|