@xpufx/paseo-top 0.4.4 → 0.4.7

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 +18 -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 +692 -616
  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/npm-shrinkwrap.json +44 -0
  49. package/package.json +8 -4
  50. package/paseo-plugin.json +2 -0
  51. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  52. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  53. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  54. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  55. package/screenshots/paseo-top-dashboard.jpg +0 -0
  56. package/screenshots/paseo-top-general-view.jpg +0 -0
  57. package/server/resources.ts +98 -0
  58. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  59. package/server/vendor/paseo-plugin-helper/index.ts +2 -0
  60. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  61. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  62. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  63. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  64. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  65. package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
  66. package/server/vendor/permission-audit/index.ts +1 -0
  67. package/server/vendor/permission-audit/server.ts +839 -0
  68. package/shared/helper-version.ts +1 -1
  69. package/shared/multi-host.ts +153 -159
  70. package/shared/resources.ts +145 -0
  71. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  72. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  73. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  74. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  75. package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
  76. package/shared/vendor/permission-audit/filter.ts +61 -0
  77. package/shared/vendor/permission-audit/index.ts +2 -0
  78. package/shared/vendor/permission-audit/shared.ts +119 -0
  79. package/shared/version.ts +1 -1
  80. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  81. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  82. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
  83. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
  84. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
  85. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  86. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  87. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  88. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  89. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  90. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  91. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
  92. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
  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 -132
  98. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
  99. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
  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 -26
  106. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
  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 -30
  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 -368
  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/theme/flair.ts +0 -76
  121. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  122. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  123. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  124. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  125. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  126. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  127. package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
  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
@@ -7,7 +7,7 @@ export interface PluginStorageOptions<T> {
7
7
  defaultData?: T;
8
8
  /**
9
9
  * Base directory override. When provided, storage is located at path.join(baseDir, pluginId).
10
- * When omitted, defaults to path.join(os.homedir(), ".paseo", namespace ?? "plugin-data/xpufx", pluginId).
10
+ * When omitted, defaults to path.join(resolvePaseoHome(), namespace ?? "plugin-data/xpufx", pluginId).
11
11
  */
12
12
  baseDir?: string;
13
13
  /**
@@ -27,6 +27,40 @@ export interface StorageStats {
27
27
  lastModified: Date | null;
28
28
  }
29
29
 
30
+ const AGENT_MUX_PROFILE_HOME_RE = /(?:^|[\\/])\.agent-mux[\\/]profiles[\\/]/;
31
+
32
+ /**
33
+ * Resolve the real user home even when `HOME` points into an agent-mux profile.
34
+ * A profile session has `HOME=~/.agent-mux/profiles/<provider>/<profile>` while
35
+ * the live Paseo state stays on the host home. agent-mux exports `REAL_HOME` for
36
+ * every profile session, so prefer it; otherwise strip the profile prefix so
37
+ * paths under `~/.paseo` still resolve to the host (#1158, #973).
38
+ */
39
+ export function resolveHostHome(env: NodeJS.ProcessEnv = process.env): string {
40
+ const home = (env.HOME || os.homedir() || "").trim();
41
+ if (!home || !AGENT_MUX_PROFILE_HOME_RE.test(home)) {
42
+ return home;
43
+ }
44
+ const realHome = (env.REAL_HOME || "").trim();
45
+ if (realHome) return realHome;
46
+ const marker = home.match(AGENT_MUX_PROFILE_HOME_RE);
47
+ if (marker && typeof marker.index === "number" && marker.index > 0) {
48
+ return home.slice(0, marker.index);
49
+ }
50
+ return home;
51
+ }
52
+
53
+ /**
54
+ * Resolve the canonical `~/.paseo` directory. `PASEO_HOME` is the daemon's own
55
+ * declaration of that root, so prefer it; otherwise anchor to the host home
56
+ * rather than an agent-mux profile (#1158).
57
+ */
58
+ export function resolvePaseoHome(env: NodeJS.ProcessEnv = process.env): string {
59
+ const paseoHome = (env.PASEO_HOME || "").trim();
60
+ if (paseoHome) return paseoHome;
61
+ return path.join(resolveHostHome(env), ".paseo");
62
+ }
63
+
30
64
  export const DEFAULT_NAMESPACE_README = `# Paseo Plugins Storage (xpufx)
31
65
 
32
66
  This directory is managed by \`paseo-plugin-helper\` to store persistent settings, cached metrics, and state for plugins.
@@ -60,7 +94,7 @@ export class PluginStorage<T extends Record<string, any>> {
60
94
  this.namespaceDir = options.baseDir;
61
95
  this.pluginDir = path.join(options.baseDir, pluginId);
62
96
  } else {
63
- this.namespaceDir = path.join(os.homedir(), ".paseo", namespace);
97
+ this.namespaceDir = path.join(resolvePaseoHome(), namespace);
64
98
  this.pluginDir = path.join(this.namespaceDir, pluginId);
65
99
  }
66
100
 
@@ -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";