@myagentroam/node 0.9.118 → 0.9.120

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.
package/dist/connector.js CHANGED
@@ -669,7 +669,10 @@ export class NodeConnector {
669
669
  (await this.runAttachmentService.readNative(runId, imageIndex)),
670
670
  respondUserInput: (runId, requestId, answers) => this.runnerInteractionService.respondUserInput(runId, requestId, answers),
671
671
  pauseQueue: (sessionId) => this.workspaceQueueWorkbenchService.pauseSessionQueue(sessionId),
672
- runInterrupted: (sessionId) => this.clearGoalState(sessionId),
672
+ runInterrupted: (sessionId) => {
673
+ const goalExit = this.clearGoalState(sessionId, 'stopped');
674
+ return goalExit === undefined ? undefined : { goalExit };
675
+ },
673
676
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
674
677
  control: (operation, payload) => this.controlMessages.handle(JSON.stringify(createEnvelope(operation, payload))),
675
678
  markInterrupted: (runId) => this.runState.interruptRequested.add(runId)
@@ -930,9 +933,13 @@ export class NodeConnector {
930
933
  };
931
934
  },
932
935
  clearGoal: (session) => {
933
- this.clearGoalState(session.id);
936
+ const goalExit = this.clearGoalState(session.id, 'removed');
934
937
  this.finishIdleGoalRun(session.id, 'SUCCEEDED');
935
- return { session: this.sessionPresentationService.present(session), cleared: true };
938
+ return {
939
+ session: this.sessionPresentationService.present(session),
940
+ cleared: true,
941
+ ...(goalExit === undefined ? {} : { goalExit })
942
+ };
936
943
  }
937
944
  });
