@myagentroam/node 0.9.139 → 0.9.141
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/claude-agent-sdk.js +1 -0
- package/dist/connector.js +6 -9
- package/dist/{marmgr.js → marctl.js} +1 -1
- package/dist/marnodes.js +1 -1
- package/dist/marscp.js +1 -1
- package/dist/marssh.js +1 -1
- package/dist/runner/abstract-runner.js +7 -7
- package/dist/runner/claude/managed-run-controller.js +60 -6
- package/dist/runner/codex/managed-run-controller.js +41 -4
- package/dist/runner/mar-agent/conversation-normalizer.js +11 -4
- package/dist/runner/mar-agent/managed-run-controller.js +15 -4
- package/dist/runner/mar-agent/mcp-client-pool.js +37 -5
- package/dist/runner/opencode/managed-run-controller.js +66 -2
- package/dist/runtime-state.js +28 -0
- package/dist/service/native-session-projection-service.js +12 -4
- package/dist/service/response-latency-tracker.js +47 -0
- package/dist/service/run-event-service.js +1 -1
- package/dist/service/session-catalog-service.js +5 -2
- package/dist/service/terminal-service.js +1 -1
- package/dist/service/workspace-file-service.js +4 -10
- package/dist/service/workspace-session-service.js +75 -20
- package/dist/workspace.js +10 -11
- package/package.json +6 -6
package/dist/claude-agent-sdk.js
CHANGED
|
@@ -49,6 +49,7 @@ export class ClaudeAgentSdkAdapter {
|
|
|
49
49
|
prompt,
|
|
50
50
|
options: {
|
|
51
51
|
cwd: input.cwd,
|
|
52
|
+
...(channelEnabled ? { includePartialMessages: true } : {}),
|
|
52
53
|
// Claude SDK defaults CLAUDE_CODE_ENTRYPOINT to sdk-ts. Keep the
|
|
53
54
|
// transcript discoverable by the same VS Code session filters used by
|
|
54
55
|
// the external Claude Code extension.
|
package/dist/connector.js
CHANGED
|
@@ -306,17 +306,12 @@ export class NodeConnector {
|
|
|
306
306
|
workspaceState = new WorkspaceDomainState();
|
|
307
307
|
workspaceWatchService = new WorkspaceWatchService(this.workspaceState, (workspaceId) => this.workspaceSessionService.ensureWatch(workspaceId), (workspaceId) => this.workspaceSessionService.stopWatch(workspaceId), (workspaceId) => this.workspaceWorkbenchService?.releaseWorkspace(workspaceId));
|
|
308
308
|
workspaceSessionService = new WorkspaceSessionService(this.workspaceState, {
|
|
309
|
-
discover: (workspaceId) => this.discoverWorkspaceSessions(workspaceId),
|
|
309
|
+
discover: (workspaceId, onRunnerDiscovered) => this.discoverWorkspaceSessions(workspaceId, onRunnerDiscovered),
|
|
310
310
|
listPage: (workspaceId) => this.executeNodeOperation('session.list', { workspaceId, limit: 20 }),
|
|
311
311
|
active: (workspaceId) => this.workspaceWatchService.topicActive(workspaceId, 'sessions'),
|
|
312
|
-
invalidate: (workspaceId) => void this.workspaceWorkbenchService?.invalidateTopic(workspaceId, 'sessions')
|
|
313
|
-
onPending: (workspaceId, waitMs) => nodeLog('native.session.discovery.pending', {
|
|
314
|
-
nodeId: this.config?.nodeId,
|
|
315
|
-
workspaceId,
|
|
316
|
-
waitMs
|
|
317
|
-
})
|
|
312
|
+
invalidate: (workspaceId) => void this.workspaceWorkbenchService?.invalidateTopic(workspaceId, 'sessions')
|
|
318
313
|
});
|
|
319
|
-
workspaceFileService = new WorkspaceFileService(this.workspaceState, (
|
|
314
|
+
workspaceFileService = new WorkspaceFileService(this.workspaceState, (workspaceId) => {
|
|
320
315
|
this.workspaceWorkbenchService?.invalidateWorkspaceFiles(workspaceId);
|
|
321
316
|
this.workspaceCoordinator.invalidate(workspaceId);
|
|
322
317
|
});
|
|
@@ -462,6 +457,7 @@ export class NodeConnector {
|
|
|
462
457
|
sequence: this.workbenchEventService.snapshotSequence(),
|
|
463
458
|
session: this.sessionPresentationService.present(session),
|
|
464
459
|
runs: this.runtime.listRunsForSession(session.id),
|
|
460
|
+
performance: this.runtime.sessionPerformance(session.id),
|
|
465
461
|
queue: this.workspaceQueueWorkbenchService.snapshot(session.id),
|
|
466
462
|
context,
|
|
467
463
|
conversation: this.conversationSegmentService.projectInitial(session, page, limit)
|
|
@@ -1130,6 +1126,7 @@ export class NodeConnector {
|
|
|
1130
1126
|
projection: this.nativeSessionProjectionService,
|
|
1131
1127
|
cachedNative: () => [...this.sessionState.directNative.values()],
|
|
1132
1128
|
cachedWorkspace: (workspaceId) => this.workspaceCoordinator.cachedSessions(workspaceId),
|
|
1129
|
+
discoveryPending: (workspaceId) => this.workspaceSessionService.discoveryPending(workspaceId),
|
|
1133
1130
|
discoverWorkspace: (workspaceId) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId),
|
|
1134
1131
|
present: (session) => this.sessionPresentationService.present(session),
|
|
1135
1132
|
projectionForNative: (session) => this.nativeSessionProjectionService.projectionForNative(session),
|
|
@@ -2184,7 +2181,7 @@ export class NodeConnector {
|
|
|
2184
2181
|
return;
|
|
2185
2182
|
this.workbenchEventService.publish(event);
|
|
2186
2183
|
};
|
|
2187
|
-
discoverWorkspaceSessions = (workspaceId) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId);
|
|
2184
|
+
discoverWorkspaceSessions = (workspaceId, onRunnerDiscovered) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId, onRunnerDiscovered);
|
|
2188
2185
|
invalidateWorkspaceWatchTopics = (workspaceId) => this.workspaceCoordinator.invalidate(workspaceId);
|
|
2189
2186
|
reconcilePersistedRuns = () => undefined;
|
|
2190
2187
|
startCapabilityRefresh = () => this.connectionLifecycle.startCapabilityRefresh();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Keep the CLI implementation in its own package; the Node install exposes the same entrypoint.
|
|
3
|
-
const entrypoint = '@myagentroam/
|
|
3
|
+
const entrypoint = '@myagentroam/marctl/bin/marctl.mjs';
|
|
4
4
|
await import(entrypoint);
|
|
5
5
|
export {};
|
package/dist/marnodes.js
CHANGED
package/dist/marscp.js
CHANGED
package/dist/marssh.js
CHANGED
|
@@ -172,17 +172,17 @@ export class AbstractRunner {
|
|
|
172
172
|
function resolvedMcpServers(raw, secrets) {
|
|
173
173
|
if (!Array.isArray(raw))
|
|
174
174
|
return [];
|
|
175
|
-
const
|
|
175
|
+
const marctlEnvironment = {
|
|
176
176
|
MAR_USERNAME: secrets['MAR_USERNAME'] ?? '',
|
|
177
|
-
...(secrets['
|
|
177
|
+
...(secrets['MAR_CTL_SERVER_URL'] === undefined
|
|
178
178
|
? {}
|
|
179
|
-
: {
|
|
180
|
-
...(secrets['
|
|
179
|
+
: { MAR_CTL_SERVER_URL: secrets['MAR_CTL_SERVER_URL'] }),
|
|
180
|
+
...(secrets['MAR_CTL_USER_TOKEN'] === undefined
|
|
181
181
|
? {}
|
|
182
|
-
: {
|
|
183
|
-
|
|
182
|
+
: { MAR_CTL_USER_TOKEN: secrets['MAR_CTL_USER_TOKEN'] }),
|
|
183
|
+
MAR_CTL_CONTEXT_REQUIRED: '1'
|
|
184
184
|
};
|
|
185
|
-
const mcpEnvironment = (provided) => nodeExecutionEnvironment({ ...provided, ...
|
|
185
|
+
const mcpEnvironment = (provided) => nodeExecutionEnvironment({ ...provided, ...marctlEnvironment }, { PATH: process.env['PATH'], Path: process.env['Path'] });
|
|
186
186
|
const result = [];
|
|
187
187
|
for (const value of raw) {
|
|
188
188
|
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -7,6 +7,7 @@ import { claudeContentBlocks } from '../../util/node-operation-parsers.js';
|
|
|
7
7
|
import { parseSecretEnvironment } from '../../util/secret-environment.js';
|
|
8
8
|
import { safeErrorCode } from '../../util/safe-error.js';
|
|
9
9
|
import { runnerErrorProjection } from '../../util/runner-error.js';
|
|
10
|
+
import { ResponseLatencyTracker } from '../../service/response-latency-tracker.js';
|
|
10
11
|
export class ClaudeManagedRunController {
|
|
11
12
|
runner;
|
|
12
13
|
host;
|
|
@@ -162,6 +163,13 @@ export class ClaudeManagedRunController {
|
|
|
162
163
|
};
|
|
163
164
|
try {
|
|
164
165
|
const gate = new ClaudeChannelGate(sessionId);
|
|
166
|
+
const responseLatency = new ResponseLatencyTracker();
|
|
167
|
+
let responseMessageId;
|
|
168
|
+
const observeResponse = (id, incremental = false) => {
|
|
169
|
+
const latencyMs = responseLatency.response(id, incremental);
|
|
170
|
+
if (latencyMs !== undefined)
|
|
171
|
+
this.host.emit(runId, 'run.performance', { latencyMs });
|
|
172
|
+
};
|
|
165
173
|
handle = this.runner.startChannel({
|
|
166
174
|
prompt: payload.input,
|
|
167
175
|
cwd,
|
|
@@ -173,7 +181,7 @@ export class ClaudeManagedRunController {
|
|
|
173
181
|
...(typeof payload.access === 'string' ? { access: payload.access } : {}),
|
|
174
182
|
environment: nodeExecutionEnvironment({
|
|
175
183
|
...secretEnvironment,
|
|
176
|
-
|
|
184
|
+
MAR_CTL_CONTEXT_REQUIRED: '1'
|
|
177
185
|
}, { PATH: process.env['PATH'], Path: process.env['Path'] }),
|
|
178
186
|
...(typeof payload.personalInstructions === 'string'
|
|
179
187
|
? { personalInstructions: payload.personalInstructions }
|
|
@@ -191,12 +199,19 @@ export class ClaudeManagedRunController {
|
|
|
191
199
|
dataBase64: attachment.dataBase64
|
|
192
200
|
}))
|
|
193
201
|
}),
|
|
194
|
-
onPermission: (toolName, toolInput, options) =>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
onPermission: (toolName, toolInput, options) => {
|
|
203
|
+
const latencyMs = responseLatency.toolStarted(options.toolUseID);
|
|
204
|
+
if (latencyMs !== undefined)
|
|
205
|
+
this.host.emit(runId, 'run.performance', { latencyMs });
|
|
206
|
+
return toolName === 'AskUserQuestion'
|
|
207
|
+
? (toolKinds.set(options.toolUseID, 'user_input_request'),
|
|
208
|
+
this.host.requestUserInput(runId, toolInput, options.toolUseID))
|
|
209
|
+
: this.host.requestApproval(runId, toolName, toolInput);
|
|
210
|
+
},
|
|
198
211
|
onChannelReply: (content) => this.host.emit(runId, 'channel.reply', { content }),
|
|
199
212
|
onMessage: (message) => {
|
|
213
|
+
if (terminal)
|
|
214
|
+
return;
|
|
200
215
|
const record = message;
|
|
201
216
|
const nativeSessionId = typeof record.session_id === 'string' ? record.session_id : undefined;
|
|
202
217
|
const current = this.runner.execution.runs.get(runId);
|
|
@@ -221,8 +236,43 @@ export class ClaudeManagedRunController {
|
|
|
221
236
|
});
|
|
222
237
|
}
|
|
223
238
|
}
|
|
239
|
+
if (record.type === 'stream_event') {
|
|
240
|
+
// Observe semantic timing without forwarding raw thinking or input fragments.
|
|
241
|
+
const event = record.event;
|
|
242
|
+
const block = event?.content_block;
|
|
243
|
+
const delta = event?.delta;
|
|
244
|
+
if (event?.type === 'message_start') {
|
|
245
|
+
const message = event.message;
|
|
246
|
+
responseMessageId = typeof message?.id === 'string' ? message.id : undefined;
|
|
247
|
+
}
|
|
248
|
+
if (event?.type === 'content_block_start' &&
|
|
249
|
+
block?.type === 'tool_use' &&
|
|
250
|
+
typeof block.id === 'string') {
|
|
251
|
+
const latencyMs = responseLatency.toolStarted(block.id);
|
|
252
|
+
if (latencyMs !== undefined)
|
|
253
|
+
this.host.emit(runId, 'run.performance', { latencyMs });
|
|
254
|
+
// A later full snapshot of this streamed message is not a new response.
|
|
255
|
+
responseLatency.response(responseMessageId);
|
|
256
|
+
}
|
|
257
|
+
if ((event?.type === 'content_block_start' && block?.type === 'thinking') ||
|
|
258
|
+
(event?.type === 'content_block_delta' &&
|
|
259
|
+
((delta?.type === 'text_delta' &&
|
|
260
|
+
typeof delta.text === 'string' &&
|
|
261
|
+
delta.text.length > 0) ||
|
|
262
|
+
(delta?.type === 'thinking_delta' &&
|
|
263
|
+
typeof delta.thinking === 'string' &&
|
|
264
|
+
delta.thinking.length > 0) ||
|
|
265
|
+
(delta?.type === 'input_json_delta' &&
|
|
266
|
+
typeof delta.partial_json === 'string' &&
|
|
267
|
+
delta.partial_json.length > 0))))
|
|
268
|
+
observeResponse(responseMessageId, event?.type === 'content_block_delta');
|
|
269
|
+
}
|
|
224
270
|
if (record.type === 'assistant') {
|
|
225
271
|
const text = extractClaudeText(record);
|
|
272
|
+
if (text.length > 0 || extractClaudeThinkingSummaries(record).length > 0)
|
|
273
|
+
observeResponse(typeof record.message?.id === 'string'
|
|
274
|
+
? record.message.id
|
|
275
|
+
: undefined);
|
|
226
276
|
const planText = claudePlanText(text);
|
|
227
277
|
if (planText !== undefined)
|
|
228
278
|
this.host.emitItem(runId, {
|
|
@@ -242,6 +292,9 @@ export class ClaudeManagedRunController {
|
|
|
242
292
|
});
|
|
243
293
|
}
|
|
244
294
|
for (const tool of extractClaudeToolUses(record)) {
|
|
295
|
+
const latencyMs = responseLatency.toolStarted(tool.id);
|
|
296
|
+
if (latencyMs !== undefined)
|
|
297
|
+
this.host.emit(runId, 'run.performance', { latencyMs });
|
|
245
298
|
if (tool.name === 'AskUserQuestion') {
|
|
246
299
|
toolKinds.set(tool.id, 'user_input_request');
|
|
247
300
|
continue;
|
|
@@ -305,6 +358,7 @@ export class ClaudeManagedRunController {
|
|
|
305
358
|
}
|
|
306
359
|
if (record.type === 'user') {
|
|
307
360
|
for (const result of extractClaudeToolResults(record)) {
|
|
361
|
+
responseLatency.toolCompleted(result.toolUseId);
|
|
308
362
|
const kind = toolKinds.get(result.toolUseId) ?? 'tool_call';
|
|
309
363
|
if (kind === 'user_input_request')
|
|
310
364
|
continue;
|
|
@@ -336,7 +390,7 @@ export class ClaudeManagedRunController {
|
|
|
336
390
|
Number.isFinite(value) &&
|
|
337
391
|
value >= 0 &&
|
|
338
392
|
Number.isSafeInteger(Math.round(value)));
|
|
339
|
-
if (nativeTtft !== undefined)
|
|
393
|
+
if (nativeTtft !== undefined && !responseLatency.hasObservedResponse)
|
|
340
394
|
this.host.emit(runId, 'run.performance', { latencyMs: Math.round(nativeTtft) });
|
|
341
395
|
const subtype = typeof record.subtype === 'string' ? record.subtype : 'unknown';
|
|
342
396
|
const completedNativeSessionId = this.runner.execution.runs.get(runId)?.nativeSessionId ?? nativeSessionId;
|
|
@@ -3,6 +3,7 @@ import { extractId } from '../../util/runner-native-session-parsers.js';
|
|
|
3
3
|
import { parseSecretEnvironment } from '../../util/secret-environment.js';
|
|
4
4
|
import { safeErrorCode } from '../../util/safe-error.js';
|
|
5
5
|
import { runnerErrorProjection } from '../../util/runner-error.js';
|
|
6
|
+
import { ResponseLatencyTracker } from '../../service/response-latency-tracker.js';
|
|
6
7
|
import { boundedConversationItemId, codexImageAttachments, codexLiveConversationItem, codexUserInputQuestions, compactRunnerText, isPlainRecord } from './conversation-parser.js';
|
|
7
8
|
export function resolveCodexNotificationRun(runs, input) {
|
|
8
9
|
const entries = [...runs.entries()];
|
|
@@ -66,7 +67,7 @@ export class CodexManagedRunController {
|
|
|
66
67
|
throw new Error('RUNNER_COMMAND_UNAVAILABLE');
|
|
67
68
|
if (command.id === 'compact') {
|
|
68
69
|
const runId = crypto.randomUUID();
|
|
69
|
-
this.runner.prepareEnvironment(nodeExecutionEnvironment({ ...context.secretEnvironment,
|
|
70
|
+
this.runner.prepareEnvironment(nodeExecutionEnvironment({ ...context.secretEnvironment, MAR_CTL_CONTEXT_REQUIRED: '1' }, { PATH: process.env['PATH'], Path: process.env['Path'] }), runId);
|
|
70
71
|
try {
|
|
71
72
|
await this.runner.start(runId);
|
|
72
73
|
return await this.compact(session, threadId, runId, context.initiatedByUserId);
|
|
@@ -199,6 +200,15 @@ export class CodexManagedRunController {
|
|
|
199
200
|
text: params.delta,
|
|
200
201
|
...(itemId === undefined ? {} : { itemId })
|
|
201
202
|
});
|
|
203
|
+
if (params.delta.length > 0)
|
|
204
|
+
this.observeResponse(runId, itemId, true);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (notification.method === 'item/reasoning/textDelta' && typeof params.delta === 'string') {
|
|
208
|
+
if (params.delta.length > 0)
|
|
209
|
+
this.observeResponse(runId, typeof params.itemId === 'string'
|
|
210
|
+
? boundedConversationItemId('codex', params.itemId)
|
|
211
|
+
: undefined, true);
|
|
202
212
|
return;
|
|
203
213
|
}
|
|
204
214
|
if (notification.method === 'item/reasoning/summaryTextDelta' &&
|
|
@@ -215,6 +225,8 @@ export class CodexManagedRunController {
|
|
|
215
225
|
merge: true,
|
|
216
226
|
append: 'sections'
|
|
217
227
|
});
|
|
228
|
+
if (params.delta.length > 0)
|
|
229
|
+
this.observeResponse(runId, boundedConversationItemId('codex', params.itemId), true);
|
|
218
230
|
return;
|
|
219
231
|
}
|
|
220
232
|
if (notification.method === 'item/commandExecution/outputDelta' &&
|
|
@@ -246,8 +258,24 @@ export class CodexManagedRunController {
|
|
|
246
258
|
isPlainRecord(params.item)) {
|
|
247
259
|
const attachments = this.host.appendImages(runId, codexImageAttachments(params.item));
|
|
248
260
|
const item = codexLiveConversationItem(params.item, notification.method === 'item/completed', attachments);
|
|
249
|
-
if (item !== undefined)
|
|
261
|
+
if (item !== undefined) {
|
|
250
262
|
this.host.emitItem(runId, item);
|
|
263
|
+
const tracker = activeRun.responseLatency;
|
|
264
|
+
const tool = ['tool_call', 'command_execution', 'file_change'].includes(item.kind);
|
|
265
|
+
if (tool) {
|
|
266
|
+
if (notification.method === 'item/completed')
|
|
267
|
+
tracker?.toolCompleted(item.itemId);
|
|
268
|
+
else {
|
|
269
|
+
const latencyMs = tracker?.toolStarted(item.itemId);
|
|
270
|
+
if (latencyMs !== undefined)
|
|
271
|
+
this.host.emit(runId, 'run.performance', { latencyMs });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else if (['assistant_message', 'reasoning_summary', 'plan'].includes(item.kind)) {
|
|
275
|
+
// A started reasoning item is itself a semantic response, even before its summary.
|
|
276
|
+
this.observeResponse(runId, item.itemId);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
251
279
|
return;
|
|
252
280
|
}
|
|
253
281
|
if (notification.method === 'turn/completed') {
|
|
@@ -273,6 +301,13 @@ export class CodexManagedRunController {
|
|
|
273
301
|
this.host.removeActive(runId);
|
|
274
302
|
}
|
|
275
303
|
}
|
|
304
|
+
observeResponse(runId, itemId, incremental = false) {
|
|
305
|
+
const latencyMs = this.runner.execution.runs
|
|
306
|
+
.get(runId)
|
|
307
|
+
?.responseLatency?.response(itemId, incremental);
|
|
308
|
+
if (latencyMs !== undefined)
|
|
309
|
+
this.host.emit(runId, 'run.performance', { latencyMs });
|
|
310
|
+
}
|
|
276
311
|
requestApproval(runId, notification) {
|
|
277
312
|
const approvalId = `codex:${runId}:${notification.requestId}`;
|
|
278
313
|
const expiresAt = Date.now() + 10 * 60_000;
|
|
@@ -386,7 +421,7 @@ export class CodexManagedRunController {
|
|
|
386
421
|
return;
|
|
387
422
|
}
|
|
388
423
|
try {
|
|
389
|
-
this.runner.prepareEnvironment(nodeExecutionEnvironment({ ...secretEnvironment,
|
|
424
|
+
this.runner.prepareEnvironment(nodeExecutionEnvironment({ ...secretEnvironment, MAR_CTL_CONTEXT_REQUIRED: '1' }, { PATH: process.env['PATH'], Path: process.env['Path'] }), runId);
|
|
390
425
|
}
|
|
391
426
|
catch (error) {
|
|
392
427
|
this.host.emit(runId, 'run.rejected', { code: safeErrorCode(error, 'SECRET_ENVIRONMENT_INVALID') }, 'FAILED');
|
|
@@ -458,7 +493,8 @@ export class CodexManagedRunController {
|
|
|
458
493
|
threadId,
|
|
459
494
|
cwd,
|
|
460
495
|
goalMcp: hasGoalMcp(payload.mcpServers),
|
|
461
|
-
scheduleMcp: hasScheduleMcp(payload.mcpServers)
|
|
496
|
+
scheduleMcp: hasScheduleMcp(payload.mcpServers),
|
|
497
|
+
responseLatency: new ResponseLatencyTracker()
|
|
462
498
|
});
|
|
463
499
|
turnResult = await this.runner.startTurn(threadId, input, cwd, turnConfiguration, materializedAttachments, runId);
|
|
464
500
|
break;
|
|
@@ -480,6 +516,7 @@ export class CodexManagedRunController {
|
|
|
480
516
|
}
|
|
481
517
|
const turnId = extractId(turnResult, 'turn');
|
|
482
518
|
this.runner.execution.runs.set(runId, {
|
|
519
|
+
...this.runner.execution.runs.get(runId),
|
|
483
520
|
sessionId: publicSessionId,
|
|
484
521
|
threadId,
|
|
485
522
|
turnId,
|
|
@@ -141,9 +141,14 @@ export class MarAgentConversationNormalizer {
|
|
|
141
141
|
}
|
|
142
142
|
#toolStarted(event) {
|
|
143
143
|
const ownItemId = itemId('tool', event.callId);
|
|
144
|
-
if (event.toolName === 'exec') {
|
|
144
|
+
if (event.toolName === 'exec' || event.toolName === 'exec_wait') {
|
|
145
145
|
const input = record(event.input) ?? {};
|
|
146
|
-
const action = input.action
|
|
146
|
+
const action = input.action ??
|
|
147
|
+
(event.toolName === 'exec_wait'
|
|
148
|
+
? 'poll'
|
|
149
|
+
: typeof input.command === 'string'
|
|
150
|
+
? 'start'
|
|
151
|
+
: undefined);
|
|
147
152
|
if (action === 'start' && typeof input.command === 'string') {
|
|
148
153
|
const command = {
|
|
149
154
|
itemId: ownItemId,
|
|
@@ -227,7 +232,8 @@ export class MarAgentConversationNormalizer {
|
|
|
227
232
|
}
|
|
228
233
|
#toolCompleted(event) {
|
|
229
234
|
const state = this.#tools.get(event.callId);
|
|
230
|
-
if (event.toolName === 'exec'
|
|
235
|
+
if ((event.toolName === 'exec' || event.toolName === 'exec_wait') &&
|
|
236
|
+
state?.commandOrigin !== undefined) {
|
|
231
237
|
const command = this.#commands.get(state.commandOrigin);
|
|
232
238
|
const result = execResult(event.data, event.summary, event.truncated === true);
|
|
233
239
|
if (command !== undefined && result !== undefined) {
|
|
@@ -293,7 +299,8 @@ export class MarAgentConversationNormalizer {
|
|
|
293
299
|
}
|
|
294
300
|
#toolFailed(event) {
|
|
295
301
|
const state = this.#tools.get(event.callId);
|
|
296
|
-
if (event.toolName === 'exec'
|
|
302
|
+
if ((event.toolName === 'exec' || event.toolName === 'exec_wait') &&
|
|
303
|
+
state?.commandOrigin !== undefined) {
|
|
297
304
|
const command = this.#commands.get(state.commandOrigin);
|
|
298
305
|
const input = record(state.input);
|
|
299
306
|
if (command !== undefined &&
|
|
@@ -273,7 +273,9 @@ export class MarAgentManagedRunController {
|
|
|
273
273
|
throw new Error('MAR_AGENT_SESSION_RUNTIME_ACTIVE');
|
|
274
274
|
runtime.activeRunId = input.runId;
|
|
275
275
|
await runtime.mcpLease.pool.refreshForExecution();
|
|
276
|
-
const executionMcpServers = input.mcpServers
|
|
276
|
+
const executionMcpServers = input.mcpServers
|
|
277
|
+
.filter(isExecutionScopedMcp)
|
|
278
|
+
.map((server) => ({ ...server, lifecycle: 'EXECUTION' }));
|
|
277
279
|
computerUseBinding = await this.#computerUse?.bind({
|
|
278
280
|
runId: input.runId,
|
|
279
281
|
sessionId: runtime.session.id,
|
|
@@ -311,6 +313,9 @@ export class MarAgentManagedRunController {
|
|
|
311
313
|
});
|
|
312
314
|
executionMcpBinding = runtime.host.bindExecutionExtensions({
|
|
313
315
|
...imageExtensions,
|
|
316
|
+
environment: input.secretEnvironment?.['MAR_CTL_USER_TOKEN'] === undefined
|
|
317
|
+
? {}
|
|
318
|
+
: { MAR_CTL_USER_TOKEN: input.secretEnvironment['MAR_CTL_USER_TOKEN'] },
|
|
314
319
|
skills: [...(imageExtensions.skills ?? []), computerUseHostSkill]
|
|
315
320
|
}, {
|
|
316
321
|
tools: [...nodeTools, ...userTools],
|
|
@@ -500,7 +505,7 @@ export class MarAgentManagedRunController {
|
|
|
500
505
|
async #createRuntime(input, sessionKey, fingerprint, hostConfiguration) {
|
|
501
506
|
const dynamicCredentials = input.models.some((model) => model.credential?.type === 'RUNTIME_TICKET' ||
|
|
502
507
|
model.imageGeneration?.credential?.type === 'RUNTIME_TICKET');
|
|
503
|
-
const sharedMcpServers = input.mcpServers.filter((server) => server
|
|
508
|
+
const sharedMcpServers = input.mcpServers.filter((server) => !isExecutionScopedMcp(server));
|
|
504
509
|
const mcpLease = await this.#mcpClients.acquire(sharedMcpServers, input.cwd, {
|
|
505
510
|
// Native Session IDs are assigned after the first lease; do not let that promotion
|
|
506
511
|
// change a confirmed actor's discovery identity when its warm runtime is recreated.
|
|
@@ -706,6 +711,10 @@ function contextUsageChanged(event) {
|
|
|
706
711
|
(event.type === 'usage.updated' &&
|
|
707
712
|
(event.contextInputTokens !== undefined || event.inputTokens !== undefined)));
|
|
708
713
|
}
|
|
714
|
+
function isExecutionScopedMcp(server) {
|
|
715
|
+
return (server.lifecycle === 'EXECUTION' ||
|
|
716
|
+
(server.transport === 'STDIO' && server.environment?.['MAR_CTL_USER_TOKEN'] !== undefined));
|
|
717
|
+
}
|
|
709
718
|
function sessionRuntimeFingerprint(input, hostConfiguration) {
|
|
710
719
|
return createHash('sha256')
|
|
711
720
|
.update(JSON.stringify({
|
|
@@ -713,7 +722,7 @@ function sessionRuntimeFingerprint(input, hostConfiguration) {
|
|
|
713
722
|
defaultModelId: input.model.id,
|
|
714
723
|
models: input.models.map(runtimeModelFingerprint),
|
|
715
724
|
personalInstructions: input.personalInstructions ?? '',
|
|
716
|
-
mcpServers: input.mcpServers.filter((server) => server
|
|
725
|
+
mcpServers: input.mcpServers.filter((server) => !isExecutionScopedMcp(server)),
|
|
717
726
|
environment: Object.entries(hostConfiguration.environment)
|
|
718
727
|
.filter((entry) => entry[1] !== undefined)
|
|
719
728
|
.sort(([left], [right]) => left.localeCompare(right)),
|
|
@@ -727,8 +736,10 @@ export function allowsUntrustedWebFetchTls(environment) {
|
|
|
727
736
|
async function resolveMarAgentHostRuntimeConfiguration(secretEnvironment = {}) {
|
|
728
737
|
const environment = nodeExecutionEnvironment({
|
|
729
738
|
...secretEnvironment,
|
|
730
|
-
|
|
739
|
+
MAR_CTL_CONTEXT_REQUIRED: '1'
|
|
731
740
|
});
|
|
741
|
+
// CLI credentials are bound to each execution, never retained as Host configuration.
|
|
742
|
+
delete environment['MAR_CTL_USER_TOKEN'];
|
|
732
743
|
const ripgrep = await availableBundledRipgrep();
|
|
733
744
|
return {
|
|
734
745
|
environment: ripgrep === undefined ? environment : prependBundledRipgrepToPath(environment, ripgrep),
|
|
@@ -188,17 +188,49 @@ export class MarAgentMcpClientPool {
|
|
|
188
188
|
: this.#controller.signal
|
|
189
189
|
});
|
|
190
190
|
const items = Array.isArray(result.content) ? result.content : [];
|
|
191
|
+
const images = [];
|
|
192
|
+
const remaining = [];
|
|
191
193
|
const content = items
|
|
192
194
|
.map((item) => {
|
|
193
|
-
if (item === null || typeof item !== 'object')
|
|
195
|
+
if (item === null || typeof item !== 'object') {
|
|
196
|
+
remaining.push(item);
|
|
194
197
|
return '[unknown]';
|
|
198
|
+
}
|
|
195
199
|
const value = item;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
200
|
+
if (value.type === 'text' && typeof value.text === 'string') {
|
|
201
|
+
const { text, ...metadata } = value;
|
|
202
|
+
if (Object.keys(metadata).some((key) => key !== 'type'))
|
|
203
|
+
remaining.push(metadata);
|
|
204
|
+
return text;
|
|
205
|
+
}
|
|
206
|
+
if (value.type === 'image' &&
|
|
207
|
+
typeof value.mimeType === 'string' &&
|
|
208
|
+
typeof value.data === 'string') {
|
|
209
|
+
// Native MCP pixels belong only in the SDK image channel, not duplicated JSON text.
|
|
210
|
+
images.push({ mimeType: value.mimeType, dataBase64: value.data });
|
|
211
|
+
const { data: _pixels, ...metadata } = value;
|
|
212
|
+
if (Object.keys(metadata).some((key) => key !== 'type' && key !== 'mimeType'))
|
|
213
|
+
remaining.push(metadata);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
remaining.push(item);
|
|
217
|
+
}
|
|
218
|
+
return `[${String(value.type ?? 'unknown')}]`;
|
|
199
219
|
})
|
|
200
220
|
.join('\n');
|
|
201
|
-
|
|
221
|
+
const structured = result.structuredContent;
|
|
222
|
+
const data = structured !== undefined && structured !== null
|
|
223
|
+
? remaining.length
|
|
224
|
+
? { structuredContent: structured, content: remaining }
|
|
225
|
+
: structured
|
|
226
|
+
: remaining.length
|
|
227
|
+
? remaining
|
|
228
|
+
: undefined;
|
|
229
|
+
return {
|
|
230
|
+
content,
|
|
231
|
+
...(data === undefined ? {} : { data }),
|
|
232
|
+
...(images.length ? { images } : {})
|
|
233
|
+
};
|
|
202
234
|
}
|
|
203
235
|
async close() {
|
|
204
236
|
if (this.#closed)
|
|
@@ -4,6 +4,7 @@ import { runnerErrorProjection } from '../../util/runner-error.js';
|
|
|
4
4
|
import { boundedConversationItemId, compactRunnerText } from '../codex/conversation-parser.js';
|
|
5
5
|
import { openCodePatchToolPayload, openCodeQuestions } from './conversation-parser.js';
|
|
6
6
|
import { normalizeOpenCodeTool } from './tool-normalizer.js';
|
|
7
|
+
import { ResponseLatencyTracker } from '../../service/response-latency-tracker.js';
|
|
7
8
|
export class OpenCodeManagedRunController {
|
|
8
9
|
runner;
|
|
9
10
|
host;
|
|
@@ -50,7 +51,7 @@ export class OpenCodeManagedRunController {
|
|
|
50
51
|
this.host.hasActiveSessionLease(session.id))
|
|
51
52
|
throw new Error('RUNNER_COMMAND_UNAVAILABLE');
|
|
52
53
|
const runId = crypto.randomUUID();
|
|
53
|
-
const client = this.runner.managedClient(nodeExecutionEnvironment({ ...environment,
|
|
54
|
+
const client = this.runner.managedClient(nodeExecutionEnvironment({ ...environment, MAR_CTL_CONTEXT_REQUIRED: '1' }, { PATH: process.env['PATH'], Path: process.env['Path'] }));
|
|
54
55
|
this.host.adopt({
|
|
55
56
|
runId,
|
|
56
57
|
sessionId: session.id,
|
|
@@ -111,7 +112,7 @@ export class OpenCodeManagedRunController {
|
|
|
111
112
|
try {
|
|
112
113
|
environment = nodeExecutionEnvironment({
|
|
113
114
|
...parseSecretEnvironment(payload.secretEnvironment),
|
|
114
|
-
|
|
115
|
+
MAR_CTL_CONTEXT_REQUIRED: '1'
|
|
115
116
|
}, { PATH: process.env['PATH'], Path: process.env['Path'] });
|
|
116
117
|
mcpSecretEnvironment = parseSecretEnvironment(payload.mcpSecretEnvironment);
|
|
117
118
|
mcp = this.runner.mcpConfiguration(payload.mcpInstallations, {
|
|
@@ -265,6 +266,13 @@ export class OpenCodeManagedRunController {
|
|
|
265
266
|
const attachmentPaths = input.attachments.length === 0
|
|
266
267
|
? []
|
|
267
268
|
: await this.host.persistAttachments(input.cwd, input.runId, input.attachments);
|
|
269
|
+
const execution = this.runner.execution.get(input.runId);
|
|
270
|
+
if (execution !== undefined) {
|
|
271
|
+
execution.responseLatency = new ResponseLatencyTracker();
|
|
272
|
+
execution.responsePartKinds = new Map();
|
|
273
|
+
execution.responseStartedAt = Date.now();
|
|
274
|
+
execution.responseMessages = new Set();
|
|
275
|
+
}
|
|
268
276
|
await client.prompt({
|
|
269
277
|
directory: input.cwd,
|
|
270
278
|
sessionID: nativeSessionId,
|
|
@@ -312,6 +320,13 @@ export class OpenCodeManagedRunController {
|
|
|
312
320
|
const roles = this.messageRoles.get(runId) ?? new Map();
|
|
313
321
|
roles.set(info.id, info.role);
|
|
314
322
|
this.messageRoles.set(runId, roles);
|
|
323
|
+
const execution = this.runner.execution.get(runId);
|
|
324
|
+
const created = record(info.time)?.created;
|
|
325
|
+
if (info.role === 'assistant' &&
|
|
326
|
+
execution?.responseStartedAt !== undefined &&
|
|
327
|
+
typeof created === 'number' &&
|
|
328
|
+
created >= execution.responseStartedAt)
|
|
329
|
+
execution.responseMessages?.add(info.id);
|
|
315
330
|
}
|
|
316
331
|
if (info?.role === 'assistant' &&
|
|
317
332
|
typeof info.id === 'string' &&
|
|
@@ -334,6 +349,19 @@ export class OpenCodeManagedRunController {
|
|
|
334
349
|
}
|
|
335
350
|
return;
|
|
336
351
|
}
|
|
352
|
+
if (event?.type === 'message.part.delta') {
|
|
353
|
+
const kind = typeof properties?.partID === 'string'
|
|
354
|
+
? this.runner.execution.get(runId)?.responsePartKinds?.get(properties.partID)
|
|
355
|
+
: undefined;
|
|
356
|
+
if ((kind === 'text' || kind === 'reasoning') &&
|
|
357
|
+
properties?.field === 'text' &&
|
|
358
|
+
typeof properties.delta === 'string' &&
|
|
359
|
+
properties.delta.length > 0 &&
|
|
360
|
+
typeof properties.messageID === 'string' &&
|
|
361
|
+
this.currentResponseMessage(runId, properties.messageID))
|
|
362
|
+
this.observeResponse(runId, boundedConversationItemId('opencode', properties.partID), true);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
337
365
|
if (event?.type === 'message.part.updated') {
|
|
338
366
|
const part = record(properties?.part);
|
|
339
367
|
if (part?.type === 'compaction' &&
|
|
@@ -356,9 +384,32 @@ export class OpenCodeManagedRunController {
|
|
|
356
384
|
const parentId = this.messageParents.get(runId)?.get(part.messageID);
|
|
357
385
|
if (parentId !== undefined && this.compactionPartIds.get(runId)?.has(parentId))
|
|
358
386
|
return;
|
|
387
|
+
if (typeof part.id === 'string' && typeof part.type === 'string')
|
|
388
|
+
this.runner.execution.get(runId)?.responsePartKinds?.set(part.id, part.type);
|
|
359
389
|
const item = openCodePart(part, this.runner.execution.get(runId)?.agent === 'plan');
|
|
360
390
|
if (item !== undefined)
|
|
361
391
|
this.host.emitItem(runId, item);
|
|
392
|
+
if (typeof part.id === 'string') {
|
|
393
|
+
const itemId = boundedConversationItemId('opencode', part.id);
|
|
394
|
+
const tracker = this.currentResponseMessage(runId, part.messageID)
|
|
395
|
+
? this.runner.execution.get(runId)?.responseLatency
|
|
396
|
+
: undefined;
|
|
397
|
+
if (part.type === 'tool') {
|
|
398
|
+
const state = record(part.state);
|
|
399
|
+
if (state?.status === 'completed' || state?.status === 'error')
|
|
400
|
+
tracker?.toolCompleted(itemId);
|
|
401
|
+
else if (state?.status === 'pending' || state?.status === 'running') {
|
|
402
|
+
const latencyMs = tracker?.toolStarted(itemId);
|
|
403
|
+
if (latencyMs !== undefined)
|
|
404
|
+
this.host.emit(runId, 'run.performance', { latencyMs });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else if (tracker !== undefined &&
|
|
408
|
+
(part.type === 'text' || part.type === 'reasoning') &&
|
|
409
|
+
typeof part.text === 'string' &&
|
|
410
|
+
part.text.length > 0)
|
|
411
|
+
this.observeResponse(runId, itemId);
|
|
412
|
+
}
|
|
362
413
|
return;
|
|
363
414
|
}
|
|
364
415
|
if (event?.type === 'permission.asked')
|
|
@@ -366,6 +417,19 @@ export class OpenCodeManagedRunController {
|
|
|
366
417
|
if (event?.type === 'question.asked')
|
|
367
418
|
this.requestQuestion(runId, properties);
|
|
368
419
|
}
|
|
420
|
+
currentResponseMessage(runId, messageId) {
|
|
421
|
+
const parent = this.messageParents.get(runId)?.get(messageId);
|
|
422
|
+
return (this.runner.execution.get(runId)?.responseMessages?.has(messageId) === true &&
|
|
423
|
+
this.messageRoles.get(runId)?.get(messageId) === 'assistant' &&
|
|
424
|
+
(parent === undefined || !this.compactionPartIds.get(runId)?.has(parent)));
|
|
425
|
+
}
|
|
426
|
+
observeResponse(runId, itemId, incremental = false) {
|
|
427
|
+
const latencyMs = this.runner.execution
|
|
428
|
+
.get(runId)
|
|
429
|
+
?.responseLatency?.response(itemId, incremental);
|
|
430
|
+
if (latencyMs !== undefined)
|
|
431
|
+
this.host.emit(runId, 'run.performance', { latencyMs });
|
|
432
|
+
}
|
|
369
433
|
requestPermission(runId, properties) {
|
|
370
434
|
const execution = this.runner.execution.get(runId);
|
|
371
435
|
const requestId = typeof properties?.id === 'string' ? properties.id : undefined;
|
package/dist/runtime-state.js
CHANGED
|
@@ -13,6 +13,7 @@ export class NodeRuntimeState {
|
|
|
13
13
|
static MAX_COMPLETED_TURNS_PER_SESSION = 10;
|
|
14
14
|
sessions = new Map();
|
|
15
15
|
runs = new Map();
|
|
16
|
+
runPerformance = new Map();
|
|
16
17
|
recentUserMessages = new Map();
|
|
17
18
|
messageSubmissions = new Map();
|
|
18
19
|
failedMessageSubmissions = new Map();
|
|
@@ -553,6 +554,28 @@ export class NodeRuntimeState {
|
|
|
553
554
|
const canonical = this.executions.get(id)?.runId ?? id;
|
|
554
555
|
return this.runs.get(canonical);
|
|
555
556
|
}
|
|
557
|
+
/** Only the current execution may replace an active Run's ephemeral metrics. */
|
|
558
|
+
updateRunPerformance(id, performance) {
|
|
559
|
+
const run = this.getRun(id);
|
|
560
|
+
if (run === undefined || terminal(run.status) || performance.runId !== run.id)
|
|
561
|
+
return false;
|
|
562
|
+
if (id !== run.id && this.currentExecutionId(run.id) !== id)
|
|
563
|
+
return false;
|
|
564
|
+
this.runPerformance.set(run.id, {
|
|
565
|
+
...this.runPerformance.get(run.id),
|
|
566
|
+
runId: performance.runId,
|
|
567
|
+
...(performance.latencyMs === undefined ? {} : { latencyMs: performance.latencyMs }),
|
|
568
|
+
...(performance.tokensPerSecond === undefined
|
|
569
|
+
? {}
|
|
570
|
+
: { tokensPerSecond: performance.tokensPerSecond })
|
|
571
|
+
});
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
sessionPerformance(sessionId) {
|
|
575
|
+
const run = this.activeSessionRun(sessionId);
|
|
576
|
+
const performance = run === undefined ? undefined : this.runPerformance.get(run.id);
|
|
577
|
+
return performance === undefined ? undefined : { ...performance };
|
|
578
|
+
}
|
|
556
579
|
findMessageRun(sessionId, clientMessageId) {
|
|
557
580
|
const submission = this.messageSubmissions.get(`${sessionId}\u0000${clientMessageId}`);
|
|
558
581
|
return submission?.kind === 'run' ? this.runs.get(submission.id) : undefined;
|
|
@@ -599,6 +622,8 @@ export class NodeRuntimeState {
|
|
|
599
622
|
updatedAt: now
|
|
600
623
|
};
|
|
601
624
|
this.runs.set(runId, value);
|
|
625
|
+
if (terminal(next))
|
|
626
|
+
this.runPerformance.delete(runId);
|
|
602
627
|
this.emitRunChanged(value, this.requireSession(value.sessionId));
|
|
603
628
|
if (terminal(next) && this.leases.get(value.sessionId) === value.id)
|
|
604
629
|
this.leases.delete(value.sessionId);
|
|
@@ -894,6 +919,7 @@ export class NodeRuntimeState {
|
|
|
894
919
|
}
|
|
895
920
|
this.pruneRetainedExecutionTurns(run.sessionId, retainedTurnIds);
|
|
896
921
|
this.runs.delete(runId);
|
|
922
|
+
this.runPerformance.delete(runId);
|
|
897
923
|
this.recentUserMessages.delete(runId);
|
|
898
924
|
this.runSequences.delete(runId);
|
|
899
925
|
for (const [key, candidate] of this.messageSubmissions)
|
|
@@ -931,6 +957,7 @@ export class NodeRuntimeState {
|
|
|
931
957
|
this.leases.delete(sessionId);
|
|
932
958
|
for (const runId of runIds) {
|
|
933
959
|
this.runs.delete(runId);
|
|
960
|
+
this.runPerformance.delete(runId);
|
|
934
961
|
this.recentUserMessages.delete(runId);
|
|
935
962
|
this.runSequences.delete(runId);
|
|
936
963
|
}
|
|
@@ -994,6 +1021,7 @@ export class NodeRuntimeState {
|
|
|
994
1021
|
}
|
|
995
1022
|
for (const runId of runIds) {
|
|
996
1023
|
this.runs.delete(runId);
|
|
1024
|
+
this.runPerformance.delete(runId);
|
|
997
1025
|
this.recentUserMessages.delete(runId);
|
|
998
1026
|
this.runSequences.delete(runId);
|
|
999
1027
|
}
|
|
@@ -33,12 +33,20 @@ export class NativeSessionProjectionService {
|
|
|
33
33
|
this.remember(sessions);
|
|
34
34
|
return sessions.map(this.options.present);
|
|
35
35
|
}
|
|
36
|
-
async discoverWorkspace(workspaceId) {
|
|
37
|
-
const discovered = await Promise.
|
|
36
|
+
async discoverWorkspace(workspaceId, onRunnerDiscovered) {
|
|
37
|
+
const discovered = await Promise.allSettled(this.options.runners
|
|
38
38
|
.advertised()
|
|
39
39
|
.filter((runner) => runner.available(this.options.capabilities()))
|
|
40
|
-
.map((runner) =>
|
|
41
|
-
|
|
40
|
+
.map(async (runner) => {
|
|
41
|
+
const name = runner.name;
|
|
42
|
+
const sessions = await this.discover(name, workspaceId);
|
|
43
|
+
onRunnerDiscovered?.(name, sessions);
|
|
44
|
+
return sessions;
|
|
45
|
+
}));
|
|
46
|
+
const failed = discovered.find((result) => result.status === 'rejected');
|
|
47
|
+
if (failed?.status === 'rejected')
|
|
48
|
+
throw failed.reason;
|
|
49
|
+
const sessions = deduplicateDirectSessions(discovered.flatMap((result) => (result.status === 'fulfilled' ? result.value : [])));
|
|
42
50
|
this.remember(sessions);
|
|
43
51
|
return sessions;
|
|
44
52
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Monotonic, observation-only timing; native event semantics belong to each Runner. */
|
|
2
|
+
export class ResponseLatencyTracker {
|
|
3
|
+
now;
|
|
4
|
+
waitingAt;
|
|
5
|
+
pendingTools = new Set();
|
|
6
|
+
completedTools = new Set();
|
|
7
|
+
observedResponses = new Set();
|
|
8
|
+
hasObservedResponse = false;
|
|
9
|
+
constructor(now = () => performance.now()) {
|
|
10
|
+
this.now = now;
|
|
11
|
+
this.waitingAt = now();
|
|
12
|
+
}
|
|
13
|
+
response(id, incremental = false) {
|
|
14
|
+
if (id !== undefined) {
|
|
15
|
+
// A new delta is semantic output even when the Runner reuses a response item.
|
|
16
|
+
if (!incremental && this.observedResponses.has(id))
|
|
17
|
+
return undefined;
|
|
18
|
+
this.observedResponses.add(id);
|
|
19
|
+
if (this.observedResponses.size > 1_024)
|
|
20
|
+
this.observedResponses.delete(this.observedResponses.values().next().value);
|
|
21
|
+
}
|
|
22
|
+
if (this.waitingAt === undefined || this.pendingTools.size > 0)
|
|
23
|
+
return undefined;
|
|
24
|
+
const latencyMs = Math.max(0, Math.round(this.now() - this.waitingAt));
|
|
25
|
+
this.waitingAt = undefined;
|
|
26
|
+
this.hasObservedResponse = true;
|
|
27
|
+
return latencyMs;
|
|
28
|
+
}
|
|
29
|
+
toolStarted(id) {
|
|
30
|
+
if (this.completedTools.has(id) || this.pendingTools.has(id))
|
|
31
|
+
return undefined;
|
|
32
|
+
const latencyMs = this.response(id);
|
|
33
|
+
this.pendingTools.add(id);
|
|
34
|
+
this.waitingAt = undefined;
|
|
35
|
+
return latencyMs;
|
|
36
|
+
}
|
|
37
|
+
toolCompleted(id) {
|
|
38
|
+
if (this.completedTools.has(id))
|
|
39
|
+
return;
|
|
40
|
+
this.completedTools.add(id);
|
|
41
|
+
if (this.completedTools.size > 1_024)
|
|
42
|
+
this.completedTools.delete(this.completedTools.values().next().value);
|
|
43
|
+
this.pendingTools.delete(id);
|
|
44
|
+
if (this.pendingTools.size === 0)
|
|
45
|
+
this.waitingAt = this.now();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -15,7 +15,7 @@ export class RunEventService {
|
|
|
15
15
|
if (run === undefined || !isPlainRecord(payload))
|
|
16
16
|
return;
|
|
17
17
|
const parsed = runPerformanceSchema.safeParse({ ...payload, runId: run.id });
|
|
18
|
-
if (parsed.success) {
|
|
18
|
+
if (parsed.success && this.options.runtime.updateRunPerformance(runId, parsed.data)) {
|
|
19
19
|
this.flushText(runId);
|
|
20
20
|
this.options.emitWorkbench('conversation', {
|
|
21
21
|
sessionId: run.sessionId,
|
|
@@ -57,7 +57,7 @@ export class SessionCatalogService {
|
|
|
57
57
|
this.options.projectionForNative(candidate) === undefined)
|
|
58
58
|
].map((session) => [session.id, session]));
|
|
59
59
|
if (workspaceId !== undefined)
|
|
60
|
-
this.mergePersisted(merged, workspaceId);
|
|
60
|
+
this.mergePersisted(merged, workspaceId, query === undefined && this.options.discoveryPending(workspaceId));
|
|
61
61
|
const sessions = [...merged.values()].filter((session) => {
|
|
62
62
|
if (query === undefined)
|
|
63
63
|
return true;
|
|
@@ -70,7 +70,7 @@ export class SessionCatalogService {
|
|
|
70
70
|
? sessions
|
|
71
71
|
: sessions.filter((session) => session.workspaceId === workspaceId), input);
|
|
72
72
|
}
|
|
73
|
-
mergePersisted(merged, workspaceId) {
|
|
73
|
+
mergePersisted(merged, workspaceId, discoveryPending) {
|
|
74
74
|
const database = this.options.database();
|
|
75
75
|
const workspace = database.getWorkspace(workspaceId);
|
|
76
76
|
if (workspace === undefined)
|
|
@@ -78,6 +78,9 @@ export class SessionCatalogService {
|
|
|
78
78
|
for (const persisted of database.listSessionRecords(workspaceId)) {
|
|
79
79
|
if (merged.has(persisted.id))
|
|
80
80
|
continue;
|
|
81
|
+
// A metadata record alone does not establish a discovered native Session.
|
|
82
|
+
if (discoveryPending)
|
|
83
|
+
continue;
|
|
81
84
|
merged.set(persisted.id, this.options.present({
|
|
82
85
|
id: persisted.id,
|
|
83
86
|
nodeId: this.options.nodeId(),
|
|
@@ -61,7 +61,7 @@ export class TerminalService {
|
|
|
61
61
|
const options = {
|
|
62
62
|
...input,
|
|
63
63
|
workspaceId: typeof input.workspaceId === 'string' ? input.workspaceId : null,
|
|
64
|
-
environment: { ...secretEnvironment,
|
|
64
|
+
environment: { ...secretEnvironment, MAR_CTL_CONTEXT_REQUIRED: '1' },
|
|
65
65
|
...(cwd === undefined ? {} : { cwd })
|
|
66
66
|
};
|
|
67
67
|
const terminal = commandMode
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import { tmpdir } from 'node:os';
|
|
2
1
|
import { isAbsolute, relative, resolve, win32 } from 'node:path';
|
|
3
2
|
import { realpath } from 'node:fs/promises';
|
|
4
|
-
import { listWorkspaceFiles, mutateWorkspaceFile, normalizeFilePreviewPath,
|
|
3
|
+
import { listWorkspaceFiles, mutateWorkspaceFile, normalizeFilePreviewPath, readAbsoluteFileContentRange, readWorkspaceFileContentRange, readAbsoluteTextFile, readWorkspaceTextFile, writeWorkspaceTextFile, WorkspaceFileIndex } from '../workspace.js';
|
|
5
4
|
const INDEX_CACHE_LIMIT = 32;
|
|
6
5
|
export class WorkspaceFileService {
|
|
7
6
|
state;
|
|
8
|
-
allowedRoots;
|
|
9
7
|
onInvalidated;
|
|
10
8
|
mutationTails = new Map();
|
|
11
|
-
constructor(state,
|
|
9
|
+
constructor(state, onInvalidated) {
|
|
12
10
|
this.state = state;
|
|
13
|
-
this.allowedRoots = allowedRoots;
|
|
14
11
|
this.onInvalidated = onInvalidated;
|
|
15
12
|
}
|
|
16
13
|
async list(workspace, input) {
|
|
@@ -38,7 +35,7 @@ export class WorkspaceFileService {
|
|
|
38
35
|
if (workspacePath !== workspace.path && isFilePreviewAbsolutePath(path))
|
|
39
36
|
throw new Error('FILE_PATH_ESCAPE');
|
|
40
37
|
return isFilePreviewAbsolutePath(path)
|
|
41
|
-
?
|
|
38
|
+
? readAbsoluteTextFile(path, offset, limit)
|
|
42
39
|
: readWorkspaceTextFile(workspacePath, path, offset, limit);
|
|
43
40
|
}
|
|
44
41
|
async readContent(workspace, input, workspacePath = workspace.path) {
|
|
@@ -69,7 +66,7 @@ export class WorkspaceFileService {
|
|
|
69
66
|
const offset = typeof input.offset === 'number' ? input.offset : 0;
|
|
70
67
|
const limit = typeof input.limit === 'number' ? input.limit : 512 * 1024;
|
|
71
68
|
return isFilePreviewAbsolutePath(path)
|
|
72
|
-
?
|
|
69
|
+
? readAbsoluteFileContentRange(path, offset, limit)
|
|
73
70
|
: readWorkspaceFileContentRange(workspacePath, path, offset, limit);
|
|
74
71
|
}
|
|
75
72
|
readDirect(workspace, input, workspacePath = workspace.path) {
|
|
@@ -218,9 +215,6 @@ export class WorkspaceFileService {
|
|
|
218
215
|
index.dispose();
|
|
219
216
|
this.state.fileIndexes.clear();
|
|
220
217
|
}
|
|
221
|
-
previewRoots() {
|
|
222
|
-
return [...new Set([...this.allowedRoots(), tmpdir()])];
|
|
223
|
-
}
|
|
224
218
|
}
|
|
225
219
|
function isFilePreviewAbsolutePath(path) {
|
|
226
220
|
return isAbsolute(path) || win32.isAbsolute(path);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { nodeLog } from '../operational.js';
|
|
1
2
|
const WATCH_INTERVAL_MS = 30_000;
|
|
2
3
|
const CACHE_TTL_MS = 15_000;
|
|
3
|
-
const INITIAL_WAIT_MS = 500;
|
|
4
4
|
export class WorkspaceSessionService {
|
|
5
5
|
state;
|
|
6
6
|
options;
|
|
7
7
|
cacheGenerations = new Map();
|
|
8
|
+
initialReads = new Map();
|
|
9
|
+
incompleteDiscoveries = new Set();
|
|
8
10
|
constructor(state, options) {
|
|
9
11
|
this.state = state;
|
|
10
12
|
this.options = options;
|
|
@@ -22,32 +24,90 @@ export class WorkspaceSessionService {
|
|
|
22
24
|
this.state.sessionWatchTimers.delete(workspaceId);
|
|
23
25
|
this.state.sessionWatchSignatures.delete(workspaceId);
|
|
24
26
|
}
|
|
27
|
+
discoveryPending(workspaceId) {
|
|
28
|
+
// Partial cold batches still await native identity confirmation for the remaining Runners.
|
|
29
|
+
return this.incompleteDiscoveries.has(workspaceId);
|
|
30
|
+
}
|
|
25
31
|
async cached(workspaceId) {
|
|
26
32
|
const cached = this.state.sessionCache.get(workspaceId);
|
|
27
33
|
if (cached !== undefined && cached.expiresAt > Date.now())
|
|
28
34
|
return cached.sessions;
|
|
29
|
-
let initialDiscoveryTimedOut = false;
|
|
30
35
|
let discovery = this.state.sessionCacheReads.get(workspaceId);
|
|
31
36
|
if (discovery === undefined) {
|
|
32
37
|
const generation = this.cacheGenerations.get(workspaceId) ?? 0;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
let resolveInitial;
|
|
39
|
+
let rejectInitial;
|
|
40
|
+
const initial = new Promise((resolve, reject) => {
|
|
41
|
+
resolveInitial = resolve;
|
|
42
|
+
rejectInitial = reject;
|
|
43
|
+
});
|
|
44
|
+
if (cached === undefined) {
|
|
45
|
+
this.initialReads.set(workspaceId, initial);
|
|
46
|
+
this.incompleteDiscoveries.add(workspaceId);
|
|
47
|
+
}
|
|
48
|
+
// A cached caller does not await this promise, but failures still belong to the read.
|
|
49
|
+
void initial.catch(() => undefined);
|
|
50
|
+
let acceptingBatches = true;
|
|
51
|
+
let batchPublished = false;
|
|
52
|
+
let notifyCompletion = false;
|
|
53
|
+
const publish = (sessions, notify = true) => {
|
|
36
54
|
if ((this.cacheGenerations.get(workspaceId) ?? 0) !== generation)
|
|
37
|
-
return
|
|
55
|
+
return;
|
|
38
56
|
const previous = this.state.sessionCache.get(workspaceId)?.sessions;
|
|
39
57
|
this.state.sessionCache.set(workspaceId, {
|
|
40
58
|
sessions,
|
|
41
59
|
expiresAt: Date.now() + CACHE_TTL_MS
|
|
42
60
|
});
|
|
43
|
-
if (
|
|
44
|
-
|
|
61
|
+
if (notify &&
|
|
62
|
+
previous !== undefined &&
|
|
63
|
+
sessionSignature(previous) !== sessionSignature(sessions))
|
|
45
64
|
this.options.invalidate(workspaceId);
|
|
65
|
+
};
|
|
66
|
+
const read = this.options
|
|
67
|
+
.discover(workspaceId, (runner, sessions) => {
|
|
68
|
+
if (!acceptingBatches || (this.cacheGenerations.get(workspaceId) ?? 0) !== generation)
|
|
69
|
+
return;
|
|
70
|
+
const previous = this.state.sessionCache.get(workspaceId)?.sessions;
|
|
71
|
+
const merged = [
|
|
72
|
+
...(previous ?? []).filter((session) => session.runner !== runner),
|
|
73
|
+
...sessions
|
|
74
|
+
];
|
|
75
|
+
// An empty early Runner must not turn an undiscovered Workspace into an empty list.
|
|
76
|
+
if (previous === undefined && merged.length === 0)
|
|
77
|
+
return;
|
|
78
|
+
publish(merged);
|
|
79
|
+
batchPublished = true;
|
|
80
|
+
resolveInitial(merged);
|
|
81
|
+
})
|
|
82
|
+
.then((sessions) => {
|
|
83
|
+
if ((this.cacheGenerations.get(workspaceId) ?? 0) === generation)
|
|
84
|
+
this.incompleteDiscoveries.delete(workspaceId);
|
|
85
|
+
const previous = this.state.sessionCache.get(workspaceId)?.sessions;
|
|
86
|
+
notifyCompletion =
|
|
87
|
+
batchPublished ||
|
|
88
|
+
(previous !== undefined && sessionSignature(previous) !== sessionSignature(sessions));
|
|
89
|
+
publish(sessions, false);
|
|
90
|
+
resolveInitial(sessions);
|
|
46
91
|
return sessions;
|
|
92
|
+
})
|
|
93
|
+
.catch((error) => {
|
|
94
|
+
rejectInitial(error);
|
|
95
|
+
if ((this.cacheGenerations.get(workspaceId) ?? 0) === generation) {
|
|
96
|
+
nodeLog('native.session.discovery.failed', { workspaceId });
|
|
97
|
+
const partial = this.state.sessionCache.get(workspaceId);
|
|
98
|
+
if (partial !== undefined)
|
|
99
|
+
this.state.sessionCache.set(workspaceId, { ...partial, expiresAt: 0 });
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
47
102
|
})
|
|
48
103
|
.finally(() => {
|
|
104
|
+
acceptingBatches = false;
|
|
49
105
|
if (this.state.sessionCacheReads.get(workspaceId) === read)
|
|
50
106
|
this.state.sessionCacheReads.delete(workspaceId);
|
|
107
|
+
if (this.initialReads.get(workspaceId) === initial)
|
|
108
|
+
this.initialReads.delete(workspaceId);
|
|
109
|
+
if (notifyCompletion && (this.cacheGenerations.get(workspaceId) ?? 0) === generation)
|
|
110
|
+
this.options.invalidate(workspaceId);
|
|
51
111
|
if (!this.state.sessionCacheReads.has(workspaceId) &&
|
|
52
112
|
!this.state.sessionCache.has(workspaceId) &&
|
|
53
113
|
(this.cacheGenerations.get(workspaceId) ?? 0) !== generation)
|
|
@@ -55,15 +115,12 @@ export class WorkspaceSessionService {
|
|
|
55
115
|
});
|
|
56
116
|
discovery = read;
|
|
57
117
|
this.state.sessionCacheReads.set(workspaceId, discovery);
|
|
118
|
+
// Later Runner failures cannot become unhandled after the first batch was returned.
|
|
119
|
+
void discovery.catch(() => undefined);
|
|
58
120
|
}
|
|
59
121
|
if (cached !== undefined)
|
|
60
122
|
return cached.sessions;
|
|
61
|
-
|
|
62
|
-
if (discovered !== undefined)
|
|
63
|
-
return discovered;
|
|
64
|
-
initialDiscoveryTimedOut = true;
|
|
65
|
-
this.options.onPending?.(workspaceId, INITIAL_WAIT_MS);
|
|
66
|
-
return [];
|
|
123
|
+
return this.initialReads.get(workspaceId) ?? discovery;
|
|
67
124
|
}
|
|
68
125
|
clear() {
|
|
69
126
|
for (const timer of this.state.sessionWatchTimers.values())
|
|
@@ -78,6 +135,8 @@ export class WorkspaceSessionService {
|
|
|
78
135
|
this.bumpCacheGeneration(workspaceId);
|
|
79
136
|
this.state.sessionCache.clear();
|
|
80
137
|
this.state.sessionCacheReads.clear();
|
|
138
|
+
this.initialReads.clear();
|
|
139
|
+
this.incompleteDiscoveries.clear();
|
|
81
140
|
}
|
|
82
141
|
invalidate(workspaceId) {
|
|
83
142
|
this.invalidateCache(workspaceId);
|
|
@@ -87,6 +146,8 @@ export class WorkspaceSessionService {
|
|
|
87
146
|
this.bumpCacheGeneration(workspaceId);
|
|
88
147
|
this.state.sessionCache.delete(workspaceId);
|
|
89
148
|
this.state.sessionCacheReads.delete(workspaceId);
|
|
149
|
+
this.initialReads.delete(workspaceId);
|
|
150
|
+
this.incompleteDiscoveries.delete(workspaceId);
|
|
90
151
|
}
|
|
91
152
|
bumpCacheGeneration(workspaceId) {
|
|
92
153
|
this.cacheGenerations.set(workspaceId, (this.cacheGenerations.get(workspaceId) ?? 0) + 1);
|
|
@@ -146,9 +207,3 @@ function sessionSignature(sessions) {
|
|
|
146
207
|
session.externalDiscovered
|
|
147
208
|
]));
|
|
148
209
|
}
|
|
149
|
-
async function within(promise, timeoutMs) {
|
|
150
|
-
return Promise.race([
|
|
151
|
-
promise,
|
|
152
|
-
new Promise((resolve) => setTimeout(resolve, timeoutMs))
|
|
153
|
-
]);
|
|
154
|
-
}
|
package/dist/workspace.js
CHANGED
|
@@ -757,15 +757,15 @@ export async function readWorkspaceFileContentRange(workspaceRoot, requestedPath
|
|
|
757
757
|
await handle.close();
|
|
758
758
|
}
|
|
759
759
|
}
|
|
760
|
-
/** Reads one absolute file range
|
|
761
|
-
export async function
|
|
760
|
+
/** Reads one regular absolute file range using the Node account's filesystem permissions. */
|
|
761
|
+
export async function readAbsoluteFileContentRange(requestedPath, offset = 0, limit = FILE_READ_LIMIT) {
|
|
762
762
|
if (!Number.isSafeInteger(offset) ||
|
|
763
763
|
offset < 0 ||
|
|
764
764
|
!Number.isSafeInteger(limit) ||
|
|
765
765
|
limit < 1 ||
|
|
766
766
|
limit > FILE_READ_LIMIT)
|
|
767
767
|
throw new Error('FILE_RANGE_INVALID');
|
|
768
|
-
const { file, metadata } = await
|
|
768
|
+
const { file, metadata } = await resolveAbsoluteFile(requestedPath);
|
|
769
769
|
if (offset > metadata.size)
|
|
770
770
|
throw new Error('FILE_RANGE_INVALID');
|
|
771
771
|
const length = Math.min(limit, metadata.size - offset);
|
|
@@ -792,9 +792,9 @@ function fileSourceRevision(metadata) {
|
|
|
792
792
|
.update([metadata.dev, metadata.ino, metadata.size, metadata.mtimeMs, metadata.ctimeMs].join(':'))
|
|
793
793
|
.digest('hex');
|
|
794
794
|
}
|
|
795
|
-
/** Reads one absolute file
|
|
796
|
-
export async function
|
|
797
|
-
const { file, metadata } = await
|
|
795
|
+
/** Reads one regular absolute file using the Node account's filesystem permissions. */
|
|
796
|
+
export async function readAbsoluteTextFile(requestedPath, offset = 0, limit = FILE_READ_LIMIT) {
|
|
797
|
+
const { file, metadata } = await resolveAbsoluteFile(requestedPath);
|
|
798
798
|
if (metadata.size > FILE_READ_LIMIT * 16)
|
|
799
799
|
throw new Error('FILE_TOO_LARGE');
|
|
800
800
|
if (!Number.isSafeInteger(offset) ||
|
|
@@ -859,7 +859,7 @@ export function normalizeFilePreviewPath(requestedPath) {
|
|
|
859
859
|
throw new Error('FILE_PATH_ESCAPE');
|
|
860
860
|
return normalized;
|
|
861
861
|
}
|
|
862
|
-
async function
|
|
862
|
+
async function resolveAbsoluteFile(requestedPath) {
|
|
863
863
|
if (!isAbsolute(requestedPath))
|
|
864
864
|
throw new Error('FILE_PATH_ESCAPE');
|
|
865
865
|
const file = await realpath(requestedPath).catch((error) => {
|
|
@@ -867,9 +867,6 @@ async function resolveAllowedFile(requestedPath, allowedRoots) {
|
|
|
867
867
|
throw new Error('FILE_NOT_FOUND');
|
|
868
868
|
throw error;
|
|
869
869
|
});
|
|
870
|
-
const roots = await Promise.all(allowedRoots.map((root) => realpath(root)));
|
|
871
|
-
if (!isWithinAllowedRoot(file, roots))
|
|
872
|
-
throw new Error('FILE_PATH_ESCAPE');
|
|
873
870
|
const metadata = await lstat(file);
|
|
874
871
|
if (!metadata.isFile())
|
|
875
872
|
throw new Error('FILE_NOT_REGULAR');
|
|
@@ -1019,7 +1016,9 @@ export function normalizeWorkspacePath(input, platform = hostPlatform()) {
|
|
|
1019
1016
|
if (!win32.isAbsolute(normalized)) {
|
|
1020
1017
|
throw new Error('WORKSPACE_PATH_NOT_ABSOLUTE');
|
|
1021
1018
|
}
|
|
1022
|
-
|
|
1019
|
+
// A drive root must remain absolute instead of becoming drive-relative.
|
|
1020
|
+
const root = win32.parse(normalized).root;
|
|
1021
|
+
return (normalized === root ? root : normalized.replace(/\\+$/, '')).toLowerCase();
|
|
1023
1022
|
}
|
|
1024
1023
|
if (platform !== 'linux' && platform !== 'darwin') {
|
|
1025
1024
|
throw new Error('WORKSPACE_PLATFORM_UNSUPPORTED');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.141",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"bin": {
|
|
15
15
|
"mar-node": "./dist/main.js",
|
|
16
|
-
"
|
|
16
|
+
"marctl": "./dist/marctl.js",
|
|
17
17
|
"marnodes": "./dist/marnodes.js",
|
|
18
18
|
"marssh": "./dist/marssh.js",
|
|
19
19
|
"marscp": "./dist/marscp.js"
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
"npm": "11.12.1",
|
|
35
35
|
"ws": "^8.21.3",
|
|
36
36
|
"zod": "4.4.3",
|
|
37
|
-
"@myagentroam/
|
|
38
|
-
"@myagentroam/agent": "0.9.
|
|
39
|
-
"@myagentroam/
|
|
40
|
-
"@myagentroam/
|
|
37
|
+
"@myagentroam/marctl": "0.9.120",
|
|
38
|
+
"@myagentroam/agent": "0.9.141",
|
|
39
|
+
"@myagentroam/protocol": "0.9.141",
|
|
40
|
+
"@myagentroam/code-navigation": "0.9.129"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/ws": "^8.18.1"
|