@yeaft/webchat-agent 1.0.413 → 1.0.415

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.
Files changed (58) hide show
  1. package/browser-runtime/browser-install.js +497 -0
  2. package/browser-runtime/cli.js +88 -0
  3. package/browser-runtime/config.js +116 -0
  4. package/browser-runtime/errors.js +8 -0
  5. package/browser-runtime/extension/manifest.json +18 -0
  6. package/browser-runtime/extension/offscreen.html +5 -0
  7. package/browser-runtime/extension/offscreen.js +101 -0
  8. package/browser-runtime/extension/popup.html +5 -0
  9. package/browser-runtime/extension/popup.js +1 -0
  10. package/browser-runtime/extension/service-worker.js +48 -0
  11. package/browser-runtime/extension.js +45 -0
  12. package/browser-runtime/index.js +5 -0
  13. package/browser-runtime/probe.js +427 -0
  14. package/browser-runtime/protocol.js +71 -0
  15. package/browser-runtime/service.js +132 -0
  16. package/browser-runtime/windows-version-job.ps1 +233 -0
  17. package/browser-runtime/windows-version-worker.js +75 -0
  18. package/browser-runtime/windows-version.js +85 -0
  19. package/cli.js +24 -7
  20. package/connection/index.js +12 -0
  21. package/context.js +1 -0
  22. package/index.js +19 -2
  23. package/llm-config-cli.js +24 -21
  24. package/local-runtime/server/client-protocol.js +14 -0
  25. package/local-runtime/server/context.js +3 -2
  26. package/local-runtime/server/handlers/agent-file-terminal.js +185 -115
  27. package/local-runtime/server/handlers/agent-output.js +3 -0
  28. package/local-runtime/server/handlers/client-misc.js +21 -4
  29. package/local-runtime/server/handlers/client-workbench.js +222 -41
  30. package/local-runtime/server/workbench-correlation.js +184 -0
  31. package/local-runtime/server/workbench-route.js +180 -0
  32. package/local-runtime/server/ws-agent.js +4 -0
  33. package/local-runtime/server/ws-client.js +25 -3
  34. package/local-runtime/version.json +1 -1
  35. package/local-runtime/web/app.bundle.js +191 -135
  36. package/local-runtime/web/app.bundle.js.gz +0 -0
  37. package/local-runtime/web/index.html +2 -2
  38. package/local-runtime/web/style.bundle.css +1 -1
  39. package/local-runtime/web/style.bundle.css.gz +0 -0
  40. package/package.json +5 -1
  41. package/service/config.js +23 -2
  42. package/service/index.js +1 -0
  43. package/service/linux.js +3 -2
  44. package/terminal.js +167 -30
  45. package/workbench/file-ops.js +21 -20
  46. package/workbench/file-search.js +4 -3
  47. package/workbench/git-ops.js +23 -22
  48. package/workbench/request-routing.js +16 -0
  49. package/yeaft/cli.js +57 -1
  50. package/yeaft/config-api.js +138 -192
  51. package/yeaft/config-store.js +192 -0
  52. package/yeaft/config.js +3 -0
  53. package/yeaft/init.js +20 -7
  54. package/yeaft/sessions/feature-flag.js +15 -33
  55. package/yeaft/sessions/session-manifest.js +114 -10
  56. package/yeaft/stdio-protocol.js +57 -0
  57. package/yeaft/storage/atomic.js +43 -17
  58. package/yeaft/tools/process-runner.js +86 -13
@@ -2,6 +2,7 @@ import { agents, userFileTabs } from '../context.js';
2
2
  import {
3
3
  sendToWebClient, forwardToAgent, broadcastAgentList
4
4
  } from '../ws-utils.js';
5
+ import { resolveWorkbenchRequest } from '../workbench-route.js';
5
6
 
6
7
  // Only Agents that explicitly advertise the package-replacement-safe updater
7
8
  // may receive remote upgrade commands. Version thresholds are insufficient:
@@ -73,11 +74,18 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
73
74
 
74
75
  // File Tab 状态保存/恢复
