phase: 160_tool_budget_telemetry
Build and Push Containers / build-and-push-app (push) Successful in 2m27s
Build and Push Containers / build-and-push-db (push) Failing after 2m8s

All green. Final report:

**Phase 160 completion pass — verified (final verification; all 5 tasks already in `complete/`)**

- **Verified, no code defects found:** budget trailers (`TOOL_BUDGET_REMAINING/LAST/SPENT`), cap-path rejection + `ROUND_CAP_FALLBACK_ANSWER` + `ROUND_CAP_WRAPUP_INSTRUCTION`, `done.budget_exhausted` (ABSENT-when-false), `rounds=/cap_hit=` log fields, `agent_max_rounds=10` default, frontend Continue button + restore + shared-page exclusion, mock markers.
- **Tests/lint:** `uv run pytest` 153/153 green (EXIT 0); `uv run pytest --cov=app --cov-report=term-missing` TOTAL **99%** (>90%); `uv run ruff check .` clean; `uv run pyright` 0 errors/0 warnings.
- **Criteria:** ① decreasing trailer pinned by unit tests (9→…→spent) ✅ ② cap path ends readable, no tool lines after cap (E2E) ✅ ③ `budget_exhausted` true-only-on-cap, absent otherwise (integration) ✅ ④ Continue button → one fresh `continue` turn, fresh budget, record intact (E2E) ✅ ⑤ flag survives refresh; shared chat never renders it (`shared.js` has no `budget_exhausted` path) ✅ ⑥ `max_rounds=0` byte-identical single `tools=None` request; default still 10 ✅ ⑦ full suite/coverage/lint green ✅ ⑧ `test_tool_budget_telemetry.py` **7/7** in isolation; all 6 regression E2E suites green in isolation ✅ ⑨ commit + phase move = harness (I make no git history/index changes).
- **Docs:** README.md — fixed: the "Continue researching" button and cap behavior were undocumented (added a Chat-section paragraph + an Agent-tools-section paragraph, incl. the phase-71 recorded-revision note); env-var table, commands, and architecture all verified consistent otherwise. AGENTS.md — verified consistent, no changes needed.
- **Decisions:** no code changes required — phase was fully implemented; only README docs added. No live-infrastructure touched.
- **Next pending phase:** `161_reasoning_bubble`.
This commit is contained in:
2026-10-06 01:42:44 -04:00
parent cb8767512a
commit 8172956c15
55 changed files with 4124 additions and 344 deletions
+132 -2
View File
@@ -232,6 +232,12 @@ def _expect(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
"stopped": m.get("stopped"),
"failed": m.get("failed"),
"error": m.get("error"),
# Phase 160 (task 04): the budget-exhausted marker —
# explicit null where the record does not carry it (the
# bor.chat.v1 shape is explicit-null for every optional
# key; a record WITH the key round-trips it byte-
# identically — the pins below).
"budget_exhausted": m.get("budget_exhausted"),
}
for m in records
]
@@ -464,8 +470,10 @@ def test_create_round_trips_full_brain_record(admin_client: TestClient) -> None:
assert r.status_code == 201
# The bor.chat.v1-shaped payload round-trips losslessly: every
# optional key (sources/related/deflected/suggestions/thinking/
# tools/stopped) survives identical.
assert r.json()["messages"][1] == FULL_BRAIN
# tools/stopped/failed/error/budget_exhausted — the phase-160
# additive marker, explicit null where absent) survives identical
# (the _expect full stored shape).
assert r.json()["messages"][1] == _expect([_user(FIRST_QUESTION), FULL_BRAIN])[1]
def test_create_rejects_empty_messages(admin_client: TestClient) -> None:
@@ -1036,6 +1044,128 @@ def test_shared_serve_includes_the_brain_answer_image_ref(
assert "answer_image" not in messages[0]
# ---------------------------------------------------------------------------
# Phase 160 (task 04, TODO L4): the budget-exhausted marker — the
# persisted answer that ran out of tool calls. The done-time
# auto-save (``rememberBrainTurn``) persists the done frame's
# ``budget_exhausted`` into the brain record's ``budget_exhausted``
# key (the ``answer_image`` precedent — the field must exist so the
# ``extra="forbid"`` boundary does NOT 422 those saves; the
# phase-113 lesson, applied up front), and the restore path re-
# renders the "Continue researching" button from it (task 04 — the
# marker is a client restore hint only, never a server behavior; no
# migration: ``saved_chats.messages`` is raw JSONB, phase 50, and
# ``app/api/chats.py``'s logic is unchanged). These round-trips are
# that regression pin.
# ---------------------------------------------------------------------------
def test_create_round_trips_the_budget_exhausted_marker(
admin_client: TestClient,
) -> None:
"""The phase-113 regression pin (the A2 quiet-failure path): a
brain record carrying ``budget_exhausted: true`` →
``POST /api/chats`` is 201 (NOT a silent 422) and the admin GET
round-trips the record BYTE-FOR-BYTE through Postgres (the
``_expect`` full bor.chat.v1 shape — the restore path re-renders
the Continue button from the marker). The user record carries the
key as an explicit null (it never did, and the stored shape is
explicit-null for every optional key)."""
brain = {**FULL_BRAIN, "budget_exhausted": True}
r = admin_client.post(
"/api/chats",
json={"messages": [_user(FIRST_QUESTION), brain]},
)
assert r.status_code == 201, f"expected 201, got {r.status_code}: {r.text}"
body = r.json()
assert body["messages"][1]["budget_exhausted"] is True
assert body["messages"][0]["budget_exhausted"] is None
got = admin_client.get(f"/api/chats/{body['id']}")
assert got.status_code == 200
assert got.json()["messages"] == _expect(
[_user(FIRST_QUESTION), {**FULL_BRAIN, "budget_exhausted": True}]
)
def test_create_without_budget_exhausted_marker_round_trips_unchanged(
admin_client: TestClient,
) -> None:
"""A record WITHOUT the marker (every non-cap turn, every pre-
phase record) round-trips with the key as an explicit null — the
stored shape never gains a value (the byte-identical contract,
the phase-50 precedent: the key is absent from the client's
``bor.chat.v1`` record via the ``undefined``-drops-the-key
pattern, and explicit-null here keeps the restore path null-safe).
The ``FULL_BRAIN`` shape (no marker) is the pin: its stored and
served form is byte-identical to pre-phase-160 apart from the
one new null key."""
r = admin_client.post(
"/api/chats",
json={"messages": [_user(FIRST_QUESTION), FULL_BRAIN]},
)
assert r.status_code == 201, r.text
body = r.json()
assert body["messages"][1]["budget_exhausted"] is None
got = admin_client.get(f"/api/chats/{body['id']}")
assert got.status_code == 200
assert got.json()["messages"] == _expect([_user(FIRST_QUESTION), FULL_BRAIN])
def test_shared_serve_includes_the_budget_exhausted_marker(
admin_client: TestClient,
) -> None:
"""The shared view is faithful (the data is in the public
snapshot), but the control is NOT: the shared page
(``/shared/<token>``, rendered server-side by ``shared.js`` —
never by ``app.js``, owner-locked zero interactive controls)
IGNORES the key by construction — no Continue control renders for
a flagged record (work item 8; pinned in
``test_frontend_tool_budget.py::test_shared_page_never_shows_
the_continue_control``). The user record stays with the explicit
null."""
brain = {**FULL_BRAIN, "budget_exhausted": True}
r = admin_client.post(
"/api/chats",
json={"messages": [_user(FIRST_QUESTION), brain]},
)
assert r.status_code == 201, r.text
share_url = _share(admin_client, r.json()["id"])["share_url"]
anon = TestClient(fastapi_app) # fresh jar: truly anonymous
got = anon.get(f"/api{share_url}")
assert got.status_code == 200
messages = got.json()["messages"]
assert messages[1]["budget_exhausted"] is True # faithful snapshot
assert messages[0]["budget_exhausted"] is None
def test_put_round_trips_the_budget_exhausted_marker(
admin_client: TestClient,
) -> None:
"""The re-Save upsert (the auto-save path the client's restore
relies on) round-trips the marker byte-identically too: a PUT
with the flagged record replaces the stored JSONB and the 200
body + the GET both carry it (a refreshed chat page restores the
Continue affordance from the persisted record)."""
created = admin_client.post(
"/api/chats",
json={"messages": [_user(FIRST_QUESTION), FULL_BRAIN]},
).json()
brain = {**FULL_BRAIN, "budget_exhausted": True}
r = admin_client.put(
f"/api/chats/{created['id']}",
json={"messages": [_user(FIRST_QUESTION), brain]},
)
assert r.status_code == 200, r.text
assert r.json()["messages"][1]["budget_exhausted"] is True
got = admin_client.get(f"/api/chats/{created['id']}")
assert got.status_code == 200
assert got.json()["messages"] == _expect(
[_user(FIRST_QUESTION), {**FULL_BRAIN, "budget_exhausted": True}]
)
# ---------------------------------------------------------------------------
# Phase 159 (task 02): edit-and-resend — the SAVE shape. The client
# edits the last question: the record is replaced in place, EVERY