linco-connect 1.0.7 → 1.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linco-connect",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "自研 IM 桥接多 Agent 服务",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -23,8 +23,8 @@ function resolvePendingDanger(confirmed, ws, session, config) {
23
23
  return providerFor(session).resolvePendingDanger?.(confirmed, ws, session, config) || false;
24
24
  }
25
25
 
26
- function resolvePendingPermission(allowed, ws, session, config) {
27
- return providerFor(session).resolvePendingPermission?.(allowed, ws, session, config) || false;
26
+ function resolvePendingPermission(allowed, ws, session, config, requestId) {
27
+ return providerFor(session).resolvePendingPermission?.(allowed, ws, session, config, requestId) || false;
28
28
  }
29
29
 
30
30
  function stopAgentProcess(session, options = {}) {
@@ -4,6 +4,13 @@ const { send, sendError, sendSystem } = require('../protocol');
4
4
  const { persistAgentSessionId, stopAgentProcess: stopSessionProcess, updateAgentSessionHistory } = require('../session');
5
5
  const { getOutboxDir } = require('../outgoingAttachmentHandler');
6
6
  const { createTextStreamBuffer, appendTextStream, flushTextStream, resetTextStream } = require('../streamBuffer');
7
+ const {
8
+ clearPendingPermissions,
9
+ getPendingPermission,
10
+ pendingPermissionIds,
11
+ removePendingPermission,
12
+ setPendingPermission,
13
+ } = require('../permissionState');
7
14
 
8
15
  function execute(input, ws, session, config) {
9
16
  const textForCheck = stringifyInput(input);
@@ -44,6 +51,7 @@ function runAppServerTurn(input, ws, session, config) {
44
51
  session.currentInputForNoOutput = input;
45
52
  session.sawPartialAssistantText = false;
46
53
  session.codexAssistantEnded = false;
54
+ session.codexEmittedAgentMessageIds = new Set();
47
55
  resetCodexAssistantText(session);
48
56
  session._lastWs = ws;
49
57
  session._lastConfig = config;
@@ -165,6 +173,7 @@ function ensureAppServer(session, config) {
165
173
  session.codexAppServer = null;
166
174
  }
167
175
  clearTurnState(session);
176
+ clearPendingPermissions(session, 'codex');
168
177
  // drain pending requests
169
178
  for (const [, pending] of session.codexPendingRequests) {
170
179
  pending.reject(new Error(`app-server 已退出,code=${code}`));
@@ -298,6 +307,31 @@ function resetCodexAssistantText(session) {
298
307
  resetTextStream(ensureCodexStreamState(session));
299
308
  }
300
309
 
310
+ function codexAgentMessageId(params) {
311
+ return String(params.item?.id || params.itemId || params.id || '').trim();
312
+ }
313
+
314
+ function markCodexAgentMessageEmitted(session, itemId) {
315
+ const id = String(itemId || '').trim();
316
+ if (!id) return;
317
+ if (!(session.codexEmittedAgentMessageIds instanceof Set)) {
318
+ session.codexEmittedAgentMessageIds = new Set();
319
+ }
320
+ session.codexEmittedAgentMessageIds.add(id);
321
+ }
322
+
323
+ function hasCodexAgentMessageEmitted(session, itemId) {
324
+ const id = String(itemId || '').trim();
325
+ return Boolean(id && session.codexEmittedAgentMessageIds?.has(id));
326
+ }
327
+
328
+ function shouldAppendCompletedAgentMessage(session, params) {
329
+ const itemId = codexAgentMessageId(params);
330
+ if (itemId) return !hasCodexAgentMessageEmitted(session, itemId);
331
+ if (!session.sawPartialAssistantText) return true;
332
+ return params.item?.phase === 'final_answer';
333
+ }
334
+
301
335
  function nextRpcId(session) {
302
336
  session.codexRpcId = (session.codexRpcId || 0) + 1;
303
337
  return session.codexRpcId;
@@ -357,16 +391,16 @@ function handleServerRequest(message, session) {
357
391
  const cmd = params.command || params.tool || '';
358
392
  session._log?.info('codex command execution approval requested', { method, command: cmd });
359
393
 
360
- if (session.pendingPermission?.requestId === String(message.id)) return;
394
+ if (getPendingPermission(session, String(message.id), 'codex')) return;
361
395
 
362
- session.pendingPermission = {
396
+ setPendingPermission(session, {
363
397
  provider: 'codex',
364
398
  requestId: String(message.id),
365
399
  toolName: 'exec',
366
400
  input: cmd,
367
401
  _codexMethod: method,
368
402
  _rpcId: message.id,
369
- };
403
+ });
370
404
 
371
405
  if (ws) {
372
406
  send(ws, 'permission_request', {
@@ -509,6 +543,7 @@ function handleAppServerMessage(message, session) {
509
543
  const delta = params.delta || '';
510
544
  if (delta) {
511
545
  appendCodexAssistantText(delta, ws, session, () => send(ws, 'assistant_start', {}));
546
+ markCodexAgentMessageEmitted(session, codexAgentMessageId(params));
512
547
  }
513
548
  return;
514
549
  }
@@ -533,10 +568,13 @@ function handleAppServerMessage(message, session) {
533
568
  return;
534
569
  }
535
570
 
536
- // final content fallback if no deltas were emitted
571
+ // final content fallback if this item did not emit deltas
537
572
  const text = extractFinalText(params);
538
- if (text && !session.sawPartialAssistantText) {
573
+ const agentMessageId = codexAgentMessageId(params);
574
+ if (text && shouldAppendCompletedAgentMessage(session, params)) {
575
+ if (session.sawPartialAssistantText) appendCodexAssistantText('\n\n', ws, session, () => send(ws, 'assistant_start', {}));
539
576
  appendCodexAssistantText(text, ws, session, () => send(ws, 'assistant_start', {}));
577
+ markCodexAgentMessageEmitted(session, agentMessageId);
540
578
  }
541
579
  // Safety fallback: if turn/completed never arrives, clear isTurnActive so the next message doesn't get stuck.
542
580
  if (session.turnCompletedTimerId) clearTimeout(session.turnCompletedTimerId);
@@ -890,13 +928,17 @@ function resolvePendingDanger(confirmed, ws, session, config) {
890
928
  return true;
891
929
  }
892
930
 
893
- function resolvePendingPermission(approved, ws, session, config) {
894
- if (!session.pendingPermission) return false;
895
-
896
- const pending = session.pendingPermission;
897
- if (pending.provider !== 'codex') return false;
931
+ function resolvePendingPermission(approved, ws, session, config, requestId) {
932
+ const pending = getPendingPermission(session, requestId, 'codex');
933
+ if (!pending) {
934
+ session._log?.warn('codex permission response without pending request', {
935
+ requestId: requestId || '',
936
+ pendingRequestIds: pendingPermissionIds(session, 'codex'),
937
+ });
938
+ return false;
939
+ }
898
940
 
899
- session.pendingPermission = null;
941
+ removePendingPermission(session, pending.requestId);
900
942
  session._log?.info('codex permission response', { approved, toolName: pending.toolName, rpcId: pending._rpcId });
901
943
 
902
944
  // Respond to Codex RPC
@@ -932,6 +974,7 @@ function stop(session, options = {}) {
932
974
  }
933
975
  stopSessionProcess(session, options);
934
976
  clearTurnState(session);
977
+ clearPendingPermissions(session, 'codex');
935
978
  if (session.codexPendingRequests) {
936
979
  for (const [, pending] of session.codexPendingRequests) {
937
980
  pending.reject(new Error('用户已停止'));
@@ -4,6 +4,13 @@ const { persistAgentSessionId, stopAgentProcess: stopSessionProcess, updateAgent
4
4
  const { getOutboxDir } = require('../outgoingAttachmentHandler');
5
5
  const { createTextStreamBuffer, appendTextStream, flushTextStream, resetTextStream } = require('../streamBuffer');
6
6
  const { DEFAULT_GATEWAY_URL, ensureHermesGateway, normalizeGatewayUrl } = require('../hermesGateway');
7
+ const {
8
+ clearPendingPermissions,
9
+ getPendingPermission,
10
+ pendingPermissionIds,
11
+ removePendingPermission,
12
+ setPendingPermission,
13
+ } = require('../permissionState');
7
14
 
8
15
  function extractText(input) {
9
16
  if (!Array.isArray(input)) return String(input || '');
@@ -178,16 +185,26 @@ function handleHermesEvent(event, ws, session, config) {
178
185
  }
179
186
 
180
187
  function handleApprovalRequest(event, ws, session) {
181
- const requestId = `hermes-${event.run_id || session.hermesRunId}-${Date.now()}`;
188
+ const requestId = String(
189
+ event.requestId ||
190
+ event.request_id ||
191
+ event.approvalId ||
192
+ event.approval_id ||
193
+ event.id ||
194
+ `hermes-${event.run_id || session.hermesRunId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
195
+ );
182
196
  const input = event.command || event.description || event.preview || '';
183
- session.pendingPermission = {
197
+
198
+ if (getPendingPermission(session, requestId, 'hermes')) return;
199
+
200
+ setPendingPermission(session, {
184
201
  provider: 'hermes',
185
202
  requestId,
186
203
  runId: event.run_id || session.hermesRunId,
187
204
  toolName: 'exec',
188
205
  input,
189
206
  choices: event.choices || [],
190
- };
207
+ });
191
208
  send(ws, 'permission_request', {
192
209
  requestId,
193
210
  toolName: 'exec',
@@ -222,7 +239,7 @@ function finishTurn(ws, session, config, options = {}) {
222
239
  session.hermesGatewayUrl = null;
223
240
  session.isTurnActive = false;
224
241
  session.currentInputForNoOutput = null;
225
- session.pendingPermission = null;
242
+ clearPendingPermissions(session, 'hermes');
226
243
  flushHermesAssistantText(ws, session);
227
244
  resetHermesAssistantText(session);
228
245
  if (drain) drainQueue(ws, session, config);
@@ -358,14 +375,21 @@ async function resolvePendingDanger(confirmed, ws, session, config) {
358
375
  return true;
359
376
  }
360
377
 
361
- async function resolvePendingPermission(approved, ws, session, config) {
362
- const pending = session.pendingPermission;
363
- if (!pending || pending.provider !== 'hermes') return false;
378
+ async function resolvePendingPermission(approved, ws, session, config, requestId) {
379
+ const pending = getPendingPermission(session, requestId, 'hermes');
380
+ if (!pending) {
381
+ config.logger?.warn('hermes permission response without pending request', {
382
+ sessionId: session.id,
383
+ requestId: requestId || '',
384
+ pendingRequestIds: pendingPermissionIds(session, 'hermes'),
385
+ });
386
+ return false;
387
+ }
364
388
 
365
389
  const agentConfig = config.agents?.hermes || {};
366
390
  const gatewayUrl = session.hermesGatewayUrl || normalizeGatewayUrl(agentConfig.gatewayUrl || agentConfig.baseUrl || DEFAULT_GATEWAY_URL);
367
391
  const runId = pending.runId || session.hermesRunId;
368
- session.pendingPermission = null;
392
+ removePendingPermission(session, pending.requestId);
369
393
 
370
394
  try {
371
395
  await fetchJson(`${gatewayUrl}/v1/runs/${encodeURIComponent(runId)}/approval`, agentConfig, {
@@ -6,6 +6,13 @@ const { buildOutboxSystemPrompt, getOutboxDir } = require('./outgoingAttachmentH
6
6
  const { send, sendError, sendSystem } = require('./protocol');
7
7
  const { appendTextStream, flushTextStream, resetTextStream } = require('./streamBuffer');
8
8
  const { persistClaudeSessionId, stopAgentProcess, updateAgentSessionHistory } = require('./session');
9
+ const {
10
+ getPendingPermission,
11
+ hasPendingPermissions,
12
+ pendingPermissionIds,
13
+ removePendingPermission,
14
+ setPendingPermission,
15
+ } = require('./permissionState');
9
16
 
10
17
  function executeClaudeQuery(input, ws, session, config) {
11
18
  const textForCheck = typeof input === 'string' ? input : extractText(input);
@@ -319,9 +326,7 @@ function handleClaudeMessage(parsed, ws, session, config) {
319
326
  handleControlRequest(parsed, ws, session, config);
320
327
  break;
321
328
  case 'control_cancel_request':
322
- if (session.pendingPermission?.requestId === parsed.request_id) {
323
- session.pendingPermission = null;
324
- }
329
+ removePendingPermission(session, parsed.request_id);
325
330
  break;
326
331
  }
327
332
  }
@@ -423,11 +428,22 @@ function handleControlRequest(parsed, ws, session, config) {
423
428
  const input = sanitizeToolInput(toolName, request.input || {});
424
429
  const inputText = summarizeInput(input);
425
430
 
426
- session.pendingPermission = {
431
+ if (requestID && getPendingPermission(session, requestID, 'claude')) {
432
+ config.logger?.info('duplicate permission request ignored', {
433
+ sessionId: session.id,
434
+ requestId: requestID,
435
+ toolName,
436
+ });
437
+ return;
438
+ }
439
+
440
+ const pending = {
441
+ provider: 'claude',
427
442
  requestId: requestID,
428
443
  toolName,
429
444
  input,
430
445
  };
446
+ setPendingPermission(session, pending);
431
447
 
432
448
  config.logger?.info('permission request received', {
433
449
  sessionId: session.id,
@@ -442,8 +458,8 @@ function handleControlRequest(parsed, ws, session, config) {
442
458
  });
443
459
  }
444
460
 
445
- function respondPermission(session, approved) {
446
- const pending = session.pendingPermission;
461
+ function respondPermission(session, approved, requestId) {
462
+ const pending = getPendingPermission(session, requestId, 'claude');
447
463
  if (!pending) return false;
448
464
 
449
465
  const response = approved
@@ -458,16 +474,23 @@ function respondPermission(session, approved) {
458
474
  response,
459
475
  },
460
476
  });
461
- session.pendingPermission = null;
477
+ removePendingPermission(session, pending.requestId);
462
478
  return true;
463
479
  }
464
480
 
465
- function resolvePendingPermission(approved, ws, session, config) {
466
- if (!session.pendingPermission) return false;
481
+ function resolvePendingPermission(approved, ws, session, config, requestId) {
482
+ const pending = getPendingPermission(session, requestId, 'claude');
483
+ if (!pending) {
484
+ config.logger?.warn('permission response without pending request', {
485
+ sessionId: session.id,
486
+ requestId: requestId || '',
487
+ pendingRequestIds: pendingPermissionIds(session, 'claude'),
488
+ });
489
+ return false;
490
+ }
467
491
 
468
492
  try {
469
- const pending = session.pendingPermission;
470
- respondPermission(session, approved);
493
+ respondPermission(session, approved, pending.requestId);
471
494
  config.logger?.info('permission response sent', {
472
495
  sessionId: session.id,
473
496
  requestId: pending?.requestId,
@@ -499,7 +522,7 @@ function resolvePendingDanger(approved, ws, session, config) {
499
522
  }
500
523
 
501
524
  function drainMessageQueue(ws, session, config) {
502
- if (session.isTurnActive || session.pendingPermission) return;
525
+ if (session.isTurnActive || hasPendingPermissions(session, 'claude')) return;
503
526
  const next = session.messageQueue.shift();
504
527
  if (!next) return;
505
528
  config.logger?.info('message dequeued', { sessionId: session.id, queueLength: session.messageQueue.length });
@@ -222,7 +222,7 @@ class ImConnector {
222
222
  return;
223
223
  }
224
224
 
225
- if (!resolvePendingPermission(!!msg.approved, session.ws, session, this.config)) {
225
+ if (!resolvePendingPermission(!!msg.approved, session.ws, session, this.config, msg.requestId)) {
226
226
  sendError(session.ws, '没有待确认的工具权限请求');
227
227
  }
228
228
  }
@@ -0,0 +1,83 @@
1
+ function ensurePendingPermissionMap(session) {
2
+ if (session.pendingPermissions instanceof Map) return session.pendingPermissions;
3
+
4
+ const map = new Map();
5
+ if (session.pendingPermission?.requestId) {
6
+ map.set(String(session.pendingPermission.requestId), session.pendingPermission);
7
+ }
8
+ session.pendingPermissions = map;
9
+ return map;
10
+ }
11
+
12
+ function setPendingPermission(session, pending) {
13
+ if (!pending?.requestId) return;
14
+ const map = ensurePendingPermissionMap(session);
15
+ map.set(String(pending.requestId), pending);
16
+ syncLegacyPendingPermission(session);
17
+ }
18
+
19
+ function getPendingPermission(session, requestId, provider) {
20
+ const map = ensurePendingPermissionMap(session);
21
+ const normalized = String(requestId || '').trim();
22
+
23
+ if (normalized) {
24
+ const pending = map.get(normalized) || null;
25
+ return matchesProvider(pending, provider) ? pending : null;
26
+ }
27
+
28
+ const matching = Array.from(map.values()).filter(pending => matchesProvider(pending, provider));
29
+ return matching.length === 1 ? matching[0] : null;
30
+ }
31
+
32
+ function removePendingPermission(session, requestId) {
33
+ const map = ensurePendingPermissionMap(session);
34
+ const normalized = String(requestId || '').trim();
35
+ if (normalized) map.delete(normalized);
36
+ syncLegacyPendingPermission(session);
37
+ }
38
+
39
+ function clearPendingPermissions(session, provider) {
40
+ const map = ensurePendingPermissionMap(session);
41
+ if (!provider) {
42
+ map.clear();
43
+ syncLegacyPendingPermission(session);
44
+ return;
45
+ }
46
+
47
+ for (const [requestId, pending] of map) {
48
+ if (matchesProvider(pending, provider)) map.delete(requestId);
49
+ }
50
+ syncLegacyPendingPermission(session);
51
+ }
52
+
53
+ function hasPendingPermissions(session, provider) {
54
+ const map = ensurePendingPermissionMap(session);
55
+ if (!provider) return map.size > 0;
56
+ return Array.from(map.values()).some(pending => matchesProvider(pending, provider));
57
+ }
58
+
59
+ function pendingPermissionIds(session, provider) {
60
+ return Array.from(ensurePendingPermissionMap(session).values())
61
+ .filter(pending => matchesProvider(pending, provider))
62
+ .map(pending => String(pending.requestId));
63
+ }
64
+
65
+ function matchesProvider(pending, provider) {
66
+ if (!pending) return false;
67
+ return !provider || pending.provider === provider;
68
+ }
69
+
70
+ function syncLegacyPendingPermission(session) {
71
+ const remaining = Array.from(ensurePendingPermissionMap(session).values());
72
+ session.pendingPermission = remaining.length ? remaining[remaining.length - 1] : null;
73
+ }
74
+
75
+ module.exports = {
76
+ clearPendingPermissions,
77
+ ensurePendingPermissionMap,
78
+ getPendingPermission,
79
+ hasPendingPermissions,
80
+ pendingPermissionIds,
81
+ removePendingPermission,
82
+ setPendingPermission,
83
+ };
package/src/session.js CHANGED
@@ -61,6 +61,7 @@ function createSession(config, { externalSessionId, agentType = 'claude' } = {})
61
61
  messageQueue: [],
62
62
  pendingDanger: null,
63
63
  pendingPermission: null,
64
+ pendingPermissions: new Map(),
64
65
  streamState: createStreamState(),
65
66
  sawPartialAssistantText: false,
66
67
  outgoingAttachments: new Map(),
@@ -294,6 +295,8 @@ function resetConversationState(session, { clearAgentSession = true, clearClaude
294
295
  session.messageQueue = [];
295
296
  session.pendingDanger = null;
296
297
  session.pendingPermission = null;
298
+ if (session.pendingPermissions?.clear) session.pendingPermissions.clear();
299
+ else session.pendingPermissions = new Map();
297
300
  session.sawPartialAssistantText = false;
298
301
  clearStreamState(session);
299
302
  }
@@ -4,6 +4,7 @@ const { resetOutgoingAttachments, startOutboxWatcher, stopOutboxWatcher } = requ
4
4
  const { sendError, sendSystem } = require('./protocol');
5
5
  const { removeAgentSessionFromHistory, saveSessionMetadata } = require('./session');
6
6
  const { stopAgentProcess } = require('./agentRunner');
7
+ const { clearPendingPermissions } = require('./permissionState');
7
8
 
8
9
  function localCommandsHelp() {
9
10
  return `📋 Linco 本地命令:
@@ -276,7 +277,7 @@ function handleSwitch(indexOrId, ws, session, config) {
276
277
  session.currentInputForNoOutput = null;
277
278
  session.messageQueue = [];
278
279
  session.pendingDanger = null;
279
- session.pendingPermission = null;
280
+ clearPendingPermissions(session);
280
281
  }
281
282
 
282
283
  // Verify the change persisted correctly
package/src/wsServer.js CHANGED
@@ -173,7 +173,7 @@ function handleMessage(data, ws, session, config) {
173
173
  }
174
174
  if (msg.type === 'permission_response') {
175
175
  config.logger?.info('permission response received (linco)', { sessionKey: msg.sessionKey, approved: !!msg.approved });
176
- if (!resolvePendingPermission(!!msg.approved, session.ws, session, config)) {
176
+ if (!resolvePendingPermission(!!msg.approved, session.ws, session, config, msg.requestId)) {
177
177
  sendError(session.ws, '❌ 没有待确认的工具权限请求');
178
178
  }
179
179
  return;
@@ -192,7 +192,7 @@ function handleMessage(data, ws, session, config) {
192
192
 
193
193
  if (msg.type === 'permission_response') {
194
194
  config.logger?.info('permission response received', { sessionId: session.id, approved: !!msg.approved });
195
- if (!resolvePendingPermission(!!msg.approved, ws, session, config)) {
195
+ if (!resolvePendingPermission(!!msg.approved, ws, session, config, msg.requestId)) {
196
196
  sendError(ws, '❌ 没有待确认的工具权限请求');
197
197
  }
198
198
  return;