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

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 (105) hide show
  1. package/THIRD_PARTY_NOTICES.txt +2 -2
  2. package/dist/acp/maka-acp-agent.js +6 -3
  3. package/dist/acp/session-registry.js +307 -0
  4. package/dist/acp/stdio-server.js +20 -1
  5. package/dist/cli-core.js +15 -2
  6. package/dist/pi-transcript.js +1 -1
  7. package/dist/pi-tui-layout.js +1 -1
  8. package/dist/pi-tui-mcp-status.js +1 -1
  9. package/dist/pi-tui-runner.js +1 -1
  10. package/dist/runtime-host-access-command.js +37 -4
  11. package/dist/runtime-host-capability-provider-command.js +16 -2
  12. package/dist/runtime-host-cli-context.js +73 -3
  13. package/dist/runtime-host-cli.js +37 -6
  14. package/dist/runtime-host-run-command.js +3 -1
  15. package/dist/runtime-host-update-command.js +59 -31
  16. package/dist/tui-copy-catalog.js +4 -2
  17. package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
  18. package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
  19. package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
  20. package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
  21. package/node_modules/@maka/core/dist/attachments.js +12 -0
  22. package/node_modules/@maka/core/dist/connection-error-copy.js +17 -34
  23. package/node_modules/@maka/core/dist/events.js +0 -13
  24. package/node_modules/@maka/core/dist/maka-wordmark.js +23 -0
  25. package/node_modules/@maka/core/dist/model-catalog.js +22 -1
  26. package/node_modules/@maka/core/dist/runtime-event.js +1 -0
  27. package/node_modules/@maka/core/dist/runtime-policy/connection-catalog-codec.js +50 -1
  28. package/node_modules/@maka/core/dist/runtime-policy/credential-vault-codec.js +9 -1
  29. package/node_modules/@maka/core/dist/runtime-policy/model-catalog-entry-codec.js +10 -1
  30. package/node_modules/@maka/core/dist/runtime-policy/policy-codec.js +75 -0
  31. package/node_modules/@maka/core/dist/runtime-policy.js +4 -2
  32. package/node_modules/@maka/core/dist/session.js +1 -0
  33. package/node_modules/@maka/core/dist/settings.js +10 -2
  34. package/node_modules/@maka/core/dist/usage-record-schema.js +11 -1
  35. package/node_modules/@maka/core/package.json +2 -1
  36. package/node_modules/@maka/mcp/dist/index.js +1 -0
  37. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +52 -80
  38. package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +178 -140
  39. package/node_modules/@maka/runtime/dist/archive-read-tool.js +26 -5
  40. package/node_modules/@maka/runtime/dist/builtin-tools.js +27 -2
  41. package/node_modules/@maka/runtime/dist/context-budget-policy.js +0 -12
  42. package/node_modules/@maka/runtime/dist/conversation-copy.js +66 -43
  43. package/node_modules/@maka/runtime/dist/durable-tool-result-projection.js +31 -0
  44. package/node_modules/@maka/runtime/dist/history-compact-error.js +10 -2
  45. package/node_modules/@maka/runtime/dist/history-compact-input-fit.js +7 -1
  46. package/node_modules/@maka/runtime/dist/history-compaction.js +13 -22
  47. package/node_modules/@maka/runtime/dist/model-history.js +41 -16
  48. package/node_modules/@maka/runtime/dist/provider-image-overflow-recovery.js +12 -31
  49. package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +3 -0
  50. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +13 -2
  51. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +3 -6
  52. package/node_modules/@maka/runtime/dist/shell-tools.js +77 -1
  53. package/node_modules/@maka/runtime/dist/tool-availability.js +74 -5
  54. package/node_modules/@maka/runtime/dist/tool-result-archive-resource.js +333 -52
  55. package/node_modules/@maka/runtime/dist/tool-result-archive-transition.js +34 -3
  56. package/node_modules/@maka/runtime/dist/tool-runtime.js +12 -0
  57. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +0 -3
  58. package/node_modules/@maka/runtime-host/dist/client/catalog-reader.js +40 -13
  59. package/node_modules/@maka/runtime-host/dist/client/connection.js +50 -31
  60. package/node_modules/@maka/runtime-host/dist/client/host-profile.js +48 -4
  61. package/node_modules/@maka/runtime-host/dist/client/index.js +2 -2
  62. package/node_modules/@maka/runtime-host/dist/client/owner-connection-code.js +32 -1
  63. package/node_modules/@maka/runtime-host/dist/client/peer-client.js +22 -4
  64. package/node_modules/@maka/runtime-host/dist/operator/access-management-frame.js +10 -1
  65. package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +56 -12
  66. package/node_modules/@maka/runtime-host/dist/peer-mesh/owner.js +3 -0
  67. package/node_modules/@maka/runtime-host/dist/protocol/configuration.js +55 -9
  68. package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +37 -0
  69. package/node_modules/@maka/runtime-host/dist/protocol/index.js +17 -1
  70. package/node_modules/@maka/runtime-host/dist/protocol/network-proxy.js +1 -7
  71. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +2 -0
  72. package/node_modules/@maka/runtime-host/dist/protocol/runtime-policy.js +48 -1
  73. package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +4 -2
  74. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +2 -12
  75. package/node_modules/@maka/runtime-host/dist/server/access-authority.js +2 -1
  76. package/node_modules/@maka/runtime-host/dist/server/agent-graph-execution-coordinator.js +1 -2
  77. package/node_modules/@maka/runtime-host/dist/server/canonical-turn-snapshot.js +0 -9
  78. package/node_modules/@maka/runtime-host/dist/server/configuration-coordinator.js +20 -1
  79. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +46 -24
  80. package/node_modules/@maka/runtime-host/dist/server/execution-service.js +4 -1
  81. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +15 -0
  82. package/node_modules/@maka/runtime-host/dist/server/listener-set.js +8 -4
  83. package/node_modules/@maka/runtime-host/dist/server/network-proxy-coordinator.js +0 -1
  84. package/node_modules/@maka/runtime-host/dist/server/peer-listener.js +3 -0
  85. package/node_modules/@maka/runtime-host/dist/server/runtime-policy-coordinator.js +28 -1
  86. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +3 -3
  87. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +35 -3
  88. package/node_modules/@maka/runtime-host/dist/server/session-sidecar-purge.js +16 -0
  89. package/node_modules/@maka/storage/dist/runtime-policy/coordinator.js +189 -6
  90. package/node_modules/@maka/storage/dist/runtime-policy-stores.js +4 -1
  91. package/node_modules/@maka/storage/dist/settings-store.js +16 -25
  92. package/node_modules/fast-uri/index.js +215 -47
  93. package/node_modules/fast-uri/lib/schemes.js +9 -4
  94. package/node_modules/fast-uri/lib/utils.js +396 -92
  95. package/node_modules/fast-uri/package.json +1 -1
  96. package/node_modules/qs/.github/THREAT_MODEL.md +3 -3
  97. package/node_modules/qs/CHANGELOG.md +15 -0
  98. package/node_modules/qs/README.md +19 -1
  99. package/node_modules/qs/dist/qs.js +21 -21
  100. package/node_modules/qs/eslint.config.mjs +1 -1
  101. package/node_modules/qs/lib/parse.js +3 -4
  102. package/node_modules/qs/lib/stringify.js +23 -8
  103. package/node_modules/qs/lib/utils.js +10 -3
  104. package/node_modules/qs/package.json +4 -4
  105. package/package.json +1 -1
