@ericsanchezok/synergy-workbench 0.0.0-stage → 3.1.1

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,521 @@
1
+ import { ResourceSources } from "@ericsanchezok/synergy-harness/observability/resource-sources"
2
+ import { RuntimeContext } from "@ericsanchezok/synergy-harness/lifecycle/context"
3
+ import { Cortex } from "@ericsanchezok/synergy-harness/cortex"
4
+ import { Diagnostics } from "@ericsanchezok/synergy-harness/observability/diagnostics"
5
+ import { ObservabilityIssues } from "@ericsanchezok/synergy-harness/observability/issues"
6
+ import { ObservabilityStore } from "@ericsanchezok/synergy-harness/observability/store"
7
+ import { SessionManager } from "@ericsanchezok/synergy-harness/session/manager"
8
+ import { SessionMessageCache } from "@ericsanchezok/synergy-harness/session/message-cache"
9
+ import { LLMTurnMemory } from "@ericsanchezok/synergy-harness/session/llm-memory"
10
+ import { parseJson } from "@ericsanchezok/synergy-harness/util/json-parse"
11
+ import { PerformanceProjection } from "./projection"
12
+ import { PerformanceSchema } from "./schema"
13
+ import { AgentTurn } from "@ericsanchezok/synergy-harness/session/agent-turn"
14
+ import { readRuntimeStats } from "@ericsanchezok/synergy-harness/lifecycle"
15
+ import { PolicyWorker } from "@ericsanchezok/synergy-harness/enforcement/policy-worker"
16
+ import { ProcessRegistry } from "@ericsanchezok/synergy-harness/process/registry"
17
+ import { SessionMemoryPressure } from "@ericsanchezok/synergy-harness/session/memory-pressure"
18
+
19
+ export namespace PerformanceDashboard {
20
+ type MetricRow = ObservabilityStore.StoredMetric
21
+
22
+ const SUMMARY_CACHE_MS = 1000
23
+ const runtimeState = RuntimeContext.state(() => ({
24
+ cached: undefined as
25
+ | {
26
+ key: string
27
+ version: number
28
+ pending: boolean
29
+ at: number
30
+ value: Promise<PerformanceSchema.DashboardSummary>
31
+ }
32
+ | undefined,
33
+ labelsCache: new WeakMap<object, Record<string, unknown>>(),
34
+ }))
35
+
36
+ // Ranked and latest detail can reuse the same row across several projections.
37
+ function metricLabels(row: object): Record<string, unknown> {
38
+ const instanceState = runtimeState()
39
+
40
+ const cachedLabels = instanceState.labelsCache.get(row)
41
+ if (cachedLabels) return cachedLabels
42
+ const labels = parseJson((row as { labels_json: string }).labels_json)
43
+ instanceState.labelsCache.set(row, labels)
44
+ return labels
45
+ }
46
+
47
+ export function summary(
48
+ input: { windowMs?: number; scopeID?: string } = {},
49
+ ): Promise<PerformanceSchema.DashboardSummary> {
50
+ const instanceState = runtimeState()
51
+
52
+ const key = `${input.windowMs ?? 300_000}:${input.scopeID ?? ""}`
53
+ const version = ObservabilityStore.dataVersion()
54
+ const now = Date.now()
55
+ if (
56
+ instanceState.cached?.key === key &&
57
+ (instanceState.cached.pending ||
58
+ (instanceState.cached.version === version && now - instanceState.cached.at < SUMMARY_CACHE_MS))
59
+ )
60
+ return instanceState.cached.value
61
+ const value = computeSummary(input)
62
+ const wrapped = value
63
+ .then((result) => {
64
+ if (instanceState.cached?.value === wrapped) {
65
+ instanceState.cached.pending = false
66
+ instanceState.cached.at = Date.now()
67
+ }
68
+ return result
69
+ })
70
+ .catch((error) => {
71
+ const instanceState = runtimeState()
72
+
73
+ if (instanceState.cached?.value === wrapped) instanceState.cached = undefined
74
+ throw error
75
+ })
76
+ instanceState.cached = { key, version, pending: true, at: now, value: wrapped }
77
+ return wrapped
78
+ }
79
+ async function computeSummary(
80
+ input: { windowMs?: number; scopeID?: string } = {},
81
+ ): Promise<PerformanceSchema.DashboardSummary> {
82
+ const windowMs = Math.max(1000, Math.min(input.windowMs ?? 300_000, 86_400_000))
83
+ // Storage uses an exclusive upper bound; include observations in the current millisecond.
84
+ const until = Date.now() + 1
85
+ const since = until - windowMs
86
+ const filter = { since, until, scopeID: input.scopeID }
87
+ const aggregate = async (name: string) =>
88
+ (
89
+ await ObservabilityStore.queryMetricBuckets({
90
+ since,
91
+ until,
92
+ scopeID: input.scopeID,
93
+ names: [name],
94
+ bucketMs: windowMs + 1,
95
+ })
96
+ )[0]
97
+ const [requests, turnSummary, resourceSummary, counts, toolSums, activeSessions] = await Promise.all([
98
+ aggregate("http.request.duration"),
99
+ aggregate("session.turn.duration"),
100
+ aggregate("frontend.resource.duration"),
101
+ ObservabilityStore.queryMetricCounts(filter),
102
+ ObservabilityStore.queryMetricSums({ ...filter, names: ["tool.execution.count", "tool.execution.error"] }),
103
+ ObservabilityStore.countMetricSessions({ ...filter, since: Math.max(since, until - 300_000) }),
104
+ ])
105
+ const highlightNames = counts
106
+ .filter(
107
+ (row) =>
108
+ (row.module === "llm" && row.name.endsWith(".duration")) ||
109
+ [
110
+ "http.request.duration",
111
+ "session.turn.duration",
112
+ "tool.execution.duration",
113
+ "storage.operation.duration",
114
+ "library.operation.duration",
115
+ "frontend.resource.duration",
116
+ "frontend.long_task.duration",
117
+ "frontend.web_vital",
118
+ "service.memory.current",
119
+ "service.memory.working_set",
120
+ "service.memory.reclaimable",
121
+ "service.memory.peak",
122
+ ].includes(row.name),
123
+ )
124
+ .map((row) => row.name)
125
+ const metrics = highlightNames.length
126
+ ? await ObservabilityStore.queryMetricHighlights({ ...filter, names: [...new Set(highlightNames)] })
127
+ : []
128
+ const count = (name: string) => counts.filter((row) => row.name === name).reduce((sum, row) => sum + row.count, 0)
129
+ const serverFilter = { role: "server", until, limit: 1 } as const
130
+ const resources = ObservabilityStore.resourceSince(since, { ...serverFilter, newestFirst: true })[0]
131
+ const serverBaseline = ObservabilityStore.resourceSince(since, serverFilter)[0]
132
+ const currentChildRows = resources
133
+ ? ObservabilityStore.resourceSince(resources.time, {
134
+ role: "tool",
135
+ until: resources.time + 1,
136
+ limit: 50_000,
137
+ })
138
+ : []
139
+ const [serverResources, childResources] = await Promise.all([
140
+ ObservabilityStore.resourceSummary({ since, until, role: "server" }),
141
+ resources
142
+ ? ObservabilityStore.resourceSummary({ since: resources.time, until: resources.time + 1, role: "tool" })
143
+ : undefined,
144
+ ])
145
+ const childProcesses = rankChildProcesses(currentChildRows)
146
+ const issueCounts = ObservabilityStore.countIssues({ status: "open", scopeID: input.scopeID, since })
147
+ const issues = ObservabilityIssues.list({
148
+ status: "open",
149
+ scopeID: input.scopeID,
150
+ since,
151
+ limit: 20,
152
+ }).map(PerformanceProjection.issue)
153
+ const diagnostics = await Diagnostics.summary().catch(() => undefined)
154
+ const runtimeStats = SessionManager.runtimeStats()
155
+ const agentWorkers = AgentTurn.stats()
156
+ const policyWorkers = PolicyWorker.stats()
157
+ const { toolTasks } = readRuntimeStats()
158
+ const contributedResources = await ResourceSources.read()
159
+ const localProcesses = await ProcessRegistry.resourceStats()
160
+ const controlPlane = SessionMemoryPressure.stats()
161
+ const messageCacheStats = SessionMessageCache.stats()
162
+ const llmTurnStats = LLMTurnMemory.stats()
163
+ const activeLLMTurns = LLMTurnMemory.activeSnapshot(20)
164
+ const cortexTasks = Cortex.list()
165
+ const cortexStats = {
166
+ totalCount: cortexTasks.length,
167
+ byStatus: {
168
+ queued: cortexTasks.filter((task) => task.status === "queued").length,
169
+ running: cortexTasks.filter((task) => task.status === "running").length,
170
+ completed: cortexTasks.filter((task) => task.status === "completed").length,
171
+ error: cortexTasks.filter((task) => task.status === "error").length,
172
+ cancelled: cortexTasks.filter((task) => task.status === "cancelled").length,
173
+ interrupted: cortexTasks.filter((task) => task.status === "interrupted").length,
174
+ },
175
+ retainedPromptChars: cortexTasks.reduce((sum, task) => sum + task.prompt.length, 0),
176
+ retainedOutputChars: cortexTasks.reduce((sum, task) => {
177
+ if (!task.output) return sum
178
+ if (task.output.mode === "summary" || task.output.mode === "final_response")
179
+ return sum + task.output.value.length
180
+ try {
181
+ return sum + JSON.stringify(task.output.value).length
182
+ } catch {
183
+ return sum
184
+ }
185
+ }, 0),
186
+ retainedErrorChars: cortexTasks.reduce((sum, task) => sum + (task.error?.length ?? 0), 0),
187
+ retainedProgressToolCount: cortexTasks.reduce((sum, task) => sum + (task.progress?.recentTools?.length ?? 0), 0),
188
+ }
189
+ const http = metrics.filter((row) => row.name === "http.request.duration")
190
+ const turns = metrics.filter((row) => row.name === "session.turn.duration")
191
+ const llm = metrics.filter((row) => row.module === "llm" && row.name.endsWith(".duration"))
192
+ const tools = metrics.filter((row) => row.name === "tool.execution.duration")
193
+ const toolFailures = rankToolFailures(toolSums)
194
+ const storage = metrics.filter((row) => row.name === "storage.operation.duration")
195
+ const library = metrics.filter((row) => row.name === "library.operation.duration")
196
+ const frontendResources = metrics.filter((row) => row.name === "frontend.resource.duration")
197
+ const frontendLongTasks = metrics.filter((row) => row.name === "frontend.long_task.duration")
198
+ const frontendVital = (name: string) =>
199
+ metrics.find((row) => row.name === "frontend.web_vital" && metricLabels(row).name === name)?.value
200
+ const criticalIssueCount = issueCounts.critical
201
+ const score = Math.max(0, 100 - criticalIssueCount * 40 - issueCounts.error * 20 - issueCounts.warning * 8)
202
+ const status = criticalIssueCount > 0 ? "critical" : issueCounts.total > 0 ? "degraded" : "healthy"
203
+ const serverPeakBytes = serverResources?.peakRssBytes ?? undefined
204
+ const currentServerBytes = resources?.memory_rss_bytes ?? undefined
205
+ const serverBaselineBytes = serverBaseline?.memory_rss_bytes ?? undefined
206
+ const latestMetric = (name: string) => metrics.find((row) => row.name === name)?.value
207
+ const owners: PerformanceSchema.DashboardSummary["resources"]["owners"] = [
208
+ {
209
+ owner: "control_plane",
210
+ processCount: 1,
211
+ measuredProcessCount: currentServerBytes === undefined ? 0 : 1,
212
+ currentBytes: currentServerBytes,
213
+ peakBytes: serverPeakBytes ?? currentServerBytes,
214
+ baselineBytes: serverBaselineBytes,
215
+ retainedBytes:
216
+ currentServerBytes === undefined || serverBaselineBytes === undefined
217
+ ? undefined
218
+ : Math.max(0, currentServerBytes - serverBaselineBytes),
219
+ heapUsedBytes: resources?.memory_heap_used_bytes ?? undefined,
220
+ externalBytes: resources?.memory_external_bytes ?? undefined,
221
+ arrayBuffersBytes: resources?.memory_array_buffers_bytes ?? undefined,
222
+ source: "process_api",
223
+ completeness: currentServerBytes === undefined ? "unavailable" : "full",
224
+ attributes: {
225
+ pressure: controlPlane.lastAssessment?.pressure ?? "unknown",
226
+ processPressure: controlPlane.lastAssessment?.processPressure ?? "unknown",
227
+ servicePressure: controlPlane.lastAssessment?.servicePressure ?? "unknown",
228
+ },
229
+ lastRecovery: controlPlane.lastRecovery,
230
+ },
231
+ {
232
+ owner: "agent",
233
+ processCount: agentWorkers.workers,
234
+ measuredProcessCount: agentWorkers.measuredWorkers,
235
+ currentBytes: agentWorkers.rssBytes,
236
+ peakBytes: agentWorkers.peakBytes,
237
+ baselineBytes: agentWorkers.baselineBytes,
238
+ retainedBytes: agentWorkers.retainedBytes,
239
+ heapUsedBytes: agentWorkers.heapUsedBytes,
240
+ externalBytes: agentWorkers.externalBytes,
241
+ arrayBuffersBytes: agentWorkers.arrayBuffersBytes,
242
+ source: "worker_ipc",
243
+ completeness:
244
+ agentWorkers.workers === 0 || agentWorkers.measuredWorkers === agentWorkers.workers ? "full" : "partial",
245
+ attributes: { active: agentWorkers.active, queued: agentWorkers.queued },
246
+ lastRecovery: agentWorkers.lastRecovery,
247
+ },
248
+ {
249
+ owner: "policy",
250
+ processCount: policyWorkers.workers,
251
+ measuredProcessCount: policyWorkers.measuredWorkers,
252
+ currentBytes: policyWorkers.rssBytes,
253
+ peakBytes: policyWorkers.peakBytes,
254
+ baselineBytes: policyWorkers.baselineBytes,
255
+ retainedBytes: policyWorkers.retainedBytes,
256
+ heapUsedBytes: policyWorkers.heapUsedBytes,
257
+ externalBytes: policyWorkers.externalBytes,
258
+ arrayBuffersBytes: policyWorkers.arrayBuffersBytes,
259
+ source: "worker_ipc",
260
+ completeness:
261
+ policyWorkers.workers === 0 || policyWorkers.measuredWorkers === policyWorkers.workers ? "full" : "partial",
262
+ attributes: { active: policyWorkers.active, queued: policyWorkers.queued },
263
+ lastRecovery: policyWorkers.lastRecovery,
264
+ },
265
+ ...contributedResources,
266
+ {
267
+ owner: "local_process",
268
+ ...localProcesses,
269
+ source: "process_registry",
270
+ completeness:
271
+ localProcesses.processCount === 0 || localProcesses.measuredProcessCount === localProcesses.processCount
272
+ ? "full"
273
+ : "partial",
274
+ attributes: {
275
+ stdioOpen: localProcesses.stdio.open,
276
+ stdioDraining: localProcesses.stdio.draining,
277
+ stdioClosed: localProcesses.stdio.closed,
278
+ timedOut: localProcesses.stdio.timedOut,
279
+ drainTimedOut: localProcesses.stdio.drainTimedOut,
280
+ descendantPipeGraceMs: localProcesses.stdio.descendantPipeGraceMs,
281
+ },
282
+ },
283
+ ]
284
+ return PerformanceSchema.DashboardSummary.parse({
285
+ generatedAt: new Date().toISOString(),
286
+ windowMs,
287
+ health: { status, score, openIssueCount: issueCounts.total, criticalIssueCount },
288
+ backend: {
289
+ requestCount: requests?.count ?? 0,
290
+ errorRate: requests ? requests.errors / requests.count : 0,
291
+ p50RequestMs: requests?.p50,
292
+ p95RequestMs: requests?.p95,
293
+ p99RequestMs: requests?.p99,
294
+ activeSessions,
295
+ pendingSessions: diagnostics?.sessions.paused.length ?? 0,
296
+ },
297
+ resources: {
298
+ rssBytes: resources?.memory_rss_bytes ?? undefined,
299
+ heapUsedBytes: resources?.memory_heap_used_bytes ?? undefined,
300
+ heapTotalBytes: resources?.memory_heap_total_bytes ?? undefined,
301
+ externalBytes: resources?.memory_external_bytes ?? undefined,
302
+ arrayBuffersBytes: resources?.memory_array_buffers_bytes ?? undefined,
303
+ cpuUtilizationRatio: resources?.cpu_utilization_ratio ?? undefined,
304
+ eventLoopLagP95Ms: serverResources?.eventLoopLagP95Ms ?? undefined,
305
+ appReadBytes: resources?.app_read_bytes ?? undefined,
306
+ appWrittenBytes: resources?.app_written_bytes ?? undefined,
307
+ appReadOps: resources?.app_read_ops ?? undefined,
308
+ appWriteOps: resources?.app_write_ops ?? undefined,
309
+ childProcessCount: childResources?.count ?? 0,
310
+ measuredChildProcessCount: childResources?.measuredCount ?? 0,
311
+ serviceMemory:
312
+ resources?.service_memory_source && resources.service_memory_completeness
313
+ ? {
314
+ rssBytes: resources.service_memory_rss_bytes ?? undefined,
315
+ currentBytes: latestMetric("service.memory.current") ?? resources.service_memory_rss_bytes ?? undefined,
316
+ workingSetBytes: latestMetric("service.memory.working_set"),
317
+ reclaimableBytes: latestMetric("service.memory.reclaimable"),
318
+ peakBytes: latestMetric("service.memory.peak"),
319
+ source: resources.service_memory_source,
320
+ completeness: resources.service_memory_completeness,
321
+ }
322
+ : undefined,
323
+ childProcessRssBytes: childResources?.rssBytes ?? 0,
324
+ owners,
325
+ },
326
+ sessions: {
327
+ turnCount: turnSummary?.count ?? 0,
328
+ p95TurnMs: turnSummary?.p95,
329
+ llmCallCount: counts
330
+ .filter((row) => row.module === "llm" && row.name.endsWith(".duration"))
331
+ .reduce((sum, row) => sum + row.count, 0),
332
+ toolCallCount: count("tool.execution.duration"),
333
+ },
334
+ frontend: {
335
+ inpMs: frontendVital("INP"),
336
+ lcpMs: frontendVital("LCP"),
337
+ cls: frontendVital("CLS"),
338
+ fcpMs: frontendVital("FCP"),
339
+ ttfbMs: frontendVital("TTFB"),
340
+ longTaskCount: count("frontend.long_task.duration"),
341
+ resourceP95Ms: resourceSummary?.p95,
342
+ },
343
+ runtime: {
344
+ alive:
345
+ diagnostics?.lock?.inspection && typeof diagnostics.lock.inspection === "object"
346
+ ? (diagnostics.lock.inspection as { alive?: boolean }).alive
347
+ : undefined,
348
+ healthy:
349
+ diagnostics?.lock?.inspection && typeof diagnostics.lock.inspection === "object"
350
+ ? (diagnostics.lock.inspection as { healthy?: boolean }).healthy
351
+ : undefined,
352
+ pid:
353
+ diagnostics?.lock?.lock && typeof diagnostics.lock.lock === "object"
354
+ ? (diagnostics.lock.lock as { pid?: number }).pid
355
+ : undefined,
356
+ mode:
357
+ diagnostics?.lock?.lock && typeof diagnostics.lock.lock === "object"
358
+ ? (diagnostics.lock.lock as { mode?: string }).mode
359
+ : undefined,
360
+ mirrorFiles: diagnostics?.traces.files.length ?? 0,
361
+ traceFiles: diagnostics?.traces.files.length ?? 0,
362
+ recentErrors: diagnostics?.traces.recentErrors.length ?? 0,
363
+ pendingSessions: diagnostics?.sessions.paused.length ?? 0,
364
+ sessionRuntimes: runtimeStats,
365
+ execution: {
366
+ agentWorkers,
367
+ policyWorkers,
368
+ toolTasks,
369
+ },
370
+ messageCache: {
371
+ totalBytes: messageCacheStats.totalBytes,
372
+ activeCount: messageCacheStats.activeCount,
373
+ entryCount: messageCacheStats.entryCount,
374
+ hits: messageCacheStats.hits,
375
+ misses: messageCacheStats.misses,
376
+ evictions: messageCacheStats.evictions,
377
+ protectedOverbudget: messageCacheStats.protectedOverbudget,
378
+ entries: messageCacheStats.entries.map((entry) => ({ estimatedBytes: entry.estimatedBytes })),
379
+ truncatedEntryCount: messageCacheStats.truncatedEntryCount,
380
+ },
381
+ llmTurns: {
382
+ ...llmTurnStats,
383
+ turns: activeLLMTurns.map((turn) => ({
384
+ ageMs: turn.ageMs,
385
+ streamActive: turn.streamActive,
386
+ providerID: turn.providerID,
387
+ modelID: turn.modelID,
388
+ historyBeforeBytes: turn.historyBeforeBytes,
389
+ historyAfterBytes: turn.historyAfterBytes,
390
+ requestBytes: turn.requestBytes,
391
+ toolSchemaBytes: turn.toolSchemaBytes,
392
+ outputChars: turn.outputChars,
393
+ toolRawChars: turn.toolRawChars,
394
+ })),
395
+ },
396
+ cortexTasks: {
397
+ totalCount: cortexStats.totalCount,
398
+ queuedCount: cortexStats.byStatus.queued,
399
+ runningCount: cortexStats.byStatus.running,
400
+ completedCount: cortexStats.byStatus.completed,
401
+ errorCount: cortexStats.byStatus.error,
402
+ cancelledCount: cortexStats.byStatus.cancelled,
403
+ interruptedCount: cortexStats.byStatus.interrupted,
404
+ retainedPromptChars: cortexStats.retainedPromptChars,
405
+ retainedOutputChars: cortexStats.retainedOutputChars,
406
+ retainedErrorChars: cortexStats.retainedErrorChars,
407
+ retainedProgressToolCount: cortexStats.retainedProgressToolCount,
408
+ },
409
+ },
410
+ top: {
411
+ slowRoutes: rank(http, "path"),
412
+ slowSessions: rank(turns, "session_id"),
413
+ slowTools: rank(tools, "tool"),
414
+ toolFailures,
415
+ slowProviders: rank(llm, "providerID", "provider", "modelID", "model"),
416
+ slowStorage: rank(storage, "operation"),
417
+ slowLibrary: rank(library, "operation"),
418
+ childProcesses,
419
+ slowFrontend: rank(
420
+ [...frontendResources, ...frontendLongTasks],
421
+ "routeName",
422
+ "pathTemplate",
423
+ "route",
424
+ "name",
425
+ "attribution",
426
+ ),
427
+ },
428
+ issues,
429
+ })
430
+ }
431
+
432
+ function rank(rows: MetricRow[], ...labelKeys: string[]): PerformanceSchema.RankedItem[] {
433
+ return [...rows]
434
+ .sort((a, b) => b.value - a.value)
435
+ .slice(0, 5)
436
+ .map((row, index) => {
437
+ const label = String(firstLabel(metricLabels(row), labelKeys) ?? row.tool ?? row.session_id ?? row.name)
438
+ return {
439
+ id: `${row.metric_id}-${index}`,
440
+ label,
441
+ module: row.module as PerformanceSchema.Module,
442
+ value: row.value,
443
+ unit: row.unit as PerformanceSchema.Unit,
444
+ traceId: row.trace_id ?? undefined,
445
+ sessionID: row.session_id ?? undefined,
446
+ tool: row.tool ?? undefined,
447
+ }
448
+ })
449
+ }
450
+
451
+ function rankToolFailures(
452
+ rows: Array<Pick<MetricRow, "name" | "tool" | "labels_json" | "value">>,
453
+ ): PerformanceSchema.ToolFailureItem[] {
454
+ const tools = new Map<string, { callCount: number; errorCount: number; categories: Map<string, number> }>()
455
+ for (const row of rows) {
456
+ if (row.name !== "tool.execution.count" && row.name !== "tool.execution.error") continue
457
+ const tool = row.tool ?? stringLabel(metricLabels(row).tool)
458
+ if (!tool) continue
459
+ const entry = tools.get(tool) ?? { callCount: 0, errorCount: 0, categories: new Map<string, number>() }
460
+ if (row.name === "tool.execution.count") {
461
+ entry.callCount += row.value
462
+ } else {
463
+ entry.errorCount += row.value
464
+ const errorClass = stringLabel(metricLabels(row).errorName) ?? "UnknownError"
465
+ entry.categories.set(errorClass, (entry.categories.get(errorClass) ?? 0) + row.value)
466
+ }
467
+ tools.set(tool, entry)
468
+ }
469
+ return Array.from(tools, ([tool, entry]) => ({
470
+ tool,
471
+ callCount: entry.callCount,
472
+ errorCount: entry.errorCount,
473
+ errorRate: entry.errorCount / Math.max(entry.callCount, entry.errorCount, 1),
474
+ categories: Array.from(entry.categories, ([errorClass, count]) => ({ errorClass, count }))
475
+ .sort((a, b) => b.count - a.count || a.errorClass.localeCompare(b.errorClass))
476
+ .slice(0, 3),
477
+ }))
478
+ .filter((entry) => entry.errorCount > 0)
479
+ .sort((a, b) => b.errorCount - a.errorCount || b.errorRate - a.errorRate || a.tool.localeCompare(b.tool))
480
+ .slice(0, 5)
481
+ }
482
+
483
+ function stringLabel(value: unknown) {
484
+ return typeof value === "string" && value.length > 0 ? value : undefined
485
+ }
486
+
487
+ function rankChildProcesses(rows: ObservabilityStore.StoredResource[]): PerformanceSchema.RankedItem[] {
488
+ const latest = new Map<string, ObservabilityStore.StoredResource>()
489
+ for (const row of rows) {
490
+ if (!row.process_role?.startsWith("tool")) continue
491
+ if (row.memory_rss_bytes === undefined || row.memory_rss_bytes === null) continue
492
+ const id = row.process_id ?? (row.pid === undefined || row.pid === null ? undefined : `pid:${row.pid}`)
493
+ if (!id) continue
494
+ const existing = latest.get(id)
495
+ if (!existing || row.time > existing.time) latest.set(id, row)
496
+ }
497
+ return Array.from(latest.values())
498
+ .sort((a, b) => (b.memory_rss_bytes ?? 0) - (a.memory_rss_bytes ?? 0))
499
+ .slice(0, 5)
500
+ .map((row, index) => {
501
+ const labels = metricLabels(row)
502
+ return {
503
+ id: `${row.sample_id}-${index}`,
504
+ label: String(labels.command ?? labels.description ?? row.process_id ?? row.pid ?? "tool child process"),
505
+ module: "process" as const,
506
+ value: row.memory_rss_bytes ?? 0,
507
+ unit: "bytes" as const,
508
+ processId: row.process_id ?? undefined,
509
+ pid: row.pid ?? undefined,
510
+ status: row.process_role ?? undefined,
511
+ }
512
+ })
513
+ }
514
+
515
+ function firstLabel(labels: Record<string, unknown>, keys: string[]) {
516
+ for (const key of keys) {
517
+ const value = labels[key]
518
+ if (value !== undefined && value !== null && value !== "") return value
519
+ }
520
+ }
521
+ }
@@ -0,0 +1,32 @@
1
+ export class PerformanceError extends Error {
2
+ constructor(
3
+ readonly code: PerformanceError.Code,
4
+ message: string,
5
+ readonly status: number,
6
+ readonly data: Record<string, unknown> = {},
7
+ ) {
8
+ super(message)
9
+ this.name = "PerformanceError"
10
+ }
11
+
12
+ toResponse() {
13
+ return { code: this.code, message: this.message, ...this.data }
14
+ }
15
+ }
16
+
17
+ export namespace PerformanceError {
18
+ export type Code =
19
+ | "PERF_INVALID_QUERY"
20
+ | "PERF_TOO_MANY_BUCKETS"
21
+ | "PERF_FORBIDDEN"
22
+ | "PERF_RATE_LIMITED"
23
+ | "PERF_OBSERVABILITY_UNAVAILABLE"
24
+ | "PERF_TRACE_NOT_FOUND"
25
+ | "PERF_INVALID_METRIC_BATCH"
26
+ | "PERF_CONFIG_RESTART_REQUIRED"
27
+ | "PERF_CONFIG_CONFLICT"
28
+ | "PERF_STORAGE_UNAVAILABLE"
29
+ | "PERF_SPAN_CONFLICT"
30
+ | "PERF_ANALYSIS_NOT_FOUND"
31
+ | "PERF_ANALYSIS_UNAVAILABLE"
32
+ }
@@ -0,0 +1,6 @@
1
+ export { PerformanceDashboard } from "./dashboard"
2
+ export { PerformanceError } from "./error"
3
+ export { PerformanceInflight } from "./inflight"
4
+ export { PerformanceSchema } from "./schema"
5
+ export { PerformanceTimeline } from "./timeline"
6
+ export { PerformanceTraceDetail } from "./trace-detail"
@@ -0,0 +1,46 @@
1
+ import { ObservabilityStore } from "@ericsanchezok/synergy-harness/observability/store"
2
+ import { PerformanceSchema } from "./schema"
3
+ import { parseJson } from "@ericsanchezok/synergy-harness/util/json-parse"
4
+
5
+ export namespace PerformanceInflight {
6
+ export function get(
7
+ input: { scopeID?: string; sessionID?: string; staleMs?: number; limit?: number } = {},
8
+ ): PerformanceSchema.Inflight {
9
+ const spans = ObservabilityStore.queryInflight(input).map((row) =>
10
+ PerformanceSchema.InflightSpan.parse({
11
+ traceId: row.trace_id,
12
+ correlationId: row.correlation_id ?? undefined,
13
+ spanId: row.span_id,
14
+ parentSpanId: row.parent_span_id ?? undefined,
15
+ kind: row.kind,
16
+ name: row.name,
17
+ module: row.module,
18
+ source: row.source,
19
+ startTime: row.start_time,
20
+ endTime: row.end_time ?? undefined,
21
+ durationMs: row.duration_ms ?? undefined,
22
+ status: row.status,
23
+ lastActivityTime: row.last_activity_time ?? undefined,
24
+ heartbeatTime: row.heartbeat_time ?? undefined,
25
+ heartbeatCount: row.heartbeat_count ?? undefined,
26
+ stalled: row.stalled ? true : undefined,
27
+ errorCode: row.error_code ?? undefined,
28
+ errorMessage: row.error_message ?? undefined,
29
+ scopeID: row.scope_id ?? undefined,
30
+ sessionID: row.session_id ?? undefined,
31
+ messageID: row.message_id ?? undefined,
32
+ callID: row.call_id ?? undefined,
33
+ rid: row.rid ?? undefined,
34
+ processId: row.process_id ?? undefined,
35
+ pid: row.pid ?? undefined,
36
+ tool: row.tool ?? undefined,
37
+ attributes: parseJson(row.attributes_json),
38
+ ageMs: row.age_ms,
39
+ idleMs: row.idle_ms,
40
+ stale: row.stale,
41
+ activeSince: row.last_activity_time ?? row.start_time,
42
+ }),
43
+ )
44
+ return PerformanceSchema.Inflight.parse({ generatedAt: new Date().toISOString(), spans })
45
+ }
46
+ }