@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.
Files changed (63) hide show
  1. package/dist/activities/agent-turn/agent-build.d.ts +2 -0
  2. package/dist/activities/agent-turn/code-search.d.ts +60 -0
  3. package/dist/activities/agent-turn/codex-capacity.d.ts +13 -0
  4. package/dist/activities/agent-turn/errors.d.ts +12 -1
  5. package/dist/activities/agent-turn/failure-settlement.d.ts +1 -1
  6. package/dist/activities/agent-turn/governance-model.d.ts +2 -0
  7. package/dist/activities/agent-turn/history.d.ts +2 -0
  8. package/dist/activities/agent-turn/media-artifacts.d.ts +5 -3
  9. package/dist/activities/agent-turn/session-title.d.ts +32 -2
  10. package/dist/activities/agent-turn/tool-environment.d.ts +4 -0
  11. package/dist/activities/context-compaction.d.ts +10 -2
  12. package/dist/activities/knowledge-indexing.d.ts +19 -0
  13. package/dist/activities/retained-screenshots.d.ts +7 -2
  14. package/dist/activities/sandbox-lease.d.ts +3 -4
  15. package/dist/{activities-control-QA6OLRQY.js → activities-control-I72CRMAE.js} +283 -35
  16. package/dist/activities-control-I72CRMAE.js.map +1 -0
  17. package/dist/{activities-turn-YVVKCGAK.js → activities-turn-CSDKK4N3.js} +531 -77
  18. package/dist/activities-turn-CSDKK4N3.js.map +1 -0
  19. package/dist/artifact-outbox-entry.js +3 -0
  20. package/dist/artifact-outbox-entry.js.map +1 -1
  21. package/dist/{chunk-M3NVMF2V.js → chunk-7M6G2SCF.js} +172 -17
  22. package/dist/chunk-7M6G2SCF.js.map +1 -0
  23. package/dist/{chunk-I7HKKMUJ.js → chunk-DSPP6CZL.js} +48 -3
  24. package/dist/chunk-DSPP6CZL.js.map +1 -0
  25. package/dist/index.js +10 -5
  26. package/dist/index.js.map +1 -1
  27. package/dist/observability-metrics.d.ts +11 -1
  28. package/dist/sandbox-resume.d.ts +46 -0
  29. package/dist/workflow-bundle.js +80 -2
  30. package/dist/workflows/session.d.ts +30 -0
  31. package/package.json +20 -19
  32. package/src/activities/agent-turn/agent-build.ts +4 -0
  33. package/src/activities/agent-turn/code-search.ts +275 -0
  34. package/src/activities/agent-turn/codex-capacity.ts +37 -2
  35. package/src/activities/agent-turn/compaction-prep.ts +52 -16
  36. package/src/activities/agent-turn/errors.ts +67 -4
  37. package/src/activities/agent-turn/failure-settlement.ts +40 -8
  38. package/src/activities/agent-turn/governance-model.ts +17 -1
  39. package/src/activities/agent-turn/history.ts +14 -1
  40. package/src/activities/agent-turn/media-artifacts.ts +2 -1
  41. package/src/activities/agent-turn/run.ts +4 -0
  42. package/src/activities/agent-turn/sandbox-establish.ts +12 -1
  43. package/src/activities/agent-turn/sandbox-runtime.ts +25 -9
  44. package/src/activities/agent-turn/session-title.ts +73 -3
  45. package/src/activities/agent-turn/stream-attempt.ts +27 -15
  46. package/src/activities/agent-turn/tool-environment.ts +65 -0
  47. package/src/activities/context-compaction.ts +49 -14
  48. package/src/activities/knowledge-indexing.ts +240 -15
  49. package/src/activities/retained-screenshots.ts +82 -11
  50. package/src/activities/sandbox-lease.ts +141 -21
  51. package/src/activities/scheduled-tasks.ts +18 -2
  52. package/src/activity-services.ts +16 -4
  53. package/src/editable-artifact-hint-broker.ts +3 -0
  54. package/src/index.ts +2 -2
  55. package/src/observability-metrics.ts +70 -1
  56. package/src/personal-github-git-credentials.ts +2 -0
  57. package/src/sandbox-resume.ts +211 -5
  58. package/src/workflows/activities.ts +14 -1
  59. package/src/workflows/session.ts +102 -6
  60. package/dist/activities-control-QA6OLRQY.js.map +0 -1
  61. package/dist/activities-turn-YVVKCGAK.js.map +0 -1
  62. package/dist/chunk-I7HKKMUJ.js.map +0 -1
  63. package/dist/chunk-M3NVMF2V.js.map +0 -1
