Skip to content

Commit f297e6b

Browse files
committed
wip
1 parent 6aa767e commit f297e6b

10 files changed

Lines changed: 312 additions & 11 deletions

File tree

TODO.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,32 @@ complete=HANG)
485485
- MergePolicy: бан живёт в одной политике (hasBan per-policy), блокирует весь
486486
вход, unban именно этой политики восстанавливает.
487487

488+
## 23-й заход: исключение на дубль tool_call id + конфигурируемые таймеры (+4 теста)
489+
490+
### Изменения поведения (по требованию)
491+
1. Дубликат tool_call id от модели теперь КИДАЕТ ИСКЛЮЧЕНИЕ (раньше оба вызова
492+
молча выполнялись). `src/client/ClientAgent.ts`: проверка уникальности id
493+
после нормализации, до среза maxToolCalls; ошибка доставляется через
494+
errorSubject + флоу восстанавливается placeholder'ом (не виснет).
495+
`src/functions/target/session.ts`: session.complete подписан на errorSubject
496+
(раньше только execute/executeForce/runStateless*/chat/json) — теперь
497+
session.complete реджектится и на дубль id, и на ошибки провайдера.
498+
2. Таймеры вынесены в GLOBAL_CONFIG (тестируемость через setConfig в воркере):
499+
- CC_OPERATOR_SIGNAL_TIMEOUT (было константой 90_000 в ClientOperator);
500+
- CC_CHAT_INACTIVITY_CHECK (было 60с в Chat.ts);
501+
- CC_CHAT_INACTIVITY_TIMEOUT (было 15мин в Chat.ts).
502+
Дефолты не изменены. Файлы: config/params.ts, model/GlobalConfig.model.ts,
503+
client/ClientOperator.ts, classes/Chat.ts.
504+
505+
### Новые тесты (4), сьют вырос до 246/246 — test/spec/modelguard.test.mjs
506+
- дубль tool_call id → session.complete реджектится ("duplicate tool call id"),
507+
ни один из вызовов не выполнен, сессия остаётся рабочей;
508+
- дубль tool_call id → execute() тоже кидает;
509+
- операторский таймаут: оператор молчит → complete возвращает "" через
510+
~CC_OPERATOR_SIGNAL_TIMEOUT (300мс в тесте);
511+
- Chat: простаивающий чат авто-диспоузится по CC_CHAT_INACTIVITY_TIMEOUT
512+
(onDispose колбэк, hasSession=false).
513+
488514
## Найденные и исправленные баги (24 итого)
489515

490516
### 1. Дедлок waitForOutput при functools-kit v4 (причина 39 упавших тестов)

