@pi-unipi/unipi 3.0.0-alpha.19 → 3.0.0-alpha.20
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 +19 -0
- package/package.json +21 -21
- 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/compactor/src/commands/index.ts +7 -2
- package/packages/core/harness-messages.ts +392 -0
- package/packages/core/index.ts +1 -0
- package/packages/core/package.json +1 -1
- package/packages/footer/package.json +3 -3
- package/packages/fusion/package.json +4 -4
- package/packages/fusion/src/index.ts +22 -4
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/package.json +7 -7
- package/packages/kanboard/skills/kanboard/SKILL.md +1 -1
- package/packages/kanboard/src/commands.ts +21 -6
- package/packages/kanboard/src/monitor.ts +3 -1
- package/packages/kanboard/src/reminders.ts +11 -2
- package/packages/long-horizon/index.ts +27 -8
- package/packages/long-horizon/package.json +2 -2
- package/packages/long-horizon/src/commands.ts +6 -2
- package/packages/long-horizon/src/engine/nudge-stash.ts +19 -0
- package/packages/long-horizon/src/runtime.ts +7 -2
- package/packages/mcp/package.json +2 -2
- package/packages/memory/commands.ts +7 -2
- package/packages/memory/index.ts +15 -6
- package/packages/memory/package.json +3 -3
- package/packages/memory/tools.ts +13 -0
- package/packages/notify/package.json +2 -2
- package/packages/skill-registry/index.ts +10 -3
- package/packages/skill-registry/package.json +2 -2
- package/packages/subagents/package.json +3 -3
- package/packages/unipi/bundled.js +1256 -396
- package/packages/unipi/index.ts +13 -3
- package/packages/updater/package.json +2 -2
- package/packages/utility/package.json +2 -2
- package/packages/utility/src/commands.ts +7 -2
- package/packages/utility/src/index.ts +12 -0
- package/packages/utility/src/render/harness.ts +683 -0
- package/packages/utility/src/render/simple.ts +12 -3
- package/packages/utility/src/render/tools.ts +20 -3
- package/packages/watchdog/index.ts +14 -2
- package/packages/watchdog/package.json +3 -3
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/package.json +2 -2
- package/packages/workflow/src/plan/index.ts +12 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,25 @@ 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.20] — 2026-10-02
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `kanboard` (board UI): **Dashboard** command center — greeting header with waiting/agents/ready/done-7d stats and a 14-day throughput spark; a per-project "Needs you" inbox (Approve / Rework / Answer inline, j/k/Enter, 3 rows per project + 10 overall, `+N more` / `Show fewer`, Expand all / Collapse all, oldest-wait on collapsed groups); project cards; live agents; up next; and an activity timeline that folds a task's quick steps into one trail. Expand/collapse animate. Backed by `GET /api/dashboard`. (UNI-67)
|
|
13
|
+
- `kanboard`: archive projects from the sidebar's right-click menu into an **Archived** section. (UNI-51)
|
|
14
|
+
- `kanboard`: labels — create or pick labels in the task form; each label gets a stable hash colour. (UNI-60)
|
|
15
|
+
- `kanboard`: task owner — tasks record "Created by agent / user" automatically; read-only. (UNI-59)
|
|
16
|
+
- `kanboard`: undo toast for moves that are hard to reverse (e.g. dragging to Cancelled, or In Review → Done); cancelled tasks stay final and offer **Recreate task** (right-click), which opens Add Task prefilled with the add-task fields only. (UNI-57)
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `kanboard` CLI: bare `list` shows only the active lanes (todo, in progress, blocked, in review, done); `--all` adds backlog, cancelled and archived. (UNI-62)
|
|
20
|
+
- `kanboard`: a user can move a Blocked task straight to Done. (UNI-63)
|
|
21
|
+
- `kanboard` (board UI): clicking outside the create/edit task form no longer closes it and discards input. (UNI-61)
|
|
22
|
+
- render: harness-origin messages (reminders, nudges, kickoffs) are styled as source-labelled violet/slate panels in every render style, while the model-facing text is unchanged. (UNI-53)
|
|
23
|
+
- `memory`: the save-pass card shows humanized token counts with explicit cache read/write labels. (UNI-56)
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- `kanboard`: titles containing `"` no longer gain an extra backslash on every save. (UNI-58)
|
|
27
|
+
|
|
9
28
|
## [3.0.0-alpha.19] — 2026-10-01
|
|
10
29
|
|
|
11
30
|
### 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.20",
|
|
4
4
|
"description": "All-in-one extension suite for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -82,26 +82,26 @@
|
|
|
82
82
|
"typebox": "^1.1.38"
|
|
83
83
|
},
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"@pi-unipi/ask-user": "3.0.0-alpha.
|
|
86
|
-
"@pi-unipi/background-tasks": "3.0.0-alpha.
|
|
87
|
-
"@pi-unipi/btw": "3.0.0-alpha.
|
|
88
|
-
"@pi-unipi/command-enchantment": "3.0.0-alpha.
|
|
89
|
-
"@pi-unipi/compactor": "3.0.0-alpha.
|
|
90
|
-
"@pi-unipi/core": "3.0.0-alpha.
|
|
91
|
-
"@pi-unipi/footer": "3.0.0-alpha.
|
|
92
|
-
"@pi-unipi/fusion": "3.0.0-alpha.
|
|
93
|
-
"@pi-unipi/info-screen": "3.0.0-alpha.
|
|
94
|
-
"@pi-unipi/input-shortcuts": "3.0.0-alpha.
|
|
95
|
-
"@pi-unipi/kanboard": "3.0.0-alpha.
|
|
96
|
-
"@pi-unipi/mcp": "3.0.0-alpha.
|
|
97
|
-
"@pi-unipi/memory": "3.0.0-alpha.
|
|
98
|
-
"@pi-unipi/notify": "3.0.0-alpha.
|
|
99
|
-
"@pi-unipi/skill-registry": "3.0.0-alpha.
|
|
100
|
-
"@pi-unipi/subagents": "3.0.0-alpha.
|
|
101
|
-
"@pi-unipi/updater": "3.0.0-alpha.
|
|
102
|
-
"@pi-unipi/utility": "3.0.0-alpha.
|
|
103
|
-
"@pi-unipi/web-api": "3.0.0-alpha.
|
|
104
|
-
"@pi-unipi/workflow": "3.0.0-alpha.
|
|
85
|
+
"@pi-unipi/ask-user": "3.0.0-alpha.20",
|
|
86
|
+
"@pi-unipi/background-tasks": "3.0.0-alpha.20",
|
|
87
|
+
"@pi-unipi/btw": "3.0.0-alpha.20",
|
|
88
|
+
"@pi-unipi/command-enchantment": "3.0.0-alpha.20",
|
|
89
|
+
"@pi-unipi/compactor": "3.0.0-alpha.20",
|
|
90
|
+
"@pi-unipi/core": "3.0.0-alpha.20",
|
|
91
|
+
"@pi-unipi/footer": "3.0.0-alpha.20",
|
|
92
|
+
"@pi-unipi/fusion": "3.0.0-alpha.20",
|
|
93
|
+
"@pi-unipi/info-screen": "3.0.0-alpha.20",
|
|
94
|
+
"@pi-unipi/input-shortcuts": "3.0.0-alpha.20",
|
|
95
|
+
"@pi-unipi/kanboard": "3.0.0-alpha.20",
|
|
96
|
+
"@pi-unipi/mcp": "3.0.0-alpha.20",
|
|
97
|
+
"@pi-unipi/memory": "3.0.0-alpha.20",
|
|
98
|
+
"@pi-unipi/notify": "3.0.0-alpha.20",
|
|
99
|
+
"@pi-unipi/skill-registry": "3.0.0-alpha.20",
|
|
100
|
+
"@pi-unipi/subagents": "3.0.0-alpha.20",
|
|
101
|
+
"@pi-unipi/updater": "3.0.0-alpha.20",
|
|
102
|
+
"@pi-unipi/utility": "3.0.0-alpha.20",
|
|
103
|
+
"@pi-unipi/web-api": "3.0.0-alpha.20",
|
|
104
|
+
"@pi-unipi/workflow": "3.0.0-alpha.20"
|
|
105
105
|
},
|
|
106
106
|
"devDependencies": {
|
|
107
107
|
"@earendil-works/pi-agent-core": "^0.87.1",
|
|
@@ -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.20",
|
|
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": "3.0.0-alpha.
|
|
43
|
+
"@pi-unipi/core": "3.0.0-alpha.20"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@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.20",
|
|
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.20",
|
|
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": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.20",
|
|
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.20"
|
|
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.20",
|
|
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.20",
|
|
37
|
+
"@pi-unipi/info-screen": "3.0.0-alpha.20",
|
|
38
38
|
"@earendil-works/pi-agent-core": "^0.87.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { COMPACTOR_INSTRUCTION, formatTokens } from "@pi-unipi/core";
|
|
11
|
+
import { COMPACTOR_INSTRUCTION, formatTokens, harnessMetadata } from "@pi-unipi/core";
|
|
12
12
|
import { loadConfig } from "../config/manager.js";
|
|
13
13
|
import { setPendingCompaction } from "../compaction/hooks.js";
|
|
14
14
|
import { vccRecall } from "../tools/vcc-recall.js";
|
|
@@ -67,7 +67,12 @@ export function registerCommands(pi: ExtensionAPI): void {
|
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
const result = vccRecall(blocks, { query, scope: parsed.scope, page });
|
|
70
|
-
pi.sendMessage({
|
|
70
|
+
pi.sendMessage({
|
|
71
|
+
customType: "compactor-recall",
|
|
72
|
+
content: result.text,
|
|
73
|
+
display: true,
|
|
74
|
+
details: { unipiHarness: harnessMetadata({ source: "Compactor", title: "Session recall", synopsis: "Results from /unipi:session-recall" }, "direct") },
|
|
75
|
+
}, { triggerTurn: true });
|
|
71
76
|
};
|
|
72
77
|
pi.registerCommand("unipi:session-recall", {
|
|
73
78
|
description: "Recall earlier parts of this session. Plain keywords work best; add scope:all to reach edited or retried turns.",
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unipi/core — harness message provenance.
|
|
3
|
+
*
|
|
4
|
+
* Harness-generated text that reaches the model as role USER (direct
|
|
5
|
+
* sendUserMessage) is annotated with a namespaced `unipiHarness` metadata field
|
|
6
|
+
* so the UI can style it — model roles, text, delivery and admission behavior
|
|
7
|
+
* stay untouched.
|
|
8
|
+
*
|
|
9
|
+
* Lifecycle (single-call synchronous arm — the SDK runs `input` handlers
|
|
10
|
+
* synchronously inside sendUserMessage):
|
|
11
|
+
* 1. `sendHarnessUserMessage` ensures the observers are installed, pushes a
|
|
12
|
+
* single-call ARM, calls the unchanged `pi.sendUserMessage`, and pops the
|
|
13
|
+
* arm in `finally` — nothing lingers across calls.
|
|
14
|
+
* 2. The input handler run DURING that call confirms the arm (source
|
|
15
|
+
* `extension`, exact text). Human/rpc inputs and extension inputs without
|
|
16
|
+
* an active arm become human/unattributed admission records.
|
|
17
|
+
* 3. Idle (non-queued) harness records are dispatch-eligible only after
|
|
18
|
+
* `before_agent_start` carries the exact prompt (auth/model passed).
|
|
19
|
+
* Queued records are eligible from their confirmed delivery while active.
|
|
20
|
+
* 4. `message_start` selects among ELIGIBLE harness records; human/
|
|
21
|
+
* unattributed records participate in conflict detection but never label.
|
|
22
|
+
* Identical texts with conflicting origins — across deliveries — fail
|
|
23
|
+
* CLOSED and clear the whole matching group. The selected message is
|
|
24
|
+
* marked processed; `message_end` never re-selects.
|
|
25
|
+
* 5. `message_end` attaches `unipiHarness` only when the finalized text still
|
|
26
|
+
* equals the selected text (later transforms fail closed). SessionManager
|
|
27
|
+
* persists the whole object, so provenance survives resume/branch.
|
|
28
|
+
*
|
|
29
|
+
* Irreducible SDK gap: input events carry no per-send ID. The next input
|
|
30
|
+
* purges ALL unmatched direct records (harness + human/rpc) and terminal
|
|
31
|
+
* boundaries purge everything outstanding — the failure mode is always native
|
|
32
|
+
* fallback, never a human message mislabelled.
|
|
33
|
+
*/
|
|
34
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
35
|
+
|
|
36
|
+
export type HarnessDelivery =
|
|
37
|
+
| "direct"
|
|
38
|
+
| "steer"
|
|
39
|
+
| "followUp"
|
|
40
|
+
| "nextTurn"
|
|
41
|
+
| "boundary"
|
|
42
|
+
| "before_agent_start";
|
|
43
|
+
|
|
44
|
+
export interface HarnessMessageMeta {
|
|
45
|
+
version: 1;
|
|
46
|
+
id: string;
|
|
47
|
+
source: string;
|
|
48
|
+
title: string;
|
|
49
|
+
synopsis?: string;
|
|
50
|
+
delivery: HarnessDelivery;
|
|
51
|
+
severity?: "warning";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type HarnessMessageMetaInput = Omit<HarnessMessageMeta, "version" | "id" | "delivery">;
|
|
55
|
+
|
|
56
|
+
/** Canonical per-API state, carried on the API ROOT via a Symbol.for property.
|
|
57
|
+
* Proxies (withCommandEcho, the umbrella registerTool proxy) forward unknown
|
|
58
|
+
* property gets/sets to their target, so every wrapper observes one state. */
|
|
59
|
+
const STATE_SYM = Symbol.for("unipi.harnessMessages.state");
|
|
60
|
+
|
|
61
|
+
interface HarnessToken {
|
|
62
|
+
kind: "harness";
|
|
63
|
+
origin: string;
|
|
64
|
+
title: string;
|
|
65
|
+
synopsis?: string;
|
|
66
|
+
severity?: "warning";
|
|
67
|
+
text: string;
|
|
68
|
+
delivery: "direct" | "steer" | "followUp";
|
|
69
|
+
/** Direct records become eligible only at before_agent_start. */
|
|
70
|
+
eligible: boolean;
|
|
71
|
+
state: "admitted" | "consumed";
|
|
72
|
+
seq: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface HarnessHumanRecord {
|
|
76
|
+
kind: "human" | "unattributed";
|
|
77
|
+
origin: string;
|
|
78
|
+
text: string;
|
|
79
|
+
delivery: "direct" | "steer" | "followUp";
|
|
80
|
+
state: "admitted" | "consumed";
|
|
81
|
+
seq: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type AdmissionRecord = HarnessToken | HarnessHumanRecord;
|
|
85
|
+
|
|
86
|
+
interface Arm {
|
|
87
|
+
origin: string;
|
|
88
|
+
title: string;
|
|
89
|
+
synopsis?: string;
|
|
90
|
+
severity?: "warning";
|
|
91
|
+
text: string;
|
|
92
|
+
delivery: "direct" | "steer" | "followUp";
|
|
93
|
+
seq: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface HarnessRegistryState {
|
|
97
|
+
records: AdmissionRecord[];
|
|
98
|
+
seq: number;
|
|
99
|
+
arms: Arm[];
|
|
100
|
+
/** message object → { meta, text } (set at message_start). */
|
|
101
|
+
pendingMeta: WeakMap<object, { meta: HarnessMessageMeta; text: string }>;
|
|
102
|
+
/** messages whose selection already happened — message_end never re-selects. */
|
|
103
|
+
processed: WeakSet<object>;
|
|
104
|
+
installed: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stateFor(pi: object): HarnessRegistryState {
|
|
108
|
+
const existing = (pi as { [STATE_SYM]?: HarnessRegistryState })[STATE_SYM];
|
|
109
|
+
if (existing) return existing;
|
|
110
|
+
const state: HarnessRegistryState = {
|
|
111
|
+
records: [],
|
|
112
|
+
seq: 0,
|
|
113
|
+
arms: [],
|
|
114
|
+
pendingMeta: new WeakMap(),
|
|
115
|
+
processed: new WeakSet(),
|
|
116
|
+
installed: false,
|
|
117
|
+
};
|
|
118
|
+
try {
|
|
119
|
+
Object.defineProperty(pi, STATE_SYM, { value: state, enumerable: false, configurable: false, writable: false });
|
|
120
|
+
} catch {
|
|
121
|
+
// Frozen hosts: fall back to an instance-local property (single-proxy use).
|
|
122
|
+
try {
|
|
123
|
+
(pi as { [STATE_SYM]?: HarnessRegistryState })[STATE_SYM] = state;
|
|
124
|
+
} catch {
|
|
125
|
+
// Absolutely frozen: operate armless — sends stay UNTRACKED (native).
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return (pi as { [STATE_SYM]?: HarnessRegistryState })[STATE_SYM] ?? state;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let idCounter = 0;
|
|
132
|
+
function nextId(): string {
|
|
133
|
+
idCounter += 1;
|
|
134
|
+
return `hm-${Date.now().toString(36)}-${idCounter.toString(36)}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Build a stable namespaced meta (no date header — ids are monotonic). */
|
|
138
|
+
export function harnessMetadata(meta: HarnessMessageMetaInput, delivery: HarnessDelivery): HarnessMessageMeta {
|
|
139
|
+
return { version: 1, id: nextId(), delivery, ...meta };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Details payload for tool results: provenance header data for annotations
|
|
143
|
+
* appended to the result content (fusion nudges, kanboard R1, watchdog kill). */
|
|
144
|
+
export interface HarnessToolAnnotation {
|
|
145
|
+
meta: HarnessMessageMeta;
|
|
146
|
+
text: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function harnessToolResultDetails(
|
|
150
|
+
eventDetails: unknown,
|
|
151
|
+
meta: HarnessMessageMetaInput,
|
|
152
|
+
delivery: HarnessDelivery,
|
|
153
|
+
text: string,
|
|
154
|
+
): Record<string, unknown> {
|
|
155
|
+
const existing = (eventDetails as { unipiHarnessAnnotations?: unknown } | null | undefined)?.unipiHarnessAnnotations;
|
|
156
|
+
const list = Array.isArray(existing) ? existing : [];
|
|
157
|
+
return {
|
|
158
|
+
...((eventDetails as Record<string, unknown> | null | undefined) ?? {}),
|
|
159
|
+
unipiHarnessAnnotations: [...list, { meta: harnessMetadata(meta, delivery), text }],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Namespaced read of harness metadata from a message or details object. */
|
|
164
|
+
export function readHarnessMeta(source: unknown): HarnessMessageMeta | undefined {
|
|
165
|
+
const meta = (source as { unipiHarness?: unknown } | null | undefined)?.unipiHarness ?? source;
|
|
166
|
+
if (!meta || typeof meta !== "object") return undefined;
|
|
167
|
+
const m = meta as Partial<HarnessMessageMeta>;
|
|
168
|
+
const ALLOWED_DELIVERY: readonly string[] = ["direct", "steer", "followUp", "nextTurn", "boundary", "before_agent_start"];
|
|
169
|
+
if (
|
|
170
|
+
m.version !== 1 || typeof m.id !== "string" || typeof m.source !== "string" ||
|
|
171
|
+
typeof m.title !== "string" || typeof m.delivery !== "string" || !ALLOWED_DELIVERY.includes(m.delivery)
|
|
172
|
+
) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
return m as HarnessMessageMeta;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Extract the exact text the SDK renders/queues for a user message. */
|
|
179
|
+
export function userMessageText(content: unknown): string {
|
|
180
|
+
if (typeof content === "string") return content;
|
|
181
|
+
if (!Array.isArray(content)) return "";
|
|
182
|
+
return content
|
|
183
|
+
.filter((c): c is { type: "text"; text: string } => (c as { type?: string }).type === "text")
|
|
184
|
+
.map((c) => (c as { text: string }).text)
|
|
185
|
+
.join("");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function userTextFromMessage(message: unknown): string {
|
|
189
|
+
if (!message || typeof message !== "object") return "";
|
|
190
|
+
const m = message as { role?: unknown; content?: unknown };
|
|
191
|
+
if (m.role !== "user") return "";
|
|
192
|
+
return userMessageText(m.content);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Send a harness-generated user message through the UNCHANGED
|
|
197
|
+
* `pi.sendUserMessage` with a synchronous single-call provenance arm. Returns
|
|
198
|
+
* the raw sender's value (callers may await; failures propagate unchanged).
|
|
199
|
+
* Provenance tracking is best-effort: if observers cannot install on this API,
|
|
200
|
+
* the raw send proceeds untracked (native fallback).
|
|
201
|
+
*/
|
|
202
|
+
export function sendHarnessUserMessage(
|
|
203
|
+
pi: ExtensionAPI,
|
|
204
|
+
content: string,
|
|
205
|
+
meta: HarnessMessageMetaInput,
|
|
206
|
+
options?: { deliverAs?: "steer" | "followUp" },
|
|
207
|
+
): ReturnType<ExtensionAPI["sendUserMessage"]> {
|
|
208
|
+
let arm: Arm | undefined;
|
|
209
|
+
try {
|
|
210
|
+
try {
|
|
211
|
+
installHarnessProvenance(pi);
|
|
212
|
+
const state = stateFor(pi);
|
|
213
|
+
state.seq += 1;
|
|
214
|
+
arm = {
|
|
215
|
+
origin: meta.source,
|
|
216
|
+
title: meta.title,
|
|
217
|
+
synopsis: meta.synopsis,
|
|
218
|
+
severity: meta.severity,
|
|
219
|
+
text: content,
|
|
220
|
+
delivery: options?.deliverAs ?? "direct",
|
|
221
|
+
seq: state.seq,
|
|
222
|
+
};
|
|
223
|
+
state.arms.push(arm);
|
|
224
|
+
} catch {
|
|
225
|
+
// Observers/state unavailable (frozen host): send untracked.
|
|
226
|
+
arm = undefined;
|
|
227
|
+
}
|
|
228
|
+
return pi.sendUserMessage(content, options);
|
|
229
|
+
} finally {
|
|
230
|
+
// The input handler ran synchronously inside the call (or never ran —
|
|
231
|
+
// compaction/auth failure). Either way the arm dies with the call: an
|
|
232
|
+
// unconfirmed arm leaves NO token, so nothing can mismatch later sends.
|
|
233
|
+
if (arm) {
|
|
234
|
+
const idx = stateFor(pi).arms.indexOf(arm);
|
|
235
|
+
if (idx !== -1) stateFor(pi).arms.splice(idx, 1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const DELIVERY_ORDER: Record<string, number> = { direct: 0, steer: 1, followUp: 2 };
|
|
241
|
+
|
|
242
|
+
/** Prune consumed records immediately (bounded arrays over long runs). */
|
|
243
|
+
function prune(state: HarnessRegistryState): void {
|
|
244
|
+
if (state.records.some((r) => r.state === "consumed")) {
|
|
245
|
+
state.records = state.records.filter((r) => r.state !== "consumed");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Select the outstanding admission record for an arriving user message.
|
|
250
|
+
* Only ELIGIBLE harness records can label; human/unattributed records
|
|
251
|
+
* participate in conflict detection but never label. Identical texts with
|
|
252
|
+
* conflicting origins — across deliveries — fail closed and clear the whole
|
|
253
|
+
* matching group. */
|
|
254
|
+
function selectRecord(state: HarnessRegistryState, text: string): HarnessMessageMeta | undefined {
|
|
255
|
+
const candidates = state.records.filter(
|
|
256
|
+
(r) => r.state === "admitted" && r.text === text && (r.kind !== "harness" || r.eligible),
|
|
257
|
+
);
|
|
258
|
+
if (candidates.length === 0) return undefined;
|
|
259
|
+
const origins = new Set(candidates.map((r) => r.origin));
|
|
260
|
+
if (origins.size > 1) {
|
|
261
|
+
for (const r of candidates) r.state = "consumed";
|
|
262
|
+
prune(state);
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
candidates.sort((a, b) => (DELIVERY_ORDER[a.delivery] ?? 3) - (DELIVERY_ORDER[b.delivery] ?? 3) || a.seq - b.seq);
|
|
266
|
+
const chosen = candidates[0]!;
|
|
267
|
+
for (const r of candidates) r.state = "consumed";
|
|
268
|
+
prune(state); // consumed records never linger, even on normal success
|
|
269
|
+
if (chosen.kind !== "harness") return undefined;
|
|
270
|
+
return harnessMetadata(
|
|
271
|
+
{ source: chosen.origin, title: chosen.title, synopsis: chosen.synopsis, severity: chosen.severity },
|
|
272
|
+
chosen.delivery,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Install the provenance observers. Idempotent per API ROOT (state travels with
|
|
278
|
+
* the root through proxies). Model roles/text/delivery/admission are untouched;
|
|
279
|
+
* the only behavior change is the namespaced `unipiHarness` field on harness
|
|
280
|
+
* user messages.
|
|
281
|
+
*/
|
|
282
|
+
export function installHarnessProvenance(pi: ExtensionAPI): void {
|
|
283
|
+
const state = stateFor(pi);
|
|
284
|
+
if (state.installed) return;
|
|
285
|
+
state.installed = true;
|
|
286
|
+
const onAny = pi.on as unknown as (event: string, handler: (event?: unknown, ctx?: unknown) => unknown) => void;
|
|
287
|
+
|
|
288
|
+
onAny("input", (event) => {
|
|
289
|
+
const ev = (event ?? {}) as { text?: unknown; source?: unknown; streamingBehavior?: unknown };
|
|
290
|
+
const text = typeof ev.text === "string" ? ev.text : "";
|
|
291
|
+
const source = typeof ev.source === "string" ? ev.source : "interactive";
|
|
292
|
+
const streaming = ev.streamingBehavior === "steer" || ev.streamingBehavior === "followUp";
|
|
293
|
+
// Effective delivery: the queued value ONLY while streaming (the real SDK
|
|
294
|
+
// omits streamingBehavior on idle events even when the caller passed one —
|
|
295
|
+
// an idle followUp option is ignored and dispatches direct).
|
|
296
|
+
const effective = streaming ? (ev.streamingBehavior as "steer" | "followUp") : "direct";
|
|
297
|
+
// The next input invalidates ALL unmatched direct records (harness and
|
|
298
|
+
// human/rpc alike) — bounds failed/aborted sends. Queued stays.
|
|
299
|
+
state.records = state.records.filter(
|
|
300
|
+
(r) => !(r.delivery === "direct" && r.state === "admitted"),
|
|
301
|
+
);
|
|
302
|
+
state.seq += 1;
|
|
303
|
+
const arm = state.arms[state.arms.length - 1];
|
|
304
|
+
if (arm && source === "extension" && text === arm.text) {
|
|
305
|
+
state.records.push({
|
|
306
|
+
kind: "harness",
|
|
307
|
+
origin: arm.origin,
|
|
308
|
+
title: arm.title,
|
|
309
|
+
synopsis: arm.synopsis,
|
|
310
|
+
severity: arm.severity,
|
|
311
|
+
text: arm.text,
|
|
312
|
+
delivery: effective,
|
|
313
|
+
// Queued is eligible immediately; idle direct only at before_agent_start.
|
|
314
|
+
eligible: streaming,
|
|
315
|
+
state: "admitted",
|
|
316
|
+
seq: arm.seq,
|
|
317
|
+
});
|
|
318
|
+
prune(state);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
state.records.push({
|
|
322
|
+
kind: source === "extension" ? "unattributed" : "human",
|
|
323
|
+
origin: source,
|
|
324
|
+
text,
|
|
325
|
+
delivery: effective,
|
|
326
|
+
state: "admitted",
|
|
327
|
+
seq: state.seq,
|
|
328
|
+
});
|
|
329
|
+
prune(state);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
onAny("before_agent_start", (event) => {
|
|
333
|
+
const prompt = (event as { prompt?: unknown } | undefined)?.prompt;
|
|
334
|
+
if (typeof prompt !== "string") return;
|
|
335
|
+
// Idle harness records matching THIS prompt become eligible; other
|
|
336
|
+
// unconfirmed direct records are stale and dropped. Queued untouched.
|
|
337
|
+
for (const r of state.records) {
|
|
338
|
+
if (r.kind === "harness" && r.delivery === "direct" && r.state === "admitted") {
|
|
339
|
+
if (r.text === prompt) r.eligible = true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
state.records = state.records.filter(
|
|
343
|
+
(r) => !(r.kind === "harness" && r.delivery === "direct" && r.state === "admitted" && !r.eligible),
|
|
344
|
+
);
|
|
345
|
+
prune(state);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
onAny("message_start", (event) => {
|
|
349
|
+
const message = (event as { message?: unknown } | undefined)?.message;
|
|
350
|
+
if (!message || typeof message !== "object") return;
|
|
351
|
+
const text = userTextFromMessage(message);
|
|
352
|
+
state.processed.add(message as object);
|
|
353
|
+
if (!text) return;
|
|
354
|
+
const meta = selectRecord(state, text);
|
|
355
|
+
if (meta) state.pendingMeta.set(message as object, { meta, text });
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
onAny("message_end", (event) => {
|
|
359
|
+
const message = (event as { message?: unknown } | undefined)?.message;
|
|
360
|
+
if (!message || typeof message !== "object") return undefined;
|
|
361
|
+
if (!state.processed.has(message as object)) return undefined; // never re-select
|
|
362
|
+
const pending = state.pendingMeta.get(message as object);
|
|
363
|
+
if (!pending) return undefined;
|
|
364
|
+
state.pendingMeta.delete(message as object);
|
|
365
|
+
// Later transforms: attach only if the finalized text is still the
|
|
366
|
+
// selected text (original content identity preserved either way).
|
|
367
|
+
if (userTextFromMessage(message) !== pending.text) return undefined;
|
|
368
|
+
return { message: { ...(message as Record<string, unknown>), unipiHarness: pending.meta } };
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Terminal boundary: purge everything outstanding (queued records were
|
|
372
|
+
// consumed by their message_start before agent_end).
|
|
373
|
+
onAny("agent_settled", () => {
|
|
374
|
+
state.records = state.records.filter((r) => r.state === "consumed");
|
|
375
|
+
prune(state);
|
|
376
|
+
state.arms.length = 0;
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Fresh session context: pending state from the old tree is meaningless.
|
|
380
|
+
for (const evt of ["session_before_switch", "session_before_fork", "session_before_tree", "session_start", "session_shutdown"]) {
|
|
381
|
+
onAny(evt, () => {
|
|
382
|
+
state.records = [];
|
|
383
|
+
state.pendingMeta = new WeakMap();
|
|
384
|
+
state.processed = new WeakSet();
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Test hook: inspect the per-API state. */
|
|
390
|
+
export function harnessStateForTests(pi: object): HarnessRegistryState {
|
|
391
|
+
return stateFor(pi);
|
|
392
|
+
}
|
package/packages/core/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/footer",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.20",
|
|
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": "3.0.0-alpha.
|
|
36
|
-
"@pi-unipi/background-tasks": "3.0.0-alpha.
|
|
35
|
+
"@pi-unipi/core": "3.0.0-alpha.20",
|
|
36
|
+
"@pi-unipi/background-tasks": "3.0.0-alpha.20"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.87.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/fusion",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.20",
|
|
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,9 +32,9 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@pi-unipi/core": "3.0.0-alpha.
|
|
36
|
-
"@pi-unipi/subagents": "3.0.0-alpha.
|
|
37
|
-
"@pi-unipi/utility": "3.0.0-alpha.
|
|
35
|
+
"@pi-unipi/core": "3.0.0-alpha.20",
|
|
36
|
+
"@pi-unipi/subagents": "3.0.0-alpha.20",
|
|
37
|
+
"@pi-unipi/utility": "3.0.0-alpha.20"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@earendil-works/pi-ai": "^0.87.1",
|