fix(ai): P1 batch — N+1 queries, vault-locked, debug hardening, timeout

Four P1 fixes from the AI Workbench audit:

#3 N+1 junction queries → batch lookups:
  - TagLinkOps gains getTagIdsForMany(entityIds) — single
    where(field).anyOf(ids).toArray() instead of N calls.
  - filterBySceneScopeBatch() uses a pre-fetched Map<id, tagId[]>.
  - All 4 module queries (notes, todo, contacts, calendar) migrated.
  - 500 notes now = 2 Dexie queries (records + junctions) instead of 501.

#4 Vault-locked detection in readLocalNote:
  - Catches VaultLockedError from decryptRecords.
  - Throws descriptive "Vault ist gesperrt" instead of returning null.
  - Tools surface it as a clear error to the planner ("bitte Vault
    entsperren") instead of "Notiz nicht gefunden".

#5 Debug log hardening:
  - Resolved-input content truncated to 500 chars before storage.
  - Time-based purge: entries older than 7 days auto-deleted.
  - Reduces privacy exposure if device is stolen/profile synced.

#6 Timeout 90s → 180s:
  - 5 LLM calls on slow models (Ollama/GPU) regularly hit 90s.
  - 180s gives comfortable headroom for the reasoning loop.

Audit doc updated with status markers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-16 16:25:17 +02:00
parent 93358ed002
commit a480393bfd
9 changed files with 131 additions and 22 deletions

View file

@ -42,6 +42,13 @@ export interface TagLinkOpsConfig {
export interface TagLinkOps {
/** Get all tag IDs linked to an entity */
getTagIds(entityId: string): Promise<string[]>;
/**
* Batch variant: fetch tag IDs for many entities in ONE Dexie query.
* Returns a Map<entityId, tagId[]>. Entities with no tags are absent
* from the map (not present with empty array). Use this in list views
* and scope filters to avoid N+1 queries.
*/
getTagIdsForMany(entityIds: string[]): Promise<Map<string, string[]>>;
/** Add a tag to an entity (no-op if already linked) */
addTag(entityId: string, tagId: string): Promise<void>;
/** Remove a tag from an entity (soft-delete) */
@ -66,6 +73,20 @@ export function createTagLinkOps(config: TagLinkOpsConfig): TagLinkOps {
return links.map((l) => l.tagId);
},
async getTagIdsForMany(entityIds: string[]): Promise<Map<string, string[]>> {
if (entityIds.length === 0) return new Map();
const all = await config.table().where(entityIdField).anyOf(entityIds).toArray();
const active = all.filter((r) => !r.deletedAt);
const result = new Map<string, string[]>();
for (const link of active) {
const eid = link[entityIdField] as string;
const arr = result.get(eid);
if (arr) arr.push(link.tagId);
else result.set(eid, [link.tagId]);
}
return result;
},
async addTag(entityId: string, tagId: string): Promise<void> {
const existing = await getActive(entityId);
if (existing.some((l) => l.tagId === tagId)) return;