Search: Ask mode — answers from your own data with citations (with the AI plugin) #60

Closed
opened 2026-09-24 15:53:01 +00:00 by kayg · 21 comments
Owner

Owner decision S4 (DESIGN §32): an Ask mode (? prefix or an Ask tab in the search window). A question ("when did I last see Ana?", "what did I spend on the Japan trip?") is answered by the user's own agent (their Claude/Codex subscription in their agent container, via the AI plugin) using the hybrid index as retrieval; the answer streams in with clickable citations (log entries, notes, files, events) that open in place. The agent only sees what the user can see (data-scope token). Designed now, built with the AI plugin; do not start before the AI plugin exists.

Context for the owning job

  • Repo: kayg/calternal (~/Developer/calternal). Read CLAUDE.md, CONTEXT.md and docs/DESIGN.md (§15, §18 budgets, §31, §32) first. Prior art: calternal.js docs/search.md (read-only at /home/kayg/Developer/calternal.js) — its palette UX, > command mode, ranking weights, a11y combobox pattern and recents carry over.
  • Existing code: crates/calternal-search (Tantivy index, watcher + reconcile, providers via calternal-plugin fan-out), the ⌘K registry in apps/web.
  • Owner rules: search must be ultra fast (⌘K results < 50 ms p95 at 100k items; first keystroke to first results < 16 ms for client providers); file over app (indexes are derived and rebuildable); performance first but never at the cost of finesse; calternal.js design system (Claude reviews screenshots; floating window over a dimmed + blurred background, spring motion, reduced-motion respected); never ship sample data; atomic commits (commit every 30–45 min); adversarial testing after API work.
  • Comment on this issue when you start, on findings, when blocked, and when finished. Never close it.
Owner decision S4 (DESIGN §32): an **Ask** mode (`?` prefix or an Ask tab in the search window). A question ("when did I last see Ana?", "what did I spend on the Japan trip?") is answered by the user's own agent (their Claude/Codex subscription in their agent container, via the AI plugin) using the hybrid index as retrieval; the answer streams in with **clickable citations** (log entries, notes, files, events) that open in place. The agent only sees what the user can see (data-scope token). Designed now, built with the AI plugin; do not start before the AI plugin exists. ## Context for the owning job - Repo: kayg/calternal (~/Developer/calternal). Read CLAUDE.md, CONTEXT.md and docs/DESIGN.md (§15, §18 budgets, §31, §32) first. Prior art: calternal.js `docs/search.md` (read-only at /home/kayg/Developer/calternal.js) — its palette UX, `>` command mode, ranking weights, a11y combobox pattern and recents carry over. - Existing code: `crates/calternal-search` (Tantivy index, watcher + reconcile, providers via calternal-plugin fan-out), the ⌘K registry in apps/web. - Owner rules: search must be **ultra fast** (⌘K results < 50 ms p95 at 100k items; first keystroke to first results < 16 ms for client providers); file over app (indexes are derived and rebuildable); performance first but never at the cost of finesse; calternal.js design system (Claude reviews screenshots; floating window over a dimmed + blurred background, spring motion, reduced-motion respected); never ship sample data; atomic commits (commit every 30–45 min); adversarial testing after API work. - Comment on this issue when you start, on findings, when blocked, and when finished. Never close it.
Author
Owner

Starting issue #60 on branch job/ask-mode at base 6e755d35da. I am tracing the existing turn, data-scope token, and hybrid search paths before adding Ask retrieval and a read-only turn scope.

Starting issue #60 on branch job/ask-mode at base 6e755d35da2aecf879ba77d96be6ac6d034d4d5d. I am tracing the existing turn, data-scope token, and hybrid search paths before adding Ask retrieval and a read-only turn scope.
Author
Owner

