utim-cli 2.3.22__tar.gz → 2.3.24__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. {utim_cli-2.3.22 → utim_cli-2.3.24}/CHANGELOG.md +10 -0
  2. {utim_cli-2.3.22/utim_cli.egg-info → utim_cli-2.3.24}/PKG-INFO +1 -1
  3. {utim_cli-2.3.22 → utim_cli-2.3.24}/pyproject.toml +1 -1
  4. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim/__init__.py +2 -2
  5. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/__init__.py +1 -1
  6. utim_cli-2.3.24/utim_cli/_chip_test.py +22 -0
  7. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/_version.py +1 -1
  8. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/config.py +65 -0
  9. utim_cli-2.3.24/utim_cli/constants.py +43 -0
  10. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/context_pruner.py +308 -19
  11. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/orchestrator.py +11 -3
  12. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/orchestrator_llm_transport.py +98 -30
  13. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/orchestrator_turn_history.py +23 -3
  14. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/key_rotator.py +83 -56
  15. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/models.py +4 -13
  16. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/completion_routes.py +93 -23
  17. utim_cli-2.3.24/utim_cli/tui/compression_dialog.py +230 -0
  18. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/utim.py +220 -36
  19. {utim_cli-2.3.22 → utim_cli-2.3.24/utim_cli.egg-info}/PKG-INFO +1 -1
  20. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli.egg-info/SOURCES.txt +2 -0
  21. utim_cli-2.3.22/utim_cli/constants.py +0 -99
  22. {utim_cli-2.3.22 → utim_cli-2.3.24}/LICENSE +0 -0
  23. {utim_cli-2.3.22 → utim_cli-2.3.24}/MANIFEST.in +0 -0
  24. {utim_cli-2.3.22 → utim_cli-2.3.24}/README.md +0 -0
  25. {utim_cli-2.3.22 → utim_cli-2.3.24}/setup.cfg +0 -0
  26. {utim_cli-2.3.22 → utim_cli-2.3.24}/setup.py +0 -0
  27. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/_verify_repro.py +0 -0
  28. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/_verify_syntax.py +0 -0
  29. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/_wizard_smoke_test.py +0 -0
  30. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/agent.py +0 -0
  31. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/ask_helper.py +0 -0
  32. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/auth.py +0 -0
  33. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/backup.py +0 -0
  34. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/billing.py +0 -0
  35. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/blender_agent.py +0 -0
  36. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/bootstrap.py +0 -0
  37. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/brain.py +0 -0
  38. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
  39. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/client_utils.py +0 -0
  40. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/crawler/__init__.py +0 -0
  41. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/crawler/engine.py +0 -0
  42. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/crawler/markdown_distiller.py +0 -0
  43. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/dependency_manager.py +0 -0
  44. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/doctor.py +0 -0
  45. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/evidence_registry.py +0 -0
  46. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/harbor.py +0 -0
  47. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/knowledge_graph.py +0 -0
  48. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/local_db.py +0 -0
  49. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/logger.py +0 -0
  50. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/mcp_clean_wrapper.py +0 -0
  51. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/mcp_client.py +0 -0
  52. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/mcp_registry.json +0 -0
  53. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/models.txt +0 -0
  54. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/orchestrator_helpers.py +0 -0
  55. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/orchestrator_tool_exec.py +0 -0
  56. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/personalities.py +0 -0
  57. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/platform_installer.py +0 -0
  58. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/plugins/__init__.py +0 -0
  59. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/plugins/hooks.py +0 -0
  60. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/plugins/loader.py +0 -0
  61. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/plugins/manager.py +0 -0
  62. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/plugins/registry.py +0 -0
  63. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/plugins/runtime.py +0 -0
  64. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/plugins/schema.py +0 -0
  65. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/plugins/tool_loader.py +0 -0
  66. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/reflection.py +0 -0
  67. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/report.py +0 -0
  68. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/scrapy_search.py +0 -0
  69. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/sdk/__init__.py +0 -0
  70. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/sdk/config.py +0 -0
  71. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/sdk/context.py +0 -0
  72. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/sdk/events.py +0 -0
  73. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/sdk/handlers.py +0 -0
  74. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/sdk/session.py +0 -0
  75. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/sdk/sidecar.py +0 -0
  76. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/__init__.py +0 -0
  77. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/admin_auth.py +0 -0
  78. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/attribution.py +0 -0
  79. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/audit_log.py +0 -0
  80. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/auth.py +0 -0
  81. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/batch_processor.py +0 -0
  82. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/captcha.py +0 -0
  83. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/cli_auth.py +0 -0
  84. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/concurrency.py +0 -0
  85. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/db.py +0 -0
  86. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  87. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/docs_md/about.md +0 -0
  88. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/docs_md/changelog.md +0 -0
  89. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/docs_md/docs.md +0 -0
  90. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/docs_md/features.md +0 -0
  91. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/docs_md/license.md +0 -0
  92. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/docs_md/pricing.md +0 -0
  93. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/docs_md/privacy.md +0 -0
  94. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/docs_md/refund.md +0 -0
  95. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/docs_md/support.md +0 -0
  96. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/docs_md/terms.md +0 -0
  97. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/email_utils.py +0 -0
  98. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/exchange_rate.py +0 -0
  99. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/firebase.py +0 -0
  100. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/fix_duplicate_users.py +0 -0
  101. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/history.py +0 -0
  102. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/local_llm.py +0 -0
  103. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/logging_config.py +0 -0
  104. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/micro_batcher.py +0 -0
  105. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/model_agent.py +0 -0
  106. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/plan_integrity.py +0 -0
  107. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/pricing_updater.py +0 -0
  108. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/provision_build.py +0 -0
  109. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/rate_limit.py +0 -0
  110. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/rewards_engine.py +0 -0
  111. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/router.py +0 -0
  112. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/__init__.py +0 -0
  113. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/admin_bonus_routes.py +0 -0
  114. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/admin_db_routes.py +0 -0
  115. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/auth_routes.py +0 -0
  116. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/billing_routes.py +0 -0
  117. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/credit_routes.py +0 -0
  118. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/feedback_routes.py +0 -0
  119. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/marketplace_routes.py +0 -0
  120. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/quota_routes.py +0 -0
  121. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/quota_share_routes.py +0 -0
  122. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/referral_routes.py +0 -0
  123. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/rewards_routes.py +0 -0
  124. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/security_routes.py +0 -0
  125. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/session_routes.py +0 -0
  126. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/share_routes.py +0 -0
  127. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/routes/webhook_routes.py +0 -0
  128. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/security_alerts.py +0 -0
  129. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/server.py +0 -0
  130. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/storage_nodes.py +0 -0
  131. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/server/utils.py +0 -0
  132. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/share.py +0 -0
  133. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/share_tui.py +0 -0
  134. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/situational_scoring.py +0 -0
  135. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/state.py +0 -0
  136. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/subagent_manager.py +0 -0
  137. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/task_dispatcher.py +0 -0
  138. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/task_logger.py +0 -0
  139. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tools.py +0 -0
  140. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tools_grep.py +0 -0
  141. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/__init__.py +0 -0
  142. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/feedback_dialog.py +0 -0
  143. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/history_dialog.py +0 -0
  144. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/marketplace_app_state.py +0 -0
  145. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/marketplace_dialog.py +0 -0
  146. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  147. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/mcp_dialog.py +0 -0
  148. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/miniagents_dialog.py +0 -0
  149. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/model_dialog.py +0 -0
  150. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/personality_dialog.py +0 -0
  151. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/plugins_dialog.py +0 -0
  152. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/publish_dialog.py +0 -0
  153. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/quota_dialog.py +0 -0
  154. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  155. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/quota_share_dialog.py +0 -0
  156. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/resume_dialog.py +0 -0
  157. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/rewards_tui.py +0 -0
  158. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/skills_dialog.py +0 -0
  159. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/subagents_dialog.py +0 -0
  160. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/tasks_dialog.py +0 -0
  161. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/thinking_display.py +0 -0
  162. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/tools_dialog.py +0 -0
  163. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/tui/update_dialog.py +0 -0
  164. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/update_checker.py +0 -0
  165. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/utilities.py +0 -0
  166. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/utimmodel.txt +0 -0
  167. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/vector_memory.py +0 -0
  168. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/wheel.py +0 -0
  169. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli/workspace.py +0 -0
  170. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli.egg-info/dependency_links.txt +0 -0
  171. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli.egg-info/entry_points.txt +0 -0
  172. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli.egg-info/requires.txt +0 -0
  173. {utim_cli-2.3.22 → utim_cli-2.3.24}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.3.24] - 2026-09-07
