@myagentroam/agent 0.11.14 → 0.11.15

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.
@@ -7,7 +7,7 @@ import type { CoreEnvironmentBackend } from '../extensions/environment.js';
7
7
  import type { CoreOperationContext } from '../extensions/operation.js';
8
8
  import type { CoreToolRef } from '../extensions/tools.js';
9
9
  export declare const MAR_AGENT_HOST_API_VERSION = "1.0";
10
- export type MarAgentHostCapability = 'workspace.read' | 'workspace.patch' | 'process.manage' | 'code.execute' | 'user-input.request' | 'event.publish' | 'session.store' | 'mcp.tools' | 'image.read';
10
+ export type MarAgentHostCapability = 'workspace.read' | 'workspace.patch' | 'process.manage' | 'code.execute' | 'user-input.request' | 'event.publish';
11
11
  export interface HostDescription {
12
12
  apiVersion: string;
13
13
  mode: 'normal' | 'node';
@@ -51,7 +51,7 @@ export interface HostSessionResourceSnapshot {
51
51
  cwd: string;
52
52
  }[];
53
53
  }
54
- /** The Host owns workspace/tool effects; the SDK owns JSONL storage beneath its supplied HOME. */
54
+ /** The Host owns workspace/tool effects; SessionStore is injected or defaults to SDK-owned JSONL. */
55
55
  export interface MarAgentHost {
56
56
  /** Independent model consumption facts, including child and auxiliary calls. */
57
57
  modelUsage?: import('../model/usage-observer.js').ModelUsageObserver | undefined;
@@ -5,8 +5,7 @@ const requiredCapabilities = [
5
5
  'workspace.patch',
6
6
  'process.manage',
7
7
  'user-input.request',
8
- 'event.publish',
9
- 'session.store'
8
+ 'event.publish'
10
9
  ];
11
10
  export function negotiateHost(description) {
12
11
  const match = /^(\d+)\.(\d+)$/.exec(description.apiVersion);
@@ -103,8 +103,7 @@ export class LocalAgentHost {
103
103
  'process.manage',
104
104
  'code.execute',
105
105
  'user-input.request',
106
- 'event.publish',
107
- 'session.store'
106
+ 'event.publish'
108
107
  ],
109
108
  limits: {
110
109
  maxProcesses: TOOL_EXECUTION_LIMITS.maxProcesses,
@@ -1,4 +1,4 @@
1
- import type { CompactInput, ExecutionHandle } from '../../sdk/types.js';
1
+ import type { CompactInput, ExecutionHandle } from '../execution-handle.js';
2
2
  import type { MarAgentEvent } from '../events.js';
3
3
  import type { CompactFlight } from './coordinator.js';
4
4
  /** External handles represent an operation inside its owning main Execution, not another loop. */
@@ -100,13 +100,16 @@ async function attemptContextGc(input) {
100
100
  records =
101
101
  input.records ?? (await input.store.readExecutionState(input.sessionId)).contextRecords;
102
102
  const checkpointCache = new ContextGcCheckpointCache();
103
- const previous = checkpointCache.read(records, undefined, input.workspace);
103
+ const previous = checkpointCache.read(records);
104
104
  if (previous) {
105
105
  deferred = {
106
106
  ...deferred,
107
107
  checkpointTrigger: previous.payload.trigger,
108
108
  protectedCandidatesPending: previous.payload.metrics.protectedCandidateTokens > 0
109
109
  };
110
+ }
111
+ if (previous?.payload.trigger.modelId === input.modelId &&
112
+ previous.payload.trigger.contextWindowTokens === input.contextWindowTokens) {
110
113
  const rolloutGrowth = records.filter((record) => record.sequence > previous.record.sequence &&
111
114
  record.type === 'model.attempt' &&
112
115
  isRecord(record.payload) &&
@@ -120,7 +123,7 @@ async function attemptContextGc(input) {
120
123
  contextGcMinimumUsageGrowth(input.contextWindowTokens))
121
124
  return deferred;
122
125
  }
123
- const projection = buildContextProjection(records, input.workspace, checkpointCache);
126
+ const projection = buildContextProjection(records, checkpointCache);
124
127
  const inspection = inspectContextGc({
125
128
  records,
126
129
  checkpointCache,
@@ -159,14 +162,14 @@ async function attemptContextGc(input) {
159
162
  const committed = await input.store.commitContextGc(input.sessionId, draft, records);
160
163
  return {
161
164
  ...deferred,
162
- result: await matchingCheckpointWasPersisted(input.store, input.sessionId, draft, input.workspace, records, committed)
165
+ result: await matchingCheckpointWasPersisted(input.store, input.sessionId, draft, records, committed)
163
166
  };
164
167
  }
165
168
  catch (commitError) {
166
169
  try {
167
170
  return {
168
171
  ...deferred,
169
- result: await matchingCheckpointWasPersisted(input.store, input.sessionId, draft, input.workspace, records)
172
+ result: await matchingCheckpointWasPersisted(input.store, input.sessionId, draft, records)
170
173
  };
171
174
  }
172
175
  catch {
@@ -176,7 +179,7 @@ async function attemptContextGc(input) {
176
179
  return deferred;
177
180
  }
178
181
  }
179
- async function matchingCheckpointWasPersisted(store, sessionId, draft, workspace, records, committed) {
182
+ async function matchingCheckpointWasPersisted(store, sessionId, draft, records, committed) {
180
183
  const record = committed ??
181
184
  (await store.readPage(sessionId, {
182
185
  limit: 1,
@@ -185,7 +188,7 @@ async function matchingCheckpointWasPersisted(store, sessionId, draft, workspace
185
188
  if (record?.sequence !== draft.boundarySequence + 1 || record.type !== 'context.gc.completed')
186
189
  return false;
187
190
  const updatedRecords = [...records, record];
188
- const projection = buildContextProjection(updatedRecords, workspace);
191
+ const projection = buildContextProjection(updatedRecords);
189
192
  if (committed)
190
193
  return { records: updatedRecords, projection };
191
194
  const payload = projection.checkpoint;
@@ -1,5 +1,5 @@
1
1
  import { codeCallEvidence, codePartialPatchCallEvidence, codeObservationEvidence, retainedCodeInputs } from '../../session/context-gc-observations.js';
2
- import { ContextGcCheckpointCache, contextGcBaseFromRecords, contextGcLogicalPathKey, contextGcMinimumUsageGrowth, contextGcTargetKey, normalizeContextGcReplacements } from '../../session/context-gc-checkpoint.js';
2
+ import { ContextGcCheckpointCache, contextGcBaseFromRecords, contextGcCheckpointIntervalEligible, contextGcLogicalPathKey, contextGcMinimumUsageGrowth, contextGcTargetKey, normalizeContextGcReplacements } from '../../session/context-gc-checkpoint.js';
3
3
  import { codeCellCompletionSequence, contextGcApplyPatchInputReplacement, contextGcCodeModeInputReplacement, contextGcCodeModeOutputReplacement, contextGcExecInputReplacement, contextGcExecOutputReplacement, contextGcPatchResultFromCompletedPayload, contextGcPatchResultFromFailedPayload, contextGcStaleReadReplacement, contextGcWebOutputReplacement, normalizeContextGcCodeModeInput, normalizeContextGcExecResult } from '../../session/context-gc-replacements.js';
4
4
  import { contextGcSubagentNeedsRetainedPriorFinal, contextGcSubagentOutputReplacement, contextGcSubagentPriorFinalIsRetained, contextGcSubagentSnapshotIsPreciselyCovered, contextGcSubagentSnapshotIsSuperseded, contextGcSubagentSnapshotTurnId, normalizeContextGcSubagentSnapshot } from '../../session/context-gc-subagent.js';
5
5
  import { parseReadContextOutput } from '../../tools/local/read-context.js';
@@ -131,15 +131,25 @@ export function inspectContextGc(input) {
131
131
  if (!usageRecord)
132
132
  return {};
133
133
  const normalRolloutOrdinal = countSuccessfulNormalRollouts(input.records, input.modelId, input.contextWindowTokens);
134
- const previous = input.checkpointCache.read(input.records, undefined, input.workspace);
135
- if (previous) {
136
- const minimumUsageGrowth = contextGcMinimumUsageGrowth(input.contextWindowTokens);
137
- const protectedCandidatesPending = previous.payload.metrics.protectedCandidateTokens > 0;
134
+ const previous = input.checkpointCache.read(input.records);
135
+ const checkpointEligible = contextGcCheckpointIntervalEligible({
136
+ usageSequence: usageRecord.sequence,
137
+ contextInputTokens: contextUsageTokens,
138
+ contextWindowTokens: input.contextWindowTokens,
139
+ normalRolloutOrdinal,
140
+ modelId: input.modelId,
141
+ platform: input.platform
142
+ }, previous?.payload.trigger);
143
+ // Discovery and economic evaluation have their own low-growth opportunities;
144
+ // neither a protected candidate nor periodic discovery makes a draft eligible.
145
+ if (previous?.payload.trigger.modelId === input.modelId &&
146
+ previous.payload.trigger.contextWindowTokens === input.contextWindowTokens) {
138
147
  const rolloutGrowth = normalRolloutOrdinal - previous.payload.trigger.normalRolloutOrdinal;
139
148
  if (rolloutGrowth < 2 ||
140
- (!protectedCandidatesPending &&
149
+ (previous.payload.metrics.protectedCandidateTokens === 0 &&
141
150
  rolloutGrowth < CONTEXT_GC_POLICY.largeExecProtectionRollouts &&
142
- contextUsageTokens - previous.payload.trigger.contextInputTokens < minimumUsageGrowth))
151
+ contextUsageTokens - previous.payload.trigger.contextInputTokens <
152
+ contextGcMinimumUsageGrowth(input.contextWindowTokens)))
143
153
  return {};
144
154
  }
145
155
  if (input.messages.length !== input.messageSources.length)
@@ -188,41 +198,45 @@ export function inspectContextGc(input) {
188
198
  normalRolloutOrdinal,
189
199
  protectedCandidateTokens
190
200
  });
191
- const plan = finalizePlan(input, marginal ?? stageAPlanWithProtectedCandidates);
201
+ const plan = checkpointEligible
202
+ ? finalizePlan(input, marginal ?? stageAPlanWithProtectedCandidates)
203
+ : undefined;
192
204
  const retryAfterRollouts = Math.min(exec.retryAfterRollouts, codeMode.retryAfterRollouts, web.retryAfterRollouts);
193
- return plan
194
- ? {
195
- plan,
196
- ...(protectedCandidateTokens > 0 && Number.isFinite(retryAfterRollouts)
197
- ? { retryAfterRollouts }
198
- : {})
199
- }
200
- : {};
205
+ if (!plan && checkpointEligible)
206
+ return {};
207
+ return {
208
+ ...(plan ? { plan } : {}),
209
+ ...(protectedCandidateTokens > 0 && Number.isFinite(retryAfterRollouts)
210
+ ? { retryAfterRollouts }
211
+ : {})
212
+ };
201
213
  }
202
214
  const exec = collectExecCandidates(input, groups, existingTargets, candidateSavings);
203
215
  const codeMode = collectCodeModeCandidates(input, groups, existingTargets, candidateSavings);
204
216
  const web = collectWebCandidates(input, groups, existingTargets, candidateSavings);
217
+ const protectedCandidateTokens = deterministicProtectedTokens +
218
+ exec.protectedTokens +
219
+ codeMode.protectedTokens +
220
+ web.protectedTokens;
205
221
  const combined = evaluateCandidateSet(input, existing, [...deterministicCandidates, ...exec.eligible, ...codeMode.eligible, ...web.eligible], {
206
222
  stage: 'STAGE_A_AND_EXEC',
207
223
  contextUsageTokens,
208
224
  usageRatio,
209
225
  usageRecord,
210
226
  normalRolloutOrdinal,
211
- protectedCandidateTokens: deterministicProtectedTokens +
212
- exec.protectedTokens +
213
- codeMode.protectedTokens +
214
- web.protectedTokens
227
+ protectedCandidateTokens
215
228
  });
216
- if (combined?.economics.passes) {
229
+ if (combined?.economics.passes && checkpointEligible) {
217
230
  const plan = finalizePlan(input, combined);
218
231
  return plan ? { plan } : {};
219
232
  }
220
233
  const retryAfterRollouts = Math.min(stageA.retryAfterRollouts, subagent.retryAfterRollouts, exec.retryAfterRollouts, codeMode.retryAfterRollouts, web.retryAfterRollouts);
221
- return deterministicProtectedTokens +
222
- exec.protectedTokens +
223
- codeMode.protectedTokens +
224
- web.protectedTokens >=
225
- contextGcMinimumReclaimedTokens(contextUsageTokens, input.contextWindowTokens) &&
234
+ // A worthwhile mixed set retains every positive protected candidate's retry
235
+ // hint even when the checkpoint interval prevents a durable draft.
236
+ return protectedCandidateTokens > 0 &&
237
+ (combined?.economics.passes ||
238
+ protectedCandidateTokens >=
239
+ contextGcMinimumReclaimedTokens(contextUsageTokens, input.contextWindowTokens)) &&
226
240
  Number.isFinite(retryAfterRollouts)
227
241
  ? { retryAfterRollouts }
228
242
  : {};
@@ -459,7 +473,7 @@ function evaluateCandidateSet(input, existing, candidates, state) {
459
473
  if (reclaimedTokens === 0)
460
474
  return undefined;
461
475
  const cache = contextGcCacheImpact(input, input.messages, projected.messages, input.usage.cacheReadTokens ?? 0, reclaimedTokens);
462
- const growthSamples = collectContextGrowthSamples(input.records, input.modelId, input.contextWindowTokens, input.workspace, input.checkpointCache);
476
+ const growthSamples = collectContextGrowthSamples(input.records, input.modelId, input.contextWindowTokens, input.checkpointCache);
463
477
  const contextGrowthP75Tokens = percentile75(growthSamples);
464
478
  const compactThresholdTokens = Math.floor(input.contextWindowTokens * COMPACT_THRESHOLD_RATIO);
465
479
  // Usage describes the request before its generated output and tool results.
@@ -1098,8 +1112,8 @@ function latestUsageRecord(records, selectedModelId) {
1098
1112
  }
1099
1113
  return latest;
1100
1114
  }
1101
- function collectContextGrowthSamples(records, modelId, contextWindowTokens, workspace, checkpointCache) {
1102
- const previous = checkpointCache.read(records, undefined, workspace);
1115
+ function collectContextGrowthSamples(records, modelId, contextWindowTokens, checkpointCache) {
1116
+ const previous = checkpointCache.read(records);
1103
1117
  const base = contextGcBaseFromRecords(records);
1104
1118
  const startSequence = previous?.record.sequence ?? (base?.type === 'COMPACT' ? base.sequence : base?.firstSequence);
1105
1119
  let activeModelId = modelId;
@@ -9,7 +9,7 @@ export interface ContextProjection {
9
9
  readonly entries: readonly ContextProjectionEntry[];
10
10
  readonly checkpoint?: ContextGcCheckpointPayload;
11
11
  }
12
- export declare function buildContextProjection(records: readonly SessionRecord[], workspace?: string, checkpointCache?: ContextGcCheckpointCache): ContextProjection;
12
+ export declare function buildContextProjection(records: readonly SessionRecord[], checkpointCache?: ContextGcCheckpointCache): ContextProjection;
13
13
  export declare function applyContextGcReplacementsToMessages(messages: readonly ModelMessage[], messageSources: readonly SessionRecord[], replacements: readonly ContextGcReplacement[]): {
14
14
  readonly messages: ModelMessage[];
15
15
  readonly messageSources: SessionRecord[];
@@ -6,7 +6,7 @@ import { TOOL_EXECUTION_LIMITS } from '../../tools/execution-limits.js';
6
6
  import { replaceReadContextItems } from '../../tools/local/read-context.js';
7
7
  import { parseCodeCalls, parseObservations, renderObservationResult } from '../../tools/code-mode/observations.js';
8
8
  const MAX_MODEL_CONTEXT_BYTES = 1024 * 1024;
9
- export function buildContextProjection(records, workspace, checkpointCache) {
9
+ export function buildContextProjection(records, checkpointCache) {
10
10
  let activeModelId;
11
11
  const rawEntries = [];
12
12
  for (const record of records) {
@@ -33,8 +33,8 @@ export function buildContextProjection(records, workspace, checkpointCache) {
33
33
  rawEntries.push({ record, message });
34
34
  }
35
35
  const valid = checkpointCache
36
- ? checkpointCache.read(records, undefined, workspace)
37
- : latestValidContextGcCheckpoint(records, undefined, workspace);
36
+ ? checkpointCache.read(records)
37
+ : latestValidContextGcCheckpoint(records);
38
38
  const messages = rawEntries.flatMap((entry) => (entry.message ? [entry.message] : []));
39
39
  const sources = rawEntries.flatMap((entry) => (entry.message ? [entry.record] : []));
40
40
  const applied = applyContextGcReplacementsToMessages(messages, sources, valid?.payload.replacements ?? []);
@@ -9,8 +9,7 @@ export async function restoreMessages(store, sessionId, records, options) {
9
9
  const messages = [];
10
10
  const outcomes = new Map();
11
11
  const toolOutcomes = new WeakMap();
12
- const modelProjection = new Map((options.projection ??
13
- buildContextProjection(records, (await store.getSession(sessionId)).workspace)).entries.map((entry) => [entry.record.recordId, entry.message]));
12
+ const modelProjection = new Map((options.projection ?? buildContextProjection(records)).entries.map((entry) => [entry.record.recordId, entry.message]));
14
13
  let userMessages = [];
15
14
  let ignoredImages = 0;
16
15
  let currentTurnUserIndex;
@@ -0,0 +1,12 @@
1
+ import type { ExecutionResult, RunInput } from './contracts.js';
2
+ import type { MarAgentEvent } from './events.js';
3
+ /** A live execution exposes streamed events separately from its terminal result. */
4
+ export interface ExecutionHandle extends AsyncIterable<MarAgentEvent> {
5
+ readonly executionId: string;
6
+ result(): Promise<ExecutionResult>;
7
+ /** Signals abort; await result() to observe the terminal status and cleanup. */
8
+ cancel(): void;
9
+ }
10
+ export type CompactInput = Partial<RunInput> & {
11
+ readonly async?: boolean;
12
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -223,29 +223,46 @@ export async function createMarAgentWithAdapter(options) {
223
223
  const runtime = sessionRuntimes.get(id);
224
224
  if ([...active.keys()].some((key) => key.startsWith(`${id}:`)))
225
225
  throw new MarAgentError('MAR_AGENT_EXECUTION_ACTIVE', 'A session cannot be cleared while an execution is active.');
226
- await extensions.endSession(id, 'clear');
227
- extensions.retireSession(id);
228
- await completeAgentCleanup([
229
- () => extensions.clearSession(id),
230
- () => runtime?.inputs?.retire(),
231
- () => runtime?.subagents?.dispose(),
232
- () => closeModelAdapters(runtime === undefined ? [] : [...runtime.adapters.values()]),
233
- () => {
234
- // Existing SDK Session handles share this runtime and remain usable after clear.
235
- if (runtime) {
236
- runtime.adapters.clear();
237
- runtime.adapterGeneration = modelGeneration;
238
- delete runtime.todo;
239
- delete runtime.subagents;
240
- delete runtime.subagentsOpening;
241
- delete runtime.contextualWorldState;
242
- delete runtime.inputs;
243
- delete runtime.requestSnapshots;
244
- }
245
- },
246
- () => options.host.disposeSessionResources?.(id),
247
- () => store.clearSession(id)
248
- ]);
226
+ const maintenance = await store.acquireSessionMaintenance(id, 'clear', sessionWriterOwner);
227
+ let failed = false;
228
+ try {
229
+ await completeAgentCleanup([
230
+ () => extensions.endSession(id, 'clear'),
231
+ () => extensions.retireSession(id),
232
+ () => extensions.clearSession(id),
233
+ () => runtime?.inputs?.retire(),
234
+ () => runtime?.subagents?.dispose(),
235
+ () => closeModelAdapters(runtime === undefined ? [] : [...runtime.adapters.values()]),
236
+ () => {
237
+ // Existing SDK Session handles share this runtime and remain usable after clear.
238
+ if (runtime) {
239
+ runtime.adapters.clear();
240
+ runtime.adapterGeneration = modelGeneration;
241
+ delete runtime.todo;
242
+ delete runtime.subagents;
243
+ delete runtime.subagentsOpening;
244
+ delete runtime.contextualWorldState;
245
+ delete runtime.inputs;
246
+ delete runtime.requestSnapshots;
247
+ }
248
+ },
249
+ () => options.host.disposeSessionResources?.(id),
250
+ () => store.clearSession(id, maintenance)
251
+ ]);
252
+ }
253
+ catch (error) {
254
+ failed = true;
255
+ throw error;
256
+ }
257
+ finally {
258
+ try {
259
+ await maintenance.release();
260
+ }
261
+ catch (error) {
262
+ if (!failed)
263
+ throw error;
264
+ }
265
+ }
249
266
  }),
250
267
  setSystemInstruction: (systemInstruction) => {
251
268
  if (disposed)
@@ -333,31 +350,62 @@ export async function createMarAgentWithAdapter(options) {
333
350
  }
334
351
  },
335
352
  deleteSession: (id) => trackSessionLifecycle(id, 'delete', async () => {
336
- const sessionIds = await store.sessionTree(id);
337
- const sessionIdSet = new Set(sessionIds);
338
- const executionKeys = [...active.keys()].filter((key) => sessionIdSet.has(key.slice(0, key.indexOf(':'))));
339
- for (const key of executionKeys)
340
- active.get(key)?.abort();
341
- await Promise.all(sessionIds.map((sessionId) => extensions.endSession(sessionId, 'other', false)));
342
- for (const sessionId of sessionIds)
343
- extensions.retireSession(sessionId);
344
- const runtimes = sessionIds.flatMap((sessionId) => {
345
- const runtime = sessionRuntimes.get(sessionId);
346
- sessionRuntimes.delete(sessionId);
347
- return runtime === undefined ? [] : [runtime];
348
- });
349
- await completeAgentCleanup([
350
- ...sessionIds.map((sessionId) => () => extensions.clearSession(sessionId)),
351
- ...runtimes.map((runtime) => () => runtime.inputs?.retire()),
352
- ...runtimes.map((runtime) => () => runtime.subagents?.dispose()),
353
- () => Promise.allSettled(executionKeys.flatMap((key) => {
354
- const result = executionResults.get(key);
355
- return result === undefined ? [] : [result];
356
- })),
357
- () => closeModelAdapters(runtimes.flatMap((runtime) => [...runtime.adapters.values()])),
358
- ...sessionIds.map((sessionId) => () => options.host.disposeSessionResources?.(sessionId)),
359
- () => store.delete(id)
360
- ]);
353
+ const maintenance = await store.acquireSessionMaintenance(id, 'delete', sessionWriterOwner);
354
+ let failed = false;
355
+ try {
356
+ const sessionIds = maintenance.sessionIds;
357
+ const sessionIdSet = new Set(sessionIds);
358
+ const executionKeys = [...active.keys()].filter((key) => sessionIdSet.has(key.slice(0, key.indexOf(':'))));
359
+ for (const key of executionKeys)
360
+ active.get(key)?.abort();
361
+ const runtimes = sessionIds.flatMap((sessionId) => {
362
+ const runtime = sessionRuntimes.get(sessionId);
363
+ return runtime === undefined ? [] : [runtime];
364
+ });
365
+ await completeAgentCleanup([
366
+ async () => {
367
+ let failure;
368
+ await Promise.all(sessionIds.map(async (sessionId) => {
369
+ try {
370
+ await extensions.endSession(sessionId, 'other', false);
371
+ }
372
+ catch (error) {
373
+ failure ??= { error };
374
+ }
375
+ }));
376
+ if (failure)
377
+ throw failure.error;
378
+ },
379
+ ...sessionIds.map((sessionId) => () => extensions.retireSession(sessionId)),
380
+ () => {
381
+ for (const sessionId of sessionIds)
382
+ sessionRuntimes.delete(sessionId);
383
+ },
384
+ ...sessionIds.map((sessionId) => () => extensions.clearSession(sessionId)),
385
+ ...runtimes.map((runtime) => () => runtime.inputs?.retire()),
386
+ ...runtimes.map((runtime) => () => runtime.subagents?.dispose()),
387
+ () => Promise.allSettled(executionKeys.flatMap((key) => {
388
+ const result = executionResults.get(key);
389
+ return result === undefined ? [] : [result];
390
+ })),
391
+ () => closeModelAdapters(runtimes.flatMap((runtime) => [...runtime.adapters.values()])),
392
+ ...sessionIds.map((sessionId) => () => options.host.disposeSessionResources?.(sessionId)),
393
+ () => store.delete(id, maintenance)
394
+ ]);
395
+ }
396
+ catch (error) {
397
+ failed = true;
398
+ throw error;
399
+ }
400
+ finally {
401
+ try {
402
+ await maintenance.release();
403
+ }
404
+ catch (error) {
405
+ if (!failed)
406
+ throw error;
407
+ }
408
+ }
361
409
  }),
362
410
  dispose: () => {
363
411
  if (disposal)
@@ -1,8 +1,8 @@
1
1
  import type { MarAgentHost } from '../host/contracts.js';
2
2
  import type { CoreExtensionSource, CoreRuntimeExtension } from '../extensions/contracts.js';
3
3
  import type { MarAgentModelConfiguration } from '../model/configuration.js';
4
- import type { ExecutionResult, RolloutBudgetOptions, RunInput } from '../runtime/contracts.js';
5
- import type { MarAgentEvent } from '../runtime/events.js';
4
+ import type { RolloutBudgetOptions, RunInput } from '../runtime/contracts.js';
5
+ import type { CompactInput, ExecutionHandle } from '../runtime/execution-handle.js';
6
6
  import type { MarAgentSessionPage, MarAgentSessionSummary } from '../session/catalog.js';
7
7
  import type { SessionListInput, SessionSource, SessionTitleUpdateInput } from '../session/types.js';
8
8
  export interface CreateSessionInput {
@@ -13,17 +13,8 @@ export type { SessionTitleUpdateInput } from '../session/types.js';
13
13
  export type { HostExecutionExtensions, HostSessionResourceSnapshot, HostSkill, MarAgentHost } from '../host/contracts.js';
14
14
  export type { AccessMode, ExecutionMode, ExecutionResult, MarAgentImageMetadata, RolloutBudgetOptions, RunInput, RunInputImage } from '../runtime/contracts.js';
15
15
  export type { MarAgentEvent, MarAgentEventBase } from '../runtime/events.js';
16
+ export type { CompactInput, ExecutionHandle } from '../runtime/execution-handle.js';
16
17
  export type { CodeCellEventData, MarAgentToolArtifact, MarAgentToolImageArtifact, McpCatalogTool, McpToolCallContext, McpToolCatalog, McpToolCatalogContext, ToolCall, ToolExecutionContext, ToolExecutionResult } from '../tools/contracts.js';
17
- /** A live execution exposes streamed events separately from its terminal result. */
18
- export interface ExecutionHandle extends AsyncIterable<MarAgentEvent> {
19
- readonly executionId: string;
20
- result(): Promise<ExecutionResult>;
21
- /** Signals abort; await result() to observe the terminal status and cleanup. */
22
- cancel(): void;
23
- }
24
- export type CompactInput = Partial<RunInput> & {
25
- readonly async?: boolean;
26
- };
27
18
  export interface SteerInput {
28
19
  readonly expectedExecutionId: string;
29
20
  readonly prompt: string;
@@ -143,9 +143,11 @@ export interface ContextGcCheckpointPayload extends ContextGcCheckpointDraft {
143
143
  /** Disposable memoization for one evaluation's immutable record snapshot. */
144
144
  export declare class ContextGcCheckpointCache {
145
145
  #private;
146
- read(records: readonly SessionRecord[], boundarySequence?: number, workspace?: string): ReturnType<typeof latestValidContextGcCheckpoint>;
146
+ read(records: readonly SessionRecord[], boundarySequence?: number): ReturnType<typeof latestValidContextGcCheckpoint>;
147
147
  }
148
148
  export declare function contextGcMinimumUsageGrowth(contextWindowTokens: number): number;
149
+ /** Compare adjacent checkpoint triggers only within the same model/window scope. */
150
+ export declare function contextGcCheckpointIntervalEligible(trigger: ContextGcCheckpointDraft['trigger'], previous?: ContextGcCheckpointDraft['trigger']): boolean;
149
151
  export declare function contextGcLogicalPathKey(path: string, workspace: string, platform?: ContextGcPlatform): string | undefined;
150
152
  export declare function contextGcTargetKey(target: ContextGcTarget): string;
151
153
  export declare function contextGcReplacementKey(replacement: ContextGcReplacement): string;
@@ -154,7 +156,7 @@ export declare function contextGcBaseSequence(base: ContextGcBase): number;
154
156
  export declare function normalizeContextGcReplacements(replacements: readonly ContextGcReplacement[]): ContextGcReplacement[] | undefined;
155
157
  export declare function parseContextGcCheckpoint(record: SessionRecord | undefined): ContextGcCheckpointPayload | undefined;
156
158
  export declare function validateContextGcCheckpoint(records: readonly SessionRecord[], record: SessionRecord, workspace?: string | undefined, committedReplacements?: readonly ContextGcReplacement[]): ContextGcCheckpointPayload | undefined;
157
- export declare function latestValidContextGcCheckpoint(records: readonly SessionRecord[], boundarySequence?: number, _workspace?: string): {
159
+ export declare function latestValidContextGcCheckpoint(records: readonly SessionRecord[], boundarySequence?: number): {
158
160
  readonly record: SessionRecord;
159
161
  readonly payload: ContextGcCheckpointPayload;
160
162
  } | undefined;
@@ -9,20 +9,28 @@ export const CONTEXT_GC_MAX_TARGETS = 4_096;
9
9
  /** Disposable memoization for one evaluation's immutable record snapshot. */
10
10
  export class ContextGcCheckpointCache {
11
11
  #snapshot;
12
- read(records, boundarySequence = records.at(-1)?.sequence ?? 0, workspace) {
12
+ read(records, boundarySequence = records.at(-1)?.sequence ?? 0) {
13
13
  const cached = this.#snapshot;
14
- if (cached?.records === records &&
15
- cached.boundarySequence === boundarySequence &&
16
- cached.workspace === workspace)
14
+ if (cached?.records === records && cached.boundarySequence === boundarySequence)
17
15
  return cached.value;
18
- const value = latestValidContextGcCheckpoint(records, boundarySequence, workspace);
19
- this.#snapshot = { records, boundarySequence, workspace, value };
16
+ const value = latestValidContextGcCheckpoint(records, boundarySequence);
17
+ this.#snapshot = { records, boundarySequence, value };
20
18
  return value;
21
19
  }
22
20
  }
23
21
  export function contextGcMinimumUsageGrowth(contextWindowTokens) {
24
22
  return Math.max(2_000, Math.floor(contextWindowTokens * 0.05));
25
23
  }
24
+ /** Compare adjacent checkpoint triggers only within the same model/window scope. */
25
+ export function contextGcCheckpointIntervalEligible(trigger, previous) {
26
+ if (!previous ||
27
+ trigger.modelId !== previous.modelId ||
28
+ trigger.contextWindowTokens !== previous.contextWindowTokens)
29
+ return true;
30
+ return (trigger.normalRolloutOrdinal - previous.normalRolloutOrdinal >= 2 &&
31
+ trigger.contextInputTokens - previous.contextInputTokens >=
32
+ contextGcMinimumUsageGrowth(trigger.contextWindowTokens));
33
+ }
26
34
  export function contextGcLogicalPathKey(path, workspace, platform) {
27
35
  if (!path || !workspace || path.includes('\0') || workspace.includes('\0'))
28
36
  return undefined;
@@ -198,7 +206,7 @@ export function validateContextGcCheckpoint(records, record, workspace = session
198
206
  }
199
207
  return payload;
200
208
  }
201
- export function latestValidContextGcCheckpoint(records, boundarySequence = records.at(-1)?.sequence ?? 0, _workspace) {
209
+ export function latestValidContextGcCheckpoint(records, boundarySequence = records.at(-1)?.sequence ?? 0) {
202
210
  const base = contextGcBaseFromRecords(records, boundarySequence);
203
211
  if (!base)
204
212
  return undefined;
@@ -49,6 +49,12 @@ export interface SessionStoreCompactInput {
49
49
  executionId?: string;
50
50
  turnId?: string;
51
51
  }
52
+ /** Store-issued ownership spanning SDK cleanup and the final durable mutation. */
53
+ export interface SessionStoreMaintenance {
54
+ readonly sessionIds: readonly string[];
55
+ /** Idempotent; releases maintenance only, never another owner's Execution/retained references. */
56
+ release(): Promise<void>;
57
+ }
52
58
  /**
53
59
  * Session persistence port. Implementations preserve record order, committed-resource
54
60
  * visibility and the existing per-Session writer/checkpoint atomic boundaries.
@@ -124,6 +130,8 @@ export interface SessionStore {
124
130
  /** Retention requires the originating owner to hold an active Execution lease. */
125
131
  retainSessionLease(sessionId: string, owner: object): Promise<() => Promise<void>>;
126
132
  hasRetainedSessionLease(sessionId: string, owner: object): boolean;
133
+ /** Conflicts fail before cleanup; delete includes the Store's current Session tree. */
134
+ acquireSessionMaintenance(sessionId: string, kind: 'clear' | 'delete', owner?: object): Promise<SessionStoreMaintenance>;
127
135
  forkSession(sessionId: string, input: {
128
136
  readonly boundaryTurnId: string | null;
129
137
  readonly title: string;
@@ -131,8 +139,8 @@ export interface SessionStore {
131
139
  rollbackSession(sessionId: string, input: {
132
140
  readonly boundaryTurnId: string | null;
133
141
  }): Promise<MarAgentSessionSummary>;
134
- clearSession(sessionId: string): Promise<MarAgentSessionSummary>;
135
- delete(sessionId: string): Promise<{
142
+ clearSession(sessionId: string, maintenance?: SessionStoreMaintenance): Promise<MarAgentSessionSummary>;
143
+ delete(sessionId: string, maintenance?: SessionStoreMaintenance): Promise<{
136
144
  readonly deleted: boolean;
137
145
  }>;
138
146
  /** Stop using the handle and release its leases before closing it. Never preempts other owners. */
@@ -1,5 +1,4 @@
1
1
  import { todoItemsFromSnapshot } from '../../tools/agent/todo.js';
2
- import { contextGcBaseSequence, latestValidContextGcCheckpoint } from '../context-gc-checkpoint.js';
3
2
  export function deriveExecutionState(records) {
4
3
  let automaticTitle = false;
5
4
  let sawUserTurn = false;
@@ -36,7 +35,6 @@ export function deriveExecutionState(records) {
36
35
  executionModel = candidate;
37
36
  }
38
37
  const boundary = records.findLastIndex(isCompactCheckpoint);
39
- const checkpoint = latestValidContextGcCheckpoint(records);
40
38
  return {
41
39
  automaticTitlePending: automaticTitle && !sawUserTurn,
42
40
  todo: { revision: todoRevision, items: todoItems },
@@ -45,13 +43,6 @@ export function deriveExecutionState(records) {
45
43
  version: 1,
46
44
  userTurns,
47
45
  ...(compactSequence === undefined ? {} : { compactSequence }),
48
- ...(checkpoint === undefined
49
- ? {}
50
- : {
51
- gcSequence: checkpoint.record.sequence,
52
- gcGeneration: checkpoint.payload.generation,
53
- gcBaseSequence: contextGcBaseSequence(checkpoint.payload.base)
54
- }),
55
46
  ...(todoSequence === undefined ? {} : { todoSequence }),
56
47
  todoRevision,
57
48
  executionModelIndexed: true,
@@ -170,9 +161,6 @@ export function sameResumeIndex(left, right) {
170
161
  return (left?.version === right.version &&
171
162
  left.userTurns === right.userTurns &&
172
163
  left.compactSequence === right.compactSequence &&
173
- left.gcSequence === right.gcSequence &&
174
- left.gcGeneration === right.gcGeneration &&
175
- left.gcBaseSequence === right.gcBaseSequence &&
176
164
  left.todoSequence === right.todoSequence &&
177
165
  left.todoRevision === right.todoRevision &&
178
166
  left.executionModelIndexed === right.executionModelIndexed &&
@@ -1,4 +1,4 @@
1
- import type { SessionStore, SessionRecordInput, SessionStoreCompactInput } from './contracts.js';
1
+ import type { SessionStore, SessionStoreMaintenance, SessionRecordInput, SessionStoreCompactInput } from './contracts.js';
2
2
  import { type CoreCheckpoint, type CoreJsonValue } from '../../extensions/data.js';
3
3
  import { type CodeModeStateEntry } from '../../tools/code-mode/state.js';
4
4
  import { type ContextGcCheckpointDraft } from '../context-gc-checkpoint.js';
@@ -10,7 +10,6 @@ export declare class JsonlSessionStore implements SessionStore {
10
10
  #private;
11
11
  private constructor();
12
12
  static open(home: string): Promise<JsonlSessionStore>;
13
- /** Append the durable creation record before making this Session visible in the index. */
14
13
  createSession(input: {
15
14
  title?: string;
16
15
  workspace: string;
@@ -57,7 +56,7 @@ export declare class JsonlSessionStore implements SessionStore {
57
56
  }>;
58
57
  updateTitle(sessionId: string, title: string, input?: SessionTitleUpdateInput, owner?: object): Promise<boolean>;
59
58
  /** Reset history with the same recovery journal while preserving the Session identity. */
60
- clearSession(sessionId: string): Promise<{
59
+ clearSession(sessionId: string, maintenance?: SessionStoreMaintenance): Promise<{
61
60
  id: string;
62
61
  title: string;
63
62
  workspace: string;
@@ -108,6 +107,7 @@ export declare class JsonlSessionStore implements SessionStore {
108
107
  /** A warm cell retains physical writer ownership between Executions of the same Agent. */
109
108
  acquireSessionLease(sessionId: string, owner?: object): Promise<() => Promise<void>>;
110
109
  hasRetainedSessionLease(sessionId: string, owner: object): boolean;
110
+ acquireSessionMaintenance(sessionId: string, kind: 'clear' | 'delete', owner?: object): Promise<SessionStoreMaintenance>;
111
111
  /** Native writes join their Agent's sole writer, without admitting another Execution. */
112
112
  acquireSessionWriteLease(sessionId: string, owner: object): Promise<() => Promise<void>>;
113
113
  retainSessionLease(sessionId: string, owner: object): Promise<() => Promise<void>>;
@@ -156,7 +156,7 @@ export declare class JsonlSessionStore implements SessionStore {
156
156
  /** Counts native input records, not the post-Compact model context. */
157
157
  userTurnCount(sessionId: string): Promise<number>;
158
158
  firstUserMessagePreview(sessionId: string): Promise<string | undefined>;
159
- delete(sessionId: string): Promise<{
159
+ delete(sessionId: string, maintenance?: SessionStoreMaintenance): Promise<{
160
160
  readonly deleted: boolean;
161
161
  }>;
162
162
  close(): Promise<void>;
@@ -7,7 +7,7 @@ import { cloneCoreJson } from '../../extensions/data.js';
7
7
  import { acquireFileLock } from '../../storage/file-lock.js';
8
8
  import { todoItemsFromSnapshot } from '../../tools/agent/todo.js';
9
9
  import { applyCodeModeStateMutations, parseCodeModeStateMutations, restoreCodeModeState } from '../../tools/code-mode/state.js';
10
- import { contextGcBaseFromRecords, contextGcBaseSequence, contextGcMinimumUsageGrowth, latestValidContextGcCheckpoint, normalizeContextGcReplacements, parseContextGcCheckpoint, sameContextGcBase, validateContextGcCheckpoint } from '../context-gc-checkpoint.js';
10
+ import { contextGcBaseFromRecords, contextGcCheckpointIntervalEligible, latestValidContextGcCheckpoint, normalizeContextGcReplacements, sameContextGcBase, validateContextGcCheckpoint } from '../context-gc-checkpoint.js';
11
11
  import { bindToolResultResources, cleanupUnreferencedToolResources, completeToolResultResources, copyToolResultResources, readToolResultResource, toolResultResourceFiles, writeToolResultResource } from '../tool-result-resources.js';
12
12
  import { beginSessionStorageTransaction, isUnjournaledTransactionFile, recoverSessionStorageTransaction } from '../transaction-recovery.js';
13
13
  import { extensionCheckpointFromPayload, indexExtensionCheckpoint, restoreExtensionCheckpoint, updateSessionCurrentState } from './current-state.js';
@@ -23,6 +23,7 @@ export class JsonlSessionStore {
23
23
  static #instances = new Map();
24
24
  static #opening = new Map();
25
25
  #shared;
26
+ #maintenance = new WeakMap();
26
27
  #leaseClosed = false;
27
28
  constructor(shared) {
28
29
  this.#shared = shared;
@@ -283,9 +284,22 @@ export class JsonlSessionStore {
283
284
  }
284
285
  return { directory, release };
285
286
  }
286
- /** Append the durable creation record before making this Session visible in the index. */
287
287
  async createSession(input) {
288
288
  this.#assertOpen();
289
+ const source = normalizeSessionSource(input.source);
290
+ if (source.type !== 'subagent')
291
+ return this.#createSession(input, source);
292
+ // Publish under the parent's existing serial domain so tree admission cannot miss a child.
293
+ return this.#serializeRecordOperation(source.parentSessionId, async () => {
294
+ this.#assertOpen();
295
+ if (this.#shared.writerLeases.get(source.parentSessionId)?.maintenance)
296
+ throw new MarAgentError('MAR_AGENT_SESSION_LOCKED', 'Session is under maintenance.');
297
+ await this.getSession(source.parentSessionId);
298
+ return this.#createSession(input, source);
299
+ });
300
+ }
301
+ /** Append the durable creation record before making this Session visible in the index. */
302
+ async #createSession(input, source) {
289
303
  const id = randomUUID();
290
304
  const now = new Date().toISOString();
291
305
  const meta = {
@@ -294,7 +308,7 @@ export class JsonlSessionStore {
294
308
  workspace: input.workspace,
295
309
  createdAt: now,
296
310
  lastActivityAt: now,
297
- source: normalizeSessionSource(input.source),
311
+ source,
298
312
  nextSequence: 1,
299
313
  automaticTitlePending: input.title === undefined,
300
314
  resume: {
@@ -697,9 +711,11 @@ export class JsonlSessionStore {
697
711
  }
698
712
  }
699
713
  /** Reset history with the same recovery journal while preserving the Session identity. */
700
- async clearSession(sessionId) {
714
+ async clearSession(sessionId, maintenance) {
701
715
  this.#assertOpen();
702
- const release = await this.acquireSessionLease(sessionId);
716
+ const holding = maintenance ?? (await this.acquireSessionMaintenance(sessionId, 'clear'));
717
+ this.#assertMaintenance(sessionId, 'clear', holding);
718
+ let failed = false;
703
719
  try {
704
720
  const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
705
721
  if (!meta)
@@ -726,8 +742,18 @@ export class JsonlSessionStore {
726
742
  ];
727
743
  return await this.#replaceHistory(meta, retained, { kind: 'clear', clearedAt });
728
744
  }
745
+ catch (error) {
746
+ failed = true;
747
+ throw error;
748
+ }
729
749
  finally {
730
- await release();
750
+ if (!maintenance) {
751
+ const releasing = holding.release();
752
+ if (failed)
753
+ await releasing.catch(() => undefined);
754
+ else
755
+ await releasing;
756
+ }
731
757
  }
732
758
  }
733
759
  /** Called only while rollback or clear holds the maintenance lease. */
@@ -1123,14 +1149,9 @@ export class JsonlSessionStore {
1123
1149
  records.at(-1)?.sessionId !== sessionId ||
1124
1150
  !sameContextGcBase(contextGcBaseFromRecords(records, boundarySequence), draft.base))
1125
1151
  throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC planning boundary is invalid.');
1126
- const previous = latestValidContextGcCheckpoint(records, boundarySequence, meta.workspace);
1127
- if (previous) {
1128
- const minimumUsageGrowth = contextGcMinimumUsageGrowth(draft.trigger.contextWindowTokens);
1129
- if (draft.trigger.normalRolloutOrdinal - previous.payload.trigger.normalRolloutOrdinal < 2 ||
1130
- draft.trigger.contextInputTokens - previous.payload.trigger.contextInputTokens <
1131
- minimumUsageGrowth)
1132
- throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint interval validation failed.');
1133
- }
1152
+ const previous = latestValidContextGcCheckpoint(records, boundarySequence);
1153
+ if (!contextGcCheckpointIntervalEligible(draft.trigger, previous?.payload.trigger))
1154
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint interval validation failed.');
1134
1155
  const plannedReplacements = normalizeContextGcReplacements(draft.replacements);
1135
1156
  if (!plannedReplacements || plannedReplacements.length === 0)
1136
1157
  throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint must contain a replacement.');
@@ -1236,7 +1257,7 @@ export class JsonlSessionStore {
1236
1257
  this.#assertOpen();
1237
1258
  let lease = this.#shared.writerLeases.get(sessionId);
1238
1259
  if (lease) {
1239
- if (lease.executing || owner === undefined || lease.owner !== owner)
1260
+ if (lease.maintenance || lease.executing || owner === undefined || lease.owner !== owner)
1240
1261
  throw new MarAgentError('MAR_AGENT_SESSION_LOCKED', 'Session already has a writer.');
1241
1262
  if (lease.appendFailure)
1242
1263
  throw lease.appendFailure.error;
@@ -1262,12 +1283,89 @@ export class JsonlSessionStore {
1262
1283
  const lease = this.#shared.writerLeases.get(sessionId);
1263
1284
  return lease?.owner === owner && lease.retained > 0;
1264
1285
  }
1286
+ async acquireSessionMaintenance(sessionId, kind, owner) {
1287
+ this.#assertOpen();
1288
+ const acquired = new Map();
1289
+ let sessionIds = [];
1290
+ let released = false;
1291
+ const holding = Object.freeze({
1292
+ get sessionIds() {
1293
+ return sessionIds;
1294
+ },
1295
+ release: async () => {
1296
+ if (released)
1297
+ return;
1298
+ released = true;
1299
+ this.#maintenance.delete(holding);
1300
+ let failure;
1301
+ for (const [id, writer] of [...acquired].reverse()) {
1302
+ try {
1303
+ await this.#serializeRecordOperation(id, async () => {
1304
+ delete writer.maintenance;
1305
+ await this.#releaseUnusedWriter(id, writer);
1306
+ });
1307
+ }
1308
+ catch (error) {
1309
+ failure ??= { error };
1310
+ }
1311
+ }
1312
+ if (failure)
1313
+ throw failure.error;
1314
+ }
1315
+ });
1316
+ try {
1317
+ // Re-read membership after acquiring writers; never hand the SDK a pre-lock tree snapshot.
1318
+ for (;;) {
1319
+ const tree = kind === 'delete' ? await this.sessionTree(sessionId) : [sessionId];
1320
+ for (const id of tree.toReversed()) {
1321
+ if (acquired.has(id))
1322
+ continue;
1323
+ await this.#serializeRecordOperation(id, async () => {
1324
+ this.#assertOpen();
1325
+ let writer = this.#shared.writerLeases.get(id);
1326
+ if (writer &&
1327
+ (writer.maintenance ||
1328
+ owner === undefined ||
1329
+ writer.owner !== owner ||
1330
+ (kind === 'clear' && writer.executing)))
1331
+ throw new MarAgentError('MAR_AGENT_SESSION_LOCKED', 'Session already has a writer.');
1332
+ if (!writer) {
1333
+ // Deleting a stale index entry must not recreate its missing directory.
1334
+ const release = kind === 'delete' && !(await sessionDirectoryExists(this.#sessionDirectory(id)))
1335
+ ? async () => { }
1336
+ : await this.#acquireSessionLease(id);
1337
+ writer = { owner, executing: false, retained: 0, release };
1338
+ this.#shared.writerLeases.set(id, writer);
1339
+ }
1340
+ writer.maintenance = holding;
1341
+ acquired.set(id, writer);
1342
+ });
1343
+ }
1344
+ const current = kind === 'clear' ? tree : await this.sessionTree(sessionId);
1345
+ if (current.every((id) => acquired.has(id))) {
1346
+ sessionIds = Object.freeze(current);
1347
+ break;
1348
+ }
1349
+ }
1350
+ this.#maintenance.set(holding, { sessionId, kind });
1351
+ return holding;
1352
+ }
1353
+ catch (error) {
1354
+ await holding.release().catch(() => undefined);
1355
+ throw error;
1356
+ }
1357
+ }
1358
+ #assertMaintenance(sessionId, kind, holding) {
1359
+ const current = this.#maintenance.get(holding);
1360
+ if (current?.sessionId !== sessionId || current.kind !== kind)
1361
+ throw new MarAgentError('MAR_AGENT_SESSION_LOCKED', 'Session maintenance ownership is invalid.');
1362
+ }
1265
1363
  /** Native writes join their Agent's sole writer, without admitting another Execution. */
1266
1364
  async acquireSessionWriteLease(sessionId, owner) {
1267
1365
  return this.#serializeRecordOperation(sessionId, async () => {
1268
1366
  this.#assertOpen();
1269
1367
  let lease = this.#shared.writerLeases.get(sessionId);
1270
- if (lease && lease.owner !== owner)
1368
+ if (lease && (lease.maintenance || lease.owner !== owner))
1271
1369
  throw new MarAgentError('MAR_AGENT_SESSION_LOCKED', 'Session already has a writer.');
1272
1370
  if (lease?.appendFailure)
1273
1371
  throw lease.appendFailure.error;
@@ -1291,7 +1389,7 @@ export class JsonlSessionStore {
1291
1389
  async retainSessionLease(sessionId, owner) {
1292
1390
  return this.#serializeRecordOperation(sessionId, async () => {
1293
1391
  const lease = this.#shared.writerLeases.get(sessionId);
1294
- if (!lease || !lease.executing || lease.owner !== owner)
1392
+ if (!lease || lease.maintenance || !lease.executing || lease.owner !== owner)
1295
1393
  throw new MarAgentError('MAR_AGENT_SESSION_LOCKED', 'Retained work requires its originating Session writer.');
1296
1394
  lease.retained++;
1297
1395
  let released = false;
@@ -1305,7 +1403,7 @@ export class JsonlSessionStore {
1305
1403
  });
1306
1404
  }
1307
1405
  async #releaseUnusedWriter(sessionId, lease) {
1308
- if (lease.executing || lease.retained)
1406
+ if (lease.maintenance || lease.executing || lease.retained)
1309
1407
  return;
1310
1408
  try {
1311
1409
  await lease.release();
@@ -1472,20 +1570,8 @@ export class JsonlSessionStore {
1472
1570
  else
1473
1571
  resume.reasoningEffort = executionModel.reasoningEffort;
1474
1572
  }
1475
- if (isCompactCheckpoint(record)) {
1573
+ if (isCompactCheckpoint(record))
1476
1574
  resume.compactSequence = record.sequence;
1477
- delete resume.gcSequence;
1478
- delete resume.gcGeneration;
1479
- delete resume.gcBaseSequence;
1480
- }
1481
- else {
1482
- const checkpoint = parseContextGcCheckpoint(record);
1483
- if (checkpoint) {
1484
- resume.gcSequence = record.sequence;
1485
- resume.gcGeneration = checkpoint.generation;
1486
- resume.gcBaseSequence = contextGcBaseSequence(checkpoint.base);
1487
- }
1488
- }
1489
1575
  if (record.type !== 'todo.updated' || !todoItemsFromSnapshot(record.payload))
1490
1576
  return;
1491
1577
  resume.todoSequence = record.sequence;
@@ -1723,22 +1809,12 @@ export class JsonlSessionStore {
1723
1809
  throw error;
1724
1810
  }
1725
1811
  }
1726
- async delete(sessionId) {
1812
+ async delete(sessionId, maintenance) {
1727
1813
  this.#assertOpen();
1728
- await this.#refreshIndex();
1729
- const tree = sessionTreeIds(this.#shared.index.sessions, sessionId);
1730
- if (tree.length === 0)
1731
- throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
1732
- const releases = [];
1733
- try {
1734
- for (const id of tree.toReversed())
1735
- if (await sessionDirectoryExists(join(this.#shared.root, 'sessions', id)))
1736
- releases.push(await this.acquireSessionLease(id));
1737
- }
1738
- catch (error) {
1739
- await Promise.allSettled(releases.toReversed().map((release) => release()));
1740
- throw error;
1741
- }
1814
+ const holding = maintenance ?? (await this.acquireSessionMaintenance(sessionId, 'delete'));
1815
+ this.#assertMaintenance(sessionId, 'delete', holding);
1816
+ const tree = holding.sessionIds;
1817
+ let failed = false;
1742
1818
  try {
1743
1819
  const deleted = await sessionDirectoryExists(join(this.#shared.root, 'sessions', sessionId));
1744
1820
  for (const id of tree.toReversed()) {
@@ -1751,8 +1827,18 @@ export class JsonlSessionStore {
1751
1827
  await this.#saveIndex(tree);
1752
1828
  return { deleted };
1753
1829
  }
1830
+ catch (error) {
1831
+ failed = true;
1832
+ throw error;
1833
+ }
1754
1834
  finally {
1755
- await Promise.allSettled(releases.toReversed().map((release) => release()));
1835
+ if (!maintenance) {
1836
+ const releasing = holding.release();
1837
+ if (failed)
1838
+ await releasing.catch(() => undefined);
1839
+ else
1840
+ await releasing;
1841
+ }
1756
1842
  }
1757
1843
  }
1758
1844
  #assertOpen() {
@@ -58,9 +58,6 @@ export interface SessionMeta {
58
58
  version: 1;
59
59
  userTurns?: number;
60
60
  compactSequence?: number;
61
- gcSequence?: number;
62
- gcGeneration?: number;
63
- gcBaseSequence?: number;
64
61
  todoSequence?: number;
65
62
  todoRevision: number;
66
63
  executionModelIndexed?: true;
package/dist/index.d.ts CHANGED
@@ -41,7 +41,7 @@ export type { CoreProcessesApi, CoreProcessesBackend, CoreProcessInput, CoreProc
41
41
  export type { CreateMarAgentOptions, CreateSessionInput, ExecutionHandle, CompactInput, SteerInput, SteerResult, MarAgent, MarAgentSession, SessionTitleUpdateInput } from './agent-core/sdk/types.js';
42
42
  export { openMarAgentSessionCatalog, type MarAgentContextUsage, type MarAgentHistoryEntry, type MarAgentHistoryPage, type MarAgentSessionCatalog, type MarAgentSessionDeleteResult, type MarAgentSessionImage, type MarAgentSessionPage, type MarAgentSessionSummary } from './agent-core/session/catalog.js';
43
43
  export type { SessionListInput, SessionSource } from './agent-core/session/types.js';
44
- export type { SessionStore, SessionStoreSession, SessionRecordInput, SessionStoreCompactInput, SessionRecord, SessionExecutionState, CompactHistoryMessage, CompactUserInput, SessionInputImage, SessionInputImageMetadata, SessionInputImageData, RestoredSessionInputImage, CodeModeStateEntry, TodoSnapshot, TodoItem, CodeToolCallRecord, CodeObservation, ModelMessage, ModelUsage, ModelInputMetadata, ModelTextBlock, ModelContextualBlock, EnvironmentContextState, ContextGcCheckpointDraft, ContextGcBase, ContextGcPlatform, ContextGcEconomicsPolicy, ContextGcMetrics, ContextGcReplacement, ContextGcTarget, ContextGcReadItemReplacement, ContextGcWebToolName, ContextGcSubagentToolName, AvailableToolResultResource } from './agent-core/session/store/contracts.js';
44
+ export type { SessionStore, SessionStoreMaintenance, SessionStoreSession, SessionRecordInput, SessionStoreCompactInput, SessionRecord, SessionExecutionState, CompactHistoryMessage, CompactUserInput, SessionInputImage, SessionInputImageMetadata, SessionInputImageData, RestoredSessionInputImage, CodeModeStateEntry, TodoSnapshot, TodoItem, CodeToolCallRecord, CodeObservation, ModelMessage, ModelUsage, ModelInputMetadata, ModelTextBlock, ModelContextualBlock, EnvironmentContextState, ContextGcCheckpointDraft, ContextGcBase, ContextGcPlatform, ContextGcEconomicsPolicy, ContextGcMetrics, ContextGcReplacement, ContextGcTarget, ContextGcReadItemReplacement, ContextGcWebToolName, ContextGcSubagentToolName, AvailableToolResultResource } from './agent-core/session/store/contracts.js';
45
45
  export type { OpenMarAgentSessionCatalogInput } from './agent-core/session/catalog.js';
46
46
  export type { ApplyPatchFileResult, ApplyPatchResult, ApplyPatchPartialResult, ClientToolDefinition, MarAgentToolArtifact, MarAgentToolImageArtifact, McpCatalogTool, McpToolCallContext, McpToolCatalog, McpToolCatalogContext, ToolCall, ToolCaller, ToolExecutionContext, ToolExecutionResult } from './agent-core/tools/contracts.js';
47
47
  export { MCP_DEFINITION_LIMITS, assertMcpToolCatalog, assertMcpToolDefinition } from './agent-core/tools/mcp/catalog.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.11.14",
3
+ "version": "0.11.15",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",