gemcode 0.3.97__tar.gz → 0.3.99__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 (157) hide show
  1. {gemcode-0.3.97/src/gemcode.egg-info → gemcode-0.3.99}/PKG-INFO +1 -1
  2. {gemcode-0.3.97 → gemcode-0.3.99}/pyproject.toml +1 -1
  3. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/invoke.py +26 -8
  4. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/multimodal_input.py +18 -0
  5. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/session_summariser.py +53 -13
  6. {gemcode-0.3.97 → gemcode-0.3.99/src/gemcode.egg-info}/PKG-INFO +1 -1
  7. {gemcode-0.3.97 → gemcode-0.3.99}/LICENSE +0 -0
  8. {gemcode-0.3.97 → gemcode-0.3.99}/MANIFEST.in +0 -0
  9. {gemcode-0.3.97 → gemcode-0.3.99}/README.md +0 -0
  10. {gemcode-0.3.97 → gemcode-0.3.99}/setup.cfg +0 -0
  11. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/__init__.py +0 -0
  12. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/__main__.py +0 -0
  13. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/agent.py +0 -0
  14. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/audit.py +0 -0
  15. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/autocompact.py +0 -0
  16. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/autotune.py +0 -0
  17. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/callbacks.py +0 -0
  18. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/capability_routing.py +0 -0
  19. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/checkpoints.py +0 -0
  20. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/cli.py +0 -0
  21. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/compaction.py +0 -0
  22. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/computer_use/__init__.py +0 -0
  23. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/computer_use/browser_computer.py +0 -0
  24. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/config.py +0 -0
  25. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/context_budget.py +0 -0
  26. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/context_warning.py +0 -0
  27. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/credentials.py +0 -0
  28. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/curated_memory.py +0 -0
  29. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/dynamic_policy.py +0 -0
  30. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/evals/harness.py +0 -0
  31. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/hitl_session.py +0 -0
  32. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/hooks.py +0 -0
  33. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/ide_protocol.py +0 -0
  34. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/ide_stdio.py +0 -0
  35. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/intent_classifier.py +0 -0
  36. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/interactions.py +0 -0
  37. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/kaira_daemon.py +0 -0
  38. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/learning.py +0 -0
  39. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/limits.py +0 -0
  40. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/live_audio_engine.py +0 -0
  41. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/logging_config.py +0 -0
  42. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/mcp_loader.py +0 -0
  43. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/memory/__init__.py +0 -0
  44. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/memory/embedding_memory_service.py +0 -0
  45. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/memory/file_memory_service.py +0 -0
  46. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/modality_tools.py +0 -0
  47. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/model_errors.py +0 -0
  48. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/model_routing.py +0 -0
  49. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/openapi_loader.py +0 -0
  50. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/output_styles.py +0 -0
  51. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/paths.py +0 -0
  52. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/permissions.py +0 -0
  53. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/plugins/__init__.py +0 -0
  54. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  55. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  56. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/policy_profile.py +0 -0
  57. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/pricing.py +0 -0
  58. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/prompt_suggestions.py +0 -0
  59. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/query/__init__.py +0 -0
  60. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/query/config.py +0 -0
  61. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/query/deps.py +0 -0
  62. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/query/engine.py +0 -0
  63. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/query/stop_hooks.py +0 -0
  64. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/query/token_budget.py +0 -0
  65. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/query/transitions.py +0 -0
  66. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/query_sanitizer.py +0 -0
  67. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/refine.py +0 -0
  68. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/repl_commands.py +0 -0
  69. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/repl_slash.py +0 -0
  70. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/review_agent.py +0 -0
  71. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/rules.py +0 -0
  72. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/session_runtime.py +0 -0
  73. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/session_store.py +0 -0
  74. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/skills.py +0 -0
  75. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/slash_commands.py +0 -0
  76. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/thinking.py +0 -0
  77. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tool_prompt_manifest.py +0 -0
  78. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tool_registry.py +0 -0
  79. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tool_result_store.py +0 -0
  80. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/__init__.py +0 -0
  81. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/bash.py +0 -0
  82. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/browser.py +0 -0
  83. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/compress_memory.py +0 -0
  84. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/curated_memory.py +0 -0
  85. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/edit.py +0 -0
  86. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/filesystem.py +0 -0
  87. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/notebook.py +0 -0
  88. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/notes.py +0 -0
  89. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/repo_map.py +0 -0
  90. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/search.py +0 -0
  91. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/shell.py +0 -0
  92. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/shell_gate.py +0 -0
  93. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/skills.py +0 -0
  94. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/subtask.py +0 -0
  95. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/tasks.py +0 -0
  96. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/think.py +0 -0
  97. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/todo.py +0 -0
  98. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/veomem_tools.py +0 -0
  99. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/web.py +0 -0
  100. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools/web_search.py +0 -0
  101. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tools_inspector.py +0 -0
  102. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/trust.py +0 -0
  103. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tui/input_handler.py +0 -0
  104. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tui/scrollback.py +0 -0
  105. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tui/spinner.py +0 -0
  106. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tui/welcome_banner.py +0 -0
  107. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/tui/welcome_rich.py +0 -0
  108. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/veomem_bridge.py +0 -0
  109. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/version.py +0 -0
  110. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/vertex.py +0 -0
  111. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/wal.py +0 -0
  112. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/web/__init__.py +0 -0
  113. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/web/sse_adapter.py +0 -0
  114. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/web/terminal_repl.py +0 -0
  115. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/web/web_sse_compat.py +0 -0
  116. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode/workspace_hints.py +0 -0
  117. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode.egg-info/SOURCES.txt +0 -0
  118. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode.egg-info/dependency_links.txt +0 -0
  119. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode.egg-info/entry_points.txt +0 -0
  120. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode.egg-info/requires.txt +0 -0
  121. {gemcode-0.3.97 → gemcode-0.3.99}/src/gemcode.egg-info/top_level.txt +0 -0
  122. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_add_dir.py +0 -0
  123. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_agent_instruction.py +0 -0
  124. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_autocompact.py +0 -0
  125. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_capability_routing.py +0 -0
  126. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_checkpoint_diff_command.py +0 -0
  127. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_cli_init.py +0 -0
  128. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_compress_memory_tool.py +0 -0
  129. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_computer_use_permissions.py +0 -0
  130. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_context_budget.py +0 -0
  131. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_context_warning.py +0 -0
  132. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_credentials.py +0 -0
  133. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_eval_harness_layout.py +0 -0
  134. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_ide_stdio_attachments.py +0 -0
  135. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_interactive_permission_ask.py +0 -0
  136. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_kaira_scheduler.py +0 -0
  137. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_modality_tools.py +0 -0
  138. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_model_error_retry.py +0 -0
  139. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_model_errors.py +0 -0
  140. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_model_routing.py +0 -0
  141. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_multimodal_input.py +0 -0
  142. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_output_styles_and_rules.py +0 -0
  143. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_paths.py +0 -0
  144. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_permissions.py +0 -0
  145. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_prompt_suggestions.py +0 -0
  146. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_repl_commands.py +0 -0
  147. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_repl_slash.py +0 -0
  148. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_skills.py +0 -0
  149. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_slash_commands.py +0 -0
  150. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_slash_completion_registry.py +0 -0
  151. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_thinking_config.py +0 -0
  152. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_token_budget.py +0 -0
  153. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_tool_context_circulation.py +0 -0
  154. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_tools.py +0 -0
  155. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_tools_inspector.py +0 -0
  156. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_web_sse_adapter.py +0 -0
  157. {gemcode-0.3.97 → gemcode-0.3.99}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.97
