Assistant: migrate to v2 runtime handler with Postgres-backed AgentRunner
All checks were successful
Deploy to VPS / test (push) Successful in 1m39s
Deploy to VPS / deploy (push) Successful in 22s

Replace the deprecated copilotRuntimeNextJSAppRouterEndpoint wrapper
(which was already the v2 single-route handler underneath) with
createCopilotRuntimeHandler from @copilotkit/runtime/v2, built once at
module scope, and plug in a custom PostgresAgentRunner:

- run() proxies the agent's event stream untouched — MAF persists every
  turn in chatthreadsnapshot, so the BFF records nothing
- connect() forwards to MAF's snapshot hydration (empty-messages run →
  RUN_STARTED → MESSAGES_SNAPSHOT → RUN_FINISHED, no LLM call), making
  the DB the only conversation store — correct across clears, restarts
  and future instances
- stop() aborts via a process-local live-run map (same pattern as the
  official sqlite-runner); isRunning() has no caller in the v2 SSE path

This deletes the whole workaround layer the in-memory runner forced:
the connect intercepts (URL + envelope forms) and Asistente's
client-side hydration run. The ask-box flow now awaits connectAgent
instead. Auth moves to a single Request rewrite (ws_token cookie →
Authorization Bearer) before the handler — the v2 path forwards
authorization + x-* headers to both run clones and runner.connect.
Stays in single-route mode: same envelope protocol the client already
auto-detected, no exposure to CopilotKit#4953, /threads* REST not
needed. Telemetry disabled via COPILOTKIT_TELEMETRY_DISABLED.

Verified full matrix in dev: fresh ask, reload replay via runner,
3 cleared convos + double navigation (only last survives), card renders
once and survives follow-up + reload, Inicio ask-box with history,
stale-tab persists only its new turn. 126 backend tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-07-04 21:09:24 -06:00
parent 8595e74566
commit 47cb1826a8
2 changed files with 188 additions and 132 deletions

View File

@@ -39,39 +39,34 @@ export default function Asistente() {
const [confirmClear, setConfirmClear] = useState(false);
const [clearing, setClearing] = useState(false);
// Boot sequence, once per page load: hydrate the persisted thread, then
// send any question that arrived from the Inicio ask-box via router state.
// Order matters — hydration first keeps the invariant that every normal
// run's input carries the full client-known history (the BFF's snapshot
// reconciliation relies on it).
// Ask-box flow: a question typed on Inicio arrives via router state.
// History hydration is no longer done here — CopilotChat's mount connect
// replays the persisted thread through the BFF's PostgresAgentRunner. We
// still await our own connect before sending so the run's input carries
// the full client-known history (idempotent with CopilotChat's connect:
// the MESSAGES_SNAPSHOT merge is id-based).
const bootRef = useRef(false);
useEffect(() => {
if (!agent || bootRef.current) return;
bootRef.current = true;
const ask = (location.state as { ask?: string } | null)?.ask;
if (ask) {
// Clear the state so back/refresh doesn't re-send the question.
navigate(location.pathname, { replace: true, state: null });
}
if (!ask || !agent || bootRef.current) return;
bootRef.current = true;
// Clear the state so back/refresh doesn't re-send the question.
navigate(location.pathname, { replace: true, state: null });
void (async () => {
agent.threadId = CHAT_THREAD_ID;
if (agent.messages.length === 0) {
// Empty-messages run = MAF's hydration trigger: replays the stored
// snapshot as MESSAGES_SNAPSHOT without invoking the LLM.
try {
await copilotkit.runAgent({ agent });
await copilotkit.connectAgent({ agent });
} catch (err) {
console.error("[asistente] thread hydration failed:", err);
console.error("[asistente] connect before ask failed:", err);
}
}
if (ask) {
agent.addMessage({
id: crypto.randomUUID(),
role: "user",
content: ask,
});
void copilotkit.runAgent({ agent });
}
agent.addMessage({
id: crypto.randomUUID(),
role: "user",
content: ask,
});
void copilotkit.runAgent({ agent });
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [agent]);