baserun-cli 0.1.4__tar.gz → 0.1.5__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 (22) hide show
  1. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/PKG-INFO +1 -1
  2. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli/channel.py +35 -44
  3. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli/runner.py +9 -7
  4. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli.egg-info/PKG-INFO +1 -1
  5. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/pyproject.toml +1 -1
  6. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/README.md +0 -0
  7. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli/__init__.py +0 -0
  8. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli/_vendored/__init__.py +0 -0
  9. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli/_vendored/base.py +0 -0
  10. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli/_vendored/cli.py +0 -0
  11. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli/_vendored/parsers/__init__.py +0 -0
  12. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli/_vendored/parsers/base.py +0 -0
  13. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli/_vendored/parsers/bash_agent.py +0 -0
  14. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli/_vendored/parsers/claude.py +0 -0
  15. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli/_vendored/parsers/codex.py +0 -0
  16. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli/main.py +0 -0
  17. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli.egg-info/SOURCES.txt +0 -0
  18. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli.egg-info/dependency_links.txt +0 -0
  19. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli.egg-info/entry_points.txt +0 -0
  20. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli.egg-info/requires.txt +0 -0
  21. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/baserun_cli.egg-info/top_level.txt +0 -0
  22. {baserun_cli-0.1.4 → baserun_cli-0.1.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: baserun-cli
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: BaseRun agent-side daemon (connects to nchan, spawns CLI agents, publishes run events)
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -82,47 +82,47 @@ class ChannelClient:
82
82
  }
83
83
 
84
84
  async def ensure_run_pub(self, run_id: str) -> None:
85
- """Deprecated no-op.
85
+ """Pre-connect the WS publisher for a run (fire-and-forget).
86
86
 
