@pi-unipi/unipi 2.18.1 → 2.19.0
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 +18 -0
- package/package.json +24 -24
- 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/btw/package.json +2 -2
- package/packages/compactor/package.json +3 -3
- package/packages/core/package.json +1 -1
- package/packages/footer/package.json +3 -3
- package/packages/fusion/README.md +5 -2
- package/packages/fusion/package.json +3 -3
- package/packages/fusion/src/sidekick-runtime.ts +62 -19
- package/packages/fusion/src/tools.ts +11 -3
- package/packages/image/package.json +2 -2
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/package.json +2 -2
- package/packages/memory/package.json +3 -3
- package/packages/milestone/package.json +2 -2
- package/packages/notify/README.md +21 -1
- package/packages/notify/activity.ts +21 -0
- package/packages/notify/events.ts +137 -14
- package/packages/notify/index.ts +4 -0
- package/packages/notify/package.json +2 -2
- package/packages/notify/settings.ts +31 -1
- package/packages/notify/skills/configure-notify/SKILL.md +29 -0
- package/packages/notify/tui/settings-overlay.ts +104 -3
- package/packages/notify/types.ts +12 -0
- package/packages/ralph/package.json +3 -3
- package/packages/subagents/package.json +2 -2
- package/packages/trajectory/package.json +1 -1
- package/packages/unipi/bundled.js +8980 -2715
- package/packages/updater/package.json +2 -2
- package/packages/utility/package.json +2 -2
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.19.0] — 2026-09-16
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `notify`: **unanswered blocking prompts are re-sent until someone acts** — `ask_user_prompt` and `permission_request` were fire-once, so a missed push left the agent parked with no further signal. New `renotify` config block (default on: `intervalMs: 120000`, `maxRepeats: 3`) re-dispatches the same title and message with a `(still waiting)` suffix at priority `high`; the loop is cancelled by any terminal keypress, `herdr:blocked` with `active: false`, `agent_start`, or session end, and re-arming replaces the pending timer (`armRenotify`/`disarmRenotify` in `events.ts`). New **Re-notify** tab in `/unipi:notify-settings` (Space toggles, `+`/`−` adjust interval and repeats). `maxRepeats: 0` sends the initial notification only.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `notify`: **blocking prompts now dispatch at `high` priority** (`ask_user_prompt`/`permission_request` → ntfy 5, Gotify 8, breaking through DND) while `agent_end`/`agent_settled` dispatch at `low`. `dispatchNotification` already accepted a priority — the event path just never passed one; an explicit `notify_user` priority still wins.
|
|
18
|
+
- `fusion`: **a sidekick handoff now stays open while its background tasks run.** `SidekickRuntime` counts in-flight `bg_run` calls (`openBgTasks`, only when `notifyOnCompletion`/`triggerOnCompletion` are not `false`), and on `agent_settled` with tasks still running it holds the handoff instead of collecting the report at an intermediate checkpoint. The background-task completion message decrements the count; with none left and the agent settled, a `settleGraceMs` timer (new spawn-config option, default 3000, `unref`'d) waits for the follow-up turn before requesting the last assistant text, and `agent_start` clears it.
|
|
19
|
+
- `fusion`: **a sidekick prompt rejected as "already processing" is retried once** as a pi `followUp` (`streamingBehavior: "followUp"`, guarded by `retriedPrompt`) instead of failing the handoff; a second rejection finishes it as an error. `PendingHandoff` is now resolve-only.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `notify`: **no duplicate "agent finished" notification while a background task is still running.** `agent_end`/`agent_settled` notified for the intermediate turn that launched a `triggerOnCompletion` task and again when the wake started its own turn, making the session look finished when it wasn't. The lifecycle handlers now skip dispatch while the background-tasks shared registry reports a running wake-pending task; the registry is read off the `Symbol.for("unipi.background-tasks.shared-registry")` global, so there is no dependency or load-order coupling (synchronous, never blocks the lifecycle hook).
|
|
24
|
+
- `notify`: **`silenceAfterInput` no longer mutes blocking prompts.** Submitting a prompt with a keypress could silence the permission request that arrived seconds later — precisely the notification that must not be missed. `ask_user_prompt`/`permission_request` now bypass the quiet window, threaded through the existing `eventType` argument (`isBlockingEvent` in `activity.ts`); no new config.
|
|
25
|
+
- `fusion`: **a failed sidekick handoff is reported as a structured error result.** `waitForReport` races the handoff rejection into an `{ error }` outcome and `sidekick`/`read_subagent` return `Handoff <id> failed: <message>` with `isError`, instead of the rejection escaping through the race.
|
|
26
|
+
|
|
9
27
|
## [2.18.1] — 2026-09-16
|
|
10
28
|
|
|
11
29
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/unipi",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "All-in-one extension suite for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -84,29 +84,29 @@
|
|
|
84
84
|
"typebox": "^1.1.38"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@pi-unipi/ask-user": "2.
|
|
88
|
-
"@pi-unipi/background-tasks": "2.
|
|
89
|
-
"@pi-unipi/btw": "2.
|
|
90
|
-
"@pi-unipi/command-enchantment": "2.
|
|
91
|
-
"@pi-unipi/compactor": "2.
|
|
92
|
-
"@pi-unipi/core": "2.
|
|
93
|
-
"@pi-unipi/footer": "2.
|
|
94
|
-
"@pi-unipi/image": "2.
|
|
95
|
-
"@pi-unipi/fusion": "2.
|
|
96
|
-
"@pi-unipi/info-screen": "2.
|
|
97
|
-
"@pi-unipi/input-shortcuts": "2.
|
|
98
|
-
"@pi-unipi/kanboard": "2.
|
|
99
|
-
"@pi-unipi/mcp": "2.
|
|
100
|
-
"@pi-unipi/memory": "2.
|
|
101
|
-
"@pi-unipi/milestone": "2.
|
|
102
|
-
"@pi-unipi/notify": "2.
|
|
103
|
-
"@pi-unipi/ralph": "2.
|
|
104
|
-
"@pi-unipi/subagents": "2.
|
|
105
|
-
"@pi-unipi/trajectory": "2.
|
|
106
|
-
"@pi-unipi/updater": "2.
|
|
107
|
-
"@pi-unipi/utility": "2.
|
|
108
|
-
"@pi-unipi/web-api": "2.
|
|
109
|
-
"@pi-unipi/workflow": "2.
|
|
87
|
+
"@pi-unipi/ask-user": "2.19.0",
|
|
88
|
+
"@pi-unipi/background-tasks": "2.19.0",
|
|
89
|
+
"@pi-unipi/btw": "2.19.0",
|
|
90
|
+
"@pi-unipi/command-enchantment": "2.19.0",
|
|
91
|
+
"@pi-unipi/compactor": "2.19.0",
|
|
92
|
+
"@pi-unipi/core": "2.19.0",
|
|
93
|
+
"@pi-unipi/footer": "2.19.0",
|
|
94
|
+
"@pi-unipi/image": "2.19.0",
|
|
95
|
+
"@pi-unipi/fusion": "2.19.0",
|
|
96
|
+
"@pi-unipi/info-screen": "2.19.0",
|
|
97
|
+
"@pi-unipi/input-shortcuts": "2.19.0",
|
|
98
|
+
"@pi-unipi/kanboard": "2.19.0",
|
|
99
|
+
"@pi-unipi/mcp": "2.19.0",
|
|
100
|
+
"@pi-unipi/memory": "2.19.0",
|
|
101
|
+
"@pi-unipi/milestone": "2.19.0",
|
|
102
|
+
"@pi-unipi/notify": "2.19.0",
|
|
103
|
+
"@pi-unipi/ralph": "2.19.0",
|
|
104
|
+
"@pi-unipi/subagents": "2.19.0",
|
|
105
|
+
"@pi-unipi/trajectory": "2.19.0",
|
|
106
|
+
"@pi-unipi/updater": "2.19.0",
|
|
107
|
+
"@pi-unipi/utility": "2.19.0",
|
|
108
|
+
"@pi-unipi/web-api": "2.19.0",
|
|
109
|
+
"@pi-unipi/workflow": "2.19.0"
|
|
110
110
|
},
|
|
111
111
|
"devDependencies": {
|
|
112
112
|
"@earendil-works/pi-agent-core": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/ask-user",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@pi-unipi/core": "2.
|
|
43
|
+
"@pi-unipi/core": "2.19.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/background-tasks",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
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.84.0",
|
|
13
|
-
"@pi-unipi/core": "2.
|
|
13
|
+
"@pi-unipi/core": "2.19.0",
|
|
14
14
|
"turndown": "^7.2.4"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/btw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
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.84.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@pi-unipi/core": "2.
|
|
40
|
+
"@pi-unipi/core": "2.19.0"
|
|
41
41
|
},
|
|
42
42
|
"pi": {
|
|
43
43
|
"extensions": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/compactor",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "Context engine for Pi — zero-LLM compaction, session continuity, sandbox execution, and tool display optimization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pi-unipi/core": "2.
|
|
38
|
-
"@pi-unipi/info-screen": "2.
|
|
37
|
+
"@pi-unipi/core": "2.19.0",
|
|
38
|
+
"@pi-unipi/info-screen": "2.19.0",
|
|
39
39
|
"@earendil-works/pi-agent-core": "^0.84.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/footer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@pi-unipi/core": "2.
|
|
36
|
-
"@pi-unipi/background-tasks": "2.
|
|
35
|
+
"@pi-unipi/core": "2.19.0",
|
|
36
|
+
"@pi-unipi/background-tasks": "2.19.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -92,8 +92,11 @@ persist across handoffs. The sidekick's context compacts independently of the
|
|
|
92
92
|
lead's (it is its own pi session). `sidekick({message, block:true})` waits by
|
|
93
93
|
default; `block:false` returns immediately and delivers a
|
|
94
94
|
`<subagent_completion_notification>`. Calling it while busy steers the same
|
|
95
|
-
handoff.
|
|
96
|
-
|
|
95
|
+
handoff. Background tasks keep that handoff open through their completion
|
|
96
|
+
notification and any follow-up turn, so the report is not released at an
|
|
97
|
+
intermediate checkpoint. If a prompt arrives while the child is processing,
|
|
98
|
+
Fusion retries it once with pi's `followUp` streaming behavior.
|
|
99
|
+
`read_subagent({agent_id?, block?, timeout?})` reads or waits for a handoff.
|
|
97
100
|
|
|
98
101
|
The child receives `UNIPI_FUSION_CHILD=1` and `UNIPI_SUBAGENT_CHILD=1`; the
|
|
99
102
|
Fusion extension guard prevents child processes from registering Fusion tools,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/fusion",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "Devin-style model picker, fusion presets (lead + sidekick), and Local Fusion runtime for UniPi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@pi-unipi/core": "2.
|
|
36
|
-
"@pi-unipi/subagents": "2.
|
|
35
|
+
"@pi-unipi/core": "2.19.0",
|
|
36
|
+
"@pi-unipi/subagents": "2.19.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-ai": "^0.84.0",
|
|
@@ -15,6 +15,7 @@ export interface SidekickSpawnConfig {
|
|
|
15
15
|
spawn?: typeof defaultSpawn;
|
|
16
16
|
command?: { command: string; args: string[] };
|
|
17
17
|
onProgress?: () => void;
|
|
18
|
+
settleGraceMs?: number;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface SidekickUsage {
|
|
@@ -54,11 +55,15 @@ export interface HandoffReport {
|
|
|
54
55
|
|
|
55
56
|
interface PendingHandoff {
|
|
56
57
|
id: string;
|
|
58
|
+
message: string;
|
|
57
59
|
startedAt: number;
|
|
58
60
|
usage: SidekickUsage;
|
|
59
61
|
progress: HandoffProgress;
|
|
62
|
+
retriedPrompt: boolean;
|
|
63
|
+
openBgTasks: number;
|
|
64
|
+
settled: boolean;
|
|
65
|
+
settleTimer?: NodeJS.Timeout;
|
|
60
66
|
resolve: (report: HandoffReport) => void;
|
|
61
|
-
reject: (error: Error) => void;
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
const emptyUsage = (): SidekickUsage => ({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
|
|
@@ -193,16 +198,35 @@ export class SidekickRuntime {
|
|
|
193
198
|
}
|
|
194
199
|
}
|
|
195
200
|
|
|
201
|
+
private requestLastAssistantText(): void {
|
|
202
|
+
const current = this.pending;
|
|
203
|
+
if (current === undefined) return;
|
|
204
|
+
this.responseText = (text) => this.finish(this.abortRequested ? "aborted" : this.pendingError === undefined ? "completed" : "error", text, this.pendingError);
|
|
205
|
+
this.responseError = (error) => this.finish("error", undefined, error.message);
|
|
206
|
+
try {
|
|
207
|
+
this.send({ type: "get_last_assistant_text" });
|
|
208
|
+
} catch (error) {
|
|
209
|
+
this.finish("error", undefined, error instanceof Error ? error.message : String(error));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
196
213
|
private handleMessage(message: Record<string, unknown>): void {
|
|
197
214
|
if (message.type === "response") {
|
|
198
215
|
const command = message.command;
|
|
199
216
|
if (command === "prompt" && message.success === false) {
|
|
200
|
-
const
|
|
217
|
+
const errorText = String(message.error ?? "Sidekick prompt rejected");
|
|
201
218
|
const current = this.pending;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
219
|
+
if (current === undefined) return;
|
|
220
|
+
if (/already processing/i.test(errorText) && !current.retriedPrompt) {
|
|
221
|
+
current.retriedPrompt = true;
|
|
222
|
+
try {
|
|
223
|
+
this.send({ id: current.id, type: "prompt", message: current.message, streamingBehavior: "followUp" });
|
|
224
|
+
} catch (error) {
|
|
225
|
+
this.finish("error", undefined, error instanceof Error ? error.message : String(error));
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
this.finish("error", undefined, errorText);
|
|
229
|
+
}
|
|
206
230
|
} else if (command === "get_last_assistant_text") {
|
|
207
231
|
const data = message.data as Record<string, unknown> | undefined;
|
|
208
232
|
const text = typeof data?.text === "string" ? data.text : this.pending?.progress.textTail ?? "";
|
|
@@ -224,6 +248,7 @@ export class SidekickRuntime {
|
|
|
224
248
|
this.closeOpenText();
|
|
225
249
|
this.pending.progress.toolCalls += 1;
|
|
226
250
|
const args = message.args !== undefined && typeof message.args === "object" && message.args !== null ? message.args as Record<string, unknown> : undefined;
|
|
251
|
+
if (message.toolName === "bg_run" && args?.notifyOnCompletion !== false && args?.triggerOnCompletion !== false) this.pending.openBgTasks += 1;
|
|
227
252
|
const argsText = args === undefined ? "" : JSON.stringify(args).replace(/\s+/gu, " ");
|
|
228
253
|
const summary = `${String(message.toolName ?? "tool")}(${argsText})`.slice(0, 40);
|
|
229
254
|
this.pending.progress.recentTools = [...this.pending.progress.recentTools, summary].slice(-6);
|
|
@@ -237,6 +262,7 @@ export class SidekickRuntime {
|
|
|
237
262
|
event.endedAt = Date.now();
|
|
238
263
|
event.isError = message.isError === true;
|
|
239
264
|
event.output = this.toolOutput(message.result);
|
|
265
|
+
if (event.name === "bg_run" && event.isError) this.pending.openBgTasks = Math.max(0, this.pending.openBgTasks - 1);
|
|
240
266
|
}
|
|
241
267
|
this.notifyProgress();
|
|
242
268
|
} else if (message.type === "message_update") {
|
|
@@ -251,7 +277,19 @@ export class SidekickRuntime {
|
|
|
251
277
|
}
|
|
252
278
|
} else if (message.type === "message_end") {
|
|
253
279
|
const msg = message.message as Record<string, unknown> | undefined;
|
|
254
|
-
if (msg?.role === "
|
|
280
|
+
if (msg?.role === "custom" && msg.customType === "background-task-notification") {
|
|
281
|
+
this.pending.openBgTasks = Math.max(0, this.pending.openBgTasks - 1);
|
|
282
|
+
if (this.pending.openBgTasks === 0 && this.pending.settled) {
|
|
283
|
+
clearTimeout(this.pending.settleTimer);
|
|
284
|
+
this.pending.settleTimer = setTimeout(() => {
|
|
285
|
+
const current = this.pending;
|
|
286
|
+
if (current === undefined || !current.settled || current.openBgTasks > 0) return;
|
|
287
|
+
this.requestLastAssistantText();
|
|
288
|
+
}, this.cfg.settleGraceMs ?? 3000);
|
|
289
|
+
this.pending.settleTimer.unref();
|
|
290
|
+
}
|
|
291
|
+
this.notifyProgress();
|
|
292
|
+
} else if (msg?.role === "assistant") {
|
|
255
293
|
this.closeOpenText();
|
|
256
294
|
this.notifyProgress();
|
|
257
295
|
if (msg.stopReason === "error" && typeof msg.errorMessage === "string") this.pendingError = msg.errorMessage;
|
|
@@ -267,16 +305,18 @@ export class SidekickRuntime {
|
|
|
267
305
|
});
|
|
268
306
|
}
|
|
269
307
|
}
|
|
308
|
+
} else if (message.type === "agent_start") {
|
|
309
|
+
clearTimeout(this.pending.settleTimer);
|
|
310
|
+
this.pending.settleTimer = undefined;
|
|
311
|
+
this.pending.settled = false;
|
|
312
|
+
this.notifyProgress();
|
|
270
313
|
} else if (message.type === "agent_settled") {
|
|
271
|
-
this.
|
|
272
|
-
this.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
this.send({ type: "get_last_assistant_text" });
|
|
277
|
-
} catch (error) {
|
|
278
|
-
this.finish("error", undefined, error instanceof Error ? error.message : String(error));
|
|
314
|
+
this.pending.settled = true;
|
|
315
|
+
if (this.pending.openBgTasks > 0) {
|
|
316
|
+
this.notifyProgress();
|
|
317
|
+
return;
|
|
279
318
|
}
|
|
319
|
+
this.requestLastAssistantText();
|
|
280
320
|
}
|
|
281
321
|
}
|
|
282
322
|
|
|
@@ -293,6 +333,8 @@ export class SidekickRuntime {
|
|
|
293
333
|
private finish(status: HandoffReport["status"], text?: string, error?: string): void {
|
|
294
334
|
const current = this.pending;
|
|
295
335
|
if (current === undefined) return;
|
|
336
|
+
clearTimeout(current.settleTimer);
|
|
337
|
+
current.settleTimer = undefined;
|
|
296
338
|
this.pending = undefined;
|
|
297
339
|
this.responseText = undefined;
|
|
298
340
|
this.responseError = undefined;
|
|
@@ -323,19 +365,20 @@ export class SidekickRuntime {
|
|
|
323
365
|
const id = randomUUID();
|
|
324
366
|
const startedAt = Date.now();
|
|
325
367
|
let resolve!: (report: HandoffReport) => void;
|
|
326
|
-
|
|
327
|
-
const done = new Promise<HandoffReport>((res, rej) => {
|
|
368
|
+
const done = new Promise<HandoffReport>((res) => {
|
|
328
369
|
resolve = res;
|
|
329
|
-
reject = rej;
|
|
330
370
|
});
|
|
331
371
|
this.pendingError = undefined;
|
|
332
372
|
this.pending = {
|
|
333
373
|
id,
|
|
374
|
+
message,
|
|
334
375
|
startedAt,
|
|
335
376
|
usage: emptyUsage(),
|
|
336
377
|
progress: { toolCalls: 0, recentTools: [], textTail: "", startedAt, events: [], droppedEvents: 0 },
|
|
378
|
+
retriedPrompt: false,
|
|
379
|
+
openBgTasks: 0,
|
|
380
|
+
settled: false,
|
|
337
381
|
resolve,
|
|
338
|
-
reject,
|
|
339
382
|
};
|
|
340
383
|
this.latestHandoff = { id, done };
|
|
341
384
|
try {
|
|
@@ -60,7 +60,7 @@ async function waitForReport(
|
|
|
60
60
|
ctx: ExtensionContext,
|
|
61
61
|
onUpdate?: (update: unknown) => void,
|
|
62
62
|
timeoutMs = 2700000,
|
|
63
|
-
): Promise<{ report?: HandoffReport; interrupted?: string; aborted?: boolean }> {
|
|
63
|
+
): Promise<{ report?: HandoffReport; interrupted?: string; aborted?: boolean; error?: string }> {
|
|
64
64
|
const started = Date.now();
|
|
65
65
|
let lastProgressKey = "";
|
|
66
66
|
while (true) {
|
|
@@ -72,8 +72,14 @@ async function waitForReport(
|
|
|
72
72
|
const remaining = timeoutMs - (Date.now() - started);
|
|
73
73
|
if (remaining <= 0) return {};
|
|
74
74
|
const timer = new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), Math.min(500, remaining)));
|
|
75
|
-
const
|
|
76
|
-
|
|
75
|
+
const outcome = await Promise.race([
|
|
76
|
+
done.then((report) => ({ report }), (error) => ({ error: error instanceof Error ? error.message : String(error) })),
|
|
77
|
+
timer,
|
|
78
|
+
]);
|
|
79
|
+
if (outcome !== undefined) {
|
|
80
|
+
if ("error" in outcome) return { error: outcome.error };
|
|
81
|
+
return { report: outcome.report };
|
|
82
|
+
}
|
|
77
83
|
const progress = progressText(runtime, id);
|
|
78
84
|
const key = progressKey(runtime, id);
|
|
79
85
|
if (key !== lastProgressKey) {
|
|
@@ -175,6 +181,7 @@ export function registerFusionTools(pi: ExtensionAPI, deps: FusionToolDeps): voi
|
|
|
175
181
|
deps.onReport?.(ctx, waited.report);
|
|
176
182
|
return result(reportText(waited.report), waited.report, waited.report.status !== "completed");
|
|
177
183
|
}
|
|
184
|
+
if (waited.error) return result(`Handoff ${handoff.id} failed: ${waited.error}`, undefined, true);
|
|
178
185
|
if (waited.aborted) return result(`${progressText(runtime, handoff.id)}\nHandoff ${handoff.id} aborted.`, undefined, true);
|
|
179
186
|
if (waited.interrupted) return result(`A user message arrived while the sidekick (agent_id ${handoff.id}) was working. The handoff continues in the background. Act on the user's message first, then call read_subagent({agent_id:"${handoff.id}", block:true}) to collect the report or sidekick({message}) to redirect it.\n${waited.interrupted}`);
|
|
180
187
|
return result(`Handoff ${handoff.id} is still running.\n${progressText(runtime, handoff.id)}`);
|
|
@@ -205,6 +212,7 @@ export function registerFusionTools(pi: ExtensionAPI, deps: FusionToolDeps): voi
|
|
|
205
212
|
deps.onReport?.(ctx, waited.report);
|
|
206
213
|
return result(reportText(waited.report), waited.report, waited.report.status !== "completed");
|
|
207
214
|
}
|
|
215
|
+
if (waited.error) return result(`Handoff ${id} failed: ${waited.error}`, undefined, true);
|
|
208
216
|
if (waited.aborted) return result(`Handoff ${id} aborted.`, undefined, true);
|
|
209
217
|
if (waited.interrupted) return result(`A user message arrived while the sidekick (agent_id ${id}) was working.\n${waited.interrupted}`);
|
|
210
218
|
return result(`Handoff ${id} is still running.\n${progressText(runtime, id)}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/image",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "Image generation and image recognition tools for the Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pi-unipi/core": "2.
|
|
37
|
+
"@pi-unipi/core": "2.19.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@earendil-works/pi-ai": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/info-screen",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "Dashboard and module registry for Unipi — configurable info overlay with tabbed groups",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@pi-unipi/core": "2.
|
|
36
|
+
"@pi-unipi/core": "2.19.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/input-shortcuts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "Keyboard shortcuts for stash/restore, undo/redo, clipboard, and thinking toggle — chord-based overlay system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@pi-unipi/core": "2.
|
|
36
|
+
"@pi-unipi/core": "2.19.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/kanboard",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "Visualization layer for unipi workflow — HTTP server with htmx/Alpine.js UI, modular parsers, TUI overlay, and kanban board",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@pi-unipi/core": "2.
|
|
42
|
+
"@pi-unipi/core": "2.19.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "MCP server management extension for Pi coding agent — browse, add, configure, and use MCP servers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"test": "npx tsx --test tests/**/*.test.ts"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@pi-unipi/core": "2.
|
|
33
|
+
"@pi-unipi/core": "2.19.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/memory",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "Persistent cross-session memory with MemPalace backend (auto-installed) and SQLite fallback for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"README.md"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@pi-unipi/core": "2.
|
|
43
|
-
"@pi-unipi/info-screen": "2.
|
|
42
|
+
"@pi-unipi/core": "2.19.0",
|
|
43
|
+
"@pi-unipi/info-screen": "2.19.0",
|
|
44
44
|
"js-yaml": "^4.1.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/milestone",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "Lifecycle layer for project-level goals — MILESTONES.md tracking, session hooks, auto-sync",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@pi-unipi/core": "2.
|
|
35
|
+
"@pi-unipi/core": "2.19.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -32,6 +32,8 @@ Notify subscribes to Pi lifecycle events and routes notifications based on your
|
|
|
32
32
|
| `ask_user_prompt` | Off | Agent asked a question and is waiting for an answer |
|
|
33
33
|
| `permission_request` | Off | A permission prompt is about to be shown (requires [`@gotgenes/pi-permission-system`](https://www.npmjs.com/package/@gotgenes/pi-permission-system)) |
|
|
34
34
|
|
|
35
|
+
`ask_user_prompt` and `permission_request` are **blocking** events: while one is unanswered the agent is parked, so notify re-sends it periodically (see [Re-notify unanswered prompts](#re-notify-unanswered-prompts)).
|
|
36
|
+
|
|
35
37
|
Notify registers with the info-screen dashboard, showing enabled platforms and last notification time. The footer subscribes to `NOTIFICATION_SENT` events to display notification stats.
|
|
36
38
|
|
|
37
39
|
## Agent Tool
|
|
@@ -75,7 +77,25 @@ After a terminal keypress, listed platforms stay quiet for `windowMs`. Default:
|
|
|
75
77
|
}
|
|
76
78
|
```
|
|
77
79
|
|
|
78
|
-
Add `gotify`, `telegram`, or `ntfy` to `platforms` to silence those channels too. Empty `platforms` silences all enabled platforms (same as `events.*.platforms`).
|
|
80
|
+
Add `gotify`, `telegram`, or `ntfy` to `platforms` to silence those channels too. Empty `platforms` silences all enabled platforms (same as `events.*.platforms`). Blocking events (`ask_user_prompt`, `permission_request`) are never silenced — see below.
|
|
81
|
+
|
|
82
|
+
### Re-notify unanswered prompts
|
|
83
|
+
|
|
84
|
+
When a blocking prompt (`ask_user_prompt`, `permission_request`) is not answered, notify re-sends the same notification every `intervalMs`, with the title suffixed `(still waiting)` and priority `high`, up to `maxRepeats` times. Default: **on**, every 2 minutes, 3 repeats. This is the one notify case where missing the push leaves the agent parked indefinitely.
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"renotify": {
|
|
89
|
+
"enabled": true,
|
|
90
|
+
"intervalMs": 120000,
|
|
91
|
+
"maxRepeats": 3
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`maxRepeats: 0` sends the initial notification only. Reminders stop as soon as any of these fires: the user presses a key, herdr reports `herdr:blocked` `active: false`, the agent starts a new turn (`agent_start`), or the session ends. Only one prompt can be outstanding at a time — arming a new one replaces the previous reminder. Reminders bypass `silenceAfterInput` because blocking events are exempt from it.
|
|
97
|
+
|
|
98
|
+
Edit in `/unipi:notify-settings` → Re-notify, or in `~/.unipi/config/notify/config.json`.
|
|
79
99
|
|
|
80
100
|
### Gotify
|
|
81
101
|
|
|
@@ -19,6 +19,20 @@ const VALID_PLATFORMS: ReadonlySet<NotifyPlatform> = new Set([
|
|
|
19
19
|
"ntfy",
|
|
20
20
|
]);
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Events where a human is blocking the session. They arrive seconds after the
|
|
24
|
+
* keypress that caused them, so recent input must never mute them.
|
|
25
|
+
*/
|
|
26
|
+
const BLOCKING_EVENTS: ReadonlySet<string> = new Set([
|
|
27
|
+
"ask_user_prompt",
|
|
28
|
+
"permission_request",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/** Whether an event type is a human-blocking prompt. */
|
|
32
|
+
export function isBlockingEvent(eventType: string | undefined): boolean {
|
|
33
|
+
return eventType !== undefined && BLOCKING_EVENTS.has(eventType);
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
let lastInputAt = 0;
|
|
23
37
|
|
|
24
38
|
/** Record a terminal keypress. `at` is injectable for tests. */
|
|
@@ -36,12 +50,19 @@ export function resetInputActivity(): void {
|
|
|
36
50
|
* Does not look at platform `enabled` flags — caller filters those first.
|
|
37
51
|
* Empty `platforms` (while enabled) silences all incoming channels, matching
|
|
38
52
|
* `events.*.platforms: []` → all enabled.
|
|
53
|
+
*
|
|
54
|
+
* Human-blocking events (`ask_user_prompt`, `permission_request`) bypass the
|
|
55
|
+
* filter entirely — the keypress that triggered them must not mute them.
|
|
39
56
|
*/
|
|
40
57
|
export function filterPlatformsAfterInput(
|
|
41
58
|
platforms: NotifyPlatform[],
|
|
42
59
|
config: Pick<NotifyConfig, "silenceAfterInput">,
|
|
43
60
|
now: number = Date.now(),
|
|
61
|
+
eventType?: string,
|
|
44
62
|
): { send: NotifyPlatform[]; silenced: NotifyPlatform[] } {
|
|
63
|
+
if (isBlockingEvent(eventType)) {
|
|
64
|
+
return { send: platforms.slice(), silenced: [] };
|
|
65
|
+
}
|
|
45
66
|
const cfg = config.silenceAfterInput;
|
|
46
67
|
if (!shouldSilence(cfg, now)) {
|
|
47
68
|
return { send: platforms.slice(), silenced: [] };
|