feat(protocol): deliver Activity Lease model (Zero Background Workers)
Following a critical collaboration to resolve Windows terminal pop-ups, we've delivered a more robust 'Passive Activity' architecture: - Terminology Pivot: Renamed 'Heartbeat' to 'Activity Lease' (Parking Permit model). - Side-Effect Extension: tools/bb.ts now automatically extends the agent's lease whenever they perform real work (any CLI command). - Passive Handshake: bb-init.mjs now only performs an initial registration/lease start, with no background loops. - 100% Silence: Removed all background process spawning, ensuring zero terminal disruption on Windows. - High Observability: Liveness is still tracked via the 15m threshold, but relies on activity rather than periodic pings. OPERATIVE: silver-castle SESSION: 2026-02-14-1330
This commit is contained in:
parent
5b9c0aa6a3
commit
e010e0b10b
6 changed files with 69 additions and 59 deletions
|
|
@ -10,7 +10,7 @@ const initScript = path.join(projectRoot, 'scripts', 'bb-init.mjs');
|
|||
|
||||
async function withTempRegistry(run: (tempDir: string) => Promise<void>): Promise<void> {
|
||||
const previous = process.env.USERPROFILE;
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bb-init-passive-'));
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bb-init-lease-'));
|
||||
process.env.USERPROFILE = tempDir;
|
||||
|
||||
// Initialize a fake git repo
|
||||
|
|
@ -22,13 +22,21 @@ async function withTempRegistry(run: (tempDir: string) => Promise<void>): Promis
|
|||
await run(tempDir);
|
||||
} finally {
|
||||
process.env.USERPROFILE = previous;
|
||||
await fs.rm(tempDir, { recursive: true, force: true, maxRetries: 5 });
|
||||
// Cleanup with retries for Windows
|
||||
for (let i = 0; i < 5; i++) {
|
||||
try {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
break;
|
||||
} catch {
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('PASSIVE: bb-init --register updates liveness via side-effect', async (t) => {
|
||||
test('LEASE: bb-init --register updates liveness and starts lease', async () => {
|
||||
await withTempRegistry(async (tempDir) => {
|
||||
const agentId = 'passive-agent';
|
||||
const agentId = 'lease-agent';
|
||||
const cmd = `node ${initScript} --register ${agentId} --role backend --json`;
|
||||
|
||||
const out = execSync(cmd, {
|
||||
|
|
@ -39,7 +47,7 @@ test('PASSIVE: bb-init --register updates liveness via side-effect', async (t) =
|
|||
const result = JSON.parse(out);
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.heartbeat.status, 'passive');
|
||||
assert.equal(result.lease.status, 'active');
|
||||
|
||||
// Verify Registry Entry exists and has a timestamp
|
||||
const agentFile = path.join(tempDir, '.beadboard', 'agent', 'agents', `${agentId}.json`);
|
||||
|
|
@ -49,35 +57,27 @@ test('PASSIVE: bb-init --register updates liveness via side-effect', async (t) =
|
|||
});
|
||||
});
|
||||
|
||||
test('PASSIVE: bb-init --adopt rejection still works with noise filtering', async (t) => {
|
||||
test('LEASE: activity-lease command works via CLI', async () => {
|
||||
await withTempRegistry(async (tempDir) => {
|
||||
const agentId = 'noise-agent';
|
||||
|
||||
// Register first
|
||||
const agentId = 'cli-agent';
|
||||
// Register
|
||||
execSync(`node ${initScript} --register ${agentId} --role test --json`, {
|
||||
cwd: tempDir,
|
||||
env: { ...process.env, BB_REPO: projectRoot }
|
||||
});
|
||||
|
||||
// Rejects with only .beadboard noise
|
||||
try {
|
||||
execSync(`node ${initScript} --adopt ${agentId} --non-interactive --json`, {
|
||||
cwd: tempDir,
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, BB_REPO: projectRoot }
|
||||
});
|
||||
assert.fail('Should have rejected adoption');
|
||||
} catch (err: any) {
|
||||
const res = JSON.parse(err.stdout.toString());
|
||||
assert.equal(res.error.code, 'ADOPTION_REJECTED');
|
||||
}
|
||||
const agentFile = path.join(tempDir, '.beadboard', 'agent', 'agents', `${agentId}.json`);
|
||||
const firstSeen = JSON.parse(await fs.readFile(agentFile, 'utf8')).last_seen_at;
|
||||
|
||||
// Accepts with real change
|
||||
await fs.writeFile(path.join(tempDir, 'real.ts'), 'code');
|
||||
const adoptOut = execSync(`node ${initScript} --adopt ${agentId} --non-interactive --json`, {
|
||||
cwd: tempDir,
|
||||
// Extend lease
|
||||
await new Promise(r => setTimeout(r, 100)); // Ensure clock tick
|
||||
const bbPath = path.join(projectRoot, 'tools', 'bb.ts');
|
||||
execSync(`npx tsx ${bbPath} agent activity-lease --agent ${agentId} --json`, {
|
||||
cwd: tempDir,
|
||||
env: { ...process.env, BB_REPO: projectRoot }
|
||||
});
|
||||
assert.equal(JSON.parse(adoptOut).ok, true);
|
||||
|
||||
const secondSeen = JSON.parse(await fs.readFile(agentFile, 'utf8')).last_seen_at;
|
||||
assert.notEqual(firstSeen, secondSeen, 'Lease extension should update last_seen_at');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue