8.1 KiB
8.1 KiB
DECISIONS.md — ADR log
Decisions inside the set architecture. D-NNN, never renumbered.
- D-001 — SDD+BDD+TDD adopted (2026-07-13). SPEC-system port:
numbered requirements in
specs/, coverage classes feature/test/manual,tools/trace.pyenforcement inmake check. Process is binding — see SPEC-000. - D-002 — BDD runs at the responder seam, not against live
services.
FakeModelResponderscripts model output; Discord-event behavior is unit-tested with mocked discord.py objects. Rationale: Discord ToS forbids test-account automation and LLM output is nondeterministic — a live-BDD lane would be flaky by construction. - D-003 — Packaging = uv + pyproject only (2026-07-13). setup.py,
requirements.txt and pytest.ini removed; single source of truth,
locked via uv.lock. Python
>=3.11floor keeps the kitchen host (py3.11) deployable until FDB-016 lands. - D-004 — openai SDK pinned
<2(1.109.x). The v2 SDK migration happens together with the FDB-005 envelope rewrite (structured outputs / Responses API) — one breaking change, one test cycle, instead of two. - D-005 —
--strict-markersstays on; requirement tags from.featurefiles are registered as pytest markers dynamically intests/conftest.py. - D-006 — FDB-005 stays on chat.completions + structured outputs; the Responses API migration is deferred to FDB-007, where history handling gets redesigned anyway — one conversation-state reshape instead of two.
- D-007 — openai SDK bumped to 2.x together with the envelope
rewrite (supersedes the D-004 pin).
multilinedependency dropped — strict schema output made relaxed-JSON parsing dead code. - D-008 — Operator runtime flags (pause/images/tasks/quiet) are in-memory only; a restart resets to config defaults. Persistence arrives with the FDB-011 task store if staff practice demands it.
- D-009 —
translate()still keys offfix-modelalthough the repair path is gone; the whole translate-before-draw step dies in FDB-009 (gpt-image-2 is multilingual). Not worth a config rename for one phase. (Closed 2026-07-13: FDB-009 deleted translate(); the fix-model config key is now fully dead and can be dropped from live configs.) - D-010 — Persistence uses stdlib
sqlite3viaasyncio.to_thread, not aiosqlite: no new dependency, and a connection-per-operation with WAL is plenty at this message volume. - D-011 —
save_historyreplaces the channel's rows wholesale per message instead of appending: histories are capped athistory-limit(~200-350 rows) and trims must be reflected; correctness over micro-optimization. - D-012 — Budget spend is estimated from configured per-token/ per-image prices, not fetched from the billing API: deterministic, testable, no extra scopes. Dashboard hard limits (FDB-001) stay the outer safety net; this ledger is the inner, immediate one. User image quota counts at grant time (reservation), global image spend at generation time.
- D-013 —
!forgetmev1 purges history rows only; the single-string channel memory cannot be selectively cleaned. Full fact-level erasure ships with FDB-007 structured memory — stated in the user-facing confirmation, not hidden. (Superseded by D-014: erasure now covers facts, observations and episode traces.) - D-016 — The reply/ignore classifier fails open (BEH-03): a broken classifier must never mute the bot; the budget gate already bounds spend. Its verdict gates BEFORE the main call, the envelope's answer_needed still gates after — two independent nets.
- D-021 — Health monitoring (FDB-012, SPEC-012 OPS-18/19): a
separate
monitor_loop(own cadence, default 300 s) rather than folding checks into the 60 s task loop — monitoring is coarse and should not run every minute. Checks are edge-triggered (alert on the rising edge, re-arm on recovery) so a standing condition never spams; they reuse the existing rate-limited staff-alert path. Metrics are the cheap, high-signal ones (spend vs budget, free disk, task-queue depth); each is independently skippable when it has no data, so a deployment without a budget or store still runs the others. Opt-in (enable-monitoring) like every other operational rollout. - D-021 — Responses API behind
use-responses-api(FDB-028, ENV-22..24, resolves D-006): the responder path can use/v1/responses, which allows tools +reasoning_effort(the chat/completions 400 from ENV-21) and keeps one chain of thought across tool rounds. Stateless by choice:store=false+ encrypted reasoning items passed back — GDPR posture unchanged, no server-side conversation retention. Flag defaults off; rollback is a config toggle (hot-reload), not a deploy. Classifier / consolidation / task-gen stay on chat/completions (no tools, no reasoning need — not worth the churn). - D-020 — Web search via Exa (FDB-022, SPEC-015): a
web_searchtool alongside fetch_url/IGDB/codex/get_news, filling the "look it up on the open web" gap. Exa (not a raw search-engine scrape) because it returns clean title+url+text in one call — no SSRF surface of our own (we call one fixed API endpoint, not arbitrary hosts), and it pairs with fetch_url for the full article. Key is a host secret (exa-api-key, envEXA_API_KEYfallback), never repo-side; results sanitized like every other external-text tool; off by default (enable-web-search), metered per user. - D-019 — News memory + on-demand tool (SPEC-013 NEWS-07..12):
the news pipeline now carries item summaries (feed descriptions,
HTML-stripped) and persists every fetched item into a deduped
newstable (schema v6), pruned to a rolling window (news-keep). Both the kroa digest run and the ggg posting run write to it, so the store is a single searchable source across both models. Aget_newstool reads that store (topic/source-filtered, metered, sanitized) rather than re-fetching feeds live: the ambient{news}digest stays a small always-on snapshot, while the tool gives unbounded on-demand reach without a fresh network round-trip per call. The store is the samebot.db(WAL) the bot uses; the cron process opens it independently — concurrent reader/writer is what WAL is for. - D-018 — Codex Mechanicus search (FDB-019, SPEC-014): Luma's
lore is grounded in the priest's real archive at binaric.tech via a
codex_searchtool over the site's publicsearch-index.json, not a bot-side copy — the index stays a single source of truth, refreshed by the site's own publish rite, and the bot caches it in memory (TTL). It reuses SPEC-011'sguard_url+read_capped(fetch is SSRF-guarded and byte-bounded) and sanitizes every returned field: one's own web content is still untrusted by the time it reaches a prompt. Luma-only (enable-codex, off elsewhere) — the Adeptus Mechanicus archive has no place in Fjærkroa's café persona. - D-017 — All human-behavior knobs default to off/v3.0.0
semantics; behavior changes are config rollouts per deployment, not
code flips. The classifier's
factualflag is the only coupling (delay bypass) and defaults to false without a classifier. - D-015 — Deploys are push-based from the dev machine
(
git archive <tag> | ssh), not pull-based: no deploy keys or git state on the hosts, the artifact is exactly the tag tree, untracked live config survives in-place extraction. Trade-off: deploys need the dev machine; acceptable for a one-operator project. - D-014 — Structured memory (FDB-007): observations are the only consolidation feed (independent of history trimming); consolidation returns NEW facts only (no wholesale rewrite — the lossiness of the old memoize path is exactly what we're removing); self-authorship is enforced in code (fact subject must be an observation author), not just in the prompt; memory reads run on the loop (small indexed SQLite queries), writes off-loop. Legacy memory strings survive as episodes; the memory table stays as a read-only legacy fallback for deployments without memory-model.