ata-coder 2.4.6__tar.gz → 2.4.8__tar.gz

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 (142) hide show
  1. {ata_coder-2.4.6/ata_coder.egg-info → ata_coder-2.4.8}/PKG-INFO +8 -4
  2. {ata_coder-2.4.6 → ata_coder-2.4.8}/README.md +7 -3
  3. {ata_coder-2.4.6 → ata_coder-2.4.8}/agent.py +43 -30
  4. ata_coder-2.4.8/agent_compact.py +159 -0
  5. {ata_coder-2.4.6 → ata_coder-2.4.8}/agent_tools.py +2 -1
  6. {ata_coder-2.4.6 → ata_coder-2.4.8/ata_coder.egg-info}/PKG-INFO +8 -4
  7. {ata_coder-2.4.6 → ata_coder-2.4.8}/ata_coder.egg-info/SOURCES.txt +2 -0
  8. {ata_coder-2.4.6 → ata_coder-2.4.8}/change_tracker.py +1 -1
  9. {ata_coder-2.4.6 → ata_coder-2.4.8}/config.py +14 -3
  10. ata_coder-2.4.8/context_manager.py +230 -0
  11. {ata_coder-2.4.6 → ata_coder-2.4.8}/main.py +3 -3
  12. {ata_coder-2.4.6 → ata_coder-2.4.8}/memory.py +76 -20
  13. {ata_coder-2.4.6 → ata_coder-2.4.8}/privilege.py +10 -6
  14. {ata_coder-2.4.6 → ata_coder-2.4.8}/pyproject.toml +1 -1
  15. {ata_coder-2.4.6 → ata_coder-2.4.8}/safety_guard.py +1 -1
  16. {ata_coder-2.4.6 → ata_coder-2.4.8}/server.py +6 -2
  17. {ata_coder-2.4.6 → ata_coder-2.4.8}/server_session.py +1 -1
  18. {ata_coder-2.4.6 → ata_coder-2.4.8}/setup_wizard.py +1 -1
  19. {ata_coder-2.4.6 → ata_coder-2.4.8}/system_prompt_builder.py +94 -34
  20. {ata_coder-2.4.6 → ata_coder-2.4.8}/token_counter.py +79 -43
  21. ata_coder-2.4.6/agent_compact.py +0 -195
  22. {ata_coder-2.4.6 → ata_coder-2.4.8}/LICENSE +0 -0
  23. {ata_coder-2.4.6 → ata_coder-2.4.8}/MANIFEST.in +0 -0
  24. {ata_coder-2.4.6 → ata_coder-2.4.8}/__init__.py +0 -0
  25. {ata_coder-2.4.6 → ata_coder-2.4.8}/agent_controller.py +0 -0
  26. {ata_coder-2.4.6 → ata_coder-2.4.8}/agent_extension.py +0 -0
  27. {ata_coder-2.4.6 → ata_coder-2.4.8}/agent_routing.py +0 -0
  28. {ata_coder-2.4.6 → ata_coder-2.4.8}/agent_subsystems.py +0 -0
  29. {ata_coder-2.4.6 → ata_coder-2.4.8}/agent_undo.py +0 -0
  30. {ata_coder-2.4.6 → ata_coder-2.4.8}/anthropic_client.py +0 -0
  31. {ata_coder-2.4.6 → ata_coder-2.4.8}/ata_coder.egg-info/dependency_links.txt +0 -0
  32. {ata_coder-2.4.6 → ata_coder-2.4.8}/ata_coder.egg-info/entry_points.txt +0 -0
  33. {ata_coder-2.4.6 → ata_coder-2.4.8}/ata_coder.egg-info/requires.txt +0 -0
  34. {ata_coder-2.4.6 → ata_coder-2.4.8}/ata_coder.egg-info/top_level.txt +0 -0
  35. {ata_coder-2.4.6 → ata_coder-2.4.8}/clawd_integration.py +0 -0
  36. {ata_coder-2.4.6 → ata_coder-2.4.8}/commands/__init__.py +0 -0
  37. {ata_coder-2.4.6 → ata_coder-2.4.8}/commands/_core.py +0 -0
  38. {ata_coder-2.4.6 → ata_coder-2.4.8}/commands/_safety.py +0 -0
  39. {ata_coder-2.4.6 → ata_coder-2.4.8}/commands/_settings.py +0 -0
  40. {ata_coder-2.4.6 → ata_coder-2.4.8}/commands/_workflow.py +0 -0
  41. {ata_coder-2.4.6 → ata_coder-2.4.8}/core/__init__.py +0 -0
  42. {ata_coder-2.4.6 → ata_coder-2.4.8}/core/events.py +0 -0
  43. {ata_coder-2.4.6 → ata_coder-2.4.8}/core/queue.py +0 -0
  44. {ata_coder-2.4.6 → ata_coder-2.4.8}/core/state.py +0 -0
  45. {ata_coder-2.4.6 → ata_coder-2.4.8}/event_queue.py +0 -0
  46. {ata_coder-2.4.6 → ata_coder-2.4.8}/extension.py +0 -0
  47. {ata_coder-2.4.6 → ata_coder-2.4.8}/extensions/__init__.py +0 -0
  48. {ata_coder-2.4.6 → ata_coder-2.4.8}/extensions/hello_skill.py +0 -0
  49. {ata_coder-2.4.6 → ata_coder-2.4.8}/fool_proof.py +0 -0
  50. {ata_coder-2.4.6 → ata_coder-2.4.8}/git_workflow.py +0 -0
  51. {ata_coder-2.4.6 → ata_coder-2.4.8}/gui.py +0 -0
  52. {ata_coder-2.4.6 → ata_coder-2.4.8}/llm_client.py +0 -0
  53. {ata_coder-2.4.6 → ata_coder-2.4.8}/mcp_client.py +0 -0
  54. {ata_coder-2.4.6 → ata_coder-2.4.8}/model_registry.py +0 -0
  55. {ata_coder-2.4.6 → ata_coder-2.4.8}/model_router.py +0 -0
  56. {ata_coder-2.4.6 → ata_coder-2.4.8}/permissions.py +0 -0
  57. {ata_coder-2.4.6 → ata_coder-2.4.8}/project.py +0 -0
  58. {ata_coder-2.4.6 → ata_coder-2.4.8}/prompt_template.py +0 -0
  59. {ata_coder-2.4.6 → ata_coder-2.4.8}/prompts/auto-mode.md +0 -0
  60. {ata_coder-2.4.6 → ata_coder-2.4.8}/prompts/coding-rules.md +0 -0
  61. {ata_coder-2.4.6 → ata_coder-2.4.8}/prompts/execution-guardrails.md +0 -0
  62. {ata_coder-2.4.6 → ata_coder-2.4.8}/prompts/memory-system.md +0 -0
  63. {ata_coder-2.4.6 → ata_coder-2.4.8}/prompts/output-style.md +0 -0
  64. {ata_coder-2.4.6 → ata_coder-2.4.8}/prompts/safety.md +0 -0
  65. {ata_coder-2.4.6 → ata_coder-2.4.8}/prompts/slash-commands.md +0 -0
  66. {ata_coder-2.4.6 → ata_coder-2.4.8}/prompts/sub-agents.md +0 -0
  67. {ata_coder-2.4.6 → ata_coder-2.4.8}/prompts/system-reminders.md +0 -0
  68. {ata_coder-2.4.6 → ata_coder-2.4.8}/prompts/system.md +0 -0
  69. {ata_coder-2.4.6 → ata_coder-2.4.8}/prompts/tool-policy.md +0 -0
  70. {ata_coder-2.4.6 → ata_coder-2.4.8}/py.typed +0 -0
  71. {ata_coder-2.4.6 → ata_coder-2.4.8}/repl_theme.py +0 -0
  72. {ata_coder-2.4.6 → ata_coder-2.4.8}/repl_tracker.py +0 -0
  73. {ata_coder-2.4.6 → ata_coder-2.4.8}/repl_ui.py +0 -0
  74. {ata_coder-2.4.6 → ata_coder-2.4.8}/self_correct.py +0 -0
  75. {ata_coder-2.4.6 → ata_coder-2.4.8}/server_shell.py +0 -0
  76. {ata_coder-2.4.6 → ata_coder-2.4.8}/session.py +0 -0
  77. {ata_coder-2.4.6 → ata_coder-2.4.8}/settings.py +0 -0
  78. {ata_coder-2.4.6 → ata_coder-2.4.8}/setup.cfg +0 -0
  79. {ata_coder-2.4.6 → ata_coder-2.4.8}/skill_extension.py +0 -0
  80. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/architect/SKILL.md +0 -0
  81. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/code-reviewer/SKILL.md +0 -0
  82. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/codecraft/SKILL.md +0 -0
  83. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/debugger/SKILL.md +0 -0
  84. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/doc-writer/SKILL.md +0 -0
  85. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/general-coder/SKILL.md +0 -0
  86. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/math-calculator/README.md +0 -0
  87. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/math-calculator/SKILL.md +0 -0
  88. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/math-calculator/handler.py +0 -0
  89. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/math-calculator/prompts/system.md +0 -0
  90. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/math-calculator/requirements.txt +0 -0
  91. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/math-calculator/resources/constants.json +0 -0
  92. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/math-calculator/tests/test_handler.py +0 -0
  93. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/security-auditor/SKILL.md +0 -0
  94. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/test-writer/SKILL.md +0 -0
  95. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/weather-skill/README.md +0 -0
  96. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/weather-skill/handler.py +0 -0
  97. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/weather-skill/manifest.json +0 -0
  98. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/weather-skill/prompts/system_prompt.txt +0 -0
  99. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/weather-skill/prompts/user_prompt_template.txt +0 -0
  100. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/weather-skill/requirements.txt +0 -0
  101. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/weather-skill/resources/city_list.json +0 -0
  102. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/weather-skill/resources/error_messages.json +0 -0
  103. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/weather-skill/tests/test_handler.py +0 -0
  104. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills/weather-skill/weather_utils.py +0 -0
  105. {ata_coder-2.4.6 → ata_coder-2.4.8}/skills.py +0 -0
  106. {ata_coder-2.4.6 → ata_coder-2.4.8}/sub_agent.py +0 -0
  107. {ata_coder-2.4.6 → ata_coder-2.4.8}/sub_agent_manager.py +0 -0
  108. {ata_coder-2.4.6 → ata_coder-2.4.8}/task_planner.py +0 -0
  109. {ata_coder-2.4.6 → ata_coder-2.4.8}/terminal.py +0 -0
  110. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_agent.py +0 -0
  111. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_change_tracker.py +0 -0
  112. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_config.py +0 -0
  113. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_event_queue.py +0 -0
  114. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_extension.py +0 -0
  115. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_fibonacci.py +0 -0
  116. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_fool_proof.py +0 -0
  117. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_llm_client.py +0 -0
  118. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_memory.py +0 -0
  119. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_model_registry.py +0 -0
  120. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_permissions.py +0 -0
  121. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_privilege.py +0 -0
  122. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_prompt_template.py +0 -0
  123. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_safety_guard.py +0 -0
  124. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_server.py +0 -0
  125. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_skill_handlers.py +0 -0
  126. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_sub_agent.py +0 -0
  127. {ata_coder-2.4.6 → ata_coder-2.4.8}/tests/test_tools.py +0 -0
  128. {ata_coder-2.4.6 → ata_coder-2.4.8}/tools/__init__.py +0 -0
  129. {ata_coder-2.4.6 → ata_coder-2.4.8}/tools/definitions.py +0 -0
  130. {ata_coder-2.4.6 → ata_coder-2.4.8}/tools/executor.py +0 -0
  131. {ata_coder-2.4.6 → ata_coder-2.4.8}/tools/result.py +0 -0
  132. {ata_coder-2.4.6 → ata_coder-2.4.8}/tools/strategy.py +0 -0
  133. {ata_coder-2.4.6 → ata_coder-2.4.8}/tools/subagent.py +0 -0
  134. {ata_coder-2.4.6 → ata_coder-2.4.8}/tools/web.py +0 -0
  135. {ata_coder-2.4.6 → ata_coder-2.4.8}/types.py +0 -0
  136. {ata_coder-2.4.6 → ata_coder-2.4.8}/utils.py +0 -0
  137. {ata_coder-2.4.6 → ata_coder-2.4.8}/web/css/style.css +0 -0
  138. {ata_coder-2.4.6 → ata_coder-2.4.8}/web/index.html +0 -0
  139. {ata_coder-2.4.6 → ata_coder-2.4.8}/web/js/app.js +0 -0
  140. {ata_coder-2.4.6 → ata_coder-2.4.8}/web/package-lock.json +0 -0
  141. {ata_coder-2.4.6 → ata_coder-2.4.8}/web/package.json +0 -0
  142. {ata_coder-2.4.6 → ata_coder-2.4.8}/web/tsconfig.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ata-coder