938
945
  this.sessionMessageService = new SessionMessageService({
@@ -1680,7 +1687,8 @@ export class NodeConnector {
1680
1687
  return;
1681
1688
  this.runEventBridge.emitRun(run.id, status === 'SUCCEEDED' ? 'run.completed' : 'run.failed', status === 'SUCCEEDED' ? {} : { code: 'GOAL_CONTINUATION_FAILED' }, status);
1682
1689
  }
1683
- clearGoalState(sessionId) {
1690
+ clearGoalState(sessionId, status) {
1691
+ const goalExit = this.goalRuntime.exitSummary(sessionId, status);
1684
1692
  this.retireGoalContinuation(sessionId);
1685
1693
  const removed = this.goalRuntime.clear(sessionId);
1686
1694
  this.continuousRunCoordinator.removeGoalQueue(sessionId);
@@ -1688,12 +1696,13 @@ export class NodeConnector {
1688
1696
  if (commandState !== undefined)
1689
1697
  this.commandStates.clear(sessionId, 'goal');
1690
1698
  if (!removed && commandState === undefined)
1691
- return;
1699
+ return goalExit;
1692
1700
  const session = this.runtime.getAgentSession(sessionId);
1693
1701
  if (session !== undefined)
1694
1702
  this.emitWorkbenchEvent('session', {
1695
1703
  session: this.sessionPresentationService.present(session)
1696
1704
  });
1705
+ return goalExit;
1697
1706
  }
1698
1707
  prepareGoalSession(session) {
1699
1708
  const access = goalUnrestrictedAccess(session.runner);
@@ -12,8 +12,12 @@ export class AbstractRunner {
12
12
  /** Test-only runners are registered explicitly but never advertised. */
13
13
  advertised = true;
14
14
  interceptsRuns = false;
15
- async refreshProfile(capabilities, workspace, environment = {}) {
16
- void [workspace, environment];
15
+ cachedProfile(capabilities, environment = {}) {
16
+ void environment;
17
+ return this.profile(capabilities);
18
+ }
19
+ async refreshProfile(capabilities, workspace, environment = {}, waitForRefresh = false) {
20
+ void [workspace, environment, waitForRefresh];
17
21
  return this.profile(capabilities);
18
22
  }
19
23
  supportsConfiguration(configuration, workspace, environment) {
@@ -14,6 +14,7 @@ const CODEX_MODEL_CACHE_MS = 10 * 60 * 1_000;
14
14
  const CODEX_PROFILE_ENTRY_TTL_MS = 30 * 60 * 1_000;
15
15
  const CODEX_MODEL_FAILURE_RETRY_MS = 30 * 1_000;
16
16
  const CODEX_MODEL_PAGE_SIZE = 100;
17
+ const CODEX_MODEL_MAX_PAGES = 6;
17
18
  export class CodexRunner extends AbstractRunner {
18
19
  client;
19
20
  profileClientFactory;
@@ -23,13 +24,12 @@ export class CodexRunner extends AbstractRunner {
23
24
  fastEnabled = false;
24
25
  environmentFingerprint;
25
26
  environmentRuns = new Set();
26
- profileCache = new Map();
27
- profileCacheTimers = new Map();
28
- profileRefreshes = new Map();
27
+ profileCache;
28
+ profileCacheVersion;
29
+ profileCacheTimer;
30
+ profileRefresh;
29
31
  profileClients = new Set();
30
- profileKeyByEnvironment = new Map();
31
- latestProfileCache;
32
- selectedProfileKey;
32
+ profileGeneration = 0;
33
33
  constructor(client = new CodexAppServerClient(), profileClientFactory = () => new CodexAppServerClient()) {
34
34
  super();
35
35
  this.client = client;
@@ -41,33 +41,42 @@ export class CodexRunner extends AbstractRunner {
41
41
  claudeCodeAvailable: capabilities.claudeCode.available,
42
42
  openCodeAvailable: capabilities.openCode?.available === true
43
43
  }).find((profile) => profile.runner === this.name);
44
- return { ...base, models: this.latestProfileCache?.profile.models ?? [] };
44
+ return { ...base, models: this.profileCache?.profile.models ?? [] };
45
45
  }
46
- async refreshProfile(capabilities, workspace, environment = {}) {
46
+ cachedProfile(capabilities, environment = {}) {
47
+ const base = this.profile(capabilities);
48
+ return this.profileCache === undefined ||
49
+ this.profileCacheVersion !== capabilities.codex.version
50
+ ? { ...base, models: [] }
51
+ : this.profileCache.profile;
52
+ }
53
+ async refreshProfile(capabilities, workspace, environment = {}, waitForRefresh = false) {
47
54
  void workspace;
48
55
  const base = this.profile(capabilities);
49
56
  if (!capabilities.codex.available)
50
57
  return { ...base, models: [] };
51
- const key = codexEnvironmentFingerprint(environment, capabilities.codex.version);
52
- this.profileKeyByEnvironment.set(codexSecretEnvironmentFingerprint(environment), key);
53
- this.selectedProfileKey = key;
58
+ const version = capabilities.codex.version;
54
59
  const now = Date.now();
55
- const cached = this.profileCache.get(key);
60
+ const cached = this.profileCacheVersion === version ? this.profileCache : undefined;
56
61
  if (cached?.successful === true && now < cached.fetchedAt + CODEX_MODEL_CACHE_MS)
57
- return this.selectCachedProfile(key, cached);
62
+ return cached.profile;
58
63
  if (cached?.successful === true) {
59
- if (now >= cached.retryAt)
60
- void this.refreshCodexProfile(key, base, environment);
61
- return this.selectCachedProfile(key, cached);
64
+ if (now >= cached.retryAt) {
65
+ const refresh = this.refreshCodexProfile(version, base, environment);
66
+ if (waitForRefresh)
67
+ return refresh;
68
+ }
69
+ return cached.profile;
62
70
  }
63
71
  if (cached !== undefined && now < cached.retryAt)
64
72
  return cached.profile;
65
- return this.refreshCodexProfile(key, base, environment);
73
+ return this.refreshCodexProfile(version, base, environment);
66
74
  }
67
- refreshCodexProfile(key, base, environment) {
68
- const existing = this.profileRefreshes.get(key);
75
+ refreshCodexProfile(version, base, environment) {
76
+ const existing = this.profileRefresh;
69
77
  if (existing !== undefined)
70
78
  return existing;
79
+ const generation = this.profileGeneration;
71
80
  const refresh = this.fetchCodexProfile(base, environment)
72
81
  .then((value) => {
73
82
  const cache = {
@@ -76,21 +85,22 @@ export class CodexRunner extends AbstractRunner {
76
85
  successful: true,
77
86
  retryAt: 0
78
87
  };
79
- this.cacheProfile(key, cache);
80
- if (this.selectedProfileKey === key)
81
- this.latestProfileCache = cache;
88
+ if (generation === this.profileGeneration)
89
+ this.cacheProfile(version, cache);
82
90
  return cache.profile;
83
91
  })
84
92
  .catch((error) => {
85
93
  nodeLog('runner.codex.models.refresh-failed', {
86
94
  error: error instanceof Error ? error.message : String(error)
87
95
  });
88
- const cached = this.profileCache.get(key);
96
+ if (generation !== this.profileGeneration)
97
+ return { ...base, models: [] };
98
+ const cached = this.profileCacheVersion === version ? this.profileCache : undefined;
89
99
  if (cached !== undefined) {
90
100
  cached.retryAt = Date.now() + CODEX_MODEL_FAILURE_RETRY_MS;
91
101
  return cached.profile;
92
102
  }
93
- this.cacheProfile(key, {
103
+ this.cacheProfile(version, {
94
104
  profile: { ...base, models: [] },
95
105
  defaultConfiguration: emptyCodexConfiguration(),
96
106
  fetchedAt: 0,
@@ -99,33 +109,24 @@ export class CodexRunner extends AbstractRunner {
99
109
  });
100
110
  return { ...base, models: [] };
101
111
  })
102
- .finally(() => this.profileRefreshes.delete(key));
103
- this.profileRefreshes.set(key, refresh);
112
+ .finally(() => {
113
+ if (this.profileRefresh === refresh)
114
+ this.profileRefresh = undefined;
115
+ });
116
+ this.profileRefresh = refresh;
104
117
  return refresh;
105
118
  }
106
- selectCachedProfile(key, cached) {
107
- if (cached.successful && this.selectedProfileKey === key)
108
- this.latestProfileCache = cached;
109
- return cached.profile;
110
- }
111
- cacheProfile(key, cache) {
112
- const previousTimer = this.profileCacheTimers.get(key);
113
- if (previousTimer !== undefined)
114
- clearTimeout(previousTimer);
115
- this.profileCache.set(key, cache);
116
- const timer = setTimeout(() => {
117
- this.profileCacheTimers.delete(key);
118
- this.profileCache.delete(key);
119
- for (const [environmentKey, profileKey] of this.profileKeyByEnvironment)
120
- if (profileKey === key)
121
- this.profileKeyByEnvironment.delete(environmentKey);
122
- if (this.selectedProfileKey === key) {
123
- this.selectedProfileKey = undefined;
124
- this.latestProfileCache = undefined;
125
- }
119
+ cacheProfile(version, cache) {
120
+ if (this.profileCacheTimer !== undefined)
121
+ clearTimeout(this.profileCacheTimer);
122
+ this.profileCache = cache;
123
+ this.profileCacheVersion = version;
124
+ this.profileCacheTimer = setTimeout(() => {
125
+ this.profileCacheTimer = undefined;
126
+ this.profileCache = undefined;
127
+ this.profileCacheVersion = undefined;
126
128
  }, CODEX_PROFILE_ENTRY_TTL_MS);
127
- timer.unref();
128
- this.profileCacheTimers.set(key, timer);
129
+ this.profileCacheTimer.unref();
129
130
  }
130
131
  async fetchCodexProfile(base, environment) {
131
132
  const client = this.profileClientFactory();
@@ -136,8 +137,12 @@ export class CodexRunner extends AbstractRunner {
136
137
  await client.start();
137
138
  const models = [];
138
139
  let cursor = null;
140
+ let pages = 0;
139
141
  const cursors = new Set();
140
142
  do {
143
+ if (pages >= CODEX_MODEL_MAX_PAGES)
144
+ throw new Error('CODEX_MODELS_PAGE_LIMIT');
145
+ pages += 1;
141
146
  const page = codexModelPage(await client.listModels({ cursor, limit: CODEX_MODEL_PAGE_SIZE, includeHidden: false }));
142
147
  models.push(...page.models);
143
148
  cursor = page.nextCursor;
@@ -286,11 +291,8 @@ export class CodexRunner extends AbstractRunner {
286
291
  profile.models.some((model) => model.id === configuration.model && model.supportedEfforts.includes(configuration.effort)) &&
287
292
  profile.accessOptions.some((option) => option.id === configuration.access));
288
293
  }
289
- profileCacheForEnvironment(environment) {
290
- if (environment === undefined)
291
- return this.latestProfileCache;
292
- const key = this.profileKeyByEnvironment.get(codexSecretEnvironmentFingerprint(environment));
293
- return key === undefined ? undefined : this.profileCache.get(key);
294
+ profileCacheForEnvironment(_environment) {
295
+ return this.profileCache;
294
296
  }
295
297
  async resumeExternal(external, context) {
296
298
  if (external.runner !== this.name ||
@@ -454,6 +456,7 @@ export class CodexRunner extends AbstractRunner {
454
456
  this.environmentFingerprint = undefined;
455
457
  }
456
458
  stop() {
459
+ this.profileGeneration += 1;
457
460
  for (const profileClient of this.profileClients)
458
461
  profileClient.stop();
459
462
  this.profileClients.clear();
@@ -462,13 +465,12 @@ export class CodexRunner extends AbstractRunner {
462
465
  this.client.clearEnvironment();
463
466
  this.environmentFingerprint = undefined;
464
467
  this.environmentRuns.clear();
465
- for (const timer of this.profileCacheTimers.values())
466
- clearTimeout(timer);
467
- this.profileCacheTimers.clear();
468
- this.profileCache.clear();
469
- this.profileKeyByEnvironment.clear();
470
- this.latestProfileCache = undefined;
471
- this.selectedProfileKey = undefined;
468
+ if (this.profileCacheTimer !== undefined)
469
+ clearTimeout(this.profileCacheTimer);
470
+ this.profileCacheTimer = undefined;
471
+ this.profileCache = undefined;
472
+ this.profileCacheVersion = undefined;
473
+ this.profileRefresh = undefined;
472
474
  }
473
475
  request(method, params) {
474
476
  return this.client.request(method, params);
@@ -516,19 +518,6 @@ export class CodexRunner extends AbstractRunner {
516
518
  function emptyCodexConfiguration() {
517
519
  return { runner: 'codex', model: '', effort: '', access: 'on-request' };
518
520
  }
519
- function codexEnvironmentFingerprint(environment, version) {
520
- return createHash('sha256')
521
- .update(JSON.stringify({
522
- environment: Object.entries(environment).sort(([left], [right]) => left.localeCompare(right)),
523
- version: version ?? null
524
- }))
525
- .digest('base64url');
526
- }
527
- function codexSecretEnvironmentFingerprint(environment) {
528
- return createHash('sha256')
529
- .update(JSON.stringify(Object.entries(environment).sort(([left], [right]) => left.localeCompare(right))))
530
- .digest('base64url');
531
- }
532
521
  function codexModelPage(value) {
533
522
  if (!isPlainRecord(value) || !Array.isArray(value.data))
534
523
  throw new Error('CODEX_MODELS_INVALID');
@@ -67,7 +67,12 @@ export class MarAgentConversationNormalizer {
67
67
  return this.#toolCompleted(event);
68
68
  if (event.type === 'tool.failed')
69
69
  return this.#toolFailed(event);
70
- if (event.type === 'hosted_tool.updated')
70
+ if (event.type === 'hosted_tool.updated') {
71
+ const sourceSummary = event.summary.sources
72
+ ?.slice(0, 20)
73
+ .map((source) => (source.title ? `${source.title} — ${source.url}` : source.url))
74
+ .join('\n');
75
+ const output = sourceSummary || event.summary.title || '';
71
76
  return [
72
77
  {
73
78
  itemId: itemId('hosted', event.itemId),
@@ -84,16 +89,17 @@ export class MarAgentConversationNormalizer {
84
89
  inputSummary: event.summary.query === undefined && event.summary.url === undefined
85
90
  ? null
86
91
  : compactRunnerText(event.summary.query ?? event.summary.url ?? '', 10_000),
87
- outputSummary: event.summary.title === undefined
88
- ? null
89
- : compactRunnerText(event.summary.title, 10_000),
92
+ outputSummary: output ? compactRunnerText(output, 10_000) : null,
90
93
  progressLabel: event.status === 'RUNNING' ? 'Running' : null,
91
94
  errorCode: event.status === 'FAILED' ? 'HOSTED_TOOL_FAILED' : null,
92
- truncated: false
95
+ truncated: event.summary.sourcesTruncated === true ||
96
+ (event.summary.sources?.length ?? 0) > 20 ||
97
+ output.length > 10_000
93
98
  },
94
99
  merge: true
95
100
  }
96
101
  ];
102
+ }
97
103
  if (event.type === 'todo.updated')
98
104
  return [
99
105
  {
@@ -8,6 +8,9 @@ import { homedir } from 'node:os';
8
8
  import { openCodeConversationPage } from './opencode/conversation-parser.js';
9
9
  import { nodeLog } from '../operational.js';
10
10
  const PROFILE_ENTRY_TTL_MS = 30 * 60_000;
11
+ const PROFILE_STALE_TTL_MS = 60 * 60_000;
12
+ const PROFILE_FAILURE_RETRY_MS = 30_000;
13
+ const PROFILE_REFRESH_TIMEOUT_MS = 90_000;
11
14
  export class OpenCodeRunner extends AbstractRunner {
12
15
  client;
13
16
  clientFactory;
@@ -15,8 +18,10 @@ export class OpenCodeRunner extends AbstractRunner {
15
18
  discoveryScope = 'WORKSPACE_REQUIRED';
16
19
  contextLimits = new Map();
17
20
  contextLimitsExpiresAt = 0;
18
- profileCache = new Map();
19
- profileRefreshes = new Map();
21
+ profileCache;
22
+ profileRetryAt = 0;
23
+ profileRefresh;
24
+ profileGeneration = 0;
20
25
  execution = new Map();
21
26
  managedTurns = new Map();
22
27
  environmentClients = new Map();
@@ -34,17 +39,21 @@ export class OpenCodeRunner extends AbstractRunner {
34
39
  }).find((profile) => profile.runner === this.name);
35
40
  return { ...base, models: [] };
36
41
  }
37
- async refreshProfile(capabilities, _workspace, environment = {}) {
42
+ cachedProfile(capabilities, environment = {}) {
43
+ this.pruneProfileCaches(Date.now());
44
+ const cached = this.profileCache;
45
+ return cached === undefined
46
+ ? this.profile(capabilities)
47
+ : { ...this.profile(capabilities), models: cached.models };
48
+ }
49
+ async refreshProfile(capabilities, _workspace, environment = {}, _waitForRefresh = false) {
38
50
  if (capabilities.openCode?.available !== true) {
39
51
  return this.profile(capabilities);
40
52
  }
41
- const cacheKey = this.environmentFingerprint(environment);
42
53
  const now = Date.now();
43
- for (const [key, entry] of this.profileCache)
44
- if (entry.expiresAt <= now)
45
- this.profileCache.delete(key);
46
- const cached = this.profileCache.get(cacheKey);
47
- if (cached !== undefined) {
54
+ this.pruneProfileCaches(now);
55
+ const cached = this.profileCache;
56
+ if (cached !== undefined && cached.expiresAt > now) {
48
57
  nodeLog('runner.opencode.profile.cache-hit', {
49
58
  models: cached.models.length,
50
59
  remainingTtlMs: cached.expiresAt - now
@@ -53,34 +62,42 @@ export class OpenCodeRunner extends AbstractRunner {
53
62
  this.contextLimitsExpiresAt = cached.expiresAt;
54
63
  return { ...this.profile(capabilities), models: cached.models };
55
64
  }
56
- const current = this.profileRefreshes.get(cacheKey);
65
+ if (now < this.profileRetryAt)
66
+ return cached === undefined
67
+ ? this.profile(capabilities)
68
+ : { ...this.profile(capabilities), models: cached.models };
69
+ const current = this.profileRefresh;
57
70
  if (current !== undefined) {
58
71
  nodeLog('runner.opencode.profile.single-flight-joined', {});
59
72
  return current;
60
73
  }
61
- const refresh = this.fetchProfile(capabilities, environment, cacheKey).finally(() => {
62
- if (this.profileRefreshes.get(cacheKey) === refresh)
63
- this.profileRefreshes.delete(cacheKey);
74
+ const generation = this.profileGeneration;
75
+ const refresh = this.fetchProfile(capabilities, environment, generation, cached).finally(() => {
76
+ if (this.profileRefresh === refresh)
77
+ this.profileRefresh = undefined;
64
78
  });
65
- this.profileRefreshes.set(cacheKey, refresh);
79
+ this.profileRefresh = refresh;
66
80
  return refresh;
67
81
  }
68
- async fetchProfile(capabilities, environment, cacheKey) {
69
- const client = this.managedClient(environment);
82
+ async fetchProfile(capabilities, environment, generation, stale) {
83
+ const client = this.managedClient(environment, 'runner-profile');
70
84
  const startedAt = performance.now();
71
85
  try {
72
- const catalog = await client.providers(homedir());
86
+ const catalog = await withProfileTimeout(client.providers(homedir()));
73
87
  const models = openCodeProfileModels(catalog);
74
88
  const contextLimits = openCodeContextLimits(catalog);
75
89
  const expiresAt = Date.now() + PROFILE_ENTRY_TTL_MS;
90
+ if (generation !== this.profileGeneration)
91
+ return this.profile(capabilities);
76
92
  this.contextLimits = contextLimits;
77
93
  this.contextLimitsExpiresAt = expiresAt;
78
- this.profileCache.set(cacheKey, {
94
+ this.profileCache = {
79
95
  expiresAt,
80
- environmentFingerprint: cacheKey,
96
+ discardAt: Date.now() + PROFILE_STALE_TTL_MS,
81
97
  models,
82
98
  contextLimits
83
- });
99
+ };
100
+ this.profileRetryAt = 0;
84
101
  nodeLog('runner.opencode.profile.providers.completed', {
85
102
  models: models.length,
86
103
  durationMs: Math.round(performance.now() - startedAt)
@@ -88,6 +105,9 @@ export class OpenCodeRunner extends AbstractRunner {
88
105
  return { ...this.profile(capabilities), models };
89
106
  }
90
107
  catch {
108
+ if (generation !== this.profileGeneration)
109
+ return this.profile(capabilities);
110
+ this.profileRetryAt = Date.now() + PROFILE_FAILURE_RETRY_MS;
91
111
  nodeLog('runner.opencode.profile.providers.failed', {
92
112
  durationMs: Math.round(performance.now() - startedAt)
93
113
  });
@@ -95,11 +115,9 @@ export class OpenCodeRunner extends AbstractRunner {
95
115
  finally {
96
116
  this.releaseManagedClient(client);
97
117
  }
98
- return this.profile(capabilities);
99
- }
100
- environmentFingerprint(environment) {
101
- const entries = Object.entries(environment).sort(([left], [right]) => left.localeCompare(right));
102
- return createHash('sha256').update(JSON.stringify(entries)).digest('hex');
118
+ return stale === undefined
119
+ ? this.profile(capabilities)
120
+ : { ...this.profile(capabilities), models: stale.models };
103
121
  }
104
122
  available(capabilities) {
105
123
  return capabilities.openCode?.available === true;
@@ -135,11 +153,17 @@ export class OpenCodeRunner extends AbstractRunner {
135
153
  };
136
154
  }
137
155
  cachedModels(environment) {
138
- const fingerprint = this.environmentFingerprint(environment);
139
- const now = Date.now();
140
- return [...this.profileCache.values()]
141
- .filter((entry) => entry.expiresAt > now && entry.environmentFingerprint === fingerprint)
142
- .flatMap((entry) => entry.models);
156
+ this.pruneProfileCaches(Date.now());
157
+ void environment;
158
+ return this.profileCache?.models ?? [];
159
+ }
160
+ pruneProfileCaches(now) {
161
+ if (this.profileCache !== undefined && this.profileCache.discardAt <= now)
162
+ this.profileCache = undefined;
163
+ if (this.profileRetryAt <= now &&
164
+ this.profileCache === undefined &&
165
+ this.profileRefresh === undefined)
166
+ this.profileRetryAt = 0;
143
167
  }
144
168
  mcpConfiguration(raw, secrets) {
145
169
  const servers = super.mcpConfiguration(raw, secrets);
@@ -300,12 +324,14 @@ export class OpenCodeRunner extends AbstractRunner {
300
324
  return maxTokens === undefined ? undefined : { model: modelId, usedTokens, maxTokens };
301
325
  }
302
326
  async stop() {
327
+ this.profileGeneration += 1;
303
328
  for (const execution of this.execution.values())
304
329
  execution.abort.abort();
305
330
  this.execution.clear();
306
331
  this.managedTurns.clear();
307
- this.profileCache.clear();
308
- this.profileRefreshes.clear();
332
+ this.profileCache = undefined;
333
+ this.profileRefresh = undefined;
334
+ this.profileRetryAt = 0;
309
335
  this.contextLimits = new Map();
310
336
  this.contextLimitsExpiresAt = 0;
311
337
  const stopping = [
@@ -351,6 +377,22 @@ export class OpenCodeRunner extends AbstractRunner {
351
377
  }
352
378
  }
353
379
  }
380
+ async function withProfileTimeout(operation) {
381
+ let timer;
382
+ try {
383
+ return await Promise.race([
384
+ operation,
385
+ new Promise((_resolve, reject) => {
386
+ timer = setTimeout(() => reject(new Error('OPENCODE_PROFILE_REFRESH_TIMEOUT')), PROFILE_REFRESH_TIMEOUT_MS);
387
+ timer.unref();
388
+ })
389
+ ]);
390
+ }
391
+ finally {
392
+ if (timer !== undefined)
393
+ clearTimeout(timer);
394
+ }
395
+ }
354
396
  function object(value) {
355
397
  return typeof value === 'object' && value !== null && !Array.isArray(value)
356
398
  ? value
@@ -29,17 +29,25 @@ export class RunnerRegistry {
29
29
  const runner = this.runners.get(name);
30
30
  return runner?.advertised === true ? runner : undefined;
31
31
  }
32
- profiles(capabilities) {
32
+ profiles(capabilities, environment = {}) {
33
33
  return this.advertised().flatMap((runner) => {
34
- const profile = runner.profile(capabilities);
34
+ const profile = runner.cachedProfile(capabilities, environment);
35
35
  return profile === undefined ? [] : [profile];
36
36
  });
37
37
  }
38
- async refreshProfiles(capabilities, workspace, environment = {}) {
38
+ async refreshProfiles(capabilities, workspace, environment = {}, waitForRefresh = false) {
39
39
  const profiles = await Promise.all(this.advertised().map(async (runner) => {
40
40
  const startedAt = performance.now();
41
41
  try {
42
- return await runner.refreshProfile(capabilities, workspace, environment);
42
+ return await runner.refreshProfile(capabilities, workspace, environment, waitForRefresh);
43
+ }
44
+ catch (error) {
45
+ nodeLog('runner.profile.refresh.failed', {
46
+ runner: runner.name,
47
+ workspaceId: workspace?.id,
48
+ error: error instanceof Error ? error.message : String(error)
49
+ });
50
+ return runner.cachedProfile(capabilities, environment);
43
51
  }
44
52
  finally {
45
53
  const durationMs = Math.round(performance.now() - startedAt);
@@ -98,7 +98,11 @@ export class GoalMcpServer {
98
98
  throw new Error('GOAL_STATUS_INVALID');
99
99
  const goal = this.goals.update(capability.sessionId, capability.goalId, capability.revision, status);
100
100
  this.changed(capability.sessionId);
101
- socket.end(`${JSON.stringify({ result: goal })}\n`);
101
+ socket.end(`${JSON.stringify({
102
+ result: goal.status === 'active'
103
+ ? goal
104
+ : { ...goal, elapsedWallMs: this.goals.elapsedWallMs(goal) }
105
+ })}\n`);
102
106
  }
103
107
  catch (error) {
104
108
  socket.end(`${JSON.stringify({ error: error instanceof Error ? error.message : 'GOAL_IPC_FAILED' })}\n`);
@@ -72,10 +72,12 @@ export function publicGoalHistoryText(text) {
72
72
  }
73
73
  export class GoalRuntime {
74
74
  changed;
75
+ now;
75
76
  goals = new Map();
76
77
  executionFailures = new Map();
77
- constructor(changed) {
78
+ constructor(changed, now = Date.now) {
78
79
  this.changed = changed;
80
+ this.now = now;
79
81
  }
80
82
  get(sessionId) {
81
83
  return this.goals.get(sessionId);
@@ -104,6 +106,7 @@ export class GoalRuntime {
104
106
  id: continuing ? current.id : randomUUID(),
105
107
  sessionId,
106
108
  objective: normalized,
109
+ startedAt: continuing ? current.startedAt : this.now(),
107
110
  revision: continuing ? current.revision + 1 : 1,
108
111
  status: 'active',
109
112
  turns: 0,
@@ -117,6 +120,15 @@ export class GoalRuntime {
117
120
  this.changed(goal);
118
121
  return goal;
119
122
  }
123
+ elapsedWallMs(goal) {
124
+ return Math.max(0, this.now() - goal.startedAt);
125
+ }
126
+ exitSummary(sessionId, status) {
127
+ const goal = this.get(sessionId);
128
+ if (goal === undefined || goal.status !== 'active')
129
+ return undefined;
130
+ return { status, elapsedWallMs: this.elapsedWallMs(goal), turns: goal.turns };
131
+ }
120
132
  recordTurn(sessionId) {
121
133
  const current = this.goals.get(sessionId);
122
134
  if (current?.status !== 'active')
@@ -235,7 +247,7 @@ export class GoalRuntime {
235
247
  const heading = kind === 'start' ? 'A Goal has started or changed.' : 'Continue the active Goal.';
236
248
  const open = kind === 'start' ? START_OPEN : CONTINUE_OPEN;
237
249
  const close = kind === 'start' ? START_CLOSE : CONTINUE_CLOSE;
238
- return `${open}\n${heading}\n\nOriginal objective:\n${goal.objective}\n\nContinue executing this Goal across turns:\n- Work on the objective now. If it is incomplete, use tools to finish a coherent unit or obtain decisive evidence; do not stop at an acknowledgement, status, plan, or promise.\n- Preserve the original scope. Before completion, review the full objective and verify it with real evidence, then call mar_update_goal with status=completed.\n- Adapt and retry after individual failures. Request blocked only after the same blocker persists for three turns and the required additional retry.\n- Use mar_get_goal only when you need to reread the authoritative objective or status.\n${close}`;
250
+ return `${open}\n${heading}\n\nOriginal objective:\n${goal.objective}\n\nContinue executing this Goal across turns:\n- Work on the objective now. If it is incomplete, use tools to finish a coherent unit or obtain decisive evidence; do not stop at an acknowledgement, status, plan, or promise.\n- Preserve the original scope. Before completion, review the full objective and verify it with real evidence, then call mar_update_goal with status=completed.\n- Adapt and retry after individual failures. Request blocked only after the same blocker persists for three turns and the required additional retry.\n- Use mar_get_goal only when you need to reread the authoritative objective or status.\n- After mar_update_goal confirms completed or finally blocked, summarize the completed work, verification evidence, remaining issues, and the tool-reported elapsedWallMs converted from milliseconds into a concise readable duration in your final user-facing response. Name whether the Goal completed or is finally blocked. That elapsed time is wall-clock time through the status update, including waiting and retries, not tool execution time or time through this final response. If blocked returns active for the required retry, continue work instead of claiming a final summary.\n${close}`;
239
251
  }
240
252
  requireCurrent(sessionId, goalId, revision) {
241
253
  const current = this.goals.get(sessionId);
@@ -67,6 +67,7 @@ export class RunWorkbenchService {
67
67
  const executionId = this.options.runtime.currentExecutionId(run.id);
68
68
  const turn = this.options.runtime.conversationTurnForRun(run.id);
69
69
  const controlRunId = executionId ?? (turn === undefined || turn.userItemId === null ? run.id : undefined);
70
+ let interruptionResult;
70
71
  try {
71
72
  this.options.pauseQueue(run.sessionId);
72
73
  if (controlRunId === undefined) {
@@ -79,9 +80,9 @@ export class RunWorkbenchService {
79
80
  }
80
81
  }
81
82
  finally {
82
- this.options.runInterrupted(run.sessionId);
83
+ interruptionResult = this.options.runInterrupted(run.sessionId);
83
84
  }
84
- return { run: next };
85
+ return { run: next, ...interruptionResult };
85
86
  }
86
87
  decideApproval(input) {
87
88
  if (typeof input.runId !== 'string' ||
@@ -19,11 +19,11 @@ export class RunnerService {
19
19
  'runner.defaults.save': (data) => this.saveDefaults(data)
20
20
  };
21
21
  }
22
- profiles() {
23
- return this.runners.profiles(this.capabilities());
22
+ profiles(environment = {}) {
23
+ return this.runners.profiles(this.capabilities(), environment);
24
24
  }
25
- refreshProfiles(environment = {}) {
26
- return this.runners.refreshProfiles(this.capabilities(), undefined, environment);
25
+ refreshProfiles(environment = {}, waitForRefresh = false) {
26
+ return this.runners.refreshProfiles(this.capabilities(), undefined, environment, waitForRefresh);
27
27
  }
28
28
  async usageStatuses() {
29
29
  return {
@@ -61,8 +61,18 @@ export class RunnerService {
61
61
  this.database().saveRunnerDefaults(normalized);
62
62
  return normalized;
63
63
  });
64
+ const cacheOnly = input.waitForRefresh === false;
65
+ const waitForRefresh = !cacheOnly;
66
+ const refresh = this.refreshProfiles(environment, waitForRefresh);
67
+ if (cacheOnly) {
68
+ void refresh.catch(() => undefined);
69
+ return {
70
+ profiles: this.profiles(environment),
71
+ defaults
72
+ };
73
+ }
64
74
  return {
65
- profiles: await this.refreshProfiles(environment),
75
+ profiles: await refresh,
66
76
  defaults
67
77
  };
68
78
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.118",
3
+ "version": "0.9.120",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -30,9 +30,9 @@
30
30
  "npm": "11.12.1",
31
31
  "ws": "^8.21.3",
32
32
  "zod": "4.4.3",
33
- "@myagentroam/agent": "0.9.118",
34
- "@myagentroam/code-navigation": "0.9.118",
35
- "@myagentroam/protocol": "0.9.118"
33
+ "@myagentroam/agent": "0.9.120",
34
+ "@myagentroam/code-navigation": "0.9.120",
35
+ "@myagentroam/protocol": "0.9.120"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/ws": "^8.18.1"