@myagentroam/agent 0.11.52 → 0.11.54

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 (46) hide show
  1. package/dist/model/anthropic-messages.d.ts +4 -0
  2. package/dist/model/anthropic-messages.js +1 -1
  3. package/dist/model/native-context.d.ts +6 -0
  4. package/dist/model/native-context.js +15 -0
  5. package/dist/runtime/compaction/retained-context.d.ts +5 -0
  6. package/dist/runtime/compaction/retained-context.js +44 -1
  7. package/dist/runtime/context/gc-candidates-code.js +36 -21
  8. package/dist/runtime/context/gc-candidates-exec.js +33 -56
  9. package/dist/runtime/context/gc-candidates-read-patch.d.ts +1 -1
  10. package/dist/runtime/context/gc-candidates-read-patch.js +98 -15
  11. package/dist/runtime/context/gc-candidates-web.js +172 -8
  12. package/dist/runtime/context/gc-contracts.d.ts +2 -0
  13. package/dist/runtime/context/gc-economics.d.ts +0 -6
  14. package/dist/runtime/context/gc-economics.js +1 -23
  15. package/dist/runtime/context/gc-tokens.js +18 -8
  16. package/dist/runtime/context/gc.d.ts +1 -3
  17. package/dist/runtime/context/gc.js +19 -96
  18. package/dist/runtime/context/projection.js +11 -1
  19. package/dist/sdk/execution-compact-commit.d.ts +4 -2
  20. package/dist/sdk/execution-compact-commit.js +7 -3
  21. package/dist/sdk/execution-compaction.d.ts +1 -1
  22. package/dist/sdk/execution-compaction.js +21 -16
  23. package/dist/sdk/session-execution-factory.js +2 -1
  24. package/dist/session/code-mode-search-projection.d.ts +5 -0
  25. package/dist/session/code-mode-search-projection.js +46 -0
  26. package/dist/session/context-gc-checkpoint.js +233 -17
  27. package/dist/session/context-gc-contract.d.ts +3 -2
  28. package/dist/session/context-gc-exec-lifecycle.d.ts +11 -0
  29. package/dist/session/context-gc-exec-lifecycle.js +41 -0
  30. package/dist/session/context-gc-markup.d.ts +1 -0
  31. package/dist/session/context-gc-markup.js +4 -1
  32. package/dist/session/context-gc-observations.d.ts +5 -1
  33. package/dist/session/context-gc-observations.js +186 -33
  34. package/dist/session/context-gc-protection.d.ts +8 -0
  35. package/dist/session/context-gc-protection.js +83 -0
  36. package/dist/session/context-gc-replacements.d.ts +6 -1
  37. package/dist/session/context-gc-replacements.js +54 -6
  38. package/dist/session/context-gc-todo.d.ts +13 -0
  39. package/dist/session/context-gc-todo.js +47 -0
  40. package/dist/session/tool-callers.d.ts +7 -0
  41. package/dist/session/tool-callers.js +28 -0
  42. package/dist/tools/callers.d.ts +1 -7
  43. package/dist/tools/callers.js +1 -28
  44. package/dist/tools/code-mode/index.d.ts +1 -3
  45. package/dist/tools/code-mode/index.js +3 -45
  46. package/package.json +1 -1
@@ -8,3 +8,7 @@ export declare class AnthropicMessagesAdapter implements ModelAdapter {
8
8
  private startAttempt;
9
9
  }
10
10
  export declare function isAnthropicRecoverableStreamError(error: unknown): error is AgentError;
11
+ export declare function anthropicMessages(messages: ModelRequest['messages'], images?: ModelRequest['images'], tools?: ModelRequest['tools'], modelId?: string, automaticCache?: boolean, cachePrefixMessages?: number): {
12
+ role: string;
13
+ content: unknown;
14
+ }[];
@@ -536,7 +536,7 @@ function boundedError(value, maximum) {
536
536
  return undefined;
537
537
  return [...normalized].slice(0, maximum).join('');
538
538
  }