87
- WS publisher sends can succeed locally while nchan receives nothing
88
- when a proxy/WebSocket keepalive breaks. Run events now use HTTP
89
- publish as the authoritative, acknowledged path.
90
- """
91
- return
92
-
93
- async def publish_event(self, run_id: str, payload: dict[str, Any]) -> bool:
94
- """Publish a run event via HTTP and require an acknowledged response.
95
-
96
- We intentionally avoid the WebSocket publisher for run events: send()
97
- only confirms that bytes entered the local socket buffer, not that nchan
98
- accepted and buffered the event. HTTP gives per-event success/failure.
87
+ Called at run start so the WS handshake completes before the first
88
+ event arrives. If this fails, publish_event will retry on demand.
99
89
  """
90
+ if run_id in self._run_pubs:
91
+ return
100
92
  try:
101
- await self._http_publish(run_id, payload)
102
- return True
93
+ ws = await self._open_run_pub(run_id)
94
+ self._run_pubs[run_id] = ws
103
95
  except Exception as e:
104
- log.warning(
105
- "http publish to run %s failed seq=%s: %s",
106
- run_id,
107
- payload.get("data", {}).get("seq"),
108
- e,
109
- )
110
- return False
96
+ log.debug("pre-connect ws pub for run %s failed (will retry on first event): %s", run_id, e)
97
+
98
+ async def publish_event(self, run_id: str, payload: dict[str, Any]) -> bool:
99
+ """Publish a run event via WebSocket (ordered streaming path)."""
100
+ data = json.dumps(payload, ensure_ascii=False)
101
+ is_terminal = (payload.get("data", {}).get("finished") is True)
102
+ max_retries = 4 if is_terminal else 2
111
103
 
112
- async def publish_event_reliably(self, run_id: str, payload: dict[str, Any]) -> bool:
113
- """Publish a critical run event with HTTP retries."""
114
- max_retries = 5
115
104
  for attempt in range(max_retries):
116
- if await self.publish_event(run_id, payload):
105
+ ws = self._run_pubs.get(run_id)
106
+ if ws is None:
107
+ try:
108
+ ws = await self._open_run_pub(run_id)
109
+ self._run_pubs[run_id] = ws
110
+ except Exception as e:
111
+ log.warning("ws pub connect for run %s failed (attempt %d): %s", run_id, attempt + 1, e)
112
+ if attempt < max_retries - 1:
113
+ await asyncio.sleep(1.0)
114
+ continue
115
+ try:
116
+ await ws.send(data)
117
117
  return True
118
- if attempt < max_retries - 1:
119
- await asyncio.sleep(1.0)
120
- log.error(
121
- "reliable publish failed for run %s seq=%s after %d attempts",
122
- run_id,
123
- payload.get("data", {}).get("seq"),
124
- max_retries,
125
- )
118
+ except Exception as e:
119
+ log.warning("ws publish to run %s failed (attempt %d): %s", run_id, attempt + 1, e)
120
+ self._run_pubs.pop(run_id, None)
121
+ if attempt < max_retries - 1:
122
+ await asyncio.sleep(1.0)
123
+
124
+ log.error("publish failed for run %s seq=%s after %d attempts",
125
+ run_id, payload.get("data", {}).get("seq"), max_retries)
126
126
  return False
127
127
 
128
128
  async def _open_run_pub(self, run_id: str) -> Any:
@@ -136,12 +136,6 @@ class ChannelClient:
136
136
  log.info("opening ws publisher for run %s", run_id)
137
137
  return await connect(url, ping_interval=30, open_timeout=30)
138
138
 
139
- async def _http_publish(self, run_id: str, payload: dict[str, Any]) -> None:
140
- """HTTP fallback for publishing events (legacy, not used in normal flow)."""
141
- url = f"{self._publish_base.rstrip('/')}/internal/run/{run_id}/publish"
142
- resp = await self._http.post(url, content=json.dumps(payload, ensure_ascii=False))
143
- resp.raise_for_status()
144
-
145
139
  async def close_run_pub(self, run_id: str) -> None:
146
140
  """Close the WS publisher for a run."""
147
141
  ws = self._run_pubs.pop(run_id, None)
@@ -228,10 +222,7 @@ class ChannelClient:
228
222
  replay_ok = True
229
223
  for ev in missing_events:
230
224
  data = ev.get("data", {})
231
- if data.get("finished") is True:
232
- ok = await self.publish_event_reliably(run_id, ev)
233
- else:
234
- ok = await self.publish_event(run_id, ev)
225
+ ok = await self.publish_event(run_id, ev)
235
226
  if not ok:
236
227
  replay_ok = False
237
228
  log.error("verify: replay still failed for run %s seq=%s", run_id, data.get("seq"))
@@ -163,6 +163,11 @@ class TaskRunner:
163
163
  connector = get_connector(connector_type, config)
164
164
  log.info("run %s mode=%s connector=%s session_id=%s", run_id, mode, connector.spec.bin, agent_session_id or "new")
165
165
 
166
+ # Open the run publisher before connector starts. Otherwise a short task
167
+ # can finish before the run channel/subscriber is ready, leaving the
168
+ # server in RUNNING even though the client process completed locally.
169
+ await self.channel.ensure_run_pub(run_id)
170
+
166
171
  events_iter = self._select_events(connector, mode, prompt, agent_session_id)
167
172
 
168
173
  # local persistence: raw event log for debugging + retry
@@ -199,11 +204,8 @@ class TaskRunner:
199
204
 
200
205
  log.debug("run %s event seq=%d kind=%s", run_id, seq, kind)
201
206
 
202
- # publish
203
- if is_terminal:
204
- await self.channel.publish_event_reliably(run_id, payload)
205
- else:
206
- await self.channel.publish_event(run_id, payload)
207
+ # publish via WebSocket only
208
+ await self.channel.publish_event(run_id, payload)
207
209
 
208
210
  if is_terminal:
209
211
  saw_terminal = True
@@ -224,7 +226,7 @@ class TaskRunner:
224
226
  }
225
227
  log_file.write(json.dumps(payload["data"], ensure_ascii=False) + "\n")
226
228
  log_file.flush()
227
- await self.channel.publish_event_reliably(run_id, payload)
229
+ await self.channel.publish_event(run_id, payload)
228
230
  log.info("run %s synthesized terminal (seq=%d)", run_id, seq)
229
231
 
230
232
  # verify-and-replay:对比 nchan 实际状态,精确补发缺失事件
@@ -268,7 +270,7 @@ class TaskRunner:
268
270
  log_file.write(json.dumps(error_payload["data"], ensure_ascii=False) + "\n")
269
271
  except Exception:
270
272
  pass
271
- ok = await self.channel.publish_event_reliably(run_id, error_payload)
273
+ ok = await self.channel.publish_event(run_id, error_payload)
272
274
  return ok, "error", seq
273
275
 
274
276
  finally:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: baserun-cli
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: BaseRun agent-side daemon (connects to nchan, spawns CLI agents, publishes run events)
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "baserun-cli"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  description = "BaseRun agent-side daemon (connects to nchan, spawns CLI agents, publishes run events)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
File without changes
File without changes