Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a4efad5
refactor: resolve session artifact paths without a store
thymikee Oct 3, 2026
3b309f0
fix: bind replay directory cleanup to joined daemon startups
thymikee Oct 2, 2026
a0ea35d
fix: preserve repair outcomes through partial replay retirement
thymikee Oct 2, 2026
ecb5472
test: simplify paired repair cleanup assertions
thymikee Oct 2, 2026
2a359db
fix: keep daemon lifecycle outside the CLI import closure
thymikee Oct 3, 2026
3e5be9a
fix(daemon): retire only the captured failed startup
thymikee Oct 2, 2026
5ec91d7
fix: join contending daemon startups within one deadline
thymikee Oct 3, 2026
604939b
fix: observe only the current daemon registration owner
thymikee Oct 3, 2026
d2c7a19
test: bound held-claim startup deadline simulation
thymikee Oct 3, 2026
3f2199b
fix: await owned daemon retirement on request timeout
thymikee Oct 3, 2026
50eafe5
chore(gates): acknowledge unchanged health payload under probe deadlines
thymikee Oct 3, 2026
740111b
fix: share manual daemon retirement and verify lock cutover
thymikee Oct 3, 2026
12781ae
test: bound legacy contender admission observation
thymikee Oct 3, 2026
464176d
fix: preserve daemon ownership across startup and cleanup
thymikee Oct 3, 2026
e4a21ee
chore(gates): expose native PID validation and discover cleanup controls
thymikee Oct 3, 2026
4ef85e9
refactor: bind session references to stable lifetime entries
thymikee Oct 3, 2026
530c6e9
fix: bind capture adoption to session lifetimes
thymikee Oct 3, 2026
bfffb65
chore(gates): declare capture adoption field owners
thymikee Oct 3, 2026
ebecbda
fix: name the capture clear outcome inline so session bindings emit d…
thymikee Oct 3, 2026
cbf0a6e
perf: load app-log cleanup on demand in session teardown
thymikee Oct 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions docs/adr/0030-process-lock-exclusion.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,32 @@ guard cannot constrain legacy code after its final check. The support boundary t
requires deployment control; host-kit does not claim to detect or evict every legacy user.

Daemon registration has a separate cutover boundary: keep the same `daemon.lock` path and
refuse legacy files rather than automatically reclaiming them. Its startup tests must cover
an already-running older daemon and concurrent old/new startup. This does not expand the
host-kit mixed-protocol support contract.
refuse legacy files rather than automatically reclaiming them. A legacy daemon creates an
exclusive file; a hardened daemon creates a directory at that same path. The old acquisition
cannot unlink a directory, and the new acquisition retains an existing file.

The [registration tests](../../src/__tests__/daemon-registration-owner.test.ts) exercise real
children using the c237027737 legacy acquisition and the current owner. They cover an older
daemon already running, a hardened owner waiting to publish metadata, and concurrent startup.
The client refuses an older registration before signaling or changing it. This proves the daemon
cutover; it does not expand the host-kit mixed-protocol support contract. Older clients still
require the deployment controls above.

## Registration operations

[Shared retirement](../../src/daemon-registration-owner.ts) owns verified termination, protected
metadata inspection and removal, and release. Takeover, failed startup, replay cleanup, timeout
reset and manual stop await its result. Abandoned recovery uses the same protected retirement
sequence without signaling a live process. Daemon publication and shutdown use functions bound
to their acquired claim.

