Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,17 @@ jobs:
# here or anywhere else. A server started here as a separate step
# would just be an unused, orphaned process.

# ONE venv, ONE command -- kept byte-identical in intent to the
# Makefile's `venv` target. The second editable target is the
# amplifier tool module under modules/: a separate installable package
# whose absence made `import amplifier_module_tool_work_tracker` a
# ModuleNotFoundError here, which is why its suite ran in nothing
# (ledger row CCV1-022). Its `[dev]` extra carries that suite's
# test-only deps (amplifier-core, pytest-asyncio).
- name: Set up amplifier-work-tracker
run: |
uv venv .venv --python 3.12
uv pip install --python .venv/bin/python -e ".[dev,web]"
uv pip install --python .venv/bin/python -e ".[dev,web]" -e "modules/tool-work-tracker[dev]"

- name: Lint + format check (ruff)
run: .venv/bin/ruff check . && .venv/bin/ruff format --check .
Expand All @@ -97,6 +104,18 @@ jobs:
- name: Tier 4 -- conformance ledger
run: .venv/bin/python -m pytest ledger/checks -v

# Tier 5 -- the amplifier tool module's own suite
# (modules/tool-work-tracker/tests). The only place the post-reclaim
# custody behaviour of the AGENT SEAM (work_claim / work_declare /
# work_resolve / work_release) is asserted mechanically; before this
# step existed the suite was importable by nothing and run by nothing
# (ledger row CCV1-022). A separate pytest invocation on purpose: it
# brings its own session-scoped isolated dolt server fixture, so
# folding it into a tier above would stand up two servers in one
# session. Uses the same pinned `bd` installed above.
- name: Tier 5 -- tool module tests
run: .venv/bin/python -m pytest modules/tool-work-tracker/tests -v

- name: Dolt server log (always, for debugging)
if: always()
run: cat /tmp/dolt-server.log || true
39 changes: 35 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: venv test test-unit test-integration test-cli test-ledger check lint types doctor clean
.PHONY: venv test test-unit test-integration test-cli test-ledger test-module check lint types doctor clean

PYTHON ?= python3.12
VENV := .venv
Expand All @@ -7,9 +7,19 @@ PYTEST := $(PY) -m pytest

# One-time (or after dependency changes) environment setup. Uses `uv` for
# speed; falls back to nothing fancier than a normal editable install.
#
# ONE venv, ONE command. The second editable target is the amplifier tool
# module under modules/ -- it is a separate installable package with its own
# pyproject.toml, and without it `import amplifier_module_tool_work_tracker`
# raises ModuleNotFoundError in this venv, which is exactly why its test
# suite ran in nothing (ledger row CCV1-022). Its `[dev]` extra carries the
# two test-only dependencies that suite needs and the root package does not
# (amplifier-core, pytest-asyncio); they are declared there rather than
# duplicated into the root `dev` extra so each package keeps owning its own
# dependencies.
venv:
uv venv $(VENV) --python $(PYTHON)
uv pip install --python $(PY) -e ".[dev,web]"
uv pip install --python $(PY) -e ".[dev,web]" -e "modules/tool-work-tracker[dev]"

## Tier 1 -- unit: pure logic, no bd, no network. Target: whole tier < 5s.
test-unit:
Expand All @@ -30,9 +40,30 @@ test-cli:
test-ledger:
$(PYTEST) ledger/checks -v

## All four tiers.
## Tier 5 -- tool module: modules/tool-work-tracker's own suite, the only
## place the post-reclaim custody behaviour of the AGENT SEAM (work_claim /
## work_declare / work_resolve / work_release) is asserted mechanically.
## Deliberately its own pytest invocation rather than another path argument
## on `test` below: that suite ships its own session-scoped isolated dolt
## server fixture (a copy of the root suite's, since fixtures cannot cross
## a pytest run), and folding the two together would stand up two servers
## in one session for no gain. Requires `make venv` (the module must be
## installed into the venv) and a real `bd` on PATH -- without bd the
## real-storage tests skip rather than fail.
test-module:
$(PYTEST) modules/tool-work-tracker/tests -v

## All five tiers. Two pytest invocations (see `test-module` above), and
## deliberately NOT fail-fast between them: the whole point of wiring the
## module suite in (ledger row CCV1-022) is that it stops being silently
## skippable, and a pre-existing failure in the root suite must not go back
## to hiding tier 5's result. Both always run; the target still fails if
## either did.
test:
$(PYTEST) tests ledger/checks -v
@rc=0; \
$(PYTEST) tests ledger/checks -v || rc=$$?; \
$(PYTEST) modules/tool-work-tracker/tests -v || rc=$$?; \
exit $$rc

## Lint + type-check.
check: lint types
Expand Down
69 changes: 45 additions & 24 deletions context/awareness.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,35 @@ You're one of several agents pulling from a shared work queue. First use in
a session, or any `work_*` call fails to connect: call `work_tracker_status`
before assuming a server is running — see "Where to go next" below.

Five things here fail **silently** if you get them wrong — no error, no undo:
Six things here fail **silently** if you get them wrong — no error, no undo:

1. **Claim only via `work_claim` / `work_status`; never list-then-pick.** The
obvious approach — read the ready queue, choose an item, mark it yours —
double-claimed in **2 of 8 measured trials**, and every losing agent
still got exit 0. `work_claim` is the single atomic claim-and-custody
operation. There is no other way to take an item.

2. **Custody is a liveness signal, not a timer.** Idle time never costs you a
claim — you may sit for hours awaiting a human's answer. Only an
**unrenewed** custody signal does: 15 minutes without a renewal releases
the item back to the queue. `awaiting_human` (via `work_declare`) only
suppresses a notification — it never exempts you from that clock.
2. **Custody is a liveness signal, not a timer — and neither end of it is
automatic.** Idle time never costs you a claim; you may sit for hours
awaiting a human's answer. Only an **unrenewed** custody signal does.
Two consequences, both silent:
- **Renewal is one-strike.** It runs in the background while your
session process lives, but a single failed renewal ends renewal
permanently — there is no retry on the next tick — and this session
goes on believing it still holds the item. The only way to find out
is `work_status`: a non-null `holding.custody_lost` means renewal
stopped and the hold is on its way to being reclaimed. Check it
before any long-running step and after any tool error.
- **The TTL does not enforce itself.** After 15 minutes with no renewal
a hold is merely *reclaim-eligible*; the out-of-band `reap` sweep is
what actually reclaims it, and only where an operator has one
installed and running. Expect a reclaim to land up to a sweep
interval (300s by default) AFTER the TTL, and expect a dead agent's
hold to persist indefinitely where no sweep runs — never wait on a
stuck held item assuming it frees itself.

`awaiting_human` (via `work_declare`) only suppresses a notification —
it never exempts you from that clock.

3. **An empty queue is a normal terminal outcome.** `work_claim` returning
`claimed: null` means stop and report — not a signal to invent work or
Expand All @@ -32,25 +48,30 @@ Five things here fail **silently** if you get them wrong — no error, no undo:
retry resolving or declaring that item — someone else may hold it now.
`work_claim` can still be used afterward to pick up new work.

6. **A reported write failure means the write did NOT land — but never
blindly retry the same operation either; re-read first.** You're sharing
a single-writer dolt server with every other agent's claims, renewals,
and resolves. A write occasionally loses a serialization race and
6. **A reported write failure does NOT prove the write failed — treat it as
UNKNOWN and re-read before you retry.** You're sharing a single-writer
dolt server with every other agent's claims, renewals, and resolves. A
write occasionally loses a serialization race and
`work_resolve`/`work_file`/the CLI raises an error like "still
conflicting after 8 retries." That specific error family (dolt/MySQL
1213/1205/"serialization failure"/"try restarting transaction") means
the transaction was aborted — by database guarantee, never partially
committed — so the write genuinely did not happen. The unsafe move is
resubmitting blind: for a non-idempotent write (creating a new item) a
blind retry after an ambiguous-looking failure can leave a duplicate.
The safe move is always the same: re-read the item first (`work_list`'s
`item_id` form, or `get_readonly` — a read-only path that cannot itself
conflict) to see its real current state, then decide whether the
original operation still needs doing. A *reported success*, by contrast,
is already independently verified — `resolve`/`unclaim` read the item
back and raise rather than report success if the change didn't actually
land — so this caution is specifically about what to do after a
*reported failure*, not a general distrust of success responses.
conflicting after 8 retries" (dolt/MySQL 1213/1205, "serialization
failure", "try restarting transaction"). Measured reality: a write that
surfaced as one of those errors can still have LANDED — an observed
incident, and the reason the read-back behaviour below exists. So:
- `work_resolve` and `work_release` already handle it for you: on a
conflict they re-read the item and report success when the write did
in fact land, and they verify their own success path by read-back
too. A *reported success* from those two is independently confirmed.
- Every other write verb (`work_add`, `work_edit`, `work_file`,
`work_defer`, `work_block`, `work_dep`, and the CLI equivalents)
still surfaces the raw conflict unverified. There, a reported failure
means *unknown*, never *didn't happen*.

The unsafe move is resubmitting blind: for a non-idempotent write
(creating a new item) a blind retry can leave a duplicate of a write
that already landed. The safe move is always the same: re-read the item
first (`work_list`'s `item_id` form, or `get_readonly` — a read-only
path that cannot itself conflict) to see its real current state, then
decide whether the original operation still needs doing.

## Where to go next

Expand Down
Loading