privateer-agent 0.15.4 → 0.15.6
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 +11 -0
- package/bin/boot-bundle.d.mts +2 -0
- package/bin/boot-bundle.mjs +44 -0
- package/bin/fresh-supervisor.d.mts +5 -1
- package/bin/fresh-supervisor.mjs +47 -10
- package/bin/privateer-launch.mjs +27 -3
- package/bin/privateer-subagent.mjs +5 -2
- package/bin/privateer-verify.mjs +12 -0
- package/bin/session-recovery.d.mts +53 -0
- package/bin/session-recovery.mjs +240 -0
- package/bin/session-seal-preload.mjs +17 -0
- package/bin/session-seal.d.mts +25 -0
- package/bin/session-seal.mjs +242 -0
- package/dist-boot/boot.mjs +3 -0
- package/dist-boot/c-27JKSMWL.mjs +1460 -0
- package/dist-boot/c-2BMRFV24.mjs +269 -0
- package/dist-boot/c-2CTF6JA3.mjs +166 -0
- package/dist-boot/c-2G3BBHRJ.mjs +84 -0
- package/dist-boot/c-2IDLKXX6.mjs +250 -0
- package/dist-boot/c-2NJD5VYM.mjs +8 -0
- package/dist-boot/c-2S5EEQPW.mjs +1602 -0
- package/dist-boot/c-2WRDCUVI.mjs +25 -0
- package/dist-boot/c-2YN3M256.mjs +14 -0
- package/dist-boot/c-37GM3GMU.mjs +187 -0
- package/dist-boot/c-3STKRSVO.mjs +2480 -0
- package/dist-boot/c-3TPV6TQQ.mjs +2320 -0
- package/dist-boot/c-3UVHLQDJ.mjs +225 -0
- package/dist-boot/c-42PUUZYC.mjs +697 -0
- package/dist-boot/c-4D5UMWVW.mjs +683 -0
- package/dist-boot/c-4ETZRMTA.mjs +46 -0
- package/dist-boot/c-4QT25BD5.mjs +1482 -0
- package/dist-boot/c-4WVR32AP.mjs +2579 -0
- package/dist-boot/c-5EMM567G.mjs +152 -0
- package/dist-boot/c-5FA4CV63.mjs +145 -0
- package/dist-boot/c-5GV3X3RB.mjs +14 -0
- package/dist-boot/c-5JCEPRIY.mjs +52 -0
- package/dist-boot/c-5NDKIIWT.mjs +16 -0
- package/dist-boot/c-5QL3AR26.mjs +4665 -0
- package/dist-boot/c-5XY6MFKN.mjs +5142 -0
- package/dist-boot/c-65UZIZC7.mjs +2897 -0
- package/dist-boot/c-6FUXKHOK.mjs +1969 -0
- package/dist-boot/c-6JHWQE4V.mjs +18242 -0
- package/dist-boot/c-6LOBR4I6.mjs +5280 -0
- package/dist-boot/c-6QHOZXKQ.mjs +14394 -0
- package/dist-boot/c-6ZO2SADQ.mjs +178 -0
- package/dist-boot/c-6ZORVVZP.mjs +1449 -0
- package/dist-boot/c-76QH4UCP.mjs +2441 -0
- package/dist-boot/c-77GO5L5Y.mjs +20982 -0
- package/dist-boot/c-7M23XFTN.mjs +5052 -0
- package/dist-boot/c-7TC367E2.mjs +32 -0
- package/dist-boot/c-7UOG7POX.mjs +1524 -0
- package/dist-boot/c-7VHP3FLN.mjs +376 -0
- package/dist-boot/c-A73JHZP2.mjs +13207 -0
- package/dist-boot/c-ABXULAFL.mjs +71 -0
- package/dist-boot/c-ANDD2AQO.mjs +74 -0
- package/dist-boot/c-AOY77AZL.mjs +1430 -0
- package/dist-boot/c-AXEPZ22I.mjs +108 -0
- package/dist-boot/c-B2Y7VNHU.mjs +427 -0
- package/dist-boot/c-B7VJUH73.mjs +77 -0
- package/dist-boot/c-BFW6CDVL.mjs +24 -0
- package/dist-boot/c-BHNIF2AR.mjs +188 -0
- package/dist-boot/c-BKF2O6BL.mjs +23 -0
- package/dist-boot/c-BOZXFTZN.mjs +8 -0
- package/dist-boot/c-BRNYJWZL.mjs +28 -0
- package/dist-boot/c-C3N4Z2HO.mjs +93 -0
- package/dist-boot/c-C4IV7ZFG.mjs +717 -0
- package/dist-boot/c-C5NK6UYV.mjs +793 -0
- package/dist-boot/c-C67644MB.mjs +1659 -0
- package/dist-boot/c-C6S2YB53.mjs +620 -0
- package/dist-boot/c-CDBEJSBF.mjs +37 -0
- package/dist-boot/c-CG47HBKU.mjs +435 -0
- package/dist-boot/c-CKCTWCT5.mjs +51 -0
- package/dist-boot/c-CN77DCGF.mjs +60 -0
- package/dist-boot/c-CRBXNKFO.mjs +1795 -0
- package/dist-boot/c-CRIHASQ6.mjs +973 -0
- package/dist-boot/c-CU4NGAUO.mjs +3786 -0
- package/dist-boot/c-CWRFLGQU.mjs +1883 -0
- package/dist-boot/c-CYQB2USO.mjs +32 -0
- package/dist-boot/c-DEE67DJO.mjs +379 -0
- package/dist-boot/c-DIFDBYOP.mjs +1258 -0
- package/dist-boot/c-DOSGVXDZ.mjs +34879 -0
- package/dist-boot/c-DR7XQI32.mjs +381 -0
- package/dist-boot/c-DTGPSB46.mjs +14666 -0
- package/dist-boot/c-DTHDNA4P.mjs +320 -0
- package/dist-boot/c-E4Z3Y5B5.mjs +3864 -0
- package/dist-boot/c-E66Z2TI7.mjs +203 -0
- package/dist-boot/c-EBU6RLRV.mjs +1527 -0
- package/dist-boot/c-EBZZNGSF.mjs +432 -0
- package/dist-boot/c-EC3LJ2KF.mjs +1032 -0
- package/dist-boot/c-EQBKXWNX.mjs +15829 -0
- package/dist-boot/c-EUEPIW4S.mjs +574 -0
- package/dist-boot/c-EX4YVWAT.mjs +378 -0
- package/dist-boot/c-EYZGSAJU.mjs +98 -0
- package/dist-boot/c-F75VRKY4.mjs +172 -0
- package/dist-boot/c-FH4NXKHV.mjs +177 -0
- package/dist-boot/c-FHNUCTEM.mjs +140 -0
- package/dist-boot/c-FJ5T42DS.mjs +5349 -0
- package/dist-boot/c-FPCNRL7G.mjs +1728 -0
- package/dist-boot/c-G4DLN7Q4.mjs +8 -0
- package/dist-boot/c-G56GYQFZ.mjs +2507 -0
- package/dist-boot/c-GLIOERDQ.mjs +1200 -0
- package/dist-boot/c-GMJ5HUBM.mjs +14 -0
- package/dist-boot/c-H3HSGMAQ.mjs +461 -0
- package/dist-boot/c-HIKTO6P7.mjs +275 -0
- package/dist-boot/c-I2MCJ34O.mjs +464 -0
- package/dist-boot/c-I3C6LBW4.mjs +765 -0
- package/dist-boot/c-I54OKQHW.mjs +387 -0
- package/dist-boot/c-IFSP3R4D.mjs +80 -0
- package/dist-boot/c-IZK77VB7.mjs +171 -0
- package/dist-boot/c-J3Y25UGE.mjs +54 -0
- package/dist-boot/c-J5IESS7W.mjs +27750 -0
- package/dist-boot/c-JJWZBY7L.mjs +254 -0
- package/dist-boot/c-JKTFXTGF.mjs +463 -0
- package/dist-boot/c-JN57AXQ4.mjs +77 -0
- package/dist-boot/c-KFIO6LUW.mjs +1096 -0
- package/dist-boot/c-KIEMEFWW.mjs +478 -0
- package/dist-boot/c-KIVXWBYD.mjs +28 -0
- package/dist-boot/c-KP6CRIP4.mjs +580 -0
- package/dist-boot/c-L26VOJX4.mjs +11558 -0
- package/dist-boot/c-L5GHWG5G.mjs +5988 -0
- package/dist-boot/c-M7NSQAKK.mjs +353 -0
- package/dist-boot/c-MCEWOPBW.mjs +56 -0
- package/dist-boot/c-MGPIEG7I.mjs +20 -0
- package/dist-boot/c-NREXJV3M.mjs +393 -0
- package/dist-boot/c-O5PP6CTX.mjs +162 -0
- package/dist-boot/c-OBQDHSXM.mjs +1774 -0
- package/dist-boot/c-OLQHHDD4.mjs +21 -0
- package/dist-boot/c-OXW4JQQH.mjs +319 -0
- package/dist-boot/c-P3D7ZUWN.mjs +1288 -0
- package/dist-boot/c-P4PQZVV5.mjs +948 -0
- package/dist-boot/c-P63T3XL2.mjs +26373 -0
- package/dist-boot/c-P6N5DQGZ.mjs +194 -0
- package/dist-boot/c-P7MLKQFC.mjs +58 -0
- package/dist-boot/c-PB3PZJL5.mjs +1276 -0
- package/dist-boot/c-PTN4S6G2.mjs +1259 -0
- package/dist-boot/c-PYCE5J7U.mjs +1083 -0
- package/dist-boot/c-Q4HYCRLB.mjs +19410 -0
- package/dist-boot/c-QUXMETGG.mjs +68 -0
- package/dist-boot/c-RDMJZCOX.mjs +356 -0
- package/dist-boot/c-ROGJK76M.mjs +17 -0
- package/dist-boot/c-RQJJ26T4.mjs +708 -0
- package/dist-boot/c-RSKCUPJF.mjs +7559 -0
- package/dist-boot/c-RVJU4O6P.mjs +37 -0
- package/dist-boot/c-S2RVHR3M.mjs +165 -0
- package/dist-boot/c-SKMK3PEE.mjs +91120 -0
- package/dist-boot/c-TI7SM4EG.mjs +20 -0
- package/dist-boot/c-TILJ6GXD.mjs +1235 -0
- package/dist-boot/c-TT6BCUJD.mjs +114 -0
- package/dist-boot/c-U4ITOXAN.mjs +2107 -0
- package/dist-boot/c-UGERXVO2.mjs +7120 -0
- package/dist-boot/c-UPBH5AXY.mjs +1363 -0
- package/dist-boot/c-US2PUNZL.mjs +597 -0
- package/dist-boot/c-UU4WZPPE.mjs +20 -0
- package/dist-boot/c-UWVIYPQS.mjs +39 -0
- package/dist-boot/c-V4RHQBPQ.mjs +17683 -0
- package/dist-boot/c-V5MRVK47.mjs +351 -0
- package/dist-boot/c-V6CL266R.mjs +1755 -0
- package/dist-boot/c-VJTMFKYE.mjs +14975 -0
- package/dist-boot/c-VJTRUKIB.mjs +773 -0
- package/dist-boot/c-W4IFSHPW.mjs +43 -0
- package/dist-boot/c-WETGB2CF.mjs +113 -0
- package/dist-boot/c-WO3QSZUU.mjs +1337 -0
- package/dist-boot/c-WQLH73TY.mjs +242 -0
- package/dist-boot/c-XL735JW5.mjs +500 -0
- package/dist-boot/c-Y7HADL5B.mjs +1107 -0
- package/dist-boot/c-YGQHSVSK.mjs +41 -0
- package/dist-boot/c-YMP2AMEB.mjs +10350 -0
- package/dist-boot/c-YRDO2WEF.mjs +12012 -0
- package/dist-boot/c-YUSTRATJ.mjs +8 -0
- package/dist-boot/c-Z2262R22.mjs +2697 -0
- package/dist-boot/c-Z4ALQMJC.mjs +709 -0
- package/dist-boot/c-Z5ZUQVUY.mjs +62 -0
- package/dist-boot/c-ZDZUKXF7.mjs +874 -0
- package/dist-boot/c-ZJYLGIC3.mjs +32 -0
- package/dist-boot/c-ZNA5CDMI.mjs +77 -0
- package/dist-boot/c-ZSO6DI3M.mjs +204 -0
- package/dist-boot/c-ZWUVHKMK.mjs +100 -0
- package/dist-boot/c-ZY4MESEW.mjs +832 -0
- package/dist-boot/c-ZZ5REI4L.mjs +329 -0
- package/dist-boot/cli.mjs +51 -0
- package/dist-boot/ext-anthropic-attribution.mjs +207 -0
- package/dist-boot/ext-pi-background-tasks.mjs +7234 -0
- package/dist-boot/ext-pi-subagents.mjs +37 -0
- package/dist-boot/ext-privateer-account.mjs +4426 -0
- package/dist-boot/ext-privateer-brand.mjs +5274 -0
- package/dist-boot/ext-privateer-codemode.mjs +37 -0
- package/dist-boot/ext-privateer-computer.mjs +626 -0
- package/dist-boot/ext-privateer-connect.mjs +1862 -0
- package/dist-boot/ext-privateer-context.mjs +323 -0
- package/dist-boot/ext-privateer-desktop.mjs +92 -0
- package/dist-boot/ext-privateer-final-audit.mjs +48 -0
- package/dist-boot/ext-privateer-fresh.mjs +81 -0
- package/dist-boot/ext-privateer-gate.mjs +5959 -0
- package/dist-boot/ext-privateer-hints.mjs +236 -0
- package/dist-boot/ext-privateer-media.mjs +2626 -0
- package/dist-boot/ext-privateer-models.mjs +3397 -0
- package/dist-boot/ext-privateer-posture.mjs +3006 -0
- package/dist-boot/ext-privateer-privacy.mjs +4578 -0
- package/dist-boot/ext-privateer-recovery.mjs +287 -0
- package/dist-boot/ext-privateer-speak.mjs +4754 -0
- package/dist-boot/ext-privateer-terminals.mjs +725 -0
- package/dist-boot/ext-privateer-tools.mjs +528 -0
- package/dist-boot/ext-privateer-truncation.mjs +74 -0
- package/dist-boot/ext-privateer-update.mjs +210 -0
- package/dist-boot/ext-privateer-vision.mjs +148 -0
- package/dist-boot/ext-privateer-web.mjs +543 -0
- package/dist-boot/ext-rpiv-ask-user-question.mjs +710 -0
- package/dist-boot/manifest.json +9 -0
- package/extensions/privateer-brand.ts +3 -1
- package/extensions/privateer-context.ts +4 -2
- package/extensions/privateer-final-audit.ts +17 -0
- package/extensions/privateer-gate.ts +55 -9
- package/extensions/privateer-recovery.ts +175 -0
- package/extensions/privateer-web.ts +4 -4
- package/package.json +3 -1
- package/patches/@earendil-works+pi-coding-agent+1.3.0.patch +199 -2
- package/patches/pi-background-tasks+2.6.10.patch +91 -7
- package/src/cli/chat.ts +7 -1
- package/src/config/moatManifest.json +2 -0
- package/src/config/whatsNew.json +4 -4
- package/src/context.ts +33 -5
- package/src/engine/finalAudit.ts +38 -0
- package/src/engine/stallWatchdog.ts +8 -1
- package/src/providers/account.ts +34 -3
- package/src/remote/attentionPush.ts +40 -0
- package/src/remote/liveTaskSession.ts +7 -1
- package/src/remote/relayClient.ts +38 -4
- package/src/remote/remoteBridge.ts +17 -0
- package/src/remote/transcriptSnapshot.ts +104 -0
|
@@ -22,7 +22,7 @@ index 2a4d905..3a689f5 100644
|
|
|
22
22
|
type HostForwardingStream = AssistantMessageEventStreamLike & HostAssistantMessageEventStream;
|
|
23
23
|
|
|
24
24
|
diff --git a/node_modules/pi-background-tasks/src/core/registry.ts b/node_modules/pi-background-tasks/src/core/registry.ts
|
|
25
|
-
index df6e6f4..
|
|
25
|
+
index df6e6f4..8cbbec2 100644
|
|
26
26
|
--- a/node_modules/pi-background-tasks/src/core/registry.ts
|
|
27
27
|
+++ b/node_modules/pi-background-tasks/src/core/registry.ts
|
|
28
28
|
@@ -1,11 +1,11 @@
|
|
@@ -32,7 +32,7 @@ index df6e6f4..cbab9f2 100644
|
|
|
32
32
|
import { createWriteStream, existsSync } from 'node:fs';
|
|
33
33
|
import { mkdir, realpath, rm, writeFile } from 'node:fs/promises';
|
|
34
34
|
-import { join } from 'node:path';
|
|
35
|
-
+import { join, resolve } from 'node:path';
|
|
35
|
+
+import { delimiter, join, resolve } from 'node:path';
|
|
36
36
|
import type { Api, Model } from '@earendil-works/pi-ai';
|
|
37
37
|
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
38
38
|
-import { formatSize } from '@earendil-works/pi-coding-agent';
|
|
@@ -40,7 +40,23 @@ index df6e6f4..cbab9f2 100644
|
|
|
40
40
|
import {
|
|
41
41
|
boundedRead,
|
|
42
42
|
deriveTaskNameFromCommand,
|
|
43
|
-
@@ -
|
|
43
|
+
@@ -791,6 +791,15 @@ function writeDelegateStdin(
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
+// Privateer: a background shell must see the machine's PATH, not Privateer's private Node.
|
|
48
|
+
+function withoutPrivateRuntime(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
49
|
+
+ const hidden = env['PRIVATEER_PRIVATE_RUNTIME_DIR'];
|
|
50
|
+
+ if (!hidden) return env;
|
|
51
|
+
+ const key = Object.keys(env).find((name) => name.toLowerCase() === 'path') ?? 'PATH';
|
|
52
|
+
+ const visible = (env[key] ?? '').split(delimiter).filter((entry) => entry !== hidden);
|
|
53
|
+
+ return { ...env, [key]: visible.join(delimiter) };
|
|
54
|
+
+}
|
|
55
|
+
+
|
|
56
|
+
export class BackgroundTaskRegistry {
|
|
57
|
+
private readonly tasks = new Map<string, BgTask>();
|
|
58
|
+
private runtimeDir: RuntimeDir | undefined;
|
|
59
|
+
@@ -1263,9 +1272,29 @@ export class BackgroundTaskRegistry {
|
|
44
60
|
if (this.runtimeDir) return this.runtimeDir;
|
|
45
61
|
const sessionId = sanitizePathSegment(ctx.sessionId ?? `session-${String(process.pid)}`);
|
|
46
62
|
const runId = `${sessionId}-${String(process.pid)}`;
|
|
@@ -73,11 +89,50 @@ index df6e6f4..cbab9f2 100644
|
|
|
73
89
|
this.runtimeDir = { abs: runtimeDirAbs, display: runtimeDirDisplay };
|
|
74
90
|
return this.runtimeDir;
|
|
75
91
|
}
|
|
92
|
+
@@ -1514,7 +1543,7 @@ export class BackgroundTaskRegistry {
|
|
93
|
+
killProcess: this.killProcess,
|
|
94
|
+
killTree: this.killTree,
|
|
95
|
+
platform: this.platform,
|
|
96
|
+
- env: this.env,
|
|
97
|
+
+ env: withoutPrivateRuntime(this.env),
|
|
98
|
+
maxOutputBytes: this.maxOutputBytes,
|
|
99
|
+
killGraceMs: this.killGraceMs,
|
|
100
|
+
stopWaitMs: this.stopWaitMs,
|
|
101
|
+
@@ -1739,7 +1768,7 @@ export class BackgroundTaskRegistry {
|
|
102
|
+
cwd: ctx.cwd,
|
|
103
|
+
detached: this.platform !== 'win32',
|
|
104
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
105
|
+
- env: this.env,
|
|
106
|
+
+ env: withoutPrivateRuntime(this.env),
|
|
107
|
+
windowsHide: true,
|
|
108
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
109
|
+
});
|
|
76
110
|
diff --git a/node_modules/pi-background-tasks/src/extension.ts b/node_modules/pi-background-tasks/src/extension.ts
|
|
77
|
-
index c966df5..
|
|
111
|
+
index c966df5..10b38c4 100644
|
|
78
112
|
--- a/node_modules/pi-background-tasks/src/extension.ts
|
|
79
113
|
+++ b/node_modules/pi-background-tasks/src/extension.ts
|
|
80
|
-
@@ -
|
|
114
|
+
@@ -223,6 +223,20 @@ function renderPlainResult(result: TextToolResult, options: ToolRenderResultOpti
|
|
115
|
+
return new Text(text, 0, 0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
+const MAX_ONE_SHOT_EXIT_NOTICES = 3;
|
|
119
|
+
+
|
|
120
|
+
+function oneShotExitNotice(tasks: string[]): string {
|
|
121
|
+
+ return [
|
|
122
|
+
+ 'You ended your turn, but this is a one-shot run. The process exits when you stop, and these background tasks will be killed with it:',
|
|
123
|
+
+ ...tasks.map((task) => `- ${task}`),
|
|
124
|
+
+ '',
|
|
125
|
+
+ 'Handle each one before you finish:',
|
|
126
|
+
+ '- If the work depends on its result, wait for it. Check with bg_status and bg_logs, using `sleep` in bash between checks, then finish and verify the work.',
|
|
127
|
+
+ '- If it has to keep running after this run, such as a server or daemon, bg_kill it, start it detached with `setsid nohup <command> > <logfile> 2>&1 < /dev/null &`, and confirm it is still up.',
|
|
128
|
+
+ '- If it is no longer needed, bg_kill it.',
|
|
129
|
+
+ ].join('\n');
|
|
130
|
+
+}
|
|
131
|
+
+
|
|
132
|
+
export default async function backgroundTasksExtension(pi: ExtensionAPI): Promise<void> {
|
|
133
|
+
const config = parseBackgroundTasksConfig();
|
|
134
|
+
const dockEntryHint = dockShortcutFooterHint(config.dockShortcut);
|
|
135
|
+
@@ -710,8 +724,8 @@ export default async function backgroundTasksExtension(pi: ExtensionAPI): Promis
|
|
81
136
|
currentCtx = ctx;
|
|
82
137
|
const nextRegistryCtx = currentRegistryCtx ?? registryContext(ctx);
|
|
83
138
|
currentRegistryCtx = nextRegistryCtx;
|
|
@@ -88,7 +143,36 @@ index c966df5..b073a15 100644
|
|
|
88
143
|
updateUi(ctx);
|
|
89
144
|
hostService.markReady();
|
|
90
145
|
if (disposed) return;
|
|
91
|
-
@@ -
|
|
146
|
+
@@ -729,6 +743,28 @@ export default async function backgroundTasksExtension(pi: ExtensionAPI): Promis
|
|
147
|
+
if (!disposed) void scheduleUpdateCheck(ctx);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
+ // Privateer patch: a one-shot run must not quietly kill what it started.
|
|
151
|
+
+ //
|
|
152
|
+
+ // In the TUI a background task outlives the turn and reports back when it finishes. `-p`
|
|
153
|
+
+ // exits when the turn ends and shutdown kills whatever is still running, so a model that
|
|
154
|
+
+ // says "training continues in the background", or leaves a server up, has just lost it.
|
|
155
|
+
+ // Say so while it can still act. Capped so a model that keeps stopping cannot loop.
|
|
156
|
+
+ let oneShotExitNotices = 0;
|
|
157
|
+
+ pi.on('agent_end', (_event, ctx) => {
|
|
158
|
+
+ if (ctx.mode !== 'print' && ctx.mode !== 'json') return;
|
|
159
|
+
+ const running = registry.allTasks().filter((task) => task.status === 'running');
|
|
160
|
+
+ if (running.length === 0 || oneShotExitNotices >= MAX_ONE_SHOT_EXIT_NOTICES) return;
|
|
161
|
+
+ oneShotExitNotices += 1;
|
|
162
|
+
+ pi.sendMessage(
|
|
163
|
+
+ {
|
|
164
|
+
+ customType: 'bg-one-shot-exit',
|
|
165
|
+
+ content: oneShotExitNotice(running.map((task) => `${task.id}: ${taskDisplayName(task)}`)),
|
|
166
|
+
+ display: false,
|
|
167
|
+
+ },
|
|
168
|
+
+ { deliverAs: 'followUp', triggerTurn: true },
|
|
169
|
+
+ );
|
|
170
|
+
+ });
|
|
171
|
+
+
|
|
172
|
+
pi.on('session_shutdown', async (event, ctx) => {
|
|
173
|
+
beginSessionShutdown(event.reason);
|
|
174
|
+
if (shutdownCleanupStarted) return;
|
|
175
|
+
@@ -830,22 +866,11 @@ export default async function backgroundTasksExtension(pi: ExtensionAPI): Promis
|
|
92
176
|
});
|
|
93
177
|
|
|
94
178
|
pi.registerCommand('bg-update', {
|
|
@@ -114,7 +198,7 @@ index c966df5..b073a15 100644
|
|
|
114
198
|
];
|
|
115
199
|
ctx.ui.notify(lines.join('\n'), 'info');
|
|
116
200
|
return Promise.resolve();
|
|
117
|
-
@@ -958,7 +
|
|
201
|
+
@@ -958,7 +983,7 @@ export default async function backgroundTasksExtension(pi: ExtensionAPI): Promis
|
|
118
202
|
pi.registerTool<typeof BgRunParams, BgRunDetails>({
|
|
119
203
|
name: 'bg_run',
|
|
120
204
|
label: 'Background Run',
|
package/src/cli/chat.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { privacyDisabled } from "../config/privacyDisabled.ts"; // no Pi deps
|
|
|
16
16
|
import type { GateController } from "../ext/permissionGate.ts"; // type-only → erased, safe pre-boot
|
|
17
17
|
import { createUIContext } from "../ext/headlessUi.ts"; // no Pi deps → safe pre-boot
|
|
18
18
|
import { canOpenBrowser, openInBrowser } from "../util/openBrowser.ts"; // node:child_process only → safe pre-boot
|
|
19
|
+
import { transcriptSnapshot, type SnapshotEntry } from "../remote/transcriptSnapshot.ts"; // no deps → safe pre-boot
|
|
19
20
|
|
|
20
21
|
// This lean REPL has no Pi TUI (and so no Theme), so it detects the terminal background
|
|
21
22
|
// itself (COLORFGBG) and picks a palette — on a light terminal the standard "\x1b[33m"
|
|
@@ -181,7 +182,12 @@ async function main() {
|
|
|
181
182
|
// the driver learns which folder they're driving without the OS username
|
|
182
183
|
// crossing the relay.
|
|
183
184
|
onControllerAttached: () => {
|
|
184
|
-
|
|
185
|
+
// The session's transcript, so an app back from sleep catches up (transcriptSnapshot.ts).
|
|
186
|
+
{
|
|
187
|
+
let entries: SnapshotEntry[] = [];
|
|
188
|
+
try { entries = transcriptSnapshot(session?.sessionManager?.getBranch?.() ?? []); } catch { /* no session yet */ }
|
|
189
|
+
relay?.sendSnapshot(entries, { full: entries.length > 0, running: turnActive });
|
|
190
|
+
}
|
|
185
191
|
relay?.sendContext({ model: currentSpec, cwd, version: agentVersion() });
|
|
186
192
|
relay?.sendCommands(availableCommands());
|
|
187
193
|
},
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
{ "entry": "extensions/privateer-hints.ts", "name": "privateer-hints", "note": "rotating tips in the working line + /hints" },
|
|
26
26
|
{ "entry": "extensions/privateer-update.ts", "name": "privateer-update", "note": "tool pack updates in place — banner flag + /update" },
|
|
27
27
|
{ "entry": "extensions/privateer-truncation.ts", "name": "privateer-truncation", "note": "picks the task back up when a reply is cut off at the output limit" },
|
|
28
|
+
{ "entry": "extensions/privateer-final-audit.ts", "name": "privateer-final-audit", "note": "makes a -p run check its result, and say what it invented or could not verify, before it ends" },
|
|
28
29
|
{ "entry": "extensions/privateer-fresh.ts", "name": "privateer-fresh", "note": "/fresh — replace the agent with a new one and stop everything it started" },
|
|
30
|
+
{ "entry": "extensions/privateer-recovery.ts", "name": "privateer-recovery", "note": "crash recovery — the live marker the supervisor resumes from, the note that tells the model its last action may have half-run, and session-sealing warnings" },
|
|
29
31
|
{ "entry": "extensions/privateer-speak.ts", "name": "privateer-speak", "note": "spoken responses (/speak) + voice input (/talk) — pi-speak + confidential account TTS/STT" },
|
|
30
32
|
{ "entry": "extensions/privateer-web.ts", "name": "privateer-web", "note": "web_search/web_fetch — account search when signed in, else the user's own provider (rpiv-web-tools); crypto_lookup — account market data, in both cases" },
|
|
31
33
|
{ "dep": ["@juicesharp/rpiv-ask-user-question", "index.ts"], "name": "rpiv-ask-user-question", "note": "ask_user_question" },
|
package/src/config/whatsNew.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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.15.
|
|
3
|
+
"reviewedFor": "0.15.6",
|
|
4
4
|
"items": [
|
|
5
|
+
{ "text": "Privateer now starts about three times faster, and quitting no longer freezes the next launch for up to 30 seconds", "since": "0.15.6" },
|
|
6
|
+
{ "text": "If the agent crashes mid-task it comes back into your session, and saved sessions are now encrypted on disk", "since": "0.15.5" },
|
|
5
7
|
{ "text": "Slow images, speech and music run as background jobs — a dropped connection no longer loses a paid result", "since": "0.15.4" },
|
|
6
|
-
{ "text": "
|
|
7
|
-
{ "text": "MCP on Pi's built-in support — sign OAuth connectors back in with", "cmd": "/mcp", "since": "0.15.0" },
|
|
8
|
-
{ "text": "Sub-agents are back: hand work to bounded parallel agents", "since": "0.15.0" }
|
|
8
|
+
{ "text": "MCP on Pi's built-in support — sign OAuth connectors back in with", "cmd": "/mcp", "since": "0.15.0" }
|
|
9
9
|
]
|
|
10
10
|
}
|
package/src/context.ts
CHANGED
|
@@ -191,14 +191,42 @@ export function discoverContextFiles(cwd: string = process.cwd()): ContextFile[]
|
|
|
191
191
|
export const CONTEXT_BLOCK_MARKER = "<!-- privateer:PRIVATEER.md -->";
|
|
192
192
|
export const RUNTIME_GUIDELINES_MARKER = "<!-- privateer:runtime-guidelines -->";
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
// bin/privateer-launch.mjs sets PRIVATEER_NODE; a host that embeds Privateer without the
|
|
195
|
+
// launcher has no runtime to promise, so the model is told to check instead.
|
|
196
|
+
function nodeGuideline(): string {
|
|
197
|
+
if (!process.env.PRIVATEER_NODE) {
|
|
198
|
+
return "- Node.js: may not be installed here. Check with `command -v node` before relying on it.";
|
|
199
|
+
}
|
|
200
|
+
return `- Node.js: Privateer's own runtime is at \`$PRIVATEER_NODE\` (v22+). Use it for throwaway calculations or JSON processing when nothing else fits: \`"$PRIVATEER_NODE" -e "..."\`. It is not part of this system: plain \`node\` exists only if the system really has it, and nothing you deliver (scripts, shebangs, services, git hooks, build output) may depend on \`$PRIVATEER_NODE\`, because it does not exist outside Privateer.`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Only for a `-p` run with nobody attached. An interactive user reviews each step and pays
|
|
204
|
+
// per call, so they keep the stricter batching rule below; a run nobody is watching has no
|
|
205
|
+
// other chance to catch its own mistakes.
|
|
206
|
+
const UNATTENDED_RUN_GUIDELINES = `\n<unattended_run>
|
|
207
|
+
Nobody is watching this run: no one can answer a question or catch a mistake before your final message. Work through to the end, take the most plausible reading of anything ambiguous, and name that assumption in your final message.
|
|
208
|
+
- Prove the result before you stop. Exercise it the way the user will: from a fresh shell, through the requested entry point, with their exact commands if they gave any. Check what has to still exist after you exit, and build whatever throwaway checks that takes. Cleaning up must not undo the result.
|
|
209
|
+
- Deliver exactly what was asked: the requested paths, formats and text, with nothing added or reworded.
|
|
210
|
+
- Treat data you were given as irreplaceable. Copy it first and experiment on the copy, because merely opening a file with its usual tool can change or delete it.
|
|
211
|
+
- Keep long jobs visible. Anything that may take more than a few minutes runs in the background writing to a log; check its progress, and shrink or drop an approach that will not finish. Never sit blocked on one silent command.
|
|
212
|
+
- Open your final message with anything unfinished, unverified or lost.
|
|
213
|
+
</unattended_run>\n`;
|
|
214
|
+
|
|
215
|
+
export function runtimeGuidelinesBlock({ unattended = false }: { unattended?: boolean } = {}): string {
|
|
216
|
+
const whenToolMissing = unattended
|
|
217
|
+
? "install it with the host package manager (e.g. `apt-get install -y`, `brew install`) and mention that in your final message"
|
|
218
|
+
: "explain clearly what is missing, and offer to install it using the host package manager (e.g. `brew install`, `xcode-select --install`, `winget install`, `apt install`) upon user approval";
|
|
219
|
+
const whatNotToBuild = unattended
|
|
220
|
+
? "."
|
|
221
|
+
: "; do not build test harnesses, scratch scripts or regression runs that the user did not ask for.";
|
|
195
222
|
return `\n\n${RUNTIME_GUIDELINES_MARKER}\n<environment_guidelines>
|
|
196
|
-
|
|
223
|
+
${nodeGuideline()}
|
|
197
224
|
- Search fallback: If \`rg\` (ripgrep) is missing or returns "command not found", fall back to standard POSIX \`grep -rn <pattern> <path>\` or \`find <path>\`.
|
|
198
|
-
- Missing system dependencies: If an essential external tool (e.g. \`git\`, \`python\`, \`rg\`) is missing and needed, check its presence (\`command -v <tool>\`),
|
|
225
|
+
- Missing system dependencies: If an essential external tool (e.g. \`git\`, \`python\`, \`rg\`) is missing and needed, check its presence (\`command -v <tool>\`), ${whenToolMissing}.
|
|
199
226
|
- Shell calls share no state: every shell call runs in a fresh subshell rooted at the session's working directory, so a \`cd\` (or a variable, or an activated venv) in one call is gone by the next. Use absolute paths, or move and work in ONE call (\`cd <dir> && <command>\`). Never spend a call on \`cd\` alone.
|
|
200
|
-
- Batch your steps: every tool call re-sends the whole conversation to the model, so a chain of one-line calls is far slower and more expensive than the same work combined into fewer calls. Group independent reads, searches and checks into one command
|
|
201
|
-
|
|
227
|
+
- Batch your steps: every tool call re-sends the whole conversation to the model, so a chain of one-line calls is far slower and more expensive than the same work combined into fewer calls. Group independent reads, searches and checks into one command${whatNotToBuild}
|
|
228
|
+
- Report what really happened: never present invented, guessed or placeholder data as a real result. If something could not be obtained, or you lost or damaged it, say so plainly.
|
|
229
|
+
</environment_guidelines>\n${unattended ? UNATTENDED_RUN_GUIDELINES : ""}`;
|
|
202
230
|
}
|
|
203
231
|
|
|
204
232
|
// Format the discovered files into a system-prompt fragment using the same framing Pi
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// One forced look back before an unattended run ends.
|
|
2
|
+
//
|
|
3
|
+
// A `-p` run has nobody to notice a wrong result, and telling the model up front to be
|
|
4
|
+
// honest did not hold. Measured on a data-recovery task (2026-11-09): having destroyed
|
|
5
|
+
// its input, the model wrote invented rows into the user's database, cited the row count
|
|
6
|
+
// as verification, and reported success with no caveat, with the honesty rule in its
|
|
7
|
+
// system prompt. The rule is one line among thousands of tokens read before the work
|
|
8
|
+
// starts; this asks the question after the work is done, with the evidence in context.
|
|
9
|
+
//
|
|
10
|
+
// So: when a top-level headless run that used tools ends normally, queue one follow-up
|
|
11
|
+
// asking the model to check its result against what actually happened, fix what it can,
|
|
12
|
+
// and open its final message with anything invented, unverified or lost. Pi continues a
|
|
13
|
+
// message queued from an agent_end handler inside the same run, so `-p` waits for it, and
|
|
14
|
+
// the audited reply is the one a script reads.
|
|
15
|
+
//
|
|
16
|
+
// Once per run. Runs that only answered have nothing to audit, and a turn that ended on
|
|
17
|
+
// an error or an abort is handled elsewhere. A subagent's parent reviews what it returns.
|
|
18
|
+
// PRIVATEER_FINAL_AUDIT=0 turns it off.
|
|
19
|
+
//
|
|
20
|
+
// IMPORT-SAFETY: no Pi imports — the shapes below are the few fields read.
|
|
21
|
+
|
|
22
|
+
import { lastAssistant } from "./truncationRecovery.ts";
|
|
23
|
+
|
|
24
|
+
export const FINAL_AUDIT_ENV = "PRIVATEER_FINAL_AUDIT";
|
|
25
|
+
|
|
26
|
+
export const FINAL_AUDIT_PROMPT = `Before this run ends, check your work against what actually happened in this session.
|
|
27
|
+
|
|
28
|
+
1. Re-read the original request. Is every part of it done, and did you see it work the way the user will use it? If something can still be fixed, fix it now.
|
|
29
|
+
2. Look for anything presented as real that you did not directly observe or derive from the inputs: invented, guessed or reconstructed values, checks you did not run, inputs you changed or lost. If you wrote such values into a deliverable or the user's files, take them out if you can.
|
|
30
|
+
|
|
31
|
+
Then write your final message to the user. Start with those items, stated plainly. If there are none, just give the result.`;
|
|
32
|
+
|
|
33
|
+
export function needsFinalAudit(messages: unknown, mode: unknown, env: NodeJS.ProcessEnv = process.env): boolean {
|
|
34
|
+
if (mode !== "print" && mode !== "json") return false;
|
|
35
|
+
if (env.PI_SUBAGENT_CHILD === "1" || env[FINAL_AUDIT_ENV] === "0") return false;
|
|
36
|
+
if (!Array.isArray(messages) || lastAssistant(messages)?.stopReason !== "stop") return false;
|
|
37
|
+
return messages.some((message) => message?.role === "toolResult");
|
|
38
|
+
}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// So: armed when a request goes out, re-armed by every streamed event (text, thinking,
|
|
10
10
|
// tool-call deltas all count, so a slow but live stream never trips it), disarmed when
|
|
11
11
|
// the message ends. Between requests (tool execution) it is off — a long local command
|
|
12
|
-
// is not a provider stall. If it fires, the caller aborts the turn and
|
|
12
|
+
// is not a provider stall. If it fires, the caller aborts the turn and either resends it
|
|
13
|
+
// (reset() re-enables the watchdog) or gives up and says why.
|
|
13
14
|
//
|
|
14
15
|
// The limit: PRIVATEER_REPLY_TIMEOUT seconds, default 180, 0 turns it off. Silence, not
|
|
15
16
|
// total time, is what is measured, so a long answer is never cut off for being long.
|
|
@@ -55,6 +56,12 @@ export class StallWatchdog {
|
|
|
55
56
|
this.timer = undefined;
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/** The stalled request is being resent: watch the new one. */
|
|
60
|
+
reset(): void {
|
|
61
|
+
this.disarm();
|
|
62
|
+
this.fired = false;
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
get stalled(): boolean {
|
|
59
66
|
return this.fired;
|
|
60
67
|
}
|
package/src/providers/account.ts
CHANGED
|
@@ -923,7 +923,17 @@ export function makeAccountProvider(opts: { armOnStart?: boolean } = {}) {
|
|
|
923
923
|
// all, and the first prompt dead-ends on "No API key found for privateer." — even
|
|
924
924
|
// though the banner says "connected". The REPL (cli/chat.ts) and the harbor
|
|
925
925
|
// already spawn one at startup; this gives the TUI the same seed.
|
|
926
|
-
|
|
926
|
+
//
|
|
927
|
+
// Nobody types faster than this resolves, so the TUI starts without waiting. `-p`
|
|
928
|
+
// prompts as soon as extensions are bound, and Pi's auth precheck runs before
|
|
929
|
+
// before_agent_start can re-arm, so a headless launch has to wait here. Without it,
|
|
930
|
+
// a signed-in machine with no leftover auth.json entry is told it isn't signed in.
|
|
931
|
+
if (opts.armOnStart !== false) {
|
|
932
|
+
pi.on?.("session_start", async (_e, ctx) => {
|
|
933
|
+
const arming = armAccountCredential(ctx);
|
|
934
|
+
if (!(ctx as SeedContext)?.hasUI) await arming;
|
|
935
|
+
});
|
|
936
|
+
}
|
|
927
937
|
|
|
928
938
|
// Two safety nets around the account channel, both OUTSIDE the request path —
|
|
929
939
|
// they read Pi's in-memory auth map and its finished messages, and never touch the
|
|
@@ -1088,8 +1098,10 @@ export async function armAccountCredential(
|
|
|
1088
1098
|
// If the credentials vanished during the call, the spawn hit a 401 and
|
|
1089
1099
|
// onSessionExpired already announced the sign-out — a second line here would
|
|
1090
1100
|
// just repeat it (we returned early above if we STARTED signed out).
|
|
1091
|
-
if (opts.notify !== false &&
|
|
1092
|
-
|
|
1101
|
+
if (opts.notify !== false && hasCredentials()) {
|
|
1102
|
+
const message = `Privateer account channel unavailable — ${(e as Error).message}`;
|
|
1103
|
+
if (c?.hasUI) c.ui?.notify?.(message, "error");
|
|
1104
|
+
else console.error(message); // headless: the real cause, not "isn't signed in"
|
|
1093
1105
|
}
|
|
1094
1106
|
return false;
|
|
1095
1107
|
}
|
|
@@ -1153,6 +1165,25 @@ export async function dropPersistedAccountCredential(
|
|
|
1153
1165
|
}
|
|
1154
1166
|
}
|
|
1155
1167
|
|
|
1168
|
+
// The drop for a process that is about to exit. Pi calls process.exit as soon as the
|
|
1169
|
+
// session_shutdown handlers return, so a drop nobody waits for dies mid-write: the
|
|
1170
|
+
// revoked credential stays on disk, and the lock on auth.json is left behind. Every
|
|
1171
|
+
// launch in the next 30 seconds then waits for that lock to go stale before it can
|
|
1172
|
+
// paint, which is what a quick quit-and-relaunch looked like: a frozen terminal.
|
|
1173
|
+
//
|
|
1174
|
+
// Bounded, because another terminal can legitimately hold that lock and quitting must
|
|
1175
|
+
// never hang. Past the bound the server-side TTL is the fallback, as for any failed drop.
|
|
1176
|
+
export async function dropPersistedAccountCredentialBeforeExit(timeoutMs = 2_000): Promise<void> {
|
|
1177
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1178
|
+
await Promise.race([
|
|
1179
|
+
dropPersistedAccountCredential().catch(() => false),
|
|
1180
|
+
new Promise<void>((resolve) => {
|
|
1181
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
1182
|
+
}),
|
|
1183
|
+
]);
|
|
1184
|
+
clearTimeout(timer);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1156
1187
|
// Make sure Pi still HAS an account credential before a turn goes out. Normally
|
|
1157
1188
|
// session_start's arm is enough and this is a free in-memory lookup. It exists for the
|
|
1158
1189
|
// case session_start can't cover: the persisted entry disappearing mid-session, which
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// "Your agent is asking you something" — a push to the account's phones when a
|
|
2
|
+
// question (ask_user_question, a select, a text prompt) is put to an app that isn't
|
|
3
|
+
// there to see it.
|
|
4
|
+
//
|
|
5
|
+
// A question sent up the relay only reaches a LIVE socket. A phone that backgrounded
|
|
6
|
+
// a minute ago has none, so the question sat held on this machine (remoteBridge's
|
|
7
|
+
// holdOpenQuestions) with nothing telling anyone it was waiting — the user found it
|
|
8
|
+
// by chance, or after the hold ran out and the tool came back as an error.
|
|
9
|
+
//
|
|
10
|
+
// Content-free on purpose, like the Inbox nudge (server services/pushService.js): the
|
|
11
|
+
// question itself never leaves the relay, so the push says only that one is waiting.
|
|
12
|
+
// Fire-and-forget and throttled — a failed or skipped push must never touch the turn.
|
|
13
|
+
|
|
14
|
+
const COOLDOWN_MS = 30_000;
|
|
15
|
+
let lastAt = 0;
|
|
16
|
+
let sink: (() => void) | null = null;
|
|
17
|
+
|
|
18
|
+
export function requestAttentionPush(): void {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
if (now - lastAt < COOLDOWN_MS) return;
|
|
21
|
+
lastAt = now;
|
|
22
|
+
if (sink) return sink();
|
|
23
|
+
void (async () => {
|
|
24
|
+
try {
|
|
25
|
+
const { apiRequest, hasCredentials } = await import("../auth/privateer.ts");
|
|
26
|
+
if (!hasCredentials()) return;
|
|
27
|
+
await apiRequest("/api/push/attention", { method: "POST", signal: AbortSignal.timeout(10_000) });
|
|
28
|
+
} catch {
|
|
29
|
+
/* best effort — the question is still held and re-sent when the app comes back */
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Test seam: count pushes instead of sending them (null restores the real one), and
|
|
35
|
+
* forget the cooldown. Tests run on machines that are signed in — without this a
|
|
36
|
+
* bridge test would buzz the developer's phone. */
|
|
37
|
+
export function setAttentionPushSink(fn: (() => void) | null): void {
|
|
38
|
+
sink = fn;
|
|
39
|
+
lastAt = 0;
|
|
40
|
+
}
|
|
@@ -25,6 +25,7 @@ import { rememberAccountCredential, persistAccountCredential, dropPersistedAccou
|
|
|
25
25
|
import { modelRegistryOf } from "../providers/piAuthStore.ts";
|
|
26
26
|
import { RelayClient, type TaskSpec } from "./relayClient.ts";
|
|
27
27
|
import { RemoteBridge } from "./remoteBridge.ts";
|
|
28
|
+
import { transcriptSnapshot, type SnapshotEntry } from "./transcriptSnapshot.ts";
|
|
28
29
|
import { AttachmentStore, type StoredAttachment } from "../util/attachmentStore.ts";
|
|
29
30
|
import { spawnAccountCredentials, revokeAccountSession } from "../auth/privateer.ts";
|
|
30
31
|
import { createUIContext } from "../ext/headlessUi.ts";
|
|
@@ -190,7 +191,12 @@ export async function createLiveTaskSession(spec: TaskSpec, deps: LiveTaskDeps):
|
|
|
190
191
|
onControllerAttached: () => {
|
|
191
192
|
attached = true;
|
|
192
193
|
if (attachTimer) { clearTimeout(attachTimer); attachTimer = undefined; }
|
|
193
|
-
|
|
194
|
+
// The session's transcript, so an app back from sleep catches up (transcriptSnapshot.ts).
|
|
195
|
+
{
|
|
196
|
+
let entries: SnapshotEntry[] = [];
|
|
197
|
+
try { entries = transcriptSnapshot(session?.sessionManager?.getBranch?.() ?? []); } catch { /* no session yet */ }
|
|
198
|
+
relay?.sendSnapshot(entries, { full: entries.length > 0, running: turnActive });
|
|
199
|
+
}
|
|
194
200
|
// cwd rides along here (and nowhere else — see RelayClient.sendContext): this
|
|
195
201
|
// session's working directory was either named by the driver in the spawn form
|
|
196
202
|
// or defaulted to the harbor's own, and they have no other way to see which.
|
|
@@ -24,6 +24,7 @@ import type { EngineEvent } from "../engine/events.ts";
|
|
|
24
24
|
import type { PermissionRequest } from "../permissions/gate.ts";
|
|
25
25
|
import { noQuarterActive } from "../permissions/noQuarter.ts";
|
|
26
26
|
import { authorizeControl } from "./controlAuth.ts";
|
|
27
|
+
import type { SnapshotTool } from "./transcriptSnapshot.ts";
|
|
27
28
|
import { CARGO_CHUNK_CHARS, type CargoSaveRequest, type CargoSaveResult } from "./cargoSave.ts";
|
|
28
29
|
import {
|
|
29
30
|
LIBRARY_CHUNK_CHARS,
|
|
@@ -1175,10 +1176,43 @@ export class RelayClient {
|
|
|
1175
1176
|
|
|
1176
1177
|
// Catch-up history sent to a controller on attach. Structured (not markdown) so
|
|
1177
1178
|
// the app renders it with the same styling as the live feed. Bounded: last 80
|
|
1178
|
-
// entries, each clipped
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1179
|
+
// entries, each clipped, and the whole frame kept well under the relay's 256 KB
|
|
1180
|
+
// frame cap by dropping the OLDEST entries first.
|
|
1181
|
+
//
|
|
1182
|
+
// `full` marks the entries as the terminal's whole current transcript (see
|
|
1183
|
+
// transcriptSnapshot.ts), so the app may REPLACE what it holds rather than only
|
|
1184
|
+
// adopting into an empty feed; `running` says whether a turn is still going, which
|
|
1185
|
+
// is how a reattaching app learns that the turn it last saw "Link dropped" on is
|
|
1186
|
+
// over. Both absent = the old notice-only snapshot, which the app keeps treating as
|
|
1187
|
+
// such.
|
|
1188
|
+
sendSnapshot(
|
|
1189
|
+
entries: { kind: string; text: string; tool?: SnapshotTool }[],
|
|
1190
|
+
meta?: { full?: boolean; running?: boolean },
|
|
1191
|
+
): void {
|
|
1192
|
+
const out: Record<string, unknown>[] = [];
|
|
1193
|
+
let budget = 160 * 1024;
|
|
1194
|
+
for (const e of entries.slice(-80).reverse()) {
|
|
1195
|
+
const t = e.tool;
|
|
1196
|
+
const entry: Record<string, unknown> = t
|
|
1197
|
+
? {
|
|
1198
|
+
kind: e.kind,
|
|
1199
|
+
text: "",
|
|
1200
|
+
tool: {
|
|
1201
|
+
id: t.id,
|
|
1202
|
+
name: t.name,
|
|
1203
|
+
input: safe(t.input, 500),
|
|
1204
|
+
...(t.output !== undefined ? { output: safe(t.output, 1500) } : {}),
|
|
1205
|
+
...(t.error !== undefined ? { error: safe(t.error, 1000) } : {}),
|
|
1206
|
+
...(t.running ? { running: true } : {}),
|
|
1207
|
+
},
|
|
1208
|
+
}
|
|
1209
|
+
: { kind: e.kind, text: safe(String(e.text ?? ""), e.kind === "reasoning" ? 1500 : 4000) };
|
|
1210
|
+
budget -= JSON.stringify(entry).length;
|
|
1211
|
+
if (budget < 0) break;
|
|
1212
|
+
out.push(entry);
|
|
1213
|
+
}
|
|
1214
|
+
out.reverse();
|
|
1215
|
+
this.rawSend({ type: "snapshot", entries: out, ...(meta?.full ? { full: true, running: meta.running === true } : {}) });
|
|
1182
1216
|
}
|
|
1183
1217
|
|
|
1184
1218
|
// ── agent → app file transfer (chunked) ─────────────────────────────────────
|
|
@@ -20,6 +20,7 @@ import { redactText } from "../util/redact.ts";
|
|
|
20
20
|
import type { PermissionRequest } from "../permissions/gate.ts";
|
|
21
21
|
import type { AskOutcome } from "../permissions/modeGate.ts";
|
|
22
22
|
import type { RelayCallbacks } from "./relayClient.ts";
|
|
23
|
+
import { requestAttentionPush } from "./attentionPush.ts";
|
|
23
24
|
import type { CargoSaveRequest, CargoSaveResult } from "./cargoSave.ts";
|
|
24
25
|
import type { LibrarySaveRequest, LibrarySaveResult } from "./librarySave.ts";
|
|
25
26
|
import type { ChartOpRequest, ChartOpResult } from "./chartOps.ts";
|
|
@@ -168,6 +169,10 @@ export interface RemoteBridgeConfig {
|
|
|
168
169
|
onFilesSearch?: (id: string, query: string) => void;
|
|
169
170
|
// A controller (re)attached — the owner should push a transcript snapshot.
|
|
170
171
|
onControllerAttached?: () => void;
|
|
172
|
+
// A question is open and no app is attached to see it. Defaults to a content-free
|
|
173
|
+
// push to the account's phones (attentionPush.ts); a test or an owner with its own
|
|
174
|
+
// channel may replace it.
|
|
175
|
+
onQuestionUnwatched?: () => void;
|
|
171
176
|
// The last controller went away. Informational for the owner (the bridge already
|
|
172
177
|
// fails pending approvals closed); a driven turn in flight keeps running.
|
|
173
178
|
onControllerDetached?: () => void;
|
|
@@ -380,6 +385,7 @@ export class RemoteBridge {
|
|
|
380
385
|
this.remote = false;
|
|
381
386
|
this.rejectAllPending();
|
|
382
387
|
this.holdOpenQuestions();
|
|
388
|
+
if (this.hasOpenQuestion()) this.questionUnwatched();
|
|
383
389
|
this.cfg.onControllerDetached?.();
|
|
384
390
|
},
|
|
385
391
|
onAttachment: (file) => {
|
|
@@ -472,6 +478,9 @@ export class RemoteBridge {
|
|
|
472
478
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
473
479
|
}
|
|
474
480
|
this.relay!.requestSelect(id, req);
|
|
481
|
+
// Sent into a socket with no app on the other end: it is held and re-sent on
|
|
482
|
+
// reattach, but nobody knows to come back unless they are told.
|
|
483
|
+
if (this.relay!.hasController?.() === false) this.questionUnwatched();
|
|
475
484
|
});
|
|
476
485
|
};
|
|
477
486
|
|
|
@@ -495,6 +504,9 @@ export class RemoteBridge {
|
|
|
495
504
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
496
505
|
}
|
|
497
506
|
this.relay!.requestInput(id, req);
|
|
507
|
+
// Sent into a socket with no app on the other end: it is held and re-sent on
|
|
508
|
+
// reattach, but nobody knows to come back unless they are told.
|
|
509
|
+
if (this.relay!.hasController?.() === false) this.questionUnwatched();
|
|
498
510
|
});
|
|
499
511
|
};
|
|
500
512
|
|
|
@@ -711,6 +723,11 @@ export class RemoteBridge {
|
|
|
711
723
|
return true;
|
|
712
724
|
}
|
|
713
725
|
|
|
726
|
+
private questionUnwatched(): void {
|
|
727
|
+
if (this.cfg.onQuestionUnwatched) return this.cfg.onQuestionUnwatched();
|
|
728
|
+
requestAttentionPush();
|
|
729
|
+
}
|
|
730
|
+
|
|
714
731
|
private resendOpenQuestions(): void {
|
|
715
732
|
this.releaseQuestionGrace();
|
|
716
733
|
for (const [id, q] of this.pendingSelects) this.relay?.requestSelect(id, q.req);
|