aidevops 3.29.39 → 3.29.42

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.
@@ -0,0 +1,335 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ type GuiPulseResourceKind,
5
+ type GuiPulseResourceSnapshot,
6
+ type GuiPulseSystemicFinding,
7
+ type GuiPulseWorkerActionSummary,
8
+ type GuiPulseWorkerActivityEvent,
9
+ type GuiPulseWorkerChartSeries,
10
+ type GuiPulseWorkerKpiCard,
11
+ type GuiPulseWorkerSummary,
12
+ type GuiPulseWorkerUsageSnapshot,
13
+ type GuiPulsePeriodBucket,
14
+ pulseWorkersFixture,
15
+ } from "../../gui-shared/src";
16
+ import { collapseHome, expandHome, formatEpochField, isRecord, readJsonObject, stringField } from "./status-adapter-utils";
17
+ import { buildAttention } from "./status-pulse-workers-attention";
18
+ import { authorAssociationFromRecord, isHealthyOutcome, issueOriginFromRecord, outcomeFromRecord, providerFromString, severityFromStatus, statusFromOutcome } from "./status-pulse-workers-classifiers";
19
+ import { costRef, countOptions, durationMsFromRecord, type MetricRecord, numberValue, recordTimeMs, stringOrNumber, summaryFromRecord } from "./status-pulse-workers-values";
20
+
21
+ export interface PulseWorkersAdapterOptions {
22
+ metricsPath?: string;
23
+ pulseStatsPath?: string;
24
+ resourceMetricsPath?: string;
25
+ tokenReportsRoot?: string;
26
+ oauthPoolPath?: string;
27
+ observedAt?: string;
28
+ nowMs?: number;
29
+ }
30
+
31
+ interface SourceState {
32
+ path_ref: string;
33
+ health: "present" | "missing" | "invalid";
34
+ observed_at: string;
35
+ }
36
+
37
+ const DAY_MS = 86_400_000;
38
+ const WEEK_MS = 7 * DAY_MS;
39
+ const DEFAULT_SCOPE = "local aidevops telemetry";
40
+ const DEFAULT_METRICS_PATH_REF = "~/.aidevops/logs/headless-runtime-metrics.jsonl";
41
+ const DEFAULT_PULSE_STATS_PATH_REF = "~/.aidevops/logs/pulse-stats.json";
42
+ const DEFAULT_RESOURCE_METRICS_PATH_REF = "~/.aidevops/logs/resource-metrics.jsonl";
43
+ const DEFAULT_TOKEN_REPORTS_ROOT_REF = "~/.aidevops/_reports/token-use";
44
+ const PULSE_WORKER_ACTIONS: GuiPulseWorkerActionSummary[] = pulseWorkersFixture.actions;
45
+
46
+ export function readPulseWorkersSummary(options: PulseWorkersAdapterOptions = {}): { summary: GuiPulseWorkerSummary; source_path_refs: string[] } {
47
+ const nowMs = options.nowMs ?? Date.parse(options.observedAt ?? new Date().toISOString());
48
+ const observedAt = new Date(Number.isFinite(nowMs) ? nowMs : Date.now()).toISOString();
49
+ const metricsPath = options.metricsPath ?? expandHome(DEFAULT_METRICS_PATH_REF);
50
+ const pulseStatsPath = options.pulseStatsPath ?? expandHome(DEFAULT_PULSE_STATS_PATH_REF);
51
+ const resourceMetricsPath = options.resourceMetricsPath ?? expandHome(DEFAULT_RESOURCE_METRICS_PATH_REF);
52
+ const tokenReportsRoot = options.tokenReportsRoot ?? expandHome(DEFAULT_TOKEN_REPORTS_ROOT_REF);
53
+ const oauthPoolPath = options.oauthPoolPath ?? expandHome("~/.aidevops/oauth-pool.json");
54
+
55
+ const metrics = readJsonLines(metricsPath);
56
+ const resources = readJsonLines(resourceMetricsPath);
57
+ const tokenReports = readTokenReports(tokenReportsRoot);
58
+ const pulseStats = readJsonObject(pulseStatsPath);
59
+ const oauthPool = readJsonObject(oauthPoolPath);
60
+ const sourceStates: SourceState[] = [
61
+ stateFor(metricsPath, metrics.health, observedAt),
62
+ stateFor(pulseStatsPath, pulseStats.health, observedAt),
63
+ stateFor(resourceMetricsPath, resources.health, observedAt),
64
+ stateFor(tokenReportsRoot, tokenReports.health, observedAt),
65
+ stateFor(oauthPoolPath, oauthPool.health, observedAt),
66
+ ];
67
+
68
+ const dayMetrics = filterWindow(metrics.records, nowMs, DAY_MS);
69
+ const previousDayMetrics = filterWindowBetween(metrics.records, nowMs - (2 * DAY_MS), nowMs - DAY_MS);
70
+ const weekMetrics = filterWindow(metrics.records, nowMs, WEEK_MS);
71
+ const pulseCounters = extractPulseCounters(pulseStats.value, nowMs);
72
+ const resourceSnapshots = resourceMetricsToSnapshots(resources.records, observedAt);
73
+ const usageSamples = collectUsageSamples([...dayMetrics, ...tokenReports.records]);
74
+ const events = buildEvents(dayMetrics, resourceSnapshots, usageSamples, observedAt);
75
+ const insights = buildInsights(events, dayMetrics, previousDayMetrics, resourceSnapshots);
76
+ const warnings = [...buildAttention(sourceStates, pulseCounters, resourceSnapshots, oauthPool.value), ...insights.map((finding) => ({ id: finding.id, severity: finding.severity, title: finding.title, detail: `${finding.detail} Recommended fix: ${finding.recommendation}`, event_ref: finding.event_refs[0] ?? null }))].slice(0, 12);
77
+
78
+ const summary: GuiPulseWorkerSummary = {
79
+ value_policy: "metadata_only_no_prompt_payloads_no_secrets",
80
+ selected_period: "day",
81
+ period_label: "Last 24h",
82
+ scope_label: DEFAULT_SCOPE,
83
+ comparison_label: "Prior local telemetry window",
84
+ updated_at: observedAt,
85
+ kpis: buildKpis(dayMetrics, weekMetrics, pulseCounters, usageSamples, resourceSnapshots),
86
+ attention: warnings,
87
+ insights,
88
+ filters: buildFilters(events),
89
+ charts: buildCharts(metrics.records, pulseCounters, nowMs),
90
+ events,
91
+ actions: PULSE_WORKER_ACTIONS,
92
+ };
93
+
94
+ return { summary, source_path_refs: sourceStates.map((source) => source.path_ref) };
95
+ }
96
+
97
+ function readJsonLines(pathName: string): { health: "present" | "missing" | "invalid"; records: MetricRecord[] } {
98
+ if (!existsSync(pathName)) {
99
+ return { health: "missing", records: [] };
100
+ }
101
+ try {
102
+ const records = readFileSync(pathName, "utf8")
103
+ .split(/\r?\n/)
104
+ .map((line) => line.trim())
105
+ .filter(Boolean)
106
+ .map((line) => JSON.parse(line))
107
+ .filter(isRecord);
108
+ return { health: "present", records };
109
+ } catch {
110
+ return { health: "invalid", records: [] };
111
+ }
112
+ }
113
+
114
+ function readTokenReports(root: string): { health: "present" | "missing" | "invalid"; records: MetricRecord[] } {
115
+ if (!existsSync(root)) {
116
+ return { health: "missing", records: [] };
117
+ }
118
+ try {
119
+ const records = readdirSync(root, { withFileTypes: true })
120
+ .filter((entry) => entry.isDirectory())
121
+ .map((entry) => join(root, entry.name, "report.json"))
122
+ .filter((pathName) => existsSync(pathName))
123
+ .slice(-25)
124
+ .map((pathName) => JSON.parse(readFileSync(pathName, "utf8")))
125
+ .filter(isRecord);
126
+ return { health: "present", records };
127
+ } catch {
128
+ return { health: "invalid", records: [] };
129
+ }
130
+ }
131
+
132
+ function stateFor(pathName: string, health: SourceState["health"], observedAt: string): SourceState {
133
+ return { path_ref: collapseHome(pathName), health, observed_at: observedAt };
134
+ }
135
+
136
+ function filterWindow(records: MetricRecord[], nowMs: number, windowMs: number): MetricRecord[] {
137
+ const cutoff = nowMs - windowMs;
138
+ return records.filter((record) => {
139
+ const time = recordTimeMs(record);
140
+ return time !== null && time >= cutoff && time <= nowMs;
141
+ });
142
+ }
143
+
144
+ function filterWindowBetween(records: MetricRecord[], startMs: number, endMs: number): MetricRecord[] {
145
+ return records.filter((record) => {
146
+ const time = recordTimeMs(record);
147
+ return time !== null && time >= startMs && time < endMs;
148
+ });
149
+ }
150
+
151
+ function extractPulseCounters(value: Record<string, unknown>, nowMs: number): Record<string, number> {
152
+ const counters = isRecord(value.counters) ? value.counters : {};
153
+ return Object.fromEntries(Object.entries(counters).map(([name, entries]) => [name, countRecentTimestamps(entries, nowMs, DAY_MS)]));
154
+ }
155
+
156
+ function countRecentTimestamps(value: unknown, nowMs: number, windowMs: number): number {
157
+ if (!Array.isArray(value)) {
158
+ return 0;
159
+ }
160
+ const cutoff = nowMs - windowMs;
161
+ return value.filter((entry) => {
162
+ const ms = typeof entry === "number" ? (entry > 9_999_999_999 ? entry : entry * 1000) : Date.parse(String(entry));
163
+ return Number.isFinite(ms) && ms >= cutoff && ms <= nowMs;
164
+ }).length;
165
+ }
166
+
167
+ function buildKpis(dayMetrics: MetricRecord[], weekMetrics: MetricRecord[], counters: Record<string, number>, usage: GuiPulseWorkerUsageSnapshot[], resources: GuiPulseResourceSnapshot[]): GuiPulseWorkerKpiCard[] {
168
+ const total = dayMetrics.length;
169
+ const healthy = dayMetrics.filter((record) => isHealthyOutcome(outcomeFromRecord(record))).length;
170
+ const rate = total === 0 ? 0 : Math.round((healthy / total) * 100);
171
+ const totalTokens = usage.reduce((sum, item) => sum + item.total_tokens, 0);
172
+ const costRefs = usage.map((item) => item.estimated_cost_ref).filter((item): item is string => item !== null);
173
+ const attentionCount = Object.values(counters).reduce((sum, count) => sum + count, 0) + resources.filter((resource) => resource.pressure !== "low" && resource.pressure !== "unknown").length;
174
+
175
+ return [
176
+ { id: "worker-outcomes-24h", label: "Healthy worker outcomes", value: total === 0 ? "unknown" : `${rate}%`, period_label: "Last 24h", scope_label: DEFAULT_SCOPE, comparison_label: "computed from local worker metrics", sample_size: total, status: total === 0 ? "attention" : rate >= 80 ? "healthy" : "attention", detail: total === 0 ? "No local worker outcome records were available for the last 24h." : `${healthy} of ${total} local worker sessions ended merged, closed, completed, or deferred.` },
177
+ { id: "attention-signals", label: "Attention signals", value: String(attentionCount), period_label: "Last 24h", scope_label: DEFAULT_SCOPE, comparison_label: "pulse counters plus resource pressure", sample_size: Object.keys(counters).length + resources.length, status: attentionCount > 0 ? "attention" : "healthy", detail: "Counts canonical Pulse counters and observed resource pressure without treating missing telemetry as failure." },
178
+ { id: "token-cost", label: "Token and cost sample", value: totalTokens > 0 ? `${totalTokens.toLocaleString()} tokens` : "unknown", period_label: "Last 24h", scope_label: "provider/model metadata", comparison_label: costRefs.length > 0 ? "cost refs observed" : "cost unavailable", sample_size: usage.length, status: usage.length > 0 ? "healthy" : "attention", detail: "Token usage is derived from metadata fields only; prompts, command output, credential paths, and message payloads are excluded." },
179
+ { id: "systemic-fixes", label: "Systemic fixes observed", value: String(weekMetrics.filter((record) => outcomeFromRecord(record) === "created_followup").length), period_label: "Trailing 7d", scope_label: DEFAULT_SCOPE, comparison_label: "worker outcome metadata", sample_size: weekMetrics.length, status: "completed", detail: "Follow-up/systemic-fix outcomes are counted only when canonical local metrics record them." },
180
+ ];
181
+ }
182
+
183
+ function buildEvents(records: MetricRecord[], resources: GuiPulseResourceSnapshot[], usage: GuiPulseWorkerUsageSnapshot[], observedAt: string): GuiPulseWorkerActivityEvent[] {
184
+ return records.slice(-25).sort((left, right) => (recordTimeMs(right) ?? 0) - (recordTimeMs(left) ?? 0)).map((record, index) => {
185
+ const outcome = outcomeFromRecord(record);
186
+ const status = statusFromOutcome(outcome);
187
+ const issue = stringOrNumber(record.issue ?? record.issue_number);
188
+ const pr = stringOrNumber(record.pr ?? record.pr_number ?? record.pull_request);
189
+ const repo = stringField(record, "repo") ?? null;
190
+ return {
191
+ id: `event:worker:${issue ?? index}:${recordTimeMs(record) ?? index}`,
192
+ type: "worker_session",
193
+ status,
194
+ outcome,
195
+ severity: severityFromStatus(status),
196
+ occurred_at: recordTimeMs(record) === null ? observedAt : new Date(recordTimeMs(record) as number).toISOString(),
197
+ title: "Worker session",
198
+ summary: summaryFromRecord(record, outcome),
199
+ pulse_run_ref: stringField(record, "pulse_run_ref") ?? null,
200
+ worker_session_ref: stringField(record, "session_key") ?? stringField(record, "worker_session_ref") ?? null,
201
+ issue_ref: issue === null ? null : `#${issue.replace(/^#/, "")}`,
202
+ pull_request_ref: pr === null ? null : `#${pr.replace(/^#/, "")}`,
203
+ command_job_ref: stringField(record, "command_job_ref") ?? null,
204
+ repo_ref: repo,
205
+ actor_ref: stringField(record, "actor") ?? "worker:auto-dispatch",
206
+ issue_origin: issueOriginFromRecord(record),
207
+ author_association: authorAssociationFromRecord(record),
208
+ duration_ms: durationMsFromRecord(record),
209
+ usage: usageFromRecord(record) ?? usage[index] ?? null,
210
+ resources,
211
+ drilldown_sections: [
212
+ { label: "Evidence", body: "Derived from local worker outcome/resource metadata; prompt text and command payloads are not exposed." },
213
+ { label: "Outcome", body: outcome },
214
+ { label: "Verification", body: verificationSummary(record) },
215
+ { label: "Systemic fix", body: systemicFixRecommendation(record, outcome) },
216
+ ],
217
+ };
218
+ });
219
+ }
220
+
221
+ function buildInsights(events: GuiPulseWorkerActivityEvent[], dayMetrics: MetricRecord[], previousDayMetrics: MetricRecord[], resources: GuiPulseResourceSnapshot[]): GuiPulseSystemicFinding[] {
222
+ const findings: GuiPulseSystemicFinding[] = [];
223
+ const thirdPartyWaiting = events.filter((event) => event.issue_origin === "third_party" && (event.outcome === "needs_maintainer_review" || event.outcome === "blocked" || event.outcome === "in_progress"));
224
+ if (thirdPartyWaiting.length > 0) {
225
+ findings.push(finding("third-party-waiting", "third_party_waiting", "warning", "Third-party issues waiting", `${thirdPartyWaiting.length} community-origin issue events need triage or resolution in the selected period.`, "Community reports are separated from aidevops-created task handling and may need a maintainer decision before dispatch.", thirdPartyWaiting, "Create a systemic triage task that preserves non-collaborator trust boundaries and adds first-response thresholds.", "community waiting count", thirdPartyWaiting.length, "high"));
226
+ }
227
+
228
+ const failedOrBlocked = events.filter((event) => event.outcome === "failed" || event.outcome === "blocked");
229
+ for (const [key, group] of groupEvents(failedOrBlocked, (event) => [event.repo_ref ?? "repo:unknown", event.usage?.provider ?? "provider:unknown", event.resources[0]?.kind ?? "resource:unknown"].join("|"))) {
230
+ if (group.length > 1) {
231
+ findings.push(finding(`repeated-${key}`, "repeated_failure", "critical", "Repeated failure pattern", `${group.length} failed or blocked runs share repo/provider/resource context.`, "A repeated worker blindspot is likely being retried as individual failures instead of being converted into a shared guardrail or helper fix.", group, "Create a systemic fix task with grouped evidence, affected repo/provider/resource, and a focused verification command.", key.replaceAll("|", " · "), group.length, "medium"));
232
+ }
233
+ }
234
+
235
+ const weakVerification = events.filter((event) => event.drilldown_sections.some((section) => section.label === "Verification" && /missing|weak|not recorded/i.test(section.body)));
236
+ if (weakVerification.length > 0) {
237
+ findings.push(finding("weak-verification", "weak_verification", "warning", "No-verification outcomes", `${weakVerification.length} events lack strong verification metadata.`, "Workers may be completing or blocking without durable command evidence in the telemetry stream.", weakVerification, "Create a systemic fix task to require verification command capture in worker outcome records.", "verification coverage", weakVerification.length, "medium"));
238
+ }
239
+
240
+ const pressureEvents = events.filter((event) => event.resources.some((resource) => resource.pressure === "medium" || resource.pressure === "high"));
241
+ if (pressureEvents.length > 0 || resources.some((resource) => resource.pressure === "medium" || resource.pressure === "high")) {
242
+ findings.push(finding("resource-pressure", "resource_pressure", resources.some((resource) => resource.pressure === "high") ? "critical" : "warning", "Resource allowance pressure", `${pressureEvents.length} events overlapped medium/high resource pressure; ${resources.length} resource samples are in scope.`, "Provider quota, GitHub API, queue, CI, or local resource pressure can inflate retry cost and slow dispatch-to-merge cycles.", pressureEvents, "Create a systemic capacity task that records allowance snapshots before redispatching failed workers.", "resource pressure", Math.max(pressureEvents.length, resources.length), "medium"));
243
+ }
244
+
245
+ const currentCost = sumCost(dayMetrics);
246
+ const previousCost = sumCost(previousDayMetrics);
247
+ const failedCostEvents = events.filter((event) => (event.outcome === "failed" || event.outcome === "blocked") && event.usage?.estimated_cost_ref != null);
248
+ if (currentCost > 0 && previousCost > 0 && currentCost >= previousCost * 1.5) {
249
+ findings.push(finding("cost-spike", "cost_spike", "warning", "Cost spike versus previous period", `Estimated selected-period cost is ${currentCost.toFixed(2)} versus ${previousCost.toFixed(2)} in the previous equivalent window.`, "Retry loops or expensive models may be consuming allowance faster than successful outcomes justify.", events, "Create a systemic cost-control task that ties model/provider selection to outcome quality and retry count.", "estimated USD", dayMetrics.length, "medium", `previous equivalent period: ${previousCost.toFixed(2)}`));
250
+ } else if (failedCostEvents.length > 0) {
251
+ findings.push(finding("failed-run-cost", "high_retry_cost", "warning", "Failed-run cost attached to outcomes", `${failedCostEvents.length} failed or blocked events include token/cost metadata.`, "Cost is being spent on outcomes that did not merge, close, defer, or create follow-up work.", failedCostEvents, "Create a systemic retry-budget task that escalates or changes strategy before another costly rerun.", "failed-run cost samples", failedCostEvents.length, "medium"));
252
+ }
253
+
254
+ const slowEvents = events.filter((event) => (event.duration_ms ?? 0) >= 1_800_000);
255
+ if (slowEvents.length > 0) {
256
+ findings.push(finding("slow-bottlenecks", "slow_bottleneck", "warning", "Slow dispatch-to-outcome bottlenecks", `${slowEvents.length} events exceeded 30 minutes.`, "Long-running dispatch, review, or merge gates can hide queue/concurrency or CI runner bottlenecks.", slowEvents, "Create a systemic bottleneck task that separates dispatch-to-PR and PR-to-merge timing evidence.", "duration >=30m", slowEvents.length, "medium"));
257
+ }
258
+
259
+ return findings.slice(0, 8);
260
+ }
261
+
262
+ function finding(id: string, kind: GuiPulseSystemicFinding["kind"], severity: GuiPulseSystemicFinding["severity"], title: string, detail: string, likelyCause: string, events: GuiPulseWorkerActivityEvent[], recommendation: string, metricLabel: string, sampleSize: number, confidence: GuiPulseSystemicFinding["confidence"], comparisonLabel = "selected period scoped to active filters"): GuiPulseSystemicFinding {
263
+ return { id: `insight-${id.replace(/[^a-z0-9-]+/gi, "-").toLowerCase()}`, kind, severity, title, detail, likely_cause: likelyCause, evidence_refs: evidenceRefs(events), event_refs: events.map((event) => event.id).slice(0, 6), confidence, recommendation, metric_label: metricLabel, period_label: "Last 24h", scope_label: DEFAULT_SCOPE, comparison_label: comparisonLabel, sample_size: sampleSize, primary_action: "create_systemic_fix" };
264
+ }
265
+
266
+ function groupEvents(events: GuiPulseWorkerActivityEvent[], keyFor: (event: GuiPulseWorkerActivityEvent) => string): Map<string, GuiPulseWorkerActivityEvent[]> {
267
+ const groups = new Map<string, GuiPulseWorkerActivityEvent[]>();
268
+ for (const event of events) {
269
+ const key = keyFor(event);
270
+ groups.set(key, [...(groups.get(key) ?? []), event]);
271
+ }
272
+ return groups;
273
+ }
274
+
275
+ function evidenceRefs(events: GuiPulseWorkerActivityEvent[]): string[] {
276
+ return events.flatMap((event) => [event.repo_ref, event.issue_ref, event.pull_request_ref, event.worker_session_ref, event.command_job_ref].filter(Boolean) as string[]).slice(0, 8);
277
+ }
278
+
279
+ function verificationSummary(record: MetricRecord): string {
280
+ const value = stringField(record, "verification") ?? stringField(record, "testing") ?? stringField(record, "verification_status");
281
+ if (typeof value === "string" && value.length > 0) return value;
282
+ return "Verification missing or weak in local telemetry; outcome should carry command evidence before completion claims.";
283
+ }
284
+
285
+ function systemicFixRecommendation(record: MetricRecord, outcome: string): string {
286
+ if (/missing|weak|none/i.test(verificationSummary(record))) return "Require worker outcome telemetry to include verification commands and results.";
287
+ if (outcome === "failed" || outcome === "blocked") return "Group repeated failure evidence and create a worker-ready helper or validator fix.";
288
+ return "Create follow-up only when the same blindspot repeats across events in the selected period.";
289
+ }
290
+
291
+ function sumCost(records: MetricRecord[]): number {
292
+ return records.reduce((sum, record) => sum + (numberValue(record.estimated_cost_usd ?? record.cost_usd) ?? 0), 0);
293
+ }
294
+
295
+ function buildFilters(events: GuiPulseWorkerActivityEvent[]): GuiPulseWorkerSummary["filters"] {
296
+ return {
297
+ repos: countOptions(events.map((event) => event.repo_ref).filter(Boolean) as string[], "repo:"),
298
+ event_types: countOptions(events.map((event) => event.type)),
299
+ outcomes: countOptions(events.map((event) => event.outcome)),
300
+ resources: countOptions(events.flatMap((event) => event.resources.map((resource) => resource.kind))),
301
+ providers: countOptions(events.map((event) => event.usage === null ? null : `${event.usage.provider}:${event.usage.model_ref ?? "unknown"}`).filter(Boolean) as string[]),
302
+ issue_origins: countOptions(events.map((event) => event.issue_origin)),
303
+ authors: countOptions(events.map((event) => event.actor_ref).filter(Boolean) as string[], "actor:"),
304
+ author_associations: countOptions(events.map((event) => event.author_association)),
305
+ };
306
+ }
307
+
308
+ function buildCharts(records: MetricRecord[], counters: Record<string, number>, nowMs: number): GuiPulseWorkerChartSeries[] {
309
+ const specs: Array<[GuiPulsePeriodBucket, number, string]> = [["day", DAY_MS, "Last 24h"], ["week", WEEK_MS, "Trailing 7d"], ["month", 30 * DAY_MS, "Trailing 30d"], ["year", 365 * DAY_MS, "Trailing 365d"]];
310
+ const counterTotal = Object.values(counters).reduce((sum, count) => sum + count, 0);
311
+ return [{ id: "worker-events", label: "Worker events", unit: "count", points: specs.map(([period, windowMs, label]) => ({ period, period_label: label, scope_label: DEFAULT_SCOPE, bucket_start: new Date(nowMs - windowMs).toISOString(), bucket_end: new Date(nowMs).toISOString(), value: filterWindow(records, nowMs, windowMs).length + (period === "day" ? counterTotal : 0) })) }];
312
+ }
313
+
314
+ function resourceMetricsToSnapshots(records: MetricRecord[], observedAt: string): GuiPulseResourceSnapshot[] {
315
+ return records.slice(-5).map((record) => {
316
+ const rssKb = numberValue(record.rss_kb ?? record.peak_rss_kb);
317
+ return { kind: "memory" as GuiPulseResourceKind, label: stringField(record, "role") ?? "Process memory", available_label: rssKb === null ? "unknown" : `${Math.round(rssKb / 1024)} MB RSS`, pressure: rssKb === null ? "unknown" : rssKb > 2_000_000 ? "high" : rssKb > 750_000 ? "medium" : "low", observed_at: recordTimeMs(record) === null ? observedAt : new Date(recordTimeMs(record) as number).toISOString(), reset_at: null };
318
+ });
319
+ }
320
+
321
+ function collectUsageSamples(records: MetricRecord[]): GuiPulseWorkerUsageSnapshot[] {
322
+ return records.map(usageFromRecord).filter((usage): usage is GuiPulseWorkerUsageSnapshot => usage !== null);
323
+ }
324
+
325
+ function usageFromRecord(record: MetricRecord): GuiPulseWorkerUsageSnapshot | null {
326
+ const input = numberValue(record.input_tokens ?? record.prompt_tokens);
327
+ const output = numberValue(record.output_tokens ?? record.completion_tokens);
328
+ const total = numberValue(record.total_tokens) ?? (input ?? 0) + (output ?? 0);
329
+ if (total <= 0 && input === null && output === null) {
330
+ return null;
331
+ }
332
+ const provider = providerFromString(stringField(record, "provider") ?? stringField(record, "provider_id"));
333
+ const model = stringField(record, "model") ?? stringField(record, "model_ref") ?? null;
334
+ return { provider, provider_ref: `provider:${provider}`, model_ref: model, input_tokens: input ?? 0, output_tokens: output ?? 0, cached_tokens: numberValue(record.cached_tokens) ?? 0, total_tokens: total, cost_ref: costRef(record), estimated_cost_ref: costRef(record), wall_time_ms: numberValue(record.wall_time_ms ?? record.duration_ms) ?? 0 };
335
+ }
@@ -1,6 +1,6 @@
1
1
  export type GuiRouteClassification = "read" | "write" | "destructive";
