linco-connect 1.1.5 → 1.1.7

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/agents/codex.js +120 -51
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linco-connect",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "自研 IM 桥接多 Agent 服务",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -12,6 +12,8 @@ const {
12
12
  setPendingPermission,
13
13
  } = require('../permissionState');
14
14
 
15
+ const CODEX_TURN_COMPLETION_FALLBACK_MS = 1000;
16
+
15
17
  function execute(input, ws, session, config) {
16
18
  const textForCheck = stringifyInput(input);
17
19
  if (isDangerousCommand(textForCheck) && session.autoApprove !== true) {
@@ -52,6 +54,7 @@ function runAppServerTurn(input, ws, session, config) {
52
54
  session.sawPartialAssistantText = false;
53
55
  session.codexAssistantEnded = false;
54
56
  session.codexEmittedAgentMessageIds = new Set();
57
+ session.codexToolStates = new Map();
55
58
  resetCodexAssistantText(session);
56
59
  session._lastWs = ws;
57
60
  session._lastConfig = config;
@@ -286,6 +289,35 @@ function clearTurnState(session) {
286
289
  }
287
290
  }
288
291
 
292
+ function finishCodexTurn(ws, session, config, reason = 'completed', payload = {}) {
293
+ if (!session.isTurnActive) return;
294
+
295
+ if (reason === 'completed') {
296
+ updateCodexSessionStats(session, payload);
297
+ }
298
+
299
+ clearTurnState(session);
300
+ if (session.sawPartialAssistantText) {
301
+ sendCodexAssistantEnd(ws, session);
302
+ } else {
303
+ sendSystem(ws, 'Codex 本次执行没有输出。');
304
+ }
305
+ sendTurnEnd(ws, session, reason, payload);
306
+ if (config) drainQueue(ws, session, config);
307
+ }
308
+
309
+ function isFinalCodexAssistantItem(params) {
310
+ const item = params.item || {};
311
+ return isCodexAssistantMessageType(item.type) && item.phase === 'final_answer';
312
+ }
313
+
314
+ function armCodexTurnCompletionFallback(ws, session, config) {
315
+ if (session.turnCompletedTimerId) clearTimeout(session.turnCompletedTimerId);
316
+ session.turnCompletedTimerId = setTimeout(() => {
317
+ finishCodexTurn(ws, session, config, 'completed');
318
+ }, CODEX_TURN_COMPLETION_FALLBACK_MS);
319
+ }
320
+
289
321
  function ensureCodexStreamState(session) {
290
322
  if (!session.codexStreamState) {
291
323
  session.codexStreamState = createTextStreamBuffer();
@@ -327,6 +359,68 @@ function hasCodexAgentMessageEmitted(session, itemId) {
327
359
  return Boolean(id && session.codexEmittedAgentMessageIds?.has(id));
328
360
  }
329
361
 
362
+ function ensureCodexToolStates(session) {
363
+ if (!(session.codexToolStates instanceof Map)) {
364
+ session.codexToolStates = new Map();
365
+ }
366
+ return session.codexToolStates;
367
+ }
368
+
369
+ function codexToolStateFor(session, id) {
370
+ const toolId = String(id || '').trim();
371
+ if (!toolId) return null;
372
+ const states = ensureCodexToolStates(session);
373
+ return states.get(toolId) || null;
374
+ }
375
+
376
+ function setCodexToolState(session, id, next) {
377
+ const toolId = String(id || '').trim();
378
+ if (!toolId) return;
379
+ const states = ensureCodexToolStates(session);
380
+ const previous = states.get(toolId) || {};
381
+ states.set(toolId, {
382
+ ...previous,
383
+ ...next,
384
+ });
385
+ }
386
+
387
+ function emitCodexToolCall(ws, session, tool) {
388
+ if (!ws) return false;
389
+ const id = String(tool.id || '').trim();
390
+ const existing = codexToolStateFor(session, id);
391
+ if (existing?.phase === 'completed' || existing?.phase === 'started') {
392
+ return false;
393
+ }
394
+ const name = String(tool.name || existing?.name || 'tool').trim() || 'tool';
395
+ const input = tool.input ?? existing?.input ?? '';
396
+ send(ws, 'tool_call', { id, name, input });
397
+ setCodexToolState(session, id, { phase: 'started', name, input });
398
+ return true;
399
+ }
400
+
401
+ function emitCodexToolResult(ws, session, tool) {
402
+ if (!ws) return false;
403
+ const id = String(tool.id || '').trim();
404
+ const existing = codexToolStateFor(session, id);
405
+ if (existing?.phase === 'completed') return false;
406
+
407
+ const name = String(tool.name || existing?.name || 'tool').trim() || 'tool';
408
+ const input = tool.input ?? existing?.input ?? '';
409
+ if (id && existing?.phase !== 'started') {
410
+ emitCodexToolCall(ws, session, { id, name, input });
411
+ }
412
+
413
+ const output = tool.output ?? '';
414
+ send(ws, 'tool_result', { id, output });
415
+ setCodexToolState(session, id, {
416
+ phase: 'completed',
417
+ name,
418
+ input,
419
+ output,
420
+ });
421
+ return true;
422
+ }
423
+
330
424
  function shouldAppendCompletedAgentMessage(session, params) {
331
425
  const itemId = codexAgentMessageId(params);
332
426
  if (itemId) return !hasCodexAgentMessageEmitted(session, itemId);
@@ -483,14 +577,12 @@ function handleServerRequest(message, session) {
483
577
 
484
578
  // Tool call from server request
485
579
  if (method === 'item/tool/call') {
486
- if (ws) {
487
- const toolName = params.name || params.tool || '';
488
- send(ws, 'tool_call', {
489
- id: String(message.id),
490
- name: toolName,
491
- input: JSON.stringify(params.input || {}).slice(0, 300),
492
- });
493
- }
580
+ const toolName = params.name || params.tool || '';
581
+ emitCodexToolCall(ws, session, {
582
+ id: String(message.id),
583
+ name: toolName,
584
+ input: JSON.stringify(params.input || {}).slice(0, 300),
585
+ });
494
586
  sendJsonRpc(session.codexAppServer, {
495
587
  jsonrpc: '2.0',
496
588
  id: message.id,
@@ -574,16 +666,19 @@ function handleAppServerMessage(message, session) {
574
666
  session._log?.info('codex item completed', { itemType, item: summarizeCodexItemForLog(params.item) });
575
667
  if (itemType === 'toolCall' || itemType === 'commandExecution' || itemType === 'webSearch') {
576
668
  const itemId = params.item?.id || params.itemId || '';
577
- if (itemType === 'webSearch' && params.item?.query) {
578
- send(ws, 'tool_call', {
579
- id: itemId,
580
- name: 'webSearch',
581
- input: params.item.query,
582
- });
583
- }
669
+ const isCommand = itemType === 'commandExecution';
670
+ const isWebSearch = itemType === 'webSearch';
671
+ const toolName = isCommand ? 'exec' : isWebSearch ? 'webSearch' : (params.item?.name || params.item?.tool || '');
672
+ const toolInput = isCommand
673
+ ? (params.item?.command || '')
674
+ : isWebSearch
675
+ ? (params.item?.query || params.item?.input || params.item?.arguments || {})
676
+ : (params.item?.input || params.item?.arguments || {});
584
677
  const output = params.item?.output || params.item?.result || params.item?.results || params.output || params.result || '';
585
- send(ws, 'tool_result', {
678
+ emitCodexToolResult(ws, session, {
586
679
  id: itemId,
680
+ name: toolName,
681
+ input: typeof toolInput === 'string' ? toolInput : JSON.stringify(toolInput).slice(0, 300),
587
682
  output: typeof output === 'string' ? output : JSON.stringify(output).slice(0, 1000),
588
683
  });
589
684
  return;
@@ -597,42 +692,16 @@ function handleAppServerMessage(message, session) {
597
692
  appendCodexAssistantText(text, ws, session, () => send(ws, 'assistant_start', {}));
598
693
  markCodexAgentMessageEmitted(session, agentMessageId);
599
694
  }
600
- // Safety fallback: if turn/completed never arrives, clear isTurnActive so the next message doesn't get stuck.
601
- if (session.turnCompletedTimerId) clearTimeout(session.turnCompletedTimerId);
602
- session.turnCompletedTimerId = setTimeout(() => {
603
- if (session.isTurnActive) {
604
- session.isTurnActive = false;
605
- session.currentInputForNoOutput = null;
606
- if (session.sawPartialAssistantText) {
607
- sendCodexAssistantEnd(ws, session);
608
- } else {
609
- sendSystem(ws, 'Codex 本次执行没有输出。');
610
- }
611
- sendTurnEnd(ws, session, 'timeout');
612
- const cfg = session._lastConfig;
613
- if (cfg) drainQueue(ws, session, cfg);
614
- }
615
- }, 10000);
695
+ // Safety fallback: some app-server builds emit task completion without a turn/completed notification.
696
+ // Arm it only after the final assistant message, not after user/tool/reasoning items.
697
+ if (isFinalCodexAssistantItem(params)) {
698
+ armCodexTurnCompletionFallback(ws, session, session._lastConfig);
699
+ }
616
700
  return;
617
701
  }
618
702
 
619
703
  if (method === 'turn/completed' || method === 'turn.completed') {
620
- if (session.turnCompletedTimerId) {
621
- clearTimeout(session.turnCompletedTimerId);
622
- session.turnCompletedTimerId = null;
623
- }
624
- updateCodexSessionStats(session, params);
625
- clearTurnState(session);
626
- if (session.sawPartialAssistantText) {
627
- sendCodexAssistantEnd(ws, session);
628
- } else {
629
- sendSystem(ws, 'Codex 本次执行没有输出。');
630
- }
631
- sendTurnEnd(ws, session);
632
- const cfg = session._lastConfig;
633
- if (cfg) {
634
- drainQueue(ws, session, cfg);
635
- }
704
+ finishCodexTurn(ws, session, session._lastConfig, 'completed', params);
636
705
  return;
637
706
  }
638
707
 
@@ -647,7 +716,7 @@ function handleAppServerMessage(message, session) {
647
716
  if (method === 'tool/start' || method === 'tool_call') {
648
717
  const toolName = params.name || params.tool || '';
649
718
  if (toolName) {
650
- send(ws, 'tool_call', {
719
+ emitCodexToolCall(ws, session, {
651
720
  id: params.id || params.toolId || '',
652
721
  name: toolName,
653
722
  input: params.input || params.arguments || {},
@@ -657,7 +726,7 @@ function handleAppServerMessage(message, session) {
657
726
  }
658
727
 
659
728
  if (method === 'tool/completed' || method === 'tool_result') {
660
- send(ws, 'tool_result', {
729
+ emitCodexToolResult(ws, session, {
661
730
  id: params.id || params.toolId || '',
662
731
  output: params.output || params.result || '',
663
732
  });
@@ -678,7 +747,7 @@ function handleAppServerMessage(message, session) {
678
747
  : (params.item?.input || params.item?.arguments || {});
679
748
  const itemId = params.item?.id || params.itemId || '';
680
749
  if (toolName) {
681
- send(ws, 'tool_call', {
750
+ emitCodexToolCall(ws, session, {
682
751
  id: itemId,
683
752
  name: toolName,
684
753
  input: typeof toolInput === 'string' ? toolInput : JSON.stringify(toolInput).slice(0, 300),