fix(server): auto-regenerate status snapshot on every change
The status snapshot (.agenthub/status/latest.md) was only refreshed on an explicit `status --update`, so it drifted stale and misled agents that follow golden rule #2 ("read status/latest.md") — e.g. an implementer seeing an old task list and picking the wrong task. - src/server/statusRefresh.ts: subscribe to the event bus and regenerate the snapshot on every task/handoff/decision/memory change (REST and filesystem-watch paths), debounced 300 ms. Presence ('agent') events are skipped; the snapshot lives outside the watched dirs so it can't feed back into the watcher. - wired into startServer lifecycle (start + stop on close). - test: status/latest.md reflects a new task shortly after POST /tasks. 117/117 green. Bump 0.3.0 -> 0.3.1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7723360054
commit
961675ac40
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agenthub",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"description": "Local coordination layer for AI coding agents",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@ -113,7 +113,7 @@ async function runRemote(serverUrl: string, fn: () => Promise<void>): Promise<vo
|
||||
export function createProgram(cwd: string): Command {
|
||||
const program = new Command('agenthub')
|
||||
.description('Local coordination layer for AI coding agents')
|
||||
.version('0.3.0')
|
||||
.version('0.3.1')
|
||||
.option('--server <url>', 'AgentHub server URL (env: AGENTHUB_SERVER)');
|
||||
|
||||
program
|
||||
|
||||
@ -2,6 +2,7 @@ import Fastify from 'fastify';
|
||||
import { registerRoutes } from './routes.js';
|
||||
import { resolveAdvertiseUrl, startDiscoveryBroadcaster } from '../discovery.js';
|
||||
import { startEntityWatcher } from './fsWatch.js';
|
||||
import { startStatusAutoRefresh } from './statusRefresh.js';
|
||||
|
||||
export function buildApp(cwd: string) {
|
||||
const app = Fastify({ logger: false });
|
||||
@ -17,9 +18,12 @@ export async function startServer(cwd: string, options: { port?: number; host?:
|
||||
let broadcaster: ReturnType<typeof startDiscoveryBroadcaster> | undefined;
|
||||
// Emit SSE events for CLI/file writes too, not just REST mutations.
|
||||
const stopWatcher = startEntityWatcher(cwd);
|
||||
// Keep status/latest.md fresh on every change so agents never read a stale snapshot.
|
||||
const stopStatusRefresh = startStatusAutoRefresh(cwd);
|
||||
app.addHook('onClose', async () => {
|
||||
broadcaster?.stop();
|
||||
stopWatcher();
|
||||
stopStatusRefresh();
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
40
src/server/statusRefresh.ts
Normal file
40
src/server/statusRefresh.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { eventBus } from './events.js';
|
||||
import type { AgentHubEvent } from './events.js';
|
||||
import { updateStatus } from '../core/services/statusService.js';
|
||||
|
||||
/**
|
||||
* Keep `.agenthub/status/latest.md` fresh. Agents are told (golden rule #2) to
|
||||
* read the status snapshot, but it was only regenerated on an explicit
|
||||
* `status --update` — so it drifted stale and misled agents (e.g. showing an
|
||||
* old task list). This subscribes to the in-process event bus and regenerates
|
||||
* the snapshot on every entity change, from both the REST and filesystem-watch
|
||||
* paths, debounced so a burst collapses to one write.
|
||||
*
|
||||
* Presence ('agent') events are ephemeral and don't affect status. Writing the
|
||||
* snapshot lives in `.agenthub/status/` — outside the watched entity dirs — so
|
||||
* it never feeds back into the watcher.
|
||||
*/
|
||||
const DEBOUNCE_MS = 300;
|
||||
|
||||
export function startStatusAutoRefresh(cwd: string): () => void {
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
|
||||
const onChange = (event: AgentHubEvent) => {
|
||||
if (event.type === 'agent') return;
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
timer = undefined;
|
||||
try {
|
||||
updateStatus(cwd);
|
||||
} catch {
|
||||
// best-effort — a failed status refresh must never break the server
|
||||
}
|
||||
}, DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
eventBus.on('change', onChange);
|
||||
return () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
eventBus.off('change', onChange);
|
||||
};
|
||||
}
|
||||
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, rmSync } from 'fs';
|
||||
import { mkdtempSync, rmSync, readFileSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { buildApp } from '../src/server/index.js';
|
||||
@ -531,3 +531,35 @@ describe('fsWatch dedup with the REST emit path', () => {
|
||||
expect(forTask[0].action).toBe('created');
|
||||
}, 4000);
|
||||
});
|
||||
|
||||
// ─── 7. status auto-refresh: snapshot stays fresh after mutations ─────────────
|
||||
|
||||
describe('status auto-refresh on mutation', () => {
|
||||
let cwd: string;
|
||||
let server: Awaited<ReturnType<typeof startServer>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
cwd = mkdtempSync(join(tmpdir(), 'ah-status-refresh-'));
|
||||
init(cwd, { projectName: 'status-refresh', yes: true });
|
||||
server = await startServer(cwd, { host: '127.0.0.1', port: 0 });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.app.close();
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('regenerates status/latest.md with the new task after POST /tasks', async () => {
|
||||
await fetch(`${server.url}/tasks`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: 'Fresh status task', role: 'implementer' }),
|
||||
});
|
||||
|
||||
// Wait past the status-refresh debounce (300 ms) + write.
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
|
||||
const status = readFileSync(join(cwd, '.agenthub', 'status', 'latest.md'), 'utf-8');
|
||||
expect(status).toContain('TSK-0001'); // the just-created task shows as active
|
||||
}, 4000);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user