3
- Version: 2.4.6
3
+ Version: 2.4.8
4
4
  Summary: ATA Coder — AI-powered coding assistant
5
5
  Author: ATA Coder Team
6
6
  License-Expression: MIT
@@ -21,13 +21,15 @@ Requires-Dist: pytest-timeout>=2.0; extra == "dev"
21
21
  Requires-Dist: tiktoken>=0.5.0; extra == "dev"
22
22
  Dynamic: license-file
23
23
 
24
- # ATA Coder v2.4.6
24
+ # ATA Coder v2.4.7
25
25
 
26
26
  **AI-powered coding assistant — async, AST-aware, single config file.**
27
27
 
28
28
  [English](#english) | [中文](#中文)
29
29
 
30
- > **v2.4.6** — 🔐 **OS-Native Credential Store**: API key encrypted at rest via Windows DPAPI / macOS Keychain / Linux secret-tool. Auto-migrates plaintext keys. Zero dependencies.
30
+ > **v2.4.7** — **Context Memory Refactor**: O(1) token tracking, ContextManager, section-level prompt caching, pre-tokenized TF-IDF, LRU token cache. ~60% less overhead in the hot loop.
31
+ >
32
+ > > **v2.4.6** — 🔐 **OS-Native Credential Store**: API key encrypted at rest via Windows DPAPI / macOS Keychain / Linux secret-tool. Auto-migrates plaintext keys. Zero dependencies.
31
33
  >
32
34
  > > **v2.4.5** — 🤖 **Self-Bootstrapped Audit**: ATA Coder found 19 bugs in its own source code and fixed them all — thread safety, SSRF IPv6, rate limiter leak, auth hardening, DRY refactoring, CI coverage. 12 files changed.
33
35
  >
@@ -550,7 +552,9 @@ All 6 findings in this release were discovered by **ATA Coder scanning its own s
550
552
 
551
553
  ## 中文
552
554
 
553
- > **v2.4.6** — 🔐 **操作系统凭据存储**: API Key 通过 Windows DPAPI / macOS Keychain / Linux secret-tool 加密存储。自动迁移明文密钥。零依赖。
555
+ > **v2.4.7** — **上下文记忆重构**: O(1) Token 追踪、ContextManager、章节级提示缓存、预分词 TF-IDF、LRU Token 缓存。热路径开销降低约 60%。
556
+ >
557
+ > > **v2.4.6** — 🔐 **操作系统凭据存储**: API Key 通过 Windows DPAPI / macOS Keychain / Linux secret-tool 加密存储。自动迁移明文密钥。零依赖。
554
558
  >
555
559
  > > **v2.4.5** — 🤖 **自举审计**: ATA Coder 审计自身源码发现 19 个 bug 并全部修复 — 线程安全、SSRF IPv6、速率限制器泄漏、认证加固、DRY 重构、CI 覆盖。12 个文件变更。
556
560
  >
@@ -1,10 +1,12 @@
1
- # ATA Coder v2.4.6
1
+ # ATA Coder v2.4.7
2
2
 
3
3
  **AI-powered coding assistant — async, AST-aware, single config file.**
4
4
 
5
5
  [English](#english) | [中文](#中文)
6
6
 
7
- > **v2.4.6** — 🔐 **OS-Native Credential Store**: API key encrypted at rest via Windows DPAPI / macOS Keychain / Linux secret-tool. Auto-migrates plaintext keys. Zero dependencies.
7
+ > **v2.4.7** — **Context Memory Refactor**: O(1) token tracking, ContextManager, section-level prompt caching, pre-tokenized TF-IDF, LRU token cache. ~60% less overhead in the hot loop.
8
+ >
9
+ > > **v2.4.6** — 🔐 **OS-Native Credential Store**: API key encrypted at rest via Windows DPAPI / macOS Keychain / Linux secret-tool. Auto-migrates plaintext keys. Zero dependencies.
8
10
  >
9
11
  > > **v2.4.5** — 🤖 **Self-Bootstrapped Audit**: ATA Coder found 19 bugs in its own source code and fixed them all — thread safety, SSRF IPv6, rate limiter leak, auth hardening, DRY refactoring, CI coverage. 12 files changed.
10
12
  >
@@ -527,7 +529,9 @@ All 6 findings in this release were discovered by **ATA Coder scanning its own s
527
529
 
528
530
  ## 中文
529
531
 
530
- > **v2.4.6** — 🔐 **操作系统凭据存储**: API Key 通过 Windows DPAPI / macOS Keychain / Linux secret-tool 加密存储。自动迁移明文密钥。零依赖。
532
+ > **v2.4.7** — **上下文记忆重构**: O(1) Token 追踪、ContextManager、章节级提示缓存、预分词 TF-IDF、LRU Token 缓存。热路径开销降低约 60%。
533
+ >
534
+ > > **v2.4.6** — 🔐 **操作系统凭据存储**: API Key 通过 Windows DPAPI / macOS Keychain / Linux secret-tool 加密存储。自动迁移明文密钥。零依赖。
531
535
  >
532
536
  > > **v2.4.5** — 🤖 **自举审计**: ATA Coder 审计自身源码发现 19 个 bug 并全部修复 — 线程安全、SSRF IPv6、速率限制器泄漏、认证加固、DRY 重构、CI 覆盖。12 个文件变更。
533
537
  >
@@ -44,6 +44,7 @@ from .agent_compact import CompactionMixin
44
44
  from .agent_tools import ToolExecutionMixin
45
45
  from .agent_routing import ModelRoutingMixin
46
46
  from .agent_extension import ExtensionMixin
47
+ from .context_manager import ContextManager
47
48
 
48
49
  # ── Event types & Agent state ──────────────────────────────────────────
49
50
  from .core import ( # noqa: F401 — re-exported for external use
@@ -141,6 +142,10 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
141
142
  self.self_correct = SelfCorrectionEngine(max_retries=1)
142
143
  self.git = GitWorkflow(self.tools.workspace)
143
144
 
145
+ # Per-instance self-correction depth (was a class variable shared across
146
+ # all agent instances — dangerous under ThreadingHTTPServer in server mode).
147
+ self._self_correct_depth: int = 0
148
+
144
149
  self._state = AgentState()
145
150
  self._on_event: Callable[[AgentEvent], None] | None = None
146
151
  self._current_session_id: str = ""
@@ -148,6 +153,10 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
148
153
  self._cached_system_prompt: str | None = None # invalidated on new build / compact
149
154
  self._cached_allowed_tools: set[str] | None = None # invalidated on skill change
150
155
 
156
+ # ── Context manager (O(1) token tracking, segment-split, adaptive compact) ──
157
+ self._context_manager = ContextManager(self.config.agent)
158
+ self._summary_llm = None # lazily created summarisation client
159
+
151
160
  # Build the combined tool list
152
161
  self._all_tools = list(TOOL_DEFINITIONS)
153
162
  if self.mcp:
@@ -227,7 +236,7 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
227
236
  if not reset_context and self._state.messages:
228
237
  # Append new user message to existing conversation; keep system
229
238
  # prompt and all prior messages intact.
230
- self._state.messages.append({"role": "user", "content": task})
239
+ self._append_message({"role": "user", "content": task})
231
240
  # Rebuild system prompt for updated memory context but don't
232
241
  # replace the original system message (memory/git context may
233
242
  # have changed, but conversation integrity is paramount).
@@ -291,10 +300,12 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
291
300
  self._cached_system_prompt = system_prompt # pre-seed cache
292
301
  self._cached_allowed_tools = None # invalidate on new run
293
302
 
294
- self._state.messages = [
303
+ initial_msgs = [
295
304
  {"role": "system", "content": system_prompt},
296
305
  {"role": "user", "content": task},
297
306
  ]
307
+ self._state.messages = initial_msgs
308
+ self._context_manager.replace_all(initial_msgs)
298
309
 
299
310
  logger.info("Agent run: skill=%s, model=%s, session=%s, task=%.100s",
300
311
  self.skills.active_skill.name if self.skills and self.skills.active_skill else "default",
@@ -352,24 +363,19 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
352
363
  # Clawd: model is generating, show thinking animation
353
364
  get_clawd().thinking()
354
365
 
355
- # Auto-compact when approaching the effective context limit.
356
- # effective_context_tokens (default 200k) reflects the range where
357
- # the model actually pays attention, not the theoretical 1M window.
358
- # We compact at 80% of effective limit, which is well below the
359
- # theoretical max_context_tokens.
360
- est_tokens = self.get_token_estimate()
361
- max_tokens = self.config.agent.max_context_tokens
362
- effective = self.config.agent.effective_context_tokens
363
- if est_tokens > effective:
366
+ # Auto-compact when approaching the effective context limit (O(1) check).
367
+ if self._context_manager.should_compact():
368
+ est = self._context_manager.token_total
369
+ max_t = self.config.agent.max_context_tokens
364
370
  logger.warning("Token budget: %d/%d effective (%.0f%% of %d max), auto-compacting",
365
- est_tokens, effective, est_tokens / max(max_tokens, 1) * 100, max_tokens)
371
+ est, self.config.agent.effective_context_tokens,
372
+ est / max(max_t, 1) * 100, max_t)
366
373
  await self.compact()
367
- # Re-estimate AFTER compaction — the message list has changed
368
- est_tokens = self.get_token_estimate()
369
374
  # Hard ceiling: if compaction didn't help enough, force-truncate
370
- if est_tokens > max_tokens * 0.95:
375
+ if self._context_manager.needs_force_truncate():
371
376
  logger.critical("Hard token ceiling: %d > 95%% of %d max. Force-truncating.",
372
- est_tokens, max_tokens)
377
+ self._context_manager.token_total,
378
+ self.config.agent.max_context_tokens)
373
379
  self._force_truncate()
374
380
 
375
381
  # Get allowed tools from multi-skill intersection
@@ -437,7 +443,7 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
437
443
  }
438
444
  if response.get("reasoning_content"):
439
445
  assistant_msg["reasoning_content"] = response["reasoning_content"]
440
- self._state.messages.append(assistant_msg)
446
+ self._append_message(assistant_msg)
441
447
  for tc, result in zip(tool_calls, results, strict=True):
442
448
  self._warn_if_large_result(result, tc["function"]["name"])
443
449
  self._store_tool_result(result, tc["id"])
@@ -463,7 +469,7 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
463
469
  }
464
470
  if response.get("reasoning_content"):
465
471
  assistant_msg["reasoning_content"] = response["reasoning_content"]
466
- self._state.messages.append(assistant_msg)
472
+ self._append_message(assistant_msg)
467
473
  for tc, result in zip(tool_calls, batch_results, strict=True):
468
474
  self._store_tool_result(result, tc["id"])
469
475
 
@@ -579,25 +585,23 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
579
585
  Mirrors the main run() loop: skill tool filtering, token compaction,
580
586
  consecutive-failure detection, and circuit breaker.
581
587
  """
582
- self._state.messages.append({"role": "user", "content": message})
588
+ self._append_message({"role": "user", "content": message})
583
589
 
584
590
  SAFETY_LIMIT = 999 # circuit breaker
585
591
  _consecutive_failures = 0
586
592
  _MAX_CONSECUTIVE_FAILURES = 5
587
593
 
588
594
  while self._state.tool_call_count < SAFETY_LIMIT:
589
- # ── Token budget: auto-compact when approaching the limit ────
590
- est_tokens = self.get_token_estimate()
591
- max_tokens = self.config.agent.max_context_tokens
592
- effective = self.config.agent.effective_context_tokens
593
- if est_tokens > effective:
595
+ # ── Token budget: auto-compact when approaching the limit (O(1)) ──
596
+ if self._context_manager.should_compact():
594
597
  logger.warning("chat(): token budget %d/%d effective, auto-compacting",
595
- est_tokens, effective)
598
+ self._context_manager.token_total,
599
+ self.config.agent.effective_context_tokens)
596
600
  await self.compact()
597
- est_tokens = self.get_token_estimate()
598
- if est_tokens > max_tokens * 0.95:
601
+ if self._context_manager.needs_force_truncate():
599
602
  logger.critical("chat(): hard ceiling %d > 95%% of %d, force-truncating",
600
- est_tokens, max_tokens)
603
+ self._context_manager.token_total,
604
+ self.config.agent.max_context_tokens)
601
605
  self._force_truncate()
602
606
 
603
607
  # ── Skill tool filtering ────────────────────────────────────
@@ -639,7 +643,7 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
639
643
  batch_results.append(result)
640
644
  self._warn_if_large_result(result, tool_name)
641
645
 
642
- self._state.messages.append({
646
+ self._append_message({
643
647
  "role": "assistant",
644
648
  "content": text or None,
645
649
  "tool_calls": [tc],
@@ -850,8 +854,15 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
850
854
 
851
855
  # ── Change tracking helper → agent_tools.py (ToolExecutionMixin._read_old_content)
852
856
 
857
+ def _append_message(self, msg: Message) -> None:
858
+ """Append a message to state AND context manager (O(1) token update)."""
859
+ self._state.messages.append(msg)
860
+ self._context_manager.append(msg)
861
+
853
862
  def get_token_estimate(self) -> int:
854
- """Estimate total tokens in the conversation."""
863
+ """O(1) token total from ContextManager. Falls back to LLM count if stale."""
864
+ if self._context_manager.messages:
865
+ return self._context_manager.token_total
855
866
  return self.llm.count_tokens_approx(self._state.messages)
856
867
 
857
868
  def get_conversation_summary(self) -> str:
@@ -880,5 +891,7 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
880
891
  # Clawd: final SessionEnd
881
892
  get_clawd().shutdown()
882
893
  await self.llm.close()
894
+ if self._summary_llm:
895
+ await self._summary_llm.close()
883
896
  if self.mcp:
884
897
  await self.mcp.stop_all()
@@ -0,0 +1,159 @@
1
+ """Context compaction and token budget management — mixin for CoderAgent.
2
+
3
+ Delegates all context operations to ContextManager. This mixin is now a
4
+ thin wrapper that provides the same public API while eliminating duplicated
5
+ logic, avoiding deep copies, and reusing the summarisation LLM client.
6
+ """
7
+
8
+ import copy
9
+ import logging
10
+
11
+ from .types import Message
12
+ from .clawd_integration import get_clawd
13
+ from .model_router import get_subagent_model
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class CompactionMixin:
19
+ """Context window compaction — thin wrapper around ContextManager."""
20
+
21
+ # ── Compaction token budget (class-level defaults, overridable) ───────
22
+ RECENT_TOKEN_BUDGET = 80_000 # max tokens to keep in the recent segment
23
+ COMPACT_IF_FEWER_THAN = 6 # skip compaction if fewer than this many msgs
24
+
25
+ # ── Core compaction ───────────────────────────────────────────────────
26
+
27
+ async def compact(self) -> str:
28
+ """Compact conversation by summarising old messages.
29
+
30
+ Strategy: keep system prompt + recent messages up to
31
+ RECENT_TOKEN_BUDGET tokens, summarise everything in between using
32
+ a cheap LLM call. Falls back to a lightweight extractive summary
33
+ if the API call fails.
34
+
35
+ Delegates segment-splitting to ContextManager to avoid the
36
+ duplicated walk-backwards logic that was previously shared with
37
+ _force_truncate.
38
+ """
39
+ cm = self._context_manager
40
+ if not cm.can_compact():
41
+ return "Already compact."
42
+
43
+ # Clawd: PreCompact
44
+ get_clawd().compact()
45
+
46
+ system_msg, recent, archive = cm.split_into_segments()
47
+
48
+ if not archive:
49
+ return "Already compact (all messages fit in recent budget)."
50
+
51
+ # Extract summary metadata from the archive segment
52
+ tool_count = sum(1 for m in archive if m.get("tool_calls"))
53
+ user_msgs = [m.get("content", "")[:200] for m in archive if m.get("role") == "user"]
54
+ file_ops = cm.collect_file_ops(archive)
55
+
56
+ summary = await self._summarise_messages(archive, file_ops, user_msgs, tool_count)
57
+
58
+ old_count = len(cm.messages)
59
+ old_tokens = cm.token_total
60
+
61
+ truncated: list[Message] = []
62
+ if system_msg:
63
+ truncated.append(system_msg)
64
+ truncated.append({
65
+ "role": "user",
66
+ "content": "[Conversation summary]\n" + summary,
67
+ })
68
+ truncated.append({
69
+ "role": "assistant",
70
+ "content": "Understood. I'll continue with the remaining context using the summary above.",
71
+ })
72
+ truncated.extend(recent)
73
+
74
+ cm.replace_all(truncated)
75
+ self._cached_system_prompt = None # system msg may have shifted
76
+ self._state.messages = cm.messages # sync for backward compat
77
+
78
+ new_tokens = cm.token_total
79
+ logger.info("Compacted: %d→%d msgs, ~%d→%d tokens (files: %d, tools: %d)",
80
+ old_count, len(truncated), old_tokens, new_tokens,
81
+ len(file_ops), tool_count)
82
+ return (f"Compacted from {old_count}→{len(truncated)} messages "
83
+ f"(~{old_tokens:,}→~{new_tokens:,} tokens, {len(file_ops)} files, {tool_count} tool calls).")
84
+
85
+ def _force_truncate(self) -> None:
86
+ """Drop the oldest non-system messages when we exceed 95% of max tokens.
87
+
88
+ Called only as a last resort after compaction has already run.
89
+ Delegates to ContextManager.build_truncated_list() — no more
90
+ duplicated walk-backwards.
91
+ """
92
+ cm = self._context_manager
93
+ if len(cm.messages) <= 6:
94
+ return
95
+ truncated, result = cm.build_truncated_list()
96
+ cm.replace_all(truncated)
97
+ self._cached_system_prompt = None
98
+ self._state.messages = cm.messages # sync
99
+ logger.warning("Force-truncated: %d → %d messages (~%d tokens kept)",
100
+ result.old_count, result.new_count, result.new_tokens)
101
+
102
+ # ── Token estimation helpers (delegate to ContextManager) ────────────
103
+
104
+ def _estimate_message_tokens(self, msg: Message) -> int:
105
+ """Rough token estimate for a single message (via ContextManager cache)."""
106
+ return self._context_manager.get_msg_tokens(msg)
107
+
108
+ @staticmethod
109
+ def _collect_file_ops(messages: list[Message]) -> list[str]:
110
+ """Collect files modified in a message list (static, delegates to CM)."""
111
+ from .context_manager import ContextManager
112
+ return ContextManager.collect_file_ops(messages)
113
+
114
+ # ── Summarisation (reuses a single cheap LLM client) ──────────────────
115
+
116
+ async def _summarise_messages(self, archive: list[Message], file_ops: list[str],
117
+ user_msgs: list[str], tool_count: int) -> str:
118
+ """Generate a summary of the archive conversation segment.
119
+
120
+ Attempts a cheap LLM call first; falls back to a lightweight extractive
121
+ summary so the user never loses context entirely. The summarisation
122
+ client is created once and reused across compactions.
123
+ """
124
+ # ── LLM-based summary (best effort) ──────────────────────────────
125
+ try:
126
+ summary_prompt = (
127
+ "Summarise this conversation segment in 3-5 bullet points. "
128
+ "Focus on: what the user asked, what files were changed, what "
129
+ "decisions were made, and any unresolved issues. "
130
+ "Be concise — this summary will replace the full conversation "
131
+ "history to save context tokens.\n\n"
132
+ f"Files modified: {', '.join(file_ops) if file_ops else 'none'}\n"
133
+ f"Tool calls: {tool_count}\n"
134
+ f"User requests: {'; '.join(user_msgs[:5])}\n"
135
+ )
136
+ sc = getattr(self, '_summary_llm', None)
137
+ if sc is None:
138
+ from .llm_client import LLMClient
139
+ summary_config = copy.deepcopy(self.llm.config)
140
+ summary_config.model = get_subagent_model()
141
+ sc = LLMClient(summary_config)
142
+ self._summary_llm = sc # cache for reuse
143
+ resp = await sc.chat([{"role": "user", "content": summary_prompt}], tools=[])
144
+ llm_summary = (resp.get("content") or "").strip()
145
+ if llm_summary:
146
+ parts = [llm_summary]
147
+ if file_ops:
148
+ parts.append(f"\nFiles touched: {', '.join(file_ops[:10])}")
149
+ return "\n".join(parts)
150
+ except Exception:
151
+ logger.debug("LLM summarisation unavailable, using extractive fallback")
152
+
153
+ # ── Extractive fallback ─────────────────────────────────────────
154
+ parts = [f"Summarised {len(archive)} messages ({tool_count} tool calls)."]
155
+ if user_msgs:
156
+ parts.append(f"Topics: {'; '.join(user_msgs[:5])}")
157
+ if file_ops:
158
+ parts.append(f"Files modified: {', '.join(file_ops[:10])}")
159
+ return "\n".join(parts)
@@ -17,7 +17,8 @@ class ToolExecutionMixin:
17
17
  # ── Tool execution ────────────────────────────────────────────────────
18
18
 
19
19
  # Guard depth for self-correction retry — prevents infinite recursion.
20
- _self_correct_depth: int = 0
20
+ # These are set as INSTANCE variables in CoderAgent.__init__ to avoid
21
+ # cross-session contamination under ThreadingHTTPServer (server mode).
21
22
  _MAX_SELF_CORRECT_DEPTH: int = 1
22
23
 
23
24
  async def _execute_tool(self, tool_name: str, arguments: dict[str, Any]) -> ToolResult:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ata-coder
3
- Version: 2.4.6
3
+ Version: 2.4.8
4
4
  Summary: ATA Coder — AI-powered coding assistant
5
5
  Author: ATA Coder Team
6
6
  License-Expression: MIT
@@ -21,13 +21,15 @@ Requires-Dist: pytest-timeout>=2.0; extra == "dev"
21
21
  Requires-Dist: tiktoken>=0.5.0; extra == "dev"
22
22
  Dynamic: license-file
23
23
 
24
- # ATA Coder v2.4.6
24
+ # ATA Coder v2.4.7
25
25
 
26
26
  **AI-powered coding assistant — async, AST-aware, single config file.**
27
27
 
28
28
  [English](#english) | [中文](#中文)
29
29
 
30
- > **v2.4.6** — 🔐 **OS-Native Credential Store**: API key encrypted at rest via Windows DPAPI / macOS Keychain / Linux secret-tool. Auto-migrates plaintext keys. Zero dependencies.
30
+ > **v2.4.7** — **Context Memory Refactor**: O(1) token tracking, ContextManager, section-level prompt caching, pre-tokenized TF-IDF, LRU token cache. ~60% less overhead in the hot loop.
31
+ >
32
+ > > **v2.4.6** — 🔐 **OS-Native Credential Store**: API key encrypted at rest via Windows DPAPI / macOS Keychain / Linux secret-tool. Auto-migrates plaintext keys. Zero dependencies.
31
33
  >
32
34
  > > **v2.4.5** — 🤖 **Self-Bootstrapped Audit**: ATA Coder found 19 bugs in its own source code and fixed them all — thread safety, SSRF IPv6, rate limiter leak, auth hardening, DRY refactoring, CI coverage. 12 files changed.
33
35
  >
@@ -550,7 +552,9 @@ All 6 findings in this release were discovered by **ATA Coder scanning its own s
550
552
 
551
553
  ## 中文
552
554
 
553
- > **v2.4.6** — 🔐 **操作系统凭据存储**: API Key 通过 Windows DPAPI / macOS Keychain / Linux secret-tool 加密存储。自动迁移明文密钥。零依赖。
555
+ > **v2.4.7** — **上下文记忆重构**: O(1) Token 追踪、ContextManager、章节级提示缓存、预分词 TF-IDF、LRU Token 缓存。热路径开销降低约 60%。
556
+ >
557
+ > > **v2.4.6** — 🔐 **操作系统凭据存储**: API Key 通过 Windows DPAPI / macOS Keychain / Linux secret-tool 加密存储。自动迁移明文密钥。零依赖。
554
558
  >
555
559
  > > **v2.4.5** — 🤖 **自举审计**: ATA Coder 审计自身源码发现 19 个 bug 并全部修复 — 线程安全、SSRF IPv6、速率限制器泄漏、认证加固、DRY 重构、CI 覆盖。12 个文件变更。
556
560
  >
@@ -14,6 +14,7 @@ anthropic_client.py
14
14
  change_tracker.py
15
15
  clawd_integration.py
16
16
  config.py
17
+ context_manager.py
17
18
  event_queue.py
18
19
  extension.py
19
20
  fool_proof.py
@@ -65,6 +66,7 @@ utils.py
65
66
  ./change_tracker.py
66
67
  ./clawd_integration.py
67
68
  ./config.py
69
+ ./context_manager.py
68
70
  ./event_queue.py
69
71
  ./extension.py
70
72
  ./fool_proof.py
@@ -143,7 +143,7 @@ class ChangeTracker:
143
143
  old_content = f.read()
144
144
  self._backup(file_path)
145
145
  except Exception:
146
- pass
146
+ logger.debug("Failed to read/backup existing file %s", file_path, exc_info=True)
147
147
 
148
148
  change = FileChange(
149
149
  id=self._next_id,
@@ -120,6 +120,13 @@ class AgentConfig:
120
120
  self, "effective_context_tokens",
121
121
  max(10000, int(self.max_context_tokens * 0.9)),
122
122
  )
123
+ # Compaction/context budgets (passed to ContextManager)
124
+ recent_token_budget: int = field(
125
+ default_factory=lambda: int(_from_settings("recent_token_budget", 80000))
126
+ )
127
+ compact_if_fewer_than: int = field(
128
+ default_factory=lambda: int(_from_settings("compact_if_fewer_than", 6))
129
+ )
123
130
  max_message_output_chars: int = field(
124
131
  default_factory=lambda: int(_from_settings("max_message_output_chars", 8000))
125
132
  )
@@ -190,17 +197,21 @@ class AppConfig:
190
197
  # Using a lazy pattern because AppConfig.load() references _from_settings()
191
198
  # which is defined after the dataclass body in this module.
192
199
  _config: AppConfig | None = None
200
+ _config_lock = threading.Lock()
193
201
 
194
202
 
195
203
  def get_config() -> AppConfig:
196
204
  """Return the module-level config singleton (lazy init on first call).
197
205
 
198
- After the first call the config is cached. The codebase is
199
- single-threaded by design so no lock is needed.
206
+ After the first call the config is cached. Double-checked locking
207
+ protects the lazy-init path when server.py runs under ThreadingHTTPServer
208
+ (where multiple threads may race on the first call).
200
209
  """
201
210
  global _config
202
211
  if _config is None:
203
- _config = AppConfig.load()
212
+ with _config_lock:
213
+ if _config is None: # double-check
214
+ _config = AppConfig.load()
204
215
  return _config
205
216
 
206
217