75
76
  case 'update_file_tabs': {
76
- if (client.userId && client.currentAgent) {
77
- const key = `${client.userId}:${client.currentAgent}`;
77
+ const ftAgentId = msg.agentId || client.currentAgent;
78
+ if (client.userId && ftAgentId) {
79
+ if (!await checkAgentAccess(ftAgentId)) break;
80
+ const resolved = resolveWorkbenchRequest(client, msg, ftAgentId);
81
+ if (!resolved) break;
82
+ const identity = resolved.routeKey
83
+ ? `${resolved.routeKey}\u0000${resolved.workspaceGeneration}`
84
+ : ftAgentId;
85
+ const key = `${client.userId}:${identity}`;
78
86
  userFileTabs.set(key, {
79
87
  files: (msg.openFiles || []).map(f => ({ path: f.path })),
80
- activeIndex: msg.activeIndex || 0,
88
+ activeIndex: Number.isFinite(msg.activeIndex) ? msg.activeIndex : 0,
81
89
  timestamp: Date.now()
82
90
  });
83
91
  }
@@ -88,10 +96,19 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
88
96
  const ftAgentId = msg.agentId || client.currentAgent;
89
97
  if (client.userId && ftAgentId) {
90
98
  if (!await checkAgentAccess(ftAgentId)) break;
91
- const key = `${client.userId}:${ftAgentId}`;
99
+ const resolved = resolveWorkbenchRequest(client, msg, ftAgentId);
100
+ if (!resolved) break;
101
+ const identity = resolved.routeKey
102
+ ? `${resolved.routeKey}\u0000${resolved.workspaceGeneration}`
103
+ : ftAgentId;
104
+ const key = `${client.userId}:${identity}`;
92
105
  const saved = userFileTabs.get(key);
93
106
  await sendToWebClient(client, {
94
107
  type: 'file_tabs_restored',
108
+ agentId: ftAgentId,
109
+ conversationId: resolved.conversationId || msg.conversationId || client.currentConversation,
110
+ workbenchRouteKey: resolved.routeKey,
111
+ workbenchWorkspaceGeneration: resolved.workspaceGeneration,
95
112
  openFiles: saved?.files || [],
96
113
  activeIndex: saved?.activeIndex || 0
97
114
  });
@@ -3,6 +3,13 @@ import {
3
3
  sendToWebClient, forwardToAgent,
4
4
  verifyConversationOwnership, getCachedDir
5
5
  } from '../ws-utils.js';
6
+ import { resolveWorkbenchRequest } from '../workbench-route.js';
7
+ import {
8
+ deleteWorkbenchRequest,
9
+ getWorkbenchTerminalOwner,
10
+ registerWorkbenchRequest,
11
+ registerWorkbenchTerminalOwner,
12
+ } from '../workbench-correlation.js';
6
13
 
7
14
  /**
8
15
  * Handle workbench messages from web client (terminal, file, git operations).
@@ -25,6 +32,101 @@ function isYeaftVirtualConversation(conversationId) {
25
32
  return typeof conversationId === 'string' && conversationId.startsWith('yeaft-');
26
33
  }
27
34
 
35
+ async function denyWorkbenchRoute(client, msg) {
36
+ console.warn(`[Security] Invalid Workbench route for ${msg?.type || 'unknown'}`);
37
+ await sendToWebClient(client, { type: 'error', message: 'Invalid Workbench Session route' });
38
+ }
39
+
40
+ const RESPONSE_TYPES = Object.freeze({
41
+ terminal_create: ['terminal_created', 'terminal_error'],
42
+ read_file: ['file_content'],
43
+ write_file: ['file_saved'],
44
+ list_directory: ['directory_listing'],
45
+ git_status: ['git_status_result'],
46
+ git_diff: ['git_diff_result'],
47
+ git_add: ['git_op_result'],
48
+ git_reset: ['git_op_result'],
49
+ git_restore: ['git_op_result'],
50
+ git_commit: ['git_op_result'],
51
+ git_push: ['git_op_result'],
52
+ file_search: ['file_search_result'],
53
+ create_file: ['file_op_result'],
54
+ delete_files: ['file_op_result'],
55
+ move_files: ['file_op_result'],
56
+ copy_files: ['file_op_result'],
57
+ upload_to_dir: ['file_op_result'],
58
+ });
59
+
60
+ function canonicalWorkbenchMessage(msg, resolved, { canonicalWorkDir = false } = {}) {
61
+ const {
62
+ _requestUserId: _ignoredUserId,
63
+ _requestClientId: _ignoredClientId,
64
+ _workbenchRequestId: _ignoredRequestId,
65
+ ...clientFields
66
+ } = msg || {};
67
+ if (resolved.legacy) return {
68
+ ...clientFields,
69
+ ...(resolved.conversationId ? { conversationId: resolved.conversationId } : {}),
70
+ };
71
+ return {
72
+ ...clientFields,
73
+ agentId: resolved.agentId,
74
+ conversationId: resolved.conversationId,
75
+ workDir: canonicalWorkDir ? resolved.workDir : resolved.requestedWorkDir,
76
+ workbenchRoute: resolved.route,
77
+ workbenchRouteKey: resolved.routeKey,
78
+ workbenchWorkspaceGeneration: resolved.workspaceGeneration,
79
+ };
80
+ }
81
+
82
+ function correlateWorkbenchRequest({ agentId, clientId, client, msg, resolved, canonical }) {
83
+ if (resolved.legacy) {
84
+ return {
85
+ ...canonical,
86
+ _requestUserId: client.userId,
87
+ _requestClientId: clientId,
88
+ };
89
+ }
90
+ const expectedResponseTypes = RESPONSE_TYPES[msg.type];
91
+ if (!expectedResponseTypes) return canonical;
92
+ const registration = {
93
+ agentId,
94
+ clientId,
95
+ userId: client.userId,
96
+ routeKey: resolved.routeKey,
97
+ conversationId: resolved.conversationId,
98
+ workspaceGeneration: resolved.workspaceGeneration,
99
+ route: resolved.route,
100
+ role: client.role,
101
+ requestType: msg.type,
102
+ expectedResponseTypes,
103
+ publicRequestId: typeof msg.requestId === 'string' ? msg.requestId : null,
104
+ terminalId: msg.terminalId || null,
105
+ };
106
+ const requestId = registerWorkbenchRequest(registration);
107
+ if (!requestId) return null;
108
+ if (msg.type === 'terminal_create'
109
+ && !registerWorkbenchTerminalOwner({ ...registration, requestId })) {
110
+ deleteWorkbenchRequest({ agentId, requestId });
111
+ return null;
112
+ }
113
+ return { ...canonical, _workbenchRequestId: requestId };
114
+ }
115
+
116
+ async function forwardCorrelatedWorkbenchRequest({ agentId, clientId, client, msg, resolved, canonical }) {
117
+ const outbound = correlateWorkbenchRequest({ agentId, clientId, client, msg, resolved, canonical });
118
+ if (!outbound) return false;
119
+ try {
120
+ await forwardToAgent(agentId, outbound);
121
+ return true;
122
+ } catch (error) {
123
+ if (outbound._workbenchRequestId) {
124
+ deleteWorkbenchRequest({ agentId, requestId: outbound._workbenchRequestId });
125
+ }
126
+ throw error;
127
+ }
128
+ }
129
+
28
130
  export async function handleClientWorkbench(clientId, client, msg, checkAgentAccess) {
29
131
  switch (msg.type) {
30
132
  // Terminal messages (forward to agent)
@@ -35,19 +137,56 @@ export async function handleClientWorkbench(clientId, client, msg, checkAgentAcc
35
137
  const termAgentId = msg.agentId || client.currentAgent;
36
138
  if (!termAgentId) return;
37
139
  if (!await checkAgentAccess(termAgentId)) return;
38
- const termConvId = msg.conversationId || client.currentConversation;
140
+ const resolved = resolveWorkbenchRequest(client, msg, termAgentId, {
141
+ allowMissingSession: msg.type === 'terminal_close',
142
+ });
143
+ if (!resolved) {
144
+ await denyWorkbenchRoute(client, msg);
145
+ return;
146
+ }
147
+ const termConvId = resolved.conversationId || msg.conversationId || client.currentConversation;
39
148
  if (!termConvId) return;
40
- if (!CONFIG.skipAuth && !isYeaftVirtualConversation(termConvId) && !verifyConversationOwnership(termConvId, client.userId, client.role)) {
149
+ if (resolved.legacy && !CONFIG.skipAuth && !isYeaftVirtualConversation(termConvId) && !verifyConversationOwnership(termConvId, client.userId, client.role)) {
41
150
  console.warn(`[Security] User ${client.userId} terminal access denied for ${termConvId}`);
42
151
  await sendToWebClient(client, { type: 'error', message: 'Permission denied' });
43
152
  return;
44
153
  }
45
- await forwardToAgent(termAgentId, {
46
- ...msg,
47
- conversationId: termConvId,
48
- _requestUserId: client.userId,
49
- _requestClientId: clientId,
154
+ let terminalResolved = { ...resolved, conversationId: termConvId };
155
+ if (!resolved.legacy && msg.type !== 'terminal_create') {
156
+ const owner = getWorkbenchTerminalOwner(termAgentId, msg.terminalId);
157
+ const ownerMatches = owner
158
+ && owner.clientId === clientId
159
+ && owner.userId === client.userId
160
+ && owner.routeKey === resolved.routeKey
161
+ && owner.conversationId === termConvId;
162
+ const generationMatches = msg.type === 'terminal_close'
163
+ ? msg.workbenchWorkspaceGeneration === owner?.workspaceGeneration
164
+ : owner?.workspaceGeneration === resolved.workspaceGeneration;
165
+ if (!ownerMatches || !generationMatches) {
166
+ await denyWorkbenchRoute(client, msg);
167
+ return;
168
+ }
169
+ if (msg.type === 'terminal_close') {
170
+ terminalResolved = {
171
+ ...terminalResolved,
172
+ workspaceGeneration: owner.workspaceGeneration,
173
+ };
174
+ }
175
+ }
176
+ const forwarded = await forwardCorrelatedWorkbenchRequest({
177
+ agentId: termAgentId,
178
+ clientId,
179
+ client,
180
+ msg,
181
+ resolved: terminalResolved,
182
+ canonical: canonicalWorkbenchMessage(msg, terminalResolved, {
183
+ canonicalWorkDir: msg.type === 'terminal_create',
184
+ }),
50
185
  });
186
+ if (!forwarded) {
187
+ await denyWorkbenchRoute(client, msg);
188
+ return;
189
+ }
51
190
  break;
52
191
  }
53
192
 
@@ -55,13 +194,20 @@ export async function handleClientWorkbench(clientId, client, msg, checkAgentAcc
55
194
  const fileAgentId = msg.agentId || client.currentAgent;
56
195
  if (!fileAgentId) { console.warn('[Server] read_file: no agentId'); return; }
57
196
  if (!await checkAgentAccess(fileAgentId)) return;
58
- const fileConvId = msg.conversationId || client.currentConversation || '_explorer';
197
+ const resolved = resolveWorkbenchRequest(client, msg, fileAgentId);
198
+ if (!resolved) {
199
+ await denyWorkbenchRoute(client, msg);
200
+ return;
201
+ }
202
+ const fileConvId = resolved.conversationId || msg.conversationId || client.currentConversation || '_explorer';
59
203
  console.log(`[Server] Forwarding read_file to agent ${fileAgentId}, conv=${fileConvId}, path=${msg.filePath}`);
60
- await forwardToAgent(fileAgentId, {
61
- ...msg,
62
- conversationId: fileConvId,
63
- _requestUserId: client.userId,
64
- _requestClientId: clientId,
204
+ await forwardCorrelatedWorkbenchRequest({
205
+ agentId: fileAgentId,
206
+ clientId,
207
+ client,
208
+ msg,
209
+ resolved,
210
+ canonical: canonicalWorkbenchMessage(msg, { ...resolved, conversationId: fileConvId }),
65
211
  });
66
212
  break;
67
213
  }
@@ -70,20 +216,27 @@ export async function handleClientWorkbench(clientId, client, msg, checkAgentAcc
70
216
  const writeAgentId = msg.agentId || client.currentAgent;
71
217
  if (!writeAgentId) return;
72
218
  if (!await checkAgentAccess(writeAgentId)) return;
73
- const writeConvId = msg.conversationId || client.currentConversation || '_explorer';
219
+ const resolved = resolveWorkbenchRequest(client, msg, writeAgentId);
220
+ if (!resolved) {
221
+ await denyWorkbenchRoute(client, msg);
222
+ return;
223
+ }
224
+ const writeConvId = resolved.conversationId || msg.conversationId || client.currentConversation || '_explorer';
74
225
  const isAgentLevelWrite = writeConvId.startsWith('_') || isYeaftVirtualConversation(writeConvId);
75
- if (!isAgentLevelWrite) {
226
+ if (resolved.legacy && !isAgentLevelWrite) {
76
227
  if (!CONFIG.skipAuth && !verifyConversationOwnership(writeConvId, client.userId, client.role)) {
77
228
  console.warn(`[Security] User ${client.userId} file write denied for ${writeConvId}`);
78
229
  await sendToWebClient(client, { type: 'error', message: 'Permission denied' });
79
230
  return;
80
231
  }
81
232
  }
82
- await forwardToAgent(writeAgentId, {
83
- ...msg,
84
- conversationId: writeConvId,
85
- _requestUserId: client.userId,
86
- _requestClientId: clientId,
233
+ await forwardCorrelatedWorkbenchRequest({
234
+ agentId: writeAgentId,
235
+ clientId,
236
+ client,
237
+ msg,
238
+ resolved,
239
+ canonical: canonicalWorkbenchMessage(msg, { ...resolved, conversationId: writeConvId }),
87
240
  });
88
241
  break;
89
242
  }
@@ -93,28 +246,40 @@ export async function handleClientWorkbench(clientId, client, msg, checkAgentAcc
93
246
  if (!dirAgentId) return;
94
247
  if (!await checkAgentAccess(dirAgentId)) return;
95
248
 
96
- // 先查缓存
97
- const cached = getCachedDir(dirAgentId, msg.dirPath);
249
+ const resolved = resolveWorkbenchRequest(client, msg, dirAgentId);
250
+ if (!resolved) {
251
+ await denyWorkbenchRoute(client, msg);
252
+ return;
253
+ }
254
+ const canonical = canonicalWorkbenchMessage(msg, {
255
+ ...resolved,
256
+ conversationId: resolved.conversationId || msg.conversationId || client.currentConversation || '_explorer',
257
+ });
258
+
259
+ // Route-scoped requests bypass the legacy Agent/path cache. Relative
260
+ // paths can mean different directories in sibling Sessions.
261
+ const cached = resolved.legacy ? getCachedDir(dirAgentId, canonical.dirPath) : null;
98
262
  if (cached) {
99
263
  await sendToWebClient(client, {
100
264
  type: 'directory_listing',
101
- conversationId: msg.conversationId,
102
- requestId: msg.requestId,
103
- dirPath: msg.dirPath,
265
+ agentId: dirAgentId,
266
+ conversationId: canonical.conversationId,
267
+ requestId: canonical.requestId,
268
+ workbenchRouteKey: canonical.workbenchRouteKey,
269
+ dirPath: canonical.dirPath,
104
270
  entries: cached,
105
271
  fromCache: true
106
272
  });
107
273
  return;
108
274
  }
109
275
 
110
- await forwardToAgent(dirAgentId, {
111
- type: 'list_directory',
112
- dirPath: msg.dirPath,
113
- workDir: msg.workDir,
114
- conversationId: msg.conversationId || client.currentConversation,
115
- requestId: msg.requestId,
116
- _requestUserId: client.userId,
117
- _requestClientId: clientId
276
+ await forwardCorrelatedWorkbenchRequest({
277
+ agentId: dirAgentId,
278
+ clientId,
279
+ client,
280
+ msg,
281
+ resolved,
282
+ canonical: { ...canonical, type: 'list_directory' },
118
283
  });
119
284
  break;
120
285
  }
@@ -130,10 +295,18 @@ export async function handleClientWorkbench(clientId, client, msg, checkAgentAcc
130
295
  const gitAgentId = msg.agentId || client.currentAgent;
131
296
  if (!gitAgentId) return;
132
297
  if (!await checkAgentAccess(gitAgentId)) return;
133
- await forwardToAgent(gitAgentId, {
134
- ...msg,
135
- conversationId: msg.conversationId || client.currentConversation,
136
- _requestUserId: client.userId
298
+ const resolved = resolveWorkbenchRequest(client, msg, gitAgentId);
299
+ if (!resolved) {
300
+ await denyWorkbenchRoute(client, msg);
301
+ return;
302
+ }
303
+ await forwardCorrelatedWorkbenchRequest({
304
+ agentId: gitAgentId,
305
+ clientId,
306
+ client,
307
+ msg,
308
+ resolved,
309
+ canonical: canonicalWorkbenchMessage(msg, resolved),
137
310
  });
138
311
  break;
139
312
  }
@@ -146,10 +319,18 @@ export async function handleClientWorkbench(clientId, client, msg, checkAgentAcc
146
319
  const fopAgentId = msg.agentId || client.currentAgent;
147
320
  if (!fopAgentId) return;
148
321
  if (!await checkAgentAccess(fopAgentId)) return;
149
- await forwardToAgent(fopAgentId, {
150
- ...msg,
151
- conversationId: msg.conversationId || client.currentConversation,
152
- _requestUserId: client.userId
322
+ const resolved = resolveWorkbenchRequest(client, msg, fopAgentId);
323
+ if (!resolved) {
324
+ await denyWorkbenchRoute(client, msg);
325
+ return;
326
+ }
327
+ await forwardCorrelatedWorkbenchRequest({
328
+ agentId: fopAgentId,
329
+ clientId,
330
+ client,
331
+ msg,
332
+ resolved,
333
+ canonical: canonicalWorkbenchMessage(msg, resolved),
153
334
  });
154
335
  break;
155
336
  }
@@ -0,0 +1,184 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ const REQUEST_TTL_MS = 2 * 60 * 1000;
4
+ const MAX_PENDING = 4096;
5
+
6
+ const pendingRequests = new Map();
7
+ const terminalOwners = new Map();
8
+
9
+ function requestKey(agentId, requestId) {
10
+ return `${String(agentId || '')}\u0000${String(requestId || '')}`;
11
+ }
12
+
13
+ function terminalKey(agentId, terminalId) {
14
+ return `${String(agentId || '')}\u0000${String(terminalId || '')}`;
15
+ }
16
+
17
+ function releasePendingTerminalReservation(pending) {
18
+ if (pending?.timeout) clearTimeout(pending.timeout);
19
+ if (!pending?.terminalId) return;
20
+ const key = terminalKey(pending.agentId, pending.terminalId);
21
+ const owner = terminalOwners.get(key);
22
+ if (owner?.pendingRequestId === pending.requestId) terminalOwners.delete(key);
23
+ }
24
+
25
+ function prune(now = Date.now()) {
26
+ for (const [key, pending] of pendingRequests) {
27
+ if (pending && pending.expiresAt > now) continue;
28
+ pendingRequests.delete(key);
29
+ releasePendingTerminalReservation(pending);
30
+ }
31
+ }
32
+
33
+ export function registerWorkbenchRequest({
34
+ agentId,
35
+ clientId,
36
+ userId,
37
+ routeKey,
38
+ conversationId,
39
+ workspaceGeneration,
40
+ route,
41
+ role = null,
42
+ requestType,
43
+ expectedResponseTypes,
44
+ publicRequestId = null,
45
+ terminalId = null,
46
+ }) {
47
+ if (!agentId || !clientId || !userId || !routeKey || !conversationId
48
+ || !workspaceGeneration || !requestType) return null;
49
+ const expected = Array.isArray(expectedResponseTypes)
50
+ ? expectedResponseTypes.filter(Boolean)
51
+ : [];
52
+ if (expected.length === 0) return null;
53
+ prune();
54
+ while (pendingRequests.size >= MAX_PENDING) {
55
+ const oldest = pendingRequests.keys().next().value;
56
+ if (oldest == null) break;
57
+ const evicted = pendingRequests.get(oldest);
58
+ pendingRequests.delete(oldest);
59
+ releasePendingTerminalReservation(evicted);
60
+ }
61
+ const requestId = randomUUID();
62
+ const key = requestKey(agentId, requestId);
63
+ const pending = {
64
+ agentId,
65
+ requestId,
66
+ clientId,
67
+ userId,
68
+ routeKey,
69
+ conversationId,
70
+ workspaceGeneration,
71
+ route: route ? { ...route } : null,
72
+ role,
73
+ requestType,
74
+ expectedResponseTypes: new Set(expected),
75
+ publicRequestId,
76
+ terminalId,
77
+ expiresAt: Date.now() + REQUEST_TTL_MS,
78
+ timeout: null,
79
+ };
80
+ pending.timeout = setTimeout(() => {
81
+ const current = pendingRequests.get(key);
82
+ if (current !== pending) return;
83
+ pendingRequests.delete(key);
84
+ releasePendingTerminalReservation(pending);
85
+ }, REQUEST_TTL_MS);
86
+ pending.timeout.unref?.();
87
+ pendingRequests.set(key, pending);
88
+ return requestId;
89
+ }
90
+
91
+ export function consumeWorkbenchRequest({ agentId, requestId, responseType, routeKey = null }) {
92
+ if (!agentId || !requestId || !responseType) return null;
93
+ prune();
94
+ const key = requestKey(agentId, requestId);
95
+ const pending = pendingRequests.get(key);
96
+ if (!pending || !pending.expectedResponseTypes.has(responseType)) return null;
97
+ if (routeKey && pending.routeKey !== routeKey) return null;
98
+ pendingRequests.delete(key);
99
+ if (pending.timeout) clearTimeout(pending.timeout);
100
+ pending.timeout = null;
101
+ return pending;
102
+ }
103
+
104
+ export function deleteWorkbenchRequest({ agentId, requestId }) {
105
+ const key = requestKey(agentId, requestId);
106
+ const pending = pendingRequests.get(key);
107
+ const deleted = pendingRequests.delete(key);
108
+ if (deleted) releasePendingTerminalReservation(pending);
109
+ return deleted;
110
+ }
111
+
112
+ export function registerWorkbenchTerminalOwner(pending) {
113
+ if (!pending?.agentId || !pending?.terminalId || !pending?.clientId
114
+ || !pending?.userId || !pending?.routeKey || !pending?.workspaceGeneration) return false;
115
+ const key = terminalKey(pending.agentId, pending.terminalId);
116
+ const existing = terminalOwners.get(key);
117
+ if (existing && (
118
+ existing.clientId !== pending.clientId
119
+ || existing.userId !== pending.userId
120
+ || existing.routeKey !== pending.routeKey
121
+ || existing.workspaceGeneration !== pending.workspaceGeneration
122
+ || existing.conversationId !== pending.conversationId
123
+ )) return false;
124
+ terminalOwners.set(key, {
125
+ agentId: pending.agentId,
126
+ terminalId: pending.terminalId,
127
+ clientId: pending.clientId,
128
+ userId: pending.userId,
129
+ routeKey: pending.routeKey,
130
+ conversationId: pending.conversationId,
131
+ workspaceGeneration: pending.workspaceGeneration,
132
+ pendingRequestId: pending.requestId || pending.pendingRequestId || null,
133
+ });
134
+ return true;
135
+ }
136
+
137
+ export function getWorkbenchTerminalOwner(agentId, terminalId) {
138
+ return terminalOwners.get(terminalKey(agentId, terminalId)) || null;
139
+ }
140
+
141
+ export function deleteWorkbenchTerminalOwner(agentId, terminalId) {
142
+ return terminalOwners.delete(terminalKey(agentId, terminalId));
143
+ }
144
+
145
+ export function clearWorkbenchCorrelationsForClient(clientId) {
146
+ if (!clientId) return [];
147
+ for (const [key, pending] of pendingRequests) {
148
+ if (pending?.clientId !== clientId) continue;
149
+ pendingRequests.delete(key);
150
+ releasePendingTerminalReservation(pending);
151
+ }
152
+ const terminals = [];
153
+ for (const [key, owner] of terminalOwners) {
154
+ if (owner?.clientId !== clientId) continue;
155
+ terminals.push(owner);
156
+ terminalOwners.delete(key);
157
+ }
158
+ return terminals;
159
+ }
160
+
161
+ export function clearWorkbenchCorrelationsForAgent(agentId) {
162
+ if (!agentId) return;
163
+ for (const [key, pending] of pendingRequests) {
164
+ if (pending?.agentId !== agentId) continue;
165
+ pendingRequests.delete(key);
166
+ releasePendingTerminalReservation(pending);
167
+ }
168
+ for (const [key, owner] of terminalOwners) {
169
+ if (owner?.agentId === agentId) terminalOwners.delete(key);
170
+ }
171
+ }
172
+
173
+ export function __testResetWorkbenchCorrelations() {
174
+ for (const pending of pendingRequests.values()) releasePendingTerminalReservation(pending);
175
+ pendingRequests.clear();
176
+ terminalOwners.clear();
177
+ }
178
+
179
+ export function __testExpireWorkbenchRequest(agentId, requestId) {
180
+ const pending = pendingRequests.get(requestKey(agentId, requestId));
181
+ if (!pending) return false;
182
+ pending.expiresAt = 0;
183
+ return true;
184
+ }