@agent-native/core 0.141.2 → 0.141.4
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/analytics/.agents/skills/analysis-workspace/SKILL.md +1 -1
- package/corpus/templates/analytics/actions/gong-calls.ts +56 -18
- package/corpus/templates/analytics/changelog/2026-08-05-analytics-queries-keep-every-table-tenant-scoped.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-05-durable-a2a-workers-avoid-startup-migration-contention.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-05-fixed-sign-in-and-dashboard-requests-failing-when-background.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +1 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
- package/corpus/templates/analytics/server/lib/emails.ts +51 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +8 -12
- package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +262 -13
- package/corpus/templates/analytics/server/lib/gong.ts +132 -33
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/analytics/server/plugins/auth.ts +21 -2
- package/corpus/templates/analytics/server/plugins/db.ts +35 -10
- package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +17 -7
- package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
- package/corpus/templates/calendar/server/lib/emails.ts +167 -0
- package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
- package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/clips/actions/invite-member.ts +39 -16
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +73 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -25
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
- package/corpus/templates/clips/changelog/2026-08-05-activity-is-now-the-default-clips-recording-tab-and-ai-setup.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-05-clips-can-copy-a-linked-email-preview-for-public-recordings.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-05-preparing-recording-now-uses-a-clean-shadow-free-status-card.md +6 -0
- package/corpus/templates/clips/desktop/src/styles.css +0 -3
- package/corpus/templates/clips/server/lib/emails.ts +256 -0
- package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
- package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
- package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +4 -1
- package/corpus/templates/clips/shared/email-preview.ts +61 -0
- package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
- package/corpus/templates/content/server/lib/emails.ts +70 -0
- package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/design/actions/import-design-tokens.ts +11 -8
- package/corpus/templates/design/actions/index-design-tokens.ts +13 -6
- package/corpus/templates/design/changelog/2026-08-05-design-token-imports-classify-typography-dimensions-correctly.md +6 -0
- package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
- package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
- package/corpus/templates/dispatch/changelog/2026-08-05-transactional-email-catalogs-and-delivery-activity-are-available-again.md +6 -0
- package/corpus/templates/factory/AGENTS.md +18 -17
- package/corpus/templates/factory/actions/get-factory-automation-health.ts +50 -0
- package/corpus/templates/factory/actions/get-triage-config.ts +8 -0
- package/corpus/templates/factory/actions/save-triage-config.ts +26 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +27 -0
- package/corpus/templates/factory/app/routes/factory.tsx +376 -206
- package/corpus/templates/factory/changelog/2026-08-05-factory-now-keeps-production-automations-running-shows-sched.md +6 -0
- package/corpus/templates/factory/server/db/schema.ts +6 -0
- package/corpus/templates/factory/server/plugins/factory-migrations.ts +10 -0
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +141 -2
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +41 -21
- package/corpus/templates/forms/changelog/2026-08-05-form-links-can-now-be-cmd-clicked-to-open-in-a-new-tab.md +6 -0
- package/corpus/templates/forms/server/lib/emails.ts +52 -0
- package/corpus/templates/forms/server/lib/response-email.ts +2 -0
- package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
- package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
- package/corpus/templates/plan/server/lib/emails.ts +67 -0
- package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/slides/actions/import-from-url.ts +1 -0
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +492 -381
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +8 -7
- package/corpus/templates/slides/app/components/layout/Layout.tsx +27 -23
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +313 -0
- package/corpus/templates/slides/app/components/ui/command.tsx +1 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +8 -2
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +305 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -12
- package/corpus/templates/slides/app/pages/Index.tsx +127 -137
- package/corpus/templates/slides/app/root.tsx +8 -0
- package/corpus/templates/slides/changelog/2026-08-05-deck-browsing-keeps-all-and-mine-in-the-header-with-a-tighte.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-05-new-deck-creation-now-flows-through-a-clean-full-screen-refe.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-05-website-imports-now-include-linked-stylesheets-for-more-accu.md +6 -0
- package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
- package/corpus/templates/slides/server/lib/emails.ts +40 -0
- package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
- package/dist/brand-kit/index.d.ts +1 -0
- package/dist/brand-kit/index.js +1 -0
- package/dist/brand-kit/tokens.d.ts +91 -0
- package/dist/brand-kit/tokens.js +279 -0
- package/dist/brand-kit/types.d.ts +25 -0
- package/dist/client/AssistantChat.js +7 -7
- package/dist/client/onboarding/FirstRunOnboarding.js +26 -2
- package/dist/client/onboarding/first-run-registry.d.ts +25 -0
- package/dist/client/onboarding/first-run-registry.js +18 -0
- package/dist/client/onboarding/index.d.ts +2 -0
- package/dist/client/onboarding/index.js +1 -0
- 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/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/migrations.d.ts +13 -3
- package/dist/db/migrations.js +19 -4
- package/dist/deploy/build.d.ts +8 -0
- package/dist/deploy/build.js +92 -3
- package/dist/deploy/workspace-deploy.js +86 -3
- package/dist/email-catalog/actions/list-email-activity.d.ts +5 -0
- package/dist/email-catalog/actions/list-email-activity.js +19 -0
- package/dist/email-catalog/actions/list-email-engagement.d.ts +5 -0
- package/dist/email-catalog/actions/list-email-engagement.js +15 -0
- package/dist/email-catalog/actions/list-email-log.d.ts +7 -0
- package/dist/email-catalog/actions/list-email-log.js +23 -0
- package/dist/email-catalog/actions/list-transactional-emails.d.ts +22 -0
- package/dist/email-catalog/actions/list-transactional-emails.js +67 -0
- package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +9 -0
- package/dist/email-catalog/actions/render-transactional-email-preview.js +25 -0
- package/dist/email-catalog/authorize.d.ts +1 -0
- package/dist/email-catalog/authorize.js +9 -0
- package/dist/email-catalog/log.d.ts +60 -0
- package/dist/email-catalog/log.js +142 -0
- package/dist/email-catalog/provider-metrics.d.ts +34 -0
- package/dist/email-catalog/provider-metrics.js +182 -0
- package/dist/email-catalog/registry.d.ts +78 -0
- package/dist/email-catalog/registry.js +70 -0
- package/dist/email-catalog/schema.d.ts +231 -0
- package/dist/email-catalog/schema.js +53 -0
- package/dist/email-catalog/system-emails.d.ts +13 -0
- package/dist/email-catalog/system-emails.js +67 -0
- package/dist/jobs/index.d.ts +2 -0
- package/dist/jobs/index.js +2 -0
- package/dist/jobs/run-history.js +48 -1
- package/dist/jobs/scheduler-dispatch.d.ts +7 -0
- package/dist/jobs/scheduler-dispatch.js +7 -0
- package/dist/jobs/scheduler-health.d.ts +22 -0
- package/dist/jobs/scheduler-health.js +172 -0
- package/dist/jobs/scheduler.js +67 -2
- package/dist/localization/default-messages.d.ts +45 -0
- package/dist/localization/default-messages.js +45 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/handlers.js +9 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/server/action-discovery.js +22 -0
- package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +7 -1
- package/dist/server/agent-chat/recurring-jobs-runtime.js +12 -0
- package/dist/server/agent-chat-plugin.d.ts +2 -1
- package/dist/server/agent-chat-plugin.js +69 -22
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/better-auth-instance.js +17 -2
- package/dist/server/design-token-utils.d.ts +6 -1
- package/dist/server/design-token-utils.js +252 -59
- package/dist/server/email.d.ts +11 -0
- package/dist/server/email.js +54 -3
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.js +3 -2
- package/dist/vite/client.js +20 -14
- package/package.json +2 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle schema for the transactional email send log.
|
|
3
|
+
*
|
|
4
|
+
* One row per `sendEmail` attempt, written by the transport itself so every
|
|
5
|
+
* send is recorded regardless of which app or code path triggered it. This is
|
|
6
|
+
* the durable record of "did we send it": the provider's own activity feed ages
|
|
7
|
+
* out (SendGrid keeps 3 days without the extended-retention add-on), so send
|
|
8
|
+
* counts and last-sent must not depend on it.
|
|
9
|
+
*
|
|
10
|
+
* Engagement (opens, clicks, bounces) is deliberately NOT stored here. Only the
|
|
11
|
+
* provider knows it, and mirroring it would go stale the moment a recipient
|
|
12
|
+
* opens an old message. Dispatch reads engagement live from the provider and
|
|
13
|
+
* joins on `template_id`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const emailLog: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
16
|
+
name: "email_log";
|
|
17
|
+
schema: undefined;
|
|
18
|
+
columns: {
|
|
19
|
+
id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
20
|
+
name: "id";
|
|
21
|
+
tableName: "email_log";
|
|
22
|
+
dataType: "string";
|
|
23
|
+
columnType: "SQLiteText";
|
|
24
|
+
data: string;
|
|
25
|
+
driverParam: string;
|
|
26
|
+
notNull: true;
|
|
27
|
+
hasDefault: false;
|
|
28
|
+
isPrimaryKey: true;
|
|
29
|
+
isAutoincrement: false;
|
|
30
|
+
hasRuntimeDefault: false;
|
|
31
|
+
enumValues: [string, ...string[]];
|
|
32
|
+
baseColumn: never;
|
|
33
|
+
identity: undefined;
|
|
34
|
+
generated: undefined;
|
|
35
|
+
}, {}, {
|
|
36
|
+
length: number;
|
|
37
|
+
}>;
|
|
38
|
+
orgId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
39
|
+
name: "org_id";
|
|
40
|
+
tableName: "email_log";
|
|
41
|
+
dataType: "string";
|
|
42
|
+
columnType: "SQLiteText";
|
|
43
|
+
data: string;
|
|
44
|
+
driverParam: string;
|
|
45
|
+
notNull: false;
|
|
46
|
+
hasDefault: false;
|
|
47
|
+
isPrimaryKey: false;
|
|
48
|
+
isAutoincrement: false;
|
|
49
|
+
hasRuntimeDefault: false;
|
|
50
|
+
enumValues: [string, ...string[]];
|
|
51
|
+
baseColumn: never;
|
|
52
|
+
identity: undefined;
|
|
53
|
+
generated: undefined;
|
|
54
|
+
}, {}, {
|
|
55
|
+
length: number;
|
|
56
|
+
}>;
|
|
57
|
+
templateId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
58
|
+
name: "template_id";
|
|
59
|
+
tableName: "email_log";
|
|
60
|
+
dataType: "string";
|
|
61
|
+
columnType: "SQLiteText";
|
|
62
|
+
data: string;
|
|
63
|
+
driverParam: string;
|
|
64
|
+
notNull: false;
|
|
65
|
+
hasDefault: false;
|
|
66
|
+
isPrimaryKey: false;
|
|
67
|
+
isAutoincrement: false;
|
|
68
|
+
hasRuntimeDefault: false;
|
|
69
|
+
enumValues: [string, ...string[]];
|
|
70
|
+
baseColumn: never;
|
|
71
|
+
identity: undefined;
|
|
72
|
+
generated: undefined;
|
|
73
|
+
}, {}, {
|
|
74
|
+
length: number;
|
|
75
|
+
}>;
|
|
76
|
+
app: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
77
|
+
name: "app";
|
|
78
|
+
tableName: "email_log";
|
|
79
|
+
dataType: "string";
|
|
80
|
+
columnType: "SQLiteText";
|
|
81
|
+
data: string;
|
|
82
|
+
driverParam: string;
|
|
83
|
+
notNull: false;
|
|
84
|
+
hasDefault: false;
|
|
85
|
+
isPrimaryKey: false;
|
|
86
|
+
isAutoincrement: false;
|
|
87
|
+
hasRuntimeDefault: false;
|
|
88
|
+
enumValues: [string, ...string[]];
|
|
89
|
+
baseColumn: never;
|
|
90
|
+
identity: undefined;
|
|
91
|
+
generated: undefined;
|
|
92
|
+
}, {}, {
|
|
93
|
+
length: number;
|
|
94
|
+
}>;
|
|
95
|
+
recipient: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
96
|
+
name: "recipient";
|
|
97
|
+
tableName: "email_log";
|
|
98
|
+
dataType: "string";
|
|
99
|
+
columnType: "SQLiteText";
|
|
100
|
+
data: string;
|
|
101
|
+
driverParam: string;
|
|
102
|
+
notNull: true;
|
|
103
|
+
hasDefault: false;
|
|
104
|
+
isPrimaryKey: false;
|
|
105
|
+
isAutoincrement: false;
|
|
106
|
+
hasRuntimeDefault: false;
|
|
107
|
+
enumValues: [string, ...string[]];
|
|
108
|
+
baseColumn: never;
|
|
109
|
+
identity: undefined;
|
|
110
|
+
generated: undefined;
|
|
111
|
+
}, {}, {
|
|
112
|
+
length: number;
|
|
113
|
+
}>;
|
|
114
|
+
sender: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
115
|
+
name: "sender";
|
|
116
|
+
tableName: "email_log";
|
|
117
|
+
dataType: "string";
|
|
118
|
+
columnType: "SQLiteText";
|
|
119
|
+
data: string;
|
|
120
|
+
driverParam: string;
|
|
121
|
+
notNull: true;
|
|
122
|
+
hasDefault: false;
|
|
123
|
+
isPrimaryKey: false;
|
|
124
|
+
isAutoincrement: false;
|
|
125
|
+
hasRuntimeDefault: false;
|
|
126
|
+
enumValues: [string, ...string[]];
|
|
127
|
+
baseColumn: never;
|
|
128
|
+
identity: undefined;
|
|
129
|
+
generated: undefined;
|
|
130
|
+
}, {}, {
|
|
131
|
+
length: number;
|
|
132
|
+
}>;
|
|
133
|
+
subject: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
134
|
+
name: "subject";
|
|
135
|
+
tableName: "email_log";
|
|
136
|
+
dataType: "string";
|
|
137
|
+
columnType: "SQLiteText";
|
|
138
|
+
data: string;
|
|
139
|
+
driverParam: string;
|
|
140
|
+
notNull: true;
|
|
141
|
+
hasDefault: false;
|
|
142
|
+
isPrimaryKey: false;
|
|
143
|
+
isAutoincrement: false;
|
|
144
|
+
hasRuntimeDefault: false;
|
|
145
|
+
enumValues: [string, ...string[]];
|
|
146
|
+
baseColumn: never;
|
|
147
|
+
identity: undefined;
|
|
148
|
+
generated: undefined;
|
|
149
|
+
}, {}, {
|
|
150
|
+
length: number;
|
|
151
|
+
}>;
|
|
152
|
+
status: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
153
|
+
name: "status";
|
|
154
|
+
tableName: "email_log";
|
|
155
|
+
dataType: "string";
|
|
156
|
+
columnType: "SQLiteText";
|
|
157
|
+
data: "failed" | "sent";
|
|
158
|
+
driverParam: string;
|
|
159
|
+
notNull: true;
|
|
160
|
+
hasDefault: false;
|
|
161
|
+
isPrimaryKey: false;
|
|
162
|
+
isAutoincrement: false;
|
|
163
|
+
hasRuntimeDefault: false;
|
|
164
|
+
enumValues: ["sent", "failed"];
|
|
165
|
+
baseColumn: never;
|
|
166
|
+
identity: undefined;
|
|
167
|
+
generated: undefined;
|
|
168
|
+
}, {}, {
|
|
169
|
+
length: number;
|
|
170
|
+
}>;
|
|
171
|
+
error: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
172
|
+
name: "error";
|
|
173
|
+
tableName: "email_log";
|
|
174
|
+
dataType: "string";
|
|
175
|
+
columnType: "SQLiteText";
|
|
176
|
+
data: string;
|
|
177
|
+
driverParam: string;
|
|
178
|
+
notNull: false;
|
|
179
|
+
hasDefault: false;
|
|
180
|
+
isPrimaryKey: false;
|
|
181
|
+
isAutoincrement: false;
|
|
182
|
+
hasRuntimeDefault: false;
|
|
183
|
+
enumValues: [string, ...string[]];
|
|
184
|
+
baseColumn: never;
|
|
185
|
+
identity: undefined;
|
|
186
|
+
generated: undefined;
|
|
187
|
+
}, {}, {
|
|
188
|
+
length: number;
|
|
189
|
+
}>;
|
|
190
|
+
provider: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
191
|
+
name: "provider";
|
|
192
|
+
tableName: "email_log";
|
|
193
|
+
dataType: "string";
|
|
194
|
+
columnType: "SQLiteText";
|
|
195
|
+
data: string;
|
|
196
|
+
driverParam: string;
|
|
197
|
+
notNull: true;
|
|
198
|
+
hasDefault: false;
|
|
199
|
+
isPrimaryKey: false;
|
|
200
|
+
isAutoincrement: false;
|
|
201
|
+
hasRuntimeDefault: false;
|
|
202
|
+
enumValues: [string, ...string[]];
|
|
203
|
+
baseColumn: never;
|
|
204
|
+
identity: undefined;
|
|
205
|
+
generated: undefined;
|
|
206
|
+
}, {}, {
|
|
207
|
+
length: number;
|
|
208
|
+
}>;
|
|
209
|
+
createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
210
|
+
name: "created_at";
|
|
211
|
+
tableName: "email_log";
|
|
212
|
+
dataType: "number";
|
|
213
|
+
columnType: "SQLiteInteger";
|
|
214
|
+
data: number;
|
|
215
|
+
driverParam: number;
|
|
216
|
+
notNull: true;
|
|
217
|
+
hasDefault: false;
|
|
218
|
+
isPrimaryKey: false;
|
|
219
|
+
isAutoincrement: false;
|
|
220
|
+
hasRuntimeDefault: false;
|
|
221
|
+
enumValues: undefined;
|
|
222
|
+
baseColumn: never;
|
|
223
|
+
identity: undefined;
|
|
224
|
+
generated: undefined;
|
|
225
|
+
}, {}, {}>;
|
|
226
|
+
};
|
|
227
|
+
dialect: 'sqlite';
|
|
228
|
+
}>;
|
|
229
|
+
export declare const EMAIL_LOG_CREATE_SQL = "CREATE TABLE IF NOT EXISTS email_log (\n id TEXT PRIMARY KEY,\n org_id TEXT,\n template_id TEXT,\n app TEXT,\n recipient TEXT NOT NULL,\n sender TEXT NOT NULL,\n subject TEXT NOT NULL,\n status TEXT NOT NULL,\n error TEXT,\n provider TEXT NOT NULL,\n created_at INTEGER NOT NULL\n)";
|
|
230
|
+
export declare const EMAIL_LOG_TEMPLATE_INDEX_SQL = "CREATE INDEX IF NOT EXISTS email_log_template_created_idx\n ON email_log (template_id, created_at)";
|
|
231
|
+
export declare const EMAIL_LOG_ORG_APP_INDEX_SQL = "CREATE INDEX IF NOT EXISTS email_log_org_app_created_idx\n ON email_log (org_id, app, created_at)";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle schema for the transactional email send log.
|
|
3
|
+
*
|
|
4
|
+
* One row per `sendEmail` attempt, written by the transport itself so every
|
|
5
|
+
* send is recorded regardless of which app or code path triggered it. This is
|
|
6
|
+
* the durable record of "did we send it": the provider's own activity feed ages
|
|
7
|
+
* out (SendGrid keeps 3 days without the extended-retention add-on), so send
|
|
8
|
+
* counts and last-sent must not depend on it.
|
|
9
|
+
*
|
|
10
|
+
* Engagement (opens, clicks, bounces) is deliberately NOT stored here. Only the
|
|
11
|
+
* provider knows it, and mirroring it would go stale the moment a recipient
|
|
12
|
+
* opens an old message. Dispatch reads engagement live from the provider and
|
|
13
|
+
* joins on `template_id`.
|
|
14
|
+
*/
|
|
15
|
+
import { table, text, integer } from "../db/schema.js";
|
|
16
|
+
export const emailLog = table("email_log", {
|
|
17
|
+
id: text("id").primaryKey(),
|
|
18
|
+
/** Organization that owns the send. Legacy rows without this are unreadable. */
|
|
19
|
+
orgId: text("org_id"),
|
|
20
|
+
/** Registered transactional email id, e.g. "calendar.booking-confirmed". */
|
|
21
|
+
templateId: text("template_id"),
|
|
22
|
+
/** App slug that sent it. */
|
|
23
|
+
app: text("app"),
|
|
24
|
+
/** Recipient address. */
|
|
25
|
+
recipient: text("recipient").notNull(),
|
|
26
|
+
/** Resolved From address, after app-sender branding is applied. */
|
|
27
|
+
sender: text("sender").notNull(),
|
|
28
|
+
subject: text("subject").notNull(),
|
|
29
|
+
/** "sent" once the provider accepted it, or "failed". Never optimistic. */
|
|
30
|
+
status: text("status", { enum: ["sent", "failed"] }).notNull(),
|
|
31
|
+
/** Provider error text when status is "failed". */
|
|
32
|
+
error: text("error"),
|
|
33
|
+
/** "resend" | "sendgrid" | "dev". */
|
|
34
|
+
provider: text("provider").notNull(),
|
|
35
|
+
createdAt: integer("created_at").notNull(),
|
|
36
|
+
});
|
|
37
|
+
export const EMAIL_LOG_CREATE_SQL = `CREATE TABLE IF NOT EXISTS email_log (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
org_id TEXT,
|
|
40
|
+
template_id TEXT,
|
|
41
|
+
app TEXT,
|
|
42
|
+
recipient TEXT NOT NULL,
|
|
43
|
+
sender TEXT NOT NULL,
|
|
44
|
+
subject TEXT NOT NULL,
|
|
45
|
+
status TEXT NOT NULL,
|
|
46
|
+
error TEXT,
|
|
47
|
+
provider TEXT NOT NULL,
|
|
48
|
+
created_at INTEGER NOT NULL
|
|
49
|
+
)`;
|
|
50
|
+
export const EMAIL_LOG_TEMPLATE_INDEX_SQL = `CREATE INDEX IF NOT EXISTS email_log_template_created_idx
|
|
51
|
+
ON email_log (template_id, created_at)`;
|
|
52
|
+
export const EMAIL_LOG_ORG_APP_INDEX_SQL = `CREATE INDEX IF NOT EXISTS email_log_org_app_created_idx
|
|
53
|
+
ON email_log (org_id, app, created_at)`;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog entries for the framework's own system emails.
|
|
3
|
+
*
|
|
4
|
+
* These ship with every app rather than belonging to one, so they register
|
|
5
|
+
* under the `core` app instead of the running app's slug. Importing this module
|
|
6
|
+
* performs the registration; `register-system-emails.ts` is the single import
|
|
7
|
+
* site so the emails appear in every app's catalog without each template
|
|
8
|
+
* remembering to opt in.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CORE_INVITE_EMAIL_ID = "core.organization-invite";
|
|
11
|
+
export declare const CORE_VERIFY_SIGNUP_EMAIL_ID = "core.verify-signup";
|
|
12
|
+
export declare const CORE_RESET_PASSWORD_EMAIL_ID = "core.reset-password";
|
|
13
|
+
export declare function registerCoreSystemEmails(): void;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog entries for the framework's own system emails.
|
|
3
|
+
*
|
|
4
|
+
* These ship with every app rather than belonging to one, so they register
|
|
5
|
+
* under the `core` app instead of the running app's slug. Importing this module
|
|
6
|
+
* performs the registration; `register-system-emails.ts` is the single import
|
|
7
|
+
* site so the emails appear in every app's catalog without each template
|
|
8
|
+
* remembering to opt in.
|
|
9
|
+
*/
|
|
10
|
+
import { renderInviteEmail, renderResetPasswordEmail, renderVerifySignupEmail, } from "../server/email-templates.js";
|
|
11
|
+
import { defineTransactionalEmail } from "./registry.js";
|
|
12
|
+
/** Obviously-fake sample data — these render in a preview pane, never send. */
|
|
13
|
+
const SAMPLE_URL = "https://example.com/accept/sample-token";
|
|
14
|
+
const SAMPLE_EMAIL = "sam.rivera@example.com";
|
|
15
|
+
export const CORE_INVITE_EMAIL_ID = "core.organization-invite";
|
|
16
|
+
export const CORE_VERIFY_SIGNUP_EMAIL_ID = "core.verify-signup";
|
|
17
|
+
export const CORE_RESET_PASSWORD_EMAIL_ID = "core.reset-password";
|
|
18
|
+
let registered = false;
|
|
19
|
+
export function registerCoreSystemEmails() {
|
|
20
|
+
if (registered)
|
|
21
|
+
return;
|
|
22
|
+
registered = true;
|
|
23
|
+
defineTransactionalEmail({
|
|
24
|
+
id: CORE_INVITE_EMAIL_ID,
|
|
25
|
+
app: "core",
|
|
26
|
+
name: "Organization invitation",
|
|
27
|
+
trigger: "A member invites someone to their organization from the team settings page.",
|
|
28
|
+
recipientLabel: "Invited address",
|
|
29
|
+
recipient: "The address typed into the invite form. One email per invited address.",
|
|
30
|
+
senderLabel: "Default, app-branded",
|
|
31
|
+
sender: "The configured EMAIL_FROM. On first-party agent-native.com deployments the display name becomes the app's own, with reply-to agent-native@builder.io.",
|
|
32
|
+
preview: () => renderInviteEmail({
|
|
33
|
+
invitee: SAMPLE_EMAIL,
|
|
34
|
+
orgName: "Northwind Design",
|
|
35
|
+
acceptUrl: SAMPLE_URL,
|
|
36
|
+
inviter: "alex.chen@example.com",
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
defineTransactionalEmail({
|
|
40
|
+
id: CORE_VERIFY_SIGNUP_EMAIL_ID,
|
|
41
|
+
app: "core",
|
|
42
|
+
name: "Verify signup",
|
|
43
|
+
trigger: "A new account is created with email and password, before the account can be used.",
|
|
44
|
+
recipientLabel: "New account address",
|
|
45
|
+
recipient: "The address the account was registered with.",
|
|
46
|
+
senderLabel: "Default, app-branded",
|
|
47
|
+
sender: "The configured EMAIL_FROM, branded with the app name the signup happened in.",
|
|
48
|
+
preview: () => renderVerifySignupEmail({
|
|
49
|
+
email: SAMPLE_EMAIL,
|
|
50
|
+
verifyUrl: SAMPLE_URL,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
defineTransactionalEmail({
|
|
54
|
+
id: CORE_RESET_PASSWORD_EMAIL_ID,
|
|
55
|
+
app: "core",
|
|
56
|
+
name: "Reset password",
|
|
57
|
+
trigger: "A user requests a password reset from the sign-in screen. The link expires after one hour.",
|
|
58
|
+
recipientLabel: "Account address",
|
|
59
|
+
recipient: "The account address the reset was requested for, never an address supplied in the request body.",
|
|
60
|
+
senderLabel: "Default, app-branded",
|
|
61
|
+
sender: "The configured EMAIL_FROM, branded with the app name the reset was requested from.",
|
|
62
|
+
preview: () => renderResetPasswordEmail({
|
|
63
|
+
email: SAMPLE_EMAIL,
|
|
64
|
+
resetUrl: SAMPLE_URL,
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
}
|
package/dist/jobs/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { nextOccurrence, isValidCron, describeCron } from "./cron.js";
|
|
2
2
|
export { processRecurringJobs, parseJobFrontmatter, buildJobContent, type JobFrontmatter, type SchedulerDeps, } from "./scheduler.js";
|
|
3
|
+
export { getAutomationSchedulerHealth, recordAutomationSchedulerHealth, type AutomationSchedulerHealth, } from "./scheduler-health.js";
|
|
4
|
+
export { RECURRING_JOBS_SWEEP_PATH, RECURRING_JOBS_SWEEP_TOKEN_SUBJECT, } from "./scheduler-dispatch.js";
|
|
3
5
|
export { createJobTools } from "./tools.js";
|
package/dist/jobs/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { nextOccurrence, isValidCron, describeCron } from "./cron.js";
|
|
2
2
|
export { processRecurringJobs, parseJobFrontmatter, buildJobContent, } from "./scheduler.js";
|
|
3
|
+
export { getAutomationSchedulerHealth, recordAutomationSchedulerHealth, } from "./scheduler-health.js";
|
|
4
|
+
export { RECURRING_JOBS_SWEEP_PATH, RECURRING_JOBS_SWEEP_TOKEN_SUBJECT, } from "./scheduler-dispatch.js";
|
|
3
5
|
export { createJobTools } from "./tools.js";
|
package/dist/jobs/run-history.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import { getDbExec, intType, isPostgres } from "../db/client.js";
|
|
3
4
|
import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
|
|
5
|
+
import { emit as emitBusEvent, registerEvent } from "../event-bus/index.js";
|
|
6
|
+
registerEvent({
|
|
7
|
+
name: "automation.run.finished",
|
|
8
|
+
description: "Fires after a scheduled or manual automation run records a terminal status.",
|
|
9
|
+
payloadSchema: z.object({
|
|
10
|
+
automationRunId: z.string(),
|
|
11
|
+
owner: z.string(),
|
|
12
|
+
automation: z.string(),
|
|
13
|
+
path: z.string(),
|
|
14
|
+
orgId: z.string().nullable(),
|
|
15
|
+
runId: z.string().nullable(),
|
|
16
|
+
threadId: z.string().nullable(),
|
|
17
|
+
status: z.enum(["success", "error", "interrupted"]),
|
|
18
|
+
error: z.string().nullable(),
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
4
21
|
const TABLE = "automation_runs";
|
|
5
22
|
const MAX_ERROR_LENGTH = 500;
|
|
6
23
|
/**
|
|
@@ -8,6 +25,7 @@ const MAX_ERROR_LENGTH = 500;
|
|
|
8
25
|
* (BACKGROUND_RUN_HARD_TIMEOUT_MS). Past this, no run is still alive.
|
|
9
26
|
*/
|
|
10
27
|
const RUN_LIVENESS_CEILING_MS = 15 * 60_000;
|
|
28
|
+
const INTERRUPTED_RUN_MESSAGE = "Worker stopped before a terminal result was recorded. The serverless worker may have timed out or been recycled. No delivery was confirmed.";
|
|
11
29
|
// The background worker has a shorter hard timeout than this lease. A worker
|
|
12
30
|
// that dies after claiming can therefore be redelivered without overlapping a
|
|
13
31
|
// still-live execution under normal runtime limits.
|
|
@@ -91,7 +109,11 @@ function toRun(row, now) {
|
|
|
91
109
|
status,
|
|
92
110
|
startedAt,
|
|
93
111
|
finishedAt: row.finished_at == null ? null : Number(row.finished_at),
|
|
94
|
-
error: row.error == null
|
|
112
|
+
error: row.error == null && status === "interrupted"
|
|
113
|
+
? INTERRUPTED_RUN_MESSAGE
|
|
114
|
+
: row.error == null
|
|
115
|
+
? null
|
|
116
|
+
: String(row.error),
|
|
95
117
|
};
|
|
96
118
|
}
|
|
97
119
|
/**
|
|
@@ -182,10 +204,35 @@ async function pruneAutomationRuns(owner, automation) {
|
|
|
182
204
|
}
|
|
183
205
|
export async function finishAutomationRun(id, status, error) {
|
|
184
206
|
await ensureTable();
|
|
207
|
+
const existing = await getDbExec().execute({
|
|
208
|
+
sql: `SELECT owner, automation, path, org_id, run_id, thread_id FROM ${TABLE} WHERE id = ? LIMIT 1`,
|
|
209
|
+
args: [id],
|
|
210
|
+
});
|
|
211
|
+
const row = existing.rows?.[0];
|
|
185
212
|
await getDbExec().execute({
|
|
186
213
|
sql: `UPDATE ${TABLE} SET status = ?, finished_at = ?, error = ? WHERE id = ?`,
|
|
187
214
|
args: [status, Date.now(), error?.slice(0, MAX_ERROR_LENGTH) ?? null, id],
|
|
188
215
|
});
|
|
216
|
+
if (!row)
|
|
217
|
+
return;
|
|
218
|
+
try {
|
|
219
|
+
emitBusEvent("automation.run.finished", {
|
|
220
|
+
automationRunId: id,
|
|
221
|
+
owner: String(row.owner),
|
|
222
|
+
automation: String(row.automation),
|
|
223
|
+
path: String(row.path),
|
|
224
|
+
orgId: row.org_id == null ? null : String(row.org_id),
|
|
225
|
+
runId: row.run_id == null ? null : String(row.run_id),
|
|
226
|
+
threadId: row.thread_id == null ? null : String(row.thread_id),
|
|
227
|
+
status,
|
|
228
|
+
error: error?.slice(0, MAX_ERROR_LENGTH) ?? null,
|
|
229
|
+
}, { owner: String(row.owner) });
|
|
230
|
+
}
|
|
231
|
+
catch (eventError) {
|
|
232
|
+
// History is the source of truth. A subscriber must never turn a recorded
|
|
233
|
+
// terminal result back into a failed automation run.
|
|
234
|
+
console.warn("[automations] terminal-run event delivery failed:", eventError);
|
|
235
|
+
}
|
|
189
236
|
}
|
|
190
237
|
/**
|
|
191
238
|
* Attach the agent thread once it exists. The thread is created after the run
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal route used by platform schedulers to run one recurring-job sweep.
|
|
3
|
+
* The route is intentionally narrow: it accepts no job or owner input and
|
|
4
|
+
* authenticates the platform handoff with the deployment's internal token.
|
|
5
|
+
*/
|
|
6
|
+
export declare const RECURRING_JOBS_SWEEP_PATH = "/_agent-native/jobs/_process-sweep";
|
|
7
|
+
export declare const RECURRING_JOBS_SWEEP_TOKEN_SUBJECT = "agent-native-recurring-jobs-sweep";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal route used by platform schedulers to run one recurring-job sweep.
|
|
3
|
+
* The route is intentionally narrow: it accepts no job or owner input and
|
|
4
|
+
* authenticates the platform handoff with the deployment's internal token.
|
|
5
|
+
*/
|
|
6
|
+
export const RECURRING_JOBS_SWEEP_PATH = "/_agent-native/jobs/_process-sweep";
|
|
7
|
+
export const RECURRING_JOBS_SWEEP_TOKEN_SUBJECT = "agent-native-recurring-jobs-sweep";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface AutomationSchedulerHealth {
|
|
2
|
+
appId: string;
|
|
3
|
+
orgId: string | null;
|
|
4
|
+
lastCheckedAt: number | null;
|
|
5
|
+
lastDispatchedAt: number | null;
|
|
6
|
+
lastError: string | null;
|
|
7
|
+
runtime: string | null;
|
|
8
|
+
updatedAt: number | null;
|
|
9
|
+
}
|
|
10
|
+
/** Record the last scheduler tick without making health bookkeeping fatal. */
|
|
11
|
+
export declare function recordAutomationSchedulerHealth(input: {
|
|
12
|
+
appId?: string;
|
|
13
|
+
orgId?: string | null;
|
|
14
|
+
checkedAt?: number;
|
|
15
|
+
dispatchedAt?: number;
|
|
16
|
+
error?: string | null;
|
|
17
|
+
runtime?: string | null;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
export declare function getAutomationSchedulerHealth(input?: {
|
|
20
|
+
appId?: string;
|
|
21
|
+
orgId?: string | null;
|
|
22
|
+
}): Promise<AutomationSchedulerHealth | null>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { getDbExec, intType, isPostgres } from "../db/client.js";
|
|
2
|
+
import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
|
|
3
|
+
const TABLE = "automation_scheduler_health";
|
|
4
|
+
const DEFAULT_APP_ID = "default";
|
|
5
|
+
const MAX_ERROR_LENGTH = 500;
|
|
6
|
+
function normalizeAppId(appId) {
|
|
7
|
+
const normalized = appId?.trim();
|
|
8
|
+
return normalized || DEFAULT_APP_ID;
|
|
9
|
+
}
|
|
10
|
+
function rowIdForScope(appId, orgId) {
|
|
11
|
+
return `${appId}:${orgId ?? "global"}`;
|
|
12
|
+
}
|
|
13
|
+
let initPromise;
|
|
14
|
+
async function ensureHealthTable() {
|
|
15
|
+
if (!initPromise) {
|
|
16
|
+
initPromise = (async () => {
|
|
17
|
+
const createSql = `
|
|
18
|
+
CREATE TABLE IF NOT EXISTS ${TABLE} (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
app_id TEXT NOT NULL DEFAULT 'default',
|
|
21
|
+
org_id TEXT,
|
|
22
|
+
last_checked_at ${intType()},
|
|
23
|
+
last_dispatched_at ${intType()},
|
|
24
|
+
last_error TEXT,
|
|
25
|
+
runtime TEXT,
|
|
26
|
+
updated_at ${intType()} NOT NULL
|
|
27
|
+
)
|
|
28
|
+
`;
|
|
29
|
+
const indexSql = `CREATE INDEX IF NOT EXISTS idx_${TABLE}_updated ON ${TABLE} (updated_at)`;
|
|
30
|
+
if (isPostgres()) {
|
|
31
|
+
await ensureTableExists(TABLE, createSql);
|
|
32
|
+
await ensureColumnExists(TABLE, "app_id", `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS app_id TEXT NOT NULL DEFAULT 'default'`);
|
|
33
|
+
await ensureColumnExists(TABLE, "org_id", `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS org_id TEXT`);
|
|
34
|
+
await ensureColumnExists(TABLE, "last_checked_at", `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS last_checked_at ${intType()}`);
|
|
35
|
+
await ensureColumnExists(TABLE, "last_dispatched_at", `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS last_dispatched_at ${intType()}`);
|
|
36
|
+
await ensureColumnExists(TABLE, "last_error", `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS last_error TEXT`);
|
|
37
|
+
await ensureColumnExists(TABLE, "runtime", `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS runtime TEXT`);
|
|
38
|
+
await ensureColumnExists(TABLE, "updated_at", `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS updated_at ${intType()}`);
|
|
39
|
+
await ensureIndexExists(`idx_${TABLE}_updated`, indexSql);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const client = getDbExec();
|
|
43
|
+
await client.execute(createSql);
|
|
44
|
+
const { rows } = await client.execute(`PRAGMA table_info("${TABLE}")`);
|
|
45
|
+
const columns = new Set(rows.map((row) => String(row.name)));
|
|
46
|
+
for (const [name, definition] of [
|
|
47
|
+
["app_id", "TEXT NOT NULL DEFAULT 'default'"],
|
|
48
|
+
["org_id", "TEXT"],
|
|
49
|
+
["last_checked_at", `${intType()}`],
|
|
50
|
+
["last_dispatched_at", `${intType()}`],
|
|
51
|
+
["last_error", "TEXT"],
|
|
52
|
+
["runtime", "TEXT"],
|
|
53
|
+
["updated_at", `${intType()} NOT NULL DEFAULT 0`],
|
|
54
|
+
]) {
|
|
55
|
+
if (columns.has(name))
|
|
56
|
+
continue;
|
|
57
|
+
try {
|
|
58
|
+
await client.execute(`ALTER TABLE ${TABLE} ADD COLUMN ${name} ${definition}`);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const message = String(error?.message ?? error);
|
|
62
|
+
if (!/duplicate column name/i.test(message) &&
|
|
63
|
+
!/column .* already exists/i.test(message)) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
await client.execute(indexSql);
|
|
69
|
+
})().catch((error) => {
|
|
70
|
+
initPromise = undefined;
|
|
71
|
+
throw error;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return initPromise;
|
|
75
|
+
}
|
|
76
|
+
function fromRow(row) {
|
|
77
|
+
return {
|
|
78
|
+
appId: String(row.app_id ?? DEFAULT_APP_ID),
|
|
79
|
+
orgId: row.org_id == null ? null : String(row.org_id),
|
|
80
|
+
lastCheckedAt: row.last_checked_at == null ? null : Number(row.last_checked_at),
|
|
81
|
+
lastDispatchedAt: row.last_dispatched_at == null ? null : Number(row.last_dispatched_at),
|
|
82
|
+
lastError: row.last_error == null ? null : String(row.last_error),
|
|
83
|
+
runtime: row.runtime == null ? null : String(row.runtime),
|
|
84
|
+
updatedAt: row.updated_at == null ? null : Number(row.updated_at),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Record the last scheduler tick without making health bookkeeping fatal. */
|
|
88
|
+
export async function recordAutomationSchedulerHealth(input) {
|
|
89
|
+
await ensureHealthTable();
|
|
90
|
+
const appId = normalizeAppId(input.appId);
|
|
91
|
+
const orgId = input.orgId ?? null;
|
|
92
|
+
const rowId = rowIdForScope(appId, orgId);
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
const checkedAt = input.checkedAt ?? now;
|
|
95
|
+
const existing = await getDbExec().execute({
|
|
96
|
+
sql: `SELECT * FROM ${TABLE} WHERE id = ? LIMIT 1`,
|
|
97
|
+
args: [rowId],
|
|
98
|
+
});
|
|
99
|
+
const row = existing.rows?.[0];
|
|
100
|
+
if (row) {
|
|
101
|
+
await getDbExec().execute({
|
|
102
|
+
sql: `UPDATE ${TABLE}
|
|
103
|
+
SET last_checked_at = ?,
|
|
104
|
+
last_dispatched_at = ?,
|
|
105
|
+
last_error = ?,
|
|
106
|
+
runtime = ?,
|
|
107
|
+
updated_at = ?
|
|
108
|
+
WHERE id = ?`,
|
|
109
|
+
args: [
|
|
110
|
+
checkedAt,
|
|
111
|
+
input.dispatchedAt ?? row.last_dispatched_at ?? null,
|
|
112
|
+
input.error?.slice(0, MAX_ERROR_LENGTH) ?? null,
|
|
113
|
+
input.runtime ?? row.runtime ?? null,
|
|
114
|
+
now,
|
|
115
|
+
rowId,
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
await getDbExec().execute({
|
|
122
|
+
sql: `INSERT INTO ${TABLE}
|
|
123
|
+
(id, app_id, org_id, last_checked_at, last_dispatched_at, last_error, runtime, updated_at)
|
|
124
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
125
|
+
args: [
|
|
126
|
+
rowId,
|
|
127
|
+
appId,
|
|
128
|
+
orgId,
|
|
129
|
+
checkedAt,
|
|
130
|
+
input.dispatchedAt ?? null,
|
|
131
|
+
input.error?.slice(0, MAX_ERROR_LENGTH) ?? null,
|
|
132
|
+
input.runtime ?? null,
|
|
133
|
+
now,
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
// Another serverless isolate may have inserted this scope between the read
|
|
139
|
+
// and insert. Turn that race into the same update path.
|
|
140
|
+
const message = String(error?.message ?? error);
|
|
141
|
+
if (!/unique|duplicate/i.test(message))
|
|
142
|
+
throw error;
|
|
143
|
+
await getDbExec().execute({
|
|
144
|
+
sql: `UPDATE ${TABLE}
|
|
145
|
+
SET last_checked_at = ?,
|
|
146
|
+
last_dispatched_at = COALESCE(?, last_dispatched_at),
|
|
147
|
+
last_error = ?,
|
|
148
|
+
runtime = ?,
|
|
149
|
+
updated_at = ?
|
|
150
|
+
WHERE id = ?`,
|
|
151
|
+
args: [
|
|
152
|
+
checkedAt,
|
|
153
|
+
input.dispatchedAt ?? null,
|
|
154
|
+
input.error?.slice(0, MAX_ERROR_LENGTH) ?? null,
|
|
155
|
+
input.runtime ?? null,
|
|
156
|
+
now,
|
|
157
|
+
rowId,
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export async function getAutomationSchedulerHealth(input = {}) {
|
|
163
|
+
await ensureHealthTable();
|
|
164
|
+
const appId = normalizeAppId(input.appId);
|
|
165
|
+
const rowId = rowIdForScope(appId, input.orgId ?? null);
|
|
166
|
+
const result = await getDbExec().execute({
|
|
167
|
+
sql: `SELECT * FROM ${TABLE} WHERE id = ? LIMIT 1`,
|
|
168
|
+
args: [rowId],
|
|
169
|
+
});
|
|
170
|
+
const row = result.rows?.[0];
|
|
171
|
+
return row ? fromRow(row) : null;
|
|
172
|
+
}
|