@myagentroam/node 0.9.140 → 0.9.142
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 +4 -8
- package/dist/runner/claude/managed-run-controller.js +59 -5
- package/dist/runner/codex/managed-run-controller.js +39 -2
- package/dist/runner/mar-agent/mcp-client-pool.js +37 -5
- package/dist/runner/opencode/managed-run-controller.js +64 -0
- package/dist/runtime-state.js +8 -1
- package/dist/service/continuous-run-coordinator.js +5 -8
- package/dist/service/native-session-projection-service.js +12 -4
- package/dist/service/response-latency-tracker.js +47 -0
- package/dist/service/session-catalog-service.js +5 -2
- package/dist/service/workspace-session-service.js +75 -20
- package/package.json +4 -4
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,15 +306,10 @@ 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
314
|
workspaceFileService = new WorkspaceFileService(this.workspaceState, (workspaceId) => {
|
|
320
315
|
this.workspaceWorkbenchService?.invalidateWorkspaceFiles(workspaceId);
|
|
@@ -1131,6 +1126,7 @@ export class NodeConnector {
|
|
|
1131
1126
|
projection: this.nativeSessionProjectionService,
|
|
1132
1127
|
cachedNative: () => [...this.sessionState.directNative.values()],
|
|
1133
1128
|
cachedWorkspace: (workspaceId) => this.workspaceCoordinator.cachedSessions(workspaceId),
|
|
1129
|
+
discoveryPending: (workspaceId) => this.workspaceSessionService.discoveryPending(workspaceId),
|
|
1134
1130
|
discoverWorkspace: (workspaceId) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId),
|
|
1135
1131
|
present: (session) => this.sessionPresentationService.present(session),
|
|
1136
1132
|
projectionForNative: (session) => this.nativeSessionProjectionService.projectionForNative(session),
|
|
@@ -2185,7 +2181,7 @@ export class NodeConnector {
|
|
|
2185
2181
|
return;
|
|
2186
2182
|
this.workbenchEventService.publish(event);
|
|
2187
2183
|
};
|
|
2188
|
-
discoverWorkspaceSessions = (workspaceId) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId);
|
|
2184
|
+
discoverWorkspaceSessions = (workspaceId, onRunnerDiscovered) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId, onRunnerDiscovered);
|
|
2189
2185
|
invalidateWorkspaceWatchTopics = (workspaceId) => this.workspaceCoordinator.invalidate(workspaceId);
|
|
2190
2186
|
reconcilePersistedRuns = () => undefined;
|
|
2191
2187
|
startCapabilityRefresh = () => this.connectionLifecycle.startCapabilityRefresh();
|
|
@@ -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,
|
|
@@ -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()];
|
|
@@ -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;
|
|
@@ -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,
|
|
@@ -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;
|
|
@@ -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
|
@@ -561,7 +561,14 @@ export class NodeRuntimeState {
|
|
|
561
561
|
return false;
|
|
562
562
|
if (id !== run.id && this.currentExecutionId(run.id) !== id)
|
|
563
563
|
return false;
|
|
564
|
-
this.runPerformance.set(run.id, {
|
|
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
|
+
});
|
|
565
572
|
return true;
|
|
566
573
|
}
|
|
567
574
|
sessionPerformance(sessionId) {
|
|
@@ -283,14 +283,11 @@ export class ContinuousRunCoordinator {
|
|
|
283
283
|
}
|
|
284
284
|
this.dispatchedExecutions.add(executionId);
|
|
285
285
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const scheduledMcp = await this.options.scheduledMcp?.(
|
|
290
|
-
const mailMcp = await this.options.mailMcp?.(
|
|
291
|
-
...pending,
|
|
292
|
-
runId: executionId
|
|
293
|
-
});
|
|
286
|
+
// Capabilities belong to this Execution, never the Run or a claimed QueueItem.
|
|
287
|
+
const executionInput = { ...pending, runId: executionId };
|
|
288
|
+
const goalMcp = pending.goal === undefined ? undefined : await this.options.goalMcp?.(executionInput);
|
|
289
|
+
const scheduledMcp = await this.options.scheduledMcp?.(executionInput);
|
|
290
|
+
const mailMcp = await this.options.mailMcp?.(executionInput);
|
|
294
291
|
if (!this.goalCurrent(pending)) {
|
|
295
292
|
this.dispatchedExecutions.delete(executionId);
|
|
296
293
|
this.rejectStaleGoal(executionId);
|
|
@@ -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
|
+
}
|
|
@@ -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(),
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.142",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"ws": "^8.21.3",
|
|
36
36
|
"zod": "4.4.3",
|
|
37
37
|
"@myagentroam/marctl": "0.9.120",
|
|
38
|
-
"@myagentroam/
|
|
39
|
-
"@myagentroam/
|
|
40
|
-
"@myagentroam/
|
|
38
|
+
"@myagentroam/agent": "0.9.142",
|
|
39
|
+
"@myagentroam/protocol": "0.9.142",
|
|
40
|
+
"@myagentroam/code-navigation": "0.9.129"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/ws": "^8.18.1"
|