feat: harden runtime readiness and session workflow

This commit is contained in:
User
2026-07-14 10:27:25 +02:00
parent 6d7738d538
commit 6dbf93fff9
52 changed files with 1464 additions and 169 deletions
+34 -12
View File
@@ -12,6 +12,15 @@ export interface SessionRuntime {
child: ChildProcessWithoutNullStreams;
}
export interface RuntimeOptions {
provider?: string;
model?: string;
thinking?: string;
author?: string;
question?: string;
mode?: "new" | "resume";
}
/** Injectable child-process boundary; callbacks may ignore arguments in simpler tests. */
type SpawnFn = (
command: string,
@@ -68,17 +77,13 @@ export class PiProcessManager {
get(id: string): SessionRuntime | undefined { return this.runtimes.get(id); }
async spawnFor(
sessionId: string,
o: { provider?: string; model?: string; thinking?: string; author?: string; question?: string; mode?: "new" | "resume" },
): Promise<SessionRuntime> {
// Idempotent per session id: tear down any existing runtime for this id
// first (before the cap check) so a resume/respawn neither leaks the old
// child nor falsely hits the process cap.
/** Spawn and register a runtime synchronously, without starting a model turn. */
createFor(sessionId: string, o: RuntimeOptions = {}): SessionRuntime {
// A duplicate start must never tear down a live session: that used to send
// SIGTERM to the in-flight Pi process and lose its pending gate.
const existing = this.runtimes.get(sessionId);
if (existing) {
existing.child.kill();
this.runtimes.delete(sessionId);
throw new Error(`session runtime already active: ${sessionId}`);
}
if (this.runtimes.size >= this.cfg.maxPiProcesses) {
throw new Error("max Pi processes reached");
@@ -103,24 +108,41 @@ export class PiProcessManager {
level: "error",
text: `Pi process exited unexpectedly (code ${code ?? "?"})`,
});
rt.bridge.emitClientEvent({ type: "system_event", event: "session_failed" });
rt.bridge.emitClientEvent({ type: "system_event", event: "agent_end" });
}
});
return rt;
}
/** Configure model and thinking. Safe to run alongside deterministic retrieval. */
async configure(rt: SessionRuntime, o: RuntimeOptions = {}): Promise<void> {
const provider = canonicalPiProvider(o.provider ?? this.cfg.defaults.provider);
const model = o.model ?? this.cfg.defaults.model;
const thinking = o.thinking ?? this.cfg.defaults.thinking;
if (provider && model) {
await rpc.request({ type: "set_model", provider, modelId: model } as object & { type: string });
await rt.rpc.request({ type: "set_model", provider, modelId: model } as object & { type: string });
}
if (thinking) {
await rpc.request({ type: "set_thinking_level", level: thinking } as object & { type: string });
await rt.rpc.request({ type: "set_thinking_level", level: thinking } as object & { type: string });
}
}
/** Start the first turn only after callers have attached the runtime bridge. */
start(sessionId: string, rt: SessionRuntime, o: RuntimeOptions = {}): void {
if (this.runtimes.get(sessionId) !== rt) throw new Error("session runtime is no longer active");
const message = o.mode === "resume"
? `/riprendi-sessione ${sessionId}`
: `/nuova-domanda ${JSON.stringify(o.question ?? "")}`;
rpc.send({ type: "prompt", message });
rt.rpc.send({ type: "prompt", message });
}
async spawnFor(sessionId: string, o: RuntimeOptions = {}): Promise<SessionRuntime> {
const rt = this.createFor(sessionId, o);
await this.configure(rt, o);
this.start(sessionId, rt, o);
return rt;
}