Finding: the existing /api/v1/search fan-out already passes an auth-derived SearchContext to each enabled provider, and IndexSearchProvider combines Tantivy and semantic results. Ask will retrieve through this route using its own short-lived read-only data token, then pass bounded citation context into the existing turn runner. The token will carry a new server-enforced read-only scope so the search call remains allowed while every write method is denied.

Finding: the existing `/api/v1/search` fan-out already passes an auth-derived `SearchContext` to each enabled provider, and `IndexSearchProvider` combines Tantivy and semantic results. Ask will retrieve through this route using its own short-lived read-only data token, then pass bounded citation context into the existing turn runner. The token will carry a new server-enforced read-only scope so the search call remains allowed while every write method is denied.
Author
Owner

Decision: the Ask request will require the provider explicitly. Both Claude and Codex credentials can be configured, so selecting one silently would make the same question run under an arbitrary provider. The offline retrieval fixture currently finds both expected citations at recall@32 (2/2, 100%) through the real Tantivy index; semantic retrieval is optional and is not loaded by this offline fixture.

Decision: the Ask request will require the provider explicitly. Both Claude and Codex credentials can be configured, so selecting one silently would make the same question run under an arbitrary provider. The offline retrieval fixture currently finds both expected citations at recall@32 (2/2, 100%) through the real Tantivy index; semantic retrieval is optional and is not loaded by this offline fixture.
Author
Owner

Finding: the hybrid Search provider returned path-based IDs for indexed Notes and files. Those IDs do not survive a rename. Ask now resolves indexed Note hits through note_items, log citations through the Note identity plus block ID, and file hits through the Files item ID; it emits stable /n, /d, /f, /t, or /p links. The offline fixture still has 2/2 expected stable citations at recall@32 (100%), and a matching Bob-only Note is absent from Alice's results.

Finding: the hybrid Search provider returned path-based IDs for indexed Notes and files. Those IDs do not survive a rename. Ask now resolves indexed Note hits through `note_items`, log citations through the Note identity plus block ID, and file hits through the Files item ID; it emits stable `/n`, `/d`, `/f`, `/t`, or `/p` links. The offline fixture still has 2/2 expected stable citations at recall@32 (100%), and a matching Bob-only Note is absent from Alice's results.
Author
Owner

Finding: Search reports Tasks file hits with a path-based index ID, while the /t/<task-id> route uses the stable Task UID from reminder_resources. Mapping these hits to Files item IDs would create links that do not resolve to Tasks. Ask now resolves the UID from the existing Notes projection and drops Task hits without a stable UID; a regression test covers the link.

Finding: Search reports Tasks file hits with a path-based index ID, while the `/t/<task-id>` route uses the stable Task UID from `reminder_resources`. Mapping these hits to Files item IDs would create links that do not resolve to Tasks. Ask now resolves the UID from the existing Notes projection and drops Task hits without a stable UID; a regression test covers the link.
Author
Owner

Finding: citation link validation rejected external https: URLs and // links but allowed a leading slash followed by backslashes. Browsers can normalize /\\host as a network-path URL. The Ask context now rejects backslashes so a provider result cannot turn a citation into an external link; a regression case covers this.

Finding: citation link validation rejected external `https:` URLs and `//` links but allowed a leading slash followed by backslashes. Browsers can normalize `/\\host` as a network-path URL. The Ask context now rejects backslashes so a provider result cannot turn a citation into an external link; a regression case covers this.
Author
Owner

Decision where DESIGN is silent: /api/v1/ai/ask requires an explicit provider so a turn has deterministic routing when both Agent providers are configured. Ask normalizes question words, requests 32 hybrid Search results, caps the streamed Search response at 2 MiB, and caps serialized context at 48 KiB; the question is capped at 12 KiB. These limits bound retrieval and Agent context cost while keeping the request under the 16 KiB body cap.