4
+
5
+ ### ⚡ Performance
6
+ - Fixed latency issues
7
+
8
+ ## [2.3.23] - 2026-09-07
9
+
10
+ ### 🛠️ Minor Bug Fixes and Enhancement
11
+ - Minor bug fixes and enhancement
12
+
3
13
  ## [2.3.22] - 2026-09-06
4
14
 
5
15
  ### 🛡️ Seamless Rate-Limit Failover to Paid Slugs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 2.3.22
3
+ Version: 2.3.24
4
4
  Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
5
5
  License: Emend AI Proprietary EULA
6
6
  Project-URL: Homepage, https://utim.dev
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "2.3.22"
7
+ version = "2.3.24"
8
8
 
9
9
  description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
10
10
  readme = "README.md"
@@ -1,4 +1,4 @@
1
- """
1
+ """
2
2
  UTIM Developer SDK
3
3
  ------------------
4
4
  Unified autonomous coding agent SDK.
@@ -14,7 +14,7 @@ Quickstart:
14
14
  try:
15
15
  from utim_cli._version import VERSION as __version__
16
16
  except ImportError:
17
- __version__ = "2.3.22"
17
+ __version__ = "2.3.24"
18
18
 
19
19
  # Developer-friendly core classes (similar to `import genai` / `import openai`)
20
20
  from utim_cli.sdk.session import UtimSession as Session
@@ -5,7 +5,7 @@ try:
5
5
  from utim_cli._version import VERSION as __version__
