@agent-native/core 0.176.4-nightly-20260902024948 → 0.176.4-nightly-20260902074703
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/dist/agent/chat-health-alert.d.ts +6 -0
- package/dist/agent/chat-health-alert.js +117 -33
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-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/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +4 -4
- package/dist/resources/handlers.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
62
62
|
error: string;
|
|
63
63
|
states?: undefined;
|
|
64
64
|
} | {
|
|
65
|
-
error?: undefined;
|
|
66
65
|
states: {
|
|
67
66
|
clientId: number;
|
|
68
67
|
state: string;
|
|
69
68
|
}[];
|
|
69
|
+
error?: undefined;
|
|
70
70
|
}>>;
|
|
71
71
|
/**
|
|
72
72
|
* GET /_agent-native/collab/:docId/users
|
|
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
|
|
|
77
77
|
error: string;
|
|
78
78
|
users?: undefined;
|
|
79
79
|
} | {
|
|
80
|
-
error?: undefined;
|
|
81
80
|
users: {
|
|
82
81
|
clientId: number;
|
|
83
82
|
lastSeen: number;
|
|
84
83
|
}[];
|
|
84
|
+
error?: undefined;
|
|
85
85
|
}>>;
|
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;
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
|
|
14
14
|
*/
|
|
15
15
|
export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
16
|
-
ok?: undefined;
|
|
17
16
|
error: string;
|
|
17
|
+
ok?: undefined;
|
|
18
18
|
} | {
|
|
19
19
|
error?: undefined;
|
|
20
20
|
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: [
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
* DELETE /_agent-native/notifications/:id — delete
|
|
12
12
|
*/
|
|
13
13
|
export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
|
|
14
|
-
error?: undefined;
|
|
15
14
|
count: number;
|
|
16
15
|
updated?: undefined;
|
|
16
|
+
error?: undefined;
|
|
17
17
|
ok?: undefined;
|
|
18
18
|
} | {
|
|
19
|
-
error?: undefined;
|
|
20
19
|
count?: undefined;
|
|
21
20
|
updated: number;
|
|
21
|
+
error?: undefined;
|
|
22
22
|
ok?: undefined;
|
|
23
23
|
} | {
|
|
24
24
|
count?: undefined;
|
|
@@ -26,8 +26,8 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
|
|
|
26
26
|
error: string;
|
|
27
27
|
ok?: undefined;
|
|
28
28
|
} | {
|
|
29
|
-
error?: undefined;
|
|
30
29
|
count?: undefined;
|
|
31
30
|
updated?: undefined;
|
|
31
|
+
error?: undefined;
|
|
32
32
|
ok: boolean;
|
|
33
33
|
}>>;
|
|
@@ -43,8 +43,8 @@ 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
46
|
error?: undefined;
|
|
47
|
+
id?: undefined;
|
|
48
48
|
ok?: undefined;
|
|
49
49
|
} | {
|
|
50
50
|
summary?: undefined;
|
|
@@ -55,13 +55,13 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
55
55
|
} | {
|
|
56
56
|
summary?: undefined;
|
|
57
57
|
spans?: undefined;
|
|
58
|
-
id?: undefined;
|
|
59
58
|
error: any;
|
|
59
|
+
id?: undefined;
|
|
60
60
|
ok?: 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
|
}>>;
|
|
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
48
48
|
}>;
|
|
49
49
|
/** DELETE /_agent-native/resources/:id — delete a resource */
|
|
50
50
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
51
|
-
error: string;
|
|
52
51
|
ok?: undefined;
|
|
52
|
+
error: string;
|
|
53
53
|
} | {
|
|
54
54
|
error?: undefined;
|
|
55
55
|
ok: boolean;
|
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-20260902074703",
|
|
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": {
|