chatccc 0.2.244 → 0.2.245

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.
@@ -55,7 +55,8 @@ import { ChatSession } from "../index.js";
55
55
  const streamTextMock = aiMocks.streamText;
56
56
  const generateTextMock = aiMocks.generateText;
57
57
 
58
- const originalRawStreamLogs = structuredClone(config.rawStreamLogs);
58
+ const originalRawStreamLogs = structuredClone(config.rawStreamLogs);
59
+ const originalStreaming = config.streaming;
59
60
  const PRIVACY_FILE = join(privacyState.dir, "privacy.json");
60
61
 
61
62
  function writePrivacy(content: string): void {
@@ -78,16 +79,18 @@ beforeEach(() => {
78
79
  } catch {}
79
80
  reloadPrivacyRules();
80
81
  streamTextMock.mockReset();
81
- generateTextMock.mockReset();
82
- config.rawStreamLogs = structuredClone(originalRawStreamLogs);
82
+ generateTextMock.mockReset();
83
+ config.rawStreamLogs = structuredClone(originalRawStreamLogs);
84
+ config.streaming = true;
83
85
  });
84
86
 
85
87
  afterEach(() => {
86
88
  try {
87
89
  rmSync(PRIVACY_FILE, { force: true });
88
90
  } catch {}
89
- reloadPrivacyRules();
90
- });
91
+ reloadPrivacyRules();
92
+ config.streaming = originalStreaming;
93
+ });
91
94
 