@@ -29,8 +29,9 @@ import { baseToolResultProjection, reduceEffectiveModelProjections, } from './mo
29
29
  import { buildRuntimeEventModelReplayPlan, collectToolActivityTurnIds, compatibleProviderReasoningReplayEventIds, } from './model-history.js';
30
30
  import { toolSchemaCharsForDiagnostics } from './request-shape.js';
31
31
  import { estimateNextRequestTokens, exceedsHighWater, planHistoryCompaction, } from './history-compaction.js';
32
- import { resolveContextBudgetCapacity, } from './context-budget-policy.js';
33
- import { collectHistoricalImageToolResults, omitHistoricalImageToolResults, } from './provider-image-overflow-recovery.js';
32
+ import { resolveSelectedModelContextWindow } from './context-budget-policy.js';
33
+ import { MATERIALIZED_IMAGE_TOKENS } from '@maka/core/attachments';
34
+ import { collectHistoricalImageToolResults, isInlineImageFilePart, omitHistoricalImageToolResults, } from './provider-image-overflow-recovery.js';
34
35
  export class AiSdkCompaction {
35
36
  input;
36
37
  sessionId;
@@ -532,7 +533,9 @@ export class AiSdkCompaction {
532
533
  * Mid-turn capacity compaction eligibility (issue #882 PR 1). Explicit
533
534
  * opt-in via `historyCompact.midTurn.enabled`; requires the checkpoint
534
535
  * writer seams plus the durable turn-ledger read, the persisted head anchor
535
- * for this turn, and a bounded capacity window.
536
+ * for this turn. A model that declares no context window still gets this
537
+ * state: reactive recovery runs off a real provider rejection and needs no
538
+ * window at all.
536
539
  */
537
540
  buildMidTurnCapacityCompactState(input) {
538
541
  const policy = this.input.contextBudget;
@@ -556,13 +559,19 @@ export class AiSdkCompaction {
556
559
  !isHistoryCompactContentEvent(headAnchor)) {
557
560
  return undefined;
558
561
  }
559
- const capacity = resolveContextBudgetCapacity(this.input.connection, this.input.modelId, policy);
560
- if (capacity === undefined)
561
- return undefined;
562
562
  const priorContentEvents = (input.runtimeContext ?? [])
563
563
  .filter((event) => event.turnId !== input.turnId)
564
564
  .filter(isHistoryCompactContentEvent);
565
- return new MidTurnCapacityCompactState(headAnchor, priorContentEvents, input.runtimeContextRunHeaders ?? [], capacity);
565
+ const state = new MidTurnCapacityCompactState(headAnchor, priorContentEvents, input.runtimeContextRunHeaders ?? [], resolveSelectedModelContextWindow(this.input.connection, this.input.modelId));
566
+ // Seed the turn's FIRST request with the last request the provider
567
+ // actually counted, so step 0 is estimated like every later step instead
568
+ // of guessing the whole payload at char/4.
569
+ const persisted = persistedRequestAnchor(input.runtimeContext ?? [], state.priorRunHeaders, this.input.modelId, this.targetConnectionId);
570
+ if (persisted) {
571
+ state.lastRequestInputTokens = persisted.inputTokens;
572
+ state.lastRequestPayloadChars = persisted.payloadChars;
573
+ }
574
+ return state;
566
575
  }
567
576
  /**
568
577
  * Request-projection stage for the mid-turn capacity invariant: between
@@ -573,15 +582,12 @@ export class AiSdkCompaction {
573
582
  * `[compact block, verbatim head anchor]`.
574
583
  *
575
584
  * This hook never terminates the turn: every failure fails open with a
576
- * diagnostic and records itself for the final-request estimate owner, which
577
- * re-measures the payload after ALL shaping (including active tool-result
578
- * pruning, which runs later and can still rescue the step) and issues the
579
- * context_budget_exhausted verdict only when the request that would really
580
- * go out exceeds the window. The trigger threshold here is deliberately
581
- * approximate — a missed or spurious trigger is recoverable; the verdict is
582
- * not, so it does not live here.
585
+ * diagnostic and the request goes out. The trigger threshold is approximate
586
+ * on purpose — a missed or spurious trigger costs at most one compaction,
587
+ * and whether the request actually fits is the provider's answer, not a
588
+ * local estimate's.
583
589
  */
584
- buildMidTurnCapacityCompactProjection(turnId, state, queue, providerTools, fallbackActiveTools, systemPromptChars, onDiagnosticPatch, origin, memoryCompactionDecision, onMemoryCompaction, abortSignal) {
590
+ buildMidTurnCapacityCompactProjection(turnId, state, queue, providerTools, onDiagnosticPatch, origin, memoryCompactionDecision, onMemoryCompaction, abortSignal) {
585
591
  if (!state)
586
592
  return undefined;
587
593
  const policy = this.input.contextBudget;
@@ -596,10 +602,6 @@ export class AiSdkCompaction {
596
602
  projectedMessages =
597
603
  projectHistoricalImageOmissions(projectedMessages ?? incomingMessages, state.omittedImageToolResults) ?? projectedMessages;
598
604
  const keepProjection = () => projectedMessages ? { messages: projectedMessages } : undefined;
599
- // Step 0 is shaped by the pre_turn path; the mid-turn trigger only runs
600
- // between steps, once completed-step usage and events exist.
601
- if (options.stepNumber < 1 || state.exhaustedDetail)
602
- return keepProjection();
603
605
  // Real usage for the last finished step, read synchronously from the
604
606
  // SDK's own step results (the same numbers the finish-step chunk
605
607
  // carries) — no coupling to how far the stream consumer has advanced.
@@ -619,14 +621,22 @@ export class AiSdkCompaction {
619
621
  // request's chars would under-estimate the retry by the whole previous
620
622
  // step growth — so a missing baseline forces the whole-payload cold
621
623
  // start, exactly like a missing usage sample.
622
- const lastStepInputTokens = options.completedSteps.at(-1)?.usage?.inputTokens;
623
- state.lastRequestInputTokens =
624
- state.lastRequestPayloadChars !== undefined &&
625
- lastStepInputTokens !== undefined &&
626
- Number.isFinite(lastStepInputTokens) &&
627
- lastStepInputTokens > 0
628
- ? lastStepInputTokens
629
- : undefined;
624
+ //
625
+ // Before the first step finishes nothing in-send has overwritten the
626
+ // seeded pair, so leave it alone rather than clearing a coherent anchor.
627
+ if (options.completedSteps.length > 0) {
628
+ const lastStepInputTokens = options.completedSteps.at(-1)?.usage?.inputTokens;
629
+ state.lastRequestInputTokens =
630
+ state.lastRequestPayloadChars !== undefined &&
631
+ lastStepInputTokens !== undefined &&
632
+ Number.isFinite(lastStepInputTokens) &&
633
+ lastStepInputTokens > 0
634
+ ? lastStepInputTokens
635
+ : undefined;
636
+ }
637
+ // The turn's first request folds as a pre_turn boundary, like the
638
+ // reactive step-0 recovery; later steps fold mid_turn.
639
+ const phase = options.stepNumber === 0 ? 'pre_turn' : 'mid_turn';
630
640
  // A skipped trigger is never silent: every failure-driven skip records a
631
641
  // failedOpen decision.
632
642
  const failOpen = (failOpenReason) => {
@@ -635,7 +645,7 @@ export class AiSdkCompaction {
635
645
  stage: 'activeStep',
636
646
  sourceKind: 'runtimeEvents',
637
647
  decision: 'failedOpen',
638
- phase: 'mid_turn',
648
+ phase,
639
649
  boundaryKind: 'historyCompact',
640
650
  reason: 'context_limit',
641
651
  failOpenReason,
@@ -644,12 +654,10 @@ export class AiSdkCompaction {
644
654
  });
645
655
  return keepProjection();
646
656
  };
647
- // A shaping failure additionally records itself for the final-request
648
- // estimate owner: when the final payload is still over the window, the
649
- // owner turns this step's failure into the terminal detail instead of
650
- // re-entering a shaper that already attempted and failed.
651
- const shapeFailure = (detail, diagnosticReason) => {
652
- state.lastShapeFailure = { stepNumber: options.stepNumber, detail, diagnosticReason };
657
+ // A shaping failure records the step so the rescue re-entry does not
658
+ // re-run a shaper that already attempted and failed this step.
659
+ const shapeFailure = (diagnosticReason) => {
660
+ state.lastShapeFailure = { stepNumber: options.stepNumber };
653
661
  return failOpen(diagnosticReason);
654
662
  };
655
663
  // Trigger estimate: the last request's input tokens plus a SIGNED char/4 delta of
@@ -661,21 +669,27 @@ export class AiSdkCompaction {
661
669
  // over-trigger; that is the recoverable direction, and the verdict owner
662
670
  // re-measures the post-shaping payload.
663
671
  const measuredMessages = projectedMessages ?? incomingMessages;
664
- const activeToolsForStep = options.activeTools ?? fallbackActiveTools();
665
- const payloadChars = midTurnRequestPayloadChars(measuredMessages, providerTools, activeToolsForStep, systemPromptChars);
672
+ // Price the request dispatch will actually build, not the pre-dispatch
673
+ // inputs: a finalization step adds prompt fragments and sends no tools.
674
+ const dispatch = options.resolveDispatch(options.activeTools);
675
+ const activeToolsForStep = dispatch.activeTools;
676
+ const payloadChars = midTurnRequestPayloadChars(measuredMessages, providerTools, activeToolsForStep, dispatch.systemPromptChars, charsPerToken);
666
677
  const forcedEstimate = state.forcedTriggerEstimate;
667
678
  state.forcedTriggerEstimate = undefined;
668
- const estimate = forcedEstimate ??
669
- estimateNextRequestTokens({
670
- ...(state.lastRequestInputTokens !== undefined
671
- ? { priorUsageTokens: state.lastRequestInputTokens }
672
- : {}),
673
- appendedChars: payloadChars - (state.lastRequestPayloadChars ?? payloadChars),
674
- charsPerToken,
675
- coldStartChars: payloadChars,
676
- });
679
+ const anchored = requestEstimateAnchor(state, payloadChars);
680
+ // The turn's FIRST request only gets a trigger when a previous turn left
681
+ // a usable anchor. Without one the estimate is the whole payload at
682
+ // chars/4 — the same guess the pre_turn gate already spends, and far too
683
+ // crude to start a summarizer on. With one, step 0 is judged exactly like
684
+ // every later step.
685
+ if (options.stepNumber < 1 &&
686
+ forcedEstimate === undefined &&
687
+ anchored.priorUsageTokens === undefined) {
688
+ return keepProjection();
689
+ }
690
+ const estimate = forcedEstimate ?? estimateNextRequestTokens({ ...anchored, charsPerToken });
677
691
  if (forcedEstimate === undefined &&
678
- !exceedsHighWater(estimate, state.capacity.tokens, reserveTokens)) {
692
+ (state.capacity === undefined || !exceedsHighWater(estimate, state.capacity, reserveTokens))) {
679
693
  return keepProjection();
680
694
  }
681
695
  // Fold a safe completed prefix of the durable turn ledger into a
@@ -684,6 +698,7 @@ export class AiSdkCompaction {
684
698
  // keep the raw projection on skip/fail, apply the fold on success.
685
699
  const outcome = await this.compactActiveRequestHistory({
686
700
  turnId,
701
+ phase,
687
702
  origin,
688
703
  state,
689
704
  queue,
@@ -691,13 +706,13 @@ export class AiSdkCompaction {
691
706
  referencePayloadChars: payloadChars,
692
707
  providerTools,
693
708
  activeToolsForStep,
694
- systemPromptChars,
709
+ systemPromptChars: dispatch.systemPromptChars,
695
710
  memoryCompactionDecision,
696
711
  onMemoryCompaction,
697
712
  abortSignal,
698
713
  });
699
714
  if (outcome.decision === 'fail') {
700
- return shapeFailure(outcome.detail, outcome.diagnosticReason);
715
+ return shapeFailure(outcome.diagnosticReason);
701
716
  }
702
717
  acceptedProjection = {
703
718
  sourceSignatures: incomingMessages.map(modelMessageSignature),
@@ -729,7 +744,6 @@ export class AiSdkCompaction {
729
744
  if (state.malformedSummaryFailure) {
730
745
  return {
731
746
  decision: 'fail',
732
- detail: state.malformedSummaryFailure,
733
747
  diagnosticReason: state.malformedSummaryFailure,
734
748
  };
735
749
  }
@@ -777,14 +791,12 @@ export class AiSdkCompaction {
777
791
  if (abortSignal?.aborted) {
778
792
  return {
779
793
  decision: 'fail',
780
- detail: 'no_safe_completed_span',
781
794
  diagnosticReason: 'ledger_wait_aborted',
782
795
  };
783
796
  }
784
797
  if (queue.consumerDetached) {
785
798
  return {
786
799
  decision: 'fail',
787
- detail: 'no_safe_completed_span',
788
800
  diagnosticReason: 'ledger_wait_aborted',
789
801
  };
790
802
  }
@@ -799,7 +811,6 @@ export class AiSdkCompaction {
799
811
  catch {
800
812
  return {
801
813
  decision: 'fail',
802
- detail: 'no_safe_completed_span',
803
814
  diagnosticReason: 'ledger_read_failed',
804
815
  };
805
816
  }
@@ -811,7 +822,6 @@ export class AiSdkCompaction {
811
822
  if (!currentTurnEvents.some((event) => event.id === state.headAnchor.id)) {
812
823
  return {
813
824
  decision: 'fail',
814
- detail: 'no_safe_completed_span',
815
825
  diagnosticReason: 'head_anchor_not_durable',
816
826
  };
817
827
  }
@@ -822,7 +832,7 @@ export class AiSdkCompaction {
822
832
  phase: input.phase ?? 'mid_turn',
823
833
  orderedEvents,
824
834
  headAnchor: { runtimeEventId: state.headAnchor.id, turnId },
825
- reserveTailEvents: midTurn.reserveTailEvents ?? 1,
835
+ reserveTailEvents: 1,
826
836
  charsPerToken,
827
837
  now: this.now(),
828
838
  ...(compactPolicy.highWaterName !== undefined
@@ -851,7 +861,9 @@ export class AiSdkCompaction {
851
861
  ...(previousCheckpoint ? { previousCheckpoint } : {}),
852
862
  newlyFoldedRuntimeEvents: [...newlyFoldedRuntimeEvents],
853
863
  inputBudget: {
854
- maxEstimatedTokens: Math.max(1, state.capacity.tokens - reserveTokens),
864
+ ...(state.capacity !== undefined
865
+ ? { maxEstimatedTokens: Math.max(1, state.capacity - reserveTokens) }
866
+ : {}),
855
867
  charsPerToken,
856
868
  },
857
869
  ...(abortSignal ? { abortSignal } : {}),
@@ -866,9 +878,6 @@ export class AiSdkCompaction {
866
878
  }
867
879
  return {
868
880
  decision: 'fail',
869
- detail: isMalformedHistoryCompactSummaryReason(diagnosticReason)
870
- ? diagnosticReason
871
- : plan.reason,
872
881
  diagnosticReason,
873
882
  };
874
883
  }
@@ -886,7 +895,6 @@ export class AiSdkCompaction {
886
895
  (replayPlan.hasProviderNativeSemantics && !this.canReplayProviderNative(replayPlan))) {
887
896
  return {
888
897
  decision: 'fail',
889
- detail: 'no_safe_completed_span',
890
898
  diagnosticReason: 'replacement_unmaterializable',
891
899
  };
892
900
  }
@@ -896,11 +904,10 @@ export class AiSdkCompaction {
896
904
  // request that overflowed for reactive recovery): a materialized
897
905
  // replacement that is not smaller proves the summarizer's OUTPUT is
898
906
  // unusable, reported as summarizer_failed via replacement_not_smaller.
899
- const replacedPayloadChars = midTurnRequestPayloadChars(replacementMessages, providerTools, activeToolsForStep, systemPromptChars);
907
+ const replacedPayloadChars = midTurnRequestPayloadChars(replacementMessages, providerTools, activeToolsForStep, systemPromptChars, charsPerToken);
900
908
  if (replacedPayloadChars >= input.referencePayloadChars) {
901
909
  return {
902
910
  decision: 'fail',
903
- detail: 'summarizer_failed',
904
911
  diagnosticReason: 'replacement_not_smaller',
905
912
  };
906
913
  }
@@ -911,7 +918,6 @@ export class AiSdkCompaction {
911
918
  if (!replayFit.fits) {
912
919
  return {
913
920
  decision: 'fail',
914
- detail: 'head_anchor_exceeds_capacity',
915
921
  diagnosticReason: `replay_rejected_${replayFit.reason}`,
916
922
  };
917
923
  }
@@ -924,7 +930,6 @@ export class AiSdkCompaction {
924
930
  catch {
925
931
  return {
926
932
  decision: 'fail',
927
- detail: 'summarizer_failed',
928
933
  diagnosticReason: 'write_failed',
929
934
  };
930
935
  }
@@ -956,8 +961,8 @@ export class AiSdkCompaction {
956
961
  * latch (pi's `_overflowRecoveryAttempted`). Returns the compacted messages
957
962
  * to resend, or undefined when recovery is impossible or already spent, in
958
963
  * which case the caller surfaces the real provider error rather than a
959
- * fabricated success or a synthesized `context_budget_exhausted` (the
960
- * provider — not the runtime — rejected the request). Non-context-length
964
+ * fabricated success or a locally synthesized verdict (the provider — not the
965
+ * runtime — rejected the request). Non-context-length
961
966
  * errors and turns without the mid-turn seam never reach compaction, so the
962
967
  * default (no seam) behavior is already better than the old fake end_turn.
963
968
  */
@@ -987,7 +992,7 @@ export class AiSdkCompaction {
987
992
  // The cold-start fallback only covers a send whose verdict owner never
988
993
  // ran request projection (defensive; step 0 records the baseline too).
989
994
  const referencePayloadChars = state.lastRequestPayloadChars ??
990
- midTurnRequestPayloadChars(input.currentMessages, input.providerTools, input.activeTools, input.systemPromptChars);
995
+ midTurnRequestPayloadChars(input.currentMessages, input.providerTools, input.activeTools, input.systemPromptChars, this.input.contextBudget?.charsPerToken ?? 4);
991
996
  const phase = input.stepNumber === 0 ? 'pre_turn' : 'mid_turn';
992
997
  const outcome = await this.compactActiveRequestHistory({
993
998
  turnId: input.turnId,
@@ -1039,8 +1044,11 @@ export class AiSdkCompaction {
1039
1044
  // Reset the baseline: the capacity hook's usage anchor is only coherent
1040
1045
  // paired with the payload chars of the SAME request, and a missing
1041
1046
  // baseline forces the whole-payload cold-start estimate instead of a
1042
- // stale pairing against the dead attempt.
1047
+ // stale pairing against the dead attempt. The cross-turn seed goes with
1048
+ // it: falling back to an even older request's anchor pairs worse, not
1049
+ // better.
1043
1050
  state.lastRequestPayloadChars = undefined;
1051
+ state.lastRequestInputTokens = undefined;
1044
1052
  return { messages: outcome.replacementMessages };
1045
1053
  }
1046
1054
  /**
@@ -1048,8 +1056,8 @@ export class AiSdkCompaction {
1048
1056
  * invariant. Every request-projection stage only shapes; this wrapper measures the
1049
1057
  * FINAL outgoing (messages, tools) payload — the bytes the provider will
1050
1058
  * actually see, after capacity compaction, active tool-result pruning, and
1051
- * semantic/active-full compaction have all run — and issues the one
1052
- * safety-critical verdict:
1059
+ * semantic/active-full compaction have all run — and spends the last
1060
+ * chance to shrink it:
1053
1061
  *
1054
1062
  * - estimate = the last request's real INPUT tokens + signed char/4 delta
1055
1063
  * against the previous request's measured payload (recorded here on
@@ -1059,50 +1067,43 @@ export class AiSdkCompaction {
1059
1067
  * cold start rather than a zero baseline;
1060
1068
  * - over the window with no capacity attempt this step (the approximate
1061
1069
  * trigger missed, e.g. growth the trigger under-weighted), force ONE
1062
- * capacity re-entry — the verdict must not terminate a turn a shaper can
1063
- * still rescue, and one bounded re-entry preserves termination;
1064
- * - still over the window → context_budget_exhausted, with the terminal
1065
- * detail taken from this step's capacity outcome: a replacement that
1066
- * remains too large is head_anchor_exceeds_capacity (the irreducible
1067
- * remainder exceeds capacity); a recorded shaping failure keeps its own
1068
- * detail and diagnostic reason.
1070
+ * capacity re-entry.
1069
1071
  *
1070
- * Step 0 is shaped by the pre_turn path. It is still measured here so an
1071
- * unshapable first request cannot bypass the capacity invariant.
1072
+ * Still over afterwards, the request goes out anyway: only the provider
1073
+ * knows whether it fits, and a rejection is recovered from.
1072
1074
  */
1073
- buildMidTurnFinalRequestVerdict(input) {
1074
- const { shaped, reentry, state, providerTools, fallbackActiveTools, charsPerToken, systemPromptChars, onDiagnosticPatch, abortController, } = input;
1075
+ buildMidTurnFinalRequestRescue(input) {
1076
+ const { shaped, reentry, state, providerTools, charsPerToken } = input;
1075
1077
  return async (options) => {
1076
1078
  let result = await Promise.resolve(shaped(options));
1077
1079
  const omissionProjection = projectHistoricalImageOmissions(result?.messages ?? options.messages, state.omittedImageToolResults);
1078
1080
  if (omissionProjection) {
1079
1081
  result = { ...(result ?? {}), messages: omissionProjection };
1080
1082
  }
1081
- const finalPayloadChars = () => midTurnRequestPayloadChars(result?.messages ?? options.messages, providerTools, result?.activeTools ?? options.activeTools ?? fallbackActiveTools(), systemPromptChars);
1083
+ const finalPayloadChars = () => {
1084
+ // Measure the dispatched shape, so the payload recorded as the anchor's
1085
+ // pair describes the same request the provider counts.
1086
+ const dispatch = options.resolveDispatch(result?.activeTools ?? options.activeTools);
1087
+ return midTurnRequestPayloadChars(result?.messages ?? options.messages, providerTools, dispatch.activeTools, dispatch.systemPromptChars, charsPerToken);
1088
+ };
1082
1089
  let payloadChars = finalPayloadChars();
1083
- if ((options.stepNumber >= 1 || state.capacity.source === 'policy_fallback') &&
1084
- !state.exhaustedDetail) {
1090
+ // Same rule as the trigger: the turn's first request is measured only
1091
+ // when a previous turn left a usable anchor to measure it against.
1092
+ const anchoredAtStepZero = requestEstimateAnchor(state, payloadChars).priorUsageTokens !== undefined;
1093
+ if (state.capacity !== undefined && (options.stepNumber >= 1 || anchoredAtStepZero)) {
1085
1094
  const estimateFinal = () => estimateNextRequestTokens({
1086
- ...(state.lastRequestInputTokens !== undefined
1087
- ? { priorUsageTokens: state.lastRequestInputTokens }
1088
- : {}),
1089
- appendedChars: payloadChars - (state.lastRequestPayloadChars ?? payloadChars),
1095
+ ...requestEstimateAnchor(state, payloadChars),
1090
1096
  charsPerToken,
1091
- coldStartChars: payloadChars,
1092
1097
  });
1093
- let estimate = estimateFinal();
1098
+ const estimate = estimateFinal();
1094
1099
  const capacityAttemptedThisStep = state.replacedStepNumber === options.stepNumber ||
1095
1100
  state.lastShapeFailure?.stepNumber === options.stepNumber;
1096
- if (options.stepNumber >= 1 &&
1097
- estimate > state.capacity.tokens &&
1098
- !capacityAttemptedThisStep) {
1099
- // One bounded capacity re-entry: the trigger threshold is
1100
- // approximate on purpose (recoverable), so a miss must become a
1101
- // rescue attempt before it can become a terminal verdict. Re-run
1102
- // only the capacity + prune shapers over the already-shaped
1103
- // projection; a second attempt after a same-step failure is
1104
- // pointless (the failure was not a trigger miss) and would double
1105
- // recorder counters and summarizer calls.
1101
+ if (estimate > state.capacity && !capacityAttemptedThisStep) {
1102
+ // One bounded capacity re-entry. Re-run only the capacity + prune
1103
+ // shapers over the already-shaped projection; a second attempt
1104
+ // after a same-step failure is pointless (the failure was not a
1105
+ // trigger miss) and would double recorder counters and summarizer
1106
+ // calls.
1106
1107
  state.forcedTriggerEstimate = estimate;
1107
1108
  const reshaped = await Promise.resolve(reentry({
1108
1109
  ...options,
@@ -1118,33 +1119,6 @@ export class AiSdkCompaction {
1118
1119
  };
1119
1120
  }
1120
1121
  payloadChars = finalPayloadChars();
1121
- estimate = estimateFinal();
1122
- }
1123
- if (estimate > state.capacity.tokens) {
1124
- const failure = state.lastShapeFailure?.stepNumber === options.stepNumber
1125
- ? state.lastShapeFailure
1126
- : undefined;
1127
- const replacedThisStep = state.replacedStepNumber === options.stepNumber;
1128
- const detail = replacedThisStep
1129
- ? 'head_anchor_exceeds_capacity'
1130
- : (failure?.detail ?? 'no_safe_completed_span');
1131
- const diagnosticReason = replacedThisStep
1132
- ? 'head_anchor_exceeds_capacity'
1133
- : (failure?.diagnosticReason ?? 'no_safe_completed_span');
1134
- state.exhaustedDetail = detail;
1135
- onDiagnosticPatch({
1136
- ...compactionDecisionDiagnosticPatch({
1137
- stage: 'activeStep',
1138
- sourceKind: 'runtimeEvents',
1139
- decision: 'unchanged',
1140
- phase: 'mid_turn',
1141
- boundaryKind: 'historyCompact',
1142
- reason: 'context_budget_exhausted',
1143
- skippedReasonCounts: { [diagnosticReason]: 1 },
1144
- }),
1145
- });
1146
- abortController?.abort(new Error(`mid-turn context budget exhausted: ${detail}`));
1147
- return result;
1148
1122
  }
1149
1123
  }
1150
1124
  state.lastRequestPayloadChars = payloadChars;
@@ -1250,6 +1224,9 @@ export class MidTurnCapacityCompactState {
1250
1224
  * Raw serialized chars of the final provider request. Overflow recovery
1251
1225
  * uses this as its shrink-reference baseline because it must compare the
1252
1226
  * actual rejected projection with a candidate replacement.
1227
+ *
1228
+ * Seeded before the turn's first request from the anchor a previous turn
1229
+ * persisted, so it can describe a request from an earlier send.
1253
1230
  */
1254
1231
  lastRequestPayloadChars;
1255
1232
  /**
@@ -1260,14 +1237,16 @@ export class MidTurnCapacityCompactState {
1260
1237
  * them twice. Undefined when the last step's usage is missing or unusable
1261
1238
  * (no positive input count); estimates then fall back to the whole-payload
1262
1239
  * cold-start path — an unusable sample is unknown, never zero.
1240
+ *
1241
+ * Written and cleared as a pair with `lastRequestPayloadChars`, seeded from
1242
+ * the previous turn's persisted anchor — see `LastRequestAnchor` for why the
1243
+ * two only mean anything together.
1263
1244
  */
1264
1245
  lastRequestInputTokens;
1265
1246
  /** Latest durable checkpoint (loaded or written) for roll-forward summaries. */
1266
1247
  previousCheckpoint;
1267
1248
  /** Checkpoint accepted during this send; pins every later durable projection. */
1268
1249
  projectionCheckpoint;
1269
- /** Set when the turn must end with a context_budget_exhausted outcome. */
1270
- exhaustedDetail;
1271
1250
  /**
1272
1251
  * Step whose request the capacity hook replaced. Semantic/active-full
1273
1252
  * compaction yields on that exact step so one step never runs two
@@ -1290,15 +1269,16 @@ export class MidTurnCapacityCompactState {
1290
1269
  /** Exact historical image results omitted after a provider overflow. */
1291
1270
  omittedImageToolResults = new Map();
1292
1271
  /**
1293
- * The capacity hook's most recent shaping failure. The owner reads it (for
1294
- * the same step only) to pick the terminal detail and diagnostic reason
1295
- * when the final payload is over the window, and to avoid re-entering a
1296
- * shaper that already attempted and failed this step.
1272
+ * The step of the capacity hook's most recent shaping failure. The rescue
1273
+ * re-entry reads it so it never re-runs a shaper that already attempted and
1274
+ * failed on the same step.
1297
1275
  */
1298
1276
  lastShapeFailure;
1299
1277
  /** Malformed summaries spend one bounded repair budget for this whole Turn. */
1300
1278
  malformedSummaryFailure;
1301
- constructor(headAnchor, priorContentEvents, priorRunHeaders, capacity) {
1279
+ constructor(headAnchor, priorContentEvents, priorRunHeaders,
1280
+ /** The model's declared context window, absent when it declares none. */
1281
+ capacity) {
1302
1282
  this.headAnchor = headAnchor;
1303
1283
  this.priorContentEvents = priorContentEvents;
1304
1284
  this.priorRunHeaders = priorRunHeaders;
@@ -1308,21 +1288,79 @@ export class MidTurnCapacityCompactState {
1308
1288
  /**
1309
1289
  * Char measure of the FULL provider-visible request input: the system prompt
1310
1290
  * (sent through the separate `system` field), the (projected) messages, and
1311
- * the serialized schemas of the active tool subset. The capacity trigger and
1312
- * the final-request estimate owner both measure with this ONE function, so
1313
- * their raw payload comparisons against `lastRequestPayloadChars` are
1314
- * commensurable and
1291
+ * the serialized schemas of the active tool subset. Media is billed in tokens
1292
+ * converted to chars, so the whole measure stays in one unit. The capacity
1293
+ * trigger and the rescue re-entry both measure with this ONE function, so
1294
+ * their comparisons against `lastRequestPayloadChars` are commensurable and
1315
1295
  * same-turn tool-schema growth (a `tool_search` activation) is counted like
1316
1296
  * any other payload growth. The system prompt is constant between adjacent
1317
1297
  * requests — signed deltas cancel it — but the cold-start estimate (no usable
1318
1298
  * usage sample) is the whole payload, so omitting it would under-estimate by
1319
- * exactly the system prompt and let an over-window request stream.
1299
+ * exactly the system prompt.
1300
+ *
1301
+ * A media part is worth what the provider charges for it, not what it
1302
+ * serializes to: materialized bytes reach the request as base64 or a byte map,
1303
+ * where a 500 KB screenshot serializes to ~667K chars. Measuring that string
1304
+ * makes one image look like a whole context window, which is how an affordable
1305
+ * request became a terminal verdict (#4458). Billing it at the same constant
1306
+ * the ledger's ruler uses keeps the two measures commensurable.
1320
1307
  */
1321
- function midTurnRequestPayloadChars(messages, providerTools, activeTools, systemPromptChars) {
1308
+ function midTurnRequestPayloadChars(messages, providerTools, activeTools, systemPromptChars, charsPerToken) {
1309
+ let mediaParts = 0;
1310
+ const serializedMessages = JSON.stringify(messages, (_key, value) => {
1311
+ if (!isInlineImageFilePart(value))
1312
+ return value;
1313
+ mediaParts += 1;
1314
+ return { type: value.type, mediaType: value.mediaType };
1315
+ });
1322
1316
  return (Math.max(0, Math.floor(systemPromptChars)) +
1323
- JSON.stringify(messages).length +
1317
+ (serializedMessages?.length ?? 0) +
1318
+ mediaParts * MATERIALIZED_IMAGE_TOKENS * Math.max(1, charsPerToken) +
1324
1319
  toolSchemaCharsForDiagnostics(providerTools, activeTools));
1325
1320
  }
1321
+ /**
1322
+ * The newest `LastRequestAnchor` persisted in the prior context.
1323
+ *
1324
+ * Reverse scan, and the FIRST anchor-bearing usage record decides — an older
1325
+ * anchor describes a request further from the one about to go out, so a
1326
+ * rejected newest anchor means cold start, never a fallback to an older one.
1327
+ * The synthetic `token_usage` a manual `/compact` writes carries no anchor and
1328
+ * is skipped for free.
1329
+ *
1330
+ * The anchor is a token count in the anchoring model's tokenizer, so it only
1331
+ * transfers to a request going to the same model over the same connection.
1332
+ */
1333
+ function persistedRequestAnchor(events, runHeaders, modelId, connectionId) {
1334
+ for (let index = events.length - 1; index >= 0; index -= 1) {
1335
+ const event = events[index];
1336
+ const anchor = event?.actions?.tokenUsage?.lastRequestAnchor;
1337
+ if (!anchor)
1338
+ continue;
1339
+ const header = runHeaders.find((candidate) => candidate.runId === event?.runId);
1340
+ if (!header || header.modelId !== modelId || header.llmConnectionId !== connectionId) {
1341
+ return undefined;
1342
+ }
1343
+ return anchor;
1344
+ }
1345
+ return undefined;
1346
+ }
1347
+ /**
1348
+ * The estimate inputs for the request about to go out: the paired anchor and
1349
+ * the signed char delta against the payload that anchor was reported for.
1350
+ *
1351
+ * A delta wider than the whole payload means the pairing has already failed, so
1352
+ * drop it and cold start: the whole payload against a zero anchor.
1353
+ */
1354
+ function requestEstimateAnchor(state, payloadChars) {
1355
+ const anchor = state.lastRequestInputTokens;
1356
+ const baseline = state.lastRequestPayloadChars;
1357
+ if (anchor === undefined || baseline === undefined)
1358
+ return { appendedChars: payloadChars };
1359
+ const appendedChars = payloadChars - baseline;
1360
+ if (Math.abs(appendedChars) > payloadChars)
1361
+ return { appendedChars: payloadChars };
1362
+ return { priorUsageTokens: anchor, appendedChars };
1363
+ }
1326
1364
  /**
1327
1365
  * The `decision: 'replaced'` diagnostic patch for a durable active-send fold,
1328
1366
  * shared by the proactive (`reason: 'context_limit'`) and reactive