@@ -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";
@@ -125,7 +126,10 @@ import {
125
126
  recordModelUsageAndDebitCredits,
126
127
  recordAuthoritativeModelCallFact,
127
128
  } from "./model-usage";
128
- import { startParallelSessionTitleGeneration } from "./session-title";
129
+ import {
130
+ sessionTitleGenerationOptions,
131
+ startParallelSessionTitleGeneration,
132
+ } from "./session-title";
129
133
  import {
130
134
  assertAgentStreamNotCancelled,
131
135
  assertSuccessfulAgentStreamCompletion,
@@ -1125,6 +1129,12 @@ export async function runTurnStreamAttempt(
1125
1129
  currentToolBatchCallIds.add(pendingToolCall.callId);
1126
1130
  if (toolCallProducesRetainableSessionImage(pendingToolCall.callName)) {
1127
1131
  media.retainedSessionImageCallIds.add(pendingToolCall.callId);
1132
+ if (retainableBrowserScreenshotToolCall(pendingToolCall.callName)) {
1133
+ media.retainedSessionImageKindsByCallId.set(
1134
+ pendingToolCall.callId,
1135
+ "browser_screenshot",
1136
+ );
1137
+ }
1128
1138
  }
1129
1139
  }
1130
1140
  const completedToolCall = completedToolCallFromSdkEvent(durableSdkEvent);
@@ -1147,6 +1157,7 @@ export async function runTurnStreamAttempt(
1147
1157
  toolCallId: completedToolCall.callId,
1148
1158
  toolOutputId: completedToolCall.callId,
1149
1159
  reason: "unsupported",
1160
+ kind: media.retainedSessionImageKindsByCallId.get(completedToolCall.callId),
1150
1161
  });
1151
1162
  media.retainedScreenshotReceiptsByCallId.set(
1152
1163
  completedToolCall.callId,
@@ -1169,12 +1180,13 @@ export async function runTurnStreamAttempt(
1169
1180
  toolCallId: typedScreenshot.callId,
1170
1181
  toolOutputId: typedScreenshot.toolOutputId,
1171
1182
  reason: "pending",
1183
+ kind: media.retainedSessionImageKindsByCallId.get(completedToolCall.callId),
1172
1184
  });
1173
1185
  media.retainedScreenshotReceiptsByCallId.set(
1174
1186
  completedToolCall.callId,
1175
1187
  retainedScreenshotMetadata,
1176
1188
  );
