utim-cli 2.3.13__tar.gz → 2.3.14__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 (160) hide show
  1. {utim_cli-2.3.13 → utim_cli-2.3.14}/CHANGELOG.md +7 -0
  2. {utim_cli-2.3.13/utim_cli.egg-info → utim_cli-2.3.14}/PKG-INFO +1 -1
  3. {utim_cli-2.3.13 → utim_cli-2.3.14}/pyproject.toml +1 -1
  4. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim/__init__.py +1 -1
  5. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/__init__.py +1 -1
  6. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/_version.py +1 -1
  7. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/ask_helper.py +1 -1
  8. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/auth.py +2 -2
  9. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/brain.py +36 -2
  10. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/context_pruner.py +1 -1
  11. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/orchestrator.py +19 -43
  12. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/orchestrator_llm_transport.py +40 -19
  13. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/reflection.py +33 -4
  14. utim_cli-2.3.14/utim_cli/server/key_rotator.py +710 -0
  15. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/router.py +11 -1
  16. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/routes/auth_routes.py +21 -11
  17. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/routes/completion_routes.py +136 -98
  18. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/routes/credit_routes.py +26 -9
  19. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/routes/marketplace_routes.py +17 -0
  20. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tools.py +6 -19
  21. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/marketplace_dialog.py +8 -3
  22. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/model_dialog.py +17 -7
  23. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/utim.py +84 -67
  24. {utim_cli-2.3.13 → utim_cli-2.3.14/utim_cli.egg-info}/PKG-INFO +1 -1
  25. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli.egg-info/SOURCES.txt +1 -0
  26. {utim_cli-2.3.13 → utim_cli-2.3.14}/LICENSE +0 -0
  27. {utim_cli-2.3.13 → utim_cli-2.3.14}/MANIFEST.in +0 -0
  28. {utim_cli-2.3.13 → utim_cli-2.3.14}/README.md +0 -0
  29. {utim_cli-2.3.13 → utim_cli-2.3.14}/setup.cfg +0 -0
  30. {utim_cli-2.3.13 → utim_cli-2.3.14}/setup.py +0 -0
  31. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/_verify_repro.py +0 -0
  32. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/_verify_syntax.py +0 -0
  33. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/agent.py +0 -0
  34. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/backup.py +0 -0
  35. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/billing.py +0 -0
  36. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/blender_agent.py +0 -0
  37. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/bootstrap.py +0 -0
  38. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/client_utils.py +0 -0
  39. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/config.py +0 -0
  40. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/constants.py +0 -0
  41. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/crawler/__init__.py +0 -0
  42. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/crawler/engine.py +0 -0
  43. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/crawler/markdown_distiller.py +0 -0
  44. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/dependency_manager.py +0 -0
  45. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/doctor.py +0 -0
  46. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/evidence_registry.py +0 -0
  47. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/harbor.py +0 -0
  48. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/knowledge_graph.py +0 -0
  49. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/local_db.py +0 -0
  50. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/logger.py +0 -0
  51. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/mcp_clean_wrapper.py +0 -0
  52. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/mcp_client.py +0 -0
  53. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/mcp_registry.json +0 -0
  54. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/models.txt +0 -0
  55. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/orchestrator_helpers.py +0 -0
  56. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/orchestrator_tool_exec.py +0 -0
  57. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/orchestrator_turn_history.py +0 -0
  58. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/personalities.py +0 -0
  59. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/platform_installer.py +0 -0
  60. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/plugins/__init__.py +0 -0
  61. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/plugins/hooks.py +0 -0
  62. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/plugins/manager.py +0 -0
  63. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/plugins/registry.py +0 -0
  64. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/plugins/schema.py +0 -0
  65. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/plugins/tool_loader.py +0 -0
  66. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/report.py +0 -0
  67. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/scrapy_search.py +0 -0
  68. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/sdk/__init__.py +0 -0
  69. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/sdk/config.py +0 -0
  70. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/sdk/context.py +0 -0
  71. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/sdk/events.py +0 -0
  72. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/sdk/handlers.py +0 -0
  73. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/sdk/session.py +0 -0
  74. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/sdk/sidecar.py +0 -0
  75. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/__init__.py +0 -0
  76. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/admin_auth.py +0 -0
  77. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/attribution.py +0 -0
  78. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/audit_log.py +0 -0
  79. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/auth.py +0 -0
  80. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/batch_processor.py +0 -0
  81. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/captcha.py +0 -0
  82. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/cli_auth.py +0 -0
  83. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/concurrency.py +0 -0
  84. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/db.py +0 -0
  85. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  86. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/docs_md/about.md +0 -0
  87. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/docs_md/changelog.md +0 -0
  88. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/docs_md/docs.md +0 -0
  89. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/docs_md/features.md +0 -0
  90. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/docs_md/license.md +0 -0
  91. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/docs_md/pricing.md +0 -0
  92. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/docs_md/privacy.md +0 -0
  93. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/docs_md/refund.md +0 -0
  94. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/docs_md/support.md +0 -0
  95. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/docs_md/terms.md +0 -0
  96. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/email_utils.py +0 -0
  97. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/exchange_rate.py +0 -0
  98. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/firebase.py +0 -0
  99. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/fix_duplicate_users.py +0 -0
  100. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/history.py +0 -0
  101. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/local_llm.py +0 -0
  102. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/logging_config.py +0 -0
  103. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/micro_batcher.py +0 -0
  104. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/model_agent.py +0 -0
  105. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/models.py +0 -0
  106. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/pricing_updater.py +0 -0
  107. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/provision_build.py +0 -0
  108. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/rate_limit.py +0 -0
  109. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/rewards_engine.py +0 -0
  110. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/routes/__init__.py +0 -0
  111. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/routes/admin_db_routes.py +0 -0
  112. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/routes/billing_routes.py +0 -0
  113. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/routes/feedback_routes.py +0 -0
  114. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/routes/quota_routes.py +0 -0
  115. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/routes/quota_share_routes.py +0 -0
  116. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/routes/referral_routes.py +0 -0
  117. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/routes/rewards_routes.py +0 -0
  118. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/routes/security_routes.py +0 -0
  119. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/routes/session_routes.py +0 -0
  120. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/routes/share_routes.py +0 -0
  121. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/server.py +0 -0
  122. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/server/storage_nodes.py +0 -0
  123. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/share.py +0 -0
  124. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/share_tui.py +0 -0
  125. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/situational_scoring.py +0 -0
  126. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/state.py +0 -0
  127. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/subagent_manager.py +0 -0
  128. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/task_dispatcher.py +0 -0
  129. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/task_logger.py +0 -0
  130. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tools_grep.py +0 -0
  131. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/__init__.py +0 -0
  132. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/feedback_dialog.py +0 -0
  133. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/history_dialog.py +0 -0
  134. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/marketplace_app_state.py +0 -0
  135. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  136. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/mcp_dialog.py +0 -0
  137. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/miniagents_dialog.py +0 -0
  138. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/personality_dialog.py +0 -0
  139. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/plugins_dialog.py +0 -0
  140. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/publish_dialog.py +0 -0
  141. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/quota_dialog.py +0 -0
  142. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  143. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/quota_share_dialog.py +0 -0
  144. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/resume_dialog.py +0 -0
  145. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/rewards_tui.py +0 -0
  146. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/skills_dialog.py +0 -0
  147. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/subagents_dialog.py +0 -0
  148. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/tasks_dialog.py +0 -0
  149. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/thinking_display.py +0 -0
  150. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/tools_dialog.py +0 -0
  151. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/tui/update_dialog.py +0 -0
  152. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/utilities.py +0 -0
  153. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/utimmodel.txt +0 -0
  154. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/vector_memory.py +0 -0
  155. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/wheel.py +0 -0
  156. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli/workspace.py +0 -0
  157. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli.egg-info/dependency_links.txt +0 -0
  158. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli.egg-info/entry_points.txt +0 -0
  159. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli.egg-info/requires.txt +0 -0
  160. {utim_cli-2.3.13 → utim_cli-2.3.14}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.3.14] - 2026-08-28