6
6
  except ImportError:
7
7
  # Fallback during first-install / editable-install edge cases
8
- __version__ = "2.3.22"
8
+ __version__ = "2.3.24"
9
9
 
10
10
 
11
11
  import builtins, os, pathlib, urllib.parse, re
@@ -0,0 +1,22 @@
1
+ _PASTE_CHIP_THRESHOLD = 3000
2
+ _PASTE_CHIP_SEQ = [0]
3
+ _PASTE_CHIPS = {}
4
+
5
+
6
+ def _make_paste_chip(data):
7
+ if len(data) >= _PASTE_CHIP_THRESHOLD:
8
+ _PASTE_CHIP_SEQ[0] += 1
9
+ n = _PASTE_CHIP_SEQ[0]
10
+ token = f"[Pasted text #{n} {len(data)} chars]"
11
+ _PASTE_CHIPS[token] = data
12
+ return token
13
+ return None
14
+
15
+
16
+ # Test cases
17
+ print("2921 chars ->", repr(_make_paste_chip("x" * 2921))) # user example
18
+ print("3000 chars ->", repr(_make_paste_chip("x" * 3000))) # at threshold
19
+ print("3001 chars ->", repr(_make_paste_chip("x" * 3001))) # just over
20
+ print("100 chars ->", repr(_make_paste_chip("x" * 100))) # small
21
+ print()
22
+ print("Chips created so far:", list(_PASTE_CHIPS.keys()))
@@ -11,6 +11,6 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "2.3.22"
14
+ VERSION = "2.3.24"
15
15
 
16
16
 
@@ -666,6 +666,71 @@ class Config:
666
666
  # Fallback – raise informative error later when used
667
667
  return ""
668
668
 
669
+ # ── API Key Exhaustion Tracking (24-hour cooldown) ────────────────────────
670
+
671
+ def mark_api_key_exhausted(self, api_key: str, duration_hours: int = 24) -> None:
672
+ """Mark an API key as exhausted due to rate limiting/429 errors.
673
+
674
+ Args:
675
+ api_key: The API key that got exhausted
676
+ duration_hours: How long to exclude this key (default 24 hours)
677
+ """
678
+ if not api_key:
679
+ return
680
+ exhausted = self.get("_api_key_exhausted", {})
681
+ exhausted[api_key] = time.time() + (duration_hours * 3600)
682
+ self.set("_api_key_exhausted", exhausted)
683
+ logger.info(f"API key marked exhausted for {duration_hours}h: {api_key[:8]}...")
684
+
685
+ def is_api_key_exhausted(self, api_key: str) -> bool:
686
+ """Check if an API key is currently exhausted (rate-limited).
687
+
688
+ Args:
689
+ api_key: The API key to check
690
+
691
+ Returns:
692
+ True if exhausted and within cooldown period, False otherwise
693
+ """
694
+ if not api_key:
695
+ return False
696
+ exhausted = self.get("_api_key_exhausted", {})
697
+ expiry = exhausted.get(api_key, 0)
698
+ if time.time() < expiry:
699
+ return True
700
+ # Clean up expired entry
701
+ if api_key in exhausted:
702
+ del exhausted[api_key]
703
+ self.set("_api_key_exhausted", exhausted)
704
+ return False
705
+
706
+ def get_available_api_keys(self, keys: List[str]) -> List[str]:
707
+ """Filter a list of API keys, excluding exhausted ones.
708
+
709
+ Args:
710
+ keys: List of API keys to filter
711
+
712
+ Returns:
713
+ List of non-exhausted API keys
714
+ """
715
+ return [k for k in keys if k and not self.is_api_key_exhausted(k)]
716
+
717
+ def get_exhaustion_countdown(self, api_key: str) -> Optional[int]:
718
+ """Get remaining seconds until an API key can be used again.
719
+
720
+ Args:
721
+ api_key: The API key to check
722
+
723
+ Returns:
724
+ Seconds remaining, or None if not exhausted
725
+ """
726
+ if not api_key:
727
+ return None
728
+ exhausted = self.get("_api_key_exhausted", {})
729
+ expiry = exhausted.get(api_key, 0)
730
+ remaining = int(expiry - time.time())
731
+ return remaining if remaining > 0 else None
732
+
733
+
669
734
  # Global config instance
670
735
  config = Config()
671
736
  # Convenience constant for direct access
