gemcode 0.4.11__tar.gz → 0.4.13__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.11/src/gemcode.egg-info → gemcode-0.4.13}/PKG-INFO +3 -1
  2. {gemcode-0.4.11 → gemcode-0.4.13}/README.md +2 -0
  3. {gemcode-0.4.11 → gemcode-0.4.13}/pyproject.toml +1 -1
  4. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/agent_habits.py +5 -0
  5. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/agent_mesh.py +102 -50
  6. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/fleet_reports.py +16 -9
  7. {gemcode-0.4.11 → gemcode-0.4.13/src/gemcode.egg-info}/PKG-INFO +3 -1
  8. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_agent_mesh.py +62 -1
  9. {gemcode-0.4.11 → gemcode-0.4.13}/LICENSE +0 -0
  10. {gemcode-0.4.11 → gemcode-0.4.13}/MANIFEST.in +0 -0
  11. {gemcode-0.4.11 → gemcode-0.4.13}/setup.cfg +0 -0
  12. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/__init__.py +0 -0
  13. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/__main__.py +0 -0
  14. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/a2a_bridge.py +0 -0
  15. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/agent.py +0 -0
  16. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/agent_intelligence.py +0 -0
  17. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/agent_triggers.py +0 -0
  18. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/audit.py +0 -0
  19. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/autocompact.py +0 -0
  20. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/automations.py +0 -0
  21. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/autotune.py +0 -0
  22. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/callbacks.py +0 -0
  23. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/capability_routing.py +0 -0
  24. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/checkpoints.py +0 -0
  25. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/cli.py +0 -0
  26. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/codebase_awareness.py +0 -0
  27. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/compaction.py +0 -0
  28. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/computer_use/__init__.py +0 -0
  29. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/computer_use/browser_computer.py +0 -0
  30. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/config.py +0 -0
  31. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/context_budget.py +0 -0
  32. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/context_warning.py +0 -0
  33. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/credentials.py +0 -0
  34. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/curated_memory.py +0 -0
  35. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/delegation_learning.py +0 -0
  36. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/dynamic_policy.py +0 -0
  37. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/evals/harness.py +0 -0
  38. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/event_bus.py +0 -0
  39. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/hitl_session.py +0 -0
  40. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/hooks.py +0 -0
  41. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/ide_protocol.py +0 -0
  42. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/ide_stdio.py +0 -0
  43. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/intent_classifier.py +0 -0
  44. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/interactions.py +0 -0
  45. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/invoke.py +0 -0
  46. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/kaira_client.py +0 -0
  47. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/kaira_daemon.py +0 -0
  48. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/kaira_ipc.py +0 -0
  49. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/kaira_job_store.py +0 -0
  50. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/learning.py +0 -0
  51. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/limits.py +0 -0
  52. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/live_audio_engine.py +0 -0
  53. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/logging_config.py +0 -0
  54. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/mcp_loader.py +0 -0
  55. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/memory/__init__.py +0 -0
  56. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/memory/embedding_memory_service.py +0 -0
  57. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/memory/file_memory_service.py +0 -0
  58. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/modality_tools.py +0 -0
  59. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/model_errors.py +0 -0
  60. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/model_routing.py +0 -0
  61. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/multimodal_input.py +0 -0
  62. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/openapi_loader.py +0 -0
  63. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/org.py +0 -0
  64. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/output_styles.py +0 -0
  65. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/paths.py +0 -0
  66. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/permissions.py +0 -0
  67. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/plugins/__init__.py +0 -0
  68. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  69. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  70. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/policy_profile.py +0 -0
  71. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/pricing.py +0 -0
  72. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/prompt_suggestions.py +0 -0
  73. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/query/__init__.py +0 -0
  74. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/query/config.py +0 -0
  75. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/query/deps.py +0 -0
  76. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/query/engine.py +0 -0
  77. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/query/stop_hooks.py +0 -0
  78. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/query/token_budget.py +0 -0
  79. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/query/transitions.py +0 -0
  80. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/query_sanitizer.py +0 -0
  81. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/refine.py +0 -0
  82. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/repl_commands.py +0 -0
  83. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/repl_slash.py +0 -0
  84. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/review_agent.py +0 -0
  85. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/rules.py +0 -0
  86. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/self_healing.py +0 -0
  87. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/session_runtime.py +0 -0
  88. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/session_store.py +0 -0
  89. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/session_summariser.py +0 -0
  90. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/skills.py +0 -0
  91. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/slash_commands.py +0 -0
  92. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/thinking.py +0 -0
  93. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tool_prompt_manifest.py +0 -0
  94. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tool_registry.py +0 -0
  95. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tool_result_store.py +0 -0
  96. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tool_synthesis.py +0 -0
  97. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/__init__.py +0 -0
  98. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/automations_tools.py +0 -0
  99. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/bash.py +0 -0
  100. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/browser.py +0 -0
  101. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/compress_memory.py +0 -0
  102. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/curated_memory.py +0 -0
  103. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/edit.py +0 -0
  104. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/filesystem.py +0 -0
  105. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/notebook.py +0 -0
  106. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/notes.py +0 -0
  107. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/org_tools.py +0 -0
  108. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/repo_map.py +0 -0
  109. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/search.py +0 -0
  110. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/shell.py +0 -0
  111. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/shell_gate.py +0 -0
  112. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/skills.py +0 -0
  113. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/subtask.py +0 -0
  114. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/tasks.py +0 -0
  115. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/think.py +0 -0
  116. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/todo.py +0 -0
  117. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/user_choice.py +0 -0
  118. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/veomem_tools.py +0 -0
  119. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/web.py +0 -0
  120. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools/web_search.py +0 -0
  121. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tools_inspector.py +0 -0
  122. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/trust.py +0 -0
  123. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tui/input_handler.py +0 -0
  124. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tui/scrollback.py +0 -0
  125. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tui/spinner.py +0 -0
  126. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tui/welcome_banner.py +0 -0
  127. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/tui/welcome_rich.py +0 -0
  128. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/veomem_bridge.py +0 -0
  129. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/version.py +0 -0
  130. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/vertex.py +0 -0
  131. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/wal.py +0 -0
  132. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/web/__init__.py +0 -0
  133. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/web/sse_adapter.py +0 -0
  134. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/web/terminal_repl.py +0 -0
  135. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/web/web_sse_compat.py +0 -0
  136. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode/workspace_hints.py +0 -0
  137. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode.egg-info/SOURCES.txt +0 -0
  138. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode.egg-info/dependency_links.txt +0 -0
  139. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode.egg-info/entry_points.txt +0 -0
  140. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode.egg-info/requires.txt +0 -0
  141. {gemcode-0.4.11 → gemcode-0.4.13}/src/gemcode.egg-info/top_level.txt +0 -0
  142. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_add_dir.py +0 -0
  143. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_agent_habits.py +0 -0
  144. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_agent_instruction.py +0 -0
  145. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_autocompact.py +0 -0
  146. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_automations.py +0 -0
  147. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_capability_routing.py +0 -0
  148. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_checkpoint_diff_command.py +0 -0
  149. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_cli_init.py +0 -0
  150. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_compress_memory_tool.py +0 -0
  151. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_computer_use_permissions.py +0 -0
  152. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_context_budget.py +0 -0
  153. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_context_warning.py +0 -0
  154. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_credentials.py +0 -0
  155. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_eval_harness_layout.py +0 -0
  156. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_event_bus.py +0 -0
  157. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_fleet_reports.py +0 -0
  158. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_ide_stdio_attachments.py +0 -0
  159. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_interactive_permission_ask.py +0 -0
  160. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_kaira_ipc_paths.py +0 -0
  161. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_kaira_scheduler.py +0 -0
  162. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_modality_tools.py +0 -0
  163. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_model_error_retry.py +0 -0
  164. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_model_errors.py +0 -0
  165. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_model_routing.py +0 -0
  166. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_multimodal_input.py +0 -0
  167. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_output_styles_and_rules.py +0 -0
  168. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_paths.py +0 -0
  169. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_permissions.py +0 -0
  170. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_prompt_suggestions.py +0 -0
  171. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_repl_commands.py +0 -0
  172. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_repl_slash.py +0 -0
  173. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_session_runtime_cache.py +0 -0
  174. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_skills.py +0 -0
  175. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_slash_commands.py +0 -0
  176. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_slash_completion_registry.py +0 -0
  177. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_thinking_config.py +0 -0
  178. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_token_budget.py +0 -0
  179. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_tool_context_circulation.py +0 -0
  180. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_tools.py +0 -0
  181. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_tools_inspector.py +0 -0
  182. {gemcode-0.4.11 → gemcode-0.4.13}/tests/test_web_sse_adapter.py +0 -0
  183. {gemcode-0.4.11 → gemcode-0.4.13}/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.11
