autosnippet 3.0.1 → 3.0.2

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 (290) hide show
  1. package/README.md +230 -324
  2. package/bin/api-server.js +1 -1
  3. package/bin/cli.js +204 -244
  4. package/bin/mcp-server.js +5 -3
  5. package/config/knowledge-base.config.js +132 -132
  6. package/dashboard/dist/assets/{icons-CEfgGaZi.js → icons-Cdq22n2i.js} +95 -100
  7. package/dashboard/dist/assets/index-ClkyPkDX.js +133 -0
  8. package/dashboard/dist/assets/index-t4QrJwv1.css +1 -0
  9. package/dashboard/dist/index.html +3 -3
  10. package/lib/bootstrap.js +8 -8
  11. package/lib/cli/AiScanService.js +86 -40
  12. package/lib/cli/KnowledgeSyncService.js +113 -74
  13. package/lib/cli/SetupService.js +439 -277
  14. package/lib/cli/UpgradeService.js +63 -100
  15. package/lib/core/AstAnalyzer.js +276 -597
  16. package/lib/core/ast/ProjectGraph.js +101 -40
  17. package/lib/core/ast/ensure-grammars.js +232 -0
  18. package/lib/core/ast/index.js +115 -0
  19. package/lib/core/ast/lang-dart.js +661 -0
  20. package/lib/core/ast/lang-go.js +530 -0
  21. package/lib/core/ast/lang-java.js +435 -0
  22. package/lib/core/ast/lang-javascript.js +272 -0
  23. package/lib/core/ast/lang-kotlin.js +423 -0
  24. package/lib/core/ast/lang-objc.js +388 -0
  25. package/lib/core/ast/lang-python.js +371 -0
  26. package/lib/core/ast/lang-swift.js +337 -0
  27. package/lib/core/ast/lang-typescript.js +503 -0
  28. package/lib/core/capability/CapabilityProbe.js +18 -9
  29. package/lib/core/constitution/Constitution.js +2 -3
  30. package/lib/core/constitution/ConstitutionValidator.js +65 -24
  31. package/lib/core/discovery/DartDiscoverer.js +534 -0
  32. package/lib/core/discovery/DiscovererRegistry.js +83 -0
  33. package/lib/core/discovery/GenericDiscoverer.js +225 -0
  34. package/lib/core/discovery/GoDiscoverer.js +541 -0
  35. package/lib/core/discovery/JvmDiscoverer.js +506 -0
  36. package/lib/core/discovery/NodeDiscoverer.js +466 -0
  37. package/lib/core/discovery/ProjectDiscoverer.js +93 -0
  38. package/lib/core/discovery/PythonDiscoverer.js +338 -0
  39. package/lib/core/discovery/SpmDiscoverer.js +5 -0
  40. package/lib/core/discovery/index.js +53 -0
  41. package/lib/core/enhancement/EnhancementPack.js +71 -0
  42. package/lib/core/enhancement/EnhancementRegistry.js +47 -0
  43. package/lib/core/enhancement/android-enhancement.js +102 -0
  44. package/lib/core/enhancement/django-enhancement.js +70 -0
  45. package/lib/core/enhancement/fastapi-enhancement.js +63 -0
  46. package/lib/core/enhancement/go-grpc-enhancement.js +152 -0
  47. package/lib/core/enhancement/go-web-enhancement.js +201 -0
  48. package/lib/core/enhancement/index.js +65 -0
  49. package/lib/core/enhancement/node-server-enhancement.js +88 -0
  50. package/lib/core/enhancement/react-enhancement.js +86 -0
  51. package/lib/core/enhancement/spring-enhancement.js +112 -0
  52. package/lib/core/enhancement/vue-enhancement.js +96 -0
  53. package/lib/core/gateway/Gateway.js +8 -9
  54. package/lib/core/gateway/GatewayActionRegistry.js +1 -1
  55. package/lib/core/permission/PermissionManager.js +12 -8
  56. package/lib/domain/index.js +13 -9
  57. package/lib/domain/knowledge/KnowledgeEntry.js +111 -101
  58. package/lib/domain/knowledge/KnowledgeRepository.js +0 -1
  59. package/lib/domain/knowledge/Lifecycle.js +22 -22
  60. package/lib/domain/knowledge/index.js +9 -12
  61. package/lib/domain/knowledge/values/Constraints.js +31 -21
  62. package/lib/domain/knowledge/values/Content.js +21 -13
  63. package/lib/domain/knowledge/values/Quality.js +31 -18
  64. package/lib/domain/knowledge/values/Reasoning.js +20 -12
  65. package/lib/domain/knowledge/values/Relations.js +37 -25
  66. package/lib/domain/knowledge/values/Stats.js +18 -12
  67. package/lib/domain/knowledge/values/index.js +4 -3
  68. package/lib/domain/snippet/Snippet.js +35 -10
  69. package/lib/external/ai/AiFactory.js +48 -16
  70. package/lib/external/ai/AiProvider.js +184 -90
  71. package/lib/external/ai/providers/ClaudeProvider.js +25 -12
  72. package/lib/external/ai/providers/GoogleGeminiProvider.js +59 -30
  73. package/lib/external/ai/providers/MockProvider.js +9 -3
  74. package/lib/external/ai/providers/OpenAiProvider.js +51 -29
  75. package/lib/external/mcp/McpServer.js +66 -36
  76. package/lib/external/mcp/errorHandler.js +23 -11
  77. package/lib/external/mcp/handlers/LanguageExtensions.js +138 -53
  78. package/lib/external/mcp/handlers/TargetClassifier.js +52 -16
  79. package/lib/external/mcp/handlers/bootstrap/pipeline/BootstrapSnapshot.js +81 -20
  80. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +71 -42
  81. package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +9 -17
  82. package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +14 -9
  83. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +15 -7
  84. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +352 -153
  85. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +52 -12
  86. package/lib/external/mcp/handlers/bootstrap/skills.js +143 -39
  87. package/lib/external/mcp/handlers/bootstrap.js +691 -168
  88. package/lib/external/mcp/handlers/browse.js +66 -22
  89. package/lib/external/mcp/handlers/candidate.js +118 -35
  90. package/lib/external/mcp/handlers/consolidated.js +49 -17
  91. package/lib/external/mcp/handlers/guard.js +104 -39
  92. package/lib/external/mcp/handlers/knowledge.js +60 -36
  93. package/lib/external/mcp/handlers/search.js +43 -14
  94. package/lib/external/mcp/handlers/skill.js +120 -45
  95. package/lib/external/mcp/handlers/structure.js +240 -86
  96. package/lib/external/mcp/handlers/system.js +42 -12
  97. package/lib/external/mcp/handlers/wiki.js +58 -33
  98. package/lib/external/mcp/tools.js +306 -123
  99. package/lib/http/HttpServer.js +72 -47
  100. package/lib/http/middleware/RateLimiter.js +5 -3
  101. package/lib/http/middleware/errorHandler.js +6 -1
  102. package/lib/http/middleware/requestLogger.js +14 -3
  103. package/lib/http/middleware/roleResolver.js +30 -23
  104. package/lib/http/routes/ai.js +387 -265
  105. package/lib/http/routes/auth.js +81 -61
  106. package/lib/http/routes/candidates.js +430 -320
  107. package/lib/http/routes/commands.js +289 -189
  108. package/lib/http/routes/extract.js +158 -125
  109. package/lib/http/routes/guardRules.js +309 -217
  110. package/lib/http/routes/knowledge.js +213 -154
  111. package/lib/http/routes/modules.js +578 -0
  112. package/lib/http/routes/monitoring.js +6 -6
  113. package/lib/http/routes/recipes.js +104 -93
  114. package/lib/http/routes/search.js +361 -305
  115. package/lib/http/routes/skills.js +145 -98
  116. package/lib/http/routes/snippets.js +42 -30
  117. package/lib/http/routes/spm.js +3 -405
  118. package/lib/http/routes/violations.js +113 -93
  119. package/lib/http/routes/wiki.js +211 -170
  120. package/lib/http/utils/routeHelpers.js +3 -1
  121. package/lib/http/utils/sse-sessions.js +16 -6
  122. package/lib/http/utils/sse.js +15 -5
  123. package/lib/infrastructure/audit/AuditLogger.js +5 -2
  124. package/lib/infrastructure/audit/AuditStore.js +10 -7
  125. package/lib/infrastructure/cache/CacheService.js +3 -1
  126. package/lib/infrastructure/cache/GraphCache.js +8 -4
  127. package/lib/infrastructure/cache/UnifiedCacheAdapter.js +1 -1
  128. package/lib/infrastructure/config/ConfigLoader.js +9 -5
  129. package/lib/infrastructure/config/Defaults.js +30 -10
  130. package/lib/infrastructure/config/Paths.js +28 -8
  131. package/lib/infrastructure/config/TriggerSymbol.js +22 -10
  132. package/lib/infrastructure/database/DatabaseConnection.js +15 -10
  133. package/lib/infrastructure/database/migrations/001_initial_schema.js +0 -1
  134. package/lib/infrastructure/external/ClipboardManager.js +6 -2
  135. package/lib/infrastructure/external/NativeUi.js +50 -43
  136. package/lib/infrastructure/external/OpenBrowser.js +14 -17
  137. package/lib/infrastructure/external/XcodeAutomation.js +14 -258
  138. package/lib/infrastructure/logging/Logger.js +46 -30
  139. package/lib/infrastructure/monitoring/ErrorTracker.js +7 -5
  140. package/lib/infrastructure/monitoring/PerformanceMonitor.js +12 -4
  141. package/lib/infrastructure/paths/HeaderResolver.js +25 -9
  142. package/lib/infrastructure/paths/PathFinder.js +34 -12
  143. package/lib/infrastructure/plugin/PluginManager.js +26 -8
  144. package/lib/infrastructure/realtime/RealtimeService.js +2 -2
  145. package/lib/infrastructure/vector/Chunker.js +22 -7
  146. package/lib/infrastructure/vector/IndexingPipeline.js +46 -22
  147. package/lib/infrastructure/vector/JsonVectorAdapter.js +90 -53
  148. package/lib/infrastructure/vector/VectorStore.js +28 -10
  149. package/lib/injection/ServiceContainer.js +247 -93
  150. package/lib/platform/ios/index.js +63 -0
  151. package/lib/platform/ios/routes/spm.js +437 -0
  152. package/lib/platform/ios/snippet/PlaceholderConverter.js +55 -0
  153. package/lib/platform/ios/snippet/XcodeCodec.js +112 -0
  154. package/lib/{service → platform/ios}/spm/DependencyGraph.js +41 -17
  155. package/lib/{service → platform/ios}/spm/PackageSwiftParser.js +41 -14
  156. package/lib/{service → platform/ios}/spm/PolicyEngine.js +9 -4
  157. package/lib/platform/ios/spm/SpmDiscoverer.js +122 -0
  158. package/lib/{service → platform/ios}/spm/SpmService.js +385 -127
  159. package/lib/{service/automation → platform/ios/xcode}/SaveEventFilter.js +8 -7
  160. package/lib/platform/ios/xcode/XcodeAutomation.js +350 -0
  161. package/lib/{service/automation → platform/ios/xcode}/XcodeIntegration.js +325 -145
  162. package/lib/repository/base/BaseRepository.js +7 -9
  163. package/lib/repository/knowledge/KnowledgeRepository.impl.js +98 -75
  164. package/lib/repository/token/TokenUsageStore.js +4 -2
  165. package/lib/service/automation/ActionPipeline.js +1 -1
  166. package/lib/service/automation/AutomationOrchestrator.js +8 -4
  167. package/lib/service/automation/ContextCollector.js +7 -5
  168. package/lib/service/automation/DirectiveDetector.js +23 -16
  169. package/lib/service/automation/FileWatcher.js +112 -56
  170. package/lib/service/automation/TriggerResolver.js +6 -4
  171. package/lib/service/automation/handlers/AlinkHandler.js +24 -12
  172. package/lib/service/automation/handlers/CreateHandler.js +19 -20
  173. package/lib/service/automation/handlers/DraftHandler.js +14 -8
  174. package/lib/service/automation/handlers/GuardHandler.js +93 -63
  175. package/lib/service/automation/handlers/HeaderHandler.js +1 -6
  176. package/lib/service/automation/handlers/SearchHandler.js +155 -88
  177. package/lib/service/bootstrap/BootstrapTaskManager.js +77 -35
  178. package/lib/service/candidate/SimilarityService.js +25 -9
  179. package/lib/service/chat/AnalystAgent.js +50 -24
  180. package/lib/service/chat/CandidateGuardrail.js +143 -17
  181. package/lib/service/chat/ChatAgent.js +655 -260
  182. package/lib/service/chat/ContextWindow.js +116 -71
  183. package/lib/service/chat/ConversationStore.js +77 -36
  184. package/lib/service/chat/EpisodicConsolidator.js +47 -23
  185. package/lib/service/chat/HandoffProtocol.js +98 -22
  186. package/lib/service/chat/Memory.js +34 -14
  187. package/lib/service/chat/ProducerAgent.js +40 -20
  188. package/lib/service/chat/ProjectSemanticMemory.js +109 -78
  189. package/lib/service/chat/ReasoningLayer.js +148 -70
  190. package/lib/service/chat/ReasoningTrace.js +44 -32
  191. package/lib/service/chat/TaskPipeline.js +39 -19
  192. package/lib/service/chat/ToolRegistry.js +48 -29
  193. package/lib/service/chat/WorkingMemory.js +44 -18
  194. package/lib/service/chat/tools.js +1096 -494
  195. package/lib/service/context/RecipeExtractor.js +132 -51
  196. package/lib/service/cursor/CursorDeliveryPipeline.js +82 -37
  197. package/lib/service/cursor/KnowledgeCompressor.js +25 -22
  198. package/lib/service/cursor/RulesGenerator.js +13 -7
  199. package/lib/service/cursor/SkillsSyncer.js +77 -27
  200. package/lib/service/cursor/TokenBudget.js +2 -2
  201. package/lib/service/cursor/TopicClassifier.js +54 -20
  202. package/lib/service/guard/ComplianceReporter.js +55 -43
  203. package/lib/service/guard/ExclusionManager.js +67 -29
  204. package/lib/service/guard/GuardCheckEngine.js +381 -86
  205. package/lib/service/guard/GuardFeedbackLoop.js +22 -10
  206. package/lib/service/guard/GuardService.js +29 -19
  207. package/lib/service/guard/RuleLearner.js +55 -23
  208. package/lib/service/guard/SourceFileCollector.js +27 -20
  209. package/lib/service/guard/ViolationsStore.js +43 -38
  210. package/lib/service/knowledge/CodeEntityGraph.js +147 -82
  211. package/lib/service/knowledge/ConfidenceRouter.js +12 -10
  212. package/lib/service/knowledge/KnowledgeFileWriter.js +147 -56
  213. package/lib/service/knowledge/KnowledgeGraphService.js +81 -34
  214. package/lib/service/knowledge/KnowledgeService.js +222 -112
  215. package/lib/service/module/ModuleService.js +969 -0
  216. package/lib/service/quality/FeedbackCollector.js +27 -15
  217. package/lib/service/quality/QualityScorer.js +78 -24
  218. package/lib/service/recipe/RecipeCandidateValidator.js +110 -44
  219. package/lib/service/recipe/RecipeParser.js +78 -45
  220. package/lib/service/search/CoarseRanker.js +43 -28
  221. package/lib/service/search/CrossEncoderReranker.js +32 -21
  222. package/lib/service/search/InvertedIndex.js +21 -7
  223. package/lib/service/search/MultiSignalRanker.js +90 -28
  224. package/lib/service/search/RetrievalFunnel.js +45 -24
  225. package/lib/service/search/SearchEngine.js +255 -103
  226. package/lib/service/skills/EventAggregator.js +32 -15
  227. package/lib/service/skills/SignalCollector.js +140 -64
  228. package/lib/service/skills/SkillAdvisor.js +79 -42
  229. package/lib/service/skills/SkillHooks.js +16 -14
  230. package/lib/service/snippet/PlaceholderConverter.js +5 -0
  231. package/lib/service/snippet/SnippetFactory.js +116 -99
  232. package/lib/service/snippet/SnippetInstaller.js +234 -62
  233. package/lib/service/snippet/codecs/SnippetCodec.js +67 -0
  234. package/lib/service/snippet/codecs/VSCodeCodec.js +102 -0
  235. package/lib/service/snippet/codecs/XcodeCodec.js +5 -0
  236. package/lib/service/wiki/WikiGenerator.js +637 -263
  237. package/lib/shared/DimensionCopyRegistry.js +472 -0
  238. package/lib/shared/LanguageService.js +399 -0
  239. package/lib/shared/PathGuard.js +45 -28
  240. package/lib/shared/RecipeReadinessChecker.js +72 -12
  241. package/lib/shared/constants.js +41 -41
  242. package/lib/shared/errors/BaseError.js +2 -2
  243. package/lib/shared/errors/index.js +4 -4
  244. package/lib/shared/similarity.js +25 -8
  245. package/lib/shared/token-utils.js +6 -2
  246. package/lib/shared/utils/common.js +12 -4
  247. package/package.json +49 -13
  248. package/scripts/bench-real-projects.mjs +256 -0
  249. package/scripts/build-native-ui.js +30 -30
  250. package/scripts/clear-old-vector-index.js +5 -35
  251. package/scripts/clear-vector-cache.js +7 -37
  252. package/scripts/collect-test-project-stats.mjs +160 -0
  253. package/scripts/diagnose-mcp.js +41 -32
  254. package/scripts/ensure-parse-package.js +6 -9
  255. package/scripts/generate-recipe-drafts.js +116 -77
  256. package/scripts/init-db.js +3 -20
  257. package/scripts/init-snippets.js +305 -0
  258. package/scripts/init-vector-db.js +173 -170
  259. package/scripts/install-cursor-skill.js +148 -104
  260. package/scripts/install-full.js +8 -21
  261. package/scripts/install-vscode-copilot.js +146 -145
  262. package/scripts/migrate-md-to-knowledge.mjs +139 -151
  263. package/scripts/postinstall-safe.js +5 -17
  264. package/scripts/recipe-audit.js +106 -82
  265. package/scripts/release.js +283 -323
  266. package/scripts/setup-mcp-config.js +60 -52
  267. package/scripts/verify-context-api.js +20 -20
  268. package/skills/autosnippet-analysis/SKILL.md +10 -6
  269. package/skills/autosnippet-candidates/SKILL.md +27 -26
  270. package/skills/autosnippet-coldstart/SKILL.md +555 -38
  271. package/skills/autosnippet-concepts/SKILL.md +349 -337
  272. package/skills/autosnippet-create/SKILL.md +5 -5
  273. package/skills/autosnippet-reference-dart/SKILL.md +543 -0
  274. package/skills/autosnippet-reference-go/SKILL.md +539 -0
  275. package/skills/autosnippet-reference-java/SKILL.md +534 -0
  276. package/skills/autosnippet-reference-jsts/SKILL.md +41 -9
  277. package/skills/autosnippet-reference-kotlin/SKILL.md +526 -0
  278. package/skills/autosnippet-reference-objc/SKILL.md +29 -6
  279. package/skills/autosnippet-reference-python/SKILL.md +800 -0
  280. package/skills/autosnippet-reference-swift/SKILL.md +70 -14
  281. package/skills/autosnippet-structure/SKILL.md +4 -4
  282. package/templates/cursor-rules/autosnippet-conventions.mdc +2 -2
  283. package/templates/recipes-setup/README.md +2 -2
  284. package/templates/recipes-setup/_template.md +1 -1
  285. package/dashboard/dist/assets/index-Bun3ld_J.css +0 -1
  286. package/dashboard/dist/assets/index-_Sk_Dmg3.js +0 -143
  287. package/resources/asd-entry/main.swift +0 -159
  288. package/scripts/build-asd-entry.js +0 -51
  289. package/scripts/init-xcode-snippets.js +0 -311
  290. package/template.json +0 -39
