gemcode 0.4.25__py3-none-any.whl → 0.4.26__py3-none-any.whl

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.
@@ -1,4 +1,10 @@
1
- """File-based HITL approval bridge for web chat (subprocess-safe)."""
1
+ """File-based HITL approval bridge for web chat (subprocess-safe).
2
+
3
+ Chat SSE runs in a ``gemcode.web.sse_adapter`` subprocess while
4
+ ``POST /api/chat/approve`` is handled by the parent ``gemcode serve`` process.
5
+ In-memory waiter sets therefore cannot detect “live” approvals across that
6
+ boundary — use ``*.waiting`` marker files on the shared approval dir instead.
7
+ """
2
8
 
3
9
  from __future__ import annotations
4
10
 
@@ -17,22 +23,64 @@ def _approval_dir() -> Path:
17
23
  return Path.home() / ".gemcode" / "web_approvals"
18
24
  _DEFAULT_TIMEOUT_S = float(os.environ.get("GEMCODE_WEB_HITL_TIMEOUT_S", "3600"))
19
25
  _POLL_INTERVAL_S = 0.25
20
- # approval_ids currently blocked in wait_for_web_approval (same process)
26
+ # Same-process hints only (parent server never shares these with sse_adapter).
21
27
  _active_waiters: set[str] = set()
22
- # emitted to the UI but wait_for_web_approval not started yet (preflight)
23
28
  _pending_approval_ids: set[str] = set()
24
29
 
25
30
 
31
+ def _safe_id(approval_id: str) -> str:
32
+ return "".join(c if c.isalnum() or c in "-_:" else "_" for c in approval_id)
33
+
34
+
35
+ def _approval_path(approval_id: str) -> Path:
36
+ return _approval_dir() / f"{_safe_id(approval_id)}.json"
37
+
38
+
39
+ def _waiting_path(approval_id: str) -> Path:
40
+ """Marker written by the SSE subprocess while an approval is expected/active."""
41
+ return _approval_dir() / f"{_safe_id(approval_id)}.waiting"
42
+
43
+
44
+ def _is_live_approval(approval_id: str) -> bool:
45
+ """True if a chat turn is still waiting for this approval (cross-process safe)."""
46
+ aid = (approval_id or "").strip()
47
+ if not aid:
48
+ return False
49
+ if aid in _active_waiters or aid in _pending_approval_ids:
50
+ return True
51
+ try:
52
+ path = _waiting_path(aid)
53
+ if not path.is_file():
54
+ return False
55
+ # Heartbeats refresh mtime; stale markers mean the SSE subprocess died.
56
+ age_s = time.time() - path.stat().st_mtime
57
+ return age_s < 90.0
58
+ except OSError:
59
+ return False
60
+
61
+
26
62
  def register_pending_approval(approval_id: str) -> None:
27
63
  """Mark an approval as expected so early UI clicks are not reported as late."""
28
64
  aid = (approval_id or "").strip()
29
- if aid:
30
- _pending_approval_ids.add(aid)
65
+ if not aid:
66
+ return
67
+ _pending_approval_ids.add(aid)
68
+ _touch_waiting(aid)
31
69
 
32
70
 
33
- def _approval_path(approval_id: str) -> Path:
34
- safe = "".join(c if c.isalnum() or c in "-_:" else "_" for c in approval_id)
35
- return _approval_dir() / f"{safe}.json"
71
+ def _touch_waiting(approval_id: str) -> None:
72
+ try:
73
+ _approval_dir().mkdir(parents=True, exist_ok=True)
74
+ _waiting_path(approval_id).write_text(f"{time.time():.3f}\n", encoding="utf-8")
75
+ except OSError:
76
+ pass
77
+
78
+
79
+ def _clear_waiting_marker(approval_id: str) -> None:
80
+ try:
81
+ _waiting_path(approval_id).unlink(missing_ok=True)
82
+ except OSError:
83
+ pass
36
84
 
37
85
 
38
86
  def _read_approval_file(path: Path) -> bool | None:
@@ -60,14 +108,16 @@ def resolve_web_approval(approval_id: str, *, confirmed: bool) -> dict[str, Any]
60
108
  return {"ok": False, "error": "approval_id is required"}
61
109
  _approval_dir().mkdir(parents=True, exist_ok=True)
62
110
  path = _approval_path(approval_id)
111
+ # Detect live wait BEFORE writing; marker is owned by the SSE subprocess.
112
+ live = _is_live_approval(approval_id)
63
113
  payload = {"confirmed": bool(confirmed), "resolved_ms": int(time.time() * 1000)}
64
114
  path.write_text(json.dumps(payload), encoding="utf-8")
65
- late = approval_id not in _active_waiters and approval_id not in _pending_approval_ids
66
115
  return {
67
116
  "ok": True,
68
117
  "approval_id": approval_id,
69
118
  "confirmed": bool(confirmed),
70
- "late": late,
119
+ # late = no waiter — UI may start a recovery turn. Never true while SSE waits.
120
+ "late": not live,
71
121
  }
72
122
 
