@myagentroam/node 0.10.61 → 0.10.63
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 +106 -873
- package/dist/database.js +4 -2
- package/dist/runner/claude-code-runner.js +15 -1
- package/dist/runner/mar-agent/managed-run-controller.js +126 -521
- package/dist/runner/mar-agent/session-runtime-registry.js +271 -0
- package/dist/runner/mar-agent/session-runtime.js +282 -0
- package/dist/service/conversation-hosting-lifecycle-service.js +195 -0
- package/dist/service/conversation-observation.js +240 -0
- package/dist/service/goal-continuation-coordinator.js +622 -0
- package/dist/service/native-session-watch-service.js +88 -270
- package/dist/service/restart-session-restorer.js +1 -1
- package/dist/service/runner-interaction-service.js +27 -1
- package/dist/service/scheduled-session-coordinator.js +119 -48
- package/dist/service/session-lifecycle-service.js +5 -2
- package/package.json +4 -4
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { closeImageCredentialProviders } from './runtime-model-factory.js';
|
|
2
|
+
import { nodeLog } from '../../operational.js';
|
|
3
|
+
import { createSessionRuntime, reconfigureSessionRuntime, releaseSessionRuntime, resolveMarAgentHostRuntimeConfiguration, sessionHostFingerprint, HOST_CONFIGURATION_CATEGORIES } from './session-runtime.js';
|
|
4
|
+
export class MarAgentSessionRuntimeRegistry {
|
|
5
|
+
resources;
|
|
6
|
+
promote;
|
|
7
|
+
releaseComputerSession;
|
|
8
|
+
#entries = new Map();
|
|
9
|
+
#disposed = false;
|
|
10
|
+
#disposePromise;
|
|
11
|
+
constructor(resources, promote, releaseComputerSession) {
|
|
12
|
+
this.resources = resources;
|
|
13
|
+
this.promote = promote;
|
|
14
|
+
this.releaseComputerSession = releaseComputerSession;
|
|
15
|
+
}
|
|
16
|
+
findRuntime(alias) {
|
|
17
|
+
const entry = this.#entries.get(alias);
|
|
18
|
+
return entry?.release === undefined ? entry?.runtime : undefined;
|
|
19
|
+
}
|
|
20
|
+
acquire(input, lifetime) {
|
|
21
|
+
if (this.#disposed)
|
|
22
|
+
return Promise.reject(new Error('MAR_AGENT_RUN_CONTROLLER_DISPOSED'));
|
|
23
|
+
const aliases = new Set([input.sessionId, input.externalSessionId ?? input.sessionId]);
|
|
24
|
+
const entries = new Set([...aliases].flatMap((alias) => {
|
|
25
|
+
const entry = this.#entries.get(alias);
|
|
26
|
+
return entry === undefined ? [] : [entry];
|
|
27
|
+
}));
|
|
28
|
+
if (entries.size > 1)
|
|
29
|
+
return Promise.reject(new Error('MAR_AGENT_SESSION_RUNTIME_ACTIVE'));
|
|
30
|
+
const entry = entries.values().next().value ?? {
|
|
31
|
+
aliases: new Set(),
|
|
32
|
+
users: new Set()
|
|
33
|
+
};
|
|
34
|
+
if (entry.release !== undefined) {
|
|
35
|
+
const release = entry.release;
|
|
36
|
+
if (release.reason !== 'idle_expired')
|
|
37
|
+
return Promise.reject(new Error('MAR_AGENT_SESSION_RUNTIME_RELEASED'));
|
|
38
|
+
return release.result.then(() => {
|
|
39
|
+
if (release.reason !== 'idle_expired')
|
|
40
|
+
throw new Error('MAR_AGENT_SESSION_RUNTIME_RELEASED');
|
|
41
|
+
return this.acquire(input, lifetime);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (entry.current !== undefined || entry.acquisition !== undefined)
|
|
45
|
+
return Promise.reject(new Error('MAR_AGENT_SESSION_RUNTIME_ACTIVE'));
|
|
46
|
+
for (const alias of aliases)
|
|
47
|
+
this.#addAlias(entry, alias);
|
|
48
|
+
this.#clearTimer(entry);
|
|
49
|
+
entry.current = lifetime;
|
|
50
|
+
entry.users.add(lifetime);
|
|
51
|
+
void lifetime.settled.then(() => entry.users.delete(lifetime));
|
|
52
|
+
// Publish the acquisition before any dependency can synchronously reenter.
|
|
53
|
+
const acquisition = Promise.resolve().then(async () => {
|
|
54
|
+
let uncommitted;
|
|
55
|
+
try {
|
|
56
|
+
this.#assertUsable(entry, lifetime);
|
|
57
|
+
const configuration = await resolveMarAgentHostRuntimeConfiguration(input.secretEnvironment);
|
|
58
|
+
this.#assertUsable(entry, lifetime);
|
|
59
|
+
const fingerprint = sessionHostFingerprint(input, configuration);
|
|
60
|
+
const existing = entry.runtime;
|
|
61
|
+
if (existing !== undefined && existing.fingerprint.value === fingerprint.value) {
|
|
62
|
+
await reconfigureSessionRuntime(existing, input, this.resources.modelFactory);
|
|
63
|
+
this.#assertUsable(entry, lifetime);
|
|
64
|
+
nodeLog('runner.mar-agent.runtime.reused', {
|
|
65
|
+
runtimeId: existing.runtimeId,
|
|
66
|
+
sessionId: existing.session.id,
|
|
67
|
+
runId: input.runId
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
let nativeId = input.externalSessionId;
|
|
72
|
+
if (existing !== undefined) {
|
|
73
|
+
nativeId = existing.session.id;
|
|
74
|
+
await Promise.all([...entry.users].filter((user) => user !== lifetime).map((user) => user.settled));
|
|
75
|
+
this.#assertUsable(entry, lifetime);
|
|
76
|
+
delete entry.runtime;
|
|
77
|
+
await this.#destroyRuntime(existing, entry.aliases, 'host_configuration_changed', undefined, HOST_CONFIGURATION_CATEGORIES.filter((category) => existing.fingerprint.categories[category] !== fingerprint.categories[category]));
|
|
78
|
+
this.#assertUsable(entry, lifetime);
|
|
79
|
+
}
|
|
80
|
+
const request = nativeId === undefined ? input : { ...input, externalSessionId: nativeId };
|
|
81
|
+
let created;
|
|
82
|
+
created = await createSessionRuntime(request, nativeId ?? input.sessionId, fingerprint, configuration, this.resources, () => created !== undefined && entry.runtime === created && entry.release === undefined
|
|
83
|
+
? entry.current?.runId
|
|
84
|
+
: undefined);
|
|
85
|
+
uncommitted = created;
|
|
86
|
+
this.#addAlias(entry, uncommitted.session.id);
|
|
87
|
+
nodeLog('runner.mar-agent.runtime.created', {
|
|
88
|
+
runtimeId: uncommitted.runtimeId,
|
|
89
|
+
sessionId: uncommitted.session.id,
|
|
90
|
+
runId: input.runId
|
|
91
|
+
});
|
|
92
|
+
if (input.externalSessionId === undefined)
|
|
93
|
+
this.promote(input.sessionId, uncommitted.session.id);
|
|
94
|
+
this.#assertUsable(entry, lifetime);
|
|
95
|
+
entry.runtime = uncommitted;
|
|
96
|
+
uncommitted = undefined;
|
|
97
|
+
}
|
|
98
|
+
const runtime = entry.runtime;
|
|
99
|
+
return {
|
|
100
|
+
runtime,
|
|
101
|
+
assertUsable: () => this.#assertUsable(entry, lifetime),
|
|
102
|
+
terminal: () => this.#terminal(entry, lifetime)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (uncommitted !== undefined) {
|
|
107
|
+
nodeLog('runner.mar-agent.runtime.releasing', {
|
|
108
|
+
runtimeId: uncommitted.runtimeId,
|
|
109
|
+
sessionId: uncommitted.session.id,
|
|
110
|
+
reason: 'admission_released',
|
|
111
|
+
runId: input.runId
|
|
112
|
+
});
|
|
113
|
+
// No Execution was bound: do not release another Entry's Session resources
|
|
114
|
+
// if alias registration failed because the SDK returned an existing identity.
|
|
115
|
+
try {
|
|
116
|
+
await releaseSessionRuntime(uncommitted);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
this.#logCleanupFailure(uncommitted.session.id);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
await closeImageCredentialProviders(uncommitted.models);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
entry.users.delete(lifetime);
|
|
126
|
+
if (entry.current === lifetime)
|
|
127
|
+
delete entry.current;
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
delete entry.acquisition;
|
|
132
|
+
if (entry.release === undefined && entry.current === undefined) {
|
|
133
|
+
if (entry.runtime === undefined)
|
|
134
|
+
this.#remove(entry);
|
|
135
|
+
else
|
|
136
|
+
this.#schedule(entry);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
entry.acquisition = acquisition;
|
|
141
|
+
return acquisition;
|
|
142
|
+
}
|
|
143
|
+
releaseSession(alias) {
|
|
144
|
+
const existing = this.#entries.get(alias);
|
|
145
|
+
if (existing?.release !== undefined) {
|
|
146
|
+
existing.release.reason = 'session_released';
|
|
147
|
+
return existing.release.result;
|
|
148
|
+
}
|
|
149
|
+
if (this.#disposed)
|
|
150
|
+
return this.#disposePromise;
|
|
151
|
+
const entry = existing ?? { aliases: new Set([alias]), users: new Set() };
|
|
152
|
+
if (existing === undefined)
|
|
153
|
+
this.#entries.set(alias, entry);
|
|
154
|
+
return this.#release(entry, 'session_released');
|
|
155
|
+
}
|
|
156
|
+
dispose() {
|
|
157
|
+
if (this.#disposePromise !== undefined)
|
|
158
|
+
return this.#disposePromise;
|
|
159
|
+
this.#disposed = true;
|
|
160
|
+
this.#disposePromise = Promise.resolve().then(async () => {
|
|
161
|
+
const results = await Promise.allSettled([...new Set(this.#entries.values())].map((entry) => this.#release(entry, 'controller_disposed')));
|
|
162
|
+
const failure = results.find((result) => result.status === 'rejected');
|
|
163
|
+
if (failure?.status === 'rejected')
|
|
164
|
+
throw failure.reason;
|
|
165
|
+
});
|
|
166
|
+
return this.#disposePromise;
|
|
167
|
+
}
|
|
168
|
+
#assertUsable(entry, lifetime) {
|
|
169
|
+
lifetime.signal.throwIfAborted();
|
|
170
|
+
if (this.#disposed)
|
|
171
|
+
throw new Error('MAR_AGENT_RUN_CONTROLLER_DISPOSED');
|
|
172
|
+
if (entry.release !== undefined || entry.current !== lifetime)
|
|
173
|
+
throw new Error('MAR_AGENT_SESSION_RUNTIME_RELEASED');
|
|
174
|
+
}
|
|
175
|
+
#addAlias(entry, alias) {
|
|
176
|
+
const existing = this.#entries.get(alias);
|
|
177
|
+
if (existing !== undefined && existing !== entry)
|
|
178
|
+
throw new Error('MAR_AGENT_SESSION_RUNTIME_ACTIVE');
|
|
179
|
+
entry.aliases.add(alias);
|
|
180
|
+
this.#entries.set(alias, entry);
|
|
181
|
+
}
|
|
182
|
+
#terminal(entry, lifetime) {
|
|
183
|
+
if (entry.current !== lifetime)
|
|
184
|
+
return;
|
|
185
|
+
delete entry.current;
|
|
186
|
+
this.#schedule(entry);
|
|
187
|
+
}
|
|
188
|
+
#clearTimer(entry) {
|
|
189
|
+
if (entry.idleTimer === undefined)
|
|
190
|
+
return;
|
|
191
|
+
clearTimeout(entry.idleTimer);
|
|
192
|
+
delete entry.idleTimer;
|
|
193
|
+
}
|
|
194
|
+
#schedule(entry) {
|
|
195
|
+
if (entry.runtime === undefined ||
|
|
196
|
+
entry.current !== undefined ||
|
|
197
|
+
entry.acquisition !== undefined ||
|
|
198
|
+
entry.release !== undefined ||
|
|
199
|
+
entry.idleTimer !== undefined ||
|
|
200
|
+
this.#disposed)
|
|
201
|
+
return;
|
|
202
|
+
const timer = setTimeout(() => {
|
|
203
|
+
if (entry.idleTimer !== timer || entry.current !== undefined || entry.release !== undefined)
|
|
204
|
+
return;
|
|
205
|
+
const sessionId = entry.runtime?.session.id;
|
|
206
|
+
void this.#release(entry, 'idle_expired').catch(() => this.#logCleanupFailure(sessionId));
|
|
207
|
+
}, this.resources.resourceIdleTtlMs);
|
|
208
|
+
entry.idleTimer = timer;
|
|
209
|
+
timer.unref();
|
|
210
|
+
}
|
|
211
|
+
#release(entry, reason) {
|
|
212
|
+
if (entry.release !== undefined)
|
|
213
|
+
return entry.release.result;
|
|
214
|
+
this.#clearTimer(entry);
|
|
215
|
+
const runId = entry.current?.runId;
|
|
216
|
+
const result = Promise.resolve().then(async () => {
|
|
217
|
+
try {
|
|
218
|
+
for (const user of entry.users)
|
|
219
|
+
user.cancel();
|
|
220
|
+
await entry.acquisition?.catch(() => undefined);
|
|
221
|
+
await Promise.all([...entry.users].map((user) => user.settled));
|
|
222
|
+
const runtime = entry.runtime;
|
|
223
|
+
delete entry.runtime;
|
|
224
|
+
if (runtime !== undefined)
|
|
225
|
+
await this.#destroyRuntime(runtime, entry.aliases, reason, runId);
|
|
226
|
+
else
|
|
227
|
+
await this.#releaseComputerAliases(entry.aliases);
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
this.#remove(entry);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
entry.release = { reason, result };
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
async #releaseComputerAliases(aliases) {
|
|
237
|
+
const results = await Promise.allSettled([...aliases].map((alias) => this.releaseComputerSession(alias)));
|
|
238
|
+
const failure = results.find((result) => result.status === 'rejected');
|
|
239
|
+
if (failure?.status === 'rejected')
|
|
240
|
+
throw failure.reason;
|
|
241
|
+
}
|
|
242
|
+
async #destroyRuntime(runtime, aliases, reason, runId, changedCategories = []) {
|
|
243
|
+
nodeLog('runner.mar-agent.runtime.releasing', {
|
|
244
|
+
runtimeId: runtime.runtimeId,
|
|
245
|
+
sessionId: runtime.session.id,
|
|
246
|
+
reason,
|
|
247
|
+
...(runId === undefined ? {} : { runId }),
|
|
248
|
+
...(changedCategories.length === 0 ? {} : { changedCategories })
|
|
249
|
+
});
|
|
250
|
+
const results = await Promise.allSettled([
|
|
251
|
+
this.#releaseComputerAliases(new Set([...aliases, runtime.session.id]))
|
|
252
|
+
]);
|
|
253
|
+
results.push(...(await Promise.allSettled([releaseSessionRuntime(runtime)])));
|
|
254
|
+
const failures = results.filter((result) => result.status === 'rejected');
|
|
255
|
+
if (failures.length > 1)
|
|
256
|
+
this.#logCleanupFailure(runtime.session.id);
|
|
257
|
+
if (failures[0]?.status === 'rejected')
|
|
258
|
+
throw failures[0].reason;
|
|
259
|
+
}
|
|
260
|
+
#remove(entry) {
|
|
261
|
+
for (const alias of entry.aliases)
|
|
262
|
+
if (this.#entries.get(alias) === entry)
|
|
263
|
+
this.#entries.delete(alias);
|
|
264
|
+
}
|
|
265
|
+
#logCleanupFailure(sessionId) {
|
|
266
|
+
nodeLog('runner.mar-agent.runtime.cleanup_failed', {
|
|
267
|
+
sessionId,
|
|
268
|
+
code: 'MAR_AGENT_RUNTIME_CLEANUP_FAILED'
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { access, constants } from 'node:fs/promises';
|
|
4
|
+
import { LocalAgentHost } from '@myagentroam/agent';
|
|
5
|
+
import { marAgentRuntimeCompactConfigurationSchema } from '@myagentroam/protocol';
|
|
6
|
+
import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
|
|
7
|
+
import { nodeLog } from '../../operational.js';
|
|
8
|
+
import { BUNDLED_RIPGREP_VERSION, bundledRipgrepPath, prependBundledRipgrepToPath } from '../../ripgrep-binary.js';
|
|
9
|
+
import { canonicalJson } from '../../util/canonical-json.js';
|
|
10
|
+
import { compactRuntimeConfigurations, marAgentCompactExtensionSource, updateMarAgentCompactState } from './compact-extensions.js';
|
|
11
|
+
import { MarAgentConversationNormalizer } from './conversation-normalizer.js';
|
|
12
|
+
import { sdkModelConfigurations } from './runtime-model-factory.js';
|
|
13
|
+
export const HOST_CONFIGURATION_CATEGORIES = [
|
|
14
|
+
'cwd',
|
|
15
|
+
'mcpServers',
|
|
16
|
+
'environment',
|
|
17
|
+
'guaranteedCommands'
|
|
18
|
+
];
|
|
19
|
+
export async function createSessionRuntime(input, sessionKey, fingerprint, hostConfiguration, dependencies, currentRunId) {
|
|
20
|
+
const sharedMcpServers = input.mcpServers.filter((server) => !isExecutionScopedMcp(server));
|
|
21
|
+
const mcpLease = await dependencies.mcpClients.acquire(sharedMcpServers, input.cwd, {
|
|
22
|
+
// Native Session IDs are assigned after the first lease; do not let that promotion
|
|
23
|
+
// change a confirmed actor's discovery identity when its warm runtime is recreated.
|
|
24
|
+
authorizationScope: input.secretEnvironment?.['MAR_USERNAME'] ? input.cwd : sessionKey
|
|
25
|
+
});
|
|
26
|
+
let agent;
|
|
27
|
+
let models = [];
|
|
28
|
+
let releaseEnvironment;
|
|
29
|
+
try {
|
|
30
|
+
releaseEnvironment = dependencies.retainEnvironment?.(input.secretEnvironment ?? {});
|
|
31
|
+
const mcp = mcpLease.pool;
|
|
32
|
+
const localHost = new LocalAgentHost({
|
|
33
|
+
modelUsage: (usage) => {
|
|
34
|
+
const runId = currentRunId();
|
|
35
|
+
if (runId !== undefined)
|
|
36
|
+
dependencies.modelUsage(runId, usage);
|
|
37
|
+
},
|
|
38
|
+
mode: 'node',
|
|
39
|
+
homeDirectory: homedir(),
|
|
40
|
+
workspace: input.cwd,
|
|
41
|
+
environment: hostConfiguration.environment,
|
|
42
|
+
guaranteedCommands: hostConfiguration.guaranteedCommands,
|
|
43
|
+
question: async (request, signal) => {
|
|
44
|
+
const runId = currentRunId();
|
|
45
|
+
return runId === undefined ? {} : dependencies.question(runId, request, signal);
|
|
46
|
+
},
|
|
47
|
+
approval: dependencies.approval
|
|
48
|
+
? (request, signal) => {
|
|
49
|
+
const runId = currentRunId();
|
|
50
|
+
if (runId === undefined)
|
|
51
|
+
return Promise.resolve('REJECTED');
|
|
52
|
+
return dependencies.approval(runId, request, signal);
|
|
53
|
+
}
|
|
54
|
+
: undefined,
|
|
55
|
+
mcpCatalog: () => mcp.catalog(),
|
|
56
|
+
callMcp: async (call, signal, context) => mcp.call(call.name, call.arguments, signal, context?.expectedTool),
|
|
57
|
+
resourceIdleTtlMs: dependencies.resourceIdleTtlMs
|
|
58
|
+
});
|
|
59
|
+
models = dependencies.modelFactory.create(compactRuntimeConfigurations(input.models, input.compact), {
|
|
60
|
+
...(input.runtimeScopeId === undefined ? {} : { runtimeScopeId: input.runtimeScopeId }),
|
|
61
|
+
executionId: input.runId
|
|
62
|
+
});
|
|
63
|
+
const compactState = { revision: 0 };
|
|
64
|
+
updateMarAgentCompactState(compactState, input.compact, models, input.models.map((model) => model.id));
|
|
65
|
+
agent = await dependencies.createAgent({
|
|
66
|
+
mode: 'node',
|
|
67
|
+
host: localHost,
|
|
68
|
+
models: sdkModelConfigurations(models.filter((model) => input.models.some((candidate) => candidate.id === model.id))),
|
|
69
|
+
defaultModelId: input.model.id,
|
|
70
|
+
runtimeExtensions: marAgentCompactExtensionSource(compactState),
|
|
71
|
+
...(input.personalInstructions ? { systemInstruction: input.personalInstructions } : {})
|
|
72
|
+
});
|
|
73
|
+
const customTitle = dependencies.customTitle(input.sessionId) ?? undefined;
|
|
74
|
+
const session = input.externalSessionId
|
|
75
|
+
? await agent.openSession(input.externalSessionId)
|
|
76
|
+
: await agent.createSession({
|
|
77
|
+
workspace: input.cwd,
|
|
78
|
+
...(customTitle === undefined ? {} : { title: customTitle })
|
|
79
|
+
});
|
|
80
|
+
const runtime = {
|
|
81
|
+
runtimeId: randomUUID(),
|
|
82
|
+
fingerprint,
|
|
83
|
+
agent,
|
|
84
|
+
session,
|
|
85
|
+
host: localHost,
|
|
86
|
+
normalizer: new MarAgentConversationNormalizer(),
|
|
87
|
+
mcpLease,
|
|
88
|
+
releaseEnvironment,
|
|
89
|
+
automaticTitleEnabled: input.externalSessionId === undefined,
|
|
90
|
+
models,
|
|
91
|
+
compactState,
|
|
92
|
+
modelFingerprint: sessionModelFingerprint(input),
|
|
93
|
+
personalInstructions: input.personalInstructions ?? ''
|
|
94
|
+
};
|
|
95
|
+
return runtime;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
for (const cleanup of [
|
|
99
|
+
() => agent?.dispose(),
|
|
100
|
+
() => closeModelCredentialProviders(models),
|
|
101
|
+
() => mcpLease.release(),
|
|
102
|
+
() => releaseEnvironment?.()
|
|
103
|
+
]) {
|
|
104
|
+
try {
|
|
105
|
+
await cleanup();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
nodeLog('runner.mar-agent.runtime.cleanup_failed', {
|
|
109
|
+
sessionId: sessionKey,
|
|
110
|
+
code: 'MAR_AGENT_RUNTIME_CLEANUP_FAILED',
|
|
111
|
+
stage: 'creation_rollback'
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export async function reconfigureSessionRuntime(existing, input, modelFactory) {
|
|
119
|
+
const modelFingerprint = sessionModelFingerprint(input);
|
|
120
|
+
const configurations = compactRuntimeConfigurations(input.models, input.compact);
|
|
121
|
+
if (existing.modelFingerprint !== modelFingerprint ||
|
|
122
|
+
configurations.some((model) => model.credential?.type === 'RUNTIME_TICKET' ||
|
|
123
|
+
model.imageGeneration?.credential?.type === 'RUNTIME_TICKET')) {
|
|
124
|
+
const models = modelFactory.create(configurations, {
|
|
125
|
+
...(input.runtimeScopeId === undefined ? {} : { runtimeScopeId: input.runtimeScopeId }),
|
|
126
|
+
executionId: input.runId
|
|
127
|
+
});
|
|
128
|
+
try {
|
|
129
|
+
await existing.agent.reconfigureModels({
|
|
130
|
+
models: sdkModelConfigurations(models.filter((model) => input.models.some((candidate) => candidate.id === model.id))),
|
|
131
|
+
defaultModelId: input.model.id
|
|
132
|
+
});
|
|
133
|
+
existing.models = models;
|
|
134
|
+
existing.modelFingerprint = modelFingerprint;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
await closeModelCredentialProviders(models);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const personalInstructions = input.personalInstructions ?? (input.mode === 'compact' ? existing.personalInstructions : '');
|
|
142
|
+
if (existing.personalInstructions !== personalInstructions) {
|
|
143
|
+
existing.agent.setSystemInstruction(personalInstructions || undefined);
|
|
144
|
+
existing.personalInstructions = personalInstructions;
|
|
145
|
+
}
|
|
146
|
+
updateMarAgentCompactState(existing.compactState, input.compact, existing.models, input.models.map((model) => model.id));
|
|
147
|
+
}
|
|
148
|
+
export async function releaseSessionRuntime(runtime) {
|
|
149
|
+
const failures = [];
|
|
150
|
+
for (const cleanup of [
|
|
151
|
+
() => runtime.agent.dispose(),
|
|
152
|
+
() => runtime.mcpLease.release(),
|
|
153
|
+
() => runtime.releaseEnvironment?.()
|
|
154
|
+
]) {
|
|
155
|
+
try {
|
|
156
|
+
await cleanup();
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
failures.push(error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (failures.length > 1)
|
|
163
|
+
nodeLog('runner.mar-agent.runtime.cleanup_failed', {
|
|
164
|
+
sessionId: runtime.session.id,
|
|
165
|
+
code: 'MAR_AGENT_RUNTIME_CLEANUP_FAILED',
|
|
166
|
+
stage: 'resource_release'
|
|
167
|
+
});
|
|
168
|
+
if (failures.length > 0)
|
|
169
|
+
throw failures[0];
|
|
170
|
+
}
|
|
171
|
+
export function isExecutionScopedMcp(server) {
|
|
172
|
+
return (server.lifecycle === 'EXECUTION' ||
|
|
173
|
+
(server.transport === 'STDIO' && server.environment?.['MAR_CTL_USER_TOKEN'] !== undefined));
|
|
174
|
+
}
|
|
175
|
+
export function sessionHostFingerprint(input, hostConfiguration) {
|
|
176
|
+
const configuration = {
|
|
177
|
+
cwd: input.cwd,
|
|
178
|
+
mcpServers: input.mcpServers.filter((server) => !isExecutionScopedMcp(server)),
|
|
179
|
+
environment: Object.entries(hostConfiguration.environment)
|
|
180
|
+
.filter((entry) => entry[1] !== undefined)
|
|
181
|
+
.sort(([left], [right]) => left.localeCompare(right)),
|
|
182
|
+
guaranteedCommands: Object.entries(hostConfiguration.guaranteedCommands).sort(([left], [right]) => left.localeCompare(right))
|
|
183
|
+
};
|
|
184
|
+
const digest = (value) => createHash('sha256').update(canonicalJson(value)).digest('hex');
|
|
185
|
+
return {
|
|
186
|
+
value: digest(configuration),
|
|
187
|
+
// Only fixed category names leave this comparison; never log these value fingerprints.
|
|
188
|
+
categories: {
|
|
189
|
+
cwd: digest(configuration.cwd),
|
|
190
|
+
mcpServers: digest(configuration.mcpServers),
|
|
191
|
+
environment: digest(configuration.environment),
|
|
192
|
+
guaranteedCommands: digest(configuration.guaranteedCommands)
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function sessionModelFingerprint(input) {
|
|
197
|
+
const compact = marAgentRuntimeCompactConfigurationSchema.safeParse(input.compact);
|
|
198
|
+
return createHash('sha256')
|
|
199
|
+
.update(JSON.stringify({
|
|
200
|
+
defaultModelId: input.model.id,
|
|
201
|
+
compact: compact.success
|
|
202
|
+
? {
|
|
203
|
+
model: runtimeModelFingerprint(compact.data.model),
|
|
204
|
+
effort: compact.data.reasoningEffort
|
|
205
|
+
}
|
|
206
|
+
: null,
|
|
207
|
+
models: input.models.map(runtimeModelFingerprint)
|
|
208
|
+
}))
|
|
209
|
+
.digest('hex');
|
|
210
|
+
}
|
|
211
|
+
export async function resolveMarAgentHostRuntimeConfiguration(secretEnvironment = {}) {
|
|
212
|
+
const environment = nodeExecutionEnvironment({
|
|
213
|
+
...secretEnvironment,
|
|
214
|
+
MAR_CTL_CONTEXT_REQUIRED: '1'
|
|
215
|
+
});
|
|
216
|
+
// CLI credentials are bound to each execution, never retained as Host configuration.
|
|
217
|
+
delete environment['MAR_CTL_USER_TOKEN'];
|
|
218
|
+
for (const key of [
|
|
219
|
+
'MAR_CTL_NODE_ID',
|
|
220
|
+
'MAR_CTL_WORKSPACE_ID',
|
|
221
|
+
'MAR_CTL_SESSION_ID',
|
|
222
|
+
'MAR_CTL_RUN_ID',
|
|
223
|
+
'MAR_CTL_RUNNER'
|
|
224
|
+
])
|
|
225
|
+
delete environment[key];
|
|
226
|
+
const ripgrep = await availableBundledRipgrep();
|
|
227
|
+
return {
|
|
228
|
+
environment: ripgrep === undefined ? environment : prependBundledRipgrepToPath(environment, ripgrep),
|
|
229
|
+
guaranteedCommands: ripgrep === undefined ? {} : { rg: BUNDLED_RIPGREP_VERSION }
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
async function availableBundledRipgrep() {
|
|
233
|
+
try {
|
|
234
|
+
const executable = bundledRipgrepPath();
|
|
235
|
+
await access(executable, process.platform === 'win32' ? constants.F_OK : constants.X_OK);
|
|
236
|
+
return executable;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function runtimeModelFingerprint(model) {
|
|
243
|
+
const { apiKey, credential, imageGeneration, ...configuration } = model;
|
|
244
|
+
return {
|
|
245
|
+
...configuration,
|
|
246
|
+
...(imageGeneration === undefined
|
|
247
|
+
? {}
|
|
248
|
+
: {
|
|
249
|
+
imageGeneration: {
|
|
250
|
+
...imageGeneration,
|
|
251
|
+
credential: runtimeCredentialFingerprint(imageGeneration.credential)
|
|
252
|
+
}
|
|
253
|
+
}),
|
|
254
|
+
...(apiKey === undefined
|
|
255
|
+
? {}
|
|
256
|
+
: { apiKeyHash: createHash('sha256').update(apiKey).digest('hex') }),
|
|
257
|
+
credential: runtimeCredentialFingerprint(credential)
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function runtimeCredentialFingerprint(credential) {
|
|
261
|
+
return credential?.type === 'INLINE_API_KEY'
|
|
262
|
+
? {
|
|
263
|
+
type: credential.type,
|
|
264
|
+
valueHash: createHash('sha256').update(credential.value).digest('hex')
|
|
265
|
+
}
|
|
266
|
+
: credential === undefined
|
|
267
|
+
? undefined
|
|
268
|
+
: {
|
|
269
|
+
type: credential.type,
|
|
270
|
+
credentialId: credential.credentialId
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
async function closeModelCredentialProviders(models) {
|
|
274
|
+
await Promise.allSettled([
|
|
275
|
+
...new Set(models.flatMap((model) => [
|
|
276
|
+
...(model.credentialProvider === undefined ? [] : [model.credentialProvider]),
|
|
277
|
+
...(model.imageGeneration?.credentialProvider === undefined
|
|
278
|
+
? []
|
|
279
|
+
: [model.imageGeneration.credentialProvider])
|
|
280
|
+
]))
|
|
281
|
+
].map((provider) => provider.close()));
|
|
282
|
+
}
|