novacode-cli 1.2.0__py3-none-any.whl

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 (691) hide show
  1. novacode_cli/__init__.py +29 -0
  2. novacode_cli/__main__.py +6 -0
  3. novacode_cli/_lazy_heavy.py +99 -0
  4. novacode_cli/_version.py +3 -0
  5. novacode_cli/agent_stream.py +178 -0
  6. novacode_cli/agents/agent_file.py +250 -0
  7. novacode_cli/agents/agent_mailbox.py +120 -0
  8. novacode_cli/agents/approval_decisions.py +146 -0
  9. novacode_cli/agents/arg_repair.py +196 -0
  10. novacode_cli/agents/async_agents/__init__.py +6 -0
  11. novacode_cli/agents/async_agents/_config.py +213 -0
  12. novacode_cli/agents/async_agents/_mcp_tools.py +192 -0
  13. novacode_cli/agents/async_agents/_specialist.py +84 -0
  14. novacode_cli/agents/async_agents/_test_commands.py +89 -0
  15. novacode_cli/agents/async_agents/_tools.py +432 -0
  16. novacode_cli/agents/async_agents/general_purpose.py +42 -0
  17. novacode_cli/agents/async_agents/langgraph.json +9 -0
  18. novacode_cli/agents/async_agents/research.py +22 -0
  19. novacode_cli/agents/async_context.py +303 -0
  20. novacode_cli/agents/async_on_demand.py +52 -0
  21. novacode_cli/agents/async_task_errors.py +75 -0
  22. novacode_cli/agents/async_workspace.py +27 -0
  23. novacode_cli/agents/attribution.py +165 -0
  24. novacode_cli/agents/browser_mode.py +119 -0
  25. novacode_cli/agents/btw_agent.py +71 -0
  26. novacode_cli/agents/caveman.py +352 -0
  27. novacode_cli/agents/caveman_support.py +370 -0
  28. novacode_cli/agents/code_mode.py +774 -0
  29. novacode_cli/agents/code_runtime.py +160 -0
  30. novacode_cli/agents/context_compaction.py +1339 -0
  31. novacode_cli/agents/core_agent.py +2710 -0
  32. novacode_cli/agents/default_subagents/__init__.py +24 -0
  33. novacode_cli/agents/default_subagents/async_subagents.py +296 -0
  34. novacode_cli/agents/default_subagents/builtin/browser-automation-agent.md +110 -0
  35. novacode_cli/agents/default_subagents/builtin/bug-fix-agent.md +22 -0
  36. novacode_cli/agents/default_subagents/builtin/bug-investigation-agent.md +42 -0
  37. novacode_cli/agents/default_subagents/builtin/code-explorer.md +37 -0
  38. novacode_cli/agents/default_subagents/builtin/code-review-agent.md +33 -0
  39. novacode_cli/agents/default_subagents/builtin/dependency-audit-agent.md +34 -0
  40. novacode_cli/agents/default_subagents/builtin/documentation-update-agent.md +46 -0
  41. novacode_cli/agents/default_subagents/builtin/performance-audit-agent.md +41 -0
  42. novacode_cli/agents/default_subagents/builtin/plan-scout-agent.md +64 -0
  43. novacode_cli/agents/default_subagents/builtin/refactoring-agent.md +35 -0
  44. novacode_cli/agents/default_subagents/builtin/refactoring-specialist-agent.md +175 -0
  45. novacode_cli/agents/default_subagents/builtin/security-audit-agent.md +96 -0
  46. novacode_cli/agents/default_subagents/builtin/test-generation-agent.md +33 -0
  47. novacode_cli/agents/default_subagents/builtin/test-runner-agent.md +55 -0
  48. novacode_cli/agents/default_subagents/prompt.py +88 -0
  49. novacode_cli/agents/default_subagents/subagents.py +138 -0
  50. novacode_cli/agents/dream_agent.py +93 -0
  51. novacode_cli/agents/edit_diagnostics.py +362 -0
  52. novacode_cli/agents/edit_fallback.py +62 -0
  53. novacode_cli/agents/handoff.py +166 -0
  54. novacode_cli/agents/invocation.py +152 -0
  55. novacode_cli/agents/model_budget.py +42 -0
  56. novacode_cli/agents/model_retry.py +113 -0
  57. novacode_cli/agents/model_router.py +420 -0
  58. novacode_cli/agents/openai_decisions.py +186 -0
  59. novacode_cli/agents/plan_agent/__init__.py +12 -0
  60. novacode_cli/agents/plan_agent/plan_agent.py +357 -0
  61. novacode_cli/agents/plan_agent/plan_mode_middleware.py +64 -0
  62. novacode_cli/agents/plan_execution.py +237 -0
  63. novacode_cli/agents/ponytail.py +66 -0
  64. novacode_cli/agents/prime.py +274 -0
  65. novacode_cli/agents/provider_caching.py +99 -0
  66. novacode_cli/agents/server_launcher.py +904 -0
  67. novacode_cli/agents/strict_tools.py +384 -0
  68. novacode_cli/agents/task_discipline.py +294 -0
  69. novacode_cli/agents/tool_call_repair.py +290 -0
  70. novacode_cli/agents/tool_offload.py +291 -0
  71. novacode_cli/agents/tool_search.py +806 -0
  72. novacode_cli/agents/tool_search_variants.py +106 -0
  73. novacode_cli/agents/tool_verdicts.py +1194 -0
  74. novacode_cli/agents/user_async_agents.py +312 -0
  75. novacode_cli/artifacts/__init__.py +10 -0
  76. novacode_cli/artifacts/registry.py +278 -0
  77. novacode_cli/artifacts/server.py +426 -0
  78. novacode_cli/assets/Nova.png +0 -0
  79. novacode_cli/assets/nova-avatar.png +0 -0
  80. novacode_cli/audio/__init__.py +301 -0
  81. novacode_cli/audio/capture.py +137 -0
  82. novacode_cli/audio/downloads.py +273 -0
  83. novacode_cli/audio/pipeline.py +391 -0
  84. novacode_cli/audio/playback.py +87 -0
  85. novacode_cli/audio/providers.py +208 -0
  86. novacode_cli/audio/speakable.py +78 -0
  87. novacode_cli/audio/stt.py +132 -0
  88. novacode_cli/audio/stt_deepgram.py +69 -0
  89. novacode_cli/audio/stt_elevenlabs.py +89 -0
  90. novacode_cli/audio/stt_parakeet.py +94 -0
  91. novacode_cli/audio/stt_whistle.py +61 -0
  92. novacode_cli/audio/summarize.py +92 -0
  93. novacode_cli/audio/tts.py +85 -0
  94. novacode_cli/audio/tts_kokoro.py +73 -0
  95. novacode_cli/audio/tts_orpheus.py +180 -0
  96. novacode_cli/audio/tts_pocket.py +131 -0
  97. novacode_cli/audio/tts_sano.py +69 -0
  98. novacode_cli/audio/vad.py +145 -0
  99. novacode_cli/backends/__init__.py +17 -0
  100. novacode_cli/backends/filesystem.py +894 -0
  101. novacode_cli/backends/native_search.py +92 -0
  102. novacode_cli/bootstrap/__init__.py +9 -0
  103. novacode_cli/bootstrap/env_snapshot.py +240 -0
  104. novacode_cli/bootstrap/steering.py +464 -0
  105. novacode_cli/bootstrap/vision_router.py +604 -0
  106. novacode_cli/brand.py +362 -0
  107. novacode_cli/cli_args.py +718 -0
  108. novacode_cli/commands/__init__.py +237 -0
  109. novacode_cli/commands/agents_commands.py +231 -0
  110. novacode_cli/commands/auth_handler.py +40 -0
  111. novacode_cli/commands/browser_use_handler.py +75 -0
  112. novacode_cli/commands/cache_handler.py +32 -0
  113. novacode_cli/commands/chat_handler.py +1357 -0
  114. novacode_cli/commands/ci_fix.py +171 -0
  115. novacode_cli/commands/claude_plan.py +331 -0
  116. novacode_cli/commands/codex_plan.py +195 -0
  117. novacode_cli/commands/commands.py +1113 -0
  118. novacode_cli/commands/create_handler.py +124 -0
  119. novacode_cli/commands/create_server.py +1887 -0
  120. novacode_cli/commands/cron_handler.py +119 -0
  121. novacode_cli/commands/dream_handler.py +203 -0
  122. novacode_cli/commands/effort_handler.py +65 -0
  123. novacode_cli/commands/evolution_handler.py +214 -0
  124. novacode_cli/commands/file_commands.py +381 -0
  125. novacode_cli/commands/handoff_handler.py +68 -0
  126. novacode_cli/commands/hooks_handler.py +269 -0
  127. novacode_cli/commands/init_handler.py +146 -0
  128. novacode_cli/commands/learning_handler.py +160 -0
  129. novacode_cli/commands/lite_mode_handler.py +76 -0
  130. novacode_cli/commands/log_commands.py +363 -0
  131. novacode_cli/commands/mcp_handler.py +29 -0
  132. novacode_cli/commands/model_handler.py +29 -0
  133. novacode_cli/commands/notifications_handler.py +158 -0
  134. novacode_cli/commands/onboard_handler.py +121 -0
  135. novacode_cli/commands/plan_handler.py +440 -0
  136. novacode_cli/commands/planner_usage.py +250 -0
  137. novacode_cli/commands/plugin_install_handler.py +38 -0
  138. novacode_cli/commands/plugins_handler.py +35 -0
  139. novacode_cli/commands/prime_handler.py +455 -0
  140. novacode_cli/commands/prompt_handler.py +149 -0
  141. novacode_cli/commands/provider_usage.py +160 -0
  142. novacode_cli/commands/ralph_events.py +106 -0
  143. novacode_cli/commands/ralph_handler.py +1293 -0
  144. novacode_cli/commands/refine_handler.py +213 -0
  145. novacode_cli/commands/research_handler.py +202 -0
  146. novacode_cli/commands/rootapp_remote.py +83 -0
  147. novacode_cli/commands/router_handler.py +36 -0
  148. novacode_cli/commands/server_commands.py +249 -0
  149. novacode_cli/commands/session_commands.py +203 -0
  150. novacode_cli/commands/share_handler.py +450 -0
  151. novacode_cli/commands/side_commands.py +366 -0
  152. novacode_cli/commands/skill_invoke.py +420 -0
  153. novacode_cli/commands/skills_commands.py +48 -0
  154. novacode_cli/commands/trace_handler.py +322 -0
  155. novacode_cli/commands/trello_handler.py +226 -0
  156. novacode_cli/commands/trello_remote.py +177 -0
  157. novacode_cli/commands/trello_server.py +1214 -0
  158. novacode_cli/commands/vision_handler.py +127 -0
  159. novacode_cli/commands/voice_handler.py +447 -0
  160. novacode_cli/commands/webhook_handler.py +134 -0
  161. novacode_cli/commands/wiki_commands.py +227 -0
  162. novacode_cli/compaction.py +753 -0
  163. novacode_cli/computation_cache.py +159 -0
  164. novacode_cli/computer/__init__.py +1 -0
  165. novacode_cli/computer/persistent.py +234 -0
  166. novacode_cli/computer/preview.py +253 -0
  167. novacode_cli/computer/runtime.py +474 -0
  168. novacode_cli/computer/windows_console.py +152 -0
  169. novacode_cli/computer/windows_qemu.py +211 -0
  170. novacode_cli/config/browser_mode.py +159 -0
  171. novacode_cli/config/catalog_models.py +95 -0
  172. novacode_cli/config/config.py +1388 -0
  173. novacode_cli/config/credentials.py +540 -0
  174. novacode_cli/config/google_oauth_auth.py +281 -0
  175. novacode_cli/config/model_capabilities.py +343 -0
  176. novacode_cli/config/model_catalog.py +275 -0
  177. novacode_cli/config/model_create.py +1325 -0
  178. novacode_cli/config/model_manager.py +695 -0
  179. novacode_cli/config/nova_config.py +1283 -0
  180. novacode_cli/config/openai_chatgpt_auth.py +384 -0
  181. novacode_cli/config/opencode_gateway.py +25 -0
  182. novacode_cli/config/plan_mode.py +188 -0
  183. novacode_cli/config/provider_auth.py +438 -0
  184. novacode_cli/config/provider_catalog.json +5127 -0
  185. novacode_cli/config/provider_registry.py +38 -0
  186. novacode_cli/config/provider_transports.py +258 -0
  187. novacode_cli/config/role_models.py +175 -0
  188. novacode_cli/context/__init__.py +50 -0
  189. novacode_cli/context/_analysis.py +799 -0
  190. novacode_cli/context/_budget.py +116 -0
  191. novacode_cli/context/_conversation.py +73 -0
  192. novacode_cli/context/_dynamic.py +264 -0
  193. novacode_cli/context/_model_config.py +380 -0
  194. novacode_cli/context/_models_dev.py +99 -0
  195. novacode_cli/context/archives.py +248 -0
  196. novacode_cli/context/estimates.py +65 -0
  197. novacode_cli/context/history.py +17 -0
  198. novacode_cli/context/knowledge_cache.py +292 -0
  199. novacode_cli/context/manager.py +101 -0
  200. novacode_cli/context/observations.py +65 -0
  201. novacode_cli/context/pressure.py +168 -0
  202. novacode_cli/context/provider_accounting.py +109 -0
  203. novacode_cli/context/snapshot.py +493 -0
  204. novacode_cli/context/strategies.py +279 -0
  205. novacode_cli/context/task_progress.py +122 -0
  206. novacode_cli/core/__init__.py +0 -0
  207. novacode_cli/core/agent_loop.py +1821 -0
  208. novacode_cli/core/autonomous_loop.py +277 -0
  209. novacode_cli/core/code_approvals.py +57 -0
  210. novacode_cli/core/input_preparation.py +276 -0
  211. novacode_cli/core/reasoning.py +67 -0
  212. novacode_cli/core/streaming.py +326 -0
  213. novacode_cli/core/subagent_tasks.py +696 -0
  214. novacode_cli/core/subagent_tracking.py +235 -0
  215. novacode_cli/core/tool_results.py +116 -0
  216. novacode_cli/core/verification_loop.py +272 -0
  217. novacode_cli/council.py +595 -0
  218. novacode_cli/council_consensus.py +115 -0
  219. novacode_cli/council_decision.py +157 -0
  220. novacode_cli/council_planner.py +1421 -0
  221. novacode_cli/council_settings.py +208 -0
  222. novacode_cli/council_web_settings.py +41 -0
  223. novacode_cli/council_workspace.py +56 -0
  224. novacode_cli/cowork/__init__.py +10 -0
  225. novacode_cli/cowork/broker_middleware.py +162 -0
  226. novacode_cli/cowork/launcher.py +102 -0
  227. novacode_cli/cowork/policy.py +284 -0
  228. novacode_cli/cowork/ui.py +826 -0
  229. novacode_cli/daemons/__init__.py +1 -0
  230. novacode_cli/daemons/registry.py +342 -0
  231. novacode_cli/desktop_notifications.py +302 -0
  232. novacode_cli/doctor.py +251 -0
  233. novacode_cli/embeddings.py +109 -0
  234. novacode_cli/entrypoint.py +56 -0
  235. novacode_cli/errors/__init__.py +19 -0
  236. novacode_cli/errors/handlers.py +648 -0
  237. novacode_cli/errors/provider_errors.py +249 -0
  238. novacode_cli/errors/taxonomy.py +80 -0
  239. novacode_cli/events.py +179 -0
  240. novacode_cli/file_ops.py +714 -0
  241. novacode_cli/git_safety.py +281 -0
  242. novacode_cli/handoff.py +383 -0
  243. novacode_cli/headless/__init__.py +18 -0
  244. novacode_cli/headless/input.py +58 -0
  245. novacode_cli/headless/output.py +224 -0
  246. novacode_cli/headless/pipe.py +532 -0
  247. novacode_cli/headless/runner.py +461 -0
  248. novacode_cli/hermes/__init__.py +39 -0
  249. novacode_cli/hermes/activation.py +297 -0
  250. novacode_cli/hermes/config.py +129 -0
  251. novacode_cli/hermes/curator.py +199 -0
  252. novacode_cli/hermes/evolution.py +374 -0
  253. novacode_cli/hermes/ledger.py +364 -0
  254. novacode_cli/hermes/memory_tiers.py +969 -0
  255. novacode_cli/hermes/middleware.py +678 -0
  256. novacode_cli/hermes/overview.py +174 -0
  257. novacode_cli/hermes/prompt_evolution.py +503 -0
  258. novacode_cli/hermes/refine_loop.py +643 -0
  259. novacode_cli/hermes/refinement_log.py +190 -0
  260. novacode_cli/hermes/review.py +674 -0
  261. novacode_cli/hermes/skill_debate.py +135 -0
  262. novacode_cli/hermes/skill_discovery.py +450 -0
  263. novacode_cli/hermes/skill_manager.py +335 -0
  264. novacode_cli/hermes/skill_proposals.py +43 -0
  265. novacode_cli/hermes/test_gaming.py +119 -0
  266. novacode_cli/hermes/tracker.py +435 -0
  267. novacode_cli/hermes/tuner.py +206 -0
  268. novacode_cli/hermes/verification_memory.py +134 -0
  269. novacode_cli/hermes/verifier.py +277 -0
  270. novacode_cli/hitl/__init__.py +22 -0
  271. novacode_cli/hitl/interrupts.py +493 -0
  272. novacode_cli/hooks.py +359 -0
  273. novacode_cli/image_utils.py +517 -0
  274. novacode_cli/init/__init__.py +7 -0
  275. novacode_cli/init/events.py +43 -0
  276. novacode_cli/input_utils.py +526 -0
  277. novacode_cli/install_commands.py +87 -0
  278. novacode_cli/integrations/__init__.py +82 -0
  279. novacode_cli/integrations/daytona.py +122 -0
  280. novacode_cli/integrations/docker.py +309 -0
  281. novacode_cli/integrations/host_path.py +111 -0
  282. novacode_cli/integrations/langsmith.py +151 -0
  283. novacode_cli/integrations/modal.py +134 -0
  284. novacode_cli/integrations/runloop.py +130 -0
  285. novacode_cli/integrations/sandbox_factory.py +1388 -0
  286. novacode_cli/integrations/sandbox_registry.py +312 -0
  287. novacode_cli/integrations/workdir_backend.py +589 -0
  288. novacode_cli/main.py +2078 -0
  289. novacode_cli/mcp/__init__.py +73 -0
  290. novacode_cli/mcp/auth.py +189 -0
  291. novacode_cli/mcp/client.py +571 -0
  292. novacode_cli/mcp/code_mode.py +5 -0
  293. novacode_cli/mcp/code_runtime.py +5 -0
  294. novacode_cli/mcp/commands.py +902 -0
  295. novacode_cli/mcp/config.py +406 -0
  296. novacode_cli/mcp/generic.py +128 -0
  297. novacode_cli/mcp/middleware.py +993 -0
  298. novacode_cli/mcp/presets.py +367 -0
  299. novacode_cli/memory/__init__.py +1 -0
  300. novacode_cli/memory/agent_memory.py +1312 -0
  301. novacode_cli/memory/builtin_lessons.py +60 -0
  302. novacode_cli/memory/dream_gates.py +192 -0
  303. novacode_cli/memory/limits.py +59 -0
  304. novacode_cli/memory/parsed_cache.py +106 -0
  305. novacode_cli/memory/recall_stats.py +97 -0
  306. novacode_cli/memory/semantic.py +116 -0
  307. novacode_cli/memory/store.py +335 -0
  308. novacode_cli/memory/wiki.py +274 -0
  309. novacode_cli/migrate.py +185 -0
  310. novacode_cli/onboarding.py +649 -0
  311. novacode_cli/onboarding_check.py +119 -0
  312. novacode_cli/path_approval.py +297 -0
  313. novacode_cli/plan_archive.py +148 -0
  314. novacode_cli/plugins/__init__.py +121 -0
  315. novacode_cli/plugins/claude_plugins.py +538 -0
  316. novacode_cli/plugins/cli.py +168 -0
  317. novacode_cli/plugins/components.py +413 -0
  318. novacode_cli/plugins/hooks.py +356 -0
  319. novacode_cli/plugins/loader.py +317 -0
  320. novacode_cli/plugins/marketplaces.py +199 -0
  321. novacode_cli/plugins/storage.py +70 -0
  322. novacode_cli/prime.py +759 -0
  323. novacode_cli/prime_factory/__init__.py +15 -0
  324. novacode_cli/prime_factory/container.py +198 -0
  325. novacode_cli/prime_factory/controller.py +565 -0
  326. novacode_cli/prime_factory/knowledge.py +261 -0
  327. novacode_cli/prime_factory/publication.py +218 -0
  328. novacode_cli/prime_factory/recovery.py +50 -0
  329. novacode_cli/prime_factory/research.py +370 -0
  330. novacode_cli/prime_factory/runner.py +608 -0
  331. novacode_cli/prime_factory/runtime.py +261 -0
  332. novacode_cli/prime_factory/state.py +201 -0
  333. novacode_cli/prime_factory/store.py +128 -0
  334. novacode_cli/prime_factory/worker.py +129 -0
  335. novacode_cli/prime_factory/workflow.py +899 -0
  336. novacode_cli/prime_factory/workspace.py +317 -0
  337. novacode_cli/prime_memory.py +290 -0
  338. novacode_cli/process_manager.py +585 -0
  339. novacode_cli/project_onboard.py +537 -0
  340. novacode_cli/prompts/System_Prompt_Nova.jinja +23 -0
  341. novacode_cli/prompts/__init__.py +168 -0
  342. novacode_cli/prompts/agent_definitions.jinja +11 -0
  343. novacode_cli/prompts/agent_generation.jinja +125 -0
  344. novacode_cli/prompts/async_agents/general_purpose.jinja +22 -0
  345. novacode_cli/prompts/claude_plan_mode.jinja +12 -0
  346. novacode_cli/prompts/coding_workflow.jinja +18 -0
  347. novacode_cli/prompts/computer_use.jinja +17 -0
  348. novacode_cli/prompts/core_agent_system.jinja +81 -0
  349. novacode_cli/prompts/delegation.jinja +15 -0
  350. novacode_cli/prompts/desktop_power.jinja +4 -0
  351. novacode_cli/prompts/file_paths.jinja +15 -0
  352. novacode_cli/prompts/file_tracker.jinja +23 -0
  353. novacode_cli/prompts/init_agents_md.jinja +56 -0
  354. novacode_cli/prompts/init_exploration.jinja +174 -0
  355. novacode_cli/prompts/init_semantic_orchestrate.jinja +37 -0
  356. novacode_cli/prompts/instruction_scope.jinja +5 -0
  357. novacode_cli/prompts/learnings_extraction.jinja +45 -0
  358. novacode_cli/prompts/longterm_memory.jinja +54 -0
  359. novacode_cli/prompts/mcp.jinja +84 -0
  360. novacode_cli/prompts/memory_consolidation.jinja +40 -0
  361. novacode_cli/prompts/nova_evolution.jinja +73 -0
  362. novacode_cli/prompts/nova_review.jinja +158 -0
  363. novacode_cli/prompts/nova_skill_debate.jinja +36 -0
  364. novacode_cli/prompts/nova_standing_instructions.jinja +26 -0
  365. novacode_cli/prompts/nova_verify.jinja +100 -0
  366. novacode_cli/prompts/onboard_dashboard.jinja +378 -0
  367. novacode_cli/prompts/plan_agent.jinja +87 -0
  368. novacode_cli/prompts/plan_scouts.jinja +5 -0
  369. novacode_cli/prompts/plan_storage.jinja +8 -0
  370. novacode_cli/prompts/planning.jinja +82 -0
  371. novacode_cli/prompts/ponytail/LICENSE +21 -0
  372. novacode_cli/prompts/ponytail/UPSTREAM.json +15 -0
  373. novacode_cli/prompts/ponytail/hooks/ponytail-config.js +175 -0
  374. novacode_cli/prompts/ponytail/hooks/ponytail-instructions.js +98 -0
  375. novacode_cli/prompts/ponytail/skills/ponytail/SKILL.md +52 -0
  376. novacode_cli/prompts/ponytail/skills/ponytail-audit/SKILL.md +103 -0
  377. novacode_cli/prompts/ponytail/skills/ponytail-debt/SKILL.md +44 -0
  378. novacode_cli/prompts/ponytail/skills/ponytail-gain/SKILL.md +52 -0
  379. novacode_cli/prompts/ponytail/skills/ponytail-help/SKILL.md +76 -0
  380. novacode_cli/prompts/ponytail/skills/ponytail-review/SKILL.md +101 -0
  381. novacode_cli/prompts/prompt_evolution.jinja +27 -0
  382. novacode_cli/prompts/ralph_iteration.jinja +75 -0
  383. novacode_cli/prompts/refine_gate.jinja +31 -0
  384. novacode_cli/prompts/refine_plan.jinja +64 -0
  385. novacode_cli/prompts/refine_review.jinja +34 -0
  386. novacode_cli/prompts/research_delivery.jinja +9 -0
  387. novacode_cli/prompts/research_swarm.jinja +130 -0
  388. novacode_cli/prompts/response_style.jinja +5 -0
  389. novacode_cli/prompts/session_summarization.jinja +86 -0
  390. novacode_cli/prompts/skill_creation.jinja +85 -0
  391. novacode_cli/prompts/subagents/fact_checker.jinja +48 -0
  392. novacode_cli/prompts/subagents/financial_analyst.jinja +50 -0
  393. novacode_cli/prompts/subagents/literature_reviewer.jinja +52 -0
  394. novacode_cli/prompts/subagents/market_analyst.jinja +48 -0
  395. novacode_cli/prompts/subagents/research_synthesizer.jinja +57 -0
  396. novacode_cli/prompts/subagents/technical_researcher.jinja +56 -0
  397. novacode_cli/prompts/subagents/web_researcher.jinja +46 -0
  398. novacode_cli/prompts/summarization.jinja +107 -0
  399. novacode_cli/prompts/summarization_answers.jinja +23 -0
  400. novacode_cli/prompts/summarization_questions.jinja +32 -0
  401. novacode_cli/prompts/vision_caption.jinja +21 -0
  402. novacode_cli/prompts/voice_summary.jinja +65 -0
  403. novacode_cli/prompts/wiki_file.jinja +57 -0
  404. novacode_cli/prompts/wiki_ingest.jinja +87 -0
  405. novacode_cli/recovery.py +383 -0
  406. novacode_cli/remote/__init__.py +45 -0
  407. novacode_cli/remote/approvals.py +282 -0
  408. novacode_cli/remote/async_task_clients.py +97 -0
  409. novacode_cli/remote/async_task_watcher.py +240 -0
  410. novacode_cli/remote/bridge.py +905 -0
  411. novacode_cli/remote/command_menu.py +26 -0
  412. novacode_cli/remote/command_ui.py +243 -0
  413. novacode_cli/remote/config.py +199 -0
  414. novacode_cli/remote/discord_bridge.py +530 -0
  415. novacode_cli/remote/discord_format.py +31 -0
  416. novacode_cli/remote/discord_threads.py +225 -0
  417. novacode_cli/remote/images.py +97 -0
  418. novacode_cli/remote/processor.py +601 -0
  419. novacode_cli/remote/rootapp_bot/bot.cjs +16 -0
  420. novacode_cli/remote/rootapp_bot/package-lock.json +2683 -0
  421. novacode_cli/remote/rootapp_bot/package.json +18 -0
  422. novacode_cli/remote/rootapp_bot/transport.cjs +80 -0
  423. novacode_cli/remote/rootapp_bridge.py +370 -0
  424. novacode_cli/remote/rootapp_setup.py +89 -0
  425. novacode_cli/remote/routing.py +103 -0
  426. novacode_cli/remote/scheduler.py +302 -0
  427. novacode_cli/remote/session.py +164 -0
  428. novacode_cli/remote/status.py +411 -0
  429. novacode_cli/remote/streaming.py +188 -0
  430. novacode_cli/remote/telegram_bridge.py +1118 -0
  431. novacode_cli/remote/telegram_format.py +215 -0
  432. novacode_cli/remote/telegram_hub.py +256 -0
  433. novacode_cli/remote/voice_notes.py +120 -0
  434. novacode_cli/remote/webhook_adapters.py +167 -0
  435. novacode_cli/remote/webhook_server.py +154 -0
  436. novacode_cli/rewind.py +150 -0
  437. novacode_cli/rooms/__init__.py +1 -0
  438. novacode_cli/rooms/bridge.py +86 -0
  439. novacode_cli/rooms/cli.py +231 -0
  440. novacode_cli/rooms/nova.py +227 -0
  441. novacode_cli/rooms/runtime.py +299 -0
  442. novacode_cli/rooms/store.py +538 -0
  443. novacode_cli/rooms/workspace.py +94 -0
  444. novacode_cli/security/__init__.py +29 -0
  445. novacode_cli/security/approval_context.py +45 -0
  446. novacode_cli/security/delegated_approval.py +68 -0
  447. novacode_cli/security/local_http.py +85 -0
  448. novacode_cli/security/middleware.py +87 -0
  449. novacode_cli/security/paths.py +35 -0
  450. novacode_cli/security/policy.py +535 -0
  451. novacode_cli/security/policy_writer.py +90 -0
  452. novacode_cli/security/prompt_injection.py +203 -0
  453. novacode_cli/security/remember.py +81 -0
  454. novacode_cli/security/rule_synthesis.py +116 -0
  455. novacode_cli/security/secret_files.py +61 -0
  456. novacode_cli/security/session_allow.py +114 -0
  457. novacode_cli/security/unicode_security.py +516 -0
  458. novacode_cli/security/validator.py +173 -0
  459. novacode_cli/server/__init__.py +9 -0
  460. novacode_cli/server/app.py +632 -0
  461. novacode_cli/server/event_adapter.py +189 -0
  462. novacode_cli/server/session_manager.py +90 -0
  463. novacode_cli/server_main.py +64 -0
  464. novacode_cli/server_runner/dev_server.py +530 -0
  465. novacode_cli/server_runner/test_runner.py +470 -0
  466. novacode_cli/session/__init__.py +1 -0
  467. novacode_cli/session/adapters/__init__.py +240 -0
  468. novacode_cli/session/adapters/claude_code.py +102 -0
  469. novacode_cli/session/adapters/codex.py +77 -0
  470. novacode_cli/session/adapters/harness_native.py +75 -0
  471. novacode_cli/session/checkpoint_coordinator.py +448 -0
  472. novacode_cli/session/context_restore.py +25 -0
  473. novacode_cli/session/history_export.py +228 -0
  474. novacode_cli/session/history_tree.py +174 -0
  475. novacode_cli/session/imported_context.py +267 -0
  476. novacode_cli/session/runtime_identity.py +32 -0
  477. novacode_cli/session/session_persistence.py +1168 -0
  478. novacode_cli/session/session_prompt_builder.py +279 -0
  479. novacode_cli/session/session_restore.py +351 -0
  480. novacode_cli/session/session_summarization.py +20 -0
  481. novacode_cli/session/transcript_journal.py +153 -0
  482. novacode_cli/sessions/__init__.py +11 -0
  483. novacode_cli/sessions/gateway.py +84 -0
  484. novacode_cli/sessions/launch.py +51 -0
  485. novacode_cli/sessions/lease.py +213 -0
  486. novacode_cli/sessions/messaging.py +255 -0
  487. novacode_cli/sessions/protocol.py +216 -0
  488. novacode_cli/sessions/resume.py +31 -0
  489. novacode_cli/sessions/supervisor.py +498 -0
  490. novacode_cli/sessions/tasks.py +73 -0
  491. novacode_cli/sessions/tui_messaging.py +413 -0
  492. novacode_cli/sessions/worker.py +652 -0
  493. novacode_cli/sessions/worktree.py +262 -0
  494. novacode_cli/shell/__init__.py +27 -0
  495. novacode_cli/shell/jobs.py +475 -0
  496. novacode_cli/shell/middleware.py +1568 -0
  497. novacode_cli/shell/os_sandbox.py +205 -0
  498. novacode_cli/shell/output.py +181 -0
  499. novacode_cli/shell/patterns.py +275 -0
  500. novacode_cli/shell/processes.py +87 -0
  501. novacode_cli/shell/utils.py +236 -0
  502. novacode_cli/skills/__init__.py +26 -0
  503. novacode_cli/skills/builtin/handoff/SKILL.md +15 -0
  504. novacode_cli/skills/curation_middleware.py +69 -0
  505. novacode_cli/skills/executable.py +161 -0
  506. novacode_cli/skills/libraries.py +102 -0
  507. novacode_cli/skills/load.py +248 -0
  508. novacode_cli/skills/prune.py +215 -0
  509. novacode_cli/skills/refreshing_middleware.py +896 -0
  510. novacode_cli/skills/retrieval.py +248 -0
  511. novacode_cli/skills/runtime.py +118 -0
  512. novacode_cli/skills/schema.py +254 -0
  513. novacode_cli/skills/skill_creation.py +1549 -0
  514. novacode_cli/skills/skill_lock.py +75 -0
  515. novacode_cli/skills/skill_system_prompt.py +16 -0
  516. novacode_cli/skills/skills_prefs.py +126 -0
  517. novacode_cli/skills/tool_resolver.py +31 -0
  518. novacode_cli/skills/upstream.py +50 -0
  519. novacode_cli/skills/versioning.py +291 -0
  520. novacode_cli/states/Session.py +847 -0
  521. novacode_cli/states/__init__.py +31 -0
  522. novacode_cli/states/slices/__init__.py +17 -0
  523. novacode_cli/states/slices/agent_runtime.py +249 -0
  524. novacode_cli/states/slices/background_tasks.py +21 -0
  525. novacode_cli/states/slices/notifications.py +74 -0
  526. novacode_cli/states/slices/remote_bridge.py +56 -0
  527. novacode_cli/states/slices/ui_settings.py +49 -0
  528. novacode_cli/states/slices/wiki.py +25 -0
  529. novacode_cli/teammates/__init__.py +1 -0
  530. novacode_cli/teammates/controller.py +557 -0
  531. novacode_cli/teammates/graph.py +104 -0
  532. novacode_cli/teammates/registry.py +175 -0
  533. novacode_cli/teammates/service.py +254 -0
  534. novacode_cli/teammates/worker.py +19 -0
  535. novacode_cli/teams/__init__.py +1 -0
  536. novacode_cli/teams/adapters/__init__.py +1 -0
  537. novacode_cli/teams/adapters/container.py +316 -0
  538. novacode_cli/teams/adapters/inprocess.py +206 -0
  539. novacode_cli/teams/adapters/process.py +364 -0
  540. novacode_cli/teams/bootstrap.py +246 -0
  541. novacode_cli/teams/container_git.py +67 -0
  542. novacode_cli/teams/context.py +34 -0
  543. novacode_cli/teams/coordinator.py +815 -0
  544. novacode_cli/teams/gateway.py +200 -0
  545. novacode_cli/teams/host.py +64 -0
  546. novacode_cli/teams/legacy.py +52 -0
  547. novacode_cli/teams/middleware.py +176 -0
  548. novacode_cli/teams/models.py +104 -0
  549. novacode_cli/teams/output.py +47 -0
  550. novacode_cli/teams/protocol.py +50 -0
  551. novacode_cli/teams/recovery.py +80 -0
  552. novacode_cli/teams/scheduler.py +44 -0
  553. novacode_cli/teams/store.py +631 -0
  554. novacode_cli/teams/worker.py +251 -0
  555. novacode_cli/teams/workspace.py +100 -0
  556. novacode_cli/token_utils.py +256 -0
  557. novacode_cli/tools/__init__.py +104 -0
  558. novacode_cli/tools/_shared.py +97 -0
  559. novacode_cli/tools/_types.py +24 -0
  560. novacode_cli/tools/artifact_tools.py +137 -0
  561. novacode_cli/tools/code_search_tools.py +330 -0
  562. novacode_cli/tools/computer_tools.py +77 -0
  563. novacode_cli/tools/cron_tools.py +65 -0
  564. novacode_cli/tools/daemon_tool.py +290 -0
  565. novacode_cli/tools/desktop_tools.py +161 -0
  566. novacode_cli/tools/fetch_tools.py +530 -0
  567. novacode_cli/tools/http_tools.py +279 -0
  568. novacode_cli/tools/inspection_tools.py +107 -0
  569. novacode_cli/tools/job_tools.py +143 -0
  570. novacode_cli/tools/knowledge_tools.py +427 -0
  571. novacode_cli/tools/memory_tools.py +461 -0
  572. novacode_cli/tools/memory_wiki_tools.py +40 -0
  573. novacode_cli/tools/notebook_tools.py +238 -0
  574. novacode_cli/tools/oracle_tool.py +83 -0
  575. novacode_cli/tools/plan_archive_tools.py +80 -0
  576. novacode_cli/tools/plan_mode_tools.py +315 -0
  577. novacode_cli/tools/python_kernel_loop.py +165 -0
  578. novacode_cli/tools/python_kernel_tool.py +222 -0
  579. novacode_cli/tools/reflection_tools.py +34 -0
  580. novacode_cli/tools/scraper_tools.py +377 -0
  581. novacode_cli/tools/skill_tools.py +275 -0
  582. novacode_cli/tools/speak_tool.py +51 -0
  583. novacode_cli/tools/store_memory_tools.py +227 -0
  584. novacode_cli/tools/ui_tools.py +62 -0
  585. novacode_cli/tools/waiter_tools.py +408 -0
  586. novacode_cli/tools/web_tools.py +349 -0
  587. novacode_cli/tools/wiki_tools.py +152 -0
  588. novacode_cli/tools/worktree_tools.py +219 -0
  589. novacode_cli/tracking/__init__.py +10 -0
  590. novacode_cli/tracking/file_tracker.py +826 -0
  591. novacode_cli/tracking/local_metrics.py +170 -0
  592. novacode_cli/tracking/loop_guard.py +507 -0
  593. novacode_cli/tracking/request_metrics.py +60 -0
  594. novacode_cli/tracking/tracing.py +692 -0
  595. novacode_cli/tracking/usage_tree.py +677 -0
  596. novacode_cli/tracking/workspace_anchoring.py +231 -0
  597. novacode_cli/tui/__init__.py +11 -0
  598. novacode_cli/tui/animation_rate.py +9 -0
  599. novacode_cli/tui/animations.py +332 -0
  600. novacode_cli/tui/app.py +19315 -0
  601. novacode_cli/tui/auth_display.py +87 -0
  602. novacode_cli/tui/auth_screens.py +826 -0
  603. novacode_cli/tui/avatar.py +294 -0
  604. novacode_cli/tui/avatar_frames.py +21 -0
  605. novacode_cli/tui/background_tasks.py +35 -0
  606. novacode_cli/tui/browser_settings_screen.py +70 -0
  607. novacode_cli/tui/caveman_screen.py +185 -0
  608. novacode_cli/tui/council_settings_screen.py +182 -0
  609. novacode_cli/tui/council_view.py +535 -0
  610. novacode_cli/tui/decision_settings.py +268 -0
  611. novacode_cli/tui/diff_preview.py +199 -0
  612. novacode_cli/tui/gc_tuning.py +63 -0
  613. novacode_cli/tui/handoff_screen.py +206 -0
  614. novacode_cli/tui/harness.py +244 -0
  615. novacode_cli/tui/history_tree_screen.py +179 -0
  616. novacode_cli/tui/home.py +328 -0
  617. novacode_cli/tui/home_scene.py +94 -0
  618. novacode_cli/tui/icons.py +81 -0
  619. novacode_cli/tui/interface_styles.tcss +327 -0
  620. novacode_cli/tui/matrix3.py +108 -0
  621. novacode_cli/tui/mcp_tools_screen.py +131 -0
  622. novacode_cli/tui/motion.py +178 -0
  623. novacode_cli/tui/output_body.py +136 -0
  624. novacode_cli/tui/output_buffer.py +59 -0
  625. novacode_cli/tui/palette.py +334 -0
  626. novacode_cli/tui/pickers.py +237 -0
  627. novacode_cli/tui/prime_panel.py +506 -0
  628. novacode_cli/tui/provider_signin.py +213 -0
  629. novacode_cli/tui/pty_shell.py +159 -0
  630. novacode_cli/tui/reference_screens.py +444 -0
  631. novacode_cli/tui/resume_picker.py +346 -0
  632. novacode_cli/tui/rooms.py +597 -0
  633. novacode_cli/tui/screens.py +7088 -0
  634. novacode_cli/tui/session_import.py +333 -0
  635. novacode_cli/tui/session_pane.py +242 -0
  636. novacode_cli/tui/session_tab.py +42 -0
  637. novacode_cli/tui/session_transcript.py +118 -0
  638. novacode_cli/tui/shell_output.py +7 -0
  639. novacode_cli/tui/stall_watch.py +174 -0
  640. novacode_cli/tui/statusline.py +40 -0
  641. novacode_cli/tui/statusline_screen.py +91 -0
  642. novacode_cli/tui/teammate_updates.py +162 -0
  643. novacode_cli/tui/teammates.py +548 -0
  644. novacode_cli/tui/terminal.py +289 -0
  645. novacode_cli/tui/terminal_view.py +235 -0
  646. novacode_cli/tui/themes.py +118 -0
  647. novacode_cli/tui/update_notice.py +84 -0
  648. novacode_cli/tui/voice_downloads.py +113 -0
  649. novacode_cli/tui/widgets.py +2038 -0
  650. novacode_cli/tui/windows_input.py +38 -0
  651. novacode_cli/tui/workspace_approval.py +165 -0
  652. novacode_cli/ui/council_web.py +113 -0
  653. novacode_cli/ui/council_workspace.py +72 -0
  654. novacode_cli/ui/cowork_web.py +41 -0
  655. novacode_cli/ui/diff_highlight.py +160 -0
  656. novacode_cli/ui/execution.py +1396 -0
  657. novacode_cli/ui/exit_summary.py +65 -0
  658. novacode_cli/ui/hitl_approval.py +196 -0
  659. novacode_cli/ui/input_preparation.py +2 -0
  660. novacode_cli/ui/interrupt_handlers.py +215 -0
  661. novacode_cli/ui/question_prompt.py +151 -0
  662. novacode_cli/ui/status_phrases.py +157 -0
  663. novacode_cli/ui/stream_preview.py +66 -0
  664. novacode_cli/ui/streaming.py +2 -0
  665. novacode_cli/ui/subagent_tracking.py +2 -0
  666. novacode_cli/ui/ui_elements.py +1585 -0
  667. novacode_cli/ui/web_workspace.py +244 -0
  668. novacode_cli/ui_events.py +299 -0
  669. novacode_cli/update_ui.py +243 -0
  670. novacode_cli/updates.py +489 -0
  671. novacode_cli/utils/backend_patches.py +399 -0
  672. novacode_cli/utils/backend_paths.py +467 -0
  673. novacode_cli/utils/lazy_providers.py +51 -0
  674. novacode_cli/utils/model_info.py +223 -0
  675. novacode_cli/utils/pdf_extraction.py +183 -0
  676. novacode_cli/utils/startup.py +29 -0
  677. novacode_cli/utils/tasks.py +42 -0
  678. novacode_cli/utils/windows.py +9 -0
  679. novacode_cli/vixie/hook_adapter.py +127 -0
  680. novacode_cli/vixie/server.py +375 -0
  681. novacode_cli/wiki/__init__.py +15 -0
  682. novacode_cli/wiki/ask.py +56 -0
  683. novacode_cli/wiki/file.py +71 -0
  684. novacode_cli/wiki/ingest.py +184 -0
  685. novacode_cli/wiki/manager.py +261 -0
  686. novacode_cli-1.2.0.dist-info/METADATA +271 -0
  687. novacode_cli-1.2.0.dist-info/RECORD +691 -0
  688. novacode_cli-1.2.0.dist-info/WHEEL +5 -0
  689. novacode_cli-1.2.0.dist-info/entry_points.txt +2 -0
  690. novacode_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
  691. novacode_cli-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,753 @@
