@xpufx/paseo-top 0.4.3 → 0.4.5

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 (130) hide show
  1. package/OVERVIEW.md +17 -0
  2. package/client/host-ui.tsx +184 -0
  3. package/client/multi-host-view.tsx +57 -43
  4. package/client/multi-host.ts +36 -13
  5. package/client/pill-labels.ts +124 -7
  6. package/client/pill.tsx +724 -669
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +33 -0
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +310 -274
  11. package/client/telemetry-copy.ts +8 -2
  12. package/client/telemetry.tsx +372 -321
  13. package/client/turn-panel.tsx +126 -104
  14. package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +21 -61
  15. package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
  16. package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
  17. package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
  18. package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
  19. package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
  20. package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
  21. package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
  22. package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
  23. package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
  24. package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
  25. package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
  26. package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
  27. package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
  28. package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +118 -338
  29. package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
  30. package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
  31. package/client/vendor/paseo-plugin-helper/lifecycle/surface.tsx +140 -0
  32. package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
  33. package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
  34. package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
  35. package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +55 -47
  36. package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
  37. package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
  38. package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
  39. package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
  40. package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
  41. package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
  42. package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
  43. package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
  44. package/client/vendor/permission-audit/client.tsx +706 -0
  45. package/client/vendor/permission-audit/index.tsx +1 -0
  46. package/index.client.tsx +58 -37
  47. package/index.server.ts +256 -227
  48. package/package.json +14 -2
  49. package/paseo-plugin.json +2 -0
  50. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  51. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  52. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  53. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  54. package/screenshots/paseo-top-dashboard.jpg +0 -0
  55. package/screenshots/paseo-top-general-view.jpg +0 -0
  56. package/server/resources.ts +98 -0
  57. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  58. package/server/vendor/paseo-plugin-helper/index.ts +4 -0
  59. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  60. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
  61. package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
  62. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  63. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  64. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  65. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  66. package/server/vendor/paseo-plugin-helper/tickets.ts +1232 -0
  67. package/server/vendor/permission-audit/index.ts +1 -0
  68. package/server/vendor/permission-audit/server.ts +839 -0
  69. package/shared/helper-version.ts +20 -0
  70. package/shared/multi-host.ts +156 -155
  71. package/shared/resources.ts +146 -1
  72. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  73. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  74. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  75. package/shared/vendor/paseo-plugin-helper/index.ts +2 -0
  76. package/shared/vendor/paseo-plugin-helper/tickets.ts +1104 -0
  77. package/shared/vendor/permission-audit/filter.ts +61 -0
  78. package/shared/vendor/permission-audit/index.ts +2 -0
  79. package/shared/vendor/permission-audit/shared.ts +119 -0
  80. package/shared/version.ts +1 -1
  81. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  82. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  83. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -156
  84. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -184
  85. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -225
  86. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  87. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  88. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  89. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  90. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  91. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  92. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -75
  93. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  94. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  95. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  96. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  97. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -118
  98. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -80
  99. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -215
  100. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  101. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  102. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  103. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  104. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  105. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -25
  106. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -224
  107. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  108. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  109. package/client/vendor/paseo-plugin-helper/index.ts +0 -29
  110. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  111. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  112. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  113. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -378
  114. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  115. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  117. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  118. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  119. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  120. package/client/vendor/paseo-plugin-helper/surface.tsx +0 -80
  121. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  122. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  123. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  124. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  125. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  126. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  127. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  128. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  129. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  130. /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