539
- function anthropicMessages(messages, images, tools = [], modelId, automaticCache = true, cachePrefixMessages) {
539
+ export function anthropicMessages(messages, images, tools = [], modelId, automaticCache = true, cachePrefixMessages) {
540
540
  if (cachePrefixMessages !== undefined &&
541
541
  (!Number.isSafeInteger(cachePrefixMessages) ||
542
542
  cachePrefixMessages < 0 ||
@@ -1,3 +1,9 @@
1
1
  import type { ModelMessage } from './contracts.js';
2
+ /** Adapters persist native items at completion; consumers need only type and identity. */
3
+ export declare function completedNativeThinkingIdentity(payload: {
4
+ role?: unknown;
5
+ provider?: unknown;
6
+ item?: unknown;
7
+ }, recordId: string): string | undefined;
2
8
  /** Interpret only the existing native context record shapes and their model ownership. */
3
9
  export declare function restoreNativeModelContext(payload: Record<string, unknown>, activeModelId?: string): ModelMessage | undefined;
@@ -1,4 +1,19 @@
1
1
  import { anthropicThinkingBlock } from './anthropic-thinking.js';
2
+ /** Adapters persist native items at completion; consumers need only type and identity. */
3
+ export function completedNativeThinkingIdentity(payload, recordId) {
4
+ if (payload.role !== 'provider')
5
+ return undefined;
6
+ const item = payload.item;
7
+ if (!item || typeof item !== 'object' || Array.isArray(item))
8
+ return undefined;
9
+ const block = item;
10
+ if (payload.provider === 'ANTHROPIC_MESSAGES') {
11
+ return block.type === 'thinking' || block.type === 'redacted_thinking' ? recordId : undefined;
12
+ }
13
+ if (payload.provider !== 'OPENAI_RESPONSES' || block.type !== 'reasoning')
14
+ return undefined;
15
+ return typeof block.id === 'string' ? block.id : recordId;
16
+ }
2
17
  /** Interpret only the existing native context record shapes and their model ownership. */
3
18
  export function restoreNativeModelContext(payload, activeModelId) {
4
19
  const modelId = typeof payload.providerModelId === 'string' && payload.providerModelId.length > 0
@@ -1,5 +1,10 @@
1
1
  import type { SessionModelMessage as ModelMessage } from '../../session/model-request.js';
2
2
  import type { CompactSnapshotAnchor } from '../events.js';
3
+ /** Keep the live suffix intact, extending into the snapshot only to fill three native blocks. */
4
+ export declare function compactRetainedStart(messages: readonly ModelMessage[], snapshotLength: number, model: {
5
+ readonly id: string;
6
+ readonly protocol: ModelMessage['provider'];
7
+ }): number;
3
8
  /** Use a public native identity, never private context text, as the visible boundary. */
4
9
  export declare function compactSnapshotAnchor(messages: readonly ModelMessage[], sources: WeakMap<ModelMessage, import('../../session/record-contract.js').SessionRecord>): CompactSnapshotAnchor | undefined;
5
10
  /** Decode the SDK-owned checkpoint suffix without promoting contextual text to real input. */
@@ -1,6 +1,49 @@
1
1
  import { parseModelInputMetadata } from '../../session/record-projection.js';
2
- import { restoreModelContext } from '../context/projection.js';
2
+ import { providerToolCallId, restoreModelContext } from '../context/projection.js';
3
3
  import { parseModelImages } from '../../model/image.js';
4
+ import { completedNativeThinkingIdentity } from '../../model/native-context.js';
5
+ /** Keep the live suffix intact, extending into the snapshot only to fill three native blocks. */
6
+ export function compactRetainedStart(messages, snapshotLength, model) {
7
+ let start = snapshotLength;
8
+ const thinking = new Set();
9
+ for (let index = messages.length - 1; index >= 0 && thinking.size < 3; index--) {
10
+ const message = messages[index];
11
+ if (message.provider !== model.protocol ||
12
+ (message.providerModelId !== undefined && message.providerModelId !== model.id))
13
+ continue;
14
+ const identity = completedNativeThinkingIdentity(message, `message:${index}`);
15
+ if (identity === undefined || thinking.has(identity))
16
+ continue;
17
+ thinking.add(identity);
18
+ start = Math.min(start, index);
19
+ }
20
+ // The existing snapshot boundary already closes tool groups.
21
+ if (start === snapshotLength)
22
+ return start;
23
+ // A native item can follow text or tool calls within the same assistant response.
24
+ while (start > 0 && messages[start - 1].role !== 'user' && messages[start - 1].role !== 'tool')
25
+ start--;
26
+ // Resolve any retained result back to its call's assistant group in two linear passes.
27
+ const calls = new Map();
28
+ let responseStart = 0;
29
+ for (let index = 0; index < start; index++) {
30
+ const message = messages[index];
31
+ if (message.role === 'user' || message.role === 'tool')
32
+ responseStart = index + 1;
33
+ const callId = message.role === 'tool_call' ? message.callId : providerToolCallId(message);
34
+ if (callId !== undefined && !calls.has(callId))
35
+ calls.set(callId, responseStart);
36
+ }
37
+ for (let index = messages.length - 1; index >= start; index--) {
38
+ const message = messages[index];
39
+ if (message.role !== 'tool' || message.callId === undefined)
40
+ continue;
41
+ const callStart = calls.get(message.callId);
42
+ if (callStart !== undefined)
43
+ start = Math.min(start, callStart);
44
+ }
45
+ return start;
46
+ }
4
47
  /** Use a public native identity, never private context text, as the visible boundary. */
5
48
  export function compactSnapshotAnchor(messages, sources) {
6
49
  for (let index = messages.length - 1; index >= 0; index--) {
@@ -1,8 +1,8 @@
1
1
  import { retainedCodeInputs } from '../../session/context-gc-observations.js';
2
2
  import { contextGcTargetKey } from '../../session/context-gc-checkpoint.js';
3
- import { codeCellCompletionSequence, contextGcCodeModeInputReplacement, contextGcCodeModeOutputReplacement, normalizeContextGcCodeModeInput } from '../../session/context-gc-replacements.js';
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 { successfulRolloutsAfter, targetFor, isRecord } from './gc-candidates.js';
5
+ import { targetFor, isRecord } from './gc-candidates.js';
6
6
  export function collectCodeModeCandidates(input, groups, existingTargets, candidateSavings) {
7
7
  const eligible = [];
8
8
  let protectedTokens = 0;
@@ -10,22 +10,12 @@ export function collectCodeModeCandidates(input, groups, existingTargets, candid
10
10
  for (const group of [...groups.values()].filter((item) => ['code_exec', 'code_exec_wait'].includes(item.toolName))) {
11
11
  if (!group.call ||
12
12
  !group.result ||
13
- group.failed ||
14
- !group.completed ||
15
13
  group.call.message.role !== 'tool_call' ||
16
14
  group.result.message.role !== 'tool' ||
17
15
  typeof group.result.message.content !== 'string')
18
16
  continue;
19
- const code = normalizeContextGcCodeModeInput(group.call.message.arguments);
20
- const completedPayload = group.completed.payload;
21
- const completionSequence = codeCellCompletionSequence(input.records, group.completed);
22
- if ((group.toolName !== 'code_exec_wait' && !code) ||
23
- completionSequence === undefined ||
24
- !isRecord(completedPayload) ||
25
- typeof completedPayload.truncated !== 'boolean')
26
- continue;
27
17
  for (const observation of group.result.message.observations ?? []) {
28
- if (observation.type !== 'value')
18
+ if (observation.type !== 'value' || !hasGenericContextGcPreview(observation.value))
29
19
  continue;
30
20
  const target = targetFor(group.result.record, {
31
21
  ...group.result.message,
@@ -34,29 +24,41 @@ export function collectCodeModeCandidates(input, groups, existingTargets, candid
34
24
  if (!target || existingTargets.has(contextGcTargetKey(target)))
35
25
  continue;
36
26
  const evidenceSequence = group.result.record.sequence;
27
+ const protection = input.protection.after(evidenceSequence);
37
28
  const candidate = {
38
29
  replacements: [
39
30
  {
40
31
  kind: 'CODE_MODE_OUTPUT',
41
32
  callId: group.callId,
42
33
  target,
43
- replacementContent: contextGcCodeModeOutputReplacement(observation.value, observation.truncated === true),
34
+ replacementContent: contextGcCodeModeOutputReplacement(observation.value, observation.truncated === true, true),
44
35
  evidenceSequence
45
36
  }
46
37
  ],
47
- observedRollouts: successfulRolloutsAfter(input.records, Math.max(evidenceSequence, completionSequence), input.modelId, input.contextWindowTokens),
38
+ observedRollouts: protection.rollouts,
48
39
  evidenceSequence
49
40
  };
50
41
  const reclaimedTokens = candidateSavings(candidate);
51
42
  if (reclaimedTokens <= 0)
52
43
  continue;
53
- if (candidate.observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts)
44
+ if (candidate.observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts &&
45
+ protection.thinking >= 3)
54
46
  eligible.push(candidate);
55
- else {
47
+ else if (candidate.observedRollouts < CONTEXT_GC_POLICY.largeExecProtectionRollouts) {
56
48
  protectedTokens += reclaimedTokens;
57
49
  retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.largeExecProtectionRollouts - candidate.observedRollouts);
58
50
  }
59
51
  }
52
+ if (group.failed || !group.completed)
53
+ continue;
54
+ const code = normalizeContextGcCodeModeInput(group.call.message.arguments);
55
+ const completedPayload = group.completed.payload;
56
+ const completionSequence = codeCellCompletionSequence(input.records, group.completed);
57
+ if ((group.toolName !== 'code_exec_wait' && !code) ||
58
+ completionSequence === undefined ||
59
+ !isRecord(completedPayload) ||
60
+ typeof completedPayload.truncated !== 'boolean')
61
+ continue;
60
62
  const callTarget = targetFor(group.call.record, group.call.message);
61
63
  const resultTarget = targetFor(group.result.record, group.result.message);
62
64
  if (!callTarget || !resultTarget)
@@ -69,7 +71,18 @@ export function collectCodeModeCandidates(input, groups, existingTargets, candid
69
71
  toolName: group.toolName,
70
72
  callId: group.callId,
71
73
  target: callTarget,
72
- replacementArguments: contextGcCodeModeInputReplacement(code, group.result.message.observations ? retainedCodeInputs(input.records, group.callId) : []),
74
+ replacementArguments: contextGcCodeModeInputReplacement(code, group.result.message.observations
75
+ ? retainedCodeInputs(input.records, group.callId, (original, successor) => {
76
+ const maturity = input.protection.after(Math.max(evidenceSequence, completionSequence, original.sequence, successor.sequence));
77
+ return maturity.rollouts >= 6 && maturity.thinking >= 3;
78
+ }, {
79
+ activeProcessIds: input.activeProcessIds,
80
+ evidenceMature: (sequence) => {
81
+ const maturity = input.protection.after(Math.max(evidenceSequence, completionSequence, sequence));
82
+ return maturity.rollouts >= 6 && maturity.thinking >= 3;
83
+ }
84
+ })
85
+ : []),
73
86
  providerAliasTargets: group.aliases
74
87
  .map(({ record, message }) => targetFor(record, message))
75
88
  .filter((target) => target !== undefined),
@@ -86,14 +99,16 @@ export function collectCodeModeCandidates(input, groups, existingTargets, candid
86
99
  });
87
100
  if (replacements.length === 0)
88
101
  continue;
89
- const observedRollouts = successfulRolloutsAfter(input.records, Math.max(evidenceSequence, completionSequence), input.modelId, input.contextWindowTokens);
102
+ const protection = input.protection.after(Math.max(evidenceSequence, completionSequence));
103
+ const observedRollouts = protection.rollouts;
90
104
  const candidate = { replacements, observedRollouts, evidenceSequence };
91
105
  const reclaimedTokens = candidateSavings(candidate);
92
106
  if (reclaimedTokens <= 0)
93
107
  continue;
94
- if (observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts)
108
+ if (observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts &&
109
+ protection.thinking >= 3)
95
110
  eligible.push(candidate);
96
- else {
111
+ else if (observedRollouts < CONTEXT_GC_POLICY.largeExecProtectionRollouts) {
97
112
  protectedTokens += reclaimedTokens;
98
113
  retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.largeExecProtectionRollouts - observedRollouts);
99
114
  }
@@ -1,7 +1,8 @@
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 { successfulRolloutsAfter, targetFor, isRecord } from './gc-candidates.js';
4
+ import { targetFor, isRecord } from './gc-candidates.js';
5
+ import { terminalExecLifecycles } from '../../session/context-gc-exec-lifecycle.js';
5
6
  export function collectExecCandidates(input, groups, existingTargets, candidateSavings) {
6
7
  const eligible = [];
7
8
  let protectedTokens = 0;
@@ -28,56 +29,18 @@ export function collectExecCandidates(input, groups, existingTargets, candidateS
28
29
  result
29
30
  });
30
31
  }
31
- for (const startGroup of execGroups) {
32
- const start = invocations.get(startGroup.callId);
33
- if (!start)
34
- continue;
35
- const isStart = start.group.toolName === 'exec' &&
36
- (start.input.action === 'start' ||
37
- (start.input.action == null && typeof start.input.command === 'string'));
38
- const processId = typeof start.input.processId === 'string'
39
- ? start.input.processId
40
- : typeof start.result.processId === 'string'
41
- ? start.result.processId
42
- : undefined;
43
- if (!isStart) {
44
- if (start.group.toolName !== 'exec_wait' || !processId)
45
- continue;
46
- // A process may start inside a cell or before the visible history boundary.
47
- // Begin with its first visible wait, but never split an unfinished lifecycle.
48
- const previous = execGroups.findLast((group) => (group.call?.record.sequence ?? Infinity) < start.group.call.record.sequence &&
49
- ((isRecord(group.call?.message.arguments) &&
50
- group.call.message.arguments.processId === processId) ||
51
- invocations.get(group.callId)?.result.processId === processId));
52
- const previousStatus = previous && invocations.get(previous.callId)?.result.status;
53
- if (previous && previousStatus !== 'completed' && previousStatus !== 'cancelled')
54
- continue;
55
- }
56
- const lifecycle = [start];
57
- let terminal = start.result.status !== 'running';
58
- if (!terminal) {
59
- if (!processId)
60
- continue;
61
- const relatedGroups = execGroups.filter((group) => (group.call?.record.sequence ?? 0) > start.group.call.record.sequence &&
62
- isRecord(group.call?.message.arguments) &&
63
- group.call.message.arguments.processId === processId);
64
- if (relatedGroups.some((group) => !invocations.has(group.callId)) ||
65
- relatedGroups.length === 0)
66
- continue;
67
- for (const relatedGroup of relatedGroups) {
68
- const invocation = invocations.get(relatedGroup.callId);
69
- lifecycle.push(invocation);
70
- if (invocation.result.status !== 'running') {
71
- terminal = true;
72
- break;
73
- }
74
- }
75
- }
76
- if (!terminal || (processId && input.activeProcessIds.has(processId)))
77
- continue;
32
+ const facts = execGroups.map((group) => ({
33
+ value: group,
34
+ callId: group.callId,
35
+ toolName: group.toolName,
36
+ sequence: group.call?.record.sequence ?? Number.MAX_SAFE_INTEGER,
37
+ ...(isRecord(group.call?.message.arguments) ? { input: group.call.message.arguments } : {}),
38
+ ...(invocations.get(group.callId) ? { result: invocations.get(group.callId).result } : {})
39
+ }));
40
+ for (const selected of terminalExecLifecycles(facts, input.activeProcessIds)) {
41
+ const lifecycle = selected.map((fact) => invocations.get(fact.callId));
78
42
  const final = lifecycle.at(-1);
79
43
  const evidenceSequence = final.group.result.record.sequence;
80
- const observedRollouts = successfulRolloutsAfter(input.records, evidenceSequence, input.modelId, input.contextWindowTokens);
81
44
  const replacements = [];
82
45
  for (const invocation of lifecycle) {
83
46
  if (invocation.group.evidenceOnly)
@@ -112,20 +75,34 @@ export function collectExecCandidates(input, groups, existingTargets, candidateS
112
75
  }
113
76
  if (replacements.length === 0)
114
77
  continue;
78
+ const mature = [];
79
+ const awaitingRollouts = [];
80
+ let observedRollouts = Infinity;
81
+ for (const replacement of replacements) {
82
+ const protection = input.protection.after(Math.max(evidenceSequence, replacement.target.sequence));
83
+ if (protection.rollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts &&
84
+ protection.thinking >= 3) {
85
+ mature.push(replacement);
86
+ observedRollouts = Math.min(observedRollouts, protection.rollouts);
87
+ }
88
+ else if (protection.rollouts < CONTEXT_GC_POLICY.largeExecProtectionRollouts) {
89
+ awaitingRollouts.push(replacement);
90
+ retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.largeExecProtectionRollouts - protection.rollouts);
91
+ }
92
+ }
93
+ if (awaitingRollouts.length)
94
+ protectedTokens += Math.max(0, candidateSavings({ replacements: awaitingRollouts, observedRollouts: 0, evidenceSequence }));
95
+ if (!mature.length)
96
+ continue;
115
97
  const candidate = {
116
- replacements,
98
+ replacements: mature,
117
99
  observedRollouts,
118
100
  evidenceSequence
119
101
  };
120
102
  const reclaimedTokens = candidateSavings(candidate);
121
103
  if (reclaimedTokens <= 0)
122
104
  continue;
123
- if (observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts)
124
- eligible.push(candidate);
125
- else {
126
- protectedTokens += reclaimedTokens;
127
- retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.largeExecProtectionRollouts - observedRollouts);
128
- }
105
+ eligible.push(candidate);
129
106
  }
130
107
  return { eligible, protectedTokens, retryAfterRollouts };
131
108
  }
@@ -1,6 +1,6 @@
1
1
  import { type ContextGcReplacement } from '../../session/context-gc-checkpoint.js';
2
2
  import { type ContextGcPlannerInput, type ContextCandidate, type CandidateSavings, type ToolGroup } from './gc-contracts.js';
3
- export declare function collectStageACandidates(input: ContextGcPlannerInput, groups: ReadonlyMap<string, ToolGroup>, existingReplacements: readonly ContextGcReplacement[], existingTargets: ReadonlySet<string>, candidateSavings: CandidateSavings): {
3
+ export declare function collectDeterministicCandidates(input: ContextGcPlannerInput, groups: ReadonlyMap<string, ToolGroup>, existingReplacements: readonly ContextGcReplacement[], existingTargets: ReadonlySet<string>, candidateSavings: CandidateSavings): {
4
4
  eligible: ContextCandidate[];
5
5
  protectedTokens: number;
6
6
  retryAfterRollouts: number;
@@ -1,18 +1,20 @@
1
1
  import { contextGcLogicalPathKey, contextGcTargetKey } from '../../session/context-gc-checkpoint.js';
2
- import { contextGcApplyPatchInputReplacement, contextGcPatchResultFromCompletedPayload, contextGcPatchResultFromFailedPayload, contextGcStaleReadReplacement } from '../../session/context-gc-replacements.js';
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 { successfulRolloutsAfter, targetFor } from './gc-candidates.js';
6
- export function collectStageACandidates(input, groups, existingReplacements, existingTargets, candidateSavings) {
5
+ import { targetFor } from './gc-candidates.js';
6
+ export function collectDeterministicCandidates(input, groups, existingReplacements, existingTargets, candidateSavings) {
7
7
  const eligible = [];
8
8
  let protectedTokens = 0;
9
9
  let retryAfterRollouts = Infinity;
10
10
  const existingReadReplacements = new Map(existingReplacements.flatMap((replacement) => replacement.kind === 'READ_OUTPUT_ITEMS'
11
11
  ? [[contextGcTargetKey(replacement.target), replacement]]
12
12
  : []));
13
- const reads = [...groups.values()].filter((group) => group.toolName === 'read' &&
13
+ const reads = [...groups.values()]
14
+ .filter((group) => group.toolName === 'read' &&
14
15
  group.call?.message.role === 'tool_call' &&
15
- group.result?.message.role === 'tool');
16
+ group.result?.message.role === 'tool')
17
+ .sort((left, right) => left.result.record.sequence - right.result.record.sequence);
16
18
  const claimedReadItems = new Set([...existingReadReplacements.values()].flatMap((replacement) => replacement.items.map((item) => `${replacement.target.recordId}:${replacement.target.observationId ?? ''}:${item.index}`)));
17
19
  const patches = [...groups.values()]
18
20
  .flatMap((patch) => {
@@ -31,7 +33,6 @@ export function collectStageACandidates(input, groups, existingReplacements, exi
31
33
  .sort((left, right) => left.evidenceSequence - right.evidenceSequence);
32
34
  for (const { patch, result, terminal, evidenceSequence } of patches) {
33
35
  const patchCall = patch.call;
34
- const observedRollouts = successfulRolloutsAfter(input.records, evidenceSequence, input.modelId, input.contextWindowTokens);
35
36
  const replacements = [];
36
37
  const candidateReadItems = new Set();
37
38
  const patchTarget = targetFor(patchCall.record, patchCall.message);
@@ -75,7 +76,7 @@ export function collectStageACandidates(input, groups, existingReplacements, exi
75
76
  operation: matched.operation
76
77
  }),
77
78
  evidenceSequence,
78
- ...(read.result.message.observationId ? { supersededByCallId: patch.callId } : {})
79
+ supersededByCallId: patch.callId
79
80
  }
80
81
  ];
81
82
  });
@@ -97,21 +98,103 @@ export function collectStageACandidates(input, groups, existingReplacements, exi
97
98
  }
98
99
  if (replacements.length === 0)
99
100
  continue;
101
+ const mature = [];
102
+ const awaitingRollouts = [];
103
+ let observedRollouts = Infinity;
104
+ for (const replacement of replacements) {
105
+ const protection = input.protection.after(Math.max(evidenceSequence, replacement.target.sequence));
106
+ if (protection.rollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts &&
107
+ protection.thinking >= 3) {
108
+ mature.push(replacement);
109
+ observedRollouts = Math.min(observedRollouts, protection.rollouts);
110
+ }
111
+ else if (protection.rollouts < CONTEXT_GC_POLICY.largeExecProtectionRollouts) {
112
+ awaitingRollouts.push(replacement);
113
+ retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.largeExecProtectionRollouts - protection.rollouts);
114
+ }
115
+ }
116
+ if (awaitingRollouts.length) {
117
+ protectedTokens += Math.max(0, candidateSavings({ replacements: awaitingRollouts, observedRollouts: 0, evidenceSequence }));
118
+ }
119
+ if (!mature.length)
120
+ continue;
100
121
  const candidate = {
101
- replacements,
122
+ replacements: mature,
102
123
  observedRollouts,
103
124
  evidenceSequence
104
125
  };
105
126
  const reclaimedTokens = candidateSavings(candidate);
106
127
  if (reclaimedTokens <= 0)
107
128
  continue;
108
- for (const itemKey of candidateReadItems)
109
- claimedReadItems.add(itemKey);
110
- if (observedRollouts >= CONTEXT_GC_POLICY.stageAProtectionRollouts)
111
- eligible.push(candidate);
112
- else {
113
- protectedTokens += reclaimedTokens;
114
- retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.stageAProtectionRollouts - observedRollouts);
129
+ for (const replacement of mature) {
130
+ if (replacement.kind !== 'READ_OUTPUT_ITEMS')
131
+ continue;
132
+ for (const item of replacement.items) {
133
+ const key = `${replacement.target.recordId}:${replacement.target.observationId ?? ''}:${item.index}`;
134
+ if (candidateReadItems.has(key))
135
+ claimedReadItems.add(key);
136
+ }
137
+ }
138
+ eligible.push(candidate);
139
+ }
140
+ const visibleReads = reads.flatMap((group) => {
141
+ if (group.evidenceOnly || group.failed || !group.completed)
142
+ return [];
143
+ const target = targetFor(group.result.record, group.result.message);
144
+ const parsed = parseReadContextOutput(group.result.message.content);
145
+ return target && parsed ? [{ group, target, items: parsed.items }] : [];
146
+ });
147
+ for (const [index, old] of visibleReads.entries()) {
148
+ for (const item of old.items) {
149
+ const itemKey = `${old.target.recordId}:${old.target.observationId ?? ''}:${item.index}`;
150
+ if (claimedReadItems.has(itemKey) || item.content.startsWith('<mar_context_gc '))
151
+ continue;
152
+ const pathKey = contextGcLogicalPathKey(item.path, input.workspace, input.platform);
153
+ if (!pathKey)
154
+ continue;
155
+ const later = visibleReads
156
+ .slice(index + 1)
157
+ .filter((entry) => entry.items.some((candidate) => contextGcLogicalPathKey(candidate.path, input.workspace, input.platform) ===
158
+ pathKey &&
159
+ contextGcReadRangeCovers(item, candidate) &&
160
+ !existingReadReplacements
161
+ .get(contextGcTargetKey(entry.target))
162
+ ?.items.some((prior) => prior.index === candidate.index)));
163
+ const successor = later.findLast((entry) => {
164
+ const count = input.protection.after(entry.target.sequence);
165
+ return count.rollouts >= 6 && count.thinking >= 3;
166
+ });
167
+ if (!successor)
168
+ continue;
169
+ const existingRead = existingReadReplacements.get(contextGcTargetKey(old.target));
170
+ const evidenceSequence = successor.target.sequence;
171
+ const replacement = {
172
+ kind: 'READ_OUTPUT_ITEMS',
173
+ callId: old.group.callId,
174
+ target: old.target,
175
+ items: [
176
+ ...(existingRead?.items ?? []),
177
+ {
178
+ index: item.index,
179
+ path: item.path,
180
+ replacement: contextGcCoveredReadReplacement(item, successor.group.callId),
181
+ evidenceSequence,
182
+ supersededByCallId: successor.group.callId
183
+ }
184
+ ],
185
+ evidenceSequences: [
186
+ ...new Set([...(existingRead?.evidenceSequences ?? []), evidenceSequence])
187
+ ]
188
+ };
189
+ const candidate = {
190
+ replacements: [replacement],
191
+ observedRollouts: input.protection.after(evidenceSequence).rollouts,
192
+ evidenceSequence
193
+ };
194
+ if (candidateSavings(candidate) > 0) {
195
+ eligible.push(candidate);
196
+ claimedReadItems.add(itemKey);
197
+ }
115
198
  }
116
199
  }
117
200
  return { eligible, protectedTokens, retryAfterRollouts };