Skip to content

fix(platform): answer the stubbed chat search legs - #3147

Merged
Israeltheminer merged 1 commit into
mainfrom
fix/chat-legs-dead-stubs
Sep 3, 2026
Merged

fix(platform): answer the stubbed chat search legs#3147
Israeltheminer merged 1 commit into
mainfrom
fix/chat-legs-dead-stubs

Conversation

@Israeltheminer

Copy link
Copy Markdown
Collaborator

Three chat search legs returned nothing, each under a comment saying no 0.5 table existed. All three tables existed.

Stacked on #3123 — review that first. Refs #3142.

Why

Leg What the stub said What is actually there
kind="website" "A corpus without a 0.5 table yet" migration 0045 created app.websites with every column 0.4's reader projected, and listWebsites already existed
kind="mail-attachment" "no 0.5 tables yet" migration 0037 shipped conversation_id and mail_received_at_ms, and the email binder writes both

Both kinds are advertised to the model on every turn — the tool description says kind="web-page" cannot be listed — search it, or list kind="website". The website stub killed the search leg too.

The mail stub also made a false claim: truncated: false is defined by the 0.4 contract as "the walk reached the end of the mail index", and it was returning that while listing nothing.

Separately, the task and project legs lost their zero-hit listing fallback. 0.4, on a text match that returned nothing, fell back to a 15-row listing and stamped listed: true — "Nothing matched the words. Answer the question the words were describing." #3029's own table promises it stays byte-identical. 0.5 returned listed: args.list === true, always false on a search, which left listedSource unreachable code.

What changed

A real mail-attachment reader in domains/file_metadata/mail-attachments.ts. Every row is gated on conversationAssignmentAllows against the conversation's current assignment, with the caller's role and team ids resolved rather than accepted as arguments, and disabled denied. A listing that skipped that would serve the whole inbox. truncated now reports the truth.

Migration 0064 adds (org_id, mail_received_at_ms DESC, id DESC) WHERE mail_received_at_ms IS NOT NULL. The only existing index leads with conversation_id, so it orders by conversation — the shape #3035 recorded as rejected, because a by-arrival page then reads most of its budget to return one row and, above the budget, reports "most recent" from an arbitrary window. id is in the index so the tiebreak inside one millisecond is index-ordered: which rows a bounded listing keeps is part of its answer.

Numbered 0064 to clear 0059 (#3130), 0060 (#3131) and 0062 (#3135). The boot runner applies by filename order and needs no contiguity.

The website leg calls the existing listWebsites. The two work legs get their fallback back.

The guard that would have caught all three

backend/domains/chat/ had no test file. The sandbox bridge has exactly the right one — a walker over its import graph requiring a handler for every reachable internal.* name. That absence is how #3112's and #3128's missing handlers shipped.

The walker is now extracted to backend/lib/ctx-shim-reachability.ts and used by both maps, rather than copied.

Tests

Mutation Went red
a chat shim handler deleted the reachability gate, naming queryContacts (called from core/chat/assistant_tools.ts)

Plus integration coverage for the three legs and the mail listing's privacy: an admin sees an unassigned conversation's attachment, a plain member sees only their own or their team's, a non-member sees nothing.

Scope

Does not index anything. kind="mail-attachment" lists attachments and reports indexed: false per row; making their content searchable is #3121, which is a different mechanism and needs the retrieval gate #3141 lands first.

Gate: typecheck, oxlint --type-aware, oxfmt --check green; the two shim suites 7/7.

@Israeltheminer

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main now that #3123 has merged, so this is no longer a stack. All four findings still stand there — both legs are still stubbed, the zero-hit fallback is still absent, and backend/domains/chat/ still has no test file. (The two listed: true hits on main are the no-readable-projects early return, not the fallback.)

Migration renumbered 0064 → 0070. main took 0064 for conversation_rfc_message_id_backfill and now runs to 0069.

Verified against a real Postgres and MinIO: 377/382, five failures — two yt-dlp probes and three agent-lane probes, all identical on unmodified main. +3 checks, +4 passes, nothing new.

The three added checks all pass, and the mail listing proves the property that mattered:

admin    = ours-newest.pdf | triage-middle.pdf | mine-older.pdf   trunc=false
member   = ours-newest.pdf | mine-older.pdf                       trunc=false   (team + own only)
outsider = (none)
stranger = (none)
indexed  = ours-newest.pdf:false, triage-middle.pdf:false, mine-older.pdf:true
pageFilled = ours-newest.pdf/trunc=true      scanCut = (none)/trunc=true

So the admin sees the unassigned triage row, the plain member sees only their team's and their own, and both the non-member and the stranger see nothing. truncated is reported honestly in both bounded cases rather than the stub's flat false.

One benign log line to expect in the output: [scaffoldNewOrganization] refusing invalid slug "…-mail-attachment-fixture". The fixture org is a synthetic id with no config tree, which the SQL-level listing does not need — the check passes with real rows, as above.

Three legs the port left returning nothing, each under a comment saying no
0.5 table existed. All three tables existed.

kind="website" answered []. Migration 0045 created app.websites with every
column 0.4's reader projected, and listWebsites already existed.

kind="mail-attachment" answered {attachments: [], truncated: false}, while
migration 0037 shipped conversation_id and mail_received_at_ms and the email
binder writes both. truncated:false means "the walk reached the end of the
mail index", so the stub was also making a false claim. The new reader gates
every row on conversationAssignmentAllows against the conversation's current
assignment, resolving the caller's role and team ids rather than taking them
as arguments — a listing that skipped that would serve the whole inbox.

Migration 0070 adds the arrival index. The only existing index leads with
conversation_id, so it orders by conversation, which is the shape #3035
recorded as rejected: a by-arrival page reads most of its budget to return
one row, and above the budget reports "most recent" from an arbitrary window.

The task and project legs also lost their zero-hit listing fallback, leaving
listedSource unreachable. Restored in both.

Adds the reachability gate the chat map never had: every internal ref
reachable from the turn now needs a handler, using the walker the sandbox
bridge already had rather than a second copy. That absence is how #3112's
and #3128's missing handlers shipped.
@Israeltheminer
Israeltheminer force-pushed the fix/chat-legs-dead-stubs branch from 3da8a6a to a5e7568 Compare September 3, 2026 13:08
@Israeltheminer
Israeltheminer merged commit 019d0a0 into main Sep 3, 2026
48 checks passed
@Israeltheminer
Israeltheminer deleted the fix/chat-legs-dead-stubs branch September 3, 2026 15:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant