@opengeni/worker-bundle 1.0.0-canary.0 → 1.0.0

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 (244) hide show
  1. package/dist/activities/agent-run-admission.d.ts +3 -1
  2. package/dist/activities/agent-turn/admission.d.ts +24 -3
  3. package/dist/activities/agent-turn/agent-build.d.ts +4 -0
  4. package/dist/activities/agent-turn/claude-usage-observer.d.ts +58 -0
  5. package/dist/activities/agent-turn/code-search.d.ts +60 -0
  6. package/dist/activities/agent-turn/codex-capacity.d.ts +13 -0
  7. package/dist/activities/agent-turn/codex-plan-entitlement.d.ts +105 -0
  8. package/dist/activities/agent-turn/codex.d.ts +3 -2
  9. package/dist/activities/agent-turn/compaction-prep.d.ts +21 -5
  10. package/dist/activities/agent-turn/credential-leases.d.ts +19 -33
  11. package/dist/activities/agent-turn/credential-restriction.d.ts +3 -0
  12. package/dist/activities/agent-turn/errors.d.ts +82 -11
  13. package/dist/activities/agent-turn/failure-settlement.d.ts +6 -3
  14. package/dist/activities/agent-turn/final-reply.d.ts +18 -0
  15. package/dist/activities/agent-turn/finalization-monitor.d.ts +17 -0
  16. package/dist/activities/agent-turn/finalization.d.ts +4 -2
  17. package/dist/activities/agent-turn/github-repository-bindings.d.ts +2 -0
  18. package/dist/activities/agent-turn/governance-model.d.ts +9 -2
  19. package/dist/activities/agent-turn/history-sink.d.ts +2 -0
  20. package/dist/activities/agent-turn/history.d.ts +2 -0
  21. package/dist/activities/agent-turn/input-wait-reply.d.ts +41 -0
  22. package/dist/activities/agent-turn/media-artifacts.d.ts +5 -3
  23. package/dist/activities/agent-turn/model-call-admission.d.ts +15 -0
  24. package/dist/activities/agent-turn/model-usage.d.ts +6 -0
  25. package/dist/activities/agent-turn/model-wire-dispatch.d.ts +15 -0
  26. package/dist/activities/agent-turn/optional-repositories.d.ts +59 -0
  27. package/dist/activities/agent-turn/provider-dispatch-barrier.d.ts +12 -1
  28. package/dist/activities/agent-turn/quiescence.d.ts +7 -7
  29. package/dist/activities/agent-turn/recovery-warning.d.ts +9 -0
  30. package/dist/activities/agent-turn/refresh-credentials.d.ts +6 -0
  31. package/dist/activities/agent-turn/run-credentials.d.ts +8 -3
  32. package/dist/activities/agent-turn/sandbox-establish.d.ts +2 -1
  33. package/dist/activities/agent-turn/sandbox-provision.d.ts +3 -1
  34. package/dist/activities/agent-turn/sandbox-route.d.ts +3 -20
  35. package/dist/activities/agent-turn/sandbox-runtime.d.ts +2 -2
  36. package/dist/activities/agent-turn/session-title.d.ts +34 -2
  37. package/dist/activities/agent-turn/skill-checkout.d.ts +25 -6
  38. package/dist/activities/agent-turn/skill-read.d.ts +70 -2
  39. package/dist/activities/agent-turn/skill-remove.d.ts +14 -0
  40. package/dist/activities/agent-turn/skill-search.d.ts +3 -1
  41. package/dist/activities/agent-turn/skill-tools.d.ts +15 -2
  42. package/dist/activities/agent-turn/skill-transfer.d.ts +18 -6
  43. package/dist/activities/agent-turn/stream-attempt.d.ts +7 -3
  44. package/dist/activities/agent-turn/subscription-lease.d.ts +38 -0
  45. package/dist/activities/agent-turn/tool-environment.d.ts +56 -6
  46. package/dist/activities/agent-turn/tool-policy.d.ts +43 -0
  47. package/dist/activities/agent-turn/turn-context.d.ts +29 -0
  48. package/dist/activities/agent-turn/turn-sandbox-access.d.ts +3 -0
  49. package/dist/activities/agent-turn/xai-capacity.d.ts +2 -1
  50. package/dist/activities/automations.d.ts +3 -1
  51. package/dist/activities/browser-deadline-checkpoint.d.ts +29 -0
  52. package/dist/activities/codemode-dispatcher.d.ts +9 -2
  53. package/dist/activities/codex-capacity.d.ts +18 -2
  54. package/dist/activities/codex-rotation.d.ts +7 -1
  55. package/dist/activities/context-compaction.d.ts +16 -2
  56. package/dist/activities/gmail-files.d.ts +32 -0
  57. package/dist/activities/goals.d.ts +8 -0
  58. package/dist/activities/knowledge-indexing.d.ts +19 -0
  59. package/dist/activities/mcp-account-routes.d.ts +3 -0
  60. package/dist/activities/mcp-credentials.d.ts +12 -13
  61. package/dist/activities/mcp-operation-authority.d.ts +0 -5
  62. package/dist/activities/model-call-fact-reconciler.d.ts +34 -0
  63. package/dist/activities/open-suffix-resume.d.ts +13 -0
  64. package/dist/activities/parent-wake.d.ts +6 -12
  65. package/dist/activities/retained-screenshots.d.ts +7 -2
  66. package/dist/activities/rig-verification.d.ts +2 -3
  67. package/dist/activities/run-credential-renewal.d.ts +1 -0
  68. package/dist/activities/run-credentials.d.ts +15 -3
  69. package/dist/activities/sandbox-images.d.ts +1 -1
  70. package/dist/activities/sandbox-lease.d.ts +47 -9
  71. package/dist/activities/scheduled-tasks.d.ts +4 -35
  72. package/dist/activities/session-state.d.ts +70 -3
  73. package/dist/activities/types.d.ts +55 -6
  74. package/dist/activities/workspace-capture.d.ts +2 -0
  75. package/dist/activities/workspace-credential-provider.d.ts +46 -0
  76. package/dist/{activities-control-HMFGSKC5.js → activities-control-BNPTRTOE.js} +1896 -418
  77. package/dist/activities-control-BNPTRTOE.js.map +1 -0
  78. package/dist/activities-control.d.ts +182 -4
  79. package/dist/{activities-turn-AP67JADS.js → activities-turn-5LGZOSVU.js} +4476 -1327
  80. package/dist/activities-turn-5LGZOSVU.js.map +1 -0
  81. package/dist/activities.d.ts +182 -4
  82. package/dist/artifact-materializer-entry.js +5 -2
  83. package/dist/artifact-materializer-entry.js.map +1 -1
  84. package/dist/artifact-outbox-entry.js +5 -2
  85. package/dist/artifact-outbox-entry.js.map +1 -1
  86. package/dist/browser-deadline-checkpoint-contract.d.ts +2 -0
  87. package/dist/{chunk-A4XQWVZK.js → chunk-3ZTRWDMN.js} +307 -4
  88. package/dist/chunk-3ZTRWDMN.js.map +1 -0
  89. package/dist/{chunk-RAMNYSAW.js → chunk-4ER3YXOT.js} +22 -4
  90. package/dist/chunk-4ER3YXOT.js.map +1 -0
  91. package/dist/{chunk-CYITU5UH.js → chunk-LJX7Z2KP.js} +1445 -343
  92. package/dist/chunk-LJX7Z2KP.js.map +1 -0
  93. package/dist/editable-artifact-materializer-service.d.ts +2 -2
  94. package/dist/editable-artifact-outbox-service.d.ts +2 -2
  95. package/dist/host-export-pump.d.ts +3 -1
  96. package/dist/host-export.d.ts +1 -1
  97. package/dist/host-export.js +1 -1
  98. package/dist/http.d.ts +1 -1
  99. package/dist/index.d.ts +95 -3
  100. package/dist/index.js +137 -43
  101. package/dist/index.js.map +1 -1
  102. package/dist/observability-metrics.d.ts +118 -8
  103. package/dist/retained-process-diagnostics.d.ts +4 -0
  104. package/dist/retained-process-retry.d.ts +7 -0
  105. package/dist/sandbox-resume.d.ts +55 -13
  106. package/dist/sandbox-routing.d.ts +5 -3
  107. package/dist/sandbox-snapshot-diagnostics.d.ts +20 -0
  108. package/dist/session-event-fanout.d.ts +6 -0
  109. package/dist/warm-capture-holder.d.ts +3 -0
  110. package/dist/worker-service-lifecycle.d.ts +12 -0
  111. package/dist/workflow-bundle.js +273 -41
  112. package/dist/workflows/activities.d.ts +1 -1
  113. package/dist/workflows/browser-deadline-checkpoint.d.ts +9 -0
  114. package/dist/workflows/scheduled-tasks.d.ts +4 -0
  115. package/dist/workflows/session.d.ts +36 -0
  116. package/dist/workflows.d.ts +1 -0
  117. package/package.json +21 -19
  118. package/src/activities/agent-run-admission.ts +17 -1
  119. package/src/activities/agent-turn/admission.ts +66 -3
  120. package/src/activities/agent-turn/agent-build.ts +84 -10
  121. package/src/activities/agent-turn/claim.ts +186 -43
  122. package/src/activities/agent-turn/claude-usage-observer.ts +221 -0
  123. package/src/activities/agent-turn/code-search.ts +275 -0
  124. package/src/activities/agent-turn/codex-capacity.ts +172 -8
  125. package/src/activities/agent-turn/codex-plan-entitlement.ts +254 -0
  126. package/src/activities/agent-turn/codex.ts +13 -5
  127. package/src/activities/agent-turn/compaction-prep.ts +170 -102
  128. package/src/activities/agent-turn/credential-leases.ts +122 -216
  129. package/src/activities/agent-turn/credential-restriction.ts +14 -0
  130. package/src/activities/agent-turn/errors.ts +647 -48
  131. package/src/activities/agent-turn/failure-settlement.ts +682 -130
  132. package/src/activities/agent-turn/final-reply.ts +46 -0
  133. package/src/activities/agent-turn/finalization-monitor.ts +120 -0
  134. package/src/activities/agent-turn/finalization.ts +146 -43
  135. package/src/activities/agent-turn/github-repository-bindings.ts +3 -1
  136. package/src/activities/agent-turn/governance-model.ts +69 -28
  137. package/src/activities/agent-turn/history-sink.ts +16 -2
  138. package/src/activities/agent-turn/history.ts +38 -11
  139. package/src/activities/agent-turn/input-wait-reply.ts +103 -0
  140. package/src/activities/agent-turn/knowledge-source-tools.ts +2 -0
  141. package/src/activities/agent-turn/media-artifacts.ts +2 -1
  142. package/src/activities/agent-turn/model-call-admission.ts +94 -0
  143. package/src/activities/agent-turn/model-usage.ts +64 -3
  144. package/src/activities/agent-turn/model-wire-dispatch.ts +27 -0
  145. package/src/activities/agent-turn/optional-repositories.ts +224 -0
  146. package/src/activities/agent-turn/provider-dispatch-barrier.ts +22 -0
  147. package/src/activities/agent-turn/quiescence.ts +10 -7
  148. package/src/activities/agent-turn/recovery-warning.ts +16 -0
  149. package/src/activities/agent-turn/refresh-credentials.ts +44 -0
  150. package/src/activities/agent-turn/run-credentials.ts +116 -55
  151. package/src/activities/agent-turn/run.ts +320 -93
  152. package/src/activities/agent-turn/sandbox-establish.ts +84 -8
  153. package/src/activities/agent-turn/sandbox-provision.ts +3 -1
  154. package/src/activities/agent-turn/sandbox-route.ts +4 -21
  155. package/src/activities/agent-turn/sandbox-runtime.ts +343 -18
  156. package/src/activities/agent-turn/session-title.ts +81 -5
  157. package/src/activities/agent-turn/skill-checkout.ts +111 -24
  158. package/src/activities/agent-turn/skill-install.ts +1 -1
  159. package/src/activities/agent-turn/skill-read.ts +374 -28
  160. package/src/activities/agent-turn/skill-remove.ts +46 -0
  161. package/src/activities/agent-turn/skill-save.ts +1 -1
  162. package/src/activities/agent-turn/skill-search.ts +10 -1
  163. package/src/activities/agent-turn/skill-selection.ts +4 -0
  164. package/src/activities/agent-turn/skill-tools.ts +112 -18
  165. package/src/activities/agent-turn/skill-transfer.ts +68 -23
  166. package/src/activities/agent-turn/stream-attempt.ts +313 -77
  167. package/src/activities/agent-turn/subscription-lease.ts +115 -0
  168. package/src/activities/agent-turn/tool-environment.ts +316 -167
  169. package/src/activities/agent-turn/tool-policy.ts +80 -1
  170. package/src/activities/agent-turn/turn-context.ts +35 -0
  171. package/src/activities/agent-turn/turn-sandbox-access.ts +7 -1
  172. package/src/activities/agent-turn/xai-capacity.ts +94 -51
  173. package/src/activities/automations.ts +76 -4
  174. package/src/activities/browser-deadline-checkpoint.ts +279 -0
  175. package/src/activities/capabilities.ts +108 -11
  176. package/src/activities/codemode-dispatcher.ts +36 -8
  177. package/src/activities/codex-capacity.ts +60 -25
  178. package/src/activities/codex-rotation.ts +39 -17
  179. package/src/activities/context-compaction.ts +75 -18
  180. package/src/activities/gmail-files.ts +88 -0
  181. package/src/activities/goals.ts +116 -121
  182. package/src/activities/google-drive-publication.ts +1 -3
  183. package/src/activities/image-generation-operation.ts +1 -1
  184. package/src/activities/knowledge-indexing.ts +269 -15
  185. package/src/activities/knowledge-source-sync.ts +10 -0
  186. package/src/activities/mcp-account-routes.ts +17 -0
  187. package/src/activities/mcp-credentials.ts +83 -194
  188. package/src/activities/mcp-operation-authority.ts +16 -41
  189. package/src/activities/model-call-fact-reconciler.ts +112 -0
  190. package/src/activities/open-suffix-resume.ts +15 -2
  191. package/src/activities/parent-wake.ts +46 -21
  192. package/src/activities/retained-screenshots.ts +82 -11
  193. package/src/activities/rig-verification.ts +38 -37
  194. package/src/activities/run-credential-renewal.ts +13 -1
  195. package/src/activities/run-credentials.ts +59 -17
  196. package/src/activities/run-input.ts +25 -16
  197. package/src/activities/sandbox-images.ts +0 -1
  198. package/src/activities/sandbox-lease.ts +798 -82
  199. package/src/activities/scheduled-tasks.ts +550 -178
  200. package/src/activities/session-state.ts +245 -6
  201. package/src/activities/site-auth-maintenance.ts +3 -0
  202. package/src/activities/types.ts +72 -5
  203. package/src/activities/workflow-wake.ts +2 -17
  204. package/src/activities/workspace-capture.ts +13 -2
  205. package/src/activities/workspace-credential-provider.ts +283 -0
  206. package/src/activities-control.ts +4 -0
  207. package/src/activities-turn.ts +5 -0
  208. package/src/activities.ts +13 -0
  209. package/src/activity-services.ts +16 -4
  210. package/src/browser-deadline-checkpoint-contract.ts +5 -0
  211. package/src/editable-artifact-hint-broker.ts +3 -0
  212. package/src/editable-artifact-materializer-service.ts +7 -3
  213. package/src/editable-artifact-outbox-service.ts +4 -4
  214. package/src/github-rest-mcp.ts +1 -2
  215. package/src/host-export-pump.ts +26 -4
  216. package/src/host-export.ts +3 -0
  217. package/src/http.ts +4 -2
  218. package/src/index.ts +99 -37
  219. package/src/observability-metrics.ts +510 -5
  220. package/src/personal-github-git-credentials.ts +2 -1
  221. package/src/pr-review-credentials.ts +1 -3
  222. package/src/retained-process-diagnostics.ts +50 -0
  223. package/src/retained-process-retry.ts +36 -0
  224. package/src/sandbox-resume.ts +388 -101
  225. package/src/sandbox-routing.ts +150 -25
  226. package/src/sandbox-snapshot-diagnostics.ts +112 -0
  227. package/src/session-event-fanout.ts +23 -0
  228. package/src/warm-capture-holder.ts +15 -0
  229. package/src/worker-service-lifecycle.ts +53 -0
  230. package/src/workflows/activities.ts +16 -1
  231. package/src/workflows/browser-deadline-checkpoint.ts +50 -0
  232. package/src/workflows/file-upload-reaper.ts +2 -0
  233. package/src/workflows/scheduled-tasks.ts +26 -13
  234. package/src/workflows/session.ts +242 -22
  235. package/src/workflows.ts +4 -0
  236. package/dist/activities/agent-turn/knowledge-sources.d.ts +0 -20
  237. package/dist/activities/packs.d.ts +0 -55
  238. package/dist/activities-control-HMFGSKC5.js.map +0 -1
  239. package/dist/activities-turn-AP67JADS.js.map +0 -1
  240. package/dist/chunk-A4XQWVZK.js.map +0 -1
  241. package/dist/chunk-CYITU5UH.js.map +0 -1
  242. package/dist/chunk-RAMNYSAW.js.map +0 -1
  243. package/src/activities/agent-turn/knowledge-sources.ts +0 -86
  244. package/src/activities/packs.ts +0 -166
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { bindMcpTelemetry, measureMcpPhase, withMcpCallIdentity } from "@opengeni/observability";
2
3
  import {
3
4
  AttemptToolApprovalRequiredError,
4
5
  AttemptToolCatalogStaleError,
@@ -16,6 +17,8 @@ import {
16
17
  CODEMODE_MAX_CONCURRENT_CALLS_PER_ATTEMPT,
17
18
  type CodemodeOperation,
18
19
  type SessionEvent,
20
+ type ToolDisplayMetadata,
21
+ type ToolFamily,
19
22
  } from "@opengeni/contracts";
20
23
  import {
21
24
  cancelQueuedCodemodeOperationsForAttempt,
@@ -70,6 +73,9 @@ export class CodemodeAttemptDispatcher {
70
73
  private readonly turnSignal?: AbortSignal,
71
74
  private readonly maxConcurrentCalls = CODEMODE_MAX_CONCURRENT_CALLS_PER_ATTEMPT,
72
75
  timings: CodemodeDispatcherTimings = {},
76
+ private readonly toolDisplayMetadata?: (modelName: string) => ToolDisplayMetadata | undefined,
77
+ /** Content-free analytics family for a catalog entry; event enrichment only. */
78
+ private readonly toolFamily?: (entry: AttemptToolCatalogEntry) => ToolFamily | null,
73
79
  ) {
74
80
  if (
75
81
  environment.catalog.accountId !== scope.accountId ||
@@ -107,7 +113,7 @@ export class CodemodeAttemptDispatcher {
107
113
  throw new Error("Codemode dispatcher cannot be restarted");
108
114
  this.unsubscribe = this.bus.subscribeRequests(
109
115
  codemodeDispatchSubject(this.scope.workspaceId, this.scope.attemptId),
110
- async (payload) => {
116
+ bindMcpTelemetry(async (payload) => {
111
117
  const request = decodeCodemodeDispatchRequest(payload);
112
118
  if (request.catalogDigest !== this.environment.catalog.digest || this.closing) {
113
119
  return encodeCodemodeDispatchAck({
@@ -136,11 +142,11 @@ export class CodemodeAttemptDispatcher {
136
142
  claimLeaseMs: this.claimLeaseMs,
137
143
  });
138
144
  if (claim.status === "claimed") {
139
- const execution = this.execute(claim.operation, claim.claimId, claim.reclaimed).finally(
140
- () => {
141
- this.inFlight.delete(request.operationId);
142
- },
143
- );
145
+ const execution = withMcpCallIdentity(claim.operation.operationId, () =>
146
+ this.execute(claim.operation, claim.claimId, claim.reclaimed),
147
+ ).finally(() => {
148
+ this.inFlight.delete(request.operationId);
149
+ });
144
150
  this.inFlight.set(request.operationId, execution);
145
151
  return encodeCodemodeDispatchAck({
146
152
  version: 1,
@@ -169,7 +175,7 @@ export class CodemodeAttemptDispatcher {
169
175
  } finally {
170
176
  this.pendingClaims -= 1;
171
177
  }
172
- },
178
+ }),
173
179
  );
174
180
  }
175
181
 
@@ -248,7 +254,11 @@ export class CodemodeAttemptDispatcher {
248
254
  );
249
255
  return;
250
256
  }
251
- if (!(await this.ensureToolCallCreated(operation, entry, reclaimed))) {
257
+ if (
258
+ !(await measureMcpPhase("event_persistence", () =>
259
+ this.ensureToolCallCreated(operation, entry, reclaimed),
260
+ ))
261
+ ) {
252
262
  await this.failBeforeExecution(
253
263
  operation.operationId,
254
264
  claimId,
@@ -351,6 +361,7 @@ export class CodemodeAttemptDispatcher {
351
361
  return true;
352
362
  }
353
363
  }
364
+ const toolFamily = this.toolFamily?.(entry) ?? null;
354
365
  try {
355
366
  const created = await appendAndPublishTurnEventsFenced(
356
367
  this.db,
@@ -371,9 +382,14 @@ export class CodemodeAttemptDispatcher {
371
382
  payload: {
372
383
  id: operation.operationId,
373
384
  name: entry.modelName,
385
+ display: this.toolDisplayMetadata?.(entry.modelName) ?? {
386
+ toolName: entry.identity.toolName,
387
+ ...(entry.title ? { title: entry.title } : {}),
388
+ },
374
389
  arguments: operation.arguments,
375
390
  origin: "codemode",
376
391
  subjectId: operation.caller.subjectId,
392
+ ...(toolFamily ? { toolFamily } : {}),
377
393
  raw: {
378
394
  type: "codemode_call",
379
395
  serverId: operation.identity.serverId,
@@ -411,6 +427,18 @@ export class CodemodeAttemptDispatcher {
411
427
  errorCode: string;
412
428
  errorMessage: string;
413
429
  },
430
+ ): Promise<void> {
431
+ return withMcpCallIdentity(operation.operationId, () =>
432
+ measureMcpPhase("event_persistence", () =>
433
+ this.persistSettlement(operation, claimId, settlement),
434
+ ),
435
+ );
436
+ }
437
+
438
+ private async persistSettlement(
439
+ operation: CodemodeOperation,
440
+ claimId: string,
441
+ settlement: Parameters<CodemodeAttemptDispatcher["settleWithOutput"]>[2],
414
442
  ): Promise<void> {
415
443
  const result = await settleCodemodeOperationWithOutput(this.db, {
416
444
  ...this.scope,
@@ -3,6 +3,9 @@ import {
3
3
  fetchCodexUsageForAccount,
4
4
  getCodexCapacityWaitForSession,
5
5
  getXaiCapacityWaitForSession,
6
+ getClaudeCapacityWaitForSession,
7
+ resolveClaudeWaiterSubject,
8
+ reconcileClaudeCapacityWait as reconcileClaudeCapacityWaitDb,
6
9
  resolveXaiWaiterSubject,
7
10
  listCodexAccountStatuses,
8
11
  listPendingCodexCapacityWakeTargets,
@@ -16,6 +19,7 @@ import { refreshExhaustedXaiQuota } from "./xai-quota";
16
19
  import {
17
20
  authoritativeCodexCapacityResetAt,
18
21
  codexAccountNeedsLiveCapacityRefresh,
22
+ codexAccountServesModel,
19
23
  isCodexCredentialEligible,
20
24
  isCodexCredentialHealthy,
21
25
  selectCodexCredentialLeaseForTurn,
@@ -90,8 +94,14 @@ export async function signalPendingCodexCapacityWakeTargets(
90
94
 
91
95
  export function codexCapacityDecision<TPolicyScope = never, TUnavailableDiagnostic = never>(
92
96
  context: CodexCapacitySelectionContext<TPolicyScope, TUnavailableDiagnostic>,
97
+ now = new Date(),
93
98
  ): ReturnType<Parameters<typeof reconcileCodexCapacityWaitDb>[2]> {
94
- const now = new Date();
99
+ context = {
100
+ ...context,
101
+ accounts: context.accounts.filter(
102
+ (account) => !context.modelId || codexAccountServesModel(account, context.modelId, now),
103
+ ),
104
+ };
95
105
  const selected = selectCodexCredentialLeaseForTurn({
96
106
  context,
97
107
  sessionId: context.sessionId,
@@ -114,8 +124,12 @@ export function codexCapacityDecision<TPolicyScope = never, TUnavailableDiagnost
114
124
  credentialId: selected.credentialId,
115
125
  diagnostic: {
116
126
  connectedCount: context.accounts.length,
117
- eligibleCount: context.accounts.filter((account) => isCodexCredentialEligible(account, now))
118
- .length,
127
+ eligibleCount: context.accounts.filter(
128
+ (account) =>
129
+ (account.id === selected.credentialId ||
130
+ !context.failedCredentialIds?.includes(account.id)) &&
131
+ isCodexCredentialEligible(account, now),
132
+ ).length,
119
133
  },
120
134
  };
121
135
  }
@@ -134,8 +148,7 @@ export function codexCapacityDecision<TPolicyScope = never, TUnavailableDiagnost
134
148
  account.status === "active" &&
135
149
  account.allocatorEnabled &&
136
150
  account.exhaustedKind === "quota" &&
137
- account.exhaustedUntil !== null &&
138
- account.exhaustedUntil > now,
151
+ account.exhaustedUntil !== null,
139
152
  );
140
153
  const policyAccount = capacityAccounts[0] ?? null;
141
154
  const mutationOnlyStatusBlock =
@@ -162,7 +175,7 @@ export function codexCapacityDecision<TPolicyScope = never, TUnavailableDiagnost
162
175
  kind: "unavailable",
163
176
  earliestResetAt: authoritativeReset,
164
177
  resetKind:
165
- selected.decision.kind === "none" ||
178
+ (selected.decision.kind === "none" && !hasReconcilableQuotaCooldown && !authoritativeReset) ||
166
179
  selected.decision.kind === "allocatorDisabled" ||
167
180
  mutationOnlyStatusBlock
168
181
  ? "mutation_only"
@@ -191,6 +204,16 @@ export async function armAndReconcileCodexCapacityWait(
191
204
  options: { onArmed?: () => void } = {},
192
205
  ) {
193
206
  const armed = await armCodexCapacityWait(services.db, input);
207
+ if (armed.action === "stopped") {
208
+ options.onArmed?.();
209
+ await publishDurableSessionEvents(
210
+ services.bus,
211
+ input.workspaceId,
212
+ input.sessionId,
213
+ armed.events,
214
+ );
215
+ return armed;
216
+ }
194
217
  if (armed.action !== "waiting") return armed;
195
218
 
196
219
  options.onArmed?.();
@@ -219,8 +242,9 @@ export async function armAndReconcileCodexCapacityWait(
219
242
  async function refreshCapacityMetadata(
220
243
  services: ControlActivityServices,
221
244
  workspaceId: string,
245
+ turnId: string,
222
246
  ): Promise<void> {
223
- const accounts = await listCodexAccountStatuses(services.db, workspaceId).catch(() => []);
247
+ const accounts = await listCodexAccountStatuses(services.db, workspaceId, turnId).catch(() => []);
224
248
  const now = new Date();
225
249
  const stale = accounts.filter(
226
250
  (account) =>
@@ -231,7 +255,14 @@ async function refreshCapacityMetadata(
231
255
  await refreshCodexUsageAndRepairCapacityWaiters(
232
256
  stale.map(
233
257
  (account) => () =>
234
- fetchCodexUsageForAccount(services.db, services.settings, workspaceId, account.id),
258
+ fetchCodexUsageForAccount(
259
+ services.db,
260
+ services.settings,
261
+ workspaceId,
262
+ account.id,
263
+ undefined,
264
+ turnId,
265
+ ),
235
266
  ),
236
267
  () => signalPendingCodexCapacityWakeTargets(services, workspaceId),
237
268
  );
@@ -248,10 +279,18 @@ export function createCodexCapacityActivities(services: () => Promise<ControlAct
248
279
  const xaiWaiter = codexWaiter
249
280
  ? null
250
281
  : await getXaiCapacityWaitForSession(db, input.workspaceId, input.sessionId);
251
- const waiter = codexWaiter ?? xaiWaiter;
282
+ const claudeWaiter =
283
+ codexWaiter || xaiWaiter
284
+ ? null
285
+ : await getClaudeCapacityWaitForSession(db, input.workspaceId, input.sessionId);
286
+ const waiter = codexWaiter ?? xaiWaiter ?? claudeWaiter;
252
287
  return waiter
253
288
  ? {
254
- ...(xaiWaiter ? { provider: "xai" as const } : {}),
289
+ ...(xaiWaiter
290
+ ? { provider: "xai" as const }
291
+ : claudeWaiter
292
+ ? { provider: "claude" as const }
293
+ : {}),
255
294
  waiterId: waiter.id,
256
295
  generation: waiter.generation,
257
296
  // A capacity mutation may have committed while its Temporal signal
@@ -271,21 +310,17 @@ export function createCodexCapacityActivities(services: () => Promise<ControlAct
271
310
  input: ReconcileCodexCapacityWaitInput,
272
311
  ): Promise<ReconcileCodexCapacityWaitResult> {
273
312
  const resolved = await services();
274
- if (input.provider === "xai") {
275
- const current = await getXaiCapacityWaitForSession(
276
- resolved.db,
277
- input.workspaceId,
278
- input.sessionId,
279
- );
313
+ if (input.provider === "xai" || input.provider === "claude") {
314
+ const claude = input.provider === "claude";
315
+ const getWaiter = claude ? getClaudeCapacityWaitForSession : getXaiCapacityWaitForSession;
316
+ const resolveAuthority = claude ? resolveClaudeWaiterSubject : resolveXaiWaiterSubject;
317
+ const reconcile = claude ? reconcileClaudeCapacityWaitDb : reconcileXaiCapacityWaitDb;
318
+ const current = await getWaiter(resolved.db, input.workspaceId, input.sessionId);
280
319
  if (!current || current.id !== input.waiterId || current.generation !== input.generation) {
281
320
  return { action: "stale" };
282
321
  }
283
- const authority = await resolveXaiWaiterSubject(
284
- resolved.db,
285
- input.workspaceId,
286
- input.sessionId,
287
- );
288
- if (authority)
322
+ const authority = await resolveAuthority(resolved.db, input.workspaceId, input.sessionId);
323
+ if (authority && !claude)
289
324
  await refreshExhaustedXaiQuota({
290
325
  db: resolved.db,
291
326
  settings: resolved.settings,
@@ -296,7 +331,7 @@ export function createCodexCapacityActivities(services: () => Promise<ControlAct
296
331
  subjectId: authority.subjectId,
297
332
  authoritySnapshot: authority.snapshot,
298
333
  });
299
- const result = await reconcileXaiCapacityWaitDb(resolved.db, {
334
+ const result = await reconcile(resolved.db, {
300
335
  accountId: input.accountId,
301
336
  workspaceId: input.workspaceId,
302
337
  sessionId: input.sessionId,
@@ -314,7 +349,7 @@ export function createCodexCapacityActivities(services: () => Promise<ControlAct
314
349
  if (result.action === "waiting") {
315
350
  return {
316
351
  action: "waiting",
317
- provider: "xai",
352
+ provider: input.provider,
318
353
  waiterId: result.waiter.id,
319
354
  generation: result.waiter.generation,
320
355
  nextCheckAt: result.waiter.nextCheckAt.toISOString(),
@@ -338,7 +373,7 @@ export function createCodexCapacityActivities(services: () => Promise<ControlAct
338
373
  if (boundedRefreshAttempted) {
339
374
  // This is a bounded secret-safe control-plane quota refresh. It creates no
340
375
  // turn, model call, user message, schedule, or entitlement action.
341
- await refreshCapacityMetadata(resolved, input.workspaceId);
376
+ await refreshCapacityMetadata(resolved, input.workspaceId, current.blockedTurnId);
342
377
  }
343
378
  const result = await reconcileCodexCapacityWaitDb(
344
379
  resolved.db,
@@ -1,3 +1,4 @@
1
+ import { subscriptionAccountShardIndex } from "@opengeni/config";
1
2
  // Multi-account P3 — the PURE rotation ranker. Zero I/O: no provider calls, no
2
3
  // decrypts, no db. It consumes the already-loaded, metadata-only account list
3
4
  // (cached usage columns + the exhausted_until cooldown column) and returns the
@@ -12,23 +13,38 @@ import type {
12
13
  CodexLeaseAccountStatus,
13
14
  CodexPinSource,
14
15
  } from "@opengeni/db";
16
+ import { codexPlanExcludesModel, connectionModelAllowed } from "@opengeni/db";
15
17
 
16
18
  export type CodexRotationAccount = CodexAccountStatus | CodexLeaseAccountStatus;
17
19
 
20
+ /**
21
+ * Model-scoped allocation filter: user connection policy plus proven plan
22
+ * entitlement. A plan exclusion applies only until it expires (one request
23
+ * then re-probes the account) or a different plan is observed.
24
+ */
25
+ export function codexAccountServesModel(
26
+ account: Pick<CodexRotationAccount, "allowedModelIds" | "planType" | "planEntitlementExclusion">,
27
+ modelId: string,
28
+ now: Date,
29
+ ): boolean {
30
+ return (
31
+ connectionModelAllowed(account.allowedModelIds, modelId) &&
32
+ !codexPlanExcludesModel(account, modelId, now)
33
+ );
34
+ }
35
+
18
36
  /** Capacity metadata worth an authoritative live read before/all through an idle. */
19
37
  export function codexAccountNeedsLiveCapacityRefresh(
20
38
  account: Pick<
21
39
  CodexAccountStatus,
22
40
  "primaryUsedPercent" | "secondaryUsedPercent" | "exhaustedUntil" | "exhaustedKind"
23
41
  >,
24
- now: Date,
42
+ _now: Date,
25
43
  ): boolean {
26
44
  return (
27
45
  (account.primaryUsedPercent ?? 0) >= CODEX_USAGE_EXHAUSTED_PCT ||
28
46
  (account.secondaryUsedPercent ?? 0) >= CODEX_USAGE_EXHAUSTED_PCT ||
29
- (account.exhaustedKind === "quota" &&
30
- account.exhaustedUntil !== null &&
31
- account.exhaustedUntil > now)
47
+ (account.exhaustedKind === "quota" && account.exhaustedUntil !== null)
32
48
  );
33
49
  }
34
50
 
@@ -208,17 +224,6 @@ export function isCodexAccountEligible(acct: CodexRotationAccount, now: Date): b
208
224
  return isCodexCredentialEligible(acct, now);
209
225
  }
210
226
 
211
- /** Deterministic 32-bit FNV-1a over a UTF-16 code-unit stream. Pure, allocation-free. */
212
- function fnv1a32(input: string): number {
213
- let hash = 0x811c9dc5;
214
- for (let i = 0; i < input.length; i++) {
215
- hash ^= input.charCodeAt(i);
216
- // 32-bit FNV prime multiply via Math.imul; `>>> 0` keeps it unsigned.
217
- hash = Math.imul(hash, 0x01000193);
218
- }
219
- return hash >>> 0;
220
- }
221
-
222
227
  /**
223
228
  * Session-sharded HOME account (AM-6): the deterministic account a session runs on
224
229
  * under the "sharded" strategy — `stableAccountList[ hash(sessionId) % N ]` over the
@@ -248,7 +253,7 @@ export function shardCredentialForSession(args: {
248
253
  if (eligibles.length === 0) {
249
254
  return null;
250
255
  }
251
- const index = fnv1a32(sessionId) % eligibles.length;
256
+ const index = subscriptionAccountShardIndex(sessionId, eligibles.length);
252
257
  return eligibles[index]!.id;
253
258
  }
254
259
 
@@ -524,8 +529,19 @@ export function selectCodexCredentialLeaseForTurn<
524
529
  sessionLastCredentialId: string | null;
525
530
  now: Date;
526
531
  }): CodexTurnLeaseSelection {
527
- const { accounts, activeCredentialId, rotationEnabled, rotationStrategy, existingCredentialId } =
532
+ const { activeCredentialId, rotationEnabled, rotationStrategy, existingCredentialId } =
528
533
  args.context;
534
+ if (args.context.failoverExhausted) {
535
+ return { credentialId: null, decision: { kind: "none" }, advanceActivePointer: false };
536
+ }
537
+ // User model policy and proven plan entitlement both remove an account for
538
+ // this model only. A plan exclusion is provider truth, never user policy; it
539
+ // becomes inert when a different plan is observed or its TTL elapses.
540
+ const accounts = args.context.modelId
541
+ ? args.context.accounts.filter((account) =>
542
+ codexAccountServesModel(account, args.context.modelId!, args.now),
543
+ )
544
+ : args.context.accounts;
529
545
  const failedCredentialIds = new Set(args.context.failedCredentialIds ?? []);
530
546
  // Normalized: rotation-enabled always behaves as sharded (see
531
547
  // effectiveRotationStrategy); the stored value is only ever legacy residue.
@@ -564,6 +580,9 @@ export function selectCodexCredentialLeaseForTurn<
564
580
  if (!pinned) {
565
581
  return { credentialId: null, decision: { kind: "none" }, advanceActivePointer: false };
566
582
  }
583
+ if (failedCredentialIds.has(pinned.id)) {
584
+ return { credentialId: null, decision: { kind: "none" }, advanceActivePointer: false };
585
+ }
567
586
  if (!pinned.allocatorEnabled) {
568
587
  return {
569
588
  credentialId: null,
@@ -633,6 +652,9 @@ export function selectCodexCredentialLeaseForTurn<
633
652
  if (!active) {
634
653
  return { credentialId: null, decision: { kind: "none" }, advanceActivePointer: false };
635
654
  }
655
+ if (failedCredentialIds.has(active.id)) {
656
+ return { credentialId: null, decision: { kind: "none" }, advanceActivePointer: false };
657
+ }
636
658
  if (!active.allocatorEnabled) {
637
659
  return {
638
660
  credentialId: null,
@@ -1,3 +1,4 @@
1
+ import { readReasoningConfiguration } from "@opengeni/codex";
1
2
  import {
2
3
  applyContextCompaction,
3
4
  getActiveSessionHistoryItemsPaged,
@@ -8,7 +9,8 @@ import {
8
9
  import {
9
10
  EmptyCompactionSummaryError,
10
11
  REMOTE_COMPACTION_V2_IMPLEMENTATION,
11
- SUMMARY_BUFFER_TOKENS,
12
+ compactionSummaryOutputTokens,
13
+ buildSummaryItem,
12
14
  buildCompactionReplacementHistory,
13
15
  buildRemoteV2ReplacementHistory,
14
16
  compactionThresholdTokens,
@@ -21,6 +23,7 @@ import {
21
23
  sanitizeHistoryItemsForModel,
22
24
  summarizeForCompaction,
23
25
  type CompactionItem,
26
+ type CompactionProviderRejection,
24
27
  } from "@opengeni/runtime";
25
28
  import { contextInputBudgetTokens, type Settings } from "@opengeni/config";
26
29
  import type { SessionEvent } from "@opengeni/contracts";
@@ -56,7 +59,13 @@ export type MaybeCompactResult =
56
59
  * Codex, call Codex `/codex/responses` with `compaction_trigger` and persist the
57
60
  * opaque compaction item. Fail closed — never silently fall back to portable.
58
61
  */
59
- 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
+ };
60
69
 
61
70
  /** Returns the opaque Codex remote compaction v2 item. */
62
71
  export type RemoteCompactionV2Requester = (
@@ -79,7 +88,7 @@ export async function maybeCompactContext(
79
88
  // Injectable for tests; defaults to the real provider-aware model call.
80
89
  summarize: CompactionSummarizer = (s, m) =>
81
90
  summarizeForCompaction(s, m, {
82
- maxOutputTokens: SUMMARY_BUFFER_TOKENS,
91
+ maxOutputTokens: compactionSummaryOutputTokens(s.contextWindowTokens),
83
92
  }),
84
93
  // Operator-forced (the /compact command): bypass the budget trigger and
85
94
  // compact now if there is anything to summarize. Structural guards still hold.
@@ -260,6 +269,12 @@ export async function settleFailedContextCompactionLandmark(
260
269
  options: {
261
270
  clearRequestedCompaction?: boolean;
262
271
  publishLiveEvents?: (events: SessionEvent[]) => Promise<void>;
272
+ /**
273
+ * Closed identifier record of a definitive provider rejection. Carried on
274
+ * the visible `compaction.skipped` landmark so the timeline can name the
275
+ * rejected field instead of offering a retry that cannot succeed.
276
+ */
277
+ providerRejection?: CompactionProviderRejection | null;
263
278
  } = {},
264
279
  ): Promise<Extract<MaybeCompactResult, { compacted: false }>> {
265
280
  const settled = await settleSkippedAfterStart(db, scope, options, "summarization_failed");
@@ -287,10 +302,12 @@ async function settleSkippedAfterStart(
287
302
  },
288
303
  options: {
289
304
  clearRequestedCompaction?: boolean;
305
+ providerRejection?: CompactionProviderRejection | null;
290
306
  },
291
307
  reason:
292
308
  | "no_history"
293
309
  | "replacement_not_smaller"
310
+ | "replacement_exceeds_model_budget"
294
311
  | "replacement_unchanged"
295
312
  | "summarization_failed",
296
313
  ): Promise<Extract<MaybeCompactResult, { compacted: false }>> {
@@ -304,6 +321,7 @@ async function settleSkippedAfterStart(
304
321
  // an operator `/compact` flag — auto/overflow never set one.
305
322
  requirePendingRequest: false,
306
323
  clearRequestedCompaction,
324
+ ...(options.providerRejection ? { providerRejection: options.providerRejection } : {}),
307
325
  });
308
326
  if (!skipped.recorded) {
309
327
  throw new TurnAttemptFencedError(
@@ -370,7 +388,10 @@ async function compactContextRemoteV2(
370
388
  );
371
389
  const estimatedTokensAfter = estimateTokens(await projectForWire(replacementHistory));
372
390
  const replacementFingerprint = compactionReplacementFingerprint(replacementHistory);
373
- const tailItem = replacementHistory.at(-1);
391
+ const summaryIndex =
392
+ replacementHistory.length -
393
+ (readReasoningConfiguration(replacementHistory.at(-1) ?? {}) ? 2 : 1);
394
+ const tailItem = replacementHistory[summaryIndex];
374
395
  if (!tailItem) {
375
396
  throw new EmptyCompactionSummaryError({
376
397
  stage: "remote_v2_replacement",
@@ -384,7 +405,10 @@ async function compactContextRemoteV2(
384
405
  turnId: scope.turnId,
385
406
  expectedExecutionGeneration: scope.executionGeneration,
386
407
  expectedAttemptId: scope.attemptId,
387
- replacementItems: replacementHistory.slice(0, -1),
408
+ replacementItems: replacementHistory.slice(0, summaryIndex),
409
+ ...(replacementHistory.length > summaryIndex + 1
410
+ ? { trailingItems: replacementHistory.slice(summaryIndex + 1) }
411
+ : {}),
388
412
  summaryItem: tailItem as Record<string, unknown>,
389
413
  ...(options.clearRequestedCompaction ? { clearRequestedCompaction: true } : {}),
390
414
  eventPayload: {
@@ -439,15 +463,29 @@ async function compactContextPortable(
439
463
  const summarized = await summarizeWithCodexOverflowTrimming(summarize, settings, items);
440
464
  const summaryBody = summarized.summaryBody;
441
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)]);
442
473
  const replacementHistory = buildCompactionReplacementHistory(
443
474
  canonicalItems,
444
475
  summaryBody,
445
476
  (item) => retainedTokens.get(item) ?? estimateTokens([item]),
477
+ Math.min(outputReserve, Math.max(0, structuralBudget - prefixTokens - summaryTokens)),
446
478
  );
447
479
  const estimatedTokensAfter = estimateTokens(await projectForWire(replacementHistory));
480
+ if (estimatedTokensAfter + prefixTokens > structuralBudget) {
481
+ return await settleSkippedAfterStart(db, scope, options, "replacement_exceeds_model_budget");
482
+ }
448
483
  const replacementFingerprint = compactionReplacementFingerprint(replacementHistory);
449
484
  const previousReplacementFingerprint = latestCompactionReplacementFingerprint(canonicalItems);
450
- const summaryItem = replacementHistory.at(-1);
485
+ const summaryIndex =
486
+ replacementHistory.length -
487
+ (readReasoningConfiguration(replacementHistory.at(-1) ?? {}) ? 2 : 1);
488
+ const summaryItem = replacementHistory[summaryIndex];
451
489
  if (!summaryItem) {
452
490
  // Started already fanout; settle visibly so the landmark cannot stick on
453
491
  // "Compacting…". This is not an operator-request clear path.
@@ -466,7 +504,10 @@ async function compactContextPortable(
466
504
  turnId: scope.turnId,
467
505
  expectedExecutionGeneration: scope.executionGeneration,
468
506
  expectedAttemptId: scope.attemptId,
469
- replacementItems: replacementHistory.slice(0, -1),
507
+ replacementItems: replacementHistory.slice(0, summaryIndex),
508
+ ...(replacementHistory.length > summaryIndex + 1
509
+ ? { trailingItems: replacementHistory.slice(summaryIndex + 1) }
510
+ : {}),
470
511
  summaryItem: summaryItem as Record<string, unknown>,
471
512
  ...(options.clearRequestedCompaction ? { clearRequestedCompaction: true } : {}),
472
513
  eventPayload: {
@@ -498,7 +539,7 @@ async function compactContextPortable(
498
539
  };
499
540
  }
500
541
 
501
- async function summarizeWithCodexOverflowTrimming(
542
+ export async function summarizeWithCodexOverflowTrimming(
502
543
  summarize: CompactionSummarizer,
503
544
  settings: Settings,
504
545
  activeHistory: CompactionItem[],
@@ -509,17 +550,32 @@ async function summarizeWithCodexOverflowTrimming(
509
550
  }> {
510
551
  // Codex's estimator is intentionally coarse. Keep the explicit checkpoint
511
552
  // request below both the effective input window and the raw window minus the
512
- // requested summary, then leave 15% estimator headroom. This changes only the
513
- // temporary summarizer input; durable active history remains untouched until
514
- // applyContextCompaction succeeds under the attempt fence.
515
- 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
+ );
516
560
  const configuredInputBudget = contextInputBudgetTokens(settings);
517
561
  const structuralBudget = Math.min(
518
562
  configuredInputBudget > 0 ? configuredInputBudget : summaryAwareBudget,
519
563
  summaryAwareBudget,
520
564
  );
521
- 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);
522
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();
523
579
  try {
524
580
  return {
525
581
  summaryBody: await summarize(settings, preparation.input),
@@ -529,15 +585,16 @@ async function summarizeWithCodexOverflowTrimming(
529
585
  } catch (error) {
530
586
  if (!isContextWindowExceeded(error)) throw error;
531
587
  // The provider is more authoritative than the byte/4 estimate. Refit once
532
- // 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;
533
589
  // then fail terminally with prior history intact. Never issue one failing
534
- // request per oldest item. The production incident proved that the
535
- // provider can count slightly more than twice the byte/4 estimate, so a
536
- // 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.
537
593
  const retryBudget = Math.floor(
538
- Math.min(initialBudget * 0.5, preparation.estimatedInputTokens * 0.5),
594
+ Math.min(initialBudget * 0.4, preparation.estimatedInputTokens * 0.4),
539
595
  );
540
596
  preparation = prepareCompactionPromptInput(activeHistory, retryBudget);
597
+ requireHistory();
541
598
  return {
542
599
  summaryBody: await summarize(settings, preparation.input),
543
600
  preparation,