73
123
 
@@ -81,14 +131,18 @@ async def wait_for_web_approval(
81
131
  """Block until the user approves/denies or timeout (deny on timeout)."""
82
132
  _approval_dir().mkdir(parents=True, exist_ok=True)
83
133
  path = _approval_path(approval_id)
134
+ register_pending_approval(approval_id)
84
135
 
85
136
  # User may have approved before we started waiting (preflight UI is immediate).
86
137
  existing = _read_approval_file(path)
87
138
  if existing is not None:
139
+ _pending_approval_ids.discard(approval_id)
140
+ _clear_waiting_marker(approval_id)
88
141
  return existing
89
142
 
90
143
  _pending_approval_ids.discard(approval_id)
91
144
  _active_waiters.add(approval_id)
145
+ _touch_waiting(approval_id)
92
146
  try:
93
147
  deadline = time.monotonic() + max(1.0, timeout_s)
94
148
  next_hb = time.monotonic()
@@ -98,17 +152,20 @@ async def wait_for_web_approval(
98
152
  if result is not None:
99
153
  return result
100
154
  now = time.monotonic()
101
- if heartbeat is not None and now >= next_hb:
102
- try:
103
- heartbeat()
104
- except Exception:
105
- pass
155
+ if now >= next_hb:
156
+ _touch_waiting(approval_id)
157
+ if heartbeat is not None:
158
+ try:
159
+ heartbeat()
160
+ except Exception:
161
+ pass
106
162
  next_hb = now + max(5.0, heartbeat_s)
107
163
  await asyncio.sleep(_POLL_INTERVAL_S)
108
164
  return False
109
165
  finally:
110
166
  _active_waiters.discard(approval_id)
111
167
  _pending_approval_ids.discard(approval_id)
168
+ _clear_waiting_marker(approval_id)
112
169
 
113
170
 
114
171
  def new_approval_id(session_id: str, fc_id: str | None) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.25
3
+ Version: 0.4.26
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -129,7 +129,7 @@ gemcode/web/chat_skills.py,sha256=VMnzKzhPZ96T-Opopbe7zJ2EUBvxrRaRMjYw4WfctuQ,40
129
129
  gemcode/web/customize_api.py,sha256=Et1HY49lleZlCBywsBg9n61KFReqTB0wGQrDmhrNEf4,24559
130
130
  gemcode/web/files_api.py,sha256=sdlSd3gj3TF_qrH2xYy70CTWMXAIzqouWe6uDkMrroo,7724
131
131
  gemcode/web/fleet_api.py,sha256=4lR7lWqpMojaAL7_c6rjjA5hMFnnUgpI1T561RvM-3w,19276
132
- gemcode/web/hitl_bridge.py,sha256=13ZyBNmAOjwHN1oHkJOC_JhJF60kgkrg6_02DMm4FpM,3670
132
+ gemcode/web/hitl_bridge.py,sha256=08rzUmVDiY6eAsMXZ-oE-XLPK5gk-wAGIFxDY2QlobA,5465
133
133
  gemcode/web/preview_api.py,sha256=vz5ddD6JYAQ5h6DEa4w95FUKalw-ujvT-jusbVa-8Ak,5828
134
134
  gemcode/web/project_root.py,sha256=rMKsp2va2z1p9CI-NKzbdJq2hyKXtNzDSLyTk-tV8Ws,1936
135
135
  gemcode/web/runtime_api.py,sha256=BezPpbNnj85aXiL5r7aXGcA3FFubadY7lHtskaNPgMc,12935
@@ -144,9 +144,9 @@ gemcode/web/ui_chat_store.py,sha256=sThUOcWC-svx-e2pt88ZazPrWelN2JbqeDicU7s5iUg,
144
144
  gemcode/web/web_config_api.py,sha256=1kmk1vSu5z63loddTNBHVsisiYy4CfyVIF93TYCgEqs,2264
145
145
  gemcode/web/web_sse_compat.py,sha256=9A2s-GI7El7AotJqhO263FrLwppCXXkdydZ5EiOQbao,504
146
146
  gemcode/web/workspace_panel_api.py,sha256=uZS11ovlZtc1IJDnOA776Z6IndC0WovcBbmPMnZyIHs,14122
147
- gemcode-0.4.25.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
148
- gemcode-0.4.25.dist-info/METADATA,sha256=PYHxD-Pv9vEgpKAqWho4p9ON8O_fuHDkvUn4CaQccq0,27378
149
- gemcode-0.4.25.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
150
- gemcode-0.4.25.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
151
- gemcode-0.4.25.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
152
- gemcode-0.4.25.dist-info/RECORD,,
147
+ gemcode-0.4.26.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
148
+ gemcode-0.4.26.dist-info/METADATA,sha256=K16ysRvmdkDwT4s6ykdDg_gfQ5C3F1h7UVA9IqNGBqg,27378
149
+ gemcode-0.4.26.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
150
+ gemcode-0.4.26.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
151
+ gemcode-0.4.26.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
152
+ gemcode-0.4.26.dist-info/RECORD,,