webbee 0.3.0__tar.gz → 0.3.1__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.
- {webbee-0.3.0 → webbee-0.3.1}/CHANGELOG.md +6 -0
- {webbee-0.3.0 → webbee-0.3.1}/PKG-INFO +1 -1
- {webbee-0.3.0 → webbee-0.3.1}/pyproject.toml +1 -1
- webbee-0.3.1/src/webbee/__init__.py +1 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/checkpoints.py +25 -4
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/tools.py +12 -8
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_checkpoints.py +27 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_tools.py +40 -16
- webbee-0.3.0/src/webbee/__init__.py +0 -1
- {webbee-0.3.0 → webbee-0.3.1}/.github/workflows/publish.yml +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/.gitignore +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/LICENSE +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/README.md +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/install.sh +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/account.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/banner_art.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/cli.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/clipboard.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/commands.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/config.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/details.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/events.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/frames.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/intel/chunker.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/intel/embed.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/intel/graph.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/intel/indexer.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/intel/models.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/intel/query.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/intel/service.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/intel/store.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/intel/watch.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/output_pane.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/render.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/repl.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/repo.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/session.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/sessions.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/stream.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/tui.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/src/webbee/update.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/__init__.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_account.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_cli.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_clipboard.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_commands.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_config.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_details.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_events.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_freeze_fix.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_intel_chunker.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_intel_embed.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_intel_graph.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_intel_indexer.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_intel_query.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_intel_service.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_intel_store.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_intel_vectors.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_intel_watch.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_marathon.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_packaging.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_render.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_repl.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_repo.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_session.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_sessions.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_stream.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_tui.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_tui_hardening.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_update.py +0 -0
- {webbee-0.3.0 → webbee-0.3.1}/tests/test_version.py +0 -0
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
- Auto-checkpointing no longer disables itself after a single transient
|
|
6
|
+
shadow-git error — it only pauses after repeated failures, and
|
|
7
|
+
`/checkpoints` shows when it's paused.
|
|
8
|
+
|
|
3
9
|
## 0.3.0
|
|
4
10
|
|
|
5
11
|
- **The agent plans in the open.** A live todo checklist (📋) rendered from the
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.1"
|
|
@@ -44,7 +44,8 @@ class ShadowGit:
|
|
|
44
44
|
base = cache_dir or os.path.expanduser("~/.cache/webbee")
|
|
45
45
|
self.git_dir = os.path.join(base, "shadow", repo_key)
|
|
46
46
|
self.available = False
|
|
47
|
-
self.auto_ok = True
|
|
47
|
+
self.auto_ok = True # auto-checkpointing enabled (F8 latch below)
|
|
48
|
+
self._auto_fail_streak = 0 # consecutive failed AUTO snapshots
|
|
48
49
|
|
|
49
50
|
def _git(self, *args: str) -> "subprocess.CompletedProcess":
|
|
50
51
|
cmd = (["git", "--git-dir", self.git_dir, "--work-tree", self.root]
|
|
@@ -202,12 +203,32 @@ class ShadowGit:
|
|
|
202
203
|
f"was saved first as checkpoint cp-{(undo or {}).get('n', '?')} "
|
|
203
204
|
f"({(undo or {}).get('id', '?')}), so this rollback is itself undoable.")}
|
|
204
205
|
|
|
206
|
+
_AUTO_FAIL_LATCH = 3 # consecutive AUTO failures before pausing auto-checkpointing
|
|
207
|
+
|
|
208
|
+
def note_auto_result(self, ok: bool) -> None:
|
|
209
|
+
"""Record an AUTO (pre-write) snapshot outcome. A single transient
|
|
210
|
+
failure must NOT disable the session's time machine (final-review F8):
|
|
211
|
+
latch auto-checkpointing OFF only after _AUTO_FAIL_LATCH CONSECUTIVE
|
|
212
|
+
failures; any success resets the streak and re-enables it."""
|
|
213
|
+
if ok:
|
|
214
|
+
self._auto_fail_streak = 0
|
|
215
|
+
self.auto_ok = True
|
|
216
|
+
else:
|
|
217
|
+
self._auto_fail_streak += 1
|
|
218
|
+
if self._auto_fail_streak >= self._AUTO_FAIL_LATCH:
|
|
219
|
+
self.auto_ok = False
|
|
220
|
+
|
|
205
221
|
def describe(self) -> str:
|
|
206
222
|
"""One printable block for the /checkpoints command."""
|
|
207
223
|
if not self.available:
|
|
208
224
|
return "Reversibility is off (git unavailable)."
|
|
209
225
|
rows = self.list_checkpoints(limit=10)
|
|
210
226
|
if not rows:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
227
|
+
body = "No checkpoints yet."
|
|
228
|
+
else:
|
|
229
|
+
lines = [f"cp-{r['n']} {r['id']} {r['when']:>16} {r['label']}" for r in rows]
|
|
230
|
+
body = "Checkpoints (newest first) — /rollback <id|cp-N|N>:\n" + "\n".join(lines)
|
|
231
|
+
if not self.auto_ok:
|
|
232
|
+
body = ("⚠ Auto-checkpointing is paused after repeated shadow errors — "
|
|
233
|
+
"manual /checkpoint + /rollback still work.\n") + body
|
|
234
|
+
return body
|
|
@@ -36,16 +36,20 @@ class LocalToolExecutor:
|
|
|
36
36
|
args = {}
|
|
37
37
|
if (tool in _WRITE_TIER and self.shadow is not None
|
|
38
38
|
and getattr(self.shadow, "auto_ok", True)):
|
|
39
|
-
# The time machine snapshots BEFORE every mutation -- and must
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
39
|
+
# The time machine snapshots BEFORE every mutation -- and must never
|
|
40
|
+
# block or fail the actual work. A SINGLE transient failure must not
|
|
41
|
+
# disable the session (final-review F8): note_auto_result latches
|
|
42
|
+
# auto-checkpointing OFF only after several CONSECUTIVE failures; a
|
|
43
|
+
# success re-enables it. Manual checkpoint/rollback always try.
|
|
44
44
|
try:
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
_cp = self.shadow.checkpoint(f"pre:{tool}")
|
|
46
|
+
_note = getattr(self.shadow, "note_auto_result", None)
|
|
47
|
+
if _note is not None:
|
|
48
|
+
_note(_cp is not None)
|
|
47
49
|
except Exception:
|
|
48
|
-
self.shadow
|
|
50
|
+
_note = getattr(self.shadow, "note_auto_result", None)
|
|
51
|
+
if _note is not None:
|
|
52
|
+
_note(False)
|
|
49
53
|
try:
|
|
50
54
|
fn = getattr(self, f"_t_{tool}", None)
|
|
51
55
|
if fn is None:
|
|
@@ -249,3 +249,30 @@ def test_failed_add_never_fakes_a_checkpoint(tmp_path, monkeypatch):
|
|
|
249
249
|
def test_shadow_dir_is_private(tmp_path):
|
|
250
250
|
sg, _root = _sg(tmp_path)
|
|
251
251
|
assert (os.stat(sg.git_dir).st_mode & 0o777) == 0o700
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_auto_latch_survives_a_single_transient_failure(tmp_path):
|
|
255
|
+
sg, _root = _sg(tmp_path)
|
|
256
|
+
sg.note_auto_result(False) # one transient miss
|
|
257
|
+
assert sg.auto_ok is True # NOT disabled on a single failure
|
|
258
|
+
sg.note_auto_result(True) # a success clears the streak
|
|
259
|
+
assert sg.auto_ok is True and sg._auto_fail_streak == 0
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_auto_latch_trips_after_consecutive_failures(tmp_path):
|
|
263
|
+
sg, _root = _sg(tmp_path)
|
|
264
|
+
for _ in range(sg._AUTO_FAIL_LATCH):
|
|
265
|
+
sg.note_auto_result(False)
|
|
266
|
+
assert sg.auto_ok is False # latched after N consecutive
|
|
267
|
+
sg.note_auto_result(True) # recovery re-enables
|
|
268
|
+
assert sg.auto_ok is True
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_describe_surfaces_paused_autocheckpoint(tmp_path):
|
|
272
|
+
sg, root = _sg(tmp_path)
|
|
273
|
+
_write(root, "a.txt", "x"); sg.checkpoint("c1")
|
|
274
|
+
for _ in range(sg._AUTO_FAIL_LATCH):
|
|
275
|
+
sg.note_auto_result(False)
|
|
276
|
+
out = sg.describe()
|
|
277
|
+
assert out.startswith("⚠ Auto-checkpointing is paused")
|
|
278
|
+
assert "cp-1" in out # the list still follows the warning
|
|
@@ -205,6 +205,8 @@ class _FakeShadow:
|
|
|
205
205
|
def checkpoint(self, label=""):
|
|
206
206
|
self.labels.append(label)
|
|
207
207
|
return {"id": "abc1234", "n": len(self.labels), "label": label, "changed": True}
|
|
208
|
+
def note_auto_result(self, ok):
|
|
209
|
+
pass
|
|
208
210
|
def diff(self, since=""):
|
|
209
211
|
return f"DIFF since={since or 'HEAD'}"
|
|
210
212
|
def rollback(self, to):
|
|
@@ -265,21 +267,43 @@ def test_reversibility_tools_honest_without_shadow(tmp_path):
|
|
|
265
267
|
assert not r["ok"] and "unavailable" in r["content"]
|
|
266
268
|
|
|
267
269
|
|
|
268
|
-
def
|
|
270
|
+
def test_auto_checkpoint_latches_off_after_consecutive_failures(tmp_path):
|
|
271
|
+
# Adapted from the P4 latch test (final-review F8): a SINGLE failed AUTO
|
|
272
|
+
# snapshot must no longer latch auto-checkpointing off; it now takes
|
|
273
|
+
# _AUTO_FAIL_LATCH CONSECUTIVE failures, driven through the real
|
|
274
|
+
# ShadowGit.note_auto_result via the executor.
|
|
269
275
|
from webbee.tools import LocalToolExecutor
|
|
276
|
+
from webbee.checkpoints import ShadowGit
|
|
270
277
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
assert
|
|
278
|
+
sg = ShadowGit(str(tmp_path), "rk_f8_latch", cache_dir=str(tmp_path / "c3"))
|
|
279
|
+
assert sg.ensure()
|
|
280
|
+
calls = {"n": 0}
|
|
281
|
+
def _always_fails(label=""):
|
|
282
|
+
calls["n"] += 1
|
|
283
|
+
return None # every AUTO snapshot fails
|
|
284
|
+
sg.checkpoint = _always_fails
|
|
285
|
+
ex = LocalToolExecutor(str(tmp_path), shadow=sg)
|
|
286
|
+
|
|
287
|
+
for i in range(sg._AUTO_FAIL_LATCH - 1):
|
|
288
|
+
ex.run("write_file", {"path": f"f{i}.txt", "content": str(i)})
|
|
289
|
+
assert sg.auto_ok is True # still enabled before the Nth failure
|
|
290
|
+
|
|
291
|
+
ex.run("write_file", {"path": "final.txt", "content": "x"})
|
|
292
|
+
assert calls["n"] == sg._AUTO_FAIL_LATCH # latched only after N consecutive failures
|
|
293
|
+
assert sg.auto_ok is False
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_executor_single_transient_does_not_latch(tmp_path):
|
|
297
|
+
from webbee.tools import LocalToolExecutor
|
|
298
|
+
from webbee.checkpoints import ShadowGit
|
|
299
|
+
sg = ShadowGit(str(tmp_path), "rk_f8", cache_dir=str(tmp_path / "c"))
|
|
300
|
+
assert sg.ensure()
|
|
301
|
+
calls = {"n": 0}
|
|
302
|
+
def _flaky(label=""):
|
|
303
|
+
calls["n"] += 1
|
|
304
|
+
return None if calls["n"] == 1 else {"id": "x", "n": calls["n"], "label": label, "changed": True}
|
|
305
|
+
sg.checkpoint = _flaky
|
|
306
|
+
ex = LocalToolExecutor(str(tmp_path), shadow=sg)
|
|
307
|
+
ex.run("write_file", {"path": "a.txt", "content": "1"}) # transient miss
|
|
308
|
+
ex.run("write_file", {"path": "b.txt", "content": "2"}) # success -> streak reset
|
|
309
|
+
assert sg.auto_ok is True # still enabled
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.0"
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|