@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/worker-bundle",
3
- "version": "2.0.1",
3
+ "version": "2.0.3-canary.36079201993001",
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": {
@@ -54,24 +54,24 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@llamaindex/liteparse": "2.14.2",
57
- "@opengeni/agent-proto": "^0.6.0",
58
- "@opengeni/capabilities": "^0.3.4",
59
- "@opengeni/codemode": "^0.6.1",
60
- "@opengeni/codex": "^0.2.25",
61
- "@opengeni/config": "^2.1.0",
62
- "@opengeni/contracts": "^5.1.0",
63
- "@opengeni/core": "^4.0.1",
64
- "@opengeni/db": "^6.0.1",
65
- "@opengeni/documents": "^0.8.32",
66
- "@opengeni/events": "^0.4.30",
67
- "@opengeni/github": "^0.7.15",
68
- "@opengeni/network": "^0.3.1",
69
- "@opengeni/observability": "^0.8.31",
70
- "@opengeni/runtime": "^4.0.1",
71
- "@opengeni/sdk": "^7.1.0",
72
- "@opengeni/storage": "^0.2.132",
73
- "@opengeni/tool-gateway": "^0.1.12",
74
- "@opengeni/xai-subscription": "^0.1.4",
57
+ "@opengeni/agent-proto": "^0.6.0-canary.36079201993001",
58
+ "@opengeni/capabilities": "^0.3.4-canary.36079201993001",
59
+ "@opengeni/codemode": "^0.6.2-canary.36079201993001",
60
+ "@opengeni/codex": "^0.2.26-canary.36079201993001",
61
+ "@opengeni/config": "^2.1.1-canary.36079201993001",
62
+ "@opengeni/contracts": "^5.1.1-canary.36079201993001",
63
+ "@opengeni/core": "^4.0.3-canary.36079201993001",
64
+ "@opengeni/db": "^6.0.3-canary.36079201993001",
65
+ "@opengeni/documents": "^0.8.34-canary.36079201993001",
66
+ "@opengeni/events": "^0.4.32-canary.36079201993001",
67
+ "@opengeni/github": "^0.7.16-canary.36079201993001",
68
+ "@opengeni/network": "^0.3.1-canary.36079201993001",
69
+ "@opengeni/observability": "^0.8.32-canary.36079201993001",
70
+ "@opengeni/runtime": "^4.0.3-canary.36079201993001",
71
+ "@opengeni/sdk": "^7.1.1-canary.36079201993001",
72
+ "@opengeni/storage": "^0.2.133-canary.36079201993001",
73
+ "@opengeni/tool-gateway": "^0.1.13-canary.36079201993001",
74
+ "@opengeni/xai-subscription": "^0.1.4-canary.36079201993001",
75
75
  "@temporalio/activity": "^1.17.0",
76
76
  "@temporalio/client": "^1.17.0",
77
77
  "@temporalio/worker": "^1.17.0",
@@ -11,7 +11,8 @@ import {
11
11
  compactionThresholdTokens,
12
12
  CompactionNeededError,
13
13
  compactionProviderRejection,
14
- SUMMARY_BUFFER_TOKENS,
14
+ estimateSerializedValueTokens,
15
+ compactionSummaryOutputTokens,
15
16
  type ModelResponseUsage,
16
17
  } from "@opengeni/runtime";
17
18
  import { type Settings } from "@opengeni/config";
@@ -168,6 +169,13 @@ export async function prepareCompaction(deps: CompactionPrepDeps): Promise<Compa
168
169
  const remotePrefix: RemoteCompactionPrefix = {
169
170
  agent: null,
170
171
  };
172
+ const portableResponsesNeedsAgentPrefix =
173
+ resolvedModel?.provider.api === "responses" &&
174
+ !(billingState.isCodexTurn && session.codexCompactionMode === "remote_v2");
175
+ const preparedPortableRequest = () => {
176
+ if (!remotePrefix.agent) throw new Error("Compaction agent is unavailable");
177
+ return preparedCompactionRequest(remotePrefix.agent);
178
+ };
171
179
 
172
180
  const promptCacheKey = acceptsPromptCacheKeyForTurn(resolvedModel) ? input.sessionId : undefined;
173
181
  const compactionUsageState = createCompactionModelUsageEventState(claimedModelUsageSourceKeys);
