@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 +14 -5
- package/dist/runner/abstract-runner.js +6 -2
- package/dist/runner/codex-runner.js +62 -73
- package/dist/runner/mar-agent/conversation-normalizer.js +11 -5
- package/dist/runner/opencode-runner.js +74 -32
- package/dist/runner/runner-registry.js +12 -4
- package/dist/service/goal-mcp-server.js +5 -1
- package/dist/service/goal-runtime.js +14 -2
- package/dist/service/run-workbench-service.js +3 -2
- package/dist/service/runner-service.js +15 -5
- package/package.json +4 -4
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) =>
|
|
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 {
|
|
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
|
-
|
|
16
|
-
void
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
profileCache;
|
|
28
|
+
profileCacheVersion;
|
|
29
|
+
profileCacheTimer;
|
|
30
|
+
profileRefresh;
|
|
29
31
|
profileClients = new Set();
|
|
30
|
-
|
|
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.
|
|
44
|
+
return { ...base, models: this.profileCache?.profile.models ?? [] };
|
|
45
45
|
}
|
|
46
|
-
|
|
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
|
|
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
|
|
60
|
+
const cached = this.profileCacheVersion === version ? this.profileCache : undefined;
|
|
56
61
|
if (cached?.successful === true && now < cached.fetchedAt + CODEX_MODEL_CACHE_MS)
|
|
57
|
-
return
|
|
62
|
+
return cached.profile;
|
|
58
63
|
if (cached?.successful === true) {
|
|
59
|
-
if (now >= cached.retryAt)
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
73
|
+
return this.refreshCodexProfile(version, base, environment);
|
|
66
74
|
}
|
|
67
|
-
refreshCodexProfile(
|
|
68
|
-
const existing = this.
|
|
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.
|
|
80
|
-
|
|
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
|
-
|
|
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(
|
|
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(() =>
|
|
103
|
-
|
|
112
|
+
.finally(() => {
|
|
113
|
+
if (this.profileRefresh === refresh)
|
|
114
|
+
this.profileRefresh = undefined;
|
|
115
|
+
});
|
|
116
|
+
this.profileRefresh = refresh;
|
|
104
117
|
return refresh;
|
|
105
118
|
}
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
this.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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(
|
|
290
|
-
|
|
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
|
-
|
|
466
|
-
clearTimeout(
|
|
467
|
-
this.
|
|
468
|
-
this.profileCache
|
|
469
|
-
this.
|
|
470
|
-
this.
|
|
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:
|
|
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:
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
79
|
+
this.profileRefresh = refresh;
|
|
66
80
|
return refresh;
|
|
67
81
|
}
|
|
68
|
-
async fetchProfile(capabilities, environment,
|
|
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
|
|
94
|
+
this.profileCache = {
|
|
79
95
|
expiresAt,
|
|
80
|
-
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
308
|
-
this.
|
|
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.
|
|
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({
|
|
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
|
|
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.
|
|
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.
|
|
34
|
-
"@myagentroam/code-navigation": "0.9.
|
|
35
|
-
"@myagentroam/protocol": "0.9.
|
|
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"
|