@agent-native/core 0.182.1-nightly-20260917183518 → 0.182.1
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/CHANGELOG.md +6 -0
- package/corpus/templates/calendar/CHANGELOG.md +1 -0
- package/corpus/templates/clips/CHANGELOG.md +7 -0
- package/corpus/templates/clips/app/components/library/sort-menu.tsx +2 -2
- package/corpus/templates/clips/changelog/2026-09-17-clips-waits-for-authentication-before-showing-the-recording-.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-17-the-clips-sort-control-keeps-its-keyboard-focus-ring-visible.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/popup.html +1 -1
- package/corpus/templates/clips/chrome-extension/src/popup.ts +30 -21
- package/corpus/templates/content/CHANGELOG.md +7 -0
- package/corpus/templates/content/actions/resolve-preview-document-draft.ts +181 -16
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +28 -15
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +11 -0
- package/corpus/templates/content/app/components/editor/document-save-rebase.ts +13 -10
- package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +80 -0
- package/corpus/templates/content/changelog/2026-09-16-recovery-choices-stay-attached-to-the-draft-you-reviewed.md +6 -0
- package/corpus/templates/design/CHANGELOG.md +29 -0
- package/corpus/templates/design/actions/import-design-source.ts +42 -2
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +26 -15
- package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +83 -1
- package/corpus/templates/design/app/i18n-data.ts +3 -1
- package/corpus/templates/design/app/lib/design-file-upload.ts +6 -4
- package/corpus/templates/design/app/lib/design-import.ts +2 -0
- package/corpus/templates/design/app/lib/fig-client-import.ts +207 -49
- package/corpus/templates/design/changelog/2026-09-17-large-fig-files-import-directly-in-the-browser-without-the-o.md +6 -0
- package/corpus/templates/design/server/lib/fig-file-decoder.ts +31 -8
- package/corpus/templates/design/server/lib/import-design-files.ts +188 -75
- package/corpus/templates/design/server/plugins/db.ts +16 -7
- package/corpus/templates/design/shared/fig-to-frames.ts +55 -25
- package/corpus/templates/factory/CHANGELOG.md +6 -0
- package/corpus/templates/mail/CHANGELOG.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +12 -0
- package/corpus/templates/slides/app/components/editor/AssetLibraryPanel.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +1 -2
- package/corpus/templates/slides/app/hooks/use-agent-generating.ts +82 -2
- package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +9 -2
- package/corpus/templates/slides/app/lib/slide-image-replacement.ts +1 -1
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +1 -1
- package/corpus/templates/slides/server/handlers/assets.ts +134 -8
- package/corpus/templates/slides/server/handlers/uploads.ts +23 -10
- package/corpus/templates/slides/shared/upload-types.ts +1 -0
- package/dist/application-state/store.d.ts +9 -0
- package/dist/application-state/store.js +18 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +7 -0
- package/dist/file-upload/actions/upload-image.d.ts +41 -10
- package/dist/file-upload/actions/upload-image.js +399 -19
- package/dist/jobs/scheduler.js +11 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/package.json +4 -4
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { defineAction } from "../../action.js";
|
|
4
|
+
import { compareAndSetAppState, readAppState, } from "../../application-state/index.js";
|
|
5
|
+
import { appStateCompareAndSet, appStateGet, appStateListByKeyPrefix, } from "../../application-state/store.js";
|
|
3
6
|
import { ssrfSafeFetch } from "../../extensions/url-safety.js";
|
|
4
|
-
import { getRequestUserEmail } from "../../server/request-context.js";
|
|
5
|
-
import { uploadFile } from "../registry.js";
|
|
7
|
+
import { getRequestOrgId, getRequestUserEmail, runWithRequestContext, } from "../../server/request-context.js";
|
|
8
|
+
import { deleteUploadedFile, uploadFile } from "../registry.js";
|
|
6
9
|
const MAX_REMOTE_FETCH_BYTES = 25 * 1024 * 1024;
|
|
7
10
|
const SUPPORTED_IMAGE_MIME_TYPES = new Set([
|
|
8
11
|
"image/png",
|
|
@@ -15,6 +18,314 @@ const SUPPORTED_IMAGE_MIME_TYPES = new Set([
|
|
|
15
18
|
"image/heic",
|
|
16
19
|
"image/heif",
|
|
17
20
|
]);
|
|
21
|
+
export const UPLOAD_RECEIPT_PREFIX = "file-upload-receipt:";
|
|
22
|
+
const UPLOAD_RECEIPT_STAGED_TTL_MS = 24 * 60 * 60 * 1000;
|
|
23
|
+
const UPLOAD_RECEIPT_PENDING_TTL_MS = 5 * 60 * 1000;
|
|
24
|
+
const UPLOAD_RECEIPT_DELETE_LEASE_MS = 5 * 60 * 1000;
|
|
25
|
+
const UPLOAD_RECEIPT_RETRY_DELAY_MS = 15 * 60 * 1000;
|
|
26
|
+
const UPLOAD_RECEIPT_CLEANUP_BATCH_SIZE = 50;
|
|
27
|
+
const UPLOAD_RECEIPT_CLEANUP_INTERVAL_MS = 15 * 60 * 1000;
|
|
28
|
+
const UPLOAD_RECEIPT_RESERVATION_WAIT_MS = 5_000;
|
|
29
|
+
const UPLOAD_RECEIPT_RESERVATION_POLL_MS = 100;
|
|
30
|
+
const UPLOAD_RECEIPT_IMPORT_BATCH_LIMIT = 2_048;
|
|
31
|
+
let uploadReceiptCleanupLastRunAt = 0;
|
|
32
|
+
function uploadReceiptKey(idempotencyKey) {
|
|
33
|
+
return `${UPLOAD_RECEIPT_PREFIX}${idempotencyKey}`;
|
|
34
|
+
}
|
|
35
|
+
function parseUploadReceipt(value) {
|
|
36
|
+
const status = value.status ?? "staged";
|
|
37
|
+
if (typeof value.filename !== "string" ||
|
|
38
|
+
!["pending", "staged", "committed", "deleting"].includes(String(status)) ||
|
|
39
|
+
(value.url !== undefined && typeof value.url !== "string") ||
|
|
40
|
+
(value.provider !== undefined && typeof value.provider !== "string") ||
|
|
41
|
+
(value.id !== undefined && typeof value.id !== "string") ||
|
|
42
|
+
(value.expiresAt !== undefined &&
|
|
43
|
+
(typeof value.expiresAt !== "number" ||
|
|
44
|
+
!Number.isFinite(value.expiresAt)))) {
|
|
45
|
+
throw new Error("Stored image upload receipt is invalid.");
|
|
46
|
+
}
|
|
47
|
+
const normalizedStatus = status;
|
|
48
|
+
if (normalizedStatus !== "pending" &&
|
|
49
|
+
(typeof value.url !== "string" || typeof value.provider !== "string")) {
|
|
50
|
+
throw new Error("Stored image upload receipt is missing provider data.");
|
|
51
|
+
}
|
|
52
|
+
if (normalizedStatus === "pending" &&
|
|
53
|
+
typeof value.reservationId !== "string") {
|
|
54
|
+
throw new Error("Stored image upload reservation is invalid.");
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
status: normalizedStatus,
|
|
58
|
+
...(typeof value.url === "string" ? { url: value.url } : {}),
|
|
59
|
+
...(typeof value.provider === "string" ? { provider: value.provider } : {}),
|
|
60
|
+
filename: value.filename,
|
|
61
|
+
...(typeof value.id === "string" ? { id: value.id } : {}),
|
|
62
|
+
expiresAt: typeof value.expiresAt === "number"
|
|
63
|
+
? value.expiresAt
|
|
64
|
+
: Date.now() + UPLOAD_RECEIPT_STAGED_TTL_MS,
|
|
65
|
+
...(typeof value.reservationId === "string"
|
|
66
|
+
? { reservationId: value.reservationId }
|
|
67
|
+
: {}),
|
|
68
|
+
...(typeof value.leaseExpiresAt === "number"
|
|
69
|
+
? { leaseExpiresAt: value.leaseExpiresAt }
|
|
70
|
+
: {}),
|
|
71
|
+
...(typeof value.ownerEmail === "string"
|
|
72
|
+
? { ownerEmail: value.ownerEmail }
|
|
73
|
+
: {}),
|
|
74
|
+
...(typeof value.orgId === "string" ? { orgId: value.orgId } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function receiptHasProviderData(receipt) {
|
|
78
|
+
return (typeof receipt.url === "string" && typeof receipt.provider === "string");
|
|
79
|
+
}
|
|
80
|
+
function requestReceiptOwner() {
|
|
81
|
+
const ownerEmail = getRequestUserEmail() ?? undefined;
|
|
82
|
+
const orgId = getRequestOrgId() ?? undefined;
|
|
83
|
+
return {
|
|
84
|
+
...(ownerEmail ? { ownerEmail } : {}),
|
|
85
|
+
...(orgId ? { orgId } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function pendingUploadReceipt(filename) {
|
|
89
|
+
return {
|
|
90
|
+
status: "pending",
|
|
91
|
+
filename,
|
|
92
|
+
reservationId: randomUUID(),
|
|
93
|
+
expiresAt: Date.now() + UPLOAD_RECEIPT_PENDING_TTL_MS,
|
|
94
|
+
...requestReceiptOwner(),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function waitForUploadReceipt() {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
setTimeout(resolve, UPLOAD_RECEIPT_RESERVATION_POLL_MS);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async function reserveUploadReceipt(idempotencyKey, filename) {
|
|
103
|
+
const key = uploadReceiptKey(idempotencyKey);
|
|
104
|
+
const deadline = Date.now() + UPLOAD_RECEIPT_RESERVATION_WAIT_MS;
|
|
105
|
+
const pending = pendingUploadReceipt(filename);
|
|
106
|
+
while (true) {
|
|
107
|
+
const stored = await readAppState(key);
|
|
108
|
+
if (!stored) {
|
|
109
|
+
if (await compareAndSetAppState(key, null, pending))
|
|
110
|
+
return { pending };
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const receipt = parseUploadReceipt(stored);
|
|
114
|
+
if (receipt.status === "staged" || receipt.status === "committed") {
|
|
115
|
+
return { receipt };
|
|
116
|
+
}
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
const leaseExpiresAt = receipt.status === "deleting"
|
|
119
|
+
? (receipt.leaseExpiresAt ?? receipt.expiresAt)
|
|
120
|
+
: receipt.expiresAt;
|
|
121
|
+
if (leaseExpiresAt > now) {
|
|
122
|
+
if (now >= deadline) {
|
|
123
|
+
throw new Error("Image upload is still being reconciled. Please retry the import.");
|
|
124
|
+
}
|
|
125
|
+
await waitForUploadReceipt();
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (await compareAndSetAppState(key, stored, pending))
|
|
129
|
+
return { pending };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function deleteReceiptProviderObject(receipt, sessionId) {
|
|
133
|
+
const ownerEmail = receipt.ownerEmail ?? sessionId;
|
|
134
|
+
return await runWithRequestContext({
|
|
135
|
+
...(ownerEmail ? { userEmail: ownerEmail } : {}),
|
|
136
|
+
...(receipt.orgId ? { orgId: receipt.orgId } : {}),
|
|
137
|
+
}, () => deleteUploadedFile(receipt.provider, {
|
|
138
|
+
url: receipt.url,
|
|
139
|
+
id: receipt.id,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
async function cleanupUnrecordedUpload(receipt) {
|
|
143
|
+
try {
|
|
144
|
+
if (await deleteReceiptProviderObject(receipt))
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
149
|
+
throw new Error(`Image upload cleanup failed: ${message}`);
|
|
150
|
+
}
|
|
151
|
+
throw new Error("Image upload cleanup failed: the provider kept the object.");
|
|
152
|
+
}
|
|
153
|
+
async function settleUploadReceipt(idempotencyKey, cleanup) {
|
|
154
|
+
const key = uploadReceiptKey(idempotencyKey);
|
|
155
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
156
|
+
const stored = await readAppState(key);
|
|
157
|
+
if (!stored)
|
|
158
|
+
return { idempotencyKey, alreadyMissing: true };
|
|
159
|
+
const receipt = parseUploadReceipt(stored);
|
|
160
|
+
if (cleanup === "release") {
|
|
161
|
+
if (receipt.status === "committed") {
|
|
162
|
+
return { idempotencyKey, released: true };
|
|
163
|
+
}
|
|
164
|
+
if (receipt.status !== "staged") {
|
|
165
|
+
return { idempotencyKey, released: false };
|
|
166
|
+
}
|
|
167
|
+
const committed = {
|
|
168
|
+
...receipt,
|
|
169
|
+
status: "committed",
|
|
170
|
+
expiresAt: Date.now() + UPLOAD_RECEIPT_STAGED_TTL_MS,
|
|
171
|
+
};
|
|
172
|
+
if (await compareAndSetAppState(key, stored, committed)) {
|
|
173
|
+
return { idempotencyKey, released: true };
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (receipt.status === "pending") {
|
|
178
|
+
if (receipt.expiresAt > Date.now()) {
|
|
179
|
+
return { idempotencyKey, deleted: false };
|
|
180
|
+
}
|
|
181
|
+
const released = await compareAndSetAppState(key, stored, null);
|
|
182
|
+
return released
|
|
183
|
+
? { idempotencyKey, released: true }
|
|
184
|
+
: { idempotencyKey, deleted: false };
|
|
185
|
+
}
|
|
186
|
+
if (!receiptHasProviderData(receipt)) {
|
|
187
|
+
throw new Error("Stored image upload receipt is missing provider data.");
|
|
188
|
+
}
|
|
189
|
+
if (receipt.status === "deleting" &&
|
|
190
|
+
(receipt.leaseExpiresAt ?? receipt.expiresAt) > Date.now()) {
|
|
191
|
+
return { idempotencyKey, deleted: false };
|
|
192
|
+
}
|
|
193
|
+
const deleting = {
|
|
194
|
+
...receipt,
|
|
195
|
+
status: "deleting",
|
|
196
|
+
expiresAt: Date.now() + UPLOAD_RECEIPT_DELETE_LEASE_MS,
|
|
197
|
+
leaseExpiresAt: Date.now() + UPLOAD_RECEIPT_DELETE_LEASE_MS,
|
|
198
|
+
};
|
|
199
|
+
if (!(await compareAndSetAppState(key, stored, deleting)))
|
|
200
|
+
continue;
|
|
201
|
+
const deleted = await deleteReceiptProviderObject(receipt);
|
|
202
|
+
if (!deleted) {
|
|
203
|
+
await compareAndSetAppState(key, deleting, {
|
|
204
|
+
...receipt,
|
|
205
|
+
status: "staged",
|
|
206
|
+
expiresAt: Date.now() + UPLOAD_RECEIPT_RETRY_DELAY_MS,
|
|
207
|
+
});
|
|
208
|
+
return { idempotencyKey, deleted: false };
|
|
209
|
+
}
|
|
210
|
+
await compareAndSetAppState(key, deleting, null);
|
|
211
|
+
return { idempotencyKey, deleted: true };
|
|
212
|
+
}
|
|
213
|
+
return { idempotencyKey, deleted: false };
|
|
214
|
+
}
|
|
215
|
+
function receiptBelongsToRequest(receipt) {
|
|
216
|
+
const owner = requestReceiptOwner();
|
|
217
|
+
return ((!receipt.ownerEmail || receipt.ownerEmail === owner.ownerEmail) &&
|
|
218
|
+
(!receipt.orgId || receipt.orgId === owner.orgId));
|
|
219
|
+
}
|
|
220
|
+
/** Commit every image receipt for a completed browser import batch. */
|
|
221
|
+
export async function commitUploadReceiptsForImport(importId) {
|
|
222
|
+
const rows = await appStateListByKeyPrefix(`${UPLOAD_RECEIPT_PREFIX}${importId}:`, UPLOAD_RECEIPT_IMPORT_BATCH_LIMIT);
|
|
223
|
+
for (const row of rows) {
|
|
224
|
+
const receipt = parseUploadReceipt(row.value);
|
|
225
|
+
if (receipt.status === "committed" || !receiptBelongsToRequest(receipt)) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (receipt.status !== "staged") {
|
|
229
|
+
throw new Error("Image upload receipt is not ready to commit.");
|
|
230
|
+
}
|
|
231
|
+
const committed = {
|
|
232
|
+
...receipt,
|
|
233
|
+
status: "committed",
|
|
234
|
+
expiresAt: Date.now() + UPLOAD_RECEIPT_STAGED_TTL_MS,
|
|
235
|
+
};
|
|
236
|
+
if (!(await appStateCompareAndSet(row.sessionId, row.key, row.value, committed))) {
|
|
237
|
+
const current = await appStateGet(row.sessionId, row.key);
|
|
238
|
+
if (!current || parseUploadReceipt(current).status !== "committed") {
|
|
239
|
+
throw new Error("Could not commit the image upload receipt.");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export async function runUploadReceiptCleanupOnce(options) {
|
|
245
|
+
const now = options?.now ?? Date.now();
|
|
246
|
+
if (!options?.force &&
|
|
247
|
+
now - uploadReceiptCleanupLastRunAt < UPLOAD_RECEIPT_CLEANUP_INTERVAL_MS) {
|
|
248
|
+
return { scanned: 0, deleted: 0, released: 0, failed: 0, skipped: true };
|
|
249
|
+
}
|
|
250
|
+
uploadReceiptCleanupLastRunAt = now;
|
|
251
|
+
const rows = await appStateListByKeyPrefix(UPLOAD_RECEIPT_PREFIX, options?.limit ?? UPLOAD_RECEIPT_CLEANUP_BATCH_SIZE);
|
|
252
|
+
let deleted = 0;
|
|
253
|
+
let released = 0;
|
|
254
|
+
let failed = 0;
|
|
255
|
+
for (const row of rows) {
|
|
256
|
+
let receipt;
|
|
257
|
+
try {
|
|
258
|
+
receipt = parseUploadReceipt(row.value);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
failed += 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (receipt.expiresAt > now)
|
|
265
|
+
continue;
|
|
266
|
+
if (receipt.status === "pending") {
|
|
267
|
+
if (await appStateCompareAndSet(row.sessionId, row.key, row.value, null)) {
|
|
268
|
+
released += 1;
|
|
269
|
+
}
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (receipt.status === "committed") {
|
|
273
|
+
if (await appStateCompareAndSet(row.sessionId, row.key, row.value, null)) {
|
|
274
|
+
released += 1;
|
|
275
|
+
}
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (!receiptHasProviderData(receipt)) {
|
|
279
|
+
failed += 1;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (receipt.status === "deleting" &&
|
|
283
|
+
(receipt.leaseExpiresAt ?? receipt.expiresAt) > now) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const leaseExpiresAt = now + UPLOAD_RECEIPT_DELETE_LEASE_MS;
|
|
287
|
+
const deleting = {
|
|
288
|
+
...receipt,
|
|
289
|
+
status: "deleting",
|
|
290
|
+
expiresAt: leaseExpiresAt,
|
|
291
|
+
leaseExpiresAt,
|
|
292
|
+
};
|
|
293
|
+
if (!(await appStateCompareAndSet(row.sessionId, row.key, row.value, deleting))) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
let objectDeleted = false;
|
|
297
|
+
try {
|
|
298
|
+
objectDeleted = await deleteReceiptProviderObject(receipt, row.sessionId);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
console.error("[file-upload] Failed to delete an expired upload receipt:", error);
|
|
302
|
+
}
|
|
303
|
+
if (objectDeleted) {
|
|
304
|
+
if (await appStateCompareAndSet(row.sessionId, row.key, deleting, null)) {
|
|
305
|
+
deleted += 1;
|
|
306
|
+
}
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
failed += 1;
|
|
310
|
+
try {
|
|
311
|
+
await appStateCompareAndSet(row.sessionId, row.key, deleting, {
|
|
312
|
+
...receipt,
|
|
313
|
+
status: "staged",
|
|
314
|
+
expiresAt: now + UPLOAD_RECEIPT_RETRY_DELAY_MS,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
console.error("[file-upload] Failed to requeue an expired upload receipt:", error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
scanned: rows.length,
|
|
323
|
+
deleted,
|
|
324
|
+
released,
|
|
325
|
+
failed,
|
|
326
|
+
skipped: false,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
18
329
|
function extensionFromMime(mimeType) {
|
|
19
330
|
const bare = mimeType.split(";")[0].trim().toLowerCase();
|
|
20
331
|
if (bare === "image/jpeg" || bare === "image/jpg")
|
|
@@ -139,11 +450,17 @@ export default defineAction({
|
|
|
139
450
|
.string()
|
|
140
451
|
.optional()
|
|
141
452
|
.describe("Optional filename hint, used by the provider for display and to derive an extension when missing."),
|
|
453
|
+
idempotencyKey: z.string().trim().min(1).max(200).optional(),
|
|
454
|
+
cleanup: z.enum(["delete", "release"]).optional(),
|
|
142
455
|
})
|
|
143
|
-
.refine((args) => !!args.data || !!args.url, {
|
|
144
|
-
message: "Either
|
|
456
|
+
.refine((args) => args.cleanup ? !!args.idempotencyKey : !!args.data || !!args.url, {
|
|
457
|
+
message: "Either a data/url upload or an idempotencyKey cleanup is required.",
|
|
145
458
|
}),
|
|
146
459
|
run: async (args) => {
|
|
460
|
+
const idempotencyKey = args.idempotencyKey?.trim();
|
|
461
|
+
if (args.cleanup && idempotencyKey) {
|
|
462
|
+
return settleUploadReceipt(idempotencyKey, args.cleanup);
|
|
463
|
+
}
|
|
147
464
|
let bytes;
|
|
148
465
|
let mimeType;
|
|
149
466
|
if (args.data) {
|
|
@@ -161,24 +478,87 @@ export default defineAction({
|
|
|
161
478
|
};
|
|
162
479
|
}
|
|
163
480
|
const filename = (args.filename || defaultFilename(mimeType)).trim();
|
|
481
|
+
let pendingReceipt;
|
|
482
|
+
if (idempotencyKey) {
|
|
483
|
+
const reserved = await reserveUploadReceipt(idempotencyKey, filename);
|
|
484
|
+
if (reserved.receipt) {
|
|
485
|
+
if (!receiptHasProviderData(reserved.receipt)) {
|
|
486
|
+
throw new Error("Stored image upload receipt is missing provider data.");
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
url: reserved.receipt.url,
|
|
490
|
+
id: reserved.receipt.id,
|
|
491
|
+
provider: reserved.receipt.provider,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
pendingReceipt = reserved.pending;
|
|
495
|
+
}
|
|
164
496
|
const ownerEmail = getRequestUserEmail() ?? undefined;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
497
|
+
try {
|
|
498
|
+
const result = await uploadFile({
|
|
499
|
+
data: bytes,
|
|
500
|
+
filename,
|
|
501
|
+
mimeType,
|
|
502
|
+
ownerEmail,
|
|
503
|
+
});
|
|
504
|
+
if (!result) {
|
|
505
|
+
if (pendingReceipt && idempotencyKey) {
|
|
506
|
+
await compareAndSetAppState(uploadReceiptKey(idempotencyKey), pendingReceipt, null);
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
error: uploadNotConfiguredError(),
|
|
510
|
+
configured: false,
|
|
511
|
+
connectPath: "/_agent-native/builder/connect",
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
if (pendingReceipt && idempotencyKey) {
|
|
515
|
+
const stagedReceipt = {
|
|
516
|
+
...pendingReceipt,
|
|
517
|
+
status: "staged",
|
|
518
|
+
url: result.url,
|
|
519
|
+
...(result.id ? { id: result.id } : {}),
|
|
520
|
+
provider: result.provider,
|
|
521
|
+
expiresAt: Date.now() + UPLOAD_RECEIPT_STAGED_TTL_MS,
|
|
522
|
+
};
|
|
523
|
+
const unrecordedReceipt = {
|
|
524
|
+
...stagedReceipt,
|
|
525
|
+
url: result.url,
|
|
526
|
+
provider: result.provider,
|
|
527
|
+
};
|
|
528
|
+
const staged = await compareAndSetAppState(uploadReceiptKey(idempotencyKey), pendingReceipt, stagedReceipt);
|
|
529
|
+
if (!staged) {
|
|
530
|
+
const current = await readAppState(uploadReceiptKey(idempotencyKey));
|
|
531
|
+
if (current) {
|
|
532
|
+
const existing = parseUploadReceipt(current);
|
|
533
|
+
if (receiptHasProviderData(existing)) {
|
|
534
|
+
await cleanupUnrecordedUpload(unrecordedReceipt);
|
|
535
|
+
return {
|
|
536
|
+
url: existing.url,
|
|
537
|
+
id: existing.id,
|
|
538
|
+
provider: existing.provider,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
await cleanupUnrecordedUpload(unrecordedReceipt);
|
|
543
|
+
throw new Error("Could not record the image upload receipt.");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
172
546
|
return {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
547
|
+
url: result.url,
|
|
548
|
+
id: result.id,
|
|
549
|
+
provider: result.provider,
|
|
176
550
|
};
|
|
177
551
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
552
|
+
catch (error) {
|
|
553
|
+
if (pendingReceipt && idempotencyKey) {
|
|
554
|
+
try {
|
|
555
|
+
await compareAndSetAppState(uploadReceiptKey(idempotencyKey), pendingReceipt, null);
|
|
556
|
+
}
|
|
557
|
+
catch (cleanupError) {
|
|
558
|
+
console.error("[file-upload] Failed to release upload reservation:", cleanupError);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
throw error;
|
|
562
|
+
}
|
|
183
563
|
},
|
|
184
564
|
});
|
package/dist/jobs/scheduler.js
CHANGED
|
@@ -112,6 +112,17 @@ export async function processRecurringJobs(deps) {
|
|
|
112
112
|
}
|
|
113
113
|
async function processRecurringJobsWithLease(deps) {
|
|
114
114
|
subscribeToJobsResourceEvents();
|
|
115
|
+
// Upload receipts are framework-owned temporary state, so the same durable
|
|
116
|
+
// scheduler sweep that runs on serverless hosts also expires abandoned
|
|
117
|
+
// provider objects. The cleanup is internally throttled and never blocks
|
|
118
|
+
// recurring jobs when a provider or database is unavailable.
|
|
119
|
+
try {
|
|
120
|
+
const { runUploadReceiptCleanupOnce } = await import("../file-upload/actions/upload-image.js");
|
|
121
|
+
await runUploadReceiptCleanupOnce();
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error("[recurring-jobs] Upload receipt cleanup failed:", error);
|
|
125
|
+
}
|
|
115
126
|
// Skip if we recently confirmed there are no job resources to run.
|
|
116
127
|
const nowMs = Date.now();
|
|
117
128
|
// Write a global heartbeat before the resource scan. A slow or failed scan
|
|
@@ -137,13 +137,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
137
137
|
inputSchema: {
|
|
138
138
|
type: string;
|
|
139
139
|
properties: {
|
|
140
|
-
count?: undefined;
|
|
141
140
|
query?: undefined;
|
|
142
141
|
minutes?: undefined;
|
|
143
142
|
limit?: undefined;
|
|
144
143
|
clientHint?: undefined;
|
|
145
144
|
timestamp?: undefined;
|
|
146
145
|
chapterId?: undefined;
|
|
146
|
+
count?: undefined;
|
|
147
147
|
startAt?: undefined;
|
|
148
148
|
endAt?: undefined;
|
|
149
149
|
reason?: undefined;
|
|
@@ -159,7 +159,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
159
159
|
inputSchema: {
|
|
160
160
|
type: string;
|
|
161
161
|
properties: {
|
|
162
|
-
count?: undefined;
|
|
163
162
|
query: {
|
|
164
163
|
type: string;
|
|
165
164
|
description: string;
|
|
@@ -175,6 +174,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
175
174
|
clientHint?: undefined;
|
|
176
175
|
timestamp?: undefined;
|
|
177
176
|
chapterId?: undefined;
|
|
177
|
+
count?: undefined;
|
|
178
178
|
startAt?: undefined;
|
|
179
179
|
endAt?: undefined;
|
|
180
180
|
reason?: undefined;
|
|
@@ -190,7 +190,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
190
190
|
inputSchema: {
|
|
191
191
|
type: string;
|
|
192
192
|
properties: {
|
|
193
|
-
count?: undefined;
|
|
194
193
|
minutes: {
|
|
195
194
|
type: string;
|
|
196
195
|
description: string;
|
|
@@ -200,6 +199,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
200
199
|
clientHint?: undefined;
|
|
201
200
|
timestamp?: undefined;
|
|
202
201
|
chapterId?: undefined;
|
|
202
|
+
count?: undefined;
|
|
203
203
|
startAt?: undefined;
|
|
204
204
|
endAt?: undefined;
|
|
205
205
|
reason?: undefined;
|
|
@@ -216,7 +216,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
216
216
|
type: string;
|
|
217
217
|
required: string[];
|
|
218
218
|
properties: {
|
|
219
|
-
count?: undefined;
|
|
220
219
|
query: {
|
|
221
220
|
type: string;
|
|
222
221
|
description: string;
|
|
@@ -235,6 +234,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
235
234
|
};
|
|
236
235
|
timestamp?: undefined;
|
|
237
236
|
chapterId?: undefined;
|
|
237
|
+
count?: undefined;
|
|
238
238
|
startAt?: undefined;
|
|
239
239
|
endAt?: undefined;
|
|
240
240
|
reason?: undefined;
|
|
@@ -250,7 +250,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
250
250
|
type: string;
|
|
251
251
|
required: string[];
|
|
252
252
|
properties: {
|
|
253
|
-
count?: undefined;
|
|
254
253
|
query?: undefined;
|
|
255
254
|
minutes?: undefined;
|
|
256
255
|
limit?: undefined;
|
|
@@ -264,6 +263,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
264
263
|
description: string;
|
|
265
264
|
};
|
|
266
265
|
chapterId?: undefined;
|
|
266
|
+
count?: undefined;
|
|
267
267
|
startAt?: undefined;
|
|
268
268
|
endAt?: undefined;
|
|
269
269
|
includeMicrophone?: undefined;
|
|
@@ -314,13 +314,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
314
314
|
type: string;
|
|
315
315
|
required: string[];
|
|
316
316
|
properties: {
|
|
317
|
-
count?: undefined;
|
|
318
317
|
query?: undefined;
|
|
319
318
|
minutes?: undefined;
|
|
320
319
|
limit?: undefined;
|
|
321
320
|
clientHint?: undefined;
|
|
322
321
|
timestamp?: undefined;
|
|
323
322
|
chapterId?: undefined;
|
|
323
|
+
count?: undefined;
|
|
324
324
|
startAt: {
|
|
325
325
|
type: string;
|
|
326
326
|
description: string;
|
|
@@ -349,13 +349,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
349
349
|
type: string;
|
|
350
350
|
required: string[];
|
|
351
351
|
properties: {
|
|
352
|
-
count?: undefined;
|
|
353
352
|
query?: undefined;
|
|
354
353
|
minutes?: undefined;
|
|
355
354
|
limit?: undefined;
|
|
356
355
|
clientHint?: undefined;
|
|
357
356
|
timestamp?: undefined;
|
|
358
357
|
chapterId?: undefined;
|
|
358
|
+
count?: undefined;
|
|
359
359
|
startAt?: undefined;
|
|
360
360
|
endAt?: undefined;
|
|
361
361
|
reason?: undefined;
|
|
@@ -76,9 +76,7 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
76
76
|
}>>;
|
|
77
77
|
}, z.core.$strip>>, {
|
|
78
78
|
id?: undefined;
|
|
79
|
-
provider?: undefined;
|
|
80
79
|
deleted?: undefined;
|
|
81
|
-
found?: undefined;
|
|
82
80
|
providers: {
|
|
83
81
|
id: string;
|
|
84
82
|
label: string;
|
|
@@ -89,6 +87,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
89
87
|
updatedAt: number;
|
|
90
88
|
}[];
|
|
91
89
|
count: number;
|
|
90
|
+
provider?: undefined;
|
|
91
|
+
found?: undefined;
|
|
92
92
|
registered?: undefined;
|
|
93
93
|
label?: undefined;
|
|
94
94
|
message?: undefined;
|
|
@@ -103,31 +103,31 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
103
103
|
label?: undefined;
|
|
104
104
|
message?: undefined;
|
|
105
105
|
} | {
|
|
106
|
-
provider?: undefined;
|
|
107
106
|
deleted?: undefined;
|
|
108
107
|
providers?: undefined;
|
|
109
108
|
count?: undefined;
|
|
109
|
+
provider?: undefined;
|
|
110
110
|
found: boolean;
|
|
111
111
|
id: string;
|
|
112
112
|
registered?: undefined;
|
|
113
113
|
label?: undefined;
|
|
114
114
|
message?: undefined;
|
|
115
115
|
} | {
|
|
116
|
-
provider?: undefined;
|
|
117
|
-
found?: undefined;
|
|
118
116
|
providers?: undefined;
|
|
119
117
|
count?: undefined;
|
|
118
|
+
provider?: undefined;
|
|
119
|
+
found?: undefined;
|
|
120
120
|
deleted: boolean;
|
|
121
121
|
id: string;
|
|
122
122
|
registered?: undefined;
|
|
123
123
|
label?: undefined;
|
|
124
124
|
message?: undefined;
|
|
125
125
|
} | {
|
|
126
|
-
provider?: undefined;
|
|
127
126
|
deleted?: undefined;
|
|
128
|
-
found?: undefined;
|
|
129
127
|
providers?: undefined;
|
|
130
128
|
count?: undefined;
|
|
129
|
+
provider?: undefined;
|
|
130
|
+
found?: undefined;
|
|
131
131
|
registered: boolean;
|
|
132
132
|
id: string;
|
|
133
133
|
label: string;
|
|
@@ -312,7 +312,8 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
312
312
|
scope?: "org" | "user" | undefined;
|
|
313
313
|
}, {
|
|
314
314
|
id?: undefined;
|
|
315
|
-
|
|
315
|
+
message?: undefined;
|
|
316
|
+
deleted?: undefined;
|
|
316
317
|
providers: {
|
|
317
318
|
id: string;
|
|
318
319
|
label: string;
|
|
@@ -324,46 +325,45 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
324
325
|
}[];
|
|
325
326
|
count: number;
|
|
326
327
|
provider?: undefined;
|
|
327
|
-
|
|
328
|
+
found?: undefined;
|
|
328
329
|
registered?: undefined;
|
|
329
330
|
label?: undefined;
|
|
330
|
-
message?: undefined;
|
|
331
331
|
} | {
|
|
332
332
|
id?: undefined;
|
|
333
|
+
message?: undefined;
|
|
334
|
+
deleted?: undefined;
|
|
333
335
|
providers?: undefined;
|
|
334
336
|
count?: undefined;
|
|
335
337
|
found: boolean;
|
|
336
338
|
provider: import("../custom-registry.js").CustomProviderConfig;
|
|
337
|
-
deleted?: undefined;
|
|
338
339
|
registered?: undefined;
|
|
339
340
|
label?: undefined;
|
|
340
|
-
message?: undefined;
|
|
341
341
|
} | {
|
|
342
|
+
message?: undefined;
|
|
343
|
+
deleted?: undefined;
|
|
342
344
|
providers?: undefined;
|
|
343
345
|
count?: undefined;
|
|
344
346
|
provider?: undefined;
|
|
345
347
|
found: boolean;
|
|
346
348
|
id: string;
|
|
347
|
-
deleted?: undefined;
|
|
348
349
|
registered?: undefined;
|
|
349
350
|
label?: undefined;
|
|
350
|
-
message?: undefined;
|
|
351
351
|
} | {
|
|
352
|
-
|
|
352
|
+
message?: undefined;
|
|
353
353
|
providers?: undefined;
|
|
354
354
|
count?: undefined;
|
|
355
355
|
provider?: undefined;
|
|
356
|
+
found?: undefined;
|
|
356
357
|
deleted: boolean;
|
|
357
358
|
id: string;
|
|
358
359
|
registered?: undefined;
|
|
359
360
|
label?: undefined;
|
|
360
|
-
message?: undefined;
|
|
361
361
|
} | {
|
|
362
|
-
|
|
362
|
+
deleted?: undefined;
|
|
363
363
|
providers?: undefined;
|
|
364
364
|
count?: undefined;
|
|
365
365
|
provider?: undefined;
|
|
366
|
-
|
|
366
|
+
found?: undefined;
|
|
367
367
|
registered: boolean;
|
|
368
368
|
id: string;
|
|
369
369
|
label: string;
|