```mermaid
flowchart LR
C[Client lifecycle and timeout] --> R[Shared retirement]
M[Manual stop] --> R
P[Abandoned recovery and pruning] --> R
R --> L[Acquired registration claim]
D[Daemon startup and shutdown] --> O[Functions bound to own claim]
O --> L
L --> F[Protected metadata and reports]
```
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { makeCaptureSessionBinding } from '../../durable-capture/session-binding.fixtures.ts';
import fs from 'node:fs/promises';
import path from 'node:path';
import { expect, test, vi } from 'vitest';
Expand Down Expand Up @@ -44,6 +45,10 @@ test('audio-probe disposes on a failed finish because terminating the helper is
);
const session: DurableCaptureSessionState = {};
sessionStore.set(sessionName, session);
const binding = makeCaptureSessionBinding(sessionStore, sessionName, {
read: (session) => session.audioProbe,
replace: (session, audioProbe) => ({ ...session, audioProbe }),
});
const statusPath = path.join(sessionStore.resolveSessionDir(sessionName), 'audio-probe.json');
const terminate = vi.fn(async () => {});
let resolveExit!: (result: HostCommandResult) => void;
Expand Down Expand Up @@ -85,9 +90,7 @@ test('audio-probe disposes on a failed finish because terminating the helper is
});
await adoptStartedAudioProbe({
admissionLedger: createAudioProbeAdmissionLedger(),
session,
sessionName,
sessionStore,
binding,
device,
owner: localRuntimeOwner('apple'),
fence,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { makeCaptureSessionBinding } from '../../durable-capture/session-binding.fixtures.ts';
import { vi } from 'vitest';
import type { AppLogCompletion, AppLogLiveHandle } from '@agent-device/contracts/app-log-runtime';
import type { CleanupOutcome, FinishOutcome } from '@agent-device/contracts/durable-resource';
Expand Down Expand Up @@ -72,6 +73,10 @@ export function makeDurableCaptureContext(
const session: TestCaptureSession = {};
sessionStore.set(sessionName, session);
return {
binding: makeCaptureSessionBinding(sessionStore, sessionName, {
read: (session) => session.appLog,
replace: (session, appLog) => ({ ...session, appLog, appLogFailure: undefined }),
}),
admissionLedger: createDurableCaptureAdmissionLedger({ displayName: 'test capture' }),
session,
sessionName,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { makeCaptureSessionBinding } from '../../durable-capture/session-binding.fixtures.ts';
import path from 'node:path';
import { expect, test, vi } from 'vitest';
import { createDurableResourceEnvelope } from '../../durable-resource-envelope.ts';
Expand Down Expand Up @@ -217,9 +218,14 @@ function makeContext(resource: ReturnType<typeof createScreenRecordingTestResour
const sessionName = 'recording';
const session: DurableCaptureSessionState = {};
sessionStore.set(sessionName, session);
const binding = makeCaptureSessionBinding(sessionStore, sessionName, {
read: (session) => session.screenRecording,
replace: (session, screenRecording) => ({ ...session, screenRecording }),
});
return {
admissionLedger: createDurableCaptureAdmissionLedger({ displayName: 'screen recording' }),
session,
binding,
sessionName,
sessionStore,
device,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { makeCaptureSessionBinding } from '../../durable-capture/session-binding.fixtures.ts';
import { expect, test, vi } from 'vitest';
import { PendingTransferGuard } from '@agent-device/contracts/async-lifecycle';
import { localRuntimeOwner } from '@agent-device/contracts/platform-runtime';
Expand Down Expand Up @@ -35,6 +36,10 @@ test('screen recording persists durable truth before adopting only handle and en
const sessionName = 'recording';
const session: TestRecordingSession = { name: sessionName, device };
sessionStore.set(sessionName, session);
const binding = makeCaptureSessionBinding(sessionStore, sessionName, {
read: (session) => session.screenRecording,
replace: (session, screenRecording) => ({ ...session, screenRecording }),
});
const owner = localRuntimeOwner('android');
const fence = { token: 'recording-fence', generation: 1 } as const;
const finish = vi.fn(async () => ({
Expand Down Expand Up @@ -79,9 +84,7 @@ test('screen recording persists durable truth before adopting only handle and en

await adoptStartedScreenRecording({
admissionLedger: createScreenRecordingAdmissionLedger(),
session,
sessionName,
sessionStore,
binding,
device: session.device,
owner,
fence,
Expand All @@ -103,7 +106,10 @@ test('screen recording persists durable truth before adopting only handle and en
if (!active) throw new Error('Expected screen-recording session');
await expect(
finishLiveScreenRecording({ intent: 'capture', session: active, sessionName, sessionStore }),
).resolves.toMatchObject({ backend: 'android', outPath: '/tmp/recording.mp4' });
).resolves.toMatchObject({
backend: 'android',
outPath: '/tmp/recording.mp4',
});
expect(finish).toHaveBeenCalledOnce();
expect(sessionStore.get(sessionName)?.screenRecording).toBeUndefined();
});
Expand All @@ -115,6 +121,10 @@ test('a failed recording finish keeps the record open and never disposes the rec
const sessionName = 'recording';
const session: TestRecordingSession = { name: sessionName, device };
sessionStore.set(sessionName, session);
const binding = makeCaptureSessionBinding(sessionStore, sessionName, {
read: (session) => session.screenRecording,
replace: (session, screenRecording) => ({ ...session, screenRecording }),
});
const owner = localRuntimeOwner('android');
const fence = { token: 'recording-fence', generation: 1 } as const;
const finishError = new Error('failed to retrieve playable Android recording');
Expand Down Expand Up @@ -150,9 +160,7 @@ test('a failed recording finish keeps the record open and never disposes the rec
});
await adoptStartedScreenRecording({
admissionLedger: createScreenRecordingAdmissionLedger(),
session,
sessionName,
sessionStore,
binding,
device: session.device,
owner,
fence,
Expand Down Expand Up @@ -182,6 +190,10 @@ test('a record stop that fails after collecting resumes through the fence withou
const sessionName = 'recording';
const session: TestRecordingSession = { name: sessionName, device };
sessionStore.set(sessionName, session);
const binding = makeCaptureSessionBinding(sessionStore, sessionName, {
read: (session) => session.screenRecording,
replace: (session, screenRecording) => ({ ...session, screenRecording }),
});
const owner = localRuntimeOwner('android');
const fence = { token: 'recording-fence', generation: 1 } as const;
const signals = vi.fn(async () => ({ observation: { recorder: 'confirmed' as const } }));
Expand Down Expand Up @@ -216,9 +228,7 @@ test('a record stop that fails after collecting resumes through the fence withou
);
await adoptStartedScreenRecording({
admissionLedger: createScreenRecordingAdmissionLedger(),
session,
sessionName,
sessionStore,
binding,
device: session.device,
owner,
fence,
Expand All @@ -239,7 +249,7 @@ test('a record stop that fails after collecting resumes through the fence withou
if (!active) throw new Error('Expected screen-recording session');
return finishLiveScreenRecording({
intent: 'capture',
session: active,
session: sessionStore.get(sessionName) ?? session,
Comment thread
thymikee marked this conversation as resolved.
sessionName,
sessionStore,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import path from 'node:path';
import { safeSessionName } from '@agent-device/host-kit/session-paths';
import type { DurableCaptureSessionStore } from '../../durable-capture/index.ts';
import { mkdtempForTestSync } from '../../tmp-dir.fixtures.ts';

export type CaptureAdmissionSessionStore<S> = DurableCaptureSessionStore<S> &
Readonly<{
get(name: string): S | undefined;
sessionsDir: string;
}>;
export type CaptureAdmissionSessionStore<S> = Readonly<{
Comment thread
thymikee marked this conversation as resolved.
set(name: string, session: S): void;
resolveSessionDir(name: string): string;
get(name: string): S | undefined;
sessionsDir: string;
}>;

/**
* The whole of the daemon `SessionStore` these admission modules ever address — `set`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import type {
RuntimeOwnerRef,
} from '@agent-device/contracts/platform-runtime';
import type { DeviceInfo } from '@agent-device/kernel/device';
import type { DurableCaptureSessionStore } from '../durable-capture/index.ts';
import type {
DurableCaptureSessionBinding,
DurableCaptureSessionStore,
} from '../durable-capture/index.ts';
import { createDurableCaptureResource } from './durable-capture-resource.ts';
import type { DurableCaptureFinishIntent } from './durable-capture-resource.ts';
import type { AudioProbeAdmissionLedger } from './audio-probe-admission-ledger.ts';
Expand Down Expand Up @@ -48,9 +51,7 @@ export const audioProbeDurableResource = createDurableCaptureResource<

export function adoptStartedAudioProbe(params: {
admissionLedger: AudioProbeAdmissionLedger;
session: DurableCaptureSessionState;
sessionName: string;
sessionStore: DurableCaptureSessionStore<DurableCaptureSessionState>;
binding: DurableCaptureSessionBinding<'audio-probe', AudioProbeLiveHandle>;
device: DeviceInfo;
owner: RuntimeOwnerRef;
fence: ResourceOwnershipFence;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import type { DurableSessionResourceKind } from './durable-session-resource-kind

export type { DurableCaptureFinishIntent, DurableSessionResourceKind };

type AdoptStartedSessionCaptureParams<K extends string, H extends AsyncDisposable, S> = Omit<
AdoptStartedDurableCaptureParams<K, H, S>,
type AdoptStartedSessionCaptureParams<K extends string, H extends AsyncDisposable> = Omit<
AdoptStartedDurableCaptureParams<K, H>,
'reportUndurableCleanup'
> &
Readonly<{ admissionLedger: DurableCaptureAdmissionLedger }>;
Expand All @@ -48,7 +48,7 @@ export function createDurableCaptureResource<
S,
>(definition: DurableCaptureResourceDefinition<K, H, C, S>) {
const sessionResourcePath = (
sessionStore: DurableCaptureSessionStore<S>,
sessionStore: Readonly<{ resolveSessionDir(name: string): string }>,
sessionName: string,
): string => definition.store.resolvePath(sessionStore.resolveSessionDir(sessionName));
const recoveryParams = (
Expand All @@ -70,7 +70,7 @@ export function createDurableCaptureResource<
}): ResourceOwnershipFence {
return createNextDurableCaptureFence(definition, params);
},
adoptStarted(params: AdoptStartedSessionCaptureParams<K, H, S>): Promise<void> {
adoptStarted(params: AdoptStartedSessionCaptureParams<K, H>): Promise<void> {
return adoptStartedDurableCapture(
definition,
{
Expand All @@ -80,7 +80,7 @@ export function createDurableCaptureResource<
else params.admissionLedger.blockUndurableCleanup(device, outcome.reason);
},
},
sessionResourcePath(params.sessionStore, params.sessionName),
definition.store.resolvePath(params.binding.sessionDir),
);
},
finishLive(params: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import type {
RuntimeOwnerRef,
} from '@agent-device/contracts/platform-runtime';
import type { DeviceInfo } from '@agent-device/kernel/device';
import type { DurableCaptureSessionStore } from '../durable-capture/index.ts';
import type {
DurableCaptureSessionBinding,
DurableCaptureSessionStore,
} from '../durable-capture/index.ts';
import { createDurableCaptureResource } from './durable-capture-resource.ts';
import type { DurableCaptureFinishIntent } from './durable-capture-resource.ts';
import type { PerfCaptureAdmissionLedger } from './perf-capture-admission-ledger.ts';
Expand Down Expand Up @@ -46,9 +49,7 @@ export const perfCaptureDurableResource = createDurableCaptureResource<

export function adoptStartedPerfCapture(params: {
admissionLedger: PerfCaptureAdmissionLedger;
session: DurableCaptureSessionState;
sessionName: string;
sessionStore: DurableCaptureSessionStore<DurableCaptureSessionState>;
binding: DurableCaptureSessionBinding<'perf-capture', PerfNativeCaptureLiveHandle>;
device: DeviceInfo;
owner: RuntimeOwnerRef;
fence: ResourceOwnershipFence;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { StopObservation } from '@agent-device/contracts/recording-stop-obs
import type { DeviceInfo } from '@agent-device/kernel/device';
import type {
DurableCaptureRecoveryControl,
DurableCaptureSessionBinding,
DurableCaptureSessionStore,
} from '../durable-capture/index.ts';
import { createDurableCaptureResource } from './durable-capture-resource.ts';
Expand Down Expand Up @@ -50,9 +51,7 @@ export const screenRecordingDurableResource = createDurableCaptureResource<

export function adoptStartedScreenRecording(params: {
admissionLedger: ScreenRecordingAdmissionLedger;
session: DurableCaptureSessionState;
sessionName: string;
sessionStore: DurableCaptureSessionStore<DurableCaptureSessionState>;
binding: DurableCaptureSessionBinding<'screen-recording', ScreenRecordingLiveHandle>;
device: DeviceInfo;
owner: RuntimeOwnerRef;
fence: ResourceOwnershipFence;
Expand Down
Loading
Loading