@@ -0,0 +1,486 @@
1
+ import { z } from "zod";
2
+ import type { FeatureModule, FeatureDisposer } from "../../../shared/vendor/paseo-plugin-helper/features";
3
+ import type { PluginRpcContract } from "../../../shared/vendor/paseo-plugin-helper/rpc";
4
+
5
+ /**
6
+ * Raw usage structure as emitted by various Paseo agent engines and providers.
7
+ */
8
+ export interface RawTurnUsage {
9
+ inputTokens?: number;
10
+ outputTokens?: number;
11
+ cachedTokens?: number;
12
+ cachedInputTokens?: number;
13
+ reasoningTokens?: number;
14
+ contextWindowUsedTokens?: number;
15
+ contextWindowMaxTokens?: number;
16
+ totalCostUsd?: number;
17
+ /** Claude Code SDK nested iterations array */
18
+ iterations?: Array<{
19
+ input_tokens?: number;
20
+ cache_creation_input_tokens?: number;
21
+ cache_read_input_tokens?: number;
22
+ output_tokens?: number;
23
+ }>;
24
+ /** OpenCode / ACP tokens object */
25
+ tokens?: {
26
+ input?: number;
27
+ output?: number;
28
+ reasoning?: number;
29
+ cache_read?: number;
30
+ };
31
+ [key: string]: unknown;
32
+ }
33
+
34
+ export interface RawTurnOutcome {
35
+ kind: "completed" | "failed" | "canceled" | string;
36
+ error?: { message: string; code?: string };
37
+ reason?: string;
38
+ stopReason?: string;
39
+ }
40
+
41
+ export interface RawTurnData {
42
+ turnId?: string | null;
43
+ agentId: string;
44
+ repo?: string | null;
45
+ role?: string | null;
46
+ model?: string | null;
47
+ provider?: string | null;
48
+ outcome?: RawTurnOutcome;
49
+ durationMs?: number;
50
+ timeline?: readonly unknown[];
51
+ liveUsage?: RawTurnUsage | null;
52
+ gitBefore?: { insertions: number; deletions: number; filesChanged: number } | null;
53
+ gitAfter?: { insertions: number; deletions: number; filesChanged: number } | null;
54
+ }
55
+
56
+ export const NormalizedTurnTelemetrySchema = z.object({
57
+ turnId: z.string().nullable(),
58
+ agentId: z.string(),
59
+ repo: z.string().nullable(),
60
+ role: z.string().nullable(),
61
+ model: z.string().nullable(),
62
+ provider: z.string().nullable(),
63
+ timestamp: z.string(),
64
+ durationMs: z.number(),
65
+ inputTokens: z.number(),
66
+ outputTokens: z.number(),
67
+ cachedTokens: z.number(),
68
+ reasoningTokens: z.number(),
69
+ totalTokens: z.number(),
70
+ tokensPerSecond: z.number(),
71
+ contextUsedTokens: z.number(),
72
+ contextMaxTokens: z.number(),
73
+ contextUtilizationPct: z.number(),
74
+ costUsd: z.number(),
75
+ toolCallsCount: z.number(),
76
+ toolErrorsCount: z.number(),
77
+ finishReason: z.string(),
78
+ stopReason: z.string().optional(),
79
+ errorMessage: z.string().optional(),
80
+ gitInsertions: z.number(),
81
+ gitDeletions: z.number(),
82
+ gitFilesChanged: z.number(),
83
+ isInterrupted: z.boolean(),
84
+ isSubscriptionZeroCost: z.boolean().optional(),
85
+ });
86
+
87
+ export type NormalizedTurnTelemetry = z.infer<typeof NormalizedTurnTelemetrySchema>;
88
+
89
+ function finite(val: unknown, fallback = 0): number {
90
+ return typeof val === "number" && Number.isFinite(val) ? val : fallback;
91
+ }
92
+
93
+ function round2(val: number): number {
94
+ return Math.round(val * 100) / 100;
95
+ }
96
+
97
+ /**
98
+ * Normalizes vendor/provider quirks into a canonical TurnTelemetry record:
99
+ * - Claude nested iterations array accumulation
100
+ * - OpenCode isolated reasoning tokens
101
+ * - ACP asynchronous context meter readings
102
+ * - Interrupted / canceled turn normalization (prevents division by zero)
103
+ * - Subscription $0 cost normalization
104
+ */
105
+ export function normalizeTurnTelemetry(
106
+ raw: RawTurnData,
107
+ now = Date.now(),
108
+ ): NormalizedTurnTelemetry {
109
+ const usage = raw.liveUsage;
110
+ let inputTokens = 0;
111
+ let outputTokens = 0;
112
+ let cachedTokens = 0;
113
+ let reasoningTokens = 0;
114
+ let contextUsedTokens = 0;
115
+ let contextMaxTokens = 0;
116
+ let costUsd = 0;
117
+
118
+ if (usage) {
119
+ if (Array.isArray(usage.iterations) && usage.iterations.length > 0) {
120
+ // Claude nested iterations normalization
121
+ let iterInput = 0;
122
+ let iterOutput = 0;
123
+ let iterCache = 0;
124
+ for (const iter of usage.iterations) {
125
+ iterInput += finite(iter.input_tokens);
126
+ iterCache += finite(iter.cache_read_input_tokens) + finite(iter.cache_creation_input_tokens);
127
+ iterOutput += finite(iter.output_tokens);
128
+ }
129
+ inputTokens = iterInput;
130
+ cachedTokens = iterCache;
131
+ outputTokens = iterOutput;
132
+ } else {
133
+ inputTokens = finite(usage.inputTokens ?? usage.tokens?.input);
134
+ outputTokens = finite(usage.outputTokens ?? usage.tokens?.output);
135
+ cachedTokens = finite(usage.cachedTokens ?? usage.cachedInputTokens ?? usage.tokens?.cache_read);
136
+ reasoningTokens = finite(usage.reasoningTokens ?? usage.tokens?.reasoning);
137
+ }
138
+
139
+ if (reasoningTokens === 0 && usage.tokens?.reasoning) {
140
+ reasoningTokens = finite(usage.tokens.reasoning);
141
+ }
142
+
143
+ contextUsedTokens = finite(usage.contextWindowUsedTokens);
144
+ contextMaxTokens = finite(usage.contextWindowMaxTokens);
145
+ costUsd = finite(usage.totalCostUsd);
146
+ }
147
+
148
+ // Count timeline tool calls and tool errors if timeline is provided
149
+ let toolCallsCount = 0;
150
+ let toolErrorsCount = 0;
151
+ if (Array.isArray(raw.timeline)) {
152
+ for (const item of raw.timeline) {
153
+ if (!item || typeof item !== "object") continue;
154
+ const type = (item as any).type;
155
+ if (type === "tool_call" || type === "tool_use" || (item as any).toolCall) {
156
+ toolCallsCount++;
157
+ if ((item as any).isError || (item as any).error || (item as any).status === "error") {
158
+ toolErrorsCount++;
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ // Interrupted / canceled normalization
165
+ const kind = raw.outcome?.kind ?? "completed";
166
+ const stopReason = raw.outcome?.stopReason ?? (raw.outcome as any)?.reason;
167
+ const isInterrupted =
168
+ kind === "canceled" ||
169
+ stopReason === "cancelled" ||
170
+ stopReason === "interrupted" ||
171
+ raw.outcome?.reason === "canceled" ||
172
+ raw.outcome?.reason === "interrupted";
173
+
174
+ let finishReason = isInterrupted ? "interrupted" : kind;
175
+ if (kind === "failed") finishReason = "failed";
176
+
177
+ const durationMs = Math.max(0, finite(raw.durationMs));
178
+ const durationSec = durationMs / 1000;
179
+
180
+ // Generation velocity (TPS): sum output + reasoning (OpenCode isolation rule)
181
+ // Interrupted turns with 0 duration or incomplete payloads yield 0 TPS to avoid skewed averages
182
+ const generationTokens = outputTokens + reasoningTokens;
183
+ const tokensPerSecond =
184
+ !isInterrupted && durationSec > 0.05 && generationTokens > 0
185
+ ? round2(generationTokens / durationSec)
186
+ : 0;
187
+
188
+ const contextUtilizationPct =
189
+ contextMaxTokens > 0 ? round2(Math.min(100, (contextUsedTokens / contextMaxTokens) * 100)) : 0;
190
+
191
+ // Git churn diff
192
+ let gitInsertions = 0;
193
+ let gitDeletions = 0;
194
+ let gitFilesChanged = 0;
195
+ if (raw.gitAfter) {
196
+ const before = raw.gitBefore;
197
+ gitInsertions = Math.max(0, raw.gitAfter.insertions - (before?.insertions ?? 0));
198
+ gitDeletions = Math.max(0, raw.gitAfter.deletions - (before?.deletions ?? 0));
199
+ gitFilesChanged = raw.gitAfter.filesChanged;
200
+ }
201
+
202
+ const isSubscriptionZeroCost = costUsd === 0 && (inputTokens > 0 || outputTokens > 0);
203
+
204
+ return {
205
+ turnId: raw.turnId ?? null,
206
+ agentId: raw.agentId,
207
+ repo: raw.repo ?? null,
208
+ role: raw.role ?? null,
209
+ model: raw.model ?? null,
210
+ provider: raw.provider ?? null,
211
+ timestamp: new Date(now).toISOString(),
212
+ durationMs,
213
+ inputTokens,
214
+ outputTokens,
215
+ cachedTokens,
216
+ reasoningTokens,
217
+ totalTokens: inputTokens + outputTokens + cachedTokens + reasoningTokens,
218
+ tokensPerSecond,
219
+ contextUsedTokens,
220
+ contextMaxTokens,
221
+ contextUtilizationPct,
222
+ costUsd: round2(costUsd),
223
+ toolCallsCount,
224
+ toolErrorsCount,
225
+ finishReason,
226
+ stopReason: stopReason ? String(stopReason) : undefined,
227
+ errorMessage: raw.outcome?.error?.message,
228
+ gitInsertions,
229
+ gitDeletions,
230
+ gitFilesChanged,
231
+ isInterrupted,
232
+ isSubscriptionZeroCost,
233
+ };
234
+ }
235
+
236
+ /**
237
+ * 1. Front Desk Suitability Index (FDSI)
238
+ * FDSI = Conversational Conciseness / (TTFT Latency * (Turn Locks + 1) * Token Burn Ratio)
239
+ */
240
+ export interface FDSIInput {
241
+ conciseness: number;
242
+ ttftLatencyMs: number;
243
+ turnLocks: number;
244
+ tokenBurnRatio: number;
245
+ }
246
+
247
+ export function calculateFDSI(input: FDSIInput): number {
248
+ const conciseness = Math.max(0, input.conciseness);
249
+ const ttftSec = Math.max(0.001, input.ttftLatencyMs / 1000);
250
+ const locksFactor = Math.max(1, input.turnLocks + 1);
251
+ const burnRatio = Math.max(1.0, input.tokenBurnRatio);
252
+
253
+ const denominator = ttftSec * locksFactor * burnRatio;
254
+ return round2(conciseness / denominator);
255
+ }
256
+
257
+ /**
258
+ * 2. Worker Implementation ROI (Cost & Churn per Merged PR)
259
+ * Worker ROI = (Net Deliverable Value / (Total Tokens * Wall Clock Time * (Rework Rounds + 1))) / ChurnRatio
260
+ */
261
+ export interface WorkerROIInput {
262
+ netDeliverableValue: number;
263
+ totalTokens: number;
264
+ wallClockTimeMs: number;
265
+ reworkRounds: number;
266
+ linesAdded: number;
267
+ linesDeleted: number;
268
+ netChanges: number;
269
+ }
270
+
271
+ export function calculateWorkerROI(input: WorkerROIInput): number {
272
+ const deliverable = Math.max(0, input.netDeliverableValue);
273
+ const tokens = Math.max(1, input.totalTokens);
274
+ const wallClockSec = Math.max(1, input.wallClockTimeMs / 1000);
275
+ const rework = Math.max(1, input.reworkRounds + 1);
276
+
277
+ const netChanges = Math.max(1, Math.abs(input.netChanges));
278
+ const totalChurn = Math.max(netChanges, input.linesAdded + input.linesDeleted);
279
+ const churnRatio = Math.max(1.0, totalChurn / netChanges);
280
+
281
+ const rawRoi = (deliverable * 1_000_000) / (tokens * wallClockSec * rework);
282
+ return round2(rawRoi / churnRatio);
283
+ }
284
+
285
+ /**
286
+ * 3. Orchestrator Audit Vigilance Score
287
+ * Vigilance = (Flaws Caught in Pre-Flight + False-Positive Claims Debunked) / Total PRs Audited
288
+ */
289
+ export interface OrchestratorVigilanceInput {
290
+ flawsCaught: number;
291
+ falsePositiveClaimsDebunked: number;
292
+ totalPRsAudited: number;
293
+ }
294
+
295
+ export function calculateOrchestratorVigilance(input: OrchestratorVigilanceInput): number {
296
+ const totalAudits = Math.max(1, input.totalPRsAudited);
297
+ const signals = Math.max(0, input.flawsCaught + input.falsePositiveClaimsDebunked);
298
+ return round2(signals / totalAudits);
299
+ }
300
+
301
+ /**
302
+ * 4. Tool Thrashing / Loop Index
303
+ * Frequency of non-zero exit codes (rc != 0) followed immediately by another command
304
+ * without intervening file inspections or edits.
305
+ */
306
+ export interface ToolCallTrace {
307
+ command?: string;
308
+ exitCode?: number;
309
+ isInspectionOrEdit?: boolean;
310
+ }
311
+
312
+ export function calculateToolThrashingIndex(traces: readonly ToolCallTrace[]): number {
313
+ if (traces.length < 2) return 0;
314
+
315
+ let thrashedSequences = 0;
316
+ let totalCommandTransitions = 0;
317
+
318
+ for (let i = 0; i < traces.length - 1; i++) {
319
+ const curr = traces[i]!;
320
+ const next = traces[i + 1]!;
321
+
322
+ if (curr.exitCode !== undefined) {
323
+ totalCommandTransitions++;
324
+ if (curr.exitCode !== 0 && !curr.isInspectionOrEdit && !next.isInspectionOrEdit) {
325
+ thrashedSequences++;
326
+ }
327
+ }
328
+ }
329
+
330
+ if (totalCommandTransitions === 0) return 0;
331
+ return round2((thrashedSequences / totalCommandTransitions) * 100);
332
+ }
333
+
334
+ /**
335
+ * 5. Context Decay & Amnesia Risk
336
+ * Correlation between context utilization (> 75%) and subsequent turn failures, tool loops, or repetitive questions.
337
+ */
338
+ export interface ContextTurnTrace {
339
+ contextUtilizationPct: number;
340
+ failed: boolean;
341
+ hasToolLoop?: boolean;
342
+ }
343
+
344
+ export function calculateContextDecayRisk(turns: readonly ContextTurnTrace[]): number {
345
+ if (turns.length === 0) return 0;
346
+
347
+ const highContextTurns = turns.filter((t) => t.contextUtilizationPct > 75);
348
+ if (highContextTurns.length === 0) return 0;
349
+
350
+ const failuresUnderHighContext = highContextTurns.filter(
351
+ (t) => t.failed || t.hasToolLoop,
352
+ ).length;
353
+
354
+ return round2((failuresUnderHighContext / highContextTurns.length) * 100);
355
+ }
356
+
357
+ export interface TurnTelemetryTapOptions {
358
+ onRecord?: (record: NormalizedTurnTelemetry) => void | Promise<void>;
359
+ onTurnStarted?: (event: any, context: any) => void;
360
+ onTurnEnded?: (event: any, context: any) => void | Promise<void>;
361
+ context?: any;
362
+ }
363
+
364
+ /**
365
+ * Sets up in-process turn telemetry listeners on the Paseo plugin server (#933).
366
+ */
367
+ export function setupTurnTelemetryTap(
368
+ server: any,
369
+ options?: TurnTelemetryTapOptions,
370
+ ): FeatureDisposer {
371
+ const turnStartTimes = new Map<string, number>();
372
+ const lastKnownUsage = new Map<string, RawTurnUsage>();
373
+ const unsubs: Array<() => void> = [];
374
+
375
+ if (typeof server?.on === "function") {
376
+ // 1. Turn started
377
+ try {
378
+ const unsubStart = server.on("agent.turn_started", (event: any, ctx: any) => {
379
+ if (!event?.agent?.id) return;
380
+ turnStartTimes.set(event.agent.id, Date.now());
381
+ if (event.agent.lastUsage) {
382
+ lastKnownUsage.set(event.agent.id, event.agent.lastUsage);
383
+ }
384
+ options?.onTurnStarted?.(event, ctx);
385
+ });
386
+ if (typeof unsubStart === "function") unsubs.push(unsubStart);
387
+ } catch {}
388
+
389
+ // 2. Usage updated
390
+ try {
391
+ const unsubUsage = server.on("agent.usage_updated", (event: any) => {
392
+ if (!event?.agent?.id) return;
393
+ const usage = event.usage ?? event.agent?.lastUsage;
394
+ if (usage) {
395
+ lastKnownUsage.set(event.agent.id, usage);
396
+ }
397
+ });
398
+ if (typeof unsubUsage === "function") unsubs.push(unsubUsage);
399
+ } catch {}
400
+
401
+ // 3. Turn ended
402
+ try {
403
+ const unsubEnd = server.on("agent.turn_ended", async (event: any, ctx: any) => {
404
+ if (!event?.agent?.id) return;
405
+ const agentId = event.agent.id;
406
+ const startTime = turnStartTimes.get(agentId);
407
+ const durationMs = startTime ? Date.now() - startTime : undefined;
408
+ turnStartTimes.delete(agentId);
409
+
410
+ const cachedUsage = lastKnownUsage.get(agentId);
411
+ const effectiveUsage = (event.agent as any)?.lastUsage ?? cachedUsage ?? null;
412
+
413
+ const rawData: RawTurnData = {
414
+ turnId: event.turnId ?? null,
415
+ agentId,
416
+ repo: (event.agent as any)?.repo ?? null,
417
+ role: (event.agent as any)?.role ?? null,
418
+ model: event.agent.model ?? null,
419
+ provider: event.agent.provider ?? null,
420
+ outcome: event.outcome ?? { kind: "completed" },
421
+ durationMs,
422
+ timeline: event.timeline,
423
+ liveUsage: effectiveUsage,
424
+ };
425
+
426
+ try {
427
+ const record = normalizeTurnTelemetry(rawData);
428
+ if (options?.onRecord) {
429
+ await options.onRecord(record);
430
+ }
431
+ if (options?.onTurnEnded) {
432
+ await options.onTurnEnded(event, ctx);
433
+ }
434
+ } catch {
435
+ // Best effort
436
+ }
437
+ });
438
+ if (typeof unsubEnd === "function") unsubs.push(unsubEnd);
439
+ } catch {}
440
+ }
441
+
442
+ let disposed = false;
443
+ return () => {
444
+ if (disposed) return;
445
+ disposed = true;
446
+ for (const unsub of unsubs) {
447
+ try {
448
+ unsub();
449
+ } catch {}
450
+ }
451
+ turnStartTimes.clear();
452
+ lastKnownUsage.clear();
453
+ };
454
+ }
455
+
456
+ export interface TimelineTelemetryFeatureOptions<TServerContext = any, TClientContext = any>
457
+ extends TurnTelemetryTapOptions {
458
+ id?: string;
459
+ contracts?: Record<string, PluginRpcContract>;
460
+ serverTap?: (server: TServerContext) => FeatureDisposer | void;
461
+ clientRenderer?: (client: TClientContext) => FeatureDisposer | void;
462
+ }
463
+
464
+ /**
465
+ * Creates a reusable FeatureModule for timeline telemetry (#933).
466
+ */
467
+ export function createTimelineTelemetryFeature<TServerContext = any, TClientContext = any>(
468
+ options: TimelineTelemetryFeatureOptions<TServerContext, TClientContext> = {},
469
+ ): FeatureModule<TServerContext, TClientContext> {
470
+ return {
471
+ id: options.id || "timeline-telemetry",
472
+ contracts: options.contracts,
473
+ contributeServer(server: TServerContext) {
474
+ if (options.serverTap) {
475
+ return options.serverTap(server);
476
+ }
477
+ return setupTurnTelemetryTap(server, options);
478
+ },
479
+ contributeClient(client: TClientContext) {
480
+ if (options.clientRenderer) {
481
+ return options.clientRenderer(client);
482
+ }
483
+ },
484
+ };
485
+ }
486
+