@myagentroam/node 0.11.68 → 0.11.69

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.
@@ -23,6 +23,9 @@ export function composeWorkspaceApplication(input) {
23
23
  uploads: resources.uploads,
24
24
  changes: input.changes,
25
25
  listSessions: input.listSessions,
26
+ ...(input.sessionNavigation === undefined
27
+ ? {}
28
+ : { sessionNavigation: input.sessionNavigation }),
26
29
  invalidate: resources.invalidateFiles,
27
30
  nodeId: input.nodeId,
28
31
  nodeGeneration: input.nodeGeneration,
@@ -61,6 +61,7 @@ export function unavailableCapabilities() {
61
61
  promptCompletion: { apiVersion: 1, contextVersion: 3, automaticTriggers: true },
62
62
  chatgptLogin: { apiVersion: 1, loopback: true },
63
63
  workspaceDirectTransfer: workspaceDirectTransferCapability(),
64
+ sessionAside: { apiVersion: 1 },
64
65
  sessionGroups: { apiVersion: 1 },
65
66
  terminalDirect: terminalDirectCapability()
66
67
  };
@@ -98,6 +99,7 @@ export function detectCapabilities(commands = {}) {
98
99
  promptCompletion: { apiVersion: 1, contextVersion: 3, automaticTriggers: true },
99
100
  chatgptLogin: { apiVersion: 1, loopback: true },
100
101
  workspaceDirectTransfer: workspaceDirectTransferCapability(),
102
+ sessionAside: { apiVersion: 1 },
101
103
  sessionGroups: { apiVersion: 1 },
102
104
  terminalDirect: terminalDirectCapability()
103
105
  };
@@ -139,6 +141,7 @@ export async function detectCapabilitiesAsync(commands = {}) {
139
141
  promptCompletion: { apiVersion: 1, contextVersion: 3, automaticTriggers: true },
140
142
  chatgptLogin: { apiVersion: 1, loopback: true },
141
143
  workspaceDirectTransfer: workspaceDirectTransferCapability(),
144
+ sessionAside: { apiVersion: 1 },
142
145
  sessionGroups: { apiVersion: 1 },
143
146
  terminalDirect: terminalDirectCapability()
144
147
  };
package/dist/connector.js CHANGED
@@ -1433,6 +1433,7 @@ export class NodeConnector {
1433
1433
  limit: 30,
1434
1434
  ...(sessionGroup === undefined ? {} : { sessionGroup })
1435
1435
  }),
1436
+ sessionNavigation: async (workspaceId, query) => this.sessionCatalogService.navigation({ workspaceId, ...query }),
1436
1437
  invalidatePrompt: (id) => this.promptCompletion?.invalidateWorkspace(id),
1437
1438
  nodeId: () => this.config?.nodeId ?? 'runtime-node',
1438
1439
  nodeGeneration: () => this.nodeGeneration,
@@ -1,5 +1,5 @@
1
1
  import { claudeContentBlocks } from './message-parsers.js';
2
- import { compactRunnerText, compactRunnerValue, publicOutputSnapshot, publicToolOutput } from '../normalization.js';
2
+ import { compactRunnerText, compactRunnerValue, publicOutputSnapshot, publicToolOutput, subagentCallDetail } from '../normalization.js';
3
3
  import { shellExecution } from '../execution-presentation.js';
4
4
  import { RunnerUsageTracker, usageToken, usageTokenSum } from '../../service/runner-usage-tracker.js';
5
5
  export class ClaudeUsageNormalizer {
@@ -176,6 +176,23 @@ export function normalizeClaudeTool(toolName, input, output, failed, truncated,
176
176
  }
177
177
  }
178
178
  : {}),
179
+ ...(toolName === 'WebFetch' && typeof values?.url === 'string'
180
+ ? {
181
+ toolPresentation: {
182
+ kind: 'WEB_FETCH',
183
+ detailOnly: true,
184
+ requestUrl: compactRunnerText(values.url, 8192)
185
+ }
186
+ }
187
+ : toolName === 'WebSearch' && typeof values?.query === 'string'
188
+ ? {
189
+ toolPresentation: {
190
+ kind: 'WEB_SEARCH',
191
+ detailOnly: true,
192
+ query: compactRunnerText(values.query, 500)
193
+ }
194
+ }
195
+ : {}),
179
196
  ...subagent
180
197
  }
181
198
  };