@@ -0,0 +1,43 @@
1
+ import os
2
+
3
+ DEFAULT_MODEL = "poolside/laguna-s-2.1:free"
4
+ SERVER_URL = os.getenv("UTIM_SERVER_URL", "https://api.utim.dev")
5
+ OPENROUTER_BASE_URL = os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")
6
+
7
+ SUBAGENT_DEFAULTS = {
8
+ "web_search": DEFAULT_MODEL,
9
+ "plan_project": DEFAULT_MODEL,
10
+ "generate_image": "qwen/qwen-image-3",
11
+ "analyze_image": "google/gemma-4-31b-it:free",
12
+ }
13
+
14
+ APINEX_MODEL_IDS = {
15
+ "claude/opus-5",
16
+ "claude/sonnet-5",
17
+ "deepseek/v4-flash-0731",
18
+ "deepseek/v4-pro-0813",
19
+ "free/deepseek-v4-flash-0731",
20
+ "free/deepseek-v4-pro-0813",
21
+ "free/glm-5.3-flash",
22
+ "free/gemini-3.1-pro",
23
+ "free/gemini-3.7-flash",
24
+ "gemini/3.1-pro",
25
+ "gemini/3.7-flash",
26
+ "kimi/k3",
27
+ "gpt/5.6-luna",
28
+ "gpt/5.6-sol",
29
+ "gpt/5.6-terra",
30
+ "glm/5.3",
31
+ "glm/5.3-flash",
32
+ "grok/4.6",
33
+ "qwen/3.8-max",
34
+ "free/qwen-3.8-max",
35
+ }
36
+
37
+
38
+ import sys
39
+ _IS_LEGACY_WIN = (sys.platform == "win32" and "WT_SESSION" not in os.environ)
40
+ PROMPT_SYMBOL = ">" if _IS_LEGACY_WIN else "❯"
41
+ ARROW_SYMBOL = "->" if _IS_LEGACY_WIN else "➔"
42
+
43
+
@@ -36,6 +36,56 @@ def get_compression_fallback_models() -> List[str]:
36
36
  return COMPRESSION_FALLBACK_MODELS
37
37
 
38
38
 
39
+ # ── Compression level runtime state ──────────────────────────────────────────
40
+ # These are mutated by apply_compression_level() (called from /compression dialog)
41
+ # and read by _progressive_keep_ratio() and build_compacted_history_message().
42
+ #
43
+ # Defaults match the "medium" preset:
44
+ # oldest_ratio=0.07, newest_ratio=0.70, merge_count=5
45
+ _COMPRESSION_PARAMS: Dict[str, Any] = {
46
+ "oldest_ratio": 0.07,
47
+ "newest_ratio": 0.70,
48
+ "merge_count": 5,
49
+ "level_id": "medium",
50
+ }
51
+
52
+
53
+ def apply_compression_level(level_id: str) -> None:
54
+ """Apply a named compression level by updating the runtime params dict.
55
+
56
+ Called immediately after the user picks a level in /compression dialog.
57
+ The change takes effect on the *next* call to build_compacted_history_message.
58
+ """
59
+ # Map level_id → (oldest_ratio, newest_ratio, merge_count)
60
+ LEVEL_MAP = {
61
+ "very_low": (0.50, 0.95, 0),
62
+ "low": (0.25, 0.85, 2),
63
+ "medium": (0.07, 0.70, 5),
64
+ "high": (0.03, 0.50, 8),
65
+ "very_high": (0.01, 0.30, 12),
66
+ }
67
+ if level_id not in LEVEL_MAP:
68
+ return
69
+ oldest, newest, merge = LEVEL_MAP[level_id]
70
+ _COMPRESSION_PARAMS["oldest_ratio"] = oldest
71
+ _COMPRESSION_PARAMS["newest_ratio"] = newest
72
+ _COMPRESSION_PARAMS["merge_count"] = merge
73
+ _COMPRESSION_PARAMS["level_id"] = level_id
74
+
75
+
76
+ def _load_compression_level_from_config() -> None:
77
+ """Seed _COMPRESSION_PARAMS from persisted config on first use."""
78
+ try:
79
+ from utim_cli.config import config
80
+ level_id = config.get("compression_level") or "medium"
81
+ apply_compression_level(level_id)
82
+ except Exception:
83
+ pass
84
+
85
+
86
+
87
+
88
+
39
89
 
40
90
 
41
91
 
@@ -1246,8 +1296,13 @@ def summarize_single_turn_outcome(turn_msgs: List[Dict], tier: str = "medium") -
1246
1296
  if cl and cl not in tool_outputs_summary and len(cl) < 80:
1247
1297
  tool_outputs_summary.append(cl)
1248
1298
 
1249
- # 1. Extract clean sentences from assistant text
1250
- base_text = assistant_texts[-1] if assistant_texts else ""
1299
+ # 1. Extract clean sentences from assistant text (combine across continuations if present)
1300
+ if len(assistant_texts) >= 2:
1301
+ base_text = " ".join(assistant_texts[-2:])
1302
+ elif assistant_texts:
1303
+ base_text = assistant_texts[-1]
1304
+ else:
1305
+ base_text = ""
1251
1306
  if not base_text and compression_notes:
1252
1307
  note = compression_notes[-1]
1253
1308
  m_goal = re.search(r'## Completed\s*\n(.*?)(?=\n##|\Z)', note, re.DOTALL)
@@ -1314,15 +1369,192 @@ def summarize_single_turn_outcome(turn_msgs: List[Dict], tier: str = "medium") -
1314
1369
  return "\n " + "\n ".join(detail_lines)
1315
1370
 
1316
1371
 
