@pellux/goodvibes-tui 0.18.13 → 0.18.18
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.
- package/CHANGELOG.md +139 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +3 -2
- package/src/daemon/cli.ts +82 -6
- package/src/input/command-registry.ts +2 -0
- package/src/input/commands/control-room-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +1 -1
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/platform-access-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +20 -0
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed.ts +6 -0
- package/src/input/handler-modal-routes.ts +19 -2
- package/src/input/handler-modal-token-routes.ts +3 -0
- package/src/input/handler-picker-routes.ts +4 -2
- package/src/input/model-picker.ts +11 -0
- package/src/input/settings-modal.ts +31 -3
- package/src/panels/agent-logs-panel.ts +23 -24
- package/src/panels/base-panel.ts +6 -0
- package/src/panels/builtin/session.ts +66 -0
- package/src/panels/builtin/shared.ts +1 -1
- package/src/panels/provider-account-snapshot.ts +1 -1
- package/src/panels/provider-accounts-panel.ts +23 -27
- package/src/panels/qr-panel.ts +182 -0
- package/src/panels/scrollable-list-panel.ts +407 -0
- package/src/panels/services-panel.ts +1 -1
- package/src/panels/subscription-panel.ts +1 -1
- package/src/panels/types.ts +6 -0
- package/src/panels/worktree-panel.ts +20 -19
- package/src/renderer/buffer.ts +19 -0
- package/src/renderer/compositor.ts +19 -6
- package/src/renderer/panel-composite.ts +24 -3
- package/src/renderer/qr-renderer.ts +117 -0
- package/src/renderer/settings-modal-helpers.ts +122 -0
- package/src/renderer/settings-modal.ts +147 -111
- package/src/runtime/bootstrap-command-context.ts +1 -1
- package/src/runtime/bootstrap-command-parts.ts +31 -15
- package/src/runtime/bootstrap-core.ts +23 -1
- package/src/runtime/bootstrap.ts +6 -1
- package/src/runtime/diagnostics/panels/index.ts +5 -5
- package/src/runtime/services.ts +1 -1
- package/src/runtime/store/domains/domain-read-matrix.ts +0 -2
- package/src/runtime/ui-events.ts +1 -46
- package/src/runtime/ui-read-model-helpers.ts +1 -32
- package/src/runtime/ui-read-models-observability-maintenance.ts +1 -81
- package/src/runtime/ui-read-models-observability-options.ts +1 -5
- package/src/runtime/ui-read-models-observability-remote.ts +1 -73
- package/src/runtime/ui-read-models-observability-security.ts +1 -172
- package/src/runtime/ui-read-models-observability-system.ts +1 -217
- package/src/runtime/ui-read-models-observability.ts +1 -59
- package/src/runtime/ui-service-queries.ts +1 -114
- package/src/version.ts +1 -1
- package/src/config/service-registry.ts +0 -1
- package/src/config/subscription-providers.ts +0 -1
- package/src/runtime/diagnostics/actions.ts +0 -776
- package/src/runtime/diagnostics/index.ts +0 -99
- package/src/runtime/diagnostics/panels/agents.ts +0 -252
- package/src/runtime/diagnostics/panels/events.ts +0 -188
- package/src/runtime/diagnostics/panels/health.ts +0 -242
- package/src/runtime/diagnostics/panels/tasks.ts +0 -251
- package/src/runtime/diagnostics/panels/tool-calls.ts +0 -267
- package/src/runtime/diagnostics/provider.ts +0 -262
- package/src/runtime/store/domains/conversation.ts +0 -1
- package/src/runtime/store/domains/permissions.ts +0 -1
- package/src/runtime/store/helpers/reducers/conversation.ts +0 -1
- package/src/runtime/store/helpers/reducers/lifecycle.ts +0 -1
- package/src/runtime/store/helpers/reducers/shared.ts +0 -60
- package/src/runtime/store/helpers/reducers/sync.ts +0 -555
- package/src/runtime/store/helpers/reducers.ts +0 -30
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Health diagnostic panel data provider.
|
|
3
|
-
*
|
|
4
|
-
* Subscribes to the RuntimeHealthAggregator and produces HealthDashboardData
|
|
5
|
-
* snapshots for the health dashboard diagnostics panel.
|
|
6
|
-
*
|
|
7
|
-
* Implements the health visualization layer for diagnostics.
|
|
8
|
-
* SLO status rows are included when an SloCollector is attached.
|
|
9
|
-
* Remediation actions are included when a CascadeTimer is attached.
|
|
10
|
-
*/
|
|
11
|
-
import type { RuntimeHealthAggregator } from '@pellux/goodvibes-sdk/platform/runtime/health/aggregator';
|
|
12
|
-
import type { CompositeHealth, HealthDomain, HealthStatus } from '@pellux/goodvibes-sdk/platform/runtime/health/types';
|
|
13
|
-
import type { HealthDashboardData, DomainHealthSummary, SloRow, SloGateStatus, RemediationAction } from '@pellux/goodvibes-sdk/platform/runtime/diagnostics/types';
|
|
14
|
-
import type { SloCollector } from '@pellux/goodvibes-sdk/platform/runtime/perf/slo-collector';
|
|
15
|
-
import type { CascadeTimer } from '@pellux/goodvibes-sdk/platform/runtime/health/cascade-timing';
|
|
16
|
-
import { SLO_METRICS } from '@pellux/goodvibes-sdk/platform/runtime/perf/slo-collector';
|
|
17
|
-
import { DEFAULT_BUDGETS } from '@pellux/goodvibes-sdk/platform/runtime/perf/budgets';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Human-readable names for playbooks, keyed by playbook ID.
|
|
21
|
-
* Used to populate RemediationAction.playbookName in the health dashboard.
|
|
22
|
-
*/
|
|
23
|
-
const PLAYBOOK_NAMES: ReadonlyMap<string, string> = new Map([
|
|
24
|
-
['stuck-turn', 'Stuck Turn / Task'],
|
|
25
|
-
['reconnect-failure', 'Reconnect Failure'],
|
|
26
|
-
['permission-deadlock', 'Permission Deadlock'],
|
|
27
|
-
['plugin-degradation', 'Plugin Degradation'],
|
|
28
|
-
['export-recovery', 'Export Recovery'],
|
|
29
|
-
['session-unrecoverable', 'Session Unrecoverable'],
|
|
30
|
-
['compaction-failure', 'Compaction Failure'],
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* HealthPanel — diagnostic data provider for runtime health telemetry.
|
|
35
|
-
*
|
|
36
|
-
* Subscribes to health aggregator updates and maintains a current
|
|
37
|
-
* HealthDashboardData snapshot for the panel to render.
|
|
38
|
-
*/
|
|
39
|
-
/** Warn threshold: 20% above the SLO target triggers a 'warn' status. */
|
|
40
|
-
const SLO_WARN_FACTOR = 1.2;
|
|
41
|
-
|
|
42
|
-
/** SLO budget metadata needed for row construction, keyed by metric name. */
|
|
43
|
-
const SLO_BUDGET_META = new Map(
|
|
44
|
-
DEFAULT_BUDGETS
|
|
45
|
-
.filter((b) => b.metric.startsWith('slo.'))
|
|
46
|
-
.map((b) => [b.metric, { name: b.name, targetMs: b.threshold }])
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
export class HealthPanel {
|
|
50
|
-
private readonly _aggregator: RuntimeHealthAggregator;
|
|
51
|
-
private readonly _sloCollector: SloCollector | null;
|
|
52
|
-
private readonly _cascadeTimer: CascadeTimer | null;
|
|
53
|
-
private _current: HealthDashboardData;
|
|
54
|
-
/** Registered change notification callbacks. */
|
|
55
|
-
private readonly _subscribers = new Set<() => void>();
|
|
56
|
-
/** Unsubscribe function from the aggregator. */
|
|
57
|
-
private _unsub: (() => void) | null = null;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* @param aggregator - The runtime health aggregator to subscribe to.
|
|
61
|
-
* @param sloCollector - Optional SLO collector for SLO status rows.
|
|
62
|
-
* When provided, SLO rows are included in every dashboard snapshot.
|
|
63
|
-
* @param cascadeTimer - Optional CascadeTimer for remediation action rows.
|
|
64
|
-
* When provided, active failed domains are evaluated and remediation
|
|
65
|
-
* playbook IDs are surfaced in every dashboard snapshot.
|
|
66
|
-
*/
|
|
67
|
-
constructor(
|
|
68
|
-
aggregator: RuntimeHealthAggregator,
|
|
69
|
-
sloCollector: SloCollector | null = null,
|
|
70
|
-
cascadeTimer: CascadeTimer | null = null,
|
|
71
|
-
) {
|
|
72
|
-
this._aggregator = aggregator;
|
|
73
|
-
this._sloCollector = sloCollector;
|
|
74
|
-
this._cascadeTimer = cascadeTimer;
|
|
75
|
-
// Capture the initial snapshot before subscribing
|
|
76
|
-
this._current = this._buildDashboard(aggregator.getCompositeHealth());
|
|
77
|
-
this._unsub = aggregator.subscribe((health) => {
|
|
78
|
-
this._current = this._buildDashboard(health);
|
|
79
|
-
this._notify();
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Build a HealthDashboardData snapshot from a CompositeHealth record.
|
|
85
|
-
*/
|
|
86
|
-
private _buildDashboard(composite: CompositeHealth): HealthDashboardData {
|
|
87
|
-
const domains: DomainHealthSummary[] = [];
|
|
88
|
-
for (const [, dh] of composite.domains) {
|
|
89
|
-
domains.push({
|
|
90
|
-
domain: dh.domain,
|
|
91
|
-
status: dh.status,
|
|
92
|
-
lastTransitionAt: dh.lastTransitionAt,
|
|
93
|
-
degradedCapabilities: dh.degradedCapabilities ?? [],
|
|
94
|
-
failureReason: dh.failureReason,
|
|
95
|
-
recoveryAttempts: dh.recoveryAttempts,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
// Sort: failed first, then degraded, then healthy, alphabetically within tier
|
|
99
|
-
domains.sort((a, b) => {
|
|
100
|
-
const order = { failed: 0, degraded: 1, healthy: 2, unknown: 3 };
|
|
101
|
-
const diff = (order[a.status] ?? 3) - (order[b.status] ?? 3);
|
|
102
|
-
return diff !== 0 ? diff : a.domain.localeCompare(b.domain);
|
|
103
|
-
});
|
|
104
|
-
return {
|
|
105
|
-
overall: composite.overall,
|
|
106
|
-
domains,
|
|
107
|
-
degradedDomains: composite.degradedDomains,
|
|
108
|
-
failedDomains: composite.failedDomains,
|
|
109
|
-
lastUpdatedAt: composite.lastUpdatedAt,
|
|
110
|
-
sloRows: this._buildSloRows(),
|
|
111
|
-
remediationActions: this._buildRemediationActions(composite),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Build remediation action rows by evaluating cascade rules for all
|
|
117
|
-
* currently-failed domains using the CascadeTimer.
|
|
118
|
-
*
|
|
119
|
-
* Returns an empty array when no CascadeTimer is attached or when
|
|
120
|
-
* no domains are in the failed state.
|
|
121
|
-
*/
|
|
122
|
-
private _buildRemediationActions(composite: CompositeHealth): readonly RemediationAction[] {
|
|
123
|
-
if (this._cascadeTimer === null || composite.failedDomains.length === 0) {
|
|
124
|
-
return [];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const actions: RemediationAction[] = [];
|
|
128
|
-
const seen = new Set<string>(); // deduplicate by playbookId+ruleId
|
|
129
|
-
|
|
130
|
-
for (const domain of composite.failedDomains) {
|
|
131
|
-
const { cascades } = this._cascadeTimer.evaluate(
|
|
132
|
-
domain,
|
|
133
|
-
'failed',
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
for (const cascade of cascades) {
|
|
137
|
-
for (const playbookId of cascade.remediationPlaybookIds) {
|
|
138
|
-
const key = `${playbookId}:${cascade.ruleId}`;
|
|
139
|
-
if (seen.has(key)) continue;
|
|
140
|
-
seen.add(key);
|
|
141
|
-
actions.push({
|
|
142
|
-
playbookId,
|
|
143
|
-
playbookName: PLAYBOOK_NAMES.get(playbookId) ?? playbookId,
|
|
144
|
-
ruleId: cascade.ruleId,
|
|
145
|
-
sourceDomain: cascade.source,
|
|
146
|
-
severity: cascade.severity ?? 'low',
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Sort by severity: critical first, then high, medium, low
|
|
153
|
-
const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
154
|
-
actions.sort((a, b) => (severityOrder[a.severity] ?? 3) - (severityOrder[b.severity] ?? 3));
|
|
155
|
-
|
|
156
|
-
return actions;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Build SLO status rows from the current SloCollector snapshot.
|
|
161
|
-
* Returns an empty array when no SloCollector is attached.
|
|
162
|
-
*/
|
|
163
|
-
private _buildSloRows(): SloRow[] {
|
|
164
|
-
if (this._sloCollector === null) return [];
|
|
165
|
-
|
|
166
|
-
const metrics = this._sloCollector.getMetrics();
|
|
167
|
-
const counts = this._sloCollector.getSampleCounts();
|
|
168
|
-
|
|
169
|
-
const SLO_ORDER = [
|
|
170
|
-
SLO_METRICS.TURN_START,
|
|
171
|
-
SLO_METRICS.CANCEL,
|
|
172
|
-
SLO_METRICS.RECONNECT_RECOVERY,
|
|
173
|
-
SLO_METRICS.PERMISSION_DECISION,
|
|
174
|
-
] as const;
|
|
175
|
-
|
|
176
|
-
return SLO_ORDER.map((metricKey): SloRow => {
|
|
177
|
-
const metric = metrics.find((m) => m.name === metricKey);
|
|
178
|
-
const meta = SLO_BUDGET_META.get(metricKey);
|
|
179
|
-
const p95Ms = metric?.value ?? 0;
|
|
180
|
-
const targetMs = meta?.targetMs ?? 0;
|
|
181
|
-
const sampleCount = counts[metricKey] ?? 0;
|
|
182
|
-
|
|
183
|
-
let status: SloGateStatus;
|
|
184
|
-
if (sampleCount === 0) {
|
|
185
|
-
status = 'no_data';
|
|
186
|
-
} else if (p95Ms > targetMs) {
|
|
187
|
-
status = 'violated';
|
|
188
|
-
} else if (p95Ms > targetMs / SLO_WARN_FACTOR) {
|
|
189
|
-
status = 'warn';
|
|
190
|
-
} else {
|
|
191
|
-
status = 'ok';
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
metric: metricKey,
|
|
196
|
-
name: meta?.name ?? metricKey,
|
|
197
|
-
p95Ms,
|
|
198
|
-
targetMs,
|
|
199
|
-
sampleCount,
|
|
200
|
-
status,
|
|
201
|
-
};
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Return the current health dashboard snapshot.
|
|
207
|
-
* This is updated synchronously when the aggregator fires.
|
|
208
|
-
*/
|
|
209
|
-
public getSnapshot(): HealthDashboardData {
|
|
210
|
-
return this._current;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Register a callback invoked whenever health data changes.
|
|
215
|
-
* @returns An unsubscribe function.
|
|
216
|
-
*/
|
|
217
|
-
public subscribe(callback: () => void): () => void {
|
|
218
|
-
this._subscribers.add(callback);
|
|
219
|
-
return () => this._subscribers.delete(callback);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Release the aggregator subscription.
|
|
224
|
-
*/
|
|
225
|
-
public dispose(): void {
|
|
226
|
-
if (this._unsub) {
|
|
227
|
-
this._unsub();
|
|
228
|
-
this._unsub = null;
|
|
229
|
-
}
|
|
230
|
-
this._subscribers.clear();
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
private _notify(): void {
|
|
234
|
-
for (const cb of this._subscribers) {
|
|
235
|
-
try {
|
|
236
|
-
cb();
|
|
237
|
-
} catch {
|
|
238
|
-
// Non-fatal: subscriber errors must not crash the provider
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tasks diagnostic panel data provider.
|
|
3
|
-
*
|
|
4
|
-
* Subscribes to task lifecycle events via the RuntimeEventBus and maintains
|
|
5
|
-
* a bounded buffer of TaskEntry records. Provides filtered snapshots
|
|
6
|
-
* for the tasks diagnostics panel.
|
|
7
|
-
*
|
|
8
|
-
* Covers all task kinds: exec, agent, acp, scheduler, daemon, mcp, plugin, integration.
|
|
9
|
-
*/
|
|
10
|
-
import type { RuntimeEventBus, EnvelopeListener } from '@pellux/goodvibes-sdk/platform/runtime/events/index';
|
|
11
|
-
import type { AnyRuntimeEvent } from '@pellux/goodvibes-sdk/platform/runtime/events/domain-map';
|
|
12
|
-
import type { RuntimeEventEnvelope } from '@pellux/goodvibes-sdk/platform/runtime/events/envelope';
|
|
13
|
-
import {
|
|
14
|
-
type TaskEntry,
|
|
15
|
-
type DiagnosticFilter,
|
|
16
|
-
type PanelConfig,
|
|
17
|
-
DEFAULT_PANEL_CONFIG,
|
|
18
|
-
applyFilter,
|
|
19
|
-
appendBounded,
|
|
20
|
-
} from '@pellux/goodvibes-sdk/platform/runtime/diagnostics/types';
|
|
21
|
-
|
|
22
|
-
/** Task state as tracked internally while a task is in progress. */
|
|
23
|
-
type MutableTaskState = TaskEntry['state'];
|
|
24
|
-
|
|
25
|
-
/** Internal mutable task record used while the task is in progress. */
|
|
26
|
-
interface MutableTaskRecord {
|
|
27
|
-
taskId: string;
|
|
28
|
-
agentId?: string;
|
|
29
|
-
description: string;
|
|
30
|
-
priority: number;
|
|
31
|
-
state: MutableTaskState;
|
|
32
|
-
createdAt: number;
|
|
33
|
-
completedAt?: number;
|
|
34
|
-
durationMs?: number;
|
|
35
|
-
progress?: number;
|
|
36
|
-
progressMessage?: string;
|
|
37
|
-
blockReason?: string;
|
|
38
|
-
error?: string;
|
|
39
|
-
traceId: string;
|
|
40
|
-
sessionId: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* TasksPanel — diagnostic data provider for runtime task telemetry.
|
|
45
|
-
*
|
|
46
|
-
* Active tasks are tracked in a live map; terminal tasks are moved
|
|
47
|
-
* to the history buffer for filtering and display.
|
|
48
|
-
*/
|
|
49
|
-
export class TasksPanel {
|
|
50
|
-
private readonly _config: PanelConfig;
|
|
51
|
-
private readonly _eventBus: RuntimeEventBus;
|
|
52
|
-
/** Active tasks keyed by taskId. */
|
|
53
|
-
private readonly _active = new Map<string, MutableTaskRecord>();
|
|
54
|
-
/** Completed task history (oldest first). */
|
|
55
|
-
private readonly _history: TaskEntry[] = [];
|
|
56
|
-
/** Registered change notification callbacks. */
|
|
57
|
-
private readonly _subscribers = new Set<() => void>();
|
|
58
|
-
/** Unsubscribe function from the event bus. */
|
|
59
|
-
private _unsub: (() => void) | null = null;
|
|
60
|
-
|
|
61
|
-
constructor(eventBus: RuntimeEventBus, config: PanelConfig = DEFAULT_PANEL_CONFIG) {
|
|
62
|
-
this._eventBus = eventBus;
|
|
63
|
-
this._config = config;
|
|
64
|
-
this._start();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
private _start(): void {
|
|
68
|
-
const handler: EnvelopeListener<AnyRuntimeEvent> = (
|
|
69
|
-
envelope: RuntimeEventEnvelope<AnyRuntimeEvent['type'], AnyRuntimeEvent>
|
|
70
|
-
) => {
|
|
71
|
-
this._handleEnvelope(envelope);
|
|
72
|
-
};
|
|
73
|
-
this._unsub = this._eventBus.onDomain('tasks', handler as EnvelopeListener);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private _handleEnvelope(
|
|
77
|
-
envelope: RuntimeEventEnvelope<AnyRuntimeEvent['type'], AnyRuntimeEvent>
|
|
78
|
-
): void {
|
|
79
|
-
const p = envelope.payload;
|
|
80
|
-
if (!('type' in p)) return;
|
|
81
|
-
const type = (p as { type: string }).type;
|
|
82
|
-
const traceId = envelope.traceId;
|
|
83
|
-
const sessionId = envelope.sessionId;
|
|
84
|
-
|
|
85
|
-
switch (type) {
|
|
86
|
-
case 'TASK_CREATED': {
|
|
87
|
-
const evt = p as { type: 'TASK_CREATED'; taskId: string; agentId?: string; description: string; priority: number };
|
|
88
|
-
this._active.set(evt.taskId, {
|
|
89
|
-
taskId: evt.taskId,
|
|
90
|
-
agentId: evt.agentId,
|
|
91
|
-
description: evt.description,
|
|
92
|
-
priority: evt.priority,
|
|
93
|
-
state: 'created',
|
|
94
|
-
createdAt: envelope.ts,
|
|
95
|
-
traceId,
|
|
96
|
-
sessionId,
|
|
97
|
-
});
|
|
98
|
-
this._notify();
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
case 'TASK_STARTED': {
|
|
102
|
-
const evt = p as { type: 'TASK_STARTED'; taskId: string };
|
|
103
|
-
const record = this._active.get(evt.taskId);
|
|
104
|
-
if (record) {
|
|
105
|
-
record.state = 'running';
|
|
106
|
-
this._notify();
|
|
107
|
-
}
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
case 'TASK_BLOCKED': {
|
|
111
|
-
const evt = p as { type: 'TASK_BLOCKED'; taskId: string; reason: string };
|
|
112
|
-
const record = this._active.get(evt.taskId);
|
|
113
|
-
if (record) {
|
|
114
|
-
record.state = 'blocked';
|
|
115
|
-
record.blockReason = evt.reason;
|
|
116
|
-
this._notify();
|
|
117
|
-
}
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
case 'TASK_PROGRESS': {
|
|
121
|
-
const evt = p as { type: 'TASK_PROGRESS'; taskId: string; progress: number; message?: string };
|
|
122
|
-
const record = this._active.get(evt.taskId);
|
|
123
|
-
if (record) {
|
|
124
|
-
record.state = 'progressing';
|
|
125
|
-
record.progress = evt.progress;
|
|
126
|
-
record.progressMessage = evt.message;
|
|
127
|
-
this._notify();
|
|
128
|
-
}
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
case 'TASK_COMPLETED': {
|
|
132
|
-
const evt = p as { type: 'TASK_COMPLETED'; taskId: string; durationMs: number };
|
|
133
|
-
const record = this._active.get(evt.taskId);
|
|
134
|
-
if (record) {
|
|
135
|
-
record.state = 'completed';
|
|
136
|
-
record.completedAt = envelope.ts;
|
|
137
|
-
record.durationMs = evt.durationMs;
|
|
138
|
-
this._finalize(record);
|
|
139
|
-
}
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
case 'TASK_FAILED': {
|
|
143
|
-
const evt = p as { type: 'TASK_FAILED'; taskId: string; error: string; durationMs: number };
|
|
144
|
-
const record = this._active.get(evt.taskId);
|
|
145
|
-
if (record) {
|
|
146
|
-
record.state = 'failed';
|
|
147
|
-
record.completedAt = envelope.ts;
|
|
148
|
-
record.durationMs = evt.durationMs;
|
|
149
|
-
record.error = evt.error;
|
|
150
|
-
this._finalize(record);
|
|
151
|
-
}
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
case 'TASK_CANCELLED': {
|
|
155
|
-
const evt = p as { type: 'TASK_CANCELLED'; taskId: string; reason?: string };
|
|
156
|
-
const record = this._active.get(evt.taskId);
|
|
157
|
-
if (record) {
|
|
158
|
-
record.state = 'cancelled';
|
|
159
|
-
record.completedAt = envelope.ts;
|
|
160
|
-
record.error = evt.reason;
|
|
161
|
-
this._finalize(record);
|
|
162
|
-
}
|
|
163
|
-
break;
|
|
164
|
-
}
|
|
165
|
-
default:
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
private _finalize(record: MutableTaskRecord): void {
|
|
171
|
-
this._active.delete(record.taskId);
|
|
172
|
-
const entry: TaskEntry = {
|
|
173
|
-
taskId: record.taskId,
|
|
174
|
-
agentId: record.agentId,
|
|
175
|
-
description: record.description,
|
|
176
|
-
priority: record.priority,
|
|
177
|
-
state: record.state,
|
|
178
|
-
createdAt: record.createdAt,
|
|
179
|
-
completedAt: record.completedAt,
|
|
180
|
-
durationMs: record.durationMs,
|
|
181
|
-
progress: record.progress,
|
|
182
|
-
progressMessage: record.progressMessage,
|
|
183
|
-
blockReason: record.blockReason,
|
|
184
|
-
error: record.error,
|
|
185
|
-
traceId: record.traceId,
|
|
186
|
-
sessionId: record.sessionId,
|
|
187
|
-
};
|
|
188
|
-
appendBounded(this._history, entry, this._config.bufferLimit);
|
|
189
|
-
this._notify();
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Return a filtered snapshot combining active tasks and completed history.
|
|
194
|
-
* Ordered most-recent first.
|
|
195
|
-
*
|
|
196
|
-
* @param filter - Optional filter to restrict entries.
|
|
197
|
-
*/
|
|
198
|
-
public getSnapshot(filter?: DiagnosticFilter): TaskEntry[] {
|
|
199
|
-
const activeEntries: TaskEntry[] = [];
|
|
200
|
-
for (const record of this._active.values()) {
|
|
201
|
-
activeEntries.push({
|
|
202
|
-
taskId: record.taskId,
|
|
203
|
-
agentId: record.agentId,
|
|
204
|
-
description: record.description,
|
|
205
|
-
priority: record.priority,
|
|
206
|
-
state: record.state,
|
|
207
|
-
createdAt: record.createdAt,
|
|
208
|
-
completedAt: record.completedAt,
|
|
209
|
-
durationMs: record.durationMs,
|
|
210
|
-
progress: record.progress,
|
|
211
|
-
progressMessage: record.progressMessage,
|
|
212
|
-
blockReason: record.blockReason,
|
|
213
|
-
error: record.error,
|
|
214
|
-
traceId: record.traceId,
|
|
215
|
-
sessionId: record.sessionId,
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
const combined: TaskEntry[] = [...this._history, ...activeEntries];
|
|
219
|
-
return applyFilter(combined, filter, (e) => e.createdAt);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Register a callback invoked whenever the data changes.
|
|
224
|
-
* @returns An unsubscribe function.
|
|
225
|
-
*/
|
|
226
|
-
public subscribe(callback: () => void): () => void {
|
|
227
|
-
this._subscribers.add(callback);
|
|
228
|
-
return () => this._subscribers.delete(callback);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Release all event bus subscriptions and clear internal state.
|
|
233
|
-
*/
|
|
234
|
-
public dispose(): void {
|
|
235
|
-
if (this._unsub) {
|
|
236
|
-
this._unsub();
|
|
237
|
-
this._unsub = null;
|
|
238
|
-
}
|
|
239
|
-
this._subscribers.clear();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
private _notify(): void {
|
|
243
|
-
for (const cb of this._subscribers) {
|
|
244
|
-
try {
|
|
245
|
-
cb();
|
|
246
|
-
} catch {
|
|
247
|
-
// Non-fatal: subscriber errors must not crash the provider
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|