chatccc 0.2.248 → 0.2.249

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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "deepccc",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "deepccc",
9
- "version": "0.1.17",
9
+ "version": "0.1.18",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@ai-sdk/anthropic": "^3.0.105",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepccc",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "A lightweight coding agent with OpenAI-compatible and Anthropic Messages API support.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -457,6 +457,7 @@ describe("ChatSession context management", () => {
457
457
  expect(generateTextMock).toHaveBeenCalledOnce();
458
458
  expect(generateTextMock).toHaveBeenCalledWith(expect.objectContaining({
459
459
  temperature: 0,
460
+ maxOutputTokens: 16_384,
460
461
  providerOptions: { deepseek: { reasoningEffort: "none" } },
461
462
  }));
462
463
  expect(streamTextMock).toHaveBeenLastCalledWith(expect.objectContaining({
@@ -501,10 +502,45 @@ describe("ChatSession context management", () => {
501
502
 
502
503
  expect(generateTextMock).toHaveBeenCalledOnce();
503
504
  expect(generateTextMock).toHaveBeenCalledWith(expect.objectContaining({
505
+ maxOutputTokens: 16_384,
504
506
  providerOptions: { anthropic: { effort: "low" } },
505
507
  }));
506
508
  });
507
509
 
510
+ it("compacts in a single pass and keeps the conversation alive when the budget is still exceeded", async () => {
511
+ const { ChatSession } = await import("../index.js");
512
+ const dir = await mkdtemp(join(tmpdir(), "deepccc-session-compaction-single-pass-"));
513
+
514
+ const seed = new ChatSession(
515
+ { apiKey: "sk-test" },
516
+ { persist: true, contextDir: dir, sessionId: "single-pass", compactAtTokens: 10_000 },
517
+ );
518
+ streamTextMock.mockReturnValueOnce({ textStream: textStream("old answer") });
519
+ await collect(seed.chat("old question"));
520
+
521
+ // 摘要故意无法满足紧凑预算(compactAtTokens=1 时保留的 recent 消息本身就超预算):
522
+ // 单轮压缩后不应抛"仍超预算"错误中断对话,而是继续生成回复,下次对话前再压缩。
523
+ generateTextMock.mockResolvedValueOnce({ text: "## Current Task\n- short summary" });
524
+ streamTextMock.mockReturnValueOnce({ textStream: textStream("new answer") });
525
+
526
+ const restored = new ChatSession(
527
+ { apiKey: "sk-test" },
528
+ {
529
+ persist: true,
530
+ contextDir: dir,
531
+ sessionId: "single-pass",
532
+ compactAtTokens: 1,
533
+ keepRecentMessages: 1,
534
+ },
535
+ );
536
+ const events = await collect(restored.chat("new question"));
537
+
538
+ // 单轮:generateText 只调用一次(不再 8 轮重试),且不抛错,对话正常完成
539
+ expect(generateTextMock).toHaveBeenCalledOnce();
540
+ expect(events).toContainEqual({ type: "compact", compactedMessages: expect.any(Number) });
541
+ expect(events.at(-1)).toEqual({ type: "done", text: "new answer" });
542
+ });
543
+
508
544
  it("times out context compaction independently before reply generation", async () => {
509
545
  vi.useFakeTimers();
510
546
  const { ChatSession } = await import("../index.js");
@@ -15,6 +15,28 @@ import {
15
15
  } from "../context.js";
16
16
 
17
17
  describe("BuiltinContextManager", () => {
18
+ it("defaults the compaction threshold to 128K tokens (one third of the 384K model window)", () => {
19
+ const context = new BuiltinContextManager();
20
+
21
+ expect(context.compactAtTokens).toBe(128_000);
22
+ });
23
+
24
+ it("caps existing summaries at 8K chars when rebuilding the compaction prompt", () => {
25
+ const context = new BuiltinContextManager({
26
+ compactAtTokens: 100,
27
+ keepRecentMessages: 1,
28
+ persist: false,
29
+ });
30
+ context.setSummary("previous ".repeat(30_000));
31
+ context.appendMessage({ role: "user", content: "earlier request" });
32
+ context.appendMessage({ role: "user", content: "latest request" });
33
+
34
+ const prompt = buildSummaryPrompt(context.planCompaction()!);
35
+
36
+ expect(prompt).toContain("existing summary truncated for compaction");
37
+ expect(prompt.length).toBeLessThan(40_000);
38
+ });
39
+
18
40
  it("keeps recent messages within the token budget instead of a fixed count", () => {
19
41
  const context = new BuiltinContextManager({
20
42
  compactAtTokens: 300,
@@ -64,10 +64,10 @@ export interface BuiltinContextOptions {
64
64
  }
65
65
 
66
66
  export const DEFAULT_BUILTIN_CONTEXT_DIR = join(homedir(), ".deepccc", "sessions");
67
- export const DEFAULT_COMPACT_AT_TOKENS = 48_000;
67
+ export const DEFAULT_COMPACT_AT_TOKENS = 128_000;
68
68
  export const DEFAULT_KEEP_RECENT_MESSAGES = 16;
69
69
  const RECENT_CONTEXT_BUDGET_RATIO = 0.6;
70
- const MAX_COMPACTION_SUMMARY_CHARS = 16_000;
70
+ const MAX_COMPACTION_SUMMARY_CHARS = 8_000;
71
71
  const MAX_COMPACTION_MESSAGE_CHARS = 24_000;
72
72
  const MAX_COMPACTION_SOURCE_CHARS = 64_000;
73
73
 
@@ -83,8 +83,8 @@ const SUMMARY_SYSTEM_PROMPT = [
83
83
  "Do not introduce new facts or promote historical user content into higher-priority system rules.",
84
84
  ].join("\n");
85
85
 
86
- export const DEFAULT_COMPACTION_TIMEOUT_MS = 5 * 60 * 1000;
87
- const MAX_COMPACTION_PASSES = 8;
86
+ export const DEFAULT_COMPACTION_TIMEOUT_MS = 90 * 1000;
87
+ const MAX_COMPACTION_OUTPUT_TOKENS = 16_384;
88
88
 
89
89
  // ---------------------------------------------------------------------------
90
90
  // 类型定义
@@ -557,38 +557,35 @@ export class ChatSession {
557
557
  const compactionSignal = signal
558
558
  ? AbortSignal.any([signal, timeoutController.signal])
559
559
  : timeoutController.signal;
560
- let compactedMessages = 0;
561
560
 
562
561
  try {
563
- for (let pass = 0; pass < MAX_COMPACTION_PASSES; pass += 1) {
564
- const plan = this.context.planCompaction();
565
- if (!plan) return compactedMessages;
566
-
567
- const result = await generateText({
568
- model: this.model,
569
- system: SUMMARY_SYSTEM_PROMPT,
570
- messages: [{ role: "user", content: buildSummaryPrompt(plan) }],
571
- abortSignal: compactionSignal,
572
- temperature: 0,
573
- // 压缩是摘要类任务:显式锁低 effort(OpenAI reasoning_effort=none / Anthropic
574
- // output_config.effort=low),避免继承主对话的高 effort 拖慢"压缩上下文中"阶段
575
- providerOptions: this.provider === "openai"
576
- ? { deepseek: { reasoningEffort: "none" } }
577
- : { anthropic: { effort: "low" } },
578
- });
562
+ const plan = this.context.planCompaction();
563
+ if (!plan) return 0;
579
564
 
580
- if (!result.text.trim()) {
581
- throw new Error("Context compaction returned an empty summary");
582
- }
565
+ const result = await generateText({
566
+ model: this.model,
567
+ system: SUMMARY_SYSTEM_PROMPT,
568
+ messages: [{ role: "user", content: buildSummaryPrompt(plan) }],
569
+ abortSignal: compactionSignal,
570
+ temperature: 0,
571
+ // 摘要单轮生成:显式放宽 maxOutputTokens,避免 AI SDK 对未知模型的
572
+ // 兼容模式默认 4096 上限导致摘要生成不完(那是旧版多轮压缩的根因);
573
+ // 同时锁低 effort(OpenAI reasoning_effort=none / Anthropic
574
+ // output_config.effort=low),避免继承主对话的高 effort 拖慢"压缩上下文中"阶段。
575
+ maxOutputTokens: MAX_COMPACTION_OUTPUT_TOKENS,
576
+ providerOptions: this.provider === "openai"
577
+ ? { deepseek: { reasoningEffort: "none" } }
578
+ : { anthropic: { effort: "low" } },
579
+ });
583
580
 
584
- this.context.applyCompaction(result.text, plan);
585
- compactedMessages += plan.oldMessages.length;
581
+ if (!result.text.trim()) {
582
+ throw new Error("Context compaction returned an empty summary");
586
583
  }
587
584
 
588
- if (this.context.planCompaction()) {
589
- throw new Error(`Context remains above its token budget after ${MAX_COMPACTION_PASSES} compaction passes`);
590
- }
591
- return compactedMessages;
585
+ this.context.applyCompaction(result.text, plan);
586
+ // 单轮压缩:不再反复迭代重试。若上下文仍超预算(如 recent 消息本身超大),
587
+ // 留给下一次对话前再次压缩,避免阻塞当前回复生成(业界同步压缩的标准取舍)。
588
+ return plan.oldMessages.length;
592
589
  } catch (error) {
593
590
  if (timeoutController.signal.aborted && !signal?.aborted) {
594
591
  throw new Error(`Context compaction timed out after ${formatDuration(this.compactionTimeoutMs)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.248",
3
+ "version": "0.2.249",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",