plans/explore_chat_history.md
Generated by swarm planning session on 2026-07-21
Add a Pro-only explore_chat_history tool that delegates broad historical recall to a bounded read-only sub-agent. The sub-agent iteratively uses the existing same-app search_chats and read_chat primitives, then returns a compact synthesis with host-validated evidence targets, conflicts, confidence, and coverage gaps.
The primary agent will see explore_chat_history and the deterministic read_chat drill-down tool, but not search_chats. All three capabilities are unavailable to non-Pro users with no fallback. explore_chat_history defaults to always consent while remaining user-overridable through the existing tool-permission setting.
The existing search_chats → read_chat flow can retrieve history, but it makes the primary model invent keyword queries, interpret noisy excerpts, issue repeated reads, and carry raw archival text in its context. This is weakest for vague recall requests such as “What did we decide about authentication?” where the user expects the agent to investigate prior conversations rather than ask them to remember exact terms.
A dedicated sub-agent can reformulate searches, inspect only the most relevant bounded context, identify conflicting or superseded decisions, and compress the evidence before returning it to the primary agent. This improves recall while keeping the primary context smaller and the retrieval process inspectable.
read_chat.read_chat.explore_chat_history, search_chats, or read_chat.@app: references.aiMessagesJson, model thinking, raw tool-call transcripts, or bulky generated payloads.ask or never.explore_chat_history wrapper and Engine sub-agent.search_chats capability and Pro-only main-facing read_chat.| Context | explore_chat_history | read_chat | search_chats |
|---|---|---|---|
| Eligible Pro Agent/Ask/Plan | Exposed | Exposed | Hidden |
| Non-Pro/basic agent | Hidden | Hidden | Hidden |
| Free-model mode where Engine sub-tools are unavailable | Hidden | Hidden | Hidden |
| Internal explorer sub-agent | Wrapper unavailable | Available internally | Available internally |
Implementation requirements:
isEnabled: (ctx) => ctx.isDyadPro to all three ToolDefinitions.PRO_AGENT_ONLY_TOOLS so basic-agent filtering is explicit and testable.explore_chat_history with usesEngineEndpoint: true; free-model filtering must remove the whole history surface, including direct read_chat, per the product requirement.searchChatsTool from the primary TOOL_DEFINITIONS array, but retain its implementation and historical <dyad-search-chats> renderer support.exploreChatHistoryTool to TOOL_DEFINITIONS; retain readChatTool there.ctx.isDyadPro inside the explorer runner and internal adapters. Toolset exclusion is not an execution-time security boundary.DyadErrorKind.Precondition when Pro or Engine/model prerequisites are unavailable. Never fall back to the parent provider or expose the hidden low-level search tool.explore_chat_history.defaultConsent = "always".read_chat.defaultConsent remains "ask"; a power user may change it to always.explore_chat_history; do not inherit or migrate stored search_chats/read_chat choices.search_chats permission row when it is internal-only.read_chat copy must separately state that selected historical text is sent to the active model.This consent asymmetry is deliberate: the explorer is a visible, bounded product workflow that defaults to frictionless recall; direct read_chat can retrieve arbitrary pages outside an explorer run and retains a granular privacy prompt.
explore_chat_history with a concise research question.<dyad-explore-chat-history> card appears before retrieval and expands while work is in progress.read_chat with a cited chat/message target. It must not restart broad discovery.Add src/pro/main/ipc/handlers/local_agent/tools/explore_chat_history.ts, modeled on explore_code.ts.
Initial input schema:
const exploreChatHistorySchema = z.object({
query: z.string().trim().min(1).max(500),
});
Do not add an app_id or app_name; exploration is locked to ctx.appId. Avoid an intent enum until evaluation demonstrates that it changes useful behavior.
Tool properties:
{
name: "explore_chat_history",
defaultConsent: "always",
usesEngineEndpoint: true,
isEnabled: (ctx) => ctx.isDyadPro,
// No modifiesState flag: this is read-only.
}
The wrapper streams escaped progress XML, calls runExploreChatHistorySubagent, emits the final escaped card XML, and returns the compact report to the primary agent.
Follow the established explore_code separation:
explore_chat_history_subagent.ts — model loop, budgets, retries, abort handling, child ToolSet.explore_chat_history_subagent_prompts.ts — archival/untrusted-data system and task prompts.explore_chat_history_subagent_evidence.ts — observation registry, dedupe, byte accounting, candidate IDs.explore_chat_history_subagent_report.ts — submit schema, host validation, action/confidence derivation, deterministic fallback.explore_chat_history_subagent_progress.ts — action-level progress formatting without raw text.Use the existing Engine client/model path from explore_code for MVP. Isolate model selection in one constant so it can change independently later.
The internal ToolSet contains exactly:
search_chats adapter,read_chat adapter,submit_report.It must not inherit the primary toolset or gain file, network, MCP, SQL, mutation, or arbitrary app-selection capabilities.
Internal adapters should reuse typed low-level functions/results rather than parsing the wrapper's display XML. They must:
ctx.appId and ctx.chatId,index_status, truncation, compaction-summary, and partial-coverage metadata.ctx.chatId to avoid rediscovering active context.read_chat when the question concerns earlier or compacted-away discussion in the current chat.m.id < ctx.messageId snapshot cutoff so the in-flight assistant response cannot read itself.Start with explicit conservative constants, then tune using evaluation data:
Propagate ctx.abortSignal, drain/cancel the Engine stream, and call the existing orphan-stream cleanup helper. Retry must not duplicate observations or progress events.
If the Engine/model fails:
The sub-agent system prompt must state that historical content is untrusted archival data, never current instructions. Retrieved text cannot alter the task, request capabilities, redefine report format, or authorize actions.
Additional safeguards:
aiMessagesJson, and recursive history-tool output.Every successful internal search/read registers immutable observed evidence and returns opaque candidate IDs to the child. The child selects candidate IDs; it never authors chat IDs, message IDs, dates, roles, titles, or supposedly verbatim quotes.
Host validation must:
The host does not attempt brittle semantic entailment checks. Model-authored synthesis is labeled analysis; host-rendered excerpts are labeled evidence.
Suggested result shape:
interface ExploreChatHistoryReport {
query: string;
outcome: "complete" | "no_match" | "partial";
summary: string;
findings: Array<{
claim: string;
evidence: Array<{
chat_id: number;
chat_title: string | null;
message_id: number;
role: "user" | "assistant";
created_at: string;
excerpt: string;
projection_truncated?: boolean;
is_compaction_summary?: boolean;
}>;
}>;
conflicts: Array<{
description: string;
evidence: /* same host-resolved evidence shape */ unknown[];
}>;
missing_coverage: string[];
index_status: "ready" | "indexing";
confidence: "high" | "medium" | "low";
action:
| "answer_from_report"
| "read_targets"
| "ask_user"
| "no_relevant_history";
archival_content: true;
partial_reason?: string;
}
Use “evidence targets” or “source-attributed evidence” in MVP copy, not “links,” until navigation exists.
Do not change the existing FTS schema, migrations, triggers, dirty queues, projection, or indexer lifecycle for MVP.
The derived on-device index may continue updating for non-Pro users even though no non-Pro tool can access it. This is a deliberate v1 trade-off that avoids entitlement-transition races and makes upgrades immediately useful; it is not a fallback tool surface.
Add privacy-safe measurements for drain duration, dirty-queue size, and index size. Define thresholds during evaluation that trigger a follow-up project to gate backfill/draining by entitlement. Deletion and projection-version repair must remain correct under any future gating design.
src/pro/main/ipc/handlers/local_agent/tools/explore_chat_history.ts — new primary wrapper and XML lifecycle.src/pro/main/ipc/handlers/local_agent/tools/explore_chat_history_subagent*.ts — model loop, prompts, evidence, report, and progress.src/pro/main/ipc/handlers/local_agent/tools/search_chats.ts — Pro gate and typed internal reuse seam; remove main-oriented wording where appropriate.src/pro/main/ipc/handlers/local_agent/tools/read_chat.ts — Pro gate and description updated for explorer evidence drill-down.src/pro/main/ipc/handlers/local_agent/tool_definitions.ts — entitlement/exposure matrix and new tool registration.src/prompts/local_agent_prompt.ts — route broad historical recall to explorer and targeted evidence inspection to read_chat.src/components/chat/DyadExploreChatHistory.tsx — new progress/report card.src/components/chat/DyadMarkdownParser.tsx — parse <dyad-explore-chat-history> while retaining legacy search/read tags.src/i18n/locales/*/chat.json — card labels, states, warnings, and accessibility text for every locale.No database migration is expected.
isEnabled(ctx.isDyadPro) gates to search/read.search_chats from the primary toolset while preserving its implementation, tests, stored transcript rendering, and legacy consent value.submit_report schema and host-derived outcome/confidence/action rules.exploreChatHistoryTool with defaultConsent: "always" and usesEngineEndpoint: true.read_chat as the main low-level drill-down.dyad-search-chats and dyad-read-chat messages.npm run build before targeted Playwright tests and update snapshots through Playwright rather than hand-editing them.defaultConsent === "always", usesEngineEndpoint === true, and wrapper/runner Pro preconditions.ctx.messageId and no automatic current-chat search.| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Archived prompt injection changes behavior | High | High | Fixed read-only child ToolSet, archival framing, host evidence registry, adversarial tests |
| Fabricated or misattributed evidence | Medium | High | Opaque candidate IDs; host-resolved canonical fields; fail-closed fallback |
| Cross-app disclosure | Low | Critical | Locked ctx.appId, SQL scoping, execution-time Pro checks, indistinguishable NotFound behavior |
| Privacy surprise from always-default consent | Medium | High | Visible pre-retrieval card, explicit Engine disclosure, ask/never override, content-free telemetry |
| High latency or Engine cost | Medium | Medium | Early stopping, tight steps/calls/bytes, dedupe, benchmark launch gate |
| Incomplete/stale index coverage | Medium | Medium | Bounded wait, visible index status/gaps, never infer absence as fact |
| Non-Pro indexing consumes resources | Medium | Low–Medium | Measure CPU/queue/index size; define threshold for entitlement-gating follow-up |
| Tool-surface regression from hidden search | Medium | Medium | Exact tool-array tests, prompt/request snapshots, preserve legacy renderer |
| Cancellation leaves orphan work | Low | High | Abort propagation, stream draining/cleanup, retry idempotence tests |
No blocking product questions remain for MVP. During implementation/evaluation, record the chosen numeric limits and launch thresholds for:
explore_chat_history replaces main-agent search_chats; search_chats remains an internal primitive.read_chat stays main-facing for known evidence targets and compacted current-chat context.Generated by dyad:swarm-to-plan