feat: harden runtime readiness and session workflow
This commit is contained in:
@@ -4,15 +4,57 @@ import type { ThtRunner } from "../tht/tht-runner.js";
|
||||
import type { SseHub } from "../sse/sse-hub.js";
|
||||
import type { Settings } from "../settings/settings-store.js";
|
||||
import { getUser } from "../auth/auth.js";
|
||||
import type { ReadinessManager } from "../runtime/readiness-manager.js";
|
||||
|
||||
export function sessionRoutes(
|
||||
app: FastifyInstance,
|
||||
d: { mgr: PiProcessManager; tht: ThtRunner; hub: SseHub; getSettings: () => Settings; ollamaEnsureTimeoutSec: number },
|
||||
d: { mgr: PiProcessManager; tht: ThtRunner; hub: SseHub; getSettings: () => Settings; readiness: ReadinessManager },
|
||||
) {
|
||||
const info = (id: string, text: string, level = "info") =>
|
||||
d.hub.publish(id, "info", { type: "info", level, text });
|
||||
|
||||
const bindRuntime = (id: string, rt: ReturnType<PiProcessManager["createFor"]>) =>
|
||||
rt.bridge.onClientEvent((e) => {
|
||||
if (e.type === "system_event" && e.event === "session_failed") {
|
||||
void d.tht.failSession(id, d.getSettings().workspace).catch(() => undefined);
|
||||
}
|
||||
d.hub.publish(id, e.type, e);
|
||||
});
|
||||
|
||||
const bootstrap = (
|
||||
id: string,
|
||||
rt: ReturnType<PiProcessManager["createFor"]>,
|
||||
configure: Promise<void>,
|
||||
retrieval: Promise<void> | null,
|
||||
start: () => void,
|
||||
) => {
|
||||
void (async () => {
|
||||
try {
|
||||
if (retrieval) info(id, "Preparing retrieval context");
|
||||
await Promise.all([configure, retrieval]);
|
||||
info(id, "Starting model");
|
||||
start();
|
||||
} catch (error) {
|
||||
d.mgr.teardown(id);
|
||||
const text = error instanceof Error ? error.message : String(error);
|
||||
void d.tht.failSession(id, d.getSettings().workspace).catch(() => undefined);
|
||||
rt.bridge.emitClientEvent({ type: "info", level: "error", text: `Session bootstrap failed: ${text}` });
|
||||
rt.bridge.emitClientEvent({ type: "system_event", event: "session_failed" });
|
||||
rt.bridge.emitClientEvent({ type: "system_event", event: "agent_end" });
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
app.post("/runtime/prewarm", async (_req, reply) => {
|
||||
const workspace = d.getSettings().workspace ?? "";
|
||||
void d.readiness.ensure(workspace).catch(() => undefined);
|
||||
return reply.code(202).send({ status: "warming" });
|
||||
});
|
||||
|
||||
app.post("/sessions", async (req, reply) => {
|
||||
const b = req.body as { question: string; name?: string };
|
||||
const s = d.getSettings();
|
||||
const ensure = await d.tht.ollamaEnsure(s.workspace ?? "", d.ollamaEnsureTimeoutSec);
|
||||
const ensure = await d.readiness.ensure(s.workspace ?? "");
|
||||
if (!ensure.ok) return reply.code(503).send({ error: ensure.error ?? "Ollama/embeddings non disponibili" });
|
||||
// Settings (global) supply workspace/provider/model/thinking. The new-question
|
||||
// form sends only the question text. `workspace` selects the tht `-c <config>`.
|
||||
@@ -24,14 +66,22 @@ export function sessionRoutes(
|
||||
model: s.model,
|
||||
thinking: s.thinking,
|
||||
});
|
||||
const rt = await d.mgr.spawnFor(id, {
|
||||
const options = {
|
||||
provider: s.provider,
|
||||
model: s.model,
|
||||
thinking: s.thinking,
|
||||
// Keep the saved preference in the manifest; F1 starts tool-first.
|
||||
thinking: "off",
|
||||
author: getUser(req).id,
|
||||
question: b.question,
|
||||
});
|
||||
rt.bridge.onClientEvent((e) => d.hub.publish(id, e.type, e));
|
||||
};
|
||||
const rt = d.mgr.createFor(id, options);
|
||||
bindRuntime(id, rt);
|
||||
info(id, "Session created");
|
||||
bootstrap(
|
||||
id, rt, d.mgr.configure(rt, options),
|
||||
d.tht.searchPack(b.question, id, s.workspace),
|
||||
() => d.mgr.start(id, rt, options),
|
||||
);
|
||||
return { id };
|
||||
});
|
||||
app.get("/sessions", async () => d.tht.sessionList(d.getSettings().workspace));
|
||||
@@ -51,20 +101,43 @@ export function sessionRoutes(
|
||||
});
|
||||
app.post("/sessions/:id/resume", async (req, reply) => {
|
||||
const id = (req.params as any).id;
|
||||
if (d.mgr.get(id)) {
|
||||
// Idempotent resume: reattach the browser to the existing Pi runtime.
|
||||
// Do not respawn it (which would discard a pending reviewer widget).
|
||||
return reply.code(200).send({ id, alreadyActive: true });
|
||||
}
|
||||
const manifest = (await d.tht.sessionShow(id, d.getSettings().workspace)) as { status?: string; archived?: boolean } | null;
|
||||
if (manifest?.status === "finalized" || manifest?.archived) {
|
||||
return reply.code(409).send({ error: "sessione in sola lettura (finalizzata o archiviata)" });
|
||||
}
|
||||
const ensure = await d.tht.ollamaEnsure(d.getSettings().workspace ?? "", d.ollamaEnsureTimeoutSec);
|
||||
const settings = d.getSettings();
|
||||
const ensure = await d.readiness.ensure(settings.workspace ?? "");
|
||||
if (!ensure.ok) return reply.code(503).send({ error: ensure.error ?? "Ollama/embeddings non disponibili" });
|
||||
const rt = await d.mgr.resume(id, d.tht);
|
||||
rt.bridge.onClientEvent((e) => d.hub.publish(id, e.type, e));
|
||||
const saved = manifest as { provider?: string; model?: string; thinking?: string } | null;
|
||||
const options = {
|
||||
provider: saved?.provider,
|
||||
model: saved?.model,
|
||||
// Phase 1 must reach a widget instead of exposing a long reasoning trace.
|
||||
// The session keeps its saved preference for later turns.
|
||||
thinking: "off",
|
||||
author: getUser(req).id,
|
||||
mode: "resume" as const,
|
||||
};
|
||||
await d.tht.reopenSession(id, settings.workspace);
|
||||
const rt = d.mgr.createFor(id, options);
|
||||
bindRuntime(id, rt);
|
||||
info(id, "Resuming session");
|
||||
bootstrap(id, rt, d.mgr.configure(rt, options), null, () => d.mgr.start(id, rt, options));
|
||||
return reply.code(200).send({ id });
|
||||
});
|
||||
app.post("/sessions/:id/close", async (req) => {
|
||||
const id = (req.params as { id: string }).id;
|
||||
d.mgr.teardown(id);
|
||||
d.hub.clear(id);
|
||||
try {
|
||||
await d.tht.closeSession(id, d.getSettings().workspace);
|
||||
} finally {
|
||||
d.mgr.teardown(id);
|
||||
d.hub.clear(id);
|
||||
}
|
||||
return { closed: true };
|
||||
});
|
||||
app.get("/sessions/:id/events", (req, reply) => {
|
||||
@@ -76,6 +149,7 @@ export function sessionRoutes(
|
||||
reply.raw.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no",
|
||||
Connection: "keep-alive",
|
||||
"Access-Control-Allow-Origin": origin,
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
|
||||
Reference in New Issue
Block a user