docs(ai-chat): задокументировать наблюдаемую поверхность #487 (F2)

- CHANGELOG [Unreleased]: серверный supersede + три кода (SUPERSEDE_INVALID /
  SUPERSEDE_TARGET_MISMATCH / SUPERSEDE_TIMEOUT) в Added; смена поведения
  (легаси вторая вкладка теперь → 409 A_RUN_ALREADY_ACTIVE вместо второго
  параллельного стрима; каждый ход — ран в обоих режимах) в Changed.
- .env.example: три новых AI_CHAT_* (SUPERSEDE_TIMEOUT_MS / RECONCILE_INTERVAL_MS
  / INAPP_TOOL_CALL_CAP_MS, дефолты 10000/120000/120000, связь cap↔staleness).
- AGENTS.md:458: ран теперь универсален (оба режима), флаг autonomousRuns =
  только disconnect-семантика; плюс упоминание supersede.
- ai-chat.service.ts:1136: устаревший коммент про «legacy → socket signal» →
  оба режима на run-signal, guard runId защищает лишь no-handle fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 07:49:14 +03:00
parent 0b9de2c25e
commit 9ba97b6d2b
4 changed files with 53 additions and 9 deletions
@@ -1131,14 +1131,13 @@ export class AiChatService implements OnModuleInit, OnModuleDestroy {
);
} catch (err) {
// An explicit Stop reached the RUN's signal DURING setup: re-throw so the
// outer catch finalizes the run as aborted — never swallow a Stop. Gated on
// `runId`: the re-throw exists ONLY to finalize the run, which exists only
// in autonomous mode. On the legacy path (no runId) `effectiveSignal` is the
// SOCKET signal (it aborts on a client disconnect); re-throwing there would
// change prior behavior and make the controller write JSON to an already-
// closed socket (it only attaches res.raw.on('error') in autonomous mode).
// So legacy keeps its prior behavior — warn + proceed, and streamText then
// observes the aborted socket signal.
// outer catch finalizes the run as aborted — never swallow a Stop. #487: the
// turn is ALWAYS run-wrapped now (both modes), so `effectiveSignal` is the
// RUN signal and `runId` is set in BOTH — a Stop (from /ai-chat/stop or a
// legacy disconnect's requestStop) aborts it identically. The `runId` guard
// now only defends the theoretical no-handle fallback (`begin` returned
// nothing, leaving `effectiveSignal` as the bare socket signal): there we
// keep the old warn-and-proceed rather than re-throw.
if (runId && effectiveSignal.aborted) {
throw err;
}