@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
@@ -17,7 +17,6 @@ export function createExecutionCompaction(ports) {
17
17
  message.contextKind === 'summary'))
18
18
  return false;
19
19
  const history = structuredClone(prefix).map(({ images: _images, ...message }) => message);
20
- const prefixInputs = modelContext.compactInputs(prefix);
21
20
  const tools = structuredClone(ports.snapshotTools());
22
21
  const generation = modelContext.worldState.generation;
23
22
  const usage = ports.usage();
@@ -62,7 +61,7 @@ export function createExecutionCompaction(ports) {
62
61
  await prepare();
63
62
  signal.throwIfAborted();
64
63
  const resources = await ports.retainedResources();
65
- const replacementHistory = buildCompactedHistory(candidate.summary, prefixInputs, ports.userTokenLimit);
64
+ let replacementHistory;
66
65
  let committed = false;
67
66
  let retained;
68
67
  try {
@@ -72,6 +71,7 @@ export function createExecutionCompaction(ports) {
72
71
  signal,
73
72
  prefix,
74
73
  messages,
74
+ model: ports.model,
75
75
  generation,
76
76
  messageSources: ports.messageSources,
77
77
  checkpoint: (value) => ports.resourceInput.checkpoint(value),
@@ -85,21 +85,26 @@ export function createExecutionCompaction(ports) {
85
85
  modelContext.replaceCompactMessages(replacementHistory, value);
86
86
  ports.updateBoundary(messages.length - value.length);
87
87
  },
88
- draft: () => ({
89
- summary: candidate.summary,
90
- replacementHistory,
91
- pendingInputs: [...ports.pendingInputs.values()],
92
- snapshotMessageCount: prefix.length,
93
- ...(anchorRecordId ? { snapshotRecordId: anchorRecordId } : {}),
94
- automatic: ports.mode !== 'compact',
95
- environment: ports.environment(),
96
- modelId: ports.model.id,
97
- contextWindowTokens: ports.model.contextWindowTokens,
98
- ...(candidate.usage === undefined ? {} : { usage: candidate.usage }),
99
- executionId: ports.executionId,
100
- turnId: ports.turnId
101
- })
88
+ draft: (retainedStart) => {
89
+ replacementHistory = buildCompactedHistory(candidate.summary, modelContext.compactInputs(prefix.slice(0, retainedStart)), ports.userTokenLimit);
90
+ return {
91
+ summary: candidate.summary,
92
+ replacementHistory,
93
+ pendingInputs: [...ports.pendingInputs.values()],
94
+ snapshotMessageCount: prefix.length,
95
+ ...(anchorRecordId ? { snapshotRecordId: anchorRecordId } : {}),
96
+ automatic: ports.mode !== 'compact',
97
+ environment: ports.environment(),
98
+ modelId: ports.model.id,
99
+ contextWindowTokens: ports.model.contextWindowTokens,
100
+ ...(candidate.usage === undefined ? {} : { usage: candidate.usage }),
101
+ executionId: ports.executionId,
102
+ turnId: ports.turnId
103
+ };
104
+ }
102
105
  });
106
+ if (!result)
107
+ return false;
103
108
  retained = result.retained;
104
109
  await modelContext.adoptCompactCheckpoint(result.record.recordId, replacementHistory, retained, resources);
105
110
  ports.invalidateUsage();
@@ -165,7 +165,8 @@ export function createSessionExecutionFactory(ports) {
165
165
  const compactMayContainAgentInstructions = contextRecords.some((record) => compactSummaryFromCheckpoint(record) !== undefined);
166
166
  const restored = await restoreMessages(store, sessionId, contextRecords, {
167
167
  messageSources,
168
- includeImages: mode !== 'compact'
168
+ // Keep durable references for the retained tail; the Compact snapshot removes images.
169
+ includeImages: true
169
170
  });
170
171
  let modelLifecycle;
171
172
  let applyCompactBoundary = () => { };
@@ -0,0 +1,5 @@
1
+ import type { ClientToolDefinition } from '../tools/contracts.js';
2
+ import type { SessionModelMessage as ModelMessage } from './model-request.js';
3
+ export declare function projectProgrammaticTool(tool: ClientToolDefinition): Pick<ClientToolDefinition, 'name' | 'description' | 'inputSchema'>;
4
+ /** Preserve native search definitions while providing the same discovery evidence to other protocols. */
5
+ export declare function projectCodeModeSearchResult(message: ModelMessage, codeModeEnabled: boolean): ModelMessage;
@@ -0,0 +1,46 @@
1
+ import { stableToolDefinition, stableToolSchema } from '../model/tool-definition-projection.js';
2
+ import { allowsDirectTool, allowsToolCaller } from './tool-callers.js';
3
+ export function projectProgrammaticTool(tool) {
4
+ return {
5
+ name: tool.name,
6
+ description: `${allowsDirectTool(tool, true) ? `Call directly or as tools.${tool.name} inside code_exec.` : `Call only as tools.${tool.name} inside code_exec, never as a direct model tool.`}\n${tool.description}`,
7
+ inputSchema: stableToolSchema(tool.inputSchema)
8
+ };
9
+ }
10
+ /** Preserve native search definitions while providing the same discovery evidence to other protocols. */
11
+ export function projectCodeModeSearchResult(message, codeModeEnabled) {
12
+ if (!message.toolSearchTools)
13
+ return message;
14
+ if (!codeModeEnabled)
15
+ return {
16
+ ...message,
17
+ toolSearchTools: message.toolSearchTools
18
+ .filter((tool) => allowsToolCaller(tool, 'direct'))
19
+ .map(stableToolDefinition)
20
+ };
21
+ const tools = message.toolSearchTools.map((tool) => ({
22
+ ...(allowsToolCaller(tool, 'programmatic')
23
+ ? {
24
+ ...projectProgrammaticTool(tool),
25
+ ...(allowsDirectTool(tool, true)
26
+ ? { inputMode: tool.inputMode ?? 'json' }
27
+ : {})
28
+ }
29
+ : {
30
+ name: tool.name,
31
+ description: tool.description,
32
+ inputSchema: stableToolSchema(tool.inputSchema),
33
+ inputMode: tool.inputMode ?? 'json'
34
+ }),
35
+ caller: allowsToolCaller(tool, 'programmatic')
36
+ ? allowsDirectTool(tool, true)
37
+ ? 'both'
38
+ : 'programmatic'
39
+ : 'direct'
40
+ }));
41
+ return {
42
+ ...message,
43
+ content: JSON.stringify({ tools }),
44
+ toolSearchTools: tools.map(({ caller: _caller, ...tool }) => tool)
45
+ };
46
+ }
@@ -1,9 +1,12 @@
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';
5
+ import { completedTodoWrite, todoInputReplacement, todoSnapshotReplacement, visibleTodoSnapshot } from './context-gc-todo.js';
6
+ import { contextGcRetainedInputMarkup } from './context-gc-markup.js';
4
7
  import { codeCallEvidence, codePartialPatchCallEvidence, callsFromRecord, codeObservationEvidence, observationsFromRecord, retainedCodeInputs } from './context-gc-observations.js';
5
8
  import { parseReadContextOutput } from '../prompts/read-context.js';
6
- import { codeCellCompletionSequence, contextGcApplyPatchInputReplacement, contextGcCodeModeInputReplacement, contextGcCodeModeOutputReplacement, contextGcExecInputReplacement, contextGcExecOutputReplacement, contextGcPatchResultFromCompletedPayload, contextGcPatchResultFromFailedPayload, contextGcStaleReadReplacement, contextGcToolOutputReplacement, contextGcWebOutputReplacement, isGenericContextGcTool, normalizeContextGcCodeModeInput, normalizeContextGcExecResult } from './context-gc-replacements.js';
9
+ import { codeCellCompletionSequence, contextGcApplyPatchInputReplacement, contextGcCodeModeInputReplacement, contextGcCodeModeOutputReplacement, contextGcExecInputReplacement, contextGcExecOutputReplacement, contextGcPatchResultFromCompletedPayload, contextGcPatchResultFromFailedPayload, contextGcStaleReadReplacement, contextGcReadRangeCovers, contextGcCoveredReadReplacement, contextGcSkillOutputReplacement, contextGcToolOutputReplacement, contextGcWebOutputReplacement, isGenericContextGcTool, normalizeContextGcCodeModeInput, normalizeContextGcExecResult } from './context-gc-replacements.js';
7
10
  import { contextGcSubagentNeedsRetainedPriorFinal, contextGcSubagentOutputReplacement, contextGcSubagentSnapshotIsPreciselyCovered, contextGcSubagentSnapshotIsSuperseded, normalizeContextGcSubagentSnapshot } from './context-gc-subagent.js';
8
11
  export const CONTEXT_GC_CHECKPOINT_VERSION = 1;
9
12
  export const CONTEXT_GC_MAX_TARGETS = 4_096;
@@ -188,6 +191,7 @@ export function validateContextGcCheckpoint(records, record, workspace = session
188
191
  ]);
189
192
  if (new Set(targets.map(contextGcTargetKey)).size !== targets.length)
190
193
  return undefined;
194
+ const protection = buildContextGcProtection(records, payload.trigger.modelId, payload.trigger.contextWindowTokens);
191
195
  for (const replacement of changed) {
192
196
  for (const target of [
193
197
  replacement.target,
@@ -211,7 +215,34 @@ export function validateContextGcCheckpoint(records, record, workspace = session
211
215
  sequence > payload.boundarySequence ||
212
216
  !recordsBySequence.has(sequence)))
213
217
  return undefined;
214
- if (!replacementMatchesRecords(replacement, records, recordsBySequence, workspace, payload.trigger.platform))
218
+ const anchors = replacement.kind === 'READ_OUTPUT_ITEMS'
219
+ ? replacement.items.map((item) => Math.max(replacement.target.sequence, item.evidenceSequence))
220
+ : [Math.max(replacement.target.sequence, ...evidence)];
221
+ if (replacement.kind === 'TOOL_CALL_INPUT' && replacement.toolName === 'todo_write') {
222
+ const terminal = records.find((item) => completedTodoWrite(item)?.callId === replacement.callId);
223
+ if (terminal)
224
+ anchors[0] = Math.max(anchors[0], terminal.sequence);
225
+ }
226
+ if ((replacement.kind === 'TOOL_CALL_INPUT' && replacement.toolName === 'code_exec') ||
227
+ (replacement.kind === 'CODE_MODE_OUTPUT' && !replacement.target.observationId)) {
228
+ const result = recordsBySequence.get(replacement.evidenceSequence);
229
+ if (result && isRecord(result.payload) && typeof result.payload.callId === 'string') {
230
+ const callId = result.payload.callId;
231
+ const terminal = records.findLast((item) => item.sequence < result.sequence &&
232
+ item.type === 'tool.completed' &&
233
+ isRecord(item.payload) &&
234
+ item.payload.callId === callId);
235
+ const completion = terminal && codeCellCompletionSequence(records, terminal);
236
+ if (completion !== undefined)
237
+ anchors[0] = Math.max(anchors[0], completion);
238
+ }
239
+ }
240
+ if (anchors.some((anchor) => {
241
+ const count = protection.after(anchor);
242
+ return count.rollouts < 6 || count.thinking < 3;
243
+ }))
244
+ return undefined;
245
+ if (!replacementMatchesRecords(replacement, records, recordsBySequence, workspace, payload.trigger.platform, payload.replacements, protection, committed.get(contextGcReplacementKey(replacement))))
215
246
  return undefined;
216
247
  }
217
248
  return payload;
@@ -260,8 +291,18 @@ function targetMatchesRecord(target, record) {
260
291
  true) &&
261
292
  (target.toolName === undefined || target.toolName === messageToolName));
262
293
  }
294
+ function resultVisibleAfter(source, observationId, successor) {
295
+ if (successor.sequence !== source.sequence)
296
+ return successor.sequence > source.sequence;
297
+ if (!isRecord(successor.payload))
298
+ return false;
299
+ const observations = observationsFromRecord(source) ?? [];
300
+ const sourceIndex = observations.findIndex((value) => value.id === observationId);
301
+ const successorId = successor.payload.observationId;
302
+ return (sourceIndex >= 0 && observations.findIndex((value) => value.id === successorId) > sourceIndex);
303
+ }
263
304
  /** A checkpoint draft is valid only when its replacements match the source records and evidence. */
264
- function replacementMatchesRecords(replacement, records, recordsBySequence, workspace, platform) {
305
+ function replacementMatchesRecords(replacement, records, recordsBySequence, workspace, platform, allReplacements, protection, previous) {
265
306
  let source = recordsBySequence.get(replacement.target.sequence);
266
307
  if (!source || !isRecord(source.payload))
267
308
  return false;
@@ -286,7 +327,6 @@ function replacementMatchesRecords(replacement, records, recordsBySequence, work
286
327
  }
287
328
  else
288
329
  source = { ...source, payload: { ...source.payload, content: selected.value } };
289
- recordsBySequence = new Map(recordsBySequence).set(source.sequence, source);
290
330
  }
291
331
  if (!isRecord(source.payload))
292
332
  return false;
@@ -302,6 +342,30 @@ function replacementMatchesRecords(replacement, records, recordsBySequence, work
302
342
  return replacement.items.every((item) => {
303
343
  const original = originalItems.get(item.index);
304
344
  const evidence = appliedPatchEvidenceForPath(records, recordsBySequence, item.evidenceSequence, item.path, workspace, platform, sourceToolSequence, item.supersededByCallId);
345
+ if (original && !evidence && item.supersededByCallId) {
346
+ const later = successfulToolEvidence(records, recordsBySequence, item.evidenceSequence, 'read', item.supersededByCallId);
347
+ if (!later ||
348
+ !resultVisibleAfter(source, replacement.target.observationId, later.result) ||
349
+ !isRecord(later.result.payload) ||
350
+ typeof later.result.payload.content !== 'string')
351
+ return false;
352
+ const laterItems = parseReadContextOutput(later.result.payload.content)?.items ?? [];
353
+ const pathKey = contextGcLogicalPathKey(original.path, workspace, platform);
354
+ const observationId = typeof later.result.payload.observationId === 'string'
355
+ ? later.result.payload.observationId
356
+ : undefined;
357
+ const laterRecord = recordsBySequence.get(item.evidenceSequence);
358
+ if (laterRecord && observationsFromRecord(laterRecord) !== undefined && !observationId)
359
+ return false;
360
+ const covered = laterItems.some((next) => pathKey !== undefined &&
361
+ contextGcLogicalPathKey(next.path, workspace, platform) === pathKey &&
362
+ contextGcReadRangeCovers(original, next) &&
363
+ !allReplacements.some((prior) => prior.kind === 'READ_OUTPUT_ITEMS' &&
364
+ prior.target.sequence === later.result.sequence &&
365
+ prior.target.observationId === observationId &&
366
+ prior.items.some((existing) => existing.index === next.index)));
367
+ return (covered && item.replacement === contextGcCoveredReadReplacement(original, later.callId));
368
+ }
305
369
  return (original?.pathTruncated === false &&
306
370
  original?.path === item.path &&
307
371
  evidence !== undefined &&
@@ -321,6 +385,19 @@ function replacementMatchesRecords(replacement, records, recordsBySequence, work
321
385
  result !== undefined &&
322
386
  isDeepStrictEqual(replacement.replacementArguments, contextGcApplyPatchInputReplacement(source.payload.arguments, result)));
323
387
  }
388
+ if (replacement.kind === 'TOOL_CALL_INPUT' && replacement.toolName === 'todo_write') {
389
+ const own = records.find((item) => completedTodoWrite(item)?.callId === replacement.callId);
390
+ const original = own && completedTodoWrite(own);
391
+ const successorRecord = recordsBySequence.get(replacement.evidenceSequence);
392
+ const successor = successorRecord && completedTodoWrite(successorRecord);
393
+ const args = source.payload.arguments;
394
+ return (original !== undefined &&
395
+ successor !== undefined &&
396
+ successor.revision > original.revision &&
397
+ isRecord(args) &&
398
+ Array.isArray(args.items) &&
399
+ isDeepStrictEqual(replacement.replacementArguments, todoInputReplacement(original.revision, args.items.length, successor.revision)));
400
+ }
324
401
  if (replacement.kind === 'TOOL_CALL_INPUT' && replacement.toolName === 'code_exec') {
325
402
  const evidence = successfulToolEvidence(records, recordsBySequence, replacement.evidenceSequence, replacement.toolName);
326
403
  const input = normalizeContextGcCodeModeInput(source.payload.arguments);
@@ -329,10 +406,38 @@ function replacementMatchesRecords(replacement, records, recordsBySequence, work
329
406
  evidence.call.sequence === replacement.target.sequence &&
330
407
  input !== undefined &&
331
408
  isDeepStrictEqual(replacement.replacementArguments, contextGcCodeModeInputReplacement(input, observationsFromRecord(evidence.result)
332
- ? retainedCodeInputs(records, replacement.callId)
409
+ ? retainedCodeInputs(records, replacement.callId, (original, successor) => {
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;
412
+ }, {
413
+ activeProcessIds: new Set(),
414
+ evidenceMature: (sequence) => {
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;
417
+ },
418
+ selected: (callId, toolName, summary) => {
419
+ const argument = replacement.replacementArguments;
420
+ const content = typeof argument === 'string'
421
+ ? argument
422
+ : isRecord(argument)
423
+ ? argument.code
424
+ : undefined;
425
+ return (typeof content === 'string' &&
426
+ content.includes(contextGcRetainedInputMarkup({ callId, toolName, input: summary })));
427
+ }
428
+ })
333
429
  : [])));
334
430
  }
335
431
  if (replacement.kind === 'CODE_MODE_OUTPUT') {
432
+ if (replacement.target.observationId) {
433
+ const observation = observationsFromRecord(source)?.find((value) => value.id === replacement.target.observationId && value.type === 'value');
434
+ return (observation?.type === 'value' &&
435
+ source.payload.callId === replacement.callId &&
436
+ replacement.evidenceSequence === source.sequence &&
437
+ ['code_exec', 'code_exec_wait'].includes(toolNameForCallId(records, replacement.callId, source.sequence) ?? '') &&
438
+ replacement.replacementContent ===
439
+ contextGcCodeModeOutputReplacement(observation.value, observation.truncated === true, true));
440
+ }
336
441
  const evidence = ['code_exec', 'code_exec_wait']
337
442
  .map((name) => successfulToolEvidence(records, recordsBySequence, replacement.evidenceSequence, name))
338
443
  .find((item) => item !== undefined);
@@ -344,13 +449,112 @@ function replacementMatchesRecords(replacement, records, recordsBySequence, work
344
449
  isRecord(evidence.completed.payload) &&
345
450
  typeof evidence.completed.payload.truncated === 'boolean' &&
346
451
  replacement.replacementContent ===
347
- contextGcCodeModeOutputReplacement(source.payload.content, replacement.target.observationId
348
- ? observationsFromRecord(source)?.some((value) => value.id === replacement.target.observationId &&
349
- value.type === 'value' &&
350
- value.truncated === true) === true
351
- : evidence.completed.payload.truncated));
452
+ contextGcCodeModeOutputReplacement(source.payload.content, evidence.completed.payload.truncated, false));
352
453
  }
353
454
  if (replacement.kind === 'TOOL_OUTPUT') {
455
+ if (replacement.toolName === 'tool_search') {
456
+ const original = successfulToolEvidence(records, recordsBySequence, source.sequence, 'tool_search', replacement.callId);
457
+ if (!original ||
458
+ !isRecord(source.payload) ||
459
+ !Array.isArray(source.payload.toolSearchTools) ||
460
+ replacement.retainedToolNames === undefined ||
461
+ replacement.replacementContent !== source.payload.content)
462
+ return false;
463
+ const sourceNames = source.payload.toolSearchTools.flatMap((tool) => isRecord(tool) && typeof tool.name === 'string' ? [tool.name] : []);
464
+ const priorNames = previous?.kind === 'TOOL_OUTPUT' && previous.retainedToolNames !== undefined
465
+ ? previous.retainedToolNames
466
+ : sourceNames;
467
+ if (replacement.retainedToolNames.some((name) => !priorNames.includes(name)) ||
468
+ !isDeepStrictEqual(replacement.retainedToolNames, priorNames.filter((name) => replacement.retainedToolNames.includes(name))))
469
+ return false;
470
+ const removed = priorNames.filter((name) => !replacement.retainedToolNames.includes(name));
471
+ if (removed.length === 0)
472
+ return false;
473
+ const successors = records.filter((item) => item.sequence > source.sequence &&
474
+ item.sequence <= replacement.evidenceSequence &&
475
+ item.type === 'model.context' &&
476
+ isRecord(item.payload) &&
477
+ item.payload.role === 'tool' &&
478
+ typeof item.payload.callId === 'string' &&
479
+ Array.isArray(item.payload.toolSearchTools) &&
480
+ successfulToolEvidence(records, recordsBySequence, item.sequence, 'tool_search', item.payload.callId) !== undefined);
481
+ return (removed.every((name) => successors.some((item) => {
482
+ if (!isRecord(item.payload) || !Array.isArray(item.payload.toolSearchTools))
483
+ return false;
484
+ const target = {
485
+ sequence: item.sequence,
486
+ recordId: item.recordId,
487
+ role: 'tool',
488
+ ...(typeof item.payload.callId === 'string' ? { callId: item.payload.callId } : {})
489
+ };
490
+ const applied = allReplacements.find((candidate) => contextGcTargetKey(candidate.target) === contextGcTargetKey(target));
491
+ return (item.payload.toolSearchTools.some((tool) => isRecord(tool) && tool.name === name) &&
492
+ !(applied?.kind === 'TOOL_OUTPUT' &&
493
+ applied.retainedToolNames !== undefined &&
494
+ !applied.retainedToolNames.includes(name)));
495
+ })) && successors.some((item) => item.sequence === replacement.evidenceSequence));
496
+ }
497
+ if (replacement.toolName === 'todo_read') {
498
+ const original = successfulToolEvidence(records, recordsBySequence, source.sequence, 'todo_read', replacement.callId);
499
+ const successorRecord = recordsBySequence.get(replacement.evidenceSequence);
500
+ const successor = successorRecord && completedTodoWrite(successorRecord);
501
+ const snapshot = typeof source.payload.content === 'string'
502
+ ? visibleTodoSnapshot(source.payload.content)
503
+ : undefined;
504
+ return (original !== undefined &&
505
+ snapshot !== undefined &&
506
+ successor !== undefined &&
507
+ successor.revision > snapshot.revision &&
508
+ replacement.replacementContent ===
509
+ todoSnapshotReplacement(snapshot.revision, snapshot.count, successor.revision));
510
+ }
511
+ if (replacement.toolName === 'skill') {
512
+ const original = successfulToolEvidence(records, recordsBySequence, source.sequence, 'skill', replacement.callId);
513
+ let successorCallId;
514
+ try {
515
+ successorCallId = JSON.parse(replacement.replacementContent).supersededByCallId;
516
+ }
517
+ catch {
518
+ return false;
519
+ }
520
+ if (typeof successorCallId !== 'string')
521
+ return false;
522
+ const successor = successfulToolEvidence(records, recordsBySequence, replacement.evidenceSequence, 'skill', successorCallId);
523
+ const successorRecord = recordsBySequence.get(replacement.evidenceSequence);
524
+ const successorObservationId = isRecord(successor?.result.payload)
525
+ ? successor.result.payload.observationId
526
+ : undefined;
527
+ if (!successorRecord ||
528
+ !isRecord(successorRecord.payload) ||
529
+ (observationsFromRecord(successorRecord) !== undefined &&
530
+ typeof successorObservationId !== 'string'))
531
+ return false;
532
+ const successorKey = contextGcTargetKey({
533
+ sequence: successorRecord.sequence,
534
+ recordId: successorRecord.recordId,
535
+ role: 'tool',
536
+ ...(typeof successorRecord.payload.callId === 'string'
537
+ ? { callId: successorRecord.payload.callId }
538
+ : {}),
539
+ ...(typeof successorObservationId === 'string'
540
+ ? { observationId: successorObservationId }
541
+ : {})
542
+ });
543
+ if (!original ||
544
+ !successor ||
545
+ !resultVisibleAfter(source, replacement.target.observationId, successor.result) ||
546
+ !isRecord(original.call.payload) ||
547
+ !isRecord(successor.call.payload) ||
548
+ !isRecord(original.call.payload.arguments) ||
549
+ !isRecord(successor.call.payload.arguments) ||
550
+ typeof original.call.payload.arguments.name !== 'string' ||
551
+ original.call.payload.arguments.name !== successor.call.payload.arguments.name ||
552
+ typeof source.payload.content !== 'string' ||
553
+ allReplacements.some((item) => contextGcTargetKey(item.target) === successorKey))
554
+ return false;
555
+ return (replacement.replacementContent ===
556
+ contextGcSkillOutputReplacement(original.call.payload.arguments.name, source.payload.content, successor.callId));
557
+ }
354
558
  const evidence = successfulToolEvidence(records, recordsBySequence, replacement.evidenceSequence, replacement.toolName, replacement.callId);
355
559
  return (isGenericContextGcTool(replacement.toolName) &&
356
560
  evidence !== undefined &&
@@ -359,8 +563,6 @@ function replacementMatchesRecords(replacement, records, recordsBySequence, work
359
563
  isRecord(evidence.completed.payload) &&
360
564
  typeof evidence.completed.payload.truncated === 'boolean' &&
361
565
  typeof source.payload.content === 'string' &&
362
- isRecord(evidence.result.payload) &&
363
- evidence.result.payload.content === source.payload.content &&
364
566
  replacement.replacementContent ===
365
567
  contextGcToolOutputReplacement(replacement.toolName, source.payload.content, evidence.completed.payload.truncated));
366
568
  }
@@ -447,7 +649,9 @@ function successfulToolEvidence(records, recordsBySequence, evidenceSequence, to
447
649
  !isRecord(call.payload) ||
448
650
  call.payload.name !== toolName ||
449
651
  (expectedCallId && expectedCallId !== callId)) {
450
- for (const observation of observationsFromRecord(result) ?? []) {
652
+ const observations = observationsFromRecord(result) ?? [];
653
+ // Repeated observations of a known call use its last visible occurrence.
654
+ for (const observation of expectedCallId ? [...observations].reverse() : observations) {
451
655
  if (observation.type !== 'tool_result' ||
452
656
  observation.source.toolName !== toolName ||
453
657
  (expectedCallId && observation.source.callId !== expectedCallId))
@@ -766,7 +970,8 @@ function isContextGcReplacement(value) {
766
970
  (value.toolName === 'apply_patch' ||
767
971
  value.toolName === 'exec' ||
768
972
  value.toolName === 'exec_wait' ||
769
- value.toolName === 'code_exec') &&
973
+ value.toolName === 'code_exec' ||
974
+ value.toolName === 'todo_write') &&
770
975
  value.target.toolName === value.toolName &&
771
976
  Array.isArray(value.providerAliasTargets) &&
772
977
  value.providerAliasTargets.every((target) => isContextGcTarget(target) &&
@@ -829,13 +1034,23 @@ function isContextGcReplacement(value) {
829
1034
  'callId',
830
1035
  'target',
831
1036
  'replacementContent',
1037
+ 'retainedToolNames',
832
1038
  'evidenceSequence'
833
1039
  ]) &&
834
1040
  value.target.role === 'tool' &&
835
1041
  isNonemptyString(value.toolName) &&
836
- isGenericContextGcTool(value.toolName) &&
1042
+ (value.toolName === 'skill' ||
1043
+ value.toolName === 'todo_read' ||
1044
+ value.toolName === 'tool_search' ||
1045
+ isGenericContextGcTool(value.toolName)) &&
837
1046
  (value.target.toolName === undefined || value.target.toolName === value.toolName) &&
838
- isNonemptyString(value.replacementContent) &&
1047
+ (isNonemptyString(value.replacementContent) ||
1048
+ (value.toolName === 'tool_search' && value.replacementContent === '')) &&
1049
+ (value.retainedToolNames === undefined ||
1050
+ (value.toolName === 'tool_search' &&
1051
+ Array.isArray(value.retainedToolNames) &&
1052
+ value.retainedToolNames.every(isNonemptyString) &&
1053
+ new Set(value.retainedToolNames).size === value.retainedToolNames.length)) &&
839
1054
  isPositiveInteger(value.evidenceSequence));
840
1055
  if (value.kind === 'CODE_MODE_OUTPUT')
841
1056
  return (hasOnlyKeys(value, ['kind', 'callId', 'target', 'replacementContent', 'evidenceSequence']) &&
@@ -906,7 +1121,8 @@ function isContextGcEconomicsPolicy(value) {
906
1121
  }
907
1122
  function isContextGcMetrics(value) {
908
1123
  if (!isRecord(value) ||
909
- (value.stage !== 'STAGE_A' &&
1124
+ (value.stage !== 'UNIFIED' &&
1125
+ value.stage !== 'STAGE_A' &&
910
1126
  value.stage !== 'STAGE_A_AND_EXEC' &&
911
1127
  value.stage !== 'INLINE_MEDIA'))
912
1128
  return false;
@@ -49,7 +49,7 @@ export type ContextGcReplacement = {
49
49
  readonly evidenceSequences: readonly number[];
50
50
  } | {
51
51
  readonly kind: 'TOOL_CALL_INPUT';
52
- readonly toolName: 'apply_patch' | 'exec' | 'exec_wait' | 'code_exec';
52
+ readonly toolName: 'apply_patch' | 'exec' | 'exec_wait' | 'code_exec' | 'todo_write';
53
53
  readonly callId: string;
54
54
  readonly target: ContextGcTarget;
55
55
  readonly replacementArguments: unknown;
@@ -74,6 +74,7 @@ export type ContextGcReplacement = {
74
74
  readonly callId: string;
75
75
  readonly target: ContextGcTarget;
76
76
  readonly replacementContent: string;
77
+ readonly retainedToolNames?: readonly string[];
77
78
  readonly evidenceSequence: number;
78
79
  } | {
79
80
  readonly kind: 'SUBAGENT_OUTPUT';
@@ -98,7 +99,7 @@ export type ContextGcReplacement = {
98
99
  readonly evidenceSequence: number;
99
100
  };
100
101
  export interface ContextGcMetrics {
101
- readonly stage: 'STAGE_A' | 'STAGE_A_AND_EXEC' | 'INLINE_MEDIA';
102
+ readonly stage: 'UNIFIED' | 'STAGE_A' | 'STAGE_A_AND_EXEC' | 'INLINE_MEDIA';
102
103
  readonly tokenEstimator?: 'UTF8_BYTES_V1';
103
104
  readonly beforeTokens: number;
104
105
  readonly afterTokens: number;
@@ -0,0 +1,11 @@
1
+ import type { ContextGcExecResult } from './context-gc-replacements.js';
2
+ /** The same process-lifetime selection is used for Direct output and retained cell inputs. */
3
+ export interface ContextGcExecFact<T> {
4
+ readonly value: T;
5
+ readonly callId: string;
6
+ readonly toolName: string;
7
+ readonly sequence: number;
8
+ readonly input?: Record<string, unknown>;
9
+ readonly result?: ContextGcExecResult;
10
+ }
11
+ export declare function terminalExecLifecycles<T>(facts: readonly ContextGcExecFact<T>[], active: ReadonlySet<string>): ContextGcExecFact<T>[][];
@@ -0,0 +1,41 @@
1
+ export function terminalExecLifecycles(facts, active) {
2
+ const ordered = [...facts].sort((a, b) => a.sequence - b.sequence);
3
+ const lifecycles = [];
4
+ for (const start of ordered) {
5
+ if (!start.input || !start.result)
6
+ continue;
7
+ const isStart = start.toolName === 'exec' &&
8
+ (start.input.action === 'start' ||
9
+ (start.input.action == null && typeof start.input.command === 'string'));
10
+ const processId = typeof start.input.processId === 'string' ? start.input.processId : start.result.processId;
11
+ if (!isStart) {
12
+ if (start.toolName !== 'exec_wait' || !processId)
13
+ continue;
14
+ const previous = ordered.findLast((item) => item.sequence < start.sequence &&
15
+ (item.input?.processId === processId || item.result?.processId === processId));
16
+ if (previous &&
17
+ previous.result?.status !== 'completed' &&
18
+ previous.result?.status !== 'cancelled')
19
+ continue;
20
+ }
21
+ const lifecycle = [start];
22
+ let terminal = start.result.status !== 'running';
23
+ if (!terminal) {
24
+ if (!processId)
25
+ continue;
26
+ const related = ordered.filter((item) => item.sequence > start.sequence && item.input?.processId === processId);
27
+ for (const item of related) {
28
+ if (!item.result)
29
+ break;
30
+ lifecycle.push(item);
31
+ if (item.result.status !== 'running') {
32
+ terminal = true;
33
+ break;
34
+ }
35
+ }
36
+ }
37
+ if (terminal && (!processId || !active.has(processId)))
38
+ lifecycles.push(lifecycle);
39
+ }
40
+ return lifecycles;
41
+ }
@@ -1 +1,2 @@
1
+ export declare function contextGcRetainedInputMarkup(input: unknown): string;
1
2
  export declare function contextGcMarkup(kind: string, fields: Record<string, unknown>): string;
@@ -1,4 +1,7 @@
1
1
  import { markupAttributes, markupText } from '../prompts/markup.js';
2
+ export function contextGcRetainedInputMarkup(input) {
3
+ return markupText('retained_input', typeof input === 'string' ? input : JSON.stringify(input));
4
+ }
2
5
  // Render message placeholders only; structured GC records keep their JSON shape.
3
6
  export function contextGcMarkup(kind, fields) {
4
7
  const attributes = { kind };
@@ -8,7 +11,7 @@ export function contextGcMarkup(kind, fields) {
8
11
  continue;
9
12
  if (key === 'retainedInputs' && Array.isArray(value)) {
10
13
  for (const input of value)
11
- children.push(markupText('retained_input', typeof input === 'string' ? input : JSON.stringify(input)));
14
+ children.push(contextGcRetainedInputMarkup(input));
12
15
  }
13
16
  else if (key === 'contextGc')
14
17
  children.push(markupText('instruction', String(value)));
@@ -51,7 +51,11 @@ export declare function codePartialPatchCallEvidence(records: readonly SessionRe
51
51
  result: SessionRecord;
52
52
  } | undefined;
53
53
  export declare function callsFromRecord(record: SessionRecord): CodeToolCallRecord[] | undefined;
54
- export declare function retainedCodeInputs(records: readonly SessionRecord[], parentCallId: string): {
54
+ export declare function retainedCodeInputs(records: readonly SessionRecord[], parentCallId: string, todoEvidenceMature?: (original: SessionRecord, successor: SessionRecord) => boolean, execOptions?: {
55
+ readonly activeProcessIds: ReadonlySet<string>;
56
+ readonly evidenceMature: (sequence: number) => boolean;
57
+ readonly selected?: (callId: string, toolName: string, summary: unknown) => boolean;
58
+ }): {
55
59
  callId: unknown;
56
60
  toolName: unknown;
57
61
  input: unknown;