4
+
5
+ ### ⚡ Request Queue & Scaled Free Capacity
6
+ - Added request queue visual, Increased global free user request cap from 20RPM - 60RPM
7
+ - Increase per user Rpm cap from 10-30RPM
8
+ - Added a new fun feature "/personality"
9
+
3
10
  ## [2.3.13] - 2026-08-28
4
11
 
5
12
  ### 🐞 Minor Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 2.3.13
3
+ Version: 2.3.14
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.13"
7
+ version = "2.3.14"
8
8
 
9
9
  description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
10
10
  readme = "README.md"
@@ -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.13"
17
+ __version__ = "2.3.14"
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.13"
8
+ __version__ = "2.3.14"
9
9
 
10
10
 
11
11
  import builtins, os, pathlib, urllib.parse, re
@@ -11,6 +11,6 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "2.3.13"
14
+ VERSION = "2.3.14"
15
15
 
16
16
 
@@ -389,7 +389,7 @@ def execute_ask_helper(
389
389
  },
390
390
  stream=False,
391
391
  timeout=(15, 300),
392
- is_reflection=True,
392
+ is_reflection=False,
393
393
  )
394
394
  if resp.status_code == 429:
395
395
  continue
@@ -321,7 +321,7 @@ class _AuthCallbackHandler(http.server.BaseHTTPRequestHandler):
321
321
 
