fix: allow research runs to restart after stop; improve FTS search, agent limits, and timeouts

- AgentTree.restart() revives a stopped run in the same session: resets the
  root, archives old branches, and restores spawnability (stop no longer
  cancels the root).
- ResearchLoop.start() restarts from stopped/completed and resets iteration
  counters; setGoal is allowed in terminal states; supervisor 'start' revives
  the tree when the root is not spawnable.
- Cancelled/failed/archived children no longer count toward the per-agent
  child limit and archived agents are ignored by the duplicate-task guard.
- cancel() no longer overwrites an already recorded agent result.
- Add agent_timeout_seconds (default 1800) so hung agent sessions fail
  instead of blocking wait() forever.
- FTS search now prefix-matches tokens (tolerates inflections) and safely
  handles punctuation/FTS5 metacharacters instead of returning false
  negatives or throwing.
- Add tests for restart semantics, child limits, result preservation,
  agent timeouts, and FTS morphology/special characters (38/38 passing).
This commit is contained in:
Emil
2026-07-31 22:57:54 +03:00
parent 528983ee50
commit 18b8a3247d
10 changed files with 90 additions and 16 deletions
+10 -2
View File
@@ -14,8 +14,16 @@ export class ResearchLoop {
this.state = (() => { try { return JSON.parse(readFileSync(this.path, "utf8")) as ResearchLoopState; } catch { const now = new Date().toISOString(); return { runId, goal, status: "planning", iteration: 0, noProgressIterations: 0, createdAt: now, updatedAt: now, reports: [] }; } })(); this.persist();
}
snapshot(): ResearchLoopState { return structuredClone(this.state); }
setGoal(goal: string): void { if (this.state.iteration > 0 && this.state.goal !== goal.trim()) throw new Error("Start a new research run to change the goal after iterations have been recorded"); this.state.goal = goal.trim(); this.persist(); }
start(): void { if (["completed", "stopped"].includes(this.state.status)) throw new Error(`Research loop is ${this.state.status}`); this.state.status = "running"; this.persist(); }
setGoal(goal: string): void {
const terminal = ["stopped", "completed"].includes(this.state.status);
if (!terminal && this.state.iteration > 0 && this.state.goal !== goal.trim()) throw new Error("Start a new research run to change the goal after iterations have been recorded");
this.state.goal = goal.trim(); this.persist();
}
start(): void {
if (this.state.status === "running") return;
if (["stopped", "completed"].includes(this.state.status)) { this.state.iteration = 0; this.state.noProgressIterations = 0; this.state.reports = []; delete this.state.stopReason; }
this.state.status = "running"; this.persist();
}
pause(): void { if (this.state.status === "running") { this.state.status = "paused"; this.persist(); } }
resume(): void { if (this.state.status !== "paused") throw new Error("Only a paused loop can resume"); this.state.status = "running"; this.persist(); }
stop(reason = "Stopped by user"): void { this.state.status = "stopped"; this.state.stopReason = reason; this.persist(); }