@@ -200,8 +208,8 @@ export async function prepareCompaction(deps: CompactionPrepDeps): Promise<Compa
200
208
  contextContributions: eventing.companyBrainContextContributions,
201
209
  });
202
210
  };
203
- const compactionSummarizerFor = (systemInstructions?: string) =>
204
- resolvedModel
211
+ const compactionSummarizerFor = (systemInstructions?: string): CompactionSummarizer => {
212
+ const summarize: CompactionSummarizer = resolvedModel
205
213
  ? (s: Settings, m: Array<Record<string, unknown>>) =>
206
214
  withProviderRequestContext(() =>
207
215
  summarizeContextForCompaction(s, m, {
@@ -209,20 +217,37 @@ export async function prepareCompaction(deps: CompactionPrepDeps): Promise<Compa
209
217
  provider: resolvedModel.provider,
210
218
  api: resolvedModel.provider.api,
211
219
  model: turnExecutionPolicy.upstreamModelId,
212
- maxOutputTokens: SUMMARY_BUFFER_TOKENS,
220
+ maxOutputTokens: compactionSummaryOutputTokens(s.contextWindowTokens),
221
+ ...(cancellationSignal ? { signal: cancellationSignal } : {}),
213
222
  onUsage: recordCompactionUsage,
214
223
  ...(systemInstructions ? { systemInstructions } : {}),
215
224
  ...(promptCacheKey ? { promptCacheKey } : {}),
225
+ ...(portableResponsesNeedsAgentPrefix
226
+ ? { preparedRequest: preparedPortableRequest() }
227
+ : {}),
216
228
  }),
217
229
  )
218
230
  : (s: Settings, m: Array<Record<string, unknown>>) =>
219
231
  summarizeContextForCompaction(s, m, {
220
232
  model: turnExecutionPolicy.upstreamModelId,
221
- maxOutputTokens: SUMMARY_BUFFER_TOKENS,
233
+ maxOutputTokens: compactionSummaryOutputTokens(s.contextWindowTokens),
234
+ ...(cancellationSignal ? { signal: cancellationSignal } : {}),
222
235
  onUsage: recordCompactionUsage,
223
236
  ...(systemInstructions ? { systemInstructions } : {}),
224
237
  ...(promptCacheKey ? { promptCacheKey } : {}),
225
238
  });
239
+ summarize.estimatePrefixTokens = () => {
240
+ if (resolvedModel?.provider.api === "chat") {
241
+ return estimateSerializedValueTokens(systemInstructions ?? "");
242
+ }
243
+ const prepared = portableResponsesNeedsAgentPrefix ? preparedPortableRequest() : null;
244
+ return (
245
+ estimateSerializedValueTokens(prepared?.systemInstructions ?? systemInstructions ?? "") +
246
+ (prepared ? estimateSerializedValueTokens(prepared.tools) : 0)
247
+ );
248
+ };
249
+ return summarize;
250
+ };
226
251
  // Prompt-cache prefix for remote_v2 MUST match ordinary turns:
227
252
  // tools → instructions → history. Filled after buildAgent for every
228
253
  // compact path (including operator /compact, which now builds the agent
@@ -265,13 +290,9 @@ export async function prepareCompaction(deps: CompactionPrepDeps): Promise<Compa
265
290
  ...(remoteCompactionRequester ? { requestRemoteCompactionV2: remoteCompactionRequester } : {}),
266
291
  } as const;
267
292
 
268
- // Operator /compact:
269
- // - portable (incl. Codex portable): early maintenance path — no
270
- // prepareTools/sandbox; summarizer only needs composed instructions.
271
- // - remote_v2: fall through to prepareTools/buildAgent so the compact
272
- // request reuses the ordinary tools→instructions cache prefix, then
273
- // settle without inference. (Requester is also wired for Codex portable
274
- // turns but unused there — gate on the frozen session mode.)
293
+ // Responses compaction, portable or remote, prepares the ordinary agent
294
+ // request first so tool schemas and instructions match the warm cache prefix.
295
+ // Chat providers keep the standalone portable maintenance path.
275
296
  const compactionOnlyTurn = turn.source === "compaction";
276
297
  const remoteV2CompactionNeedsAgentPrefix =
