gemcode 0.4.12__tar.gz → 0.4.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 (183) hide show
  1. {gemcode-0.4.12/src/gemcode.egg-info → gemcode-0.4.14}/PKG-INFO +3 -1
  2. {gemcode-0.4.12 → gemcode-0.4.14}/README.md +2 -0
  3. {gemcode-0.4.12 → gemcode-0.4.14}/pyproject.toml +1 -1
  4. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/agent_habits.py +5 -3
  5. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/agent_mesh.py +110 -60
  6. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/fleet_reports.py +57 -0
  7. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/repl_commands.py +1 -0
  8. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/repl_slash.py +55 -0
  9. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tui/scrollback.py +79 -0
  10. {gemcode-0.4.12 → gemcode-0.4.14/src/gemcode.egg-info}/PKG-INFO +3 -1
  11. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_agent_mesh.py +62 -1
  12. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_fleet_reports.py +22 -0
  13. {gemcode-0.4.12 → gemcode-0.4.14}/LICENSE +0 -0
  14. {gemcode-0.4.12 → gemcode-0.4.14}/MANIFEST.in +0 -0
  15. {gemcode-0.4.12 → gemcode-0.4.14}/setup.cfg +0 -0
  16. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/__init__.py +0 -0
  17. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/__main__.py +0 -0
  18. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/a2a_bridge.py +0 -0
  19. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/agent.py +0 -0
  20. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/agent_intelligence.py +0 -0
  21. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/agent_triggers.py +0 -0
  22. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/audit.py +0 -0
  23. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/autocompact.py +0 -0
  24. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/automations.py +0 -0
  25. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/autotune.py +0 -0
  26. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/callbacks.py +0 -0
  27. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/capability_routing.py +0 -0
  28. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/checkpoints.py +0 -0
  29. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/cli.py +0 -0
  30. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/codebase_awareness.py +0 -0
  31. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/compaction.py +0 -0
  32. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/computer_use/__init__.py +0 -0
  33. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/computer_use/browser_computer.py +0 -0
  34. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/config.py +0 -0
  35. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/context_budget.py +0 -0
  36. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/context_warning.py +0 -0
  37. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/credentials.py +0 -0
  38. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/curated_memory.py +0 -0
  39. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/delegation_learning.py +0 -0
  40. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/dynamic_policy.py +0 -0
  41. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/evals/harness.py +0 -0
  42. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/event_bus.py +0 -0
  43. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/hitl_session.py +0 -0
  44. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/hooks.py +0 -0
  45. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/ide_protocol.py +0 -0
  46. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/ide_stdio.py +0 -0
  47. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/intent_classifier.py +0 -0
  48. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/interactions.py +0 -0
  49. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/invoke.py +0 -0
  50. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/kaira_client.py +0 -0
  51. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/kaira_daemon.py +0 -0
  52. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/kaira_ipc.py +0 -0
  53. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/kaira_job_store.py +0 -0
  54. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/learning.py +0 -0
  55. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/limits.py +0 -0
  56. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/live_audio_engine.py +0 -0
  57. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/logging_config.py +0 -0
  58. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/mcp_loader.py +0 -0
  59. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/memory/__init__.py +0 -0
  60. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/memory/embedding_memory_service.py +0 -0
  61. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/memory/file_memory_service.py +0 -0
  62. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/modality_tools.py +0 -0
  63. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/model_errors.py +0 -0
  64. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/model_routing.py +0 -0
  65. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/multimodal_input.py +0 -0
  66. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/openapi_loader.py +0 -0
  67. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/org.py +0 -0
  68. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/output_styles.py +0 -0
  69. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/paths.py +0 -0
  70. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/permissions.py +0 -0
  71. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/plugins/__init__.py +0 -0
  72. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  73. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  74. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/policy_profile.py +0 -0
  75. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/pricing.py +0 -0
  76. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/prompt_suggestions.py +0 -0
  77. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query/__init__.py +0 -0
  78. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query/config.py +0 -0
  79. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query/deps.py +0 -0
  80. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query/engine.py +0 -0
  81. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query/stop_hooks.py +0 -0
  82. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query/token_budget.py +0 -0
  83. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query/transitions.py +0 -0
  84. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query_sanitizer.py +0 -0
  85. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/refine.py +0 -0
  86. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/review_agent.py +0 -0
  87. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/rules.py +0 -0
  88. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/self_healing.py +0 -0
  89. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/session_runtime.py +0 -0
  90. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/session_store.py +0 -0
  91. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/session_summariser.py +0 -0
  92. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/skills.py +0 -0
  93. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/slash_commands.py +0 -0
  94. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/thinking.py +0 -0
  95. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tool_prompt_manifest.py +0 -0
  96. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tool_registry.py +0 -0
  97. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tool_result_store.py +0 -0
  98. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tool_synthesis.py +0 -0
  99. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/__init__.py +0 -0
  100. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/automations_tools.py +0 -0
  101. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/bash.py +0 -0
  102. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/browser.py +0 -0
  103. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/compress_memory.py +0 -0
  104. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/curated_memory.py +0 -0
  105. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/edit.py +0 -0
  106. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/filesystem.py +0 -0
  107. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/notebook.py +0 -0
  108. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/notes.py +0 -0
  109. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/org_tools.py +0 -0
  110. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/repo_map.py +0 -0
  111. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/search.py +0 -0
  112. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/shell.py +0 -0
  113. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/shell_gate.py +0 -0
  114. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/skills.py +0 -0
  115. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/subtask.py +0 -0
  116. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/tasks.py +0 -0
  117. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/think.py +0 -0
  118. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/todo.py +0 -0
  119. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/user_choice.py +0 -0
  120. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/veomem_tools.py +0 -0
  121. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/web.py +0 -0
  122. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/web_search.py +0 -0
  123. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools_inspector.py +0 -0
  124. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/trust.py +0 -0
  125. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tui/input_handler.py +0 -0
  126. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tui/spinner.py +0 -0
  127. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tui/welcome_banner.py +0 -0
  128. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tui/welcome_rich.py +0 -0
  129. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/veomem_bridge.py +0 -0
  130. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/version.py +0 -0
  131. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/vertex.py +0 -0
  132. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/wal.py +0 -0
  133. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/web/__init__.py +0 -0
  134. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/web/sse_adapter.py +0 -0
  135. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/web/terminal_repl.py +0 -0
  136. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/web/web_sse_compat.py +0 -0
  137. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/workspace_hints.py +0 -0
  138. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode.egg-info/SOURCES.txt +0 -0
  139. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode.egg-info/dependency_links.txt +0 -0
  140. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode.egg-info/entry_points.txt +0 -0
  141. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode.egg-info/requires.txt +0 -0
  142. {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode.egg-info/top_level.txt +0 -0
  143. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_add_dir.py +0 -0
  144. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_agent_habits.py +0 -0
  145. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_agent_instruction.py +0 -0
  146. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_autocompact.py +0 -0
  147. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_automations.py +0 -0
  148. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_capability_routing.py +0 -0
  149. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_checkpoint_diff_command.py +0 -0
  150. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_cli_init.py +0 -0
  151. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_compress_memory_tool.py +0 -0
  152. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_computer_use_permissions.py +0 -0
  153. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_context_budget.py +0 -0
  154. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_context_warning.py +0 -0
  155. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_credentials.py +0 -0
  156. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_eval_harness_layout.py +0 -0
  157. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_event_bus.py +0 -0
  158. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_ide_stdio_attachments.py +0 -0
  159. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_interactive_permission_ask.py +0 -0
  160. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_kaira_ipc_paths.py +0 -0
  161. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_kaira_scheduler.py +0 -0
  162. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_modality_tools.py +0 -0
  163. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_model_error_retry.py +0 -0
  164. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_model_errors.py +0 -0
  165. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_model_routing.py +0 -0
  166. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_multimodal_input.py +0 -0
  167. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_output_styles_and_rules.py +0 -0
  168. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_paths.py +0 -0
  169. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_permissions.py +0 -0
  170. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_prompt_suggestions.py +0 -0
  171. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_repl_commands.py +0 -0
  172. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_repl_slash.py +0 -0
  173. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_session_runtime_cache.py +0 -0
  174. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_skills.py +0 -0
  175. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_slash_commands.py +0 -0
  176. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_slash_completion_registry.py +0 -0
  177. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_thinking_config.py +0 -0
  178. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_token_budget.py +0 -0
  179. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_tool_context_circulation.py +0 -0
  180. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_tools.py +0 -0
  181. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_tools_inspector.py +0 -0
  182. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_web_sse_adapter.py +0 -0
  183. {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.12
3
+ Version: 0.4.14
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -398,6 +398,8 @@ The LLM calls `transfer_to_agent(agent_name='verifier')` → ADK routes natively
398
398
 
399
399
  For background work: `org_delegate("kaira", "run tests")` → mesh runs kaira as a full GemCode session → result flows back via fleet reports.
400
400
 
401
+ **Mesh / habits reliability:** overlapping jobs for the **same** org member serialize writes to that agent’s durable SQLite session so ADK does not raise “stale session” errors. Mesh workers default to **unattended tool approval** (env **`GEMCODE_MESH_WORKER_UNATTENDED`**, on by default) so background shell / delegation / writes do not block the main TUI on HITL. Fleet auto-continue digests the inbox after **assistant** turns; while idle at ❯, use **`/fleet`** / **`/fleet show`** or any message — see **`GEMCODE_FLEET_TUI_NOTIFY`** in [`../docs/configuration.md`](../docs/configuration.md). See [`../docs/orchestration.md`](../docs/orchestration.md).
402
+
401
403
  Docs:
402
404
  - [`../docs/orchestration.md`](../docs/orchestration.md)
403
405
 
@@ -205,6 +205,8 @@ The LLM calls `transfer_to_agent(agent_name='verifier')` → ADK routes natively
205
205
 
206
206
  For background work: `org_delegate("kaira", "run tests")` → mesh runs kaira as a full GemCode session → result flows back via fleet reports.
207
207
 
208
+ **Mesh / habits reliability:** overlapping jobs for the **same** org member serialize writes to that agent’s durable SQLite session so ADK does not raise “stale session” errors. Mesh workers default to **unattended tool approval** (env **`GEMCODE_MESH_WORKER_UNATTENDED`**, on by default) so background shell / delegation / writes do not block the main TUI on HITL. Fleet auto-continue digests the inbox after **assistant** turns; while idle at ❯, use **`/fleet`** / **`/fleet show`** or any message — see **`GEMCODE_FLEET_TUI_NOTIFY`** in [`../docs/configuration.md`](../docs/configuration.md). See [`../docs/orchestration.md`](../docs/orchestration.md).
209
+
208
210
  Docs:
209
211
  - [`../docs/orchestration.md`](../docs/orchestration.md)
210
212
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.4.12"
7
+ version = "0.4.14"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -324,9 +324,11 @@ def make_habits_tools(cfg: GemCodeConfig) -> list:
324
324
  They fire as long as GemCode is open (REPL/TUI session).
325
325
 
326
326
  Results go to the fleet inbox (.gemcode/fleet_reports.jsonl). Fleet auto-continue
327
- (GEMCODE_FLEET_REPORTS_AUTO_CONTINUE, default on) can inject digest turns after each assistant
328
- reply so the main agent summarizes habit output. Set GEMCODE_FLEET_REPORTS_AUTO_CONTINUE=0 to
329
- only drain on your next normal message.
327
+ (GEMCODE_FLEET_REPORTS_AUTO_CONTINUE, default on) runs digest turns **after each assistant
328
+ reply** when the inbox still has entries it does **not** wake the model while the TUI is
329
+ idle at the prompt. While waiting at ❯, use **`/fleet`** (digest) or **`/fleet show`** (peek),
330
+ or send any message; the TUI also prints a throttled hint when mesh jobs finish
331
+ (GEMCODE_FLEET_TUI_NOTIFY).
330
332
 
331
333
  Args:
332
334
  name: Unique name for this habit (e.g., "test-watch", "nightly-audit").
@@ -24,7 +24,7 @@ from dataclasses import dataclass, field
24
24
  from pathlib import Path
25
25
  from typing import Any
26
26
 
27
- from gemcode.config import GemCodeConfig
27
+ from gemcode.config import GemCodeConfig, _truthy_env
28
28
  from gemcode.event_bus import BusMessage, EventBus, get_bus
29
29
  from gemcode.org import OrgMember, find_member, list_members, resolve_fleet_root
30
30
 
@@ -44,6 +44,21 @@ class AgentJob:
44
44
  created_ms: int = field(default_factory=lambda: int(time.time() * 1000))
45
45
 
46
46
 
47
+ def _apply_mesh_worker_unattended_policy(cfg: GemCodeConfig) -> None:
48
+ """
49
+ Mesh jobs run on a background thread: there is no human at a separate keyboard.
50
+ When enabled (default), treat the worker like ``--yes`` and disable in-run HITL so
51
+ shell / org_delegate / write tools from habits and workers do not block the main TUI.
52
+
53
+ Opt out: ``GEMCODE_MESH_WORKER_UNATTENDED=0`` (mesh workers then inherit the
54
+ manager's ``yes_to_all`` / ``interactive_permission_ask`` as before).
55
+ """
56
+ if not _truthy_env("GEMCODE_MESH_WORKER_UNATTENDED", default=True):
57
+ return
58
+ cfg.yes_to_all = True
59
+ cfg.interactive_permission_ask = False
60
+
61
+
47
62
  class AgentMesh:
48
63
  """
49
64
  In-process agent orchestration mesh.
@@ -72,6 +87,12 @@ class AgentMesh:
72
87
  # _stop is created lazily in the background loop
73
88
  self._stop_flag = False
74
89
 
90
+ # Serialize ADK SqliteSessionService writes per (db, user_id, session_id).
91
+ # Concurrent mesh jobs for the same agent share one session row; interleaved
92
+ # append_event calls hit optimistic-lock checks and raise "stale session".
93
+ self._mesh_sqlite_session_locks: dict[str, asyncio.Lock] = {}
94
+ self._mesh_sqlite_session_locks_guard = asyncio.Lock()
95
+
75
96
  # Subscribe to org.assign messages on the bus
76
97
  self._bus.subscribe(
77
98
  topic="org.assign",
@@ -115,6 +136,22 @@ class AgentMesh:
115
136
  def bus(self) -> EventBus:
116
137
  return self._bus
117
138
 
139
+ async def _lock_sqlite_session(
140
+ self,
141
+ *,
142
+ db_path: Path,
143
+ user_id: str,
144
+ session_id: str,
145
+ ) -> asyncio.Lock:
146
+ """Return the asyncio lock for this ADK SQLite session (create if needed)."""
147
+ key = f"{db_path.resolve()}\0{user_id}\0{session_id}"
148
+ async with self._mesh_sqlite_session_locks_guard:
149
+ lock = self._mesh_sqlite_session_locks.get(key)
150
+ if lock is None:
151
+ lock = asyncio.Lock()
152
+ self._mesh_sqlite_session_locks[key] = lock
153
+ return lock
154
+
118
155
  def start(self) -> None:
119
156
  """Start the mesh in a dedicated background thread with its own event loop.
120
157
 
@@ -354,18 +391,22 @@ class AgentMesh:
354
391
  duration_ms = int(time.time() * 1000) - start_ms
355
392
 
356
393
  # Publish completion
394
+ _jr_finished: dict[str, Any] = {
395
+ "job_id": job.job_id,
396
+ "session_id": job.session_id,
397
+ "status": "finished",
398
+ "member": job.member_name,
399
+ "report": result_text[:8000],
400
+ "duration_ms": duration_ms,
401
+ }
402
+ _hm0 = job.meta.get("habit") if isinstance(job.meta, dict) else None
403
+ if isinstance(_hm0, dict):
404
+ _jr_finished["habit"] = _hm0
357
405
  await self._bus.publish(BusMessage(
358
406
  topic="job.report",
359
407
  from_addr=job.member_name or "mesh",
360
408
  to_addr="manager",
361
- payload={
362
- "job_id": job.job_id,
363
- "session_id": job.session_id,
364
- "status": "finished",
365
- "member": job.member_name,
366
- "report": result_text[:8000],
367
- "duration_ms": duration_ms,
368
- },
409
+ payload=_jr_finished,
369
410
  ))
370
411
 
371
412
  # Also publish org.report if this was an org delegation
@@ -422,17 +463,21 @@ class AgentMesh:
422
463
  job.status = "failed"
423
464
  job.error = f"{type(e).__name__}: {e}"
424
465
 
466
+ _jr_failed: dict[str, Any] = {
467
+ "job_id": job.job_id,
468
+ "session_id": job.session_id,
469
+ "status": "failed",
470
+ "member": job.member_name,
471
+ "error": job.error,
472
+ }
473
+ _hm1 = job.meta.get("habit") if isinstance(job.meta, dict) else None
474
+ if isinstance(_hm1, dict):
475
+ _jr_failed["habit"] = _hm1
425
476
  await self._bus.publish(BusMessage(
426
477
  topic="job.report",
427
478
  from_addr=job.member_name or "mesh",
428
479
  to_addr="manager",
429
- payload={
430
- "job_id": job.job_id,
431
- "session_id": job.session_id,
432
- "status": "failed",
433
- "member": job.member_name,
434
- "error": job.error,
435
- },
480
+ payload=_jr_failed,
436
481
  ))
437
482
 
438
483
  # Persist failure to fleet reports
@@ -475,14 +520,17 @@ class AgentMesh:
475
520
  This means agents build up context over time — they remember past tasks,
476
521
  learn from their history, and maintain their own notes.
477
522
  """
523
+ import hashlib
524
+
478
525
  from gemcode.invoke import run_turn
479
- from gemcode.session_runtime import create_runner
526
+ from gemcode.session_runtime import create_runner, session_db_path
480
527
  from gemcode.capability_routing import apply_capability_routing
481
528
  from gemcode.model_routing import pick_effective_model
482
529
 
483
530
  # Resolve the agent's workspace as their project root
484
531
  # This gives them their own .gemcode/ directory, sessions, memory, etc.
485
532
  agent_cfg = copy.deepcopy(self.cfg)
533
+ _apply_mesh_worker_unattended_policy(agent_cfg)
486
534
  fleet_root = resolve_fleet_root(self.cfg.project_root)
487
535
 
488
536
  if job.member_name:
@@ -507,54 +555,56 @@ class AgentMesh:
507
555
  parent_tools = self._build_parent_access_tools(fleet_root)
508
556
  all_extra = (mesh_tools or []) + (parent_tools or [])
509
557
 
510
- # Create a FULL runner rooted at the agent's workspace
511
- # This gives them their own SQLite session DB, their own memory, etc.
512
- runner = create_runner(agent_cfg, extra_tools=all_extra or None)
558
+ # Stable session ID per agent (same id across jobs = durable history).
559
+ stable_session_id = (job.session_id or "").strip()
560
+ if job.member_name:
561
+ stable_session_id = f"agent_{hashlib.sha256(job.member_name.encode()).hexdigest()[:12]}"
562
+ mesh_user_id = job.member_name or "mesh"
513
563
 
514
- try:
515
- # Use a stable session ID per agent so they accumulate history
516
- # (not a random UUID — the same agent keeps the same session across jobs)
517
- stable_session_id = job.session_id
518
- if job.member_name:
519
- # Stable session = agent name hash (persists across jobs)
520
- import hashlib
521
- stable_session_id = f"agent_{hashlib.sha256(job.member_name.encode()).hexdigest()[:12]}"
522
-
523
- # Execute the turn with full power
524
- max_calls = min(int(self.cfg.max_llm_calls or 128), 128)
525
- events = await run_turn(
526
- runner,
527
- user_id=job.member_name or "mesh",
564
+ sqlite_lock = await self._lock_sqlite_session(
565
+ db_path=session_db_path(agent_cfg),
566
+ user_id=mesh_user_id,
528
567
  session_id=stable_session_id,
529
- prompt=job.prompt,
530
- max_llm_calls=max_calls,
531
- cfg=agent_cfg,
532
- consume_fleet_reports=False,
533
- )
568
+ )
534
569
 
535
- # Extract text from events
536
- parts: list[str] = []
537
- for ev in events:
538
- try:
539
- if not ev.content or not ev.content.parts:
540
- continue
541
- if getattr(ev, "author", None) == "user":
542
- continue
543
- for part in ev.content.parts:
544
- t = getattr(part, "text", None)
545
- is_thought = getattr(part, "thought", None)
546
- if isinstance(t, str) and t.strip() and not is_thought:
547
- parts.append(t)
548
- except Exception:
549
- continue
570
+ async with sqlite_lock:
571
+ # One runner + turn at a time per session row — avoids ADK "stale session"
572
+ # when habits or overlapping delegations write events concurrently.
573
+ runner = create_runner(agent_cfg, extra_tools=all_extra or None)
550
574
 
551
- return "".join(parts).strip() or "(no output)"
552
- finally:
553
- # Clean up the runner
554
575
  try:
555
- await runner.close()
556
- except Exception:
557
- pass
576
+ max_calls = min(int(self.cfg.max_llm_calls or 128), 128)
577
+ events = await run_turn(
578
+ runner,
579
+ user_id=mesh_user_id,
580
+ session_id=stable_session_id,
581
+ prompt=job.prompt,
582
+ max_llm_calls=max_calls,
583
+ cfg=agent_cfg,
584
+ consume_fleet_reports=False,
585
+ )
586
+
587
+ parts: list[str] = []
588
+ for ev in events:
589
+ try:
590
+ if not ev.content or not ev.content.parts:
591
+ continue
592
+ if getattr(ev, "author", None) == "user":
593
+ continue
594
+ for part in ev.content.parts:
595
+ t = getattr(part, "text", None)
596
+ is_thought = getattr(part, "thought", None)
597
+ if isinstance(t, str) and t.strip() and not is_thought:
598
+ parts.append(t)
599
+ except Exception:
600
+ continue
601
+
602
+ return "".join(parts).strip() or "(no output)"
603
+ finally:
604
+ try:
605
+ await runner.close()
606
+ except Exception:
607
+ pass
558
608
 
559
609
  def _build_parent_access_tools(self, fleet_root: Path) -> list:
560
610
  """
@@ -366,3 +366,60 @@ def drain_for_prompt(project_root: Path, *, max_chars: int | None = None) -> str
366
366
  "increase GEMCODE_FLEET_REPORTS_MAX_CHARS to drain more per turn)"
367
367
  )
368
368
  return header + body
369
+
370
+
371
+ def preview_fleet_inbox(project_root: Path, *, max_chars: int | None = None) -> str:
372
+ """
373
+ Format pending fleet inbox lines without draining the file (for ``/fleet show``).
374
+ """
375
+ if max_chars is None:
376
+ try:
377
+ max_chars = int(os.environ.get("GEMCODE_FLEET_REPORTS_MAX_CHARS", "14000"))
378
+ except Exception:
379
+ max_chars = 14_000
380
+ try:
381
+ from gemcode.org import resolve_fleet_root
382
+
383
+ fleet_root = resolve_fleet_root(project_root)
384
+ except Exception:
385
+ fleet_root = project_root
386
+ p = _fleet_reports_path(fleet_root)
387
+ if not p.is_file():
388
+ return "(no `.gemcode/fleet_reports.jsonl` yet — background jobs append here when they finish)"
389
+ try:
390
+ raw = p.read_text(encoding="utf-8", errors="replace")
391
+ except Exception:
392
+ return "(could not read fleet_reports.jsonl)"
393
+ if not raw.strip():
394
+ return "(fleet inbox is empty)"
395
+
396
+ lines_in = [ln.strip() for ln in raw.splitlines() if ln.strip()]
397
+ blocks: list[str] = []
398
+ total = 0
399
+ truncated = False
400
+ for line in lines_in:
401
+ try:
402
+ rec = json.loads(line)
403
+ except Exception:
404
+ continue
405
+ if not isinstance(rec, dict):
406
+ continue
407
+ b = _format_record(rec)
408
+ if not b:
409
+ continue
410
+ need = len(b) + 2
411
+ if total + need > max_chars:
412
+ truncated = True
413
+ break
414
+ blocks.append(b)
415
+ total += need
416
+
417
+ if not blocks:
418
+ return "(fleet inbox has no readable entries)"
419
+ header = "Fleet / agent reports (preview — inbox not cleared):\n\n"
420
+ body = "\n\n".join(blocks)
421
+ if truncated:
422
+ body += (
423
+ "\n\n… (truncated; increase GEMCODE_FLEET_REPORTS_MAX_CHARS or run /fleet show after /fleet digest)"
424
+ )
425
+ return header + body
@@ -236,6 +236,7 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
236
236
  ("runtime", "Fleet socket status · gemcode runtime · attach/connect"),
237
237
  ("bus", "Runtime bus — send/publish lightweight messages over IPC"),
238
238
  ("inbox", "Bus inbox filters for this UI (to/topics)"),
239
+ ("fleet", "Fleet inbox — /fleet show | digest (habits / mesh reports)"),
239
240
  ("agent", "Create/manage a child agent workspace (folder + registry)"),
240
241
  # NOTE: /org and /delegate are deprecated aliases; keep working but do not list.
241
242
  ("limits", "Execution limits (calls, context, …)"),
@@ -91,6 +91,61 @@ async def process_repl_slash(
91
91
  out()
92
92
  return ReplSlashResult(skip_model_turn=True)
93
93
 
94
+ # ── /fleet (fleet_reports.jsonl — habits / mesh without waiting for a model turn) ─
95
+ if name in ("fleet", "fleet_reports", "reports"):
96
+ from gemcode.fleet_reports import (
97
+ fleet_digest_prompt,
98
+ has_pending_fleet_reports,
99
+ inject_enabled,
100
+ preview_fleet_inbox,
101
+ )
102
+
103
+ try:
104
+ object.__setattr__(cfg, "_fleet_auto_chain", 0)
105
+ except Exception:
106
+ pass
107
+
108
+ sub = (sc.args or "").strip()
109
+ sub_l = sub.lower()
110
+ first = sub_l.split()[0] if sub_l else ""
111
+
112
+ if sub_l in ("help", "?") or (not sub_l and not has_pending_fleet_reports(cfg.project_root)):
113
+ out("Fleet inbox (`.gemcode/fleet_reports.jsonl` at fleet root):")
114
+ out(" /fleet If reports are pending: run a digest turn (drains inbox into the model).")
115
+ out(" /fleet digest Same, when you want to force digest.")
116
+ out(" /fleet show Print pending lines without draining (peek only).")
117
+ out("While the TUI is idle at ❯, auto-continue only runs after an assistant reply;")
118
+ out("mesh/habit completions still land in the inbox — use /fleet or any message to drain.")
119
+ if not inject_enabled():
120
+ out(" (GEMCODE_FLEET_REPORTS_INJECT=0 — inbox is not written.)")
121
+ out()
122
+ return ReplSlashResult(skip_model_turn=True)
123
+
124
+ if sub_l in ("show", "peek", "cat") or first in ("show", "peek", "cat"):
125
+ out(preview_fleet_inbox(cfg.project_root))
126
+ out()
127
+ return ReplSlashResult(skip_model_turn=True)
128
+
129
+ if sub_l in ("digest", "summarize", "sum") or first in ("digest", "summarize", "sum"):
130
+ if not has_pending_fleet_reports(cfg.project_root):
131
+ out("[fleet] no pending reports in inbox.")
132
+ out()
133
+ return ReplSlashResult(skip_model_turn=True)
134
+ return ReplSlashResult(model_prompt=fleet_digest_prompt())
135
+
136
+ if not sub_l:
137
+ if has_pending_fleet_reports(cfg.project_root):
138
+ return ReplSlashResult(model_prompt=fleet_digest_prompt())
139
+ out("[fleet] no pending reports. Try `/fleet help`.")
140
+ out()
141
+ return ReplSlashResult(skip_model_turn=True)
142
+
143
+ if not has_pending_fleet_reports(cfg.project_root):
144
+ out("[fleet] no pending reports in inbox.")
145
+ out()
146
+ return ReplSlashResult(skip_model_turn=True)
147
+ return ReplSlashResult(model_prompt=fleet_digest_prompt())
148
+
94
149
  # ── /attach (queue files for the next user message: PDF, images, audio, …) ─
95
150
  if name in ("attach", "file", "image", "img"):
96
151
  raw_i = (sc.args or "").strip()
@@ -4,6 +4,7 @@ import asyncio
4
4
  import json
5
5
  import os
6
6
  import sys
7
+ import time
7
8
  from dataclasses import dataclass
8
9
 
9
10
  from google.adk.agents.run_config import RunConfig
@@ -278,6 +279,84 @@ async def run_gemcode_scrollback_tui(
278
279
  get_cfg=lambda: cfg,
279
280
  )
280
281
 
282
+ # When mesh/habit jobs finish, fleet_reports.jsonl updates while we are idle at ❯.
283
+ # Auto-continue only runs after an *assistant* turn, so print a throttled hint.
284
+ _tui_loop = asyncio.get_running_loop()
285
+ _last_fleet_notify_s = [0.0]
286
+
287
+ def _on_mesh_job_report_notify(msg: object) -> None:
288
+ if os.environ.get("GEMCODE_FLEET_TUI_NOTIFY", "1").strip().lower() in (
289
+ "0", "false", "no", "off",
290
+ ):
291
+ return
292
+ try:
293
+ from gemcode.event_bus import BusMessage
294
+
295
+ if not isinstance(msg, BusMessage) or msg.topic != "job.report":
296
+ return
297
+ pl = msg.payload if isinstance(msg.payload, dict) else {}
298
+ if str(pl.get("status") or "").strip().lower() not in ("finished", "failed"):
299
+ return
300
+ except Exception:
301
+ return
302
+ try:
303
+ min_gap = float(os.environ.get("GEMCODE_FLEET_TUI_NOTIFY_MIN_S", "8") or "8")
304
+ except Exception:
305
+ min_gap = 8.0
306
+ now = time.time()
307
+ if now - _last_fleet_notify_s[0] < min_gap:
308
+ return
309
+ _last_fleet_notify_s[0] = now
310
+
311
+ def _print_hint() -> None:
312
+ try:
313
+ mem = ""
314
+ if isinstance(msg.payload, dict):
315
+ mem = str(msg.payload.get("member") or "").strip()
316
+ habit = ""
317
+ if isinstance(msg.payload, dict):
318
+ hm = msg.payload.get("habit")
319
+ if isinstance(hm, dict):
320
+ habit = str(hm.get("name") or "").strip()
321
+ bits = []
322
+ if mem:
323
+ bits.append(mem)
324
+ if habit:
325
+ bits.append(f"habit:{habit}")
326
+ extra = f" ({' · '.join(bits)})" if bits else ""
327
+ line = (
328
+ f"{ansi.dim}[gemcode] Background job finished{extra} — "
329
+ f"/fleet to summarize inbox (any message also drains).{ansi.reset}"
330
+ )
331
+ if input_handler.is_interactive():
332
+ try:
333
+ from prompt_toolkit.patch_stdout import patch_stdout
334
+
335
+ with patch_stdout():
336
+ print(line, flush=True)
337
+ except Exception:
338
+ print(line, flush=True)
339
+ else:
340
+ print(line, flush=True)
341
+ except Exception:
342
+ pass
343
+
344
+ try:
345
+ _tui_loop.call_soon_threadsafe(_print_hint)
346
+ except Exception:
347
+ pass
348
+
349
+ try:
350
+ from gemcode.event_bus import get_bus
351
+
352
+ get_bus().subscribe(
353
+ topic="job.report",
354
+ to_addr="manager",
355
+ callback=_on_mesh_job_report_notify,
356
+ )
357
+ except Exception:
358
+ pass
359
+
281
360
  # ── Optional: embed Kaira daemon in this TUI ─────────────────────────────
282
361
  # This enables continuous background automation without requiring a second
283
362
  # terminal. The TUI will also auto-subscribe to IPC events (below) so job
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.12
3
+ Version: 0.4.14
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -398,6 +398,8 @@ The LLM calls `transfer_to_agent(agent_name='verifier')` → ADK routes natively
398
398
 
399
399
  For background work: `org_delegate("kaira", "run tests")` → mesh runs kaira as a full GemCode session → result flows back via fleet reports.
400
400
 
401
+ **Mesh / habits reliability:** overlapping jobs for the **same** org member serialize writes to that agent’s durable SQLite session so ADK does not raise “stale session” errors. Mesh workers default to **unattended tool approval** (env **`GEMCODE_MESH_WORKER_UNATTENDED`**, on by default) so background shell / delegation / writes do not block the main TUI on HITL. Fleet auto-continue digests the inbox after **assistant** turns; while idle at ❯, use **`/fleet`** / **`/fleet show`** or any message — see **`GEMCODE_FLEET_TUI_NOTIFY`** in [`../docs/configuration.md`](../docs/configuration.md). See [`../docs/orchestration.md`](../docs/orchestration.md).
402
+
401
403
  Docs:
402
404
  - [`../docs/orchestration.md`](../docs/orchestration.md)
403
405
 
@@ -3,11 +3,18 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import os
6
7
  from pathlib import Path
7
8
 
8
9
  import pytest
9
10
 
10
- from gemcode.agent_mesh import AgentMesh, ensure_mesh, get_mesh, reset_mesh
11
+ from gemcode.agent_mesh import (
12
+ AgentMesh,
13
+ _apply_mesh_worker_unattended_policy,
14
+ ensure_mesh,
15
+ get_mesh,
16
+ reset_mesh,
17
+ )
11
18
  from gemcode.config import GemCodeConfig
12
19
  from gemcode.event_bus import BusMessage, get_bus, reset_bus
13
20
 
@@ -21,6 +28,26 @@ def _reset():
21
28
  reset_bus()
22
29
 
23
30
 
31
+ @pytest.mark.asyncio
32
+ async def test_lock_sqlite_session_same_key_reuses_lock(tmp_path: Path) -> None:
33
+ cfg = GemCodeConfig(project_root=tmp_path)
34
+ mesh = AgentMesh(cfg)
35
+ db = tmp_path / ".gemcode" / "sessions.sqlite"
36
+ a = await mesh._lock_sqlite_session(db_path=db, user_id="agent_x", session_id="sess1")
37
+ b = await mesh._lock_sqlite_session(db_path=db, user_id="agent_x", session_id="sess1")
38
+ assert a is b
39
+
40
+
41
+ @pytest.mark.asyncio
42
+ async def test_lock_sqlite_session_different_session_distinct_locks(tmp_path: Path) -> None:
43
+ cfg = GemCodeConfig(project_root=tmp_path)
44
+ mesh = AgentMesh(cfg)
45
+ db = tmp_path / ".gemcode" / "sessions.sqlite"
46
+ a = await mesh._lock_sqlite_session(db_path=db, user_id="agent_x", session_id="sess1")
47
+ b = await mesh._lock_sqlite_session(db_path=db, user_id="agent_x", session_id="sess2")
48
+ assert a is not b
49
+
50
+
24
51
  def test_mesh_creation(tmp_path: Path) -> None:
25
52
  cfg = GemCodeConfig(project_root=tmp_path)
26
53
  mesh = ensure_mesh(cfg)
@@ -76,6 +103,40 @@ def test_mesh_bus_integration(tmp_path: Path) -> None:
76
103
  assert received[0].payload["member"] == "test"
77
104
 
78
105
 
106
+ def test_apply_mesh_worker_unattended_default_on(tmp_path: Path) -> None:
107
+ cfg = GemCodeConfig(project_root=tmp_path)
108
+ cfg.yes_to_all = False
109
+ cfg.interactive_permission_ask = True
110
+ old = os.environ.get("GEMCODE_MESH_WORKER_UNATTENDED")
111
+ try:
112
+ os.environ.pop("GEMCODE_MESH_WORKER_UNATTENDED", None)
113
+ _apply_mesh_worker_unattended_policy(cfg)
114
+ finally:
115
+ if old is None:
116
+ os.environ.pop("GEMCODE_MESH_WORKER_UNATTENDED", None)
117
+ else:
118
+ os.environ["GEMCODE_MESH_WORKER_UNATTENDED"] = old
119
+ assert cfg.yes_to_all is True
120
+ assert cfg.interactive_permission_ask is False
121
+
122
+
123
+ def test_apply_mesh_worker_unattended_off_inherits_manager(tmp_path: Path) -> None:
124
+ cfg = GemCodeConfig(project_root=tmp_path)
125
+ cfg.yes_to_all = False
126
+ cfg.interactive_permission_ask = True
127
+ old = os.environ.get("GEMCODE_MESH_WORKER_UNATTENDED")
128
+ try:
129
+ os.environ["GEMCODE_MESH_WORKER_UNATTENDED"] = "0"
130
+ _apply_mesh_worker_unattended_policy(cfg)
131
+ finally:
132
+ if old is None:
133
+ os.environ.pop("GEMCODE_MESH_WORKER_UNATTENDED", None)
134
+ else:
135
+ os.environ["GEMCODE_MESH_WORKER_UNATTENDED"] = old
136
+ assert cfg.yes_to_all is False
137
+ assert cfg.interactive_permission_ask is True
138
+
139
+
79
140
  def test_mesh_priority_ordering(tmp_path: Path) -> None:
80
141
  """Higher priority jobs should be dequeued first."""
81
142
  cfg = GemCodeConfig(project_root=tmp_path)
@@ -12,6 +12,7 @@ from gemcode.fleet_reports import (
12
12
  has_pending_fleet_reports,
13
13
  inject_enabled,
14
14
  maybe_append_org_report,
15
+ preview_fleet_inbox,
15
16
  )
16
17
  from gemcode.org import resolve_fleet_root
17
18
 
@@ -98,3 +99,24 @@ def test_inject_disabled_skips_append(tmp_path: Path, monkeypatch: pytest.Monkey
98
99
  append_fleet_report(tmp_path, topic="org.report", payload={"status": "finished"})
99
100
  p = tmp_path / ".gemcode" / "fleet_reports.jsonl"
100
101
  assert not p.exists()
102
+
103
+
104
+ def test_preview_fleet_inbox_does_not_clear(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
105
+ monkeypatch.setenv("GEMCODE_FLEET_REPORTS_INJECT", "1")
106
+ (tmp_path / ".gemcode").mkdir()
107
+ maybe_append_org_report(
108
+ tmp_path,
109
+ {
110
+ "status": "finished",
111
+ "job_id": "j-preview",
112
+ "task": "t",
113
+ "member": {"name": "w"},
114
+ "result": {"report": "hello"},
115
+ },
116
+ )
117
+ p = tmp_path / ".gemcode" / "fleet_reports.jsonl"
118
+ assert p.read_text(encoding="utf-8").strip()
119
+ prev = preview_fleet_inbox(tmp_path)
120
+ assert "preview" in prev.lower() or "Fleet / agent reports" in prev
121
+ assert "hello" in prev
122
+ assert p.read_text(encoding="utf-8").strip()
File without changes
File without changes
File without changes