@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +1 -1
  3. package/package.json +23 -23
  4. package/packages/ask-user/package.json +2 -2
  5. package/packages/autocomplete/package.json +1 -1
  6. package/packages/background-tasks/package.json +2 -2
  7. package/packages/btw/package.json +2 -2
  8. package/packages/compactor/package.json +3 -3
  9. package/packages/core/package.json +1 -1
  10. package/packages/footer/README.md +20 -3
  11. package/packages/footer/package.json +2 -2
  12. package/packages/footer/src/commands.ts +9 -15
  13. package/packages/footer/src/config.ts +2 -0
  14. package/packages/footer/src/glance-editor.ts +285 -0
  15. package/packages/footer/src/help.ts +1 -1
  16. package/packages/footer/src/index.ts +360 -27
  17. package/packages/footer/src/presets.ts +16 -1
  18. package/packages/footer/src/rendering/icons.ts +8 -0
  19. package/packages/footer/src/rendering/theme.ts +2 -0
  20. package/packages/footer/src/segments/core.ts +114 -7
  21. package/packages/footer/src/tps-tracker.ts +529 -240
  22. package/packages/footer/src/tui/settings-tui.ts +18 -2
  23. package/packages/footer/src/types.ts +4 -0
  24. package/packages/image/package.json +2 -2
  25. package/packages/info-screen/package.json +2 -2
  26. package/packages/input-shortcuts/package.json +2 -2
  27. package/packages/kanboard/package.json +2 -2
  28. package/packages/mcp/package.json +2 -2
  29. package/packages/memory/package.json +3 -3
  30. package/packages/milestone/package.json +2 -2
  31. package/packages/notify/package.json +2 -2
  32. package/packages/ralph/package.json +3 -3
  33. package/packages/subagents/package.json +2 -2
  34. package/packages/trajectory/README.md +23 -7
  35. package/packages/trajectory/index.ts +23 -3
  36. package/packages/trajectory/package.json +1 -1
  37. package/packages/trajectory/src/capture.ts +150 -30
  38. package/packages/trajectory/src/prefix-integrity.ts +213 -0
  39. package/packages/trajectory/src/project.ts +57 -5
  40. package/packages/trajectory/src/server.ts +10 -3
  41. package/packages/trajectory/src/telemetry.ts +79 -18
  42. package/packages/trajectory/src/tracer.ts +401 -0
  43. package/packages/trajectory/src/ui.ts +14 -12
  44. package/packages/unipi/bundled.js +10080 -3406
  45. package/packages/unipi/index.ts +25 -22
  46. package/packages/updater/package.json +2 -2
  47. package/packages/utility/package.json +2 -2
  48. package/packages/web-api/package.json +2 -2
  49. 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
- export function showFooterSettings(ctx: ExtensionCommandContext, groups: FooterGroup[], onSettingsChanged?: () => void): void {
54
- ctx.ui.custom<void>(
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.10.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.10.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.10.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.10.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.10.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.10.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.10.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.10.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.10.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.10.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.10.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.10.2",
43
- "@pi-unipi/info-screen": "2.10.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.10.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.10.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.10.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.10.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.10.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.10.2",
35
- "@pi-unipi/info-screen": "2.10.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.10.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.10.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 500 ms. The server is owned by the current Pi process and is stopped on any Pi session shutdown/reload/resume/new/fork event.
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
- - Assistant reasoning and final output
24
- - Tool payloads, results, errors, and measured call duration
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/options, tool schemas, response status/headers
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
- Detailed request fields appear when captured by the trajectory telemetry sidecar.
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 500 ms polling rather than a push stream.
46
- - Very large branches are rendered in one table; browser virtualization can be added if measured sessions require it.
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 default function trajectory(pi: ExtensionAPI): void {
17
- const readTelemetry = registerTelemetryCapture(pi);
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
- return projectTrajectory(active.sessionManager.getBranch() as SessionEntry[], {
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/trajectory",
3
- "version": "2.10.2",
3
+ "version": "2.12.0",
4
4
  "description": "Live UniPi trajectory inspector for Pi sessions",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -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
- startedAt: number;
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(pi: ExtensionAPI): () => ReturnType<TelemetrySidecar["read"]> {
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
- sidecar = new TelemetrySidecar(ctx.sessionManager.getSessionId());
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 = (type: Parameters<TelemetrySidecar["append"]>[0]["type"], data?: unknown, extra: Record<string, unknown> = {}) => {
23
- sidecar?.append({ type, at: Date.now(), ...extra, ...(data === undefined ? {} : { data }) });
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
- active = { id: ++requestSeq, startedAt };
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
- tools: Array.isArray((event.payload as { tools?: unknown })?.tools)
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
- if (!active || active.firstTokenAt !== undefined) return;
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
- active.firstTokenAt = Date.now();
47
- append("first-token", {
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.firstTokenAt === undefined ? undefined : active.firstTokenAt - active.startedAt,
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
- }, { requestId: active.id });
170
+ }, requestExtra());
63
171
  active = null;
64
172
  });
65
- pi.on("tool_execution_start", (event) => {
66
- const at = Date.now();
67
- toolStarts.set(event.toolCallId, at);
68
- append("tool-start", { toolName: event.toolName, args: event.args }, { toolCallId: event.toolCallId });
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
- return () => sidecar?.read() ?? [];
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
  }