1
+ """Conversation compaction and summarization for NovaCode-cli.
2
+
3
+ This module provides functionality to compress conversation history
4
+ by generating an intelligent summary that preserves key context.
5
+ """
6
+
7
+ import asyncio
8
+ import hashlib
9
+ import json
10
+ import logging
11
+ from datetime import UTC, datetime
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from langchain_core.language_models import BaseChatModel
16
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, RemoveMessage, ToolMessage
17
+ from langgraph.graph.message import REMOVE_ALL_MESSAGES
18
+
19
+ from novacode_cli.context import CompactionResult
20
+ from novacode_cli.prompts import render_template
21
+
22
+ # Summarization prompt template (loaded from Jinja)
23
+ # Template file: NovaCode_cli/prompts/summarization.jinja
24
+
25
+ # Compaction rewrites the whole conversation into ONE synthetic HumanMessage
26
+ # carrying the summary (see compact_conversation). It has to be a HumanMessage
27
+ # for the provider's turn ordering, but it is not something the user said — so
28
+ # any surface that renders history must skip it, or the entire summarized
29
+ # context appears in the transcript as a message the user supposedly typed.
30
+ COMPACTION_SUMMARY_MARKER = "[Conversation context — previous session summarized]"
31
+
32
+ logger = logging.getLogger(__name__)
33
+ SUMMARY_TIMEOUT_SECONDS = 60.0
34
+ SUMMARY_ATTEMPTS = 3
35
+
36
+
37
+ async def _summary_call(model: BaseChatModel, prompt: str, purpose: str) -> BaseMessage:
38
+ """Preserve each independent summary request and label its actual dispatch."""
39
+ from novacode_cli.tracking.request_metrics import dispatch
40
+
41
+ with dispatch(purpose):
42
+ response = await model.ainvoke([HumanMessage(content=prompt)])
43
+ if getattr(response, "tool_calls", None) or getattr(response, "invalid_tool_calls", None):
44
+ message = "Summary provider returned tool calls"
45
+ raise SummaryUnavailableError(message)
46
+ return response
47
+
48
+
49
+ def _state_generation(state: Any) -> str: # noqa: ANN401 - compiled graph state is framework-defined
50
+ """Fingerprint checkpoint and complete messages before preparing a rewrite."""
51
+ data = {
52
+ "checkpoint": getattr(state, "config", None),
53
+ "messages": [m.model_dump(mode="json") for m in state.values.get("messages", [])],
54
+ "summarization_event": state.values.get("_summarization_event"),
55
+ }
56
+ return hashlib.sha256(json.dumps(data, sort_keys=True, default=str).encode()).hexdigest()
57
+
58
+
59
+ def _validate_generation(state: Any, expected: str) -> None: # noqa: ANN401 - custom graph state
60
+ """Discard a prepared summary whose source checkpoint was superseded."""
61
+ if _state_generation(state) != expected:
62
+ message = "Conversation changed during compaction; original history retained"
63
+ raise SummaryUnavailableError(message)
64
+
65
+
66
+ class SummaryUnavailableError(RuntimeError):
67
+ """The provider could not produce a usable compaction summary."""
68
+
69
+ def __init__(self, message: str = "Provider returned an empty response") -> None:
70
+ """Describe a failed or empty summarization response."""
71
+ super().__init__(message)
72
+
73
+
74
+ def is_compaction_summary(text: str) -> bool:
75
+ """True if *text* is the synthetic message compaction injects."""
76
+ return text.lstrip().startswith(COMPACTION_SUMMARY_MARKER)
77
+
78
+
79
+ def _format_message_content(content: Any) -> str:
80
+ """Format message content to a string.
81
+
82
+ Handles both string content and content blocks (list of dicts).
83
+
84
+ Args:
85
+ content: The message content (str or list of content blocks)
86
+
87
+ Returns:
88
+ Formatted string representation
89
+ """
90
+ if isinstance(content, str):
91
+ return content
92
+
93
+ if isinstance(content, list):
94
+ # Handle content blocks (e.g., from Claude)
95
+ text_parts = []
96
+ for block in content:
97
+ if isinstance(block, dict):
98
+ if block.get("type") == "text":
99
+ text_parts.append(block.get("text", ""))
100
+ elif block.get("type") == "tool_use":
101
+ tool_name = block.get("name", "unknown_tool")
102
+ text_parts.append(f"[Called tool: {tool_name}]")
103
+ elif isinstance(block, str):
104
+ text_parts.append(block)
105
+ return " ".join(text_parts)
106
+
107
+ return str(content)
108
+
109
+
110
+ def _message_parts(messages: list[BaseMessage]) -> list[str]:
111
+ """Render each message to a compact ``ROLE: text`` line (per-message truncation)."""
112
+ parts: list[str] = []
113
+ for msg in messages:
114
+ if isinstance(msg, HumanMessage):
115
+ # An earlier summary being folded into a new one goes in whole: cut
116
+ # to 2,000 characters like a user message, everything it carried
117
+ # from before the previous compaction would be lost at this one.
118
+ earlier = msg.additional_kwargs.get("nova_summary")
119
+ if earlier:
120
+ parts.append(f"EARLIER SUMMARY (carry its facts forward):\n{earlier}")
121
+ continue
122
+ content = _format_message_content(msg.content)
123
+ if len(content) > 2000:
124
+ content = content[:2000] + "... [truncated]"
125
+ parts.append(f"USER: {content}")
126
+ elif isinstance(msg, AIMessage):
127
+ content = _format_message_content(msg.content)
128
+ if len(content) > 2000:
129
+ content = content[:2000] + "... [truncated]"
130
+ # Tool calls live in .tool_calls, not .content — on a tool-calling
131
+ # turn the content is usually empty, so without this the summarizer
132
+ # never saw WHICH file was edited or WHAT command ran, and "Files
133
+ # Modified" was guessed from truncated tool results.
134
+ calls = [_render_tool_call(tc) for tc in getattr(msg, "tool_calls", None) or []]
135
+ parts.append("ASSISTANT: " + "\n".join(filter(None, [content, *calls])))
136
+ elif isinstance(msg, ToolMessage):
137
+ content = _format_message_content(msg.content)
138
+ if len(content) > 500:
139
+ content = content[:500] + "... [truncated]"
140
+ parts.append(f"TOOL({msg.name}): {content}")
141
+ return parts
142
+
143
+
144
+ _TOOL_ARGS_CHARS = 300
145
+
146
+
147
+ def _render_tool_call(call: dict) -> str:
148
+ """Render a tool call as ``-> name({args})`` with the args capped.
149
+
150
+ Enough for the summarizer to know what was touched, not a copy of the payload.
151
+ """
152
+ args = json.dumps(call.get("args") or {}, ensure_ascii=False, default=str)
153
+ if len(args) > _TOOL_ARGS_CHARS:
154
+ args = args[:_TOOL_ARGS_CHARS] + "...}"
155
+ return f"-> {call.get('name', 'tool')}({args})"
156
+
157
+
158
+ def _window_tokens(model: BaseChatModel, context_window: int | None = None) -> int:
159
+ """Return the model's context window in tokens.
160
+
161
+ The caller's value wins (the TUI passes the tracker's effective window,
162
+ which accounts for Ollama ``num_ctx``); else the static table
163
+ (``use_dynamic=False`` — no slow/flaky live query here).
164
+ """
165
+ if context_window and context_window > 0:
166
+ return context_window
167
+ name = getattr(model, "model_name", None) or getattr(model, "model", None) or ""
168
+ try:
169
+ from novacode_cli.context._analysis import get_context_window_size
170
+
171
+ return get_context_window_size(str(name), use_dynamic=False)
172
+ except Exception: # noqa: BLE001
173
+ return 8192
174
+
175
+
176
+ def _budget_chars(model: BaseChatModel, context_window: int | None = None) -> int:
177
+ """Character budget for the summarizer's input, sized to the model's context
178
+ window so a long conversation never overflows the summarization call itself.
179
+
180
+ Reserves ~45% of the window for the prompt template + generated summary;
181
+ rough 4-chars/token.
182
+ """
183
+ return max(8000, int(_window_tokens(model, context_window) * 0.55) * 4)
184
+
185
+
186
+ # The recent tail compaction keeps VERBATIM, as a share of the window, capped.
187
+ # Codex keeps ~20k tokens of recent messages; deepagents' own summarizer keeps
188
+ # 10% of the window. Summarizing the turn the agent is in the middle of is the
189
+ # lossiest possible cut — the exact error text, the file just edited, the
190
+ # user's latest wording all become a paraphrase.
191
+ KEEP_TAIL_FRACTION = 0.10
192
+ KEEP_TAIL_MAX_TOKENS = 20_000
193
+
194
+
195
+ def _message_chars(msg: BaseMessage) -> int:
196
+ size = len(_format_message_content(msg.content))
197
+ for call in getattr(msg, "tool_calls", None) or []:
198
+ size += len(json.dumps(call.get("args") or {}, default=str))
199
+ return size
200
+
201
+
202
+ def _split_tail(messages: list[BaseMessage], keep_tokens: int) -> int:
203
+ """Index where the verbatim tail starts (``len(messages)`` = keep nothing).
204
+
205
+ Cuts only at a real user message, so a tool call is never separated from
206
+ its result, and takes the EARLIEST such cut whose tail fits the budget. A
207
+ tail with a ToolMessage whose call is not in the tail is rejected as a
208
+ second guard (a steer injected mid-turn is also a HumanMessage).
209
+ """
210
+ budget = keep_tokens * 4
211
+ best = len(messages)
212
+ size = 0
213
+ for i in range(len(messages) - 1, 0, -1): # index 0: nothing left to summarize
214
+ size += _message_chars(messages[i])
215
+ if size > budget:
216
+ break
217
+ msg = messages[i]
218
+ if isinstance(msg, HumanMessage) and not is_compaction_summary(
219
+ _format_message_content(msg.content)
220
+ ):
221
+ best = i
222
+ tail = messages[best:]
223
+ issued = {tc.get("id") for m in tail for tc in (getattr(m, "tool_calls", None) or [])}
224
+ if any(isinstance(m, ToolMessage) and m.tool_call_id not in issued for m in tail):
225
+ return len(messages)
226
+ return best
227
+
228
+
229
+ def _rehydration_note() -> str:
230
+ """Paths the agent touched, so it can re-read them just in time.
231
+
232
+ Claude Code and Codex re-attach recently used files after compacting. Paths
233
+ are the cheap form of that: the content is on disk, the summary only has
234
+ to say where to look.
235
+ """
236
+ try:
237
+ from novacode_cli.tracking.file_tracker import (
238
+ get_modified_files,
239
+ get_recently_read_files,
240
+ )
241
+
242
+ modified = get_modified_files()[-15:]
243
+ read = [p for p in get_recently_read_files(15) if p not in modified]
244
+ except Exception: # noqa: BLE001 — a hint, never worth failing compaction
245
+ return ""
246
+ lines = []
247
+ if modified:
248
+ lines.append("- Modified: " + ", ".join(modified))
249
+ if read:
250
+ lines.append("- Read recently: " + ", ".join(read))
251
+ if not lines:
252
+ return ""
253
+ return (
254
+ "## Files touched this session\n"
255
+ "Re-read a file before relying on its contents; the summary only "
256
+ "describes it.\n" + "\n".join(lines)
257
+ )
258
+
259
+
260
+ def _continuation_framing(*, tail_kept: bool) -> str:
261
+ """Mark compacted requests as history, with the current tail taking precedence."""
262
+ tail = (
263
+ "The verbatim tail after this summary is authoritative for current progress. "
264
+ if tail_kept
265
+ else "Use the latest recorded state to determine current progress. "
266
+ )
267
+ return (
268
+ "The earlier conversation was compacted into the summary below. "
269
+ + tail
270
+ + "Continue the work only if it is unfinished, from the last observed action. "
271
+ "Do not repeat completed steps or tool calls. Do not acknowledge or restate this summary."
272
+ )
273
+
274
+
275
+ def _historical_user_requests(requests: list[str]) -> str:
276
+ """Preserve earlier user words without presenting completed requests as new turns."""
277
+ if not requests:
278
+ return ""
279
+ return (
280
+ "## Earlier user requests and constraints (historical context)\n\n"
281
+ "Keep applicable constraints, but do not assume these requests are still pending. "
282
+ "The summary and any verbatim tail record what was already completed.\n\n"
283
+ + "\n\n".join(f"{index}. {text}" for index, text in enumerate(requests, 1))
284
+ )
285
+
286
+
287
+ def _archive_messages(thread_id: str, messages: list[BaseMessage]) -> Path | None:
288
+ """Write the pre-compaction transcript to the session dir; return its path.
289
+
290
+ Compaction is the one irreversible step in the context pipeline: the
291
+ summary is a paraphrase and the originals are replaced. LangGraph keeps the
292
+ old checkpoint, but nothing surfaces it — so the raw messages also go to a
293
+ file next to the session, the way Claude Code keeps a pre-compaction
294
+ transcript. Reaped with the session directory. Best-effort.
295
+ """
296
+ try:
297
+ from novacode_cli.context.archives import default_recovery_store
298
+ return default_recovery_store().archive(thread_id, messages)
299
+ except Exception:
300
+ logger.debug("Could not publish recovery archive", exc_info=True)
301
+ return None
302
+
303
+
304
+ def _chunk_parts(parts: list[str], max_chars: int) -> list[list[str]]:
305
+ """Group formatted parts into chunks each within ``max_chars``."""
306
+ chunks: list[list[str]] = []
307
+ cur: list[str] = []
308
+ cur_len = 0
309
+ for p in parts:
310
+ if len(p) > max_chars: # a single oversized part — hard-truncate it
311
+ p = p[:max_chars] + "... [truncated]"
312
+ if cur and cur_len + len(p) + 2 > max_chars:
313
+ chunks.append(cur)
314
+ cur, cur_len = [], 0
315
+ cur.append(p)
316
+ cur_len += len(p) + 2
317
+ if cur:
318
+ chunks.append(cur)
319
+ return chunks
320
+
321
+
322
+ async def _summarize_text(
323
+ model: BaseChatModel,
324
+ conversation_text: str,
325
+ focus_instructions: str | None,
326
+ *,
327
+ tail_kept: bool = False,
328
+ ) -> str:
329
+ """One summarization LLM call over ``conversation_text``."""
330
+ if focus_instructions:
331
+ focus_text = (
332
+ f"\n**IMPORTANT - User requested focus on**: {focus_instructions}\n\n"
333
+ "Make sure to especially preserve information related to this focus area.\n"
334
+ )
335
+ else:
336
+ focus_text = ""
337
+ prompt = render_template(
338
+ "summarization.jinja",
339
+ focus_instructions=focus_text,
340
+ conversation=conversation_text,
341
+ tail_kept=tail_kept,
342
+ )
343
+ from novacode_cli.errors import is_retryable_model_error
344
+
345
+ for attempt in range(SUMMARY_ATTEMPTS):
346
+ try:
347
+ response = await asyncio.wait_for(
348
+ _summary_call(model, prompt, "summary.generate"), SUMMARY_TIMEOUT_SECONDS
349
+ )
350
+ summary = _format_message_content(response.content).strip()
351
+ if not summary:
352
+ raise SummaryUnavailableError() # noqa: TRY301 - retry empty output
353
+ return summary # noqa: TRY300
354
+ except Exception as error:
355
+ if not is_retryable_model_error(error):
356
+ raise
357
+ if attempt + 1 == SUMMARY_ATTEMPTS:
358
+ message = (
359
+ "The summary provider is unavailable after 3 attempts. "
360
+ "Conversation history was kept. Try /compact again shortly "
361
+ "or switch models with /model."
362
+ )
363
+ raise SummaryUnavailableError(message) from error
364
+ logger.debug("Retrying compaction summary", exc_info=True)
365
+ await asyncio.sleep(2**attempt)
366
+ message = "No summary attempts configured"
367
+ raise AssertionError(message)
368
+
369
+
370
+ def _answer_budget(budget: int, questions: str) -> int:
371
+ """Conversation budget for the answer pass (reserve room for questions + template)."""
372
+ return max(1000, budget - len(questions) - 500)
373
+
374
+
375
+ async def _generate_questions(model: BaseChatModel, summary: str) -> str:
376
+ """One LLM call: read the summary and ask what important details are missing."""
377
+ prompt = render_template("summarization_questions.jinja", summary=summary)
378
+ response = await _summary_call(model, prompt, "summary.questions")
379
+ return _format_message_content(response.content).strip()
380
+
381
+
382
+ async def _answer_questions(
383
+ model: BaseChatModel, questions: str, parts: list[str], budget: int
384
+ ) -> str:
385
+ """Answer the clarifying questions from the full conversation.
386
+
387
+ Fits in one call when the conversation does; otherwise answers per chunk
388
+ and merges the per-chunk answers into one consolidated Q&A block.
389
+ """
390
+ text = "\n\n".join(parts)
391
+ q_budget = _answer_budget(budget, questions)
392
+ if len(text) <= q_budget:
393
+ prompt = render_template(
394
+ "summarization_answers.jinja", questions=questions, conversation=text
395
+ )
396
+ response = await _summary_call(model, prompt, "summary.answers")
397
+ return _format_message_content(response.content).strip()
398
+
399
+ per_chunk: list[str] = []
400
+ for chunk in _chunk_parts(parts, q_budget):
401
+ prompt = render_template(
402
+ "summarization_answers.jinja",
403
+ questions=questions,
404
+ conversation="\n\n".join(chunk),
405
+ )
406
+ response = await _summary_call(model, prompt, "summary.chunk_answers")
407
+ per_chunk.append(_format_message_content(response.content).strip())
408
+
409
+ # Merge: consolidate the per-chunk answers into one Q&A block (bounded).
410
+ merged = "\n\n".join(
411
+ f"## Excerpt {i + 1} of {len(per_chunk)}\n{a}" for i, a in enumerate(per_chunk)
412
+ )
413
+ prompt = render_template(
414
+ "summarization_answers.jinja",
415
+ questions=questions,
416
+ conversation=merged[:q_budget],
417
+ )
418
+ response = await _summary_call(model, prompt, "summary.merge_answers")
419
+ return _format_message_content(response.content).strip()
420
+
421
+
422
+ async def _qa_refine(model: BaseChatModel, summary: str, parts: list[str], budget: int) -> str:
423
+ """Gap-fill a lossy summary: ask what it missed, restore it from the source.
424
+
425
+ Best-effort — any failure returns ``""`` so compaction falls back to the
426
+ plain summary. Returns the Q&A block to append, or ``""`` when nothing
427
+ is missing.
428
+ """
429
+ try:
430
+ questions = await _generate_questions(model, summary)
431
+ if not questions or "NONE" in questions.upper():
432
+ return ""
433
+ answers = await _answer_questions(model, questions, parts, budget)
434
+ if not answers.strip():
435
+ return ""
436
+ return f"## Clarifying Q&A\n\n{answers.strip()}"
437
+ except Exception: # noqa: BLE001 — refinement must never break compaction
438
+ return ""
439
+
440
+
441
+ async def summarize_conversation(
442
+ model: BaseChatModel,
443
+ messages: list[BaseMessage],
444
+ focus_instructions: str | None = None,
445
+ context_window: int | None = None,
446
+ *,
447
+ tail_kept: bool = False,
448
+ ) -> str:
449
+ """Summarize a conversation, budgeted to the model's context window.
450
+
451
+ Short conversations are summarized in a single call. Long ones (that would
452
+ overflow the summarizer's own window — exactly when compaction matters most)
453
+ are summarized **hierarchically**: chunk → summarize each chunk → summarize
454
+ the chunk-summaries into one cohesive summary, recursing if still too large.
455
+ The hierarchical path then runs a **Q&A gap-filling pass** (Meta-Harness
456
+ port): a second call reads the summary and asks what important details are
457
+ missing, and a third answers those questions from the full conversation —
458
+ recovering details a single lossy pass drops. Best-effort: any failure falls
459
+ back to the plain summary.
460
+
461
+ Args:
462
+ model: The LLM to use for summarization
463
+ messages: The conversation messages to summarize
464
+ focus_instructions: Optional focus instructions from the user
465
+ context_window: Optional explicit context window (tokens); the TUI passes
466
+ the token tracker's effective window.
467
+ tail_kept: The most recent messages stay verbatim after the summary, so
468
+ the summary covers only what precedes them.
469
+
470
+ Returns:
471
+ The summarized conversation as a string
472
+
473
+ Raises:
474
+ Exception: If the LLM call fails
475
+ """
476
+ parts = _message_parts(messages)
477
+ budget = _budget_chars(model, context_window)
478
+ text = "\n\n".join(parts)
479
+
480
+ if len(text) <= budget:
481
+ return await _summarize_text(model, text, focus_instructions, tail_kept=tail_kept)
482
+
483
+ # Too big for one call → hierarchical summarization.
484
+ chunks = _chunk_parts(parts, budget)
485
+ summaries: list[str] = []
486
+ for i, chunk in enumerate(chunks):
487
+ s = await _summarize_text(model, "\n\n".join(chunk), focus_instructions)
488
+ summaries.append(f"## Part {i + 1} of {len(chunks)}\n{s}")
489
+ combined = "\n\n".join(summaries)
490
+
491
+ # If even the combined chunk-summaries are too large, collapse them further.
492
+ depth = 0
493
+ while len(combined) > budget and depth < 3:
494
+ depth += 1
495
+ summaries = [
496
+ await _summarize_text(model, "\n\n".join(group), focus_instructions)
497
+ for group in _chunk_parts(combined.split("\n\n"), budget)
498
+ ]
499
+ combined = "\n\n".join(summaries)
500
+
501
+ # Final pass: one cohesive summary from the collapsed material.
502
+ final = await _summarize_text(model, combined[:budget], focus_instructions, tail_kept=tail_kept)
503
+
504
+ # Q&A gap-filling (Meta-Harness port): the hierarchical path is lossy — ask
505
+ # what the summary missed and restore it from the full conversation. Only
506
+ # here: short single-pass summaries are already high-fidelity.
507
+ qa = await _qa_refine(model, final, parts, budget)
508
+ if qa:
509
+ final = f"{final}\n\n{qa}"
510
+ return final
511
+
512
+
513
+ async def compact_conversation(
514
+ agent: Any,
515
+ model: BaseChatModel,
516
+ thread_id: str,
517
+ focus_instructions: str | None = None,
518
+ context_window: int | None = None,
519
+ agent_dir: Path | None = None,
520
+ ) -> CompactionResult:
521
+ """Prepare a generation-validated rewrite with correlated summary requests."""
522
+ from novacode_cli.computation_cache import digest
523
+ from novacode_cli.tracking.request_metrics import correlate
524
+
525
+ with correlate(digest(thread_id)):
526
+ return await _compact_conversation(
527
+ agent, model, thread_id, focus_instructions, context_window, agent_dir
528
+ )
529
+
530
+
531
+ async def _compact_conversation(
532
+ agent: Any, # noqa: ANN401 - custom compiled graphs share a duck-typed state API
533
+ model: BaseChatModel,
534
+ thread_id: str,
535
+ focus_instructions: str | None = None,
536
+ context_window: int | None = None,
537
+ agent_dir: Path | None = None,
538
+ ) -> CompactionResult:
539
+ """Compact a conversation by summarizing and replacing history.
540
+
541
+ This function:
542
+ 1. Retrieves the current conversation history
543
+ 2. Generates a summary using the LLM
544
+ 3. Replaces the conversation with a single summary message
545
+
546
+ Args:
547
+ agent: The LangGraph agent
548
+ model: The LLM model for summarization
549
+ thread_id: The thread/session ID
550
+ focus_instructions: Optional user instructions for what to preserve
551
+ context_window: Model context window (tokens) to budget the summary to
552
+ agent_dir: If given, the compaction summary is also persisted as a
553
+ durable memory lesson (``memories/session-summary.md``) so the
554
+ conversation's knowledge survives the rewrite.
555
+
556
+ Returns:
557
+ CompactionResult with details of the compaction operation
558
+ """
559
+ config = {"configurable": {"thread_id": thread_id}}
560
+
561
+ try:
562
+ # Get current state
563
+ state = await agent.aget_state(config)
564
+ generation = _state_generation(state)
565
+ messages = state.values.get("messages", [])
566
+
567
+ if not messages:
568
+ return CompactionResult(
569
+ success=False,
570
+ original_tokens=0,
571
+ new_tokens=0,
572
+ tokens_saved=0,
573
+ messages_before=0,
574
+ messages_after=0,
575
+ summary="",
576
+ error="No conversation history to compact.",
577
+ )
578
+
579
+ messages_before = len(messages)
580
+ from novacode_cli.agents.context_compaction import user_messages
581
+
582
+ protected_tokens = sum(max(1, len(text) // 3) for text in user_messages(messages, 0))
583
+ reservation = getattr(model, "max_tokens", None) or 0
584
+ if isinstance(reservation, int) and protected_tokens + reservation > _window_tokens(
585
+ model, context_window
586
+ ):
587
+ raise SummaryUnavailableError(
588
+ "Protected instructions exceed the usable context window. Select a larger model window or explicitly revise the task. Conversation history was kept."
589
+ )
590
+ # A long transcript is tens of MB of JSON; write it off the UI's loop.
591
+ archived = await asyncio.to_thread(_archive_messages, thread_id, messages)
592
+ if not archived:
593
+ raise SummaryUnavailableError(
594
+ "Compaction archive could not be saved. Conversation history was kept."
595
+ )
596
+ from novacode_cli.session.checkpoint_coordinator import CoordinatedGraph
597
+ if isinstance(agent, CoordinatedGraph):
598
+ from novacode_cli.context.archives import default_recovery_store
599
+ saver = agent.coordinator.saver
600
+ original = await saver.aget_tuple(state.config)
601
+ if original:
602
+ packed = {
603
+ "checkpoint": saver.serde.dumps_typed(original.checkpoint),
604
+ "pending": saver.serde.dumps_typed(original.pending_writes),
605
+ "metadata": original.metadata,
606
+ }
607
+ archived = await asyncio.to_thread(
608
+ default_recovery_store().archive,
609
+ thread_id,
610
+ messages,
611
+ checkpoint=packed,
612
+ )
613
+
614
+ # These display statistics are estimates. Provider token counters may
615
+ # make synchronous network calls; use Nova's local, image-aware estimate.
616
+ from novacode_cli.context.estimates import estimate_content
617
+
618
+ original_tokens = (await asyncio.to_thread(estimate_content, messages)).total
619
+
620
+ # Keep the recent tail verbatim; summarize only what precedes it.
621
+ keep_tokens = min(
622
+ KEEP_TAIL_MAX_TOKENS,
623
+ int(_window_tokens(model, context_window) * KEEP_TAIL_FRACTION),
624
+ )
625
+ cut = _split_tail(messages, keep_tokens)
626
+ from novacode_cli.agents.context_compaction import pending_tool_start
627
+
628
+ cut = min(cut, pending_tool_start(messages))
629
+ if cut == 0:
630
+ raise SummaryUnavailableError(
631
+ "Pending tool calls prevent compaction. Resolve the pending approval or tool call first. Conversation history was kept."
632
+ )
633
+ head, tail = messages[:cut], messages[cut:]
634
+
635
+ # Generate summary (budgeted to the model's context window)
636
+ summary = await summarize_conversation(
637
+ model,
638
+ head,
639
+ focus_instructions,
640
+ context_window=context_window,
641
+ tail_kept=bool(tail),
642
+ )
643
+
644
+ # Replace the conversation in a single atomic update: REMOVE_ALL then
645
+ # summary + tail, so the summary lands BEFORE the tail (a plain
646
+ # RemoveMessage per id would append it after) and the state never
647
+ # passes through an invalid intermediate (ToolMessages with no
648
+ # AIMessage), which crashes langchain's _fetch_last_ai_and_tool_messages.
649
+ framing = _continuation_framing(tail_kept=bool(tail))
650
+ archive_note = (
651
+ f"The full pre-compaction transcript is at {archived} if an exact detail is needed."
652
+ if archived
653
+ else ""
654
+ )
655
+ from novacode_cli.agents.context_compaction import user_content_blocks, user_messages
656
+ from novacode_cli.context.task_progress import render_task_progress, task_progress
657
+
658
+ said = user_messages(head, 0)
659
+ progress = task_progress(head, state.values.get("todos"))
660
+ protected = _historical_user_requests(said)
661
+ body = "\n\n".join(
662
+ filter(None, [framing, protected, summary, render_task_progress(progress),
663
+ _rehydration_note(), archive_note])
664
+ )
665
+ blocks = user_content_blocks(head)
666
+ content = f"{COMPACTION_SUMMARY_MARKER}\n\n{body}"
667
+ summary_message = HumanMessage(
668
+ content=[{"type": "text", "text": content}, *blocks] if blocks else content,
669
+ additional_kwargs={
670
+ "lc_source": "summarization",
671
+ "user_messages": said,
672
+ "nova_summary": summary,
673
+ "nova_task_progress": progress,
674
+ "history_files": [str(archived)] if archived else [],
675
+ "user_content_blocks": blocks,
676
+ },
677
+ )
678
+ # Also clear any prior auto-summarization event.
679
+ # ContextCompactionMiddleware reconstructs the effective message list from
680
+ # `_summarization_event` (a cutoff index into the OLD message list); if we
681
+ # rewrite messages without clearing it, the next turn would slice the new
682
+ # list at a stale index and corrupt context. Resetting it makes the fresh
683
+ # summary the whole context.
684
+ new_messages = [RemoveMessage(id=REMOVE_ALL_MESSAGES), summary_message, *tail]
685
+ update_values: dict[str, Any] = {
686
+ "messages": new_messages,
687
+ "_summarization_event": None,
688
+ }
689
+ current = await agent.aget_state(config)
690
+ _validate_generation(current, generation)
691
+ from novacode_cli.session.checkpoint_coordinator import CoordinatedGraph
692
+ commit_options = {}
693
+ if isinstance(agent, CoordinatedGraph):
694
+ commit_options["expected_revision"] = (state.config or {}).get("configurable", {}).get("checkpoint_id")
695
+ try:
696
+ await agent.aupdate_state(config=config, values=update_values, as_node="model", **commit_options)
697
+ except Exception:
698
+ # Older graphs without the summarization state key reject the extra
699
+ # field; retry with just the message rewrite.
700
+ current = await agent.aget_state(config)
701
+ _validate_generation(current, generation)
702
+ await agent.aupdate_state(
703
+ config=config,
704
+ values={"messages": new_messages},
705
+ as_node="model",
706
+ **commit_options,
707
+ )
708
+
709
+ new_tokens = (await asyncio.to_thread(estimate_content, [summary_message, *tail])).total
710
+
711
+ # Persist the summary as a durable memory lesson so the conversation's
712
+ # knowledge survives the rewrite (best-effort; never fails compaction).
713
+ learnings = ""
714
+ if agent_dir is not None:
715
+ try:
716
+ from novacode_cli.hermes.memory_tiers import record_lesson
717
+
718
+ # The marker travels WITH the artifact: a derived summary is
719
+ # injected into future prompts, so it must carry its own
720
+ # provenance rather than relying on an instruction elsewhere to
721
+ # establish precedence. Unlabelled, it reads as verified fact.
722
+ record_lesson(
723
+ agent_dir,
724
+ "session-summary",
725
+ "[derived summary, not a verified record — may be incomplete; "
726
+ "observed reality wins on any conflict]\n\n" + summary,
727
+ )
728
+ learnings = summary
729
+ except Exception: # noqa: BLE001 — memory persistence is best-effort
730
+ learnings = ""
731
+
732
+ return CompactionResult(
733
+ success=True,
734
+ original_tokens=original_tokens,
735
+ new_tokens=new_tokens,
736
+ tokens_saved=max(0, original_tokens - new_tokens),
737
+ messages_before=messages_before,
738
+ messages_after=1 + len(tail),
739
+ summary=summary,
740
+ learnings=learnings,
741
+ )
742
+
743
+ except Exception as e:
744
+ return CompactionResult(
745
+ success=False,
746
+ original_tokens=0,
747
+ new_tokens=0,
748
+ tokens_saved=0,
749
+ messages_before=0,
750
+ messages_after=0,
751
+ summary="",
752
+ error=str(e),
753
+ )