@@ -55,37 +55,37 @@ export class ContextWindow {
55
55
  */
56
56
  static MODEL_CONTEXT_WINDOWS = [
57
57
  // ── Google Gemini ──
58
- [/gemini-3/i, 1_000_000],
59
- [/gemini-2\.5/i, 1_000_000],
60
- [/gemini-2/i, 1_000_000],
61
- [/gemini-1\.5-pro/i, 1_000_000],
62
- [/gemini-1\.5-flash/i, 1_000_000],
63
- [/gemini-1\.0/i, 32_000],
64
- [/gemini/i, 1_000_000], // 未知版本回退
58
+ [/gemini-3/i, 1_000_000],
59
+ [/gemini-2\.5/i, 1_000_000],
60
+ [/gemini-2/i, 1_000_000],
61
+ [/gemini-1\.5-pro/i, 1_000_000],
62
+ [/gemini-1\.5-flash/i, 1_000_000],
63
+ [/gemini-1\.0/i, 32_000],
64
+ [/gemini/i, 1_000_000], // 未知版本回退
65
65
  // ── OpenAI ──
66
- [/gpt-4o/i, 128_000],
67
- [/gpt-4-turbo/i, 128_000],
68
- [/gpt-4-(?!turbo)/i, 8_192],
69
- [/gpt-3\.5-turbo-16k/i, 16_384],
70
- [/gpt-3\.5/i, 4_096],
71
- [/o1|o3|o4/i, 200_000], // OpenAI reasoning models
66
+ [/gpt-4o/i, 128_000],
67
+ [/gpt-4-turbo/i, 128_000],
68
+ [/gpt-4-(?!turbo)/i, 8_192],
69
+ [/gpt-3\.5-turbo-16k/i, 16_384],
70
+ [/gpt-3\.5/i, 4_096],
71
+ [/o1|o3|o4/i, 200_000], // OpenAI reasoning models
72
72
  // ── Anthropic ──
73
- [/claude-.*sonnet-4/i, 200_000],
74
- [/claude-3[\.\-]5/i, 200_000],
75
- [/claude-3[\.\-]opus/i, 200_000],
76
- [/claude-3/i, 200_000],
77
- [/claude/i, 200_000], // 未知 claude 回退
73
+ [/claude-.*sonnet-4/i, 200_000],
74
+ [/claude-3[.-]5/i, 200_000],
75
+ [/claude-3[.-]opus/i, 200_000],
76
+ [/claude-3/i, 200_000],
77
+ [/claude/i, 200_000], // 未知 claude 回退
78
78
  // ── DeepSeek ──
79
- [/deepseek/i, 64_000],
79
+ [/deepseek/i, 64_000],
80
80
  // ── 本地 Ollama ──
81
- [/llama3[\.\-]?[23]/i, 128_000],
82
- [/llama3/i, 8_192],
83
- [/llama/i, 4_096],
84
- [/mistral/i, 32_000],
85
- [/qwen/i, 128_000],
86
- [/phi/i, 128_000],
81
+ [/llama3[.-]?[23]/i, 128_000],
82
+ [/llama3/i, 8_192],
83
+ [/llama/i, 4_096],
84
+ [/mistral/i, 32_000],
85
+ [/qwen/i, 128_000],
86
+ [/phi/i, 128_000],
87
87
  // ── Mock(测试) ──
88
- [/mock/i, 32_000],
88
+ [/mock/i, 32_000],
89
89
  ];
90
90
 
91
91
  /**
@@ -233,13 +233,15 @@ export class ContextWindow {
233
233
 
234
234
  // 找到最后一个 assistant-with-toolCalls 的位置
235
235
  const lastRoundStart = this.#findLastToolRoundStart();
236
- if (lastRoundStart < 0) return { level: 1, removed: 0 };
236
+ if (lastRoundStart < 0) {
237
+ return { level: 1, removed: 0 };
238
+ }
237
239
 
238
240
  // 只截断 lastRoundStart 之前的 tool results
239
241
  for (let i = 1; i < lastRoundStart; i++) {
240
242
  const msg = this.#messages[i];
241
243
  if (msg.role === 'tool' && msg.content && msg.content.length > TRUNCATE_THRESHOLD) {
242
- msg.content = msg.content.substring(0, TRUNCATE_TO) + `\n... [truncated from ${msg.content.length} chars]`;
244
+ msg.content = `${msg.content.substring(0, TRUNCATE_TO)}\n... [truncated from ${msg.content.length} chars]`;
243
245
  truncated++;
244
246
  }
245
247
  }
@@ -259,10 +261,14 @@ export class ContextWindow {
259
261
  #compactL2() {
260
262
  // 找到倒数第 2 个 tool round 的起始(保留最后 2 轮)
261
263
  const roundStarts = this.#findAllToolRoundStarts();
262
- if (roundStarts.length < 2) return { level: 2, removed: 0 };
264
+ if (roundStarts.length < 2) {
265
+ return { level: 2, removed: 0 };
266
+ }
263
267
 
264
268
  const keepFrom = roundStarts[roundStarts.length - 2]; // 保留从倒数第 2 轮开始
265
- if (keepFrom <= 1) return { level: 2, removed: 0 };
269
+ if (keepFrom <= 1) {
270
+ return { level: 2, removed: 0 };
271
+ }
266
272
 
267
273
  return this.#spliceAndSummarize(keepFrom, 2);
268
274
  }
@@ -310,8 +316,8 @@ export class ContextWindow {
310
316
  }
311
317
 
312
318
  // 计算历史统计
313
- const toolCallCount = removed.filter(m => m.role === 'assistant' && m.toolCalls).length;
314
- const toolResultCount = removed.filter(m => m.role === 'tool').length;
319
+ const toolCallCount = removed.filter((m) => m.role === 'assistant' && m.toolCalls).length;
320
+ const toolResultCount = removed.filter((m) => m.role === 'tool').length;
315
321
 
316
322
  // Splice: 移除 messages[1..keepFrom-1]
317
323
  this.#messages.splice(1, keepFrom - 1);
@@ -330,8 +336,12 @@ export class ContextWindow {
330
336
  });
331
337
 
332
338
  const removedCount = keepFrom - 1;
333
- this.#compactionLog.push(`L${level}: removed ${removedCount} messages (${toolCallCount} rounds)`);
334
- this.#logger.info(`[ContextWindow] L${level} compact: removed ${removedCount} messages, kept last ${level === 2 ? 2 : 1} rounds`);
339
+ this.#compactionLog.push(
340
+ `L${level}: removed ${removedCount} messages (${toolCallCount} rounds)`
341
+ );
342
+ this.#logger.info(
343
+ `[ContextWindow] L${level} compact: removed ${removedCount} messages, kept last ${level === 2 ? 2 : 1} rounds`
344
+ );
335
345
 
336
346
  return { level, removed: removedCount };
337
347
  }
@@ -369,8 +379,12 @@ export class ContextWindow {
369
379
  estimateTokens() {
370
380
  let total = 0;
371
381
  for (const m of this.#messages) {
372
- if (m.content) total += estimateTokensFast(m.content);
373
- if (m.toolCalls) total += estimateTokensFast(JSON.stringify(m.toolCalls));
382
+ if (m.content) {
383
+ total += estimateTokensFast(m.content);
384
+ }
385
+ if (m.toolCalls) {
386
+ total += estimateTokensFast(JSON.stringify(m.toolCalls));
387
+ }
374
388
  }
375
389
  return total;
376
390
  }
@@ -390,9 +404,15 @@ export class ContextWindow {
390
404
  */
391
405
  getToolResultQuota() {
392
406
  const usage = this.getTokenUsageRatio();
393
- if (usage < 0.4) return { maxChars: 6000, maxMatches: 15 };
394
- if (usage < 0.6) return { maxChars: 3000, maxMatches: 8 };
395
- if (usage < 0.8) return { maxChars: 1500, maxMatches: 5 };
407
+ if (usage < 0.4) {
408
+ return { maxChars: 6000, maxMatches: 15 };
409
+ }
410
+ if (usage < 0.6) {
411
+ return { maxChars: 3000, maxMatches: 8 };
412
+ }
413
+ if (usage < 0.8) {
414
+ return { maxChars: 1500, maxMatches: 5 };
415
+ }
396
416
  return { maxChars: 800, maxMatches: 3 };
397
417
  }
398
418
 
@@ -493,7 +513,7 @@ export function limitToolResult(toolName, result, quota) {
493
513
  limited.batchResults[key] = limitSearchResultObj(sub, Math.min(maxMatches, 3), perKeyChars);
494
514
  }
495
515
  const raw = JSON.stringify(limited);
496
- return raw.length > maxChars ? raw.substring(0, maxChars) + '\n... [batch truncated]' : raw;
516
+ return raw.length > maxChars ? `${raw.substring(0, maxChars)}\n... [batch truncated]` : raw;
497
517
  }
