docs(6205): task_logs, task_claims, reply_shapes, design_starter_roles, db_maintenance_scheduler comments drop citations and incident stories

- db_maintenance_scheduler states the storage-stall hazard the startup
  timeout guards, without the dated outage.
- design_starter_roles states why a missing role produces literals, without
  the call-site count, the operator's call date or a project's name.
- The rest drop milestone/step labels and record citations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:31:14 -04:00
co-authored by Claude Opus 5.5
parent 32ebc2b1d4
commit cc5401b2d3
5 changed files with 43 additions and 49 deletions
+12 -15
View File
@@ -25,16 +25,14 @@ logger = logging.getLogger(__name__)
_JOB_ID = "db_maintenance_vacuum"
_DEFAULT_HOUR = 4
# HOW LONG A COLD START WILL WAIT FOR THIS ONE SETTING (#4181).
# HOW LONG A COLD START WILL WAIT FOR THIS ONE SETTING.
#
# This read is the FIRST database call in the app's `before_serving` hook, and
# a lifespan hook that does not return is a worker that never serves. On
# 2026-09-19 a host storage stall made one Postgres checkpoint of 14 buffers
# take 281 seconds against a 1.3-second baseline; the app restarted into the
# tail of it, this query hung with no deadline, Hypercorn killed the worker at
# its 60-second lifespan timeout, and nothing retries a failed lifespan. A
# five-minute disk hiccup became a three-hour outage that only a human restart
# could clear.
# a lifespan hook that does not return is a worker that never serves. If the
# app starts while the database host is stalled on storage, a query with no
# deadline hangs, Hypercorn kills the worker at its 60-second lifespan timeout,
# and nothing retries a failed lifespan — a few minutes of disk trouble becomes
# an outage that only a human restart clears.
#
# Three seconds because the honest requirement is "don't hold up the boot",
# not "get the right hour". The value is one small indexed row on a local
@@ -47,12 +45,11 @@ _STARTUP_READ_TIMEOUT = 3.0
async def get_maintenance_hour() -> int:
"""The configured run-hour (UTC, 0–23), clamped; default 04:00.
BOUNDED, because the caller is a lifespan hook (rule 156). The fallback is
not new behaviour invented for the timeout — this function already answers
`_DEFAULT_HOUR` for a value it cannot parse, and a database that will not
answer in three seconds is the same class of "no usable value here". What
changes is that the failure is now a logged line and a default hour rather
than the application failing to start.
BOUNDED, because the caller is a lifespan hook. The fallback is the same
one this function gives a value it cannot parse: a database that will not
answer in three seconds is the same class of "no usable value here". The
failure is a logged line and a default hour rather than the application
failing to start.
Degrading to the default is the right trade in both directions: the cost of
being wrong is that a VACUUM runs at 04:00 instead of the configured hour,
@@ -66,7 +63,7 @@ async def get_maintenance_hour() -> int:
)
except (TimeoutError, asyncio.TimeoutError):
# WARNING, not debug: this is never normal, and it is the breadcrumb
# that would have named #4181 in seconds instead of requiring
# that names a stalled database in seconds instead of requiring
# Postgres's own log to be read.
logger.warning(
"db maintenance: reading db_maintenance_hour exceeded %.1fs; "
+13 -14
View File
@@ -3,18 +3,17 @@
WHY THIS EXISTS
---------------
A literal gets written into a stylesheet when there is no role to reach for.
That is the mechanism, and this codebase produced a clean demonstration of it:
the house style had no "text on a filled colour" role, so 76 call sites wrote
a pure-white literal — not out of defiance, but because nothing existed to write
instead (#2275). The correction was not a better ban list. It was declaring the
missing role.
A house style with no "text on a filled colour" role gets a pure-white literal
at every call site that needs one — not out of defiance, but because nothing
exists to write instead. The correction is not a better ban list. It is
declaring the missing role.
So the useful moment is CREATION. A system whose roles are named on day one
never presents the occasion for a literal, and never needs a list of values it
forbids.
WHAT SHIPS AND WHAT DOES NOT (rule #115)
----------------------------------------
WHAT SHIPS AND WHAT DOES NOT
----------------------------
The ROLES ship: `surface-page`, `text-primary`, `action-destructive` are
generic CSS-design vocabulary, not one operator's kit. Every install that has a
page has a page background.
@@ -25,21 +24,21 @@ appears anywhere in this file, and none should ever be added to it — a default
palette would be this operator's palette wearing product clothes.
A valueless token is already legible downstream: `render_stylesheet` emits it as
a commented-out declaration in its group (#2299), and `stylesheet_for_system`
a commented-out declaration in its group, and `stylesheet_for_system`
reports it under `valueless`. So a blank role reads as "to be decided" rather
than as breakage, without anything new.
THE PREFIX IS THE INSTALL'S
---------------------------
`--fs-` is FabledSword's convention, not the product's. The prefix is a
parameter with a neutral default; a caller that has a house convention passes
it. Baking `--fs-` in would put one family's naming into every install.
A house prefix such as `--fs-` is one family's convention, not the
product's. The prefix is a parameter with a neutral default; a caller that has
a house convention passes it. Baking one in would put one family's naming into
every install.
FLAT, NOT PRESET
----------------
One list, every group individually skippable, all on by default (operator's
call, 2026-08-03). Presets keyed to app shape — web / CLI / docs — were
considered and rejected: they would require the product to hold opinions about
One list, every group individually skippable, all on by default. Presets
keyed to app shape — web / CLI / docs — are not offered: they would require the product to hold opinions about
app categories, and a wrong category is worse than a generic list someone
prunes once.
"""
+6 -7
View File
@@ -1,4 +1,4 @@
"""The default shapes of a reply, and the moment each one rides (milestone 500).
"""The default shapes of a reply, and the moment each one rides.
WHY THIS EXISTS
@@ -33,7 +33,7 @@ the operator asked for.
HOW TO WRITE ONE
Domain-neutral (rule 115): the work may be software, configuration,
Domain-neutral: the work may be software, configuration,
infrastructure or anything else a person drives through an agent. "Evidence"
is what the reader could open to check; "verified" is whatever checking means
for that work. The test beside this module refuses software-only vocabulary.
@@ -80,12 +80,11 @@ class ReplyShape:
CORE_KEY = "core"
# ~550 tokens at four characters a token. The core is paid for in every
# session, so this is the ceiling the test holds it to — not a target. It sits
# at ~2,050 with the judgement line (milestone 500 step 2): close to full, so a
# sentence added now has to replace one.
# session, so this is the ceiling the test holds it to — not a target. The
# core sits close to it, so a sentence added has to replace one.
CORE_BUDGET_CHARS = 2200
# A slice is paid for only at its moment, and once per session; it can afford
# more than the core and still has to stay a slice, not the old skill again.
# more than the core and still has to stay a slice, not a whole skill.
# The asks slice is the largest (~1,530) because it carries who decides what in
# full: handing a question back is what an ask is tempted to do.
SLICE_BUDGET_CHARS = 1600
@@ -289,7 +288,7 @@ async def record_delivery(user_id: int | None, forms: dict[str, str], *, via: st
logger.debug("reply shape delivery not recorded", exc_info=True)
# ── The operator's view (milestone 500 step 5) ──────────────────────────
# ── The operator's view ─────────────────────────────────────────────────
# The window the Settings view counts deliveries over — the same 30 days the
# moments panel beside it reads, so the two count the same stretch of work.
+5 -5
View File
@@ -1,4 +1,4 @@
"""A task's claim — which session is working it right now (milestone 381 step 2).
"""A task's claim — which session is working it right now.
`status` says where THE WORK stands and is durable: `in_progress` means
committed to, not finished. It cannot also say "someone is on this now",
@@ -38,7 +38,7 @@ if TYPE_CHECKING:
# session gone overnight reads as gone. The cost either way is stated rather
# than hidden: readers show the age beside `live`, never the boolean alone.
#
# NOT A SETTING, deliberately (#4385 weighed it). Settings are per user, and a
# NOT A SETTING, deliberately. Settings are per user, and a
# claim is read by everyone who can see the task: a per-user lease would make
# one shared task read as live to one collaborator and dead to another, and
# "is anyone on this?" only means something if every reader gets the same
@@ -131,9 +131,9 @@ async def bind_session(user_id: int, task_id: int, session_id: str) -> dict | No
return claim_state(note, now)
# --- The readers (milestone 381 step 3) ---------------------------------------
# --- The readers --------------------------------------------------------------
#
# A claim nobody reads is the state before this milestone. SessionStart is the
# A claim nobody reads is worth nothing. SessionStart is the
# reader that pays rent to the session that set it, and it branches on the
# `source` the host sends, because the same claim means different things
# depending on what just happened to the context:
@@ -290,7 +290,7 @@ async def claims_for_session_start(
async def release_session(user_id: int, session_id: str) -> int:
"""Release every claim the caller holds under `session_id` (milestone 381 step 4).
"""Release every claim the caller holds under `session_id`.
Called by the plugin's SessionEnd hook: the session's context is about to
stop existing, so the attention its claims record is ending too. A TIDY-UP,
+7 -8
View File
@@ -16,11 +16,10 @@ _UNSET = object()
async def _refresh_task_document(session, task_id: int) -> None:
"""Re-embed the task whose work log just changed (#4251).
"""Re-embed the task whose work log just changed.
A task's embedded document carries its logs, so a log written and not
indexed is #4241's half-surface wearing different clothes: the entry is
readable and unfindable, and the next session rebuilds what this one ruled
indexed is half a surface: the entry is readable and unfindable, and the next session rebuilds what this one ruled
out. Create, edit and delete all go through here — an edited log that keeps
matching its old wording is the stale-vector problem `upsert_note_embedding`
already refuses to leave behind for a body.
@@ -51,9 +50,9 @@ async def create_log(
content: str,
duration_minutes: int | None = None,
) -> TaskLog:
# Whoever may WRITE the task may log on it (rule #78) — a collaborator on a
# shared project included. This used to be a bare owner filter, which
# refused exactly the person a shared task exists for.
# Whoever may WRITE the task may log on it — a collaborator on a shared
# project included. An owner-only filter would refuse exactly the person a
# shared task exists for.
if not await can_write_note(user_id, task_id):
raise ValueError(f"Task {task_id} not found")
async with async_session() as session:
@@ -61,7 +60,7 @@ async def create_log(
task = result.scalars().first()
if task is None:
raise ValueError(f"Task {task_id} not found")
# Logging IS working the task, so it stamps the claim (milestone 381) —
# Logging IS working the task, so it stamps the claim —
# unless the work is over: a retrospective note on a closed task is not
# a session picking it up.
if task.status not in (TaskStatus.done.value, TaskStatus.cancelled.value):
@@ -101,7 +100,7 @@ async def logs_for_task(
exactly what a collaborator needs — `list_logs`' `TaskLog.user_id ==
user_id` returns them an empty list, which reads as "no work has been
done" rather than "not yours". The permission question is asked of the
note through `can_read_note` (rule #78), so this is not an unscoped
note through `can_read_note`, so this is not an unscoped
read with the check left to the caller.
2. Newest first, because the question a reader asks of a work log is
"where does this actually stand", and the answer is the last entry.