1372
+ def _progressive_keep_ratio(idx_0based: int, total_turns: int) -> float:
1373
+ """Return the fraction of assistant text to keep for a given turn.
1374
+
1375
+ Progressive compression curve driven by the user-chosen compression level:
1376
+ - idx=0 (oldest) → _COMPRESSION_PARAMS["oldest_ratio"]
1377
+ - idx=N-1 (newest) → _COMPRESSION_PARAMS["newest_ratio"]
1378
+ - Linear interpolation for all turns in between.
1379
+
1380
+ When there is only one turn the ratio is always newest_ratio (near-verbatim).
1381
+ """
1382
+ oldest = _COMPRESSION_PARAMS["oldest_ratio"] # e.g. 0.07 for medium
1383
+ newest = _COMPRESSION_PARAMS["newest_ratio"] # e.g. 0.70 for medium
1384
+ if total_turns <= 1:
1385
+ return newest
1386
+ t = idx_0based / (total_turns - 1) # 0.0 → oldest, 1.0 → newest
1387
+ return oldest + (newest - oldest) * t # lerp
1388
+
1389
+
1390
+ def _compress_turn_text(text: str, keep_ratio: float) -> str:
1391
+ """Truncate assistant text to ``keep_ratio`` of its original length at a
1392
+ sentence/newline boundary so no sentence is split in half.
1393
+
1394
+ Continuity anchors (next step, todo, error, file paths) are always
1395
+ preserved in full as a closing addendum even if they fall outside the
1396
+ budget.
1397
+ """
1398
+ if not text:
1399
+ return text
1400
+ budget = max(80, int(len(text) * keep_ratio))
1401
+ if len(text) <= budget:
1402
+ return text
1403
+
1404
+ # Find the last sentence/newline boundary within budget
1405
+ cut = budget
1406
+ for sep in ("\n\n", "\n", ". ", "! ", "? "):
1407
+ pos = text.rfind(sep, 0, budget)
1408
+ if pos > 0:
1409
+ cut = pos + len(sep)
1410
+ break
1411
+
1412
+ kept = text[:cut].rstrip()
1413
+
1414
+ # Always append continuity anchors even if they were trimmed off
1415
+ anchor_lines = []
1416
+ for line in text[cut:].splitlines():
1417
+ ls = line.strip()
1418
+ if ls and re.search(
1419
+ r"(?i)(next step|todo|to-do|pending|remaining|blocker|continue|error|"
1420
+ r"exception|failed|fixed|modified|created|file:|path:|def |class )",
1421
+ ls,
1422
+ ):
1423
+ anchor_lines.append(ls)
1424
+ if anchor_lines:
1425
+ kept = kept + "\n[…] " + " | ".join(anchor_lines[:3])
1426
+
1427
+ omitted = len(text) - len(kept)
1428
+ if omitted > 30:
1429
+ kept += f"\n[{omitted:,} chars omitted — {int((1 - keep_ratio) * 100)}% compressed]"
1430
+
1431
+ return kept
1432
+
1433
+
1434
+ def _merge_oldest_turns(turns: List, merge_count: int) -> str:
1435
+ """Collapse the ``merge_count`` oldest turns into one dense merged entry.
1436
+
1437
+ Produces a single bullet block: what each user asked + one-line outcome.
1438
+ """
1439
+ lines = ["[Merged earliest turns — oldest context, highest compression]"]
1440
+ for i, (prompt, msgs) in enumerate(turns[:merge_count], 1):
1441
+ # User prompt: one line, word-boundary capped at 120 chars
1442
+ prompt_lines = [l.strip() for l in prompt.splitlines() if l.strip()]
1443
+ clean_prompt = prompt_lines[0] if prompt_lines else prompt.strip()
1444
+ if len(clean_prompt) > 120:
1445
+ clean_prompt = clean_prompt[:120].rsplit(" ", 1)[0] + "…"
1446
+
1447
+ outcome = summarize_single_turn_outcome(msgs, tier="low")
1448
+ # Apply the lowest ratio (7%) to these merged entries
1449
+ outcome = _compress_turn_text(outcome, keep_ratio=0.07)
1450
+ lines.append(f" • Turn {i}: \"{clean_prompt}\" → {outcome}")
1451
+ return "\n".join(lines)
1452
+
1453
+
1454
+
1455
+ def is_continuation_prompt(prompt_or_msg: Any) -> bool:
1456
+ """Check if a prompt or user message is simply a continuation nudge or inquiry about
1457
+ an abrupt stop (e.g. 'continue', 'why are u stopping', 'keep going', 'proceed'),
1458
+ rather than a distinct new task.
1459
+
1460
+ When True, the compression pipeline folds this interaction into the parent task
1461
+ turn instead of incrementing the turn count and prematurely pushing real tasks
1462
+ into aggressive compression/merging.
1463
+ """
1464
+ if not prompt_or_msg:
1465
+ return False
1466
+
1467
+ if isinstance(prompt_or_msg, dict):
1468
+ if prompt_or_msg.get("role") != "user":
1469
+ return False
1470
+ content = prompt_or_msg.get("content") or ""
1471
+ else:
1472
+ content = prompt_or_msg
1473
+
1474
+ if isinstance(content, list):
1475
+ content = " ".join(p.get("text", "") for p in content if isinstance(p, dict))
1476
+ elif not isinstance(content, str):
1477
+ content = str(content)
1478
+
1479
+ s = content.strip()
1480
+ if not s:
1481
+ return False
1482
+
1483
+ # 1. Automated continuation nudges & wrappers
1484
+ if s.startswith(("CONTINUE:", "[CONTINUE", "[Continue", "<CONTINUATION_REQUEST", "CONTINUE ")):
1485
+ return True
1486
+ if "CONTINUE: Your last message" in s or "CONTINUE: Please continue" in s:
1487
+ return True
1488
+
1489
+ # Strip surrounding quotes, punctuation and trailing emojis/whitespace
1490
+ cleaned = re.sub(r'^[^\w]+|[^\w]+$', '', s).strip().lower()
1491
+
1492
+ # If the message is long (> 160 chars), it is likely a substantive new prompt with new instructions
1493
+ if len(cleaned) > 160:
1494
+ return False
1495
+
1496
+ # 2. Exact single-word or short continuation keywords
1497
+ EXACT_CONTINUE_PHRASES = {
1498
+ "c", "continue", "pls continue", "please continue", "continue please",
1499
+ "pls continue the task", "please continue the task", "continue the task",
1500
+ "continue task", "continue working", "continue execution", "continue generating",
1501
+ "keep going", "please keep going", "pls keep going", "keep going please",
1502
+ "go on", "please go on", "pls go on",
1503
+ "proceed", "please proceed", "pls proceed",
1504
+ "carry on", "please carry on", "pls carry on",
1505
+ "resume", "please resume", "pls resume", "resume task", "resume working",
1506
+ "keep at it", "keep working", "keep running",
1507
+ "finish", "finish it", "finish this", "finish the task", "complete it", "complete this", "complete the task",
1508
+ "dont stop", "don't stop", "do not stop", "stop stopping",
1509
+ "more", "next", "go ahead",
1510
+ "continue from where you left off", "continue from last step", "continue from before",
1511
+ "please continue from where you left off", "pls continue from where you left off",
1512
+ }
1513
+ if cleaned in EXACT_CONTINUE_PHRASES:
1514
+ return True
1515
+
1516
+ # 3. Questions asking why the model stopped / paused / cut off
1517
+ STOP_INQUIRY_PATTERNS = [
1518
+ r"^why\s+(?:did\s+(?:you|u|it)|are\s+(?:you|u)|have\s+you|is\s+it)\s+(?:stop(?:ped|ping)?|pause[d]?|halt(?:ed)?|cut\s*off)[\s\w?!.]*$",
1519
+ r"^why\s+(?:stop(?:ped|ping)?|paused?|halted?|cut\s*off)[\s\w?!.]*$",
1520
+ r"^(?:you|u|it)\s+(?:stopped|paused|halted|cut\s*off)[\s\w?!.]*$",
1521
+ r"^(?:did\s+(?:you|u)|why\s+did\s+(?:you|u))\s+(?:freeze|crash|quit|give\s*up)[\s\w?!.]*$",
1522
+ r"^(?:what\s+happened(?:\s+why\s+did\s+(?:you|u)\s+stop)?)[\s\w?!.]*$",
1523
+ r"^(?:why\s+didn'?t\s+(?:you|u)\s+(?:finish|complete|continue))[\s\w?!.]*$",
1524
+ r"^(?:why\s+did\s+(?:you|u)\s+not\s+(?:finish|complete|continue))[\s\w?!.]*$",
1525
+ ]
1526
+ for pat in STOP_INQUIRY_PATTERNS:
1527
+ if re.search(pat, cleaned):
1528
+ return True
1529
+
1530
+ # 4. General continue regex pattern
1531
+ if re.match(r"^(?:please\s+|pls\s+)?(?:continue|keep\s+going|go\s+on|proceed|resume|carry\s+on)(?:\s+(?:the\s+task|working|generating|code|it|with\s+(?:this|it|the\s+task)|from\s+(?:where\s+you\s+left\s+off|here|before)))?[\s.!?]*$", cleaned):
1532
+ return True
1533
+
1534
+ return False
1535
+
1536
+
1317
1537
  def build_compacted_history_message(past_messages: List[Dict]) -> Optional[Dict]:
1318
- """Consolidates all previous turns into a single milestone message with complete semantic sentences."""
1538
+ """Consolidate all previous turns into a single milestone message using
1539
+ **progressive compression** so the model retains rich recent context while
1540
+ oldest turns are heavily summarised to save tokens.
1541
+
1542
+ Compression curve (per turn position 1..N):
1543
+ • Turns 1–5 (oldest block) → merged into **one** combined entry at ~7%
1544
+ • Turn 6 onwards → individual entries with keep-ratio interpolating
1545
+ linearly from ~7% (oldest remaining) up to ~70% (Turn N, most recent).
1546
+ • Turn N+1 is the live current turn and is NEVER touched here.
1547
+
1548
+ Continuity anchors (next-step hints, file paths, errors) are always
1549
+ appended even if they fall outside the compression budget.
1550
+ """
1319
1551
  if not past_messages:
1320
1552
  return None
1321
1553
 
1322
- # Partition past_messages into turns by user messages
1323
- turns = []
1324
- current_user_prompt = None
1325
- current_turn_msgs = []
1554
+ # ── 1. Partition messages into (prompt, [msgs]) turns ─────────────────────
1555
+ turns: List = []
1556
+ current_user_prompt: Optional[str] = None
1557
+ current_turn_msgs: List = []
1326
1558
 
