maka-agent 0.2.0-dev.14.20260902 → 0.2.0-dev.16.20260903

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 (160) hide show
  1. package/dist/cli-core.js +0 -1
  2. package/dist/pi-transcript.js +48 -0
  3. package/dist/pi-tui-form-interaction.js +497 -0
  4. package/dist/pi-tui-pickers.js +256 -55
  5. package/dist/pi-tui-runner.js +215 -12
  6. package/dist/runtime-host-cli.js +17 -42
  7. package/dist/runtime-host-lifecycle-transaction.js +31 -13
  8. package/dist/runtime-host-managed-lifecycle-manager.js +1 -1
  9. package/dist/runtime-host-onboarding.js +3 -1
  10. package/dist/runtime-host-openrc-service.js +469 -0
  11. package/dist/runtime-host-run-command.js +6 -2
  12. package/dist/runtime-host-service-command.js +3 -3
  13. package/dist/runtime-host-service-management-command.js +43 -15
  14. package/dist/runtime-host-session-channel.js +3 -0
  15. package/dist/runtime-host-session-driver.js +21 -6
  16. package/dist/runtime-host-setup-command.js +30 -31
  17. package/dist/tui-context-refresh.js +103 -0
  18. package/dist/tui-copy-catalog.js +73 -0
  19. package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
  20. package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
  21. package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
  22. package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
  23. package/node_modules/@maka/core/dist/chat-model-choice.js +4 -0
  24. package/node_modules/@maka/core/dist/computer-use.js +27 -92
  25. package/node_modules/@maka/core/dist/interaction.js +577 -3
  26. package/node_modules/@maka/core/dist/llm-connections.js +4 -0
  27. package/node_modules/@maka/core/dist/local-memory.js +2 -2
  28. package/node_modules/@maka/core/dist/model-call-usage-projection.js +3 -0
  29. package/node_modules/@maka/core/dist/model-metadata.js +26 -7
  30. package/node_modules/@maka/core/dist/model-thinking.js +17 -0
  31. package/node_modules/@maka/core/dist/provider-registry.js +1 -0
  32. package/node_modules/@maka/core/dist/runtime-event.js +23 -1
  33. package/node_modules/@maka/core/dist/scheduled-task.js +4 -0
  34. package/node_modules/@maka/core/dist/session.js +95 -2
  35. package/node_modules/@maka/core/dist/text-sanitize.js +19 -0
  36. package/node_modules/@maka/core/dist/usage-ledger-merge.js +3 -0
  37. package/node_modules/@maka/core/dist/usage-record-schema.js +11 -5
  38. package/node_modules/@maka/core/dist/workhub-creation-intent.js +75 -9
  39. package/node_modules/@maka/core/dist/workhub-session-resolver.js +50 -0
  40. package/node_modules/@maka/core/package.json +2 -0
  41. package/node_modules/@maka/eval/dist/harbor-external-subject.js +2 -1
  42. package/node_modules/@maka/eval/dist/harness-executor.js +3 -2
  43. package/node_modules/@maka/eval/dist/install-preflight.js +39 -10
  44. package/node_modules/@maka/eval/dist/process-termination.js +83 -0
  45. package/node_modules/@maka/mcp/dist/sep-2243.js +3 -3
  46. package/node_modules/@maka/runtime/dist/agent-run.js +6 -3
  47. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +332 -118
  48. package/node_modules/@maka/runtime/dist/ai-sdk-compaction-contract.js +6 -1
  49. package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +171 -341
  50. package/node_modules/@maka/runtime/dist/bots/telegram-bridge.js +3 -26
  51. package/node_modules/@maka/runtime/dist/bundled-skill-catalog.generated.js +1 -1
  52. package/node_modules/@maka/runtime/dist/computer-use-codec.js +1 -141
  53. package/node_modules/@maka/runtime/dist/computer-use-tools.js +23 -105
  54. package/node_modules/@maka/runtime/dist/context-budget-policy.js +20 -26
  55. package/node_modules/@maka/runtime/dist/context-budget.js +18 -20
  56. package/node_modules/@maka/runtime/dist/conversation-copy.js +23 -19
  57. package/node_modules/@maka/runtime/dist/cua-frame-state.js +1 -69
  58. package/node_modules/@maka/runtime/dist/github-copilot-oauth-enrollment.js +267 -0
  59. package/node_modules/@maka/runtime/dist/history-compact-checkpoint.js +6 -9
  60. package/node_modules/@maka/runtime/dist/history-compact-summarizer.js +67 -23
  61. package/node_modules/@maka/runtime/dist/history-compact-summary-validation.js +11 -18
  62. package/node_modules/@maka/runtime/dist/history-compaction.js +66 -73
  63. package/node_modules/@maka/runtime/dist/interaction-authority.js +69 -0
  64. package/node_modules/@maka/runtime/dist/mcp-tools.js +13 -1
  65. package/node_modules/@maka/runtime/dist/memory-extraction.js +11 -11
  66. package/node_modules/@maka/runtime/dist/model-adapter.js +24 -6
  67. package/node_modules/@maka/runtime/dist/model-factory.js +14 -5
  68. package/node_modules/@maka/runtime/dist/oauth-provider-contracts.js +28 -1
  69. package/node_modules/@maka/runtime/dist/openai-codex-history-compactor.js +7 -6
  70. package/node_modules/@maka/runtime/dist/opencode-session-header.js +29 -0
  71. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +11 -1
  72. package/node_modules/@maka/runtime/dist/runtime-kernel.js +12 -11
  73. package/node_modules/@maka/runtime/dist/runtime-read-model.js +51 -13
  74. package/node_modules/@maka/runtime/dist/scheduled-task-tools.js +3 -0
  75. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +26 -0
  76. package/node_modules/@maka/runtime/dist/session-manager.js +1 -0
  77. package/node_modules/@maka/runtime/dist/session-projection-helpers.js +16 -1
  78. package/node_modules/@maka/runtime/dist/stream-graph-projection.js +6 -0
  79. package/node_modules/@maka/runtime/dist/stream-graph-read-model.js +5 -0
  80. package/node_modules/@maka/runtime/dist/stream-usage-fallback-fetch.js +74 -0
  81. package/node_modules/@maka/runtime/dist/subscription-credentials.js +51 -4
  82. package/node_modules/@maka/runtime/dist/subscription-model-fetch.js +47 -3
  83. package/node_modules/@maka/runtime/dist/system-prompt/main-session-prompt.js +20 -1
  84. package/node_modules/@maka/runtime/dist/test-connection.js +18 -11
  85. package/node_modules/@maka/runtime/dist/tool-runtime.js +183 -0
  86. package/node_modules/@maka/runtime/package.json +1 -0
  87. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +14 -0
  88. package/node_modules/@maka/runtime-host/dist/client/client-capability-channel.js +84 -1
  89. package/node_modules/@maka/runtime-host/dist/client/host-profile.js +68 -51
  90. package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
  91. package/node_modules/@maka/runtime-host/dist/client/owner-connection-code.js +8 -5
  92. package/node_modules/@maka/runtime-host/dist/client/peer-client.js +342 -36
  93. package/node_modules/@maka/runtime-host/dist/client/reconnect-lifecycle.js +60 -4
  94. package/node_modules/@maka/runtime-host/dist/peer-mesh/index.js +3 -3
  95. package/node_modules/@maka/runtime-host/dist/peer-mesh/limits.js +1 -1
  96. package/node_modules/@maka/runtime-host/dist/peer-mesh/model.js +48 -60
  97. package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +847 -431
  98. package/node_modules/@maka/runtime-host/dist/peer-mesh/owner.js +33 -74
  99. package/node_modules/@maka/runtime-host/dist/peer-mesh/store.js +82 -69
  100. package/node_modules/@maka/runtime-host/dist/peer-reachability/index.js +21 -0
  101. package/node_modules/@maka/runtime-host/dist/peer-reachability/model.js +202 -0
  102. package/node_modules/@maka/runtime-host/dist/peer-reachability/owner.js +119 -0
  103. package/node_modules/@maka/runtime-host/dist/peer-reachability/publisher.js +245 -0
  104. package/node_modules/@maka/runtime-host/dist/protocol/agent-graph.js +4 -1
  105. package/node_modules/@maka/runtime-host/dist/protocol/client-capability.js +68 -0
  106. package/node_modules/@maka/runtime-host/dist/protocol/connection-effects.js +15 -8
  107. package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +7 -23
  108. package/node_modules/@maka/runtime-host/dist/protocol/index.js +47 -1
  109. package/node_modules/@maka/runtime-host/dist/protocol/oauth.js +25 -1
  110. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
  111. package/node_modules/@maka/runtime-host/dist/protocol/peer-mesh.js +14 -31
  112. package/node_modules/@maka/runtime-host/dist/protocol/scheduled-task.js +19 -6
  113. package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +36 -1
  114. package/node_modules/@maka/runtime-host/dist/protocol/session-revision.js +11 -2
  115. package/node_modules/@maka/runtime-host/dist/protocol/usage-pricing.js +16 -2
  116. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +51 -2
  117. package/node_modules/@maka/runtime-host/dist/server/access-authority.js +35 -2
  118. package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +1 -0
  119. package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +12 -10
  120. package/node_modules/@maka/runtime-host/dist/server/client-capability-invocation-broker.js +163 -30
  121. package/node_modules/@maka/runtime-host/dist/server/connection-effect-coordinator.js +38 -14
  122. package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +45 -22
  123. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +78 -20
  124. package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +4 -2
  125. package/node_modules/@maka/runtime-host/dist/server/execution-model-composition.js +2 -1
  126. package/node_modules/@maka/runtime-host/dist/server/execution-service.js +49 -28
  127. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +3 -6
  128. package/node_modules/@maka/runtime-host/dist/server/interaction-coordinator.js +130 -27
  129. package/node_modules/@maka/runtime-host/dist/server/interaction-projection.js +30 -1
  130. package/node_modules/@maka/runtime-host/dist/server/listener-set.js +2 -4
  131. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +16 -3
  132. package/node_modules/@maka/runtime-host/dist/server/oauth-coordinator.js +66 -3
  133. package/node_modules/@maka/runtime-host/dist/server/oauth-execution-authority.js +20 -3
  134. package/node_modules/@maka/runtime-host/dist/server/peer-listener.js +8 -21
  135. package/node_modules/@maka/runtime-host/dist/server/peer-mesh-authority.js +1 -1
  136. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +6 -3
  137. package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +72 -12
  138. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +48 -18
  139. package/node_modules/@maka/runtime-host/dist/server/session-turn-access-request-coordinator.js +43 -26
  140. package/node_modules/@maka/runtime-host/dist/server/usage-pricing-coordinator.js +15 -1
  141. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +229 -4
  142. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +146 -8
  143. package/node_modules/@maka/runtime-host/dist/transport/peer-native.js +32 -6
  144. package/node_modules/@maka/runtime-host/package.json +1 -0
  145. package/node_modules/@maka/storage/dist/agent-run-store.js +18 -5
  146. package/node_modules/@maka/storage/dist/codex-session-adapter.js +14 -0
  147. package/node_modules/@maka/storage/dist/execution-stores.js +5 -0
  148. package/node_modules/@maka/storage/dist/interaction-store.js +13 -1
  149. package/node_modules/@maka/storage/dist/runtime-policy/connection-catalog-document.js +9 -3
  150. package/node_modules/@maka/storage/dist/runtime-policy/coordinator.js +49 -19
  151. package/node_modules/@maka/storage/dist/runtime-policy/oauth-login-receipt-document.js +3 -1
  152. package/node_modules/@maka/storage/dist/runtime-policy/onboarding-transaction.js +13 -5
  153. package/node_modules/@maka/storage/dist/session-copy-cleanup.js +4 -2
  154. package/node_modules/@maka/storage/dist/session-store.js +39 -6
  155. package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +16 -9
  156. package/node_modules/@maka/storage/dist/sqlite-session-metadata-schema.js +30 -3
  157. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +117 -0
  158. package/node_modules/@maka/storage/dist/sqlite-usage-store.js +29 -6
  159. package/node_modules/@maka/storage/dist/usage-stores.js +3 -1
  160. package/package.json +1 -1