1177
- retainedScreenshotMetadata = await retainComputerScreenshot({
1189
+ retainedScreenshotMetadata = await retainSessionScreenshot({
1178
1190
  db,
1179
1191
  objectStorage,
1180
1192
  accountId: input.accountId,
@@ -1183,6 +1195,7 @@ export async function runTurnStreamAttempt(
1183
1195
  turnId: activeTurnId,
1184
1196
  attemptId: input.attemptId,
1185
1197
  output: typedScreenshot,
1198
+ kind: media.retainedSessionImageKindsByCallId.get(completedToolCall.callId),
1186
1199
  });
1187
1200
  media.retainedScreenshotReceiptsByCallId.set(
1188
1201
  completedToolCall.callId,
@@ -1339,6 +1352,7 @@ export async function runTurnStreamAttempt(
1339
1352
  }
1340
1353
  for (const callId of stableToolCallIdsToClear) {
1341
1354
  media.retainedSessionImageCallIds.delete(callId);
1355
+ media.retainedSessionImageKindsByCallId.delete(callId);
1342
1356
  }
1343
1357
  }
1344
1358
  }
@@ -1775,18 +1789,16 @@ export async function runTurnStreamAttempt(
1775
1789
  signal: runtimeCancellationSignal,
1776
1790
  generate: async (signal) =>
1777
1791
  await withSessionTitleProviderRequestContext(() =>
1778
- runtime.generateSessionTitle!(runSettings, sessionTitlePrompt, {
1779
- ...(resolvedModel
1780
- ? {
1781
- client: resolvedModel.client,
1782
- provider: resolvedModel.provider,
1783
- model: resolvedModel.model,
1784
- }
1785
- : {}),
1786
- modelName: turnExecutionPolicy.upstreamModelId,
1787
- ...(serviceTier ? { serviceTier } : {}),
1788
- signal,
1789
- }),
1792
+ runtime.generateSessionTitle!(
1793
+ runSettings,
1794
+ sessionTitlePrompt,
1795
+ sessionTitleGenerationOptions({
1796
+ resolvedModel,
1797
+ modelName: turnExecutionPolicy.upstreamModelId,
1798
+ serviceTier,
1799
+ signal,
1800
+ }),
1801
+ ),
1790
1802
  ),
1791
1803
  onError: (error) => {
1792
1804
  observability.warn("parallel session title generation failed", {
@@ -15,6 +15,7 @@ import {
15
15
  organizationModelProviderConnectionActiveForWorkspace,
16
16
  persistAttemptToolCatalog,
17
17
  prepareConnectorActionApproval,
18
+ recordUsageEvent,
18
19
  previewConnectorActionApproval,
19
20
  namedSubjectHasLiveWorkspaceAuthority,
20
21
  updateSessionTitleWithEvent,
@@ -101,11 +102,13 @@ import type {
101
102
  } from "./turn-context";
102
103
  import {
103
104
  createSessionTitleAttemptToolDefinition,
105
+ routeAllowsSessionTitleRequests,
104
106
  sessionTitleToolPlan,
105
107
  shouldRequestMissingSessionTitle,
106
108
  } from "./session-title";
107
109
  import { resolveTurnSandboxAccess } from "./turn-sandbox-access";
108
110
  import { createListModelsAttemptToolDefinition } from "./list-models";
111
+ import { codeSearchToolDefinitions, codeSearchWorkspaceFromChannel } from "./code-search";
109
112
  import { createWorkspaceSkillTools } from "./skill-tools";
110
113
  import { loadConfiguredBundledSkills } from "./skill-selection";
111
114
  import { guardSkillFilesystem } from "./skill-transfer";
@@ -152,6 +155,7 @@ export type PrepareTurnToolRuntimeDeps = {
152
155
  turnExecutionPolicy: ClaimTurnOk["turnExecutionPolicy"];
153
156
  trigger: ClaimTurnOk["trigger"];
154
157
  runSettings: GovernanceModelOk["runSettings"];
158
+ resolvedModel: GovernanceModelOk["resolvedModel"];
155
159
  lazyToolTransport: GovernanceModelOk["lazyToolTransport"];
156
160
  turnTools: ReturnType<typeof withFirstPartyTools>;
157
161
  connectionScope: { accountId: string; workspaceId: string };
@@ -163,6 +167,8 @@ export type PrepareTurnToolRuntimeDeps = {
163
167
  credentialSubjectId: ClaimTurnOk["credentialSubjectId"];
164
168
  interactionInterventionResume: ClaimTurnOk["interactionInterventionResume"];
165
169
  runWorkspaceMutationForSandbox: SandboxTurnRuntime["runWorkspaceMutationForSandbox"];
170
+ /** Deployment and workspace allow the Jev-backed code_search tool. */
171
+ codeSearchEnabled: boolean;
166
172
  throwIfWorkerShuttingDown: () => void;
167
173
  throwIfTurnCancelled: () => void;
168
174
  };
@@ -371,6 +377,7 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
371
377
  turnExecutionPolicy,
372
378
  trigger,
373
379
  runSettings: canonicalRunSettings,
380
+ resolvedModel,
374
381
  lazyToolTransport,
375
382
  turnTools: canonicalTurnTools,
376
383
  sandboxArtifactRuntime,
@@ -381,6 +388,7 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
381
388
  credentialSubjectId,
382
389
  interactionInterventionResume,
383
390
  runWorkspaceMutationForSandbox,
391
+ codeSearchEnabled,
384
392
  throwIfWorkerShuttingDown,
385
393
  throwIfTurnCancelled,
386
394
  } = deps;
@@ -573,6 +581,7 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
573
581
  firstPartyMcpPermissions: effectiveFirstPartyPermissions,
574
582
  }),
575
583
  parallelGenerationAvailable: typeof runtime.generateSessionTitle === "function",
584
+ routeAllowsTitleRequests: routeAllowsSessionTitleRequests(resolvedModel),
576
585
  });
577
586
  const googleDrivePublicationAllowed =
578
587
  selectedFirstPartyMcpTools.includes("editable_artifact_export") &&
@@ -762,6 +771,60 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
762
771
  return ((await prepared.ready) ?? prepared).attemptToolEnvironment;
763
772
  },
764
773
  });