322
322
  # ── Public login() function ───────────────────────────────────────────────────
323
323
 
324
- def login(restart: bool = True) -> None:
324
+ def login(restart: bool = False) -> None:
325
325
  """
326
326
  Sign in via the Device Authorization Flow.
327
327
 
@@ -504,8 +504,8 @@ def login(restart: bool = True) -> None:
504
504
  raw_print(f" Signed in as {BLUE}{body.get('email', '')}{RESET}")
505
505
  if "credits" in body:
506
506
  raw_print(f" Credits: {body.get('credits', 0):,.0f} UTIM")
507
- raw_print(f" {DIM}Restarting UTIM CLI…{RESET}")
508
507
  if restart:
508
+ raw_print(f" {DIM}Restarting UTIM CLI…{RESET}")
509
509
  restart_process()
510
510
  # If restart wasn't called, fall through and let the main loop exit.
511
511
 
@@ -353,6 +353,13 @@ def store_brain_memory(
353
353
  if not content or not content.strip():
354
354
  return ""
355
355
 
356
+ try:
357
+ from utim_cli.tools import is_user_paid
358
+ if not is_user_paid():
359
+ return ""
360
+ except Exception:
361
+ pass
362
+
356
363
  pid = project_id or _project_id()
357
364
  now = datetime.now().isoformat()
358
365
  clean_title = title or content[:60].replace("\n", " ").strip()
@@ -444,6 +451,13 @@ def retrieve_brain_memories(
444
451
  if not query:
445
452
  return []
446
453
 
454
+ try:
455
+ from utim_cli.tools import is_user_paid
456
+ if not is_user_paid():
457
+ return []
458
+ except Exception:
459
+ pass
460
+
447
461
  pid = project_id or _project_id()
448
462
 
449
463
  # ── Stage 1: Forward pass ─────────────────────────────────────────────
@@ -696,6 +710,13 @@ def index_project_architecture(workspace: str = "."):
696
710
  If yes, store an updated architecture memory in the brain.
697
711
  Maintains exactly one deterministic architecture memory per project.
698
712
  """
713
+ try:
714
+ from utim_cli.tools import is_user_paid
715
+ if not is_user_paid():
716
+ return
717
+ except Exception:
718
+ pass
719
+
699
720
  pid = _project_id(workspace)
700
721
  snap = _snapshot_project_architecture(workspace)
701
722
  snap_hash = hashlib.sha1(snap.encode()).hexdigest()
@@ -776,13 +797,19 @@ _watcher_lock = threading.Lock()
776
797
 
777
798
  def start_brain_watcher():
778
799
  """
779
- Start the background brain watcher daemon thread.
780
- Safe to call multiple times — only one thread is ever started.
800
+ Start the background daemon watcher if not already running.
801
+ Safe to call multiple times.
781
802
  """
