@opengeni/worker-bundle 2.0.1 → 2.0.3-canary.36079201993001

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.
Files changed (31) hide show
  1. package/dist/activities/agent-turn/failure-settlement.d.ts +1 -1
  2. package/dist/activities/agent-turn/history.d.ts +2 -0
  3. package/dist/activities/agent-turn/media-artifacts.d.ts +5 -3
  4. package/dist/activities/context-compaction.d.ts +10 -2
  5. package/dist/activities/retained-screenshots.d.ts +7 -2
  6. package/dist/activities/sandbox-lease.d.ts +3 -4
  7. package/dist/{activities-control-QA6OLRQY.js → activities-control-IVTL723D.js} +204 -23
  8. package/dist/activities-control-IVTL723D.js.map +1 -0
  9. package/dist/{activities-turn-YVVKCGAK.js → activities-turn-AJHDYNZU.js} +180 -58
  10. package/dist/activities-turn-AJHDYNZU.js.map +1 -0
  11. package/dist/{chunk-M3NVMF2V.js → chunk-JMO5ZUW3.js} +14 -7
  12. package/dist/{chunk-M3NVMF2V.js.map → chunk-JMO5ZUW3.js.map} +1 -1
  13. package/dist/index.js +7 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/workflow-bundle.js +4 -1
  16. package/package.json +19 -19
  17. package/src/activities/agent-turn/compaction-prep.ts +52 -16
  18. package/src/activities/agent-turn/errors.ts +8 -3
  19. package/src/activities/agent-turn/failure-settlement.ts +40 -8
  20. package/src/activities/agent-turn/history.ts +14 -1
  21. package/src/activities/agent-turn/media-artifacts.ts +2 -1
  22. package/src/activities/agent-turn/sandbox-runtime.ts +25 -9
  23. package/src/activities/agent-turn/stream-attempt.ts +13 -2
  24. package/src/activities/context-compaction.ts +49 -14
  25. package/src/activities/knowledge-indexing.ts +133 -6
  26. package/src/activities/retained-screenshots.ts +82 -11
  27. package/src/activities/sandbox-lease.ts +141 -21
  28. package/src/sandbox-resume.ts +6 -0
  29. package/src/workflows/activities.ts +14 -1
  30. package/dist/activities-control-QA6OLRQY.js.map +0 -1
  31. package/dist/activities-turn-YVVKCGAK.js.map +0 -1
@@ -1,15 +1,21 @@
1
1
  import { configuredStaticUsageLimits } from "@opengeni/config";
2
+ import { documentEmbeddingCostMicros, paidDocumentEmbedding } from "@opengeni/core";
2
3
  import {
4
+ applyCreditDebitAfterUse,
3
5
  appendKnowledgeIndexChunks,
4
6
  claimKnowledgeIndexJobs,
5
7
  completeKnowledgeIndexJob,
6
8
  continueKnowledgeIndexJob,
7
9
  deferKnowledgeIndexJob,
10
+ freezeKnowledgeIndexBillingMode,
8
11
  getBillingBalance,
12
+ guardPaidKnowledgeIndexPublication,
13
+ knowledgeIndexBillingActivationTime,
9
14
  readKnowledgeIndexSource,
10
15
  recordUsageEvent,
11
16
  sumUsageQuantity,
12
17
  withWorkspaceUsageLock,
18
+ waitKnowledgeIndexForFunding,
13
19
  } from "@opengeni/db";
14
20
  import type { DocumentServices } from "@opengeni/documents";
15
21
  import type { ControlActivityServices } from "./types";
