@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/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-3PJDM3QQ.js")).createControlActivities(dependencies) : (await import("./activities-turn-2ILW6PRK.js")).createTurnActivities(dependencies);
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.0.1-canary.38872461489001",
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.0.1-canary.38872461489001",
59
- "@opengeni/capabilities": "3.0.1-canary.38872461489001",
60
- "@opengeni/codemode": "3.0.1-canary.38872461489001",
61
- "@opengeni/codex": "3.0.1-canary.38872461489001",
62
- "@opengeni/config": "3.0.1-canary.38872461489001",
63
- "@opengeni/contracts": "3.0.1-canary.38872461489001",
64
- "@opengeni/core": "3.0.1-canary.38872461489001",
65
- "@opengeni/db": "3.0.1-canary.38872461489001",
66
- "@opengeni/documents": "3.0.1-canary.38872461489001",
67
- "@opengeni/events": "3.0.1-canary.38872461489001",
68
- "@opengeni/github": "3.0.1-canary.38872461489001",
69
- "@opengeni/interaction": "3.0.1-canary.38872461489001",
70
- "@opengeni/jev": "3.0.1-canary.38872461489001",
71
- "@opengeni/network": "3.0.1-canary.38872461489001",
72
- "@opengeni/observability": "3.0.1-canary.38872461489001",
73
- "@opengeni/runtime": "3.0.1-canary.38872461489001",
74
- "@opengeni/sdk": "3.0.1-canary.38872461489001",
75
- "@opengeni/storage": "3.0.1-canary.38872461489001",
76
- "@opengeni/subscriptions": "3.0.1-canary.38872461489001",
77
- "@opengeni/tool-gateway": "3.0.1-canary.38872461489001",
78
- "@opengeni/vaults": "3.0.1-canary.38872461489001",
79
- "@opengeni/vaults-onepassword": "3.0.1-canary.38872461489001",
80
- "@opengeni/xai-subscription": "3.0.1-canary.38872461489001",
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 { RetainedAttachmentTransportLimitError } from "../run-input";
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: { retainAll?: boolean } = {},
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
- for (const file of readable) {
483
- if (
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
- // Do not let a retry silently proceed with pixels removed.
490
- for (const candidate of readable) attemptedContentIds.delete(candidate.id);
491
- throw new Error(`Retained attachment bytes unavailable or invalid: ${file.id}`);
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 [receipt];
625
+ return [
626
+ {
627
+ type: "input_text",
628
+ text: excluded ? unavailableAttachmentReceiptText(ref) : attachmentReceiptText(ref),
629
+ },
630
+ ];
528
631
  }
529
- return [receipt, { type: "input_image", image: attachment.dataUrl }];
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,