@@ -35,6 +35,7 @@ import { ToolRuntime, formatSyntheticToolErrorText, formatToolArgsViolationText,
35
35
  import { ModelAdapter, } from './model-adapter.js';
36
36
  import { buildProviderOptions } from './model-factory.js';
37
37
  import { persistedOpenAiResponsesStepMessages } from './openai-responses-continuation.js';
38
+ import { nonCanonicalContentOrder } from './runtime-event-read-model.js';
38
39
  import { composeRequestProjection, } from './request-projection.js';
39
40
  import { decodePlaintextResponsesReasoningState, replayPlaintextResponsesProviderOptions, responsesReasoningItemId, } from './responses-reasoning-state.js';
40
41
  import { toolResultOutput } from './tool-result-output.js';
@@ -58,7 +59,7 @@ import { MEMORY_EXTRACT_TOOL_NAME, MEMORY_REMEMBER_TOOL_NAME, buildMemoryExtract
58
59
  import { modelUsesNativeOpenAiResponses, resolveModelRuntime } from './model-runtime.js';
59
60
  import { applyPatchReplayFactText, normalizeApplyPatchReplayInput, routeApplyPatchTools, } from './apply-patch-profile.js';
60
61
  import { applyRuntimeEventContextBudget, buildContextBudgetDiagnosticShell, estimateRuntimeEventsTokens, mergeContextBudgetDiagnostic, mergeContextBudgetDiagnosticPatches, minimalContextBudgetDiagnostic, shouldAppendContextCompactedNote, shouldAppendContextCompactionFailedOpenNote, } from './context-budget.js';
61
- import { evaluateHistoryCompactCheckpointReplay, isHistoryCompactContentEvent, } from './history-compaction.js';
62
+ import { isHistoryCompactContentEvent } from './history-compaction.js';
62
63
  import { canContinueHistoryCompactCheckpointForModel, historyCompactCheckpointToModelMessage, isProviderHistoryCompactCheckpoint, matchHistoryCompactCheckpointPrefix, projectHistoryCompactCheckpointReplay, } from './history-compact-checkpoint.js';
63
64
  import { resolveSelectedModelContextWindow } from './context-budget-policy.js';
64
65
  export { DEFAULT_PERMISSION_TIMEOUT_MS, MAX_ACTIVE_CHILD_AGENT_RUNS_PER_TURN, MAX_ACTIVE_SUBAGENT_TOOLS_PER_TURN, TOOL_ERROR_RESULT_MAX_CHARS, formatSyntheticToolErrorText, } from './tool-runtime.js';
@@ -439,6 +440,11 @@ const MAX_WAITING_CODE_MODE_CELLS = 1;
439
440
  const MAX_PROVIDER_ATTEMPTS_PER_STEP = 10;
440
441
  const MAX_IDLE_WATCHDOG_RETRIES_PER_STEP = 1;
441
442
  const MAX_INCOMPLETE_STREAM_RETRIES_PER_STEP = 1;
443
+ // A mid-stream cut after partial thinking seals one transcript fragment per
444
+ // retry. A gateway that systematically kills long thinking streams (the
445
+ // 2026-08-28 incident shape) would otherwise spend the full attempt budget
446
+ // accumulating fragments before failing anyway, so fail fast after one.
447
+ const MAX_SEALED_THINKING_RETRIES_PER_STEP = 1;
442
448
  const PROVIDER_RETRY_BASE_DELAY_MS = 1_000;
443
449
  const PROVIDER_RETRY_MAX_DELAY_MS = 32_000;
444
450
  const PROVIDER_RETRY_JITTER_FACTOR = 0.25;
@@ -597,6 +603,15 @@ export class AiSdkBackend {
597
603
  */
598
604
  activeTurns = new Set();
599
605
  compaction;
606
+ /**
607
+ * The provider has been reported dropping context, for this backend.
608
+ *
609
+ * Not per send: the condition persists once a provider starts truncating, so
610
+ * a note on every later turn would repeat one fact the user has already been
611
+ * told. The scope is this backend's lifetime rather than the Session's, so a
612
+ * backend that is disposed and rebuilt may say it once more.
613
+ */
614
+ contextProviderDroppingReported = false;
600
615
  /** Session-scoped running total, deliberately accumulated across turns. */
601
616
  cumulativeUsageCheckpoint;
602
617
  memoryReplayMessageEvents = new WeakMap();
@@ -899,7 +914,12 @@ export class AiSdkBackend {
899
914
  let stepTextPartStartOffset = 0;
900
915
  let stepThinkingParts = [];
901
916
  let stepThinkingPartsById = new Map();
917
+ let stepContentOrder = [];
902
918
  const startedAt = this.now();
919
+ const recordStepContent = (kind) => {
920
+ if (!stepContentOrder.includes(kind))
921
+ stepContentOrder.push(kind);
922
+ };
903
923
  // Flush the current step's AssistantMessage (text + thinking) and the paired
904
924
  // terminal thinking/text events, then clear the per-step accumulators.
905
925
  // Persist when the step produced text OR reasoning — a thinking-only step
@@ -909,12 +929,23 @@ export class AiSdkBackend {
909
929
  // precedes text_complete so the read-model attaches this step's reasoning to
910
930
  // this step's assistant row. Hoisted to send() scope so both the streaming
911
931
  // path and the abort/error handler can flush a partial step.
932
+ const resetStep = () => {
933
+ stepText = '';
934
+ stepTextProviderOptions = undefined;
935
+ stepTextPartStartOffset = 0;
936
+ stepThinkingParts = [];
937
+ stepThinkingPartsById = new Map();
938
+ stepContentOrder = [];
939
+ };
912
940
  const flushStep = async () => {
913
941
  const hasThinking = stepThinkingParts.length > 0;
914
- if (stepText.length === 0 && !hasThinking)
942
+ if (stepText.length === 0 && !hasThinking) {
943
+ resetStep();
915
944
  return;
945
+ }
916
946
  const stepId = currentStepMessageId;
917
947
  const thinkingText = stepThinkingParts.map((part) => part.text).join('');
948
+ const contentOrder = nonCanonicalContentOrder(stepContentOrder);
918
949
  const msg = {
919
950
  type: 'assistant',
920
951
  id: stepId,
@@ -924,6 +955,7 @@ export class AiSdkBackend {
924
955
  ...(stepTextProviderOptions !== undefined
925
956
  ? { providerOptions: stepTextProviderOptions }
926
957
  : {}),
958
+ ...(contentOrder ? { contentOrder } : {}),
927
959
  modelId: this.input.modelId,
928
960
  ...(hasThinking
929
961
  ? {
@@ -976,11 +1008,7 @@ export class AiSdkBackend {
976
1008
  : {}),
977
1009
  });
978
1010
  scope.finalAssistantText = stepText.length > 0 ? stepText : undefined;
979
- stepText = '';
980
- stepTextProviderOptions = undefined;
981
- stepTextPartStartOffset = 0;
982
- stepThinkingParts = [];
983
- stepThinkingPartsById = new Map();
1011
+ resetStep();
984
1012
  };
985
1013
  let tokenUsage;
986
1014
  let tokenUsageCostUsd;
@@ -999,6 +1027,11 @@ export class AiSdkBackend {
999
1027
  // result.usage.inputTokens is cumulative across steps and would produce
1000
1028
  // misleading >100% percentages, so the per-step value is captured here.
1001
1029
  let lastStepInputTokens;
1030
+ /** Tool count of the request that produced `lastStepInputTokens`. */
1031
+ let lastStepActiveToolCount;
1032
+ // Output tokens of the same step: with the input they are the baseline the
1033
+ // next request is judged from (everything the model produced is re-sent).
1034
+ let lastStepOutputTokens;
1002
1035
  let streamStatus = 'success';
1003
1036
  let streamErrorClass;
1004
1037
  let runtimeSteps = 0;
@@ -1006,6 +1039,14 @@ export class AiSdkBackend {
1006
1039
  let contextBudgetForTelemetry;
1007
1040
  let contextCompactedNoteWritten = false;
1008
1041
  let contextCompactionFailedOpenNoteWritten = false;
1042
+ let contextWindowOverrunNoteWritten = false;
1043
+ let contextReportedWindowNoteWritten = false;
1044
+ let contextOverflowAfterCompactionNoteWritten = false;
1045
+ let contextWindowSuggestionNoteWritten = false;
1046
+ // Request index (0-based) at which the active prune last rewrote the
1047
+ // request. A step Maka pruned is not append-only, so usage may legitimately
1048
+ // shrink.
1049
+ let pruneAppliedAtStep;
1009
1050
  const trace = new RunTrace({
1010
1051
  sessionId: this.sessionId,
1011
1052
  turnId,
@@ -1332,22 +1373,19 @@ export class AiSdkBackend {
1332
1373
  // the newest completed step; see collectPrunableCompletedStepToolCallIds.
1333
1374
  const activeToolResultPruneIncludesNewestStep = midTurnState !== undefined;
1334
1375
  const activeToolResultPruneHook = this.compaction.buildActiveToolResultPruneProjection(turnId, activeToolResultPruneIncludesNewestStep, (patch) => {
1376
+ pruneAppliedAtStep = runtimeSteps;
1335
1377
  activeToolResultPruneDiagnosticPatch = mergeActiveToolResultPruneDiagnosticPatches(activeToolResultPruneDiagnosticPatch, patch);
1336
1378
  });
1337
1379
  const shapedProjection = composeRequestProjection(plan.projectActiveTools, midTurnCapacityHook, activeToolResultPruneHook);
1338
- // The verdict owner wraps the WHOLE shaping pipeline: hooks shape, one
1339
- // owner measures the final payload and decides pass/terminate.
1340
- const requestProjection = midTurnState && midTurnCapacityHook && shapedProjection
1341
- ? this.compaction.buildMidTurnFinalRequestRescue({
1342
- shaped: shapedProjection,
1343
- reentry: composeRequestProjection(undefined, midTurnCapacityHook, activeToolResultPruneHook),
1344
- state: midTurnState,
1345
- providerTools,
1346
- charsPerToken: this.input.contextBudget?.charsPerToken ?? 4,
1347
- })
1348
- : shapedProjection;
1380
+ // Hooks shape; nothing measures the final payload. Whether it fits is
1381
+ // the provider's answer (#4559).
1382
+ const requestProjection = shapedProjection;
1349
1383
  const completedProviderSteps = [];
1350
1384
  let requestMessages = messages;
1385
+ // The compaction module runs at most once per send. This tracks the
1386
+ // reactive entry; the proactive one sets the same flag on the mid-turn
1387
+ // state, and each consults the other, so a send that already folded
1388
+ // reports the oversized message instead of folding again (#4559).
1351
1389
  let overflowRetryUsed = false;
1352
1390
  let result;
1353
1391
  let providerOutcome;
@@ -1407,6 +1445,7 @@ export class AiSdkBackend {
1407
1445
  let providerAttempt = 1;
1408
1446
  let idleWatchdogRetryCount = 0;
1409
1447
  let incompleteStreamRetryCount = 0;
1448
+ let sealedThinkingRetryCount = 0;
1410
1449
  const returnedToolCalls = [];
1411
1450
  let providerToolActivityCount = 0;
1412
1451
  const providerToolInputs = new Map();
@@ -1428,7 +1467,13 @@ export class AiSdkBackend {
1428
1467
  !attemptSawToolActivity &&
1429
1468
  !attemptSawContinuationMetadata &&
1430
1469
  !attemptReachedStepBoundary;
1431
- const attemptCanRecoverFromIdleTimeout = () => !attemptSawText &&
1470
+ // Thinking is the only output that can be sealed into its own
1471
+ // message before a retry: flushStep() closes the fragment under
1472
+ // the current message id and the retry streams into a fresh one,
1473
+ // so the user never sees spliced or duplicated content. Text,
1474
+ // tool activity, continuation metadata, and step boundaries stay
1475
+ // non-recoverable for the reasons each of them is tracked.
1476
+ const attemptCanRecoverWithSealedThinking = () => !attemptSawText &&
1432
1477
  !attemptSawToolActivity &&
1433
1478
  !attemptSawContinuationMetadata &&
1434
1479
  !attemptReachedStepBoundary;
@@ -1508,9 +1553,159 @@ export class AiSdkBackend {
1508
1553
  providerStepUsage = stepUsage;
1509
1554
  if (!stepUsage)
1510
1555
  sawUnusableStepUsage = true;
1556
+ // Silent eviction / rewrite check (#4559): this step only
1557
+ // appended (no fold, no prune, no image omission) yet the
1558
+ // provider counted no more input tokens than for the previous
1559
+ // request. Not-greater, not strictly-fewer: a provider that
1560
+ // truncates to a fixed window (Ollama's `num_ctx`) reports the
1561
+ // same total on every later request while Maka keeps
1562
+ // appending, so a plateau is the signal, and an equal count
1563
+ // after an append is already impossible without provider-side
1564
+ // eviction or rewriting. Input against input: the previous
1565
+ // reply's reasoning may not be resent, so input + output is
1566
+ // not the floor of the next input on every wire.
1567
+ const completedRequestIndex = runtimeSteps - 1;
1568
+ // A finalization step resolves an empty tool set, so its
1569
+ // request legitimately drops several thousand schema tokens
1570
+ // with no fold, prune or image omission. Maka shaped that
1571
+ // request; the provider did not drop anything.
1572
+ const toolSchemaShrank = lastStepActiveToolCount !== undefined &&
1573
+ activeToolsForRequest.length < lastStepActiveToolCount;
1574
+ // Across the send boundary the comparison is the same one,
1575
+ // against the last request a provider accepted before this
1576
+ // send. A provider that truncates to a fixed window reports
1577
+ // the same input on every later request while the user keeps
1578
+ // adding turns, and a send of one or two steps never sees
1579
+ // that from the inside: the live evidence plateaus at 3,716
1580
+ // input tokens across eight turns with nothing reported
1581
+ // (#4623). The first request of a send therefore compares
1582
+ // against the persisted anchor, which is route-validated
1583
+ // where it is read; a fold before that request would explain
1584
+ // a smaller input by itself, so it disables the comparison.
1585
+ const acrossSends = completedRequestIndex === 0;
1586
+ const priorInput = acrossSends
1587
+ ? midTurnState?.compactionAppliedThisSend === true
1588
+ ? undefined
1589
+ : midTurnState?.priorAcceptedInputTokens
1590
+ : lastStepInputTokens;
1591
+ if (!this.contextProviderDroppingReported &&
1592
+ !toolSchemaShrank &&
1593
+ midTurnState &&
1594
+ priorInput !== undefined &&
1595
+ midTurnState.replacedStepNumber !== completedRequestIndex &&
1596
+ pruneAppliedAtStep !== completedRequestIndex &&
1597
+ midTurnState.omittedImageToolResults.size === 0 &&
1598
+ stepUsage !== undefined &&
1599
+ Number.isFinite(stepUsage.inputTokens) &&
1600
+ stepUsage.inputTokens > 0 &&
1601
+ // Across sends the test is equality, not "did not grow".
1602
+ // Inside a send Maka knows it only appended, so any
1603
+ // shortfall is the provider's. Across the boundary it does
1604
+ // not: a manual compaction leaves the pre-compaction anchor
1605
+ // behind, a turn can carry a smaller tool set, and a user
1606
+ // can edit or branch history. All three shrink the input
1607
+ // legitimately, and none of them lands on exactly the same
1608
+ // count. A provider truncating to a fixed window does, on
1609
+ // every later request.
1610
+ (acrossSends
1611
+ ? stepUsage.inputTokens === priorInput
1612
+ : stepUsage.inputTokens <= priorInput)) {
1613
+ this.contextProviderDroppingReported = true;
1614
+ const note = {
1615
+ type: 'system_note',
1616
+ id: this.newId(),
1617
+ turnId,
1618
+ ts: this.now(),
1619
+ kind: 'context_provider_dropping',
1620
+ data: { inputTokens: stepUsage.inputTokens, priorInputTokens: priorInput },
1621
+ };
1622
+ await this.input.appendMessage(note).catch(() => { });
1623
+ }
1511
1624
  // Fail closed: reset on every step boundary so a missing final
1512
1625
  // step's usage does not leave a stale value from an earlier step.
1626
+ // The reply needed more room than the declared window had
1627
+ // left after this request's own input. Both halves are the
1628
+ // provider's numbers, read after the fact: the reserve that
1629
+ // should have kept them apart was measured from a smaller
1630
+ // previous reply. Say so once per send; the next request
1631
+ // folds anyway because the baseline now exceeds the window.
1632
+ if (!contextWindowOverrunNoteWritten &&
1633
+ midTurnState?.capacity !== undefined &&
1634
+ stepUsage !== undefined &&
1635
+ Number.isFinite(stepUsage.inputTokens) &&
1636
+ stepUsage.inputTokens > 0 &&
1637
+ Number.isFinite(stepUsage.outputTokens) &&
1638
+ stepUsage.outputTokens > 0 &&
1639
+ stepUsage.inputTokens + stepUsage.outputTokens > midTurnState.capacity) {
1640
+ contextWindowOverrunNoteWritten = true;
1641
+ const note = {
1642
+ type: 'system_note',
1643
+ id: this.newId(),
1644
+ turnId,
1645
+ ts: this.now(),
1646
+ kind: 'context_window_overrun',
1647
+ data: {
1648
+ usedTokens: stepUsage.inputTokens + stepUsage.outputTokens,
1649
+ declaredContextWindow: midTurnState.capacity,
1650
+ },
1651
+ };
1652
+ await this.input.appendMessage(note).catch(() => { });
1653
+ }
1654
+ // Nothing declared, and the provider accepted a request past
1655
+ // the window this model reports. Every other signal in this
1656
+ // design stays dark there: no rejection to recover from, no
1657
+ // plateau to read, and no declaration to arm the proactive
1658
+ // threshold, so the session degrades quietly and
1659
+ // indefinitely (#4634). Report the two real numbers and
1660
+ // leave the decision with the user: a reported window is a
1661
+ // hint, and Maka still declares nothing on their behalf.
1662
+ //
1663
+ // Once per crossing, not once per send. On these providers
1664
+ // usage keeps growing past the line (305K → 322K observed),
1665
+ // so the note fires on the transition: the previous accepted
1666
+ // total was still inside the reported window and this one is
1667
+ // not. The baseline carries that previous total across
1668
+ // sessions through the persisted anchor, so a resumed
1669
+ // session does not repeat a crossing it already reported.
1670
+ if (!contextReportedWindowNoteWritten &&
1671
+ midTurnState !== undefined &&
1672
+ midTurnState.capacity === undefined &&
1673
+ stepUsage !== undefined &&
1674
+ Number.isFinite(stepUsage.inputTokens) &&
1675
+ stepUsage.inputTokens > 0 &&
1676
+ Number.isFinite(stepUsage.outputTokens)) {
1677
+ const reported = resolveSelectedModelContextWindow(this.input.connection, this.input.modelId);
1678
+ const used = stepUsage.inputTokens + Math.max(0, stepUsage.outputTokens);
1679
+ // `baselineTokens` still describes the request before this
1680
+ // one: the capacity hook sets it from the previous step, or
1681
+ // from the persisted anchor on a send's first request.
1682
+ const previousTotal = midTurnState.baselineTokens;
1683
+ const crossedNow = reported !== undefined &&
1684
+ used > reported &&
1685
+ (previousTotal === undefined || previousTotal <= reported);
1686
+ if (reported !== undefined && crossedNow) {
1687
+ contextReportedWindowNoteWritten = true;
1688
+ const note = {
1689
+ type: 'system_note',
1690
+ id: this.newId(),
1691
+ turnId,
1692
+ ts: this.now(),
1693
+ kind: 'context_reported_window_exceeded',
1694
+ data: { usedTokens: used, reportedContextWindow: reported },
1695
+ };
1696
+ await this.input.appendMessage(note).catch(() => { });
1697
+ }
1698
+ }
1513
1699
  lastStepInputTokens = stepUsage?.inputTokens;
1700
+ lastStepOutputTokens = stepUsage?.outputTokens;
1701
+ lastStepActiveToolCount = activeToolsForRequest.length;
1702
+ // A `finishReason: length` is deliberately not a trigger. The
1703
+ // reply may have been cut because the provider ran out of
1704
+ // window room, or because the provider's own output cap is
1705
+ // lower than the one Maka sends. Those are indistinguishable
1706
+ // from outside, and an indistinguishable signal must not
1707
+ // drive an action; the cut reply is visible to the user
1708
+ // either way (#4559).
1514
1709
  if (stepUsage) {
1515
1710
  completedStepUsage = mergeNormalizedUsage(completedStepUsage, stepUsage);
1516
1711
  this.cumulativeUsageCheckpoint = mergeNormalizedUsage(this.cumulativeUsageCheckpoint, stepUsage);
@@ -1522,9 +1717,15 @@ export class AiSdkBackend {
1522
1717
  }
1523
1718
  }
1524
1719
  if (event.kind === 'text-start') {
1720
+ if (stepText.length > 0 && event.providerItemBoundary === true) {
1721
+ await flushStep();
1722
+ currentStepMessageId = this.newId();
1723
+ }
1525
1724
  stepTextPartStartOffset = stepText.length;
1526
1725
  }
1527
1726
  else if (event.kind === 'text') {
1727
+ if (event.text.length > 0)
1728
+ recordStepContent('text');
1528
1729
  stepText += event.text;
1529
1730
  if (event.text.length > 0)
1530
1731
  attemptSawText = true;
@@ -1537,9 +1738,15 @@ export class AiSdkBackend {
1537
1738
  text: event.text,
1538
1739
  });
1539
1740
  }
1540
- else if (event.kind === 'text-metadata') {
1541
- attemptSawContinuationMetadata = true;
1542
- stepTextProviderOptions = mergeTextProviderOptions(stepTextProviderOptions, stripUndefinedDeep(event.providerOptions), stepTextPartStartOffset);
1741
+ else if (event.kind === 'text-end') {
1742
+ if (event.providerOptions !== undefined) {
1743
+ attemptSawContinuationMetadata = true;
1744
+ stepTextProviderOptions = mergeTextProviderOptions(stepTextProviderOptions, stripUndefinedDeep(event.providerOptions), stepTextPartStartOffset);
1745
+ }
1746
+ if (event.providerItemBoundary === true) {
1747
+ await flushStep();
1748
+ currentStepMessageId = this.newId();
1749
+ }
1543
1750
  }
1544
1751
  else if (event.kind === 'thinking-start') {
1545
1752
  if (event.providerOptions !== undefined) {
@@ -1557,6 +1764,8 @@ export class AiSdkBackend {
1557
1764
  }
1558
1765
  }
1559
1766
  else if (event.kind === 'thinking') {
1767
+ if (event.text.length > 0)
1768
+ recordStepContent('thinking');
1560
1769
  if (event.text.length > 0)
1561
1770
  attemptSawThinking = true;
1562
1771
  if (event.providerOptions !== undefined) {
@@ -1638,6 +1847,7 @@ export class AiSdkBackend {
1638
1847
  }
1639
1848
  else if (event.kind === 'tool-call') {
1640
1849
  attemptSawToolActivity = true;
1850
+ recordStepContent('tools');
1641
1851
  if (event.toolCall.providerExecuted) {
1642
1852
  providerToolActivityCount += 1;
1643
1853
  providerToolInputs.set(event.toolCall.toolCallId, event.toolCall.input);
@@ -1732,14 +1942,12 @@ export class AiSdkBackend {
1732
1942
  const recovered = stepBudgetRemains && attemptHasNoObservableOutput()
1733
1943
  ? await this.compaction.recoverFromOverflowError({
1734
1944
  error: attemptFailure,
1735
- retryAlreadyUsed: overflowRetryUsed,
1945
+ retryAlreadyUsed: overflowRetryUsed || (midTurnState?.compactionAttemptedThisSend ?? false),
1736
1946
  midTurnState,
1737
1947
  turnId,
1738
1948
  stepNumber: runtimeSteps,
1739
1949
  currentMessages: attemptMessages,
1740
- providerTools,
1741
1950
  activeTools: activeToolsForRequest,
1742
- systemPromptChars: requestSystemPrompt?.length ?? 0,
1743
1951
  queue,
1744
1952
  onDiagnosticPatch: onMidTurnDiagnosticPatch,
1745
1953
  origin: scope,
@@ -1771,32 +1979,98 @@ export class AiSdkBackend {
1771
1979
  attemptMessages = recoveredProjection?.messages ?? recovered.messages;
1772
1980
  continue;
1773
1981
  }
1982
+ // Window suggestion (#4559): the provider rejected a request and
1983
+ // no recovery is left — the one fold is spent, or there was no
1984
+ // seam. The baseline is a proven-fit total (input + output of an
1985
+ // accepted request), so it is a number the user can declare; the
1986
+ // trigger is `>=`, so declaring exactly it folds before this
1987
+ // point next time. Once per send, and only when the turn is
1988
+ // about to surface the error rather than continue.
1989
+ const acceptedTotal = midTurnState?.lastAcceptedTotalTokens;
1990
+ if (!contextWindowSuggestionNoteWritten &&
1991
+ failure.kind === 'context_overflow' &&
1992
+ midTurnState &&
1993
+ acceptedTotal !== undefined &&
1994
+ (midTurnState.capacity === undefined || acceptedTotal < midTurnState.capacity)) {
1995
+ contextWindowSuggestionNoteWritten = true;
1996
+ const note = {
1997
+ type: 'system_note',
1998
+ id: this.newId(),
1999
+ turnId,
2000
+ ts: this.now(),
2001
+ kind: 'context_window_suggestion',
2002
+ data: {
2003
+ suggestedContextWindow: acceptedTotal,
2004
+ ...(midTurnState.capacity !== undefined
2005
+ ? { declaredContextWindow: midTurnState.capacity }
2006
+ : {}),
2007
+ },
2008
+ };
2009
+ await this.input.appendMessage(note).catch(() => { });
2010
+ }
2011
+ // A folded projection was selected in this send and the provider
2012
+ // still rejects the request. That is worth saying, because the
2013
+ // usual remedy has already been applied; it is NOT proof that the
2014
+ // new message alone is the cause, since what remains also carries
2015
+ // the system prompt, the tool schemas, the summary and the recent
2016
+ // tail. A fold that failed open is deliberately excluded: that
2017
+ // request went out with its full raw history, so nothing about
2018
+ // its size can be concluded (#4559).
2019
+ if (!contextOverflowAfterCompactionNoteWritten &&
2020
+ failure.kind === 'context_overflow' &&
2021
+ midTurnState?.compactionAppliedThisSend === true) {
2022
+ contextOverflowAfterCompactionNoteWritten = true;
2023
+ const note = {
2024
+ type: 'system_note',
2025
+ id: this.newId(),
2026
+ turnId,
2027
+ ts: this.now(),
2028
+ kind: 'context_overflow_after_compaction',
2029
+ };
2030
+ await this.input.appendMessage(note).catch(() => { });
2031
+ }
1774
2032
  const idleWatchdogRecovery = settledWatchdogTimeout?.phase === 'idle' &&
1775
2033
  idleWatchdogRetryCount < MAX_IDLE_WATCHDOG_RETRIES_PER_STEP &&
1776
- attemptCanRecoverFromIdleTimeout();
2034
+ attemptCanRecoverWithSealedThinking();
1777
2035
  const incompleteStreamRecovery = incompleteStreamTerminal &&
1778
2036
  incompleteStreamRetryCount < MAX_INCOMPLETE_STREAM_RETRIES_PER_STEP &&
1779
2037
  incompleteStreamHasNoObservableOutput;
2038
+ // Same seal-and-retry contract as the watchdog path, entered when
2039
+ // the failure arrives as a retryable provider/network error
2040
+ // instead of a local idle timeout. `!idleWatchdogRecovery` keeps
2041
+ // every watchdog-shaped outcome on its existing path, and
2042
+ // `!attemptHasNoObservableOutput()` keeps no-output retries on
2043
+ // the plain budget so this one is spent only on sealed fragments.
2044
+ const sealedThinkingRecovery = !idleWatchdogRecovery &&
2045
+ failure.retryable &&
2046
+ sealedThinkingRetryCount < MAX_SEALED_THINKING_RETRIES_PER_STEP &&
2047
+ attemptCanRecoverWithSealedThinking() &&
2048
+ !attemptHasNoObservableOutput();
1780
2049
  if ((failure.retryable || idleWatchdogRecovery || incompleteStreamRecovery) &&
1781
2050
  failure.kind !== 'context_overflow' &&
1782
2051
  providerAttempt < MAX_PROVIDER_ATTEMPTS_PER_STEP &&
1783
2052
  stepBudgetRemains &&
1784
- (attemptHasNoObservableOutput() || idleWatchdogRecovery || incompleteStreamRecovery)) {
1785
- if (idleWatchdogRecovery) {
2053
+ (attemptHasNoObservableOutput() ||
2054
+ idleWatchdogRecovery ||
2055
+ incompleteStreamRecovery ||
2056
+ sealedThinkingRecovery)) {
2057
+ if (idleWatchdogRecovery)
1786
2058
  idleWatchdogRetryCount += 1;
1787
- if (stepThinkingParts.length > 0) {
1788
- await flushStep();
1789
- currentStepMessageId = this.newId();
1790
- }
1791
- }
2059
+ if (sealedThinkingRecovery)
2060
+ sealedThinkingRetryCount += 1;
1792
2061
  if (incompleteStreamRecovery)
1793
2062
  incompleteStreamRetryCount += 1;
2063
+ if ((idleWatchdogRecovery || sealedThinkingRecovery) &&
2064
+ stepThinkingParts.length > 0) {
2065
+ await flushStep();
2066
+ currentStepMessageId = this.newId();
2067
+ }
1794
2068
  // The failed request did not return authoritative usage. Keep
1795
2069
  // effectiveness recoverable, but fail final metering closed.
1796
2070
  sawUnusableStepUsage = true;
1797
2071
  const delayMs = providerRetryDelayMs(providerAttempt, failure.retryAfterMs);
1798
2072
  const nextAttempt = providerAttempt + 1;
1799
- const maxAttempts = idleWatchdogRecovery || incompleteStreamRecovery
2073
+ const maxAttempts = idleWatchdogRecovery || incompleteStreamRecovery || sealedThinkingRecovery
1800
2074
  ? nextAttempt
1801
2075
  : MAX_PROVIDER_ATTEMPTS_PER_STEP;
1802
2076
  const reason = providerRetryReason(failure.kind);
@@ -2030,13 +2304,16 @@ export class AiSdkBackend {
2030
2304
  }
2031
2305
  return undefined;
2032
2306
  })();
2033
- // The anchor the NEXT turn estimates its first request from — see
2307
+ // The anchor the NEXT turn judges its first request from — see
2034
2308
  // `LastRequestAnchor`. `input` below is the sum across this send's
2035
- // steps and anchors nothing. Either half missing (no usable usage
2036
- // sample, or no mid-turn seam to measure the payload) drops the
2037
- // whole pair and the next turn cold starts.
2309
+ // steps and anchors nothing; the LAST step's real input and output
2310
+ // are what the next request re-sends. No usable input count, no
2311
+ // anchor: the next turn then has no proactive fold until its first
2312
+ // accepted request.
2038
2313
  const anchorInputTokens = finitePositive(lastStepInputTokens);
2039
- const anchorPayloadChars = finitePositive(midTurnState?.lastRequestPayloadChars);
2314
+ const anchorOutputTokens = lastStepOutputTokens !== undefined && Number.isFinite(lastStepOutputTokens)
2315
+ ? Math.max(0, lastStepOutputTokens)
2316
+ : undefined;
2040
2317
  // One shared usage payload for the durable message and the live
2041
2318
  // event: twin per-field literals drifted before (#4019), so a field
2042
2319
  // now has exactly one definition site.
@@ -2065,11 +2342,17 @@ export class AiSdkBackend {
2065
2342
  ? { contextRemaining: contextRemainingForUsage }
2066
2343
  : {}),
2067
2344
  ...(providerRequestTraceId ? { providerRequestTraceId } : {}),
2068
- ...(anchorInputTokens !== undefined && anchorPayloadChars !== undefined
2345
+ ...(anchorInputTokens !== undefined
2069
2346
  ? {
2070
2347
  lastRequestAnchor: {
2071
2348
  inputTokens: anchorInputTokens,
2072
- payloadChars: anchorPayloadChars,
2349
+ ...(anchorOutputTokens !== undefined
2350
+ ? { outputTokens: anchorOutputTokens }
2351
+ : {}),
2352
+ modelId: this.input.modelId,
2353
+ ...(this.input.header.llmConnectionId !== undefined
2354
+ ? { connectionId: this.input.header.llmConnectionId }
2355
+ : {}),
2073
2356
  },
2074
2357
  }
2075
2358
  : {}),
@@ -2604,63 +2887,9 @@ export class AiSdkBackend {
2604
2887
  contextBudgetDiagnostic = mergeContextBudgetDiagnostic(contextBudgetDiagnostic ??
2605
2888
  buildContextBudgetDiagnosticShell(priorRuntimeContext, runtimeContext, contextBudget), preparedContextBudget.diagnosticPatch);
2606
2889
  }
2607
- const maxHistoryTokens = contextBudget?.maxHistoryEstimatedTokens;
2608
- // FALLBACK, not a second authority: step 0's anchored estimate measures the
2609
- // whole outgoing payload against the real window, where this gate only
2610
- // weighs prior history events at chars/4. It stands in only where that
2611
- // estimate cannot reach — no anchor, no mid-turn seam, or no declared
2612
- // window — so an oversized history never goes out unshaped.
2613
- const needsCompaction = !(midTurnState?.capacity !== undefined && midTurnState.lastRequestInputTokens !== undefined) &&
2614
- maxHistoryTokens !== undefined &&
2615
- estimateRuntimeEventsTokens(runtimeContext, contextBudget?.charsPerToken) > maxHistoryTokens;
2616
- if (needsCompaction &&
2617
- contextBudget?.historyCompact?.enabled === true &&
2618
- this.compaction.hasHistoryCompactCheckpointWriter()) {
2619
- const automaticMemoryDecision = automaticMemory
2620
- ? this.automaticMemoryCompactionDecision()
2621
- : undefined;
2622
- const automaticMemorySource = automaticMemoryDecision
2623
- ? lastNonCompactRuntimeEvent(priorRuntimeContext)
2624
- : undefined;
2625
- const compactResult = await this.compaction.compactHistory({
2626
- turnId: input.turnId,
2627
- runId: scope.runId,
2628
- runtimeContext: priorRuntimeContext,
2629
- runtimeContextRunHeaders: input.runtimeContextRunHeaders,
2630
- }, automaticMemorySource
2631
- ? {
2632
- runId: automaticMemorySource.runId,
2633
- turnId: automaticMemorySource.turnId,
2634
- runtimeEventId: automaticMemorySource.id,
2635
- disposition: automaticMemoryDecision.disposition,
2636
- }
2637
- : undefined);
2638
- let durableCheckpoint = compactResult.checkpoint;
2639
- if (!durableCheckpoint && compactResult.outcome.kind === 'unchanged') {
2640
- try {
2641
- durableCheckpoint = await Promise.resolve(this.input.loadHistoryCompactCheckpoint?.());
2642
- }
2643
- catch {
2644
- durableCheckpoint = undefined;
2645
- }
2646
- }
2647
- if (durableCheckpoint) {
2648
- const replay = buildHistoryCompactCheckpointFailOpenContext(durableCheckpoint, priorRuntimeContext, contextBudget, priorRuntimeContext);
2649
- runtimeContext = replay.events;
2650
- projectedHistoryCompactCheckpoint = replay.checkpoint;
2651
- if (replay.checkpoint &&
2652
- compactResult.outcome.kind === 'compacted' &&
2653
- automaticMemoryDecision?.dispatch &&
2654
- automaticMemory) {
2655
- this.dispatchAutomaticMemoryCompaction(scope, {
2656
- checkpoint: replay.checkpoint,
2657
- activeTools: [],
2658
- });
2659
- }
2660
- }
2661
- contextBudgetDiagnostic = mergeContextBudgetDiagnostic(contextBudgetDiagnostic ??
2662
- buildContextBudgetDiagnosticShell(priorRuntimeContext, runtimeContext, contextBudget), compactResult.contextBudget ?? {});
2663
- }
2890
+ // No pre-turn estimate gate: the turn's first request is judged by the
2891
+ // request-projection hook from the previous request's real usage, and by
2892
+ // the provider when it goes out (#4559).
2664
2893
  // The boundary belongs to the runtime-event projection above. A gate that
2665
2894
  // falls back to the stored-message projection returns a prompt no
2666
2895
  // checkpoint shaped, so it reports none rather than one the request never
@@ -3811,28 +4040,13 @@ function buildHistoryCompactCheckpointFailOpenContext(checkpoint, priorRuntimeCo
3811
4040
  byTurn.set(event.turnId, [event]);
3812
4041
  }
3813
4042
  }
3814
- const maxTokens = policy.maxHistoryEstimatedTokens ?? Number.POSITIVE_INFINITY;
3815
- const replayPrefix = projectHistoryCompactCheckpointReplay(checkpoint, match.coveredRuntimeEvents, []);
3816
- let selectedTokens = checkpoint.version === 3
3817
- ? checkpoint.estimatedTokens
3818
- : estimateRuntimeEventsTokens(replayPrefix, charsPerToken);
3819
- const selectedGroups = [];
3820
- for (let index = turnOrder.length - 1; index >= 0; index -= 1) {
3821
- const group = byTurn.get(turnOrder[index]) ?? [];
3822
- const groupTokens = estimateRuntimeEventsTokens(group, charsPerToken);
3823
- if (selectedTokens + groupTokens > maxTokens)
3824
- break;
3825
- selectedGroups.unshift(group);
3826
- selectedTokens += groupTokens;
3827
- }
4043
+ // Replay is structural: the checkpoint plus everything after its boundary.
4044
+ // No size-based selection stands between them and dispatch; whether the
4045
+ // result fits is the provider's answer (#4559).
4046
+ const selectedGroups = turnOrder.map((turnId) => byTurn.get(turnId) ?? []);
3828
4047
  const replayTail = selectedGroups.flat();
3829
4048
  const replayEvents = projectHistoryCompactCheckpointReplay(checkpoint, match.coveredRuntimeEvents, replayTail);
3830
- const replayTailForFit = checkpoint.version === 3 ? replayEvents : replayEvents.slice(1);
3831
- return evaluateHistoryCompactCheckpointReplay(checkpoint, replayTailForFit, policy?.charsPerToken, policy?.maxHistoryEstimatedTokens, {
3832
- sourceReplayEvents: [...match.coveredRuntimeEvents, ...replayTail],
3833
- }).fits
3834
- ? { events: replayEvents, checkpoint }
3835
- : { events: [...retainedCandidates] };
4049
+ return { events: replayEvents, checkpoint };
3836
4050
  }
3837
4051
  function projectMemoryConversationPrefix(messages, eventMessagePositions) {
3838
4052
  // Context visibility and durable evidence authority are separate boundaries.