@xpufx/paseo-top 0.4.4 → 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 (129) 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 +94 -1
  6. package/client/pill.tsx +690 -614
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +1 -1
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +308 -276
  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} +9 -3
  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} +51 -18
  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/{surface.tsx → lifecycle/surface.tsx} +6 -6
  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} +50 -58
  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 -234
  48. package/package.json +7 -4
  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 +2 -0
  59. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  60. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  61. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  62. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  63. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  64. package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
  65. package/server/vendor/permission-audit/index.ts +1 -0
  66. package/server/vendor/permission-audit/server.ts +839 -0
  67. package/shared/helper-version.ts +1 -1
  68. package/shared/multi-host.ts +153 -159
  69. package/shared/resources.ts +145 -0
  70. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  71. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  72. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  73. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  74. package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
  75. package/shared/vendor/permission-audit/filter.ts +61 -0
  76. package/shared/vendor/permission-audit/index.ts +2 -0
  77. package/shared/vendor/permission-audit/shared.ts +119 -0
  78. package/shared/version.ts +1 -1
  79. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  80. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  81. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
  82. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
  83. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
  84. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  85. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  86. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  87. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  88. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  89. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  90. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
  91. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
  92. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  93. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  94. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  95. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  96. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -132
  97. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
  98. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
  99. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  100. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  101. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  102. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  103. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  104. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -26
  105. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
  106. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  107. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  108. package/client/vendor/paseo-plugin-helper/index.ts +0 -30
  109. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  110. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  111. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  112. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -368
  113. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  114. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  115. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  117. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  118. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  119. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  120. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  121. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  122. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  123. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  124. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  125. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  126. package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
  127. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  128. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  129. /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
+
@@ -1,5 +1,4 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
- import { homedir } from "node:os";
3
2
  import { isAbsolute, join, resolve } from "node:path";
4
3
  import { PluginStorage } from "./storage";
5
4
  import { createPluginLogger } from "./logger";
@@ -13,7 +12,6 @@ import {
13
12
  activeForgeForDirectory,
14
13
  forgeCapabilityFromRepo,
15
14
  forgeContextContract,
16
- forgeForgeContextContract,
17
15
  openIssuesContract,
18
16
  searchIssuesContract,
19
17
  issueDetailContract,
@@ -624,18 +622,20 @@ export async function resolveForgeToken(
624
622
  } catch {}
625
623
  }
626
624
 
627
- const storagePluginId = options?.storagePluginId ?? "forges";
628
- try {
629
- const storage = new PluginStorage<{ tokensByHost?: Record<string, string> }>(
630
- storagePluginId,
631
- "settings.json",
632
- );
633
- const data = await storage.readAsync();
634
- const token = data?.tokensByHost?.[host];
635
- if (typeof token === "string" && token.trim()) {
636
- return token.trim();
637
- }
638
- } catch {}
625
+ // Read ONLY from the caller's own plugin scope — never fall back to 'forges'.
626
+ if (options?.storagePluginId) {
627
+ try {
628
+ const storage = new PluginStorage<{ tokensByHost?: Record<string, string> }>(
629
+ options.storagePluginId,
630
+ "settings.json",
631
+ );
632
+ const data = await storage.readAsync();
633
+ const token = data?.tokensByHost?.[host];
634
+ if (typeof token === "string" && token.trim()) {
635
+ return token.trim();
636
+ }
637
+ } catch {}
638
+ }
639
639
 
640
640
  if (process.env.FORGEJO_TOKEN?.trim()) {
641
641
  return process.env.FORGEJO_TOKEN.trim();
@@ -644,24 +644,6 @@ export async function resolveForgeToken(
644
644
  return process.env.GITEA_TOKEN.trim();
645
645
  }
646
646
 
647
- try {
648
- const teaConfigPath = join(homedir(), ".config", "tea", "config.yml");
649
- const content = await readFile(teaConfigPath, "utf8");
650
- const lines = content.split("\n");
651
- let currentLoginMatches = false;
652
- for (const line of lines) {
653
- if (line.includes(host) || line.includes("https://" + host)) {
654
- currentLoginMatches = true;
655
- } else if (line.trim().startsWith("- name:")) {
656
- currentLoginMatches = line.includes(host);
657
- }
658
- if (currentLoginMatches && line.trim().startsWith("token:")) {
659
- const token = line.replace(/.*token:\s*/, "").trim();
660
- if (token) return token;
661
- }
662
- }
663
- } catch {}
664
-
665
647
  return undefined;
666
648
  }