774
+ const codeSearchTools = codeSearchToolDefinitions({
775
+ enabled: codeSearchEnabled,
776
+ settings: runSettings,
777
+ backend: activeSandboxBackend ?? groupBoxBackend,
778
+ machineWorkspaceRoot: sandboxState.machinePrimarySession?.workspaceRoot ?? null,
779
+ observability,
780
+ // OpenGeni's Jev key pays for these calls whatever model billing the
781
+ // workspace uses; record them per workspace so the cost stays visible.
782
+ recordUsage: async (usage) => {
783
+ const shared = {
784
+ accountId: input.accountId,
785
+ workspaceId: input.workspaceId,
786
+ sourceResourceType: "code_search",
787
+ sourceResourceId: usage.operationId,
788
+ sessionId: input.sessionId,
789
+ turnId: turn.id,
790
+ turnAttemptId: input.attemptId,
791
+ };
792
+ await recordUsageEvent(db, {
793
+ ...shared,
794
+ eventType: "code_search.jev_input_tokens",
795
+ quantity: usage.jevInputTokens,
796
+ unit: "tokens",
797
+ idempotencyKey: `usage:code_search.jev_input_tokens:${input.attemptId}:${usage.operationId}`,
798
+ });
799
+ await recordUsageEvent(db, {
800
+ ...shared,
801
+ eventType: "code_search.jev_cost",
802
+ quantity: Math.round(usage.jevCostUsd * 1_000_000),
803
+ unit: "usd_micros",
804
+ idempotencyKey: `usage:code_search.jev_cost:${input.attemptId}:${usage.operationId}`,
805
+ });
806
+ },
807
+ workspace: async () => {
808
+ throwIfWorkerShuttingDown();
809
+ throwIfTurnCancelled();
810
+ const access = await resolveTurnSandboxAccess(
811
+ sandboxState,
812
+ media.sdkOwnedSandboxSession,
813
+ "code_search requires a sandbox or Connected Machine.",
814
+ );
815
+ const machineRoot = sandboxState.machinePrimarySession?.workspaceRoot;
816
+ const runAs = sandboxRunAs(runSettings);
817
+ return codeSearchWorkspaceFromChannel(
818
+ new SandboxChannelAService({
819
+ session: access.session,
820
+ workspaceRoot: machineRoot ?? "/workspace",
821
+ ...(machineRoot ? { providerPathMode: "workspace-relative" as const } : {}),
822
+ leaseEpoch: access.leaseEpoch,
823
+ ...(runAs ? { runAs } : {}),
824
+ }),
825
+ );
826
+ },
827
+ });
765
828
  const attemptToolDefinitions = [
766
829
  ...(operationReadStore
767
830
  ? [
@@ -903,6 +966,7 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
903
966
  ...(googleDrivePublicationTool && googleDrivePublicationAllowed
904
967
  ? [googleDrivePublicationTool]
905
968
  : []),
969
+ ...codeSearchTools,
906
970
  ];
907
971
  recordTurnStartupPhase(observability, {
908
972
  phase: "tool_context_preparation",
@@ -1124,6 +1188,7 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
1124
1188
  ...titleToolPlan.preparationIndependentToolNames,
1125
1189
  "skill_read",
1126
1190
  ],
1191
+ codeSearchAvailable: codeSearchTools.length > 0,
1127
1192
  skillCatalog,
1128
1193
  };
1129
1194
  }
@@ -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,