498
518
  return limitSearchResult(result, maxMatches, maxChars);
499
519
  }
@@ -502,7 +522,7 @@ export function limitToolResult(toolName, result, quota) {
502
522
  if (toolName === 'read_project_file') {
503
523
  if (result && typeof result === 'object' && result.batchResults) {
504
524
  const raw = JSON.stringify(result);
505
- return raw.length > maxChars ? raw.substring(0, maxChars) + '\n... [batch truncated]' : raw;
525
+ return raw.length > maxChars ? `${raw.substring(0, maxChars)}\n... [batch truncated]` : raw;
506
526
  }
507
527
  return limitFileContent(result, maxChars);
508
528
  }
@@ -510,7 +530,7 @@ export function limitToolResult(toolName, result, quota) {
510
530
  // 通用: 按字符限制
511
531
  const raw = typeof result === 'string' ? result : JSON.stringify(result);
512
532
  if (raw.length > maxChars) {
513
- return raw.substring(0, maxChars) + `\n... [truncated, ${raw.length} total chars]`;
533
+ return `${raw.substring(0, maxChars)}\n... [truncated, ${raw.length} total chars]`;
514
534
  }
515
535
  return raw;
516
536
  }
@@ -523,9 +543,7 @@ export function limitToolResult(toolName, result, quota) {
523
543
  */
524
544
  function limitSearchResult(result, maxMatches, maxChars) {
525
545
  if (typeof result === 'string') {
526
- return result.length > maxChars
527
- ? result.substring(0, maxChars) + '\n... [truncated]'
528
- : result;
546
+ return result.length > maxChars ? `${result.substring(0, maxChars)}\n... [truncated]` : result;
529
547
  }
530
548
 
531
549
  if (!result || typeof result !== 'object') {
@@ -535,13 +553,13 @@ function limitSearchResult(result, maxMatches, maxChars) {
535
553
  // 深拷贝避免修改原对象
536
554
  const limited = { ...result };
537
555
  if (Array.isArray(limited.matches)) {
538
- limited.matches = limited.matches.slice(0, maxMatches).map(m => {
556
+ limited.matches = limited.matches.slice(0, maxMatches).map((m) => {
539
557
  const copy = { ...m };
540
558
  // 截断每个匹配的 context 字段(多行文本)
541
559
  if (copy.context && typeof copy.context === 'string') {
542
560
  const contextLines = copy.context.split('\n');
543
561
  if (contextLines.length > 7) {
544
- copy.context = contextLines.slice(0, 7).join('\n') + '\n... [truncated]';
562
+ copy.context = `${contextLines.slice(0, 7).join('\n')}\n... [truncated]`;
545
563
  }
546
564
  }
547
565
  // 兼容旧格式: 也处理 lines 数组
@@ -558,7 +576,7 @@ function limitSearchResult(result, maxMatches, maxChars) {
558
576
 
559
577
  const str = JSON.stringify(limited);
560
578
  if (str.length > maxChars) {
561
- return str.substring(0, maxChars) + '\n... [truncated]';
579
+ return `${str.substring(0, maxChars)}\n... [truncated]`;
562
580
  }
563
581
  return str;
564
582
  }
@@ -568,21 +586,25 @@ function limitSearchResult(result, maxMatches, maxChars) {
568
586
  * 当源码含控制字符时,stringify→substring 截断会破坏 JSON 结构导致 parse 失败
569
587
  */
570
588
  function limitSearchResultObj(result, maxMatches, maxChars) {
571
- if (!result || typeof result !== 'object') return result || {};
572
- if (typeof result === 'string') return { _raw: result.substring(0, maxChars) };
589
+ if (!result || typeof result !== 'object') {
590
+ return result || {};
591
+ }
592
+ if (typeof result === 'string') {
593
+ return { _raw: result.substring(0, maxChars) };
594
+ }
573
595
 
574
596
  const limited = { ...result };
575
597
  if (Array.isArray(limited.matches)) {
576
- limited.matches = limited.matches.slice(0, maxMatches).map(m => {
598
+ limited.matches = limited.matches.slice(0, maxMatches).map((m) => {
577
599
  const copy = { ...m };
578
600
  if (copy.context && typeof copy.context === 'string') {
579
601
  const contextLines = copy.context.split('\n');
580
602
  if (contextLines.length > 7) {
581
- copy.context = contextLines.slice(0, 7).join('\n') + '\n... [truncated]';
603
+ copy.context = `${contextLines.slice(0, 7).join('\n')}\n... [truncated]`;
582
604
  }
583
605
  // 按字符上限截断 context(防止单个代码块过大)
584
606
  if (copy.context.length > 500) {
585
- copy.context = copy.context.substring(0, 500) + '\n... [truncated]';
607
+ copy.context = `${copy.context.substring(0, 500)}\n... [truncated]`;
586
608
  }
587
609
  }
588
610
  if (Array.isArray(copy.lines) && copy.lines.length > 5) {
@@ -603,9 +625,7 @@ function limitSearchResultObj(result, maxMatches, maxChars) {
603
625
  */
604
626
  function limitFileContent(result, maxChars) {
605
627
  if (typeof result === 'string') {
606
- return result.length > maxChars
607
- ? result.substring(0, maxChars) + '\n... [truncated]'
608
- : result;
628
+ return result.length > maxChars ? `${result.substring(0, maxChars)}\n... [truncated]` : result;
609
629
  }
610
630
 
611
631
  if (!result || typeof result !== 'object') {
@@ -617,10 +637,12 @@ function limitFileContent(result, maxChars) {
617
637
  const lines = limited.content.split('\n');
618
638
  let truncated = '';
619
639
  for (const line of lines) {
620
- if (truncated.length + line.length + 1 > maxChars) break;
621
- truncated += line + '\n';
640
+ if (truncated.length + line.length + 1 > maxChars) {
641
+ break;
642
+ }
643
+ truncated += `${line}\n`;
622
644
  }
623
- limited.content = truncated + `... [truncated at ${maxChars} chars, total ${result.content.length}]`;
645
+ limited.content = `${truncated}... [truncated at ${maxChars} chars, total ${result.content.length}]`;
624
646
  }
625
647
 
626
648
  return JSON.stringify(limited);
@@ -718,7 +740,9 @@ export class PhaseRouter {
718
740
  switch (this.#phase) {
719
741
  case 'EXPLORE':
720
742
  // 最后一轮 EXPLORE 用 auto 保底
721
- if (this.#phaseRounds >= this.#budget.searchBudget - 1) return 'auto';
743
+ if (this.#phaseRounds >= this.#budget.searchBudget - 1) {
744
+ return 'auto';
745
+ }
722
746
  return 'required';
723
747
  case 'PRODUCE':
724
748
  return 'auto';
@@ -735,11 +759,15 @@ export class PhaseRouter {
735
759
  */
736
760
  shouldExit() {
737
761
  // 已在 SUMMARIZE 阶段 → 给 2 轮输出总结后退出
738
- if (this.#phase === 'SUMMARIZE' && this.#phaseRounds >= 2) return true;
762
+ if (this.#phase === 'SUMMARIZE' && this.#phaseRounds >= 2) {
763
+ return true;
764
+ }
739
765
 
740
766
  // 达到 maxIterations → 不硬退出,而是强制转入 SUMMARIZE 让 AI 在完整上下文中收尾
741
767
  if (this.#totalIterations >= this.#budget.maxIterations && this.#phase !== 'SUMMARIZE') {
742
- this.#logger.info(`[PhaseRouter] maxIterations reached (${this.#totalIterations}/${this.#budget.maxIterations}), forcing → SUMMARIZE for graceful exit`);
768
+ this.#logger.info(
769
+ `[PhaseRouter] maxIterations reached (${this.#totalIterations}/${this.#budget.maxIterations}), forcing → SUMMARIZE for graceful exit`
770
+ );
743
771
  this.#forcedSummarize = true;
744
772
  this.#transitionTo('SUMMARIZE');
745
773
  // 返回 false 让主循环继续运行 SUMMARIZE 阶段的 2 轮收尾
@@ -747,7 +775,9 @@ export class PhaseRouter {
747
775
  }
748
776
 
749
777
  // SUMMARIZE 阶段超限兜底(maxIterations + 2 轮 grace)
750
- if (this.#totalIterations >= this.#budget.maxIterations + 2) return true;
778
+ if (this.#totalIterations >= this.#budget.maxIterations + 2) {
779
+ return true;
780
+ }
751
781
 
752
782
  return false;
753
783
  }
@@ -770,7 +800,7 @@ export class PhaseRouter {
770
800
  * @returns {{ transitioned: boolean, newPhase: string, exitReason?: string }}
771
801
  */
772
802
  update(roundResult) {
773
- const { functionCalls, submitCount = 0, isTextOnly = false } = roundResult;
803
+ const { submitCount = 0, isTextOnly = false } = roundResult;
774
804
  this.#totalSubmits += submitCount;
775
805
 
776
806
  // ── EXPLORE → PRODUCE/SUMMARIZE ──
@@ -783,7 +813,9 @@ export class PhaseRouter {
783
813
  // 搜索轮次耗尽 → 转换
784
814
  if (this.#phaseRounds >= this.#budget.searchBudget) {
785
815
  const next = this.#isSkillOnly ? 'SUMMARIZE' : 'PRODUCE';
786
- this.#logger.info(`[PhaseRouter] search budget exhausted (${this.#phaseRounds}/${this.#budget.searchBudget}) → ${next}`);
816
+ this.#logger.info(
817
+ `[PhaseRouter] search budget exhausted (${this.#phaseRounds}/${this.#budget.searchBudget}) → ${next}`
818
+ );
787
819
  return this.#transitionTo(next);
788
820
  }
789
821
 
@@ -806,7 +838,9 @@ export class PhaseRouter {
806
838
 
807
839
  // 硬上限
808
840
  if (this.#budget.maxSubmits > 0 && this.#totalSubmits >= this.#budget.maxSubmits) {
809
- this.#logger.info(`[PhaseRouter] hard submit cap reached (${this.#totalSubmits}/${this.#budget.maxSubmits}) → SUMMARIZE`);
841
+ this.#logger.info(
842
+ `[PhaseRouter] hard submit cap reached (${this.#totalSubmits}/${this.#budget.maxSubmits}) → SUMMARIZE`
843
+ );
810
844
  return this.#transitionTo('SUMMARIZE');
811
845
  }
812
846
 
@@ -823,18 +857,24 @@ export class PhaseRouter {
823
857
  // 否则视为 AI 的中间分析,不立即退出
824
858
  if (isTextOnly) {
825
859
  if (this.#totalSubmits >= this.#budget.softSubmitLimit) {
826
- this.#logger.info(`[PhaseRouter] text reply after ${this.#totalSubmits} submits (≥ softLimit=${this.#budget.softSubmitLimit}) → SUMMARIZE`);
860
+ this.#logger.info(
861
+ `[PhaseRouter] text reply after ${this.#totalSubmits} submits (≥ softLimit=${this.#budget.softSubmitLimit}) → SUMMARIZE`
862
+ );
827
863
  return this.#transitionTo('SUMMARIZE');
828
864
  }
829
865
  // 未达 softLimit — 空转已在上方通用分支计数,不再额外递增
830
- this.#logger.info(`[PhaseRouter] text reply in PRODUCE — idleRounds=${this.#idleRounds}, totalSubmits=${this.#totalSubmits}, continuing`);
866
+ this.#logger.info(
867
+ `[PhaseRouter] text reply in PRODUCE — idleRounds=${this.#idleRounds}, totalSubmits=${this.#totalSubmits}, continuing`
868
+ );
831
869
  return { transitioned: false, newPhase: this.#phase };
832
870
  }
833
871
 
834
872
  // PRODUCE 阶段容忍期耗尽且 0 提交 → 强制进入 SUMMARIZE
835
873
  // 使用 PRODUCE 阶段内的轮次计数,而非 totalIterations
836
874
  if (this.#phaseRounds >= this.#budget.searchBudgetGrace && this.#totalSubmits === 0) {
837
- this.#logger.info(`[PhaseRouter] PRODUCE grace exhausted (${this.#phaseRounds}/${this.#budget.searchBudgetGrace} rounds, 0 submits) → SUMMARIZE`);
875
+ this.#logger.info(
876
+ `[PhaseRouter] PRODUCE grace exhausted (${this.#phaseRounds}/${this.#budget.searchBudgetGrace} rounds, 0 submits) → SUMMARIZE`
877
+ );
838
878
  return this.#transitionTo('SUMMARIZE');
839
879
  }
840
880
 
@@ -869,7 +909,10 @@ export class PhaseRouter {
869
909
  ? '你已收集足够信息,请在回复中直接输出 dimensionDigest JSON。'
870
910
  : '⚠️ 探索阶段已结束。你已收集了足够的项目信息,请 **立即** 调用 submit_knowledge 提交候选。不要继续搜索,直接提交。';
871
911
  }
872
- if (this.#totalSubmits >= this.#budget.softSubmitLimit && this.#budget.softSubmitLimit > 0) {
912
+ if (
913
+ this.#totalSubmits >= this.#budget.softSubmitLimit &&
914
+ this.#budget.softSubmitLimit > 0
915
+ ) {
873
916
  const remaining = this.#budget.maxSubmits - this.#totalSubmits;
874
917
  return `已提交 ${this.#totalSubmits} 个候选(上限 ${this.#budget.maxSubmits})。${remaining > 0 ? `还可提交 ${remaining} 个。` : ''}如果还有值得记录的发现可以继续提交,否则请产出 dimensionDigest 总结。\n⚠️ 如果还有未处理的信号,请在 dimensionDigest 的 remainingTasks 字段中标记,下次运行时会续传。`;
875
918
  }
@@ -892,7 +935,9 @@ export class PhaseRouter {
892
935
  this.#phase = newPhase;
893
936
  this.#phaseRounds = 0;
894
937
  this.#idleRounds = 0;
895
- this.#logger.info(`[PhaseRouter] ${oldPhase} → ${newPhase} (iter=${this.#totalIterations}, submits=${this.#totalSubmits})`);
938
+ this.#logger.info(
939
+ `[PhaseRouter] ${oldPhase} → ${newPhase} (iter=${this.#totalIterations}, submits=${this.#totalSubmits})`
940
+ );
896
941
  return { transitioned: true, newPhase };
897
942
  }
898
943
  }
@@ -18,16 +18,16 @@
18
18
  * {id}.jsonl — 每行一条消息 {role, content, ts}
19
19
  */
20
20
 
21
+ import crypto from 'node:crypto';
21
22
  import fs from 'node:fs';
22
23
  import path from 'node:path';
23
- import crypto from 'node:crypto';
24
24
  import Logger from '../../infrastructure/logging/Logger.js';
25
25
  import pathGuard from '../../shared/PathGuard.js';
26
26
  import { estimateTokens as _estimateTokens } from '../../shared/token-utils.js';
27
27
 
28
- const DEFAULT_TOKEN_BUDGET = 12000; // ~12K tokens 留给历史, 其余给系统提示词和当前消息
29
- const MAX_CONVERSATIONS = 100; // 索引最多保留 100 个对话
30
- const SUMMARY_TARGET_TOKENS = 500; // 压缩后的摘要目标 token 数
28
+ const DEFAULT_TOKEN_BUDGET = 12000; // ~12K tokens 留给历史, 其余给系统提示词和当前消息
29
+ const MAX_CONVERSATIONS = 100; // 索引最多保留 100 个对话
30
+ const _SUMMARY_TARGET_TOKENS = 500; // 压缩后的摘要目标 token 数
31
31
 
32
32
  export class ConversationStore {
33
33
  #dir;
@@ -98,11 +98,11 @@ export class ConversationStore {
98
98
  content: message.content,
99
99
  ts: new Date().toISOString(),
100
100
  });
101
- fs.appendFileSync(filePath, line + '\n', 'utf-8');
101
+ fs.appendFileSync(filePath, `${line}\n`, 'utf-8');
102
102
 
103
103
  // 更新索引
104
104
  const index = this.#loadIndex();
105
- const entry = index.find(e => e.id === conversationId);
105
+ const entry = index.find((e) => e.id === conversationId);
106
106
  if (entry) {
107
107
  entry.updatedAt = new Date().toISOString();
108
108
  entry.messageCount = (entry.messageCount || 0) + 1;
@@ -133,18 +133,25 @@ export class ConversationStore {
133
133
  load(conversationId, { tokenBudget = DEFAULT_TOKEN_BUDGET } = {}) {
134
134
  try {
135
135
  const filePath = this.#conversationPath(conversationId);
136
- if (!fs.existsSync(filePath)) return [];
136
+ if (!fs.existsSync(filePath)) {
137
+ return [];
138
+ }
137
139
 
138
140
  const raw = fs.readFileSync(filePath, 'utf-8').trim();
139
- if (!raw) return [];
141
+ if (!raw) {
142
+ return [];
143
+ }
140
144
 
141
- const messages = raw.split('\n')
145
+ const messages = raw
146
+ .split('\n')
142
147
  .filter(Boolean)
143
- .map(line => {
148
+ .map((line) => {
144
149
  try {
145
150
  const parsed = JSON.parse(line);
146
151
  return { role: parsed.role, content: parsed.content };
147
- } catch { return null; }
152
+ } catch {
153
+ return null;
154
+ }
148
155
  })
149
156
  .filter(Boolean);
150
157
 
@@ -165,7 +172,7 @@ export class ConversationStore {
165
172
  const index = this.#loadIndex();
166
173
  let results = index;
167
174
  if (category) {
168
- results = results.filter(e => e.category === category);
175
+ results = results.filter((e) => e.category === category);
169
176
  }
170
177
  return results.slice(0, limit);
171
178
  }
@@ -177,7 +184,7 @@ export class ConversationStore {
177
184
  delete(conversationId) {
178
185
  this.#deleteConversationFile(conversationId);
179
186
  const index = this.#loadIndex();
180
- const filtered = index.filter(e => e.id !== conversationId);
187
+ const filtered = index.filter((e) => e.id !== conversationId);
181
188
  this.#saveIndex(filtered);
182
189
  }
183
190
 
@@ -191,36 +198,54 @@ export class ConversationStore {
191
198
  * @returns {Promise<boolean>} 是否成功压缩
192
199
  */
193
200
  async summarize(conversationId, { aiProvider }) {
194
- if (!aiProvider) return false;
201
+ if (!aiProvider) {
202
+ return false;
203
+ }
195
204
 
196
205
  try {
197
206
  const filePath = this.#conversationPath(conversationId);
198
- if (!fs.existsSync(filePath)) return false;
207
+ if (!fs.existsSync(filePath)) {
208
+ return false;
209
+ }
199
210
 
200
211
  const raw = fs.readFileSync(filePath, 'utf-8').trim();
201
- if (!raw) return false;
212
+ if (!raw) {
213
+ return false;
214
+ }
202
215
 
203
- const messages = raw.split('\n')
216
+ const messages = raw
217
+ .split('\n')
204
218
  .filter(Boolean)
205
- .map(line => { try { return JSON.parse(line); } catch { return null; } })
219
+ .map((line) => {
220
+ try {
221
+ return JSON.parse(line);
222
+ } catch {
223
+ return null;
224
+ }
225
+ })
206
226
  .filter(Boolean);
207
227
 
208
- if (messages.length < 6) return false; // 太短不需要压缩
228
+ if (messages.length < 6) {
229
+ return false; // 太短不需要压缩
230
+ }
209
231
 
210
232
  // 保留最近 4 条消息,压缩其余
211
233
  const toSummarize = messages.slice(0, -4);
212
234
  const toKeep = messages.slice(-4);
213
235
 
214
- const summaryPrompt = `请用 2-3 句话总结以下对话的要点(保留关键决策、用户偏好、操作结果):\n\n${
215
- toSummarize.map(m => `[${m.role}] ${m.content}`).join('\n').substring(0, 4000)
216
- }`;
236
+ const summaryPrompt = `请用 2-3 句话总结以下对话的要点(保留关键决策、用户偏好、操作结果):\n\n${toSummarize
237
+ .map((m) => `[${m.role}] ${m.content}`)
238
+ .join('\n')
239
+ .substring(0, 4000)}`;
217
240
 
218
241
  const summary = await aiProvider.chat(summaryPrompt, {
219
242
  temperature: 0.3,
220
243
  maxTokens: 300,
221
244
  });
222
245
 
223
- if (!summary) return false;
246
+ if (!summary) {
247
+ return false;
248
+ }
224
249
 
225
250
  // 重写对话文件: 摘要 + 最近消息
226
251
  const newMessages = [
@@ -230,20 +255,22 @@ export class ConversationStore {
230
255
 
231
256
  fs.writeFileSync(
232
257
  filePath,
233
- newMessages.map(m => JSON.stringify(m)).join('\n') + '\n',
234
- 'utf-8',
258
+ `${newMessages.map((m) => JSON.stringify(m)).join('\n')}\n`,
259
+ 'utf-8'
235
260
  );
236
261
 
237
262
  // 更新索引
238
263
  const index = this.#loadIndex();
239
- const entry = index.find(e => e.id === conversationId);
264
+ const entry = index.find((e) => e.id === conversationId);
240
265
  if (entry) {
241
266
  entry.hasSummary = true;
242
267
  entry.messageCount = newMessages.length;
243
268
  this.#saveIndex(index);
244
269
  }
245
270
 
246
- this.#logger.info(`[ConversationStore] summarized conversation ${conversationId}: ${messages.length} → ${newMessages.length} messages`);
271
+ this.#logger.info(
272
+ `[ConversationStore] summarized conversation ${conversationId}: ${messages.length} → ${newMessages.length} messages`
273
+ );
247
274
  return true;
248
275
  } catch (err) {
249
276
  this.#logger.warn(`[ConversationStore] summarize failed: ${err.message}`);
@@ -263,8 +290,10 @@ export class ConversationStore {
263
290
  const cutoff = Date.now() - maxAgeDays * 86400000;
264
291
  let deleted = 0;
265
292
 
266
- const kept = index.filter(entry => {
267
- if (category && entry.category !== category) return true;
293
+ const kept = index.filter((entry) => {
294
+ if (category && entry.category !== category) {
295
+ return true;
296
+ }
268
297
  const updatedAt = new Date(entry.updatedAt).getTime();
269
298
  if (updatedAt < cutoff) {
270
299
  this.#deleteConversationFile(entry.id);
@@ -300,17 +329,21 @@ export class ConversationStore {
300
329
  * 策略: 保留首条摘要(如有) + 最新消息,丢弃中间旧消息
301
330
  */
302
331
  #fitWithinBudget(messages, tokenBudget) {
303
- if (messages.length === 0) return [];
332
+ if (messages.length === 0) {
333
+ return [];
334
+ }
304
335
 
305
336
  // 计算总 token
306
337
  let totalTokens = 0;
307
- const tokenCounts = messages.map(m => {
338
+ const tokenCounts = messages.map((m) => {
308
339
  const tokens = this.estimateTokens(m.content);
309
340
  totalTokens += tokens;
310
341
  return tokens;
311
342
  });
312
343
 
313
- if (totalTokens <= tokenBudget) return messages;
344
+ if (totalTokens <= tokenBudget) {
345
+ return messages;
346
+ }
314
347
 
315
348
  // 超预算 — 保留首条(摘要) + 从末尾往前取
316
349
  const result = [];
@@ -325,7 +358,9 @@ export class ConversationStore {
325
358
  // 从末尾往前填充
326
359
  const tail = [];
327
360
  for (let i = messages.length - 1; i >= (result.length > 0 ? 1 : 0); i--) {
328
- if (used + tokenCounts[i] > tokenBudget) break;
361
+ if (used + tokenCounts[i] > tokenBudget) {
362
+ break;
363
+ }
329
364
  tail.unshift(messages[i]);
330
365
  used += tokenCounts[i];
331
366
  }
@@ -353,8 +388,12 @@ export class ConversationStore {
353
388
  #deleteConversationFile(id) {
354
389
  try {
355
390
  const filePath = this.#conversationPath(id);
356
- if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
357
- } catch { /* ignore */ }
391
+ if (fs.existsSync(filePath)) {
392
+ fs.unlinkSync(filePath);
393
+ }
394
+ } catch {
395
+ /* ignore */
396
+ }
358
397
  }
359
398
 
360
399
  #loadIndex() {
@@ -362,7 +401,9 @@ export class ConversationStore {
362
401
  if (fs.existsSync(this.#indexPath)) {
363
402
  return JSON.parse(fs.readFileSync(this.#indexPath, 'utf-8'));
364
403
  }
365
- } catch { /* corrupt — reset */ }
404
+ } catch {
405
+ /* corrupt — reset */
406
+ }
366
407
  return [];
367
408
  }
368
409