Decision where DESIGN is silent: `/api/v1/ai/ask` requires an explicit `provider` so a turn has deterministic routing when both Agent providers are configured. Ask normalizes question words, requests 32 hybrid Search results, caps the streamed Search response at 2 MiB, and caps serialized context at 48 KiB; the question is capped at 12 KiB. These limits bound retrieval and Agent context cost while keeping the request under the 16 KiB body cap.
Author
Owner

Finding: the initial WebSocket handshake is a GET, so checking only the HTTP method would let a READ_ONLY Ask token pass into collab and then send write messages. I am updating the shared auth authority extractor to reject protocol upgrades for READ_ONLY sessions as well as write methods, with an explicit WebSocket handshake regression test.

Finding: the initial WebSocket handshake is a GET, so checking only the HTTP method would let a READ_ONLY Ask token pass into collab and then send write messages. I am updating the shared auth authority extractor to reject protocol upgrades for READ_ONLY sessions as well as write methods, with an explicit WebSocket handshake regression test.
Author
Owner

Finding: cargo clippy --workspace --all-targets -- -D warnings rejected the context byte-budget check serialized.len() + 1 <= remaining under clippy::int_plus_one. Replacing it with the equivalent serialized.len() < remaining preserves the required newline-byte budget and clears the warning.

Finding: `cargo clippy --workspace --all-targets -- -D warnings` rejected the context byte-budget check `serialized.len() + 1 <= remaining` under `clippy::int_plus_one`. Replacing it with the equivalent `serialized.len() < remaining` preserves the required newline-byte budget and clears the warning.
Author
Owner

Adversarial probe finding outside Ask ownership: in attack2.py isolation, after the probe removes A's files_index row for AShared/unindexed.txt, B's shared-folder listing makes the row reappear synchronously. The probe expects that listing to serve the existing Index and the owner reconcile to restore the row asynchronously. This is in Files behavior, outside issue #60's owned crates; the run continued to check Ask input and read-only handling.

Adversarial probe finding outside Ask ownership: in `attack2.py` isolation, after the probe removes A's `files_index` row for `AShared/unindexed.txt`, B's shared-folder listing makes the row reappear synchronously. The probe expects that listing to serve the existing Index and the owner reconcile to restore the row asynchronously. This is in Files behavior, outside issue #60's owned crates; the run continued to check Ask input and read-only handling.
Author
Owner

Adversarial run finding outside Ask ownership: the Round 2 deletion section timed out on archive user Home (NO RESPONSE (b'timed out')), consistent with the known deletion deadlock tracked in #78 and the server test exclusion in this job. The probe continued to the AI/Ask section.

Adversarial run finding outside Ask ownership: the Round 2 deletion section timed out on `archive user Home` (`NO RESPONSE (b'timed out')`), consistent with the known deletion deadlock tracked in #78 and the server test exclusion in this job. The probe continued to the AI/Ask section.
Author
Owner

Finished issue #60 backend work.

Branch: job/ask-mode
Head SHA: d84cab438c785ade02a70362985ac622893c9db0

Gates (verbatim result lines):

cargo fmt --all -- --check: exit 0, no output
Finished `dev` profile [unoptimized + debuginfo] target(s) in 26.23s
Ask retrieval recall @32: 2/2 (100%)
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s
test result: ok. 18 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 1.88s
test result: ok. 13 passed; 0 failed; 2 ignored; 0 measured; 1 filtered out; finished in 0.30s
🚀 ../../contracts/openapi.json → src/generated.ts [576.7ms]
==== FINDINGS 0
server alive at end: True
==== ROUND 2 FINDINGS 4
 - list shared folder :: recipient listing wrote an unindexed child to the owner's Index synchronously
 - archive user Home :: NO RESPONSE (b'timed out')
 - purge user Home :: NO RESPONSE (b'timed out')
 - transfer user Home :: NO RESPONSE (b'timed out')
Removed 21594 files, 15.0GiB total

The Ask section had no findings. The other Round 2 findings are outside this job's owned files: Files synchronously restored an Index row during a shared-folder listing, and archive/purge/transfer Home operations timed out under the deletion deadlock tracked by #78. The test server remained alive.