3
+ Version: 0.3.99
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.3.97"
7
+ version = "0.3.99"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -184,15 +184,33 @@ async def run_turn(
184
184
  # user-granted "Files and Folders" permissions on first access.
185
185
  # If approved once, we don't re-prompt for the rest of this session.
186
186
  attach_allow = True
187
- if cfg is not None:
188
- attach_allow = bool(getattr(cfg, "interactive_permission_ask", False))
189
- attach_allow = attach_allow and hasattr(sys.stdin, "isatty") and sys.stdin.isatty()
190
- if attach_allow and not bool(getattr(cfg, "_attachments_allowed", False)):
191
- attach_allow = _prompt_yes_no(
192
- "Allow GemCode to read and upload the attached file(s) from disk? (y/n) "
193
- )
194
- if attach_allow:
187
+ if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
188
+ # Default-on: attachments can read any local file path (not workspace-scoped),
189
+ # but we ask once per session so the user is in control and macOS can trigger
190
+ # its permission prompt at the moment we attempt the read.
191
+ attach_allow = os.environ.get("GEMCODE_ATTACHMENTS_ASK", "1").lower() not in (
192
+ "0",
193
+ "false",
194
+ "no",
195
+ "off",
196
+ )
197
+ if cfg is not None:
198
+ # If user already approved earlier in this session, don't prompt again.
199
+ if bool(getattr(cfg, "_attachments_allowed", False)):
200
+ attach_allow = True
201
+ # If yes-to-all is enabled, auto-allow attachments.
202
+ elif bool(getattr(cfg, "yes_to_all", False)):
203
+ attach_allow = True
195
204
  object.__setattr__(cfg, "_attachments_allowed", True)
205
+ elif attach_allow:
206
+ attach_allow = _prompt_yes_no(
207
+ "Allow GemCode to read and upload the attached file(s) from disk? (y/n) "
208
+ )
209
+ if attach_allow:
210
+ object.__setattr__(cfg, "_attachments_allowed", True)
211
+ else:
212
+ # Non-interactive sessions can't prompt; default to allow.
213
+ attach_allow = True
196
214
  effective_attachments = attachment_paths if attach_allow else None
197
215
 
198
216
  root = cfg.project_root if cfg is not None else Path.cwd()
@@ -97,6 +97,18 @@ def _infer_mime(path: Path, data: bytes) -> tuple[str, list[str]]:
97
97
  return "application/octet-stream", warnings
98
98
 
99
99
 
100
+ def _is_supported_mime(m: str) -> bool:
101
+ mm = (m or "").strip().lower()
102
+ if not mm:
103
+ return False
104
+ if mm.startswith(("image/", "audio/", "video/", "text/")):
105
+ return True
106
+ if mm == "application/pdf":
107
+ return True
108
+ # Most other application/* types are rejected by Gemini file parts.
109
+ return False
110
+
111
+
100
112
  def build_user_content(
101
113
  prompt: str,
102
114
  attachment_paths: Sequence[Path | str] | None,
@@ -135,6 +147,12 @@ def build_user_content(
135
147
  continue
136
148
  mime, mw = _infer_mime(p, data)
137
149
  warnings.extend(mw)
150
+ if not _is_supported_mime(mime) or mime == "application/octet-stream":
151
+ warnings.append(
152
+ f"unsupported attachment type for {p} (mime={mime}); "
153
+ "skipping (export to PDF/image/text if needed)"
154
+ )
155
+ continue
138
156
  parts.append(types.Part(inline_data=types.Blob(data=data, mime_type=mime)))
139
157
 
140
158
  text = (prompt or "").strip() or (
@@ -100,16 +100,51 @@ def _build_prompt(transcript_lines: list[str], *, focus: str = "") -> str:
100
100
  )
101
101
 
102
102
 
103
- def _call_summary_model(*, model: str, prompt: str) -> dict[str, Any]:
103
+ def _extract_json_object(text: str) -> dict[str, Any] | None:
104
+ """
105
+ Best-effort parse:
106
+ - strict JSON object
107
+ - JSON object embedded in extra text (common model behavior)
108
+ """
109
+ raw = (text or "").strip()
110
+ if not raw:
111
+ return None
112
+ try:
113
+ obj = json.loads(raw)
114
+ return obj if isinstance(obj, dict) else None
115
+ except Exception:
116
+ pass
117
+
118
+ # Try to recover embedded JSON: find the largest {...} span.
119
+ start = raw.find("{")
120
+ end = raw.rfind("}")
121
+ if start == -1 or end == -1 or end <= start:
122
+ return None
123
+ candidate = raw[start : end + 1].strip()
124
+ try:
125
+ obj = json.loads(candidate)
126
+ return obj if isinstance(obj, dict) else None
127
+ except Exception:
128
+ return None
129
+
130
+
131
+ def _call_summary_model(*, model: str, prompt: str) -> tuple[dict[str, Any] | None, str]:
104
132
  api_key = os.environ.get("GOOGLE_API_KEY")
105
133
  if not api_key:
106
134
  raise RuntimeError("GOOGLE_API_KEY not set")
107
135
 
108
136
  client = Client(api_key=api_key)
137
+ cfg = types.GenerateContentConfig(temperature=0.2)
138
+ # Prefer structured JSON output when supported by the SDK/model.
139
+ try:
140
+ setattr(cfg, "response_mime_type", "application/json")
141
+ except Exception:
142
+ pass
143
+
109
144
  resp = client.models.generate_content(
110
- model=model,
111
- contents=[types.Content(role="user", parts=[types.Part(text=prompt)])],
112
- config=types.GenerateContentConfig(temperature=0.2),
145
+ model=model,
146
+ contents=[types.Content(role="user", parts=[types.Part(text=prompt)])],
147
+ config=cfg,
113
148
  )
114
149
 
115
150
  out_parts: list[str] = []
@@ -127,14 +162,7 @@ def _call_summary_model(*, model: str, prompt: str) -> dict[str, Any]:
127
162
  text = "".join(out_parts).strip()
128
163
  if not text:
129
164
  raise RuntimeError("session summariser returned empty text")
130
-
131
- try:
132
- data = json.loads(text)
133
- except Exception as e:
134
- raise RuntimeError(f"session summariser returned invalid JSON: {e}") from e
135
- if not isinstance(data, dict):
136
- raise RuntimeError("session summariser returned non-object JSON")
137
- return data
165
+ return _extract_json_object(text), text
138
166
 
139
167
 
140
168
  def summarise_session(
@@ -149,7 +177,18 @@ def summarise_session(
149
177
  return {"error": "session transcript is empty", "session_id": session_id}
150
178
 
151
179
  prompt = _build_prompt(transcript_lines, focus=focus)
152
- data = _call_summary_model(model=model, prompt=prompt)
180
+ data, raw_text = _call_summary_model(model=model, prompt=prompt)
181
+ if data is None:
182
+ # Graceful fallback: save raw text as summary_markdown so /summarise never hard-fails
183
+ # due to JSON formatting drift.
184
+ data = {
185
+ "title": f"Session {session_id[:8]}",
186
+ "summary_markdown": raw_text[:20_000],
187
+ "memory_facts": [],
188
+ "user_facts": [],
189
+ "notes_markdown": "",
190
+ "open_items": [],
191
+ }
153
192
 
154
193
  title = str(data.get("title") or f"Session {session_id[:8]}").strip()[:120]
155
194
  summary_markdown = str(data.get("summary_markdown") or "").strip()
@@ -219,6 +258,7 @@ def summarise_session(
219
258
  "summary_path": str(out_path),
220
259
  "title": title,
221
260
  "summary_markdown": summary_markdown,
261
+ "used_json": bool(data is not None and _extract_json_object(raw_text) is not None),
222
262
  "memory_facts_saved": saved_memory,
223
263
  "user_facts_saved": saved_user,
224
264
  "notes_status": note_status,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.97
3
+ Version: 0.3.99
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
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