privateer-agent 0.12.18 → 0.12.19
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 +1 -1
- package/src/harbor/index.ts +30 -4
- package/src/harbor/ipc.ts +17 -0
- package/src/remote/relayClient.ts +24 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "privateer-agent",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.19",
|
|
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",
|
package/src/harbor/index.ts
CHANGED
|
@@ -651,6 +651,16 @@ export class Harbor {
|
|
|
651
651
|
}
|
|
652
652
|
|
|
653
653
|
private async tick(): Promise<void> {
|
|
654
|
+
// Retry the relay every tick, not just at startup. syncRelay() bails when the
|
|
655
|
+
// account isn't signed in on this machine — and a harbor that came up in that
|
|
656
|
+
// state used to stay off the relay FOREVER, because start() was the only caller
|
|
657
|
+
// that mattered (the three IPC commands that also call it are ones the desktop
|
|
658
|
+
// app never sends). Signing in afterwards wrote credentials.json and changed
|
|
659
|
+
// nothing: the harbor kept answering IPC, so the app called it running, while
|
|
660
|
+
// its relay socket had never been opened — the permanent "Connecting" with a
|
|
661
|
+
// harbor that fires no routine and answers no spawn. Cheap and idempotent: it
|
|
662
|
+
// returns immediately once a client exists, or while remote access is off.
|
|
663
|
+
this.syncRelay();
|
|
654
664
|
void this.flushPendingCloud();
|
|
655
665
|
const now = Date.now();
|
|
656
666
|
for (const r of loadRoutines()) {
|
|
@@ -1269,19 +1279,35 @@ export class Harbor {
|
|
|
1269
1279
|
private relayStatus(): RelayStatus {
|
|
1270
1280
|
const termId = routineRelayId();
|
|
1271
1281
|
if (this.relayTerminated) {
|
|
1272
|
-
return { termId, connected: false, detail: "remote access was turned off from the app — restart the harbor to re-enable it" };
|
|
1282
|
+
return { termId, connected: false, reason: "terminated", detail: "remote access was turned off from the app — restart the harbor to re-enable it" };
|
|
1273
1283
|
}
|
|
1274
1284
|
if (!this.relay) {
|
|
1285
|
+
// "relay not started" with credentials present is now a sub-tick window, not a
|
|
1286
|
+
// permanent state: tick() re-runs syncRelay(), so a harbor that came up signed
|
|
1287
|
+
// out connects on its own once you sign in. Reported as "connecting" because
|
|
1288
|
+
// that is what it now is.
|
|
1289
|
+
const signedIn = hasCredentials();
|
|
1275
1290
|
return {
|
|
1276
1291
|
termId,
|
|
1277
1292
|
connected: false,
|
|
1278
|
-
|
|
1293
|
+
reason: signedIn ? "connecting" : "signed-out",
|
|
1294
|
+
detail: signedIn
|
|
1279
1295
|
? "relay not started"
|
|
1280
|
-
: "no account signed in on this machine — run `privateer` and /login
|
|
1296
|
+
: "no account signed in on this machine — run `privateer` and /login",
|
|
1281
1297
|
};
|
|
1282
1298
|
}
|
|
1283
1299
|
const conn = this.relay.connectionStatus();
|
|
1284
|
-
|
|
1300
|
+
// The client knows why it isn't up — a refused ticket, an unreachable server —
|
|
1301
|
+
// and reporting a flat "connecting…" over the top of that is what made a harbor
|
|
1302
|
+
// that will never connect look like one that is about to.
|
|
1303
|
+
if (!conn.connected) {
|
|
1304
|
+
return {
|
|
1305
|
+
termId,
|
|
1306
|
+
connected: false,
|
|
1307
|
+
reason: conn.refused ? "refused" : "connecting",
|
|
1308
|
+
detail: conn.detail ?? "connecting…",
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1285
1311
|
return { termId, connected: true, upSec: conn.upSec, quietSec: conn.quietSec };
|
|
1286
1312
|
}
|
|
1287
1313
|
|
package/src/harbor/ipc.ts
CHANGED
|
@@ -42,6 +42,21 @@ export type IpcRequest =
|
|
|
42
42
|
| { cmd: "run-now"; idOrName: string }
|
|
43
43
|
| { cmd: "reload" };
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Why a harbor isn't on the relay, as a code rather than a sentence.
|
|
47
|
+
*
|
|
48
|
+
* `detail` below is written for a terminal — it names CLI commands and is English
|
|
49
|
+
* only — so the app can't put it in front of a user. It used to have nothing else
|
|
50
|
+
* to go on, and said "give it a moment" about every one of these, including the
|
|
51
|
+
* three that never clear on their own. This is the machine-readable half.
|
|
52
|
+
*
|
|
53
|
+
* terminated remote access was switched off from the app; needs a restart
|
|
54
|
+
* signed-out no account credentials on this machine
|
|
55
|
+
* refused the server said no (plan's agent cap, rejected ticket) — standing
|
|
56
|
+
* connecting genuinely still trying; this one really is worth a moment
|
|
57
|
+
*/
|
|
58
|
+
export type RelayReason = "terminated" | "signed-out" | "refused" | "connecting";
|
|
59
|
+
|
|
45
60
|
/**
|
|
46
61
|
* The harbor's view of its own relay connection, reported by `status`.
|
|
47
62
|
*
|
|
@@ -62,6 +77,8 @@ export interface RelayStatus {
|
|
|
62
77
|
quietSec?: number;
|
|
63
78
|
/** Why it isn't connected, when we know: signed out, turned off from the app, … */
|
|
64
79
|
detail?: string;
|
|
80
|
+
/** The same fact as `detail`, for a caller that has to localize it. */
|
|
81
|
+
reason?: RelayReason;
|
|
65
82
|
}
|
|
66
83
|
|
|
67
84
|
export interface IpcResponse {
|
|
@@ -383,6 +383,11 @@ export class RelayClient {
|
|
|
383
383
|
private reconnectDelay = RECONNECT_MS;
|
|
384
384
|
// Last refusal reason reported, so a 4xx is logged once instead of on every retry.
|
|
385
385
|
private refusal: string | null = null;
|
|
386
|
+
// Why the last connection attempt failed, kept for connectionStatus(). A refusal
|
|
387
|
+
// (4xx) outranks it — that one is a decision, not a hiccup — but without either,
|
|
388
|
+
// every un-connected relay reports as a bare "connecting…", which is the same
|
|
389
|
+
// sentence whether the socket opens in two seconds or never opens again.
|
|
390
|
+
private lastFailure: string | null = null;
|
|
386
391
|
// Ordered delta buffer (text/reasoning) coalesced into one frame per flush.
|
|
387
392
|
private bufKind: "text" | "reasoning" | null = null;
|
|
388
393
|
private buf = "";
|
|
@@ -518,6 +523,7 @@ export class RelayClient {
|
|
|
518
523
|
ws.on("open", () => {
|
|
519
524
|
opened = true;
|
|
520
525
|
this.refusal = null; // a later refusal is news again
|
|
526
|
+
this.lastFailure = null; // whatever kept us out is history
|
|
521
527
|
this.reconnectDelay = RECONNECT_MS; // reachable again — next blip retries fast
|
|
522
528
|
this.connectedAt = Date.now();
|
|
523
529
|
this.startHeartbeat(ws);
|
|
@@ -537,6 +543,9 @@ export class RelayClient {
|
|
|
537
543
|
// attach/detach frame said. Re-learned on the next attach or inbound frame.
|
|
538
544
|
this.controllerHere = false;
|
|
539
545
|
this.cb.onDisconnected?.();
|
|
546
|
+
// A socket that never opened failed for a reason worth reporting; one that
|
|
547
|
+
// opened and dropped is an ordinary blip the reconnect handles.
|
|
548
|
+
this.lastFailure = opened ? null : lastErr || "the relay connection closed before it opened";
|
|
540
549
|
if (!this.closed) {
|
|
541
550
|
this.cb.onStatus?.(
|
|
542
551
|
opened
|
|
@@ -560,6 +569,7 @@ export class RelayClient {
|
|
|
560
569
|
const status = (err as { status?: number })?.status;
|
|
561
570
|
const refused = typeof status === "number" && status >= 400 && status < 500;
|
|
562
571
|
if (refused) {
|
|
572
|
+
this.lastFailure = msg;
|
|
563
573
|
this.settleFirstConnect(err instanceof Error ? err : new Error(msg));
|
|
564
574
|
// A refusal is a decision, not a hiccup: hammering the same request every few
|
|
565
575
|
// seconds can't change it, and for a harbor — whose onStatus goes to a log file,
|
|
@@ -578,6 +588,7 @@ export class RelayClient {
|
|
|
578
588
|
}
|
|
579
589
|
// Transient (network/route/5xx): stay on the fast retry.
|
|
580
590
|
this.refusal = null;
|
|
591
|
+
this.lastFailure = msg;
|
|
581
592
|
this.cb.onStatus?.(`Remote access couldn't reach the relay (${msg}) — retrying…`);
|
|
582
593
|
this.scheduleReconnect();
|
|
583
594
|
} finally {
|
|
@@ -970,8 +981,19 @@ export class RelayClient {
|
|
|
970
981
|
// quiet for longer than the server's 25s ping cadence is the shape of the half-open
|
|
971
982
|
// failure the watchdog exists to catch, so it is worth showing rather than a bare
|
|
972
983
|
// "connected".
|
|
973
|
-
connectionStatus(): { connected: boolean; upSec?: number; quietSec?: number } {
|
|
974
|
-
|
|
984
|
+
connectionStatus(): { connected: boolean; upSec?: number; quietSec?: number; detail?: string; refused?: boolean } {
|
|
985
|
+
// Not connected: say WHY when we know. A standing refusal (the plan's agent cap,
|
|
986
|
+
// a rejected ticket) is the answer that matters most — it will not clear on its
|
|
987
|
+
// own, and it's the one case a caller should present as a decision rather than a
|
|
988
|
+
// wait — so it wins over the last transient error and is flagged as itself.
|
|
989
|
+
if (!this.isConnected()) {
|
|
990
|
+
const detail = this.refusal ?? this.lastFailure;
|
|
991
|
+
return {
|
|
992
|
+
connected: false,
|
|
993
|
+
...(detail ? { detail } : {}),
|
|
994
|
+
...(this.refusal ? { refused: true } : {}),
|
|
995
|
+
};
|
|
996
|
+
}
|
|
975
997
|
const now = Date.now();
|
|
976
998
|
return {
|
|
977
999
|
connected: true,
|