Decision where DESIGN was silent: Ask requires an explicit provider. Limits are 12 KiB per question, 16 KiB for the request body, 2 MiB for the streamed Search response, 32 results, and 48 KiB of serialized context.

Finished issue #60 backend work. Branch: `job/ask-mode` Head SHA: `d84cab438c785ade02a70362985ac622893c9db0` Gates (verbatim result lines): ```text cargo fmt --all -- --check: exit 0, no output Finished `dev` profile [unoptimized + debuginfo] target(s) in 26.23s Ask retrieval recall @32: 2/2 (100%) test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s test result: ok. 18 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 1.88s test result: ok. 13 passed; 0 failed; 2 ignored; 0 measured; 1 filtered out; finished in 0.30s 🚀 ../../contracts/openapi.json → src/generated.ts [576.7ms] ==== FINDINGS 0 server alive at end: True ==== ROUND 2 FINDINGS 4 - list shared folder :: recipient listing wrote an unindexed child to the owner's Index synchronously - archive user Home :: NO RESPONSE (b'timed out') - purge user Home :: NO RESPONSE (b'timed out') - transfer user Home :: NO RESPONSE (b'timed out') Removed 21594 files, 15.0GiB total ``` The Ask section had no findings. The other Round 2 findings are outside this job's owned files: Files synchronously restored an Index row during a shared-folder listing, and archive/purge/transfer Home operations timed out under the deletion deadlock tracked by #78. The test server remained alive. Decision where DESIGN was silent: Ask requires an explicit `provider`. Limits are 12 KiB per question, 16 KiB for the request body, 2 MiB for the streamed Search response, 32 results, and 48 KiB of serialized context.
Author
Owner

Starting ask-merge on branch job/ask-mode. Starting head: d84cab438c. Merge base/current main: 1bbd40e1c7.

Starting ask-merge on branch job/ask-mode. Starting head: d84cab438c785ade02a70362985ac622893c9db0. Merge base/current main: 1bbd40e1c75d0059eaa84ea39e4abc763bd3b52c.
Author
Owner

Merge finding: both branches used AI migration 0003; main owns 0003_ai_turn_prompts.sql and Ask currently uses 0003_ask_turns.sql. I will keep main 0003 and register Ask as 0004. Auth already has migrations 0001–0007 here; 0007_read_only_agent_scope.sql is the next free auth migration after main 0001–0006.

Merge finding: both branches used AI migration 0003; main owns 0003_ai_turn_prompts.sql and Ask currently uses 0003_ask_turns.sql. I will keep main 0003 and register Ask as 0004. Auth already has migrations 0001–0007 here; 0007_read_only_agent_scope.sql is the next free auth migration after main 0001–0006.
Author
Owner

Resolution decision: Ask and Agent share one turn history and Turn view. I added an explicit kind field to history, persist the raw Ask question and source citations as durable turn events, and render valid local citations as links with a Sources list. Ask undo returns 409 with “Ask turns are read-only and cannot be undone”; the Turn view hides Undo for Ask. DESIGN §32 requires clickable Ask citations but does not specify their API/event or view shape.

Resolution decision: Ask and Agent share one turn history and Turn view. I added an explicit kind field to history, persist the raw Ask question and source citations as durable turn events, and render valid local citations as links with a Sources list. Ask undo returns 409 with “Ask turns are read-only and cannot be undone”; the Turn view hides Undo for Ask. DESIGN §32 requires clickable Ask citations but does not specify their API/event or view shape.
Author
Owner

Finding during merge hygiene: git diff --check --cached reported “crates/calternal-path/Cargo.toml:7: new blank line at EOF.” I am removing the trailing blank line from the merged new manifest, then will rerun the check.

Finding during merge hygiene: `git diff --check --cached` reported “crates/calternal-path/Cargo.toml:7: new blank line at EOF.” I am removing the trailing blank line from the merged new manifest, then will rerun the check.
Author
Owner

Gate finding: the first cargo test --workspace stopped at calternal-collab hostile_clients test unrepresentable_update_is_rejected_and_room_keeps_saving, failing assertion failed: eventually_loaded(&fixture, None).await at tests/hostile_clients.rs:268. This matches the intermittent collab test noted in the job prompt; I am rerunning the workspace once.

Gate finding: the first `cargo test --workspace` stopped at `calternal-collab` hostile_clients test `unrepresentable_update_is_rejected_and_room_keeps_saving`, failing `assertion failed: eventually_loaded(&fixture, None).await` at tests/hostile_clients.rs:268. This matches the intermittent collab test noted in the job prompt; I am rerunning the workspace once.
Author
Owner

Retry result: the second cargo test --workspace failed at the same hostile_clients::unrepresentable_update_is_rejected_and_room_keeps_saving assertion (eventually_loaded, line 268). Per the prompt, I will not rerun that intermittent test again. I will run the remaining workspace targets separately so their results are still available.

Retry result: the second `cargo test --workspace` failed at the same `hostile_clients::unrepresentable_update_is_rejected_and_room_keeps_saving` assertion (`eventually_loaded`, line 268). Per the prompt, I will not rerun that intermittent test again. I will run the remaining workspace targets separately so their results are still available.
Author
Owner

Finding in the remaining workspace tests: calternal-server live-app test signs the same cookie out twice. The first DELETE succeeds with 204; the duplicate DELETE at wire.rs:2423 returns 401 while the test expects 204 at line 2424. Both merge parents contain the duplicated assertion block. I will remove the duplicate sign-out and rerun the remaining workspace gate.

Finding in the remaining workspace tests: `calternal-server` live-app test signs the same cookie out twice. The first DELETE succeeds with 204; the duplicate DELETE at wire.rs:2423 returns 401 while the test expects 204 at line 2424. Both merge parents contain the duplicated assertion block. I will remove the duplicate sign-out and rerun the remaining workspace gate.
Author
Owner

Finished issue #60.

Branch: job/ask-mode
Merge parents: d84cab438c785ade02a70362985ac622893c9db0 and 1bbd40e1c75d0059eaa84ea39e4abc763bd3b52c
Head: f3210239c1015876b876afb4aabdf42a9673a67f

Integrated Ask turns with main's AI history. Ask prompts, kind, and citation data persist and appear in history; Ask uses the read-only scope; API undo returns 409 for Ask and the UI hides undo. Preserved main's adversarial harness and added Ask probes. Ask migration is 0004_ask_turns.sql; main's deployed migration remains 0003_ai_turn_prompts.sql. Auth migration 0007_read_only_agent_scope.sql is next after main's 0001–0006.

Gate results:

  • cargo fmt --check: exit 0, no output.
  • cargo clippy --all-targets -- -D warnings: passed. Output: Compiling calternal-server v0.0.1 (/home/kayg/Developer/calternal-wt/ask-mode/crates/calternal-server) / Finished \dev` profile [unoptimized + debuginfo] target(s) in 14.04s`.
  • cargo test --workspace: failed on the known intermittent collab hostile-client test on both attempts. Output: test unrepresentable_update_is_rejected_and_room_keeps_saving ... FAILED; assertion failed: eventually_loaded(&fixture, None).await; crates/calternal-collab/tests/hostile_clients.rs:268.
  • cargo test --workspace --exclude calternal-collab: passed, exit 0. Server test summary: test result: ok. 14 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 8.97s.
  • cargo test -p calternal-collab --test shared_notes: passed: test owner_editor_viewer_and_live_revoke ... ok (1 passed).
  • cargo test -p calternal-collab --test two_clients: passed: 2 passed; 0 failed.
  • bash packages/api-client/check-generated.sh: passed; generated OpenAPI client is current.
  • cd apps/web && bun run check && bun run test: svelte-check found 0 errors and 0 warnings; Test Files 27 passed (27); Tests 193 passed (193).
  • bash tests/adversarial/run.sh: passed; ==== FINDINGS 0; ==== ROUND 2 FINDINGS 0; server alive at end.
  • cargo clean: Removed 19775 files, 14.4GiB total.

