@agent-native/core 0.176.4-nightly-20260902024948 → 0.176.4-nightly-20260902103608
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/corpus/README.md +1 -1
- package/corpus/templates/assets/.agents/skills/a2a-assets/SKILL.md +8 -6
- package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +7 -5
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +5 -2
- package/corpus/templates/assets/actions/_helpers.ts +106 -0
- package/corpus/templates/assets/actions/generate-image-batch.ts +38 -2
- package/corpus/templates/assets/actions/generate-image.ts +6 -5
- package/corpus/templates/assets/actions/list-assets.ts +25 -3
- package/corpus/templates/assets/changelog/2026-09-01-image-generation-returns-compact-results.md +6 -0
- package/corpus/templates/slides/server/lib/assets-image-delegation.ts +2 -1
- package/dist/agent/chat-health-alert.d.ts +6 -0
- package/dist/agent/chat-health-alert.js +117 -33
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/channels.js +2 -0
- package/dist/observability/routes.d.ts +6 -6
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/package.json +3 -3
package/corpus/README.md
CHANGED
|
@@ -22,8 +22,9 @@ delegation, but A2A replies cannot render MCP App pickers.
|
|
|
22
22
|
`callerAppId` with the calling app id (`slides`, `design`, `content`,
|
|
23
23
|
`mail`) so the Assets audit log can group cross-agent generations.
|
|
24
24
|
3. Treat image batches as complete when the action returns. Use returned
|
|
25
|
-
successful `images` entries directly; only regenerate slots that
|
|
26
|
-
`ok: false`.
|
|
25
|
+
successful compact `images` entries directly; only regenerate slots that
|
|
26
|
+
returned `ok: false`. Use `get-asset` for full asset details and
|
|
27
|
+
`get-audit-run` / `list-audit-runs` for prompts, references, and settings.
|
|
27
28
|
A result carrying `draftPendingApproval: true` means the caller can draft in
|
|
28
29
|
that kit but not save into it. The image is real and usable — say it is
|
|
29
30
|
waiting on a kit editor instead of reporting it as saved, and do not retry.
|
|
@@ -33,12 +34,13 @@ delegation, but A2A replies cannot render MCP App pickers.
|
|
|
33
34
|
5. When a human designer needs to continue the work, create or update a
|
|
34
35
|
generation session and preserve the returned `sessionId`.
|
|
35
36
|
6. For video, call `generate-video` and then `refresh-generation-run` until the run completes.
|
|
36
|
-
7. Preserve returned `
|
|
37
|
-
|
|
37
|
+
7. Preserve returned `id`, `runId`, `previewUrl`, `downloadUrl`, and `embedUrl`
|
|
38
|
+
exactly.
|
|
38
39
|
8. Insert chosen/exported URLs into the caller's artifact. Design callers should
|
|
39
40
|
call `insert-asset` after the picker returns a selected asset.
|
|
40
|
-
9. On feedback, call `refine-image` with the prior
|
|
41
|
-
and the same `callerAppId`, then replace only the
|
|
41
|
+
9. On feedback, call `refine-image` with `assetId` set to the prior result's
|
|
42
|
+
`id`, `source: "a2a"`, and the same `callerAppId`, then replace only the
|
|
43
|
+
affected destination.
|
|
42
44
|
|
|
43
45
|
## Audit Trail
|
|
44
46
|
|
|
@@ -89,9 +89,10 @@ claiming a brand match.
|
|
|
89
89
|
3. For one asset, call `generate-image`; for multiple independent slots, call
|
|
90
90
|
`generate-image-batch` with stable `slotId` values.
|
|
91
91
|
4. Image generation actions are synchronous. After `generate-image` or
|
|
92
|
-
`generate-image-batch` returns, use its
|
|
92
|
+
`generate-image-batch` returns, use its compact `images` / asset summaries
|
|
93
93
|
directly; do not call `get-generation-run`, `refresh-generation-run`, or
|
|
94
|
-
regenerate just to verify image runs.
|
|
94
|
+
regenerate just to verify image runs. Use `get-asset` for full asset details
|
|
95
|
+
and the audit-run actions for prompts, references, and settings.
|
|
95
96
|
A result with `draftPendingApproval: true` came from a kit the user can draft
|
|
96
97
|
in but not save into. Offer the candidate and say it needs a kit editor to be
|
|
97
98
|
saved; `save-generated-image` will refuse, so do not call it or retry. Video
|
|
@@ -112,9 +113,10 @@ claiming a brand match.
|
|
|
112
113
|
another ratio (16:9, 9:16, 4:5, 21:9, …), pick a Gemini model rather than
|
|
113
114
|
`gpt-image-2` — an unsupported pairing is rejected upstream. Source of truth
|
|
114
115
|
is `supportedAspectRatiosForModel` / `MODEL_ASPECT_RATIOS` in `shared/api.ts`.
|
|
115
|
-
8.
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
8. Direct generation returns `id`; picker selections return `assetId`. Preserve
|
|
117
|
+
that asset identifier with `runId`, `previewUrl`, `downloadUrl`, and
|
|
118
|
+
`embedUrl`. Preserve the immutable `contextPackId` and reuse labels on both
|
|
119
|
+
generation run and output-asset metadata; rendered pixels are not provenance.
|
|
118
120
|
9. Use `refine-image` for feedback on an existing asset, `edit-image` for
|
|
119
121
|
targeted changes, and `restyle-image` with `subjectAssetId` and
|
|
120
122
|
`styleStrength` for subject-preserving brand restyles.
|
|
@@ -54,10 +54,13 @@ evidence, screenshots, or exact logos.
|
|
|
54
54
|
Use `generate-image-batch` with stable `slotId`s so the shared generation tray
|
|
55
55
|
can show live slots.
|
|
56
56
|
- `generate-image` and `generate-image-batch` are synchronous for images. One
|
|
57
|
-
batch call should produce the requested candidates and return
|
|
58
|
-
IDs
|
|
57
|
+
batch call should produce the requested candidates and return compact asset
|
|
58
|
+
summaries with IDs and URLs; do not follow it with `get-generation-run`,
|
|
59
59
|
`refresh-generation-run`, or more generation unless the user asks for another
|
|
60
60
|
direction or the returned slot has `ok: false`.
|
|
61
|
+
- Use `get-asset` when full asset details are needed. Use `get-audit-run` or
|
|
62
|
+
`list-audit-runs` for the prompt, compiled prompt, references, and generation
|
|
63
|
+
settings.
|
|
61
64
|
- For repeatable deliverables, honor a `template` @mention as `templateId` or
|
|
62
65
|
call `list-templates` when choosing one. Pass the template through
|
|
63
66
|
`generate-image`, `generate-image-batch`, `refine-image`, or
|
|
@@ -438,6 +438,112 @@ export function serializeAsset(
|
|
|
438
438
|
};
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Caller-facing generation result. Generation internals are replayed into the
|
|
443
|
+
* model context on later turns and can be truncated by delegated/ledger caps;
|
|
444
|
+
* full details remain available through get-asset and the audit-run actions.
|
|
445
|
+
*/
|
|
446
|
+
export function serializeAssetSummary(row: {
|
|
447
|
+
id: string;
|
|
448
|
+
generationRunId?: string | null;
|
|
449
|
+
title?: string | null;
|
|
450
|
+
libraryId: string;
|
|
451
|
+
collectionId?: string | null;
|
|
452
|
+
status: string;
|
|
453
|
+
mediaType?: string | null;
|
|
454
|
+
aspectRatio?: string | null;
|
|
455
|
+
width?: number | null;
|
|
456
|
+
height?: number | null;
|
|
457
|
+
mimeType: string;
|
|
458
|
+
objectKey: string;
|
|
459
|
+
thumbnailObjectKey?: string | null;
|
|
460
|
+
}) {
|
|
461
|
+
const urls = assetUrls(row);
|
|
462
|
+
return {
|
|
463
|
+
id: row.id,
|
|
464
|
+
runId: row.generationRunId ?? null,
|
|
465
|
+
artifactType: "image" as const,
|
|
466
|
+
title: row.title ?? null,
|
|
467
|
+
libraryId: row.libraryId,
|
|
468
|
+
collectionId: row.collectionId ?? null,
|
|
469
|
+
status: row.status,
|
|
470
|
+
mediaType:
|
|
471
|
+
row.mediaType ?? (row.mimeType.startsWith("video/") ? "video" : "image"),
|
|
472
|
+
aspectRatio: row.aspectRatio ?? null,
|
|
473
|
+
width: row.width ?? null,
|
|
474
|
+
height: row.height ?? null,
|
|
475
|
+
mimeType: row.mimeType,
|
|
476
|
+
url: urls.url,
|
|
477
|
+
previewUrl: urls.previewUrl,
|
|
478
|
+
downloadUrl: urls.downloadUrl,
|
|
479
|
+
embedUrl: urls.embedUrl,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function serializeAssetListItem(
|
|
484
|
+
row: {
|
|
485
|
+
id: string;
|
|
486
|
+
libraryId: string;
|
|
487
|
+
collectionId?: string | null;
|
|
488
|
+
folderId?: string | null;
|
|
489
|
+
mediaType?: string | null;
|
|
490
|
+
role: string;
|
|
491
|
+
status: string;
|
|
492
|
+
title?: string | null;
|
|
493
|
+
description?: string | null;
|
|
494
|
+
altText?: string | null;
|
|
495
|
+
prompt?: string | null;
|
|
496
|
+
model?: string | null;
|
|
497
|
+
aspectRatio?: string | null;
|
|
498
|
+
mimeType: string;
|
|
499
|
+
width?: number | null;
|
|
500
|
+
height?: number | null;
|
|
501
|
+
durationSeconds?: number | null;
|
|
502
|
+
objectKey: string;
|
|
503
|
+
thumbnailObjectKey?: string | null;
|
|
504
|
+
metadata?: string | null;
|
|
505
|
+
},
|
|
506
|
+
lineage: AssetLineageSummary | null = null,
|
|
507
|
+
) {
|
|
508
|
+
const metadata = parseJson<Record<string, unknown>>(row.metadata, {});
|
|
509
|
+
const urls = assetUrls(row);
|
|
510
|
+
return {
|
|
511
|
+
id: row.id,
|
|
512
|
+
libraryId: row.libraryId,
|
|
513
|
+
collectionId: row.collectionId ?? null,
|
|
514
|
+
folderId: row.folderId ?? null,
|
|
515
|
+
mediaType:
|
|
516
|
+
row.mediaType ?? (row.mimeType.startsWith("video/") ? "video" : "image"),
|
|
517
|
+
role: row.role,
|
|
518
|
+
status: row.status,
|
|
519
|
+
category: metadata.category ?? null,
|
|
520
|
+
title: row.title ?? null,
|
|
521
|
+
description: row.description ?? metadata.description ?? null,
|
|
522
|
+
altText: row.altText ?? null,
|
|
523
|
+
prompt: row.prompt ?? null,
|
|
524
|
+
model: row.model ?? null,
|
|
525
|
+
aspectRatio: row.aspectRatio ?? null,
|
|
526
|
+
mimeType: row.mimeType,
|
|
527
|
+
width: row.width ?? null,
|
|
528
|
+
height: row.height ?? null,
|
|
529
|
+
durationSeconds: row.durationSeconds ?? null,
|
|
530
|
+
metadata: {
|
|
531
|
+
category: metadata.category ?? null,
|
|
532
|
+
intent: metadata.intent ?? null,
|
|
533
|
+
description: metadata.description ?? null,
|
|
534
|
+
originalName: metadata.originalName ?? null,
|
|
535
|
+
provider: metadata.provider ?? null,
|
|
536
|
+
},
|
|
537
|
+
lineage,
|
|
538
|
+
url: urls.url,
|
|
539
|
+
previewUrl: urls.previewUrl,
|
|
540
|
+
thumbnailUrl: urls.thumbnailUrl,
|
|
541
|
+
downloadUrl: urls.downloadUrl,
|
|
542
|
+
embedPath: urls.embedPath,
|
|
543
|
+
embedUrl: urls.embedUrl,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
441
547
|
export async function getAssetOrThrow(id: string) {
|
|
442
548
|
const db = getDb();
|
|
443
549
|
const [asset] = await db
|
|
@@ -70,7 +70,7 @@ const imageBatchAgentInputSchema = z.object({
|
|
|
70
70
|
|
|
71
71
|
export default defineAction({
|
|
72
72
|
description:
|
|
73
|
-
"Generate several brand-consistent images in parallel from one brand kit/library. Use @brand-kit mentions as libraryId and @preset mentions as presetId when present. If no preset is tagged, call list-generation-presets first and use a matching preset's presetId; the user may not know presets exist. Generate presetless only when no preset matches the request. This is synchronous for images: one call waits for every slot and returns
|
|
73
|
+
"Generate several brand-consistent images in parallel from one brand kit/library. Use @brand-kit mentions as libraryId and @preset mentions as presetId when present. If no preset is tagged, call list-generation-presets first and use a matching preset's presetId; the user may not know presets exist. Generate presetless only when no preset matches the request. This is synchronous for images: one call waits for every slot and returns compact image summaries; use get-asset for full asset details and get-audit-run for prompt, references, and settings. Use this for slide decks, landing pages, and multi-slot design work. Do not call get-generation-run or refresh-generation-run after a normal image batch result.",
|
|
74
74
|
schema: z.object({
|
|
75
75
|
libraryId: z
|
|
76
76
|
.string()
|
|
@@ -296,15 +296,45 @@ export default defineAction({
|
|
|
296
296
|
.where(eq(schema.assetGenerationSessions.id, base.sessionId));
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
|
+
const creativeContextProvenance = firstCreativeContextProvenance(results);
|
|
299
300
|
return {
|
|
300
301
|
count: results.length,
|
|
301
302
|
images: results.map((result, index) =>
|
|
302
303
|
serializeBatchResult(slots[index].slotId, result),
|
|
303
304
|
),
|
|
305
|
+
...creativeContextProvenance,
|
|
304
306
|
};
|
|
305
307
|
},
|
|
306
308
|
});
|
|
307
309
|
|
|
310
|
+
function firstCreativeContextProvenance(
|
|
311
|
+
results: PromiseSettledResult<Record<string, unknown>>[],
|
|
312
|
+
) {
|
|
313
|
+
for (const result of results) {
|
|
314
|
+
if (result.status !== "fulfilled") continue;
|
|
315
|
+
const contextMode = result.value.contextMode;
|
|
316
|
+
if (
|
|
317
|
+
contextMode !== "off" &&
|
|
318
|
+
contextMode !== "auto" &&
|
|
319
|
+
contextMode !== "pinned"
|
|
320
|
+
) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const contextPackId = result.value.contextPackId;
|
|
324
|
+
return {
|
|
325
|
+
contextMode,
|
|
326
|
+
contextPackId:
|
|
327
|
+
typeof contextPackId === "string" || contextPackId === null
|
|
328
|
+
? contextPackId
|
|
329
|
+
: null,
|
|
330
|
+
reuseLabels: Array.isArray(result.value.reuseLabels)
|
|
331
|
+
? result.value.reuseLabels
|
|
332
|
+
: [],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
return {};
|
|
336
|
+
}
|
|
337
|
+
|
|
308
338
|
function firstSuccessfulAssetId(
|
|
309
339
|
results: PromiseSettledResult<Record<string, unknown>>[],
|
|
310
340
|
): string | null {
|
|
@@ -351,7 +381,13 @@ function serializeBatchResult(
|
|
|
351
381
|
};
|
|
352
382
|
}
|
|
353
383
|
|
|
354
|
-
|
|
384
|
+
const {
|
|
385
|
+
contextMode: _contextMode,
|
|
386
|
+
contextPackId: _contextPackId,
|
|
387
|
+
reuseLabels: _reuseLabels,
|
|
388
|
+
...image
|
|
389
|
+
} = result.value;
|
|
390
|
+
return { slotId, ok: true, ...image };
|
|
355
391
|
}
|
|
356
392
|
|
|
357
393
|
function imageAssetId(value: Record<string, unknown>): string | undefined {
|
|
@@ -73,9 +73,10 @@ import {
|
|
|
73
73
|
type StyleBrief,
|
|
74
74
|
} from "../shared/api.js";
|
|
75
75
|
import {
|
|
76
|
+
assetUrls,
|
|
76
77
|
imageArtifactLinks,
|
|
77
78
|
requireGenerationSessionInLibrary,
|
|
78
|
-
|
|
79
|
+
serializeAssetSummary,
|
|
79
80
|
} from "./_helpers.js";
|
|
80
81
|
import { readImageModelDefault } from "./_image-model-default.js";
|
|
81
82
|
import { resolveTemplateAccess } from "./_template-access.js";
|
|
@@ -127,7 +128,7 @@ const imageGenerationAgentInputSchema = z.object({
|
|
|
127
128
|
|
|
128
129
|
export default defineAction({
|
|
129
130
|
description:
|
|
130
|
-
"Generate one brand-consistent image from a brand kit/library. This is synchronous for images and returns
|
|
131
|
+
"Generate one brand-consistent image from a brand kit/library. This is synchronous for images and returns a compact asset summary with preview/download/embed URLs; use get-asset for full asset details and get-audit-run for prompt, references, and settings. Use @brand-kit mentions as libraryId and @preset mentions as presetId when present. If no preset is tagged, call list-generation-presets first and use a matching preset's presetId; the user may not know presets exist. Generate presetless only when no preset matches the request. Use generate-image-batch for multiple independent slots; do not poll image runs after this action returns.",
|
|
131
132
|
schema: z.object({
|
|
132
133
|
libraryId: z
|
|
133
134
|
.string()
|
|
@@ -1129,7 +1130,8 @@ export default defineAction({
|
|
|
1129
1130
|
}),
|
|
1130
1131
|
})
|
|
1131
1132
|
.where(eq(schema.assetGenerationRuns.id, runId));
|
|
1132
|
-
const serialized =
|
|
1133
|
+
const serialized = serializeAssetSummary(asset);
|
|
1134
|
+
const urls = assetUrls(asset);
|
|
1133
1135
|
await upsertVariantSlot({
|
|
1134
1136
|
runId,
|
|
1135
1137
|
batchId: args.variantBatchId ?? null,
|
|
@@ -1144,12 +1146,11 @@ export default defineAction({
|
|
|
1144
1146
|
status: "ready",
|
|
1145
1147
|
assetId: asset.id,
|
|
1146
1148
|
previewUrl: serialized.previewUrl,
|
|
1147
|
-
thumbnailUrl:
|
|
1149
|
+
thumbnailUrl: urls.thumbnailUrl,
|
|
1148
1150
|
});
|
|
1149
1151
|
return {
|
|
1150
1152
|
...serialized,
|
|
1151
1153
|
runId,
|
|
1152
|
-
artifactType: "image",
|
|
1153
1154
|
// Cross-app callers embed the artifact in HTML. `url` is the Assets
|
|
1154
1155
|
// detail page; previewUrl is the actual media response.
|
|
1155
1156
|
Artifacts: imageArtifactLinks({
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import {
|
|
19
19
|
buildAssetLineage,
|
|
20
20
|
requireLibrary,
|
|
21
|
-
|
|
21
|
+
serializeAssetListItem,
|
|
22
22
|
} from "./_helpers.js";
|
|
23
23
|
|
|
24
24
|
export default defineAction({
|
|
@@ -99,7 +99,29 @@ export default defineAction({
|
|
|
99
99
|
const candidateRunIdSet = new Set(candidateRunIds ?? []);
|
|
100
100
|
const [rows, lineageRows] = await Promise.all([
|
|
101
101
|
db
|
|
102
|
-
.select(
|
|
102
|
+
.select({
|
|
103
|
+
id: schema.assets.id,
|
|
104
|
+
libraryId: schema.assets.libraryId,
|
|
105
|
+
collectionId: schema.assets.collectionId,
|
|
106
|
+
folderId: schema.assets.folderId,
|
|
107
|
+
mediaType: schema.assets.mediaType,
|
|
108
|
+
role: schema.assets.role,
|
|
109
|
+
status: schema.assets.status,
|
|
110
|
+
title: schema.assets.title,
|
|
111
|
+
description: schema.assets.description,
|
|
112
|
+
altText: schema.assets.altText,
|
|
113
|
+
prompt: schema.assets.prompt,
|
|
114
|
+
model: schema.assets.model,
|
|
115
|
+
aspectRatio: schema.assets.aspectRatio,
|
|
116
|
+
mimeType: schema.assets.mimeType,
|
|
117
|
+
width: schema.assets.width,
|
|
118
|
+
height: schema.assets.height,
|
|
119
|
+
durationSeconds: schema.assets.durationSeconds,
|
|
120
|
+
objectKey: schema.assets.objectKey,
|
|
121
|
+
thumbnailObjectKey: schema.assets.thumbnailObjectKey,
|
|
122
|
+
generationRunId: schema.assets.generationRunId,
|
|
123
|
+
metadata: schema.assets.metadata,
|
|
124
|
+
})
|
|
103
125
|
.from(schema.assets)
|
|
104
126
|
.where(and(...filters))
|
|
105
127
|
.orderBy(desc(schema.assets.createdAt)),
|
|
@@ -148,7 +170,7 @@ export default defineAction({
|
|
|
148
170
|
})
|
|
149
171
|
.filter((asset) => assetMatchesSearch(asset, normalizedQuery, category))
|
|
150
172
|
.map((asset) => ({
|
|
151
|
-
...
|
|
173
|
+
...serializeAssetListItem(asset, lineageById.get(asset.id) ?? null),
|
|
152
174
|
libraryTitle: libraryTitleById.get(asset.libraryId) ?? null,
|
|
153
175
|
}));
|
|
154
176
|
return { count: assets.length, assets };
|
|
@@ -107,7 +107,8 @@ function buildDelegationMessage(request: AssetsImageRequest): string {
|
|
|
107
107
|
`${references.join(", ")}\n`
|
|
108
108
|
: "") +
|
|
109
109
|
`\nPick the best matching library via match-library if no libraryId is ` +
|
|
110
|
-
`obvious, then generate with generate-image-batch. Return
|
|
110
|
+
`obvious, then generate with generate-image-batch. Return id (the asset ` +
|
|
111
|
+
`ID), runId, ` +
|
|
111
112
|
`previewUrl, and downloadUrl verbatim so the slides agent can drop them ` +
|
|
112
113
|
`into the slide HTML. Set source: "a2a" and callerAppId: "slides" so the ` +
|
|
113
114
|
`Assets audit log groups these generations.`
|
|
@@ -18,6 +18,12 @@ export type ChatHealthAlertOutcome = {
|
|
|
18
18
|
turns: number;
|
|
19
19
|
badRate: number;
|
|
20
20
|
recipients: number;
|
|
21
|
+
} | {
|
|
22
|
+
status: "delivery-failed";
|
|
23
|
+
reason: string;
|
|
24
|
+
} | {
|
|
25
|
+
status: "persistence-failed";
|
|
26
|
+
reason: string;
|
|
21
27
|
} | {
|
|
22
28
|
status: "check-failed";
|
|
23
29
|
reason: string;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { getDbExec } from "../db/client.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { notifyWithDelivery } from "../notifications/registry.js";
|
|
4
|
+
import { runWithRequestContext } from "../server/request-context.js";
|
|
5
|
+
import { deleteSettingIfValue, mutateSetting } from "../settings/store.js";
|
|
4
6
|
/**
|
|
5
|
-
*
|
|
7
|
+
* Sends one Slack alert when an app's chat stops answering.
|
|
6
8
|
*
|
|
7
9
|
* The detector for this already existed as `scripts/chat-health.mjs --strict`,
|
|
8
10
|
* correctly calibrated and exiting 1 on a partial outage — but nothing ever ran
|
|
@@ -27,7 +29,9 @@ const MIN_TURNS = 5;
|
|
|
27
29
|
const BAD_RATE_THRESHOLD = 0.5;
|
|
28
30
|
/** One page per outage, not one per sweep. */
|
|
29
31
|
const COOLDOWN_MS = 60 * 60_000;
|
|
30
|
-
|
|
32
|
+
/** Slack sends time out well inside this lease; failed sends release it early. */
|
|
33
|
+
const CLAIM_LEASE_MS = 5 * 60_000;
|
|
34
|
+
const LAST_ALERT_SETTING_KEY = "chat-health-alert:last-slack-alert-at";
|
|
31
35
|
/**
|
|
32
36
|
* Scores the LAST run of each turn in the window, matching how
|
|
33
37
|
* `scripts/chat-health.mjs` reports so a page and the CLI never disagree.
|
|
@@ -57,16 +61,38 @@ async function countRecentTurns(since) {
|
|
|
57
61
|
bad: Number(row?.bad ?? 0),
|
|
58
62
|
};
|
|
59
63
|
}
|
|
60
|
-
/**
|
|
61
|
-
async function
|
|
64
|
+
/** Use one owner/admin only when the app has an unambiguous org scope. */
|
|
65
|
+
async function alertOwner() {
|
|
62
66
|
const { rows } = await getDbExec().execute({
|
|
63
|
-
sql: `SELECT
|
|
64
|
-
WHERE role IN ('owner', 'admin')
|
|
67
|
+
sql: `SELECT org_id, email, role FROM org_members
|
|
68
|
+
WHERE role IN ('owner', 'admin')
|
|
69
|
+
ORDER BY CASE role WHEN 'owner' THEN 0 WHEN 'admin' THEN 1 ELSE 2 END, email`,
|
|
65
70
|
args: [],
|
|
66
71
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
const orgIds = new Set(rows.map((row) => String(row.org_id ?? "")));
|
|
73
|
+
if (orgIds.size !== 1 || orgIds.has(""))
|
|
74
|
+
return null;
|
|
75
|
+
const recipient = rows.find((row) => {
|
|
76
|
+
const role = row.role;
|
|
77
|
+
return role === "owner" || role === "admin";
|
|
78
|
+
});
|
|
79
|
+
const email = String(recipient?.email ?? "");
|
|
80
|
+
const [orgId] = [...orgIds];
|
|
81
|
+
return email && orgId ? { owner: email, orgId } : null;
|
|
82
|
+
}
|
|
83
|
+
async function releaseAlertClaim(claimId, claimExpiresAt) {
|
|
84
|
+
try {
|
|
85
|
+
await deleteSettingIfValue(LAST_ALERT_SETTING_KEY, {
|
|
86
|
+
claimId,
|
|
87
|
+
claimExpiresAt,
|
|
88
|
+
});
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
const reason = "The Slack alert claim could not be released.";
|
|
93
|
+
console.error(`[chat-health-alert] ${reason}`, error);
|
|
94
|
+
return reason;
|
|
95
|
+
}
|
|
70
96
|
}
|
|
71
97
|
export async function checkChatHealthAndAlert(now = Date.now()) {
|
|
72
98
|
let counts;
|
|
@@ -84,58 +110,116 @@ export async function checkChatHealthAndAlert(now = Date.now()) {
|
|
|
84
110
|
if (badRate < BAD_RATE_THRESHOLD) {
|
|
85
111
|
return { status: "healthy", turns: counts.turns, badRate };
|
|
86
112
|
}
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
113
|
+
// Claim the page before awaiting the external send. The short lease keeps
|
|
114
|
+
// overlapping sweeps from both sending; failed sends release it below,
|
|
115
|
+
// while a crashed send becomes retryable after the lease.
|
|
116
|
+
const claimId = randomUUID();
|
|
117
|
+
const claimExpiresAt = now + CLAIM_LEASE_MS;
|
|
118
|
+
let claim;
|
|
91
119
|
try {
|
|
92
|
-
|
|
120
|
+
claim = await mutateSetting(LAST_ALERT_SETTING_KEY, (current) => {
|
|
121
|
+
const lastPagedAt = Number(current?.at ?? 0);
|
|
122
|
+
const existingClaimExpiresAt = Number(current?.claimExpiresAt ?? 0);
|
|
123
|
+
if ((Number.isFinite(lastPagedAt) && now - lastPagedAt < COOLDOWN_MS) ||
|
|
124
|
+
(Number.isFinite(existingClaimExpiresAt) &&
|
|
125
|
+
existingClaimExpiresAt > now)) {
|
|
126
|
+
return current ?? {};
|
|
127
|
+
}
|
|
128
|
+
return { claimId, claimExpiresAt };
|
|
129
|
+
});
|
|
93
130
|
}
|
|
94
131
|
catch (error) {
|
|
95
132
|
return { status: "check-failed", reason: String(error) };
|
|
96
133
|
}
|
|
97
|
-
|
|
98
|
-
|
|
134
|
+
if (String(claim.claimId ?? "") !== claimId) {
|
|
135
|
+
const lastPagedAt = Number(claim.at ?? 0);
|
|
136
|
+
const retryAfterMs = Number.isFinite(lastPagedAt) && now - lastPagedAt < COOLDOWN_MS
|
|
137
|
+
? COOLDOWN_MS - (now - lastPagedAt)
|
|
138
|
+
: Math.max(0, Number(claim.claimExpiresAt ?? 0) - now);
|
|
99
139
|
return {
|
|
100
140
|
status: "cooldown",
|
|
101
|
-
retryAfterMs
|
|
141
|
+
retryAfterMs,
|
|
102
142
|
};
|
|
103
143
|
}
|
|
104
|
-
let
|
|
144
|
+
let recipient;
|
|
105
145
|
try {
|
|
106
|
-
|
|
146
|
+
recipient = await alertOwner();
|
|
107
147
|
}
|
|
108
148
|
catch (error) {
|
|
109
|
-
|
|
149
|
+
const releaseReason = await releaseAlertClaim(claimId, claimExpiresAt);
|
|
150
|
+
return {
|
|
151
|
+
status: "check-failed",
|
|
152
|
+
reason: releaseReason
|
|
153
|
+
? `${String(error)} ${releaseReason}`
|
|
154
|
+
: String(error),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (!recipient) {
|
|
158
|
+
const reason = "No single owner/admin organization scope is available for Slack health alerts.";
|
|
159
|
+
const releaseReason = await releaseAlertClaim(claimId, claimExpiresAt);
|
|
160
|
+
return {
|
|
161
|
+
status: "delivery-failed",
|
|
162
|
+
reason: releaseReason ? `${reason} ${releaseReason}` : reason,
|
|
163
|
+
};
|
|
110
164
|
}
|
|
111
165
|
const pct = Math.round(badRate * 100);
|
|
112
|
-
|
|
113
|
-
|
|
166
|
+
let delivery;
|
|
167
|
+
try {
|
|
168
|
+
delivery = await runWithRequestContext({ userEmail: recipient.owner, orgId: recipient.orgId }, () => notifyWithDelivery({
|
|
114
169
|
severity: "critical",
|
|
115
170
|
title: `Chat is failing: ${pct}% of turns ended without an answer`,
|
|
116
171
|
body: `${counts.bad} of ${counts.turns} turns in the last hour ended without ` +
|
|
117
172
|
`an answer. Run \`node scripts/chat-health.mjs --hours 1\` for the ` +
|
|
118
173
|
`per-reason breakdown.`,
|
|
174
|
+
channels: ["slack"],
|
|
119
175
|
metadata: {
|
|
120
176
|
turns: counts.turns,
|
|
121
177
|
bad: counts.bad,
|
|
122
178
|
badRate,
|
|
123
179
|
windowMs: WINDOW_MS,
|
|
124
180
|
},
|
|
125
|
-
}, { owner })
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
181
|
+
}, { owner: recipient.owner }));
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
console.error("[chat-health-alert] Slack delivery failed:", error);
|
|
185
|
+
const releaseReason = await releaseAlertClaim(claimId, claimExpiresAt);
|
|
186
|
+
return {
|
|
187
|
+
status: "delivery-failed",
|
|
188
|
+
reason: releaseReason
|
|
189
|
+
? `${String(error)} ${releaseReason}`
|
|
190
|
+
: String(error),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (!delivery.deliveredChannels.includes("slack")) {
|
|
194
|
+
const reason = "Slack health alert was not delivered.";
|
|
195
|
+
console.error(`[chat-health-alert] ${reason}`);
|
|
196
|
+
const releaseReason = await releaseAlertClaim(claimId, claimExpiresAt);
|
|
197
|
+
return {
|
|
198
|
+
status: "delivery-failed",
|
|
199
|
+
reason: releaseReason ? `${reason} ${releaseReason}` : reason,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// Finalize only after Slack confirms delivery. The claim id keeps a slow or
|
|
203
|
+
// expired sender from overwriting a newer claim's cooldown.
|
|
204
|
+
try {
|
|
205
|
+
const finalized = await mutateSetting(LAST_ALERT_SETTING_KEY, (current) => String(current?.claimId ?? "") === claimId
|
|
206
|
+
? { at: now, claimId }
|
|
207
|
+
: (current ?? {}));
|
|
208
|
+
if (String(finalized.claimId ?? "") !== claimId) {
|
|
209
|
+
const reason = "Slack delivered, but its alert cooldown claim was lost before persistence.";
|
|
210
|
+
console.error(`[chat-health-alert] ${reason}`);
|
|
211
|
+
return { status: "persistence-failed", reason };
|
|
212
|
+
}
|
|
129
213
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
await putSetting(LAST_ALERT_SETTING_KEY, { at: now }).catch((error) => {
|
|
214
|
+
catch (error) {
|
|
215
|
+
const reason = "Slack delivered, but the alert cooldown could not be persisted.";
|
|
133
216
|
console.error("[chat-health-alert] could not stamp cooldown:", error);
|
|
134
|
-
|
|
217
|
+
return { status: "persistence-failed", reason };
|
|
218
|
+
}
|
|
135
219
|
return {
|
|
136
220
|
status: "alerted",
|
|
137
221
|
turns: counts.turns,
|
|
138
222
|
badRate,
|
|
139
|
-
recipients:
|
|
223
|
+
recipients: 1,
|
|
140
224
|
};
|
|
141
225
|
}
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
|
|
|
26
26
|
* Body: { update: string (base64), requestSource?: string }
|
|
27
27
|
*/
|
|
28
28
|
export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
29
|
-
error: string;
|
|
30
29
|
ok?: undefined;
|
|
30
|
+
error: string;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
33
33
|
ok: boolean;
|
|
@@ -3,18 +3,18 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
3
3
|
url?: string;
|
|
4
4
|
filename?: string;
|
|
5
5
|
}, {
|
|
6
|
-
id?: undefined;
|
|
7
6
|
error: string;
|
|
8
7
|
configured?: undefined;
|
|
9
8
|
connectPath?: undefined;
|
|
10
9
|
url?: undefined;
|
|
10
|
+
id?: undefined;
|
|
11
11
|
provider?: undefined;
|
|
12
12
|
} | {
|
|
13
|
-
id?: undefined;
|
|
14
13
|
error: string;
|
|
15
14
|
configured: boolean;
|
|
16
15
|
connectPath: string;
|
|
17
16
|
url?: undefined;
|
|
17
|
+
id?: undefined;
|
|
18
18
|
provider?: undefined;
|
|
19
19
|
} | {
|
|
20
20
|
error?: undefined;
|
|
@@ -32,6 +32,7 @@ import { getKeyAllowlist, getResolvedKeyAllowlist, resolveKeyReferencesWithReque
|
|
|
32
32
|
import { sendEmail } from "../server/email.js";
|
|
33
33
|
import { registerNotificationChannel } from "./registry.js";
|
|
34
34
|
let _registered = false;
|
|
35
|
+
const SLACK_DELIVERY_TIMEOUT_MS = 10_000;
|
|
35
36
|
export function registerBuiltinNotificationChannels() {
|
|
36
37
|
if (_registered)
|
|
37
38
|
return;
|
|
@@ -89,6 +90,7 @@ function createSlackWebhookChannel(envUrlTemplate) {
|
|
|
89
90
|
const res = await ssrfSafeFetch(url, {
|
|
90
91
|
method: "POST",
|
|
91
92
|
headers,
|
|
93
|
+
signal: AbortSignal.timeout(SLACK_DELIVERY_TIMEOUT_MS),
|
|
92
94
|
body: JSON.stringify({
|
|
93
95
|
text: slackText(input.severity, input.title, input.body),
|
|
94
96
|
blocks: [
|
|
@@ -43,25 +43,25 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
43
43
|
} | {
|
|
44
44
|
summary: import("./types.js").TraceSummary;
|
|
45
45
|
spans: import("./types.js").TraceSpan[];
|
|
46
|
-
id?: undefined;
|
|
47
|
-
error?: undefined;
|
|
48
46
|
ok?: undefined;
|
|
47
|
+
error?: undefined;
|
|
48
|
+
id?: undefined;
|
|
49
49
|
} | {
|
|
50
50
|
summary?: undefined;
|
|
51
51
|
spans?: undefined;
|
|
52
52
|
id: string;
|
|
53
|
-
error?: undefined;
|
|
54
53
|
ok?: undefined;
|
|
54
|
+
error?: undefined;
|
|
55
55
|
} | {
|
|
56
56
|
summary?: undefined;
|
|
57
57
|
spans?: undefined;
|
|
58
|
-
id?: undefined;
|
|
59
58
|
error: any;
|
|
60
59
|
ok?: undefined;
|
|
60
|
+
id?: undefined;
|
|
61
61
|
} | {
|
|
62
62
|
summary?: undefined;
|
|
63
63
|
spans?: undefined;
|
|
64
|
-
id?: undefined;
|
|
65
|
-
error?: undefined;
|
|
66
64
|
ok: boolean;
|
|
65
|
+
error?: undefined;
|
|
66
|
+
id?: undefined;
|
|
67
67
|
}>>;
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -37,17 +37,17 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
|
|
|
37
37
|
ok?: undefined;
|
|
38
38
|
status?: undefined;
|
|
39
39
|
} | {
|
|
40
|
-
error?: undefined;
|
|
41
40
|
ok: boolean;
|
|
42
41
|
status: string;
|
|
42
|
+
error?: undefined;
|
|
43
43
|
} | {
|
|
44
44
|
ok?: undefined;
|
|
45
45
|
error: string;
|
|
46
46
|
removed?: undefined;
|
|
47
47
|
} | {
|
|
48
|
-
error?: undefined;
|
|
49
48
|
ok: boolean;
|
|
50
49
|
removed: boolean;
|
|
50
|
+
error?: undefined;
|
|
51
51
|
}>>;
|
|
52
52
|
/**
|
|
53
53
|
* POST /_agent-native/secrets/:key/test — validate an optional candidate value
|
|
@@ -58,13 +58,13 @@ export declare function createTestSecretHandler(): import("h3").EventHandlerWith
|
|
|
58
58
|
error: string;
|
|
59
59
|
note?: undefined;
|
|
60
60
|
} | {
|
|
61
|
-
error?: undefined;
|
|
62
61
|
ok: boolean;
|
|
63
62
|
note?: undefined;
|
|
64
|
-
} | {
|
|
65
63
|
error?: undefined;
|
|
64
|
+
} | {
|
|
66
65
|
ok: boolean;
|
|
67
66
|
note: string;
|
|
67
|
+
error?: undefined;
|
|
68
68
|
} | {
|
|
69
69
|
note?: undefined;
|
|
70
70
|
ok: boolean;
|
|
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
|
|
|
95
95
|
export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
|
|
96
96
|
error: string;
|
|
97
97
|
} | {
|
|
98
|
-
error?: undefined;
|
|
99
98
|
ok: boolean;
|
|
100
99
|
key: string;
|
|
101
|
-
} | {
|
|
102
100
|
error?: undefined;
|
|
101
|
+
} | {
|
|
103
102
|
ok: boolean;
|
|
104
103
|
removed: boolean;
|
|
104
|
+
error?: undefined;
|
|
105
105
|
}>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.176.4-nightly-
|
|
3
|
+
"version": "0.176.4-nightly-20260902103608",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -437,8 +437,8 @@
|
|
|
437
437
|
"y-protocols": "^1.0.7",
|
|
438
438
|
"yjs": "^13.6.32",
|
|
439
439
|
"zod": "^4.3.6",
|
|
440
|
-
"@agent-native/
|
|
441
|
-
"@agent-native/
|
|
440
|
+
"@agent-native/toolkit": "^0.19.1",
|
|
441
|
+
"@agent-native/recap-cli": "0.5.23"
|
|
442
442
|
},
|
|
443
443
|
"devDependencies": {
|
|
444
444
|
"@ai-sdk/anthropic": "^3.0.71",
|