@myagentroam/agent 0.11.54 → 0.11.56

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.
@@ -2,6 +2,7 @@ import { retainedCodeInputs } from '../../session/context-gc-observations.js';
2
2
  import { contextGcTargetKey } from '../../session/context-gc-checkpoint.js';
3
3
  import { codeCellCompletionSequence, contextGcCodeModeInputReplacement, contextGcCodeModeOutputReplacement, hasGenericContextGcPreview, normalizeContextGcCodeModeInput } from '../../session/context-gc-replacements.js';
4
4
  import { CONTEXT_GC_POLICY } from './gc-economics.js';
5
+ import { contextGcProtectionMature } from '../../session/context-gc-protection.js';
5
6
  import { targetFor, isRecord } from './gc-candidates.js';
6
7
  export function collectCodeModeCandidates(input, groups, existingTargets, candidateSavings) {
7
8
  const eligible = [];
@@ -41,8 +42,7 @@ export function collectCodeModeCandidates(input, groups, existingTargets, candid
41
42
  const reclaimedTokens = candidateSavings(candidate);
42
43
  if (reclaimedTokens <= 0)
43
44
  continue;
44
- if (candidate.observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts &&
45
- protection.thinking >= 3)
45
+ if (contextGcProtectionMature(protection))
46
46
  eligible.push(candidate);
47
47
  else if (candidate.observedRollouts < CONTEXT_GC_POLICY.largeExecProtectionRollouts) {
48
48
  protectedTokens += reclaimedTokens;
@@ -74,12 +74,12 @@ export function collectCodeModeCandidates(input, groups, existingTargets, candid
74
74
  replacementArguments: contextGcCodeModeInputReplacement(code, group.result.message.observations
75
75
  ? retainedCodeInputs(input.records, group.callId, (original, successor) => {
76
76
  const maturity = input.protection.after(Math.max(evidenceSequence, completionSequence, original.sequence, successor.sequence));
77
- return maturity.rollouts >= 6 && maturity.thinking >= 3;
77
+ return contextGcProtectionMature(maturity);
78
78
  }, {
79
79
  activeProcessIds: input.activeProcessIds,
80
80
  evidenceMature: (sequence) => {
81
81
  const maturity = input.protection.after(Math.max(evidenceSequence, completionSequence, sequence));
82
- return maturity.rollouts >= 6 && maturity.thinking >= 3;
82
+ return contextGcProtectionMature(maturity);
83
83
  }
84
84
  })
85
85
  : []),
@@ -105,8 +105,7 @@ export function collectCodeModeCandidates(input, groups, existingTargets, candid
105
105
  const reclaimedTokens = candidateSavings(candidate);
106
106
  if (reclaimedTokens <= 0)
107
107
  continue;
108
- if (observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts &&
109
- protection.thinking >= 3)
108
+ if (contextGcProtectionMature(protection))
110
109
  eligible.push(candidate);
111
110
  else if (observedRollouts < CONTEXT_GC_POLICY.largeExecProtectionRollouts) {
112
111
  protectedTokens += reclaimedTokens;
@@ -1,6 +1,7 @@
1
1
  import { contextGcTargetKey } from '../../session/context-gc-checkpoint.js';
2
2
  import { contextGcExecInputReplacement, contextGcExecOutputReplacement, normalizeContextGcExecResult } from '../../session/context-gc-replacements.js';
3
3
  import { CONTEXT_GC_POLICY } from './gc-economics.js';
4
+ import { contextGcProtectionMature } from '../../session/context-gc-protection.js';
4
5
  import { targetFor, isRecord } from './gc-candidates.js';
5
6
  import { terminalExecLifecycles } from '../../session/context-gc-exec-lifecycle.js';
6
7
  export function collectExecCandidates(input, groups, existingTargets, candidateSavings) {
@@ -80,8 +81,7 @@ export function collectExecCandidates(input, groups, existingTargets, candidateS
80
81
  let observedRollouts = Infinity;
81
82
  for (const replacement of replacements) {
82
83
  const protection = input.protection.after(Math.max(evidenceSequence, replacement.target.sequence));
83
- if (protection.rollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts &&
84
- protection.thinking >= 3) {
84
+ if (contextGcProtectionMature(protection)) {
85
85
  mature.push(replacement);
86
86
  observedRollouts = Math.min(observedRollouts, protection.rollouts);
87
87
  }
@@ -2,6 +2,7 @@ import { contextGcLogicalPathKey, contextGcTargetKey } from '../../session/conte
2
2
  import { contextGcApplyPatchInputReplacement, contextGcPatchResultFromCompletedPayload, contextGcPatchResultFromFailedPayload, contextGcStaleReadReplacement, contextGcReadRangeCovers, contextGcCoveredReadReplacement } from '../../session/context-gc-replacements.js';
3
3
  import { parseReadContextOutput } from '../../prompts/read-context.js';
4
4
  import { CONTEXT_GC_POLICY } from './gc-economics.js';
5
+ import { contextGcProtectionMature } from '../../session/context-gc-protection.js';
5
6
  import { targetFor } from './gc-candidates.js';
6
7
  export function collectDeterministicCandidates(input, groups, existingReplacements, existingTargets, candidateSavings) {
7
8
  const eligible = [];
@@ -103,8 +104,7 @@ export function collectDeterministicCandidates(input, groups, existingReplacemen
103
104
  let observedRollouts = Infinity;
104
105
  for (const replacement of replacements) {
105
106
  const protection = input.protection.after(Math.max(evidenceSequence, replacement.target.sequence));
106
- if (protection.rollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts &&
107
- protection.thinking >= 3) {
107
+ if (contextGcProtectionMature(protection)) {
108
108
  mature.push(replacement);
109
109
  observedRollouts = Math.min(observedRollouts, protection.rollouts);
110
110
  }
@@ -162,7 +162,7 @@ export function collectDeterministicCandidates(input, groups, existingReplacemen
162
162
  ?.items.some((prior) => prior.index === candidate.index)));
163
163
  const successor = later.findLast((entry) => {
164
164
  const count = input.protection.after(entry.target.sequence);
165
- return count.rollouts >= 6 && count.thinking >= 3;
165
+ return contextGcProtectionMature(count);
166
166
  });
167
167
  if (!successor)
168
168
  continue;
@@ -2,6 +2,7 @@ import { contextGcTargetKey } from '../../session/context-gc-checkpoint.js';
2
2
  import { completedTodoWrite, todoInputReplacement, todoSnapshotReplacement, visibleTodoSnapshot } from '../../session/context-gc-todo.js';
3
3
  import { contextGcWebOutputReplacement, contextGcToolOutputReplacement, contextGcSkillOutputReplacement, hasGenericContextGcPreview, isGenericContextGcTool } from '../../session/context-gc-replacements.js';
4
4
  import { CONTEXT_GC_POLICY } from './gc-economics.js';
5
+ import { contextGcProtectionMature } from '../../session/context-gc-protection.js';
5
6
  import { targetFor, isRecord } from './gc-candidates.js';
6
7
  export function collectWebCandidates(input, groups, existingTargets, candidateSavings) {
7
8
  const eligible = [];
@@ -46,8 +47,7 @@ export function collectWebCandidates(input, groups, existingTargets, candidateSa
46
47
  const reclaimedTokens = candidateSavings(candidate);
47
48
  if (reclaimedTokens <= 0)
48
49
  continue;
49
- if (observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts &&
50
- protection.thinking >= 3)
50
+ if (contextGcProtectionMature(protection))
51
51
  eligible.push(candidate);
52
52
  else if (observedRollouts < CONTEXT_GC_POLICY.largeExecProtectionRollouts) {
53
53
  protectedTokens += reclaimedTokens;
@@ -82,7 +82,7 @@ export function collectToolOutputCandidates(input, groups, existingTargets, cand
82
82
  candidate.result.message.toolSearchTools.some((item) => item.name === tool.name) &&
83
83
  (() => {
84
84
  const count = input.protection.after(Math.max(target.sequence, candidate.result.record.sequence));
85
- return count.rollouts >= 6 && count.thinking >= 3;
85
+ return contextGcProtectionMature(count);
86
86
  })());
87
87
  if (successor)
88
88
  evidence.push(successor.result.record.sequence);
@@ -133,8 +133,7 @@ export function collectToolOutputCandidates(input, groups, existingTargets, cand
133
133
  continue;
134
134
  const successor = writes
135
135
  .filter((write) => write.revision > currentRevision &&
136
- input.protection.after(Math.max(target.sequence, group.completed.sequence, write.record.sequence)).rollouts >= 6 &&
137
- input.protection.after(Math.max(target.sequence, group.completed.sequence, write.record.sequence)).thinking >= 3)
136
+ contextGcProtectionMature(input.protection.after(Math.max(target.sequence, group.completed.sequence, write.record.sequence))))
138
137
  .sort((a, b) => b.revision - a.revision)[0];
139
138
  if (!successor)
140
139
  continue;
@@ -196,7 +195,7 @@ export function collectToolOutputCandidates(input, groups, existingTargets, cand
196
195
  });
197
196
  const successor = later.findLast((candidate) => {
198
197
  const count = input.protection.after(Math.max(target.sequence, candidate.result.record.sequence));
199
- return count.rollouts >= 6 && count.thinking >= 3;
198
+ return contextGcProtectionMature(count);
200
199
  });
201
200
  if (!successor)
202
201
  continue;
@@ -259,8 +258,7 @@ export function collectToolOutputCandidates(input, groups, existingTargets, cand
259
258
  const reclaimedTokens = candidateSavings(candidate);
260
259
  if (reclaimedTokens <= 0)
261
260
  continue;
262
- if (observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts &&
263
- protection.thinking >= 3)
261
+ if (contextGcProtectionMature(protection))
264
262
  eligible.push(candidate);
265
263
  else if (observedRollouts < CONTEXT_GC_POLICY.largeExecProtectionRollouts) {
266
264
  protectedTokens += reclaimedTokens;
@@ -1,10 +1,24 @@
1
1
  import type { ModelUsage } from '../../model/contracts.js';
2
- import { type ContextGcEconomicsPolicy } from '../../session/context-gc-checkpoint.js';
3
- export declare const CONTEXT_GC_POLICY: ContextGcEconomicsPolicy;
2
+ export declare const CONTEXT_GC_POLICY: {
3
+ version: 1;
4
+ immediatePaybackRollouts: number;
5
+ minimumPaybackRollouts: number;
6
+ maximumPaybackRollouts: number;
7
+ contextGrowthSampleWindow: number;
8
+ minimumContextGrowthSamples: number;
9
+ minimumObservationRollouts: number;
10
+ stageAProtectionRollouts: number;
11
+ largeExecProtectionRollouts: number;
12
+ normalInputCostRate: number;
13
+ cacheReadCostRate: number;
14
+ outputCostRate: number;
15
+ minimumGrossReductionRate: number;
16
+ };
4
17
  export declare const COMPACT_CREDIT_ACTIVATION_RATIO = 0.8;
5
- export declare const COMPACT_THRESHOLD_RATIO = 0.9;
18
+ export declare const COMPACT_THRESHOLD_RATIO: number;
6
19
  interface ContextGcEconomicsInput {
7
20
  readonly contextUsageTokens: number;
21
+ readonly contextWindowTokens: number;
8
22
  readonly reclaimedTokens: number;
9
23
  readonly reclaimedCachedTokens: number;
10
24
  readonly cacheReadTokens: number;
@@ -17,10 +31,8 @@ interface ContextGcEconomicsInput {
17
31
  }
18
32
  export interface ContextGcEconomicsResult {
19
33
  readonly cacheRebuildPenaltyCostUnits: number;
20
- readonly requiredPositiveNetRollouts: number;
21
- readonly requiredTargetRoiRollouts: number;
34
+ readonly requiredPositiveNetRollouts: number | null;
22
35
  readonly expectedNetSavedCostUnits: number;
23
- readonly cacheInvestmentRoi: number;
24
36
  readonly grossReductionRate: number;
25
37
  readonly passes: boolean;
26
38
  }
@@ -1,25 +1,26 @@
1
+ import { AGENT_EXECUTION_POLICY } from '../execution-policy.js';
2
+ import { CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS } from '../../session/context-gc-protection.js';
1
3
  export const CONTEXT_GC_POLICY = {
2
4
  version: 1,
3
5
  immediatePaybackRollouts: 3,
4
- maximumPaybackRollouts: 20,
6
+ minimumPaybackRollouts: 30,
7
+ maximumPaybackRollouts: 60,
5
8
  contextGrowthSampleWindow: 16,
6
9
  minimumContextGrowthSamples: 6,
7
10
  minimumObservationRollouts: 1,
8
- stageAProtectionRollouts: 6,
9
- largeExecProtectionRollouts: 6,
11
+ stageAProtectionRollouts: CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS,
12
+ largeExecProtectionRollouts: CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS,
10
13
  normalInputCostRate: 1,
11
- cacheReadCostRate: 0.1,
12
- cacheWriteCostRate: 1.25,
14
+ cacheReadCostRate: 0.05,
13
15
  outputCostRate: 1,
14
- minimumGrossReductionRate: 0.2,
15
- minimumCacheInvestmentRoi: 0.1
16
+ minimumGrossReductionRate: 0.2
16
17
  };
17
18
  const CONTEXT_GC_START_RATIO = 0.5;
18
19
  const HIGH_WATER_GROSS_REDUCTION_START_RATIO = 0.7;
19
20
  const HIGH_WATER_MINIMUM_GROSS_REDUCTION_RATE = 0.1;
20
21
  const MINIMUM_CONTEXT_WINDOW_REDUCTION_RATIO = 0.05;
21
22
  export const COMPACT_CREDIT_ACTIVATION_RATIO = 0.8;
22
- export const COMPACT_THRESHOLD_RATIO = 0.9;
23
+ export const COMPACT_THRESHOLD_RATIO = AGENT_EXECUTION_POLICY.backgroundCompactionPercent / 100;
23
24
  const IMMINENT_COMPACT_CREDIT_DISCOUNT = 0.8;
24
25
  export function evaluateContextGcEconomics(input) {
25
26
  const reclaimedTokens = Math.max(0, input.reclaimedTokens);
@@ -28,49 +29,51 @@ export function evaluateContextGcEconomics(input) {
28
29
  const invalidatedCachedSuffixTokens = cacheReadTokens - preservedCachedPrefixTokens;
29
30
  const reclaimedCachedTokens = clamp(input.reclaimedCachedTokens, 0, invalidatedCachedSuffixTokens);
30
31
  const invalidatedRetainedCachedTokens = Math.max(0, invalidatedCachedSuffixTokens - reclaimedCachedTokens);
31
- const cacheRebuildPenaltyCostUnits = invalidatedRetainedCachedTokens *
32
- (CONTEXT_GC_POLICY.cacheWriteCostRate - CONTEXT_GC_POLICY.cacheReadCostRate);
32
+ // Calculate token-derived costs in cache-read units to keep exact zero exact.
33
+ const unitsPerNormalInput = CONTEXT_GC_POLICY.normalInputCostRate / CONTEXT_GC_POLICY.cacheReadCostRate;
34
+ const cacheRebuildPenalty = invalidatedRetainedCachedTokens * (unitsPerNormalInput - 1);
35
+ const cacheRebuildPenaltyCostUnits = cacheRebuildPenalty * CONTEXT_GC_POLICY.cacheReadCostRate;
33
36
  const reclaimedUncachedTokens = Math.max(0, reclaimedTokens - reclaimedCachedTokens);
34
- const firstRolloutSavingsCostUnits = reclaimedCachedTokens * CONTEXT_GC_POLICY.cacheReadCostRate +
35
- reclaimedUncachedTokens * CONTEXT_GC_POLICY.normalInputCostRate;
36
- const steadyRolloutSavingsCostUnits = reclaimedTokens * CONTEXT_GC_POLICY.cacheReadCostRate;
37
- const netAt = (rollouts) => input.compactDeferralCreditCostUnits -
38
- cacheRebuildPenaltyCostUnits +
39
- (rollouts <= 0
40
- ? 0
41
- : firstRolloutSavingsCostUnits + Math.max(0, rollouts - 1) * steadyRolloutSavingsCostUnits);
42
- const maximumSearch = Math.max(CONTEXT_GC_POLICY.maximumPaybackRollouts + 1, 10_000);
43
- const requiredPositiveNetRollouts = firstMatchingRollout(maximumSearch, (rollouts) => netAt(rollouts) > 0);
44
- const requiredTargetRoiRollouts = cacheRebuildPenaltyCostUnits === 0
45
- ? requiredPositiveNetRollouts
46
- : firstMatchingRollout(maximumSearch, (rollouts) => netAt(rollouts) > 0 &&
47
- netAt(rollouts) / cacheRebuildPenaltyCostUnits >=
48
- CONTEXT_GC_POLICY.minimumCacheInvestmentRoi);
49
- const targetRollouts = Number.isFinite(requiredTargetRoiRollouts)
50
- ? requiredTargetRoiRollouts
51
- : CONTEXT_GC_POLICY.maximumPaybackRollouts;
52
- const expectedNetSavedCostUnits = netAt(targetRollouts);
53
- const cacheInvestmentRoi = cacheRebuildPenaltyCostUnits === 0
54
- ? 0
55
- : expectedNetSavedCostUnits / cacheRebuildPenaltyCostUnits;
37
+ const firstRolloutSavings = reclaimedCachedTokens + reclaimedUncachedTokens * unitsPerNormalInput;
38
+ const steadyRolloutSavings = reclaimedTokens;
39
+ const netAt = (rollouts) => rollouts <= 0
40
+ ? -cacheRebuildPenalty
41
+ : firstRolloutSavings +
42
+ Math.max(0, rollouts - 1) * steadyRolloutSavings -
43
+ cacheRebuildPenalty;
44
+ let requiredPositiveNetRollouts;
45
+ if (netAt(1) > 0)
46
+ requiredPositiveNetRollouts = 1;
47
+ else if (steadyRolloutSavings <= 0)
48
+ requiredPositiveNetRollouts = null;
49
+ else {
50
+ requiredPositiveNetRollouts = Math.max(2, Math.floor(-netAt(1) / steadyRolloutSavings) + 2);
51
+ if (netAt(requiredPositiveNetRollouts - 1) > 0)
52
+ requiredPositiveNetRollouts--;
53
+ else if (netAt(requiredPositiveNetRollouts) <= 0)
54
+ requiredPositiveNetRollouts++;
55
+ }
56
+ const expectedNetSavedCostUnits = netAt(requiredPositiveNetRollouts ?? 0) * CONTEXT_GC_POLICY.cacheReadCostRate;
56
57
  const grossReductionRate = reclaimedTokens / Math.max(1, Math.max(0, input.contextUsageTokens));
57
58
  const minimumGrossReductionRate = Math.max(0, input.minimumGrossReductionRate ?? CONTEXT_GC_POLICY.minimumGrossReductionRate);
58
59
  const minimumReclaimedTokens = Math.max(0, input.minimumReclaimedTokens ??
59
60
  Math.ceil(Math.max(0, input.contextUsageTokens) * minimumGrossReductionRate));
60
- const immediate = requiredTargetRoiRollouts <= CONTEXT_GC_POLICY.immediatePaybackRollouts;
61
+ const usageRatio = input.contextUsageTokens / input.contextWindowTokens;
62
+ const progress = clamp((usageRatio - CONTEXT_GC_START_RATIO) / (COMPACT_THRESHOLD_RATIO - CONTEXT_GC_START_RATIO), 0, 1);
63
+ const paybackWindow = CONTEXT_GC_POLICY.minimumPaybackRollouts +
64
+ progress *
65
+ (CONTEXT_GC_POLICY.maximumPaybackRollouts - CONTEXT_GC_POLICY.minimumPaybackRollouts);
66
+ const withinWindow = requiredPositiveNetRollouts !== null && requiredPositiveNetRollouts <= paybackWindow;
61
67
  const passes = reclaimedTokens > 0 &&
62
68
  reclaimedTokens >= minimumReclaimedTokens &&
63
- Number.isFinite(requiredTargetRoiRollouts) &&
64
- (immediate ||
65
- (requiredTargetRoiRollouts <= CONTEXT_GC_POLICY.maximumPaybackRollouts &&
66
- input.contextGrowthSampleCount >= CONTEXT_GC_POLICY.minimumContextGrowthSamples &&
67
- requiredTargetRoiRollouts <= input.projectedRolloutsUntilCompactAfterGc));
69
+ Number.isFinite(usageRatio) &&
70
+ usageRatio >= CONTEXT_GC_START_RATIO &&
71
+ usageRatio < COMPACT_THRESHOLD_RATIO &&
72
+ withinWindow;
68
73
  return {
69
74
  cacheRebuildPenaltyCostUnits,
70
75
  requiredPositiveNetRollouts,
71
- requiredTargetRoiRollouts,
72
76
  expectedNetSavedCostUnits,
73
- cacheInvestmentRoi,
74
77
  grossReductionRate,
75
78
  passes
76
79
  };
@@ -128,12 +131,6 @@ export function modelContextUsage(value) {
128
131
  ? context
129
132
  : undefined;
130
133
  }
131
- function firstMatchingRollout(limit, predicate) {
132
- for (let rollouts = 0; rollouts <= limit; rollouts++)
133
- if (predicate(rollouts))
134
- return rollouts;
135
- return Number.POSITIVE_INFINITY;
136
- }
137
134
  export function clamp(value, minimum, maximum) {
138
135
  return Math.min(maximum, Math.max(minimum, value));
139
136
  }
@@ -2,7 +2,7 @@ import { ContextGcCheckpointCache, contextGcBaseFromRecords, contextGcCheckpoint
2
2
  import { buildContextGcProtection } from '../../session/context-gc-protection.js';
3
3
  import { CONTEXT_GC_TOKEN_ESTIMATOR, contextGcCacheImpact, contextGcMessageTokens, contextGcPrefixTokens, contextGcReclaimedTokens } from './gc-tokens.js';
4
4
  import { applyContextGcReplacementsToMessages, providerToolCallId } from './projection.js';
5
- import { CONTEXT_GC_POLICY, COMPACT_CREDIT_ACTIVATION_RATIO, COMPACT_THRESHOLD_RATIO, evaluateContextGcEconomics, contextGcMinimumGrossReductionRate, contextGcMinimumReclaimedTokens, contextGcEligibleUsage, contextGcImminentCompactAvoidanceCredit, contextGcAppliedCompactCreditCostUnits, modelContextUsage, clamp } from './gc-economics.js';
5
+ import { CONTEXT_GC_POLICY, COMPACT_CREDIT_ACTIVATION_RATIO, COMPACT_THRESHOLD_RATIO, evaluateContextGcEconomics, contextGcMinimumGrossReductionRate, contextGcMinimumReclaimedTokens, contextGcEligibleUsage, contextGcImminentCompactAvoidanceCredit, modelContextUsage, clamp } from './gc-economics.js';
6
6
  import { buildToolGroups, countSuccessfulNormalRollouts, isRecord } from './gc-candidates.js';
7
7
  import { collectCodeModeCandidates } from './gc-candidates-code.js';
8
8
  import { collectDeterministicCandidates } from './gc-candidates-read-patch.js';
@@ -181,19 +181,13 @@ function finalizePlan(input, evaluated, includeMedia = false) {
181
181
  compactDeferralCreditCostUnits: evaluated.compactDeferralCreditCostUnits,
182
182
  imminentCompactAvoidanceCreditCostUnits: evaluated.imminentCompactAvoidanceCreditCostUnits,
183
183
  appliedCompactCreditCostUnits: evaluated.appliedCompactCreditCostUnits,
184
- requiredPositiveNetRollouts: Number.isFinite(evaluated.economics.requiredPositiveNetRollouts)
185
- ? evaluated.economics.requiredPositiveNetRollouts
186
- : null,
187
- requiredTargetRoiRollouts: Number.isFinite(evaluated.economics.requiredTargetRoiRollouts)
188
- ? evaluated.economics.requiredTargetRoiRollouts
189
- : null,
184
+ requiredPositiveNetRollouts: evaluated.economics.requiredPositiveNetRollouts,
190
185
  projectedRolloutsUntilCompactAfterGc: evaluated.projectedRolloutsUntilCompactAfterGc,
191
186
  projectedTokensBeforeGc: evaluated.projectedTokensBeforeGc,
192
187
  projectedTokensAfterGc: evaluated.projectedTokensAfterGc,
193
188
  contextGrowthSampleCount: evaluated.contextGrowthSamples.length,
194
189
  contextGrowthP75Tokens: evaluated.contextGrowthP75Tokens,
195
190
  expectedNetSavedCostUnits: evaluated.economics.expectedNetSavedCostUnits,
196
- cacheInvestmentRoi: evaluated.economics.cacheInvestmentRoi,
197
191
  minimumObservedRollouts: evaluated.minimumObservedRollouts,
198
192
  protectedCandidateTokens: evaluated.protectedCandidateTokens,
199
193
  eligibleCandidateCount: evaluated.eligibleCandidateCount,
@@ -254,8 +248,6 @@ function evaluateCandidateSet(input, existing, candidates, state, mediaProjectio
254
248
  const pendingTokens = Math.max(0, input.usage.outputTokens ?? 0) + contextGcMessageTokens(input, pendingMessages);
255
249
  const projectedTokensBeforeGc = state.contextUsageTokens + pendingTokens;
256
250
  const projectedTokensAfterGc = Math.max(0, projectedTokensBeforeGc - reclaimedTokens);
257
- if (!state.mediaOnly && projectedTokensAfterGc >= compactThresholdTokens)
258
- return undefined;
259
251
  const projectedRolloutsUntilCompactAfterGc = contextGrowthP75Tokens <= 0
260
252
  ? 0
261
253
  : Math.max(0, Math.floor((compactThresholdTokens - projectedTokensAfterGc) / Math.max(1, contextGrowthP75Tokens)));
@@ -270,9 +262,11 @@ function evaluateCandidateSet(input, existing, candidates, state, mediaProjectio
270
262
  projectedTokensAfterGc,
271
263
  contextGrowthP75Tokens
272
264
  });
273
- const appliedCompactCreditCostUnits = contextGcAppliedCompactCreditCostUnits(compactDeferralCreditCostUnits, imminentCompactAvoidanceCreditCostUnits);
265
+ // Compact estimates remain diagnostic; only normal-request savings repay GC.
266
+ const appliedCompactCreditCostUnits = 0;
274
267
  const economics = evaluateContextGcEconomics({
275
268
  contextUsageTokens: state.contextUsageTokens,
269
+ contextWindowTokens: input.contextWindowTokens,
276
270
  reclaimedTokens,
277
271
  reclaimedCachedTokens: cache.reclaimedCachedTokens,
278
272
  cacheReadTokens: input.usage.cacheReadTokens ?? 0,
@@ -382,20 +376,15 @@ function compactDeferralCredit(input, reclaimedTokens, compactThresholdTokens) {
382
376
  const usage = compact.payload.usage;
383
377
  const contextInput = modelContextUsage(usage);
384
378
  const cacheRead = nonnegativeNumber(usage.cacheReadTokens);
385
- const cacheWrite = nonnegativeNumber(usage.cacheWriteTokens);
386
379
  const output = nonnegativeNumber(usage.outputTokens);
387
- if (contextInput === undefined ||
388
- cacheRead === undefined ||
389
- cacheWrite === undefined ||
390
- output === undefined)
380
+ if (contextInput === undefined || cacheRead === undefined || output === undefined)
391
381
  return 0;
392
382
  const baseline = firstMatchingUsageAfter(input.records, compact.sequence, input.modelId, input.contextWindowTokens);
393
383
  if (baseline === undefined || baseline >= compactThresholdTokens)
394
384
  return 0;
395
- const fresh = Math.max(0, contextInput - cacheRead - cacheWrite);
385
+ const fresh = Math.max(0, contextInput - cacheRead);
396
386
  const compactCost = fresh * CONTEXT_GC_POLICY.normalInputCostRate +
397
387
  cacheRead * CONTEXT_GC_POLICY.cacheReadCostRate +
398
- cacheWrite * CONTEXT_GC_POLICY.cacheWriteCostRate +
399
388
  output * CONTEXT_GC_POLICY.outputCostRate;
400
389
  const cycleCapacity = Math.max(1, compactThresholdTokens - baseline);
401
390
  return compactCost * clamp(reclaimedTokens / cycleCapacity, 0, 1);
@@ -1,7 +1,7 @@
1
1
  import { posix, win32 } from 'node:path';
2
2
  import { isDeepStrictEqual } from 'node:util';
3
3
  import { inlineMediaGcAnchored, parseInlineMediaGc } from './inline-media.js';
4
- import { buildContextGcProtection } from './context-gc-protection.js';
4
+ import { buildContextGcProtection, contextGcProtectionMature } from './context-gc-protection.js';
5
5
  import { completedTodoWrite, todoInputReplacement, todoSnapshotReplacement, visibleTodoSnapshot } from './context-gc-todo.js';
6
6
  import { contextGcRetainedInputMarkup } from './context-gc-markup.js';
7
7
  import { codeCallEvidence, codePartialPatchCallEvidence, callsFromRecord, codeObservationEvidence, observationsFromRecord, retainedCodeInputs } from './context-gc-observations.js';
@@ -239,7 +239,7 @@ export function validateContextGcCheckpoint(records, record, workspace = session
239
239
  }
240
240
  if (anchors.some((anchor) => {
241
241
  const count = protection.after(anchor);
242
- return count.rollouts < 6 || count.thinking < 3;
242
+ return !contextGcProtectionMature(count);
243
243
  }))
244
244
  return undefined;
245
245
  if (!replacementMatchesRecords(replacement, records, recordsBySequence, workspace, payload.trigger.platform, payload.replacements, protection, committed.get(contextGcReplacementKey(replacement))))
@@ -408,12 +408,12 @@ function replacementMatchesRecords(replacement, records, recordsBySequence, work
408
408
  isDeepStrictEqual(replacement.replacementArguments, contextGcCodeModeInputReplacement(input, observationsFromRecord(evidence.result)
409
409
  ? retainedCodeInputs(records, replacement.callId, (original, successor) => {
410
410
  const count = protection.after(Math.max(replacement.target.sequence, replacement.evidenceSequence, codeCellCompletionSequence(records, evidence.completed) ?? 0, original.sequence, successor.sequence));
411
- return count.rollouts >= 6 && count.thinking >= 3;
411
+ return contextGcProtectionMature(count);
412
412
  }, {
413
413
  activeProcessIds: new Set(),
414
414
  evidenceMature: (sequence) => {
415
415
  const count = protection.after(Math.max(replacement.target.sequence, replacement.evidenceSequence, codeCellCompletionSequence(records, evidence.completed) ?? 0, sequence));
416
- return count.rollouts >= 6 && count.thinking >= 3;
416
+ return contextGcProtectionMature(count);
417
417
  },
418
418
  selected: (callId, toolName, summary) => {
419
419
  const argument = replacement.replacementArguments;
@@ -1110,14 +1110,15 @@ function isContextGcEconomicsPolicy(value) {
1110
1110
  'stageAProtectionRollouts',
1111
1111
  'largeExecProtectionRollouts'
1112
1112
  ].every((key) => isNonnegativeInteger(value[key])) &&
1113
+ (value.minimumPaybackRollouts === undefined ||
1114
+ isNonnegativeInteger(value.minimumPaybackRollouts)) &&
1113
1115
  [
1114
1116
  'normalInputCostRate',
1115
1117
  'cacheReadCostRate',
1116
- 'cacheWriteCostRate',
1117
1118
  'outputCostRate',
1118
- 'minimumGrossReductionRate',
1119
- 'minimumCacheInvestmentRoi'
1120
- ].every((key) => isNonnegativeFinite(value[key])));
1119
+ 'minimumGrossReductionRate'
1120
+ ].every((key) => isNonnegativeFinite(value[key])) &&
1121
+ ['cacheWriteCostRate', 'minimumCacheInvestmentRoi'].every((key) => value[key] === undefined || isNonnegativeFinite(value[key])));
1121
1122
  }
1122
1123
  function isContextGcMetrics(value) {
1123
1124
  if (!isRecord(value) ||
@@ -1150,10 +1151,13 @@ function isContextGcMetrics(value) {
1150
1151
  'projectedTokensAfterGc'
1151
1152
  ];
1152
1153
  return (nonnegative.every((key) => isNonnegativeFinite(value[key])) &&
1153
- ['requiredPositiveNetRollouts', 'requiredTargetRoiRollouts'].every((key) => isNonnegativeFinite(value[key]) || (value.stage === 'INLINE_MEDIA' && value[key] === null)) &&
1154
+ [
1155
+ 'requiredPositiveNetRollouts',
1156
+ ...(value.requiredTargetRoiRollouts === undefined ? [] : ['requiredTargetRoiRollouts'])
1157
+ ].every((key) => isNonnegativeFinite(value[key]) || (value.stage === 'INLINE_MEDIA' && value[key] === null)) &&
1154
1158
  optionalNonnegative.every((key) => value[key] === undefined || isNonnegativeFinite(value[key])) &&
1155
1159
  Number.isFinite(value.expectedNetSavedCostUnits) &&
1156
- Number.isFinite(value.cacheInvestmentRoi) &&
1160
+ (value.cacheInvestmentRoi === undefined || Number.isFinite(value.cacheInvestmentRoi)) &&
1157
1161
  (value.tokenEstimator === undefined || value.tokenEstimator === 'UTF8_BYTES_V1') &&
1158
1162
  (value.cachePenaltyKnown === undefined || typeof value.cachePenaltyKnown === 'boolean'));
1159
1163
  }
@@ -5,6 +5,7 @@ export type ContextGcPlatform = 'linux' | 'darwin' | 'win32';
5
5
  export interface ContextGcEconomicsPolicy {
6
6
  readonly version: 1;
7
7
  readonly immediatePaybackRollouts: number;
8
+ readonly minimumPaybackRollouts?: number;
8
9
  readonly maximumPaybackRollouts: number;
9
10
  readonly contextGrowthSampleWindow: number;
10
11
  readonly minimumContextGrowthSamples: number;
@@ -13,10 +14,10 @@ export interface ContextGcEconomicsPolicy {
13
14
  readonly largeExecProtectionRollouts: number;
14
15
  readonly normalInputCostRate: number;
15
16
  readonly cacheReadCostRate: number;
16
- readonly cacheWriteCostRate: number;
17
+ readonly cacheWriteCostRate?: number;
17
18
  readonly outputCostRate: number;
18
19
  readonly minimumGrossReductionRate: number;
19
- readonly minimumCacheInvestmentRoi: number;
20
+ readonly minimumCacheInvestmentRoi?: number;
20
21
  }
21
22
  export type ContextGcBase = {
22
23
  readonly type: 'SESSION_START';
@@ -112,14 +113,14 @@ export interface ContextGcMetrics {
112
113
  readonly imminentCompactAvoidanceCreditCostUnits?: number;
113
114
  readonly appliedCompactCreditCostUnits?: number;
114
115
  readonly requiredPositiveNetRollouts: number | null;
115
- readonly requiredTargetRoiRollouts: number | null;
116
+ readonly requiredTargetRoiRollouts?: number | null;
116
117
  readonly projectedRolloutsUntilCompactAfterGc: number;
117
118
  readonly projectedTokensBeforeGc?: number;
118
119
  readonly projectedTokensAfterGc?: number;
119
120
  readonly contextGrowthSampleCount: number;
120
121
  readonly contextGrowthP75Tokens: number;
121
122
  readonly expectedNetSavedCostUnits: number;
122
- readonly cacheInvestmentRoi: number;
123
+ readonly cacheInvestmentRoi?: number;
123
124
  readonly minimumObservedRollouts: number;
124
125
  readonly protectedCandidateTokens: number;
125
126
  readonly eligibleCandidateCount: number;
@@ -1,4 +1,9 @@
1
1
  import type { SessionRecord } from './record-contract.js';
2
+ export declare const CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS = 10;
3
+ export declare function contextGcProtectionMature(count: {
4
+ readonly rollouts: number;
5
+ readonly thinking: number;
6
+ }): boolean;
2
7
  /** One immutable boundary index, shared by all candidate anchors in this evaluation. */
3
8
  export declare function buildContextGcProtection(records: readonly SessionRecord[], modelId: string, contextWindowTokens: number): {
4
9
  after(anchor: number): {
@@ -1,4 +1,8 @@
1
1
  import { completedNativeThinkingIdentity } from '../model/native-context.js';
2
+ export const CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS = 10;
3
+ export function contextGcProtectionMature(count) {
4
+ return count.rollouts >= CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS && count.thinking >= 3;
5
+ }
2
6
  /** One immutable boundary index, shared by all candidate anchors in this evaluation. */
3
7
  export function buildContextGcProtection(records, modelId, contextWindowTokens) {
4
8
  const configurations = new Map();
@@ -24,7 +24,7 @@ export function contextGcCodeModeInputReplacement(input, retainedInputs = []) {
24
24
  originalBytes: Buffer.byteLength(input.code),
25
25
  lines: lineCount(input.code),
26
26
  inputMode: input.inputMode,
27
- contextGc: 'Completed Code Mode source was reclaimed after six successful rollouts. The paired output, images, completion evidence, and nested tool side effects remain in history.',
27
+ contextGc: 'Completed Code Mode source was reclaimed after the protection period. The paired output, images, completion evidence, and nested tool side effects remain in history.',
28
28
  retainedInputs
29
29
  });
30
30
  return input.inputMode === 'freeform' ? content : { code: content };
@@ -37,7 +37,7 @@ export function contextGcCodeModeOutputReplacement(originalContent, truncated, v
37
37
  truncated,
38
38
  preview,
39
39
  previewComplete: !truncated && preview === originalContent,
40
- contextGc: 'Detailed Code Mode output was reclaimed after six successful rollouts. An incomplete preview is not the complete cell output. The source was not replayed and nested tool side effects were not changed.'
40
+ contextGc: 'Detailed Code Mode output was reclaimed after the protection period. An incomplete preview is not the complete cell output. The source was not replayed and nested tool side effects were not changed.'
41
41
  });
42
42
  }
43
43
  export function contextGcPatchResultFromCompletedPayload(payload) {
@@ -111,7 +111,7 @@ export function contextGcWebOutputReplacement(toolName, originalContent, truncat
111
111
  truncated,
112
112
  preview,
113
113
  previewComplete: preview === originalContent,
114
- contextGc: 'Detailed web tool output was reclaimed after six successful rollouts. An incomplete preview is not the complete search result or page, and GC did not access the network.'
114
+ contextGc: 'Detailed web tool output was reclaimed after the protection period. An incomplete preview is not the complete search result or page, and GC did not access the network.'
115
115
  });
116
116
  }
117
117
  /** Tools with their own evidence and lifecycle policies are never handled as generic output. */
@@ -142,7 +142,7 @@ export function contextGcToolOutputReplacement(toolName, originalContent, trunca
142
142
  truncated,
143
143
  preview,
144
144
  previewComplete: !truncated && preview === originalContent,
145
- contextGc: 'Detailed tool output was reclaimed after six successful rollouts. An incomplete preview is not the complete result; the tool was not replayed.'
145
+ contextGc: 'Detailed tool output was reclaimed after the protection period. An incomplete preview is not the complete result; the tool was not replayed.'
146
146
  });
147
147
  }
148
148
  export function contextGcSkillOutputReplacement(name, originalContent, successorCallId) {
@@ -3,7 +3,7 @@ import { mkdir, rename, rm } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { AgentError } from '../../../error.js';
5
5
  import { inlineMediaHistory } from '../../inline-media.js';
6
- import { copyToolResultResources } from '../../tool-result-resources.js';
6
+ import { prepareToolResultResources, writeToolResultResourceIndex } from '../../tool-result-resources.js';
7
7
  import { syncJsonlDirectory } from './fs-durability.js';
8
8
  import { forkedPayload, stageSessionSegments } from './history.js';
9
9
  import { copyGeneratedImages, generatedImageFileNames } from './images.js';
@@ -63,14 +63,49 @@ export async function forkSessionOperation(source, records, input, sourceRoot, s
63
63
  }
64
64
  ];
65
65
  const retained = input.boundaryTurnId === null ? [] : records.slice(1, boundaryIndex + 1);
66
+ const resourcesByOwner = new Map();
67
+ for (const [resourceRef, { owner, bytes }] of await prepareToolResultResources(sourceDirectory, retained)) {
68
+ const resources = resourcesByOwner.get(owner) ?? [];
69
+ resources.push({ resourceRef, bytes });
70
+ resourcesByOwner.set(owner, resources);
71
+ }
72
+ const resourceLocators = new Map();
66
73
  const inlineState = inlineMediaHistory(retained).state;
67
74
  const stateAnchor = inlineState &&
68
75
  retained.findLast((record) => record.type === 'resource.window' || record.type === 'compact.completed');
69
76
  const forkIdentities = new Map();
70
77
  for (const record of retained) {
71
- if (record.type === 'context.gc.completed' || record.type === 'responses.continuation')
78
+ if (record.type === 'context.gc.completed' ||
79
+ record.type === 'responses.continuation' ||
80
+ record.type === 'tool.resource')
72
81
  continue;
82
+ const pendingLocators = [];
83
+ for (const { resourceRef, bytes } of resourcesByOwner.get(record.recordId) ?? []) {
84
+ const bodySequences = [];
85
+ // Small private records share normal segment durability and rollback boundaries.
86
+ for (let offset = 0; offset < Math.max(1, bytes.length); offset += 1024 * 1024) {
87
+ const sequence = copied.length + 1;
88
+ bodySequences.push(sequence);
89
+ copied.push({
90
+ recordId: randomUUID(),
91
+ sequence,
92
+ sessionId: target.id,
93
+ type: 'tool.resource',
94
+ payload: {
95
+ resourceRef,
96
+ dataBase64: bytes.subarray(offset, offset + 1024 * 1024).toString('base64')
97
+ },
98
+ ...(record.executionId === undefined ? {} : { executionId: record.executionId }),
99
+ ...(record.turnId === undefined ? {} : { turnId: record.turnId }),
100
+ timestamp: record.timestamp
101
+ });
102
+ }
103
+ pendingLocators.push({ resourceRef, bodySequences });
104
+ }
105
+ resourcesByOwner.delete(record.recordId);
73
106
  const identity = { recordId: randomUUID(), sequence: copied.length + 1 };
107
+ for (const { resourceRef, bodySequences } of pendingLocators)
108
+ resourceLocators.set(resourceRef, { ...identity, bodySequences });
74
109
  forkIdentities.set(record.recordId, identity);
75
110
  copied.push({
76
111
  ...identity,
@@ -95,7 +130,7 @@ export async function forkSessionOperation(source, records, input, sourceRoot, s
95
130
  await rename(file.temporaryPath, file.path);
96
131
  target.segments = staged.segments;
97
132
  target.nextSequence = copied.length + 1;
98
- await copyToolResultResources(sourceDirectory, targetDirectory, copied);
133
+ await writeToolResultResourceIndex(targetDirectory, resourceLocators);
99
134
  await copyGeneratedImages(sourceRoot, publication.directory, generatedImageFileNames(retained));
100
135
  await copyResources(sourceDirectory, targetDirectory, retained);
101
136
  for (const path of [
@@ -39,8 +39,9 @@ export type HistoryIndexOutcome = {
39
39
  readonly error: unknown;
40
40
  };
41
41
  /** Execute one rollback/clear transaction while its caller holds maintenance. */
42
- export declare function replaceSessionHistory(meta: SessionMeta, retained: readonly SessionRecord[], operation: {
42
+ export declare function replaceSessionHistory(meta: SessionMeta, prefix: readonly SessionRecord[], operation: {
43
43
  kind: 'rollback';
44
+ reuseSegments: boolean;
44
45
  } | {
45
46
  kind: 'clear';
46
47
  clearedAt: string;
@@ -4,17 +4,26 @@ import { basename, join } from 'node:path';
4
4
  import { toolResultResourceFiles } from '../../tool-result-resources.js';
5
5
  import { beginSessionStorageTransaction, retiredSessionTrashPath, recoverSessionStorageTransaction } from './transaction-recovery.js';
6
6
  import { deriveExecutionState } from '../execution-state.js';
7
- import { renameIfExists, retainedSessionTitle, stageSessionSegments, stageUnreferencedImages } from './history.js';
7
+ import { renameIfExists, retainedSessionTitle, stageRollbackSegments, stageSessionSegments, stageUnreferencedImages } from './history.js';
8
8
  import { generatedImageFileNames } from './images.js';
9
9
  import { CONTEXTUAL_WORLD_STATE_FILE } from './open-recovery.js';
10
10
  import { resourceFiles } from './resources.js';
11
11
  import { syncJsonlDirectory } from './fs-durability.js';
12
12
  /** Execute one rollback/clear transaction while its caller holds maintenance. */
13
- export async function replaceSessionHistory(meta, retained, operation, context) {
13
+ export async function replaceSessionHistory(meta, prefix, operation, context) {
14
+ const retained = operation.kind === 'rollback'
15
+ ? prefix.filter((record) => record.type !== 'responses.continuation')
16
+ : prefix;
14
17
  const targetDirectory = context.targetGeneration === undefined
15
18
  ? context.directory
16
19
  : join(context.sessionDirectory, 'v2', context.targetGeneration);
17
- const staged = await stageSessionSegments(targetDirectory, retained);
20
+ const incremental = operation.kind === 'rollback' &&
21
+ operation.reuseSegments &&
22
+ meta.storage?.encoding === 2 &&
23
+ meta.segments !== undefined;
24
+ const staged = incremental
25
+ ? await stageRollbackSegments(targetDirectory, prefix, meta.segments)
26
+ : await stageSessionSegments(targetDirectory, retained);
18
27
  const transactionId = randomUUID();
19
28
  const targetNextSequence = operation.kind === 'clear' ? 2 : (retained.at(-1)?.sequence ?? 0) + 1;
20
29
  const contextualWorldStatePath = join(context.directory, CONTEXTUAL_WORLD_STATE_FILE);
@@ -22,7 +31,9 @@ export async function replaceSessionHistory(meta, retained, operation, context)
22
31
  const originalTransactionId = meta.storageTransactionId;
23
32
  const originalPaths = await context.segmentPaths();
24
33
  const retiredPaths = await retiredSessionPaths(context.sessionDirectory, context.directory, targetDirectory);
25
- const backups = originalPaths.map((path) => ({
34
+ const backups = originalPaths
35
+ .filter((path) => !staged.reused?.has(basename(path)))
36
+ .map((path) => ({
26
37
  path,
27
38
  backup: `${path}.${transactionId}.${operation.kind}-backup`
28
39
  }));
@@ -30,7 +41,7 @@ export async function replaceSessionHistory(meta, retained, operation, context)
30
41
  version: 1,
31
42
  id: transactionId,
32
43
  kind: operation.kind,
33
- originalSegments: originalPaths.map((path) => basename(path)),
44
+ originalSegments: backups.map((item) => basename(item.path)),
34
45
  stagedSegments: staged.files.map((item) => ({
35
46
  path: basename(item.path),
36
47
  temporary: basename(item.temporaryPath)
@@ -6,8 +6,13 @@ interface StagedSessionSegments {
6
6
  readonly temporaryPath: string;
7
7
  }[];
8
8
  readonly segments: NonNullable<SessionMeta['segments']>;
9
+ readonly reused?: ReadonlySet<string>;
9
10
  }
10
11
  export declare function stageSessionSegments(sessionDirectory: string, records: readonly SessionRecord[], operation?: 'rollback' | 'fork'): Promise<StagedSessionSegments>;
12
+ /** Reuse intact files in ordered v2 histories; journal only replacements and removals. */
13
+ export declare function stageRollbackSegments(directory: string, prefix: readonly SessionRecord[], originals: NonNullable<SessionMeta['segments']>): Promise<StagedSessionSegments & {
14
+ readonly reused: ReadonlySet<string>;
15
+ }>;
11
16
  export declare function stageUnreferencedImages(sessionDirectory: string, directoryName: 'generated_images' | 'tool_results' | 'resources', retained: ReadonlySet<string>, transactionId: string): Promise<Array<{
12
17
  from: string;
13
18
  to: string;
@@ -4,6 +4,7 @@ import { join } from 'node:path';
4
4
  import { AgentError } from '../../../error.js';
5
5
  import { parseInlineMediaState } from '../../inline-media.js';
6
6
  import { MAX_SEGMENT_BYTES } from './records.js';
7
+ import { forEachTransactionFile } from './transaction-files.js';
7
8
  export async function renameIfExists(from, to) {
8
9
  try {
9
10
  await rename(from, to);
@@ -26,23 +27,9 @@ export async function stageSessionSegments(sessionDirectory, records, operation
26
27
  if (group === undefined)
27
28
  return;
28
29
  const number = segments.length + 1;
29
- const path = join(sessionDirectory, `events-${String(number).padStart(6, '0')}.jsonl`);
30
- const temporaryPath = `${path}.${transactionId}.${operation}-tmp`;
31
- const handle = await open(temporaryPath, 'wx', 0o600);
32
- files.push({ path, temporaryPath });
33
- try {
34
- await handle.writeFile(group.lines.join(''));
35
- await handle.sync();
36
- }
37
- finally {
38
- await handle.close();
39
- }
40
- segments.push({
41
- number,
42
- firstSequence: group.firstSequence,
43
- lastSequence: group.lastSequence,
44
- bytes: group.bytes
45
- });
30
+ const staged = await writeStagedSegment(sessionDirectory, group, number, transactionId, operation);
31
+ files.push(staged.file);
32
+ segments.push(staged.segment);
46
33
  group = undefined;
47
34
  };
48
35
  try {
@@ -67,10 +54,86 @@ export async function stageSessionSegments(sessionDirectory, records, operation
67
54
  return { files, segments };
68
55
  }
69
56
  catch (error) {
70
- await Promise.all(files.map((item) => rm(item.temporaryPath, { force: true }))).catch(() => undefined);
57
+ await forEachTransactionFile(files, (file) => rm(file.temporaryPath, { force: true }).catch(() => undefined));
58
+ throw error;
59
+ }
60
+ }
61
+ /** Reuse intact files in ordered v2 histories; journal only replacements and removals. */
62
+ export async function stageRollbackSegments(directory, prefix, originals) {
63
+ const files = [];
64
+ const segments = [];
65
+ const reused = new Set();
66
+ const transactionId = randomUUID();
67
+ let offset = 0;
68
+ try {
69
+ for (const original of originals) {
70
+ const start = offset;
71
+ while (offset < prefix.length && prefix[offset].sequence <= original.lastSequence)
72
+ offset++;
73
+ const source = prefix.slice(start, offset);
74
+ if (source.length === 0)
75
+ break;
76
+ const retained = source.filter((record) => record.type !== 'responses.continuation');
77
+ if (retained.length === source.length && source.at(-1).sequence === original.lastSequence) {
78
+ reused.add(`events-${String(original.number).padStart(6, '0')}.jsonl`);
79
+ segments.push({ ...original });
80
+ }
81
+ else if (retained.length > 0) {
82
+ const lines = retained.map((record) => `${JSON.stringify(record)}\n`);
83
+ const staged = await writeStagedSegment(directory, {
84
+ lines,
85
+ bytes: lines.reduce((total, line) => total + Buffer.byteLength(line), 0),
86
+ firstSequence: retained[0].sequence,
87
+ lastSequence: retained.at(-1).sequence
88
+ }, original.number, transactionId, 'rollback');
89
+ files.push(staged.file);
90
+ segments.push(staged.segment);
91
+ }
92
+ }
93
+ return { files, segments, reused };
94
+ }
95
+ catch (error) {
96
+ await forEachTransactionFile(files, (file) => rm(file.temporaryPath, { force: true }).catch(() => undefined));
71
97
  throw error;
72
98
  }
73
99
  }
100
+ async function writeStagedSegment(directory, group, number, transactionId, operation) {
101
+ const path = join(directory, `events-${String(number).padStart(6, '0')}.jsonl`);
102
+ const temporaryPath = `${path}.${transactionId}.${operation}-tmp`;
103
+ const handle = await open(temporaryPath, 'wx', 0o600);
104
+ let failed = false;
105
+ let failure;
106
+ try {
107
+ await handle.writeFile(group.lines.join(''));
108
+ await handle.sync();
109
+ }
110
+ catch (error) {
111
+ failed = true;
112
+ failure = error;
113
+ }
114
+ try {
115
+ await handle.close();
116
+ }
117
+ catch (error) {
118
+ if (!failed) {
119
+ failed = true;
120
+ failure = error;
121
+ }
122
+ }
123
+ if (failed) {
124
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
125
+ throw failure;
126
+ }
127
+ return {
128
+ file: { path, temporaryPath },
129
+ segment: {
130
+ number,
131
+ firstSequence: group.firstSequence,
132
+ lastSequence: group.lastSequence,
133
+ bytes: group.bytes
134
+ }
135
+ };
136
+ }
74
137
  export async function stageUnreferencedImages(sessionDirectory, directoryName, retained, transactionId) {
75
138
  const directory = join(sessionDirectory, directoryName);
76
139
  let names;
@@ -84,14 +147,12 @@ export async function stageUnreferencedImages(sessionDirectory, directoryName, r
84
147
  }
85
148
  const moved = [];
86
149
  try {
87
- for (const name of names) {
88
- if (retained.has(name))
89
- continue;
150
+ await forEachTransactionFile(names.filter((name) => !retained.has(name)), async (name) => {
90
151
  const from = join(directory, name);
91
152
  const trash = join(directory, `.${name}.${transactionId}.rollback-trash`);
92
153
  await rename(from, trash);
93
154
  moved.push({ from: trash, to: from, trash });
94
- }
155
+ });
95
156
  return moved;
96
157
  }
97
158
  catch (error) {
@@ -100,8 +161,7 @@ export async function stageUnreferencedImages(sessionDirectory, directoryName, r
100
161
  }
101
162
  }
102
163
  export async function restoreRenamedFiles(files) {
103
- for (const file of files.toReversed())
104
- await rename(file.from, file.to).catch(() => undefined);
164
+ await forEachTransactionFile(files, (file) => rename(file.from, file.to).catch(() => undefined));
105
165
  }
106
166
  export function retainedSessionTitle(records, fallback) {
107
167
  let title = fallback;
@@ -698,14 +698,28 @@ export class JsonlSessionStore {
698
698
  const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
699
699
  if (!meta)
700
700
  throw new AgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
701
- const records = await this.read(sessionId);
701
+ const indexedSegments = meta.segments;
702
+ let segmentIndex = 0;
703
+ let matchingSegments = indexedSegments !== undefined;
704
+ // Check optional cache bounds during the history read already required by rollback.
705
+ const records = await this.#serializeRecordOperation(sessionId, () => this.#readRecords(sessionId, undefined, (path, segment) => {
706
+ const indexed = indexedSegments?.[segmentIndex];
707
+ segmentIndex++;
708
+ matchingSegments &&=
709
+ indexed !== undefined &&
710
+ path === this.#segmentPath(sessionId, indexed.number, meta) &&
711
+ segment[0]?.sequence === indexed.firstSequence &&
712
+ segment.at(-1)?.sequence === indexed.lastSequence;
713
+ }));
702
714
  if (records[0]?.type !== 'session.created')
703
715
  throw new AgentError('MAR_AGENT_SESSION_CORRUPT', 'Session history is corrupt.');
704
716
  const boundaryIndex = rollbackBoundaryIndex(records, input);
705
- const retained = records
706
- .slice(0, boundaryIndex + 1)
707
- .filter((record) => record.type !== 'responses.continuation');
708
- return await this.#replaceHistory(meta, retained, { kind: 'rollback' });
717
+ const retained = records.slice(0, boundaryIndex + 1);
718
+ // Migrated v1 histories may preserve duplicate or non-increasing sequences.
719
+ const reuseSegments = matchingSegments &&
720
+ segmentIndex === indexedSegments?.length &&
721
+ records.every((record, index) => index === 0 || record.sequence > records[index - 1].sequence);
722
+ return await this.#replaceHistory(meta, retained, { kind: 'rollback', reuseSegments });
709
723
  }
710
724
  catch (error) {
711
725
  failed = true;
@@ -1110,7 +1124,7 @@ export class JsonlSessionStore {
1110
1124
  async read(sessionId) {
1111
1125
  return this.#serializeRecordOperation(sessionId, () => this.#readRecords(sessionId));
1112
1126
  }
1113
- async #readRecords(sessionId, selectedMeta) {
1127
+ async #readRecords(sessionId, selectedMeta, onSegment) {
1114
1128
  return withSessionReadPin(this.#sessionRoot(sessionId), async () => {
1115
1129
  this.#assertOpen();
1116
1130
  if (!selectedMeta)
@@ -1125,8 +1139,11 @@ export class JsonlSessionStore {
1125
1139
  const records = [];
1126
1140
  const meta = selectedMeta ?? this.#shared.index.sessions.find((item) => item.id === sessionId);
1127
1141
  const encoding = meta?.storage?.encoding ?? 1;
1128
- for (const path of await this.#segmentPaths(sessionId, meta))
1129
- records.push(...(await readRecords(path, encoding)));
1142
+ for (const path of await this.#segmentPaths(sessionId, meta)) {
1143
+ const segment = await readRecords(path, encoding);
1144
+ onSegment?.(path, segment);
1145
+ records.push(...segment);
1146
+ }
1130
1147
  return records;
1131
1148
  }
1132
1149
  catch (cause) {
@@ -0,0 +1,2 @@
1
+ /** Stop scheduling on the first failure, then drain all in-flight filesystem operations. */
2
+ export declare function forEachTransactionFile<T>(files: readonly T[], operation: (file: T) => Promise<unknown>): Promise<void>;
@@ -0,0 +1,22 @@
1
+ /** Stop scheduling on the first failure, then drain all in-flight filesystem operations. */
2
+ export async function forEachTransactionFile(files, operation) {
3
+ let next = 0;
4
+ let failed = false;
5
+ let failure;
6
+ await Promise.all(Array.from({ length: Math.min(8, files.length) }, async () => {
7
+ while (!failed && next < files.length) {
8
+ const file = files[next++];
9
+ try {
10
+ await operation(file);
11
+ }
12
+ catch (error) {
13
+ if (!failed) {
14
+ failed = true;
15
+ failure = error;
16
+ }
17
+ }
18
+ }
19
+ }));
20
+ if (failed)
21
+ throw failure;
22
+ }
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
3
3
  import { basename, dirname, join } from 'node:path';
4
4
  import { AgentError } from '../../../error.js';
5
+ import { forEachTransactionFile } from './transaction-files.js';
5
6
  const JOURNAL = 'storage-transaction.json';
6
7
  const SEGMENT = /^events-\d{6}\.jsonl$/u;
7
8
  const TRANSACTION_ID = /^[0-9a-f-]{36}$/iu;
@@ -291,24 +292,24 @@ export async function recoverSessionStorageTransaction(directory, committedTrans
291
292
  ? directory
292
293
  : join(sessionDirectory, 'v2', transaction.targetGeneration);
293
294
  const originals = new Set(transaction.originalSegments);
294
- for (const name of transaction.originalSegments) {
295
+ await forEachTransactionFile([...originals], async (name) => {
295
296
  const canonical = join(directory, name);
296
297
  const backup = `${canonical}.${transaction.id}.${transaction.kind}-backup`;
297
- if (!(await exists(backup)))
298
- continue;
299
298
  if (committed)
300
299
  await rm(backup, { force: true });
301
300
  else {
301
+ if (!(await exists(backup)))
302
+ return;
302
303
  await rm(canonical, { force: true });
303
304
  await rename(backup, canonical);
304
305
  }
305
- }
306
+ });
306
307
  if (!committed)
307
- for (const entry of transaction.stagedSegments)
308
+ await forEachTransactionFile(transaction.stagedSegments, async (entry) => {
308
309
  if (stagedDirectory !== directory || !originals.has(entry.path))
309
310
  await rm(join(stagedDirectory, entry.path), { force: true });
310
- for (const entry of transaction.stagedSegments)
311
- await rm(join(stagedDirectory, entry.temporary), { force: true });
311
+ });
312
+ await forEachTransactionFile(transaction.stagedSegments, (entry) => rm(join(stagedDirectory, entry.temporary), { force: true }));
312
313
  if (!committed && transaction.targetGeneration !== undefined)
313
314
  await rm(stagedDirectory, { recursive: true, force: true });
314
315
  const sidecar = join(directory, 'context-world-state.json');
@@ -333,15 +334,13 @@ export async function recoverSessionStorageTransaction(directory, committedTrans
333
334
  throw error;
334
335
  }
335
336
  const suffix = `.${transaction.id}.rollback-trash`;
336
- for (const name of names) {
337
- if (!name.startsWith('.') || !name.endsWith(suffix))
338
- continue;
337
+ await forEachTransactionFile(names.filter((name) => name.startsWith('.') && name.endsWith(suffix)), async (name) => {
339
338
  const trash = join(path, name);
340
339
  if (committed)
341
340
  await rm(trash, { force: true });
342
341
  else
343
342
  await rename(trash, join(path, name.slice(1, -suffix.length)));
344
- }
343
+ });
345
344
  }
346
345
  for (const path of transaction.retiredPaths ?? []) {
347
346
  const source = join(sessionDirectory, path);
@@ -357,9 +356,7 @@ export async function recoverSessionStorageTransaction(directory, committedTrans
357
356
  await removeUnjournaledTransactionFiles(directory);
358
357
  }
359
358
  async function removeUnjournaledTransactionFiles(directory) {
360
- for (const name of await readdir(directory))
361
- if (isUnjournaledTransactionFile(name))
362
- await rm(join(directory, name), { force: true });
359
+ await forEachTransactionFile((await readdir(directory)).filter(isUnjournaledTransactionFile), (name) => rm(join(directory, name), { force: true }));
363
360
  }
364
361
  async function exists(path) {
365
362
  try {
@@ -20,7 +20,16 @@ export declare function recordToolResources(record: Pick<SessionRecord, 'type' |
20
20
  /** A locator is only a hint: reads must verify its exact committed record identity. */
21
21
  export declare function bindToolResultResources(directory: string, record: SessionRecord, readRecord?: (sequence: number) => Promise<SessionRecord | undefined>, degrade?: boolean): Promise<void>;
22
22
  export declare function toolResultResourceFiles(records: readonly SessionRecord[]): Set<string>;
23
- export declare function copyToolResultResources(source: string, target: string, records: readonly SessionRecord[]): Promise<void>;
23
+ export interface ToolResourceLocator {
24
+ sequence: number;
25
+ recordId: string;
26
+ bodySequences?: number[];
27
+ }
28
+ export declare function prepareToolResultResources(source: string, records: readonly SessionRecord[]): Promise<Map<string, {
29
+ owner: string;
30
+ bytes: Buffer;
31
+ }>>;
32
+ export declare function writeToolResultResourceIndex(target: string, locators: ReadonlyMap<string, ToolResourceLocator>): Promise<void>;
24
33
  export declare function completeToolResultResources(directory: string, record: SessionRecord): Promise<void>;
25
34
  export declare function cleanupUnreferencedToolResources(directory: string, readRecords: () => Promise<readonly SessionRecord[]>): Promise<void>;
26
35
  /** Caller must hold a stable history and resource generation for the entire read. */
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
3
- import { join } from 'node:path';
3
+ import { dirname, join } from 'node:path';
4
4
  import { AgentError } from '../error.js';
5
5
  import { parseToolResultExtensions } from '../tools/result-extensions.js';
6
6
  const REF = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/u;
@@ -73,13 +73,8 @@ export async function bindToolResultResources(directory, record, readRecord, deg
73
73
  async function bindToolResultResource(directory, record, resource, readRecord) {
74
74
  const path = join(directory, 'tool_results', `${resource.resourceRef}.json`);
75
75
  try {
76
- await writeFile(path, JSON.stringify({ sequence: record.sequence, recordId: record.recordId }), { flag: 'wx', mode: 0o600, flush: true });
77
- }
78
- catch (error) {
79
- if (error.code !== 'EEXIST')
80
- throw error;
81
- try {
82
- const locator = JSON.parse(await readFile(path, 'utf8'));
76
+ const locator = await readToolResourceLocator(path, resource.resourceRef);
77
+ if (locator !== undefined) {
83
78
  const existing = locator.sequence === record.sequence && locator.recordId === record.recordId
84
79
  ? record
85
80
  : Number.isSafeInteger(locator.sequence)
@@ -89,46 +84,103 @@ async function bindToolResultResource(directory, record, resource, readRecord) {
89
84
  existing.recordId !== locator.recordId ||
90
85
  !recordToolResources(existing).some((entry) => entry.resourceRef === resource.resourceRef && entry.sha256 === resource.sha256))
91
86
  throw resourceError('CORRUPT', 'Tool resource locator conflicts with committed history.');
87
+ return;
92
88
  }
93
- catch (cause) {
89
+ }
90
+ catch (cause) {
91
+ if (cause.code !== 'ENOENT')
94
92
  throw normalizedResourceError(cause);
95
- }
96
93
  }
94
+ await writeFile(path, JSON.stringify({ sequence: record.sequence, recordId: record.recordId }), {
95
+ flag: 'wx',
96
+ mode: 0o600,
97
+ flush: true
98
+ });
97
99
  }
98
100
  export function toolResultResourceFiles(records) {
99
- return new Set(records.flatMap((record) => recordToolResources(record).flatMap((resource) => [
101
+ const files = new Set(records.flatMap((record) => recordToolResources(record).flatMap((resource) => [
100
102
  `${resource.resourceRef}.diff`,
101
103
  `${resource.resourceRef}.json`
102
104
  ])));
105
+ if (files.size > 0)
106
+ files.add('index.json');
107
+ return files;
103
108
  }
104
- export async function copyToolResultResources(source, target, records) {
105
- const copied = new Set();
109
+ export async function prepareToolResultResources(source, records) {
110
+ const resources = new Map();
106
111
  for (const record of records) {
107
112
  for (const resource of recordToolResources(record)) {
108
- if (copied.has(resource.resourceRef))
109
- continue;
110
- copied.add(resource.resourceRef);
113
+ const previous = resources.get(resource.resourceRef);
114
+ if (previous && previous.resource.sha256 !== resource.sha256)
115
+ throw resourceError('CORRUPT', 'Tool resource locator conflicts with committed history.');
116
+ if (!previous)
117
+ resources.set(resource.resourceRef, { record, resource });
118
+ }
119
+ }
120
+ const bodies = new Map();
121
+ if (resources.size === 0)
122
+ return bodies;
123
+ const bySequence = new Map(records.map((record) => [record.sequence, record]));
124
+ const pending = resources.values();
125
+ let failure;
126
+ const read = async () => {
127
+ while (!failure) {
128
+ const next = pending.next();
129
+ if (next.done)
130
+ return;
131
+ const { record, resource } = next.value;
111
132
  try {
112
- const bytes = await readResourceBytes(source, resource);
113
- const directory = join(target, 'tool_results');
114
- await mkdir(directory, { recursive: true, mode: 0o700 });
115
- await writeFile(join(directory, `${resource.resourceRef}.diff`), bytes, {
116
- flag: 'wx',
117
- mode: 0o600,
118
- flush: true
119
- });
133
+ const bytes = await readResourceBytes(source, resource, async (sequence) => bySequence.get(sequence));
134
+ bodies.set(resource.resourceRef, { owner: record.recordId, bytes });
120
135
  }
121
136
  catch (error) {
122
- if (error instanceof AgentError && error.code === 'MAR_AGENT_TOOL_RESOURCE_NOT_FOUND')
123
- continue;
124
- throw error;
137
+ if (!(error instanceof AgentError && error.code === 'MAR_AGENT_TOOL_RESOURCE_NOT_FOUND'))
138
+ failure ??= { error };
125
139
  }
126
140
  }
127
- if (recordToolResources(record).length) {
128
- await mkdir(join(target, 'tool_results'), { recursive: true, mode: 0o700 });
129
- await bindToolResultResources(target, record, async (sequence) => records.find((item) => item.sequence === sequence));
130
- }
141
+ };
142
+ await Promise.all(Array.from({ length: Math.min(8, resources.size) }, read));
143
+ if (failure)
144
+ throw failure.error;
145
+ return bodies;
146
+ }
147
+ export async function writeToolResultResourceIndex(target, locators) {
148
+ if (locators.size === 0)
149
+ return;
150
+ const directory = join(target, 'tool_results');
151
+ await mkdir(directory, { recursive: true, mode: 0o700 });
152
+ await writeFile(join(directory, 'index.json'), JSON.stringify({ version: 1, locators: Object.fromEntries(locators) }), {
153
+ flag: 'wx',
154
+ mode: 0o600,
155
+ flush: true
156
+ });
157
+ }
158
+ async function readToolResourceLocator(path, resourceRef) {
159
+ let text;
160
+ try {
161
+ text = await readFile(path, 'utf8');
131
162
  }
163
+ catch (error) {
164
+ if (error.code !== 'ENOENT')
165
+ throw error;
166
+ text = await readFile(join(dirname(path), 'index.json'), 'utf8');
167
+ }
168
+ const value = JSON.parse(text);
169
+ const locator = value?.version === 1 ? value.locators?.[resourceRef] : value;
170
+ if (value?.version === 1 && locator === undefined)
171
+ return undefined;
172
+ if (!Number.isSafeInteger(locator?.sequence) ||
173
+ locator.sequence < 1 ||
174
+ typeof locator.recordId !== 'string')
175
+ throw resourceError('CORRUPT', 'Invalid tool resource locator.');
176
+ if (locator.bodySequences !== undefined &&
177
+ (!Array.isArray(locator.bodySequences) ||
178
+ locator.bodySequences.length === 0 ||
179
+ !locator.bodySequences.every((sequence) => Number.isSafeInteger(sequence) &&
180
+ sequence > 0 &&
181
+ sequence < locator.sequence)))
182
+ throw resourceError('CORRUPT', 'Invalid tool resource body locator.');
183
+ return locator;
132
184
  }
133
185
  export async function completeToolResultResources(directory, record) {
134
186
  await Promise.all(recordToolResources(record).map((resource) => rm(join(directory, 'tool_results', `.${resource.resourceRef}.pending`), {
@@ -177,22 +229,20 @@ export async function readToolResultResource(directory, sessionId, input, readRe
177
229
  let locator;
178
230
  try {
179
231
  const file = join(directory, 'tool_results', `${input.resourceRef}.json`);
180
- locator = JSON.parse(await readFile(file, 'utf8'));
232
+ locator = await readToolResourceLocator(file, input.resourceRef);
181
233
  }
182
234
  catch (error) {
183
235
  throw normalizedResourceError(error);
184
236
  }
185
- if (!Number.isSafeInteger(locator.sequence) ||
186
- locator.sequence < 1 ||
187
- typeof locator.recordId !== 'string')
188
- throw resourceError('CORRUPT', 'Invalid tool resource locator.');
237
+ if (locator === undefined)
238
+ throw resourceError('NOT_FOUND', 'Tool resource is unavailable.');
189
239
  const record = await readRecord(locator.sequence);
190
240
  const resource = record?.recordId === locator.recordId
191
241
  ? recordToolResources(record).find((entry) => entry.resourceRef === input.resourceRef)
192
242
  : undefined;
193
243
  if (!resource)
194
244
  throw resourceError('NOT_FOUND', 'Tool resource is not part of this session history.');
195
- const bytes = await readResourceBytes(directory, resource);
245
+ const bytes = await readResourceBytes(directory, resource, readRecord, locator);
196
246
  let offset = 0;
197
247
  if (input.cursor !== undefined) {
198
248
  try {
@@ -237,10 +287,31 @@ export async function readToolResultResource(directory, sessionId, input, readRe
237
287
  : {})
238
288
  };
239
289
  }
240
- async function readResourceBytes(directory, resource) {
290
+ async function readResourceBytes(directory, resource, readRecord, knownLocator) {
241
291
  try {
242
292
  return await readFile(join(directory, 'tool_results', `${resource.resourceRef}.diff`));
243
293
  }
294
+ catch (error) {
295
+ if (error.code !== 'ENOENT')
296
+ throw normalizedResourceError(error);
297
+ }
298
+ try {
299
+ const locator = knownLocator ??
300
+ (await readToolResourceLocator(join(directory, 'tool_results', `${resource.resourceRef}.json`), resource.resourceRef));
301
+ if (!locator?.bodySequences || !readRecord)
302
+ throw resourceError('NOT_FOUND', 'Tool resource is unavailable.');
303
+ const chunks = [];
304
+ for (const sequence of locator.bodySequences) {
305
+ const record = await readRecord(sequence);
306
+ const payload = record?.payload;
307
+ if (record?.type !== 'tool.resource' ||
308
+ payload?.resourceRef !== resource.resourceRef ||
309
+ typeof payload.dataBase64 !== 'string')
310
+ throw resourceError('CORRUPT', 'Invalid tool resource body.');
311
+ chunks.push(Buffer.from(payload.dataBase64, 'base64'));
312
+ }
313
+ return Buffer.concat(chunks);
314
+ }
244
315
  catch (error) {
245
316
  throw normalizedResourceError(error);
246
317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.11.54",
3
+ "version": "0.11.56",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",