2
2
 
3
- export type GuiOperationId = "setup.status.read" | "capabilities.read" | "filesystem.read" | "vault.status.read" | "apps.action.run" | "apps.action.status" | "tambo.session.read";
3
+ export type GuiOperationId = "setup.status.read" | "capabilities.read" | "filesystem.read" | "vault.status.read" | "apps.action.run" | "apps.action.status" | "pulse_workers.action.run" | "pulse_workers.action.status" | "tambo.session.read";
4
4
 
5
5
  export type GuiFileRootId = "agents" | "config" | "localSetup" | "git";
6
6
 
@@ -106,6 +106,34 @@ export interface GuiAppActionJobSummary {
106
106
  output: string[];
107
107
  }
108
108
 
109
+ export type GuiPulseWorkerActionId = "diagnose" | "run_pulse" | "open_logs" | "create_systemic_fix";
110
+
111
+ export interface GuiPulseWorkerActionSummary {
112
+ id: GuiPulseWorkerActionId;
113
+ label: string;
114
+ enabled: boolean;
115
+ classification: GuiRouteClassification;
116
+ confirmation: "none" | "recommended" | "required";
117
+ command_preview: string;
118
+ target_ref: string;
119
+ scope_copy: string;
120
+ expected_effect: string;
121
+ audit_ref: string;
122
+ }
123
+
124
+ export interface GuiPulseWorkerActionJobSummary {
125
+ id: string;
126
+ action: GuiPulseWorkerActionId;
127
+ target_ref: string;
128
+ status: "running" | "completed" | "failed" | "rejected";
129
+ command_preview: string;
130
+ started_at: string;
131
+ finished_at: string | null;
132
+ exit_code: number | null;
133
+ output: string[];
134
+ audit_ref: string;
135
+ }
136
+
109
137
  export interface GuiSecretReference {
110
138
  name: string;
111
139
  status: "configured" | "missing" | "unchecked";
@@ -239,9 +267,9 @@ export type GuiPulseWorkerOutcome = "merged" | "closed" | "in_progress" | "needs
239
267
 
240
268
  export type GuiPulseWorkerSeverity = "info" | "success" | "warning" | "critical";
241
269
 
242
- export type GuiPulseIssueOrigin = "aidevops_created" | "maintainer_created" | "origin_interactive" | "third_party" | "unknown";
270
+ export type GuiPulseIssueOrigin = "aidevops_created" | "self_created" | "maintainer_created" | "origin_interactive" | "third_party" | "imported" | "unknown";
243
271
 
244
- export type GuiPulseAuthorAssociation = "OWNER" | "MEMBER" | "COLLABORATOR" | "CONTRIBUTOR" | "FIRST_TIME_CONTRIBUTOR" | "NONE" | "UNKNOWN";
272
+ export type GuiPulseAuthorAssociation = "OWNER" | "MEMBER" | "COLLABORATOR" | "CONTRIBUTOR" | "FIRST_TIMER" | "FIRST_TIME_CONTRIBUTOR" | "NONE" | "UNKNOWN";
245
273
 
246
274
  export type GuiPulseResourceKind = "provider_model" | "github_api" | "ci_capacity" | "queue" | "cpu" | "memory" | "disk" | "network";
247
275
 
@@ -294,6 +322,27 @@ export interface GuiPulseWorkerChartSeries {
294
322
  points: GuiPulseWorkerChartPoint[];
295
323
  }
296
324
 
325
+ export type GuiPulseInsightKind = "third_party_waiting" | "high_retry_cost" | "repeated_failure" | "cost_spike" | "weak_verification" | "resource_pressure" | "slow_bottleneck";
326
+
327
+ export interface GuiPulseSystemicFinding {
328
+ id: string;
329
+ kind: GuiPulseInsightKind;
330
+ severity: GuiPulseWorkerSeverity;
331
+ title: string;
332
+ detail: string;
333
+ likely_cause: string;
334
+ evidence_refs: string[];
335
+ event_refs: string[];
336
+ confidence: "low" | "medium" | "high";
337
+ recommendation: string;
338
+ metric_label: string;
339
+ period_label: string;
340
+ scope_label: string;
341
+ comparison_label: string;
342
+ sample_size: number;
343
+ primary_action: GuiPulseWorkerActionId;
344
+ }
345
+
297
346
  export interface GuiPulseWorkerActivityEvent {
298
347
  id: string;
299
348
  type: GuiPulseWorkerEventType;
@@ -333,6 +382,7 @@ export interface GuiPulseWorkerSummary {
333
382
  updated_at: string;
334
383
  kpis: GuiPulseWorkerKpiCard[];
335
384
  attention: Array<{ id: string; severity: GuiPulseWorkerSeverity; title: string; detail: string; event_ref: string | null }>;
385
+ insights: GuiPulseSystemicFinding[];
336
386
  filters: {
337
387
  repos: GuiPulseWorkerFilterOption[];
338
388
  event_types: GuiPulseWorkerFilterOption[];
@@ -345,6 +395,7 @@ export interface GuiPulseWorkerSummary {
345
395
  };
346
396
  charts: GuiPulseWorkerChartSeries[];
347
397
  events: GuiPulseWorkerActivityEvent[];
398
+ actions: GuiPulseWorkerActionSummary[];
348
399
  }
349
400
 
350
401
  export interface GuiConversationMessage {
@@ -655,6 +706,28 @@ export const APP_ACTION_STATUS_ROUTE_MANIFEST: GuiRouteManifest = {
655
706
  redactions: ["secret_values", "credential_paths", "private_key_material"],
656
707
  };
657
708
 
709
+ export const PULSE_WORKERS_ACTION_ROUTE_MANIFEST: GuiRouteManifest = {
710
+ route: "/api/pulse-workers/actions/:action",
711
+ method: "POST",
712
+ operation_id: "pulse_workers.action.run",
713
+ classification: "write",
714
+ source_surface: "pulse_workers",
715
+ adapter: "pulseWorkerActions.startPulseWorkerAction",
716
+ command_pattern: ["allowlisted", "aidevops", "pulse/worker-diagnostics", "background-job"],
717
+ redactions: ["secret_values", "credential_paths", "private_key_material", "github_tokens", "authorization_headers", "session_cookies"],
718
+ };
719
+
720
+ export const PULSE_WORKERS_ACTION_STATUS_ROUTE_MANIFEST: GuiRouteManifest = {
721
+ route: "/api/pulse-workers/jobs/:jobId",
722
+ method: "GET",
723
+ operation_id: "pulse_workers.action.status",
724
+ classification: "read",
725
+ source_surface: "pulse_workers",
726
+ adapter: "pulseWorkerActions.readPulseWorkerActionJob",
727
+ command_pattern: ["background-job", "metadata-and-redacted-output-only"],
728
+ redactions: ["secret_values", "credential_paths", "private_key_material", "github_tokens", "authorization_headers", "session_cookies"],
729
+ };
730
+
658
731
  export const GUI_FILE_ROOTS: readonly GuiFileRootDefinition[] = [
659
732
  {
660
733
  id: "agents",
@@ -27,6 +27,11 @@ export const pulseWorkersFixture: GuiPulseWorkerSummary = {
27
27
  { id: "stale-loop", severity: "warning", title: "1 worker loop matched stale-symptom diagnostics", detail: "Pulse should suggest a systemic diagnostics task instead of another retry.", event_ref: null },
28
28
  { id: "github-api", severity: "critical", title: "GitHub API allowance below dispatch comfort threshold", detail: "REST budget remains available; GraphQL-heavy list operations should pause.", event_ref: "event:community-25876" },
29
29
  ],
30
+ insights: [
31
+ { id: "insight-community-waiting", kind: "third_party_waiting", severity: "warning", title: "Third-party issues waiting", detail: "1 community-origin issue needs triage before dispatch.", likely_cause: "Community reports require collaborator trust-boundary handling and may need maintainer decisions.", evidence_refs: ["marcusquinn/aidevops", "#25876"], event_refs: ["event:community-25876"], confidence: "high", recommendation: "Create a systemic triage task with first-response thresholds and non-collaborator safety checks.", metric_label: "community waiting count", period_label: pulsePeriod, scope_label: pulseScope, comparison_label: "previous 24h baseline", sample_size: 1, primary_action: "create_systemic_fix" },
32
+ { id: "insight-weak-verification", kind: "weak_verification", severity: "warning", title: "No-verification outcomes", detail: "1 event lacks strong verification metadata.", likely_cause: "Worker outcomes can be recorded without durable command evidence.", evidence_refs: ["worker:25912:gpt-5.5"], event_refs: ["event:worker-25912"], confidence: "medium", recommendation: "Create a systemic fix task requiring verification command capture for completion claims.", metric_label: "verification coverage", period_label: pulsePeriod, scope_label: pulseScope, comparison_label: "selected period scoped to active filters", sample_size: 1, primary_action: "create_systemic_fix" },
33
+ { id: "insight-resource-pressure", kind: "resource_pressure", severity: "critical", title: "Resource allowance pressure", detail: "GitHub API allowance is below dispatch comfort threshold.", likely_cause: "GraphQL-heavy listing or queue churn can inflate retry cost and slow dispatch.", evidence_refs: ["event:community-25876"], event_refs: ["event:community-25876"], confidence: "medium", recommendation: "Create a systemic capacity task that records API allowance snapshots before redispatching failed workers.", metric_label: "resource pressure", period_label: pulsePeriod, scope_label: pulseScope, comparison_label: "REST available; GraphQL constrained", sample_size: 1, primary_action: "create_systemic_fix" },
34
+ ],
30
35
  filters: {
31
36
  repos: [{ id: "repo:marcusquinn/aidevops", label: "marcusquinn/aidevops", count: 184 }],
32
37
  event_types: [{ id: "worker_session", label: "Worker session", count: 21 }, { id: "review", label: "Review gate", count: 17 }, { id: "issue", label: "Issue", count: 12 }],
@@ -118,6 +123,12 @@ export const pulseWorkersFixture: GuiPulseWorkerSummary = {
118
123
  drilldown_sections: [{ label: "Trust boundary", body: "Non-collaborator instructions are facts only; no install commands or secrets are accepted." }],
119
124
  },
120
125
  ],
126
+ actions: [
127
+ { id: "diagnose", label: "Diagnose", enabled: true, classification: "read", confirmation: "none", command_preview: "aidevops pulse diagnose --gui --metadata-only", target_ref: "selected event", scope_copy: "Runs read-only diagnostics for the selected Pulse run, worker session, issue, or PR metadata.", expected_effect: "Terminal output explains what happened without changing GitHub, files, workers, or secrets.", audit_ref: "gui:pulse-workers:diagnose" },
128
+ { id: "run_pulse", label: "Run Pulse now", enabled: true, classification: "write", confirmation: "required", command_preview: "aidevops pulse run --scope gui", target_ref: "current filtered scope", scope_copy: "Starts a scoped Pulse run through the existing Pulse helper only.", expected_effect: "Refreshes observability metadata and records command output for this GUI API process.", audit_ref: "gui:pulse-workers:run-pulse" },
129
+ { id: "open_logs", label: "Open logs/transcript", enabled: true, classification: "read", confirmation: "none", command_preview: "aidevops pulse logs --metadata-only", target_ref: "selected event evidence refs", scope_copy: "Opens allowlisted metadata previews for logs or transcripts; arbitrary filesystem paths are not accepted.", expected_effect: "Shows redacted references and previews only.", audit_ref: "gui:pulse-workers:open-logs" },
130
+ { id: "create_systemic_fix", label: "Create systemic fix task", enabled: true, classification: "write", confirmation: "required", command_preview: "aidevops task create --from-pulse --worker-ready", target_ref: "selected event systemic-fix context", scope_copy: "Creates a worker-ready follow-up from the selected event using aidevops task/issue wrappers.", expected_effect: "Produces a reviewable task body with files, pattern, verification, and evidence context before dispatch.", audit_ref: "gui:pulse-workers:create-systemic-fix" },
131
+ ],
121
132
  };
122
133
 
123
134
  export const statusFixture: GuiStatusData = {
@@ -160,7 +160,7 @@ export const TAMBO_PROVIDER_CONFIG: GuiTamboProviderConfig = {
160
160
 
161
161
  export function validateTamboComponentPayload(payload: unknown, scope: GuiConversationScope): GuiTamboValidationResult {
162
162
  const envelope = validateTamboPayloadEnvelope(payload, scope);
163
- if (!envelope.ok) {
163
+ if (envelope.ok === false) {
164
164
  return envelope.result;
165
165
  }
166
166
 
@@ -172,28 +172,58 @@ type TamboPayloadEnvelopeValidation =
172
172
  | { ok: true; schema: GuiTamboComponentSchema; props: Record<string, unknown> }
173
173
  | { ok: false; result: GuiTamboValidationResult };
174
174
 
175
+ type TamboComponentLookup =
176
+ | { ok: true; payload: Record<string, unknown>; schema: GuiTamboComponentSchema }
177
+ | { ok: false; result: GuiTamboValidationResult };
178
+
175
179
  function validateTamboPayloadEnvelope(payload: unknown, scope: GuiConversationScope): TamboPayloadEnvelopeValidation {
180
+ const lookup = resolveTamboComponentLookup(payload);
181
+ if (lookup.ok === false) {
182
+ return lookup;
183
+ }
184
+
185
+ const envelopeError = validateScopedTamboEnvelope(lookup.payload, scope);
186
+ if (envelopeError !== null) {
187
+ return { ok: false, result: invalid(lookup.schema.name, {}, envelopeError) };
188
+ }
189
+
190
+ const props = resolveTamboComponentProps(lookup.schema, lookup.payload.props);
191
+ if (props.ok === false) {
192
+ return props;
193
+ }
194
+
195
+ return { ok: true, schema: lookup.schema, props: props.value };
196
+ }
197
+
198
+ function resolveTamboComponentLookup(payload: unknown): TamboComponentLookup {
176
199
  if (!isRecord(payload)) {
177
200
  return { ok: false, result: invalid(null, {}, "payload_not_object") };
178
201
  }
179
- if (typeof payload.component !== "string") {
180
- return { ok: false, result: invalid(null, {}, "component_missing") };
181
- }
182
202
 
183
- const schema = findTamboComponentSchema(payload.component);
203
+ const schema = resolveTamboComponentSchema(payload.component);
184
204
  if (schema === undefined) {
185
- return { ok: false, result: invalid(null, {}, "component_not_registered") };
205
+ return { ok: false, result: invalid(null, {}, componentLookupError(payload.component)) };
186
206
  }
187
207
 
188
- const envelopeError = validateScopedTamboEnvelope(payload, scope);
189
- if (envelopeError !== null) {
190
- return { ok: false, result: invalid(schema.name, {}, envelopeError) };
191
- }
192
- if (!isRecord(payload.props)) {
208
+ return { ok: true, payload, schema };
209
+ }
210
+
211
+ function resolveTamboComponentSchema(component: unknown): GuiTamboComponentSchema | undefined {
212
+ return typeof component === "string" ? findTamboComponentSchema(component) : undefined;
213
+ }
214
+
215
+ function componentLookupError(component: unknown): string {
216
+ return typeof component === "string" ? "component_not_registered" : "component_missing";
217
+ }
218
+
219
+ function resolveTamboComponentProps(
220
+ schema: GuiTamboComponentSchema,
221
+ props: unknown,
222
+ ): { ok: true; value: Record<string, unknown> } | { ok: false; result: GuiTamboValidationResult } {
223
+ if (!isRecord(props)) {
193
224
  return { ok: false, result: invalid(schema.name, {}, "props_not_object") };
194
225
  }
195
-
196
- return { ok: true, schema, props: payload.props };
226
+ return { ok: true, value: props };
197
227
  }
198
228
 
199
229
  function findTamboComponentSchema(component: string): GuiTamboComponentSchema | undefined {
@@ -1,11 +1,14 @@
1
- import type { GuiAppActionJobSummary } from "@aidevops/gui-shared";
1
+ import type { GuiAppActionJobSummary, GuiPulseWorkerActionJobSummary } from "@aidevops/gui-shared";
2
2
  import { type ReactElement, type ReactNode, useEffect, useRef } from "react";
3
3
  import { FiX } from "react-icons/fi";
4
4
 
5
5
  const ANSI_PATTERN = /\x1b\[([0-9;]*)m/g;
6
6
 
7
- export function AppActionTerminal({ job, onDismiss }: { job: GuiAppActionJobSummary; onDismiss: (jobId: string) => void }): ReactElement {
7
+ type TerminalJob = GuiAppActionJobSummary | GuiPulseWorkerActionJobSummary;
8
+
9
+ export function AppActionTerminal({ job, label, onDismiss }: { job: TerminalJob; label?: string; onDismiss: (jobId: string) => void }): ReactElement {
8
10
  const outputRef = useRef<HTMLPreElement | null>(null);
11
+ const title = label ?? terminalTitle(job);
9
12
 
10
13
  useEffect(() => {
11
14
  if (outputRef.current !== null) {
@@ -14,14 +17,18 @@ export function AppActionTerminal({ job, onDismiss }: { job: GuiAppActionJobSumm
14
17
  }, [job.output.length]);
15
18
 
16
19
  return (
17
- <section className="app-terminal" id={`app-job-${job.id}`} aria-label="App action terminal output">
18
- <header><strong>{job.app_id} {job.action}</strong><span>{terminalStatusLabel(job)}</span><button aria-label={`Dismiss ${job.app_id} ${job.action} terminal output`} className="terminal-close-button" onClick={() => onDismiss(job.id)} title="Dismiss terminal output" type="button"><FiX aria-hidden="true" /></button></header>
20
+ <section className="app-terminal" id={`app-job-${job.id}`} aria-label={`${title} terminal output`}>
21
+ <header><strong>{title}</strong><span>{terminalStatusLabel(job)}</span><button aria-label={`Dismiss ${title} terminal output`} className="terminal-close-button" onClick={() => onDismiss(job.id)} title="Dismiss terminal output" type="button"><FiX aria-hidden="true" /></button></header>
19
22
  <pre ref={outputRef}>{renderTerminalOutput(job.output)}</pre>
20
23
  </section>
21
24
  );
22
25
  }
23
26
 
24
- function terminalStatusLabel(job: GuiAppActionJobSummary): string {
27
+ function terminalTitle(job: TerminalJob): string {
28
+ return "app_id" in job ? `${job.app_id} ${job.action}` : `Pulse & Workers ${job.action}`;
29
+ }
30
+
31
+ function terminalStatusLabel(job: TerminalJob): string {
25
32
  if (job.status === "running") {
26
33
  return "running";
27
34
  }