feat: harden runtime readiness and session workflow
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user