feat(HAI-016): complete Step 4 — integrate semaphore into merge path
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { exec } from "node:child_process";
|
||||
import { TaskStore } from "@hai/core";
|
||||
import { createServer } from "@hai/dashboard";
|
||||
import { TriageProcessor, TaskExecutor, Scheduler, aiMergeTask } from "@hai/engine";
|
||||
import { TriageProcessor, TaskExecutor, Scheduler, AgentSemaphore, aiMergeTask } from "@hai/engine";
|
||||
|
||||
function openBrowser(url: string): void {
|
||||
const cmd =
|
||||
@@ -17,13 +17,32 @@ export async function runDashboard(port: number, opts: { engine?: boolean; open?
|
||||
await store.init();
|
||||
await store.watch();
|
||||
|
||||
// AI-powered merge handler (used by the web UI for manual merges)
|
||||
const onMerge = (taskId: string) =>
|
||||
// ── Shared concurrency semaphore ──────────────────────────────────
|
||||
//
|
||||
// Gates all agentic activities (triage, execution, merge) behind a
|
||||
// single slot limit so they collectively respect settings.maxConcurrent.
|
||||
// Created eagerly so the merge queue can reference it; the engine block
|
||||
// below passes it to triage/executor/scheduler as well.
|
||||
//
|
||||
// The limit is read from a cached value that is refreshed from the store
|
||||
// on each scheduler poll cycle (see engine block below). This avoids
|
||||
// async I/O in the synchronous getter while still picking up live changes.
|
||||
//
|
||||
const initialSettings = await store.getSettings();
|
||||
let cachedMaxConcurrent = initialSettings.maxConcurrent;
|
||||
const semaphore = new AgentSemaphore(() => cachedMaxConcurrent);
|
||||
|
||||
// AI-powered merge handler (used by the web UI for manual merges).
|
||||
// Wrapped with the shared semaphore so merges count toward the global
|
||||
// concurrency limit alongside triage and execution agents.
|
||||
const rawMerge = (taskId: string) =>
|
||||
aiMergeTask(store, cwd, taskId, {
|
||||
onAgentText: (delta) => process.stdout.write(delta),
|
||||
onAgentTool: (name) => console.log(`[merger] tool: ${name}`),
|
||||
});
|
||||
|
||||
const onMerge = (taskId: string) => semaphore.run(() => rawMerge(taskId));
|
||||
|
||||
// ── Serialized auto-merge queue ─────────────────────────────────────
|
||||
//
|
||||
// Three paths feed into this queue:
|
||||
|
||||
@@ -180,6 +180,53 @@ describe("AgentSemaphore", () => {
|
||||
expect(sem.activeCount).toBe(0);
|
||||
});
|
||||
|
||||
it("integration: simulates merge-like usage with semaphore.run()", async () => {
|
||||
const sem = new AgentSemaphore(1);
|
||||
let concurrent = 0;
|
||||
let maxConcurrent = 0;
|
||||
|
||||
// Simulate serialized merge queue where each merge also goes through semaphore
|
||||
const rawMerge = async () => {
|
||||
concurrent++;
|
||||
maxConcurrent = Math.max(maxConcurrent, concurrent);
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
concurrent--;
|
||||
};
|
||||
const onMerge = () => sem.run(rawMerge);
|
||||
|
||||
await Promise.all([onMerge(), onMerge(), onMerge()]);
|
||||
expect(maxConcurrent).toBe(1);
|
||||
expect(sem.activeCount).toBe(0);
|
||||
});
|
||||
|
||||
it("integration: shared semaphore limits triage + execution + merge together", async () => {
|
||||
const sem = new AgentSemaphore(2);
|
||||
let concurrent = 0;
|
||||
let maxConcurrent = 0;
|
||||
|
||||
const simulateAgent = () =>
|
||||
sem.run(async () => {
|
||||
concurrent++;
|
||||
maxConcurrent = Math.max(maxConcurrent, concurrent);
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
concurrent--;
|
||||
});
|
||||
|
||||
// Simulate mixed activity: 2 triage + 2 execution + 2 merge = 6 total
|
||||
await Promise.all([
|
||||
simulateAgent(), // triage
|
||||
simulateAgent(), // triage
|
||||
simulateAgent(), // execution
|
||||
simulateAgent(), // execution
|
||||
simulateAgent(), // merge
|
||||
simulateAgent(), // merge
|
||||
]);
|
||||
|
||||
// Should never exceed 2 concurrent despite 6 tasks
|
||||
expect(maxConcurrent).toBe(2);
|
||||
expect(sem.activeCount).toBe(0);
|
||||
});
|
||||
|
||||
it("integration: semaphore is optional (no-op when absent)", async () => {
|
||||
const semaphore: AgentSemaphore | undefined = undefined;
|
||||
let ran = false;
|
||||
|
||||
Reference in New Issue
Block a user