webbee 0.3.6__tar.gz → 0.3.8__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 (83) hide show
  1. {webbee-0.3.6 → webbee-0.3.8}/CHANGELOG.md +20 -0
  2. {webbee-0.3.6 → webbee-0.3.8}/PKG-INFO +1 -1
  3. {webbee-0.3.6 → webbee-0.3.8}/pyproject.toml +1 -1
  4. webbee-0.3.8/src/webbee/__init__.py +1 -0
  5. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/render.py +14 -0
  6. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/repl.py +47 -13
  7. webbee-0.3.8/src/webbee/session.py +512 -0
  8. webbee-0.3.8/src/webbee/steer.py +112 -0
  9. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/thread.py +20 -1
  10. webbee-0.3.8/src/webbee/tokens.py +39 -0
  11. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/tui.py +17 -7
  12. {webbee-0.3.6 → webbee-0.3.8}/tests/test_marathon.py +2 -0
  13. {webbee-0.3.6 → webbee-0.3.8}/tests/test_render.py +18 -0
  14. {webbee-0.3.6 → webbee-0.3.8}/tests/test_repl.py +122 -2
  15. {webbee-0.3.6 → webbee-0.3.8}/tests/test_session.py +269 -0
  16. webbee-0.3.8/tests/test_steer.py +266 -0
  17. {webbee-0.3.6 → webbee-0.3.8}/tests/test_thread.py +113 -0
  18. webbee-0.3.8/tests/test_tokens.py +57 -0
  19. webbee-0.3.6/src/webbee/__init__.py +0 -1
  20. webbee-0.3.6/src/webbee/session.py +0 -361
  21. {webbee-0.3.6 → webbee-0.3.8}/.github/workflows/publish.yml +0 -0
  22. {webbee-0.3.6 → webbee-0.3.8}/.gitignore +0 -0
  23. {webbee-0.3.6 → webbee-0.3.8}/LICENSE +0 -0
  24. {webbee-0.3.6 → webbee-0.3.8}/README.md +0 -0
  25. {webbee-0.3.6 → webbee-0.3.8}/install.sh +0 -0
  26. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/account.py +0 -0
  27. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/banner_art.py +0 -0
  28. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/checkpoints.py +0 -0
  29. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/cli.py +0 -0
  30. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/clipboard.py +0 -0
  31. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/commands.py +0 -0
  32. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/config.py +0 -0
  33. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/details.py +0 -0
  34. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/events.py +0 -0
  35. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/frames.py +0 -0
  36. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/intel/__init__.py +0 -0
  37. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/intel/chunker.py +0 -0
  38. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/intel/embed.py +0 -0
  39. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/intel/graph.py +0 -0
  40. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/intel/indexer.py +0 -0
  41. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/intel/models.py +0 -0
  42. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/intel/query.py +0 -0
  43. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/intel/service.py +0 -0
  44. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/intel/store.py +0 -0
  45. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/intel/vectors.py +0 -0
  46. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/intel/watch.py +0 -0
  47. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/output_pane.py +0 -0
  48. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/remote.py +0 -0
  49. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/repo.py +0 -0
  50. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/sessions.py +0 -0
  51. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/stream.py +0 -0
  52. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/tools.py +0 -0
  53. {webbee-0.3.6 → webbee-0.3.8}/src/webbee/update.py +0 -0
  54. {webbee-0.3.6 → webbee-0.3.8}/tests/__init__.py +0 -0
  55. {webbee-0.3.6 → webbee-0.3.8}/tests/test_account.py +0 -0
  56. {webbee-0.3.6 → webbee-0.3.8}/tests/test_checkpoints.py +0 -0
  57. {webbee-0.3.6 → webbee-0.3.8}/tests/test_cli.py +0 -0
  58. {webbee-0.3.6 → webbee-0.3.8}/tests/test_clipboard.py +0 -0
  59. {webbee-0.3.6 → webbee-0.3.8}/tests/test_commands.py +0 -0
  60. {webbee-0.3.6 → webbee-0.3.8}/tests/test_config.py +0 -0
  61. {webbee-0.3.6 → webbee-0.3.8}/tests/test_cpc_contract_stable.py +0 -0
  62. {webbee-0.3.6 → webbee-0.3.8}/tests/test_details.py +0 -0
  63. {webbee-0.3.6 → webbee-0.3.8}/tests/test_events.py +0 -0
  64. {webbee-0.3.6 → webbee-0.3.8}/tests/test_freeze_fix.py +0 -0
  65. {webbee-0.3.6 → webbee-0.3.8}/tests/test_intel_chunker.py +0 -0
  66. {webbee-0.3.6 → webbee-0.3.8}/tests/test_intel_embed.py +0 -0
  67. {webbee-0.3.6 → webbee-0.3.8}/tests/test_intel_graph.py +0 -0
  68. {webbee-0.3.6 → webbee-0.3.8}/tests/test_intel_indexer.py +0 -0
  69. {webbee-0.3.6 → webbee-0.3.8}/tests/test_intel_no_numpy.py +0 -0
  70. {webbee-0.3.6 → webbee-0.3.8}/tests/test_intel_query.py +0 -0
  71. {webbee-0.3.6 → webbee-0.3.8}/tests/test_intel_service.py +0 -0
  72. {webbee-0.3.6 → webbee-0.3.8}/tests/test_intel_store.py +0 -0
  73. {webbee-0.3.6 → webbee-0.3.8}/tests/test_intel_vectors.py +0 -0
  74. {webbee-0.3.6 → webbee-0.3.8}/tests/test_intel_watch.py +0 -0
  75. {webbee-0.3.6 → webbee-0.3.8}/tests/test_packaging.py +0 -0
  76. {webbee-0.3.6 → webbee-0.3.8}/tests/test_repo.py +0 -0
  77. {webbee-0.3.6 → webbee-0.3.8}/tests/test_sessions.py +0 -0
  78. {webbee-0.3.6 → webbee-0.3.8}/tests/test_stream.py +0 -0
  79. {webbee-0.3.6 → webbee-0.3.8}/tests/test_tools.py +0 -0
  80. {webbee-0.3.6 → webbee-0.3.8}/tests/test_tui.py +0 -0
  81. {webbee-0.3.6 → webbee-0.3.8}/tests/test_tui_hardening.py +0 -0
  82. {webbee-0.3.6 → webbee-0.3.8}/tests/test_update.py +0 -0
  83. {webbee-0.3.6 → webbee-0.3.8}/tests/test_version.py +0 -0
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.8
4
+
5
+ - No more sign-in races: token refresh is serialized and retries once after
6
+ a sibling terminal rotates the session — multiple open terminals no longer
7
+ knock each other out with "session expired".
8
+ - No more dock lockup: a turn that fails (for example, an expired session)
9
+ now clears the "working" state, and every key keeps responding even if the
10
+ turn state ever goes stale. The idle pickup poller also backs off instead
11
+ of hammering a signed-out session.
12
+
13
+ ## 0.3.7
14
+
15
+ - Terminal liveness: a consent answered from Telegram/panel no longer
16
+ freezes the terminal — the `approve? y/n` prompt dismisses itself
17
+ ("answered from another surface") and the turn keeps rendering live.
18
+ - Idle pickup: an open, idle terminal now picks up instructions sent from
19
+ Telegram/panel within seconds and runs them as normal turns, tagged with
20
+ their origin. If no terminal is open, the instruction waits (up to an
21
+ hour) and Telegram says so instead of failing.
22
+
3
23
  ## 0.3.6
