@myagentroam/agent 0.9.163 → 0.9.165

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.
@@ -4,9 +4,11 @@ export interface LocalProcessSpawnRequest {
4
4
  readonly args?: readonly string[];
5
5
  readonly cwd: string;
6
6
  readonly shell?: boolean | string;
7
+ /** Shell startup for exec only; native processes and workers omit this field. */
8
+ readonly shellLogin?: boolean;
7
9
  readonly environment: NodeJS.ProcessEnv;
8
10
  /** Explicit per-start values take precedence over execution-bound Host environment. */
9
- readonly environmentOverrides?: Readonly<Record<string, string>>;
11
+ readonly environmentOverrides?: Readonly<Record<string, string | undefined>>;
10
12
  readonly environmentKeys?: readonly string[];
11
13
  /** Trusted, bounded read-only worker assets; never supplied by model tool input. */
12
14
  readonly runtimeDirectories?: readonly string[];
@@ -1,7 +1,9 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { prepareLocalShellProcess } from './local-shell.js';
2
3
  /** Preserves the direct local execution behavior used by the CLI and normal Node Workbench. */
3
4
  export const directLocalProcessExecutor = {
4
5
  async spawn(request) {
6
+ request = prepareLocalShellProcess(request);
5
7
  return spawn(request.command, [...(request.args ?? [])], {
6
8
  cwd: request.cwd,
7
9
  shell: request.shell ?? false,
@@ -50,12 +50,22 @@ export class LocalSessionEnvironment {
50
50
  (this.platform !== 'win32' || request.environmentOverrides === undefined))
51
51
  return request;
52
52
  const environment = { ...request.environment };
53
+ const overrides = {};
53
54
  const assign = (key, value) => {
54
55
  if (this.platform === 'win32') {
55
56
  for (const current of Object.keys(environment))
56
57
  if (this.#key(current) === this.#key(key))
57
58
  delete environment[current];
59
+ for (const current of Object.keys(overrides))
60
+ if (this.#key(current) === this.#key(key))
61
+ delete overrides[current];
58
62
  }
63
+ Object.defineProperty(overrides, key, {
64
+ value,
65
+ enumerable: true,
66
+ writable: true,
67
+ configurable: true
68
+ });
59
69
  if (value === undefined)
60
70
  delete environment[key];
61
71
  else
@@ -75,6 +85,7 @@ export class LocalSessionEnvironment {
75
85
  return {
76
86
  ...request,
77
87
  environment,
88
+ environmentOverrides: overrides,
78
89
  ...(request.environmentKeys === undefined
79
90
  ? {}
80
91
  : {
@@ -0,0 +1,3 @@
1
+ import type { LocalProcessSpawnRequest } from './local-process-executor.js';
2
+ /** Resolve Shell startup in the executor, after its environment policy has been applied. */
3
+ export declare function prepareLocalShellProcess(request: LocalProcessSpawnRequest, platform?: NodeJS.Platform): LocalProcessSpawnRequest;
@@ -0,0 +1,78 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { basename, win32 } from 'node:path';
3
+ /** Resolve Shell startup in the executor, after its environment policy has been applied. */
4
+ export function prepareLocalShellProcess(request, platform = process.platform) {
5
+ if (!request.shell || request.shellLogin === undefined)
6
+ return request;
7
+ const shell = typeof request.shell === 'string'
8
+ ? request.shell
9
+ : platform === 'win32'
10
+ ? (request.environment.ComSpec ?? request.environment.COMSPEC ?? 'cmd.exe')
11
+ : '/bin/sh';
12
+ const name = (platform === 'win32' ? win32.basename(shell) : basename(shell))
13
+ .replace(/\.exe$/iu, '')
14
+ .toLowerCase();
15
+ const powershell = name === 'pwsh' || name === 'powershell';
16
+ if (!powershell && !['bash', 'zsh', 'sh'].includes(name))
17
+ return request;
18
+ const login = request.shellLogin;
19
+ const environment = { ...request.environment };
20
+ const script = [];
21
+ if (login && name === 'bash') {
22
+ script.push('if [ -z "${BASH_ENV-}" ] && [ -n "${HOME-}" ] && [ -r "$HOME/.bashrc" ]; then', ' . "$HOME/.bashrc"', 'fi');
23
+ }
24
+ else if (login && name === 'zsh') {
25
+ script.push('if [[ -n "${ZDOTDIR-}" ]]; then', ' [[ ! -r "$ZDOTDIR/.zshrc" ]] || . "$ZDOTDIR/.zshrc"', 'elif [[ -n "${HOME-}" ]]; then', ' [[ ! -r "$HOME/.zshrc" ]] || . "$HOME/.zshrc"', 'fi');
26
+ }
27
+ // Keep values out of argv and tool-visible command history, including quoting-sensitive values.
28
+ const privatePrefix = `__MAR_SHELL_${randomUUID().replaceAll('-', '')}_`;
29
+ const restoration = [];
30
+ let index = 0;
31
+ for (const [key, value] of Object.entries(request.environmentOverrides ?? {})) {
32
+ if (!powershell && !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key))
33
+ continue;
34
+ const quotedKey = `'${key.replaceAll("'", "''")}'`;
35
+ if (value === undefined) {
36
+ restoration.push(powershell
37
+ ? `[Environment]::SetEnvironmentVariable(${quotedKey}, $null, 'Process')`
38
+ : `if [ "\${${key}+x}" = x ]; then unset ${key} || exit; fi`);
39
+ continue;
40
+ }
41
+ const privateKey = `${privatePrefix}${index++}`;
42
+ environment[privateKey] = value;
43
+ restoration.push(powershell
44
+ ? `if (-not (Test-Path Env:${privateKey})) { throw 'Shell startup removed a protected environment value.' }`
45
+ : `[ "\${${privateKey}+x}" = x ] || { printf '%s\\n' 'Shell startup removed a protected environment value.' >&2; exit 1; }`);
46
+ restoration.push(powershell
47
+ ? `[Environment]::SetEnvironmentVariable(${quotedKey}, $env:${privateKey}, 'Process'); Remove-Item Env:${privateKey}`
48
+ : `if [ "\${${key}+x}" != x ] || [ "\${${key}-}" != "\$${privateKey}" ]; then export ${key}="\$${privateKey}" || exit; else export ${key} || exit; fi; unset ${privateKey}`);
49
+ }
50
+ if (restoration.length && !powershell) {
51
+ const traceKey = `${privatePrefix}TRACE`;
52
+ // A profile may enable xtrace; do not print expanded private values during restoration.
53
+ script.push(`case $- in *x*) set +x; ${traceKey}=1 ;; *) ${traceKey}= ;; esac`, ...restoration, `if [ "\$${traceKey}" = 1 ]; then unset ${traceKey}; set -x; else unset ${traceKey}; fi`);
54
+ }
55
+ else {
56
+ script.push(...restoration);
57
+ }
58
+ script.push(request.command);
59
+ return {
60
+ ...request,
61
+ command: shell,
62
+ args: [
63
+ ...(powershell ? [...(login ? [] : ['-NoProfile']), '-Command'] : [login ? '-lc' : '-c']),
64
+ script.join('\n'),
65
+ ...(request.args ?? [])
66
+ ],
67
+ shell: false,
68
+ environment,
69
+ ...(request.environmentKeys === undefined
70
+ ? {}
71
+ : {
72
+ environmentKeys: [
73
+ ...request.environmentKeys,
74
+ ...Object.keys(environment).filter((key) => key.startsWith(privatePrefix))
75
+ ]
76
+ })
77
+ };
78
+ }
@@ -1,5 +1,3 @@
1
- import { countModelTokens } from '../token-budget.js';
2
- import { renderObservations } from '../../tools/code-mode/observations.js';
3
1
  import { codeCallEvidence, codeObservationEvidence, retainedCodeInputs } from '../../session/context-gc-observations.js';
4
2
  import { ContextGcCheckpointCache, contextGcBaseFromRecords, contextGcLogicalPathKey, contextGcMinimumUsageGrowth, contextGcRecordSourceHash, contextGcTargetKey, normalizeContextGcReplacements } from '../../session/context-gc-checkpoint.js';
5
3
  import { codeCellCompletionSequence, contextGcApplyPatchInputReplacement, contextGcCodeModeInputReplacement, contextGcCodeModeOutputReplacement, contextGcExecInputReplacement, contextGcExecOutputReplacement, contextGcPatchResultFromCompletedPayload, contextGcStaleReadReplacement, contextGcWebOutputReplacement, normalizeContextGcCodeModeInput, normalizeContextGcExecResult } from '../../session/context-gc-replacements.js';
@@ -30,8 +28,6 @@ const MINIMUM_CONTEXT_WINDOW_REDUCTION_RATIO = 0.05;
30
28
  const COMPACT_CREDIT_ACTIVATION_RATIO = 0.8;
31
29
  const COMPACT_THRESHOLD_RATIO = 0.9;
32
30
  const IMMINENT_COMPACT_CREDIT_DISCOUNT = 0.8;
33
- const LARGE_TERMINAL_OUTPUT_TOKENS = 4_096;
34
- const HIGH_WATER_LARGE_TERMINAL_OUTPUT_TOKENS = 2_048;
35
31
  export function evaluateContextGcEconomics(input) {
36
32
  const reclaimedTokens = Math.max(0, input.reclaimedTokens);
37
33
  const cacheReadTokens = Math.max(0, input.cacheReadTokens);
@@ -150,14 +146,15 @@ export function inspectContextGc(input) {
150
146
  return {};
151
147
  const groups = buildToolGroups(input.records, input.messages, input.messageSources);
152
148
  const existing = previous?.payload.replacements ?? [];
149
+ const candidateSavings = createCandidateSavingsEstimator(input, existing);
153
150
  const existingTargets = new Set(existing.flatMap((replacement) => [
154
151
  contextGcTargetKey(replacement.target),
155
152
  ...(replacement.kind === 'TOOL_CALL_INPUT'
156
153
  ? replacement.providerAliasTargets.map(contextGcTargetKey)
157
154
  : [])
158
155
  ]));
159
- const stageA = collectStageACandidates(input, groups, existing, existingTargets);
160
- const subagent = collectSubagentCandidates(input, groups, existingTargets);
156
+ const stageA = collectStageACandidates(input, groups, existing, existingTargets, candidateSavings);
157
+ const subagent = collectSubagentCandidates(input, groups, existingTargets, candidateSavings);
161
158
  const deterministicCandidates = [...stageA.eligible, ...subagent.eligible];
162
159
  const deterministicProtectedTokens = stageA.protectedTokens + subagent.protectedTokens;
163
160
  const stageAPlan = evaluateCandidateSet(input, existing, deterministicCandidates, {
@@ -169,9 +166,9 @@ export function inspectContextGc(input) {
169
166
  protectedCandidateTokens: deterministicProtectedTokens
170
167
  });
171
168
  if (stageAPlan?.economics.passes) {
172
- const exec = collectExecCandidates(input, groups, existingTargets, usageRatio);
173
- const codeMode = collectCodeModeCandidates(input, groups, existingTargets, usageRatio);
174
- const web = collectWebCandidates(input, groups, existingTargets, usageRatio);
169
+ const exec = collectExecCandidates(input, groups, existingTargets, candidateSavings);
170
+ const codeMode = collectCodeModeCandidates(input, groups, existingTargets, candidateSavings);
171
+ const web = collectWebCandidates(input, groups, existingTargets, candidateSavings);
175
172
  const protectedCandidateTokens = deterministicProtectedTokens +
176
173
  exec.protectedTokens +
177
174
  codeMode.protectedTokens +
@@ -202,9 +199,9 @@ export function inspectContextGc(input) {
202
199
  }
203
200
  : {};
204
201
  }
205
- const exec = collectExecCandidates(input, groups, existingTargets, usageRatio);
206
- const codeMode = collectCodeModeCandidates(input, groups, existingTargets, usageRatio);
207
- const web = collectWebCandidates(input, groups, existingTargets, usageRatio);
202
+ const exec = collectExecCandidates(input, groups, existingTargets, candidateSavings);
203
+ const codeMode = collectCodeModeCandidates(input, groups, existingTargets, candidateSavings);
204
+ const web = collectWebCandidates(input, groups, existingTargets, candidateSavings);
208
205
  const combined = evaluateCandidateSet(input, existing, [...deterministicCandidates, ...exec.eligible, ...codeMode.eligible, ...web.eligible], {
209
206
  stage: 'STAGE_A_AND_EXEC',
210
207
  contextUsageTokens,
@@ -230,7 +227,7 @@ export function inspectContextGc(input) {
230
227
  ? { retryAfterRollouts }
231
228
  : {};
232
229
  }
233
- function collectCodeModeCandidates(input, groups, existingTargets, usageRatio) {
230
+ function collectCodeModeCandidates(input, groups, existingTargets, candidateSavings) {
234
231
  const eligible = [];
235
232
  let protectedTokens = 0;
236
233
  let retryAfterRollouts = Infinity;
@@ -251,19 +248,8 @@ function collectCodeModeCandidates(input, groups, existingTargets, usageRatio) {
251
248
  !isRecord(completedPayload) ||
252
249
  typeof completedPayload.truncated !== 'boolean')
253
250
  continue;
254
- const visibleTokens = contextGcMessageTokens(input, [
255
- group.call.message,
256
- group.result.message,
257
- ...group.aliases.map((alias) => alias.message)
258
- ]);
259
- const minimumVisibleTokens = usageRatio >= COMPACT_CREDIT_ACTIVATION_RATIO
260
- ? HIGH_WATER_LARGE_TERMINAL_OUTPUT_TOKENS
261
- : LARGE_TERMINAL_OUTPUT_TOKENS;
262
- if (visibleTokens < minimumVisibleTokens)
263
- continue;
264
251
  for (const observation of group.result.message.observations ?? []) {
265
- if (observation.type !== 'value' ||
266
- countModelTokens(observation.value) < minimumVisibleTokens)
252
+ if (observation.type !== 'value')
267
253
  continue;
268
254
  const target = targetFor(group.result.record, {
269
255
  ...group.result.message,
@@ -285,10 +271,13 @@ function collectCodeModeCandidates(input, groups, existingTargets, usageRatio) {
285
271
  observedRollouts: successfulRolloutsAfter(input.records, Math.max(evidenceSequence, completionSequence), input.modelId, input.contextWindowTokens),
286
272
  evidenceSequence
287
273
  };
274
+ const reclaimedTokens = candidateSavings(candidate);
275
+ if (reclaimedTokens <= 0)
276
+ continue;
288
277
  if (candidate.observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts)
289
278
  eligible.push(candidate);
290
279
  else {
291
- protectedTokens += candidateTokens(input, candidate);
280
+ protectedTokens += reclaimedTokens;
292
281
  retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.largeExecProtectionRollouts - candidate.observedRollouts);
293
282
  }
294
283
  }
@@ -323,10 +312,13 @@ function collectCodeModeCandidates(input, groups, existingTargets, usageRatio) {
323
312
  continue;
324
313
  const observedRollouts = successfulRolloutsAfter(input.records, Math.max(evidenceSequence, completionSequence), input.modelId, input.contextWindowTokens);
325
314
  const candidate = { replacements, observedRollouts, evidenceSequence };
315
+ const reclaimedTokens = candidateSavings(candidate);
316
+ if (reclaimedTokens <= 0)
317
+ continue;
326
318
  if (observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts)
327
319
  eligible.push(candidate);
328
320
  else {
329
- protectedTokens += candidateTokens(input, candidate);
321
+ protectedTokens += reclaimedTokens;
330
322
  retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.largeExecProtectionRollouts - observedRollouts);
331
323
  }
332
324
  }
@@ -528,7 +520,7 @@ function evaluateCandidateSet(input, existing, candidates, state) {
528
520
  eligibleCandidateCount: candidates.length
529
521
  };
530
522
  }
531
- function collectStageACandidates(input, groups, existingReplacements, existingTargets) {
523
+ function collectStageACandidates(input, groups, existingReplacements, existingTargets, candidateSavings) {
532
524
  const eligible = [];
533
525
  let protectedTokens = 0;
534
526
  let retryAfterRollouts = Infinity;
@@ -555,6 +547,7 @@ function collectStageACandidates(input, groups, existingReplacements, existingTa
555
547
  const patchCall = patch.call;
556
548
  const observedRollouts = successfulRolloutsAfter(input.records, evidenceSequence, input.modelId, input.contextWindowTokens);
557
549
  const replacements = [];
550
+ const candidateReadItems = new Set();
558
551
  const patchTarget = targetFor(patchCall.record, patchCall.message);
559
552
  if (patchTarget && !existingTargets.has(contextGcTargetKey(patchTarget)))
560
553
  replacements.push({
@@ -578,14 +571,14 @@ function collectStageACandidates(input, groups, existingReplacements, existingTa
578
571
  if (item.pathTruncated)
579
572
  return [];
580
573
  const itemKey = `${read.result.record.recordId}:${read.result.message.observationId ?? ''}:${item.index}`;
581
- if (claimedReadItems.has(itemKey))
574
+ if (claimedReadItems.has(itemKey) || candidateReadItems.has(itemKey))
582
575
  return [];
583
576
  const pathKey = contextGcLogicalPathKey(item.path, input.workspace, input.platform);
584
577
  const matched = result.files.find((file) => file.operation !== 'added' &&
585
578
  contextGcLogicalPathKey(file.path, input.workspace, input.platform) === pathKey);
586
579
  if (!pathKey || !matched || matched.operation === 'added')
587
580
  return [];
588
- claimedReadItems.add(itemKey);
581
+ candidateReadItems.add(itemKey);
589
582
  return [
590
583
  {
591
584
  index: item.index,
@@ -625,16 +618,21 @@ function collectStageACandidates(input, groups, existingReplacements, existingTa
625
618
  observedRollouts,
626
619
  evidenceSequence
627
620
  };
621
+ const reclaimedTokens = candidateSavings(candidate);
622
+ if (reclaimedTokens <= 0)
623
+ continue;
624
+ for (const itemKey of candidateReadItems)
625
+ claimedReadItems.add(itemKey);
628
626
  if (observedRollouts >= CONTEXT_GC_POLICY.stageAProtectionRollouts)
629
627
  eligible.push(candidate);
630
628
  else {
631
- protectedTokens += candidateTokens(input, candidate);
629
+ protectedTokens += reclaimedTokens;
632
630
  retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.stageAProtectionRollouts - observedRollouts);
633
631
  }
634
632
  }
635
633
  return { eligible, protectedTokens, retryAfterRollouts };
636
634
  }
637
- function collectSubagentCandidates(input, groups, existingTargets) {
635
+ function collectSubagentCandidates(input, groups, existingTargets, candidateSavings) {
638
636
  const eligible = [];
639
637
  let protectedTokens = 0;
640
638
  let retryAfterRollouts = Infinity;
@@ -714,16 +712,19 @@ function collectSubagentCandidates(input, groups, existingTargets) {
714
712
  observedRollouts,
715
713
  evidenceSequence
716
714
  };
715
+ const reclaimedTokens = candidateSavings(candidate);
716
+ if (reclaimedTokens <= 0)
717
+ continue;
717
718
  if (observedRollouts >= CONTEXT_GC_POLICY.stageAProtectionRollouts)
718
719
  eligible.push(candidate);
719
720
  else {
720
- protectedTokens += candidateTokens(input, candidate);
721
+ protectedTokens += reclaimedTokens;
721
722
  retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.stageAProtectionRollouts - observedRollouts);
722
723
  }
723
724
  }
724
725
  return { eligible, protectedTokens, retryAfterRollouts };
725
726
  }
726
- function collectExecCandidates(input, groups, existingTargets, usageRatio) {
727
+ function collectExecCandidates(input, groups, existingTargets, candidateSavings) {
727
728
  const eligible = [];
728
729
  let protectedTokens = 0;
729
730
  let retryAfterRollouts = Infinity;
@@ -797,18 +798,6 @@ function collectExecCandidates(input, groups, existingTargets, usageRatio) {
797
798
  if (!terminal || (processId && input.activeProcessIds.has(processId)))
798
799
  continue;
799
800
  const final = lifecycle.at(-1);
800
- const visibleTokens = contextGcMessageTokens(input, lifecycle
801
- .filter((invocation) => !invocation.group.evidenceOnly)
802
- .flatMap((invocation) => [
803
- invocation.group.call.message,
804
- invocation.group.result.message,
805
- ...invocation.group.aliases.map((alias) => alias.message)
806
- ]));
807
- const minimumVisibleTokens = usageRatio >= COMPACT_CREDIT_ACTIVATION_RATIO
808
- ? HIGH_WATER_LARGE_TERMINAL_OUTPUT_TOKENS
809
- : LARGE_TERMINAL_OUTPUT_TOKENS;
810
- if (visibleTokens < minimumVisibleTokens)
811
- continue;
812
801
  const evidenceSequence = final.group.result.record.sequence;
813
802
  const observedRollouts = successfulRolloutsAfter(input.records, evidenceSequence, input.modelId, input.contextWindowTokens);
814
803
  const replacements = [];
@@ -850,22 +839,22 @@ function collectExecCandidates(input, groups, existingTargets, usageRatio) {
850
839
  observedRollouts,
851
840
  evidenceSequence
852
841
  };
842
+ const reclaimedTokens = candidateSavings(candidate);
843
+ if (reclaimedTokens <= 0)
844
+ continue;
853
845
  if (observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts)
854
846
  eligible.push(candidate);
855
847
  else {
856
- protectedTokens += candidateTokens(input, candidate);
848
+ protectedTokens += reclaimedTokens;
857
849
  retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.largeExecProtectionRollouts - observedRollouts);
858
850
  }
859
851
  }
860
852
  return { eligible, protectedTokens, retryAfterRollouts };
861
853
  }
862
- function collectWebCandidates(input, groups, existingTargets, usageRatio) {
854
+ function collectWebCandidates(input, groups, existingTargets, candidateSavings) {
863
855
  const eligible = [];
864
856
  let protectedTokens = 0;
865
857
  let retryAfterRollouts = Infinity;
866
- const minimumVisibleTokens = usageRatio >= COMPACT_CREDIT_ACTIVATION_RATIO
867
- ? HIGH_WATER_LARGE_TERMINAL_OUTPUT_TOKENS
868
- : LARGE_TERMINAL_OUTPUT_TOKENS;
869
858
  for (const group of [...groups.values()].sort((left, right) => (left.result?.record.sequence ?? Number.MAX_SAFE_INTEGER) -
870
859
  (right.result?.record.sequence ?? Number.MAX_SAFE_INTEGER))) {
871
860
  if (!isContextGcWebToolName(group.toolName) ||
@@ -885,9 +874,6 @@ function collectWebCandidates(input, groups, existingTargets, usageRatio) {
885
874
  const target = targetFor(group.result.record, group.result.message);
886
875
  if (!target || existingTargets.has(contextGcTargetKey(target)))
887
876
  continue;
888
- const visibleTokens = contextGcMessageTokens(input, [group.result.message]);
889
- if (visibleTokens < minimumVisibleTokens)
890
- continue;
891
877
  const evidenceSequence = group.result.record.sequence;
892
878
  const observedRollouts = successfulRolloutsAfter(input.records, evidenceSequence, input.modelId, input.contextWindowTokens);
893
879
  const candidate = {
@@ -904,10 +890,13 @@ function collectWebCandidates(input, groups, existingTargets, usageRatio) {
904
890
  observedRollouts,
905
891
  evidenceSequence
906
892
  };
893
+ const reclaimedTokens = candidateSavings(candidate);
894
+ if (reclaimedTokens <= 0)
895
+ continue;
907
896
  if (observedRollouts >= CONTEXT_GC_POLICY.largeExecProtectionRollouts)
908
897
  eligible.push(candidate);
909
898
  else {
910
- protectedTokens += candidateTokens(input, candidate);
899
+ protectedTokens += reclaimedTokens;
911
900
  retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_POLICY.largeExecProtectionRollouts - observedRollouts);
912
901
  }
913
902
  }
@@ -1230,23 +1219,46 @@ function firstMatchingUsageAfter(records, sequence, modelId, contextWindowTokens
1230
1219
  }
1231
1220
  return undefined;
1232
1221
  }
1233
- function candidateTokens(input, candidate) {
1234
- const targets = new Set(candidate.replacements.flatMap((replacement) => [
1235
- contextGcTargetKey(replacement.target),
1236
- ...(replacement.kind === 'TOOL_CALL_INPUT'
1237
- ? replacement.providerAliasTargets.map(contextGcTargetKey)
1238
- : [])
1239
- ]));
1240
- return contextGcMessageTokens(input, input.messages.flatMap((message, index) => {
1241
- const target = targetFor(input.messageSources[index], message);
1242
- if (target && targets.has(contextGcTargetKey(target)))
1243
- return [message];
1244
- const selected = target &&
1245
- message.observations?.filter((value) => targets.has(contextGcTargetKey({ ...target, observationId: value.id })));
1246
- return selected?.length
1247
- ? [{ ...message, observations: selected, content: renderObservations(selected) }]
1248
- : [];
1249
- }));
1222
+ /** Measure only affected call families, retaining native aliases for provider lowering. */
1223
+ function createCandidateSavingsEstimator(input, existing) {
1224
+ const byCall = new Map();
1225
+ const byRecord = new Map();
1226
+ for (const [index, message] of input.messages.entries()) {
1227
+ const callId = message.role === 'provider' ? providerToolCallId(message) : message.callId;
1228
+ if (callId) {
1229
+ const indices = byCall.get(callId) ?? [];
1230
+ indices.push(index);
1231
+ byCall.set(callId, indices);
1232
+ }
1233
+ const recordId = input.messageSources[index].recordId;
1234
+ const indices = byRecord.get(recordId) ?? [];
1235
+ indices.push(index);
1236
+ byRecord.set(recordId, indices);
1237
+ }
1238
+ return (candidate) => {
1239
+ const indices = new Set();
1240
+ for (const replacement of candidate.replacements) {
1241
+ const targets = [
1242
+ replacement.target,
1243
+ ...(replacement.kind === 'TOOL_CALL_INPUT' ? replacement.providerAliasTargets : [])
1244
+ ];
1245
+ for (const target of targets) {
1246
+ for (const index of byRecord.get(target.recordId) ?? [])
1247
+ indices.add(index);
1248
+ if (target.callId)
1249
+ for (const index of byCall.get(target.callId) ?? [])
1250
+ indices.add(index);
1251
+ }
1252
+ }
1253
+ const ordered = [...indices].sort((a, b) => a - b);
1254
+ const messages = ordered.map((index) => input.messages[index]);
1255
+ const sources = ordered.map((index) => input.messageSources[index]);
1256
+ const replacements = normalizeContextGcReplacements([...existing, ...candidate.replacements]);
1257
+ if (!replacements)
1258
+ return 0;
1259
+ const projected = applyContextGcReplacementsToMessages(messages, sources, replacements);
1260
+ return contextGcReclaimedTokens(input, messages, projected.messages);
1261
+ };
1250
1262
  }
1251
1263
  function targetFor(record, message) {
1252
1264
  if (record.type !== 'model.context' ||
@@ -272,7 +272,9 @@ function targetFromMessage(record, message) {
272
272
  };
273
273
  }
274
274
  function targetMatchesReplacement(left, right) {
275
- return (contextGcTargetKey(left) === contextGcTargetKey(right) && left.sourceHash === right.sourceHash);
275
+ return (contextGcTargetKey(left) === contextGcTargetKey(right) &&
276
+ left.sourceHash === right.sourceHash &&
277
+ (right.toolName === undefined || right.toolName === left.toolName));
276
278
  }
277
279
  function cloneMessage(message) {
278
280
  return structuredClone(message);
@@ -58,7 +58,8 @@ export function contextGcLogicalPathKey(path, workspace, platform) {
58
58
  return resolved;
59
59
  }
60
60
  export function contextGcTargetKey(target) {
61
- return `${target.sequence}:${target.recordId}:${target.role}:${target.callId ?? ''}:${target.toolName ?? ''}${target.observationId ? `:${target.observationId}` : ''}`;
61
+ // Tool outputs are identified by their record/call and observation, not optional tool-name metadata.
62
+ return `${target.sequence}:${target.recordId}:${target.role}:${target.callId ?? ''}:${target.role === 'tool' ? '' : (target.toolName ?? '')}${target.observationId ? `:${target.observationId}` : ''}`;
62
63
  }
63
64
  export function contextGcReplacementKey(replacement) {
64
65
  return contextGcTargetKey(replacement.target);
@@ -2,7 +2,7 @@ export const todoReadToolDefinition = {
2
2
  name: 'todo_read',
3
3
  allowedCallers: ['direct', 'programmatic'],
4
4
  parallelSafety: 'safe',
5
- description: 'Read this session’s current Todo revision and items. Use before updating when the revision may have changed. Do not read an empty or new Todo merely to decide whether straightforward bounded work needs one.',
5
+ description: 'Read this session’s current Todo revision and items when you need the existing list. A prior read is not required to write Todo. Do not read an empty or new Todo merely to decide whether straightforward bounded work needs one.',
6
6
  inputSchema: { type: 'object', properties: {}, additionalProperties: false }
7
7
  };
8
8
  export function executeTodoRead(context) {
@@ -4,11 +4,10 @@ export const todoWriteToolDefinition = {
4
4
  name: 'todo_write',
5
5
  allowedCallers: ['direct', 'programmatic'],
6
6
  parallelSafety: 'serial',
7
- description: 'Replace this session’s entire Todo list using optimistic expectedRevision. Preserve stable IDs and statuses only when extending the same objective. For a materially different objective, include only the new objective’s items. Do not retain completed items from an earlier objective; when all current items are completed, default a substantive new request to replacement unless the user explicitly asks to continue, extend, verify, or follow up. Use only after Todo has been chosen for coordination-heavy work; straightforward bounded work does not need it. Keep at most one in_progress item and update statuses after meaningful stage changes. When another substantive tool call is already ready and does not depend on the Todo result, include both in the same model response instead of spending a response only for Todo bookkeeping.',
7
+ description: 'Replace this session’s entire Todo list with the supplied items; no prior read is required. Preserve stable IDs and statuses only when extending the same objective. For a materially different objective, include only the new objective’s items. Do not retain completed items from an earlier objective; when all current items are completed, default a substantive new request to replacement unless the user explicitly asks to continue, extend, verify, or follow up. Use only after Todo has been chosen for coordination-heavy work; straightforward bounded work does not need it. Keep at most one in_progress item and update statuses after meaningful stage changes. When another substantive tool call is already ready and does not depend on the Todo result, include both in the same model response instead of spending a response only for Todo bookkeeping.',
8
8
  inputSchema: {
9
9
  type: 'object',
10
10
  properties: {
11
- expectedRevision: { type: 'integer' },
12
11
  items: {
13
12
  type: 'array',
14
13
  maxItems: TOOL_EXECUTION_LIMITS.todoMaxItems,
@@ -24,7 +23,7 @@ export const todoWriteToolDefinition = {
24
23
  }
25
24
  }
26
25
  },
27
- required: ['expectedRevision', 'items'],
26
+ required: ['items'],
28
27
  additionalProperties: false
29
28
  }
30
29
  };
@@ -38,9 +37,7 @@ function parseTodoWriteInput(value) {
38
37
  if (!value || typeof value !== 'object' || Array.isArray(value))
39
38
  return invalid();
40
39
  const input = value;
41
- if (Object.keys(input).some((key) => !['expectedRevision', 'items'].includes(key)) ||
42
- !Number.isInteger(input.expectedRevision) ||
43
- input.expectedRevision < 0 ||
40
+ if (Object.keys(input).some((key) => key !== 'items') ||
44
41
  !Array.isArray(input.items) ||
45
42
  input.items.length > TOOL_EXECUTION_LIMITS.todoMaxItems)
46
43
  return invalid();
@@ -59,7 +56,7 @@ function parseTodoWriteInput(value) {
59
56
  return invalid();
60
57
  return { id: record.id, step: record.step, status: record.status };
61
58
  });
62
- return { expectedRevision: input.expectedRevision, items };
59
+ return { items };
63
60
  }
64
61
  function invalid() {
65
62
  throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Todo input is invalid.');
@@ -12,7 +12,6 @@ export declare class TodoStore {
12
12
  }[];
13
13
  };
14
14
  write(input: {
15
- expectedRevision: number;
16
15
  items: readonly TodoItem[];
17
16
  }): {
18
17
  revision: number;
@@ -14,10 +14,6 @@ export class TodoStore {
14
14
  return { revision: this.#revision, items: this.#items.map((item) => ({ ...item })) };
15
15
  }
16
16
  write(input) {
17
- if (input.expectedRevision !== this.#revision)
18
- throw new MarAgentError('MAR_AGENT_TODO_CONFLICT', 'Todo revision is stale.', {
19
- details: { revision: this.#revision }
20
- });
21
17
  this.#items = normalizeTodoItems(input.items);
22
18
  this.#revision++;
23
19
  return this.read();
@@ -44,10 +40,7 @@ export function restoreTodoStore(snapshots) {
44
40
  const items = todoItemsFromSnapshot(snapshot);
45
41
  if (!items)
46
42
  continue;
47
- todo.write({
48
- expectedRevision: todo.read().revision,
49
- items
50
- });
43
+ todo.write({ items });
51
44
  }
52
45
  return todo;
53
46
  }
@@ -9,6 +9,7 @@ export type ExecInput = {
9
9
  action?: 'start';
10
10
  command: string;
11
11
  cwd?: string;
12
+ login?: boolean;
12
13
  timeoutMs?: number;
13
14
  yieldMs?: number;
14
15
  } | {
@@ -53,6 +53,11 @@ export const execToolDefinition = {
53
53
  type: 'string',
54
54
  description: 'Optional relative/absolute start directory; defaults to workspace. Command-internal relative paths resolve from this directory.'
55
55
  },
56
+ login: {
57
+ type: 'boolean',
58
+ default: true,
59
+ description: 'Use the Shell login/profile initialization. Defaults to true; false skips login startup. Does not force an interactive Shell.'
60
+ },
56
61
  timeoutMs: {
57
62
  type: 'integer',
58
63
  minimum: 1,
@@ -105,7 +110,7 @@ export function execToolInput(name, value) {
105
110
  return invalidExecInput();
106
111
  const input = value;
107
112
  const allowed = name === 'exec'
108
- ? ['command', 'cwd', 'timeoutMs', 'yieldMs']
113
+ ? ['command', 'cwd', 'login', 'timeoutMs', 'yieldMs']
109
114
  : ['processId', 'action', 'yieldMs', 'data'];
110
115
  if (Object.keys(input).some((key) => !allowed.includes(key)))
111
116
  return invalidExecInput();
@@ -174,6 +179,7 @@ export class ExecTool {
174
179
  command: input.command,
175
180
  cwd: cwd.physicalPath,
176
181
  shell: this.shell ?? true,
182
+ shellLogin: input.login ?? true,
177
183
  environment: this.environment,
178
184
  displayCwd: cwd.displayPath,
179
185
  detached: process.platform !== 'win32',
@@ -662,11 +668,14 @@ function parseExecInput(value) {
662
668
  return invalidExecInput();
663
669
  if (input.cwd !== undefined && input.cwd !== null && typeof input.cwd !== 'string')
664
670
  return invalidExecInput();
671
+ if (input.login !== undefined && input.login !== null && typeof input.login !== 'boolean')
672
+ return invalidExecInput();
665
673
  const timeoutMs = optionalBoundedInteger(input.timeoutMs, 1, TOOL_EXECUTION_LIMITS.execMaxTimeoutMs);
666
674
  return {
667
675
  ...(input.action === 'start' ? { action: 'start' } : {}),
668
676
  command: input.command,
669
677
  ...(typeof input.cwd === 'string' ? { cwd: input.cwd } : {}),
678
+ ...(typeof input.login === 'boolean' ? { login: input.login } : {}),
670
679
  ...(timeoutMs === undefined ? {} : { timeoutMs }),
671
680
  ...(yieldMs === undefined ? {} : { yieldMs })
672
681
  };
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os';
5
5
  import { isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
6
6
  import { promisify } from 'node:util';
7
7
  import { MarAgentError } from '../../agent-core/error.js';
8
+ import { prepareLocalShellProcess } from '../../agent-core/host/local-shell.js';
8
9
  import { LINUX_BUBBLEWRAP_SANDBOX_POLICY_VERSION } from './profile.js';
9
10
  const execFileAsync = promisify(execFile);
10
11
  const DEFAULT_BUBBLEWRAP_PATH = '/usr/bin/bwrap';
@@ -63,12 +64,24 @@ export class BubblewrapSandboxExecutor {
63
64
  const mounts = await this.#mounts();
64
65
  const runtimeMounts = await trustedRuntimeMounts(request.runtimeDirectories ?? [], mounts);
65
66
  const cwd = await workingDirectory(request, mounts, this.options.profile.workingDirectory);
66
- const command = sandboxCommand(request, mounts);
67
+ // Apply the sandbox policy before creating private startup-override variables.
68
+ const environment = Object.fromEntries(explicitEnvironment(request));
69
+ const shellRequest = prepareLocalShellProcess({
70
+ ...request,
71
+ environment,
72
+ environmentKeys: Object.keys(environment),
73
+ environmentOverrides: Object.fromEntries(Object.entries(request.environmentOverrides ?? {})
74
+ .filter(([name, value]) => SAFE_ENVIRONMENT_NAME.test(name) &&
75
+ !FORBIDDEN_ENVIRONMENT_NAMES.has(name) &&
76
+ (value === undefined || Object.hasOwn(environment, name)))
77
+ .map(([name, value]) => [name, value === undefined ? undefined : environment[name]]))
78
+ });
79
+ const command = sandboxCommand(shellRequest, mounts);
67
80
  const launchDirectory = await mkdtemp(join(tmpdir(), 'mar-agent-bwrap-launch-'));
68
81
  const argumentPath = join(launchDirectory, 'arguments');
69
82
  const seccompPath = join(launchDirectory, 'seccomp.bpf');
70
83
  try {
71
- const arguments_ = this.#arguments(mounts, cwd, request, runtimeMounts);
84
+ const arguments_ = this.#arguments(mounts, cwd, shellRequest, runtimeMounts);
72
85
  await Promise.all([
73
86
  writeFile(argumentPath, `${arguments_.join('\0')}\0`, { mode: 0o600, flag: 'wx' }),
74
87
  writeFile(seccompPath, seccompFilter(process.arch), { mode: 0o600, flag: 'wx' })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.9.163",
3
+ "version": "0.9.165",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",