@ericsanchezok/synergy-workbench 0.0.0-stage → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/AGENTS.md +30 -0
  2. package/README.md +16 -2
  3. package/dist/agents.js +11 -0
  4. package/dist/cli-adapter.js +8 -0
  5. package/dist/component.js +65 -0
  6. package/dist/config-schema.js +265 -0
  7. package/dist/execution/activity.js +78 -0
  8. package/dist/execution/content.js +184 -0
  9. package/dist/execution/json-sections.js +231 -0
  10. package/dist/execution/presentation.js +79 -0
  11. package/dist/execution/preview.js +13 -0
  12. package/dist/execution/process.js +125 -0
  13. package/dist/execution/projection.js +327 -0
  14. package/dist/execution/query.js +18 -0
  15. package/dist/execution/routes.js +189 -0
  16. package/dist/execution/schema.js +322 -0
  17. package/dist/execution/service.js +1993 -0
  18. package/dist/features.js +22 -0
  19. package/dist/http.js +65 -0
  20. package/dist/performance/analysis.js +330 -0
  21. package/dist/performance/catalog.js +391 -0
  22. package/dist/performance/dashboard.js +483 -0
  23. package/dist/performance/error.js +15 -0
  24. package/dist/performance/index.js +6 -0
  25. package/dist/performance/inflight.js +43 -0
  26. package/dist/performance/projection.js +138 -0
  27. package/dist/performance/prompt/base.txt +38 -0
  28. package/dist/performance/prompt/builder.js +21 -0
  29. package/dist/performance/routes/performance-route.js +439 -0
  30. package/dist/performance/schema.js +628 -0
  31. package/dist/performance/timeline.js +98 -0
  32. package/dist/performance/trace-detail.js +91 -0
  33. package/dist/presentation/service.js +270 -0
  34. package/dist/presentation/tools/workbench-view.js +65 -0
  35. package/dist/presentation/tools.js +10 -0
  36. package/dist/project/directories.js +363 -0
  37. package/dist/project/git-health.js +498 -0
  38. package/dist/project/ide.js +66 -0
  39. package/dist/project/migration.js +22 -0
  40. package/dist/project/routes/directories.js +89 -0
  41. package/dist/project/routes/git.js +30 -0
  42. package/dist/project/routes/task-defaults.js +42 -0
  43. package/dist/project/session-health.js +9 -0
  44. package/dist/project/startup.js +28 -0
  45. package/dist/project/task-defaults.js +68 -0
  46. package/dist/project/tools/session-control.js +664 -0
  47. package/dist/project/tools/session-control.txt +60 -0
  48. package/dist/project/tools/worktree-archive.js +76 -0
  49. package/dist/project/tools/worktree-enter.js +175 -0
  50. package/dist/project/tools/worktree-leave.js +119 -0
  51. package/dist/project/tools/worktree-list.js +52 -0
  52. package/dist/project/tools.js +26 -0
  53. package/dist/project/vcs-branch-watcher.js +76 -0
  54. package/dist/project/vcs.js +114 -0
  55. package/dist/project/worktree-janitor.js +140 -0
  56. package/dist/project/worktrees.js +98 -0
  57. package/dist/push/bridge.js +171 -0
  58. package/dist/push/index.js +4 -0
  59. package/dist/push/routes/push.js +224 -0
  60. package/dist/push/service.js +93 -0
  61. package/dist/push/store.js +121 -0
  62. package/dist/push/types.js +58 -0
  63. package/dist/review/git.js +226 -0
  64. package/dist/review/routes.js +45 -0
  65. package/dist/review/schema.js +32 -0
  66. package/dist/review/state.js +79 -0
  67. package/dist/stats/aggregator.js +252 -0
  68. package/dist/stats/canonical.js +89 -0
  69. package/dist/stats/cli/stats.js +302 -0
  70. package/dist/stats/engine.js +176 -0
  71. package/dist/stats/index.js +5 -0
  72. package/dist/stats/rollup.js +460 -0
  73. package/dist/stats/routes/stats.js +94 -0
  74. package/dist/stats/routes/usage.js +83 -0
  75. package/dist/stats/storage.js +86 -0
  76. package/dist/stats/types.js +267 -0
  77. package/dist/worker.js +6 -0
  78. package/package.json +232 -4
  79. package/src/agents.ts +15 -0
  80. package/src/cli-adapter.ts +11 -0
  81. package/src/component.ts +73 -0
  82. package/src/config-schema.ts +289 -0
  83. package/src/execution/activity.ts +83 -0
  84. package/src/execution/content.ts +189 -0
  85. package/src/execution/json-sections.ts +230 -0
  86. package/src/execution/presentation.ts +91 -0
  87. package/src/execution/preview.ts +12 -0
  88. package/src/execution/process.ts +118 -0
  89. package/src/execution/projection.ts +330 -0
  90. package/src/execution/query.ts +19 -0
  91. package/src/execution/routes.ts +324 -0
  92. package/src/execution/schema.ts +336 -0
  93. package/src/execution/service.ts +2149 -0
  94. package/src/features.ts +22 -0
  95. package/src/http.ts +75 -0
  96. package/src/performance/analysis.ts +356 -0
  97. package/src/performance/catalog.ts +902 -0
  98. package/src/performance/dashboard.ts +521 -0
  99. package/src/performance/error.ts +32 -0
  100. package/src/performance/index.ts +6 -0
  101. package/src/performance/inflight.ts +46 -0
  102. package/src/performance/projection.ts +143 -0
  103. package/src/performance/prompt/base.txt +38 -0
  104. package/src/performance/prompt/builder.ts +24 -0
  105. package/src/performance/routes/performance-route.ts +541 -0
  106. package/src/performance/schema.ts +695 -0
  107. package/src/performance/timeline.ts +100 -0
  108. package/src/performance/trace-detail.ts +97 -0
  109. package/src/presentation/service.ts +303 -0
  110. package/src/presentation/tools/workbench-view.ts +75 -0
  111. package/src/presentation/tools.ts +11 -0
  112. package/src/project/directories.ts +391 -0
  113. package/src/project/git-health.ts +577 -0
  114. package/src/project/ide.ts +80 -0
  115. package/src/project/migration.ts +24 -0
  116. package/src/project/routes/directories.ts +124 -0
  117. package/src/project/routes/git.ts +42 -0
  118. package/src/project/routes/task-defaults.ts +50 -0
  119. package/src/project/session-health.ts +10 -0
  120. package/src/project/startup.ts +28 -0
  121. package/src/project/task-defaults.ts +70 -0
  122. package/src/project/tools/session-control.ts +725 -0
  123. package/src/project/tools/session-control.txt +60 -0
  124. package/src/project/tools/worktree-archive.ts +92 -0
  125. package/src/project/tools/worktree-enter.ts +227 -0
  126. package/src/project/tools/worktree-leave.ts +140 -0
  127. package/src/project/tools/worktree-list.ts +68 -0
  128. package/src/project/tools.ts +29 -0
  129. package/src/project/vcs-branch-watcher.ts +81 -0
  130. package/src/project/vcs.ts +124 -0
  131. package/src/project/worktree-janitor.ts +153 -0
  132. package/src/project/worktrees.ts +110 -0
  133. package/src/push/bridge.ts +182 -0
  134. package/src/push/index.ts +4 -0
  135. package/src/push/routes/push.ts +267 -0
  136. package/src/push/service.ts +108 -0
  137. package/src/push/store.ts +120 -0
  138. package/src/push/types.ts +67 -0
  139. package/src/review/git.ts +221 -0
  140. package/src/review/routes.ts +65 -0
  141. package/src/review/schema.ts +34 -0
  142. package/src/review/state.ts +77 -0
  143. package/src/stats/aggregator.ts +277 -0
  144. package/src/stats/canonical.ts +93 -0
  145. package/src/stats/cli/stats.ts +348 -0
  146. package/src/stats/engine.ts +197 -0
  147. package/src/stats/index.ts +5 -0
  148. package/src/stats/rollup.ts +522 -0
  149. package/src/stats/routes/stats.ts +107 -0
  150. package/src/stats/routes/usage.ts +103 -0
  151. package/src/stats/storage.ts +78 -0
  152. package/src/stats/types.ts +351 -0
  153. package/src/worker.ts +7 -0