782
803
  global _watcher_started
783
804
  with _watcher_lock:
784
805
  if _watcher_started:
785
806
  return
807
+ try:
808
+ from utim_cli.tools import is_user_paid
809
+ if not is_user_paid():
810
+ return
811
+ except Exception:
812
+ pass
786
813
  _watcher_started = True
787
814
 
788
815
  # One-time cleanup of legacy duplicate records on startup
@@ -946,6 +973,13 @@ def get_brain_context_prompt(user_prompt: str = "") -> str:
946
973
  relevant brain project folder matching the project data/query, and injects
947
974
  direct clickable paths into the system prompt.
948
975
  """
976
+ try:
977
+ from utim_cli.tools import is_user_paid
978
+ if not is_user_paid():
979
+ return ""
980
+ except Exception:
981
+ pass
982
+
949
983
  try:
950
984
  pid, proj_dir = find_most_relevant_brain_project(user_prompt)
951
985
  brain_root = _brain_root()
@@ -1496,7 +1496,7 @@ def _call_compression_model_with_fallback(messages: List[Dict], llm_key: str, ma
1496
1496
  },
1497
1497
  stream=False,
1498
1498
  timeout=(15, 300),
1499
- is_reflection=True
1499
+ is_reflection=False
1500
1500
  )
1501
1501
  if resp.status_code == 429:
1502
1502
  # On 429 rate limit, immediately try fallback without blocking
@@ -1010,14 +1010,14 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
1010
1010
 
1011
1011
  # If the model response was cut off mid-turn due to length/token limits, nudge it to continue
1012
1012
  # ONLY trigger when the response was genuinely truncated by token limits (never when finish_reason is "stop" or "end_turn")
1013
+ _fr = msg.get("finish_reason") or "stop"
1013
1014
  _is_truly_token_truncated = (
1014
1015
  not msg.get("aborted")
1015
1016
  and not tool_calls
1016
- and msg.get("finish_reason") not in ("stop", "end_turn", "tool_calls", "repetition_loop")
1017
+ and _fr not in ("stop", "end_turn", "tool_calls", "repetition_loop")
1017
1018
  and (
1018
- msg.get("was_truncated_by_limit")
1019
- or msg.get("finish_reason") in ("length", "max_tokens")
1020
- or (msg.get("was_cut_off") and msg.get("finish_reason") not in ("stop", "end_turn"))
1019
+ _fr in ("length", "max_tokens")
1020
+ or bool(msg.get("was_truncated_by_limit"))
1021
1021
  )
1022
1022
  )
1023
1023
  if _is_truly_token_truncated:
@@ -1067,29 +1067,24 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
1067
1067
  or (_attempted_tool_call and any(t in _MUTATION_TOOLS for t in _recent_tool_names))
1068
1068
  )
1069
1069
 
1070
- # If the model stopped with empty OR trivially short response after running tools
1071
- _is_empty = not content.strip()
1070
+ # If the model stopped with empty OR purely an unperformed action promise
1071
+ _content_clean = content.strip()
1072
+ _content_len = len(_content_clean)
1073
+ _is_empty = (_content_len == 0)
1072
1074
  _lower_content = content.lower()
1073
1075
  _is_lazy_transition = False
1074
1076
  is_planning = False
1075
1077
 
1076
- # ── Fix 3: Never lazy-nudge when a tool call was attempted ──────
1077
- # If we have parse failures, the model DID try to call a tool — it's
1078
- # not lazy. We handle it separately in the parse-failure block below.
1079
- if _attempted_tool_call and not tool_calls:
1080
- # Tool call was attempted but failed to parse or reconstruct — skip lazy logic
1081
- pass
1078
+ # ── Fix: Never lazy-nudge when a tool call was attempted or already executed ──
1079
+ if _attempted_tool_call or _had_tools_this_turn or tool_calls:
1080
+ # The model already executed tools or attempted tools this turn — it's reporting results
1081
+ _is_lazy_transition = False
1082
1082
  else:
1083
- _only_inspection_tools = _had_tools_this_turn and all(t in _INSPECTION_TOOLS for t in _recent_tool_names)
1084
- _content_clean = content.strip()
1085
- _content_len = len(_content_clean)
1086
- _ends_with_cliffhanger = _content_clean.endswith(":") or _content_clean.endswith("...")
1087
-
1088
1083
  # Tail action promise: only genuine cliffhangers at the end of the text (e.g. "I will now edit foo.py:")
1089
1084
  _ACTION_PROMISE_TAIL = [
1090
- r"(?:i(?:'ll| will)|we(?:'ll| will))\s+(?:now\s+)?(?:update|edit|implement|create|fix|write|build|start|run)\b[^.\n]*[:\.\.\.]?$",
1091
- r"let me\s+(?:now\s+)?(?:update|edit|implement|create|fix|write|build|start|run|check)\b[^.\n]*[:\.\.\.]?$",
1092
- r"proceeding to\s+(?:implement|create|build|write|update|run)\b[^.\n]*[:\.\.\.]?$",
1085
+ r"^(?:i(?:'ll| will)|we(?:'ll| will))\s+(?:now\s+)?(?:update|edit|implement|create|fix|write|build|start|run)\b[^\n]*[:\.\.\.]?$",
1086
+ r"^let me\s+(?:now\s+)?(?:update|edit|implement|create|fix|write|build|start|run|check)\b[^\n]*[:\.\.\.]?$",
1087
+ r"^proceeding to\s+(?:implement|create|build|write|update|run)\b[^\n]*[:\.\.\.]?$",
1093
1088
  ]
1094
1089
  _has_tail_action_promise = any(
1095
1090
  re.search(pat, _content_clean, re.IGNORECASE)
@@ -1104,31 +1099,12 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
1104
1099
  "exit code 0", "exit_code: 0", "passes", "passing", "as requested", "here are the findings",
1105
1100
  "here is the result", "here is the breakdown", "here is what i found",
1106
1101
  ])
1107
- _model_already_delivered = bool(_successful_mutations_this_turn)
1108
1102
 
1109
- # Substantive responses (≥ 80 chars) that do not end in a dangling action cliffhanger
1110
- # are treated as completed answers/findings and are NOT nudged.
1111
- if _content_len >= 80 and not _has_tail_action_promise and not _ends_with_cliffhanger:
1103
+ # Only nudge if the ENTIRE message is a short (< 60 chars) unperformed action promise without tools
1104
+ if _content_len < 60 and _has_tail_action_promise and not _has_any_completion_marker and not is_planning:
1105
+ _is_lazy_transition = True
1106
+ else:
1112
1107
  _is_lazy_transition = False
1113
- elif _had_tools_this_turn:
1114
- if _only_inspection_tools and not _attempted_mutations:
1115
- # If the model inspected and returned a short uninformative cliffhanger without answering
1116
- if _content_len < 40 and not _has_any_completion_marker and (_has_tail_action_promise or _ends_with_cliffhanger):
1117
- _is_lazy_transition = True
1118
- else:
1119
- _is_lazy_transition = False
1120
- elif is_planning:
1121
- _is_lazy_transition = _content_len < 20
1122
- else:
1123
- # Nudge ONLY on genuine cliffhangers or empty promises
1124
- _is_lazy_transition = (
1125
- (_has_tail_action_promise or _ends_with_cliffhanger)
1126
- or (_content_len < 30 and not _has_any_completion_marker and not _model_already_delivered)
1127
- )
1128
- elif not _had_tools_this_turn and not _is_empty and not is_planning and not tool_calls:
1129
- # Model did not call any tools. Only nudge if it gave a short cliffhanger promising action.
1130
- if _content_len < 100 and (_has_tail_action_promise or _ends_with_cliffhanger):
1131
- _is_lazy_transition = True
1132
1108
 
1133
1109
  # ── Fix 3/7: Handle tool-call parse failures explicitly ──────────
1134
1110
  # The model emitted a tool call but the JSON was malformed/truncated.
@@ -1193,9 +1193,11 @@ class LLMTransportMixin:
1193
1193
  try:
1194
1194
  if getattr(self, "_subagent_depth", 0) == 0:
1195
1195
  from utim_cli.state import STATE
1196
- STATE["thinking_topic"] = _THINKING_PHASES[_phase_idx % len(_THINKING_PHASES)]
1197
- _phase_idx += 1
1198
- _last_phase_time = now
1196
+ cur_top = STATE.get("thinking_topic", "")
1197
+ if not str(cur_top).startswith("Request Queued"):
1198
+ STATE["thinking_topic"] = _THINKING_PHASES[_phase_idx % len(_THINKING_PHASES)]
1199
+ _phase_idx += 1
1200
+ _last_phase_time = now
1199
1201
  except Exception:
1200
1202
  pass
1201
1203
 
@@ -1240,7 +1242,23 @@ class LLMTransportMixin:
1240
1242
  if "type" in chunk:
1241
1243
  last_content_time = time.time()
1242
1244
  t = chunk["type"]
1243
- if t == "thinking_delta":
1245
+ if t == "queue_status":
1246
+ pos = chunk.get("position", 1)
1247
+ status = chunk.get("status", "queued")
1248
+ if status == "queued":
1249
+ try:
1250
+ from utim_cli.state import STATE
1251
+ STATE["thinking_topic"] = f"Request Queued Position {pos}"
1252
+ except Exception:
1253
+ pass
1254
+ elif status == "processing":
1255
+ try:
1256
+ from utim_cli.state import STATE
1257
+ STATE["thinking_topic"] = "Formulating response..."
1258
+ except Exception:
1259
+ pass
1260
+ continue
1261
+ elif t == "thinking_delta":
1244
1262
  t_text = chunk.get("text", "")
1245
1263
  if t_text:
1246
1264
  if _rep_detector.append_and_check(t_text):
@@ -1339,6 +1357,10 @@ class LLMTransportMixin:
1339
1357
  if not silent:
1340
1358
  self.console.print(f"\n[bold red]Model Error ({current_model}): {err_msg}[/bold red]\n")
1341
1359
  raise RuntimeError(f"Server completion error ({current_model}): {err_msg}")
1360
+ if "finish_reason" in chunk and chunk["finish_reason"]:
1361
+ _stream_finish_reason = chunk["finish_reason"]
1362
+ elif not _stream_finish_reason:
1363
+ _stream_finish_reason = "stop"
1342
1364
  final_content = chunk.get("content") or final_content
1343
1365
  if not final_content and chunk.get("reasoning"):
1344
1366
  final_content = chunk["reasoning"]
@@ -1347,6 +1369,7 @@ class LLMTransportMixin:
1347
1369
  if usage:
1348
1370
  in_t = usage.get("input_tokens", 0)
1349
1371
  out_t = usage.get("output_tokens", 0)
1372
+ _stream_completion_tokens = out_t
1350
1373
  from utim_cli.server.models import estimate_cost
1351
1374
  cost = estimate_cost(current_model, in_t, out_t)
1352
1375
  from utim_cli.client_utils import update_session_usage
@@ -1679,22 +1702,20 @@ class LLMTransportMixin:
1679
1702
  except Exception:
1680
1703
  pass
1681
1704
 
1682
- # Heuristic abrupt-cutoff detector:
1683
- # Only check if finish_reason was NOT an explicit completion ("stop" / "end_turn")
1684
- if not was_cut_off and clean_content and not final_tool_calls and _stream_finish_reason not in ("stop", "end_turn"):
1685
- _backtick_count = clean_content.count("```")
1686
- if _backtick_count % 2 != 0:
1687
- was_cut_off = True # unclosed code fence
1688
-
1689
- # Truncation diagnosis: was the stream cut short by output token limit?
1690
- _max_out = _get_model_max_output(current_model)
1691
- _was_truncated_by_limit = (
1692
- _stream_finish_reason in ("length", "max_tokens")
1693
- or (
1694
- _stream_completion_tokens > 0
1695
- and _stream_completion_tokens >= _max_out - 5
1705
+ # If finish_reason is an explicit completion, response is NOT cut off
1706
+ if _stream_finish_reason in ("stop", "end_turn"):
1707
+ was_cut_off = False
1708
+ _was_truncated_by_limit = False
1709
+ else:
1710
+ # Truncation diagnosis: was the stream cut short by output token limit?
1711
+ _max_out = _get_model_max_output(current_model)
1712
+ _was_truncated_by_limit = (
1713
+ _stream_finish_reason in ("length", "max_tokens")
1714
+ or (
1715
+ _stream_completion_tokens > 0
1716
+ and _stream_completion_tokens >= _max_out - 5
1717
+ )
1696
1718
  )
1697
- )
1698
1719
 
1699
1720
  final_msg = {
1700
1721
  "role": "assistant",
@@ -1374,14 +1374,29 @@ def get_buffered_interactions(limit: int = 5) -> List[Dict]:
1374
1374
  return []
1375
1375
 
1376
1376
 
1377
+ _REFLECTION_KEY_ROTATION_IDX: int = 0
1378
+ _REFLECTION_KEY_ROTATION_LOCK = threading.Lock()
1379
+
1377
1380
  def _get_reflection_key() -> str:
1378
1381
  """
