@pi-unipi/unipi 3.0.0-alpha.43 → 3.0.0-alpha.45
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/CHANGELOG.md +16 -0
- package/package.json +23 -23
- package/packages/app-bridge/package.json +2 -2
- package/packages/app-bridge/src/bridge.ts +244 -47
- package/packages/ask-user/package.json +2 -2
- package/packages/autocomplete/package.json +1 -1
- package/packages/background-tasks/package.json +2 -2
- package/packages/background-tasks/src/index.ts +12 -0
- package/packages/btw/package.json +2 -2
- package/packages/compactor/package.json +3 -3
- package/packages/core/index.ts +1 -0
- package/packages/core/package.json +1 -1
- package/packages/core/src/child-agent/runtime.ts +21 -4
- package/packages/core/src/child-usage.ts +131 -0
- package/packages/core/src/tui/kit.ts +62 -0
- package/packages/dream/package.json +2 -2
- package/packages/footer/package.json +3 -3
- package/packages/footer/src/session-scan.ts +13 -0
- package/packages/fusion/package.json +4 -4
- package/packages/fusion/src/index.ts +3 -1
- package/packages/info-screen/package.json +2 -2
- package/packages/info-screen/usage-parser.ts +30 -2
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/package.json +7 -7
- package/packages/long-horizon/package.json +2 -2
- package/packages/long-horizon/src/progress-mermaid.ts +1 -1
- package/packages/long-horizon/src/progress-snapshot.ts +1 -1
- package/packages/long-horizon/src/progress-view.ts +6 -4
- package/packages/long-horizon/src/visualize.ts +1 -1
- package/packages/mcp/package.json +2 -2
- package/packages/memory/package.json +3 -3
- package/packages/notify/package.json +2 -2
- package/packages/skill-registry/package.json +2 -2
- package/packages/subagents/package.json +3 -3
- package/packages/subagents/src/index.ts +3 -1
- package/packages/subagents/src/manager.ts +13 -0
- package/packages/unipi/bundled.js +378 -87
- package/packages/updater/package.json +2 -2
- package/packages/utility/package.json +2 -2
- package/packages/utility/skills/cron/SKILL.md +6 -0
- package/packages/watchdog/package.json +3 -3
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [3.0.0-alpha.45] — 2026-10-24
|
|
10
|
+
|
|
11
|
+
Includes the never-published 3.0.0-alpha.44 changes below.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Graph and swarm progress bars count what's settled. The number is done + failed over the total (1 done, 1 failed, 1 running, 1 not started reads `2/4`). The bar shows done in green, failed in red and running in the accent colour, with a highlight sweeping across the running part while the run is live. Items not started yet stay grey. The same change applies to `/unipi:visualize-progress`, the app's Progress sheet and mode popover, the chart title and the end-of-run log line.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Session cost now includes what child agents spend. Graph and swarm work, subagents, the Fusion sidekick and `bg_run` agent tasks each run as a separate pi process, and the footer, the app's session cost and `/unipi:info` usage used to count only the lead's own messages. So any multi-agent run was under-priced. Each child model reply is now written into the lead session as a `unipi-child-usage` entry. The entry is saved with the session, follows the branch you're on and is never sent to the model. The footer and app add these entries to the cost, and info-screen stats count the subagent and sidekick ones too. `bg_run` agents are already counted from their own session files, so they aren't counted twice. Token counts are still the lead's own. Not covered yet: grandchildren (subagents started by subagents) and runs from before this fix.
|
|
18
|
+
|
|
19
|
+
## [3.0.0-alpha.44] — 2026-10-19
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Windows: the app can reach pi chats on a Windows PC. On Windows the bridge now listens on a named pipe (`\\.\pipe\unipi-bridge-<pid>`) instead of a Unix socket file, which Node can't create there (EACCES); Linux and macOS are unchanged. Needs UniPi host 0.5.2 on the Windows PC.
|
|
23
|
+
- Deleting a queued message from the app no longer stops the run. Editing, removing or steer-promoting one of pi's own queued messages used to abort the whole run and re-send what was left (UNI-305) — which meant deleting the queue's only entry killed the chat outright. The bridge now reaches pi's `AgentSession.clearQueue()` (captured with a one-time prototype patch, used only when it provably belongs to the session being edited) to drain and re-queue around the change while pi keeps streaming; the abort-and-resend path remains only as a fallback for older pi builds, and "Send now" still stops the run on purpose. (UNI-320)
|
|
24
|
+
|
|
9
25
|
## [3.0.0-alpha.43] — 2026-10-16
|
|
10
26
|
|
|
11
27
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/unipi",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.45",
|
|
4
4
|
"description": "All-in-one extension suite for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -82,28 +82,28 @@
|
|
|
82
82
|
"typebox": "^1.1.38"
|
|
83
83
|
},
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"@pi-unipi/app-bridge": "3.0.0-alpha.
|
|
86
|
-
"@pi-unipi/ask-user": "3.0.0-alpha.
|
|
87
|
-
"@pi-unipi/background-tasks": "3.0.0-alpha.
|
|
88
|
-
"@pi-unipi/btw": "3.0.0-alpha.
|
|
89
|
-
"@pi-unipi/command-enchantment": "3.0.0-alpha.
|
|
90
|
-
"@pi-unipi/compactor": "3.0.0-alpha.
|
|
91
|
-
"@pi-unipi/core": "3.0.0-alpha.
|
|
92
|
-
"@pi-unipi/dream": "3.0.0-alpha.
|
|
93
|
-
"@pi-unipi/footer": "3.0.0-alpha.
|
|
94
|
-
"@pi-unipi/fusion": "3.0.0-alpha.
|
|
95
|
-
"@pi-unipi/info-screen": "3.0.0-alpha.
|
|
96
|
-
"@pi-unipi/input-shortcuts": "3.0.0-alpha.
|
|
97
|
-
"@pi-unipi/kanboard": "3.0.0-alpha.
|
|
98
|
-
"@pi-unipi/mcp": "3.0.0-alpha.
|
|
99
|
-
"@pi-unipi/memory": "3.0.0-alpha.
|
|
100
|
-
"@pi-unipi/notify": "3.0.0-alpha.
|
|
101
|
-
"@pi-unipi/skill-registry": "3.0.0-alpha.
|
|
102
|
-
"@pi-unipi/subagents": "3.0.0-alpha.
|
|
103
|
-
"@pi-unipi/updater": "3.0.0-alpha.
|
|
104
|
-
"@pi-unipi/utility": "3.0.0-alpha.
|
|
105
|
-
"@pi-unipi/web-api": "3.0.0-alpha.
|
|
106
|
-
"@pi-unipi/workflow": "3.0.0-alpha.
|
|
85
|
+
"@pi-unipi/app-bridge": "3.0.0-alpha.45",
|
|
86
|
+
"@pi-unipi/ask-user": "3.0.0-alpha.45",
|
|
87
|
+
"@pi-unipi/background-tasks": "3.0.0-alpha.45",
|
|
88
|
+
"@pi-unipi/btw": "3.0.0-alpha.45",
|
|
89
|
+
"@pi-unipi/command-enchantment": "3.0.0-alpha.45",
|
|
90
|
+
"@pi-unipi/compactor": "3.0.0-alpha.45",
|
|
91
|
+
"@pi-unipi/core": "3.0.0-alpha.45",
|
|
92
|
+
"@pi-unipi/dream": "3.0.0-alpha.45",
|
|
93
|
+
"@pi-unipi/footer": "3.0.0-alpha.45",
|
|
94
|
+
"@pi-unipi/fusion": "3.0.0-alpha.45",
|
|
95
|
+
"@pi-unipi/info-screen": "3.0.0-alpha.45",
|
|
96
|
+
"@pi-unipi/input-shortcuts": "3.0.0-alpha.45",
|
|
97
|
+
"@pi-unipi/kanboard": "3.0.0-alpha.45",
|
|
98
|
+
"@pi-unipi/mcp": "3.0.0-alpha.45",
|
|
99
|
+
"@pi-unipi/memory": "3.0.0-alpha.45",
|
|
100
|
+
"@pi-unipi/notify": "3.0.0-alpha.45",
|
|
101
|
+
"@pi-unipi/skill-registry": "3.0.0-alpha.45",
|
|
102
|
+
"@pi-unipi/subagents": "3.0.0-alpha.45",
|
|
103
|
+
"@pi-unipi/updater": "3.0.0-alpha.45",
|
|
104
|
+
"@pi-unipi/utility": "3.0.0-alpha.45",
|
|
105
|
+
"@pi-unipi/web-api": "3.0.0-alpha.45",
|
|
106
|
+
"@pi-unipi/workflow": "3.0.0-alpha.45",
|
|
107
107
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
108
108
|
"beautiful-mermaid": "^1.1.3"
|
|
109
109
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/app-bridge",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.45",
|
|
4
4
|
"description": "Live bridge between a pi TUI session and the UniPi phone app — streaming chat, prompts, steering and dialogs from the phone",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@pi-unipi/core": "3.0.0-alpha.
|
|
34
|
+
"@pi-unipi/core": "3.0.0-alpha.45"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@earendil-works/pi-coding-agent": "^0.87.1",
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* - Every hook body is try/catch: the bridge must never break a turn.
|
|
9
9
|
*/
|
|
10
10
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { AgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { createRequire } from "node:module";
|
|
12
13
|
import { createServer, type Server, type Socket } from "node:net";
|
|
13
14
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
14
15
|
import { homedir } from "node:os";
|
|
15
|
-
import { join, relative, resolve as resolvePath } from "node:path";
|
|
16
|
+
import { dirname, join, relative, resolve as resolvePath } from "node:path";
|
|
16
17
|
import { DialogHub, wrapUi } from "./dialogs.js";
|
|
17
18
|
import { ENTRIES_BUDGET, HELLO_ENTRIES_BUDGET, LINE_BUDGET, fitLine, historyPage, jsonBytes, phoneSafe, snapshotEntries, clipText, editedAway, wantedEntry, treeGoRestore, type TreeGoRestore } from "./snapshot.js";
|
|
18
19
|
import { searchSession } from "./search.js";
|
|
@@ -45,7 +46,7 @@ import { fileSuggestions } from "./files.js";
|
|
|
45
46
|
import { statPaths } from "./paths.js";
|
|
46
47
|
import { registerPath, resolveMedia } from "./media.js";
|
|
47
48
|
import { listWorkItems, stopWorkItem, backgroundWorkItem, workLogPage, workTranscript } from "./work.js";
|
|
48
|
-
import { bus, callCommandRunner, commandArgCompleters, pendingWorkLabel, silenceCommandEcho, subscribePendingWork, UNIPI_EVENTS } from "@pi-unipi/core";
|
|
49
|
+
import { bus, callCommandRunner, childUsageOf, commandArgCompleters, onChildUsage, pendingWorkLabel, silenceCommandEcho, subscribePendingWork, UNIPI_EVENTS } from "@pi-unipi/core";
|
|
49
50
|
import type { BtwListPage } from "./wire.js";
|
|
50
51
|
|
|
51
52
|
/** `media_chunk.data` (base64) stays well under the 900 KiB bridge line
|
|
@@ -151,7 +152,7 @@ interface TpsTrackerLike {
|
|
|
151
152
|
const FOOTER_TPS_KEY = Symbol.for("unipi.footer.shared-tps");
|
|
152
153
|
const getSharedTps = (): TpsTrackerLike | undefined => (globalThis as unknown as Record<symbol, unknown>)[FOOTER_TPS_KEY] as TpsTrackerLike | undefined;
|
|
153
154
|
|
|
154
|
-
export const BRIDGE_VERSION = "1.
|
|
155
|
+
export const BRIDGE_VERSION = "1.1.0";
|
|
155
156
|
|
|
156
157
|
/** The hidden extension command the bridge uses to get a command-capable
|
|
157
158
|
* context (newSession/fork/navigateTree/switchSession only exist there). */
|
|
@@ -197,6 +198,21 @@ export function bridgeDir(): string {
|
|
|
197
198
|
return process.env.UNIPI_BRIDGE_DIR || join(homedir(), ".unipi", "bridge");
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
/** The path/pipe name the bridge listens on for a given pid. On win32 this
|
|
202
|
+
* is a named pipe (`\\\\.\\pipe\\unipi-bridge-<pid>`) so unipi-host (Windows)
|
|
203
|
+
* can connect; everywhere else it's a unix socket file inside `dir`.
|
|
204
|
+
* `platform` is injectable so this stays unit-testable on Linux. */
|
|
205
|
+
export function bridgeSocketPath(dir: string, pid: number, platform: NodeJS.Platform = process.platform): string {
|
|
206
|
+
if (platform === "win32") return "\\\\.\\pipe\\unipi-bridge-" + pid;
|
|
207
|
+
return join(dir, `${pid}.sock`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Whether `path` is a Windows named pipe (as produced by `bridgeSocketPath`
|
|
211
|
+
* on win32), rather than a unix socket file path. */
|
|
212
|
+
export function isBridgePipe(path: string): boolean {
|
|
213
|
+
return path.startsWith("\\\\.\\pipe\\");
|
|
214
|
+
}
|
|
215
|
+
|
|
200
216
|
function alive(pid: number): boolean {
|
|
201
217
|
try {
|
|
202
218
|
process.kill(pid, 0);
|
|
@@ -250,7 +266,107 @@ export interface BridgeDeps {
|
|
|
250
266
|
}
|
|
251
267
|
const defaultDeps: BridgeDeps = { listSessions: SessionManager.list, listAllSessions: SessionManager.listAll as typeof SessionManager.listAll };
|
|
252
268
|
|
|
269
|
+
/** UNI-320: pi's AgentSession.clearQueue() drains its own steer/followUp queue
|
|
270
|
+
* WITHOUT touching the running turn — the one primitive the extension API
|
|
271
|
+
* never exposed (the `input`/`sendUserMessage` surface can only ADD queue
|
|
272
|
+
* items, which is why editing/removing a `tui-*` row used to abort the run).
|
|
273
|
+
* The session instance isn't handed to extensions either, so it is captured
|
|
274
|
+
* by a one-time prototype patch (same approach as core's UNI-304 patch):
|
|
275
|
+
* methods pi itself always dispatches through `AgentSession.prototype` —
|
|
276
|
+
* `bindExtensions` on every session bind, `sendUserMessage`/`steer`/
|
|
277
|
+
* `followUp`/`abort` on activity — record the instance they are called on.
|
|
278
|
+
* A capture is only USED when it provably belongs to the context's session
|
|
279
|
+
* (same `sessionManager` object): a stale reference after a session switch
|
|
280
|
+
* would drain the wrong queue. Anything else — an older pi without
|
|
281
|
+
* clearQueue, a fake in tests — answers undefined and the bridge falls back
|
|
282
|
+
* to UNI-305's abort-and-resend. */
|
|
283
|
+
type ClearQueueSession = {
|
|
284
|
+
clearQueue: () => { steering: string[]; followUp: string[] };
|
|
285
|
+
sessionManager?: unknown;
|
|
286
|
+
};
|
|
287
|
+
let capturedSession: ClearQueueSession | undefined;
|
|
288
|
+
let sessionCapturePatched = false;
|
|
289
|
+
/** Wrap the prototype methods pi itself always dispatches through
|
|
290
|
+
* `AgentSession.prototype` — `bindExtensions` on every session bind,
|
|
291
|
+
* `sendUserMessage`/`steer`/`followUp`/`abort` on activity — so the instance
|
|
292
|
+
* records itself. Capture-only: behavior is unchanged, and the __unipiCapture
|
|
293
|
+
* flag keeps repeated patching idempotent. Must run EAGERLY (at bridge
|
|
294
|
+
* creation), before the first prompt: a lazily-installed patch misses every
|
|
295
|
+
* dispatch that already happened, and the first queue edit would find nothing
|
|
296
|
+
* captured and fall back to abort-and-resend. */
|
|
297
|
+
const patchAgentSessionPrototype = (Ctor: new () => unknown): void => {
|
|
298
|
+
const proto = Ctor.prototype as unknown as Record<string, ((...args: never[]) => unknown) | undefined>;
|
|
299
|
+
for (const name of ["bindExtensions", "sendUserMessage", "steer", "followUp", "abort"]) {
|
|
300
|
+
const current = proto[name];
|
|
301
|
+
if (typeof current !== "function" || (current as { __unipiCapture?: boolean }).__unipiCapture) continue;
|
|
302
|
+
const wrapped = function (this: unknown, ...args: never[]) {
|
|
303
|
+
capturedSession = this as ClearQueueSession;
|
|
304
|
+
return current!.apply(this, args);
|
|
305
|
+
} as ((...args: never[]) => unknown) & { __unipiCapture?: boolean };
|
|
306
|
+
wrapped.__unipiCapture = true;
|
|
307
|
+
proto[name] = wrapped;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
/** The installed pi copies whose AgentSession could be the live one. There
|
|
311
|
+
* are SEVERAL in one process and which of them is real depends on how pi was
|
|
312
|
+
* started: pi's npm dist ships two separate module graphs (dist/bundle/* —
|
|
313
|
+
* what the CLI runs — and dist/index.js — the public barrel), and the
|
|
314
|
+
* extension's own static import is yet another copy: pi loads extensions
|
|
315
|
+
* through jiti, which evaluates the aliased pi module separately, so classes
|
|
316
|
+
* from the static import are never the classes the runtime uses. All
|
|
317
|
+
* candidates get the capture-only patch; the copies the runtime never
|
|
318
|
+
* instantiates simply never fire, and liveAgentSession() still validates the
|
|
319
|
+
* capture against this context's sessionManager before any use. */
|
|
320
|
+
const patchAllRuntimeCopies = (): void => {
|
|
321
|
+
patchAgentSessionPrototype(AgentSession as new () => unknown);
|
|
322
|
+
const pkgRoot = join(dirname(dirname(process.execPath)), "lib", "node_modules", "@earendil-works", "pi-coding-agent");
|
|
323
|
+
const candidates = [
|
|
324
|
+
join(pkgRoot, "dist", "bundle", "index.js"),
|
|
325
|
+
join(pkgRoot, "dist", "index.js"),
|
|
326
|
+
process.argv[1]
|
|
327
|
+
? resolvePath(dirname(process.argv[1]), "..", "lib", "node_modules", "@earendil-works", "pi-coding-agent", "dist", "bundle", "index.js")
|
|
328
|
+
: "",
|
|
329
|
+
];
|
|
330
|
+
for (const index of candidates) {
|
|
331
|
+
if (!index) continue;
|
|
332
|
+
try {
|
|
333
|
+
const mod = createRequire(index)(index) as { AgentSession?: new () => unknown };
|
|
334
|
+
const Ctor = mod?.AgentSession;
|
|
335
|
+
if (Ctor && typeof (Ctor.prototype as { clearQueue?: unknown }).clearQueue === "function") patchAgentSessionPrototype(Ctor);
|
|
336
|
+
} catch {
|
|
337
|
+
// candidate not present in this install — the others cover it
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
/** Test hook: plant a stand-in session (the fake pi in tests has no real
|
|
342
|
+
* AgentSession to capture). Pass undefined to reset. */
|
|
343
|
+
export function setCapturedAgentSessionForTests(session: unknown): void {
|
|
344
|
+
capturedSession = (session ?? undefined) as ClearQueueSession | undefined;
|
|
345
|
+
sessionCapturePatched = true;
|
|
346
|
+
}
|
|
347
|
+
/** The live AgentSession behind `c`, captured through the prototype patch —
|
|
348
|
+
* but only trusted when it provably belongs to THIS session (same
|
|
349
|
+
* sessionManager object as the extension context): a stale reference after a
|
|
350
|
+
* session switch would drain the wrong queue. Anything else — an older pi,
|
|
351
|
+
* a fake in tests — answers undefined and the bridge falls back to
|
|
352
|
+
* UNI-305's abort-and-resend. */
|
|
353
|
+
const liveAgentSession = (c: ExtensionContext): ClearQueueSession | undefined => {
|
|
354
|
+
if (!sessionCapturePatched) {
|
|
355
|
+
patchAllRuntimeCopies();
|
|
356
|
+
sessionCapturePatched = true;
|
|
357
|
+
}
|
|
358
|
+
const session = capturedSession;
|
|
359
|
+
if (!session || typeof session.clearQueue !== "function") return undefined;
|
|
360
|
+
if (session.sessionManager !== (c as unknown as { sessionManager?: unknown }).sessionManager) return undefined;
|
|
361
|
+
return session;
|
|
362
|
+
};
|
|
363
|
+
|
|
253
364
|
export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
365
|
+
// UNI-320: install the AgentSession capture now, before any prompt can be
|
|
366
|
+
// sent — a lazily-installed patch misses every dispatch that already
|
|
367
|
+
// happened, and the first queue edit would fall back to abort-and-resend.
|
|
368
|
+
// (Session-bound validation happens later, in liveAgentSession().)
|
|
369
|
+
patchAllRuntimeCopies();
|
|
254
370
|
let ctx: ExtensionContext | undefined;
|
|
255
371
|
let server: Server | undefined;
|
|
256
372
|
let socketPath: string | undefined;
|
|
@@ -259,10 +375,13 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
259
375
|
/** pi's own queue (steer/follow-up from ANY source — TUI keybindings, the
|
|
260
376
|
* harness, an extension's `pi.sendUserMessage({deliverAs})`), mirrored from
|
|
261
377
|
* the `input` event's `streamingBehavior`. UNI-202: editable/removable/
|
|
262
|
-
* promotable from the phone too
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
378
|
+
* promotable from the phone too. UNI-305 did that by aborting the run and
|
|
379
|
+
* re-sending what's left; UNI-320 found a real primitive
|
|
380
|
+
* (`AgentSession.clearQueue()`, captured via a prototype patch — see
|
|
381
|
+
* `liveAgentSession`) that drains pi's queue WITHOUT touching the run, so
|
|
382
|
+
* an edit/remove/promote now keeps pi streaming and only falls back to
|
|
383
|
+
* abort-and-resend on older pi builds ("send now" always aborts: that's
|
|
384
|
+
* the point of the button). */
|
|
266
385
|
const tuiQueue: Array<{ id: string; text: string; mode: "steer" | "followUp"; images?: Array<{ mime: string; data: string } | { mime: string; path: string }> }> = [];
|
|
267
386
|
/** The bridge's own queue ("after it ends"): phone-only, editable/removable/reorderable/promotable,
|
|
268
387
|
* delivered one at a time on `agent_end`. Survives phone reconnects (sent in `hello`), not a pi restart. */
|
|
@@ -287,6 +406,10 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
287
406
|
let deltaTimer: NodeJS.Timeout | undefined;
|
|
288
407
|
const toolTimers = new Map<string, NodeJS.Timeout>();
|
|
289
408
|
let sessionCost = 0;
|
|
409
|
+
// Child agents (graph/swarm work, subagents, Fusion sidekick, bg_run
|
|
410
|
+
// agents) record their spend into this session as custom entries; count it
|
|
411
|
+
// live too so the phone's cost isn't lead-only.
|
|
412
|
+
let unsubChildUsage: (() => void) | undefined;
|
|
290
413
|
/** "Needs you" (UNI-161): an open dialog, or idle right after an
|
|
291
414
|
* `agent_end` no phone has seen yet. Written into the discovery record so
|
|
292
415
|
* the host's `chat_list` (crates/host/src/chat.rs) can float it to the top
|
|
@@ -478,6 +601,7 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
478
601
|
try {
|
|
479
602
|
for (const e of ctx!.sessionManager.getEntries() as Array<{ type?: string; message?: { role?: string; usage?: { cost?: { total?: number } } } }>) {
|
|
480
603
|
if (e.type === "message" && e.message?.role === "assistant") sessionCost += e.message.usage?.cost?.total ?? 0;
|
|
604
|
+
else sessionCost += childUsageOf(e)?.cost ?? 0;
|
|
481
605
|
}
|
|
482
606
|
} catch {
|
|
483
607
|
// ignore
|
|
@@ -1135,23 +1259,100 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1135
1259
|
}
|
|
1136
1260
|
};
|
|
1137
1261
|
|
|
1138
|
-
const editPiQueued = async (sock: Socket, id: string, ref: string | undefined, apply: (item: (typeof tuiQueue)[number]) => "drop" | "promote" | void) => {
|
|
1262
|
+
const editPiQueued = async (sock: Socket, id: string, ref: string | undefined, apply: (item: (typeof tuiQueue)[number]) => "drop" | "promote" | void, promoteNow = false) => {
|
|
1139
1263
|
const c = ctx;
|
|
1140
1264
|
const ack = () => write(sock, { t: "ack", ref });
|
|
1141
1265
|
const fail = (message: string) => write(sock, { t: "error", message, ref });
|
|
1142
1266
|
if (!c) return fail("pi is gone.");
|
|
1143
1267
|
const i = tuiQueue.findIndex((q) => q.id === id);
|
|
1144
1268
|
if (i < 0) return fail("That queued message is gone.");
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1269
|
+
const target = tuiQueue[i];
|
|
1270
|
+
const contentOf = (q: (typeof tuiQueue)[number]) => (q.images?.length ? [{ type: "text" as const, text: q.text }, ...q.images.map(imageContent)] : q.text);
|
|
1271
|
+
/** Re-send survivors once pi's real queue already matches: the first item
|
|
1272
|
+
* starts a fresh run when idle (a deliverAs while idle would be rejected
|
|
1273
|
+
* with "Specify streamingBehavior"), then the rest in pi's own order
|
|
1274
|
+
* (steer drains before followUp). */
|
|
1275
|
+
const resend = async (rest: Array<(typeof tuiQueue)[number]>) => {
|
|
1276
|
+
send({ t: "queue", items: queueView() });
|
|
1277
|
+
const order = [...rest].sort((a, b) => (a === rest[0] ? -1 : b === rest[0] ? 1 : a.mode === b.mode ? 0 : a.mode === "steer" ? -1 : 1));
|
|
1278
|
+
for (const q of order) {
|
|
1279
|
+
if (c.isIdle()) {
|
|
1280
|
+
pi.sendUserMessage(contentOf(q), { expandPromptTemplates: true } as never);
|
|
1281
|
+
const until = Date.now() + 5_000;
|
|
1282
|
+
while (c.isIdle() && Date.now() < until) await sleep(20);
|
|
1283
|
+
} else {
|
|
1284
|
+
pi.sendUserMessage(contentOf(q), { deliverAs: q.mode, expandPromptTemplates: true } as never);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
// UNI-320: preferred path — drain pi's own queue with clearQueue() (which
|
|
1289
|
+
// leaves the running turn alone), drop/edit/promote the target in the
|
|
1290
|
+
// drained truth, and re-queue the survivors. No abort, no idle-wait, no
|
|
1291
|
+
// hold: a busy pi keeps streaming mid-sentence, and deleting the queue's
|
|
1292
|
+
// ONLY entry no longer kills the run (the UNI-305 hole where nothing was
|
|
1293
|
+
// left to re-send). "Send now" (promoteNow) still aborts on purpose —
|
|
1294
|
+
// stopping the run is what that button says it does.
|
|
1295
|
+
const session = promoteNow ? undefined : liveAgentSession(c);
|
|
1296
|
+
if (session) {
|
|
1297
|
+
tuiQueue.splice(i, 1);
|
|
1298
|
+
// pi's queue holds plain expanded texts; match the target by its
|
|
1299
|
+
// ORIGINAL text (apply() may edit it) so edit keeps working. Images
|
|
1300
|
+
// live only in the mirror, so keep a lookup before emptying it.
|
|
1301
|
+
const original = { text: target!.text, mode: target!.mode };
|
|
1302
|
+
const mirrorImages = new Map(tuiQueue.filter((q) => q.images?.length).map((q) => [`${q.mode}\u0000${q.text}`, q.images]));
|
|
1303
|
+
const imageOf = (text: string, mode: "steer" | "followUp") =>
|
|
1304
|
+
(text === original.text && mode === original.mode ? target!.images : mirrorImages.get(`${mode}\u0000${text}`));
|
|
1305
|
+
const action = apply(target!) ?? undefined;
|
|
1306
|
+
const drained = session.clearQueue();
|
|
1307
|
+
// The mirror must match pi's now-drained queue: empty it (the re-sent
|
|
1308
|
+
// survivors echo back through `input` and re-mirror themselves).
|
|
1309
|
+
tuiQueue.length = 0;
|
|
1310
|
+
const steers = [...drained.steering];
|
|
1311
|
+
const follows = [...drained.followUp];
|
|
1312
|
+
const pool = original.mode === "steer" ? steers : follows;
|
|
1313
|
+
const at = pool.indexOf(original.text);
|
|
1314
|
+
if (at < 0) {
|
|
1315
|
+
// pi delivered it between the phone's tap and now: put everything
|
|
1316
|
+
// back untouched and refuse, like the "already handed to pi" case.
|
|
1317
|
+
try {
|
|
1318
|
+
await resend([
|
|
1319
|
+
...steers.map((text) => ({ id: "", text, mode: "steer" as const, images: imageOf(text, "steer") })),
|
|
1320
|
+
...follows.map((text) => ({ id: "", text, mode: "followUp" as const, images: imageOf(text, "followUp") })),
|
|
1321
|
+
]);
|
|
1322
|
+
} catch {
|
|
1323
|
+
// re-queue best-effort; the refusal still tells the user the truth
|
|
1324
|
+
}
|
|
1325
|
+
return fail("That message was already handed to pi — it can't be taken back now.");
|
|
1326
|
+
}
|
|
1327
|
+
pool.splice(at, 1);
|
|
1328
|
+
const kept = action === "drop" ? undefined : target!;
|
|
1329
|
+
const rest: Array<(typeof tuiQueue)[number]> = [
|
|
1330
|
+
...steers.map((text) => ({ id: "", text, mode: "steer" as const, images: imageOf(text, "steer") })),
|
|
1331
|
+
...follows.map((text) => ({ id: "", text, mode: "followUp" as const, images: imageOf(text, "followUp") })),
|
|
1332
|
+
];
|
|
1333
|
+
if (kept) {
|
|
1334
|
+
if (action === "promote") rest.unshift(kept);
|
|
1335
|
+
else rest.splice(original.mode === "steer" ? Math.min(at, steers.length) : steers.length + Math.min(at, follows.length), 0, kept);
|
|
1336
|
+
}
|
|
1337
|
+
try {
|
|
1338
|
+
await resend(rest);
|
|
1339
|
+
ack();
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
1342
|
+
}
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
// Fallback (older pi, or "send now"): UNI-305's abort-and-resend. pi has
|
|
1346
|
+
// no API to drop ONE item from its own live steer/followUp queue there —
|
|
1347
|
+
// the only way to edit it is to abort the whole run and re-send what's
|
|
1348
|
+
// left (see the class comment above `tuiQueue`). That's fine between tool
|
|
1349
|
+
// calls (there is nothing running to lose), but aborting WHILE a tool is
|
|
1350
|
+
// executing kills it mid-flight — real, unsaved work — and if the removed
|
|
1351
|
+
// item was the queue's only entry nothing is left to re-send: the run
|
|
1352
|
+
// just dies instead of continuing. So this waits a short beat for the
|
|
1353
|
+
// in-flight tool to reach its own end (the next safe boundary) before
|
|
1354
|
+
// touching the queue; a tool still running past that is left alone and
|
|
1355
|
+
// the edit is refused instead of destroying it.
|
|
1155
1356
|
if (tools.size > 0) {
|
|
1156
1357
|
const deadline = Date.now() + 4_000;
|
|
1157
1358
|
while (tools.size > 0 && Date.now() < deadline) await sleep(100);
|
|
@@ -1162,14 +1363,15 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1162
1363
|
// actually sends it) — re-check instead of operating on a stale index.
|
|
1163
1364
|
const liveIndex = tuiQueue.findIndex((q) => q.id === id);
|
|
1164
1365
|
if (liveIndex < 0) return fail("That message was already handed to pi — it can't be taken back now.");
|
|
1165
|
-
const [
|
|
1166
|
-
const action = apply(
|
|
1366
|
+
const [liveTarget] = tuiQueue.splice(liveIndex, 1);
|
|
1367
|
+
const action = apply(liveTarget!) ?? undefined;
|
|
1167
1368
|
// Keep the edited/kept target in its original slot (promote: first).
|
|
1168
1369
|
const rest = tuiQueue.splice(0, tuiQueue.length);
|
|
1169
|
-
if (action === "promote") rest.unshift(
|
|
1170
|
-
else if (action !== "drop") rest.splice(liveIndex, 0,
|
|
1370
|
+
if (action === "promote") rest.unshift(liveTarget!);
|
|
1371
|
+
else if (action !== "drop") rest.splice(liveIndex, 0, liveTarget!);
|
|
1171
1372
|
if (!c.isIdle()) {
|
|
1172
1373
|
await abortKeepingEditor(c);
|
|
1374
|
+
|
|
1173
1375
|
const deadline = Date.now() + 10_000;
|
|
1174
1376
|
while (!c.isIdle() && Date.now() < deadline) await sleep(50);
|
|
1175
1377
|
// The abort was only the means to edit pi's queue: the re-sent items
|
|
@@ -1177,23 +1379,8 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1177
1379
|
// re-sent the hold lapses and the "after" queue delivers normally.
|
|
1178
1380
|
holdUntilNextRun();
|
|
1179
1381
|
}
|
|
1180
|
-
send({ t: "queue", items: queueView() });
|
|
1181
|
-
const contentOf = (q: (typeof tuiQueue)[number]) => (q.images?.length ? [{ type: "text" as const, text: q.text }, ...q.images.map(imageContent)] : q.text);
|
|
1182
1382
|
try {
|
|
1183
|
-
|
|
1184
|
-
// streamingBehavior"/"already processing"). The first item starts a
|
|
1185
|
-
// fresh run; wait until pi is streaming, then queue the rest in pi's
|
|
1186
|
-
// own order (steer drains before followUp).
|
|
1187
|
-
const order = [...rest].sort((a, b) => (a === rest[0] ? -1 : b === rest[0] ? 1 : a.mode === b.mode ? 0 : a.mode === "steer" ? -1 : 1));
|
|
1188
|
-
for (const q of order) {
|
|
1189
|
-
if (c.isIdle()) {
|
|
1190
|
-
pi.sendUserMessage(contentOf(q), { expandPromptTemplates: true } as never);
|
|
1191
|
-
const until = Date.now() + 5_000;
|
|
1192
|
-
while (c.isIdle() && Date.now() < until) await sleep(20);
|
|
1193
|
-
} else {
|
|
1194
|
-
pi.sendUserMessage(contentOf(q), { deliverAs: q.mode, expandPromptTemplates: true } as never);
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1383
|
+
await resend(rest);
|
|
1197
1384
|
ack();
|
|
1198
1385
|
} catch (error) {
|
|
1199
1386
|
fail(error instanceof Error ? error.message : String(error));
|
|
@@ -1311,7 +1498,9 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1311
1498
|
return;
|
|
1312
1499
|
}
|
|
1313
1500
|
case "queue_promote": {
|
|
1314
|
-
|
|
1501
|
+
// "now" means stop the run on purpose — keep it on the abort path
|
|
1502
|
+
// even when clearQueue surgery is available.
|
|
1503
|
+
if (msg.id.startsWith("tui-")) return void (await editPiQueued(sock, msg.id, msg.ref, () => "promote", msg.to === "now"));
|
|
1315
1504
|
const i = bridgeQueue.findIndex((q) => q.id === msg.id);
|
|
1316
1505
|
if (i < 0) return fail("That queued message is gone.");
|
|
1317
1506
|
const [item] = bridgeQueue.splice(i, 1);
|
|
@@ -1766,8 +1955,8 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1766
1955
|
return;
|
|
1767
1956
|
}
|
|
1768
1957
|
sweepDead(dir);
|
|
1769
|
-
socketPath =
|
|
1770
|
-
rmSync(socketPath, { force: true });
|
|
1958
|
+
socketPath = bridgeSocketPath(dir, process.pid);
|
|
1959
|
+
if (!isBridgePipe(socketPath)) rmSync(socketPath, { force: true });
|
|
1771
1960
|
const srv = createServer((sock) => {
|
|
1772
1961
|
clients.add(sock);
|
|
1773
1962
|
// A phone connecting has now seen the state (it's in `hello`): the
|
|
@@ -1808,10 +1997,12 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1808
1997
|
// Socket trouble (dir removed…) never matters to pi.
|
|
1809
1998
|
});
|
|
1810
1999
|
srv.listen(socketPath, () => {
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
2000
|
+
if (!isBridgePipe(socketPath!)) {
|
|
2001
|
+
try {
|
|
2002
|
+
chmodSync(socketPath!, 0o600);
|
|
2003
|
+
} catch {
|
|
2004
|
+
// ignore
|
|
2005
|
+
}
|
|
1815
2006
|
}
|
|
1816
2007
|
writeRecord();
|
|
1817
2008
|
});
|
|
@@ -1850,7 +2041,7 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1850
2041
|
server = undefined;
|
|
1851
2042
|
const dir = bridgeDir();
|
|
1852
2043
|
rmSync(join(dir, `${process.pid}.json`), { force: true });
|
|
1853
|
-
if (socketPath) rmSync(socketPath, { force: true });
|
|
2044
|
+
if (socketPath && !isBridgePipe(socketPath)) rmSync(socketPath, { force: true });
|
|
1854
2045
|
socketPath = undefined;
|
|
1855
2046
|
};
|
|
1856
2047
|
|
|
@@ -1875,6 +2066,10 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1875
2066
|
syncSoon();
|
|
1876
2067
|
});
|
|
1877
2068
|
computeCost();
|
|
2069
|
+
unsubChildUsage?.();
|
|
2070
|
+
unsubChildUsage = onChildUsage((data) => {
|
|
2071
|
+
sessionCost += data.cost;
|
|
2072
|
+
});
|
|
1878
2073
|
setRemoteDialogs(hub);
|
|
1879
2074
|
listen();
|
|
1880
2075
|
writeRecord();
|
|
@@ -1998,6 +2193,8 @@ export function createBridge(pi: ExtensionAPI, deps: BridgeDeps = defaultDeps) {
|
|
|
1998
2193
|
};
|
|
1999
2194
|
on("session_start", safe(() => watchDream()));
|
|
2000
2195
|
on("session_shutdown", safe(() => {
|
|
2196
|
+
unsubChildUsage?.();
|
|
2197
|
+
unsubChildUsage = undefined;
|
|
2001
2198
|
unsubDream?.();
|
|
2002
2199
|
unsubDream = undefined;
|
|
2003
2200
|
if (dreamTimer) clearTimeout(dreamTimer);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/ask-user",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.45",
|
|
4
4
|
"description": "Structured user input tool for Pi coding agent — single-select, multi-select, freeform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@pi-unipi/core": "3.0.0-alpha.
|
|
45
|
+
"@pi-unipi/core": "3.0.0-alpha.45"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@earendil-works/pi-coding-agent": "^0.87.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/background-tasks",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.45",
|
|
4
4
|
"description": "Background tasks for UniPi — durable shell jobs and delegated background agents with a footer dock",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@earendil-works/pi-ai": "^0.87.1",
|
|
13
|
-
"@pi-unipi/core": "3.0.0-alpha.
|
|
13
|
+
"@pi-unipi/core": "3.0.0-alpha.45",
|
|
14
14
|
"turndown": "^7.2.4"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
isChildProcess,
|
|
15
15
|
type KitTheme,
|
|
16
16
|
openWorkTray,
|
|
17
|
+
recordChildUsage,
|
|
17
18
|
registerWaitSource,
|
|
18
19
|
registerWorkTrayTab,
|
|
19
20
|
setBashBackgroundAdopter,
|
|
@@ -55,6 +56,7 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
const seenTaskIds = new Set<string>();
|
|
59
|
+
const costRecorded = new Set<string>();
|
|
58
60
|
let currentCtx: ExtensionContext | undefined;
|
|
59
61
|
|
|
60
62
|
const registry = new BackgroundTaskRegistry({
|
|
@@ -68,6 +70,16 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
|
68
70
|
pi.sendMessage(message as never, options as never);
|
|
69
71
|
},
|
|
70
72
|
publishTerminal: (task) => {
|
|
73
|
+
// An agent task's spend belongs in the lead session's cost. Its own pi
|
|
74
|
+
// session already lives under the global sessions dir, so it is not
|
|
75
|
+
// standalone for cross-session stats.
|
|
76
|
+
if (task.isAgent && task.tokenUsage && !costRecorded.has(task.id)) {
|
|
77
|
+
costRecorded.add(task.id);
|
|
78
|
+
const u = task.tokenUsage;
|
|
79
|
+
recordChildUsage((type, data) => pi.appendEntry(type, data), { source: "bg-task", id: task.id, model: task.model, standalone: false }, {
|
|
80
|
+
input: u.input, output: u.output, cacheRead: u.cacheRead, cacheWrite: u.cacheWrite, cost: u.costTotal,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
71
83
|
eventService.publishTerminal(task);
|
|
72
84
|
},
|
|
73
85
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/btw",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.45",
|
|
4
4
|
"description": "A pi extension for parallel side conversations with /unipi:btw — part of the Unipi suite",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "extensions/btw.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@earendil-works/pi-tui": "^0.87.1"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@pi-unipi/core": "3.0.0-alpha.
|
|
40
|
+
"@pi-unipi/core": "3.0.0-alpha.45"
|
|
41
41
|
},
|
|
42
42
|
"pi": {
|
|
43
43
|
"extensions": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/compactor",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.45",
|
|
4
4
|
"description": "Compaction for Pi — lossless zero-LLM or model summaries, rebuilt from full history, with session recall",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@pi-unipi/core": "3.0.0-alpha.
|
|
37
|
-
"@pi-unipi/info-screen": "3.0.0-alpha.
|
|
36
|
+
"@pi-unipi/core": "3.0.0-alpha.45",
|
|
37
|
+
"@pi-unipi/info-screen": "3.0.0-alpha.45",
|
|
38
38
|
"@earendil-works/pi-agent-core": "^0.87.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|