@opengeni/worker-bundle 2.0.1 → 2.0.3-canary.36199476632001
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/agent-build.d.ts +2 -0
- package/dist/activities/agent-turn/code-search.d.ts +60 -0
- package/dist/activities/agent-turn/codex-capacity.d.ts +13 -0
- package/dist/activities/agent-turn/errors.d.ts +12 -1
- package/dist/activities/agent-turn/failure-settlement.d.ts +1 -1
- package/dist/activities/agent-turn/governance-model.d.ts +2 -0
- package/dist/activities/agent-turn/history.d.ts +2 -0
- package/dist/activities/agent-turn/media-artifacts.d.ts +5 -3
- package/dist/activities/agent-turn/session-title.d.ts +32 -2
- package/dist/activities/agent-turn/tool-environment.d.ts +4 -0
- package/dist/activities/context-compaction.d.ts +10 -2
- package/dist/activities/knowledge-indexing.d.ts +19 -0
- package/dist/activities/retained-screenshots.d.ts +7 -2
- package/dist/activities/sandbox-lease.d.ts +3 -4
- package/dist/{activities-control-QA6OLRQY.js → activities-control-I72CRMAE.js} +283 -35
- package/dist/activities-control-I72CRMAE.js.map +1 -0
- package/dist/{activities-turn-YVVKCGAK.js → activities-turn-CSDKK4N3.js} +531 -77
- package/dist/activities-turn-CSDKK4N3.js.map +1 -0
- package/dist/artifact-outbox-entry.js +3 -0
- package/dist/artifact-outbox-entry.js.map +1 -1
- package/dist/{chunk-M3NVMF2V.js → chunk-7M6G2SCF.js} +172 -17
- package/dist/chunk-7M6G2SCF.js.map +1 -0
- package/dist/{chunk-I7HKKMUJ.js → chunk-DSPP6CZL.js} +48 -3
- package/dist/chunk-DSPP6CZL.js.map +1 -0
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/dist/observability-metrics.d.ts +11 -1
- package/dist/sandbox-resume.d.ts +46 -0
- package/dist/workflow-bundle.js +80 -2
- package/dist/workflows/session.d.ts +30 -0
- package/package.json +20 -19
- package/src/activities/agent-turn/agent-build.ts +4 -0
- package/src/activities/agent-turn/code-search.ts +275 -0
- package/src/activities/agent-turn/codex-capacity.ts +37 -2
- package/src/activities/agent-turn/compaction-prep.ts +52 -16
- package/src/activities/agent-turn/errors.ts +67 -4
- package/src/activities/agent-turn/failure-settlement.ts +40 -8
- package/src/activities/agent-turn/governance-model.ts +17 -1
- package/src/activities/agent-turn/history.ts +14 -1
- package/src/activities/agent-turn/media-artifacts.ts +2 -1
- package/src/activities/agent-turn/run.ts +4 -0
- package/src/activities/agent-turn/sandbox-establish.ts +12 -1
- package/src/activities/agent-turn/sandbox-runtime.ts +25 -9
- package/src/activities/agent-turn/session-title.ts +73 -3
- package/src/activities/agent-turn/stream-attempt.ts +27 -15
- package/src/activities/agent-turn/tool-environment.ts +65 -0
- package/src/activities/context-compaction.ts +49 -14
- package/src/activities/knowledge-indexing.ts +240 -15
- package/src/activities/retained-screenshots.ts +82 -11
- package/src/activities/sandbox-lease.ts +141 -21
- package/src/activities/scheduled-tasks.ts +18 -2
- package/src/activity-services.ts +16 -4
- package/src/editable-artifact-hint-broker.ts +3 -0
- package/src/index.ts +2 -2
- package/src/observability-metrics.ts +70 -1
- package/src/personal-github-git-credentials.ts +2 -0
- package/src/sandbox-resume.ts +211 -5
- package/src/workflows/activities.ts +14 -1
- package/src/workflows/session.ts +102 -6
- package/dist/activities-control-QA6OLRQY.js.map +0 -1
- package/dist/activities-turn-YVVKCGAK.js.map +0 -1
- package/dist/chunk-I7HKKMUJ.js.map +0 -1
- package/dist/chunk-M3NVMF2V.js.map +0 -1
|
@@ -1,28 +1,138 @@
|
|
|
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";
|
|
16
22
|
|
|
23
|
+
/** The configured monthly indexed-chunk limit, not a provider failure. */
|
|
24
|
+
export class KnowledgeIndexUsageLimitError extends Error {
|
|
25
|
+
constructor() {
|
|
26
|
+
super("monthly document indexing limit reached");
|
|
27
|
+
this.name = "KnowledgeIndexUsageLimitError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type KnowledgeIndexFailureStage = "embedding" | "processing";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Content-free classification for a deferred Knowledge index batch. Only
|
|
35
|
+
* protocol constants, an HTTP status, and a PostgreSQL SQLSTATE are retained;
|
|
36
|
+
* provider messages, bodies, SQL, and identifiers never leave the process.
|
|
37
|
+
* Outside the embedding call, only a PostgreSQL error in the cause chain is
|
|
38
|
+
* attributed to the database; any other failure stays a worker failure.
|
|
39
|
+
*/
|
|
40
|
+
export function knowledgeIndexFailureDiagnostic(
|
|
41
|
+
stage: KnowledgeIndexFailureStage,
|
|
42
|
+
error: unknown,
|
|
43
|
+
): {
|
|
44
|
+
errorClass: "KnowledgeIndexOperationError";
|
|
45
|
+
errorCode:
|
|
46
|
+
| "knowledge_index_usage_limit_reached"
|
|
47
|
+
| "knowledge_index_embedding_failed"
|
|
48
|
+
| "knowledge_index_persistence_failed"
|
|
49
|
+
| "knowledge_index_failed";
|
|
50
|
+
origin: "worker" | "db";
|
|
51
|
+
status?: number;
|
|
52
|
+
sqlState?: string;
|
|
53
|
+
} {
|
|
54
|
+
if (error instanceof KnowledgeIndexUsageLimitError) {
|
|
55
|
+
return {
|
|
56
|
+
errorClass: "KnowledgeIndexOperationError",
|
|
57
|
+
errorCode: "knowledge_index_usage_limit_reached",
|
|
58
|
+
origin: "worker",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (stage === "embedding") {
|
|
62
|
+
const status = ownValue(error, "status");
|
|
63
|
+
return {
|
|
64
|
+
errorClass: "KnowledgeIndexOperationError",
|
|
65
|
+
errorCode: "knowledge_index_embedding_failed",
|
|
66
|
+
origin: "worker",
|
|
67
|
+
...(typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599
|
|
68
|
+
? { status }
|
|
69
|
+
: {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const postgres = postgresErrorState(error);
|
|
73
|
+
if (!postgres) {
|
|
74
|
+
return {
|
|
75
|
+
errorClass: "KnowledgeIndexOperationError",
|
|
76
|
+
errorCode: "knowledge_index_failed",
|
|
77
|
+
origin: "worker",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
errorClass: "KnowledgeIndexOperationError",
|
|
82
|
+
errorCode: "knowledge_index_persistence_failed",
|
|
83
|
+
origin: "db",
|
|
84
|
+
...(postgres.sqlState ? { sqlState: postgres.sqlState } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ownValue(value: unknown, key: string): unknown {
|
|
89
|
+
try {
|
|
90
|
+
if (!value || typeof value !== "object") return undefined;
|
|
91
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
92
|
+
return descriptor && "value" in descriptor ? descriptor.value : undefined;
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The nearest PostgreSQL error in a short cause chain, with its SQLSTATE
|
|
99
|
+
* when that is a well-formed five-character code. */
|
|
100
|
+
function postgresErrorState(error: unknown): { sqlState?: string } | undefined {
|
|
101
|
+
let current = error;
|
|
102
|
+
for (let depth = 0; depth < 4 && current && typeof current === "object"; depth += 1) {
|
|
103
|
+
if (ownValue(current, "name") === "PostgresError") {
|
|
104
|
+
const code = ownValue(current, "code");
|
|
105
|
+
return typeof code === "string" && /^[0-9A-Z]{5}$/.test(code) ? { sqlState: code } : {};
|
|
106
|
+
}
|
|
107
|
+
current = ownValue(current, "cause");
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
17
112
|
export function createKnowledgeIndexingActivities(
|
|
18
113
|
services: () => Promise<ControlActivityServices>,
|
|
19
114
|
resolveDocumentServices?: () => Promise<DocumentServices>,
|
|
20
115
|
) {
|
|
116
|
+
// Unpaid modes retain a conservative first-poll cutoff; paid mode uses the
|
|
117
|
+
// operator's explicit timestamp so process restarts cannot change eligibility.
|
|
118
|
+
let activationTime: Promise<Date> | undefined;
|
|
21
119
|
return {
|
|
22
120
|
indexKnowledge: async () => {
|
|
23
121
|
const result = { completed: 0, advanced: 0, deferred: 0, unavailable: 0 };
|
|
24
122
|
if (!resolveDocumentServices) return result;
|
|
25
123
|
const { db, settings, observability } = await services();
|
|
124
|
+
let policyActivatedAt: Date;
|
|
125
|
+
if (paidDocumentEmbedding(settings)) {
|
|
126
|
+
if (!settings.documentEmbeddingCreditsActivatedAt)
|
|
127
|
+
throw new Error("paid Knowledge embedding requires an activation cutoff");
|
|
128
|
+
policyActivatedAt = new Date(settings.documentEmbeddingCreditsActivatedAt);
|
|
129
|
+
} else {
|
|
130
|
+
activationTime ??= knowledgeIndexBillingActivationTime(db).catch((error) => {
|
|
131
|
+
activationTime = undefined;
|
|
132
|
+
throw error;
|
|
133
|
+
});
|
|
134
|
+
policyActivatedAt = await activationTime;
|
|
135
|
+
}
|
|
26
136
|
const { embedder } = await resolveDocumentServices();
|
|
27
137
|
const { knowledgeIndexChunks } = await import("@opengeni/documents");
|
|
28
138
|
const claims = await claimKnowledgeIndexJobs(db, {
|
|
@@ -31,15 +141,16 @@ export function createKnowledgeIndexingActivities(
|
|
|
31
141
|
limit: 2,
|
|
32
142
|
});
|
|
33
143
|
for (const claim of claims) {
|
|
144
|
+
let stage: KnowledgeIndexFailureStage = "processing";
|
|
34
145
|
try {
|
|
35
146
|
const source = await readKnowledgeIndexSource(db, claim);
|
|
36
147
|
if (!source) {
|
|
37
148
|
result.unavailable++;
|
|
38
149
|
continue;
|
|
39
150
|
}
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
151
|
+
// The checkpoint, usage and post-use debit commit together. A new
|
|
152
|
+
// generation requires funding once; committed batches may finish even
|
|
153
|
+
// if their accumulated cost takes the balance below zero.
|
|
43
154
|
await withWorkspaceUsageLock(db, source.billingWorkspaceId, async (lockedDb) => {
|
|
44
155
|
const current = await readKnowledgeIndexSource(lockedDb, claim);
|
|
45
156
|
if (!current) {
|
|
@@ -57,9 +168,43 @@ export function createKnowledgeIndexingActivities(
|
|
|
57
168
|
chunks.push(chunk);
|
|
58
169
|
}
|
|
59
170
|
if (chunks.length) {
|
|
60
|
-
|
|
171
|
+
const frozenPolicy = await freezeKnowledgeIndexBillingMode(
|
|
172
|
+
lockedDb,
|
|
173
|
+
claim,
|
|
174
|
+
// Deterministic embeddings incur no provider charge; a valid
|
|
175
|
+
// credits-mode config with zero tariff must still index them.
|
|
176
|
+
// OpenAI shadow keeps its price snapshot for internal estimates.
|
|
177
|
+
settings.documentEmbeddingProvider === "openai"
|
|
178
|
+
? (settings.documentEmbeddingBillingMode ?? "usage_only")
|
|
179
|
+
: "usage_only",
|
|
180
|
+
policyActivatedAt,
|
|
181
|
+
settings.documentEmbeddingRateMicrosPerMillionBytes ?? 0,
|
|
182
|
+
);
|
|
183
|
+
if (frozenPolicy.mode === "awaiting_review") {
|
|
184
|
+
result.deferred++;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (frozenPolicy.mode === "obsolete") {
|
|
188
|
+
result.unavailable++;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// An already-priced generation cannot become free when the
|
|
192
|
+
// operator turns off paid embedding. Retain its checkpoint and
|
|
193
|
+
// retry only after paid mode is restored; never call the provider
|
|
194
|
+
// or append a batch that the live policy would not debit.
|
|
195
|
+
if (frozenPolicy.mode === "credits" && !paidDocumentEmbedding(settings)) {
|
|
196
|
+
await deferKnowledgeIndexJob(lockedDb, claim);
|
|
197
|
+
result.deferred++;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const paid = frozenPolicy.mode === "credits";
|
|
201
|
+
if (paid && current.nextIndex === 0) {
|
|
61
202
|
const balance = await getBillingBalance(lockedDb, claim.accountId);
|
|
62
|
-
if (balance.balanceMicros <= 0)
|
|
203
|
+
if (balance.balanceMicros <= 0) {
|
|
204
|
+
await waitKnowledgeIndexForFunding(lockedDb, claim);
|
|
205
|
+
result.deferred++;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
63
208
|
}
|
|
64
209
|
if (settings.usageLimitsMode === "static" || settings.usageLimitsMode === "managed") {
|
|
65
210
|
const limit =
|
|
@@ -71,13 +216,29 @@ export function createKnowledgeIndexingActivities(
|
|
|
71
216
|
eventType: "document.indexed",
|
|
72
217
|
since: new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)),
|
|
73
218
|
});
|
|
74
|
-
if (used + chunks.length > limit)
|
|
75
|
-
throw new Error("monthly document indexing limit reached");
|
|
219
|
+
if (used + chunks.length > limit) throw new KnowledgeIndexUsageLimitError();
|
|
76
220
|
}
|
|
77
221
|
}
|
|
78
|
-
const
|
|
222
|
+
const inputs = chunks.map((chunk) => chunk.embeddingInput);
|
|
223
|
+
const bytes = inputs.reduce(
|
|
224
|
+
(sum, input) => sum + Buffer.byteLength(input, "utf8"),
|
|
225
|
+
0,
|
|
226
|
+
);
|
|
227
|
+
stage = "embedding";
|
|
228
|
+
const vectors = await embedder.embedMany(inputs);
|
|
79
229
|
if (vectors.length !== chunks.length)
|
|
80
230
|
throw new Error("Incomplete Knowledge embeddings");
|
|
231
|
+
stage = "processing";
|
|
232
|
+
// A reviewer may have rejected this revision during the provider
|
|
233
|
+
// call. The DB guard holds its publication row through settlement.
|
|
234
|
+
if (paid) {
|
|
235
|
+
const publication = await guardPaidKnowledgeIndexPublication(lockedDb, claim);
|
|
236
|
+
if (publication !== "published") {
|
|
237
|
+
if (publication === "obsolete") result.unavailable++;
|
|
238
|
+
else result.deferred++;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
81
242
|
const appended = await appendKnowledgeIndexChunks(
|
|
82
243
|
lockedDb,
|
|
83
244
|
claim,
|
|
@@ -98,6 +259,63 @@ export function createKnowledgeIndexingActivities(
|
|
|
98
259
|
sourceResourceId: claim.revisionId,
|
|
99
260
|
idempotencyKey: `knowledge.indexed:${claim.revisionId}:${claim.generation}:${current.nextIndex}`,
|
|
100
261
|
});
|
|
262
|
+
await recordUsageEvent(lockedDb, {
|
|
263
|
+
accountId: claim.accountId,
|
|
264
|
+
workspaceId: current.billingWorkspaceId,
|
|
265
|
+
eventType: "document.embedding_bytes",
|
|
266
|
+
quantity: bytes,
|
|
267
|
+
unit: "byte",
|
|
268
|
+
sourceResourceType: "knowledge_revision",
|
|
269
|
+
sourceResourceId: claim.revisionId,
|
|
270
|
+
idempotencyKey: `knowledge.embedding_bytes:${claim.revisionId}:${claim.generation}:${current.nextIndex}`,
|
|
271
|
+
});
|
|
272
|
+
if (frozenPolicy.mode === "shadow" && frozenPolicy.rateMicrosPerMillionBytes > 0) {
|
|
273
|
+
const estimate = documentEmbeddingCostMicros(
|
|
274
|
+
{
|
|
275
|
+
...settings,
|
|
276
|
+
documentEmbeddingRateMicrosPerMillionBytes:
|
|
277
|
+
frozenPolicy.rateMicrosPerMillionBytes,
|
|
278
|
+
},
|
|
279
|
+
bytes,
|
|
280
|
+
);
|
|
281
|
+
if (estimate > 0)
|
|
282
|
+
await recordUsageEvent(lockedDb, {
|
|
283
|
+
accountId: claim.accountId,
|
|
284
|
+
workspaceId: current.billingWorkspaceId,
|
|
285
|
+
eventType: "document.embedding_shadow_estimate",
|
|
286
|
+
quantity: estimate,
|
|
287
|
+
unit: "micro_usd",
|
|
288
|
+
sourceResourceType: "knowledge_revision",
|
|
289
|
+
sourceResourceId: claim.revisionId,
|
|
290
|
+
idempotencyKey: `knowledge.embedding_shadow:${claim.revisionId}:${claim.generation}:${current.nextIndex}`,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (paid) {
|
|
294
|
+
const cost = documentEmbeddingCostMicros(
|
|
295
|
+
{
|
|
296
|
+
...settings,
|
|
297
|
+
documentEmbeddingRateMicrosPerMillionBytes:
|
|
298
|
+
frozenPolicy.rateMicrosPerMillionBytes,
|
|
299
|
+
},
|
|
300
|
+
bytes,
|
|
301
|
+
);
|
|
302
|
+
if (cost > 0)
|
|
303
|
+
await applyCreditDebitAfterUse(lockedDb, {
|
|
304
|
+
accountId: claim.accountId,
|
|
305
|
+
workspaceId: current.billingWorkspaceId,
|
|
306
|
+
type: "document_embedding_debit",
|
|
307
|
+
amountMicros: cost,
|
|
308
|
+
sourceType: "knowledge_revision",
|
|
309
|
+
sourceId: claim.revisionId,
|
|
310
|
+
idempotencyKey: `knowledge.embedding:${claim.revisionId}:${claim.generation}:${current.nextIndex}`,
|
|
311
|
+
metadata: {
|
|
312
|
+
model: claim.model,
|
|
313
|
+
bytes,
|
|
314
|
+
chunks: chunks.length,
|
|
315
|
+
rateMicrosPerMillionBytes: frozenPolicy.rateMicrosPerMillionBytes,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
}
|
|
101
319
|
}
|
|
102
320
|
if (more) {
|
|
103
321
|
await continueKnowledgeIndexJob(lockedDb, claim);
|
|
@@ -112,16 +330,23 @@ export function createKnowledgeIndexingActivities(
|
|
|
112
330
|
else result.unavailable++;
|
|
113
331
|
}
|
|
114
332
|
});
|
|
115
|
-
} catch {
|
|
333
|
+
} catch (error) {
|
|
116
334
|
// Provider failures retain the last completed projection. The durable
|
|
117
335
|
// queue owns retry/backoff; do not retry an entire activity implicitly.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
observability.warn(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
336
|
+
// The stored reason stays the SQL lifecycle's fixed code; the log
|
|
337
|
+
// carries the content-free class/code of the actual cause.
|
|
338
|
+
observability.warn(
|
|
339
|
+
"Knowledge indexing batch deferred",
|
|
340
|
+
knowledgeIndexFailureDiagnostic(stage, error),
|
|
341
|
+
);
|
|
342
|
+
await deferKnowledgeIndexJob(db, claim).catch((deferError: unknown) => {
|
|
343
|
+
const deferDiagnostic = knowledgeIndexFailureDiagnostic("processing", deferError);
|
|
344
|
+
observability.warn("Knowledge indexing batch deferral failed", {
|
|
345
|
+
...deferDiagnostic,
|
|
346
|
+
errorCode: "knowledge_index_defer_failed",
|
|
347
|
+
});
|
|
124
348
|
});
|
|
349
|
+
result.deferred++;
|
|
125
350
|
}
|
|
126
351
|
}
|
|
127
352
|
return result;
|
|
@@ -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" &&
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 &&
|
|
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" &&
|
|
1204
|
+
(record.type === "image" && isDeclaredInlineMcpImage(record))
|
|
1134
1205
|
);
|
|
1135
1206
|
}
|
|
1136
1207
|
|