1379
- Returns the active API key for reflection, prioritizing dedicated OPENROUTER_API_KEY2,
1380
- then logged-in UTIM credentials, and ambient OPENROUTER_API_KEY.
1382
+ Returns an active API key for reflection, rotating among dedicated OPENROUTER_API_KEY2,
1383
+ OPENROUTER_API_KEY3, OPENROUTER_API_KEY4, logged-in UTIM credentials, and ambient OPENROUTER_API_KEY.
1381
1384
  """
1382
1385
  try:
1383
- if os.getenv("OPENROUTER_API_KEY2"):
1384
- return os.getenv("OPENROUTER_API_KEY2")
1386
+ # Collect all configured OpenRouter keys for reflection rotation
1387
+ keys = []
1388
+ for var in ["OPENROUTER_API_KEY2", "OPENROUTER_API_KEY3", "OPENROUTER_API_KEY4", "OPENROUTER_API_KEY"]:
1389
+ val = os.getenv(var, "").strip()
1390
+ if val and val not in keys:
1391
+ keys.append(val)
1392
+
1393
+ if keys:
1394
+ global _REFLECTION_KEY_ROTATION_IDX
1395
+ with _REFLECTION_KEY_ROTATION_LOCK:
1396
+ chosen = keys[_REFLECTION_KEY_ROTATION_IDX % len(keys)]
1397
+ _REFLECTION_KEY_ROTATION_IDX = (_REFLECTION_KEY_ROTATION_IDX + 1) % len(keys)
1398
+ return chosen
1399
+
1385
1400
  from utim_cli.config import config
1386
1401
  api_key = config.get("api_key") or os.getenv("UTIM_API_KEY")
1387
1402
  source = config.get("main_model_source")
@@ -1410,6 +1425,13 @@ def _call_reflection_llm(prompt: str, llm_key: str = None, system: str = "", max
1410
1425
  """
1411
1426
  global _LAST_REFLECTION_CALL_TIME, _REFLECTION_COOLDOWN_UNTIL
1412
1427
 
1428
+ try:
1429
+ from utim_cli.tools import is_user_paid
1430
+ if not is_user_paid():
1431
+ return {}
1432
+ except Exception:
1433
+ pass
1434
+
1413
1435
  now = time.time()
1414
1436
  if now < _REFLECTION_COOLDOWN_UNTIL:
1415
1437
  # Under active rate-limit cooldown — skip background reflection gracefully
@@ -2299,6 +2321,13 @@ class BackgroundReflectionDispatcher:
2299
2321
  tool_results: List[Dict], elapsed_seconds: int,
2300
2322
  iterations: int, hints: Optional[List[str]],
2301
2323
  messages_snapshot: Optional[List[Dict]] = None):
2324
+ try:
2325
+ from utim_cli.tools import is_user_paid
2326
+ if not is_user_paid():
2327
+ return
2328
+ except Exception:
2329
+ pass
2330
+
2302
2331
  self.start()
2303
2332
  while self._task_queue.qsize() >= 3:
2304
2333
  try: