@pi-unipi/unipi 2.10.2 → 2.12.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 +37 -0
- package/README.md +1 -1
- package/package.json +23 -23
- 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/README.md +20 -3
- package/packages/footer/package.json +2 -2
- package/packages/footer/src/commands.ts +9 -15
- package/packages/footer/src/config.ts +2 -0
- package/packages/footer/src/glance-editor.ts +285 -0
- package/packages/footer/src/help.ts +1 -1
- package/packages/footer/src/index.ts +360 -27
- package/packages/footer/src/presets.ts +16 -1
- package/packages/footer/src/rendering/icons.ts +8 -0
- package/packages/footer/src/rendering/theme.ts +2 -0
- package/packages/footer/src/segments/core.ts +114 -7
- package/packages/footer/src/tps-tracker.ts +529 -240
- package/packages/footer/src/tui/settings-tui.ts +18 -2
- package/packages/footer/src/types.ts +4 -0
- 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/package.json +2 -2
- package/packages/ralph/package.json +3 -3
- package/packages/subagents/package.json +2 -2
- package/packages/trajectory/README.md +23 -7
- package/packages/trajectory/index.ts +23 -3
- package/packages/trajectory/package.json +1 -1
- package/packages/trajectory/src/capture.ts +150 -30
- package/packages/trajectory/src/prefix-integrity.ts +213 -0
- package/packages/trajectory/src/project.ts +57 -5
- package/packages/trajectory/src/server.ts +10 -3
- package/packages/trajectory/src/telemetry.ts +79 -18
- package/packages/trajectory/src/tracer.ts +401 -0
- package/packages/trajectory/src/ui.ts +14 -12
- package/packages/unipi/bundled.js +10080 -3406
- package/packages/unipi/index.ts +25 -22
- 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
|
@@ -50,8 +50,14 @@ const overlay = new OverlayTheme();
|
|
|
50
50
|
|
|
51
51
|
// ─── Show the footer settings overlay ──────────────────────────────────
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Returns the overlay's promise so callers can run post-close actions
|
|
55
|
+
* (e.g. the glance editor swap) AFTER focus has returned to the editor —
|
|
56
|
+
* setEditorComponent() steals keyboard focus and would strand this very
|
|
57
|
+
* overlay unclosable if invoked from onSettingsChanged while it is open.
|
|
58
|
+
*/
|
|
59
|
+
export function showFooterSettings(ctx: ExtensionCommandContext, groups: FooterGroup[], onSettingsChanged?: () => void): Promise<void> {
|
|
60
|
+
return ctx.ui.custom<void>(
|
|
55
61
|
(tui, _theme, _keybindings, done) => {
|
|
56
62
|
const overlay = new FooterSettingsOverlay(groups, onSettingsChanged);
|
|
57
63
|
overlay.onClose = () => done();
|
|
@@ -214,6 +220,13 @@ class FooterSettingsOverlay {
|
|
|
214
220
|
|
|
215
221
|
private buildAppearanceList(): void {
|
|
216
222
|
const items: SettingItem[] = [
|
|
223
|
+
{
|
|
224
|
+
id: "glanceMode",
|
|
225
|
+
label: "Glance Footer",
|
|
226
|
+
description: "Experimental: framed input box + session strip (replaces classic row)",
|
|
227
|
+
currentValue: this.settings.glanceMode !== false ? "on" : "off",
|
|
228
|
+
values: ["on", "off"],
|
|
229
|
+
},
|
|
217
230
|
{
|
|
218
231
|
id: "preset",
|
|
219
232
|
label: "Preset",
|
|
@@ -352,6 +365,9 @@ class FooterSettingsOverlay {
|
|
|
352
365
|
|
|
353
366
|
private onAppearanceChange(id: string, newValue: string): void {
|
|
354
367
|
switch (id) {
|
|
368
|
+
case "glanceMode":
|
|
369
|
+
this.settings.glanceMode = newValue === "on";
|
|
370
|
+
break;
|
|
355
371
|
case "preset":
|
|
356
372
|
this.settings.preset = newValue;
|
|
357
373
|
break;
|
|
@@ -15,7 +15,9 @@ export type SegmentZone = "left" | "center" | "right";
|
|
|
15
15
|
/** Semantic color names mapped to segment groups */
|
|
16
16
|
export type SemanticColor =
|
|
17
17
|
// ── Model & Identity (Left zone) ──
|
|
18
|
+
| "brand"
|
|
18
19
|
| "model"
|
|
20
|
+
| "directory"
|
|
19
21
|
| "path"
|
|
20
22
|
| "git"
|
|
21
23
|
| "gitClean"
|
|
@@ -181,6 +183,8 @@ export interface FooterSettings {
|
|
|
181
183
|
enabled: boolean;
|
|
182
184
|
/** Active preset name */
|
|
183
185
|
preset: string;
|
|
186
|
+
/** Glance-style experiment: frame input box + session strip, suppress classic row */
|
|
187
|
+
glanceMode?: boolean;
|
|
184
188
|
/** Separator style */
|
|
185
189
|
separator: SeparatorStyle;
|
|
186
190
|
/** Icon style: nerd (Nerd Font glyphs), emoji (Unicode emoji), text (plain labels) */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/image",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.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.12.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.12.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.12.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.12.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.12.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.12.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.12.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.12.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.12.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.12.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.12.0",
|
|
43
|
+
"@pi-unipi/info-screen": "2.12.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.12.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.12.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/notify",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "Cross-platform notification extension for Pi — native OS, Gotify, and Telegram notifications for agent lifecycle events",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "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.12.0",
|
|
38
38
|
"node-notifier": "^10.0.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/ralph",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "Long-running iterative development loops for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"test": "npx tsx --test reminder.test.ts"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@pi-unipi/core": "2.
|
|
35
|
-
"@pi-unipi/info-screen": "2.
|
|
34
|
+
"@pi-unipi/core": "2.12.0",
|
|
35
|
+
"@pi-unipi/info-screen": "2.12.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@earendil-works/pi-ai": "^0.84.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/subagents",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "Subagents for UniPi — parallel execution, file locking, workflow integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"test": "npx tsx --test src/__tests__/*.test.ts"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@pi-unipi/core": "2.
|
|
12
|
+
"@pi-unipi/core": "2.12.0",
|
|
13
13
|
"@earendil-works/pi-agent-core": "^0.84.0",
|
|
14
14
|
"acorn": "8.18.0"
|
|
15
15
|
},
|
|
@@ -15,15 +15,23 @@ Typing `/unipi:trajectory` followed by a space auto-completes the available
|
|
|
15
15
|
actions (`stop`, `off`, `toggle`), so the options are discoverable without
|
|
16
16
|
reading the docs. Running it with no argument opens or reuses the server.
|
|
17
17
|
|
|
18
|
-
The command starts or reuses an in-process server on `127.0.0.1:8176-8186` and opens it in the default browser. The page follows the active session every
|
|
18
|
+
The command starts or reuses an in-process server on `127.0.0.1:8176-8186` and opens it in the default browser. The page follows the active session every 1.5 seconds, skips unchanged snapshots, and reuses an in-memory telemetry tail instead of rereading the JSONL file. The server is owned by the current Pi process and is stopped on any Pi session shutdown/reload/resume/new/fork event.
|
|
19
19
|
|
|
20
20
|
## What it shows
|
|
21
21
|
|
|
22
|
+
- The exact provider-context path: user/assistant/tool messages, effective system prompt, serialized provider request, and prefix-integrity verdict
|
|
23
|
+
- UniPi package operations only when they mutate context or cross an explicit context epoch boundary; UI-only hooks, registrations, notifications, command bookkeeping, and unrelated EventBus traffic are intentionally excluded from the ledger
|
|
24
|
+
- A per-request prefix-integrity verdict: first request, identical retry, append-only extension, explicit lifecycle boundary, or violation
|
|
25
|
+
- Exact prefix-break evidence: first changed message index/path plus independent system-prompt, tool-schema/order, route, thinking-level, and provider-envelope drift
|
|
26
|
+
- Contributing UniPi package operations between the previous request and a violation
|
|
27
|
+
- One-click **UniPi** and **Violations** filters, plus Trace, Integrity, and Attribution detail tabs
|
|
28
|
+
- The fully assembled effective system prompt for each agent run
|
|
22
29
|
- Turn-aware User, Assistant, Tool, Compaction, and Branch ledger
|
|
23
|
-
-
|
|
24
|
-
-
|
|
30
|
+
- Effective context messages before each model request and the final provider payload after serialization/extension rewrites
|
|
31
|
+
- Assistant reasoning and final output, including streamed assistant events
|
|
32
|
+
- Tool payloads, final results, errors, and measured call duration
|
|
25
33
|
- Provider, model, stop reason, token usage, cache reads/writes, and cost
|
|
26
|
-
- Provider, model, thinking level, request payload
|
|
34
|
+
- Provider, model, thinking level, serialized request payload, and tool schemas
|
|
27
35
|
- Exact TTFT, decoding/total request timing, and tool execution timing
|
|
28
36
|
- Input/Model/Tools overview timeline with drag-to-focus
|
|
29
37
|
- Search, turn folding, call folding, and a local record inspector
|
|
@@ -34,7 +42,15 @@ The server binds only to `127.0.0.1`. It reads `SessionManager.getBranch()` and
|
|
|
34
42
|
|
|
35
43
|
Detailed telemetry is appended to `~/.unipi/trajectory/<session-id>.jsonl` with a private directory/file mode. Authorization, API-key, token, cookie, secret, password, and credential fields and credential-shaped values are recursively redacted before persistence.
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
Context-relevant prompt, request, package mutation, and prefix-integrity fields appear when captured by the trajectory telemetry sidecar. Stream deltas, response headers, inert hooks, and other activity that cannot affect model context are not persisted. Secret-shaped headers and values are redacted before persistence, and pathological events/read volume are bounded so debug capture cannot exhaust the Pi process.
|
|
46
|
+
|
|
47
|
+
## Prefix integrity
|
|
48
|
+
|
|
49
|
+
Inside one cache epoch, the previous serialized message sequence must be an exact prefix of the next request. An identical retry is also safe. Any earlier edit, removal, reorder, or middle insertion is a violation. System instructions, tool definitions/order, provider route, thinking level, and request envelope are checked independently because changes there also invalidate provider prefix caches.
|
|
50
|
+
|
|
51
|
+
Compaction, tree navigation, model changes, and thinking-level changes create explicit epoch boundaries. The first request after a boundary is labeled as such; append-only enforcement resumes on the following request.
|
|
52
|
+
|
|
53
|
+
Package attribution is intentionally limited to UniPi modules loaded through the all-in-one `@pi-unipi/unipi` umbrella. Standalone trajectory still captures Pi's observable hooks and requests, but cannot attribute calls made by independently loaded packages.
|
|
38
54
|
|
|
39
55
|
## Lifecycle
|
|
40
56
|
|
|
@@ -42,6 +58,6 @@ Each Pi process owns its own trajectory server. A second Pi process running `/un
|
|
|
42
58
|
|
|
43
59
|
## Limitations
|
|
44
60
|
|
|
45
|
-
- Live transport uses
|
|
46
|
-
- Very large branches are rendered in one table;
|
|
61
|
+
- Live transport uses conditional 1.5-second polling rather than a push stream.
|
|
62
|
+
- Very large active branches are rendered in one table; unchanged snapshots are not rebuilt or rerendered.
|
|
47
63
|
- Tool duration uses exact executor telemetry when captured and session timestamps otherwise.
|
|
@@ -2,19 +2,33 @@ import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-wor
|
|
|
2
2
|
import { registerTelemetryCapture } from "./src/capture.js";
|
|
3
3
|
import { projectTrajectory } from "./src/project.js";
|
|
4
4
|
import { openBrowser, TrajectoryServer } from "./src/server.js";
|
|
5
|
+
import { createUnipiTracer, type UnipiTraceRecorder } from "./src/tracer.js";
|
|
6
|
+
|
|
7
|
+
export { createUnipiTracer, type UnipiTraceRecorder, type UnipiTracer } from "./src/tracer.js";
|
|
5
8
|
|
|
6
9
|
let server: TrajectoryServer | null = null;
|
|
7
10
|
let currentContext: ExtensionContext | null = null;
|
|
11
|
+
let cachedLeafId: string | null | undefined;
|
|
12
|
+
let cachedTelemetryRevision = -1;
|
|
13
|
+
let cachedSnapshot: ReturnType<typeof projectTrajectory> | null = null;
|
|
8
14
|
|
|
9
15
|
function stopServer(): boolean {
|
|
10
16
|
if (!server?.url) return false;
|
|
11
17
|
server.stop();
|
|
12
18
|
server = null;
|
|
19
|
+
cachedLeafId = undefined;
|
|
20
|
+
cachedTelemetryRevision = -1;
|
|
21
|
+
cachedSnapshot = null;
|
|
13
22
|
return true;
|
|
14
23
|
}
|
|
15
24
|
|
|
16
|
-
export
|
|
17
|
-
|
|
25
|
+
export interface TrajectoryOptions {
|
|
26
|
+
traceRecorder?: UnipiTraceRecorder;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function trajectory(pi: ExtensionAPI, options: TrajectoryOptions = {}): void {
|
|
30
|
+
const recorder = options.traceRecorder ?? createUnipiTracer(pi).recorder;
|
|
31
|
+
const readTelemetry = registerTelemetryCapture(pi, undefined, recorder);
|
|
18
32
|
|
|
19
33
|
pi.registerCommand("unipi:trajectory", {
|
|
20
34
|
description: "Open or manage UniPi's live trajectory for the current session",
|
|
@@ -54,11 +68,17 @@ export default function trajectory(pi: ExtensionAPI): void {
|
|
|
54
68
|
server = new TrajectoryServer({
|
|
55
69
|
snapshot: () => {
|
|
56
70
|
const active = currentContext ?? ctx;
|
|
57
|
-
|
|
71
|
+
const leafId = active.sessionManager.getLeafId();
|
|
72
|
+
const telemetryRevision = recorder.revision();
|
|
73
|
+
if (cachedSnapshot && cachedLeafId === leafId && cachedTelemetryRevision === telemetryRevision) return cachedSnapshot;
|
|
74
|
+
cachedLeafId = leafId;
|
|
75
|
+
cachedTelemetryRevision = telemetryRevision;
|
|
76
|
+
cachedSnapshot = projectTrajectory(active.sessionManager.getBranch() as SessionEntry[], {
|
|
58
77
|
sessionId: active.sessionManager.getSessionId(),
|
|
59
78
|
name: active.sessionManager.getSessionName(),
|
|
60
79
|
cwd: active.cwd,
|
|
61
80
|
}, readTelemetry());
|
|
81
|
+
return cachedSnapshot;
|
|
62
82
|
},
|
|
63
83
|
});
|
|
64
84
|
}
|
|
@@ -1,83 +1,203 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { PrefixIntegrityTracker } from "./prefix-integrity.js";
|
|
2
3
|
import { TelemetrySidecar } from "./telemetry.js";
|
|
4
|
+
import type { UnipiTraceRecorder } from "./tracer.js";
|
|
3
5
|
|
|
4
6
|
interface RequestState {
|
|
5
7
|
id: number;
|
|
6
|
-
|
|
8
|
+
turnIndex?: number;
|
|
9
|
+
startedAt?: number;
|
|
7
10
|
firstTokenAt?: number;
|
|
8
11
|
}
|
|
9
12
|
|
|
13
|
+
interface PendingAgent {
|
|
14
|
+
runId: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
/** Register read-only observability hooks. Returned reader follows the active session. */
|
|
11
|
-
export function registerTelemetryCapture(
|
|
18
|
+
export function registerTelemetryCapture(
|
|
19
|
+
pi: ExtensionAPI,
|
|
20
|
+
sidecarRoot?: string,
|
|
21
|
+
traceRecorder?: UnipiTraceRecorder,
|
|
22
|
+
): () => ReturnType<TelemetrySidecar["read"]> {
|
|
12
23
|
let sidecar: TelemetrySidecar | null = null;
|
|
13
24
|
let requestSeq = 0;
|
|
25
|
+
let runSeq = 0;
|
|
14
26
|
let active: RequestState | null = null;
|
|
27
|
+
let lastRequestId: number | null = null;
|
|
28
|
+
let currentTurnIndex: number | undefined;
|
|
29
|
+
let pendingAgent: PendingAgent | null = null;
|
|
30
|
+
let previousTraceCursor = 0;
|
|
31
|
+
const prefixIntegrity = new PrefixIntegrityTracker();
|
|
15
32
|
const toolStarts = new Map<string, number>();
|
|
16
33
|
|
|
17
34
|
const bind = (ctx: ExtensionContext) => {
|
|
18
|
-
|
|
35
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
36
|
+
sidecar = new TelemetrySidecar(sessionId, sidecarRoot);
|
|
37
|
+
traceRecorder?.bind(sessionId);
|
|
38
|
+
const existing = sidecar.read();
|
|
39
|
+
requestSeq = Math.max(0, ...existing.flatMap(event => typeof event.requestId === "number" ? [event.requestId] : []));
|
|
40
|
+
runSeq = Math.max(0, ...existing.flatMap(event => typeof event.runId === "number" ? [event.runId] : []));
|
|
19
41
|
active = null;
|
|
42
|
+
lastRequestId = null;
|
|
43
|
+
currentTurnIndex = undefined;
|
|
44
|
+
pendingAgent = null;
|
|
45
|
+
previousTraceCursor = traceRecorder?.cursor() ?? 0;
|
|
46
|
+
prefixIntegrity.reset();
|
|
20
47
|
toolStarts.clear();
|
|
21
48
|
};
|
|
22
|
-
const append = (
|
|
23
|
-
|
|
49
|
+
const append = (
|
|
50
|
+
type: Parameters<TelemetrySidecar["append"]>[0]["type"],
|
|
51
|
+
data?: unknown,
|
|
52
|
+
extra: Record<string, unknown> = {},
|
|
53
|
+
at = Date.now(),
|
|
54
|
+
) => {
|
|
55
|
+
sidecar?.append({ type, at, ...extra, ...(data === undefined ? {} : { data }) });
|
|
24
56
|
};
|
|
25
57
|
|
|
58
|
+
const ensureRequest = (startedAt?: number) => {
|
|
59
|
+
if (!active) {
|
|
60
|
+
active = { id: ++requestSeq, turnIndex: currentTurnIndex, startedAt };
|
|
61
|
+
} else if (startedAt !== undefined) {
|
|
62
|
+
active.startedAt = startedAt;
|
|
63
|
+
}
|
|
64
|
+
lastRequestId = active.id;
|
|
65
|
+
return active;
|
|
66
|
+
};
|
|
67
|
+
const requestExtra = (request = active) => ({
|
|
68
|
+
...(request?.id !== undefined ? { requestId: request.id } : lastRequestId === null ? {} : { requestId: lastRequestId }),
|
|
69
|
+
...(request?.turnIndex !== undefined ? { turnIndex: request.turnIndex } : currentTurnIndex === undefined ? {} : { turnIndex: currentTurnIndex }),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
|
|
26
73
|
pi.on("session_start", (_event, ctx) => { bind(ctx); });
|
|
74
|
+
pi.on("resources_discover", () => {});
|
|
75
|
+
pi.on("session_info_changed", () => {});
|
|
76
|
+
pi.on("session_before_switch", () => {});
|
|
77
|
+
pi.on("session_before_fork", () => {});
|
|
78
|
+
pi.on("session_before_compact", () => {});
|
|
79
|
+
pi.on("session_compact", (event) => { prefixIntegrity.markBoundary(`session_compact:${event.reason}`); });
|
|
80
|
+
pi.on("session_before_tree", () => {});
|
|
81
|
+
pi.on("session_tree", () => { prefixIntegrity.markBoundary("session_tree"); });
|
|
82
|
+
pi.on("session_shutdown", () => {});
|
|
83
|
+
|
|
84
|
+
pi.on("before_agent_start", (event) => {
|
|
85
|
+
pendingAgent = { runId: ++runSeq };
|
|
86
|
+
});
|
|
87
|
+
pi.on("agent_start", (event, ctx) => {
|
|
88
|
+
active = null;
|
|
89
|
+
lastRequestId = null;
|
|
90
|
+
currentTurnIndex = undefined;
|
|
91
|
+
const runId = pendingAgent?.runId ?? ++runSeq;
|
|
92
|
+
append("system-prompt", {
|
|
93
|
+
systemPrompt: ctx.getSystemPrompt(),
|
|
94
|
+
provider: ctx.model?.provider,
|
|
95
|
+
model: ctx.model?.id,
|
|
96
|
+
thinkingLevel: ctx.thinkingLevel,
|
|
97
|
+
}, { runId });
|
|
98
|
+
});
|
|
99
|
+
pi.on("agent_end", () => {});
|
|
100
|
+
pi.on("agent_settled", () => { pendingAgent = null; });
|
|
101
|
+
pi.on("turn_start", (event) => {
|
|
102
|
+
currentTurnIndex = event.turnIndex;
|
|
103
|
+
active = {
|
|
104
|
+
id: ++requestSeq,
|
|
105
|
+
turnIndex: event.turnIndex,
|
|
106
|
+
};
|
|
107
|
+
lastRequestId = active.id;
|
|
108
|
+
});
|
|
109
|
+
pi.on("turn_end", () => {});
|
|
110
|
+
|
|
111
|
+
pi.on("context", () => { ensureRequest(); });
|
|
112
|
+
pi.on("before_provider_headers", () => { ensureRequest(); });
|
|
27
113
|
pi.on("before_provider_request", (event, ctx) => {
|
|
28
114
|
const startedAt = Date.now();
|
|
29
|
-
|
|
115
|
+
const request = ensureRequest(startedAt);
|
|
116
|
+
const integrity = prefixIntegrity.observe(event.payload, {
|
|
117
|
+
provider: ctx.model?.provider,
|
|
118
|
+
model: ctx.model?.id,
|
|
119
|
+
api: ctx.model?.api,
|
|
120
|
+
thinkingLevel: ctx.thinkingLevel,
|
|
121
|
+
});
|
|
122
|
+
const traceCursor = traceRecorder?.cursor() ?? 0;
|
|
123
|
+
const contributingTrace = traceRecorder?.since(previousTraceCursor) ?? [];
|
|
124
|
+
previousTraceCursor = traceCursor;
|
|
125
|
+
const attributed = integrity.verdict === "violation"
|
|
126
|
+
? contributingTrace
|
|
127
|
+
.filter(item => item.type === "unipi-trace")
|
|
128
|
+
.map(item => item.data)
|
|
129
|
+
.filter((data): data is Record<string, unknown> => Boolean(data && typeof data === "object"))
|
|
130
|
+
.filter(data => data.phase === "exit" && (
|
|
131
|
+
data.surface === "hook" && data.mutation && (data.mutation as Record<string, unknown>).changed === true ||
|
|
132
|
+
data.surface === "api" && ["sendMessage", "sendUserMessage", "setActiveTools", "setModel", "setThinkingLevel", "registerProvider", "unregisterProvider"].includes(String(data.action)) ||
|
|
133
|
+
data.surface === "context-api" && ["compact", "navigateTree", "switchSession", "newSession", "fork", "reload"].includes(String(data.action))
|
|
134
|
+
))
|
|
135
|
+
.slice(-20)
|
|
136
|
+
: [];
|
|
137
|
+
append("prefix-integrity", {
|
|
138
|
+
...integrity,
|
|
139
|
+
attribution: attributed.length > 0 ? attributed : undefined,
|
|
140
|
+
}, requestExtra(request));
|
|
30
141
|
append("request", {
|
|
31
142
|
payload: event.payload,
|
|
32
143
|
provider: ctx.model?.provider,
|
|
33
144
|
model: ctx.model?.id,
|
|
34
145
|
thinkingLevel: ctx.thinkingLevel,
|
|
35
|
-
|
|
36
|
-
? (event.payload as { tools: unknown }).tools
|
|
37
|
-
: undefined,
|
|
38
|
-
}, { requestId: active.id });
|
|
39
|
-
});
|
|
40
|
-
pi.on("after_provider_response", (event) => {
|
|
41
|
-
append("response", { status: event.status, headers: event.headers }, active ? { requestId: active.id } : {});
|
|
146
|
+
}, requestExtra(request));
|
|
42
147
|
});
|
|
148
|
+
pi.on("after_provider_response", () => {});
|
|
149
|
+
|
|
150
|
+
pi.on("message_start", () => {});
|
|
43
151
|
pi.on("message_update", (event) => {
|
|
44
|
-
|
|
152
|
+
const request = ensureRequest();
|
|
153
|
+
if (request.firstTokenAt !== undefined) return;
|
|
45
154
|
if (!["text_delta", "thinking_delta", "toolcall_delta"].includes(event.assistantMessageEvent.type)) return;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
ttftMs: active.firstTokenAt - active.startedAt,
|
|
49
|
-
eventType: event.assistantMessageEvent.type,
|
|
50
|
-
}, { requestId: active.id });
|
|
155
|
+
request.firstTokenAt = Date.now();
|
|
156
|
+
|
|
51
157
|
});
|
|
52
158
|
pi.on("message_end", (event) => {
|
|
53
159
|
if (event.message.role !== "assistant" || !active) return;
|
|
54
160
|
const completedAt = Date.now();
|
|
55
161
|
append("message-end", {
|
|
56
|
-
totalMs: completedAt - active.startedAt,
|
|
57
|
-
ttftMs: active.
|
|
162
|
+
totalMs: active.startedAt === undefined ? undefined : completedAt - active.startedAt,
|
|
163
|
+
ttftMs: active.startedAt === undefined || active.firstTokenAt === undefined
|
|
164
|
+
? undefined
|
|
165
|
+
: active.firstTokenAt - active.startedAt,
|
|
58
166
|
decodingMs: active.firstTokenAt === undefined ? undefined : completedAt - active.firstTokenAt,
|
|
59
167
|
usage: event.message.usage,
|
|
60
168
|
stopReason: event.message.stopReason,
|
|
61
169
|
errorMessage: event.message.errorMessage,
|
|
62
|
-
},
|
|
170
|
+
}, requestExtra());
|
|
63
171
|
active = null;
|
|
64
172
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
});
|
|
173
|
+
|
|
174
|
+
pi.on("tool_execution_start", (event) => { toolStarts.set(event.toolCallId, Date.now()); });
|
|
175
|
+
pi.on("tool_call", () => {});
|
|
176
|
+
pi.on("tool_execution_update", () => {});
|
|
177
|
+
pi.on("tool_result", () => {});
|
|
70
178
|
pi.on("tool_execution_end", (event) => {
|
|
71
179
|
const at = Date.now();
|
|
72
180
|
const startedAt = toolStarts.get(event.toolCallId);
|
|
73
181
|
append("tool-end", {
|
|
74
182
|
toolName: event.toolName,
|
|
75
|
-
result: event.result,
|
|
76
183
|
isError: event.isError,
|
|
77
184
|
durationMs: startedAt === undefined ? undefined : at - startedAt,
|
|
78
|
-
}, { toolCallId: event.toolCallId });
|
|
185
|
+
}, { ...requestExtra(), toolCallId: event.toolCallId });
|
|
79
186
|
toolStarts.delete(event.toolCallId);
|
|
80
187
|
});
|
|
81
188
|
|
|
82
|
-
|
|
189
|
+
pi.on("model_select", (event) => {
|
|
190
|
+
if (event.previousModel && (
|
|
191
|
+
event.previousModel.provider !== event.model.provider ||
|
|
192
|
+
event.previousModel.id !== event.model.id ||
|
|
193
|
+
event.previousModel.api !== event.model.api
|
|
194
|
+
)) prefixIntegrity.markBoundary("model_select");
|
|
195
|
+
});
|
|
196
|
+
pi.on("thinking_level_select", (event) => {
|
|
197
|
+
if (event.previousLevel !== event.level) prefixIntegrity.markBoundary("thinking_level_select");
|
|
198
|
+
});
|
|
199
|
+
pi.on("user_bash", () => {});
|
|
200
|
+
pi.on("input", () => {});
|
|
201
|
+
|
|
202
|
+
return () => sidecar?.read() ?? traceRecorder?.read() ?? [];
|
|
83
203
|
}
|