@opengeni/worker-bundle 2.0.1 → 2.0.3
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/failure-settlement.d.ts +1 -1
- package/dist/activities/agent-turn/history.d.ts +2 -0
- package/dist/activities/agent-turn/media-artifacts.d.ts +5 -3
- 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-IVTL723D.js} +204 -23
- package/dist/activities-control-IVTL723D.js.map +1 -0
- package/dist/{activities-turn-YVVKCGAK.js → activities-turn-EZEDZTEZ.js} +108 -33
- package/dist/activities-turn-EZEDZTEZ.js.map +1 -0
- package/dist/{chunk-M3NVMF2V.js → chunk-JMO5ZUW3.js} +14 -7
- package/dist/{chunk-M3NVMF2V.js.map → chunk-JMO5ZUW3.js.map} +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/workflow-bundle.js +4 -1
- package/package.json +15 -15
- package/src/activities/agent-turn/errors.ts +8 -3
- package/src/activities/agent-turn/failure-settlement.ts +40 -8
- package/src/activities/agent-turn/history.ts +14 -1
- package/src/activities/agent-turn/media-artifacts.ts +2 -1
- package/src/activities/agent-turn/sandbox-runtime.ts +25 -9
- package/src/activities/agent-turn/stream-attempt.ts +13 -2
- package/src/activities/knowledge-indexing.ts +133 -6
- package/src/activities/retained-screenshots.ts +82 -11
- package/src/activities/sandbox-lease.ts +141 -21
- package/src/sandbox-resume.ts +6 -0
- package/src/workflows/activities.ts +14 -1
- package/dist/activities-control-QA6OLRQY.js.map +0 -1
- package/dist/activities-turn-YVVKCGAK.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/worker-bundle",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
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": {
|
|
@@ -56,21 +56,21 @@
|
|
|
56
56
|
"@llamaindex/liteparse": "2.14.2",
|
|
57
57
|
"@opengeni/agent-proto": "^0.6.0",
|
|
58
58
|
"@opengeni/capabilities": "^0.3.4",
|
|
59
|
-
"@opengeni/codemode": "^0.6.
|
|
60
|
-
"@opengeni/codex": "^0.2.
|
|
61
|
-
"@opengeni/config": "^2.1.
|
|
62
|
-
"@opengeni/contracts": "^5.1.
|
|
63
|
-
"@opengeni/core": "^4.0.
|
|
64
|
-
"@opengeni/db": "^6.0.
|
|
65
|
-
"@opengeni/documents": "^0.8.
|
|
66
|
-
"@opengeni/events": "^0.4.
|
|
67
|
-
"@opengeni/github": "^0.7.
|
|
59
|
+
"@opengeni/codemode": "^0.6.2",
|
|
60
|
+
"@opengeni/codex": "^0.2.26",
|
|
61
|
+
"@opengeni/config": "^2.1.1",
|
|
62
|
+
"@opengeni/contracts": "^5.1.1",
|
|
63
|
+
"@opengeni/core": "^4.0.3",
|
|
64
|
+
"@opengeni/db": "^6.0.3",
|
|
65
|
+
"@opengeni/documents": "^0.8.34",
|
|
66
|
+
"@opengeni/events": "^0.4.32",
|
|
67
|
+
"@opengeni/github": "^0.7.16",
|
|
68
68
|
"@opengeni/network": "^0.3.1",
|
|
69
|
-
"@opengeni/observability": "^0.8.
|
|
70
|
-
"@opengeni/runtime": "^4.0.
|
|
71
|
-
"@opengeni/sdk": "^7.1.
|
|
72
|
-
"@opengeni/storage": "^0.2.
|
|
73
|
-
"@opengeni/tool-gateway": "^0.1.
|
|
69
|
+
"@opengeni/observability": "^0.8.32",
|
|
70
|
+
"@opengeni/runtime": "^4.0.3",
|
|
71
|
+
"@opengeni/sdk": "^7.1.1",
|
|
72
|
+
"@opengeni/storage": "^0.2.133",
|
|
73
|
+
"@opengeni/tool-gateway": "^0.1.13",
|
|
74
74
|
"@opengeni/xai-subscription": "^0.1.4",
|
|
75
75
|
"@temporalio/activity": "^1.17.0",
|
|
76
76
|
"@temporalio/client": "^1.17.0",
|
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
type CompactionProviderRejection,
|
|
17
17
|
isMcpRequestTimeoutError,
|
|
18
18
|
isMcpTransportConnectivityError,
|
|
19
|
-
|
|
19
|
+
isModalTaskExecStartPreDispatchUnavailableError,
|
|
20
|
+
isRoutingMutationOutcomeUnknownError,
|
|
20
21
|
RoutingWorkspaceRootChangedError,
|
|
21
22
|
SandboxMaterializationVerificationError,
|
|
22
23
|
materializationVerificationDiagnostic,
|
|
@@ -119,6 +120,7 @@ export function providerRecoveryResult(input: {
|
|
|
119
120
|
input.failureCode === "sandbox_command_start_unavailable" ||
|
|
120
121
|
input.failureCode === "mcp_transport_timeout" ||
|
|
121
122
|
input.failureCode === "mcp_transport_unavailable" ||
|
|
123
|
+
input.failureCode === "turn_execution_policy_definition_mismatch" ||
|
|
122
124
|
input.failureCode === POST_COMPACTION_CONTINUATION_EMPTY_CODE
|
|
123
125
|
? Math.max(
|
|
124
126
|
providerDelay ?? 0,
|
|
@@ -1021,10 +1023,13 @@ function baseAgentRunFailurePayload(
|
|
|
1021
1023
|
retryable: true,
|
|
1022
1024
|
};
|
|
1023
1025
|
}
|
|
1024
|
-
if (
|
|
1026
|
+
if (
|
|
1027
|
+
!isRoutingMutationOutcomeUnknownError(error) &&
|
|
1028
|
+
isModalTaskExecStartPreDispatchUnavailableError(error)
|
|
1029
|
+
) {
|
|
1025
1030
|
return {
|
|
1026
1031
|
error:
|
|
1027
|
-
"The managed sandbox command
|
|
1032
|
+
"The managed sandbox command router was not ready before the command was sent. The same turn will retry after a short delay.",
|
|
1028
1033
|
code: "sandbox_command_start_unavailable",
|
|
1029
1034
|
retryable: true,
|
|
1030
1035
|
};
|
|
@@ -16,14 +16,14 @@ import {
|
|
|
16
16
|
} from "@opengeni/db";
|
|
17
17
|
import { publishDurableSessionEvents } from "@opengeni/events";
|
|
18
18
|
import { maxTurnsExceededRunState } from "@opengeni/runtime";
|
|
19
|
-
import { CancelledFailure } from "@temporalio/activity";
|
|
19
|
+
import { ApplicationFailure, CancelledFailure } from "@temporalio/activity";
|
|
20
20
|
import {
|
|
21
21
|
authoritativeCodexCapacityResetAt,
|
|
22
22
|
classifyCodexPin,
|
|
23
23
|
selectCodexCredentialLeaseForTurn,
|
|
24
24
|
type CodexRotationStrategy,
|
|
25
25
|
} from "../codex-rotation";
|
|
26
|
-
import type
|
|
26
|
+
import { TurnExecutionPolicyDefinitionMismatchError, type Settings } from "@opengeni/config";
|
|
27
27
|
import {
|
|
28
28
|
classifyCodexEncryptedArtifactRejection,
|
|
29
29
|
classifyCodexUsageLimitError,
|
|
@@ -1499,10 +1499,28 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1499
1499
|
// truth, recover this SAME accepted turn, then let the workflow re-claim
|
|
1500
1500
|
// it after a pacing delay. This is independent of goal state and never
|
|
1501
1501
|
// relies on a synthetic continuation prompt.
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1502
|
+
// A rolling-deployment definition mismatch is a separate configuration
|
|
1503
|
+
// class: only the exact typed setup error can use this checkpoint before
|
|
1504
|
+
// eventing exists. No generic setup/credential failure gains retry authority.
|
|
1505
|
+
const earlyDefinitionMismatch =
|
|
1506
|
+
error instanceof TurnExecutionPolicyDefinitionMismatchError &&
|
|
1507
|
+
!attempt.modelRequestStarted &&
|
|
1508
|
+
!eventing.turnStartedPublished &&
|
|
1509
|
+
!!attempt.turnId &&
|
|
1510
|
+
!!attempt.triggerEventId &&
|
|
1511
|
+
attempt.executionGeneration > 0;
|
|
1512
|
+
let failure = (
|
|
1513
|
+
earlyDefinitionMismatch
|
|
1514
|
+
? { error: error.message, code: error.code, retryable: true }
|
|
1515
|
+
: agentRunFailurePayload(error, {
|
|
1516
|
+
isCodexTurn: billingState.isCodexTurn,
|
|
1517
|
+
})
|
|
1518
|
+
) as ReturnType<typeof agentRunFailurePayload>;
|
|
1519
|
+
if (
|
|
1520
|
+
attempt.turnId &&
|
|
1521
|
+
(earlyDefinitionMismatch ||
|
|
1522
|
+
(failure.retryable && eventing.publish && eventing.turnStartedPublished))
|
|
1523
|
+
) {
|
|
1506
1524
|
const nextProviderRecoveryCount = attempt.providerRecoveryCount + 1;
|
|
1507
1525
|
const recoveryResult = providerRecoveryResult({
|
|
1508
1526
|
failureCode: failure.code,
|
|
@@ -1511,8 +1529,10 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1511
1529
|
});
|
|
1512
1530
|
try {
|
|
1513
1531
|
if (recoveryResult.status === "recovering") {
|
|
1514
|
-
|
|
1515
|
-
|
|
1532
|
+
if (!earlyDefinitionMismatch) {
|
|
1533
|
+
await flushRuntimeBatcher();
|
|
1534
|
+
await historySink.reconcileConversationTruth({ requireDurable: true });
|
|
1535
|
+
}
|
|
1516
1536
|
const recovery = await requestSessionTurnRecovery(db, input.workspaceId, {
|
|
1517
1537
|
sessionId: input.sessionId,
|
|
1518
1538
|
turnId: attempt.turnId,
|
|
@@ -1540,6 +1560,18 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
|
|
|
1540
1560
|
return claimedResult(recoveryResult);
|
|
1541
1561
|
}
|
|
1542
1562
|
failure = providerRecoveryExhaustedFailure(failure, recoveryResult);
|
|
1563
|
+
if (earlyDefinitionMismatch) {
|
|
1564
|
+
// Setup has no eventing sink yet. Carry only the fixed, safe diagnostic
|
|
1565
|
+
// through Temporal into exact-attempt workflow failure settlement.
|
|
1566
|
+
control.activityStatus = "failed";
|
|
1567
|
+
control.turnMetricOutcome = "failed";
|
|
1568
|
+
control.activityError = error;
|
|
1569
|
+
throw ApplicationFailure.create({
|
|
1570
|
+
message: `${error.message}. Automatic same-turn configuration recovery exhausted after ${recoveryResult.providerRecoveryCount} retries.`,
|
|
1571
|
+
type: "TurnExecutionPolicyDefinitionMismatchError",
|
|
1572
|
+
nonRetryable: true,
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1543
1575
|
} catch (recoveryError) {
|
|
1544
1576
|
const escaped =
|
|
1545
1577
|
recoveryResult.status === "recovering"
|
|
@@ -186,7 +186,20 @@ export function toolCallProducesRetainableSessionImage(name: string | null): boo
|
|
|
186
186
|
return (
|
|
187
187
|
name === "computer_screenshot" ||
|
|
188
188
|
name === "view_image" ||
|
|
189
|
-
name === "interaction__computer_observe"
|
|
189
|
+
name === "interaction__computer_observe" ||
|
|
190
|
+
retainableBrowserScreenshotToolCall(name)
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Browser interactions can carry an image block, including opt-in observe frames. */
|
|
195
|
+
export function retainableBrowserScreenshotToolCall(name: string | null): boolean {
|
|
196
|
+
return (
|
|
197
|
+
name === "browser_screenshot" ||
|
|
198
|
+
name === "interaction__browser_screenshot" ||
|
|
199
|
+
name === "browser_observe" ||
|
|
200
|
+
name === "interaction__browser_observe" ||
|
|
201
|
+
name === "browser_act" ||
|
|
202
|
+
name === "interaction__browser_act"
|
|
190
203
|
);
|
|
191
204
|
}
|
|
192
205
|
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type TurnToolCancellationFence,
|
|
13
13
|
} from "@opengeni/runtime";
|
|
14
14
|
import type { Settings } from "@opengeni/config";
|
|
15
|
-
import type { RetainedArtifactMetadata } from "@opengeni/contracts";
|
|
15
|
+
import type { RetainedArtifactMetadata, RetainedSessionScreenshotKind } from "@opengeni/contracts";
|
|
16
16
|
import type { ResumedTurnSandbox } from "../../sandbox-resume";
|
|
17
17
|
import type { SharedActivityServices } from "../types";
|
|
18
18
|
import {
|
|
@@ -79,6 +79,7 @@ export class TurnMediaArtifacts {
|
|
|
79
79
|
sandboxFileDownloadBackend: Settings["sandboxBackend"];
|
|
80
80
|
nativeImageGenerationRetention: NativeImageGenerationRetention | null = null;
|
|
81
81
|
readonly retainedSessionImageCallIds = new Set<string>();
|
|
82
|
+
readonly retainedSessionImageKindsByCallId = new Map<string, RetainedSessionScreenshotKind>();
|
|
82
83
|
|
|
83
84
|
constructor(private readonly deps: TurnMediaArtifactDeps) {
|
|
84
85
|
this.sandboxFileDownloadBackend = deps.getModelRunSettings().sandboxBackend;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
advanceWorkspaceGeneration,
|
|
3
3
|
verifyWorkspaceMutationSettlement,
|
|
4
|
-
|
|
4
|
+
heartbeatLeaseHolderStatus,
|
|
5
5
|
readLease,
|
|
6
6
|
accrueWarmSeconds,
|
|
7
7
|
SandboxWorkspaceMutationFencedError,
|
|
@@ -526,12 +526,15 @@ export function createSandboxTurnRuntime(deps: SandboxTurnRuntimeDeps) {
|
|
|
526
526
|
// home turn that degraded to the cloud group box (swap-away / flag-off), that
|
|
527
527
|
// is the deployment default (modal), so the fallback box is warm-metered at
|
|
528
528
|
// the cloud rate instead of selfhosted's rate-0 (which would under-bill).
|
|
529
|
-
const warmRate =
|
|
530
|
-
settings
|
|
531
|
-
|
|
532
|
-
|
|
529
|
+
const warmRate =
|
|
530
|
+
settings.sandboxWarmBillingMode === "usage_only"
|
|
531
|
+
? 0
|
|
532
|
+
: sandboxWarmRateMicrosPerSecond(
|
|
533
|
+
settings,
|
|
534
|
+
warmBackend ?? (sandbox.established.backendId as Settings["sandboxBackend"]),
|
|
535
|
+
);
|
|
533
536
|
sandboxState.leaseHeartbeatTimer = setInterval(() => {
|
|
534
|
-
void
|
|
537
|
+
void heartbeatLeaseHolderStatus(db, {
|
|
535
538
|
accountId: input.accountId,
|
|
536
539
|
workspaceId: input.workspaceId,
|
|
537
540
|
sandboxGroupId: heartbeatGroupId,
|
|
@@ -539,9 +542,17 @@ export function createSandboxTurnRuntime(deps: SandboxTurnRuntimeDeps) {
|
|
|
539
542
|
holderId: heartbeatHolderId,
|
|
540
543
|
leaseTtlMs: settings.sandboxLeaseTtlMs,
|
|
541
544
|
expectedEpoch: heartbeatEpoch,
|
|
545
|
+
billingMode: settings.sandboxWarmBillingMode,
|
|
542
546
|
})
|
|
543
|
-
.then(async (
|
|
544
|
-
if (
|
|
547
|
+
.then(async (status) => {
|
|
548
|
+
if (status.fence === "funding") {
|
|
549
|
+
stopLeaseHeartbeat();
|
|
550
|
+
sandboxRotationController.abort(
|
|
551
|
+
new Error("Insufficient OpenGeni credits to extend paid sandbox compute"),
|
|
552
|
+
);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (status.leaseExtended) return;
|
|
545
556
|
const rotation = await beginRotationPreemption(sandbox, heartbeatEpoch, heartbeatGroupId);
|
|
546
557
|
if (rotation === "not_rotating") {
|
|
547
558
|
// The holder was reaped, the exact attempt closed, the epoch was
|
|
@@ -557,9 +568,14 @@ export function createSandboxTurnRuntime(deps: SandboxTurnRuntimeDeps) {
|
|
|
557
568
|
sandboxGroupId: heartbeatGroupId,
|
|
558
569
|
expectedEpoch: heartbeatEpoch,
|
|
559
570
|
warmRateMicrosPerSecond: warmRate,
|
|
571
|
+
billingMode: settings.sandboxWarmBillingMode,
|
|
560
572
|
subjectId: input.sessionId,
|
|
561
573
|
})
|
|
562
|
-
.then((result) =>
|
|
574
|
+
.then((result) => {
|
|
575
|
+
if (settings.sandboxWarmBillingMode === "credits") {
|
|
576
|
+
recordCreditMicros(observability, "usage", result.costMicros);
|
|
577
|
+
}
|
|
578
|
+
})
|
|
563
579
|
.catch(() => undefined);
|
|
564
580
|
// MID-SESSION snapshot (sandbox-file-persistence): while the turn holds
|
|
565
581
|
// the box, fold a fresh /workspace snapshot onto the lease every
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
import {
|
|
62
62
|
compactRetainedScreenshotHistory,
|
|
63
63
|
sdkEventContainsInlineImage,
|
|
64
|
-
|
|
64
|
+
retainSessionScreenshot,
|
|
65
65
|
typedScreenshotFromSdkEvent,
|
|
66
66
|
unavailableRetainedSessionImage,
|
|
67
67
|
} from "../retained-screenshots";
|
|
@@ -108,6 +108,7 @@ import {
|
|
|
108
108
|
import {
|
|
109
109
|
pendingToolCallFromSdkEvent,
|
|
110
110
|
toolCallProducesRetainableSessionImage,
|
|
111
|
+
retainableBrowserScreenshotToolCall,
|
|
111
112
|
completedToolCallFromSdkEvent,
|
|
112
113
|
} from "./history";
|
|
113
114
|
import { checkpointHistoryBeforeProviderDispatch } from "./provider-dispatch-barrier";
|
|
@@ -1125,6 +1126,12 @@ export async function runTurnStreamAttempt(
|
|
|
1125
1126
|
currentToolBatchCallIds.add(pendingToolCall.callId);
|
|
1126
1127
|
if (toolCallProducesRetainableSessionImage(pendingToolCall.callName)) {
|
|
1127
1128
|
media.retainedSessionImageCallIds.add(pendingToolCall.callId);
|
|
1129
|
+
if (retainableBrowserScreenshotToolCall(pendingToolCall.callName)) {
|
|
1130
|
+
media.retainedSessionImageKindsByCallId.set(
|
|
1131
|
+
pendingToolCall.callId,
|
|
1132
|
+
"browser_screenshot",
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1128
1135
|
}
|
|
1129
1136
|
}
|
|
1130
1137
|
const completedToolCall = completedToolCallFromSdkEvent(durableSdkEvent);
|
|
@@ -1147,6 +1154,7 @@ export async function runTurnStreamAttempt(
|
|
|
1147
1154
|
toolCallId: completedToolCall.callId,
|
|
1148
1155
|
toolOutputId: completedToolCall.callId,
|
|
1149
1156
|
reason: "unsupported",
|
|
1157
|
+
kind: media.retainedSessionImageKindsByCallId.get(completedToolCall.callId),
|
|
1150
1158
|
});
|
|
1151
1159
|
media.retainedScreenshotReceiptsByCallId.set(
|
|
1152
1160
|
completedToolCall.callId,
|
|
@@ -1169,12 +1177,13 @@ export async function runTurnStreamAttempt(
|
|
|
1169
1177
|
toolCallId: typedScreenshot.callId,
|
|
1170
1178
|
toolOutputId: typedScreenshot.toolOutputId,
|
|
1171
1179
|
reason: "pending",
|
|
1180
|
+
kind: media.retainedSessionImageKindsByCallId.get(completedToolCall.callId),
|
|
1172
1181
|
});
|
|
1173
1182
|
media.retainedScreenshotReceiptsByCallId.set(
|
|
1174
1183
|
completedToolCall.callId,
|
|
1175
1184
|
retainedScreenshotMetadata,
|
|
1176
1185
|
);
|
|
1177
|
-
retainedScreenshotMetadata = await
|
|
1186
|
+
retainedScreenshotMetadata = await retainSessionScreenshot({
|
|
1178
1187
|
db,
|
|
1179
1188
|
objectStorage,
|
|
1180
1189
|
accountId: input.accountId,
|
|
@@ -1183,6 +1192,7 @@ export async function runTurnStreamAttempt(
|
|
|
1183
1192
|
turnId: activeTurnId,
|
|
1184
1193
|
attemptId: input.attemptId,
|
|
1185
1194
|
output: typedScreenshot,
|
|
1195
|
+
kind: media.retainedSessionImageKindsByCallId.get(completedToolCall.callId),
|
|
1186
1196
|
});
|
|
1187
1197
|
media.retainedScreenshotReceiptsByCallId.set(
|
|
1188
1198
|
completedToolCall.callId,
|
|
@@ -1339,6 +1349,7 @@ export async function runTurnStreamAttempt(
|
|
|
1339
1349
|
}
|
|
1340
1350
|
for (const callId of stableToolCallIdsToClear) {
|
|
1341
1351
|
media.retainedSessionImageCallIds.delete(callId);
|
|
1352
|
+
media.retainedSessionImageKindsByCallId.delete(callId);
|
|
1342
1353
|
}
|
|
1343
1354
|
}
|
|
1344
1355
|
}
|
|
@@ -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
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
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
|
-
|
|
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)
|
|
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
|
|
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);
|