3
+ Version: 0.4.13
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. See [`../docs/configuration.md`](../docs/configuration.md) and [`../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. See [`../docs/configuration.md`](../docs/configuration.md) and [`../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.11"
7
+ version = "0.4.13"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -323,6 +323,11 @@ def make_habits_tools(cfg: GemCodeConfig) -> list:
323
323
  Habits run inside the main GemCode process — no separate daemon needed.
324
324
  They fire as long as GemCode is open (REPL/TUI session).
325
325
 
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.
330
+
326
331
  Args:
327
332
  name: Unique name for this habit (e.g., "test-watch", "nightly-audit").
328
333
  agent: Org member name to run this (e.g., "kaira", "verifier").
@@ -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
 
@@ -400,16 +437,21 @@ class AgentMesh:
400
437
  except Exception:
401
438
  pass
402
439
 
403
- # Persist to fleet reports
440
+ # Persist to fleet reports (include habit + member so the manager inbox reads clearly)
404
441
  try:
405
442
  from gemcode.fleet_reports import maybe_append_job_report
406
443
  fleet_root = resolve_fleet_root(self.cfg.project_root)
407
- maybe_append_job_report(fleet_root, {
444
+ pl: dict[str, Any] = {
408
445
  "job_id": job.job_id,
409
446
  "session_id": job.session_id,
410
447
  "status": "finished",
411
448
  "report": result_text[:8000],
412
- })
449
+ "member": job.member_name or "",
450
+ }
451
+ hm = job.meta.get("habit") if isinstance(job.meta, dict) else None
452
+ if isinstance(hm, dict):
453
+ pl["habit"] = hm
454
+ maybe_append_job_report(fleet_root, pl)
413
455
  except Exception:
414
456
  pass
415
457
 
@@ -434,12 +476,17 @@ class AgentMesh:
434
476
  try:
435
477
  from gemcode.fleet_reports import maybe_append_job_report
436
478
  fleet_root = resolve_fleet_root(self.cfg.project_root)
437
- maybe_append_job_report(fleet_root, {
479
+ plf: dict[str, Any] = {
438
480
  "job_id": job.job_id,
439
481
  "session_id": job.session_id,
440
482
  "status": "failed",
441
483
  "report": job.error[:8000],
442
- })
484
+ "member": job.member_name or "",
485
+ }
486
+ hm = job.meta.get("habit") if isinstance(job.meta, dict) else None
487
+ if isinstance(hm, dict):
488
+ plf["habit"] = hm
489
+ maybe_append_job_report(fleet_root, plf)
443
490
  except Exception:
444
491
  pass
445
492
 
@@ -465,14 +512,17 @@ class AgentMesh:
465
512
  This means agents build up context over time — they remember past tasks,
466
513
  learn from their history, and maintain their own notes.
467
514
  """
515
+ import hashlib
516
+
468
517
  from gemcode.invoke import run_turn
469
- from gemcode.session_runtime import create_runner
518
+ from gemcode.session_runtime import create_runner, session_db_path
470
519
  from gemcode.capability_routing import apply_capability_routing
471
520
  from gemcode.model_routing import pick_effective_model
472
521
 
473
522
  # Resolve the agent's workspace as their project root
474
523
  # This gives them their own .gemcode/ directory, sessions, memory, etc.
475
524
  agent_cfg = copy.deepcopy(self.cfg)
525
+ _apply_mesh_worker_unattended_policy(agent_cfg)
476
526
  fleet_root = resolve_fleet_root(self.cfg.project_root)
477
527
 
478
528
  if job.member_name:
@@ -497,54 +547,56 @@ class AgentMesh:
497
547
  parent_tools = self._build_parent_access_tools(fleet_root)
498
548
  all_extra = (mesh_tools or []) + (parent_tools or [])
499
549
 
500
- # Create a FULL runner rooted at the agent's workspace
501
- # This gives them their own SQLite session DB, their own memory, etc.
502
- runner = create_runner(agent_cfg, extra_tools=all_extra or None)
550
+ # Stable session ID per agent (same id across jobs = durable history).
551
+ stable_session_id = (job.session_id or "").strip()
552
+ if job.member_name:
553
+ stable_session_id = f"agent_{hashlib.sha256(job.member_name.encode()).hexdigest()[:12]}"
554
+ mesh_user_id = job.member_name or "mesh"
503
555
 
504
- try:
505
- # Use a stable session ID per agent so they accumulate history
506
- # (not a random UUID — the same agent keeps the same session across jobs)
507
- stable_session_id = job.session_id
508
- if job.member_name:
509
- # Stable session = agent name hash (persists across jobs)
510
- import hashlib
511
- stable_session_id = f"agent_{hashlib.sha256(job.member_name.encode()).hexdigest()[:12]}"
512
-
513
- # Execute the turn with full power
514
- max_calls = min(int(self.cfg.max_llm_calls or 128), 128)
515
- events = await run_turn(
516
- runner,
517
- user_id=job.member_name or "mesh",
556
+ sqlite_lock = await self._lock_sqlite_session(
557
+ db_path=session_db_path(agent_cfg),
558
+ user_id=mesh_user_id,
518
559
  session_id=stable_session_id,
519
- prompt=job.prompt,
520
- max_llm_calls=max_calls,
521
- cfg=agent_cfg,
522
- consume_fleet_reports=False,
523
- )
560
+ )
524
561
 
525
- # Extract text from events
526
- parts: list[str] = []
527
- for ev in events:
528
- try:
529
- if not ev.content or not ev.content.parts:
530
- continue
531
- if getattr(ev, "author", None) == "user":
532
- continue
533
- for part in ev.content.parts:
534
- t = getattr(part, "text", None)
535
- is_thought = getattr(part, "thought", None)
536
- if isinstance(t, str) and t.strip() and not is_thought:
537
- parts.append(t)
538
- except Exception:
539
- continue
562
+ async with sqlite_lock:
563
+ # One runner + turn at a time per session row — avoids ADK "stale session"
564
+ # when habits or overlapping delegations write events concurrently.
565
+ runner = create_runner(agent_cfg, extra_tools=all_extra or None)
540
566
 
541
- return "".join(parts).strip() or "(no output)"
542
- finally:
543
- # Clean up the runner
544
567
  try:
545
- await runner.close()
546
- except Exception:
547
- pass
568
+ max_calls = min(int(self.cfg.max_llm_calls or 128), 128)
569
+ events = await run_turn(
570
+ runner,
571
+ user_id=mesh_user_id,
572
+ session_id=stable_session_id,
573
+ prompt=job.prompt,
574
+ max_llm_calls=max_calls,
575
+ cfg=agent_cfg,
576
+ consume_fleet_reports=False,
577
+ )
578
+
579
+ parts: list[str] = []
580
+ for ev in events:
581
+ try:
582
+ if not ev.content or not ev.content.parts:
583
+ continue
584
+ if getattr(ev, "author", None) == "user":
585
+ continue
586
+ for part in ev.content.parts:
587
+ t = getattr(part, "text", None)
588
+ is_thought = getattr(part, "thought", None)
589
+ if isinstance(t, str) and t.strip() and not is_thought:
590
+ parts.append(t)
591
+ except Exception:
592
+ continue
593
+
594
+ return "".join(parts).strip() or "(no output)"
595
+ finally:
596
+ try:
597
+ await runner.close()
598
+ except Exception:
599
+ pass
548
600
 
549
601
  def _build_parent_access_tools(self, fleet_root: Path) -> list:
550
602
  """
@@ -33,12 +33,9 @@ def inject_enabled() -> bool:
33
33
 
34
34
 
35
35
  def auto_continue_enabled() -> bool:
36
- return os.environ.get("GEMCODE_FLEET_REPORTS_AUTO_CONTINUE", "").strip().lower() in (
37
- "1",
38
- "true",
39
- "yes",
40
- "on",
41
- )
36
+ """When true, queue a manager digest after each turn if fleet reports are pending (TUI/REPL). Default on; set GEMCODE_FLEET_REPORTS_AUTO_CONTINUE=0 to disable."""
37
+ v = os.environ.get("GEMCODE_FLEET_REPORTS_AUTO_CONTINUE", "1").strip().lower()
38
+ return v not in ("0", "false", "no", "off")
42
39
 
43
40
 
44
41
  def auto_continue_mode() -> str:
@@ -59,9 +56,10 @@ def max_auto_chain() -> int:
59
56
  def fleet_digest_prompt() -> str:
60
57
  return (
61
58
  f"{_DIGEST_MARKER}\n"
62
- "Background agents may have completed work; fleet reports were drained at the start of this turn if any were queued.\n"
63
- "Summarize outcomes for the user in short bullets. Do not call org_delegate, org_spawn, or spawn_subtasks "
64
- "unless you must fix a reported error."
59
+ "New background work arrived (habits, mesh jobs, or agent messages). Summarize for the user in a short, "
60
+ "conversational way—like you are relaying what the other agent just did. Call out what changed since last time "
61
+ "if the report looks like a recurring check. Do not call org_delegate, org_spawn, or spawn_subtasks unless you "
62
+ "must fix a reported error."
65
63
  )
66
64
 
67
65
 
@@ -248,6 +246,15 @@ def _format_record(rec: dict[str, Any]) -> str:
248
246
  f"[job.report] job_id={payload.get('job_id')} status={payload.get('status')} "
249
247
  f"session_id={payload.get('session_id')}"
250
248
  )
249
+ mem = str(payload.get("member") or "").strip()
250
+ if mem:
251
+ lines.append(f" member: {mem}")
252
+ hab = payload.get("habit")
253
+ if isinstance(hab, dict):
254
+ hn = str(hab.get("name") or "").strip()
255
+ ha = str(hab.get("agent") or "").strip()
256
+ if hn:
257
+ lines.append(f" habit: {hn}" + (f" (runs as agent `{ha}`)" if ha else ""))
251
258
  rpt = str(payload.get("report") or "").strip()
252
259
  if rpt:
253
260
  lines.append(f" report: {rpt[:8000]}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.11
3
+ Version: 0.4.13
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. See [`../docs/configuration.md`](../docs/configuration.md) and [`../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)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes