Skip to main content

The commercial chat cutover

What /w/:slug/chat became, why each piece moved, and what is still owed.
The routing half of this document is superseded by the chat manager. classifyChatRoute is deleted: a regex ladder held the authority over meaning and failed by a conjugation, so one typed manager decision replaced both it and the research layer’s second semantic router on the Chat path. Everything below about EXECUTION — the Work Session, the activity trail, the report artifact, the customer-safe projection — is unchanged and still current.

The request path, end to end

The browser renders the Work Session projection and nothing else. It never sees an operation name, a semantic code, a row count phrased as a fraction, or a statement.

Root cause of each observed incident

Incidents 6 and 7 are one cause. The router existed and was already deterministic; it was missing the shapes people use — an offer framing (“vendo”, “fabricamos”), the demand side (“quién compra”, “quiénes han ganado”), and a supply-side noun list that omitted fabricantes and proveedores. contact_request also sat above the market ladder, so the bare word contacto in a supplier search sent the errand to the CRM.

Routing, before and after

Deletion ledger

Every deletion below was proven unreachable by reference before it was made.

Deleted

Total: 7,864 lines removed, 30 added, in the consolidation commit alone.

Kept, because the experience rests on them

redesign/chatThreads.ts (thread creation, message POST, regenerate, checkpoint resolution), redesign/agentic-events.ts (A1 parser and reducer), the SSE subscription in api.ts, the Work Session snapshot route, ResearchReport and its artifacts, stop and retry.

Kept, with the deletion condition named

The second row is the honest reading of “quiero que todo el UI se utilice”: the nine-route client and its three domain hooks are built and correct, and exactly one of the three domains — opportunities — currently has rows in the report to render. The other two are one backend field away, and are named here rather than deleted quietly or left to look used.

Capability isolation

The classifier can be wrong. Safety does not rest on it being right. A commercial capability used to be reachable from the general runtime through two doors, both open by default:
  • the belt. chat-tools.ts registered discover_market_capabilities, query_market_signals and get_signal_evidence behind includeMarketTools, computed as “not a market route” — so the tools built for commercial work were withheld from commercial turns and handed to every other one.
  • the manual. chat.skill.ts opened its market section by instructing the model to call the first two in sequence.
Both are closed. marketIntelligence, runMarketIntelligence and includeMarketTools are gone from ChatToolDeps; there is no dependency to inject and no flag to get wrong. The manual carries a boundary instead, naming Collections, Topics and Projects explicitly as things that must not be substituted for market data. apps/api/src/chat/commercial-isolation.spec.ts pins all eight invariants: Plus two the eight imply: the SEMANTIC operations are not on the general belt either (replacing one leak with a newer one is the same defect), and a market-research turn builds no chat tool belt at all.

The activity trail

The runner has always reported every stage through step_started / step_finished. The turn pipeline dropped all of them and forwarded only tool calls — which is why the wait began after the search was already prepared. The channel is the A1 contract’s own ACTIVITY_DELTA / ActivityEntry, already frozen, already reduced into snapshot.activity, already served by the Work Session endpoint. No second stream was built, and the trail survives a reload because it is the same durable projection everything else here reads. investigate is deliberately not narrated: it is the tool loop and its children already name themselves. Allowed in an entry: a human sentence (summary), a domain (name), a status, ok, and a safe count or sanitized reason (preview). Never: an operation name, a semantic code, a UUID as a label, SQL, a prompt, reasoning, a capability catalogue, warehouse rows, or a provider — providerId is always null. Ids are stable KEYS (phase:route_research, tool:q_0), never labels. The prefix also keeps them non-numeric, so object key order stays insertion order and the trail rehydrates in the order it happened.

Known residual exposure

A commercial question that slips the ladder still classifies as general. That degradation is now bounded rather than dangerous: the general belt holds no market capability, so the worst case is an honest “I can’t answer that from here” instead of a tour of Topics and Collections. The legacy Opportunity Flow still runs where the market-research dependencies are absent. Its deletion condition is unchanged: those dependencies ceasing to be @Optional().

What is NOT proven

No live end-to-end run against staging, and no screenshots. The container has no API on localhost:3000, no Docker, no database URL, no model credential and no Clerk session; Playwright is not resolvable from apps/dashboard, and /chat sits behind an authenticated workspace route, so a browser would reach a sign-in screen rather than any of the states above. Every claim here rests on hermetic tests and on reading the code.

The live E2E checklist, for whoever has staging

Then, in /w/<slug>/chat, with the network tab open on the SSE stream: Repeat 1 for each domain: Busca fabricantes de bombas en Jalisco con contacto publicado. · ¿Qué licitaciones accionables hay para tratamiento de agua? · ¿Quiénes han ganado contratos de alumbrado público y por cuánto, separados por moneda y ámbito?