The live production screenshots are attached in the job workspace. They use a real local account and empty AI API responses (credentials and turns both returned 200); no sample turns were added.

Decision recorded for the owner: keep Agent and Ask in shared AI history with a kind field and durable prompt/citations; refuse Ask undo with HTTP 409 because Ask never writes. DESIGN §32 specifies clickable citations but not their event or view shape, so the UI renders links only for citation markers returned by the API.

Finished issue #60. Branch: `job/ask-mode` Merge parents: `d84cab438c785ade02a70362985ac622893c9db0` and `1bbd40e1c75d0059eaa84ea39e4abc763bd3b52c` Head: `f3210239c1015876b876afb4aabdf42a9673a67f` Integrated Ask turns with main's AI history. Ask prompts, kind, and citation data persist and appear in history; Ask uses the read-only scope; API undo returns 409 for Ask and the UI hides undo. Preserved main's adversarial harness and added Ask probes. Ask migration is `0004_ask_turns.sql`; main's deployed migration remains `0003_ai_turn_prompts.sql`. Auth migration `0007_read_only_agent_scope.sql` is next after main's `0001`–`0006`. Gate results: - `cargo fmt --check`: exit 0, no output. - `cargo clippy --all-targets -- -D warnings`: passed. Output: `Compiling calternal-server v0.0.1 (/home/kayg/Developer/calternal-wt/ask-mode/crates/calternal-server)` / `Finished \`dev\` profile [unoptimized + debuginfo] target(s) in 14.04s`. - `cargo test --workspace`: failed on the known intermittent collab hostile-client test on both attempts. Output: `test unrepresentable_update_is_rejected_and_room_keeps_saving ... FAILED`; `assertion failed: eventually_loaded(&fixture, None).await`; `crates/calternal-collab/tests/hostile_clients.rs:268`. - `cargo test --workspace --exclude calternal-collab`: passed, exit 0. Server test summary: `test result: ok. 14 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 8.97s`. - `cargo test -p calternal-collab --test shared_notes`: passed: `test owner_editor_viewer_and_live_revoke ... ok` (1 passed). - `cargo test -p calternal-collab --test two_clients`: passed: `2 passed; 0 failed`. - `bash packages/api-client/check-generated.sh`: passed; generated OpenAPI client is current. - `cd apps/web && bun run check && bun run test`: `svelte-check found 0 errors and 0 warnings`; `Test Files 27 passed (27)`; `Tests 193 passed (193)`. - `bash tests/adversarial/run.sh`: passed; `==== FINDINGS 0`; `==== ROUND 2 FINDINGS 0`; server alive at end. - `cargo clean`: `Removed 19775 files, 14.4GiB total`. The live production screenshots are attached in the job workspace. They use a real local account and empty AI API responses (credentials and turns both returned 200); no sample turns were added. Decision recorded for the owner: keep Agent and Ask in shared AI history with a `kind` field and durable prompt/citations; refuse Ask undo with HTTP 409 because Ask never writes. DESIGN §32 specifies clickable citations but not their event or view shape, so the UI renders links only for citation markers returned by the API.
Author
Owner

Completed on dev in 7f0d66dcc7 (Merge job/ask-mode: Ask answers from your own data with citations, read-only agent scope (#60)).

Completed on dev in 7f0d66dcc727effe019ffa1c05f1ced8710a2261 (Merge job/ask-mode: Ask answers from your own data with citations, read-only agent scope (#60)).
kayg closed this issue 2026-10-01 05:08:54 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#60
No description provided.