Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,7 @@ test('audio-probe disposes on a failed finish because terminating the helper is
await expect(
finishLiveAudioProbe({
intent: 'capture',
session: sessionStore.get(sessionName) ?? session,
sessionName,
sessionStore,
binding,
}),
).rejects.toThrow('helper exited before completing the capture');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,10 @@ export const testCaptureStore = createDurableCaptureResourceStore({
export function createTestCaptureResource(
store: DurableCaptureResourceStore<'app-log'> = testCaptureStore,
) {
return createDurableCaptureResource<
'app-log',
AppLogLiveHandle,
AppLogCompletion,
TestCaptureSession
>({
return createDurableCaptureResource<'app-log', AppLogLiveHandle, AppLogCompletion>({
resourceKind: 'app-log',
displayName: 'test capture',
store,
sessionSlot: {
read: (session) => session.appLog,
replace: (session, appLog) => ({ ...session, appLog, appLogFailure: undefined }),
},
completionMetadata: (completion) => ({
outputPath: completion.outputPath,
completedAt: completion.completedAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ test('one coordinator exposes the typed manifest and all lifecycle entrypoints',
await expect(
testCaptureResource.finishLive({
intent: 'capture',
session: active,
sessionName: context.sessionName,
sessionStore: context.sessionStore,
binding: context.binding,
}),
).resolves.toMatchObject({ outputPath: '/tmp/app.log', completedAt: 2 });
expect(context.sessionStore.get(context.sessionName)?.appLog).toBeUndefined();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,11 @@ function createScreenRecordingTestResource(
return createDurableCaptureResource<
'screen-recording',
ScreenRecordingLiveHandle,
ScreenRecordingCompletion,
DurableCaptureSessionState
ScreenRecordingCompletion
>({
resourceKind: 'screen-recording',
displayName: 'screen recording',
store,
sessionSlot: {
read: (session) => session.screenRecording,
replace: (session, screenRecording) => ({ ...session, screenRecording }),
},
completionMetadata: (completion) => ({
backend: completion.backend,
outputPath: completion.outPath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,7 @@ test('screen recording persists durable truth before adopting only handle and en

const active = sessionStore.get(sessionName);
if (!active) throw new Error('Expected screen-recording session');
await expect(
finishLiveScreenRecording({ intent: 'capture', session: active, sessionName, sessionStore }),
).resolves.toMatchObject({
await expect(finishLiveScreenRecording({ intent: 'capture', binding })).resolves.toMatchObject({
backend: 'android',
outPath: '/tmp/recording.mp4',
});
Expand Down Expand Up @@ -171,9 +169,7 @@ test('a failed recording finish keeps the record open and never disposes the rec

const active = sessionStore.get(sessionName);
if (!active) throw new Error('Expected screen-recording session');
await expect(
finishLiveScreenRecording({ intent: 'capture', session: active, sessionName, sessionStore }),
).rejects.toBe(finishError);
await expect(finishLiveScreenRecording({ intent: 'capture', binding })).rejects.toBe(finishError);

expect(forceCleanup).not.toHaveBeenCalled();
expect(sessionStore.get(sessionName)?.screenRecording?.handle).toBe(handle);
Expand Down Expand Up @@ -249,9 +245,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: sessionStore.get(sessionName) ?? session,
sessionName,
sessionStore,
binding,
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,20 @@ import type {
RuntimeOwnerRef,
} from '@agent-device/contracts/platform-runtime';
import type { DeviceInfo } from '@agent-device/kernel/device';
import type {
DurableCaptureSessionBinding,
DurableCaptureSessionStore,
} from '../durable-capture/index.ts';
import type { DurableCaptureSessionBinding } 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';
import { audioProbeResourceStore } from './audio-probe-resource-store.ts';
import type { DurableCaptureSessionState } from './session-state-slice.ts';

export const audioProbeDurableResource = createDurableCaptureResource<
'audio-probe',
AudioProbeLiveHandle,
AudioProbeCompletion,
DurableCaptureSessionState
AudioProbeCompletion
>({
resourceKind: 'audio-probe',
displayName: 'audio probe',
store: audioProbeResourceStore,
sessionSlot: {
read: (session) => session.audioProbe,
replace: (session, audioProbe) => ({ ...session, audioProbe }),
},
completionMetadata: (completion) => ({
backend: completion.backend ?? 'unknown',
source: completion.source,
Expand Down Expand Up @@ -63,9 +54,7 @@ export function adoptStartedAudioProbe(params: {
}

export function finishLiveAudioProbe(params: {
session: DurableCaptureSessionState;
sessionName: string;
sessionStore: DurableCaptureSessionStore<DurableCaptureSessionState>;
binding: DurableCaptureSessionBinding<'audio-probe', AudioProbeLiveHandle>;
intent: DurableCaptureFinishIntent;
}): Promise<AudioProbeCompletion> {
return audioProbeDurableResource.finishLive(params);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {
type AdoptStartedDurableCaptureParams,
type DurableCaptureFinishIntent,
type DurableCaptureRecoveryParams,
type DurableCaptureResourceDefinition,
type DurableCaptureSessionStore,
type DurableCaptureRecordDefinition,
type DurableCaptureSessionBinding,
type FinishRecoveredDurableCaptureParams,
} from '../durable-capture/index.ts';
import type { LiveResourceHandle } from '@agent-device/contracts/durable-resource';
Expand All @@ -34,19 +34,12 @@ type SessionCaptureRecoveryParams<K extends string, H extends LiveResourceHandle
'definition' | 'resolveSessionDir'
>;

/**
* Where the shared durable-capture mechanics meet the two authorities that stay with the session
* owner: the admission ledger, which decides whether a failed adoption blocks a replacement start,
* and the session store, whose naming rule turns a session id into the one directory its records
* may occupy. The session record itself stays opaque behind `S`; only the definition's own
* `sessionSlot` looks inside it.
*/
/** The session binding owns its slot and path; the ledger owns failed-adoption admission. */
export function createDurableCaptureResource<
K extends DurableSessionResourceKind,
H extends LiveResourceHandle<C>,
C,
S,
>(definition: DurableCaptureResourceDefinition<K, H, C, S>) {
>(definition: DurableCaptureRecordDefinition<K, C>) {
const sessionResourcePath = (
sessionStore: Readonly<{ resolveSessionDir(name: string): string }>,
sessionName: string,
Expand Down Expand Up @@ -84,27 +77,23 @@ export function createDurableCaptureResource<
);
},
finishLive(params: {
session: S;
sessionName: string;
sessionStore: DurableCaptureSessionStore<S>;
binding: DurableCaptureSessionBinding<K, H>;
intent: DurableCaptureFinishIntent;
}): Promise<C> {
return finishLiveDurableCapture(
definition,
params,
sessionResourcePath(params.sessionStore, params.sessionName),
definition.store.resolvePath(params.binding.sessionDir),
);
},
finishRecovered(params: FinishRecoveredDurableCaptureParams<K, H, C>): Promise<C> {
return finishRecoveredDurableCapture(definition, params);
},
forceCleanupLive(params: {
session: S;
sessionName?: string;
sessionStore?: DurableCaptureSessionStore<S>;
resourcePath: string;
}): Promise<void> {
return forceCleanupLiveDurableCapture(definition, params);
forceCleanupLive(params: { binding: DurableCaptureSessionBinding<K, H> }): Promise<void> {
return forceCleanupLiveDurableCapture(definition, {
...params,
resourcePath: definition.store.resolvePath(params.binding.sessionDir),
});
},
recoverAll(params: SessionCaptureRecoveryParams<K, H, C>) {
return recoverDurableCaptureResourcesAfterDaemonLock(recoveryParams(params));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,20 @@ import type {
RuntimeOwnerRef,
} from '@agent-device/contracts/platform-runtime';
import type { DeviceInfo } from '@agent-device/kernel/device';
import type {
DurableCaptureSessionBinding,
DurableCaptureSessionStore,
} from '../durable-capture/index.ts';
import type { DurableCaptureSessionBinding } 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';
import { perfCaptureResourceStore } from './perf-capture-resource-store.ts';
import type { DurableCaptureSessionState } from './session-state-slice.ts';

export const perfCaptureDurableResource = createDurableCaptureResource<
'perf-capture',
PerfNativeCaptureLiveHandle,
PerfNativeCaptureCompletion,
DurableCaptureSessionState
PerfNativeCaptureCompletion
>({
resourceKind: 'perf-capture',
displayName: 'perf capture',
store: perfCaptureResourceStore,
sessionSlot: {
read: (session) => session.perfCapture,
replace: (session, perfCapture) => ({ ...session, perfCapture }),
},
completionMetadata: (completion) => ({
kind: typeof completion.kind === 'string' ? completion.kind : 'unknown',
mode: typeof completion.mode === 'string' ? completion.mode : 'unknown',
Expand Down Expand Up @@ -61,9 +52,7 @@ export function adoptStartedPerfCapture(params: {
}

export function finishLivePerfCapture(params: {
session: DurableCaptureSessionState;
sessionName: string;
sessionStore: DurableCaptureSessionStore<DurableCaptureSessionState>;
binding: DurableCaptureSessionBinding<'perf-capture', PerfNativeCaptureLiveHandle>;
intent: DurableCaptureFinishIntent;
}): Promise<PerfNativeCaptureCompletion> {
return perfCaptureDurableResource.finishLive(params);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,20 @@ 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';
import type { DurableCaptureFinishIntent } from './durable-capture-resource.ts';
import type { ScreenRecordingAdmissionLedger } from './screen-recording-admission-ledger.ts';
import { screenRecordingResourceStore } from './screen-recording-resource-store.ts';
import type { DurableCaptureSessionState } from './session-state-slice.ts';

export const screenRecordingDurableResource = createDurableCaptureResource<
'screen-recording',
ScreenRecordingLiveHandle,
ScreenRecordingCompletion,
DurableCaptureSessionState
ScreenRecordingCompletion
>({
resourceKind: 'screen-recording',
displayName: 'screen recording',
store: screenRecordingResourceStore,
sessionSlot: {
read: (session) => session.screenRecording,
replace: (session, screenRecording) => ({ ...session, screenRecording }),
},
completionMetadata: encodeScreenRecordingCompletionMetadata,
// ADR 0024 rule 6: the next stop re-collects the native artifact a failed export left behind, and
// forced cleanup would delete exactly that. Disposal belongs to teardown and start rollback.
Expand All @@ -63,9 +56,7 @@ export function adoptStartedScreenRecording(params: {
}

export function finishLiveScreenRecording(params: {
session: DurableCaptureSessionState;
sessionName: string;
sessionStore: DurableCaptureSessionStore<DurableCaptureSessionState>;
binding: DurableCaptureSessionBinding<'screen-recording', ScreenRecordingLiveHandle>;
intent: DurableCaptureFinishIntent;
}): Promise<ScreenRecordingCompletion> {
return screenRecordingDurableResource.finishLive(params);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,11 @@ import { deviceIdentity, sameDeviceIdentity, type DeviceInfo } from '@agent-devi
import { AppError } from '@agent-device/kernel/errors';
import { isRecord } from '@agent-device/kernel/record';
import { emitDiagnostic } from '@agent-device/host-kit/diagnostics';
import type {
DurableCaptureResourceRecord,
DurableCaptureSessionStore,
} from '../durable-capture/index.ts';
import type { DurableCaptureResourceRecord } from '../durable-capture/index.ts';
import {
SCREEN_RECORDING_COMPLETION_METADATA_KEY,
screenRecordingDurableResource,
} from './screen-recording-session-resource.ts';
import type { DurableCaptureSessionState } from './session-state-slice.ts';

/**
* What a `record stop` owes a session, decided from the durable recording manifest alone.
Expand All @@ -45,7 +41,7 @@ const OPTIONAL_RESPONSE_FIELDS = [

type ScreenRecordingManifestParams = Readonly<{
sessionName: string;
sessionStore: DurableCaptureSessionStore<DurableCaptureSessionState>;
sessionStore: Readonly<{ resolveSessionDir(name: string): string }>;
}>;

export function resolveScreenRecordingStopRecovery(
Expand Down
4 changes: 1 addition & 3 deletions packages/capture-kit/src/durable-capture/adoption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
import type {
AdoptStartedDurableCaptureParams,
DurableCaptureRecordDefinition,
DurableCaptureResourceDefinition,
} from './definition.ts';
import { capitalizeDurableCaptureLabel, durableCaptureDiagnosticPrefix } from './labels.ts';

Expand All @@ -28,9 +27,8 @@ export async function adoptStartedDurableCapture<
K extends string,
H extends LiveResourceHandle<C>,
C,
S,
>(
definition: DurableCaptureResourceDefinition<K, H, C, S>,
definition: DurableCaptureRecordDefinition<K, C>,
params: AdoptStartedDurableCaptureParams<K, H>,
resourcePath: string,
): Promise<void> {
Expand Down
19 changes: 0 additions & 19 deletions packages/capture-kit/src/durable-capture/definition.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { JsonObject } from '@agent-device/contracts/client';
import type { DurableResourceEnvelope } from '@agent-device/contracts/durable-resource-envelope';
import type { LiveResourceHandle } from '@agent-device/contracts/durable-resource';
import type { PendingTransferGuard } from '@agent-device/contracts/async-lifecycle';
import type {
ResourceOwnershipFence,
Expand All @@ -24,16 +23,6 @@ export type DurableCaptureSessionBinding<K extends string, H extends AsyncDispos
clear(expected: DurableCaptureSessionResource<K, H>): 'cleared' | 'retired' | 'resource-changed';
}>;

export type DurableCaptureSessionStore<S> = Readonly<{
set(name: string, session: S): void;
resolveSessionDir(name: string): string;
}>;

export type DurableCaptureSessionSlot<K extends string, H extends AsyncDisposable, S> = Readonly<{
read(session: S): DurableCaptureSessionResource<K, H> | undefined;
replace(session: S, resource: DurableCaptureSessionResource<K, H> | undefined): S;
}>;

/**
* What a failed capture finish may do to a kind's material (ADR 0024 rule 6). A kind declares here
* whether the shared coordinator's failed capture finish still earns a forced cleanup; a disposal
Expand Down Expand Up @@ -75,14 +64,6 @@ export type DurableCaptureRecordDefinition<K extends string, C> = Readonly<{
}>;
}>;

export type DurableCaptureResourceDefinition<
K extends string,
H extends LiveResourceHandle<C>,
C,
S,
> = DurableCaptureRecordDefinition<K, C> &
Readonly<{ sessionSlot: DurableCaptureSessionSlot<K, H, S> }>;

/**
* What the mechanics observed about a failed adoption's cleanup. Reporting it keeps the
* admission decision — block a replacement start, or clear an earlier block — with the caller.
Expand Down
Loading
Loading