@@ -229,6 +246,11 @@ function claudeSubagentProjection(toolName, input, output, knownSubagents) {
229
246
  knownSubagents.set(subagentId, title);
230
247
  return {
231
248
  subagentOperation: operation,
249
+ ...(operation === 'START'
250
+ ? subagentCallDetail(record(input)?.prompt)
251
+ : operation === 'MESSAGE'
252
+ ? subagentCallDetail(record(input)?.message)
253
+ : {}),
232
254
  subagentId: subagentId === '*' ? null : (subagentId ?? null),
233
255
  ...(operation === 'STATUS' ? { subagentWait: inputData?.block === true } : {}),
234
256
  ...(title.length === 0 ? {} : { title })
@@ -1,4 +1,4 @@
1
- import { compactRunnerText, publicToolOutput } from '../normalization.js';
1
+ import { compactRunnerText, publicToolOutput, subagentCallDetail } from '../normalization.js';
2
2
  import { shellExecution } from '../execution-presentation.js';
3
3
  export function normalizeCodexApproval(method) {
4
4
  return method === 'item/commandExecution/requestApproval'
@@ -114,6 +114,9 @@ function codexSubagentProjection(namespace, toolName, input, output) {
114
114
  : '';
115
115
  return {
116
116
  subagentOperation: operation,
117
+ ...(operation === 'START' || operation === 'MESSAGE'
118
+ ? subagentCallDetail(record(input)?.message)
119
+ : {}),
117
120
  ...(toolName === 'wait_agent' ? { subagentWait: true } : {}),
118
121
  subagentId: Array.isArray(inputData?.targets) && inputData.targets.length > 1
119
122
  ? null
@@ -226,7 +226,7 @@ export function agentNetworkExtension(invoke, currentExecution) {
226
226
  : tool.action === 'view'
227
227
  ? { session: rows[0] }
228
228
  : (rows[0] ?? {});
229
- await call.publishEventData(agentOperationEvent(tool.action, {}, result).eventData);
229
+ await call.publishEventData(agentOperationEvent(tool.action, operationInput, result).eventData);
230
230
  }
231
231
  : undefined;
232
232
  const result = await invoke(executionId, tool.action, operationInput, call, onResolved);
@@ -61,6 +61,15 @@ export function agentOperationEvent(action, input, result) {
61
61
  const operation = agentOperationPresentation(`agent_${action}`, input);
62
62
  const value = isPlainRecord(result) ? result : {};
63
63
  const args = isPlainRecord(input) ? input : {};
64
+ const message = action === 'start' || action === 'send' ? text(args.content, 2000) : undefined;
65
+ const title = action === 'start' ? text(args.title, 500) : undefined;
66
+ if (message || title)
67
+ operation.detail = {
68
+ ...(title ? { title } : {}),
69
+ ...(message
70
+ ? { message, ...(message !== args.content ? { messageTruncated: true } : {}) }
71
+ : {})
72
+ };
64
73
  const rows = action === 'list' ? value.sessions : action === 'wait' ? value.agents : undefined;
65
74
  const records = Array.isArray(rows)
66
75
  ? rows
@@ -82,7 +91,31 @@ export function agentOperationEvent(action, input, result) {
82
91
  if (!agentId)
83
92
  return [];
84
93
  const title = text(entry.title ?? (action === 'start' ? args.title : undefined), 500);
85
- return [{ agentId, sessionId: agentId, ...(title ? { title } : {}) }];
94
+ const status = ['list', 'view', 'wait'].includes(action) ? text(entry.status) : undefined;
95
+ const latest = action === 'wait' ? latestWaitMessage(entry.updates) : undefined;
96
+ const viewMessage = action === 'view' && isPlainRecord(entry.latestMessage) ? entry.latestMessage : undefined;
97
+ const summary = action === 'wait' ? text(entry.summary, 800) : undefined;
98
+ const rawMessage = summary ?? latest?.value ?? text(viewMessage?.text, 800);
99
+ const messageTruncated = summary
100
+ ? entry.summaryTruncated === true || summary !== entry.summary
101
+ : latest
102
+ ? latest.truncated === true
103
+ : viewMessage?.truncated === true ||
104
+ (typeof viewMessage?.text === 'string' && viewMessage.text.length > 800);
105
+ return [
106
+ {
107
+ agentId,
108
+ sessionId: agentId,
109
+ ...(title ? { title } : {}),
110
+ ...(status ? { status } : {}),
111
+ ...(rawMessage
112
+ ? {
113
+ message: rawMessage,
114
+ ...(messageTruncated ? { messageTruncated: true } : {})
115
+ }
116
+ : {})
117
+ }
118
+ ];
86
119
  })
87
120
  .slice(0, 100);
88
121
  // An any-wait may return one result for several requested targets; keep the call multi-target.
@@ -90,12 +123,67 @@ export function agentOperationEvent(action, input, result) {
90
123
  ? operation.targets.map((target) => returnedTargets.find((result) => result.agentId === target.agentId) ?? target)
91
124
  : returnedTargets;
92
125
  const presentation = { ...operation, targets };
93
- if (records.length > returnedTargets.length)
126
+ const messageReceipt = isPlainRecord(value.message) ? value.message : undefined;
127
+ const messageResult = isPlainRecord(messageReceipt?.result) ? messageReceipt.result : undefined;
128
+ const auditReceipt = isPlainRecord(messageResult?.auditReceipt)
129
+ ? messageResult.auditReceipt
130
+ : undefined;
131
+ const failures = [value.message, value.goal, value.approval, value.observation, value.start]
132
+ .filter((part) => isPlainRecord(part) &&
133
+ ['unknown', 'failed', 'partial', 'not_attempted'].includes(String(part.status)))
134
+ .map((part) => (isPlainRecord(part) ? (text(part.code) ?? text(part.status)) : undefined))
135
+ .filter((part) => part !== undefined);
136
+ if (failures.length)
137
+ presentation.detail = {
138
+ ...presentation.detail,
139
+ failed: true,
140
+ error: failures.join('\n').slice(0, 500)
141
+ };
142
+ if (action === 'send' &&
143
+ message &&
144
+ failures.length === 0 &&
145
+ messageReceipt?.status === 'accepted') {
146
+ const delivery = messageResult?.delivery === 'QUEUED' || auditReceipt?.inputMode === 'QUEUE'
147
+ ? 'QUEUE'
148
+ : auditReceipt?.inputMode === 'REPLACE_CURRENT'
149
+ ? 'STEER'
150
+ : undefined;
151
+ if (delivery)
152
+ presentation.detail = { ...presentation.detail, delivery };
153
+ }
154
+ if (!presentation.detail &&
155
+ ((action === 'list' && Array.isArray(value.sessions)) ||
156
+ (action === 'view' && isPlainRecord(value.session)) ||
157
+ (action === 'wait' && records.length > 0) ||
158
+ (action === 'stop' && typeof value.agentId === 'string')))
159
+ presentation.detail = {};
160
+ if (value.truncated === true || records.length > returnedTargets.length)
94
161
  presentation.targetsTruncated = true;
95
162
  // Core accepts at most 8 KiB of explicit extension event data.
96
163
  while (Buffer.byteLength(JSON.stringify({ agentOperation: presentation }), 'utf8') > 7000) {
97
- targets.pop();
98
- presentation.targetsTruncated = true;
164
+ const targetWithMessage = targets.findLast((target) => target.message);
165
+ if (targetWithMessage?.message) {
166
+ const shortened = text(targetWithMessage.message, Math.floor(targetWithMessage.message.length / 2));
167
+ if (shortened)
168
+ targetWithMessage.message = shortened;
169
+ else
170
+ delete targetWithMessage.message;
171
+ targetWithMessage.messageTruncated = true;
172
+ }
173
+ else if (presentation.detail?.message) {
174
+ const shortened = text(presentation.detail.message, Math.floor(presentation.detail.message.length / 2));
175
+ if (shortened)
176
+ presentation.detail.message = shortened;
177
+ else
178
+ delete presentation.detail.message;
179
+ presentation.detail.messageTruncated = true;
180
+ }
181
+ else if (targets.length) {
182
+ targets.pop();
183
+ presentation.targetsTruncated = true;
184
+ }
185
+ else
186
+ break;
99
187
  }
100
188
  const receipt = {};
101
189
  if (action === 'list' && Array.isArray(value.models)) {
@@ -1059,11 +1059,24 @@ function mergeAgentOperationData(toolName, previous, current) {
1059
1059
  ...record(current),
1060
1060
  agentOperation: {
1061
1061
  ...after.data,
1062
- targets: before.data.targets.map((target) => ({
1063
- ...after.data.targets.find((candidate) => candidate.agentId === target.agentId),
1064
- ...target
1065
- })),
1066
- ...(before.data.targetsTruncated ? { targetsTruncated: true } : {})
1062
+ ...(after.data.detail === undefined && before.data.detail !== undefined
1063
+ ? { detail: before.data.detail }
1064
+ : {}),
1065
+ targets: (after.data.action === 'WAIT'
1066
+ ? [
1067
+ ...after.data.targets,
1068
+ ...before.data.targets.filter((target) => !after.data.targets.some((candidate) => candidate.agentId === target.agentId))
1069
+ ]
1070
+ : after.data.targets).map((target) => {
1071
+ const resolved = before.data.targets.find((candidate) => candidate.agentId === target.agentId);
1072
+ const current = after.data.targets.find((candidate) => candidate.agentId === target.agentId);
1073
+ return {
1074
+ agentId: target.agentId,
1075
+ ...(resolved?.sessionId ? { sessionId: resolved.sessionId } : {}),
1076
+ ...(resolved?.title ? { title: resolved.title } : {}),
1077
+ ...current
1078
+ };
1079
+ })
1067
1080
  }
1068
1081
  };
1069
1082
  }
@@ -1213,7 +1226,10 @@ function genericToolItem(id, toolName, status, input, outputSummary = null, erro
1213
1226
  ];
1214
1227
  }
1215
1228
  const toolCategory = isImageGenerationTool(toolName) ? 'IMAGE_GENERATION' : undefined;
1216
- const presentation = projectToolPresentation(toolName, input, output, sleepStartedAt);
1229
+ const readOutput = toolName === 'read' && status === 'COMPLETED' && outputSummary !== null
1230
+ ? publicToolOutput(outputSummary, summaryTruncated ? 'SOURCE' : outputTruncation)
1231
+ : undefined;
1232
+ const presentation = projectToolPresentation(toolName, input, output, sleepStartedAt, outputSummary, readOutput);
1217
1233
  const inputSummary = compactRunnerValue(input, 10_000);
1218
1234
  return {
1219
1235
  itemId: id,
@@ -1224,8 +1240,12 @@ function genericToolItem(id, toolName, status, input, outputSummary = null, erro
1224
1240
  toolName: compactRunnerText(toolName, 128) || 'unknown',
1225
1241
  title: compactRunnerText(toolName, 500) || 'unknown',
1226
1242
  inputSummary: inputSummary.text || null,
1227
- outputSummary: typeof output === 'string' || errorCode !== null ? null : outputSummary,
1228
- ...(typeof output === 'string' ? { output: publicToolOutput(output, outputTruncation) } : {}),
1243
+ outputSummary: typeof output === 'string' || errorCode !== null || readOutput ? null : outputSummary,
1244
+ ...(readOutput
1245
+ ? { output: readOutput }
1246
+ : typeof output === 'string'
1247
+ ? { output: publicToolOutput(output, outputTruncation) }
1248
+ : {}),
1229
1249
  ...(typeof output !== 'string' && outputSummary !== null && summaryTruncated
1230
1250
  ? { outputSummaryTruncated: true }
1231
1251
  : {}),
@@ -1243,7 +1263,7 @@ function genericToolItem(id, toolName, status, input, outputSummary = null, erro
1243
1263
  merge: true
1244
1264
  };
1245
1265
  }
1246
- function projectToolPresentation(toolName, input, output, startedAt) {
1266
+ function projectToolPresentation(toolName, input, output, startedAt, readSummary, readOutput) {
1247
1267
  if (toolName === 'read') {
1248
1268
  const reads = record(input)?.reads;
1249
1269
  const paths = (Array.isArray(reads) ? reads : [])
@@ -1252,8 +1272,88 @@ function projectToolPresentation(toolName, input, output, startedAt) {
1252
1272
  return typeof path === 'string' && path.length > 0 ? [compactRunnerText(path, 4096)] : [];
1253
1273
  })
1254
1274
  .slice(0, 16);
1255
- return { kind: 'READ', paths };
1275
+ const data = record(output);
1276
+ if (data?.mode !== 'batch' ||
1277
+ !Array.isArray(data.results) ||
1278
+ readSummary == null ||
1279
+ !readOutput)
1280
+ return { kind: 'READ', paths };
1281
+ const text = readOutput.blocks.find((block) => block.channel === 'TEXT')?.text ?? '';
1282
+ const removed = readSummary.length - text.length;
1283
+ const results = data.results.slice(0, 16).flatMap((value) => {
1284
+ const item = record(value);
1285
+ if (typeof item?.path !== 'string' || !item.path)
1286
+ return [];
1287
+ const range = record(item.contentRange);
1288
+ const start = range?.start;
1289
+ const end = range?.end;
1290
+ const validRange = typeof start === 'number' &&
1291
+ Number.isSafeInteger(start) &&
1292
+ start >= 0 &&
1293
+ typeof end === 'number' &&
1294
+ Number.isSafeInteger(end) &&
1295
+ start <= end &&
1296
+ end <= readSummary.length &&
1297
+ (end > removed || (start === end && start >= removed));
1298
+ const error = record(item.error);
1299
+ return [
1300
+ {
1301
+ path: compactRunnerText(item.path, 4096),
1302
+ ...(item.startLine === undefined ? {} : { startLine: item.startLine }),
1303
+ ...(item.endLine === undefined ? {} : { endLine: item.endLine }),
1304
+ ...(item.empty === true ? { empty: true } : {}),
1305
+ ...(typeof error?.code === 'string' && typeof error.message === 'string'
1306
+ ? {
1307
+ error: {
1308
+ code: compactRunnerText(error.code, 80),
1309
+ message: compactRunnerText(error.message, 1000)
1310
+ }
1311
+ }
1312
+ : {}),
1313
+ ...(validRange
1314
+ ? {
1315
+ contentRange: {
1316
+ channel: 'TEXT',
1317
+ start: Math.max(start, removed) - removed,
1318
+ end: end - removed
1319
+ }
1320
+ }
1321
+ : {})
1322
+ }
1323
+ ];
1324
+ });
1325
+ const parsed = toolPresentationSchema.safeParse({ kind: 'READ', paths, results });
1326
+ return parsed.success ? parsed.data : { kind: 'READ', paths };
1327
+ }
1328
+ const args = record(input) ?? {};
1329
+ const data = record(output) ?? {};
1330
+ if (toolName === 'view_image') {
1331
+ const parsed = toolPresentationSchema.safeParse({
1332
+ kind: 'IMAGE_VIEW',
1333
+ path: typeof args.path === 'string' ? compactRunnerText(args.path, 4096) : undefined,
1334
+ resourceId: typeof args.resourceId === 'string' ? compactRunnerText(args.resourceId, 512) : undefined,
1335
+ resize: args.resize,
1336
+ mimeType: data.mimeType,
1337
+ width: data.width,
1338
+ height: data.height,
1339
+ sourceWidth: data.sourceWidth,
1340
+ sourceHeight: data.sourceHeight,
1341
+ region: data.region
1342
+ });
1343
+ return parsed.success
1344
+ ? Object.fromEntries(Object.entries(parsed.data).filter(([, value]) => value !== undefined))
1345
+ : { kind: 'IMAGE_VIEW' };
1346
+ }
1347
+ if (toolName === 'tool_search') {
1348
+ const parsed = toolPresentationSchema.safeParse({
1349
+ kind: 'TOOL_SEARCH',
1350
+ ...(typeof args.query === 'string' ? { query: compactRunnerText(args.query, 500) } : {}),
1351
+ ...(data.tools === undefined ? {} : { tools: data.tools })
1352
+ });
1353
+ return parsed.success ? parsed.data : { kind: 'TOOL_SEARCH' };
1256
1354
  }
1355
+ if (toolName === 'smart_compact')
1356
+ return { kind: 'COMPACT_REQUEST', ...(data.status === 'accepted' ? { accepted: true } : {}) };
1257
1357
  const category = toolName === 'web_search'
1258
1358
  ? 'WEB_SEARCH'
1259
1359
  : toolName === 'web_fetch'
@@ -1265,8 +1365,6 @@ function projectToolPresentation(toolName, input, output, startedAt) {
1265
1365
  : undefined;
1266
1366
  if (category === undefined)
1267
1367
  return undefined;
1268
- const args = record(input) ?? {};
1269
- const data = record(output) ?? {};
1270
1368
  // A provider-hosted search tool can report a page-read action under its native name.
1271
1369
  const kind = category === 'WEB_SEARCH' && data.hostedToolKind === 'WEB_FETCH' ? 'WEB_FETCH' : category;
1272
1370
  const text = (value, limit) => typeof value === 'string' ? compactRunnerText(value, limit) : undefined;
@@ -2,6 +2,21 @@ import { boundToolOutput } from '@myagentroam/protocol';
2
2
  export function compactRunnerText(value, limit) {
3
3
  return value.length <= limit ? value : `${value.slice(0, Math.max(0, limit - 1))}…`;
4
4
  }
5
+ /** The current native call's message, never a target's result or inferred history. */
6
+ export function subagentCallDetail(value) {
7
+ if (typeof value !== 'string' || value.trim().length === 0)
8
+ return {};
9
+ let message = value.slice(0, 2000);
10
+ const last = message.charCodeAt(message.length - 1);
11
+ if (last >= 0xd800 && last <= 0xdbff)
12
+ message = message.slice(0, -1);
13
+ return {
14
+ subagentCallDetail: {
15
+ message,
16
+ ...(message.length < value.length ? { messageTruncated: true } : {})
17
+ }
18
+ };
19
+ }
5
20
  export function publicOutputSnapshot(value, truncated = false) {
6
21
  const text = typeof value === 'string' ? value : compactRunnerValue(value, Number.MAX_SAFE_INTEGER).text;
7
22
  let bounded = text.slice(-10_000);
@@ -1,4 +1,4 @@
1
- import { compactRunnerText, compactRunnerValue, publicToolOutput } from '../normalization.js';
1
+ import { compactRunnerText, compactRunnerValue, publicToolOutput, subagentCallDetail } from '../normalization.js';
2
2
  import { shellExecution } from '../execution-presentation.js';
3
3
  export function normalizeOpenCodeTool(toolName, state) {
4
4
  const status = state?.status === 'completed'
@@ -53,6 +53,23 @@ export function normalizeOpenCodeTool(toolName, state) {
53
53
  }
54
54
  }
55
55
  : {}),
56
+ ...(normalizedName === 'webfetch' && typeof input?.url === 'string'
57
+ ? {
58
+ toolPresentation: {
59
+ kind: 'WEB_FETCH',
60
+ detailOnly: true,
61
+ requestUrl: compactRunnerText(input.url, 8192)
62
+ }
63
+ }
64
+ : normalizedName === 'websearch' && typeof input?.query === 'string'
65
+ ? {
66
+ toolPresentation: {
67
+ kind: 'WEB_SEARCH',
68
+ detailOnly: true,
69
+ query: compactRunnerText(input.query, 500)
70
+ }
71
+ }
72
+ : {}),
56
73
  ...(typeof state?.output === 'string' ? { output: publicToolOutput(state.output) } : {}),
57
74
  ...subagent
58
75
  }
@@ -72,6 +89,7 @@ function openCodeSubagentProjection(toolName, input, output) {
72
89
  const title = typeof inputData?.description === 'string' ? compactRunnerText(inputData.description, 500) : '';
73
90
  return {
74
91
  subagentOperation: taskId === undefined ? 'START' : 'RESUME',
92
+ ...(taskId === undefined ? subagentCallDetail(inputData?.prompt) : {}),
75
93
  subagentId: taskId ?? nativeResult?.taskId ?? null,
76
94
  ...(title.length === 0 ? {} : { title }),
77
95
  ...(nativeResult === undefined
@@ -31,13 +31,16 @@ export class NativeSessionWatchService {
31
31
  typeof input.watchId !== 'string' ||
32
32
  typeof input.workbenchConnectionId !== 'string' ||
33
33
  (input.mode !== 'initial' && input.mode !== 'renew') ||
34
+ (input.position !== undefined &&
35
+ input.position !== 'primary' &&
36
+ input.position !== 'secondary') ||
34
37
  (input.unit !== undefined && input.unit !== 'SEGMENT'))
35
38
  throw new Error('SESSION_INVALID');
36
39
  const ownerKey = this.ownerKey(input);
37
40
  if (input.mode === 'renew') {
38
41
  const lease = this.leases.get(ownerKey);
39
42
  if (lease === undefined ||
40
- !this.sessionWatchSlots.owns(ownerKey, lease.slotToken) ||
43
+ !this.sessionWatchSlots.owns(lease.slotOwnerKey, lease.slotToken) ||
41
44
  lease.watchId !== input.watchId ||
42
45
  lease.sessionId !== input.sessionId ||
43
46
  (input.workspaceId !== undefined && lease.workspaceId !== input.workspaceId))
@@ -57,7 +60,7 @@ export class NativeSessionWatchService {
57
60
  const existing = this.leases.get(ownerKey);
58
61
  const unit = input.unit === 'SEGMENT' ? 'SEGMENT' : 'TURN';
59
62
  if (existing?.watchId === input.watchId &&
60
- this.sessionWatchSlots.owns(ownerKey, existing.slotToken) &&
63
+ this.sessionWatchSlots.owns(existing.slotOwnerKey, existing.slotToken) &&
61
64
  existing.sessionId === input.sessionId &&
62
65
  (input.workspaceId === undefined || existing.workspaceId === input.workspaceId) &&
63
66
  existing.unit === unit &&
@@ -68,9 +71,12 @@ export class NativeSessionWatchService {
68
71
  return this.accepted(existing);
69
72
  }
70
73
  this.removeLease(ownerKey);
71
- const slotToken = this.sessionWatchSlots.replace(ownerKey, 'session', () => this.removeLease(ownerKey));
74
+ const slotOwnerKey = this.slotOwnerKey(input);
75
+ const position = input.position === 'secondary' ? 'secondary' : 'primary';
76
+ const slotToken = this.sessionWatchSlots.replace(slotOwnerKey, 'session', () => this.removeLease(ownerKey), position);
72
77
  const lease = {
73
78
  ownerKey,
79
+ slotOwnerKey,
74
80
  sessionHash: this.sessionHash(input),
75
81
  workbenchConnectionId: input.workbenchConnectionId,
76
82
  watchId: input.watchId,
@@ -96,7 +102,10 @@ export class NativeSessionWatchService {
96
102
  executeUnwatch(input) {
97
103
  if (typeof input.sessionId !== 'string' ||
98
104
  typeof input.watchId !== 'string' ||
99
- typeof input.workbenchConnectionId !== 'string')
105
+ typeof input.workbenchConnectionId !== 'string' ||
106
+ (input.position !== undefined &&
107
+ input.position !== 'primary' &&
108
+ input.position !== 'secondary'))
100
109
  throw new Error('SESSION_INVALID');
101
110
  const ownerKey = this.ownerKey(input);
102
111
  const lease = this.leases.get(ownerKey);
@@ -182,14 +191,13 @@ export class NativeSessionWatchService {
182
191
  return this.watches.has(sessionId);
183
192
  }
184
193
  observes(sessionHash, connectionId, workspaceId, sessionId) {
185
- const ownerKey = `${sessionHash}:${connectionId}`;
186
- const lease = this.leases.get(ownerKey);
187
- return (lease !== undefined &&
194
+ const slotOwnerKey = `${sessionHash}:${connectionId}`;
195
+ return [...this.leases.values()].some((lease) => lease.slotOwnerKey === slotOwnerKey &&
188
196
  lease.ready &&
189
197
  lease.expiresAt > Date.now() &&
190
198
  lease.sessionId === sessionId &&
191
199
  lease.workspaceId === workspaceId &&
192
- this.sessionWatchSlots.owns(ownerKey, lease.slotToken));
200
+ this.sessionWatchSlots.owns(slotOwnerKey, lease.slotToken));
193
201
  }
194
202
  isWatching(sessionId) {
195
203
  return this.has(sessionId);
@@ -286,6 +294,9 @@ export class NativeSessionWatchService {
286
294
  : 'local-test';
287
295
  }
288
296
  ownerKey(input) {
297
+ return `${this.slotOwnerKey(input)}:${input.position === 'secondary' ? 'secondary' : 'primary'}`;
298
+ }
299
+ slotOwnerKey(input) {
289
300
  return `${this.sessionHash(input)}:${input.workbenchConnectionId}`;
290
301
  }
291
302
  accepted(lease) {
@@ -378,7 +389,7 @@ export class NativeSessionWatchService {
378
389
  return;
379
390
  clearTimeout(lease.timer);
380
391
  this.leases.delete(ownerKey);
381
- this.sessionWatchSlots.release(ownerKey, lease.slotToken);
392
+ this.sessionWatchSlots.release(lease.slotOwnerKey, lease.slotToken);
382
393
  const observer = this.watches.get(lease.sessionId);
383
394
  const nextExpiry = this.latestLeaseExpiry(lease.sessionId);
384
395
  if (nextExpiry === 0)
@@ -1,21 +1,35 @@
1
- /** Coordinates the single Session Watch slot owned by one logical Workbench connection. */
1
+ /** Coordinates bounded ordinary positions and the mutually exclusive hosted watch. */
2
2
  export class SessionWatchSlotCoordinator {
3
3
  slots = new Map();
4
- replace(ownerKey, kind, release) {
4
+ replace(ownerKey, kind, release, position = 'primary') {
5
+ const key = kind === 'session' ? position : 'hosted';
5
6
  const previous = this.slots.get(ownerKey);
6
7
  if (previous !== undefined) {
7
- this.slots.delete(ownerKey);
8
- previous.release();
8
+ for (const [slotKey, slot] of previous) {
9
+ if (kind === 'hosted-conversation' || slotKey === 'hosted' || slotKey === key) {
10
+ previous.delete(slotKey);
11
+ slot.release();
12
+ }
13
+ }
9
14
  }
10
15
  const token = Symbol(kind);
11
- this.slots.set(ownerKey, { kind, token, release });
16
+ const slots = this.slots.get(ownerKey) ?? new Map();
17
+ slots.set(key, { kind, token, release });
18
+ this.slots.set(ownerKey, slots);
12
19
  return token;
13
20
  }
14
21
  owns(ownerKey, token) {
15
- return this.slots.get(ownerKey)?.token === token;
22
+ return [...(this.slots.get(ownerKey)?.values() ?? [])].some((slot) => slot.token === token);
16
23
  }
17
24
  release(ownerKey, token) {
18
- if (this.slots.get(ownerKey)?.token === token)
25
+ const slots = this.slots.get(ownerKey);
26
+ if (slots === undefined)
27
+ return;
28
+ for (const [key, slot] of slots) {
29
+ if (slot.token === token)
30
+ slots.delete(key);
31
+ }
32
+ if (slots.size === 0)
19
33
  this.slots.delete(ownerKey);
20
34
  }
21
35
  }
@@ -154,16 +154,18 @@ export class WebToolService {
154
154
  } while (countModelTokens(content) > budget && metadataLimit >= 32);
155
155
  if (countModelTokens(content) > budget)
156
156
  throw invalidResult();
157
+ const data = {
158
+ query: parsed.data.query,
159
+ results: results.map((item, index) => ({
160
+ ...item,
161
+ url: parsed.data.results[index].url
162
+ })),
163
+ truncated
164
+ };
157
165
  return {
158
166
  content,
159
- data: {
160
- query: parsed.data.query,
161
- results: results.map((item, index) => ({
162
- ...item,
163
- url: parsed.data.results[index].url
164
- })),
165
- truncated
166
- },
167
+ data,
168
+ eventData: { ...data, sourcesOnly: true },
167
169
  truncated
168
170
  };
169
171
  }
@@ -48,6 +48,7 @@ export class WorkbenchManifestService {
48
48
  },
49
49
  directTransfer: capabilities.workspaceDirectTransfer,
50
50
  terminalDirect: capabilities.terminalDirect,
51
+ sessionAside: capabilities.sessionAside,
51
52
  sessionGroups: capabilities.sessionGroups,
52
53
  git: {
53
54
  available: true,
@@ -28,16 +28,27 @@ export class WorkspaceWatchController {
28
28
  existing.topics.changes === topics.changes &&
29
29
  JSON.stringify(existing.topics.files) === JSON.stringify(topics.files) &&
30
30
  (existing.prepareFiles === true) === (input.prepareFiles === true);
31
+ const navigationOnly = existing !== undefined &&
32
+ existing.topics.sessions === topics.sessions &&
33
+ existing.topics.sessionGroup === topics.sessionGroup &&
34
+ existing.topics.changes === topics.changes &&
35
+ JSON.stringify(existing.topics.files) === JSON.stringify(topics.files) &&
36
+ !sameNavigation(existing.topics.sessionNavigation, topics.sessionNavigation) &&
37
+ (existing.prepareFiles === true) === (input.prepareFiles === true);
31
38
  if (existing !== undefined &&
32
39
  existing.watchId === input.watchId &&
33
- JSON.stringify(existing.topics) === JSON.stringify(topics) &&
40
+ existing.topics.sessions === topics.sessions &&
41
+ existing.topics.sessionGroup === topics.sessionGroup &&
42
+ existing.topics.changes === topics.changes &&
43
+ JSON.stringify(existing.topics.files) === JSON.stringify(topics.files) &&
44
+ sameNavigation(existing.topics.sessionNavigation, topics.sessionNavigation) &&
34
45
  (existing.prepareFiles === true) === (input.prepareFiles === true)) {
35
46
  this.options.watch.renew(workspace.id, key, input.watchId);
36
47
  return this.accepted(existing);
37
48
  }
38
49
  const watch = this.options.watch.create(workspace.id, input.watchId, this.options.watch.sessionHash(input), input.workbenchConnectionId, this.options.nodeGeneration(), topics, input.prepareFiles === true);
39
- void this.options.snapshots.initial(workspace, watch, groupOnly ? existing : undefined);
40
- if (watch.prepareFiles)
50
+ void this.options.snapshots.initial(workspace, watch, groupOnly || navigationOnly ? existing : undefined);
51
+ if (watch.prepareFiles && !navigationOnly)
41
52
  this.options.snapshots.prewarmFiles(workspace, watch);
42
53
  return this.accepted(watch);
43
54
  }
@@ -72,6 +83,16 @@ export class WorkspaceWatchController {
72
83
  };
73
84
  }
74
85
  }
86
+ function sameNavigation(left, right) {
87
+ if (left === undefined || right === undefined)
88
+ return left === right;
89
+ return (left.limit === right.limit &&
90
+ left.treeAnchor === right.treeAnchor &&
91
+ left.sessionIds.length === right.sessionIds.length &&
92
+ left.sessionIds.every((id, index) => id === right.sessionIds[index]) &&
93
+ left.summaryAnchors.length === right.summaryAnchors.length &&
94
+ left.summaryAnchors.every((id, index) => id === right.summaryAnchors[index]));
95
+ }
75
96
  function record(value) {
76
97
  if (typeof value !== 'object' || value === null || Array.isArray(value))
77
98
  throw new Error('WORKSPACE_WATCH_INVALID');
@@ -76,23 +76,104 @@ export class SessionCatalogService {
76
76
  ancestors.add(parent.id);
77
77
  root = parent;
78
78
  }
79
+ const project = this.navigationProjector(input.workspaceId);
80
+ const queue = [root];
81
+ const visited = new Set([root.id]);
82
+ const nodes = [];
83
+ for (let index = 0; index < queue.length && index < limit; index += 1) {
84
+ const session = queue[index];
85
+ nodes.push(project(session));
86
+ const children = [...database.listChildSessionRecords(input.workspaceId, session.id)];
87
+ if (input.includeAncestors === true)
88
+ children.sort((left, right) => Number(ancestors.has(right.id)) - Number(ancestors.has(left.id)));
89
+ for (const child of children) {
90
+ if (visited.has(child.id))
91
+ continue;
92
+ visited.add(child.id);
93
+ queue.push(child);
94
+ }
95
+ }
96
+ return { rootSessionId: root.id, nodes, truncated: queue.length > nodes.length };
97
+ }
98
+ navigation(query) {
99
+ const database = this.options.database();
100
+ const project = this.navigationProjector(query.workspaceId);
101
+ const get = (id) => {
102
+ const record = database.getSessionRecord(id);
103
+ return record?.workspaceId === query.workspaceId ? record : undefined;
104
+ };
105
+ const requestedIds = new Set([
106
+ ...query.sessionIds,
107
+ ...query.summaryAnchors,
108
+ ...(query.treeAnchor === undefined ? [] : [query.treeAnchor])
109
+ ]);
110
+ const missingSessionIds = [...requestedIds].filter((id) => get(id) === undefined);
111
+ const sessions = query.sessionIds.flatMap((id) => {
112
+ const record = get(id);
113
+ return record === undefined ? [] : [project(record)];
114
+ });
115
+ const descendantsByAnchor = new Map();
116
+ const descendants = (anchor) => {
117
+ let records = descendantsByAnchor.get(anchor);
118
+ if (records === undefined) {
119
+ records = database.sessionTreeRecords(query.workspaceId, anchor).slice(1);
120
+ descendantsByAnchor.set(anchor, records);
121
+ }
122
+ return records;
123
+ };
124
+ const summaries = query.summaryAnchors.flatMap((anchorSessionId) => {
125
+ if (get(anchorSessionId) === undefined)
126
+ return [];
127
+ let runningCount = 0;
128
+ let sleepingCount = 0;
129
+ const records = descendants(anchorSessionId);
130
+ for (const record of records) {
131
+ const status = sessionActivityStatus(this.options.runtime, record.id);
132
+ if (status === 'RUNNING')
133
+ runningCount++;
134
+ else if (status === 'SLEEPING')
135
+ sleepingCount++;
136
+ }
137
+ return [
138
+ {
139
+ anchorSessionId,
140
+ descendantCount: records.length,
141
+ activeCount: runningCount + sleepingCount,
142
+ runningCount,
143
+ sleepingCount,
144
+ hasActiveDescendants: runningCount + sleepingCount > 0
145
+ }
146
+ ];
147
+ });
148
+ const anchor = query.treeAnchor === undefined ? undefined : get(query.treeAnchor);
149
+ const records = anchor === undefined ? [] : descendants(anchor.id);
150
+ const parent = anchor?.metadata.parentSessionId
151
+ ? get(anchor.metadata.parentSessionId)
152
+ : undefined;
153
+ const tree = anchor === undefined
154
+ ? null
155
+ : {
156
+ anchorSessionId: anchor.id,
157
+ parent: parent === undefined ? null : project(parent),
158
+ nodes: records.slice(0, query.limit).map(project),
159
+ truncated: records.length > query.limit
160
+ };
161
+ return { sessions, summaries, tree, missingSessionIds };
162
+ }
163
+ navigationProjector(workspaceId) {
79
164
  const native = this.options
80
165
  .cachedNative()
81
- .filter((session) => session.workspaceId === input.workspaceId);
166
+ .filter((session) => session.workspaceId === workspaceId);
82
167
  const projections = this.options.projection.projectionsForNative(native);
83
168
  const activityById = new Map();
84
169
  for (const source of native) {
85
170
  const id = projections.get(source)?.id ?? source.id;
86
171
  activityById.set(id, latestSessionActivityAt(activityById.get(id) ?? 0, source.lastActivityAt));
87
172
  }
88
- const queue = [root];
89
- const visited = new Set([root.id]);
90
- const nodes = [];
91
- for (let index = 0; index < queue.length && index < limit; index += 1) {
92
- const session = queue[index];
173
+ return (session) => {
93
174
  const active = this.options.runtime.getAgentSession(session.id);
94
175
  const live = active === undefined ? undefined : this.options.present(active);
95
- nodes.push({
176
+ return {
96
177
  id: session.id,
97
178
  parentSessionId: session.metadata.parentSessionId ?? null,
98
179
  title: live?.customTitle ??
@@ -104,18 +185,8 @@ export class SessionCatalogService {
104
185
  activity: live && 'activity' in live ? live.activity : 'IDLE',
105
186
  status: sessionActivityStatus(this.options.runtime, session.id),
106
187
  lastActivityAt: latestSessionActivityAt(live?.lastActivityAt ?? 0, activityById.get(session.id) ?? 0)
107
- });
108
- const children = [...database.listChildSessionRecords(input.workspaceId, session.id)];
109
- if (input.includeAncestors === true)
110
- children.sort((left, right) => Number(ancestors.has(right.id)) - Number(ancestors.has(left.id)));
111
- for (const child of children) {
112
- if (visited.has(child.id))
113
- continue;
114
- visited.add(child.id);
115
- queue.push(child);
116
- }
117
- }
118
- return { rootSessionId: root.id, nodes, truncated: queue.length > nodes.length };
188
+ };
189
+ };
119
190
  }
120
191
  async collect(input) {
121
192
  const workspaceId = typeof input.workspaceId === 'string' ? input.workspaceId : undefined;
@@ -1,4 +1,5 @@
1
1
  import { posix } from 'node:path';
2
+ import { workspaceSessionNavigationQuerySchema } from '@myagentroam/protocol';
2
3
  const WATCH_TTL_MS = 45_000;
3
4
  export class WorkspaceWatchService {
4
5
  onSessionWatchRequired;
@@ -22,6 +23,14 @@ export class WorkspaceWatchService {
22
23
  if (sessionGroup !== undefined && sessionGroup !== 'MAIN' && sessionGroup !== 'CHILD')
23
24
  throw new Error('WORKSPACE_WATCH_INVALID');
24
25
  const changes = input.topics.changes === true;
26
+ const rawNavigation = input.topics.sessionNavigation;
27
+ let sessionNavigation;
28
+ if (rawNavigation !== undefined) {
29
+ const parsed = workspaceSessionNavigationQuerySchema.safeParse(rawNavigation);
30
+ if (!parsed.success)
31
+ throw new Error('WORKSPACE_WATCH_INVALID');
32
+ sessionNavigation = parsed.data;
33
+ }
25
34
  const rawFiles = input.topics.files;
26
35
  let files;
27
36
  if (rawFiles !== undefined) {
@@ -40,9 +49,15 @@ export class WorkspaceWatchService {
40
49
  ...(typeof rawFiles.limit === 'number' ? { limit: rawFiles.limit } : {})
41
50
  };
42
51
  }
43
- if (!sessions && !changes && files === undefined)
52
+ if (!sessions && !changes && files === undefined && sessionNavigation === undefined)
44
53
  throw new Error('WORKSPACE_WATCH_INVALID');
45
- return { sessions, changes, files, ...(sessionGroup === undefined ? {} : { sessionGroup }) };
54
+ return {
55
+ sessions,
56
+ changes,
57
+ files,
58
+ ...(sessionGroup === undefined ? {} : { sessionGroup }),
59
+ ...(sessionNavigation === undefined ? {} : { sessionNavigation })
60
+ };
46
61
  }
47
62
  key(input) {
48
63
  return `${this.sessionHash(input)}:${this.connectionId(input)}`;
@@ -81,10 +96,11 @@ export class WorkspaceWatchService {
81
96
  if (previous !== undefined) {
82
97
  clearTimeout(previous.timer);
83
98
  this.onWatchRemoved(previous);
84
- if (previous.topics.sessions && !this.topicActive(workspaceId, 'sessions'))
99
+ if ((previous.topics.sessions || previous.topics.sessionNavigation !== undefined) &&
100
+ !this.topicActive(workspaceId, 'sessions'))
85
101
  this.onSessionWatchUnused(workspaceId);
86
102
  }
87
- if (topics.sessions)
103
+ if (topics.sessions || topics.sessionNavigation !== undefined)
88
104
  this.onSessionWatchRequired(workspaceId);
89
105
  return watch;
90
106
  }
@@ -167,7 +183,11 @@ export class WorkspaceWatchService {
167
183
  return this.watchSnapshotSequence;
168
184
  }
169
185
  topicActive(workspaceId, topic) {
170
- return [...(this.watches.get(workspaceId)?.values() ?? [])].some((watch) => topic === 'files' ? watch.topics.files !== undefined : watch.topics[topic]);
186
+ return [...(this.watches.get(workspaceId)?.values() ?? [])].some((watch) => topic === 'files'
187
+ ? watch.topics.files !== undefined
188
+ : topic === 'sessions'
189
+ ? watch.topics.sessions || watch.topics.sessionNavigation !== undefined
190
+ : watch.topics[topic]);
171
191
  }
172
192
  activeWatches(workspaceId) {
173
193
  return [...(this.watches.get(workspaceId)?.values() ?? [])].filter((watch) => watch.expiresAt > Date.now());
@@ -20,7 +20,9 @@ export class WorkspaceWatchSnapshotService {
20
20
  await Promise.all(this.subscribedTopics(watch).map(async (topic) => {
21
21
  const revision = this.readRevision(workspace.id, topic);
22
22
  try {
23
- const retained = topic === 'sessions' || previous === undefined
23
+ const retained = topic === 'sessionNavigation' ||
24
+ previous === undefined ||
25
+ (topic === 'sessions' && previous.topics.sessionGroup !== watch.topics.sessionGroup)
24
26
  ? undefined
25
27
  : this.resourceSnapshots.get(previous)?.[topic];
26
28
  const payload = retained !== undefined &&
@@ -35,7 +37,8 @@ export class WorkspaceWatchSnapshotService {
35
37
  void this.invalidate(workspace.id, topic);
36
38
  return;
37
39
  }
38
- this.signatures.set(this.viewKey(workspace.id, topic, watch), signature(payload));
40
+ if (topic !== 'sessionNavigation')
41
+ this.signatures.set(this.viewKey(workspace.id, topic, watch), signature(payload));
39
42
  this.deliver(workspace, watch, topic, payload);
40
43
  }
41
44
  catch {
@@ -64,6 +67,8 @@ export class WorkspaceWatchSnapshotService {
64
67
  this.filePrewarmTimers.delete(watch);
65
68
  }
66
69
  async invalidate(workspaceId, topic) {
70
+ if (topic === 'sessions')
71
+ void this.invalidate(workspaceId, 'sessionNavigation');
67
72
  const key = `${workspaceId}:${topic}`;
68
73
  const active = this.refreshes.get(key);
69
74
  if (active !== undefined) {
@@ -134,6 +139,18 @@ export class WorkspaceWatchSnapshotService {
134
139
  const subscribers = this.subscribers(workspaceId, topic);
135
140
  if (subscribers.length === 0)
136
141
  return;
142
+ if (topic === 'sessionNavigation') {
143
+ await Promise.all(subscribers.map(async (watch) => {
144
+ try {
145
+ const payload = await this.read(workspace, topic, watch);
146
+ this.deliver(workspace, watch, topic, payload);
147
+ }
148
+ catch {
149
+ this.fail(workspaceId, watch, topic);
150
+ }
151
+ }));
152
+ return;
153
+ }
137
154
  if (topic === 'changes') {
138
155
  const first = subscribers[0];
139
156
  if (first === undefined)
@@ -191,23 +208,28 @@ export class WorkspaceWatchSnapshotService {
191
208
  }));
192
209
  }
193
210
  read(workspace, topic, watch) {
194
- const key = topic === 'files'
195
- ? `${workspace.id}:files:${JSON.stringify(watch.topics.files)}`
196
- : `${workspace.id}:${topic}:${watch.topics.sessionGroup ?? 'ALL'}:${this.readRevision(workspace.id, topic)}`;
211
+ const key = topic === 'sessionNavigation'
212
+ ? `${workspace.id}:sessionNavigation:${watch.watchId}`
213
+ : topic === 'files'
214
+ ? `${workspace.id}:files:${JSON.stringify(watch.topics.files)}`
215
+ : `${workspace.id}:${topic}:${watch.topics.sessionGroup ?? 'ALL'}:${this.readRevision(workspace.id, topic)}`;
197
216
  const active = this.reads.get(key);
198
217
  if (active !== undefined)
199
218
  return active;
200
- const read = (topic === 'sessions'
201
- ? this.options.listSessions(workspace.id, watch.topics.sessionGroup)
202
- : topic === 'changes'
203
- ? workspace.kind === 'GIT_WORKSPACE'
204
- ? this.options.changes.current(workspace)
205
- : Promise.resolve(workspace.includeParentRepository === false
206
- ? { workspace, changes: [], repositories: [] }
207
- : null)
208
- : this.options.files
209
- .list(workspace, watch.topics.files)
210
- .then((files) => ({ files, workspace: this.options.requireWorkspace(workspace.id) }))).finally(() => {
219
+ const read = (topic === 'sessionNavigation'
220
+ ? this.options.sessionNavigation(workspace.id, watch.topics.sessionNavigation)
221
+ : topic === 'sessions'
222
+ ? this.options.listSessions(workspace.id, watch.topics.sessionGroup)
223
+ : topic === 'changes'
224
+ ? workspace.kind === 'GIT_WORKSPACE'
225
+ ? this.options.changes.current(workspace)
226
+ : Promise.resolve(workspace.includeParentRepository === false
227
+ ? { workspace, changes: [], repositories: [] }
228
+ : null)
229
+ : this.options.files.list(workspace, watch.topics.files).then((files) => ({
230
+ files,
231
+ workspace: this.options.requireWorkspace(workspace.id)
232
+ }))).finally(() => {
211
233
  if (this.reads.get(key) === read)
212
234
  this.reads.delete(key);
213
235
  });
@@ -225,9 +247,11 @@ export class WorkspaceWatchSnapshotService {
225
247
  return topic === 'changes' ? this.options.changes.revision(workspaceId) : 0;
226
248
  }
227
249
  viewKey(workspaceId, topic, watch) {
228
- return topic === 'files'
229
- ? `${workspaceId}:files:${JSON.stringify(watch.topics.files)}`
230
- : `${workspaceId}:${topic}:${watch.topics.sessionGroup ?? 'ALL'}`;
250
+ return topic === 'sessionNavigation'
251
+ ? `${workspaceId}:sessionNavigation:${watch.sessionHash}:${watch.workbenchConnectionId}`
252
+ : topic === 'files'
253
+ ? `${workspaceId}:files:${JSON.stringify(watch.topics.files)}`
254
+ : `${workspaceId}:${topic}:${watch.topics.sessionGroup ?? 'ALL'}`;
231
255
  }
232
256
  readActive(workspaceId, topic, watch) {
233
257
  if (topic === 'files' && watch !== undefined)
@@ -238,11 +262,16 @@ export class WorkspaceWatchSnapshotService {
238
262
  subscribers(workspaceId, topic) {
239
263
  return this.options.watch
240
264
  .activeWatches(workspaceId)
241
- .filter((watch) => topic === 'files' ? watch.topics.files !== undefined : watch.topics[topic]);
265
+ .filter((watch) => topic === 'files'
266
+ ? watch.topics.files !== undefined
267
+ : topic === 'sessionNavigation'
268
+ ? watch.topics.sessionNavigation !== undefined
269
+ : watch.topics[topic]);
242
270
  }
243
271
  subscribedTopics(watch) {
244
272
  return [
245
273
  ...(watch.topics.sessions ? ['sessions'] : []),
274
+ ...(watch.topics.sessionNavigation === undefined ? [] : ['sessionNavigation']),
246
275
  ...(watch.topics.files === undefined ? [] : ['files']),
247
276
  ...(watch.topics.changes ? ['changes'] : [])
248
277
  ];
@@ -250,7 +279,7 @@ export class WorkspaceWatchSnapshotService {
250
279
  deliver(workspace, watch, topic, payload) {
251
280
  if (!this.matches(workspace.id, watch))
252
281
  return;
253
- if (topic !== 'sessions') {
282
+ if (topic !== 'sessionNavigation') {
254
283
  const snapshots = this.resourceSnapshots.get(watch) ?? {};
255
284
  snapshots[topic] = { payload, revision: this.readRevision(workspace.id, topic) };
256
285
  this.resourceSnapshots.set(watch, snapshots);
@@ -272,7 +301,7 @@ export class WorkspaceWatchSnapshotService {
272
301
  fail(workspaceId, watch, topic) {
273
302
  if (!this.matches(workspaceId, watch))
274
303
  return;
275
- if (topic !== 'sessions') {
304
+ if (topic !== 'sessionNavigation') {
276
305
  const snapshots = this.resourceSnapshots.get(watch);
277
306
  if (snapshots !== undefined)
278
307
  delete snapshots[topic];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.11.68",
3
+ "version": "0.11.69",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -40,10 +40,10 @@
40
40
  "turndown-plugin-gfm": "^1.0.2",
41
41
  "ws": "^8.21.3",
42
42
  "zod": "4.4.3",
43
- "@myagentroam/agent": "0.11.68",
43
+ "@myagentroam/agent": "0.11.69",
44
44
  "@myagentroam/code-navigation": "0.11.1",
45
45
  "@myagentroam/marctl": "0.11.12",
46
- "@myagentroam/protocol": "0.11.68"
46
+ "@myagentroam/protocol": "0.11.69"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/turndown": "^5.0.6",