Transcript analysis (359 insights / ~3.3k tool results) showed 31
"database is locked" errors from knowledge-memory tools and 50/359
runs (14%) exhausting the 6-iteration budget with no final answer.
The exhaustion nudge re-invoked chat with an EMPTY tool list, so a
model that still needed information could not call anything.
- utils: add retry_with_backoff_if — same exponential backoff + jitter
as retry_with_backoff, but retries only while the predicate classifies
the error as transient; deterministic failures short-circuit.
- insight_generator: wrap the four knowledge write tools
(store_fact/upsert_fact, store_entity/upsert_entity, update_fact,
supersede_fact) plus the store_fact photo-link upsert in
retry_with_backoff_if gated on a SQLITE_BUSY detector ("database is
locked"/"database is busy" massage in DbError.source). WAL +
busy_timeout absorb most contention; a parallel insight run or the
face-watch write storm still surfaces it, and a short backoff retry
clears it instead of surfacing a hard error to the model.
- insight_generator: exhaustion nudge now re-offers the full tool set
instead of vec![] and explicitly says the model may call more tools;
if it does take one, results are collected and a bounded tool-less
turn forces the actual answer so we never store an empty insight.
Same treatment for the read-only tool loop.
- models/knowledge_dao: derive Clone on InsertEntity, InsertEntityFact,
InsertEntityPhotoLink and FactPatch so retried FnMut closures can
re-issue the same write value on each attempt.
OpenRouter reserves credit against the requested max_tokens, falling
back to the model's full output window when omitted, so expensive
models failed with credit-limit errors even for short replies. Default
the cap to 16384; OPENROUTER_MAX_TOKENS overrides it and 0 disables it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sqlite_stat1 was last gathered by a migration while image_exif had 402
rows and an all-NULL content_hash, so the planner treated the hash index
as non-selective and scanned it for every row in the back-ref refresh's
correlated subqueries. The face_detections pass took ~20s per watcher
tick while holding the write lock, so chat/persona-chat persistence blew
past busy_timeout and returned 500s. With fresh stats the three
refreshes drop from ~22s to ~0.1s.
- library_maintenance: add refresh_planner_stats (plain ANALYZE;
PRAGMA optimize doesn't catch this on SQLite 3.45), run on full-scan
ticks ahead of reconcile / back-ref refresh.
- utils: retry backoff used 1_u32.pow(..), a flat ~100ms; make it the
documented exponential 100/200/400ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cameron
merged commit 0aeeb2d1f4 into master2026-09-24 15:59:59 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Transcript analysis (359 insights / ~3.3k tool results) showed 31 "database is locked" errors from knowledge-memory tools and 50/359 runs (14%) exhausting the 6-iteration budget with no final answer. The exhaustion nudge re-invoked chat with an EMPTY tool list, so a model that still needed information could not call anything. - utils: add retry_with_backoff_if — same exponential backoff + jitter as retry_with_backoff, but retries only while the predicate classifies the error as transient; deterministic failures short-circuit. - insight_generator: wrap the four knowledge write tools (store_fact/upsert_fact, store_entity/upsert_entity, update_fact, supersede_fact) plus the store_fact photo-link upsert in retry_with_backoff_if gated on a SQLITE_BUSY detector ("database is locked"/"database is busy" massage in DbError.source). WAL + busy_timeout absorb most contention; a parallel insight run or the face-watch write storm still surfaces it, and a short backoff retry clears it instead of surfacing a hard error to the model. - insight_generator: exhaustion nudge now re-offers the full tool set instead of vec![] and explicitly says the model may call more tools; if it does take one, results are collected and a bounded tool-less turn forces the actual answer so we never store an empty insight. Same treatment for the read-only tool loop. - models/knowledge_dao: derive Clone on InsertEntity, InsertEntityFact, InsertEntityPhotoLink and FactPatch so retried FnMut closures can re-issue the same write value on each attempt.