667
649
 
@@ -672,45 +654,15 @@ export async function resolveDefaultForgeHost(): Promise<string> {
672
654
  if (process.env.GITEA_HOST?.trim()) {
673
655
  return process.env.GITEA_HOST.trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
674
656
  }
675
- try {
676
- const teaConfigPath = join(homedir(), ".config", "tea", "config.yml");
677
- const content = await readFile(teaConfigPath, "utf8");
678
- const lines = content.split("\n");
679
- let defaultLoginUrl = "";
680
- let firstLoginUrl = "";
681
- let inDefaultLogin = false;
682
- for (const line of lines) {
683
- if (line.trim().startsWith("- name:")) {
684
- inDefaultLogin = false;
685
- }
686
- if (line.trim().startsWith("default: true")) {
687
- inDefaultLogin = true;
688
- }
689
- if (line.trim().startsWith("url:") || line.trim().startsWith("ssh_host:")) {
690
- const urlOrHost = line.replace(/.*(?:url|ssh_host):\s*/, "").trim();
691
- if (urlOrHost) {
692
- const host = urlOrHost.replace(/^https?:\/\//, "").replace(/\/+$/, "");
693
- if (inDefaultLogin && !defaultLoginUrl) {
694
- defaultLoginUrl = host;
695
- }
696
- if (!firstLoginUrl) {
697
- firstLoginUrl = host;
698
- }
699
- }
700
- }
701
- }
702
- if (defaultLoginUrl) return defaultLoginUrl;
703
- if (firstLoginUrl) return firstLoginUrl;
704
- } catch {}
705
657
 
706
658
  return "forge.mrs.uppidi.com";
707
659
  }
708
660
 
709
661
  export async function storedForgeSelection(
710
662
  directory: string | undefined,
711
- storagePluginId = "forges",
663
+ storagePluginId?: string,
712
664
  ): Promise<string | undefined> {
713
- if (!directory) return undefined;
665
+ if (!directory || !storagePluginId) return undefined;
714
666
  try {
715
667
  const storage = new PluginStorage<Record<string, any>>(storagePluginId, "settings.json");
716
668
  const settings = await storage.readAsync();
@@ -781,8 +733,8 @@ export interface TicketHandlers {
781
733
  }
782
734
 
783
735
  export function createTicketHandlers(options: TicketHandlerOptions = {}): TicketHandlers {
784
- const storagePluginId = options.storagePluginId ?? "forges";
785
- const log = createPluginLogger(storagePluginId, { banner: false });
736
+ const storagePluginId = options.storagePluginId; // no 'forges' fallback
737
+ const log = createPluginLogger(storagePluginId || "tickets", { banner: false });
786
738
  const guard = options.guard ?? new ForgeGuard();
787
739
 
788
740
  function logAt(level: GuardLogLevel, message: string, context: Record<string, unknown>): void {
@@ -1271,7 +1223,6 @@ export function registerTicketHandlers(
1271
1223
  server.handle(openIssuesContract, (input: OpenIssuesInput) => handlers.handleOpenIssues(input));
1272
1224
  server.handle(searchIssuesContract, (input: SearchIssuesInput) => handlers.handleSearchIssues(input));
1273
1225
  server.handle(forgeContextContract, (input: ForgeContextInput) => handlers.handleForgeContext(input));
1274
- server.handle(forgeForgeContextContract, (input: ForgeContextInput) => handlers.handleForgeContext(input));
1275
1226
  server.handle(issueDetailContract, (input: IssueDetailInput) => handlers.handleIssueDetail(input));
1276
1227
  server.handle(setLabelContract, (input: SetLabelInput) => handlers.handleSetLabel(input));
1277
1228
  server.handle(addCommentContract, (input: AddCommentInput) => handlers.handleAddComment(input));
@@ -0,0 +1 @@
1
+ export * from "./server.js";