@@ -0,0 +1,541 @@
1
+ import { RuntimeContext } from "@ericsanchezok/synergy-harness/lifecycle/context"
2
+ import { type Context, Hono } from "hono"
3
+ import { streamSSE } from "hono/streaming"
4
+ import { describeRoute, resolver, validator } from "hono-openapi"
5
+ import z from "zod"
6
+ import { Config } from "@ericsanchezok/synergy-harness/config/config"
7
+ import { ObservabilityConfig } from "@ericsanchezok/synergy-harness/observability/config"
8
+ import { ObservabilityLiveEvents } from "@ericsanchezok/synergy-harness/observability/live-events"
9
+ import { PerformanceError } from "../error"
10
+ import { PerformanceAnalysis } from "../analysis"
11
+ import { ObservabilityBrowserMetrics } from "@ericsanchezok/synergy-harness/observability/browser-metrics"
12
+ import { ObservabilityIssues } from "@ericsanchezok/synergy-harness/observability/issues"
13
+ import { ObservabilityResources } from "@ericsanchezok/synergy-harness/observability/resources"
14
+ import { ObservabilityStore } from "@ericsanchezok/synergy-harness/observability/store"
15
+ import { PerformanceDashboard } from "../dashboard"
16
+ import { PerformanceInflight } from "../inflight"
17
+ import { PerformanceProjection } from "../projection"
18
+ import { ServerSseMetrics } from "@ericsanchezok/synergy-server/server/sse-metrics"
19
+ import { PerformanceSchema } from "../schema"
20
+ import { PerformanceTimeline } from "../timeline"
21
+ import { PerformanceTraceDetail } from "../trace-detail"
22
+ import { Log } from "@ericsanchezok/synergy-harness/util/log"
23
+
24
+ const SummaryQuery = z
25
+ .object({
26
+ windowMs: z.coerce.number().int().min(1000).max(86_400_000).optional(),
27
+ includeInactive: z.coerce.boolean().optional(),
28
+ scopeID: z.string().optional(),
29
+ })
30
+ .meta({ ref: "PerformanceSummaryQuery" })
31
+
32
+ const InflightQuery = z.object({
33
+ scopeID: z.string().optional(),
34
+ sessionID: z.string().optional(),
35
+ staleMs: z.coerce.number().int().positive().optional(),
36
+ limit: z.coerce.number().int().min(1).max(500).optional(),
37
+ })
38
+ const IssuesQuery = z
39
+ .object({
40
+ status: PerformanceSchema.IssueStatus.default("open"),
41
+ severity: PerformanceSchema.IssueSeverity.optional(),
42
+ module: PerformanceSchema.Module.optional(),
43
+ scopeID: z.string().optional(),
44
+ tool: z.string().min(1).optional(),
45
+ since: z.coerce.number().int().nonnegative().optional(),
46
+ until: z.coerce.number().int().nonnegative().optional(),
47
+ limit: z.coerce.number().int().min(1).max(200).optional(),
48
+ })
49
+ .meta({ ref: "PerformanceIssuesQuery" })
50
+
51
+ const TraceDetailQuery = z
52
+ .object({
53
+ includeEvents: z.coerce.boolean().default(true),
54
+ includeAttributes: z.coerce.boolean().default(true),
55
+ maxEvents: z.coerce.number().int().min(1).max(2000).optional(),
56
+ })
57
+ .meta({ ref: "PerformanceTraceDetailQuery" })
58
+
59
+ const ConfigPatch = PerformanceSchema.Config.partial().meta({ ref: "PerformanceConfigPatch" })
60
+
61
+ const restartRequiredFields = new Set(["storage.sqliteEnabled"])
62
+
63
+ const runtimeState = RuntimeContext.state(() => ({
64
+ rateBuckets: new Map<string, { count: number; resetAt: number }>(),
65
+ }))
66
+
67
+ function clientKey(c: Context) {
68
+ return c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip") || "local"
69
+ }
70
+
71
+ function rateLimit(c: Context, bucket: string, limit: number | undefined) {
72
+ const instanceState = runtimeState()
73
+
74
+ const max = limit ?? 60
75
+ const now = Date.now()
76
+ const key = `${bucket}:${clientKey(c)}`
77
+ const current = instanceState.rateBuckets.get(key)
78
+ if (!current || current.resetAt <= now) {
79
+ instanceState.rateBuckets.set(key, { count: 1, resetAt: now + 60_000 })
80
+ return undefined
81
+ }
82
+ current.count++
83
+ if (current.count <= max) return undefined
84
+ const retryAfterMs = Math.max(0, current.resetAt - now)
85
+ c.header("Retry-After", String(Math.ceil(retryAfterMs / 1000)))
86
+ c.header("X-RateLimit-Limit", String(max))
87
+ c.header("X-RateLimit-Remaining", "0")
88
+ c.header("X-RateLimit-Reset", String(current.resetAt))
89
+ return c.json({ code: "PERF_RATE_LIMITED", retryAfterMs }, 429)
90
+ }
91
+
92
+ function rejectLargePayload(c: Context, maxBytes: number) {
93
+ const length = Number(c.req.header("content-length") ?? 0)
94
+ if (!Number.isFinite(length) || length <= maxBytes) return undefined
95
+ return c.json({ code: "PERF_INVALID_METRIC_BATCH", message: "Performance metric batch is too large." }, 413)
96
+ }
97
+
98
+ function fail(c: Context, error: PerformanceError) {
99
+ return c.json(error.toResponse(), error.status as 400)
100
+ }
101
+
102
+ function performanceValidator<T extends z.ZodTypeAny>(
103
+ target: "query" | "json" | "param",
104
+ schema: T,
105
+ code: PerformanceError.Code,
106
+ ) {
107
+ return validator(target, schema, (result, c) => {
108
+ if (result.success) return
109
+ const issues = Array.isArray(result.error)
110
+ ? result.error
111
+ : (result.error as { issues?: unknown[] } | undefined)?.issues
112
+ return c.json({ code, message: "Invalid performance request.", issues }, 400)
113
+ })
114
+ }
115
+
116
+ function handlePerformanceError(c: Context, callback: () => Response | Promise<Response>) {
117
+ try {
118
+ const result = callback()
119
+ if (result instanceof Promise) {
120
+ return result.catch((error) => handleUnknownPerformanceError(c, error))
121
+ }
122
+ return result
123
+ } catch (error) {
124
+ return handleUnknownPerformanceError(c, error)
125
+ }
126
+ }
127
+
128
+ function handleUnknownPerformanceError(c: Context, error: unknown) {
129
+ if (error instanceof PerformanceError) return fail(c, error)
130
+ throw error
131
+ }
132
+
133
+ function ensureStorageAvailable() {
134
+ if (!ObservabilityStore.open()) {
135
+ throw new PerformanceError("PERF_STORAGE_UNAVAILABLE", "Performance SQLite storage is unavailable.", 503)
136
+ }
137
+ }
138
+
139
+ function requiredRestartFields(patch: Partial<PerformanceSchema.Config>) {
140
+ const fields: string[] = []
141
+ if (patch.storage && Object.hasOwn(patch.storage, "sqliteEnabled")) fields.push("storage.sqliteEnabled")
142
+ return fields.filter((field) => restartRequiredFields.has(field))
143
+ }
144
+
145
+ function mergePerformanceConfigPatch(
146
+ current: Awaited<ReturnType<typeof Config.current>>,
147
+ patch: Partial<PerformanceSchema.Config>,
148
+ ) {
149
+ const raw = (current.observability?.performance ?? {}) as ObservabilityConfig.Raw
150
+ return ObservabilityConfig.effective({
151
+ observability: {
152
+ ...current.observability,
153
+ performance: {
154
+ ...raw,
155
+ ...patch,
156
+ rateLimits: { ...(raw.rateLimits ?? {}), ...(patch.rateLimits ?? {}) },
157
+ storage: { ...(raw.storage ?? {}), ...(patch.storage ?? {}) },
158
+ thresholds: { ...(raw.thresholds ?? {}), ...(patch.thresholds ?? {}) },
159
+ },
160
+ },
161
+ })
162
+ }
163
+
164
+ export const PerformanceRoute = () =>
165
+ new Hono()
166
+ .get(
167
+ "/performance/summary",
168
+ describeRoute({
169
+ summary: "Get performance summary",
170
+ description: "Get the local Synergy performance dashboard summary.",
171
+ operationId: "performance.summary",
172
+ responses: {
173
+ 200: {
174
+ description: "Performance summary",
175
+ content: { "application/json": { schema: resolver(PerformanceSchema.DashboardSummary) } },
176
+ },
177
+ },
178
+ }),
179
+ performanceValidator("query", SummaryQuery, "PERF_INVALID_QUERY"),
180
+ async (c) =>
181
+ handlePerformanceError(c, async () => {
182
+ ensureStorageAvailable()
183
+ return (
184
+ rateLimit(c, "summary", ObservabilityConfig.current().rateLimits.summaryPerMinute) ??
185
+ c.json(await PerformanceDashboard.summary(c.req.valid("query")))
186
+ )
187
+ }),
188
+ )
189
+ .post(
190
+ "/performance/analysis",
191
+ describeRoute({
192
+ summary: "Start AI performance analysis",
193
+ description: "Snapshot redacted runtime telemetry and analyze it in one durable top-level Session.",
194
+ operationId: "performance.analysis.start",
195
+ responses: {
196
+ 202: {
197
+ description: "Performance analysis started",
198
+ content: { "application/json": { schema: resolver(PerformanceSchema.AnalysisView) } },
199
+ },
200
+ },
201
+ }),
202
+ performanceValidator("json", PerformanceSchema.AnalysisRequest, "PERF_INVALID_QUERY"),
203
+ async (c) =>
204
+ handlePerformanceError(c, async () => {
205
+ ensureStorageAvailable()
206
+ const limited = rateLimit(c, "analysis", ObservabilityConfig.current().rateLimits.analysisPerMinute)
207
+ if (limited) return limited
208
+ return c.json(await PerformanceAnalysis.start(c.req.valid("json")), 202)
209
+ }),
210
+ )
211
+ .get(
212
+ "/performance/analysis/:sessionID",
213
+ describeRoute({
214
+ summary: "Get AI performance analysis",
215
+ description: "Read live or durable analysis state from its Session messages and runtime.",
216
+ operationId: "performance.analysis.get",
217
+ responses: {
218
+ 200: {
219
+ description: "Performance analysis state",
220
+ content: { "application/json": { schema: resolver(PerformanceSchema.AnalysisView) } },
221
+ },
222
+ },
223
+ }),
224
+ performanceValidator("param", z.object({ sessionID: z.string() }), "PERF_INVALID_QUERY"),
225
+ (c) =>
226
+ handlePerformanceError(c, async () => c.json(await PerformanceAnalysis.get(c.req.valid("param").sessionID))),
227
+ )
228
+ .post(
229
+ "/performance/analysis/:sessionID/cancel",
230
+ describeRoute({
231
+ summary: "Cancel AI performance analysis",
232
+ description: "Cancel a queued or running Performance analysis Session and return its durable state.",
233
+ operationId: "performance.analysis.cancel",
234
+ responses: {
235
+ 200: {
236
+ description: "Performance analysis state",
237
+ content: { "application/json": { schema: resolver(PerformanceSchema.AnalysisView) } },
238
+ },
239
+ },
240
+ }),
241
+ performanceValidator("param", z.object({ sessionID: z.string() }), "PERF_INVALID_QUERY"),
242
+ (c) =>
243
+ handlePerformanceError(c, async () => c.json(await PerformanceAnalysis.cancel(c.req.valid("param").sessionID))),
244
+ )
245
+ .get(
246
+ "/performance/inflight",
247
+ describeRoute({
248
+ summary: "List inflight performance spans",
249
+ description: "List running spans and stale operations from the indexed observability store.",
250
+ operationId: "performance.inflight",
251
+ responses: {
252
+ 200: {
253
+ description: "Inflight performance spans",
254
+ content: { "application/json": { schema: resolver(PerformanceSchema.Inflight) } },
255
+ },
256
+ },
257
+ }),
258
+ performanceValidator("query", InflightQuery, "PERF_INVALID_QUERY"),
259
+ (c) =>
260
+ handlePerformanceError(c, () => {
261
+ ensureStorageAvailable()
262
+ return c.json(PerformanceInflight.get(c.req.valid("query")))
263
+ }),
264
+ )
265
+ .get(
266
+ "/performance/timeline",
267
+ describeRoute({
268
+ summary: "Get performance timeline",
269
+ description: "Get bucketed performance metric series for the selected range.",
270
+ operationId: "performance.timeline",
271
+ responses: {
272
+ 200: {
273
+ description: "Performance timeline",
274
+ content: { "application/json": { schema: resolver(PerformanceSchema.Timeline) } },
275
+ },
276
+ },
277
+ }),
278
+ performanceValidator("query", PerformanceSchema.TimelineQuery, "PERF_INVALID_QUERY"),
279
+ (c) =>
280
+ handlePerformanceError(c, async () => {
281
+ ensureStorageAvailable()
282
+ return (
283
+ rateLimit(c, "timeline", ObservabilityConfig.current().rateLimits.timelinePerMinute) ??
284
+ c.json(await PerformanceTimeline.get(c.req.valid("query")))
285
+ )
286
+ }),
287
+ )
288
+ .get(
289
+ "/performance/traces",
290
+ describeRoute({
291
+ summary: "List performance traces",
292
+ description: "List recent redacted performance traces.",
293
+ operationId: "performance.traces.list",
294
+ responses: {
295
+ 200: {
296
+ description: "Performance traces",
297
+ content: { "application/json": { schema: resolver(PerformanceSchema.TraceList) } },
298
+ },
299
+ },
300
+ }),
301
+ performanceValidator("query", PerformanceSchema.TraceListQuery, "PERF_INVALID_QUERY"),
302
+ (c) =>
303
+ handlePerformanceError(c, () => {
304
+ ensureStorageAvailable()
305
+ return (
306
+ rateLimit(c, "traces", ObservabilityConfig.current().rateLimits.traceListPerMinute) ??
307
+ c.json(PerformanceTraceDetail.list(c.req.valid("query")))
308
+ )
309
+ }),
310
+ )
311
+ .get(
312
+ "/performance/traces/:traceId",
313
+ describeRoute({
314
+ summary: "Get performance trace detail",
315
+ description: "Get one redacted performance trace with spans and related events.",
316
+ operationId: "performance.traces.detail",
317
+ responses: {
318
+ 200: {
319
+ description: "Performance trace detail",
320
+ content: { "application/json": { schema: resolver(PerformanceSchema.TraceDetail) } },
321
+ },
322
+ },
323
+ }),
324
+ performanceValidator("param", z.object({ traceId: z.string() }), "PERF_INVALID_QUERY"),
325
+ performanceValidator("query", TraceDetailQuery, "PERF_INVALID_QUERY"),
326
+ async (c) =>
327
+ handlePerformanceError(c, async () => {
328
+ ensureStorageAvailable()
329
+ return (
330
+ rateLimit(c, "trace-detail", ObservabilityConfig.current().rateLimits.traceDetailPerMinute) ??
331
+ c.json(await PerformanceTraceDetail.detail(c.req.valid("param").traceId, c.req.valid("query")))
332
+ )
333
+ }),
334
+ )
335
+ .get(
336
+ "/performance/issues",
337
+ describeRoute({
338
+ summary: "List filtered performance issues",
339
+ description: "List open or historical performance issues filtered by scope, tool, or last-seen time range.",
340
+ operationId: "performance.issues.list",
341
+ responses: {
342
+ 200: {
343
+ description: "Performance issues",
344
+ content: {
345
+ "application/json": {
346
+ schema: resolver(
347
+ z
348
+ .object({ generatedAt: z.string(), issues: z.array(PerformanceSchema.Issue) })
349
+ .meta({ ref: "PerformanceIssues" }),
350
+ ),
351
+ },
352
+ },
353
+ },
354
+ },
355
+ }),
356
+ performanceValidator("query", IssuesQuery, "PERF_INVALID_QUERY"),
357
+ (c) =>
358
+ handlePerformanceError(c, () => {
359
+ ensureStorageAvailable()
360
+ return (
361
+ rateLimit(c, "issues", ObservabilityConfig.current().rateLimits.issueListPerMinute) ??
362
+ c.json({
363
+ generatedAt: new Date().toISOString(),
364
+ issues: ObservabilityIssues.list(c.req.valid("query")).map(PerformanceProjection.issue),
365
+ })
366
+ )
367
+ }),
368
+ )
369
+ .get(
370
+ "/performance/config",
371
+ describeRoute({
372
+ summary: "Get performance config",
373
+ description: "Get effective performance observability configuration and default metadata.",
374
+ operationId: "performance.config.get",
375
+ responses: {
376
+ 200: {
377
+ description: "Performance config",
378
+ content: {
379
+ "application/json": {
380
+ schema: resolver(
381
+ z
382
+ .object({
383
+ config: PerformanceSchema.Config,
384
+ defaults: PerformanceSchema.Config,
385
+ sources: z.array(z.string()),
386
+ })
387
+ .meta({ ref: "PerformanceConfigResponse" }),
388
+ ),
389
+ },
390
+ },
391
+ },
392
+ },
393
+ }),
394
+ async (c) => {
395
+ const current = await Config.current()
396
+ return c.json({
397
+ config: ObservabilityConfig.effective(current),
398
+ defaults: ObservabilityConfig.defaults,
399
+ sources: ["runtime.observability.performance"],
400
+ })
401
+ },
402
+ )
403
+ .patch(
404
+ "/performance/config",
405
+ describeRoute({
406
+ summary: "Patch performance config",
407
+ description:
408
+ "Validate and persist runtime performance configuration fields in the runtime observability config domain.",
409
+ operationId: "performance.config.update",
410
+ responses: {
411
+ 200: {
412
+ description: "Validated performance config",
413
+ content: { "application/json": { schema: resolver(PerformanceSchema.Config) } },
414
+ },
415
+ },
416
+ }),
417
+ performanceValidator("json", ConfigPatch, "PERF_INVALID_QUERY"),
418
+ async (c) => {
419
+ const limited = rateLimit(c, "config-patch", ObservabilityConfig.current().rateLimits.configPatchPerMinute)
420
+ if (limited) return limited
421
+ const patch = c.req.valid("json")
422
+ const restartFields = requiredRestartFields(patch)
423
+ if (restartFields.length) {
424
+ return c.json({ code: "PERF_CONFIG_RESTART_REQUIRED", fields: restartFields }, 409)
425
+ }
426
+ try {
427
+ const current = await Config.current()
428
+ const nextPerformance = mergePerformanceConfigPatch(current, patch)
429
+ await Config.domainUpdate("runtime", {
430
+ observability: { ...(current.observability ?? {}), performance: nextPerformance },
431
+ })
432
+ ObservabilityConfig.refresh(await Config.current())
433
+ ObservabilityStore.reconfigure()
434
+ ObservabilityResources.reconfigure()
435
+ return c.json(ObservabilityConfig.current())
436
+ } catch (error) {
437
+ Log.create({ service: "performance-route" }).error("Failed to persist performance configuration", { error })
438
+ return c.json({ code: "PERF_CONFIG_CONFLICT", message: "Failed to persist performance configuration." }, 409)
439
+ }
440
+ },
441
+ )
442
+ .post(
443
+ "/performance/browser-metrics",
444
+ describeRoute({
445
+ summary: "Ingest browser performance metrics",
446
+ description: "Validate, redact, and store a batch of frontend/browser performance metrics.",
447
+ operationId: "performance.browserMetrics.ingest",
448
+ responses: {
449
+ 200: {
450
+ description: "Browser metrics ingest result",
451
+ content: { "application/json": { schema: resolver(PerformanceSchema.BrowserMetricIngestResult) } },
452
+ },
453
+ },
454
+ }),
455
+ (c, next) => {
456
+ const tooLarge = rejectLargePayload(c, 256 * 1024)
457
+ if (tooLarge) return tooLarge
458
+ const limited = rateLimit(c, "browser-ingest", ObservabilityConfig.current().rateLimits.browserIngestPerMinute)
459
+ if (limited) return limited
460
+ return next()
461
+ },
462
+ performanceValidator("json", PerformanceSchema.BrowserMetricBatch, "PERF_INVALID_METRIC_BATCH"),
463
+ (c) => handlePerformanceError(c, () => c.json(ObservabilityBrowserMetrics.ingest(c.req.valid("json")))),
464
+ )
465
+ .get(
466
+ "/performance/events",
467
+ describeRoute({
468
+ summary: "Subscribe to performance events",
469
+ description: "Server-sent stream for performance dashboard refresh hints and heartbeats.",
470
+ operationId: "performance.events.stream",
471
+ responses: { 200: { description: "Performance event stream" } },
472
+ }),
473
+ performanceValidator(
474
+ "query",
475
+ z.object({
476
+ scopeID: z.string().optional(),
477
+ sessionID: z.string().optional(),
478
+ includeTraces: z.coerce.boolean().default(false),
479
+ heartbeatMs: z.coerce.number().int().min(5000).max(60000).default(15000),
480
+ sinceEventId: z.string().optional(),
481
+ }),
482
+ "PERF_INVALID_QUERY",
483
+ ),
484
+ (c) => {
485
+ const query = c.req.valid("query")
486
+ if (query.includeTraces && !query.sessionID) return c.json({ code: "PERF_FORBIDDEN" }, 403)
487
+ const limited = rateLimit(c, "sse", ObservabilityConfig.current().rateLimits.sseConnectionsPerClient)
488
+ if (limited) return limited
489
+ c.header("X-Accel-Buffering", "no")
490
+ c.header("Cache-Control", "no-cache, no-transform")
491
+ return streamSSE(c, async (stream) => {
492
+ const connectedAt = Date.now()
493
+ ServerSseMetrics.open("performance")
494
+ await stream.writeSSE({
495
+ event: "performance.summary.updated",
496
+ data: JSON.stringify(await PerformanceDashboard.summary({ scopeID: query.scopeID })),
497
+ })
498
+ let pendingWrites = 0
499
+ const maxPendingWrites = ObservabilityConfig.current().perClientSseQueueSize
500
+ const write = (event: string, data: unknown) => {
501
+ if (pendingWrites >= maxPendingWrites) {
502
+ ServerSseMetrics.writeDropped("performance", event)
503
+ return
504
+ }
505
+ pendingWrites++
506
+ void stream
507
+ .writeSSE({ event, data: JSON.stringify(data) })
508
+ .catch(() => ServerSseMetrics.writeFailure("performance", event))
509
+ .finally(() => pendingWrites--)
510
+ }
511
+ const unsubscribe = ObservabilityLiveEvents.subscribe((event) => {
512
+ if (event.type === "issue.raised" || event.type === "issue.resolved") {
513
+ if (query.scopeID && (event.issue.scopeID ?? event.issue.evidence.scopeID) !== query.scopeID) return
514
+ if (query.sessionID && event.issue.sessionID !== query.sessionID) return
515
+ write(`performance.${event.type}`, PerformanceProjection.issue(event.issue))
516
+ return
517
+ }
518
+ if (event.type === "trace.ended") {
519
+ if (!query.includeTraces) return
520
+ if (query.scopeID && event.trace.scopeID !== query.scopeID) return
521
+ if (query.sessionID && event.trace.sessionID !== query.sessionID) return
522
+ write("performance.trace.ended", PerformanceProjection.traceListItem(event.trace))
523
+ }
524
+ })
525
+ const heartbeat = setInterval(() => {
526
+ void stream
527
+ .writeSSE({ event: "heartbeat", data: JSON.stringify({ time: new Date().toISOString() }) })
528
+ .then(() => ServerSseMetrics.heartbeat("performance"))
529
+ .catch(() => ServerSseMetrics.writeFailure("performance", "heartbeat"))
530
+ }, query.heartbeatMs)
531
+ await new Promise<void>((resolve) => {
532
+ stream.onAbort(() => {
533
+ unsubscribe()
534
+ clearInterval(heartbeat)
535
+ ServerSseMetrics.duration("performance", connectedAt)
536
+ resolve()
537
+ })
538
+ })
539
+ })
540
+ },
541
+ )