From 6f028bf6c305aa25288dce7b161f0884fa2b2a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 2 Nov 2026 10:18:33 +0100 Subject: [PATCH] fix(daemon): preserve active human-control holds during renewal --- .../lease-registry-human-control.test.ts | 42 +++++++++++++++++++ .../daemon/src/resources/lease-registry.ts | 9 ++-- 2 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 packages/daemon/src/resources/__tests__/lease-registry-human-control.test.ts diff --git a/packages/daemon/src/resources/__tests__/lease-registry-human-control.test.ts b/packages/daemon/src/resources/__tests__/lease-registry-human-control.test.ts new file mode 100644 index 0000000000..232a91df26 --- /dev/null +++ b/packages/daemon/src/resources/__tests__/lease-registry-human-control.test.ts @@ -0,0 +1,42 @@ +import { test } from 'vitest'; +import assert from 'node:assert/strict'; +import { createRequestCanceledError } from '@agent-device/kernel/errors'; +import { LeaseRegistry } from '../lease-registry.ts'; +import { + HUMAN_CONTROL_LEASE_REQUEST, + isHumanControlError, +} from '../../__tests__/support/human-control-fixtures.ts'; + +test('canceling an active hold re-upsert preserves the hold and blocks device admission', async () => { + let now = 0; + const registry = new LeaseRegistry({ now: () => now }); + const lease = registry.allocateLease(HUMAN_CONTROL_LEASE_REQUEST); + const authority = { kind: 'lease', leaseId: lease.leaseId } as const; + const original = await registry.putHumanControlHold(authority, 'console', { + reason: 'Human is using the device', + ttlMs: 10_000, + }); + assert.equal(original.state, 'active'); + now = 1_000; + const controller = new AbortController(); + const renewal = registry.putHumanControlHold( + authority, + 'console', + { reason: 'Renewed hold', ttlMs: 20_000 }, + controller.signal, + ); + const canceled = createRequestCanceledError(); + const rejected = assert.rejects(renewal, (error) => error === canceled); + controller.abort(canceled); + await rejected; + + assert.deepEqual(registry.listHumanControlHolds(authority), [original]); + assert.throws(() => registry.assertHumanControlAdmission(lease), isHumanControlError); + await assert.rejects( + registry.runDeviceMutation(lease, async () => 'unwanted mutation'), + isHumanControlError, + ); + now = 10_000; + assert.deepEqual(registry.listHumanControlHolds(authority), []); + assert.doesNotThrow(() => registry.assertHumanControlAdmission(lease)); +}); diff --git a/packages/daemon/src/resources/lease-registry.ts b/packages/daemon/src/resources/lease-registry.ts index db63aa2af4..535e17be2d 100644 --- a/packages/daemon/src/resources/lease-registry.ts +++ b/packages/daemon/src/resources/lease-registry.ts @@ -392,14 +392,16 @@ export class LeaseRegistry { }, }; const holds = this.holdsByDevice.get(key) ?? new Map(); + const current = existing?.hold.state === 'active' ? existing : pending; this.holdsByDevice.set(key, holds); - holds.set(id, pending); - return await this.activateHumanControlHold(key, pending, input.ttlMs, signal); + holds.set(id, current); + return await this.activateHumanControlHold(key, pending, current, input.ttlMs, signal); } private async activateHumanControlHold( key: string, pending: OwnedHumanControlHold, + current: OwnedHumanControlHold, ttlMs: number | undefined, signal?: AbortSignal, ): Promise { @@ -408,7 +410,7 @@ export class LeaseRegistry { try { await this.mutations.wait(key, signal); signal?.throwIfAborted(); - if (holds.get(id) !== pending) { + if (holds.get(id) !== current) { throw new AppError( 'COMMAND_FAILED', 'Human-control hold changed before activation completed.', @@ -422,6 +424,7 @@ export class LeaseRegistry { updatedAt: activatedAt, ...(ttlMs === undefined ? {} : { expiresAt: activatedAt + ttlMs }), }; + holds.set(id, pending); this.refreshHeldLease(key, activatedAt); return cloneHumanControlHold(pending.hold); } catch (error) {