4
24
 
5
25
  - Boot replay shows only the conversation: flattened tool traffic
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webbee
3
- Version: 0.3.6
3
+ Version: 0.3.8
4
4
  Summary: Webbee 🐝 — the Imperal Cloud coding agent in your terminal
5
5
  Project-URL: Homepage, https://imperal.io
6
6
  Project-URL: Documentation, https://docs.imperal.io
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "webbee"
3
- version = "0.3.6"
3
+ version = "0.3.8"
4
4
  description = "Webbee 🐝 — the Imperal Cloud coding agent in your terminal"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1 @@
1
+ __version__ = "0.3.8"
@@ -233,6 +233,20 @@ class RichSink:
233
233
  self.console.print(_pad(Text(f"{who}{tag}: {_clean(text)}", style=_BEE)))
234
234
  self._nudge()
235
235
 
236
+ def consent_dismissed(self, note: str) -> None:
237
+ """Liveness A: the pending consent was answered on ANOTHER surface
238
+ (e.g. relayed from Telegram) — the kernel park is over. Retire the
239
+ pinned prompt so the dock leaves `approve? y/n`: cancel the armed
240
+ Future (consent_pending() flips False, so the toolbar repaints out of
241
+ the consent state on the next _nudge) and print ONE note-style line
242
+ so the scrollback shows why the y/n prompt vanished."""
243
+ if self._consent is not None and not self._consent.done():
244
+ self._consent.cancel()
245
+ self._consent = None
246
+ self._consent_summary = ""
247
+ self.console.print(_pad(Text(_clean(note), style=_BEE)))
248
+ self._nudge()
249
+
236
250
  def clear(self) -> None:
237
251
  """/clear: wipe the pane/screen + reset the session counters."""
238
252
  self.console.clear()
@@ -58,8 +58,8 @@ async def run_marathon(cfg, mode: str, goal: str, *, sink=None, auth=None,
58
58
 
59
59
  workspace = os.getcwd()
60
60
 
61
- async def token_provider() -> str:
62
- return await auth.ensure_access_token(cfg)
61
+ from webbee.tokens import make_token_provider
62
+ token_provider = make_token_provider(cfg, auth)
63
63
 
64
64
  if agent_factory is None:
65
65
  agent_factory = lambda c, tp, ws, m: AgentSession(c, tp, ws, m) # noqa: E731
@@ -72,9 +72,11 @@ async def run_marathon(cfg, mode: str, goal: str, *, sink=None, auth=None,
72
72
  except (KeyboardInterrupt, asyncio.CancelledError):
73
73
  await agent.stop()
74
74
  sink.note("Interrupted.")
75
+ sink.end_turn("") # clear busy (poller starvation guard)
75
76
  return ""
76
77
  except Exception as e: # network/auth/etc — never crash
77
78
  sink.note(f"Error: {type(e).__name__}: {e}")
79
+ sink.end_turn("") # clear busy: a stuck 'working' also starves the idle-steer poller
78
80
  return ""
79
81
  sink.end_turn(text)
80
82
  return text
@@ -122,8 +124,8 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
122
124
 
123
125
  workspace = os.getcwd()
124
126
 
125
- async def token_provider() -> str:
126
- return await auth.ensure_access_token(cfg)
127
+ from webbee.tokens import make_token_provider
128
+ token_provider = make_token_provider(cfg, auth)
127
129
 
128
130
  # Prod dock path = the default reader + a real tty + no injected sink; tests
129
131
  # inject sink/read_line and take the plain fallback loop.
@@ -134,6 +136,7 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
134
136
  intel = None # assigned by _boot -- IntelService, or None (off/base-install/boot failure)
135
137
  shadow = None # assigned by _boot -- ShadowGit, or None (git unavailable / boot failure)
136
138
  watcher_task = None # assigned by _boot -- background watchfiles task, cancelled on exit
139
+ steer_task = None # assigned by _boot -- idle-steer poller (webbee.steer), cancelled on exit
137
140
 
138
141
  def _cycle() -> None:
139
142
  state["mode"] = next_mode(state["mode"])
@@ -269,21 +272,44 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
269
272
 
270
273
  # A task for the agent.
271
274
  _sink.user_echo(line)
275
+ await _run_turn(line)
276
+ return "continue"
277
+
278
+ async def _run_turn(line: str, surface: str = "") -> None:
279
+ """ONE agent turn -- the SAME path for a typed line and an idle-steer
280
+ pickup (liveness v2 §B), which threads the queued item's origin
281
+ `surface` into the turn so the kernel stamps provenance start-path.
282
+ Only the echo differs at the call sites: user_echo for a typed line,
283
+ foreign_turn for a remote one."""
272
284
  _sink.begin_turn()
285
+ kw = {"surface": surface} if surface else {}
273
286
  try:
274
- text = await agent.run(line, _sink, marathon=not once, goal=(line if not once else ""))
287
+ text = await agent.run(line, _sink, marathon=not once,
288
+ goal=(line if not once else ""), **kw)
275
289
  except (KeyboardInterrupt, asyncio.CancelledError):
276
290
  _sink.abort()
277
291
  _sink.note("Interrupted.")
278
- return "continue"
292
+ _sink.end_turn("") # clear busy (poller starvation guard)
293
+ return
279
294
  except Exception as e: # network/auth/etc — never crash the REPL
280
295
  _sink.note(f"Error: {type(e).__name__}: {e}")
281
- return "continue"
296
+ _sink.end_turn("") # clear busy: a stuck 'working' also starves the idle-steer poller
297
+ return
282
298
  _sink.end_turn(text)
283
- return "continue"
299
+
300
+ async def _steer_submit(text: str, surface: str) -> None:
301
+ """webbee.steer hands a drained remote instruction here: render it as
302
+ the remote user's own line, then run it as a normal turn."""
303
+ _sink.foreign_turn(surface, "user", text)
304
+ await _run_turn(text, surface=surface)
305
+
306
+ def _cancel_background() -> None:
307
+ for _t in (watcher_task, steer_task):
308
+ if _t is not None:
309
+ _t.cancel()
284
310
 
285
311
  async def _boot(s) -> None:
286
- nonlocal _sink, agent, intel, watcher_task, shadow
312
+ nonlocal _sink, agent, intel, watcher_task, shadow, steer_task
287
313
  _sink = s
288
314
  # Cache git branch OFF the event loop (subprocess.run blocks it). Only
289
315
  # /status reads it; recomputing it per input line froze the dock.
@@ -333,6 +359,16 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
333
359
  except Exception:
334
360
  shadow = None
335
361
  agent = agent_factory(cfg, token_provider, workspace, state["mode"])
362
+ # Liveness v2 §B: idle-steer pickup. All poll/drain logic lives in
363
+ # webbee.steer -- this is wiring only: the sink's live turn state
364
+ # gates polling, the agent's session id (once a turn has run) is the
365
+ # gateway truth, and _steer_submit is the normal turn path.
366
+ from webbee import steer as _steer
367
+ steer_task = asyncio.ensure_future(_steer.poll_idle_steer(
368
+ cfg, token_provider, workspace=workspace, marathon=not once,
369
+ is_busy=lambda: bool(getattr(_sink, "is_busy", None) and _sink.is_busy()),
370
+ live_session_id=lambda: getattr(agent, "session_id", ""),
371
+ submit=_steer_submit))
336
372
 
337
373
  if use_dock:
338
374
  ok = False
@@ -369,8 +405,7 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
369
405
  stop_turn=lambda: agent.stop(),
370
406
  )
371
407
  finally:
372
- if watcher_task is not None:
373
- watcher_task.cancel()
408
+ _cancel_background()
374
409
  except Exception:
375
410
  ok = False
376
411
  finally:
@@ -404,5 +439,4 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
404
439
  if await _handle(line) == "exit":
405
440
  return
406
441
  finally:
407
- if watcher_task is not None:
408
- watcher_task.cancel()
442
+ _cancel_background()