1327
1559
  for m in past_messages:
1328
1560
  role = m.get("role", "")
@@ -1342,6 +1574,12 @@ def build_compacted_history_message(past_messages: List[Dict]) -> Optional[Dict]
1342
1574
  current_turn_msgs.append(m)
1343
1575
  continue
1344
1576
 
1577
+ # Check if this user message is just a continuation nudge of the previous task
1578
+ if current_user_prompt is not None and is_continuation_prompt(content):
1579
+ # Fold into current turn rather than starting an extra turn!
1580
+ current_turn_msgs.append(m)
1581
+ continue
1582
+
1345
1583
  if current_user_prompt is not None:
1346
1584
  turns.append((current_user_prompt, current_turn_msgs))
1347
1585
  current_user_prompt = content.strip()
@@ -1350,42 +1588,90 @@ def build_compacted_history_message(past_messages: List[Dict]) -> Optional[Dict]
1350
1588
  if current_user_prompt is not None:
1351
1589
  current_turn_msgs.append(m)
1352
1590
 
1591
+
1353
1592
  if current_user_prompt is not None:
1354
1593
  turns.append((current_user_prompt, current_turn_msgs))
1355
1594
 
1356
1595
  if not turns:
1357
1596
  return None
1358
1597
 
1359
- turn_entries = []
1360
- total_turns = len(turns)
1598
+ # Seed _COMPRESSION_PARAMS from config on the very first call (lazy init)
1599
+ if _COMPRESSION_PARAMS.get("level_id") == "medium":
1600
+ _load_compression_level_from_config()
1361
1601
 
1362
- for idx, (prompt, msgs) in enumerate(turns, 1):
1363
- dist = total_turns - idx
1364
- tier = "high" if dist <= 1 else ("medium" if dist <= 4 else "low")
1602
+ # ── 2. Decide merge boundary ───────────────────────────────────────────────
1603
+ # Use the user-chosen merge count from _COMPRESSION_PARAMS.
1604
+ # Clamped so we never merge all turns (always keep >= 1 individual entry).
1605
+ total_turns = len(turns)
1606
+ level_merge = _COMPRESSION_PARAMS.get("merge_count", 5) # 0, 2, 5, 8, or 12
1607
+ merge_count = min(level_merge, max(0, total_turns - 1))
1608
+ if total_turns <= 3:
1609
+ merge_count = 0 # too few turns — keep all individually
1610
+
1611
+ # ── 3. Build turn entries ──────────────────────────────────────────────────
1612
+ turn_entries: List[str] = []
1613
+
1614
+ if merge_count > 0:
1615
+ merged_block = _merge_oldest_turns(turns, merge_count)
1616
+ turn_entries.append(merged_block)
1617
+
1618
+ # Individual turns after the merged block — apply progressive compression
1619
+ individual_turns = turns[merge_count:]
1620
+ ind_total = len(individual_turns)
1621
+
1622
+ for local_idx, (prompt, msgs) in enumerate(individual_turns):
1623
+ # Global index within the full history (for labelling)
1624
+ global_idx = merge_count + local_idx + 1 # 1-based
1625
+
1626
+ # Keep ratio grows from near-low to 70 % as turns approach N
1627
+ keep_ratio = _progressive_keep_ratio(local_idx, ind_total)
1628
+
1629
+ # Summarisation tier mirrors the keep ratio
1630
+ if keep_ratio >= 0.55:
1631
+ tier = "high"
1632
+ elif keep_ratio >= 0.25:
1633
+ tier = "medium"
1634
+ else:
1635
+ tier = "low"
1365
1636
 
1366
- # Extract first clean line/sentence of user prompt without slicing mid-word
1637
+ # User prompt: truncate cleanly at word boundary
1367
1638
  prompt_lines = [l.strip() for l in prompt.splitlines() if l.strip()]
1368
1639
  clean_prompt = prompt_lines[0] if prompt_lines else prompt.strip()
1369
- if len(clean_prompt) > 200:
1370
- # Clean break at word boundary
1371
- clean_prompt = clean_prompt[:200].rsplit(' ', 1)[0]
1640
+ # Prompt char budget also scales: 60 chars oldest → 200 chars newest
1641
+ prompt_budget = max(60, int(60 + (200 - 60) * (local_idx / max(1, ind_total - 1))))
1642
+ if len(clean_prompt) > prompt_budget:
1643
+ clean_prompt = clean_prompt[:prompt_budget].rsplit(" ", 1)[0] + "…"
1372
1644
 
1373
1645
  outcome = summarize_single_turn_outcome(msgs, tier=tier)