src/classes/Chat.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@ interface TChatInstance extends IChatInstance {
1717
*/
1818
type DisposeFn = () => void;
1919

20-
/** @constant {number} INACTIVITY_CHECK - Interval for checking inactivity in milliseconds (1 minute)*/
21-
const INACTIVITY_CHECK = 60 * 1_000;
22-
/** @constant {number} INACTIVITY_TIMEOUT - Timeout duration for inactivity in milliseconds (15 minutes)*/
23-
const INACTIVITY_TIMEOUT = 15 * 60 * 1000;
20+
2421

2522
/**
2623
* @constant {Function} BEGIN_CHAT_FN
@@ -179,7 +176,8 @@ class ChatInstance<Payload extends unknown = any> implements IChatInstance {
179176
clientId: this.clientId,
180177
swarmName: this.swarmName,
181178
});
182-
const isActive = now - this._lastActivity <= INACTIVITY_TIMEOUT;
179+
const isActive =
180+
now - this._lastActivity <= GLOBAL_CONFIG.CC_CHAT_INACTIVITY_TIMEOUT;
183181
this.callbacks.onCheckActivity &&
184182
this.callbacks.onCheckActivity(
185183
this.clientId,
@@ -269,7 +267,7 @@ export class ChatUtils implements IChatControl {
269267
await chat.dispose();
270268
}
271269
};
272-
setInterval(handleCleanup, INACTIVITY_CHECK);
270+
setInterval(handleCleanup, GLOBAL_CONFIG.CC_CHAT_INACTIVITY_CHECK);
273271
});
274272

275273
/**

src/client/ClientAgent.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { IBusEvent } from "../model/Event.model";
2121
import { MCPToolProperties } from "../interfaces/MCP.interface";
2222
import createToolRequest from "../utils/createToolRequest";
2323
import { resolveTools } from "../utils/resolveTools";
24+
import { errorSubject } from "../config/emitters";
2425
import swarm from "../lib";
2526

2627
const CANCEL_OUTPUT_SYMBOL = Symbol("cancel-output");
@@ -388,6 +389,29 @@ const EXECUTE_FN = async (
388389
self.params.clientId,
389390
self.params.agentName
390391
);
392+
{
393+
const toolCallIds = new Set(toolCalls.map(({ id }) => id));
394+
if (toolCallIds.size !== toolCalls.length) {
395+
// Duplicate ids break the call-to-output linkage in history and mean
396+
// the model output is malformed: surface it as an exception to the
397+
// caller (via errorSubject) and recover the flow with a placeholder.
398+
const error = new Error(
399+
`agent-swarm duplicate tool call id detected agentName=${
400+
self.params.agentName
401+
} clientId=${self.params.clientId} ids=${JSON.stringify(
402+
toolCalls.map(({ id }) => id)
403+
)}`
404+
);
405+
console.error(error.message);
406+
await errorSubject.next([self.params.clientId, error]);
407+
const result = await self._resurrectModel(
408+
mode,
409+
`Duplicate tool call id in model output`
410+
);
411+
await self._emitOutput(mode, result, outputEpoch);
412+
return;
413+
}
414+
}
391415
toolCalls = toolCalls.slice(-self.params.maxToolCalls);
392416

393417
{

src/client/ClientOperator.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ import { GLOBAL_CONFIG } from "../config/params";
1010
*/
1111
type DisposeFn = () => void;
1212

13-
/** Timeout duration for operator signal in milliseconds*/
14-
const OPERATOR_SIGNAL_TIMEOUT = 90_000;
15-
1613
/** Symbol representing operator timeout*/
1714
const OPERATOR_SIGNAL_SYMBOL = Symbol("operator-timeout");
1815

@@ -139,12 +136,14 @@ export class ClientOperator implements IAgent {
139136
}
140137
const result = await Promise.race([
141138
this._outgoingSubject.toPromise(),
142-
sleep(OPERATOR_SIGNAL_TIMEOUT).then(() => OPERATOR_SIGNAL_SYMBOL),
139+
sleep(GLOBAL_CONFIG.CC_OPERATOR_SIGNAL_TIMEOUT).then(
140+
() => OPERATOR_SIGNAL_SYMBOL
141+
),
143142
]);
144143
if (typeof result === "symbol") {
145144
GLOBAL_CONFIG.CC_LOGGER_ENABLE_DEBUG &&
146145
this.params.logger.debug(
147-
`ClientOperator agentName=${this.params.agentName} clientId=${this.params.clientId} waitForOutput timeout after ${OPERATOR_SIGNAL_TIMEOUT}ms`
146+
`ClientOperator agentName=${this.params.agentName} clientId=${this.params.clientId} waitForOutput timeout after ${GLOBAL_CONFIG.CC_OPERATOR_SIGNAL_TIMEOUT}ms`
148147
);
149148
await this._operatorSignal.dispose();
150149
return "";

src/config/params.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ const CC_DEFAULT_CONNECT_OPERATOR = (clientId: string, agentName: AgentName) =>
211211
*/
212212
const CC_ENABLE_OPERATOR_TIMEOUT = false;
213213

214+
const CC_OPERATOR_SIGNAL_TIMEOUT = 90_000;
215+
216+
const CC_CHAT_INACTIVITY_CHECK = 60 * 1_000;
217+
218+
const CC_CHAT_INACTIVITY_TIMEOUT = 15 * 60 * 1_000;
219+
214220
/**
215221
* Disable fetch of data from all storages. Quite usefull for unit tests
216222
*/
@@ -272,6 +278,9 @@ const GLOBAL_CONFIG: IGlobalConfig = {
272278
CC_THROW_WHEN_NAVIGATION_RECURSION,
273279
CC_DEFAULT_CONNECT_OPERATOR,
274280
CC_ENABLE_OPERATOR_TIMEOUT,
281+
CC_OPERATOR_SIGNAL_TIMEOUT,
282+
CC_CHAT_INACTIVITY_CHECK,
283+
CC_CHAT_INACTIVITY_TIMEOUT,
275284
CC_STORAGE_DISABLE_GET_DATA,
276285
CC_MAX_NESTED_EXECUTIONS,
277286
};

src/functions/target/session.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { disposeConnection } from "./disposeConnection";
1212
import beginContext from "../../utils/beginContext";
1313
import PayloadContextService from "../../lib/services/context/PayloadContextService";
1414
import { markOnline } from "../other/markOnline";
15+
import { errorSubject } from "../../config/emitters";
1516

1617
/**
1718
* Type definition for the complete function used in session objects.
@@ -61,13 +62,29 @@ const sessionInternal = (
6162
swarm.navigationValidationService.beginMonit(clientId, swarmName);
6263
try {
6364
swarm.busService.commitExecutionBegin(clientId, { swarmName });
65+
66+
let errorValue: Error = null;
67+
68+
const unError = errorSubject.subscribe(([errorClientId, error]) => {
69+
if (clientId === errorClientId) {
70+
errorValue = error;
71+
}
72+
});
73+
6474
const result = await swarm.sessionPublicService.execute(
6575
content,
6676
"user",
6777
METHOD_NAME,
6878
clientId,
6979
swarmName
7080
);
81+
82+
unError();
83+
84+
if (errorValue) {
85+
throw errorValue;
86+
}
87+
7188
isFinished = swarm.perfService.endExecution(
7289
executionId,
7390
clientId,

src/model/GlobalConfig.model.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,22 @@ export interface IGlobalConfig {
467467
*/
468468
CC_ENABLE_OPERATOR_TIMEOUT: boolean;
469469

470+
/**
471+
* Timeout in milliseconds for operator signal waiting in `ClientOperator.waitForOutput`.
472+
* Applied only when CC_ENABLE_OPERATOR_TIMEOUT is true; expired wait resolves with an empty output.
473+
*/
474+
CC_OPERATOR_SIGNAL_TIMEOUT: number;
475+
476+
/**
477+
* Interval in milliseconds between chat inactivity sweeps in `ChatUtils`.
478+
*/
479+
CC_CHAT_INACTIVITY_CHECK: number;
480+
481+
/**
482+
* Inactivity duration in milliseconds after which an idle chat instance is disposed by `ChatUtils`.
483+
*/
484+
CC_CHAT_INACTIVITY_TIMEOUT: number;
485+
470486
/**
471487
* Disable fetch of data from all storages. Quite usefull for unit tests
472488
*/

test/index.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import "./spec/doublesend.test.mjs";
3535
import "./spec/lifecycle.test.mjs";
3636
import "./spec/numindex.test.mjs";
3737
import "./spec/banhammer.test.mjs";
38+
import "./spec/modelguard.test.mjs";
3839

3940
run(import.meta.url, () => {
4041
console.log("All tests are finished");

0 commit comments

Comments
 (0)