@pellux/goodvibes-agent 1.5.2 → 1.5.5
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 +26 -0
- package/README.md +1 -1
- package/dist/package/main.js +5337 -2190
- package/package.json +2 -3
- package/release/live-verification/live-verification.json +12 -12
- package/release/live-verification/live-verification.md +13 -13
- package/release/release-readiness.json +7 -7
- package/src/agent/channel-draft.ts +431 -0
- package/src/agent/channel-profile-routing.ts +330 -0
- package/src/agent/email/style-reply-lane.ts +179 -0
- package/src/agent/email/style-reply.ts +358 -0
- package/src/agent/memory-prompt.ts +2 -1
- package/src/agent/skill-draft-proposer.ts +3 -3
- package/src/agent/unified-inbox.ts +393 -0
- package/src/tools/agent-harness-comms.ts +378 -0
- package/src/tools/agent-harness-learning-auto-promote.ts +288 -0
- package/src/tools/agent-harness-learning-curator.ts +9 -9
- package/src/tools/agent-harness-mode-catalog.ts +16 -0
- package/src/tools/agent-harness-personal-ops-discovery.ts +2 -1
- package/src/tools/agent-harness-personal-ops-lanes.ts +9 -5
- package/src/tools/agent-harness-remote.ts +220 -0
- package/src/tools/agent-harness-setup-smoke.ts +2 -4
- package/src/tools/agent-harness-tool-schema.ts +80 -1
- package/src/tools/agent-harness-tool-types.ts +18 -0
- package/src/tools/agent-harness-tool.ts +61 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* unified-inbox.ts
|
|
3
|
+
*
|
|
4
|
+
* Agent-side unified inbox read-model. Aggregates the three existing daemon-exposed
|
|
5
|
+
* sources that the triage layer already fetches:
|
|
6
|
+
*
|
|
7
|
+
* 1. /api/deliveries — outbound delivery attempts (status / failures)
|
|
8
|
+
* 2. /api/control-plane/messages — surface messages visible to the TUI client
|
|
9
|
+
* 3. /api/routes/bindings — live route binding continuity records
|
|
10
|
+
*
|
|
11
|
+
* SEAM — Provider-specific inbound inbox feeds (Slack DMs, Discord DMs, email
|
|
12
|
+
* threads, etc.) are NOT published by any current daemon contract. When the
|
|
13
|
+
* daemon publishes a `channels.inbox.*` operator method or a matching REST
|
|
14
|
+
* endpoint, add an adapter here by implementing `InboundChannelFeedAdapter` and
|
|
15
|
+
* registering it in `aggregateUnifiedInbox`. The rest of the model is already
|
|
16
|
+
* shaped to accept per-channel inbound items.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { AgentWorkspaceChannelTriage } from '../input/agent-workspace-channel-triage.ts';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Item kinds
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/** An outbound delivery attempt reported by /api/deliveries. */
|
|
26
|
+
export interface UnifiedInboxDeliveryItem {
|
|
27
|
+
readonly kind: 'delivery';
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly runId: string;
|
|
30
|
+
readonly jobId: string;
|
|
31
|
+
readonly status: 'queued' | 'sending' | 'completed' | 'failed' | 'dead_lettered' | string;
|
|
32
|
+
readonly target: UnifiedInboxTarget;
|
|
33
|
+
readonly startedAt?: number;
|
|
34
|
+
readonly endedAt?: number;
|
|
35
|
+
readonly error?: string;
|
|
36
|
+
readonly responseId?: string;
|
|
37
|
+
/** Inspect route for the daemon UI / model tool. */
|
|
38
|
+
readonly inspectRoute: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A control-plane surface message reported by /api/control-plane/messages. */
|
|
42
|
+
export interface UnifiedInboxSurfaceMessageItem {
|
|
43
|
+
readonly kind: 'surface_message';
|
|
44
|
+
readonly id: string;
|
|
45
|
+
readonly surface: string;
|
|
46
|
+
readonly level: 'info' | 'warning' | 'error' | string;
|
|
47
|
+
readonly title: string;
|
|
48
|
+
readonly bodyPreview: string;
|
|
49
|
+
readonly routeId: string | null;
|
|
50
|
+
readonly surfaceId: string | null;
|
|
51
|
+
readonly clientId: string | null;
|
|
52
|
+
readonly attachmentCount: number;
|
|
53
|
+
readonly createdAt: number | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A route binding reported by /api/routes/bindings. */
|
|
57
|
+
export interface UnifiedInboxRouteBindingItem {
|
|
58
|
+
readonly kind: 'route_binding';
|
|
59
|
+
readonly id: string;
|
|
60
|
+
readonly bindingKind: string;
|
|
61
|
+
readonly surfaceKind: string;
|
|
62
|
+
readonly surfaceId: string | null;
|
|
63
|
+
/** External id is digested — never the raw identifier. */
|
|
64
|
+
readonly externalIdDigest: string | null;
|
|
65
|
+
readonly sessionPolicy: string | null;
|
|
66
|
+
readonly threadPolicy: string | null;
|
|
67
|
+
readonly deliveryGuarantee: string | null;
|
|
68
|
+
readonly lastSeenAt: number | null;
|
|
69
|
+
readonly sessionId: string | null;
|
|
70
|
+
readonly runId: string | null;
|
|
71
|
+
readonly jobId: string | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type UnifiedInboxItem =
|
|
75
|
+
| UnifiedInboxDeliveryItem
|
|
76
|
+
| UnifiedInboxSurfaceMessageItem
|
|
77
|
+
| UnifiedInboxRouteBindingItem;
|
|
78
|
+
|
|
79
|
+
export interface UnifiedInboxTarget {
|
|
80
|
+
readonly kind: string;
|
|
81
|
+
readonly surfaceKind: string | null;
|
|
82
|
+
readonly routeId: string | null;
|
|
83
|
+
readonly label: string | null;
|
|
84
|
+
readonly address?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Aggregate model
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
export type UnifiedInboxSourceState = 'ready' | 'empty' | 'unavailable';
|
|
92
|
+
|
|
93
|
+
export interface UnifiedInboxSource {
|
|
94
|
+
readonly name: string;
|
|
95
|
+
readonly route: string;
|
|
96
|
+
readonly state: UnifiedInboxSourceState;
|
|
97
|
+
readonly itemCount: number;
|
|
98
|
+
readonly error?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface UnifiedInboxSummary {
|
|
102
|
+
readonly totalItems: number;
|
|
103
|
+
readonly deliveryItems: number;
|
|
104
|
+
readonly surfaceMessageItems: number;
|
|
105
|
+
readonly routeBindingItems: number;
|
|
106
|
+
readonly attentionCount: number;
|
|
107
|
+
readonly failureCount: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* SEAM: When the daemon exposes per-channel inbound feeds, add items here.
|
|
112
|
+
* The `inboundChannelFeed` field is intentionally typed as a discriminated union
|
|
113
|
+
* so callers can detect the absent-contract case at compile time.
|
|
114
|
+
*/
|
|
115
|
+
export type InboundChannelFeedState =
|
|
116
|
+
| { readonly available: false; readonly reason: 'contract_not_published'; readonly daemonMethodNeeded: 'channels.inbox.list' }
|
|
117
|
+
| { readonly available: true; readonly items: readonly UnifiedInboxItem[] };
|
|
118
|
+
|
|
119
|
+
export interface UnifiedInbox {
|
|
120
|
+
readonly mode: 'unified_inbox';
|
|
121
|
+
readonly status: 'ready' | 'attention' | 'blocked';
|
|
122
|
+
readonly summary: UnifiedInboxSummary;
|
|
123
|
+
readonly items: readonly UnifiedInboxItem[];
|
|
124
|
+
readonly sources: readonly UnifiedInboxSource[];
|
|
125
|
+
/** Outbound delivery attempts. Subset of items where kind === 'delivery'. */
|
|
126
|
+
readonly deliveryItems: readonly UnifiedInboxDeliveryItem[];
|
|
127
|
+
/** Control-plane surface messages. Subset of items where kind === 'surface_message'. */
|
|
128
|
+
readonly surfaceMessageItems: readonly UnifiedInboxSurfaceMessageItem[];
|
|
129
|
+
/** Route binding continuity. Subset of items where kind === 'route_binding'. */
|
|
130
|
+
readonly routeBindingItems: readonly UnifiedInboxRouteBindingItem[];
|
|
131
|
+
/**
|
|
132
|
+
* SEAM: Provider-specific inbound channel feed.
|
|
133
|
+
*
|
|
134
|
+
* This will remain `{ available: false }` until the daemon publishes the
|
|
135
|
+
* `channels.inbox.list` operator method (or equivalent REST endpoint). Once
|
|
136
|
+
* published, replace the `InboundChannelFeedState` branch here and add an
|
|
137
|
+
* adapter in `aggregateUnifiedInbox`.
|
|
138
|
+
*/
|
|
139
|
+
readonly inboundChannelFeed: InboundChannelFeedState;
|
|
140
|
+
readonly policy: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Internal helpers — converting triage sub-records to typed items
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
148
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readString(record: Record<string, unknown>, key: string, fallback = ''): string {
|
|
152
|
+
const value = record[key];
|
|
153
|
+
return typeof value === 'string' ? value : fallback;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readNumber(record: Record<string, unknown>, key: string): number | null {
|
|
157
|
+
const value = record[key];
|
|
158
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readStringOrNull(record: Record<string, unknown>, key: string): string | null {
|
|
162
|
+
const value = record[key];
|
|
163
|
+
return typeof value === 'string' && value ? value : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function toDeliveryItem(raw: Record<string, unknown>): UnifiedInboxDeliveryItem {
|
|
167
|
+
const target = isRecord(raw.target) ? raw.target : {};
|
|
168
|
+
return {
|
|
169
|
+
kind: 'delivery',
|
|
170
|
+
id: readString(raw, 'id', 'delivery'),
|
|
171
|
+
runId: readString(raw, 'runId'),
|
|
172
|
+
jobId: readString(raw, 'jobId'),
|
|
173
|
+
status: readString(raw, 'status', 'unknown'),
|
|
174
|
+
target: {
|
|
175
|
+
kind: readString(target, 'kind', 'unknown'),
|
|
176
|
+
surfaceKind: readStringOrNull(target, 'surfaceKind'),
|
|
177
|
+
routeId: readStringOrNull(target, 'routeId'),
|
|
178
|
+
label: readStringOrNull(target, 'label'),
|
|
179
|
+
...(typeof target.address === 'string' && target.address ? { address: target.address } : {}),
|
|
180
|
+
},
|
|
181
|
+
...(readNumber(raw, 'startedAt') !== null ? { startedAt: readNumber(raw, 'startedAt') as number } : {}),
|
|
182
|
+
...(readNumber(raw, 'endedAt') !== null ? { endedAt: readNumber(raw, 'endedAt') as number } : {}),
|
|
183
|
+
...(readString(raw, 'error') ? { error: readString(raw, 'error') } : {}),
|
|
184
|
+
...(readString(raw, 'responseId') ? { responseId: readString(raw, 'responseId') } : {}),
|
|
185
|
+
inspectRoute: readString(raw, 'inspectRoute', `/api/deliveries/${encodeURIComponent(readString(raw, 'id', 'delivery'))}`),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function toSurfaceMessageItem(raw: Record<string, unknown>): UnifiedInboxSurfaceMessageItem {
|
|
190
|
+
return {
|
|
191
|
+
kind: 'surface_message',
|
|
192
|
+
id: readString(raw, 'id', 'message'),
|
|
193
|
+
surface: readString(raw, 'surface', 'unknown'),
|
|
194
|
+
level: readString(raw, 'level', 'info'),
|
|
195
|
+
title: readString(raw, 'title'),
|
|
196
|
+
bodyPreview: readString(raw, 'bodyPreview'),
|
|
197
|
+
routeId: readStringOrNull(raw, 'routeId'),
|
|
198
|
+
surfaceId: readStringOrNull(raw, 'surfaceId'),
|
|
199
|
+
clientId: readStringOrNull(raw, 'clientId'),
|
|
200
|
+
attachmentCount: typeof raw.attachmentCount === 'number' ? raw.attachmentCount : 0,
|
|
201
|
+
createdAt: readNumber(raw, 'createdAt'),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function toRouteBindingItem(raw: Record<string, unknown>): UnifiedInboxRouteBindingItem {
|
|
206
|
+
return {
|
|
207
|
+
kind: 'route_binding',
|
|
208
|
+
id: readString(raw, 'id', 'binding'),
|
|
209
|
+
bindingKind: readString(raw, 'kind', 'unknown'),
|
|
210
|
+
surfaceKind: readString(raw, 'surfaceKind', 'unknown'),
|
|
211
|
+
surfaceId: readStringOrNull(raw, 'surfaceId'),
|
|
212
|
+
externalIdDigest: readStringOrNull(raw, 'externalIdDigest'),
|
|
213
|
+
sessionPolicy: readStringOrNull(raw, 'sessionPolicy'),
|
|
214
|
+
threadPolicy: readStringOrNull(raw, 'threadPolicy'),
|
|
215
|
+
deliveryGuarantee: readStringOrNull(raw, 'deliveryGuarantee'),
|
|
216
|
+
lastSeenAt: readNumber(raw, 'lastSeenAt'),
|
|
217
|
+
sessionId: readStringOrNull(raw, 'sessionId'),
|
|
218
|
+
runId: readStringOrNull(raw, 'runId'),
|
|
219
|
+
jobId: readStringOrNull(raw, 'jobId'),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const DELIVERY_ATTENTION_STATUSES = new Set(['failed', 'dead_lettered', 'pending', 'sending']);
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Public aggregation API
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Build a UnifiedInbox from an already-fetched AgentWorkspaceChannelTriage.
|
|
231
|
+
*
|
|
232
|
+
* This is intentionally a pure transformation — it does not perform any I/O.
|
|
233
|
+
* The caller (command handler or tool) is responsible for calling
|
|
234
|
+
* `buildAgentWorkspaceChannelTriage` first.
|
|
235
|
+
*/
|
|
236
|
+
export function aggregateUnifiedInbox(
|
|
237
|
+
triage: AgentWorkspaceChannelTriage,
|
|
238
|
+
options: { readonly limit?: number } = {},
|
|
239
|
+
): UnifiedInbox {
|
|
240
|
+
const limit = typeof options.limit === 'number' && options.limit > 0 ? Math.min(options.limit, 200) : 50;
|
|
241
|
+
|
|
242
|
+
// --- Delivery items ---
|
|
243
|
+
const deliveriesSection = triage.deliveries;
|
|
244
|
+
const rawAttempts = Array.isArray(deliveriesSection.attempts)
|
|
245
|
+
? (deliveriesSection.attempts as unknown[]).filter(isRecord)
|
|
246
|
+
: [];
|
|
247
|
+
const deliveryItems = rawAttempts.slice(0, limit).map(toDeliveryItem);
|
|
248
|
+
const deliveriesState: UnifiedInboxSourceState = deliveriesSection.state === 'unavailable'
|
|
249
|
+
? 'unavailable'
|
|
250
|
+
: deliveriesSection.state === 'empty' || deliveryItems.length === 0
|
|
251
|
+
? 'empty'
|
|
252
|
+
: 'ready';
|
|
253
|
+
|
|
254
|
+
// --- Surface message items ---
|
|
255
|
+
const messagesSection = triage.surfaceMessages;
|
|
256
|
+
const rawMessages = Array.isArray(messagesSection.messages)
|
|
257
|
+
? (messagesSection.messages as unknown[]).filter(isRecord)
|
|
258
|
+
: [];
|
|
259
|
+
const surfaceMessageItems = rawMessages.slice(0, limit).map(toSurfaceMessageItem);
|
|
260
|
+
const messagesState: UnifiedInboxSourceState = messagesSection.state === 'unavailable'
|
|
261
|
+
? 'unavailable'
|
|
262
|
+
: messagesSection.state === 'empty' || surfaceMessageItems.length === 0
|
|
263
|
+
? 'empty'
|
|
264
|
+
: 'ready';
|
|
265
|
+
|
|
266
|
+
// --- Route binding items ---
|
|
267
|
+
const bindingsSection = triage.routeBindings;
|
|
268
|
+
const rawBindings = Array.isArray(bindingsSection.bindings)
|
|
269
|
+
? (bindingsSection.bindings as unknown[]).filter(isRecord)
|
|
270
|
+
: [];
|
|
271
|
+
const routeBindingItems = rawBindings.slice(0, limit).map(toRouteBindingItem);
|
|
272
|
+
const bindingsState: UnifiedInboxSourceState = bindingsSection.state === 'unavailable'
|
|
273
|
+
? 'unavailable'
|
|
274
|
+
: bindingsSection.state === 'empty' || routeBindingItems.length === 0
|
|
275
|
+
? 'empty'
|
|
276
|
+
: 'ready';
|
|
277
|
+
|
|
278
|
+
const items: UnifiedInboxItem[] = [
|
|
279
|
+
...deliveryItems,
|
|
280
|
+
...surfaceMessageItems,
|
|
281
|
+
...routeBindingItems,
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
const attentionCount = deliveryItems.filter((item) => DELIVERY_ATTENTION_STATUSES.has(item.status)).length;
|
|
285
|
+
const failureCount = deliveryItems.filter((item) => item.status === 'failed' || item.status === 'dead_lettered').length;
|
|
286
|
+
|
|
287
|
+
const sources: UnifiedInboxSource[] = [
|
|
288
|
+
{
|
|
289
|
+
name: 'deliveries',
|
|
290
|
+
route: '/api/deliveries',
|
|
291
|
+
state: deliveriesState,
|
|
292
|
+
itemCount: deliveryItems.length,
|
|
293
|
+
...(deliveriesState === 'unavailable' && typeof deliveriesSection.message === 'string'
|
|
294
|
+
? { error: deliveriesSection.message as string }
|
|
295
|
+
: {}),
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: 'surface_messages',
|
|
299
|
+
route: '/api/control-plane/messages',
|
|
300
|
+
state: messagesState,
|
|
301
|
+
itemCount: surfaceMessageItems.length,
|
|
302
|
+
...(messagesState === 'unavailable' && typeof messagesSection.message === 'string'
|
|
303
|
+
? { error: messagesSection.message as string }
|
|
304
|
+
: {}),
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: 'route_bindings',
|
|
308
|
+
route: '/api/routes/bindings',
|
|
309
|
+
state: bindingsState,
|
|
310
|
+
itemCount: routeBindingItems.length,
|
|
311
|
+
...(bindingsState === 'unavailable' && typeof bindingsSection.message === 'string'
|
|
312
|
+
? { error: bindingsSection.message as string }
|
|
313
|
+
: {}),
|
|
314
|
+
},
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
const blockedSources = sources.filter((s) => s.state === 'unavailable').length;
|
|
318
|
+
const overallStatus: UnifiedInbox['status'] =
|
|
319
|
+
blockedSources === sources.length ? 'blocked' : attentionCount > 0 ? 'attention' : 'ready';
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
mode: 'unified_inbox',
|
|
323
|
+
status: overallStatus,
|
|
324
|
+
summary: {
|
|
325
|
+
totalItems: items.length,
|
|
326
|
+
deliveryItems: deliveryItems.length,
|
|
327
|
+
surfaceMessageItems: surfaceMessageItems.length,
|
|
328
|
+
routeBindingItems: routeBindingItems.length,
|
|
329
|
+
attentionCount,
|
|
330
|
+
failureCount,
|
|
331
|
+
},
|
|
332
|
+
items,
|
|
333
|
+
sources,
|
|
334
|
+
deliveryItems,
|
|
335
|
+
surfaceMessageItems,
|
|
336
|
+
routeBindingItems,
|
|
337
|
+
inboundChannelFeed: {
|
|
338
|
+
available: false,
|
|
339
|
+
reason: 'contract_not_published',
|
|
340
|
+
daemonMethodNeeded: 'channels.inbox.list',
|
|
341
|
+
},
|
|
342
|
+
policy:
|
|
343
|
+
'Read-only unified inbox. Aggregates daemon-exposed delivery attempts, control-plane surface messages, and route bindings. ' +
|
|
344
|
+
'Provider-specific inbound channel feeds (Slack DMs, Discord messages, email threads) are not yet published by the daemon contract — ' +
|
|
345
|
+
'field inboundChannelFeed.available === false until daemon publishes channels.inbox.list.',
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Format a UnifiedInbox for human-readable output.
|
|
351
|
+
*/
|
|
352
|
+
export function formatUnifiedInbox(inbox: UnifiedInbox): string {
|
|
353
|
+
const { summary, sources, deliveryItems, surfaceMessageItems, routeBindingItems } = inbox;
|
|
354
|
+
const lines: string[] = [
|
|
355
|
+
'Unified Inbox',
|
|
356
|
+
` status: ${inbox.status}`,
|
|
357
|
+
` items: ${summary.totalItems} (deliveries: ${summary.deliveryItems}, surface messages: ${summary.surfaceMessageItems}, route bindings: ${summary.routeBindingItems})`,
|
|
358
|
+
` attention: ${summary.attentionCount} failures: ${summary.failureCount}`,
|
|
359
|
+
` inbound channel feed: ${inbox.inboundChannelFeed.available ? 'available' : `not available — daemon method needed: ${inbox.inboundChannelFeed.daemonMethodNeeded}`}`,
|
|
360
|
+
` policy: ${inbox.policy}`,
|
|
361
|
+
'',
|
|
362
|
+
' Sources',
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
for (const source of sources) {
|
|
366
|
+
const errorPart = source.error ? ` error=${source.error}` : '';
|
|
367
|
+
lines.push(` ${source.name}: ${source.state} (${source.itemCount} items) route=${source.route}${errorPart}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (deliveryItems.length > 0) {
|
|
371
|
+
lines.push('', ' Delivery Attempts');
|
|
372
|
+
for (const item of deliveryItems.slice(0, 10)) {
|
|
373
|
+
const targetDesc = [item.target.surfaceKind, item.target.routeId, item.target.label].filter(Boolean).join('/');
|
|
374
|
+
lines.push(` - [${item.status}] ${item.id} target=${targetDesc || item.target.kind}${item.error ? ` err=${item.error}` : ''}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (surfaceMessageItems.length > 0) {
|
|
379
|
+
lines.push('', ' Surface Messages');
|
|
380
|
+
for (const item of surfaceMessageItems.slice(0, 10)) {
|
|
381
|
+
lines.push(` - [${item.level}] ${item.surface}: ${item.title}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (routeBindingItems.length > 0) {
|
|
386
|
+
lines.push('', ' Route Bindings');
|
|
387
|
+
for (const item of routeBindingItems.slice(0, 10)) {
|
|
388
|
+
lines.push(` - ${item.id}: ${item.surfaceKind} ${item.bindingKind} ext=${item.externalIdDigest ?? 'none'}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return lines.join('\n');
|
|
393
|
+
}
|