92
95
  afterAll(() => {
93
96
  try {
@@ -11,8 +11,8 @@
11
11
  * 非 TTY(管道/CI)或 --plain 回退为纯文本流式输出;--stream-json 机器接口不变。
12
12
  */
13
13
 
14
- import * as readline from "node:readline";
15
- import * as process from "node:process";
14
+ import * as readline from "node:readline";
15
+ import process from "node:process";
16
16
  import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
17
17
  import { homedir } from "node:os";
18
18
  import { join, resolve as resolvePath } from "node:path";
@@ -637,10 +637,11 @@ async function main(): Promise<void> {
637
637
 
638
638
  const args = parseArgs();
639
639
 
640
- if (args.streamJson) {
641
- const code = await runStreamJson(args);
642
- process.exit(code);
643
- }
640
+ if (args.streamJson) {
641
+ const code = await runStreamJson(args);
642
+ process.exitCode = code;
643
+ return;
644
+ }
644
645
 
645
646
  if (args.help) {
646
647
  const { appConfig } = await loadRuntime();
@@ -8,6 +8,8 @@ export interface DeepCccConfig {
8
8
  model: string;
9
9
  /** Reasoning effort(none/minimal/low/medium/high/xhigh/max),留空不传 reasoning_effort */
10
10
  effort: string;
11
+ /** 主对话是否使用流式请求;默认开启 */
12
+ streaming: boolean;
11
13
  rawStreamLogs: {
12
14
  enabled: boolean;
13
15
  maxBytesPerTurn: number;
@@ -25,6 +27,7 @@ const DEFAULT_CONFIG: DeepCccConfig = {
25
27
  baseURL: "https://api.deepseek.com/v1",
26
28
  model: "deepseek-v4-pro",
27
29
  effort: "",
30
+ streaming: true,
28
31
  rawStreamLogs: {
29
32
  enabled: false,
30
33
  maxBytesPerTurn: 1024 * 1024,
@@ -68,6 +71,7 @@ function loadConfig(): DeepCccConfig {
68
71
  baseURL: env("DEEPCCC_BASE_URL") ?? env("DEEPSEEK_BASE_URL") ?? file.baseURL ?? DEFAULT_CONFIG.baseURL,
69
72
  model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
70
73
  effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
74
+ streaming: boolEnv("DEEPCCC_STREAMING") ?? file.streaming ?? DEFAULT_CONFIG.streaming,
71
75
  rawStreamLogs: {
72
76
  enabled: boolEnv("DEEPCCC_RAW_STREAM_LOGS") ?? rawLogs.enabled ?? DEFAULT_CONFIG.rawStreamLogs.enabled,
73
77
  maxBytesPerTurn: numberEnv("DEEPCCC_RAW_STREAM_MAX_BYTES") ?? rawLogs.maxBytesPerTurn ?? DEFAULT_CONFIG.rawStreamLogs.maxBytesPerTurn,
@@ -280,10 +280,11 @@ export class ChatSession {
280
280
  const modelId = overrides.model ?? appConfig.model;
281
281
  this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
282
282
 
283
- const provider = createOpenAICompatible({
284
- name: "deepccc",
285
- baseURL,
286
- apiKey,
283
+ const provider = createOpenAICompatible({
284
+ name: "deepccc",
285
+ baseURL,
286
+ apiKey,
287
+ includeUsage: true,
287
288
  });
288
289
  this.model = provider(modelId);
289
290
  this.cwd = options.cwd ?? process.cwd();
@@ -383,20 +384,27 @@ export class ChatSession {
383
384
  const skills = await scanSkillsDirs(this.skillDirs);
384
385
  const system = this.buildSystemPrompt(skills);
385
386
  this.systemPrompt = system;
386
- const result = streamText({
387
- model: this.model,
388
- system,
389
- messages: this.context.buildModelMessages() as any,
387
+ const generationOptions = {
388
+ model: this.model,
389
+ system,
390
+ messages: this.context.buildModelMessages() as any,
390
391
  tools: createBuiltinFileTools(this.cwd, { permissionGate: this.permissionGate }),
391
392
  stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
392
393
  abortSignal: signal,
393
- // DeepSeek OpenAI 兼容接口:providerOptions.deepseek.reasoningEffort
394
- // 由 @ai-sdk/openai-compatible 自动映射为请求体 reasoning_effort 字段
395
- ...(this.effort ? { providerOptions: { deepseek: { reasoningEffort: this.effort } } } : {}),
396
- });
397
-
398
- const stream = result.fullStream ?? textStreamToFullStream(result.textStream);
399
- for await (const part of stream as AsyncIterable<TextStreamPart<any>>) {
394
+ // DeepSeek OpenAI 兼容接口:providerOptions.deepseek.reasoningEffort
395
+ // 由 @ai-sdk/openai-compatible 自动映射为请求体 reasoning_effort 字段
396
+ ...(this.effort ? { providerOptions: { deepseek: { reasoningEffort: this.effort } } } : {}),
397
+ };
398
+ let stream: AsyncIterable<TextStreamPart<any>>;
399
+ if (appConfig.streaming) {
400
+ const result = streamText(generationOptions);
401
+ stream = result.fullStream ?? textStreamToFullStream(result.textStream);
402
+ } else {
403
+ const result = await generateText(generationOptions);
404
+ stream = generateResultToFullStream(result);
405
+ }
406
+
407
+ for await (const part of stream as AsyncIterable<TextStreamPart<any>>) {
400
408
  rawLog?.writeLine(safeRawStreamJson(part));
401
409
  if (part.type === "text-delta") {
402
410
  fullText += part.text;
@@ -554,11 +562,40 @@ export class ChatSession {
554
562
  }
555
563
  }
556
564
 
557
- async function* textStreamToFullStream(stream: AsyncIterable<string>): AsyncIterable<{ type: "text-delta"; text: string }> {
558
- for await (const text of stream) {
559
- yield { type: "text-delta", text };
560
- }
561
- }
565
+ async function* textStreamToFullStream(stream: AsyncIterable<string>): AsyncIterable<{ type: "text-delta"; text: string }> {
566
+ for await (const text of stream) {
567
+ yield { type: "text-delta", text };
568
+ }
569
+ }
570
+
571
+ async function* generateResultToFullStream(result: any): AsyncIterable<TextStreamPart<any>> {
572
+ let emittedText = false;
573
+ for (const step of result.steps ?? []) {
574
+ for (const call of step.toolCalls ?? []) {
575
+ yield {
576
+ type: "tool-call",
577
+ toolCallId: call.toolCallId,
578
+ toolName: call.toolName,
579
+ input: call.input,
580
+ } as TextStreamPart<any>;
581
+ }
582
+ for (const toolResult of step.toolResults ?? []) {
583
+ yield {
584
+ type: "tool-result",
585
+ toolCallId: toolResult.toolCallId,
586
+ toolName: toolResult.toolName,
587
+ output: toolResult.output,
588
+ } as TextStreamPart<any>;
589
+ }
590
+ if (step.text) {
591
+ emittedText = true;
592
+ yield { type: "text-delta", text: step.text } as TextStreamPart<any>;
593
+ }
594
+ }
595
+ if (!emittedText && result.text) {
596
+ yield { type: "text-delta", text: result.text } as TextStreamPart<any>;
597
+ }
598
+ }
562
599
 
563
600
  function safeJson(value: unknown): string {
564
601
  try {
package/package.json CHANGED
@@ -1,73 +1,73 @@
1
- {
2
- "name": "chatccc",
3
- "version": "0.2.244",
4
- "description": "Feishu bot bridge for Claude Code",
5
- "license": "Apache-2.0",
6
- "type": "module",
7
- "main": "./src/index.ts",
8
- "bin": {
9
- "chatccc": "bin/chatccc.mjs",
10
- "cccagent": "bin/cccagent.mjs"
11
- },
12
- "files": [
13
- "src/",
14
- "deepccc-agent/",
15
- "bin/",
16
- "scripts/postinstall-sharp-check.mjs",
17
- "demo/ilink_echo_probe.ts",
18
- "agent-prompts/",
19
- "im-skills/",
20
- ".agents/skills/create-chatccc-feishu-app/",
21
- ".claude/skills/create-chatccc-feishu-app/",
22
- ".cursor/skills/create-chatccc-feishu-app/",
23
- "images/img_readme_*.jpg",
24
- "images/img_readme_*.png",
25
- "images/avatars/status_*.png",
26
- "images/avatars/badges/",
27
- "images/avatars/combinations/",
28
- "package.json",
29
- "README.md",
30
- "config.sample.json"
31
- ],
32
- "scripts": {
33
- "dev": "tsx src/index.ts",
34
- "chatccc": "tsx src/index.ts",
35
- "start": "tsx src/index.ts",
36
- "demo:bot-test": "tsx demo/bot_test.ts",
37
- "demo:bot-test:local": "tsx demo/bot_test.ts --local",
38
- "demo:create-group": "tsx src/index.ts",
39
- "demo:create-group:local": "tsx src/index.ts --local",
40
- "demo:permission-check": "tsx demo/permission_check.ts",
41
- "demo:claude-hi": "tsx demo/claude_say_hi.ts",
42
- "demo:codex-hi": "tsx demo/codex_say_hi.ts",
43
- "demo:codex-app-server-approval": "tsx demo/codex-app-server-approval/approval_demo.ts",
44
- "demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
45
- "claude-proxy": "tsx src/litellm-proxy.ts",
46
- "test": "vitest run",
47
- "test:deepccc": "vitest run --root deepccc-agent",
48
- "test:watch": "vitest",
49
- "postinstall": "node scripts/postinstall-sharp-check.mjs"
50
- },
51
- "dependencies": {
52
- "@ai-sdk/openai-compatible": "^2.0.47",
53
- "@larksuiteoapi/node-sdk": "^1.59.0",
54
- "@openilink/openilink-sdk-node": "^0.6.0",
55
- "@vscode/ripgrep": "^1.18.0",
56
- "ai": "^6.0.184",
57
- "nodemailer": "^8.0.7",
58
- "qrcode-terminal": "^0.12.0",
59
- "sharp": "^0.34.5",
60
- "tsx": "^4.0.0",
61
- "ws": "^8.18.0"
62
- },
63
- "devDependencies": {
64
- "@types/node": "^20.0.0",
65
- "@types/qrcode-terminal": "^0.12.2",
66
- "@types/ws": "^8.18.1",
67
- "typescript": "^5.0.0",
68
- "vitest": "^3.2.4"
69
- },
70
- "engines": {
71
- "node": ">=20"
72
- }
73
- }
1
+ {
2
+ "name": "chatccc",
3
+ "version": "0.2.245",
4
+ "description": "Feishu bot bridge for Claude Code",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./src/index.ts",
8
+ "bin": {
9
+ "chatccc": "bin/chatccc.mjs",
10
+ "cccagent": "bin/cccagent.mjs"
11
+ },
12
+ "files": [
13
+ "src/",
14
+ "deepccc-agent/",
15
+ "bin/",
16
+ "scripts/postinstall-sharp-check.mjs",
17
+ "demo/ilink_echo_probe.ts",
18
+ "agent-prompts/",
19
+ "im-skills/",
20
+ ".agents/skills/create-chatccc-feishu-app/",
21
+ ".claude/skills/create-chatccc-feishu-app/",
22
+ ".cursor/skills/create-chatccc-feishu-app/",
23
+ "images/img_readme_*.jpg",
24
+ "images/img_readme_*.png",
25
+ "images/avatars/status_*.png",
26
+ "images/avatars/badges/",
27
+ "images/avatars/combinations/",
28
+ "package.json",
29
+ "README.md",
30
+ "config.sample.json"
31
+ ],
32
+ "scripts": {
33
+ "dev": "tsx src/index.ts",
34
+ "chatccc": "tsx src/index.ts",
35
+ "start": "tsx src/index.ts",
36
+ "demo:bot-test": "tsx demo/bot_test.ts",
37
+ "demo:bot-test:local": "tsx demo/bot_test.ts --local",
38
+ "demo:create-group": "tsx src/index.ts",
39
+ "demo:create-group:local": "tsx src/index.ts --local",
40
+ "demo:permission-check": "tsx demo/permission_check.ts",
41
+ "demo:claude-hi": "tsx demo/claude_say_hi.ts",
42
+ "demo:codex-hi": "tsx demo/codex_say_hi.ts",
43
+ "demo:codex-app-server-approval": "tsx demo/codex-app-server-approval/approval_demo.ts",
44
+ "demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
45
+ "claude-proxy": "tsx src/litellm-proxy.ts",
46
+ "test": "vitest run",
47
+ "test:deepccc": "vitest run --root deepccc-agent",
48
+ "test:watch": "vitest",
49
+ "postinstall": "node scripts/postinstall-sharp-check.mjs"
50
+ },
51
+ "dependencies": {
52
+ "@ai-sdk/openai-compatible": "^2.0.47",
53
+ "@larksuiteoapi/node-sdk": "^1.59.0",
54
+ "@openilink/openilink-sdk-node": "^0.6.0",
55
+ "@vscode/ripgrep": "^1.18.0",
56
+ "ai": "^6.0.184",
57
+ "nodemailer": "^8.0.7",
58
+ "qrcode-terminal": "^0.12.0",
59
+ "sharp": "^0.34.5",
60
+ "tsx": "^4.0.0",
61
+ "ws": "^8.18.0"
62
+ },
63
+ "devDependencies": {
64
+ "@types/node": "^20.0.0",
65
+ "@types/qrcode-terminal": "^0.12.2",
66
+ "@types/ws": "^8.18.1",
67
+ "typescript": "^5.0.0",
68
+ "vitest": "^3.2.4"
69
+ },
70
+ "engines": {
71
+ "node": ">=20"
72
+ }
73
+ }
@@ -34,6 +34,21 @@ afterEach(() => {
34
34
  });
35
35
 
36
36
  describe("createCccAdapter", () => {
37
+ it("disables response-stall detection when DeepCCC streaming is disabled", async () => {
38
+ const { config: deepCccConfig } = await import("../../deepccc-agent/src/config.ts");
39
+ const previousStreaming = deepCccConfig.streaming;
40
+ deepCccConfig.streaming = false;
41
+
42
+ try {
43
+ const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
44
+ const adapter = createCccAdapter({ apiKey: "sk-test" });
45
+
46
+ expect(adapter.responseStallDetectionEnabled).toBe(false);
47
+ } finally {
48
+ deepCccConfig.streaming = previousStreaming;
49
+ }
50
+ });
51
+
37
52
  it("creates a persisted ccc session and exposes model/cwd metadata", async () => {
38
53
  const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
39
54
  const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-meta-"));
@@ -928,7 +928,7 @@ describe("runAgentSession response stall watchdog", () => {
928
928
  if (tempDir) await rm(tempDir, { recursive: true, force: true });
929
929
  });
930
930
 
931
- it("auto-ends every Agent after three minutes of unchanged reply characters, including zero", async () => {
931
+ it("auto-ends adapters with stall detection after three minutes of unchanged reply characters, including zero", async () => {
932
932
  vi.setSystemTime(0);
933
933
  _setResponseStallTimeoutForTest(180_000);
934
934
  _setResponseStallCheckIntervalForTest(1_000);
@@ -994,13 +994,76 @@ describe("runAgentSession response stall watchdog", () => {
994
994
  expect(closeSession).toHaveBeenCalledTimes(1);
995
995
  expect(killProcessTreeMock).toHaveBeenCalledWith(4242);
996
996
  expect(activePrompts.has("sid-response-stall")).toBe(false);
997
- expect(mockStreamStates.get("sid-response-stall")).toMatchObject({
998
- status: "auto_ended",
999
- finalReply: "",
1000
- autoEndedAt: expect.any(Number),
1001
- });
1002
- });
1003
-
997
+ expect(mockStreamStates.get("sid-response-stall")).toMatchObject({
998
+ status: "auto_ended",
999
+ finalReply: "",
1000
+ autoEndedAt: expect.any(Number),
1001
+ });
1002
+ });
1003
+
1004
+ it("does not detect response stalls when the adapter disables streamed-output monitoring", async () => {
1005
+ vi.setSystemTime(0);
1006
+ _setResponseStallTimeoutForTest(180_000);
1007
+ _setResponseStallCheckIntervalForTest(1_000);
1008
+ _setProcessAliveForTest(() => true);
1009
+
1010
+ const platform = mockPlatform("feishu");
1011
+ setSessionPlatform(platform);
1012
+ bindChatToSession("sid-non-streaming", "chat-non-streaming");
1013
+ recordLastActiveChat("sid-non-streaming", "chat-non-streaming");
1014
+
1015
+ let finishPrompt: (() => void) | undefined;
1016
+ const closeSession = vi.fn();
1017
+ const adapter: ToolAdapter = {
1018
+ displayName: "Non-streaming DeepCCC",
1019
+ sessionDescPrefix: "CCC Session:",
1020
+ responseStallDetectionEnabled: false,
1021
+ createSession: async () => ({ sessionId: "sid-non-streaming" }),
1022
+ getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "F:\\repo" }),
1023
+ closeSession: async () => {},
1024
+ prompt: async function* (
1025
+ _sid: string,
1026
+ _text: string,
1027
+ _cwd: string,
1028
+ _signal?: AbortSignal,
1029
+ options?: ToolPromptOptions,
1030
+ ) {
1031
+ options?.onSessionCreated?.(closeSession);
1032
+ options?.onProcessStart?.({ pid: 4545 });
1033
+ yield { type: "assistant", blocks: [{ type: "agent_status", status: "responding" }] };
1034
+ await new Promise<void>((resolve) => {
1035
+ finishPrompt = resolve;
1036
+ });
1037
+ },
1038
+ };
1039
+ _setAdapterForToolForTest("ccc", adapter);
1040
+
1041
+ const runPromise = runAgentSession(
1042
+ "sid-non-streaming",
1043
+ "prompt",
1044
+ platform,
1045
+ "chat-non-streaming",
1046
+ Date.now(),
1047
+ "ccc",
1048
+ );
1049
+
1050
+ await vi.waitFor(() => {
1051
+ expect(activePrompts.get("sid-non-streaming")?.processPid).toBe(4545);
1052
+ expect(finishPrompt).toBeTypeOf("function");
1053
+ });
1054
+ expect(activePrompts.get("sid-non-streaming")?.responseStallMonitor).toBeUndefined();
1055
+
1056
+ await vi.advanceTimersByTimeAsync(181_001);
1057
+
1058
+ expect(activePrompts.has("sid-non-streaming")).toBe(true);
1059
+ expect(closeSession).not.toHaveBeenCalled();
1060
+ expect(killProcessTreeMock).not.toHaveBeenCalledWith(4545);
1061
+ expect(mockStreamStates.get("sid-non-streaming")?.status).toBe("running");
1062
+
1063
+ finishPrompt?.();
1064
+ await runPromise;
1065
+ });
1066
+
1004
1067
  it("does not apply the reply-stall timeout while an Agent is compacting context", async () => {
1005
1068
  vi.setSystemTime(0);
1006
1069
  _setResponseStallTimeoutForTest(180_000);
@@ -151,12 +151,18 @@ export interface ToolPromptOptions {
151
151
  // ToolAdapter — 统一的 AI 工具适配器接口
152
152
  // ---------------------------------------------------------------------------
153
153
 
154
- export interface ToolAdapter {
154
+ export interface ToolAdapter {
155
155
  /** 日志/展示用名称,如 "Claude" */
156
156
  readonly displayName: string;
157
157
 
158
158
  /** 群描述中会话 ID 的前缀,如 "Claude Session:" */
159
- readonly sessionDescPrefix: string;
159
+ readonly sessionDescPrefix: string;
160
+
161
+ /**
162
+ * Whether ChatCCC may infer a stalled response from unchanged streamed output.
163
+ * Defaults to true. Adapters that only emit output after a request completes must disable it.
164
+ */
165
+ readonly responseStallDetectionEnabled?: boolean;
160
166
 
161
167
  /**
162
168
  * 创建新会话,返回会话 ID。
@@ -1,4 +1,5 @@
1
1
  import { ChatSession, type ChatSessionConfig, type ChatSessionOptions } from "../../deepccc-agent/src/index.ts";
2
+ import { config as deepCccConfig } from "../../deepccc-agent/src/config.ts";
2
3
  import {
3
4
  getBuiltinContextSession,
4
5
  newBuiltinSessionId,
@@ -56,6 +57,9 @@ export function createCccAdapter(options: CccAdapterOptions = {}): ToolAdapter {
56
57
  return {
57
58
  displayName: "CCC Agent",
58
59
  sessionDescPrefix: CCC_SESSION_PREFIX,
60
+ // Non-streaming DeepCCC emits its reply only after the provider request finishes,
61
+ // so unchanged output cannot distinguish a slow request from a stalled response.
62
+ responseStallDetectionEnabled: deepCccConfig.streaming,
59
63
 
60
64
  async createSession(cwd: string): Promise<CreateSessionResult> {
61
65
  const sessionId = newBuiltinSessionId();
package/src/session.ts CHANGED
@@ -1521,7 +1521,8 @@ export async function runAgentSession(
1521
1521
  let streamErrored = false;
1522
1522
  let streamTerminalError: TerminalErrorInfo | undefined;
1523
1523
  let runOutcome: SessionRunOutcome = "error";
1524
-
1524
+ const responseStallDetectionEnabled = adapter.responseStallDetectionEnabled !== false;
1525
+
1525
1526
  const runningPrompt = activePrompts.get(sessionId);
1526
1527
  if (runningPrompt) {
1527
1528
  startPromptAvatarRefresh(sessionId, tool, platform, runningPrompt);
@@ -1607,14 +1608,16 @@ export async function runAgentSession(
1607
1608
  );
1608
1609
  };
1609
1610
 
1610
- const responseStallMonitor = setInterval(() => {
1611
- void checkResponseStall().catch((err) => {
1612
- console.warn(`[${ts()}] [RESPONSE-STALL] check failed for ${sessionId}: ${(err as Error).message}`);
1613
- });
1614
- }, responseStallCheckIntervalMs);
1615
- responseStallMonitor.unref?.();
1616
- runningPrompt.responseStallMonitor = responseStallMonitor;
1617
- }
1611
+ if (responseStallDetectionEnabled) {
1612
+ const responseStallMonitor = setInterval(() => {
1613
+ void checkResponseStall().catch((err) => {
1614
+ console.warn(`[${ts()}] [RESPONSE-STALL] check failed for ${sessionId}: ${(err as Error).message}`);
1615
+ });
1616
+ }, responseStallCheckIntervalMs);
1617
+ responseStallMonitor.unref?.();
1618
+ runningPrompt.responseStallMonitor = responseStallMonitor;
1619
+ }
1620
+ }
1618
1621
 
1619
1622
  try {
1620
1623
  for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
@@ -1664,7 +1667,7 @@ export async function runAgentSession(
1664
1667
  }
1665
1668
 
1666
1669
  const prompt = activePrompts.get(sessionId);
1667
- if (prompt && !prompt.autoEnded) {
1670
+ if (prompt && !prompt.autoEnded) {
1668
1671
  const totalChars = state.accumulatedContent.length + pickFinalReply(state).length;
1669
1672
  prompt.responseProgress = observeResponseProgress(
1670
1673
  // starting → responding 本身是一次有效进展,即便首个文本块仍为空也应