gemcode 0.4.14__tar.gz → 0.4.16__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.14/src/gemcode.egg-info → gemcode-0.4.16}/PKG-INFO +1 -1
  2. {gemcode-0.4.14 → gemcode-0.4.16}/pyproject.toml +1 -1
  3. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/agent_mesh.py +194 -49
  4. {gemcode-0.4.14 → gemcode-0.4.16/src/gemcode.egg-info}/PKG-INFO +1 -1
  5. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_agent_mesh.py +86 -35
  6. {gemcode-0.4.14 → gemcode-0.4.16}/LICENSE +0 -0
  7. {gemcode-0.4.14 → gemcode-0.4.16}/MANIFEST.in +0 -0
  8. {gemcode-0.4.14 → gemcode-0.4.16}/README.md +0 -0
  9. {gemcode-0.4.14 → gemcode-0.4.16}/setup.cfg +0 -0
  10. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/__init__.py +0 -0
  11. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/__main__.py +0 -0
  12. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/a2a_bridge.py +0 -0
  13. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/agent.py +0 -0
  14. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/agent_habits.py +0 -0
  15. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/agent_intelligence.py +0 -0
  16. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/agent_triggers.py +0 -0
  17. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/audit.py +0 -0
  18. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/autocompact.py +0 -0
  19. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/automations.py +0 -0
  20. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/autotune.py +0 -0
  21. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/callbacks.py +0 -0
  22. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/capability_routing.py +0 -0
  23. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/checkpoints.py +0 -0
  24. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/cli.py +0 -0
  25. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/codebase_awareness.py +0 -0
  26. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/compaction.py +0 -0
  27. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/computer_use/__init__.py +0 -0
  28. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/computer_use/browser_computer.py +0 -0
  29. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/config.py +0 -0
  30. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/context_budget.py +0 -0
  31. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/context_warning.py +0 -0
  32. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/credentials.py +0 -0
  33. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/curated_memory.py +0 -0
  34. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/delegation_learning.py +0 -0
  35. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/dynamic_policy.py +0 -0
  36. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/evals/harness.py +0 -0
  37. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/event_bus.py +0 -0
  38. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/fleet_reports.py +0 -0
  39. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/hitl_session.py +0 -0
  40. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/hooks.py +0 -0
  41. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/ide_protocol.py +0 -0
  42. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/ide_stdio.py +0 -0
  43. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/intent_classifier.py +0 -0
  44. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/interactions.py +0 -0
  45. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/invoke.py +0 -0
  46. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/kaira_client.py +0 -0
  47. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/kaira_daemon.py +0 -0
  48. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/kaira_ipc.py +0 -0
  49. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/kaira_job_store.py +0 -0
  50. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/learning.py +0 -0
  51. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/limits.py +0 -0
  52. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/live_audio_engine.py +0 -0
  53. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/logging_config.py +0 -0
  54. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/mcp_loader.py +0 -0
  55. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/memory/__init__.py +0 -0
  56. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/memory/embedding_memory_service.py +0 -0
  57. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/memory/file_memory_service.py +0 -0
  58. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/modality_tools.py +0 -0
  59. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/model_errors.py +0 -0
  60. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/model_routing.py +0 -0
  61. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/multimodal_input.py +0 -0
  62. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/openapi_loader.py +0 -0
  63. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/org.py +0 -0
  64. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/output_styles.py +0 -0
  65. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/paths.py +0 -0
  66. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/permissions.py +0 -0
  67. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/plugins/__init__.py +0 -0
  68. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  69. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  70. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/policy_profile.py +0 -0
  71. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/pricing.py +0 -0
  72. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/prompt_suggestions.py +0 -0
  73. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query/__init__.py +0 -0
  74. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query/config.py +0 -0
  75. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query/deps.py +0 -0
  76. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query/engine.py +0 -0
  77. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query/stop_hooks.py +0 -0
  78. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query/token_budget.py +0 -0
  79. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query/transitions.py +0 -0
  80. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query_sanitizer.py +0 -0
  81. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/refine.py +0 -0
  82. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/repl_commands.py +0 -0
  83. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/repl_slash.py +0 -0
  84. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/review_agent.py +0 -0
  85. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/rules.py +0 -0
  86. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/self_healing.py +0 -0
  87. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/session_runtime.py +0 -0
  88. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/session_store.py +0 -0
  89. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/session_summariser.py +0 -0
  90. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/skills.py +0 -0
  91. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/slash_commands.py +0 -0
  92. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/thinking.py +0 -0
  93. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tool_prompt_manifest.py +0 -0
  94. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tool_registry.py +0 -0
  95. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tool_result_store.py +0 -0
  96. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tool_synthesis.py +0 -0
  97. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/__init__.py +0 -0
  98. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/automations_tools.py +0 -0
  99. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/bash.py +0 -0
  100. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/browser.py +0 -0
  101. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/compress_memory.py +0 -0
  102. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/curated_memory.py +0 -0
  103. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/edit.py +0 -0
  104. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/filesystem.py +0 -0
  105. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/notebook.py +0 -0
  106. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/notes.py +0 -0
  107. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/org_tools.py +0 -0
  108. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/repo_map.py +0 -0
  109. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/search.py +0 -0
  110. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/shell.py +0 -0
  111. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/shell_gate.py +0 -0
  112. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/skills.py +0 -0
  113. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/subtask.py +0 -0
  114. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/tasks.py +0 -0
  115. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/think.py +0 -0
  116. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/todo.py +0 -0
  117. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/user_choice.py +0 -0
  118. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/veomem_tools.py +0 -0
  119. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/web.py +0 -0
  120. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/web_search.py +0 -0
  121. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools_inspector.py +0 -0
  122. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/trust.py +0 -0
  123. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tui/input_handler.py +0 -0
  124. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tui/scrollback.py +0 -0
  125. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tui/spinner.py +0 -0
  126. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tui/welcome_banner.py +0 -0
  127. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tui/welcome_rich.py +0 -0
  128. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/veomem_bridge.py +0 -0
  129. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/version.py +0 -0
  130. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/vertex.py +0 -0
  131. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/wal.py +0 -0
  132. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/web/__init__.py +0 -0
  133. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/web/sse_adapter.py +0 -0
  134. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/web/terminal_repl.py +0 -0
  135. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/web/web_sse_compat.py +0 -0
  136. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/workspace_hints.py +0 -0
  137. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode.egg-info/SOURCES.txt +0 -0
  138. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode.egg-info/dependency_links.txt +0 -0
  139. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode.egg-info/entry_points.txt +0 -0
  140. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode.egg-info/requires.txt +0 -0
  141. {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode.egg-info/top_level.txt +0 -0
  142. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_add_dir.py +0 -0
  143. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_agent_habits.py +0 -0
  144. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_agent_instruction.py +0 -0
  145. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_autocompact.py +0 -0
  146. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_automations.py +0 -0
  147. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_capability_routing.py +0 -0
  148. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_checkpoint_diff_command.py +0 -0
  149. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_cli_init.py +0 -0
  150. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_compress_memory_tool.py +0 -0
  151. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_computer_use_permissions.py +0 -0
  152. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_context_budget.py +0 -0
  153. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_context_warning.py +0 -0
  154. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_credentials.py +0 -0
  155. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_eval_harness_layout.py +0 -0
  156. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_event_bus.py +0 -0
  157. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_fleet_reports.py +0 -0
  158. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_ide_stdio_attachments.py +0 -0
  159. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_interactive_permission_ask.py +0 -0
  160. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_kaira_ipc_paths.py +0 -0
  161. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_kaira_scheduler.py +0 -0
  162. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_modality_tools.py +0 -0
  163. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_model_error_retry.py +0 -0
  164. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_model_errors.py +0 -0
  165. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_model_routing.py +0 -0
  166. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_multimodal_input.py +0 -0
  167. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_output_styles_and_rules.py +0 -0
  168. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_paths.py +0 -0
  169. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_permissions.py +0 -0
  170. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_prompt_suggestions.py +0 -0
  171. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_repl_commands.py +0 -0
  172. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_repl_slash.py +0 -0
  173. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_session_runtime_cache.py +0 -0
  174. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_skills.py +0 -0
  175. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_slash_commands.py +0 -0
  176. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_slash_completion_registry.py +0 -0
  177. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_thinking_config.py +0 -0
  178. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_token_budget.py +0 -0
  179. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_tool_context_circulation.py +0 -0
  180. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_tools.py +0 -0
  181. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_tools_inspector.py +0 -0
  182. {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_web_sse_adapter.py +0 -0
  183. {gemcode-0.4.14 → gemcode-0.4.16}/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.14
3
+ Version: 0.4.16
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.4.14"
7
+ version = "0.4.16"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -16,8 +16,10 @@ Slash **`/agent assign`** / **`trigger`** publish `org.assign` over IPC when the
16
16
  from __future__ import annotations
17
17
 
18
18
  import asyncio
19
+ import concurrent.futures
19
20
  import copy
20
21
  import os
22
+ import threading
21
23
  import time
22
24
  import uuid
23
25
  from dataclasses import dataclass, field
@@ -42,6 +44,8 @@ class AgentJob:
42
44
  result: str = ""
43
45
  error: str = ""
44
46
  created_ms: int = field(default_factory=lambda: int(time.time() * 1000))
47
+ # When set, completed with this AgentJob from the mesh thread (cross-thread wait).
48
+ completion_future: concurrent.futures.Future | None = None
45
49
 
46
50
 
47
51
  def _apply_mesh_worker_unattended_policy(cfg: GemCodeConfig) -> None:
@@ -80,6 +84,11 @@ class AgentMesh:
80
84
  self._sem = asyncio.Semaphore(self.max_concurrency)
81
85
  self._running: dict[str, asyncio.Task] = {}
82
86
  self._completed: list[AgentJob] = []
87
+ self._completed_lock = threading.Lock()
88
+ self._enqueue_lock = threading.Lock()
89
+ self._bg_thread_ident: int | None = None
90
+ # >0 while executing a mesh job on the background loop (nested delegate avoids deadlock).
91
+ self._mesh_job_depth: int = 0
83
92
  self._scheduler_task: asyncio.Task | None = None
84
93
  self._stop: asyncio.Event | None = None # Created in background loop
85
94
  self._bg_thread: "threading.Thread | None" = None
@@ -172,6 +181,7 @@ class AgentMesh:
172
181
  """Background thread entry: create a new event loop and run the scheduler."""
173
182
  self._bg_loop = asyncio.new_event_loop()
174
183
  asyncio.set_event_loop(self._bg_loop)
184
+ self._bg_thread_ident = threading.current_thread().ident
175
185
  try:
176
186
  self._bg_loop.run_until_complete(self._bg_main())
177
187
  except Exception:
@@ -190,7 +200,9 @@ class AgentMesh:
190
200
  self._stop = asyncio.Event() # Create in the correct loop
191
201
 
192
202
  # Start all sub-systems in this loop
193
- self._scheduler_task = asyncio.create_task(self._scheduler_loop())
203
+ # Tests can set PYTEST_GEMCODE_MESH_SCHEDULER=0 to inspect the queue without workers consuming it.
204
+ if os.environ.get("PYTEST_GEMCODE_MESH_SCHEDULER", "").strip() != "0":
205
+ self._scheduler_task = asyncio.create_task(self._scheduler_loop())
194
206
 
195
207
  if self._trigger_engine is not None:
196
208
  self._trigger_engine.start()
@@ -217,6 +229,40 @@ class AgentMesh:
217
229
  except Exception:
218
230
  pass
219
231
 
232
+ def _is_on_mesh_thread(self) -> bool:
233
+ ident = threading.current_thread().ident
234
+ return self._bg_thread_ident is not None and ident == self._bg_thread_ident
235
+
236
+ def _wait_bg_loop_ready(self, timeout_s: float = 5.0) -> bool:
237
+ """Wait until the background thread has assigned ``_bg_loop``."""
238
+ deadline = time.time() + timeout_s
239
+ while time.time() < deadline:
240
+ if self._bg_loop is not None:
241
+ return True
242
+ if self._bg_thread is not None and not self._bg_thread.is_alive():
243
+ return False
244
+ time.sleep(0.001)
245
+ return self._bg_loop is not None
246
+
247
+ def wait_for_pending_enqueues(self, timeout: float = 5.0) -> None:
248
+ """
249
+ Block until callbacks scheduled with ``call_soon_threadsafe`` (for enqueues
250
+ from other threads) have run on the mesh loop. Useful in tests.
251
+ """
252
+ if self._bg_loop is None:
253
+ return
254
+ fut: concurrent.futures.Future[None] = concurrent.futures.Future()
255
+
256
+ def _poke() -> None:
257
+ if not fut.done():
258
+ try:
259
+ fut.set_result(None)
260
+ except Exception:
261
+ pass
262
+
263
+ self._bg_loop.call_soon_threadsafe(_poke)
264
+ fut.result(timeout=timeout)
265
+
220
266
  def enqueue(
221
267
  self,
222
268
  *,
@@ -225,10 +271,13 @@ class AgentMesh:
225
271
  session_id: str = "",
226
272
  member_name: str = "",
227
273
  meta: dict[str, Any] | None = None,
274
+ completion_future: concurrent.futures.Future | None = None,
228
275
  ) -> str:
229
- """Enqueue a job and return its job_id. Thread-safe."""
276
+ """Enqueue a job and return its job_id. Safe to call from any thread."""
230
277
  job_id = f"mesh_{uuid.uuid4().hex[:10]}"
231
- self._seq += 1
278
+ with self._enqueue_lock:
279
+ self._seq += 1
280
+ seq = self._seq
232
281
  job = AgentJob(
233
282
  job_id=job_id,
234
283
  prompt=prompt,
@@ -236,22 +285,45 @@ class AgentMesh:
236
285
  session_id=session_id or str(uuid.uuid4()),
237
286
  member_name=member_name,
238
287
  meta=meta or {},
288
+ completion_future=completion_future,
239
289
  )
240
- # Higher priority = runs first (negate for min-heap)
241
- self._queue.put_nowait((-priority, self._seq, job))
290
+ item = (-priority, seq, job)
242
291
 
243
- # Publish queued event
244
- self._bus.publish_sync(BusMessage(
245
- topic="job.queued",
246
- from_addr="mesh",
247
- to_addr="manager",
248
- payload={"job_id": job_id, "member": member_name, "priority": priority},
249
- ))
292
+ def do_put() -> None:
293
+ try:
294
+ self._queue.put_nowait(item)
295
+ except Exception as e:
296
+ fut = job.completion_future
297
+ if fut is not None and not fut.done():
298
+ try:
299
+ fut.set_exception(e)
300
+ except Exception:
301
+ pass
302
+ return
303
+ self._bus.publish_sync(BusMessage(
304
+ topic="job.queued",
305
+ from_addr="mesh",
306
+ to_addr="manager",
307
+ payload={"job_id": job_id, "member": member_name, "priority": priority},
308
+ ))
250
309
 
251
- # Auto-start the background thread if not running
252
310
  if self._bg_thread is None or not self._bg_thread.is_alive():
253
311
  self.start()
254
312
 
313
+ if not self._wait_bg_loop_ready():
314
+ fut = job.completion_future
315
+ if fut is not None and not fut.done():
316
+ try:
317
+ fut.set_exception(RuntimeError("mesh background loop failed to start"))
318
+ except Exception:
319
+ pass
320
+ return job_id
321
+
322
+ if self._is_on_mesh_thread():
323
+ do_put()
324
+ else:
325
+ self._bg_loop.call_soon_threadsafe(do_put)
326
+
255
327
  return job_id
256
328
 
257
329
  async def delegate_to_member(
@@ -303,39 +375,63 @@ class AgentMesh:
303
375
  if context:
304
376
  full_prompt += "\n\nContext:\n" + context
305
377
 
378
+ org_meta = {
379
+ "org": {
380
+ "member": m.to_dict() if hasattr(m, "to_dict") else {},
381
+ "task": task,
382
+ "context": context,
383
+ }
384
+ }
385
+
386
+ if not wait:
387
+ job_id = self.enqueue(
388
+ prompt=full_prompt,
389
+ priority=priority,
390
+ session_id="",
391
+ member_name=m.name,
392
+ meta=org_meta,
393
+ )
394
+ return {"ok": True, "job_id": job_id, "delegated_to": m.name, "async": True}
395
+
396
+ # Nested wait=True from inside a running mesh job would deadlock the scheduler
397
+ # (parent holds a concurrency slot while the child waits for another slot).
398
+ # Only use the inline path on the mesh thread: the manager runs on a different
399
+ # event loop; _mesh_job_depth can be >0 while a habit runs, but inline
400
+ # _run_job_inner must not run there (asyncio locks / ADK are mesh-loop-local).
401
+ if self._is_on_mesh_thread() and self._mesh_job_depth > 0:
402
+ job_id = f"mesh_{uuid.uuid4().hex[:10]}"
403
+ with self._enqueue_lock:
404
+ self._seq += 1
405
+ inline_job = AgentJob(
406
+ job_id=job_id,
407
+ prompt=full_prompt,
408
+ priority=priority,
409
+ session_id="",
410
+ member_name=m.name,
411
+ meta=org_meta,
412
+ )
413
+ await self._run_job_inner(inline_job)
414
+ if inline_job.status == "finished":
415
+ return {"ok": True, "job_id": job_id, "result": inline_job.result}
416
+ return {"ok": False, "job_id": job_id, "error": inline_job.error}
417
+
418
+ loop = asyncio.get_running_loop()
419
+ fut: concurrent.futures.Future[AgentJob] = concurrent.futures.Future()
306
420
  job_id = self.enqueue(
307
421
  prompt=full_prompt,
308
422
  priority=priority,
309
423
  session_id="",
310
424
  member_name=m.name,
311
- meta={
312
- "org": {
313
- "member": m.to_dict() if hasattr(m, "to_dict") else {},
314
- "task": task,
315
- "context": context,
316
- }
317
- },
425
+ meta=org_meta,
426
+ completion_future=fut,
318
427
  )
319
-
320
- if not wait:
321
- return {"ok": True, "job_id": job_id, "delegated_to": m.name, "async": True}
322
-
323
- # Wait for completion
324
- result = await self._wait_for_job(job_id, timeout=300.0)
325
- return result
326
-
327
- async def _wait_for_job(self, job_id: str, timeout: float = 300.0) -> dict[str, Any]:
328
- """Wait for a specific job to complete."""
329
- deadline = time.time() + timeout
330
- while time.time() < deadline:
331
- for job in self._completed:
332
- if job.job_id == job_id:
333
- if job.status == "finished":
334
- return {"ok": True, "job_id": job_id, "result": job.result}
335
- else:
336
- return {"ok": False, "job_id": job_id, "error": job.error}
337
- await asyncio.sleep(0.1)
338
- return {"ok": False, "job_id": job_id, "error": "timeout"}
428
+ try:
429
+ job = await asyncio.wait_for(asyncio.wrap_future(fut, loop=loop), timeout=300.0)
430
+ except asyncio.TimeoutError:
431
+ return {"ok": False, "job_id": job_id, "error": "timeout"}
432
+ if job.status == "finished":
433
+ return {"ok": True, "job_id": job_id, "result": job.result}
434
+ return {"ok": False, "job_id": job_id, "error": job.error}
339
435
 
340
436
  async def _handle_org_assign(self, msg: BusMessage) -> None:
341
437
  """Handle org.assign bus messages (A2A-style delegation)."""
@@ -373,6 +469,14 @@ class AgentMesh:
373
469
 
374
470
  async def _run_job(self, job: AgentJob) -> None:
375
471
  """Execute a single job using a fresh ADK Runner."""
472
+ self._mesh_job_depth += 1
473
+ try:
474
+ await self._run_job_inner(job)
475
+ finally:
476
+ self._mesh_job_depth -= 1
477
+
478
+ async def _run_job_inner(self, job: AgentJob) -> None:
479
+ """Full job lifecycle (shared by the scheduler and nested inline delegation)."""
376
480
  job.status = "running"
377
481
  start_ms = int(time.time() * 1000)
378
482
 
@@ -499,10 +603,17 @@ class AgentMesh:
499
603
  pass
500
604
 
501
605
  finally:
502
- self._completed.append(job)
503
- # Keep completed list bounded
504
- if len(self._completed) > 200:
505
- self._completed = self._completed[-100:]
606
+ cf = job.completion_future
607
+ if cf is not None and not cf.done():
608
+ try:
609
+ cf.set_result(job)
610
+ except Exception:
611
+ pass
612
+ with self._completed_lock:
613
+ self._completed.append(job)
614
+ # Keep completed list bounded
615
+ if len(self._completed) > 200:
616
+ self._completed = self._completed[-100:]
506
617
 
507
618
  async def _execute_agent_turn(self, job: AgentJob) -> str:
508
619
  """
@@ -740,15 +851,18 @@ class AgentMesh:
740
851
 
741
852
  def status(self) -> dict[str, Any]:
742
853
  """Get mesh status for debugging/display."""
854
+ with self._completed_lock:
855
+ recent = [
856
+ {"job_id": j.job_id, "member": j.member_name, "status": j.status}
857
+ for j in self._completed[-10:]
858
+ ]
859
+ n_completed = len(self._completed)
743
860
  return {
744
861
  "running_jobs": len(self._running),
745
862
  "queued_jobs": self._queue.qsize(),
746
- "completed_jobs": len(self._completed),
863
+ "completed_jobs": n_completed,
747
864
  "max_concurrency": self.max_concurrency,
748
- "recent_completed": [
749
- {"job_id": j.job_id, "member": j.member_name, "status": j.status}
750
- for j in self._completed[-10:]
751
- ],
865
+ "recent_completed": recent,
752
866
  }
753
867
 
754
868
 
@@ -757,12 +871,41 @@ class AgentMesh:
757
871
  _global_mesh: AgentMesh | None = None
758
872
 
759
873
 
874
+ def _sync_mesh_cfg_project_root(mesh: AgentMesh, cfg: GemCodeConfig) -> None:
875
+ """
876
+ Keep the singleton aligned with the active manager ``cfg.project_root``.
877
+
878
+ If the mesh was created from a different cwd than a later ``ensure_mesh`` /
879
+ ``get_mesh`` (e.g. ``-C``), habits/triggers/fleet would otherwise read the
880
+ wrong ``.gemcode/`` tree while the UI used another.
881
+ """
882
+ try:
883
+ cur = Path(mesh.cfg.project_root).resolve()
884
+ nxt = Path(cfg.project_root).resolve()
885
+ if cur == nxt:
886
+ return
887
+ mesh.cfg.project_root = cfg.project_root
888
+ if mesh._trigger_engine is not None:
889
+ mesh._trigger_engine.cfg = mesh.cfg
890
+ mesh._trigger_engine.reload()
891
+ if mesh._habit_scheduler is not None:
892
+ mesh._habit_scheduler.cfg = mesh.cfg
893
+ if mesh._learner is not None:
894
+ mesh._learner.cfg = mesh.cfg
895
+ if mesh._self_healing is not None:
896
+ mesh._self_healing.cfg = mesh.cfg
897
+ except Exception:
898
+ pass
899
+
900
+
760
901
  def get_mesh(cfg: GemCodeConfig | None = None) -> AgentMesh | None:
761
902
  """Get or create the global agent mesh."""
762
903
  global _global_mesh
763
904
  if _global_mesh is None and cfg is not None:
764
905
  concurrency = int(os.environ.get("GEMCODE_MESH_CONCURRENCY", "3"))
765
906
  _global_mesh = AgentMesh(cfg, max_concurrency=concurrency)
907
+ elif _global_mesh is not None and cfg is not None:
908
+ _sync_mesh_cfg_project_root(_global_mesh, cfg)
766
909
  return _global_mesh
767
910
 
768
911
 
@@ -772,6 +915,8 @@ def ensure_mesh(cfg: GemCodeConfig) -> AgentMesh:
772
915
  if _global_mesh is None:
773
916
  concurrency = int(os.environ.get("GEMCODE_MESH_CONCURRENCY", "3"))
774
917
  _global_mesh = AgentMesh(cfg, max_concurrency=concurrency)
918
+ else:
919
+ _sync_mesh_cfg_project_root(_global_mesh, cfg)
775
920
  return _global_mesh
776
921
 
777
922
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.14
3
+ Version: 0.4.16
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import os
7
+ import threading
8
+ import time
7
9
  from pathlib import Path
8
10
 
9
11
  import pytest
@@ -63,22 +65,52 @@ def test_mesh_singleton(tmp_path: Path) -> None:
63
65
 
64
66
 
65
67
  def test_mesh_enqueue(tmp_path: Path) -> None:
66
- cfg = GemCodeConfig(project_root=tmp_path)
67
- mesh = AgentMesh(cfg, max_concurrency=2)
68
- job_id = mesh.enqueue(prompt="test task", priority=5, member_name="worker")
69
- assert job_id.startswith("mesh_")
70
- assert mesh._queue.qsize() == 1
68
+ old_s = os.environ.get("PYTEST_GEMCODE_MESH_SCHEDULER")
69
+ old_h = os.environ.get("GEMCODE_AGENT_HABITS")
70
+ os.environ["PYTEST_GEMCODE_MESH_SCHEDULER"] = "0"
71
+ os.environ["GEMCODE_AGENT_HABITS"] = "0"
72
+ try:
73
+ cfg = GemCodeConfig(project_root=tmp_path)
74
+ mesh = AgentMesh(cfg, max_concurrency=2)
75
+ job_id = mesh.enqueue(prompt="test task", priority=5, member_name="worker")
76
+ assert job_id.startswith("mesh_")
77
+ mesh.wait_for_pending_enqueues()
78
+ assert mesh._queue.qsize() == 1
79
+ finally:
80
+ if old_s is None:
81
+ os.environ.pop("PYTEST_GEMCODE_MESH_SCHEDULER", None)
82
+ else:
83
+ os.environ["PYTEST_GEMCODE_MESH_SCHEDULER"] = old_s
84
+ if old_h is None:
85
+ os.environ.pop("GEMCODE_AGENT_HABITS", None)
86
+ else:
87
+ os.environ["GEMCODE_AGENT_HABITS"] = old_h
71
88
 
72
89
 
73
90
  def test_mesh_status(tmp_path: Path) -> None:
74
- cfg = GemCodeConfig(project_root=tmp_path)
75
- mesh = AgentMesh(cfg, max_concurrency=2)
76
- mesh.enqueue(prompt="task1", priority=1, member_name="a")
77
- mesh.enqueue(prompt="task2", priority=2, member_name="b")
78
- status = mesh.status()
79
- assert status["queued_jobs"] == 2
80
- assert status["running_jobs"] == 0
81
- assert status["max_concurrency"] == 2
91
+ old_s = os.environ.get("PYTEST_GEMCODE_MESH_SCHEDULER")
92
+ old_h = os.environ.get("GEMCODE_AGENT_HABITS")
93
+ os.environ["PYTEST_GEMCODE_MESH_SCHEDULER"] = "0"
94
+ os.environ["GEMCODE_AGENT_HABITS"] = "0"
95
+ try:
96
+ cfg = GemCodeConfig(project_root=tmp_path)
97
+ mesh = AgentMesh(cfg, max_concurrency=2)
98
+ mesh.enqueue(prompt="task1", priority=1, member_name="a")
99
+ mesh.enqueue(prompt="task2", priority=2, member_name="b")
100
+ mesh.wait_for_pending_enqueues()
101
+ status = mesh.status()
102
+ assert status["queued_jobs"] == 2
103
+ assert status["running_jobs"] == 0
104
+ assert status["max_concurrency"] == 2
105
+ finally:
106
+ if old_s is None:
107
+ os.environ.pop("PYTEST_GEMCODE_MESH_SCHEDULER", None)
108
+ else:
109
+ os.environ["PYTEST_GEMCODE_MESH_SCHEDULER"] = old_s
110
+ if old_h is None:
111
+ os.environ.pop("GEMCODE_AGENT_HABITS", None)
112
+ else:
113
+ os.environ["GEMCODE_AGENT_HABITS"] = old_h
82
114
 
83
115
 
84
116
  def test_mesh_bus_integration(tmp_path: Path) -> None:
@@ -88,17 +120,21 @@ def test_mesh_bus_integration(tmp_path: Path) -> None:
88
120
  bus = get_bus()
89
121
 
90
122
  received: list[BusMessage] = []
123
+ got = threading.Event()
124
+
125
+ async def capture(msg: BusMessage) -> None:
126
+ received.append(msg)
127
+ got.set()
91
128
 
92
129
  async def run():
93
- sub = bus.subscribe(topic="job.queued")
130
+ bus.subscribe(topic="job.queued", callback=capture)
94
131
  mesh.enqueue(prompt="hello", priority=0, member_name="test")
95
- # Give the sync publish a moment to schedule
96
- await asyncio.sleep(0.1)
97
- msg = await sub.get(timeout=1.0)
98
- if msg:
99
- received.append(msg)
132
+ mesh.wait_for_pending_enqueues()
100
133
 
101
134
  asyncio.run(run())
135
+ # Mesh loop processes publish asynchronously on a background thread.
136
+ assert got.wait(timeout=3.0), "timed out waiting for job.queued on bus"
137
+ time.sleep(0.01)
102
138
  assert len(received) == 1
103
139
  assert received[0].payload["member"] == "test"
104
140
 
@@ -139,19 +175,34 @@ def test_apply_mesh_worker_unattended_off_inherits_manager(tmp_path: Path) -> No
139
175
 
140
176
  def test_mesh_priority_ordering(tmp_path: Path) -> None:
141
177
  """Higher priority jobs should be dequeued first."""
142
- cfg = GemCodeConfig(project_root=tmp_path)
143
- mesh = AgentMesh(cfg, max_concurrency=1)
144
-
145
- mesh.enqueue(prompt="low", priority=1, member_name="a")
146
- mesh.enqueue(prompt="high", priority=10, member_name="b")
147
- mesh.enqueue(prompt="mid", priority=5, member_name="c")
148
-
149
- # Drain the queue manually to check ordering
150
- items = []
151
- while not mesh._queue.empty():
152
- neg_pri, seq, job = mesh._queue.get_nowait()
153
- items.append((job.prompt, job.priority))
154
-
155
- assert items[0] == ("high", 10)
156
- assert items[1] == ("mid", 5)
157
- assert items[2] == ("low", 1)
178
+ old_s = os.environ.get("PYTEST_GEMCODE_MESH_SCHEDULER")
179
+ old_h = os.environ.get("GEMCODE_AGENT_HABITS")
180
+ os.environ["PYTEST_GEMCODE_MESH_SCHEDULER"] = "0"
181
+ os.environ["GEMCODE_AGENT_HABITS"] = "0"
182
+ try:
183
+ cfg = GemCodeConfig(project_root=tmp_path)
184
+ mesh = AgentMesh(cfg, max_concurrency=1)
185
+
186
+ mesh.enqueue(prompt="low", priority=1, member_name="a")
187
+ mesh.enqueue(prompt="high", priority=10, member_name="b")
188
+ mesh.enqueue(prompt="mid", priority=5, member_name="c")
189
+ mesh.wait_for_pending_enqueues()
190
+
191
+ # Drain the queue manually to check ordering
192
+ items = []
193
+ while not mesh._queue.empty():
194
+ neg_pri, seq, job = mesh._queue.get_nowait()
195
+ items.append((job.prompt, job.priority))
196
+
197
+ assert items[0] == ("high", 10)
198
+ assert items[1] == ("mid", 5)
199
+ assert items[2] == ("low", 1)
200
+ finally:
201
+ if old_s is None:
202
+ os.environ.pop("PYTEST_GEMCODE_MESH_SCHEDULER", None)
203
+ else:
204
+ os.environ["PYTEST_GEMCODE_MESH_SCHEDULER"] = old_s
205
+ if old_h is None:
206
+ os.environ.pop("GEMCODE_AGENT_HABITS", None)
207
+ else:
208
+ os.environ["GEMCODE_AGENT_HABITS"] = old_h
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