@myagentroam/agent 0.10.28 → 0.10.30

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 (28) hide show
  1. package/dist/agent-core/extensions/session-input.d.ts +2 -0
  2. package/dist/agent-core/extensions/session-input.js +21 -3
  3. package/dist/agent-core/model/openai-responses.js +1 -1
  4. package/dist/agent-core/runtime/compaction/coordinator.d.ts +15 -0
  5. package/dist/agent-core/runtime/compaction/coordinator.js +88 -0
  6. package/dist/agent-core/runtime/compaction/requests.d.ts +16 -0
  7. package/dist/agent-core/runtime/compaction/requests.js +114 -0
  8. package/dist/agent-core/runtime/compaction/retained-context.d.ts +6 -0
  9. package/dist/agent-core/runtime/compaction/retained-context.js +52 -0
  10. package/dist/agent-core/runtime/context/restore.d.ts +1 -0
  11. package/dist/agent-core/runtime/context/restore.js +14 -4
  12. package/dist/agent-core/runtime/context/usage.js +3 -1
  13. package/dist/agent-core/runtime/event-projection.js +1 -1
  14. package/dist/agent-core/runtime/events.d.ts +21 -0
  15. package/dist/agent-core/runtime/execution-policy.d.ts +1 -0
  16. package/dist/agent-core/runtime/execution-policy.js +1 -0
  17. package/dist/agent-core/runtime/execution.d.ts +1 -0
  18. package/dist/agent-core/runtime/execution.js +1 -0
  19. package/dist/agent-core/sdk/extension-session.js +4 -2
  20. package/dist/agent-core/sdk/session.d.ts +2 -0
  21. package/dist/agent-core/sdk/session.js +307 -191
  22. package/dist/agent-core/sdk/types.d.ts +4 -1
  23. package/dist/agent-core/session/catalog.js +2 -2
  24. package/dist/agent-core/session/jsonl-store.d.ts +2 -0
  25. package/dist/agent-core/session/jsonl-store.js +6 -0
  26. package/dist/agent-core/tools/local/exec.js +9 -1
  27. package/dist/index.d.ts +1 -1
  28. package/package.json +1 -1
@@ -2,7 +2,7 @@ import { isProviderContextOverflow, recoverModelStream } from '../model/recovery
2
2
  import { estimatedRequestTokens } from '../runtime/context/request-budget.js';
3
3
  import { restoreMessages } from '../runtime/context/restore.js';
4
4
  import { lastConfigurationEffort, latestModelUsage, reasoningEffortBaseline, shouldUseNativeCompaction } from '../runtime/context/usage.js';
5
- import { splitContextualPrompt, projectCompactPromptMessages } from '../runtime/context/contextual-blocks.js';
5
+ import { splitContextualPrompt } from '../runtime/context/contextual-blocks.js';
6
6
  import { contextualInstructionInsertIndex, insertContextualWorldState, latestContextualWorldStateGeneration, parsePersistedContextualWorldState, persistContextualWorldState, resetContextualWorldState, updateContextualWorldState, updatePromptWorldState } from '../runtime/context/world-state.js';
7
7
  import { eventForPersistence, publicModelUsage } from '../runtime/event-projection.js';
8
8
  import { ExecutionLoop } from '../runtime/execution.js';
@@ -32,6 +32,9 @@ import { retainedSessionResourcesSnapshot } from '../prompts/resources.js';
32
32
  import { childSubagentInstruction } from '../prompts/subagent.js';
33
33
  import { AsyncQueue } from '../runtime/async-queue.js';
34
34
  import { CompactionOperation } from '../runtime/compaction/operation.js';
35
+ import { CompactCoordinator } from '../runtime/compaction/coordinator.js';
36
+ import { CompactRequests } from '../runtime/compaction/requests.js';
37
+ import { compactSnapshotAnchor } from '../runtime/compaction/retained-context.js';
35
38
  import { DEFAULT_COMPACTION_FOCUS, buildCompactedHistory, buildCompactionInput, buildCompactionMessages, buildNativeCompactionMessages, compactedHistoryMessages } from '../runtime/compaction/history.js';
36
39
  import { currentLocalTimeContext, environmentContextUpdate, restoreEnvironmentState } from '../runtime/context/environment.js';
37
40
  import { ContextGcRuntime } from '../runtime/context/gc-runtime.js';
@@ -197,6 +200,11 @@ export function createSessionOpener(services) {
197
200
  executionModels.get(input.modelId ?? executionDefaultModelId)?.conversationModel === false)
198
201
  throw new MarAgentError('MAR_AGENT_MODEL_DISABLED', 'The selected model is not a conversation model.');
199
202
  const executionId = randomUUID(), turnId = internal.turnId ?? randomUUID(), queue = new AsyncQueue(), controller = new AbortController();
203
+ const compactCoordinator = new CompactCoordinator();
204
+ const messageSources = new WeakMap();
205
+ let liveModelMessages = [];
206
+ const compactRequests = new CompactRequests(executionId, () => compactCoordinator.assertNotReentrant());
207
+ sessionRuntime.compactRequests = compactRequests;
200
208
  const permissionPolicy = new ExecutionToolPermissions(mode === 'compact' ? 'full-access' : resolvePermissionMode(input.permissionMode), { sessionId, executionId }, host.requestToolApproval
201
209
  ? {
202
210
  request: (request, signal) => host.requestToolApproval(request, signal),
@@ -265,7 +273,6 @@ export function createSessionOpener(services) {
265
273
  acceptParent(message);
266
274
  let acceptsMailbox = sessionSource.type === 'subagent' && mode !== 'compact';
267
275
  let sequence = 0;
268
- let manualCompactionStarted = false;
269
276
  let emitTail = Promise.resolve();
270
277
  let releaseSession;
271
278
  let exhaustedModelAttempt;
@@ -284,14 +291,21 @@ export function createSessionOpener(services) {
284
291
  const operation = emitTail.then(async () => {
285
292
  assertLive?.();
286
293
  const persistent = eventForPersistence(full);
287
- if (persistent !== undefined && releaseSession !== undefined)
288
- await store.append(sessionId, {
294
+ if (persistent !== undefined && releaseSession !== undefined) {
295
+ const stored = await store.append(sessionId, {
289
296
  type: full.type,
290
297
  payload: persistent,
291
298
  executionId,
292
299
  turnId
293
300
  });
301
+ if (full.type === 'message.completed') {
302
+ const message = liveModelMessages.findLast((item) => item.role === 'assistant' && item.content === full.text);
303
+ if (message)
304
+ messageSources.set(message, stored);
305
+ }
306
+ }
294
307
  queue.push(full);
308
+ compactRequests.publish(full);
295
309
  if (internal.publishHostEvents !== false)
296
310
  await host.emit(full);
297
311
  });
@@ -337,7 +351,6 @@ export function createSessionOpener(services) {
337
351
  const result = withToolPermissions(permissionPolicy, async () => {
338
352
  let finalAnswer = '';
339
353
  let executionLoop;
340
- let manualCompaction;
341
354
  let executionToolSources;
342
355
  let selectedTurn;
343
356
  const contextGc = new ContextGcRuntime();
@@ -392,9 +405,11 @@ export function createSessionOpener(services) {
392
405
  }
393
406
  const compactMayContainAgentInstructions = contextRecords.some((record) => compactSummaryFromCheckpoint(record) !== undefined);
394
407
  const restored = await restoreMessages(store, sessionId, contextRecords, {
408
+ messageSources,
395
409
  includeImages: mode !== 'compact'
396
410
  });
397
411
  const messages = restored.messages;
412
+ liveModelMessages = messages;
398
413
  const restoredRecordIds = new Set(contextRecords.map((record) => record.recordId));
399
414
  for (const pending of pendingAppends)
400
415
  if (!restoredRecordIds.has(pending.recordId))
@@ -403,6 +418,7 @@ export function createSessionOpener(services) {
403
418
  immediateTarget = messages;
404
419
  let recordedEnvironment = restored.environmentState;
405
420
  let compactContext;
421
+ let startCompact;
406
422
  const writeInputRecord = async (content, type, metadata, call, committed) => {
407
423
  call.signal.throwIfAborted();
408
424
  if (content.images?.length && !selectedModel.inputCapabilities.includes('IMAGE'))
@@ -439,8 +455,14 @@ export function createSessionOpener(services) {
439
455
  };
440
456
  const appendMailboxMessages = async () => inputs.drain(async (entry, request, call) => {
441
457
  call.signal.throwIfAborted();
442
- if (request.kind === 'compact')
458
+ if (request.kind === 'compact') {
459
+ if (request.input.async) {
460
+ const flight = startCompact(request.input);
461
+ entry.start();
462
+ return { changed: await flight.result, executionId };
463
+ }
443
464
  return { changed: await compactContext(request.input), executionId };
465
+ }
444
466
  if (request.kind === 'append') {
445
467
  const result = await appendExtensionMessage(entry, request, call);
446
468
  return { ...result, executionId };
@@ -765,14 +787,31 @@ export function createSessionOpener(services) {
765
787
  contextualWorldState = next;
766
788
  sessionRuntime.contextualWorldState = next;
767
789
  };
768
- const resetCompactWorldState = async (generation) => {
790
+ const resetCompactWorldState = async (generation, committed) => {
769
791
  agentInstructionInsertIndex = contextualInstructionInsertIndex(messages);
770
792
  const next = resetContextualWorldState(generation, agentInstructionInsertIndex, {
771
793
  instructionSnapshot: currentInstructionSnapshot,
772
794
  instructionContent: renderAgentInstructions(instructionEntries, true),
773
- resourceSnapshot: retainedSessionResourcesSnapshot(await loadRetainedSessionResources())
795
+ resourceSnapshot: committed
796
+ ? committed.resources
797
+ : retainedSessionResourcesSnapshot(await loadRetainedSessionResources())
774
798
  });
775
- await commitWorldState(next);
799
+ if (!committed)
800
+ await commitWorldState(next);
801
+ else {
802
+ // The checkpoint is authoritative; its sidecar is recoverable on restart.
803
+ contextualWorldState = next;
804
+ sessionRuntime.contextualWorldState = next;
805
+ try {
806
+ await persistContextualWorldState(store, sessionId, next);
807
+ }
808
+ catch {
809
+ coreExtensions.diagnose({
810
+ code: 'MAR_AGENT_COMPACT_SIDECAR_PERSIST_FAILED',
811
+ sessionId
812
+ });
813
+ }
814
+ }
776
815
  };
777
816
  // A missing sidecar is rebuilt only for the latest Compact, never for a GC boundary.
778
817
  if (!contextualWorldState.initialized &&
@@ -1062,8 +1101,6 @@ export function createSessionOpener(services) {
1062
1101
  const initialSystemPrompt = mode === 'compact'
1063
1102
  ? buildExecutionSystemPrompt(executionTools, 'run')
1064
1103
  : buildExecutionSystemPrompt();
1065
- let useManualCompactionFallback;
1066
- const manualAppendBoundary = pendingAppends.length;
1067
1104
  const generateCompactExtension = async (history, reason, focus, compaction) => {
1068
1105
  if (!coreExtensions.hooks.hasInterceptors('compact.generate'))
1069
1106
  return undefined;
@@ -1114,50 +1151,6 @@ export function createSessionOpener(services) {
1114
1151
  acceptsDiagnostics = false;
1115
1152
  }
1116
1153
  };
1117
- let manualExtension;
1118
- if (mode === 'compact' && coreExtensions.hooks.hasInterceptors('compact.generate')) {
1119
- manualCompactionStarted = true;
1120
- manualCompaction = createCompactionOperation(manualCompactionId, compactSignal);
1121
- await emit({ type: 'context.compacting', compactionId: manualCompactionId });
1122
- manualExtension = await generateCompactExtension(messages, 'manual', compactFocus, manualCompaction);
1123
- }
1124
- if (mode === 'compact' && manualExtension === undefined) {
1125
- preparedCompactPrompt = await operation((call) => coreExtensions.composeModelSystem(promptComposition(executionTools, 'run'), call));
1126
- const compactComposition = splitContextualPrompt(preparedCompactPrompt);
1127
- const compactSystemPrompt = compactComposition.system;
1128
- const projectCompact = (input) => projectCompactPromptMessages(input, compactComposition.parts, contextualWorldState.promptSnapshot ?? []);
1129
- const compactHistory = [...messages];
1130
- const projectedHistory = normalizeMessagesForModel(insertContextualWorldState(compactHistory, contextualWorldState.entries), { supportsImages: false });
1131
- const nativeMessages = buildNativeCompactionMessages(projectedHistory, {
1132
- focus: compactFocus
1133
- });
1134
- const compactOverheadTokens = estimatedRequestTokens(compactSystemPrompt, projectCompact(buildCompactionMessages('')), executionTools);
1135
- const compactInputTokens = selectedModel.contextWindowTokens - compactOverheadTokens;
1136
- if (compactInputTokens <= 0)
1137
- throw new MarAgentError('MAR_AGENT_CONTEXT_LIMIT', 'Compaction prompt exceeds the selected model context window.');
1138
- const fallbackMessages = () => buildCompactionMessages(buildCompactionInput(compactHistory, {
1139
- focus: compactFocus,
1140
- maxTokens: compactInputTokens
1141
- }));
1142
- const useNative = shouldUseNativeCompaction({
1143
- estimatedRequestTokens: estimatedRequestTokens(compactSystemPrompt, projectCompact(nativeMessages), executionTools),
1144
- contextWindowTokens: selectedModel.contextWindowTokens,
1145
- latestUsage: lastServerUsage,
1146
- historyMessages: compactHistory,
1147
- appendedMessages: projectCompact(nativeMessages).slice(projectedHistory.length)
1148
- });
1149
- messages.splice(0, messages.length, ...(useNative ? nativeMessages : fallbackMessages()));
1150
- if (useNative) {
1151
- let available = true;
1152
- useManualCompactionFallback = () => {
1153
- if (!available)
1154
- return false;
1155
- available = false;
1156
- messages.splice(0, messages.length, ...fallbackMessages(), ...pendingAppends.slice(manualAppendBoundary).map((entry) => entry.message));
1157
- return true;
1158
- };
1159
- }
1160
- }
1161
1154
  await store.append(sessionId, {
1162
1155
  type: 'execution.header',
1163
1156
  payload: {
@@ -1173,29 +1166,53 @@ export function createSessionOpener(services) {
1173
1166
  turnId
1174
1167
  });
1175
1168
  let lastServerContextTokens = lastServerUsage?.contextInputTokens;
1176
- const compactLeaf = async (input = {}, signal = controller.signal) => {
1169
+ let compactBoundary = messages.length;
1170
+ const compactLeaf = async (input, signal, prepare, compactionId) => {
1177
1171
  if (messages.length < 2)
1178
1172
  return false;
1179
1173
  let compactSummary = '';
1180
- const current = messages.at(-1);
1181
- const retainCurrent = current.role === 'user' && current.contextKind === undefined;
1182
- const compactHistory = retainCurrent ? messages.slice(0, -1) : [...messages];
1183
- const appendBoundary = pendingAppends.length;
1184
- const compactComposition = splitContextualPrompt(await operation((call) => coreExtensions.composeModelSystem(promptComposition(executionTools, 'run'), call)));
1174
+ const cutoff = Math.min(compactBoundary, messages.length);
1175
+ const current = messages[cutoff - 1];
1176
+ const retainCurrent = mode !== 'compact' && current.role === 'user' && current.contextKind === undefined;
1177
+ const prefix = messages.slice(0, cutoff - (retainCurrent ? 1 : 0));
1178
+ if (!prefix.some((message) => message.role !== 'user' ||
1179
+ message.contextKind === undefined ||
1180
+ message.contextKind === 'summary'))
1181
+ return false;
1182
+ const compactHistory = structuredClone(prefix);
1183
+ const prefixInputs = compactionUserMessages.filter((entry) => prefix.some((message) => message.role === 'user' &&
1184
+ message.contextKind === undefined &&
1185
+ (entry.metadata?.recordId
1186
+ ? message.metadata?.recordId === entry.metadata.recordId
1187
+ : message.content === entry.content)));
1188
+ const snapshotTools = structuredClone(executionTools);
1189
+ const generation = contextualWorldState.generation;
1190
+ const snapshotUsage = lastServerUsage;
1191
+ const snapshotEffort = requestReasoningEffort ?? reasoningEffort;
1192
+ const anchorRecordId = prefix
1193
+ .map((message) => messageSources.get(message)?.recordId ?? message.metadata?.recordId)
1194
+ .findLast((id) => id !== undefined);
1195
+ const visibleAnchor = compactSnapshotAnchor(prefix, messageSources);
1196
+ const anchor = {
1197
+ ...(anchorRecordId ? { anchorRecordId } : {}),
1198
+ ...(visibleAnchor ? { anchor: visibleAnchor } : {})
1199
+ };
1200
+ const compactComposition = splitContextualPrompt(await operation((call) => coreExtensions.composeModelSystem(promptComposition(snapshotTools, 'run'), call)));
1185
1201
  const compactSystemPrompt = compactComposition.system;
1186
- const projectCompact = (input) => projectCompactPromptMessages(input, compactComposition.parts, contextualWorldState.promptSnapshot ?? []);
1187
- const projectedHistory = normalizeMessagesForModel(insertContextualWorldState(compactHistory, contextualWorldState.entries), { supportsImages: false });
1202
+ const projectedHistory = normalizeMessagesForModel(compactHistory, {
1203
+ supportsImages: false
1204
+ });
1188
1205
  const nativeCompactMessages = buildNativeCompactionMessages(projectedHistory, {
1189
1206
  ...(input.focus === undefined ? {} : { focus: input.focus })
1190
1207
  });
1191
- const compactOverheadTokens = estimatedRequestTokens(compactSystemPrompt, projectCompact(buildCompactionMessages('')), executionTools);
1208
+ const compactOverheadTokens = estimatedRequestTokens(compactSystemPrompt, buildCompactionMessages(''), snapshotTools);
1192
1209
  const compactInputTokens = Math.max(0, selectedModel.contextWindowTokens - compactOverheadTokens);
1193
1210
  let usingNativeMessages = shouldUseNativeCompaction({
1194
- estimatedRequestTokens: estimatedRequestTokens(compactSystemPrompt, projectCompact(nativeCompactMessages), executionTools),
1211
+ estimatedRequestTokens: estimatedRequestTokens(compactSystemPrompt, nativeCompactMessages, snapshotTools),
1195
1212
  contextWindowTokens: selectedModel.contextWindowTokens,
1196
- latestUsage: lastServerUsage,
1213
+ latestUsage: snapshotUsage,
1197
1214
  historyMessages: compactHistory,
1198
- appendedMessages: projectCompact(nativeCompactMessages).slice(projectedHistory.length)
1215
+ appendedMessages: nativeCompactMessages.slice(projectedHistory.length)
1199
1216
  });
1200
1217
  const fallbackMessages = () => buildCompactionMessages(buildCompactionInput(compactHistory, {
1201
1218
  ...(input.focus === undefined ? {} : { focus: input.focus }),
@@ -1204,18 +1221,28 @@ export function createSessionOpener(services) {
1204
1221
  let compactMessages = usingNativeMessages
1205
1222
  ? nativeCompactMessages
1206
1223
  : fallbackMessages();
1207
- const compactionId = randomUUID();
1208
- await emit({ type: 'context.compacting', compactionId });
1224
+ await emit({
1225
+ type: 'context.compacting',
1226
+ compactionId,
1227
+ async: input.async === true,
1228
+ stage: 'GENERATING',
1229
+ ...anchor
1230
+ });
1209
1231
  let latestUsage;
1210
1232
  const compactionRetryState = { attempts: 0 };
1211
1233
  const compaction = createCompactionOperation(compactionId, signal);
1234
+ const compactTurn = createModelTurnSession(selectedAdapter);
1212
1235
  try {
1213
- const extensionResult = await generateCompactExtension(compactHistory, 'automatic', input.focus, compaction);
1236
+ const extensionResult = await generateCompactExtension(compactHistory, mode === 'compact' ? 'manual' : 'automatic', input.focus, compaction);
1214
1237
  if (extensionResult) {
1215
1238
  compactSummary = extensionResult.summary;
1216
1239
  latestUsage = extensionResult.usage;
1217
1240
  if (latestUsage)
1218
- await emit({ type: 'usage.updated', ...publicModelUsage(latestUsage) });
1241
+ await emit({
1242
+ type: 'usage.updated',
1243
+ purpose: 'compact',
1244
+ ...publicModelUsage(latestUsage)
1245
+ });
1219
1246
  }
1220
1247
  while (extensionResult === undefined) {
1221
1248
  if (compactInputTokens === 0)
@@ -1229,20 +1256,24 @@ export function createSessionOpener(services) {
1229
1256
  ...(compactComposition.systemBlocks === undefined
1230
1257
  ? {}
1231
1258
  : { systemBlocks: compactComposition.systemBlocks }),
1232
- messages: projectCompact(compactMessages),
1233
- tools: executionTools,
1259
+ messages: compactMessages,
1260
+ tools: snapshotTools,
1234
1261
  toolChoice: 'none',
1235
1262
  promptCacheKey: sessionId,
1236
- reasoningEffort: requestReasoningEffort ?? reasoningEffort,
1263
+ reasoningEffort: snapshotEffort,
1237
1264
  onAttemptDiagnostic: recordModelAttempt('COMPACTION')
1238
1265
  }, undefined, selectedModel.id, 'compact');
1239
- for await (const event of streamModel((signal) => compaction.start({ start: (request) => selected.start(request, signal) }, request), compaction.signal)) {
1266
+ for await (const event of streamModel((signal) => compaction.start({ start: (request) => compactTurn.start(request, signal) }, request), compaction.signal)) {
1240
1267
  if (event.type === 'text.completed' &&
1241
1268
  (event.phase ?? 'final_answer') === 'final_answer')
1242
1269
  compactSummary = event.text.trim();
1243
1270
  else if (event.type === 'usage') {
1244
1271
  latestUsage = event.usage;
1245
- await emit({ type: 'usage.updated', ...publicModelUsage(event.usage) });
1272
+ await emit({
1273
+ type: 'usage.updated',
1274
+ purpose: 'compact',
1275
+ ...publicModelUsage(event.usage)
1276
+ });
1246
1277
  }
1247
1278
  else if (event.type === 'client_tool.call')
1248
1279
  throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Compaction returned a tool call while tools were disabled.');
@@ -1267,32 +1298,36 @@ export function createSessionOpener(services) {
1267
1298
  }
1268
1299
  finally {
1269
1300
  await compaction.dispose();
1301
+ await compactTurn.close();
1270
1302
  }
1271
1303
  if (!compactSummary)
1272
1304
  throw new MarAgentError('MAR_AGENT_COMPACT_FAILED', 'Automatic compact failed.');
1273
- let replacementHistory = buildCompactedHistory(compactSummary, retainCurrent ? compactionUserMessages.slice(0, -1) : compactionUserMessages, compactUserTokenLimit, retainCurrent
1274
- ? {
1275
- content: current.content,
1276
- ...(current.metadata ? { metadata: current.metadata } : {})
1277
- }
1278
- : undefined);
1305
+ await emit({
1306
+ type: 'context.compacting',
1307
+ compactionId,
1308
+ async: !compactCoordinator.waiting && input.async === true,
1309
+ stage: compactCoordinator.waiting ? 'WAITING' : 'READY',
1310
+ ...anchor
1311
+ });
1312
+ await prepare();
1279
1313
  signal.throwIfAborted();
1314
+ const compactResources = retainedSessionResourcesSnapshot(await loadRetainedSessionResources());
1315
+ const replacementHistory = buildCompactedHistory(compactSummary, prefixInputs, compactUserTokenLimit);
1316
+ let retainedModelContext = [];
1280
1317
  const compactRecord = await writeCompactionRecord(compactSummary, true, signal, async (actual, options) => {
1281
- compactSummary = actual;
1282
- replacementHistory = buildCompactedHistory(actual, retainCurrent ? compactionUserMessages.slice(0, -1) : compactionUserMessages, compactUserTokenLimit, retainCurrent
1283
- ? {
1284
- content: current.content,
1285
- ...(current.metadata ? { metadata: current.metadata } : {})
1286
- }
1287
- : undefined);
1288
- const retainedModelContext = pendingAppends
1289
- .slice(appendBoundary)
1290
- .map((entry) => entry.message);
1318
+ if (contextualWorldState.generation !== generation ||
1319
+ prefix.some((message, index) => messages[index] !== message))
1320
+ throw new MarAgentError('MAR_AGENT_COMPACT_STALE', 'The compaction prefix has changed.');
1321
+ retainedModelContext = messages
1322
+ .slice(prefix.length)
1323
+ .filter((message) => message.contextKind !== 'private' || message.role === 'provider');
1291
1324
  const record = await store.compact(sessionId, {
1292
- summary: compactSummary,
1325
+ summary: actual,
1293
1326
  replacementHistory,
1294
1327
  retainedModelContext,
1295
- automatic: true,
1328
+ snapshotMessageCount: prefix.length,
1329
+ ...(anchorRecordId ? { snapshotRecordId: anchorRecordId } : {}),
1330
+ automatic: mode !== 'compact',
1296
1331
  environment: executionEnvironment,
1297
1332
  modelId: selectedModel.id,
1298
1333
  contextWindowTokens: selectedModel.contextWindowTokens,
@@ -1314,30 +1349,115 @@ export function createSessionOpener(services) {
1314
1349
  content: message.content,
1315
1350
  ...(message.metadata ? { metadata: { ...message.metadata } } : {})
1316
1351
  }));
1317
- await resetCompactWorldState(compactRecord.recordId);
1352
+ compactionUserMessages.push(...retainedModelContext
1353
+ .filter((message) => message.role === 'user' && message.contextKind === undefined)
1354
+ .map((message) => ({
1355
+ content: message.content,
1356
+ ...(message.metadata ? { metadata: message.metadata } : {})
1357
+ })));
1358
+ await resetCompactWorldState(compactRecord.recordId, { resources: compactResources });
1318
1359
  lastServerContextTokens = undefined;
1319
1360
  lastServerUsage = undefined;
1320
1361
  requestReasoningEffort = reasoningEffort;
1321
1362
  effectiveReasoningEffort = reasoningEffort;
1322
- await emit({ type: 'context.compacted', compactionId });
1363
+ await emit({
1364
+ type: 'context.compacted',
1365
+ compactionId,
1366
+ async: input.async === true,
1367
+ retainedContext: retainedModelContext.length > 0,
1368
+ ...anchor
1369
+ });
1323
1370
  return true;
1324
1371
  };
1372
+ startCompact = (input = {}) => {
1373
+ const id = manualCompactionId ?? randomUUID();
1374
+ return compactCoordinator.start(id, input.focus, async (prepare, signal) => {
1375
+ const linked = AbortSignal.any([signal, controller.signal, compactSignal]);
1376
+ try {
1377
+ if (mode === 'compact' && inputs.currentCompact())
1378
+ return await compactLeaf(input, linked, prepare, id);
1379
+ return (await performCompact(input, async (effective, call) => ({
1380
+ changed: await compactLeaf({ ...effective, async: input.async === true }, AbortSignal.any([linked, call.signal]), prepare, id),
1381
+ executionId
1382
+ }))).changed;
1383
+ }
1384
+ catch (error) {
1385
+ const diagnostic = describeMarAgentError(error, 'MAR_AGENT_COMPACT_FAILED');
1386
+ await emit({
1387
+ type: 'context.compact_failed',
1388
+ compactionId: id,
1389
+ code: diagnostic.code,
1390
+ async: input.async === true,
1391
+ cancelled: linked.aborted
1392
+ });
1393
+ throw error;
1394
+ }
1395
+ });
1396
+ };
1325
1397
  compactContext = async (input = {}) => {
1326
- sources.bind(captureRoundToolScope().toolScope);
1327
- return (await performCompact(input, async (effective, call) => ({
1328
- changed: await compactLeaf(effective, call.signal),
1329
- executionId
1330
- }))).changed;
1398
+ const flight = startCompact(input);
1399
+ if (input.async)
1400
+ return false;
1401
+ await emit({
1402
+ type: 'context.compacting',
1403
+ compactionId: flight.id,
1404
+ stage: 'WAITING',
1405
+ async: false
1406
+ });
1407
+ return (await compactCoordinator.checkpoint(true)) ?? false;
1331
1408
  };
1409
+ compactRequests.bind((input) => compactCoordinator.pending
1410
+ ? startCompact({
1411
+ ...(input.prompt === undefined ? {} : { focus: input.prompt }),
1412
+ async: input.async === true
1413
+ })
1414
+ : undefined);
1415
+ if (mode === 'compact') {
1416
+ startCompact({ focus: compactFocus });
1417
+ compactRequests.drain((input) => startCompact({
1418
+ ...(input.prompt === undefined ? {} : { focus: input.prompt }),
1419
+ async: input.async === true
1420
+ }));
1421
+ await compactCoordinator.checkpoint(true);
1422
+ compactRequests.seal();
1423
+ compactRequests.drain((input) => startCompact({
1424
+ ...(input.prompt === undefined ? {} : { focus: input.prompt }),
1425
+ async: input.async === true
1426
+ }));
1427
+ await compactCoordinator.checkpoint(true);
1428
+ return;
1429
+ }
1332
1430
  const ensureContextBudget = async () => {
1333
- if (mode === 'compact' || lastServerContextTokens === undefined)
1334
- return;
1335
- const hardLimit = selectedModel.contextWindowTokens;
1336
- const compactLimit = Math.floor((hardLimit * AGENT_EXECUTION_POLICY.automaticCompactionPercent) / 100);
1337
- if (lastServerContextTokens < compactLimit)
1338
- return;
1339
- if (!(await compactContext()))
1340
- throw new MarAgentError('MAR_AGENT_CONTEXT_LIMIT', 'Provider usage reached the compaction threshold but no history can be compacted.');
1431
+ const percent = lastServerContextTokens === undefined
1432
+ ? undefined
1433
+ : (lastServerContextTokens * 100) / selectedModel.contextWindowTokens;
1434
+ if (percent !== undefined &&
1435
+ percent >= AGENT_EXECUTION_POLICY.automaticCompactionPercent) {
1436
+ const hadPending = compactCoordinator.pending;
1437
+ let compacted;
1438
+ try {
1439
+ compacted = await compactContext();
1440
+ }
1441
+ catch (error) {
1442
+ controller.signal.throwIfAborted();
1443
+ if (!hadPending)
1444
+ throw error;
1445
+ compactBoundary = messages.length;
1446
+ compacted = await compactContext();
1447
+ }
1448
+ if (!compacted)
1449
+ throw new MarAgentError('MAR_AGENT_CONTEXT_LIMIT', 'Provider usage reached the compaction threshold but no history can be compacted.');
1450
+ }
1451
+ else {
1452
+ const adopted = await compactCoordinator.checkpoint(false).catch(() => {
1453
+ controller.signal.throwIfAborted();
1454
+ });
1455
+ if (!adopted &&
1456
+ percent !== undefined &&
1457
+ percent >= AGENT_EXECUTION_POLICY.backgroundCompactionPercent &&
1458
+ !compactCoordinator.pending)
1459
+ startCompact({ async: true });
1460
+ }
1341
1461
  };
1342
1462
  executionLoop = new ExecutionLoop({
1343
1463
  mode,
@@ -1349,27 +1469,30 @@ export function createSessionOpener(services) {
1349
1469
  toolImages: currentExecutionToolImages,
1350
1470
  publish: emit,
1351
1471
  appendContext: (message) => withRecordProjection(async () => {
1352
- await store.append(sessionId, {
1472
+ const stored = await store.append(sessionId, {
1353
1473
  type: 'model.context',
1354
1474
  payload: message,
1355
1475
  executionId,
1356
1476
  turnId
1357
1477
  });
1478
+ messageSources.set(message, stored);
1358
1479
  }),
1359
- prepareRound: async (pendingOutputContinuation) => {
1360
- if (mode !== 'compact')
1361
- await appendMailboxMessages();
1480
+ prepareRound: async function prepareRound(pendingOutputContinuation) {
1481
+ await appendMailboxMessages();
1362
1482
  updateMcpRegistry();
1363
1483
  executionTools = mcpRegistry.project(baseTools());
1364
- if (mode !== 'compact')
1365
- await syncMcpWorldState(messages.length);
1484
+ await syncMcpWorldState(messages.length);
1485
+ compactBoundary = messages.length;
1486
+ const blocking = compactRequests.drain((input) => startCompact({
1487
+ ...(input.prompt === undefined ? {} : { focus: input.prompt }),
1488
+ async: input.async === true
1489
+ }));
1490
+ if (blocking)
1491
+ await compactCoordinator.checkpoint(true);
1366
1492
  await ensureContextBudget();
1367
- if (mode !== 'compact')
1368
- await syncMcpWorldState(messages.length);
1369
- if (mode !== 'compact') {
1370
- executionEnvironment = { ...currentEnvironment, ...currentLocalTimeContext() };
1371
- await recordEnvironment();
1372
- }
1493
+ await syncMcpWorldState(messages.length);
1494
+ executionEnvironment = { ...currentEnvironment, ...currentLocalTimeContext() };
1495
+ await recordEnvironment();
1373
1496
  const { roundTools, roundBindings, toolScope, composeSystemPrompt } = captureRoundToolScope();
1374
1497
  const codeModeEnabled = roundTools.some((tool) => tool.name === 'code_exec');
1375
1498
  const residentTools = projectTools(mcpRegistry.projectDirect(baseTools()));
@@ -1385,13 +1508,8 @@ export function createSessionOpener(services) {
1385
1508
  : [...messages, pendingOutputContinuation];
1386
1509
  sources.bind(toolScope);
1387
1510
  const composedSystem = splitContextualPrompt(await composeSystemPrompt());
1388
- const candidateWorld = mode === 'compact'
1389
- ? contextualWorldState
1390
- : updatePromptWorldState(contextualWorldState, messages.length, composedSystem.parts);
1391
- const projectedMessages = projectSessionRequestMessages(requestContextMessages, mode === 'compact' ? [] : candidateWorld.entries, mode !== 'compact' && selectedModel.inputCapabilities.includes('IMAGE'), codeModeEnabled, permissionPolicy.mode === 'auto');
1392
- const modelMessages = mode === 'compact'
1393
- ? projectCompactPromptMessages(projectedMessages, composedSystem.parts, contextualWorldState.promptSnapshot ?? [])
1394
- : projectedMessages;
1511
+ const candidateWorld = updatePromptWorldState(contextualWorldState, messages.length, composedSystem.parts);
1512
+ const modelMessages = projectSessionRequestMessages(requestContextMessages, candidateWorld.entries, selectedModel.inputCapabilities.includes('IMAGE'), codeModeEnabled, permissionPolicy.mode === 'auto');
1395
1513
  const systemPrompt = composedSystem.system;
1396
1514
  const candidateRequest = {
1397
1515
  system: systemPrompt,
@@ -1401,12 +1519,11 @@ export function createSessionOpener(services) {
1401
1519
  messages: modelMessages,
1402
1520
  tools: requestTools,
1403
1521
  ...(permissionPolicy.mode === 'auto' ? { allowHostedTools: false } : {}),
1404
- ...(mode === 'compact' ? { toolChoice: 'none' } : {}),
1405
1522
  promptCacheKey: sessionId,
1406
1523
  reasoningEffort,
1407
1524
  ...(requestReasoningEffort === undefined ? {} : { requestReasoningEffort }),
1408
1525
  ...(modelInputImages?.length ? { images: modelInputImages } : {}),
1409
- onAttemptDiagnostic: recordModelAttempt(mode === 'compact' ? 'COMPACTION' : 'EXECUTION')
1526
+ onAttemptDiagnostic: recordModelAttempt('EXECUTION')
1410
1527
  };
1411
1528
  preparingRequestMessages = modelMessages;
1412
1529
  preparingRequestBaseLength = messages.length;
@@ -1422,33 +1539,41 @@ export function createSessionOpener(services) {
1422
1539
  // Commit only after request middleware accepts the entire candidate.
1423
1540
  await commitWorldState(candidateWorld);
1424
1541
  }
1542
+ const lateCoreChange = inputs.hasBoundaryCompactPending && (await appendMailboxMessages());
1543
+ const lateBlocking = compactRequests.drain((input) => startCompact({
1544
+ ...(input.prompt === undefined ? {} : { focus: input.prompt }),
1545
+ async: input.async === true
1546
+ }));
1547
+ const lateAdopted = lateBlocking && (await compactCoordinator.checkpoint(true));
1548
+ if (lateCoreChange || lateAdopted) {
1549
+ // Middleware may admit a blocking join after the initial checkpoint.
1550
+ // Never sample the candidate assembled against the replaced context.
1551
+ return prepareRound(pendingOutputContinuation);
1552
+ }
1425
1553
  return { request, toolScope };
1426
1554
  },
1427
1555
  startModel: async function* (request) {
1428
- const snapshots = mode === 'compact'
1429
- ? undefined
1430
- : (sessionRuntime.requestSnapshots ??= new ExtensionRequestSnapshots());
1431
- const snapshot = snapshots?.record(selectedModel.id, request);
1432
- if (manualCompactionId !== undefined && !manualCompactionStarted) {
1433
- manualCompactionStarted = true;
1434
- manualCompaction = createCompactionOperation(manualCompactionId, compactSignal);
1435
- await emit({ type: 'context.compacting', compactionId: manualCompactionId });
1436
- }
1437
- const compaction = manualCompaction;
1556
+ const snapshots = (sessionRuntime.requestSnapshots ??=
1557
+ new ExtensionRequestSnapshots());
1558
+ const snapshot = snapshots.record(selectedModel.id, request);
1438
1559
  yield* streamModel(async function* (nativeSignal) {
1439
- const events = compaction
1440
- ? compaction.start({ start: (value) => selected.start(value, nativeSignal) }, request)
1441
- : selected.start(request, nativeSignal);
1442
- for await (const event of events) {
1560
+ for await (const event of selected.start(request, nativeSignal)) {
1443
1561
  if (snapshot && event.type === 'completed' && event.reason !== 'cancelled')
1444
1562
  snapshots.complete(snapshot);
1445
1563
  yield event;
1446
1564
  }
1447
- }, compaction?.signal ?? controller.signal);
1565
+ }, controller.signal);
1448
1566
  },
1449
- recoverySignal: () => manualCompaction?.signal ?? controller.signal,
1567
+ recoverySignal: () => controller.signal,
1450
1568
  compactContext,
1451
- compactionFallback: useManualCompactionFallback,
1569
+ afterModel: async (closedPrefixLength) => {
1570
+ compactBoundary = closedPrefixLength;
1571
+ if (lastServerContextTokens !== undefined &&
1572
+ (lastServerContextTokens * 100) / selectedModel.contextWindowTokens >=
1573
+ AGENT_EXECUTION_POLICY.backgroundCompactionPercent &&
1574
+ !compactCoordinator.pending)
1575
+ startCompact({ async: true });
1576
+ },
1452
1577
  observeContextTokens: (tokens) => {
1453
1578
  lastServerContextTokens = tokens;
1454
1579
  },
@@ -1459,7 +1584,18 @@ export function createSessionOpener(services) {
1459
1584
  }
1460
1585
  },
1461
1586
  checkpoint: async (round, latestUsage) => {
1462
- if (mode !== 'compact' &&
1587
+ compactBoundary = messages.length;
1588
+ const blocking = compactRequests.drain((input) => startCompact({
1589
+ ...(input.prompt === undefined ? {} : { focus: input.prompt }),
1590
+ async: input.async === true
1591
+ }));
1592
+ if (blocking)
1593
+ await compactCoordinator.checkpoint(true);
1594
+ else
1595
+ await compactCoordinator.checkpoint(false).catch(() => {
1596
+ controller.signal.throwIfAborted();
1597
+ });
1598
+ if (!compactCoordinator.pending &&
1463
1599
  sessionSource.type !== 'subagent' &&
1464
1600
  latestUsage !== undefined) {
1465
1601
  // Only a committed GC checkpoint may replace active context and invalidate continuation.
@@ -1511,51 +1647,23 @@ export function createSessionOpener(services) {
1511
1647
  }
1512
1648
  },
1513
1649
  mailbox: {
1514
- hasPending: () => inputs.hasBoundaryPending && !inputs.executing && mode !== 'compact',
1650
+ hasPending: () => inputs.hasBoundaryPending && !inputs.executing,
1515
1651
  append: appendMailboxMessages,
1516
1652
  close: () => {
1517
1653
  acceptsMailbox = false;
1518
1654
  }
1519
1655
  }
1520
1656
  });
1521
- if (manualExtension) {
1522
- if (manualExtension.usage)
1523
- await emit({ type: 'usage.updated', ...publicModelUsage(manualExtension.usage) });
1524
- }
1525
- const completed = manualExtension
1526
- ? { finalAnswer: manualExtension.summary, usage: manualExtension.usage }
1527
- : await executionLoop.run();
1657
+ const completed = await executionLoop.run();
1658
+ compactRequests.seal();
1659
+ compactRequests.drain((input) => startCompact({
1660
+ ...(input.prompt === undefined ? {} : { focus: input.prompt }),
1661
+ async: input.async === true
1662
+ }));
1663
+ await compactCoordinator.checkpoint(true).catch(() => {
1664
+ controller.signal.throwIfAborted();
1665
+ });
1528
1666
  finalAnswer = completed.finalAnswer;
1529
- const compactSummary = mode === 'compact' ? finalAnswer.trim() : '';
1530
- if (manualCompaction) {
1531
- manualCompaction.signal.throwIfAborted();
1532
- await manualCompaction.dispose();
1533
- }
1534
- if (mode === 'compact') {
1535
- const compactRecord = await writeCompactionRecord(compactSummary, false, compactSignal, async (actual, options) => {
1536
- const retainedModelContext = pendingAppends
1537
- .slice(manualAppendBoundary)
1538
- .map((entry) => entry.message);
1539
- const replacementHistory = buildCompactedHistory(actual, compactionUserMessages, compactUserTokenLimit);
1540
- const record = await store.compact(sessionId, {
1541
- summary: actual,
1542
- retainedModelContext,
1543
- replacementHistory,
1544
- modelId: selectedModel.id,
1545
- contextWindowTokens: selectedModel.contextWindowTokens,
1546
- ...(completed.usage === undefined ? {} : { usage: completed.usage }),
1547
- executionId,
1548
- turnId
1549
- }, options);
1550
- messages.splice(0, messages.length, ...compactedHistoryMessages(replacementHistory), ...retainedModelContext);
1551
- pendingAppends.length = 0;
1552
- selected.resetContinuation?.();
1553
- return record;
1554
- });
1555
- await resetCompactWorldState(compactRecord.recordId);
1556
- }
1557
- if (mode === 'compact' && manualCompactionId !== undefined)
1558
- await emit({ type: 'context.compacted', compactionId: manualCompactionId });
1559
1667
  };
1560
1668
  if (mode === 'compact')
1561
1669
  await performCompact({ focus: input.prompt }, async (effective, call) => {
@@ -1579,6 +1687,10 @@ export function createSessionOpener(services) {
1579
1687
  catch (error) {
1580
1688
  finalAnswer = executionLoop?.finalAnswer ?? finalAnswer;
1581
1689
  acceptsMailbox = false;
1690
+ await compactCoordinator.close(error);
1691
+ compactRequests.close(new MarAgentError('MAR_AGENT_CANCELLED', 'Execution ended.'));
1692
+ if (sessionRuntime.compactRequests === compactRequests)
1693
+ delete sessionRuntime.compactRequests;
1582
1694
  if (controller.signal.aborted) {
1583
1695
  subagents?.endParentExecution(executionId);
1584
1696
  await emit({ type: 'execution.cancelled' });
@@ -1602,6 +1714,9 @@ export function createSessionOpener(services) {
1602
1714
  finally {
1603
1715
  if (sessionRuntime.requestMessages?.executionId === executionId)
1604
1716
  delete sessionRuntime.requestMessages;
1717
+ compactRequests.close(new MarAgentError('MAR_AGENT_CANCELLED', 'Execution ended.'));
1718
+ if (sessionRuntime.compactRequests === compactRequests)
1719
+ delete sessionRuntime.compactRequests;
1605
1720
  if (sessionRuntime.appendedMessages?.executionId === executionId)
1606
1721
  delete sessionRuntime.appendedMessages;
1607
1722
  if (promptDefaults?.executionId === executionId)
@@ -1611,7 +1726,7 @@ export function createSessionOpener(services) {
1611
1726
  if (sessionRuntime.toolSources === executionToolSources)
1612
1727
  delete sessionRuntime.toolSources;
1613
1728
  internal.signal?.removeEventListener('abort', abortSource);
1614
- await manualCompaction?.dispose();
1729
+ await compactCoordinator.close(controller.signal.reason ?? new MarAgentError('MAR_AGENT_CANCELLED', 'Execution ended.'));
1615
1730
  await selectedTurn?.close().catch(() => undefined);
1616
1731
  try {
1617
1732
  await closeModelAdapters(executionScopedAdapters);
@@ -1900,10 +2015,11 @@ export function createSessionOpener(services) {
1900
2015
  id: sessionId,
1901
2016
  run: (input) => execution('run', input),
1902
2017
  plan: (input) => execution('plan', input),
1903
- compact: (input = {}) => execution('compact', {
1904
- prompt: input.prompt ?? DEFAULT_COMPACTION_FOCUS,
1905
- ...input
1906
- }),
2018
+ compact: (input = {}) => sessionRuntime.compactRequests?.request(input) ??
2019
+ execution('compact', {
2020
+ prompt: input.prompt ?? DEFAULT_COMPACTION_FOCUS,
2021
+ ...input
2022
+ }),
1907
2023
  cancel: async (executionId) => active.get(`${sessionId}:${executionId}`)?.abort(),
1908
2024
  startSubagentTurn: (turn) => execution(turn.role === 'explore' ? 'plan' : 'run', {
1909
2025
  prompt: turn.prompt,