1646
+ outcome = _compress_turn_text(outcome, keep_ratio=keep_ratio)
1374
1647
 
1648
+ pct_label = f"{int(keep_ratio * 100)}% kept"
1375
1649
  if tier == "high" and "\n" in outcome:
1376
- turn_entries.append(f"- **Turn {idx} (User)**: \"{clean_prompt}\"\n ↳ **Assistant**:{outcome}")
1650
+ turn_entries.append(
1651
+ f"- **Turn {global_idx} (User)** [{pct_label}]: \"{clean_prompt}\"\n"
1652
+ f" ↳ **Assistant**:{outcome}"
1653
+ )
1377
1654
  else:
1378
- turn_entries.append(f"- **Turn {idx} (User)**: \"{clean_prompt}\"\n ↳ **Assistant**: {outcome}")
1655
+ turn_entries.append(
1656
+ f"- **Turn {global_idx} (User)** [{pct_label}]: \"{clean_prompt}\"\n"
1657
+ f" ↳ **Assistant**: {outcome}"
1658
+ )
1659
+
1660
+ cur_oldest_pct = int(_COMPRESSION_PARAMS.get("oldest_ratio", 0.07) * 100)
1661
+ cur_newest_pct = int(_COMPRESSION_PARAMS.get("newest_ratio", 0.70) * 100)
1662
+ cur_level_id = str(_COMPRESSION_PARAMS.get("level_id", "medium")).replace("_", " ").upper()
1379
1663
 
1380
1664
  history_content = (
1381
1665
  "### SESSION HISTORY & PREVIOUS MILESTONES (COMPACTED CONTEXT)\n"
1382
- "The following user requests were completed earlier in this session:\n\n"
1666
+ f"Turns are compressed progressively ({cur_level_id}): oldest ≈{cur_oldest_pct}% kept, newest ≈{cur_newest_pct}% kept.\n\n"
1383
1667
  + "\n\n".join(turn_entries)
1384
1668
  )
1385
1669
 
1386
1670
  return {"role": "user", "content": history_content}
1387
1671
 
1388
1672
 
1673
+
1674
+
1389
1675
  def is_compression_artifact_message(message: Dict) -> bool:
1390
1676
  """Check if message is a synthetic compression note/digest/summary rather than a genuine user instruction."""
1391
1677
  if not isinstance(message, dict):
@@ -1512,6 +1798,9 @@ def score_message_importance(message: Dict, llm_key: str = None) -> float:
1512
1798
  if is_compression_artifact_message(message):
1513
1799
  min_floor = 0.2
1514
1800
  score = 0.35 # Synthetic notes get low score to allow replacement on next pass
1801
+ elif is_continuation_prompt(message):
1802
+ min_floor = 0.3
1803
+ score = 0.40 # Continuation prompts ('continue', 'why are u stopping') can be compressed/digested
1515
1804
  else:
1516
1805
  # Genuine user instructions must NEVER be below the compression threshold
1517
1806
  min_floor = 1.0 # Never compress genuine user instructions
@@ -847,7 +847,8 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
847
847
  t_llm_start = time.time()
848
848
  msg, was_streamed = self._call_llm(
849
849
  send_msgs,
850
- override_tools=override_tools or getattr(self, "_subagent_tool_override", None)
850
+ override_tools=override_tools or getattr(self, "_subagent_tool_override", None),
851
+ silent=False,
851
852
  )
852
853
  reasoning_duration = time.time() - t_llm_start
853
854
 
@@ -855,12 +856,19 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
855
856
  except _ServerUnavailableError as exc:
856
857
  _err_text = str(exc).lower()
857
858
  if "rate limit" in _err_text or "rate-limit" in _err_text or "429" in _err_text or "too many requests" in _err_text:
858
- self.console.print(f"\n[bold yellow]⏳ Model is temporarily rate-limited by the provider.[/bold yellow]")
859
+ # Mark the current API key as exhausted for 24 hours
860
+ from utim_cli.config import config
861
+ current_key = config.get("api_key", "")
862
+ if current_key:
863
+ config.mark_api_key_exhausted(current_key, duration_hours=24)
864
+ self.console.print(f"\n[bold yellow]⏳ Rate-limited on API key {current_key[:12]}... — excluded for 24 hours.[/bold yellow]")
865
+ else:
866
+ self.console.print(f"\n[bold yellow]⏳ Model is temporarily rate-limited by the provider.[/bold yellow]")
859
867
  self.console.print(" [dim]Please wait a moment before retrying, or switch models using [bold]/model[/bold] (Ctrl+M).[/dim]\n")
860
868
  if len(self.messages) <= turn_msg_start + 1:
861
869
  self.messages.append({
862
870
  "role": "assistant",
863
- "content": "⏳ **Model Rate Limited**: The model is temporarily rate-limited by the provider. Please wait a short moment and retry, or use `/model` to switch models."
871
+ "content": "⏳ **Rate Limited**: The API key has been temporarily exhausted (24h cooldown). Please wait and retry, or use `/model` to switch models."
864
872
  })
865
873
  self._persist_messages()
866
874
  return