@@ -18,11 +24,26 @@ export function createKnowledgeIndexingActivities(
18
24
  services: () => Promise<ControlActivityServices>,
19
25
  resolveDocumentServices?: () => Promise<DocumentServices>,
20
26
  ) {
27
+ // Unpaid modes retain a conservative first-poll cutoff; paid mode uses the
28
+ // operator's explicit timestamp so process restarts cannot change eligibility.
29
+ let activationTime: Promise<Date> | undefined;
21
30
  return {
22
31
  indexKnowledge: async () => {
23
32
  const result = { completed: 0, advanced: 0, deferred: 0, unavailable: 0 };
24
33
  if (!resolveDocumentServices) return result;
25
34
  const { db, settings, observability } = await services();
35
+ let policyActivatedAt: Date;
36
+ if (paidDocumentEmbedding(settings)) {
37
+ if (!settings.documentEmbeddingCreditsActivatedAt)
38
+ throw new Error("paid Knowledge embedding requires an activation cutoff");
39
+ policyActivatedAt = new Date(settings.documentEmbeddingCreditsActivatedAt);
40
+ } else {
41
+ activationTime ??= knowledgeIndexBillingActivationTime(db).catch((error) => {
42
+ activationTime = undefined;
43
+ throw error;
44
+ });
45
+ policyActivatedAt = await activationTime;
46
+ }
26
47
  const { embedder } = await resolveDocumentServices();
27
48
  const { knowledgeIndexChunks } = await import("@opengeni/documents");
28
49
  const claims = await claimKnowledgeIndexJobs(db, {
@@ -37,9 +58,9 @@ export function createKnowledgeIndexingActivities(
37
58
  result.unavailable++;
38
59
  continue;
39
60
  }
40
- // Reuse the existing embedding budget and usage ledger. Each batch's
41
- // checkpoint and charge commit together, so a restart never charges
42
- // twice for an accepted projection. Canonical text is never changed.
61
+ // The checkpoint, usage and post-use debit commit together. A new
62
+ // generation requires funding once; committed batches may finish even
63
+ // if their accumulated cost takes the balance below zero.
43
64
  await withWorkspaceUsageLock(db, source.billingWorkspaceId, async (lockedDb) => {
44
65
  const current = await readKnowledgeIndexSource(lockedDb, claim);
45
66
  if (!current) {
@@ -57,9 +78,43 @@ export function createKnowledgeIndexingActivities(
57
78
  chunks.push(chunk);
58
79
  }
59
80
  if (chunks.length) {
60
- if (settings.billingMode === "stripe" || settings.usageLimitsMode === "managed") {
81
+ const frozenPolicy = await freezeKnowledgeIndexBillingMode(
82
+ lockedDb,
83
+ claim,
84
+ // Deterministic embeddings incur no provider charge; a valid
85
+ // credits-mode config with zero tariff must still index them.
86
+ // OpenAI shadow keeps its price snapshot for internal estimates.
87
+ settings.documentEmbeddingProvider === "openai"
88
+ ? (settings.documentEmbeddingBillingMode ?? "usage_only")
89
+ : "usage_only",
90
+ policyActivatedAt,
91
+ settings.documentEmbeddingRateMicrosPerMillionBytes ?? 0,
92
+ );
93
+ if (frozenPolicy.mode === "awaiting_review") {
94
+ result.deferred++;
95
+ return;
96
+ }
97
+ if (frozenPolicy.mode === "obsolete") {
98
+ result.unavailable++;
99
+ return;
100
+ }
101
+ // An already-priced generation cannot become free when the
102
+ // operator turns off paid embedding. Retain its checkpoint and
103
+ // retry only after paid mode is restored; never call the provider
104
+ // or append a batch that the live policy would not debit.
105
+ if (frozenPolicy.mode === "credits" && !paidDocumentEmbedding(settings)) {
106
+ await deferKnowledgeIndexJob(lockedDb, claim);
107
+ result.deferred++;
108
+ return;
109
+ }
110
+ const paid = frozenPolicy.mode === "credits";
111
+ if (paid && current.nextIndex === 0) {
61
112
  const balance = await getBillingBalance(lockedDb, claim.accountId);
62
- if (balance.balanceMicros <= 0) throw new Error("insufficient OpenGeni credits");
113
+ if (balance.balanceMicros <= 0) {
114
+ await waitKnowledgeIndexForFunding(lockedDb, claim);
115
+ result.deferred++;
116
+ return;
117
+ }
63
118
  }
64
119
  if (settings.usageLimitsMode === "static" || settings.usageLimitsMode === "managed") {
65
120
  const limit =
@@ -75,9 +130,24 @@ export function createKnowledgeIndexingActivities(
75
130
  throw new Error("monthly document indexing limit reached");
76
131
  }
77
132
  }
78
- const vectors = await embedder.embedMany(chunks.map((chunk) => chunk.embeddingInput));
133
+ const inputs = chunks.map((chunk) => chunk.embeddingInput);
134
+ const bytes = inputs.reduce(
135
+ (sum, input) => sum + Buffer.byteLength(input, "utf8"),
136
+ 0,
137
+ );
138
+ const vectors = await embedder.embedMany(inputs);
79
139
  if (vectors.length !== chunks.length)
80
140
  throw new Error("Incomplete Knowledge embeddings");
141
+ // A reviewer may have rejected this revision during the provider
142
+ // call. The DB guard holds its publication row through settlement.
143
+ if (paid) {
144
+ const publication = await guardPaidKnowledgeIndexPublication(lockedDb, claim);
145
+ if (publication !== "published") {
146
+ if (publication === "obsolete") result.unavailable++;
147
+ else result.deferred++;
148
+ return;
149
+ }
150
+ }
81
151
  const appended = await appendKnowledgeIndexChunks(
82
152
  lockedDb,
83
153
  claim,
@@ -98,6 +168,63 @@ export function createKnowledgeIndexingActivities(
98
168
  sourceResourceId: claim.revisionId,
99
169
  idempotencyKey: `knowledge.indexed:${claim.revisionId}:${claim.generation}:${current.nextIndex}`,
100
170
  });
171
+ await recordUsageEvent(lockedDb, {
172
+ accountId: claim.accountId,
173
+ workspaceId: current.billingWorkspaceId,
174
+ eventType: "document.embedding_bytes",
175
+ quantity: bytes,
176
+ unit: "byte",
177
+ sourceResourceType: "knowledge_revision",
178
+ sourceResourceId: claim.revisionId,
179
+ idempotencyKey: `knowledge.embedding_bytes:${claim.revisionId}:${claim.generation}:${current.nextIndex}`,
180
+ });
181
+ if (frozenPolicy.mode === "shadow" && frozenPolicy.rateMicrosPerMillionBytes > 0) {
182
+ const estimate = documentEmbeddingCostMicros(
183
+ {
184
+ ...settings,
185
+ documentEmbeddingRateMicrosPerMillionBytes:
186
+ frozenPolicy.rateMicrosPerMillionBytes,
187
+ },
188
+ bytes,
189
+ );
190
+ if (estimate > 0)
191
+ await recordUsageEvent(lockedDb, {
192
+ accountId: claim.accountId,
193
+ workspaceId: current.billingWorkspaceId,
194
+ eventType: "document.embedding_shadow_estimate",
195
+ quantity: estimate,
196
+ unit: "micro_usd",
197
+ sourceResourceType: "knowledge_revision",
198
+ sourceResourceId: claim.revisionId,
199
+ idempotencyKey: `knowledge.embedding_shadow:${claim.revisionId}:${claim.generation}:${current.nextIndex}`,
200
+ });
201
+ }
202
+ if (paid) {
203
+ const cost = documentEmbeddingCostMicros(
204
+ {
205
+ ...settings,
206
+ documentEmbeddingRateMicrosPerMillionBytes:
207
+ frozenPolicy.rateMicrosPerMillionBytes,
208
+ },
209
+ bytes,
210
+ );
211
+ if (cost > 0)
212
+ await applyCreditDebitAfterUse(lockedDb, {
213
+ accountId: claim.accountId,
214
+ workspaceId: current.billingWorkspaceId,
215
+ type: "document_embedding_debit",
216
+ amountMicros: cost,
217
+ sourceType: "knowledge_revision",
218
+ sourceId: claim.revisionId,
219
+ idempotencyKey: `knowledge.embedding:${claim.revisionId}:${claim.generation}:${current.nextIndex}`,
220
+ metadata: {
221
+ model: claim.model,
222
+ bytes,
223
+ chunks: chunks.length,
224
+ rateMicrosPerMillionBytes: frozenPolicy.rateMicrosPerMillionBytes,
225
+ },
226
+ });
227
+ }
101
228
  }
102
229
  if (more) {
103
230
  await continueKnowledgeIndexJob(lockedDb, claim);
@@ -6,13 +6,16 @@ import {
6
6
  COMPUTER_SCREENSHOT_WORKSPACE_QUOTA_BYTES,
7
7
  RetainedArtifactMetadataSchema,
8
8
  retainedScreenshotReferenceFromFile,
9
+ retainedSessionScreenshotKindFromObjectKey,
9
10
  type RetainedArtifactMetadata,
10
11
  type RetainedArtifactReference,
11
12
  type RetainedOutputUnavailableReason,
13
+ type RetainedSessionScreenshotKind,
12
14
  } from "@opengeni/contracts";
13
15
  import {
14
16
  RetainedScreenshotQuotaExceededError,
15
17
  getRetainedScreenshotArtifact,
18
+ getRetainedScreenshotArtifactForToolCall,
16
19
  isDatabasePersistenceFailure,
17
20
  isSessionEventPersistenceError,
18
21
  prepareRetainedScreenshotArtifact,
@@ -209,14 +212,26 @@ export function toolOutputContainsInlineImage(output: unknown): boolean {
209
212
  return (
210
213
  (record.type === "input_image" &&
211
214
  sdkImageSourceContainsInlineImage(record.image ?? record.image_url ?? record.imageUrl)) ||
212
- (record.type === "image" && decodeInlineMcpImage(record.data, record.mimeType) !== null)
215
+ (record.type === "image" && isDeclaredInlineMcpImage(record))
213
216
  );
214
217
  });
215
218
  }
216
219
  if (!output || typeof output !== "object") return false;
217
220
  const record = output as Record<string, unknown>;
218
221
  if (Array.isArray(record.content)) return toolOutputContainsInlineImage(record.content);
219
- return record.type === "image" && sdkImageSourceContainsInlineImage(record.image);
222
+ return (
223
+ record.type === "image" &&
224
+ (sdkImageSourceContainsInlineImage(record.image) || isDeclaredInlineMcpImage(record))
225
+ );
226
+ }
227
+
228
+ /** Detect image-bearing MCP blocks even when decoding rejects oversized or malformed bytes. */
229
+ function isDeclaredInlineMcpImage(record: Record<string, unknown>): boolean {
230
+ return (
231
+ typeof record.data === "string" &&
232
+ typeof record.mimeType === "string" &&
233
+ record.mimeType.toLowerCase().startsWith("image/")
234
+ );
220
235
  }
221
236
 
222
237
  function decodeInlineMcpImage(
@@ -520,9 +535,14 @@ export function retainedScreenshotIdentity(input: {
520
535
  attemptId: string;
521
536
  toolCallId: string;
522
537
  toolOutputId: string;
538
+ kind?: RetainedSessionScreenshotKind | undefined;
523
539
  }): { artifactId: string; settlementKey: string } {
524
540
  const settlementKey = createHash("sha256")
525
- .update("opengeni:computer-screenshot:v1\0")
541
+ .update(
542
+ input.kind === "browser_screenshot"
543
+ ? "opengeni:browser-screenshot:v1\0"
544
+ : "opengeni:computer-screenshot:v1\0",
545
+ )
526
546
  .update(input.sessionId)
527
547
  .update("\0")
528
548
  .update(input.turnId)
@@ -551,12 +571,13 @@ export function unavailableRetainedSessionImage(input: {
551
571
  toolCallId: string;
552
572
  toolOutputId: string;
553
573
  reason: RetainedOutputUnavailableReason;
574
+ kind?: RetainedSessionScreenshotKind | undefined;
554
575
  }): RetainedArtifactMetadata {
555
576
  const identity = retainedScreenshotIdentity(input);
556
577
  return unavailable(identity.artifactId, input.reason);
557
578
  }
558
579
 
559
- export async function retainComputerScreenshot(input: {
580
+ export async function retainSessionScreenshot(input: {
560
581
  db: Database;
561
582
  objectStorage: ObjectStorage | null;
562
583
  accountId: string;
@@ -565,6 +586,7 @@ export async function retainComputerScreenshot(input: {
565
586
  turnId: string;
566
587
  attemptId: string;
567
588
  output: TypedScreenshotToolOutput;
589
+ kind?: RetainedSessionScreenshotKind | undefined;
568
590
  now?: Date;
569
591
  retentionMs?: number;
570
592
  workspaceQuotaBytes?: number;
@@ -575,6 +597,7 @@ export async function retainComputerScreenshot(input: {
575
597
  attemptId: input.attemptId,
576
598
  toolCallId: input.output.callId,
577
599
  toolOutputId: input.output.toolOutputId,
600
+ kind: input.kind,
578
601
  });
579
602
  let screenshot: ValidatedSessionImage;
580
603
  try {
@@ -594,7 +617,8 @@ export async function retainComputerScreenshot(input: {
594
617
  const retentionExpiresAt = new Date(
595
618
  now.getTime() + (input.retentionMs ?? COMPUTER_SCREENSHOT_RETENTION_MS),
596
619
  );
597
- const objectKey = `workspaces/${input.workspaceId}/files/${identity.artifactId}/retained/session-image.${screenshot.extension}`;
620
+ const objectName = input.kind === "browser_screenshot" ? "browser-screenshot" : "session-image";
621
+ const objectKey = `workspaces/${input.workspaceId}/files/${identity.artifactId}/retained/${objectName}.${screenshot.extension}`;
598
622
  let prepared: RetainedScreenshotArtifact;
599
623
  try {
600
624
  prepared = (
@@ -697,6 +721,9 @@ export async function retainComputerScreenshot(input: {
697
721
  }
698
722
  }
699
723
 
724
+ /** Existing computer callers keep their stable entry point. */
725
+ export const retainComputerScreenshot = retainSessionScreenshot;
726
+
700
727
  /** Replace only new screenshot bytes with a compact receipt before first persistence. */
701
728
  export function compactRetainedScreenshotHistory(
702
729
  history: Array<Record<string, unknown>>,
@@ -874,6 +901,26 @@ async function materializeRetainedScreenshotHistoryWithCache(
874
901
  cache: Map<string, string>,
875
902
  ): Promise<Array<Record<string, unknown>>> {
876
903
  const now = input.now ?? new Date();
904
+ const receiptForMarker = async (
905
+ marker: unknown,
906
+ callId: string | null,
907
+ ): Promise<RetainedArtifactMetadata | null> => {
908
+ if (!isRetainedImageMarker(marker)) return null;
909
+ const receipt = retainedReceipt(marker.artifact);
910
+ if (receipt) return receipt;
911
+ // Older canonical history could truncate the artifact UUID and reason as
912
+ // ordinary text. Recover only from this exact session and an unambiguous
913
+ // tool call; otherwise fail before constructing a malformed model image.
914
+ if (!callId) throw new Error("Retained screenshot receipt has no tool call identity");
915
+ const artifact = await getRetainedScreenshotArtifactForToolCall(
916
+ input.db,
917
+ input.workspaceId,
918
+ input.sessionId,
919
+ callId,
920
+ );
921
+ if (!artifact) throw new Error("Retained screenshot receipt cannot be recovered");
922
+ return reference(artifact);
923
+ };
877
924
  const dataUrlForReceipt = async (receipt: RetainedArtifactMetadata): Promise<string> => {
878
925
  let dataUrl = cache.get(receipt.artifactId);
879
926
  if (!dataUrl) {
@@ -921,8 +968,12 @@ async function materializeRetainedScreenshotHistoryWithCache(
921
968
  }
922
969
  return dataUrl;
923
970
  };
924
- const materializeEntry = async (entry: unknown): Promise<unknown> => {
925
- const receipt = retainedReceiptFromImageContent(entry);
971
+ const materializeEntry = async (entry: unknown, callId: string | null): Promise<unknown> => {
972
+ const image =
973
+ entry && typeof entry === "object" && !Array.isArray(entry)
974
+ ? (entry as Record<string, unknown>).image
975
+ : null;
976
+ const receipt = await receiptForMarker(image, callId);
926
977
  if (!receipt) return entry;
927
978
  const dataUrl = await dataUrlForReceipt(receipt);
928
979
  return { ...(entry as Record<string, unknown>), image: dataUrl };
@@ -930,7 +981,8 @@ async function materializeRetainedScreenshotHistoryWithCache(
930
981
 
931
982
  const materialized: Array<Record<string, unknown>> = [];
932
983
  for (const item of input.history) {
933
- const directReceipt = retainedReceiptFromDirectOutput(item.output);
984
+ const callId = historyCallId(item);
985
+ const directReceipt = await receiptForMarker(item.output, callId);
934
986
  if (directReceipt) {
935
987
  materialized.push({
936
988
  ...item,
@@ -954,7 +1006,7 @@ async function materializeRetainedScreenshotHistoryWithCache(
954
1006
  let changed = false;
955
1007
  const output: unknown[] = [];
956
1008
  for (const entry of outputEntries) {
957
- const next = await materializeEntry(entry);
1009
+ const next = await materializeEntry(entry, callId);
958
1010
  changed ||= next !== entry;
959
1011
  output.push(next);
960
1012
  }
@@ -1059,12 +1111,15 @@ function reference(artifact: RetainedScreenshotArtifact): RetainedArtifactRefere
1059
1111
  if (!artifact.sessionId) {
1060
1112
  throw new Error(`Ready retained screenshot is detached: ${artifact.artifactId}`);
1061
1113
  }
1114
+ const kind = retainedSessionScreenshotKindFromObjectKey(artifact.file.objectKey);
1115
+ if (!kind) throw new Error(`Ready retained screenshot has unknown key: ${artifact.artifactId}`);
1062
1116
  const value = retainedScreenshotReferenceFromFile({
1063
1117
  ...artifact.file,
1064
1118
  sessionId: artifact.sessionId,
1065
1119
  width: artifact.width,
1066
1120
  height: artifact.height,
1067
1121
  expiresAt: artifact.retentionExpiresAt.toISOString(),
1122
+ kind,
1068
1123
  });
1069
1124
  if (!value)
1070
1125
  throw new Error(`Ready retained screenshot receipt is invalid: ${artifact.artifactId}`);
@@ -1110,6 +1165,18 @@ function retainedReceiptFromImageContent(entry: unknown): RetainedArtifactMetada
1110
1165
  return retainedReceipt(marker.artifact);
1111
1166
  }
1112
1167
 
1168
+ function isRetainedImageMarker(value: unknown): value is {
1169
+ type: "retained_artifact";
1170
+ artifact: unknown;
1171
+ } {
1172
+ return Boolean(
1173
+ value &&
1174
+ typeof value === "object" &&
1175
+ !Array.isArray(value) &&
1176
+ (value as Record<string, unknown>).type === RETAINED_IMAGE_MARKER,
1177
+ );
1178
+ }
1179
+
1113
1180
  function retainedReceiptFromDirectOutput(output: unknown): RetainedArtifactMetadata | null {
1114
1181
  if (!output || typeof output !== "object" || Array.isArray(output)) return null;
1115
1182
  const marker = output as Record<string, unknown>;
@@ -1120,7 +1187,11 @@ function retainedReceiptFromDirectOutput(output: unknown): RetainedArtifactMetad
1120
1187
  function retainedReceipt(value: unknown): RetainedArtifactMetadata | null {
1121
1188
  const parsed = RetainedArtifactMetadataSchema.safeParse(value);
1122
1189
  if (!parsed.success) return null;
1123
- return parsed.data.available && parsed.data.kind !== "computer_screenshot" ? null : parsed.data;
1190
+ return parsed.data.available &&
1191
+ parsed.data.kind !== "computer_screenshot" &&
1192
+ parsed.data.kind !== "browser_screenshot"
1193
+ ? null
1194
+ : parsed.data;
1124
1195
  }
1125
1196
 
1126
1197
  function isInlineImageContent(entry: unknown): boolean {
@@ -1130,7 +1201,7 @@ function isInlineImageContent(entry: unknown): boolean {
1130
1201
  (record.type === "input_image" &&
1131
1202
  typeof record.image === "string" &&
1132
1203
  record.image.startsWith("data:image/")) ||
1133
- (record.type === "image" && decodeInlineMcpImage(record.data, record.mimeType) !== null)
1204
+ (record.type === "image" && isDeclaredInlineMcpImage(record))
1134
1205
  );
1135
1206
  }
1136
1207