@opengeni/worker-bundle 3.0.1-canary.38872461489001 → 3.1.2
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/activities/agent-turn/errors.d.ts +1 -0
- package/dist/activities/run-input.d.ts +16 -0
- package/dist/{activities-control-3PJDM3QQ.js → activities-control-ROJJNNTJ.js} +2 -2
- package/dist/{activities-turn-2ILW6PRK.js → activities-turn-4SWYWKI5.js} +2 -2
- package/dist/artifact-materializer-entry.js +0 -0
- package/dist/artifact-outbox-entry.js +0 -0
- package/dist/{chunk-QCD6PEBU.js → chunk-6SHXF4SU.js} +103 -13
- package/dist/{chunk-QCD6PEBU.js.map → chunk-6SHXF4SU.js.map} +1 -1
- package/dist/index.js +1 -1
- package/package.json +24 -24
- package/src/activities/agent-turn/errors.ts +30 -1
- package/src/activities/agent-turn/provider-recovery-copy.ts +2 -0
- package/src/activities/run-input.ts +122 -14
- /package/dist/{activities-control-3PJDM3QQ.js.map → activities-control-ROJJNNTJ.js.map} +0 -0
- /package/dist/{activities-turn-2ILW6PRK.js.map → activities-turn-4SWYWKI5.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1434,7 +1434,7 @@ async function createDefaultWorkerActivities(role, dependencies = {}) {
|
|
|
1434
1434
|
if (dependencies.objectStorage == null) {
|
|
1435
1435
|
prepareObjectStorageProvider(dependencies.settings ?? getSettings());
|
|
1436
1436
|
}
|
|
1437
|
-
return role === "control" ? (await import("./activities-control-
|
|
1437
|
+
return role === "control" ? (await import("./activities-control-ROJJNNTJ.js")).createControlActivities(dependencies) : (await import("./activities-turn-4SWYWKI5.js")).createTurnActivities(dependencies);
|
|
1438
1438
|
}
|
|
1439
1439
|
function resolveOpenGeniWorkflowDefinition(moduleUrl = import.meta.url) {
|
|
1440
1440
|
const modulePath = fileURLToPath(moduleUrl).replaceAll("\\", "/");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/worker-bundle",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "Opengeni worker entry and reusable embedded lifecycle, shipped with a release-coherent pre-bundled Temporal workflow artifact.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -55,29 +55,29 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@1password/sdk": "0.5.0",
|
|
57
57
|
"@llamaindex/liteparse": "2.14.2",
|
|
58
|
-
"@opengeni/agent-proto": "3.
|
|
59
|
-
"@opengeni/capabilities": "3.
|
|
60
|
-
"@opengeni/codemode": "3.
|
|
61
|
-
"@opengeni/codex": "3.
|
|
62
|
-
"@opengeni/config": "3.
|
|
63
|
-
"@opengeni/contracts": "3.
|
|
64
|
-
"@opengeni/core": "3.
|
|
65
|
-
"@opengeni/db": "3.
|
|
66
|
-
"@opengeni/documents": "3.
|
|
67
|
-
"@opengeni/events": "3.
|
|
68
|
-
"@opengeni/github": "3.
|
|
69
|
-
"@opengeni/interaction": "3.
|
|
70
|
-
"@opengeni/jev": "3.
|
|
71
|
-
"@opengeni/network": "3.
|
|
72
|
-
"@opengeni/observability": "3.
|
|
73
|
-
"@opengeni/runtime": "3.
|
|
74
|
-
"@opengeni/sdk": "3.
|
|
75
|
-
"@opengeni/storage": "3.
|
|
76
|
-
"@opengeni/subscriptions": "3.
|
|
77
|
-
"@opengeni/tool-gateway": "3.
|
|
78
|
-
"@opengeni/vaults": "3.
|
|
79
|
-
"@opengeni/vaults-onepassword": "3.
|
|
80
|
-
"@opengeni/xai-subscription": "3.
|
|
58
|
+
"@opengeni/agent-proto": "3.1.2",
|
|
59
|
+
"@opengeni/capabilities": "3.1.2",
|
|
60
|
+
"@opengeni/codemode": "3.1.2",
|
|
61
|
+
"@opengeni/codex": "3.1.2",
|
|
62
|
+
"@opengeni/config": "3.1.2",
|
|
63
|
+
"@opengeni/contracts": "3.1.2",
|
|
64
|
+
"@opengeni/core": "3.1.2",
|
|
65
|
+
"@opengeni/db": "3.1.2",
|
|
66
|
+
"@opengeni/documents": "3.1.2",
|
|
67
|
+
"@opengeni/events": "3.1.2",
|
|
68
|
+
"@opengeni/github": "3.1.2",
|
|
69
|
+
"@opengeni/interaction": "3.1.2",
|
|
70
|
+
"@opengeni/jev": "3.1.2",
|
|
71
|
+
"@opengeni/network": "3.1.2",
|
|
72
|
+
"@opengeni/observability": "3.1.2",
|
|
73
|
+
"@opengeni/runtime": "3.1.2",
|
|
74
|
+
"@opengeni/sdk": "3.1.2",
|
|
75
|
+
"@opengeni/storage": "3.1.2",
|
|
76
|
+
"@opengeni/subscriptions": "3.1.2",
|
|
77
|
+
"@opengeni/tool-gateway": "3.1.2",
|
|
78
|
+
"@opengeni/vaults": "3.1.2",
|
|
79
|
+
"@opengeni/vaults-onepassword": "3.1.2",
|
|
80
|
+
"@opengeni/xai-subscription": "3.1.2",
|
|
81
81
|
"@temporalio/activity": "^1.17.0",
|
|
82
82
|
"@temporalio/client": "^1.17.0",
|
|
83
83
|
"@temporalio/worker": "^1.17.0",
|
|
@@ -57,7 +57,10 @@ import {
|
|
|
57
57
|
import { ApplicationFailure, CancelledFailure } from "@temporalio/activity";
|
|
58
58
|
import type { ProviderErrorOutcome } from "@opengeni/subscriptions";
|
|
59
59
|
import { CODEX_USAGE_EXHAUSTED_PCT } from "./codex-usage-policy";
|
|
60
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
RetainedAttachmentTransportLimitError,
|
|
62
|
+
RetainedAttachmentUnavailableError,
|
|
63
|
+
} from "../run-input";
|
|
61
64
|
import type { CodexAccountStatus } from "@opengeni/db";
|
|
62
65
|
import {
|
|
63
66
|
CODEX_USAGE_LIMIT_ERROR_TYPE,
|
|
@@ -144,6 +147,21 @@ export function providerRecoveryJitterMs(delayMs: number, sample: number): numbe
|
|
|
144
147
|
return Math.floor(Math.min(5_000, delayMs * 0.2) * bounded);
|
|
145
148
|
}
|
|
146
149
|
export const POST_COMPACTION_CONTINUATION_EMPTY_CODE = "post_compaction_continuation_empty";
|
|
150
|
+
export const RETAINED_ATTACHMENT_UNAVAILABLE_CODE = "retained_attachment_unavailable";
|
|
151
|
+
|
|
152
|
+
function findRetainedAttachmentUnavailableError(
|
|
153
|
+
error: unknown,
|
|
154
|
+
): RetainedAttachmentUnavailableError | null {
|
|
155
|
+
let current = error;
|
|
156
|
+
const seen = new Set<unknown>();
|
|
157
|
+
for (let depth = 0; depth < 6 && current && typeof current === "object"; depth += 1) {
|
|
158
|
+
if (current instanceof RetainedAttachmentUnavailableError) return current;
|
|
159
|
+
if (seen.has(current)) break;
|
|
160
|
+
seen.add(current);
|
|
161
|
+
current = (current as { cause?: unknown }).cause;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
147
165
|
|
|
148
166
|
export class PostCompactionContinuationEmptyError extends Error {
|
|
149
167
|
readonly code = POST_COMPACTION_CONTINUATION_EMPTY_CODE;
|
|
@@ -221,6 +239,7 @@ export function providerRecoveryResult(input: {
|
|
|
221
239
|
input.failureCode === "provider_unavailable" ||
|
|
222
240
|
input.failureCode === "upstream_connectivity_unavailable" ||
|
|
223
241
|
input.failureCode === "sandbox_command_start_unavailable" ||
|
|
242
|
+
input.failureCode === RETAINED_ATTACHMENT_UNAVAILABLE_CODE ||
|
|
224
243
|
input.failureCode === "mcp_transport_timeout" ||
|
|
225
244
|
input.failureCode === "mcp_transport_unavailable" ||
|
|
226
245
|
input.failureCode === "turn_execution_policy_definition_mismatch" ||
|
|
@@ -1809,6 +1828,16 @@ function baseAgentRunFailurePayload(
|
|
|
1809
1828
|
if (error instanceof RetainedAttachmentTransportLimitError) {
|
|
1810
1829
|
return { error: error.message, code: "retained_attachment_transport_limit", retryable: false };
|
|
1811
1830
|
}
|
|
1831
|
+
if (findRetainedAttachmentUnavailableError(error)) {
|
|
1832
|
+
// Storage may lag after a replica outage. The same turn retries with the
|
|
1833
|
+
// connectivity backoff and never continues without the image's pixels.
|
|
1834
|
+
return {
|
|
1835
|
+
error:
|
|
1836
|
+
"An image attached earlier in this conversation can't be read from storage right now. The same turn will retry after a short delay.",
|
|
1837
|
+
code: RETAINED_ATTACHMENT_UNAVAILABLE_CODE,
|
|
1838
|
+
retryable: true,
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1812
1841
|
if (error instanceof MandatoryHistoryPersistenceError) {
|
|
1813
1842
|
const databaseFailure =
|
|
1814
1843
|
isSessionEventPersistenceError(error.cause) ||
|
|
@@ -56,6 +56,8 @@ export function providerRecoverySubject(input: ProviderRecoveryCopyInput): strin
|
|
|
56
56
|
return "A required MCP server is unreachable";
|
|
57
57
|
case "sandbox_command_start_unavailable":
|
|
58
58
|
return "The sandbox isn't ready yet";
|
|
59
|
+
case "retained_attachment_unavailable":
|
|
60
|
+
return "An image from earlier in this conversation can't be read from storage";
|
|
59
61
|
case "turn_execution_policy_definition_mismatch":
|
|
60
62
|
return "Opengeni is applying a configuration update";
|
|
61
63
|
default:
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
MODEL_ATTACHMENT_REFS_FIELD,
|
|
3
3
|
MODEL_ATTACHMENT_CATALOG_MARKER,
|
|
4
4
|
MODEL_ATTACHMENT_CATALOG_RENDERED_FIELD,
|
|
5
|
+
MODEL_ATTACHMENT_IMAGE_MAX_DIMENSION_FIELD,
|
|
5
6
|
FileResourceRef,
|
|
6
7
|
resourceMountPath,
|
|
7
8
|
readTurnExecutionPolicyV1,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
type Database,
|
|
22
23
|
} from "@opengeni/db";
|
|
23
24
|
import {
|
|
25
|
+
createModelImageSizer,
|
|
24
26
|
projectHistoryForProvider,
|
|
25
27
|
projectRejectedProviderArtifactsFromSerializedRunState,
|
|
26
28
|
projectRejectedReasoningArtifact,
|
|
@@ -219,6 +221,25 @@ export class RetainedAttachmentTransportLimitError extends Error {
|
|
|
219
221
|
}
|
|
220
222
|
}
|
|
221
223
|
|
|
224
|
+
/**
|
|
225
|
+
* A retained image the conversation already sent could not be read from
|
|
226
|
+
* object storage, for example while a storage replica catches up after an
|
|
227
|
+
* outage. Its pixels are part of the accepted prompt, so the turn waits and
|
|
228
|
+
* retries rather than continuing without them. Integrity mismatches are not
|
|
229
|
+
* this error: retrying cannot repair different bytes.
|
|
230
|
+
*/
|
|
231
|
+
export class RetainedAttachmentUnavailableError extends Error {
|
|
232
|
+
readonly fileIds: readonly string[];
|
|
233
|
+
|
|
234
|
+
constructor(fileIds: readonly string[]) {
|
|
235
|
+
super("An image attached earlier in this conversation can't be read from storage right now.");
|
|
236
|
+
this.name = "RetainedAttachmentUnavailableError";
|
|
237
|
+
this.fileIds = [...fileIds];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export type ModelAttachmentReadFailure = "unavailable" | "integrity_mismatch";
|
|
242
|
+
|
|
222
243
|
export type ModelAttachmentContent = {
|
|
223
244
|
kind: "image" | "file";
|
|
224
245
|
fileId: string;
|
|
@@ -286,7 +307,11 @@ function modelAttachmentDescriptor(
|
|
|
286
307
|
export async function modelAttachmentContentForFiles(
|
|
287
308
|
files: FileAsset[],
|
|
288
309
|
readFileBytes: (file: FileAsset) => Promise<Uint8Array>,
|
|
289
|
-
options: {
|
|
310
|
+
options: {
|
|
311
|
+
retainAll?: boolean;
|
|
312
|
+
/** Why a selected file produced no content; omitted files are not reported. */
|
|
313
|
+
onReadFailure?: (fileId: string, failure: ModelAttachmentReadFailure) => void;
|
|
314
|
+
} = {},
|
|
290
315
|
): Promise<ModelAttachmentContent[]> {
|
|
291
316
|
const selected: Array<{
|
|
292
317
|
file: FileAsset;
|
|
@@ -327,12 +352,14 @@ export async function modelAttachmentContentForFiles(
|
|
|
327
352
|
expectedSizeBytes: file.sizeBytes,
|
|
328
353
|
actualSizeBytes: bytes.byteLength,
|
|
329
354
|
});
|
|
355
|
+
options.onReadFailure?.(file.id, "integrity_mismatch");
|
|
330
356
|
continue;
|
|
331
357
|
}
|
|
332
358
|
if (createHash("sha256").update(bytes).digest("hex") !== checksum) {
|
|
333
359
|
console.error("model attachment checksum did not match finalized metadata", {
|
|
334
360
|
fileId: file.id,
|
|
335
361
|
});
|
|
362
|
+
options.onReadFailure?.(file.id, "integrity_mismatch");
|
|
336
363
|
continue;
|
|
337
364
|
}
|
|
338
365
|
attachments[index] = {
|
|
@@ -350,6 +377,7 @@ export async function modelAttachmentContentForFiles(
|
|
|
350
377
|
fileId: file.id,
|
|
351
378
|
errorType: safeErrorType(error),
|
|
352
379
|
});
|
|
380
|
+
options.onReadFailure?.(file.id, "unavailable");
|
|
353
381
|
}
|
|
354
382
|
}
|
|
355
383
|
};
|
|
@@ -392,10 +420,64 @@ function unavailableAttachmentReceiptText(ref: FileResourceRef): string {
|
|
|
392
420
|
return requesterUnavailableReceiptText(`Attachment: fileId=${ref.fileId}`, "file");
|
|
393
421
|
}
|
|
394
422
|
|
|
423
|
+
/** The image bound stamped on a message when it was accepted, if any. */
|
|
424
|
+
function attachmentImageBound(item: Record<string, unknown>): number | undefined {
|
|
425
|
+
const raw = item[MODEL_ATTACHMENT_IMAGE_MAX_DIMENSION_FIELD];
|
|
426
|
+
return typeof raw === "number" && Number.isSafeInteger(raw) && raw >= 64 && raw <= 8192
|
|
427
|
+
? raw
|
|
428
|
+
: undefined;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Process-wide, so a long conversation does not re-encode the same photos on
|
|
432
|
+
// every turn. The projection depends only on the image bytes and the bound.
|
|
433
|
+
const boundedImageSizers = new Map<number, ReturnType<typeof createModelImageSizer>>();
|
|
434
|
+
|
|
435
|
+
type BoundedAttachmentImage = { image: string; note: string };
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* The model-facing rendition of one image under the bound its message was
|
|
439
|
+
* stamped with. Smaller images keep their original bytes. The file in the
|
|
440
|
+
* sandbox and in storage stays at full resolution.
|
|
441
|
+
*/
|
|
442
|
+
async function boundedAttachmentImage(
|
|
443
|
+
attachment: ModelAttachmentContent,
|
|
444
|
+
bound: number,
|
|
445
|
+
): Promise<BoundedAttachmentImage> {
|
|
446
|
+
const original = { image: attachment.dataUrl, note: "" };
|
|
447
|
+
const comma = attachment.dataUrl.indexOf(",");
|
|
448
|
+
if (comma < 0) return original;
|
|
449
|
+
const data = attachment.dataUrl.slice(comma + 1);
|
|
450
|
+
let sizer = boundedImageSizers.get(bound);
|
|
451
|
+
if (!sizer) {
|
|
452
|
+
sizer = createModelImageSizer(bound, 10 * 1024 * 1024, { jpegQuality: 85 });
|
|
453
|
+
boundedImageSizers.set(bound, sizer);
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
const sized = await sizer(data, attachment.contentType);
|
|
457
|
+
if (sized.data === data && sized.mediaType === attachment.contentType) return original;
|
|
458
|
+
return {
|
|
459
|
+
image: `data:${sized.mediaType};base64,${sized.data}`,
|
|
460
|
+
note:
|
|
461
|
+
` Shown to you downscaled to ${sized.width}x${sized.height} from ` +
|
|
462
|
+
`${sized.originalWidth}x${sized.originalHeight}; the file itself is full resolution.`,
|
|
463
|
+
};
|
|
464
|
+
} catch (error) {
|
|
465
|
+
// Decoding is deterministic: an image that cannot be resized here keeps
|
|
466
|
+
// the original projection on every turn rather than being dropped.
|
|
467
|
+
console.error("model attachment image could not be resized; sending original", {
|
|
468
|
+
fileId: attachment.fileId,
|
|
469
|
+
errorType: safeErrorType(error),
|
|
470
|
+
});
|
|
471
|
+
return original;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
395
475
|
/**
|
|
396
476
|
* Build one turn-scoped durable-attachment projector. Resolve active refs under
|
|
397
477
|
* current file authority and memoize metadata/bytes for retries and compaction.
|
|
398
478
|
* A turn boundary never changes an authorized attachment's representation.
|
|
479
|
+
* Images of a message stamped with an image bound are shown downscaled to fit
|
|
480
|
+
* it; unstamped (older) messages keep their full-resolution projection.
|
|
399
481
|
*/
|
|
400
482
|
export function createModelHistoryAttachmentProjector(
|
|
401
483
|
policy: ModelAttachmentInputPolicy,
|
|
@@ -407,6 +489,7 @@ export function createModelHistoryAttachmentProjector(
|
|
|
407
489
|
const fileById = new Map<string, FileAsset>();
|
|
408
490
|
const resolvedFileIds = new Set<string>();
|
|
409
491
|
const retainedByteSizes = new Map<string, number>();
|
|
492
|
+
const boundedById = new Map<string, BoundedAttachmentImage>();
|
|
410
493
|
|
|
411
494
|
return async (items, options = {}) => {
|
|
412
495
|
for (const file of options.inlineFiles ?? []) {
|
|
@@ -475,25 +558,44 @@ export function createModelHistoryAttachmentProjector(
|
|
|
475
558
|
return Boolean(descriptor && descriptor.kind === "image" && policy.supportsImageInput);
|
|
476
559
|
});
|
|
477
560
|
for (const file of readable) attemptedContentIds.add(file.id);
|
|
561
|
+
const readFailures = new Map<string, ModelAttachmentReadFailure>();
|
|
478
562
|
const content = await modelAttachmentContentForFiles(readable, readFileBytes, {
|
|
479
563
|
retainAll: true,
|
|
564
|
+
onReadFailure: (fileId, failure) => readFailures.set(fileId, failure),
|
|
480
565
|
});
|
|
481
566
|
const loadedIds = new Set(content.map((entry) => entry.fileId));
|
|
482
|
-
|
|
483
|
-
|
|
567
|
+
const missing = readable.filter(
|
|
568
|
+
(file) =>
|
|
484
569
|
file.status === "ready" &&
|
|
485
570
|
file.sizeBytes <= MAX_INLINE_MODEL_ATTACHMENT_BYTES &&
|
|
486
571
|
/^[a-f0-9]{64}$/i.test(file.sha256 ?? "") &&
|
|
487
|
-
!loadedIds.has(file.id)
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
572
|
+
!loadedIds.has(file.id),
|
|
573
|
+
);
|
|
574
|
+
if (missing.length > 0) {
|
|
575
|
+
// Do not let a retry silently proceed with pixels removed.
|
|
576
|
+
for (const candidate of readable) attemptedContentIds.delete(candidate.id);
|
|
577
|
+
const corrupt = missing.find((file) => readFailures.get(file.id) === "integrity_mismatch");
|
|
578
|
+
if (corrupt) {
|
|
579
|
+
throw new Error(`Retained attachment bytes unavailable or invalid: ${corrupt.id}`);
|
|
492
580
|
}
|
|
581
|
+
throw new RetainedAttachmentUnavailableError(missing.map((file) => file.id));
|
|
493
582
|
}
|
|
494
583
|
for (const attachment of content) contentById.set(attachment.fileId, attachment);
|
|
495
584
|
}
|
|
496
585
|
|
|
586
|
+
for (const [index, refs] of refsByIndex) {
|
|
587
|
+
const item = items[index]!;
|
|
588
|
+
if (item[MODEL_ATTACHMENT_CATALOG_MARKER] === true) continue;
|
|
589
|
+
const bound = attachmentImageBound(item);
|
|
590
|
+
if (bound === undefined) continue;
|
|
591
|
+
for (const ref of refs) {
|
|
592
|
+
const attachment = fileById.has(ref.fileId) ? contentById.get(ref.fileId) : undefined;
|
|
593
|
+
const key = `${bound}:${ref.fileId}`;
|
|
594
|
+
if (!attachment || attachment.kind !== "image" || boundedById.has(key)) continue;
|
|
595
|
+
boundedById.set(key, await boundedAttachmentImage(attachment, bound));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
497
599
|
const projected = [...items];
|
|
498
600
|
for (const [index, refs] of refsByIndex) {
|
|
499
601
|
const original = items[index]!;
|
|
@@ -519,14 +621,20 @@ export function createModelHistoryAttachmentProjector(
|
|
|
519
621
|
resolvedFileIds.has(ref.fileId) &&
|
|
520
622
|
currentFile === undefined;
|
|
521
623
|
const attachment = currentFile ? contentById.get(ref.fileId) : undefined;
|
|
522
|
-
const receipt = {
|
|
523
|
-
type: "input_text",
|
|
524
|
-
text: excluded ? unavailableAttachmentReceiptText(ref) : attachmentReceiptText(ref),
|
|
525
|
-
};
|
|
526
624
|
if (!attachment || attachment.kind !== "image") {
|
|
527
|
-
return [
|
|
625
|
+
return [
|
|
626
|
+
{
|
|
627
|
+
type: "input_text",
|
|
628
|
+
text: excluded ? unavailableAttachmentReceiptText(ref) : attachmentReceiptText(ref),
|
|
629
|
+
},
|
|
630
|
+
];
|
|
528
631
|
}
|
|
529
|
-
|
|
632
|
+
const bound = attachmentImageBound(original);
|
|
633
|
+
const bounded = bound === undefined ? undefined : boundedById.get(`${bound}:${ref.fileId}`);
|
|
634
|
+
return [
|
|
635
|
+
{ type: "input_text", text: attachmentReceiptText(ref) + (bounded?.note ?? "") },
|
|
636
|
+
{ type: "input_image", image: bounded?.image ?? attachment.dataUrl },
|
|
637
|
+
];
|
|
530
638
|
});
|
|
531
639
|
const clone: Record<string, unknown> = {
|
|
532
640
|
...original,
|
|
File without changes
|
|
File without changes
|