diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 4b47a87..9f37723 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -97,11 +97,108 @@ "dependencies": [], "createdAt": "2026-04-25T10:00:00Z" } + , + { + "id": 9, + "title": "feat(kb/ask): rejoin in-flight SSE stream after browser reload", + "description": "When the user F5s or closes+reopens the tab while Shira is thinking, the asyncio task in shira-hermes keeps running (tokens burn) but the EventSource is gone — there's no way to reattach. After reload, the v0.1.9 sessionStorage replay only shows the LAST completed answer, not the live in-flight stream. Build a rejoinable SSE channel: POST /kb/ask/stream allocates a request_id and starts the runner; the SSE response writes events into an in-memory store keyed by request_id AND streams them. New GET /kb/ask/stream/{request_id} reconnects: replays accumulated events first, then continues with new ones as they arrive. Garbage-collect after 5 minutes past the answer event.", + "status": "pending", + "priority": "normal", + "details": "Architecture: in-process LRU dict {request_id -> {events:list, queue:asyncio.Queue, done:bool, last_used:ts}}. POST returns Set-Cookie or response header X-Request-Id; client stores it in _activeAsk.requestId + sessionStorage. afterRender: if _activeAsk has requestId and no live es, open EventSource to /kb/ask/stream/{requestId}. Server: re-emit all stored events on reconnect, then drain queue. Two-process safety: this works only because shira-hermes runs as a single FastAPI process — if we ever scale horizontally we'll need Redis pub/sub or sticky sessions. Note that for now it is single-process.", + "testStrategy": "1) Ask Shira a long question. 2) F5 after 10s. 3) After reload, KB tab should resume showing live events including those that arrived during the reload window. 4) Answer event arrives — refresh again still shows it (sessionStorage path, unchanged from v0.1.9). 5) After 5 minutes, GET /kb/ask/stream/{old_request_id} returns 410 Gone.", + "subtasks": [], + "dependencies": [], + "createdAt": "2026-04-25T13:30:00Z" + }, + { + "id": 10, + "title": "ops: investigate n8n release-asset auto-attach failures", + "description": "The n8n workflow `Git: Gitea Push - Auto-Release + Mattermost` (id IcF30v1Pw3wbucbu, per ~/CLAUDE.md) is supposed to attach the built .zip as a release asset within ~12s of a tag push. Across every KnowledgeBase release in this period (v0.1.8 through v0.2.1) the workflow did NOT attach the zip — every release was published with zero assets and I had to upload manually via curl + the Gitea API. Investigate the workflow's recent execution history, find the failing step, and either fix the n8n workflow or document a better fallback.", + "status": "pending", + "priority": "low", + "details": "Test pattern: after `mcp__gitea-dev__create_release`, poll GET /api/v1/repos/.../releases/{id}/assets every 10s for ~60s. Currently always returns 0. The 'Check Existing Release → Release Exists?' branch works (skips creating a duplicate release). The asset-upload branch is what's silently failing. Possible causes: (a) the n8n workflow doesn't have the zip artifact (zip is built ad-hoc with `bash build.sh` from the workspace, not in CI), (b) bad credential to Gitea API, (c) the release-creation event doesn't trigger that node. Note this workflow is for OTHER extensions too — if it's broken for all of them, fixing helps the whole release pipeline.", + "testStrategy": "After fix: push v0.X.Y tag to KnowledgeBase via mcp__gitea-dev__create_release; verify the zip appears at GET /releases/{id}/assets within 30s without manual upload.", + "subtasks": [], + "dependencies": [], + "createdAt": "2026-04-25T13:30:00Z" + }, + { + "id": 11, + "title": "feat(kb/ask): show text-source citations in the ask mode source picker", + "description": "/kb/ask currently filters sources_used through _filter_pdf_sources (kb_public.py) before returning, dropping any source whose original_path doesn't end with .pdf. The Wikisource law (source 5) is therefore invisible to the user even when Shira explicitly cites a section from it. With the v0.1.11 'section in context' preview that already exists for search mode, ask mode should be able to show the same. Drop the filter, and when the user clicks a non-PDF source pill, route to the same _renderSectionContext flow that search uses (factor it into a shared helper).", + "status": "pending", + "priority": "normal", + "details": "Server (shira-hermes): drop _filter_pdf_sources in both /kb/ask and /kb/ask/stream answer-event payload. Either return all sources, or re-introduce a different filter that keeps text sources but strips ones with no source_id. Client: in renderAskAnswer the source-pill click handler currently always rebuilds the iframe; route through showSearchPreview when the source is text-only. Probably easiest to extract _renderSectionContext / _renderSectionContextPlaceholder into a shared helper that both modes call. Need to add chunk_index to the sources returned from /kb/ask too — currently only on /kb/search hits.", + "testStrategy": "Ask 'מהי תקנה 36' — Shira's answer cites both תקנה 37 (PDF) and ס׳ 36 of the law (text). Source picker shows both. Click PDF pill → iframe at the cited page. Click law pill → section in context (matched section + 2 neighbors + Wikisource button) — same look as search mode.", + "subtasks": [], + "dependencies": [], + "createdAt": "2026-04-25T13:30:00Z" + }, + { + "id": 12, + "title": "Phase 1 — Multi-topic KB foundation (DB schema, topic-aware endpoints, topic selector)", + "description": "Generalize the KB from being insurance-only to supporting multiple legal domains within one CRM (e.g. ביטוח לאומי, דיני עבודה, דין פלילי). A 'topic' is the firm-level grouping; each kb_source belongs to exactly one topic. Search/ask are scoped to a single topic at a time, chosen from a topic dropdown the user controls. The system_prompt for /kb/ask becomes per-topic so Shira's domain context is correct (today the prompt hardcodes 'Israeli National Insurance'). Backwards-compatible migration: all 6 existing sources move to topic_id=1 named 'ביטוח לאומי' with the existing system-prompt addendum, and the UI defaults to that topic so nothing visible changes for existing users until they choose another topic.", + "status": "pending", + "priority": "high", + "details": "DB migration (shira-hermes side, against insurance_kb DB):\n- New table kb_topic: id PK, slug TEXT UNIQUE, name TEXT NOT NULL, description TEXT, system_prompt_addendum TEXT, is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now().\n- Seed: INSERT (id=1, slug='national-insurance', name='ביטוח לאומי', system_prompt_addendum=, is_active=true). RENAME the DB itself? Probably leave as 'insurance_kb' — costly to rename, no real value. Document in memory.\n- ALTER TABLE kb_source ADD COLUMN topic_id INTEGER REFERENCES kb_topic(id). Backfill: UPDATE kb_source SET topic_id = 1. Then ALTER COLUMN SET NOT NULL.\n\nshira-hermes endpoints:\n- New GET /kb/topics — public, returns active topics with id, slug, name, description (no prompt).\n- /kb/search: accepts optional topic_id (single int). When present, the SQL adds AND s.topic_id = $N. When absent (back-compat), still returns all (might want to deprecate this and require topic_id later).\n- /kb/sources, /kb/source/{id}/chunks, /kb/source/{id}/pdf: same — optional topic filter on listings, no change to single-source endpoints.\n- /kb/ask + /kb/ask/stream: accept topic_id param; load topic.system_prompt_addendum and inject into system_prompt instead of the hardcoded insurance text in _build_ask_runner_context. Pin the legal_kb tool to topic_id too (so search_insurance_kb queries are constrained — RENAME the tool to search_legal_kb and pass topic_id at registration time).\n- _expand_query in kb_search.py: parametrize the prompt by topic name (currently hardcoded 'הביטוח הלאומי בישראל'). Pass topic.name in.\n\nKnowledgeBase extension (EspoCRM):\n- New route+action GET /KnowledgeBase/action/topics → proxies /kb/topics.\n- Service.search/ask gain topic_id, controller passes through.\n- Template gets a topic