277
298
  Boolean(remoteCompactionRequester) && session.codexCompactionMode === "remote_v2";
@@ -308,7 +329,11 @@ export async function prepareCompaction(deps: CompactionPrepDeps): Promise<Compa
308
329
  control.activityStatus = "idle";
309
330
  return claimedResult({ status: "idle" });
310
331
  };
311
- if (compactionOnlyTurn && !remoteV2CompactionNeedsAgentPrefix) {
332
+ if (
333
+ compactionOnlyTurn &&
334
+ !remoteV2CompactionNeedsAgentPrefix &&
335
+ !portableResponsesNeedsAgentPrefix
336
+ ) {
312
337
  const compactionInstructions = appendWorkspaceMemory(
313
338
  appendSessionInstructions(
314
339
  appendWorkspaceGovernance(
@@ -466,6 +491,7 @@ export async function runPostAgentCompaction(
466
491
  claimedResult,
467
492
  turn,
468
493
  session,
494
+ resolvedModel,
469
495
  remotePrefix,
470
496
  remoteCompactionRequester,
471
497
  publishCompactionLiveEvents,
@@ -480,10 +506,13 @@ export async function runPostAgentCompaction(
480
506
  } = deps;
481
507
 
482
508
  const agentInstructions = typeof agent.instructions === "string" ? agent.instructions : "";
509
+ const preparedPortable =
510
+ resolvedModel?.provider.api === "responses" &&
511
+ !(remoteCompactionRequester && session.codexCompactionMode === "remote_v2");
483
512
  const compactSummarizer = compactionSummarizerFor(
484
513
  agentInstructions.trim() ? agentInstructions : undefined,
485
514
  );
486
- if (remoteCompactionRequester) {
515
+ if (remoteCompactionRequester || preparedPortable) {
487
516
  // The prefix is captured only after this agent passes normal SDK preparation.
488
517
  remotePrefix.agent = agent;
489
518
  }
@@ -491,7 +520,11 @@ export async function runPostAgentCompaction(
491
520
  if (compactionOnlyTurn) {
492
521
  const requested = await isSessionCompactionRequested(db, input.workspaceId, input.sessionId);
493
522
  let outcome: Awaited<ReturnType<typeof maybeCompactContext>> | null = null;
494
- if (requested && remoteCompactionRequester && session.codexCompactionMode === "remote_v2") {
523
+ if (
524
+ requested &&
525
+ (preparedPortable ||
526
+ (remoteCompactionRequester && session.codexCompactionMode === "remote_v2"))
527
+ ) {
495
528
  queuePreparedCompaction(
496
529
  agent,
497
530
  new CompactionNeededError({
@@ -610,7 +643,10 @@ export async function runPostAgentCompaction(
610
643
  return { exit: claimedResult({ status: "idle" }) };
611
644
  }
612
645
 
613
- if (remoteCompactionRequester && session.codexCompactionMode === "remote_v2") {
646
+ if (
647
+ preparedPortable ||
648
+ (remoteCompactionRequester && session.codexCompactionMode === "remote_v2")
649
+ ) {
614
650
  const forced = await isSessionCompactionRequested(db, input.workspaceId, input.sessionId);
615
651
  const thresholdTokens = compactionThresholdTokens(eventing.modelRunSettings);
616
652
  if (
@@ -16,7 +16,8 @@ import {
16
16
  type CompactionProviderRejection,
17
17
  isMcpRequestTimeoutError,
18
18
  isMcpTransportConnectivityError,
19
- isModalTaskExecStartDnsResolutionError,
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 (isModalTaskExecStartDnsResolutionError(error)) {
1026
+ if (
1027
+ !isRoutingMutationOutcomeUnknownError(error) &&
1028
+ isModalTaskExecStartPreDispatchUnavailableError(error)
1029
+ ) {
1025
1030
  return {
1026
1031
  error:
1027
- "The managed sandbox command transport was temporarily unreachable before the command started. The same turn will retry after a short delay.",
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 { Settings } from "@opengeni/config";
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
- let failure = agentRunFailurePayload(error, {
1503
- isCodexTurn: billingState.isCodexTurn,
1504
- }) as ReturnType<typeof agentRunFailurePayload>;
1505
- if (failure.retryable && eventing.publish && attempt.turnId && eventing.turnStartedPublished) {
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
- await flushRuntimeBatcher();
1515
- await historySink.reconcileConversationTruth({ requireDurable: true });
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
- heartbeatLeaseHolder,
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 = sandboxWarmRateMicrosPerSecond(
530
- settings,
531
- warmBackend ?? (sandbox.established.backendId as Settings["sandboxBackend"]),
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 heartbeatLeaseHolder(db, {
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 (alive) => {
544
- if (alive) return;
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) => recordCreditMicros(observability, "usage", result.costMicros))
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
- retainComputerScreenshot,
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 retainComputerScreenshot({
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
  }
@@ -9,7 +9,8 @@ import {
9
9
  import {
10
10
  EmptyCompactionSummaryError,
11
11
  REMOTE_COMPACTION_V2_IMPLEMENTATION,
12
- SUMMARY_BUFFER_TOKENS,
12
+ compactionSummaryOutputTokens,
13
+ buildSummaryItem,
13
14
  buildCompactionReplacementHistory,
14
15
  buildRemoteV2ReplacementHistory,
15
16
  compactionThresholdTokens,
@@ -58,7 +59,13 @@ export type MaybeCompactResult =
58
59
  * Codex, call Codex `/codex/responses` with `compaction_trigger` and persist the
59
60
  * opaque compaction item. Fail closed — never silently fall back to portable.
60
61
  */
61
- export type CompactionSummarizer = (settings: Settings, input: CompactionItem[]) => Promise<string>;
62
+ export type CompactionSummarizer = ((
63
+ settings: Settings,
64
+ input: CompactionItem[],
65
+ ) => Promise<string>) & {
66
+ /** Model-visible instructions and tool schemas outside the history estimate. */
67
+ estimatePrefixTokens?: () => number;
68
+ };
62
69
 
63
70
  /** Returns the opaque Codex remote compaction v2 item. */
64
71
  export type RemoteCompactionV2Requester = (
@@ -81,7 +88,7 @@ export async function maybeCompactContext(
81
88
  // Injectable for tests; defaults to the real provider-aware model call.
82
89
  summarize: CompactionSummarizer = (s, m) =>
83
90
  summarizeForCompaction(s, m, {
84
- maxOutputTokens: SUMMARY_BUFFER_TOKENS,
91
+ maxOutputTokens: compactionSummaryOutputTokens(s.contextWindowTokens),
85
92
  }),
86
93
  // Operator-forced (the /compact command): bypass the budget trigger and
87
94
  // compact now if there is anything to summarize. Structural guards still hold.
@@ -300,6 +307,7 @@ async function settleSkippedAfterStart(
300
307
  reason:
301
308
  | "no_history"
302
309
  | "replacement_not_smaller"
310
+ | "replacement_exceeds_model_budget"
303
311
  | "replacement_unchanged"
304
312
  | "summarization_failed",
305
313
  ): Promise<Extract<MaybeCompactResult, { compacted: false }>> {
@@ -455,12 +463,23 @@ async function compactContextPortable(
455
463
  const summarized = await summarizeWithCodexOverflowTrimming(summarize, settings, items);
456
464
  const summaryBody = summarized.summaryBody;
457
465
  const retainedTokens = await retentionTokenCounts(canonicalItems, projectForWire);
466
+ const outputReserve = compactionSummaryOutputTokens(settings.contextWindowTokens);
467
+ const structuralBudget = Math.min(
468
+ contextInputBudgetTokens(settings) || settings.contextWindowTokens - outputReserve,
469
+ settings.contextWindowTokens - outputReserve,
470
+ );
471
+ const prefixTokens = Math.max(0, Math.ceil(summarize.estimatePrefixTokens?.() ?? 0));
472
+ const summaryTokens = estimateTokens([buildSummaryItem(summaryBody)]);
458
473
  const replacementHistory = buildCompactionReplacementHistory(
459
474
  canonicalItems,
460
475
  summaryBody,
461
476
  (item) => retainedTokens.get(item) ?? estimateTokens([item]),
477
+ Math.min(outputReserve, Math.max(0, structuralBudget - prefixTokens - summaryTokens)),
462
478
  );
463
479
  const estimatedTokensAfter = estimateTokens(await projectForWire(replacementHistory));
480
+ if (estimatedTokensAfter + prefixTokens > structuralBudget) {
481
+ return await settleSkippedAfterStart(db, scope, options, "replacement_exceeds_model_budget");
482
+ }
464
483
  const replacementFingerprint = compactionReplacementFingerprint(replacementHistory);
465
484
  const previousReplacementFingerprint = latestCompactionReplacementFingerprint(canonicalItems);
466
485
  const summaryIndex =
@@ -520,7 +539,7 @@ async function compactContextPortable(
520
539
  };
521
540
  }
522
541
 
523
- async function summarizeWithCodexOverflowTrimming(
542
+ export async function summarizeWithCodexOverflowTrimming(
524
543
  summarize: CompactionSummarizer,
525
544
  settings: Settings,
526
545
  activeHistory: CompactionItem[],
@@ -531,17 +550,32 @@ async function summarizeWithCodexOverflowTrimming(
531
550
  }> {
532
551
  // Codex's estimator is intentionally coarse. Keep the explicit checkpoint
533
552
  // request below both the effective input window and the raw window minus the
534
- // requested summary, then leave 15% estimator headroom. This changes only the
535
- // temporary summarizer input; durable active history remains untouched until
536
- // applyContextCompaction succeeds under the attempt fence.
537
- const summaryAwareBudget = Math.max(0, settings.contextWindowTokens - SUMMARY_BUFFER_TOKENS);
553
+ // requested summary. Preserve the full portable history copy on the first
554
+ // call whenever it fits; only trim further after an actual provider overflow.
555
+ // Durable active history remains untouched until applyContextCompaction.
556
+ const summaryAwareBudget = Math.max(
557
+ 0,
558
+ settings.contextWindowTokens - compactionSummaryOutputTokens(settings.contextWindowTokens),
559
+ );
538
560
  const configuredInputBudget = contextInputBudgetTokens(settings);
539
561
  const structuralBudget = Math.min(
540
562
  configuredInputBudget > 0 ? configuredInputBudget : summaryAwareBudget,
541
563
  summaryAwareBudget,
542
564
  );
543
- const initialBudget = Math.floor(structuralBudget * 0.85);
565
+ const prefixTokens = Math.max(0, Math.ceil(summarize.estimatePrefixTokens?.() ?? 0));
566
+ const initialBudget = Math.max(0, structuralBudget - prefixTokens);
544
567
  let preparation = prepareCompactionPromptInput(activeHistory, initialBudget);
568
+ // A checkpoint prompt without source history cannot summarize that history.
569
+ // Do not let a plausible-sounding reply replace durable active context.
570
+ const requireHistory = () => {
571
+ if (activeHistory.length > 0 && preparation.input.length === 1) {
572
+ throw new EmptyCompactionSummaryError({
573
+ stage: "portable_input_budget",
574
+ reason: "no_history_fit",
575
+ });
576
+ }
577
+ };
578
+ requireHistory();
545
579
  try {
546
580
  return {
547
581
  summaryBody: await summarize(settings, preparation.input),
@@ -551,15 +585,16 @@ async function summarizeWithCodexOverflowTrimming(
551
585
  } catch (error) {
552
586
  if (!isContextWindowExceeded(error)) throw error;
553
587
  // The provider is more authoritative than the byte/4 estimate. Refit once
554
- // to 70% of both the configured target and the actual prepared estimate;
588
+ // to 40% of both the available target and the actual prepared estimate;
555
589
  // then fail terminally with prior history intact. Never issue one failing
556
- // request per oldest item. The production incident proved that the
557
- // provider can count slightly more than twice the byte/4 estimate, so a
558
- // half-size retry is the smallest honest bound for that observed skew.
590
+ // request per oldest item. The retry is bounded, not a guarantee: the
591
+ // provider can count slightly more than twice the byte/4 estimate. The
592
+ // prepared prefix has already been reserved from the available budget.
559
593
  const retryBudget = Math.floor(
560
- Math.min(initialBudget * 0.5, preparation.estimatedInputTokens * 0.5),
594
+ Math.min(initialBudget * 0.4, preparation.estimatedInputTokens * 0.4),
561
595
  );
562
596
  preparation = prepareCompactionPromptInput(activeHistory, retryBudget);
597
+ requireHistory();
563
598
  return {
564
599
  summaryBody: await summarize(settings, preparation.input),
565
600
  preparation,