phase: 189_chat_save_long_turns
**Phase 189 final verification — all green**
- **Verified (all 4 tasks already implemented):** A1 caps in `app/schemas.py` — `sources`/`related`/`tools` 200, `suggestions` 100, `segments` 450; `text`/`thinking` 600 000, `error`/`image` 500, `answer_text` 50, `messages` 500, 8 M budget all UNCHANGED; rationale docstrings rewritten to `merge_cited_docs`/`agent_max_rounds`. A2 `shareCurrentChat` linked branch upserts (PUT, 404→create+re-link) before `POST /{id}/share`, aborts on failure (banner, no token); unlinked branch byte-identical. A3 `savePending` + `finally` re-run. A4 `saveFailedSticky` + one 5 s `setTimeout` retry on network/5xx only, 4xx never retries.
- `uv run pytest --cov=app --cov-report=term-missing` → **4609 passed, 1 warning, EXIT=0**, `TOTAL 99%`, `app/schemas.py 100%` (>90%).
- `uv run pytest tests/e2e/test_chat_save_long_turns.py -v --no-cov` → **4 passed, EXIT=0** (isolation).
- Regression green & **unedited** (`git status` clean): `test_chat_save_payload_limits.py` 2, `test_chat_history.py` 5, `test_share_chat.py` 4. Extra blast-radius sweep (share/save/persist/segments surface, all EXIT=0): `test_save_share_ux`, `test_stale_saved_chats`, `test_history_pagination`, `test_history_copy`, `test_bottom_chat_actions`, `test_reasoning_bubble`, `test_tool_budget_telemetry`, `test_verbatim_show_text`, `test_smoke`. No leaked `saved_chats` rows (`save-long-%` = 0).
- `uv run ruff check .` → All checks passed; `uv run pyright` → **0 errors**.
- **Completion criteria:** caps ✓ · incident payload 22/33/51 → 201/200 anonymously + 201 tools/451 segments/101 suggestions → 422 naming the offender, row unchanged ✓ · share-saves-first + abort + unlinked byte-identical ✓ · overlap re-run + sticky note + one-shot retry ✓ · phase E2E in isolation + 3 regression suites unedited ✓ · full suite / coverage / lint / types ✓ · `git diff --stat` = `app/schemas.py`, `frontend/assets/app.js`, 3 test files, `README.md` + new E2E & phase files (no `app/api/chats.py`, no migration, no new dependency) ✓ · commit + `todo/`→`complete/` move left to the harness (rules forbid git index writes).
- **Docs:** full README review (all 2 258 lines) + machine cross-check — all 85 settings documented in the Configuration reference, all 34 `.env.example` vars documented, documented defaults match `app/config.py`. Fixed 2 inconsistencies: (1) the 117-char prose line task 04 left when reflowing the Saved-chats pager paragraph; (2) the phase-189 sentence implying the 8 M total bounds the widened arrays — now states precisely that the total counts `text`/`thinking`/verbatim-block text only, and that the arrays are bounded by fixed item COUNT (matches `_require_within_total_budget`). AGENTS.md verified consistent — it enumerates no save contract or list cap (rule 5's phase-166 in-flight-turn clause untouched), so **no AGENTS.md change** per the phase mandate.
- **Notable/deviation:** the wider caps raise the *theoretical* anonymous-POST worst case (arrays are outside the 8 M char budget); left as designed (A1 fixes every other bound), documented honestly in README — realistic payloads stay ~10 MB, so the existing `client_max_body_size ≥ 16 MB` guidance still holds. No live-infra changes were made (data-repair of the two stale shared rows stays owner action).
- **Next pending phase:** none — `189_chat_save_long_turns` is the last entry in `.agents/phases/todo/`.
This commit is contained in:
@@ -285,9 +285,21 @@ List, edit, or remove them here — no conversation required.
|
||||
### Saved chats (`/history.html`)
|
||||
|
||||
Every conversation is saved automatically. Click a title to return to that
|
||||
chat. Admin-only view. The list is paginated server-side (25 rows per
|
||||
page) — with more than 25, a Prev / “Page N of M” / Next pager appears
|
||||
under the table to walk it, and **Refresh** re-fetches the current page.
|
||||
chat. Admin-only view.
|
||||
|
||||
**The auto-save never drops or silently loses a turn** (phase 189). Save points
|
||||
overlap on a long turn (the send, the answer landing, a page unload), and only
|
||||
one upsert runs at a time — a save point that arrives mid-upsert is now queued
|
||||
and re-runs once the in-flight request settles, so the last word always reaches
|
||||
the row instead of waiting for a next save point that may never come. When a
|
||||
save does fail, the one-line note under the composer *stays* until the next
|
||||
successful save (it used to be overwritten and forgotten), and a transient
|
||||
failure — a network drop or a 5xx — retries by itself after 5 s; a rejected
|
||||
payload (4xx) never retries, because retrying it would reject it again.
|
||||
|
||||
The list is paginated server-side (25 rows per page) — with more than 25, a
|
||||
Prev / “Page N of M” / Next pager appears under the table to walk it, and
|
||||
**Refresh** re-fetches the current page.
|
||||
`GET /api/chats` carries the same contract: `?limit=` (default 25,
|
||||
clamped 1–100) / `?offset=` and a full `total` on every response.
|
||||
|
||||
@@ -325,6 +337,18 @@ file has to let the reader copy it and check it. Both come from the same
|
||||
owner's), and a guest who follows the viewer link lands on the sign-in page —
|
||||
the documents API is admin-gated.
|
||||
|
||||
**Sharing saves first** (phase 189). **Share** on a conversation that is
|
||||
already saved writes the CURRENT conversation to its row — the same upsert the
|
||||
auto-save uses, deleted-row included (a 404 recreates the row and re-links) —
|
||||
and only then mints the token, so the shared page is the conversation you are
|
||||
looking at and never a snapshot it has outgrown. The owner incident: a long
|
||||
research turn whose done-time auto-save was rejected left the row holding only
|
||||
the question, and the Share froze that loss behind a link. If that save fails
|
||||
the share **aborts** — the neutral *"Couldn't share the conversation — try
|
||||
again."* banner, no link, no toast — because a link to content that is not the
|
||||
current conversation is worse than no link. A conversation that was never
|
||||
saved still saves **and** shares in one action.
|
||||
|
||||
### Conversation payload limits
|
||||
|
||||
A conversation has one shape, and it carries the **same static bounds**
|
||||
@@ -339,10 +363,29 @@ saving a conversation can never be stricter than continuing it (phase
|
||||
| Turns sent as `history` | 500 | the whole browser conversation (the client sends everything it holds, minus the question being asked) |
|
||||
| Messages per saved chat | 500 | the same conversation, saved |
|
||||
| One verbatim block (`answer_text[].text`) | 600 000 chars | the same answer-text cap (phase 182) — comfortably above `BOR_SHOW_TEXT_MAX_CHARS`, so raising that budget never hits the wire limit |
|
||||
| Verbatim blocks per saved message | 50 | the `tools` / `segments` number (phase 182) — far past the `BOR_SHOW_TEXT_MAX_BLOCKS` default of 5 |
|
||||
| Verbatim blocks per saved message | 50 | 10× the `BOR_SHOW_TEXT_MAX_BLOCKS` default of 5 (phase 182) — this one follows the display budget, not the round count |
|
||||
| Cited / related docs per message (`sources`, `related`) | 200 each | **4× the round cap** (production `BOR_AGENT_MAX_ROUNDS=50`) — the citation chips are the documents the agent actually READ, not just the top-N retrieved, so one long research turn cites roughly one document per round and more (phase 189) |
|
||||
| Tool calls per message (`tools`) | 200 | the same 4× the round cap: a round is one model request, and one-or-more tool calls per round are legal (phase 189) |
|
||||
| Timeline entries per message (`segments`) | 450 | the thinking/tool timeline of that same turn — ≤ 200 tool entries + one thinking entry per contiguous thinking run (≤ ~50) + margin (phase 189) |
|
||||
| Suggestion chips per message | 100 | 20× the `BOR_SUGGESTED_DOCS` default of 5; each chip stays ≤ 200 chars (phase 189) |
|
||||
| **Total chars per saved chat** | **8 000 000** | ~15 full-length answers; the one bound on the anonymous save surface, which stays bounded on purpose — and it counts the verbatim blocks' bytes too (phase 182), so the display blocks cannot become a second, unbounded byte channel |
|
||||
| One question (`message`) | 4 000 chars | unchanged; the composer counts it live |
|
||||
|
||||
**The list caps follow the agent's real round cap** (phase 189). They were
|
||||
sized for the shipped `BOR_AGENT_MAX_ROUNDS` default of 10, when the citation
|
||||
row was assumed to be "the top-N docs plus a handful of reads"; since phase
|
||||
119 the cited tier *is* the agent's read documents, so a long research turn —
|
||||
like the owner's, on a container running `BOR_AGENT_MAX_ROUNDS=50` — cites two
|
||||
dozen documents. Such a turn used to be **rejected by its own auto-save** (a
|
||||
silent 422 at the old 20-source cap, swallowed by the quiet-failure contract),
|
||||
which is why a shared chat could hold only the question. An operator raising
|
||||
`BOR_AGENT_MAX_ROUNDS` far past 50 still hits a fixed boundary — the caps
|
||||
scale with the configuration, not with a guess. The byte bound is unchanged
|
||||
too: the 8 000 000-character total above counts each message's `text`,
|
||||
`thinking` and verbatim-block text (never the citation/tool arrays), which is
|
||||
exactly why those arrays keep a fixed item COUNT instead of a byte count —
|
||||
neither channel can grow without a limit.
|
||||
|
||||
These are sanity ceilings, not the prompt's budget: the real gate is
|
||||
`BOR_HISTORY_MAX_TURNS` / `BOR_HISTORY_MAX_CHARS`, and a turn that does
|
||||
not fit the budget is dropped **whole** from later prompts (never cut
|
||||
@@ -1303,7 +1346,8 @@ The model never retypes quoted text, so a displayed quote cannot drift.
|
||||
model's, never a silently-substituted hybrid. Buttons appear when the turn
|
||||
settles, never mid-stream. The share link's copy path delegates to the same
|
||||
`copyTextWithFallback`, so the http-origin inline select-to-copy degradation
|
||||
exists once (the owner-locked share strings and behavior are unchanged).
|
||||
exists once (the owner-locked share strings are unchanged; phase 189 added
|
||||
the save-before-the-token upsert in front of the same success paths).
|
||||
- **Parity, and what stays untouched.** The live turn, a reload and an
|
||||
anonymous shared page call the same three globals in the same order, so one
|
||||
answer renders one way. Only the two answer renderers (plus the shared
|
||||
|
||||
Reference in New Issue
Block a user